美文网首页
K8s watch长连接中Okhttp的坑

K8s watch长连接中Okhttp的坑

作者: Teddy_b | 来源:发表于2023-05-29 12:16 被阅读0次

    背景

    一个HA的K8s集群,部署了三节点的kube-apiserver,然后通过域名访问kube-apiserver;其中一个master挂了后,客户端的watch就再也收不到事件了,其他的list等查询、create操作一切正常

    开始

    客户端使用的是k8s官方的Java客户端库,简化后的代码示例:

    ApiClient client = Config.fromToken("https://prod.domain.ss:6443", "token", false);
    
    CoreV1Api coreV1Api = new CoreV1Api(client);
    
            SharedInformerFactory factory = new SharedInformerFactory(client);
            SharedIndexInformer<V1Pod> podInformer =
                    factory.sharedIndexInformerFor(
                            (CallGeneratorParams params) -> coreV1Api.listPodForAllNamespacesCall(null, null, null, labelQuery, null,
                                    null, params.resourceVersion, null, null, params.watch, null), V1Pod.class, V1PodList.class, 60 * 1000L);
    
            factory.startAllRegisteredInformers();
    

    其实和官方的Informer使用示例基本一致,但是在master节点挂掉后Informer就一直收不到事件了,表现很奇怪,只要重启Informer即可恢复。为了搞清楚为什么Informer会收不到事件,做了一系列尝试去复现这个问题;

    探索

    添加额外的日志

    K8s的官方Java客户端中主要执行list+watch逻辑的是ReflectorRunnable,因此我们参考官方的实现,重新实现一个自己的ReflectorRunnable,并加上一些异常日志方便我们排查问题

    public class MyReflectorRunnable <
            ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>
            implements Runnable {
            
            public void run() {
                      ...
                      if ((t instanceof RuntimeException)
                                && t.getMessage() != null
                                && t.getMessage().contains("IO Exception during hasNext")) {
                            t.printStackTrace();
                            log.info("{}#Read timeout retry list and watch", this.apiTypeClass);
                            // IO timeout should be taken as a normal case
                            return;
                        }
                      ...
            }
    }
    

    扩展kube-apiserver

    由于只能在测试环境进行复现,而我们的测试环境是单点的kube-apiserver,为了能够模拟线上的多节点kube-apiserver,然后通过域名访问,首先在测试集群中快速复制出了另一个kube-apiserver,参考上一篇(https://www.jianshu.com/p/c683ccbfa30f);两个kube-apiserver,我们在测试环境也通过域名去访问

    由于稍后需要对kube-apiserver不断的对kube-apiserver进行重启测试,我们首先需要修改下kube-apiserver的启动配置,关闭kube-apiserver的自动重启

    root@bigo:~# cat /lib/systemd/system/kube-apiserver.service 
    [Unit]
    Description=Kubernetes API Server
    Documentation=https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver https://kubernetes.io/docs/reference/generated/kube-apiserver/
    After=network.target
    
    [Service]
    EnvironmentFile=-/etc/kubernetes/apiserver
    ExecStart=/usr/bin/kube-apiserver \
                $KUBE_API_ARGS \
                $KUBE_LOGTOSTDERR \
                $KUBE_LOG_LEVEL \
                $KUBE_ETCD_SERVERS \
                $KUBE_API_ADDRESS \
                $KUBE_API_PORT \
                $KUBELET_PORT \
                $KUBE_ALLOW_PRIV \
                $KUBE_SERVICE_ADDRESSES \
                $KUBE_ADMISSION_CONTROL 
    Restart=no   # 把这个改为no,这样systemd才不会再我们kill掉kube-apiserver的时候自动重新拉起
    Type=notify
    LimitNOFILE=65536
    
    [Install]
    WantedBy=multi-user.target
    

    测试

    测试代码:

    CoreV1Api coreV1Api = new CoreV1Api(client);
    
            SharedInformerFactory factory = new SharedInformerFactory(client);
            SharedIndexInformer<V1Pod> podInformer =
                        factory.sharedIndexInformerFor(
                                (CallGeneratorParams params) -> coreV1Api.listPodForAllNamespacesCall(null, null, null, labelQuery, null,
                                        null, params.resourceVersion, null, params.timeoutSeconds, params.watch, null), V1Pod.class, V1PodList.class, 60 * 1000L);
           
            factory.startAllRegisteredInformers();
    
    
            ScheduledExecutorService checkExecutor =
                    Executors.newSingleThreadScheduledExecutor(
                            Threads.threadFactory("controller-resync-" + "test" + "-%d"));
            try {
                checkExecutor.scheduleWithFixedDelay(() -> {
                    List<V1Pod> podIndex = podInformer.getIndexer().byIndex("podIndex", "demo-a-null");
                    log.info("Check size================================================:{}", podIndex.size());
                    for (V1Pod pod : podIndex) {
                        log.info("Check name================================================:{}", pod.getMetadata().getName());
                    }
                }
                        , 0L, 2000, TimeUnit.MILLISECONDS);
            } catch (RejectedExecutionException e) {
                // submitting reflector list-watching job can fail due to concurrent invocation of
                // `shutdown`. handling exception with a warning then return.
                log.warn("reflector list-watching job exiting because the thread-pool is shutting down");
                return;
            }
    

    测试代码使用的都是我们加了异常日志后的那些类,然后加上了一个定时执行用于探测指定deployment的pod数量及其名称。因为我们会在kill掉kube-apiserver后,手动删除这个deployment的pod,然后让这个定时任务检测pod数量和名称是否会变化,如果正常的变化了,说明Informer仍在正常工作,如果在我们删除pod后,定时任务打印的却没变化,说明Informer已经卡死了,没有收到后续的事件通知。

    kill & kill -9

    首先我们尝试正常kill掉kube-apiserver,kill后的日志输出


    image.png

    可以看到kill掉kube-apiserver后,Informer正常的抛出了异常,并且开始重新的list+watch,很遗憾,没能复现。

    直接重启节点

    第二次尝试直接重启节点,节点重启后的日志和上面kill的一致,Informer也是正常的抛出了异常,并且开始重新的list+watch

    添加http请求详细日志
    HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
            client.setHttpClient(client.getHttpClient().newBuilder().addNetworkInterceptor(loggingInterceptor).build());
    

    k8s官方的Java库使用的是okhttp客户端,首先修改下okhttp的日志级别,打印出HTTP请求的详细信息


    image.png

    h2对应的是http2,由于我window本机的jdk版本为1.8.0_291,默认使用的就是http2协议,我们尝试将协议改为http1.1

    List<Protocol> protocols = new ArrayList<>();
            protocols.add(Protocol.HTTP_1_1);
            client.setHttpClient(client.getHttpClient().newBuilder()
                    .protocols(protocols).build());
    

    直接重启节点,发现这次Informer就没法收到后续的watch事件了,至此复现了线上的场景

    原因分析

    登录线上服务器,发现线上的jdk版本是1.8.0_121,这个版本的jdk默认使用的是http1.1协议,okhttp长连接默认是没有开启tcp keepalive的,因此在服务器节点直接挂掉后,就会出现这种死连接的情况;因此,总结其原因是http1.1协议+没开启tcp keepalive,出现了死连接,导致watch没法收到后续的事件。

    解决方案

    • 升级jdk的小版本,打破原因中的第一项,强制使用http2协议

    • 启用tcp keepalive,我们可以直接使用curl 来进行试验;由于操作系统默认的tcp keepalive时间是7200s(我这里为了测试改成了72s),所以不太建议依赖tcp keepalive来规避这个问题,线上故障两个小时,可能黄花菜都凉了~

    # curl 发起watch请求
    curl -k -H 'Authorazation: Bearer token' https://test.domain.ss:6443/api/v1/pods?watch=true
    
    
    # 查看连接状况
    # 可以看到curl的时候默认就是开启了tcp keepalive
    netstat -anpo|grep curl
    tcp     0      0     source_ip:22345     target_ip:6443   ESTABLISHED 51730/cur7      keepalive (1.02/0/1)
    
    # 查看系统tcp keepalive内核参数
    # 在72s后,再次netstat查看的时候连接已经被正常释放
    root@bigo:# sysct1 -a lgrep keepalive
    net.ipv4.tcp_keepalive_intv7  =  5
    net.ipv4 .tcp_keepalive_probes = 1
    net.ipv4.tcp_keepalive_time = 72
    
    • 另外一种方式是将长连接改为websocket应该也是可行的,websocket的心跳机制应该可以避免死连接的出现,待验证

    相关文章

      网友评论

          本文标题:K8s watch长连接中Okhttp的坑

          本文链接:https://www.haomeiwen.com/subject/rrujedtx.html