Redis 8 容器实战:Docker、rootless Podman 与 redis-cli 速查

在 2 vCPU 测试机上,容器中的 Redis 通过 Docker 发布的回环端口完成每秒 21,400 次 SET;rootless Podman 的发布端口达到 25,200,容器内基准测试达到 44,800。经过发布端口会损失约一半吞吐,但它很少是初次 docker run redis 或 podman run redis 失败的原因。真正常见的问题有三个:使用 -a 传入密码导致其进入 shell 历史、Rocky Linux 上的 SELinux 阻止 Podman 卷,以及首次重启前从未验证持久化。

本文在 Docker 和 rootless Podman 中运行 Redis(镜像 redis:8.10.3,于 2026 年 11 月在 Ubuntu 26.04 与 Rocky Linux 10 上测试),随后介绍日常使用的 redis-cli 命令,包括用 SCAN 替代 KEYS、TTL、哈希、列表、集合、慢日志、大键扫描、持久化验证和最小权限 ACL 用户。速查表在前,后面的编号步骤展示每条命令在测试机上的实际表现。

redis-cli 速查表(快速参考)

下表中的每一行都在后文构建的 redis:8.10.3 容器中运行过。表格假设已在第 6 步完成两行 shell 设置:REDISCLI_AUTH 保存密码,redis-cli 是 docker exec -i -e REDISCLI_AUTH redis redis-cli 的别名(Rocky 上将 docker 换成 podman)。由于客户端在容器内没有 TTY,返回采用 raw 模式,因此看到的是 3,而不是 (integer) 3。

连接与认证

命令 作用
export REDISCLI_AUTH="$(cat ~/redis-lab/default.pass)" 向 redis-cli 提供密码,且不放进 argv
redis-cli PING 健康检查;返回 PONG 表示已连接并通过认证
redis-cli -h 127.0.0.1 -p 6379 PING 连接指定主机和端口
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app GET app:visits 以 ACL 用户身份执行一条命令
redis-cli --user app --askpass GET app:visits < ~/redis-lab/app.pass 从 stdin 而非环境变量读取密码
redis-cli -n 1 DBSIZE 在 1 号数据库中执行命令

服务器信息与运行时配置

命令 作用
redis-cli INFO server 版本、模式、操作系统与运行时间
redis-cli INFO memory 已用内存、RSS、碎片率与淘汰策略
redis-cli INFO persistence RDB 与 AOF 状态、上次保存结果
redis-cli CONFIG GET 'maxmemory*' 按 glob 模式读取设置
redis-cli CONFIG SET maxmemory 256mb 在线修改设置(重启后丢失)
redis-cli CONFIG SET maxmemory-policy allkeys-lru 达到内存上限时淘汰最近最少使用的键
redis-cli DBSIZE 当前数据库的键数量
redis-cli CLIENT LIST 列出所有客户端及标志

查找和检查键

命令 作用
redis-cli --scan --pattern 'user:9999*' 以非阻塞方式遍历匹配键
redis-cli SCAN 0 MATCH 'user:*' COUNT 1000 执行一页 SCAN;第一行是下一游标
redis-cli KEYS 'user:*' 在单次阻塞调用中匹配所有键(生产环境避免)
redis-cli TYPE profile:1001 键的数据类型
redis-cli OBJECT ENCODING profile:1001 内部编码(listpack、hashtable 等)
redis-cli MEMORY USAGE profile:1001 单个键占用的字节数,含开销
redis-cli EXISTS profile:1001 存在返回 1,不存在返回 0
redis-cli UNLINK big:list 删除键并在后台释放内存

字符串、计数器、过期与锁

命令 作用
redis-cli SET session:42 token-abc EX 300 设置 300 秒后过期的值
redis-cli GET lock:job1 读取字符串值(不存在时为空行)
redis-cli TTL session:42 剩余秒数;-1 无过期,-2 键不存在
redis-cli EXPIRE session:42 60 设置或替换键的过期时间
redis-cli PERSIST session:42 移除过期时间
redis-cli SET lock:job1 worker-a NX EX 30 仅在无人持有时获取锁
redis-cli INCR counter:hits 原子计数器,不存在时从 0 创建

哈希、列表和集合

命令 作用
redis-cli HSET profile:1001 name ada plan pro logins 0 设置哈希字段
redis-cli HGET profile:1001 plan 读取一个字段
redis-cli HGETALL profile:1001 读取全部字段和值,逐行交替
redis-cli HINCRBY profile:1001 logins 1 递增数值字段
redis-cli RPUSH queue:emails job1 job2 向列表尾部追加(队列尾)
redis-cli LPOP queue:emails 从列表头部取出
redis-cli LRANGE queue:emails 0 -1 打印整个列表
redis-cli BLPOP queue:emails 2 等待列表元素,最多 2 秒
redis-cli SADD tags:post1 redis docker podman 添加集合成员

延迟、慢命令和大键排障

命令 作用
redis-cli SLOWLOG GET 2 最近 2 条超过阈值的命令
redis-cli CONFIG SET slowlog-log-slower-than 1000 记录超过 1 ms 的命令(默认 10000 µs)
redis-cli SLOWLOG RESET 清空慢日志
redis-cli --bigkeys 按元素数量列出每种类型的最大键
redis-cli --memkeys 按字节数列出每种类型的最大键
redis-cli --keystats 键大小百分位和各类型总量
redis-cli --intrinsic-latency 5 5 秒内的主机调度延迟
redis-cli --latency 往返延迟;无 TTY 时采样 1 秒
docker exec -e REDISCLI_AUTH redis timeout 3 redis-cli MONITOR 监视所有命令 3 秒后退出

持久化与 ACL 用户

命令 作用
redis-cli BGSAVE 后台写出 RDB 快照
redis-cli LASTSAVE 上次成功保存的 Unix 时间
redis-cli ACL USERS 列出 ACL 用户
redis-cli ACL DRYRUN app GET user:1 按用户规则测试命令
redis-cli ACL LOG 1 最近被拒绝的命令及客户端
redis-cli ACL SETUSER app '+ping' '+echo' 逐条授予命令(比整类更安全)
redis-cli ACL GETUSER app 显示用户标志、命令规则和键模式
seq 1 100000 | awk '{print "SET user:"$1" value-"$1}' | redis-cli --pipe 通过单个流批量加载 100,000 个键

本页其余部分展示各组命令在测试机上的运行结果,并从容器本身开始。输出均来自真实记录,可与自己的结果对照。

步骤 1:安装 Docker 或 Podman

测试使用两台 e2-standard-2 云虚拟机(2 vCPU、8 GB 内存):Ubuntu 26.04 使用发行版仓库中的 Docker 与 Podman,Rocky Linux 10.2 使用 AppStream 中的 Podman。请选择对应主机的选项卡。只需一个引擎;除第 4 步的 SELinux 处理外,Redis 步骤完全相同。若希望把 Redis 作为发行版软件包的主机服务运行,可参考在 Ubuntu 26.04 或 Rocky Linux/AlmaLinux 上安装 Redis 的指南。本页 redis-cli 表格对这些安装方式同样适用。

Ubuntu 26.04

Rocky Linux 10

sudo apt-get update && sudo apt-get install -y docker.io docker-compose-v2 podman
sudo usermod -aG docker $USER
sudo dnf -y install podman openssl vim-enhanced

执行 usermod 后请注销再登录,使 docker 组权限应用到当前会话。该组成员在主机的权限等价于 root,因此只应授予管理员账户;Docker 与 Podman 速查表涵盖了本文未涉及的引擎命令。随后确认安装版本,因为两台主机的引擎版本不同,后续所有输出都与之相关。

