#Nginx 源码分析 upstream指令
想要的解决问题:
1:upstream存储结构
2:动态 upstream 流程(proxy_pass跟随变量或者域名)
最简单的配置文件
http {
upstrem abc {
server 1.1.1.1;
server 2.2.2.2;
}
upstrem efg {
server 3.3.3.3;
server 4.4.4.4;
}
server {
listen 80;
location / {
proxy_pass http://abc;
}
}
}
存储结构
首先,upstream是复杂指令,里面还需要解析 server 指令;所以其存储形式如下:
每个upstream指令用ngx_http_upstream_srv_conf_t
表示,每个server指令,用ngx_http_upstream_server_t
描述。
ngx_http_upstream_server_t
----ngx_http_upstream_srv_conf_t
--------ngx_http_upstream_server_t
------------1.1.1.1
------------2.2.2.2
ngx_http_upstream_server_t
----ngx_http_upstream_srv_conf_t
--------ngx_http_upstream_server_t
------------3.3.3.3
------------4.4.4.4
我们来看一下Nginx如何将upstream组织成如上存储结构的:
首先,每解析一个upstream块时,就往 ngx_http_upstream_main_conf_t
中插入upstreams:每个upstream指令都会往 ngx_http_upstream_main_conf_t->upstreams
数组中插入一个对象。每个对象是 ngx_http_upstream_srv_conf_t
类型的数据结构。ngx_http_upstream_srv_conf_t
对象的名字就是 upstream
指令后面跟的名字abc或者efg,方便proxy_pass
指令根据名字查找到;
static char *
ngx_http_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
........
ngx_http_upstream_srv_conf_t *uscf;
/*每解析一个upstream,就生成一个ngx_http_upstream_srv_conf_t对象,当然也会查重*/
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);
}
换句话说,每个 ngx_http_upstream_srv_conf_t
挂在 ngx_http_upstream_main_conf_t
中的upstreams
成员下面。ngx_http_upstream_srv_conf_t
用来描述一个upstream块,ngx_http_upstream_main_conf_t->upstreams
用来管理所有 upstream块。
每个 ngx_http_upstream_srv_conf_t
中,有个 servers
成员,其类型是ngx_http_upstream_server_t
,用来描述一个 server
,换句话说,每解析到一个server指令,则会新建一个ngx_http_upstream_server_t
对象挂在其所属的upstream的servers
字段中。
至此,解析完upstream以及其server指令后,其存储在内存的结构描述完成;http_proxy 模块是如何找到对应的upsream呢?
proxy_pass 引用 upstream
proxy_pass 为非变量
location / {
proxy_pass http://abc;
}
在解析proxy_pass
指令时通过ngx_http_upstream_add
函数,找到对应的upstream(由ngx_http_upstream_srv_conf_t
描述)
plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
if (plcf->upstream.upstream == NULL) {
return NGX_CONF_ERROR;
}
由于Nginx可能会先解析到proxy_pass
指令,也有可能先解析到upstream
指令,所以统一调用ngx_http_upstream_add
来创建/查找 upstream,通过flag来控制流程,核心就是谁先解析,就由谁来创建。
由此可见,Nginx在配置解析阶段,就就确定好了upstream,在滴啊用ngx_http_upstream_init_request
时,通过uscf = u->conf->upstream;
直接获得这个ngx_http_upstream_srv_conf_t
(u->conf
是在函数ngx_http_proxy_handler
时,指向了配置结构的内存)。
proxy_pass 为 变量
通常,我们需要根据不同条件来选择不同的upstream,使用变量就方便的多。
set $ups abc;
if ( $host = "www.efg.com" ) {
set $ups efg;
}
location / {
proxy_pass http://$ups;
}
在解析proxy_pass
指令时,发现后面跟随了变量
url = &value[1];
n = ngx_http_script_variables_count(url);
if (n) {
ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));
sc.cf = cf;
sc.source = url;
sc.lengths = &plcf->proxy_lengths;
sc.values = &plcf->proxy_values;
sc.variables = n;
sc.complete_lengths = 1;
sc.complete_values = 1;
if (ngx_http_script_compile(&sc) != NGX_OK) {
return NGX_CONF_ERROR;
}
#if (NGX_HTTP_SSL)
plcf->ssl = 1;
#endif
return NGX_CONF_OK;
}
plcf->proxy_lengths
和 plcf->proxy_values
被赋上了值。即plcf->upstream.upstream
中的upstream字段未被赋值。
具体被赋值的时间点,是在业务处理流程函数ngx_http_proxy_handler
和ngx_http_upstream_init_request
被处理的。
if (plcf->proxy_lengths == NULL) {
//不是变量
ctx->vars = plcf->vars;
u->schema = plcf->vars.schema;
#if (NGX_HTTP_SSL)
u->ssl = (plcf->upstream.ssl != NULL);
#endif
} else {
//变量的情况下走这里
if (ngx_http_proxy_eval(r, ctx, plcf) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
}
所以核心函数是ngx_http_proxy_eval
,proxy_lengths
和proxy_values
保存着待处理的变量,该函数就是将其变量变成str。
static ngx_int_t
ngx_http_proxy_eval(ngx_http_request_t *r, ngx_http_proxy_ctx_t *ctx,
ngx_http_proxy_loc_conf_t *plcf)
{
ngx_str_t proxy;
if (ngx_http_script_run(r, &proxy, plcf->proxy_lengths->elts, 0,
plcf->proxy_values->elts)
== NULL)
{
return NGX_ERROR;
}
//proxy.data 就是变量被替换后的字符串,例如此时proxy.data就是字符串"http://efg"
......
u->resolved = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t));
if (u->resolved == NULL) {
return NGX_ERROR;
}
if (url.addrs && url.addrs[0].sockaddr) {
u->resolved->sockaddr = url.addrs[0].sockaddr;
u->resolved->socklen = url.addrs[0].socklen;
u->resolved->naddrs = 1;
u->resolved->host = url.addrs[0].name;
} else {
//将"efg"赋值给 u->resolved->host
u->resolved->host = url.host;
}
}
此时,还没找真正的upstream,只是将变量翻译成了对应的值。
static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{
......
if (u->resolved == NULL) {
uscf = u->conf->upstream;
} else {
#if (NGX_HTTP_SSL)
u->ssl_name = u->resolved->host;
#endif
host = &u->resolved->host;
//如果proxy_pass 配置为变量,且变量翻译后为一个ip时,走这里
if (u->resolved->sockaddr) {
if (u->resolved->port == 0
&& u->resolved->sockaddr->sa_family != AF_UNIX)
{
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"no port in upstream \"%V\"", host);
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
if (ngx_http_upstream_create_round_robin_peer(r, u->resolved)
!= NGX_OK)
{
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
ngx_http_upstream_connect(r, u);
return;
}
umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);
uscfp = umcf->upstreams.elts;
//如果proxy_pass 配置为变量,且变量翻译后为非IP时走这里
//根据 u->resolved->host 从 存储结构中找到对应的upstream,找到后,后续的流程就和未配置变量一样。
for (i = 0; i < umcf->upstreams.nelts; i++) {
uscf = uscfp[i];
if (uscf->host.len == host->len
&& ((uscf->port == 0 && u->resolved->no_port)
|| uscf->port == u->resolved->port)
&& ngx_strncasecmp(uscf->host.data, host->data, host->len) == 0)
{
goto found;
}
}
}
proxy_pass 为 域名
例如
location / {
proxy_pass http://www.example.com;
}
此时,在 proxy_pass
时创建了 upstream对象 ngx_http_upstream_srv_conf_t
,但是里面的 servers 字段为空,则会在ngx_http_upstream_init_main_conf
时,查询域名对应的dns。这也就是为什么如果proxy_pass 写死域名时,不会动态更新DNS的原因,比较获取DNS的流程是在进程启动阶段,后续就不更新了。
调用栈如下:
ngx_inet_resolve_host
ngx_http_upstream_init_round_robin
ngx_http_upstream_init_main_conf
proxy_pass 为 ip
例如
location / {
proxy_pass http://127.0.0.1;
}
在解析 proxy_pass
命令时,调用 ngx_http_upstream_add
创建了 ngx_http_upstream_srv_conf_t
对象,然后在执行ngx_parse_url
函数时,解析到了发现参数是ip地址,此时他会创建一个 ngx_http_upstream_server_t
对象,而不是等待 server 指令时创建 ngx_http_upstream_server_t
对象。
server指令参数为域名
upstrem abc {
server www.example.com;
}
server {
listen 80;
location / {
proxy_pass http://abc;
}
}
在处理 upsream 的 server 指令时,会调用 ngx_parse_url
处理 server参数,其中会获取域名对应的ip,解析到ip后,其余的处理流程,就和server指令参数是ip的一样了。
proxy_pass 如何区分参数 是 域名 还是 upstream块的名字?
例如
upstrem www.exapmle.com {
server 127.0.0.1;
}
server {
listen 80;
location / {
proxy_pass http://www.exapmle.com;
}
}
Nginx是去 解析www.example.com
的ip然后去访问,还是去127.0.0.1
去访问?
Nginx 逻辑是这样的:
1:proxy_pass 参数 除了是ip地址以为,其余的全部默认为 upstream 块名字。
2:proxy_pass 参数 如果是变量,变量解析完成之后,按照1处理。
3:当 配置解析完成之后,发现 upstream 里面的 server 不存在时,则在ngx_http_upstream_init_round_robin
中,进行解析。
由此可见,域名解析都在配置阶段进行了处理,若是想要通过变量形式动态获取到某个域名,默认情况下,解析域名是行不通的。因为变量处理时在请求处理阶段早就过了配置解析阶段。
例如下面这个配置,想要访问 "www.efg.com"是不成功的,因为Nginx会去找名为"www.efg.com"的upstream块,但是未找到。
server {
listen 80;
set $ups abc;
if ( $host = "www.efg.com" ) {
set $ups "www.efg.com";
}
location / {
proxy_pass http://$ups;
}
}
想要动态解析域名的方式是 配置 resolver
指令。
resolver 114.114.114.114;
server {
listen 80;
set $ups abc;
if ( $host = "www.efg.com" ) {
set $ups "www.efg.com";
}
location / {
proxy_pass http://$ups;
}
}
由此可见,Nginx代码真烂