Closed namelessssssssssss closed 1 month ago
Hi @namelessssssssssss, welcome to SOFAStack community, Please sign Contributor License Agreement!
After you signed CLA, we will automatically sync the status of this pull request in 3 minutes.
@EvenLjj PTAL :)
Attention: Patch coverage is 83.66013%
with 50 lines
in your changes are missing coverage. Please review.
Project coverage is 72.19%. Comparing base (
e67ea54
) to head (df27427
). Report is 8 commits behind head on master.:exclamation: Current head df27427 differs from pull request most recent head c7435df. Consider uploading reports for the commit c7435df to get more accurate results
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
This is interesting 😀
更好的方案是使用自定义的 StreamObserver(此处先叫做StreamHandler),将其和gRPC解耦,在 Triple 传输层通过适配器转换为gRPC StreamObserver。此处使用自定义的StreamHandler。
确实是这样的, 因此方案中还需要考虑如何让 bolt 协议来支持流式处理. 我能想到两种方案:
我个人比较希望采用第一种方案,让bolt 原生支持 stream ,这能让使用bolt 的其他框架,也能享受到 stream 的优势.
回到这个 PR 上, 我建议统筹考虑 bolt/sofarpc 应该如何支持 stream ,再来合并这个PR.
The updates involve enriching RPC functionalities with added support for various streaming types (unary, client, server, and bidirectional). Changes include adjusting method signatures to accommodate streaming, introducing new utilities for handling streaming and exceptions, and expanding test cases to ensure robustness. This overhaul not only streamlines method invocations but also extends the RPC framework's capabilities to effectively handle complex streaming scenarios.
Files | Change Summary |
---|---|
RpcConstants.java , AbstractInterfaceConfig.java , ConsumerConfig.java , ProviderConfig.java |
Added constants for new streaming types, caching method call types, and updated method configurations. |
MethodConfig.java , AbstractCluster.java , TripleClientInvoker.java |
Enhanced handling of different streaming scenarios, updated timeout behaviors, and improved error handling. |
MessageBuilder.java , StreamHandler.java , SofaStreamObserver.java |
Introduced new methods for message handling and stream observation. |
ConsumerGenericFilter.java , UniqueIdInvoker.java , TripleServer.java |
Updated method signatures to accept SofaRequest , improved method identification, and enhanced stream handling. |
GenericServiceImpl.java , SofaProtoUtils.java |
Expanded service functionalities, added utility methods for streaming, and updated method mapping utilities. |
transformer.proto , helloworld.proto |
Added new RPC methods supporting various streaming types. |
HelloService.java , HelloServiceImpl.java , ServerResponse.java , ClientRequest.java , ExtendClientRequest.java |
Defined new interfaces and classes for handling streaming in test scenarios. |
TripleGenericStreamTest.java , TripleStubStreamTest.java |
Introduced comprehensive test cases for bi-directional and server streaming. |
🐇✨📜 O hark! New streams do flow through code, With swift, smooth currents, they now bode. From unary to bidirectional streams, Through SOFA's veins, the data gleams. A rabbit’s cheer for the tech brigade, For each clever tweak and upgrade made! 🌟🐾
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?
Motivation:
This PR can provides stream transport support for triple protocol, including server streaming and bidirectional/client streaming.
Modification:
Change log (Chinese) :
概述
Stream 方式是一种异步的流式处理方式,可以在数据传输过程中逐个处理数据,避免一次性传输大量数据造成的性能问题。服务端Stream 是指服务端在处理请求时,将数据分成多个部分逐个返回给客户端的过程;客户端 Stream 是指客户端在请求服务器时,将请求参数分成多个部分逐个发送给服务器的过程。Stream 方式可以让我们在处理大量数据时更高效地使用资源,提高系统的性能和响应速度。
业务场景
流式传输适配下列业务场景:
实现的流传输应具有以下语义保证:
流传输模型
对于流式传输,我们可以将Stream分为四种类型 (借鉴于gRPC):
Unary,传统请求-响应模型:客户端一次请求,服务端一次响应
ClientStream,客户端流:客户端多次请求,服务端最终返回一次响应,服务端接收请求期间可以返回已完成/取消信号来停止客户端继续发送请求,或者在客户端请求完成后返回响应。
ServerStream,服务端流:客户端单次请求,服务端返回多次响应,客户端接收请求期间可以返回已完成/取消信号来停止接受服务端的响应,或者等待服务端发送完成相应信号。
BidirectionalStream,双向流:客户端发送一次请求后,客户端-服务端均可相互多次乱序发送请求,直到任意一方发送已完成/取消信号。
流传输接口定义
我们先从RPC调用的最上层开始分析。首先,我们需要定义客户端/服务端用于接收/发送流式请求的接口,该接口定义了如何发送、接收、处理数据流,以及数据流的异常、结束操作。
考虑到目前传输层的实现,此处可以考虑直接使用 gRPC 中的 StreamObserver,这样对于 Triple 协议的传输层实现来说比较方便(TripleClientInvoker 直接依赖 gRPC 进行传输),但缺点是可能导致其它未来可能支持流传输的协议同时耦合了gRPC的API。
更好的方案是使用自定义的 StreamObserver(此处先叫做StreamHandler),将其和gRPC解耦,在 Triple 传输层通过适配器转换为gRPC StreamObserver。此处使用自定义的StreamHandler。
我们为该接口定义以下方法,实际和gRPC中的StreamObserver一致:
全局处理
1,标记请求类型
首先是识别方法调用类型,并为 SofaRequest 打上流传输的标记。
对于客户端来说,SofaRequest在客户端代理中创建。其中标记传输类型的字段在DefaultClientProxyInvoker中的 decorateRequest方法中设置(对于泛型调用)。
setInvokeType方法最终调用ConsumerConfig中的getMethodInvokeType,尝试从接口配置中获取已缓存的调用方式,否则使用默认值(一元调用)。可以将判断调用类型的操作添加在其中的 getMethodInvokeType 中,这样也可以将方法调用模式缓存,防止每次调用都要进行繁琐的判断操作。
修改后的方法:
主要完成以下三个操作:
此处将调用方式缓存到了MethodConfig中,因为InterfaceConfig使用的是UnmodifiableMap,在其初始化完成后已不能再添加新的信息。由于 MethodConfig 本身没有保存 Method 引用,且不会默认创建,使得获取方法参数及返回值的具体类型有些困难,需要在实际请求时通过 SofaRequest 拿到具体的参数 Class,才可较简单的判断方法参数及返回值中StreamHandler的出现情况,判断请求类型。
2,修改AbstractCluster.doSendMsg
在该方法中通过获取SofaRequest的RequestType,判断是使用同步、异步、回调还是其它调用方法。对于流式调用,需要在其中添加新请求类型的处理逻辑。
根据流式传输方法的定义,如果客户端希望通过Stream向服务端再次发送信息,它需要通过调用RPC方法返回的StreamHandler中的onMessage方法来向服务端发送下一条信息。
消费者对流传输方法的第一次调用实际是向提供者注册传输会话,且需要依赖返回的StreamHandler向提供者发送后续信息(且需要确保StreamHandler已初始化完毕,保证消费者直接使用其发送消息不会出错),此处选择同步调用。
协议处理
Triple协议
SofaRPC 的 Triple 协议传输层实现目前直接依赖 gRPC 进行。
泛型调用
当 TripleClientInvoker 中 useGeneric 字段为 false 时,表示消费者调用的服务存在IDL及对应的Stub,可以直接通过生成的Stub进行调用。当 useGeneric 为 true 时,将在运行时动态指定调用的服务及方法,借助底层的泛型服务完成传输。
因此,Triple协议的修改分为两个方面:非泛型调用时,通过服务接口生成的stub进行流式调用;以及泛型调用时,通过预生成的GenericService stub发起流式调用。
IDL修改
对于泛型调用,Triple协议通过将已序列化完成的请求统一封装为 Request,通过预生成的gRPC stub完成传输过程。
因此需要修改transformer.proto, 添加流式调用方法的定义。 考虑客户端流和双向流使用可能使用相同的调用方法,此处没有单独定义客户端流的传输方法。
之后,服务端实现新的泛型服务方法
以上实现的主要难点在于,流调用方法传入及返回的均为StreamHandler,这意味着在实际请求到来之前无法得知具体的请求类型。因此以上的实现中,序列化及方法参数信息是在接收到第一次请求之后才被初始化的。
Triple传输层修改
*由于实际上三种流传输的底层流程差别不大,以下的修改示例均为双向流(BidirectionalStream)。
客户端
以下为对TripleClientInvoker相关的修改。
重构invoke方法,根据callType区分为三种调用流程
然后,根据具体调用方式选择具体调用逻辑
实现双向流传输
为MethodDescriptor设置新的调用类型
添加工具方法:
服务端
方法绑定问题
对于服务端,泛型调用的入口是 GenericService 的实现,即 GenericSerivceImpl。
在为泛型服务的IDL中添加流式方法的定义并重新编译后,发现一元调用时(UNARY)不能正确的选择GenericServiceImple 的 generic 方法,而选择调用了 genericBiStream 方法。
断点调试可以发现,底层 ServerMethodDefinition 和 CallHandler 的绑定错误,使得方法选择了ID错误的CallHandler,从而调用了错误的方法。ServerMethodDefinition 在 ServerImpl.runInternal() 中通过 registry.lookupMethod(methodName)获取。实际调用的是 MutableHandlerRegistry,通过其中的 services 获取 ServerServiceDefinition。而 service 中的 ServerServiceDefinition 由 TripleServer.registerProcessor 放入,自此回到了 SofaRPC 自己的实现。
分析数据流,可以发现将客户端实际服务方法和泛型服务方法绑定起来的操作发生在TripleServer.buildSofaServiceDef方法中。该流程实际在服务提供者启动,导出服务接口时进行。
修改后的实现:
这样 ServerMethodDefinition 就和 callHandler 正确的绑定起来了。
方法定位问题
以上方法传入的 ProviderConfig 提供了实际服务接口的配置信息,包括这些方法的调用方式。但是调用方式默认被设置为UNARY。在上层需要附加一部分根据接口方法参数判断调用类型的逻辑。
修改后:
在接口配置(AbstractInterfaceConfig)中提供了按Class匹配并缓存方法调用模式的方法:
*写在 AbstractInterfaceConfig 是考虑消费者是否能复用这部分逻辑
在 ProviderConfig 初始化时,设置服务引用时会尝试匹配服务方法的具体调用方式,并缓存:
流信息序列化问题
对于双向流和客户端流,接口声明时的入参和返回值均仅为 StreamHandler。这导致无论是客户端请求和服务端响应都无法在调用开始就得知具体的消息类型。因此获取、缓存具体消息类型、序列化器的操作需要延迟在客户端/服务端第一次得到具体请求时再进行。
客户端向服务端发送消息使用的StreamHandler适配器,TripleClientInvoker中的匿名实现,将请求序列化为Request:
客户端处理服务端响应使用的StreamHandler适配器,将Response反序列化为指定类型:
服务端发送响应时使用的StreamHandler适配器,将原始响应序列化为Response:
服务端处理客户端请求使用的StreamHandler,TripleClientInvoker 中的匿名实现,将请求反序列化为指定类型:
Summary by CodeRabbit
New Features
GenericService
.Bug Fixes
Documentation
Refactor
Tests
Chores