高性能 RPC 框架 CloudWeGo-Kitex 内外统一的开源实践

Posted CloudWeGo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高性能 RPC 框架 CloudWeGo-Kitex 内外统一的开源实践相关的知识,希望对你有一定的参考价值。

日前,字节跳动技术社区 ByteTech 举办的第七期字节跳动技术沙龙圆满落幕,本期沙龙以《字节高性能开源微服务框架:CloudWeGo》为主题。在沙龙中,字节跳动字节跳动基础架构服务框架资深研发工程师杨芮,跟大家分享了《高性能 RPC 框架 Kitex 内外统一的开源实践》,本文根据分享整理而成。

本文将从以下四个方面介绍 CloudWeGo 高性能 RPC 框架 Kitex 的实践及开源:

1. 由内至外 - 开源过渡;

2. 开源一年变更回顾;

3. 社区共建完善生态及企业落地;

4. 总结和展望。

由内至外 - 开源过渡

很多同学可能刚刚了解 CloudWeGo,先介绍一下 CloudWeGo 和 Kitex 的关系。

CloudWeGo 和 Kitex

Kitex 是 CloudWeGo 开源的第一个微服务框架,它是一个支持多协议的 Golang RPC 框架,从网络库、序列化库到框架的实现基本完全自研的。特别地,Kitex 对 gRPC 协议的支持使用了 gRPC 官方的源码,但是我们对 gRPC 的实现做了深度且定制的优化,所以 Kitex 支持的 gRPC 协议性能优于 gRPC 官方框架。同时这也是 Kitex 与目前已经开源的、支持 gRPC 协议的其他 Golang 框架的主要差异。如果用户想使用 gRPC 又对性能有很高的要求,那么 Kitex 框架将会是一个很不错的选择。

继 Kitex 开源后,今年 CloudWeGo 又陆续开源了 Golang HTTP 框架 Hertz,Rust RPC 框架 Volo,同时围绕这些微服务框架和微服务的一些通用能力,我们还开源了一些高性能的基础库。关于更多 CloudWeGo 开源的子项目,可以进入 CloudWeGo 官网详细了解。

CloudWeGo 官网:https://www.cloudwego.io/

根据社区同学反馈,在一些开源群里大家会讨论 Kitex 会不会是一个字节跳动的开源 KPI 项目呢?它的稳定性、持续性能够得到保障吗?我可以负责任地讲,Kitex 不是一个 KPI 项目,它是来自字节跳动内部大规模实践的真实项目。在 Kitex 开源后始终保持内外统一,基于内外代码的统一我们保证了 Kitex 的持续迭代。为了进一步消除大家的顾虑,下面具体介绍一下 Kitex 的诞生和开源历程。

Kitex 发展历史

2014 年,字节跳动开始引入 Golang。2015 年,字节跳动内部的服务化开启。在 RPC 调用的场景选择了 Thrift 协议,在内部开始支持 RPC 框架。2016 年,第一个 Golang RPC 框架 Kite 正式发布。通常在一个公司高速发展的初期,基础能力都是为了快速支持需求落地,面对的需求场景也较单一,设计上不会有较多考量,其实这也是合理的,因为探索阶段并不完全清楚还需要支持哪些场景,过多的考虑反而会出现过度设计的问题。

但是,随着业务场景复杂化,需求也会多样化,而且接入服务及调用量逐年增长,Kite 已经不足以支持后续的迭代,在线上服役三年多后,2019 年我们开启了新的项目 Kitex,2020 年初发布了正式版本,在 2020 年底字节内部已经有 1w+ 服务接入 Kitex。

从 2014 年到 2020 年,Golang 已经是字节跳动内部主要的业务开发语言,应该是业界 Golang 应用最多的公司。我们的服务框架支持着数万个 Golang 微服务的可靠通信,经过数量众多的微服务和海量流量的验证,我们已经有了较为成熟的微服务最佳实践,于是考虑将内部的实践开源出去丰富云原生社区的 Golang 产品体系。在 2021年,我们以 CloudWeGo 品牌正式开源了第一个服务框架 Kitex。截至今年 8 月,Kitex 已经为字节跳动内部 6w+ 的服务提供支持,峰值 QPS 达到上亿级别

