Apache HTTP Server 版本 2.4
描述 | 为每个请求提供一个包含唯一标识符的环境变量 |
---|---|
状态 | 扩展 |
模块标识符 | unique_id_module |
源文件 | mod_unique_id.c |
该模块为每个请求提供一个神奇的标记,在非常特定的条件下,该标记在“所有”请求中都是唯一的。即使在经过适当配置的机器集群中,唯一标识符在多台机器之间也是唯一的。环境变量UNIQUE_ID
被设置为每个请求的标识符。唯一标识符由于各种原因而有用,这些原因超出了本文档的范围。
首先简要回顾一下 Apache 服务器在 Unix 机器上的工作原理。此功能目前在 Windows NT 上不受支持。在 Unix 机器上,Apache 创建了多个子进程,子进程一次处理一个请求。每个子进程在其生命周期内可以服务多个请求。为了讨论的目的,子进程之间不共享任何数据。我们将子进程称为httpd 进程。
您的网站有一台或多台机器在您的管理控制之下,我们将它们统称为机器集群。每台机器都可能运行多个 Apache 实例。所有这些都被认为是“宇宙”,在某些假设下,我们将证明在这个宇宙中,我们可以为每个请求生成唯一的标识符,而无需在集群中的机器之间进行大量通信。
集群中的机器应满足以下要求。(即使您只有一台机器,也应使用 NTP 与其时钟同步。)
就操作系统假设而言,我们假设 pid(进程 ID)适合 32 位。如果操作系统为 pid 使用超过 32 位,则修复很简单,但必须在代码中执行。
在这些假设下,在某个时间点,我们可以从集群中所有其他 httpd 进程中识别出任何机器上的任何 httpd 进程。机器的 IP 地址和 httpd 进程的 pid 足以做到这一点。如果您使用多线程 MPM,则 httpd 进程可以同时处理多个请求。为了识别线程,我们使用 Apache httpd 在内部使用的线程索引。因此,为了为请求生成唯一标识符,我们只需要区分不同的时间点。
为了区分时间,我们将使用 Unix 时间戳(自 1970 年 1 月 1 日 UTC 以来的秒数)和一个 16 位计数器。时间戳只有 1 秒的粒度,因此计数器用于在 1 秒内表示最多 65536 个值。四元组( ip_addr, pid, time_stamp, counter )足以枚举每个 httpd 进程每秒 65536 个请求。但是,随着时间的推移,pid 会重复使用,计数器用于缓解此问题。
当创建 httpd 子进程时,计数器将初始化为(当前微秒除以 10)模 65536(选择此公式是为了消除某些系统上微秒计时器的低位位的某些方差问题)。当生成唯一标识符时,使用的时间戳是请求到达 Web 服务器的时间。每次生成标识符时,计数器都会递增(并允许回滚)。
内核为每个进程生成一个 pid,因为它派生了进程,并且允许 pid 回滚(它们在许多 Unix 上是 16 位,但较新的系统已扩展到 32 位)。因此,随着时间的推移,相同的 pid 将被重复使用。但是,除非它在同一秒内被重复使用,否则它不会破坏我们四元组的唯一性。也就是说,我们假设系统不会在一秒钟内生成 65536 个进程(在某些 Unix 上甚至可能是 32768 个进程,但这不太可能发生)。
假设时间由于某种原因重复。也就是说,假设系统的时钟被搞砸了,它重新访问了过去的时间(或者它太超前了,被正确重置,然后重新访问了未来的时间)。在这种情况下,我们可以很容易地证明我们可以获得 pid 和时间戳重复使用。选择计数器的初始化器旨在帮助消除这种情况。请注意,我们真正想要一个随机数来初始化计数器,但在大多数系统上都没有现成的数字(即,您不能使用 rand(),因为您需要播种生成器,并且不能使用时间播种它,因为时间,至少在 1 秒的分辨率下,已经重复了)。这不是一个完美的防御。
它有多好的防御?假设您的机器之一每秒最多服务 500 个请求(在撰写本文时这是一个非常合理的上限,因为系统通常不仅仅是输出静态文件)。要做到这一点,它将需要一定数量的子进程,具体取决于您有多少并发客户端。但我们将悲观地假设单个子进程能够每秒服务 500 个请求。有 1000 种可能的起始计数器值,使得 500 个请求的两个序列重叠。因此,如果时间(以 1 秒的分辨率)重复,则此子进程有 1.5% 的机会重复计数器值,并且唯一性将被破坏。这是一个非常悲观的例子,在现实世界中,这种情况发生的可能性更小。如果您的系统是这样的,它仍然有可能发生,那么您可能应该将计数器设为 32 位(通过编辑代码)。
您可能担心时钟在夏季日光节约时间期间“被调回”。但是,这不是问题,因为这里使用的时间是 UTC,它“总是”向前走。请注意,基于 x86 的 Unix 可能需要适当的配置才能使此情况成立——它们应该配置为假设主板时钟处于 UTC 并相应地进行补偿。但即使这样,如果您运行 NTP,那么您的 UTC 时间将在重新启动后不久就变得正确。
UNIQUE_ID
环境变量是通过使用字母表[A-Za-z0-9@-]
对 144 位(32 位 IP 地址、32 位 pid、32 位时间戳、16 位计数器、32 位线程索引)四元组进行编码来构建的,其方式类似于 MIME base64 编码,生成 24 个字符。MIME base64 字母表实际上是[A-Za-z0-9+/]
,但是+
和/
需要在 URL 中进行特殊编码,这使得它们不太理想。所有值都以网络字节顺序进行编码,以便编码在不同字节顺序的体系结构之间具有可比性。编码的实际顺序是:时间戳、IP 地址、pid、计数器。此排序有其目的,但应强调应用程序不应解析编码。应用程序应将整个编码的UNIQUE_ID
视为不透明的标记,该标记只能与其他UNIQUE_ID
进行比较以检查是否相等。
选择此排序是为了将来可以更改编码,而无需担心与现有UNIQUE_ID
数据库发生冲突。新的编码也应将时间戳作为第一个元素,并且可以使用相同的字母表和位长度。由于时间戳本质上是一个递增序列,因此在所有集群中的机器停止服务任何请求并停止使用旧的编码格式的标志秒中,拥有一个标志秒就足够了。之后,它们可以恢复请求并开始发出新的编码。
我们相信这是一个针对此问题的相对可移植的解决方案。生成的标识符本质上具有无限的寿命,因为将来可以根据需要使标识符更长。本质上,集群中的机器之间不需要任何通信(只需要 NTP 同步,这开销很低),并且 httpd 进程之间不需要任何通信(通信隐含在内核分配的 pid 值中)。在非常特定的情况下,可以缩短标识符,但需要假设更多信息(例如,32 位 IP 地址对于任何站点来说都过大了,但没有可移植的更短的替代方案)。