容器技术是存储、网络、计算的集合体。换言之,要想使用好容器,需要对容器的存储、网络、计算模型有比较丰富的经验。本文介绍在飞牛或其他任何 Linux 系统部署多个容器服务时可以使用的方案。
思考几个问题
首先,在使用容器的过程中,我们先考虑一下有可能遇到的问题。
存储性能
现在大部分容器都使用数据库技术,数据库和普通存储不一样。一般来说,数据库处于文件系统和应用程序之间。
数据库会向文件系统申请较大的单一文件存储自己的所有表。因此数据库的存储粒度是「自行管理」的。虽然看上去 .db 文件很大,但是数据库操作基本上也是小粒度的随机 I/O.
如果底层文件系统的逻辑块大小非常大,每次数据库读写的时候将会带来极大的写放大效应。因此,数据库的底层 I/O 应当和 MP4/MKV 等影音类型有所区别。

图 1. 给不同容器设置不同的 ZFS dataset 或者 btrfs subvolume,并设置不同的 recordsize, 对于数据库,用较小的 recordsize,影音文件可以用更大的/巨大的 recordsize.
如果数据库的 recordsize 被设置的很大,如 128KiB,那么一次 8KiB 的读取,就会「拔出萝卜带出泥」,需要先从硬盘读取 128KiB 左右的数据,然后从里面取出 8KiB. 当然,如果你使用的是 EXT4 这种简单的文件系统就不需要考虑这个问题了,然而,EXT4 的问题更大。
笼统地说,容器的存储应该有以下特征:
- 容器使用独立的目录或者 ZFS dataset / BtrFS subvolume. 而且需要对数据库、影音库设置不同的参数,以优化存取带宽,降低延迟。
- 容器最好使用 CoW 文件系统,这样会更容易地进行快照操作。
最后讲一下,为什么一个容器的存储要使用一个独立的 ZFS dataset 或 BtrFS subvolume?
我们考虑一个场景,Jellyfin 在升级的过程中,可能需要对数据库做转换,而这是一个非常危险的操作。如果你使用 jellyfin/jellyfin:latest 镜像,而且打开了自动更新,那么下次 docker compose up -d 的时候,就会自动使用最新的镜像,然后触发意料之外的数据库大更新。一旦失败,数据库可能也无法回滚,结果只能是重建数据库,背后的辛酸可想而知。如果可以执行细粒度的快照,这个问题将会不复存在,出问题直接回滚即可。
网络隔离
容器使用的网络命名空间技术也比较复杂。
网络命名空间指的是一个独立的网络堆栈环境,它拥有自己的网卡、IP、路由、iptables,这样会形成比较干净、安全的独立网络环境。即使两个容器在同一台物理机,只要在不同的网络命名空间里,它们就像在不同网络里一样,互不可见。
Docker 的网络是构建在 Linux 网络命名空间之上的:每个容器都有一个 独立的 network namespace,Docker 网络负责把这些 namespace 连接起来。
[ Host namespace ]
|
| veth pair (虚拟网线)
|
+---------------+ +---------------------------+
| docker0 bridge| <-------> | veth0 (in container) |
+---------------+ | Network Namespace |
| eth0 + IP + Route + FW |
+---------------------------+
使用下列命令,可以创建一个 Docker 网络,并创建一个对应的 Linux 网络 interface.
docker network create --ipv6 --subnet 2001:0DB8::/112 reverse_proxy
并让 hexo 和 nginx 容器挂载到这个网络里:
services:
# hexo 博客应用,其实就是 web 服务器而已。
hexo:
image: littlenewton/hexo:latest
container_name: hexo
hostname: hexo
environment:
- HEXO_SERVER_PORT=4000
- GIT_USER=LittleNewton
- GIT_EMAIL=littlenewton6@gmail.com
- TZ=Asia/Shanghai
volumes:
- ${APPDATA_PATH}/Hexo/blog:/app
networks:
- reverse_proxy
restart: unless-stopped
# nginx 反向代理
nginxproxymanager:
image: "jc21/nginx-proxy-manager:latest"
container_name: "nginxproxymanager"
hostname: nginx-proxy-manager
restart: unless-stopped
privileged: true
networks:
- reverse_proxy
ports:
- "80:80"
- "81:81"
- "443:443"
volumes:
- ${APPDATA_PATH}/NginxProxyManager/data:/data
- ${APPDATA_PATH}/NginxProxyManager/letsencrypt:/etc/letsencrypt
# 网络定义:此前通过命令行已经定义过了。
networks:
reverse_proxy:
name: reverse_proxy
external: true
由于每次重启,容器都会被分配不同的 IP,甚至其网段也会小有变化。因此如何在容器里做反向代理就非常令人头疼。
- 写 IP 地址会变化,导致反向代理失效;
- 设置静态 IP,相对比较繁琐。
问题其实很简单:什么都不需要操作,在 Docker 的自定义网络(比如刚才创建的 reverse_proxy)里,容器之间可以通过容器名进行 DNS 解析。
比如,我们在 NginxProxyManager 里,可以通过 hexo 这个容器名字,找到 hexo 容器的 IP 地址。而且,我们不需要手动设置任何 DNS 解析!
[root@docker-nginx-proxy-manager:/app]# nslookup hexo
Server: 127.0.0.11
Address: 127.0.0.11#53
Non-authoritative answer:
Name: hexo
Address: 172.18.0.4
Name: hexo
Address: 2001:db8::4

图 2. 在 NginxProxyManager 里,直接使用容器名进行记录添加。
绑定 CPU 核心
考虑到目前很多用户的 CPU 只有 8 核心左右,而且要开十几个甚至几十个容器,这里就暂时不讲了。