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