ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

剖析Cronet连接复用判断:协议原理、代码定位与NetLog实战

剖析Cronet连接复用判断:协议原理、代码定位与NetLog实战 做网络优化的人大概率都有过这种经历接口体量不大服务端响应也快但线上P99总是偶尔掉链子抓包一看TCP握手次数比预期多出好几倍。说白了就是HTTP连接复用没生效一个本来可以走老连接的请求偏偏每次都重新建连。我去年排查Cronet相关的问题最头疼的不是怎么优化而是“你怎么判断一个请求到底有没有复用已有连接”。Cronet作为一个基于Chromium网络栈的库连接复用机制和OkHttp、HttpURLConnection完全不同很多从OkHttp转过来的同学第一步就懵了。这篇文章我就把“Cronet判断连接是否复用”这件事从原理、代码、抓包到踩坑完整讲一遍。网上关于Cronet连接复用的讨论其实不少但大多是零散的截图和口述真正能落地的判断方法尤其是不依赖外部抓包、能在业务侧直接感知的方案反而很少。我这次会先从协议层面把连接复用的几种形态理清楚再按“代码层拿数据”“NetLog看过程”“指标反推结果”三条路分别给出可操作的做法最后把我在线上遇到过的几个误判场景列出来。如果你现在正在做Cronet接入或网络性能优化这篇文章应该能帮你少走不少弯路。1. 先把“连接复用”这件事拆明白Cronet到底在复用谁1.1 三层协议三种复用含义很多人一听到“连接复用”第一反应就是Keep-Alive也就是HTTP/1.1里那个Connection头。在Cronet的语境下问题远比这个复杂因为Cronet同时支持HTTP/1.1、HTTP/2和QUIC而每一种协议里的“复用”含义都不一样。先说HTTP/1.1。它的连接复用就是我们最熟悉的Keep-Alive一个TCP连接在响应请求结束后不直接关闭而是在一段时间内保持空闲后续请求继续用这条连接。判断一个请求是否复用了连接标准很简单后一个请求是否使用了前一个请求的同一个TCP socket。然后是HTTP/2。HTTP/2在一个TCP连接上可以同时跑多个Stream每个Stream承载一个请求。所以连续发两个请求到同一个服务端它们很可能共用同一个TCP连接但各自有不同的Stream ID。这里有一个容易混淆的点HTTP/2的“多路复用”是指应用层的请求并发真正的底层TCP连接复用还是那条老连接。判断时不能看到两个请求共用连接就说是HTTP/2的特性还要区分连接和Stream的关系。QUIC是第三种情况。它基于UDP把TCP的连接管理和TLS握手合并到了自己的会话层。QUIC的连接复用更像是“session复用”同一个QUIC Connection ID对应的会话可以用0-RTT直接发数据省掉了一次RTT。技术上看它甚至连“TCP连接”都没有但从上层请求的视角看它的确做到了连接复用。包括连接迁移时Connection ID保持不变但底层网络路径可能已经换过一条。1.2 Cronet连接池的组织方式Cronet底层是Chromium网络栈它对连接的管理比你想象中还要底层。很多从OkHttp切过来的人以为Cronet也有一个“类似ConnectionPool的类”然后通过这个类去查连接复用状态——这是个大误区。Cronet负责连接复用的是网络栈内部的HttpStreamFactory、SocketPool和QuicSessionPool这些核心对象没有对外开放Java API。Cronet对外暴露的是UrlRequest和BidirectionalStream你只能发起和取消请求拿不到底层SocketPool里的连接列表。这带来的直接问题是判断连接是否复用的难度比OkHttp大很多。OkHttp里你可以看到connectionPool的connectionCount和idleConnectionCountCronet压根没有这种直接入口。所以很多习惯了OkHttp风格的同学换到Cronet后第一反应就是“我怎么什么都看不到”。如果把Cronet的连接体系比作一个停车场SocketPool是车位HTTP/2 Session和QUIC Session是停好的车请求就是来找车位的司机。司机来了先看停车场有没有空闲的车位有就直接停进去没有就新建一个车位同时把车开进来。这是最朴素的连接复用逻辑NetLog里能看到的就是这个“找车位”的过程。1.3 明确判断目标才能选对判断方法“怎么判断连接是否复用”这个问题的难点在于目标不同答案完全不同。你是想证明“同一个客户端进程内多次请求复用了同一条连接”还是想看“连接建立次数在请求总量中的占比”又或者是“服务端主动断开后重新建连的频率”这三个问题对应的判断方法完全不是一套东西。我自己的经验是先给判断目标定个范围再选手段如果只想快速确认线上有没有复用看连接建立次数和请求总次数的比例指标反推就够了。如果想定位某个请求到底有没有复用老连接只能靠NetLog因为它会记录每个请求绑定到了哪个Socket或Session。如果想知道业务在特定场景下的复用表现可以在RequestFinishedInfo上做采样上报用代码层的数据辅助分析。后面三个部分我会分别把这三条路展开讲。2. 代码层感知连接复用从RequestFinishedInfo里挖线索2.1 RequestFinishedInfo能暴露出什么Cronet提供了一套请求结束回调RequestFinishedInfo.Listener。在请求结束之后这个回调会带着RequestFinishedInfo对象返回里面包含Metrics、Annotations和FinishedReason三个关键信息。其中最被忽视也最有价值的是Metrics。Metrics类暴露了请求开始时间、结束时间、首字节时间、发送字节数、接收字节数等数据。这些字段对判断连接复用有什么帮助关键在于部分版本的Metrics里还有“连接建立”阶段的时间信息。如果一段流量里某几个请求的TTFB时间到首字节很高而另几个请求的TTFB很低低的那部分大概率是走了连接复用。但这里要特别提醒Cronet的API一直在演进不同版本里Metrics暴露的字段差别很大。我最早用的版本里Metrics连连接信息都没有只能通过时间间接推断后来某次升级后多了ConnectionInfo对象才总算能拿到协议、RTT、本地IP、远端IP之类的数据。所以依赖Metrics做判断时一定要先确认你接入的Cronet版本里RequestFinishedInfo.Metrics到底开放了哪些字段。最好的办法是直接翻本地依赖里的源码或者写个Demo把所有字段打出来看一眼不要凭网上的旧文章猜。2.2 ConnectionInfo里的协议、网络状态和复用痕迹如果你用的Cronet版本比较新那么Metrics里会有一个getConnectionInfo()方法返回ConnectionInfo对象。这个对象里能拿出什么比较常见的有协议类型h2、h3、http/1.1等本地IP、远端IPRTT估算值网络信号相关参数收发字节数需要注意的是ConnectionInfo并不是专门为“判断连接复用”设计的它更偏向网络质量诊断。所以你不会在里面看到一个字段叫reused或者“是否复用”的布尔值。但你可以拿它做一件事对比前后两个请求的ConnectionInfo里的本地IP、远端IP和连接相关的标识。如果两个请求的远端IP相同协议也相同那它们很有可能复用了同一条连接再结合耗时就能进一步确认。不过这条路的局限也很明显拿不到Socket级别的连接ID靠IP和协议做“痕迹对比”只能算间接证据。如果线上有负载均衡、DNS变化、连接迁移等因素干扰就很可能会出现误判。所以我的习惯是ConnectionInfo用于线上粗筛真正要下结论时还是要靠NetLog。2.3 一个可用的采集器示例我分享一下我自己在工程里用的粗筛代码思路很简单给CronetEngine加一个RequestFinishedListener把每次请求的协议、远端IP、TTFB、总耗时、请求URL放进采样数据里按域名聚合再观察同域名下“连接建立次数”的变化趋势。val engine CronetEngine.Builder(context) .enableQuic(true) .enableHttp2(true) .addRequestFinishedListener(object : RequestFinishedInfo.Listener() { override fun onRequestFinished(requestInfo: RequestFinishedInfo) { val metrics requestInfo.metrics ?: return val sb StringBuilder() sb.append(url).append(requestInfo.url) sb.append(finishedReason).append(requestInfo.finishedReason) metrics.connectionInfo?.let { conn - sb.append(protocol).append(conn.protocol) sb.append(remoteIp).append(conn.remoteIp) sb.append(rtt).append(conn.rtt) } sb.append(ttfb).append(metrics.ttfbMs) sb.append(total).append(metrics.totalTimeMs) Log.d(CronetReuse, sb.toString()) } }) .build()这里有一个判断技巧如果连续访问同一个域名多次前面一两次的TTFB明显高于后面几次且后面的远端IP没有变化那基本可以判定连接复用生效了。反过来如果每个请求的TTFB都很平均地高或者远端IP每几次就换一次那就要怀疑连接复用压根没匹配上。记得给采样上报加上开关。线上全量打日志会把输出打爆我一般只在Debug包或者线上特定版本开一个小比例采样定位问题足够了。3. 用NetLog把连接的一生看个清清楚楚3.1 开启NetLog拿到最底层证据如果说代码层是“看体检报告”那NetLog就是“看监控录像”。NetLog是Chromium网络栈自带的事件日志系统Cronet把它暴露给了Android。开启方式很直接val builder CronetEngine.Builder(this) builder.enableNetLog( File(filesDir, cronet_netlog.json).absolutePath, 50 * 1024 * 1024 ) val engine builder.build()这个方法的第一个参数是日志输出路径第二个参数是日志文件的最大体积。看效果的话建议开到几十MB起步不然高并发场景日志很快就截断了。需要注意的是不同版本里enableNetLog的接口有过变化有些版本还在Builder上有些则挪到了别的扩展方法而且这个方法主要是给排障用的线上不建议长期开启否则磁盘写入有点疼。日志文件是JSON格式直接用文本编辑器打开也能看但事件太多人眼根本扫不过来。最好用Chromium的netlog viewer也就是chrome://net-export那个工具加载它会按时间线把所有事件渲染出来还可以按事件类型过滤。3.2 在NetLog里找“复用”的关键事件NetLog记录的是底层事件流里面有几个关键事件直接对应连接复用的判断。最核心的是URL_REQUEST_START_JOB。这个事件发生在请求真正开始创建网络任务时它的参数里经常会带一个叫做reused_socket的字段。如果这个字段为true说明这次请求复用了已经存在的Socket为false则说明新建了一条连接。这个字段可以说是我判断连接是否复用最直接的依据。除此之外请求事件和连接池事件之间还有source_dependency关系。你会看到一个URL_REQUEST的source_id依赖了一个HTTP_STREAM_JOB而这个HTTP_STREAM_JOB又可能依赖某个SOCKET_POOL事件。追踪这条依赖链你就能知道当前请求到底绑在哪条连接上。举个我实际排查过的例子线上反馈某个域名的请求经常出现高延迟我抓了NetLog一看URL_REQUEST_START_JOB里reused_socket字段大量出现false。进一步顺着source_dependency看每个请求都新建了Socket而且新建完很快就因为某些原因被回收了。这就不是复不复用的问题而是连接根本活不过一个请求生命周期后面排查发现是服务端主动关闭了连接客户端还没来得及复用就被断掉了。3.3 不同协议下的NetLog事件差异很多同学拿到NetLog后不知道看什么主要是因为HTTP/1.1、HTTP/2、QUIC的事件名称不一样。这里我列一张典型的对照表方便你按协议类型快速定位协议常见事件复用时的表现HTTP/1.1SOCKET_POOL_ADD_SOCKET、SOCKET_CONNECT、SOCKET_CONNECTED后续请求依赖同一SOCKET_POOL的socket出现SOCKET_POOL_RESERVED_SOCKET_IDHTTP/2HTTP2_SESSION_POOL_INITIALIZED、HTTP2_STREAM_UPDATE_STREAM_ID请求依赖同一个HTTP2_SESSION新增的是stream_id而不是新连接QUICQUIC_SESSION_CONNECTED、QUIC_SESSION_POOLED请求依赖同一个QUIC_SESSION连接层不重建RTT事件能看出来我一般不会逐条看NetLog而是用脚本先按source_dependency做聚合找出每个请求依赖的最底层Socket或Session的ID然后看这些ID在不同请求之间有没有复用。如果10个请求里9个依赖同一个Socket ID连接复用率就是90%。这个思路不依赖具体字段名换成什么协议都好使。4. 指标反推法不依赖API也能判断连接复用4.1 连接建立次数 vs 请求次数有时候你只是想快速评估“连接复用”在线上整体做得怎么样没必要给每个请求都上NetLog那样太费了。最简单有效的方法是统计连接建立次数和请求次数的比例。怎么统计连接建立次数Android端最常见的做法是抓包tcpdump或者Wireshark看TCP握手报文。如果你只是想验证某一个测试场景手动抓包是最直观的连续发10个HTTP请求看抓包里出现了几次SYN包。出现1次那说明连接全部复用出现10次那说明每个请求都在重建连接。这只适用于HTTP/1.1和HTTP/2场景。QUIC没有TCP握手得看UDP层面有没有新的QUIC握手包。相比之下HTTP/2的判定更容易因为一个TCP连接内的请求都带相同四元组看报文源端口就够了。有人会问能不能在业务代码里统计连接建立次数理论上可以通过反射或者Hook拿到Cronet内部状态但那是内网API很不稳定Cronet升级一次可能就要改一次。我的建议是线上不要硬刚把NetLog采样做一个轻量统计脚本每次采样500条请求统计一下同样协议同样远端IP下不同Socket ID的数量基本就能算出复用率了。这个方法比反射稳定太多。4.2 用耗时分布定位异常连接复没复用在耗时分布上有一个非常明显的特征首次请求要经历DNS、TCP握手、TLS握手耗时会比较长后续复用连接的请求省掉握手阶段TTFB会明显下降。所以你把同域名请求按“首次”和“非首次”分组对比分组TTFB的P50和P90如果P50差距不到10ms那说明“每次都在建连”复用可能失效如果P90差距明显但P50很小说明大多数请求拉平了只有部分连接重建导致尾部变差。我有一个线上监控的实践针对某个核心域名把每个请求是否“可能复用”定义为首字节时间低于该域名P25的一个阈值。低于阈值就标记为“疑似复用”高于阈值标记为“疑似新建连接”然后按小时统计疑似复用比例。这个指标虽然不能替代NetLog但作为线上趋势监控非常管用能第一时间发现服务端调整连接超时参数导致复用率暴跌的情况。4.3 从Cronet的trace事件和内部指标辅助定位Cronet还提供了一些实验性的trace和性能指标能力。这些能力在不同版本里差异很大有的通过ExperimentalCronetEngineBuilder开启有的需要编译期的flag打开普通业务方很难直接使用。我在排查时用过一种讨巧的办法结合Cronet自带的Bandwidth和RTT estimation接口做一个“连接健康度”标记。思路是这样的Cronet会根据历史请求维护RTT和带宽估计。如果一个请求的RTT变化很小带宽估计也稳定说明它大概率走的是一个稳定的连接状态如果RTT突然跳高一个量级那很可能是新建连接或者路径变了。这个方法不精确但在没有NetLog的情况下能辅助判断连接复用的稳定性。真正需要精确定位时还是回到NetLog用脚本做事件聚合。以下是我常用的一段Python脚本逻辑解析NetLog里的URL_REQUEST_START_JOB事件提取每个请求的reused_socket、source_dependency和连线然后用集合去重计数。import json with open(netlog.json, r) as f: data json.load(f) events data[events] url_requests [] for event in events: if event.get(type) URL_REQUEST_START_JOB: params event.get(params, {}) url_requests.append({ id: event[source][id], reused_socket: params.get(reused_socket), dep: params.get(source_dependency), }) reused [r for r in url_requests if r[reused_socket] is True] new_conn [r for r in url_requests if r[reused_socket] is False] print(f总请求: {len(url_requests)}, 复用socket: {len(reused)}, 新建连接: {len(new_conn)})注意NetLog的JSON结构在不同Cronet版本里略有差异不保证脚本每个版本都能直接跑通但思路是一致的找事件看复用标志聚合统计。写一遍以后换版本修下字段路径就能接着用。5. 判断连接复用时最容易踩的坑5.1 QUIC连接迁移让“连接ID不变”变成假象如果你判断连接复用的唯一依据是QUIC Connection ID那你就掉进了一个大坑。QUIC设计了一个很优秀的特性叫连接迁移它允许Connection ID保持不变但底层网络路径、甚至网络类型WiFi和蜂窝之间切换发生变化时连接依然可以存活。从业务层看请求确实复用了同一个QUIC session但你关心的“连接”可能已经悄悄换了一条物理通路。在NetLog里连接迁移通常表现在QUIC_SESSION_CONNECTED之后又出现了网络路径变化相关的事件RTT值也会跟着突变。所以我判断QUIC场景下的连接复用不会只盯着Connection ID而是会同时看RTT和路径信息。如果你发现了Connection ID没变但RTT显著升高的现象不要轻易下“复用正常”的结论要排查一下是不是连接迁移触发了路径切换。5.2 HTTP/2的多路复用容易把Stream当成连接这是我在团队里被问过最多的一次。有同事拿着抓包结果说“你看这个请求复用了连接stream_id是7上一个请求是5下一个又变成9这不是复用得挺好的吗”我一看这只能说明HTTP/2在同一TCP连接上开了多个Stream根本不等于每个HTTP请求都是一次“新建连接”。Stream是复用TCP连接之上的逻辑流Stream ID变化是正常的不代表TCP连接发生了重建。判断HTTP/2的连接复用要看的是TCP四元组和TLS会话不是Stream ID。在NetLog里也要注意HTTP2_SESSION_POOL_INITIALIZED事件只代表Session初始化不代表每个请求都新建连接。正确的方法是统计请求依赖的HTTP2_SESSION ID是否一致不能依赖Stream ID。5.3 连接池的空闲策略和过期时间造成的误判即使你抓到了复用的证据也不能保证下一次请求还能复用到。服务端和客户端都有自己的连接空闲和过期策略。Chromium网络栈会关闭空闲太久的连接服务端也会根据Keep-AliveTimeout主动断连。于是你可能会遇到一种奇怪的规律前5个请求全部复用连接第6个请求突然reused_socket变为false后面又恢复复用。这种周期性断连不是Cronet的bug而是空闲连接被清理了。如果你在统计连接复用率时没有按时间窗口细分这种周期性波动会被平均掉很难发现问题。我的做法是把采样NetLog按分钟切片分片统计每个窗口内新建连接数量。如果新建连接数量呈现固定周期波动就去看服务端的空闲连接超时参数往往一找一个准。5.4 版本升级导致的判断逻辑失效Cronet是Chrome团队维护的版本迭代快API变化也快。我在项目里就遇到过这么一次旧版本里NetLog的URL_REQUEST_START_JOB事件里还能看到reused_socket字段升级到新版本后这个字段被改名了脚本直接解析不到数据。更麻烦的是RequestFinishedInfo.ConnectionInfo在不同版本里的字段也不完全一样之前的粗筛代码在新版本里拿不到RTT。所以在做连接复用判断的过程中数据的准确性是以“和当前Cronet版本匹配”为前提的。我的经验是每次升级Cronet后都要先跑一遍解析脚本看看字段路径有没有变化同时保留升级前的NetLog样本方便对比解析结果。别图省事这一步跳过了后面排查问题时会把自己坑死。5.5 多域名和DNS轮询下的连接匹配最后提一个容易被忽略的问题Cronet的连接复用是按“地址组”来匹配的简单理解就是同一个host、同一个IP、同一个端口才可能复用连接。如果你的域名开启了DNS轮询同一个域名解析出来的IP每次都不一样连接复用率就会很低。从NetLog里看你会发现URL_REQUEST_START_JOB里出现新的IP即使前面刚发过这个域名的请求也没法复用老连接。这种情况不是Cronet能解决的得从业务侧做改造比如接入HTTPDNS和IP端口直连把域名解析稳定下来才能让连接复用真正生效。判断的时候也不能只看域名复没复用必须把IP也纳入对比。我之前就吃过这个亏盯着域名看了半天发现IP一直在变回过头才意识到是DNS轮询在捣乱。个人用这套方法排查下来最大的感受是连接复用不是一次配好就完事的静态指标而是一个受客户端、服务端、网络路径共同影响的动态过程。有效判断的核心不是某个单一API而是把代码层、NetLog、指标反推三条路结合起来互相印证。尤其是线上问题我一般先用指标监控抓趋势再用RequestFinishedInfo粗筛样本最后用NetLog精确定位整个链路跑下来基本上不会误判。如果你正在做Cronet接入建议提前把NetLog解析脚本写好版本升级时也顺手更新一下等线上真出问题的时候你就能比别人快一步。这套判断连接复用的经验至少在目前我接触过的Cronet版本里是通用的希望对你有帮助。
返回列表