本地跑得好好的接口,一上量就开始慢、开始报错——线程池被占满、数据库连接不够、下游模型接口被限流,问题往往不在你写的那几行业务代码。这篇按「先量、再算、后调」的顺序,把 ASP.NET Core 高并发系统要盯的几个环节拆成清单,方便你对着自己的服务逐条核对,也方便出问题时按顺序定位。
本地跑得好好的接口,一上量就慢、就报错。多数时候问题不在你写的那几行业务逻辑,而在请求链路里某个会排队的地方:线程池被阻塞式调用占满、数据库连接被慢查询占着不还、下游接口把并发顶回去。所以这篇不讲概念,只给一条能照着走的顺序——先量、再算、后调,出问题时也按这个顺序定位。
下面四个环节可以当成自查清单,对着自己的服务逐条核对。需要提醒一句:任何调参结论都只在当时的容量和依赖状态下成立,依赖换了、流量结构变了,就得不重新量,别把上个月的参数当永久答案。
第一步:把基线量出来,别凭感觉调参
很多人一遇到慢就开始改配置、加缓存、扩线程池,改完不知道有没有用,因为压根没有改之前的数字。基线要解决的第一个问题是:瓶颈到底在哪一段。是 Kestrel 接收就顶住了,还是应用内计算慢,还是数据库和缓存的 IO 慢,还是下游第三方接口让你一直等。这四种,对应完全不同的改法。
- 先分清瓶颈段:接收、应用计算、数据库或缓存 IO、下游第三方等待,四类问题不要混着改。
- 同一个压测脚本跑三轮:单请求延迟、逐步加压直到开始出错的并发点、出错时的错误类型分布。
- 关键指标必须进日志:请求耗时分布、内部队列等待时间、连接池占用、GC 暂停时长。
- 基线要在接近生产的配置下测,本地单机的数据只能看趋势,不能当容量结论。
- 外部依赖的延迟先用站内 /tools/speed-test 量一遍,作为对照基线,别把它当成你服务的性能。
三轮压测里最容易漏的是第二轮。单请求延迟好看,不代表到某个并发点不会突然崩;那个「从正常到出错的拐点」才是你要的容量参考。第三轮则告诉你崩的时候是超时为主还是连接被拒为主,两者的排查方向完全不同。
第二步:顺着请求链路找会排队的地方
ASP.NET Core 本身是异步模型,能扛多少并发,往往不取决于框架,而取决于你把哪些环节做成了同步阻塞。同步 IO、全局锁、Task.Result、单例里维护的可变状态,这几个是热点路径上的常见堵点。判断方法很直接:看这个环节在等的时候,线程是释放了还是被占着。
| 要盯的地方 | 正常表现 | 可疑信号 |
|---|---|---|
| 异步链路 | 从入口到下游一路 async | 中间出现 .Result 或 .Wait() |
| 锁的使用 | 锁内只有内存操作 | 持锁期间做 IO 或调用下游 |
| 连接池 | 占用随流量涨落、能回收 | 长期高位、借出后迟迟不还 |
| 线程池 | 阻塞任务少、队列平稳 | 队列持续增长、线程数顶到上限 |
| 下游调用 | 有独立超时与隔离 | 一个下游慢就拖垮整条链路 |
线程池和连接池是最容易被打满的两个池子,但原因不同:前者通常是被阻塞式调用耗掉,后者通常是被慢查询或下游超时占住。定位思路不是加机器,而是找出那个借了不还的东西——是一段卡住的 SQL,还是一个没设超时的外部调用。
限流和熔断建议放在入口和出口两处。入口按调用方分档,防止一个客户端把你的容量吃光;出口针对慢下游,防止你把它的问题变成自己的问题。具体分档口径、额度和退避策略会随依赖方调整,按当时的数字去 /official-api 与 /usage 核对后再定,别照抄旧配置。
第三步:超时、降级与重试的取舍
这一节的核心判断只有一句话:每个外部依赖都得有自己的超时,并且失败时你得有地方可去。把请求的默认超时一路带到底,等于让最慢的那个依赖决定你所有接口的表现;而一个没有降级路径的接口,本质上是把可用性外包给了别人。
- 给每个依赖设独立超时按该依赖自己的延迟分布设阈值,超过就快速失败,不要沿用调用方传下来的超时。验证方式:人为注入延迟,确认超时按预期触发而不是拖住整条链路。
- 只对幂等操作开重试重试必须带次数上限和随机抖动。验证方式:压测时观察下游侧收到的请求量,如果重试把下游打得更慢,说明策略要收。
- 明确降级出口可选项是返回缓存快照、返回简化结果、或直接拒绝并给出可读错误码。验证方式:关掉依赖,看接口是否还能给出稳定且可预期的响应。
- 用背压替代无脑扩容把并发卡在一个数,超出的排队或拒绝。验证方式:观察排队长度是否可控,以及被拒绝的流量是否有明确返回,而不是静默超时。
第四步:上线后怎么确认它真的扛住了
压测通过不等于生产通过。上量之后要盯的是四个角:成功率、尾延迟、排队长度,以及被限流和降级的比例。只看平均延迟最容易被骗——平均值好看、尾部拖长,用户体验照样是挂的,而且长尾往往就是最先出问题的信号。
- 盯四个角:成功率、P95 与 P99 延迟、排队长度、限流与降级占比。
- 日志保留可复盘字段:请求标识、调用方、依赖名、耗时分段、失败原因。
- 灰度先放一小部分流量,稳定一段时间再加,性能回归就回滚,别在线调参。
- 把复测排进迭代节奏,依赖的容量和价格都会变,定期去 /price-watch 与 /ai-status 看当时的数字。
- 每次复测都重新量一遍基线,旧结论不要直接复用。
如果排查到一半发现是下游服务本身不稳,那问题就不在你的应用层,可以参考站内的服务状态页确认当时的整体情况,再决定是自己降级还是切换路径。判断标准是:你的接口能否在下游异常时保持住成功率下限,能就继续用,不能就别把关键链路压在这一个依赖上。
下一步把你线上服务的四个角指标和当时的依赖额度对一遍,缺哪个补哪个,顺手把复测安排进最近的迭代。→常见问题
ASP.NET Core 高并发一定要用异步吗?
热点路径上基本要用。异步的价值是让线程在等 IO 时能被释放去处理别的请求,如果代码里到处是阻塞调用,异步的收益会被抵消。反例是纯 CPU 密集的计算,异步不解决问题,要靠拆分、缓存或加机器。
并发上不去,先加机器还是先改代码?
先定位再花钱。如果瓶颈在应用内的锁或同步阻塞,加机器只是把浪费摊薄;如果是数据库或下游容量不够,加应用实例还会加重下游压力。先量出排队发生在哪一段,再决定是扩容量、加缓存还是改调用方式。
限流的值设多少合适?
没有通用数。参考三个输入:压测出的稳定并发点、下游允许的调用额度、以及你能接受的排队时长。初值宁可保守,留出余量,上线后按被拒比例逐步上调,而不是一上来按峰值设。