在之前的线程机制中我们提到辅助线程,其中
LRU
和ASSOC扩容
就是其中比较典型的辅助功能,分别 负责 缓存回收、存储扩容
什么是 LRU
LRU,Least Recently Used
最近最少使用的一种页面置换算法。算法根据数据的历史访问记录的时间来进行淘汰数据,其核心思想是 如果最近没有被访问过,那么将来被访问的概率也比较低,所以被删除的几率就更大
另外,除了 LRU 还有另外两种常用的缓存 页面置换算法:FIFO
(先进先出,先来先服务)、LFU
(最近最少使用算法,跟 LRU 的区别为 LFU是按照访问次数进行处理 而 LRU是访问时间)
LRU的实现原理比较简单:维护一个链表,INPUT操作的时候如果对应元素在链表已经存在,则把UPDATE后将该元素放到链表顶端,如果不存在则INSERT后将元素放到链表顶端;SELECT操作的后将查询到的元素移动到链表顶端;这样就能确保不常用的数据在链表底端。
但是如果你觉得 MEMCACHED的LRU机制 也是这么单纯的话,就 too young, too simple...
MEMCACHED 的 LRU 机制
memcached 的 LRU 机制其实不止单纯的 LRU,它是由几种策略组成的一种机制:
惰性删除
memcached 一般不主动积极删除过期,当被访问的时候才根据时间判断是否过期flush_all
flush 命令专门用来清理所有数据,但是实际代码逻辑中也并不是一次清理了所有数据,一般在申请内存的时候或者查询的时候进行清理,这样保证了效率创建的时候检查
需要 set/add 的时候,需要申请一个新的 item,这个时候会检查同一个 slabs 里面的过期数据;另外一种情况,当没有内存分配给新的item,memcached 会从 LRU链表的尾部进行释放,即使还没有到 item 的过期时间LRU爬虫机制
LRU爬虫机制 实际是由多个爬虫联合组合而成的完整机制:item爬虫
、lru爬虫
、slab爬虫
item爬虫
memcached 是惰性删除机制的,但是如果有些 item 一直未被 get 呢,对应资源就只能一直被占用而无法释放,所以才有 启动单独的 辅助线程,独立进行 过期item 的清理lru爬虫
维护每个 slabclass 对应的HOT_LRU
( 热数据 ) 、WARM_LRU
( 暖数据 ) 、COLD_LRU
( 冷数据 ) 三个队列,不断的调整三个队列下的item链表,当需要申请一个新的item 的时候,如果没有内存可以分配,则从这三个队列里面进行淘汰item,所以需要维护队列数据,保证经常访问的不被淘汰,不经常访问或者过期的item优先被淘汰- 新的item会被添加至
HOT_LRU
队列头部 - 超过
HOT_LRU
队列长度阀值的,添加至COLD_LRU
队列 - 超过
WARM_LRU
队列长度阀值的,添加至COLD_LRU
队列 - 如果
COLD_LRU
队列数据被访问,则转移到WARM_LRU
队列 - 如果
HOT_LRU
队列 或者WARM_LRU
队列 数据被访问,则转移到WARM_LRU
队列头部 - 如果内存不够需要淘汰 item,则优先回收
COLD_LRU
队列的内存 - 以上三个队列都有可能 item 被 删除 或者 强制过期 而回收
- 新的item会被添加至
slab爬虫
用来维护 slabclass 的空间,举个栗子,之前我们在 存储机制部分 ( http://www.web-lovers.com/memcached-source-storage.html ) 就讲述过存储 slabclass -> chunk -> item 的三级概念,每个 slabclass区域 ( slabclass[1] = 96K, slabclass[2] = 120K … ) 存放不同大小的 item,但是如果存储的一直都是 96K 以内的 item,一直存储在 slabclass[1] 这个内存空间,那么就会一直申请 chunk (每次1M ) ,直到内存申请完毕,但是万一后续需要存储 120K 规格的 item,则会出现无法申请内存的情况,那么就不能存储 120K 规格的item,所以slab爬虫
就是用来处理这一尴尬情况的
ASSOC 扩容机制
memcached 的 assoc 扩容逻辑是在 单独的扩容线程中进行的
- main逻辑中开启 扩容线程,线程中 while 循环(默认挂起) 判断是否需要扩容
- main逻辑中新建扩容线程以后建立 1s 频率的定时器,每隔 1s 判断是否需要扩容
- 需要的情况下唤醒 挂起的扩容线程,进行扩容操作
这里面涉及 evtimer_set
定时器、pthread_cond_wait
挂起等待、pthread_cond_signal
唤醒线程 等知识点
源码解析
由于有些方法里的代码量比较大,我们这里按照 典型的代码片段进行解析,同志们可以根据文章提示的代码位置 和 代码里面的关键词 在源码中搜素,可能数据结构一些元素 看不太懂什么意思,没关系,先混个脸熟,后面看完回头再看过来就明白了
LRU 机制
惰性删除
do_item_get
item 查询函数里面有一些逻辑,当 item 查询到以后去检测是否执行过 flush 或者 时间过期
flush
memcached 在 接收到 flush 命令的时候,并不会立即删除所有,而是会设置一个全局变量,当 item 被操作的时候才进行时间对比看是否需要过期 item,比如上面 do_item_get
函数中调用的 item_is_flushed
判断
创建的时候检查
申请新的 item 的函数 do_item_alloc
逻辑中,如果 slab 没有空间,则调用 lru_pull_tail
进行 LRU 机制去查找是否有过期的 item(先 COLD_LRU 后 HOT_LRU)默认最多5次查找机会,从 LRU 列表的底端开始查找,如果一直没有找到过期的 item,则开始 “踢人” 模式,那么即使没有过期的 item 也会有危险。这部分可能会穿插部分 LRU 爬虫队列知识,可能比较吃力,不过不要紧,先看着脑子里有个印象,结合后面知识...
LRU爬虫机制
当前版本 LRU爬虫机制 item爬虫
、lru爬虫
、slab爬虫
是默认开启的,由 main主线程 初始化逻辑中进行控制
而实际中 虽然 爬虫线程都被启动,但是刚开始处于挂起状态,等待激活,启动线程的方法比较简单我们就不进行查看了,我们更多关注它们分别的主要控制逻辑:item_crawler_thread
、lru_maintainer_thread
、slab_rebalance_thread
刚才我们说起 这三个 爬虫线程实际都是处于 挂起状态,而这个激活信号就是 LRU爬虫线程函数里面处理逻辑的时候根据一些条件触发的,也可以说相当于有 LRU线程去 统一调度,所以我们先来看看 lru_maintainer_thread
LRU爬虫线程的逻辑
lru_maintainer_juggle
HOT_LRU、WARM_LRU、COLD_LRU 列表的维护函数
lru_maintainer_crawler_check
实际就相当于开始 item爬虫逻辑了,检查每个slab lass中的 lru 是否需要进行爬虫操作,如果是则将需要进行爬虫的 slab class 的 id 等信息传递给 lru_crawler_start 用来启动爬虫功能
lru_crawler_start
函数,为需要进行爬虫操作的 slab 的每个 LRU 链表 后插入用于 爬虫的item, 这个 爬虫item 的主要目的是记录当前在 LRU链表 中的位置,并检查 item 是否已经过期或者已经被 flushed,如果是则将其删除,这就是所谓的惰性删除。函数的最后是唤醒用于爬虫的线程 item_crawler_thread
item_crawler_thread
对所有需要进行爬虫的 LRU 依次进行爬虫,每次检查当前 LRU 中的一个 item,接着检查下一个LRU。但是为了防止爬虫时间过长影响正常的处理,每处理一个 item,则将相关的锁释放,源码中会每处理 crawls_persleep 个 item 就进入睡眠。当所有的 LRU 都处理完毕,则爬虫线程将会进入睡眠,等待 lru_maintainer_thread
线程将其唤醒
item 爬虫的做法比较经典
对每个需要爬虫的 LRU队列安装一个 item爬虫,item爬虫 和 item结构体 除了最后一个属性 remaining 其他都一样,所以二者可以互换,不影响原有的 item 属性值。这个是为了解决一个问题: 每次对 LRU 进行爬取的时候,由于 LRU 队列仅仅是一个链表队列,不支持随机存储,每次访问一个 item 都必须从 head 开始,一个一个访问,所以有了这个 item爬虫,下次对 对应LRU进行爬取的时候,就可以从这个 item爬虫结点 进行继续往下走
|
|
slabs_reassign
这个函数的作用是设置需要进行内存页转移的源和目的slab,并唤醒 rebalance线程 进行内存页转移操作,具体操作 函数为 do_slabs_reassign
,唤醒执行的线程函数为 slab_rebalance_thread
slab_rebalance_thread
这是 rebalance 线程,首先调用 slab_rebalance_start
确定要转移的源 slab class 中的 slab,然后调用 slab_rebalance_move
将要转移的 slab 中的 item 全部回收,最后如果回收完毕,则会调用slab_rebalance_finish
将 slab 从源 slab class 转移到目的 slab class 中
slab_rebalance_start
这个函数的功能主要的作用是选取需要转出内存页的 slab class 的内存页,源码中选取的是 slab class 中的第一个内存页,因为第一个内存页中的 items 最先分配出去,其中的 items 存在的时间一般较大,确定这个内存页的起始位置和终止位置
|
|
|
|
ASSOC 扩容机制
main 函数中建立单独的扩容线程,并新增检测定时器
start_assoc_maintenance_thread
assoc扩容线程
assoc_maintenance_thread
扩容线程处理逻辑
assoc_expand
申请hash空间
clock_handler
定时器逻辑中跟 扩容有关的 主要调用 assoc_start_expand
,进行扩容条件判断
本文作者: wettper
本文链接: http://www.web-lovers.com/memcached-source-lru-and-assoc-expand.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!