ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

WebApi接口开发全流程:契约设计、高频性能优化与线上排查

WebApi接口开发全流程:契约设计、高频性能优化与线上排查 1. 接口开发前的整体思路与选型拆解接接口开发的活儿最容易出问题的从来不是写代码那一步而是动手之前没想清楚这组 WebApi 到底服务于谁。我这些年做接口开发踩得最狠的几个坑回头看几乎都能归到契约没定好和选型拍脑袋这两件事上。所以这篇笔记我按自己真实的推进顺序来写从需求拆解、技术选型、契约设计一路讲到高频调用场景的性能优化、发布部署和线上排查尽量把每一步背后的理由讲透方便你直接对照自己的项目抄作业。先说结论性的判断WebApi 接口开发的核心矛盾永远是消费方的诉求和服务方的成本之间的平衡。消费方要的是简单、稳定、快服务方要的是可维护、可观测、扛得住。很多团队一上来就纠结用 REST 还是 RPC、用哪个框架、要不要上网关其实这些问题的答案都藏在谁在调你的接口里。把消费方画像画清楚后面八成的选型纠结会自动消失。下面我把这套拆解思路展开讲。1.1 先搞清楚接口是给谁用的三类消费方决定设计走向我把接口消费方粗略分成三类每一类对应的设计重心完全不同混着做必翻车。第一类是前端页面或移动端。这类调用特点是请求频率中等、对响应体结构敏感、经常需要聚合数据。给它们设计接口时字段命名要统一空值处理要明确最好一次性把页面需要的字段都给到减少往返次数。我见过一个列表页调了七个接口首屏要等两秒多最后合并成一个聚合接口直接掉到三百毫秒。第二类是内部服务之间的调用。这类更看重吞吐和延迟字段能省就省往往用更紧凑的序列化格式接口粒度也会更粗一次传一批数据比一条一条传划算得多。这时候别拘泥于面向前端的 REST 风格内部调用追求的是效率。第三类就是高频柜台类调用。这个场景我专门花了一整块篇幅在后面讲它的典型特征是调用方数量少但单方调用极其密集同一个接口在极短时间内被反复打到而且对延迟的容忍度非常低。给它做设计思路跟前两类几乎是反过来的——不是考虑怎么把接口设计得优雅而是考虑怎么让它少做无用功。提示动手写第一行代码之前先拿张纸写下这三类消费方各自的数量级、峰值 QPS、可接受的 P99 延迟。这三个数字会直接决定你后面所有的技术决策。1.2 技术栈选型我为什么最后落在 ASP.NET Core Web API框架选择这事没有银弹但有几个判断维度是通用的团队熟悉度、生态成熟度、性能天花板、部署便利性。我主力用 ASP.NET Core 做 WebApi理由很实在。一是性能底座够用。.NET 这几代在 HTTP 处理链路上做了大量优化配合 Kestrel 直接对外或放在反向代理后面单机撑几千 QPS 的常规接口没什么压力。对于大多数业务量级的接口来说性能从来不是瓶颈瓶颈往往在数据库和序列化上。二是内置能力齐全。依赖注入、配置系统、日志抽象、中间件管道、模型绑定与验证、限流组件、健康检查这些东西都是框架自带的不需要你再去拼装一堆第三方库。接口开发最怕的就是胶水代码太多框架自带能省下大量维护成本。三是类型系统帮大忙。C# 的强类型配合可空引用类型能把不少接口契约层面的错误在编译期就拦住。线上接口因为字段类型对不上炸掉这种事故用强类型语言能规避掉一大半。当然也有代价。C# 生态在某些特定中间件上不如其他语言丰富团队如果全是别的语言背景强行切换的沟通成本也不低。所以我的建议是如果团队已经有成熟的技术栈优先沿用如果是新项目且没有强绑定ASP.NET Core 做 WebApi 是个稳妥选择。1.3 项目目录与分层结构别一上来就往 Controller 里堆新手最容易犯的错是把所有逻辑塞进 Controller。一个方法从参数校验、业务计算、数据库访问到组装返回写了两百多行后面想改一个字都心惊胆战。我现在的分层习惯是这样的Controllers只做协议转换收参数、调服务、返结果单个方法控制在二十行以内。Services业务逻辑主体接口和实现分离方便单测和替换。Repositories数据访问屏蔽具体存储细节。Models / Dtos请求响应模型和领域模型分开别把数据库实体直接当接口返回值。Middlewares横切关注点异常处理、日志、限流、鉴权都放这里。DTO 和实体分离这步千万别省。我早期图省事直接把数据库实体序列化返回结果数据库加了个内部字段接口响应体跟着变了前端直接崩。后来老老实实加一层 DTO 映射虽然多写点代码但接口对外就稳定了。// 一个典型的瘦 Controller [ApiController] [Route(api/v1/[controller])] public class OrdersController : ControllerBase { private readonly IOrderService _service; public OrdersController(IOrderService service) _service service; [HttpGet({id:long})] public async TaskActionResultOrderDto Get(long id, CancellationToken ct) { var order await _service.GetAsync(id, ct); return order is null ? NotFound() : Ok(order); } }这段代码里有几个细节值得说CancellationToken一路透传下去客户端断开连接时能及时释放资源路由里带了v1版本前缀为后续升级留后路返回类型用ActionResultT既能返回数据也能返回状态码。2. 接口契约设计那些上线前就该定死的东西接口契约一旦发布改动成本就指数级上升。所以这一章我讲的全是发版前必须敲定的内容。契约设计的核心原则就一句让调用方猜不到的地方越少越好。字段命名、错误码、分页参数、时间格式每一个模糊点后面都会变成沟通成本和线上事故。2.1 URL 与动作设计REST 不是万能药REST 风格很好但别把它当教条。资源型操作比如订单、用户、商品用名词复数加 HTTP 动词语义清晰又好维护GET /api/v1/orders 查询列表 GET /api/v1/orders/{id} 查询单个 POST /api/v1/orders 创建 PUT /api/v1/orders/{id} 全量更新 PATCH /api/v1/orders/{id} 部分更新 DELETE /api/v1/orders/{id} 删除但遇到动作型接口硬套 REST 会很别扭。比如提交订单审核通过重算余额这类操作本质是动了一个过程而不是一个资源。这时候我倾向于直接用动词比如POST /api/v1/orders/{id}/submit可读性反而更好。强扭的 REST 只会让调用方一脸问号。还有一个网上讨论很多的细节筛选条件别塞进路径。/orders/status/paid/date/2024这种写法看着很 REST实际维护起来很痛苦。查询参数就是干这个的/orders?statuspaiddate2024-01-01清晰、可组合、方便加字段。2.2 请求与响应体的统一约定统一约定能省掉大量重复解释。我现在的默认约定是这几条项目约定理由字段命名小驼峰 camelCase前端友好多数序列化器默认行为时间格式ISO 8601 带时区避免时区歧义金额字段整型最小单位或定点字符串绕开浮点精度问题空值明确区分 null 和空集合减少前端判空逻辑布尔字段用 is/has 前缀语义自解释金额这块我必须多说一句。永远不要用浮点数表示金额。我见过一次对账差一分钱的问题排查了整整两天最后发现是 double 累加误差。现在我的做法是统一用分做单位的整数或者金额字符串序列化成 decimal。响应体我给一个通用的包装结构{ code: 0, message: ok, data: { }, traceId: a1b2c3d4 }traceId这个字段看似不起眼线上排查时是救命的。用户报下单失败客服把 traceId 给我我 grep 日志一秒定位。没有它就得靠时间戳猜效率差十倍。注意包装结构会带来一层解包成本对于高频内部调用可以考虑省略直接返回裸数据体用 HTTP 状态码表达结果。这两套策略不要在一个项目里混用。2.3 参数校验与错误码体系参数校验要做在入口处别让它漏到业务层。ASP.NET Core 的模型验证配合注解能在进入 Controller 之前就拦掉大部分非法请求public class CreateOrderRequest { [Required, StringLength(64)] public string OrderNo { get; set; } string.Empty; [Range(1, 1000000)] public int Amount { get; set; } [Required] public string Channel { get; set; } string.Empty; }配合[ApiController]特性验证失败会自动返回 400不用你手动写 if。但默认的错误信息格式不够友好我一般会自定义一个InvalidModelStateResponseFactory把验证错误统一整理成自己的错误结构。错误码体系我坚持分层设计HTTP 状态码表达协议层的语义400 参数错、401 未认证、403 无权限、404 不存在、429 限流、500 服务端异常业务错误码表达具体原因。两者结合调用方既能做通用处理也能做精细分支。错误码规划上我建议按模块分段编号比如 10xxx 是订单20xxx 是支付30xxx 是账户。留足扩展空间别从 1 开始连续编后面加模块会很难受。2.4 分页、排序、过滤的通用写法这三个是列表接口的老三样也是最容易被重复造轮子的地方。我的做法是抽一个基类查询参数public abstract class PagedQuery { private int _pageSize 20; public int Page { get; set; } 1; public int PageSize { get _pageSize; set _pageSize value is 1 or 200 ? 20 : value; } public string? SortBy { get; set; } public bool Desc { get; set; } true; }这里有个关键防护PageSize 必须设上限。我吃过大亏有次调用方传了pageSize1000000数据库直接把内存打满服务雪崩。现在无论谁传多大我都强制夹到 200 以内。深度分页也是个坑OFFSET 100000这种查询数据库会扫很久数据量大的表我会改成基于游标的翻页用上一页最后一条记录的 ID 做起点。排序字段别直接拼 SQL那是注入漏洞的重灾区。我会维护一个白名单只允许按预先定义好的字段排序private static readonly HashSetstring AllowedSorts new(StringComparer.OrdinalIgnoreCase) { createdAt, amount, status }; if (query.SortBy is not null !AllowedSorts.Contains(query.SortBy)) throw new BizException(ErrorCodes.InvalidSortField);3. 高频柜台场景下的性能与稳定性实操高频柜台类接口是接口开发里最考验功力的一类。它的调用模式跟普通业务接口完全不同用常规思路优化往往收效甚微。这一章我把这类场景的实战经验整理出来里面有些参数是我反复压测调出来的可以直接拿去参考。3.1 高频柜台接口的三个特征与应对策略先描述特征。第一调用极度密集。同一批调用方在毫秒级间隔内反复打同一个接口峰值 QPS 可能是平均值的几十倍。第二请求体高度相似。很多请求参数只差一两个字段大量重复计算和重复查询。第三延迟容忍度极低。慢个几百毫秒调用方的整体处理节奏就被拖垮。对应的三条应对策略减少每次请求的固定开销。这包括避免每次请求都新建昂贵的对象、避免在请求路径里做反射、避免重复读配置。我做过一次对比测试把一个每次请求都重新构造的序列化选项改成静态复用单请求耗时直接从 1.8ms 降到 0.6ms。固定开销在高频场景下会被放大成千上万倍。利用请求之间的相似性。相同参数的查询结果可以短暂缓存哪怕只缓存几百毫秒也能把后端的实际压力削掉一大截。缓存键要精心设计用参数哈希而不是完整参数拼接能省内存也能提速。把能挪出请求路径的事都挪出去。日志异步写、统计异步累加、非关键校验延迟执行。请求路径上只留必须同步完成的操作其余全部丢到后台队列。3.2 连接复用、序列化与数据访问层的优化这三个是高频接口的性能大头我逐个说。连接复用。对外部服务的调用一定要用连接池每次新建连接的握手开销在毫秒级高频场景下完全不能接受。HTTP 客户端要单例复用不要每次 new。数据库连接池的大小也有讲究不是越大越好。连接池大小的经验公式大概是最佳连接数 ≈ CPU 核数 × 2 磁盘数。四核机器配 8 到 12 个连接通常就够配到 100 个反而会因为上下文切换导致性能下降。这个数字你一定要自己压测确认不同业务的读写比例差异很大。序列化。JSON 序列化在高频场景里是实打实的开销。选择上优先用性能好的序列化器并做好配置复用。如果调用双方都是内部服务可以考虑更紧凑的二进制格式体积和解析速度都更优。但别为了省这点开销牺牲可读性内部调用改成二进制后排查问题会痛苦很多我一般只在真正的热点接口上用。数据访问层。原则是一次请求尽量只查一次数据库。避免在循环里查库这是最常见也最致命的性能杀手// 反例N1 查询 foreach (var id in ids) { var item await _repo.GetAsync(id, ct); // 每次一次数据库往返 result.Add(item); } // 正例批量查询后内存组装 var items await _repo.GetManyAsync(ids, ct); var map items.ToDictionary(x x.Id); var result ids.Select(id map.GetValueOrDefault(id)).ToList();这两段代码在 100 个 ID 的场景下耗时差距能达到两个数量级。索引也要针对性优化高频查询条件的字段一定要建索引并且用执行计划确认索引真的被用上了。3.3 限流、熔断与幂等设计高频场景下限流不是可选项而是必需品。没有限流一次异常流量就能把整个服务打挂。限流算法我用得最多的是令牌桶因为它允许一定程度的突发。参数怎么定有个简单算法设你希望接口稳定支撑 QPS 为 Q允许的突发倍数为 B那么桶容量设为Q × B令牌填充速率设为 Q。比如稳定支撑 1000 QPS、允许 3 倍突发那就是桶容量 3000每秒填充 1000 个令牌。builder.Services.AddRateLimiter(options { options.AddFixedWindowLimiter(counter, opt { opt.PermitLimit 3000; // 窗口内允许的请求数 opt.Window TimeSpan.FromSeconds(1); opt.QueueLimit 0; // 柜台类接口不排队直接拒绝更干脆 }); options.RejectionStatusCode 429; });提示高频柜台接口我建议QueueLimit设为 0超额直接返回 429。排队会让延迟不可控调用方拿到一个慢响应比拿到一个明确的拒绝更难受。熔断主要保护下游。当某个依赖服务的错误率超过阈值直接快速失败一段时间避免请求堆积把线程池耗尽。阈值我一般设成 50% 错误率、最少 20 次请求采样、熔断 30 秒后试探恢复。幂等是高频接口的生命线。网络抖动、超时重试在密集调用下太常见了没有幂等保护一次重试就可能造成重复扣减、重复下单。做法是让调用方带一个唯一请求号服务端用这个号做去重public async TaskResult HandleAsync(Request req, CancellationToken ct) { var key $idem:{req.RequestId}; if (!await _store.TrySetIfAbsentAsync(key, TimeSpan.FromMinutes(10))) return await _store.GetResultAsync(key); // 重复请求返回首次结果 var result await ProcessAsync(req, ct); await _store.SetResultAsync(key, result, TimeSpan.FromMinutes(10)); return result; }这里TrySetIfAbsent必须是原子操作用 Redis 的 SET NX 或者数据库唯一索引都行。窗口期设多久要结合业务我一般设 10 分钟足够覆盖所有合理重试。3.4 缓存策略与热点数据高频接口的缓存要分层做。本地内存缓存最快但多实例之间不一致适合放几乎不变的基础数据比如字典表、配置项。分布式缓存一致性好适合放有变化但变化不频繁的数据。缓存键的设计有个经验把变化维度都编进键里把无关维度排除在外。比如查询商品价格键里要有商品 ID 和时间粒度但不要带上调用方 IP 这种无关字段否则缓存命中率会被稀释得很低。过期时间怎么定我的原则是宁可短一点也不要脏数据。价格类数据我一般设 1 到 5 秒字典类数据可以设几十分钟。要更新数据时先更新数据库再删缓存不要反过来否则中间窗口会读到旧值写到缓存里。还有一个容易被忽略的点缓存击穿。某个热点键刚过期大量请求同时打到数据库。解决办法是加互斥锁只让一个请求去加载其余短暂等待var value await _cache.GetAsync(key); if (value is null) { await using var gate await _locker.AcquireAsync(key, TimeSpan.FromSeconds(5), ct); value await _cache.GetAsync(key); // 二次确认别人可能已经填好了 if (value is null) { value await LoadFromDbAsync(key, ct); await _cache.SetAsync(key, value, TimeSpan.FromSeconds(30)); } }这个锁内二次确认的写法很关键能避免大量重复加载。4. 认证鉴权、日志与线上可观测性接口上线之后最怕的就是出事查不到。这一章讲三件保障性的事怎么证明请求是合法的、怎么在出问题时快速定位、怎么让异常不把服务打崩。这三块做扎实日常运维会轻松非常多。4.1 鉴权方案选型与落地内部接口和开放接口的鉴权思路不一样。内部服务之间我一般用双向 TLS 或者服务间签名简单直接不用担心令牌泄露。面向外部调用方用带签名的访问凭证比较合适。签名方案的核心是调用方用密钥对请求内容加时间戳做签名服务端验证签名并检查时间戳在有效窗口内。这个设计能同时防篡改和防重放。时间戳窗口我一般设 5 分钟太短会因为时钟偏差误杀太长会放大重放风险。var raw ${method}\n{path}\n{timestamp}\n{bodyHash}; var expected ComputeHmac(secret, raw); if (!CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature))) return Unauthorized();注意这里用了FixedTimeEquals做定长比较避免时序攻击。普通字符串比较会因为提前返回泄露信息虽然在高频接口里这个风险相对低但既然有现成的方法没理由不用。不管用哪种方案密钥都要能轮换。我在项目里给每个调用方配两个可用密钥轮换时先下发新密钥、观察一段时间、再禁用旧密钥整个过程业务无感。4.2 日志分级与链路追踪日志的常见问题是两个极端要么什么都不打出事两眼一抹黑要么什么都打日志文件一天涨几十 G关键信息淹没在噪音里。我的分级原则Error需要人介入的问题比如数据库连不上、外部服务持续失败。Warning异常但可自愈的情况比如重试成功、缓存未命中率异常。Information关键业务节点比如请求进入、核心处理完成、返回结果。Debug排查用的细节生产默认关闭需要时动态打开。高频接口特别要注意日志的同步写入开销。我一般用异步批量写并且对高频路径只记录采样日志比如百分之一的请求打完整日志其余只记录耗时和状态码。这样既有数据可分析又不会把磁盘写满。链路追踪这块核心是给每个请求分配一个唯一 ID并一路透传到下游调用。日志里带上这个 ID排查时就能把一次请求经过的所有环节串起来。跨服务调用时通过请求头传递服务内部用异步上下文保存随用随取。4.3 全局异常处理与统一返回没做全局异常处理的服务一旦出异常调用方会收到一个格式完全不同的错误页前端解析直接炸。这个坑我在早期项目里踩过从此每个项目第一件事就是挂异常中间件。public class ExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILoggerExceptionMiddleware _logger; public ExceptionMiddleware(RequestDelegate next, ILoggerExceptionMiddleware logger) { _next next; _logger logger; } public async Task InvokeAsync(HttpContext ctx) { try { await _next(ctx); } catch (BizException ex) { _logger.LogWarning(ex, 业务异常 {Code}, ex.Code); ctx.Response.StatusCode 200; await ctx.Response.WriteAsJsonAsync(new { code ex.Code, message ex.Message }); } catch (Exception ex) { _logger.LogError(ex, 未处理异常); ctx.Response.StatusCode 500; await ctx.Response.WriteAsJsonAsync(new { code 50000, message 服务繁忙 }); } } }两个要点业务异常返回 200 加业务码系统异常返回 500 且绝对不能把堆栈返回给调用方那既是安全隐患也是对调用方的噪音。堆栈只进日志不进响应体。注意异常中间件要放在管道很靠前的位置否则前面中间件抛的异常它捕获不到。但又要放在日志中间件之后保证异常能被记录完整上下文。这个顺序调整起来很微妙建议画一次管道顺序图再动手。5. 从本地到生产的发布实操代码写完只是完成一半发布环节的坑一点不少。我见过太多项目在本地跑得好好的一上生产就各种状态码不对、连接超时、配置读不到。这一章把发布流程掰开讲包含配置管理、发布方式和上线后的验证动作。5.1 配置管理与多环境切换配置管理的核心原则配置跟代码分离敏感配置跟普通配置分离。我吃过把数据库密码硬编码进代码的亏后来改成从环境变量读本地开发用用户机密文件生产用环境变量或者配置中心。环境切换靠环境变量标记代码里通过对应的环境对象判断。我的习惯是配置三套开发、预发、生产。预发环境不能省它和生产用同一套配置结构、同样的数据规模专门用来验证部署流程。{ App: { DbConnection: , CacheEndpoint: , RateLimit: { PermitLimit: 1000, WindowSeconds: 1 } }, Logging: { LogLevel: { Default: Information, Microsoft: Warning } } }生产环境里把敏感项留空实际值从环境变量注入。这样配置文件即使被误提交到仓库也不会有明文密钥泄露。5.2 发布方式选择与部署步骤发布方式我按场景选。小规模服务直接发布到 Linux 上用 systemd 托管简单可靠。需要灰度、滚动更新的场景上容器编排。不管哪种方式发布过程必须可回滚这是底线。一个可回滚的发布流程大概是这样的构建产物打上版本号和提交哈希。备份当前运行的版本目录。解压新版本到新目录不要覆盖旧目录。切换软链接指向新目录。重启服务观察健康检查。健康检查失败则把软链接切回去重启。软链接切换这个技巧很好用切换和回滚都是瞬时操作不涉及文件复制风险极低。# 发布脚本的核心几行 dotnet publish -c Release -o /opt/app/releases/$VERSION ln -sfn /opt/app/releases/$VERSION /opt/app/current systemctl restart myapi sleep 5 curl -fs http://127.0.0.1:5000/health/ready || rollbackcurl -fs里的-f很关键让 HTTP 非 2xx 状态时返回非零退出码这样才能正确触发回滚。我第一次写这个脚本忘了加-f健康检查返回 500 也判定成功白白多跑了一轮故障。5.3 上线后的健康检查与压测健康检查要区分存活和就绪。存活检查只看进程还在不在别把数据库连接放进去否则数据库抖动会导致服务被反复重启。就绪检查才去验证依赖是否可用用来决定要不要把流量导进来。builder.Services.AddHealthChecks() .AddCheck(self, () HealthCheckResult.Healthy(), tags: new[] { live }) .AddNpgSql(connStr, name: db, tags: new[] { ready }); app.MapHealthChecks(/health/live, new HealthCheckOptions { Predicate r r.Tags.Contains(live) }); app.MapHealthChecks(/health/ready, new HealthCheckOptions { Predicate r r.Tags.Contains(ready) });压测这块我坚持上线前必压。工具用什么都行关键是压出三个数P50、P95、P99 延迟以及错误率随压力的变化曲线。柜台类接口我重点看 P99因为它决定了最慢的调用方体验。压测时记得把日志级别调高、关闭调试功能否则压出来的数据不真实。6. 常见问题与排查技巧实录这一章是我这些年攒下来的问题清单按现象—原因—处理整理成表遇到类似问题可以直接对照。后面再补几条文档里不写、只有踩过才知道的经验。6.1 高频问题速查表现象常见原因处理方向偶发 401时间戳偏差、密钥轮换未同步校验服务端与调用方时钟检查密钥版本大量 429限流阈值偏低或存在突发流量看实际 QPS 曲线调整桶容量而非填充速率响应体字段缺失序列化配置忽略空值、DTO 映射遗漏检查序列化选项核对映射代码请求超时但服务端无日志请求未到达、上游代理超时检查反向代理配置和网络链路内存持续上涨缓存无上限、事件未退订给缓存设容量上限和过期策略数据库连接耗尽连接未释放、慢查询堆积检查 using 释放看慢查询日志重复扣减/重复下单缺少幂等保护加请求号去重注意原子性深度分页变慢大偏移量扫描改游标翻页或加覆盖索引这张表我建议直接贴到团队文档里新人排查问题时先过一遍能省不少时间。6.2 几条文档里不写的避坑经验第一条接口文档一定要和代码同源生成。手写文档的结局必然是过期我试过各种办法最后发现只要不是自动生成就一定会腐烂。用注解生成文档代码改了文档跟着改虽然牺牲一点描述自由度但省下的沟通成本远超这点损失。第二条不要在日志里打完整的请求体。高频接口的请求体可能很大全量打日志既拖慢性能又可能把敏感信息写进去。我的做法是只记录关键字段和请求体哈希需要详细内容时靠请求号去抽样日志里捞。第三条给接口加耗时埋点并且按百分位统计。平均值会掩盖问题一个接口 95% 的请求 10ms、5% 的请求 3 秒平均下来才 160ms看着挺好实际上那 5% 的调用方已经炸了。我把 P99 设成告警指标超过阈值就通知。第四条超时时间要层层递减。调用方超时 3 秒被调方内部调用下游超时 5 秒这种情况下被调方还在等下游调用方已经超时重试了直接造成请求翻倍。正确做法是内层超时严格小于外层留出处理时间。第五条压力测试要跑真实数据分布。用均匀分布的数据压出来的结果会过于乐观。真实场景往往是少数几个热点键承担大部分流量缓存命中率、锁竞争情况完全不同。我一般会把历史流量录下来做回放压测结果才有参考价值。第六条灰度发布先切小流量。第一次上线新接口我会先切百分之一的流量观察半小时重点看错误率和延迟的百分位分布。有问题及时回滚损失可控没问题再逐步放大。这个习惯帮我避免过好几次大规模故障。6.3 我个人的一点体会做接口开发这些年我越来越觉得技术方案的复杂度应该和业务规模匹配而不是和想象中的规模匹配。我早期特别喜欢把所有能加的东西都加上限流、熔断、多级缓存、消息队列、分布式追踪结果服务启动要十几秒改一行代码要理解八个模块维护成本高得离谱而实际流量根本用不上这些。后来我的做法是先把最简单的版本跑起来把接口契约和错误码定死把日志和监控做扎实然后根据真实压测数据去加优化。大多数接口的瓶颈就那一两个点针对性解决比全面铺开有效得多。高频柜台接口是少数需要提前设计的场景但即便如此也是先测出瓶颈再优化而不是先堆一堆机制再去猜哪里有用。接口这行还有个特点就是老的接口永远比新的重要。你可以随便改一个新接口但一个被十几个调用方依赖的老接口动一个字都要评估半天。所以一开始就把契约设计好、把兼容性留足这份投入在两年后一定会得到回报。最后分享一个小技巧。每次要改已有接口时我都会先花十分钟写一版改动影响清单列出所有已知调用方、字段变化、兼容性判断、回滚方案。这个清单看着麻烦但能把相当一部分事故挡在上线之前。我现在的习惯是清单不写完就不动代码坚持两年下来接口相关的线上事故基本归零了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表