1. kpatch 是什么?

livepatch 实时或动态内核修补允许为正在运行的内核提供功能增强,无需重新启动系统,这对于某些在线系统修复安全漏洞非常有帮助。

Kpatch 是给 Linux 内核 livepatch 的工具,由 Redhat 公司出品。最早出现的打热补丁工具是 Ksplice。但是 Ksplice 被 Oracle 收购后,一些发行版生产商就不得不开发自己的热补丁工具,分别是 Redhat 的 Kpatch 和 Suse 的 KGraft。同时,在这两家厂商的推进下,kernel 4.0 开始,开始集成了 livepatch 技术。 Kpatch 虽然是 Redhat 研发,但其也支持 Ubuntu、Debian、Oracle Linux 等的发行版。

kpatch 使用限制

livepatch 不是万能的,尤其是现在技术还不足够成熟的时候。在使用 Kpatch livepatch 存在的限制:

  • 不支持修改 init 函数 (用 __init 注解) 的补丁。如果补丁试图这样做,kpatch-build 将返回一个错误。
  • 不直接支持修改静态分配数据的补丁, kpatch-build 会检测到并返回错误。这个限制可以通过使用回调或影子变量来克服,如补丁作者指南中所述。
  • 改变函数与动态分配数据交互方式的补丁可能是安全的,也可能不是。kpatch-build 不可能验证这种补丁的安全性。这取决于用户是否了解这个补丁的作用,新的函数与动态分配数据的交互方式是否与旧的函数不同,以及将这样的补丁原子化地应用到运行中的内核上是否安全。
  • 不支持修改 vdso 函数的补丁。这些补丁在用户空间中运行,ftrace 无法钩住它们。
  • 不支持修改缺少 fentry 调用的函数的补丁。这包括任何被归档到 lib.a 库中供以后链接的 lib-y 目标 (例如 lib/string.o)。

目前,kpatch 与 ftrace 和 kprobes 的使用之间存在一些不兼容的情况。更多细节请参见常见问题部分

本文将介绍在 CentOS7.7下安装和使用 Kpatch 软件,并用于解决我们在 kubenetes 集群中由于 service 过多导致 ipvs 中 estimation_timer 函数的内核延时过高,从而导致网络都发包抖动的问题。 该问题的完整描述可以参见这里 的第二部分,我们的排查方式类似,这里我们重点讲述修复的方式。

我们生产环境中大概存在 30000 条左右的 ipvs 规则,主机命名空间中的 estimation_timer 在内核中的遍历耗时非常高,参见下图

ipvs_timer.png

2. kpatch 样例测试

2.1 安装

我们的平台是 CentOS 7.7.1908 版本,内核版本为 3.10.0-1062.9.1.el7.x86_64

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ lsb_release -a
LSB Version:	:core-4.1-amd64:core-4.1-noarch
Distributor ID:	CentOS
Description:	CentOS Linux release 7.7.1908 (Core)
Release:	7.7.1908
Codename:	Core

$ uname  -a
Linux 3.10.0-1062.9.1.el7.x86_64 #1 SMP Fri Dec 6 15:49:49 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

# 检查一下我们系统是否已经支持了 live patch
$ cat /boot/config-3.10.0-1062.9.1.el7.x86_64 |grep PATCH
CONFIG_HAVE_LIVEPATCH=y
CONFIG_LIVEPATCH=y
CONFIG_DVB_BUDGET_PATCH=m
CONFIG_SND_HDA_PATCH_LOADER=y

安装官方仓库给的安装文档中关于 Fedora, RHEL, CentOS 中的章节:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 从官方拉取仓库
$ git clone https://github.com/dynup/kpatch.git
$ source test/integration/lib.sh
# 中间会使用 yum 安装相关的依赖包,安装时间视网络情况而定,在阿里云的环境下需要的时间比较长
$ sudo kpatch_dependencies

$ cd kpatch 

# 进行编译
$ make

