to inspire confidence in somebody.

0%

Apache: No space left on device: Couldn't create accept lock

服务器的Apache进程突然无法启动了,在错误日志中,有如下信息:

1
[Mon Feb 13 14:54:10 2017] [emerg] (28)No space left on device: Couldn't create accept lock (/var/logs/accept.lock.8173) (5)
2
[Mon Feb 13 14:55:02 2017] [emerg] (28)No space left on device: Couldn't create accept lock (/var/logs/accept.lock.8823) (5)
3
[Mon Feb 13 14:56:01 2017] [emerg] (28)No space left on device: Couldn't create accept lock (/var/logs/accept.lock.9113) (5)
4
[Mon Feb 13 14:57:01 2017] [emerg] (28)No space left on device: Couldn't create accept lock (/var/logs/accept.lock.9765) (5)

看了一下磁盘,空间并没有被占满,于是搜索了一下,找到了办法。

使用 ipcs -s 查看一下当前的系统信号量占用情况:

1
[root@phpruntime ~]# ipcs -s
2
3
------ Semaphore Arrays --------
4
key        semid      owner      perms      nsems
5
0x00000000 0          root       600        1
6
0x00000000 32769      root       600        1
7
0x00000000 688130     nobody     600        1
8
0x00000000 720899     nobody     600        1
9
0x00000000 753668     nobody     600        1
10
0x00000000 786437     nobody     600        1
11
0x00000000 819206     nobody     600        1
12
0x00000000 851975     nobody     600        1
13
0x7a03096d 587137032  root       600        13
14
0x00000000 1114121    nobody     600        1
15
0x00000000 1212426    nobody     600        1
16
0x00000000 1146891    nobody     600        1
17
0x00000000 1081356    nobody     600        1
18
0x00000000 1179661    nobody     600        1
19
0x00000000 1245198    nobody     600        1
20
0x00000000 586940431  nobody     600        1
21
0x00000000 586973200  nobody     600        1
22
...

其中nobody用户占用的信号量总数非常多,超过了100个,而我们的Apache也是运行在nobody下的,应该是信号量没有正确释放导致的,手动释放一下:

1
for i in `ipcs -s|awk '/nobody/ {print $2}'`; do (ipcrm -s $i); done

释放结束后,Apache便可以正常启动了。

具体到Semaphore,也就是信号量,有一个内核参数可以修改:

1
[root@yq138.phpruntime ~]# cat /proc/sys/kernel/sem
2
250     32000   32      128

上面的4个数字分别代表SEMMSL, SEMMNS, SEMOPM, SEMMNI这4个属性。

  • SEMMSL:用于控制每个信号集的最大信号数量。(defines the maximum number of semaphores per semaphore set.)
  • SEMMNS:用于控制整个 Linux 系统中信号(不是信号集)的最大数。(defines the total number of semaphores (not semaphore sets) for the entire Linux system)
  • SEMOPM:用于控制每次semop系统调用最大可以调用的信号数量 。(defines the maximum number of semaphore operations that can be performed per semop(2) system call (semaphore call))
  • SEMMNI:用于控制整个 Linux 系统中信号集的最大数量。(defines the maximum number of semaphore sets for the entire Linux system.)

可以通过调整这4个数值来解决上面问题,但是是治标不治本的,因为问题发生的原因不是信号量资源不够用,而是因为没有正确释放。这里顺便看了看httpd的代码:

在prefork模式中,需要创建一个fork的锁,调用的是apr_proc_mutex_create这个函数。

1
int ap_mpm_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
2
{
3
    int index;
4
    int remaining_children_to_start;
5
    apr_status_t rv;
6
7
    ap_log_pid(pconf, ap_pid_fname);
8
9
    first_server_limit = server_limit;
10
    if (changed_limit_at_restart) {
11
        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
12
                     "WARNING: Attempt to change ServerLimit "
13
                     "ignored during restart");
14
        changed_limit_at_restart = 0;
15
    }
16
17
    /* Initialize cross-process accept lock */
18
    ap_lock_fname = apr_psprintf(_pconf, "%s.%" APR_PID_T_FMT,
19
                                 ap_server_root_relative(_pconf, ap_lock_fname),
20
                                 ap_my_pid);
21
    // 调用apr_proc_mutex_create创建accept_mutex
22
    rv = apr_proc_mutex_create(&accept_mutex, ap_lock_fname,
23
                               ap_accept_lock_mech, _pconf);
24
    if (rv != APR_SUCCESS) {
25
        ap_log_error(APLOG_MARK, APLOG_EMERG, rv, s,
26
                     "Couldn't create accept lock (%s) (%d)",
27
                     ap_lock_fname, ap_accept_lock_mech);
28
        mpm_state = AP_MPMQ_STOPPING;
29
        return 1;
30
    }
31
    // 省略....
32
}

调用时,传入了一个参数 ap_accept_lock_mech,这个参数可以通过配置文件的 AcceptMutex 这个配置项进行配置,默认如果不配置的话,会使用 APR_LOCK_DEFAULT这个默认方式。

apr_proc_mutex_create 是apr库的一部分,apr库在不同平台有不同的实现,我们就看针对unix系统的实现。

