https://zhuanlan.zhihu.com/p/616500765
 
 

Nginx通过使用多路复用IO(如Linux的epoll、FreeBSD的kqueue等)技术很好的解决了c10k问题,但前提是Nginx的请求不能有阻塞操作,否则将会导致整个Nginx进程停止服务。

但很多时候阻塞操作是不可避免的,例如客户端请求静态文件时,由于磁盘IO可能会导致进程阻塞,所以将会导致Nginx的性能下降。为了解决这个问题,Nginx在1.7.11版本中实现了线程池机制。

下面我们将会分析Nginx是怎么通过线程池来解决阻塞操作问题。

启用线程池功能

要使用线程池功能,首先需要在配置文件中添加如下配置项:

location / {

    root /html;

    thread_pool default threads=32 max_queue=65536;

    aio threads=default;

}

上面定义了一个名为“default”,包含32个线程,任务队列最多支持65536个请求的线程池。如果任务队列过载,Nginx将输出如下错误日志并拒绝请求:

thread pool "default" queue overflow: N tasks waiting

如果出现上面的错误,说明线程池的负载很高,这是可以通过添加线程数来解决这个问题。当达到机器的最高处理能力之后,增加线程数并不能改善这个问题。

一切从“源”开始

下面主要通过剖析Nginx的源码来了解线程池机制实现原理。现在先来了解Nginx线程池的两个重要数据结构ngx_thread_pool_t和ngx_thread_task_t。

ngx_thread_pool_t结构体

struct ngx_thread_pool_s {

ngx_thread_mutex_t mtx;

ngx_thread_pool_queue_t queue;

ngx_int_t waiting;

ngx_thread_cond_t cond;

ngx_log_t *log;

ngx_str_t name;

ngx_uint_t threads;

ngx_int_t max_queue;

u_char *file;

ngx_uint_t line;

};

下面解释下每个字段的用途:

  1. mtx: 互斥锁,用于锁定任务队列,避免竞争状态。
  2. queue: 任务队列。
  3. waiting: 有多少个任务正在等待处理。
  4. cond: 用于通知线程池有任务需要处理。
  5. name: 线程池名称。
  6. threads: 线程池由多少个线程组成(线程数)。
  7. max_queue: 线程池最大能处理的任务数。

ngx_thread_task_t结构体

struct ngx_thread_task_s {

    ngx_thread_task_t   *next;

    ngx_uint_t           id;

    void                *ctx;

    void               (*handler)(void *data, ngx_log_t *log);

    ngx_event_t          event;

};

下面解释下每个字段的用途:

  1. next: 指向下一个任务。
  2. id: 任务ID。
  3. ctx: 任务的上下文。
  4. handler: 处理任务的函数句柄。
  5. event: 跟任务关联的事件对象(当线程池处理成任务之后将会由主线程调用event对象的handler回调函数)。

线程池初始化

下面介绍下线程池的初始化过程。

在Nginx启动的时候,首先会调用ngx_thread_pool_init_worker()函数来初始化线程池。ngx_thread_pool_init_worker()函数最终会调用ngx_thread_pool_init(),源码如下:

static ngx_int_t

ngx_thread_pool_init(ngx_thread_pool_t *tp, ngx_log_t *log, ngx_pool_t *pool)

{

    ...

    for (n = 0; n < tp->threads; n++) {

        err = pthread_create(&tid, &attr, ngx_thread_pool_cycle, tp);

        if (err) {

            ngx_log_error(NGX_LOG_ALERT, log, err,

                          "pthread_create() failed");

            return NGX_ERROR;

        }

}

...

    return NGX_OK;

}

ngx_thread_pool_init()最终调用pthread_create()函数创建线程池中的工作线程,工作线程会从ngx_thread_pool_cycle()函数开始执行。

ngx_thread_pool_cycle()函数源码如下:

static void *

ngx_thread_pool_cycle(void *data)

{

    ...

    for ( ;; ) {

        if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) {

            return NULL;

        }

        tp->waiting--;

        while (tp->queue.first == NULL) {

            if (ngx_thread_cond_wait(&tp->cond, &tp->mtx, tp->log)

                != NGX_OK)

            {

                (void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);

                return NULL;

            }

        }

        // 获取一个任务对象

        task = tp->queue.first;

        tp->queue.first = task->next;

        if (tp->queue.first == NULL) {

            tp->queue.last = &tp->queue.first;

        }

        if (ngx_thread_mutex_unlock(&tp->mtx, tp->log) != NGX_OK) {

            return NULL;

        }

        // 处理任务

        task->handler(task->ctx, tp->log);

        task->next = NULL;

        ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);

        // 把处理完的任务放置到完成队列中

        *ngx_thread_pool_done.last = task;

        ngx_thread_pool_done.last = &task->next;

        ngx_unlock(&ngx_thread_pool_done_lock);

        (void) ngx_notify(ngx_thread_pool_handler); // 通知主线程

    }

}

