Skip to content

Latest commit

 

History

History
846 lines (544 loc) · 37.4 KB

File metadata and controls

846 lines (544 loc) · 37.4 KB

十、理解 Linux 用户和内核限制

在前一章中,我们使用了lsofstrace等工具来确定应用问题的根本原因。

在本章中,我们将再次确定应用相关问题的根本原因。 然而,我们还将重点学习和理解 Linux 用户和内核的限制。

已报告的问题

就像前一章的所关注的自定义应用的问题一样,今天的问题也来自相同的自定义应用。

今天,我们将处理一个应用支持团队报告的问题。 然而,这一次支持团队能够为我们提供相当多的信息。

我们在第 9 章、使用系统工具对应用进行故障排除中所处理的应用,现在通过port 25接收消息并将其存储在队列目录中。 一个作业周期性地运行以处理这些排队的消息,但是作业似乎不再工作

应用支持团队注意到队列中积压了相当多的消息。 然而,尽管他们已经尽可能地解决了问题,他们还是卡住了,需要我们的帮助。

为什么这项工作失败了?

由于报告的问题是一个预定的工作不工作,我们应该首先关注工作本身。 在这个场景中,我们的应用支持团队可以回答任何问题。 所以,让我们了解更多关于这项工作的细节。

背景问题

以下是问题的快速列表,应该可以帮助您提供额外的信息:

  • 作业是如何运行的?
  • 如果需要,我们可以手动运行该工作吗?
  • 这个作业执行什么?

这三个问题可能看起来很基本,但它们很重要。 让我们先来看看应用团队提供的答案:

  • How is the job run?

    作业以 cron 作业的形式执行。

  • Can we run the job manually if we need to?

    是的,可以根据需要经常手动执行作业。

  • What does this job execute?

    作业作为流浪用户执行/opt/myapp/bin/processor 命令

前面的三个问题非常重要,因为它们将为我们节省大量的故障排除时间。 第一个问题关注的是如何执行作业。 由于报告的问题是作业不工作,我们还不知道问题是由于作业没有运行,还是作业正在执行但由于某种原因失败。

第一个问题的答案告诉我们作业是由crond执行的,它是运行在 Linux 上的cron 守护进程。 这很有用,因为我们可以使用该信息来确定作业是否正在执行。 通常,有许多方法用于执行调度作业。 有时,执行计划作业的软件运行在不同的系统上,有时它运行在相同的本地系统上。

在这种情况下,作业正在同一台服务器上由crond执行。

第二个问题也很重要。 就像在上一章中我们必须手动启动应用一样,对于报告的问题,我们可能也需要执行这个故障排除步骤。 根据答案,似乎我们可以根据需要任意多次执行该命令。

第三个问题很有用,因为它不仅告诉我们正在执行什么命令,而且还告诉我们要寻找哪个作业。 Cron 作业是一种非常常见的任务调度方法。 对于一个系统来说,调度许多 cron 作业是很常见的。

cron 作业正在运行吗?

因为我们知道作业正在由crond执行,所以我们应该首先检查作业是否正在执行。 为此,我们可以检查相关服务器上的 cron 日志。 例如,考虑以下日志:

# ls -la /var/log/cron*
-rw-r--r--. 1 root root 30792 Jun 10 18:05 /var/log/cron
-rw-r--r--. 1 root root 28261 May 18 03:41 /var/log/cron-20150518
-rw-r--r--. 1 root root  6152 May 24 21:12 /var/log/cron-20150524
-rw-r--r--. 1 root root 42565 Jun  1 15:50 /var/log/cron-20150601
-rw-r--r--. 1 root root 18286 Jun  7 16:22 /var/log/cron-20150607

具体来说,在 Red Hat 的 Linux 系统上,我们可以检查/var/log/cron日志文件。 我在前面的句子中指定了“基于 Red Hat”,因为在非基于 Red Hat 的系统上,cron 日志可能位于不同的日志文件中。 例如,基于 debian 的系统默认为/var/log/syslog

如果我们不知道哪个日志文件包含 cron 日志,那么有一个简单的技巧可以找到它。 只需运行以下命令行:

# grep -ic cron /var/log/* | grep -v :0
/var/log/cron:400
/var/log/cron-20150518:379
/var/log/cron-20150524:86
/var/log/cron-20150601:590
/var/log/cron-20150607:248
/var/log/messages:1
/var/log/secure:1

前面的命令将使用grep来搜索/var/log中的所有日志文件以查找字符串cron。 该命令还将搜索CronCRONcRon等,因为我们将–i(不敏感)标志添加到grep命令中。 这告诉grep在不区分大小写的模式下搜索。 从本质上说,这意味着将找到单词“cron”的任何匹配,即使该单词是大写或大小写混合的。 我们还在grep命令中添加了–c(count)标志,这将使它计算找到的实例数:

/var/log/cron:400

如果我们查看第一个结果,可以看到grep/var/log/cron中找到了 400 个单词“cron”的实例。

最后,我们将结果重定向到另一个带有–v标志和:0grep命令。 这个grep将接受第一次执行的结果,并省略(-v)包含字符串:0的任何行。 这对于将结果限制为只包含cron字符串的文件非常有用。

从前面的结果中,我们可以看到文件/var/log/cron中包含单词“cron”的最多实例。 这个事实本身就很好地说明了/var/log/croncrond守护进程的日志文件。

现在我们知道了哪个日志文件包含我们要查找的日志消息,我们可以查看该日志文件的内容。 由于这个日志文件非常大,我们将使用less命令来读取这个文件:

# less /var/log/cron

由于这个日志中有相当多的信息,我们将只关注有助于解释这个问题的日志条目。 下面的段是一组有趣的日志消息,它们可以回答我们的作业是否在运行:

Jun 10 18:01:01 localhost CROND[2033]: (root) CMD (run-parts /etc/cron.hourly)
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2042]: finished 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0yum-hourly.cron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2048]: finished 0yum-hourly.cron
Jun 10 18:05:01 localhost CROND[2053]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
Jun 10 18:10:01 localhost CROND[2086]: (root) CMD (/usr/lib64/sa/sa1 1 1)
Jun 10 18:10:01 localhost CROND[2087]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
Jun 10 18:15:01 localhost CROND[2137]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
Jun 10 18:20:01 localhost CROND[2147]: (root) CMD (/usr/lib64/sa/sa1 1 1)

前面的日志消息显示了相当多的行。 让我们分解这些日志,以便更好地理解正在执行的内容。 考虑以下几行:

Jun 10 18:01:01 localhost CROND[2033]: (root) CMD (run-parts /etc/cron.hourly)
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2042]: finished 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0yum-hourly.cron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2048]: finished 0yum-hourly.cron

开头几行似乎不是我们正在寻找的工作,而是cron.hourly工作。

在 Linux 系统上,有多种指定 cron 作业的方法。 在 RHEL 系统中,/etc/中有几个目录以cron开头:

# ls -laF /etc/ | grep cron
-rw-------.  1 root root      541 Jun  9  2014 anacrontab
drwxr-xr-x.  2 root root       34 Jan 23 15:43 cron.d/
drwxr-xr-x.  2 root root       62 Jul 22  2014 cron.daily/
-rw-------.  1 root root        0 Jun  9  2014 cron.deny
drwxr-xr-x.  2 root root       44 Jul 22  2014 cron.hourly/
drwxr-xr-x.  2 root root        6 Jun  9  2014 cron.monthly/
-rw-r--r--.  1 root root      451 Jun  9  2014 crontab
drwxr-xr-x.  2 root root        6 Jun  9  2014 cron.weekly/

cron.dailycron.hourlycron.monthlycron.weekly目录都是可以包含脚本的目录。 这些脚本将按照目录名中指定的时间运行。

例如,让我们看看/etc/cron.hourly/0yum-hourly.cron:

# cat /etc/cron.hourly/0yum-hourly.cron
#!/bin/bash

# Only run if this flag is set. The flag is created by the yum-cron init
# script when the service is started -- this allows one to use chkconfig and
# the standard "service stop|start" commands to enable or disable yum-cron.
if [[ ! -f /var/lock/subsys/yum-cron ]]; then
 exit 0
