线程池出来时让人眼前一亮,随笔写了一篇 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倍