Ubuntu 26.04

Rocky Linux 10

docker --version; docker compose version; podman --version

Ubuntu 软件仓库提供 Docker 29.1、Compose 2.40 和 Podman 5.7,因此该主机无需第三方仓库。

Docker version 29.1.3, build 29.1.3-0ubuntu4.1
Docker Compose version 2.40.3+ds1-0ubuntu1
podman version 5.7.0
podman --version; getenforce; cat /etc/rocky-release

Rocky 显示 Podman 5.8,且 SELinux 为 Enforcing;下文所有 Rocky 步骤都假设这一状态,因此第 4 步的卷标签非常重要。

podman version 5.8.2
Enforcing
Rocky Linux release 10.2 (Red Quartz)

测试过程中有两点需要注意。在 Ubuntu 上,除非启用 lingering,否则 SSH 会话结束时 rootless Podman 容器会停止;该主机上的基准容器在退出登录后停止,直到执行 sudo loginctl enable-linger $USER,与第 5 步 Rocky 使用的命令相同。在 Rocky 上,来自 download.docker.com 的 Docker CE 也能安装(测试版本 29.9.3),但其守护进程关闭了 SELinux 支持;未加标签挂载的主机目录会报 Can't open or create append-only dir appendonlydir: Permission denied。同一 Rocky 主机上,第 3 步的命名卷方式可在 Docker CE 中正常启动,因此在这里应使用命名卷或继续使用 Podman。

步骤 2:将 Redis 密码保存在文件中

官方镜像默认没有密码;一旦有人把端口发布到 0.0.0.0,这就不再安全。这里从生成文件读取 requirepass,而不是手工输入密码,因为使用 -a 输入的密码会进入 shell 历史和主机进程列表。以下三条命令在 Ubuntu 与 Rocky 上相同,用来创建私有目录和 48 位随机密码。目录权限比看起来更重要,因为它们保护的是本步骤稍后写入的配置文件。

mkdir -p ~/redis-lab && chmod 700 ~/redis-lab
openssl rand -hex 24 > ~/redis-lab/default.pass && chmod 600 ~/redis-lab/default.pass

创建一个最小 redis.conf,从该文件读取密码,并启用每秒 fsync 的 AOF。该设置崩溃时最多损失约一秒写入,适合既要作为缓存、又保存希望保留数据的 Redis。

printf 'requirepass %s\nappendonly yes\nappendfsync everysec\n' "$(cat ~/redis-lab/default.pass)" > ~/redis-lab/redis.conf

挂载前检查权限,因为容器会以不同于文件创建者的用户读取文件。密码文件应只允许所有者访问,下面的列表将验证这一点。

ls -l ~/redis-lab

密码文件仅所有者可读,而 redis.conf 保留 umask 产生的 664 权限。这个差异是有意为之,下一段会解释。

total 8
-rw------- 1 klinsmann klinsmann 49 Nov 11 07:48 default.pass
-rw-rw-r-- 1 klinsmann klinsmann 97 Nov 11 07:48 redis.conf

redis.conf 的 664 看似宽松,因为文件已经包含密码。这里之所以可接受,是因为父目录为 700,其他主机用户无法进入。容器内的 Redis 以 redis 用户(UID 999)运行,通过“其他用户”权限位读取绑定挂载的文件,因此更严格的模式会让服务器无法读取。我们测试过 600 权限的副本,Docker 容器立即退出,日志只说 Redis 无法读取配置,文件明明存在时这条信息很容易误导。

1:C 11 Nov 2026 08:03:59.578 # Fatal error, can't open config file '/usr/local/etc/redis/redis.conf': Permission denied

因此应收紧目录权限,而不是文件权限,并让 redis.conf 保持 664。700 的目录可以阻止其他主机用户进入,文件权限只需保证 UID 999 能读取。

步骤 3:在 Docker 容器中运行 Redis

将镜像固定到完整版本,而不是 redis:latest,这样下次 docker pull 或重建容器时不会在数据下方突然更换主版本。下面的标签是测试时 Redis 发布页上的最新稳定版,包含 cluster-bus 堆溢出修复,较旧的 8.x 标签值得替换。

time docker pull redis:8.10.3 # https://github.com/redis/redis/releases

测试机拉取耗时 4.2 秒。检查磁盘上的实际内容:

docker image ls redis

镜像下载大小为 57.6 MB,解包后为 213 MB,因为 Redis 8 内置了原先 Redis Stack 的 Search、JSON、时间序列和概率模块。这些模块会在启动时加载,也是空闲服务器后续显示较高碎片率的原因。

WARNING: This output is designed for human readability. For machine-readable output, please use --format.
IMAGE          ID            DISK USAGE   CONTENT SIZE   EXTRA
redis:8.10.3   1f88291b4c57        213MB         57.6MB

启动容器,为 /data 使用命名卷,以只读方式挂载配置,并只将端口发布到回环地址。绑定 127.0.0.1 可以防止 Docker 通过自身绕过 UFW 的 iptables 规则把 6379 暴露给全世界,因此这是安全配置而非装饰性参数。

docker run -d --name redis --restart unless-stopped -p 127.0.0.1:6379:6379 -v redis-data:/data -v ~/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro redis:8.10.3 redis-server /usr/local/etc/redis/redis.conf

确认容器已启动并绑定到预期地址,因为使用错误地址启动的容器看起来仍健康,直到有人扫描主机。因此先检查 PORTS 列,它能最快显示绑定地址。

docker ps --filter name=redis

PORTS 列必须显示 127.0.0.1:6379->6379/tcp。若只显示 0.0.0.0:6379,说明 -p 参数丢失了地址,应停止容器并修正命令后再继续;否则后续写入的数据会暴露在网络上所有主机都能访问的端口后面。

CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                      NAMES
4c27982ae0af   redis:8.10.3   "docker-entrypoint.s…"   6 seconds ago   Up 4 seconds   127.0.0.1:6379->6379/tcp   redis

最后几行日志表明配置已被读取,因为只有在 appendonly yes 生效时,启动阶段才会创建 AOF 基础文件。如果缺少这些行,说明文件没有挂载到 Redis 期望的位置。应先修复挂载,因为后续所有持久化检查都依赖该设置。

docker logs redis 2>&1 | tail -n 6

寻找两条 “Creating AOF” 和 “Ready to accept connections”。这组信息共同说明 Redis 已创建只追加文件并开始监听;我们将其作为就绪检查,因为它同时证明配置、卷和服务器都已正常启动。

1:M 11 Nov 2026 07:48:42.574 * <search> Acquired RedisJSON_V8 API
1:M 11 Nov 2026 07:48:42.575 * Server initialized
1:M 11 Nov 2026 07:48:42.575 * BGSAVE done, 0 keys saved, 0 keys skipped, 89 bytes written.
1:M 11 Nov 2026 07:48:42.578 * Creating AOF base file appendonly.aof.1.base.rdb on server start
1:M 11 Nov 2026 07:48:42.582 * Creating AOF incr file appendonly.aof.1.incr.aof on server start
1:M 11 Nov 2026 07:48:42.582 * Ready to accept connections tcp

现在无凭据命令会被拒绝,这是期望结果。直接发送不带密码的 PING 来验证。每个新容器都应做此检查,因为无需凭据即可响应的服务器会让后续安全措施失去意义。

docker exec redis redis-cli PING

Redis 返回认证错误而不是 PONG,说明第 2 步设置的密码已生效,端口没有敞开。这个失败正是预期结果,因此本次检查应视为通过。

