加入收藏 | 设为首页 | 会员中心 | 我要投稿 拼字网 - 核心网 (https://www.hexinwang.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 站长学院 > MySql教程 > 正文

如履薄冰:Redis懒惰删除的巨大牺牲

发布时间:2019-01-10 18:07:46 所属栏目:MySql教程 来源:老钱
导读:大家都知道 Redis 是单线程的,但是 Redis 4.0 增加了懒惰删除功能,懒惰删除需要使用异步线程对已删除的节点进行内存回收,这意味着 Redis 底层其实并不是单线程,它内部还有几个额外的鲜为人知的辅助线程。 这几个辅助线程在 Redis 内部有一个特别的名称
副标题[/!--empirenews.page--]

如履薄冰:Redis懒惰删除的巨大牺牲

大家都知道 Redis 是单线程的,但是 Redis 4.0 增加了懒惰删除功能,懒惰删除需要使用异步线程对已删除的节点进行内存回收,,这意味着 Redis 底层其实并不是单线程,它内部还有几个额外的鲜为人知的辅助线程。

这几个辅助线程在 Redis 内部有一个特别的名称,就是“BIO”,全称是 Background IO,意思是在背后默默干活的 IO 线程。

不过内存回收本身并不是什么 IO 操作,只是 CPU 的计算消耗可能会比较大而已。

01.懒惰删除的最初实现不是异步线程

Redis 大佬 Antirez 实现懒惰删除时,它并不是一开始就想到了异步线程。它最初的尝试是在主线程里,使用类似于字典渐进式搬迁的方式来实现渐进式删除回收。

比如对于一个非常大的字典来说,懒惰删除是采用类似于 scan 操作的方法,通过遍历第一维数组来逐步删除回收第二维链表的内容,等到所有链表都回收完了,再一次性回收第一维数组。这样也可以达到删除大对象时不阻塞主线程的效果。

但是说起来容易做起来却很难。渐进式回收需要仔细控制回收频率,它不能回收得太猛,这会导致 CPU 资源占用过多,也不能回收得像蜗牛那么慢,因为内存回收不及时可能导致内存消耗持续增长。

Antirez 需要采用合适的自适应算法来控制回收频率。他首先想到的是通过检测内存增长的趋势是增长“+1”还是下降“-1”,来渐进式调整回收频率系数,这样的自适应算法实现也很简单。

但是测试后发现在服务繁忙的时候,QPS 会下降到正常情况下 65% 的水平,这点非常致命。

所以 Antirez 才使用了如今的方案——异步线程。异步线程这套方案就简单多了,释放内存不用为每种数据结构适配一套渐进式释放策略,也不用搞个自适应算法来仔细控制回收频率,只是将对象从全局字典中摘掉,然后往队列里一扔,主线程就干别的去了。异步线程从队列里取出对象来,直接走正常的同步释放逻辑就可以了。

不过使用异步线程也是有代价的,主线程和异步线程之间在内存回收器(jemalloc)的使用上存在竞争。

这点竞争消耗是可以忽略不计的,因为 Redis 的主线程在内存的分配与回收上花的时间相对整体运算时间而言是极少的。

02.异步线程方案其实也相当复杂

上文笔者刚说异步线程方案很简单,为什么在这里又说它很复杂呢?因为有一点,笔者之前没有提到,这点非常可怕,严重阻碍了异步线程方案的改造,那就是 Redis 的内部对象有共享机制。

比如集合的并集操作 sunionstore 用来将多个集合合并成一个新集合。

  1. > sadd src1 value1 value2 value3  
  2. (integer) 3  
  3. > sadd src2 value3 value4 value5  
  4. (integer) 3  
  5. > sunionstore dest src1 src2  
  6. (integer) 5  
  7. > smembers dest  
  8. 1) "value2"  
  9. 2) "value3"  
  10. 3) "value1"  
  11. 4) "value4"  
  12. 5) "value5" 

我们看到新的集合包含了旧集合的所有元素。但是这里有一个我们没看到的 trick,那就是底层的字符串对象被共享了,如下图所示。

如履薄冰:Redis懒惰删除的巨大牺牲

为什么对象共享是懒惰删除的巨大障碍呢?因为懒惰删除相当于彻底砍掉某个树枝,将它扔到异步删除队列里去。

注意这里必须是彻底删除,不能藕断丝连。如果底层对象是共享的,那就做不到彻底删除。如图 2 所示的删除就不是彻底删除。

如履薄冰:Redis懒惰删除的巨大牺牲

所以 Antirez 为了支持懒惰删除,将对象共享机制彻底抛弃,它将这种对象结构称为“share-nothing”,也就是无共享设计。

但是甩掉对象共享谈何容易!这种对象共享机制散落在源代码的各个角落,牵一发而动全身,改起来犹如在布满地雷的道路上小心翼翼地行走。

不过 Antirez 还是决心改了,它将这种改动描述为“绝望而疯狂”,可见改动之大、之深、之险,前后花了好几周时间才改完。

不过这次修改的效果也是很明显的,对象的删除操作再也不会导致主线程卡顿了。

03.异步删除的实现

主线程需要将删除任务传递给异步线程,它是通过一个普通的双向链表来传递的。因为链表需要支持多线程并发操作,所以它需要有锁来保护。

执行懒惰删除时,Redis 将删除操作的相关参数封装成一个 bio_job 结构,然后追加到链表尾部。异步线程通过遍历链表摘取 job 元素来挨个执行异步任务。

  1. struct bio_job {  
  2.     time_t time;  // 时间字段暂时没有使用,应该是预留的  
  3.     void *arg1, *arg2, *arg3;  
  4. }; 

我们注意到这个 job 结构有三个参数。为什么删除对象需要三个参数呢?我们看如下代码。 

  1. /* What we free changes depending on what arguments are set:  
  2.      * arg1 -> free the object at pointer.  
  3.      * arg2 & arg3 -> free two dictionaries (a Redis DB).  
  4.      * only arg3 -> free the skiplist. */  
  5.     if (job->arg1)  
  6.         // 释放一个普通对象,string/set/zset/hash 等,用于普通对象的异步删除  
  7.         lazyfreeFreeObjectFromBioThread(job->arg1);  
  8.     else if (job->arg2 && job->arg3)  
  9.         // 释放全局 redisDb 对象的 dict 字典和 expires 字典,用于 flushdb  
  10.         lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);  
  11.     else if (job->arg3)  
  12.         // 释放 Cluster 的 slots_to_keys 对象,请参见第 5.7 节  
  13.         lazyfreeFreeSlotsMapFromBioThread(job->arg3); 

可以看到,通过组合这三个参数可以实现不同结构的释放逻辑。

(编辑:拼字网 - 核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!