习惯传统服务器的工程师进入 Kubernetes 后,会做他一直以来都在做的事:把 HTTP_PROXY 导出到环境里,重启进程,然后等着所有出站流量都走代理。有时这确实有效。但更常见的情况是,这会弄坏半个集群,而另一半流量仍然绕过代理直接访问互联网。然后最精彩的部分开始了:为什么有些 Pod 识别了环境变量,有些没有?为什么服务之间突然无法互相访问?为什么 healthcheck 突然走了不在信任列表里的代理服务器?

本文深入剖析 Kubernetes 集群中出站流量的真实机制,以及代理应该正确嵌入在哪里。我们不会复述环境变量的基础配置——这些你早就知道了。我们要谈的是 Kubernetes 的特殊性:那些习以为常的做法在这里会产生意想不到的效果。所有示例都来自真实的清单文件片段,代理基础设施选用 Proxeon(proxeon.net)。

基础:为什么集群里的情况完全不同

从根本说起。在传统服务器上,出站流量的概念很简单:一个网络接口,一张路由表,一组系统环境变量,几乎启动的所有进程都会从父 shell 继承它们。在 profile 里导出变量,所有用户进程都能看到。

Kubernetes 里没有这个统一入口。这里有 Pod——最小部署单元,里面运行一个或多个容器。Pod 有自己的网络命名空间、自己的 IP 地址、由清单文件定义的自己的环境变量集合。根本不存在一个可以继承的 shell。环境变量只有在 Pod 规范或镜像中显式声明时,才会出现在容器里。

什么是 Pod 出站流量

当容器访问外部地址时,数据包会走一条很长的路。首先从容器网络命名空间通过虚拟接口发出,然后进入节点的网络栈,由 CNI 插件(负责集群网络的组件)处理。接着 iptables 或 eBPF 规则介入,SNAT(源地址替换)机制生效,最后数据包通过节点网络接口进入外部世界。

关键在于:从外部服务看,请求不是来自 Pod 地址,而是来自集群节点地址。Pod 被地址转换隐藏了。这是新手尝试按 IP 白名单配置访问时遇到的第一个意外:他们把 Pod 地址加到白名单,但流量却来自完全不同的地址。

两种出站流量

从一开始就要区分两种根本不同的流量:

  • 东西向——集群内部服务之间的流量。一个 Pod 通过 Service、ClusterIP 或 my-service.namespace.svc.cluster.local 形式的 DNS 名称访问另一个。
  • 南北向——向外部的流量,访问外部 API、数据库、合作伙伴服务、对象存储。

几乎只有南北向流量需要代理。而最常见、最痛苦的错误,是代理配置不当,意外捕获了东西向流量,切断了内部通信。正因如此,这里的排除列表比任何地方都重要。稍后详谈。

深入剖析:代理所在的三个层级

要把代理嵌入 Pod 的出站路径,正好有三个架构层级。每个层级各司其职,各有代价。我们诚实地把三者讲清楚,包括优缺点。

层级 1:容器本身

也就是容器内的应用自己知道代理的存在。它读取 HTTP_PROXY 和 HTTPS_PROXY 环境变量,或者在自身配置里写死代理地址,将 HTTP 请求导向代理。代理逻辑内置在应用自己的客户端库里。

优点。起步时极简单,不需要向集群添加任何东西,不需要额外组件。可以精确到具体 Pod 控制:你清楚知道哪个应用访问哪里。

缺点。每个应用都必须支持读取这些变量,而远非所有应用都支持。配置分散在几十个清单文件里。更新代理地址意味着要遍历所有 Deployment。很容易漏掉某个服务,它就直接出去了。没有集中化策略。

层级 2:sidecar 容器

这里在同一个 Pod 中,与主容器一起启动第二个容器——sidecar。它拦截出站流量并转发给代理。应用甚至不需要知道代理存在:它照常发请求,sidecar 悄悄代理它们。Istio 和 Linkerd 这类服务网格就是这么工作的,按同样原理也可以放一个轻量本地代理 agent。

优点。对应用透明。通过 Pod 模板实现统一策略。可以在代理之上增加可观测性:指标、追踪、重试、超时。Sidecar 在 Pod 内隔离,与它共享生命周期。

