Apache HTTP Server 2.4 版
可用語言: en
撰寫輸出濾網時遇到許多常見的陷阱;此網頁旨在為現有濾網或新增濾網的作者記錄最佳實務。
此文件適用於 Apache HTTP Server 的 2.0 和 2.2 版本;它特別針對 RESOURCE
層級或 CONTENT_SET
層級濾網,不過有些建議適用於所有類型的濾網。
每次呼叫濾網時,都會傳遞一個 串列儲存格,其中包含一組 串列儲存格,表示資料內容和元資料。每個串列儲存格有一個 串列儲存格類型;httpd
核心模組(以及提供串列儲存格介面的 apr-util
函式庫)定義並使用許多串列儲存格類型,但模組可以自由定義自己的類型。
濾網可以使用 APR_BUCKET_IS_METADATA
巨集判斷串列儲存格表示資料或元資料。一般而言,所有元資料儲存格都應該由輸出濾網傳遞至濾網鏈。濾網可以適當地轉換、刪除和插入資料串列儲存格。
所有濾網都必須注意兩種元資料儲存格類型:EOS
儲存格類型和 FLUSH
儲存格類型。EOS
儲存格表示已到達回應結束位置,不需要處理更多串列儲存格。FLUSH
儲存格表示濾網應立即將任何暫存之串列儲存格(如果適用)向下傳遞至濾網鏈。
FLUSH
儲存區。過濾器會立即將 FLUSH
儲存區向下傳遞至過濾器鏈,確保用戶端不用等候未處理資料的時間超過必要。過濾器可以建立 FLUSH
儲存區,然後向下傳遞至過濾器鏈(如果需要)。過度或太頻繁建立 FLUSH
儲存區可能會導致網路使用量下降,這是因為這可能會強迫系統傳送大量封包(而非少量較大的封包)。非封鎖式儲存區讀取 一節,說明鼓勵過濾器建立 FLUSH
儲存區的案例。
HEAP FLUSH 檔案 EOS
這顯示了可能傳遞至過濾器的儲存區中隊;其中包含兩個中繼資料儲存區(FLUSH
和 EOS
),以及兩個資料儲存區(HEAP
和 FILE
)。
針對任何特定要求,只會呼叫輸出過濾器一次,然後提供代表整個回應的單一中隊。過濾器針對單一回應呼叫的次數也時常會與正在過濾的內容大小成比例,每次傳遞一個包含單一儲存區的中隊給過濾器。過濾器在任何一種情況下都必須能正確運作。
輸出過濾器可以透過中隊中 EOS
儲存區的存在,區分出針對特定回應的最後一次呼叫。EOS 之後的中隊中的所有儲存區都應該忽略。
輸出過濾器絕對不應該向下傳遞空中隊至過濾器鏈。為了保護,過濾器應該準備接受空儲存區,並在不向下傳遞這個中隊的情況下傳回成功。處理空儲存區不應該有副作用(例如變更過濾器的任何私有狀態)。
apr_status_t dummy_filter(ap_filter_t *f, apr_bucket_brigade *bb) { if (APR_BRIGADE_EMPTY(bb)) { return APR_SUCCESS; } ...
儲存區中隊是儲存區的雙向連接清單。清單兩端的終結者為指標,此指標可以藉由與 APR_BRIGADE_SENTINEL
傳回的指標進行比較加以區分。清單指標實際上並非有效的儲存區結構;任何嘗試對指標呼叫一般儲存區程式(例如 apr_bucket_read
)的動作,都將導致未定義行為(例如會使處理程序異常終止)。
有各種不同的程式和巨集可以用於移動和調整儲存區中隊;請參閱 apr_buckets.h 標頭,以取得完整說明。常用的巨集包括
APR_BRIGADE_FIRST(bb)
APR_BRIGADE_LAST(bb)
APR_BUCKET_NEXT(e)
APR_BUCKET_PREV(e)
apr_bucket_brigade
結構本身會在池中配置,所以如果濾淨器建立新的工作隊列,則必須確保記憶體使用量被正確限制。例如,一個濾淨器在每次呼叫時,會從要求池 (r->pool
) 中配置新的工作隊列,會違反 上面 有關記憶體使用的警告。這種濾淨器應該針對每個要求在第一次呼叫時建立工作隊列,然後將該工作隊列儲存在 狀態結構 中。
通常建議不要使用 apr_brigade_destroy
來「銷毀」工作隊列,除非你確定工作隊列永遠不會再次被使用,即使如此,也應該很少使用。呼叫這個函數不會釋放工作隊列結構使用的記憶體 (因為它是從池中取得),但關聯池清除註冊會被取消。使用 apr_brigade_destroy
實際上會導致記憶體外洩;如果「已銷毀」工作隊列在包含它的池被銷毀時包含儲存區,那些儲存區將不會立即被銷毀。
一般來說,濾淨器應該優先使用 apr_brigade_cleanup
,而不是 apr_brigade_destroy
。
在處理非資料儲存區時,了解「apr_bucket *
」物件是資料的抽象表示非常重要
->length
欄位會設定為值 (apr_size_t)-1
。例如,PIPE
儲存區類型的儲存區具有不確定長度;它們表示管線的輸出。FILE
儲存區類型表示儲存在磁碟檔案中的資料。濾淨器使用 apr_bucket_read
函數從儲存區讀取資料。當呼叫此函數時,儲存區可能會轉換成不同的儲存區類型,且可能會在儲存區工作隊列中插入新的儲存區。這必須發生在表示未對應到記憶體中的資料的儲存區中。
舉例來說,考慮包含單一 FILE
儲存區的工作隊列,表示整個檔案,大小為 24 千位元組
FILE(0K-24K)
讀取這個儲存區時,它會從檔案中讀取一個資料區塊,轉換為 HEAP
儲存區來表示該資料,然後將資料傳回給呼叫者。它也會插入一個表示檔案剩餘部分的新 FILE
儲存區;呼叫 apr_bucket_read
之後,工作隊列看起來像這樣
HEAP(8K) FILE(8K-24K)
任何輸出過濾器的基本功能都是遍歷傳入的旅進行轉換(或僅檢查)內容的某種方式。遍歷循環的執行對產生行為良好的輸出過濾器至關重要。
使用範例,遍歷整個旅如下所示:
apr_bucket *e = APR_BRIGADE_FIRST(bb); const char *data; apr_size_t length; while (e != APR_BRIGADE_SENTINEL(bb)) { apr_bucket_read(e, &data, &length, APR_BLOCK_READ); e = APR_BUCKET_NEXT(e); } return ap_pass_brigade(bb);
上述執行會消耗與內容大小成正比的記憶體。例如,如果傳遞給 FILE
bucket,每次 apr_bucket_read
呼叫將 FILE
bucket 轉換成 HEAP
bucket,因此會將整個檔案內容讀入記憶體中。
相反地,以下執行會消耗固定數量的記憶體對任何旅進行過濾;需要一個暫存旅,而且必須每個回應只分配一次,請參閱 維護狀態 部分。
apr_bucket *e; const char *data; apr_size_t length; while ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) { rv = apr_bucket_read(e, &data, &length, APR_BLOCK_READ); if (rv) ...; /* Remove bucket e from bb. */ APR_BUCKET_REMOVE(e); /* Insert it into temporary brigade. */ APR_BRIGADE_INSERT_HEAD(tmpbb, e); /* Pass brigade downstream. */ rv = ap_pass_brigade(f->next, tmpbb); if (rv) ...; apr_brigade_cleanup(tmpbb); }
需要對每個回應的多次呼叫維護狀態的過濾器可以使用其 ap_filter_t
結構的 ->ctx
欄位。典型的做法是將暫存旅儲存在此類結構中,避免在每次呼叫時都要分配一個新的旅,如 旅結構 部分所述。
struct dummy_state { apr_bucket_brigade *tmpbb; int filter_state; ... }; apr_status_t dummy_filter(ap_filter_t *f, apr_bucket_brigade *bb) { struct dummy_state *state; state = f->ctx; if (state == NULL) { /* First invocation for this response: initialise state structure. */ f->ctx = state = apr_palloc(f->r->pool, sizeof *state); state->tmpbb = apr_brigade_create(f->r->pool, f->c->bucket_alloc); state->filter_state = ...; } ...
如果過濾器決定在單一過濾器函式呼叫期間儲存 bucket(例如將其儲存在其 ->ctx
狀態結構中),則這些 bucket 必須「區分出來」。這是必要的,因為某些 bucket 類型會提供表示暫時資源(例如堆疊記憶體)的 bucket,在過濾器鏈完成旅的處理後會超出範圍。
要區分 bucket,可以呼叫 apr_bucket_setaside
函式。並非所有類型的 bucket 都可以區分出來,但如果成功,bucket 將進行轉換以確保其使用年限至少與作為引數傳遞給 apr_bucket_setaside
函式的 pool 一樣長。
或者,可以使用 ap_save_brigade
函式,它會將所有 bucket 移到另一個旅中,其中包含使用年限與給定的 pool 引數一樣長的 bucket。必須小心使用此函式,並考量以下各點:
ap_save_brigade
會保證傳回的旅中的所有 bucket 會表示已映射至記憶體的資料。如果給定一個包含 PIPE
bucket 的輸入旅,例如,ap_save_brigade
會消耗任意數量的記憶體來儲存管路的整個輸出。ap_save_brigade
從無法區分出來的 bucket 中讀取資料時,它會一直執行鎖定讀取,因此無法使用 非鎖定 bucket 讀取。ap_save_brigade
而不傳遞非 NULL 的「saveto
」(目的地)旅團參數,該函式將會建立新的旅團,可能導致記憶體使用量與內容大小成正比,如 旅團結構 區段中所述。apr_bucket_read
函式使用 apr_read_type_e
引數,決定是否會從資料來源執行阻擋或非阻擋讀取。良好的過濾器會首先使用非阻擋讀取嘗試讀取每個資料儲存區;如果使用 APR_EAGAIN
失敗,則將 FLUSH
儲存區傳遞給過濾器鏈,並使用阻擋讀取重新嘗試。
此操作模式可確保在使用緩慢內容來源時,過濾器鏈中任何進一步的過濾器會清除任何緩衝儲存區。
CGI 程式是實作為儲存區類型的緩慢內容來源範例。 mod_cgi
將傳送表示 CGI 程式輸出的 PIPE
儲存區;當等待 CGI 程式產生更多輸出的時候,讀取此類儲存區會阻擋。
apr_bucket *e; apr_read_type_e mode = APR_NONBLOCK_READ; while ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) { apr_status_t rv; rv = apr_bucket_read(e, &data, &length, mode); if (rv == APR_EAGAIN && mode == APR_NONBLOCK_READ) { /* Pass down a brigade containing a flush bucket: */ APR_BRIGADE_INSERT_TAIL(tmpbb, apr_bucket_flush_create(...)); rv = ap_pass_brigade(f->next, tmpbb); apr_brigade_cleanup(tmpbb); if (rv != APR_SUCCESS) return rv; /* Retry, using a blocking read. */ mode = APR_BLOCK_READ; continue; } else if (rv != APR_SUCCESS) { /* handle errors */ } /* Next time, try a non-blocking read first. */ mode = APR_NONBLOCK_READ; ... }
總之,以下是供所有輸出過濾器遵循的一套守則
FLUSH
儲存區。EOS
儲存區之後的任何儲存區。ap_pass_brigade
來將旅團傳遞給過濾器鏈之後,輸出過濾器應呼叫 apr_brigade_cleanup
,以確保在重複使用該旅團結構之前,旅團為空;輸出過濾器切勿使用 apr_brigade_destroy
來「摧毀」旅團。ap_pass_brigade
的傳回值,並且必須將適當的錯誤傳回過濾器鏈。FLUSH
區塊到過濾器鍊,再使用阻隔式讀取重試。r1833875 變更是一個很好的範例,顯示在輸出過濾器的脈絡中,緩衝和保留狀態是什麼意思。在此使用個案中,使用者在使用者的寄件清單上詢問了一個有趣的問題,關於為何 mod_ratelimit
似乎不尊重其設定,使用代理內容(速率限制為不同的速度或根本不執行)。在深入探討解決方案之前,最好在高層級解釋一下 mod_ratelimit
如何運作。技巧真的非常簡單:使用速率限制設定並計算資料的區塊大小,每 200 毫秒傳輸一次至 Client。例如,假設想像在設定中設定 rate-limit 60
,以下為尋找區塊大小的高層級步驟
/* milliseconds to wait between each flush of data */ RATE_INTERVAL_MS = 200; /* rate limit speed in b/s */ speed = 60 * 1024; /* final chunk size is 12228 bytes */ chunk_size = (speed / (1000 / RATE_INTERVAL_MS));
如果將此計算套用於承載 38400 位元組的區塊隊伍,這表示過濾器會嘗試執行下列動作
以上偽程式碼在輸出過濾器處理每個回應只有一個隊伍的情況下正常運作,但可能會需要多次呼叫來處理不同隊伍大小的情況。前者的使用個案是例如,當 httpd 直接傳遞某些內容時,如靜態檔案:區塊隊伍抽象化負責處理整個內容,而速率限制則運作良好。但如果相同的靜態內容是透過 mod_proxy_http 傳遞(例如,後端在傳遞而非 httpd),那麼內容產生器(在此個案中為 mod_proxy_http)可能會使用一個最大的緩衝區大小,然後定期將資料作為區塊隊伍傳送至輸出過濾器鍊,當然會觸發多次呼叫 mod_ratelimit
。如果讀者嘗試執行偽程式碼,假設多次呼叫輸出過濾器,每個呼叫需要處理一個 38400 個位元組的區塊隊伍,那麼很容易發現一些異常現象
在這種情況下,以下兩項措施可能有所幫助
mod_ratelimit
初始化的 ctx 內部資料結構,用於「記住」跨多個呼叫時最後一次執行休眠的時間,並根據情況採取相應的措施。ap_save_brigade
將它們保留。這些位元組將會加在後續呼叫中處理的下一個區塊之前。本節開頭連結的提交也包含一些程式碼重新組態,因此第一次閱讀時並不容易理解,但整體概念基本上就像到目前為止所寫的一樣。本節目標並非讓讀者在嘗試閱讀 C 程式碼時頭痛不已,而是讓他們具備正確的心態,以有效率的方式使用 httpd 篩選器鏈工具組所提供的工具。
可用語言: en