工作简介
哎,其实说起来,这周主要是在读代码和找原因了,关于memcached的代码在申请项目的时候看过一些大概的,也就是只熟悉他的几个经典的模型和处理流程,具体到函数的原因还是找了和分析了好久了,其实主要原因是得结合应用背景分析下实际的工作原理来着,这个确实以前做的有点少。
本周工作
系统瓶颈分析
这周分析了半天的源代码,发现问题是在
pthread_mutex_trylock这里,memcached在这里用了一个类似于自旋锁的一个设定,这个自旋锁的好处就是在被锁入的临界区的内容如果是短时间作业的话将会有良好的性能优势。
于是这一周我的一个开始的工作就是想着怎么把锁去掉,看能不能有一个高效和快捷的实现,于是我去网上找了找资料,其实上次提到的STM是一个解决的办法,但是这个思路很快的被我否定了,原因是为何能,首先从代码上看。主要费时间的do_item_alloc这个函数看看。
item *do_item_alloc(char *key, const size_t nkey, const int flags,
const rel_time_t exptime, const int nbytes,
const uint32_t cur_hv) {
uint8_t nsuffix;
item *it = NULL;
char suffix[40];
size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
if (settings.use_cas) {
ntotal += sizeof(uint64_t);
}
unsigned int id = slabs_clsid(ntotal);
if (id == 0)
return 0;
mutex_lock(&cache_lock);
/* do a quick check if we have any expired items in the tail.. */
int tries = 5;
int tried_alloc = 0;
item *search;
void *hold_lock = NULL;
rel_time_t oldest_live = settings.oldest_live;
search = tails[id];
/* We walk up *only* for locked items. Never searching for expired.
* Waste of CPU for almost all deployments */
for (; tries > 0 && search != NULL; tries--, search=search->prev) {
if (search->nbytes == 0 && search->nkey == 0 && search->it_flags == 1) {
/* We are a crawler, ignore it. */
tries++;
continue;
}
uint32_t hv = hash(ITEM_key(search), search->nkey);
/* Attempt to hash item lock the "search" item. If locked, no
* other callers can incr the refcount
*/
/* Don't accidentally grab ourselves, or bail if we can't quicklock */
if (hv == cur_hv || (hold_lock = item_trylock(hv)) == NULL)
continue;
/* Now see if the item is refcount locked */
if (refcount_incr(&search->refcount) != 2) {
refcount_decr(&search->refcount);
/* Old rare bug could cause a refcount leak. We haven't seen
* it in years, but we leave this code in to prevent failures
* just in case */
if (settings.tail_repair_time &&
search->time + settings.tail_repair_time < current_time) {
itemstats[id].tailrepairs++;
search->refcount = 1;
do_item_unlink_nolock(search, hv);
}
if (hold_lock)
item_trylock_unlock(hold_lock);
continue;
}
/* Expired or flushed */
if ((search->exptime != 0 && search->exptime < current_time)
|| (search->time <= oldest_live && oldest_live <= current_time)) {
itemstats[id].reclaimed++;
if ((search->it_flags & ITEM_FETCHED) == 0) {
itemstats[id].expired_unfetched++;
}
it = search;
slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);
do_item_unlink_nolock(it, hv);
/* Initialize the item block: */
it->slabs_clsid = 0;
} else if ((it = slabs_alloc(ntotal, id)) == NULL) {
tried_alloc = 1;
if (settings.evict_to_free == 0) {
itemstats[id].outofmemory++;
} else {
itemstats[id].evicted++;
itemstats[id].evicted_time = current_time - search->time;
if (search->exptime != 0)
itemstats[id].evicted_nonzero++;
if ((search->it_flags & ITEM_FETCHED) == 0) {
itemstats[id].evicted_unfetched++;
}
it = search;
slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);
do_item_unlink_nolock(it, hv);
/* Initialize the item block: */
it->slabs_clsid = 0;
/* If we've just evicted an item, and the automover is set to
* angry bird mode, attempt to rip memory into this slab class.
* TODO: Move valid object detection into a function, and on a
* "successful" memory pull, look behind and see if the next alloc
* would be an eviction. Then kick off the slab mover before the
* eviction happens.
*/
if (settings.slab_automove == 2)
slabs_reassign(-1, id);
}
}
refcount_decr(&search->refcount);
/* If hash values were equal, we don't grab a second lock */
if (hold_lock)
item_trylock_unlock(hold_lock);
break;
}
if (!tried_alloc && (tries == 0 || search == NULL))
it = slabs_alloc(ntotal, id);
if (it == NULL) {
itemstats[id].outofmemory++;
mutex_unlock(&cache_lock);
return NULL;
}
assert(it->slabs_clsid == 0);
assert(it != heads[id]);
/* Item initialization can happen outside of the lock; the item's already
* been removed from the slab LRU.
*/
it->refcount = 1; /* the caller will have a reference */
mutex_unlock(&cache_lock);
it->next = it->prev = it->h_next = 0;
it->slabs_clsid = id;
DEBUG_REFCNT(it, '*');
it->it_flags = settings.use_cas ? ITEM_CAS : 0;
it->nkey = nkey;
it->nbytes = nbytes;
memcpy(ITEM_key(it), key, nkey);
it->exptime = exptime;
memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
it->nsuffix = nsuffix;
return it;
}
这么一大段的源代码其实就是干了一个很简单的事情,就是插入的时候,首先从LRU表的后面查找有没有过期和删除的节点,如果没有就从slab里面分配一个。
那么貌似看起来好像是没啥问题,但是其实这个里面其实就是造成锁的效率低效的原因。首先是那个搜索,在大量的插入这个过程中,虽然大量的插入在实际的过程中比较少见,但是在数据load的时候这个量还是比较大的。尤其是lru如果大面积失效的情况下,这个情况我面后续在考虑。那考虑如果插入了大量的数据,而其中item都没有过期的话,这个函数首先会从尾部查找一遍,查找所有的LRU链表,看看博客二的测试数据,我们的数据都是长度是50的,那么按照这个分析,他肯定是在同一个slabclass里,然后之前我想看它对cache_lock上锁了,我觉得吧是不是锁的粒度太大了,试着加了个LRU_cache,但是简单的测试下貌似没有什么改变,这是为啥,我想了2天,发现当然没有效果,他就在一个slabclass里面啊,怎么可能有效果,问题的原因看着是锁的问题,但本质的问题不再锁,就是在LRU查找那里,我们可以看看do_item_get这个函数
item *do_item_get(const char *key, const size_t nkey, const uint32_t hv) {
//mutex_lock(&cache_lock);
item *it = assoc_find(key, nkey, hv);
if (it != NULL) {
refcount_incr(&it->refcount);
/* Optimization for slab reassignment. prevents popular items from
* jamming in busy wait. Can only do this here to satisfy lock order
* of item_lock, cache_lock, slabs_lock. */
if (slab_rebalance_signal &&
((void *)it >= slab_rebal.slab_start && (void *)it < slab_rebal.slab_end)) {
do_item_unlink_nolock(it, hv);
do_item_remove(it);
it = NULL;
}
}
//mutex_unlock(&cache_lock);
int was_found = 0;
if (settings.verbose > 2) {
int ii;
if (it == NULL) {
fprintf(stderr, "> NOT FOUND ");
} else {
fprintf(stderr, "> FOUND KEY ");
was_found++;
}
for (ii = 0; ii < nkey; ++ii) {
fprintf(stderr, "%c", key[ii]);
}
}
if (it != NULL) {
if (settings.oldest_live != 0 && settings.oldest_live <= current_time &&
it->time <= settings.oldest_live) {
do_item_unlink(it, hv);
do_item_remove(it);
it = NULL;
if (was_found) {
fprintf(stderr, " -nuked by flush");
}
} else if (it->exptime != 0 && it->exptime <= current_time) {
do_item_unlink(it, hv);
do_item_remove(it);
it = NULL;
if (was_found) {
fprintf(stderr, " -nuked by expire");
}
} else {
it->it_flags |= ITEM_FETCHED;
DEBUG_REFCNT(it, '+');
}
}
if (settings.verbose > 2)
fprintf(stderr, "\n");
return it;
}
简单的说他就是找到了hash表的item然后呢,然后它检查一下他是不是超时和删除的,然后返回结果,话说说好了LRU的操作呢,怎们美乐,对,他就是这个意思,这个就是memcached对get的优化,对于过期和超时间的item的检测是在get的过程中进行的,也就是说其实并没有维护这个超期的表这个过程,当然有个lru_crawler这个线程会做一些清理的工作,检测超期的过程,但是本质上如果存在lru中的元素如果都没有过期的话,反向遍历这个lru的表不是一个好的操作,应为这个操作每一次的操作是线性的是,如果有N个item的话,每次操作是O(N)的话,那么N次插入的时间就是O(N^2)的平方的,这个过程显然不是我们想要的,而且当N很大的话,O(N)并非是一个很好的解决办法,这样每个操作的作业过程就不是一个很小的时间,这样用类似spinlock方式上锁的过程显然就会造成插入的瓶颈。这才是问题的关键。
总结一下,上次造成锁的时延那么大的原因并不在锁,而是在LRU这里的实现。所以优化的工作就是LRU算法的改进。
解决思路
所以优化的关键是设计这么一种LRU算法的结构,能够支持下面几个操作
- 能够支持并发量大的操作
- 能够快速的找到要被淘汰的元素的集合
- 能够快速的定位的查找的元素
- 能够支持CAS,STM等后续可能使用的无所结构的实现
这一周看到一个叫bagLRU的算法,目前在学习和借鉴中。
下周工作
- 估计该撰写中期报告了,争取中期通过吧
- 开始一部分的代码工作,争取在8月中旬左右弄个初稿出来