写点什么

nginx 惊群处理及原子锁技术

用户头像
赖猫
关注
发布于: 2021 年 03 月 02 日

视频:


从nginx“惊群”问题来看高并发锁的方案​www.bilibili.com


nginx 原子锁实现介绍


Nginx 中的锁是自己实现的,分为两种,一种是支持原子实现的原子锁,另外一种是文件锁。本文我们重点介绍原子锁的实现。


我们可以看到在线程中实现锁就是通过一个共享的堆上的内存(通过 malloc 实现),那么在进程中实现锁也是通过这样一个共享的区域来实现进程的同步。说白了就是共享一个变量,然后通过这个变量来控制多个进程同步运行。


“无锁”技术和“原子锁”概念,专业一点的称无锁技术,从原理上讲原子操作还需要锁吗?所以又叫无锁技术,本贴不再深究其叫法,认为是同一种技术。


处理惊群实现原理

nginx 是如何实现锁的?如何使用原子锁实现来避免惊群的发生的?


在 Nginx 的锁的实现中,要分为两种情况,分别为支持原子操作以与不支持原子操作。其定义在 Ngx_shmtx.h 当中:


//锁的定义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;#endif#else    ngx_fd_t       fd;   //不支持原子操作的话就使用文件锁来实现    u_char        *name;#endif    ngx_uint_t     spin;     //这是自旋锁么?} ngx_shmtx_t;
复制代码


其实定义还是很简单的,一看就明白了。好接下来看支持原子操作的方式是如何实现的吧,在 ngx_event_core_module 模块的 ngx_event_module_init 函数中会有如下代码:


    /*后面将会创建size大小的共享内存,这块共享内存将被均分成三段,     分别供ngx_accept_mutex、ngx_connection_counter、ngx_temp_number     使用。     */      /* cl should be equal to or greater than cache line size */    cl = 128;    size = cl            /* ngx_accept_mutex */           + cl          /* ngx_connection_counter */           + cl;         /* ngx_temp_number */
//共享内存的初始化 shm.size = size; shm.name.len = sizeof("nginx_shared_zone"); shm.name.data = (u_char *) "nginx_shared_zone"; shm.log = cycle->log;
if (ngx_shm_alloc(&shm) != NGX_OK) { //为共享内存分配内存空间 return NGX_ERROR; }
shared = shm.addr; //获取共享内存的地址
ngx_accept_mutex_ptr = (ngx_atomic_t *) shared; //存放互斥量内存地址的指针 ngx_accept_mutex.spin = (ngx_uint_t) -1; //初始化自旋锁的初值为-1
if (ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared, //如果支持原子操作的话,这个就很简单了,就直接将内存地址分配过去就行了 cycle->lock_file.data) != NGX_OK) { return NGX_ERROR; }
ngx_connection_counter = (ngx_atomic_t *) (shared + 1 * cl); //ngx_connection_counter为其分配共享内存的内存空间
(void) ngx_atomic_cmp_set(ngx_connection_counter, 0, 1);
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "counter: %p, %d", ngx_connection_counter, *ngx_connection_counter);
ngx_temp_number = (ngx_atomic_t *) (shared + 2 * cl); //ngx_temp_number的内存空间
复制代码


这段代码的意思是首先调用 ngx_shm_alloc 函数创建共享内存,然后再为 ngx_accept_mutex 变量在其中分配其 lock 域内存,嗯,这个变量的用处大概大家也知道吧。(其实 lock 说白了也就是一个 64 位的 int 而已),当然共享内存中还有其他一些变量的定义,ngx_connection_counter 变量用于保存当前服务器总共持有的 connection。


注意 ngx_shmtx_create 函数,它用于创建锁,这里有两种方式的实现,分别为支持原子操作,和不支持原子操作的两种,这里我们只看支持原子操作的方式吧:


//为锁mtx的lock域分配内存ngx_int_tngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name){    mtx->lock = &addr->lock;  //其实就是直接将内存地址赋个mtx的lock域就完事了
if (mtx->spin == (ngx_uint_t) -1) { return NGX_OK; }
mtx->spin = 2048;
return NGX_OK;}
复制代码


