nginx源码分析
nginx-1.11.1
参考书籍《深入理解nginx模块开发与架构解析》
Nginx的惊群处理与负载均衡概述
当Nginx工作在master/worker模式下时,就会涉及到多个子进程共同接受请求并处理请求,由于在早期版本的Linux内核中,当多个子进程监听同一个端口的时候,此时当新连接请求进入的时候,多个子进程会被唤醒,但是能处理的新进的连接就这有一个子进程处理,导致其他被唤醒的子进程接受请求失败,导致新增了唤醒子进程的系统开销进而影响服务器处理请求的性能(主要是accept建立连接,当前Linux内核已经处理了该问题);由于Nginx使用的epoll模型中在每个子进程中,都重新初始化一个epfd,但是监听的却是同一个socket,会导致监听的套接字在每个子进程会被epoll唤醒,Nginx通过了锁的方式进行了处理(有关epoll惊群的内容大家可以自行查阅资料)。由于多个子进程之间监听同一个端口时,会造成各个子进程之间的处理请求会不一样,所以Nginx也采取了负载均衡的策略同时也通过锁的机制来在应用层保证同一个请求被一个子进程处理。
惊群处理与负载均衡
Nginx中为了保证在接受请求与epoll处理过程中,使用了锁来规避这个问题,本文我们就在代码中去查看相关内容。
请求与时间处理函数
在前文的分析中,子进程处理请求的核心函数如下;
ngx_process_events_and_timers(cycle);
处理通知时间与定时器相关操作。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
if (ngx_timer_resolution) { // 设置了时间精度
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
timer = ngx_event_find_timer(); // 查找定时器任务找到最近的定时器
flags = NGX_UPDATE_TIME;
#if (NGX_WIN32)
/* handle signals from master in case of network inactivity */
if (timer == NGX_TIMER_INFINITE || timer > 500) {
timer = 500;
}
#endif
}
if (ngx_use_accept_mutex) { // 是否使用accept_mutex
if (ngx_accept_disabled > 0) { // 检查当前进程是否可以继续处理请求如果大于0则可以继续处理
ngx_accept_disabled--; // 可用计数减一
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { // 当不能获取连接的时候就尝试获取锁
return; // 如果报错则返回
}
if (ngx_accept_mutex_held) { // 如果获取了锁
flags |= NGX_POST_EVENTS; // 添加接受请求标志位
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay; // 获取当前最小的延迟时间
}
}
}
}
delta = ngx_current_msec;
(void) ngx_process_events(cycle, timer, flags); // 处理请求
delta = ngx_current_msec - delta;
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"timer delta: %M", delta);
ngx_event_process_posted(cycle, &ngx_posted_accept_events); // 处理accept请求
if (ngx_accept_mutex_held) { // 检查是否获取锁
ngx_shmtx_unlock(&ngx_accept_mutex); // 释放锁
}
if (delta) {
ngx_event_expire_timers(); // 处理过期请求
}
ngx_event_process_posted(cycle, &ngx_posted_events); // 处理正常的读写事件请求
}
此时,从该代码可知查看ngx_trylock_accept_mutex,该函数如下;
ngx_int_t
ngx_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");
if (ngx_accept_mutex_held && ngx_accept_events == 0) { // 如果已经获取了锁则直接返回
return NGX_OK;
}
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; // 获取锁标志置1
return NGX_OK; // 返回成功
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held); // 获取锁失败
if (ngx_accept_mutex_held) { // 如果获取锁标志
if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) { // 删除读标志
return NGX_ERROR;
}
ngx_accept_mutex_held = 0; // 获取锁标志重置
}
return NGX_OK; // 返回成功
}
static ngx_int_t
ngx_enable_accept_events(ngx_cycle_t *cycle)
{
ngx_uint_t i;
ngx_listening_t *ls;
ngx_connection_t *c;
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) { // 遍历监听列表
c = ls[i].connection;
if (c == NULL || c->read->active) { // 如果为空或者读已经使用则查找下一个
continue;
}
if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) { // 添加读事件
return NGX_ERROR;
}
}
return NGX_OK;
}
static ngx_int_t
ngx_disable_accept_events(ngx_cycle_t *cycle, ngx_uint_t all)
{
ngx_uint_t i;
ngx_listening_t *ls;
ngx_connection_t *c;
ls = cycle->listening.elts; // 遍历监听列表
for (i = 0; i < cycle->listening.nelts; i++) {
c = ls[i].connection;
if (c == NULL || !c->read->active) { // 如果为空或者不为读状态的连接
continue;
}
#if (NGX_HAVE_REUSEPORT)
/*
* do not disable accept on worker's own sockets
* when disabling accept events due to accept mutex
*/
if (ls[i].reuseport && !all) {
continue;
}
#endif
if (ngx_del_event(c->read, NGX_READ_EVENT, NGX_DISABLE_EVENT) // 删除监听的读事件
== NGX_ERROR)
{
return NGX_ERROR;
}
}
return NGX_OK;
}
检查是否获取锁的状态,并根据获取锁添加读事件,该代码其中也说明了Nginx的简单负载均衡的调度模式;
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
该行代码就是判断当前的子进程连接的数量达到了7/8的时候,此时就不会让该子进程去注册读事件去处理连接请求,而是让该进程处理已经连接进来的请求的读写时间。由此可知Nginx的worker的负载均衡与accept的惊群的处理过程。
总结
本文只是简单的概述了Nginx有关子进程的负载均衡与惊群问题的简单实现,主要通过检查已经连接的请求数量如果达到了7/8则该子进程就不再处理新连接的请求,通过锁来实现一个请求来处理连接接入的请求从而规避了惊群的问题。由于本人才疏学浅,如有错误请批评指正。