NOAUTH Authentication required.

下面的终端展示 Ubuntu 26.04 测试机从固定版本拉取到未认证 PING 被拒绝的同一过程。

Ubuntu 26.04 上 Redis 8.10.3 Docker 容器、回环端口和未认证 PING 被拒绝

从第 6 步开始,所有操作都连接名为 redis 的容器,因此在完成本页其余内容前请保持其运行。

使用 Docker Compose 运行同一容器

当 Redis 与应用程序容器相邻部署时,Compose 更合适,因为服务定义可保存在 Git 中。创建项目目录并复制自己的配置文件。

mkdir -p ~/redis-compose && chmod 700 ~/redis-compose && cp ~/redis-lab/redis.conf ~/redis-compose/redis.conf
vim ~/redis-compose/compose.yaml

粘贴以下服务定义。这里发布到 6381,只是为了能与第一个容器并行运行;若只有一个容器则使用 6379。回环地址必须保留,Compose 文件不能撤销第 3 步的保护。

services:
  redis:
    image: redis:8.10.3
    container_name: redis-compose
    restart: unless-stopped
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    ports:
      - "127.0.0.1:6381:6379"
    volumes:
      - redis-compose-data:/data
      - ./redis.conf:/usr/local/etc/redis/redis.conf:ro

volumes:
  redis-compose-data:

在项目目录中启动:

cd ~/redis-compose && docker compose up -d

Compose 会创建项目网络、命名卷和容器。独立网络与卷意味着这套栈无法接触第一个容器的数据。

 Network redis-compose_default  Creating
 Network redis-compose_default  Created
 Volume redis-compose_redis-compose-data  Creating
 Volume redis-compose_redis-compose-data  Created
 Container redis-compose  Creating
 Container redis-compose  Created
 Container redis-compose  Starting
 Container redis-compose  Started

先导出密码到 REDISCLI_AUTH(原因见第 6 步),再统计键数量。该计数同时证明复制后的配置密码在此容器中有效,因为未认证计数会被拒绝。这里统计键而不是发送 PING,还能显示卷是否为空。

export REDISCLI_AUTH="$(cat ~/redis-lab/default.pass)"
docker exec -e REDISCLI_AUTH redis-compose redis-cli DBSIZE

新卷为空,因此结果为 0。用 docker compose down 拆除项目;命名卷会保留,只有 docker compose down -v 才会删除,真正清理时应保留 -v。

步骤 4:在启用 SELinux 的 rootless Podman 中运行 Redis

这一 Podman 配置以普通用户在 Rocky Linux 10 上运行同一容器,并保持 SELinux Enforcing。使用完全限定镜像名,避免 Podman 为短名称选择仓库。拉取耗时 5.3 秒。

podman pull docker.io/library/redis:8.10.3 # https://github.com/redis/redis/releases

在这台主机上重新执行第 2 步命令,并为数据添加普通目录,因为绑定挂载才会让 SELinux 标签产生影响。命名卷会掩盖接下来要演示的问题。

mkdir -p ~/redis-lab/data && chmod 700 ~/redis-lab

先使用大量 Docker 教程中常见的卷参数,不加标签选项,以观察失败。见过一次后,之后更容易识别该症状。我们完全按常见写法执行,因为静默退出的容器最容易浪费读者一小时。

podman run -d --name redis -p 127.0.0.1:6379:6379 -v ~/redis-lab/data:/data -v ~/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro docker.io/library/redis:8.10.3 redis-server /usr/local/etc/redis/redis.conf

Podman 打印容器 ID 并返回 0,因此运行看似成功。四秒后容器消失,说明故障发生在容器内部,而不是 podman run 调用本身。

podman ps -a --filter name=redis --format '{{.Names}} {{.Status}}'

状态 1 是退出码。如此早的非零退出通常指向入口脚本,下一步应查看容器日志。

redis Exited (1) 4 seconds ago

日志只有一行,而且完全没有提到 SELinux。它来自镜像入口脚本,该脚本在启动 Redis 前遍历 /data 修正所有权,因此读取被拒绝会让容器在 Redis 启动前退出。

podman logs redis 2>&1 | tail -n 5

下面是那条唯一日志。它报告 . 的权限错误,却没有说明原因是 SELinux。

find: '.': Permission denied

在我们的主机上,sudo ausearch -m avc -ts recent 对该拒绝没有任何输出,因此审计日志不是最快的诊断方式,文件标签才是。两个路径仍带有主目录类型 user_home_t,容器进程无权读取;用 ls -dZ 查看标签即可确认。

ls -dZ ~/redis-lab/data ~/redis-lab/redis.conf

两者都显示主目录上下文。这就是失败原因:SELinux 策略不允许容器进程读取 user_home_t 文件。第 3 步的 Docker 在 Ubuntu 上不需要标签,因此相同参数在那里可用,在这里却失败。

unconfined_u:object_r:user_home_t:s0 /home/klinsmann/redis-lab/data
unconfined_u:object_r:user_home_t:s0 /home/klinsmann/redis-lab/redis.conf

用 podman rm -f redis 删除已退出的容器,并在两个挂载上加入 :Z 重新运行。两种标签选项的区别在于之后谁可以访问文件。

  • 大写 Z: 为这个容器重新标记私有标签,其他容器无法读取。

  • 小写 :z: 应用共享标签,任何容器都可使用,只应在多个容器挂载相同数据时选择。

Redis 数据只属于一个容器,因此这里应使用私有标签。

podman run -d --name redis -p 127.0.0.1:6379:6379 -v ~/redis-lab/data:/data:Z -v ~/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z docker.io/library/redis:8.10.3 redis-server /usr/local/etc/redis/redis.conf

现在容器保持运行。能通过入口脚本检查,说明重新标记后的路径可以从容器内读取。

podman ps --filter name=redis --format '{{.Names}} {{.Status}} {{.Ports}}'

状态为 Up,且端口绑定在回环地址,说明容器既在运行,也只绑定本机,与 Docker 下相同。我们有意保留该绑定,因为标签修复与网络暴露无关。

redis Up 4 seconds 127.0.0.1:6379->6379/tcp

再次列出标签,可看到 :Z 对主机文件所做的改变;重标记作用于文件本身,而不是容器内部。并排比较两次列表,类型列的变化就是全部修复。

ls -dZ ~/redis-lab/data ~/redis-lab/redis.conf

类型现在为 container_file_t,MCS 对 c332,c647 将两个路径仅绑定到此容器。不同 MCS 对的另一个容器会被拒绝,这就是私有标签提供的隔离。

system_u:object_r:container_file_t:s0:c332,c647 /home/klinsmann/redis-lab/data
system_u:object_r:container_file_t:s0:c332,c647 /home/klinsmann/redis-lab/redis.conf

下面的截图将失败运行、修改前后的标签和成功运行的容器放在一起,来自 SELinux Enforcing 的 Rocky Linux 10 测试机。

Rocky Linux 10 上未加 :Z 失败的 rootless Podman 容器与加 :Z 后成功的 SELinux 标签对比

切勿将 :Z 指向 /etc 等系统路径或整个主目录。Podman 会重新标记你提供的所有对象,其他受限服务随后会失去访问。

rootless 模式下数据文件属于谁

写入一个键并执行 BGSAVE 后(第 6 步会设置客户端),在主机上列出数据目录。对习惯 Docker 的人来说,所有者列会带来意外。

ls -ln ~/redis-lab/data

