告别CFS:EEVDF调度器在百万QPS场景下的实测与调优

我们团队在去年重构那个负责全局流量调度的网关服务时,遇到了一个挺头疼的问题。服务跑在 96 核的 AMD EPYC 服务器上,内核还是 5.15,用的 CFS 调度器。平时 QPS 在 60 万左右稳得一批,但只要流量稍微抖动一下,或者同机房的几个容器同时启动,P99 延迟就会从 2ms 突然跳到 50ms 以上,像过山车一样。

我当时盯着 perf sched latency 的输出看了半天,发现很多延迟敏感的 worker 线程在 runqueue 里排队的时间特别长。CFS 那个基于虚拟运行时间(vruntime)的算法,虽然公平,但在这种高吞吐、短生命周期的 RPC 场景下,太过于追求“平均”了。它不会优先照顾那些马上就要处理完请求、或者等待了很久的线程,导致延迟抖动。

后来我注意到 Linux 6.6 LTS(2023年11月发布)里正式合并了 EEVDF (Earliest Eligible Virtual Deadline First) 调度器。这玩意儿的核心逻辑变了,不再单纯看 vruntime 谁小,而是看谁最早到达“虚拟截止时间”(Eligible Virtual Deadline)。简单点说,它更懂“急事急办”。

我们直接把测试环境的内核升级到了 6.8(2024年3月发布的最新稳定版),没做任何其他改动,就切到了 EEVDF。结果让我挺惊讶的,在同样的 80 万 QPS 压力下,P99 延迟的抖动幅度直接缩小了 60%。

我特意写了一段脚本,用来模拟当时那种高并发下的调度延迟情况,顺便验证 EEVDF 的表现。这个脚本会创建一堆短生命周期的进程,模拟 RPC 请求:

#!/bin/bash # 测试 EEVDF 调度延迟的脚本 # 需要 root 权限运行,且内核支持 sched_debug LOOP_COUNT=10000 WORKERS=200 echo "Starting high-frequency task simulation..." for i in $(seq 1 $WORKERS); do ( for j in $(seq 1 $LOOP_COUNT); do # 模拟极短的计算任务 awk 'BEGIN { for (i=0; i<100; i++); }' > /dev/null done ) & done wait echo "Simulation finished. Check 'perf sched latency' for details."

跑完这个脚本,我对比了 perf sched latency 的数据。在 CFS 下,很多任务的 max delay 能到 30ms+,而在 EEVDF 下,基本都压在了 5ms 以内。

为什么这么做?

因为对于网关这种东西,公平性没那么重要,重要的是确定性。EEVDF 的引入,让调度器有了“截止时间”的概念。如果一个任务等了很久(Eligible),或者它处理完了能立刻释放资源,调度器会优先把它捞起来。

不这么做会怎样?

如果不切 EEVDF,我们当时为了压住延迟,只能把容器绑核(pin core)做得更死,或者干脆加机器做物理隔离。CFS 在那种“瞬间爆发”的场景下,调度延迟是不可控的,你没法保证那个毫秒级的响应。

不过,升级内核也不是无脑升的。我在另一个跑着 Java 应用的节点上试了 6.8,发现因为调度策略变了,原本通过 nice 值调整优先级的策略有点失灵。后来我查了下社区讨论,发现 EEVDF 对 nice 值的权重计算方式和 CFS 有细微差别。我不得不把原本 nice -10 的后台计算任务调整到了 nice -15,才恢复了原来的资源抢占力度。

基于eBPF的零侵入式监控:从bpftrace到全栈可观测性实战

以前我们排查线上问题,特别是那种“幽灵”般的卡顿,真的很痛苦。有一次,一个支付接口突然变慢,从 50ms 变成了 800ms,持续了大概 10 秒又自己好了。传统的 topvmstat 根本抓不到那一瞬间,因为采样周期太长了。

后来我彻底投奔了 eBPF。这东西现在真的太强了,尤其是 bpftrace v0.20(2024年1月刚更新),配合 Linux 6.8 内核,几乎可以无死角地观测内核。

我现在的排查思路基本是这样的:不用重启服务,不用改代码,直接上 bpftrace 挂个探针。比如那次支付接口卡顿,我怀疑是内核态的锁或者系统调用出了问题。我直接写了个脚本去抓 futex 的等待时间:

