Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Request-ID plugin add snowflake algorithm #4559

Merged
merged 38 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2d74653
feat Request-ID plugin adds Snowflake algorithm
dickens7 Jul 7, 2021
ce66bfb
fixed no newline at end of file
dickens7 Jul 7, 2021
c3529cc
feat Request-ID plugin adds Snowflake algorithm
dickens7 Jul 7, 2021
1ca0f46
sytle line is to long
dickens7 Jul 7, 2021
c239679
fixed default config
dickens7 Jul 7, 2021
da2370e
fixed conflict
dickens7 Jul 7, 2021
f2f9cc0
fixed lint
dickens7 Jul 7, 2021
1b8565e
fixed ci
dickens7 Jul 8, 2021
bdf46f4
fixed test case
dickens7 Jul 8, 2021
d095301
fixed no newline at end of file
dickens7 Jul 8, 2021
1245d9f
optimied: Split function
dickens7 Jul 10, 2021
89cdec0
optimized: use apisix.timers
dickens7 Jul 10, 2021
428cb08
remove: api
dickens7 Jul 10, 2021
4f1717f
feat: support timestamp delta offset configuration
dickens7 Jul 11, 2021
7c6bfa7
docs: add snowflake algorithm instructions
dickens7 Jul 11, 2021
e0284a2
rename worker_number to data_machine
dickens7 Jul 13, 2021
fe8568b
fixed workerid and datacenter_id computation error
dickens7 Jul 13, 2021
90e3544
etcd grant add exception validation
dickens7 Jul 13, 2021
cb07351
fix: goto may causes the id > max_number
dickens7 Jul 13, 2021
61ed66e
chore: fix name comment
dickens7 Jul 13, 2021
b97bb94
feat: limit data_machine_bits maximun
dickens7 Jul 13, 2021
3c2d4e7
chore: use the old version lua-snowflake first
dickens7 Jul 13, 2021
cd39e27
docs: optimzie 'data_machine_bits' describe
dickens7 Jul 13, 2021
77dd441
docs: fix end of line
dickens7 Jul 13, 2021
5941f62
docs: fix markdownlint
dickens7 Jul 13, 2021
49aae46
fix: plugin destroy unregister timer
dickens7 Jul 20, 2021
ea7c7c6
chore: remove delta offset
dickens7 Jul 20, 2021
82e6250
docs: added English doc
dickens7 Jul 20, 2021
c487aeb
fix: markdownlint
dickens7 Jul 20, 2021
9e5e70a
fixcheck if the timer is registered
dickens7 Jul 22, 2021
756ba19
fix: etcd_cli:grant error continue
dickens7 Jul 22, 2021
9611604
Merge branch 'master' into feat-add-snowflake
dickens7 Jul 26, 2021
8e7d777
fix: infinite loop
dickens7 Jul 27, 2021
99bc9f3
docs: fix describe the error
dickens7 Jul 27, 2021
b777bc4
Merge branch 'feat-add-snowflake' of https://github.com/dickens7/apis…
dickens7 Jul 27, 2021
9894d69
docs: fixed attributes
dickens7 Jul 27, 2021
72a7b0b
fix: have default values, set optional
dickens7 Jul 28, 2021
fd3dc4f
Merge branch 'master' into feat-add-snowflake
dickens7 Aug 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 194 additions & 8 deletions apisix/plugins/request-id.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
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}},
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
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
}


Expand All @@ -41,9 +74,142 @@ 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
core.log.error("Etcd grant failure, err: ".. err)
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
end

local _, err1 = etcd_cli:setnx(prefix .. tostring(id), uuid)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have to write ETCD so that we can write the uuid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of writing etcd here is to generate DataMachineId for snowflake. The value is uuid because etcd_cli:setnx still returns succeeded even if the key exists. UUID is used for verification after etcd_cli:setnx.

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
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
}
)

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)
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
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
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we put them under utils? Snowflake ID generating should also be used elsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's implement this in the next pr


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
Expand All @@ -53,7 +219,6 @@ function _M.rewrite(conf, ctx)
end
end


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this blank?

function _M.header_filter(conf, ctx)
if not conf.include_in_response then
return
Expand All @@ -65,4 +230,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
9 changes: 9 additions & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should check data_machine_bits + sequence_bits = 22 always? may the test case need to cover it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data_machine_bits and sequence_bits are not fixed and can be configured according to different requirements

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)

54 changes: 54 additions & 0 deletions docs/en/latest/plugins/request-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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 cannot arbitrarily adjust the configuration information. Failure to do so may result in duplicate ID being generated.
dickens7 marked this conversation as resolved.
Show resolved Hide resolved

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 | required | false | | When set it to true, enable the snowflake algorithm. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| enable | boolean | required | false | | When set it to true, enable the snowflake algorithm. |
| enable | boolean | optional | false | | When set it to true, enable the snowflake algorithm. |

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified jsonSchema required = {"enable", "snowflake_epoc "}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you provide a default value, the field is actually optional, as users don't need to specify their values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I'll make it optional

| snowflake_epoc | integer | required | 1609459200000 | | Start timestamp (in milliseconds) |
| data_machine_bits | integer | deprecated | 12 | | Maximum number of supported machines (processes) `1 << data_machine_bits` |
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
| sequence_bits | integer | deprecated | 10 | | Maximum number of generated ID per millisecond per node `1 << sequence_bits` |
| data_machine_ttl | integer | deprecated | 30 | | Valid time of registration of 'data_machine' in 'etcd' (unit: seconds) |
| data_machine_interval | integer | deprecated | 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per second here and the per millisecond above confuse me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the definition of two parts of the snowflake algorithm. Millisecond is the definition of the timestamp part, which means that the timestamp part is in milliseconds. Second is the definition of the sequence number part, which means that the number of id can be generated 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`.
Expand Down
61 changes: 58 additions & 3 deletions docs/zh/latest/plugins/request-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ title: request-id

| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ------------------- | ------- | -------- | -------------- | ------ | ------------------------------ |
| header_name | string | 可选 | "X-Request-Id" | | Request ID header name |
| include_in_response | boolean | 可选 | false | | 是否需要在返回头中包含该唯一ID |
| header_name | string | 可选 | "X-Request-Id" | | Request ID header name |
| include_in_response | boolean | 可选 | false | | 是否需要在返回头中包含该唯一ID |
| algorithm | string | 可选 | "uuid" | ["uuid", "snowflake"] | ID 生成算法 |

## 如何启用

Expand Down Expand Up @@ -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 和 DatacenterIDd的集合,插件会为每一个进程分配一个唯一ID,最大支持进程数为 `pow(2, data_machine_bits)`。默认占 `12 bits` 最多支持 `4096` 个进程。
dickens7 marked this conversation as resolved.
Show resolved Hide resolved
- 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 '
Expand Down
Loading