apache / apache/brpc

Socket health-check/revive 后丢失 client_host 绑定,但仍保留 SO_BINDTODEVICE

Closed
#3,530 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
17.6k
Forks
4.1k
Avg merge
2d 12h
Merged PRs (30d)
69

Description

**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`。

1. `OnCreated()` 保存 `options.local_side` 和 `options.device_name`。
2. 首次连接建立后,`ResetFileDescriptor()` 通过 `getsockname()` 将 `_local_side` 覆盖为实际 IP:临时端口。
3. socket 失败后,`WaitAndReset()` 关闭旧 fd、清空 `_local_side`,但保留 `_device_name`。
4. `CheckHealth()` 调用 `Connect()`:继续设置 `SO_BINDTODEVICE`,但因本地 IP 为 ANY 而跳过 `bind()`。成功的探测 fd 被直接关闭,没有经过 `ResetFileDescriptor()` 安装,因此也不会回填 `_local_side`。
5. `Revive()` 恢复 socket 的可用状态。下一次业务写入经 `ConnectIfNot()` 重建连接,仍然跳过 IP bind。

以下源码链接固定到本次检查的上游 master commit `ffaa33e7395af6d58b8ac0d07b518ecd6023a08d`:

- [Socket 初始化](https://github.com/apache/brpc/blob/ffaa33e7395af6d58b8ac0d07b518ecd6023a08d/src/brpc/socket.cpp#L744)
- [WaitAndReset 清空本地端点](https://github.com/apache/brpc/blob/ffaa33e7395af6d58b8ac0d07b518ecd6023a08d/src/brpc/socket.cpp#L1017)
- [Connect 独立判断设备绑定和 IP bind](https://github.com/apache/brpc/blob/ffaa33e7395af6d58b8ac0d07b518ecd6023a08d/src/brpc/socket.cpp#L1282)
- [health-check 的 reset/check/revive 顺序](https://github.com/apache/brpc/blob/ffaa33e7395af6d58b8ac0d07b518ecd6023a08d/src/brpc/details/health_check.cpp#L184)

**To Reproduce(复现方法及已有验证)**

下面是根据源码路径整理的 brpc 复现步骤,尚未完成独立的 brpc 回归程序及其系统调用抓取:

1. 使用非本机 TCP 服务端,创建持久的 `connection_type = "single"` channel,配置有效的本地 `client_host` 及对应 `device_name`。
2. 成功发送一次 RPC,保留该 channel。
3. 让测试服务端关闭或 reset 已接受的连接,使客户端 socket 被 brpc 标记为 failed;保持或恢复服务端监听,使 health-check 可以成功。
4. 等待该 socket revive,再经同一 channel 发送 RPC。
5. 跟踪 `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,仍然是需要修复的行为。

Contributor guide

Open the contributing guide

Research direction

Start in src/brpc/socket.cpp at OnCreated(), WaitAndReset(), ResetFileDescriptor(), and Connect(), then follow the reset/check/revive flow in src/brpc/details/health_check.cpp. Verify how configured client_host and device_name are retained separately from the runtime local endpoint. Done means health-check and post-revive connections preserve both bindings; validate with the issue’s socket and syscall-tracing reproduction.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.