大家或许还有疑问,完整的微服务体系离不开基础的云生态,无论在公有云、私有云,都需要搭建额外的服务以很好地支持微服务的治理,比如治理平台、注册中心、配置中心、监控、链路跟踪、服务网格等,而且还存在一些定制的规范。字节跳动自然也有完善的内部服务支持微服务体系,但这些服务短期还无法开源,那 CloudWeGo 如何内外维护一套代码,统一迭代呢?

关于这个问题,我们看一下 Kitex 的模块划分。Kitex 的模块分为三个部分:中间是 Kitex 主干部分 Kitex Core,它定义了框架的层次结构、接口核心逻辑的实现以及接口的默认实现;左边的 Kitex Tool 则是与生成代码相关的实现,我们的生成代码工具就是编译这个包得到的,其中包括 IDL 的解析、校验、代码生成、插件支持等。不过为了便于用户使用同时提供更友好的扩展,主要能力也做了拆分作为基础库独立开源,如 Thriftgo、Thrift-validator 插件、Fastpb;右边的 Kitex Byted 是对字节内部基础能力集成的扩展实现,我们在开始就将内部的能力作为扩展收敛到一个 package 下。

如此,我们就可以将 Kitex Core 和 Tool 部分开源出去。我们将代码做了拆分,Kitex 的核心代码和工具部分迁移到开源库,集成内部扩展的模块作为 Kitex 的扩展保留在内部库,同时内部库封装一层壳保证内部用户可以无感知地升级。

那么 Kitex 的开源就只是代码拆分这么简单吗?显然不是。2021 年 2 月,我们开始筹备 Kitex 的开源,虽然基于 Kitex 的扩展性,我们可以与内部基础设施集成的能力解耦,但是 Kitex 仍然依赖内部的一些基础库,如果要开源必须先开源基础库的能力。所以我们首先做了依赖库的梳理,与相关的同学合作首先开源了 bytedance/gopkg 库。这个库由 CloudWeGo 与字节跳动的语言团队合作维护,里面包含也了对 Golang 标准库能力的增强,感兴趣的同学可以关注使用。

bytedance/gopkg: https://github.com/bytedance/gopkg

在 gopkg 库开源后,我们调整代码进行开源适配。2021 年 7 月,Kitex 正式开源,在内部发布中版本使用开源库。但 Kitex 毕竟支持了内部几万的微服务,我们必须要确保内部服务在这个变更后可以平滑过渡,所以在开源初我们没有对外官宣,在确认稳定性后,2021 年 9 月,Kitex 正式对外官宣开源

介绍了Kitex诞生、开源的历程,希望能够解除外部同学关于“Kitex 会不会是一个 KPI 项目?”的顾虑。

开源的价值

第一部分的最后,简单讲一下开源能为我们带来的价值。Kitex 不是为了开源而实现的,但它的实现是面向开源的。Kitex 本身是一个经过内部大规模实现的项目,我们希望 Kitex 开源后能帮助更多用户在内部快速搭建微服务,同时开源能让我们收集更多社区和企业的反馈,也能吸引外部开发者共建,促进 Kitex 面向多元场景支持的演进,丰富产品能力,然后能在更多场景和企业得到落地,这是一个正向循环,互利共赢的过程。

开源一年变更回顾

框架的衡量指标

在介绍 Kitex 开源一年变更前,先分享一下框架的衡量指标,这是大家在选择一个框架时要考虑的。

  • 扩展性

如果一个框架与内部能力强耦合,就无法移植到其他平台,或框架的支持场景单一也无法进行扩展,这样的框架很难得到外部的使用。

  • 易用性

框架的易用性体现在两个方面。第一是面向业务开发者,如果一个框架在使用过程中需要让用户关注很多框架的细节,那么对研发效率要求很高的团队可能无法接受。第二是面向框架的二次开发者,他们需要对框架做一些定制支持,如果框架提供的扩展能力过于宽泛,扩展成本很高,或者可扩展的能力不够多,那么这个框架也是存在局限性的。

  • 功能的丰富度