缺点。额外开销:每个 Pod 现在有两个容器,意味着更多内存和 CPU。调试变复杂——链路中多了一环。容器启动顺序很重要:如果应用比 sidecar 先启动,最初的请求可能失败。Kubernetes 1.28+ 用 原生 sidecar 容器(作为 init 容器但 restartPolicy 为 Always)解决了这个问题。

层级 3:集群 egress 网关

这是一个专用节点或 Pod,集群所有出站流量被强制导向它。数据包通过 CNI 路由到 egress 网关,再由它与外部代理通信,或者它本身就是具有稳定源 IP 的出口点。

优点。整个集群的单一控制点。稳定、可预测的源地址,方便加入外部合作伙伴的白名单。策略在一个地方修改。应用什么都不用知道。

缺点。Egress 网关成为关键单点:挂了,整个南北向就断了。需要冗余和监控。配置更复杂,需要 CNI 支持。粒度更低:没有额外规则,很难对不同应用设置不同策略。

如何选择层级

经验给出的实用指引:小型项目只有几个服务需要外部代理——用容器层级。中型集群有几十个服务且要求统一策略——用 sidecar。大型基础设施,外部合作伙伴要求固定源 IP 并审计所有流量——用 egress 网关。通常还会组合使用:egress 网关做基础控制,加上环境变量通过 Proxeon 对个别 Pod 做精细调整。

清单文件中的环境变量与 NO_PROXY 的作用

现在进入最被低估的细节。清单文件里 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 通过容器规范的 env 块设置。经典命名习惯会同时用大写和小写,因为有些库只读小写版本。

apiVersion: apps/v1
kind: Deployment
metadata:
 name: worker
spec:
 replicas: 2
 selector:
matchLabels:
 app: worker
 template:
metadata:
 labels:
app: worker
spec:
 containers:
- name: app
 image: registry.example.com/worker:1.4.0
 env:
- name: HTTP_PROXY
 value: "http://gate.proxeon.net:8080"
- name: HTTPS_PROXY
 value: "http://gate.proxeon.net:8080"
- name: NO_PROXY
 value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.svc.cluster.local,.cluster.local,kubernetes.default"
- name: http_proxy
 value: "http://gate.proxeon.net:8080"
- name: https_proxy
 value: "http://gate.proxeon.net:8080"
- name: no_proxy
 value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.svc.cluster.local,.cluster.local,kubernetes.default"

为什么 NO_PROXY 配置不当会让集群崩溃

问题本质在这里。一旦你声明了 HTTP_PROXY,应用的 HTTP 客户端就会把所有请求都导向代理,包括访问集群内相邻服务的请求。而内部服务只在集群网络内可达——外部代理服务器物理上够不到它们。结果:对 orders.default.svc.cluster.local 的请求发到了外部代理,代理尝试解析这个名字,失败,返回错误。内部通信瞬间断裂。

NO_PROXY 是排除列表——需要绕过代理直接发送的地址和域名。普通服务器上通常放 localhost,也许还有几个内部子网。在 Kubernetes 里这个列表变得至关重要,因为它必须覆盖集群的全部内部拓扑。漏掉一个后缀,部分服务间流量就跑到外部代理去了。

集群 NO_PROXY 必须包含什么

  • localhost 和 127.0.0.1——Pod 内部的访问。
  • Pod CIDR 网段——Pod 地址所在子网,例如 10.0.0.0/8 或你集群更精确的网段。
  • Service CIDR 网段——Service 的 ClusterIP 子网,常见为 10.96.0.0/12。
  • 集群 DNS 后缀——.svc、.svc.cluster.local、.cluster.local。它们覆盖所有内部 DNS 服务名。
  • kubernetes.default——API 服务器名称,应用和 agent 会访问它。
  • 云元数据地址——169.254.169.254,如果在云环境中,确保元数据查询不走代理。

NO_PROXY 语法细节

这里藏着大量不明显的坑,连资深工程师都会踩。

