一 基于源码安装

1. 依赖安装

Kong 依赖 OpenResty、 Nginx 补丁、 OpenResy 补丁、 OpenSSL、 PCRE、 LuaRocks、 LibYAML. 使用 kong-build-tools 工具可以方便安装依赖.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
git clone git@github.com:Kong/kong-build-tools.git
cd kong-build-tools

./openresty-build-tools/kong-ngx-build \
--prefix ${HOME}/work/kong-deps \
--work ${HOME}/work/kong-work \
--openresty 1.17.8.2 \
--openssl 1.1.1i \
--kong-nginx-module 0.0.8 \
--pcre 8.44

# 设置 PATH
export KONG_DEPS_PATH=${HOME}/work/kong-deps
export OPENSSL_DIR=${KONG_DEPS_PATH}/openssl
export PATH=${KONG_DEPS_PATH}/openresty/bin:$PATH
export PATH=${KONG_DEPS_PATH}/openresty/nginx/sbin:$PATH
export PATH=${KONG_DEPS_PATH}/openssl/bin:$PATH
export PATH=${KONG_DEPS_PATH}/luarocks/bin:$PATH

export PATH=${HOME}/work/luarocks/bin:$PATH

选项解释:

  • --prefix : 程序安装目录
  • --work : 依赖文件(openresty、openssl)下载及编译目录
  • --openresty : 指定 OpenResty 版本
  • --openssl : 指定 OpenSSL 版本
  • --kong-nginx-module : 指定 lua-kong-nginx-module 版本, 主要是对 nginx、lua-nginx-module 补丁
  • --pcre : 指定 pcre 版本, 如果未指定, 则不添加 pcre 模块
  • --force : 删除 work、prefix 中内容, 重新下载编译

测试安装是否成功:

1
2
/usr/local/kong/openresty/bin/openresty -v
openresty -v

安装 LibYAML 库:

1
2
3
4
5
6
7
8
# Ubuntu:
apt install libyaml-dev

# Fedora:
dnf install libyaml-devel

# macOS:
brew install libyaml --build-from-source

‘kong-build-tools’ 的核心是 ‘/openresty-build-tools/kong-ngx-build’ 脚本, 会依次下载 OpenSSL, OpenResty, PCRE, LuaRocks, lua-kong-nginx-module 并对 OpenResty, Nginx 打补丁. 最终会安装 OpenResty 和 LuaRocks.

2. Kong 安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
git clone git@github.com:Kong/kong.git
cd kong
git checkout 2.3.2
make install

cp bin/kong bin/busted ~/bin/
# kong 指令拷贝到 bin 目录后不需要将 kong/bin 添加到 PATH 中
cd ..
export PATH=$(pwd)/kong/bin:$PATH

# 添加设置 Kong 安装目录到 lua 包搜索路径
export KONG_LUA_PATH_OVERRIDE=$(pwd)/kong/

# 导出 luarocks 库变量到终端环境, 会导出 LUA_PATH, LUA_CPATH, PATH
eval $(luarocks path --bin)

# 检查安装结果
kong version --vv

如果不设置 KONG_LUA_PATH_OVERRIDE 环境变量, 需要进入 kong 安装目录执行 kong 命令.

执行 make install 命令会通过 luarocks 安装 Kong.

1
2
install:
@luarocks make OPENSSL_DIR=$(OPENSSL_DIR) CRYPTO_DIR=$(OPENSSL_DIR)

luarocks make 会根据当前目录下 ‘kong-*.rockspec’ 文件进行 Kong 安装, 将 kong 文件拷贝到 luarocks 本地库.

安装完成后, kong 代码会被安装到 ‘/usr/local/share/lua/5.1/kong’ 目录下, 可以修改此处代码进行调试. 通过可执行文件 ‘kong’, 输出 package.path 可以找点 OpenResty 库加载路径.

二 配置管理

1. Kong 配置

使用 PostgreSQL 作为配置数据库, 安装 PostgreSQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sudo apt-get update
sudo apt install postgresql-12

