zhangguanzhang's Blog

如何打包一个不依赖 python 的 supervisor 的包去离线部署

字数统计: 2.6k阅读时长: 12 min
2022/11/03

近期的一次信创适配,需要 supervisor 离线安装

由来

信创涉密设备介绍

无网络的信创涉密设备适配,要求列举下就是:

  1. 光盘拷贝 rpm 包上去,实际可以绕过去(电脑网线直连服务器,scp),但是不重要
  2. 不能使用容器,必须全部 rpm 包方式部署业务,有专门的图形软件添加和安装 rpm 包,意味着你搭建本地源啥的都用不了,只能一个个 rpm 包添加到界面安装
  3. 系统的 rpm 包不能升级和降级,这个代表着你拷贝了全套的 rpm 包,很可能都用不了,例如自带的某个 so 版本和你的业务的冲突或者不匹配
  4. 只有系统里注册的 rpm 包里的文件和脚本才具备执行能力,比如你在设备上 vi test.sh 写 echo 123, 然后 bash test.sh 都无法执行
  5. 自己的要求: 对于业务进程以及方便现场的实施查看我们的所有业务服务和支持日志轮转。

选型考虑

业务侧是业务研发处理,大部分都是 golang 开发的(都是静态编译的),我们需要做的就是进程纳管,这方面的轮子简单对比下,大概需求列为表格就是下面的需求

选型 安装过程 支持 env file 支持给进程 logfile rotate 组(或者label)纳管 备注
systemd 不老的系统都自带 有通配符,但是没组的概念
supervisor-py版本 需要pip或者包管理安装 x env file 的支持 hack 就行
supervisor-go版本 单独二进制 x
pm2 还得安装node x x 除了安装过程,貌似我都不了解

pm2 从安装就不考虑了,supervisor-go 版本感觉开发者都不看 issue,而且它的 master 提交记录修复的 bug 描述看都是很常见的场景,出现这个问题我认为是单元测试覆盖不全面以及用户面小,现阶段和生产上我不会考虑它。而 systemd 不支持组纳管,初步是选用 python 版本的 supervisor,但是 supervisor 安装无非就两种,一种是 pip 安装,一种是全套 rpm 包安装:

  • python-meld3 >= 0.6.5
  • python2-meld3
  • supervisor

涉密设备系统上不一定有 pip,使用 rpm 的方式的话,在 arm64 的麒麟里下的上面三个 rpm 可能在另一个 arm64 的 uos 上就无法使用了,并且实际上麒麟也有好几个类型,穷举的话可以,但是同一个系统还可能在迭代升级,之前的 rpm 包里的某个依赖可能就不满足了,再穷举每个os的所有版本去维护的话,难度就非常痛苦。

之前组长的解决办法就是起容器安装 supervisor,然后把 site-package 目录打包,事实证明这个确实可以,但是部署的客户数量少,如果碰到部分客户已经安装了某个 site-package 的依赖,我们这个 supervisor 的包就安装不上去了。

这次就想着如何把 supervisor 打包成一个没有任何依赖的 rpm 包,想着是利用 pyinstaller,搜了下这块没人做过,现在是已经搞完了验证可行了。

过程

pyinstaller 打包

pyinstaller 是一个把 py 脚本打包成独立的可执行文件的工具,推荐 pip 安装 pyinstaller,我的思路是容器里安装 supervisor 后打包

1
2
3
4
5
6
7
docker run --rm -ti -w /test --entrypoint bash centos:7
# python3 会自带 python3-pip , binutils 是 pyinstaller 打包的时候依赖
yum install -y python3 binutils gcc zlib-devel which

pip3 install wheel
pip3 install pyinstaller
pip3 install supervisor

过程和遇到的错误

1
pyinstaller `which supervisord`