第一,不同库对条目的解释不同。有些认为 .cluster.local 带前导点表示后缀,匹配所有子域。有些要求 cluster.local 不带点。实践做法:两种都写,带点和不带点,覆盖最多客户端。

第二,CIDR 表示法支持并不通用。Go 库理解 10.0.0.0/8,但其他语言的一些老版本客户端不支持,需要单独地址或其他格式的范围。测试你具体技术栈的行为。

第三,端口。NO_PROXY 条目如果不带端口,通常适用于该主机的任意端口。但有些客户端严格按端口匹配。内部服务使用非标准端口时,这点需要验证。

来自实践的洞察:代理上线后内部通信故障,十有八九是因为 NO_PROXY 不完整。为你的集群一次性制定一份标准列表,放进共享 ConfigMap,在所有 Deployment 中复用。这能省下几十小时的调试时间。

哪些东西不会读取环境变量

“我设了 HTTP_PROXY,所以所有流量都走代理了”——这种朴素信念在集群里错得离谱。有一整类组件直接忽略这些变量。必须提前了解。

kubelet 和系统组件

容器的环境变量只对该容器内的进程可见。kubelet——负责拉取镜像、启动容器、与 API 服务器通信的节点 agent——运行在节点层级,不是 Pod 层级。它不读清单文件里的 env。如果想让 kubelet 通过代理拉取镜像,要在 kubelet 的 systemd 服务或容器运行时配置里设置,而不是在 Pod 规范里。

# /etc/systemd/system/kubelet.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://gate.proxeon.net:8080"
Environment="HTTPS_PROXY=http://gate.proxeon.net:8080"
Environment="NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"

容器运行时也一样——containerd 或 CRI-O。镜像拉取走它们自己的网络上下文。镜像仓库的代理配置在运行时配置里,是独立于 Pod 的另一回事。

自带配置的镜像

许多流行镜像内置网络配置,会覆盖环境变量。例如镜像内的包管理器、Web 服务器或代理工具可能读取自己的配置文件,而不是环境。如果容器内的应用使用带有显式连接参数的配置文件,你的 env 变量它根本不会注意到。

单独一类是网络客户端在读取环境之前就初始化,或启动时缓存配置的应用。不重启进程,改变量没用。

个别 SDK 和语言运行时

这是最阴险的一类。对 HTTP_PROXY 的支持是约定,不是标准。有人遵守,有人不。

  • Go。标准 http.Client 通过 ProxyFromEnvironment 尊重变量。但如果代码显式设置 Proxy 为 nil 创建 transport,变量就被忽略。
  • Python。requests 库默认读环境。但底层 socket、部分 gRPC 客户端和异步库不读。
  • Java。JVM 使用自己的系统属性 http.proxyHost 和 https.proxyHost,默认完全不读环境变量。需要通过 JAVA_TOOL_OPTIONS 传递。
  • Node.js。内置 http 模块不读环境变量。需要第三方 agent 来读取。
  • gRPC。部分实现读专用变量 grpc_proxy,而非标准变量。

结论很简单:不能假设声明了变量,所有流量就会走它。每个运行时都要单独验证。例如 JVM 的清单文件是这样的:

env:
 - name: JAVA_TOOL_OPTIONS
value: "-Dhttp.proxyHost=gate.proxeon.net -Dhttp.proxyPort=8080 -Dhttps.proxyHost=gate.proxeon.net -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.svc|*.cluster.local"

注意:JVM 的排除分隔符是竖线,不是逗号,模板用星号。这是机械复制 NO_PROXY 的又一个坑。

密钥:代理凭据用 Secret,不要放清单文件

如果代理需要认证,就会面临凭据存储问题。诱惑很大:直接写进 env 值里的 URL。不能这么做,原因如下。

Deployment 清单文件几乎总是放在版本控制里。env 里明文密码意味着凭据泄露到 Git 历史,所有有仓库访问权限的人都能看到。此外,env 值对任何能执行 kubectl describe pod 的人可见。这直接违反最小权限原则。

正确做法是使用 Secret 对象。它单独存储敏感数据,可以通过 RBAC 限制访问,并在 etcd 存储中启用加密。

apiVersion: v1
kind: Secret
metadata:
 name: proxeon-credentials