虽然基于扩展性可以对框架进行定制,但不是所有开发者都有足够的精力做定制开发,如果框架本身对各种扩展能力提供了不同选择的支持,对于开发者来说只需要根据自己的基础设施进行组合就能在自己的环境中运行。

  • 高性能

前面三点是初期选择框架需要重点关注的指标,但随着服务规模和资源消耗变大,性能就成了不容忽视的问题。从长期的角度来说,选择框架的时候一定要关注性能,否则后续只能面临框架替换的问题,或者被迫对这个框架做定制维护。

关于以上四点框架的衡量指标,虽然 Kitex 目前还没做到最好,但是这四个要素都是 Kitex 设计和实现中一直在兼顾的,我们不会顾此失彼。

功能特性

下面就几个开源一年来重要的功能特性进行介绍。

Proxyless

Proxyless 是 Kitex 面向开源场景提供的支持。在 Kitex 开源初期,我们内部讨论过是否要支持 xDS 对接 Istio,对于外部用户来说,使用 Istio 可以快速搭建一套基本的微服务架构,解决服务发现、流量路由、配置下发等问题,但是如果使用完整的 Istio 的解决方案,就要引入 Envoy,这会增加运维成本,而且直接使用官方的 Envoy 方案对性能有损,会引入额外的 CPU 开销且增加延迟。如果 Kitex 能直接对接 Istio,既能让用户享受到部分 Istio 的能力,又可以避免 Envoy 带来的性能损失和部署运维成本。但是在开源初期,我们没有看到很明确的用户诉求,因此没有对此做高优的支持。

后来 gRPC 官方也发布了 Proxyless 的支持,同时 Istio 的官方也将 Proxyless 作为使用 Istio 的一种方式。Kitex 现在也已完成支持,目前主要是对接服务发现,xDS 支持的扩展单独开源到了 kitex-contrib/xds 库中,后续还会完善。大家可以根据 README 了解如何使用 Kitex 对接 Istio。

xDS Support: https://github.com/kitex-contrib/xds

JSON 和 Protobuf 泛化调用支持

之前,Kitex 支持了应用在网关场景的 HTTP 泛化,以及支持了应用在一些通用服务场景的 Map 和二进制泛化。开源后,根据用户的需求反馈又新增了 JSON 和 Protobuf 的泛化。

Protobuf 的泛化也是应用在 API 网关的场景。原来的 HTTP 泛化传输的数据格式是 JSON,但是 JSON 的序列化体积大、效率低,对性能有影响,所以很多移动端的接口选择使用 Protobuf 传输数据,因此增加了 Protobuf 泛化的支持。

目前 Kitex 的泛化主要针对后端的 Thrift 服务,无论是 Protobuf、Map 还是 JSON,Kitex 都会在调用端结合 IDL 解析,将这些数据映射编码为 Thrift 包发给后端服务。

那么为什么把泛化放在调用端而不是服务端呢?大家广泛了解的泛化都是服务端对泛化请求做了解析处理,当然调用端也要相应地提供泛化的 Client。但是泛化面向的是通用服务,泛化使用成本其实是比较高的,它并不适用于普通的 RPC 场景,而通用服务面向的是所有后端的服务,有 Golang/Java/C++/Python/Rust,如果每一种语言框架都支持泛化,成本是非常高的。就算各个语言都对泛化做了支持,框架版本收敛又是一个漫长的过程,对于通用服务来说,对接所有的服务就显得不太现实。综合以上原因,泛化放在调用端支持。

重试能力增强

去年开源时,Kitex 已经支持了重试功能。之前支持的重试有两类,一个是超时重试,一个是 Backup Request。

对于超时来重试来说,我们只会对超时这一种异常进行重试,但为了进一步提高请求成功率,用户希望对其他的异常也进行重试,或者用户可能会定义一些用户请求的状态码,结合用户状态码进行重试,在这种情况下,显然我们只支持超时重试是不满足用户需求的。基于这个背景,Kitex 新增了指定结果重试,用户可以指定其他异常或指定某一类 Response,框架会结合用户指定的结果进行重试。