#!/usr/bin/env bpftrace # 监控支付进程(假设PID是 12345)的 futex 等待延迟 # 如果等待时间超过 10ms,就打印出来 BEGIN { printf("Tracing futex wait time for PID 12345... Hit Ctrl-C to end.\n"); } uprobe:/lib/x86_64-linux-gnu/libc.so.6:futex /pid == 12345/ { @start[tid] = nsecs; } uretprobe:/lib/x86_64-linux-gnu/libc.so.6:futex /pid == 12345 && @start[tid]/ { $latency = nsecs - @start[tid]; if ($latency > 10000000) { // 超过 10ms printf("PID %d TID %d: futex blocked for %d us\n", pid, tid, $latency / 1000); @blocked[tid] = sum($latency); } delete(@start[tid]); } END { printf("\nTotal blocked time per thread:\n"); print(@blocked); clear(@start); }

跑这个脚本,我直接定位到是某个线程在等一把互斥锁,而且这把锁被一个 write 系统调用给阻塞了。顺着往下查,发现是当时日志系统的磁盘 IO 抖动,导致 write 卡在内核态,进而拖住了业务线程。

为什么用 eBPF?

传统的 strace 开销太大了,线上跑基本等于自杀,因为它会频繁陷入内核态。eBPF 是 JIT 编译后在内核内部运行的,开销极低。我测过,在生产环境跑上面的脚本,CPU 开销基本可以忽略不计,也就是 0.1% 左右。

不这么做会怎样?

如果不懂 eBPF,我可能得在那儿瞎猜是 GC 问题、网络问题还是数据库慢查询。或者得把服务停了,挂上 gdb 或者 perf 录一大堆数据回来慢慢看,效率极低。

现在的趋势是,连 Prometheus v2.50(2024年2月版)都在考虑通过 eBPF 来采集更底层的网络指标。我自己在项目里也封装了一些基于 BCC 的工具,用来监控容器的软中断分布。比如下面这个 Python 片段,是用来统计每个 CPU 上的软中断次数的,这比读 /proc/softirqs 要灵活得多:

#!/usr/bin/env python3 from bcc import BPF import time import sys # BPF 程序 bpf_text = """ #include <linux/interrupt.h> #include <linux/percpu.h> BPF_ARRAY(softirq_count, u64, NR_SOFTIRQS); // 抓取软中断入口 int trace_softirq_entry(struct pt_regs *ctx, struct softirq_action *h, unsigned int vec_nr) { if (vec_nr < NR_SOFTIRQS) { u64 *val = softirq_count.lookup(&vec_nr); if (val) { lock_xadd(val, 1); } } return 0; } """ b = BPF(text=bpf_text) b.attach_kprobe(event="__do_softirq", fn_name="trace_softirq_entry") # 这里的软中断名字对应内核定义 softirq_names = ["hi", "timer", "net_tx", "net_rx", "block", "irq_poll", "tasklet", "sched", "hrtimer", "rcu"] print("Collecting softirq stats for 5 seconds...") time.sleep(5) print("\n%-10s %-10s" % ("SOFTIRQ", "COUNT")) for i in range(len(softirq_names)): val = b["softirq_count"][i].value if val > 0: print("%-10s %-10d" % (softirq_names[i], val))

这个脚本帮我解决过一次 Nginx 软中断不均衡的问题。当时发现 net_rx 全打在 CPU0 上了,后来配合 irqbalancesmp_affinity 调整,把网卡中断打散了。

电商大促复盘:利用MGLRU与透明大页解决内存颠簸实战

那是去年双11的复盘会,我们那个核心的订单数据库(跑在 256G 内存的物理机上)在大促峰值期间,性能曲线简直没法看。QPS 刚过 15 万,系统就开始频繁地触发内存回收,导致 kswapd 进程狂吃 CPU,数据库响应时间从 5ms 飙升到 200ms。

我上去看了一下 sar -B 的输出,pgscandpgscan 的数值高得离谱。当时内核是 5.10,用的是传统的基于活跃/非活跃链表的内存回收机制。那个机制有个毛病,就是当内存压力大时,它很难精准判断哪些页面是“真的”冷数据,经常把马上要用的数据给换出去了,然后又得换回来,这就是所谓的“内存颠簸”。

