Nginx怎么做域名解析?怎么在你自己開發的模塊里面使用Nginx提供的方法解析域名?它內部實現是什么樣的?
本文以Nginx 1.5.1為例,從nginx_mail_smtp模塊如何進行域名解析出發,分析Nginx進行域名解析的過程。為了簡化流程,突出重點,在示例代碼中省掉了一些異常部分的處理,比如內存分配失敗等。
DNS查詢分為兩種:根據域名查詢地址和根據地址查詢域名,在代碼結構上這兩種方式非常相似,這里只介紹根據域名查詢地址這一種方式。本文將從以下幾個方面進行介紹:
- 域名查詢的函數接口介紹
- 域名解析流程分析
- 查詢場景分析及實現介紹
一、域名查詢的函數接口介紹
在使用同步IO的情況下,調用gethostbyname()或者gethostbyname_r()就可以根據域名查詢到對應的IP地址, 但因為可能會通過網絡進行遠程查詢,所以需要的時間比較長。
為了不阻塞當前線程,Nginx采用了異步的方式進行域名查詢。整個查詢過程主要分為三個步驟,這點在各種異步處理時都是一樣的:
- 準備函數調用需要的信息,并設置回調方法
- 調用函數
- 處理結束后回調方法被調用
另外,為了盡量減少查詢花費的時間,Nginx還對查詢結果做了本地緩存。為了初始化DNS Server地址和本地緩存等信息,需要在真正查詢前需要先進行一些全局的初始化操作。
下面先從調用者的角度對每個步驟做詳細的分析:
- 初始化域名查詢所需要的的全局信息
- 需要初始化的全局信息包括:
- 因為resolver是全局的,與任何一個connection都無關,所有需要放在一個隨時都可以取到的地方,如 ngx_mail_core_srv_conf_t結構體上,在使用時從當前session找到ngx_mail_core_srv_conf_t,然后找到resolver。
- DNS 服務器的信息需要在配置文件中明確指出,比如
#nginx.conf resolver 8.8.8.8 #nginx 默認會根據DNS請求結果里的TTL值來進行緩存, #當然也可以通過一個可選的參數valid來設置過期時間,如: #resolver 127.0.0.1 [::1]:5353 valid=30s;
- 下面根據配置中的resolver參數,初始化全局的ngx_resolver_t,其中保存了前面提及的DNS服務器地址和查詢結果等信息:
static char * ngx_mail_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_mail_core_srv_conf_t *cscf = conf; ngx_str_t *value; value = cf->args->elts; cscf->resolver = ngx_resolver_create(cf, &value[1], cf->args->nelts - 1); return NGX_CONF_OK; }
- DNS 服務器的地址,如果指定了多個服務器,nginx會采用Round Robin的方式輪流查詢每個服務器
- 對查詢結果的緩存,采用Red Black Tree的數據結構,以要查詢名字的Hash作為Key, 節點信息存放在 struct ngx_resolver_node_t中。
- 準備本次查詢的信息
- 和本次查詢相關的信息放在ngx_resolver_ctx_t結構體中,包括要查詢的名稱,查詢完的回調方法,以及超時時間等。如果本次要查詢的地址已經是IPv4用點分隔的地址了,比如74.125.128.100, nginx會在ngx_resolve_start中進行判斷,并設置好標志位,在調用ngx_resolve_name時不會發送真正的DNS查詢請求。
static void ngx_mail_smtp_resolve_name(ngx_event_t *rev) { ngx_connection_t *c; ngx_mail_session_t *s; ngx_resolver_ctx_t *ctx; ngx_mail_core_srv_conf_t *cscf; c = rev->data; s = c->data; cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); ctx = ngx_resolve_start(cscf->resolver, NULL); if (ctx == NULL) { ngx_mail_close_connection(c); return; } ctx->name = s->host; ctx->type = NGX_RESOLVE_A; ctx->handler = ngx_mail_smtp_resolve_name_handler; ctx->data = s; ctx->timeout = cscf->resolver_timeout; //根據名字進行IP地址查詢 if (ngx_resolve_name(ctx) != NGX_OK) { ngx_mail_close_connection(c); } }
- 根據名字進行IP地址查詢
- 前面方法的最后通過ngx_resolve_name方法進行IP地址查詢。查詢時,Nginx會先檢查本地緩存,如果在緩存中,就更新緩存過期時間,并回調設置的handler, 如前面設置的:ngx_mail_smtp_resolve_name_handler,然后整個查詢過程結束。如果沒有在緩存中就發送查詢請求給dns server,同時方法返回。
- 查詢完成后回調在ngx_resolver_ctx_t中指定的方法
- 真正的DNS查詢完成后,不管成功,失敗或是超時,nginx會回調相應查詢的handler, 如前面設置的:ngx_mail_smtp_resolve_name_handler。在handler中都需要調用ngx_resolve_addr_done來標識查詢結束。
static void ngx_mail_smtp_resolve_name_handler(ngx_resolver_ctx_t *ctx) { in_addr_t addr; ngx_uint_t i; ngx_connection_t *c; struct sockaddr_in *sin; ngx_mail_session_t *s; s = ctx->data; c = s->connection; if (ctx->state) { ngx_log_error(NGX_LOG_ERR, c->log, 0, ""%V" could not be resolved (%i: %s)", &ctx->name, ctx->state, ngx_resolver_strerror(ctx->state)); } else { /* AF_INET only */ sin = (struct sockaddr_in *) c->sockaddr; for (i = 0; i < ctx->naddrs; i++) { addr = ctx->addrs[i]; ngx_log_debug4(NGX_LOG_DEBUG_MAIL, c->log, 0, "name was resolved to %ud.%ud.%ud.%ud", (ntohl(addr) >> 24) & 0xff, (ntohl(addr) >> 16) & 0xff, (ntohl(addr) >> 8) & 0xff, ntohl(addr) & 0xff); if (addr == sin->sin_addr.s_addr) { goto found; } } s->host = smtp_unavailable; } found: //不管成功失敗都要執行 ngx_resolve_name_done(ctx); }
二、域名解析流程分析
通過Nginx進行域名查詢的流程圖如下,顏色越深花費的時間越長。調用過程分為三種:
- 首先判斷是不是IPv4地址,如果是就直接調用Handler
- 再次檢查是不是在緩存中,如果有,就調用Handler
- 最后發送遠程DNS請求,收到回復后調用Handler
三、查詢場景分析及實現介紹
查詢的地址是IP v4地址
比如74.125.128.100, nginx會在ngx_resolve_start中通過ngx_inet_addr方法進行判斷,如果是IPv4的地址,就設置好標志位 ngx_resolver_ctx_t->quick,在接下來的ngx_resolve_name中會對這個標志位進行判斷,如果為1,就直接調用ngx_resolver_ctx_t->handler
ngx_resolver_ctx_t * ngx_resolve_start(ngx_resolver_t *r, ngx_resolver_ctx_t *temp) { in_addr_t addr; ngx_resolver_ctx_t *ctx; if (temp) { addr = ngx_inet_addr(temp->name.data, temp->name.len); if (addr != INADDR_NONE) { temp->resolver = r; temp->state = NGX_OK; temp->naddrs = 1; temp->addrs = &temp->addr; temp->addr = addr; temp->quick = 1; return temp; } } ... }
- 超時沒有得到查詢結果
- 調用ngx_resolve_name時設置的回調方法被調用,同時ngx_resolver_ctx_t->state被設置為NGX_RESOLVE_TIMEDOUT。相應的代碼為:
static void ngx_resolver_timeout_handler(ngx_event_t *ev) { ngx_resolver_ctx_t *ctx; ctx = ev->data; ctx->state = NGX_RESOLVE_TIMEDOUT; ctx->handler(ctx); }
- 正常查詢一個不在緩存中的域名
- 如果要查詢的域名不在緩存中,首先把域名按hash值放在緩存中,然后準備查詢需要的數據,發送DNS查詢的UDP請求給DNS服務器,
static ngx_int_t ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx) { ngx_resolver_node_t *rn; rn = ngx_resolver_alloc(r, sizeof(ngx_resolver_node_t)); ngx_rbtree_insert(&r->name_rbtree, &rn->node); ngx_resolver_create_name_query(rn, ctx); ngx_resolver_send_query(r, rn); rn->cnlen = 0; rn->naddrs = 0; rn->valid = 0; rn->waiting = ctx; ctx->state = NGX_AGAIN; } //收到DNS查詢結果后的回調方法 static void ngx_resolver_read_response(ngx_event_t *rev) { ssize_t n; ngx_connection_t *c; u_char buf[NGX_RESOLVER_UDP_SIZE]; c = rev->data; do { n = ngx_udp_recv(c, buf, NGX_RESOLVER_UDP_SIZE); if (n < 0) { return; } ngx_resolver_process_response(c->data, buf, n); } while (rev->ready); } static void ngx_resolver_process_a(ngx_resolver_t *r, u_char *buf, size_t last, ngx_uint_t ident, ngx_uint_t code, ngx_uint_t nan, ngx_uint_t ans) { hash = ngx_crc32_short(name.data, name.len); rn = ngx_resolver_lookup_name(r, &name, hash); //copy addresses to cached node rn->u.addrs = addrs; //回調所有等待本域名解析的請求 next = rn->waiting; rn->waiting = NULL; while (next) { ctx = next; ctx->state = NGX_OK; ctx->naddrs = naddrs; ctx->addrs = (naddrs == 1) ? &ctx->addr : addrs; ctx->addr = addr; next = ctx->next; ctx->handler(ctx); } }
- 對同一域名查詢多次查詢
- 如果多次查詢時,之前的查詢結果還在緩存中并且沒有失效,就直接從緩存中取到查詢結果,并調用設置的回調方法。
static ngx_int_t ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx) { uint32_t hash; in_addr_t addr, *addrs; ngx_uint_t naddrs; ngx_resolver_ctx_t *next; ngx_resolver_node_t *rn; hash = ngx_crc32_short(ctx->name.data, ctx->name.len); rn = ngx_resolver_lookup_name(r, &ctx->name, hash); if (rn) { if (rn->valid >= ngx_time()) { naddrs = rn->naddrs; if (naddrs) { ctx->next = rn->waiting; rn->waiting = NULL; do { ctx->state = NGX_OK; ctx->naddrs = naddrs; ctx->addrs = (naddrs == 1) ? &ctx->addr : addrs; ctx->addr = addr; next = ctx->next; ctx->handler(ctx); ctx = next; } while (ctx); return NGX_OK; } } } }
- 得到查詢結果時同時超時了
- 如果在得到查詢結果的同時,設置的超時時間也到期了,那該怎么辦呢?Nginx會先處理各種網絡讀寫事件,再處理超時事件,在處理網絡事件時,會相應地把設置的定時器刪除,所以在執行超時事件時就不會再執行了。
void ngx_process_events_and_timers(ngx_cycle_t *cycle) { ngx_uint_t flags; ngx_msec_t timer, delta; //處理各種網絡事件 (void) ngx_process_events(cycle, timer, flags); //處理各種timer事件,其中包含了查詢超時 ngx_event_expire_timers(); }
- 得到查詢結果時客戶端已經關閉連接
- 如果不做任何處理,那么在收到dns查詢結果后,會回調查詢時設置的回調方法,但因為連接已經被關閉,相應的內存已經被釋放,所以會有非法內存訪問的問題。怎么避免呢?在處理連接關閉事件時,同時需要調用ngx_resolve_name_done(ctx)方法,調用時需要把state設為NGX_AGAIN或者NGX_RESOLVE_TIMEDOUT,這樣就會刪除查詢所設置的回調信息:
void ngx_close_xxx_session(ngx_xxx_session_t *s) { if(s->resolver_ctx != NULL) { s->resolver_ctx->state = NGX_RESOLVE_TIMEDOUT; ngx_resolve_name_done(s->resolver_ctx); s->resolver_ctx = NULL; } } void ngx_resolve_name_done(ngx_resolver_ctx_t *ctx) { uint32_t hash; ngx_resolver_t *r; ngx_resolver_ctx_t *w, **p; ngx_resolver_node_t *rn; r = ctx->resolver; if (ctx->state == NGX_AGAIN || ctx->state == NGX_RESOLVE_TIMEDOUT) { hash = ngx_crc32_short(ctx->name.data, ctx->name.len); rn = ngx_resolver_lookup_name(r, &ctx->name, hash); if (rn) { p = &rn->waiting; w = rn->waiting; while (w) { if (w == ctx) { *p = w->next; goto done; } p = &w->next; w = w->next; } } } done: ngx_resolver_free_locked(r, ctx); }
- 本地緩存的地址沒有再次被查詢
- 每次在查詢結束的時候(調用ngx_resolve_addr_done),都會檢查有沒有緩存過期,如果有,就會進行釋放。
static void ngx_resolver_expire(ngx_resolver_t *r, ngx_rbtree_t *tree, ngx_queue_t *queue) { time_t now; ngx_uint_t i; ngx_queue_t *q; ngx_resolver_node_t *rn; now = ngx_time(); for (i = 0; i < 2; i++) { if (ngx_queue_empty(queue)) { return; } q = ngx_queue_last(queue); rn = ngx_queue_data(q, ngx_resolver_node_t, queue); if (now <= rn->expire) { return; } ngx_log_debug2(NGX_LOG_DEBUG_CORE, r->log, 0, "resolver expire "%*s"", (size_t) rn->nlen, rn->name); ngx_queue_remove(q); ngx_rbtree_delete(tree, &rn->node); ngx_resolver_free_node(r, rn); } }
- 域名對應這多個IP地址
- 如果對應的有多個ip,那么在每次查詢時,會隨機的重新排列順序,然后返回。對于調用者來說,只要去第一個地址,就可以達到取隨機地址的目的了。
static ngx_int_t ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx) { if (naddrs) { if (naddrs != 1) { addr = 0; addrs = ngx_resolver_rotate(r, rn->u.addrs, naddrs); if (addrs == NULL) { return NGX_ERROR; } } else { addr = rn->u.addr; addrs = NULL; } } } static in_addr_t * ngx_resolver_rotate(ngx_resolver_t *r, in_addr_t *src, ngx_uint_t n) { void *dst, *p; ngx_uint_t j; dst = ngx_resolver_alloc(r, n * sizeof(in_addr_t)); j = ngx_random() % n; if (j == 0) { ngx_memcpy(dst, src, n * sizeof(in_addr_t)); return dst; } p = ngx_cpymem(dst, &src[j], (n - j) * sizeof(in_addr_t)); ngx_memcpy(p, src, j * sizeof(in_addr_t)); return dst; }
- 指定了多個dns server地址會怎么查詢
- 如果在配置文件里指定了多個dns server地址會發生什么呢?比如
#nginx.conf resolver 8.8.8.8 8.8.4.4
- 那么nginx 會采用Round Robin 的方式輪流查詢各個dns server。在方法ngx_resolver_send_query中通過在每次調用時改變last_connection來輪流使用不同的dns server進行查詢
static ngx_int_t ngx_resolver_send_query(ngx_resolver_t *r, ngx_resolver_node_t *rn) { ssize_t n; ngx_udp_connection_t *uc; uc = r->udp_connections.elts; uc = &uc[r->last_connection++]; if (r->last_connection == r->udp_connections.nelts) { r->last_connection = 0; } ...
end:如果你覺得本文對你有幫助的話,記得關注點贊轉發,你的支持就是我更新動力。