这些文件属于 UID 1180646,而不是你本人,因此普通主机工具会把它们视为其他用户的数据。这是查看数据目录时首先注意到的现象,也解释了下面的备份建议。

total 4
drwx------. 2 1180646 1180646 103 Nov 11 08:02 appendonlydir
-rw-------. 1 1180646 1180646 112 Nov 11 08:02 dump.rdb

该数字是容器内 redis 用户(UID 999)映射到从属 UID 范围后的结果。在用户命名空间内读取同一目录,可以看到容器视角。

podman unshare ls -ln ~/redis-lab/data

在命名空间内所有者又显示为 999,确认文件本身没有问题,只是主机视角不同。备份时要利用该映射,因为命名空间视角才与 Redis 看到的相同。

total 4
drwx------. 2 999 999 103 Nov 11 08:02 appendonlydir
-rw-------. 1 999 999 112 Nov 11 08:02 dump.rdb

这对备份有直接影响。以普通用户在主机侧对 dump.rdb 执行 cp 或 tar 会因 0600 权限失败,因此应使用 podman unshare 包一层,或用 podman cp redis:/data/dump.rdb . 复制出来。

步骤 5:使用 Quadlet 单元在开机时启动 Redis

Docker 的 --restart unless-stopped 依赖 root 守护进程在开机时启动。rootless Podman 没有守护进程,因此由 systemd 通过 Quadlet 文件接管,替代旧的 podman generate systemd 输出。先用 podman rm -f redis 删除手工容器,否则单元会与名称和端口冲突;然后创建单元目录和文件。

mkdir -p ~/.config/containers/systemd
vim ~/.config/containers/systemd/redis.container

粘贴该单元。说明符 %h 会展开为主目录,两个卷都保留 :Z,从而让第 4 步的标签延续到服务。若省略,单元会像首次运行一样因 SELinux 阻止绑定挂载而失败。

[Unit]
Description=Redis 8 in rootless Podman

[Container]
ContainerName=redis
Image=docker.io/library/redis:8.10.3
Exec=redis-server /usr/local/etc/redis/redis.conf
PublishPort=127.0.0.1:6379:6379
Volume=%h/redis-lab/data:/data:Z
Volume=%h/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z

[Service]
Restart=always

[Install]
WantedBy=default.target

启动前先让生成器展示将产生的内容。键名拼写错误会在此出现,而不是变成一个静默缺失的服务,因此 dry run 几乎无成本,可省去一次排障。启动单元前阅读生成文件中的挂载和端口。

/usr/libexec/podman/quadlet -dryrun -user 2>&1 | head -n 25

输出为生成的 redis.service,其中自动加入 network-online 依赖和 podman rm 停止钩子。这些都不是手工编写的,也是使用 Quadlet 而不是手写单元的原因。

quadlet-generator[8752]: Loading source unit file /home/klinsmann/.config/containers/systemd/redis.container
---redis.service---
[Unit]
Wants=podman-user-wait-network-online.service
After=podman-user-wait-network-online.service
Description=Redis 8 in rootless Podman
SourcePath=/home/klinsmann/.config/containers/systemd/redis.container
RequiresMountsFor=%t/containers

[X-Container]
ContainerName=redis
Image=docker.io/library/redis:8.10.3
Exec=redis-server /usr/local/etc/redis/redis.conf
PublishPort=127.0.0.1:6379:6379
Volume=%h/redis-lab/data:/data:Z
Volume=%h/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z

[Service]
Restart=always
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/usr/bin/podman rm -v -f -i redis
ExecStopPost=-/usr/bin/podman rm -v -f -i redis
Delegate=yes
Type=notify

启用 lingering,使用户的 systemd 实例无需登录即可随系统启动,然后加载并启动服务。跳过 linger 时,Redis 只会在你 SSH 登录后出现,单元在测试中看似正常,但首次无人值守重启后会失败。

sudo loginctl enable-linger $USER
systemctl --user daemon-reload && systemctl --user start redis.service

生成的单元不能通过 systemctl --user enable 启用,因为 .container 文件中的 [Install] 节才负责挂接到 default.target。检查状态确认生效;若显示已加载但未激活,通常说明安装步骤被跳过。

systemctl --user status redis.service --no-pager | head -n 12

单元处于 Active (running),主 PID 为 conmon,Redis 位于负载 cgroup 中,占用 39.7 MB 内存。此时数据集几乎为空,因此这个数字只是空闲下限,不能用于容量规划。

● redis.service - Redis 8 in rootless Podman
     Loaded: loaded (/home/klinsmann/.config/containers/systemd/redis.container; generated)
     Active: active (running) since Wed 2026-11-11 08:02:53 UTC; 3s ago
 Invocation: 1a0dc9ca1c3341f5b466c88cdc37c85e
   Main PID: 8862 (conmon)
      Tasks: 8 (limit: 48395)
     Memory: 39.7M (peak: 46.9M)
        CPU: 283ms
     CGroup: /user.slice/user-1010.slice/user@1010.service/app.slice/redis.service
             ├─libpod-payload-cae8a5a48306f156eb9f5fea6ffff51cbbad20b19cce8e264ae349499a1d251d
             │ └─8864 "redis-server *:6379"
             └─runtime

真正的测试是重启。执行 sudo systemctl reboot 并重新 SSH 登录后,单元已自动激活,重启前写入的键仍存在,因此该检查同时覆盖开机启动和磁盘数据。

systemctl --user is-active redis.service
redis-cli GET probe:selinux

两个结果都符合预期,证明 lingering、生成单元和重新标记的数据目录协同工作。任何一个缺失,服务都不会激活,或者键会丢失。

active
ok

下面的截图展示第一次启动时的单元状态,以及同一 Rocky 主机重启后的两项检查。

Rocky Linux 10 上 Redis Quadlet systemd 用户单元及重启后仍存在的键

第二条命令中的 redis-cli 是第 6 步的两行 shell 设置,别名中使用 podman exec。重启后的新登录不会自动具备该别名,因此要先重新执行两行,或将其加入 ~/.bashrc。

步骤 6:使用 redis-cli 连接且不暴露密码

redis-cli 已包含在镜像内,主机无需安装。两行 shell 设置让它像本地命令一样工作。第一行把密码导出到 REDISCLI_AUTH,这是 redis-cli 未收到 -a 时读取的变量(显式 -a 或 --askpass 优先)。第二行让 redis-cli 在容器内运行,并按名称传递该变量,因此值不会出现在任何进程参数中。在共享主机上这很重要,否则任何能运行 ps 的人都能读到 -a 后的密码;同时也要让第 2 步的设置对后续所有命令保持一致。

export REDISCLI_AUTH="$(cat ~/redis-lab/default.pass)"
alias redis-cli='docker exec -i -e REDISCLI_AUTH redis redis-cli'

在 Podman 主机上,别名相同,只需把 docker exec 换成 podman exec。用 PING 测试连接,该命令需要密码,因此能证明变量已传达到服务器。

redis-cli PING

返回 PONG 说明密码已到达并被接受。若出现认证错误,应检查导出的变量,而不是容器。

PONG

该别名有一个陷阱:shell 只在命令的第一个词展开别名。因此 timeout 3 redis-cli MONITOR 会在主机上寻找真实的 redis-cli 二进制,Ubuntu 26.04 的 Rust coreutils 会报告找不到。

timeout: failed to execute process: No such file or directory (os error 2)

凡是用 timeout 包装的命令,都应把超时放进容器内,如第 9 步所示。若想完全避免环境变量,可用 --askpass 从 stdin 读取密码,使密码既不进入 argv,也不进入 shell 环境。