# 开启
sudo /etc/init.d/postgresql start
# 关闭
sudo /etc/init.d/postgresql stop
# 重启
sudo /etc/init.d/postgresql restart

sudo -i -u postgres
# 进入 postgre 数据库
psql

# 创建 kong 用户与数据库
CREATE USER kong;
CREATE DATABASE kong OWNER kong;
ALTER USER kong WITH PASSWORD 'kong';
# 修改 postgresql.conf pg_hba.conf 允许远程访问

Kong 启动时需要根据配置文件生成 NGINX 配置文件, 默认会尝试读取 ‘/etc/kong/kong.conf’, ‘/etc/kong.conf’ 作为启动配置文件. 在启动时可以通过 --conf file_path 指定配置文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo mkdir -p /etc/kong/
# 注意修改 postgre 数据库密码
cp kong.conf.default /etc/kong/kong.conf

# 检查配置是否有问题
kong check

# 生成 nginx 配置
kong prepare

# 初始化数据库
kong migrations bootstrap

# 启动
kong start

2. Kong 默认监听端口

默认情况下, Kong 监听以下端口进行转发、管理

端口 功能 配置名
8000 反向代理端口 proxy_listen
8001 管理端口, http admin_listen
8444 管理端口, https admin_listen

3. 常用 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 节点信息
curl -i "http://localhost:8001/"

# 查看状态
curl -i "http://localhost:8001/status"

# 查看工作区
curl -i "http://localhost:8001/workspaces"

# 列出 service
curl -i "http://localhost:8001/services"

# 创建 service
curl -i \
-H "content-type: application/json" \
"http://localhost:8001/services" \
-d '{
"name": "my-service",
"retries": 5,
"protocol": "http",
"host": "example.com",
"port": 80,
"path": "/some_api",
"connect_timeout": 60000,
"write_timeout": 60000,
"read_timeout": 60000,
"tags": ["user-level", "low-priority"]
}'

# 列出 route
curl -i "http://localhost:8001/routes"

# 使用之前创建的 service 创建 route
curl -i \
-H "content-type: application/json" \
"http://localhost:8001/routes" \
-d '{
"name": "my-route",
"protocols": ["http", "https"],
"methods": ["GET", "POST"],
"hosts": ["example.com", "foo.test"],
"paths": ["/foo", "/bar"],
"https_redirect_status_code": 426,
"strip_path": true,
"path_handling": "v0",
"preserve_host": true,
"request_buffering": true,
"response_buffering": true,
"tags": ["user-level", "low-priority"],
"service": {"id":"afa80698-f510-4f28-b12e-642cb447f91c"}
}'

# 列出插件配置
curl -i "http://localhost:8001/plugins"

# 创建插件配置
curl -i \
-H "content-type: application/json" \
"http://localhost:8001/plugins" \
-d '{
"name": "rate-limiting",
"config": { "hour": 500 }
}'

三 数据层面代码

在执行完 ‘kong migration bootstrap’ 后, 会生成 ‘nginx-kong.conf’ 文件(根据 ‘kong.conf’ 配置生成), 通过 ‘nginx-kong.conf’ 文件可以看到 kong 介入的处理阶段.

下面介绍各个阶段执行流程.

1. 库加载

在导入 kong 模块时会调用 ‘kong.globalpatches’ 对 ngx.socket.tcp, ngx.socket.udp 进行 hook, 目的时在进行连接时调用 resty.dns.client 进行地址解析.

lua-resty-dns-client 是独立的库.

