第7章:单个服务的基本结构

对齐原书第 7 章:服务模型演化、Reactor/one thread one loop、正确收发与缓冲、网络库分层、定时器和经典服务结构。

学习目标

  • 能比较原始串行、一连接一线程、Reactor 与 one thread one loop 的 ownership、阻塞传播和资源成本,选择服务并发结构
  • 能设计 EventLoop 的 I/O、wakeup、handle_other_things、timer 与 thread 分工,使同一 socket 的收发、关闭和 buffer state 由单 owner 推进
  • 能分析 Session/Connection/Buffer 分层、主动/被动关闭、长短连接、侵入式定时器和 high-water backpressure,定位服务性能瓶颈

机制总览

单服务请求路径与模块责任

  1. 1

    接入连接

    acceptor 只建立 session 与初始限制,不承载业务工作。

  2. 2

    执行请求

    decoder 产生 typed request,worker 在明确超时和并发预算内调用业务。

  3. 3

    回包清理

    响应按连接写队列串行化,关闭时取消 pending work。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

单服务请求路径与模块责任

选择请求阶段,检查 acceptor、session、业务执行和回包是否拥有清楚边界。

选择推理阶段

当前阶段 · 接入连接

acceptor 只建立 session 与初始限制,不承载业务工作。

可核验证据

accept latency、backlog 与 session 数。

单服务结构的好坏由请求路径是否可追踪、状态是否单一归属、慢依赖是否受控来判断。

失效—证据矩阵

单服务请求路径与模块责任

接入连接

典型失效

接入线程做 DNS、鉴权或数据库调用,监听队列被拖死。

核验证据

accept latency、backlog 与 session 数。

执行请求

典型失效

session 锁覆盖慢调用,单个请求阻塞同连接全部工作。

核验证据

queue time、锁等待、deadline 与 trace span。

回包清理

典型失效

多个线程并发写 socket,断连后任务继续持有 session。

核验证据

写队列深度、取消日志与析构断言。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么高性能服务首先是 ownership 设计

epoll、线程池和 lock-free queue 都不能替代明确 owner。若多个线程可同时 send、close、改 output offset 和移除 timer,任何局部优化都会扩大竞态。稳定结构先规定每个 listening fd、connected fd、Connection、Session、Buffer 与 Timer 由谁在哪条线程修改。

原书第 7 章从通信效率和服务模型出发,最终落到 one thread one loop、正确收发、缓冲区、网络库分层、定时器与经典结构。这些不是孤立技巧,而是同一个 ownership graph 的不同切面。

7.1 网络通信组件的效率、关闭与连接长度

网络通信组件的效率取决于 syscall 数、copy/allocation、event dispatch、cache locality、lock contention、protocol parsing 与 backpressure,不只取决于 fd 数。先用 throughput、tail latency、event-loop lag、queue depth 和 CPU profile 找瓶颈。

主动关闭与被动关闭要分开记录。RST、timeout、protocol error、server drain 也各有原因码。long-lived connection 摊薄握手成本,但占用 connection state、需要 heartbeat/idle timeout;short-lived connection 隔离简单,却增加 handshake、TIME_WAIT 与 TLS 开销。

7.2 原始服务器结构:accept 后同步处理

最原始结构在单线程中 accept → recv → process → send → close。代码直观,但任一慢 client、blocking business 或 partial I/O 都阻塞后续连接。它适合教学、低并发管理工具或受控批处理,不是通用高并发基础。

for (;;) {
  int client = accept(listen_fd, nullptr, nullptr);
  if (client < 0) continue;
  handle_connection_blocking(client);
  close(client);
}

7.3 一个连接对应一个线程

thread-per-connection 把阻塞隔离到各连接,开发简单;连接数高时 thread stack memory、scheduler context switch、run queue、per-thread cache 和 lock contention 快速增加。任务大量 idle 时,用一个 OS thread 表达一个等待连接浪费明显。

7.4 Reactor:readiness 到 handler 的分派

Reactor 将 fd 注册到 select/poll/epoll/kqueue,poller 返回 ready events,dispatcher 调用 accept/read/write/error handlers。readiness 只表示可推进,不表示完整 request;handler 必须短小、nonblocking,并维护 per-connection state。

先预测:1000 个连接中只有 20 个活跃时,thread-per-connection 与 4 个 event loops 各需要多少 OS threads;若 handler 内执行阻塞数据库调用,停顿会传播到哪些连接?

分步1 / 3

比较四种服务并发模型

7.5 one thread one loop:线程分工、唤醒与 other things

常见分工:acceptor loop 接入后按 round-robin/hash/load 把 connected fd 移交 I/O loops;I/O loop 负责 recv/send/close/timer;business workers 处理耗时计算,不能直接操作 socket。connection 一旦分配到 loop,通常不迁移,以保持 cache locality 和无锁单 owner。

跨线程提交 command 时,producer 先把 task push 到 thread-safe queue,再写 eventfd/pipe/socketpair 唤醒 poller;loop drain wakeup fd 后执行 tasks。只 enqueue 不 wake 会等到下一个 I/O/timer;只 wake 不正确发布 queue 会丢任务或空转。

分步1 / 3

选择 I/O、timer 或跨线程事件

void EventLoop::run() {
  while (!stopping_) {
    const auto now = clock_.sample();
    auto events = poller_.wait(timers_.timeout(now));
    dispatch_io(events);
    timers_.expire(now);
    handle_other_things(kTaskBudget);
  }
}

7.6 收发数据的正确做法:single reader / single writer state

nonblocking recv 循环读取到 EAGAIN,append 到 input buffer,再循环 decode 完整 frames。send 先尝试 immediate write;partial/would-block 的 suffix 加入 output buffer并注册 writable;flush 完后取消 write interest,避免 busy loop。