type: Opaque
stringData:
 proxy-user: my_account
 proxy-pass: s3cr3t_token_value

然后通过 secretKeyRef 把 Secret 中的值接入容器环境变量,同时让代理 URL 本身不在清单文件里暴露凭据:

env:
 - name: PROXY_USER
valueFrom:
 secretKeyRef:
name: proxeon-credentials
key: proxy-user
 - name: PROXY_PASS
valueFrom:
 secretKeyRef:
name: proxeon-credentials
key: proxy-pass
 - name: HTTPS_PROXY
value: "http://$(PROXY_USER):$(PROXY_PASS)@gate.proxeon.net:8080"
 - name: NO_PROXY
value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"

Kubernetes 通过美元符号加括号的语法,从先前声明的变量插值。这样可以在运行时组装带凭据的 URL,不用把密码直接放进清单文本。注意:组装出的 HTTPS_PROXY 值仍然会在运行中容器的 env 里可见,所以还要限制谁可以对这些 Pod 执行 exec 和 describe。

Secret 使用良好实践

  • 启用 etcd 静态加密——否则 Secret 只以 base64 存储,算不上保护。
  • 通过 RBAC 限制 Secret 访问:Pod 只能看到自己的 Secret。
  • 定期轮换代理凭据,并自动化轮换后重启 Pod。
  • 考虑外部密钥管理器,通过 CSI 驱动接入,让凭据根本不进 etcd。
  • 永远不要记录组装后的代理 URL——密码会泄露到日志系统。

Sidecar 方案:何时值得用,能带来什么

我们已经把 sidecar 列为三个层级之一。现在作为独立策略详细展开,因为如果你愿意为资源买单,它是最灵活的工具。

何时 sidecar 值得用

  • 应用不支持读取 HTTP_PROXY,改代码不可能或太贵。
  • 需要统一代理策略,不修改每个应用。
  • 需要透明流量拦截,包括非 HTTP 协议。
  • 需要可观测性:出站连接指标、追踪、审计。
  • 需要在连接层面做重试、超时、熔断策略。

Sidecar 除了代理还能带来什么

真正的价值在这里。Sidecar 不只是包转发器。配置得当,它变成 Pod 所有出站流量的控制和观测点。

  • 指标。发了多少外部请求、目标主机、延迟、错误数。没有 sidecar,这些数据得在每个应用里单独收集。
  • 追踪。与入站请求关联的出站调用分布式追踪。
  • 可靠性策略。幂等请求自动重试、超时、并发连接限制。
  • 统一 TLS。Sidecar 可集中终止和建立安全连接。
  • 审计与合规。应用访问目标的完整日志——合规不可或缺。

带 sidecar 代理的 Pod 示例

下面是简化模板,主容器把出站 HTTP 请求发到本地 sidecar,再由它通过 Proxeon 代理转发。对应用来说代理地址是 localhost,如果应用直接访问相邻服务,这自动把服务间流量排除在外部代理之外。

apiVersion: v1
kind: Pod
metadata:
 name: app-with-proxy-sidecar
 labels:
app: billing
spec:
 containers:
- name: app
 image: registry.example.com/billing:2.1.0
 env:
- name: HTTP_PROXY
 value: "http://127.0.0.1:3128"
- name: HTTPS_PROXY
 value: "http://127.0.0.1:3128"
- name: NO_PROXY
 value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"
- name: egress-proxy
 image: registry.example.com/egress-agent:1.0.0
 ports:
- containerPort: 3128
 env:
- name: UPSTREAM_PROXY
 value: "http://gate.proxeon.net:8080"
 resources:
requests:
 cpu: 50m
 memory: 64Mi
limits:
 cpu: 200m
 memory: 128Mi

原生 sidecar 与启动顺序

经典 sidecar 的痛点是启动竞争。如果主应用先启动,在 sidecar 能接受连接之前发出第一个出站请求,请求就失败。从 Kubernetes 1.28 起支持原生 sidecar 容器:在 initContainers 块中声明,restartPolicy 为 Always,保证在主容器之前启动并保持存活。这干净地解决了竞争问题,不用延迟或启动重试之类的补丁。