2. init

  • ‘.kong_env’ 配置文件加载, 作为全局配置数据

  • 重置共享内存内容

  • 根据 Kong 版本加载相应的 PDK 加载, 插件加载后可以直接通过全局 ‘kong’ 实例访问插件代码

  • 数据库初始化:
    初始化数据库实体模型(schema): Connector 和 Strategy. 在 init、init_worker 阶段可以使用 luasocket 进行同步网络操作, 建立与数据库连接进行查询.

  • 初始化 dns_client

  • 如果转发端口开启 HTTPS, 加载转发使用的 SSL 证书、秘钥

  • 加载集群数据层面、控制层面 SSL 证书、秘钥

  • 加载插件数据库实体模型(schema)
    是通过 DB.__index 元方法加载 DAO 中的 ‘plugins’.

  • 构建插件迭代器:
    插件配置的 ‘combos’ 数组, 通过 ‘combos’ 可以实现插件配置优先级. Route、Service、Consumer 相互组合后优先级. 在一次请求中, 最多运行插件一次, 但是插件使用的配置可以有多种选择. 选择使用 Service、Route、Consumer 的配置是有优先级顺序的.
    插件可以介入多个处理阶段(init_worker/access…), 在插件迭代器构建阶段, 会将插件的处理函数插入 workspace 中阶段表中.

  • 路由初始化
    构建路由匹配表, 路由匹配优先级: 完整域名 > 正则域名 > 请求头特征多 > 请求头特征少 > 正则优先级高 > 正则优先级低 > URI 路径长 > URI 路径短 > 创建时间晚 > 创建时间早.

    详见四 数据层面代码 - 路由初始化.

    3. init_worker

  • 数据库、缓存初始化

  • 创建统计信息上报定时器, 定时向 kong-hf.konghq.com 发送报告, 将统计信息上报

  • 检查插件是否更新, 构建插件迭代器(plugins_iterator)

  • 插件初始化

4. ssl_certificate

  • 根据 SNI(非请求头 host)查找对应的证书、私钥, 并更新到请求信息中
  • 执行插件

5. rewrite

  • 执行插件

6. access

  • 路由匹配, 未匹配返回 404
  • balancer 预处理: 初始化 balancer_data, 设置默认超时时间、重试次数; service 证书、秘钥处理
  • websocket 升级处理、XFF 头处理
  • 如果 service 使用 grpc 协议, 跳转到 grpc location 处理
  • 根据路由是否启用请求体、响应体缓存, 跳转到相应 location 处理
  • 执行插件
  • balancer 执行:
    将数据库 upstream 表中所有 upstream 加载到进程内(在 init 阶段可能已经加载到进程内).
    根据路由匹配的 service 名, 从 upstream 字典中查找 upstream.
    根据 upstream.id 从缓存获取或创建 balancer 对象, 支持: 一致哈希(resty.dns.balancer.consistent_hashing)、最小连接(resty.dns.balancer.least_connections)、轮询(resty.dns.balancer.ring)三种负载均衡器. 从缓存或数据中获取 upstream 的负载实例(target), 并添加到负载均衡器中. 创建相应的健康检查对象, 并缓存负载均衡对象. 尝试使用 balancer 对象查找负载实例, 失败则直接响应 503.
    如果未查找到 balancer 对象, 使用 dns 解析(resty.dns.client)域名获得 IP、Port.
    保存获取到的 IP、Port 转发使用.

7. balancer

  • 通过 balancer 对象获取负载实例, 设置后端实例 IP、Port. 如果是重试进入 balancer 阶段, 上报上个实例的健康状态
  • 设置实例超时时间
  • 设置 keepalive (kong 修改了 lua-nginx-module, 可以通过 lua 代码控制开启长连接)

8. header filter

  • websocket upgrade 处理, 请求头过滤
  • 执行插件

9. body filter

  • 执行插件

10. log

  • 执行插件
  • balancer 健康检查上报

四 数据层面代码 - 路由初始化

在 ‘init’ 阶段会进行路由初始化, 构建路由匹配表.

1. 优先级

路由匹配优先级: 完整域名 > 正则域名 > 请求头特征多 > 请求头特征少 > 正则优先级高 > 正则优先级低 > URI 路径长 > URI 路径短 > 创建时间晚 > 创建时间早.

2. 路由分类

