服务集群:使用SelectPolicy选择目标节点
Created By RV, and licensed with Creative Commons "CC BY-NC-ND 4.0"
目的与场景
在这一章,讲解如何使用 SelectPolicy,为服务集群的调用选择目标节点:在完成WhoHas-IHave服务动态发现后,从发现的服务节点群中,挑选出1到N个节点以备调用。
一. 概念
1. 服务集群:ServiceCluster
这里,应该是我们第一次引入“服务集群”的概念。
Kree4X中,多个节点可以注册同名服务。这些提供了同名服务的节点,共同构成一个服务集群(ServiceCluster)。
我们一直讲的“服务存根”,实际就是“服务集群”的透明代理。
详情,参阅:服务集群:ServiceCluster
2. 服务选择:SelectPolicy
调用发起者通过 Kree4X.service(name) 得到服务存根后,可以发起服务调用。
一次服务调用,能发现的服务节点,来自于WhoHas-IHave服务动态发现机制。
一次服务调用,从已经发现的节点中,最终选择哪些服务节点作为调用目标,由 SelectPolicy 决定。
这个挑选的过程,被称为服务选择:Service Selection。
具体的选择算法,被称为选择策略:Select Policy。
详情,参阅:服务选择与SelectPolicy
3. 单目标vs多目标
Kree4X,并不限制一次服务调用可以实际调用的目标服务的个数。
如果SelectPolicy执行的结果是多个节点,则会对选中的每个节点发起调用,调用的多个结果,需要使用ReducePolicy归约为“1”个结果,这个下章再讲。
- 单目标选择:
SelectFirst/SelectLoop/SelectRandom/SelectSticky等选择策略,选择的结果是“1”个节点。 - 多目标选择:
SelectTop(number)/SelectAll等选择策略,选择的结果是多个节点。
4. 默认策略
默认使用的SelectPolicy是 SelectFirst(取候选的第一个节点)。
但这并不总是合适:
- 若调用者在网格中分布均匀,每个调用者都使用SelectFirst,整体上看负载大致均衡。
- 若调用者分布不均衡,SelectFirst会导致负载堆积到少数节点,此时使用SelectLoop或SelectRandom较好。
- 若服务有状态,期望一次调用后始终与该服务节点保持会话,应使用 SelectSticky。
二. 示例代码
在下边的示例中,我们将:
- node-a / node-b / node-c:3个服务节点,注册同名
sensor服务,分别返回10 / 20 / 30 - caller:调用发起者,依次演示不同选择策略
1. 注册服务,获取存根
// node-a / node-b / node-c:注册同名sensor服务,返回10 / 20 / 30
nodeA.register('sensor', { read () { return 10 } })
nodeB.register('sensor', { read () { return 20 } })
nodeC.register('sensor', { read () { return 30 } })
// 调用发起者,获取服务存根
const caller = create('caller')
const sensor = caller.service('sensor')
// 如何获得候选节点? 等待收集到3个提供者
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
2. 单目标选择策略
每个策略演示前,都重新获取 sensor 服务存根(类型都是服务存根,只是设定的选择策略不同),并 waitServiceFind(three) 等满3个提供者:
// SelectFirst:始终选第一个节点(等满3个,始终命中node-a)
let sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(new SelectPolicy.SelectFirst())
await sensor.read() // 10,落在node-a
// SelectLoop:轮询选择,从3个候选中依次挑选
sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(new SelectPolicy.SelectLoop())
await sensor.read() // 10(node-a)
await sensor.read() // 20(node-b)
await sensor.read() // 30(node-c)
// SelectRandom:随机选择,从3个候选中随机挑一个
sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(new SelectPolicy.SelectRandom())
await sensor.read() // 随机命中某个节点
// SelectSticky:首次选定后,后续尽量复用同一节点
sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(new SelectPolicy.SelectSticky(1))
await sensor.read() // 首次选定某个节点
await sensor.read() // 后续复用同一节点
3. 多目标选择策略,默认ReducePolicy是SelectFirst
// SelectTop(2):取前2个节点发起调用,结果用默认ReduceFirst归约
sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(new SelectPolicy.SelectTop(2))
await sensor.read()
// SelectAll:全选所有节点发起调用
sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(new SelectPolicy.SelectAll())
await sensor.read()
4. 自定义SelectPolicy
实现 SelectPolicy.SelectPolicy 的子类,覆盖 select(candidates, ...),即可注入自定义策略。以下示例按最近响应时间选择最快节点:
// 自定义SelectPolicy:按最近响应时间选最快节点
class FastestResponsePolicy extends SelectPolicy.SelectPolicy {
constructor (howMany = 1) {
super(howMany)
this._responseTimes = new Map() // nodeId → lastResponseTime
}
// 记录响应时间,通常由拦截器在afterCall中调用
recordResponseTime (nodeId, time) {
this._responseTimes.set(nodeId, time)
}
select (candidates, ctx, methodName, params, options) {
if (candidates.length === 0) return undefined
// 按上次响应时间排序(快者优先),无记录的排最后
const scored = candidates
.map(c => ({ candidate: c, time: this._responseTimes.get(c) ?? Infinity }))
.sort((a, b) => a.time - b.time)
return scored.slice(0, this.howMany).map(s => s.candidate)
}
}
// 注入自定义策略
const fastestPolicy = new FastestResponsePolicy(1)
fastestPolicy.recordResponseTime('node-a', 50)
fastestPolicy.recordResponseTime('node-b', 30) // 最快
fastestPolicy.recordResponseTime('node-c', 100)
sensor = caller.service('sensor')
sensor.waitServiceFind(ServiceFindWaitPolicy.three(5000))
sensor.select(fastestPolicy)
await sensor.read() // 落在node-b(响应最快)
SelectPolicy从@kree4js/kree4n命名空间导出:import { SelectPolicy } from '@kree4js/kree4n'。
三. 须强调的细节
1. 候选来源:WhoHas-IHave动态发现
select() 的候选集来自WhoHas-IHave动态发现机制。
动态发现的IHava应答节点,作为candidates候选者,交给 SelectPolicy.select(candidates) 去挑选。
设 ServiceFindWaitPolicy.three() ,发现3个候选者,则SelectPolicy只能从中选择。
首次调用后发现的候选者,会被缓存,后续直接复用。
2. 多目标需要足够候选
SelectTop(number) 等策略要求候选节点数不小于指定的 number。
候选不足时,会导致动态发现过程失败,服务调用抛出异步异常。
3. 多目标搭配ReducePolicy
多目标选择会发起多次调用,产生多个结果。
调用者只需一个结果,因此必须配合 ReducePolicy 归约(见下一章)。
不显式配置,默认 ReduceFirst 。
4. Sticky的会话粘性
SelectSticky 首次选中一个节点后,后续调用尽量复用同一节点,直到其失效。
适合有状态服务(如会话保持)。
注意:SelectSticky仅保证始终选中同一个服务节点,如果此服务节点开放了多个同名服务,或者开放的是CallService(每次调用创建新实例),则不能保证多次调用的是同一个服务实例。
5. 网格变化清理缓存
网格节点增删时,SelectSticky/SelectLoop 等有内部状态(索引、粘性节点)的策略,会调用清理内部缓存的对应节点,避免选择已失效节点。
四. 涉及到的API:
1. 设定选择策略:serviceStub.select()
/**
* 为服务存根设定目标节点选择策略。
*
* @param {SelectPolicy} selectPolicy - 选择策略(决定本次调用落到哪些候选节点)。
* @returns {this} 当前存根,支持链式。
*/
select(selectPolicy): this
2. SelectPolicy:选择策略基类
/**
* 从候选节点群中选择本次调用的目标节点。
*/
class SelectPolicy {
/**
* @param {number} [howMany] - 选择目标节点数量,默认1。
*/
constructor(howMany?: number)
/**
* 从候选节点中挑选目标。
*
* @param {Service[]} candidates - 候选节点数组。
* @param {ServiceClusterCallContext} [clusterCallContext] - 调用上下文。
* @returns {Service[]|undefined} 选中的目标节点数组。
*/
select(candidates: Service[], clusterCallContext?: ServiceClusterCallContext): Service[] | undefined
}
3. 內建的选择策略实现
| 类名 | 作用 |
|---|---|
SelectFirst |
始终选第一个候选节点。 |
SelectLoop(howMany=1) |
轮询,按序依次选择,支持环绕取数。 |
SelectRandom(howMany=1) |
随机选择指定数量的节点。 |
SelectSticky(howMany, picker?) |
首次选择后,后续尽量复用同一节点;候选不足时用 picker(默认随机)补齐。 |
SelectTop(howMany=1) |
取候选的前N个节点。 |
SelectAll |
全选所有候选节点(howMany=全部)。 |
五. 可运行代码
完整示例代码,参见:04-select-policy.mjs