spec:
 initContainers:
- name: egress-proxy
 image: registry.example.com/egress-agent:1.0.0
 restartPolicy: Always
 ports:
- containerPort: 3128

Egress 网关:为集群提供稳定源地址

第三个层级值得单独讨论,因为它解决的正是通常让人们走进集群代理的原因:可预测的源 IP。

为什么需要固定源 IP

外部合作伙伴、支付网关、数据供应商经常按地址白名单工作。他们说:只接受来自这些 IP 的请求。在普通集群里,Pod 的源地址是它当前所在节点的地址。节点很多,会扩缩容、替换,自动伸缩时新增。地址不可预测。合作伙伴无法把整个节点池加进白名单,何况它还在变。

Egress 网关解决这个问题:所有出站流量汇聚到一个固定地址的点。合作伙伴把一两个稳定 IP 加入白名单——就搞定了。使用 Proxeon 作为出口点时,你得到稳定的外部地址,一次性交给合作伙伴。

流量如何路由到 egress 网关

机制取决于 CNI。有些 CNI 插件支持 egress 策略对象,描述:来自带某些标签的 Pod、向外走的流量应该通过某个节点或地址出去。插件自动配置相应的 SNAT 和路由规则。概念上像这样:

apiVersion: policy.example.io/v1
kind: EgressPolicy
metadata:
 name: billing-egress
spec:
 selector:
matchLabels:
 egress: proxeon
 egressIP: 203.0.113.10
 destinationCIDRs:
- 0.0.0.0/0

具体语法因 CNI 而异,请查阅你插件的文档。理念不变:标记流量经过网关的 Pod,指定稳定出口地址。

Egress 网关的可靠性

既然是单一节点,也是单点故障。生存法则:

  • 冗余:至少两个网关节点,自动切换。
  • 监控:每分钟单独发一个合成请求到外部,检测出口可用性。
  • 关注吞吐量:所有南北向流量都走网关,瓶颈会影响全局。
  • 分离策略:关键流量和后台流量最好走不同路径,避免后台负载干扰重要业务。

集群出站流量诊断

出问题时——一定会出——需要一套检查工具。这里汇集实用命令和方法。

第 1 步:查明 Pod 的真实源 IP

首先检查:Pod 对外部世界显示的是什么地址。启动临时调试容器或 exec 进现有 Pod,访问返回你公网地址的服务。

kubectl run nettest --rm -it --image=registry.example.com/nettools:1.0 --restart=Never -- sh
# 容器内
curl -s https://api.ipify.org
curl -s -x http://gate.proxeon.net:8080 https://api.ipify.org

第一个请求显示不走代理的地址,第二个显式走代理。如果两者相同,而你期待不同,说明代理没生效。如果第二个返回预期的 Proxeon 地址,代理是工作的,问题在应用配置。

第 2 步:检查应用是否看到变量

kubectl exec deploy/worker -c app -- env | grep -i proxy

如果输出为空——变量没到容器。检查清单文件,并确认修改后 Pod 已重建。提醒:改 env 需要重建 Pod,不会热加载。

第 3 步:检查内部 DNS 解析

很多错误伪装成代理问题,实际是 DNS。检查内部名和外部名的解析:

kubectl exec deploy/worker -c app -- nslookup orders.default.svc.cluster.local
kubectl exec deploy/worker -c app -- nslookup api.external-partner.com

如果内部名不解析——问题在 CoreDNS,或者因为 NO_PROXY 缺少后缀,请求发到了外部代理。这是排除列表不完整的直接证据。

第 4 步:去哪里看失败

  • 应用日志。找连接错误、超时、代理认证失败(通常是 407)。
  • Sidecar 日志。如果有,能看到它接受了哪些请求、转发到哪里。
  • Egress 网关指标。网关失败和延迟增长。
  • Pod 事件。kubectl describe pod 显示启动问题,包括 sidecar 竞争。
  • NetworkPolicy。检查网络策略是否阻断出站流量——静默失败的常见原因。

