Skip to content

Commit e127cc7

Browse files
authored
feat: Request-ID plugin add snowflake algorithm (#4559)
1 parent a7c040f commit e127cc7

File tree

6 files changed

+585
-12
lines changed

6 files changed

+585
-12
lines changed

apisix/plugins/request-id.lua

Lines changed: 196 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,58 @@
1414
-- See the License for the specific language governing permissions and
1515
-- limitations under the License.
1616
--
17-
local core = require("apisix.core")
18-
local plugin_name = "request-id"
19-
local ngx = ngx
20-
local uuid = require("resty.jit-uuid")
17+
18+
local ngx = ngx
19+
local bit = require("bit")
20+
local core = require("apisix.core")
21+
local snowflake = require("snowflake")
22+
local uuid = require("resty.jit-uuid")
23+
local process = require("ngx.process")
24+
local timers = require("apisix.timers")
25+
local tostring = tostring
26+
local math_pow = math.pow
27+
local math_ceil = math.ceil
28+
local math_floor = math.floor
29+
30+
local plugin_name = "request-id"
31+
32+
local data_machine = nil
33+
local snowflake_inited = nil
34+
35+
local attr = nil
2136

2237
local schema = {
2338
type = "object",
2439
properties = {
2540
header_name = {type = "string", default = "X-Request-Id"},
26-
include_in_response = {type = "boolean", default = true}
41+
include_in_response = {type = "boolean", default = true},
42+
algorithm = {type = "string", enum = {"uuid", "snowflake"}, default = "uuid"}
2743
}
2844
}
2945

46+
local attr_schema = {
47+
type = "object",
48+
properties = {
49+
snowflake = {
50+
type = "object",
51+
properties = {
52+
enable = {type = "boolean", default = false},
53+
snowflake_epoc = {type = "integer", minimum = 1, default = 1609459200000},
54+
data_machine_bits = {type = "integer", minimum = 1, maximum = 31, default = 12},
55+
sequence_bits = {type = "integer", minimum = 1, default = 10},
56+
delta_offset = {type = "integer", default = 1, enum = {1, 10, 100, 1000}},
57+
data_machine_ttl = {type = "integer", minimum = 1, default = 30},
58+
data_machine_interval = {type = "integer", minimum = 1, default = 10}
59+
}
60+
}
61+
}
62+
}
3063

3164
local _M = {
3265
version = 0.1,
3366
priority = 11010,
3467
name = plugin_name,
35-
schema = schema,
68+
schema = schema
3669
}
3770

3871

@@ -41,9 +74,144 @@ function _M.check_schema(conf)
4174
end
4275

4376

77+
-- Generates the current process data machine
78+
local function gen_data_machine(max_number)
79+
if data_machine == nil then
80+
local etcd_cli, prefix = core.etcd.new()
81+
local prefix = prefix .. "/plugins/request-id/snowflake/"
82+
local uuid = uuid.generate_v4()
83+
local id = 1
84+
::continue::
85+
while (id <= max_number) do
86+
local res, err = etcd_cli:grant(attr.snowflake.data_machine_ttl)
87+
if err then
88+
id = id + 1
89+
core.log.error("Etcd grant failure, err: ".. err)
90+
goto continue
91+
end
92+
93+
local _, err1 = etcd_cli:setnx(prefix .. tostring(id), uuid)
94+
local res2, err2 = etcd_cli:get(prefix .. tostring(id))
95+
96+
if err1 or err2 or res2.body.kvs[1].value ~= uuid then
97+
core.log.notice("data_machine " .. id .. " is not available")
98+
id = id + 1
99+
else
100+
data_machine = id
101+
102+
local _, err3 =
103+
etcd_cli:set(
104+
prefix .. tostring(id),
105+
uuid,
106+
{
107+
prev_kv = true,
108+
lease = res.body.ID
109+
}
110+
)
111+
112+
if err3 then
113+
id = id + 1
114+
etcd_cli:delete(prefix .. tostring(id))
115+
core.log.error("set data_machine " .. id .. " lease error: " .. err3)
116+
goto continue
117+
end
118+
119+
local lease_id = res.body.ID
120+
local start_at = ngx.time()
121+
local handler = function()
122+
local now = ngx.time()
123+
if now - start_at < attr.snowflake.data_machine_interval then
124+
return
125+
end
126+
127+
local _, err4 = etcd_cli:keepalive(lease_id)
128+
if err4 then
129+
snowflake_inited = nil
130+
data_machine = nil
131+
core.log.error("snowflake data_machine: " .. id .." lease faild.")
132+
end
133+
start_at = now
134+
core.log.info("snowflake data_machine: " .. id .." lease success.")
135+
end
136+
137+
timers.register_timer("plugin#request-id", handler)
138+
core.log.info(
139+
"timer created to lease snowflake algorithm data_machine, interval: ",
140+
attr.snowflake.data_machine_interval)
141+
core.log.notice("lease snowflake data_machine: " .. id)
142+
break
143+
end
144+
end
145+
146+
if data_machine == nil then
147+
core.log.error("No data_machine is not available")
148+
return nil
149+
end
150+
end
151+
return data_machine
152+
end
153+
154+
155+
-- Split 'Data Machine' into 'Worker ID' and 'datacenter ID'
156+
local function split_data_machine(data_machine, node_id_bits, datacenter_id_bits)
157+
local num = bit.tobit(data_machine)
158+
local worker_id = bit.band(num, math_pow(2, node_id_bits) - 1)
159+
num = bit.rshift(num, node_id_bits)
160+
local datacenter_id = bit.band(num, math_pow(2, datacenter_id_bits) - 1)
161+
return worker_id, datacenter_id
162+
end
163+
164+
165+
-- Initialize the snowflake algorithm
166+
local function snowflake_init()
167+
if snowflake_inited == nil then
168+
local max_number = math_pow(2, (attr.snowflake.data_machine_bits))
169+
local datacenter_id_bits = math_floor(attr.snowflake.data_machine_bits / 2)
170+
local node_id_bits = math_ceil(attr.snowflake.data_machine_bits / 2)
171+
data_machine = gen_data_machine(max_number)
172+
if data_machine == nil then
173+
return ""
174+
end
175+
176+
local worker_id, datacenter_id = split_data_machine(data_machine,
177+
node_id_bits, datacenter_id_bits)
178+
179+
core.log.info("snowflake init datacenter_id: " ..
180+
datacenter_id .. " worker_id: " .. worker_id)
181+
snowflake.init(
182+
datacenter_id,
183+
worker_id,
184+
attr.snowflake.snowflake_epoc,
185+
node_id_bits,
186+
datacenter_id_bits,
187+
attr.snowflake.sequence_bits,
188+
attr.delta_offset
189+
)
190+
snowflake_inited = true
191+
end
192+
end
193+
194+
195+
-- generate snowflake id
196+
local function next_id()
197+
if snowflake_inited == nil then
198+
snowflake_init()
199+
end
200+
return snowflake:next_id()
201+
end
202+
203+
204+
local function get_request_id(algorithm)
205+
if algorithm == "uuid" then
206+
return uuid()
207+
end
208+
return next_id()
209+
end
210+
211+
44212
function _M.rewrite(conf, ctx)
45213
local headers = ngx.req.get_headers()
46-
local uuid_val = uuid()
214+
local uuid_val = get_request_id(conf.algorithm)
47215
if not headers[conf.header_name] then
48216
core.request.set_header(ctx, conf.header_name, uuid_val)
49217
end
@@ -53,7 +221,6 @@ function _M.rewrite(conf, ctx)
53221
end
54222
end
55223

56-
57224
function _M.header_filter(conf, ctx)
58225
if not conf.include_in_response then
59226
return
@@ -65,4 +232,25 @@ function _M.header_filter(conf, ctx)
65232
end
66233
end
67234

235+
function _M.init()
236+
local local_conf = core.config.local_conf()
237+
attr = core.table.try_read_attr(local_conf, "plugin_attr", plugin_name)
238+
local ok, err = core.schema.check(attr_schema, attr)
239+
if not ok then
240+
core.log.error("failed to check the plugin_attr[", plugin_name, "]", ": ", err)
241+
return
242+
end
243+
if attr.snowflake.enable then
244+
if process.type() == "worker" then
245+
ngx.timer.at(0, snowflake_init)
246+
end
247+
end
248+
end
249+
250+
function _M.destroy()
251+
if snowflake_inited then
252+
timers.unregister_timer("plugin#request-id")
253+
end
254+
end
255+
68256
return _M

conf/config-default.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,12 @@ plugin_attr:
349349
report_ttl: 3600 # live time for server info in etcd (unit: second)
350350
dubbo-proxy:
351351
upstream_multiplex_count: 32
352+
request-id:
353+
snowflake:
354+
enable: false
355+
snowflake_epoc: 1609459200000 # the starting timestamp is expressed in milliseconds
356+
data_machine_bits: 12 # data machine bit, maximum 31, because Lua cannot do bit operations greater than 31
357+
sequence_bits: 10 # each machine generates a maximum of (1 << sequence_bits) serial numbers per millisecond
358+
data_machine_ttl: 30 # live time for data_machine in etcd (unit: second)
359+
data_machine_interval: 10 # lease renewal interval in etcd (unit: second)
360+

docs/en/latest/plugins/request-id.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ API request. The plugin will not add a request id if the `header_name` is alread
3939
| Name | Type | Requirement | Default | Valid | Description |
4040
| ------------------- | ------- | ----------- | -------------- | ----- | -------------------------------------------------------------- |
4141
| header_name | string | optional | "X-Request-Id" | | Request ID header name |
42-
| include_in_response | boolean | optional | true | | Option to include the unique request ID in the response header |
42+
| include_in_response | boolean | optional | true | | Option to include the unique request ID in the response header |
43+
| algorithm | string | optional | "uuid" | ["uuid", "snowflake"] | ID generation algorithm |
4344

4445
## How To Enable
4546

@@ -72,6 +73,60 @@ X-Request-Id: fe32076a-d0a5-49a6-a361-6c244c1df956
7273
......
7374
```
7475

76+
### Use the snowflake algorithm to generate an ID
77+
78+
> supports using the Snowflake algorithm to generate ID.
79+
> 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.
80+
81+
The Snowflake algorithm is not enabled by default and needs to be configured in 'conf/config.yaml'.
82+
83+
```yaml
84+
plugin_attr:
85+
request-id:
86+
snowflake:
87+
enable: true
88+
snowflake_epoc: 1609459200000
89+
data_machine_bits: 12
90+
sequence_bits: 10
91+
data_machine_ttl: 30
92+
data_machine_interval: 10
93+
```
94+
95+
#### Configuration parameters
96+
97+
| Name | Type | Requirement | Default | Valid | Description |
98+
| ------------------- | ------- | ------------- | -------------- | ------- | ------------------------------ |
99+
| enable | boolean | optional | false | | When set it to true, enable the snowflake algorithm. |
100+
| snowflake_epoc | integer | optional | 1609459200000 | | Start timestamp (in milliseconds) |
101+
| data_machine_bits | integer | optional | 12 | | Maximum number of supported machines (processes) `1 << data_machine_bits` |
102+
| sequence_bits | integer | optional | 10 | | Maximum number of generated ID per millisecond per node `1 << sequence_bits` |
103+
| data_machine_ttl | integer | optional | 30 | | Valid time of registration of 'data_machine' in 'etcd' (unit: seconds) |
104+
| data_machine_interval | integer | optional | 10 | | Time between 'data_machine' renewal in 'etcd' (unit: seconds) |
105+
106+
- `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
107+
- `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`.
108+
- `sequence_bits` defaults to `10 bits` and each process generates up to `1024` ID per second
109+
110+
#### example
111+
112+
> Snowflake supports flexible configuration to meet a wide variety of needs
113+
114+
- Snowflake original configuration
115+
116+
> - Start time 2014-10-20 T15:00:00.000z, accurate to milliseconds. It can last about 69 years
117+
> - supports up to `1024` processes
118+
> - Up to `4096` ID per second per process
119+
120+
```yaml
121+
plugin_attr:
122+
request-id:
123+
snowflake:
124+
enable: true
125+
snowflake_epoc: 1413817200000
126+
data_machine_bits: 10
127+
sequence_bits: 12
128+
```
129+
75130
## Disable Plugin
76131

77132
Remove the corresponding json configuration in the plugin configuration to disable the `request-id`.

0 commit comments

Comments
 (0)