背景
我们的服务是有状态服务,服务在提供对外访问时,需要负载到特定节点。
基本方案
一般来说,这种特定节点的负载均衡有两种基本方案:
- 重定向
- 代理模式
一起来看一下几个有状态服务的集群处理方式:
- Redis 在 6.0 以前是直接的采用 重定向 的方式,6.0 也提供了集群内代理的模式。
- Redis 的 codis 版集群,采用的是 代理模式。
- Mycat 数据库代理采用的是 代理模式。
- Mongodb cluster 采用的是 mongos 代理。
- Kafka 采用的是客户端自定义的负载均衡方式,整体来看是 重定向方式 (根据 partition 的位置决定地址)。
基本可以认为,负载均衡的这两种策略没有太大的优劣之分,只要实现好 client,对业务方来说,区别不大。
由于代理模式对机器资源的消耗更多,并且将来维护更加复杂,于是我选择先采用 重定向 的策略,这需要有两方面改动:
- 所有节点均知道特定的 key 应该到哪一个具体的 节点。
- 返回的重定向数据,能够达到正确的节点。
下面分别解决这两个问题。
解决负载信息同步问题
在业务侧,需要通过类似于 注册中心的机制,用于确定不同的 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
| 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
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 拥有了根据特定的参数进行定向路由的能力。
[鼓掌 ! 👏]
这里实际上是有优化空间的,有两个方向:
- 添加 缓存 => documentID : insip 在nginx进行缓存,没有传 insip 的参数时,先通过缓存判断,没有再走轮询。
- 直接接入向 config-server 访问的能力,在网关层直接定位到确定的节点,而不是靠重定向。
各有优劣,之后再做分析