典型问题特征

  • 代理返回 407——凭据错误或缺失。检查 Secret。
  • 启用代理后访问内部服务超时——NO_PROXY 不完整。
  • 重复请求源 IP 不同——流量没走 egress 网关,走的是普通节点 SNAT。
  • 启动后最初请求失败,之后正常——sidecar 竞争,迁移到原生 sidecar。
  • Java 应用忽略代理——忘了 JAVA_TOOL_OPTIONS 和系统属性。

应避免的典型错误

把最常踩的坑集中在一起。在事故之前而不是之后对照检查。

NO_PROXY 不完整

绝对第一。忘了 Service CIDR,忘了 .svc 后缀,没考虑云元数据地址——然后得到一连串莫名其妙的失败。始终从你集群的完整标准列表出发。

代理密码在清单文件中明文

泄露到 Git 和 describe 输出。只用 Secret,只限访问。

假设所有运行时都尊重变量

JVM、Node.js、部分 gRPC 客户端忽略 HTTP_PROXY。每个技术栈单独验证,用原生方式配置。

忽略 kubelet 和运行时

镜像拉取代理在节点层级配置,不是 Pod 层级。如果只配了清单文件里的 env,镜像拉不下来别惊讶。

Sidecar 启动竞争

应用先启动,头几个请求失败。解决方案是原生 sidecar 容器。

Egress 网关无冗余

没有复制的单点故障瘫痪所有出站流量。冗余并监控。

混用 CIDR 格式

给不懂 CIDR 的客户端写了 10.0.0.0/8。确认你的库支持哪种格式,并提供替代方案。

改 env 后未重建 Pod

在 Deployment 里改了变量,但旧 Pod 继续用旧值直到重启。确保滚动更新。

忘了变量小写

有些库只读小写 http_proxy。两种大小写都写。

工具与资源

处理集群出站流量时手边该有的东西。

诊断类

  • kubectl exec 和 kubectl debug——从 Pod 内部检查的基础。
  • 临时调试容器——无需重新构建镜像,就能给运行中 Pod 挂上一套网络工具。
  • 带网络工具的镜像——curl、dig、nslookup、traceroute 打包成一个容器,快速检查。
  • 公网 IP 回显服务——检查真实源地址。

基础设施类

  • 标准 NO_PROXY 的 ConfigMap——所有 Deployment 的唯一事实来源。
  • Secret 和外部密钥管理器——用于代理凭据。
  • 服务网格——如果需要开箱即用的 sidecar 可观测性。
  • CNI egress 策略——用于路由到网关。
  • Proxeon(proxeon.net)——提供稳定源地址和认证访问的代理基础设施。

监控类

  • 来自 sidecar 或 egress 网关的出站连接指标。
  • 从集群到外部地址的合成可用性检查。
  • 407 代码和出站请求超时增长的告警。
  • 按目标主机分布出站流量的仪表盘。

案例与结果

分析三个概括场景,展示层级选择如何影响结果。

案例 1:支付集成与 IP 白名单

团队集成外部支付网关,它只接受约定地址的请求。起初尝试容器层级环境变量——结果发现自动伸缩时 Pod 迁到有不同地址的新节点,源 IP 仍然是节点地址,不是代理。部分请求开始被拒绝。

解决方案:把支付服务通过 Proxeon 转到具有固定源地址的 egress。合作伙伴把一个 IP 加入白名单。因未知地址导致的拒绝消失了。额外好处是支付网关所有访问的集中审计日志。

案例 2:代理上线后服务间通信断裂

公司通过共享模板一次性给所有 Deployment 加了 HTTP_PROXY。几分钟后错误涌现:服务互相看不见了。内部调用发到外部代理,代理无法解析它们。

诊断花了一些时间,直到检查 Pod 内部解析,发现 .svc.cluster.local 名称走了代理。原因:NO_PROXY 只含 localhost。制定包含 Pod CIDR、Service CIDR 和所有 DNS 后缀的完整标准列表,放进 ConfigMap,接入所有 Pod。内部通信恢复。从此标准 NO_PROXY 成为 Deployment 模板的必备部分。

案例 3:通过 sidecar 实现出站流量可观测性

