秒杀系统高并发优化实战(C++ / Drogon):2.4 Drogon 多模块项目搭建(CMake + 子项目)
服务能跑、压测能过,可换台机器 cmake --build 之后报 No database is supported——Drogon 启动不报任何错,数据库客户端却永远是空的。这种"编译不报错、运行才炸"的坑,根子不在业务代码,而在项目搭建这道关:C++ 的依赖是 ABI 级别的,探测失败不会红字报错,只会静默给你装一个"缺胳膊少腿"的版本。本文把 seckill-cpp 当前的工程结构、CMake 两层写法、以及依赖安装里的暗坑一次讲透。
配套仓库
seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),本文对应阶段一v0.1.x,全部脚本与 CMake 见根目录CMakeLists.txt、src/CMakeLists.txt、scripts/setup-wsl.sh、scripts/build-wsl.sh。真实编译在 WSL(Ubuntu 22.04+)完成。
一、项目搭建是什么、坑在哪、本质一句话
- 是什么:把 Drogon、MySQL 客户端、
jsoncpp这些依赖,和自己的源码(controller / service / main)组织成一个可重复构建的产物。Java 改个pom.xml加个 jar 就完事,C++ 这一步要管头文件路径、链接库、编译标准、以及依赖"到底有没有真的被编进去"。 - 坑在哪:Drogon 的 MySQL 后端是一个编译期开关。它的
FindMySQL.cmake探测失败时不会停下来报错,而是默默改用"跳过版" DB 管理器——find_package(Drogon)照过,add_executable照编,build/里照样出一个能跑的二进制,直到你getDbClient("default")时才发现客户端是空的。 - 本质一句话:C++ 项目搭建的核心矛盾是依赖的编译期耦合——一个依赖没编对,不是连不上那么简单,而是你拿到的整个库对象内部都可能是残的,而且它不会主动告诉你。
二、当前工程结构:两级 CMake 平铺
先看真实目录树(节选自仓库):
1 | seckill-cpp/ |
顶层 CMakeLists.txt 完整内容(已是仓库真实写法):
1 | cmake_minimum_required(VERSION 3.16) |
子项目 src/CMakeLists.txt:
1 | add_executable(seckill-cpp |
几个容易忽略的点:
CMAKE_CXX_STANDARD_REQUIRED ON不是装饰——它强制 C++17,否则编译器按默认(可能 C++14)编,Drogon::Drogon的目标属性不一定够用。target_include_directories(... PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})是让#include "service/SeckillService.h"能按"相对 src/ 的路径"解析的关键。不写它,你就得写一堆相对上层的../service/xxx.h,丑且易错。add_subdirectory(src)把构建单元下沉到子目录,顶层只管"全局标准 + 找 Drogon",子目录只管"编什么文件"。这是 CMake 最朴素的模块化形态。
整个构建与链接关系如下:
三、功能抉择一:单层 src/ 平铺 vs 拆多个 CMake 子项目
这是搭建时第一个要拍板的结构决策:
| 方案 | 做法 | 优点 | 代价 |
|---|---|---|---|
| A. 两级平铺(选定) | 顶层 + src/ 一个可执行文件,controller/service 按目录分但共用一个编译单元 |
配置极简、target_include_directories 一处搞定;改一个文件只重编小量 |
没有编译期模块边界,source 间可任意互相 include,容易长出循环依赖 |
| B. 多子项目 | 拆成 common/、service/、web/ 三个 add_library 静态库,各自 target_include_directories + target_link_libraries |
编译隔离、接口边界清晰、可并行编译、单模块改动只重编该库 | 要管 link 顺序、头文件可见性(PUBLIC/PRIVATE)、跨库依赖,配置成本陡增 |
我选 A,放弃的代价是"编译期边界":阶段一只有 5 个源文件,拆成三个静态库带来的并行编译收益几乎为零,反而要天天跟 target_link_libraries 的可见性、循环依赖较劲。C++ 里"目录分层"和"编译单元分层"是两件事——我现在只做目录分层(物理好读),不做编译单元分层(等真需要)。
什么时候该翻到方案 B?两条硬指标,命中任意一条就该拆:
- 源文件超过 ~20 个、或单文件编译时间显著拖慢整体(增量编译受不了);
- 出现"为了用一个小工具函数,把整条业务链路都拉进同一个编译单元"的循环依赖苗头。
真要拆,写法是把 src/CMakeLists.txt 改成:
1 | # common/ 基础库(无业务依赖) |
注意 PUBLIC vs PRIVATE:上层需要的头文件路径用 PUBLIC(传递下去),只在自己内部用的用 PRIVATE。这一步做对,模块边界才是真的边界。
四、功能抉择二:C++17 回调式 ORM vs C++20 协程
src/CMakeLists.txt 里 set(CMAKE_CXX_STANDARD 17) 不是随手写的。Drogon 的 orm 有两套 API 形态,取决于编译标准:
- C++17 回调式:
commit()/rollback()是void,结果和异常靠"成功回调 + 异常回调"两路>>挂接。SeckillService.cc里execSqlAsync(sql, 成功回调, 异常回调, 参数...)就是这套。 - C++20 协程式:
commit()返回drogon::Task<bool>,必须co_await才能拿到结果,整条链路写成async函数。
选 C++17,放弃的代价是"回调嵌套":newTransactionAsync → 扣减成功回调 → 落单回调 三层 lambda 嵌套,可读性差、变量要靠按值捕获传递(见 SeckillService.cc 里那一串 cb = std::move(callback)、by value userId)。换来的是:编译环境要求低(WSL 默认 g++ 够用)、不用纠结 co_await 和栈展开的边界。等后面阶段四要做复杂编排时,再评估切协程版——届时把 SeckillService.cc 的提交改成 Task<bool> 即可。
五、功能抉择三:构建类型与三套 build 目录
scripts/build-wsl.sh 固定走 Release:
1 | cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release |
选 Release 默认,代价是我被迫准备了另外两个构建目录:前面排查 SIGSEGV 时,Release 是 -O3 -DNDEBUG,gdb 拿不到行号、bt 全是空括号——死活定位不到崩溃行。于是在 scripts/debug-wsl.sh 里补了:
build-debug:-DCMAKE_BUILD_TYPE=Debug(-g),进 gdb 看带行号栈;build-asan:Debug+-fsanitize=address,undefined,让 ASan 直接报"第几行踩了空指针"。
教训:默认 Release 让交付产物快、体积小,但调试体验为零。正确做法是"跑用 Release、查问题切 Debug/ASan",而不是为了好调就常驻 Debug(那样压测数据失真)。三套目录各司其职,互不覆盖。
六、依赖安装的暗坑:libmariadb-dev 而不是 libmysqlclient-dev
scripts/setup-wsl.sh 是整套搭建里最值钱的一段——它决定了 Drogon 的 MySQL 后端到底编没编进去:
1 | sudo apt-get install -y git g++ cmake ninja-build \ |
两个必须知道的坑:
FindMySQL.cmake只认这三个库名:mysqlclient_r/mariadbclient/mariadb。Ubuntu 的libmysqlclient-dev只装出libmysqlclient.so(5.5+ 已去掉_r变体),不在查找名单里 → 探测静默失败 → 装出"跳过版"libdrogon.a,运行时报No database is supported。必须装libmariadb-dev(提供libmariadbclient.so,命中名单)才能把 orm/MySQL 后端编进去。- Drogon 是静态库
libdrogon.a:ldd ./build/src/seckill-cpp查不到任何 Drogon/MySQL 记录,因为它被静态编进了可执行文件。排查"到底带没带 MySQL"不能靠 ldd,要nm libdrogon.a | grep -i mysql看符号、或nm ./build/src/seckill-cpp | grep MysqlConnection。这也是前面 debug 脚本里那 7 步探测的来源。
-DBUILD_MYSQL=ON 是 Drogon 源码编译的开关——漏了它,前面所有 apt 装得再对也白搭。-DBUILD_EXAMPLES=OFF 只是省编译时间,与功能无关。sudo ldconfig 让新装的库进系统缓存,否则运行时找库会 404。
七、可运行验证步骤
在 WSL 里搭一遍,确认 MySQL 后端真的编进去了:
1 | # 1) 一次性安装依赖 + Drogon 源码编译(约十几分钟) |
第 2、4 步是最值得养成的习惯:C++ 的依赖真不真,用 nm 看符号比看 find_package 是否成功靠谱一万倍。
八、搭建取舍总结
| 决策点 | 选定 | 放弃的代价 |
|---|---|---|
| 模块拆分 | 两级平铺(1 个可执行文件) | 无编译期边界,易长循环依赖 |
| 编译标准 | C++17 回调式 ORM | 三层 lambda 嵌套,可读性差 |
| 默认构建 | Release | 调试无行号,需额外 build-debug/asan |
| 生成器 | Ninja | 首次配置信息比 Make 少(习惯问题) |
| 依赖探测 | find_package(Drogon) | 成功 ≠ 后端真编入,得用 nm 复核 |
| MySQL 客户端库 | libmariadb-dev | 不是 Ubuntu 默认的 libmysqlclient-dev |
| Drogon 链接 | 静态库 | ldd 查不到,排查走 nm |
一句话收尾:C++ 项目搭建的可靠性,后端依赖探测>>CMake 写法本身。CMake 写错会立刻红字报错,依赖"探测静默降级"才最要命——而唯一的治本手段,是在 CI 或本地加一条 nm 符号校验,把"看起来编过了"变成"确认编进去了"。
配套仓库:
https://github.com/Hespethorn/seckill-cpp(本文对应v0.1.x,构建脚本scripts/setup-wsl.sh、scripts/build-wsl.sh、排错scripts/debug-wsl.sh)。本系列是作者个人的 C++ 秒杀系统实战记录,所有方案、代码与踩坑均为原创。

