Skip to content

镜像与源健康度 ​

启动器只改写 Mojang 官方域名的 URL,别的域名(Modrinth、Fabric、CurseForge)一概原样放行。这一页讲改写规则、竞速池的组成、以及最关键的一件事:能不能分片必须按通道判。

只改写 7 个前缀 ​

csharp
private static readonly string[] MojangPrefixes =
[
    "https://piston-meta.mojang.com",
    "https://piston-data.mojang.com",
    "https://launchermeta.mojang.com",
    "https://launcher.mojang.com",
    "https://resources.download.minecraft.net",
    "https://libraries.minecraft.net",
    "https://libraries.mojang.com",
];

用前缀匹配而不是域名匹配,是为了路径也一起对得上——镜像把 piston-meta 的路径原样挂在自己的根下,resources.download.minecraft.net 挂到 /assets,libraries.minecraft.net 挂到 /maven。这套规则是 BMCLAPI 风格;FastMinecraftMirror 拆到独立子域。

镜像清单 ​

自动模式下的候选镜像是三个:Bmclapi / Fastmirror / Custom。Custom 只在用户填了自定义镜像地址时入池。

RainYun 不在自动候选里

SourceKind.Rainyun 这个类型和改写规则都保留着,用户显式选它依然生效——但它不在自动候选里。原因不是慢:它的镜像上根本没有任何 Minecraft 文件,请求返回的是站点 HTML,元数据请求拿到 HTML 会直接 JSON 解析失败,比「慢」糟糕得多。

竞速池怎么排 ​

健康镜像 → 降级镜像 → 官方兜底,官方恒为最后一个:

csharp
public IReadOnlyList<SourceKind> RacingPool()
{
    if (Pinned() is { } pinned) return [pinned];
    var pool = mirrors.OrderBy(k => Get(k).Degraded ? 1 : 0).ToList();
    pool.Add(SourceKind.Official);
    return pool;
}

用户显式指定单一源时池里只有那一个,不受降级影响——否则用户会被自己选的坏源锁死。降级只是排后不是删除,误判代价有限。

非 Mojang 域名的池只有一个源:

csharp
public IReadOnlyList<SourceKind> RacingPoolFor(string url) =>
    IsMojangUrl(url) ? RacingPool() : [Pinned() ?? SourceKind.Official];

非 Mojang 域名不会被任何镜像改写。沿用镜像池就会得到 5 个同址的「源」:失败重试时把无关镜像记成不健康,误导源排序。

降级阈值是 2,不是 5 ​

csharp
internal const int DegradeAfterFailures = 2;

旧 Rust 版是 5,实测结论是 2。理由很直接:不可达镜像的连接超时约 3.7 秒一次,阈值 5 意味着每个新进程的头几个分片要白等十几秒(自动模式实测 31.1 秒 vs 仅官方 3.7 秒)。误降级风险由一次成功立即恢复。

关键:分片能力按通道判 ​

这是整套镜像逻辑里最容易错的一处。同一个镜像在不同通道上的分片能力可以完全不同:实测(2026-09-22)BMCLAPI 的 /maven(库)节点一律无视 Range 返回整文件,而它给客户端 jar 分片的节点支持 206。

所以健康度里存的是按通道的计数,通道由 URL 前缀决定,判定粒度是「源 + 通道」:

csharp
public bool NoRangeSupport(SourceKind k, SourceChannel channel) =>
    Get(k).RangeIgnored.TryGetValue(channel, out var hits) && hits >= RangeIgnoredThreshold;

WARNING

把通道混为一谈(按源一刀切记「这个镜像不支持 Range」),后果是把 BMCLAPI 的库文件全部打成单连接——/maven 不分片,客户端 jar 却白白受牵连。

RangeIgnoredThreshold = 3 ​

csharp
internal const int RangeIgnoredThreshold = 3;

镜像是 302 到随机 P2P 节点,个别节点不认 Range 很正常。一次就判死会白丢一个快源——那个源下次可能就 206 了。所以连续 3 次同一通道无视 Range 才算它不能分片;拿到 206 就清零计数:

csharp
public void RecordRangeHonored(SourceKind k, SourceChannel channel) =>
    Get(k).RangeIgnored.TryRemove(channel, out _);

「源忽略 Range」不消耗重试额度——它是源的能力问题,不是本次请求失败:

csharp
if (resp.StatusCode == HttpStatusCode.OK)
{
    _sources.RecordRangeIgnored(kind, channel);
    return new ChunkFetchResult(false, 0, 0);   // 立即换源
}

主机级 Range 记忆 ​

Range 能力按主机记,不按源:非 Mojang 域名下所有源都改写成同一个 URL,按源记忆会让同一台 CDN 被反复试探。ProbeRangeAsync 只在明确的 206 / 200 时写记忆,网络故障返回 (0, false) 但不写——探测失败不等于源不支持分片。

质量预检 ​

进程内第一次分片前,对每个候选镜像发一个 1 字节 Range 请求,并行做,每个源 4 秒封顶(实测不可达镜像连接超时约 3.7 秒,取 4 秒足够)。官方源不预检——它是兜底,没有更后面的源可换。

源质量在单机网络上很稳定,所以预检值得做:FastMinecraftMirror 的 TLS 一直是坏的、RainYun 返回站点页面,不预检的话它们和可用源一样排在前面,每个大文件都要先浪费一轮重试才轮到官方。

相关链接:下载引擎总览、分片与竞速

基于 MIT 许可证(有修改)开源