<-
Apache > HTTP Server > 文件 > 2.4 版 > 開發人員文件

輸出濾網撰寫指南

可用語言:  en 

撰寫輸出濾網時遇到許多常見的陷阱;此網頁旨在為現有濾網或新增濾網的作者記錄最佳實務。

此文件適用於 Apache HTTP Server 的 2.0 和 2.2 版本;它特別針對 RESOURCE 層級或 CONTENT_SET 層級濾網,不過有些建議適用於所有類型的濾網。

Support Apache!

請參閱

top

濾網和串列儲存格

每次呼叫濾網時,都會傳遞一個 串列儲存格,其中包含一組 串列儲存格,表示資料內容和元資料。每個串列儲存格有一個 串列儲存格類型httpd核心模組(以及提供串列儲存格介面的 apr-util 函式庫)定義並使用許多串列儲存格類型,但模組可以自由定義自己的類型。

輸出濾網必須準備處理非標準類型的串列儲存格;除了少數例外,濾網無需在意所過濾之串列儲存格類型。

濾網可以使用 APR_BUCKET_IS_METADATA 巨集判斷串列儲存格表示資料或元資料。一般而言,所有元資料儲存格都應該由輸出濾網傳遞至濾網鏈。濾網可以適當地轉換、刪除和插入資料串列儲存格。

所有濾網都必須注意兩種元資料儲存格類型:EOS 儲存格類型和 FLUSH 儲存格類型。EOS 儲存格表示已到達回應結束位置,不需要處理更多串列儲存格。FLUSH 儲存格表示濾網應立即將任何暫存之串列儲存格(如果適用)向下傳遞至濾網鏈。

當內容產生器(或上游過濾器)知道在傳送更多內容之前可能有延遲時,就會傳送 FLUSH 儲存區。過濾器會立即將 FLUSH 儲存區向下傳遞至過濾器鏈,確保用戶端不用等候未處理資料的時間超過必要。

過濾器可以建立 FLUSH 儲存區,然後向下傳遞至過濾器鏈(如果需要)。過度或太頻繁建立 FLUSH 儲存區可能會導致網路使用量下降,這是因為這可能會強迫系統傳送大量封包(而非少量較大的封包)。非封鎖式儲存區讀取 一節,說明鼓勵過濾器建立 FLUSH 儲存區的案例。

建立儲存區中隊的範例

HEAP FLUSH 檔案 EOS

這顯示了可能傳遞至過濾器的儲存區中隊;其中包含兩個中繼資料儲存區(FLUSHEOS),以及兩個資料儲存區(HEAPFILE)。

top

過濾器呼叫

針對任何特定要求,只會呼叫輸出過濾器一次,然後提供代表整個回應的單一中隊。過濾器針對單一回應呼叫的次數也時常會與正在過濾的內容大小成比例,每次傳遞一個包含單一儲存區的中隊給過濾器。過濾器在任何一種情況下都必須能正確運作。

每次呼叫後,如果輸出過濾器分配出永續生命週期的記憶體,可能會使用與回應大小成比例的記憶體。需要分配記憶體的輸出過濾器,應該針對每個回應分配一次記憶體;請參閱以下的 維護狀態

輸出過濾器可以透過中隊中 EOS 儲存區的存在,區分出針對特定回應的最後一次呼叫。EOS 之後的中隊中的所有儲存區都應該忽略。

輸出過濾器絕對不應該向下傳遞空中隊至過濾器鏈。為了保護,過濾器應該準備接受空儲存區,並在不向下傳遞這個中隊的情況下傳回成功。處理空儲存區不應該有副作用(例如變更過濾器的任何私有狀態)。

如何處理空儲存區

apr_status_t dummy_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    if (APR_BRIGADE_EMPTY(bb)) {
        return APR_SUCCESS;
    }
    ...
top

中隊結構

儲存區中隊是儲存區的雙向連接清單。清單兩端的終結者為指標,此指標可以藉由與 APR_BRIGADE_SENTINEL 傳回的指標進行比較加以區分。清單指標實際上並非有效的儲存區結構;任何嘗試對指標呼叫一般儲存區程式(例如 apr_bucket_read)的動作,都將導致未定義行為(例如會使處理程序異常終止)。

有各種不同的程式和巨集可以用於移動和調整儲存區中隊;請參閱 apr_buckets.h 標頭,以取得完整說明。常用的巨集包括

APR_BRIGADE_FIRST(bb)
傳回中隊 bb 中的第一個儲存區
APR_BRIGADE_LAST(bb)
傳回中隊 bb 中的最後一個儲存區
APR_BUCKET_NEXT(e)
提供儲存區 e 之後的下一個儲存區
APR_BUCKET_PREV(e)
提供儲存區 e 之前的儲存區

