笔者基于线上实际问题,以及 brpc code base,和 codex 深度讨论形成
这篇文章整理 brpc client 在常见异常场景下的 socket 状态变化、错误码来源和日志表现。重点放在一条实际排障中最容易混淆的链路:
上游进程 coredump 或卡死时,client 先看到 RPC timeout;当对端进程退出、连接被内核关闭或重连失败后,brpc 把 socket 标成 failed;后续请求在发包前快速失败,日志表现为
E112 Not connected ...。
理解这条链路,需要先把 brpc 里的几个对象分清楚。
一、brpc client socket 的基本模型
一次 client RPC 大致走这几步:
graph LR
A["Channel.CallMethod"] --> B["Controller.IssueRPC"]
B --> C["选目标 SocketId"]
C --> D["Socket.Address / IsAvailable"]
D --> E["选择连接: single / pooled / short"]
E --> F["pack request"]
F --> G["Socket.Write"]
G --> H["等待 response / error / timeout"]
代码入口主要在:
src/brpc/channel.cpp:创建 timeout timer、调用IssueRPC()。src/brpc/controller.cpp:选 server、写请求、处理 RPC 完成和 retry。src/brpc/socket.cpp:connect、write、read 失败后的 socket 状态切换。src/brpc/input_messenger.cpp:fd 可读事件、read 错误、协议解析错误。src/brpc/versioned_ref_with_id.h:SocketId到 socket 对象的可寻址状态。
brpc 里的 SocketId 带有比普通指针更多的状态信息。它背后是 VersionedRefWithId<T>:SocketId 编码了 slot 和 version。socket 正常时,Address(id, &ptr) 能拿到对象;socket 被 SetFailed() 后,version 会从 created version 变成 failed version,普通 Address() 就拿不到它。health check 能通过 AddressFailedAsWell() 访问 failed socket,再在探活成功后 Revive()。
这解释了一个常见现象:同一个 endpoint 出现一次真实网络错误后,后续大量 RPC 不一定真的都尝试了 TCP connect。它们可能只是命中了 brpc 里已经 failed 的 SocketId,在 IssueRPC() 前半段就返回了 E112 Not connected。
二、Socket::SetFailed 是状态切换中心
异常路径最终大多汇到 Socket::SetFailed()。
int Socket::SetFailed(int error_code, const char* error_fmt, ...) {
std::string error_text;
...
return VersionedRefWithId<Socket>::SetFailed(error_code, error_text);
}
VersionedRefWithId::SetFailed() 做的关键动作是把 version 加 1:
// Try to set version=id_ver+1 (to make later address() return NULL)
_versioned_ref.compare_exchange_strong(
vref, MakeVRef(id_ver + 1, NRefOfVRef(vref)), ...);
成功后回调 Socket::OnFailed():
void Socket::OnFailed(int error_code, const std::string& error_text) {
_error_code = error_code;
_error_text = error_text;
if (HCEnabled()) {
GetOrNewSharedPart()->circuit_breaker.MarkAsBroken();
StartHealthCheck(id(), isolation_duration_ms);
}
bthread_id_list_reset2_pthreadsafe(
&_id_wait_list, error_code, error_text, ...);
ResetAllStreams(error_code, error_text);
}
这一段有三个效果:
- 让普通请求再也
Address()不到这个SocketId。 - 唤醒当前还挂在这个 socket 上等待 response 的 RPC。
- 如果启用了 health check,启动恢复流程。
所以 SetFailed() 的作用超过记录错误日志,它会改变后续请求的命运。
三、场景一:应用层无响应,最终表现为 RPC timeout
典型日志:
[E1008]Reached timeout=20000ms @10.251.52.82:9010
路径:
graph LR
A["请求已发出"] --> B["对端 TCP 连接仍存在"]
B --> C["没有业务 response"]
C --> D["Channel timeout timer 触发"]
D --> E["bthread_id_error(..., ERPCTIMEDOUT)"]
E --> F["Controller.HandleSocketFailed"]
F --> G["E1008 Reached timeout"]
对应代码:
Channel::HandleTimeout()调bthread_id_error(correlation_id, ERPCTIMEDOUT)。Controller::HandleSocketFailed()把ERPCTIMEDOUT格式化成Reached timeout=%dms @host。
这一类 timeout 本身不等于 socket 已经坏了。它只能说明本次 RPC 在 brpc 的 timeout 窗口内没有完成。对端 coredump、业务线程卡死、server receive buffer 被打满、协议处理线程阻塞,都可能导致这个结果。
在 coredump 场景里,内核 TCP 栈可能继续 ACK client 数据,client 端看不到 RST/FIN。brpc 已经把 request 写进 socket,但业务 response 不回来,于是最终只得到 E1008。
默认 retry policy 里没有把 ERPCTIMEDOUT 当成可重试错误。brpc 源码里的注释也写了:ERPCTIMEDOUT/ECANCELED are not retrying error by default。这点很重要,RPC timeout 和 connect timeout 的处理策略不同。
四、场景二:connect 失败,socket 被打成 failed
典型日志:
[E110]Fail to connect Socket{id=... addr=10.251.52.83:9010}: Connection timed out
[E101]Fail to connect Socket{id=... addr=10.251.52.83:9010}: Network is unreachable
[E111]Fail to connect Socket{id=... addr=10.251.52.82:9010}: Connection refused
路径:
graph LR
A["Socket.Write"] --> B["ConnectIfNot"]
B --> C["AfterAppConnected(err)"]
C --> D["err != 0"]
D --> E["s->SetFailed(err, Fail to connect ...)"]
E --> F["ReleaseAllFailedWriteRequests"]
F --> G["等待在这次 write 上的 RPC 被唤醒"]
connect 超时时还有一个细节:connect_timeout_as_unreachable 默认是 3。连续 connect timeout 达到阈值后,brpc 会把 ETIMEDOUT 转成 ENETUNREACH:
if (err == ETIMEDOUT) {
if (++num_continuous_connect_timeouts >= FLAGS_connect_timeout_as_unreachable) {
err = ENETUNREACH;
}
}
所以日志中 E101 Network is unreachable 的来源需要结合上下文判断。它可能来自操作系统原始返回的 ENETUNREACH,也可能是 brpc 对连续 connect timeout 的归类,用来更快地把路径判成不可达。
对 CONNECTION_TYPE_POOLED 和 CONNECTION_TYPE_SHORT,connect 类错误还有可能连带把 main socket 标成 failed。Controller::Call::OnComplete() 里有一段判断:
return error_code == ECONNREFUSED ||
error_code == ENETUNREACH ||
error_code == EHOSTUNREACH ||
error_code == EINVAL;
这些错误被认为很可能代表 server 侧不可用。pooled/short 连接遇到这些错误时,brpc 会 Socket::SetFailed(peer_id),触发 main socket health check。后续请求就可能进入 E112 快失败。
五、场景三:read 失败,已有连接被判死
典型日志:
[E104]Fail to read from Socket{id=... fd=307 addr=10.251.52.82:9010:38758}: Connection reset by peer
路径:
graph LR
A["fd 可读事件"] --> B["InputMessenger.OnNewMessages"]
B --> C["read 返回错误,errno != EAGAIN/EINTR"]
C --> D["m->SetFailed(errno, Fail to read ...)"]
D --> E["唤醒未完成 RPC"]
E --> F["普通 Address 不再能拿到该 SocketId"]
代码在 src/brpc/input_messenger.cpp:
if (errno != EAGAIN) {
const int saved_errno = errno;
PLOG(WARNING) << "Fail to read from " << *m;
m->SetFailed(saved_errno, "Fail to read from %s: %s", ...);
return;
}
ECONNRESET 对应 Linux errno 104。它说明 TCP 连接已经被对端 reset。对 coredump 事件来说,这通常出现在进程真正退出、fd 被内核回收、服务重启或监听进程替换之后。
这类错误比 E1008 更接近”连接已断”的证据。E1008 只说明业务响应没回来;E104 说明内核已经告诉 client 这条连接不能用了。
六、场景四:write 失败和写队列拥塞
写请求路径在 Socket::Write() 和 Socket::StartWrite()。
先看两个轻量失败:
data为空、pipelined_count太大、内存不足等,直接给当前请求设置错误。- socket 已经 failed,
ConductError()会把_error_code/_error_text回填给当前 RPC。
然后是拥塞保护:
if (!opt.ignore_eovercrowded && _overcrowded) {
return SetError(opt.id_wait, EOVERCROWDED);
}
_overcrowded 来自每个 socket 的未写出数据量限制,默认 socket_max_unwritten_bytes = 64MB。触发后 RPC 会得到 EOVERCROWDED,但 TCP 连接并未因此死亡;它表示 brpc 不想让这个 socket 上的排队写数据继续无限增长。
真正会把 socket 置为 failed 的写错误发生在 StartWrite() 或 KeepWrite():
if (nw < 0 && errno != EAGAIN && errno != EOVERCROWDED) {
SetFailed(saved_errno, "Fail to write into ...");
ReleaseAllFailedWriteRequests(req);
}
后台写线程 KeepWrite() 里也类似:
if (nw < 0 && errno != EAGAIN && errno != EOVERCROWDED) {
s->SetFailed(saved_errno, "Fail to keep-write into ...");
break;
}
这里要区分:
EAGAIN:非阻塞 fd 暂时写不进去,等待 epollout。EOVERCROWDED:brpc 自己的写队列保护。EPIPE/ECONNRESET/其他 errno:写失败,socket failed。
写失败后,ReleaseAllFailedWriteRequests() 会把当前链表里还没成功写完的请求全部唤醒,返回同一个 socket failure 原因。
七、场景五:E112 Not connected 快失败
典型日志:
[E112]Not connected to 10.251.52.82:9010 yet, server_id=0
路径:
graph LR
A["之前某次 read/connect/write/circuit breaker 已调用 SetFailed"] --> B["SocketId 普通 Address 失败"]
B --> C["新的 RPC 进入 Controller.IssueRPC"]
C --> D["SingleServer: Socket.Address(_single_server_id)"]
D --> E["rc != 0 或 IsAvailable=false"]
E --> F["SetFailed(EHOSTDOWN, Not connected ...)"]
F --> G["HandleSendFailed,立即结束 RPC"]
对应代码:
const int rc = Socket::Address(_single_server_id, &tmp_sock);
if (rc != 0 || (!is_health_check_call() && !tmp_sock->IsAvailable())) {
SetFailed(EHOSTDOWN, "Not connected to %s yet, server_id=%" PRIu64, ...);
return HandleSendFailed();
}
这里的 E112 是 Linux EHOSTDOWN。它表示 brpc 在发请求前发现目标 socket 当前不可用,并直接结束本次 RPC;当前 RPC 通常还没有真正发到网络上。
这种快失败会在日志里大量出现,因为业务可能持续向同一个不可用 endpoint 发请求。真正导致 socket failed 的错误一般在更早的位置,比如 E104 Connection reset by peer、E110 Connection timed out、E111 Connection refused、E101 Network is unreachable,或者 circuit breaker 的 isolate 日志。
八、场景六:health check 和 revive
socket failed 后,是否会恢复,取决于 health check。
Socket::OnFailed() 里只有 HCEnabled() 为真才会启动探活:
if (HCEnabled()) {
circuit_breaker.MarkAsBroken();
StartHealthCheck(id(), isolation_duration_ms);
}
HCEnabled() 的条件是:
return _health_check_interval_s > 0 && _is_hc_related_ref_held;
health check 流程:
graph LR
A["SetFailed"] --> B["StartHealthCheck"]
B --> C["AddressFailedAsWell"]
C --> D["WaitAndReset"]
D --> E["CheckHealth"]
E --> F{"成功?"}
F -->|"否"| G["按 health_check_interval_s 继续"]
F -->|"是"| H["Revive"]
H --> I["SocketId 重新可 Address"]
WaitAndReset() 会等待旧引用释放到预期值,然后关闭旧 fd、清理读缓冲、认证状态、pipeline 队列等连接相关状态。随后 CheckHealth() 成功才会 Revive()。
默认 health_check_path 为空时,health check 的标准偏 TCP 连接可达;配置了 health_check_path 后,还要发 HTTP health check 请求,应用层成功才会完成恢复。配置应用层 health check 更适合识别”进程还在但业务不工作”的假活。
IsAvailable() 也会看 _ninflight_app_health_check:
return !_logoff_flag && _ninflight_app_health_check == 0;
这意味着应用层 health check 正在进行时,普通业务请求也会被挡住,避免 health check 尚未确认成功就恢复流量。
九、场景七:circuit breaker 隔离
每个 call 完成后,Controller::Call::OnComplete() 会把错误反馈给 socket:
if (error_code != 0) {
sending_sock->AddRecentError();
}
if (enable_circuit_breaker) {
sending_sock->FeedbackCircuitBreaker(error_code, latency);
}
FeedbackCircuitBreaker() 内部调用 circuit_breaker.OnCallEnd()。一旦熔断器认为节点应该隔离,会调用:
SetFailed(main_socket_id());
LOG(ERROR) << "Socket[" << *this << "] isolated by circuit breaker";
这条路径和网络错误的结果类似:main socket 被置为 failed,后续请求可能 E112 快失败,health check 负责后续恢复。
排障时要注意日志里有没有:
isolated by circuit breaker
如果有,E112 不一定来自 TCP 层故障,也可能来自错误率/延迟触发的主动隔离。
十、连接类型对失败传播的影响
brpc 支持三种常见连接类型:
| 类型 | 发请求时用的 socket | 失败影响 |
|---|---|---|
CONNECTION_TYPE_SINGLE | 直接使用 main socket | 该 socket failed 后,后续单 server 请求 E112 |
CONNECTION_TYPE_POOLED | 从 pool 取一个连接 | 单个 pooled socket 失败会被丢弃;某些 connect 类错误会连带把 main socket 置为 failed |
CONNECTION_TYPE_SHORT | 每次创建短连接 | 当前短连接失败会结束;某些 connect 类错误会连带把 main socket 置为 failed |
pooled 连接有个细节:如果请求失败但已经收到了 response,socket 可以归还池子;如果失败且没有 response,这条连接不会复用,避免未来迟到 response 破坏”一条 pooled 连接同一时刻只承载一个消息”的假设。
if (sending_sock != NULL && (error_code == 0 || responded)) {
sending_sock->ReturnToPool();
break;
}
// fall through to short: sending_sock->SetFailed()
这也是为什么 pooled 模式下会看到两层状态:
- 单条 pooled socket 被关闭或丢弃。
- main socket 被置为 failed,触发
E112和 health check。
十一、几类日志的读法
E1008 Reached timeout
本次 RPC 超过 cntl.timeout_ms()。它优先指向”请求没有按时完成”,不直接证明 socket 已经断开。
常见原因:
- server 进程 coredump 或卡住,TCP 层还 ACK,应用层不回包。
- server 业务线程池饱和。
- server 读不到请求或写不回响应。
- 网络丢包导致 request/response 卡住。
E104 Connection reset by peer
已有连接被对端 RST。它通常是连接断开的强信号。
常见原因:
- 对端进程退出,fd 被内核回收。
- 服务重启。
- 对端主动 abort connection。
- 中间设备发送 RST。
E110 Connection timed out
connect 阶段超时。它发生在新建连接阶段,和 RPC response timeout 属于不同层次。
常见原因:
- SYN 没有收到 SYN-ACK。
- listen/accept 队列满后内核丢 SYN。
- 网络路径丢包或被防火墙丢弃。
E101 Network is unreachable
可能来自系统,也可能来自 brpc 对连续 connect timeout 的升级归类。需要结合前后是否有大量 E110 看。
E111 Connection refused
对端端口明确拒绝连接,一般说明没有进程监听,或者内核主动 RST。
E112 Not connected
brpc 层快失败。读这类日志时要往前找第一个把 socket 置为 failed 的错误。
常见前置错误:
Fail to read ... Connection reset by peerFail to connect ... Connection timed outFail to connect ... Connection refusedNetwork is unreachableisolated by circuit breaker
十二、用 coredump 场景串起来
上游 coredump 时,client 侧经常看到这样的时间线:
sequenceDiagram
participant C as brpc client
participant K as server kernel TCP
participant P as server process
P->>P: crash, kernel writing core
C->>K: send request on existing connection
K-->>C: ACK
P--xC: no application response
C->>C: RPC timeout -> E1008
P->>K: process finally exits / fd closes
K-->>C: RST or FIN
C->>C: read fails -> E104, Socket.SetFailed
C->>C: later RPC Address failed -> E112
C->>K: health check / reconnect
K-->>C: connect timeout/refused/success
这个模型能解释三种看似矛盾的日志同时出现:
- 前期
E1008:TCP 没断,但应用层不回。 - 中间
E104:对端 fd 被关闭,已有连接收到 RST。 - 后期
E112:brpc 内部 socket 已经 failed,新请求不再进入正常发送路径。
如果还看到 E110/E101/E111,说明 brpc 后续尝试建立新连接或 health check 时也失败了。
十三、排障顺序
遇到大量 brpc client 错误时,可以按这个顺序看:
- 先按 endpoint 聚合错误码和时间线。
- 找每个 endpoint 的第一个非
E112错误。 - 区分
E1008和 connect/read/write 错误:E1008看应用层响应链路。E104/EPIPE/EOF看已有连接被关闭。E110/E111/E101看新建连接。
- 查是否有
isolated by circuit breaker。 - 查 health check 配置:
health_check_interval_shealth_check_pathhealth_check_timeout_ms
- 查连接类型:
- single 连接更容易把 endpoint 状态直接暴露成
E112。 - pooled/short 需要区分”单条连接失败”和”main socket 被置为 failed”。
- single 连接更容易把 endpoint 状态直接暴露成
- 对 coredump 类事件,结合 server 侧 core 开始/结束时间、服务重启时间、端口监听状态、accept 队列和 SYN 重传。
十四、几个判断边界
E1008 不能单独证明网络黑洞。它只说明 RPC 没按时完成。server coredump、业务卡死、线程池阻塞、网络丢包都可能让它出现。
E112 不能单独证明当前请求连不上网络。它大概率说明 brpc 已经把这个 SocketId 标成 failed,当前请求在发包前被挡住。
E101 不能单独按系统路由不可达理解。brpc 会把连续 connect timeout 转成 ENETUNREACH,要结合 connect_timeout_as_unreachable 和前序 E110 判断。
read/write/connect 错误一旦触发 Socket::SetFailed(),影响的是后续请求路径。大量 E112 往往是前序 socket failure 的后续表现,需要往前找最初原因。
健康检查默认偏连接可达。要识别”TCP 活着、应用死了”的假活,需要配置应用层 health check,或者由业务层/LB 做应用层探活。
十五、快速对照表
| 日志/错误 | 触发点 | 是否一定 SetFailed socket | 诊断含义 |
|---|---|---|---|
E1008 Reached timeout | RPC timer | 不一定 | 请求超时,应用层没有按时完成 |
E104 Connection reset by peer | read | 是 | 已有连接被对端 reset |
E110 Connection timed out | connect | 是 | 新建连接阶段超时 |
E101 Network is unreachable | connect / brpc 升级归类 | 是 | 路径不可达或连续 connect timeout |
E111 Connection refused | connect | 是 | 端口拒绝连接 |
E112 Not connected | IssueRPC 前置检查 | 当前 RPC 直接失败 | 目标 socket 当前不可用 |
EOVERCROWDED | Write 前置检查 | 否 | socket 未写出数据过多,brpc 拒绝继续排队 |
isolated by circuit breaker | OnCallEnd 反馈 | 是 | 熔断器主动隔离节点 |
结语
brpc client 的 socket 异常路径可以归成两层:
第一层是真正把 socket 置为 failed 的事件:read、write、connect、EOF、协议错误、熔断。它们会进入 Socket::SetFailed(),改变 SocketId 的可寻址状态。
第二层是 failed 状态带来的后续表现:新 RPC 在 Controller::IssueRPC() 里发现 socket 不可用,直接返回 E112;health check 在后台尝试 WaitAndReset() 和 Revive(),成功后 socket 才重新接流量。
排障时要抓第一层错误。看到大量 E112 时,继续往前翻,找到第一个 E1008、E104、E110、E111、E101 或熔断日志,再结合 server 侧时间线判断是应用假死、连接断开、监听不可用,还是主动隔离。