作为Greenplum Release Engineering团队的工程师,最近有机会深入探讨Postgres的构建系统。Greenplum Server基于Postgres,并从上游继承了构建系统。gp-releng团队正在创建可重定位版本的Greenplum Server,这使我们开始研究如何进行可重定位的Postgres版本。本文提到的可重定位(Relocated)指在不重新编译安装Postgres的情况下,改变原有安装路径后,程序依然可以正常运行,并且不需要设置LD_LIBRARY_PATH就能使程序找到正确的共享库。 考虑以下假设情景,以突出关键思想和发现过程,而不是被我们团队的用例的细节所困扰。
构建Postgres 11.8
Cardenia是一家美国大学实验室的研究生。她想使用Postgres进行数据分析,但是她对实验室的共享服务器没有root访问权限。她不想打扰该服务器的银发管理员,于是决定在自己的主目录中构建,安装和运行Postgres。由于数据分析使用了Perl模块,因此需要配置Postgres使用PL / Perl。
mkdir -p ~/.local/src
cd ~/.local/src
curl https://ftp.postgresql.org/pub/source/v11.8/postgresql-11.8.tar.bz2 | \
tar -xj
cd ./postgresql-11.8
./configure \
--prefix=/home/cardenia/.local/postgres \
--with-perl
make -j
make install
至此已成功构建并安装了Postgres。她决定进行一次快速验证
$ cd ~
$ export PATH="${HOME}/.local/postgres/bin:${PATH}"
$ initdb ~/test
...
$ pg_ctl -D ~/test -l logfile start
waiting for server to start.... done
server started
$ createdb
$ psql -c 'SELECT version();'
version
---------------------------------------------------------------------------------------------------------
PostgreSQL 11.8 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39), 64-bit
(1 row)
看起来Postgres服务器已启动并运行,但她还想确保PL / Perl也能正常工作。于是启动psql并创建PL / Perl函数
-- taken from https://www.postgresql.org/docs/11/plperl-funcs.html
CREATE EXTENSION plperl;
CREATE FUNCTION perl_max (integer, integer) RETURNS integer AS $$
if ($_[0] > $_[1]) { return $_[0]; }
return $_[1];
$$ LANGUAGE plperl;
她通过调用以下函数来测试PL / Perl是否正常工作:
$ psql -c 'SELECT perl_max(1, 2);'
perl_max
----------
2
(1 row)
构建Postgres 12
距离Cardenia构建并安装Postgres 11.8,过去一段时间了,这时,Postgres 12发布了。新版本看起来有一些很好的新功能,但是Cardenia不确定她是否要从Postgres 11原地升级。于是她决定与现有Postgres 11.8并行安装Postgres 12。
现有的Postgres 11.8安装在~/.local/postgres中。那么在哪里安装新版本呢?Cardenia 决定将在安装位置包含版本信息,这样可以方便区分两个Postgres版本。
$ pg_ctl -D ~/test -l logfile stop
$ mv ~/.local/postgres ~/.local/postgres-11
$ export PATH="${HOME}/.local/postgres-11/bin:${PATH}"
$ pg_ctl -D ~/test -l logfile start
$ psql -c 'SELECT version();'
psql: error while loading shared libraries: libpq.so.5: cannot open shared object file: No such file or directory
Cardenia知道应用程序在运行时,需要ld-linux帮助二进制文件找到依赖的共享库,而通过ldd,可以帮助Cardenia打印二进制文件所依赖的共享库。
$ ldd ~/.local/postgres-11/bin/psql
linux-vdso.so.1 => (0x00007ffe5b9d2000)
libpq.so.5 => not found
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f6981cae000)
libreadline.so.6 => /lib64/libreadline.so.6 (0x00007f6981a68000)
librt.so.1 => /lib64/librt.so.1 (0x00007f6981860000)
libm.so.6 => /lib64/libm.so.6 (0x00007f698155e000)
libc.so.6 => /lib64/libc.so.6 (0x00007f6981190000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6981eca000)
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007f6980f66000)
注意:在不受信任的二进制文件上执行ldd是危险的,因为它会执行所探测的程序--相关概念参考ldd arbitrary code execution(https://catonmat.net/ldd-arbi...。当使用未知来源的二进制文件时,最好来自binutils的readelf。
ldd的输出显示动态链接器无法找到libpq.so.5。但是她通过以下检查,得知libpq.so.5确实存在:
$ ls -l ~/.local/postgres-11/lib/libpq.so*
lrwxrwxrwx 1 cardenia cardenia 13 Jul 23 21:14 /home/cardenia/.local/postgres-11/lib/libpq.so -> libpq.so.5.11
lrwxrwxrwx 1 cardenia cardenia 13 Jul 23 21:14 /home/cardenia/.local/postgres-11/lib/libpq.so.5 -> libpq.so.5.11
-rwxr-xr-x 1 cardenia cardenia 298384 Jul 23 21:14 /home/cardenia/.local/postgres-11/lib/libpq.so.5.11
既然libpq.so.5存在,那么为什么ldd找不到psql的依赖库libpq.so.5呢,Carenia决定阅读有关动态链接器的手册(man 8 ld-linux.so)。她了解到,链接器将在"/etc/ld.so.cache" 中查找共享库,并且该缓存由ldconfig管理。不幸的是,ldconfig需要root用户访问权限,因此Carenia无法添加“~cardenia/.local/postgres-11/lib”作为搜索共享库的位置。即使她可以,此配置也将适用于整个操作系统,而不仅仅是她的用户。另一种方式是使用LD_LIBRARY_PATH搜索共享库,但是此设置将应用于她在shell中运行的所有程序。动态链接器手册中还提到,链接器会使用二进制文件中dynamic section的DR_RPATH和DT_RUNPATH这两个属性所指定的目录加载共享库。读到这里,Cardenia猜想psql的这两个属性所指向的路径不对,导致无法找到共享库libpq.so.5,于是,她使用readelf检查psql是否已设置RUNPATH的值。
$ readelf --dynamic ~/.local/postgres-11/bin/psql
...
0x000000000000001d (RUNPATH) Library runpath: [/home/cardenia/.local/postgres/lib]
...
$ ls -l /home/cardenia/.local/postgres/lib
ls: cannot access /home/cardenia/.local/postgres/lib: No such file or directory
原因找到了,在移动安装目录后(/home/cardenia/.local/postgres -> /home/cardenia/.local/postgres-11)psql的DT_RUNPATH属性值依然是移动安装目录前的路径(/home/cardenia/.local/postgres/lib),这导致了链接器找不到libpq.so.5。这个问题可以通过重新构建Postgres 11来解决。但是在不重新构建的情况下,有可行的方法吗?动态链接器手册中提到了关于$ ORIGIN的允许为RUNPATH使用相对路径,试一试?
创建可重定位的Postgres- 第一次尝试
Cardenina认为:“我应该不是第一个想要构建可重定位的Postgres的人。”通过互联网搜索,她找到了完全符合她想要的如何构建可重定位的PostgreSQL。由于她想确保可重定位Postgres可以正常工作,因此她将--prefix设置为非预期的最终位置。
cd ~/.local/src/postgresql-11.8/
make clean
./configure \
--prefix=/home/cardenia/.local/postgres \
--with-perl \
--disable-rpath
LD_RUN_PATH='$ORIGIN/../lib' make -j
make install
目前构建安装成功了,Cardenia移动了安装目录:
mv ~/.local/postgres ~/.local/postgres-11
现在尝试一次链接器能否找到psql的相关依赖共享库:
$ ldd ~/.local/postgres-11/bin/psql
linux-vdso.so.1 => (0x00007ffcad968000)
libpq.so.5 => /home/cardenia/.local/postgres-11/bin/../lib/libpq.so.5 (0x00007f0cc305a000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f0cc2e3e000)
libreadline.so.6 => /lib64/libreadline.so.6 (0x00007f0cc2bf8000)
librt.so.1 => /lib64/librt.so.1 (0x00007f0cc29f0000)
libm.so.6 => /lib64/libm.so.6 (0x00007f0cc26ee000)
libc.so.6 => /lib64/libc.so.6 (0x00007f0cc2320000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0cc329d000)
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007f0cc20f6000)
目前psql可以找到相关依赖库,那么现在改试一试运行psql语句:
$ export PATH="${HOME}/.local/postgres-11/bin:${PATH}"
$ pg_ctl -D ~/test -l logfile start
waiting for server to shut down.... done
server stopped
$ psql -c 'SELECT version();'
version
-------------------------------------------------------------
PostgreSQL 11.8 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39), 64-bit
(1 row)
psql -c 'SELECT perl_max(1, 2);'
ERROR: could not load library "/home/cardenia/.local/postgres-11/lib/plperl.so": libperl.so: cannot open shared object file: No such file or directory
失败了?!? Cardenia 决定用ldd看看怎么回事。
$ ldd ~/.local/postgres-11/lib/plperl.so
linux-vdso.so.1 => (0x00007fffd4593000)
libperl.so => not found
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fe5c4e92000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe5c4ac4000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe5c52c4000)
原来是在RHEL7平台上,默认情况下libperl.so不在ld-linux.so的默认搜索的路径中:
$ find /usr -name 'libperl.so'
/usr/lib64/perl5/CORE/libperl.so
没有root权限她无法将此路径添加到“ /etc/ld.so.cache”,并且对RUNPATH使用单个值将不起作用。因此,Cardenia需要为Postgres的不同二进制文件和动态链接库设置RUNPATH值。
理解Postgres的构建系统
让我们看一下在Postgres的构建系统中如何处理RUNPATH,看看是否可以为Cardenia提出解决方案。
Configure
configure 的选项 --enable-rpath 来自 configure.in#L177-L182(https://github.com/postgres/p..._11_8/configure.in#L177-L182)
#
# '-rpath'-like feature can be disabled
#
PGAC_ARG_BOOL(enable, rpath, yes,
[do not embed shared library search path in executables])
AC_SUBST(enable_rpath)
- PGAC_ARG_BOOL 定义于 general.m4#L77-L102并且定义了两个可接受的选项yes 和 no
- 该宏同时用于 --enable-<option> 和 --with-<option>
- AC_SUBST 用shell变量($enable_rpath) 创建一个导出变量(@enable_rpath@)在文件中
- 在这里,导出变量的值是yes 或者 no
Makefile
$(enable_rpath)
唯一引用@enable_rpath@ 的文件是 src/Makefile.global.in#L199
enable_rpath = @enable_rpath@
这里设置了一个Makefile的变量,它的值等于configure脚本的shell变量enable_rpath,如下:
$ ./configure --enable-rpath
...
$ grep '^enable_rpath' src/Makefile.global
enable_rpath = yes
$ ./configure --disable-rpath
...
$ grep '^enable_rpath' src/Makefile.global
enable_rpath = no
Makefile的变量$(enable_rpath)有两个引用的地方。
在 "src/Makefile.shlib" 中的引用仅在 HP-UX 系统时使用,由于我们关注Linux系统,因此忽略. 于是我们关注在 "src.Makefile.global.in" 中的引用:当 $(enable_rpath) 为 yes时, 添加链接选项。
# Set up rpath if enabled. By default it will point to our libdir,
# but individual Makefiles can force other rpath paths if needed.
rpathdir = $(libdir)
ifeq ($(enable_rpath), yes)
LDFLAGS += $(rpath)
endif
$(rpath)
上一小节中,我们看到,当 $(enable\_rpath) 设置为 yes时候, $(rpath)的值会追加到 LDFLAGS 变量. 查看所有设置rpath的地方:
$ grep --recursive '^rpath ='
src/backend/utils/mb/conversion_procs/proc.mk:rpath =
src/backend/snowball/Makefile:rpath =
src/pl/plpgsql/src/Makefile:rpath =
src/makefiles/Makefile.freebsd:rpath = -Wl,-R'$(rpathdir)'
src/makefiles/Makefile.openbsd:rpath = -Wl,-R'$(rpathdir)'
src/makefiles/Makefile.netbsd:rpath = -Wl,-R'$(rpathdir)'
src/makefiles/Makefile.netbsd:rpath = -Wl,-R'$(rpathdir)'
src/makefiles/Makefile.solaris:rpath = -Wl,-rpath,'$(rpathdir)'
src/makefiles/Makefile.solaris:rpath = -Wl,-R'$(rpathdir)'
src/makefiles/Makefile.linux:rpath = -Wl,-rpath,'$(rpathdir)',--enable-new-dtags
可以看到
- 在某些地方,$(rpath)的值被设置为空
- 默认值由平台相关的Makefile设置
- 实际的RPATH/RUNPATH 值被设置为 $(rpathdir)
$(rpathdir)
上一小节看到,实际的 RPATH/RUNPATH 值由 $(rpathdir) 决定,查看它被赋值的地方:
$ grep --recursive '^rpathdir ='
contrib/jsonb_plperl/Makefile:rpathdir = $(perl_archlibexp)/CORE
contrib/hstore_plpython/Makefile:rpathdir = $(python_libdir)
contrib/jsonb_plpython/Makefile:rpathdir = $(python_libdir)
contrib/ltree_plpython/Makefile:rpathdir = $(python_libdir)
contrib/hstore_plperl/Makefile:rpathdir = $(perl_archlibexp)/CORE
src/Makefile.global.in:rpathdir = $(libdir)
src/pl/plperl/GNUmakefile:rpathdir = $(perl_archlibexp)/CORE
src/pl/plpython/Makefile:rpathdir = $(python_libdir)
可以看到:
- 默认的 $(rpathdir) 值是 $(libdir)
- 部分构建系统会将$(rpathdir)设置为不同的值
创建可重定位的Postgres- 第二次尝试
如果想将 RPATH/RUNPATH 设置为 $ORIGIN/../lib, 可以通过以下补丁实现:
diff -urN postgresql-11.8.orig/src/Makefile.global.in postgresql-11.8/src/Makefile.global.in
--- postgresql-11.8.orig/src/Makefile.global.in 2020-05-11 21:10:48.000000000 +0000
+++ postgresql-11.8/src/Makefile.global.in 2020-07-23 22:18:56.589659501 +0000
@@ -525,7 +525,7 @@
# Set up rpath if enabled. By default it will point to our libdir,
# but individual Makefiles can force other rpath paths if needed.
-rpathdir = $(libdir)
+rpathdir = $$ORIGIN/../lib
ifeq ($(enable_rpath), yes)
LDFLAGS += $(rpath)
这个补丁的假设是 $(bindir) 和 $(libdir) 位于同一个父目录.
根据上述补丁,Cardenia重新构建了Postgres 11。
cd ~/.local/src/postgresql-11.8
./configure \
--prefix=/home/cardenia/.local/postgres \
--with-perl \
--enable-rpath
make -j
make install
移动Postgres的安装路径
mv ~/.local/postgres ~/.local/postgres-11
这个方法没有重写 RPATH/RUNPATH 所有被赋值的地方。因此,Makefile中PL/Perl中的 RUNPATH 依然通过Perl runtime获取:
rpathdir = $(perl_archlibexp)/CORE
Cardenia 通过下列方式验证 psql and plperl.so
$ readelf -d ~/.local/postgres-11/bin/psql
...
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
...
$ ldd ~/.local/postgres-11/bin/psql
linux-vdso.so.1 => (0x00007ffed39a2000)
libpq.so.5 => /home/cardenia/.local/postgres-11/bin/../lib/libpq.so.5 (0x00007ffba7078000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007ffba6e5c000)
libreadline.so.6 => /lib64/libreadline.so.6 (0x00007ffba6c16000)
librt.so.1 => /lib64/librt.so.1 (0x00007ffba6a0e000)
libm.so.6 => /lib64/libm.so.6 (0x00007ffba670c000)
libc.so.6 => /lib64/libc.so.6 (0x00007ffba633e000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffba72bb000)
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007ffba6114000)
$ readelf -d ~/.local/postgres-11/lib/plperl.so
...
0x000000000000001d (RUNPATH) Library runpath: [/usr/lib64/perl5/CORE]
...
$ ldd ~/.local/postgres-11/bin/psql
linux-vdso.so.1 => (0x00007ffed39a2000)
libpq.so.5 => /home/cardenia/.local/postgres-11/bin/../lib/libpq.so.5 (0x00007ffba7078000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007ffba6e5c000)
libreadline.so.6 => /lib64/libreadline.so.6 (0x00007ffba6c16000)
librt.so.1 => /lib64/librt.so.1 (0x00007ffba6a0e000
libm.so.6 => /lib64/libm.so.6 (0x00007ffba670c000)
libc.so.6 => /lib64/libc.so.6 (0x00007ffba633e000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffba72bb000)
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007ffba6114000)
至此, Cardenia 终于有了一个可重定位的 Postgres 11 ,并且PL/Perl 可以正常运行。
$ export PATH="${HOME}/.local/postgres-11/bin:${PATH}"
$ pg_ctl -D ~/test -l logfile start
waiting for server to shut down.... done
server stopped
$ psql -c 'SELECT version();'
version
---------------------------------------------------------------------------------------------------------
PostgreSQL 11.8 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39), 64-bit
(1 row)
$ psql -c 'SELECT perl_max(1, 2);'
perl_max
----------
2
(1 row)
通过理解可执行文件,共享库和Postgres的构建系统,Cardenia现在可以并行构建和安装Postgres 11和Postgres 12。当发布Postgres 13时,她可以以此类推,安装Postgres 13!
总结
构建可重定位的Postgres似乎应该一个显而易见的功能。既然我们能够通过patch实现这一目标,那么为什么已有的Postgres构建系统不支持这一功能呢?
这里我们的patch能够做到这一点,依赖于以下的假设能够做到这一点:
- 我们忽略了非Linux 平台如 BSD 和 HP-UX
- 我们假设了 $(bindir) and $(libdir) 拥有共同的父目录。但configure 脚本支持定制$(bindir) and $(libdir)为任何路径
- 我们的patch不能成功跑测试 (如 installcheck 和 installcheck-world)
实际上,Postgres构建系统很早就提出过可重定位的Postgres。
Postgres开发者邮件列表
在pgsql-hackers的一封邮件中提交了创建相对RPATH/RUNPATH的patch。该patch存在几个问题:
- check-world 会失败,原因是没有安装用于测试的可执行文件
- 可能是邮件列表中表述错误,应该是 check-world 运行成功,但是 installcheck-world 运行失败
- readelf -d ./src/test/isolation/isolationtester 的输出显示,isolationtester依赖的 libpq.so.5 找不到
- 从source tree(而不是从安装目录)中运行二进制可执行文件会失败
- 这个提交到开发者邮件列表的patch和我们的patch有同样的假设,即二进制和库文件存在于同一个父目录(比如$(bindir) 和 $(libdir))
- 对于存在于$(libdir)子目录的库, $ORIGIN/../lib无法为这些库找到正确的依赖
- 对pgxs的模块无法生效
了解Greenplum实时资讯欢迎前往Greenplum中文社区网站
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。