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

<think>嗯,用户想了解Nginx现象解决方案和原理。首先,我需要回忆一下效应是什么。根据之前的引用,效应指的是当多个进程或线程同时等待同一个资源时,资源就绪导致所有等待者都被唤醒,但最终只有一个能获得资源,其他只能重新进入等待,这样导致性能浪费。 接下来,用户提到了Nginx解决方案。根据引用[3]和[4],Linux本身部分解决了accept,但epoll在fork之后仍有问题。Nginx使用了自实现的互斥,也就是ngx_shmtx_t结构体中的机制。 然后,我需要解释Nginx的具体解决步骤。根据引用[4],Nginx的worker进程在监听端口时,会通过共享避免同时处理事件。每个worker进程在监听事件前会尝试获取,只有成功获取的进程才能处理事件,其他进程则继续等待。这样避免了多个进程被同时唤醒。 另外,引用[2]中的代码显示Nginx使用了原子操作和信号量来处理,这可能涉及到自旋和信号量结合的方式,减少上下文切换。spin字段可能用于控制自旋次数,优化性能。 可能用户还关心为什么传统方法会有问题,比如频繁的上下文切换,而Nginx机制如何优化这一点。需要说明Nginx通过竞争的方式,确保只有一个worker进程处理连接,减少无效唤醒。 最后,生成相关问题时要覆盖原理、实现、性能优化、适用场景以及与其他方案对比等方面,确保用户能全面理解。</think>### Nginx现象解决方案与原理 #### 1. 现象的定义 在Nginx多进程模型中,当客户端发起新连接请求时,所有监听了相同端口的`worker`进程会被**同时唤醒**,但最终只有一个进程能成功处理该连接,其他进程因“事件失效”而重新休眠。这种**无效唤醒导致的资源浪费**被称为效应[^3][^4]。 #### 2. Nginx解决方案 ##### (1) **共享机制** Nginx通过`ngx_shmtx_t`结构体实现进程间共享,关键代码定义如下[^2]: ```c typedef struct { #if (NGX_HAVE_ATOMIC_OPS) ngx_atomic_t *lock; // 原子指针 #if (NGX_HAVE_POSIX_SEM) ngx_atomic_t *wait; // 等待计数器 ngx_uint_t semaphore; sem_t sem; // POSIX信号量 #endif #else ngx_fd_t fd; // 文件描述符(非原子操作时使用) u_char *name; #endif ngx_uint_t spin; // 自旋次数控制 } ngx_shmtx_t; ``` - **原子(`lock`)**:通过原子操作实现**非阻塞竞争**,避免进程阻塞 - **信号量(`sem`)**:在竞争失败时,通过信号量挂起进程,减少CPU空转 - **自旋控制(`spin`)**:通过`spin`参数设置自旋次数,平衡CPU消耗与响应速度 ##### (2) 工作流程 1. **事件监听阶段** 所有`worker`进程通过`epoll_wait()`监听端口事件[^4] 2. **竞争阶段** - 当新连接到达时,`worker`进程尝试通过`ngx_shmtx_trylock()`获取共享 - 成功获取的进程处理连接,其他进程**立即放弃处理**并重新监听 3. **负载均衡** 通过`ngx_accept_disabled`变量实现进程间的**动态负载均衡**,避免某个进程长期独占 #### 3. 性能优化原理 | 传统问题 | Nginx解决方案 | |------------------------------|-----------------------------| | 所有进程被唤醒→上下文切换 | 仅一个进程被唤醒→减少切换 | | 频繁的无效系统调用 | 原子操作+信号量减少系统调用 | | 固定进程负载分配 | 动态权重调整负载均衡 | 该方案将效应发生的概率从$O(n)$降低到$O(1)$(n为进程数),实测中可减少约80%的无效上下文切换[^1]。 #### 4. 适用场景 - **高并发连接**:适用于`keepalive`长连接较少的场景 - **短时请求**:对连接建立阶段的优化效果最显著 - **Linux 2.6+系统**:依赖内核的`EPOLLEXCLUSIVE`特性[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值