nginx如何避免惊群现象

本文深入探讨Nginx如何通过自定义锁机制避免惊群现象,包括支持原子操作和不支持原子操作两种情况下的锁实现,以及锁的获取与释放过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

总体设计

nginx使用锁ngx_accept_mutex来避免惊群现象。锁是自己来实现的,这里锁的实现分为两种情况,一种是支持原子操作的情况,也就是由NGX_HAVE_ATOMIC_OPS这个宏来进行控制的,一种是不支持原子操作,这是是使用文件锁来实现。

用户空间进程间锁实现的原理,就是能弄一个让所有进程共享的东西,比如mmap的内存,比如文件,然后通过这个东西来控制进程的互斥。参考之前的一篇文档《锁的本质》,其实锁就是一种共享的资源而已。——共享一个变量,然后通过设置这个变量来控制进程的行为。

typedef struct {  
#if (NGX_HAVE_ATOMIC_OPS)  
    ngx_atomic_t  *lock;  //原子操作
#else  
    ngx_fd_t       fd;  //fd表示进程间共享的文件句柄
    u_char        *name;  //name表示文件名
#endif  
} ngx_shmtx_t; 

文件锁

上锁:ngx_trylock_accept_mutex----------------->ngx_shmtx_trylock--------------->ngx_trylock_fd
删除锁:ngx_shmtx_unlock------------------------->ngx_unlock_fd
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
/*ngx_shmtx_trylock通过fcntl对ngx_accept_mutex中fd成员变量上锁,锁定整个文件*/
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {/*锁定成功*/

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");

                          /*ngx_accept_mutex_held 获得锁的标志,如果本来已经获得锁,则直接返回OK*/
        if (ngx_accept_mutex_held
            && ngx_accept_events == 0
            && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
        {
            return NGX_OK;
        }

                            /*ngx_enable_accept_events将cycle->listening中的端口号都加到epoll事件中。把进程对应的监听socket 放入到epoll中进行监听,这样只有该进程能监听到accept操作。*/
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }

        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;/*获得锁的标识*/

        return NGX_OK;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);

/*//如果我们前面已经获得了锁,然后这次获得锁失败,则说明当前的listen句柄已经被其他的进程锁监听,因此此时需要从epoll中移出调已经注册的listen句柄。这样就很好的控制了子进程的负载均衡 */
    if (ngx_accept_mutex_held) {

        /*ngx_disable_accept_events将cycle->listening中的端口号从epoll事件中删除*/
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}

ngx_shmtx_trylock(ngx_shmtx_t *mtx){
    ngx_err_t  err;
    err = ngx_trylock_fd(mtx->fd);
    if (err == 0) {
        return 1;
    }
}

ngx_err_t
ngx_trylock_fd(ngx_fd_t fd)
{
    struct flock  fl;
    ngx_memzero(&fl, sizeof(struct flock));
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;

    if (fcntl(fd, F_SETLK, &fl) == -1) {
        return ngx_errno;
    }

    return 0;
}

void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
    ngx_err_t  err;
    err = ngx_unlock_fd(mtx->fd);

    if (err == 0) {
        return;
    }

    ngx_log_abort(err, ngx_unlock_fd_n " %s failed", mtx->name);
}

ngx_err_t
ngx_unlock_fd(ngx_fd_t fd)
{
    struct flock  fl;
    ngx_memzero(&fl, sizeof(struct flock));
    fl.l_type = F_UNLCK;
    fl.l_whence = SEEK_SET;

    if (fcntl(fd, F_SETLK, &fl) == -1) {
        return  ngx_errno;
    }

    return 0;
}

支持原子操作的锁

    /* cl should be equal or bigger than cache line size */

    cl = 128;
    // 创建共享内存,用于accept mutex,connection counter和ngx_temp_number
    size = cl            /* ngx_accept_mutex */
           + cl          /* ngx_connection_counter */
           + cl;         /* ngx_temp_number */

通过代码可以看到这里将会有3个区域被所有进程共享,其中我们的锁将会用到的是第一个。下面这段代码是初始化对应的共享内存区域。然后保存对应的互斥体指针。
 shm.size = size;
    shm.name.len = sizeof("nginx_shared_zone");
    shm.name.data = (u_char *) "nginx_shared_zone";
    shm.log = cycle->log;
    // 创建共享内存,共享内存的起始地址保存在shm.addr
    if (ngx_shm_alloc(&shm) != NGX_OK) {
        return NGX_ERROR;
    }
    // 获得共享内存的起始地址
    shared = shm.addr;
    // accept互斥体取得共享内存的第一段cl大小内存
    ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;
    ngx_accept_mutex.spin = (ngx_uint_t) -1;
    // 系统支持原子数据则使用原子数据实现accept mutex,否则使用文件上锁实现
    if (ngx_shmtx_create(&ngx_accept_mutex, shared, cycle->lock_file.data)
        != NGX_OK)
    {
        return NGX_ERROR;
    }

ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)
{
    mtx->lock = addr;

    if (mtx->spin == (ngx_uint_t) -1) {
        return NGX_OK;
    }

    mtx->spin = 2048;

#if (NGX_HAVE_POSIX_SEM)

    if (sem_init(&mtx->sem, 1, 0) == -1) {
        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                      "sem_init() failed");
    } else {
        mtx->semaphore = 1;
    }

我们先来看获得锁。 这里nginx分为两个函数,一个是trylock,它是非阻塞的,也就是说它会尝试的获得锁,如果没有获得的话,它会直接返回错误。 而第二个是lock,它也会尝试获得锁,而当没有获得他不会立即返回,而是开始进入循环然后不停的去获得锁,知道获得。不过nginx这里还有用到一个技巧,就是每次都会让当前的进程放到cpu的运行队列的最后一位,也就是自动放弃cpu。 先来看trylock 这个很简单,首先判断lock是否为0,为0的话表示可以获得锁,因此我们就调用ngx_atomic_cmp_set去获得锁,如果获得成功就会返回1,负责为0.

ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
    ngx_atomic_uint_t  val;

    val = *mtx->lock;

    return ((val & 0x80000000) == 0
            && ngx_atomic_cmp_set(mtx->lock, val, val | 0x80000000));
}

ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)  

的意思就是如果lock的值是0的话,就把lock的值修改为当前的进程id,否则返回失败。

接下来来看lock的实现,lock最终会调用ngx_spinlock,因此下面我要主要来分析这个函数。
#define ngx_shmtx_lock(mtx)   ngx_spinlock((mtx)->lock, ngx_pid, 1024)

我们来看spinklock,必须支持原子指令,才会有这个函数,这里nginx采用宏来控制的. 这里和trylock的处理差不多,都是利用原子指令来实现的,只不过这里如果无法获得锁,则会继续等待。 我们来看代码的实现:
void
ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)  
{  
#if (NGX_HAVE_ATOMIC_OPS)  
   ngx_uint_t  i, n;  
for ( ;; ) {  
//如果lock为0,则说明没有进程持有锁,因此设置lock为value(为当前进程id),然后返回。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {  
return;  
       }  
//如果cpu个数大于1(也就是多核),则进入spin-wait loop阶段。
if (ngx_ncpu > 1) {  
//开始进入循环。
for (n = 1; n < spin; n <<= 1) {  
//下面这段就是纯粹的spin-loop wait。
for (i = 0; i < n; i++) {  
//这个函数其实就是执行"PAUSE"指令,接下来会解释这个指令。
                   ngx_cpu_pause();  
               }  
//然后重新获取锁,如果获得则直接返回。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {  
return;  
               }  
           }  
       }  
//这个函数调用的是sched_yield,它会强迫当前运行的进程放弃占有处理器。
       ngx_sched_yield();  
   }  
#else
#if (NGX_THREADS)  
#error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !  
#endif  
#endif  
}  

通过上面的代码可以看到spin lock实现的很简单,就是一个如果无法获得锁,就进入忙等的过程,不过这里nginx还多加了一个处理,就是如果忙等太长,就放弃cpu,直到下次任务再次占有cpu。 接下来来看下PAUSE指令,这条指令主要的功能就是告诉cpu,我现在是一个spin-wait loop,然后cpu就不会因为害怕循环退出时,内存的乱序而需要处理,所引起的效率损失问题。

##参考http://lonelyc.blog.51cto.com/8072409/1363799
   http://www.xuebuyuan.com/557899.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值