k8s服务流量全部打到了一个容器上的问题排查

问题详细描述

最近在生产上碰到一个比较诡异的问题,有k8s中的两个服务A和B,B是一个新服务,只有A会调用且是单线程循环调用,且B都有两个副本。上线后发现A调用B的请求都打在了B的同一个容器上,另一个没有任何流量。
这里还要补充说明下A中是用SpringBoot RestTemplate发起的http请求B
原因从两方面去看
1 调用姿势不对,导致连接建立之后就一直没断开
2 负载均衡算法的问题
ps:问题还没有定位到,有空继续跟进~~
笔者目测第二点的概率较小,所以先从1着手去分析

一 RestTemplate背景知识

RestTemplate中有个关键的接口ClientHttpRequestFactory,它是个函数式接口,用于根据URI和HttpMethod创建出一个ClientHttpRequest来发送请求

public interface ClientHttpRequestFactory {	
	// 返回一个ClientHttpRequest,这样调用其execute()方法就可以发送rest请求了~
	ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}

Netty、HttpComponents、OkHttp3,HttpUrlConnection对它都有实现~
在A服务中用的是一种比较简单使用方式

@Bean
    public RestTemplate restTemplate1()
    {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setConnectTimeout(500);
        requestFactory.setReadTimeout(500);
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        return restTemplate;
    }

SimpleClientHttpRequestFactory内部并没有使用连接池
这里跟踪一下A单线程循环调用B的源码,调用链大概是这个样子的:
postForEntity() -> execute() -> doExecute() -> createRequest()
createRequest这里会走到上面提到的SimpleClientHttpRequestFactory里面。
之后调用openConnection() -> URL. openConnnection()
执行上面的逻辑之后我们有了一个封装好的SimpleBufferingClientHttpRequest对象,里面包含我们生成好的Connnection对象(连接的服务地址,端口号,超时时间等等)
再往后就会执行到比较核心的地方request.excute

protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
			@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

		Assert.notNull(url, "URI is required");
		Assert.notNull(method, "HttpMethod is required");
		ClientHttpResponse response = null;
		try {
			ClientHttpRequest request = createRequest(url, method);
			if (requestCallback != null) {
				requestCallback.doWithRequest(request);
			}
			//这里是核心的代码!!!
			response = request.execute();
			handleResponse(url, method, response);
			return (responseExtractor != null ? responseExtractor.extractData(response) : null);
		}
		catch (IOException ex) {
			String resource = url.toString();
			String query = url.getRawQuery();
			resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
			throw new ResourceAccessException("I/O error on " + method.name() +
					" request for \"" + resource + "\": " + ex.getMessage(), ex);
		}
		finally {
			if (response != null) {
				response.close();
			}
		}
	} 

excute() -> executeInternal() -> HttpsURLConnectionImpl.connect()
执行到这里的时候我们就能看到DelegateHttpsURLConnection对象对应的httpsClient属性里面已经能够看到建立tcp请求对应的socket四元组了。
在这里插入图片描述
当我执行下次循环的时候,发现依然会创建新的tcp链接,明显看到四元组中对应的localPort已经发生变化了加粗样式
这就推翻了我们之前的猜想,连接没有保持不变,每次请求都会创建新的连接。当然这种方式比较低效,每次都进行tcp握手会比较耗费时间,使用连接池是更优雅的方式。
接下来我们验证第二种猜想,是否跟负载均衡策略有关

二 k8s基础知识

在排查这个问题之前,我们需要先具备一些关于K8s的基础知识,这里我大概列举一下

1 容器和pod

pod是k8s的最小单元,是一个Kubernetes抽象,表示一组一个或多个应用程序容器(如Docker或rkt),以及这些容器的一些共享资源。共享存储,网络等,k8s主要是为了扩展,他支持多种容器,甚至是用户自定义的容器
node

2 Service, kube-proxy

kube-proxy是Kubernetes的核心组件,部署在每个Node节点上
kube-proxy的作用主要是负责service的实现,四层负载,属于client-proxy(与之对应的是service-proxy),为service提供服务发现和负载均衡的能力。

service是一组pod的服务抽象,相当于一组pod,负责将请求分发给对应的pod。对外为这组pod统一提供一个IP,一般称为cluster IP(也称Virtual IP)。
service通过配置文件中的selector绑定到对应的pod

