今天在一台 Redhat 7 服务器上启动一个 Kafka 3.7.2 容器,遇到一个很诡异的报错。经过排查发现,问题是由容器里新版 bash 引起的,应该是新 bash 或动态库可能跟老内核不兼容。
最终,我们把宿主上的旧版 bash 及其依赖动态库挂载到容器上解决问题。整个问题排查过程和解决思路挺有意思的,涉及不少基础知识,分享给大家。
报错背景
Kafka 容器启动方式平白无奇:
1
|
docker run --rm -it apache/kafka:3.7.2
|
按预期应该是日志哐哐刷一下,然后服务起来,接下来开始指定相关配置参数和数据目录。但结果出了个幺蛾子:
1
2
3
4
5
6
|
===> User
uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
===> Setting default values of environment variables if not already set.
CLUSTER_ID not set. Setting it to default value: "5L6g3nShT-eMCtK--X86sw"
===> Configuring ...
/opt/kafka/config/ file not writable
|
报错有点诡异,显示 /opt/kafka/config/ 配置目录不可写。问题是我没把这个目录映射到宿主文件系统,不可能 Docker 镜像本身就有问题吧?
为什么 Kafka 容器启动后会写配置文件?
后台服务程序通常是通过配置文件来配置的,Kafka 也不例外。然而在容器时代,配置文件的维护不太方便,通过环境变量配置更加灵活,启动容器时即可通过命令行参数指定。
因此,很多容器镜像都会打包一个服务启动脚本,这个脚本可以检查当前环境变量,并据此生成对应的配置文件,以达到通过环境变量配置服务的目的。
定位过程
有点好奇,所以重新把容器跑起来,这次直接进去 sh ,看看配置目录是什么情况:
1
|
docker run --rm -it apache/kafka:3.7.2 sh
|
检查了配置目录所属用户和权限位,用户和用户组都是 appuser :
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
|
$ ls -l /opt/kafka/
total 60644
-rw-r--r-- 1 appuser appuser 15260 Dec 4 18:50 LICENSE
-rw-r--r-- 1 appuser appuser 28359 Dec 4 18:50 NOTICE
drwxr-xr-x 3 appuser appuser 4096 Dec 4 19:55 bin
drwxr-xr-x 3 appuser appuser 4096 Dec 4 19:55 config
-r--r--r-- 1 root root 45522944 Dec 4 19:55 kafka.jsa
drwxr-xr-x 2 appuser appuser 8192 Dec 4 19:55 libs
drwxr-xr-x 2 appuser appuser 4096 Dec 4 19:55 licenses
drwxr-xr-x 2 appuser appuser 44 Dec 4 19:55 site-docs
-r--r--r-- 1 root root 16506880 Dec 4 19:55 storage.jsa
$ ls -l /opt/kafka/config/
total 72
-rw-r--r-- 1 appuser appuser 906 Dec 4 18:50 connect-console-sink.properties
-rw-r--r-- 1 appuser appuser 909 Dec 4 18:50 connect-console-source.properties
-rw-r--r-- 1 appuser appuser 5475 Dec 4 18:50 connect-distributed.properties
-rw-r--r-- 1 appuser appuser 883 Dec 4 18:50 connect-file-sink.properties
-rw-r--r-- 1 appuser appuser 881 Dec 4 18:50 connect-file-source.properties
-rw-r--r-- 1 appuser appuser 2063 Dec 4 18:50 connect-log4j.properties
-rw-r--r-- 1 appuser appuser 2540 Dec 4 18:50 connect-mirror-maker.properties
-rw-r--r-- 1 appuser appuser 2262 Dec 4 18:50 connect-standalone.properties
-rw-r--r-- 1 appuser appuser 1221 Dec 4 18:50 consumer.properties
drwxr-xr-x 2 appuser appuser 85 Dec 4 19:55 kraft
-rw-r--r-- 1 appuser appuser 4917 Dec 4 18:50 log4j.properties
-rw-r--r-- 1 appuser appuser 2065 Dec 4 18:50 producer.properties
-rw-r--r-- 1 appuser appuser 6896 Dec 4 18:50 server.properties
-rw-r--r-- 1 appuser appuser 1094 Dec 4 18:50 tools-log4j.properties
-rw-r--r-- 1 appuser appuser 1169 Dec 4 18:50 trogdor.conf
-rw-r--r-- 1 appuser appuser 1205 Dec 4 18:50 zookeeper.properties
$ echo abc > /opt/kafka/config/abc
|
回到上面的报错信息,可以看到 Kafka 也是以 appuser 用户执行的啊,怎么可能没有写入权限?我直接跑 echo 命令往配置目录写入一个新文件,也是可以的。诡异……
会不会是启动脚本有 BUG ,导致配置文件权限不对。因此,就想检查一下启动脚本,看看是哪个位置报错。那么问题来了,启动脚本在哪里?
这个问题等价于:一个容器镜像启动后,执行什么程序是怎么决定的?这是最基本的 Docker 基础知识,但我发现很多人并不会……扯远了,实际上这是由镜像配置里面的 Entrypoint 、 Cmd 决定的。
先执行 docker inspect 查看镜像信息:
1
|
docker inspect apache/kafka:3.7.2
|
在 Config 里面就可以看到相关配置:
1
2
3
4
5
6
7
8
9
10
11
12
|
[
{
"Config": {
"Cmd": [
"/etc/kafka/docker/run"
],
"Entrypoint": [
"/__cacert_entrypoint.sh"
]
}
}
]
|
这个配置决定了,镜像起来后,默认执行的是:
1
|
/__cacert_entrypoint.sh /etc/kafka/docker/run
|
/__cacert_entrypoint.sh 这个入口脚本看名字跟 CA 证书设置有关,看了脚本内容,它根据环境变量设置证书,最后在执行参数指定的程序:
由于我们没有指定相关环境变量,因此问题就出在其后执行的 Kafka 启动程序 /etc/kafka/docker/run 上。它应该是一个脚本,位于 etc 目录内更说明这一点,所以我直接把它 cat 出来看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
. /etc/kafka/docker/bash-config
# Set environment values if they exist as arguments
if [ $# -ne 0 ]; then
echo "===> Overriding env params with args ..."
for var in "$@"
do
export "$var"
done
fi
echo "===> User"
id
echo "===> Setting default values of environment variables if not already set."
. /etc/kafka/docker/configureDefaults
echo "===> Configuring ..."
. /etc/kafka/docker/configure
echo "===> Launching ... "
. /etc/kafka/docker/launch
|
结合最上面的报错信息,===> Configuring ...
有输出,而===> Launching ...
没有,说明问题出在 /etc/kafka/docker/configure 这个脚本上。看名字,这个脚本应该是负责初始化配置文件,跟报错信息配置目录不可写也是吻合的。
直接在这个脚本里面搜报错关键字,很快就可以来到这个 path 这个 shell 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
path() {
if [[ $2 == "writable" ]]; then
if [[ ! -w "$1" ]]; then
echo "$1 file not writable"
exit 1
fi
elif [[ $2 == "existence" ]]; then
if [[ ! -e "$1" ]]; then
echo "$1 file does not exist"
exit 1
fi
fi
}
|
这个子程序接收两个参数,$1
是待检查路径名,$2
是预期权限,里面通过 [[ -w /some/path ]]
这种 shell 语法进行权限检查。由于 shell 语法支持度历史上也是比较混乱,怀疑会不会是 shell 不支持。在容器 sh 又确认了一下,发现是可以的:
1
2
|
$ [[ -w /opt/kafka/config/ ]] && echo yes
yes
|
容器 sh 的执行身份跟 Kafka 的执行身份也是一致的:
1
2
|
$ id
uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
|
还是很诡异……又想到了启动脚本用的 shell 可能跟我启动的不一样,我执行容器启动的是 sh :
1
2
3
|
$ ps u -p $$
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
appuser 1 0.1 0.0 1680 656 ? Ss 02:12 0:00 sh
|
检查了启动脚本头,发现它用的是 bash ,讲道理 bash 应该比 sh 牛逼啊,怎么就有问题呢?仍然诡异:
1
2
3
4
|
$ cat /etc/kafka/docker/run | head -n 3
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
|
直接在容器里面用 bash 执行检查逻辑,发现 bash 真的有问题!没有输出预期的 yes ,什么鬼❓
1
|
$ bash -c '[[ -w /opt/kafka/config/ ]] && echo yes'
|
但我回到宿主上执行 bash 检查 /tmp/ 目录权限结果发现宿主上的版本又是正常的:
1
2
|
$ bash -c '[[ -w /tmp/ ]] && echo yes'
yes
|
那问题应该出在 bash 版本上,宿主上的 bash 版本是 4 :
1
2
3
4
5
6
7
|
$ bash --version
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
|
容器里面的 bash 版本是 5 ,相差一个大版本:
1
2
3
4
5
6
7
|
$ bash --version
GNU bash, version 5.2.26(1)-release (x86_64-alpine-linux-musl)
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
|
内核版本是 3 ,也算比较老了:
1
2
|
$ uname -a
Linux cb747a3b7a7d 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 GNU/Linux
|
至此,问题水落石出,容器里面的 bash 版本太新,跟旧的内核不兼容。
解决思路
那么,问题应该怎么解决呢?
系统升级
Redhat 7.9 确实太老了,能重装更新系统最后,但因其他因素无法实现。
用 sh 来执行启动脚本
sh 跑 [[ -w /some/path ]]
是正常的,就想到用它来跑启动脚本。这个路子我没抱什么希望,因为 sh 通常只是一个最小功能集,应该有很多语法不支持,要是启动脚本用到就跪了。不过,试一下嘛也没啥成本,在容器里面跑这个命令:
1
|
/__cacert_entrypoint.sh sh /etc/kafka/docker/run
|
不出意外抛锚了,因为 declare
不支持。我又看了一下,容器里面的 sh 是链到 busybox 上的:
1
2
3
4
|
$ which sh
/bin/sh
$ ls -l /bin/sh
lrwxrwxrwx 1 root root 12 Sep 6 11:34 /bin/sh -> /bin/busybox
|
魔改启动脚本
权限没问题你不是瞎报错嘛,我就把报错去掉,闭上嘴:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
path() {
if [[ $2 == "writable" ]]; then
if [[ ! -w "$1" ]]; then
echo "$1 file not writable"
#exit 1
fi
elif [[ $2 == "existence" ]]; then
if [[ ! -e "$1" ]]; then
echo "$1 file does not exist"
#exit 1
fi
fi
}
|
改完再重新跑一下:
1
|
/__cacert_entrypoint.sh /etc/kafka/docker/run
|
发现可以正常启动了!!!
宿主命令和动态库映射进容器
由于魔改启动脚本需要重新打镜像,而且可能有坑,所以并不是一个完美的解决方案。然后脑袋突然灵光一现,宿主上的 bash 不是正常嘛,能否把它映射进容器里用?由于容器和宿主是共用内核,理论上是可行的。由于可执行程序需要依赖动态库,所以也要映射进去,只是暂不确定是否有冲突。
先只把可执行程序 mount 进去,不出意外报错了,因为依赖的动态库还没映射进去:
1
2
3
4
5
6
7
8
9
10
11
|
$ docker run -v /bin/bash:/bin/bash --rm -it apache/kafka:3.7.2 bash --version
Error loading shared library libtinfo.so.5: No such file or directory (needed by /bin/bash)
Error relocating /bin/bash: tputs: symbol not found
Error relocating /bin/bash: tgoto: symbol not found
Error relocating /bin/bash: tgetflag: symbol not found
Error relocating /bin/bash: tgetent: symbol not found
Error relocating /bin/bash: tgetnum: symbol not found
Error relocating /bin/bash: tgetstr: symbol not found
Error relocating /bin/bash: UP: symbol not found
Error relocating /bin/bash: PC: symbol not found
Error relocating /bin/bash: BC: symbol not found
|
先在宿主上看看 bash 都链接了哪些动态库:
1
2
3
4
5
6
|
$ ldd /usr/bin/bash
linux-vdso.so.1 => (0x00007ffc9b1d0000)
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007fce0e4da000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fce0e2d6000)
libc.so.6 => /lib64/libc.so.6 (0x00007fce0df08000)
/lib64/ld-linux-x86-64.so.2 (0x00007fce0e704000)
|
先映射 libtinfo.so.5 试试:
1
|
docker run -v /bin/bash:/bin/bash -v /lib64/libtinfo.so.5:/lib64/libtinfo.so.5 --rm -it apache/kafka:3.7.2 bash --version
|
发现还是报错:
1
2
3
4
5
6
7
8
9
10
|
Error loading shared library libtinfo.so.5: No such file or directory (needed by /bin/bash)
Error relocating /bin/bash: tputs: symbol not found
Error relocating /bin/bash: tgoto: symbol not found
Error relocating /bin/bash: tgetflag: symbol not found
Error relocating /bin/bash: tgetent: symbol not found
Error relocating /bin/bash: tgetnum: symbol not found
Error relocating /bin/bash: tgetstr: symbol not found
Error relocating /bin/bash: UP: symbol not found
Error relocating /bin/bash: PC: symbol not found
Error relocating /bin/bash: BC: symbol not found
|
还是找不到映射进去的库文件,有点奇怪……不过无妨,容器直接执行 ls 一下看 /lib64 里面有没有:
1
|
docker run -v /bin/bash:/bin/bash -v /lib64/libtinfo.so.5:/lib64/libtinfo.so.5 --rm -it apache/kafka:3.7.2 ls -al /lib64
|
发现 libtinfo.so.5 已经映射进来了:
1
2
|
lrwxrwxrwx 1 root root 27 Dec 4 19:55 ld-linux-x86-64.so.2 -> ../lib/ld-linux-x86-64.so.2
-rwxr-xr-x 1 root root 174576 Sep 6 2017 libtinfo.so.5
|
很有可能 /lib64 不是容器动态库的默认搜索路径,该目录原本只有 ld 链接器似乎也在暗示这一点。没关系,加个环境变量 LD_LIBRARY_PATH 显式指定一下呗,发现旧版的 bash 成功在容器里跑起来了!!!
1
2
3
4
5
6
7
|
$ docker run -e LD_LIBRARY_PATH=/lib64 -v /bin/bash:/bin/bash -v /lib64/libtinfo.so.5:/lib64/libtinfo.so.5 --rm -it apache/kafka:3.7.2 bash --version
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
|
测了一下 -w 这个特性,发现高兴早了,没有出现预期的 yes ……
刚刚 ldd 看了宿主的 bash 是需要链接多个动态库的,包括 libc 等,都还没映射。容器里用的应该还是自带的新版动态库,所以问题依然存在。接下来,把宿主 ldd 输出的所有动态库都映射进去,发现可以了!!!
1
2
|
$ docker run -e LD_LIBRARY_PATH=/lib64 -v /bin/bash:/bin/bash -v /lib64/linux-vdso.so.1:/lib64/linux-vdso.so.1 -v /lib64/libdl.so.2:/lib64/libdl.so.2 -v /lib64/libtinfo.so.5:/lib64/libtinfo.so.5 -v /lib64/libc.so.6:/lib64/libc.so.6 -v /lib64/ld-linux-x86-64.so.2:/lib64/ld-linux-x86-64.so.2 --rm -it apache/kafka:3.7.2 bash -c '[[ -w /tmp/ ]] && echo yes'
yes
|
最后把自定义执行命令去掉,按照默认命令启动容器,发现 Kafka 可以正常启动了!!!
1
|
docker run -e LD_LIBRARY_PATH=/lib64 -v /bin/bash:/bin/bash -v /lib64/linux-vdso.so.1:/lib64/linux-vdso.so.1 -v /lib64/libdl.so.2:/lib64/libdl.so.2 -v /lib64/libtinfo.so.5:/lib64/libtinfo.so.5 -v /lib64/libc.so.6:/lib64/libc.so.6 -v /lib64/ld-linux-x86-64.so.2:/lib64/ld-linux-x86-64.so.2 --rm -it apache/kafka:3.7.2
|
订阅更新,获取更多学习资料,请关注我们的公众号: