apache / 2.4 / reference / developer-output-filters.html

编写输出过滤器的指南

编写输出滤波器时会遇到许多常见的陷阱;此页面旨在为新过滤器或现有过滤器的作者记录最佳实践。

本文档适用于 Apache HTTP Server 的 2.0 版和 2.2 版。尽管某些建议对于所有类型的过滤器都是通用的,但它专门针对RESOURCE级或CONTENT_SET级过滤器。

过滤器和水桶旅

每次调用过滤器时,都会传递一个* bucket brigade ,其中包含一个 buckets *序列,代表数据内容和元数据。每个存储桶都有一个“存储桶类型”; httpd核心模块(和提供 bucket brigade 接口的apr-util库)定义并使用了许多存储桶类型,但是模块可以自由定义自己的类型。

Note

必须准备输出过滤器以处理非标准类型的铲斗;除少数 exception,过滤器无需关心要过滤的桶的类型。

过滤器可以使用APR_BUCKET_IS_METADATA宏来判断存储桶是表示数据还是元数据。通常,所有元数据段都应由输出过滤器沿过滤器链向下传递。过滤器可以适当地转换,删除和插入数据桶。

所有过滤器都必须注意两种元数据存储桶类型:EOS存储桶类型和FLUSH存储桶类型。 EOS存储桶表示已到达响应的结尾,并且不再需要处理其他存储桶。 FLUSH桶表示过滤器应立即沿过滤器链向下冲洗所有缓冲桶(如果适用)。

Note

当内容生成器(或上游过滤器)知道可能会有延迟才能发送更多内容时,将发送FLUSH个存储桶。通过立即在过滤器链中传递FLUSH个存储桶,过滤器可确保 Client 端 await 未决数据的时间不会超过所需时间。

过滤器可以创建FLUSH个存储桶,并根据需要将其沿过滤器链向下传递。不必要或太频繁地生成FLUSH存储桶可能会损害网络利用率,因为这可能会强制发送大量的小数据包,而不是少量的大数据包。 非阻塞存储桶读取部分介绍了鼓励过滤器生成FLUSH个存储桶的情况。

铲斗旅示例

HEAP FLUSH FILE EOS

这显示了可以通过过滤器的铲斗旅。它包含两个元数据存储区(FLUSHEOS)和两个数据存储区(HEAPFILE)。

Filter invocation

对于任何给定的请求,输出过滤器可能仅被调用一次,并被赋予代表整个响应的单个旅。也有可能针对单个响应调用过滤器的次数与要过滤的内容的大小成比例,每次使过滤器通过一个包含单个存储桶的旅。在任何一种情况下,过滤器都必须正确运行。

Warning

每次调用时分配一个长寿命内存的输出过滤器可能会消耗与响应大小成比例的内存。需要分配内存的输出过滤器应在每个响应中分配一次;请参阅下面的Maintaining state

输出过滤器可以通过在旅中存在EOS桶来区分给定响应的最终调用。 EOS 之后,旅中的任何水桶都应忽略。

输出过滤器绝不能使空的旅通过过滤器链。为了防御起见,过滤器应准备好接受一个空的旅,并且应该返回成功而不使该旅沿过滤器链向下通过。空旅的处理不应有任何副作用(例如更改过滤器专用的任何状态)。

如何处理空旅

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

Brigade structure

桶旅是一个双向链接的桶 Lists。该列表以* sentinel *终止(在两端),可以通过将其与APR_BRIGADE_SENTINEL返回的指针进行比较来区别于正常存储桶。实际上,Lists 哨兵不是有效的存储区结构;在前哨上调用常规存储桶函数(例如apr_bucket_read)的任何尝试都将具有未定义的行为(即,将导致进程崩溃)。

遍历和操纵铲斗旅有多种功能和宏。请参阅apr_buckets.hHeaders 以了解完整的覆盖范围。常用的宏包括:

  • 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)中分配一个新的旅,这将违反关于内存使用的warning above。此类过滤器应改为在每个请求的第一个调用上创建一个旅,并将该旅存储在其state structure中。

Warning

