☰
Finagle ThriftMux 分区感知客户端(Partition Aware Client)完整实践指南
2026/9/25 1:37:33 网站建设 项目流程
  • 后端
  • RPC框架

【免费下载链接】finagle

A fault tolerant, protocol-agnostic RPC system

项目地址:https://gitcode.com/gh_mirrors/fi/finagle
点击查看免费下载

本篇技术指南以 PartitionAwareClient.rst 为骨架,结合 Finagle 仓库中finagle-thrift与finagle-partitioning模块的源码实现与端到端测试,系统讲解如何为 Thrift/ThriftMux 客户端启用分区感知(Partition Aware)路由:从PartitioningParams配置、Hashing 与 Custom 两种策略的选型,到非 fan-out / fan-out(散射-聚合)场景下的请求拆分与合并函数实现,以及动态重分片与相关 Metrics。读完本文,你将能够为按数据分区部署的 Thrift 后端服务编写可运行的分区感知客户端,并能依据源码理解其底层路由机制。

注意:本文介绍的这套 API 位于com.twitter.finagle.thrift.exp.partitioning包中,属于实验性(experimental)API,其类名与方法签名可能在后续版本中调整。

核心术语:Partition 与 Shard / Instance

在动手配置之前,先厘清文档中定义的两个基础概念:

  • Partition(分区):一个处理数据的逻辑实体。它可以是一个物理实例,也可以是一组物理实例的集合;分区内每个实例在该分区负责的数据范围内被视为等价(equivalent)。分区之间可以重叠,即一个实例可以同时属于多个分区。
  • Shard / Instance(分片 / 实例):一个物理实例(一个进程、一台机器上的服务)。

理解这一区别很重要:分区是数据维度的逻辑概念,实例是部署维度的物理概念。Custom 策略中的"逻辑分区"(logical partition)正是利用二者的映射关系来实现"多实例共属一分区、一实例跨多分区"的灵活拓扑。

启用分区感知:PartitioningParams 配置 API

配置 Thrift/ThriftMux 客户端分区能力的 API 集中在 PartitioningParams.scala(com.twitter.finagle.thrift.exp.partitioning.PartitioningParams)。它通过self.configured(...)把参数注入客户端栈,包含以下入口:

API作用说明
.strategy(partitioningStrategy: PartitioningStrategy)为客户端配置分区策略接受HashingPartitioningStrategy或CustomPartitioningStrategy两种实现
.ejectFailedHost(eject: Boolean)决定失败主机是否从哈希环上剔除仅 Hashing 策略相关,默认关闭(false)
.keyHasher(hasher: KeyHasher)定义将 key 映射到分区的哈希函数仅 Hashing 策略相关,默认KeyHasher.KETAMA
.numReps(reps: Int)每个节点在哈希环上的虚拟副本数仅 Hashing 策略相关,默认160

这三个 Hashing 专属参数的默认值可以在 Params.scala 中直接找到证据:

  • EjectFailedHost的默认值是EjectFailedHost(false),即默认不剔除;
  • KeyHasher的默认值是KeyHasher.KETAMA(Ketama 一致性哈希算法);
  • NumReps的默认值是NumReps(160),Default = 160。

关于ejectFailedHost,源码注释给出了一条重要的工程提醒:该开关开启后,剔除动作依赖ConsistentHashingFailureAccrualFactory(见 ConsistentHashingFailureAccrualFactory.scala)收集的失败信号,集群中各进程可能对同一主机持有不同的健康视图;在结合分区策略更新时,可能引入进程间哈希环的不一致。文档与源码都建议:在多数场景下,最好通过基于全局视图的独立机制(如服务发现层)来剔除失败主机。

在 Finagle 6 客户端栈上启用分区感知的完整示例(文档原文):

import com.twitter.finagle.ThriftMux import com.twitter.finagle.thrift.exp.partitioning.{ClientCustomStrategy, ClientHashingStrategy} val hashingPartitioningStrategy: ClientHashingStrategy = ??? val clientWithHashing = ThriftMux.client .withPartitioning.strategy(hashingPartitioningStrategy) .withPartitioning.ejectFailedHost(false) val customPartitioningStrategy: ClientCustomStrategy = ??? val clientWithHashing = ThriftMux.client .withPartitioning.strategy(customPartitioningStrategy)

