MongoDB 3.2.9 请求 hang 分析及 wiredtiger 调优

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_triggereviction_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_triggereviction_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云服务。

发表评论