后来我们决定升级到 Linux 6.6 LTS,主要是冲着它的 MGLRU (Multi-Generational LRU) 去的。MGLRU 不再用那套老旧的链表,而是引入了“代”(Generation)的概念,有点像 CPU 的缓存策略,能更精准地追踪页面的访问热度。

升级完内核,我打开了 MGLRU(默认是关闭的,需要改 sysctl):

# 启用 MGLRU sudo sysctl vm.lru_gen.enabled=1 # 查看状态 cat /sys/kernel/mm/lru_gen/enabled

光开 MGLRU 还不够,我们还得配合透明大页(THP)一起调。数据库这种随机 IO 大的场景,其实不太适合用 always 模式的透明大页,因为大页分配失败会导致卡顿。我们当时用的是 madvise 模式,并且针对数据库的 buffer pool 做了专门的配置。

我写了一个简单的 C 程序,用来模拟当时内存压力下的 page fault 情况,顺便验证 MGLRU 的效果:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> #define SIZE (1024 * 1024 * 1024) // 1GB int main() { char *p; int i; // 分配内存 p = (char *)malloc(SIZE); if (p == NULL) { perror("malloc"); return 1; } // 建议内核使用大页(MADV_HUGEPAGE) madvise(p, SIZE, MADV_HUGEPAGE); printf("Start writing memory to trigger page faults...\n"); // 写入数据,触发缺页中断 for (i = 0; i < SIZE; i += 4096) { p[i] = 'A'; } printf("Memory write done. Sleeping for a while...\n"); sleep(60); free(p); return 0; }

编译运行后,我对比了 /proc/vmstat 里的 thp_fault_allocpgmajfault。在开启 MGLRU 之前,大页分配失败率很高,导致很多 4k 的小页分配,增加了 TLB Miss。开启 MGLRU 后,内核在回收内存时更聪明了,它不会轻易去拆散大页,除非真的没路走了。

为什么这么做?

因为传统的 LRU 在高内存压力下,回收策略太粗暴。MGLRU 能区分出“最近没访问但可能马上要用”的页面和“真的冷”的页面。对于数据库这种场景,它的 buffer pool 很大,传统的 LRU 可能会把 buffer pool 里的热数据误杀,导致磁盘 IO 暴增。

不这么做会怎样?

如果不上 MGLRU,我们当时只能选择加内存,或者把数据库拆得更碎。但加内存解决不了算法层面的低效,只要压力一上来,回收机制还是会乱杀。

实际效果非常明显。同样的 20 万 QPS 压测,升级 6.6 内核并开启 MGLRU 后,kswapd 的 CPU 占用从之前的 30% 降到了 5% 以下,数据库的平均延迟稳定在 8ms 左右。这次调优让我意识到,内核的版本迭代真的不是修修 bug 那么简单,像 MGLRU 这种核心机制的替换,对上层应用性能的影响是巨大的。

4. 硬核排查:Nginx高负载下软中断不均衡与网卡优化方案

去年双十一大促,我负责的一个电商核心 API 网关(基于 OpenResty/Nginx 1.21)在流量峰值达到 12 万 QPS 时,出现了极其诡异的现象:服务响应延迟从平均 15ms 飙升至 800ms 以上,但 CPU 整体使用率仅 40%,系统负载却高达 30。通过 top 观察,发现 CPU 使用率呈严重的两极分化:CPU0 的 si(软中断)占用率长时间保持在 95% 以上,而其余 23 个逻辑核心却几乎处于空闲状态。

原因在于网卡的中断亲和性(IRQ Affinity)配置不当。我当时的环境是 Intel XXV710 25G 网卡,配合 Linux 内核 6.6 LTS。默认情况下,网卡的所有硬件队列中断都绑定在 CPU0 上。当网络流量暴增,CPU0 忙于处理网卡收包的中断下半部(SoftIRQ,主要是 NET_RXNET_TX),导致应用进程(Nginx Worker)无法获得调度,从而引发了全局性的延迟抖动。

我当时的排查路径如下:

解决方案是实施中断负载均衡。我没有直接使用 irqbalance 服务,因为在实际生产环境中,我发现 irqbalance 的策略过于保守且不可控,它无法感知 Nginx Worker 的 CPU 亲和性。我选择手动编写脚本,将网卡队列的中断绑定到特定的物理核心上,并避开 Nginx Worker 所在的核。

