前一阵子接手了一个实时数据接入平台业务高峰期连接数一上去整个服务就像卡了壳要么报Too many open files要么大量连接直接被拒掉要么 CPU 在si上居高不下。一开始大家习惯性往应用层找原因代码评审批了一轮又一轮最后才发现问题压根不在代码上而是 Linux 内核参数还停在默认配置压根没为高并发场景做过调整。这轮调优做完系统承载能力直接上了个台阶所以把这次实战的思路和操作完整整理出来希望对遇到类似问题的朋友有帮助尤其是正在做高并发服务、中间件建设或者容器化改造的同学。1. 这次调优的起点业务瓶颈到底出在哪先交代一下项目背景。这套模拟项目X是一套面向多端接入的实时数据服务平台白天有大量设备持续上报数据晚间又有一波集中的批量任务触发流量曲线是典型的“双高峰”。压测阶段发现当并发连接数超过一定阈值后整个服务就开始出现连锁反应最明显的几个现象是应用日志频繁抛出SocketException: Too many open files但这会儿文件描述符貌似还有余量。短连接场景下服务端出现大量TIME_WAIT客户端新建连接时不时被Connection reset或Connection refused。系统负载并不高但top里si和wa指标明显偏高内存回收出现抖动。偶尔还会出现Out of memory: Kill process可物理内存明明还没用完。刚开始很多人的第一反应是服务配置不够比如线程池太小、连接池不够用或者负载均衡策略有问题。但把应用层的连接管理、线程模型都优化完后效果依然不理想。这时候才意识到真正卡脖子的是操作系统默认的内核参数设置。Linux 内核默认的参数设计目标是“通用兼容”。它要保证绝大多数场景下系统不会出大问题但在高并发、高吞吐的业务场景下这种保守设置往往会成为瓶颈。举例来说默认的net.core.somaxconn只有 4096部分发行版甚至更小一旦等待队列满了新连接就会被内核直接丢弃表现为“连接被重置”。再比如fs.file-max默认值可能是根据内存算出来的看着很大但单个进程能打开的文件数还受ulimit限制软件层面没调好Too many open files就会如约而至。所以我的思路很明确不是乱抄一堆参数而是先根据业务特征锁定瓶颈维度再逐个维度确认参数语义、当前值和预期值最后通过压测数据验证每一个改动。整个过程遵循一个原则——每次只改一组参数改完立刻压测观察宁可多花时间也不要一把梭。1.1 调优前先确认三件事动系统参数之前有些前置检查必须做不然会南辕北辙确认当前系统版本和内核版本不同发行版、不同内核版本的默认值差异很大有些参数甚至在不同版本里含义都有微妙变化比如net.ipv4.tcp_tw_recycle在较新内核上已经被移除。确认业务类型和连接模型是长连接多还是短连接多TCP 连接是否大多来自内网代理层内存密集型还是 IO 密集型这些直接决定参数侧重点。确认性能基线和压测方案没有基线数据调优就是盲人摸象。我习惯先用sar、vmstat、dmesg、ss -s等工具收集一段时间的数据再开始动手。有个细节值得多说一句拿到一台新机器我第一件事是去看这几个文件——/etc/sysctl.conf、/etc/security/limits.conf、/etc/systemd/system.conf以及systemctl show输出的当前进程限制。因为即使你改了系统参数systemd 托管的服务或容器还受各自的LimitNOFILE约束两层限制都得对齐。很多朋友调了半天发现没效果就是漏了这层。2. 关键内核参数解析与实操选型这次涉及的核心参数大致可以分为文件句柄、网络连接、内存回收三大类。我把我实际调过的、在项目里真正起到关键作用的参数逐个拆开讲每个参数都说明语义、为什么调、以及我最终选了哪个值。2.1 文件句柄别让“句柄耗尽”成为第一个拦路虎文件句柄这块涉及的参数其实有两层内核层fs.file-max和fs.nr_open以及用户态进程层ulimit -n/LimitNOFILE。缺一不可。先看内核层。fs.file-max表示系统范围内可以打开的文件句柄总数上限它直接影响Too many open files能否出现而fs.nr_open是单个进程可分配的文件句柄上限它约束的是ulimit能设置到的最大值。默认情况下fs.nr_open是 1048576如果你想把某个进程的句柄数调到百万以上就得同步调这个参数。用户态层则是nofile软硬限制。软限制是当前生效值硬限制是能提升到的最大值。很多服务部署完默认就是 1024这在高并发下就是物理上限再怎么样都上不去。我这边的实际操作是把单机最大句柄数调到了 1048576进程级软硬限制也分别调到1048576。有人问是不是太大了说实话对于普通服务确实没必要但吞吐量大的接入层机器连接数一涨句柄数会非常夸张。一次连接至少占两三个文件句柄socket 本身、event poll、连接日志等你可以按“峰值连接数 × 每连接句柄数”反推一下算完你就知道默认 1024 有多不够看。2.2 网络连接队列与端口范围承接高并发连接的第一道闸门连接进来后内核会把它放进两个队列一个是 SYN 队列半连接队列一个是 accept 队列全连接队列。这两个队列的大小直接决定了高并发下用户端能不能连上。net.ipv4.tcp_max_syn_backlog是 SYN 队列的最大长度net.core.somaxconn则是 accept 队列的最大长度。这里有个误区很多人以为应用层设置了监听队列长度为 1024 或者 2048内核就会照做实际上somaxconn大于应用层设置时取应用层值小于时内核会强制截断成somaxconn的值。比如你在程序里设置 backlog 为 1024但系统somaxconn是 4096那就用 1024反过来如果你程序里设置 2048系统只有 256那实际就是 256。所以两边要对齐我这次统一把somaxconn调到了 65535应用层监听队列也同步扩到 65535。net.ipv4.ip_local_port_range决定的是客户端发起连接时可用的本地端口范围。这个参数常被忽略但它非常要命。如果服务自身要作为客户端去请求下游端口范围太小加上TIME_WAIT占用很快就发不出新连接了。默认范围一般是 32768 到 60999算下来能用的端口也就两万多个。对于高并发出流量的服务来说这个数是远远不够的。我这次按照“峰值出去连接数 余量”的思路把它调成了 1024 到 65535一下多出来好几万可用端口。net.ipv4.tcp_tw_reuse和net.ipv4.tcp_fin_timeout这两个是配合使用的。tcp_tw_reuse允许内核在安全场景下复用处于TIME_WAIT状态的连接前提是开启tcp_timestampstcp_fin_timeout则决定TIME_WAIT状态下连接保持的时间默认 60 秒。短连接密集的场景TIME_WAIT连接一大堆如果既不复用也不缩短保持时间连接表就会快速膨胀本地端口也会被占死。我这次开了tcp_tw_reuse并保持了tcp_fin_timeout为 30配合端口范围扩大TIME_WAIT的影响基本就控制住了。这里特别提醒tcp_tw_recycle这个参数在新版内核已经被移除而且它在 NAT 环境下会引发严重丢包问题看到了也千万别开。2.3 内存回收与脏页别让系统抖动吃掉你的延迟服务端高并发场景内存层面的内核参数主要关注几个点是否使用 swap、内存超卖策略、mmap上限以及内存回收的激进程度。vm.swappiness是最容易被人忽略的参数之一。默认值普遍是 60意思是系统在内存压力不大的情况下也会倾向于把某些内存页换到 swap 上。对于追求稳定低延迟的在线业务这显然是灾难——进程明明还在用某段内存却可能被换到磁盘上一旦访问就触发磁盘 IO延迟瞬间飙升。我在这次的机器上统一调成 10甚至在部分纯接入型节点直接调成 1。不过说实话更彻底的方案是直接不配 swap但某些监控/安全组件会检查 swap 是否存在所以保守起见我保留了一个很小的 swap 并调低 swappiness。vm.max_map_count这个参数在 Java 服务、Elasticsearch、以及大量使用共享内存和 mmap 的组件里特别关键。很多朋友用 ES 跑着跑着就报max virtual memory areas vm.max_map_count 65530 is too low, increase to at least 262144原因就是进程需要创建的映射区域数超过了默认上限。这个参数默认值 65530 在大型 Java 应用里非常容易被打爆我统计过一台跑了多个 Java 服务的机器映射数量轻松超过 12 万。这次直接把它调成了 262144一些重缓冲的组件机器上调到了 524288效果立竿见影。vm.overcommit_memory控制着内核对内存申请过量的处理方式。默认 0 属于启发式过载系统会根据内存情况进行合理拒绝对于数据库或 Redis 这类希望尽量使用内存作为缓存的场景可以设置为 1表示永远允许过载由业务自己管理内存风险。我这里有一个 Redis 集群节点因为大 key 加载时频繁出现内存申请被拒绝的问题调成 1 之后才算稳定。但这参数不是什么场景都适用需要谨慎评估。此外还有vm.dirty_ratio和vm.dirty_background_ratio这两个脏页参数控制着脏页在内存中的积累上限以及后台回写时机。对于日志量大、频繁写磁盘的应用适当降低后台回写阈值能让写入更平滑避免脏页积压到临界点后产生巨大的写停顿。我这次把vm.dirty_background_ratio调为 5vm.dirty_ratio调为 10。3. 实操过程与核心环节实现理论说再多也不如亲手操作一遍来得实在。我把这次标准化的调整流程写出来每一步都是实际操作并且标注了哪些地方容易翻车。3.1 调整前建立安全回退基线动手之前必须留后路这是老司机和新手的本质区别。我用的方法是先备份当前生效配置sysctl -a /tmp/sysctl_bf.conf确认当前应用层限制和 systemd 限制ulimit -n、systemctl show -p LimitNOFILE service_name记录当前连接状态基线ss -s、free -h、uptime这个基线不仅是回退依据更是调优后对比效果的重要参考。我见过有人改完参数觉得系统“变好了”但拿不出数据最后同事都不信就是没做这步。然后编辑/etc/sysctl.conf把所有要调整的参数集中写到这个文件里。不要在/etc/sysctl.d/下撒得到处都是除非你已经知道优先级规则。我这次的做法是直接在sysctl.conf末尾追加一个带注释的分区。# 文件句柄与用户态进程限制 fs.file-max 1048576 fs.nr_open 1048576 # 网络连接队列与端口 net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_keepalive_time 1200 net.ipv4.tcp_keepalive_intvl 30 net.ipv4.tcp_keepalive_probes 3 # 内存回收与映射 vm.swappiness 10 vm.max_map_count 262144 vm.overcommit_memory 0 vm.dirty_background_ratio 5 vm.dirty_ratio 10 # 连接跟踪与防护按业务需要调整生产环境必须评估 net.netfilter.nf_conntrack_max 1048576 net.ipv4.tcp_max_tw_buckets 1048576有几个点我必须单独拎出来说第一个是net.ipv4.tcp_max_tw_buckets这个参数控制TIME_WAIT的最大数量超过的会被内核直接清掉。单纯调大它并不是常规调优手段但如果你有集中的短连接高并发业务并且短时间TIME_WAIT激增这个参数能帮你限制内核维护的连接表规模防止某些极端情况打满系统内存。注意不要把这个参数当作“清 TIME_WAIT 神器”去理解它只是限流。第二个是net.netfilter.nf_conntrack_max这是连接跟踪机制的参数只有开启了 conntrack比如调用了iptables规则、docker 默认环境等才需要关注。这个表项一旦满了新连接会直接异常表现比拒绝连接更诡异。默认值往往偏小负载高时建议加大同时还要看nf_conntrack_buckets是否匹配。但如果你没有用防火墙也不走 NAT完全可以不调它。写完之后执行sysctl -p让它生效。如果哪一行参数名字写错了sysctl -p会直接报错并告诉你具体是哪一行逐个修正即可。对于已经生效但只影响新建连接的参数比如somaxconn需要重启服务让监听 socket 重新创建才能真正生效。3.2 进程级限制改完 sysctl 还不够这是非常典型的一个环节很多人改完fs.file-max后觉得大功告成结果压测时照样报Too many open files。原因在于进程的文件描述符上限等级跟系统级完全是两码事。对于非 systemd 托管的进程你需要修改/etc/security/limits.conf* soft nofile 1048576 * hard nofile 1048576 * soft nproc 65535 * hard nproc 65535但注意limits.conf 只对通过 PAM 登录的会话生效。如果你的服务是由 systemd 托管的它还受 systemd 的DefaultLimitNOFILE控制这个值默认可能是 1024。必须在/etc/systemd/system.conf和/etc/systemd/user.conf里加上DefaultLimitNOFILE1048576 DefaultLimitNPROC65535改完之后systemctl daemon-reexec再重启服务确保LimitNOFILE生效。如果服务是启动后才改的光daemon-reload不行必须重启服务进程才能继承新的限制。我用cat /proc/pid/limits来确认进程实际生效的软硬限制而不是凭感觉判断。这一步非常关键因为有些服务自己内部还会再设一遍setrlimit()如果你发现进程限制被调回 1024说明是它的启动框架干的得去查应用配置而不是系统配置。3.3 压测验证用数据说话参数全部就位后我用压测工具做了一轮完整的负载验证。压测分三个步骤第一步单连接基准测试确认基础带宽和 RTT 没有变差。第二步逐步升高并发连接数从 1000 到 10000 再到 50000观察连接建立成功率、错误率、响应时间和失败原因。第三步混合场景测试短连接 长连接同时压模拟真实业务流量。压测结果对比如下指标项调优前默认参数调优后备注最大并发连接数约 1.2 万接近 5 万超过阈值后出现Connection refused新建连接成功率99.2%99.98%调优前存在连接被刷掉的情况平均响应时延 P9948ms23ms内存回收抖动减少延迟明显降低Too many open files触发频率中等未触发进程句柄上限扩大了 1024 倍系统软中断 CPU 占比15% ~ 25%12% ~ 18%需要配合网卡多队列优化效果更好压测过程中我特别留意了ss -s里TIME_WAIT的数量变化。调优后连接关闭速度肉眼可见地变快了TIME_WAIT表格数量从高峰时的两三万直接降到几千。这说明tcp_fin_timeout加tcp_tw_reuse的组合确实在起作用。压测里还有个容易被忽略的指标是Current: Total, 即当前连接数占系统连接跟踪表项的比例。如果你的ip_local_port_range扩大了但服务本身发起的出流量连接也多就会和入流量连接抢占端口这时还得监控端口占用情况别等端口耗尽才反应过来。4. 常见问题与排查技巧实录这部分内容全部来自我这次实战里真实踩过的坑以及长期里排查类似问题沉淀下来的一些判断方法。按场景分类整理成速查表遇到类似问题可以照着排查。4.1 参数改了但没生效怎么定位优先级最高的思路先确认参数写入位置和当前实际生效值再看进程是否重新加载了配置。执行sysctl 参数名确认内核实际看到的当前值。如果执行后值不对查看/proc/sys/下面对应的文件如果还没变检查是否存在后续的配置覆盖。执行cat /proc/pid/limits确认进程实际继承的限制。执行systemctl show -p LimitNOFILE service_name确认 systemd 层有没有限制。三个地方全部对齐参数才算落地。前阵子有朋友说他的nofile改了没用一查是 Docker 容器里的进程宿主机改了容器内还要在docker run时加--ulimit nofile1048576:1048576或 compose 文件里配ulimits否则容器进程继承的是容器默认限制。4.2 连接被拒绝但端口没满这个现象曾经让我排查了很久。表面上看ss -s连接总数并不高但新连接就是“挤不进来”。最后发现是 accept 队列满了。ss -lnt输出里Send-Q那个列显示的就是 accept 队列最大长度Recv-Q是当前积压数量。当Recv-Q长期接近Send-Q的值说明应用层accept()处理不过来或者队列配小了这时候既要考虑调somaxconn也要检查应用线程模型和accept策略。这里补一个细节如果你用的是高版本内核可以通过ss -lnt看到有没有出现accept队列溢出计数配合netstat -s里的ListenOverflows指标能精确量化问题程度。4.3 TIME_WAIT 依然居高不下开了tcp_tw_reuse和tcp_fin_timeout之后TIME_WAIT数量通常能保持平稳但如果你看到的还是三五十万的状态别急着怀疑参数先搞清楚它的来源。如果是服务端主动关闭连接而造成的TIME_WAIT这是正常连接生命周期的一部分如果只是量大但不持续上涨一般不用焦虑。如果是客户端主动发起的短连接极多除了复用端口和TIME_WAIT还得确认目标服务器有没有开启tcp_timestamps因为tcp_tw_reuse依赖时间戳判断安全复用时间戳没开启时这个参数等于没开。还有一个容易忽略的坑某些业务在代码层面错误地使用了长连接关闭方式导致服务端每响应一个请求就关一次连接连接关闭频率远超预期这时候光调内核参数治标不治本必须回去改代码。4.4 参数调完之后出现概率性超时怎么回事可能是net.ipv4.tcp_fin_timeout调得太短导致连接的TIME_WAIT状态还没充分完成就关闭了某些旧数据包在网络里刚好延迟到达造成新连接上出现数据串扰。这是短连接场景中最典型的调优副作用。我的建议是tcp_fin_timeout不要低于 15 秒尤其是处于大规模负载均衡后面的节点宁可多维护一小段TIME_WAIT也不要去赌恰好碰上乱序包的概率。同时检查tcp_max_tw_buckets如果这个值被设得极小内核会强制回收TIME_WAIT连接效果等同于主动掐掉了安全的等待时间同样会引发概率性异常。4.5 内存 swap 的热点调优后有段时间观察到某台机器kswapd进程的 CPU 占用偏高检查后发现是 swap 空间虽然没用多少但内核仍然把某些进程的内存页标记为“可回收”。这时候vm.swappiness调得再低也架不住vm.vfs_cache_pressure维持默认值 100 时的缓存回收强度。适当调低vm.vfs_cache_pressure到 50 左右可以减少目录项和 inode 缓存的回收频率对大量小文件的处理类服务有比较明显的收益。但vfs_cache_pressure也不是越低越好。如果系统本身的缓存压力非常大调低了它反而会让内核在回收内存时腾不出空间最终触发更剧烈的直接回收。这个值一般从 50 开始试不要一步调到 10。4.6 容器化环境的内核参数注意点现在服务很多跑在容器里这里有个容易产生误判的点容器内执行sysctl -p通常只能改部分属于net.ipv4.*命名空间级别的参数fs.file-max这类全局参数在容器内改了要么无效要么直接报只读。所以容器场景的内核调优该在宿主机做的必须在宿主机做该在容器编排层做的就通过sysctls字段声明。宿主机侧fs.file-max、内存参数、somaxconn等。容器侧net.ipv4.tcp_tw_reuse、net.ipv4.ip_local_port_range、net.core.somaxconn等网络命名空间相关参数。如果用的编排系统支持直接在部署配置里声明就好。为了保证容器内进程的句柄上限还需同步声明ulimits。数据类型检查定义变量时...这里内容有点跳我重新梳理一下容器部分的补充。容器排查时另一个高发问题就是LimitNOFILE没透传例如docker run默认继承的 nofile 可能是 1024压测时容器内进程直接崩掉但宿主机的fs.file-max早就调到百万级别了。检查时先docker inspect看Ulimits字段再进容器看ulimit -n如果发现不一致重建容器时用--ulimit nofile65535:65535或者编排配置里的ulimits弄上去。5. 一点补充内核参数的“生命周期”管理调优不是一次性动作因为你不可能保证业务流量模型永远不变也很难保证每个版本的内核行为一致。我现在的习惯是把每个改动参数都写清楚“调优原因”和“预期效果”以注释形式写在sysctl.conf里方便后来人接手时能看懂。版本升级或者换新机器时对比新默认值与当前配置的差异判断哪些参数已经在新内核里不需要了。每次 CD 发布执行一遍配置检查脚本把关键参数的实际生效值、进程限制、连接状态三个维度打包输出到监控平台。我之前曾在一个 Freelancer 项目里把一套配置原封不动复制到新服务器上结果新内核版本直接把tcp_tw_recycle相关配置移除启动时报了一大堆未知参数警告有些参数干脆没生效。从那时起我就学会了升级内核后必须重新验证全套参数而不是无脑迁移配置。调内核参数这个事说起来是敲几行命令但背后是对“系统如何处理连接、如何管理内存、如何调度资源”这三件事的理解。参数是死的业务是活的照搬参数表只是入门能解释清每个参数在你的业务里到底挡住了什么才叫真正调优。最后分享一个实战小技巧调优完成后用sysctl -a | grep -E (...)把关注的参数导出成文件同时配合/var/log/messages或journalctl -k检查有没有内核报错。很多问题其实在参数变更后并不会立刻暴露往往要等系统跑到那个边界值才会出现日志。所以调优后至少观察一周的监控数据别改完就以为万事大吉系统会对你的每次调整给出反馈你要做的就是用数据去听它说话。