记一次有状态服务的负载均衡方案探索

背景

我们的服务是有状态服务,服务在提供对外访问时,需要负载到特定节点。

基本方案

一般来说,这种特定节点的负载均衡有两种基本方案:

  1. 重定向
  2. 代理模式

一起来看一下几个有状态服务的集群处理方式:

  • Redis 在 6.0 以前是直接的采用 重定向 的方式,6.0 也提供了集群内代理的模式。
  • Redis 的 codis 版集群,采用的是 代理模式。
  • Mycat 数据库代理采用的是 代理模式。
  • Mongodb cluster 采用的是 mongos 代理。
  • Kafka 采用的是客户端自定义的负载均衡方式,整体来看是 重定向方式 (根据 partition 的位置决定地址)。

基本可以认为,负载均衡的这两种策略没有太大的优劣之分,只要实现好 client,对业务方来说,区别不大。

由于代理模式对机器资源的消耗更多,并且将来维护更加复杂,于是我选择先采用 重定向 的策略,这需要有两方面改动:

  1. 所有节点均知道特定的 key 应该到哪一个具体的 节点。
  2. 返回的重定向数据,能够达到正确的节点。

下面分别解决这两个问题。

解决负载信息同步问题

在业务侧,需要通过类似于 注册中心的机制,用于确定不同的 key 对应的 节点地址。这个注册中心有两种可供参考的模式: ① 无状态的包,所有状态通过中央存储( eg: redis/etcd ) 进行共享。② 状态交由特定的服务进行维护,其他 client 通过调用这个服务的接口获取信息。

第一种方案,类似于 k8s 的设计,所有源信息全在 etcd 中,各模块均通过监听 etcd 中的元信息变化做出自己的动作,这样做的好处是 轻量化,仅需要约定好数据结构即可,不用维护单独的服务,但为避免误用,需要提供 SDK。

第二种方案,类似于 mongodb 中的 config-server ,所有元信息交由 config-server 维护,其他节点 (mongos)通过本地缓存的方式提升性能。 这种方式的好处在于 权责分明,在没有太多精力维护 sdk 的情况下,这种方式更不容易出错。

其实,也可以认为还有第三种方案: 去中心化方案。类似于 redis 的集群通信方式,每个节点都存着一份整个集群的信息,并且通过一定的方式保证集群内数据一致性。但这种方式的实现更加复杂,也没有看到有什么更大的价值,暂不考虑。

在我的基本实现中,采用 抢占式 的模式,用 redis 做状态同步,整个流程类似于 “分布式锁” 的过程,可以达到负载到特定节点的目的,但整体比较粗糙,将来的可扩展性也不是很好。

不过值得参考的是,该实现中,采用了 redis 的 watch 机制,可以在各节点做本地缓存,有更新后也能更新缓存。这是一个很不错的技术点。

在将来要实现的版本中,应当是由一个服务来做负载均衡的策略,包括收集节点状态、新节点启动、老节点清理、数据迁移 等操作。 这部分可以更多参考 mongodb 的 config-server 相关设计。

在保证了注册中心机制后,就是网络路由问题了。

解决 nginx 定向路由问题

由于服务是在内网中,也不能将内网服务的 ip 直接暴露在公网上,因此,要有从公网路由到内网特定节点的能力。

我们目前采用的是 k8s 的部署方式,网关处使用 nginx-ingress 进行路由 和 负载均衡。nginx-ingress 默认提供了 轮询、加权、hash、一致性hash 的负载均衡策略,且 hash 函数不是我们能指定的。因此,这些策略无法满足我们的需求。

不过 nginx-ingress 提供了自定义负载均衡策略的方式(通过 lua 脚本),也就意味着我们能够自定义负载均衡策略。

以下是基本实现:

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
-- file longbalancer.lua
local util = require("util")

local string_format = string.format
local ngx_log = ngx.log
local INFO = ngx.INFO
local _M = {}

function _M.new(self, backend)
local o = {
name = "longbalance"
}
o.addrs, o.addrList, o.nums = util.get_addrs(backend.endpoints)
o.eps = util.get_nodes(backend.endpoints)
o.nowLen = 0

setmetatable(o, self)

self.__index = self
return o
end

function _M.sync(self, backend)

local eps = util.get_nodes(backend.endpoints)
local changed = not util.deep_compare(self.eps, eps)
if not changed then
return
end

ngx_log(INFO, string_format("nodes have changed for backend %s", backend.name))

self.addrs, self.addrList, self.nums= util.get_addrs(backend.endpoints)
self.eps = eps
self.nowLen = 0

end

function _M.balance(self)
local balance_by = ngx.var["balance_by"]
if balance_by == nil then
balance_by = "$docdoc"
end

local balance_val = util.lua_ngx_var(balance_by)

ngx_log(INFO, string_format("balance key is : %s, val is : %s", balance_by,balance_val))

return self.find(self, balance_val)
end

function _M.getnext(self)
local addr = self.addrList[self.nowLen]

self.nowLen = self.nowLen + 1

if self.nowLen == self.nums then
self.nowLen = 0
end

return addr
end

function _M.find(self, balance_val)
local addr

if balance_val == nil or balance_val == "" or balance_val == 0 then
addr = self.getnext(self)
return addr
end

addr = self.addrs[balance_val]
if addr == nil then
addr = self.getnext(self)
end

return addr
end

return _M

-- file util.lua
function _M.get_addrs(endpoints)
local addrs = {}
local addrList = {}
local nums = 0

for _, endpoint in pairs(endpoints) do
addrs[endpoint.address] = endpoint.address .. ":" .. endpoint.port
addrList[nums] = endpoint.address .. ":" .. endpoint.port
nums = nums + 1
end

return addrs, addrList, nums
end

然后在 balancer.lua 文件中导入 longbalancer 即可。

另外,为了服务能够使用正确的负载均衡策略,需要在 服务的 ingress 中添加如下注解

1
2
3
nginx.ingress.kubernetes.io/configuration-snippet: |
set $docdoc $arg_insip; # 设置负载均衡参数
nginx.ingress.kubernetes.io/load-balance: longbalance # 选择负载均衡策略

自此,nginx 拥有了根据特定的参数进行定向路由的能力。
[鼓掌 ! 👏]

这里实际上是有优化空间的,有两个方向:

  1. 添加 缓存 => documentID : insip 在nginx进行缓存,没有传 insip 的参数时,先通过缓存判断,没有再走轮询。
  2. 直接接入向 config-server 访问的能力,在网关层直接定位到确定的节点,而不是靠重定向。

各有优劣,之后再做分析


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!