lua-resty-txid
生成可排序的唯一事务或请求 ID。
$ opm get GUI/lua-resty-txid
lua-resty-txid
[!CircleCI](https://circleci.com/gh/GUI/lua-resty-txid)
lua-resty-txid 提供了一个函数,可用于为 OpenResty/nginx 生成唯一的事务/请求 ID。这些 ID 可用于关联日志或上游请求,并具有以下特征
20 个字符
base32hex 编码
时间和词法可排序
不区分大小写
96 位标识符
lua-resty-txid 是 ngx_txid 的 LuaJIT 移植版,适用于 OpenResty(或带有 ngx_lua 的 nginx)。lua-resty-txid 生成的 ID 遵循完全相同的模式,并且与 ngx_txid 兼容。
安装
通过 OPM
opm get GUI/lua-resty-txid
或通过 LuaRocks
luarocks install lua-resty-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 {
# Pre-load the module.
require "resty.txid"
}
server {
listen 8080;
access_log logs/agents.log agent;
access_log logs/addrs.log addr;
# Set an nginx variable that is cached per request and can be used in the
# nginx log_format.
set_by_lua_block $lua_txid {
local txid = require "resty.txid"
return txid()
}
location / {
# Set a header on the response providing the ID.
more_set_headers "X-Request-Id: $lua_txid";
# Set a header on the request providing the ID (which will be sent to the
# proxied upstream).
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 毫秒,因此如果 timer_resolution
大于 1 毫秒,则碰撞的概率会增加。如果您的 timer_resolution
为 10 毫秒,则在最坏情况下,每秒 100 万次请求需要每秒 10,000 个随机值。
编码
选择 base32hex(使用小写字母,不带填充字符)的原因如下
词法排序顺序等效于数字排序顺序
不区分大小写的相等性
小写字母便于视觉比较
比十六进制编码紧凑 4 字节
其他技术
snowflake:使用 time(41) + unique id(10) + sequence(12)。
优点:保证唯一的序列
优点:适合 63 位
缺点:需要为每个服务器协调唯一的 ID - 每个主机 16 个工作进程意味着 nginx 实例限制为 64 个
缺点:唯一 ID 可用的位数仅为 11 位,需要监控
缺点:仅在同一进程中才能进行完全排序
缺点:时钟不同步时可能会出现服务中断
flake:使用 time + mac id + sequence。
优点:保证唯一的序列
缺点:使用 128 位
缺点:浪费 22 位的时间戳数据
缺点:每个主机只能有一个进程生成 ID - 需要同步对每个工作进程中序列的访问
缺点:时钟不同步时可能会出现服务中断
缺点:跨平台 MAC 地址查找的种子。
UUIDv4:122 位熵
优点:碰撞概率极低
缺点:不可排序
带时间戳的 UUID:48 位时间 + 74 位熵
优点:碰撞概率极低
缺点:字符串表示形式不是时间本地化的
httpd mod_unique_id:Host ip(32) + pid(32) + time(32) + sequence (16) + thread id (32)
优点:确定性
缺点:使用 144 位
缺点:假设主机名的接口具有唯一的 IPv4
缺点:不可排序,区分大小写的自定义表示形式 - 带有自定义字母表的 base64
缺点:每秒每个 pid 的 ID 限制为 65535 个 - 对时钟步长的容差很小
开发
检出仓库后,可以使用 Docker 运行测试套件
docker-compose run --rm app make test
发布流程
要将发行版发布到 OPM 和 LuaRocks
VERSION=x.x.x make release
致谢
原始设计的功劳归功于 ngx_txid。这只是一个纯 LuaJIT 移植版,便于在 OpenResty 中安装。
作者
Nick Muerdter
许可证
mit