既然 Pod 会重建,就很难记住它的地址。Service 在这组 Pod 前面固定一个名字,让调用方不因 Pod 更换而受影响。

一个地址,一组 Pod

普通 ClusterIP Service 在对象存续期间保持虚拟地址,集群 DNS 提供发现名称。调用方可以依赖这个名称,而后端可互换的 Pod 逐渐被替换。这把稳定的客户端契约与单个后端生命周期分开,不需要每替换一个副本就推送客户端配置。但稳定身份仍有明确范围,不能扩写成全部网络行为都不会变化。

无头和 ExternalName Service 具有不同发现行为,无选择器 Service 则可以使用单独管理的 EndpointSlice。虚拟地址稳定的结论应限于普通情形。Deployment 管理工作负载,本身不是发起网络连接的客户端;真正使用发现结果的是应用、网关和其他组件。审阅调用关系时,应区分资源控制与请求转发这两层。

选择规则决定由谁响应

带选择器的 Service 识别本命名空间里匹配的 Pod,EndpointSlice 控制器异步协调地址和条件。就绪通常决定普通流量资格,但未就绪端点可能继续存在,只是就绪条件为假,不一定从 API 对象中彻底删除。列表成员、成员条件和最终是否选择它,是需要分别观察的状态。

选择器不会授权网络访问,标签匹配也不证明 targetPort 正确或应用能返回有用响应。publishNotReadyAddresses 与终止中端点处理还会影响解释。需要先核对选中集合及条件,再检查实际数据路径;这些观察相关,却不能互相替代。未被一个 Service 选中的 Pod,也可能通过其他入口被访问,不能把发现名单当成隔离策略。

流量总给当前匹配的

扩缩容和替换可以在同一名称背后进行,但名称不保证请求连续。已经连接到某个后端的会话,可能因后端终止而失败;更新传播到代理需要时间,没有可用端点时,即使 DNS 仍能解析也不能服务。稳定地址并不负责迁移已建立连接,更不会自动补足被删掉的副本容量。

应联合规划就绪、更新容量、终止排空和客户端重试;非幂等操作还需要应用层保证重复请求安全。替换期间要实际观察可用性,不能宣称选择器足够精确就没有任何中断。下方本地模型只演示集合选择,不验证传播时间和传输行为,也不证明一次集合替换就等于完成无损发布。

稳定名字是对调用方的承诺

应把 Service 名称当成受支持的发现接口,而不是缓存某次看到的 Pod IP。使用普通 Service VIP 的调用方,不需要每次后端变化都重新解析,但永远只在启动解析一次,会在 Service 删除重建、地址变化时失败。应按客户端库的行为处理解析与连接生命周期,不能把后端稳定抽象误读成无限缓存许可。

无头服务的答案可能包含变化的端点地址,因此调用方需要自己的发现与均衡行为。要测试库如何刷新 DNS、替换连接和处理过期结果。稳定是带生命周期条件的接口契约,不是关闭刷新或忽略失败的理由;名称继续存在、地址仍有效、连接仍可用,也不是完全相同的三个判断。

坏掉的样子

最安静的故障是选择器谁也选不中。比如把 app: web 写成 app: webs,或者 Deployment 不再给 Pod 打预期标签,Service 的端点集合就会变成空。此时集群 IP 与 DNS 名字依然能解析,故障表现为连接被拒绝或超时,而不是名字不存在。当 Service 能解析却送不出流量时,先看它的端点集合:空的可用集合可能来自标签、就绪、Pod 地址或协调进度,本身也不能排除另有网络问题。

第二个常见陷阱是顺手写成的无头 Service。一旦设置 clusterIP: None,虚拟 IP 就不存在了,DNS 直接返回不断变化的 Pod 地址,每个客户端都要自己做负载均衡。为单一稳定入口编写的代码,突然要面对多个漂移地址,重试不均、连接复用失效都会跟着来。只有调用方明确想要服务发现时才用无头形式,不要把它当成默认写法。

端口配错是从另一侧打破承诺。Service 映射到错误的容器 targetPort,就无法把请求交给预期监听者;手选一个已经分配的 NodePort 会让 API 操作失败。ClientIP 亲和可能把共享来源地址的客户端集中到同一后端,但不表示永久绑定已经移除的端点,也不表示每次后端失败都会变成全局中断。应根据应用需求选择亲和,并单独验证故障切换。

快速验证

把下面程序保存到临时工作目录的 service_endpoints.py,运行 python3 service_endpoints.py。它只用字典模拟 Service 端点收敛,不修改进程之外的任何东西:一个函数根据选择器、Pod 标签与就绪状态推导端点集合,四个场景依次演练它,其中故意留空的选择器是反例,用于证明空选择器必须告警,而不是悄悄匹配到奇怪的结果。

"""用选择器加就绪状态模拟 Service 端点收敛。"""

PODS = [
    {"name": "web-1", "labels": {"app": "web"}, "ready": True},
    {"name": "web-2", "labels": {"app": "web"}, "ready": True},
    {"name": "web-3", "labels": {"app": "web"}, "ready": False},
    {"name": "worker-1", "labels": {"app": "worker"}, "ready": True},
]


def select_endpoints(pods, selector):
    """推导端点集合;空选择器告警而不是默默匹配。"""
    if not selector:
        return [], "warning: empty selector matches nothing"
    matched = sorted(
        pod["name"]
        for pod in pods
        if pod.get("ready")
        and all(pod.get("labels", {}).get(key) == value for key, value in selector.items())
    )
    return matched, ""


def check(condition, message):
    if not condition:
        raise AssertionError(message)


def main():
    endpoints, warning = select_endpoints(PODS, {"app": "web"})
    check(endpoints == ["web-1", "web-2"], "就绪的 web Pod 应组成集合")
    check(warning == "", "精确的选择器不应告警")
    print("steady state:", endpoints)

    rolling = [dict(pod, ready=True) if pod["name"] == "web-3" else pod for pod in PODS]
    rolling = [pod for pod in rolling if pod["name"] != "web-1"]
    endpoints, _ = select_endpoints(rolling, {"app": "web"})
    check(endpoints == ["web-2", "web-3"], "滚动应以新就绪 Pod 替换旧 Pod")
    print("after rollover:", endpoints)

    endpoints, _ = select_endpoints(PODS, {"app": "web", "track": "stable"})
    check(endpoints == [], "过窄的选择器应收敛为空,而不是报错")
    print("over-narrow selector:", endpoints)

    endpoints, warning = select_endpoints(PODS, {})
    check(endpoints == [] and "empty selector" in warning, "空选择器必须大声告警")
    print("empty selector:", repr(warning))

    print("PASS: four convergences; readiness-gated selection, rollover, and empty warning hold")


if __name__ == "__main__":
    main()

四个场景验证的是这个本地选择模型:稳态时两个就绪的 web Pod 在集合里,未就绪者被挡在外面;滚动时选择器不变,集合从一个就绪 Pod 换成另一个;过窄的选择器会清楚地收敛为空。空选择器是反例:它大声告警,而不是悄悄匹配全部或悄悄返回空。本轮在 Linux、Python 3.12.14 上运行两种语言模型通过。空选择器告警是程序自己的保护;Kubernetes 可以通过显式管理的 EndpointSlice 支持无选择器 Service。

这个模拟只验证标签加就绪的收敛逻辑,不实现 kube-proxy、集群 DNS 与真实探针时序,也不覆盖生产环境的端口冲突。发布前还要对照 Pod 标签与就绪状态检查真实端点集合,并用真正的 Service 名字在两个语言环境里各走一遍查询。

延伸阅读

来源