AI时代的程序员修养:高并发不是加机器那么简单

《AI时代的程序员修养》前几篇从接口、数据库、缓存、队列讲到异步任务。它们都是高并发系统的一部分。到了这一篇,要把视角抬高一点:一个服务到底怎么扛流量。

AI 很容易把“高并发”回答成几个熟词:加缓存、加机器、用异步、上队列、分库分表。这些词都对,但都不够。真正的高并发不是把每个点都换成更快的实现,而是知道整条调用链哪里会先满,哪里要限流,哪里能降级,哪里必须保护,哪里不能再往下游传压力。

高并发不是加机器那么简单。机器只是容量的一部分,边界才是关键。

先算账,再谈优化

很多性能问题,一开始不是代码问题,而是没有算账。

假设一个接口目标是:

  • 峰值 2000 QPS。
  • P95 延迟低于 200ms。
  • 每个请求查一次用户、一次订单、一次库存。
  • 每个请求还要调用一次推荐服务。

如果直接让 AI “优化接口”,它可能建议缓存、异步、索引。更应该先算一遍:

2000 QPS * 每请求 3 次数据库查询 = 6000 次数据库查询/秒 2000 QPS * 每请求 1 次推荐调用 = 2000 次下游调用/秒

再看延迟。如果平均请求耗时 200ms,根据 Little’s Law,系统同时在处理的请求数大约是:

并发数 = 吞吐量 * 平均响应时间 2000 * 0.2 = 400

这还只是入口服务的并发。下游数据库连接、HTTP 连接、线程池、协程数量、队列长度,都要围绕这个量级设计。

如果入口服务部署 8 个实例,每个实例平均 250 QPS。每个实例数据库连接池设 50,看起来不大,但总连接数是:

8 * 50 = 400

如果数据库最大连接数只有 300,服务还没跑满就已经把数据库连接打爆了。

高并发第一步不是写代码,是把 QPS、延迟、并发、连接、下游容量算清楚。

让 AI 做高并发设计时,可以先让它算:

先不要给优化建议。 请基于目标 QPS、P95 延迟、每请求下游调用次数、实例数、连接池大小,估算: - 入口并发数。 - 每个实例平均 QPS。 - 数据库总连接数。 - 下游服务调用 QPS。 - 哪个资源最可能先成为瓶颈。

不会算容量,谈优化就是凭感觉。

连接池是乘法,不是配置项

连接池经常被当成一个随手调大的配置。

比如:

DB_POOL_SIZE=50 HTTP_POOL_SIZE=100 WORKER_COUNT=8

问题在于,这些数字会乘起来。

如果每个进程有一个数据库连接池,4 台机器,每台 4 个进程,每个进程 50 条连接,总连接数就是:

4 * 4 * 50 = 800

这还不包括后台 worker、管理任务、迁移脚本、临时查询工具。数据库可能根本承受不了。

HTTP 连接池也一样。入口服务调用推荐服务,每个实例开 100 个连接,20 个实例就是 2000 个连接。推荐服务能不能承受?它自己的线程池、连接池和下游又会怎样?

连接池设计要看三件事:

  • 上游会产生多少并发。
  • 当前服务希望最多放多少请求进入下游。
  • 下游真实容量是多少。

连接池不是“越大越不容易卡”。连接池太大,会把压力完整传给下游;连接池太小,会在本服务排队。排在哪里,是设计选择。

更稳的做法是让连接池成为保护下游的闸门:

入口服务每实例最多允许 20 个并发 DB 查询。 超过连接池能力的请求在本地排队,排队超过 50ms 直接失败或降级。

这比无脑调大连接池更可靠。

让 AI 配连接池时,可以要求:

不要只给单实例连接池大小。 请按实例数、进程数、worker 数计算全局连接数。 说明下游数据库或服务的最大可承受连接数。 给出连接等待超时和降级策略。

连接池配置不算全局乘法,就很容易线上事故。

批量能救系统,也能拖垮系统