以下是我在生产环境中使用的核心脚本逻辑(基于 ethtooltaskset 的思路):

#!/bin/bash # 假设网卡为 eth0,Nginx Worker 绑定在 CPU 8-23 # 目标是将网卡中断绑定在 CPU 0-7 set -e interface="eth0" # 获取网卡对应的中断号 irq_list=$(grep $interface /proc/interrupts | awk '{print $1}' | sed 's/://') # 定义要绑定的 CPU 掩码 (CPU 0-7) # 0-7 的掩码计算: (1<<8)-1 = 255 -> 十六进制 0xFF cpu_mask="ff" echo "Starting IRQ affinity tuning for $interface..." for irq in $irq_list; do # 检查是否为网卡队列中断 if [ -f "/proc/irq/$irq/smp_affinity" ]; then echo "Binding IRQ $irq to CPUs 0-7" # 设置中断亲和性 echo $cpu_mask > /proc/irq/$irq/smp_affinity fi done # 验证 Nginx 配置,确保 worker_cpu_affinity 与中断隔离 # nginx.conf 片段: # worker_processes 16; # worker_cpu_affinity 0000ff00; # 仅作示意,实际为 16 个核的掩码

执行上述脚本后,软中断被分散到了 CPU 0-7。紧接着,我调整了 Nginx 的配置,将 Worker 进程绑定到剩余的 CPU 8-23,并开启了 reuseport 选项。

# nginx.conf 关键配置 worker_processes 16; # 将 worker 绑定到物理核 8-23,避开处理中断的核 worker_cpu_affinity 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111; http { # 开启 reuseport 减少锁竞争,内核 6.6 对此支持非常成熟 server { listen 80 reuseport; server_name _; # ... 其他配置 } }

优化后的结果是立竿见影的。CPU0 的 si 使用率从 95% 降至 15%,整体 P99 延迟从 800ms 回落至 120ms,且 QPS 稳定支撑住了 12 万+ 的峰值。如果不做这个隔离,单纯增加 Nginx Worker 数量只会导致更多的上下文切换,根本无法解决软中断导致的调度饥饿问题。

5. 云原生环境K8s节点调优:CPU缓存隔离与NUMA亲和性配置

在 Kubernetes 集群中,我遇到过一个典型的性能问题:一个跑着大数据计算任务的 Pod(Java 应用),在节点资源看似充足(CPU 使用率 60%)的情况下,其 Full GC 频率异常高,且计算吞吐量比物理机部署低了 30%。经过深入分析,我发现问题出在 CPU 缓存失效和跨 NUMA 内存访问上。

原因在于 Kubernetes 默认的调度机制只关注 CPU 核数的分配,并不感知 CPU 的物理拓扑(Cache 层级和 NUMA 节点)。当时我的节点是双路 Intel Xeon Silver 4314,每路 16 核,共 32 核,分为两个 NUMA 节点(node 0 和 node 1)。Pod 的 CPU 请求被随机分配到了两个 NUMA 节点上,导致进程在运行时频繁跨 NUMA 访问内存,同时由于 L3 Cache 被不同 Pod 争抢,命中率大幅下降。

为了解决这个问题,我采取了以下措施,核心是利用内核的 cgroup 特性和 numactl 工具。

首先,我确保节点内核版本在 6.6 以上,因为新内核对于 cgroup v2 的 CPU 资源隔离支持更加完善。然后,我没有直接使用 K8s 原生的 cpu-manager-policy=static(这只能做绑核,不能做 NUMA 亲和),而是采用了更底层的配置。

我编写了一个 DaemonSet,在节点上通过 sysfscgroup 手动干预特定高优先级 Pod 的亲和性。以下是一个简化的逻辑,展示如何将一个进程(假设 PID 为 12345)绑定到 NUMA 0 并限制其使用的 CPU 缓存范围:

