1. 项目概述从“C异步网络编程”到“百万并发”的挑战最近在社区里看到不少朋友在讨论高并发服务器开发尤其是用C实现百万级连接这个听起来有点“玄学”的目标。很多人一上来就埋头写代码调Epoll结果跑个几万连接就卡住或者内存直接爆掉。我自己在游戏服务器和金融交易系统里摸爬滚打多年处理过不少这类问题。今天想和大家深入聊聊如何用经典的Reactor模式作为核心架构真正逼近甚至实现C服务器的“百万并发连接”这个里程碑。首先得明确“百万并发”不是一个简单的数字游戏。它背后是一整套系统工程涉及操作系统调优、网络协议栈理解、内存管理、以及最核心的——高效的异步事件处理模型。为什么是Reactor模式因为它本质上是一种事件驱动的编程范式将I/O事件的到来与业务逻辑的处理解耦。一个线程或少量线程负责监听所有连接上的事件如可读、可写当事件发生时再分发给对应的处理单元Handler去执行。这种设计避免了为每个连接创建一个线程所带来的巨大开销上下文切换、内存占用是支撑海量连接的理论基础。但理论归理论从理论到能稳定跑在线上中间隔着无数个坑。比如如何设计高效的内存池来应对海量连接对象的创建与销毁如何管理这百万个文件描述符fd而不让Epoll本身成为瓶颈当连接数暴涨时业务逻辑的延迟是否会劣化这些都不是看两篇博客就能解决的需要你对整个系统栈有清晰的认识。这篇文章我就结合自己的实战经验拆解用C和Reactor模式构建高并发服务器的核心要点、常见陷阱以及那些只有踩过坑才知道的优化技巧。无论你是正在学习网络编程的学生还是工作中需要应对性能挑战的工程师希望这些内容能给你带来一些实实在在的启发。2. Reactor模式核心思想与架构选型2.1 为什么是Reactor事件驱动模型的本质在讨论具体实现之前我们必须先理解为什么在高并发场景下传统的“一个连接一个线程”Thread-Per-Connection模型会失效。假设我们有100万个连接如果每个连接对应一个线程那么操作系统需要调度100万个线程。线程的上下文切换、栈内存每个线程通常需要MB级别的栈空间的消耗将是灾难性的系统资源会迅速耗尽性能急剧下降。Reactor模式的核心思想是**“不要用等要用通知”。它由一个或多个事件分发器**通常叫Reactor或EventLoop和多个事件处理器EventHandler组成。分发器的工作是进行事件多路分离如调用epoll_wait它阻塞等待直到一个或多个I/O事件或定时器事件发生然后将这些事件分派给对应的处理器。处理器则负责实现具体的业务逻辑如读取数据、处理请求、发送响应。这个过程是非阻塞和异步的。当一个连接的数据没有准备好时线程不会傻等而是去处理其他已经就绪的连接。这样少数几个线程甚至一个线程就能管理成千上万个连接。Linux下的epoll、BSD的kqueue、Windows的IOCP都是实现事件分发器的强大系统调用其中epoll因其在Linux上的高性能和易用性成为大多数C高性能网络库的首选。注意很多人容易混淆Reactor和Proactor。简单来说Reactor是“通知你何时可以开始一个I/O操作”比如通知你socket可读了然后你自己去调用read而Proactor是“通知你某个I/O操作已经完成了”比如系统帮你读好数据通知你来处理。Linux原生生态更偏向Reactor模型。2.2 单Reactor与多Reactor架构的权衡确定了使用Reactor模式下一个关键决策是用几个Reactor事件循环1. 单Reactor单线程这是最简单的模型。所有工作事件监听、分发、业务处理都在一个线程内完成。它的优点是极致简单没有线程同步问题。Redis的早期版本就近似这种模型。但对于百万连接的目标它有致命缺陷所有连接的事件处理和业务计算都挤在同一个线程一旦某个业务处理耗时较长比如一个复杂查询整个事件循环就会被卡住导致所有连接的响应延迟增加。因此它只适合业务逻辑非常轻量、纯粹的I/O密集型场景。2. 单Reactor多线程这是更实用的模型。主线程通常只有一个仍然只负责事件监听和分发即epoll_wait和事件派发。但当某个连接上有数据可读时主线程并不直接处理业务而是将读取到的数据包封装成一个“任务”或称为请求对象投递到一个共享的任务队列中。后台有一个由多个工作线程Worker Thread组成的线程池它们从队列中取出任务并执行实际的业务逻辑。处理完成后如果需要回写数据工作线程会将回写数据通过某种方式如再次投递到队列或通过管道通知主线程交还给主线程由主线程执行实际的send操作。这种模型的优势是将耗时的业务计算与高效的I/O事件分离。主线程可以保持高速运转专门应对海量连接的事件通知而计算任务则由线程池并行处理充分利用多核CPU。这是实现百万并发最主流、最稳健的架构选择。我们后续的讨论也将主要围绕这种模型展开。3. 多Reactor多线程这种模型更进一步通常用于需要更高吞吐量的场景。它会有多个Reactor线程也叫IO线程每个Reactor线程运行独立的事件循环管理自己的一组连接例如使用Round-Robin方式分配新连接。每个Reactor线程既处理自己管理的连接的I/O事件也可能自己处理业务或者再将业务投递给一个共享的全局线程池。Nginx、Netty等采用的就是类似的多Reactor模型。它的优点是进一步减少了单个Reactor的压力并且由于连接被分散到多个Reactor在单个Reactor上的锁竞争可能更少。但架构复杂度也显著增加需要考虑连接在不同Reactor间的均衡以及跨线程通信。对于百万并发这个目标我个人的经验是从“单Reactor多线程”模型起步。它的复杂度可控性能潜力巨大足以应对绝大多数场景。当你在这种模型下优化到极致仍然遇到瓶颈时比如单个epoll实例管理百万fd本身成为瓶颈再考虑演进到多Reactor模型。3. 核心组件实现与关键技术细节3.1 事件多路分离器Epoll的深度使用与优化epoll是我们实现Reactor的基石。它的基本用法大家应该都熟悉epoll_create1、epoll_ctl、epoll_wait。但要用到百万连接就不能停留在基本用法上。1. 边缘触发ET与水平触发LT的选择这是第一个关键抉择。LT模式是默认模式只要文件描述符处于就绪状态比如读缓冲区有数据每次调用epoll_wait都会报告它。ET模式则只在状态发生变化时报告一次比如从无数据到有数据。LT模式编程更简单不容易遗漏事件。但可能会导致效率问题如果一次没有读完所有数据下次epoll_wait会立即再次通知可能造成不必要的唤醒。ET模式效率更高减少了相同事件被重复触发的次数。但编程要求更严格你必须一次性把缓冲区里的数据全部读完直到read返回EAGAIN或EWOULDBLOCK否则这个fd上剩下的数据将再也无法被通知到除非有新数据到来触发新的状态变化。对于追求极致性能的百万并发服务器强烈推荐使用ET模式。它能最大程度减少系统调用的次数。代价是你需要为每个socket连接维护一个应用层的缓冲区。当收到ET通知可读时必须循环调用read直到读完所有数据并将数据存入该连接对应的缓冲区中供后续业务逻辑处理。2. Epoll实例与文件描述符上限一个epoll实例能监控多少fd这个数字很大通常只受系统内存限制。但管理百万个fd用一个epoll实例真的是最佳实践吗不一定。当单个epoll实例管理的fd数量极大时每次epoll_wait返回的事件集合可能非常庞大遍历和分发这些事件本身会成为开销。一种优化思路是使用多个epoll实例对应多Reactor线程将连接分摊到不同的实例上。此外需要调整系统的文件描述符限制ulimit -n和内核参数如fs.file-max确保能打开百万个socket。3. Epoll事件的数据结构使用epoll_data的ptrepoll_event结构体中有一个联合体epoll_data通常我们使用其ptr成员。这是一个至关重要的优化点。当epoll_wait返回一个事件时我们可以通过ev.data.ptr直接拿到与这个fd关联的连接对象Connection的指针而无需再去维护一个从fd到对象的映射表如std::unordered_mapint, Connection*并进行查找。这省去了一次哈希查找的开销在事件处理的核心路径上任何微小的优化都会被百万倍的放大。struct Connection { int fd; sockaddr_in peer_addr; Buffer input_buffer; // 应用层读缓冲区 Buffer output_buffer; // 应用层写缓冲区 // ... 其他状态信息 }; // 将Connection对象指针绑定到epoll事件 struct epoll_event ev; ev.events EPOLLIN | EPOLLET; // 监听读事件边缘触发 ev.data.ptr (void*)conn; // 关键存储连接对象指针 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn-fd, ev);3.2 连接管理与对象池应对海量对象的生与死百万连接意味着百万个连接对象。如果每次accept一个新连接都new一个对象断开时都delete频繁的内存申请和释放会导致严重的内存碎片和性能下降尽管现代malloc实现已经很优秀但在极致场景下仍是瓶颈。解决方案是使用对象池Object Pool。我们可以预先分配一大块内存在其中创建固定数量的连接对象比如120万个形成一个空闲链表。当需要新连接对象时从池中取一个当连接关闭时将对象重置状态后放回池中而不是真正释放内存。class ConnectionPool { public: Connection* acquire(int fd, const sockaddr_in addr) { if (free_list_.empty()) { // 池耗尽可以动态扩容或返回nullptr return nullptr; } Connection* conn free_list_.back(); free_list_.pop_back(); // 初始化或重置conn对象 conn-fd fd; conn-peer_addr addr; conn-input_buffer.clear(); conn-output_buffer.clear(); // ... 重置其他状态 return conn; } void release(Connection* conn) { // 清理conn但不释放内存 conn-reset(); free_list_.push_back(conn); } private: std::vectorConnection* all_connections_; // 实际内存块 std::vectorConnection* free_list_; // 空闲对象链表 };这样做的好处极速分配/释放只是操作链表指针速度极快。内存局部性友好连续分配的对象在内存中可能更紧凑有利于CPU缓存。避免碎片内存块大小固定且循环使用。实操心得对象池的大小需要仔细评估。设得太小连接数突增时可能耗尽设得太大浪费内存。一个常见的策略是初始化为最大预期连接数的120%。同时池中的对象需要有一个“重置”方法确保放回池前其内部缓冲区、状态机等都恢复到初始状态避免脏数据带到下一个连接。3.3 缓冲区设计高效的内存管理艺术如前所述使用ET模式必须要有应用层缓冲区。缓冲区的设计直接影响到内存占用和性能。1. 读缓冲区Input Buffer当ET模式通知可读时我们需要将socket内核缓冲区中的数据全部读到应用层缓冲区。这个缓冲区应该足够大能容纳一个或多个完整的应用层报文Packet。常见的做法是使用可变长的缓冲区如std::vectorchar或自己实现的链式缓冲区。为了减少内存拷贝应该优先考虑一次read系统调用就读尽可能多的数据例如使用readv进行分散读。2. 写缓冲区Output Buffer发送数据时如果TCP发送缓冲区已满send返回EAGAIN我们不能阻塞必须将剩余待发送的数据存入该连接的应用层写缓冲区并监听该fd的写事件EPOLLOUT。当epoll通知可写时再尝试发送写缓冲区中的数据。发送成功后从缓冲区中移除已发送的数据。3. 缓冲区实现策略连续缓冲区简单但频繁的头部移除操作可能导致内存拷贝或需要维护读写指针。链式缓冲区由多个固定大小的块如4KB组成链表。读数据从第一个块开始写数据追加到最后一个块。当第一个块的数据被消费完就释放该块。这种设计避免了大规模的内存拷贝是高性能服务器的常见选择。你可以自己实现也可以使用现成的库如boost::asio::streambuf的设计思想。// 一个简化的链式缓冲区块 struct BufferBlock { static const size_t SIZE 4096; char data[SIZE]; size_t read_idx 0; size_t write_idx 0; BufferBlock* next nullptr; }; // Buffer类管理一个BufferBlock链表3.4 定时器管理百万连接的心跳与超时百万个连接中很多可能是空闲连接。我们需要一种机制来检测死连接客户端异常断开或清理长时间不活动的连接这就是心跳和超时机制。我们需要为每个连接设置一个超时时间点并高效地管理这百万个定时器。1. 定时器数据结构的选择排序链表插入O(n)触发O(1)。百万级连接下插入性能不可接受。最小堆插入和删除都是O(log n)取最小超时节点是O(1)。这是一个不错的选择但堆结构的调整在频繁的插入删除下也有开销。时间轮Time Wheel这是网络编程中非常经典的高效定时器算法。它将时间划分为一个个刻度tick每个刻度对应一个桶bucket桶里存放在该刻度超时的连接链表。只需一个指针随着时间推进指向当前刻度处理对应桶中的所有连接即可。它的插入、删除、触发复杂度都可以接近O(1)非常适合海量定时任务。但实现相对复杂需要处理时间回绕等问题。跳表也能实现O(log n)的复杂度但实现复杂不如最小堆直观。对于百万并发我推荐使用最小堆或多层级时间轮。最小堆实现简单使用std::priority_queue即可在连接数百万且超时时间分布均匀时性能足够好。如果对定时精度和性能有极致要求可以自己实现时间轮。2. 定时检查机制定时器需要被定期检查。通常的做法是在主事件循环中每次epoll_wait返回后或之前检查一次定时器。epoll_wait可以设置一个超时时间如100ms这样既能及时处理I/O事件又能定期唤醒处理定时任务。检查时从堆顶取出所有已超时的连接对象执行超时回调如关闭连接。// 伪代码在主循环中处理定时器 while (!stop) { int timeout_ms calculate_next_timer_timeout(); // 计算距离最近超时还有多久 int event_count epoll_wait(epoll_fd, events, MAX_EVENTS, timeout_ms); // 处理网络事件... process_expired_timers(); // 处理所有已超时的定时器 }4. 从零搭建一个简易Reactor服务器的实现步骤4.1 基础框架搭建EventLoop与Channel我们首先构建最核心的事件循环和事件通道。1. Channel类它封装了一个文件描述符fd及其感兴趣的事件读、写等和对应的回调函数。它是事件处理器EventHandler的具体体现。class Channel { public: using EventCallback std::functionvoid(); Channel(EventLoop* loop, int fd); ~Channel(); void handleEvent(); // 被EventLoop调用根据revents_调用相应的回调 void setReadCallback(EventCallback cb) { readCallback_ std::move(cb); } void setWriteCallback(EventCallback cb) { writeCallback_ std::move(cb); } void setErrorCallback(EventCallback cb) { errorCallback_ std::move(cb); } void enableReading() { events_ | kReadEvent; update(); } void enableWriting() { events_ | kWriteEvent; update(); } void disableWriting() { events_ ~kWriteEvent; update(); } void disableAll() { events_ kNoneEvent; update(); } int fd() const { return fd_; } int events() const { return events_; } void set_revents(int revt) { revents_ revt; } // 由EventLoop设置 private: void update(); // 将当前关注的事件注册到EpollPoller EventLoop* loop_; // 所属EventLoop const int fd_; // 负责的文件描述符生命周期不由Channel管理 int events_; // 它关心的事件 int revents_; // Epoll返回的事件 EventCallback readCallback_; EventCallback writeCallback_; EventCallback errorCallback_; // ... 其他状态如是否在Epoll中注册等 };2. EventLoop类它是Reactor是事件循环的核心。每个EventLoop绑定一个线程。它持有一个EpollPoller对象并拥有一个Channel列表。class EventLoop { public: EventLoop(); ~EventLoop(); void loop(); // 核心循环 void quit(); // 退出循环 void updateChannel(Channel* channel); // 添加或更新Channel到Poller void removeChannel(Channel* channel); // 从Poller移除Channel void runInLoop(std::functionvoid() cb); // 跨线程调用的关键 void queueInLoop(std::functionvoid() cb); bool isInLoopThread() const { return threadId_ std::this_thread::get_id(); } private: void wakeup(); // 通过eventfd唤醒阻塞在epoll_wait的线程 void handleWakeup(); // 处理唤醒事件 void doPendingFunctors(); // 执行其他线程投递过来的任务 const std::thread::id threadId_; // 记录所属线程ID std::unique_ptrEpollPoller poller_; int wakeupFd_; // 用于跨线程唤醒的eventfd std::unique_ptrChannel wakeupChannel_; std::vectorstd::functionvoid() pendingFunctors_; // 待执行函数队列 std::mutex mutex_; // 保护pendingFunctors_ bool callingPendingFunctors_; std::atomicbool looping_; };EventLoop::loop()函数的实现是核心void EventLoop::loop() { looping_ true; while (looping_) { activeChannels_.clear(); pollReturnTime_ poller_-poll(kPollTimeMs, activeChannels_); // 处理活跃事件 for (Channel* channel : activeChannels_) { channel-handleEvent(); } // 处理其他线程投递过来的任务 doPendingFunctors(); } }4.2 构建TCP服务器Acceptor与TcpConnection1. Acceptor类负责监听套接字接受新连接。它本身是一个Channel监听socket上的读事件EPOLLIN表示有新连接到来。class Acceptor { public: using NewConnectionCallback std::functionvoid(int sockfd, const sockaddr_in); Acceptor(EventLoop* loop, const sockaddr_in listenAddr); void setNewConnectionCallback(const NewConnectionCallback cb) { newConnectionCallback_ cb; } void listen(); private: void handleRead(); // 监听socket可读时的回调 EventLoop* loop_; int listenFd_; std::unique_ptrChannel acceptChannel_; NewConnectionCallback newConnectionCallback_; };在handleRead()中循环调用accept直到返回EAGAINET模式对每个新连接调用newConnectionCallback_。2. TcpConnection类这是最重要的类代表一个TCP连接。它继承自std::enable_shared_from_this因为其生命周期由shared_ptr管理。它内部包含输入输出缓冲区并提供了发送数据的接口。class TcpConnection : public std::enable_shared_from_thisTcpConnection { public: using MessageCallback std::functionvoid(const std::shared_ptrTcpConnection, Buffer*, TimeStamp); using CloseCallback std::functionvoid(const std::shared_ptrTcpConnection); TcpConnection(EventLoop* loop, const std::string name, int sockfd, const sockaddr_in localAddr, const sockaddr_in peerAddr); ~TcpConnection(); void send(const std::string message); // 线程安全的发送函数 void shutdown(); // 关闭写端 void forceClose(); void setMessageCallback(const MessageCallback cb) { messageCallback_ cb; } void setCloseCallback(const CloseCallback cb) { closeCallback_ cb; } void connectEstablished(); // 在连接建立后调用开始监听读事件 void connectDestroyed(); // 在连接关闭前调用 private: void handleRead(TimeStamp receiveTime); void handleWrite(); void handleClose(); void handleError(); void sendInLoop(const std::string message); // 在IO线程中实际发送 void shutdownInLoop(); EventLoop* loop_; // 所属IO线程的EventLoop const std::string name_; const int fd_; std::unique_ptrChannel channel_; const sockaddr_in localAddr_; const sockaddr_in peerAddr_; Buffer inputBuffer_; // 应用层读缓冲区 Buffer outputBuffer_; // 应用层写缓冲区 MessageCallback messageCallback_; // 收到完整消息的回调 CloseCallback closeCallback_; // 连接关闭的回调 // ... 连接状态kConnecting, kConnected, kDisconnecting, kDisconnected };send函数需要是线程安全的因为业务线程可能调用它。它通过runInLoop将实际的发送操作sendInLoop转移到连接所属的IO线程中执行。4.3 线程池与任务队列分离IO与计算这是实现“单Reactor多线程”的关键。我们创建一个全局的ThreadPool它管理一组工作线程和一个任务队列。class ThreadPool { public: using Task std::functionvoid(); explicit ThreadPool(size_t numThreads, const std::string name std::string()); ~ThreadPool(); void start(); void stop(); void run(Task task); private: void runInThread(); // 工作线程函数 std::string name_; std::vectorstd::unique_ptrstd::thread threads_; std::dequeTask queue_; std::mutex mutex_; std::condition_variable cond_; std::atomicbool running_; };在服务器主类TcpServer中当Acceptor接收到新连接并创建TcpConnection后设置其messageCallback_。当该连接有数据可读时TcpConnection::handleReadIO线程会将读取到的数据可能是一个或多个完整报文打包成一个任务投递到ThreadPool的任务队列中。// 在TcpServer中设置回调 void TcpServer::newConnection(int sockfd, const sockaddr_in peerAddr) { // ... 创建conn conn-setMessageCallback(std::bind(TcpServer::onMessage, this, _1, _2, _3)); } void TcpServer::onMessage(const TcpConnectionPtr conn, Buffer* buf, TimeStamp time) { // 从缓冲区解析出一个完整的应用层请求 std::string request parseRequestFromBuffer(buf); // 将业务处理封装成任务提交到线程池 threadPool_.run([conn, request, time]() { // 这里是工作线程上下文 std::string response handleBusinessLogic(request); // 耗时的业务处理 // 处理完成后通过conn发送回响应conn-send是线程安全的 conn-send(response); }); }这样IO线程只负责高效的网络数据收发而耗时的业务逻辑由线程池并行处理充分利用多核。5. 性能调优与百万连接压测实战5.1 系统级参数调优在代码层面优化到极致后操作系统内核参数的调整是通往百万连接的必经之路。以下是一些关键参数在/etc/sysctl.conf中修改后需sysctl -p生效文件描述符数量# 系统最大文件描述符数 fs.file-max 1000000 # 进程最大文件描述符数软限制和硬限制 # 需在/etc/security/limits.conf中设置如* soft nofile 1000000 * hard nofile 1000000网络核心参数# 允许端口重用对于服务器快速重启至关重要 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_tw_recycle 0 # 在NAT环境下建议为0避免问题 # 半连接队列和全连接队列大小需要根据并发数调大 net.ipv4.tcp_max_syn_backlog 65536 net.core.somaxconn 65536 # 增加TCP缓冲区大小提升吞吐量 net.core.rmem_max 16777216 net.core.wmem_max 16777216 net.ipv4.tcp_rmem 4096 87380 16777216 net.ipv4.tcp_wmem 4096 65536 16777216 # 减少TIME_WAIT状态连接的影响 net.ipv4.tcp_max_tw_buckets 2000000 # 开启TCP快速打开TFO加速后续连接 net.ipv4.tcp_fastopen 3虚拟内存与网络缓冲区# 减少内存分配压力特别是在有大量socket时 net.core.optmem_max 25165824 # 调整虚拟内存过度分配策略避免因突发大量内存申请导致OOM Killer杀进程 vm.overcommit_memory 1 # 或 2需根据实际情况选择5.2 应用层关键优化点内存分配器考虑使用tcmalloc或jemalloc替代默认的glibc malloc。它们在多线程环境下的小内存分配和碎片处理上表现更优。日志输出压测时务必关闭或大幅降低日志级别如设为FATAL或ERROR。磁盘I/O和格式化输出是性能杀手。使用异步日志库是生产环境的标配。禁用Nagle算法对于低延迟要求的服务可以设置TCP_NODELAY选项避免小数据包的发送延迟。int flag 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, flag, sizeof(flag));设置SO_REUSEPORT如果你的服务是多进程模型可以使用SO_REUSEPORT选项让多个进程绑定同一个端口由内核进行负载均衡这能进一步提升连接建立性能。5.3 压测工具与模拟百万连接单机模拟百万客户端连接需要工具和技巧。常用的压测工具有wrk/wrk2优秀的HTTP压测工具但主要用于模拟高并发请求而非长连接。ApacheBench (ab)类似主要用于HTTP。自定义压测客户端为了模拟纯TCP长连接我们通常需要自己编写一个简单的压测程序。这个程序会创建大量线程或使用异步IO如libevent每个线程管理数千个连接连接建立后定期发送心跳包。压测步骤与观察点逐步加压不要一下子启动百万连接。从1万、5万、10万逐步增加观察服务器资源CPU、内存、网络使用情况。监控关键指标系统层面使用top、htop看CPU利用率free、vmstat看内存ss -s、netstat -s看socket统计dstat、nethogs看网络流量。进程层面使用pidstat监控服务器进程的CPU、内存、上下文切换。连接状态ss -tan | grep ESTAB | wc -l查看ESTABLISHED连接数。关注瓶颈CPU如果CPU us用户态很高可能是业务逻辑或缓冲区处理成为瓶颈。如果CPU sy系统态很高可能是系统调用过于频繁如epoll_wait返回的事件列表太大遍历耗时。内存观察内存是否稳步增长后趋于稳定。如果持续增长可能有内存泄漏检查Connection对象是否被正确释放回池。文件描述符使用ls /proc/pid/fd | wc -l查看进程打开的fd数量确认是否接近预期。模拟真实流量除了建立连接还要模拟数据收发。可以设计简单的请求-响应协议让压测客户端随机间隔发送小数据包观察服务器处理能力。6. 常见问题、故障排查与稳定性保障6.1 典型问题与解决方案问题现象可能原因排查思路与解决方案连接数达到几万后无法再上升1. 系统文件描述符限制未调整。2. 服务器端口耗尽客户端频繁连接断开TIME_WAIT状态过多。3. 内存不足无法创建新的连接对象。1. 检查ulimit -n和fs.file-max。2. 启用tcp_tw_reuse增加tcp_max_tw_buckets。压测客户端使用连接池避免短连接。3. 检查服务器内存使用优化连接对象内存占用使用对象池。连接建立缓慢CPU sys很高1.accept处理慢可能因为全连接队列太小导致连接被丢弃或重传。2. 为每个新连接new对象开销大。1. 增大net.core.somaxconn和tcp_max_syn_backlog。2. 使用对象池预分配连接对象。Established连接数波动有大量连接断开1. 服务器处理不过来主动断开如读超时。2. 网络问题或客户端异常。3. 服务器内存不足触发OOM Killer。1. 检查服务器负载和业务处理延迟。优化业务逻辑或增加工作线程。2. 检查服务器和客户端日志、网络状况。3. 监控系统日志/var/log/messages看是否有OOM Killer记录。优化内存使用。业务处理延迟随连接数增加而线性增长1. 工作线程池任务队列堆积。2. 锁竞争激烈如日志锁、全局统计锁。3. 单个Reactor线程成为瓶颈。1. 增加工作线程数或优化任务粒度。2. 减少锁范围使用无锁数据结构或使用线程局部存储TLS。3. 考虑演进到多Reactor模型将连接分摊到多个IO线程。发送数据时偶尔出现EPIPE或ECONNRESET错误对端已经关闭了连接本端仍尝试写入。在send或write后检查返回值。如果错误是EPIPE通常可以忽略关闭本地连接即可。更优雅的做法是在开始写之前检查连接状态或者注册EPOLLHUP事件进行处理。6.2 内存泄漏排查在高并发长连接服务中内存泄漏是致命的。除了使用Valgrind等工具在线上可以通过以下方式监控监控进程RSS使用ps或/proc/pid/status定期查看进程常驻内存集RSS的增长趋势。使用TCMalloc或JeMalloc的堆分析功能它们提供了heap profiler可以在特定时间点dump出内存分配情况。在对象池中增加统计记录分配和释放的次数确保两者最终相等。检查智能指针的循环引用如果使用shared_ptr管理TcpConnection要确保回调函数中不会意外形成循环引用例如将TcpConnection的shared_ptr捕获到其自身的回调函数中。必要时使用weak_ptr。6.3 线上运维与可观测性一个稳定的百万并发服务离不开完善的监控。核心指标监控连接数当前ESTABLISHED连接数、每秒新建连接数。资源CPU使用率区分us/sy、内存使用量、网络带宽入/出。队列深度线程池任务队列长度、Epoll中注册的fd数量。业务指标请求QPS、平均/分位延迟、错误率。日志与追踪结构化日志如JSON格式便于分析。对于复杂请求可以考虑集成分布式追踪系统如OpenTelemetry跟踪一个请求在不同线程和模块间的流转路径。优雅退出与热更新实现信号处理收到SIGTERM时不再接受新连接等待现有连接处理完毕后再退出。对于配置更新可以考虑通过Unix Domain Socket发送命令实现不停服热更新。实现一个能稳定承载百万并发的C服务器是一个将深度知识操作系统、网络、数据结构与工程实践代码架构、调试、运维紧密结合的过程。Reactor模式提供了优秀的骨架但血肉需要你自己用扎实的细节去填充。从理解每个系统调用的行为到设计每一块内存的布局再到应对线上各种诡异的故障每一步都是挑战也是乐趣所在。希望这篇长文能为你扫清一些障碍更希望你能动手实践在代码和日志中真正理解这些概念。毕竟服务器编程是一门实践的艺术所有的理论最终都要接受accept和send的检验。