# 性能优化

# 服务器状态

  • 在 shell 中,可用 mongostat、mongostop 命令查看 mongod 的状态。
  • 在 mongo 客户端中,可以用以下命令查看 mongod 的状态:
    db.serverStatus()               // 查看服务器的状态信息
    db.serverStatus().connections   // 查看客户端连接数
    
    db.currentOp()                  // 查看数据库正在执行的所有操作
    db.currentOp({op: "query", active: true}) // 只返回符合查询条件的操作
    db.killOp(<opid>)               // 终止一个操作
    

# 内存

  • mongod 占用内存的因素包括:

    • mongod 的程序文件
      • mongod 刚启动时会加载程序文件,此时只占用 100MB 内存,其中 WiredTiger Cache 几乎为 0 。等之后读写数据,才会占用更多内存。
    • collection、index 的元数据
    • 客户端连接
      • mongod 不限制客户端连接数,为每个连接创建一个最多占用 1MB 栈内存的线程。还会因为 Socket 占用内核内存。
    • WiredTiger Cache
      • mongod 需要读取某条文档时,先找到磁盘中的数据文件 collection-xx.wt ,读取包含目标数据的那个文件块,经过 Page Cache(外部缓存),然后解压并载入 WiredTiger Cache(内部缓存),最后根据 _id 在 B+ tree 结构中找到那条数据。
      • 查询一个集合时,可能读取其 index 的部分或全部内容,载入内部缓存。写入文档时,也会更新 index 。
      • 内部缓存不包括构建索引时占用的内存。在单个集合上构建索引时最多占用内存为 maxIndexBuildMemoryUsageMegabytes(默认 500MB )。
    • Page Cache
      • WiredTiger 读写磁盘文件时,不使用 MMAP 技术,而是依赖操作系统的文件缓存。默认将 50% 的主机内存用作内部缓存,剩下的 50% 用作 Page Cache ,从而加速读写磁盘。
      • Page Cache 不是 mongod 进程主动申请的内存,而是被操作系统自动分配的。减少 Page Cache 不会停止 mongod 进程,只是可能减慢读写磁盘的速度。
  • WiredTiger 会跟踪内部缓存中 clean pages 和 dirty pages 的体积。

    • 内部缓存达到 cacheSizeGB 的 eviction_target(默认 80% )时,后台的 evict 线程会开始驱逐内存中较少使用的 clean pages ,腾出内存空间。
    • 内部缓存达到 eviction_trigger(默认 95% )时,前台线程也开始驱逐 clean pages ,导致读写操作变慢。
    • 内部缓存达到 100% 时,会停止数据库的读写操作。
    • dirty pages 达到 eviction_dirty_target(默认 5% )时,后台的 evict 线程会开始驱逐 dirty pages ,导致丢失数据。
    • dirty pages 达到 eviction_dirty_trigger(默认 20% )时,前台线程也开始驱逐 dirty pages ,导致读写操作变慢。
    • 如果占用内存长时间超过驱逐阈值,则说明 mongod 的内存不足。
  • 例:查看服务器的内存开销

    > db.serverStatus().tcmalloc.tcmalloc.formattedString
    ------------------------------------------------
    MALLOC:    29943407472 (28556.3 MiB) Bytes in use by application        // mongod 进程正在使用的内存,用于存放文档、索引等数据
    MALLOC: +   6028980224 ( 5749.7 MiB) Bytes in page heap freelist        // 堆内存中的空闲空间
    MALLOC: +   1164940264 ( 1111.0 MiB) Bytes in central cache freelist
    MALLOC: +            0 (    0.0 MiB) Bytes in transfer cache freelist   // transfer cache 用于在 central cache 与 thread cache 之间传输数据
    MALLOC: +    788441256 (  751.9 MiB) Bytes in thread cache freelists
    MALLOC: +    220553472 (  210.3 MiB) Bytes in malloc metadata
    MALLOC:   ------------
    MALLOC: =  38146322688 (36379.2 MiB) Actual memory used (physical + swap) // mongod 进程在操作系统中占用的内存,等于上面几项内存之和
    MALLOC: +   1304309760 ( 1243.9 MiB) Bytes released to OS (aka unmapped)
    MALLOC:   ------------
    MALLOC: =  39450632448 (37623.1 MiB) Virtual address space used
    MALLOC:
    MALLOC:        2613472              Spans in use
    MALLOC:           4708              Thread heaps in use
    MALLOC:           4096              Tcmalloc page size
    
    • mongod 默认采用 tcmalloc 内存分配器,会将空闲内存释放给操作系统,但可能不会立即释放。
  • 查看数据体积:

    db.stats().dataSize         // 数据库中全部文档的体积,单位 bytes 。这是读取到内存时的体积,没有压缩
    db.stats().storageSize      // 全部文档占用的磁盘空间。可能比 dataSize 小,因为 WiredTiger 存储引擎默认进行压缩。也可能比 dataSize 大,因为删除的文档不会释放磁盘空间
    db.stats().indexSize        // 全部索引占用的磁盘空间
    db.stats().totalSize        // 等于 storageSize + indexSize ,包括已分配但尚未使用的空闲磁盘空间
    db.stats().freeStorageSize      // 文档已分配但尚未使用的空闲磁盘空间。这是 Mongo v5.0 新增的监控指标
    db.stats().indexFreeStorageSize // 索引已分配但尚未使用的空闲磁盘空间
    
    db.<collection>.stats().wiredTiger["block-manager"]["file size in bytes"]             // 集合占用的磁盘空间
    db.<collection>.stats().wiredTiger["block-manager"]["file bytes available for reuse"] // 集合占用的磁盘空间中,标记为 deleted 的空间
    db.<collection>.stats().wiredTiger["cache"]["bytes currently in the cache"]           // 集合占用的内存体积
    db.<collection>.stats().wiredTiger["cache"]["bytes read into cache"]                  // 累计从磁盘读取到内存的数据体积
    db.<collection>.stats().wiredTiger["cache"]["bytes written from cache"]               // 累计从内存写入磁盘的数据体积
    db.<collection>.stats().wiredTiger["cache"]["tracked dirty bytes in the cache"]       // 内存中的脏页数
    db.<collection>.stats().indexSizes              // 查看各个索引占用的磁盘空间
    db.<collection>.aggregate([{$indexStats:{}}])   // 查看各个索引的累计使用次数
    
    db.serverStatus().wiredTiger["cache"]["pages requested from the cache"]     // 累计从内存读取的数据量。如果查询的数据在内存中不存在,则需要从磁盘读取
    db.serverStatus().wiredTiger["cache"]["pages read into cache"]              // 累计从磁盘读入内存的数据量
    db.serverStatus().metrics.query.planCacheTotalSizeEstimateBytes             // planCache 占用的内存
    

