news 2026/11/5 2:29:42

网络波动后重试变成风暴,连接观察器怎样安排退避

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
网络波动后重试变成风暴,连接观察器怎样安排退避

面板上feed?page=2这条 XHR 耗时 214ms,状态 200,看着很正常。但它的响应头里x-request-id出现过三次,说明这条请求悄悄重试过。我把重试间隔设成 1s、2s、4s 这样翻倍,心想够稳了。跑起来才发现不对——网络一断,页面里 18 个有效请求同时失败,18 个都从第 1 秒开始重试,第 1 秒就有 18 条并发打出去,刚恢复的 Wi-Fi 6 又被压垮,于是一起失败、一起再等 2 秒、一起再打。翻倍没有救场,反而把 18 条请求锁进了同一个节拍。

这就是重试风暴。反直觉的地方在于:指数退避本身没错,错在没有把"同时失败"这件事算进去。请求们共享同一个失败时刻,如果退避序列是确定性的,它们下一次重试也会精确地撞在一起。

退避要带抖动,且抖动要先算好上限

核心思路就一条:在指数退避的基础上加一个随机抖动,把每个请求的下一次重试时刻打散。我用的 full jitter,退避区间取[0, base * factor^(attempt-1)]的均匀随机,而不是在确定值上加减一个固定百分比。