docker exec -i redis redis-cli --user app --askpass GET app:visits < ~/redis-lab/app.pass

它返回了第 11 步创建的 app 用户保存的值 1。从其他机器或客户端容器连接时,按常规使用 -h 和 -p,同样传递变量,密码也不会进入参数列表。

docker run --rm --network host -e REDISCLI_AUTH redis:8.10.3 redis-cli -h 127.0.0.1 -p 6379 PING

主机网络上的临时客户端容器返回 PONG,说明远程客户端只需地址、端口和相同的密码处理方式。图形客户端同理;Redis GUI 指南涵盖了这一部分。

读取服务器信息并在线修改设置

INFO 按区段划分,只请求一个区段可保持输出可读。新实例的内存区段是有用基线,之后每次读数都可与它比较。加载数据前先保存输出,以便为增长量提供起点。

redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|maxmemory_human|maxmemory_policy|mem_fragmentation_ratio'

镜像默认没有内存上限,也没有淘汰策略。应先检查输出中的 maxmemory 和策略行,因为它们决定内存不足时发生什么。按镜像默认值,答案是没有任何键被淘汰,服务器会持续增长。

used_memory_human:1.40M
used_memory_rss_human:24.27M
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:17.52

17.52 的碎片率看起来惊人,但它是 RSS 除以 1.4 MB 数据集的结果,空服务器上加载的模块占据了大部分 RSS。只有数据集达到数百 MB 后再判断该比例才有意义。第一天上线的关键设置是 maxmemory:在 noeviction 且无上限时,Redis 会持续增长,直到内核 OOM killer 选择进程。可用 glob 读取当前值。

redis-cli CONFIG GET 'maxmemory*'

raw 模式把每个名称和值分别放在一行,因此输出在设置名和当前值之间交替。应按对读取,因为 glob 匹配了五个 maxmemory 设置,每个名称都紧挨其值上方。

maxmemory
0
maxmemory-eviction-tenacity
10
maxmemory-clients
0
maxmemory-policy
noeviction
maxmemory-samples
5

为缓存负载设置上限和淘汰策略。两者都会返回 OK 并立即生效,无需重启,因此可在运行中的服务器上尝试。

redis-cli CONFIG SET maxmemory 256mb
redis-cli CONFIG SET maxmemory-policy allkeys-lru

重新读取会得到字节数 268435456。这些更改只存在内存中。CONFIG REWRITE 通常会保存到配置文件,但本文文件以只读方式挂载,因此会失败。

redis-cli CONFIG REWRITE

这个错误是有意保留的,因为第 3 步的只读挂载阻止任何人通过运行中的服务器修改文件。这样 Git 中的文件始终是配置的唯一来源。

ERR Rewriting config file: Permission denied

我们更希望如此。容器配置应来自 Git 中保存的文件;若要让设置持久化,请把 maxmemory 256mb 和 maxmemory-policy allkeys-lru 加入 ~/redis-lab/redis.conf,再重启容器。

步骤 7:加载测试键并用 SCAN 替代 KEYS

空服务器无法体现 KEYS 与 SCAN 的差异,因此先加载 100,000 个字符串键。--pipe 把 stdin 中的全部内容一次性传输给服务器,只等待最后一个回复,比 100,000 次单独调用快得多。这里的 awk 生成简单内联命令,适合基本键值;二进制安全加载应按 Redis 批量插入指南生成 raw RESP 协议。

time (seq 1 100000 | awk '{print "SET user:"$1" value-"$1}' | redis-cli --pipe)

100,000 次写入在半秒内完成,且零错误。错误数比耗时更重要,因为静默失败会让后续扫描测试拿到的键少于预期。干净加载也能保证后续计时每次可重复。

All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 100000

real    0m0.490s
user    0m0.077s
sys 0m0.037s

现在分别计时两种查找以 user:9999 开头的 11 个键的方法。如下方结果所示,实际耗时只是比较的一半。

time redis-cli KEYS 'user:9999*' | wc -l
time redis-cli --scan --pattern 'user:9999*' | wc -l

两者都返回 11 个键,而 KEYS 以 0.097 秒快于 --scan 的 0.629 秒。这正是陷阱。KEYS 在客户端墙钟时间上更快,因为它一次完成完整遍历;但遍历期间,单线程服务器无法服务其他客户端。--scan 通过许多小调用分页读取键空间,因此更慢,但客户端之间能得到服务。要观察阻塞成本,可将慢日志阈值降到 1 ms,再执行不带过滤条件的 KEYS。

redis-cli CONFIG SET slowlog-log-slower-than 1000
redis-cli SLOWLOG RESET
redis-cli KEYS 'user:*' | wc -l
redis-cli SLOWLOG GET 2

raw 模式下每条日志被展平为每行一个值,因此需按顺序阅读。字段依次为条目 ID 1、Unix 时间戳、微秒耗时、命令及参数、客户端地址、空客户端名和参数总数。最后一个值是 Redis 8.10 给 SLOWLOG GET 新增的第七个字段。

1
1794383388
19368
KEYS
user:*
127.0.0.1:37980

2

服务器为返回 100,000 个键名阻塞了 19.4 ms。按线性比例推算到一百万键约为 200 ms(这是估算而非实测),足以触发应用超时,因此不带过滤的 KEYS 不应出现在在线服务器上。慢日志捕捉到了它,因为其记录所有超过阈值的命令;建议在任何批量操作后检查慢日志,确认单次调用是否长时间占住服务器。完成后用 redis-cli CONFIG SET slowlog-log-slower-than 10000 恢复默认阈值。

100000 个键的批量加载、KEYS 与 SCAN 计时以及 KEYS 阻塞的慢日志

raw SCAN 还有一个意外:某一页可能为空,但游标仍非零,因此空回复不代表扫描结束。我们在前面加载的 100,000 个键上遇到了这一点,因此下面两张截图分别展示游标和空页。

redis-cli SCAN 0 MATCH 'user:9999*' COUNT 1000

第一行是下一游标,第二行空内容是本页匹配结果。非零游标且无匹配,正是会让简单脚本出错的场景。

49472

MATCH 在读取分页后进行过滤,因此遇到第一个空页就停止的脚本会漏键。应循环到游标回到 0,或让 --scan 代为循环。内置循环更安全,因为它不会因空页提前停止。

步骤 8:处理字符串、TTL、哈希、列表和集合

缓存缺陷大多与过期有关,因此值得弄清 TTL 的返回码。设置一个 300 秒后过期的会话键,并依次观察各状态。

redis-cli SET session:42 token-abc EX 300
redis-cli TTL session:42
redis-cli EXPIRE session:42 60
redis-cli PERSIST session:42
redis-cli TTL session:42
redis-cli TTL nosuchkey

每条命令返回一个值,各值含义不同。

  • 299: 键剩余秒数,比初始 300 少 1 秒。

  • 1 和 1: EXPIRE 与 PERSIST 均成功。

  • -1: 键存在但没有过期时间。

  • -2: 键不存在。

下面的 raw 回复按照该顺序排列,请逐行对应。

OK
299
1
1
-1
-2

对本应过期的键出现 -1 是典型泄漏,因为不带 EX 的普通 SET 会静默移除已有 TTL。带 NX 和 EX 的 SET 也可实现简单锁,只在无人持有时设置,因此持有者崩溃也不会永久阻塞其他人。过期时间是安全网;没有过期时间的锁会一直保持,直到有人手工删除键,因此我们绝不在没有 EX 时设置锁。