其次,用户在配置重试时,如果通过代码配置的方式设置重试,它会对整个 Client 的所有 RPC 方法生效,但是用户希望针对不同的 RPC 方法应用不同的重试策略,甚至同一个方法也希望可以采用不同的重试策略,因为不同链路上发起的同一个方法的请求对指标要求也会不同。比如有些想使用 Backup Request 减少延迟,有些想做异常重试提高成功率,对于这种情况,Kitex 新的版本支持了请求粒度配置重试

下图是使用示例。以请求粒度重试配置为例,比如 RPC 方法是 Mock,那么我们在发起 RPC 调用的时候,在后面可以配置一个 callopt 指定重试策略,此次请求就会使用这个重试策略。

Thrift Validator

Thrift-gen-validator 是 Thriftgo 的一个工具插件,它可以根据 Thrift IDL 中定义的注解描述约束给对应的 struct 生成 IsValid() error 方法,校验值的合法性。通常做 RPC 调用的时候,用户可能会对一些字段校验合法性,用户如果直接写这些校验代码,投入的成本会很高。所以我们就提供了注解支持,只要用户在 IDL 中根据规范定义注解,Kitex 就可以帮助用户生成校验代码

下图是代码生成的命令和一个 IDL 注解定义示例,在生成代码的时候指定 Thrift Validator 的插件,我们的插件工具就会解析注解,为用户生成这一套合法性校验的代码。目前我们也将 Thrift Validator 的功能贡献给了 Apache Thrift。

性能优化

介绍完几个重要的功能特性,再介绍几个在性能上的优化特性。

Thrift 高性能编解码

Frugal 是一个无需生成编解码代码、基于 JIT 的高性能动态 Thrift 编解码器。虽然我们针对官方 Thrift 编解码已经做了优化,支持了 FastThrift,这个在我们开源前发布的优化实践里也有介绍,但我们希望能有进一步的性能提升,参考我们开源的高性能 JSON 库 Sonic 的设计,实现了 Thrift JIT 编解码器。下图中的表格是 Frugal 结合 Kitex 与 FastThrift 的性能对比。

可以看到在大部分场景 RPC 性能表现都较优。除了性能上的优势,Frugal 还有另一个优势是无需生成编解码生成代码。Thrift 的生成代码比 Protobuf 繁重,一个复杂的 IDL 代码生成文件可以达到几万行,而这些代码本来对用户来说无需关注,却需要由用户来维护。Frugal 只需要生成结构体代码,不需生成编解码代码,就大大解决了这个问题。

关于如何在 Kitex 中使用 Frugal,可以参考仓库的 Readme。当然用户也可以单独使用 Frugal 作为 Thrift 高性能编解码器,Kitex 后续也会考虑默认使用 Frugal。

Frugal: https://github.com/cloudwego/frugal#readme

Protobuf 高性能编解码

虽然我们内部主要支持 Thrift,但开源之后我们发现外部用户对于 Protobuf 或 gRPC 的关注会更多,所以参考 Kitex FastThrift 的优化思路,重新实现了 Protobuf 的生成代码。在 v0.4.0 版本,如果用户使用 Kitex 的工具生成 Protobuf 的代码,就会默认生成 Fastpb 的编解码代码,在发起 RPC 调用的时候,Kitex 也会默认使用 Fastpb。

下图是 Fastpb 与官方 Protobuf 序列化的性能对比,可以看到无论是编码还是解码,在效率和内存分配上,Fastpb 都远远优于官方 Protobuf 序列化库。

gRPC 性能优化

开源初期,我们对 gRPC 整体稳定性和性能的关注是比较少的。因为内部使用的场景不是很多。开源后收到了很多外部同学的反馈,所以我们针对 gRPC 做了一个专项的问题治理以及性能优化。今年中旬我们已经把相关的优化正式提交到开源库,在 v0.4.0 版本发布。

Kitex v0.4.0: https://mp.weixin.qq.com/s/ezifbQkHcZQP6MygmJABYA

