Linux | systemd - 在SystemD中限制进程的资源消耗和行为
发现git stash里面这篇没写完也没发,今天补上。也算是对加强系统安全的一个补充吧。
前两天有几次发现我的git服务和登录什么的都挂了,重启以后又会挂掉。最后发现php是php内存占了太多导致oom。加上了内存限制,遂解决问题。
故事背景
我因为有些代码还有文档不想放到别人那里,加上图片啊视频啊什么的数量爆炸耗干了iCloud,所以先后搭了Gitea啊NextCloud啊这些东西,为了统一登录又加了Keycloak。参数基本上都是默认的,就是逻辑改了一些,存储后端改成了S3放到了七牛上。
前两天我把我的照片全部放到了NextCloud上,腾空了手机存储。很爽。出问题就是第二天,代码推不上去了。上去一看,好家伙,内核OOM,杀了一大堆进程祭天,从Keycloak到MySQL全部都挂了。
看了一眼请求,似乎是家里人看我分享图片的时候,NextCloud每个图片预览都会发起一个请求,对于PHP-FPM来说,会产生多个Worker处理请求,每个请求又会占用不少内存,于是,Boooooom。
怎么办呢?改NextCloud显然是不现实的,插件也没有合适的。合适的方法只有在php-fpm的pm配置里面限制worker数量,以及 SystemD 里面设置MemoryLimit。
今天主要要提的就是 SystemD 的资源限制。
资源
systemd管理了很多资源,我们比较常见的就是进程一类东西。要存储一段程序到磁盘要占用空间,程序要存储数据也要用到磁盘空间,加载程序、读写数据要用到磁盘带宽和读写,文件会占用inodes和 路径,数据加载要占用内存空间,和网络上其他程序通信要占用网络带宽,网络、打开的文件句柄都要消耗fd。这些都是资源消耗。
Linux中针对大部分的资源消耗,都有办法来限制进程的行为,比较常见的包括cgroup、namespace,也有些就是限制权限的比如说DAC、MAC(AppArmor、Selinux)、Capabilities。
根据大致的分类来介绍一下什么东西可以做什么。
权限
权限就是说进程可以做什么不可以做什么。Unix的权限很多都是默认你不可以,而且只要有一个不可以就不行,所以一个操作需要考虑每类权限。
DAC
Wikipedia:
restricting access to objects based on the identity of subjects and/or groups to which they belong
根据Wikipedia,DAC就是根据用户和用户组来确认可不可以做什么。具体来说比较常见的问题就是:我有个文件不能访问了。这个时候看看文件的所有者。除非你是root,或者你是文件所有者的用户,或者在所有者群组中而且群组有对应的权限,否则对不上就访问不了。
其他的方面有一些特殊的操作,比如说Unix Domain Socket可以获取到peer的用户,通过这个进行认证。我觉得也算是DAC吧。
Capabilities
除了用户以外,还有一些权限可以通过Capability管理。
比如说我们要在没有root用户身份的情况下绑定一个低于1000的端口,正常情况是绑不上的,有人就会和你说,切root啊。但是切root不是最好的做法,我们可以直接给这个进程NET_ADMIN的capability,这样进程就会获得网络上的各种特权,包括绑定low ports。
MAC
MAC简单说就是类似于一个规则引擎。
举个例子,我比较熟悉SELinux。SELinux中我们可以定义什么文件有什么上下文,启动以后可以从什么类型转移到什么类型,什么类型的进程可以做什么事情,什么类型的进程可以访问什么资源。通过这些规则来规范进程的行为,即使进程被其他人通过0day执行了恶意代码,也不会扩散影响。
隔离
除了限制权限以外,我们也可以让一个进程不知道具体有什么,从而让它无从下手。
比较传统的手段就是虚拟化,比如说Linux的KVM、Windows的Hyper-V。这些手段都要模拟一个完整的机器的大部分组件,为了加速也有一部分是交给物理机的硬件来处理的。一般来说虚拟化也要占用固定的内存,要占用不少的磁盘和CPU,所以也有一些Memory Ballooning的手段来减少内存消耗。但是即使这样,虚拟机还是不能满足一些需求,所以后来就有了容器。
容器指的是共用一部分资源的情况下,隔离出来一块特定的空间,看起来像是虚拟机但是实际上只是部分的隔离开了,比如说内存、内核之类的都是共用的,只是进程看不到彼此、被限制了手脚。容器的背后并不需要虚拟化的支持,因为只是内核把资源隔离开了而已,但是如果需要的话、也可以通过虚拟化加强容器的安全性。在Linux中,主要支持了容器的特性是Namespace,也用到了CGroup和一些其他手段来限制资源消耗。
除此之外,还有些Unix通用的限制资源的手段。比如说chroot。
资源限制
Nice进程优先级
Linux中可以调整进程的优先级,从而对CPU的使用做出限制,当两个进程都需要CPU资源时,在不饿死低优先级的进程的同时,优先让高优先级的进程使用CPU。
除了Nice以外,也有其他的一些优先级,比如说ionice,就是限制IO带宽占用的。默认情况下IO优先级通过公式从CPU优先级计算出来,IO优先级 = ( CPU优先级 + 20 ) / 5,也可以用ionice设置一个值。
CGroups
刚才提到容器也用到了CGroups来限制资源消耗。
Linux里面很多时候会用到CGroups来做资源限制。通常是一次对一组进程进行限制,比如说刚才说的CPU啊IO带宽啊什么的,还可以限制比如说使用具体哪一个CPU核心、用多少内存、更具体的磁盘的优先级、网络的优先级、使用多少PID、使用多少大页。
除此之外,CGroups还有些额外的功能:CPU审计,可以统计进程占用了多少CPU时间;Net Classifier,可以给进程分组,给分组内的进程设置nftables规则、路由、tc或者网络上的各种配置;进程暂停,可以暂停或者恢复一组进程的执行。
Systemd中的应用
systemd对于执行进程一类的Unit,比如说service,可以限制非常多种类的资源,原理就是上面说的这些手段。
写一个例子来说明这些吧。
假设我写了一个服务,要用systemd管理。那么我就需要一个Unit定义,刨除无关的部分,核心大概是这样:
[Service]
ExecStart=/usr/local/bin/my-service
因为自己写的服务有Bug是必然的,所以一般会选择用普通用户来运行程序:
[Service]
; ExecStart=/usr/local/bin/my-service
User=guochao
Group=guochao
但是普通用户无法绑定我需要的端口,我就需要NET_ADMIN的Cap
[Service]
; ExecStart=/usr/local/bin/my-service
; User=guochao
; Group=guochao
AmbientCapabilities=CAP_NET_ADMIN
这个进程有可能会造成比较高的负载,同时可能会有不少内存消耗,所以我们限制下CPU和内存占用。
[Service]
; ExecStart=/usr/local/bin/my-service
; User=guochao
; Group=guochao
; AmbientCapabilities=CAP_NET_ADMIN
CPUQuota=85%
CPUWeight=10
MemoryHigh=300M
MemoryMax=400M
这个服务占用的CPU时间不会超过85,而且当它和其他进程都需要运算时,其他进程和它的权重比是默认(100):10。
除了消耗资源以外,我们的程序可能会执行其他人的恶意代码,所以我们需要为进程设置指定的上下文环境。
需要注意的是,这里设置上下文的类型,只是替代了自动类型转移,我们还是需要写SELinux的策略,并且在策略中还是需要允许这项转移的,systemd无法突破SELinux做SELinux不允许的事情。相对的,如果设置了init_t到myservice_t的自动转移,效果和在这里设置SELinux上下文是一样的。
[Service]
; ExecStart=/usr/local/bin/my-service
; User=guochao
; Group=guochao
; AmbientCapabilities=CAP_NET_ADMIN
; CPUQuota=85%
; CPUWeight=10
; MemoryHigh=300M
; MemoryMax=400M
SELinuxContext=system_u:system_r:mysrv_t:s0
除了SELinux和CGroups限制以外,systemd也可以用namespace限制进程。比如说我希望我的服务有个 独卫 独立的tmp,那么我可以设置。
[Service]
; ExecStart=/usr/local/bin/my-service
; User=guochao
; Group=guochao
; AmbientCapabilities=CAP_NET_ADMIN
; CPUQuota=85%
; CPUWeight=10
; MemoryHigh=300M
; MemoryMax=400M
; SELinuxContext=system_u:system_r:mysrv_t:s0
PrivateTmp=true
类似的还有ProtectHome和ProtectSystem,通过创建mount namespace,重新挂载了文件系统,设置了挂载参数,从而让服务只访问到挂载到的这些目录。