interfaceRetryPolicy{baseMs:number// 首次重试的基础间隔factor:number// 每次退避的乘数maxMs:number// 退避上限maxAttempts:number// 最大重试次数}constdefaultPolicy:RetryPolicy={baseMs:200,factor:2,maxMs:5000,maxAttempts:4}functionnextDelayMs(policy:RetryPolicy,attempt:number):number{constcap=Math.min(policy.maxMs,policy.baseMs*Math.pow(policy.factor,attempt-1))returnMath.floor(Math.random()*cap)}

用 full jitter 而不是"确定值 ± 25%"有个具体好处:当 18 个请求的 attempt 都是 1 时,确定值会让它们都落在 200ms 附近一个窄带里,full jitter 让它们均匀铺在 0 到 200ms 之间。18 个请求的第 1 次重试,最坏情况全部集中在 200ms 附近,最好情况撒成一片,期望值就是分散的。

重试循环里要留一个取消口

@ohos.net.http的请求是异步的,重试循环如果只关心次数和延迟,很容易写出一个关不掉的循环。我加了一个取消令牌,组件销毁或者用户切走页面时把令牌置位,让还在等待中的重试立刻退出。

import{http}from'@kit.NetworkKit'interfaceRetryHandle{cancel():void}functionrequestWithRetry(url:string,policy:RetryPolicy):RetryHandle{letcancelled=falseconstrun=async():Promise<void>=>{letattempt=0while(attempt<=policy.maxAttempts){if(cancelled){return}try{constresp=awaithttp.createHttp().request(url,{method:http.RequestMethod.GET,connectTimeout:3000,readTimeout:5000})if(resp.responseCode>=200&&resp.responseCode<300){onSuccess(resp)return}// 状态码失败也走重试,但要区分 4xx 和 5xxif(resp.responseCode>=400&&resp.responseCode<500){onError(resp.responseCode)return}}catch(e){// 网络错误,继续退避}if(attempt===policy.maxAttempts){onExhausted()return}attempt+=1constdelay=nextDelayMs(policy,attempt)awaitsleep(delay)}}run()return{cancel:()=>{cancelled=true}}}functionsleep(ms:number):Promise<void>{returnnewPromise(resolve=>setTimeout(resolve,ms))}

这里有两个失败分支需要说清楚。4xx 直接不重试——404、403这种是请求本身错了,重试一百次还是错的,等退避纯属浪费时间;只有网络错误和 5xx 才值得重试。第二个分支是maxAttempts耗尽,这时候返回给上层一个"已放弃"的结果,观察面板把这条请求标成红色,而不是静默吞掉。

attempt === policy.maxAttempts这个判断要放在重试前,不能放到重试后。第一版我写在sleep之后,结果多等了一轮退避才发现没次数了,白睡一个maxMs。

另一个坑在sleep本身。第一版我直接setTimeout睡满退避时长,取消信号要等睡醒才被看到,组件销毁后一个已经取消的请求还会再多发一次。改成可打断的 sleep,每 50 毫秒检查一次取消状态:

functioncancellableSleep(ms:number,isCancelled:()=>boolean):Promise<boolean>{returnnewPromise(resolve=>{conststart=Date.now()constcheck=():void=>{if(isCancelled()){resolve(false)return}if(Date.now()-start>=ms){resolve(true)return}setTimeout(check,50)}check()})}

返回值区分"睡够"和"被取消",重试循环里根据它是继续下一轮还是直接退出。

面板怎么呈现退避

观察面板的数据列里,feed?page=2的 214ms 是最终成功那次的时间,它前面还有两次失败的请求被折叠掉了。为了不误导看的人,我把重试次数和退避轨迹单独记下来。

interfaceRetryTrace{url:stringattempts:numberdelays:number[]// 每次重试前的等待时长finalStatus:number}functionnextDelayWithTrace(policy:RetryPolicy,attempt:number,trace:RetryTrace):number{constdelay=nextDelayMs(policy,attempt)trace.attempts=attempt trace.delays.push(delay)returndelay}

这样在请求详情里就能看到一串退避数字,比如[86, 310, 1240, 4800]。这串数字比一个孤零零的"耗时 214ms"更能说明问题——它会告诉看面板的人,这条请求是扛过了四次失败才成功的,而不是一次就通。

工程里的样子

NetworkScope/ ├─ entry/src/main/ets/ │ ├─ pages/Index.ets # 请求列表与详情面板 │ ├─ net/RetryScheduler.ets # nextDelayMs / requestWithRetry │ ├─ net/RetryTrace.ets # 退避轨迹记录 │ └─ entryability/EntryAbility.ets ├─ entry/src/main/resources/ └─ build-profile.json5

请求详情里那行等待 142ms,其实已经把重试的隐形成本吃进去了。加了退避之后,feed?page=2这类接口在弱网下的最终耗时会被拉长,但观察面板能同时看到传输资源 742 KB、页面完成 1.84s 和缓存命中 91% 这三个数,不至于把重试抖动误判成接口变慢。

边界

这套退避只解决"客户端自己发起的重试"。如果服务端也有重试、或者中间网关在重试,客户端再加指数退避就是重试的重试,风暴照样会起来,那种场景要靠x-request-id做幂等去重,不是本方案能覆盖的。另外maxAttempts和maxMs是一对需要按业务调的参数,短视频 feed 这种轻接口可以设得激进一点,下单、支付这类写操作应该把maxAttempts收到 1~2,宁可放弃也不能让用户重复扣款——重试本身是有副作用的,观测面板只负责把退避过程讲清楚,不替你决定哪些接口该重试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 8:40:12

谷歌上线差评勒索举报表单:商家应对恶意差评的实操指南

差评勒索这四个字&#xff0c;做过线上生意的人应该都不陌生。上月谷歌低调上线了专门针对负面评论敲诈的举报表单&#xff0c;我第一时间就去试了试&#xff0c;也跟踪了一波商家反馈。结论先说&#xff1a;这东西真的管用&#xff0c;而且它的有效性比很多人想象的要深——它…

作者头像 李华
网站建设 2026/10/31 7:02:54

Django学生信息管理系统实战:从模型设计到生产部署

1. 项目概述&#xff1a;为什么一个学生信息管理系统&#xff0c;值得花两周时间从零搭起&#xff1f;“Django实战项目 —— 学生信息管理系统”&#xff0c;这行字在招聘JD里出现频率高得有点反常。不是因为它多炫酷&#xff0c;恰恰相反——它朴素得像食堂窗口的价目表&…

作者头像 李华
网站建设 2026/10/31 20:04:33

真实场景图像质量诊断:六维工程化评估框架

1. 项目概述&#xff1a;这不是一张“好看”的图&#xff0c;而是一份故障诊断报告“Assessing Image Quality Issues for Real-World Problem”——这个标题乍看像一篇标准的学术论文题名&#xff0c;但拆开来看&#xff0c;它根本不是在教你怎么调高分辨率或加滤镜&#xff0…

作者头像 李华
网站建设 2026/11/4 22:41:20

传媒行业手游流量运营模式:从买量到留存的三层结构解析

简介&#xff1a;这份中金公司2019年6月发布的传媒行业研究报告&#xff0c;聚焦手游流量运营&#xff08;买量&#xff09;模式&#xff0c;适合游戏行业从业者、投资研究人员及关注游戏发行策略的读者&#xff0c;用于理解效果广告分发逻辑与厂商竞争力评估。资源为1个PDF文件…

作者头像 李华
网站建设 2026/11/3 6:47:04

DSCP标记原理与配置排错:从报文标识到QoS队列调度全解析

搞网络的人大概都经历过这种局面&#xff1a;视频会议开着开着突然卡住&#xff0c;语音时断时续&#xff0c;核心链路明明还有余量&#xff0c;可关键业务就是抢不过后台批量任务。很多人第一反应是加带宽&#xff0c;带宽加完问题依旧&#xff0c;最后只能在日志和抓包里来回…

作者头像 李华
网站建设 2026/11/3 17:44:05

大文件分片上传实战:Flask+WebUploader实现与避坑指南

简介&#xff1a;面向Python Web开发者的分片上传与下载演示项目&#xff0c;后端基于Flask框架&#xff0c;前端采用WebUploader组件&#xff0c;可实现大文件分段上传、服务器端合并以及低内存流式下载&#xff0c;适合学习文件传输处理、内存控制和并发场景的工程师。压缩包…

作者头像 李华