# 默认安装到 /usr/local,需要注意 kpatch-build 在目录 /usr/local/bin/ 下,而 kpatch 在 /usr/local/sbin/ 目录
$ sudo make install

$ /usr/local/bin/kpatch-build -h
usage: kpatch-build [options] <patch1 ... patchN>
		patchN             Input patchfile(s)
		-h, --help         Show this help message
		-a, --archversion  Specify the kernel arch version
		-r, --sourcerpm    Specify kernel source RPM
		-s, --sourcedir    Specify kernel source directory
		-c, --config       Specify kernel config file
		-v, --vmlinux      Specify original vmlinux
		-j, --jobs         Specify the number of make jobs
		-t, --target       Specify custom kernel build targets
		-n, --name         Specify the name of the kpatch module
		-o, --output       Specify output folder
		-d, --debug        Enable 'xtrace' and keep scratch files
		                   in <CACHEDIR>/tmp
		                   (can be specified multiple times)
		-e, --oot-module   Enable patching out-of-tree module,
		                   specify current version of module
		--skip-cleanup     Skip post-build cleanup
		--skip-gcc-check   Skip gcc version matching check
		                   (not recommended)

到此 kpatch 已经安装到了系统中。舞台已经搭建完成,但是我们还需要演员登场,而演员就是我们系统运行的内核源码。

在安装过程中已经安装了内核的对应的 debug 包,内核的源码位于以下目录:

1
/usr/src/debug/kernel-3.10.0-1062.9.1.el7/linux-3.10.0-1062.9.1.el7.x86_64

2.2 官方样例测试

我们使用官方提供的样例程序进行简单测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ cd kpatch/test/integration/rhel-7.7
$ cat meminfo-string.patch
Index: kernel/fs/proc/meminfo.c
===================================================================
--- kernel.orig/fs/proc/meminfo.c
+++ kernel/fs/proc/meminfo.c
@@ -99,7 +99,7 @@ static int meminfo_proc_show(struct seq_
 		"Committed_AS:   %8lu kB\n"
 		"VmallocTotal:   %8lu kB\n"
 		"VmallocUsed:    %8lu kB\n"
-		"VmallocChunk:   %8lu kB\n"
+		"VMALLOCCHUNK:   %8lu kB\n"
 #ifdef CONFIG_MEMORY_FAILURE
 		"HardwareCorrupted: %5lu kB\n"
 #endif
 
$sudo  /usr/local/bin/kpatch-build -t vmlinux meminfo-string.patch
ERROR: gcc/kernel version mismatch
gcc version:    GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)
kernel version: GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)
Install the matching gcc version (recommended) or use --skip-gcc-check
to skip the version matching enforcement (not recommended)
ERROR: kpatch build failed. 

# 这个报错是因为系统当前的 gcc 版本系统是 Red Hat 4.8.5-44,而 kernel version 的是 Red Hat 4.8.5-39
# 但是 gcc 的版本是一致的 4.8.5 20150623, 这里我们通过 --skip-gcc-check 跳过 gcc 检测

$ sudo /usr/local/bin/kpatch-build -t vmlinux meminfo-string.patch  --skip-gcc-check
WARNING: Skipping gcc version matching check (not recommended)
Fedora/Red Hat distribution detected
Downloading kernel source for 3.10.0-1062.9.1.el7.x86_64
ERROR: kpatch build failed. Check /root/.kpatch/build.log for more details.

# cat /root/.kpatch/build.log
Loaded plugins: auto-update-debuginfo, fastestmirror
Enabling updates-source repository
Enabling base-source repository
Enabling extras-source repository
Loading mirror speeds from cached hostfile
No Match for argument kernel-3.10.0-1062.9.1.el7
Nothing to download

