Describe the bug(问题描述)
配置了 ChannelOptions::client_host 和 device_name 的 TCP channel,在 socket 失败并经过 health-check/revive 后,会丢失显式的源 IP 绑定。
首次建连执行 SO_BINDTODEVICE 和 bind(配置IP:0)。但 WaitAndReset() 将 _local_side 清为 0.0.0.0:0,仍保留 _device_name,导致后续 health-check 探测连接以及实际 RPC 重建的连接只绑定设备,不再调用 bind()。
根据源码分析,这应是 #3179 引入客户端绑定功能时遗漏的连接恢复路径:配置的绑定地址与运行中的实际本地端点共用了 _local_side。
OnCreated() 保存 options.local_side 和 options.device_name。
- 首次连接建立后,
ResetFileDescriptor() 通过 getsockname() 将 _local_side 覆盖为实际 IP:临时端口。
- socket 失败后,
WaitAndReset() 关闭旧 fd、清空 _local_side,但保留 _device_name。
CheckHealth() 调用 Connect():继续设置 SO_BINDTODEVICE,但因本地 IP 为 ANY 而跳过 bind()。成功的探测 fd 被直接关闭,没有经过 ResetFileDescriptor() 安装,因此也不会回填 _local_side。
Revive() 恢复 socket 的可用状态。下一次业务写入经 ConnectIfNot() 重建连接,仍然跳过 IP bind。
以下源码链接固定到本次检查的上游 master commit ffaa33e7395af6d58b8ac0d07b518ecd6023a08d:
To Reproduce(复现方法及已有验证)
下面是根据源码路径整理的 brpc 复现步骤,尚未完成独立的 brpc 回归程序及其系统调用抓取:
- 使用非本机 TCP 服务端,创建持久的
connection_type = "single" channel,配置有效的本地 client_host 及对应 device_name。
- 成功发送一次 RPC,保留该 channel。
- 让测试服务端关闭或 reset 已接受的连接,使客户端 socket 被 brpc 标记为 failed;保持或恢复服务端监听,使 health-check 可以成功。
- 等待该 socket revive,再经同一 channel 发送 RPC。
- 跟踪
socket、setsockopt、bind、connect,同时检查临时 health-check fd 和后续业务 fd。根据上述源码路径,只有初次连接执行 bind(配置IP:0)。
已有现场日志确认:后续发生 TCP 故障的同一 SocketId、同一 socket 对象,在故障之前已经历过 revive。这支持生命周期前置条件,但日志本身不能证明当时具体执行了哪些 bind 系统调用。
另外,已用独立的 Linux TCP socket 测试验证“丢失 bind”可能带来的四元组冲突影响。测试程序逻辑如下:
- 建立一条设置了
SO_BINDTODEVICE 的客户端长连接 A,仅切换它是否额外调用 bind(本地IP:0)。
- 创建不设置设备绑定、也不执行 bind 的 socket B,连接同一服务端。
- 在隔离 network namespace 中使用小范围临时端口池;A 获得端口 P 后,预留其余端口,使 B 只能选择 P 或连接失败。两个客户端均关闭
SO_REUSEADDR。
- A 只绑定设备时:B 第一次尝试即复用相同线上四元组并连接成功,服务端观察到原 A 连接被 reset。
- A 同时执行
bind(本地IP:0) 时:B 的 4 次尝试均返回 EADDRNOTAVAIL,服务端旧连接未被 reset。
这组实验直接使用 Linux socket,不调用 brpc;它验证的是退化后连接配置在所测内核上的影响,不等同于完整的 brpc revive 复现,也不代表所有 Linux 版本都存在同样的碰撞行为。
Expected behavior(预期行为)
显式配置的客户端源 IP 应在 channel 生命周期内始终生效,包括 health-check 探测连接以及 revive 后重建的业务连接。配置的设备绑定也应保留。
一次可恢复的连接失败不应静默改变客户端绑定策略。
Versions(版本信息)
- OS:Linux x86_64。独立 TCP 对照实验内核为
5.14.0-162.6.1.el9_1.x86_64;原故障节点内核为 5.14.0-570.17.1.el9_6.x86_64。
- Compiler:尚未采集原故障二进制的精确编译器版本。
- brpc:下游使用的包版本为
1.18.0-rc1,源码 commit 为 3fe5abfcdc3285422045b98742c998413f980e00。2026-09-07 检查的上游 master ffaa33e7395af6d58b8ac0d07b518ecd6023a08d 仍包含相同的相关逻辑;本次对上游 master 做了源码核对,未重新构建运行。
- protobuf:独立 TCP 对照实验不依赖 protobuf;原故障二进制实际链接的版本尚未独立核实。
Additional context/screenshots(补充说明)
建议将“配置的本地绑定端点”与“实际连接端点”分开保存,所有 Connect() 均使用持久配置决定是否执行 bind;_local_side 继续表达运行态,在 reset 时可以清空。
不宜直接删除 _local_side 的清零逻辑:已建立连接后,它包含实际临时端口,直接保留可能让重连尝试绑定旧端口,而非原配置的端口 0。
四元组冲突属于额外的影响证据。本 issue 的核心是:即使某个内核或拓扑不会产生碰撞,brpc 在恢复过程中丢失用户配置的源 IP bind,仍然是需要修复的行为。
Describe the bug(问题描述)
配置了
ChannelOptions::client_host和device_name的 TCP channel,在 socket 失败并经过 health-check/revive 后,会丢失显式的源 IP 绑定。首次建连执行
SO_BINDTODEVICE和bind(配置IP:0)。但WaitAndReset()将_local_side清为0.0.0.0:0,仍保留_device_name,导致后续 health-check 探测连接以及实际 RPC 重建的连接只绑定设备,不再调用bind()。根据源码分析,这应是 #3179 引入客户端绑定功能时遗漏的连接恢复路径:配置的绑定地址与运行中的实际本地端点共用了
_local_side。OnCreated()保存options.local_side和options.device_name。ResetFileDescriptor()通过getsockname()将_local_side覆盖为实际 IP:临时端口。WaitAndReset()关闭旧 fd、清空_local_side,但保留_device_name。CheckHealth()调用Connect():继续设置SO_BINDTODEVICE,但因本地 IP 为 ANY 而跳过bind()。成功的探测 fd 被直接关闭,没有经过ResetFileDescriptor()安装,因此也不会回填_local_side。Revive()恢复 socket 的可用状态。下一次业务写入经ConnectIfNot()重建连接,仍然跳过 IP bind。以下源码链接固定到本次检查的上游 master commit
ffaa33e7395af6d58b8ac0d07b518ecd6023a08d:To Reproduce(复现方法及已有验证)
下面是根据源码路径整理的 brpc 复现步骤,尚未完成独立的 brpc 回归程序及其系统调用抓取:
connection_type = "single"channel,配置有效的本地client_host及对应device_name。socket、setsockopt、bind、connect,同时检查临时 health-check fd 和后续业务 fd。根据上述源码路径,只有初次连接执行bind(配置IP:0)。已有现场日志确认:后续发生 TCP 故障的同一 SocketId、同一 socket 对象,在故障之前已经历过 revive。这支持生命周期前置条件,但日志本身不能证明当时具体执行了哪些 bind 系统调用。
另外,已用独立的 Linux TCP socket 测试验证“丢失 bind”可能带来的四元组冲突影响。测试程序逻辑如下:
SO_BINDTODEVICE的客户端长连接 A,仅切换它是否额外调用bind(本地IP:0)。SO_REUSEADDR。bind(本地IP:0)时:B 的 4 次尝试均返回EADDRNOTAVAIL,服务端旧连接未被 reset。这组实验直接使用 Linux socket,不调用 brpc;它验证的是退化后连接配置在所测内核上的影响,不等同于完整的 brpc revive 复现,也不代表所有 Linux 版本都存在同样的碰撞行为。
Expected behavior(预期行为)
显式配置的客户端源 IP 应在 channel 生命周期内始终生效,包括 health-check 探测连接以及 revive 后重建的业务连接。配置的设备绑定也应保留。
一次可恢复的连接失败不应静默改变客户端绑定策略。
Versions(版本信息)
5.14.0-162.6.1.el9_1.x86_64;原故障节点内核为5.14.0-570.17.1.el9_6.x86_64。1.18.0-rc1,源码 commit 为3fe5abfcdc3285422045b98742c998413f980e00。2026-09-07 检查的上游 masterffaa33e7395af6d58b8ac0d07b518ecd6023a08d仍包含相同的相关逻辑;本次对上游 master 做了源码核对,未重新构建运行。Additional context/screenshots(补充说明)
建议将“配置的本地绑定端点”与“实际连接端点”分开保存,所有
Connect()均使用持久配置决定是否执行 bind;_local_side继续表达运行态,在 reset 时可以清空。不宜直接删除
_local_side的清零逻辑:已建立连接后,它包含实际临时端口,直接保留可能让重连尝试绑定旧端口,而非原配置的端口 0。四元组冲突属于额外的影响证据。本 issue 的核心是:即使某个内核或拓扑不会产生碰撞,brpc 在恢复过程中丢失用户配置的源 IP bind,仍然是需要修复的行为。