k8s服务流量全部打到了一个容器上的问题排查
最近在生产上碰到一个比较诡异的问题,有k8s中的两个服务A和B,B是一个新服务,只有A会调用且是单线程循环调用,且B都有两个副本。上线后发现A调用B的请求都打在了B的同一个容器上,另一个没有任何流量。...
访问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
更多推荐
所有评论(0)