通过日志查看,我们的得知是 kernel-3.10.0-1062.9.1.el7 的源码没有下载,通过查看源码我们发现 kpatch-build 中的下载源码逻辑为(备注:下面脚本是我提取出来,用于验证的脚本):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
ARCHVERSION=$(uname -r)
KVER="${ARCHVERSION%%-*}"
if [[ "$ARCHVERSION" =~ - ]]; then
	KREL="${ARCHVERSION##*-}"
	KREL="${KREL%.*}"
	[[ "$KREL" =~ .el7a. ]] && ALT="-alt"
fi

yumdownloader --source "kernel$ALT-$KVER-$KREL"

# yumdownloader --source kernel-3.10.0-1062.9.1.el7

yumdownloader 最后执行的命令为:

1
2
3
4
5
6
7
8
9
# yumdownloader --source kernel-3.10.0-1062.9.1.el7,采用手工执行
$ sudo yumdownloader --source kernel-3.10.0-1062.9.1.el7
Loaded plugins: auto-update-debuginfo, fastestmirror
Enabling updates-source repository
Enabling base-source repository
Enabling extras-source repository
Loading mirror speeds from cached hostfile
No Match for argument kernel-3.10.0-1062.9.1.el7
Nothing to download

报错的信息与 kpatch 完全一致,搜索 kpatch 仓库发现有个 issue #887,解决了内核版本下载名字的问题,但是对于我们当前的问题还是没有帮助,于是决定直接手工下载源码 rpm 包。具体下载的方式参见 2.3 章节,我们从 这里 下载 kernel-3.10.0-1062.9.1.el7.src.rpm。然后我们调整一下 kpatch-build 的命令格式,我们指定源码的来源 -r kernel-3.10.0-1062.9.1.el7.src.rpm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ sudo /usr/local/bin/kpatch-build -t vmlinux meminfo-string.patch  --skip-gcc-check -r kernel-3.10.0-1062.9.1.el7.src.rpm
WARNING: Skipping gcc version matching check (not recommended)
Fedora/Red Hat distribution detected
Downloading kernel source for 3.10.0-1062.9.1.el7.x86_64
Unpacking kernel source
Testing patch file(s)
Reading special section data
Building original source
Building patched source
Extracting new and modified ELF sections
meminfo.o: changed function: meminfo_proc_show
Patched objects: vmlinux
Building patch module: livepatch-meminfo-string.ko
SUCCESS

# 编译完成后,目录下会存在一个 livepatch-meminfo-string.ko 文件,如果系统支持 livepatch 那么生成的 ko 以 livepath 开头
# 否则,则会以 kpatch 开头
$ ls -hl livepatch-meminfo-string.ko
-rw-r--r-- 1 root root 450K Dec  3 15:46 livepatch-meminfo-string.ko

# 至此为止,我们已经完成了 livepatch-meminfo-string.ko 的生成

在编译完成后将源代码解压到目录 ~/.kpatch/src 中。

1
2
3
4
$ ls -hl ~/.kpatch/
total 8.0K
drwxr-xr-x 25 root root 4.0K Dec  3 17:07 src
-rw-r--r--  1 root root   27 Dec  3 17:04 version

通过 kpatch 命令加载生成的 ko 文件:

1
2
3
4
5
6
7
$ sudo /usr/local/sbin/kpatch load livepatch-meminfo-string.ko
loading patch module: livepatch-meminfo-string.ko
waiting (up to 15 seconds) for patch transition to complete...
patch transition has stalled!
signaling stalled process(es):
waiting (up to 60 seconds) for patch transition to complete...
transition complete (1 seconds)

这时候我们通过 dmesg 查看内核日志,可以发现以下记录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ dmesg -T
[Thu Dec  3 14:58:01 2020] livepatch: signaling remaining tasks
[Thu Dec  3 14:58:01 2020] livepatch: 'livepatch_ipvs_timer': patching complete
[Thu Dec  3 15:07:14 2020] livepatch: 'livepatch_ipvs_timer': starting unpatching transition
[Thu Dec  3 15:07:29 2020] livepatch: signaling remaining tasks
[Thu Dec  3 15:07:30 2020] livepatch: 'livepatch_ipvs_timer': unpatching complete
[Thu Dec  3 15:59:49 2020] livepatch: enabling patch 'livepatch_meminfo_string'
[Thu Dec  3 15:59:49 2020] livepatch: 'livepatch_meminfo_string': starting patching transition
[Thu Dec  3 16:00:04 2020] livepatch: signaling remaining tasks
[Thu Dec  3 16:00:05 2020] livepatch: 'livepatch_meminfo_string': patching complete