打包完后:

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
[root@6ec6694c6688 test]# ls -l
total 4
drwxr-xr-x 3 root root 25 Nov 3 10:57 build
drwxr-xr-x 3 root root 25 Nov 3 10:57 dist
-rw-r--r-- 1 root root 1150 Nov 3 10:57 supervisord.spec
[root@6ec6694c6688 test]# ls -l dist/
total 4
drwxr-xr-x 3 root root 4096 Nov 3 10:57 supervisord
[root@6ec6694c6688 test]# ls -l dist/supervisord/
total 11580
-rw-r--r-- 1 root root 786953 Nov 3 10:57 base_library.zip
drwxr-xr-x 2 root root 4096 Nov 3 10:57 lib-dynload
-rwxr-xr-x 1 root root 68192 Nov 20 2015 libbz2.so.1
-rwxr-xr-x 1 root root 15856 Sep 30 2020 libcom_err.so.2
-rwxr-xr-x 1 root root 2521144 Aug 9 2019 libcrypto.so.10
-rwxr-xr-x 1 root root 173320 Sep 30 2020 libexpat.so.1
-rwxr-xr-x 1 root root 32328 Apr 1 2020 libffi.so.6
-rwxr-xr-x 1 root root 320720 Sep 30 2020 libgssapi_krb5.so.2
-rwxr-xr-x 1 root root 210784 Sep 30 2020 libk5crypto.so.3
-rwxr-xr-x 1 root root 15688 Jun 10 2014 libkeyutils.so.1
-rwxr-xr-x 1 root root 967840 Sep 30 2020 libkrb5.so.3
-rwxr-xr-x 1 root root 67104 Sep 30 2020 libkrb5support.so.0
-rwxr-xr-x 1 root root 157424 Nov 5 2016 liblzma.so.5
-rwxr-xr-x 1 root root 402384 Aug 2 2017 libpcre.so.1
-rwxr-xr-x 1 root root 3144192 Nov 16 2020 libpython3.6m.so.1.0
-rwxr-xr-x 1 root root 285136 Aug 8 2019 libreadline.so.6
-rwxr-xr-x 1 root root 155744 Apr 1 2020 libselinux.so.1
-rwxr-xr-x 1 root root 470376 Aug 9 2019 libssl.so.10
-rwxr-xr-x 1 root root 174576 Sep 6 2017 libtinfo.so.5
-rwxr-xr-x 1 root root 90160 May 12 14:58 libz.so.1
-rwxr-xr-x 1 root root 1749176 Nov 3 10:57 supervisord

然后执行了下,发现报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ./supervisord --version
Traceback (most recent call last):
File "supervisord", line 7, in <module>
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "PyInstaller/loader/pyimod03_importers.py", line 495, in exec_module
File "supervisor/supervisord.py", line 41, in <module>
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "PyInstaller/loader/pyimod03_importers.py", line 495, in exec_module
File "supervisor/options.py", line 63, in <module>
File "supervisor/options.py", line 61, in _read_version_txt
FileNotFoundError: [Errno 2] No such file or directory: '/root/supervisor/dist/supervisord/supervisor/version.txt'
[18114] Failed to execute script 'supervisord' due to unhandled exception!

解决办法就是创建文件 supervisor/version.txt ,但是实际上可以 hack 下:

1
2
3
4
5
6
7
ver=$(find /usr/ -type f -name 'version.txt' -path '*/supervisor/*' -exec cat {} \; )
op_file=$(find /usr/ -type f -name 'options.py' -path '*/supervisor/*')
# 设置 VERSION
sed -ri '/^VERSION\s+=\s+/s#= .+#= "'"${ver}"'"#' $op_file

# 打包
pyinstaller `which supervisord`

然后发现移动路径会无法执行:

1
2
3
$ cp dist/supervisord/supervisord .
$ ./supervisord --help
[1648] Error loading Python lib '/test/libpython3.6m.so.1.0': dlopen: /test/libpython3.6m.so.1.0: cannot open shared object file: No such file or directory

看样子是 dlopen 的形式打开相对目录的 so,但是我想打包成一个文件,看了下不支持静态编译

1
pyinstaller --help |& grep -P 'link|static'

最后搜索了下,加上选项 -F, --onefile Create a one-file bundled executable. 打包成一个文件:

1
2
3
rm -rf supervisord* dist/ build

pyinstaller --onefile `which supervisord`

然后发现容器内可以执行,但是在 arm64 的 os 上执行会报错 No moudle named supervisor.xxx,需要加 -p DIR, --paths DIR A path to search for imports (like using PYTHONPATH).

1
pyinstaller --onefile -p /usr/local/lib/python3.7/site-packages `which supervisord`

打包成 rpm 包的话,以下文件需要打包:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 可以 rpm 包安装下 supervisor ,看下哪些文件需要
$ rpm -ql supervisor | grep -Pv '\.py|/site-packages/|/share/'
/etc/logrotate.d/supervisor
/etc/supervisord.conf
/etc/supervisord.d
/etc/tmpfiles.d/supervisor.conf
/usr/bin/echo_supervisord_conf
/usr/bin/pidproxy
/usr/bin/supervisorctl
/usr/bin/supervisord
/usr/lib/systemd/system/supervisord.service
/var/log/supervisor
/var/run/supervisor

完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
docker run --rm -ti -w /test --entrypoint bash centos:7
# python3 会自带 python3-pip , binutils 是 pyinstaller 打包的时候依赖
yum install -y python3 binutils gcc zlib-devel which

pip3 install wheel
pip3 install pyinstaller
pip3 install supervisor

