<-
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 FILE 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 桶,那么整个文件内容将被读入内存,因为每个 apr_bucket_read 调用都会将 FILE 桶变形为 HEAP 桶。

相反,下面的实现将消耗固定数量的内存来过滤任何传送带;需要一个临时传送带,并且必须只在每个响应中分配一次,请参阅维护状态部分。

更好的输出过滤器

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

缓冲桶

如果过滤器决定将桶存储在单个过滤器函数调用持续时间之外(例如将其存储在其 ->ctx 状态结构中),那么这些桶必须被隔离。这是必要的,因为某些桶类型提供表示临时资源(例如堆栈内存)的桶,这些桶将在过滤器链完成处理传送带后立即超出范围。

要隔离桶,可以调用 apr_bucket_setaside 函数。并非所有桶类型都可以被隔离,但如果成功,桶将变形以确保其生命周期至少与作为 apr_bucket_setaside 函数参数给出的池的生命周期一样长。

或者,可以使用 ap_save_brigade 函数,该函数会将所有桶移动到一个单独的传送带中,该传送带包含生命周期与给定池参数一样长的桶。必须谨慎使用此函数,并考虑以下几点

  1. 在返回时,ap_save_brigade 保证返回的旅中所有桶都代表映射到内存中的数据。如果给定一个包含例如 PIPE 桶的输入旅,ap_save_brigade 将消耗任意数量的内存来存储管道的整个输出。
  2. ap_save_brigade 从无法设置的桶中读取时,它将始终执行阻塞读取,从而取消使用 非阻塞桶读取 的机会。
  3. 如果 ap_save_brigade 在不传递非 NULL 的 "saveto"(目标)旅参数的情况下使用,该函数将创建一个新的旅,这可能会导致内存使用量与内容大小成正比,如 旅结构 部分所述。
过滤器必须确保在给定响应的最后一次调用(包含 EOS 桶的旅)期间,处理并向下传递过滤器链中任何缓冲的数据。否则,此类数据将丢失。
top

非阻塞桶读取

apr_bucket_read 函数接受一个 apr_read_type_e 参数,该参数确定将从数据源执行阻塞还是非阻塞读取。一个好的过滤器将首先尝试使用非阻塞读取从每个数据桶中读取;如果读取失败并返回 APR_EAGAIN,则向下发送一个 FLUSH 桶到过滤器链,并使用阻塞读取重试。

这种操作模式确保如果使用缓慢的内容源,过滤器链中更下游的任何过滤器将刷新任何缓冲的桶。

CGI 脚本是作为桶类型实现的缓慢内容源的一个示例。mod_cgi 将发送 PIPE 桶,这些桶代表来自 CGI 脚本的输出;当等待 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 毫秒刷新到客户端的数据块大小。例如,假设在我们的配置中设置 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 字节),并且最终客户端的速度会很快变得与 httpd 配置中设置的速度不同。

在这种情况下,两件事可能会有所帮助

  1. 使用由 mod_ratelimit 为每个响应处理周期初始化的 ctx 内部数据结构,以“记住”上次休眠是在何时执行的,并在跨多个调用时相应地采取行动。
  2. 如果当前正在处理的桶旅无法分成有限数量的 chunk_size 块,则将剩余的字节(位于桶旅的尾部)存储在临时保存区域(即另一个桶旅)中,然后使用 ap_save_brigade 将它们设置到一边。这些字节将被预先附加到将在后续调用中处理的下一个桶旅。
  3. 如果当前正在处理的桶旅包含流结束桶 (EOS),则避免使用前面的逻辑。如果到达流结束,则无需休眠或缓冲数据。

在部分开头链接的提交中还包含一些代码重构,因此在第一次传递期间阅读起来并不容易,但总体思路基本上与现在写的内容相同。本节的目标不是让读者在阅读 C 代码时头疼,而是让他们处于正确的心态,以便有效地使用 httpd 过滤器链工具集提供的工具。

可用语言:  en 

top

评论

注意
这不是问答部分。此处放置的评论应指向有关改进文档或服务器的建议,如果它们被实施或被认为无效/与主题无关,则可能会被我们的版主删除。有关如何管理 Apache HTTP Server 的问题应发送到我们的 IRC 频道 #httpd(在 Libera.chat 上)或发送到我们的 邮件列表