fi

# Action!
exec /usr/sbin/yum-cron /etc/yum/yum-cron-hourly.conf

前面的文件是一个简单的bash脚本,crond守护进程将每小时执行一次,因为它位于cron.hourly目录中。 通常,包含在这些目录中的脚本是由系统服务放置在那里的。 但是,这些目录也对系统管理员开放,系统管理员可以放置自己的脚本。

用户 crontabs

如果我们继续向下查看日志文件,我们可以看到与我们的自定义作业相关的条目:

Jun 10 18:10:01 localhost CROND[2087]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)

这一行显示了应用支持团队引用的processor命令。 这一行必须是应用支持团队遇到问题的工作。 日志记录告诉我们相当多有用的信息。 首先,它为我们提供了传递给这个作业的命令行选项:

/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null

它还告诉我们作业以vagrant的形式执行。 这个日志条目告诉我们的最重要的事情是作业正在执行。

因为我们知道作业正在执行,所以我们应该验证作业是否成功。 为了做到这一点,我们将采取一种简单的方法,手动执行工作:

$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Traceback (most recent call last):
 File "app.py", line 28, in init app (app.c:1488)
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'

我们应该省略 cron 任务末尾的> /dev/null,因为这会将输出重定向到/dev/null。 这是丢弃 cron 作业输出的一种常见方法。 对于这个手动执行,我们可以利用输出来帮助解决问题。

一旦执行,任务似乎失败了。 它不仅失败了,而且在失败的同时还产生了一条错误消息:

IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'

这个错误很有趣,因为它似乎表明应用打开了太多的文件。 这有什么关系?

理解用户限制

在 Linux 系统上,每个进程都有限制。 设置这些限制是为了防止进程占用过多的系统资源。

虽然这些限制是对每个用户强制执行的,但是,可以为每个用户设置不同的限制。 要查看默认情况下对vagrant用户设置了哪些限制,可以使用ulimit命令:

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3825
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 3825
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

当我们执行ulimit命令时,我们是作为流浪用户执行的。 这一点很重要,因为当我们作为任何其他用户(包括 root)运行ulimit命令时,输出将是该用户的限制。

如果我们看一下ulimit命令的输出,我们可以看到可以设置很多限制。

文件大小限制

让我们来看看和细分几个关键限制:

file size               (blocks, -f) unlimited

第一个有趣的项目是file size限制。 这个限制将限制用户可以创建的文件的大小。 流浪用户的当前设置是unlimited,但是如果我们将该值设置为更小的数字,会发生什么情况?

我们可以通过执行ulimit –f和限制文件的块数来实现。 例如,考虑以下命令行:

$ ulimit -f 10

在将值设置为10后,我们可以再次运行ulimit –f 来验证它是否生效,但这一次没有设置值:

$ ulimit -f
10

现在我们的限制设置为 10 个块,让我们尝试使用dd命令创建一个 500 MB 的文件:

$ dd if=/dev/zero of=/var/tmp/bigfile bs=1M count=500
File size limit exceeded

Linux 上的用户限制的一个好处是,所提供的错误通常是自明的。 我们可以从前面的输出中看到,dd命令不仅无法创建文件,而且还收到了一个错误,说明文件的大小限制已经超出。

最大用户进程限制

另一个有趣的极限是max processes极限:

max user processes              (-u) 3825

此限制可防止用户同时拥有太多的进程。 这是一个非常有用和有趣的限制,因为它可以很容易地防止流氓应用接管系统。

它也可能是你经常遇到的限制。 对于启动许多子进程或线程的应用尤其如此。 为了了解这种限制是如何工作的,我们可以将设置更改为10:

$ ulimit -u 10
$ ulimit -u
10

与文件大小限制一样,我们可以使用ulimit命令修改进程限制。 然而,这一次我们使用了-u标志。 使用ulimit命令,每个用户限制都有自己的唯一标志。 我们可以在ulimit –a的输出中看到这些标志,当然,每个标志都在ulimit的手册页中引用。

