k8s servelList(服务列表) 卡死不同步问题分析

提要


容器集群版本情况:k8s 1.20

客户端k8s client版本: 0.21

事情是这样的,运行了一年的服务,突然有一天业务反馈服务使用异常,然后初步调查结果如下

以下截图是网关异常

以下截图是客户端zull(feign)转发异常(底层都是使用ribbon)

翻译信息:

某个service的Endpoints已经更新了,但是客户端的服务发现没有更新过来,还是使用了历史的serverList 配置,配置的信息包含已经下线的pod ip,这样就导致这个调用出现问题了,最直接的异常就是 No route to host (Host unreachable)

科普一下:

Endpoints与Pod的关系是什么?

Endpoints是Kubernetes集群中的一个资源对象,存储在etcd中,用来记录一个Service对应的所有Pod的访问地址。

故障分析


一:CoreDns 排查

最开始怀疑是coredns出问题了,因为客户端使用的服务发现直接跟dns挂钩

这个是注册中心流程图,最开始是apiserver -> coredns -> kube-proxy -> iptables ,然后这个serverList的更新,却不是直接跟coredns打交道的,而是跟kube-proxy。

创建新的 Service 对象时,会得到一个虚拟 IP,被称为 ClusterIP。服务名及其 ClusterIP 被自动注册到集群 DNS 中,并且会创建相关的 Endpoints 对象用于保存符合标签条件的健康 Pod 的列表,Service 对象会向列表中的 Pod 转发流量。

与此同时集群中所有节点都会配置相应的 iptables/IPVS 规则,监听目标为 ClusterIP 的流量并转发给真实的 Pod IP。

以上参考:https://ost.51cto.com/posts/17770

究竟是不是coredns出问题了呢,或者说是kube-proxy出问题了呢?!

答案是否定的!

检查容器的所有coredns的pod日志以及kube-proxy的pod日志,没有显著的异常。

另外并不是所有的客户端服务发现列表出现问题,没法同步,只是少数的应用(微服务,pod -> service)出故障了。

那,究竟是什么问题呢?!

二:客户端的服务发现排查

通过上面的分析,大概率是应用的服务发现出问题了。现在开始分析出问题应用的依赖配置

        <!-- 服务发现 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-kubernetes-discovery</artifactId>
            <version>0.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-kubernetes-ribbon</artifactId>
            <version>0.2.1.RELEASE</version>
        </dependency>

纳尼?!我的乖乖,该应用的k8s服务发现的版本居然这么旧,这个跟公司推荐的版本差太远了,公司推荐的是1.1.6以上,这个版本的服务发现目前来看是没有出问题的。

直观感受是需要客户端需要升级这个服务发现的版本,但很尴尬发现,该应用绑定的spring boot版本也比较老(很老很老这种),直接升1.1.6版本,会导致一系列的版本冲突,比较恶心!

有没有其它好的办法呢?!

没办法了,需要被迫研究源代码了!

三:服务发现源代码分析

Spring Cloud Kubernetes服务注册与发现实现原理

在sck-demo项目搭建之初,我们是跟着官方提供的demo去实现服务注册和发现的,也就是在每个服务的Application类上添加一个@EnableDiscoveryClient注解。

我们并未配置Kubernetes的地址,但我们使用DiscoveryClient却能获取到服务节点。要解开这个疑惑我们需要了解Kubernetes,以及了解Spring Cloud Kubernetes Discovery的源码。

前面我们在分析Ribbon的源码时也了解到,Ribbon并非通过DiscoveryClient去获取服务提供者的。

Ribbon通过提供一个ServerList接口让使用者自己去实现来完成Ribbon的服务发现。Ribbon定时调用ServerList更新自身缓存的服务提供者列表,默认30秒更新一次。

Spring Cloud Kubernetes Ribbon的作用就是实现Ribbon的ServerList接口,从Kubernetes获取可用的服务提供者。

实际上,Spring Cloud Kubernetes Ribbon也并未使用到Spring Cloud Kubernetes Discovery提供的DiscoveryClient接口的实现来获取服务列表,而是直接调用接口从Kubernetes中获取。

正是因为如此,笔者尝试去掉@EnableDiscoveryClient注解后,以及去掉Spring Cloud Kubernetes Discovery的依赖后,项目依然能正常运作。

需要注意,Spring Cloud Kubernetes Ribbon依赖Spring Cloud Kubernetes Core,如果去掉Spring Cloud Kubernetes Discovery,可能就要自动手动添加Spring Cloud Kubernetes Core的依赖,否则服务启动失败。

Spring Cloud Kubernetes Discovery实现DiscoveryClient接口只是能够让我们通过DiscoveryClient获取服务提供者。

SpringCloud Kubernetes Discovery实现的服务注册接口也并未真正的去注册服务。

可以这么说,在Spring Cloud Kubernetes项目中,Spring Cloud Kubernetes Discovery是一个多余的存在。

如果去掉Spring Cloud Kubernetes Discovery后,我们想要获取某个服务的当前可用服务提供者怎么获取呢?

我们可以通过使用Ribbon的ServerList去获取,由Spring Cloud Kubernetes Ribbon实现。

本地使用kubectl proxy

本地使用kubectl proxy命令就会运行一个Kubernetes API代理服务。

例如:

$ kubectl proxy --port=8004Starting to serve on 127.0.0.1:8004

使用kubectl proxy —port=8004开启Kubernetes API代理服务,监听请求的端口为8004。代理服务启动成功后,我们就可以使用127.0.0.1:8004访问Kubernetes的API了。

例如,获取sck-demo-provider这个服务的所有endpoints,在浏览器输入:

http://127.0.0.1:8004/api/v1/namespaces/default/endpoints/sck-demo-provider

其中default为名称空间,sck-demo-provider为服务名。响应结果如下图所示。

以上参考:https://blog.51cto.com/u_15064638/2871844

四:客户端的服务发现源代码分析

好了,通用的源码已经分析完了,现在我们正式分析应用的具体依赖源码。

k8s client 0.21 依赖的 ribbon-loadbanlancer 版本是2.2.5,然后具体触发这个更新server list列表的是 PollingServerListUpdater类,

注意,ribbon这里只管调度,不管具体实现,具体实现继续看!

如截图所示,具体实现是k8s client服务发现的依赖包。

再接着看!

接着实现http调用的是 io.fabric8.kubernetes.client 的 client.newCall(request).execute() 方法。

再再接着看!

最底层实现调用的是okhttp3工具!目前k8s 服务发现对应的okhttp3依赖版本是3.8.1 版本!

本质原因找到了,是这个版本的execute出问题了,卡住了,导致ribbon的更新任务对于的线程假死了。

最后我们来看看,k8s 服务发现 1.1.6版本对应的okhttp3版本是多少。

        <!-- 尝试解决更新列表卡主问题 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.14.9</version>
        </dependency>

然后其对应的http调用执行有啥不一样

多了 transmitter.timeoutEnter() 这个调用钩子,能够有效的修复卡死bug!

查看Okhttp源码时,在Transmitter类中发现了一个AsyncTimeout对象。了解代码后得知,该类是用于做一些超时检测的操作。

/**
 * This timeout uses a background thread to take action exactly when the timeout occurs. Use this to
 * implement timeouts where they aren't supported natively, such as to sockets that are blocked on
 * writing.
 *
 * <p>Subclasses should override {@link #timedOut} to take action when a timeout occurs. This method
 * will be invoked by the shared watchdog thread so it should not do any long-running operations.
 * Otherwise we risk starving other timeouts from being triggered.
 *
 * <p>Use {@link #sink} and {@link #source} to apply this timeout to a stream. The returned value
 * will apply the timeout to each operation on the wrapped stream.
 *
 * <p>Callers should call {@link #enter} before doing work that is subject to timeouts, and {@link
 * #exit} afterwards. The return value of {@link #exit} indicates whether a timeout was triggered.
 * Note that the call to {@link #timedOut} is asynchronous, and may be called after {@link #exit}.
 */publicclassAsyncTimeoutextendsTimeout{

这里提供了几个有用的信息:

  • 这是一个利用统一子线程检测超时的工具,主要针对的是一些原生不支持超时检测的类。

  • 它提供了一个timedOut()方法,作为检测到超时的回调

  • 内部提供的sink()source()方法可以适配流的读写超时检测,这可以对应到网络请求的流读写,后面会讲到。

  • 提供enter()exit()作为开始计时和结束计时的调用。也就是说开始执行计时的起点将会在enter()发生

详情见: https://juejin.cn/post/6962464239864774664

验证


如截图所示,超时按约定被捕获了。

总结


这个服务列表更新的异常有3种处理方式

1. 在ribbon调度这块加一层线程卡死监控,如果线程长时间处理Thread.State.WAITING.name()状态的,定时强制唤醒!(感谢东平的早期方向引导)


2. 在spring-cloud-kubernetes-ribbon执行更新的地方,额外加一个监控钩子,处理方式跟上面一致。

3. 其它依赖不变的情况下,直接升级okhttp3版本,升到3.14.9以上

        <!-- 尝试解决更新列表卡主问题 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.14.9</version>
        </dependency>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值