Prhub

#2389 fix: avoid Mooncake metrics port collisions

原始 PR 作者 guapisolo 合并时间 2026-08-13 15:32 文件变更 2 提交数 3 评论 0 代码增减 +17 / -1

执行摘要

默认 metrics 端口改为 0,避免 Mooncake 启动端口碰撞

PR body 描述了完整症状与复现路径:Mooncake 启动时 RPC 端口 50051 正常,但 admin 端口 50052 绑定失败即退出,launcher 报 RPC readiness timeout;复现方式是先 hold 住 127.0.0.1:50052 再启动 mooncake_master。根因是 MOONCAKE_MASTER_METRICS_PORT 把每次启动都固定到 50052,任何其他监听者都能抢在 Mooncake 绑定之前占有该端口。修复思路是让 OS 分配端口,而不是在固定端口上等待重试。

值得快速浏览:这是一个教科书式的“固定端口 → 端口 0 原子分配”修复,改动极小但解决了多进程环境下的真实启动故障。值得关注的设计点是让就绪判定与可选项端口解耦、只等待核心 RPC 端口,以及用测试锁定默认命令格式。若团队有依赖 50052 拉取指标的自动化,需要同步排查适配。

讨论亮点

PR 没有线上 review 评论,Shi-Dong 直接 APPROVED。真正有价值的取舍体现在三个 commit 的演进中:第二个 commit 移除了新增测试,理由是当时 CPU CI 失败与 GPU W&B 认证问题混在一起;第三个 commit 恢复测试,并明确说明该测试在 CPU CI 已通过、与 W&B 认证失败无关。最终保留了这条回归保护,说明作者经历了“先缩小嫌疑面、再还原保护”的判断过程。

实现拆解

  1. 修改常量 MOONCAKE_MASTER_METRICS_PORT 的默认值,从 50052 改为 0(miles/utils/external_utils/command_utils.py),使 start_mooncake_master 默认把 --metrics_port 0 传给 mooncake_master,由内核在 bind 时原子分配空闲 admin 端口,消除固定端口的竞争窗口。
  2. 保持 RPC 端口 50051 固定:_is_tcp_server_ready 与 wait_for_server_ready 的就绪判定仍只看 RPC 端口,metrics 端口动态化不会影响启动等待逻辑,调用方拿到的“就绪”语义不变。
  3. 新增回归测试 test_lets_the_os_choose_the_metrics_port(tests/fast/utils/test_command_utils.py),验证默认启动命令包含 --metrics_port 0,且等待目标仍是 ("127.0.0.1", 50051);原有显式传端口测试 test_restarts_and_waits_until_ready 保持 50152 显式传递,确保覆盖行为不被破坏。
  4. 提交历史显示测试曾一度被移除(第二个 commit)后又恢复(第三个 commit),最终确认该测试与 GPU W&B 认证失败无关,保留为回归保护。
  5. 验证:pytest -q tests/fast/utils/test_command_utils.py 共 71 个用例通过;人工端口占用 smoke 测试中,50052 被占用时 Mooncake 绑定到 41935,/metrics 返回 HTTP 200。
文件 模块 状态 重要度
miles/utils/external_utils/command_utils.py 命令工具 modified 4.89
tests/fast/utils/test_command_utils.py 单元测试 modified 4.82

关键符号

start_mooncake_master test_lets_the_os_choose_the_metrics_port

关键源码片段

miles/utils/external_utils/command_utils.py core-logic

核心修复点:MOONCAKE_MASTER_METRICS_PORT 默认值从固定 50052 改为 0,让 OS 原子分配 admin 端口,start_mooncake_master 的就绪判定仍只依赖固定 RPC 端口。