高并发系统里,逐条处理很贵。批量可以显著降低数据库往返、网络开销和事务开销。

比如写日志或历史记录。逐条写:

for record in records: await db.insert(record)

批量写:

for batch in chunks(records, 1000): await db.insert_many(batch)

批量能把 1000 次网络往返变成 1 次。但批量不是越大越好。批太大,单次事务变长,锁时间变长,失败重试成本变高,内存占用也变高。

批量参数通常要从几个维度定:

参数影响
batch_size吞吐、事务时长、单次失败成本
flush_interval延迟和吞吐之间的平衡
queue_size内存占用和反压触发点
max_retry下游抖动时的恢复能力
timeout防止批处理长期卡住

一个常见短期缓解方案,是把批量从100/1s调到1000-5000/5s。这会降低数据库写入频率,但会增加数据可见延迟。适不适合,要看这批数据是业务状态还是观测明细。

业务状态不能随便延迟;日志、遥测、历史明细可以接受几秒延迟。

让 AI 优化批处理时,要明确:

请区分业务状态写入和观测明细写入。 业务状态必须可靠、及时落库。 观测明细可以进入内存队列,由后台 task 按 1000-5000 条或 5 秒批量写入。 队列满时低优先级 DEBUG 记录可以采样或丢弃摘要。

批量的本质是用延迟换吞吐。这个交换必须说清楚。

限流是系统的保险丝

高并发系统不能假设所有请求都能被处理。

限流不是产品体验差,而是保护系统最基本的手段。没有限流,突发流量会继续进入服务、占满线程、打满连接、压垮数据库,最后所有用户都不可用。

常见限流维度:

  • 全局限流:整个服务最大 QPS。
  • 用户限流:单个用户最大请求频率。
  • IP 限流:防止异常来源。
  • 接口限流:重接口单独控制。
  • 下游限流:保护数据库、第三方服务、模型服务。
  • 任务限流:每个用户最多 pending/running 多少任务。

一个简单 token bucket 可以这样理解:

class TokenBucket: def __init__(self, capacity: int, refill_per_second: int): self.capacity = capacity self.tokens = capacity self.refill_per_second = refill_per_second self.last_refill = time.monotonic() def allow(self) -> bool: now = time.monotonic() elapsed = now - self.last_refill self.tokens = min( self.capacity, self.tokens + elapsed * self.refill_per_second, ) self.last_refill = now if self.tokens >= 1: self.tokens -= 1 return True return False

这是本地版本。多实例需要集中式计数、分片计数或网关限流。精度、性能和一致性之间要取舍。

限流响应也要是契约:

{ "error": { "code": "RATE_LIMITED", "message": "请求过于频繁,请稍后再试", "retryable": true }, "retry_after_seconds": 3 }

让 AI 写限流时,可以要求:

请给出限流维度:全局、用户、接口、下游。 说明本地限流和分布式限流的取舍。 429 响应必须包含 retry_after_seconds。 限流命中要打指标,避免只在日志里出现。

限流不是代码片段,是服务承诺的一部分。

降级不是失败,是保核心

降级的目标不是“少做点功能”,而是保住核心路径。

比如一个商品详情页包含:

  • 商品基本信息。
  • 价格和库存。
  • 推荐商品。
  • 评论摘要。
  • 浏览历史。
  • 广告位。

高峰期或下游故障时,商品基本信息、价格、库存是核心;推荐、评论摘要、浏览历史、广告可以降级。

降级策略可以很具体:

模块降级方式
推荐商品返回空列表或缓存旧结果
评论摘要返回旧缓存,标记可能延迟
浏览历史跳过写入,进入低优队列
广告位返回默认广告或空
库存不能降级为假数据,只能短超时失败

AI 生成接口时,常常把所有下游调用都放在一个gather里,任何一个失败就整体失败。更好的写法是区分核心和非核心:

product = await get_product(product_id) price = await get_price(product_id) inventory = await get_inventory(product_id) recommendations = await best_effort( get_recommendations(product_id), fallback=[], timeout=0.05, )

