秒杀系统高并发优化实战(C++ / Drogon):2.5 完善项目基础设施(配置 / 日志 / 全局异常)
秒杀接口上线后的第一个周末,运维问了我三个问题:端口在哪个文件改?请求失败到底是"抢光了"还是"系统错了"?出问题的时候日志在哪看?——三个问题我愣了一分钟。功能写完了,但**配置、日志、异常这三件"看不见的基础设施"**决定了一个系统能不能被运维、被排查、被信任。本文讲 seckill-cpp 在这三件事上的落地:config.json 怎么组织、Drogon 内置日志怎么用、错误如何从 SeckillService 一路映射成客户端看得懂的 HTTP 语义。
配套仓库
seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),本文对应阶段一v0.1.x,代码见config.json、src/main.cc、src/controllers/SeckillController.cc、src/service/SeckillService.cc、src/controllers/HealthController.cc。
一、基础设施是什么、坑在哪、本质一句话
- 是什么:三个横切关注点——配置(环境相关的参数不硬编码)、日志(出问题时能还原现场)、异常(错误以统一的语义传回调用方)。
- 坑在哪:基础设施的坑都是"平时看不见、出事才咬人"。配置放错位置 → 换环境部署时改代码;日志打太细 → 秒杀高峰期 IO 被日志拖垮;异常处理不一 → 客户端把"售罄"当成"系统错误"疯狂重试,把故障放大成雪崩。
- 本质一句话:基础设施的价值是"出事时的可运维性",而不是"正常运行时的功能"——它所有的设计取舍,都要以"真出问题时能不能快速定位"为标尺。
二、配置:config.json 的三段式组织
seckill-cpp 的配置全部外置到 config.json,三段各管一件事:
1 | { |
listeners:监听端口与协议。https: false是阶段一的选择——本地压测不需要 TLS 开销,代价是流量明文,部署到公网前必须开 HTTPS(见 2.2 接口设计的讨论)。app:进程级参数。threads_num: 4决定 Drogon 起几个 IO 线程(等于压测时的并发处理能力);log_path/log_level控制日志去哪、打多细(下一节展开);run_as_daemon: false让程序前台跑,方便 systemd 托管和调试。db_clients:Drogon 内置连接池的声明。这里写的只是"配置",真正创建连接池对象要等run()(详见 2.4 的getDbClient时序坑)。connection_number: 10是连接池大小——它和阶段一 ~371 QPS 的瓶颈直接相关:10 条连接 × 每条持行锁到事务提交,就是你能同时处理的上限。
加载方式在 main.cc:
1 | drogon::app().loadConfigFile("./config.json"); |
一个容易误会的点:loadConfigFile 返回的是 HttpAppFramework&(链式调用),不是 bool。配置解析失败不是返回 false,而是抛异常——ConfigLoader 构造时 throw runtime_error,程序根本起不来。换句话说:能正常启动 = 配置必然加载成功,不需要在启动日志里找"配置加载失败"这种永远看不到的话。
三、日志:Drogon 内置日志够用在哪,能力边界在哪
Drogon 内置了基于 trantor 的日志(LOG_INFO / LOG_WARN / LOG_ERROR / LOG_FATAL),直接可用:
1 | // src/main.cc |
LOG_FATAL输出后直接终止进程——用在"配置正确性不可挽回"的场合(db 客户端为空意味着任何请求都会 SIGSEGV,与其崩得莫名其妙,不如带诊断信息退出)。- 日志去往
log_path(./logs),级别由log_level控制。
功能抉择:为什么阶段一选内置日志,而不是立刻上重型异步日志方案?
| 方案 | 优点 | 代价 |
|---|---|---|
| A. Drogon 内置(选定) | 零额外依赖、API 极简、和 Drogon 生命周期天然对齐 | 同步落盘、无环形缓冲、无滚动策略细粒度控制 |
| B. 异步日志(spdlog + 环形缓冲) | 日志 IO 不阻塞业务线程、吞吐高几个数量级 | 多一个依赖 + 配置 + 队列丢弃策略要自己定 |
选 A,放弃的代价是"高峰期日志吞吐":同步落盘意味着每条日志都要等文件 IO 完成,日志打得多会直接拖慢 IO 线程。所以配套决定是日志级别默认 warn——秒杀高峰期把 info 全关掉,只留警告和错误。这是典型的"靠配置保命":代码里 LOG_INFO 照写,部署时用 log_level 决定开多少。等压测数据显示"日志本身成了瓶颈"的那一天,再换异步方案,代价是换来(换走)什么到时候用数据说话。
四、全局异常:从 SQL 异常到 HTTP 语义的一条链路
异常处理最容易做坏的地方是"每一层各自为政"。seckill-cpp 的做法是错误语义在服务层定死,状态码在控制器层统一映射。
服务层 SeckillService 用 (bool ok, std::string msg) 回调收口,语义在头文件里写死:
1 | // 回调语义: |
SQL 层的异常在两处被转译:execSqlAsync 的异常回调拿到 std::exception_ptr,用 std::rethrow_exception 取消息,统一加工成 "DB_ERROR: " + what():
1 | [tx, cb](const std::exception_ptr &eptr) { |
控制器层 SeckillController 再把业务语义映射成 HTTP 状态码:
1 | root["code"] = ok ? 0 : 1; |
功能抉择:为什么用 409 而不是笼统的 400/500?
| 场景 | HTTP | body | 客户端语义 |
|---|---|---|---|
| 下单成功 | 200 | {"code":0,"msg":"success"} |
完成 |
| 库存不足 | 409 | {"code":1,"msg":"SOLD_OUT"} |
不该重试,抢光了 |
| 重复下单 | 409 | {"code":1,"msg":"DUPLICATE_ORDER"} |
不该重试,已有订单 |
| 参数缺失 | 400 | {"code":400,"msg":"missing or invalid userId/skuId"} |
请求本身错了 |
| DB 异常 | 500 | {"code":1,"msg":"DB_ERROR: ..."} |
可以重试,系统问题 |
选 409 的原因:它是"资源当前状态冲突"的标准语义(RFC 7231),比 400 更能表达"你的请求没问题、是资源状态不对"。这个区分直接关系到雪崩——如果售罄也返回 500,客户端重试逻辑会把流量持续打在已售罄的接口上;而 409 让客户端知道"别试了"。错误语义如果不在源头定死,一定会在某个中间层被吞掉或误分类。
入参校验放在控制器层第一道(缺 userId/skuId 直接 400,不进服务层):
1 | const auto &json = req->getJsonObject(); |
五、健康检查与排错脚本:可运维性的另一半
基础设施不只是代码,还有探活和排错工具:
GET /api/health(HealthController,无依赖、不碰 DB):给网关/探活/K8s liveness 用。秒杀服务挂了,负载均衡器靠它摘流量。scripts/debug-wsl.sh三件套:check(MySQL 连通 + 表数据 + Drogon 是否真带 MySQL 后端的 7 步探测)/asan(ASan/UBSan 构建精确定位崩溃行)/gdb(Debug 构建带行号 bt)。scripts/smoke-seckill.sh:并发抢购 + MySQL 核对不超卖的回归验证——每次改完代码跑一遍,等于把"不超卖"这条底线变成自动化检查。
这三个加在一起,才让"出问题时能快速定位"成立:探活告诉你在哪台机器挂了,排错脚本告诉你为什么挂,冒烟脚本告诉你改完有没有破坏底线。
六、可运行验证步骤
1 | # 1) 启动服务(前台,方便看日志) |
七、基础设施取舍总结
| 决策点 | 选定 | 放弃的代价 |
|---|---|---|
| 配置 | 全部外置 config.json(三段式) |
多一层文件管理;改端口/线程要改文件 |
| 日志 | Drogon 内置 + log_level=warn 默认 |
无异步落盘/环形缓冲,高峰期日志吞吐受限 |
| 错误语义 | 服务层 (ok, msg) 定死 |
枚举/异常式表达更类型安全,字符串有拼写风险 |
| 状态码 | 业务拒绝 409 / 参数 400 / 系统 500 | 客户端需按文档区分,不能只认 200 |
| 连接池 | connection_number: 10 |
池小限并发,池大耗内存;需按压测调 |
| 探活/排错 | health + debug 三件套 + smoke 脚本 | 维护成本,但换排查效率 |
一句话收尾:配置让系统可部署,日志让事故可还原,异常语义让故障不扩散——三件套在功能跑通之后补上,才算是"能上线"而不是"能跑"。
配套仓库:
https://github.com/Hespethorn/seckill-cpp(本文对应v0.1.x,配置config.json、错误链路src/controllers/SeckillController.cc+src/service/SeckillService.cc、探活src/controllers/HealthController.cc、排错scripts/debug-wsl.sh、回归scripts/smoke-seckill.sh)。本系列是作者个人的 C++ 秒杀系统实战记录,所有方案、代码与踩坑均为原创。

