技术分析报告:Kubernetes 环境中 Java 应用通过代理访问公网失败

11 阅读4分钟

一、问题背景

在 Kubernetes 集群中部署的 Java 应用(Spring Boot),受集群出网策略限制,必须通过 HTTP 代理(192.168.0.96:3128)访问外部互联网服务。

在业务运行过程中,部分接口(如行政区划查询)访问外部 HTTP/HTTPS 服务失败,导致业务异常。

涉及技术组件包括:

  • Kubernetes Deployment / Pod
  • Java 8 + Spring Boot
  • Apache HttpClient
  • Nacos(gRPC / Netty)
  • HTTP Proxy(Squid)

二、问题现象

1. 业务层异常表现

接口调用失败,日志显示连接超时:

org.apache.http.conn.HttpHostConnectException: Connect to restapi.amap.com:443 failed: Operation timed out

异常调用链集中在:

com.lishicloud.ygj.common.utils.HttpClientUtil.doGet org.apache.http.impl.client.CloseableHttpClient.execute

  1. 关键特征现象
  • Pod 内使用 wget / curl 访问外部站点 正常
  • JVM 启动参数中已正确注入代理配置
  • Java 应用中的 Apache HttpClient 请求 仍然直连外网并超时
  • 早期曾出现 Nacos 访问内网服务被代理拦截(403),后续已解决

三、排查过程与分析路径

1. JVM 层代理参数验证

通过 Pod 内检查 Java 进程启动参数:

ps -ef | grep java

确认 JVM 已生效的系统属性包括:

-Dhttp.proxyHost=192.168.0.96 -Dhttp.proxyPort=3128 -Dhttps.proxyHost=192.168.0.96 -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts=...

结论:
JVM 系统属性层面的代理配置完全生效,排除 JVM 参数未注入的问题。

2. 集群出网策略验证

在 Pod 内进行对照测试:

curl https://restapi.amap.com curl -x http://192.168.0.96:3128 https://restapi.amap.com

结果:

  • 直连外网请求超时
  • 通过代理访问成功

结论:
集群出网策略要求所有外部流量必须走代理

3. Pod 环境变量与 JVM 行为对比

Deployment 中配置了以下环境变量:

  • http_proxy
  • https_proxy

验证结论:

  • wget / curl 会自动使用 http(s)_proxy

  • JVM 不会自动读取 Pod 中的 http_proxy / https_proxy 环境变量

  • JVM 只识别:

    • -Dhttp.proxyHost
    • -Dhttps.proxyHost
    • 或应用代码中显式配置的代理

➡ Pod 层代理环境变量 ≠ JVM 代理配置

4. Nacos gRPC 问题(阶段性问题)

曾出现异常:

HttpProxyHandler$HttpProxyConnectException => nacos.middleware:9848, status: 403 Forbidden

原因分析:

  • Nacos gRPC 基于 Netty
  • Netty 会优先读取 http_proxy / https_proxy
  • 未配置 NO_PROXY,导致内网服务也被错误地代理

处理措施:

  • 补齐 NO_PROXY / no_proxy
  • 覆盖 .svc / .cluster.local / 内网网段 / nacos.middleware

结果:
Nacos 相关异常消失。该问题与 Apache HttpClient 外网访问问题根因不同

四、根因分析

根因一:Pod 中配置的 http(s)_proxy 环境变量不会自动代入 JVM

这是本次问题中最容易被误判的关键点

事实结论:

  • http_proxy / https_proxy

    • 对 curl / wget / Netty / 部分 C 库 生效
  • JVM 不会自动使用这些环境变量

  • JVM 只认:

    • 启动参数 -Dhttp.proxyHost/-Dhttps.proxyHost
    • 或代码级别代理配置

因此,仅在 Deployment 中配置 http(s)_proxy 不足以保证 Java 应用出网

根因二:Apache HttpClient 默认不使用 JVM 代理系统属性

项目中 HttpClientUtil 构建 HttpClient 的方式为默认模式:

HttpClients.createDefault(); // 或 HttpClients.custom().build();

该模式下:

  • Apache HttpClient 不会自动读取 JVM 的代理系统属性
  • 即使 JVM 中已存在 -Dhttp.proxyHost,HttpClient 仍然直连

根因三:错误的工程认知假设

错误假设:

“只要在 Pod 或 JVM 里配了代理,所有 Java HTTP 请求都会自动走代理”

事实:

  • JVM、Apache HttpClient、Netty、Nacos 各自独立决定是否使用代理
  • 代理行为 不会跨层自动继承

五、解决方案

方案一:让 Apache HttpClient 使用 JVM 系统代理

在 HttpClientUtil 中构建 client 时显式启用:

CloseableHttpClient httpClient = HttpClients.custom() .useSystemProperties() .build();

效果:

  • Apache HttpClient 读取 JVM 的 http.proxyHost / https.proxyHost
  • 行为与 JVM 参数保持一致
  • 最小侵入、最易维护

方案二:在代码中显式指定代理

适用于 所有外网请求必须强制走代理 的场景。

HttpHost proxy = new HttpHost("192.168.0.96", 3128); RequestConfig config = RequestConfig.custom() .setProxy(proxy) .build(); CloseableHttpClient httpClient = HttpClients.custom() .setDefaultRequestConfig(config) .build();

六、最终结论

  1. Pod 层 http(s)_proxy 环境变量 不会自动代入 JVM
  2. JVM 代理系统属性已正确生效
  3. Apache HttpClient 默认 不会使用 JVM 代理
  4. 导致 Java 应用部分 HTTP 请求直连外网并超时
  5. 问题根因在 应用代码中 HttpClient 的构建方式

七、经验总结与工程建议

1. 明确三层代理作用域

Pod http_proxyJVM 不继承仅对 curl、wget、Netty 等生效
JVM -Dhttp.proxyHostHttpClient 不自动用需显式开启
代码级代理配置内建行为最可控

2. Kubernetes + Java 出网推荐实践

  • Pod 层:

    • http_proxy / https_proxy
    • NO_PROXY 明确排除内网
  • JVM 层:

    • 使用 JAVA_TOOL_OPTIONS 注入 -Dhttp.proxyHost
  • 代码层:

    • Apache HttpClient 必须 useSystemProperties() 或显式 setProxy()

3. 排查方法论沉淀

  1. 先验证网络路径是否通(curl / wget)
  2. 再验证 JVM 参数是否真实生效
  3. 最后审计具体客户端库的代理使用逻辑