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 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)); }
作者简介
张友东,阿里巴巴技术专家,主要关注分布式存储、Nosql数据库等技术领域,先后参与TFS(淘宝分布式文件系统)、Redis云数据库等项目,目前主要从事MongoDB云数据库的研发工作,致力于让开发者用上最好的MongoDB云服务。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/55286.html