标签: 性能优化

  • 深入 Go Runtime 内存雪崩排查:time.After 滥用引发的 Mark Assist 飙升与 GMP 调度饥饿实战

    某次核心网关服务在常规流量高峰期突发 p99 延迟雪崩,从平时的 15ms 暴增至 3000ms 以上,节点 Load Average 飙升至 CPU 核数的 3 倍。机器并未发生 OOM,但 CPU 处于满载状态。一句话交待最终结论:开发人员在高达 30k QPS 的核心消费逻辑的 for/select 循环中,直接使用了 time.After() 控制超时。由于底层 Timer 对象逃逸到堆上且在超时前无法被 GC 回收,导致堆内存分配速率远超 GC 处理能力。Go Runtime 触发背压机制,强迫大量业务 Goroutine 进入 Mark Assist(协助标记)状态,不仅榨干了 CPU,更导致 GMP 调度器中的 P 队列严重饥饿,最终演变为全局雪崩。

    time.After 放进高频 for 循环,几乎是 Go 新手最容易踩的雷,但在核心链路上犯这种低级错误,属于对系统吞吐量毫无敬畏之心。

    案发现场与指标特征

    监控大盘上的指标呈现出典型的“假死”特征:

    1. QPS 未见明显突增,但网关大量请求报 504 Gateway Timeout。

    2. CPU User 飙升至 95%,Sys 占用很低,说明没有任何阻塞型系统调用。

    3. Goroutine 数量平稳,没有出现 Goroutine 泄露导致的暴涨。

    4. 堆内存(Heap Inuse)呈剧烈的锯齿状,GC 频率极高,几乎每秒都在触发。

    在现场直接抓个 CPU pprof 分析:

    go tool pprof -http=:8080 http://127.0.0.1:6060/debug/pprof/profile?seconds=10
    

    打开火焰图,排在第一的根本不是什么业务逻辑,而是大片刺眼的系统函数:

    • runtime.gcAssistAlloc

    • runtime.gcBgMarkWorker

    • runtime.mallocgc

    这三者加起来吃掉了近 70% 的 CPU。再去抓 Heap pprof,alloc_objects 视图下,分配量 Top 1 赫然是 time.After 底层调用的 time.NewTimer

    扒开 Runtime 找死因

    为什么一个看似无害的 time.After 能把系统拖垮?这需要从 Go 的逃逸分析、三色标记法和 GMP 调度模型三个维度来看。

    1. 逃逸分析与海量垃圾产生

    来看一段精简后的肇事代码:

    func processStream(ch <-chan Msg) {
        for {
            select {
            case msg := <-ch:
                handle(msg)
            case <-time.After(5 * time.Second): 
                // 记录超时日志
            }
        }
    }
    

    time.After 的源码实现是返回一个 <-chan Time。为了保证在这个 channel 触发前定时器不出问题,Go Runtime 会将其挂载到内部的 timer heap 上。这意味着该 Timer 对象必然逃逸到堆上。 在 30k QPS 的场景下,如果每次处理耗时极短,这个 for 循环每秒会执行数万次。由于 time.After 设定的时间是 5 秒,这意味着在任何时刻,堆上都堆积了 30,000 * 5 = 150,000 个未到期的 Timer 对象。它们在到期前绝对不会被 GC 释放。

    2. 三色标记与 Mark Assist(协助标记)的背压

    Go 的 GC 采用并发三色标记法。正常情况下,后台会有专门的 GC worker(占 CPU 核数的 25%)在默默进行对象扫描和着色。 但是,当业务 Goroutine 的堆内存分配速率过快,导致后台 GC 线程来不及标记时,Go Runtime 为了防止内存无限膨胀触发 OOM,会启用背压(Backpressure)机制 —— 即 Mark Assist(协助标记)。

    runtime.mallocgc 源码中,如果检测到当前处于 GC mark 阶段且分配信用额度(assist credit)不足,当前的 Goroutine 就会被迫“打工”:

    // runtime/malloc.go 伪代码逻辑
    if gcBlackenEnabled != 0 {
        // 强制业务 Goroutine 参与 GC 标记
        gcAssistAlloc(assistG)
    }
    

    于是,原本应该去处理网络包的业务 Goroutine,被强制抓壮丁去扫描和标记堆上的几百万个 Timer 对象。

    3. GMP 调度器饥饿

    在 GMP 模型中,P(Processor)的本地运行队列(LRQ)中排满了等待执行的 Goroutine。 当大量正在执行的 G 被迫陷入 gcAssistAlloc 这个极其耗时的 CPU 密集型操作时,它们紧紧霸占了 M(OS 线程)。

    • M 被长时间占用,无法执行其他 G。

    • 系统内核态并未陷入阻塞,sysmon 监控线程的抢占机制(基于 10ms 运行时间)虽然会触发,但由于整个系统都在狂跑 GC,切换上下文后新的 G 只要一分配内存,又会立马陷入 Mark Assist

    • 最终结果:有效吞吐量降至冰点,p99 延迟突破天际。

    止血与修复方案

    对于高频循环,严禁在循环体内部直接调用 time.After。 修复方式是典型的防御性编程:使用 time.NewTimer 并在循环中复用(Reset)。

    func processStreamSafe(ch <-chan Msg) {
        // 循环外初始化,只分配一次堆内存
        timer := time.NewTimer(5 * time.Second)
        defer timer.Stop() // 防御性释放
    
        for {
            // 重置定时器前,必须确保 channel 已被抽干,防止死锁或泄露
            if !timer.Stop() {
                select {
                case <-timer.C: 
                default:
                }
            }
            timer.Reset(5 * time.Second)
    
            select {
            case msg := <-ch:
                handle(msg)
            case <-timer.C:
                // 记录超时日志
            }
        }
    }
    

    代码上线后,CPU User 瞬间回落至 15%,gcAssistAlloc 从火焰图中完全消失,p99 延迟稳如死狗。

    通过配置 GODEBUG=gctrace=1 观察修复前后的 GC 表现: 修复前: gc 1234 @10.123s 15%: 0.1+150+0.05 ms clock, 1.2+600/150/0+0.5 ms cpu, 45->46->20 MB, 50 MB goal, 8 P (墙上时钟耗时高达 150ms,且 CPU 时间全砸在了 Mark 阶段)

    修复后: gc 1235 @10.500s 2%: 0.05+2+0.02 ms clock, 0.5+8/2/0+0.1 ms cpu, 15->15->8 MB, 16 MB goal, 8 P (GC 耗时骤降到 2ms 级别,CPU 占用极其平缓)

    排查清单:Go Runtime 性能与 GC 调度异常速查

    1. 火焰图定位协助标记:若 go tool pprof 火焰图中 runtime.gcAssistAllocruntime.gcBgMarkWorker 占据较大宽度(>20%),说明对象分配速率已严重超载,必须排查高频调用的堆内存分配点。

    2. Timer 泄露核查:在 Heap Profiling 的 alloc_objects 视图中,重点排查 time.Aftertime.Tick 或未 Stop 的 time.Ticker。高并发下这些是 GC 杀手。

    3. 大 Map 的扫描开销:如果 GC STW 或 Mark 阶段耗时极长,检查业务中是否存在含有指针的巨型 Map(如 map[string]*Struct)。Go 的 GC 必须扫描这些 Map 里的所有指针。解法是改用非指针结构(如 map[int]Struct)或使用 Slice 下标映射。

    4. GMP 队列阻塞排查:通过 go tool trace 观察 Scheduler latency。如果发现大量的 Goroutine 处于 Runnable 状态但长时间无法转为 Running,除了 GC 抢占外,还需排查是否存在未释放系统线程(runtime.LockOSThread)或大规模阻塞的 CGO 调用。

  • 深入 Zabbix Proxy 雪崩排查:断网恢复引发的 Trapper 耗尽与 MySQL InnoDB 锁死实战

    某次排查过程中,某跨机房专线发生了一次约 15 分钟的抖动,导致该机房的 Zabbix Proxy 节点与中心 Zabbix Server 短暂失联。当专线恢复的瞬间,中心 Zabbix Server 瞬间陷入瘫痪:Load Average 飙升至 120+,监控大盘变成一片空白,所有告警通道静默。最终排查结论极其经典——典型的“重试风暴与并发写入雪崩”。Proxy 在离线期间囤积了海量历史数据,恢复网络后全量、高并发地向 Server 端倾泻。Server 端的 Trapper 进程瞬间耗尽,且底层 MySQL 因未做针对性调优,在海量并发 INSERT 下触发 InnoDB 严重锁等待(Lock wait timeout)与 IO 饱和,最终拖垮了整个监控体系。

    将企业级监控系统当成黑盒跑默认配置,是对生产环境的极度不负责任。监控系统的架构设计同样需要“防御性编程”思维,任何一个下游组件的故障恢复,都不应该成为压垮中心节点的最后一根稻草。

    案发现场:队列爆炸与 DB 假死

    故障发生时,登录中心 Zabbix Server,终端已经非常卡顿。第一时间查看系统指标与核心进程状态:

    1. 系统负载极高,IO 成为瓶颈 执行 top 发现 mysqld 进程 CPU 占用达到 600%,但更致命的是 %wa(IO Wait)高达 45%。

    2. Zabbix Server 内部运行状态崩溃 查看 /var/log/zabbix/zabbix_server.log,满屏的告警日志: text Zabbix server history syncer processes more than 75% busy Zabbix server trapper processes more than 75% busy [Z3005] query failed: [1205] Lock wait timeout exceeded; try restarting transaction [insert into history_uint (itemid,clock,ns,value) values ...] server is out of memory: out of memory (allocating 4294967296 bytes) Trapper(负责接收 Proxy 数据)和 History Syncer(负责将数据刷入 DB)进程全部处于打满状态。

    3. MySQL 锁等待与堆积 直连 MySQL 数据库,执行 SHOW PROCESSLIST,看到上百个状态为 update 的线程,全部卡在写入历史表: sql INSERT INTO history_uint (itemid,clock,ns,value) VALUES ... 查看 SHOW ENGINE INNODB STATUS\G,发现大量的 Record Lock 争用,Buffer Pool 命中率急剧下降,磁盘 IOPS 被写 Redo Log 和 Flush Page 彻底榨干。

    根因剖析:为什么一次断网恢复会引发全局瘫痪?

    问题的核心在于 Zabbix Proxy 的离线缓存机制中心数据库的并发写入能力 极度不匹配。

    在分布式架构中,Zabbix Proxy 在与 Server 断开连接时,会将采集到的监控数据缓存在本地(默认是 SQLite 或轻量 MySQL/PostgreSQL)。当网络恢复,Proxy 会尝试将离线期间积压的数据(通过 DataSenderFrequency 控制周期)打包发送给 Zabbix Server(端口 10051)。

    雪崩的传导链条如下:

    1. Proxy 数据洪峰:一个承载了 10,000 NVPS(每秒新值)的 Proxy,断网 15 分钟会囤积约 900 万条数据。网络恢复后,Proxy 会以极具攻击性的方式将这些数据并发推向 Server。

    2. Trapper 耗尽:Zabbix Server 的 StartTrappers 进程负责接收这些数据,存入内存共享池。面对突发洪峰,Trapper 进程瞬间被全部分配完毕,导致其他正常的 Proxy 或 Agent Active 检查也无法建立连接,监控出现全局断点。

    3. History Syncer 阻塞:内存池迅速填满,StartHistorySyncers 进程开始疯狂从内存中提取数据拼接成 INSERT 语句写入 MySQL。

    4. MySQL InnoDB IO 饱和与锁死: 这是最致命的一环。如果 MySQL 采用了默认的 innodb_flush_log_at_trx_commit = 1,意味着每一次 History Syncer 的批量事务提交,都会强制触发一次 Redo Log 的 fsync 刷盘。 机械硬盘或普通 SSD 的 IOPS 瞬间被击穿。IO 阻塞导致 INSERT 事务执行变慢,持有的行锁或间隙锁迟迟不释放。新的写入请求被阻塞(Lock wait timeout),进而导致 History Syncer 挂起,最终内存池爆满,Zabbix Server 进程直接 OOM 崩溃。

    止血与根治方案:防御性监控架构的落地

    面对这种雪崩,常规的重启服务毫无意义,启动后几秒钟内又会被积压的数据再次击穿。正确的止血操作是:先切断洪峰源头,再提升消化能力。

    紧急止血操作:

    1. 停掉故障节点的 zabbix_proxy 服务。

    2. 重启中心 zabbix_server,让其消化掉内存中和 DB 中积压的残余事务,确保其他机房的监控恢复正常。

    3. 如果 Proxy 积压数据已经失去时效性且无关紧要,直接清空 Proxy 侧本地 DB 的 proxy_history 表(极其暴力的断臂求生,视业务容忍度而定)。

    深度调优与根治配置(必须落地的最佳实践):

    1. MySQL 底层性能释放(关键) 监控数据是典型的“写多读少、允许极少量丢失”的时序数据。严格的 ACID 对 Zabbix 历史表来说是性能毒药。

    # /etc/my.cnf
    # 牺牲极为罕见的 MySQL 宕机(非 OS 宕机)时 1 秒的数据,换取成百上千倍的写入性能提升
    innodb_flush_log_at_trx_commit = 2 
    
    # 将 Buffer Pool 设置为物理内存的 60%-70%,让尽可能多的数据在内存中合并写入
    innodb_buffer_pool_size = 64G 
    
    # 提升 IO 线程数,榨干 NVMe SSD 的并发能力
    innodb_read_io_threads = 16
    innodb_write_io_threads = 16
    innodb_io_capacity = 5000
    innodb_io_capacity_max = 10000
    

    注:对于 TB 级别的企业级 Zabbix,强烈建议对 historyhistory_uint 表实施按天/周的 MySQL Table Partitioning(表分区),利用 DROP PARTITION 替代 Zabbix 自带的 Housekeeper 删除过期数据,这能彻底解决 Housekeeper 引发的数据库 CPU 毛刺死锁。

    2. Zabbix Server 并发与缓冲调优 扩展 Server 的吞吐管道,并增加共享内存以缓冲洪峰。

    # /etc/zabbix/zabbix_server.conf
    # 增加处理 Proxy 数据的 Trapper 进程(视代理数量和内存而定)
    StartTrappers=50
    # 增加历史数据同步进程,加速刷盘
    StartHistorySyncers=30
    # 扩大缓存池,避免瞬间 OOM 或假死
    CacheSize=8G
    HistoryCacheSize=2G
    HistoryIndexCacheSize=512M
    

    3. Proxy 侧的防御性限流 控制离线数据的囤积量和发送频率,避免在恢复时“一波流”带走中心端。

    # /etc/zabbix/zabbix_proxy.conf
    # 离线数据最多保留时长(小时)。断网太久的数据直接丢弃,保全大局
    ProxyOfflineBuffer=2
    # 控制 Proxy 发送数据的频率,避免过高的并发连接
    DataSenderFrequency=1
    

    排查清单:Zabbix 并发写入雪崩同类问题速查

    1. 检查 Zabbix Server 内部队列与进程瓶颈:使用 zabbix_get -s 127.0.0.1 -k "zabbix[process,history syncer,avg,busy]" 获取内部指标,超过 75% 即需警惕 DB 写入瓶颈。

    2. 排查 MySQL InnoDB 锁等待:在 MySQL 执行 SELECT * FROM information_schema.innodb_trx WHERE trx_state = 'LOCK WAIT'; 揪出阻塞源头,通常是 Housekeeper 与 History Syncer 发生了锁冲突。

    3. 确认磁盘 IO 饱和度:使用 iostat -xz 1 观察 %utilawait,如果 %util 长期 100% 且以写 IO 为主,必须立刻调整 innodb_flush_log_at_trx_commit

    4. Housekeeper 负载自查:如果未开启 DB 表分区,检查 zabbix_server.log 中 housekeeper 每次删除数据耗时,若超过 1 分钟,必须调整 MaxHousekeeperDelete 或尽快实施分区改造。

  • 深入 Zabbix 监控雪崩排查:Proxy 积压引发的 History Syncer 阻塞与数据库底层调优实战

    Zabbix 队列风暴的元凶往往不是 Server 计算能力不足,而是底层数据库 IO 瓶颈与 Proxy 离线数据猛灌。本文通过排查某次 NVPS 突发至 35k 导致的 History Syncer 打满与监控瘫痪事件,深入解析 MySQL 8.0 针对 Zabbix 写入优化的底层逻辑,并给出 Proxy 防御性配置与模板预处理过滤的实战方案。

    故障现场:History Syncer 进程 100% 死亡螺旋

    某次排查过程中,一套承载 20,000+ 主机、2,000,000+ 监控项的 Zabbix 6.0.22 LTS 集群突发严重告警。现象非常典型:

    1. Zabbix Queue 爆炸:延迟超过 10 分钟的 item 数量瞬间飙升到 150,000 以上。

    2. 内部进程打满:Zabbix Server 告警 Zabbix server history syncer processes more than 100% busy

    3. 前端瘫痪:Web 界面卡死,报 Zabbix server is not running: the information displayed may not be current

    查看 zabbix_server.log,满屏的慢查询和超时:

    23851:20231012:142211.512 [Z3005] query failed: [1205] Lock wait timeout exceeded; try restarting transaction [insert into history_uint (itemid,clock,ns,value) values (152342,1697101321,213412,0),(...)]
    23851:20231012:142215.123 server #12 active [history syncer #4]
    23851:20231012:142215.123 Zabbix server history syncer processes 100% busy
    

    很明显,数据落盘卡住了。History Syncer 负责将内存 cache 中的监控数据批量写入底层 MySQL。一旦它被阻塞,Server 内存中的 History Cache 会迅速耗尽,触发自我保护机制,拒绝接收任何新数据,最终导致 Poller 和 Trapper 进程全部雪崩。

    登陆底层 MySQL 8.0.34 节点,敲下 iostat -x 1,看到数据盘的 %util 稳稳地锁死在 100%,await 高达 200ms+。

    为什么 Proxy 断网恢复会导致 Zabbix Server 瞬间雪崩?

    排查发现,在雪崩发生前 15 分钟,某跨机房专线发生了短暂抖动。该机房部署了 3 台 Zabbix Proxy(Active 模式),承载了约 8,000 台主机的监控抓取。

    这里牵扯到 Zabbix Proxy 的底层工作机制。Proxy 默认会将采集到的数据暂存在本地的 SQLite3(或 MySQL)中。当与 Server 断开连接时,Proxy 会根据 ProxyOfflineBuffer 的配置(默认 1 小时)在本地堆积数据。

    雪崩的逻辑链条:

    1. 专线抖动,3 台 Proxy 与 Server 失联,期间不断采集并缓存数据到本地。

    2. 专线恢复,Proxy 瞬间将积压的数十万条历史数据打包。

    3. Proxy 根据 DataSenderFrequency=1(每秒发送)无脑向 Server 的 Trapper 进程猛灌。

    4. Server 的 Trapper 进程将海量数据塞入 History Cache。

    5. History Syncer 进程全速运转,向 MySQL 发起天量 INSERT INTO history... 请求。

    6. MySQL InnoDB Buffer Pool 的脏页刷新速率跟不上写入速率,Redo Log 爆满,触发同步刷盘,导致 IO 彻底僵死。

    防御性配置:限制 Proxy 突发流量

    为了防止类似情况再次发生,必须对 Proxy 的回传机制进行限流。

    1. 调优 Proxy 端缓存发送频率与体积 不要让 Proxy 一次性将积压数据全吐出来。在 zabbix_proxy.conf 中调整:

    # ProxyOfflineBuffer=1 # 离线缓存保留时间,不要设置太大,无意义的历史数据宁可丢弃
    DataSenderFrequency=1
    # 增加批量发送的限制(隐式受制于 Server 端 Trapper 进程处理能力)
    

    注:Zabbix 6.0 引入了 Proxy 内存缓存机制,但在面对海量离线回传时,核心仍是保护 Server 的 DB IO。

    2. 调优 Server 端接收与刷盘并发zabbix_server.conf 中调整:

    # 控制 Trapper 进程数,不要无限调大,防止压垮 History Cache
    StartTrappers=50
    # 增加 History Cache 大小,做大内存缓冲池,争取时间
    HistoryCacheSize=2G
    HistoryIndexCacheSize=512M
    # 增加 Syncer 进程数,但不要超过 DB 磁盘阵列的物理 IO 并发能力上限
    StartHistoryPollers=20
    

    数据库底层调优:拯救被压垮的 MySQL 8.0

    Zabbix 是一个典型的“读少写极其密集”的系统,标准的 MySQL 默认配置在这里就是灾难。针对本次 IO 瓶颈,我们在 my.cnf 中进行了以下针对性调优。

    1. 禁用 Doublewrite Buffer 与放宽事务持久性

    由于 Zabbix 数据并非金融级账本,丢失一两秒的监控数据完全可以接受。

    [mysqld]
    # 核心:将事务刷盘策略改为 2。每次提交仅写入 OS Cache,每秒刷盘一次。
    # 直接将 IOPS 需求降低一个数量级。
    innodb_flush_log_at_trx_commit = 2
    
    # 针对支持原子写的存储设备(如现代 NVMe SSD 或部分企业级 SAN),关闭双写缓冲
    innodb_doublewrite = 0
    
    # 优化 Redo log 大小,防止频繁触发 Checkpoint 导致 IO 抖动
    innodb_redo_log_capacity = 4G # MySQL 8.0.30+ 的新参数,替代旧的 innodb_log_file_size
    

    2. 匹配硬件的 InnoDB IO 刷盘能力

    MySQL 默认假设你的磁盘很慢(innodb_io_capacity=200),这会导致在 SSD 环境下脏页刷得太慢,最终堆积引发急剧抖动。

    # 根据实际 FIO 测试结果配置
    innodb_io_capacity = 3000
    innodb_io_capacity_max = 6000
    innodb_flush_sync = OFF # 避免 Checkpoint 时卡死用户查询
    

    3. 表分区与废弃 Housekeeper

    导致底层 IO 缓慢的另一个隐患是 Zabbix 自带的 Housekeeper 清理进程。它通过 DELETE FROM history WHERE clock < ... 清理过期数据,这在海量数据下会产生巨大的锁竞争和 Undo Log 开销。

    必须彻底关闭 Housekeeper 对历史表和趋势表的清理: Web 界面:Administration -> General -> Housekeeping,关闭 History and TrendEnable internal housekeeping

    替代方案:使用 MySQL 表分区(Table Partitioning)。 每天为 historyhistory_uinthistory_str 等表建一个新分区,清理数据时直接 ALTER TABLE ... DROP PARTITION,这是元数据操作,耗时 0.1 秒,没有任何 IO 负担。

    自定义模板防作死指南:在 Proxy 侧掐断垃圾数据

    数据库调优只是续命,真正的治本之策是降低 NVPS(New Values Per Second)。排查中发现,某开发团队的自定义模板中,有一个抓取应用日志错误状态的 item,类型居然是 Text,且每 5 秒抓取一次。无论状态是否改变,全量文本都在往 DB 里塞,直接打爆了 history_str 表。

    过滤绝招:Discard unchanged with heartbeat

    利用 Zabbix 的 Preprocessing(预处理)功能,直接在 Proxy 内存中过滤掉无用数据,根本不让它通过网络发给 Server。

    配置步骤:

    1. 打开 Item 的 Preprocessing 选项卡。

    2. 添加 Step:Discard unchanged with heartbeat

    3. 参数设置为 1h(或 3600s)。

    原理解析: 如果该 Item 的值相比上次抓取没有发生变化,Proxy 会直接将这个数据丢弃,不往 Server 发送。只有当值发生变化,或者超过设定的 heartbeat 时间(比如 1 小时没有变化),才会发送一次数据保持激活。 仅仅配置了这一项,我们的整体 NVPS 从 35,000 直接断崖式下降到 12,000,MySQL IO 负载瞬间降至 15% 以下。

    常见问题

    Q1:Web 界面经常报 Zabbix Server is not running,但查看进程都在,怎么回事? 通常是因为 PHP 前端通过 TCP 10051 端口请求 Server 的 StartTrappers 进程超时。大概率是因为 History Syncer 阻塞,导致 Trapper 进程全都在等待获取 Cache 锁。检查数据库负载,或适当增加 StartTrappers 数量。

    Q2:StartPollers 到底设置多少合适?为什么我设了 1000 还是不够? 千万不要无脑调大 Poller 数量。Poller 过多会导致严重的上下文切换和内存消耗。查看 Zabbix server data collector processes 图表,如果 Poller 使用率长期 > 75%,首先应该考虑将监控项改为 Active 模式(让 Agent 主动推),或者把采集任务剥离给下层 Proxy。

    Q3:存在大量 SNMP 监控导致 Poller 经常超时卡死,如何缓解? SNMP 采用 UDP,极易丢包阻塞。最佳实践:1) 将 SNMP 采集全部下放给专属的 Zabbix Proxy,将故障隔离;2) 在 Host 级别勾选 Use bulk requests;3) 在 zabbix_server.confzabbix_proxy.conf 中增加 Timeout=15(默认只有 3 秒,对于老旧交换机绝对不够)。