下图中左侧是优化前 Kitex-gRPC 和官方 gRPC 框架对 Unary 请求的压测吞吐对比,在并发比较低的情况下,Kitex 的吞吐并不具有优势,使用 Fastpb 的时候,Kitex 的吞吐表现会好一些,但低并发的吞吐依然低于官方 gRPC。在优化之后,吞吐对比如右图所示。相比优化前吞吐提升 46% - 70%,相比官方 gRPC 框架,吞吐高 51% - 70%

下图中右侧是优化后 Unary 请求的延迟对比,在吞吐比官方 gRPC 高出很多的情况下,Kitex 的延迟也显著低于官方的 gRPC。同时就 Kitex 自身而言,在优化后延迟表现也好了很多。

我们再看下 Streaming 请求的压测性能对比,优化前 Streaming 请求的表现同样在低并发的情况下,相对 gRPC 框架没有优势。经过优化后,Kitex 吞吐显著高于官方 gRPC,如下图,同时低并发下吞吐高但延迟持平,增加并发后能看到延迟出现分叉。所以在性能上, Kitex 支持的 gRPC 协议相对官方有明显的优势。

虽然在部分功能上,Kitex 还没有完全对齐,但是目前已经可以满足大部分的场景需求,我们后续也会继续进行功能对齐。

社区共建完善生态及企业落地

社区共建的 Kitex 扩展生态

开源后,我们很欣慰得到了很多开发者的关注,坦白说内部团队精力有限,无法快速建立起面向外部用户的 Kitex 扩展生态。但是一年以来借助社区的力量,Kitex 在服务注册/发现可观测性服务治理几部分的扩展得到了很多补充,尤其是服务注册/发现相关的扩展,目前开源的主流注册中心都已完成对接,虽然在功能丰富度上我们还有待加强,但结合已有的支持,对于外部用户已经具备了搭建微服务架构的能力。

衷心感谢积极参与 CloudWeGo 社区建设的同学们!关于 Kitex 相关的生态支持,大家可以进入 Kitex-contrib 了解更多的开源仓库。

Kitex-contrib: https://github.com/kitex-contrib

对接外部企业,协助落地

我们开源的初衷是为了助力其他外部企业快速地搭建企业级的云原生架构。开源后,森马、华兴证券、贪玩游戏、禾多科技先后主动与我们联系,反馈使用问题、提出需求,的确让我们发现了一些和内部场景不一样的问题,需要我们去关注、支持和优化,我们很开心 Kitex 能在这些企业内部得到应用。在今年 6 月 25 日的 CloudWeGo Meetup 中,森马和华兴证券的研发同学也分享了他们使用 Kitex 的内部实践。

森马:https://mp.weixin.qq.com/s/JAurW4P2E3NIduFaVY6jew

华兴证券:https://mp.weixin.qq.com/s/QqGdzp-7rTdlxedy6bsXiw

除了以上企业,还有一些公司也私下向我们咨询过使用问题,我们非常感谢这些企业用户的支持,以及向我们提出的反馈信息。如第一部分所讲,收集社区和企业的反馈可以促进 Kitex 面向多元场景支持的演进,企业用户如果有相关需求,欢迎联系我们。

如何使用 Kitex 与内部基础设施集成

这里再简单介绍下如何使用 Kitex 与大家的内部基础设施集成。以字节内部为例,内部仓库里有开源库中的扩展实现,集成内部的能力,在 bytedSuite 中,我们针对不同场景对 Kitex 进行初始化。如下面的代码示例,用户只需要在构造 Client 和 Server 时增加一个 option 配置就可以完成集成,不过为了让用户完全不需关注内部能力的集成,我们将该配置放在了生成的脚手架代码中,关于配置如何内嵌在生成代码中,后续我们也会开放出来,方便外部的框架二次开发者能以同样的方式为业务开发同学提供集成能力。

总结和展望

总结

本次分享主要介绍了以下内容:

  • Kitex 如何保持内外统一地从内部应用较广的框架转为开源框架;

  • 开源一年以来发布了哪些重要的功能特性,做了哪些性能优化;

  • 借助社区的力量现在 Kitex 的周边生态如何、企业落地情况以及如何使用 Kitex 优雅地集成内部能力。