如果日志没有任何报错,则表明 livepatch 已经生效。

1
2
3
4
#-		"VmallocChunk:   %8lu kB\n"
#+		"VMALLOCCHUNK:   %8lu kB\n"
$ grep -i chunk /proc/meminfo
VMALLOCCHUNK:   34359580860 kB

验证 livepatch 已经生效。通过 list 命令可以参考到 livepatch 状态:

1
2
3
4
5
# /usr/local/sbin/kpatch list
Loaded patch modules:
livepatch_meminfo_string [enabled]

Installed patch modules:

我们卸载 livepatch,再次进行验证:

1
2
3
4
5
6
7
8
$ sudo /usr/local/sbin/kpatch unload livepatch-meminfo-string.ko
disabling patch module: livepatch_meminfo_string
waiting (up to 15 seconds) for patch transition to complete...
patch transition has stalled!
signaling stalled process(es):
waiting (up to 60 seconds) for patch transition to complete...
transition complete (1 seconds)
unloading patch module: livepatch_meminfo_string

通过 list 命令检查,这时候可以验证 livepatch 已经卸载。

1
2
3
4
5
6
7
$ sudo /usr/local/sbin/kpatch list
Loaded patch modules:

Installed patch modules:

$ grep -i chunk /proc/meminfo
VmallocChunk:   34359580860 kB

如果担心 livepatch 会在机器重启失效,那么可以使用 kpatch install 的命令安装,可以保证下次重新启动的时候能够生效。

kpatch 的使用命令详情如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 /usr/local/sbin/kpatch -h
usage: kpatch <command> [<args>]

Valid commands:
   install [-k|--kernel-version=<kernel version>] <module>
      install patch module to be loaded at boot
   uninstall [-k|--kernel-version=<kernel version>] <module>
      uninstall patch module

   load --all
      load all installed patch modules into the running kernel
   load <module>
      load patch module into the running kernel
   unload --all
      unload all patch modules from the running kernel
   unload <module>
      unload patch module from the running kernel

   info <module>
      show information about a patch module

   list
      list installed patch modules

   signal
      signal/poke any process stalling the current patch transition

   version
      display the kpatch version

2.3 安装 CentOS 系统源码

CentOS 内核的安装参见:我需要内核的源代码,大多数场景下只需要 header 文件即可,但是我们是要修改源码并进行重新编译,因此需要下载完整的源码 rpm。按照文档的提示,源码目录格式为:

http://vault.centos.org/7.N.YYMM/os/Source/SPackages/ http://vault.centos.org/7.N.YYMM/updates/Source/SPackages/

我们系统版本为 CentOS 7.7.1908,因此下载安装包的目录为

rpm 包格式采用 cpio 格式打包,可以使用 cpio 解压:

1
2
3
4
$ rpm2cpio xxx.rpm| cpio -div

# tar -xvf xxx.tar.xz
$ tar -xvJf xxx.tar.xz

3. 实战 ipvs estimation_timer 的延时问题

将下载的 rpm.src 解压:

1
2
3
4
$ rpm2cpio kernel-3.10.0-1062.9.1.el7.src.rpm |cpio -div 
$ xz -d linux-3.10.0-1062.9.1.el7.tar.xz 
$ tar -xvf linux-3.10.0-1062.9.1.el7.tar
$ cp -ra linux-3.10.0-1062.9.1.el7/ linux-3.10.0-1062.9.1.el7-patch