累加路由启用的匹配类别得到路由匹配类别(categories), 依次路由匹配特征以特征值为键, 路由为值保存到匹配表中. 在路由分类处理中, 使用路由的 ‘match_weight’ 作为类别的 ‘match_weight’, 因为路由已经根据优先级进行排序, 类别的 ‘match_weight’ 是本类别中所有路由最大的 ‘match_weight’. 路由类别(categories)会根据优先级从高到低进行排序, 在路由匹配过程中会尝试从高优先级开启匹配, 直至匹配成功.

3. 路由匹配

路由匹配逻辑复杂, 会先根据请求特征(方法、URI、域名、源地址/端口、目的地址/端口、SNI)从缓存中获取历史匹配结果. 缓存获取失败, 会根据请求头、域名、URI、方法、源/目的地址、SNI单独匹配, 记录适用匹配类别和命中的特征(每各类别仅命中一次). 在构建匹配索引(‘plain_indexes’, ‘prefix_uris’, ‘regex_uris’…)时已经根据路由优先级排好序, 高优先级在前.
使用上一步的处理结果, 遍历比命中的匹配类别低的所有类别; 对于每个类别, 取类别中 routes_by_hosts/routes_by_headers… 中最小数量的路由数组(使用最小集合进行匹配, 提高效率; 最小集合匹配失败后会遍历所有路由进行匹配). 对于集合(最小集合/全部路由集合)中的每个路由, 使用匹配的请求特征进行匹配, 路由必须满着所有匹配的请求特征. 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
--- ctx 中保存有匹配的请求特征值
match_route = function(route_t, ctx)
-- run cached matcher
if type(matchers[route_t.match_rules]) == "function" then
clear_tab(ctx.matches)
return matchers[route_t.match_rules](route_t, ctx)
end

-- build and cache matcher

local matchers_set = {}

for _, bit_match_rule in pairs(MATCH_RULES) do
--- 遍历特征 匹配
if band(route_t.match_rules, bit_match_rule) ~= 0 then
matchers_set[#matchers_set + 1] = matchers[bit_match_rule]
end
end

matchers[route_t.match_rules] = function(route_t, ctx)
-- clear matches context for this try on this route
clear_tab(ctx.matches)

-- 路由 route_t 必须满足匹配的所有特征
for i = 1, #matchers_set do
if not matchers_set[i](route_t, ctx) then
return
end
end

return true
end

return matchers[route_t.match_rules](route_t, ctx)
end

路由构建, 路由分类函数 ‘categorize_route_t’ 代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
... -- init, omit
insert(category.all, route_t)

for _, host_t in ipairs(route_t.hosts) do
if not category.routes_by_hosts[host_t.value] then
category.routes_by_hosts[host_t.value] = {}
end

insert(category.routes_by_hosts[host_t.value], route_t)
end

--- omit code,
--- category.routes_by_headers
--- category.routes_by_uris
--- category.routes_by_methods
--- category.routes_by_sources
--- category.routes_by_destinations
--- category.routes_by_sni

分类后 ‘categories’ 结构示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"category_bit, 类别二进制位" : {
"match_weight": "同类别中, 最高优先级路由的匹配权重",
"all" : [ "route-1 对象", "route-2 对象" ],
"routes_by_hosts": {
"host-a" : ["route-1 对象", "route 对象"],
"host-b" : ["route-1 对象"]
},
"routes_by_headers" : { "header" : ["route 对象"] },
"routes_by_uris" : { "uri" : ["route 对象"] },
"routes_by_methods" : { "method" : ["route 对象"] },
"routes_by_sources" : { "source" : ["route 对象"] },
"routes_by_destinations" : { "dest" : ["route 对象"] },
"routes_by_sni" : { "sni" : ["route 对象"] },
}
}

地址/端口:

目的地址、端口其实是 Nginx 接收请求时 Server 端的地址、端口.
地址端口匹配用于 Stream 模式.

路由匹配实现函数 ‘find_route’ 代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
-- header match
for _, header_name in ipairs(plain_indexes.headers) do
if req_headers[header_name] then
req_category = bor(req_category, MATCH_RULES.HEADER)
hits.header_name = header_name
break // 匹配后结束 header 类别匹配
end
end