ngx_thread_pool_cycle()函数的主要工作是从待处理的任务队列中获取一个任务,然后调用任务对象的handler()函数处理任务,完成后把任务放置到完成队列中,并通过ngx_notify()通知主线程。

添加任务到任务队列

通过上面的分析,我们知道了线程池是怎么从任务队列获取任务并处理。但任务队列的任务从哪里来的呢?因为Nginx的使命是处理客户端请求,所以可以知道任务是通过客户端请求产生的。也就是说,任务是主线程创建的(主线程负责处理客户端请求)。

主线程通过ngx_thread_task_post()函数向任务队列中添加一个任务,代码如下:

ngx_int_t

ngx_thread_task_post(ngx_thread_pool_t *tp, ngx_thread_task_t *task)

{

...

if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) {

    return NGX_ERROR;

}

    // 通知线程池有任务需要处理

    if (ngx_thread_cond_signal(&tp->cond, tp->log) != NGX_OK) {

        (void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);

        return NGX_ERROR;

    }

    // 把任务添加到任务队列中

    *tp->queue.last = task;

    tp->queue.last = &task->next;

    tp->waiting++;

(void) ngx_thread_mutex_unlock(&tp->mtx, tp->log);

    return NGX_OK;

}

ngx_thread_task_post()函数首先调用ngx_thread_cond_signal()通知线程池的线程有任务需要处理,然后把任务添加到任务队列中。可能有人会问,先通知线程池在添加任务到任务队列中会不会有顺序问题。其实这样做是没问题的,这是因为只要主线程不调用ngx_thread_mutex_unlock()把互斥锁解开,线程池中的工作线程是不会从ngx_thread_cond_wait()返回的。

收尾工作

当线程池把任务处理完后会把其放置到完成队列中(ngx_thread_pool_done),然后调用ngx_notify()通知主线程有任务完成了。主线程收到通知后,会在事件模块中进行收尾工作:调用task.event.handler()。task.event.handler由任务创建者设置,例如在ngx_http_copy_filter模块的ngx_http_copy_thread_handler()函数:

static ngx_int_t

ngx_http_copy_thread_handler(ngx_thread_task_t *task, ngx_file_t *file)

{

    ...

    if (tp == NULL) {

        if (ngx_http_complex_value(r, clcf->thread_pool_value, &name)

            != NGX_OK)

        {

            return NGX_ERROR;

        }

        tp = ngx_thread_pool_get((ngx_cycle_t *) ngx_cycle, &name);

    }

task->event.data = r;

// 设置event的回调函数

    task->event.handler = ngx_http_copy_thread_event_handler;

    if (ngx_thread_task_post(tp, task) != NGX_OK) {

        return NGX_ERROR;

    }

    r->main->blocked++;

    r->aio = 1;

    return NGX_OK;

}

task.event.handler被设置为ngx_http_copy_thread_event_handler,就是说当任务处理完成后,主线程将会调用ngx_http_copy_thread_event_handler来进行收尾工作。

哪些操作会使用线程池

那么哪些操作会使用线程池去处理。一般来说,磁盘IO会使用线程池来处理。在ngx_http_copy_filter模块中,会调用ngx_thread_read()读取文件的内容(当启用了线程池时),而ngx_thread_read()会把读取文件内容的操作让线程池去处理。ngx_thread_read()代码如下:

ssize_t

ngx_thread_read(ngx_thread_task_t **taskp, ngx_file_t *file, u_char *buf,

    size_t size, off_t offset, ngx_pool_t *pool)

{

    ...

    task = *taskp;

    if (task == NULL) {

        task = ngx_thread_task_alloc(pool, sizeof(ngx_thread_read_ctx_t));

        if (task == NULL) {

            return NGX_ERROR;

        }

        task->handler = ngx_thread_read_handler;

        *taskp = task;

    }

    ctx = task->ctx;

    ...

    ctx->fd = file->fd;

    ctx->buf = buf;

    ctx->size = size;

    ctx->offset = offset;

    if (file->thread_handler(task, file) != NGX_OK) {

        return NGX_ERROR;

    }

    return NGX_AGAIN;

}

从上面的代码看到,task的handler被设置为ngx_thread_read_handler,也就是说在线程池中将会调用ngx_thread_read_handler()去读取文件内容。而file->thread_handler()将会调用ngx_thread_task_post(),前面已经分析过,ngx_thread_task_post()会把任务添加到任务队列中。

图解

最后用一张图来解释Nginx线程池机制的原理吧。

原文作者:Linux内核那些事
原文链接:Nginx线程池浅析

首页 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛

[转帖]一文浅析Nginx线程池!的更多相关文章

  1. 线程池机制使nginx性能提高9倍

    原文标题:Thread Pools in NGINX Boost Performance 9x! 原文官方地址:https://www.nginx.com/blog/thread-pools-boos ...

  2. NGINX引入线程池 性能提升9倍

    1. 引言 正如我们所知,NGINX采用了异步.事件驱动的方法来处理连接.这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作进程中处理多个连接和请求.为 ...

  3. nginx性能优化之线程池

    默认情况下,nginx的work process按照顺序一个个处理http请求,因此如果后台处理时间较长,则work process会长时间等待IO状态,因此限制并发性.如下所示: 所以,对于可能存在 ...

  4. Nginx 引入线程池,提升 9 倍性能

    转载:http://blog.csdn.net/wuliusir/article/details/50760357 众所周知,NGINX 采用异步.事件驱动的方式处理连接.意味着无需对每个请求创建专门 ...

  5. Nginx 的线程池与性能剖析

    http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt158   正如我们所知,NGINX采用了异步.事件驱动的方法来处理连接.这种处理方 ...

  6. nginx源码分析——线程池

    源码: nginx 1.13.0-release   一.前言      nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影 ...

  7. Nginx 学习笔记(六)引入线程池 性能提升9倍

    原文地址:https://www.cnblogs.com/shitoufengkuang/p/4910333.html 一.前言 1.Nignx版本:1.7.11 以上 2.NGINX采用了异步.事件 ...

  8. Nginx 的线程池与性能剖析【转载】

    正如我们所知,NGINX采用了异步.事件驱动的方法来处理连接.这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作进程中处理多个连接和请求.为此,NGIN ...

  9. nginx源码分析线程池详解

    nginx源码分析线程池详解 一.前言     nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,n ...

  10. 高并发之——不得不说的线程池与ThreadPoolExecutor类浅析

    一.抛砖引玉 既然Java中支持以多线程的方式来执行相应的任务,但为什么在JDK1.5中又提供了线程池技术呢?这个问题大家自行脑补,多动脑,肯定没坏处,哈哈哈... 说起Java中的线程池技术,在很多 ...

随机推荐

  1. gh-pages在线演示踩的坑

    git在线演示 1.新建一个gh-pages分支 2.打包好的dist上传到分支里 3.访问:https://[用户名].github.io/[项目名]/dist ( 会自动访问dist下的index ...

  2. 干掉PPT!现场编码的职级晋升答辩你参加过么?

    摘要:研发讲究的是真本事,是骡子是马咱们还得代码上见真章. 最近这小半年的时间,凭借对各种API的巧妙应用,我从一个差点被淘汰的"前浪"变成了公司人人尊敬的技术委员会副主席,工作思 ...

  3. Python 没有函数重载?如何用装饰器实现函数重载?

    摘要:Python 不支持函数重载.当我们定义了多个同名的函数时,后面的函数总是会覆盖前面的函数,因此,在一个命名空间中,每个函数名仅会有一个登记项(entry). 本文分享自华为云社区<为什么 ...

  4. 关于单元测试的那些事儿,Mockito 都能帮你解决

    摘要:相信每一个程序猿在写Unit Test的时候都会碰到一些令人头疼的问题:如何测试一个rest接口:如何测试一个包含客户端调用服务端的复杂方法:如何测试一个包含从数据库读取数据的复杂方法...这些 ...

  5. 你知道,什么时候用Vue计算属性吗?

    摘要:当我们处理复杂逻辑时,都应该使用计算属性. 本文分享自华为云社区<深入理解计算属性,知道什么时候该用Vue计算属性吗?>,作者: 前端老实人 . 计算属性 有些时候,我们在模板中放入 ...

  6. 漏洞评分高达9.8分!Text4Shell 会是下一个 Log4Shell吗?

    在过去的几天里,Apache Commons Text 库中一个名为 Text4Shell 的新漏洞引起很大的轰动,该漏洞存在于 Apache Commons Text 1.5到1.9版本中.此警报于 ...

  7. Html 表格 在线转 Markdown

    复制 HTML Table F12 查看网页源代码 Html to markdown 在线转换 https://tableconvert.com/html-to-markdown 复制 Markdow ...

  8. Jenkins Pipeline 流水线 - 添加节点 使用代理

    Jenkins 安装在 Windows 上 Docker 在Linux 上 流程 将 Docker 在 Jenkins 节点中维护 Pipeline 中指定某些阶段使用哪个节点 添加节点 Checki ...

  9. 创建一个简单的Docker镜像

    1. 创建 Dockerfile 文件.index.html测试页面 [root@localhost docker]# vi Dockerfile FROM nginx:1.17.6 #基于 ngin ...

  10. 接口文档 token发展史 jwt介绍和原理 drf-jwt快速使用

    目录 昨日回顾 认证 权限 频率 全局异常处理 接口文档 接口文档编写 drf自动生成接口文档 cookies-session-token发展史 jwt介绍和原理 jwt的构成 base64的编码和解 ...