裸金属服务器 2Gbps 带宽劣化排障实施手册（Opus4.6）
裸金属服务器 2Gbps 带宽劣化排障实施手册定位：公开版实施操作手册。目标：任何人或 AI 只读这一篇，就能在相同场景下完成诊断、修复、验证和持久化，并避开已确认的坑。 场景：裸金属服务器（非虚拟化），公网 IP 直发（非 NAT），2Gbps 出口带宽随连接数增加出现抖动/下降。 实测环境：Debian 12 + xanmod 6.x 内核, 双路 Xeon (72 逻辑核), 251GB RAM, ixgbe 10GbE 单口 + VLAN 子接口, 中继/转发类业务 ~4 万并发连接。 ⚠️ 安全声明：本文所有 IP、端口、密码、密钥、节点名均为占位符。请替换为你自己的真实值，不要将真实值提交到公开仓库。〇、占位符对照表在执行本手册前，将以下占位符全局替换为你的实际值：占位符含义示例格式<SERVER_IP>服务器公网管理 IP198.51.100.10<SSH_PORT>SSH 端口22<SSH_USER>SSH 用户名root<SSH_PASS>SSH 密码your-password<SSH_HOSTKEY>SSH 主机密钥指纹ssh-ed25519 255 SHA256:xxxxx<SSH_KEY_PATH>本地 SSH 私钥路径D:\keys\id_rsa<NIC>物理网卡名enp130s0 / eth0 / ens5<VLAN_IF>VLAN 子接口名 (如有)enp130s0.100<BIZ_IP_A>业务绑定 IP (主)198.51.100.20<BIZ_IP_B>业务绑定 IP (副, 如有)198.51.100.21<GW_A>策略路由网关 A198.51.100.1<APP_BIN>业务进程二进制名my_relay_server<APP_PID>业务进程 PID12345<INSTANCE_A>业务实例名 Arelay-primary<INSTANCE_B>业务实例名 Brelay-secondaryWindows 端 SSH 辅助函数 (PowerShell + plink)$HK = '<SSH_HOSTKEY>' function rvps($c) { plink -ssh <SSH_USER>@<SERVER_IP> -P <SSH_PORT> -hostkey $HK -pw '<SSH_PASS>' -batch $c 2>&1 }坑 #1 (PowerShell 转义)：plink 传递复杂 bash 命令时，$() / $(()) / awk '{}' 会被 PowerShell 解析。对超过一行的命令，必须用 here-string 写成脚本 → 管道上传 → 远程执行，不要试图在 rvps() 的引号里硬塞。示例见「上传脚本模式」。上传脚本模式$script = @' #!/bin/bash echo "hello from remote" whoami '@ $script | plink -ssh <SSH_USER>@<SERVER_IP> -P <SSH_PORT> -hostkey $HK -pw '<SSH_PASS>' -batch "cat > /tmp/myscript.sh && chmod +x /tmp/myscript.sh && echo ok" # 然后执行 rvps "bash /tmp/myscript.sh"一、诊断阶段（只读，不改任何东西）1.1 环境摸底uname -a # 内核版本 lscpu # CPU 拓扑、NUMA 节点 free -h # 内存 ip link show # 网卡列表 → 记录 <NIC> 和 <VLAN_IF> ip addr show # 确认 IP 绑定 ip rule show # 策略路由 ip route show table all | head -50确认业务进程：ps aux | grep <APP_BIN> ls /proc/<APP_PID>/fd | wc -l # fd 数 cat /proc/<APP_PID>/status | grep -E 'Threads|VmRSS' top -bn1 -p <APP_PID> | tail -1 # CPU% ss -s # 连接总览1.2 网卡硬件层ethtool <NIC> # 链路速率 ethtool -i <NIC> # 驱动名+版本 ethtool -g <NIC> # ★ Ring Buffer 当前值 vs 最大值 ethtool -S <NIC> | grep -iE 'drop|error|miss|over|fifo' # ★ 硬件丢包计数 ethtool -l <NIC> # 多队列数 ethtool -c <NIC> # coalesce判断矩阵：指标正常异常 → 对应修复rx_missed_errors 持续增长0 或极低→ Fix 1: Ring BufferRing Buffer 当前值 远小于 最大值差距 < 2x→ Fix 1: Ring Bufferrx_dropped 持续增长0→ 检查 netdev_max_backlog1.3 内核网络栈# TCP 缓冲区 sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem sysctl net.core.rmem_max sysctl net.core.wmem_max sysctl net.ipv4.tcp_mem # 队列与连接 sysctl net.core.somaxconn sysctl net.core.netdev_max_backlog sysctl net.core.netdev_budget sysctl net.core.netdev_budget_usecs # 拥塞控制 sysctl net.ipv4.tcp_congestion_control # conntrack (如在用) cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null1.4 softirq 与中断分布# IRQ 亲和性 cat /proc/interrupts | grep <NIC> | head -10 # 每个 IRQ 绑定了哪些 CPU？ for irq in $(grep <NIC> /proc/interrupts | awk -F: '{print $1}' | head -5); do echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity_list)" done # softnet_stat — 全局快照 cat /proc/net/softnet_stat | head -10 # time_squeeze 10 秒增量 S1=$(cat /proc/net/softnet_stat); sleep 10; S2=$(cat /proc/net/softnet_stat) # 对比第三列 (squeeze) 的变化 # irqbalance 状态 systemctl is-active irqbalanceNUMA 感知判断：# NIC 在哪个 NUMA 节点? cat /sys/class/net/<NIC>/device/numa_node # 该节点对应哪些 CPU? lscpu | grep "NUMA"坑 #2 (IRQ 亲和性误判)：如果 NIC 在 NUMA node 1，IRQ 绑定到 node 1 的 CPU（如全部奇数核）是 正确的 NUMA-local 策略，不是"分布不均"。不要盲目把 IRQ 打散到所有核——跨 NUMA 访问内存会增加延迟。1.5 RPS/RFS 状态cat /sys/class/net/<NIC>/queues/rx-0/rps_cpus cat /sys/class/net/<VLAN_IF>/queues/rx-0/rps_cpus 2>/dev/null cat /proc/sys/net/core/rps_sock_flow_entries如果 rps_cpus 全零 → RPS 未启用，所有 softirq 仅在硬中断所在 CPU 处理。1.6 TCP 异常计数器（关键指标）nstat -az | grep -E 'TcpExtTCPRcvCollapsed|TcpExtPruneCalled|TcpExtListenDrops|TcpExtTCPAbortOnData|TcpRetransSegs'10 秒采样：C1=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') sleep 10 C2=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') echo "delta=$((C2-C1)) per_sec=$(( (C2-C1)/10 ))"计数器含义增长率 > 0 意味着TcpExtTCPRcvCollapsed内核合并接收缓冲区 skb应用 recv() 读取不够快TcpExtPruneCalled内核主动丢弃接收缓冲区数据比 Collapsed 更严重TcpExtListenDrops监听队列溢出somaxconn / backlog 不够TcpRetransSegsTCP 重传网络质量问题或对端处理慢1.7 定时任务与杂项crontab -l # 任何 drop_caches / clean_mem ? dmesg -T | grep drop_caches | tail -5 # 曾触发过 drop_caches? systemctl --failed # 有没有失败的服务 dmesg -T | grep -iE 'error|oom|panic|call trace' | tail -201.8 VLAN 子接口队列数ls /sys/class/net/<VLAN_IF>/queues/ 2>/dev/null坑 #3 (VLAN 单队列)：VLAN 子接口通常只有 1 个 rx 队列，即使物理网卡有 63 个队列。所有子接口流量只由一个 softirq 上下文处理。这就是为什么 RPS 对 VLAN 场景尤其重要——它是把单队列 softirq 负载分散到多核的唯一途径。二、诊断结论模板完成第一阶段后，填写此表：#问题严重性证据1Ring Buffer 过小，rx_missed_errors 持续增长★★★ / 无ethtool -g / ethtool -S 值2TCPRcvCollapsed 持续增长★★★ / 无nstat 10s 采样值3softirq time_squeeze★★ / 无/proc/net/softnet_stat 值4RPS/RFS 禁用★★ / 无rps_cpus 值5定时 drop_caches★ / 无crontab 内容6conntrack 表满★★★ / 无count vs max7其他 三、修复操作（按优先级排序）Fix 1: 扩大 Ring Buffer（消除硬件丢包）前提条件：ethtool -g <NIC> 显示当前值远小于最大值，且 rx_missed_errors 持续增长。# 记录修复前基线 ethtool -S <NIC> | grep rx_missed # 执行 (一条命令, 立即生效, 不中断流量) ethtool -G <NIC> rx 8192 tx 8192 # 验证 ethtool -g <NIC> # 确认 Current = 8192 ethtool -S <NIC> | grep rx_missed # 记录值 # 30 秒后再查一次，确认增量 = 0 sleep 30 ethtool -S <NIC> | grep rx_missed风险：极低。热生效，无需中断网络。 持久化：重启后回退！需要 systemd service：cat > /etc/systemd/system/nic-ring-buffer.service << 'EOF' [Unit] Description=Set NIC ring buffer size After=network-pre.target Before=network.target [Service] Type=oneshot ExecStart=/usr/sbin/ethtool -G <NIC> rx 8192 tx 8192 RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable nic-ring-buffer.serviceFix 2: 启用 RPS/RFS（softirq 多核分发）前提条件：rps_cpus 全零，或 VLAN 子接口仅 1 个 rx 队列。CPU 掩码计算：# 获取 CPU 数 nproc # 例如 72 # 72 个 CPU 全覆盖的掩码: # 72 bit = 9 bytes = 0xff,0xffffffff,0xffffffff # 通用公式: python3 -c "n=72; h=hex((1<<n)-1)[2:]; print(','.join([h[max(0,len(h)-8*(i+1)):len(h)-8*i] for i in range(len(h)//8,-1,-1) if h[max(0,len(h)-8*(i+1)):len(h)-8*i]]))"# 物理网卡所有 rx 队列 for rxq in /sys/class/net/<NIC>/queues/rx-*/rps_cpus; do echo "ff,ffffffff,ffffffff" > "$rxq" done # VLAN 子接口 (如有) for rxq in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_cpus; do echo "ff,ffffffff,ffffffff" > "$rxq" done # RFS 全局表 echo 65536 > /proc/sys/net/core/rps_sock_flow_entries # RFS 每队列表 — 物理网卡 NRXQ=$(ls -d /sys/class/net/<NIC>/queues/rx-* | wc -l) PER_Q=$((65536 / NRXQ)) for rxq in /sys/class/net/<NIC>/queues/rx-*/rps_flow_cnt; do echo "$PER_Q" > "$rxq" done # RFS 每队列表 — VLAN 子接口 NRXQ_V=$(ls -d /sys/class/net/<VLAN_IF>/queues/rx-* 2>/dev/null | wc -l) if [ "$NRXQ_V" -gt 0 ]; then PER_Q_V=$((65536 / NRXQ_V)) for rxq in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_flow_cnt; do echo "$PER_Q_V" > "$rxq" done fi验证：# 确认配置写入 cat /sys/class/net/<NIC>/queues/rx-0/rps_cpus cat /sys/class/net/<VLAN_IF>/queues/rx-0/rps_cpus cat /proc/sys/net/core/rps_sock_flow_entries # 30 秒后检查 softnet_stat — 之前全零的偶数 CPU 应该开始有 processed 计数 # (用 /proc/net/softnet_stat 第一列, 每行一个 CPU)风险：极低。热生效，如果有问题写回 00000000 即可还原。 持久化：重启后回退！systemd service：cat > /etc/systemd/system/nic-rps-rfs.service << 'EOF' [Unit] Description=Enable RPS/RFS on all NIC queues After=network-pre.target Before=network.target [Service] Type=oneshot ExecStart=/bin/bash -c '\ for q in /sys/class/net/<NIC>/queues/rx-*/rps_cpus; do echo ff,ffffffff,ffffffff > "$q"; done; \ for q in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_cpus 2>/dev/null; do echo ff,ffffffff,ffffffff > "$q"; done; \ echo 65536 > /proc/sys/net/core/rps_sock_flow_entries; \ for q in /sys/class/net/<NIC>/queues/rx-*/rps_flow_cnt; do echo 1024 > "$q"; done; \ for q in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_flow_cnt 2>/dev/null; do echo 32768 > "$q"; done' RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable nic-rps-rfs.serviceFix 3: IRQ 亲和性 / softirq 预算（按需）先判断是否需要：systemctl is-active irqbalance # 如果 active → 已在自动平衡 cat /sys/class/net/<NIC>/device/numa_node # NIC 的 NUMA 节点坑 #4 (盲目调整 IRQ)：如果 NIC 在 NUMA node 1，irqbalance 把 IRQ 绑到 node 1 CPU 是 最优策略。不要强制打散到所有 CPU。跨 NUMA 内存访问比不均衡更致命。如果 irqbalance 未运行：apt install -y irqbalance systemctl enable --now irqbalancesoftirq 预算（仅在 time_squeeze 持续增长时才调）：# 先看当前值 sysctl net.core.netdev_budget sysctl net.core.netdev_budget_usecs # 如果 budget < 600 或 budget_usecs < 4000, 考虑提高 # 但如果已经 >= 1000/8000, 不要动坑 #5 (盲目调参)：我们实测发现服务器已有 netdev_budget=1000, budget_usecs=8000（远高于常见建议的 600/4000）。永远先查当前值再决定是否修改。 预设方案的值可能比线上低——"优化"反而是降级。Fix 4: TCP 缓冲区调整（通常已够大）坑 #6 (最大的教训)：我们在实测中发现服务器的 TCP 缓冲区已经是 32MB（而预设方案假设是 4MB 默认值），差点把 32MB 降到 16MB。必须先 sysctl net.ipv4.tcp_rmem 查看当前值，如果已经 >= 预设目标，不要碰。# 先看当前值! sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem sysctl net.core.rmem_max sysctl net.core.wmem_max sysctl net.ipv4.tcp_mem仅当当前值明显偏小才执行：sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216" sysctl -w net.ipv4.tcp_wmem="4096 32768 16777216" sysctl -w net.core.rmem_max=16777216 sysctl -w net.core.wmem_max=16777216回滚（如果不小心执行了）：# 立刻恢复原值 (替换为你之前记录的值) sysctl -w net.ipv4.tcp_rmem="4096 131072 33554432" sysctl -w net.ipv4.tcp_wmem="4096 131072 33554432" sysctl -w net.core.rmem_max=33554432 sysctl -w net.core.wmem_max=33554432持久化：写入 /etc/sysctl.d/99-tcp-tuning.conf，然后 sysctl --system。Fix 5: 停止定时 drop_caches# 查找触发源 crontab -l | grep -i 'drop_caches\|clean_mem' grep -rl 'drop_caches' /etc/cron* /var/spool/cron/ 2>/dev/null systemctl list-timers | grep -i clean # 注释掉 crontab 条目 crontab -e # 在对应行前加 ## 注释 # 验证 dmesg -T | grep drop_caches | tail -3 # 记录最后一次时间 # 等 20 分钟后再查，如果没有新的 → 修复成功为什么不该 drop_caches：在 251GB RAM、仅用 10GB 的机器上，drop_caches 会强制释放所有 page cache/dentries/inodes，导致后续文件 I/O 全部 cache miss。对网络密集型中继服务来说，周期性 cache 抖动 = 周期性延迟毛刺。四、修复后综合验证4.1 快速验证脚本#!/bin/bash echo "=== 1. Ring Buffer ===" ethtool -g <NIC> | grep -A4 "Current" echo "=== 2. rx_missed_errors (应为 0 增长) ===" V1=$(ethtool -S <NIC> | awk '/rx_missed_errors/{print $2}') sleep 10 V2=$(ethtool -S <NIC> | awk '/rx_missed_errors/{print $2}') echo "before=$V1 after=$V2 delta=$((V2-V1))" echo "=== 3. RPS ===" cat /sys/class/net/<NIC>/queues/rx-0/rps_cpus cat /sys/class/net/<VLAN_IF>/queues/rx-0/rps_cpus 2>/dev/null cat /proc/sys/net/core/rps_sock_flow_entries echo "=== 4. time_squeeze (10s delta) ===" T1=0 while read line; do hex=$(echo "$line" | cut -c19-26) val=$(printf "%d" "0x$hex" 2>/dev/null) T1=$((T1+val)) done < /proc/net/softnet_stat sleep 10 T2=0 while read line; do hex=$(echo "$line" | cut -c19-26) val=$(printf "%d" "0x$hex" 2>/dev/null) T2=$((T2+val)) done < /proc/net/softnet_stat echo "squeeze_delta_10s=$((T2-T1))" echo "=== 5. TCPRcvCollapsed (10s delta) ===" C1=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') sleep 10 C2=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') echo "collapsed_delta_10s=$((C2-C1)) per_sec=$(( (C2-C1)/10 ))" echo "=== 6. drop_caches ===" dmesg -T | grep drop_caches | tail -1 echo "=== 7. General ===" uptime free -h | head -2 ss -s坑 #7 (mawk vs gawk)：Debian 默认 awk 是 mawk，不支持 strtonum()。解析 /proc/net/softnet_stat 的十六进制时，用 cut + printf "%d" "0x$hex" 替代。上面的脚本已经规避了此问题。4.2 验证通过标准指标预期未通过则rx_missed_errors 10s 增量0检查 Ring Buffer 是否生效time_squeeze 10s 增量< 10检查 RPS 和 netdev_budgetRPS cpumask非全零重新写入 rps_cpusdrop_caches 最后时间20 分钟前以上检查 crontabTCPRcvCollapsed 速率观测记录即可这是应用层问题，见第五章五、应用层瓶颈识别（TCPRcvCollapsed）如果内核层修复全部到位、rx_missed=0、squeeze≈0，但 TCPRcvCollapsed 仍持续增长（如 >1000/s） → 几乎可以确定是应用层处理跟不上。5.1 证据采集# 进程画像 echo "=== Process profile ===" cat /proc/<APP_PID>/status | grep -E 'Threads|VmRSS|VmSize' ls /proc/<APP_PID>/fd | wc -l top -bn1 -p <APP_PID> | tail -1 # socket 队列分析 echo "=== Recv-Q top 10 ===" ss -tn | awk 'NR>1 && $2>0' | sort -k2 -rn | head -10 echo "=== Send-Q top 10 ===" ss -tn | awk 'NR>1 && $3>0' | sort -k3 -rn | head -10 # 连接分布 ss -tn | awk 'NR>1{split($4,a,":"); print a[1]}' | sort | uniq -c | sort -rn | head -105.2 判断逻辑数据路径： 网卡 → Ring Buffer → 内核 softirq → TCP 协议栈 → socket recv 队列 → 应用 recv() ✅ Fix1 ✅ Fix2/3 ✅ Fix4 ⚠️ 瓶颈在此 ← 应用层如果 TCP 参数已到极致（32MB rmem、somaxconn >100K）、硬件层不丢包、softirq 不 squeeze，那 TCPRcvCollapsed 持续增长 只有一个可能：应用没来得及调 recv()，内核被迫合并 skb 腾空间。5.3 典型应用层瓶颈特征特征含义线程数 << 连接数/100每线程负担的连接过多CPU < 200% 但连接 > 2 万不是 CPU 瓶颈，是 I/O 调度受限RSS 恒定不变没有内存泄漏，但也没动态扩展缓冲Send-Q 积压 > 10MB下游拉不走 → write 阻塞 → 线程被占 → 其他连接 recv 延迟级联效应：少量慢客户端 Send-Q 满 → 该连接 write 阻塞 → 线程被占 → 同线程其他连接 recv 延后 → Recv-Q 积压 → TCPRcvCollapsed。5.4 给应用开发者的建议增加处理并行度：139 线程 / 38K 连接 → 273 连接/线程，偏高。72 核机器可支撑 500+ 线程检查阻塞点：如果 CPU < 200%，说明线程在等 I/O，非阻塞读写分离可能缺失连接级流控：对 Send-Q 持续积压的慢速下游设超时或限速多实例部署：拆端口到多进程，各自 CPU 绑定六、压力测试监测模板用 iperf3 做高压测试时，同步监测系统指标：#!/bin/bash ROUNDS=8 INTERVAL=30 NIC="<NIC>" BM=$(ethtool -S $NIC | awk '/rx_missed_errors/{print $2}') BD=$(ethtool -S $NIC | awk '/rx_dropped/{print $2}') BC=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') BP=$(nstat -az TcpExtPruneCalled | awk 'NR==2{print $2}') BR=$(nstat -az TcpRetransSegs | awk 'NR==2{print $2}') echo "BASELINE: missed=$BM dropped=$BD collapse=$BC prune=$BP retrans=$BR" for ((i=1; i<=ROUNDS; i++)); do echo "==== ROUND $i $(date +%H:%M:%S) ====" # 流量速率 RX1=$(cat /sys/class/net/$NIC/statistics/rx_bytes) TX1=$(cat /sys/class/net/$NIC/statistics/tx_bytes) RP1=$(cat /sys/class/net/$NIC/statistics/rx_packets) sleep $INTERVAL RX2=$(cat /sys/class/net/$NIC/statistics/rx_bytes) TX2=$(cat /sys/class/net/$NIC/statistics/tx_bytes) RP2=$(cat /sys/class/net/$NIC/statistics/rx_packets) echo "traffic: rx=$(( (RX2-RX1)*8/INTERVAL/1000000 ))Mbps tx=$(( (TX2-TX1)*8/INTERVAL/1000000 ))Mbps rx_pps=$(( (RP2-RP1)/INTERVAL ))" # NIC 错误 (与基线比) CM=$(ethtool -S $NIC | awk '/rx_missed_errors/{print $2}') CD=$(ethtool -S $NIC | awk '/rx_dropped/{print $2}') echo "nic: rx_missed=$CM(+$((CM-BM))) rx_dropped=$CD(+$((CD-BD)))" # TCP 计数器 CC=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') CP=$(nstat -az TcpExtPruneCalled | awk 'NR==2{print $2}') CR=$(nstat -az TcpRetransSegs | awk 'NR==2{print $2}') echo "tcp: collapse=$CC(+$((CC-BC))) prune=$CP(+$((CP-BP))) retrans=$CR(+$((CR-BR)))" # 系统 echo "load: $(cat /proc/loadavg)" echo "mem_avail: $(awk '/MemAvailable/{print $2}' /proc/meminfo)kB" ss -s | head -2 echo done echo "==== TOTAL DELTA ====" FM=$(ethtool -S $NIC | awk '/rx_missed_errors/{print $2}') FD=$(ethtool -S $NIC | awk '/rx_dropped/{print $2}') FC=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}') echo "rx_missed: +$((FM-BM)) rx_dropped: +$((FD-BD)) collapse: +$((FC-BC))"七、持久化检查表操作重启后保留？持久化方式Ring Buffer 8192❌systemd service (见 Fix 1)RPS/RFS❌systemd service (见 Fix 2)TCP sysctl 参数✅/etc/sysctl.d/*.confnetdev_budget sysctl✅/etc/sysctl.d/*.confdrop_caches 停止✅crontab 已注释irqbalance✅systemctl enable必须确认：ethtool -G 和 rps_cpus 写入在 /sys 下的虚拟文件系统，重启即丢失。如果不做 systemd service，每次重启后必须手动重新执行。八、已确认的坑汇总#坑后果正确做法1PowerShell 里 rvps() 传复杂 bash$() 被 PS 解析，` ` 被拆分写成脚本 → pipe 上传 → 远程执行2IRQ 只在奇数核 = "分布不均"强行打散 = 跨 NUMA 降性能先查 numa_node，IRQ 在同 NUMA 是正确的3VLAN 子接口仅 1 个 rx 队列所有子接口流量挤一个 softirq用 RPS 分散（Fix 2）4不看当前值就调 TCP 参数32MB 降到 16MB = 降级永远先 sysctl 查当前值5netdev_budget 按文章写 600服务器已经 1000，写 600 = 降级同上，先查再调6mawk 没有 strtonum()脚本静默失败或输出 0用 printf "%d" "0x$hex" 替代7nohup cmd > file 2>&1 & 在 PowerShell SSH 中重定向被 PS 解析整条命令包在单引号里，或走上传脚本模式8TCPRcvCollapsed ≠ 内核问题在内核参数已极致的前提下，唯一原因是应用 recv() 慢排查应用层而非继续调内核9iperf3 压测期间 Collapsed 倍增正常现象：额外流量加重应用负担关注 rx_missed 和 squeeze，不要被 Collapsed 涨了吓到10ethtool -G / echo > rps_cpus 重启后丢失问题"复发"必须做 systemd service 持久化九、实测数据快照（本次行动）以下数据供参考对比，数值会因环境不同而异。修复前 vs 修复后指标修复前修复后变化Ring Buffer5128192×16rx_missed_errors 增量/30s数千~数万0归零time_squeeze 增量/30s数百<10近零RPS even CPU processed/30s0~12,000从零到分担负载TCPRcvCollapsed/s (空闲)~7,400~7,400无变化 (应用层)TCPRcvCollapsed/s (iperf3 压测)—~30,000高压下倍增 (预期)iperf3 高压测试期间 (8 轮 × 30s)指标结果平均流量rx ≈ 2.7 Gbps, tx ≈ 2.7 Gbpsrx_missed_errors+0 (全程零增长)rx_dropped+42 (极少, 可忽略)TCPRcvCollapsed+2,060,664 (应用层, 预期)TcpRetransSegs+416,540系统负载2.1~3.4 (72 核, 完全正常)内存241 GB 可用 (无压力)结论：内核/网卡层修复在高压下完全稳定。rx_missed 全程归零，squeeze 近零。TCPRcvCollapsed 增长仅反映应用层在高压下更来不及消费数据。十、故障排查决策树带宽下降/抖动 ├── ethtool -S <NIC> | grep rx_missed → 持续增长？ │ ├── 是 → Fix 1: Ring Buffer │ └── 否 ↓ ├── /proc/net/softnet_stat 第 3 列 → squeeze 持续增长？ │ ├── 是 → 检查 IRQ 亲和性 + RPS → Fix 2/3 │ └── 否 ↓ ├── nstat TcpExtTCPRcvCollapsed → 持续增长？ │ ├── 是 → 内核 TCP 参数已经很高？ │ │ ├── 否 → Fix 4: 调大 tcp_rmem 等 │ │ └── 是 → 应用层瓶颈 → 第五章 │ └── 否 ↓ ├── nstat TcpExtListenDrops → 持续增长？ │ ├── 是 → 调大 somaxconn / backlog │ └── 否 ↓ ├── conntrack count ≥ 80% max？ │ ├── 是 → 调大 conntrack_max + hashsize │ └── 否 ↓ ├── tc qdisc show → 有 tbf/htb 限速？ │ ├── 是 → 调整或移除 │ └── 否 ↓ └── dmesg | grep -i error → 硬件故障？ ├── 是 → 联系供应商 └── 否 → 抓包分析具体连接手册版本: 2026-04-06 v1.0 — 基于火山引擎裸金属实测
RouterOS 多节点 VPN 出口方案公开版：L2TP/IPsec、IKEv2、WireGuard 接入，广州中转，香港默认出口，可切日本出口这是一份公开版实施规范，不是凭据备份。目标只有一个：让人或 AI 只读这一篇，就能在相同架构下完成部署、验证、切换和排障，并避开已经确认过的坑。本文所有公网地址、端口、账号、PSK、密钥、节点名都必须替换为你自己的真实值；不要把真实值公开到网站。1. 文档目标本文描述的是一套三层结构的 VPN 出口网络：广州 RouterOS 负责接入。香港 RouterOS 作为默认互联网出口。日本 Debian 作为可选出口，由香港侧开关切换。需要同时满足以下要求：支持 L2TP/IPsec、IKEv2/IPsec、WireGuard 三种接入方式。客户端接入广州后，默认经广州转发到香港出网。可以按协议分别把出口从香港切换到日本。可以为少量目标地址做“广州直出”例外，不跟随默认香港出口。后续 AI 执行同类任务时，只要读到这篇文档，就知道应该先做什么、哪里最容易错、错了看什么。2. 适用场景本文适用于以下场景：RouterOS v7。至少两台 RouterOS 节点，其中广州节点负责 VPN 接入，香港节点负责默认出口。可选第三个日本节点，系统为 Debian，作为 WireGuard 出口。广州和香港之间同时具备公网与数据中心内网可达性。需要把远程接入客户端的全流量导出到香港或日本。需要明确记录实施顺序、验证步骤和排障决策，降低 AI 自行脑补设计的空间。本文不直接覆盖以下场景：纯站点到站点 IPsec。证书版 IKEv2。ROS v6 老版本。只做单节点本地拨号、不经过中转节点的简单 VPN。3. 先给结论：这套方案里必须遵守的规则这部分最重要，后续实施和排障都以它为准。3.1 RouterOS v7 上，源地址策略路由优先用 routing rule，不要优先用 mangle mark-routing如果你做的是“指定客户端、指定地址池、指定 LAN 主机走某条 VPN 出口”，在 RouterOS v7 上优先使用：routing table/ip route/routing rule不要把 mangle mark-routing 当成默认方案。原因很简单：它更容易引入局域网访问异常。它更容易引入回程非对称。在 L2TP/IPsec 这类隧道场景下，可能出现“小包能过，大流量极差，上传为 0，网页半残”的问题。如果你已经遇到这些现象，先停掉 mangle PBR，再改成 routing rule，通常比继续给 mangle 打补丁更接近根因。3.2 如果数据中心内网不转发 10.x / 172.x 之外的业务源地址，不要直接在 GZ 和 HK 之间裸跑客户端地址池如果广州和香港之间的内网只愿意转发它自己认可的源/目的地址，而你的客户端地址池是独立的 10.255.x.x，那么最稳妥的做法不是强行让这些地址穿过内网，而是：在 GZ 和 HK 之间先建立一条 WireGuard 传输隧道。隧道外层用数据中心认可的内网地址通信。客户端地址池作为隧道内层载荷传输。3.3 L2TP 和 IKEv2 共存时，IKEv2 必须用独立命名的 IPsec 对象不要为了 IKEv2 去乱改 RouterOS 的 default profile、default proposal、default peer。L2TP 的 use-ipsec=yes 会依赖默认 IKEv1 逻辑。正确做法是：L2TP 继续使用 RouterOS 默认的 L2TP/IPsec 机制。IKEv2 使用独立命名的 profile、proposal、peer、identity、mode-config、policy group。3.4 每个出口点都必须有对应的 NAT只要某个地址池可能从某个接口出互联网，就必须在那个出口接口上为该地址池准备 srcnat/masquerade。不能只在接入侧做 NAT。3.5 MTU 与 MSS 的处理顺序不能反正确顺序是：先确认路由正确。再确认 NAT 正确。再确认回程正确。最后再调 MTU/MSS。如果路由本身是错的，单纯把 MSS 一路压到 1200 也救不回来。3.6 RouterOS WireGuard peer 的 allowed-address 既是允许列表，也是回程路由线索只要返回流量需要经过某个 peer，那个 peer 的 allowed-address 就必须覆盖需要回去的地址池。否则会出现握手有了、单向通、回包丢失这类问题。4. 公开文档里必须保留的变量，而不是必须公开的秘密公开版文档不应该包含真实凭据，但必须把实施所需的变量列全。后续 AI 或运维执行时，先收集这些变量，再替换到模板里。变量说明GZ_PUBLIC_IP广州节点公网地址GZ_PUBLIC_GW广州节点公网网关GZ_DC_IP广州节点数据中心内网地址GZ_DC_GW广州节点数据中心内网网关HK_PUBLIC_IP香港节点公网地址HK_PUBLIC_GW香港节点公网网关HK_DC_IP香港节点数据中心内网地址HK_DC_GW香港节点数据中心内网网关JP_PUBLIC_IP日本节点公网地址JP_PUBLIC_GW日本节点公网网关L2TP_POOLL2TP 客户端地址池IKEV2_POOLIKEv2 客户端地址池WG_LOCAL_POOL本地 WireGuard 接入地址池WG_GZ_HK_TRANSITGZ 和 HK 之间的 WireGuard 传输网段WG_HK_JP_TRANSITHK 和 JP 之间的 WireGuard 传输网段VPN_DNS下发给客户端的 DNSDIRECT_VIA_GZ_LIST需要广州直出的目标地址列表FORCE_HK_LOCAL_SUBNET即使切到日本也必须继续从香港直出的网段SSH_PORT远程 SSH 端口WINBOX_PORTWinbox 端口如果这些变量没有收集完整，AI 不应该直接硬写配置，而应该先补采集。5. 推荐的逻辑拓扑远程客户端 ├─ L2TP/IPsec ├─ IKEv2/IPsec └─ WireGuard │ ▼ GZ RouterOS 接入、地址分配、策略路由汇聚 │ │ WireGuard 传输隧道（优先用 DC 内网地址做 endpoint） ▼ HK RouterOS 默认互联网出口 │ │ WireGuard 传输隧道 ▼ JP Debian 可选互联网出口这套结构里，广州不是默认互联网出口，而是入口和中转层；香港是默认出口，日本是开关式备用出口。6. 推荐地址规划下面是一个可复用、且已经被验证过思路合理的规划方式。你可以保留网段结构，替换成你自己的值。网段用途10.255.0.0/30GZ↔HK WireGuard 传输网段10.255.10.0/24L2TP 地址池10.255.20.0/30HK↔JP WireGuard 传输网段10.255.30.0/24本地 WireGuard 接入网段10.255.40.0/24IKEv2 地址池推荐原因：地址池职责清楚。L2TP、IKEv2、本地 WireGuard 互不混淆。回程路由、NAT 和 allowed-address 更好写。7. MTU 设计原则不要直接拿外层 PMTU 当内层隧道 MTU。要按承载层逐级扣头。推荐起点如下：本地 PPPoE 如果是 1450，那么本地到广州的 WireGuard 可以先从 1390 起步。GZ↔HK 如果跑在 1500 的数据中心内网上，WireGuard 可以先从 1400 起步。HK↔JP 如果跑在 1500 公网上，WireGuard 可以先从 1420 起步。L2TP/IPsec 不要一上来写 1450，先从 1400 起步更稳。所有隧道出口都应启用 change-tcp-mss 或 clamp-to-pmtu。如果业务已经通，但速度差、网页半开、测速上传异常，再做 MTU 微调；如果业务本身不通，先别碰 MTU。8. 实施顺序顺序不能乱。正确顺序如下：先打通 GZ↔HK 传输层。再打通 HK↔JP 传输层。再配置 GZ 的 L2TP 服务。再配置 GZ 的 IKEv2 服务。再配置 GZ 的本地 WireGuard 接入。再写 GZ 的策略路由表和 routing rule。再写 HK 的默认出口 NAT 和日本开关。再写 JP 的 NAT 和转发。最后做例外路由、验证和测速。9. 广州节点配置模板以下是广州节点的最小可用模板，真实值必须替换。9.1 GZ↔HK WireGuard 传输接口/interface wireguard add name=wg-hk listen-port=<GZ_HK_WG_PORT> mtu=1400 comment="gz-hk-transit" /ip address add address=<GZ_WG_GZ_HK_IP/CIDR> interface=wg-hk comment="gz-hk-transit" /interface wireguard peers add interface=wg-hk public-key="<HK_WG_PUBKEY>" \ endpoint-address=<HK_DC_IP> endpoint-port=<GZ_HK_WG_PORT> \ allowed-address=0.0.0.0/0,<WG_GZ_HK_TRANSIT>,<L2TP_POOL>,<IKEV2_POOL>,<WG_LOCAL_POOL> \ persistent-keepalive=25s comment="hk-peer"说明：GZ 这一侧需要把默认互联网流量塞进发往 HK 的 WireGuard 传输隧道，所以 allowed-address 要覆盖 0.0.0.0/0。endpoint-address 优先用 HK 的数据中心内网地址，而不是香港公网地址。9.2 L2TP/IPsec 服务器/ip pool add name=pool-l2tp ranges=<L2TP_POOL_RANGE> /ppp profile add name=prof-l2tp local-address=<L2TP_GW_IP> remote-address=pool-l2tp \ dns-server=<VPN_DNS> change-tcp-mss=yes only-one=yes \ use-compression=no use-encryption=no /ppp secret add name=<L2TP_USERNAME> password="<L2TP_PASSWORD>" service=l2tp profile=prof-l2tp /interface l2tp-server server set enabled=yes use-ipsec=yes ipsec-secret="<L2TP_PSK>" \ authentication=mschap2 default-profile=prof-l2tp \ max-mtu=1400 max-mru=1400 one-session-per-host=yes9.3 IKEv2/IPsec 服务器/ip pool add name=pool-ikev2 ranges=<IKEV2_POOL_RANGE> /ip ipsec profile add name=prof-ikev2-gz enc-algorithm=aes-256,aes-128 hash-algorithm=sha256 \ dh-group=modp2048,modp1024 /ip ipsec proposal add name=prop-ikev2-gz enc-algorithms=aes-256-cbc,aes-128-cbc \ auth-algorithms=sha256 pfs-group=none /ip ipsec mode-config add name=modeconf-ikev2 address-pool=pool-ikev2 address-prefix-length=32 \ split-include=0.0.0.0/0 static-dns=<VPN_DNS> system-dns=no /ip ipsec policy group add name=group-ikev2 /ip ipsec policy add group=group-ikev2 template=yes proposal=prop-ikev2-gz \ src-address=0.0.0.0/0 dst-address=<IKEV2_POOL_CIDR> /ip ipsec peer add name=peer-ikev2 passive=yes exchange-mode=ike2 profile=prof-ikev2-gz /ip ipsec identity add peer=peer-ikev2 auth-method=pre-shared-key secret="<IKEV2_PSK>" \ generate-policy=port-strict mode-config=modeconf-ikev2 \ policy-template-group=group-ikev2这一步的关键不是命令本身，而是命名隔离：prof-ikev2-gz 不是 default。prop-ikev2-gz 不是 default。peer-ikev2 不是 default。9.4 本地 WireGuard 接入接口/interface wireguard add name=wg-local listen-port=<WG_LOCAL_PORT> mtu=1390 comment="local-ingress" /ip address add address=<GZ_WG_LOCAL_IP/CIDR> interface=wg-local comment="local-ingress" /interface wireguard peers add interface=wg-local public-key="<LOCAL_PEER_PUBKEY>" \ allowed-address=<WG_LOCAL_PEER_IP/32> persistent-keepalive=25s \ comment="local-router-peer"9.5 GZ 的策略路由表与规则/routing table add fib name=to-hk-egress /ip route add dst-address=0.0.0.0/0 gateway=<GZ_PUBLIC_GW> add dst-address=<HK_DC_SUBNET> gateway=<GZ_DC_GW> comment="to-hk-private-subnet" add dst-address=<HK_DC_IP/32> gateway=<GZ_DC_GW> scope=10 comment="hk-private-recursive" add dst-address=0.0.0.0/0 gateway=<HK_WG_TRANSIT_IP> routing-table=to-hk-egress target-scope=11 \ comment="default-via-hk"/routing rule add src-address=<L2TP_POOL_CIDR> action=lookup-only-in-table table=to-hk-egress \ comment="l2tp-via-hk" add src-address=<IKEV2_POOL_CIDR> action=lookup-only-in-table table=to-hk-egress \ comment="ikev2-via-hk" add interface=wg-local action=lookup-only-in-table table=to-hk-egress \ comment="wg-local-via-hk"说明：这部分是整套方案的核心。这里故意用 routing rule，不用 mangle mark-routing。如果你做的是“指定某台 LAN 主机走 VPN 出口”，也是同一个原则：给该主机写 src-address 的 routing rule，而不是先上 mangle。9.6 广州直出例外路由/ip route add dst-address=<EXCEPTION_IP_1/32> gateway=<GZ_PUBLIC_GW> routing-table=to-hk-egress \ comment="force-gz-egress-1" add dst-address=<EXCEPTION_IP_2/32> gateway=<GZ_PUBLIC_GW> routing-table=to-hk-egress \ comment="force-gz-egress-2"原理是：在 to-hk-egress 表里放比默认路由更具体的主机路由，让特定目的地址继续从广州本地公网出去。9.7 广州直出 NAT/ip firewall nat add chain=srcnat action=masquerade out-interface=<GZ_PUBLIC_IF> src-address=<L2TP_POOL_CIDR> \ comment="l2tp-egress-via-gz-direct" add chain=srcnat action=masquerade out-interface=<GZ_PUBLIC_IF> src-address=<IKEV2_POOL_CIDR> \ comment="ikev2-egress-via-gz-direct" add chain=srcnat action=masquerade out-interface=<GZ_PUBLIC_IF> src-address=<WG_LOCAL_POOL_CIDR> \ comment="wg-local-egress-via-gz-direct"如果你不写这三条，那么命中广州直出例外路由时，客户端源地址不会被转换，出口将异常。9.8 广州侧 MSS Clamping/ip firewall mangle add chain=forward action=change-mss new-mss=clamp-to-pmtu protocol=tcp tcp-flags=syn \ out-interface=wg-hk comment="clamp-mss-out-wg-hk" add chain=forward action=change-mss new-mss=clamp-to-pmtu protocol=tcp tcp-flags=syn \ out-interface=wg-local comment="clamp-mss-out-wg-local"10. 香港节点配置模板10.1 HK↔GZ WireGuard 传输接口/interface wireguard add name=wg-gz listen-port=<GZ_HK_WG_PORT> mtu=1400 comment="hk-gz-transit" /ip address add address=<HK_WG_GZ_HK_IP/CIDR> interface=wg-gz comment="hk-gz-transit" /interface wireguard peers add interface=wg-gz public-key="<GZ_WG_PUBKEY>" \ endpoint-address=<GZ_DC_IP> endpoint-port=<GZ_HK_WG_PORT> \ allowed-address=<WG_GZ_HK_TRANSIT>,<L2TP_POOL>,<IKEV2_POOL>,<WG_LOCAL_POOL> \ persistent-keepalive=25s comment="gz-peer"说明：香港侧主要负责回程，不必默认把全部互联网流量送回广州，所以这里通常不需要 0.0.0.0/0。但必须包含所有客户端地址池，否则回包找不到路。10.2 HK↔JP WireGuard 传输接口/interface wireguard add name=wg-jp listen-port=<HK_JP_WG_PORT> mtu=1420 comment="hk-jp-egress" /ip address add address=<HK_WG_HK_JP_IP/CIDR> interface=wg-jp comment="hk-jp-egress" /interface wireguard peers add interface=wg-jp public-key="<JP_WG_PUBKEY>" \ endpoint-address=<JP_PUBLIC_IP> endpoint-port=<HK_JP_WG_PORT> \ allowed-address=0.0.0.0/0,<WG_HK_JP_TRANSIT> \ persistent-keepalive=25s comment="jp-peer"这里需要 0.0.0.0/0，因为当“日本开关”启用时，香港要把默认互联网流量送去日本。10.3 香港默认出口、回程路由与日本开关/routing table add fib name=to-jp-egress /ip route add dst-address=0.0.0.0/0 gateway=<HK_PUBLIC_GW> add dst-address=<GZ_DC_SUBNET> gateway=<HK_DC_GW> comment="to-gz-private-subnet" add dst-address=<GZ_DC_IP/32> gateway=<HK_DC_GW> scope=10 comment="gz-private-recursive" add dst-address=<L2TP_POOL_CIDR> gateway=<GZ_WG_TRANSIT_IP> target-scope=11 comment="l2tp-pool-via-gz" add dst-address=<IKEV2_POOL_CIDR> gateway=<GZ_WG_TRANSIT_IP> target-scope=11 comment="ikev2-pool-via-gz" add dst-address=<WG_LOCAL_POOL_CIDR> gateway=<GZ_WG_TRANSIT_IP> target-scope=11 comment="wg-local-pool-via-gz" add dst-address=0.0.0.0/0 gateway=<JP_WG_TRANSIT_IP> routing-table=to-jp-egress comment="default-via-jp" add dst-address=<FORCE_HK_LOCAL_SUBNET> gateway=<HK_PUBLIC_IF> routing-table=to-jp-egress \ comment="force-hk-egress-subnet"/routing rule add src-address=<L2TP_POOL_CIDR> action=lookup-only-in-table table=to-jp-egress \ comment="switch-l2tp-to-jp" disabled=yes add src-address=<IKEV2_POOL_CIDR> action=lookup-only-in-table table=to-jp-egress \ comment="switch-ikev2-to-jp" disabled=yes add src-address=<WG_LOCAL_POOL_CIDR> action=lookup-only-in-table table=to-jp-egress \ comment="switch-wg-local-to-jp" disabled=yes默认禁用意味着默认走香港；启用后才走日本。10.4 香港默认出口 NAT/ip firewall nat add chain=srcnat action=masquerade out-interface=<HK_PUBLIC_IF> src-address=<L2TP_POOL_CIDR> \ comment="l2tp-egress-via-hk" add chain=srcnat action=masquerade out-interface=<HK_PUBLIC_IF> src-address=<IKEV2_POOL_CIDR> \ comment="ikev2-egress-via-hk" add chain=srcnat action=masquerade out-interface=<HK_PUBLIC_IF> src-address=<WG_LOCAL_POOL_CIDR> \ comment="wg-local-egress-via-hk"10.5 香港侧 MSS Clamping/ip firewall mangle add chain=forward action=change-mss new-mss=clamp-to-pmtu protocol=tcp tcp-flags=syn \ out-interface=wg-gz comment="clamp-mss-out-wg-gz" add chain=forward action=change-mss new-mss=clamp-to-pmtu protocol=tcp tcp-flags=syn \ out-interface=wg-jp comment="clamp-mss-out-wg-jp"11. 日本 Debian 配置模板11.1 系统要求开启 net.ipv4.ip_forward=1。安装 WireGuard。出口网卡记为 eth0，实际环境按真实网卡名替换。11.2 WireGuard 配置模板文件路径可以使用 /etc/wireguard/wg-hk.conf。[Interface] Address = <JP_WG_HK_JP_IP/CIDR> ListenPort = <HK_JP_WG_PORT> PrivateKey = <JP_WG_PRIVKEY> MTU = 1420 PostUp = iptables -A FORWARD -i wg-hk -j ACCEPT PostUp = iptables -A FORWARD -o wg-hk -j ACCEPT PostUp = iptables -t nat -A POSTROUTING -s <L2TP_POOL_CIDR> -o eth0 -j MASQUERADE PostUp = iptables -t nat -A POSTROUTING -s <IKEV2_POOL_CIDR> -o eth0 -j MASQUERADE PostUp = iptables -t nat -A POSTROUTING -s <WG_LOCAL_POOL_CIDR> -o eth0 -j MASQUERADE PostUp = iptables -t mangle -A FORWARD -o wg-hk -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu PreDown = iptables -D FORWARD -i wg-hk -j ACCEPT PreDown = iptables -D FORWARD -o wg-hk -j ACCEPT PreDown = iptables -t nat -D POSTROUTING -s <L2TP_POOL_CIDR> -o eth0 -j MASQUERADE PreDown = iptables -t nat -D POSTROUTING -s <IKEV2_POOL_CIDR> -o eth0 -j MASQUERADE PreDown = iptables -t nat -D POSTROUTING -s <WG_LOCAL_POOL_CIDR> -o eth0 -j MASQUERADE PreDown = iptables -t mangle -D FORWARD -o wg-hk -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu [Peer] PublicKey = <HK_WG_PUBKEY> AllowedIPs = <HK_WG_TRANSIT_IP/32>,<L2TP_POOL_CIDR>,<IKEV2_POOL_CIDR>,<WG_LOCAL_POOL_CIDR> PersistentKeepalive = 25说明：日本节点只负责把来自香港的客户端地址池 NAT 出去。它不需要宣称自己拥有整个互联网的回程，只需要把这些客户端网段接住即可。12. 本地 RouterOS 通过 WireGuard 接入广州的模板如果你有一台本地 RouterOS，想把它的部分或全部流量送到广州，再转香港/日本，可以按下列方式接入。/interface wireguard add name=wg-gz listen-port=<WG_LOCAL_PORT> mtu=1390 comment="wg-to-gz" /ip address add address=<LOCAL_WG_IP/CIDR> interface=wg-gz comment="wg-to-gz" /interface wireguard peers add interface=wg-gz public-key="<GZ_WG_LOCAL_PUBKEY>" \ endpoint-address=<GZ_PUBLIC_IP> endpoint-port=<WG_LOCAL_PORT> \ allowed-address=0.0.0.0/0 persistent-keepalive=25s comment="gz-server-peer"12.1 如果是全局 LAN 走广州/routing table add fib name=to-gz-egress /ip route add dst-address=0.0.0.0/0 gateway=<GZ_WG_LOCAL_IP> routing-table=to-gz-egress /routing rule add src-address=<LOCAL_LAN_SUBNET> action=lookup-only-in-table table=to-gz-egress \ comment="lan-via-gz"12.2 如果只是指定某台 LAN 主机走广州/routing table add fib name=to-gz-egress /ip route add dst-address=0.0.0.0/0 gateway=<GZ_WG_LOCAL_IP> routing-table=to-gz-egress /routing rule add src-address=<LAN_HOST_IP/32> action=lookup-only-in-table table=to-gz-egress \ comment="host-via-gz"这里再次强调：优先用 routing rule。不要默认用 mangle mark-routing。13. 日本出口开关的标准操作在香港节点执行。13.1 切 L2TP 到日本/routing rule enable [find where comment="switch-l2tp-to-jp"]13.2 切 IKEv2 到日本/routing rule enable [find where comment="switch-ikev2-to-jp"]13.3 切本地 WireGuard 到日本/routing rule enable [find where comment="switch-wg-local-to-jp"]13.4 关闭日本开关/routing rule disable [find where comment~"switch.*to-jp"]验证标准只有一个：客户端看到的出口公网 IP 必须符合预期。14. AI 或运维执行时的标准验证顺序不要上来就测速。正确验证顺序如下。14.1 先看隧道握手RouterOS：/interface wireguard peers print /ip ipsec active-peers print /ip ipsec installed-sa printLinux：wg show14.2 再看路由广州：/ip route print where active /routing rule print /ping address=8.8.8.8 src-address=<L2TP_GW_IP> count=3 /ping address=8.8.8.8 src-address=<IKEV2_TEST_IP> count=3香港：/ip route print where active /routing rule print14.3 再看 NAT 命中/ip firewall nat print statsLinux：iptables -t nat -L POSTROUTING -n -v14.4 再看客户端出口 IPcurl -s ifconfig.me如果要看更完整的 AS 信息，使用：curl -sL -A 'Mozilla/5.0' 'https://bgp.he.net' | grep 'visiting from'不要再用 HTTP 版本去请求 bgp.he.net，否则容易 403。14.5 最后才做应用层验证打开网页。播放视频。跑上下行测速。对比切换前后出口 IP 是否符合预期。15. 标准排障决策表这一节是给 AI 用的，也是给人用的。遇到问题，先对号入座，再查对应项。现象最可能原因优先检查L2TP 或 IKEv2 能连上，但完全不能上网GZ 路由规则没命中，HK 回程路由缺失，或 HK NAT 漏写GZ routing rule、HK route、HK NATGZ 和 HK 的内网地址互 ping 正常，但客户端地址池过不去数据中心内网不转发客户端地址池GZ↔HK 必须用 WireGuard 传输层封装WireGuard 握手有，但业务单向通peer 的 allowed-address 不完整双端 peer 的 allowed-address加完 IKEv2 以后 L2TP 断了改坏了 default IPsec 配置IKEv2 是否用了独立命名对象网页半开、图片不全、测速上传为 0不是单纯 MSS 问题，更可能是 RouterOS v7 mangle PBR 不稳定是否使用了 mark-routing；先改 routing rule把 MSS 压很小还是不通根因不在 MSS回到路由、回程、NATRouterOS SSH 执行 WireGuard peer 命令时报 expected end of command公钥里的 / 被当成命令路径分隔符用 Here-String 经 stdin 发送命令Linux L2TP 已拨上，但 DNS 不通ppp0 的 DNS 没被 systemd-resolved 接管resolvectl dns ppp0 切到日本后香港自身公网段访问异常to-jp-egress 表里没保留香港本地直出例外FORCE_HK_LOCAL_SUBNET 的更具体路由客户端连上后访问 VPN 服务器公网地址异常默认路由把服务器地址也送进隧道，形成回环先加服务器公网地址的直连路由16. 已验证的典型坑16.1 不要把“外层 PMTU 1450”直接理解成“L2TP MTU 应该设 1450”外层 PMTU 只说明最外层 IP 包能承载多大，不代表 L2TP/IPsec 内层接口也能安全吃满这个值。L2TP、PPP、ESP、UDP、NAT-T 都会继续吃头部。16.2 不要把 FastTrack、QUIC、rp-filter 当成第一嫌疑人如果你看到的是“路由级别不稳定”，先回到策略路由实现方式本身。很多时候问题根本不在 QUIC 或 FastTrack，而在 PBR 选型。16.3 不要假设 SSH 自动化里 Base64 是普通字符串RouterOS 在命令行里对特殊字符解析很敏感，尤其是 WireGuard 公钥里的斜杠。自动化时最好直接通过 stdin 管道喂整段命令。16.4 不要漏掉每个出口点的 NATGZ 负责直出例外时需要 NAT，HK 作为默认出口需要 NAT，JP 作为可选出口同样需要 NAT。缺哪个，哪个方向就会出问题。17. 自动化执行时的补充约束如果后续让 AI 直接按这篇文章实施，请额外遵守下面几条：先盘点变量，再写配置，不要先写后问。先建传输隧道，再建接入服务，不要倒序。变更 IKEv2 时，不要动 L2TP 默认 IPsec 对象。做策略路由时，优先用 routing rule，不要默认上 mangle mark-routing。做例外路由时，要同时考虑路由与 NAT，不能只写路由。测速前先确认出口 IP 正确，否则测速结果没有意义。如果某一步现象和本文的排障表冲突，先检查是不是基础变量拿错了。18. 如果你的任务不是远程接入，而是“本地某台 LAN 主机走 VPN 出口”这属于同类问题，只是接入源从地址池变成单一主机。处理原则不变：为这台主机建立独立 routing table。用 routing rule 按 src-address=<LAN_HOST/32> 查表。如有必要，在该表中为本地网关、局域网、特定公网目标写更具体例外路由。不要把 mangle mark-routing 当默认实现。如果你已经见到这些现象：本地路由器 192.168.x.1 无法访问。外部从原公网入口访问该主机时回包异常。网页卡、图片不全、测速上传为 0。优先怀疑策略路由实现方式，而不是先把问题归咎于 MSS。19. 公开发布时的脱敏要求如果你要把这篇文档放到网站上，至少做到下面几点：所有公网 IP 改为占位符或文档保留地址。所有账号、密码、PSK、私钥、公钥全部去掉或替换为 。所有自定义端口如果没有公开必要，也改为占位符。所有截图打码 IP、用户名、peer 名、SPI、时间戳、接口名、序列号。所有看起来像管理入口的域名不要放在正文里。公开文档应该公开的是架构、顺序、判断逻辑和坑，不是资产清单。20. 一句话总结这套方案的核心不是“把 VPN 配起来”，而是明确把问题拆成四层来做：传输层先打通。策略路由用 routing rule 固化。每个出口点把 NAT 补齐。MTU/MSS 最后再调。只要顺序不乱，GZ 负责接入、HK 负责默认出口、JP 负责可选出口的这套结构是可以稳定复用的；反过来，如果一开始就用 mangle PBR 去硬扛复杂隧道，大概率会再次踩回同一批坑。
这个用8011，IP用 172.18.4.2/24docker network create --subnet=172.18.4.0/24 convertx-network官方docker运行docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx我直接进行更改，懒得查了。docker run -d \ --name convertx \ --restart=always \ --network convertx-network \ --ip 172.18.4.2 \ -p 8011:3000 \ -v /data/wwwroot/convert.wingsrabbit.com/files:/app/data \ ghcr.io/c4illin/convertx:latest 直接就好了，访问IP+8011。NGINX反代。不想写了。实际用起来对中文bug比较多，让copilot帮忙整了点别的。1.专门针对 Convertx 做增强：加上 Java、常见西文字体和中文字体（思源黑体、文泉驿），这样 LibreOffice/Calibre 在处理 PDF → Word/Docx 时成功率会高很多。FROM ghcr.io/c4illin/convertx:latest # 安装 Java 和常见字体（西文 + 中文） RUN apt-get update && \ apt-get install -y --no-install-recommends \ default-jre \ fonts-dejavu \ fonts-liberation \ fonts-noto-cjk \ fonts-wqy-zenhei \ fonts-wqy-microhei \ && rm -rf /var/lib/apt/lists/* # 设置 Java 环境变量（LibreOffice 有时需要） ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 ENV PATH=$JAVA_HOME/bin:$PATH # 保持原有启动命令 CMD ["bun", "run", "start"] 在服务器上新建一个目录，比如 123456，把上面的内容保存为 Dockerfile。docker build -t convertx-custom .运行容器。完成。
好吧，继续造。今天用8010，IP用 172.18.3.2/24造网络docker network create --subnet=172.18.3.0/24 stirling-pdf-network官网docker run，要是担心的话就跑一下再说。docker run -d \ --name stirling-pdf \ -p 8080:8080 \ -v "./StirlingPDF/trainingData:/usr/share/tessdata" \ -v "./StirlingPDF/extraConfigs:/configs" \ -v "./StirlingPDF/customFiles:/customFiles/" \ -v "./StirlingPDF/logs:/logs/" \ -v "./StirlingPDF/pipeline:/pipeline/" \ -e DISABLE_ADDITIONAL_FEATURES=true \ -e LANGS=en_GB \ docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest 我这里就不跑了，直接上修改版。（有点好奇啊，这东西就不能-v个数据库？算了到时候再说）docker run -d \ --name stirling-pdf \ --restart=always \ --network stirling-pdf-network \ --ip 172.18.3.2 \ -e SERVER_PORT=8010 \ -e DISABLE_ADDITIONAL_FEATURES=true \ -e LANGS=en_GB \ -p 8010:8010 \ -v /root/StirlingPDF/trainingData:/usr/share/tessdata \ -v /root/StirlingPDF/extraConfigs:/configs \ -v /root/StirlingPDF/customFiles:/customFiles/ \ -v /root/StirlingPDF/logs:/logs/ \ -v /root/StirlingPDF/pipeline:/pipeline/ \ docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest 直接就好了，访问IP+8010。NGINX反代。注释掉这些东西# root /data/wwwroot/docker.wingsrabbit.com; # location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ { # expires 30d; # access_log off; # } # location ~ .*\.(js|css)?$ { # expires 7d; # access_log off; # }在localtion well know之后加这一段。#Nginx-443-Start location / { proxy_pass http://127.0.0.1:8010; proxy_ssl_verify off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
设计要求：用 pan.wingsrabbit.com 访问，同时本地储存，储存在 /data/www/wwwroot/pan.wingsrabbit.com下。先设置目录权限。chown -R 1001:1001 /data/wwwroot/pan.wingsrabbit.com考虑到标准化操作，我们仍然规定端口，端口8009，IP 172.18.2.2/24创建docker网络docker network create --subnet=172.18.2.0/24 openlist-network然后用docker run命令docker run -d \ --name openlist \ --restart=unless-stopped \ -v /data/wwwroot/pan.wingsrabbit.com:/opt/openlist/data \ --network openlist-network \ --ip 172.18.2.2 \ -p 8009:5244 \ -e UMASK=022 \ openlistteam/openlist:latest 这时候应该是可以打开IP+8009了。接下来配置NGINX反代，进/usr/local/nginx/conf/vhost来找到相关配置。说实话，和portainer差不多，区别就是要用http。注释掉这些东西# root /data/wwwroot/docker.wingsrabbit.com; # location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ { # expires 30d; # access_log off; # } # location ~ .*\.(js|css)?$ { # expires 7d; # access_log off; # } 在localtion well know之后加这一段。#Nginx-443-Start location / { proxy_pass http://127.0.0.1:8009; proxy_ssl_verify off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } 前面因为自定义了 -v /data/wwwroot/pan.wingsrabbit.com:/opt/openlist/data \所以在后台储存这里写/opt/openlist/data建议是mkdir files/opt/openlist/data/files并且chown -R 1001:1001 /data/wwwroot/pan.wingsrabbit.com一下
用docker在linux造一个docker管理web portainer并放给443
注：本文是只在本机建立portainer，并不涉及portrainer agent。换句话说，它只能自己管自己。考虑到之前没有用compose，本文也用docker run先。debian12，oneinstackdocker安装curl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.sh尝试先通过官方一键安装，先创建数据库。docker volume create portainer-data用官方的数据造一下。docker run -d -p 8000:8000 -p 9443:9443 \ --name portainer \ --restart=always \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer-data:/data \ portainer/portainer-ce:latest 来用https来访问一下IP+端口。OK没问题，让他走吧。docker stop portainerdocker rm portainer按照termix的想法，就+1，本地端口用8008，IP用 172.18.1.2，反正内网不缺，就加/24就行了。再来创建个docker网络。docker network create --subnet=172.18.1.0/24 portainer-network如果创建错了可以删掉docker network rm portainer-network看一眼创建好没有。docker network ls也可以用这个来看详细的数据docker network inspect portainer-network还有个问题，就是termix的数据端口是8081-8085。但是我们也要考虑到之后agent的管理端口，我希望管理agent的数据端口要用8086。不过很可惜的是你并不能更改容器内部的通信端口到匹配8086，因为portainer ce没有提供这个环境变量。你可以用iptables转发等方式来实现，不过太麻烦了，等之后遇到问题再说。好了，让我们更改一下官方的docker数据吧。docker run -d -p 8086:8000 -p 8008:9443 \ --name portainer \ --restart=always \ --network portainer-network \ --ip 172.18.1.2 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer-data:/data \ portainer/portainer-ce:latest 看起来这个数据没问题。让我们通过IP+端口8008尝试注册admin，看起来是ok的，那就证明数据库也ok。接下来进入反代，进/usr/local/nginx/conf/vhost来找到相关配置。要做的比termix少的多。注释掉这些东西# root /data/wwwroot/docker.wingsrabbit.com; # location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ { # expires 30d; # access_log off; # } # location ~ .*\.(js|css)?$ { # expires 7d; # access_log off; # } 在localtion well know之后加这一段。#Nginx-443-Start location / { proxy_pass https://127.0.0.1:8008; proxy_ssl_verify off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } 好了，搞定。
debian12，oneinstackdocker安装curl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.sh在本地8080尝试安装Termix，先创建数据储存。docker volume create termix-datadocker run -d \ --name termix \ --restart unless-stopped \ -p 8080:8080 \ -v termix-data:/app/data \ -e PORT=8080 \ ghcr.io/lukegus/termix:latest 很好，用HTTP+8080打开，没有问题。关掉，删了他。docker stop termixdocker rm termix考虑了一下，希望本地端口用8007，对docker固定ip 172.18.0.2，方便后面nginx反代。docker本身用了 172.17.0.1/16，那我们就创建 172.18.0.0/24 分隔。先创建个docker网络docker network create --subnet=172.18.0.0/24 termix-network如果创建错了可以删掉docker network rm termix-network也可以看看网络创建好没有docker network ls再运行docker run -d \ --name termix \ --restart unless-stopped \ --network termix-network \ --ip 172.18.0.2 \ -p 127.0.0.1:8007:8080 \ -v termix-data:/app/data \ -e PORT=8080 \ ghcr.io/lukegus/termix:latest 我们可以看到-p这里，这是因为指定网络和IP，并映射端口8007了，之后只需要在Nginx配置中反代这个443就行了。注意，这个-p 127.0.0.1是因为我不希望从别的地方可以访问，不加这个的话，你就可以从HTTP+IP+8007访问了。这里可以通过删掉-p下的127.0.0.1，来通过 localhost:8007 看一眼，数据库不报错就ok了。你可能会报错，提示The container name "/termix" is already in use by container这时候用docker ps -adocker rm -f termix好了，接下来配置nginx，因为基于oneinstack，所以正常配置后，进/usr/local/nginx/conf/vhost来找到相关配置。之前的server{}都不用管，我们只看location。在所有location块前面加location ~ ^/assets/ { proxy_pass http://127.0.0.1:8007; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } 在所有location块最后加 location / { proxy_pass http://127.0.0.1:8007; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } 好了，用 nginx -t 检测一下，没问题的话用 nginx -s reload 就可以了。可以通过域名访问了。在创建的时候遇到了一些问题，一个是，在nginx一开始通过普通匹配的时候，会报错404，找不到文件。究其原因，是因为原始配置如下location /assets/ { # 普通前缀匹配 proxy_pass http://127.0.0.1:8007/assets/; } location ~ .*\.(js|css)?$ { # 正则匹配 expires 7d; access_log off; } 普通匹配没有正则优先级高，所以请求被第二个location处理了，没有代理到termix。另一个是可能出现502 bad gateway，这是因为我在nginx配置中配置了 172.18.0.2，我改成127.0.0.1就可以了。那如果nginx强制要求配置172.18.0.2，或许可以通过docker写-p 172.18.0.2:8007:8080 \来实现？待测试，情况不明。然后这个termix就好了，但是你会发现你怎么不能连接ssh，竟然是没有写ssh代理！那就整一个，放在哪都行。我放在了第二位，测试没问题。location /ssh/ { proxy_pass http://127.0.0.1:8007; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 86400; }
用这个来进入pve的backup页面cd ../var/lib/vz/dump 这是debian12，但是如果没有网或者网不好开机巨几把慢。wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian12-2025-cloudinit/vzdump-qemu-500-2025_09_29-14_58_32.log wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian12-2025-cloudinit/vzdump-qemu-500-2025_09_29-14_58_32.vma.zst wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian12-2025-cloudinit/vzdump-qemu-500-2025_09_29-14_58_32.vma.zst.notes 这是debian11，2024年的版本了，但是开机巨几把快。wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian11-2024-cloudinit/vzdump-qemu-499-2025_09_30-15_15_07.log wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian11-2024-cloudinit/vzdump-qemu-499-2025_09_30-15_15_07.vma.zst wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian11-2024-cloudinit/vzdump-qemu-499-2025_09_30-15_15_07.vma.zst.notes 这是debian13，不知道什么玩意。wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian13-2025-cloudinit/vzdump-qemu-501-2025_09_30-15_28_49.log wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian13-2025-cloudinit/vzdump-qemu-501-2025_09_30-15_28_49.vma.zst wget https://wingsrabbit-1319758772.cos.ap-hongkong.myqcloud.com/debian13-2025-cloudinit/vzdump-qemu-501-2025_09_30-15_28_49.vma.zst.notes pve上开simple httppython3 -m http.serverdocker-master.zip
mkdir cloudinitcd cloudinit下载相应的qcow2并且检查qcow2是否完好。wget https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-20250923.0.x86_64.qcow2qm create 500 --cores 4 --memory 4096 -name cloudinit --net0 virtio,bridge=vmbr0qm importdisk 500 CentOS-Stream-GenericCloud-10-20250923.0.x86_64.qcow2 local-lvmqm set 500 --scsi0 local-lvm:vm-500-disk-0qm set 500 --ide0 local-lvm:cloudinitqm set 500 --serial0 socketqm set 500 --boot c --bootdisk scsi0qm set 500 --ciuser root --cipassword xxxqm set 500 --ipconfig0 ip=192.168.1.1/24,gw=192.168.1.1注意你qm importdisk进去之后，可以的话最好去web上面改。更改qcow2配置virt-filesystems -a CentOS-Stream-GenericCloud-10-20250923.0.x86_64.qcow2查看位置，例如 /dev/sda2guestmount -a CentOS-Stream-GenericCloud-10-20250923.0.x86_64.qcow2 -m /dev/sda2 /mnt这样就挂载到 /mnt 了。用完之后umount /mnt