一、在k8s集群中重启容器的方法
1、使用Rolling Restart(滚动重启):
如果使用的是Deployment,StatefulSet等控制器,可以通过更新相关的Pod模板或配置来触发滚动重启。Kubernetes将逐步替换现有的Pod,确保新的Pod逐步启动并替代旧的Pod。这可以通过修改相关资源的定义,然后应用这些更改来实现。
例如,使用kubectl编辑Deployment,然后保存文件并退出编辑器,Kubernetes将开始滚动更新。
kubectl edit deployment <deployment-name>
2、删除Pod:
直接删除Pod将导致控制器(例如Deployment)创建新的Pod,实现容器的重启。您可以使用以下命令删除Pod,触发新的Pod的启动,以替代被删除的Pod。
kubectl delete pod <pod-name>
3、使用kubectl rollout restart(从Kubernetes v1.15版本开始):
从Kubernetes v1.15版本开始,kubectl提供了一个方便的命令来触发滚动重启,滚动重启与指定Deployment相关联的Pod,这并不会一次性杀死pod,而是滚动重启,比较平滑。推荐这种方法。
kubectl rollout restart deployment <deployment-name>
4、kubectl scale
先把副本数设置为0,然后再把副本数调整回来,但是会中断服务,因为副本数会先被设置为0。不建议通过该方法重启pod。
kubectl scale deployment <deployment name> -n <namespace> --replicas=0
kubectl scale deployment <deployment name> -n <namespace> --replicas=3
5、kubectl replace 替换资源
通过更新Pod的资源 ,从触发k8s pod 的更新,不建议通过这样来重启pod。
kubectl replace -f new-deployment.yaml
kubectl get pod <pod_name> -n <namespace> -o yaml | kubectl replace --force -f -
6、kubectl set env 设置环境变量
通过 设置环境变量,其实也是更新pod spec 从而触发滚动升级。不建议通过这样来重启pod。
kubectl set env deployment <deployment name> -n <namespace> DEPLOY_DATE="$(date)"
只不过这里通过kubectl 命令行,当我们通过API 更新pod spec 后一样会触发滚动升级
7、kill 1 杀掉init进程
这种方法就是在容器里面 kill 1 号进程。这种方法:必须要求你的 1 号进程要 捕获 SIGTERM 信号,否则在容器里面是杀不死自己的。可行,但是需要init进程能够捕获 SIGTERM 信号,这样pod不需要重建,pod在集群内的ip也不会改变。因为当 pod 被停掉后,K8S 会自动调度把它给启动。
kubectl exec -it <pod_name> -c <container_name> --/bin/sh -c "kill 1"
二、原地重启
上面提到的几种重启方法中,除kill命令杀掉init进程外,其他都是会重新构建pod,然后pod会被调度,在集群内的ip会改变。而通过kill关掉init进程的话,可以实现让容器原地重启,并且不会改变pod的ip,不需要重新构建pod。
三、理解1号进程/init进程
在容器中运行程序的最理想的状态就是只运行一个进程。但是现实是有时候是做不到的,例如有时候需要启动子进程来运行日志落盘、同步或备份等工作。或者说本来我们运行的程序就是多进程的。一旦我们在容器中启动了多个进程,那么容器中就会出现一个进程号为 1 的进程。这个就是常说的1号进程或者init进程,并由这个1号进程来创建子进程。
目前主流的 Linux 发行版,无论是 RedHat 还是 Debian,都会把 /sbin/init
作为符号链接指向 Systemd。Systemd 是目前最流行的 Linux init 进程。但是无论是哪种 Linux init 进程,最基本的功能就是创建出 Linux 系统中其他的所有的进程,并管理这些进程。
Linux 内核源码:
init/main.c
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. ","See Linux Documentation/admin-guide/init.rst for guidance.");
在 Linux 系统中执行命令ls -l /sbin/init
可以看到是链接指向systemd
。
ls -l /sbin/init
lrwxrwxrwx 1 root root 22 1月 8 20:10 /sbin/init -> ../lib/systemd/systemd
Linux 上的容器建立了自己的Pid Namespace
后,这个 Namespace 里面的进程号也是从1号开始标记。所以容器的 init 进程也被称为1号进程。具体可以看一下:实现容器化的基础:Namespace和Cgroups
3.1 Linux信号
通过kill -l
命令查看 Linux 里的信号,这里信号的编号是从1开始编号,信号其实就是 Linux 进程收到的一个通知。Linux 进程收到不同的信号可以做一些事情,例如:收到终止进程的信号,进程就会自我了结并清理进程数据,回收系统资源等操作。
以下的是我在 Centos8 上的执行结果:
kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
以我们最常见的场景为例:
-
按下键盘的
Ctrl+C
、Control+C
,会向当前进程发送一个 SIGINT 信号而退出。 -
如果代码写得有问题,导致访问内存出错,当前的进程就会收到 SIGSEGV(11) 信号。
-
kill -9 进程号 或 kill 进程号。其中 -9 就是向进程号对应的进程发送 SIGKILL 信号(编号为 9 的信号),如果没指定在缺省的情况下这个信号是 SEGTERM。
3.2 SEGTERM信号和SIGKILL信号
在进程收到信号后,有三个选择:
- 忽略:就是进程对这个信号不做任何处理,但是对于 SEGTERM(15) 信号和 SIGKILL(9) 信号来说,这两个信号不能忽略,因为这是终止进程的操作,这是为 Linux 内核和超级用户提供删除任意进程的权限。
- 捕获:这是指用户进程可以注册自己针对这个信号的 handler。例如:JVM 注册关闭钩子,在 java 应用关闭的时候,正确关闭线程池,回收系统资源,防止内存泄漏等。
- 缺省:Linux 为每个信号都定义了一个缺省的行为,在 Linux 系统可以执行命令:
man 7 signal
来查看每个信号的缺省行为。
但是 SIGSTOP(19) 和 SIGKILL(9) 信号除外,这两个信号不能有用户自己的处理,即用户程序不能捕获到这两个终止进程的信号,只能执行缺省行为(Default)。所以我们经常可以看一些文章和规范说到:关闭进程不要使用 kill -9 进程号
,要使用kill 进程号
的原因。因为 kill 进程号
默认是向进程发送 SEGTERM(15) 信号,这是用户程序可以捕获到的。
SIGKILL(9)是 Linux 的特权信号之一。特权信号就是 Linux 为内核和超级用户去删除进程所保留的,不能被忽略和捕获。所以进程一旦收到该信号,就要退出。
3.3 实操为什么 kill -9 1 都无法干掉 1 号进程
1、Dockfile构建一个单进程镜像
如果是Mac M1/M2/M3 芯片,需要使用支持arm64的镜像和安装包。
# 使用CentOS官方镜像作为基础镜像
FROM centos:latest
RUN set -eux; \
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*; \
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*; \
yum clean all; \
yum makecache; \
yum -y install gcc \
gcc-c++ \
gdb \
make \
zlib \
zlib-devel \
glibc-devel \
pcre \
pcre-devel \
cmake; \
yum clean all; \
yum makecache
# 安装Go语言编译器和标准库
ENV GO_VERSION 1.15.6
# arm64 架构
RUN curl -SLO "https://dl.google.com/go/go$GO_VERSION.linux-arm64.tar.gz" \
&& tar -C /usr/local -xzf "go$GO_VERSION.linux-arm64.tar.gz" \
&& rm "go$GO_VERSION.linux-arm64.tar.gz"
# amd64 架构
#RUN curl -SLO "https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz" \
# && tar -C /usr/local -xzf "go$GO_VERSION.linux-amd64.tar.gz" \
# && rm "go$GO_VERSION.linux-amd64.tar.gz"
# 设置Go语言环境变量
ENV PATH="/usr/local/go/bin:${PATH}"
# 设置工作目录
WORKDIR /
# 复制源代码
COPY ./c-sleep-nosig.c /
COPY ./c-sleep-sig.c /
COPY ./go-sleep.go /
COPY ./sleep.sh /
# 编译Go代码
RUN cd / && go build ./go-sleep.go
# 编译C代码(假设有一个名为myapp.c的C源文件)
RUN cd / && gcc -I/usr/include c-sleep-nosig.c -o c-sleep-nosig
RUN cd / && gcc -I/usr/include c-sleep-sig.c -o c-sleep-sig
# 设置容器启动时执行的命令
CMD ["/bin/bash"]
构建镜像
docker build . test-sig:latest
- c-sleep-nosig.c 没处理信号
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
printf("Process is sleeping\n");
while (1) {
sleep(100);
}
return 0;
}
- c-sleep-sig.c 有处理信号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void sig_handler(int signo){
if (signo == SIGTERM) {
printf("received SIGTERM\n");
exit(0);
}
}
int main(int argc, char *argv[]){
signal(SIGTERM, sig_handler);
printf("Process is sleeping\n");
while (1) {
sleep(100);
}
return 0;
}
- go-sleep.go
package main
import (
"fmt";
"time";
)
func main() {
fmt.Println("Start app\n")
time.Sleep(time.Duration(100000) * time.Millisecond)
}
- sleep.sh
#!/bin/bash
while true
do
sleep 100
done
3.3.1 没注册信号处理:c-sleep-nosig 启动容器
运行容器,执行c-sleep-nosig:docker run --name test-sig -d test-sig /c-sleep-nosig
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f68c80231d7d test-sig "/c-sleep-nosig" 2 seconds ago Up 2 seconds test-sig
进入容器:docker exec -it test-sig /bin/bash
。进入到容器可以看到,1号进程是运行容器的时候执行c-sleep-nosig
,/bin/bash
是我们使用交互的形式进入容器内,ps -ef
是我们查看容器内的进程是执行的。可以看到在正常情况下,我们创建出来的容器内是只有一个进程的。
docker exec -it test-sig /bin/bash
[root@f68c80231d7d /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 23:24 ? 00:00:00 /c-sleep-nosig
root 7 0 0 23:24 pts/0 00:00:00 /bin/bash
root 21 7 0 23:24 pts/0 00:00:00 ps -ef
把1号进程杀掉,分别执行kill 1
和kill -9 1
,发现当前会话仍在容器里面,说明1号进程仍然没被杀死,因为容器还在运行中。
[root@f68c80231d7d /]# kill 1
[root@f68c80231d7d /]# kill -9 1
[root@f68c80231d7d /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 23:24 ? 00:00:00 /c-sleep-nosig
root 7 0 0 23:24 pts/0 00:00:00 /bin/bash
root 22 7 0 23:24 pts/0 00:00:00 ps -ef
可以看到,甚至是kill -9 1
都无法停掉1号进程。这是为什么呢?
kill 1
(用户态) -> sys_kill()
(内核态) -> _send_signal()
(内核态) -> sig_task_ignored()
(内核态) -> 1号进程 init proc
(用户态)
要想知道为什么 init 进程为什么收到或者收不到信号,需要去看一下Linux 内核代码kernel/signal.c
中的sig_task_ignored()
方法,一旦这三个条件都满足,那么这个信号就不会发送给进程。
static bool sig_task_ignored(struct task_struct *t, int sig, bool force)
{
void __user *handler;
handler = sig_handler(t, sig);
/* SIGKILL and SIGSTOP may not be sent to the global init */
if (unlikely(is_global_init(t) && sig_kernel_only(sig)))
return true;
if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) &&
handler == SIG_DFL && !(force && sig_kernel_only(sig)))
return true;
/* Only allow kernel generated signals to this kthread */
if (unlikely((t->flags & PF_KTHREAD) &&
(handler == SIG_KTHREAD_KERNEL) && !force))
return true;
return sig_handler_ignored(handler, sig);
}
第一个if条件,注释说了SIGKILL
信号和SIGSTOP
信号可能不会发送给 init 进程。
第三if条件,注释也说了只允许内核生成的信号到此线程
第二条件,需要关注分析。
第一个子条件:t->signal->flags & SIGNAL_UNKILLABLE
,进程必须是SIGNAL_UNKILLABLE
。可看内核代码kernel/fork.c
中注释也写到如果当前 Namespace 的 init 进程建立的时候会打上SIGNAL_UNKILLABLE
标签。所以第一个子条件是满足的。
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
/*
* is_child_reaper returns true if the pid is the init process
* of the current namespace. As this one could be checked before
* pid_ns->child_reaper is assigned in copy_process, we check
* with the pid number.
*/
static inline bool is_child_reaper(struct pid *pid)
{
return pid->numbers[pid->level].nr == 1;
}
第二个子条件handler == SIG_DEL
,是判断 handler 是否是 SIG_DEL。如果用户进程不注册一个自己的 handler,系统会有一个缺省的 handler,这个缺省的 handler 就是 SIG_DEL。如果使用的是 kill -9 1
,信号 SIGKILL 是不允许捕获的,所以 handler 是 SIG_DEL。如果使用 kill 1
,信号是 SIGTERM,但是启动容器时用的是c-sleep-nosig
这个文件,并没有注册 handler,所以也是 SIG_DEL。所以此时无论是kill -9 1
还是kill 1
都是满足的。
第三个子条件:对应同一个 Namespace 里发出的信号来说,调用值是0,这个条件也是满足的。
由此可见,要想要停掉 1 号进程,在用户态能做的就是让 handler != SIG_DEL
,使用kill 1
,同时 1 号进程需要注册一个 handler。
3.3.2 用户注册 handler 处理 SIGTERM 信号:c-sleep-sig 启动容器
现将之前启动的容器给停了,并删除。
docker stop test-sig;docker rm test-sig
运行容器,执行c-sleep-sig:docker run --name test-sig -d test-sig /c-sleep-sig
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3e06592d2ac6 test-sig "/c-sleep-sig" 40 seconds ago Up 40 seconds test-sig
进入容器内部,当执行完kill 1
后,就会发现自动地退出了当前会话。再执行docker ps
便可以发现,容器已经被停止了。
docker exec -it test-sig /bin/bash
[root@3e06592d2ac6 /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:39 ? 00:00:00 /c-sleep-sig
root 7 0 0 09:41 pts/0 00:00:00 /bin/bash
root 21 7 0 09:41 pts/0 00:00:00 ps -ef
[root@3e06592d2ac6 /]# kill 1
3.4 验证 SIGTERM 为什么能杀死 1 号进程
像运行在JVM上的Java程序,Go程序等,都有注册一些信号的handler,SIGTERM(15)信号也有注册handler。可以查看 1 号进程的 SigCgt Bitmap 来看一下是不是15,因为 SIGTERM 序号是 15。SigCgt 中存放的是进程会去捕获的信号。
- 1 号进程运行 Go 程序:go-sleep
docker run -d --name test-sig-go test-sig /go-sleep
docker exec -it test-sig-go /bin/bash
[root@de32c79d7b99 /]# cat /proc/1/status | grep -i SigCgt
SigCgt: fffffffe7fc1feff
- 1 号进程运行 shell 脚本:sleep.sh
docker run -d --name test-sig-shell test-sig /bin/bash sleep.sh
docker exec -it test-sig-shell /bin/bash
[root@428cf81f02bd /]# cat /proc/1/status | grep -i SigCgt
SigCgt: 0000000000010002
- 1 号进程运行 C 程序注册 SIGTERM handler:
docker run -d --name test-sig test-sig /c-sleep-sig
docker exec -it test-sig /bin/bash
[root@304983e5bb39 /]# cat /proc/1/status | grep -i SigCgt
SigCgt: 0000000000004000
- 1 号进程运行 C 程序不注册 handler:
$ docker run -d --name test-sig test-sig /c-sleep-nosig
docker exec -it test-sig /bin/bash
[root@9fec3cb314a6 /]# cat /proc/1/status | grep -i SigCgt
SigCgt: 0000000000000000
怎么解读 SigCgt 呢?这一串的值其实是位掩码,以 sleep.sh 脚本注册的捕获的信号0000000000010002
为例:第二位和第五位有1,也就是 bit2 和 bit17 (二进制)。所以 sleep.sh 脚本是能捕获 SIGINT 和 SIGCHLD 信号。
SigCgt: 0000000000010002 -> BitMask:0001 0000 0000 0000 0010
C 程序 c-sleep-sig 注册了 SIGTERM 信号的 handler,所以看到 bit15 为1,对应就是 SIGTERM 的序号。
SigCgt: 0000000000004000 -> BitMask:0100 0000 0000 0000
由此可见,上面的结果可以看到 Go 程序是注册了很多的信号handler;C 程序在缺省(c-sleep-nosig)的情况下一个信号的handler都没注册;bash 运行简单的shell脚本(sleep.sh)的时候是注册了两个信号的handler,bit2 和 bit 17,分别是 SIGINT 和 SIGCHLD 信号,并没有注册 SIGTERM 信号。而 C 程序 c-sleep-sig 注册了 SIGTERM 信号的handler,可以看到 bit15 上有值。
四、总结
从 kill 掉 pod 的1号进程原地重启开始,到解析 kill 1 和 kill -9 1,再到 Linux 信号处理,可以说是十分清晰了,大家可以去跟别人吹牛了。
相关文章
- # 实现容器化的基础:Namespace和Cgroups