Skip to content
团子云技术 Lite 1.048596
Go back

brpc client socket 异常路径拆解

笔者基于线上实际问题,以及 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"]

代码入口主要在:

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);
}

这一段有三个效果:

  1. 让普通请求再也 Address() 不到这个 SocketId
  2. 唤醒当前还挂在这个 socket 上等待 response 的 RPC。
  3. 如果启用了 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"]

对应代码:

这一类 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_POOLEDCONNECTION_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()

先看两个轻量失败:

  1. data 为空、pipelined_count 太大、内存不足等,直接给当前请求设置错误。
  2. 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;
}

这里要区分:

写失败后,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 peerE110 Connection timed outE111 Connection refusedE101 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 模式下会看到两层状态:

十一、几类日志的读法

E1008 Reached timeout

本次 RPC 超过 cntl.timeout_ms()。它优先指向”请求没有按时完成”,不直接证明 socket 已经断开。

常见原因:

E104 Connection reset by peer

已有连接被对端 RST。它通常是连接断开的强信号。

常见原因:

E110 Connection timed out

connect 阶段超时。它发生在新建连接阶段,和 RPC response timeout 属于不同层次。

常见原因:

E101 Network is unreachable

可能来自系统,也可能来自 brpc 对连续 connect timeout 的升级归类。需要结合前后是否有大量 E110 看。

E111 Connection refused

对端端口明确拒绝连接,一般说明没有进程监听,或者内核主动 RST。

E112 Not connected

brpc 层快失败。读这类日志时要往前找第一个把 socket 置为 failed 的错误。

常见前置错误:

十二、用 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

这个模型能解释三种看似矛盾的日志同时出现:

如果还看到 E110/E101/E111,说明 brpc 后续尝试建立新连接或 health check 时也失败了。

十三、排障顺序

遇到大量 brpc client 错误时,可以按这个顺序看:

  1. 先按 endpoint 聚合错误码和时间线。
  2. 找每个 endpoint 的第一个非 E112 错误。
  3. 区分 E1008 和 connect/read/write 错误:
    • E1008 看应用层响应链路。
    • E104/EPIPE/EOF 看已有连接被关闭。
    • E110/E111/E101 看新建连接。
  4. 查是否有 isolated by circuit breaker
  5. 查 health check 配置:
    • health_check_interval_s
    • health_check_path
    • health_check_timeout_ms
  6. 查连接类型:
    • single 连接更容易把 endpoint 状态直接暴露成 E112
    • pooled/short 需要区分”单条连接失败”和”main socket 被置为 failed”。
  7. 对 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 timeoutRPC timer不一定请求超时,应用层没有按时完成
E104 Connection reset by peerread已有连接被对端 reset
E110 Connection timed outconnect新建连接阶段超时
E101 Network is unreachableconnect / brpc 升级归类路径不可达或连续 connect timeout
E111 Connection refusedconnect端口拒绝连接
E112 Not connectedIssueRPC 前置检查当前 RPC 直接失败目标 socket 当前不可用
EOVERCROWDEDWrite 前置检查socket 未写出数据过多,brpc 拒绝继续排队
isolated by circuit breakerOnCallEnd 反馈熔断器主动隔离节点

结语

brpc client 的 socket 异常路径可以归成两层:

第一层是真正把 socket 置为 failed 的事件:read、write、connect、EOF、协议错误、熔断。它们会进入 Socket::SetFailed(),改变 SocketId 的可寻址状态。

第二层是 failed 状态带来的后续表现:新 RPC 在 Controller::IssueRPC() 里发现 socket 不可用,直接返回 E112;health check 在后台尝试 WaitAndReset()Revive(),成功后 socket 才重新接流量。

排障时要抓第一层错误。看到大量 E112 时,继续往前翻,找到第一个 E1008E104E110E111E101 或熔断日志,再结合 server 侧时间线判断是应用假死、连接断开、监听不可用,还是主动隔离。


Share this post on:

Previous Post
宏观金融危机环境下的资产保值机制与跨周期动态表现深度剖析
Next Post
C++ 服务端 Coredump 假死之谜:TCP 黑洞现象剖析与 brpc 最佳实践