MongoDB 3.2.9 版本在 wiredtiger 上做了很多改进,但不幸的时,这个版本引入了一个新的 bug,持续大量 insert/update 场景,有一定的可能导致 wiredtiger 进入 deadlock,MongoDB 官方迅速的在3.2.10里修复了该问题,该版本在 wiredtiger 内存使用上也做了控制,尽量避免了因为内存碎片导致 wiredtiger 内存使用远超出 cacheSizeGB 配置的问题,目前 MongoDB 3.2.10+ 的版本已经非常稳定。
MongoDB 目前有4个可配置的参数来支持 wiredtiger 存储引擎的 eviction 策略调优,其含义是:
参数 | 默认值 | 含义 |
---|---|---|
eviction_target | 80 | 当 cache used 超过 eviction_target ,后台evict线程开始淘汰 CLEAN PAGE |
eviction_trigger | 95 | 当 cache used 超过 eviction_trigger ,用户线程也开始淘汰 CLEAN PAGE |
eviction_dirty_target | 5 | 当 cache dirty 超过 eviction_dirty_target ,后台evict线程开始淘汰 DIRTY PAGE |
eviction_dirty_trigger | 20 | 当 cache dirty 超过 eviction_dirty_trigger , 用户线程也开始淘汰 DIRTY PAGE |
上述默认值是在3.2.10版本里调整的,如果你正在使用 MongoDB 3.0/3.2,遇到了 wiredtiger 相关问题(绝大部分场景遇不到),可以先升级到3.2的最新版本。
在此基础上(使用3.2.10+),如果通过 mongostat 发现 used、dirty 持续超出eviction_trigger
、eviction_dirty_trigger
,这时用户的请求线程也会去干 evict的事情(开销大),会导致请求延时上升,这时基本可以判定,mongodb 已经存在资源不足的问题,即用户读写『从磁盘上读取的数据的速率』 远远 超出了 『mongodb 将数据从内存淘汰出去速率』,可以做的优化包括:
-
增强 IO 能力
- SATA 盘升级到 SSD
- 将 wiredtiger 的数据和 journal 分到不同的盘上
-
扩充机器内存,加大 wiredtiger cache
- cache 越大,越能平衡上述2个速率的差距
-
eviction 参数调优
- 降低eviction_target 或 eviction_dirty_target,让evict 尽早将数据从 wiredtiger 的 cache 刷到操作系统的 page cache,以便提早刷盘。
db.runCommand({setParameter: 1, wiredTigerEngineRuntimeConfig: "eviction_dirty_target=5,eviction_target=80"})
--
接下来分析一下 MongoDB 3.2.9 bug 产生的原因,想了解源码的往下看
当用户请求打开wiredtiger cursor 的时候,会检查是否需要 进行 cache 淘汰,当 『cache 使用百分比超出 eviction_trigger
』 或者 『cache 脏页百分比超过 eviction_dirty_triger
』,用户请求线程就会进入到 cache 淘汰逻辑,执行__wt_cache_eviction_worker
static inline bool
__wt_eviction_needed(WT_SESSION_IMPL *session) {
bytes_inuse = __wt_cache_bytes_inuse(cache);
bytes_max = conn->cache_size + 1; // Avoid division by zero
pct_full = (u_int)((100 * bytes_inuse) / bytes_max);
if (pct_full > cache->eviction_trigger)
return true;
if (__wt_cache_dirty_inuse(cache) >
(cache->eviction_dirty_trigger * bytes_max) / 100)
return (true);
return false;
}
用户线程执行 __wt_cache_eviction_worker
会持续的检查 __wt_eviction_needed
条件是否满足,不需要 evict 时,用户线程就会继续响应请求;如果需要evict,就会从 evict queue 里取 page 进行淘汰,当 evict queue 为空时,用户线程 wait 一段时间继续重复上述逻辑。
int
__wt_cache_eviction_worker(WT_SESSION_IMPL *session, bool busy, u_int pct_full) {
for (;;) {
/* See if eviction is still needed. */
if (!__wt_eviction_needed(session, busy, &pct_full) ||
(pct_full < 100 &&
cache->pages_evict > init_evict_count + max_pages_evicted))
return (0);
/* Evict a page. */
switch (ret = __evict_page(session, false)) {
case 0:
if (busy)
return (0);
/* FALLTHROUGH */
case EBUSY:
break;
case WT_NOTFOUND:
/* Allow the queue to re-populate before retrying. */
__wt_cond_wait(
session, conn->evict_threads.wait_cond, 10000);
cache->app_waits++;
break;
}
}
后台的 evict server 线程会遍历 wiredtiger 的 btree 页,将满足条件的的 page 加入到 evict queue 并进行淘汰,每一轮都会通过 __evict_update_work
更新当前的工作状态信息,并告知调用者是否还需要继续执行 evict。
// 这个就是执行上述表格中描述的逻辑
// WT_CACHE_EVICT_CLEAN 标记代表后台线程需要淘汰 CLEAN PAGE
// WT_CACHE_EVICT_CLEAN_HARD 代表用户线程也需要去淘汰 CLEAN PAGE
// DIRTY* 参数类似
static bool
__evict_update_work(WT_SESSION_IMPL *session)
{
bytes_max = conn->cache_size + 1;
bytes_inuse = __wt_cache_bytes_inuse(cache);
if (bytes_inuse > (cache->eviction_target * bytes_max) / 100)
F_SET(cache, WT_CACHE_EVICT_CLEAN);
if (__wt_eviction_clean_needed(session, NULL))
F_SET(cache, WT_CACHE_EVICT_CLEAN_HARD);
dirty_inuse = __wt_cache_dirty_leaf_inuse(cache);
if (dirty_inuse > (cache->eviction_dirty_target * bytes_max) / 100)
F_SET(cache, WT_CACHE_EVICT_DIRTY);
if (__wt_eviction_dirty_needed(session, NULL))
F_SET(cache, WT_CACHE_EVICT_DIRTY_HARD);
return (F_ISSET(cache, WT_CACHE_EVICT_ALL | WT_CACHE_EVICT_URGENT));
}
__evict_update_work
最后通过 F_ISSET(cache, WT_CACHE_EVICT)
来判断是否要继续 evict
#define WT_CACHE_EVICT_ALL (WT_CACHE_EVICT_CLEAN | WT_CACHE_EVICT_DIRTY)
这样可能会出现一种情况,eviction_trigger
或 eviction_dirty_trigger
触发了,这时后台线程是需要继续进行 evict 的,但eviction_target、eviction_ditry_target都不满足,导致上述判断条件返回 false,后台线程不继续干活,这样就不会有新的 page 加入到 evict queue,而上述用户线程还在继续等待 evict,一直不会返回,这样就会导致请求 hang 。
修复上述问题的主要代码如下:github commit
主要修改逻辑是,当 used 超过eviction_trigger
时,同时也设置WT_CACHE_EVICT_CLEAN
标记(DIRTY 类似),这样确保有用户线程在等时,evict 一定会进行。
static bool
__evict_update_work(WT_SESSION_IMPL *session)
{
bytes_max = conn->cache_size + 1;
bytes_inuse = __wt_cache_bytes_inuse(cache);
if (bytes_inuse > (cache->eviction_target * bytes_max) / 100)
F_SET(cache, WT_CACHE_EVICT_CLEAN);
if (__wt_eviction_clean_needed(session, NULL))
- F_SET(cache, WT_CACHE_EVICT_CLEAN_HARD);
+ F_SET(cache, WT_CACHE_EVICT_CLEAN | WT_CACHE_EVICT_CLEAN_HARD);
dirty_inuse = __wt_cache_dirty_leaf_inuse(cache);
if (dirty_inuse > (cache->eviction_dirty_target * bytes_max) / 100)
F_SET(cache, WT_CACHE_EVICT_DIRTY);
if (__wt_eviction_dirty_needed(session, NULL))
- F_SET(cache, WT_CACHE_EVICT_DIRTY_HARD);
+ F_SET(cache, WT_CACHE_EVICT_DIRTY | WT_CACHE_EVICT_DIRTY_HARD);
return (F_ISSET(cache, WT_CACHE_EVICT_ALL | WT_CACHE_EVICT_URGENT));
}