现在我们已经将进程限制为10,我们可以通过运行一个命令来执行这个限制:

$ man ulimit
man: fork failed: Resource temporarily unavailable

通过简单地通过 SSH 登录到流浪用户,我们已经在利用多个进程。 很容易遇到10进程的限制,因为我们运行的任何新命令都将使我们的登录超过限制。

从前面的示例中,我们可以看到,当man命令被执行时,无法启动子进程,因此返回了一个错误Resource temporarily unavailable

打开的文件限制

我想要探索的最后一个有趣的用户限制是open files限制:

open files                      (-n) 1024

open files限制将限制一个进程打开的文件数量不能超过定义的数量。 这个限制可以用来防止一个进程一次打开太多的文件。 当防止应用占用过多的系统资源时,这是非常有用的。

像其他极限一样,让我们看看当我们把这个极限简化到一个非常不合理的数时会发生什么:

$ ulimit -n 2
$ ls
-bash: start_pipeline: pgrp pipe: Too many open files
ls: error while loading shared libraries: libselinux.so.1: cannot open shared object file: Error 24

与其他示例一样,在本例中我们收到了一个错误Too many open files。 然而,这个错误看起来很熟悉。 如果我们回顾从预定作业接收到的错误,我们将看到原因。

IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'

在将最大打开文件数设置为2后,ls命令产生了一个错误; 该错误与应用在早些时候执行时收到的错误消息完全相同。

这是否意味着我们的应用试图打开比系统配置允许的更多的文件? 这种可能性很大。

更改用户限制

因为我们怀疑open files限制阻止了应用的执行,所以我们可以将其限制设置为一个更高的值。 然而,这并不像执行ulimit –n那么简单; 下面的输出是我们在执行它时得到的结果:

$ ulimit -n
1024
$ ulimit -n 5000
-bash: ulimit: open files: cannot modify limit: Operation not permitted
$ ulimit -n 4096
$ ulimit -n
4096

默认情况下,在我们的示例系统中,游荡用户可以将open files限制提高到的最高值是4096。 从前面的错误中我们可以看出,任何更高的都被拒绝; 但就像 Linux 的大多数事情一样,我们可以改变这一点。

limits.conf 文件

我们一直在中使用和修改的用户限制是 Linux PAM 系统的一部分。 PAM 或可插入身份验证模块是一个提供模块化身份验证系统的系统。

例如,如果我们的系统要使用 LDAP 进行身份验证,那么将使用pam_ldap.so库来提供此功能。 但是,由于我们的系统使用本地用户进行身份验证,所以pam_localuser.so 库处理用户身份验证。

我们可以通过读取/etc/pam.d/system-auth文件来验证:

$ cat /etc/pam.d/system-auth
#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth        required      pam_env.so
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 1000 quiet_success
auth        required      pam_deny.so

account     required      pam_unix.so
account     sufficient    pam_localuser.so
account     sufficient    pam_succeed_if.so uid < 1000 quiet
account     required      pam_permit.so

password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
-session     optional      pam_systemd.so
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so

如果我们看前面的例子,我们可以看到pam_localuser.soaccount作为第一列:

account     sufficient    pam_localuser.so

这个意味着pam_localuser.so模块是一个允许使用帐户的sufficient模块,这本质上意味着如果用户有正确的/etc/passwd/etc/shadow条目,就可以登录。

session     required      pam_limits.so

如果我们看前面一行,我们可以看到在哪里执行了用户限制。 这一行实际上告诉系统,pam_limits.so模块对于所有用户会话都是必需的。 这有效地确保了在每个用户会话上强制执行pam_limits.so模块标识的用户限制。

这个 PAM 模块的配置位于/etc/security/limits.conf/etc/security/limits.d/:

$ cat /etc/security/limits.conf
#This file sets the resource limits for the users logged in via PAM.
#        - core - limits the core file size (KB)
#        - data - max data size (KB)
#        - fsize - maximum filesize (KB)
#        - memlock - max locked-in-memory address space (KB)
#        - nofile - max number of open files
#        - rss - max resident set size (KB)
#        - stack - max stack size (KB)
#        - cpu - max CPU time (MIN)
#        - nproc - max number of processes
#        - as - address space limit (KB)
#        - maxlogins - max number of logins for this user
#        - maxsyslogins - max number of logins on the system
#        - priority - the priority to run user process with
#        - locks - max number of file locks the user can hold
#        - sigpending - max number of pending signals
#        - msgqueue - max memory used by POSIX message queues (bytes)
#        - nice - max nice priority allowed to raise to values: [-20, 19]
#        - rtprio - max realtime priority
#
#<domain>      <type>  <item>         <value>
#

#*               soft    core            0
#*               hard    rss             10000
#@student        hard    nproc           20
#@faculty        soft    nproc           20
#@faculty        hard    nproc           50
#ftp             hard    nproc           0
#@student        -       maxlogins       4

当我们阅读limits.conf文件时,我们可以看到很多关于用户限制的有用信息。

在这个文件中,列出了可用的限制,并对该限制强制执行的内容进行了描述。 例如,在前面的命令行中,我们可以看到以下关于open files限制的数量:

#        - nofile - max number of open files

从这一行中我们可以看到,如果我们想改变对用户可用的打开文件的数量,我们将需要使用nofile类型。 在列出每个限制所做的上面,limits.conf文件还包含了为用户和组设置自定义限制的示例:

#ftp             hard    nproc           0

给出这个例子,我们可以看到我们需要使用什么格式来设置限制; 但是我们也应该设置什么限制呢? 如果我们回头看看作业中的错误,可以看到错误列出了/opt/myapp/queue目录下的一个文件:

IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'

可以放心地说,应用正试图打开这个目录中的文件。 因此,为了确定这个进程需要打开多少个文件,让我们通过下面的命令行来找出这个目录中有多少个文件:

$ ls -la /opt/myapp/queue/ | wc -l
492304

前面的命令使用ls –la列出queue/目录中的所有文件和目录,并将输出重定向到wc –lwc命令将计算所提供输出的行数(-l),这实际上意味着在queue/目录中有 492,304 个文件和/或目录。

由于数量很大,我们应该将open files的数量限制设置为500000,这足以处理queue/目录,以防万一还需要一点额外的量。 我们可以通过在limits.conf文件中添加以下行来实现:

# vi /etc/security/limits.conf

在使用vi或其他文本编辑器添加行后,我们可以使用tail命令验证它是否存在:

$ tail /etc/security/limits.conf
#@student        hard    nproc           20
#@faculty        soft    nproc           20
#@faculty        hard    nproc           50
#ftp             hard    nproc           0
#@student        -       maxlogins       4

vagrant    soft  nofile    100000
vagrant    hard  nofile    500000

# End of file

更改这些设置并不意味着我们的登录 shell 会立即限制500000。 我们的登录会话仍然有4096设置的限制。

$ ulimit -n
4096

我们也仍然不能增加它超过这个价值。

$ ulimit -n 9000
-bash: ulimit: open files: cannot modify limit: Operation not permitted

为了使我们的更改生效,我们必须再次登录到我们的用户。

正如前面所讨论的,这些限制是由 PAM 设置的,它在 shell 会话登录期间应用。 由于限制是在登录期间设置的,所以我们仍然受到上次登录时获得的前一个值的限制。

为了获得新的限制,我们必须注销并重新登录(或生成一个新的登录会话)。 对于我们的示例,我们将注销 shell 并重新登录。

$ ulimit -n
100000
$ ulimit -n 501000
-bash: ulimit: open files: cannot modify limit: Operation not permitted
$ ulimit -n 500000
$ ulimit -n
500000

如果我们看一下前面的命令行,我们可以看到一些非常有趣的东西。

当我们这次登录时,我们的文件数量限制被设置为100000,正好与我们在limits.conf 文件中设置的soft限制相同。 这是因为soft限制是为每个会话默认设置的限制。

hard限制是该用户可以设置的soft限制之上的最高值。 我们可以在前面的示例中看到这一点,因为我们能够将nofile限制设置为500000而不是501000