redis-cli SET lock:job1 worker-a NX EX 30
redis-cli SET lock:job1 worker-b NX EX 30
redis-cli GET lock:job1

第一次调用输出 OK,第二次输出空行(raw 模式下为 nil,表示锁已被占用),GET 返回 worker-a。哈希在一个键下保存对象字段。这里使用 profile:1001,因为 user:1001 已在批量加载中作为字符串存在,而在字符串键上执行 HSET 会报 WRONGTYPE Operation against a key holding the wrong kind of value。

redis-cli HSET profile:1001 name ada plan pro logins 0
redis-cli HINCRBY profile:1001 logins 1
redis-cli HGETALL profile:1001

HSET 报告新增 3 个字段,HINCRBY 返回新计数,HGETALL 交替输出字段和值。由于 raw 回复是扁平结构,脚本必须自行配对行。

3
1
name
ada
plan
pro
logins
1

列表可作为队列:RPUSH 追加,LPOP 从头部取出,BLPOP 带超时等待元素,因此工作进程可以睡眠到任务到来,而不是轮询。超时参数很重要;没有它时,调用会在列表持续为空期间一直等待。

redis-cli RPUSH queue:emails job1 job2 job3
redis-cli LPOP queue:emails
redis-cli LLEN queue:emails
time redis-cli BLPOP queue:empty 2

列表长度增至 3,LPOP 返回 job1,剩余 2 项;对空列表执行 BLPOP 在 2.139 秒后返回空,即 2 秒等待加上约 0.14 秒的 docker exec 启动时间。集合保存唯一成员,并回答成员关系与交集问题。

redis-cli SADD tags:post1 redis docker podman
redis-cli SADD tags:post2 redis valkey
redis-cli SISMEMBER tags:post1 docker
redis-cli SINTER tags:post1 tags:post2

返回结果表示新增 3 个和 2 个成员,1 表示“docker 是成员”,共享标签只有 redis。键出现异常时,有三条命令可告诉你它是什么、消耗多少,先运行它们再猜测。每条命令只读取单个键并立即返回,因此可在在线服务器上安全试查。

redis-cli TYPE profile:1001
redis-cli OBJECT ENCODING profile:1001
redis-cli MEMORY USAGE profile:1001

结果分别是 hash、listpack 和 70 字节。小哈希使用紧凑的 listpack 编码,超过 hash-max-listpack-entries 或包含超过 hash-max-listpack-value 的值后切换为真正哈希表(默认 512 个字段和 64 字节;redis-cli CONFIG GET 'hash-max-listpack-*' 在我们的服务器上打印了两者)。切换后每个字段的内存开销会跳升,因此 70 字节的小哈希不能代表大哈希成本。

步骤 9:查找慢命令、大键与延迟

MONITOR 打印服务器收到的每条命令,是必须始终限制运行时间的 redis-cli 模式。在主机侧用 timeout 包裹 docker exec 看似合理,但我们的机器在主机侧进程被杀后,监视器仍在容器内运行。下面的检查列出已连接客户端以证明这一点。

timeout 3 docker exec -i -e REDISCLI_AUTH redis redis-cli MONITOR
redis-cli CLIENT LIST | grep 'flags=O' | cut -d' ' -f1-3

O 标志表示 monitor 客户端,在 timeout 结束主机侧命令后仍有一个保持连接。因此杀死主机侧进程不会结束服务器端会话。

id=26 addr=127.0.0.1:38274 laddr=127.0.0.1:6379

孤立的 monitor 会持续损耗吞吐,因为服务器要把每条命令复制给它。先用 redis-cli CLIENT KILL ID 26 按 ID 杀死,再在容器内执行 timeout,让超时结束客户端本身。后台任务会在一秒后写入键,以便流中能看到内容。

(sleep 1; redis-cli SET probe 2 >/dev/null) & docker exec -e REDISCLI_AUTH redis timeout 3 redis-cli MONITOR

流中的密码会被遮蔽,因此 monitor 截图可以安全粘贴到工单。之后 redis-cli CLIENT LIST | grep -c 'flags=O' 输出 0,说明容器内 timeout 已自行清理。每次监视会话后都应检查该计数,因为孤立 monitor 在被清除前会一直损耗吞吐。

OK
1794384072.931575 [0 127.0.0.1:50954] "AUTH" "(redacted)"
1794384072.931815 [0 127.0.0.1:50954] "SET" "probe" "2"

通过别名执行延迟模式不需要 timeout。没有 TTY 时,redis-cli 只采样一秒、打印一行摘要并退出,适合写入日志(真实终端中会持续重绘,直到按 Ctrl+C)。

redis-cli --latency

四列分别是往返延迟的最小值、最大值、平均值(毫秒)和样本数。应重点看最大值,因为平均值会掩盖偶发慢回复。这里最大值为 0.584 ms,98 个样本的平均值为 0.250 ms,本次没有明显离群值。

0.114 0.584 0.250 98

这类亚毫秒数字对云虚拟机回环连接很正常。要判断尖峰来自 Redis 还是 hypervisor,应测量主机本身。--intrinsic-latency 不执行任何 Redis 命令,而是对 CPU 忙循环计时。

redis-cli --intrinsic-latency 5

最后两行给出结论,因为它们报告繁忙循环期间调度器造成的最坏暂停。

Max latency so far: 209 microseconds.

96814192 total runs (avg latency: 0.0516 microseconds / 51.65 nanoseconds per run).
Worst run took 4047x longer than the average latency.

仅调度器就产生 209 µs 最坏延迟,说明该虚拟机上任何 Redis 命令的尾部延迟都不可能稳定低于约 0.2 ms。若 --latency 远高于此,问题在 Redis 或网络路径,而不是主机。

查找最大的键

刻意加入一个大列表,让扫描有对象可发现,因为干净数据集没有可供大键扫描排序的内容。

seq 1 50000 | awk '{print "RPUSH big:list item-"$1}' | redis-cli --pipe

三种扫描模式回答不同问题,应根据正在排查的问题选择。

  • --bigkeys: 按元素数量排序,找出元素最多的列表。

  • --memkeys: 按字节数排序,找出真正占用内存的对象。

  • --keystats: 增加大小百分位,展示整个键空间的分布。

在同一服务器上运行三者,因为它们从不同角度读取数据。

redis-cli --bigkeys | tail -n 13
redis-cli --memkeys | tail -n 11

--memkeys 汇总显示 50,000 项列表占 592,400 字节,而平均字符串仅占 47.92 字节。一个键的重量相当于数千个普通键,这正是大键扫描要揭示的问题。

Biggest   list found "big:list" has 592400 bytes
Biggest   hash found "profile:1001" has 70 bytes
Biggest string found "session:43" has 56 bytes
Biggest    set found "tags:post1" has 59 bytes

1 lists with 592400 bytes (00.00% of keys, avg size 592400.00)
1 hashs with 70 bytes (00.00% of keys, avg size 70.00)
0 streams with 0 bytes (00.00% of keys, avg size 0.00)
100004 strings with 4792180 bytes (100.00% of keys, avg size 47.92)
2 sets with 110 bytes (00.00% of keys, avg size 55.00)
0 zsets with 0 bytes (00.00% of keys, avg size 0.00)

两种模式选出的最大字符串不同。--bigkeys 按 12 字节值选择 user:100000,而 --memkeys 选择 56 字节的 session: 键,因为它还把键名和分配器开销计入。截图中写的是侧测产生的 session:43;你的运行中应为 session:42,第一轮同样占 56 字节。问题在内存时以 --memkeys 为准;容量规划时 --keystats 更适合:

redis-cli --keystats | tail -n 20

其百分位表显示 99.994% 的键不超过 48 字节,而单个列表键占 578.53K。

Key size Percentile Total keys
-------- ---------- -----------
     29B    0.0010%           1
     48B   99.9940%      100002
     56B   99.9970%      100005
     70B   99.9990%      100007
 578.53K  100.0000%      100008
Note: 0.01% size precision, Mean: 53B, StdDeviation: 1.83K

Key name length Percentile Total keys
--------------- ---------- -----------
            12B  100.0000%      100008
Total key length is 965.80K (9B avg)

Type        Total keys  Keys % Tot size Avg size  Total length/card Avg ln/card
--------- ------------ ------- -------- -------- ------------------ -----------
list                 1   0.00%  578.52K  578.52K        50000 items    50000.00
hash                 1   0.00%      70B      70B           3 fields        3.00
string          100004 100.00%    4.57M      47B              1.04M         11B
set                  2   0.00%     110B      55B          5 members        2.50

三种模式内部都使用 SCAN,因此可安全用于在线服务器。删除大键时使用 redis-cli UNLINK big:list 而非 DEL;UNLINK 在后台线程释放内存,不会因整个列表而阻塞。对于 50,000 项的列表,这决定其他客户端是否会感知清理。

redis-cli --bigkeys 与 --memkeys 扫描到 50000 项列表

50,000 项列表会在每份报告中突出显示,这正是希望在事故前而不是事故中扫描得到的结果。

步骤 10:验证持久化能经受重启

重启后丢键的容器通常缺少 /data 卷或 AOF 设置,因此在把重要数据交给它前先测试持久化。触发快照并读取持久化计数器,以确认上次保存是否成功、AOF 是否启用。

redis-cli BGSAVE
redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|rdb_last_bgsave_time_sec|rdb_changes_since_last_save|aof_enabled|aof_last_write_status'

出现 Background saving started 后,计数器显示快照在一秒内成功且 AOF 已启用。两点都重要,因为第 2 步配置要求每秒 fsync 的 AOF,崩溃时最多损失约一秒写入。

rdb_changes_since_last_save:0
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
aof_enabled:1
aof_last_write_status:ok

查看 Redis 写入卷中的文件,大小可以显示每种持久化方式的磁盘成本,以及写入积累时哪种增长更快。快照和只追加文件彼此独立,因此都会出现在列表中。

docker exec redis ls -la /data /data/appendonlydir

RDB 快照为 2.9 MB,而 AOF 增量文件已增长到 7.0 MB。差距来自各自存储内容,下一段会计算。

/data:
total 2868
drwxr-xr-x 3 redis redis    4096 Nov 11 07:53 .
drwxr-xr-x 1 root  root     4096 Nov 11 07:48 ..
drwx------ 2 redis redis    4096 Nov 11 07:48 appendonlydir
-rw------- 1 redis redis 2924463 Nov 11 07:53 dump.rdb

/data/appendonlydir:
total 6900
drwx------ 2 redis redis    4096 Nov 11 07:48 .
drwxr-xr-x 3 redis redis    4096 Nov 11 07:53 ..
-rw------- 1 redis redis      89 Nov 11 07:48 appendonly.aof.1.base.rdb
-rw------- 1 redis redis 7046471 Nov 11 07:50 appendonly.aof.1.incr.aof
-rw------- 1 redis redis     102 Nov 11 07:48 appendonly.aof.manifest

AOF 是快照的 2.4 倍,因为它记录每次写入,包括 50,000 次 RPUSH,直到重写将其压缩为新的基础文件。磁盘规划应关注这一比例,按 AOF 而非快照规划卷容量。上面的数字只是某一时刻的值,不是上限。现在重启并统计键数,总数一致即证明数据从卷中读回。

redis-cli DBSIZE; docker restart redis >/dev/null && sleep 2 && redis-cli DBSIZE

重启前后的计数完全相同:

100007
100007

100,007 个键全部恢复。只有带 TTL 的键可能合法丢失,因为它们在容器停止期间会过期。LASTSAVE 返回上次成功保存的 Unix 时间;若该值停止变化应触发告警,因为冻结的时间戳意味着保存正在静默失败。

步骤 11:创建最小权限 ACL 用户

应用程序不应共享默认用户密码。创建一个仅能访问 app:* 键的 app 用户,并通过 -x 从 stdin 传入密码,使其不出现在 argv 中。> 前缀是 ACL 中“添加密码”的语法,因此从 stdin 传入的值应包含该前缀。

openssl rand -hex 24 > ~/redis-lab/app.pass && chmod 600 ~/redis-lab/app.pass
printf '>%s' "$(cat ~/redis-lab/app.pass)" | redis-cli -x ACL SETUSER app on resetkeys '~app:*' resetchannels '+@read' '+@write' '-@dangerous'

无需以该用户登录即可测试规则。ACL DRYRUN 只执行权限检查,不写入数据、不建立会话,因此可在调整模式时反复运行。应分别测试用户应访问和不应访问的键,因为要信任规则必须同时验证两侧。

redis-cli ACL DRYRUN app SET app:visits 1
redis-cli ACL DRYRUN app GET user:1

Redis 允许第一条命令并拒绝第二条,因为该键不在 app:* 模式内。对每个键模式做 dry run,是在应用正式使用前低成本验证规则的方法。

OK
User app has no permissions to access the 'user:1' key

然后真正登录,仅覆盖当前命令的 REDISCLI_AUTH。写入成功,但用户甚至无法查询自己是谁,因为账户只授予了数据命令。

REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app SET app:visits 1
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app ACL WHOAMI

写入返回 OK,身份查询被拒绝。这一对结果说明规则双向有效:同一用户既被允许执行一条命令,也被拒绝另一条。

OK
NOPERM User app has no permissions to run the 'acl|whoami' command

下一个会出问题的是健康检查,因为连接池会发送 PING,而 app 用户尚无权限。授予任何权限前先尝试,可知道连接池会记录哪种错误。

REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app PING

拒绝信息会指出命令,从而明确应授予什么。

NOPERM User app has no permissions to run the 'ping' command

我们的第一次修复是添加 +@connection,该类别包含 PING 与 ECHO,但也包含 CLIENT KILL 和 CLIENT PAUSE。ACL 规则从左到右应用,因此在 -@dangerous 后写 +@connection 会把危险命令重新授予。按这种方式创建的临时用户在我们的服务器上通过了 ACL DRYRUN probe CLIENT KILL ID 1 并返回 OK,应用因此可以断开所有其他客户端。应改为授予单独命令:

redis-cli ACL SETUSER app '+ping' '+echo' '+client|setname'
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app PING
redis-cli ACL DRYRUN app CLIENT KILL ID 1

SETUSER 返回 OK,app 用户现在能得到 PONG,dry run 也证明危险的 client 命令仍不可用。CLIENT SETNAME 用于会给连接命名的客户端库;若不需要可去掉。

OK
PONG
User app has no permissions to run the 'client|kill' command

用默认用户执行 redis-cli ACL LOG 1 可看到先前拒绝记录,包括客户端地址、原因和命令,这对定位配置错误的服务很有用。检查最终规则集:

redis-cli ACL GETUSER app | grep -A1 -E '^(commands|keys)$'

命令规则从 -@all 开始,仅添加我们授予的内容,未列出的全部拒绝。仅授予的类别是 @read 与 @write,并用 -@dangerous 裁剪;未来 Redis 若把某命令归入这两类,该用户会自动获得权限,因此其他权限都应逐命令授予。这个默认拒绝结构使账户保持最小权限,前提是后续每次授权都指定命令而非整个类别。