# explain

  • 在 mongo 客户端执行命令时,可加上 explain() 方法,显示统计信息。
  • explain() 有多种工作模式:
    queryPlanner        // 显示当前操作的执行计划,不会实际执行操作
    executionStats      // 执行当前操作,然后显示统计信息
    allPlansExecution   // 先显示执行计划,然后执行当前操作、显示统计信息
    
  • 可通过以下格式调用 explain() 方法:
    db.<collection>.<method>.explain()    // 默认采用 queryPlanner 模式
    db.<collection>.explain().<method>
    
    例:
    > db.books.find({"group": "art"}).explain('allPlansExecution')
    {
        "queryPlanner": {
            "plannerVersion": 1,
            "namespace": "test.books",      // 当前位于的 db.collection
            "indexFilterSet": false,
            "parsedQuery": {                // 将查询语句解析成基础指令
                "group": {
                    "$eq": "art"
                }
            },
            "winningPlan": {                // 准备了多个执行计划,从中选出效率最高的一个计划来执行,其它计划记录到 rejectedPlans 字段
                "stage": "FETCH",           // FETCH 表示根据索引去获取集合中的文档。IXSCAN 表示读取索引。COLLSCAN 表示全表扫描,不使用索引。IDHACK 表示根据 _id 直接获取文档,不使用索引
                "filter": {},               // 在 inputStage 之后,筛选文档
                "inputStage": {             // 输入数据的阶段,负责查询文档
                    "stage": "IXSCAN",
                    "keyPattern": {         // 该索引包含哪些字段
                        "group": 1
                    },
                    "indexName": "group_1", // 使用的索引名
                    "indexBounds": {        // 根据 parsedQuery 中哪些字段的取值上下边界,来读取索引
                        "group": [
                            "[\"art\", \"art\"]"
                        ],
                    }
                }
            },
            "rejectedPlans": []
        },
        "executionStats": {
            "executionSuccess": true,       // 操作是否执行成功
            "nReturned": 829330,            // 返回的文档数
            "executionTimeMillis": 4825,    // 操作的执行耗时
            "totalKeysExamined": 829330,    // 在索引中读取了多少条目。如果为 0 ,说明没有使用索引,或在索引中没有查询到匹配的文档
            "totalDocsExamined": 829330,    // 在集合中读取了多少文档。这里 totalDocsExamined 等于 nReturned ,说明成功用索引提高了查询效率,没有读取无关的文档
            "executionStages": {
                ...
            },
            "allPlansExecution": []
        },
        "serverInfo": {
            "host": "c442af87b30b",
            "port": 27017,
            "version": "4.4.17",
            "gitVersion": "85de0cc83f4dc64dbbac7fe028a4866228c1b5d1"
        },
        "ok": 1
    }