容器云平台API Server卡顿问题排查
58云端计算平台是58集团架构线基于Kubernetes + Docker技术为集团内部服务开发的一套业务例项管理平台,它具有简单,轻量的特点及高效利用物理资源,更快的部署和统一规范的标准化执行环境,通过云平台,使得服务标准化,上线流程规范化,资源利用合理化。然而云平台的建设过程不是一帆风顺,也不乏出现一些问题挑战,本文就针对云平台现实中遇到的一个问题和大家分享。
1、 关于问题
1.1 问题概述近期,很多业务同事反馈使用云平台上线存在容器部署慢,平台反应慢的问题。通过详细的问题排查定位后,最终问题得以解决。
1.2 kubernetes基本知识
私有云平台通过Kubernetes对容器进行编排。Kubernetes整体架构如下图所示:

其中几个主要的模组的功能简要描述如下:
Etcd:用于 Kubernetes的后端储存。
Pod:Kubernetes最基本的操作单元,包含一个或多个紧密相关的容器。
Replication Controller:副本控制器,用来保证Deployment或者RC中副本的数量。
Scheduler:Kubernetes的排程器,Scheduler监听API Server,当需要建立新的Pod时Scheduler负责选择该Pod与哪个Node进行系结。
Kubelet:每个Node节点上都会有一个Kubelet负责Master下发到该节点的具体任务,管理该节点上的Pod和容器。
API Server:对于整个Kubernetes丛集而言,API Server是通过暴露Kubernetes API的方式提供给内部元件或者外部程式呼叫去完成对Kubernetes的操作。各个元件之间也是通过API Server作为桥梁进行间接通讯,这种方式做到各个元件间充分解耦。
业务同事操作管理平台发出建立丛集请求到丛集建立完成的整个流程如下:
1. 业务同学操作管理平台进行升级操作,管理平台通过http方式向API Server发出请求。
2. API Server处理和解析请求引数,将待建立的Pod资讯通过API Server储存到Etcd。
3. Scheduler通过API Server的watch机制,检视到新的Pod,尝试为Pod系结Node。
4. 经过预选筛除不合适节点及从待选节点中根据一定规则选出最适合的节点。
5. 对选中的节点及Pod进行binding操作,将相关的结果通过API Server储存到Etcd
6. 对应Node的Kubelet程序呼叫容器执行时建立容器。
2、 定位问题
2.1 问题排查从1.2可以看到,API Server在建立Pod过程中起到非常关键的中间桥梁作用,解析外部请求及读写Etcd。因此决定首先从API Server程序所在宿主机的各项效能指标及日志方面进行排查,看是否有所发现。
目前线上环境有3台主机执行API Server,以达到流量负载均衡的目的,异常时间段网络卡eth2入流量如下图所示:

由3台API Server主机的监控资料,发现服务器A的网络卡入流量远高于另外两台,说明绝大部分请求传送到了服务器A。
通过对比三台服务器API Server 的CPU利用率,发现服务器A的API Server程序CPU使用率一直保持在2000%(20核)上下波动,而另外两台服务器的API Server的CPU利用率没有超过100%(1核)。进一步证实了A的API Server程序处理了绝大多数的请求。
检视A服务器的API Server的相关log,发现正在大量输出如下的日志:

这个日志显示有大量请求通过API Server到Etcd查询Pod的状态。
对于Kubernetes后端的储存目前采用5个Etcd节点组成Etcd丛集。登陆其中一个节点(E1),发现对E1节点执行Etcd操作命令,比如命令: “etcdctl ls /registry/pods/default”,命令执行也会经常超时。
同时对比5台Etcd节点的流量,发现有一个节点网络卡入流量远高于其他四个节点,该节点(E1)的Etcd程序的CPU利用率在100%左右,明显高于剩余的4个节点CPU利用率。检视节点E1的Etcd程序日志,经常看到如下报错:

