Nginx upstream原理分析【0】指令解析
熟悉nginx的同学都知道upstream的重要作用,当nginx接收到一个连接后,读取完客户端发送出来的Header,然后就会进行各个处理过程的调用。
之后就是upstream发挥作用的时候了,upstream在客户端跟后端比如FCGI/PHP之间,接收客户端的HTTP body,发送给FCGI,然后接收FCGI的结果,发送给客户端。作为一个桥梁的作用。同时,upstream为了充分显示其灵活性,至于后端具体是什么协议,什么系统他都不care,我只实现主体的框架,具体到FCGI协议的发送,接收,解析,这些都交给后面的插件来处理,比如有fastcgi,memcached,proxy等插件,我之前也做过一个简单的跟公司内部系统的module,其实他们都是一种回调形式的模块,跟upstream模块配合从而完成整个请求。如无特殊说明,后面都已FCGI作为后端模块为例说明。
0、跟FCGI协议处理模块的关系:
upstream有个重要的结构体ngx_http_upstream_s,每个请求一个独立的结构体,其存储了upstream的绝大部分数据,比如各个读写回调,接收到的数据缓冲链表,要发送给客户端的缓冲链表。
其中一套很重要的回调函数是跟FCGI模块的交互接口,比如create_request,process_header 是其中最重要的2个函数,他们的作用分别为:
create_request用来创建要发送给后面FCGI的FCGI格式的数据,存放在u->request_bufs链接表里面。用来待会进行发送。具体看这个ngx_http_upstream_s结构体的内容,字段含义注释在后面了。
struct ngx_http_upstream_s {//本结构体用来保存一个连接的upstream信息,包括各种需要upstream回调的函数等。 ngx_http_upstream_handler_pt read_event_handler; //ngx_http_upstream_process_header ngx_http_upstream_handler_pt write_event_handler;// ngx_http_upstream_send_request_handler ngx_peer_connection_t peer; ngx_event_pipe_t *pipe; ngx_chain_t *request_bufs;//客户端发送过来的数据body部分,在ngx_http_upstream_init_request设置为客户端发送的HTTP BODY //也可能是代表要发送给后端的数据链表结构,比如ngx_http_proxy_create_request会这么放的。比如是FCGI结构数据,或者Proxy结构等。 ngx_output_chain_ctx_t output;//输出数据的结构,里面存有要发送的数据,以及发送的output_filter指针 ngx_chain_writer_ctx_t writer;//参考ngx_chain_writer,里面会将输出buf一个个连接到这里。 ngx_http_upstream_conf_t *conf;//为u->conf = &flcf->upstream; ngx_http_upstream_headers_in_t headers_in;//存放从上游返回的头部信息, ngx_http_upstream_resolved_t *resolved;//解析出来的fastcgi_pass 127.0.0.1:9000;后面的字符串内容,可能有变量嘛。 ngx_buf_t buffer;///读取上游返回的数据的缓冲区,也就是proxy,FCGI返回的数据。这里面有http头部,也可能有body部分。 //其body部分会跟event_pipe_t的preread_bufs结构对应起来。就是预读的buf,其实是i不小心读到的。 size_t length;//要发送给客户端的数据大小,还需要读取这么多进来。 ngx_chain_t *out_bufs;//这个是要发送给客户端的数据链接表? ngx_chain_t *busy_bufs;//调用了ngx_http_output_filter,并将out_bufs的链表数据移动到这里,待发送完毕后,会移动到free_bufs ngx_chain_t *free_bufs;//空闲的缓冲区。可以分配 ngx_int_t (*input_filter_init)(void *data);//进行初始化,没什么用,memcache设置为ngx_http_memcached_filter_init ngx_int_t (*input_filter)(void *data, ssize_t bytes);//用来读取后端的数据,非buffering模式。 //ngx_http_upstream_non_buffered_filter,ngx_http_memcached_filter等。 //这个函数的调用时机: ngx_http_upstream_process_non_buffered_upstream等调用ngx_unix_recv接收到upstream返回的数据后 //就调用这里进行协议转换,不过目前转换不多。 void *input_filter_ctx;//指向所属的请求等上下文 #if (NGX_HTTP_CACHE) ngx_int_t (*create_key)(ngx_http_request_t *r); #endif //下面的upstream回调指针是各个模块设置的,比如ngx_http_fastcgi_handler里面设置了fcgi的相关回调函数。 ngx_int_t (*create_request)(ngx_http_request_t *r);//生成发送到上游服务器的请求缓冲(或者一条缓冲链) ngx_int_t (*reinit_request)(ngx_http_request_t *r);//在后端服务器被重置的情况下(在create_request被第二次调用之前)被调用 ngx_int_t (*process_header)(ngx_http_request_t *r);//处理上游服务器回复的第一个bit,时常是保存一个指向上游回复负载的指针 void (*abort_request)(ngx_http_request_t *r);//在客户端放弃请求的时候被调用 void (*finalize_request)(ngx_http_request_t *r,//在Nginx完成从上游服务器读入回复以后被调用 ngx_int_t rc); ngx_int_t (*rewrite_redirect)(ngx_http_request_t *r, ngx_table_elt_t *h, size_t prefix); ngx_msec_t timeout; ngx_http_upstream_state_t *state;//当前的状态 ngx_str_t method;//GET,HEAD,POST ngx_str_t schema;//就是前面的http,https,mecached://等。 ngx_str_t uri; ngx_http_cleanup_pt *cleanup;//ngx_http_upstream_cleanup unsigned store:1; unsigned cacheable:1; unsigned accel:1; unsigned ssl:1; #if (NGX_HTTP_CACHE) unsigned cache_status:3; #endif unsigned buffering:1;//是否要buffer 后端的数据,如果要用event_pipe的方式发送数据 unsigned request_sent:1;//是否已经将request_bufs的数据放入输出链表里面 unsigned header_sent:1;//标记已经发送了头部字段。 };
上面的会回调函数我们会在后面不断引用介绍的。这里先带过知道其存在即可。
1、upstream模块配置解析:
这个模块的配置指令很少,只有upstream,server2个指令。官网列了个格式案例:
upstream backend {
server backend1.example.com weight=5;
server backend2.example.com:8080;
server unix:/tmp/backend3;
}server {
location / {
proxy_pass http://backend;
}
}
下面分2块介绍这2条指令。
1.1、upstream指令解析:
这个指令其实是个指令块,upstream{},不涉及到具体参数等解析。当nginx解析到这个块block的时候,会调用ngx_http_upstream函数进行处理。函数比较简单,首先将u代表名字比如上面的“backend”的数据设置到umcf->upstreams里面去。然后返回对应的upstream{}结构数据指针。也就是增加一个命名的upstream,放到umcf->upstreams数组里面去。这个数组是每http{}块都有一份的。或者里面有的话也可以。Context: http,不能存在server里面的。这个数组,代表有多少个upstream{}块。存放server xx.xx.xx.xx:xx weight=2 max_fails=3; 信息的数组。当然从之前的文章中可以看到,这样的语句也能产生一个新的upstream结构:fastcgi_pass 127.0.o.1:8888;
typedef struct {//这个数组是每http{}块都有一份的。或者里面有的话也可以。Context: http,不能存在server里面的 ngx_hash_t headers_in_hash;//ngx_http_upstream_headers_in里面的数据. ngx_array_t upstreams;//数组,代表有多少个upstream{}块。server xx.xx.xx.xx:xx weight=2 max_fails=3; 信息的数组。 //由ngx_http_upstream_create_main_conf函数返回。存放在上层的ctx中。 /* ngx_http_upstream_srv_conf_t */ } ngx_http_upstream_main_conf_t;
继续上面的分析,接着nginx会创建一个ngx_http_conf_ctx_t结构体,这个不陌生了,使用来存储该指令上下文的,过几天写个文章介绍一下nginx的配置解析,这个很有意思。nginx的配置文件比apache什么的复杂多了。
我们可以从ngx_http_upstream()看到,upstream指令块的上下文ctx结构中,main_conf是跟上次http{}块共用的,这样在upstream里面修改的main_conf等都是跟http一个。然后就是创建了srv_conf,loc_conf,然后循环调用各个模块的create_srv_conf,create_loc_conf,注意没有main_conf,公用的。
之后就是用新分配的ctx替换原来的ctx:cf->ctx = ctx;临时切换ctx,进入upstream{}块中进行解析。切换后,调用ngx_conf_parse(cf, NULL);进行块内的解析,这样在块内获取到的配置数据都是刚刚申请的这个ctx,而不是http{}层的ctx了。简单看一下下面函数:
static char * ngx_http_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy) {//当碰到upstream{}指令的时候调用这里。 char *rv; void *mconf; ngx_str_t *value; ngx_url_t u; ngx_uint_t m; ngx_conf_t pcf; ngx_http_module_t *module; ngx_http_conf_ctx_t *ctx, *http_ctx; ngx_http_upstream_srv_conf_t *uscf; ngx_memzero(&u, sizeof(ngx_url_t)); value = cf->args->elts; u.host = value[1]; u.no_resolve = 1; //下面将u代表的数据设置到umcf->upstreams里面去。然后返回对应的upstream{}结构数据指针。 uscf = ngx_http_upstream_add(cf, &u, NGX_HTTP_UPSTREAM_CREATE |NGX_HTTP_UPSTREAM_WEIGHT |NGX_HTTP_UPSTREAM_MAX_FAILS |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT |NGX_HTTP_UPSTREAM_DOWN |NGX_HTTP_UPSTREAM_BACKUP); if (uscf == NULL) { return NGX_CONF_ERROR; } //申请ngx_http_conf_ctx_t结构,经典的main/srv/local_conf指针结构 ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t)); if (ctx == NULL) { return NGX_CONF_ERROR; } http_ctx = cf->ctx;//跟上层的HTTP公用main_conf,这里跟server{}指令一样的,共享main_conf ctx->main_conf = http_ctx->main_conf; /* the upstream{}'s srv_conf */ ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module); if (ctx->srv_conf == NULL) { return NGX_CONF_ERROR; } //在ngx_http_upstream_module模块里面记录我的srv_conf。里面记录了我里面有哪几个server指令 ctx->srv_conf[ngx_http_upstream_module.ctx_index] = uscf;//uscf里面记录了server列表信息。 uscf->srv_conf = ctx->srv_conf;//这一条,记住我这个upstream所属的srv_conf数组。也就是所属的http{}块里面的srv_conf /* the upstream{}'s loc_conf */ ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module); if (ctx->loc_conf == NULL) { return NGX_CONF_ERROR; } for (m = 0; ngx_modules[m]; m++) {//老规矩,初始化所有HTTP模块的srv,loc配置。调用每个模块的create回调 if (ngx_modules[m]->type != NGX_HTTP_MODULE) { continue; } module = ngx_modules[m]->ctx; if (module->create_srv_conf) { mconf = module->create_srv_conf(cf); if (mconf == NULL) { return NGX_CONF_ERROR; } ctx->srv_conf[ngx_modules[m]->ctx_index] = mconf; } if (module->create_loc_conf) { mconf = module->create_loc_conf(cf); if (mconf == NULL) { return NGX_CONF_ERROR; } ctx->loc_conf[ngx_modules[m]->ctx_index] = mconf; } } /* parse inside upstream{} */ pcf = *cf; cf->ctx = ctx;//临时切换ctx,进入upstream{}块中进行解析。 cf->cmd_type = NGX_HTTP_UPS_CONF; rv = ngx_conf_parse(cf, NULL); *cf = pcf; if (rv != NGX_CONF_OK) { return rv; } if (uscf->servers == NULL) { "no servers are inside upstream"); return NGX_CONF_ERROR; } return rv; }
上面代码中,ngx_http_upstream_add函数比较重要,它用来增加一个upstream的.如果u代表的server已经存在,则返回句柄,否则在umcf->upstreams里面新加一个,设置初始化。ngx_http_fastcgi_pass等函数也会调用这个函数用来增加一个upstream的server.
函数通过根据名字等查找umcf->upstreams.elts数组,看看是否存在一个名字相同的upstream,如果有就返回其ngx_http_upstream_srv_conf_t的地址。否则新建一个ngx_http_upstream_srv_conf_t结构,存放相关的配置,然后分配uscf->servers数组,这个数组就是用来记录server xxxx;指令的数组。如下图所示:
下面看一下ngx_http_upstream_add的代码和注释:
ngx_http_upstream_srv_conf_t * ngx_http_upstream_add(ngx_conf_t *cf, ngx_url_t *u, ngx_uint_t flags) {//如果u代表的server已经存在,则返回句柄,否则在umcf->upstreams里面新加一个,设置初始化。 //在ngx_http_fastcgi_pass等碰到后端地址的地方,会调用这个函数,增加一个upstream的server. //这里的单位是upstream,不是server行,调用的上层一般只有一个地址,于是就只有一个server.当做单个upstream处理 ngx_uint_t i; ngx_http_upstream_server_t *us; ngx_http_upstream_srv_conf_t *uscf, **uscfp; ngx_http_upstream_main_conf_t *umcf; if (!(flags & NGX_HTTP_UPSTREAM_CREATE)) {//如果没有设置CREATE标志,表示不需要创建。 if (ngx_parse_url(cf->pool, u) != NGX_OK) {//简析一下地址格式,名称,unix:域,inet6,4地址,http://host:port/等 if (u->err) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "%s in upstream \"%V\"", u->err, &u->url); } return NULL; } } umcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_upstream_module); uscfp = umcf->upstreams.elts; for (i = 0; i < umcf->upstreams.nelts; i++) { //遍历当前的upstream,如果有重复的,则比较其相关的字段,并打印日志。如果找到相同的,则返回对应指针。 if (uscfp[i]->host.len != u->host.len || ngx_strncasecmp(uscfp[i]->host.data, u->host.data, u->host.len) != 0) { continue;//不相同的不管。 } if ((flags & NGX_HTTP_UPSTREAM_CREATE) && (uscfp[i]->flags & NGX_HTTP_UPSTREAM_CREATE)) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "duplicate upstream \"%V\"", &u->host); return NULL; } if ((uscfp[i]->flags & NGX_HTTP_UPSTREAM_CREATE) && u->port) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "upstream \"%V\" may not have port %d", &u->host, u->port); return NULL; } if ((flags & NGX_HTTP_UPSTREAM_CREATE) && uscfp[i]->port) { ngx_log_error(NGX_LOG_WARN, cf->log, 0, "upstream \"%V\" may not have port %d in %s:%ui", &u->host, uscfp[i]->port, uscfp[i]->file_name, uscfp[i]->line); return NULL; } if (uscfp[i]->port != u->port) { continue; } if (uscfp[i]->default_port && u->default_port && uscfp[i]->default_port != u->default_port) { continue; } return uscfp[i];//找到相同的配置数据了,直接返回它的指针。 } //没有找到相同的配置upstream,下面创建一个。这里的srv_conf跟server{}不是一回事,是指upstream{}里面的server xxxx:xxx;行 uscf = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_srv_conf_t)); if (uscf == NULL) { return NULL; } uscf->flags = flags; uscf->host = u->host;//这里的host其实可以为任何字符串都行的。 uscf->file_name = cf->conf_file->file.name.data; uscf->line = cf->conf_file->line; uscf->port = u->port;//没有就默认80 uscf->default_port = u->default_port; if (u->naddrs == 1) {//比如: server xx.xx.xx.xx:xx weight=2 max_fails=3; 刚开始,ngx_http_upstream会调用本函数。但是其naddres=0. uscf->servers = ngx_array_create(cf->pool, 1, sizeof(ngx_http_upstream_server_t)); if (uscf->servers == NULL) { return NGX_CONF_ERROR; } us = ngx_array_push(uscf->servers);//记录本upstream{}块的所有server指令。 if (us == NULL) { return NGX_CONF_ERROR; } ngx_memzero(us, sizeof(ngx_http_upstream_server_t)); us->addrs = u->addrs;//拷贝地址 信息。 us->naddrs = u->naddrs; } // uscfp = ngx_array_push(&umcf->upstreams);//放到upstream的main_conf里面去。 if (uscfp == NULL) { return NULL; } *uscfp = uscf;//在当前的umcf->upstreams updstream配置里面增加一项。 return uscf; }
1.2、server指令解析:
server指令解析函数是ngx_http_upstream_server,用来在所在upstream中增加一个server配置。
当解析到"server"的时候调用ngx_http_upstream_server这里.里面只是在uscf->servers里面增加了一个server,也就是面途中的server,最后下面的一个组成部分并设置好ngx_http_upstream_server_t结构的数据。其他没干。下面看一下代码和注释:
static char * ngx_http_upstream_server(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {//解析到"server"的时候调用这里.里面只是在uscf->servers里面增加了一个server,并设置好ngx_http_upstream_server_t结构的数据。其他没干。 ngx_http_upstream_srv_conf_t *uscf = conf;//从ngx_conf_handler里面可以看出,这个conf就是upstream的ctx->srv_conf[upstream模块.index]的值 //upstream模块的conf为NGX_HTTP_SRV_CONF_OFFSET,所以决定了ngx_conf_handler里面的conf参数 time_t fail_timeout; ngx_str_t *value, s; ngx_url_t u; ngx_int_t weight, max_fails; ngx_uint_t i; ngx_http_upstream_server_t *us; if (uscf->servers == NULL) {//如果本upstream的servers数组为空,初始化之 uscf->servers = ngx_array_create(cf->pool, 4, sizeof(ngx_http_upstream_server_t)); if (uscf->servers == NULL) { return NGX_CONF_ERROR; } } us = ngx_array_push(uscf->servers); //增加一个server.下面如果配置失败,会直接返回失败的,也就不需要把这项删了。 if (us == NULL) { return NGX_CONF_ERROR; } ngx_memzero(us, sizeof(ngx_http_upstream_server_t)); value = cf->args->elts; ngx_memzero(&u, sizeof(ngx_url_t)); u.url = value[1]; u.default_port = 80; if (ngx_parse_url(cf->pool, &u) != NGX_OK) {//解析一下URL的数据结构。 if (u.err) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "%s in upstream \"%V\"", u.err, &u.url); } return NGX_CONF_ERROR; } weight = 1; max_fails = 1; fail_timeout = 10; for (i = 2; i < cf->args->nelts; i++) { //遍历后面的每一个参数,比如: server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; if (ngx_strncmp(value[i].data, "weight=", 7) == 0) {//得到整数类型的权重。 if (!(uscf->flags & NGX_HTTP_UPSTREAM_WEIGHT)) { goto invalid; }//weight = NUMBER - set weight of the server, if not set weight is equal to one. weight = ngx_atoi(&value[i].data[7], value[i].len - 7); if (weight == NGX_ERROR || weight == 0) { goto invalid; } continue; } if (ngx_strncmp(value[i].data, "max_fails=", 10) == 0) { //解析max_fails参数,表示 if (!(uscf->flags & NGX_HTTP_UPSTREAM_MAX_FAILS)) { goto invalid; }//NUMBER - number of unsuccessful attempts at communicating with the server within the time period max_fails = ngx_atoi(&value[i].data[10], value[i].len - 10); if (max_fails == NGX_ERROR) { goto invalid; } continue; } if (ngx_strncmp(value[i].data, "fail_timeout=", 13) == 0) { //这么fail_timeout多的时间内,出现max_fails的失败的服务器将被标记为出问题的。 if (!(uscf->flags & NGX_HTTP_UPSTREAM_FAIL_TIMEOUT)) { goto invalid; }//fail_timeout = TIME - the time during which must occur *max_fails* number of unsuccessful attempts at communication with the server that would cause the server to be considered inoperative s.len = value[i].len - 13; s.data = &value[i].data[13]; fail_timeout = ngx_parse_time(&s, 1); if (fail_timeout == NGX_ERROR) { goto invalid; } continue; } if (ngx_strncmp(value[i].data, "backup", 6) == 0) { if (!(uscf->flags & NGX_HTTP_UPSTREAM_BACKUP)) { goto invalid; }//backup - (0.6.7 or later) only uses this server if the non-backup servers are all down or busy us->backup = 1; continue; } if (ngx_strncmp(value[i].data, "down", 4) == 0) { if (!(uscf->flags & NGX_HTTP_UPSTREAM_DOWN)) { goto invalid; } us->down = 1; continue; } goto invalid; } //下面拷贝设置一下这个server的相关信息。 us->addrs = u.addrs; us->naddrs = u.naddrs; us->weight = weight; us->max_fails = max_fails; us->fail_timeout = fail_timeout; return NGX_CONF_OK; invalid: ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"", &value[i]); return NGX_CONF_ERROR; }
upstream模块就包含这2个指令,到这里介绍完了,基本就死http{}块的main_conf里面包含一个upstreams数组,其里面记录了所有upstream{}块的配置。每个upstreams数组元素代表一个upstream{}块,其里面又有servers数组,代表每条server指令。
由于upstream模块比较复杂,有点长,因此后面分篇文章一一介绍吧。
近期评论