k8s 容器热替换 / 重启主过程 – gdb execve syscall 法
指标
k8s 环境下,在不进行或重启 container 的状况下,重启利用过程(pid:1),甚至从新加载运行新版本的利用。本文以 gdb 作为工具,调用内核的 close / execve syscall,去实现这个指标。
背景
K8s 显然曾经由衰亡转向成熟。大潮过后,是时候思考一下,当初吹过的牛有哪些是真的,哪些是还未对现的。不可否认,k8s 改革了运维的工作形式,这根本是提高的。但对于开发,特地是阻碍问题定位、程序调试办法,显然难度是减少了。
在利用的阻碍问题定位、程序调试时,咱们时常心愿能在雷同的环境下反复重启利用,去察看咱们对配置的批改,或者程序的更新是否真正解决了问题。要实现这个指标,通常须要:
- 批改利用代码,跑 CI pipeline 从新打包 docker image,上传。—— 费时费力 😱
- 想方法重启容器。—— 环境毁坏了,问题可能重现不了 😱
如果容器启动脚本设计时就反对重启,当然没问题,但大部分状况下,均是不间接反对的,很多时候,利用主过程就间接是 container 的主过程 pid:1 了。
我钻研过三个办法去替换主过程:
- gdb 调用 libc 的 execl。
- gdb 调用 syscall execve。这个办法比较复杂,但也更通用,这是本文要说的办法。
kill -STOP
挂起主过程的父过程- gdb 主过程的父过程,让它收不到
SIGCHLD
知识点
- Linux 的一些内存布局
- Linux 的过程启动参数和环境变量布局
- Linux /proc/$pid/stat 的小机密
- syscall 常识
- x86 一点寄存器常识
- gdb 大法之魔幻
系列简介
《k8s 容器热替换 / 重启主过程》是一个系列的文章,指标都是雷同的,但在不同的状况下应用不同的伎俩:
-
k8s 容器热替换 / 重启主过程 – gdb exec 法
- 长处:应用比拟不便
- 毛病:依赖可执行文件应用了 glibc.so。golang 编写生成的可执行文件,通常不应用 glibc.so
-
k8s 容器热替换 / 重启主过程 – gdb execve syscall 法(本文)
- 长处:不依赖 glibc.so,golang 编写生成的可执行文件可用。
- 毛病:应用有点麻烦,须要理解一些 linux 过程内存布局、一点点 x86 64bit 汇编入门。
正告
因为本文应用了 gdb attach 和非常规办法敞开文件描述符 (close(fd)) 和替换过程执行文件(execve),潜在比拟大的未知危险,请不要在生产环境中应用。我也未充沛验证这个办法的可靠性,和副作用。包含 close 和 execve 是否能洁净清理后任的问题。所以,应用有危险。
思路
- gdb attach 过程
- 调用
close
fd syscall,特地是 socket 相干的,特地是 listen tcp port 的。 - 调用
execve
syscall,以雷同的启动参数和环境变量,执行雷同的可执行文件
其中有几个难点:
- gdb 下,在过程无加载 glibc 时调用 syscall
-
execve
须要一些入参,包含- char* filepath:执行文件地位
- char * argv[]:启动参数
- char *envp[]:环境变量
筹备常识
如果你不太理解 syscall 原理,可能是时候补补课了。以下是一些我的参考资料:
- https://blog.packagecloud.io/the-definitive-guide-to-linux-system-calls/
- https://www.juliensobczak.com/inspect/2021/08/10/linux-system-calls-under-the-hood.html
- https://cs61.seas.harvard.edu/site/2021/Kernel/
寄存器
要调用 execve syscall,须要一些参数:
- char* filepath:执行文件地位
- char * argv[]:启动参数
- char *envp[]:环境变量
详见:https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
NR | syscall name | references | %rax | arg0 (%rdi) | arg1 (%rsi) | arg2 (%rdx) | arg3 (%r10) | arg4 (%r8) | arg5 (%r9) |
---|---|---|---|---|---|---|---|---|---|
0 | read | man/ cs/ | 0x00 | unsigned int fd | char *buf | size\_t count | – | – | – |
… | |||||||||
3 | close | man/ cs/ | 0x03 | unsigned int fd | – | – | – | – | – |
… | |||||||||
59 | execve | man/ cs/ | 0x3b | const char *filename | const char const argv | const char const envp | – | – | – |
60 | exit | man/ cs/ | 0x3c | int error_code | – | – | – | – | – |
… |
其中,%rax 寄存器寄存 syscall 的 id。%rdi / %rsi / %rdx 别离寄存第一到三个参数。
syscall 机器指令
过程从用户态过程内核态,CPU 须要执行一个计算机指令码
计算机指令名字:syscall
计算机指令编码:0x050f
具体的原理阐明可见:https://thomasw.dev/post/killbutmakeitlooklikeanaccident/
gdb 不间接反对在过程中长期间接 eval(执行) 一个汇编指令。咱们只能另想它法:间接把指令码写到 %rip
寄存器指向的内存地址中。原理是 %rip
就是指向以后线程下一个要执行的指令,而咱们就是想马上执行一个 syscall。
步骤
搭建试验指标环境
环境阐明:
-
node: 192.168.122.55
- Ubuntu 22.04.2 LTS
- kernel: 5.4.0-152-generic
- hostname: worknode5
- gdb 9.2
我应用 docker.io/fortio/fortio 作为例子:
docker pull docker.io/fortio/fortio
fortio 是一个 http 测试服务端,listen 了一些端口。它是由 golang 编写的动态链接的可执行文件。即不应用 glibc 动静库。
运行 pod:
kubectl delete StatefulSet fortio-server-l2
kubectl apply -f - <<"EOF"
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: fortio-server-l2
name: fortio-server-l2
spec:
podManagementPolicy: OrderedReady
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: fortio-server-l2
serviceName: ""
template:
metadata:
labels:
app: fortio-server-l2
app.kubernetes.io/name: fortio-server-l2
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worknode5
containers:
- args:
- server
- -M
- 8070 http://fortio-server-l2:8080
command:
- /usr/bin/fortio
image: docker.io/fortio/fortio
imagePullPolicy: IfNotPresent
name: main-app
ports:
- containerPort: 8080
name: http
protocol: TCP
- containerPort: 8070
name: http-m
protocol: TCP
- containerPort: 8079
name: grpc
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: docker-registry-key
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
EOF
查看 fortio 过程
ssh labile@192.168.122.55 # ssh 到运行的 worker node
# get fortio PID
export POD="fortio-server-l2-0"
fortio_pids=$(pgrep fortio)
while IFS= read -r fortio_pid; do
HN=$(sudo nsenter -u -t $fortio_pid hostname)
if [["$HN" = "$POD"]]; then # space between = is important
sudo nsenter -u -t $fortio_pid hostname
export POD_PID=$fortio_pid
fi
done <<< "$fortio_pids"
echo $POD_PID
export PID=$POD_PID
# 可执行文件无依赖,即也不会依赖 glibc
sudo ldd /proc/$PID/root/usr/bin/fortio
not a dynamic executable
ps -f -p $PID
UID PID PPID C STIME TTY TIME CMD
root 3589 3080 0 02:32 ? 00:00:00 /usr/bin/fortio server -M 8070 http://fortio-server-l2:8080
# 查看关上的文件与 socket(包含 listen socket 和 accepted socket )
ls -l /proc/$PID/fd
total 0
lrwx------ 1 root root 64 Jun 22 01:24 0 -> /dev/null`
l-wx------ 1 root root 64 Jun 22 01:24 1 -> 'pipe:[36078]'
lrwx------ 1 root root 64 Jun 22 01:25 10 -> 'socket:[36963]'
lrwx------ 1 root root 64 Jun 22 01:25 11 -> 'socket:[36965]'
l-wx------ 1 root root 64 Jun 22 01:24 2 -> 'pipe:[36079]'
lrwx------ 1 root root 64 Jun 22 01:25 3 -> 'socket:[36951]'
lrwx------ 1 root root 64 Jun 22 01:25 4 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Jun 22 01:25 5 -> 'pipe:[36522]'
l-wx------ 1 root root 64 Jun 22 01:24 6 -> 'pipe:[36522]'
lrwx------ 1 root root 64 Jun 22 01:25 7 -> 'socket:[36959]'
lrwx------ 1 root root 64 Jun 22 01:25 8 -> 'socket:[36960]'
lrwx------ 1 root root 64 Jun 22 01:25 9 -> 'socket:[36962]'
敞开文件与 listen socket
因为将启动过程也会 listen 雷同的端口。而 execve 自身只会清理内存和线程,不会敞开文件与 socket,所以得先手工敞开。
(gdb) info proc
process 3589
(gdb) shell ls -l /proc/3589/fd
total 0
lrwx------ 1 root root 64 Jun 22 01:24 0 -> /dev/null`
l-wx------ 1 root root 64 Jun 22 01:24 1 -> 'pipe:[36078]'
lrwx------ 1 root root 64 Jun 22 01:25 10 -> 'socket:[36963]'
lrwx------ 1 root root 64 Jun 22 01:25 11 -> 'socket:[36965]'
l-wx------ 1 root root 64 Jun 22 01:24 2 -> 'pipe:[36079]'
lrwx------ 1 root root 64 Jun 22 01:25 3 -> 'socket:[36951]'
lrwx------ 1 root root 64 Jun 22 01:25 4 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Jun 22 01:25 5 -> 'pipe:[36522]'
l-wx------ 1 root root 64 Jun 22 01:24 6 -> 'pipe:[36522]'
lrwx------ 1 root root 64 Jun 22 01:25 7 -> 'socket:[36959]'
lrwx------ 1 root root 64 Jun 22 01:25 8 -> 'socket:[36960]'
lrwx------ 1 root root 64 Jun 22 01:25 9 -> 'socket:[36962]'
# 手工一一 fd 敞开。其实能够思考用 gdb 内置的 while 循环,不过先手工吧
# close 的 syscall id
set $rax=0x03
# close 的 fd
set $rdi=10
#syscall
# 批改 $rip 指向的指令为 syscall
set {short}$rip = 0x050f
# 执行 $rip 指向的指令
stepi
(gdb) shell ls -l /proc/3589/fd
total 0
lrwx------ 1 root root 64 Jun 22 01:24 0 -> /dev/null
l-wx------ 1 root root 64 Jun 22 01:24 1 -> 'pipe:[36078]'
lrwx------ 1 root root 64 Jun 22 01:25 11 -> 'socket:[36965]'
..
lrwx------ 1 root root 64 Jun 22 01:25 9 -> 'socket:[36962]'
# 手工一一 fd 敞开。其实能够思考用 gdb 内置的 while 循环,不过先手工吧
...
set $rax=0x03
set $rdi=3
# syscall
set {short}$rip = 0x050f
stepi
....
(gdb) shell ls -l /proc/3589/fd
total 0
lrwx------ 1 root root 64 Jun 22 01:24 0 -> /dev/null
l-wx------ 1 root root 64 Jun 22 01:24 1 -> 'pipe:[36078]'
l-wx------ 1 root root 64 Jun 22 01:24 2 -> 'pipe:[36079]'
筹备 execve 的参数
要调用 execve syscall,须要一些参数:
- char* filepath:执行文件地位
- char * argv[]:启动参数
- char *envp[]:环境变量
过程启动的 argv
/proc/[pid]/stat
文件(文档),通知了咱们:
(48) arg_start %lu (since Linux 3.5) [PT]
Address above which program command-line arguments
(argv) are placed.
(49) arg_end %lu (since Linux 3.5) [PT]
Address below program command-line arguments (argv)
are placed.
(50) env_start %lu (since Linux 3.5) [PT]
Address above which program environment is placed.
(51) env_end %lu (since Linux 3.5) [PT]
Address below which program environment is placed.
能够通过以下办法获取过程里的内存块地址:
#argv
pid=$PID && a=(`sudo cat /proc/$pid/stat`) && echo ${a[48]}
140737488348939
能够看看这个 argv 内存块放了什么:
sudo gdb -p $PID
# 10 进制地址变为 16 进制
(gdb) p (void*)140737488348939
$4 = (void *) 0x7fffffffe70b
# 查看地址批
(gdb) x/100bc 0x7fffffffe70b
0x7fffffffe70b: 47 '/' 117 'u' 115 's' 114 'r' 47 '/' 98 'b' 105 'i' 110 'n'
0x7fffffffe713: 47 '/' 102 'f' 111 'o' 114 'r' 116 't' 105 'i' 111 'o' 0 '\000'
0x7fffffffe71b: 115 's' 101 'e' 114 'r' 118 'v' 101 'e' 114 'r' 0 '\000' 45 '-'
0x7fffffffe723: 77 'M' 0 '\000' 56 '8' 48 '0' 55 '7' 48 '0' 32 '' 104'h'0x7fffffffe72b: 116't'116't'112'p'58':'47'/'47'/'102'f'111'o'0x7fffffffe733: 114'r'116't'105'i'111'o'45'-'115's'101'e'114'r'0x7fffffffe73b: 118'v'101'e'114'r'45'-'108'l'50'2'58':'56'8'0x7fffffffe743: 48'0'56'8'48'0'0'\000'80'P'65'A'84'T'72'H'
还记得 ps 的输入吗?
ps -f -p $PID
UID PID PPID C STIME TTY TIME CMD
root 3589 3080 0 02:32 ? 00:00:00 /usr/bin/fortio server -M 8070 http://fortio-server-l2:8080
可见下面是个内存块,放了 char argv[][]
的内容。咱们晓得,有一个在 stack 中的 char* argv[]
指针数组,其中每一个 char* argv[x]
元素都会指向这个下面内存块的每个参数的首个字符。所以只有在 stack 中找到这个 char* argv[]
即可:
(gdb) info proc mappings
process 3589
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x9b1000 0x5b1000 0x0 /usr/bin/fortio
0x9b1000 0xf8b000 0x5da000 0x5b1000 /usr/bin/fortio
0xf8b000 0xfeb000 0x60000 0xb8b000 /usr/bin/fortio
0xfeb000 0x102c000 0x41000 0x0 [heap]
0xc000000000 0xc000400000 0x400000 0x0
...
0x7ffff7f9b000 0x7ffff7ffb000 0x60000 0x0
0x7ffff7ffb000 0x7ffff7ffe000 0x3000 0x0 [vvar]
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0 [vdso]
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
可见 statck 的空间是 0x7ffffffde000
到 0x7ffffffff000
,留神,不包含 0x7ffffffff000
。 0x7ffffffff000 - 1 = 0x7fffffffefff
。所以:
find 0x7ffffffde000, 0x7fffffffefff, 0x7fffffffe70b
0x7fffffffe3b8
可见,0x7fffffffe3b8 即是传给 main() 的 argv 了。
因程序自身的逻辑不同,不排除有可能找到多个在 stack 的指针指向
0x7fffffffe70b
过程启动环境变量
env
与 argv
同理,这里不说了。
# 获取 env[][] 内存块地址
pid=$PID && e=(`sudo cat /proc/$pid/stat`) && echo ${e[50]}
140737488348999
#获取 char* env[] 内存块地址
find 0x7ffffffde000, 0x7fffffffefff, 0x7fffffffe747
0x7fffffffe3e0
# 10 进制地址变为 16 进制
(gdb) p (void*)140737488348999
$4 = (void *) 0x7fffffffe747
可执行文件门路
path 能够间接用下面的 0x7fffffffe70b
。
执行 execve syscall
# execve 的 syscall id
set $rax=0x3b
# execve 的 三个参数
set $rdi=0x7fffffffe70b
set $rsi=0x7fffffffe3b8
set $rdx=0x7fffffffe3e0
# 批改 $rip 指向的指令为 syscall
set {short}$rip = 0x050f
# 执行 $rip 指向的指令
stepi
# gdb 离场
detach
quit
验证重启
$ kubectl logs -f fortio-server-l2-0
01:24:54 I scli.go:90> Starting Φορτίο 1.54.3 h1:c9WIOtp4A2lSvDLs1Y01S6yNirtAvaBJJnTzcv/9G/M= go1.20.4 amd64 linux
01:24:54 Fortio 1.54.3 tcp-echo server listening on tcp [::]:8078
01:24:54 Fortio 1.54.3 udp-echo server listening on udp [::]:8078
01:24:54 Fortio 1.54.3 grpc 'ping' server listening on tcp [::]:8079
01:24:54 Fortio 1.54.3 https redirector server listening on tcp [::]:8081
01:24:54 Fortio 1.54.3 http-echo server listening on tcp [::]:8080
01:24:54 Data directory is /var/lib/fortio
01:24:54 REST API on /fortio/rest/run, /fortio/rest/status, /fortio/rest/stop, /fortio/rest/dns
01:24:54 Debug endpoint on /debug, Additional Echo on /debug/echo/, Flags on /fortio/flags, and Metrics on /debug/metrics
01:24:54 Fortio 1.54.3 Multi on 8070 server listening on tcp [::]:8070
01:24:54 I http_forwarder.go:288> Multi-server on [::]:8070 running with &{Targets:[{Destination:http://fortio-server-l2:8080 MirrorOrigin:true}] Serial:false Name:Multi on [::]:8070 client:0xc0001f0f00}
01:24:54 I fortio_main.go:292> All fortio 1.54.3 h1:c9WIOtp4A2lSvDLs1Y01S6yNirtAvaBJJnTzcv/9G/M= go1.20.4 amd64 linux servers started!
UI started - visit:
http://localhost:8080/fortio/
(or any host/ip reachable on this server)
########### after restarted ############
03:08:08 I scli.go:90> Starting Φορτίο 1.54.3 h1:c9WIOtp4A2lSvDLs1Y01S6yNirtAvaBJJnTzcv/9G/M= go1.20.4 amd64 linux
03:08:08 Fortio 1.54.3 tcp-echo server listening on tcp [::]:8078
03:08:08 Fortio 1.54.3 udp-echo server listening on udp [::]:8078
03:08:08 Fortio 1.54.3 grpc 'ping' server listening on tcp [::]:8079
03:08:08 Fortio 1.54.3 https redirector server listening on tcp [::]:8081
03:08:08 Fortio 1.54.3 http-echo server listening on tcp [::]:8080
03:08:08 Data directory is /var/lib/fortio
03:08:08 REST API on /fortio/rest/run, /fortio/rest/status, /fortio/rest/stop, /fortio/rest/dns
03:08:08 Debug endpoint on /debug, Additional Echo on /debug/echo/, Flags on /fortio/flags, and Metrics on /debug/metrics
03:08:08 Fortio 1.54.3 Multi on 8070 server listening on tcp [::]:8070
03:08:08 I http_forwarder.go:288> Multi-server on [::]:8070 running with &{Targets:[{Destination:http://fortio-server-l2:8080 MirrorOrigin:true}] Serial:false Name:Multi on [::]:8070 client:0xc000254f00}
03:08:08 I fortio_main.go:292> All fortio 1.54.3 h1:c9WIOtp4A2lSvDLs1Y01S6yNirtAvaBJJnTzcv/9G/M= go1.20.4 amd64 linux servers started!
UI started - visit:
http://localhost:8080/fortio/
(or any host/ip reachable on this server)