写 C# 调 WebAPI,第一次能跑通不难,难的是接进项目以后:超时该设几秒、并发一上来就报限流、流式返回中途断掉、出问题只能靠猜。这篇不重复讲协议和客户端配置,只讲工程侧怎么写:一次请求怎么发、错误怎么分类、重试和降级放哪一层、日志该记什么,以及调不通时按什么顺序排查。
写 C# 调 WebAPI,第一次跑通其实不难,难的是接进项目以后:超时设几秒合适、并发一上来就报限流、流式返回中途断掉、出问题只能靠猜。这些都不是协议层的问题,而是工程层的问题。
这篇不重复讲 HttpClient 怎么 new、DI 怎么配,那些官方文档和站内已有篇目都写过。这里只讲一件事:一次调用怎么写成能长期活在项目里的方法——请求怎么构造、超时并发放哪、重试降级归谁管、日志记什么,以及调不通时按什么顺序排查。
把一次调用写成可复制的方法
最常见也最伤人的写法,是在业务方法里直接 new HttpClient()。短时间请求量大一点,连接池就耗尽,报出来的错还很难定位。正确做法是通过 IHttpClientFactory 拿实例,由容器管理生命周期。但也要留意:长生命周期的静态实例虽然省事,仍会碰到 DNS 变更不生效、默认请求头被别人改脏的问题,多线程共用时尤其明显。
- URL 拼接、鉴权位置、Body 格式三件事先在纸上写清楚:鉴权放 Header 还是查询串、Body 是 JSON 还是表单,别一边写一边改
- 序列化统一走一种:System.Text.Json 或 Newtonsoft 选一个,团队里混用迟早出现日期格式和空值处理不一致的问题
- POST 用 HttpRequestMessage 显式设方法、头和内容,不要用 GetAsync 拼字符串绕过语义
- 读响应先判 IsSuccessStatusCode,成功再按对象反序列化,失败保留原文,否则报错信息会被序列化异常盖掉
把这套流程封装成一个泛型方法:传 URL、HTTP 方法、请求模型,内部负责发请求、拿字符串、转成目标类型。业务代码只关心这几个参数,后面加超时、重试、日志才有统一入口。没有这层封装,每加一个能力都要在几十个调用点改一遍。
| 环节 | 容易出错的做法 | 建议改成 |
|---|---|---|
| 客户端获取 | 方法内 new HttpClient() | IHttpClientFactory 注入或命名客户端 |
| 请求构造 | GetAsync 拼查询串做写操作 | HttpRequestMessage 显式设方法与内容 |
| 响应处理 | 直接反序列化不看状态码 | 先判 IsSuccessStatusCode 再分支 |
| 错误处理 | catch 住全部异常统一吞掉 | 按状态码与异常类型分类上抛 |
超时、并发和取消,别等出事再补
超时要分两层理解:HttpClient.Timeout 是这次请求的整体上限,CancellationToken 是调用方主动喊停。两者同时存在时,谁先触发谁生效。只设其中一个都不完整——只设 Timeout,上层业务想提前放弃时请求还在跑;只传 Token,忘了设上限,对方不响应就能一直挂着。
具体设几秒,取决于你自己链路的延迟基线,不要抄别人的数字。先到 /tools/speed-test 量一遍真实往返时间,再留出合理余量。同一套参数换到不同地区、不同网络出口,结果可能差很多。
- 先量基线用 /tools/speed-test 对目标端点采样,记录正常延迟区间,而不是拍一个整数当超时
- 设两层保护HttpClient.Timeout 设整体上限,方法签名统一带 CancellationToken,由上层决定何时取消
- 限在途数量用 SemaphoreSlim 或 Parallel.ForEachAsync 限制并发数,不要靠 for 循环里堆 Task.Run
- 验证效果压一轮观察是否出现限流类状态码,出现就下调并发,上限口径可到 /ai-nav/category/ai-api 比对
这里有个例外:如果调用的是自己内网、延迟稳定可控的服务,并发可以放得比调外部接口宽一些。外网链路受运营商和对端限流影响,参数就得保守。这个判断不能反过来用。
重试、降级和幂等:哪些能重,哪些不能
很多人一遇到失败就加循环重试,结果是参数写错了也重试三次,白白浪费时间还掩盖了真正的问题。正确顺序是先分类,再决定要不要重试。
| 失败类型 | 能否重试 | 处理方式 |
|---|---|---|
| 连接失败、网关超时 | 可以 | 带退避与抖动,限次数 |
| 限流类状态码 | 可以 | 延长间隔,必要时降并发 |
| 鉴权失败、参数错误 | 不可以 | 直接上抛,重试无意义 |
| 资源不存在 | 不可以 | 检查目标是否正确 |
| 扣款、下单等写操作 | 默认不重 | 带幂等键才考虑自动重试 |
退避间隔要递增并加随机抖动,避免多个实例同一时刻一起重试把对端再打挂。次数要有上限,超过了就交给上层处理或走降级。降级的顺序建议是:换备用端点或备用模型、缩短要求的回复长度、返回缓存或兜底文案。每次降级都要打点记录,否则线上悄悄降级运行,过很久才发现。
- 重试只放在幂等请求上,写操作要么带幂等键,要么不自动重试
- 退避间隔递增加抖动,设最大次数,避免重试风暴
- 降级路径要打点,能被监控看见,不能只在代码里默默走掉
- 错误分类沿用本区已有篇目的口径,别自己另起一套命名
日志记什么,以及调不通时的排查顺序
日志不是记得越多越好。每条请求至少要能回答三件事:发了什么、花了多久、结果算哪一类。具体字段可以固定成:时间、目标、方法、耗时、状态码、重试次数、错误分类、请求标识。请求正文只在脱敏后按需记录,Key 和用户数据绝不能整段进日志。
- 第一步查连通先确认域名解析和网络出口正常,curl 或简单脚本能通再说别的,这一步不通后面都是白费
- 第二步看状态码按状态码归类,鉴权类、限流类、网关类各有各的处理方向,别混在一起猜
- 第三步对字段检查请求体字段名、类型、必填项是否和对端文档一致,大小写和嵌套层级最容易出错
- 第四步才怀疑服务端前三步都排除后,再联系对端或换备用端点验证,避免一上来就甩锅
还有个容易踩的坑:流式接口断流和普通请求失败不是同一条链路,排查手段也不同,别拿普通请求的经验套上去。这个顺序做成项目里的一页清单,新同事第一次排错就照着走,比口头传授靠谱。例外情况是:如果错误信息本身就明确指向参数问题,可以直接跳到第三步,不必死板走完四步。
下一步想知道自己链路的真实延迟和限流表现,用真实端点跑一遍再定参数,比抄别人的配置靠谱得多。→常见问题
HttpClient 该用单例还是每次都 new?
都不要。用 IHttpClientFactory 按名取实例,它帮你管连接池和 DNS 刷新;直接 new 多了会耗尽端口,做成一个静态全局实例则容易因为改默认头和超时而互相影响。核心是把创建收敛到一处,由容器统一提供。
接口偶尔超时,是不是把超时时间调大就行?
调大只是把问题往后推。先区分是网络慢、服务端慢还是并发太高排队。用日志里的耗时分布看是普遍慢还是偶发尖刺,普遍慢就该改小请求或换端点,偶发尖刺优先加退避重试和限流,而不是无限加时间。
重试几次比较合适?
从两三次起步,配合递增间隔和上限总时长,别写成无脑循环。判断依据有三个:这个接口是否幂等、上层能不能等、失败重试要付多少调用成本。任何一条不满足就减少次数甚至不重试,改为直接返回错误让业务侧决定。