txid: 为 nginx-module-lua/nginx 生成可排序的唯一事务或请求 ID
安装
如果您尚未设置 RPM 仓库订阅,请 注册。然后您可以继续以下步骤。
CentOS/RHEL 7 或 Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-txid
CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-txid
要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua。
本文档描述了 lua-resty-txid v1.0.0,于 2018 年 4 月 1 日发布。
lua-resty-txid 提供了一个函数,可以用于为 OpenResty/nginx 生成唯一的事务/请求 ID。这些 ID 可用于关联日志或上游请求,并具有以下特征:
- 20 个字符
- base32hex 编码
- 时间上和字典上可排序
- 不区分大小写
- 96 位标识符
lua-resty-txid 是 OpenResty(或带有 ngx_lua 的 nginx)中 ngx_txid 的 LuaJIT 移植。lua-resty-txid 生成的 ID 遵循完全相同的模式,并与 ngx_txid 兼容。
用法
该模块暴露了一个单一的 txid() Lua 函数来生成 ID:
local txid = require "resty.txid"
local id = txid() -- b2g6q94qdn6h84an7vfg
每次调用 txid() 时,将返回一个新的唯一 ID,因此如果希望在单个请求的多个地方重用相同的 ID,您需要缓存结果。根据您的使用情况,ngx.ctx 或 set_by_lua 提供了一些简单的选项,以便在每个请求的基础上缓存该值。
txid() -- b2g83t2oshrg092mjggg
txid() -- b2g83t2oodncokuges00
ngx.ctx.txid = txid() -- b2g83t2od939mdvb2l0g
ngx.ctx.txid -- b2g83t2od939mdvb2l0g
最后,txid() 接受一个可选参数,用于在生成 ID 时使用哪个时间戳(以毫秒为单位)。默认情况下,使用当前时间戳。由于生成的 ID 在时间上和字典上都是可排序的,因此可以用来生成基于之前日期或时间的 ID。
local timestamp_ms = 655829050000 -- 1990-10-13 14:44:10
txid(timestamp_ms) -- 4om9qi54la8ffr4bd9sg
local timestamp_ms = 655929050000 -- 1990-10-14 12:30:50
txid(timestamp_ms) -- 4on1lg74nt0ud2ssllu0
示例
一个更完整的示例,包括缓存、设置请求/响应头以及与 nginx 日志的集成:
http {
log_format agent "$lua_txid $http_user_agent";
log_format addr "$lua_txid $remote_addr";
init_by_lua_block {
# 预加载模块。
require "resty.txid"
}
server {
listen 8080;
access_log logs/agents.log agent;
access_log logs/addrs.log addr;
# 设置一个 nginx 变量,该变量在每个请求中缓存并可用于 nginx 的 log_format。
set_by_lua_block $lua_txid {
local txid = require "resty.txid"
return txid()
}
location / {
# 在响应中设置一个头部提供 ID。
more_set_headers "X-Request-Id: $lua_txid";
# 在请求中设置一个头部提供 ID(将发送到代理的上游)。
more_set_input_headers "X-Request-Id: $lua_txid";
proxy_pass http://localhost:8081;
}
}
}
性能
基准测试表明性能与 ngx_txid C 扩展相当。
设计
事务 ID 的设计是 ngx_txid 的直接移植,因此以下是关于 ngx_txid 设计的所有原始信息:
背景
此事务 ID 的设计应满足以下要求:
- 在大约秒粒度下,数字上大致可时间排序。
- 具有大致在秒粒度上可字典排序的表示。
- 在每秒 100 万个事务的情况下,碰撞概率小于 1e-9。
- 高效且易于解码为固定大小的 C 类型。
- 始终可用,尽管碰撞概率可能更高。
- 尽可能使用较少的字节。
- 与 IPv4 和 IPv6 网络兼容。
技术
在高 42 位中使用单调毫秒分辨率时钟,在低 54 位中使用系统熵。使用足够的熵位以满足所需的全局请求速率下的碰撞概率。
+------------- 64 bits------------+--- 32 bits ----+
+------ 42 bits ------+--22 bits--|----------------+
| msec since 1970-1-1 | random | random |
+---------------------+-----------+----------------+
在所有服务器上每秒 100 万个请求意味着每毫秒 1000 个随机值。使用 生日悖论 估算碰撞概率可以使用以下公式:1 - e^(-((m^2)/(2*n))),其中 m 是 ID 的数量,n 是可能的随机值数量。
使用 54 位熵时:
1mil req/s = 1 - exp(-((1000^2) /(2*2^54))) = 2.775558e-11
10mil req/s = 1 - exp(-((10000^2)/(2*2^54))) = 2.775558e-09
即使在每秒 1000 万个请求的情况下,碰撞的几率也很小。
Nginx 通过配置指令 timer_resolution 以增量方式跟踪当前时钟。$txid 的时钟分辨率为 1 毫秒,因此大于 1 毫秒的定时器分辨率意味着碰撞概率将增加。如果您的 timer_resolution 为 10 毫秒,则每秒 100 万个请求在最坏情况下将需要每秒 10,000 个随机值。
编码
选择使用 base32hex 进行编码,采用小写字母且不带填充字符,原因如下:
- 字典排序顺序与数字排序顺序相同
- 不区分大小写的相等性
- 小写更易于视觉比较
- 比十六进制编码密度高 4 字节
其他技术
- snowflake:使用时间(41) + 唯一 ID(10) + 序列(12)。
- 优点:保证唯一序列
- 优点:适合 63 位
- 缺点:每个服务器需要唯一 ID 协调 - 每个主机 16 个工作进程意味着 nginx 实例的限制为 64
- 缺点:唯一 ID 仅有 11 位可用,需要监控
- 缺点:仅在同一进程中可能实现总排序
-
缺点:当时钟失去同步时可能会发生服务中断
-
flake:使用时间 + MAC ID + 序列。
- 优点:保证唯一序列
- 缺点:使用 128 位
- 缺点:浪费 22 位时间戳数据
- 缺点:每个主机只能有一个进程生成 ID - 需要同步访问每个工作进程的序列
- 缺点:当时钟失去同步时可能会发生服务中断
-
缺点:种子跨平台 MAC 地址查找。
-
UUIDv4:122 位熵
- 优点:碰撞概率非常低
-
缺点:不可排序
-
带时间戳的 UUID:48 位时间 + 74 位熵
- 优点:碰撞概率非常低
-
缺点:字符串表示不是时间局部的
-
httpd mod_unique_id:主机 IP(32) + PID(32) + 时间(32) + 序列(16) + 线程 ID(32)
- 优点:确定性
- 缺点:使用 144 位
- 缺点:假设主机名接口的唯一 IPv4
- 缺点:不可排序的区分大小写自定义表示 - 使用自定义字母表的 base64
- 缺点:每个 PID 每秒最多 65535 个 ID 的硬限制 - 时钟步进的容忍度小
开发
在检出仓库后,可以使用 Docker 运行测试套件:
docker-compose run --rm app make test
发布流程
要将版本发布到 OPM 和 LuaRocks:
VERSION=x.x.x make release
GitHub
您可以在 nginx-module-txid 的 GitHub 仓库 中找到此模块的其他配置提示和文档。