其中strategy(...)的底层动作是把策略封装成ThriftPartitioningService.Strategy(partitioningStrategy)参数注入客户端栈(见PartitioningParams.scala第 18-19 行),后续由ThriftPartitioningService在栈中负责按分区分发请求。

ThriftMux MethodBuilder 方式配置分区策略

MethodBuilder 构建在 Finagle 6 API 之上,定位是"按 endpoint(方法)定制客户端",因此分区配置可以精确到每个 endpoint。分区策略通过.withPartitioningStrategy(partitioningStrategy: PartitioningStrategy)应用到某个 MethodBuilder endpoint 上,同一个 MethodBuilder 可以为不同 endpoint 装配不同策略。

两者的分工在文档中说得非常明确:

  • 上面提到的 Hashing 专属参数(ejectFailedHost、keyHasher、numReps)仍然留在 Finagle 客户端栈层,因为它们拥有相当通用的默认值,几乎不需要为每个 endpoint 单独配置;
  • 而PartitioningStrategy本身则在 MethodBuilder 层按 endpoint 定制。

MethodBuilder endpoint 与 Finagle 客户端栈的核心差异在于"作用域":MethodBuilder 是逐 endpoint 定制的,分区策略只需负责一个 endpoint 的请求;Finagle 客户端栈则要兼顾同一客户端上所有 endpoint,因此其路由函数必须写成PartialFunction,以便用模式匹配区分不同请求类型(不同方法)。附录部分给出了完整的 MethodBuilder 实现示例。

如何选择分区策略:Hashing vs Custom

文档给出两种开箱即用的抽象,选择时主要考虑拓扑管理的自动化程度与对热分片的掌控力:

HashingPartitioningStrategy(哈希策略)

  • 底层内置一致性哈希(consistent hashing)算法,将分区节点分布到哈希环上,对每个请求的 key 施加哈希函数后路由到目标节点;
  • 可以免去服务运维人员手工管理分区拓扑;
  • 天然支持弹性扩缩容:扩容或缩容时,只有少量 key 的归属发生变化,负载变化最小化;
  • 局限:这些内置机制"不感知你的负载特征",对于特定拓扑未必完美适配。

CustomPartitioningStrategy(自定义策略)

  • 提供更大的灵活性来定义分区拓扑,客户端配置完全掌控请求的分布;
  • 需要实现者认真对待热分片(hot shards)问题,主动预判并协调流量;
  • 典型用例是key range 策略:把整个 key 集合划分为连续区间,把每个区间分配给一个分区;
  • 附带**动态重分片(dynamic resharding)**支持。

除策略选型外,文档还提醒要考虑两个业务维度:

  1. 是否做 messaging fan-out(散射/扇出):即单个请求是否要拆成多个子请求并发发给多个分区,再把结果合并(scatter/gather)。Fan-out 的请求与响应必须是**可合并(mergeable)**的格式,例如数组类型变量,可以程序化地拆分与合并。
  2. 分区服务是否需要动态重分片:这决定了 Custom 策略选noResharding、resharding还是clusterResharding。

下面分别给出两种策略的完整实现步骤。

实战:实现 HashingPartitioningStrategy

示例 Thrift 服务定义

文档以deliveryService.thrift为例(仓库中的对应文件位于 finagle-thrift/src/test/thrift/delivery_service.thrift,生成的 Scala 类型为com.twitter.delivery.thriftscala):

namespace java com.twitter.delivery.thriftjava #@namespace scala com.twitter.delivery.thriftscala exception AException { 1: i32 errorCode } service DeliveryService { // non-fanout message Box getBox(1: AddrInfo addrInfo, 2: i8 passcode) throws ( 1: AException ex ) // fan-out message, easy to merge list<Box> getBoxes(1: list<AddrInfo> listAddrInfo, 2: i8 passcode) throws ( 1: AException ex ) } struct AddrInfo { 1: string name; 2: i32 zipCode; } struct Box { 1: AddrInfo addrInfo; 2: string item; }

该服务故意同时提供两个 endpoint:getBox(非 fan-out)与getBoxes(fan-out,list<Box>天然可合并),用于演示两种路由模式。

非 fan-out:定义getHashingKeyAndRequest

