mc集群写入恍惚问题排查

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