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

📅 2026/7/18 14:53:59
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_SIZE50 HTTP_POOL_SIZE100 WORKER_COUNT8问题在于这些数字会乘起来。如果每个进程有一个数据库连接池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[], timeout0.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_ms50), ) recommendations await best_effort( get_recommendations(product_id), timeout_msmin(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 QPSP95 200ms错误率 0.1% 结果 - 1500 QPS 时 P95130msDB CPU55% - 2000 QPS 时 P95210msDB 连接等待开始升高 - 热点 key 场景下 Redis 命中率 99%本地缓存生效 - 推荐服务慢 500ms 时详情接口降级率 18%整体 P95170ms 瓶颈数据库连接等待和订单查询索引 处理连接池从 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 可以写出很快的局部代码但高并发系统靠的不是局部聪明而是容量、边界和失败路径都被提前想过。机器可以加边界不能省。