Fabric8 Kubernetes Client无法连接集群:unexpected end of stream问题排查
问题背景
使用Fabric8 Kubernetes Client 6.7.2执行简单的get pods请求时失败,相同KUBECONFIG配置下kubectl可正常执行操作。本地通过隧道连接Kubernetes 1.24集群,无隧道的集群配置下代码可正常运行。
环境配置
本地kubectl配置如下:
apiVersion: v1 clusters: - cluster: insecure-skip-tls-verify: true server: https://127.0.0.1:6443 tls-server-name: xxx.<redacted>NNNN.us-west-2.aws.host name: mycluster contexts: - context: cluster: mycluster namespace: default user: kube-aws-admin name: mycluster current-context: mycluster kind: Config preferences: {} users: - name: kube-aws-admin user: token: REDACTED
设置环境变量KUBECONFIG=$HOME/.kube/mycluster.conf后,kubectl -n myns get pods可正常输出,但使用以下Fabric8 Client代码执行时失败:
new KubernetesClientBuilder().build().pods().inNamespace("myns").list().getItems().... // 常规调用逻辑
错误信息
运行代码时抛出以下异常:
Exception in thread "main" io.fabric8.kubernetes.client.KubernetesClientException: Operation: [list] for kind: [Pod] with name: [null] in namespace: [myns] failed. at io.fabric8.kubernetes.client.KubernetesClientException.launderThrowable(KubernetesClientException.java:159) at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.list(BaseOperation.java:422) at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.list(BaseOperation.java:388) at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.list(BaseOperation.java:92) ... Caused by: java.io.IOException: unexpected end of stream on Connection{127.0.0.1:6443, proxy=DIRECT hostAddress=/127.0.0.1:6443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1} at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.waitForResult(OperationSupport.java:515) at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.list(BaseOperation.java:420) ... 4 more Caused by: java.io.IOException: unexpected end of stream on Connection{127.0.0.1:6443, proxy=DIRECT hostAddress=/127.0.0.1:6443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1} at okhttp3.internal.http1.Http1Codec.readResponseHeaders(Http1Codec.java:208) at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:88) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:45) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:127) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:257) at okhttp3.RealCall$AsyncCall.execute(RealCall.java:201) at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:833) Caused by: java.io.EOFException: \n not found: limit=0 content=… at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:236) at okhttp3.internal.http1.Http1Codec.readHeaderLine(Http1Codec.java:215) at okhttp3.internal.http1.Http1Codec.readResponseHeaders(Http1Codec.java:189) ... 19 more
排查过程与尝试
- 确认客户端访问地址
127.0.0.1:6443及TLS使用符合配置要求 - 移除kubeconfig中的
tls-server-name字段后,kubectl也会出现Unable to connect to the server: EOF错误,说明该字段是连接成功的关键 - 已尝试设置
KUBERNETES_MASTER、NO_PROXY等环境变量,指定context,更换JDK及OkHttp版本,问题仍存在;官方Java Kubernetes Client也出现相同错误
疑问解答
1. 是否Fabric8 Kubernetes Client忽略了tls-server-name配置?
是的,Fabric8 Kubernetes Client 6.7.2版本确实存在未正确读取并使用kubeconfig中tls-server-name字段的问题。该字段用于TLS握手时指定SNI(Server Name Indication),而kubectl/client-go内部会正确处理该配置,确保TLS握手时传递正确的服务器名称,从而匹配隧道后端的Kubernetes API服务器证书。
2. 问题是否出在OkHttp而非Fabric8?
不是。错误的直接表现是OkHttp连接出现EOF,但根源是Fabric8未将tls-server-name配置传递到OkHttp的TLS握手参数中,导致SNI未设置,隧道后端的API服务器无法完成TLS握手,提前关闭连接,最终触发EOF错误。
解决方案
方案一:手动配置Fabric8 Client的SNI参数
通过自定义OkHttp客户端,显式从kubeconfig中读取tls-server-name并设置到TLS握手的SNI中:
import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubeConfigBuilder; import okhttp3.OkHttpClient; import javax.net.ssl.*; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.security.SecureRandom; import java.security.cert.X509Certificate; public class K8sClientExample { public static void main(String[] args) throws Exception { // 加载kubeconfig配置 Config config = (Config) new KubeConfigBuilder().build(); String tlsServerName = config.getTlsServerName(); // 构建自定义OkHttp客户端 OkHttpClient okHttpClient = new OkHttpClient.Builder() .sslSocketFactory(createSslSocketFactory(tlsServerName), new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {} @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {} @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }) .hostnameVerifier((hostname, session) -> true) // 对应insecure-skip-tls-verify=true .build(); // 创建Fabric8客户端 try (KubernetesClient client = new KubernetesClientBuilder() .withConfig(config) .withHttpClient(okHttpClient) .build()) { // 执行pod列表查询 client.pods().inNamespace("myns").list().getItems().forEach(pod -> System.out.println(pod.getMetadata().getName())); } } private static SSLSocketFactory createSslSocketFactory(String tlsServerName) throws Exception { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, new SecureRandom()); return new SSLSocketFactory() { private final SSLSocketFactory delegate = sslContext.getSocketFactory(); @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { Socket socket = delegate.createSocket(s, host, port, autoClose); if (socket instanceof SSLSocket && tlsServerName != null) { ((SSLSocket) socket).setHostname(tlsServerName); } return socket; } @Override public Socket createSocket(String host, int port) throws IOException { return delegate.createSocket(host, port); } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { return delegate.createSocket(host, port, localHost, localPort); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return delegate.createSocket(host, port); } @Override public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { return delegate.createSocket(host, port, localAddress, localPort); } }; } }
方案二:升级Fabric8 Kubernetes Client版本
升级到7.x及以上版本,该版本已修复tls-server-name字段的读取和使用问题,会自动从kubeconfig中读取该字段并设置到TLS握手的SNI参数中,无需手动配置。
内容的提问来源于stack exchange,提问作者winitzki

