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

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

[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)
[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)
[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)
[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 查看一下当前的系统信号量占用情况:

[root@phpruntime ~]# ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x00000000 0          root       600        1
0x00000000 32769      root       600        1
0x00000000 688130     nobody     600        1
0x00000000 720899     nobody     600        1
0x00000000 753668     nobody     600        1
0x00000000 786437     nobody     600        1
0x00000000 819206     nobody     600        1
0x00000000 851975     nobody     600        1
0x7a03096d 587137032  root       600        13
0x00000000 1114121    nobody     600        1
0x00000000 1212426    nobody     600        1
0x00000000 1146891    nobody     600        1
0x00000000 1081356    nobody     600        1
0x00000000 1179661    nobody     600        1
0x00000000 1245198    nobody     600        1
0x00000000 586940431  nobody     600        1
0x00000000 586973200  nobody     600        1
...

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

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

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

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

[root@yq138.phpruntime ~]# cat /proc/sys/kernel/sem
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这个函数。

int ap_mpm_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
{
    int index;
    int remaining_children_to_start;
    apr_status_t rv;

    ap_log_pid(pconf, ap_pid_fname);

    first_server_limit = server_limit;
    if (changed_limit_at_restart) {
        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
                     "WARNING: Attempt to change ServerLimit "
                     "ignored during restart");
        changed_limit_at_restart = 0;
    }

    /* Initialize cross-process accept lock */
    ap_lock_fname = apr_psprintf(_pconf, "%s.%" APR_PID_T_FMT,
                                 ap_server_root_relative(_pconf, ap_lock_fname),
                                 ap_my_pid);
    // 调用apr_proc_mutex_create创建accept_mutex
    rv = apr_proc_mutex_create(&accept_mutex, ap_lock_fname,
                               ap_accept_lock_mech, _pconf);
    if (rv != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_EMERG, rv, s,
                     "Couldn't create accept lock (%s) (%d)",
                     ap_lock_fname, ap_accept_lock_mech);
        mpm_state = AP_MPMQ_STOPPING;
        return 1;
    }
    // 省略....
}

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

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

static apr_status_t proc_mutex_create(apr_proc_mutex_t *new_mutex, apr_lockmech_e mech, const char *fname)
{
    apr_status_t rv;
    // 根据mech选择合适的实现
    if ((rv = proc_mutex_choose_method(new_mutex, mech)) != APR_SUCCESS) {
        return rv;
    }

    new_mutex->meth = new_mutex->inter_meth;
    // 调用对应的实现
    if ((rv = new_mutex->meth->create(new_mutex, fname)) != APR_SUCCESS) {
        return rv;
    }

    return APR_SUCCESS;
}
// apr_proc_mutex_create 实现
APR_DECLARE(apr_status_t) apr_proc_mutex_create(apr_proc_mutex_t **mutex,
                                                const char *fname,
                                                apr_lockmech_e mech,
                                                apr_pool_t *pool)
{
    apr_proc_mutex_t *new_mutex;
    apr_status_t rv;

    new_mutex = apr_pcalloc(pool, sizeof(apr_proc_mutex_t));
    new_mutex->pool = pool;
    // 调用上面的proc_mutex_create
    if ((rv = proc_mutex_create(new_mutex, mech, fname)) != APR_SUCCESS)
        return rv;

    *mutex = new_mutex;
    return APR_SUCCESS;
}

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

static apr_status_t proc_mutex_choose_method(apr_proc_mutex_t *new_mutex, apr_lockmech_e mech)
{
    // 如果指定了某个确定的选项,则直接使用对应的实现。
    switch (mech) {
    case APR_LOCK_FCNTL:
#if APR_HAS_FCNTL_SERIALIZE
        new_mutex->inter_meth = &mutex_fcntl_methods;
#else
        return APR_ENOTIMPL;
#endif
        break;
    case APR_LOCK_FLOCK:
#if APR_HAS_FLOCK_SERIALIZE
        new_mutex->inter_meth = &mutex_flock_methods;
#else
        return APR_ENOTIMPL;
#endif
        break;
    case APR_LOCK_SYSVSEM:
#if APR_HAS_SYSVSEM_SERIALIZE
        new_mutex->inter_meth = &mutex_sysv_methods;
#else
        return APR_ENOTIMPL;
#endif
        break;
    case APR_LOCK_POSIXSEM:
#if APR_HAS_POSIXSEM_SERIALIZE
        new_mutex->inter_meth = &mutex_posixsem_methods;
#else
        return APR_ENOTIMPL;
#endif
        break;
    case APR_LOCK_PROC_PTHREAD:
#if APR_HAS_PROC_PTHREAD_SERIALIZE
        new_mutex->inter_meth = &mutex_proc_pthread_methods;
#else
        return APR_ENOTIMPL;
#endif
        break;
    // 默认选择,和编译环境相关,当前环境中选择的是&mutex_sysv_methods。
    case APR_LOCK_DEFAULT:
#if APR_USE_FLOCK_SERIALIZE
        new_mutex->inter_meth = &mutex_flock_methods;
#elif APR_USE_SYSVSEM_SERIALIZE
        new_mutex->inter_meth = &mutex_sysv_methods;
#elif APR_USE_FCNTL_SERIALIZE
        new_mutex->inter_meth = &mutex_fcntl_methods;
#elif APR_USE_PROC_PTHREAD_SERIALIZE
        new_mutex->inter_meth = &mutex_proc_pthread_methods;
#elif APR_USE_POSIXSEM_SERIALIZE
        new_mutex->inter_meth = &mutex_posixsem_methods;
#else
        return APR_ENOTIMPL;
#endif
        break;
    default:
        return APR_ENOTIMPL;
    }
    return APR_SUCCESS;
}

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

static apr_status_t proc_mutex_sysv_create(apr_proc_mutex_t *new_mutex,
                                           const char *fname)
{
    union semun ick;
    apr_status_t rv;
    
    new_mutex->interproc = apr_palloc(new_mutex->pool, sizeof(*new_mutex->interproc));
    // 调用semget获得信号
    new_mutex->interproc->filedes = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);

    if (new_mutex->interproc->filedes < 0) {
        rv = errno;
        proc_mutex_sysv_cleanup(new_mutex);
        return rv;
    }
    ick.val = 1;
    if (semctl(new_mutex->interproc->filedes, 0, SETVAL, ick) < 0) {
        rv = errno;
        proc_mutex_sysv_cleanup(new_mutex);
        return rv;
    }
    new_mutex->curr_locked = 0;
    // 注册清理函数
    apr_pool_cleanup_register(new_mutex->pool,
                              (void *)new_mutex, apr_proc_mutex_cleanup, 
                              apr_pool_cleanup_null);
    return APR_SUCCESS;
}

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

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

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