C++集群聊天服务器:从网关分离到Redis Pub/Sub的架构实践

📅 2026/7/16 8:00:29
C++集群聊天服务器:从网关分离到Redis Pub/Sub的架构实践
1. 项目概述从零构建一个高可用的C集群聊天服务器最近在整理过去几年做过的项目发现一个挺有意思的C集群聊天服务器项目它算是我在后台服务架构领域的一个“集大成”练习。这个项目不是简单的单机版聊天室而是从一开始就瞄准了“集群”和“高可用”这两个关键词。简单来说它的目标是在多台机器上部署聊天服务让用户无论连接到哪台服务器都能看到完整的在线好友列表、收到实时的消息并且在一台服务器宕机时用户能自动切换到其他健康的服务器聊天记录不丢失会话不断开。听起来是不是有点像微信或者QQ后台的简化版没错核心思想就是如此。为什么用C来做很多人可能会觉得Java的生态或者Go的并发模型更适合。确实对于快速迭代的业务系统它们有优势。但C在追求极致性能、低延迟和高资源利用率的场景下依然是无可替代的选择尤其是在需要自己掌控内存、网络IO等底层细节构建通信中间件或基础服务时。这个项目就是一个很好的练兵场它几乎涵盖了后台开发的所有核心知识点网络编程、多线程、数据库、缓存、序列化、负载均衡、服务发现等等。通过它你能把书本上那些零散的概念像TCP三次握手、线程池、Redis发布订阅、RPC调用串成一个真正能跑起来的系统。这个项目博客目录就是我打算系统性地复盘整个项目的构建过程、技术选型、踩过的坑以及最终的优化心得。无论你是C初学者想找一个有挑战性的综合项目来练手还是有一定经验的开发者想深入理解分布式系统的设计精髓这个系列都应该能给你带来不少干货。接下来我就按照实际构建的逻辑分模块拆解这个“巨无霸”项目。2. 核心架构设计与技术选型背后的思考做一个项目最忌讳的就是拿到需求就开始埋头写代码。尤其是这种涉及多模块协作的分布式系统前期的架构设计和技术选型直接决定了后期开发的难度和系统的天花板。在这个集群聊天服务器项目中我主要考虑了以下几个核心问题并做出了相应的技术决策。2.1 整体架构模式为什么选择“网关-业务节点”分离集群服务常见的架构有去中心化的P2P模式和中心化的星型模式。我选择了后者具体来说是“网关层”与“业务逻辑层”分离的架构。网关节点也称为接入层或Proxy。它的职责非常单纯维护与客户端的TCP长连接进行协议的编解码、基础的鉴权并将收到的客户端请求通过内部RPC转发到后端的业务节点。网关本身是无状态的这意味着它可以水平扩展用户连接到任意一个网关都可以。业务节点负责处理核心的业务逻辑比如用户登录验证、好友关系管理、点对点聊天、群聊消息的存储与转发等。业务节点是有状态的它需要访问数据库和缓存来维护和查询数据。这么设计的好处显而易见职责清晰易于维护网关只关心网络IO和高并发连接管理可以用更专注的优化策略如使用libevent或asio处理海量连接。业务节点专注于逻辑代码更纯粹。水平扩展能力强当用户量增长时我们只需要增加网关服务器的数量即可分摊连接压力。业务节点也可以根据不同的业务模块如消息服务、关系服务进行拆分和独立扩容。提升安全性业务节点不直接暴露在公网由网关作为统一入口便于实施统一的限流、熔断和安全策略。2.2 通信协议自定义二进制协议 vs. 通用协议这是一个关键选择。HTTP/WebSocket是通用、易调试的协议但对于追求性能的即时通讯场景自定义的二进制协议通常是更优解。我选择了自定义二进制协议。一个典型的消息包结构可以设计为消息头固定长度 消息体变长。消息头包含魔数用于快速校验包有效性、版本号、主命令字、子命令字、序列号用于请求-响应匹配、消息体长度等字段。通常用固定大小的结构体表示例如16个字节。消息体采用Protocol Buffersprotobuf进行序列化。Protobuf是Google开源的高效序列化工具二进制编码体积小解析速度快并且支持前后向兼容非常适合作为微服务或内部模块间的数据交换格式。注意自定义协议的最大挑战是解决TCP的粘包/拆包问题。我采用的方法是在消息头中明确指定消息体的长度。接收方先读取固定长度的消息头解析出body_len然后再精确地读取body_len字节的数据从而完整地获取一个业务消息包。这是网络编程的基石务必理解透彻。2.3 状态同步与消息广播Redis Pub/Sub的核心角色在单机聊天室里所有用户连接和会话都在同一进程内广播消息很简单。但在集群中用户A连接在网关1上用户B连接在网关2上A给B发消息如何让B的网关2知道并转发这条消息 这就是集群状态同步的核心问题。我引入Redis及其发布订阅Pub/Sub功能作为“消息总线”或“事件中心”。工作流程每个业务节点和网关节点在启动时都订阅一个公共的Channel例如cluster_chat_channel。当用户A通过网关1发送一条消息给B时请求被路由到某个业务节点。业务节点处理完如存储消息到MySQL后需要通知B所在的网关。但它不知道B在哪。于是它向Redis的cluster_chat_channel发布一条事件消息内容包含“目标用户B的ID”和“要转发的消息数据”。所有网关节点都订阅了这个Channel因此都能收到这条事件消息。每个网关节点检查自己本地维护的“用户ID-连接映射表”如果发现用户B正连接在自己身上就将消息通过对应的TCP连接发送给B如果不在则忽略。为什么是Redis Pub/Sub它提供了一个轻量级、高性能的跨进程通信机制。虽然它不支持消息持久化消息如果当时没有订阅者就丢失了但在这个场景下业务节点在发布事件前已经将消息持久化到数据库事件只是用于实时触发推送短暂丢失可以通过其他机制如离线消息拉取弥补因此是可以接受的。它的简单和高效是最大优势。2.4 数据存储MySQL与Redis的分工协作数据存储层采用经典的“MySQL Redis”组合。MySQL作为源数据存储。存储所有需要持久化、强一致性的数据。例如user表用户基本信息ID、账号、密码哈希、昵称等。friend表好友关系。group表群组信息。message表所有的聊天消息记录。这里设计有讲究可以采用按用户ID或时间分表的方式避免单表过大。Redis作为缓存和会话存储。主要用于会话信息存储“用户ID - 当前连接的网关节点ID”的映射。当业务节点需要定向推送时可以先查Redis快速知道用户在哪台网关上虽然我们用了Pub/Sub广播但这份映射对于其他查询操作如获取好友在线状态很有用。热点数据缓存例如用户信息、群成员列表。遵循Cache-Aside模式先读缓存未命中则读库并回写缓存。分布式锁在创建群组、添加好友等需要防止并发操作的地方使用Redis的SETNX命令实现简单的分布式锁。2.5 内部通信RPC框架的选择网关节点需要调用业务节点的服务这就是一个典型的RPC远程过程调用场景。我选择了brpc一个百度开源的优秀RPC框架。为什么是brpcC原生与项目语言栈完美契合性能极高。功能丰富支持多种协议baidu_std, http, h2, rdma内置负载均衡、熔断、超时、重试等治理能力。生态完善与bvar监控、bthread协程等组件配合好文档和社区相对活跃。替代方案考虑也考虑过Thrift或gRPC。Thrift接口定义语言IDL强大但C版本性能和易用性上我觉得不如brpc顺手。gRPC基于HTTP/2更通用但在追求极致延迟的内部服务间通信上brpc通常表现更优。3. 关键模块实现细节与核心代码剖析架构确定后就是一步步用代码将其实现。下面我挑几个最核心、也最容易出问题的模块讲讲我的实现思路和关键代码。3.1 网络层基于Reactor模型的高性能网关网关的核心是处理成千上万的并发连接。我采用了经典的Reactor事件驱动模型配合非阻塞IO和多线程这是C高性能网络服务器的标准答案。核心组件Main Reactor主线程只有一个。负责监听服务器端口接受accept新的客户端连接。当新连接到来时通过Round-Robin或更智能的方式将其分发给某个Sub Reactor。Sub Reactor子线程可以有多个通常与CPU核心数相同。每个Sub Reactor运行一个独立的事件循环Event Loop使用epollLinux或kqueueBSD/Mac来监听分配给它的一系列连接上的读写事件。线程池对于读到的完整业务数据包解码后需要处理。为了避免业务处理阻塞网络IO线程Sub Reactor我将解码后的任务投递到一个独立的业务逻辑线程池中。这样Sub Reactor线程在投递任务后立刻返回继续监听其他连接的事件极大提升了吞吐量。// 简化的SubReactor线程函数伪代码 void SubReactor::run() { while (!stopped_) { int event_count epoll_wait(epoll_fd_, events_, MAX_EVENTS, timeout); for (int i 0; i event_count; i) { Connection* conn static_castConnection*(events_[i].data.ptr); if (events_[i].events EPOLLIN) { // 有数据可读 int n conn-recvData(); // 非阻塞读处理粘包 if (n 0) { while (conn-hasCompletePacket()) { ChatMessage msg conn-decodePacket(); // 解码为protobuf对象 // 将消息投递到业务线程池而非在此处理 thread_pool_-submit(std::bind(BusinessHandler::onMessage, business_handler_, conn-id(), msg)); } } else if (n 0) { // 客户端关闭连接 handleClose(conn); } else { // 错误处理 handleError(conn); } } // ... 处理 EPOLLOUT 等其它事件 } } }实操心得epoll的边缘触发ET模式比水平触发LT模式性能更高但编程更复杂必须一次性读完所有数据直到EAGAIN。我强烈建议在彻底理解两者区别前先使用LT模式虽然可能损失一点性能但能保证正确性。等系统稳定后再考虑优化到ET模式。3.2 会话管理如何高效维护海量用户连接网关需要知道哪个TCP连接对应哪个用户。我设计了一个SessionManager类来管理。核心数据结构使用两个std::unordered_map。std::unordered_mapint64_t, std::weak_ptrConnection userid_to_conn_; // 用户ID - 连接弱引用 std::unordered_mapint, int64_t fd_to_userid_; // 文件描述符 - 用户ID连接建立客户端登录验证成功后业务节点通过RPC返回成功并附带用户ID。网关收到后在SessionManager中建立双向映射。连接断开在handleClose函数中清理这两个映射。消息推送当业务逻辑线程池处理完消息需要推送给某个用户时它调用SessionManager::sendToUser(int64_t userid, const ChatMessage msg)。这个函数会查找userid_to_conn_如果找到有效的连接指针就将消息编码后放入该连接的发送缓冲区并触发EPOLLOUT事件如果使用ET模式可能需要直接尝试发送。注意事项这里使用std::weak_ptr是为了防止循环引用。Connection对象可能由shared_ptr管理如果userid_to_conn_存储的是shared_ptr那么即使客户端断开只要SessionManager还持有引用Connection对象就无法释放导致内存泄漏。weak_ptr允许你安全地检查对象是否还存在。3.3 业务节点基于Protobuf和brpc的Service实现业务节点对外提供RPC服务。首先需要用Protobuf定义服务接口和消息格式。定义Proto文件(chat_service.proto)syntax proto3; package chat; service ChatService { rpc Login (LoginRequest) returns (LoginResponse); rpc SendMsg (SendMsgRequest) returns (SendMsgResponse); rpc GetFriendList (GetFriendListRequest) returns (GetFriendListResponse); // ... 其他RPC方法 } message LoginRequest { string username 1; string password_md5 2; } message LoginResponse { int32 code 1; // 0-成功其他-错误码 string msg 2; int64 userid 3; } // ... 其他消息定义实现Service使用brpc你需要继承生成的ChatService类并实现具体的RPC方法。class ChatServiceImpl : public chat::ChatService { public: ChatServiceImpl() {}; virtual ~ChatServiceImpl() {}; void Login(google::protobuf::RpcController* cntl_base, const chat::LoginRequest* request, chat::LoginResponse* response, google::protobuf::Closure* done) override { brpc::ClosureGuard done_guard(done); // 确保done-Run()被调用 brpc::Controller* cntl static_castbrpc::Controller*(cntl_base); // 1. 参数校验 // 2. 查询数据库验证用户名密码 // 3. 生成或获取用户唯一ID // 4. 将用户会话信息写入Redis (userid - node_id) // 5. 填充response response-set_code(0); response-set_msg(ok); response-set_userid(123456); LOG(INFO) User request-username() logged in, userid 123456; } // ... 实现其他RPC方法 };启动服务在主函数中创建brpc::Server实例添加ChatServiceImpl并启动服务。brpc::Server server; ChatServiceImpl chat_service_impl; if (server.AddService(chat_service_impl, brpc::SERVER_DOESNT_OWN_SERVICE) ! 0) { LOG(ERROR) Fail to add service; return -1; } brpc::ServerOptions options; options.idle_timeout_sec -1; // 连接永不超时 if (server.Start(port, options) ! 0) { LOG(ERROR) Fail to start server; return -1; } server.RunUntilAskedToQuit();3.4 集群状态同步Redis Pub/Sub事件驱动模型这是集群的“神经系统”。每个节点网关和业务节点都需要一个常驻的线程来监听Redis的Pub/Sub频道。监听线程实现我使用hiredis这个C客户端库的异步API或者在一个独立线程中使用阻塞式订阅。为了简单起见我起了一个单独的RedisSubscriber线程。void RedisSubscriber::run() { redisContext* c redisConnect(redis_ip_.c_str(), redis_port_); if (c nullptr || c-err) { LOG(ERROR) Cannot connect to Redis; return; } // 订阅频道 redisReply* reply (redisReply*)redisCommand(c, SUBSCRIBE %s, channel_name_.c_str()); freeReplyObject(reply); // SUBSCRIBE命令的回复需要释放 while (!stopped_) { redisReply* reply nullptr; if (redisGetReply(c, (void**)reply) REDIS_OK) { // 解析回复reply是一个数组 // reply-element[0] 是 message // reply-element[1] 是 频道名 // reply-element[2] 是 实际的消息内容 if (reply-type REDIS_REPLY_ARRAY reply-elements 3) { std::string msg(reply-element[2]-str, reply-element[2]-len); handlePubSubMessage(msg); // 处理收到的消息 } freeReplyObject(reply); } else { // 连接出错重连逻辑 redisReconnect(c); // 重新订阅 redisReply* resub_reply (redisReply*)redisCommand(c, SUBSCRIBE %s, channel_name_.c_str()); freeReplyObject(resub_reply); } } redisFree(c); }事件处理handlePubSubMessage函数负责解析消息。消息体本身也是一个Protobuf序列化后的二进制数据定义了各种事件类型比如UserOnlineEvent、P2PMsgEvent、GroupMsgEvent等。网关节点收到P2PMsgEvent后会提取目标用户ID然后查询本地的SessionManager如果用户在线就进行转发。4. 项目构建、部署与运维实践代码写完了如何把它跑起来并且稳定地运行在多台机器上这涉及到构建工具链、部署脚本和基本的监控。4.1 构建系统CMake组织跨平台编译项目模块多依赖第三方库brpc, protobuf, redis, mysqlclient等一个清晰的构建系统至关重要。我使用CMake。目录结构ClusterChatServer/ ├── CMakeLists.txt # 根目录CMake ├── third_party/ # 放置依赖库源码或Find脚本 ├── common/ # 公共代码协议定义、工具类 │ ├── CMakeLists.txt │ └── proto/ # .proto文件 ├── gateway/ # 网关服务 │ ├── CMakeLists.txt │ ├── src/ │ └── include/ ├── business/ # 业务节点服务 │ ├── CMakeLists.txt │ ├── src/ │ └── include/ └── scripts/ # 部署运维脚本关键CMake技巧使用find_package优先使用系统包管理器安装的库如apt-get install libprotobuf-dev。备用方案FetchContent或ExternalProject对于brpc等复杂库可以编写脚本自动下载编译。统一编译选项在根CMakeLists.txt中设置统一的CMAKE_CXX_FLAGS开启警告-Wall -Wextra -Werror、优化级别-O2和C标准-stdc17。生成Protobuf代码使用protobuf_generate_cpp函数自动将.proto文件生成.pb.cc和.pb.h。4.2 服务发现与负载均衡基于Nginx的网关层暴露用户客户端需要连接网关。我们有多台网关服务器客户端该连哪台这就需要负载均衡。方案使用Nginx的stream模块用于TCP/UDP负载均衡或http模块如果网关暴露HTTP/WebSocket接口。这里我们假设网关是TCP服务。# nginx.conf 片段 stream { upstream chat_gateway_backend { # 负载均衡算法如轮询、最小连接数等 least_conn; server gateway01:8000 weight1 max_fails3 fail_timeout30s; server gateway02:8000 weight1 max_fails3 fail_timeout30s; server gateway03:8000 weight1 max_fails3 fail_timeout30s; } server { listen 9000; # 客户端连接这个端口 proxy_pass chat_gateway_backend; proxy_connect_timeout 3s; proxy_timeout 3600s; # 长连接超时时间设长 } }服务发现上面的配置是静态的。在生产环境中网关节点可能动态增减。这就需要结合Consul、Etcd或Nacos等服务发现组件配合Nginx的nginx-upsync-module等模块实现上游服务器的动态更新。4.3 基础监控与日志排查没有监控的系统就是在“裸奔”。对于这个项目我实现了几个基础的监控点日志系统使用glogGoogle Logging Library。它为每个级别的日志INFO, WARNING, ERROR, FATAL生成单独的文件并支持按大小和日期滚动。在代码关键路径如连接建立/断开、消息收发、RPC调用打上日志是后期排查问题的唯一依据。基础指标利用brpc内置的bvar统计功能暴露一些关键指标。网关当前连接数、每秒收包数、发包数、消息处理队列长度。业务节点各RPC方法的QPS、平均延迟、错误率。这些指标可以通过brpc的内置HTTP服务如/vars暴露出来方便使用Prometheus等工具抓取。进程守护使用systemd或supervisor来管理服务进程实现开机自启、崩溃后自动重启。5. 开发与调试中遇到的典型问题及解决方案在实际编码和联调过程中踩坑是必然的。下面记录了几个让我印象深刻的“坑”以及填坑方法。5.1 TCP粘包/拆包处理不当导致的消息解析混乱这是网络编程新手最容易出错的地方。现象是客户端发送的完整消息在服务端接收时被合并成一条或者一条消息被拆成多次收到。问题复现客户端快速连续发送两条消息“Hello”和“World”。服务端可能一次recv收到“HelloWorld”也可能第一次收到“Hel”第二次收到“loWorld”。解决方案如之前所述采用定长消息头变长消息体的方案。关键在于recv循环和缓冲区管理。// 在Connection类的recvData方法中 int Connection::recvData() { char buffer[4096]; int n ::recv(fd_, buffer, sizeof(buffer), 0); if (n 0) { input_buffer_.append(buffer, n); // 追加到应用层缓冲区 // 循环处理缓冲区中的完整包 while (true) { if (input_buffer_.size() HEADER_LEN) { break; // 头都不够继续等数据 } // 解析头部获取消息体长度 body_len int32_t body_len parseHeader(input_buffer_); if (body_len MAX_PACKET_SIZE) { // 非法包关闭连接 return -1; } if (input_buffer_.size() HEADER_LEN body_len) { break; // 包体数据不够继续等 } // 取出一个完整包 std::string packet input_buffer_.substr(0, HEADER_LEN body_len); input_buffer_.erase(0, HEADER_LEN body_len); // 将完整包交给解码器 message_queue_.push(packet); } } return n; }踩坑心得input_buffer_必须是这个连接独有的应用层缓冲区。绝对不能试图在一次recv调用中解析出完整业务包必须缓存起来。5.2 多线程环境下数据共享与竞态条件网关的SessionManager会被多个网络IO线程Sub Reactor和业务逻辑线程同时访问必须加锁。问题一个线程正在遍历userid_to_conn_发送广播消息另一个线程因为用户断开连接正在删除同一个条目导致迭代器失效或访问野指针程序崩溃。解决方案使用读写锁对于SessionManager的查找操作读远多于修改操作写的场景使用std::shared_mutexC17可以提升并发性能。缩短锁的持有时间在查找到具体的Connection对象后应尽快释放管理容器的锁。对Connection对象本身的操作可以考虑用原子操作或更细粒度的锁。使用线程局部存储对于一些只读的全局配置数据可以初始化后复制到每个线程的局部存储中避免每次访问都加锁。智能指针与弱引用的正确使用如前所述使用weak_ptr来打破循环引用在需要使用前通过lock()方法尝试提升为shared_ptr提升成功则说明对象存活可以安全使用。5.3 Redis连接池与订阅连接的管理每个业务节点和网关都需要频繁读写Redis查缓存、存会话、Pub/Sub。如果每次操作都创建新的连接开销巨大。问题频繁创建连接导致Redis服务器负载高性能下降。解决方案实现一个Redis连接池。对于普通的命令请求GET/SET等使用连接池管理一批长连接每次操作从池中借出用完后归还。对于Pub/Sub的订阅连接由于其需要独占并长期阻塞监听必须使用独立的、不与连接池混用的连接。连接池简单实现思路class RedisPool { public: std::shared_ptrredisContext getConnection(); void returnConnection(std::shared_ptrredisContext conn); private: std::queuestd::shared_ptrredisContext idle_connections_; std::mutex mutex_; std::condition_variable cond_; // ... 还有创建新连接、检查连接健康度的逻辑 };注意Redis的C客户端hiredis是线程不安全的一个连接不能同时被多个线程使用。连接池必须保证借出和归还的线程安全。5.4 分布式场景下的“消息必达”与幂等性在集群中消息可能因为网络分区、节点宕机而丢失或重复。消息丢失业务节点发布事件到Redis后崩溃可能事件未被任何网关收到。我的策略是业务节点先在MySQL中持久化消息并标记为“已发送”再发布事件。网关收到事件并成功推送给用户后可以发送一个ACK回执给业务节点通过另一个RPC调用业务节点再将消息标记为“已送达”。对于未确认的消息可以有一个后台补偿任务定期从MySQL中拉取“已发送未送达”的消息重新发布事件。消息重复网络延迟可能导致事件被重复发布或者ACK丢失导致补偿任务重复发送。这就要求消息处理要具备幂等性。我为每条消息生成一个全局唯一的ID如雪花算法生成。在网关或客户端对同一消息ID进行去重处理确保即使收到多次也只生效一次。6. 性能优化与压测经验分享当基本功能跑通后性能优化就成了下一个目标。我主要从以下几个层面进行了优化和验证。6.1 网络IO优化从多线程到多Reactor最初的网关模型是“一个监听线程 一个线程池”监听线程accept后将新连接直接扔进线程池由线程池中的线程负责这个连接后续的所有读写。这在连接数不多时没问题但当连接数上万时线程上下文切换开销巨大且每个线程都要管理自己的epoll复杂度高。优化为多Reactor模型如前文所述这是更成熟的模式。Main Reactor负责accept然后将连接分发给多个Sub Reactor。每个Sub Reactor管理一组连接运行在自己的线程中。这样将连接均匀分散到多个epoll实例上极大地提升了可扩展性。6.2 内存管理减少拷贝与对象池高性能C服务中内存分配和拷贝是性能杀手。零拷贝优化在网络收发时尽量让数据在用户态缓冲区之间传递避免内核态到用户态的多次拷贝。例如可以使用readv/writev系统调用结合struct iovec或者更高级的splice、sendfile对于文件传输。在我们的协议中确保编解码过程直接在输入/输出缓冲区上进行避免中间生成临时字符串。使用对象池频繁创建和销毁小对象如每个消息包的对象会带来内存碎片和分配器开销。我实现了一个简单的MessageObjectPool用于复用ChatMessage等Protobuf对象。注意Protobuf对象复用前必须调用Clear()方法重置。6.3 数据库与缓存优化MySQL索引优化message表在(receiver_id, send_time)上建立联合索引加速按接收者和时间范围查询消息。批量操作对于离线消息拉取等场景使用IN语句或分批查询避免循环单条查询。连接池使用如libmysqlclient自带的连接池或第三方库管理数据库连接。RedisPipeline对于需要连续执行多个Redis命令的操作如一次性获取多个用户的在线状态使用Pipeline将多个命令打包一次发送减少网络往返延迟。避免大Key将会话信息、好友列表等数据结构合理拆分不要将一个拥有上万成员的大集合作为一个Key存储。6.4 压力测试与瓶颈定位我使用wrk或自定义的压测客户端来模拟大量用户。压测场景模拟10万用户同时在线保持长连接心跳。模拟其中1万用户以每秒1条的速率发送点对点消息。模拟创建大群进行群消息广播。监控指标系统层面使用top、vmstat、iostat观察CPU、内存、IO使用率。使用ss -ant观察连接状态。进程层面使用brpc内置的/vars、/rpcz页面查看QPS、延迟、错误率。网络层面使用iftop或nethogs观察网络流量。典型瓶颈与调优CPU跑满使用perf或gprof进行性能剖析找出热点函数。往往是序列化/反序列化Protobuf、日志打印、锁竞争。内存持续增长检查是否有内存泄漏使用valgrind对象池或缓存是否无限增长。延迟变高检查消息队列是否堆积线程池是否饱和数据库/Redis是否响应变慢。这个项目从设计到实现再到优化是一个不断遇到问题、解决问题的过程。它让我对“分布式”、“高并发”、“高可用”这些概念有了血肉般的体会而不仅仅是停留在理论层面。如果你能跟着这个思路亲手敲一遍代码把各个模块跑通再尝试解决其中遇到的问题那么你对后台开发的理解一定会上升一个巨大的台阶。最后所有的源码和更详细的配置文档我都会整理到我的代码仓库中希望能对大家有所帮助。