展望

  • 与社区同学共建,持续丰富社区生态;

  • 结合工程实践,为微服务开发者提供更多便利;

  • 完善好 BDThrift 生态,持续优化 Protobuf/gRPC;

  • 更多特性支持或开源,ShmIPC、QUIC、Protobuf 泛化…

以上内容整理自第七期字节跳动技术沙龙《字节高性能开源微服务框架:CloudWeGo》,获取讲师 PPT 和回放视频,请关注 CloudWeGo 公众号,并在后台回复关键词“一周年”

项目地址

GitHub:https://github.com/cloudwego

官网:www.cloudwego.io

RPC理念,高性能RPC框架gRpc核心概念及示例

RPC理念

RPC(Remote Procedure Call)即远程过程调用,RPC框架的目标就是让远程服务调用更加简单,透明,RPC框架负责屏蔽底层的传输方式(TCP或UDP),序列化方式(XML/JSON/二进制)和通信细节,服务调用者可以像调用本地接口一样调用远程服务的提供者,而不必关心底层通信细节和调用过程,RPC调用的基本原理图:

图注:RPC调用基本原理

一次完整的RPC调用流程(同步调用,异步另说)如下:
1)服务消费方(client)调用以本地调用方式调用服务;
2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3)client stub找到服务地址,并将消息发送到服务端;
4)server stub收到消息后进行解码;
5)server stub根据解码结果调用本地的服务;
6)本地服务执行并将结果返回给server stub;
7)server stub将返回结果打包成消息并发送至消费方;
8)client stub接收到消息,并进行解码;
9)服务消费方得到最终结果。

RPC框架的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

目前在RPC领域可谓百花齐放,各大厂均有重复造轮子的迹象,业界主流的RPC框架主要分三类:

  1. 只支持特定语言的RPC框架,例如新浪微博的Moton

  2. 支持多种语言RPC框架,如Google的gRpc,Facebook的Thrift

  3. 支持服务治理等服务化特性的分布式服务框架,底层仍然是RPC框架,如阿里的Dubbo,华为的ServiceComb,目前这两项目均在Apache的孵化器中孵化

gRpc简介

gRPC  是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用,gRpc采用Google内部长期使用,经大量生产流量验证的protobuf序列化,具有很高的性能,有了 gRPC, 我们可以一次性的在一个 .proto 文件中定义服务并使用任何支持它的语言去实现客户端和服务器,反过来,它们可以在各种环境中,从Google的服务器到你自己的平板电脑—— gRPC 帮你解决了不同语言及环境间通信的复杂性。使用 protocol buffers 还能获得其他好处,包括高效的序列号,简单的 IDL 以及容易进行接口更新

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

RPC理念,高性能RPC框架gRpc核心概念及示例
图注:gRpc调用原理

gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里

另外在学习gRpc是强烈建议掌握的知识点:
  • JavaNIO

  • Netty

  • Google protobuff

  • Http2协议

  • Reactor模式

  • 多线程

gRpc核心概念

注:本文列举的概念并不是全部的gRpc概念,如需了解更多,请参阅其官网
gRPc官网文档:https://grpc.io/docs/guides/

定义服务

