字节微服务框架的挑战与演进
2014 年以来,字节跳动内部业务的快速发展,推动了长连接推送服务,它们面临着高并发的业务需求问题,对性能和开发效率都有很高要求。当时的业务,大部分都是由 Python 开发,难以应对新出现的问题。项目负责人在一众现存的技术栈中选择了 Golang 这一门新兴的编程语言,快速解决了性能和开发效率的问题。随后,字节跳动内部开始逐渐推广使用 Golang 进行服务开发。
2016 年, 第一代 Golang RPC 框架 Kite 正式发布。Kite 是一个基于 Apache Thrift 进行包装的 RPC 框架,它在 Facebook 开源的 Thrift 之上提供了结合字节跳动内部基础设施的治理功能,同时还提供了一套简单易用的生成工具。随着 Kite 的发展,业务开始大规模使用 Golang。然而,在业务发展的过程中,由于研发专注于实现业务需求,对于框架的可维护性考量不足,Kite 逐渐背上了一些技术包袱,越来越难以满足业务在高性能和新特性方面的需求。因此我们决定对 Kite 进行重新设计,于是出现了 Kitex。
2020 年,Kitex 在内部发布了 v1.0.0,并且直接接入了 1,000+ 服务。由于 Kitex 的优秀性能和易用性,Kitex 在内部得到了大规模发展。直到 2021 年年中,字节跳动内部已有 2w+ 服务使用了 Kitex。因此,我们决定全面优化 Kitex,将其实践成果进行开源,反馈给开源社区。
字节跳动 Golang RPC 框架的演进
Kite 的缺陷
Kite 作为字节跳动第一代 Golang RPC 框架,主要存在以下缺陷:
-
Kite 为了快速支持业务发展需求,不可避免地耦合了部分中台业务的功能;
-
Kite 对 Go modules 支持不友好(Go modules 在 2019 年才进入语言核心);
-
Kite 自身的代码拆分成多仓库,版本更新时推动业务升级困难;
-
Kite 强耦合了早期版本的 Apache Thrift,协议和功能拓展困难;
-
Kite 的生成代码逻辑与框架接口强耦合,成为了性能优化的天花板。
因此,业务的快速发展和需求场景的多样化,催生了新一代 Golang RPC 框架 Kitex。
Kitex
Kitex 的架构主要包括四个部分:Kitex Tool、Kitex Core、Kitex Byted、Second Party Pkg。
-
Kitex Core 是一个携带了一套微服务治理功能的 RPC 框架,它是 Kitex 的核心部分。
-
Kitex Byted 是一套结合了字节跳动内部基础设施的拓展集合。通过这一套拓展集合,Kitex 能够在内部支持业务的发展。
-
Kitex Tool 是一个命令行工具,能够在命令行生成我们的代码以及服务的脚手架,可以提供非常便捷的开发体验。
-
Second Party Pkg,例如 netpoll, netpoll-http2,是 Kitex 底层的网络库,这两个库也开源在 CloudWeGo 组织中。
Kitex 的架构设计
总的来说, Kitex 主要有五个特点:面向开源、功能丰富、灵活可拓展、支持多协议、高性能。
面向开源
由于之前已经体验过了 Kite 维护的各种问题,我们在立项之初就考虑到了未来可能会开源 Kitex。因此,我们设计的第一个宗旨就是不将 Kitex 和公司内部的基础设施进行强耦合或者硬编码绑定。Kitex Core 是一个非常简洁的框架,公司内部的所有基础设施都以拓展的方式注入到 Kitex Core 里。即使我们现在已经开源了,它也以这种形式存在。公司内部基础设施的更新换代,和 Kitex 自身的迭代是相互独立的,这对于业务来说是非常好的体验。同时,在 Kitex 的接口设计上,我们使用了 Golang 经典的 Option 模式,它是可变参数,通过 Option 能够提供各种各样的功能,这为我们的开发和业务的使用都带来了非常大的灵活性。
Kitex 的功能特性
治理能力
Kitex 内置了丰富的服务治理能力,例如超时熔断、重试、负载均衡、泛化调用、数据透传等功能。业务或者外部的用户使用 Kitex 都是可以开箱即用的。如果你有非常特殊的需求,你也可以通过我们的注入点去进行定制化操作,比如你可以自定义中间件去过滤或者拦截请求,定义跟踪器去注入日志、去注入服务发现等。在 Kitex 中,几乎一切与策略相关的东西都是可以定制的。
以服务发现为例,Kitex 的核心库里定义了一个 Resolver interface 。任何一个实现了这四个方法的类型都可以作为一个服务发现的组件,然后注入到 Kitex 来取代 Kitex 的服务发现功能。在使用时,客户端只需要创建一个 Resolver 的对象,然后通过 client.WithResolver 注入客户端,就可以使用自己开发的服务发现组件。
Kitex 的一个创新之处是使用 Suite 来打包自定义的功能,提供一键配置基础依赖的体验。
它能在什么地方起作用呢?例如,一个外部企业想要启用或者接入 Kitex, 它不可能拥有字节跳动内部的所有基础设施。那么企业在使用的时候肯定需要定制化,他可能需要定义自己的注册中心、负载均衡、连接池等等。如果业务方要使用这些功能的话,就需要加入非常非常多的参数。而 Suite 可以通过一个简单的类一次性包装这些功能,由此,业务方使用时,仍然是以单一的参数的方式添加,十分方便。又例如,我现在开发一个叫 mysuite 的东西,我可能提供一个特殊的服务发现功能,提供了一个拦截的中间件,还有负载均衡功能等。业务方使用时,不需要感知很多东西去配置,只需要添加一个 suite 就足够了,这点非常方便一些中台方或者第三方去做定制。
示例
多协议
Kitex 网络层基于高性能网络库 Netpoll 实现。在 Netpoll 上,我们构建了 Thrift 和 netpoll-http2;在 Thrift 上,我们还做了一些特殊的定制,例如,支持 Thrift 的泛化调用,还有基于 Thrift 的连接多路复用。
多协议
代码生成工具
和 Kitex 一同出现的,还有我们开发的一个简单易用的命令行工具。如果我们写了一个 IDL, 只需要提供一个 module 参数和一个服务名称,Kitex 就会为你生成服务代码脚手架。
目前 Kitex 支持了 Protobuf 和 Thrift 这两种 IDL 的定义。命令行工具内置丰富的选项,可以进行项目代码定制;同时,它底层依赖 Protobuf 官方的编译器,和我们自研的 Thriftgo 的编译器,两者都支持自定义的生成代码插件。
Kitex 的性能表现
字节跳动内部 RPC 框架使用的协议主要都是基于 Thrift,所以我们在 Thrift 上深耕已久。结合自研的 netpoll 能力,它可以直接暴露底层连接的 buffer。在此基础上,我们设计出了 FastRead/FastWrite 编解码实现,测试发现它具有远超过 apache thrift 生成代码的性能。整体而言,Kitex 的性能相当不错,今年 1 月份的数据如下图所示,可以看到,Kitex 在使用 Thrift 作为 Payload 的情况下,性能优于官方 gRPC,吞吐接近 gRPC 的两倍;此外,在 Kitex 使用定制的 Protobuf 协议时,性能也优于 gRPC。
Kitex/gRPC 性能对比(2022 年 1 月数据)
Kitex:一个 demo
下面简单演示一下 Kitex 是如何开发一个服务的。
首先,定义 IDL。这里使用 Thrift 作为 IDL 的定义,编写一个名为 Demo 的 service。方法 Test 的参数是 String,它的返回也是 String。编写完这个 demo.thrift 文件之后,就可以使用 Kitex 在命令行生成指定的生成代码。如图所示,只需要传入 module name,service name 和目标 IDL 就行了。
定义 IDL
随后,我们需要填充业务逻辑。文件中除了第 12 行,全部代码都是 Kitex 命令行工具生成的。通常一个 RPC 方法需要返回一个 Response,例如这里需要返回一个字符串,那么我们给 Response 赋值即可。接下来需要通过 go mod tidy 把依赖拉下来,然后用 build.sh 构建,就可以启动服务了。Kitex 默认的接听端口是 8888。
定义 Handler 方法
编译、运行
对于刚刚启动的服务端,我们可以写一个简单的客户端去调用它。服务端写完之后,写客户端也是非常方便的。这里同样是 import 刚刚生成的生成代码,创建 Client、指定服务名字、构成相应的参数,填上“Hello,word!” ,然后就可以调用了。
编写 Client
Kitex 在字节内部的落地
与内部基础设施的集成
谈到落地,第一步就是 Kitex 和字节跳动内部的基础设施进行结合。字节跳动内部的所有基础设施都是以依赖的方式注入到 Kitex 的。我们将日志、监控、tracing 都定义为 tracer,然后通过 WithTracer 这个 Option 将其注入到 Kitex 里;服务发现是 WithResolver;Service Mesh 则是 WtihProxy 等。字节跳动内部的基础设施都是通过 Option 被注入到 Kitex 的,而且所有的 Option 都是通过前面说的 Suite 打包,简单地添加到业务的代码里完成。
与内部基础设施的集成
能力内部落地的经典案例:合并部署
这里介绍一个内部落地的经典案例:合并部署。其背景是,在开发微服务时,由于业务拆分和业务场景的多样化,微服务容易出现过微的情况。当服务数量越来越多,网络传输和序列化开销就会越来越大,变得不可忽视。因此,Kitex 框架需要考虑如何减小网络传输和序列化的开销。
字节跳动基础架构经过一系列的探索和实践,最终推出了合并部署的机制。它的思路是:将有强依赖关系的服务进行同机部署,减少它们之间的调用开销。理论上说起来比较简单,实际过程中需要非常多的组件进行配合。
Kitex 的做法是:首先,它会依赖一套中心化的部署调度和流量控制;其次,我们开发了一套基于共享内存的通信协议,它可以使得我们两个不同的服务在同一台机器部署时,不需要通过网络进行数据传输,直接通过共享内存,减少额外的数据拷贝。
在服务合并部署的模式下,我们需要特殊的服务发现和连接池的实现、定制化的服务启动和监听逻辑。这些在 Kitex 框架里都是通过依赖注入的方式给添加进来的。Kitex 服务在启动过程中会感知到我们 PaaS 平台提供的指定的环境变量。当它察觉到自己需要按合并部署的方式启动之后,就会启动一个预先注入的特定 Suite,随后将相应的功能全都添加进来再启动,就可以执行我们的合并部署。
那么,它的效果如何呢?在 2021 年的实践过程中,我们对抖音的某个服务约 30% 的流量进行了合并,服务端的 CPU 的消耗减少了 19%, TP99 延迟下降到 29%,效果相当显著。
内部落地的经典案例:合并部署
微服务框架推进的痛点
-
升级慢
大家可能好奇 Kitex 在字节跳动内部推广是不是很顺畅?其实并不是。作为一个相对而言比较新的框架, Kitex 和其它新生项目一样,在推广的过程中都会遇到同样的问题。特别是, Kitex 作为一个 RPC 框架,我们提供给用户的其实是一个代码的 SDK, 我们的更新是需要业务方的用户去感知、升级、部署上线,才能最终体现在他们的服务逻辑里,因此具有升级慢的问题。
-
召回慢
同时,因为代码都是由研发人员编写,如果代码出现了 bug,我们就需要及时地去感知定位问题,通知负责人去更新版本。因此,会有召回慢的问题。
-
问题排查困难
业务方的用户在写代码时,他们其实往往关注的是自己的业务逻辑,他们不会深入理解一个框架内部的实现。所以如果出现问题,他们往往会不知所措,需要依赖我们的业务同学才能进行相应的问题排查。所以会有问题排查困难的问题。
针对升级慢,我们有两个操作。一是,代码生成工具支持自动更新:当用户在使用时,我们会检查最新版本,然后直接将我们的版本更新到最新版本,这样可以及时把我们的框架新 feature、bug fix 直接推送到业务方;二是,用户群发版周知:我们有一个几千人的用户群,当有了新版本,我们会在用群里周知,可以最大范围的覆盖到我们的目标用户。
针对召回慢,我们有三个操作。一是,我们在线上建立完整的版本分布统计,监控所有服务上线部署的框架的版本;二是,我们会跟 PaaS 平台合作,在服务上线时进行卡点操作,检查它们使用的框架版本是不是有 bug,是否需要拦截;三是,针对有问题的版本,我们会及时封禁,及时推动用户更新。
针对问题排查困难,我们有两个操作。一是,我们积累了非常丰富的 Wiki 和问题排查手册,例如超时问题、 协议解析问题等。二是,如果遇到难以解决的问题,我们在线上服务默认开启了 Debug 端口,保证框架开发同学可以第一时间赶到现场去排查。
Kitex在字节内部的发展
数据显示,在 2020 年,v1.0 版本发布的初始阶段,用户的接受度比较低。直到 2020 年 6 月,线上接受 Kitex 的数量还不到 1000。随后进入快速发展的阶段,到 2021 年年初,累积接近 1w+ 的服务开始使用 Kitex。2021 年底,4w+服务使用 Kitex。
Kitex 的开源实践
开源工作主要包括代码、文档和社区运营三个层面。
代码层面
-
代码拆分、脱敏;
-
内部仓库引用开源仓库,避免内外多副本同时维护;
-
在开源过程中确保内部用户平滑切换、体验无损;
文档层面
-
重新梳理用户文档,覆盖方方面面;
-
建立详尽的用例仓库(CloudWeGo/Kitex-examples)。
社区运营
-
官网建设;
-
组建用户群,进行答疑解惑;
-
飞书机器人对接 Github 的 Issue 管理、PR 管理之类的业务,可以快速响应;
-
对优秀贡献者进行奖励。
在以上努力下,CloudWeGo/Kitex 仓库目前收获了 4.1k+ stars;Kitex-Contrib 获得多个外部用户贡献的仓库;CloudWeGo 飞书用户群近 950 个用户……
未来展望
首先,我们仍然会持续向开源社区反馈最新的技术进展。例如在 Thrift 协议上,虽然对 Thrift 的编解码已经做到非常极致的优化了,我们还在探索利用 JIT 手段来提供更多的性能提升;在 Protobuf 上,我们会补足短板,将在 Thrift 方面的优化经验迁移到 Protobuf 上,对 Protobuf 的生成代码和编解码进行优化;Kitex 后续也会进一步融入云原生社区,所以也在考虑支持 xDS 协议。其次,我们会去拓展更多的开源组件,去对接现存的云原生社区的各种常用的或者热门组件。最后,我们也会尝试去对接更多的公有云基础设施,使得用户在公有云上使用 Kitex 时能够拥有愉悦的体验。