上个月去帮一个客户排查数据库一体机的性能问题,折腾了整整一周。起因特别简单:两台机器配置完全一样,都是48核、512GB内存、全闪存储,连型号批次都相同,但压测得到的TPS一个稳定在2.1万,另一个只有1.1万上下,差了将近一倍。客户一开始坚持认为是硬件故障,CPU、内存、磁盘、网卡全测了一遍,厂商也来现场巡检,结论都是“硬件没问题”。最后定位下来,问题全出在软件层面——内核参数、CPU亲和性、NUMA策略、IO队列几项配置的差异叠加起来,把一台好机器硬生生拖成了“半血”状态。
这种事儿在数据库一体机的日常运维里其实非常典型。很多人以为“一体机”就是买了一个预调好的黑盒子,插上电就能发挥出标称性能。但实际上,一体机只是把硬件和基础软件打包了,真正让你花出去的钱产生价值、让同样的CPU核数跑出应有TPS的,是硬件之上那一层“软硬协同”的功夫。这篇文章就把这次排查过程中最关键的几个环节完整拆开,讲讲为什么48核配置能差出1倍TPS,以及用JMeter做混合测试时那些虚高的数据是怎么来的,最后再说说怎么在数据库和接入层给某个具体交易限制TPS。
1. 为什么同样48核配置,TPS却差一倍?
1.1 48核不等于“48个平等核心”:先看懂NUMA和超线程
先别急着喷硬件,绝大多数同配置性能差异问题,第一步就应该看CPU的物理架构。现在的x86服务器,只要上了双路,基本就是NUMA架构。以常见的2路48核服务器为例,通常每颗CPU是24核,内存控制器分别挂在各自的CPU上,于是服务器被分成两个NUMA Node,每个Node拥有24核和一部分本地内存。
关键点在于:CPU访问本Node内存的速度,和跨Node访问远端内存的速度,差距不是一星半点。跨NUMA的内存访问延迟通常要比本地访问高30%到70%,如果数据库进程的线程被调度到了Node 0,但内存却分配在Node 1,那么每一次内存读写都要绕道UPI总线,频繁的跨Node访问会让整个数据库的等待时间直线上升。这个问题在高并发事务场景下会被无限放大,因为每一笔事务都涉及大量内存操作,跨Node访问的惩罚直接体现在TPS上。
再有就是超线程。48核物理核开启超线程后,系统里会看到96个逻辑核,但逻辑核之间是共享执行单元的。数据库这种CPU密集型负载,很多情况下超线程不仅不能带来收益,还会因为缓存争用和调度器把线程切到另一个逻辑核上,导致性能不升反降。所以在做软硬协同调优时,我一向建议先把超线程的影响单独测一遍,不要想当然认为逻辑核多就一定是好事。
1.2 TPS虚高:单交易压测掩盖了真实性能问题
说到TPS差距,就不得不提压测方法这个更大的坑。排查过程中我发现,客户最初给出“2.1万 vs 1.1万”这个结论时,用的压测方式其实非常粗糙——只压了单个交易,而且压测数据全是热点数据,锁竞争、日志刷盘、网络往返这些真实业务里躲不掉的因素基本都被绕开了。
这就是最近圈子里总在说的“TPS虚高”:单交易、全热数据、无思考时间、无限并发一起压出来的数据,数字确实好看,但它和真实业务之间差了十万八千里。真实业务是混合流量——查询、更新、插入、删除各种交易按一定比例同时打进来,数据有冷有热,连接有建立有释放,事务有提交有回滚,任何一环发生变化,TPS都会出现明显波动。
所以后来我给客户重新出压测方案时,第一件事就是把“混合测试”提上日程。具体怎么设计JMeter脚本,后面第4章会展开讲,但这里想先给大家提个醒:以后再看到某个厂商或者某个同行汇报数据库一体机性能,先别急着信那个单交易TPS数字,先问一句“是不是混合场景下跑出来的”。这四个字就是分水岭,软硬协同做得好不好,在混合测试里一眼就能看出来。
2. 软硬协同第一板斧:把CPU资源“钉”在数据库进程上
2.1 内核层调优:隔离CPU、关闭NUMA自动迁移
搞清楚NUMA的原理之后,下一步就是动手让数据库进程乖乖留在本Node。这部分的第一个关键操作用一句话概括:让操作系统少干预,让数据库进程有固定的“家”。
我这次排查时第一步看的就是内核启动参数。默认情况下,Linux内核的调度器为了保证所有CPU核负载均衡,会时不时把进程从一个核迁移到另一个核,迁移本身是有代价的,尤其是在NUMA架构下,线程一旦从Node 0迁移到Node 1,它之前在那个Node上分配的内存全变成远端内存,性能立刻崩给你看。所以对数据库这类延迟敏感型应用,我一般建议在/etc/default/grub的GRUB_CMDLINE_LINUX里加上CPU隔离参数,把大部分核心从内核调度器中“摘”出来,专门留给数据库进程用。
一个可供参考的启动参数示例如下:
GRUB_CMDLINE_LINUX="... isolcpus=2-23,26-47 nohz_full=2-23,26-47 rcu_nocbs=2-23,26-47 transparent_hugepage=never numa_balancing=disable"这里简单解释一下几个参数的含义。
- isolcpus:将指定CPU核从内核调度器中隔离出来,普通进程默认不会调度到这些核上,也就减少了内核线程和数据库线程抢CPU的情况。
- nohz_full:关闭隔离核上的时钟中断,减少不必要的周期性中断对数据库线程的打扰。
- rcu_nocbs:把RCU回调从隔离核上挪走,避免RCU机制带来的延迟抖动。
- transparent_hugepage=never:关掉透明大页。数据库应用对内存分配模式很敏感,透明大页容易造成内存碎片和分配延迟,生产环境建议直接关闭。
- numa_balancing=disable:关闭内核的自动NUMA平衡。自动NUMA平衡本身是好功能,但对于数据库这种不吃这套的负载,它反而会频繁迁移线程,造成性能抖动。
注意,这里我特意保留了0、1、24、25几个核不隔离,是为了让系统中断和内核线程有地方跑。如果你把全部核都隔离了,网卡中断、存储中断没有CPU处理,数据库进程反而会被反复打断,那就得不偿失了。
2.2 数据库层绑核:numactl、taskset和SMP中断绑定
内核参数调完之后,还需要在数据库进程层面做绑定。这一步的核心工具是numactl和taskset。
启动数据库时,用numactl明确指定进程的CPU亲和性和内存分配策略:
numactl --cpunodebind=0 --membind=0 /path/to/database/startup这条命令的意思是,数据库进程只允许在Node 0的CPU上运行,内存也只从Node 0分配。这样做的好处是,进程从启动那一刻起就已经把“家”安在了Node 0,后续所有线程和内存分配都会优先留在本Node。
如果是主从多实例部署,还可以把不同实例分别绑到不同NUMA Node上,比如实例1绑Node 0,实例2绑Node 1,两台实例互不干扰,内存带宽和CPU资源都能充分利用。这一步对多实例混合部署的场景尤其有效,曾经在一套48核机器上跑两个数据库实例,绑Node之后整体TPS比之前涨了40%以上。
中断绑定也是容易被忽略的一环。网卡、存储控制器都有自己的中断,默认情况下这些中断很可能全部落在CPU 0上,导致CPU 0被打满,而数据库进程所在的核却在傻等。通过smp_affinity把中断分散到多个核上,可以有效减轻CPU 0的压力:
echo 2 > /proc/irq/<中断号>/smp_affinity这里的值是一个十六进制位图,2表示只允许CPU 1处理该中断。实际生产环境中,可以结合irqbalance或者自己写脚本,把不同设备的中断分散到不同的物理核上。我个人的习惯是:每个NUMA Node留一个核专门处理本Node对应设备的IO中断,其他核全部留给数据库。
3. 软硬协同第二板斧:存储与网络IO路径不能拖后腿
3.1 存储多队列、IO调度器与redo刷盘
CPU层面的问题理顺之后,第二板斧就是存储和网络IO路径。很多同配置机器在CPU利用率不高的情况下TPS却上不去,问题往往出在IO路径上。
现代NVMe SSD都是多队列设计,配合内核的blk-mq机制,每个CPU核都可以直接向设备提交IO请求,减少了传统单队列的锁竞争。但前提是内核参数和队列深度要配置正确。我这次排查时发现,客户机器上的IO调度器还是默认的deadline,这在老式SATA盘上问题不大,但在NVMe全闪盘上反而会增加无谓的排序和合并开销。对于数据库这种对延迟极其敏感的场景,NVMe盘建议直接把IO调度器改成none:
echo none > /sys/block/nvme0n1/queue/scheduler改成none之后,IO请求直接下发到设备层,由SSD内部的调度逻辑处理,延迟和吞吐都能得到改善。这个操作需要重启或者即时生效,生产环境要小心操作。
还有一个容易被忽略的点是数据库的redo/日志文件刷盘。事务提交时必须等待日志落盘成功,这个fsync延迟直接决定了事务提交的极限速度。如果redo日志所在的存储卷IO队列深度配得太小,或者被其他业务IO干扰,那么TPS一定会被死死压住。建议把redo日志单独放在一个存储卷上,并用fio工具提前测一下这个卷在最坏情况下的延迟表现,确保p99延迟在个位数毫秒级别。
3.2 网卡多队列、连接池与并发模型
网络层面,网卡多队列和RSS(Receive Side Scaling)是让数据库服务器扛住高并发连接的基础配置。默认情况下,网卡中断会集中到一个CPU核上,如果连接数上来,这个核直接成为瓶颈。正确的做法是让网卡的队列数等于CPU物理核数,同时开启RSS,让每个核处理属于自己的网络中断。
这里给一个配置RSS的参考方式,不同网卡驱动命令稍有差异:
ethtool -L eth0 combined 48这条命令把网卡的combined队列数设置成48,配合中断绑核,可以让每个CPU核都参与网络包的接收和处理。对于数据库一体机来说,这是让TPS稳定的基础条件之一。
网络层还有一个重型话题:数据库连接池到底开多大。很多人有个误解,以为连接数越大TPS越高。实际上,CPU核数固定时,线程超过一定数量,上下文切换的成本会急剧上升,TPS反而下降。我在这次调优中专门做了连接数的梯度测试,48核机器上,数据库连接池从64个涨到128个,TPS确实涨了;但从128继续涨到256、512时,TPS不仅没涨,反而因为上下文切换和锁竞争开始下跌。
连接池的大小怎么定,没有一个固定的公式,但可以按照“连接数≈CPU核数×(2~4)”这个经验范围做起步值,再结合TPS和CPU利用率的拐点去微调。关键是你要通过压测数据找到那个“过了这个点再加连接数就没意义”的阈值,而不是拍脑袋设一个看起来很大的数字。
4. 用JMeter混合场景验收TPS,别让数据骗了你
4.1 从单交易到混合场景:一份可复用的JMeter脚本设计
CPU、NUMA、IO、网络这些软硬协同的底层工作做完了,怎么验证效果?直接用JMeter上混合测试。
先说说为什么混合测试比单交易压测更能反映真实水平。真实业务里,不同交易对资源的消耗差异非常大:简单查询很快,更新涉及锁,大报表查询可能跑几秒,写操作要刷redo。这些交易同时冲进来,数据库内部会有锁等待、IO竞争、CPU排队。单交易压测把这些全过滤掉了,你看到的只是数据库的理想状态。而混合测试就像把数据库扔进真实的人流里,是骡子是马立刻见分晓。
一份能拿来做上线依据的JMeter混合测试脚本,至少应该包含以下几个要素。
第一,数据规模要贴近生产。不要用几百条数据做压测,那等于让数据库在“缓存里玩”,再差的配置都能跑出好数据。压测前导入生产级别或等比例缩放的业务数据,确保不同数据分区冷热程度不同,才能模拟真实IO行为。
第二,交易结构要按比例配。我常用的事务分布是:查询类70%左右,短更新类20%,长事务或者报表类10%。JMeter里可以用随机控制器加权重因子来实现这种比例。
第三,并发模型要有思考时间。用户不会像机器一样毫秒不差地连续点击,所以每个线程在事务之间需要加一个随机思考时间,例如1到3秒。这个设计直接影响TPS的绝对值,也更接近真实体验。
第四,事务内部尽量包含多条SQL。一个真实业务事务往往是先查询再更新再提交,甚至包含多条SQL,不是单条SQL跑完就算。一定要用事务控制器把多条SQL包起来,并且显式提交commit,不能靠自动提交糊弄。
压测脚本的结构大致是这样的:
线程组 └─ 随机控制器(按权重分配交易) ├─ 查询接口(权重70) ├─ 更新接口(权重20) └─ 报表接口(权重10) └─ 固定/随机思考时间跑压测的时候,建议先预热5到10分钟,让数据库的buffer pool和执行计划稳定下来,再正式采样15到30分钟。采样时间如果太短,碰到一次垃圾回收或者checkpoint,数据就会很难看,很容易得出错误结论。
4.2 如何判断TPS是“虚高”还是“实在”
混合测试跑完之后,接下来要做的就是擦亮眼睛,判断手里的TPS到底值不值得信。这里画一条很简单的分界线:单交易压测数据高,不叫本事;混合场景下TPS下降幅度可控,才叫软硬协同到位。
我一般从下面几个维度来判断:
| 检查项 | 虚高表现 | 软硬协同到位的表现 |
|---|---|---|
| 单交易TPS vs 混合TPS | 混合场景掉一半以上 | 下降幅度在20%~30%以内 |
| CPU利用率 | 长期只有30%以下却自称高TPS | 核心线程CPU吃满,整体可控 |
| 响应时间 | 平均值很低,TP999突然飙高 | TP99和TP999平缓,没有长尾尖刺 |
| 数据库等待事件 | 应用侧等得厉害,数据库侧却很闲 | 有合理的IO和锁等待,没有单点堆积 |
| 连接数变化 | 加连接数TPS不涨反而乱跳 | 连接数稳定,TPS波动小 |
这里多说一句,看TPS不能只看总吞吐,一定要看响应时间分布。尤其是TP999,一个正常的系统不会允许大量请求的延迟是正常值的几十倍。软硬协同做得好,系统在混合负载下的响应时间分布是平滑的;做不好,就会出现明显的长尾尖刺,这种尖刺在单交易压测里根本暴露不出来。
5. 怎么给某个交易限制TPS:从数据库到接入层的完整方案
5.1 数据库层限流:资源管理器与资源组
混合测试验证通过之后,还有一个高频需求:某个吃资源的交易把整个实例拖垮,怎么单独把它限住?比如每天上午9点的批量报表查询,一个查询就可能消耗掉大量CPU和IO,把在线交易全部堵死。这时候就需要给这个交易单独设置TPS或资源上限。
先说数据库层的通用方案。很多数据库都提供了资源管理机制,比如老牌的Oracle Database Resource Manager,以及MySQL 8.0之后引入的Resource Group。它们的思路类似:把特定用户、服务或会话归入一个资源组,然后给这个资源组限定CPU使用率、并发度、IO优先级等指标。
拿Oracle来举例,可以通过DBMS_RESOURCE_MANAGER创建资源计划,把负责报表的用户组放到一个CPU使用率上限比较低的组里,同时限制并行度,这样报表查询就算再慢再重,也不会把在线交易的CPU份额抢光。MySQL的话,可以用CREATE RESOURCE GROUP给特定线程组绑定CPU核,并设置线程优先级,再把需要限流的会话手动放进这个资源组。
这类数据库层方案的优点是精准,能控制到资源消耗的最底层。缺点也很明显,配置相对复杂,需要在测试环境反复验证。在调整前,我强烈建议先用监控确认目标交易的平均资源消耗和峰值资源消耗,再决定资源组的上限,而不是随便拍一个百分比就能完事。
5.2 接入层与应用层限流:令牌桶与滑动窗口
数据库层的资源管理,解决的是“这个交易不能吃太多资源”的问题;而接入层和应用层限流,解决的是“这个交易一秒最多能进来多少次”的问题。两者配合,才能真正做到让某个交易限制TPS。
接入层常见的方案是用网关或负载均衡器做限流,比如Nginx的limit_req模块、APISIX的limit-count插件、Spring Cloud Gateway的RequestRateLimiter过滤器。用得最多的算法是令牌桶和滑动窗口。
令牌桶的核心理念是:系统以固定速率往桶里放令牌,每个请求进来先拿一个令牌,桶里没令牌就拒绝或者排队。这样即使上游瞬间涌入大量请求,最终落到数据库的TPS也是平滑可控的。
给一个参考配置思路,比如限制某个交易接口TPS不超过50,Nginx的配置大致如下:
limit_req_zone $binary_remote_addr zone=trade_limit:10m rate=50r/s; server { location /api/trade { limit_req zone=trade_limit burst=20 nodelay; proxy_pass http://backend; } }这里的rate=50r/s就是每秒50次请求的令牌桶速率,burst=20表示允许短时间内的突发20个请求。nodelay参数的意思是突发请求不用排队等待,直接放行,但后续超过速率的请求会被拒绝。
应用层限流则更灵活,像Sentinel、Resilience4j、Guava RateLimiter这类库可以直接嵌在业务代码里,对某个具体的service方法做限流。相比接入层的IP维度,应用层可以做到按用户、按账号、按订单类型做更细粒度的控制。比如我之前在某个项目里,就针对大客户的批量同步接口单独加了令牌桶,限制这个接口的TPS不超过20,既保住了大客户的需求,又不影响普通用户的在线交易。
5.3 落地示例:给一个“吃CPU大户”交易设定限流阈值
最后分享一个近期项目的落地示例,方便大家把前面这些串起来。
业务背景是一套48核数据库一体机上跑着一个核心交易系统,每天上午10点左右,一批报表查询任务会定时触发,这些查询单条要跑5到10秒,并发上来后直接让CPU飙到95%以上,导致在线支付的TPS从平时的3000掉到不足800。客户的需求很明确:让报表查询不要再拖垮在线交易,同时允许报表每天跑完。
我们做的配置分三层。
第一层是接入层。在网关里为报表查询接口单独配置了一个限流规则,rate限制为20r/s,burst为5。这样无论调度平台怎么并发触发,真正能打到数据库的报表请求每秒最多只有20个左右,在线交易永远能占据绝大多数CPU资源。
第二层是数据库层。通过数据库资源管理器,把报表查询所用的数据库账号归入一个CPU使用率上限为30%的资源组,同时限制该组的并行度不超过4。这样做的好处是,即使报表请求进来了,它最多也只能吃掉30%的CPU份额,剩下的70%留给在线交易;万一报表请求被限流堆压,也不会影响核心交易。
第三层是应用层。在报表服务的代码里设置了语句级超时时间,单条大查询超过30秒直接中止,并在应用层做了排队机制,超过队列长度的请求直接快速失败并重试,而不是无限制地堆积在数据库连接池里。
这套方案上线之后,报表查询高峰时段在线交易TPS稳定在2800到3200之间,CPU利用率被压在一个相对平缓的曲线附近。效果立竿见影,后台报表也能在宽松的时限内跑完,两边都保住了。
6. 这次调优之后,我留存下来的几条实战体会
这次调优的最后一环,是给整个方案做个阶段复盘。我最大的体会是:软硬协同不是玄学,它是一整套可以被验证、被量化、被复现的操作方法。CPU隔离、NUMA绑核、IO多队列、混合压测、限流策略,每一项单独拿出来都是老生常谈,但能把它们按顺序组合起来,让一台48核机器在混合场景下跑出真实的高TPS,这才是数据库一体机真正的分水岭。
复盘时我还发现一个规律:很多团队纠结于硬件选型,却忽略了软件配置的精细度。换一台更贵的机器,远不如把现有机器的内核参数、CPU亲和性、IO路径和压测方法理顺来得实在。我见过太多“48核机器跑出24核效果”的案例,最后查下来全是这类软配置问题。
如果你现在也在为数据库一体机的性能头疼,我的建议是从基线开始:先跑一遍混合场景压测,记录下TPS、CPU利用率、响应时间分布和数据库等待事件,然后一次只改一个配置项,记录变化。不要试图一天把所有参数都调到位,那样出了问题你都不知道是哪个调整带来的副作用。
最后分享一个操作上的小心得:做限流的时候,阈值永远不要拍脑袋定。从生产监控里取目标交易过去一周的P99和P999指标,再乘上1.2到1.3的余量系数,这才是合理的限流数值。限流限得太紧,业务受损;限得太松,又起不到保护作用。数据和监控,才是软硬协同这条路上最值得信任的伙伴。