#!/bin/bash # 针对特定高优先级 Java 进程进行 NUMA 绑定和缓存隔离 # 假设该进程 PID 为 12345 TARGET_PID=12345 TARGET_NUMA_NODE=0 # 1. 使用 numactl 将进程内存分配限制在 NUMA 0 # 这能避免内存被分配到远端 NUMA 节点 numactl --cpunodebind=$TARGET_NUMA_NODE --membind=$TARGET_NUMA_NODE --pid $TARGET_PID # 2. 调整 cgroup v2 的 cpuset,确保 CPU 亲和性 # 假设 K8s 将该 Pod 的 cgroup 放在 /sys/fs/cgroup/kubepods/podxxx CGROUP_PATH="/sys/fs/cgroup/kubepods/pod_xxx" # 获取 NUMA 0 对应的 CPU 列表 (通常是 0-15) NUMA_0_CPUS=$(cat /sys/devices/system/node/node0/cpulist) # 设置 cpuset.cpus 强制绑定到 NUMA 0 的 CPU echo $NUMA_0_CPUS > $CGROUP_PATH/cpuset.cpus # 设置 cpuset.mems 强制内存分配在 NUMA 0 echo $TARGET_NUMA_NODE > $CGROUP_PATH/cpuset.mems # 3. 监控验证 echo "Checking NUMA stats for PID $TARGET_PID:" numastat -p $TARGET_PID

在执行上述操作后,我使用 perf stat 验证了 L3 Cache 的命中率变化:

# 优化前 perf stat -e cache-references,cache-misses -p 12345 sleep 10 # 结果: cache-misses 占比约 25% # 优化后 (绑定 NUMA 0 且独占部分 L3 缓存) # 结果: cache-misses 占比降至 8%

除了手动干预,我在后续的集群建设中引入了 Topology ManagerNUMA-aware Scheduler。在 K8s 1.28+ 版本中,结合内核 6.6 的特性,我配置了 topologyManagerPolicy: single-numa-node。这确保了 Pod 的所有资源(CPU、内存、设备)都来自同一个 NUMA 节点。

如果不做 NUMA 亲和性,在跨 NUMA 访问时,内存延迟会增加 1.5 到 2 倍(从约 80ns 增加到 140ns+)。对于计算密集型任务,这种延迟累积足以导致性能雪崩。此外,我还发现,如果不隔离 CPU 缓存,相邻的 Pod 可能会频繁驱逐共享的 L3 缓存行,导致 perf 显示大量的 cache-misses,这也是为什么那个 Java 应用 GC 频繁——GC 线程在扫描内存时遭遇了极高的内存访问延迟。

6. 技术选型与避坑:为什么我放弃传统Agent选择eBPF做安全审计

在 2023 年底,我负责重构公司的基础设施安全审计系统。最初的技术栈是经典的“用户态 Agent + 内核模块”模式:我们在每台主机上部署了一个基于 Go 语言开发的 DaemonSet,它通过 auditd 读取日志,并定期执行 execve 系统调用来监控进程行为。

但在实际运行中,这个方案暴露了两个致命缺陷。第一是性能开销。在 6.6 LTS 内核上,开启 auditd 全量监控后,系统调用开销增加了约 15%,对于高并发的 Nginx 节点,这直接导致了 P99 延迟上升。第二是数据盲区。我们的 Agent 为了性能,只能采样采集,导致很多短生命周期的恶意脚本(如通过 bash -c 执行的反弹 Shell)在 Agent 轮询间隙执行完毕并消失,根本无法捕获。

原因在于传统 Agent 处于用户态,它获取内核数据的路径太长了:内核 -> auditd 日志 -> 磁盘 -> Agent 读取 -> 解析 -> 上报。这个链路不仅慢,而且容易被攻击者干扰(例如清空日志文件)。

我决定全面转向 eBPF(扩展伯克利包过滤器) 技术。基于 Linux 内核 6.8 的最新特性,eBPF 允许我在内核中直接运行沙箱程序,无需修改内核源码即可实现深度观测。我选择了 bpftrace (v0.20) 和 Cilium 生态中的 Tetragon 作为核心组件。

我放弃传统 Agent 的核心逻辑是:eBPF 程序是事件驱动的,它挂载在 tracepointkprobe 上,当系统调用发生时,eBPF 程序在内核态直接处理数据,过滤掉无关信息,只将关键的安全事件(如 execve, connect, open 等)通过环形缓冲区(Ring Buffer)发送给用户态的收集器。

以下是一个我实际编写用于检测异常 execve 行为的 bpftrace 脚本,它监控所有包含 "curl" 或 "wget" 且参数可疑的进程创建:

