写 C++ 后端的同学多半遇到过这种局面:需求要把模型接口包一层自己的 HTTP 服务,结果卡在选库上——上 cpp-httplib 觉得太轻,上 Boost.Beast 又怕工期撑不住,选完还发现超时、并发、日志、重试这些工程活没地方下手。这篇按「先定形态、再选依赖、最后补工程项」的顺序走一遍,给的是判断标准和可直接照做的动作,不是某个库的广告。
写 C++ 后端的同学多半遇到过这种局面:需求要把模型接口包一层自己的 HTTP 服务,结果卡在选库上——上 cpp-httplib 觉得太轻,上 Boost.Beast 又怕工期撑不住,选完还发现超时、并发、日志、重试这些工程活没地方下手。这篇按「先定形态、再选依赖、最后补工程项」的顺序走一遍,给的是判断标准和可直接照做的动作,不是某个库的广告。
下面每一节都尽量给可执行的东西:选型对照、骨架拆分方式、工程项清单、排查顺序。假设场景是一家 5 人小团队,要把一个模型接口包成自己的 WebAPI 给内部业务调用,接口不多但要求稳。你可以边看边对照自己的项目,缺哪块补哪块。
先定服务形态,再谈选库
很多人一上来就比库,其实顺序错了。先回答「这个服务是什么形态」,形态定了,候选库自然收窄到两三个。形态大致分三种:进程内嵌的同步服务,请求进来阻塞处理完返回;异步事件循环服务;以及只做转发、不透传业务逻辑的薄网关。形态不同,后面所有的依赖、并发模型、超时设计都不一样。
| 形态 | 适合什么场景 | 不适合什么场景 |
|---|---|---|
| 同步阻塞服务 | 逻辑简单、QPS 预期低、下游响应快 | 下游慢且并发高,线程会被占满 |
| 异步事件循环 | 并发高、下游慢、需要大量长连接 | 业务逻辑复杂、回调嵌套深,可读性差 |
| 薄转发网关 | 只做鉴权和转发,不改业务字段 | 需要做请求合并或业务编排时,会很快写乱 |
判断依据是并发量与业务复杂度,不是团队喜好。如果 QPS 预期不高、逻辑也就几步,同步阻塞模型更好读、更好排错,别为了「先进」硬上异步。要提具体并发水位,到站内性能相关页面核对当时的数字,别凭感觉拍。
依赖怎么挑:按可维护性倒推
选库别按名气排,按三个问题过滤:是否需要 TLS、是否需要 HTTP/2、是否接受引入体积较大的第三方依赖。三个问题的答案能砍掉大部分候选。剩下的再按可维护性排序——谁依赖少、谁接口稳、谁出问题时有明确的日志和错误码。
- 先写一页选型记录:候选库、许可证、最近维护情况、被砍掉的原因,半年后回头看能省很多争论
- 对每个候选做一次探针式验证:起一个最小服务,压一次并发,量出首字节延迟,建立自己的基线
- 把基线数据到 /tools/speed-test 对照测量口径,确认两边量的是同一个东西,不然结论会互相打架
- 对依赖多的库保持警惕:一个接口服务如果拉进来一堆用不上的组件,构建和排障成本都会上去
- 锁定版本并写进构建脚本,别用「最新版」,C++ 生态里小版本差异踩坑的概率不低
要注意一个不适用的情况:如果项目本来就重度依赖某个大框架,硬换一个轻量库反而增加维护面。判断标准是「换掉之后总依赖数是变少还是变多」,变多就别换。
一个最小可跑接口的骨架该有哪些
不管用哪个库,一个能上线的接口骨架都可以拆成五段:路由注册、请求体解析、下游调用、响应序列化、错误码返回。这五段分开写,别揉成一个函数。分开之后,改下游、改字段、加日志都在各自的位置动手,不用动整条链路。
- 路由注册把路径、方法、处理函数一一对应写在同一个文件里,方便一眼看完所有暴露出去的接口,改协议时不会漏。
- 请求体解析先做字段校验再转发:必填缺失、类型不符直接返回客户端错误,不要浪费一次下游调用。字段命名与含义到 /ai-dict 对照术语,避免上游叫 input、你叫 prompt 这种对不上。
- 下游调用把下游封装成一个函数,输入输出都用内部结构体,不直接透传上游字段名。要核对下游的实际计费口径,到 /price-watch 看当时的数字。
- 响应序列化统一收敛成内部结构再对外输出,后面换供应商时改动面小很多,调用方的解析代码也不用动。
- 错误码返回区分「客户端错」和「下游错」两类码,如果客户端也分得清这类错,就返回能定位原因的那一类,而不是统一回一个笼统的失败。
拆分之后有个额外好处:写单元测试时只测中间三段,不用真的起服务,测试跑得快,改动也敢改。
工程项清单:超时、并发、降级、日志
骨架搭完只是能跑,能不能稳看的是这几项有没有做。按清单过一遍,缺哪项补哪项,不要等线上出事再回头加。
| 工程项 | 最低要求 | 不做的后果 |
|---|---|---|
| 超时 | 连接、读取、整体三层分别给值并写明依据 | 单值一刀切,慢的时候分不清卡在哪一段 |
| 并发 | 用线程池或异步任务队列限量,出口加排队与退避 | 突发流量把下游打爆,连带自己的服务一起挂 |
| 降级 | 备用地址加开关,切换事件写进日志 | 主通道挂了只能等人工介入 |
| 日志 | 记请求 ID、耗时、下游状态码、重试次数四项 | 事后复盘不知道慢在哪、贵在哪 |
- 超时分三层设,每层都写清楚数值来源:连接超时看网络稳定性,读取超时看下游正常响应时间,整体超时按业务能接受的上限
- 并发限量在出口处做,不是入口处随便设个数字;触发限流后的恢复步骤见 /guides/api-rate-limit-recovery
- 降级开关要能在不重启的情况下切换,不然「备用」就是个摆设
- 日志按请求 ID 串起来,别每个模块各记各的,出问题时拼不出一个完整链路
- 把上面四项写成上线前检查单,每次发版过一遍,比事后补更省事
也要说清楚什么情况下不必做全套:内部工具、调用量很小的旁路服务,超时和日志做到位就够,并发限量和降级可以先留接口不实现。判断标准是「这个服务挂了会不会影响外部用户」。不会,就先简化。
出问题时的排查顺序
排查最容易犯的错是顺序颠倒:一上来就查性能,结果发现是连不通。正确的顺序是先验证通不通,再验证对不对,最后才看快不快。这个顺序能让大多数问题在前两步就被定位。
- 通不通:查 DNS 解析、端口是否可达、证书是否过期,这一步只在网络层做判断,不掺业务逻辑
- 对不对:看状态码和响应体,区分是鉴权问题、参数问题还是下游问题,别急着看耗时
- 快不快:前三步都正常再看耗时,分别量连接、首字节、整体三段,定位到具体环节再优化
- 写回文档:每次排查结论都记下来,同类问题第二次出现时直接查清单,不用从头再来
- 本地联调不通时按三步走,具体做法可参考 /guides/local-api-network-troubleshoot
如果排查中怀疑是对接的中转站或供应商不稳定,别只看一次结果,到 /sites 看这家的可信度记录和价格变动,再决定是继续调还是换。判断类结论都要写清不适用的情况:如果你和供应商之间还有一层内网代理,站内的可信度记录只能作参考,真实瓶颈可能在你自己的代理上。
下一步把上面的清单对着自己的项目过一遍,缺的模板和对照数据可以直接到站内对应页面取。→常见问题
C++ 做 WebAPI 一定要用重型框架吗?
不一定。并发要求不高、逻辑以转发和组装为主时,轻量单头文件库更省事,编译快、读起来也直白。只有当你要自己管连接生命周期、上长连接或高并发异步时,重型框架的复杂度才换得回价值。判断点只有一个:你的并发预期和下游延迟,而不是别人用什么。
接口和下游模型服务之间要加中间层吗?
看你是否需要统一鉴权、限流、日志和降级。如果需要,加一层薄网关比在每个业务接口里各写一遍划算;如果只是单个服务调单个下游,直接调更简单。折中做法是先把这四件事收敛到一个模块里,等第二个调用方出现时再独立成网关。
怎么判断并发数设多少合适?
从下游的限流额度和自身机器核数两头夹。先量出单请求平均耗时,再用目标 QPS 反推需要的并发数,最后压测确认不触发限流。上下游对不上时,宁可把并发压低、加排队,也不要靠猛试撞上限;具体档位和退避做法可查 /guides/concurrency-throttle-by-tier。