工作中使用openresty,使用第三方服務API通過域名訪問。但是,域名通過DNS解析出來之后,在openresty是有
配置解析階段
很多時候我們會在 Nginx 配置文件里配置上一些域名,比如配置我們的上游服務器。
upstream example.com { server test.example.com; }
對於這類域名,Nginx 會在配置解析階段就將其解析出來,接下來(請求處理過程)使用的都是當時解析得到的 IP。Nginx 核心有一個函數 ngx_parse_url
,負責對 url 格式進行分析,包括解析出主機名,端口號以及 URL path 等。針對 IPv4 的情況,它會調用 ngx_parse_inet_url
進行具體的解析任務,如果必要,最終它會調用到 ngx_inet_resolve_host
進行域名解析,ngx_inet_resolve_host
大多情況下會使用 getaddrinfo 進行解析,最終向 /etc/resolv.conf 下所配置的 DNS server 發起解析請求。
歸納來說這個解析過程有兩個特點,一是使用了系統配置的 DNS server;二是解析過程是同步且阻塞的,因此這種解析方式僅在 Nginx 配置解析階段會被使用。另外這種解析方式的缺點就是只解析一次,所以如果在 Nginx 運行過程中域名解析發生了改變也是無法感知到的,除非手動重啟 Nginx 服務。
運行時 DNS resolver
Nginx 核心提供了一套供運行時使用的 DNS 解析機制,它充分契合 Nginx 的事件模型,同樣是異步非阻塞的,並且提供了緩存機制。http、stream 和 mail 模塊分別提供了配置指令(比如 http 模塊提供的 resolver),供我們配置相關 DNS server 地址等信息。
下面這個簡單的反向代理配置,就會在進行代理前解析 www.baidu.com 這個域名。
location / { set $myupstream www.baidu.com; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://${myupstream}/index.html; }
注意如果直接在 proxy_pass
指令里寫明需要代理的域名(即不使用變量的方式),那么域名解析就會發生在配置解析階段了,即上面所講的過程。這其實也是一種實現動態 upstream 的方式。
這套運行時 DNS resolver 其實是一個 DNS client 的角色,由它自己組織查詢報文並發送給目標 DNS 服務器,同時支持解析 IPv6 地址(從 1.5.8
開始),支持反向地址解析和 SRV 解析。它把對每個域名的解析抽象為一棵紅黑樹的節點,包括任何必要的信息。同時這棵紅黑樹也充當着緩存,查詢時會以域名作為 key,如果對應緩存是新鮮的,即會復用緩存,並且會對解析得到的地址順序進行一定的回轉后再提供給上層使用。如果沒有緩存或者緩存過期,新的 DNS 請求會被構建並且發送。
當然,很多時候這套運行時的 DNS resolver 也不能完全滿足需求:
- 無法配置主備 DNS 服務器地址,我們在 resolver 指令里配置的地址都會按順序被輪詢到。
- 無法在 DNS 服務器故障或者網絡質量不佳的情況下復用陳舊的緩存,這可能導致上層服務不可用。
- 每個 Nginx worker 進程獨享解析緩存.
運行時 balancer_by_lua_file
使用 OpenResty 做反向代理的傳統模式是在配置文件的 upstream{ } 塊里書寫多個服務器定義集群。這種方式不夠靈活,增加服務器必須手動修改配置后重啟 OpenResty,會影響正常服務。
OpenResty 的 “balancer_by_lua” 指令讓動態負載均衡稱為可能,它替代了原生的 hash/ip_hash/least_conn 等算法,不僅可以讓自由定制負載均衡策略,還可以隨意調整后端服務器的數量,完全超越了 upstream 系列指令,實現了接近商業版 Nginx Plus 的功能。
使用方式
upstream dyn_backend { # 動態上游集群 server 0.0.0.0; # 占位用,無實際意義 balancer_by_lua_file service /proxy/balancer .lua; # 執行負載均衡的 Lua 代碼 keepalive 10; # 需在 balancer 指令之后 } |
“balancer_by_lua” 也是一個比較特殊的執行階段,在這里不能使用 ngx.sleep、ngx.req.* 或 coocket,同時應當盡量避免大計算量操作或磁盤讀寫,否則會導致阻塞。
動態負載均衡使用的服務器列表通常存儲在外部的 Redis 或 MySQL 里,由於不能直接使用 cosocker,所以在 “balancer_by_lua” 里也就不能操作這些服務器。但這並不是什么大問題,完全可以在其他的階段(例如 access_by_lua、ngx.timer)里訪問服務器獲取列表、解析域名,然后放在 ngx.ctx 或全局模塊里傳遞過來。
支持版本
This directive was first introduced in the v0.10.0
release.
功能接口
在 “balancer_by_lua” 里除了基本的 ngx.* 功能接口外,主要使用的是庫 ngx.balancer,它必須顯式加載后才能使用,即:
local balancer - require "ngx.balancer" -- 顯式加載 ngx.balancer 庫
ngx.balancer 提供四個函數:
- set_current_peer:設置使用的后端服務器,必須是 IP 地址,不能是域名;
- set_timeouts:設置后端的連接和讀寫超時時間,單位是秒;
- set_more_tries:設置連接失敗后的重試次數;
- get_last_failure:獲取上一次連接失敗的具體原因。
這幾個函數的的用法都很簡單,動態負載均衡的重點其實是服務器列表的維護和選擇算法,這些工作通常應該在 “balancer_by_lua” 之外完成,ngx.balancer 只是最后的執行者。
下面的代碼是 ngx.balancer 的典型用法,使用了固定的服務器列表和隨機數來選擇后端,實際應用應該替換為動態更新的數據和更有意義的算法:
local servers = { -- 簡單的服務器列表,IP 地址 {"127.0.0.1", 80}, -- 實際上應該從 Redis {"127.0.0.1", 81}, -- 等服務器里動態加載 } balancer.set_timeouts(1, 0.5, 0.5) -- 后端的連接和讀寫超時時間 balancer.set_more_tries(2) -- 連接失敗后最多在重試 2 次 local n = math.random(#servers) -- 這里使用隨機算法作為示例 local ok, err = balancer.set_current_peer( -- 設置使用的后端服務器 servers[n][1], servers[n][2]) -- 使用 IP 地址和端口號 if not ok then -- 檢查是否設置成功 ngx.log(ngx.ERR, "failed to set peer: ", err) return ngx.exit(500) end
另一種實現方式是把負載均衡算法的主要計算工作放在 “access_by_lua” 等階段里完成,這樣更加靈活,計算出的后端服務器地址等數據放在 ngx.var 或 ngx.ctx 里傳遞,“balancer_by_lua” 階段只需要少量的代碼:
local server = ngx.ctx.server -- 之前計算得到的后端服務器 if balancer.get_last_failure() then -- 后端出錯,需要重新選擇 server = ... -- 重新計算后端服務器 end local ok, err = balancer.set_current_peer(server[1], server[2]) -- 通常無需在計算,直接設置,IP 地址和端口號
動態域名使用
upstream dynamicBackend { server 0.0.0.1; # just an invalid address as a place holder balancer_by_lua_file 'conf/dynamic_domain/balancer_handle.lua'; keepalive 100; # connection pool }
location / {
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
#默認值為0:重試次數不受限制
proxy_next_upstream_tries 2;
#默認情況下只有GET請求會重試(基於這樣的考慮:只有GET請求才是冪等)
proxy_next_upstream error timeout non_idempotent;
access_by_lua_file 'conf/access_handle.lua';
log_by_lua_file 'conf/log_handle.lua';
proxy_pass $proxy_scheme://dynamicBackend;
}