ARTICLE · 1196502
让网卡学会闭嘴:四行 ARP 参数的源码真相
让网卡学会闭嘴:四行 ARP 参数的源码真相让网卡学会闭嘴:四行 ARP 参数的源码真相 上一篇讲 LVS,结尾留了个尾巴:DR 模式下真实服务器必须配四行内核参数(arp_ignore=1、arp_announce=2,all 和 eth0 各写一遍),「一个不能少」。当时没展开——因为这一口值得单独一篇。 今天兑现。这四行参数,内核里管事的代码加起来不到 80 行,全在 一声广播,全网举手 以太网的寻址规则粗暴:想知道一个 IP 归谁,就在整个二层网络里喊广播——「谁是 10.0.0.100?把 MAC 地址报上来」。这声喊话就是 ARP 请求。 按协议设计,只有 IP 的主人该应答。问题出在「主人」怎么认定:Linux 内核的默认答案是——这个 IP 只要配在这台机器的任意一块网卡上,我就应答,不管包从哪块网卡进来。 平时这个默认没毛病,一台机器一个 IP,天经地义。坏在 DR 模式:为了 RS 能直接回包,VIP 被配到了每台真实服务器的回环口 lo 上。于是交换机喊「谁是 VIP」—— 交换机 · ARP 广播 "谁是 10.0.0.100(VIP)?" ↓ 未压制:全网抢答 调度器 DS✋ 我有RS-1✋ 我也有RS-2✋ 我还有 交换机 ARP 表:VIP → 最后应答的那台 —— 学歪了 ↓ 配上四行参数之后 调度器 DS✋ 我有RS-1🔇 装聋RS-2🔇 装聋 交换机 ARP 表:VIP → DS ✅ 流量老老实实走调度器 三台都答,交换机的 ARP 表就指向了最后应答的那台——流量绕过调度器直奔 RS,负载均衡当场失效。更阴的是这个错误带概率:谁答得晚表就指向谁,症状是「负载忽高忽低」「时好时坏」,翻查日志什么都没有,典型的静默故障。 解法就是那四行参数:让 RS 对 VIP 的问话装聋(arp_ignore),同时自己开口问路时也别报 VIP 的名号(arp_announce)。云厂商的文档——比如 BytePlus ECS 的内核微调指南——都会让你配上它们,但「为什么是这两个值、为什么写四行」,得去内核里找。 arp_ignore:别人问,答不答 内核收到 ARP 请求、决定要不要回话,走的是 先看 case 0:直接 再看 case 1:进 switch 先做一个小动作 翻译成人话:arp_ignore=1 之后,内核只认「收包的那块网卡」。IP 配在 eth0、ARP 从 eth0 进来——答;IP 配在 lo 上、ARP 从 eth0 进来——eth0 上查无此 IP,装聋。 DR 场景正好卡在这个缝隙里:VIP 以 /32 掩码配在 RS 的 lo 上,ARP 请求从 eth0 进来。设了 arp_ignore=1,RS 对「谁是 VIP」这声广播就再也不举手了。 八个取值不用都背,表留着备查: 值 行为 0 默认:IP 配在本机任意网卡上都应答 1 只应答「配在收包网卡上」的 IP 2 在 1 的基础上,还要求问话方 IP 与之同网段 3 不应答主机作用域地址(典型:lo 上的 /32) 4–7 保留 8 一律不应答,连自己的正经 IP 也不答 生产里真正在用的就是 0 和 1。2 比 1 更严但依赖网段规划;8 是彻底闭嘴,一般只在排查时临时用。 arp_announce:自己问,报什么身份 arp_ignore 管被动应答,arp_announce 管主动发起——这台机器要发 ARP 去问「网关的 MAC 是多少」时,ARP 包里「发送方 IP」填什么。 直觉上应该填出口网卡的 IP。默认行为偏偏不是。看 三个档位: case 0(默认):只要触发这次 ARP 的那个 IP 包的源地址( case 1:加一道约束,源地址必须和目标 IP 同网段( case 2:最干脆——直接忽略包里的源地址, 坑在 case 0 上。DR 模式下,RS 的应用回包时源 IP 是 VIP(配在 lo 上),内核处理这个回包要发 ARP 问网关,按 case 0 的逻辑,ARP 的发送方 IP 就填成了 VIP——问个路都要报 VIP 的名号,交换机顺手又把「VIP 在这台 RS」学进 ARP 表。刚才 arp_ignore 压下去的抢答,从主动方向又漏了出来。 arp_announce=2 把这个口子也堵上:出口是 eth0,就用 eth0 上的内网 IP 去问路,身份干净。 一个管「答」,一个管「问」,两个参数把一台机器的网络身份收拾利索——这就是四行里的两行。 为什么是四行,不是两行 all 和 eth0 各写一遍,不是保险起见的仪式,内核里真有讲究。 Linux 网络参数有三个作用域: arp_ignore 和 arp_announce 属于一个小团体,规则不同。 conf/allarp_ignore = 1┐│┌conf/eth0arp_ignore = 0 ↓ max() 生效值 = 1(更严格者胜) 也就是说:只写 顺带一个冷知识:走 max() 逻辑的还有 rp_filter、arp_notify 这批「安全属性」参数,方向统一是「更严格者胜」——语义上说得通:安全参数,取松紧更紧的那个。 动手改一遍 临时改(重启即失效,适合先验证再固化): 永久改:写进 验证三件套: 第 2 步最有说服力:配置正确的话,arping 只有一个应答方——DS。冒出多个应答方,就是有 RS 没压住。 两个容易踩的细节:接口名按实际改,网卡叫 ens5、enp0s3 的机器照抄 eth0 等于没配; 除了 LVS,还有谁在用 这套参数的适用面比 LVS 宽: Keepalived / HA 的 VIP:主备机器都「有」VIP,ARP 抑制决定漂移时谁应答(Keepalived 会自己动这些参数,原理相同)。 云主机多网卡 / 辅助 IP:辅助 IP 的 ARP 应答行为影响路由收敛,各云厂商文档——包括开头那篇 BytePlus 的——主要服务这个场景。 容器与 k8s 的静态 VIP:节点上挂了 service 网段地址,同样要管住 ARP。 本质是同一个问题:当一台机器「拥有」多个 IP、而其中一些不该从当前网卡露面时,就得教内核管住嘴。 收个尾 四行参数,两个旋钮:arp_ignore 管「别人问到头上,答不答」——只认收包网卡;arp_announce 管「自己开口问路,报什么身份」——只认出口网卡的主地址。一个 max() 决定了 all 和接口要各写一遍。DR 模式的静默故障,九成九栽在这四行上。 参考:内核源码 聊两句:你第一次撞上 ARP 抢答是什么场景?我猜大部分人是 DR 环境负载不均、甚至 curl 偶发超时,回头才查到交换机 ARP 表学歪了——评论区说说你的排查路径,被 ARP 表坑过的举个手,我置顶一条我的判断。 下一篇拆 conntrack:那张每天替你记几百万条连接的内核表,满了之后新连接是什么下场——一个
net/ipv4/arp.c 一个文件里。先把事故现场还原,再进源码。arp_process()。剥掉枝节,核心就这几行:if (addr_type == RTN_LOCAL) { int dont_send; dont_send = arp_ignore(in_dev, sip, tip); // 回不回,这一行说了算 if (!dont_send && IN_DEV_ARPFILTER(in_dev)) dont_send = arp_filter(sip, tip, dev); if (!dont_send) { /* 更新邻居表,发 ARP 应答 */ } } in_dev 是收到这个包的网卡设备,sip 是问话人的 IP,tip 是被问的 IP。dont_send 为 0 就应答。而 dont_send 完全由 arp_ignore() 决定——这个函数 35 行,是全文最值得读的一段内核代码:static int arp_ignore(struct in_device *in_dev, __be32 sip, __be32 tip) { struct net *net = dev_net(in_dev->dev); int scope; switch (IN_DEV_ARP_IGNORE(in_dev)) { case 0: /* Reply, the tip is already validated */ return 0; case 1: /* Reply only if tip is configured on the incoming interface */ sip = 0; scope = RT_SCOPE_HOST; break; case 2: /* * Reply only if tip is configured on the incoming interface * and is in same subnet as sip */ scope = RT_SCOPE_HOST; break; case 3: /* Do not reply for scope host addresses */ sip = 0; scope = RT_SCOPE_LINK; in_dev = NULL; break; case 4: /* Reserved */ case 5: case 6: case 7: return 0; case 8: /* Do not reply */ return 1; default: return 0; } return !inet_confirm_addr(net, in_dev, sip, tip, scope); } return 0,不拦,应答。这就是默认行为——tip 是本机地址就行,配在哪块网卡上不管。sip = 0,把「谁在问」的信息抹掉;然后带着 scope = RT_SCOPE_HOST 落到最后一行,调 inet_confirm_addr()。这个函数干的事:在 in_dev——也就是收到这声 ARP 的那块网卡上——确认 tip(被问的 IP)是不是配置在这里。找到了返回非 0,取反就是 0,应答;找不到就不答。arp_solicit()(内核替邻居表发 ARP 探测的函数)里选源地址的这段:switch (IN_DEV_ARP_ANNOUNCE(in_dev)) { default: case 0: /* By default announce any local IP */ if (skb && inet_addr_type_dev_table(dev_net(dev), dev, ip_hdr(skb)->saddr) == RTN_LOCAL) saddr = ip_hdr(skb)->saddr; break; case 1: /* Restrict announcements of saddr in same subnet */ if (!skb) break; saddr = ip_hdr(skb)->saddr; if (inet_addr_type_dev_table(dev_net(dev), dev, saddr) == RTN_LOCAL) { /* saddr should be known to target */ if (inet_addr_onlink(in_dev, target, saddr)) break; } saddr = 0; break; case 2: /* Avoid secondary IPs, get a primary/preferred one */ break; } rcu_read_unlock(); if (!saddr) saddr = inet_select_addr(dev, target, RT_SCOPE_LINK); ip_hdr(skb)->saddr)是本机地址,就直接拿它当 ARP 的发送方 IP——注意 inet_addr_type_dev_table 查的是全机地址表,不限出口网卡。inet_addr_onlink),否则作废,落到下面的兜底逻辑。saddr 保持 0,交给 inet_select_addr(dev, target, RT_SCOPE_LINK):在出口网卡上,挑一个和目标同网段的主地址。conf/default(新接口出生时的模板)、conf/all(全机层)、conf/具体接口。绝大多数参数的规则是「接口值优先」——接口上设了就用接口的,没设才回退到 all。include/linux/inetdevice.h:#define IN_DEV_MAXCONF(in_dev, attr) \ (max(IPV4_DEVCONF_ALL_RO(dev_net(in_dev->dev), attr), \ IN_DEV_CONF_GET((in_dev), attr))) #define IN_DEV_ARP_ANNOUNCE(in_dev) IN_DEV_MAXCONF((in_dev), ARP_ANNOUNCE) #define IN_DEV_ARP_IGNORE(in_dev) IN_DEV_MAXCONF((in_dev), ARP_IGNORE) IN_DEV_MAXCONF 就是一个 max():生效值 = max(conf/all 的值,本接口的值)。all=1、eth0 留着 0,max(1,0)=1,照样生效——理论上两行就够。但反过来,接口上残留了一个更大的值,光改 all 是压不下去的;何况 NetworkManager、systemd-networkd 这类工具重建接口时,接口级值可能被重置。写全四行是成本最低的确定性:两层都钉死,谁来 max 都是你要的值。sysctl -w net.ipv4.conf.all.arp_ignore=1 sysctl -w net.ipv4.conf.eth0.arp_ignore=1 sysctl -w net.ipv4.conf.all.arp_announce=2 sysctl -w net.ipv4.conf.eth0.arp_announce=2 /etc/sysctl.conf,或者在 /etc/sysctl.d/ 下单起一个文件(更清爽),然后 sysctl -p 生效:net.ipv4.conf.all.arp_ignore = 1 net.ipv4.conf.eth0.arp_ignore = 1 net.ipv4.conf.all.arp_announce = 2 net.ipv4.conf.eth0.arp_announce = 2 # 1. 看值落没落下去 sysctl net.ipv4.conf.all.arp_ignore net.ipv4.conf.eth0.arp_ignore # 2. 在另一台同网段机器上问 VIP,看谁应答——金标准 arping -I eth0 -c 3 10.0.0.100 # 3. 在 RS 上抓 ARP 包,确认自己没抢答 tcpdump -i eth0 -n arp conf/default 别拿来当全机开关——它只影响之后新建的接口(VLAN、隧道、容器 veth),对已存在的 eth0 无效。net/ipv4/arp.c(arp_process / arp_ignore / arp_solicit)、include/linux/inetdevice.h(IN_DEV_MAXCONF)、Documentation/networking/ip-sysctl.rst——文中代码取自 torvalds/master,这几段十几年没大改,行号会随版本漂移。操作步骤参考 BytePlus ECS 内核微调文档(docs.byteplus.com,搜 arp_announce)。老规矩:原理不过时,命令动手前先看清自己的接口名。nf_conntrack: table full 告警背后的故事。关注不迷路。














































