可以推断节点E1的负载非常高,节点间同步心跳都已经超时,无法正常的响应外部的请求了。
2.2 问题分析:
经过上述排查,主要集中在这两个问题上:
1. 负载均衡策略失效。
2. Etcd存取资料缓慢。
2.2.1 负载均衡策略失效
首先可以看到对Kubernetes丛集的操作请求大部分都落在某个API Server上,导致其中一个API Server负载很高,那么有可能负载均衡策略有些问题。那就先看看当前负载均衡策略是如何的。
当前我们租赁的是腾讯的机房,负载均衡策略采用的是TGW(Tencent Gateway)系统所自带支援的负载均衡策略。腾讯云上有关介绍如下:
TGW负载均衡策略保证请求的分摊转发,也会自动对resource server(RS)进行存活检测,每分钟会有心跳包去对接入TGW的IP Port进行探测。
关于TGW相关配置具体如下:
1. 做域名解析:我们对需要访问到API Server的物理机都做了本地DNS, 将一个固定域名(D)解析到一个特定的VIP(V),而该VIP就是TGW对外提供的虚拟IP。
2. 配置TGW服务的RS列表:将三台API Server节点对应的物理IP加入到RS列表。
正常情况下,所有需要访问API Server的请求都先本地域名解析到虚拟IP V,将请求的资料包都发送到V,V相当于是TGW对外的接入点,再通过TGW内部负载均衡策略将请求资料包进行目的网络地址转换(DNAT),分发到不同的RS上。
经排查,TGW的监控检测模组定期向所有的RS传送心跳包,但是TGW监控检测模组只能收到A服务器的回包,因此TGW认为只有A节点是存活状态,所有的请求资料包最终就由TGW转发到A服务器上了,这就是负载均衡策略失效的根本原因。
这里还有一个现象是为什么Etcd丛集中只有一个节点的负载很高呢?
五个节点的Etcd丛集中只有一个节点负载很高,其他正常,通过检视A服务器的API Server的log,可以看到的大量的读请求都固定传送到了同一个Etcd节点。
对于这个现象,可以看下API Server访问后端储存的源代码,目前线上Kubernetes基于v1.7.12的源代码编译执行,API Server访问Etcd是在内部初始化一个Etcd client端,然后通过Etcd client端传送请求到Etcd server端。Etcd client端有v2和v3两个版本。线上API Server使用的是v2版本客户端。主要程式码如下:
//初始化EtcdClient工作
func New(cfg Config) (Client, error) {
c := &httpClusterClient{//返回一个http型别的client
clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect(), cfg.HeaderTimeoutPerRequest),
rand: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),//传入一个当前时间的随机种子
selectionMode: cfg.SelectionMode,
}
if err := c.SetEndpoints(cfg.Endpoints); err != nil {
return nil, err
}
return c, nil
}
//对Etcd列表进行打乱
func (c *httpClusterClient) SetEndpoints(eps []string) error {
...
neps, err := c.parseEndpoints(eps)
c.Lock()
defer c.Unlock()
c.endpoints = shuffleEndpoints(c.rand, neps)//打乱Etcd列表
c.pinned = 0
...
return nil
}
func shuffleEndpoints(r *rand.Rand, eps []url.URL) []url.URL {
p := r.Perm(len(eps))//rank库的Perm方法可以返回[0,n)之间的随机乱序阵列
neps := make([]url.URL, len(eps))
for i, k := range p {
neps[i] = eps[k]
}
return neps
}
可以看到在初始化Etcd客户端时候会传入一个当前时间的随机种子去打乱所有endpoints(Etcd节点)的顺序。
对于Etcd的操作都是通过API Server内部的Etcd客户端传送http请求到Etcd Server端,最主要是呼叫如下方法:
func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
...
for i := pinned; i k := i % leps
hc := c.clientFactory(eps[k])
resp, body, err = hc.Do(ctx, action)
...
if resp.StatusCode/100 == 5 {
switch resp.StatusCode {
case http.StatusInternalServerError, http.StatusServiceUnavailable:
cerr.Errors = ...
default:
cerr.Errors = ...
}
...
continue
}
if k != pinned {
c.Lock()
c.pinned = k
c.Unlock()
}
return resp, body, nil
}
return nil, nil, cerr
}
该方法表明每次请求时候,会从pinned节点开始尝试传送请求,如果传送请求异常,则按照初始化时候打乱顺序的下一个节点(pinned++)开始尝试传送资料。如此看来,如果API Server使用了某个endpoint传送资料,除非用坏了这个节点,否则会一直使用该节点(pinned)传送资料。这就说明了,没有异常情况下,一个API Server就对应往一个固定的Etcd传送请求。
对于Etcd丛集,如果是写请求的话,follower节点会把请求先转发给leader节点处理,然后leader再转发给follower同步。那么5个节点CPU负载不会这么不均衡,但是根据2.1排查API Server日志看到这里是大量的读请求,相对于写请求,读请求是所有follower节点都能对外提供的。也就是大量请求由于负载均衡策略失效都转发到A服务器,A再把查询请求都打到其中一个固定的Etcd,导致该节点忙于处理Etcd查询请求,负载就会飙高。
总的来说,TGW做负载均衡时候,由于心跳检测模组和其中两个Resource Server间连线不通,导致误将所有请求都转发到其中一个API Server,而一个特定的API Server使用v2版本Etcd客户端就只会往一个固定的Etcd服务端发请求,这样整个负载均衡策略就失效了。
2.2.2 Etcd存取资料缓慢
namespace未做划分从2.1中检视API Server 的日志可以看出,很多get请求Pod物件资讯,比如:“Get /api/v1/namespaces/default/pods?...” 这些都是从default namespace下获取Pod资讯,这就说明线上并没有对Pod的namespace做划分。
Kubernetes是通过namespace对容器资源进行隔离,预设情况下,如果未指定namespace的话,建立的容器都被划分到default namespace下,因为这个原因也给后面往Etcd中储存容器元资料资讯也留下了坑。所有的Kuberentes的元资料都储存在Etcd的/registry目录下,整体如下图所示:

Kubernetes中Pod的资讯储存在/registry/pods/#{名称空间}/#{具体例项名}的目录结构中,正因为如果不指定namespace的话,就会储存到default的namespace中,也就是/registry/pods/default目录下储存了线上全部Pod物件资讯。
也就是说大量get请求Pod物件资讯,由于未做namespace划分,每次都会去访问default子目录,每次请求相当于都要做全域性搜寻,随着丛集的增多,Pod不断的存入到该子目录中,搜寻效能也会变得越来越差。
查询结果未加入快取从2.1中检视API Server 的日志看到很多Get/List操作,那么可以仔细看看相关方法的执行流程,下面是List方法执行过程中呼叫的中间函式:
func (c *Cacher) GetToList(ctx context.Context, key string, resourceVersion string, pred SelectionPredicate, listObj runtime.Object) error {
if resourceVersion == "" {
return c.storage.GetToList(ctx, key, resourceVersion, pred, listObj)//直接查询Etcd
}
listRV, err := ParseListResourceVersion(resourceVersion)
...
obj, exists, readResourceVersion, err := c.watchCache.WaitUntilFreshAndGet(listRV, key, trace)//从快取中获取
...
return nil
}
可以看到,GetToList方法中传入的有个resourceVersion 引数,如果设定了就会从快取中获取,如果不设定就会去Etcd中查询。这个也是一个关键点,有关resourceVersion 的相关使用如下:
不设定:通过API Server从Etcd读取。
设定成0:从API Server的cache读取,减轻API Server和Etcd压力。例如Kubelet经常通过此方法Get Node物件,Kubernetes Infomer第一次启动时List也通过此方法获得物件。
大于0:读取物件指定版本。
线上管理平台通过http界面去查询Pod资讯时候是没有设定resourceVersion,所以每次通过Get/List方法获取资源时候都会查询Etcd,如此一来经常大量高频率的查询Etcd会导致其压力较大,开启快取策略不仅可以减轻访问Etcd压力而且还可以加快查询速度。
总结以上两点:所有的请求都发往一个固定的API Server,导致该API Server节点负载较高,同时该API Server又会将查询请求固定的发给某个Etcd节点,然而请求结果并没有在API Server端做快取,每次都会直接查询Etcd,在从Etcd中获取Pod资讯又是从default这个大的子目录中全域性搜寻,每次请求都比较费时,这样导致某一个固定的Etcd一直处理大量的费时的请求,最终将该Etcd资源耗尽,负载过高,因而查询结果不能及时返回给API Server,导致建立Pod时候拿不到相关的资讯,Pod建立工作无法进行,所以最终表象是丛集部署长时间卡顿。
3、 解决方案
1. 切换负载均衡方案:临时切换为DNS轮询方式,保证每个API Server节点的流量均衡。同时跟进TGW对于某些网段的RS和TGW服务不能探测心跳及后续改进。2. 将Kubernetes中Pod按多个namespace划分,目前线上所有的Pod都划分到预设的default的namespace下,每次读取Pod资讯都是从Etcd检索整个namespace,比较损耗Etcd效能,目前已经将Pod的namespace进行细分,加快了读取Pod资讯速度同时减少了Etcd效能损耗。
3. Etcd v3版本客户端会对endpoints定期打乱,后续我们会升级到v3版本,这样同一个API Server的请求就不会一直落到某一个Etcd上,这样即使负载均衡策略失效也能做到对Etcd请求的分摊。
4. 查询Kubernetes资源资讯时带入resourceVersion开启快取机制,减轻对Etcd的访问压力。
4、 总结
从API Server卡顿问题排查过程来看,潜在的问题是长期存在的,只是积累到一定量后,问题的影响才会凸显。这就要求我们平时对Kubernetes相关元件的效能指标,日志等要保持时刻敏感,要对Kubernetes各种预设策略及引数非常熟悉,同时对于重要功能模组做到源代码层面了解,这样才能规避潜在风险和出问题后能快速定位,保证生产环境稳定健康的执行。欢迎大家关注“58架构师”微信公众号,定期分享云端计算、AI、区块链、大资料、搜寻、推荐、储存、中介软件、移动、前端、运维等方面的前沿技术和实践经验。