3.1 第一版,设置为空函数

estimation_timer 函数位于 net/netfilter/ipvs/ip_vs_est.c 文件中,函数声明如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void estimation_timer(unsigned long arg)
{
	struct ip_vs_estimator *e;
	struct ip_vs_stats *s;
	u32 n_conns;
	u32 n_inpkts, n_outpkts;
	u64 n_inbytes, n_outbytes;
	u32 rate;
	struct net *net = (struct net *)arg;
	struct netns_ipvs *ipvs;

	ipvs = net_ipvs(net);
	spin_lock(&ipvs->est_lock);
	list_for_each_entry(e, &ipvs->est_list, list) {  // 因为遍历的条目不固定,在条目多的时候引起延时过高
		s = container_of(e, struct ip_vs_stats, est);

		spin_lock(&s->lock);
		ip_vs_read_cpu_stats(&s->ustats, s->cpustats);
    
    // ...
	}
	spin_unlock(&ipvs->est_lock);
	mod_timer(&ipvs->est_timer, jiffies + 2*HZ); // 每 2s 启动启动一次
}

如何修改这个函数可以快速达到我们的效果,我的第一直觉是把这个函数完全清空,这样比较容易实现:

1
2
3
4
static void estimation_timer(unsigned long arg)
{
    return;
}

生成我们第一份的 patch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ diff -u linux-3.10.0-1062.9.1.el7/net/netfilter/ipvs/ip_vs_est.c linux-3.10.0-1062.9.1.el7-patch/net/netfilter/ipvs/ip_vs_est.c > ip_vs_timer_v1.patch
$ cat ip_vs_timer_v1.patch
--- linux-3.10.0-1062.9.1.el7/net/netfilter/ipvs/ip_vs_est.c	2019-12-02 21:08:40.000000000 +0800
+++ linux-3.10.0-1062.9.1.el7-patch/net/netfilter/ipvs/ip_vs_est.c	2020-12-03 18:07:53.206490443 +0800
@@ -91,52 +91,7 @@

 static void estimation_timer(unsigned long arg)
 {
-	struct ip_vs_estimator *e;
-	struct ip_vs_stats *s;
-	u32 n_conns;
-	u32 n_inpkts, n_outpkts;
-	u64 n_inbytes, n_outbytes;
-	u32 rate;
-	struct net *net = (struct net *)arg;
-	struct netns_ipvs *ipvs;
-
-	ipvs = net_ipvs(net);
-	spin_lock(&ipvs->est_lock);
-	list_for_each_entry(e, &ipvs->est_list, list) {
-		s = container_of(e, struct ip_vs_stats, est);
-
-		spin_lock(&s->lock);
-		ip_vs_read_cpu_stats(&s->ustats, s->cpustats);
....
-		spin_unlock(&s->lock);
-	}
-	spin_unlock(&ipvs->est_lock);
-	mod_timer(&ipvs->est_timer, jiffies + 2*HZ);
+	return;
 }

 void ip_vs_start_estimator(struct net *net, struct ip_vs_stats *stats)

将 patch 文件复制到我们制定目录后,进行验证:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ sudo /usr/local/bin/kpatch-build ip_vs_timer_v1.patch --skip-gcc-check --skip-cleanup -r /root/kernel-3.10.0-1062.9.1.el7.src.rpm
WARNING: Skipping gcc version matching check (not recommended)
Skipping cleanup
Using cache at /root/.kpatch/src
Testing patch file(s)
Reading special section data
Building original source
Building patched source
ERROR: kpatch build failed. Check /root/.kpatch/build.log for more details

