feat: Request-ID plugin add snowflake algorithm (#4559)

This commit is contained in:
seven dickens 2021-08-09 10:20:52 +08:00 committed by GitHub
parent a7c040f117
commit e127cc7a43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 585 additions and 12 deletions

View File

@ -14,25 +14,58 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local plugin_name = "request-id"
local ngx = ngx
local uuid = require("resty.jit-uuid")
local ngx = ngx
local bit = require("bit")
local core = require("apisix.core")
local snowflake = require("snowflake")
local uuid = require("resty.jit-uuid")
local process = require("ngx.process")
local timers = require("apisix.timers")
local tostring = tostring
local math_pow = math.pow
local math_ceil = math.ceil
local math_floor = math.floor
local plugin_name = "request-id"
local data_machine = nil
local snowflake_inited = nil
local attr = nil
local schema = {
type = "object",
properties = {
header_name = {type = "string", default = "X-Request-Id"},
include_in_response = {type = "boolean", default = true}
include_in_response = {type = "boolean", default = true},
algorithm = {type = "string", enum = {"uuid", "snowflake"}, default = "uuid"}
}
}
local attr_schema = {
type = "object",
properties = {
snowflake = {
type = "object",
properties = {
enable = {type = "boolean", default = false},
snowflake_epoc = {type = "integer", minimum = 1, default = 1609459200000},
data_machine_bits = {type = "integer", minimum = 1, maximum = 31, default = 12},
sequence_bits = {type = "integer", minimum = 1, default = 10},
delta_offset = {type = "integer", default = 1, enum = {1, 10, 100, 1000}},
data_machine_ttl = {type = "integer", minimum = 1, default = 30},
data_machine_interval = {type = "integer", minimum = 1, default = 10}
}
}
}
}
local _M = {
version = 0.1,
priority = 11010,
name = plugin_name,
schema = schema,
schema = schema
}
@ -41,9 +74,144 @@ function _M.check_schema(conf)
end
-- Generates the current process data machine
local function gen_data_machine(max_number)
if data_machine == nil then
local etcd_cli, prefix = core.etcd.new()
local prefix = prefix .. "/plugins/request-id/snowflake/"
local uuid = uuid.generate_v4()
local id = 1
::continue::
while (id <= max_number) do
local res, err = etcd_cli:grant(attr.snowflake.data_machine_ttl)
if err then
id = id + 1
core.log.error("Etcd grant failure, err: ".. err)
goto continue
end
local _, err1 = etcd_cli:setnx(prefix .. tostring(id), uuid)
local res2, err2 = etcd_cli:get(prefix .. tostring(id))
if err1 or err2 or res2.body.kvs[1].value ~= uuid then
core.log.notice("data_machine " .. id .. " is not available")
id = id + 1
else
data_machine = id
local _, err3 =
etcd_cli:set(
prefix .. tostring(id),
uuid,
{
prev_kv = true,
lease = res.body.ID
}
)
if err3 then
id = id + 1
etcd_cli:delete(prefix .. tostring(id))
core.log.error("set data_machine " .. id .. " lease error: " .. err3)
goto continue
end
local lease_id = res.body.ID
local start_at = ngx.time()
local handler = function()
local now = ngx.time()
if now - start_at < attr.snowflake.data_machine_interval then
return
end
local _, err4 = etcd_cli:keepalive(lease_id)
if err4 then
snowflake_inited = nil
data_machine = nil
core.log.error("snowflake data_machine: " .. id .." lease faild.")
end
start_at = now
core.log.info("snowflake data_machine: " .. id .." lease success.")
end
timers.register_timer("plugin#request-id", handler)
core.log.info(
"timer created to lease snowflake algorithm data_machine, interval: ",
attr.snowflake.data_machine_interval)
core.log.notice("lease snowflake data_machine: " .. id)
break
end
end
if data_machine == nil then
core.log.error("No data_machine is not available")
return nil
end
end
return data_machine
end
-- Split 'Data Machine' into 'Worker ID' and 'datacenter ID'
local function split_data_machine(data_machine, node_id_bits, datacenter_id_bits)
local num = bit.tobit(data_machine)
local worker_id = bit.band(num, math_pow(2, node_id_bits) - 1)
num = bit.rshift(num, node_id_bits)
local datacenter_id = bit.band(num, math_pow(2, datacenter_id_bits) - 1)
return worker_id, datacenter_id
end
-- Initialize the snowflake algorithm
local function snowflake_init()
if snowflake_inited == nil then
local max_number = math_pow(2, (attr.snowflake.data_machine_bits))
local datacenter_id_bits = math_floor(attr.snowflake.data_machine_bits / 2)
local node_id_bits = math_ceil(attr.snowflake.data_machine_bits / 2)
data_machine = gen_data_machine(max_number)
if data_machine == nil then
return ""
end
local worker_id, datacenter_id = split_data_machine(data_machine,
node_id_bits, datacenter_id_bits)
core.log.info("snowflake init datacenter_id: " ..
datacenter_id .. " worker_id: " .. worker_id)
snowflake.init(
datacenter_id,
worker_id,
attr.snowflake.snowflake_epoc,
node_id_bits,
datacenter_id_bits,
attr.snowflake.sequence_bits,
attr.delta_offset
)
snowflake_inited = true
end
end
-- generate snowflake id
local function next_id()
if snowflake_inited == nil then
snowflake_init()
end
return snowflake:next_id()
end
local function get_request_id(algorithm)
if algorithm == "uuid" then
return uuid()
end
return next_id()
end
function _M.rewrite(conf, ctx)
local headers = ngx.req.get_headers()
local uuid_val = uuid()
local uuid_val = get_request_id(conf.algorithm)
if not headers[conf.header_name] then
core.request.set_header(ctx, conf.header_name, uuid_val)
end
@ -53,7 +221,6 @@ function _M.rewrite(conf, ctx)
end
end
function _M.header_filter(conf, ctx)
if not conf.include_in_response then
return
@ -65,4 +232,25 @@ function _M.header_filter(conf, ctx)
end
end
function _M.init()
local local_conf = core.config.local_conf()
attr = core.table.try_read_attr(local_conf, "plugin_attr", plugin_name)
local ok, err = core.schema.check(attr_schema, attr)
if not ok then
core.log.error("failed to check the plugin_attr[", plugin_name, "]", ": ", err)
return
end
if attr.snowflake.enable then
if process.type() == "worker" then
ngx.timer.at(0, snowflake_init)
end
end
end
function _M.destroy()
if snowflake_inited then
timers.unregister_timer("plugin#request-id")
end
end
return _M

View File

@ -349,3 +349,12 @@ plugin_attr:
report_ttl: 3600 # live time for server info in etcd (unit: second)
dubbo-proxy:
upstream_multiplex_count: 32
request-id:
snowflake:
enable: false
snowflake_epoc: 1609459200000 # the starting timestamp is expressed in milliseconds
data_machine_bits: 12 # data machine bit, maximum 31, because Lua cannot do bit operations greater than 31
sequence_bits: 10 # each machine generates a maximum of (1 << sequence_bits) serial numbers per millisecond
data_machine_ttl: 30 # live time for data_machine in etcd (unit: second)
data_machine_interval: 10 # lease renewal interval in etcd (unit: second)

View File

@ -39,7 +39,8 @@ API request. The plugin will not add a request id if the `header_name` is alread
| Name | Type | Requirement | Default | Valid | Description |
| ------------------- | ------- | ----------- | -------------- | ----- | -------------------------------------------------------------- |
| header_name | string | optional | "X-Request-Id" | | Request ID header name |
| include_in_response | boolean | optional | true | | Option to include the unique request ID in the response header |
| include_in_response | boolean | optional | true | | Option to include the unique request ID in the response header |
| algorithm | string | optional | "uuid" | ["uuid", "snowflake"] | ID generation algorithm |
## How To Enable
@ -72,6 +73,60 @@ X-Request-Id: fe32076a-d0a5-49a6-a361-6c244c1df956
......
```
### Use the snowflake algorithm to generate an ID
> supports using the Snowflake algorithm to generate ID.
> read the documentation first before deciding to use snowflake. Because once the configuration information is enabled, you can not arbitrarily adjust the configuration information. Failure to do so may result in duplicate ID being generated.
The Snowflake algorithm is not enabled by default and needs to be configured in 'conf/config.yaml'.
```yaml
plugin_attr:
request-id:
snowflake:
enable: true
snowflake_epoc: 1609459200000
data_machine_bits: 12
sequence_bits: 10
data_machine_ttl: 30
data_machine_interval: 10
```
#### Configuration parameters
| Name | Type | Requirement | Default | Valid | Description |
| ------------------- | ------- | ------------- | -------------- | ------- | ------------------------------ |
| enable | boolean | optional | false | | When set it to true, enable the snowflake algorithm. |
| snowflake_epoc | integer | optional | 1609459200000 | | Start timestamp (in milliseconds) |
| data_machine_bits | integer | optional | 12 | | Maximum number of supported machines (processes) `1 << data_machine_bits` |
| sequence_bits | integer | optional | 10 | | Maximum number of generated ID per millisecond per node `1 << sequence_bits` |
| data_machine_ttl | integer | optional | 30 | | Valid time of registration of 'data_machine' in 'etcd' (unit: seconds) |
| data_machine_interval | integer | optional | 10 | | Time between 'data_machine' renewal in 'etcd' (unit: seconds) |
- `snowflake_epoc` default start time is `2021-01-01T00:00:00Z`, and it can support `69 year` approximately to `2090-09-0715:47:35Z` according to the default configuration
- `data_machine_bits` corresponds to the set of workIDs and datacEnteridd in the snowflake definition. The plug-in aslocates a unique ID to each process. Maximum number of supported processes is `pow(2, data_machine_bits)`. The default number of `12 bits` is up to `4096`.
- `sequence_bits` defaults to `10 bits` and each process generates up to `1024` ID per second
#### example
> Snowflake supports flexible configuration to meet a wide variety of needs
- Snowflake original configuration
> - Start time 2014-10-20 T15:00:00.000z, accurate to milliseconds. It can last about 69 years
> - supports up to `1024` processes
> - Up to `4096` ID per second per process
```yaml
plugin_attr:
request-id:
snowflake:
enable: true
snowflake_epoc: 1413817200000
data_machine_bits: 10
sequence_bits: 12
```
## Disable Plugin
Remove the corresponding json configuration in the plugin configuration to disable the `request-id`.

View File

@ -37,8 +37,9 @@ title: request-id
| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ------------------- | ------- | -------- | -------------- | ------ | ------------------------------ |
| header_name | string | 可选 | "X-Request-Id" | | Request ID header name |
| include_in_response | boolean | 可选 | true | | 是否需要在返回头中包含该唯一ID |
| header_name | string | 可选 | "X-Request-Id" | | Request ID header name |
| include_in_response | boolean | 可选 | false | | 是否需要在返回头中包含该唯一ID |
| algorithm | string | 可选 | "uuid" | ["uuid", "snowflake"] | ID 生成算法 |
## 如何启用
@ -71,9 +72,63 @@ X-Request-Id: fe32076a-d0a5-49a6-a361-6c244c1df956
......
```
### 使用 snowflake 算法生成ID
> 支持使用 snowflake 算法来生成ID。
> 在决定使用snowflake时请优先阅读一下文档。因为一旦启用配置信息则不可随意调整配置信息。否则可能会导致生成重复ID。
snowflake 算法默认是不启用的,需要在 `conf/config.yaml` 中开启配置。
```yaml
plugin_attr:
request-id:
snowflake:
enable: true
snowflake_epoc: 1609459200000
data_machine_bits: 12
sequence_bits: 10
data_machine_ttl: 30
data_machine_interval: 10
```
#### 配置参数
| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ------------------- | ------- | -------- | -------------- | ------ | ------------------------------ |
| enable | boolean | 可选 | false | | 当设置为true时 启用snowflake算法。 |
| snowflake_epoc | integer | 可选 | 1609459200000 | | 起始时间戳(单位: 毫秒) |
| data_machine_bits | integer | 可选 | 12 | | 最多支持机器(进程)数量 `1 << data_machine_bits` |
| sequence_bits | integer | 可选 | 10 | | 每个节点每毫秒内最多产生ID数量 `1 << sequence_bits` |
| data_machine_ttl | integer | 可选 | 30 | | `etcd``data_machine` 注册有效时间(单位: 秒)|
| data_machine_interval | integer | 可选 | 10 | | `etcd``data_machine` 续约间隔时间(单位: 秒)|
- snowflake_epoc 默认起始时间为 `2021-01-01T00:00:00Z`, 按默认配置可以支持 `69年` 大约可以使用到 `2090-09-07 15:47:35Z`
- data_machine_bits 对应的是 snowflake 定义中的 WorkerID 和 DatacenterID 的集合插件会为每一个进程分配一个唯一ID最大支持进程数为 `pow(2, data_machine_bits)`。默认占 `12 bits` 最多支持 `4096` 个进程。
- sequence_bits 默认占 `10 bits`, 每个进程每秒最多生成 `1024` 个ID
#### 配置示例
> snowflake 支持灵活配置来满足各式各样的需求
- snowflake 原版配置
> - 起始时间 2014-10-20T15:00:00.000Z 精确到毫秒为单位。大约可以使用 `69年`
> - 最多支持 `1024` 个进程
> - 每个进程每秒最多产生 `4096` 个ID
```yaml
plugin_attr:
request-id:
snowflake:
enable: true
snowflake_epoc: 1413817200000
data_machine_bits: 10
sequence_bits: 12
```
## 禁用插件
在路由 `plugins` 配置块中删除 `request-id 配置,即可禁用该插件,无需重启 APISIX。
在路由 `plugins` 配置块中删除 `request-id 配置,reload 即可禁用该插件,无需重启 APISIX。
```shell
curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '

View File

@ -68,6 +68,7 @@ dependencies = {
"penlight = 1.9.2-1",
"ext-plugin-proto = 0.2.1",
"casbin = 1.26.0",
"api7-snowflake = 2.0-1",
}
build = {

265
t/plugin/request-id.t vendored
View File

@ -470,3 +470,268 @@ GET /t
X-Request-Id and Custom-Header-Name are different
--- no_error_log
[error]
=== TEST 12: check for snowflake id
--- yaml_config
plugins:
- request-id
plugin_attr:
request-id:
snowflake:
enable: true
snowflake_epoc: 1609459200000
data_machine_bits: 10
sequence_bits: 10
data_machine_ttl: 30
data_machine_interval: 10
--- config
location /t {
content_by_lua_block {
ngx.sleep(3)
local core = require("apisix.core")
local key = "/plugins/request-id/snowflake/1"
local res, err = core.etcd.get(key)
if err ~= nil then
ngx.status = 500
ngx.say(err)
return
end
if res.body.node.key ~= "/apisix/plugins/request-id/snowflake/1" then
ngx.say(core.json.encode(res.body.node))
end
ngx.say("ok")
}
}
--- request
GET /t
--- response_body
ok
--- no_error_log
[error]
=== TEST 13: wrong type
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.request-id")
local ok, err = plugin.check_schema({algorithm = "bad_algorithm"})
if not ok then
ngx.say(err)
end
ngx.say("done")
}
}
--- request
GET /t
--- response_body
property "algorithm" validation failed: matches none of the enum values
done
--- no_error_log
[error]
=== TEST 14: add plugin with algorithm snowflake (default uuid)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"request-id": {
"algorithm": "snowflake"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1982": 1
},
"type": "roundrobin"
},
"uri": "/opentracing"
}]],
[[{
"node": {
"value": {
"plugins": {
"request-id": {
"algorithm": "snowflake"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1982": 1
},
"type": "roundrobin"
},
"uri": "/opentracing"
},
"key": "/apisix/routes/1"
},
"action": "set"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]
=== TEST 15: check for snowflake id
--- yaml_config
plugins:
- request-id
plugin_attr:
request-id:
snowflake:
enable: true
--- config
location /t {
content_by_lua_block {
local http = require "resty.http"
local t = {}
local ids = {}
for i = 1, 180 do
local th = assert(ngx.thread.spawn(function()
local httpc = http.new()
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/opentracing"
local res, err = httpc:request_uri(uri,
{
method = "GET",
headers = {
["Content-Type"] = "application/json",
}
}
)
if not res then
ngx.log(ngx.ERR, err)
return
end
local id = res.headers["X-Request-Id"]
if not id then
return -- ignore if the data is not synced yet.
end
if ids[id] == true then
ngx.say("ids not unique")
return
end
ids[id] = true
end, i))
table.insert(t, th)
end
for i, th in ipairs(t) do
ngx.thread.wait(th)
end
ngx.say("true")
}
}
--- request
GET /t
--- wait: 5
--- response_body
true
--- no_error_log
[error]
=== TEST 16: check for delta_offset 1000 milliseconds
--- yaml_config
plugins:
- request-id
plugin_attr:
request-id:
snowflake:
enable: true
snowflake_epoc: 1609459200000
data_machine_bits: 12
sequence_bits: 10
data_machine_ttl: 30
data_machine_interval: 10
delta_offset: 1000
--- config
location /t {
content_by_lua_block {
local http = require "resty.http"
local t = {}
local ids = {}
for i = 1, 180 do
local th = assert(ngx.thread.spawn(function()
local httpc = http.new()
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/opentracing"
local res, err = httpc:request_uri(uri,
{
method = "GET",
headers = {
["Content-Type"] = "application/json",
}
}
)
if not res then
ngx.log(ngx.ERR, err)
return
end
local id = res.headers["X-Request-Id"]
if not id then
return -- ignore if the data is not synced yet.
end
if ids[id] == true then
ngx.say("ids not unique")
return
end
ids[id] = true
end, i))
table.insert(t, th)
end
for i, th in ipairs(t) do
ngx.thread.wait(th)
end
ngx.say("true")
}
}
--- request
GET /t
--- wait: 5
--- response_body
true
--- no_error_log
[error]
=== TEST 17: wrong delta_offset
--- yaml_config
plugins:
- request-id
plugin_attr:
request-id:
snowflake:
enable: true
delta_offset: 1001
--- config
location /t {
content_by_lua_block {
ngx.say("done")
}
}
--- request
GET /t
--- response_body
done
--- error_log
ailed to check the plugin_attr[request-id]: property "snowflake" validation failed: property "delta_offset" validation failed: matches none of the enum values