OpenResty中的upstream healthcheck功能沉思录
healthcheck功能本質上還是個定時器,去定期檢查指定upstream組的狀態,它發送指定的http請求并解析響應碼,去探測upstream中每個peer的存活狀態,再結合歷史請求記錄來判斷并標記其狀態,如果有狀態改變,就在共享內存中更新版本記錄,下次執行時,所有的worker進程都要更新到最新的peer狀態。
下面的表述都假定我們要監控的upstream組名是ats_node_backend, 也就是對應nginx.conf中的這個代碼塊
upstream ats_node_backend {
? ? server 127.0.0.2;
? ? server 127.0.0.3 backup;
? ? server 127.0.0.4 fail_timeout=23 weight=7 max_fails=200 backup;
}
配置參數解釋
hc.spawn_checker(options)
options中包含如下選項,在調用該接口時作為參數傳遞進來
type? ? ? ? 必須存在并且是http,目前只支持http
http_req? 必須存在,健康探測的http請求raw字符串
timeout? ? 默認1000,單位ms
interval? ? 健康探測的時間間隔,單位ms, 默認1000,推薦2000
valid_status? ?合法響應碼的表,比如{200, 302}
concurrency? ?并發數,默認1
fall? ? ? ? ? ? 默認5,對UP的設備,連續fall次失敗,認定為DOWN
rise? ? ? ? ? ?默認2,對DOWN的設備,連續rise次成功,認定為UP
shm? ? ? ?必須配置,用于健康檢查的共享內存名稱,通過ngx.shared[shm]得到共享內存
upstream? ?指定要做健康檢查的upstream組名,必須存在
version? 默認0
primary_peers? 主組
backup_peers? 備組
statuses? 存放合法響應碼的數組,來自ipairs()得到的valid_status配置項
根據options會構造一個ctx表來存放所有的配置數據,并會作為定時器ngx.timer.at()中的第三個參數
ctx的內容如下
upstream? ?指定的upstream組名
primary_peers 主組
backup_peers? 備組
http_req? 健康檢查的raw http請求
timeout? 超時時間,單位s,注意不是ms
interval? 健康檢查的間隔,單位s,注意不是ms
dict? 存放統計數據的共享內存
fall? ?認為DOWN之前的連續失敗次數,默認5
rise? 認為UP之前的連續成功次數,默認2
statuses? 認為正常的http狀態碼的表{200,302}
version? ? 0 每次執行定時任務時的版本號,有peer狀態改變,版本號加1
concurrency? ? 創建該數目的輕量線程來并發發送健康檢測請求的個數
上面的這些配置項,將作為一個上下文保存下來,在不同的階段被反復調用
使用方法
在init_worker_by_lua_block階段,在nginx.conf中放入下面的代碼
local hc = require "resty.upstream.healthcheck"local ok, err = hc.spawn_checker{shm = "healthcheck", -- defined by "lua_shared_dict"upstream = "foo.com", -- defined by "upstream"type = "http",http_req = "GET /status HTTP/1.0\r\nHost: foo.com\r\n\r\n",-- raw HTTP request for checkinginterval = 2000, -- run the check cycle every 2 sectimeout = 1000, -- 1 sec is the timeout for network operationsfall = 3, -- # of successive failures before turning a peer downrise = 2, -- # of successive successes before turning a peer upvalid_statuses = {200, 302}, -- a list valid HTTP status codeconcurrency = 10, -- concurrency level for test requests}if not ok thenngx.log(ngx.ERR, "failed to spawn health checker: ", err)returnend
這里Host可以是127.0.0.1或是其它ip。如果你需要一個查看探測的upstream組的狀態,可以再在nginx.conf中配置
server {...# status page for all the peers:location = /status {access_log off;allow 127.0.0.1;deny all;default_type text/plain;content_by_lua_block {local hc = require "resty.upstream.healthcheck"ngx.say("Nginx Worker PID: ", ngx.worker.pid())ngx.print(hc.status_page())}}}
然后在外面直接訪問
wget 'http://127.0.0.1/status'
就可以看到所有upstream的peer狀態,特別是我們檢測的ats_node_backend這組,沒有配置檢測的upstream組前面會有"(NO checkers)"字樣。
代碼架構
本身是個定時器,該模塊對外提供了兩個接口
該定時器內干的活兒如下:
自問自答
1.如何對一個peer做健康探測?
通俗的健康探測可以是嘗試與peer連接,或者發送http請求。后者是一種比較精確地檢查服務是否正常的情況,因為只是做端口探測的話,如果服務僵死了,端口還是探測得通的。
healthcheck模塊這里采用lua輕量級線程去專門向要檢查的服務發送指定的http請求,并接收服務的響應狀態碼,根據狀態碼的情況來判斷服務是否是正常的,斷定UP或是DOWN
探測的結果無非兩種情況:ok或是fail。因此,分為兩種情況來對應存放,比如以key為ok:ats_node_backend:p9的記錄來primary peers中peer id為9的設備的累計成功次數,同理nok:ats_node_backend:b1表示backup peers中peer id為1的設備的累計失敗次數。
如果當前探測結果是成功,會首先去共享內存中查詢累計的成功次數,并在原來的次數基礎上增加1并更新到共享內存中。修改之后如果次數為0,就需要將對應的失敗記錄從共享內存中清除。
需要考慮3個因素,參見上面配置的標注(1)(2)(3):
(1)當前peer的狀態peer.down, 從upstream模塊的接口upstream.get_primary_servers()中獲取peer.down的值, 另外也要謹慎處理當前worker中的這個peer.down值
(2)目前成功或是失敗的次數
(3)healthcheck模塊配置中的判斷成功或失敗的閾值,從定時器的ctx參數中可以得到, ctx.ok或ctx.fail
下面以判斷下線狀態為例來說明:
該peer當前是上線狀態,也就是peer.down=nil或false,fails次數超過設定失敗閾值,認定為下線(DOWN)
進一步的操作為調用set_peer_down_globally()函數,同時更改peer.down=true
關系是一樣的
5.節點內多臺設備的健康探測是怎樣執行的?
采用nginx輕量級線程去并發執行多個檢查任務,異步執行完之后,回調處理函數,處理完后該線程死掉。
6.當一個worker在healthcheck過程中發現某個peer掉線了,它是如何處理的?它如何將該peer的狀態傳遞到所有其它worker知道?
在該腳本中是分為三步來完成的,參考上面的圖示:(1)在set_peer_down_globally()函數中,對探測做出的結果,來設置一次set_peer_down,同時給出下面的兩步來誘發后面的處理
說明ctx.new_version=true;
同時在共享內存中存入對應的記錄,標記以d:ats_node_backend:p9為key的記錄來表明該peer是UP還是DOWN狀態
(2)在do_check()函數中,如果有新版本的話,先查詢共享內存中的v:ats_node_backend的記錄的值,加1,更新ctx.version,同時置空ctx.new_version。
這里的代碼處理非常巧妙:
dict:add(key, 0)
local new_ver, err = dict:incr(key, 1)
使用dict:add()是為了避免插入重復的鍵值,如果該鍵已經存在,直接返回nil和和err="exists",如果不存在,直接置0
下一步是在原來的基礎上加1,這一步妥妥能執行。
(3)在下一次定時任務執行時,所有worker進程首先去共享內存中查找key為v:ats_node_backend的記錄的值,也就是獲取ctx.version的精確值。比較當前值與ctx.version,如果ctx.version的值小于共享內存中的值,說明需要更新peer版本。
注意每條日志的worker進程號不一樣,說明它們都在更新peer版本號。
7.其它的worker如何更新peer版本呢?
就是去共享內存中檢查key為d:ats_node_backend:p9的記錄,如果存在該記錄,說明peer是DOWN,否則peer是UP。只有peer.down和查到的值不同,就需要set_peer_down,同時設置peer.down=down這主要是涉及到nginx中worker進程的執行和配置同步問題。一般來說,通常每份代碼都會被所有的worker執行到,但是對peers的健康檢查,我們只需要一個worker去執行就夠了,一個worker執行完后,其它的worker來同步它的狀態就夠了。在do_check中我們看到除了下面的get_lock保護的代碼是某個worker執行的,其它的代碼,所有的worker都會去執行。定時任務采用進程搶占方式,每次執行的worker進程并不固定,這樣的話,ctx.version一般是不連續的,通過共享內存方式,可以保證每個worker進程每次都可以得到最新的peer信息,而且peer version是逐漸遞增的。
但是在一個worker執行過程中,ctx中的所有的字段都是可以在函數中傳遞的。
9.為啥要所有的worker進程都執行set_peer_down這個函數?
https://github.com/openresty/lua-upstream-nginx-module#set_peer_down
上面的官方文檔強調了,該函數的執行必須所有的worker都執行,才能在server級別上生效。只在一個worker中執行,只能在該worker內部生效。
10.在nginx.conf中調用時,能否采用單進程調用方式,比如使用ngx.worker.id()==0的if條件語句?
這也是我曾經犯過的錯誤,答案當然是否定的。原因有兩個:一個是為了讓upstream.set_peer_down接口在server級別生效,必須所有的worker進程都要調用該函數;另一個,就是healthcheck模塊內部已經實現了使用單個worker去進行實際的健康檢測功能。
11.外部如何獲取upstream中各peer之間的狀態?
該模塊對外提供了一個狀態查詢接口_M.status_page()下面說下它的處理細節:
在status_page()中會創建一個局部數組類型的表,來獲取所有的統計信息,最后將這些數組中的元素拼接成字符串。
local bits = new_tab(n * 20, 0)
n = #us? ?us是upstream的縮寫,指upstream的組數,每組upstream會占用20個數組元素
table.concat? 將table中的元素按照指定分隔符連接起來
bits數組內容如下
bits[0]="Upstream "
bits[1]="ats_node_backend"
bits[2]="? ?(NO checkers)"
bits[3]="\n? ? Primary Peers\n"
bits[4]="? ? ? ? "
bits[5]="10.10.101.10:18980"
bits[6]=" DOWN\n"
……
bits[n]="? ?Backup Peers\n"
bits[n+1]="? ? ? ? "
bits[n+2]="10.10.101.12:18980"
bits[n+3]=" up\n"
……
bits[n+m]="\n"
參考文獻
[1].https://github.com/openresty/lua-resty-upstream-healthcheck
[2].https://github.com/openresty/lua-upstream-nginx-module#set_peer_down
總結
以上是生活随笔為你收集整理的OpenResty中的upstream healthcheck功能沉思录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在MacBook Pro 2015上安装
- 下一篇: 在Ubuntu 16.04.3 LTS上