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 FILE 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 桶,那么整个文件内容将被读入内存,因为每个 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);
}
需要在每个响应的多次调用中维护状态的过滤器可以使用其 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 = ...;
}
...
如果过滤器决定将桶存储在单个过滤器函数调用持续时间之外(例如将其存储在其 ->ctx 状态结构中),那么这些桶必须被隔离。这是必要的,因为某些桶类型提供表示临时资源(例如堆栈内存)的桶,这些桶将在过滤器链完成处理传送带后立即超出范围。
要隔离桶,可以调用 apr_bucket_setaside 函数。并非所有桶类型都可以被隔离,但如果成功,桶将变形以确保其生命周期至少与作为 apr_bucket_setaside 函数参数给出的池的生命周期一样长。
或者,可以使用 ap_save_brigade 函数,该函数会将所有桶移动到一个单独的传送带中,该传送带包含生命周期与给定池参数一样长的桶。必须谨慎使用此函数,并考虑以下几点
ap_save_brigade 保证返回的旅中所有桶都代表映射到内存中的数据。如果给定一个包含例如 PIPE 桶的输入旅,ap_save_brigade 将消耗任意数量的内存来存储管道的整个输出。ap_save_brigade 从无法设置的桶中读取时,它将始终执行阻塞读取,从而取消使用 非阻塞桶读取 的机会。ap_save_brigade 在不传递非 NULL 的 "saveto"(目标)旅参数的情况下使用,该函数将创建一个新的旅,这可能会导致内存使用量与内容大小成正比,如 旅结构 部分所述。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;
...
}
总之,以下是一组所有输出过滤器应遵循的规则
FLUSH 桶应通过向下传递过滤器链中的任何待处理或缓冲的桶来尊重。EOS 桶后面的任何桶。ap_pass_brigade 向下传递过滤器链中的旅之后,输出过滤器应调用 apr_brigade_cleanup 以确保旅在重复使用该旅结构之前为空;输出过滤器永远不应使用 apr_brigade_destroy 来“销毁”旅。ap_pass_brigade 的返回值,并且必须将适当的错误返回到过滤器链中。FLUSH 桶到过滤器链,然后使用阻塞读取重试。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 字节的桶旅,这意味着过滤器将尝试执行以下操作
如果输出过滤器仅处理每个响应的一个旅,则上述伪代码可以正常工作,但可能会发生需要多次调用它,并且每次调用都需要处理不同旅大小的情况。例如,前一种用例是当 httpd 直接提供一些内容时,例如静态文件:桶旅抽象负责处理整个内容,并且速率限制可以正常工作。但是,如果通过 mod_proxy_http 提供相同静态内容(例如,后端而不是 httpd 提供它),则内容生成器(在本例中为 mod_proxy_http)可能会使用最大缓冲区大小,然后定期将数据作为桶旅发送到输出过滤器链,当然会触发对 mod_ratelimit 的多次调用。如果读者尝试执行伪代码,假设对输出过滤器的多次调用,每次调用都需要处理 38400 字节的桶旅,那么很容易发现一些异常
在这种情况下,两件事可能会有所帮助
mod_ratelimit 为每个响应处理周期初始化的 ctx 内部数据结构,以“记住”上次休眠是在何时执行的,并在跨多个调用时相应地采取行动。ap_save_brigade 将它们设置到一边。这些字节将被预先附加到将在后续调用中处理的下一个桶旅。在部分开头链接的提交中还包含一些代码重构,因此在第一次传递期间阅读起来并不容易,但总体思路基本上与现在写的内容相同。本节的目标不是让读者在阅读 C 代码时头疼,而是让他们处于正确的心态,以便有效地使用 httpd 过滤器链工具集提供的工具。
可用语言: en