nginx源码分析之线程池

线程池出来时让人眼前一亮,随笔写了一篇 nginx另一个性能杀手锏-线程池。想让更多人关注这个例好。现在想来有点偏,改为'nginx开发杀手锏-线程池'更贴切点。

来自大神的指点
agentzh:你这是滥用 nginx 的线程池。线程池的引入是为了处理不得不阻塞的 I/O,比如文件 I/O. 将之用于本可以 100% 非阻塞的网络 I/O 是巨大的退步,让 nginx 退化为 Apache worker fpm 了,靠 OS 线程来拼并发,彻底告别 C10K 了。nginx 官方介绍此特性的博客也是拿 disk I/O 来举例的。

1、开发nginx的挑战
虽然nginx的源码非常精致,但是不得不说开发nginx很有挑战性,越想更大程度上定制自己的模块,越需要对nginx的每个细节了解颇深。而且跟第三方服务通讯是非常需要的业务,你必须熟悉upstream,需要关注如何构造请求,如何解析响应,处理错误等。而且大部分情况下异步第三方服务处理的库不够丰富,反之同步的库足够丰富稳定,一般开源的服务器都提供c api开发库,比如mysql, redis, mongodb等等。

2、线程池的出现
nginx的异步处理方式让它的性能发挥到极致,但如果折衷下稍微在性能上退一点,但获得更大方便的开发,这是很值得研究和投入的。线程池可以让你按同步方式去实现你的业务,当然线程池可以应用的地方更多,但是开发这块是我们要关注的。因此,线程池是对那些熟悉c,又不需要非常熟悉nginx源码的开发者是个极大利好消息。

3、内部实现
现在切入源码分析,看下其内部是如何实现的。首先,您可以定义多个线程池,每个线程池有自己的名称,在nginx启动时将根据线程池配置信息处理线程。
src/core/ngx_thread_pool.c

static ngx_int_t
ngx_thread_pool_init_worker(ngx_cycle_t *cycle)
{
    ......
    tpp = tcf->pools.elts;
    for (i = 0; i < tcf->pools.nelts; i++) {
        if (ngx_thread_pool_init(tpp[i], cycle->log, cycle->pool) != NGX_OK) {
            return NGX_ERROR;

        }    
    }
    return NGX_OK;
}

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);
        ......
    }
    ......
    return NGX_OK;
}


static void *
ngx_thread_pool_cycle(void *data)
{
    ......
    for ( ;; ) {
        ......
        ngx_thread_cond_wait(&tp->cond, &tp->mtx, tp->log);
        ......
        task->handler(task->ctx, tp->log); /* 执行任务处理,可以是阻塞操作 */
        .....
        (void) ngx_notify(ngx_thread_pool_handler); /* 让主线程得到回调 */
    }
}

在nginx工作进程启动时,被创建的线程也进入预备状态,这个很类似epoll_wait,但线程是在条件满足时进入wait的代码流程的。所以nginx已经将线程的外部流程做了足够的工作,让你可以只关注跑在线程里的开发。接下来了解下线程开发需要知道的结构体。每个线程池有一个任务队列,挂着你添加的任务,任务的结构体为:

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;  
};

其实非常的简单,你只需要关注两个 handler和event:
handler是跑在线程里的处理,即上面的task->handler。
event是用于回调给主线程的,所以还需要注册一个event->handler,这个是nginx内部的eventfd机制处理的。

4、开发实践
以nginx-http-mysql-module为例解释下如何开发:

a、添加任务
tp = mlcf->thread_pool;
task = ngx_thread_task_alloc(r->pool, 0);
if (task == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

task->ctx = r;
task->handler = ngx_http_mysql_thread_handler;

task->event.data = r;
task->event.handler = ngx_http_mysql_thread_event_handler;

if (ngx_thread_task_post(tp, task) != NGX_OK) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

b、处理线程任务
ctx = ngx_http_get_module_ctx(r, ngx_http_mysql_module);
rc = ngx_http_mysql_run(r);
if (rc == NGX_ERROR) {
    ctx->err = 1;
}

这里需要注意的是,在线程里执行的代码不要处理任务错误,将执行结果放到ctx即可。

c、处理回调
r = ev->data;
ctx = ngx_http_get_module_ctx(r, ngx_http_mysql_module);
if (ctx->err) {
    ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
    return;
}

rc = ngx_http_mysql_output(r);
ngx_http_finalize_request(r, rc);

开发非常简单吧,只需要了解简单的nginx处理,大部分业务开发可以利用第三方现成的api库。

5、想法需要更多碰撞

线程池可以发挥更多作用,比如可以把连接放到线程池里。nginx的异步加lua的协程是个非常好的组合,现在有了线程池后,线程池加协程将是另一个选择。总而言之,如果在保证性能的情况下,让nginx开发变得非常简单,这是非常利好的消息。真诚欢迎更多关于这个话题的见解。

推荐阅读:
nginx另一个性能杀手锏-线程池
NGINX使用线程池提升性能9x倍

你可能感兴趣的:(nginx源码分析之线程池)