Endpoints创建service的时候,通过设置selector关联pod,就会创建一个与service同名的endpoints。记录service对应的所有pod的访问地址

注意 service只是抽象定义,kube-proxy才是干活的

3 kube-proxy的代理模式

简单列举下

userspace模式

因为请求从用户空间进入内核iptables,然后再回到用户空间,效率低。
kube-proxy要持续通过api-server(k8s的数据总线,各种api)监听Service和Endpoint的变化。

iptables

与 userspace 相同,kube-proxy 持续监听 Service 以及 Endpoints 对象的变化;但它并不在本地节点开启反向代理服务,而是把反向代理全部交给 iptables 来实现;即 iptables 直接将对 VIP 的请求转发给后端 Pod,通过 iptables 设置转发策略.
性能有所提升,但是如果Node上的service比较庞大, iptables rules将会非常,性能也会降低。

目前大部分企业用k8s上生产时,都不会直接用kube-proxy作为服务代理,而是通过自己开发或者通过IngressController来集成HAProxy, Nginx来代替kube-proxy。

ipvs

IPVS基于netfilter的散列表,相对于同样基于netfilter框架的iptables有更好的性能表现和扩展性。iptable主攻防火墙,ipvs主攻内核态4层负载均衡
贴一篇大牛的博客:https://www.zsythink.net/archives/1199

排查问题的过程中在github找到了一个关于ipvs load balance bug的issue
https://github.com/kubernetes/kubernetes/issues/79213
里面提到了lc(Least Connections)算法的bug,因为我们每次调用服务B都是建立连接后其中一个连接数变成1,调用完成之后释放连接,然后B服务的两个容器链接数归0。又会按照pod list选择连接数最少的,这个时候拿到的是同一个连接。

The lc scheduler iterates over the destination list and keeps the first one with the lowest number of connections.
My understanding is that destinations are added to the list every time an endpoint is added so all nodes could easily end up with the same list in the same order and make the same decision

我们生产环境用的就是ipvs,但是询问运维的得知负载均衡算法用的是rr(Round Robin)跟github上的描述不符空!!空欢喜一场
问题后续有空再看,此贴先做记录,也希望有经验的大佬指点一下思路

参考

  • 1: https://www.cnblogs.com/fuyuteng/p/11598768.html
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Kubernetes(简称K8s)是一个开源的容器编排平台,用于管理容器化应用程序的部署、扩展和自动化操作。在Kubernetes中,我们可以过将容器镜像部署到Kubernetes集群中的容器中来运行应用程序。 首先,我们需要创建一个Docker镜像,该镜像包含了我们想要运行的应用程序及其依赖项。Docker镜像可以使用Dockerfile来定义,其中包含了构建这个镜像所需的步骤和命令。常见的构建步骤包括下载所需的软件包、设置环境变量、复制应用程序代码等。过运行`docker build`命令,我们可以构建出一个可用的Docker镜像。 接下来,我们需要将这个Docker镜像上传到一个镜像仓库中,这样Kubernetes集群就可以拿到这个镜像并运行。常见的镜像仓库有Docker Hub、Google Container Registry等。我们可以使用`docker push`命令将镜像推送到镜像仓库中,并确保设置了适当的访问控制。 然后,在Kubernetes集群中创建一个容器部署。容器部署是一个Kubernetes管理的逻辑单元,用于定义如何在集群中运行一个或多个同样的容器实例。我们可以使用Kubernetes配置文件(常是YAML格式)来定义容器部署。在配置文件中,我们需要指定容器部署所使用的镜像、容器的资源要求和限制、容器之间的网络和存储配置等信息。 最后,我们可以使用Kubernetes命令行工具(如kubectl)来创建和管理容器部署。过运行`kubectl apply -f`命令,我们可以将容器部署配置文件应用到Kubernetes集群中。Kubernetes会根据配置文件中的定义,自动创建容器实例,并在集群的节点上运行这些容器。 总体而言,将Docker镜像运行到Kubernetes容器中需要完成以下步骤:构建Docker镜像、上传到镜像仓库、创建容器部署配置文件、应用配置到Kubernetes集群。过这些步骤,我们可以在Kubernetes集群中轻松地部署和管理容器化应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值