1. “同配置不同命”:一场让我怀疑测试脚本的48核对比
先说结论:我见过太多人把数据库一体机的性能直接等同于“CPU核数+内存大小+NVMe数量”,然后拿着配置单去比价。这种思维不能说错,但在真实业务里会踩大坑。最典型的例子就是这次要聊的:两台都是48核配置的数据库一体机,硬件参数几乎一致,可一跑交易负载,TPS直接差了一倍。
当时的情景是这样的。一台一体机是客户A在用的,型号属于某厂家的“高性价比”系列,48核、256GB内存、全NVMe盘、万兆网络,数据库是同一版本,甚至连操作系统小版本都一样。另一台是客户B在用的,同样是48核、256GB内存、全NVMe盘。对比之前,我看配置单的反应是:这两台机器跑同一个交易场景,性能差距应该不会超过5%。结果压力一打上去就懵了:客户A的机器TPS稳定在21000左右,客户B的机器能跑到43000多,误差范围内的确差了一倍。
第一反应是怀疑测试脚本出了问题,或者是JMeter本身没配好。可把两边的压测脚本拉出来对比,交易比例、并发线程数、数据量、提交方式全都一样。又怀疑是数据库参数不一致,结果show variables里的核心参数也都差不多。最后我把排查范围扩大到BIOS设置、NUMA拓扑、网卡中断绑定、I/O调度策略这些“配置单上不写”的层面,才算找到了真正的差异。
先说清一个概念:这里的TPS,指的是数据库每秒钟能处理的事务数。事务是一组逻辑上完整的数据库操作,比如“查询订单+扣减库存+生成流水”算一个事务,执行完要么全部成功,要么全部回滚。TPS是数据库性能最核心的指标之一,但它并不只取决于CPU有多少核。CPU核数决定的是“理论上限”,而操作系统、数据库内核、存储协议、网络链路这几层能不能把CPU喂饱,才决定你实际能拿到多少。
把48核机器跑成只有一半性能,恰恰是因为“硬”配置没问题,但“软”的那套协同机制没用对。这篇文章就把这些藏在后台的协同细节掰开揉碎讲一遍,顺便聊聊怎么用JMeter把真实TPS测准,以及在实际项目中怎么对某个交易做TPS限流。
2. 软硬协同的胜负手:一体机的性能藏在配置单之外
2.1 CPU与NUMA:让每个核守好自己的“本地内存”
服务器是双路架构时,每一颗CPU都有自己的内存控制器,访问自己直连的内存最快,访问另一颗CPU的内存要跨QPI/UPI总线,延迟会明显变高。这就是NUMA(非一致内存访问)的基本逻辑。数据库一体机如果没做NUMA优化,线程可能被操作系统随便调度到任意CPU上,内存却分配在另一个CPU的远端,结果就是大量访问绕远路,CPU忙等内存回包,TPS自然上不去。
我在那次对比测试里看到的最直观差异就是:客户B那台机器,数据库进程的CPU绑定和内存分配全部固定在Socket 0上,客户A那台则是数据库线程在两个Socket之间来回漂移。用numastat看,客户A的跨节点内存访问占比明显偏高,CPU的系统态消耗也多出一截。这不是厂商给的文档里会写的东西,但你一旦理解NUMA对数据库吞吐的影响,就会明白一体机的“软”调优有多重要。
实际操作上,最常见的做法是将数据库实例通过numactl --cpunodebind绑定到一组固定的CPU核心上,再配合taskset把主要线程固定好。如果数据库本身支持线程肥化,比如每个CPU核心对应一个worker线程,那还要确保线程不被随意迁移。另外需要留意的是超线程。数据库这类高负载业务,很多时候关闭超线程比开着更稳,因为超线程共享执行单元,两个逻辑核抢资源反而可能导致延迟抖动。
2.2 网卡与中断绑定:别让一个CPU成为“快递员瓶颈”
压力测试时,每秒几万个请求从JMeter发到数据库一体机,每个数据包到达网卡都会触发中断。Linux默认情况下,中断可能被分配到任意CPU上处理,如果各网卡队列和各CPU之间的映射不合理,个别CPU会被大量中断淹没,别的CPU却闲着。数据库的SQL处理本身要消耗CPU计算资源,如果CPU时间被频繁抢走去处理中断,TPS自然受影响。
好的软硬协同是:每张网卡支持多队列(RSS),每个队列绑定一个CPU核心,并且这个核心尽量和数据库实例处理网络线程的核分开,或者干脆让同一批核同时处理数据接收和SQL解析,只要绑定的核不会因为中断处理而“饿死”就行。我记得那个测试里,客户A的机器没有做网卡队列和CPU的亲和性设置,所有网络中断都集中到了CPU 0和CPU 1上;客户B则把48个核里面分离出4个核专门跑中断,数据库主体在另外44个核上。就这一个调整,TPS就可能差出去20%。
2.3 内存与缓冲池:在“快”和“稳”之间找平衡
数据库一体机通常预装了大量内存,但内存快不代表命中率高。数据库的Buffer Pool是性能和磁盘I/O之间的缓冲层,Buffer Pool越大、命中率越高,磁盘访问就越少。可底层还有个细节容易被忽略:TLB(页表缓存)。如果操作系统默认使用4KB小页,一张大表被载入内存时页表条目数量巨大,TLB缓存经常失效,CPU每次访问内存都要多查好几轮页表。开启HugePages(大页)后,2MB大页对应的页表条目少得多,TLB命中率提升,CPU访问内存的路径被缩短。
不少一体机出厂时已经默认开启了大页,但如果你在自己搭建的环境里复现,一定要记得把数据库进程的内存锁定、关闭transparent_hugepage的defrag模式,否则系统后台整理内存时会带来不小的抖动。顺带提醒,hugepages配置过大或过小都会出问题,一般按数据库SGA/PGA总需求预留,再用cat /proc/meminfo核对HugePages_Free的值。
2.4 存储与落盘:事务提交的“最后一道关卡”
数据库事务提交要保证持久性,也就是redo/undo日志要落盘。传统机械盘时代,每次commit都调用一次fsync,这几乎是性能瓶颈的根源。现在NVMe盘虽然单次延迟已经很低,但同样存在“日志组提交”的协同问题。一体机如果支持组提交(group commit),多个事务在极短时间窗口里共享一次fsync,吞吐就会明显上升。
我之前对比那两台机器时,发现客户A的innodb_flush_log_at_trx_commit配置与客户B相同,但I/O调度器不一样。客户A使用的是通用cfq调度,而NVM Express盘其实用none或noop更合适。这个调整小,但批量提交场景下对TPS稳定性和尾部延迟影响是实实在在的。更专业的一体机还会用类似io_uring的异步I/O框架,让数据库日志写入不阻塞用户态线程,减少CPU的上下文切换开销。
2.5 数据库内核参数与“出厂预调优”的价值
一体机与普通服务器的本质区别,不只是硬件选型,还包括厂商在出厂前针对这套硬件做过的系统性参数优化。比如数据库实例的并发线程数、锁等待超时、日志缓冲大小、网络连接超时、优化器的统计信息策略等等——这些参数在通用服务器上通常由DBA根据经验去调,在一体机上则被集成到一套“交付基线”里。
“高性价比”一体机之所以能维持相对较低的价格,往往是把钱花在了刀刃上:采用中等偏上的CPU和NVMe,省掉没必要的高端外设,但把所有可能影响数据库性能的操作系统级和数据库级参数都在出厂前固化好了。所以你在JD上买一台同样配件的“裸服务器”,跑出来的性能很可能不如这一体机。这不是玄学,是软件调优的累积差距。
下面用一张表概括常见的软硬协同维度,方便你在做选型或自建时逐项核对:
| 协同层面 | 关键点 | 常见问题 | 合理调优方向 |
|---|---|---|---|
| CPU调度 | NUMA绑定、超线程 | 线程跨Socket迁移 | numactl/taskset绑定,必要时关HT |
| 网络 | 多队列、RSS、中断亲和 | 中断集中到少数CPU | 每队列绑一个独立CPU核心 |
| 内存 | 大页、内存锁定 | TLB频繁失效 | 开启HugePages,关闭THP defrag |
| 存储 | I/O调度、组提交、异步I/O | fsync阻塞用户态线程 | NVMe用none,开启日志组提交 |
| 数据库 | 并发模型、日志参数、统计信息 | 出厂基线缺失 | 按硬件规模预调优,形成基线模板 |
3. TPS虚高的真相:为什么JMeter跑出的漂亮数字不能信
3.1 什么是“TPS虚高”
近几年大家讨论数据库性能时,越来越爱提“TPS虚高”这个词。意思是,压测工具跑出来的TPS很高,但这个数字放到生产环境里没有意义,甚至起误导作用。虚高不是工具造假,而是测试模型和真实用户行为脱节产生的“失真”。
最常见的虚高原因有三个:
- 压测脚本没有设置思考时间(think time)。真实用户在页面查询后至少会看一眼结果再操作,而压测脚本如果循环之间没有间隔,数据库就像在被“死命抽打”,CPU跑得很高,TPS也好看,但生产环境根本不会有这么密集的请求。
- 交易并发比例不真实。真实业务往往是80%的查询、15%的更新、5%的复杂报表,但很多人压测时只跑一条主键更新语句,TPS看起来很高,却评估不了范围查询、排序、大事务带来的锁竞争和I/O压力。
- 错误处理和超时被忽略。JMeter默认对超时或断言失败的事务也许也会记入数据,统计口径一旦错了,TPS自然虚高。通常只看成功事务数时,如果响应时间已经飙升到客户不可接受,这时的TPS再高也是“假的”。
3.2 混合测试:模拟真实业务的关键设计
要避免虚高,还是得回到JMeter里做混合场景。所谓混合测试,就是在一个线程组里同时按比例运行多个不同的Sampler,比如下单交易、订单查询、库存修改、日志写入。每个Sampler的权重按照真实业务的占比去分配。
举个例子。假设一个电商库的业务构成大致是:订单创建占20%,订单查询占50%,库存扣减占10%,对账查询占20%。在JMeter里,可以用Switch Controller或者Throughput Controller组合组织这些Sampler,也可以直接建立多个线程组,给每个线程组分配不同比例的线程数。更精细的做法是在脚本里通过JSR223 Sampler计算权重并动态选择交易类型。
我建议的方案是先把每个交易单独做成一个测试片段(Test Fragment),然后用模块控制器引用,再通过吞吐量控制器设置占比。这样后续调整比例特别方便,不用改一堆Sampler。跑混合测试时注意,压测机也容易成为瓶颈,JMeter的聚合报告里如果出现网络延迟导致的错误,要先确认客户端资源是否够用,别把客户端的瓶颈错怪到数据库头上。
混合测试的另一个目的是找出“比例放大”后才会暴露的问题。单一事务并发再高,可能锁竞争很小;但混合事务里,写事务和读事务同时操作同一批数据,行锁、间隙锁、日志争用都会冒出来。很多一体机在厂商宣传材料里的测试是单一模型,你能跑到的数往往和宣传数字不一致,这在正常范围内,混合场景下的表现才是选型参考。
3.3 判断测试结果合理性的几个标杆
我自己判断一组压测数据可不可信,一般会同时看四个指标:
第一,CPU利用率能不能达到合理水平。如果数据库服务器的CPU利用率只有30%,TPS却标称几万,要么是客户端打不上去,要么是脚本里大量时间耗在等待而不是计算,这笔账要算清楚。第二,响应时间的分布。重点关注TP99,而不是平均值。如果TP99是平均值的5倍以上,说明系统在某个临界点开始排队了,这时的TPS要谨慎参考。第三,资源瓶颈在哪个环节。跑混合测试时可以监控磁盘I/O、网络带宽、锁等待、redo日志写入量,哪个先到瓶颈,TPS的天花板就在哪。第四,系统的稳定性。跑5分钟和跑1小时的结果可能是两回事,内存泄漏、连接堆积、日志膨胀会在长稳测试里现出原形。
用一句话总结:TPS不是单点指标,它只有在“资源利用率健康、响应时间可控、错误率接近零、长时间稳定”这四条同时成立时才有意义。
4. 怎么给某个交易限制TPS?从JMeter到数据库端的完整方案
4.1 为什么会需要“限制某个交易TPS”
这个需求初看有点反直觉——压测不是要把TPS拉得越高越好吗?怎么会有人要限制TPS?实际场景还真不少。
最常见的是混合测试里的“按比例控速”。比如真实业务要求下单交易峰值不超过每秒200笔,你压测时如果没有限速,下单交易可能冲到每秒500笔,把共享资源全吃掉,查询交易就会大幅退化。为了让测试贴近现实容量,就要主动给某个交易设置TPS上限。
其次是生产数据库的保护。某些慢查询或批量任务如果无限制地并发执行,可能会占满连接池,把关键交易拖垮。通过限流,让这个交易最多跑多少TPS,就能把影响控制在可控范围内。再有就是SLA验证。客户要求某个接口的TPS不能低于多少,同时也要求不能超过多少,因为超过意味着上游系统可能扛不住,这种情况下限流就是硬需求。
4.2 JMeter限流:Constant Throughput Timer还是Throughput Shaping Timer
JMeter里最常见的限流组件有两个:一个是自带的Constant Throughput Timer,另一个是配合插件使用的Throughput Shaping Timer。
Constant Throughput Timer的作用是让线程组以接近“每分钟X次”的速率发送请求。配置里的Target throughput是每分钟请求数,Calculate throughput based on有几种计算方式,常用的是All active threads in current thread group。它的实现方式是每个线程运行完一次后,计算提前或滞后量,再动态sleep补足时间差。优点是简单,不需要装插件;缺点是速率波动比较大,尤其在线程并发高、单个事务耗时不均匀时,实际TPS会来回震荡。
Throughput Shaping Timer是JMeter Plugins Manager里的组件,可以按时间段定义TPS曲线。比如前60秒从0慢慢升到200,中间120秒保持200稳定,后面60秒再降为0。它能更平滑地控制请求注入速率,也更符合“阶梯压测”和“稳定性压测”场景。因为限流更稳,我在做正式基准测试时一般首选它。
如果只需要限制某个Sampler,而不是整个线程组,可以把对应Sampler放在独立线程组里,再用Throughput Shaping Timer绑定到这个线程组。这样其他交易组照常压,只有这个交易组被限制在指定TPS范围内。
4.3 数据库端限流:连接池配额、并发控制与队列
但JMeter限流只解决了测试端问题。生产环境如果某个应用真的一股脑把请求打到数据库上,光靠应用层自觉限制是不够的,更稳妥的做法是在数据库访问链路里加一道闸。
连接池是最容易入手的地方。假设某个交易独占一个专用的数据库账号或连接池,那么把连接池的maximumPoolSize设小,就直接限制了并发数,进而限制了最大TPS。比如每个事务平均耗时50ms,一个连接每秒最多约20个事务,那么配置10个连接就意味着理论上限200TPS左右。按这个思路可以倒推连接数。
另一种方式是使用数据库侧的并发控制或资源组特性。部分商业数据库支持资源组,可以把某个SQL指纹或账号划分到低优先级资源组,限制其并行度和CPU时间片。MySQL没有这么细粒度的资源隔离,但可以通过中间件(如ProxySQL)做查询规则,把特定SQL路由到独立的连接池或者干脆拒绝超出阈值的请求。开源的conntrack之类的机制也能用,不过要小心对正常业务的影响。
还有一个比较实用的是“令牌桶”思路。在应用侧用一个轻量的分布式限流组件,每秒往桶里放固定数量的令牌,请求打到数据库前先取令牌,取不到就排队等待或快速失败。这样能精确控制单个交易的TPS,但需要开发配合,改成限流组件,对老系统的改造成本会高一些。相比之下,用连接池和中间件方式对应用侵入最小,也最容易落地。
4.4 回压测试与SLA验证的一个小技巧
限制TPS不光是为了不让系统过载,也常用来做“回压测试”。所谓回压,就是当数据库处理不过来时,请求方是排队等还是直接报错。在JMeter里,结合Throughput Shaping Timer把TPS逐步提高,观察数据库TPS上升曲线,一旦发现数据库TPS增长开始明显放缓,而响应时间和队列长度开始飙升,那个拐点就是系统吞吐的真实上限。这种拐点测试比一股脑并发打满更能体现系统的软硬协同能力。
我在实际项目里给客户验证一体机性能时,通常会按“阶梯升温”的方式做:TPS从100开始,每5分钟涨100,直到出现拐点。这样做出来的数据既有说服力,也不会一上来就把系统压垮。限流组件在这里起了关键作用,不然很难做到平稳的阶梯注入。
5. 高性价比一体机的选型经验:别只看配置单,要验证软硬协同
聊到这儿,你大概理解了配置相同的机器为什么会跑出相差一倍的TPS。那回到采购选型这个实际问题上,怎么判断一台“高性价比数据库一体机”是真好还是虚标?
我的建议是:不管厂商的宣传页写得多漂亮,拿到机器后,一定按下面几个动作做一轮实测和探查。
第一,确认NUMA和绑核能力是否开放。很多一体机的软硬协同体现在固件和驱动层,如果管理界面里能直接配置CPU亲和和中断绑定,说明厂商在这块下了功夫;如果什么都没有,只能当作普通服务器自己折腾。
第二,用你自己的混合脚本去跑,不要用厂商的演示测试包。厂商提供的压测脚本通常是他们最擅长的模型,不一定匹配你的业务。哪怕先从生产环境抓取一个交易比例,做成简化版混合场景,也能看出差距。跑的时候记得监控CPU的user态和sys态比例。如果sys态占得过高,说明内核和驱动层不够顺滑,这种系统到高并发时大概率会拖后腿。
第三,验证“稳定后的TPS”而不是“瞬时峰值TPS”。缓存未预热时的短时飙高没有意义,跑满1小时以上,看TPS曲线是否平稳,TP99是否可控。真正的高性价比,不是峰值有多猛,而是长时间负载下不衰减、不抖动。
第四,考虑运维和调优成本。软硬协同做得好的机器,DBA上手以后需要调整的参数很少,很多基线在交付时已经配置好,出了问题也更容易定位。如果一台机器买回来便宜,却要花两周时间去调内核和数据库参数,那段时间成本和风险成本都应该算进总成本里。
最后再分享一个个人习惯:在做每台一体机的验收测试时,我都会把上述涉及的协同相关配置项逐一截图存档,包括numactl --hardware的输出、网卡队列绑定、大页配置、I/O调度器、数据库关键参数。等将来遇到性能瓶颈,先对照这份基线看有没有配置漂移。很多时候“同样的机器突然慢了”,并不是机器坏了,而是某次系统补丁或运维操作把绑定关系重置了。这套方法帮我在不少项目里省下了排查时间,建议你也在自己负责的环境里提前留下一份“配置快照”。
回到开头的场景,客户A的机器后来就是按照客户B的软硬协同配置重新做了绑定和调优,TPS虽然没有完全追上,但从21000提升到了35000以上。配置没变、数据库没换,只动“软”的部分就挤出了60%以上的性能。这就是软硬协同的价值,它不写在配置单上,却真真切切地决定了每一核CPU能发挥出多少力量。