-- host match

if plain_indexes.hosts[host_with_port] or plain_indexes.hosts[host_no_port] then
req_category = bor(req_category, MATCH_RULES.HOST)
elseif ctx.req_host then
for i = 1, #wildcard_hosts do
local from, _, err = re_find(host_with_port, wildcard_hosts[i].regex, "ajo")
... -- omit
if from then
hits.host = wildcard_hosts[i].value
req_category = bor(req_category, MATCH_RULES.HOST)
break
end
end
end

-- uri match

for i = 1, #regex_uris do
local from, _, err = re_find(req_uri, regex_uris[i].regex, "ajo")

... -- omit

if from then
hits.uri = regex_uris[i].value
req_category = bor(req_category, MATCH_RULES.URI)
break
end
end

if not hits.uri then
if plain_indexes.uris[req_uri] then
hits.uri = req_uri
req_category = bor(req_category, MATCH_RULES.URI)
else
for i = 1, #prefix_uris do
if find(req_uri, prefix_uris[i].value, nil, true) == 1 then
hits.uri = prefix_uris[i].value
req_category = bor(req_category, MATCH_RULES.URI)
break
end
end
end
end

-- method match

if plain_indexes.methods[req_method] then
req_category = bor(req_category, MATCH_RULES.METHOD)
end

-- src match

if plain_indexes.sources[ctx.src_ip] then
req_category = bor(req_category, MATCH_RULES.SRC)
elseif plain_indexes.sources[ctx.src_port] then
req_category = bor(req_category, MATCH_RULES.SRC)
else
for i = 1, #src_trust_funcs do
if src_trust_funcs[i](ctx.src_ip) then
req_category = bor(req_category, MATCH_RULES.SRC)
break
end
end
end

-- dst match

if plain_indexes.destinations[ctx.dst_ip] then
req_category = bor(req_category, MATCH_RULES.DST)
elseif plain_indexes.destinations[ctx.dst_port] then
req_category = bor(req_category, MATCH_RULES.DST)
else
for i = 1, #dst_trust_funcs do
if dst_trust_funcs[i](ctx.dst_ip) then
req_category = bor(req_category, MATCH_RULES.DST)
break
end
end
end

-- sni match

if plain_indexes.snis[ctx.sni] then
req_category = bor(req_category, MATCH_RULES.SNI)
end

五 数据层面代码 - 事件触发与管理API

1. 事件

init_worker 阶段 runloop 执行 register_events 函数, 注册 routes/services/plugins/upstreams/targes 的 ‘crud’ 事件处理回调. 对于 routes/service/plugins 会使当前版本无效, 触发重建对象. 对于 upstreams 变动会发布 balancer 事件.

init_worker 阶段 runloop.balancer 模块执行初始化动作: 从数据库查询 upstream, 创建 balancer, 并创建定时器检查 upstream 是否有更新, 更新后查询所有的 upstream 更新到 ‘kong.core_cache’(共享内存) 中.

2. 管理 API

管理端会调用节点 API 接口, 触发事件处理.

Kong 使用 Lapis 框架提供管理 API, 所有的管理 API 位于 ‘kong.api’ 包下. ‘admin_content’ 函数使 API 入口, 在 ‘serve_content’ 中会调用 lapis.serve('kong.api') 使用 ‘kong.api’ 中所有路由进行管理.

读下 lapis 框架 serve 函数可以发现 serve 使用 require 加载模块, 并建立路由匹配对象进行请求处理.