接下来看 nginx 如何获取以及释放锁。嗯,实现有两种,分别为 lock 与 trylock,如果是 lock 的话,那么会组设,也就是自旋,直到获取了锁位置,如果使用 trylock 的话,那么就是非阻塞的方式,如果没有获取到,那么直接返回错误就好了。我们先看 trylock 吧,定义在 Ngx_shmtx.c 当中(还是只看支持原子操作的实现方式吧):


//尝试获取锁,原子的方式ngx_uint_tngx_shmtx_trylock(ngx_shmtx_t *mtx){    return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));}
复制代码


其实很简单,首先是判断 mtx 的 lock 域是否等于 0,如果不等于,那么就直接返回 false 好了,如果等于的话,那么就要调用原子操作 ngx_atomic_cmp_set 了,它用于比较 mtx 的 lock 域,如果等于零,那么设置为当前进程的进程 id 号,否则返回 false。嗯,这个 ngx_atomic_cmp_set 函数是跟体系结构相关的,这里就不细讲了。


然后就可以将 lock 的实现了:


//尝试获取锁,原子的方式ngx_uint_tngx_shmtx_trylock(ngx_shmtx_t *mtx){    return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));}
//阻塞的方式获取锁voidngx_shmtx_lock(ngx_shmtx_t *mtx){ ngx_uint_t i, n;
ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx lock");//一个死循环,不断的去看是否获取了锁,直到获取了之后才退出 for ( ;; ) {//如果获取了锁,那么就可以直接返回了 if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) { return; }//如果cpu的数量大于一 if (ngx_ncpu > 1) { for (n = 1; n < mtx->spin; n <<= 1) { for (i = 0; i < n; i++) { ngx_cpu_pause(); }
if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) { return; } } }
ngx_sched_yield(); }}
复制代码


一个 for 循环就暴露了其自旋的本质。里面还涉及到 一些优化的,嗯,我也不太懂,以后再说吧。接下来就可以将 unlock 了:


//释放锁voidngx_shmtx_unlock(ngx_shmtx_t *mtx){    if (mtx->spin != (ngx_uint_t) -1) {        ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx unlock");    }
if (ngx_atomic_cmp_set(mtx->lock, ngx_pid, 0)) { ngx_shmtx_wakeup(mtx); }}
复制代码


还是很简单,判断锁的 lock 域与当前进程的进程 id 是否相等,如果相等的话,那么就将 lock 设置为 0,然后就相当于释放了锁。


好接下来可以看如何用锁来避免惊群了。在 ngx_event_core_module 模块的 ngx_event_module_init 函数中我们已经看到了 ngx_accept_mutex 的 lock 域的内存是在共享内存中,因而,所有 worker 进程都共享它,在 ngx_process_events_and_timers 函数中我们可以看到如下的代码:


/*尝试锁accept mutex,只有成功获取锁的进程,才会将listen   套接字放入epoll中。因此,这就保证了只有一个进程拥有   监听套接口,故所有进程阻塞在epoll_wait时,不会出现惊群现象。 */  //这里的ngx_trylock_accept_mutex函数中,如果顺利的获取了锁,那么它会将监听端口注册到当前worker进程的epoll当中if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {                return;            }
复制代码


函数 ngx_trylock_accept_mutex 用于尝试获取 ngx_accept_mutex 锁,如果获取了的话,那么就将 listening 加入到 epoll 当中,我们可以来看这个函数:


//尝试获取锁,如果获取了锁,那么还要将当前监听端口全部注册到当前 worker 进程的 epoll 当中去ngx_int_tngx_trylock_accept_mutex(ngx_cycle_t *cycle){    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  //尝试获取互斥锁
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex locked"); //如果本来已经获得锁,则直接返回Ok if (ngx_accept_mutex_held && ngx_accept_events == 0 && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) { return NGX_OK; } //到达这里,说明重新获得锁成功,因此需要打开被关闭的listening句柄,调用ngx_enable_accept_events函数,将监听端口注册到当前worker进程的epoll当中去 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);//这里表示的是以前曾经获取过,但是这次却获取失败了,那么需要将监听端口从当前的worker进程的epoll当中移除,调用的是ngx_disable_accept_events函数 if (ngx_accept_mutex_held) { if (ngx_disable_accept_events(cycle) == NGX_ERROR) { return NGX_ERROR; }
ngx_accept_mutex_held = 0; //表示当前并没有获取锁 }
return NGX_OK;}
复制代码


