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

依赖项

luajit

版本