ver=$(find /usr/ -type f -name 'version.txt' -path '*/supervisor/*' -exec cat {} \; )
op_file=$(find /usr/ -type f -name 'options.py' -path '*/supervisor/*')
# 设置 VERSION
sed -ri '/^VERSION\s+=\s+/s#= .+#= "'"${ver}"'"#' $op_file

dir=$(find /usr -type d -name supervisor -path '*/site-packages/*' -exec dirname {} \;)
pyinstaller --onefile -p $dir `which pidproxy`
pyinstaller --onefile -p $dir `which supervisord`
pyinstaller --onefile -p $dir `which supervisorctl`

制作 rpm 包

其实这个步骤也可以用于容器内无 python 添加 supervisor
相关步骤和文件存放在我 github 上
https://github.com/zhangguanzhang/compile-and-packages

后续

取消 onefile 打包

歇逼了,部署上去无法运行,运行报错:

1
error while loading shared libraries: libz.so.1: failed to map segment from shared object

设置 TMPDIR 啥的都不行,用 staticx 打包成静态依赖也不行,现在取消 --onefile 参数打包整个目录 + 软连接能运行

被纳管的进程无法启动

2023/05/22 发现部署上去后,supervisor 是 rpm 打包部署涉密设备上放在 /opt/supervisor/bin 下的,有个纳管的 Xvfb 无法启动,报错

1
xvfb: /opt/supervisor/bin/libz.so.1: version `ZLIB_1.2.9' not found (required by /lib64/libpng16.so.16)

思考了一番和结合谷歌,大致知道问题原因了,pyinstaller 非 --onefile 打包会设置最终启动的 supervisord 的 LD_LIBRARY_PATH 为存放目录(因为目录下都是 so),而 supervisord 又会传递自身的变量给纳管进程,需要在启动 Xvfb 前处理下传进来的 LD_LIBRARY_PATH,大概类似下面:

1
2
3
4
5
6
7
8
...
if command -v supervisorctl &>/dev/null;then
sp_ld_path=$(dirname $(readlink -f `which supervisorctl`))
if [ -n "$LD_LIBRARY_PATH" ] && [ -n "$sp_ld_path" ] && echo $sp_ld_path | grep -wq supervisor/bin;then
LD_LIBRARY_PATH=$(echo $LD_LIBRARY_PATH | sed -e "s#${sp_ld_path}##" -e 's#::#:#g')
fi
fi
exec Xvfb -nolisten tcp -dpi 96 -ac :7 -screen 0 1280x1024x8

所以部署的其他依赖 so 的程序也可能会有类似问题,需要处理下 LD_LIBRARY_PATH

es jna 行为

2023/11/15 自己制作的 es 不行(es 7.3.2后是 gradlew 打包rpm的,我这里是下载 tar 包的版本自己制作 rpm),部署上去后会报错:

1
2
3
"stacktrace": ["java.lang.UnsatisfiedLinkError: /tmp/elasticsearch-10723602160698077945/jna12230389653417135224.tmp: /tmp/elasticsearch-10723602160698077945/jna12230389653417135224.tmp: failed to map segment from shared object",
"at jdk.internal.loader.NativeLibraries.load(Native Method) ~[?:?]",
...

报错就是 jna 啥库 load 失败,就是 jna 的行为是解压内部的 so 文件到 TMPDIR 里后加载,这个行为被系统安全机制拦截了,最后把 so 解压后处理的:

1
2
3
4
5
6
tar zxf elasticsearch-*.tar.gz -C SOURCES/elasticsearch/elasticsearch --strip-components=1; \
cd /opt; \
# es jna的jar 会自解压so到TMDIR后加载so,会被拦截无法启动,这里解压so出来后配合 ES_JAVA_OPTS="-Djna.nosys=false" 解决
/root/rpmbuild/SOURCES/elasticsearch/elasticsearch/jdk/bin/jar -xf /root/rpmbuild/SOURCES/elasticsearch/elasticsearch/lib/jna-?.*.jar; \
mv com/sun/jna/linux-$(uname -m|tr _ -)/libjnidispatch.so /root/rpmbuild/SOURCES/elasticsearch/elasticsearch/jdk/lib; \
cd -; \

主要是 jna 有 java 的 -D 选项支持,所以解压出 so 放 lib 目录配合参数就能解决

参考:

参考

CATALOG
  1. 1. 由来
    1. 1.1. 信创涉密设备介绍
    2. 1.2. 选型考虑
  2. 2. 过程
    1. 2.1. pyinstaller 打包
      1. 2.1.1. 过程和遇到的错误
      2. 2.1.2. 完整流程
    2. 2.2. 制作 rpm 包
  3. 3. 后续
    1. 3.1. 取消 onefile 打包
    2. 3.2. 被纳管的进程无法启动
    3. 3.3. es jna 行为
  4. 4. 参考