Skip to content

问题现象

环境: datacenter 客户端-服务端通信场景

触发条件:

  1. 启动 datacenter
  2. 启动 server 端
  3. 启动 client 端
  4. 断开 server 端

观察到的异常:

  • 客户端进程的文件描述符 (fd) 持续增长
  • lsof 显示大量 eventfd 未释放
  • Valgrind 报告 "invalid file descriptor in syscall eventfd 2 ()"
  • 进程最终因达到 fd 上限而崩溃

问题定位过程

1. FD 泄漏检测

bash
# 监控 fd 数量变化
watch -n 1 'ls /proc/`pidof service_cli_demo`/fd | wc -l'

结果: fd 数量持续增长

2. Lsof 分析

bash
lsof -p <pid>

发现: 大量 eventfd 未释放

service_c 863452 user  131 u  a_inode   0,14        0    12612 [eventfd]
service_c 863452 user  132 u  a_inode   0,14        0    12612 [eventfd]
service_c 863452 user  133 u  a_inode   0,14        0    12612 [eventfd]
...

3. Valgrind 内存检查

bash
valgrind --leak-check=full --track-fds=yes --log-file=valgrind.log ./service_cli_demo 1

关键发现:

cpp
==982074== Warning: invalid file descriptor 1031 in syscall eventfd2()
==982074== FILE DESCRIPTORS: 1024 open at exit.
==982074== Open file descriptor 1023:
==982074==    at 0x502654B: eventfd (syscall-template.S:78)
==982074==    by 0x5128D2D: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x51513A3: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x512CB76: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x5154FAD: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x51219F9: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x514AB52: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x51520C6: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x511696B: ??? (in /usr/lib/x86_64-linux-gnu/libzmq.so.5.2.2)
==982074==    by 0x499A2CB: zmq::socket_t::socket_t(zmq::context_t&, int) (zmq.hpp:1600)
==982074==    by 0x499A378: zmq::socket_t::socket_t(zmq::context_t&, zmq::socket_type) (zmq.hpp:1608)
==982074==    by 0x49D68E0: datacenter::MsgCenterBaseImpl::QryServerPort(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, datacenter::proto::ServerType) (msg_center_base_impl_broker.cpp:122)

调用栈指向:

cpp
datacenter::MsgCenterBaseImpl::QryServerPort()
  -> zmq::socket_t::socket_t()
    -> libzmq.so eventfd()

4. GDB 堆栈分析

bash
gdb attach <pid>

发现 ZMQ IO 线程阻塞:

cpp
Thread 5 (LWP 14473):
#0  __GI___poll (fds=0xffff8dff8d00, nfds=1, timeout=...)
#1  ?? () from /lib/libzmq.so.5
...
#5  zmq_msg_send () from /lib/libzmq.so.5

结论: ZMQ IO 线程阻塞在 zmq_msg_send() 调用中

5. 日志分析

重连成功后出现大量 Monitor 事件:

log
[2026-02-05 12:27:56.306] [net] [debug] Monitor event: 4, info: tcp://127.0.0.1:36035
[2026-02-05 12:27:56.306] [net] [debug] Monitor event: 2, info: tcp://127.0.0.1:36035
[2026-02-05 12:27:56.306] [net] [debug] Monitor event: 128, info: tcp://127.0.0.1:36035
... (重复数百次)

Monitor 事件对照表:

事件宏 十进制值 说明
ZMQ_EVENT_CONNECT_DELAYED 2 连接被延迟
ZMQ_EVENT_CONNECT_RETRIED 4 连接重试
ZMQ_EVENT_CLOSED 128 套接字关闭
ZMQ_EVENT_CONNECTED 1 连接成功
ZMQ_EVENT_HANDSHAKE_SUCCEED 4096 握手成功(十六进制 0 x 1000)

根因分析

问题链路

1. Server 端断开

2. Client 端 Monitor Socket 检测到断开事件

3. Boost 定时器被取消,Monitor recv 停止从缓冲区取数据

4. 主 Socket 持续重连,产生大量 Monitor 事件

5. Monitor Socket 发送缓冲区满(消息积压)

6. zmq_msg_send() 阻塞(部分 socket 类型会阻塞发送)

7. ZMQ IO 线程阻塞

8. 新创建的 zmq::socket_t 中 eventfd 无法释放

9. FD 泄漏

核心问题

  1. Monitor 接收端暂停消费

    • 断开连接时,Boost 定时器被取消
    • Monitor socket 的 recv 操作停止
    • 接收缓冲区消息积压
  2. Monitor 发送端持续发送

    • 主 socket 在重连过程中产生大量事件
    • Monitor socket 持续发送事件到 inproc 通道
    • 发送缓冲区满后阻塞
  3. IO 线程阻塞

    • 某些类型的 zmq::socket_t 缓冲区满时会阻塞发送
    • ZMQ IO 线程被 zmq_msg_send() 阻塞
  4. 资源无法释放

    • IO 线程阻塞导致 socket 析构被延迟
    • Socket 内部的 eventfd 无法正常关闭

实验验证

调大 Monitor 接收缓冲区:

  • FD 增长速度明显变慢
  • 验证了缓冲区满是触发条件

解决方案

临时方案

  1. Register Socket 优化()
    cpp
    // 使用常驻 socket 避免频繁创建销毁
    // 避免重连时重复创建 socket 导致 eventfd 泄漏

register socket 作为常驻 socket 时一旦 zmq io 阻塞,代码逻辑会执行两次 send,zmq 报错:

bash
terminate called after throwing an instance of 'zmq::error_t'
  what():  Operation cannot be accomplished in current state
  1. 屏蔽重连事件(已实施)
    cpp
    // 在 Monitor 中过滤 CONNECT_RETRIED 和 CONNECT_DELAYED 事件
    // 减少消息积压

待深入研究的问题

核心疑问:

为什么 ZMQ IO 线程阻塞会导致申请的 eventfd 无法释放?

可能的原因:

  1. zmq::socket_t 析构函数需要 IO 线程协作完成
  2. eventfd 的关闭操作在 IO 线程中执行
  3. 析构过程中需要等待 IO 线程清空发送队列

需要验证:

  • [ ] 查看 libzmq 源码中 socket 析构流程
  • [ ] 确认 eventfd 创建和释放的具体时机
  • [ ] 分析 IO 线程与主线程的同步机制

基于 VitePress 构建