nginx实现动态resolve的思路

1.摘要

在nginx用作负载均衡的时候,通常配置的源站都是一个或若干个IP+端口号,但在有些需求的情况下,我们需要回源到一个域名instead of一组ip,比如有如下的需求:

  • 一个域名对应的IP列表是变化的
  • nginx部署在不同的运营商后面,解析出来的IP可能是不一样的

一个简单的upstream可能如下

    upstream server_group {
        server mysite.com;
    }

我们想支持前面提到的需求,怎么办呢?在nginx的商业版中,配置是这样的:

    upstream server_group{
        server mysite.com resolve;
    }

这样就可以动态的解析mysite.com了。
我没有商业版的代码,表面上能满足我们动态解析的需求,但在有些情况下,上面的配置就显得捉襟见肘了。

我们可以先理一下,当upstream的server列表发生变化,哪些模块会受影响呢?这是和我们使用了哪些模块有关系的,在我的场景下,有如下模块收到了影响

  • 负载均衡模块
  • 健康检查模块

负载均衡模块需要感知变化,是因为需要在这些server负载均衡,多了少了server那得知道
健康检查模块也一样的,upstream的server列表指定了需要对哪些server做检查

因为通常来讲,这两个模块是需要感知upstream成员变化的。那么我们到底该如何改动源站域名的动态解析呢?

2.实现思路

我们可以先理一下,nginx现状是什么样呢?nginx本身是支持配置域名回源的,正如前面的配置。但这个配置是一次性判定的,加载配置的时候就定下来了,后期无法改变。
如果你的源站一直没有变化,其实这样的配置也是足够的。

但我们关注的是源站可以变化的这种case,要么随着时间变化,要么随着部署环境变化(部署在移动/联通/电信的IDC里)。

2.1.upstream模块的实现