安全要求:知道每个应用访问外部的确切目标,有完整日志。应用语言各异,部分不会读 HTTP_PROXY。改几十个服务的代码太贵。

在 Pod 模板中引入 sidecar 代理。应用把出站流量发给本地 agent,它通过 Proxeon 代理并记录指标:目标主机、延迟、响应码。有了按服务查看出站访问的仪表盘。还在 sidecar 层配置了超时和重试,减少了外部 API 短暂不可用时的级联故障。代价是 sidecar 的额外资源,但可观测性和可靠性的收益证明了它的价值。

FAQ:工程师常见问题

为什么访问相邻 Pod 的流量走了外部代理,明明是内部地址?

因为 HTTP 客户端不知道这是内部地址。它看到名称或地址,如果不属于 NO_PROXY,就按规则把请求发给代理。客户端自己区分不了东西向和南北向——排除列表替它做这件事。把内部后缀和子网加入 NO_PROXY。

拉取镜像也需要配置代理吗?

需要,但不是通过 Pod 的 env。镜像拉取由节点层级的容器运行时和 kubelet 执行。仓库代理配置在运行时配置或 kubelet 的 systemd unit 里。容器环境变量完全不影响它。

要稳定源 IP,sidecar 和 egress 网关哪个更重要?

Egress 网关。Sidecar 代理单个 Pod 的流量,但源地址仍取决于连接后续走向。要保证外部合作伙伴接受的固定地址,需要 egress 网关,或者通过具有固定地址的外部出口点,例如 Proxeon。

为什么 Java 应用忽略 HTTP_PROXY?

JVM 出于历史原因不读代理环境变量。它使用自己的系统属性 http.proxyHost、https.proxyHost 和排除项 http.nonProxyHosts。通过 JAVA_TOOL_OPTIONS 传递。记住:JVM 的排除分隔符是竖线,模板用星号,不是 CIDR。

如何安全存储代理密码?

放在 Secret 对象里,通过 RBAC 限制访问,启用 etcd 加密。通过 secretKeyRef 接入值。不要把密码明文写进清单文件,否则会泄露到 Git 和 describe 输出。更高要求时通过 CSI 使用外部密钥管理器。

改了环境变量,行为没变——为什么?

环境变量在容器启动时固定。Pod 未重建前,一直用旧值。更新 Deployment 让 Pod 滚动更新。同时确认应用确实读取环境,而不是初始化时缓存配置。

怎么知道我用的库需要什么 NO_PROXY 格式?

经验法:配好代理,从 Pod 内访问内部名,看请求是走代理还是直连。如果走了代理——格式没被识别。尝试带前导点和不带点的变体,用单独地址替代 CIDR,检查变量名大小写。具体客户端的文档是最好的参考。

为了代理是否应该总是用服务网格?

不该。服务网格是带可观测性和策略的强大工具,但带来明显的开销和运维复杂性。如果任务只是把出站流量导向代理,轻量 sidecar agent 或 egress 网关更便宜。当你确实需要它全部能力时再上网格。

完全非 HTTP 的流量怎么办?

HTTP_PROXY 变量只对尊重它们的 HTTP 和 HTTPS 客户端有效。任意 TCP 连接需要在 sidecar 或 egress 网关层做透明拦截,配上相应路由规则。这里环境变量无能为力。

能对不同 Pod 设置不同代理策略吗?

可以。容器层级——在不同 Deployment 用不同 env。Egress 层级——通过 egress 策略中的标签选择器,把标记的 Pod 导向所需出口。组合使用带来灵活性:基础出口走网关,加个别服务的独立配置。

结语:整理上线清单

我们从为什么“像普通服务器那样配置”在集群里不工作,走到代理嵌入的三个层级、NO_PROXY 细节、Secret、sidecar、egress 网关和诊断。主要结论:在 Kubernetes 里,出站流量不是一个变量,而是一个分层系统,最重要的不是如何启用代理,而是如何不破坏内部通信。

整理最终上线清单,每次实施时放在眼前。

集群代理上线清单

  • 确定层级。有意识地根据具体任务选择容器、sidecar 或 egress 网关。
  • 制定完整 NO_PROXY。