service/route/upstream 管理 API 是通过 ‘kong.api.endpoints’ 模块中 ‘generate_collection_endpoints’ 函数创建, 在 ‘kong.api’ 加载时会调用. POST 调用接口时将数据保存到数据库中, 会调用 DAO 对象的 ‘insert’ 方法调用 post_crud_event 发布 ‘worker-events’ 事件, 触发在 runloop.init_worker 中注册的回调函数进行更新操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-- 生成通用 API 函数
local function generate_collection_endpoints(endpoints, schema, foreign_schema, foreign_field_name)
local collection_path
if foreign_schema then
collection_path = fmt("/%s/:%s/%s", foreign_schema.admin_api_name or
foreign_schema.name,
foreign_schema.name,
schema.admin_api_nested_name or
schema.admin_api_name or
schema.name)

else
collection_path = fmt("/%s", schema.admin_api_name or
schema.name)
end

endpoints[collection_path] = {
schema = schema,
methods = {
--OPTIONS = method_not_allowed,
--HEAD = method_not_allowed,
GET = get_collection_endpoint(schema, foreign_schema, foreign_field_name),
POST = post_collection_endpoint(schema, foreign_schema, foreign_field_name),
--PUT = method_not_allowed,
--PATCH = method_not_allowed,
--DELETE = method_not_allowed,
},
}
end

local function post_collection_endpoint(schema, foreign_schema, foreign_field_name, method)
return function(self, db, helpers, post_process)

if foreign_schema then
local foreign_entity, _, err_t = select_entity(self, db, foreign_schema)
if err_t then
return handle_error(err_t)
end

if not foreign_entity then
return not_found()
end

self.args.post[foreign_field_name] = foreign_schema:extract_pk_values(foreign_entity)
end

-- 数据插入数据库
local entity, _, err_t = insert_entity(self, db, schema, method)
if err_t then
return handle_error(err_t)
end

-- 调用注册回调方法
if post_process then
entity, _, err_t = post_process(entity)
if err_t then
return handle_error(err_t)
end
end

return created(entity)
end
end

六 负载选择

kong 使用 lua-resty-dns-client 进行负载选择, 借助 lua-resty-dns 实现域名解析. 在 lua-resty-dns-client 中抽象出 balancer/host/address 对象, balancer-host, host-address 都是一对多关系, 在负载选择最后使用 address 作为结果进行转发.

在生产环境中应该考虑 host 权重与 address 权重问题, lua-resty-dns-client 对此有阐述.

kong 支持轮询、哈希、最小连接负载方式, 完全由 Lua 代码实现.

1. 轮询

使用表 wheel 存储 address 对象,在实现中使用 randomlist 生成一个随机序列, 用来实现负载随机分布. 每个 address 对象会存储其占用的索引, 用于释放索引时使用. wheel 中所有记录都会被占满, 不会有空隙.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function _M.new(opts)
-- ... 忽略无关代码
local self = assert(balancer_base.new(opts))
-- ..
-- inject additional properties
self.wheel = nil -- 用于存储负载地址的表
self.pointer = nil -- 轮询时下个待选择地址
self.wheelSize = opts.wheelSize or 1000 -- 负载地址表大小
self.unassignedWheelIndices = nil -- wheel 中未分配的索引

-- ring_balancer 对 balancer_base 继承
-- inject overridden methods
for name, method in pairs(ring_balancer) do
self[name] = method
end

-- initialize the balancer
self.wheel = new_tab(self.wheelSize, 0)
self.unassignedWheelIndices = new_tab(self.wheelSize, 0)
self.pointer = math.random(1, self.wheelSize) -- ensure each worker starts somewhere else

-- Create a list of entries, and randomize them.
local unassignedWheelIndices = self.unassignedWheelIndices
local duplicateCheck = new_tab(self.wheelSize, 0)
-- 使用 randomlist 创建一个 1-wheelSize 范围的随机数数组, 数值不会重复. 用于将负载地址随机分布在 wheel 数组中
local orderlist = opts.order or randomlist(self.wheelSize)

for i = 1, self.wheelSize do
local order = orderlist[i]
unassignedWheelIndices[i] = order
end

