本文完整阅读约需 24 分钟,如时间较长请考虑收藏后慢慢阅读~

近年来,几乎所有现代Linux发行版都将service替换成了systemd以实现更完善的服务管理,而随之变化的是相关用户策略的变化。可能不少读者都遇到过systemd服务提示打开文件数过多,但设置ulimit无效的问题,尤其是对于数据库这种并发数较大的业务。本文将为读者讲解如何正确地为systemd的服务配置ulimit限制。

0x01 问题重现

最近我负责了一台数据库服务器的操作系统升级,将其从CentOS6升级到CentOS7。这台数据库服务器的平均QPS高达1200,有着3000多张表,1.3TB的数据库文件,可就在我升级成功正式上线后的十分钟内,就出现了业务中断情况。

于是我查看日志,发现日志里频繁提示打开文件数过多:

众所周知,大部分Linux发行版中的默认最大打开文件数都是1024,可以使用ulimit -a查看:

对于一个有3000多张表(每个表都是MyISAM存储格式,意味着有至少3000×3=9000张表,因为MyISAM会将一张表分为frm MYD MYI三部分)的数据库来说,在高负载访问下很轻松就可以达到限制。

有一定Linux使用经验的读者应该知道如何修改最大文件打开数,即编辑/etc/security/limits.conf文件,设置对应用户或用户组的软/硬最大打开文件数。

我将最大文件数设置为20480后,重启服务器,执行ulimit -a,发现修改成功。

但当我打开MySQL,执行show global variables like '%open%';查看MySQL最大打开文件数状态时,数值却依旧是1024。

0x02 问题分析

实际上/etc/security/limits.confpam_limits.so的配置文件,可以通过man limits.conf看到

LIMITS.CONF(5)                 Linux-PAM Manual                 LIMITS.CONF(5)

NAME
       limits.conf - configuration file for the pam_limits module

DESCRIPTION
       The pam_limits.so module applies ulimit limits, nice priority
       and number of simultaneous login sessions limit to user login
       sessions. This description of the configuration file syntax
       applies to the /etc/security/limits.conf file and *.conf files
       in the /etc/security/limits.d directory.

PAM全称为插入式验证模块(Pluggable Authentication Module,PAM),主要目的是为Linux下不同依赖用户体系的应用程序提供统一身份认证和用户资料读写API。

通过man 8 pam可以看到关于PAM模块的描述如下:

PAM(8)                         Linux-PAM Manual                         PAM(8)

NAME
       PAM, pam - Pluggable Authentication Modules for Linux

DESCRIPTION
       This manual is intended to offer a quick introduction to Linux-PAM.
       For more information the reader is directed to the Linux-PAM system
       administrators´ guide.

       Linux-PAM is a system of libraries that handle the authentication
       tasks of applications (services) on the system. The library provides
       a stable general interface (Application Programming Interface - API)
       that privilege granting programs (such as login(1) and su(1)) defer
       to to perform standard authentication tasks.

描述中明确表示PAM既可以用于应用程序鉴权,也可以用于服务鉴权。这里的服务指的是以init进程为根进程的,被称作SysV的机制,也就是各发行版在使用systemd之前广泛使用的服务机制。

那么问题来了:对于systemd,到底是否依旧沿用PAM模块实现资源限制呢?我在RedHat Bugzilla找到了Systemd最初被引入时的一篇Ticket:Bug 754285 – Hint that /etc/security/limits.conf does not apply to systemd services。帖子中提到了一模一样的问题。Systemd的作者之一Kay Sievers当时给与了以下回复:

Systemd does not support global limits, the file is intentionally ignored.
LimitNOFILE= in the service file can be set to specify the number of open
file descriptors for a specific service.

也就是说,Systemd设计的时候故意忽略了全局限制,转而在配置文件中配置对每个服务的资源限制,结合/etc/security/limits.conf文件开头的注释来看,果然如此:

# /etc/security/limits.conf
#
#This file sets the resource limits for the users logged in via PAM.
#It does not affect resource limits of the system services.
...

既然了解了Systemd不会遵循PAM模块的配置,那么接下来要做的就是思考如何在Systemd的配置文件中设置资源限制。

0x03 问题解决

要想知道Systemd配置资源限制的方法,还得求助于man。这里我在命令行输入man systemd.exec,看到了以下信息:

LimitCPU=, LimitFSIZE=, LimitDATA=, LimitSTACK=, LimitCORE=, LimitRSS=, LimitNOFILE=, LimitAS=,
LimitNPROC=, LimitMEMLOCK=, LimitLOCKS=, LimitSIGPENDING=, LimitMSGQUEUE=, LimitNICE=,
LimitRTPRIO=, LimitRTTIME=
    These settings set both soft and hard limits of various resources for executed processes. See
setrlimit(2) for details. The resource limit is possible to specify in two formats, value to
set soft and hard limits to the same value, or soft:hard to set both limits individually
(e.g. LimitAS=4G:16G). Use the string infinity to configure no limit on a specific resource.
    The multiplicative suffixes K (=1024), M (=1024*1024) and so on for G, T, P and E may be used
for resource limits measured in bytes (e.g. LimitAS=16G). For the limits referring to time
values, the usual time units ms, s, min, h and so on may be used (see systemd.time(7) for
    details). Note that if no time unit is specified for LimitCPU= the default unit of seconds is
implied, while for LimitRTTIME= the default unit of microseconds is implied. Also, note that
the effective granularity of the limits might influence their enforcement. For example, time
limits specified for LimitCPU= will be rounded up implicitly to multiples of 1s. For
LimitNICE= the value may be specified in two syntaxes: if prefixed with "+" or "-", the value
is understood as regular Linux nice value in the range -20..19. If not prefixed like this the
value is understood as raw resource limit parameter in the range 0..40 (with 0 being
equivalent to 1).

