一、背景介绍
公司部署的 Kubernetes 集群偶尔会出现容器杀不死的情况,影响了应用程序的正常运行
本着解决问题的态度,博主查看了 kubernetes 源码,尤其是 kubelet 部分,对其中的部分逻辑进行了增强
可能这不是问题的真正症结所在,但间接地也一定程度解决了问题,下边简单记录下源码修改的过程
二、源码分析
源码地址:https://github.com/kubernetes/kubernetes
新版本的 kubernetes(这里是 1.10.4),不再直接操作 docker ,而是使用了 CRI (容器运行时)接口间接调用
之所以这么设计,主要是为了屏蔽容器的具体实现(目前的容器实现不止 Docker,还有诸如 rkt、gVisor 等等)
kuernetes 只需要面向 CRI 接口编程就可以,无需关注具体容器的实现细节
使用 CRI 实现的逻辑架构如下:
Kubelet 的代码中提供了 CRI shim 的实现代码,kubelet 自己既充当客户端,又充当服务器,之间通过 gRPC 调用
实际操作 Docker 的过程通过 CRI shim 实现
因此,增强杀死 container 的代码也许可以嵌入到 CRI shim 的代码实现中,我实际也是这么做的
好了,下面就是去找 CRI shim 中关于杀死 container 的代码了
CRI shim 的源码地址在 pkg/kubelet/dockershim/libdocker 中
首先,查看 client.go
// Interface is an abstract interface for testability. It abstracts the interface of docker client.
type Interface interface {
ListContainers(options dockertypes.ContainerListOptions) ([]dockertypes.Container, error)
InspectContainer(id string) (*dockertypes.ContainerJSON, error)
InspectContainerWithSize(id string) (*dockertypes.ContainerJSON, error)
CreateContainer(dockertypes.ContainerCreateConfig) (*dockercontainer.ContainerCreateCreatedBody, error)
StartContainer(id string) error
StopContainer(id string, timeout time.Duration) error
UpdateContainerResources(id string, updateConfig dockercontainer.UpdateConfig) error
RemoveContainer(id string, opts dockertypes.ContainerRemoveOptions) error
InspectImageByRef(imageRef string) (*dockertypes.ImageInspect, error)
InspectImageByID(imageID string) (*dockertypes.ImageInspect, error)
ListImages(opts dockertypes.ImageListOptions) ([]dockertypes.ImageSummary, error)
PullImage(image string, auth dockertypes.AuthConfig, opts dockertypes.ImagePullOptions) error
RemoveImage(image string, opts dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error)
ImageHistory(id string) ([]dockerimagetypes.HistoryResponseItem, error)
Logs(string, dockertypes.ContainerLogsOptions, StreamOptions) error
Version() (*dockertypes.Version, error)
Info() (*dockertypes.Info, error)
CreateExec(string, dockertypes.ExecConfig) (*dockertypes.IDResponse, error)
StartExec(string, dockertypes.ExecStartCheck, StreamOptions) error
InspectExec(id string) (*dockertypes.ContainerExecInspect, error)
AttachToContainer(string, dockertypes.ContainerAttachOptions, StreamOptions) error
ResizeContainerTTY(id string, height, width uint) error
ResizeExecTTY(id string, height, width uint) error
GetContainerStats(id string) (*dockertypes.StatsJSON, error)
}
发现,其中并没有诸如 KillContainer 的接口,只有一个 StopContainer 方法,
(也许就是 Stop 不够干脆导致的问题?我的心里萌生了这样的念头)
继续查找具体实现,在 pkg/kubelet/dockershim/libdocker/kube_docker_client.go 中
// Stopping an already stopped container will not cause an error in dockerapi.
func (d *kubeDockerClient) StopContainer(id string, timeout time.Duration) error {
ctx, cancel := d.getCustomTimeoutContext(timeout)
defer cancel()
err := d.client.ContainerStop(ctx, id, &timeout)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
源码确实是调用了 docker client 的 ContainerStop 方法
继续查看 docker client 提供的接口,发现不止提供了 Stop 方法,同样提供了 Kill 方法
func (cli *Client) ContainerStop(ctx context.Context, containerID string, timeout *time.Duration) error {
query := url.Values{}
if timeout != nil {
query.Set("t", timetypes.DurationToSecondsString(*timeout))
}
resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil)
ensureReaderClosed(resp)
return err
}func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error {
query := url.Values{}
query.Set("signal", signal)resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil)
ensureReaderClosed(resp)
return err
}
至此,一个尝试方案是在 StopContainer 函数中,判断 Stop 是否成功,不成功就补一刀,调用 Kill 强制杀死容器
三、修改源码并编译替换
修改 pkg/kubelet/dockershim/libdocker/kube_docker_client.go 的 StopContainer 函数,
增加强制 Kill 逻辑,改后如下
func (d *kubeDockerClient) StopContainer(id string, timeout time.Duration) error {
ctx, cancel := d.getCustomTimeoutContext(timeout)
defer cancel()
err := d.client.ContainerStop(ctx, id, &timeout)
if err != nil {
err = d.client.ContainerKill(ctx, id, "SIGKILL")
}
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
执行 make quick-release 开始编译(这步需要下载相关镜像,需要 Cross the Great Wall,你懂的)
编译完后,新版的 kubelet 文件位于 _output/release-stage/server/linux-amd64/kubernetes/server/bin
我们将新版本的 kubelet 替换原来的 kubelet,执行 systemctl restart kubelet 就能启用了
好了,本文就到此结束了,不对的地方还请指正!