apr_bucket_brigade 結構本身會在池中配置,所以如果濾淨器建立新的工作隊列,則必須確保記憶體使用量被正確限制。例如,一個濾淨器在每次呼叫時,會從要求池 (r->pool) 中配置新的工作隊列,會違反 上面 有關記憶體使用的警告。這種濾淨器應該針對每個要求在第一次呼叫時建立工作隊列,然後將該工作隊列儲存在 狀態結構 中。

通常建議不要使用 apr_brigade_destroy 來「銷毀」工作隊列,除非你確定工作隊列永遠不會再次被使用,即使如此,也應該很少使用。呼叫這個函數不會釋放工作隊列結構使用的記憶體 (因為它是從池中取得),但關聯池清除註冊會被取消。使用 apr_brigade_destroy 實際上會導致記憶體外洩;如果「已銷毀」工作隊列在包含它的池被銷毀時包含儲存區,那些儲存區將不會立即被銷毀。

一般來說,濾淨器應該優先使用 apr_brigade_cleanup,而不是 apr_brigade_destroy

top

處理儲存區

在處理非資料儲存區時,了解「apr_bucket *」物件是資料的抽象表示非常重要

  1. 儲存區表示的資料量可能會有固定長度,也可能沒有;對於表示不確定長度的資料的儲存區,->length 欄位會設定為值 (apr_size_t)-1。例如,PIPE 儲存區類型的儲存區具有不確定長度;它們表示管線的輸出。
  2. 由儲存區表示的資料可能已對應到記憶體中,也可能沒有。例如,FILE 儲存區類型表示儲存在磁碟檔案中的資料。

濾淨器使用 apr_bucket_read 函數從儲存區讀取資料。當呼叫此函數時,儲存區可能會轉換成不同的儲存區類型,且可能會在儲存區工作隊列中插入新的儲存區。這必須發生在表示未對應到記憶體中的資料的儲存區中。

舉例來說,考慮包含單一 FILE 儲存區的工作隊列,表示整個檔案,大小為 24 千位元組

FILE(0K-24K)

讀取這個儲存區時,它會從檔案中讀取一個資料區塊,轉換為 HEAP 儲存區來表示該資料,然後將資料傳回給呼叫者。它也會插入一個表示檔案剩餘部分的新 FILE 儲存區;呼叫 apr_bucket_read 之後,工作隊列看起來像這樣

HEAP(8K) FILE(8K-24K)

top

過濾工作隊列

任何輸出過濾器的基本功能都是遍歷傳入的旅進行轉換(或僅檢查)內容的某種方式。遍歷循環的執行對產生行為良好的輸出過濾器至關重要。

使用範例,遍歷整個旅如下所示:

不佳的輸出過濾器——請勿模仿!

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);
}
top

維護狀態

需要對每個回應的多次呼叫維護狀態的過濾器可以使用其 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 = ...;
    }
    ...
top

暫存 bucket

如果過濾器決定在單一過濾器函式呼叫期間儲存 bucket(例如將其儲存在其 ->ctx 狀態結構中),則這些 bucket 必須「區分出來」。這是必要的,因為某些 bucket 類型會提供表示暫時資源(例如堆疊記憶體)的 bucket,在過濾器鏈完成旅的處理後會超出範圍。

要區分 bucket,可以呼叫 apr_bucket_setaside 函式。並非所有類型的 bucket 都可以區分出來,但如果成功,bucket 將進行轉換以確保其使用年限至少與作為引數傳遞給 apr_bucket_setaside 函式的 pool 一樣長。

或者,可以使用 ap_save_brigade 函式,它會將所有 bucket 移到另一個旅中,其中包含使用年限與給定的 pool 引數一樣長的 bucket。必須小心使用此函式,並考量以下各點:

  1. 在傳回時,ap_save_brigade 會保證傳回的旅中的所有 bucket 會表示已映射至記憶體的資料。如果給定一個包含 PIPE bucket 的輸入旅,例如,ap_save_brigade 會消耗任意數量的記憶體來儲存管路的整個輸出。
  2. ap_save_brigade 從無法區分出來的 bucket 中讀取資料時,它會一直執行鎖定讀取,因此無法使用 非鎖定 bucket 讀取
  3. 如果使用 ap_save_brigade 而不傳遞非 NULL 的「saveto」(目的地)旅團參數,該函式將會建立新的旅團,可能導致記憶體使用量與內容大小成正比,如 旅團結構 區段中所述。
過濾器必須確保在特定回應(包含 EOS 儲存區的旅團)的最後一次呼叫期間,處理任何緩衝資料並傳遞給過濾器鏈。否則,這些資料將遺失。
top

非阻擋儲存區讀取

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;
    ...
}
top

輸出過濾器十項守則

