回复里有汉字就慢,坑在计数单位被写进了中间件

一、同一句话有四种数法
限流、计费、配额这三类逻辑,第一件事都是把“这个请求算多少”变成一个数。能用的数法至少有四种,而且它们之间没有稳定的换算系数。
| 计数单位 | 怎么得到 | 拉丁文字与表意文字的差别 | 常见误用 |
|---|---|---|---|
| 字符数 | 逐个可见字符累加 | 表达同一层意思,表意文字的字符数往往更少 | 拿字符数估服务端容量 |
| 字节数 | 按编码后的字节累加 | 表意文字在常见编码下每字占更多字节 | 拿字节数当生成代价 |
| 词元数 | 按模型的分词表切分 | 同长度回复切出的片段数量差别很大 | 拿词元数做统一阈值 |
| 词数 | 按空格或语言习惯切词 | 表意文字没有天然空格,结果取决于切词方式 | 拿词数折算配额 |
四种里只有词元数与模型侧的真实计算量有对应关系,字符数和字节数是文本属性,词数还额外依赖切词实现。坑就出在:中间件往往取最容易拿到的那个数当阈值,而这个数并不代表代价。要把口径写成一条可核对的定义,至少回答三件事:数的是输入、输出还是两者之和;在哪个环节数,请求进网关时估一次,还是上游返回后取实际值;粒度按什么走,按单次调用、按会话,还是按主体在时间窗内的累计。这三件事不写清,同一个数字挂在不同位置上就不是同一个意思。
二、阈值一视同仁,等于按语言发额度
限速的目的通常是两件事:保护上游容量,以及在多个使用者之间公平分配。用词元数当单元,本身并不错,错在把阈值取成对所有文字体系相同的数值。表意文字的单位信息密度高,同样一段回答切出的片段数量可能是拉丁文字的数倍,于是同一个“每分钟多少单元”的阈值,实际给不同语言的人发的是不同额度。这不是谁的立场问题,是把“数出来的量”直接等同于“付出的代价”必然出现的偏差。
修法不是把阈值调大就算完,而是让阈值对应的量尽量靠近真实占用。可以用的量有:并发中的请求数、服务端实际处理时长、排队位置的等待深度、由服务端计算的输出规模。这些量与文字体系无关,也与使用者的输入习惯无关。如果仍然要用词元数,那就必须在同一处实现里给出统一定义,并把阈值按语言与场景分档,每一档写明依据——写“经验值”不算依据。
要把这种差异暴露出来,需要的测试其实很小:准备同一句语义的四种语言样本,两种拉丁文字、两种表意文字,逐种记下网关计到的单元数、端到端耗时,以及把阈值逐档压低时先在哪一档被拒。四种语言必须是同一句话——用四条各自通顺但语义不同的句子,测出来的差别无法归因到文字体系上。这张表跑一次用不了多少时间,却能在上线前回答“同一件事在不同语言里被算成多少”,而不是等到有人反馈含某种文字的回复会变慢才发现。
还有一层“慢”跟限速无关,容易被一起归因。前端做流式渲染时,增量文本是一段段往节点里追加的,不同文字的分段边界、行高与换行规则不同,追加时触发的重排与回流代价也不同。表意文字的行宽估算与断行位置在窄容器里更容易造成整块重算。这类代价在数据上表现为“界面卡”,在日志里却不属于被拒绝,如果不分桶统计,就会被算进限速问题的样本里。
三、一处口径写错,会串到四个地方
计数口径不是限速一个模块的私事,它至少会串到这四处,一处写错四处互相对不上。
- 计费单价:对外报价用的单位和内部计量用的单位不同名,账单与合同就得靠人工解释。
- 配额申请:申请单上写的是“每日多少次”,系统里扣的是累计单元,使用者按次数估、系统按单元拒。
- 缓存键与命中率:把长度或计数的一部分塞进缓存键,两种文字体系下同一语义的请求会算成两个键,命中率莫名其妙偏低。
- 日志聚合统计:日志里同时存在字符数、字节数与上游返回的单元数三个字段,看板取哪一个没有约定,跨期对比就失去意义。
这四处的共同点是:它们各自实现了“数一下”,但没人负责让它们数同一个东西。
四、中间件里三类典型实现错误
第一类,把长度当代价。用输入或输出的文本长度折算占用,长度与真实计算量之间隔着分词、上下文与采样,不同语言下这条换算线完全不同。
第二类,把请求数当负载。一次调用背后可能是长输出、多轮重排或一次昂贵的检索,按请求数分配容量时,服务端最重的那类流量反而拿到了最宽松的额度。
第三类,把上游返回的计数直接当真值。上游的计数字段本身可能是估算值,也可能与网关侧的语义不一致;把它原样写进账本,等于把口径的归属权交给了别人。要么在接口约定里写明这个数的定义并纳入版本管理,要么在自己这一侧用同一处实现重算。
五、复盘里真正可抄的不是倍数
据公开报道,那篇复盘把研究版模型放进内部协作频道连续运行两周,网页端与桌面端整体速度提升约三倍,此前含表意文字回复被限速的现象也一并处理。倍数不是我们的实测值,也不构成本篇的判断依据。值得抄的是方法:把一个版本长期放在真实高频环境里跑,而不是把极端用法挡在预生产之外。
内部真实环境有两种做法。一种是自家团队全员长期使用,并且把每一次抱怨落成工单——“怎么又卡了”如果不落成条目,过一阵之后只剩一句“感觉变慢了”。另一种是建一个模拟真实混合负载的常驻回放环境,把线上抓到的负载形态按语言、端型与场景比例回放,让长尾用法每天都被撞一遍。前者成本低但覆盖取决于人员构成,后者可控但要维护,两者不互相替代。
看这类问题必须守分位数纪律,平均值恰好会把被限速的那一小部分人抹平。至少要按四列看,并按语言与端型分桶。
| 观测列 | 用途 | 只看平均值会漏掉什么 |
|---|---|---|
| 中位耗时 | 判断典型体验 | 不反映尾部 |
| 高分位耗时 | 判断被拖累的那部分 | 被大样本稀释 |
| 极高分位耗时 | 发现最差场景 | 完全看不见 |
| 被拒绝或降级比例 | 判断阈值影响面 | 与耗时混成一个数 |
国际化与无障碍问题常常出在同一处代码上,原因也相同:默认按主流用户设计。两者都只在非主流用户身上暴露,所以只要观测没有分桶,这两类缺陷就会长期隐身。
六、把口径写进代码、合同与自查表
口径要落到工程上,有三条硬要求。计数函数只有一个名字、只有一处实现,其他地方不得自行长度估算;每个调用点注明用途,标明这次数是给限速、给账单还是给看板;阈值配置按语言与场景分档,每档写明设定依据与生效条件,并留一处能读到当前生效值的入口。
对外报价与合同要跟着一起修。如果内部按词元计量,合同里写“按字符”或“按次”都会对不上,出账时只能靠人工折算。同名同单位是底线:内部字段叫什么,合同里就写什么,换算关系写死在文本里而不是留在工程师的脑子里。洹拓在给客户做接入时,把这两处的单位一致性当作交付项而不是文档细节,因为对账争议往往从这里开始。
最后一件事是做一张拒绝点自查表。把系统里所有会做拒绝或降级的点逐个列出——网关、任务队列、检索配额、并发上限、前端降级策略都算——每一点回答三个问题:数的是什么,按什么粒度,谁能改。三个问题里只要有一格填不出来,那个点就不该被认为已经处于受控状态。我们内部把这张表当成国际化专项的入口,因为绝大多数按语言失衡的问题,第一次被发现都是在这张表上。