调用 ngx_shmtx_trylock 来尝试获取 ngx_accept_mutex 锁,如果获取了的话,在判断在上次循环中是否已经获取了锁,如果获取了,那么 listening 就已经在当前 worker 进程的 epoll 当中了,否则的话就调用 ngx_enable_accept_events 函数来讲 listening 加入到 epoll 当中,并要对变量 ngx_accept_mutex_held 赋值,表示已经获取了锁。如果没有获取到锁的话,还要判断上次是否已经获取了锁,如果上次获取了的话,那么还要调用 ngx_disable_accept_events 函数,将 listening 从 epoll 当中移除。


就这样就可以保证所有的 worker 进程中就只有一个 worker 将 listening 放入到了 epoll 当中,也就避免了惊群的发生。好了,就讲完了(当然我只讲了有原子操作的情况下的实现方案,并没有讲文件锁的实现方案,但是其实也都大同小异)。


gcc 原子操作


nginx 的原子锁主要基于 gcc 原子操作接口实现,详细的接口定义如下:


type __sync_fetch_and_add (type *ptr, type value, ...)

type __sync_fetch_and_sub (type *ptr, type value, ...)

type __sync_fetch_and_or (type *ptr, type value, ...)

type __sync_fetch_and_and (type *ptr, type value, ...)

type __sync_fetch_and_xor (type *ptr, type value, ...)

type __sync_fetch_and_nand (type *ptr, type value, ...)


type __sync_add_and_fetch (type *ptr, type value, ...)

type __sync_sub_and_fetch (type *ptr, type value, ...)

type __sync_or_and_fetch (type *ptr, type value, ...)

type __sync_and_and_fetch (type *ptr, type value, ...)

type __sync_xor_and_fetch (type *ptr, type value, ...)

type __sync_nand_and_fetch (type *ptr, type value, ...)


nginx 原子锁自实现程序


主函数代码如下所示,你可以通过修改 #define ATOMIC_LOCK,来测试不使用原子锁和使用的区别,详细代码如下:


#include <stdio.h>#include <stdlib.h>#include <pthread.h>#include <unistd.h>#include <time.h>#include <sys/wait.h>#include<sys/mman.h>#include<sys/types.h>#include<fcntl.h>#include<string.h>
#include "timer.h"#define ATOMIC_LOCKint* mutex = 0;typedef struct{ char name[4]; int count;}data_info;data_info* data;
void test_atomic_lock_func(){ int i = 0; pid_t pid = getpid(); for(i = 0; i < 10000; i++) {#ifdef ATOMIC_LOCK while(!(__sync_bool_compare_and_swap (mutex,0, pid))) usleep(1);#endif // 临界区 data->count++;#ifdef ATOMIC_LOCK __sync_bool_compare_and_swap (mutex, pid, 0);#endif }
exit(0);}
int main(int argc, const char *argv[]){ Timer timer; timer.Start(); int process_count = 4; int i = 0; pid_t pid; int stat; mutex = (int*)mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); data = (data_info*)mmap(NULL, sizeof(data_info), PROT_READ| PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
for(i = 0; i < process_count; i++) { pid = fork(); if (pid == 0) { // 子进程 test_atomic_lock_func(); } }
// 等待所有进程执行完毕 while(wait(NULL)!=-1) usleep(1);
// 计算时间 timer.Stop(); timer.Cost_time(); printf("结果:count = %d\n", (data->count));
return 0;}
复制代码


github 完整代码:


https://github.com/raoping2017/atomic_lock.git


Linux、C/C++技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、大厂面试题、和热门技术教学视频资料共享在里面(包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK 等等.),有需要的可以自行添加哦!~




以上不足的地方欢迎指出讨论。


用户头像

赖猫

关注

还未添加个人签名 2020.11.28 加入

纸上得来终觉浅,绝知此事要躬行

评论

发布
暂无评论
nginx 惊群处理及原子锁技术