正如其他 RPC 系统,gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言(如果尚不熟悉protobuff,可以先参考Google protobuff, protocol-buffers文档:https://developers.google.com/protocol-buffers/),来描述服务接口和有效载荷消息结构。如果有需要的话,可以使用其他替代方案。

service HelloService {
 rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
 required string greeting = 1;
}
message HelloResponse {
 required string reply = 1;
}

gRPC 允许你定义四类服务方法:

  • 单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

rpc SayHello(HelloRequest) returns (HelloResponse){
}
  • 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
  • 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
  • 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

使用 API 接口

gRPC 提供 protocol buffer 编译插件,能够从一个服务定义的 .proto 文件生成客户端和服务端代码。通常 gRPC 用户可以在服务端实现这些API,并从客户端调用它们。

  • 在服务侧,服务端实现服务接口,运行一个 gRPC 服务器来处理客户端调用。gRPC 底层架构会解码传入的请求,执行服务方法,编码服务应答。

  • 在客户侧,客户端有一个存根实现了服务端同样的方法。客户端可以在本地存根调用这些方法,用合适的 protocol buffer 消息类型封装这些参数— gRPC 来负责发送请求给服务端并返回服务端 protocol buffer 响应。

RPC 生命周期

现在让我们来仔细了解一下当 gRPC 客户端调用 gRPC 服务端的方法时到底发生了什么

单向RPC

首先我们来了解一下最简单的 RPC 形式:客户端发出单个请求,获得单个响应。

  • 一旦客户端通过桩调用一个方法,服务端会得到相关通知 ,通知包括客户端的元数据,方法名,允许的响应期限(如果可以的话)

  • 服务端既可以在任何响应之前直接发送回初始的元数据,也可以等待客户端的请求信息,到底哪个先发生,取决于具体的应用。

  • 一旦服务端获得客户端的请求信息,就会做所需的任何工作来创建或组装对应的响应。如果成功的话,这个响应会和包含状态码以及可选的状态信息等状态明细及可选的追踪信息返回给客户端 。
    假如状态是 OK 的话,客户端会得到应答,这将结束客户端的调用。

服务端流式 RPC

服务端流式 RPC 除了在得到客户端请求信息后发送回一个应答流之外,与我们的简单例子一样。在发送完所有应答后,服务端的状态详情(状态码和可选的状态信息)和可选的跟踪元数据被发送回客户端,以此来完成服务端的工作。客户端在接收到所有服务端的应答后也完成了工作。

客户端流式 RPC

客户端流式 RPC 也基本与我们的简单例子一样,区别在于客户端通过发送一个请求流给服务端,取代了原先发送的单个请求。服务端通常(但并不必须)会在接收到客户端所有的请求后发送回一个应答,其中附带有它的状态详情和可选的跟踪数据。

双向流式 RPC

双向流式 RPC ,调用由客户端调用方法来初始化,而服务端则接收到客户端的元数据,方法名和截止时间。服务端可以选择发送回它的初始元数据或等待客户端发送请求。
下一步怎样发展取决于应用,因为客户端和服务端能在任意顺序上读写 - 这些流的操作是完全独立的。例如服务端可以一直等直到它接收到所有客户端的消息才写应答,或者服务端和客户端可以像"乒乓球"一样:服务端后得到一个请求就回送一个应答,接着客户端根据应答来发送另一个请求,以此类推。

gRpc示例

本示例模拟了日常开发中,不同服务间的间的调用,具有很强的实战性,项目代码已上传至githug,如有需要clone到本地及可运行

RPC理念,高性能RPC框架gRpc核心概念及示例

grpc-api:存放由.proto文件生产的服务接口及实体类,因为客户端及服务端均需要此模块,故将其抽取成公共模块(此处注意:proto编译器能生成我们需要的文件,但每次执行命令是比较烦人的事情,maven中有专门生产protobuff文件的插件,根据.proto文件生成需要的服务接口及实体类的工程我已上传到github,如需要自取: https://github.com/JavaDebugMan/Protobuf)
grpc-commom:公共模块
grpc-task:客户端模块
grpc-user:服务端模块

服务端

服务端的工作有两个:
1.实现我们服务定义的生成的服务接口:做我们的服务的实际的“工作”。
2.运行一个 gRPC 服务器,监听来自客户端的请求并返回服务的响应。

实现 GreeterGrpc.GreeterImplBase

    static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
       @Override
       public void sayHelloAgain(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
           HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + request.getName()).build();
           responseObserver.onNext(reply);
           responseObserver.onCompleted();
       }
       @Override
       public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
           HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
           responseObserver.onNext(reply);
           responseObserver.onCompleted();
       }
   }

