Podman 的 auto-update 和 rollback

2024-09-24, 星期二, 16:29

DevOpsContainer培训

本文将通过一个简单的 Spring Boot 3 Web 项目演示 Podman 的 Auto-update 功能和 Rollback 机制。项目使用 eclipse-temurin:17-jre-noble 基底打包为容器镜像,并可通过如下命令启动:

docker run --rm -p 8080:8080 registry.ddrpa.cc/gunvor:latest

程序监听 HTTP 请求并返回诸如 1.0.0 之类的版本号,可确认程序正常运行并且是期望的版本。

curl localhost:8080/

> 1.0.0

Auto-update

参考 Quadlet 让 systemd 管理容器更容易 创建容器单元文件并启动服务。

# $HOME/.config/containers/systemd/user/gunvor.container
[Unit]
Description=Podman container-gunvor-showcase.service
Documentation=man:podman-systemd.unit
Wants=network-online.target
After=network-online.target

[Container]
Image=registry.ddrpa.cc/gunvor:latest
AutoUpdate=registry
PublishPort=8080:8080

[Install]
WantedBy=default.target

Podman 通过比较镜像的摘要与 registry(如果设置 AutoUpdate=registry)或本地(AutoUpdate=local)存储的摘要是否一致判断当前使用的镜像是否为新版本。因此在实际操作过程中,如果你需要将容器回滚到一个较旧的版本,完全可以将其 tag 修改为 latest

可以使用 podman auto-update --dry-run 检查镜像是否有更新。

$ podman auto-update --dry-run

UNIT            CONTAINER                      IMAGE                            POLICY      UPDATED
gunvor.service  4247e399b668 (systemd-gunvor)  registry.ddrpa.cc/gunvor:latest  registry    false

如果有镜像更新,UPDATED 就会显示为 pending,此时执行 podman auto-update 即可。

Podman 会注册名为 podman-auto-update 的服务单元和定时器单元。定时器默认设置每日零点启动更新服务,可以使用 systemctl --user enable podman-auto-update.timer 启用,下方的日志就展示了一次定时检查。

$ journalctl --user -xeu podman-auto-update.service

Sep 24 00:07:52 alma systemd[1213]: Starting Podman auto-update service...
░░ Subject: A start job for unit UNIT has begun execution
░░ Defined-By: systemd
░░ Support: https://access.redhat.com/support
░░
░░ A start job for unit UNIT has begun execution.
░░
░░ The job identifier is 524.
Sep 24 00:07:52 alma podman[32573]: 2024-09-24 00:07:52.966786397 +0800 CST m=+0.108055223 system auto-update
Sep 24 00:07:53 alma podman[32573]:             UNIT            CONTAINER                      IMAGE                     >
Sep 24 00:07:53 alma podman[32573]:             gunvor.service  a1481e70ace5 (systemd-gunvor)  registry.ddrpa.cc/gunvor:l>
Sep 24 00:07:54 alma.red.stratus.zjxasz.com systemd[1213]: Finished Podman auto-update service.
░░ Subject: A start job for unit UNIT has finished successfully
░░ Defined-By: systemd
░░ Support: https://access.redhat.com/support
░░
░░ A start job for unit UNIT has finished successfully.
░░
░░ The job identifier is 524.

你可以修改定时器配置,改变自动更新的频率和时间。

Simple Rollback

尽管发版之前程序已经通过了测试人员的重重检查,有的时候仍会出现程序无法正常启动的问题。Podman 3.4 开始支持在容器启动失败时自动回滚到上一个可用的镜像。

在下文的例子中,当前的 latest 版本在程序启动时故意抛出一个 NPE 令程序异常退出,可以看到 podman auto-update 在执行结果中标注 UPDATEDrolled back。此时容器自动回退到旧版本的镜像。

> podman auto-update

Trying to pull registry.ddrpa.cc/gunvor:latest...
Getting image source signatures
Copying blob bd40a8ba9b98 skipped: already exists
Copying blob 44b1b7b9b07e skipped: already exists
Copying blob 2bdf9306e3bf done   |
Copying config 6a7488ac68 done   |
Writing manifest to image destination
    UNIT            CONTAINER                      IMAGE                            POLICY      UPDATED
    gunvor.service  01f9ccc70a61 (systemd-gunvor)  registry.ddrpa.cc/gunvor:latest  registry    rolled back

如何实现?

systemd 需要知道服务的进程 ID 来跟踪服务的状态,例如在主进程退出后将其标记为 stopped,根据退出代码(exit code)判断是否 failed

在 Podman 中,主进程默认指向 conmon(Container monitor)。conmon 管理并监控着容器,当容器退出时,conmon 会使用相同的代码退出。

这就带来了一个问题,conmon 在启动容器后会发出 ready 信号,systemd 就认为服务启动成功了。就算你的程序写的有问题,退出也是那一刻之后的事情了,而回滚只会在 conmon(作为主进程)启动后立即异常退出的情况下发生。

sd_notify

一种解决方法是让容器负责发出 ready 信号。

通过 --sdnotify=container 参数或是在容器单元文件的 [container] 章节中配置 Notify=true,容器就有充足的时间完成初始化,然后通知 systemd 服务就绪。如果容器在启动时出现异常退出,或因为各种各样的原因超时,则 systemd 会检测到服务启动失败,令 Podman 开始回滚。

发出 ready 信号是通过向环境变量 NOTIFY_SOCKET 指定的套接字发送消息来实现的。对 C/C++ 技术栈,可以使用 sd_notify 函数或写入 /run/systemd/notify 套接字文件来完成。虽然 Java 技术栈在 JDK16 实现了 JEP-380: Unix domain socket channels,相关功能还未得到支持。

使用一些基于 junixsocket - GitHub 或 JNI 的库

可以使用 SDNotify - GitHub,在程序启动完成后发出就绪信号。注意这里通过检查环境变量 NOTIFY_SOCKET 判断程序是否需要发送就绪信号。

import info.faljse.SDNotify.SDNotify;

@Component
public class ReadyNotifier {
    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady() throws IOException {
        if (System.getenv("NOTIFY_SOCKET") != null) {
            SDNotify.sendNotify();
        }
    }
}

或是参考 septatrix/SdNotify.java - GitHub

Red Hat Universal Base Image 镜像 (UBI)

使用 UBI 镜像作为基底(例如eclipse-temurin:17-jdk-ubi9-minimal),可以通过执行 shell 命令发送 ready 消息:

systemd-notify --ready

在 Java 代码中使用 Runtime.getRuntime().exec("systemd-notify --ready")

你不需要考虑这些代码的异常处理,等待 ready 信号超时后,systemd 会自动结束服务。

Notify=healthy

Podman 5.0.0+ 支持设置 Notify=healthy,此时会使用 HealthCmd 的执行结果判断服务是否启动成功。

[Container]
Image=registry.dddrpa.cc/gunvor:latest
AutoUpdate=registry
Notify=healthy
HealthCmd=curl -f http://localhost:8080/ || exit 1

不过截至本文写作时,你只能在 Fedora 40 中使用 Podman 5.0。