kube-apiserver内存溢出问题调查及go tool pprof工具的使用
问题描述
测试集群三台master,每个master上面的kube-apiserver都频繁的重启。登录其中一台master,发现kube-apiserver的内存占用特别高,每次重启完后内存很快就飙到了20G左右,而且还有继续增长的趋势。因为默认kube-apiserver的静态pod是没有设置memeory limit的,最终api-server会吃光机器的所有内存,导致master机器运行异常。
查看容器内存占用的命令:
docker stats --no-stream |grep kube-apiserver crio 使用 crictl container_id=`crictl ps -a | grep etcd` crictl stats $container_id
临时方案
出现问题后,没有找到问题根源,所以先修改了kube-apiserver的静态pod yaml文件,位于/etc/kubernetes/manifests/kube-apiserver.yaml,添加resources.limits.memory 为32g(本机内存为64g)。这样现在kube-apiserver的内存,使它消耗内存到32g的时候就oom kill自动重启, 大内存方案可行,如果内存较小,很快就会重启。
问题分析
api-server内存持续升高,肯定是有资源在堆内存里申请了空间但是没有释放。首先考虑是否是因为建立了太多的链接导致的,使用如下指令查询kube-apiserver链接数:
netstat -nat | grep -i "6443" | wc -l
发现链接数在100多并不算多。
继续分析只能考虑导出kube-apiserver的heap文件来查看其详细的内存分布。这种方式需要使用go语言包的pprof工具,下面详细讲解go tool pprof 工具的使用以及kube-apiserver的配置。
go tool pprof 工具使用
kube-apiserver集成了pprof工具,可以通过链接/debug/prof/heap的url来获得heap文件。
pprof是什么
pprof 是 go 语言自带的基础库,可以分析程序的运行情况,并且提供可视化的功能。它包含两个相关的库:runtime/pprof 和 net/http/pprof , net/http/pprof 可以提供一个 HTTP 接口,获得性能数据,它的底层使用的 runtime/pprof
pprof如何使用
一般如果我们使用 net/http/pprof 并开启了端口监听,如8001端口,我们就可以通过浏览器访问该服务pprof的页面,例如 http://127.0.0.1:8080/debug/pprof/
我们常用的参数为
allocs | 所有过去内存分配的采样 |
goroutine | 所有当前goroutine的堆栈跟踪 |
heap | 对活动对象的内存分配进行采样 |
profile | cpu profile采样 |
刚才分析的是 heap 内存情况,同样可以根据需要分析前面说的所有采样。
#所有过去内存分配的采样 go tool pprof http://127.0.0.1:8001/debug/pprof/allocs #对活动对象的内存分配进行采样(活动) go tool pprof http://127.0.0.1:8001/debug/pprof/heap # 下载 cpu profile,默认从当前开始收集 30s 的 cpu 使用情况,需要等待 30s go tool pprof http://127.0.0.1:8001/debug/pprof/profile # wait 120s go tool pprof http://127.0.0.1:8001/debug/pprof/profile?seconds=120 #导致同步原语阻塞的堆栈跟踪 go tool pprof http://127.0.0.1:8001/debug/pprof/block #所有当前goroutine的堆栈跟踪 go tool pprof http://127.0.0.1:8001/debug/pprof/goroutine #争用互斥锁持有者的堆栈跟踪 go tool pprof http://127.0.0.1:8001/debug/pprof/mutex #当前程序的执行轨迹。
使用 pprof 分析 kubelet 性能
想要获取 kubelet 的 pprof 数据,首先需要使用kubectl proxy命令启动API server代理
# kubectl proxy --address='0.0.0.0' --accept-hosts='^*$' Starting to serve on [::]:8001
#新开一个terminal,执行如下指令,进入交互界面
#输入 ./go tool pprof http://127.0.0.1:8001/debug/pprof/heap # 输入top 20 #进入交互界面后,输入top 20查看内存使用前20的函数调用 Fetching profile over HTTP from http://127.0.0.1:8001/debug/pprof/heap Saved profile in /root/pprof/pprof.kube-apiserver.alloc_objects.alloc_space.inuse_objects.inuse_space.004.pb.gz File: kube-apiserver Type: inuse_space Time: Dec 10, 2020 at 2:46pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof)top 20 #得到如下输出 Showing nodes accounting for 11123.22MB, 95.76% of 11616.18MB total Dropped 890 nodes (cum <= 58.08MB) Showing top 20 nodes out of 113 flat flat% sum% cum cum% 9226.15MB 79.43% 79.43% 9226.15MB 79.43% bytes.makeSlice 1122.61MB 9.66% 89.09% 1243.65MB 10.71% k8s.io/kubernetes/vendor/k8s.io/api/core/v1.(*ConfigMap).Unmarshal 139.55MB 1.20% 90.29% 153.05MB 1.32% k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/apis/meta/v1.(*ObjectMeta).Unmarshal 120.53MB 1.04% 91.33% 165.03MB 1.42% encoding/json.(*decodeState).objectInterface 117.29MB 1.01% 92.34% 117.29MB 1.01% reflect.unsafe_NewArray 108.03MB 0.93% 93.27% 108.03MB 0.93% reflect.mapassign 66.51MB 0.57% 93.84% 66.51MB 0.57% k8s.io/kubernetes/vendor/github.com/json-iterator/go.(*Iterator).ReadString 62.51MB 0.54% 94.38% 62.51MB 0.54% k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/registry/generic.ObjectMetaFieldsSet 61.51MB 0.53% 94.91% 61.51MB 0.53% k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource.objectMetaFieldsSet 43.01MB 0.37% 95.28% 137.03MB 1.18% k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/runtime.structToUnstructured 18MB 0.15% 95.43% 183.61MB 1.58% k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/storage/cacher.(*watchCache).Replace 13.50MB 0.12% 95.55% 137.03MB 1.18% k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/runtime.toUnstructured 9MB 0.077% 95.63% 237.54MB 2.04% k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.unstructuredJSONScheme.decodeToUnstructured 7.50MB 0.065% 95.69% 144.53MB 1.24% k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/runtime.(*unstructuredConverter).ToUnstructured 6MB 0.052% 95.74% 123.29MB 1.06% reflect.MakeSlice 1.50MB 0.013% 95.76% 8944.02MB 77.00% encoding/json.mapEncoder.encode
通过heap文件输出,可以看到占用内存最多的是makeSlice函数占用了9G多的内存。它的调用者是v1.(*ConfigMap).Unmarshal,configmap是kubernetes的一种资源,Unmarshal是json和struct做转换的函数。所以怀疑与configmap资源有关。
在宿主机上使用
kubectl get configmap -A 发现命令卡死没有输出,但是在特定的某一个namespace下进行kubectl get configmap是有返回的。所以怀疑其中的某一个namespace下有大量的configmap,kubectl get configma卡死从而导致kubectl get configmap -A卡死。
下面就尝试在每一个namespace下执行kubectl get configmap。当执行到kubectl get configmap -n kubesphere-controls-system,时发现命令卡住,在等待了3分钟后有数据返回。使用wc统计了数量发现kubesphere-controls-system下有17万多的configmap,大多是kubeconfig-xxxx样子的。后面的xxx是用户名。使用kubectl get user同样发现了有17万多的用户。应该是之前同步的ldap的用户,每创建一个用户,就会在kubesphere-controls-system下新建一个configmaps。查看etcd db文件,发现达到了1.5g左右,其他正常的集群db文件一般在50M甚至更低。
分析到这里,问题基本明朗了。可能是kubesphere 2.x版本时同步了ldap的数据到kubesphere中,kubesphere每创建一个user就会新建个对应的configmap里面存着key和cert。17万configmap,导致api-server去list configmap时无法一次获取到,就会一直创建slice(这里应该是golang语言包里的一个bug https://studygolang.com/artic...,从而使api-server耗尽内存
解决方案
最终的解决方案就是删除这些ldap用户,kubesphere升级到3.0后每一次登录都直接到ldap验证,且是在host集群上执行的。被纳管集群不需要存贮这些数据。在使用kubectl delete user发现用户无法删除,因为etcd数据量太大了,kube-apiserver与etcd之间的调用性能下降很严重。kube-apiserver已经无法正常的提供服务了。所以考虑使用etcdctl直接链接etcd来删除数据
#删除 user ETCDCTL_API=3 etcdctl --endpoints https://10.250.7.21:2379 --cacert $ETCD_TRUSTED_CA_FILE --cert $ETCD_CERT_FILE --key $ETCD_KEY_FILE del /registry/iam.kubesphere.io/users/ --prefix #删除对应的 configmap ETCDCTL_API=3 etcdctl --endpoints https://10.250.7.21:2379 --cacert $ETCD_TRUSTED_CA_FILE --cert $ETCD_CERT_FILE --key $ETCD_KEY_FILE del /registry/configmaps/kubesphere-controls-system/kubeconfig- --prefix
删除完数据后,使用docker restart 重启kube-apiserver,观测了一会,发现内存一直保持在1g左右。kubectl操作以及sit集群的web页面响应都比之前快了许多。
参考:
https://segmentfault.com/a/1190000039649589
https://blog.csdn.net/buppt/article/details/127505818