在DSC集群配置的时候,有这么一个参数BUFFER_MODE,其默认值为0,=0与=1时分别对应两种淘汰算法,本文将介绍两种算法,并通过测试实验说明,配置BUFFER_MODE=1能有效避免配置BUFFER_MODE=0带来的高并发热点冲突问题,更能提升DSC集群的并发访问能力。
数据缓冲区是 DM Server 在将数据页写入磁盘之前以及从磁盘上读取数据页之后,数据页所存储的地方。
数据缓冲区存在三条链来管理被缓冲的数据页,一条是自由链,用于存放目前尚未使用的内存数据页,一条是 LRU 链,用于存放已被使用的内存数据页(包括未修改和已修改),还有一条即为脏链,用于存放已被修改过的内存数据页。
LRU链对系统当前使用的页其最近是否被使用的顺序进行了排序。
自由链用完时,LRU淘汰掉部分最近未使用的数据页,减少IO。
缺页时,在该分片内部执行时钟淘汰:
与LRU不同的地方在于正常的页面命中访问不需要对分片加 X 锁,只有在发生缺页访问(需要将磁盘中的新页面加载到缓冲池,触发页面置换)时,才需要对目标 BUFFER 分片加 X 锁。
在 BUFFER_MODE=0(LRU)模式下,每一次数据页访问都需要更新该分片的 LRU 链表——无论该页面是否在内存中命中,都需要将页面移动至链头或更新其访问计数。这个更新操作需要获取 buffer 分片的 X 互斥锁(mutex),导致同一分片上的并发访问产生热点竞争。
当大量并发会话同时访问落在同一个 BUFFER 分片上的不同数据页时,这些会话会排队争抢该分片的互斥锁,大量线程陷入内核态睡眠等待,响应时间急剧上升。
时钟算法将 LRU 模式下"每次访问都要改链表"的频繁加互斥锁操作,收敛为"只有缺页置换时才加 X 锁"。正常页面命中时,线程以无锁或轻量级方式并发访问 buffer 分片;仅在需要淘汰页面时,才通过访问计数器和时钟指针的周期性扫描来决定淘汰对象。
这一设计在高并发场景下显著降低了缓冲池的互斥锁竞争,减少了 CPU 在线程调度与上下文切换上的开销,从而将更多算力释放给业务逻辑。
为验证上述理论,我们在一个两节点的 DSC 集群上进行了 JMeter 压测。测试中除 BUFFER_MODE 外,其他数据库与服务端配置均保持一致。我们通过观测不同并发数下的 TPS、平均响应时间、最大响应时间和 99 百分位延迟等指标,对比两种淘汰算法在热点冲突场景下的表现。
建表相关的语句:
为了更贴合真实业务场景,并确保热点冲突能够被有效触发,测试用例将查询条件中的 PID 值设定在一个较小的随机范围内(1~10000),使大量并发请求集中在少数 BUFFER 分片上,从而放大 X 锁竞争效应。
<!-- JSR223 PreProcessor: 每次迭代生成随机 PID (1~10000) -->
<JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="随机 PID 1~10000" enabled="true">
<stringProp name="scriptLanguage">groovy</stringProp>
<stringProp name="script">int pid = (int)(Math.random() * 10000) + 1; vars.put("pid", String.valueOf(pid));</stringProp>
</JSR223PreProcessor>
<hashTree/>
<!-- SELECT 分区表回表查询 -->
<JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="分区表回表查询" enabled="true">
<stringProp name="dataSource">dm_pool</stringProp>
<stringProp name="queryType">Prepared Select Statement</stringProp>
<stringProp name="query">SELECT COUNT(*) FROM test_lkup WHERE PID = ? AND C1 > '0'</stringProp>
<stringProp name="queryArguments">${pid}</stringProp>
<stringProp name="queryArgumentsTypes">INTEGER</stringProp>
<stringProp name="variableNames"></stringProp>
<stringProp name="resultVariable"></stringProp>
<stringProp name="queryTimeout">10</stringProp>
<stringProp name="resultSetHandler">Store as String</stringProp>
</JDBCSampler>
<hashTree/>
10并发下的压测结果:
100并发下的压测结果:
400并发下的压测结果:
10并发下的压测结果:
100并发下的压测结果:
400并发下的压测结果:
通过对比,整理数据成表格
| 并发数 | 模式 | 平均响应时间 (ms) | 最大响应时间 (ms) | TPS | 99th pct |
|---|---|---|---|---|---|
| 10 | MODE=0 | 1.27 | 2489 | 7158 | 63 |
| 10 | MODE=1 | 1.02 | 903 | 8720 | 34 |
| 100 | MODE=0 | 12.34 | 3683 | 7100 | 572.99 |
| 100 | MODE=1 | 9.47 | 1757 | 9425 | 369 |
| 400 | MODE=0 | 59.94 | 5693 | 6120 | 916 |
| 400 | MODE=1 | 46.81 | 6013 | 7572 | 815 |
从上表数据可以清晰地得出以下结论:
MODE=1 的平均响应时间均低于 MODE=0。在 400 并发时,MODE=1 比 MODE=0 降低了约 22% 的平均延迟。MODE=1 的 99 百分位延迟同样显著低于 MODE=0,说明时钟算法在大压力下能更稳定地维持服务质量,减少长尾请求。MODE=1 的 TPS 在所有并发级别下均高于 MODE=0。在 100 并发时差距最为显著,MODE=1 较 MODE=0 提升了约 32.7% 的吞吐量。在测试中,100 并发时 TPS 达到峰值 9425,而当并发提升到 400 时,TPS 却回落至 7572。表面上看,并发翻了 4 倍,吞吐量理应线性增长,但实测结果却背道而驰。通过操作系统对cpu的监控,发现在100并发时cpu的占用率已经高达90多,400并发更是冲到了100,这意味着新增的并发线程并没有被真正并行执行,而是堆积在 CPU 运行队列里空等,所以会出现TPS不升反降的现象。
热点冲突的间接证据,当我们尝试把查询条件中的 PID 值的范围,从1-10000增加到1-100000,保持其他条件相同,用100的并发压测,结果发现TPS能增长到19000,这说明查询的数据页被打散到更多的 buffer 分片上,本挤在一个分片上的线程,现在被均匀分流到多个分片上,每个分片的并发压力降低,x锁竞争也降低了,TPS有所增加。这间接证明了LRU算法带来的热点冲突,会显著影响集群的性能。
宏观层面来看,配置BUFFER_MODE=1 能够提供更低的响应延迟、更高的吞吐量,接下来通过抓取堆栈,观察两种模式对于热点冲突发生时的锁竞争的详细情况。
ps -ef | grep dmserver | grep -v grep
gdb -p 8839 -batch -ex "set logging file stack1.log" -ex "set logging on" -ex "thread apply all bt" -ex "set logging off"
LRU 模式的堆栈中,通过统计,有大量线程卡在 __lll_lock_wait → buf4_mutex_enter → for_LRU,这是 buffer 分片互斥锁竞争的直接证据;而时钟模式的堆栈中几乎没有这种 mutex 阻塞。
综上所述,在 DSC 集群高并发场景下,将 BUFFER_MODE 配置为 1(时钟淘汰算法),能有效的减少 buffer 分片的 X 锁争用,降低平均响应时间和长尾延迟,提升了集群整体吞吐量。
文章
阅读量
获赞