$ cat /root/.kpatch/build.log
...
net/netfilter/ipvs/ip_vs_est.c:58:13: error: ‘ip_vs_read_cpu_stats’ defined but not used [-Werror=unused-function]
 static void ip_vs_read_cpu_stats(struct ip_vs_stats_user *sum,
             ^
cc1: all warnings being treated as errors
make[3]: *** [net/netfilter/ipvs/ip_vs_est.o] Error 1
make[2]: *** [net/netfilter/ipvs] Error 2
make[1]: *** [net/netfilter] Error 2
make: *** [net] Error 2
make: *** Waiting for unfinished jobs....

这是因为我们修改了注释函数 estimation_timer 中的所有调用,导致这个函数定义的函数 ip_vs_read_cpu_stats 没有了调用方。

如果 /usr/local/bin/kpatch-build 添加了 –skip-cleanup 选项,则不会清理缓存,下次在进行 patch 的时候,会使用缓存目录 /root/.kpatch/src 中已经 patch 过的源代码,所以会导致 patch 的时候出错:

1
2
3
4
$ cat /root/.kpatch/build.log
checking file net/netfilter/ipvs/ip_vs_est.c
Reversed (or previously applied) patch detected!  Skipping patch.
1 out of 1 hunk ignored

解决的方式是将 patch 过程中的出错的文件 net/netfilter/ipvs/ip_vs_est.c 还原成未执行 patch 的文件。

1
2
$ cp linux-3.10.0-1062.9.1.el7/net/netfilter/ipvs/ip_vs_est.c /root/.kpatch/src/net/netfilter/ipvs/ip_vs_est.c
cp: overwrite ‘/root/.kpatch/src/net/netfilter/ipvs/ip_vs_est.c’? y

该问题需要验证是否有其他方式规避。

3.2 第二版,注释掉定时器设置

生成第二份 patch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# cat ip_vs_timer_v2.patch
--- linux-3.10.0-1062.9.1.el7/net/netfilter/ipvs/ip_vs_est.c	2019-12-02 21:08:40.000000000 +0800
+++ linux-3.10.0-1062.9.1.el7-patch/net/netfilter/ipvs/ip_vs_est.c	2020-12-03 18:48:45.245063479 +0800
@@ -136,7 +136,10 @@
 		spin_unlock(&s->lock);
 	}
 	spin_unlock(&ipvs->est_lock);
-	mod_timer(&ipvs->est_timer, jiffies + 2*HZ);
+	/* delete timer */
+	/* mod_timer(&ipvs->est_timer, jiffies + 2*HZ); */
+	printk("hotfix estimation_timer patched\n");
+
 }

 void ip_vs_start_estimator(struct net *net, struct ip_vs_stats *stats)

继续编译生成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ sudo /usr/local/bin/kpatch-build ip_vs_timer_v2.patch --skip-gcc-check --skip-cleanup -r /root/kernel-3.10.0-1062.9.1.el7.src.rpm
WARNING: Skipping gcc version matching check (not recommended)
Skipping cleanup
Using cache at /root/.kpatch/src
Testing patch file(s)
Reading special section data
Building original source
Building patched source
Extracting new and modified ELF sections
ip_vs_est.o: changed function: estimation_timer
Patched objects: net/netfilter/ipvs/ip_vs.ko
Building patch module: livepatch-ip_vs_timer_v2.ko
SUCCESS

$ ls -hl livepatch-ip_vs_timer_v2.ko
-rw-r--r-- 1 root root 586K Dec  3 18:53 livepatch-ip_vs_timer_v2.ko

在 patch 前我们先使用 perf-tools 的 funcgraph 工具来验证是否 ip_vs 模块中 estimation_timer 的函数调用层级和耗时:

# ./funcgraph  estimation_timer
Tracing "estimation_timer"... Ctrl-C to end.
  0)               |  estimation_timer [ip_vs]() {
  0)   0.234 us    |    _raw_spin_lock();
  0)   0.399 us    |    _raw_spin_lock();
  					...
  0)               |    mod_timer() {
  0)               |      lock_timer_base.isra.37() {
  0)   0.202 us    |        _raw_spin_lock_irqsave();
  0)   0.566 us    |      }
  0)   0.044 us    |      detach_if_pending();
  0)   0.599 us    |      get_nohz_timer_target();
  0)               |      internal_add_timer() {
  0)   0.213 us    |        __internal_add_timer();
  0)   0.053 us    |        wake_up_nohz_cpu();
  0)   0.994 us    |      }
  0)   0.072 us    |      _raw_spin_unlock_irqrestore();
  0)   4.105 us    |    }
  0) ! 345.402 us  |  }

我们使用 nm 查看 livepatch-ip_vs_timer_v2.ko 文件的符号表:

1
2
# nm livepatch-ip_vs_timer_v2.ko|grep estimation_timer
0000000000000000 t estimation_timer

使用 kpatch 加载 ko 模块

1
2
3
4
5
6
7
$ sudo /usr/local/sbin/kpatch load livepatch-ip_vs_timer_v2.ko
loading patch module: livepatch-ip_vs_timer_v2.ko
waiting (up to 15 seconds) for patch transition to complete...
patch transition has stalled!
signaling stalled process(es):
waiting (up to 60 seconds) for patch transition to complete...
transition complete (1 seconds)

查看内核日志

1
2
3
4
$ dmesg -T
[Thu Dec  3 19:50:50 2020] livepatch: enabling patch 'livepatch_ip_vs_timer_v2'
[Thu Dec  3 19:50:50 2020] livepatch: 'livepatch_ip_vs_timer_v2': starting patching transition
[Thu Dec  3 19:50:50 2020] hotfix estimation_timer patched

hotfix estimation_timer patched 正是我们 v2 版本函数替换的打印,表明我们已经使用我们定义的函数完成了 ipvs 模块对应函数的替换。

1
2
3
# grep estimation_timer /proc/kallsyms
ffffffffc06561c0 t estimation_timer	[livepatch_ip_vs_timer_v2]
ffffffffc0511cf0 t estimation_timer	[ip_vs]

我们再次使用 perf_tools 中的工具 funcgraph 验证:

1
2
3
4
$ sudo ./funcgraph estimation_timer -d 20
Tracing "estimation_timer"... Ctrl-C to end.
^C
Ending tracing.

通过我们的 livepatch 成功修订了 estimation_timer 的调用,一切看起来很成功。

验证完成后,unload 掉 livepatch,我们期望是能够正常恢复 estimation_timer 的统计,但是采用 funcgraph 工具查看,却迟迟看不到

estimation_timer 的踪影,如果我们对 estimation_timer 的修改还有印象的话,那么一定还会记着,我们在函数尾部注销每次 2s 启动的逻辑,通过我们自己的函数替换,导致了我们模块卸载以后,也没有定时器来触发原有 ipvs 模块中的 estimation_timer 函数。尽管我们实现了期望的功能,但是在回滚后却中断了原有的业务逻辑功能,这在生产环境中是存在风险了,那么我们如何修改才能够实现可替换、可回滚呢?

如果我们能够容忍一定时间的延时,降低该函数带来的影响,我们可以把函数的触发周期从 2s 调整成我们可以接受的时间内,比如 5分钟,这样如果我们 livepatch 卸载以后,最多 5 分钟就可以实现功能恢复至原有逻辑,一定程度也是可以接受的。

3.3 第三版,只是修改定时器时长

1
mod_timer(&ipvs->est_timer, jiffies + 300*HZ); // 2s -> 5min

整个验证过程同上,省略。

estimation_timer 函数的缺失,理论是影响了 ipvs 中 RS 的统计,如下:

1
2
3
4
5
# ipvsadm -Ln --stats|grep "10.85.0.1:443" -A 3
TCP  10.85.0.1:443                      15     1044     1003   125874  1163064
  -> 172.16.132.179:6443                 5      579      592    55305  1007040
  -> 172.16.133.23:6443                  5       96       99    13676    57537
  -> 172.16.134.72:6443                  5      369      312    56893    98487

如果定时器停止运转,那么上述的统计数据则不再更新。

4. 参考