服务:kvm内steam下载速度分析

问题

KVM 虚拟机内的 NixOS 使用宿主机的 mihomo 作系统代理下载 Steam 游戏,观测到下载速率在 10~30 MB/s 之间波动。需要判定:这些字节究竟是走代理出去的,还是直连。

本次为只读分析:仅调用 mihomo 的只读 API(/connections、/proxies、/providers/proxies),未修改任何配置、未重启任何服务。

结论

100% 直连(DIRECT),经代理 0 字节。两个独立测量窗口的归因结果完全一致。

承运方全部是国内 CDN;分流由规则 GEOSITE,category-games@cn → DIRECT 与 GEOSITE,steam@cn → DIRECT 命中。走代理的只有商店/页面的少量接口流量,字节量可忽略。

判定方法

第一步:先确认「是否经过 mihomo」

比较宿主网卡全局吞吐与 mihomo 自身计数器:

宿主网卡 rx23.90 MB/s
mihomo downloadTotal 增量22.58 MB/s

两者接近 ⇒ 虚拟机全部流量确实穿过 mihomo。所以「经不经 mihomo」不是问题,真正的问题是流量被哪条出口链终结。

第二步:用 mihomo 连接表 + chains 字段归因

GET http://127.0.0.1:9091/connections 返回的每条连接都带 chains 字段:

  • ["DIRECT"] → mihomo 用宿主机本机网络出境,即直连
  • ["节点名","组名","节点选择"] → 经代理节点出境

⚠ 关键坑:必须「全速轮询」才能归因完整

归因靠「逐连接累加 download 增量」,而覆盖率高度依赖轮询频率:

轮询方式覆盖率表现
每 0.6 s 采样一次6%只看到个别慢速长连接,结论完全失真
全速轮询(约 294 次/秒)100%归因总量与 mihomo 全局计数吻合

原因:Steam 下载跑在大量「采样间隔内开完就关」的短连接上,慢采样会把它们整段漏掉。必须去掉 sleep、连续轮询,覆盖率才能到 100%。

实测数据(两个独立窗口)

窗口mihomo 全局 ↓承运主机速率出口链
#1(20 s)21.84 MB/sfiles.steam.nsclouds.cn19.82 MB/sDIRECT
xz.pphimalayanrt.com2.00 MB/sDIRECT
— 经代理合计 —0.00 MB/s—
#2(10 s)23.87 MB/sfiles.steam.nsclouds.cn22.02 MB/sDIRECT
xz.pphimalayanrt.com1.83 MB/sDIRECT
— 经代理合计 —0.00 MB/s—

归因总量 21.83 / 23.86 MB/s ≈ mihomo 全局计数 ⇒ 归因完整。两窗口均为 DIRECT ≈ 100%、PROXY = 0。

承运方与解析

承运域名实测速率解析结果性质
files.steam.nsclouds.cn19.8~22.0 MB/s39.137.99.115(bjxlv4.y.ngaagslb.net)国内 CDN
xz.pphimalayanrt.com1.8~2.0 MB/sCNAME → m.alikunlun.com阿里云 CDN

命中规则

- GEOSITE,category-games@cn,DIRECT
- GEOSITE,steam@cn,DIRECT

那走代理的是什么

Steam 的 *.steamstatic.com 系(clan. / store.fastly. / community.fastly. / shared.)走代理,但累计字节只有 0.3~2 MB、速率 ≈ 0 —— 属于商店页面 / 接口类流量。

⇒ 「内容下载直连、商店浏览走代理」的分流正在正常工作。

含义

  1. 下载速率不受机场带宽 / 代理节点限制,由国内 CDN 决定;10~30 MB/s 的波动是 CDN 侧行为。
  2. 不吃订阅流量配额 —— Steam 下载不计入套餐月流量。
  3. 虚拟机内「系统代理」只决定规则分流;命中 @cn 规则族的流量实际仍走本机网络出境。

复现方法

全速轮询连接表并归因(只读,可随时重跑):

import json, urllib.request, time, collections
last = {}; agg = collections.defaultdict(int)
END = time.time() + 20
while time.time() < END:
    d = json.load(urllib.request.urlopen("http://127.0.0.1:9091/connections", timeout=10))
    for c in d.get("connections") or []:
        cid = c["id"]; m = c.get("metadata", {})
        host = m.get("host") or m.get("destinationIP")
        ch = tuple(c.get("chains") or [])
        kind = "DIRECT" if ch == ("DIRECT",) else "PROXY"
        dl = c.get("download", 0)
        if cid in last: agg[(host, kind)] += max(0, dl - last[cid])
        last[cid] = dl
for (h, k), b in sorted(agg.items(), key=lambda kv: -kv[1])[:10]:
    print(b, h, k)

注意:不要加 time.sleep(),否则覆盖率会掉到个位数百分比。

边界

  • 判定依据是 mihomo 自报的 chains 字段(["DIRECT"] = 用宿主机本机网络出境)。
  • 测量窗口为瞬时段(20 s / 10 s),结论是「当下这一时刻的下载」;Steam 若切换到境外 CDN,分流结果可能不同,需重跑复现脚本确认。
  • 本次只回答「走了哪条出口」,不涉及限速、QoS 或 CDN 选路优化。