1.现象
业务方反馈在向memcache集群写入数据时,出现不稳定。表现为向mc写入一个creative和ad对象的list,有的时候能写进去并读出来,有的时候写成功但是读不出来。
2.问题排查
2.1 复现问题
- a.有的key没有问题,能够一直写+读。
- b.有的key一直都是写ok,读None。
- c.有的key写ok,有的时候读ok有的时候读None.
2.2 proxy的问题?
使用同一个proxy的再次复现问题,出现了之前的多种情况。所以排除proxy问题。
另外在排查中发现出现问题的key长度小于20B、value长度在9K~10K左右。
2.3.mc集群问题?
2.3.1 集群各节点状态
集群各个节点状态,以10.2.10.10:11211为例:
============10.2.10.10:11211
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
1 96B 604774s 158 1021044 no 0 0 0
2 120B 3600s 2 10013 no 0 0 0
3 152B 186s 1 1 no 0 0 0
4 192B 6157491s 1 69 no 0 0 0
5 240B 1721785s 1 835 no 0 0 0
6 304B 1721786s 1 2673 no 0 0 0
7 384B 1721783s 1 91 no 0 0 0
8 480B 1721786s 1 6 no 0 0 0
9 600B 140686s 2 1 no 0 0 0
10 752B 125s 7 11 no 0 0 0
11 944B 121s 4 940 no 0 0 0
12 1.2K 120s 9 4666 yes 3 562 0
13 1.4K 121s 5 1447 yes 2047 495 0
14 1.8K 437s 754 1209 no 0 0 0
15 2.3K 83618s 58 261 yes 575 138922 0
16 2.8K 172787s 558 80573 no 0 0 0
17 3.5K 172780s 576 131417 yes 96835 172745 0
18 4.4K 172788s 2869 169486 no 0 0 0
19 5.5K 3576s 90 16560 yes 3357047 3577 0
20 6.9K 118334s 7 988 yes 1 72968 0
21 8.7K 82s 6 708 yes 12016644 85 88
22 10.8K 1s 2 188 yes 393841058 1 8640
23 13.6K 1s 1 75 yes 118541885 1 1153
24 16.9K 59s 1 60 yes 1262831 60 14
25 21.2K 338s 1 16 no 0 0 0
26 26.5K 144s 1 5 no 0 0 0
27 33.1K 21s 1 1 no 0 0 0
28 41.4K 5s 1 2 no 0 0 0
30 64.7K 23s 1 2 no 0 0 0
31 80.9K 0s 1 0 no 0 0 0
通过看集群各个节点的状态,发现节点的slab存在不同程度的Full.
情节较为严重的是10.2.10.8:11211,10.2.10.9:11211,10.2.10.10:11211这三个节点,并且Evicted很多。
2.3.2 再次复现问题
用client直接连接不同的节点,在有的节点上读写都ok,有的节点上出现了之前的问题。确定是集群节点出现问题。
基本确定问题和mc集群节点上5.5K~16.9K之间的slab的Full状态以及Evicted有关。
2.3.3 集群内存使用情况
内存使用情况
============10.2.10.8
limit_maxbytes 5368709120, used_bytes 1664011691
============10.2.10.9
limit_maxbytes 5368709120, used_bytes 1573404180
============10.2.10.10
limit_maxbytes 5368709120, used_bytes 1764601562
============10.2.10.11
limit_maxbytes 5368709120, used_bytes 1549783251
最大内存5GB,每个实例用了1.5GB左右。 内存没满啊,为什么存不进去?
再次返回来看各个节点的状态,以10.2.10.10:11211为例:
把分配的Page都加起来:158+2+1+1+1+1+1+1+2+7+4+9+5+754+58+558+576+2869+90+7+6+2+1+1+1+1+1+1+1+1=5121 ~ 5GB
5GB是分配给每个节点的maxmemory.
说明所有的memory page都被分配给相应的slab了,目前即使有一部分page回收后空闲,但是这部分空闲的page没有被重新分配到全局空闲空间,供其他slab使用。
看一下chunk size为1.8K的一行,分配page为754,item数量1209,也就是说这个slab里面,实际只有1MB左右的数据,却分配了754M的空间,严重浪费。
为什么mc就不能把已经分配的空闲空间回收呢?
问题定位:mc没有把已经分配的空闲空间回收。
3.问题解决
3.1 再造集群复现问题
1.自己搭了一个64M的mc节点。
2.用4k的value数据写满:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 6s 64 14720 yes 2056 2 0
3.删除所有数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 64 0 yes 0 0 0
4.再用1k的value写满:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
12 1.2K 9s 2 885 no 66222 0 0
18 4.4K 0s 63 0 yes 0 0 0
发现这次value大小为1k的很多都Evited了。并且上次value大小为4k的数据虽然已经删除了,但是page大多数还处于被分配状态。
STAT slab_reassign_running 0
STAT slabs_moved 2
在stats里面看到,slabs也出现了reassign(就是在启动参数里面指定了slabsreassign和slabsautomove),但是和我们要的差距有点大。
在1.4.11的ReleaseNote里面看到:
可以通过命令手动重新分配slot,试一下
$ echo "slabs reassign 1 4" | nc localhost 11211
写满1k数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
12 1.2K 6s 7 6195 yes 193356 1 0
18 4.4K 0s 58 0 yes 0 0 0
有4个page迁移了
$ echo "slabs reassign 1 10" | nc localhost 11211
再写一遍1k数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
12 1.2K 50s 14 12390 yes 254268 31 0
18 4.4K 0s 51 0 yes 0 0 0
STAT slab_reassign_running 0
STAT slabs_moved 13
STAT bytes 13232520
写一遍2k的数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
12 1.2K 223s 13 11505 yes 254268 31 0
15 2.3K 15s 2 902 yes 32651 4 0
18 4.4K 0s 51 0 yes 0 0 0
STAT slab_reassign_running 0
STAT slabs_moved 14
STAT bytes 14154480
能看到reassign的速度变快了。但还是和我们要的差距有点大。我们不能经常手动执行slabs reassign.
我们用的mc是1.4.13,新版本的mc是不是解决了这个问题?
于是下载最新的1.4.33,重复上面的测试。
3.3 新版本测试
下载最新版本1.4.33,重试上面的测试:
用4k的value数据写满:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 11s 64 14720 yes 2056 1 0
删除所有数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 64 0 yes 0 0 0
用1k的value写满:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
12 1.2K 8s 1 885 yes 66222 1 0
18 4.4K 0s 64 0 yes 0 0 0
貌似没什么变化啊,赶紧下载最新的代码看看在申请空间的时候怎么做的。
item *do_item_alloc(char *key, const size_t nkey, const unsigned int flags,
const rel_time_t exptime, const int nbytes);
static void *lru_maintainer_thread(void *arg);
static int lru_maintainer_juggle(const int slabs_clsid);
static int lru_pull_tail(const int orig_id, const int cur_lru, const uint64_t total_bytes, uint8_t flags);
这4个函数基本就是lru_maintainer线程回收空间的核心代码了,限于篇幅不再罗列代码。
概括一下就是:
maintainer线程处于一个while循环中,不断对所有的slabscls进行循环,看看哪些slabscls里面空闲空间的>2.5个page,就标记一下到slab_reb里面,等待回收。
并且不断对lru表维护,如果hot,warm lru占有内存超过限定额度,将hot lru的item移至warm lru, warm lru的item移至cold lru,以及对cold lru里面对象的回收等等.
slabclass_t对应三条lru队列,即hot,warm,cold lru,最终内存不足的时候会有优先删除cold lru的数据。
另外,最新的mc里面也支持一个crawler线程和maintainer线程配合。crawler线程用来检查当前memcache里面的所有item是否过期等。
3.4 新版本再测试
1.启动参数:
增加lru_maintainer参数
2.写4k数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 27s 64 14720 yes 2056 4 0
3.全部删除:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 2 0 yes 0 0 0
4.写入8k数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 2 0 yes 0 0 0
21 8.7K 27s 62 7316 yes 1071 1 0
5.删除8k数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 2 0 yes 0 0 0
21 8.7K 0s 2 0 yes 0 0 0
6.写满6k数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 2 0 yes 0 0 0
20 6.9K 7s 60 8820 yes 2363 3 0
21 8.7K 0s 2 0 yes 0 0 0
7.写入4k无过期数据,8k有过期的数据600s
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 484s 11 2000 yes 0 0 0
21 8.7K 9s 26 2999 yes 0 0 0
8.写入5k的不过期数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 2 0 yes 0 0 0
19 5.5K 5s 33 5999 yes 0 0 0
21 8.7K 0s 2 0 yes 0 0 0
9.写入5k无过期数据,8k有过期的数据600s
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 0s 2 0 yes 0 0 0
19 5.5K 57s 18 3000 yes 0 0 0
21 8.7K 8s 26 2999 yes 0 0 0
10.写入4k带过期数据:
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
18 4.4K 5s 9 1999 yes 0 0 0
19 5.5K 176s 18 3000 yes 0 0 0
21 8.7K 127s 10 1000 yes 0 0 0
删除了8k的过期数据。
加入lru_maintainer线程之后效果大好,另外,如果增加crawler线程的话会占用锁,可能会影响mc的性能(需要性能测试)
4.结论
通过上面的实验看出,1.4.33的mc在page分配完成后的回收上效果很好。
如果集群已经出现了page分配完的情况,如果使用新版的mc,一方面会缓存之前1.4.13版本写不进去的数据,提高在slab钙化情况下的空间利用率,提高mc命中率。另一方面因为将数据分别放在hot,warm,cold lru里面,能快速的找到替换的空间,大大降低查找已经过期的空间回收时间,进一步提高性能。
5.升级新版本
目前广告的所有mc都已经升级到1.4.33版本。
6.Ref
https://github.com/memcached/memcached
https://github.com/memcached/memcached/wiki/ReleaseNotes1411