Hashing 策略要求实现getHashingKeyAndRequest方法。它的类型别名定义在 PartitioningStrategy.scala 第 188 行:

type ToPartitionedMap = PartialFunction[ThriftStructIface, Map[Any, ThriftStructIface]]

这是一个PartialFunction:输入原始 Thrift 请求,输出"哈希 key -> Thrift 请求"的 Map。非 fan-out 是简化形态——总是返回只含一个用户指定哈希 key 的 Map:

import com.twitter.delivery.thriftscala.Box import com.twitter.delivery.thriftscala.DeliveryService._ import com.twitter.finagle.thrift.exp.partitioning.ClientHashingStrategy val getHashingKeyAndRequest: ClientHashingStrategy.ToPartitionedMap = { // specify the AddrInfo.name as the hash key case getBox: GetBox.Args => Map(getBox.addrInfo.name -> getBox) } val hashingPartitioningStrategy = new ClientHashingStrategy(getHashingKeyAndRequest)

PartialFunction的好处是:一个策略可以通过多个 case 分支,为同一服务不同方法(endpoint)配置不同路由。未在模式中指定的方法会落入内置的defaultHashingKeyAndRequest(见PartitioningStrategy.scala第 209-212 行,实现为Map(None -> args))。这意味着分区感知客户端可以只服务于一个服务中的部分 endpoint。关键约束:未指定的 endpoint 不应通过该客户端调用,否则客户端会抛出NoPartitioningKeys异常(定义于 ConsistentHashPartitioningService.scala 第 22 行)。此外,若使用 MethodBuilder(逐 endpoint 配置),getHashingKeyAndRequest是普通函数而非PartialFunction。

fan-out:请求拆分与 RequestMerger / ResponseMerger

扩展开来,fan-out 场景的getHashingKeyAndRequest返回"多个哈希 key -> 子请求"的 Map,需要把原始请求按 key 拆分成多个子请求:

import com.twitter.delivery.thriftscala.DeliveryService._ import com.twitter.finagle.thrift.exp.partitioning.ClientHashingStrategy val getHashingKeyAndRequest: ClientHashingStrategy.ToPartitionedMap = { case getBoxes: GetBoxes.Args => getBoxes.listAddrInfo .groupBy(_.name).map { case (hashingKey, subListAddrInfo) => hashingKey -> GetBoxes.Args(subListAddrInfo, getBoxes.passcode) } } val hashingPartitioningStrategy = new ClientHashingStrategy(getHashingKeyAndRequest)

由于一致性哈希的本质,多个不同哈希 key 可能落在同一个分区上,因此需要告知 Finagle 分区层如何把发往同一分区的多个子请求合并为一个请求。为此提供RequestMerger辅助函数:接收一个 Thrift 请求序列,返回单个请求(要求请求格式可合并,即mergeable):

import com.twitter.finagle.thrift.exp.partitioning.PartitioningStrategy.RequestMerger val getBoxesReqMerger: RequestMerger[GetBoxes.Args] = listGetBoxes => GetBoxes.Args(listGetBoxes.map(_.listAddrInfo).flatten, listGetBoxes.head.passcode)

类型定义见 PartitioningStrategy.scala 第 38 行:type RequestMerger[Req <: ThriftStructIface] = Seq[Req] => Req。

fan-out 意味着客户端会收到来自一组分区的响应,因此还需要ResponseMerger统一处理批量成功与批量失败:接收"成功响应序列 + 失败异常序列",返回一个Try[ResponseType]:

import com.twitter.finagle.thrift.exp.partitioning.PartitioningStrategy.ResponseMerger import com.twitter.util.{Return, Throw} val getBoxesRepMerger: ResponseMerger[Seq[Box]] = (successes, failures) => if (successes.nonEmpty) Return(successes.flatten) else Throw(failures.head)

其类型定义为type ResponseMerger[Rep] = (Seq[Rep], Seq[Throwable]) => Try[Rep](同文件第 61 行)。文档与源码均强调:失败的子响应需要由应用自行处理(记录日志、异常处理等)——ResponseMerger只负责把它们汇集成最终结果,例如"全部失败才抛出第一个异常"。

注册 mergers

最后一步是把RequestMerger与ResponseMerger注册到策略的requestMergerRegistry与responseMergerRegistry中,与对应的ThriftMethod绑定。多个ThriftMethod可以级联注册(add返回 registry 自身,见源码第 80-86 行、第 124-127 行):

hashingPartitioningStrategy.requestMergerRegistry.add(GetBoxes, getBoxesReqMerger) hashingPartitioningStrategy.responseMergerRegistry.add(GetBoxes, getBoxesRepMerger)

需要留意 registry 的实现细节(PartitioningStrategy.scala第 66-152 行):底层Map非线程安全,源码注释明确假设add只在客户端初始化阶段被调用,运行时请求线程只做get读取。因此不要在运行期动态修改 merger 注册表。

实战:实现 CustomPartitioningStrategy

Custom 策略与 Hashing 策略共享同一套 fan-out / 非 fan-out 矩阵,但它把后端分区拓扑的管理权完全交给应用,并且支持把多个 shard 归并为一个逻辑分区、一个 shard 属于多个分区。Custom 分区还通过观察用户提供的状态来支持动态重分片。根据重分片需求,PartitioningStrategy.scala 提供三组 API:

API适用场景源码位置
ClientCustomStrategy.noResharding(...)无动态重分片,后端分区拓扑保持静态第 295-335 行
ClientCustomStrategy.reshardingA通过提供完整描述的重分片状态让客户端感知动态重分片;分区 schema 需要针对每个状态做出反应,且必须是纯函数(仅依赖传入状态);状态更新成功后,策略切换到新 schema第 439-493 行
ClientCustomStrategy.clusterResharding(...)resharding的半成品版本,适用于只需观察集群信息即可重分片的场景,例如安全地增删容量第 364-411 行

文档建议:resharding的完整 API 与测试示例分别参考PartitioningStrategy.scala第 439 行起与 PartitionAwareClientEndtoEndTest.scala 第 395 行起的"with custom strategy, partitioning strategy dynamically changing"用例;clusterResharding参考PartitioningStrategy.scala第 364 行起与同一测试文件第 462 行起的"with cluster resharding, expanding cluster's instances"用例。下文以noResharding为例展开,因为它与其他两者共享全部基础理念。

非 fan-out:getPartitionIdAndRequest与分区 ID 的来源

Custom 策略要求实现getPartitionIdAndRequest:一个PartialFunction,输入 Thrift 请求,输出Future[Map(分区 Id -> Thrift 请求)]。非 fan-out 简化为"始终返回只含一个分区 Id 的 Map"。其类型别名在源码第 277 行:

type ToPartitionedMap = PartialFunction[ThriftStructIface, Future[Map[Int, ThriftStructIface]]]

分区 Id 是整数。文档明确指出其语义取决于服务调度方式:如果服务地址元数据由 ZooKeeper 支撑,则分区 Id 就是 ZooKeeper 宣告的shardId(仓库测试中通过ZkMetadata构造,见测试第 52-60 行);如果使用 Aurora 作为服务调度器,则分区 Id 与 Aurora job Id 相同。getPartitionIdAndRequest使用Future的原因是:分区数据本身可能要通过一次 RPC 调用获取,因此映射函数是异步的。

import com.twitter.delivery.thriftscala.AddrInfo import com.twitter.delivery.thriftscala.DeliveryService._ import com.twitter.finagle.thrift.exp.partitioning.ClientCustomStrategy import com.twitter.util.Future def lookUp(addrInfo: AddrInfo): Int = { addrInfo.name match { case "name1" | "name2" => 0 // partition 0 case "name3" => 1 // partition 1 } } val getPartitionIdAndRequest: ClientCustomStrategy.ToPartitionedMap = { case getBox: GetBox.Args => Future.value(Map(lookUp(getBox.addrInfo) -> getBox)) } val customPartitioningStrategy = ClientCustomStrategy.noResharding(getPartitionIdAndRequest)

与 Hashing 策略对称:PartialFunction允许一个策略服务同一 Thrift 服务的多个 endpoint;未定义的请求类型落入内置的defaultPartitionIdAndRequest(源码第 501-508 行,实现为直接返回一个携带PartitioningStrategyException的Future.exception),即"未指定 endpoint 不应被该客户端调用",否则抛出PartitioningStrategyException(该异常定义于ThriftPartitioningService,见 ThriftPartitioningService.scala)。MethodBuilder 场景下getPartitionIdAndRequest同样退化为普通函数。

逻辑分区映射:一个实例可属于多个分区

Custom 策略的另一大能力是逻辑分区:把一组 shard 归并到一个分区,同时允许一个 shard 同时出现在多个分区中。通过第二参数getLogicalPartition(Int => Seq[Int])描述"实例 Id -> 逻辑分区 Id 集合"的映射:

// group instances to logical partition // partition0 (instance 0 - 9), partition1(instance 0 - 19) partition3(instance 20 - 29) val getLogicalPartition: Int => Seq[Int] = { case a if Range(0, 10).contains(a) => Seq(0, 1) case b if Range(10, 20).contains(b) => Seq(1) case c if Range(20, 30).contains(c) => Seq(2) case _ => throw new Exception("out of index") } val customPartitioningStrategy = ClientCustomStrategy.noResharding(getPartitionIdAndRequest, getLogicalPartition)

注意示例中:实例 0-9 同时属于分区 0 和分区 1(Seq(0, 1)),实例 10-19 只属于分区 1,实例 20-29 属于分区 2——这正是"分区可以重叠、实例可以属于多个分区"的落地写法。若省略该参数,默认行为是每个实例自成一个分区(源码第 298 行:noResharding(getPartitionIdAndRequest, { a: Int => Seq(a) }))。分区 Id 由ZkMetadata的shardId派生(源码第 314-316 行注释)。

fan-out:ResponseMerger 与注册

在非 fan-out 基础上扩展,fan-out 的getPartitionIdAndRequest返回Future[Map(分区 Ids -> 子请求)],需要重建拆分后的 Thrift 请求:

val getPartitionIdAndRequest: ClientCustomStrategy.ToPartitionedMap = { case getBoxes: GetBoxes.Args => Future.value(getBoxes.listAddrInfo.groupBy(lookUp).map { case (partitionId, listAddrInfo) => partitionId -> GetBoxes.Args(listAddrInfo, getBoxes.passcode) }) } val customPartitioningStrategy = ClientCustomStrategy.noResharding(getPartitionIdAndRequest, getLogicalPartition)

fan-out 意味着客户端收到一组分区的响应,同样需要ResponseMerger分别处理成功与失败。Custom 策略只需注册ResponseMerger(请求拆分完全由用户控制,无需RequestMerger归并——注意这与 Hashing 策略必须注册两个 merger 不同)。注册通过responseMergerRegistry,多个ThriftMethod可级联:

import com.twitter.finagle.thrift.exp.partitioning.PartitioningStrategy.ResponseMerger val getBoxesRepMerger: ResponseMerger[Seq[Box]] = (successes, failures) => if (successes.nonEmpty) Return(successes.flatten) else Throw(failures.head) customPartitioningStrategy.responseMergerRegistry.add(GetBoxes, getBoxesRepMerger)

从源码可以看到,responseMergerRegistry是CustomPartitioningStrategytrait 的成员(PartitioningStrategy.scala第 180 行),因此所有 Custom 变体(含 resharding / clusterResharding)都天然携带它。

底层原理:分区服务如何路由请求

一致性哈希的实现

Hashing 策略底层的路由核心是ConsistentHashPartitioningService(见 ConsistentHashPartitioningService.scala)。它的工作流程可以概括为:

  1. HashRingNodeManager依据numReps参数把每个节点在哈希环上复制为多个虚拟节点(new HashRingNodeManager(underlying, params, numReps),第 53 行),节点组是动态的,一旦观察到组变化就重建哈希环;
  2. 每个请求先由子类提供getPartitionKeys取出哈希 key 集合,然后partitionRequest(第 78-99 行)按 key 分组:单个 key 直接路由;多个 key 先groupByPartition按"归属的服务"分组,同属一个分区的 key 合并为一个子请求,跨分区的 key 各自成请求;
  3. hashForKey使用keyHasher.hashKey(getKeyBytes(key))计算哈希(第 109-110 行),默认哈希器即KeyHasher.KETAMA;
  4. 当EjectFailedHost参数为真时,ConsistentHashingFailureAccrualFactory标记的不健康节点会被移出哈希环(注释见第 11-16 行)。

对应地,ThriftHashingPartitioningService在 Thrift 层负责把getHashingKeyAndRequest产出的"key -> 请求"Map 转换为底层ConsistentHashPartitioningService需要的 key 序列,并调用 merger 处理 fan-out 请求/响应。

参数即 Stack.Param

PartitioningParams的每个配置项(strategy、ejectFailedHost、keyHasher、numReps)最终都转化为Stack.Param注入客户端栈(见 Params.scala 与PartitioningParams.scala中的self.configured(...))。这意味着分区参数与其他 Finagle 栈参数(负载均衡、失败重试等)遵循同样的配置与传播机制,也可以通过Stack.Params直接组装。

动态重分片的可观测状态

ClientCustomStrategy的构造函数(源码第 662-679 行)持有observable: Activity[A]与两个纯函数A => ToPartitionedMap、A => Int => Seq[Int]。重分片发生时,策略通过PartitionNodeManager观察Activity的状态变化并切换 schema——这正是测试"with custom strategy, partitioning strategy dynamically changing"(第 395-460 行)所验证的行为:用一个Var(0)驱动的Activity[Int]作为状态,状态变化后新请求路由到新分区,且重分区前后负载均衡器(Balancer)数量保持不变。clusterResharding则把观察对象替换为集群地址集合Set[Address](第 364-411 行),测试"with cluster resharding, expanding cluster's instances"(第 462-528 行)演示了集群从 2 个实例扩到 5 个实例时逻辑分区映射随之改变、且实例 1 收到的请求数在重分片前后不变。

可观测性:partition 相关 Metrics

分区层 Metrics 位于clnt/<server_label>/partitioner/作用域下(详见 metrics/Partitioning.rst),用于观察客户端栈如何管理分区节点。

HashingPartitioningStrategy:

Metric类型含义
redistributescounter哈希环上节点被重新分布的次数
joinscounter新节点加入哈希环的次数,表示新分区加入集群(服务发现更新)
leavescounter节点离开哈希环的次数,表示服务发现检测到节点离开(服务发现更新)
ejectionscounter被ConsistentHashingFailureAccrual标记为不健康的节点被移出哈希环的次数(节点健康状态)
revivalscounter被剔除的节点重新在哈希环上标记为存活(节点健康状态)
live_nodesgauge当前健康分区总数
dead_nodesgauge当前被ConsistentHashingFailureAccrual标记为不健康的分区总数

其中leaves/joins反映服务发现更新,ejections/revivals反映节点健康状态——两类信号来源不同,排查问题时可以据此快速定位根因。

CustomPartitioningStrategy(ThriftMux):

Metric类型含义
nodesgauge当前逻辑分区总数

端到端测试验证

仓库中的 PartitionAwareClientEndtoEndTest.scala 是这套 API 的权威行为参考,覆盖了文档提及的几乎全部场景,可作为实现时的对照清单:

  • "without partition strategy"(第 144 行):无分区策略时,请求全部路由到同一节点,作为基线对照;
  • "with consistent hashing strategy"(第 164 行):验证相同哈希 key("one")的多个请求可落在同一节点并被getBoxesReqMerger合并;
  • "with consistent hashing strategy, unspecified endpoint returns error"(第 188 行):未指定 endpoint 调用时抛出NoPartitioningKeys;
  • "with errored hashing strategy"(第 203 行):路由函数抛异常时封装为PartitioningStrategyException;
  • "with custom partitioning strategy, each shard is a partition"(第 222 行):用服务器端口作为分区 Id,验证每个 shard 独立成分区;
  • "custom partitioning strategy, each shard is a partition, fanout the same request"(第 258 行):同一请求广播到 5 个分区,ResponseMerger合并出 15 条结果;
  • "with custom partitioning strategy, logical partition"(第 304 行):getLogicalPartition映射实例到逻辑分区,验证多实例归并与跨分区归属;
  • "with custom strategy, partitioning strategy dynamically changing"(第 395 行):resharding+Activity状态驱动动态重分片;
  • "with cluster resharding, expanding cluster's instances"(第 462 行):clusterResharding观察集群地址变化并安全扩缩容。

测试还揭示了一个实现要点:测试中地址通过ZkMetadata(Some(shardId))携带分区元数据(第 52-60 行),shardId即端口号——这印证了文档"分区 Id 来自 ZooKeeper 宣告的 shardId"的说明;自定义策略把lookUp的结果直接用作分区 Id(端口),从而把请求精确路由到对应测试服务器。

附录:MethodBuilder 自定义分区策略完整示例

文档附录给出了 MethodBuilder 层使用 Custom 策略的完整代码。注意:MethodBuilder 场景下getPartitionIdAndRequest是普通函数(不是PartialFunction),且一个策略只服务一个 endpoint:

def lookUp(addrInfo: AddrInfo): Int = { addrInfo.name match { case "name1" | "name2" => 0 // partition 0 case "name3" => 1 // partition 1 } } // group instances to logical partition // partition0 (instance 0 - 9), partition1(instance 0 - 19) partition3(instance 20 - 29) val getLogicalPartition: Int => Seq[Int] = { case a if Range(0, 10).contains(a) => Seq(0, 1) case b if Range(10, 20).contains(b) => Seq(1) case c if Range(20, 30).contains(c) => Seq(2) case _ => throw new Exception("out of index") } // response merger functions val getBoxesRepMerger: ResponseMerger[Seq[Box]] = (successes, failures) => if (successes.nonEmpty) Return(successes.flatten) else Throw(failures.head) val methodBuilderStrategy1 = new MethodBuilderCustomStrategy[GetBoxes.Args, Seq[Box]]( { getBoxes: GetBoxes.Args => val partitionIdAndRequest: Map[Int, GetBoxes.Args] = getBoxes.listAddrInfo.groupBy(lookUp).map { case (partitionId, listAddrInfo) => partitionId -> GetBoxes.Args(listAddrInfo, getBoxes.passcode) } Future.value(partitionIdAndRequest) }, getLogicalPartition, Some(getBoxesRepMerger) ) val methodBuilderStrategy2 = new MethodBuilderCustomStrategyGetBox.Args, Box -> getBox)) }, getLogicalPartition ) val builder = ThriftMux.client.methodBuilder(???) val getBoxesEndpoint = builder .withPartitioningStrategy(methodBuilderStrategy1) .servicePerEndpointDeliveryService.ServicePerEndpoint .getBoxes val getBoxEndpoint = builder .withPartitioningStrategy(methodBuilderStrategy2) .servicePerEndpointDeliveryService.ServicePerEndpoint .getBox

对应地,MethodBuilder 的 Hashing 策略使用MethodBuilderHashingStrategy[Req, Rep],其getHashingKeyAndRequest类型为Req => Map[Any, Req](PartitioningStrategy.scala第 249 行),且 request/response merger 以Option参数形式随构造传入(第 264-272 行),fan-out 场景只需在构造时提供Some(merger)。这套 API 的 Java 友好版本分别位于ClientHashingStrategy.create与ClientCustomStrategies(第 517-618 行),Java 用户无需手写PartialFunction即可使用。

总结

分区感知客户端把"按数据路由"从业务代码中抽象出来,落到 Finagle 客户端栈中:Hashing 策略以 Ketama 一致性哈希 + 可调虚拟节点数(numReps)、可选的失败主机剔除(ejectFailedHost)换取免运维的拓扑管理与弹性扩缩容;Custom 策略则以getPartitionIdAndRequest的Future化映射、逻辑分区映射与三种重分片模式(noResharding/resharding/clusterResharding)换取对拓扑的完全掌控。无论哪种策略,fan-out 场景都要求请求/响应"可合并",并通过RequestMerger/ResponseMerger注册表完成拆分与聚合。整套 API 目前处于实验阶段,实现前建议对照 PartitionAwareClientEndtoEndTest.scala 中的用例逐项验证行为,并通过clnt/<server_label>/partitioner/下的 Metrics 持续观测分区节点的健康与分布状态。

  • 后端
  • RPC框架

【免费下载链接】finagle

A fault tolerant, protocol-agnostic RPC system

项目地址:https://gitcode.com/gh_mirrors/fi/finagle
点击查看免费下载
上一篇:AFDropdownNotification高级技巧:重力动画与手势操作优化
下一篇:Ferret高级配置:自定义搜索工具、参数和显示选项

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询