Kubernetes、Nomad 或任何云托管平台即服务 (Paas) 等平台提供了各种强大的功能。从扩展工作负载到机密管理再到部署策略,这些工作负载编排器经过优化,可帮助以不同方式扩展基础设施。
但运营商是否总是需要为最大的可扩展性付出成本?有时,复杂性和抽象性的成本会抵消它们的好处。相反,许多构建者开始依赖极其简单的部署架构来简化管理。与跨容器主机群的庞大微服务器集群相比,负载均衡器后面的两台虚拟专用服务器的堆栈管理起来要简单得多。当出现问题时需要调试的移动部件较少或需要维护时需要升级时,这可以开始带来好处。
许多现代 Linux 发行版的基础是systemd ,它具有一组强大的功能,通常可与容器编排器或 PaaS 系统相媲美。在本文中,我们将探讨如何利用最新的 systemd 功能来获得其他大型系统的许多功能,而无需担心管理问题,并将任何普通Linux服务器增强为功能强大的应用程序平台。
在单个主机上,编写 systemd .service
文件是运行托管进程的理想方法。大多数时候,您甚至根本不需要更改应用程序:systemd 支持各种不同类型的服务,并且可以进行相应的调整。
例如,考虑这个简单的.service
,它定义了如何运行简单的 Web 服务:
[Unit] Description=a simple web service [Service] ExecStart=/usr/bin/python3 -m http.server 8080
请记住 systemd 服务的默认设置: ExecStart=
必须是绝对路径,进程不应分叉到后台,并且您可能需要使用Environment=
选项设置必要的环境变量。
当放入/etc/systemd/system/webapp.service
这样的文件中时,这将创建一个可以使用systemctl
控制的服务:
systemctl start webapp
将启动该进程。systemctl status webapp
将显示服务是否正在运行、其正常运行时间、 stderr
和stdout
的输出,以及进程的 ID 和其他信息。systemctl stop webapp
将结束服务。
此外,打印到stderr
和stdout
的所有输出都将由journald聚合,并通过系统日志(使用journalctl
)访问或使用--unit
标志专门定位:
journalctl --unit webapp
由于journald默认情况下会轮换和管理其存储,因此通过journal收集日志是管理日志存储的一个很好的策略。
本文的其余部分将探讨增强此类服务的选项。
像 Kubernetes 这样的容器编排器支持安全注入Secrets 的能力:从安全数据存储中提取并暴露给正在运行的工作负载的值。 API 密钥或密码等敏感数据需要与环境变量或配置文件不同的处理方式,以避免意外泄露。
LoadCredential=
systemd 选项支持从磁盘上的文件加载敏感值,并以安全的方式将它们公开给正在运行的服务。与远程管理机密的托管平台一样,systemd 将以不同于环境变量等值的方式对待凭证,以确保它们的安全。
要将机密注入到 systemd 服务中,首先将包含机密值的文件放入文件系统上的路径中。例如,要将 API 密钥公开给.service
单元,请在/etc/credstore/api-key
中创建一个文件以在重新启动后保留该文件,或在/run/credstore/api-key
以避免永久保留该文件(路径可以是任意的,但 systemd 会将这些credstore
路径视为默认值)。无论哪种情况,该文件都应使用chmod 400 /etc/credstore/api-key
等命令具有受限权限。
在.service
文件的[Service]
部分下,定义LoadCredential=
选项并向其传递两个用冒号 ( :
分隔的值:凭证的名称及其路径。例如,要调用/etc/credstore/api-key
文件“token”,请定义以下 systemd 服务选项:
LoadCredential=token:/etc/credstore/api-key
当 systemd 启动您的服务时,密钥将在${CREDENTIALS_DIRECTORY}/token
形式的路径下公开给正在运行的服务,其中${CREDENTIALS_DIRECTORY}
是由 systemd 填充的环境变量。您的应用程序代码应读取以这种方式定义的每个机密,以便在需要 API 令牌或密码等安全值的库或代码中使用。例如,在 Python 中,您可以使用如下代码读取此机密:
from os import environ from pathlib import Path credentials_dir = Path(environ["CREDENTIALS_DIRECTORY"]) with Path(credentials_dir / "token").open() as f: secret = f.read().strip()
然后,您可以将secret
变量与您的 Secret 内容一起用于任何可能需要 API 令牌或密码的库。
Nomad等编排器的另一个功能是能够自动重新启动已崩溃的工作负载。无论是由于未处理的应用程序错误还是其他原因,重新启动失败的应用程序都是一项非常有用的功能,在设计具有弹性的应用程序时,它通常是第一道防线。
Restart=
systemd选项控制 systemd 是否自动重新启动正在运行的进程。此选项有多个潜在值,但对于基本服务, on-failure
设置非常适合满足大多数用例。
配置自动重启时要考虑的另一个设置是RestartSec RestartSec=
选项,它指示 systemd 在再次启动服务之前将等待多长时间。通常,应自定义此值,以避免在紧密循环中重新启动失败的服务,并可能在重新启动进程时消耗过多的 CPU 时间。一个不会等待太久的短值(如5s
通常就足够了。
RestartSec=
等接受持续时间或基于时间的值的选项可以根据您的需要解析各种格式,例如5min 10s
或1hour
。请参阅systemd.time 手册以获取更多信息。
最后,另外两个选项决定了 systemd 在最终放弃之前将如何积极地尝试重新启动失败的单元。 StartLimitIntervalSec=
和StartLimitBurst=
将控制在给定时间段内允许单元启动的频率。例如,以下设置:
StartLimitBurst=5 StartLimitIntervalSec=10
它只允许设备在 10 秒内最多尝试启动 5 次。如果配置的服务在 10 秒内尝试第六次启动,systemd 将停止尝试重新启动设备并将其标记为failed
。
结合所有这些设置,您可以为您的.service
单元添加以下选项来配置自动重新启动:
[Unit] StartLimitBurst=5 StartLimitIntervalSec=10 [Service] Restart=on-failure RestartSec=1
如果服务失败,此配置将重新启动服务 - 也就是说,它意外退出,例如使用非零退出代码 - 等待一秒钟后,如果服务在整个过程中尝试启动超过五次,则将停止尝试重新启动服务10 秒。
在容器内运行的主要好处之一是安全沙箱。通过将应用程序进程与底层操作系统分开,服务中可能存在的任何漏洞都更难以升级为全面的危害。像 Docker 这样的运行时通过 cgroup 和其他安全原语的组合来实现这一点。
您可以启用多个 systemd 选项来强制实施类似的限制,从而帮助保护底层主机免受不可预测的工作负载行为的影响:
ProtectSystem=
可以限制对敏感系统路径(如/boot
和/usr
的写访问。此选项的文档枚举了所有可用选项,但一般来说,将此选项设置为full
是保护这些文件系统路径的合理默认值。ProtectHome=
可以使用只读设置将/home
、 /root
和/run/user
目录设置为read-only
,或者当设置为true
时,将它们作为空目录挂载到服务的文件系统中。除非您的应用程序有访问这些目录的特定需要,否则将其设置为true
可以安全地强化系统,防止对这些目录的非法访问。PrivateTmp=
为配置的服务维护单独的/tmp
和/var/tmp
,以便该服务和其他进程的临时文件保持私有。除非有令人信服的理由让进程通过临时文件共享信息,否则这是一个有用的选项。NoNewPrivileges=
是另一种安全且直接的强化服务的方法,通过确保执行的进程无法提升其权限。如果您不确定是否能够使用其他强化选项,这通常是启用时问题最少的选项之一。
systemd.exec 的手册页是一个有用的资源,可用于探索适用于服务等可执行工作负载的不同选项。
systemd 项目的手册页内容丰富,对于了解可用于运行您自己的应用程序的所有选项非常有用。无论您是运行 Web 服务器之类的持久服务还是定期.timer
单元来替换 cron 作业,systemd 文档都可以提供有用的指导。