Apache 模块 mod_unique_id

Description:为每个请求提供一个具有唯一标识符的环境变量
Status:Extension
Module Identifier:unique_id_module
Source File:mod_unique_id.c

Summary

该模块为每个请求提供一个魔术令牌,该令牌在非常特定的条件下保证在“所有”请求中是唯一的。唯一标识符甚至在正确配置的机器集群中的多台机器之间也是唯一的。环境变量UNIQUE_ID设置为每个请求的标识符。出于各种原因,唯一标识符很有用,这些原因超出了本文档的范围。

Theory

首先简要回顾一下 Apache 服务器在 Unix 计算机上的工作方式。 Windows NT 当前不支持此功能。在 Unix 机器上,Apache 创建多个子进程,这些子进程一次请求一个子进程。每个孩子在其一生中可以满足多个请求。出于讨论目的,children 彼此之间不共享任何数据。我们将这些子代称为 httpd 进程。

您的网站上有一台或多台由您 Management 的计算机,我们将它们统称为计算机集群。每台机器都可以运行 Apache 的多个实例。所有这些统称为“宇宙”,在某些假设下,我们将证明在此宇宙中,我们可以为每个请求生成唯一的标识符,而无需在集群中的机器之间进行广泛的通信。

群集中的计算机应满足这些要求。 (即使只有一台计算机,也应将其时钟与 NTP 同步.)

  • 机器的时间通过 NTP 或其他网络时间协议进行同步。

  • 机器的主机名各不相同,因此模块可以在主机名上进行主机名查找,并为群集中的每台机器接收不同的 IP 地址。

就 os 假设而言,我们假设 pid(进程 ID)适合 32 位。如果 os 为 pid 使用的位数超过 32 位,则此修复很简单,但必须在代码中执行。

根据这些假设,我们可以在单个时间点上从所有其他 httpd 进程中识别集群中任何计算机上的任何 httpd 进程。机器的 IP 地址和 httpd 进程的 pid 足以完成此操作。如果您使用多线程 MPM,则 httpd 进程可以同时处理多个请求。为了识别线程,我们使用 Apache httpd 内部使用的线程索引。因此,为了生成请求的唯一标识符,我们只需要区分不同的时间点即可。

为了区分时间,我们将使用 Unix 时间戳(自 UTC 1970 年 1 月 1 日以来的秒数)和 16 位计数器。时间戳仅具有一秒的粒度,因此该计数器用于在一秒钟内表示最多 65536 个值。四倍*(ip_addr,pid,time_stamp,counter)*足以枚举每个 httpd 进程每秒 65536 个请求。但是随着时间的流逝,pid 重用会出现问题,并且使用计数器来缓解此问题。

创建 httpd 子级时,将使用(当前微秒除以 10)以 65536 为模数初始化计数器(选择此公式是为了消除某些系统上微秒计时器的低序位引起的一些方差问题)。生成唯一标识符时,使用的时间戳是请求到达 Web 服务器的时间。每当生成标识符(并允许翻转)时,计数器就会递增。

内核在派生该进程时会为每个进程生成一个 pid,并且允许 pid 进行翻转(在许多 Unix 上,它们为 16 位,但较新的系统已扩展为 32 位)。因此,随着时间的流逝,相同的 pid 将被重用。但是,除非它在同一秒内被重用,否则它不会破坏我们四 Tuples 的唯一性。也就是说,我们假设系统不会在一秒钟的间隔内产生 65536 个进程(在某些 Unix 上它甚至可能是 32768 个进程,但是这种情况不太可能发生)。

假设时间由于某种原因而重演。也就是说,假设系统的时钟搞砸了,并且它会重新查看过去的时间(或者它太远了,正确地重置了,然后重新考虑了将来的时间)。在这种情况下,我们可以轻松地表明我们可以获得 pid 和时间戳重用。选择计数器的初始化程序旨在帮助克服这一问题。请注意,我们确实希望使用随机数来初始化计数器,但是大多数系统上没有任何可用的数字(* ie *,您不能使用 rand(),因为您需要为生成器提供种子,并且不能请为该种子添加时间,因为时间(至少以一秒的分辨率)已经重复了一次。这不是一个完美的防御。

防御有多好?假设您的一台计算机每秒最多可处理 500 个请求(在撰写本文时,这是一个非常合理的上限,因为系统通常不仅仅清除静态文件而已)。为此,将需要多个子代,具体取决于您有多少个并发 Client 端。但是我们会感到悲观,并假设一个孩子每秒能够处理 500 个请求。有 1000 个可能的起始计数器值,以使 500 个请求的两个序列重叠。因此,如果时间(以一秒钟的分辨率)重复一次,则此孩子有 1.5%的机会重复该计数器的值,并且唯一性将被破坏。这是一个非常悲观的例子,而按照现实世界的价值,它发生的可能性甚至更低。如果您的系统仍然很可能发生,那么也许您应该将计数器设置为 32 位(通过编辑代码)。

您可能会担心在夏令时期间时钟会“倒退”。但这不是问题,因为这里使用的时间是 UTC,“总是”向前 Developing。请注意,基于 x86 的 Unix 可能需要适当的配置才能实现-应当将它们配置为假定主板时钟为 UTC 并进行适当补偿。但是即使如此,如果您正在运行 NTP,那么重新启动后不久,您的 UTC 时间也将正确。

通过使用字母[A-Za-z0-9@-]以类似于 MIME base64 编码的方式对四倍的 144 位(32 位 IP 地址,32 位 pid,32 位时间戳,16 位计数器,32 位线程索引)进行编码来构造UNIQUE_ID环境变量。 ,产生 24 个字符。 MIME base64 字母表实际上是[A-Za-z0-9+/],但是+/需要在 URL 中进行特殊编码,这使得它们不太受欢迎。所有值均以网络字节 Sequences 编码,因此在不同字节 Sequences 的体系结构中,该编码具有可比性。编码的实际 Sequences 是:时间戳记,IP 地址,pid,计数器。这种排序是有目的的,但是应该强调的是,应用程序不应剖析编码。应用程序应将整个编码的UNIQUE_ID视为不透明令牌,可以将其与其他UNIQUE_ID进行比较,以确保相等。

这样选择 Sequences,以便将来可以更改编码,而不必担心与现有的UNIQUE_ID s 数据库冲突。新的编码还应将时间戳记作为第一个元素,否则可以使用相同的字母和位长。由于时间戳从本质上来说是一个递增的序列,因此只需* flag 秒*就足够了,群集中的所有计算机都停止服务任何请求,并停止使用旧的编码格式。之后,他们可以恢复请求并开始发布新的编码。

我们认为,这是解决此问题的相对便携式的解决方案。生成的标识符本质上具有无限的使用寿命,因为可以根据需要使将来的标识符更长。基本上,集群中的机器之间不需要通信(仅需要 NTP 同步,这是较低的开销),并且 httpd 进程之间也不需要通信(通信隐含在内核分配的 pid 值中)。在非常特殊的情况下,可以缩短标识符的长度,但是需要假设更多的信息(例如,对于任何站点,32 位 IP 地址都是多余的,但是没有可移植的,更短的替换项)。