通常不建议使用apr_brigade_destroy来“消灭”一个旅团,除非您确定该旅团将不再使用,即使如此,也应该很少使用它。调用此函数不会释放旅结构使用的内存(因为它来自池),但是关联的池清理未注册。实际上使用apr_brigade_destroy可能会导致内存泄漏;如果一个“被摧毁”的旅在其收容池被破坏时也有水桶,这些水桶将立即被销毁。

通常,过滤器应优先使用apr_brigade_cleanup而不是apr_brigade_destroy

Processing buckets

在处理非元数据存储桶时,重要的是要了解“ apr_bucket *”对象是数据的抽象表示

  • 存储桶表示的数据量可以具有或可以不具有确定的长度。对于代表不确定长度数据的存储桶,->length字段设置为值(apr_size_t)-1。例如,PIPE桶类型的桶的长度不确定。它们代表管道的输出。

  • 存储桶表示的数据可能会 Map 也可能不会 Map 到内存中。例如,FILE存储桶类型表示存储在磁盘文件中的数据。

过滤器使用apr_bucket_read函数从存储桶中读取数据。调用此函数时,存储桶可能会“变形”为其他存储桶类型,也可能会在存储桶大队中插入新的存储桶。对于表示未 Map 到内存的数据的存储桶,必须发生这种情况。

举个例子;考虑一个包含一个FILE桶的桶大队,该桶代表整个文件,大小为 24 KB:

FILE(0K-24K)

读取此存储桶时,它将从文件中读取一个数据块,变形为HEAP存储桶以表示该数据,然后将数据返回给调用方。它还会插入一个代表文件其余部分的新FILE存储桶; apr_bucket_read通话后,该旅的外观如下:

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

Filtering brigades

任何输出过滤器的基本功能将是遍历传入的旅并以某种方式转换(或简单地检查)内容。迭代循环的实现对于生成行为良好的输出滤波器至关重要。

以遍历整个旅的示例为例:

输出滤波器不良-不要模仿!

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存储桶,整个文件内容将被读取到内存中。

相反,下面的实现将消耗固定数量的内存来过滤任何旅。需要一个临时旅,每个响应只能分配一次,请参阅Maintaining state部分。

更好的输出滤波器

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

Maintaining state

需要在每个响应的多个调用上维持状态的过滤器可以使用其ap_filter_t结构的->ctx字段。通常,在这种结构中存储一个临时旅,以避免每次调用都必须分配一个新的旅,如Brigade structure部分所述。

维护过滤器状态的示例代码

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

Buffering buckets

如果过滤器决定在一次过滤器函数调用持续时间之外存储存储桶(例如,将存储桶存储在其->ctx状态结构中),则这些存储桶必须被“shelve”。这是必要的,因为某些存储桶类型会提供代表临时资源(例如堆栈内存)的存储桶,这些临时资源会在过滤链完成对旅的处理后立即超出范围。

要放置存储桶,可以调用apr_bucket_setaside函数。并非所有存储桶类型都可以保留,但是如果成功,存储桶将进行变形以确保其生存期至少与作为apr_bucket_setaside函数的参数给出的池一样长。

另外,也可以使用ap_save_brigade函数,该函数会将所有存储桶移动到一个单独的旅中,该旅中的存储桶的寿命与给定的 pool 参数一样长。在考虑以下几点时,必须谨慎使用此功能:

  • 返回时,ap_save_brigade保证返回的旅中的所有存储桶都将代表 Map 到内存中的数据。如果给定一个 Importing 旅包含例如PIPE桶,则ap_save_brigade将消耗任意数量的内存来存储管道的整个输出。

  • ap_save_brigade从无法保留的存储桶中读取数据时,它将始终执行阻塞读取,从而消除了使用非阻塞存储桶读取的机会。

  • 如果使用ap_save_brigade而不传递非 NULL 的“ saveto”(目标)旅参数,则该函数将创建一个新的旅,这可能导致内存使用与内容大小成正比,如Brigade structure部分中所述。

Warning

过滤器必须确保在给定响应(包含 EOS 桶的旅)的最后一次调用期间,处理所有缓冲的数据并将其向下传递到过滤器链中。否则,此类数据将丢失。

非阻塞存储桶读取

