0. 连续五次失败,但这不是 flaky
我有一条每夜跑的 E2E:起一套完整的 compose(MySQL、Redis、所有服务),然后用 Playwright 跑管理后台的关键路径。
我有一条每夜跑的 E2E:起一套完整的 compose(MySQL、Redis、所有服务),然后用 Playwright 跑管理后台的关键路径。
这篇讲两个性能决策,它们的结论看起来是相反的。
前面 24 篇讲的是"我为什么这么做"。这一篇不一样——它讲具体怎么做。
有一段时间我觉得管理后台"卡"。具体表现是:点侧边栏切到 /dashboard、/usage、/admin/logs,要等大约一秒才出内容。
有一阵子我觉得代码里积了不少问题——文件越来越大、有几处逻辑我自己都记不清了。于是我让一个模型对 develop 做了一轮系统性审查,产出一份问题清单。
这次重设计的起点不是"界面不好看",是一件更实际的事:字体没覆盖中文。
做一个"在浏览器里调模型"的 Playground,聊天界面本身没什么难的:一个输入框、一个消息列表、一个流式渲染。
我第一版的对账逻辑很简单:把钱包余额和账本净额比一下,不一致就报出来。
这段代码注释是我在 review 自己的时候发现的,编号 platform-L9:
这篇复盘两次问题。它们表面上看没有关系:
做这个接口的时候遇到的第一个问题不是技术问题,是语义问题。
第 12 篇讲了三层缓存,但留了一个尾巴没讲:缓存什么时候该失效。
第 1 篇里算过一笔账:relay-gateway 处理一个请求,要打四次下游 gRPC——identity 鉴权、channel 选路、billing 预扣、billing 结算,外加写日志。
我第一版迁移就是一个编号序列:001_xxx.sql、002_xxx.sql……每个文件一个号,跑的时候按号排序。
用户买了一个月的套餐,付了 20 块。用了 10 天,要求退款。
做网关的时候我一开始的想法是"给每个上游接口写一个适配器"。
做订阅功能的时候,我脑子里只有一个"额度"概念:用户买了套餐,每个月能用 20 美元,超过就停。
接上游 provider 这件事,我一开始以为是"一家一个适配器"。写了才发现不是:三十多个渠道类型里,真正需要单独写协议转换逻辑的只有四家,其余二十多家全是同一个适配器的不同配置。
我第一版的 token 刷新逻辑大概是这样的:
订阅账号会有各种"暂时不能用"的情况:被上游限流了、额度用完了、凭据过期了、上游在抖动。这些状态都要有退出机制——总不能因为一次 429 就永远不再用这个账号。
有一批请求要的模型在所有渠道上都没配置,channel-service 全部返回"没有可用渠道"。这个错误跨 gRPC 回来是 codes.Unknown,而我的熔断器把它算作失败——于是用户传错一个模型名,整个网关的流量都被熔断器拒了。这篇讲这次之后我怎么重排错误分类,以及跨服务降级的其他几处取舍。
并发一上来,上游返回 429,而我当时的逻辑把 429 当失败,重试几次就把一个完全健康的账号标记成不健康、从候选里摘掉。这就是"用满"和"用坏"没分开的后果。这篇讲我最后怎么在限流器、冷却时长、重试策略、健康记录、指标标签每一处把它们分开。
选路这件事比我想的难。优先级相同的渠道怎么分流量、重试该换谁、换的时候会不会误伤别的渠道、授权要不要再验一次、一个渠道坏到什么程度该被摘掉——这篇按我实际动手的顺序讲,中间有两次返工,还有一个后来 review 时自己揪出来的、后果比较严重的 bug。
从一次我自己撞的墙讲起:网关把上游的 4xx 改写成了 502,而终止分支缺日志,我完全看不到发生了什么。顺着这个坑,我把 /v1/chat/completions 这条链路从头到尾捋了一遍,这篇记录其中的选择——哪些是当初顺手写下的、哪些是返工回来的、哪些我到现在也还没底。
这个项目还没上线。我在开发过程中把结构翻了一遍,而且方向还相反:第一次是把一个 2391 行的 internal/relay/server/http.go 拆开,第二次是把拆出去的九个服务收进 Kratos 大仓结构、抽出共享层,并写脚本卡住服务之间互相 import。这篇讲哪些线是我真的按需要切的,哪些只是抄了微服务的目录形状。