告别“土豆服务器”:从指标到根因的性能排查与优化实战 当一款在线服务被用户称为“土豆服务器”时通常意味着他们遇到了高延迟、频繁掉线、页面转圈、请求超时或者一到高峰期就明显变慢。这个说法最初用来形容性能撑不住玩家数量的服务器后来逐渐变成对一切卡顿体验的统称。对后端开发者和运维人员来说真正重要的不是给服务器起外号而是建立一套从现象到根因的排查方法用户说卡到底卡在客户端渲染、公网链路还是服务端处理负载升高时瓶颈是在 CPU、内存、磁盘、数据库、线程池还是锁竞争本文以最常见的在线业务场景为例整理一条可用于实际项目的性能排查与优化路径覆盖指标认知、压测准备、系统排查、应用分析和网络检查最后给出一份可以直接复用的检查清单。内容偏工程实战适合后端开发、运维、游戏服务器开发和刚接触性能调优的读者。1. 先统一对“服务器卡顿”的认知指标不同方向完全不同1.1 用户说“卡”后台指标不一定是同一件事很多团队在收到“服务器好卡”的反馈后第一个动作是打开监控看 CPU。但“卡”是一个极其含糊的描述它可能是下面几类问题中的任意一种。第一类是用户端渲染卡顿。画面掉帧、滑动不流畅、页面加载慢这些问题出在浏览器、客户端或资源包本身后端日志完全正常。第二类是网络链路延迟。用户和服务端之间的物理距离、运营商链路、DNS 解析、代理转发都可能导致高 RTT此时服务端处理时间很短但用户感知端到端延迟很大。第三类是服务端处理变慢。应用线程在等待数据库、锁、外部接口或者 GC 停顿导致响应时间上升。第四类是连接层不稳定。用户频繁掉线、重连可能由网关超时、连接池耗尽、TCP 重传、负载均衡健康检查失败等原因引起。所以在动手排查前先用一句话把现象描述清楚是“所有请求都慢”还是“部分请求超时”还是“连接经常断”不同现象对应的排查入口完全不同。为了直观对比可以按下面这张表对号入座。用户可感知现象可能根因首要排查指标画面卡顿但接口响应正常客户端渲染、资源包加载、本地内存客户端 CPU/GPU、资源加载耗时点击操作后明显有等待感公网链路或服务端响应慢RTT、服务端 RT、请求排队时长高峰期大量请求超时并发超过系统承载连接数、线程池、慢 SQL无规律掉线、重连网关超时、TCP 连接被重置网关日志、TCP 重传、连接队列溢出1.2 后端需要盯住的核心指标无论业务多复杂后端排查最终都会落到少数几个指标上。这些指标应该先固定下来再谈优化。RT响应时间是最直观的体验指标但要注意平均值没有太大意义。一次请求耗时 50ms另一次耗时 5 秒平均下来可能是 2.5 秒但这个平均值掩盖了长尾问题。主流做法是看 P95、P99也就是 95% 和 99% 的请求落在多少毫秒以内。P99 能暴露那些真正让用户崩溃的慢请求。QPS/TPS 描述系统处理能力。QPS 上不去可能是应用代码问题也可能是数据库、缓存、下游接口被拖住。错误率关注的是 5xx、超时、异常退出的比例。一个系统如果错误率从 0.1% 涨到 2%即使 RT 没变化也说明链路已经开始不稳定。连接数也是一个容易被忽略的指标。操作系统 TCP 连接数、数据库连接池活跃连接数、线程池活跃线程数都会在最坏情况下变成系统崩溃的推手。系统指标方面CPU、内存、磁盘、网络是四个基础维度但它们的正常范围与机器规格、业务类型强相关不能直接套一个死值。指标含义学习环境参考生产环境参考常用排查入口平均 RT所有请求响应时间均值低于 200ms按业务 SLA 定APM、网关日志P99 RT99% 请求耗时的上限低于 500ms按业务 SLA 定链路追踪、中间件 Access LogQPS每秒请求数看机器规格看容量评估压测工具、监控系统错误率失败请求占比低于 0.1%通常低于 1%应用日志、网关日志CPU 使用率CPU 忙碌比例低于 70%低于 85%避免打满top、sar内存使用率物理内存占用低于 80%低于 85%free、监控系统磁盘 IO util磁盘繁忙程度低于 60%低于 80%iostatTCP 重传率重传报文占比越低越好通常低于 1%netstat、ss、抓包这些数值不是绝对标准落地时要结合自己项目的监控历史基线。没有历史基线的项目先采集两周数据形成基线再决定阈值。1.3 为什么“加机器”不能解决所有卡顿遇到卡顿就想扩容这是最直觉的解法但很多情况下并不管用。无状态 API 服务确实可以通过增加副本提升吞吐但问题往往不出在副本数量上。如果瓶颈是数据库慢查询加十台应用服务器只会让更多请求同时打在数据库上反而把数据库拖得更垮。如果有状态连接比如 WebSocket、Session、分布式锁扩容还需要考虑连接迁移和一致性处理不当会引入新的故障。再比如热点数据某一个 key 被大量请求同时访问即使整个集群有一百个节点流量还是会全部打到一个分片上加机器解决不了单点热点。所以正确的顺序是先定位再优化最后考虑扩容。扩容是结果不是起点。只有在确认系统具备水平扩展条件、瓶颈确实在资源容量时加机器才有意义。这也是为什么后面所有排查步骤都要尽量带上证据链而不是拍脑袋。2. 准备一套最小可用的压测与监控环境2.1 环境清单与实际拓扑性能优化不能只在生产环境“开盲盒”。更稳妥的做法是在本地或测试环境搭建一套最小可复现的压测环境先把瓶颈找出来再去生产环境验证。下面是一个比较典型的最小环境。这里给出的版本号只是示例落地前一定要先确认与自身项目依赖版本兼容。角色建议配置软件示例用途应用服务器4 核 8G 内存LinuxNginx、JDK 17、MySQL 8.0、Redis 7部署被测服务压测机2 核 4GLinux/macOSwrk、ab、hey产生压测流量监控终端可复用压测机sysstat、dstat、Arthas采集系统与应用指标如果被测服务是 Java 应用建议保留 Arthas如果是 Python 或 Node 服务保留进程级 CPU 分析工具即可。压测机要与应用服务器分开否则压测工具会和应用抢 CPU结果完全失真。2.2 安装常用工具# 系统性能工具 sudo apt update sudo apt install -y sysstat dstat net-tools tcpdump mtr # 压测工具 sudo apt install -y wrk apache2-utils # 如果使用 Java可下载 arthas 到指定目录 curl -L -O https://github.com/alibaba/arthas/releases/latest/download/arthas-boot.jar安装完成后用下面一组命令先采集基线。vmstat看整体负载和上下文切换iostat看磁盘sar看网络流量。# 每秒刷新一次系统状态共 120 次 vmstat 1 120 # 查看磁盘 IO 详细情况 iostat -x 1 120 # 查看网络设备流量 sar -n DEV 1 0这些命令不会告诉你具体是哪个方法慢但它们能在最短时间内告诉你“机器是不是被打满了、瓶颈在哪一层”。2.3 压测前必须完成的环境检查很多人压测得出的结论不可信是因为压测前没有做环境检查。压测结果忽高忽低往往不是代码问题而是环境条件没有被控制住。检查项命令通过标准时间同步chronyc trackingoffset 无明显跳变文件句柄限制ulimit -n生产建议不低于 10240TCP 连接队列ss -ltn无 Send-Q 持续积压MySQL 连接数SHOW PROCESSLIST;无堆积与大量 Sleep日志级别检查应用配置压测环境不用 DEBUG业务开关检查配置中心关闭不必要的外部调用与风控逻辑时间不同步会影响日志时间戳排序严重时还会干扰分布式追踪。文件句柄太小会在大并发下出现Too many open files。日志级别开 DEBUG 会让磁盘 IO 和日志框架成为瓶颈压测出来的结果实际上是日志系统的上限而不是业务代码的上限。压测前把这些检查一遍能减少大量无效分析。3. 从系统指标开始定位资源瓶颈3.1 CPU先看负载再看使用率观察 CPU 时第一步是看平均负载。top输出里的load average: 4.50, 3.80, 3.20表示最近 1、5、15 分钟的可运行进程与不可中断进程数量平均值。这里的“不可中断进程”通常指正在等待磁盘 IO 的进程所以负载高并不一定代表 CPU 忙。如果机器是 4 核负载长期高于 4说明任务已经排队如果只有 2 核负载 4 意味着每个核大约有两个任务在抢时间片。但也要注意负载高可能是磁盘等待引起的这时 CPU 使用率反而不高。top - 10:30:01 up 3 days, 12:11, 2 users, load average: 4.50, 3.80, 3.20 Tasks: 180 total, 2 running, 178 sleeping %Cpu(s): 60.0 us, 15.0 sy, 0.0 ni, 20.0 id, 5.0 wa这一行里us是用户态 CPUsy是系统态 CPUwa是等待 IO 的 CPU。如果sy偏高常见原因包括系统调用过多、上下文切换频繁、网络软中断集中在某一个核。可以用mpstat -P ALL 1查看每个 CPU 核心的分布情况如果只有一个核跑满而其他核很空闲就要警惕网卡多队列、单线程模型或者锁竞争问题。常见误区是只看空闲百分比。CPU idle 还有 30% 不代表没问题因为可能是某个关键线程在锁等待CPU 空转也可能是大部分线程阻塞在数据库连接池整体资源利用不充分。要结合线程栈和连接池状态一起看。3.2 内存别让 swap 拖垮一切内存问题的排查第一步是看free -h。free -h重点关注available而不是free。available 表示在不触发 swap 的前提下还可以分配给新进程的内存它已经考虑了 page cache 的可回收部分。如果 available 持续走低并且si/so两列出现持续非零值说明系统正在从 swap 换入换出内存页这会让应用响应时间剧烈抖动因为换页的磁盘延迟远远高于内存访问延迟。# 查看占用内存最多的进程 ps aux --sort-%mem | head -10对于 Java 服务还要区分进程 RSS 与堆内存。RSS 包含堆外内存、元空间、JIT 编译产物、Direct Buffer。如果堆内使用率正常但 RSS 持续增长可以检查 NIO 的 Direct Buffer 是否未释放或者 JVM 元空间是否在动态扩张。生产环境建议显式设置堆大小和元空间最大值避免 JVM 不断向操作系统申请内存。常见误区内存使用率不高但应用性能很差。这种情况经常是因为操作系统已经开始使用 swap或者 page cache 被频繁回收导致磁盘 IO 上升。可以先停用 swap 或降低 swappiness再从代码层面减少大对象分配。3.3 磁盘慢查询最终会体现在 IO 上磁盘瓶颈经常以“应用很慢、CPU 不高”的形式出现。iostat -x 1是排查磁盘问题时最常用的命令。avg-cpu: %user %nice %system %iowait %steal %idle 30.00 0.00 8.00 5.00 0.00 57.00 Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util vda 12.00 25.00 100.00 300.00 0.00 0.00 0.00 0.00 1.50 2.00 0.10 8.00 12.00 0.50 90.00重点看%util、r_await、w_await。%util接近 100% 说明设备已经很忙但注意 SSD、云盘和本地机械盘的表现不同。传统机械盘在随机小 IO 下%util很容易打满云盘因为有网络 IO 和虚拟化层%util的含义会弱一些需要结合时延判断。如果发现磁盘压力大继续检查是不是以下原因造成日志写得太频繁、慢查询导致临时表落盘、数据库 binlog 刷盘策略过严、或者应用频繁写小文件。最简单的定位方法是看哪个进程写盘最多# 通过 pidstat 定位 IO 占用高的进程 pidstat -d 1 10常见误区是看到磁盘 util 高就马上加磁盘。先确认写入大小和次数如果只是大量小 IO合并日志、减少刷盘频率、优化 SQL 往往比扩容更有效。3.4 网络重传和丢包是隐形杀手网络问题不会直接显示在 CPU 或内存上但会让所有接口的 RT 看起来都很高。先用ss -s看协议统计。ss -s再检查 TCP 重传统计netstat -s | grep -i retrans重传率升高通常意味着网络链路不稳定可能是带宽打满、MTU 不一致、网卡驱动异常或者跨机房链路抖动。如果想要确认丢包发生在哪一跳可以使用mtr。# -rw 表示使用报告模式并显示真实 IP mtr -rw 10.0.0.5需要说明的是mtr中间某几跳出现丢包并不一定代表链路故障很多路由器会对探测包做限速。真正需要关注的是最终目标节点的丢包率和延迟。最后一跳丢包率高、延迟波动大才能说明端到端网络质量确实存在严重问题。系统指标排查的整体逻辑是先看机器还能不能撑再确定瓶颈是 CPU、内存、磁盘还是网络。这一层的产出是一个初步判断比如“CPU 的 sys 高用户态不高”带着这个判断进入应用层分析会更有方向。4. 再到应用层找罪魁祸首4.1 慢 SQL最常见的高延迟来源系统指标正常但接口就是慢第一优先检查数据库。绝大多数普通的 Web 应用瓶颈都出在 SQL 而不是应用代码。先开启 MySQL 慢查询日志把阈值设为 1 秒压测后查看慢日志文件。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SHOW VARIABLES LIKE slow_query_log_file;拿到慢 SQL 后用EXPLAIN分析执行计划。EXPLAIN SELECT * FROM orders WHERE user_id 10010 AND create_time 2025-01-01;重点看type、key、rows三个字段。type是ALL表示全表扫描通常需要优化key为空说明没有使用索引rows越大说明需要扫描的行数越多。要避免三种典型索引失效场景在索引列上使用函数、隐式类型转换导致索引列被转换、前导模糊查询LIKE %keyword。-- 推荐做法创建一个组合索引 ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time);慢 SQL 优化有一个容易被忽视的点索引不是越多越好。每个索引都会增加写入成本和存储空间频繁更新的字段不适合加太多索引。建索引前先用EXPLAIN验证确认执行计划已经走到预期索引再放到生产环境。4.2 线程池与连接池表面繁忙、实际等待数据库没有慢 SQL接口仍然慢就要检查应用内部的队列和等待。典型表现包括接口超时集中在高峰期、线程栈大量出现BLOCKED或WAITING状态、应用日志反复出现“连接不可用”或“任务被拒绝”。以 Java 为例先通过jstack抓一次线程快照。jstack pid jstack.log然后统计线程状态分布。grep java.lang.Thread.State jstack.log | sort | uniq -c | sort -nr | head如果大量线程阻塞在线程池队列说明核心线程数偏少或任务执行时间太长如果大量线程阻塞在数据库连接获取说明连接池的最大连接数不够或者连接获取后没有及时归还。线程池参数需要结合业务场景。一个比较通用的初始模板如下但实际数量要通过压测调整。ThreadPoolExecutor executor new ThreadPoolExecutor( 8, 16, 30, TimeUnit.SECONDS, new ArrayBlockingQueue(128), new ThreadFactoryBuilder().setNamePrefix(api-worker-).build(), new ThreadPoolExecutor.CallerRunsPolicy() );这里CallerRunsPolicy的意思是当队列满时由提交任务的线程自己执行相当于一种背压机制。它比抛出RejectedExecutionException更温和但会阻塞调用方使用前要确认业务可以接受这种限流方式。数据库连接池示例使用 HikariCPspring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000常见误区是把核心线程数调得很大觉得这样能提升吞吐。线程数超过 CPU 核数太多时上下文切换成本会明显上升性能反而下降。IO 密集型任务可以多配一些线程CPU 密集型任务线程数不应超过核数太多。需要反复强调线程池参数没有银弹必须结合压测数据和硬件规格调整。4.3 锁竞争与热点数据要借助火焰图如果线程池和连接池都正常接口仍然慢下一个嫌疑对象是锁竞争或热点数据。单机锁竞争表现为大量线程阻塞在同一个synchronized方法或ReentrantLock上。分布式锁竞争表现为大量请求等待 Redis 锁或数据库锁通常是热点账户、热点商品、热点用户这类数据被高频更新导致的。定位锁竞争最有效的方式是生成火焰图。Java 服务可以使用 Arthas 的 profiler 命令profiler start # 压测一段时间后 profiler stop火焰图里每个方块的宽度代表该函数的采样耗时占比。如果某个锁获取操作占用了非常大比例的宽度就可以顺着调用链找到具体是哪个锁、由哪段代码触发。如果热点集中在缓存 key 上比如一个 Redis key 被大量并发访问单分片流量打满常见方案是把热点 key 打散成多个带随机后缀的副本并在应用侧分片读取。如果热点集中在数据库行锁比如某个账户的余额字段被高并发更新通常要引入异步队列或者对行更新做串行化处理而不是简单增加应用实例。5. 网络链路不能只看服务器本身5.1 全链路有哪些节点谁慢要先定位服务端自身指标都很健康但用户仍然感觉慢问题可能出在网络上。一个完整请求会经过用户终端、DNS、CDN、接入层、负载均衡、网关、应用服务、缓存和数据库。任何一个节点出现延迟最终都会表现为用户侧的 RT 上升。要快速分段可以在服务端入口记录请求实际到达时间。比如网关日志显示请求从接收到返回只花了 50ms但用户端感知是 800ms那么大部分时间消耗在公网或用户终端侧。反过来如果网关日志显示耗时 700ms说明问题在服务端链路内部。引入链路追踪后可以按 traceId 查看每一跳的耗时分布。没有链路追踪的项目最低要求是在网关和核心服务之间透传一个x-request-id让日志能够对上号。5.2 用 mtr 判断公网链路哪一跳异常公网延迟问题可以用mtr做一个快速分段。mtr -rw 203.0.113.10报告里会列出从本机到目标服务器经过的每一跳路由。如果最终目标节点正常中间某跳丢包率高通常不用太担心因为一些运营商路由器会刻意丢弃探测包。真正需要处理的是最终节点的丢包率超过 1%或者最终节点的平均延迟比前一跳明显加大。如果确认最终节点延迟高先区分是本机到机房互联接口的问题还是机房最后一公里链路问题。可以分别从两个不同运营商网络的主机做测试对比结果。如果只有某一个地区网络异常那就不是运维可以单方面解决的需要联系运营商处理。5.3 内网调用延迟连接复用与 TIME_WAIT 检查公网没问题时内网服务之间的调用也可能存在隐藏瓶颈。最典型的是短连接过多导致大量TIME_WAIT尤其是在高并发下频繁创建和关闭连接会消耗端口和内核资源。# 统计 TIME_WAIT 连接数 ss -tan state time-wait | wc -l如果数量非常大优先检查客户端是否正确使用连接池服务端是否设置了过短的 keepalive。HTTP 客户端要开启连接复用数据库和 Redis 客户端要使用连接池减少无谓的新建连接。内网调用延迟还有一个容易被忽略的问题是网卡软中断分布不均。高并发小包场景下如果单核 CPUsys过高其他核比较闲可以通过启用 RSS 多队列或者在云主机上调整队列绑定来缓解。排查时可以执行下面的命令观察中断分布cat /proc/interrupts | grep -i eth如果中断集中在 0 号 CPU而业务进程占用其他核网络包处理就会成为瓶颈。6. 一套可复用的排查清单与生产预防方案6.1 从现象到根因的排查顺序性能问题最忌讳东查一下西查一下。下面这条顺序来自实际故障排查经验可以作为团队统一流程。确认用户现象、影响范围和时间范围。比如“从 14:00 开始订单创建接口 P99 从 300ms 涨到 2000ms”。查看监控大盘。先看错误率、RT、QPS 三个核心业务指标有没有同时恶化。查看应用日志。搜索超时、拒绝、OOM、连接异常等关键字。不要一上来就抓线程栈。查看中间件指标。数据库慢日志、Redis 慢日志、MQ 堆积数、连接池使用率。查看系统资源。CPU、内存、磁盘、网络四类指标优先看是否打满或接近打满。压测复现或查看链路追踪。如果生产难以操作在测试环境用相同场景复现。对照变更记录。检查最近是否有发版、配置变更、容量调整、外部依赖变更。这个顺序的核心思想是从用户可观测现象出发一步步缩小范围避免在没确认现象之前就进入代码级分析。6.2 优化动作的优先级先做低风险操作再动架构定位到根因后优化动作也要分级。不要第一天就提出拆分数据库表这样的高风险方案先看看低风险手段能不能解决问题。优化手段实施成本收益风险适用场景索引优化低高低慢 SQL、全表扫描连接池参数调整低中低线程等待、连接不可用缓存热点数据中高中读多写少、热点 key业务逻辑异步化中中中长链路、非核心逻辑限流熔断中高中下游不稳定、过载保护数据库分库分表高高高单库写入容量到达上限低风险操作做完后必须通过压测或生产监控对比确认效果。如果优化后指标没有变化说明根因判断有误要回到排查流程重新梳理而不是继续调整参数。6.3 监控与可观测性建设建议排查性能问题最怕没有历史数据。每次故障解决后团队最容易忽略的就是把监控补全。建议至少在以下四个层面埋点。应用层要采集 QPS、平均 RT、P99 RT、错误率、活跃线程数、JVM 堆内存和 GC 时间。中间件层要采集数据库连接数、慢 SQL 数量、Redis 命中率和慢命令、MQ 积压数量。基础设施层要采集 CPU、内存、磁盘 IO、网络带宽和 TCP 重传率。业务层要采集核心接口的成功率、关键业务流程的耗时以及任何能直接反映用户体感的指标。告警规则也要围绕这些指标设置。不要只盯着 CPU 超过 80% 就告警更有效的告警是“P99 超过基线 2 倍持续 5 分钟”或者“错误率超过 1% 持续 3 分钟”。这类告警与用户体感直接相关能真正把故障提前暴露出来。7. 写在调优结束之后让过程可复现、结论可验证7.1 调优时最容易犯的四个错误第一个错误是压测条件不一致。今天的压测是 10 个并发线程明天是 20 个后天的结果自然没有可比性。每次压测前要记录并发数、请求参数、数据量、JVM 参数和服务器负载否则无法形成对照组。第二个错误是只看平均值。平均值会掩盖长尾请求真正影响用户体感的往往是 P99 甚至 P99.9。优化前后对比时至少要同时看平均 RT、P95、P99 和错误率。第三个错误是拿生产环境直接做危险实验。修改线程池、连接池、JVM 参数前先在测试环境压测验证再通过发布系统灰度到生产。生产环境上的“小修改”也可能引发雪崩。第四个错误是在没有证据的情况下调到最优参数。调参本身不是目的找到瓶颈并解决瓶颈才是。如果不知道当前瓶颈是什么任何参数调整都只是在碰运气。7.2 一份可执行的性能复盘模板每次性能问题结束后建议用下面这个模板记录完整过程避免下次重新踩坑。背景哪个接口、什么时间、什么用户规模下出现问题。现象用户侧反馈和监控指标变化。变更记录最近是否有发版、配置调整、容量变化。指标对比优化前和优化后的 RT、QPS、错误率、资源使用率。根因分析最终确认的瓶颈点是什么用了什么证据支撑。短期措施当日快速恢复用了什么手段。长期措施如何从架构或规范上避免同类问题。预防方案增加了哪些监控、告警、压测场景。这个模板的作用不是写文档而是让团队在下次遇到相似问题时可以快速检索到历史排查路径。7.3 下一步可以做的扩展如果本文的排查流程能解决日常问题下一步可以从三个方向继续深入。一是学习更系统的性能分析知识比如 CPU 缓存失效、内存分配、锁粒度、网络协议栈对性能的影响。二是掌握一种链路追踪工具的落地方式把“日志找请求”升级为“请求找完整链路”。三是主动做一次故障演习在测试环境人为制造慢 SQL、线程池饱和、网络丢包让团队在低风险环境下练习这套排查流程。服务器被叫作“土豆”通常不是某一个代码问题造成的而是系统、应用、网络三层共同作用的结果。先看指标再动参数最后用压测或链路数据验证是唯一靠谱的调优顺序。对新手来说最值得练的不是看多少理论而是完整跑通一次“压测-定位-优化-复测”的循环。只要跑完一次后面遇到类似问题就会有明确的下手方向。