快速定位分析问题- 记 mongodb 详细表级操作及详细时延统计 实现原理
背景
mongodb 内核代码中提供有完善的 gotool 工具,这些开源工具作用主要有:数据导出及恢复 (mongodump 、 mongorestore 、 mongoexport 、 mongoimport) 工具、客户端 shell 链接工具 (mongo) 、 IO 测试工具 (mongoperf) 、流量 qps/ 时延等监控统计工具 (mongostat 、 mongotop) 。
mongodb 默认只提供 mongostat 和 mongotop 工具来完成流量和时延统计,这两个工具的主要功能如下:
l mongostat :监控整个集群的 qps 统计信息
l mongotop :监控表级的读写时延统计信息。
问题:
l 问题一: mongostat 可以监控整个集群的 qps 信息,但是表级的 qps 信息如何监控?例如如果某一时刻读写流量突然暴涨引起集群抖动,怎么知道是那个具体的表引起?
l 问题二: mongotop 可以获取整个表的读写时延消耗,如果某个表写时延很高,我们如何快速定位写时延高具体由增、删、改操作中的那个操作引起?
显然,mongostat 和 mongotop 满足不了我们怼上面的两个问题的需求。实际上, mongodb 内部实现上提供有对应的表级别 qps 和表级别时延统计接口,拿到这些接口统计后,我们就可以快速获取对应的数据结果,本文讲分析表级统计的实现原理及核心代码实现。
1. mongostat 、 mongotop 监控统计信息
mongodb 官方对外开源的 qps 及时延监控主要有 mongostat 和 mongotop ,本章节分析这两个工具的用法及监控项。
1.1 mongostat 监控统计
mongodb 提供了 mongostat 工具来监控当前集群的各种操作统计。 Mongostat 监控统计如下图所示:

其中,insert 、 delete 、 update 、 query 这四项统计比较好理解,分别对应增、删、改、查, getMore 记录批量拉数据时候的游标操作统计, command 统计在 mongos 和 mongod 中有不同的涵义,具体参考: mongodb 内核源码实现、性能调优、最佳运维实践系列 -command 命令处理模块源码实现三
mongostat help 参数功能详细说明如下:


|
参数项 |
功能说明 |
|
general options: |
获取版本信息、 help 帮助信息 |
|
verbosity options: |
是否打印日志信息, -v 表示打印日志, v 个数越多日志打印越多 quiet 默认不答应日志 |
|
connection options: |
链接的 mongo 实例 ip:port 地址 |
|
ssl options: |
SSL 认证相关配置 |
|
authentication options: |
鉴权认证的用户名和密码 |
|
uri options: |
uri 链接认证方式,类似 mongodb://username1:password1@ip:port |
|
stat options: |
统计选项设置: --discover :如果链接的是复制集节点,则输出整个复制集所有节点监控信息;如果 链接的是代理 mongos 节点,则输出整个分片集群节点监控信息。 -n :一共输出多少行即停止监控输出,默认没限制 --json :指定输出个数为 json 格式 -i: 直接同一个屏幕显示统计信息,屏幕刷新周期就是 -i 指定的时间 --humanReadable :是否进行字节到 M 或者 K 等的转换,默认 true |
1.2 mongotop 监控统计
mongotop 实现对所有表的读写时延消耗统计,并按照总耗时排序直观输出,对应统计打印信息如下图所示:

mongotop 监控输出项各字段说明如下:
l ns: 表名
l read : 1 秒钟内客户端对该表读操作消耗的总时间
l write : 1 秒钟内客户端对该表写操作消耗的总时间
l total : 1 秒钟内客户端对该表读写消耗的总时间
mongotop 工具 help 参数信息说明如下表所示:

|
参数项 |
功能说明 |
|
general options: |
获取版本信息、 help 帮助信息 |
|
verbosity options: |
是否打印日志信息, -v 表示打印日志, v 个数越多日志打印越多 quiet 默认不答应日志 |
|
connection options: |
链接的 mongo 实例 ip:port 地址 |
|
ssl options: |
SSL 认证相关配置 |
|
authentication options: |
鉴权认证的用户名和密码 |
|
uri options: |
uri 链接认证方式,类似 mongodb://username1:password1@ip:port |
|
stat options: |
统计选项设置: -n :一共输出多少行即停止监控输出,默认没限制 --json :指定输出个数为 json 格式 |
2. 表级详细操作统计及其时延监控统计
mongod 实例会对表级别的增、删、改、查、 getMore 、 command 进行详细的操作统计,并对每种操作的时延进行统计。每个表都拥有一个 CollectionData 结构,该结构中存储所有操作统计和时延统计;同一个操作的 qps 统计和时延统计通过 UsageData 结构实现,包含 count 和 time 两个成员。
2.1 表级统计实现原理
详细的表级统计通过以下几个类结构分层实现:
l 全局UsageMap 表
UsageMap 是一个 StringMap 表结构,该 map 表中的成员类型为 CollectionData ,一个 CollectionData 对应一个表名及其该表的各自详细 qps 和时延统计信息,核心代码定义如下:
typedef StringMap
l CollectionData 表统计信息
CollectionData 结构中包含多个成员,包含了三个维度的统计,每个维度中的成员对应一个操作统计项,统计维度及其操作类型如下表:

l UsageData
UsageData 完成上面的锁维度和请求类型维度的操作计数和时延计数, UsageData 包含 count 和 time 两个成员,分别用于操作计数和时延计数。
l OperationLatencyHistogram 表级汇总型统计
OperationLatencyHistogram 实现表级别的操作汇总计数和汇总型时延统计,在该汇总型统计中把 请求类型维度中的六项操作(queries 、 getmore 、 insert 、 update 、 remove 、 commands) 合并汇总为三项统计: _reads 、 _writes 、 _commands 。
2.2 核心代码实现
mongodb 表级详细统计实现主要由 src/mongo/db/stats/ 目录中的 top.cpp 、 top.h 、 operation_latency_histogram.cpp 、 operation_latency_histogram.h 四个文件完成。
2.2.1 核心数据结构实现
核心数据结构代码实现如下:
1. class Top {
2. ......
3. //map表中每个表占用一个
4. struct CollectionData {
5. ......
6. //锁维度
7. UsageData readLock;
8. UsageData writeLock;
9.
10. //表级别不同操作的时延统计,粒度相比OperationLatencyHistogram更小
11. //请求类型维度,包含增、删、改、查、getMore、command六类
12. UsageData queries;
13. UsageData getmore;
14. UsageData insert;
15. UsageData update;
16. UsageData remove;
17. UsageData commands;
18. //总的,上面的[queries,commands]
19. UsageData total;
20.
21. //汇总型维度,包含读、写、command三个维度
22. OperationLatencyHistogram opLatencyHistogram;
23. };
24. //锁类型,读锁还是写锁
25. enum class LockType {
26. ReadLocked,
27. WriteLocked,
28. NotLocked,
29. };
30. //Top._usage 各种命令的详细统计记录在该map表中
31. //map表中每个表占用一个,参考Top::record
32. typedef StringMap UsageMap;
33.
34. public:
35. //全局UsageMap表,表中每个成员对应一个collection表
36. UsageMap _usage;
37. ......
38. }
从上面的核心算法可以看出, UsageMap 为map 表结构,包含有所有表名及其对应的表级请求统计和时延统计,每个表的所有统计记录到 struct CollectionData {} 结构中。
CollectionData 结构中的成员可以分为三类:锁统计、详细请求统计、汇总型统计,其中汇总型统计由class OperationLatencyHistogram {} 类实现,核心成员如下:
1. class OperationLatencyHistogram {
2. ......
3. private:
4. //可以用于记录历史统计,通过buckets来区分,最大可以记录kMaxBuckets个历史统计信息
5. struct HistogramData {
6. std::array buckets{};
7. uint64_t entryCount = 0;
8. uint64_t sum = 0;
9. };
10. ......
11. HistogramData _reads, _writes, _commands;
12. }
2.2.2 核心算法实现
按照不同的维度,表级详细统计核心算法实现可以包含:锁及请求类型详细统计算法实现、汇总型表级详细统计算法实现。
l 锁类型统计和请求类型详细统计核心算法实现
mongodb 按照不同统计维度,同一个请求可以归纳到不同锁类型,同时也可以归纳到不同请求类型。例如, db.test.find({xxx}) 这个查询,在对 test 表详细统计的时候,该查询会同时对该表的读锁 readLock 统计及 queries 统计进行计数,也就是会同时记录该操作锁操作计数和查询操作计数。
锁类型统计及请求类型表级统计核心算法实现如下:
1. 找出对应表统计存储结构 CollectionData
1. void Top::record(...) {
2. ......
3.
4. //根据表名从Map表种找到该表在表中对应hash位置
5. auto hashedNs = UsageMap::HashedKey(ns);
6. stdx::lock_guard lk(_lock);
7.
8. //如果ns是已经删除的表,直接返回
9. if ((command || logicalOp == LogicalOp::opQuery) && ns == _lastDropped) {
10. _lastDropped = "";
11. return;
12. }
13. //找到改表对应的CollectionData
14. CollectionData& coll = _usage[hashedNs];
15. //开始表级计数统计
16. _record(opCtx, coll, logicalOp, lockType, micros, readWriteType);
17. }
2. 对该表进行真正的计数统计操作
1. //Top::record调用 各个命令的op及时延统计
2. void Top::_record(...) {
3. //汇总型详细表级统计
4. _incrementHistogram(opCtx, micros, &c.opLatencyHistogram, readWriteType);
5. //该表总时延计数,包括增删改查getMore command六项 及其他所有的统计
6. c.total.inc(micros);
7. //写锁计数
8. if (lockType == LockType::WriteLocked)
9. c.writeLock.inc(micros);
10. //读锁计数
11. else if (lockType == LockType::ReadLocked)
12. c.readLock.inc(micros);
13.
14. //详细增 删 改 查 getMore command统计及时延
15. switch (logicalOp) {
16. //无效类型
17. case LogicalOp::opInvalid:
18. // use 0 for unknown, non-specific
19. break;
20. case LogicalOp::opUpdate: //增
21. c.update.inc(micros);
22. break;
23. case LogicalOp::opInsert: //插入
24. c.insert.inc(micros);
25. break;
26. case LogicalOp::opQuery: //查询
27. c.queries.inc(micros);
28. break;
29. case LogicalOp::opGetMore: //getMore游标
30. c.getmore.inc(micros);
31. break;
32. case LogicalOp::opDelete: //删除
33. c.remove.inc(micros);
34. break;
35. case LogicalOp::opKillCursors: //
36. break;
37. case LogicalOp::opCommand:
38. c.commands.inc(micros);
39. break;
40. default:
41. MONGO_UNREACHABLE;
42. }
43. }
l 表级汇总型操作及时延统计
汇总型操作详细统计主要实现读、写、command 操作统计及对应时延统计,这类操作核心代码实现如下:
1. 按照不同操作分类
1. //不同请求归类参考getReadWriteType
2. //Top::_incrementHistogram 操作和时延计数操作
3. void OperationLatencyHistogram::increment(uint64_t latency, Command::ReadWriteType type) {
4. //确定latency时延对应在[0-2]、(2-4]、(4-8]、(8-16]、(16-32]、(32-64]、(64-128]...中的那个区间
5. int bucket = _getBucket(latency);
6. switch (type) {
7. //读时延累加,操作计数自增
8. case Command::ReadWriteType::kRead:
9. _incrementData(latency, bucket, &_reads);
10. break;
11. //写时延累加,操作计数自增
12. case Command::ReadWriteType::kWrite:
13. _incrementData(latency, bucket, &_writes);
14. break;
15. //command时延累加,操作计数自增
16. case Command::ReadWriteType::kCommand:
17. _incrementData(latency, bucket, &_commands);
18. break;
19. default:
20. MONGO_UNREACHABLE;
21. }
}
2. 对应分类操作计数、时延计数
1. //OperationLatencyHistogram::increment中调用
2. //读 写 command总操作自增,时延对应增加latency
3. void OperationLatencyHistogram::_incrementData(uint64_t latency, int bucket, HistogramData* data) {
4. //落在bucket桶指定时延范围的对应操作数自增
5. data->buckets[bucket]++;
6. //该操作总计数
7. data->entryCount++;
8. //该操作总时延计数
9. data->sum += latency;
10. }
3. 时延范围分区桶统计
mongodb 进行汇总型操作及时延统计后,可以获取总体的读、写、 command 平均时延,但是无法获取例如最大时延、 95% 分位时延、 99 分位时延等。 mongodb 为了满足这些需求,同时降低代码实现难度,通过分区时延统计来满足业务的这些需求。
时延范围分区桶实现原理:根据时延值,按照如下时延范围和分区桶得对应关系来完成统计操作,时延和桶的对应关系如下图所示:

时延范围分区桶核心算法实现核心代码实现如下:
1. //桶计数
2. void OperationLatencyHistogram::_incrementData(uint64_t latency, int bucket, HistogramData* data) {
3. //落在bucket桶指定时延范围的对应操作数自增
4. data->buckets[bucket]++;
5. ......
6. }
7.
8. //不同请求归类参考getReadWriteType
9. //Top::_incrementHistogram 操作和时延计数操作
10. void OperationLatencyHistogram::increment(uint64_t latency, Command::ReadWriteType type) {
11. //确定latency时延对应在[0-2]、(2-4]、(4-8]、(8-16]、(16-32]、(32-64]、(64-128]...中的那个区间
12. int bucket = _getBucket(latency);
13. switch (type) {
14. //读时延累加,操作计数自增
15. case Command::ReadWriteType::kRead:
16. _incrementData(latency, bucket, &_reads);
17. break;
18. //写时延累加,操作计数自增
19. case Command::ReadWriteType::kWrite:
20. _incrementData(latency, bucket, &_writes);
21. break;
22. //command时延累加,操作计数自增
23. case Command::ReadWriteType::kCommand:
24. _incrementData(latency, bucket, &_commands);
25. break;
26. default:
27. MONGO_UNREACHABLE;
28. }
}
从上面的代码可以看出,汇总型统计中的读、写、command 操作统计及时延统计包含该请求类型中的所有时延范围分区桶统计,已下图中的 collection 表 read 统计为例:
1. reads.ops=reads.histogram[] 数组 count 之和
2. histogram.micros 代表时延范围分区桶的时延边界值,例如 2 、 4 、 8 、 16 ,以此类推。


3. 表级详细统计对外接口
3.1 表级别锁维度及请求类型维度相关统计接口
表级别 锁维度及请求类型维度相关统计对外接口可以通过下面的命令获取得到( 注:只能在 mongod 实例执行 ) :
use admin
db.runCommand( { top: 1 } )

3.2 汇总型表级别统计
表级别汇总型读、写、command 相关操作及时延统计可以通过如下命令获取:
db.collection.latencyStats( { histograms:false}).pretty()

不同时间段对应有那些操作,例如那些操作时延比较高,可以通过时延范围分区桶统计接口获取: