介绍
多租户/共同托管总是具有挑战性的。运行多个 PostgreSQL 实例,有助于减少 PostgreSQL 中的内部争用点(扩展性问题)。但是,其中一个租户产生的负载可能会影响其他租户,这通常被称为 “近邻干扰” 效应。幸运的是,Linux 允许用户使用 cgroups(控制组),控制每个程序消耗的资源。cgroup2 是 cgroup 版本 1 的替代品,几乎解决了版本 1 架构上的所有限制。
如果 Linux 内核版本为 5.2.0 或更高版本,我们应该能够可靠地使用 cgroup2。更实际地说,如果我们运行的是 2022 年或之后版本的 Linux 发行版,您的主机很可能已经为 cgroup2 提供了支持。
检查 Linux 使用的是 cgroup 版本 1 还是 2 的一种简单方法是,使用 cgroup 检查挂载数量。
$ grep -c cgroup /proc/mounts
1
如果计数为 1,则我们使用的是 cgroup2。由于 cgroup2 具有统一的单层次结构,因此如果使用的是 cgroup 版本 1,我们可能会看到多个挂载。
如果内核版本是新的,但使用的仍然是 cgroup 版本 1,您可能必须使用引导参数:“systemd.unified_cgroup_hierarchy=1”。在 Redhat/OEL 系统上,我们可以通过执行以下操作,来添加该参数:
sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
基本上,它会将该参数作为引导加载器选项,添加到内核参数中,例如:
$ cat /etc/default/grub
...
GRUB_CMDLINE_LINUX="xxxxxx systemd.unified_cgroup_hierarchy=1"
...
此更改需要重新启动机器。
重新启动后,您可以这样验证:
$ sudo mount -l | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate)
请确保其中显示的是 “cgroup2”。
现在我们来检查这个虚拟文件系统,以更好地理解。
$ ls -l /sys/fs/cgroup/
total 0
-r--r--r--. 1 root root 0 May 27 02:10 cgroup.controllers
-rw-r--r--. 1 root root 0 May 27 02:10 cgroup.max.depth
-rw-r--r--. 1 root root 0 May 27 02:10 cgroup.max.descendants
-rw-r--r--. 1 root root 0 May 27 02:10 cgroup.procs
-r--r--r--. 1 root root 0 May 27 02:10 cgroup.stat
-rw-r--r--. 1 root root 0 May 27 02:10 cgroup.subtree_control
-rw-r--r--. 1 root root 0 May 27 02:10 cgroup.threads
-rw-r--r--. 1 root root 0 May 27 02:10 cpu.pressure
-r--r--r--. 1 root root 0 May 27 02:10 cpuset.cpus.effective
-r--r--r--. 1 root root 0 May 27 02:10 cpuset.mems.effective
-r--r--r--. 1 root root 0 May 27 02:10 cpu.stat
drwxr-xr-x. 2 root root 0 May 27 02:10 init.scope
-rw-r--r--. 1 root root 0 May 27 02:10 io.pressure
-r--r--r--. 1 root root 0 May 27 02:10 io.stat
drwxr-xr-x. 2 root root 0 May 27 02:10 machine.slice
-r--r--r--. 1 root root 0 May 27 02:10 memory.numa_stat
-rw-r--r--. 1 root root 0 May 27 02:10 memory.pressure
-r--r--r--. 1 root root 0 May 27 02:10 memory.stat
-r--r--r--. 1 root root 0 May 27 02:10 misc.capacity
drwxr-xr-x. 107 root root 0 May 27 02:10 system.slice
drwxr-xr-x. 3 root root 0 May 27 02:16 user.slice
这是根控制组。所有切片都来自于这里。我们可以看到 “system.slice” 和 “user.slice”,它们显示为目录,因为它们是下一个级别的。
我们可以检查机器上可用的 cgroup 控制器有哪些,如下所示:
$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc
实践使用 cgroup2
创建切片
当有多个实例时,为 PostgreSQL 实例创建单独的切片是一个好主意。这将使我们能够从更高的层次控制资源的整体消耗。假设我们想限制所有 PostgreSQL 服务不能使用超过 25% 的机器 CPU。第一步是创建一个切片:
sudo systemctl edit --force postgres.slice
为了进行演示,我添加了以下单元配置:
[Unit]
Description=PostgreSQL Slice
Before=slices.target
[Slice]
MemoryAccounting=true
MemoryLimit=2048M
CPUAccounting=true
CPUQuota=25%
TasksMax=4096
保存并退出编辑器,然后重新加载。
sudo systemctl daemon-reload
任何时候我们需要检查切片的状态,可以执行sudo systemctl status postgres.slice。
更改 PostgreSQL 服务
我们可以使用在 PostgreSQL 服务中创建的切片。为此,我们需要编辑服务单元:
$ sudo systemctl edit --full postgresql-16
在单元文件的 [Service] 部分下,添加有关切片的定义,例如 Slice=postgres.slice。
...
[Service]
Type=notify
User=postgres
Group=postgres
Slice=postgres.slice
...
保存并退出编辑器。此更改需要重新启动 PostgreSQL 服务。
重新启动 PostgreSQL 服务时,PostgreSQL 将开始在新的切片下运行。
$ systemd-cgls | grep post
├─postgres.slice
│ └─postgresql-16.service
│ ├─3760 /usr/pgsql-16/bin/postgres -D /var/lib/pgsql/16/data/
│ ├─3761 postgres: logger
│ ├─3762 postgres: checkpointer
│ ├─3763 postgres: background writer
│ ├─3765 postgres: walwriter
│ ├─3766 postgres: autovacuum launcher
│ └─3767 postgres: logical replication launcher
│ └─3770 grep --color=auto post
同样的内容也会显示在服务状态的输出中。
$ sudo systemctl status postgresql-16
postgresql-16.service - PostgreSQL 16 database server
Loaded: loaded (/etc/systend/systen/postgresql-16.service; enabled; vendor preset: disabled)
Active: since Mon 2024-05-27 12:54:26 EDT; 7s ago
Docs: https://www.postaresql.org/docs/16/static/
Process: 5957 ExecStartPre=/usr/pgsql-16/bin/postgresql-16-check-db-dir ${PGDATA} (code=exited, status=0/SUCCESS)
Main PID: 5962 (postgres)
Tasks: 7 (limit: 29176)
Memory: 18.1M
CGroup: /postgres.slice/postgresql-16.service
├─5962 /usr/pgsql-16/bin/postgres -D /var/lib/pgsql/16/data/
├─5963 postgres: logger
├─5964 postgres: checkpointer
├─5965 postgres: background writer
├─5967 postgres: walwriter
├─5968 postgres: autovacuum launcher
└─5969 postgres: logical replication launcher
May 27 12:54:25 localhost.localdomain systemd[1]: postgresql-16.service: Succeeded.
May 27 12:54:25 localhost.localdomain systend[1]: Stopped PostgreSQL 16 database server.
May 27 12:54:25 localhost.localdomain systend[1]: Starting PostgreSQL 16 database server...
May 27 12:54:26 localhost.localdomain postgres[5962]: 2024-05-27 12:54:26.153 EDT [5962] LOG: redirecting log output to logging collector process
May 27 12:54:26 localhost.localdomain postgres[5962]: 2024-05-27 12:54:26.153 EDT [5962] HINT: Future log output will appear in directory "log"
May 27 12:54:26 localhost.localdomain systend[1]: Started PostgreSQL 16 database server.
验证
通过在单个 CPU 的机器上,尝试并行运行具有多个会话的基准测试套,来在系统上创建大压力的负载。无论尝试什么样的业务模型,Linux 都会限制 PostgreSQL 超过切片指定的限制。
图片
如果我们将 PostgreSQL 所有进程的所有 CPU 利用率相加,我们会看到 2.34+27+1.7 = 24.9!(为了更容易计数,这里使用了单 CPU 核心的机器。)
没有任何 cgroup 限制的相同业务负载,可以使服务器达到 100% 的利用率(0% 空闲)。
图片
*cgroup 切片的限制降低了吞吐量,这是意料之中的。
我们可以在一个切片中拥有多个服务,这将是在层次结构中的下一个级别。systemd-cgtop 可以向我们展示切片和各个服务的资源利用率。
图片
看上去很棒,不是吗?快速演示到此结束。
服务级别的控制
cgroup2 用途广泛,并且还存在更多的选项。例如,您可能不希望为 PostgreSQL 服务创建单独的切片,如演示中所示,尤其是当主机上只有一个 PostgreSQL 实例时。默认情况下,PostgreSQL 和所有服务都将是 “system.slice” 的一部分。在这种情况下,简单的方法是在服务级别而不是切片级别指定 cgroup 限制。
例如:
sudo systemctl edit --full postgresql-16
并在 [Service] 部分下的服务单元中直接指定资源控制的配置。
...
[Service]
User=postgres
Group=postgres
CPUAccounting=true
CPUQuota=25%
...
* 更改将在下次重新启动时生效。
总结
如今,Docker 和 Kubernetes 等其他程序都在广泛且默默地使用控制组。它们是限制机器资源消耗的行之有效的方法之一。新的 cgroup2 使它们的使用变得更加简单。
对主机上的资源使用率进行明确控制,开辟了许多可能性。能想到的一些有:
- 1. 更好的多租户环境。我们可以通过防止租户竞争同一组资源,来防止多租户环境中的 “近邻干扰” 效应。
- 2. 在同一台计算机上一起托管应用服务器和数据库服务器。绝大多数应用程序是 CPU 密集型的,而数据库服务器仍然是内存和 I/O 密集型的。因此,在某些情况下,将它们放在同一台机器上是有意义的,尤其是对于小型和简单的应用程序。一起托管应用程序和数据库的一大优势是,它们可以通过本地套接字而不是 TCP/IP 进行通信。实际上,我们看到许多情况下,网络在默默地拖慢系统性能。如何衡量网络对 PostgreSQL 性能的影响。另一个优点是,我们不必将数据库服务(端口)开放给网络环境。
- 3. 保护系统免受滥用、服务拒绝攻击,尤其是不必要的故障转移。当系统过载时,它可能会使得机器上运行的所有程序(而不仅仅是数据库)无响应。这种情况通常会导致高可用部件进行不必要的故障转移。对资源使用率进行良好控制可以防止这种情况发生。