简介:面向Ubuntu平台Web开发者的Drogon框架实战源码包,适合熟悉C++14、希望构建高性能HTTP服务的中高级开发者,也可作为学习轻量级C++Web框架的入门范例。包内16个文件,包含6个cc源码文件与4个头文件,覆盖登录控制器、Token校验、HTTP GET/POST接口实现,另有JSON配置文件、数据库文件db及工程说明txt,可快速定位功能模块。资源仅23KB,结构紧凑,已有374人学习。重点展示了如何用Drogon集成MySQL与SQLite3数据库,实现基于Token的登录认证流程,并通过ORM完成用户校验与数据查询。对理解Drogon路由注册、数据库配置及HTTP响应构造有直接参考价值,可在实际项目中借鉴其登录鉴权与双数据库适配思路。
1. 在Ubuntu上跑通Drogon,先解决C++ Web服务“重”的问题
如果手里有个 QPS 要求上万、延迟 P99 不超过 50ms 的 API 服务,用动态语言写业务很快,但压测时 CPU 先跑满、连接一多吞吐往下掉,这是很多团队在 Ubuntu 上转向 C++ Web 框架的直接原因。Drogon 框架恰好填了这个空档:C++17 编写、基于 epoll 做非阻塞 IO,内置协程支持,代码写起来像 Spring MVC,但性能数据比常见动态语言高一到两个量级。本篇文章面向两类人:一类是 C++ 基础不错、想在 Ubuntu 上快速搭出可交付 API 服务的后端开发者;另一类是已经被并发折腾过、想评估 Drogon 值不值得用在生产项目里的技术负责人。我会从编译安装、路由控制器、数据落库到生产部署完整走一遍,把能复现的命令都贴出来,把会让新手翻车的细节单独拎出来讲。
2. 从零编译 Drogon 框架:依赖清单、CMake 参数与第一个 HTTP 服务
2.1 依赖准备:Ubuntu 下装齐编译工具和基础库
Drogon 官方推荐从源码编译安装,而不是直接用发行版自带的旧版本包。原因有两个:Ubuntu 官方源里的版本往往滞后,缺少新特性;Drogon 的 cmake 配置会按可用依赖动态开启功能,自己编译能精确控制哪些模块编译进去。在 Ubuntu 20.04 及更高版本上,默认的 gcc 版本已经支持 C++17,直接安装工具链即可。
# 基础编译工具链 sudo apt update sudo apt install -y build-essential cmake git libssl-dev zlib1g-dev # 数据库客户端库,按需选装 sudo apt install -y libmysqlclient-dev # 使用 MySQL 时安装 # sudo apt install -y libpq-dev # 使用 PostgreSQL 时安装这段命令先装的是编译 C++ 项目最基础的四样东西:gcc/g++ 靠build-essential提供,cmake用来生成构建系统,git拉源码,libssl-dev提供 OpenSSL 开发头文件。Drogon 把 TLS 支持做成了硬依赖,哪怕你只想跑 HTTP 明文服务也要装这个库,否则后面 cmake 配置阶段直接报错。zlib1g-dev是压缩支持用的,Drogon 处理 gzip 响应时依赖它。
数据库客户端库是按需的。我一般建议在编译前就把最终要用的数据库客户端装好,因为 Drogon 的 cmake 检测到libmysqlclient或pq存在时会自动启用对应的 ORM 后端,后续生成模型和查询代码就不用重新编译。如果编译后才装数据库库,得把 build 目录删掉重新配置,多花十分钟。
2.2 源码编译与安装:cmake 选项怎么选
源码编译的流程在所有 Linux 发行版上大同小异,Ubuntu 上最容易踩的坑是git submodule update --init这一步漏掉。Drogon 仓库里通过 submodule 引用了几个三方库,不拉全的话 configure 阶段会提示找不到依赖头文件。
# 拉取 Drogon 官方仓库源码 git clone https://github.com/drogonframework/drogon.git cd drogon # 初始化并拉取 submodule 依赖 git submodule update --init # 配置并编译 mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local make -j4 sudo make install sudo ldconfig逻辑说明:git submodule update --init会把仓库里标记为子模块的三方库(如 JSON 解析相关的依赖)拉取到对应目录,这是新手最容易漏的一步。-DCMAKE_BUILD_TYPE=Release必须显式指定,Debug 模式下编译出来的二进制性能差好几倍,而且生成的库文件名带 debug 后缀,后面链接经常出问题。-DCMAKE_INSTALL_PREFIX=/usr/local决定头文件和库文件的安装路径,保持默认即可。
参数说明:make -j4中数字表示并行编译任务数。如果机器内存只有 2GB,-j$(nproc)可能把内存吃满导致编译进程被杀,保守用-j2或-j4。Ubuntu 20.04 默认 gcc 9 编译 Drogon 没问题,但如果你用的是 Ubuntu 18.04(默认 gcc 7),建议先升级到 gcc 9 再编译,否则协程相关代码可能触发旧编译器 bug。
编译完sudo ldconfig这一步很多人忘。Drogon 安装的动态库在/usr/local/lib,这个路径不一定在系统默认搜索范围内,不执行 ldconfig 的话后面 pkg-config 能找到.pc文件,但运行时-ldrogon会报找不到共享库。
2.3 最小可运行 main.cpp:注册监听端口并启动
依赖装齐、编译安装完成后,写一个最小可运行的服务验证环境是否真的通了。这一步不要一上来就套框架,一个能监听端口、能响应请求的 main 函数足够说明 Drogon 已经能用了。
#include <drogon/drogon.h> int main() { // 监听所有网卡的 8080 端口 drogon::app() .addListener("0.0.0.0", 8080) .setThreadNum(4) // 启动 4 个 IO 线程,按 CPU 核数调整 .run(); return 0; }逻辑说明:addListener("0.0.0.0", 8080)让服务监听本机所有网卡的 8080 端口,这样内网其他机器也能访问;setThreadNum(4)设置 IO 线程数,这个值直接决定并发处理能力;run()进入事件循环,进程在这里阻塞直到收到退出信号。此时没有注册任何路由,curl 访问会得到一个 404,这属于正常现象。
# 编译链接,pkg-config 会自动带上头文件路径和库路径 g++ -std=c++17 main.cpp -o hello $(pkg-config --cflags --libs drogon) # 启动服务 ./hello # 另开终端验证 curl -v http://127.0.0.1:8080/逻辑说明:pkg-config --cflags --libs drogon展开后是-I/usr/local/include/drogon -L/usr/local/lib -ldrogon这类参数。如果提示找不到drogon.pc,多半是 pkg-config 的搜索路径没包含/usr/local/lib/pkgconfig,执行export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH后再编译。
参数说明:编译命令里-std=c++17必须显式声明,Drogon 头文件里大量使用了if constexpr、std::optional这类 C++17 特性。如果你的代码要跑在 Ubuntu 18.04 的旧 gcc 上,还需要额外加-pthread链接线程库。
提示:curl 返回 404 不代表环境有问题,反而说明 HTTP 服务已经在正常处理请求了。环境验证的目的就是确认进程能起来、端口能监听、编译链没问题,路由注册是下一章的事。
3. 路由、控制器与中间件:Drogon 的 HTTP 处理链条
3.1 注解路由:用宏把 URL 映射到 C++ 方法
Drogon 的路由设计借鉴了 Java Web 框架的写法,每个处理器类继承HttpController模板,在类内部用宏注册路由映射。这种方式比集中式路由表好在哪?路由和处理函数放在同一个文件里,改动一个接口不需要在路由表和控制器之间反复跳转,接口多了以后维护成本低很多。
#include <drogon/drogon.h> class HelloCtrl : public drogon::HttpController<HelloCtrl> { public: METHOD_LIST_BEGIN // GET /hello 映射到 hello 方法 METHOD_ADD(HelloCtrl::hello, "/hello", drogon::Get); // GET /user/{id} 映射到 getUser,{id} 为路径参数 METHOD_ADD(HelloCtrl::getUser, "/user/{id}", drogon::Get); METHOD_LIST_END void hello(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { auto resp = drogon::HttpResponse::newHttpResponse(); resp->setBody("Hello, Drogon"); callback(resp); } void getUser(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { auto id = req->getPathParameter("id"); auto resp = drogon::HttpResponse::newHttpJsonResponse( drogon::json{{"id", id}, {"name", "user_" + id}}); callback(resp); } };逻辑说明:HttpController<HelloCtrl>这种写法是 CRTP 静态多态,框架在编译期就拿到了控制器类型信息,不需要运行时反射,这也是 Drogon 性能好的原因之一。METHOD_ADD宏有三个参数:回调函数指针、URL 路径、HTTP 方法。第三个参数后面其实还隐藏着第四个参数——过滤器列表,下一小节会用到。
回调函数的签名必须严格匹配:const drogon::HttpRequestPtr&是请求对象,std::function<void(const drogon::HttpResponsePtr&)>&&是响应回调。拿到响应对象后调用callback(resp)把它交回框架,整个请求就结束了。如果一直不调用 callback,客户端会一直挂着,直到超时。
路径参数{id}的写法对应 Spring 里的@PathVariable。注意参数名必须和 URL 里的花括号名一致,取的时候通过req->getPathParameter("id")。返回 JSON 时用newHttpJsonResponse传入drogon::json{{...}}初始化列表,框架会自动设置 Content-Type 为 application/json。如果你要返回数组,drogon::json也支持drogon::json::array()。
3.2 请求参数:Query、路径参数和 JSON 体
真实业务里接口拿参数的方式无外乎三种:URL 上的 Query 参数、路径里带的资源 ID、POST 请求体里的 JSON。Drogon 对这三种场景都提供了直接 API,但有个容易搞混的点:getParameter同时覆盖 Query 和表单格式的 body,getJsonObject只解析 application/json 类型的请求体。
void createUser(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { // 1. Query 参数: /create_user?name=tom&age=25 auto name = req->getParameter("name"); auto ageStr = req->getParameter("age"); // 2. 请求体 JSON: {"age": 25, "tags": ["admin", "vip"]} auto jsonPtr = req->getJsonObject(); if (jsonPtr && jsonPtr->contains("age")) { int age = (*jsonPtr)["age"].as<int>(); } if (jsonPtr && jsonPtr->contains("tags")) { auto tags = (*jsonPtr)["tags"]; for (auto& tag : tags) { // 遍历数组 } } // 3. 所有参数的原始 map(表单场景很有用) auto params = req->getParameters(); auto resp = drogon::HttpResponse::newHttpJsonResponse( drogon::json{{"status", 0}}); callback(resp); }逻辑说明:getParameter返回的是字符串,数字类型要自己转换,所以我先用getParameter("age")取出 string 再手动转。getJsonObject()返回的是Json::Value的智能指针,如果请求体不是合法 JSON 会返回空指针,所以调用前必须判空。JSON 里的数字用.as<int>()转成 int,字符串用.asString()。
这里有个值得注意的细节:req->getJsonObject()只认Content-Type: application/json的请求,如果你用 Postman 发的是text/plain但 body 是 JSON 格式,这个接口拿到的还是空指针。建议请求端统一 Content-Type。另外.as<int>()在类型不匹配时会抛std::runtime_error,生产环境建议包一层 try-catch,或者用asInt64()避免 32 位溢出。
路径参数getPathParameter的用法上一节已经演示过,这里补充一个常见误用:getParameter是拿不到{id}这种路径参数的,两者 API 完全不同,写代码时要注意区分。表单提交场景,getParameters()拿到的是 key-value map,配合auto&遍历可以处理动态表单字段。
3.3 中间件过滤链:在控制器之前统一做鉴权与日志
鉴权、访问日志、限流这些横切逻辑如果塞进每个业务方法里,代码会越来越臃肿。Drogon 的过滤器(Filter)机制做这件事,概念上和 Servlet 的 Filter 一样,每个过滤器实现doFilter方法,内部分支决定是放行还是直接返回响应。
#include <drogon/drogon.h> #include <drogon/HttpFilter.h> class AuthFilter : public drogon::HttpFilter<AuthFilter> { public: void doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& fcb, drogon::FilterChainCallback&& fccb) override { auto token = req->getHeader("X-Token"); if (token.empty()) { // 无令牌,直接返回 401,不再往下走 auto resp = drogon::HttpResponse::newHttpResponse(); resp->setStatusCode(drogon::k401Unauthorized); resp->setBody("missing token"); fcb(resp); // 终止过滤链 return; } // 校验通过,放行到下一个过滤器或控制器 fccb(); } };逻辑说明:doFilter的三个参数中,fcb是终止回调,调用后请求立即返回,后续过滤器和控制器的代码都不会执行;fccb是放行回调,调用后框架自动进入下一个过滤器或直接到达控制器。两个回调必须二选一调用,否则请求会一直阻塞到超时。这是新手最容易漏写的分支。
过滤器在路由上的挂载方式和业务方法绑定:METHOD_ADD宏的第四个参数是过滤器类名。一个控制器方法可以挂多个过滤器,字符串用逗号隔开,执行顺序从左到右。如果把过滤器挂在类级别(在类定义上通过宏声明),则对类下所有方法生效,实际项目里我习惯把全局鉴权放类级别,把单接口的校验放方法级别。
过滤器链的好处在于拿到请求时就能拦截,不需要每个接口里都写一遍req->getHeader("X-Token")的判空逻辑。除了鉴权,日志过滤器和限流过滤器也可以用同样的方式接入,改一个接口的过滤配置只需要改METHOD_ADD那一行,对比集中式路由表要改两处的方式确实省事。
提示:过滤器类的注册由框架自动完成,前提是它被链接进最终二进制。如果你把过滤器写在一个单独的静态库里但没被主程序引用,链接器可能会将其丢弃,导致过滤器不生效。常见的解决方式是在主程序源文件里显式 include 过滤器头文件。
4. 数据层落库:Drogon ORM 与数据库连接池配置
4.1 在配置文件中定义数据库客户端
Web 服务离不开数据库,Drogon 的数据访问层设计比较特别:不是每个请求自己 new 一个连接,而是启动时按配置创建连接池,运行期所有请求共享池里的连接。配置走 JSON 文件,启动时加载。下面这个配置片段以 PostgreSQL 为例,MySQL 的差异只体现在db_type字段上。
{ "db_clients": [ { "name": "default", "db_type": "PostgreSQL", "host": "127.0.0.1", "port": 5432, "dbname": "app", "user": "app_user", "password": "app_pass", "connection_number": 10, "timeout": 5.0 } ], "listeners": [ { "address": "0.0.0.0", "port": 8080 } ] }逻辑说明:db_clients可以配置多个数据库实例,每个实例有自己的连接池,name字段用来在代码里区分。启动时框架会预先创建connection_number个连接放进池子,业务代码向池子申请连接时如果全部占用,就等待timeout指定的秒数,超时后抛出异常。
timeout的单位是秒,Drogon 内部使用浮点数所以可以配0.5这种值。实际项目中 5 秒偏长,客户端等 5 秒才能拿到数据库连接,用户的感知就是接口卡死。我一般配 1~2 秒,宁可快速失败返回错误,也不要让请求长时间挂在连接池上。
配置文件的加载方式有两种:代码里调用drogon::app().loadConfigFile("config.json")然后再run(),或者启动命令行加-c config.json。推荐后者,部署环境切配置不需要重新编译。注意listeners如果也在配置文件里写,代码里的addListener可以省略,两者同时存在时以配置文件为准。
4.2 生成 ORM 模型并实现增删改查
Drogon 提供了代码生成器drogon_ctl,根据数据库表结构自动生成 ORM 模型类。前提是已经有了表和配置文件,生成命令指定配置文件、模型输出目录和要生成的表名。
# 生成 models 目录下的模型类(自动连接数据库读取表结构) drogon_ctl create model -c config.json -m ./models参数说明:-c指定上一步写好的配置文件,-m指定模型输出目录,-t可以指定只生成某几张表(例如-t users,orders),不加-t则生成库里所有表的模型。生成的模型类命名规则是表名单数帕斯卡命名法,users表对应Users.h。
生成完后,业务代码里操作表有两种方式:一种是更底层的execSqlAsync写 SQL,另一种是直接用生成的模型类。对于刚接触 Drogon 的团队,我建议先从参数化 SQL 入手,因为模型类的 API 需要一点时间熟悉,而 SQL 谁都会写。
#include "models/Users.h" #include <drogon/drogon.h> void addUser(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { // 从连接池获取一个数据库客户端 auto db = drogon::app().getDbClient(); auto jsonPtr = req->getJsonObject(); std::string name = (*jsonPtr)["name"].asString(); // 参数化插入,$1 是占位符,name 作为绑定参数传入 db->execSqlAsync( "INSERT INTO users(name) VALUES($1) RETURNING id", [callback](const drogon::orm::Result& result) { auto resp = drogon::HttpResponse::newHttpJsonResponse( drogon::json{{"status", 0}, {"id", result[0]["id"].as<int64_t>()}}); callback(resp); }, [callback](const drogon::orm::DrogonDbException& e) { auto resp = drogon::HttpResponse::newHttpJsonResponse( drogon::json{{"status", 1}, {"error", e.base().what()}}); callback(resp); }, name); // 绑定参数,对应 SQL 里的 $1 }逻辑说明:execSqlAsync是异步接口,调用后立即返回,SQL 在后台执行,结果通过两个回调返回——第一个回调传成功结果,第二个传异常。这样设计的好处是数据库操作不阻塞 IO 线程,在高并发下即使数据库慢,服务进程也不会卡死。
参数化是必须养成的习惯:SQL 字符串里用$1、$2这样的占位符,第 4 个参数开始按顺序传入实际值,框架内部走预处理语句。直接把name拼进 SQL 字符串是典型的注入漏洞写法,Drogon 的接口设计虽然允许你拼 SQL,但生产项目里绝对不要这么做。
Result对象的访问方式和二维数组类似:result[0]是第一行,["id"]是列名,.as<int64_t>()做类型转换。注意 PostgreSQL 的RETURNING id子句可以让 INSERT 语句直接返回自增主键,省掉一条单独的 SELECT。
4.3 连接池参数:connection_number 与 timeout 的权衡
连接池参数调优是所有数据库相关项目的隐藏难点,参数配小了高峰时连接不够用,配大了数据库服务端可能先扛不住。Drogon 的参数集中在db_clients节点里,下面这张表把关键参数和调整思路列出来。
| 参数 | 配置示例 | 调优建议 |
|---|---|---|
| connection_number | 10 | 先按 CPU 核数×2 起步,压测时观察是否报连接池超时 |
| timeout | 2.0 | 获取连接的最大等待秒数,生产建议 1~2 秒 |
| client_encoding | utf8mb4 | MySQL 必配,否则中文可能乱码 |
connection_number不是越大越好。每个数据库连接都要占用服务端进程/线程资源,如果配的 50 个连接全都建立起来,而数据库实例的max_connections默认只有 100,其他应用就连不上了。我见过一个项目因为连接池配了 80,两个实例就把数据库连接耗尽,运维排查了很久才发现是 Drogon 连接池干的。
timeout在并发尖峰时会成为瓶颈。如果业务高峰期出现大量Timeout while getting connection from pool的报错,优先检查是不是connection_number太小,而不是盲目调大timeout。调大 timeout 只是让请求等更久,吞吐量并没有提升,用户体验反而更差。
client_encoding这个参数在 Ubuntu + MySQL 8.x 的常见组合里很关键。MySQL 默认字符集可能是utf8mb4,但连接层字符集如果没有显式设置为utf8mb4,写入中文后查询出来就是乱码。PostgreSQL 场景下一般不需要额外配置,因为服务端和客户端编码默认一致。
5. Drogon 开发避坑指南:编译失败、乱码与协程内存翻车
5.1 缺少 libssl-dev 导致 TLS 相关编译失败
现象:cmake 配置阶段报Could NOT find OpenSSL,或者编译过程中出现openssl/ssl.h: No such file or directory。
原因:Drogon 把 OpenSSL 作为硬依赖,不管理论上你是否用到 HTTPS,cmake 在检测阶段找不到头文件就会直接终止。这是很多人第一次编译 Drogon 遇到的第一个拦路虎,和你在代码里写不写 SSL 没有任何关系。
解决:执行sudo apt install -y libssl-dev后重新从mkidr build && cd build开始配置。注意只需要重跑 cmake 配置阶段,不用重新 clone 源码。血泪经验:如果是在容器环境里编译,Dockerfile 里要在apt-get install段就加上libssl-dev,否则每次重建镜像都会卡在同一处。
5.2 协程线程池参数设置不当导致内存飙升
现象:启用协程后服务平稳运行,但 RSS 内存从几十 MB 慢慢涨到几个 GB,重启后回落,过段时间又涨上去。
原因:Drogon 每建一个协程要分配一块独立的栈空间,默认协程栈大小是 128KB。如果业务代码里大量并发发起协程,同时每个协程的调用链很深,栈空间会不断扩张且不会立即收缩归还给操作系统。另一个叠加因素是把setThreadNum设得过高,几十个线程共同吃内存,翻车速度更快。
解决:先确认协程栈大小和线程数都在合理范围。协程栈小于 128KB 时如果跳转深会导致栈溢出,这个值不建议向下调;线程数我一般设成物理核数,最多不超过 2 倍。真正优雅的解法是业务代码里避免在协程中嵌套大数组或深度递归,把大块数据放堆上而不是栈上。
另外注意:内存上升和不释放是两回事。协程结束后栈空间会复用而不是立刻释放,小幅波动正常,持续上升才是问题。观察手段是watch -n 1 ps -o rss,cmd -p <pid>,连续看五分钟趋势再下结论。
5.3 MySQL 8.x 下查询结果中文乱码
现象:接口写入中文后,再从数据库查出来显示成???,或者客户端收到的 JSON 中文变成乱码。
原因:两层问题叠加。第一层是 MySQL 表结构本身的字符集不是utf8mb4;第二层是 Drogon 连接 MySQL 时客户端字符集没有显式指定。Ubuntu 默认 locale 如果是 C/POSIX,客户端传给 MySQL 的连接字符集可能被协商成latin1,写入时做了错误转码。
解决:建表时用CREATE TABLE ... DEFAULT CHARSET=utf8mb4,这是治本。同时在 Drogon 配置文件里给对应db_clients项加上"client_encoding": "utf8mb4"。两层都改完后,重启服务再写入测试数据。乱码问题一旦发生,库里已有的脏数据通常要手动清理,所以在上线前就配好最省事。
5.4 路由注册没生效?先确认 Controller 是否被链接进二进制
现象:代码和路由注解都写好了,编译也成功,但 curl 访问对应 URL 永远 404,启动日志里也看不到路由表输出。
原因:Drogon 的路由注册发生在启动阶段,框架通过扫描已加载的符号找到所有HttpController的子类实例。如果把控制器写在了一个独立的静态库里,而主程序又没有直接引用这个静态库的任何符号,链接器在链接静态库时会把整个库丢弃,控制器根本没进最终二进制。
解决:最省事但不一定优雅的做法的把控制器源文件直接编进主程序编译命令里,或者用 CMake 的target_sources添加源文件而不要放进静态库再链接。如果必须用静态库,给链接器加-Wl,--whole-archive参数解决问题。启动后看到日志里有相关的控制器注册数量,再测路由。
5.5 高并发下出现大量 503,根因是监听 backlog 太小
现象:压测时 QPS 一到某个临界值,错误率突然飙升,全是 503,服务端 CPU 还没跑满。
原因:Drogon 默认的监听 backlog 参数可能不足以支撑瞬时高并发连接请求。当 TCP 三次握手完成但应用层还没来得及 accept 的连接数超过 backlog,内核会直接丢弃新连接,客户端表现为连接失败或 503。
解决:addListener方法可以多传一个参数指定 backlog 值,例如drogon::app().addListener("0.0.0.0", 8080, 1024)。压测前先把 backlog 调到 1024 或更大,能让峰值连接更平滑地被处理。这个参数平时不显眼,但线上扩容时经常是隐藏瓶颈。
6. 生产环境部署与压测:systemd 守护、关键参数与性能验证
6.1 影响性能的三个关键配置
代码能跑只是起点,真正上生产前我习惯先过一遍三处配置,它们对最终交付的可用性影响远超业务代码本身的优化空间。
| 配置项 | 建议值 | 说明 |
|---|---|---|
| setThreadNum | 物理核数 | 过高增加切换开销,过低浪费 CPU |
| setIdleConnectionTimeout | 120s | 与反向代理 keep-alive 对齐,避免连接被代理先断 |
| 日志级别 | kWarn | Debug 日志在高并发下会让吞吐量腰斩 |
线程数的坑在于超卖。8 核机器配 8 个线程是合理起步值,配 32 个线程反而因为锁竞争和上下文切换导致吞吐下降。验证方式很简单:压测时把线程数从 4 往上涨,每次涨一倍,记录 QPS 曲线,找到拐点回退一档。
日志级别的坑更隐蔽。Debug 日志每请求打 3~5 行,高并发下日志 IO 会成为主瓶颈。我见过一个项目从 Debug 切到 Warn 后 QPS 直接翻倍,不用改任何业务代码。
6.2 用 systemd 把 Drogon 服务托管成守护进程
手动./app启动的进程一关终端就没了,生产环境必须交给 systemd 管理。Ubuntu 20.04 及以上版本自带 systemd,写好 unit 文件后开机自启、崩溃重启都交给系统处理。
[Unit] Description=Drogon Web App After=network.target postgresql.service [Service] Type=simple User=www-data WorkingDirectory=/srv/app ExecStart=/srv/app/bin/app -c /srv/app/config.json Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.target逻辑说明:Type=simple是最常见模式,systemd 认为ExecStart启动的进程就是主进程,进程退出即服务结束。Restart=on-failure搭配RestartSec=3可以让进程崩溃后等 3 秒自动拉起。After=network.target postgresql.service保证网络和数据库先起来再启动应用,避免启动时连不上数据库直接报错退出。
配置好之后执行sudo systemctl daemon-reload重新加载配置,再用sudo systemctl enable app && sudo systemctl start app完成开机自启和首次启动。查看日志用journalctl -u app -f,这个命令会实时打印应用的 stdout/stderr,排查启动失败时第一步就看这里。
6.3 压测结果怎么看:吞吐量和延迟的平衡点
部署完成后用 wrk 做一个基准压测,确认配置和代码没有明显瓶颈。wrk 在 Ubuntu 上安装很简单,apt install wrk即可。
# 8 个线程、100 个并发连接、压测 30 秒 wrk -t8 -c100 -d30s http://127.0.0.1:8080/hello压测输出里重点看两个数:Requests/sec和Latency分布。Requests/sec是吞吐量,代表服务每秒能处理的请求数;Latency里的p99比p50更能反映真实体验。如果 p99 比 p50 高一个数量级,说明存在明显的长尾延迟,常见原因是连接池不够或 GC 式的暂停。
需要注意:wrk 在本机跑只能验证应用层能力,真正反映线上表现要在独立压测机上打流量,绕过本机网络栈的干扰。压测时如果出现连接被拒绝,先看服务进程还在不在,再看 backlog 是否太小,如果都没问题最后才怀疑防火墙。
这套流程走下来,一套能扛住常规生产压力的 Drogon 服务就落地了。说句实在话,Drogon 的上手门槛不比写一个 Spring Boot 服务高太多,坑主要集中在前三天的编译环境和连接池配置上,趟过去之后开发体验很顺畅。我自己的习惯是把常用的 Controller 基类、统一响应格式和过滤链抽出来作为团队内部脚手架,新项目起步能省半天时间。框架本身迭代快,遇到奇怪的编译问题先确认依赖版本和编译器版本,再去看 issue,基本都能找到答案。希望帮到你。
本文还有配套的精品资源,点击获取