简单rpc

    private Server server;
   private void start() throws IOException {
       /* The port on which the server should run */
       int port = 50051;
       server = ServerBuilder.forPort(port)
               .addService(new GreeterImpl())
               .build()
               .start();
       logger.info("Server started, listening on " + port);
       Runtime.getRuntime().addShutdownHook(new Thread() {
           @Override
           public void run() {
               // Use stderr here since the logger may have been reset by its JVM shutdown hook.
               System.err.println("*** shutting down gRPC server since JVM is shutting down");
               HelloWorldServer.this.stop();
               System.err.println("*** server shut down");
           }
       });
   }
   private void stop() {
       if (server != null) {
           server.shutdown();
       }
   }
   /**
    * Await termination on the main thread since the grpc library uses daemon threads.
    */

   private void blockUntilShutdown() throws InterruptedException {
       if (server != null) {
           server.awaitTermination();
       }
   }
   /**
    * Main launches the server from the command line.
    */

   public static void main(String[] args) throws IOException, InterruptedException {
       final HelloWorldServer server = new HelloWorldServer();
       server.start();
       server.blockUntilShutdown();
   }

客户端

创建存根

为了调用服务方法,我们需要首先创建一个 存根,或者两个存根:

channel = NettyChannelBuilder.forAddress(host, port)
       .negotiationType(NegotiationType.PLAINTEXT)
       .build();

如你所见,我们用一个 NettyServerBuilder 构建和启动服务器。这个服务器的生成器基于 Netty 传输框架。

我们使用 Netty 传输框架,所以我们用一个 NettyServerBuilder 启动服务器。

调用服务方法

 private final ManagedChannel channel;
   private final GreeterGrpc.GreeterBlockingStub blockingStub;
   /**
    * Construct client connecting to HelloWorld server at {@code host:port}.
    */

   public HelloWorldClient(String host, int port) {
       this(ManagedChannelBuilder.forAddress(host, port)
               // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
               // needing certificates.
               .usePlaintext(true)
               .build());
   }
   /**
    * Construct client for accessing RouteGuide server using the existing channel.
    */

   HelloWorldClient(ManagedChannel channel) {
       this.channel = channel;
       blockingStub = GreeterGrpc.newBlockingStub(channel);
   }
   public void shutdown() throws InterruptedException {
       channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
   }
   /**
    * Say hello to server.
    */

   public void greet(String name) {
       logger.info("Will try to greet " + name + " ...");
       HelloRequest request = HelloRequest.newBuilder().setName(name).build();
       HelloReply response;
       try {
           response = blockingStub.sayHello(request);
       } catch (StatusRuntimeException e) {
           logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
           return;
       }
       logger.info("Greeting: " + response.getMessage());
       try {
           response = blockingStub.sayHelloAgain(request);
       } catch (StatusRuntimeException e) {
           logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
           return;
       }
       logger.info("Greeting: " + response.getMessage());
   }
   /**
    * Greet server. If provided, the first element of {@code args} is the name to use in the
    * greeting.
    */

   public static void main(String[] args) throws Exception {
       HelloWorldClient client = new HelloWorldClient("localhost", 50051);
       try {
           /* Access a service running on the local machine on port 50051 */
           String user = "world";
           if (args.length > 0) {
               user = args[0]; /* Use the arg as the name to greet if provided */
           }
           client.greet(user);
       } finally {
           client.shutdown();
       }
   }


运行服务端及客户端

服务端:


RPC理念,高性能RPC框架gRpc核心概念及示例

可以发现服务端已在50051端口监听客户端连接


客户端:

运行客户端已答应出我们希望出现的Hello world


关于gRpc的服务注册与发现及负载均衡

gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展

我们完全可以使用etcd,consul等注册中心实现gRpc服务的注册于发现及 负载均衡


gRpc与Srpingboot的集成

https://github.com/LogNet/grpc-spring-boot-starter

以上是关于高性能 RPC 框架 CloudWeGo-Kitex 内外统一的开源实践的主要内容,如果未能解决你的问题,请参考以下文章

gRPC 1.11.0 发布,Google 高性能 RPC 框架

REST RPC:简单易用高性能的开源RPC框架

自己动手写RPC框架有那么难吗?这次我设计了一款TPS百万级别的分布式高性能可扩展的RPC框架(赶快收藏)

自己动手写RPC框架有那么难吗?这次我设计了一款TPS百万级别的分布式高性能可扩展的RPC框架(赶快收藏)

腾讯高性能 RPC 开发框架

GRPC 1.15.0 发布,Google 高性能 RPC 框架