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