Note that most process resource limits configured with these options are per-process, and
processes may fork in order to acquire a new set of resources that are accounted
independently of the original process, and may thus escape limits set. Also note that
LimitRSS= is not implemented on Linux, and setting it has no effect. Often it is advisable to
prefer the resource controls listed in systemd.resource-control(5) over these per-process
limits, as they apply to services as a whole, may be altered dynamically at runtime, and are
generally more expressive. For example, MemoryLimit= is a more powerful (and working)
replacement for LimitRSS=.

关于这一段的讲解非常详细且复杂,但我们只要知道以下映射关系即可:

Table 1. Limit directives and their equivalent with ulimit
┌─────────────────┬───────────────────┬────────────────────────────┐
│Directive        │ ulimit equivalent │ Unit                       │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitCPU=        │ ulimit -t         │ Seconds                    │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitFSIZE=      │ ulimit -f         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitDATA=       │ ulimit -d         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitSTACK=      │ ulimit -s         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitCORE=       │ ulimit -c         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitRSS=        │ ulimit -m         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitNOFILE=     │ ulimit -n         │ Number of File Descriptors │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitAS=         │ ulimit -v         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitNPROC=      │ ulimit -u         │ Number of Processes        │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitMEMLOCK=    │ ulimit -l         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitLOCKS=      │ ulimit -x         │ Number of Locks            │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitSIGPENDING= │ ulimit -i         │ Number of Queued Signals   │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitMSGQUEUE=   │ ulimit -q         │ Bytes                      │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitNICE=       │ ulimit -e         │ Nice Level                 │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitRTPRIO=     │ ulimit -r         │ Realtime Priority          │
├─────────────────┼───────────────────┼────────────────────────────┤
│LimitRTTIME=     │ No equivalent     │ Microseconds               │
└─────────────────┴───────────────────┴────────────────────────────┘

从表格中可以看到,这里我们需要修改的是最大打开文件数,也就是LimitNOFILE

直接编辑/usr/lib/systemd/system/mysqld.service,在Service段下面附上LimitNOFILE=20480,然后执行systemctl daemon-reload && systemctl restart mysqld.service,重启后再查看MySQL的相关配置:

此时设置生效。

0x04 一些感想

我觉得能读到这里的读者一定非常有耐心,因为写作前我在搜索引擎中查询了一下,发现大部分网站都只写了如何解决,对于问题分析的过程只字未提,但我写到这里的时候,字数统计器已经逼近7000字了。

其实这也不是一个多么复杂的问题,我完全可以在文章的最开头就告诉大家:直接打开/usr/lib/systemd/system/mysqld.service,在Service下面附上LimitNOFILE=20480然后重启服务即可生效。

但我觉得如果只是这样,读者们下次遇到其他的问题(例如配置进程限制、文件大小限制等)依旧不知道如何下手。本着授人以渔的想法,我觉得还是应该负责的把发现问题——分析问题——解决问题的思路告知读者。这样如果下次遇到相同的问题,大家就知道如何自行解决,而非依赖搜索引擎中繁杂的N手消息或是求助于他人。

以下是可以帮助大家迅速解决同类问题的三个重要思路:

1. 阅读文档

阅读文档是解决问题最直接的途径。实际上在我当时解决这一问题的时候,最开始使用的就是Linux内建文档。当然我不是超人,不可能一下子就知道所需内容在手册的哪个位置,因此我使用到了die.net提供的在线Linux Manual服务。这一网站嵌入了谷歌自定义搜索,因此可以使用各种各样的筛选符号迅速找到自己所需的文档内容。

如果对于没有自建Manual或是极为复杂的的软件,例如MySQL,我们还可以在官方网站找到其文档。MySQL的文档极为详细,几乎涵盖了所有会用到的操作及其背后原理,无论是初学者希望夯实基础,还是遇到问题寻求解决方法,都值得一看。

2. 在官方网站寻求支援

我刚刚提到了RedHat官方的Bugzilla,其实几乎每个开源/自由软件都有自己的社区。对于一款成熟的软件来说我们所遇到的大部分问题都不是新问题,因此当我们有疑惑时,可以直接在这些社区的网站进行搜索。

对于Linux,可以在各个发行版的论坛,例如Redhat – Bugzilla或者是Ubuntu – Bugs进行搜寻;对于GitHub上的开源软件,可以直接在Issues中搜索或提出疑问。

有一点非常重要:除非是完全搜索不到所需内容,否则最好不要求助于搜索引擎、问答网站或博客站点如CSDN、博客园等网站。因为在官方社区,与你沟通的可能是相关专家,更有可能是作者。试问谁能比作者更了解自己的作品呢?

3. 不要排斥英文

这一点同样非常重要,倒不是因为崇洋媚外,也和『计算机是外国人开发的』无关。不要排斥英文真正所指的是不要只看中文内容,中文社区固然庞大,但的确有很多一手资料是使用英文写作而成,无论是翻译版手册还是简易版教程可能都不如原文来得生动直接,很多作者的本意只有结合写作时的语境才能理解,这一点在计算机相关教材方面的体现同样非常明显。

很多读者可能会担心自己的英文水平能否正常阅读英文文档,这一点其实完全不用担心。英文文档在撰写的时候通常会最大限度考虑其他语种读者阅读的难度,选取的词汇几乎都是最简明的,严谨范畴内最贴近口语化的,这和莎士比亚的文学作品不一样。

如果发现自己阅读英文文档有难度,最大可能是因为不善于文档阅读,而非英语水平问题,阅读能力无关乎语言,只要多练习就会提升。


总而言之,授人以鱼不如授人以渔,希望这三条建议能帮助读者在遇到问题时更快更准确地找到问题的解答: )