首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >网络监控与容灾:让网络问题无处遁形

网络监控与容灾:让网络问题无处遁形

作者头像
陆业聪
发布2026-05-18 12:53:35
发布2026-05-18 12:53:35
2600
举报

📚 Android网络优化系列 · 第5/5篇(完结)

从DNS到连接池,打造极速网络体验

✅ 第1篇:Android网络全链路拆解:一次HTTP请求背后的性能陷阱

✅ 第2篇:DNS优化实战:从运营商DNS到HttpDNS的进化之路

✅ 第3篇:连接优化与复用:让每一次握手都物超所值

✅ 第4篇:数据压缩与缓存策略:把带宽用到极致

👉 第5篇:网络监控与容灾:让网络问题无处遁形(本篇·完结)

📰 科技要闻

• 豆包App推出付费订阅(68-500元/月),AI工具商业化进入新阶段——免费午餐正在收桌。

• Kotlin官方发布OpenTelemetry可观测性教程——恰好与本篇网络监控主题呼应,全链路可观测正在成为工程标配。

• Android Weekly #725聚焦移动端Release工程化,Monzo/Spotify等团队分享自建发布工具的代价与反思。

从一次"幽灵故障"说起

去年有一次,我们的App突然收到大量投诉——"加载不出来"、"一直转圈"。但诡异的是:服务端监控一切正常,接口处理时间稳定在50ms以内,错误率不到0.1%。

排查了两个小时才定位到原因:某个省份的联通用户被DNS调度到了一个异常CDN节点——TCP能建立连接,但数据传输断断续续,实际吞吐量不到正常值的十分之一。

这种故障最让人崩溃:服务端看不到、后端监控抓不到、只有终端能感知。如果我们当时有完善的端侧网络监控,10分钟就能定位而不是盲查两小时。

这就是为什么我把"网络监控与容灾"放在系列最后一篇——前四篇教你怎么把网络做快,这篇教你怎么知道它慢了,以及慢了之后怎么自动兜底。优化做得再好,没有监控就是盲人走路,没有容灾就是裸奔。

OkHttp EventListener:端侧网络的CT扫描仪

第一篇简单提过EventListener,这次彻底用到极致。它是OkHttp提供的全链路埋点钩子,能精确拿到DNS、连接、TLS、发送、接收每个阶段的耗时,且零侵入——不用改任何业务代码。

生产级监控Listener

不是demo版的打个log完事,而是真正线上跑了半年的版本:

代码语言:javascript
复制
class NetMonitorListener(
private val reporter:
NetMetricReporter
) : EventListener() {private val t = NetTiming()override fun callStart(
call: Call
) {
t.callStart = System.nanoTime()
t.url = call.request().url
.toString()
}override fun dnsStart(
call: Call,
name: String
) { t.dnsStart = System.nanoTime() }override fun dnsEnd(
call: Call,
name: String,
ips: List<InetAddress>
) {
t.dnsEnd = System.nanoTime()
t.resolvedIp = ips.firstOrNull()
?.hostAddress
}override fun connectStart(
call: Call,
addr: InetSocketAddress,
proxy: Proxy
) { t.connectStart = System.nanoTime() }override fun secureConnectStart(
call: Call
) { t.tlsStart = System.nanoTime() }override fun secureConnectEnd(
call: Call,
hs: Handshake?
) { t.tlsEnd = System.nanoTime() }override fun connectEnd(
call: Call,
addr: InetSocketAddress,
proxy: Proxy,
protocol: Protocol?
) {
t.connectEnd = System.nanoTime()
t.protocol = protocol?.toString()
}override fun responseHeadersStart(
call: Call
) {
t.responseStart =
System.nanoTime()
}override fun responseBodyEnd(
call: Call,
byteCount: Long
) {
t.responseEnd = System.nanoTime()
t.bytes = byteCount
}override fun callEnd(
call: Call
) {
t.callEnd = System.nanoTime()
t.success = true
reporter.report(t.toMetric())
}override fun callFailed(
call: Call,
e: IOException
) {
t.callEnd = System.nanoTime()
t.success = false
t.errorType = classifyError(e)
reporter.report(t.toMetric())
}
}

错误分类:不要把所有异常扔进一个桶

线上网络错误五花八门,如果不做分类,监控大盘就是一坨无法下钻的数字。我的分类逻辑:

代码语言:javascript
复制
private fun classifyError(
e: IOException
): String = when {
e is UnknownHostException
-> "dns_fail"
e is ConnectException
-> "connect_fail"
e is SSLException
-> "tls_fail"
e is SocketTimeoutException
-> "timeout"
e.message?.contains("canceled")
== true -> "canceled"
e.message?.contains("reset")
== true -> "conn_reset"
else ->
"unknown_${e.javaClass.simpleName}"
}

这样在Grafana上按 error_type 维度拆分,一眼就能看出来今天是DNS挂了还是证书过期了。某天 dns_fail 突然飙升——八成是运营商DNS抽风;tls_fail 集中出现——赶紧检查证书有效期。

采样策略:别把监控后端搞崩了

日活百万的App,一天几亿次网络请求,每次都上报?你的监控后端先扛不住。策略很简单:异常全量、慢请求全量、正常请求采样

代码语言:javascript
复制
class SmartSampler(
private var sampleRate: Float
= 0.1f
) {
// 远程可配,出问题时调到1.0
fun updateRate(r: Float) {
sampleRate = r
}fun shouldReport(
metric: NetMetric
): Boolean = when {
// 失败请求:100%上报
!metric.success -> true
// 慢请求(>3s):100%上报
metric.totalMs > 3000 -> true
// 正常请求:按采样率
else ->
Random.nextFloat() < sampleRate
}
}

💡 踩坑经验:采样率一定要支持远程热更新。我们之前写死了10%,有一次定位长尾问题怎么都复现不了——因为那个case刚好没被采样到。后来改成远程配置,排查时临时调100%,问题秒出。

主动探测:不等用户投诉才发现问题

EventListener是被动监控——有业务请求才有数据。但很多时候你需要主动探测网络质量:App启动时判断当前网络等级、从后台恢复时快速评估、用户切网后马上探一把。

代码语言:javascript
复制
class NetworkProber(
private val client: OkHttpClient
) {
// 探测endpoint:空body,只测链路RTT
private val probeUrl =
"https://probe.example.com/ping"enum class Quality {
EXCELLENT, // < 100ms
GOOD,      // 100-300ms
MODERATE,  // 300-1000ms
POOR,      // 1000-3000ms
TERRIBLE   // > 3s 或失败
}suspend fun probe(): Quality {
val start =
System.currentTimeMillis()
return try {
client.newCall(
Request.Builder()
.url(probeUrl)
.head().build()
).await()
val rtt =
System.currentTimeMillis() -
start
classify(rtt)
} catch (_: Exception) {
Quality.TERRIBLE
}
}private fun classify(
rtt: Long
) = when {
rtt < 100 -> Quality.EXCELLENT
rtt < 300 -> Quality.GOOD
rtt < 1000 -> Quality.MODERATE
rtt < 3000 -> Quality.POOR
else -> Quality.TERRIBLE
}
}

探测驱动的动态策略

知道网络好不好之后,关键是自动调整行为——这才是监控的真正价值:

代码语言:javascript
复制
class AdaptiveStrategy(
private val prober: NetworkProber
) {
fun getTimeout(
q: Quality
): Long = when (q) {
EXCELLENT -> 10_000L
GOOD -> 15_000L
MODERATE -> 20_000L
POOR -> 30_000L
TERRIBLE -> 45_000L
}fun getImageQuality(
q: Quality
) = when (q) {
EXCELLENT, GOOD ->
ImageQuality.HIGH
MODERATE ->
ImageQuality.MEDIUM
POOR, TERRIBLE ->
ImageQuality.THUMBNAIL
}fun shouldPreload(
q: Quality
) = q == EXCELLENT || q == GOODfun getRetryCount(
q: Quality
) = when (q) {
EXCELLENT, GOOD -> 1
MODERATE -> 2
POOR, TERRIBLE -> 3
}
}

这套方案跑了大半年的数据:弱网超时率降了40%(不再用固定10s超时"逼"弱网用户),好网络场景的首屏时间反而提升了——因为敢预加载了。

弱网模拟:不要等上线才发现问题

扎心事实:大部分开发者测试网络功能时的环境——公司WiFi,满格信号,延迟5ms。什么代码都跑得飞快。等到用户在地铁里断断续续的4G下使用时,问题才暴露。

四种方案对比

方案

优点

缺点

Emulator限速

零成本内建

不能模拟丢包抖动

Charles限速

真机可用,GUI方便

HTTPS需配证书

OkHttp Interceptor

代码级精确控制

只影响OkHttp请求

Linux tc+netem

最真实,系统级

需root/共享网络

我的日常搭配:开发阶段用Interceptor(秒切场景),CI用Emulator限速跑弱网用例,发版前Charles完整测一轮。给个Interceptor实现:

代码语言:javascript
复制
class WeakNetSimulator(
private val delayMs: Long = 2000,
private val jitterMs: Long = 500,
private val dropRate: Float = 0.1f
) : Interceptor {override fun intercept(
chain: Interceptor.Chain
): Response {
// 模拟随机丢包
if (Random.nextFloat()
< dropRate) {
throw SocketTimeoutException(
"[SIM] packet loss"
)
}
// 模拟延迟+抖动
val actual = delayMs +
Random.nextLong(
-jitterMs, jitterMs
)
Thread.sleep(actual)return chain.proceed(
chain.request()
)
}
}

⚠️ 血泪教训:这个Interceptor千万别出现在Release包里!别只靠 BuildConfig.DEBUG 控制——把整个类放在 debugImplementation 的source set里,编译时物理隔离最安全。我们组有人曾经因为条件判断写反了,让10%的线上用户体验了"弱网模拟"……

容灾降级:网络挂了不等于App挂了

监控告诉你"出事了",容灾决定"出事之后怎么办"。目标很清晰:核心功能在极端网络下依然可用,哪怕是降级版本。

多域名自动切换

最基本也是最有效的容灾手段:主域名挂了,自动切备用。做好了能解决80%的可用性问题:

代码语言:javascript
复制
class DomainFailover(
private val domains: List<String>
) : Interceptor {// ["api.example.com",
//  "api-bk.example.com",
//  "api.example.cn"]private val fails =
ConcurrentHashMap<
String, AtomicInteger>()
private val current =
AtomicReference(domains[0])override fun intercept(
chain: Interceptor.Chain
): Response {
val req = chain.request()
var lastErr: IOException? = nullfor (domain in sortedDomains()) {
try {
val newUrl = req.url
.newBuilder()
.host(domain).build()
val resp = chain.proceed(
req.newBuilder()
.url(newUrl).build()
)
// 成功 → 重置计数
fails[domain]?.set(0)
current.set(domain)
return resp
} catch (e: IOException) {
fails.getOrPut(domain) {
AtomicInteger()
}.incrementAndGet()
lastErr = e
}
}
throw lastErr
?: IOException(
"All domains failed")
}private fun sortedDomains() =
domains.sortedBy {
if (it == current.get()) 0
else fails[it]?.get() ?: 1
}
}

三级兜底:网络→缓存→预埋数据

比域名切换更彻底的方案——让关键页面在完全没网时也不白屏

代码语言:javascript
复制
class ResilientRepo<T>(
private val api: ApiService,
private val cache: LocalCache<T>,
private val fallback:
AssetFallback<T>
) {
suspend fun getData(
key: String
): DataResult<T> {
// Level 1: 网络
try {
val data = api.fetch(key)
cache.save(key, data)
return DataResult.fresh(data)
} catch (_: Exception) { }// Level 2: 本地缓存
cache.get(key)?.let {
return DataResult.stale(it)
}// Level 3: 预埋兜底包
fallback.get(key)?.let {
return DataResult.fallback(it)
}return DataResult.empty()
}
}

数据获取的三级容灾

发起数据请求

网络请求成功?

✅ 是 → 返回最新数据 + 更新缓存

❌ 否 → 降级到本地缓存 ↓

缓存命中?

✅ 是 → 返回缓存(标记stale状态)

❌ 否 → 降级到Asset兜底包 ↓

展示兜底数据(不白屏)

UI层配合 DataResult 的状态来显示不同的提示:fresh时正常展示,stale时顶部加个"数据可能不是最新"的横条,fallback时提示"网络不可用,显示离线数据"。用户虽然拿不到最新数据,但至少App没崩没白屏——这在用户体验上是天壤之别。

CDN节点健康检测

回到开头那个"幽灵故障",最终方案是在端侧做CDN节点健康检测——当某个IP表现异常时,主动屏蔽它:

代码语言:javascript
复制
class CdnHealthChecker {// IP → 解禁时间戳
private val blocked =
ConcurrentHashMap<String, Long>()fun markBad(ip: String) {
// 屏蔽5分钟
blocked[ip] =
System.currentTimeMillis() +
5 * 60_000
}fun filterHealthy(
ips: List<InetAddress>
): List<InetAddress> {
val now =
System.currentTimeMillis()
val healthy = ips.filter {
val exp =
blocked[it.hostAddress]
exp == null || now > exp
}
// 全挂了就返回全量,避免死锁
return healthy.ifEmpty { ips }
}
}

配合第二篇讲的自定义DNS接口,在DNS解析结果返回时过滤掉被标记为异常的IP。这样下次请求自动绕过故障节点,用户甚至感知不到出过问题。

告警体系:让监控真正有用

监控不告警=没监控。但告警做不好=狼来了。我踩过的坑总结成一个原则:分层、带上下文、做收敛

告警分层模型

🔴 P0 致命 — 整体成功率 < 95% | TTFB P99 > 10s → 电话+短信+群通知

🟡 P1 严重 — 某域名/ISP失败率 > 10% → 群通知+Oncall DM

🔵 P2 警告 — DNS P95 > 500ms | 慢请求率 > 5% → 群通知

🟢 P3 观察 — 接口P95同比上涨20% → 日报汇总

三个血泪教训:

用趋势告警,不用绝对值。"P95是500ms"不一定有问题,但"P95比昨天同期涨了200%"一定有鬼。

告警要带维度上下文。光说"错误率升高"没用,说"联通+广东+dns_fail飙升300%"才能秒级定位。

5分钟收敛窗口。同一问题5分钟内只通知一次,附带影响用户数。否则值班人手机先炸了。

串联全局:一个完整的OkHttpClient

最后,把这篇和前四篇的能力串起来,这是我们线上的OkHttpClient配置:

代码语言:javascript
复制
fun buildProductionClient():
OkHttpClient {
val health = CdnHealthChecker()
val reporter =
NetMetricReporter(reportUrl)return OkHttpClient.Builder()
// DNS优化(第2篇)+健康过滤
.dns(SmartDns(httpDns, health))
// 域名容灾(本篇)
.addInterceptor(
DomainFailover(domains)
)
// 连接池(第3篇)
.connectionPool(
ConnectionPool(
32, 5, MINUTES
)
)
// HTTP/2多路复用(第3篇)
.protocols(listOf(
Protocol.HTTP_2,
Protocol.HTTP_1_1
))
// 全链路监控(本篇)
.eventListenerFactory {
NetMonitorListener(reporter)
}
.build()
}

每一行配置背后都是这个系列某一篇的内容。这就是"体系"的意思——单点优化有上限,但当DNS + 连接池 + 压缩 + 监控 + 容灾组合在一起时,效果是乘法不是加法。

系列完结:回顾与展望

篇目

解决什么

核心收益

第1篇 全链路拆解

知道慢在哪

建立性能心智模型

第2篇 DNS优化

首连接耗时

首请求快200ms+

第3篇 连接优化

握手成本

复用率>90%

第4篇 压缩缓存

传输体积

流量省60%+

第5篇 监控容灾

发现并兜住异常

故障定位10min→秒级

做性能优化有个原则我想在最后强调一次:先测量,再优化,别靠猜。今天这篇讲的监控体系其实是所有优化的起点——你得先知道慢在哪,才知道该改哪。

网络优化是个永远做不完的事。WebSocket长连接优化、基于ML的质量预测、QUIC在生产环境的灰度方案……每个方向都能再写一个系列。但基础就是这五篇的内容——把基本功做扎实了,80%的网络问题都能搞定。

系列完结。如果这五篇对你的工作有帮助,转给你的团队同事——网络优化从来不是一个人的事。

📚 Android网络优化系列 · 全部完结

✅ 第1篇:Android网络全链路拆解:一次HTTP请求背后的性能陷阱

✅ 第2篇:DNS优化实战:从运营商DNS到HttpDNS的进化之路

✅ 第3篇:连接优化与复用:让每一次握手都物超所值

✅ 第4篇:数据压缩与缓存策略:把带宽用到极致

✅ 第5篇:网络监控与容灾:让网络问题无处遁形(本篇)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档