Skip to content
Go back

Ubuntu 基础 NIS 客户端容器从 18.04 升级到 22.04 后 `getent passwd -s nis` 失效问题排查与兼容方案

Edit page

Ubuntu 基础 NIS 客户端容器从 18.04 升级到 22.04 后 getent passwd -s nis 失效问题排查与兼容方案

最近在维护一个 NIS 客户端容器镜像时,我们把基础镜像从 Ubuntu 18.04 升级到了 Ubuntu 22.04,同时顺手做了多架构支持。升级完成后,下游依赖的一些老调用开始出现异常:getent passwd -s nisgetent group -s nis 无法再正常返回 NIS 数据。

一开始这个问题很容易被误判成 “ARM 适配引发的兼容性问题”,但继续往下追后会发现,真正的触发点其实是 基础 OS 升级后 NIS/RPC 用户态栈的行为变化,而不是 ARM 本身。本文把问题背景、现象、根因和最终采用的兼容方案整理出来,希望能帮后来人少走一点弯路。

背景

我们的容器本质上是一个 NIS client,容器里会安装 nisrpcbindypbind 等组件,并通过 /etc/nsswitch.confpasswdgroupshadow 等数据库接到 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

例如:

但一旦显式加上 -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

这里有两个容易让人误判的点:

  1. ypwhich 明明是通的,看起来不像 NIS server 宕了。
  2. 普通 getent passwd 也能返回数据,看起来不像 nsswitch.conf/etc/yp.conf 写错了。

所以问题并不在“整个 NIS 都坏了”,而是在 getent -s nis 这条特殊调用路径 上。

为什么这个问题容易被误判成 ARM 回归

在我们的项目里,多架构支持和基础镜像升级是在同一个提交里做的,所以从时间线上看,大家第一反应很容易是:

之前 x86_64 正常,改了 ARM 支持后就不正常了,那大概率是 ARM 相关兼容性问题。

但回头检查历史可以发现:

也就是说,ARM 适配和问题出现只是“同时发生”,更可能的触发点是 22.04 下 NIS / libnsl / libtirpc / ypbind-mt 这套用户态组件的行为与 18.04 不同

根因分析

1. getent -s nis 的语义不是“只让 passwd 走 NIS”

很多人第一次看到这条命令时,会下意识理解为:

getent passwd -s nis

等价于:

只让 passwd 这个数据库走 nis

但实际上不是。

getent(1) 的语义是:

也就是说:

getent passwd -s nis

本质上等价于:

getent -s nis passwd

它的真实含义是:

当前这个 getent 进程里,不只是 passwd,连 hostsservicesrpc 等数据库也都被强制切到 nis

而真正只覆盖 passwd 的正确写法应该是:

getent -s passwd:nis passwd

group 也是同理:

getent -s group:nis group

2. 22.04 使用的是更新的 NIS/RPC 栈

在 Ubuntu 22.04 上,NIS 客户端相关组件主要是:

这和较老系统上的实现路径已经不完全一样了。上游 libnsl / libnss_nis 也明确说明过,这些功能已经从 glibc 中独立出来,改为基于 TI-RPC 的实现。

从现象上看,这一点非常关键:

3. 真正被 -s nis 毒化的,不只是 passwd,还有 RPC 启动依赖

getent passwd -s nis 出问题时,最迷惑人的地方在于:

为什么还是会报:

yp_bind_client_create_v3: RPC: Unknown host

继续往下看用户态调用链会发现,现代 libnsl / libtirpc 在建立 RPC client 时,不仅要处理 host,还会依赖类似下面这种解析:

getent -s nis 恰好会把 services 数据库也强制切到 NIS

于是就变成了一个非常隐蔽的循环依赖:

  1. getent 想通过 NIS 查询 passwd
  2. NIS client 需要先通过 RPC 找到 ypbind / rpcbind
  3. RPC 建连过程里要解析 sunrpc
  4. services 现在也被 -s nis 强制成 NIS 了
  5. 想查 sunrpc,又得先把 NIS 自己拉起来
  6. 递归依赖形成,最后报成 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 来:

这意味着:

所以普通 getent passwd 能工作,说明的只是:

默认 NSS 顺序下,这套环境是健康的

并不能说明:

getent -s nis 这种“强制所有数据库全走 NIS”的调用方式也是健康的

5. 为什么把 /etc/yp.conf 写成 IP 也不够

/etc/yp.conf 配成 IP 当然是对的,它至少避免了“解析 NIS server hostname 时又依赖 NIS”的经典递归问题。

但它只能解决 host 这一半,解决不了 sunrpc 服务名解析 这一半。

所以你会看到一种很有迷惑性的状态:

这也是为什么单纯反复调整 yp.confhostsnsswitch.conf,通常都治不好这个问题。

解决方案

方案 A:如果允许改上层调用,改成 database-scoped 的写法

如果上层调用可以改,那么最推荐的做法其实很简单:

getent -s passwd:nis passwd
getent -s group:nis group

这样只会覆盖目标数据库,不会把 hostsservices 等一并拖下水。

但在我们的场景里,上层调用已经存在很多年,短期内不希望逐个修改和回归测试,所以还需要一个“兼容旧调用方式”的方案。

方案 B:在镜像里增加 getent 兼容层(最终采用)

这是我们最后采用的方案,也是我认为 平滑升级、又不改变上层调用方式 时最实际的一种做法。

思路很简单:

这样既满足兼容性,也不会再污染 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 "$@"

这个方案的优点是:

  1. 不改上层调用方式,兼容成本最低。
  2. 不需要维护系统库 patch,升级风险更可控。
  3. 行为范围很窄,只对 passwd/group + -s nis 做重写,其他 getent 用法不受影响。

方案 C:更底层的方案,但不推荐作为首选

如果你完全不想引入 wrapper,那理论上还有两条路:

  1. patch libtirpc,让 RPC bootstrap 直接使用数字端口 111,而不是依赖 sunrpc 服务名解析;
  2. patch libnsl,尽量绕开当前这条会回到 host/service 解析的路径。

但这两条路的问题也很明显:

如果只是想让镜像在升级后平滑兼容历史调用方式,wrapper 基本是性价比最高的选择。

验证方式

兼容层加好之后,可以按下面的方式验证:

ypwhich
getent passwd
getent passwd -s nis
getent group -s nis

重点关注两点:

  1. getent passwd -s nis 能否正常枚举 NIS 用户
  2. 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 兼容改写,通常是最实用、也最好维护的方案。

参考资料


Edit page
Share this post on:

Previous Post
基于 `cturra/docker-ntp` 的 NTP 容器 `chronyd.pid` Permission Denied 问题排查与迁移方案
Next Post
排查 VMware 虚拟机中 vmwgfx 与 wlroots 的 DMA-BUF 导入失败问题