在做一切之前,我们需要了解upstream模块是怎么实现的,以及upstream是如何和proxy_pass协作的,一个简单的协作配置可以为如下

    upstream server_group {
        server mysite.com;
    }

    server {
        listen       80;
        server_name  abonege.com;

        location / {

            proxy_pass http://server_group/;
        }

然后proxy模块会去找名字为server_group的upstream去回源,然后从server_group里指定的server列表里选择一个回源。
但我们需要注意到,把proxy_pass后面的东西换成合法的域名也可以work的。

    upstream server_group {
        server mysite.com;
    }

    server {
        listen       80;
        server_name  abonege.com;

        location / {

            proxy_pass http://www.baidu.com/;
        }

那么问题来了,nginx是如何知道这是一个合法的域名还是应该从upstream里找呢?
看了一下代码思路是这样的:
首先,解析到proxy_pass的时候,不管是域名还是upstream的名字,直接当成upstream,放到upstream模块的upstream数组里,

    u.url.len = url->len - add;
    u.url.data = url->data + add;
    u.default_port = port;
    u.uri_part = 1;
    u.no_resolve = 1;  // 不用resolve
    plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);

当然,parse一个upstream block的时候,也会调用相同的函数,来添加一个upstream

    u.host = value[1];
    u.no_resolve = 1;  //不用resolve
    u.no_port = 1;

    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_add里有一些合并的逻辑,因为有可能先解析upstream{}这样的block,也可能先解析到proxy_pass里的upstream名字(这种情况只建了一个upstream的空壳),总之能merge起来。
那么回到之前的话题,我们如何区分proxy_pass里指定的是域名还是upstream的名字呢?
其实这发生在upstream初始化的时候

    (gdb) bt
    #0  ngx_inet_resolve_host (pool=0x6c0130, u=u@entry=0x7fffffffd9d0) at src/core/ngx_inet.c:1097
    #1  0x000000000044aacf in ngx_http_upstream_init_round_robin (cf=0x7fffffffe0a0, us=0x6dcbc0)
        at src/http/ngx_http_upstream_round_robin.c:189
    #2  0x0000000000444494 in ngx_http_upstream_init_main_conf (cf=0x7fffffffe0a0, conf=0x6c2bf0) at src/http/ngx_http_upstream.c:6103
    #3  0x000000000042e99f in ngx_http_block (cf=0x7fffffffe0a0, cmd=, conf=)    at src/http/ngx_http.c:262
    #4  0x000000000041c914 in ngx_conf_handler (last=1, cf=0x7fffffffe0a0) at src/core/ngx_conf_file.c:427
    #5  ngx_conf_parse (cf=cf@entry=0x7fffffffe0a0,    filename=filename@entry=0x6c0348) at src/core/ngx_conf_file.c:283
    #6  0x000000000041a38a in ngx_init_cycle (old_cycle=old_cycle@entry=0x7fffffffe150) at src/core/ngx_cycle.c:274
    #7  0x000000000040bcce in main (argc=, argv=) at src/core/nginx.c:276

ngx_inet_resolve_host就是我们要找的地方了。如果ngx_http_upstream_srv_conf_t里挂了server列表(例如upstream server_group),则走一个分支,如果是没有挂server列表的
则有可能是域名回源,然后对其进行域名解析,如果这种情况还是解析不了的,说明是瞎编的一个名字,既不是域名也不是upstream名字,则在启动的时候会报错,说host not found之类的。
总结一下:先看这个名字是不是upstream的名字,如果不是则尝试当成一个域名解析,成功则以,不成功则报错。

好啦,我们继续下一个问题,前面提到的mysite.com是什么时候解析的呢?因为我们想实现动态解析,必须知道当前是怎么解析的。
继续看ngx_http_upstream_init_round_robin
悲喜交加的是,你会发现,不知道什么时候,mysite.com已经被解析好躺在server的怀抱里了

    for (i = 0; i < us->servers->nelts; i++) {
        if (server[i].backup) {
            continue;
        }

        for (j = 0; j < server[i].naddrs; j++) {
            peer[n].sockaddr = server[i].addrs[j].sockaddr;
            peer[n].socklen = server[i].addrs[j].socklen;
            peer[n].name = server[i].addrs[j].name;
            peer[n].weight = server[i].weight;
            peer[n].effective_weight = server[i].weight;
            peer[n].current_weight = 0;
            peer[n].max_fails = server[i].max_fails;
            peer[n].fail_timeout = server[i].fail_timeout;
            peer[n].down = server[i].down;
            peer[n].server = server[i].name;

那么,server[i].addrs到底是啥时候解析出来的呢?在加载配置文件的时候

    gdb) bt
    #0  ngx_inet_resolve_host (pool=pool@entry=0x6c0130, u=u@entry=0x7fffffffd550) at src/core/ngx_inet.c:1120
    #1  0x0000000000413572 in ngx_parse_inet_url (u=0x7fffffffd550, pool=0x6c0130) at src/core/ngx_inet.c:787
    #2  ngx_parse_url (pool=0x6c0130, u=u@entry=0x7fffffffd550) at src/core/ngx_inet.c:545
    #3  0x0000000000444302 in ngx_http_upstream_server (cf=0x7fffffffe0a0, cmd=, conf=0x6d26b0)
        at src/http/ngx_http_upstream.c:5631
    #4  0x000000000041c914 in ngx_conf_handler (last=0, cf=0x7fffffffe0a0) at src/core/ngx_conf_file.c:427

所以,综上,我们认为在加载完毕配置文件的时候,想要的东西都好了。比如proxy和upstream的关联,upstream内部server名字的解析。但这一切都发生在解析文件的时候,
也就是一锤子打死的配置,不能变化,但我们要的就是变化,该当如何呢?

2.2.我们的方案

我们注意到,在ngx_http_upstream_init_request里居然还有

    ngx_resolve_start...
    ngx_http_upstream_resolve_handler...

这个逻辑是干啥用的么,看起来很像运行太解析的,对不对?难道nginx自动支持动态DNS解析么,想多了。那个商业版才有的。是这样的,因为proxy_pass传递的参数里,是可以以变量形式配置的

    proxy_pass http://$dest_hostname/;

如果$dst_hostname是动态解析出来的,比如针对a请求$dest_hostname翻译成a.com,针对b请求翻译成b.com,辣么这个回源的域名就是动态变化的,就需要我们动态解析。

有没有发现,已经在一定程度上解决我们的问题了。但前提是,我们不是用upstream回源,而是直接采用域名回源(nginx变量的形式)。
这样就能动态的翻译源站域名到不同的IP了。解决了我们的一部分问题,但还有别的问题,需要我们搞定,比如回话保持,健康检查都是配置在upstream里的。
如果我们不用upstream回源,相当于这部分功能无法使用了。那么应该怎么搞呢?

举tengine的dyups模块为例,他的工作原理大致如下,当一个nginx worker进程收到一个创建upstream的API后,会验证upstream的配置语法,然后通过share memory传递给其他的
worker。然后各个worker会把这个upstream同步给自己的内存,注意,这时候会调用健康检查的添加peer的逻辑,以及对应算法模块的初始化(rr,lc,haship等算法的初始化逻辑是不一样的)

所以我们为了支持动态解析,动手的逻辑应该是这里。

  • 持久化dyups的原始配置信息,虽然每个worker进程知道upstream信息,但是从upstream信息反推配置是很困难的。因为每次域名解析的内容变动后,我们需要重新构造dyups的upstream配置block
  • 周期的检查dyups的所有upstream配置,然后进行dns解析,这里nginx已经提供了很友好的函数。


ngx_resolve_start…
ngx_http_upstream_resolve_handler…

  • 如果发现dyups的server域名已经发生变化,则走dyups update流程,即删除旧的upstream,创建新的upstream

其实说句实话,选一个worker是不好选择的,我的建议是有一个专门的controller进程,用于做这件事。

以上。