總之,以下是供所有輸出過濾器遵循的一套守則

  1. 輸出過濾器不應將空旅團傳遞給過濾器鏈,但應容忍傳遞空旅團給它們。
  2. 輸出過濾器必須將所有中繼資料儲存區傳遞給過濾器鏈;應透過傳遞任何待處理或緩衝的儲存區給過濾器鏈,來尊重 FLUSH 儲存區。
  3. 輸出過濾器應忽略 EOS 儲存區之後的任何儲存區。
  4. 輸出過濾器一次必須處理固定數量的資料,以確保記憶體消耗不會與過濾內容大小成正比。
  5. 輸出過濾器應不論儲存區類型而知,並且必須能夠處理類型陌生的儲存區。
  6. 呼叫 ap_pass_brigade 來將旅團傳遞給過濾器鏈之後,輸出過濾器應呼叫 apr_brigade_cleanup,以確保在重複使用該旅團結構之前,旅團為空;輸出過濾器切勿使用 apr_brigade_destroy 來「摧毀」旅團。
  7. 輸出過濾器必須將在過濾器函式執行時間外保留的任何儲存區「保留」。
  8. 輸出過濾器不可忽略 ap_pass_brigade 的傳回值,並且必須將適當的錯誤傳回過濾器鏈。
  9. 輸出過濾器必須只為每個回應,建立固定數量的儲存區旅團,而非每個呼叫建立一個旅團。
  10. 輸出過濾器應先嘗試從每個資料區塊進行非阻隔式讀取,並在讀取區塊前向下傳送 FLUSH 區塊到過濾器鍊,再使用阻隔式讀取重試。
top

使用個案:緩衝 mod_ratelimit

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 位元組的區塊隊伍,這表示過濾器會嘗試執行下列動作

  1. 將 38400 個位元組分割成每個最多 12228 個位元組的區塊。
  2. 傳送出第一個 12228 個位元組的區塊,並休眠 200 毫秒。
  3. 傳送出第二個 12228 個位元組的區塊,並休眠 200 毫秒。
  4. 傳送出第三個 12228 個位元組的區塊,並休眠 200 毫秒。
  5. 傳送出剩下的 1716 個位元組。

以上偽程式碼在輸出過濾器處理每個回應只有一個隊伍的情況下正常運作,但可能會需要多次呼叫來處理不同隊伍大小的情況。前者的使用個案是例如,當 httpd 直接傳遞某些內容時,如靜態檔案:區塊隊伍抽象化負責處理整個內容,而速率限制則運作良好。但如果相同的靜態內容是透過 mod_proxy_http 傳遞(例如,後端在傳遞而非 httpd),那麼內容產生器(在此個案中為 mod_proxy_http)可能會使用一個最大的緩衝區大小,然後定期將資料作為區塊隊伍傳送至輸出過濾器鍊,當然會觸發多次呼叫 mod_ratelimit。如果讀者嘗試執行偽程式碼,假設多次呼叫輸出過濾器,每個呼叫需要處理一個 38400 個位元組的區塊隊伍,那麼很容易發現一些異常現象

  1. 在一個隊伍的最後一次傳送與下一個隊伍的第一次傳送之間,沒有休眠。
  2. 即使在最後一次傳送後強制休眠,那區塊大小也不是理想的大小(1716 個位元組,而非 12228 個位元組),且最終的 Client 速度會與 httpd 設定中所設定的快速相去甚遠。

在這種情況下,以下兩項措施可能有所幫助

  1. 使用由 mod_ratelimit 初始化的 ctx 內部資料結構,用於「記住」跨多個呼叫時最後一次執行休眠的時間,並根據情況採取相應的措施。
  2. 如果一個區塊無法分割成有限數量的 chunk_size 區塊,請將剩餘位元組(位於區塊的尾部)儲存在暫時保留區(也就是另一個區塊),然後使用 ap_save_brigade 將它們保留。這些位元組將會加在後續呼叫中處理的下一個區塊之前。
  3. 如果目前正在處理的區塊包含串流結尾區塊 (EOS),請避免使用上一個邏輯。如果已到達串流結尾,則不需要執行休眠或緩衝資料。

本節開頭連結的提交也包含一些程式碼重新組態,因此第一次閱讀時並不容易理解,但整體概念基本上就像到目前為止所寫的一樣。本節目標並非讓讀者在嘗試閱讀 C 程式碼時頭痛不已,而是讓他們具備正確的心態,以有效率的方式使用 httpd 篩選器鏈工具組所提供的工具。

可用語言:  en 

top

意見

注意事項
此處並非問答區。放在這裡的留言應針對改善文件或伺服器提出建議,如果我們的管理員將其實作或認為無效/與主題無關,則可能會移除這些留言。有關如何管理 Apache HTTP 伺服器的問題,應轉到我們的 IRC 頻道(Libera.chat 上的 #httpd),或傳送至我們的 郵件清單