#!/usr/bin/env bpftrace # 监控可疑的网络下载执行行为 BEGIN { printf("Tracing suspicious execve events... Hit Ctrl-C to end.\n"); } tracepoint:syscalls:sys_enter_execve { // 获取进程名和参数 // 注意:bpftrace v0.20 对字符串处理更加稳定 $comm = comm; $filename = str(arg0); // 过滤条件:如果文件名包含 curl 或 wget if ($filename == "curl" || $filename == "wget" || strstr($filename, "curl") != -1 || strstr($filename, "wget") != -1) { // 尝试读取第一个参数 (URL) // 实际生产环境需要更复杂的参数解析逻辑,这里简化示意 printf("PID: %d, Comm: %s, Executing: %s\n", pid, $comm, $filename); // 这里可以加入更复杂的逻辑,比如判断参数是否包含 http:// 或 https:// // 并将事件推送到用户态进行告警 } } tracepoint:syscalls:sys_exit_execve { // 可以在这里记录退出状态 }

使用 eBPF 后,效果非常显著。首先,性能损耗几乎为零。根据我在生产环境(内核 6.8)的压测,开启全量系统调用监控后,CPU 开销增加不到 1%,内存开销仅为传统 Agent 的 1/5(约 20MB vs 100MB)。其次,可见性大幅提升。我不再依赖日志轮询,而是实时捕获每一个系统调用。有一次,我们捕获到了一次通过 LD_PRELOAD 注入的提权攻击,传统 Agent 因为采样间隔错过了这次攻击,而 eBPF 程序在攻击发生的瞬间就捕获到了 mmapexecve 的异常序列。

当然,eBPF 也不是银弹。在开发过程中,我遇到了 eBPF 验证器(Verifier) 的限制。有一次我写了一个复杂的循环逻辑来解析网络包,结果加载失败,原因是验证器无法证明循环一定会在有限次数内结束。解决方案是进行循环展开或者简化逻辑,只在内核中做最基础的过滤,将复杂的解析逻辑放到用户态。这也是 2024 年社区讨论的热点:如何在保证安全性的前提下,让 eBPF 程序更灵活。

对比传统方案,eBPF 不仅解决了性能问题,还实现了安全与性能的融合。我不再需要担心 Agent 挂掉导致监控断点,因为 eBPF 程序是内核的一部分,只要内核在运行,监控就在运行。这种架构上的优势,让我彻底放弃了维护传统 Agent 的念头。

站长实战手记

去年双十一前,我接了个挺头疼的活儿。我们那个做直播带货的App,平时流量还行,一到晚上八点黄金档,API网关就开始抽风。用户疯狂反映下单卡顿,后台日志里全是超时。

我接手的时候,第一反应是去看CPU和内存,但 top 命令看了一圈,机器负载也就60%左右,没跑满啊。这就很诡异了,资源没耗尽,为啥处理不动?

我蹲在电脑前盯着监控大屏发呆,后来灵机一动,去查了软中断(si)。好家伙,不看不知道,两个CPU核心的软中断直接飙到90%以上,其他核却在围观。典型的网卡中断分配不均。

当时我试了几个法子,先是把 irqbalance 关了,手动写了脚本把中断绑定到特定的物理核上,结果效果一般。最后还是上了Receive Side Scaling (RSS) 配合网卡多队列,把流量打散。折腾到凌晨三点,终于把软中断压下去了。

关于技术选型,我想啰嗦两句。

* eBPF 虽好,别乱上:现在大家都吹 eBPF,我也爱用。但如果你只是个日活几千的小站,装个 Prometheus + Node Exporter 就够了。为了炫技上 eBPF 搞可观测性,维护成本能让你哭出来。

* 调度器不是万能药:文章里提到的 EEVDF 或者 CFS 调优,那是针对那种百万并发、对延迟极其敏感的场景。普通业务改内核参数,大概率不如优化一下你的 SQL 语句或者代码逻辑来得实在。

最后给各位正在死磕性能的朋友一句劝:别一上来就盯着内核参数看。很多时候瓶颈不在 Linux,而在你写的代码里。先确认是系统问题再动手,不然改了一堆 sysctl 配置,最后发现是数据库连接池漏了,那就真的尴尬了。