void Connection::send(Buffer message) {
  if (!loop_->in_owner_thread()) {
    loop_->post([self = shared_from_this(), message = std::move(message)]() mutable {
      self->send_in_loop(std::move(message));
    });
    return;
  }
  send_in_loop(std::move(message));
}

“不要多个线程同时利用一个 socket 收(发)数据”不仅因为 byte order 会交叉,还因为一个线程可能 close/reuse fd,另一个仍持旧 integer。command 应携带 stable connection id/generation,而不是只携带裸 fd。

7.7 发送、接收缓冲区与慢对端

input buffer 解耦 recv chunks 与 protocol frames;output buffer 保存 partial writes。Buffer API 要有 readable/writable region、append/peek/consume、capacity limit 与 compaction policy。频繁 memmove 可用 prependable/readable/writable indexes、chain 或 slab 优化,但先测量。

分步1 / 3

调整 output 积压与跨线程发送

对端一直不接收时,kernel send buffer 填满,send would-block,应用 output queue 随业务产生而增长。TCP 无法替你限制业务积压;high-water/timeout 是服务内存生存边界。

7.8 网络库分层:Poller、Channel、Connection、Session

一种可解释分层:Poller 封装 OS readiness;Channel 保存 fd/interests/callback;EventLoop 串行 dispatch;Connection 管 transport state 和 buffers;Codec 做 framing;Session 管 protocol/auth/business identity;Service 组织业务。

Service / Handler
Session + Codec
Connection + Buffers
Channel + EventLoop
Poller (epoll/kqueue/IOCP adapter)

Session 进一步分层可把 protocol state 与 user/business state 分开;避免让 Connection 知道 login、order 或 room。每层 ownership 和 callback direction 明确,lower layer 不反向持有任意 upper object 的 raw pointer。

connection info 保存 owner EventLoop/thread,所有变更路由到它。global map 可做 lookup,但不能因此允许任意线程改 connection internals。

7.9 后端服务中的定时器与时间缓存

简单定时器可每轮扫描所有 entries,规模大时用 min-heap、ordered tree、timing wheel 或分层 wheel。timer API 要定义 one-shot/repeating、cancel race、callback ownership、monotonic deadline 与 clock jump。

TimerId id = loop.run_after(30s, [weak = weak_from_this()] {
  if (auto self = weak.lock()) self->on_idle_timeout();
});

每次业务字段格式化都调用 clock syscall 会有成本,可在一轮 event loop 缓存 monotonic/wall time;monotonic 用于 timeout,wall clock 用于日志。缓存粒度会降低精度,不能用于需要严格当前值的安全/计费判断。

7.10 业务数据是否一定单开线程

不一定。短小、无阻塞、预算明确的 parsing/routing 可在 I/O loop 完成,减少 queue/context switch;CPU-heavy、blocking、不可控 callback 必须移出。判断依据是 event-loop lag 与 worst-case duration,而不是“业务”标签。

worker result 回 loop 时,connection 可能已关闭或 generation 改变;通过 weak handle/id 检查,不能捕获 raw this

7.11 侵入式与非侵入式结构

non-intrusive container 持独立 entry/value,ownership 清晰、可让同一 object 多种索引,但有额外 allocation/indirection。intrusive timer/list 将 node 嵌入 Connection,可 O(1) remove 并改善 locality;对象销毁前必须从所有 intrusive containers 解除,一份 node 不能同时入同一类多条链。

7.12 经典服务器结构与性能瓶颈

listen fd 应 nonblocking:readiness 到实际 accept 之间可能被其他 consumer、error 或状态变化消耗;blocking accept 会卡住 loop。ET 下 accept 循环到 EAGAIN,并处理 EMFILE/ENFILE/backoff。

for (;;) {
  int fd = accept4(listen_fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
  if (fd >= 0) { choose_loop(fd).adopt(fd); continue; }
  if (errno == EINTR) continue;
  if (errno == EAGAIN || errno == EWOULDBLOCK) break;
  handle_accept_error(errno);
  break;
}

经典 one thread one loop server:acceptor → I/O loops/Poller → Connection/Buffer/Codec/Session → bounded business pool → result command/wakeup → owner loop;timer 与 deferred destruction 也在 owner loop。

本章回顾:一条连接只有一个可变状态 owner

  1. Reactor/one thread one loop 用少量线程 multiplex I/O,但 handler 必须 nonblocking、有预算。
  2. 跨线程只 enqueue command + wakeup,socket/buffer/close/timer state 回 owner loop 修改。
  3. input/output buffer 解决 partial I/O;high-water 和 timeout 解决慢消费者的内存风险。
  4. Poller/EventLoop/Connection/Codec/Session/Service 分层隔离 OS、transport、protocol 与业务。
  5. timer、intrusive link、deferred destruction 和 fd reuse 都必须纳入 connection lifetime。

练习

问题 1:业务线程完成请求后直接对 fd 调用 send;与此同时 I/O loop 因 peer FIN close 该 fd。为什么加 mutex 仍不充分?

问题 2:对端不再读取,服务仍每秒产生 10 MiB 响应。Connection 应怎样避免拖垮进程?

问题 3:设计 one thread one loop 的 shutdown 顺序,怎样处理 timer、worker result 和当前 event batch?

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

single-loop ownership
active close
passive close
long versus short connection
Reactor pattern
one thread one loop
event-loop wakeup
single-writer socket state
output high-water mark
Session layer
event-loop timer
time caching
intrusive structure
server performance bottleneck

资料与写作方式声明

本章以C++服务器开发精髓,第7章 单个服务的基本结构权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…