一、问题现象
2024年9月9日19:20左右出现某 SQL 单表索引查询,索引选择性高且返回行数只有几行,该 SQL 大部分时间执行只要几毫秒,然而 SQL 偶尔会出现 1~2 秒的抖动,观察抖动 SQL 执行计划 rpc_time 高达1秒多。
SELECT
cust_name,
cust_uid
FROM
bbc_cx_orgm
WHERE
bbc_id= ?
ORDER BY grg_cd DESC
Sort_5 root 145285.54 bbc.bbc_cx_orgm.grg_cd:desc 2 time:1.25s, loops:2 179.8 KB 0 Bytes
└─Selection_15 root 145285.54 eq(bbc.bbc_cx_orgm.bbc_id, "1") 2 time:1.25s, loops:2 179.8 KB N/A
└─IndexLookUp_14 root 145285.54 2 time:1.25s, loops:3, index_task: {total_time: 1.24ms, fetch_handle: 1.23ms, build: 1.52µs, wait: 9.52µs}, table_task: {total_time: 1.25s, num: 1, concurrency: 5}, next: {wait_index: 1.35ms, wait_table_lookup_build: 82.8µs, wait_table_lookup_resp: 1.25s} 187.8 KB N/A
├─IndexRangeScan_12(Build) cop[tikv] 145285.54 table:bbc_cx_orgm, index:idx_bbc_id(bbc_id), range:["1","1"], keep order:false, stats:pseudo 2 time:1.21ms, loops:3, cop_task: {num: 1, max: 1.07ms, proc_keys: 2, rpc_num: 1, rpc_time: 1.06ms, copr_cache_hit_ratio: 0.00, distsql_concurrency: 20}, tikv_task:{time:0s, loops:1}, scan_detail: {total_process_keys: 2, total_process_keys_size: 222, total_keys: 3, get_snapshot_time: 25.9µs, rocksdb: {key_skipped_count: 2, block: {cache_hit_count: 11, read_count: 1, read_byte: 23.9 KB, read_time: 14.2µs}}} N/A N/A
└─TableRowIDScan_13(Probe) cop[tikv] 145285.54 table:bbc_cx_orgm, keep order:false, stats:pseudo 2 time:1.25s, loops:2, cop_task: {num: 2, max: 1.25s, min: 1.41ms, avg: 624.6ms, p95: 1.25s, max_proc_keys: 1, p95_proc_keys: 1, rpc_num: 2, rpc_time: 1.25s, copr_cache_hit_ratio: 0.00, distsql_concurrency: 20}, tikv_task:{proc max:1ms, min:1ms, avg: 1ms, p80:1ms, p95:1ms, iters:2, tasks:2}, scan_detail: {total_process_keys: 2, total_process_keys_size: 452, total_keys: 4, get_snapshot_time: 616.3µs, rocksdb: {key_skipped_count: 2, block: {cache_hit_count: 31, read_count: 2, read_byte: 27.6 KB, read_time: 323.8µs}}} N/A N/A
二、问题分析
集群拓扑是 3PD-3TiDB-12TiKV,共3台 64c / 768GB / 4 * nvme 的 x86 2numa 物理机,每台分别部署 1PD-1TiDB-4TiKV。三大组件 TiDB、TiKV、PD 无瓶颈,且物理机 CPU、IO 资源使用率低,网络无波动与异常。观察到在2024年9月9日19:20 左右,TiDB - KV Request -RPC Layer Latency
监控有异常,偶尔会出现出现1~2秒的抖动,监控含义是:
RPC Layer Latency
= KV Requst Duration
- 在 TiKV 真实执行时间(TiKV req wall time),说明 TiDB Server 到 TiKV 中间链路或 TiDB 的 TiKV client 存在抖动:
同时观察到 TiDB Server 有分配和释放较多内存的 SQL 语句:
三、分析操作系统内存
经分析三大组件 TiDB、TiKV、PD 无瓶颈,且 CPU、网络、IO 正常,接下来分析操作系统内存。检查内存时发现当操作系统 free 内存仅剩约 1GB :
再进一步看 Node_exporter - Vmstat-Page - Allocstall 监控,三台物理机都出现分配内存卡顿(allocstall),以下监控取自 Linux 虚拟文件系统 /proc/vmstat 中的 allocstall,代表发生分配内存卡顿(allocstall)的次数:
为什么 free 内存非常少?
文件缓存介绍
在 Linux 系统中,内存使用情况可分为几类,包括 used
、free
、buffers
和 cached
等。对于部署了 TiKV 的服务器上 Cached
内存较大(200GB)是正常行为,因为 TiKV 底层存储引擎使用了buffered IO
模型,后续会介绍 。
Cached
表示被用于文件缓存的内存大小(page cache
)。操作系统会尽可能地利用空闲内存作为文件缓存,以提高系统性能。当访问文件或进行文件操作时,系统会将这些文件数据缓存在内存中,下次访问相同文件时,直接从内存读取,而不是从磁盘读取,提升了访问速度。
Linux 的内存管理机制设计为尽可能地使用所有可用内存来提高系统性能。free
命令显示的 Cached
部分实际上是可用内存的一部分,当系统需要时,内核会根据需要释放缓存空间给其它程序使用。Linux 默认并不对 page cache
大小做限制,只能通过其它参数间接地影响其大小。page cache
也作为可用内存的一部分,计算 “真正可用内存” 公式是:
可用内存 = Free + Buffers + Cached
比如free -h
查看内存输出:
total used free shared buff/cache available
Mem: 768G 538G 1G 512M 204G 206G
真正可用内存大约是 206GB,看起来内存非常充裕。但实际上程序在请求内存时,需请求连续且足够大的内存才没问题。
TiKV 占用大量文件缓存
对于这3台数据库专用物理机来说,TiKV 占用的 page cache
最大。TiKV 底层 raft log 和 KV db 读写 IO 模式都是 buffered IO
,IO 读写操作都会使用 page cache
,由 RocksDB 参数use-direct-io-for-flush-and-compaction
控制,默认不使用 Direct IO
。
buffered IO
:数据在读写文件时,先被缓存在内核的 Page Cache
中。所有的文件读写请求首先会从 Page Cache
中进行。如果数据已经在缓存中,读取会非常快。如果写入操作,数据会先写入 Page Cache
中,操作系统将数据异步地刷入磁盘。但会使得 Page Cache
占用较大内存。这种设计有利于数据访问的性能,但可能在内存有限的情况下造成资源争夺和性能瓶颈,尤其是组件混合部署时。
Direct IO
: 直接将数据从用户空间传输到磁盘,绕过 Page Cache
,也就是不利用操作系统的缓存机制。文件的读写操作直接与磁盘进行交互,读取数据时直接从磁盘加载到应用程序的内存,写入时直接写入磁盘。
总的来讲,TiKV 占用了大量文件缓存使 free 内存非常少。
为什么会发生 allocstall:内存碎片
随着时间的推移内存可能会变得碎片化,虽然有足够的可用内存但不连续,这将导致进程无法分配连续的内存出现 allocstall,此时操作系统需等待直接内存回收(Direct Reclaim)和内存碎片整理 (memory compaction)。为应对内存碎片问题,操作系统会进内存行碎片整理 compaction 将正在使用的页面移动到一起创建出较大的连续区域,这个过程暂时阻止其它进程内存分配直到压缩完成,以下是 Node_exporter - Vmstat-Compact - Compact Stall 监控:
何时发生直接内存回收 Direct Reclaim?
大块内存请求
有新的大块内存分配请求,buddy 系统无法提供足够的连续内存时,系统需回收部分内存。
内存低于水位线
如果内存分配器发现空余内存的值低于 min 水位线,内存严重不足,会产生 direct reclaim。由内核参数 vm.min_free_kbytes
设置,当前3台物理机设置为16MB,本案例 free 内存并未低于 16MB,allocstall 和水位线无关。
关于 buddy 系统简介
buddy 系统用于分配连续的内存页,每个 zone 都有各自的 buddy 系统。buddy allocator 将内存分为2的幂次方的页,最大阶数为 10(order),例如:
cat /proc/buddyinfo
Node 0, zone DMA 0 0 0 1 2 1 1 0 1 1 3
Node 0, zone DMA32 7 6 5 6 5 6 7 7 6 2 272
Node 0, zone Normal 317681 38869 31620 19250 8931 2579 815 182 19 5 0
上述包含3个 ZONE:DMA,DMA32,Normal。阶数(order): 0 ~ 10,即 buddy 里对应每一阶对应的数量,最大的阶数为10, 即 1024 个 pages。例如 Normal 行第3列表示有 31620 个 2^2 连续内存块可以用。以此类推越是往后的空间越连续,表示连续空间越多,当大连续空间少时说明内存碎片严重。大部分内核操作都在 NORMAL 区进行,这是最重要的一个区域。当内存页分配时,先根据请求页的大小到相应的链表申请。如果在相应的链表上没有内存页,则向更高阶的链表去查找,如果没找到操作系统将进行内存回收和碎片整理。
以下是 SQL 抖动期间 Node_exporter - Memory - Buddy 监控,从 order 1 到 order 4 均存在归零的情况,高阶内存 order 5~10 一直是 0,说明内存碎片严重,操作系统频繁内存回收和碎片整理,从而影响系统性能:
为什么 min_free_kbytes=16MB
引发 allocstall
操作系统默认设置 vm.min_free_kbytes
16 MB, min_free_kbytes
设置得太低,操作系统会倾向于不提前释放内存,文件缓存(page cache
)占用较大导致操作系统内存紧张,可用连续内存非常少。如果此时有大量连续内存分配请求,操作系统无法满足则触发 allocstall。
四、问题根因
在抖动时段运行较多需分配较大内存的 SQL 语句,由于操作系统使用默认设置 vm.min_free_kbytes
只有16 MB。在混合部署的服务器上,TiKV 占用大量的文件缓存使 free 内存紧张,可用连续内存不足,TiDB Server 申请内存时需等待先释放内存完毕后再分配,出现 allocstall 引起抖动。
五、改善建议
将内核参数 vm.min_free_kbytes
从默认 16MB 变更为 8GB 后(变更不需重启数据库),SQL 抖动问题恢复:
修改 vm.min_free_kbytes
后高阶内存更加充裕,不再发生 allocstall:
建议未合理设置vm.min_free_kbytes
、出现 allocstall 的 TiDB 集群选择低峰期主动设置,设置后进行观察,如果仍出现 allocstall 则应继续调整。vm.min_free_kbytes
会影响内存回收机制,设置得过大会导致可用内存变少,设置得过小可能会引起内存分配延迟。对于vm.min_free_kbytes
设置的建议经验公式:
= min( number of NUMA node * 0.5% * total memory, number of TiKV node * 4GB )
以 768GB 内存 x86 2numa 服务器为例 = min( 2 * 0.5% * 768 GB, 4 * 4GB ) ≈ 8GB。
官方文档在2024年4月26日补充了建议设置,请亡羊补牢:https://github.com/pingcap/docs-cn/commit/666c3c58e4ceee517238ea4a50ce6bd3a545552b