# 固定 RPC 端口保持不变,metrics/admin 端口默认交由 OS 分配
MOONCAKE_MASTER_PORT = 50051
MOONCAKE_MASTER_METRICS_PORT = 0 # 0 表示由内核在 bind 时原子分配空闲端口
MOONCAKE_MASTER_LOG_PATH = Path("/tmp/mooncake_master.log")
​
​
def start_mooncake_master(
    rpc_port: int = MOONCAKE_MASTER_PORT,
    metrics_port: int = MOONCAKE_MASTER_METRICS_PORT,
    timeout: float = 30,
    log_path: str | Path = MOONCAKE_MASTER_LOG_PATH,
) -> None:
    host = "127.0.0.1"
    # 若 RPC 端口已在监听,说明 master 已就绪,直接复用,避免重复拉起
    if _is_tcp_server_ready(host, rpc_port):
        print(f"Mooncake master is already ready at {host}:{rpc_port}", flush=True)
        return
​
    # 先清理可能残留的旧进程,再异步启动;metrics_port 传 0 时由内核原子分配空闲端口
    cmd = (
        f"pkill -x mooncake_master; "
        f"mooncake_master --rpc_port {rpc_port} --metrics_port {metrics_port} "
        f"> {shlex.quote(str(log_path))} 2>&1 &"
    )
    exec_command_cpu(cmd)
    # 阻塞等待 RPC 端口就绪;metrics 端口不再参与就绪判定,与启动参数解耦
    wait_for_server_ready(host, rpc_port, timeout=timeout)
tests/fast/utils/test_command_utils.py test-coverage

新增回归测试 test_lets_the_os_choose_the_metrics_port,锁定默认启动命令携带 --metrics_port 0 且仍等待固定 RPC 端口,防止后续改动回退。

def test_lets_the_os_choose_the_metrics_port(self, monkeypatch):
    """Binding port zero atomically avoids collisions with other listeners."""
    commands = []
    waits = []
    # 强制认为 RPC 端口不可用,才会真正执行启动命令
    monkeypatch.setattr(command_utils, "_is_tcp_server_ready", lambda host, port: False)
    monkeypatch.setattr(command_utils, "exec_command_cpu", commands.append)
    monkeypatch.setattr(
        command_utils, "wait_for_server_ready", lambda *args, **kwargs: waits.append((args, kwargs))
    )
​
    command_utils.start_mooncake_master()
​
    # 只拉起一次,且命令里必须带 --metrics_port 0
    assert len(commands) == 1
    assert "mooncake_master --rpc_port 50051 --metrics_port 0" in commands[0]
    # 就绪等待仍只看固定的 RPC 端口,与 admin 端口无耦合
    assert waits == [(("127.0.0.1", 50051), {"timeout": 30})]

评论区精华

新增测试该不该保留 测试

第二个 commit 移除了本 PR 新增的 metrics 端口测试,第三个 commit 又将其恢复;commit message 说明移除原因是误以为与 GPU W&B 认证失败有关,恢复时确认该测试在 CPU CI 已通过、与 W&B 失败无关。

结论:保留测试,作为默认启动命令的回归保护。 · 已解决

风险与影响

  1. 默认端口行为变化:metrics 端口不再固定为 50052,任何依赖固定端口抓取 /metrics 的外部监控或脚本需要改为读取实际分配端口,或显式传入 --metrics_port。
  2. 就绪判定不等待 metrics 端口:start_mooncake_master 只等待 RPC 端口,不等待 admin 端口就绪;由于端口 0 在 bind 时已分配,实际可用性风险很低,但调用方在 RPC ready 后立即访问 metrics 仍可能遇到瞬时未监听窗口。
  3. 测试覆盖局限:新增测试只断言命令字符串与等待参数,没有做真实端口绑定集成测试;端口 0 依赖 OS/内核语义,跨平台行为可能不同,不过 Mooncake 目标环境以 Linux 为主。
  4. 显式覆盖路径不受影响:test_restarts_and_waits_until_ready 仍验证 50152 显式传递的场景,兼容性有保障。

影响范围集中在通过 start_mooncake_master 默认启动 Mooncake master 的训练任务与 launcher,尤其是多实例或残留进程场景下 50052 冲突导致的启动失败;RPC 端口语义不变,客户端寻址不受影响。对用户而言启动更健壮,但使用固定 metrics 端口的监控需要适配;对团队而言是单行常量加单条测试的小改动,回归风险低。

默认端口行为变更 依赖固定 50052 的监控需适配 就绪判定不等待 metrics 端口 缺少真实绑定集成测试

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论