1
static apr_status_t proc_mutex_create(apr_proc_mutex_t *new_mutex, apr_lockmech_e mech, const char *fname)
2
{
3
    apr_status_t rv;
4
    // 根据mech选择合适的实现
5
    if ((rv = proc_mutex_choose_method(new_mutex, mech)) != APR_SUCCESS) {
6
        return rv;
7
    }
8
9
    new_mutex->meth = new_mutex->inter_meth;
10
    // 调用对应的实现
11
    if ((rv = new_mutex->meth->create(new_mutex, fname)) != APR_SUCCESS) {
12
        return rv;
13
    }
14
15
    return APR_SUCCESS;
16
}
17
// apr_proc_mutex_create 实现
18
APR_DECLARE(apr_status_t) apr_proc_mutex_create(apr_proc_mutex_t **mutex,
19
                                                const char *fname,
20
                                                apr_lockmech_e mech,
21
                                                apr_pool_t *pool)
22
{
23
    apr_proc_mutex_t *new_mutex;
24
    apr_status_t rv;
25
26
    new_mutex = apr_pcalloc(pool, sizeof(apr_proc_mutex_t));
27
    new_mutex->pool = pool;
28
    // 调用上面的proc_mutex_create
29
    if ((rv = proc_mutex_create(new_mutex, mech, fname)) != APR_SUCCESS)
30
        return rv;
31
32
    *mutex = new_mutex;
33
    return APR_SUCCESS;
34
}

proc_mutex_choose_method 选择对应实现的时候,会根据传入的mech参数进行选择,当参数为APR_LOCK_DEFAULT时:

1
static apr_status_t proc_mutex_choose_method(apr_proc_mutex_t *new_mutex, apr_lockmech_e mech)
2
{
3
    // 如果指定了某个确定的选项,则直接使用对应的实现。
4
    switch (mech) {
5
    case APR_LOCK_FCNTL:
6
#if APR_HAS_FCNTL_SERIALIZE
7
        new_mutex->inter_meth = &mutex_fcntl_methods;
8
#else
9
        return APR_ENOTIMPL;
10
#endif
11
        break;
12
    case APR_LOCK_FLOCK:
13
#if APR_HAS_FLOCK_SERIALIZE
14
        new_mutex->inter_meth = &mutex_flock_methods;
15
#else
16
        return APR_ENOTIMPL;
17
#endif
18
        break;
19
    case APR_LOCK_SYSVSEM:
20
#if APR_HAS_SYSVSEM_SERIALIZE
21
        new_mutex->inter_meth = &mutex_sysv_methods;
22
#else
23
        return APR_ENOTIMPL;
24
#endif
25
        break;
26
    case APR_LOCK_POSIXSEM:
27
#if APR_HAS_POSIXSEM_SERIALIZE
28
        new_mutex->inter_meth = &mutex_posixsem_methods;
29
#else
30
        return APR_ENOTIMPL;
31
#endif
32
        break;
33
    case APR_LOCK_PROC_PTHREAD:
34
#if APR_HAS_PROC_PTHREAD_SERIALIZE
35
        new_mutex->inter_meth = &mutex_proc_pthread_methods;
36
#else
37
        return APR_ENOTIMPL;
38
#endif
39
        break;
40
    // 默认选择,和编译环境相关,当前环境中选择的是&mutex_sysv_methods。
41
    case APR_LOCK_DEFAULT:
42
#if APR_USE_FLOCK_SERIALIZE
43
        new_mutex->inter_meth = &mutex_flock_methods;
44
#elif APR_USE_SYSVSEM_SERIALIZE
45
        new_mutex->inter_meth = &mutex_sysv_methods;
46
#elif APR_USE_FCNTL_SERIALIZE
47
        new_mutex->inter_meth = &mutex_fcntl_methods;
48
#elif APR_USE_PROC_PTHREAD_SERIALIZE
49
        new_mutex->inter_meth = &mutex_proc_pthread_methods;
50
#elif APR_USE_POSIXSEM_SERIALIZE
51
        new_mutex->inter_meth = &mutex_posixsem_methods;
52
#else
53
        return APR_ENOTIMPL;
54
#endif
55
        break;
56
    default:
57
        return APR_ENOTIMPL;
58
    }
59
    return APR_SUCCESS;
60
}

系统选择了sysv的实现,所以create的具体实现是:

1
static apr_status_t proc_mutex_sysv_create(apr_proc_mutex_t *new_mutex,
2
                                           const char *fname)
3
{
4
    union semun ick;
5
    apr_status_t rv;
6
    
7
    new_mutex->interproc = apr_palloc(new_mutex->pool, sizeof(*new_mutex->interproc));
8
    // 调用semget获得信号
9
    new_mutex->interproc->filedes = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
10
11
    if (new_mutex->interproc->filedes < 0) {
12
        rv = errno;
13
        proc_mutex_sysv_cleanup(new_mutex);
14
        return rv;
15
    }
16
    ick.val = 1;
17
    if (semctl(new_mutex->interproc->filedes, 0, SETVAL, ick) < 0) {
18
        rv = errno;
19
        proc_mutex_sysv_cleanup(new_mutex);
20
        return rv;
21
    }
22
    new_mutex->curr_locked = 0;
23
    // 注册清理函数
24
    apr_pool_cleanup_register(new_mutex->pool,
25
                              (void *)new_mutex, apr_proc_mutex_cleanup, 
26
                              apr_pool_cleanup_null);
27
    return APR_SUCCESS;
28
}

所以实际的情况是,Apache在启动的时候申请了信号集,但是并没有正常的在退出的时候执行清理,导致了信号集的堆积,当超过了系统的上限,就会导致申请失败,Apache无法启动。

而我们的系统在Apache相关的扩展或者依赖有更新时,会使用非常暴力的 kill -9 强制让Apache退出的方式以便加快更新速度,由于是 kill -9,程序直接就退出了,没有执行清理操作,
所以才会导致没释放的信号集越来越多,最终导致出现问题。

解决方法也比较简单,不使用 kill -9的方式杀死Apache进程,让进程自然退出就好了。