摘要:上圖中,每個紅圈表示一個請求,每一層的請求分別是上一層請求的子請求。換而言之,父請求是依賴于子請求的。特別地,的子請求運行時,會阻塞父請求掛起其對應的協程。
張超:又拍云系統開發高級工程師,負責又拍云 CDN 平臺相關組件的更新及維護。Github ID: tokers,活躍于 OpenResty 社區和 Nginx 郵件列表等開源社區,專注于服務端技術的研究;曾為 ngx_lua 貢獻源碼,在 Nginx、ngx_lua、CDN 性能優化、日志優化方面有較為深入的研究。子請求、父請求和主請求
Nginx 所處理的大部分請求,都是在接收到客戶端發來的 HTTP 請求報文后創建的,這些請求直接與客戶端打交道,稱之為主請求;與之相對的則是子請求,顧名思義,子請求是由另外的請求創建的,比如主請求(當然子請求本身也可以創建子請求),當一個請求創建一個子請求后,它就成了該子請求的父請求。從源碼層面來說,當前請求的主請求通過 r->main 指針獲取,父請求則通過 r->parent 指針獲取。
使用子請求機制的意義在于,它能夠分散原本集中在單個請求里的處理邏輯,簡化任務,大大降低請求的復雜度。例如當既需要訪問一個 MySQL 集群,又需要訪問一個 Redis 集群時,我們就可以分別創建一個子請求負責和 MySQL 的交互,另外一個負責和 Redis 的交互,簡化主請求的業務復雜度。而且創建子請求的過程不涉及任何的網絡 I/O,僅僅是一些內存的分配,其代價非常可控,因此在筆者看來,子請求機制是 Nginx 里最為巧妙的設計之一。
子請求創建與驅動通常需要創建子請求時,模塊開發者們可以調用函數 ngx_http_subrequest 來實現,默認情況下,子請求會共享父請求的內存池,變量緩存,下游連接和 HTTP 請求頭等數據。當子請求創建完畢后,它會被掛到 r->main->posted_requests 鏈表上,這個鏈表用以保存需要延遲處理的請求(不局限于子請求)。因此子請求會在父請求本地調度完畢后得到運行的機會,這通常是子請求獲得首次運行機會的手段。
我們知道 Nginx 針對一個 HTTP 請求,將其處理邏輯分別劃分到了 11 個不同的階段。當一個子請求被創建出來后,它首先運行的是 find config 階段,即尋找一個合適的 location,然后開始后續的邏輯處理。通常,如果一個子請求不涉及任何的網絡 I/O 操作,或者定時器處理,一次調度即可完成當前的子請求;而如果子請求需要處理一些網絡、定時器事件,那么后續該子請求的調度,都會由這些事件來驅動,這使得它的調度和普通的主請求變得無差別。
既然除第一次外,子請求的驅動可能是由網絡事件來驅動的,那么子請求的調度就是亂序的了。假設當前主請求需要向后端請求一個大小 2MB 的資源,我們通過產生兩個子請求,分別獲取 0-1MB 和 1MB - 2MB 的部分,然后發往下游,因為網絡的不確定性,很有可能后者(1MB - 2MB)先獲取到并往下游傳輸。那么此時下游所得到的數據就成了臟數據了。
為了解決這個問題,Nginx 為子請求機制引入了另外一個稱為 postpone_filter 的模塊。該模塊的目的在于,判斷當前準備發送數據的請求,是否是“活躍的”,如果當前請求不是“活躍”的,則它期望發送的數據會被暫時保存起來,直到某一刻它“活躍”了,才能將這些數據發往下游。
怎么判斷一個請求是否是“活躍”的?我們需要先了解父、子請求之間的保存形式。對于當前請求,它的子請求以鏈表的方式被維護起來,而前面提到,子請求也可以創建子請求,因此這些請求間完整的保存形式可以理解成一顆分層樹,如下圖所示。
上圖中,每個紅圈表示一個請求,每一層的請求分別是上一層請求的子請求。從樹遍歷的角度講,在這樣一棵樹上,哪個節點應該最先被處理?結合子請求機制的實際意義來分析,子請求是為了分攤父請求的處理邏輯,降低業務復雜度。換而言之,父請求是依賴于子請求的。很大程度上父請求可能需要等到當前子請求運行完畢后根據子請求反饋的結果來做一些收尾工作。所以需要采用的是類似后序遍歷的規則。即上圖最右下角的請求是第一個“活躍”的請求。
從源碼層面來說,這顆分層樹的保存用到了兩個數據結構,r->postponed 和 r->parent這兩個指針,遍歷 r->postponed 來按序訪問當前請求的子請求(樹中同層的兄弟節點);遍歷 r->parent 訪問到父請求(樹中上一層的父節點)。
postpone_filter 模塊會判斷當前請求是否“活躍”,如果不“活躍”,則把將要發送的數據臨時攔截到它自己的 r->postponed鏈表上(所以這個鏈表上其實既有數據也有請求);如果是活躍的,則遍歷它的 r->postponed 鏈表,要么把被臨時攔截下來的數據發送出去,要么找到第一個子請求,將其標記為 “活躍”,然后返回。等到該子請求處理結束,重新將其父請求標記為“活躍”,這樣一來,當父請求再一次運行到 postpone_filter 模塊的時候,又可以遍歷 r->postponed 鏈表,循環往復直到所有請求或者數據處理完畢。感興趣的同學可以自行閱讀相關源碼(http://hg.nginx.org/nginx/file/tip/src/http/ngx_http_postpone_filter_module.c)。
使用了子請求機制的模塊目前整個 Nginx 生態圈,有很多使用子請求的例子,最著名的便是 ngx_lua 的子請求和 Nginx 官方的 slice_filter 模塊了。
ngx_lua 提供給用戶的 API (ngx.location.capture)靈活性非常大。 包括針對是否共享變量也可自行選擇。特別地,ngx_lua 的子請求運行時,會阻塞父請求(掛起其對應的 Lua 協程)。直到子請求運行完畢,子請求的響應頭、響應體(所以如果響應體比較大,則會消耗很多內存)等信息都會返回給父請求。ngx_lua 的子請求是不經過 postpone_filter模塊的,它在一個較早的 filter 模塊(ngx_http_lua_capture_filter) 里就完成了對子請求響應體的攔截。
Nginx 官方提供的 slice_filter模塊,可以將一個資源下載,拆分成若干個 HTTP Range 請求,這樣做最大的好處是分散熱點。這個模塊允許我們設置一個指令 slice_size,用以設置后續 Range 請求的區間大小。該模塊會陸續創建子請求(在前一個完成后),直到所需資源下載完畢。
另外, Nginx/1.13.1 也引入了一個稱為 Background subrequests 的機制(用以更新緩存)。基于這個機制,Nginx/1.13.4 引入了一個 mirror 模塊,通過創建子請求,可以讓用戶自定義一些后臺任務。比如預熱一些資源,直接將它們放入 Nginx 自身的 proxy_cache 緩存中。
陷阱與缺陷前文說到,子請求創建出來時,復用了父請求的一些數據,這無形中引入了一些坑點。
比如變量緩存,如果在子請求中訪問并緩存了某個變量,當后續在父請求中使用時,我們就會得到之前的緩存數據,這可能造成工程師們花費大量的時間和精力去調試這個問題。
另外筆者認為一個非常重大的缺陷是,子請求復用了父請求的內存池,以 slice_filter 模塊舉例,它將一個 HTTP 請求劃分成若干個的子請求,每個子請求向后端發起 HTTP Range 請求,在資源非常大 ,而配置的 slice_size 相對比較小的時候,會造成有大量的子請求的創建,整個資源下載過程可能會持續很長一段時間,這導致父請求的內存池在一段時間內沒有釋放,加之如果并發數比較大,可能會造成進程內存使用率變得很高,嚴重時可能會 OOM,影響到服務。因此在考慮使用的時候,需要權衡這些問題,有必要的話可能需要自行修改源碼,以滿足業務上的需要。
雖然一些缺點是在所難免的,但是子請求機制很大程度上簡化了請求的處理邏輯,它分而治之的處理思想非常值得我們去學習和借鑒,無論如何,子請求機制也將是后續進行系統設計時的一大參考范例。
《我眼中的 Nginx》系列:
我眼中的 Nginx(一):Nginx 和位運算
我眼中的 Nginx(二):HTTP/2 dynamic table size update
我眼中的 Nginx(三):Nginx 變量和變量插值?
我眼中的 Nginx(四):是什么讓你的 Nginx 服務退出這么慢?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/40398.html
摘要:子進程啟動后監控維護區。每來一個新的連接都會觸發新的事件,這些事件送給內的狀態機來處理。大部分的邏輯上都有這樣的狀態機,只是實現方式不一樣。另外通過進程綁定技術可以進一步減少上下文切換和失效等系統開銷。 Nginx在web開發者眼中就是高并發高性能的代名詞,其基于事件的架構也被眾多開發者效仿。我從Nginx的網站找到一篇技術文章將Nginx是怎樣實現的,文章是Nginx的產品老大Owe...
摘要:最近面試了不少公司,正好把記得的問題做個總結。抽象類的接口的區別,不在于編程實現,而在于程序設計模式的不同。一般來講,抽象用于不同的事物,而接口用于事物的行為。 最近面試了不少公司,正好把記得的問題做個總結。 本文 github 會持續更新 公眾號 搜索 蘇生不惑 或者掃二維碼關注,每周更新。 showImg(https://segmentfault.com/img/bVbsYyM?w...
閱讀 1472·2021-10-18 13:29
閱讀 2715·2021-10-12 10:18
閱讀 3588·2021-09-22 15:06
閱讀 2605·2019-08-29 17:09
閱讀 2794·2019-08-29 16:41
閱讀 1500·2019-08-29 13:48
閱讀 3233·2019-08-26 13:49
閱讀 3332·2019-08-26 13:34