未来打样作业

我们将soft限制设置为100000的原因是我们正在为未来类似的场景做计划。 通过将soft限制设置为100000,运行此调度作业的 cron 作业将被限制为 100,000 个打开的文件。 但是,由于hard限制被设置为500000,因此用户可以在登录会话上手动运行设置更高限制的作业。

只要queue目录中的文件数量不超过 500,000 个,任何人都不应该再需要编辑/etc/security/limits.conf文件。

再次运行作业

既然我们的限制已经增加,我们可以尝试再次运行作业。

$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Traceback (most recent call last):
 File "app.py", line 28, in init app (app.c:1488)
IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt'

我们再次收到一个错误。 然而,这一次的错误只是有一点不同。

在之前的运行中,我们收到了以下错误。

IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'

然而,这次我们收到了这个错误。

IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt'

差异非常细微,但是在第二次运行中,我们的错误显示系统中打开的文件太多,而第一次运行中没有包含in system。 这是因为我们遇到了不同类型的限制,不是用户限制,而是系统限制。

内核可调项

Linux 内核本身也可以在系统上设置限制。 这些限制是根据内核参数定义的。 其中一些参数是静态的,不能在运行时更改; 而另一些人。 如果可以在运行时更改内核参数,则称为可调参数

我们可以使用sysctl命令查看静态和可调内核参数及其当前值:

# sysctl -a | head
abi.vsyscall32 = 1
crypto.fips_enabled = 0
debug.exception-trace = 1
debug.kprobes-optimization = 1
dev.hpet.max-user-freq = 64
dev.mac_hid.mouse_button2_keycode = 97
dev.mac_hid.mouse_button3_keycode = 100
dev.mac_hid.mouse_button_emulation = 0
dev.parport.default.spintime = 500
dev.parport.default.timeslice = 200

由于有许多可用参数,我使用head命令将输出限制为前 10 个。 我们之前收到的错误提到了系统上的一个限制,这表明我们可能达到了内核本身施加的限制。

唯一的问题是我们怎么知道是哪一个? 最快的答案当然是搜索谷歌。 由于有如此多的内核参数(在我们正在处理的系统上有 800 多个),很难简单地读取sysctl –a的输出并找到正确的一个。

更现实的方法是简单地搜索我们想要修改的参数类型。 我们的场景的一个搜索示例是Linux parameter max open files。 如果我们执行这个搜索,我们很可能会找到参数和如何修改它。 然而,如果谷歌不是一个选择,还有另一种方法。

通常,内核参数有一个名称来描述该参数控制的内容。

例如,如果我们要寻找禁用 IPv6 的内核参数,我们将首先搜索net字符串,如 network:

# sysctl -a | grep -c net
556

然而,这仍然返回大量的结果。 在这些结果中,我们可以看到字符串ipv6

# sysctl -a | grep -c ipv6
233

尽管如此,还是有很多结果; 但是,如果我们添加字符串disable的搜索,则会得到以下输出:

# sysctl -a | grep ipv6 | grep disable
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.enp0s3.disable_ipv6 = 0
net.ipv6.conf.enp0s8.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0

我们终于可以缩小可能的参数范围了。 然而,我们并不完全了解这些参数的作用。 至少现在还没有。

如果我们通过/usr/share/doc进行快速搜索,我们可能会找到一些文档来解释这些设置的作用。 我们可以通过使用grep在该目录中执行-r的递归搜索来快速完成这一任务。 为了保持输出简单,我们可以添加-l(列表文件),这会导致grep只列出它在以下文件中找到所需字符串的文件名:

# grep -rl net.ipv6 /usr/share/doc/
/usr/share/doc/grub2-tools-2.02/grub.html

在基于 Red Hat 的 Linux 系统上,/usr/share/doc目录用于系统手册页之外的其他文档。 如果我们被限制只能使用系统本身上的文档,那么/usr/share/doc目录就是首先要检查的地方之一。

查找打开文件的内核参数