-- Sort the hosts, to make order deterministic
-- ...
table_sort(hosts, function(a,b) return (a.name..":"..(a.port or "") < b.name..":"..(b.port or "")) end)
-- Insert the hosts
-- 添加地址, 并在 wheel 中添加索引
for _, host in ipairs(hosts) do
local ok, err = self:addHost(host.name, host.port, host.weight)
end

return self
end

地址变更后调用 redistributeIndices 重新分配各个负载在 ‘wheel’ 中的分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
function ring_balancer:redistributeIndices()
local totalWeight = self.weight -- Host 变更时会在 base 中会修改 weight
local movingIndexList = self.unassignedWheelIndices

-- NOTE: calculations are based on the "remaining" indices and weights, to
-- prevent issues due to rounding: eg. 10 equal systems with 19 indices.
-- Calculated to get each 1.9 indices => 9 systems would get 1, last system would get 10
-- by using "remaining" indices, the first would get 1 index, the other 9 would get 2.

-- first; reclaim extraneous indices
local weightLeft = totalWeight
local indicesLeft = self.wheelSize
local addList = {} -- addresses that need additional indices
local addListCount = {} -- how many extra indices the address needs
local addCount = 0
local dropped, added = 0, 0
-- 便利所有负载
for weight, address, _ in self:addressIter() do

-- 根据权重, 计算负载索引个数
local count
if weightLeft == 0 then
count = 0
else
count = math_floor(indicesLeft * (weight / weightLeft) + 0.0001) -- 0.0001 to bypass float arithmetic issues
end
local drop = #address.indices - count
if drop > 0 then
-- we need to reclaim some indices
address:dropIndices(movingIndexList, drop)
dropped = dropped + drop
elseif drop < 0 then
-- this one needs extra indices, so record the changes needed
-- 记录需要添加索引的地址, 稍后为地址分配索引
addCount = addCount + 1
addList[addCount] = address
addListCount[addCount] = -drop -- negate because we need to add them
end
indicesLeft = indicesLeft - count
weightLeft = weightLeft - weight
end

-- 为地址分配索引
-- second: add freed indices to the recorded addresses that were short of them
for i, address in ipairs(addList) do
address:addIndices(movingIndexList, addListCount[i])
added = added + addListCount[i]
end

return self
end

function ring_address:addIndices(availableIndicesList, count)
count = count or #availableIndicesList
if count > 0 then
local myWheelIndices = self.indices
local size = #myWheelIndices
-- ...
local wheel = self.host.balancer.wheel
local lsize = #availableIndicesList + 1
for i = 1, count do
-- 获取一个未分配索引
local availableIdx = lsize - i
local wheelIdx = availableIndicesList[availableIdx]
availableIndicesList[availableIdx] = nil
-- 当前地址增加一个索引,(地址删除索引时使用)
myWheelIndices[size + i] = wheelIdx
wheel[wheelIdx] = self
end

end
return self
end

2. 一致哈希

与轮询相似, 使用表 continuum 存储所有负载, 每个负载根据权重得出其在 continuum 表中的索引条数. 在添加负载时会根据 IP、Port、第几条索引计算哈希值, 确定其在 continuum 表中的索引, 参见 consistent_hashing:afterHostUpdate 函数.

负载选择时会根据哈希值选择负载, 如果 continuum 表中条目为空则索引减一尝试选择之前索引, 参见 consistent_hashing:getPeer 函数.

3. 最小连接

与 Nginx 实现不同, Kong 实现的最小连接是通过使用最小堆记录每个负载的连接数, 选择连接数最小的负载. 使用者需要在负载使用完毕后调用(或通过 GC 触发) release 方法更新负载连接数.

七 konga

可以使用 konga 作为管理端, 在编译时需要使用 node 10.x 版本, 安装操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
npm install -g n

#切换到 10.24.0
sudo n v10.24.0
node -v

# 数据库初始化
su - postgres
psql
# 创建用户
CREATE USER konga WITH PASSWORD 'konga';
# 创建数据库
CREATE DATABASE konga OWNER konga;
# 授权
GRANT ALL PRIVILEGES ON DATABASE konga TO konga;

参考文档