commands
-@all +@read +@write -@dangerous +ping +echo +client|setname
keys
~app:*

下面是 Ubuntu 测试机上从 dry run 到拒绝 CLIENT KILL 的完整 ACL 过程:

redis-cli ACL app 用户权限验证与最终最小权限规则

运行时用户会在重启后消失。ACL SAVE 需要 aclfile 指令(我们的配置没有,因此会报错),而 CONFIG REWRITE 无法写入只读配置。我们执行 docker restart redis 验证了丢失,重启后 ACL USERS 只列出 default。适合只读配置的修复方式是在 redis.conf 中声明用户:

printf 'user app on >%s ~app:* resetchannels +@read +@write -@dangerous +ping +echo +client|setname\n' "$(cat ~/redis-lab/app.pass)" >> ~/redis-lab/redis.conf
docker restart redis >/dev/null && sleep 2 && redis-cli ACL USERS

这次两个用户在重启后都保留,因为定义已进入 Redis 每次启动都会读取的配置文件。

app
default

该命令中的两秒暂停很重要,因为容器需要片刻才能重新接受连接。没有暂停时,我们有一次运行与重启竞速,得到 Could not connect to Redis at 127.0.0.1:6379: Connection refused。

步骤 12:运行协议兼容的 Valkey 替代方案

Valkey 是 Redis 7.2.4 的 Linux Foundation 分支,使用相同协议,因此客户端和 redis-cli 可直接工作。数据文件则不同:Valkey 迁移指南说明 Redis 7.4 及之后写出的 RDB 文件无法加载,包括本文的 redis-data 卷。应在新卷上启动 Valkey,并逐键迁移数据,不能复制文件。将它运行在另一个回环端口旁;标签为 Valkey 发布页上的当前稳定版:

docker run -d --name valkey -p 127.0.0.1:6380:6379 valkey/valkey:9.1.3 # https://github.com/valkey-io/valkey/releases

Redis 8 镜像中的 redis-cli 可直接连接,因为 Valkey 保持相同协议。让新端口上的服务器表明身份:

docker run --rm --network host redis:8.10.3 redis-cli -p 6380 INFO server | grep -E 'server_name|valkey_version'

Valkey 会返回自己的名称和版本,因此同一主机上同时运行时可以区分。把应用指向错误端口前应检查,因为两者响应相同命令,这种错误很难察觉。

server_name:valkey
valkey_version:9.1.3

在 Valkey 自己的容器中客户端名为 valkey-cli,其 INFO 仍报告 redis_version:7.2.4,以兼容据此分支的客户端库。我们仅用 Redis 8 客户端对 Valkey 执行 PING 和 INFO,两者均正常工作;本速查表其余部分只在 Redis 上测试。若要在两者之间选择,Valkey 与 Redis 对比涵盖许可证和 RDB 安全迁移路径。

Docker 与 rootless Podman:Redis 实测吞吐

开头吞吐数据来自 redis-benchmark:每个路径 200,000 请求、50 客户端、运行三次,目标为各引擎上的一次性无认证容器。发布端口行使用主机网络中的客户端,回环行使用服务器容器内的客户端,因此两者差距隔离了端口路径成本。为每个引擎启动一个无密码、无卷的一次性服务器:

docker run -d --name bench-docker -p 127.0.0.1:6390:6379 redis:8.10.3
podman run -d --name bench-podman -p 127.0.0.1:6391:6379 docker.io/library/redis:8.10.3

然后让基准测试指向各发布端口(Podman 为 6391);回环行则在容器内执行 docker exec bench-docker redis-benchmark -q -n 200000 -c 50 -t set,get:

docker run --rm --network host redis:8.10.3 redis-benchmark -p 6390 -q -n 200000 -c 50 -t set,get | tr '\r' '\n' | grep 'requests per second'

tr 步骤很重要,因为 -q 用回车重绘进度行;不处理会令日志文件充满约 70 KB 的不完整行。每次运行输出 SET 与 GET 两行,三次运行即每条路径六个数字。

SET: 21281.12 requests per second, p50=1.831 msec
GET: 21588.95 requests per second, p50=1.791 msec

下表全部为每秒 SET 请求数。每个 Ubuntu 运行中 GET 与 SET 差距在约 2.2% 内;Rocky 的一次容器内运行差距达到 12%,这也是 Rocky 行波动较大的原因之一。每格是一轮运行,可自行判断离散度。

主机与路径 第 1 轮 第 2 轮 第 3 轮 p50 延迟
Ubuntu 26.04,Docker 发布端口 127.0.0.1 21,281 21,452 21,333 1.80–1.83 ms
Ubuntu 26.04,rootless Podman 发布端口 25,179 25,278 25,000 1.63–1.71 ms
Ubuntu 26.04,Docker 容器内 44,633 45,147 44,663 0.57 ms
Rocky Linux 10,rootless Podman 发布端口 20,947 20,931 19,737 1.45–1.57 ms
Rocky Linux 10,Podman 容器内 40,104 37,030 34,941 0.62–0.64 ms

表格有三种读法,因为每组比较回答不同问题,只有同一主机内的行才能直接公平比较。

  • 发布端口 vs 容器内: 两个引擎中端口路径都把吞吐减半,Docker 从约 44,800 降至 21,400。2 vCPU 下,基准客户端、端口转发器和 Redis 竞争同样核心,因此这是最差情况;另一台机器上的客户端不会消耗 Redis 主机的转发器 CPU。

  • Ubuntu 上 Docker vs rootless Podman: Podman 发布端口每次都较 Docker 的 docker-proxy 路径快 18%,波动低于 1.2%。rootless 网络已不再是旧版 Podman 的性能惩罚。

  • Ubuntu vs Rocky: Rocky 主机更慢、波动更大,容器内运行从最好到最差相差 13%。共享云核心上的跨虚拟机比较只能作为参考;同主机内的行才可信。

一个有趣现象:Rocky 发布端口虽吞吐更低,但 p50 延迟为 1.45–1.57 ms,低于 Ubuntu Docker 端口的 1.80–1.83 ms,因此其慢速来自尾延迟而不是典型请求。内存不是差异因素:空闲基准容器在 Docker 下使用 5.7 MiB、Podman 下 5.8 MB。

若缓存只供本机容器访问,应完全跳过发布端口,把应用放入同一 Docker 网络或 Podman pod,从而保持在更快的容器内路径。当主机上的应用必须连接时,rootless Podman 在 Ubuntu 上是更快、更安全的默认选择;在 Rocky 上,它也是无需更改守护进程就能正确处理 SELinux 标签的唯一引擎。若要在这些数字之上构建仪表板,Redis Prometheus 与 Grafana 配置可导出本速查表手工读取的相同 INFO 计数器。

Avatar photo

关于 木子

Email: muzi@vip.rockylinux.cn 微信:VebinLee QQ: 2306867585
Founder of the Rocky Linux Chinese community, MVP、VMware vExpert、TVP, advocate for cloud native technologies, with over ten years of experience in site reliability engineering (SRE) and the DevOps field. Passionate about Cloud Computing、Microservices、CI&CD、DevOps、Kubernetes, currently dedicated to promoting and implementing Rocky Linux in Chinese-speaking regions.
用一杯咖啡支持我们,我们的每一篇[文档]都经过实际操作和精心打磨,而不是简单地从网上复制粘贴。期间投入了大量心血,只为能够真正帮助到您。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