因为我们喜欢以困难的方式执行任务,所以我们将尝试识别可能限制我们的内核参数,而不需要在谷歌上搜索它。 执行此操作的第一步是在sysctl输出中搜索字符串file

我们搜索file的原因是我们正在达到文件数量的限制。 虽然这可能不能提供我们试图识别的确切参数,但搜索至少会让我们开始:

# sysctl -a | grep file
fs.file-max = 48582
fs.file-nr = 1088  0  48582
fs.xfs.filestream_centisecs = 3000

搜索file实际上可能是一个很好的选择。 仅仅根据参数的名称,我们可能感兴趣的两个是fs.file-maxfs.file-nr。 在这一点上,我们不知道哪个控制打开的文件的数量,或者它们中是否有一个控制。

要找到更多的信息,我们可以搜索doc目录。

# grep -r fs.file- /usr/share/doc/
/usr/share/doc/postfix-2.10.1/README_FILES/TUNING_README:
fs.file-max=16384

似乎位于 Postfix 服务文档中名为TUNING_README、的文档至少引用了我们的一个值。 让我们检查一下这个文件,看看这个文档对这个内核参数说了什么:

* Configure the kernel for more open files and sockets. The details are
 extremely system dependent and change with the operating system version. Be
 sure to verify the following information with your system tuning guide:

 o Linux kernel parameters can be specified in /etc/sysctl.conf or changed
 with sysctl commands:

 fs.file-max=16384
 kernel.threads-max=2048

如果我们阅读文件中列出内核参数的地方的内容,可以看到它专门调用参数来为内核配置更多的打开文件和套接字

该文档调用了两个内核参数,以允许打开更多的文件和套接字。 第一个称为fs.file-max,这也是我们通过sysctl搜索所确定的。 第二个被称为kernel.threads-max,这是一个相当新的概念。

仅仅根据名称,似乎我们想要修改的可调参数是fs.file-max 参数。 让我们看看它的当前值如下:

# sysctl fs.file-max
fs.file-max = 48582

我们可以通过执行sysctl后跟参数名来列出该参数的当前值(如前面的命令行所示)。 这将简单地显示当前定义的值; 这个数字似乎被设置为48582,远远低于我们目前的用户限制。

提示

在前面的例子中,我们在后缀文档中找到了这个参数。 虽然这可能是好的,但并不准确。 如果您经常发现自己需要在本地搜索内核参数,那么安装kernel-doc包将是一个好主意。 包包含相当多的信息,特别是关于可调参数的信息。

更改内核可调项

因为我们认为fs.file-max 参数控制系统可以打开的最大文件数,所以我们应该更改这个值以允许作业运行。

与 Linux 上的大多数系统配置项一样,可以选择在重新启动时更改这个值。 前面我们设置了limits.conf文件,以允许游移用户能够打开 100,000 个文件作为soft的限制,并允许 500,000 个文件作为hard的限制。 问题是,我们希望这个用户能够正常操作打开 500,000 个文件吗? 或者这应该是一个一次性的任务来纠正我们目前面临的问题?

答案很简单:看情况!

如果我们看看我们目前正在处理的情况,所讨论的工作已经有一段时间没有运行了。 因此,队列中积压了大量的消息。 然而,这些都不是正常情况。

在前面,当我们将用户限制设置为 100,000 个文件时,我们这样做是因为这对于这个作业来说是比较合适的值。 考虑到这一点,我们还应该将内核参数设置为略高于100000的值,但不要过大。

对于这个场景和这个环境,我们将执行两个操作。 第一个是配置系统,默认允许125,000 个打开的文件。 第二种方法是将当前参数设置为525,000 个打开文件,以允许计划的作业成功运行。

永久更改可调项

由于我们希望在默认情况下将fs.file-max的值更改为125000,因此我们需要编辑sysctl.conf文件。 sysctl.conf文件是一个系统配置文件,它允许您为可调内核参数指定自定义值。 在每次系统重新引导期间,将读取该文件并应用其中的值。

为了将我们的fs.file-max值设置为125000,我们可以简单地将以下行添加到这个文件:

# vi /etc/sysctl.conf
fs.file-max=125000

