nginx模块开发的一些经验
fastdfs有nginx模块,在上面做一些缩略图缓存的功能。我当时总是觉得lua-nginx这种方式太重了,因此就入了nginx模块开发这个坑。
nginx模块开发的资料网上还是比较多的,《nginx模块开发与架构解析》这一本就不错,推荐入手实体书。我看PDF看的有点烦躁,因为很多东西要来回对照着看。
很多基础东西书里写的很细,就不谈了,就说说自己开发时候的过程和遇到的坑吧。
nginx模块是存在http module和http filter module的。
http module依次处理,最后输出内容,输出的内容再由http filter module依次过滤内容输出。
我的缩略图方案就是request->fastdfs-nginx-module->nginx-image-filter-module这个顺序进行的
因此cache模块应当改写在image-filter内
look up cache没什么好看的,中规中矩的本地目录检索。看下save cache吧。
save cache的AIO实现
nginx本身因为不支持文件的上传,因此没有设计异步的write方式。只有异步的read方式。
把异步write融入nginx本身的框架废了不少手脚。
extern int ngx_eventfd;
extern aio_context_t ngx_aio_ctx;
static void ngx_file_aio_event_handler(ngx_event_t *ev);
static int
(aio_context_t ctx, long n, struct iocb **paiocb)
io_submit{
return syscall(SYS_io_submit, ctx, n, paiocb);
}
ssize_t
(ngx_file_t *file, u_char *buf, size_t size, off_t offset,
ngx_file_aio_write*pool)
ngx_pool_t {
;
ngx_err_t errstruct iocb *piocb[1];
*ev;
ngx_event_t *aio;
ngx_event_aio_t
if (!ngx_file_aio) {
return ngx_write_file(file, buf, size, offset);
}
= file->aio;
aio
if (aio == NULL) {
= ngx_pcalloc(pool, sizeof(ngx_event_aio_t));
aio if (aio == NULL) {
return NGX_ERROR;
}
->file = file;
aio->fd = file->fd;
aio->event.data = aio;
aio->event.ready = 1;
aio->event.log = file->log;
aio->aio = aio;
file}
= &aio->event;
ev
if (!ev->ready) {
(NGX_LOG_ALERT, file->log, 0,
ngx_log_error"second aio post for \"%V\"", &file->name);
return NGX_AGAIN;
}
(NGX_LOG_DEBUG_CORE, file->log, 0,
ngx_log_debug4"aio complete:%d @%O:%z %V",
->complete, offset, size, &file->name);
ev
if (ev->complete) {
->active = 0;
ev->complete = 0;
ev
if (aio->res >= 0) {
(0);
ngx_set_errnoreturn aio->res;
}
(-aio->res);
ngx_set_errno
(NGX_LOG_CRIT, file->log, ngx_errno,
ngx_log_error"aio write \"%s\" failed", file->name.data);
return NGX_ERROR;
}
(&aio->aiocb, sizeof(struct iocb));
ngx_memzero
->aiocb.aio_data = (uint64_t) (uintptr_t) ev;
aio->aiocb.aio_lio_opcode = IOCB_CMD_PWRITE;
aio->aiocb.aio_fildes = file->fd;
aio->aiocb.aio_buf = (uint64_t) (uintptr_t) buf;
aio->aiocb.aio_nbytes = size;
aio->aiocb.aio_offset = offset;
aio->aiocb.aio_flags = IOCB_FLAG_RESFD;
aio->aiocb.aio_resfd = ngx_eventfd;
aio
->handler = ngx_file_aio_event_handler;
ev
[0] = &aio->aiocb;
piocb
if (io_submit(ngx_aio_ctx, 1, piocb) == 1) {
->active = 1;
ev->ready = 0;
ev->complete = 0;
ev
return NGX_AGAIN;
}
= ngx_errno;
err
if (err == NGX_EAGAIN) {
return ngx_write_file(file, buf, size, offset);
}
(NGX_LOG_CRIT, file->log, err,
ngx_log_error"io_submit(\"%V\") failed", &file->name);
if (err == NGX_ENOSYS) {
= 0;
ngx_file_aio return ngx_write_file(file, buf, size, offset);
}
return NGX_ERROR;
}
static void
(ngx_event_t *ev)
ngx_file_aio_event_handler{
*aio;
ngx_event_aio_t
= ev->data;
aio
(NGX_LOG_DEBUG_CORE, ev->log, 0,
ngx_log_debug2"aio event handler fd:%d %V", aio->fd, &aio->file->name);
->handler(ev);
aio}
struct image_cache_s {
[128];
u_char file_nameint name_len;
;
ngx_file_t file*buf;
u_char ;
ngx_log_t log;
ngx_open_file_t log_file;
ngx_event_aio_t aio};
typedef struct image_cache_s image_cache_t;
static void
(ngx_event_t *ev)
ngx_http_save_cache_file_handler{
*im;
image_cache_t [128];
u_char new_name= ((ngx_event_aio_t *)ev->data)->data;
im
if(ngx_close_file(im->file.fd) == NGX_FILE_ERROR){
(NGX_LOG_ALERT,ev->log,ngx_errno,ngx_close_file_n " \"%s\" failed",im->file_name);
ngx_log_error}
(new_name,128);
ngx_memzero(new_name,im->file_name,im->name_len);
ngx_memcpyif(ngx_rename_file(im->file_name,new_name) == NGX_FILE_ERROR){
(NGX_LOG_ERR, ev->log, ngx_errno, "\"%s\"",im->file_name);
ngx_log_error}
(NGX_LOG_ERR, ev->log, 0, "\"%s\",\"%s\"",im->file_name,new_name);
ngx_log_error(im->buf);
free(im);
free}
static ngx_int_t
(ngx_http_request_t *r, ngx_chain_t *in,ngx_chain_t *out)
ngx_http_save_cache_file{
*uri_file_name;
u_char size_t file_len;
int rand_stamp;
*im;
image_cache_t ;
ngx_int_t result
= calloc(1,sizeof(image_cache_t));
im if(im == NULL){
(NGX_LOG_ERR, r->connection->log, 0, "\"%d\" :can't malloc space",__LINE__);
ngx_log_errorreturn NGX_ERROR;
}
/*uri_start plus 12 than point to file_name*/
->name_len = r->uri_end - r->uri_start - 12;
im= r->uri_start + 12;
uri_file_name
/*doesn't exist temp file*/
do{
= rand()%1000000;
rand_stamp ((char *)im->file_name,128,"/tmp/image_cache/%.*s-%d",im->name_len,uri_file_name,rand_stamp);
snprintf}while(access((char *)im->file_name,F_OK) == 0);
/*strlen("/tmp/image_cache/") = 17*/
//ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "\"%s\"",im->file_name);
->name_len += 17;
im
->file.fd = ngx_open_file(im->file_name, NGX_FILE_WRONLY|NGX_FILE_NONBLOCK, NGX_FILE_CREATE_OR_OPEN,NGX_FILE_DEFAULT_ACCESS);
im
if(im->file.fd <= 0){
(NGX_LOG_ERR, r->connection->log, ngx_errno, "can't open file \"%s\"",im->file_name);
ngx_log_error(im);
freereturn NGX_ERROR;
}
= out->buf->last - out->buf->pos;
file_len ->buf = malloc(file_len);
imif(im->buf == NULL){
(im);
free(NGX_LOG_ERR, r->connection->log, ngx_errno, "\"%d\" :can't malloc space",__LINE__);
ngx_log_errorreturn NGX_ERROR;
}
(im->buf,out->buf->pos,file_len);
ngx_memcpy
->file.aio = &im->aio;
im
->log.file = &im->log_file;
im->log.log_level = NGX_LOG_DEBUG;
im->log_file.fd = r->connection->log->file->fd;
im
->file.aio->file = &im->file;
im->file.aio->fd = im->file.fd;
im->file.aio->event.data = &im->aio;
im->file.aio->event.ready = 1;
im->file.aio->data = im;
im->file.log = &im->log;
im->file.aio->event.log = im->file.log;
im#if (NGX_HAVE_AIO_SENDFILE)
->file.aio->last_offset = -1;
im#endif
->file.aio->handler = ngx_http_save_cache_file_handler;
imif((result = ngx_file_aio_write(&im->file,im->buf,file_len,0,r->pool)) != NGX_AGAIN){
(im->buf);
free(im);
freereturn result;
}
return NGX_OK;
}
首先会检索是否存在temp file,这是因为存储时不是直接存储原名,而是存储原名+一定后缀的temp file
而后创建相应的文件结构填充数据。然后创建AIO结构,并且调用改写自ngx_file_aio_read的ngx_file_aio_write函数,并且绑定异步IO完成后的回调函数ngx_http_save_cache_file_handler
就算是大功告成了。
而这个异步IO方式真的完美融入了nginx的框架了吗?来探寻下nginx的异步IO框架吧。
ngx_epoll_init() -> ngx_epoll_aio_init()
static void
(ngx_cycle_t *cycle, ngx_epoll_conf_t *epcf)
ngx_epoll_aio_init{
int n;
struct epoll_event ee;
= syscall(SYS_eventfd, 0);
ngx_eventfd ...
= 1;
n
if (ioctl(ngx_eventfd, FIONBIO, &n) == -1) {
...
if (io_setup(epcf->aio_requests, &ngx_aio_ctx) == -1) {
...
.data = &ngx_eventfd_conn;
ngx_eventfd_event.handler = ngx_epoll_eventfd_handler;
ngx_eventfd_event.log = cycle->log;
ngx_eventfd_event.active = 1;
ngx_eventfd_event.fd = ngx_eventfd;
ngx_eventfd_conn.read = &ngx_eventfd_event;
ngx_eventfd_conn.log = cycle->log;
ngx_eventfd_conn
.events = EPOLLIN|EPOLLET;
ee.data.ptr = &ngx_eventfd_conn;
ee
if (epoll_ctl(ep, EPOLL_CTL_ADD, ngx_eventfd, &ee) != -1) {
return;
}
这里创建一个ngx_eventfd的变量,创建和初始化了ngx_eventfd_event,ngx_eventfd,ngx_eventfd_conn和ngx_aio_ctx这些全局变量。
而后把代表aio的描述符ngx_eventfd加入epoll,就完成了epoll和native aio的绑定啦。
当实际使用aio write的时候,使用了ngx_eventfd这个全局变量进行了操作,当描述符活跃时就能触发epoll进行异步IO的操作。
例如上文的:
ssize_t
(ngx_file_t *file, u_char *buf, size_t size, off_t offset,
ngx_file_aio_writed*pool)
ngx_pool_t {
...
->aiocb.aio_flags = IOCB_FLAG_RESFD;
aio->aiocb.aio_resfd = ngx_eventfd;
aio
->handler = ngx_file_aio_event_handler; ev
然后再来看看触发epoll的过程
static ngx_int_t
(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
ngx_epoll_process_events{
...
= epoll_wait(ep, event_list, (int) nevents, timer);
events ...
for (i = 0; i < events; i++) {
= event_list[i].data.ptr;
c
= (uintptr_t) c & 1;
instance = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
c
= c->read;
rev ...
if ((revents & EPOLLIN) && rev->active) {
...
->handler(rev);
rev...
}
...
aio的描述符被触发后最终会进入rev->handler(rev)
也就是ngx_epoll_eventfd_handler函数内
static void
(ngx_event_t *ev)
ngx_epoll_eventfd_handler{
...
= read(ngx_eventfd, &ready, 8);
n ...
.tv_sec = 0;
ts.tv_nsec = 0;
ts
while (ready) {
= io_getevents(ngx_aio_ctx, 1, 64, event, &ts);
events ...
if (events > 0) {
-= events;
ready
for (i = 0; i < events; i++) {
...
= (ngx_event_t *) (uintptr_t) event[i].data;
e
->complete = 1;
e->active = 0;
e->ready = 1;
e
= e->data;
aio ->res = event[i].res;
aio
(e, &ngx_posted_events);
ngx_post_event}
continue;
}
...
在这里会使用AIO的调用获取一共触发了多少AIO事件,而后把这些事件加入post事件队列,这个队列在epoll的下一轮时会被操作
void
(ngx_cycle_t *cycle)
ngx_process_events_and_timers{
...
if (ngx_posted_events) {
if (ngx_threaded) {
(cycle);
ngx_wakeup_worker_thread
} else {
(cycle, &ngx_posted_events);
ngx_event_process_posted}
}
}
void
(ngx_cycle_t *cycle,
ngx_event_process_posted**posted)
ngx_thread_volatile ngx_event_t {
*ev;
ngx_event_t
for ( ;; ) {
= (ngx_event_t *) *posted;
ev ...
(ev);
ngx_delete_posted_event
->handler(ev);
ev}
}
这里的ev->handler(ev);所调用的就是上文中的ev->handler = ngx_file_aio_event_handler;
也就是
static void
(ngx_event_t *ev)
ngx_file_aio_event_handler{
*aio;
ngx_event_aio_t
= ev->data;
aio
(NGX_LOG_DEBUG_CORE, ev->log, 0,
ngx_log_debug2"aio event handler fd:%d %V", aio->fd, &aio->file->name);
->handler(ev);
aio}
这个aio->handler(ev);回调函数的逻辑是自己设计的,一般是用作数据的清理的
我是这么定义的im->file.aio->handler = ngx_http_save_cache_file_handler;
做了一些末尾的清理操作。
另外说个无关的,如果是AIO_READ,那么这里的aio操作是已经被写死的
ngx_http_copy_aio_event_handler() -> ngx_http_request_handler() -> ngx_http_writer() -> ngx_http_output_filter() -> ngx_http_top_body_filter()
这样一个流程,有兴趣的同学可以跟着看下
至此,AIO的初始化,结合epoll的处理以及最后的清尾操作都已经分析完毕。
可以看到我的这个AIO_WRITE机制很完美的融入了nginx的全流程中。
nginx的http模块的协作
lookup cache不仅仅应该出现在filter模块里,应当在fastdfs的nginx模块之前就拦截下已经存在在缓存的文件请求。
所以应当是这样的处理顺序
lookup cache -> fastdfs nginx -> image filter look up cache -> image filter save cache
这就带来一个问题,这还得从http的介入阶段说起
大部分的http模块都在NGX_HTTP_CONTENT_PHASE阶段介入(一共是11个阶段)
该阶段提供两种介入方式
方法一:与其他10个阶段一样,在必定会调用的postconfiguration方法向全局的ngx_http_core_main_conf_t结构体phases[NGX_HTTP_CONTENT_PHASE]动态数组添加ngx_htpp_hadler_pt处理方法,这种处理方式将应用全部HTTP请求,无视你的SERVER LOC配置。
方法二:通过设置ngx_http_core_loc_conf_t的handler指针来实现,这样可以匹配location段。
fastdfs的nginx模块是使用的方法二,看起来是不错,但是拓展开发的时候是存在坑的。
那就是ngx_htpp_core_loc_conf_t的handler指针不是数组,所以第二种方法只能用一个ngx_http_hander方法,也就是不能和其他http模块一起协作了。
所以不得已改成了方法一的形式,但是方法一的缺点也很明显,他会应用所有的HTTP请求。
包括/web_status和CDN的/do_not_delete
由fastdfs的nginx模块操作时就直接返回错误400了
所以只好在fastdfs nginx的handler入口加了一句
if(memcmp(r->uri_start,"/g",2) != 0){
return NGX_DECLINED;
}
当判断不是/g的url请求时,直接跳过交给下一个http模块处理。
这样就完美运行了
fastdfs nginx的IP映射模块
首先在ngx_http_fastdfs_commands中加入命令
{ ngx_string("ngx_fdfs_org_ip"),
|NGX_CONF_ANY,
NGX_HTTP_LOC_CONF,
ngx_conf_set_org_ip,
NGX_HTTP_LOC_CONF_OFFSET0,
},
NULL
{ ngx_string("ngx_fdfs_chg_ip"),
|NGX_CONF_ANY,
NGX_HTTP_LOC_CONF,
ngx_conf_set_chg_ip,
NGX_HTTP_LOC_CONF_OFFSET0,
}, NULL
然后添加实现命令
static char* ngx_conf_set_org_ip(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
*mycf = conf;
ngx_http_fastdfs_loc_conf_t *org_ip = cf->args;
ngx_array_t ->org_ip = ngx_array_create(cf->pool,org_ip->nelts,sizeof(ngx_str_t));
mycf
= 0;
ngx_uint_t array_seq for(;array_seq < org_ip->nelts;++array_seq) {
((ngx_str_t *)mycf->org_ip->elts + array_seq,(ngx_str_t *)org_ip->elts + array_seq,sizeof(ngx_str_t));
memcpy}
->org_ip->nelts = org_ip->nelts;
mycf
return NGX_CONF_OK;
}
static char* ngx_conf_set_chg_ip(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
*mycf = conf;
ngx_http_fastdfs_loc_conf_t *chg_ip = cf->args;
ngx_array_t ->chg_ip = ngx_array_create(cf->pool,chg_ip->nelts,sizeof(ngx_str_t));
mycf
= 0;
ngx_uint_t array_seq for(;array_seq < chg_ip->nelts;++array_seq) {
((ngx_str_t *)mycf->chg_ip->elts + array_seq,(ngx_str_t *)chg_ip->elts + array_seq,sizeof(ngx_str_t));
memcpy}
->chg_ip->nelts = chg_ip->nelts;
mycf
return NGX_CONF_OK;
}
实现命令将读出的cf->args数组进行数据拷贝,因为cf->args数组在初始化完毕后会被销毁。光是指向这个指针是无效的,这个我也踩了个坑才发现。
merge_ptr和init_ptr我就不贴了
然后就是IP映射的代码
static void ngx_http_fastdfs_set_ctx_dest_ip_addr(ngx_http_fastdfs_loc_conf_t *plcf,ngx_http_fastdfs_proxy_ctx_t *ctx,const char *dest_ip_addr)
{
if(plcf->org_ip != NGX_CONF_UNSET_PTR && plcf->chg_ip != NGX_CONF_UNSET_PTR) {
*org_ip = plcf->org_ip->elts;
ngx_str_t *chg_ip = plcf->chg_ip->elts;
ngx_str_t = 1;
ngx_uint_t array_seq while(array_seq < plcf->org_ip->nelts) {
if(memcmp(org_ip[array_seq].data,dest_ip_addr,org_ip[array_seq].len) == 0){
(ctx->dest_ip_addr,(const char *)chg_ip[array_seq].data,chg_ip[array_seq].len);
strncpyreturn ;
}
++array_seq;
}
}
(ctx->dest_ip_addr, dest_ip_addr);
strcpy}
非常简单,子串匹配罢了。
这样一个IP映射模块就完成了。
未完待续
-
2015-01-28
nginx真是博大精深,最近写fastdfs的动态缩略图模块,刚好能有时间研究下,真是满心欢喜。
学习主要是靠两本书,其他的稍微搜搜也就能解惑了。
《Nginx模块开发与架构解析》这个我看的电子PDF
《Nginx开发从入门到精通》这个有网页版本的,挺好的:
-
2015-05-06
nginx的文件缓存分为两种,内存缓存和硬盘缓存。
内存缓存指文件句柄等信息进行缓存,减少使用open等的系统调用。
硬盘缓存指文件被缓存到硬盘上,一般是因为当作反向代理用才会有这种需求。
内存缓存是全模块都可以调用的,因为封装在core/ngx_open_file_cache.c中。
而硬盘缓存只有http模块可以调用,因为封装在http/ngx_http_file_cache.c中,最常见的就是upstream模块的cache了。