Ubuntu 基础 NIS 客户端容器从 18.04 升级到 22.04 后 getent passwd -s nis 失效问题排查与兼容方案
最近在维护一个 NIS 客户端容器镜像时,我们把基础镜像从 Ubuntu 18.04 升级到了 Ubuntu 22.04,同时顺手做了多架构支持。升级完成后,下游依赖的一些老调用开始出现异常:getent passwd -s nis 和 getent group -s nis 无法再正常返回 NIS 数据。
一开始这个问题很容易被误判成 “ARM 适配引发的兼容性问题”,但继续往下追后会发现,真正的触发点其实是 基础 OS 升级后 NIS/RPC 用户态栈的行为变化,而不是 ARM 本身。本文把问题背景、现象、根因和最终采用的兼容方案整理出来,希望能帮后来人少走一点弯路。
背景
我们的容器本质上是一个 NIS client,容器里会安装 nis、rpcbind、ypbind 等组件,并通过 /etc/nsswitch.conf 将 passwd、group、shadow 等数据库接到 NIS。
典型配置类似下面这样:
passwd: files systemd nis
group: files systemd nis
shadow: files nis
hosts: files dns
同时 /etc/yp.conf 中会显式指定 NIS server,尽量使用 IP,避免主机名解析再依赖 NIS:
domain nis.example server 10.106.4.145
升级后出现问题的调用方式是:
getent passwd -s nis
getent group -s nis
注意,这里上层调用方式已经存在很多年了,短期内不想改动,所以我们的目标并不是“换成更正确的调用方式”,而是希望在升级到新基础镜像后,继续兼容旧调用方式。
故障现象
容器启动后,一些基本检查其实是正常的:
domainname
ypwhich
getent passwd
例如:
domainname能看到正确的 NIS domainypwhich能返回 NIS server 地址- 普通的
getent passwd可以看到 NIS 用户
但一旦显式加上 -s nis,问题就出现了:
getent passwd -s nis
报错类似:
yp_bind_client_create_v3: RPC: Unknown host
yp_bind_client_create_v3: RPC: Unknown host
yp_bind_client_create_v3: RPC: Unknown host
如果你的 ypbind 恰好是用 -debug 方式以前台启动的,还可能看到额外的调试日志混在终端里,例如:
ypbindproc_domain_3_svc ...
Ping active server for 'nis.example'
YPBIND_SUCC_VAL
这里有两个容易让人误判的点:
ypwhich明明是通的,看起来不像 NIS server 宕了。- 普通
getent passwd也能返回数据,看起来不像nsswitch.conf或/etc/yp.conf写错了。
所以问题并不在“整个 NIS 都坏了”,而是在 getent -s nis 这条特殊调用路径 上。
为什么这个问题容易被误判成 ARM 回归
在我们的项目里,多架构支持和基础镜像升级是在同一个提交里做的,所以从时间线上看,大家第一反应很容易是:
之前 x86_64 正常,改了 ARM 支持后就不正常了,那大概率是 ARM 相关兼容性问题。
但回头检查历史可以发现:
ypbind的启动方式没有实质变化nsswitch.conf没有变化entrypoint里生成/etc/yp.conf的逻辑也没有变化- 真正大的变化是 基础镜像从 Ubuntu 18.04 升到了 Ubuntu 22.04
也就是说,ARM 适配和问题出现只是“同时发生”,更可能的触发点是 22.04 下 NIS / libnsl / libtirpc / ypbind-mt 这套用户态组件的行为与 18.04 不同。
根因分析
1. getent -s nis 的语义不是“只让 passwd 走 NIS”
很多人第一次看到这条命令时,会下意识理解为:
getent passwd -s nis
等价于:
只让
passwd这个数据库走nis
但实际上不是。
getent(1) 的语义是:
-s service:覆盖所有数据库,都改用这个 service-s database:service:只覆盖指定数据库
也就是说:
getent passwd -s nis
本质上等价于:
getent -s nis passwd
它的真实含义是:
当前这个
getent进程里,不只是passwd,连hosts、services、rpc等数据库也都被强制切到nis
而真正只覆盖 passwd 的正确写法应该是:
getent -s passwd:nis passwd
group 也是同理:
getent -s group:nis group
2. 22.04 使用的是更新的 NIS/RPC 栈
在 Ubuntu 22.04 上,NIS 客户端相关组件主要是:
libnss-nislibnsl2libtirpc3ypbind-mt
这和较老系统上的实现路径已经不完全一样了。上游 libnsl / libnss_nis 也明确说明过,这些功能已经从 glibc 中独立出来,改为基于 TI-RPC 的实现。
从现象上看,这一点非常关键:
- 旧系统里,错误用法有时“碰巧能工作”
- 新系统里,这个错误用法更容易触发 bootstrap 依赖问题
3. 真正被 -s nis 毒化的,不只是 passwd,还有 RPC 启动依赖
getent passwd -s nis 出问题时,最迷惑人的地方在于:
ypwhich能通- 普通
getent passwd能返回用户 /etc/yp.conf已经用的是 IP,不是 hostname
为什么还是会报:
yp_bind_client_create_v3: RPC: Unknown host
继续往下看用户态调用链会发现,现代 libnsl / libtirpc 在建立 RPC client 时,不仅要处理 host,还会依赖类似下面这种解析:
- 解析 host
- 解析 RPC 服务名
sunrpc(对应 111 端口)
而 getent -s nis 恰好会把 services 数据库也强制切到 NIS。
于是就变成了一个非常隐蔽的循环依赖:
getent想通过 NIS 查询passwd- NIS client 需要先通过 RPC 找到
ypbind/rpcbind - RPC 建连过程里要解析
sunrpc - 但
services现在也被-s nis强制成 NIS 了 - 想查
sunrpc,又得先把 NIS 自己拉起来 - 递归依赖形成,最后报成
RPC: Unknown host
这里的 Unknown host 其实有一定误导性。它并不一定真的意味着“你写的主机名不存在”,也可能是底层 getaddrinfo() 在处理 host / service 任一侧解析失败时,统一落成了这个错误文本。
调用链示意
可以把问题简化成下面这条链路:
getent passwd -s nis
-> 强制 passwd / hosts / services / rpc ... 全部走 nis
-> libnsl / libtirpc 需要先建立 RPC 连接
-> RPC bootstrap 需要解析 host 和服务名 sunrpc
-> 但 services 现在也被强制走 nis
-> 想访问 nis 之前又要先访问 nis
-> 循环依赖
-> yp_bind_client_create_v3: RPC: Unknown host
而普通 getent passwd 的路径则是:
getent passwd
-> passwd 按 nsswitch.conf 走 files/systemd/nis
-> hosts 仍然走 files/dns
-> services 仍然走 db/files
-> RPC bootstrap 不会被一并拖进 NIS
-> 查询成功
4. 为什么普通 getent passwd 又是正常的
因为普通调用会按照 /etc/nsswitch.conf 来:
passwd走files systemd nishosts走files dnsservices走db files
这意味着:
- 账号查询本身可以走 NIS
- 但 RPC 启动过程中依赖的 host/service 解析,依旧可以从本地文件或 DNS 解决
所以普通 getent passwd 能工作,说明的只是:
默认 NSS 顺序下,这套环境是健康的
并不能说明:
getent -s nis这种“强制所有数据库全走 NIS”的调用方式也是健康的
5. 为什么把 /etc/yp.conf 写成 IP 也不够
把 /etc/yp.conf 配成 IP 当然是对的,它至少避免了“解析 NIS server hostname 时又依赖 NIS”的经典递归问题。
但它只能解决 host 这一半,解决不了 sunrpc 服务名解析 这一半。
所以你会看到一种很有迷惑性的状态:
yp.conf已经是 IP 了ypwhich也能通- 但
getent passwd -s nis还是失败
这也是为什么单纯反复调整 yp.conf、hosts、nsswitch.conf,通常都治不好这个问题。
解决方案
方案 A:如果允许改上层调用,改成 database-scoped 的写法
如果上层调用可以改,那么最推荐的做法其实很简单:
getent -s passwd:nis passwd
getent -s group:nis group
这样只会覆盖目标数据库,不会把 hosts、services 等一并拖下水。
但在我们的场景里,上层调用已经存在很多年,短期内不希望逐个修改和回归测试,所以还需要一个“兼容旧调用方式”的方案。
方案 B:在镜像里增加 getent 兼容层(最终采用)
这是我们最后采用的方案,也是我认为 平滑升级、又不改变上层调用方式 时最实际的一种做法。
思路很简单:
- 保留上层原始调用:
getent passwd -s nisgetent group -s nis
- 但在镜像里包一层
getentwrapper - 当识别到
passwd/group + -s nis这种老写法时,自动改写成更精确的形式:getent -s passwd:nis passwdgetent -s group:nis group
这样既满足兼容性,也不会再污染 hosts / services / rpc 等数据库。
Dockerfile 中的处理大致如下:
RUN mv /usr/bin/getent /usr/bin/getent.real && \
install -m 0755 /nis/getent-wrapper.sh /usr/bin/getent
wrapper 的核心逻辑是:
# 伪代码
if service == "nis" and database == "passwd":
exec /usr/bin/getent.real -s passwd:nis passwd ...
if service == "nis" and database == "group":
exec /usr/bin/getent.real -s group:nis group ...
# 其他调用原样透传
exec /usr/bin/getent.real "$@"
这个方案的优点是:
- 不改上层调用方式,兼容成本最低。
- 不需要维护系统库 patch,升级风险更可控。
- 行为范围很窄,只对
passwd/group + -s nis做重写,其他getent用法不受影响。
方案 C:更底层的方案,但不推荐作为首选
如果你完全不想引入 wrapper,那理论上还有两条路:
- patch
libtirpc,让 RPC bootstrap 直接使用数字端口111,而不是依赖sunrpc服务名解析; - patch
libnsl,尽量绕开当前这条会回到 host/service 解析的路径。
但这两条路的问题也很明显:
- 需要长期维护自定义系统库
- 后续 OS 升级还要继续背 patch
- 对团队其他人来说,排障和交接成本都更高
如果只是想让镜像在升级后平滑兼容历史调用方式,wrapper 基本是性价比最高的选择。
验证方式
兼容层加好之后,可以按下面的方式验证:
ypwhich
getent passwd
getent passwd -s nis
getent group -s nis
重点关注两点:
getent passwd -s nis能否正常枚举 NIS 用户getent group -s nis能否正常枚举 NIS 组
如果你还保留了 ypbind -debug 这样的启动参数,建议先去掉,不然调试日志可能会混在命令输出里,影响判断。
小结
这个问题的本质不是 “NIS 配错了”,也不是 “ARM 不兼容”,而是:
一个历史上就不够准确、但在旧系统里凑巧能工作的调用方式,到了更新的 Ubuntu 22.04 NIS/RPC 栈上,被放大成了显性故障。
如果可以修改上层代码,最正统的方式当然是:
getent -s passwd:nis passwd
getent -s group:nis group
但如果你的目标是 平滑升级、又不动上层调用,那么在镜像里做一层精确的 getent 兼容改写,通常是最实用、也最好维护的方案。
参考资料
- getent(1) - Ubuntu 18.04 (Bionic)
- getent(1) - Ubuntu 22.04 (Jammy)
- glibc
nss/getent.c - Ubuntu Jammy
libnss-nispackage - Ubuntu Jammy
libnsl2package - Ubuntu Jammy
ypbind-mtpackage - yp.conf(5) - Ubuntu 22.04
- ypbind(8) - Ubuntu 22.04
- thkukuk/libnsl
- thkukuk/libnss_nis
- thkukuk/ypbind-mt
- getaddrinfo(3) - Linux man-pages