现在我们已经添加了自定义值,我们需要告诉系统应用它。

如前所述,sysctl.conf文件是在重新引导时应用的,但是我们也可以在任何时候使用带有–p标志的sysctl命令将这些设置应用到该文件。

# sysctl -p
fs.file-max = 125000

当给出了–p标志时,sysctl命令将读取并将值应用到指定的文件,或者如果没有指定文件/etc/sysctl.conf。 因为我们没有在–p标志之后指定文件,所以sysctl命令应用添加到/etc/sysctl.conf 的值并打印它修改的值。

让我们再次执行sysctl来验证是否正确地应用了它。

# sysctl fs.file-max
fs.file-max = 125000

似乎在中,该值被适当地应用了,但是如果将其设置为525000呢?

临时更改可调项

虽然简单的足以将/etc/sysctl.conf更改为更高的值,但是应用它,然后恢复更改。 有一种更简单的方法可以临时更改可调值。

当提供了–w选项时,sysctl命令将允许修改可调值。 要查看实际效果,我们将使用此方法将fs.file-max值设置为525000

# sysctl -w fs.file-max=525000
fs.file-max = 525000

就像应用sysctl.conf 文件的值一样,当我们执行sysctl –w时,它会打印它所应用的值。 如果我们再次验证它们,我们将看到该值被设置为525000files:

# sysctl fs.file-max
fs.file-max = 525000

最后一次运行作业

现在,我们已经为流浪用户设置了的open files限制为500000,并为整个系统设置了525000。 我们可以再次手动执行该任务,这一次应该成功:

$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Added 492304 to queue
Processing 492304 messages
Processed 492304 messages

这一次作业执行时没有提供任何错误! 我们可以从作业的输出中看到,/opt/myapp/queue中的所有文件也都被处理了。

回首往事

现在我们已经解决了问题,让我们花点时间看看我们是如何解决问题的。

打开的文件太多

为了解决我们的问题,我们手动执行了一个调度的 cron 作业。 如果我们回到前面的章节,这是一个主要的例子,重复一个问题,并看到它自己。

在这种情况下,工作没有执行它应该执行的任务。 为了确定原因,我们手动运行它。

在手动执行过程中,我们能够识别以下错误:

IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'

此错误非常常见,是由于作业运行到阻止单个用户打开过多文件的用户限制而导致的。 为了解决这个问题,我们在/etc/security/limits.conf文件中添加了自定义设置。

这些更改将我们的用户的open filessoft限制默认设置为100000。 我们还允许用户通过hard设置将open files限制增加到500000:

IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt'

在修改了这些限制之后,我们再次执行作业,并遇到了一个类似但不同的错误。

这一次,将open files限制强加于系统本身,在本例中,将系统范围内的打开文件限制为 48,000 个。

为了解决这个问题,我们在/etc/sysctl.conf文件中设置一个永久的125000设置,并将其值临时更改为525000

从那时起,我们就可以手动执行任务了。 但是,在这个实例之外,由于我们更改了默认限制,我们还为这个作业提供了更多的资源来正常执行。 只要不存在超过 100,000 个文件的积压,该任务在未来应该可以毫无问题地执行。

一点清理

说到正常执行,为了减少内核对打开文件的限制,我们可以使用–p选项再次执行sysctl命令。 这将把值重置为/etc/sysctl.conf文件中定义的值。

# sysctl -p
fs.file-max = 125000

该方法需要注意的一点是,sysctl -p将只重置/etc/sysctl.conf中指定的值; 它在默认情况下只包含少数几个可调值。 如果在/etc/sysctl.conf 中没有指定的值被修改,sysctl -p方法不会将该值重置为默认值。

总结

在本章中,我们非常熟悉 Linux 中的内核和用户限制。 这些设置变得非常有用,因为任何使用许多资源的应用最终都将运行其中一个。

在下一章中,我们将集中讨论一个非常普遍但又非常棘手的问题。 我们将着重于故障排除和确定系统内存耗尽的原因。 当系统耗尽内存时,会产生很多后果,比如应用进程被杀死。