​作为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)有两个引用的地方。

  1. src/Makefile.global.in#L526-L532
  2. src/Makefile.shlib#L196-L227

在 "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能够做到这一点,依赖于以下的假设能够做到这一点:

  1. 我们忽略了非Linux 平台如 BSD 和 HP-UX
  2. 我们假设了 $(bindir) and $(libdir) 拥有共同的父目录。但configure 脚本支持定制$(bindir) and $(libdir)为任何路径
  3. 我们的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中文社区网站

image


Greenplum
153 声望66 粉丝

Greenplum 是全球领先的开源、多云大数据分析平台,被广泛运用于大规模商业智能和分析中,具有极高的稳定性。