apr_bucket_read函数采用apr_read_type_e参数,该参数确定将从数据源读取阻塞非阻塞。一个好的过滤器将首先尝试使用非阻塞读取从每个数据桶中读取数据;如果以APR_EAGAIN失败,则在过滤器链下发送FLUSH存储桶,然后使用阻塞读取重试。

这种操作模式可确保如果使用慢速内容源,则位于过滤器链下游的所有过滤器都将刷新所有缓冲的存储桶。

CGI 脚本是一个慢速内容源的示例,它被实现为存储桶类型。 mod_cgi将发送PIPE个存储桶,它们代表 CGI 脚本的输出;在 await 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来“摧毁”旅。

  • 输出过滤器必须* setaside *保留超出过滤器功能持续时间的所有存储区。

  • 输出过滤器不得忽略ap_pass_brigade的返回值,并且必须返回适当的错误以备份过滤器链。

  • 输出过滤器必须为每个响应仅创建固定数量的存储桶旅,而不是为每个调用创建一个固定数量的存储桶旅。

  • 输出过滤器应首先尝试从每个数据桶中进行非阻塞读取,如果读取阻塞,则沿过滤器链向下发送FLUSH桶,然后再尝试阻塞读取。

用例:在 mod_ratelimit 中缓冲

r1833875更改是一个很好的示例,显示了在输出过滤器的上下文中缓冲和保持状态的含义。在这种使用情况下,用户在用户的邮件列表上询问了一个有趣的问题,即为什么mod_ratelimit似乎不接受代理内容(要么以不同的速度进行速率限制,要么根本不这样做)。在深入研究解决方案之前,最好从较高的角度解释mod_ratelimit的工作原理。诀窍很简单:进行速率限制设置并计算数据块大小,每 200ms 刷新一次到 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 字节的桶式大队,则意味着过滤器将尝试执行以下操作:

  • 将 38400 字节分成最多 12228 字节的块。

  • 刷新前 12228 个字节的块并休眠 200ms。

  • 刷新第二个 12228 字节块并休眠 200ms。

  • 刷新第三个 12228 字节块并休眠 200ms。

  • 刷新剩余的 1716 字节。

如果输出过滤器对每个响应仅处理一个旅,则上述伪代码可以很好地工作,但是可能会发生这样的情况,即需要以不同的旅规模来多次调用它。例如,前一个用例是当 httpd 直接提供某些内容(例如静态文件)时:桶队抽象负责处理整个内容,并且速率限制效果很好。但是,如果通过 mod_proxy_http 提供相同的静态内容(例如,后端在提供它而不是 httpd 服务),则内容生成器(在本例中为 mod_proxy_http)可以使用最大缓冲区大小,然后将数据作为存储桶旅发送给输出过滤器链通常会触发对mod_ratelimit的多次呼叫。如果阅读器假设多次调用输出过滤器来尝试执行伪代码,每个调用都需要处理 38400 字节的存储桶旅,那么很容易发现一些异常:

  • 在一个旅的最后一次冲洗与第二次冲洗之间,没有睡眠。

  • 即使在最后一次刷新后强制睡眠,该块大小也不是理想大小(1716 字节而不是 12228 字节),并且最终 Client 端的速度将很快不同于 httpd 的配置中设置的速度。

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

  • 使用 ctx 内部数据结构(由每个响应处理周期的mod_ratelimit初始化)来“记住”跨多个调用执行的最后一次睡眠的时间,并采取相应的措施。

  • 如果不能将一个桶大队划分为有限数量的 chunk_size 块,请将剩余的字节(位于桶大队的尾部)存储在临时存放区(即另一个桶大队)中,然后使用ap_save_brigade将它们放在一边。这些字节将被预先添加到下一个存储桶旅,该存储桶旅将在后续调用中处理。

  • 如果当前正在处理的存储桶旅包含流存储桶(EOS)的末尾,请避免使用先前的逻辑。如果到达流的末尾,则无需休眠或缓冲数据。

在本节开始处链接的提交还包含一些代码重构,因此在第一遍过程中阅读它并不容易,但是总体思路基本上是到目前为止所写的内容。本部分的目的不是使尝试阅读 C 代码的 Reader 感到头疼,而是使他/她有一种有效使用 httpd 筛选器链工具集提供的工具所需的正确思维方式。