best_effort调用失败不能影响主响应,但必须记录指标。降级不是悄悄吞错误。

让 AI 做降级设计时,可以要求:

请把下游依赖分成核心依赖和可降级依赖。 核心依赖失败时接口失败;可降级依赖失败时返回 fallback。 每个降级都必须记录指标和 trace,不允许静默吞掉。

降级设计的关键,是提前决定什么能丢,什么不能丢。

热点比平均流量危险

平均 QPS 很容易骗人。

全站 2000 QPS 不一定危险;如果其中 1500 QPS 都打到同一个商品、同一个用户、同一个缓存 key、同一行数据库,那就是热点。

热点常见形式:

  • 热门商品详情。
  • 大 V 用户主页。
  • 秒杀库存。
  • 单个配置 key。
  • 同一个搜索词。
  • 同一个租户的大批任务。

热点问题不能只靠扩容,因为热点可能集中在同一个 key 或同一行。

几种常见处理:

热点类型处理方式
热点读多级缓存、本地缓存、提前刷新、随机 TTL
热点写分片计数、异步聚合、队列串行化
热点库存原子扣减、令牌预占、分段库存
热点用户用户维度限流、隔离队列
热点租户租户级配额、独立 worker 池

比如点赞计数。每次点赞都直接更新同一行:

update posts set like_count = like_count + 1 where id = :post_id;

热门文章下,这一行会变成写热点。可以改成分片计数:

create table post_like_counters ( post_id uuid not null, shard_id integer not null, count bigint not null, primary key (post_id, shard_id) );

写入时按用户或随机 shard 更新:

update post_like_counters set count = count + 1 where post_id = :post_id and shard_id = :shard_id;

读取时聚合多个 shard,或者后台定时汇总到主表。

让 AI 设计热点方案时,要明确:

请不要只按平均 QPS 分析。 列出可能的热点 key、热点行、热点用户和热点租户。 针对热点读和热点写分别给出隔离方案。

热点才是高并发里最容易把系统打穿的地方。

雪崩通常是连锁反应

服务雪崩很少是一个点突然坏掉。更多是连锁反应。

一个典型链条:

缓存过期 -> 大量请求打到数据库 -> 数据库变慢 -> 请求线程堆积 -> 连接池耗尽 -> 上游超时重试 -> 流量翻倍 -> 更多服务被拖慢

这里真正危险的是重试和排队。下游已经慢了,上游继续重试,相当于往火上加流量。

防雪崩要有几层保护:

  • 缓存 TTL 随机抖动,避免同时过期。
  • 热点 key 提前刷新。
  • 下游调用设置短超时。
  • 重试次数少,并加退避。
  • 连接池等待也要有超时。
  • 限流和熔断保护下游。
  • 非核心依赖可降级。
  • 队列长度超过阈值触发反压。

熔断可以理解为:如果某个下游短时间内连续失败,就暂时不再调用它,直接走 fallback 或快速失败,给下游恢复时间。

让 AI 写重试逻辑时,不要只说“失败重试”:

重试只针对临时错误。 最多重试 2 次,使用指数退避和随机抖动。 总耗时不能超过接口 timeout budget。 如果下游连续失败率超过阈值,进入熔断,短时间快速失败或降级。

重试必须有预算。没有预算的重试,就是雪崩放大器。

Timeout budget 要分配

很多接口只设置一个总超时,比如 3 秒。但它内部可能调用多个下游,每个下游又有自己的超时。如果每个下游都设 3 秒,总请求就不可控。

更好的做法是分配 timeout budget。

假设接口目标 P95 200ms,可以这样分:

阶段预算
鉴权和参数校验10ms
主数据库查询50ms
库存服务40ms
推荐服务30ms,可降级
序列化和返回20ms
预留50ms

推荐服务如果 30ms 没返回,就降级,而不是等到 200ms 全部耗完。

在代码里要把超时传下去:

async def get_detail(product_id: str, deadline: Deadline): product = await with_timeout( get_product(product_id), deadline.remaining(max_ms=50), ) recommendations = await best_effort( get_recommendations(product_id), timeout_ms=min(30, deadline.remaining_ms()), fallback=[], ) return build_response(product, recommendations)

这是示意代码,重点是每个下游都有预算,不允许无限等待。

让 AI 写高并发接口时,可以要求:

请给出 timeout budget。 每个下游调用必须有独立超时。 所有重试总耗时不能超过总预算。 可降级依赖超过预算直接 fallback。

延迟不是最后测出来才管,应该在设计时分配。

容量评估要有压测闭环

高并发设计不能只停在纸面上。必须压测。

压测不是只看 QPS。至少要看:

  • P50/P95/P99 延迟。
  • 错误率。
  • CPU 使用率。
  • 内存和 GC。
  • 数据库连接数。
  • 数据库慢查询。
  • Redis 命中率和延迟。
  • 队列长度和消费延迟。
  • 下游调用耗时。
  • 限流、降级、熔断次数。

压测也不能只跑 happy path。要压:

  • 正常流量。
  • 峰值流量。
  • 突刺流量。
  • 热点 key。
  • 下游慢。
  • 缓存失效。
  • 部分实例重启。
  • 队列堆积。

一个压测结论应该长这样:

目标:2000 QPS,P95 < 200ms,错误率 < 0.1% 结果: - 1500 QPS 时 P95=130ms,DB CPU=55% - 2000 QPS 时 P95=210ms,DB 连接等待开始升高 - 热点 key 场景下 Redis 命中率 99%,本地缓存生效 - 推荐服务慢 500ms 时,详情接口降级率 18%,整体 P95=170ms 瓶颈:数据库连接等待和订单查询索引 处理:连接池从 50 降到 30,订单查询增加复合索引,推荐服务 50ms 超时降级

这才是可执行的容量结论。

让 AI 帮你做压测方案时,可以要求:

请给出压测计划,不只写工具命令。 包含目标、场景、流量模型、观测指标、瓶颈判断标准和回滚策略。

工具能打流量,方案决定你能不能读懂结果。

一份给 AI 的高并发提示词

让 AI 设计高并发方案时,可以先用这段:

先不要写代码。 请为这个服务做高并发设计: 1. 给出目标 QPS、P95/P99 延迟、错误率目标和关键业务路径。 2. 估算入口并发数、实例数、每实例 QPS、数据库总连接数、下游调用 QPS。 3. 列出所有连接池、线程池、队列和 worker,并计算全局容量。 4. 分析哪些下游是核心依赖,哪些可以降级。 5. 给出限流维度:全局、用户、接口、下游、任务。 6. 分析热点 key、热点行、热点用户、热点租户,并给出隔离方案。 7. 设计超时、重试、熔断和 timeout budget。 8. 说明缓存失效、下游慢、队列堆积、部分实例重启时如何保护系统。 9. 给出压测场景、指标、瓶颈判断和容量结论格式。 10. 最后再给代码和配置修改建议。

这段 prompt 会迫使 AI 先算容量、画边界、找瓶颈,再写实现。

卷二收个尾

卷二讲的是“把功能做成能扛流量的服务”。接口契约、数据库约束、缓存队列、高并发设计,其实都在讲同一件事:系统不是单个函数,系统是一组有边界的承诺。

写代码前,问自己几句话:

  • 目标 QPS 和延迟是多少?
  • 入口并发会变成多少下游调用?
  • 连接池总数有没有算全局乘法?
  • 哪些请求必须成功,哪些可以降级?
  • 热点 key 和热点行在哪里?
  • 重试会不会放大流量?
  • 队列满了谁负责拒绝?
  • 下游慢了,压力会停在哪里?
  • 压测能不能复现这些场景?

AI 可以写出很快的局部代码,但高并发系统靠的不是局部聪明,而是容量、边界和失败路径都被提前想过。

机器可以加,边界不能省。