日前,字节跳动技术社区 ByteTech 举办的第七期字节跳动技术沙龙圆满落幕,本期沙龙以《字节高性能开源微服务框架:CloudWeGo》为主题。在沙龙中,字节跳动字节跳动基础架构服务框架资深研发工程师高文举,跟大家分享了《大规模企业级 HTTP 框架的设计和实践》,本文根据分享整理而成。
本文将从以下五个方面介绍 CloudWeGo 大规模企业级 HTTP 框架 Hertz:
1. 字节跳动内部 Go HTTP 框架的变迁;
2. 企业级 HTTP 框架的设计考量和落地思路;
3. Hertz 的核心特点;
4. 未来规划和挑战;
5. 总结。
在正式开始介绍第一部分的内容之前,先给大家展示一组关键词。2020 年初 Hertz 立项,2020 年 10 月,Hertz 发布第一个可用版本。2022 年 6 月,Hertz 正式开源。截至目前,Hertz 在字节内部已经支撑超过 1.4 万个业务服务,日峰值 QPS 超过 5000 万。
Hertz 不仅支持业务服务,同时还会横向支持字节内部的各种基础组件,包括但不限于字节跳动服务网格控制面、公司级别压测平台以及 FaaS,还包括各种业务网关等等。Hertz 的高性能和极强的稳定性可以支撑业务复杂多变的场景。在公司内部 Hertz 接替了大量基于 Gin 框架开发的存量服务,大幅度降低了业务资源使用成本以及服务延时,助力公司层面的降本增效。
下面我们可以从 Hertz 出现的背景以及 Hertz 的设计目标和思路体会到,Hertz 的出现绝不是偶然。
众所周知,字节内部使用 Golang 比较早,在大约 2014 年左右,公司就已经开始尝试做一些 Golang 业务的转型。2016 年,我们基于已开源的 Golang HTTP 框架 Gin 框架,封装了 Ginex,这是 Ginex 刚开始出现的时期。
同时,2016 年还是一个开荒的时代,这个时期框架伴随着业务快速野蛮地生长,我们的口号是“大力出奇迹”,把优先解决业务需求作为第一要务。Ginex 的迭代方式是业务侧和框架侧在同一个仓库里面共同维护和迭代。
2017 - 2019 年期间,也就是 Ginex 发布之后,问题逐渐显现。主要有以下几点:
迭代受开源项目限制
Ginex 是一个基于 Gin 的开源封装,所以它本身在迭代方面是受到一些限制的。一旦有针对公司级的需求开发,以及 Bugfix 等等,我们都需要和开源框架 Gin 做联合开发和维护,这个周期不能完全由我们自己控制。
代码混乱膨胀、维护困难
由于我们和业务同学共同开发和维护 Ginex 框架,因此我们对于控制整个框架的走向没有完全的自主权,从而导致了整体代码混乱膨胀,到后期我们发现越来越难维护。
无法满足性能敏感业务需求
另外,我们能用 Gin 做的性能优化非常少,因为 Gin 的底层是基于 Golang 的一个原生库,所以如果我们要做优化,需要在原生库的基础上做很多改造,这个其实是非常困难的。
无法满足不同场景的功能需求
我们内部逐渐出现了一些新的场景,因此会有对 HTTP Client 的需求,支持 Websocket、支持 HTTP/2 以及支持 HTTP/3 等等需求,而在原生的 Ginex 上还是很难扩展的这些功能需求。
逐渐地,某些业务线开始做初步的尝试,他们会对另外的一些开源框架进行魔改。比较典型的例子是有一些业务线尝试基于 Fasthttp 进行魔改,Fasthttp 是一款主打高性能的开源框架,基于它进行魔改可以短期内帮助业务解决问题。这种魔改现象带来的问题是,框架魔改是一些业务线自发的行为,各个业务线可能会基于自身业务特性进行各自维护,从而导致维护成本上升非常严重。
到这里我们仿佛陷入了 Ginex 的怪圈。如前段时间爆火的电视剧《开端》一样,我们仿佛是从一辆开往学院南路的 45 路公交车上醒来,发现自己要前往公司进行下一代 Ginex 框架的维护工作。
大家也可以思考一下,如果是你来应对这样的场景,你会怎么做呢?
第一章节的内容总结如下:
早期基于开源框架封装
基于早期开源的 Golang HTTP 框架,实现了 Ginex 的封装。
随着实践发展,问题逐渐出现
框架混乱膨胀,框架的维护越来越困难,业务的新需求无法得到很好地满足。
为了解决问题出现基于另外的开源框架魔改的萌芽
我们需要思考如何跳出魔改的怪圈,把字节内部的企业级框架做得更好。
另外,还有一个遗留问题,就是应该如何跳出这个魔改的怪圈呢?这个问题第二章节会为大家进行解答。
为了跳出魔改的怪圈,我们决定从以下三个方面开始着手。
自主研发
既然 Ginex 是因为基于开源框架 Gin,没法做一些灵活的控制,那我们就改为完全自主研发框架。自主研发框架的代码全链路自主可控,也可以避免引入任何三方不可控因素,这样我们能够对自己的框架有一个比较完备的掌控力。
质量控制
下图列举了一些常规的质量控制手段。我要着重强调的是模糊测试,模糊测试在字节内部是广泛应用于 Hertz 框架的稳定性测试中。它的核心点在于通过一系列的模拟服务,尝试模拟出线上用户在使用我们的框架时,实际遇到的一些场景和使用方式。然后通过一些随机的算法,生成尽可能复杂、覆盖各种 Case 的场景,这可以让我们检测出一些潜在的问题。这套测试也在 Hertz 早期的质量建设中,帮助我们将一些问题防患于未然。
严格准入
既然 Ginex 的问题是大家都在向里面写入内容,那么我们可以控制入口,建立一套完备的需求开发以及 Review 的闭环,控制迭代的整体流程,从而控制代码准入。同时我们配备统一的需求管理以及严格的发版准入规范,做一个标准的公司级别的框架。
举一个比较形象的例子,如果我们把下一代框架比作一个人——“框架人”,自主研发表示这个“框架人”首先会拥有对自己身体的主导权,他不会受到来自于环境或者他人的影响;质量控制表示“框架人”能够定期体检,提早发现一些潜在的疾病,将其扼杀于摇篮;严格准入表示“框架人”有科学的饮食摄入和自律的生活习惯。可想而知,如果我们能够做到以上三点,我们的“框架人”就能够拥有一个健康的体魄。
明确了应该如何跳出怪圈之后,我们还应该明确知道这个框架要具备哪些功能和特性,也就是首先应该聚焦到框架的核心痛点上。“框架人”不能只有健康的体魄,还应该拥有有趣的思想和灵魂。一个成熟的框架不仅仅要应对来自业务侧的需求,如功能需求、性能需求和易用稳定等,还要考虑框架自身的发展,而这一点恰恰是我们在 Ginex 的迭代过程中忽略的。
如下图右侧金字塔所示,最上层是高效支撑,毋庸置疑框架的存在肯定是为了支撑我们的业务需求。中间层是一个质量保证的红线框架,框架需要保证它自身的质量,只有以高质量完成的框架才能有自信承担字节内部的 5000 万 QPS,以及各种各样的使用场景。金字塔的最底层是长期、可持续性发展,这也是作为未来想要保持持续迭代的框架最重要的一点。
基于上一部分,我们可以进一步梳理出框架的需求痛点。痛点主要有两个方面:
多样的需求:支撑支撑各个业务线及基础设施 (横向扩展性)。
灵活的结构:贯穿 HTTP 生命周期的掌控力 (纵向模块化)。
在此基础上进一步抽象出框架的科学发展观:
聚类需求:面向通用能力展开设计。
跳出局部:针对一些复杂问题,在更大范围内寻求最优解。
后续我会针对这个科学发展观进一步阐述 Hertz 究竟是如何实现的。
第二章节的内容总结如下:
跳出怪圈
引入“框架人”的概念,帮助大家理解框架的自研、质量控制和严格准入。
痛点梳理
为“框架人”注入有趣的灵魂,框架需要应对来自业务侧的多样化需求,还要保证自己的可持续性发展。
框架科学发展观
需求聚类,跳出局部。
Hertz 框架是如何实现第二章节中提到的框架痛点和科学发展观的呢?本章节将具体进行介绍。
首先介绍 Hertz 框架的架构设计。下图是一个请求从建立、连接到完成的全过程。左侧是客户端,右侧是服务端,在我们发起链接建立请求之后,链接建立完成;之后客户端发起请求到服务端,服务端进行路由处理,然后将路由导向业务逻辑处理;业务逻辑处理完毕后,服务端返回这个请求,完成一次 HTTP 请求的调用。
那么在这个过程中我们的框架到底做了哪些事情呢?从图中不难发现,首先框架进行了链接处理,其次是协议处理,之后基于路由做了逻辑分发,即路由处理,最后做了业务逻辑处理。我们把框架做成一个结构之后会发现,这个结构包含的就是这四部分。
基于这个逻辑,我们可以看一下 Hertz 的整体架构图。如下图所示,从下往上看红线框圈住的部分,可以发现这就是上文提到的请求建立的全过程。各层的能力及作用如下:
传输层 Transport:抽象网络接口;
协议层 Protocol:解析请求,渲染响应编码;
路由层 Route:基于URL进行逻辑分发;
应用层 Application:业务直接交互,出现大量 API。
我们可以看到图中除了中间部分包含的四层,左右两侧各有两列。右侧是通用层 Common,主要负责提供通用能力、常用的日志接口、链路追踪以及一些配置处理相关的能力等。左侧是 Hertz 的代码生成工具 Hz,又称脚手架工具,它可以帮助我们在内部基于 IDL 快速地生成项目骨架,以加速业务迭代。
Hertz 的分层设计是能够和代码组织结构一一映射的。下图是 Hertz 仓库里面的代码组织结构,可以看到根目录下的 cmd 包里面存放着 Hz 工具,在 pkg 包下存放着上述主要四层以及通用层 Common。因此同学们看到架构设计图之后,可以直接在 Github 学习 Hertz 的代码。
Hertz: https://github.com/cloudwego/hertz
总体来说,Hertz 的架构设计理念就是“简洁有序,保证让所有开发者轻松理解,在开发的过程中持续贯彻”。
那么基于 Hertz 的架构设计,应该如何展开易用性和可扩展性呢?下图是 Hertz 架构主要四个层级的抽象。
应用层
应用层提供了一些通用能力,包括绑定请求、响应渲染、服务发现/注册/负载均衡以及服务治理等等。其中,洋葱模型中间件的核心目的是让业务开发同学基于这个中间件快速地给业务逻辑进行扩展,扩展方式是可以在业务逻辑处理前和处理后分别插桩埋点做相应处理。一些比较有代表性的应用,包括日志打点、前置的安全检测,都是通过洋葱模型中间件进行处理的。
路由层
路由层也是非常通用的,主要提供静态路由、参数路由、为路由配置优先级以及路由修复的能力,如果我们的路由层没办法满足用户需求,它还能支撑用户做自定义路由的扩展。但实际应用中这些路由能力完全能够满足绝大多数用户的需求。
协议层
Hertz 同时提供 HTTP/1.1 和 HTTP/2,HTTP/3 也是我们在建设中的能力,我们还会提供 Websocket 等 HTTP 相关的多协议支持,以及支持完全由业务决定的自定义协议层扩展。
传输层
目前我们已经内置了两个高性能的传输层实现。一个是基于 CloudWeGo 开源的高性能网络库 Netpoll 的传输层扩展,另一个是支持基于标准库的传输层扩展。此外,我们也同样能支持在传输层上进行自定义传输层协议扩展。
下图每一层中标红的能力都能够体现出,我们能够在框架的任何一个分层上支撑用户做最大程度的自由定制,这样可以最大程度地满足企业级内部用户和潜在用户的业务需求。如果同学们想要深入了解 Hertz,可以参考 CloudWeGo 官网的 Hertz 部分,上述所有内容均有具体描述。
官网:https://www.cloudwego.io/zh/docs/hertz/
在性能方面,Hertz 又是如何在自主可控的范围内做高性能探索的呢?
熟悉 Hertz 代码的同学会发现,我们的 HTTP/1.1 协议借鉴了一些 Fasthttp 的优化思路和手段。HTTP/1.1 协议中的 Header 为不定长数据段,往往需要解析到最后一行,才能够确定是否完成解析。同时,为了减少系统调用次数,提升整体解析效率,涉及 IO 操作时,我们通常引入带 buffer 的 IO 数据结构。如下图所示,它的核心点是最下层的 buffer,buffer 是一个类似于一块完整的内存空间,我们可以将 IO 读到的数据放进这个空间做暂存。
这样做出现的问题是,原生的 bufio.Reader 长度是固定的,请求的 Header 大小超出 buffer 长度后,.Peek() 方法直接报错 (ErrBufferFul),无法完成既定语义功能。
对于上述问题,其实有一些可能的解决方法:
直接利用 bufio.Reader 的局限当做 Feature,通过 buffer 大小作为 Header 大小的限制。如果超出这个大小,Header 直接解析报错,这也是 Fasthttp 的做法。但实际上超出 buffer 长度后报错会导致我们没办法处理这部分请求,从而导致框架功能受限。
header 解析带状态,暂存中间数据,通过在上层堆叠额外复杂度的方式突破 bufio 本身的限制。但是暂存中间态会涉及到一些内存的拷贝,必然会导致性能受限。
字节内部的使用场景非常多,我们不仅要支持各种业务线的开发,还要支持一些横向的基础组件。不同的业务,不同的场景,数据规模各异。如何成为通用且高效的地解决 bufio.Reader 的问题成为 Hertz 面临的内部重要挑战。我们既然已经站在 Fasthttp 这个“巨人”的肩膀上了,能否往前再走一步呢?
答案是肯定的。基于内部的使用场景,同时结合 Netpoll 的优势,我们设计出了自适应 linked buffer,并且用它替代掉了原生的 bufio.Reader。从下图可以看到,我们的 buffer 不再是一个固定长度的 buffer,而是一条链,这条链上的每一个 buffer 大小能够根据线上真实请求进行动态扩缩容调整,同时搭配 Netpoll 中基于 LT 触发的模型做数据预拷贝。从实施效果上来看,这个自适应调整能够让我们的业务方完全无感地支撑任何他们的业务特性。也是因为我们能够将 buffer 进行动态扩缩容调整,从而能够保证在协议层最大程度做到零拷贝协议解析,这能够带来整体解析上的性能提升,时延也会更低。
因为目前在字节内部 HTTP/1.1 还是一个比较主流的协议,所以我们基于 HTTP/1.1 做了很多尝试。
首先是协议层探索。我们正在尝试基于 Header Passer 的重构,把解析 Header 的流程做得更高效。我们还尝试了做一些传输层预解析,将一些比较固化的逻辑下沉到传输层做加速。
其次是传输层探索。这包括使用 writev 整合发送 Header & Body 达到减少系统调用次数的目的,以及通过新增接口整合 .Peek() + .Skip() 语义,在内部提供一个更高效的实现。
下图是 Benchmark 的开源数据。左侧第一张图是在同等的机器环境上,Hertz 和横向的框架 Gin、Fasthttp 极限 QPS 比较情况,蓝线是 Hertz 处于较高极限 QPS 的状态。第二张图是 TP99 时延状态,第三张图是 TP999 时延状态,可以看到 Hertz 的整体时延是处于一个更低的水平上。
CloudWeGo 公众号曾发布关于字节跳动服务网格控制面的文章,讲述字节跳动服务网格从 Gin 框架迁移到 Hertz 的落地实践。下图是他们代码展示的真实收益,从 Gin 框架替换成为 Hertz 框架后,CPU 流量从大概快到 4K 降到大约只有 2.5K,Goroutine 数量从 6w 降到不足 100 个,Goroutine 稳定性得到极大地提升。同时替换成 Hertz 后,框架相关的开销已经基本消失,服务网格在线上稳定承载了超过 13M QPS 的流量。
字节跳动服务网格基于 Hertz 框架的实践:
https://mp.weixin.qq.com/s/koi9q_57Vk59YYtO9cyAFA
第三章节的内容总结如下:
分层抽象
解构 HTTP 框架,分层解耦。
易用可扩展
提供了更丰富 API 和足够灵活的拓展能力,在每一层抽象中都提供了一个足够灵活的扩展能力应对可能的需求。
自主可控的高性能探索
自适应 buffer,零拷贝解析,未来将会进行更多的高性能探索。
我认为 Hertz 未来的发展规划主要围绕以下几个方面:首先,打造泛 HTTP 框架。我们的最终目标是希望 Hertz 能够解决在 HTTP 领域内的所有问题;其次,助力 CloudWeGo,希望 Hertz 能够助力 CloudWeGo 打造一个企业级云原生微服务矩阵;最后希望 Hertz 能够持续服务更多的用户。
本次分享的主要内容总结如下:
字节跳动内部 Go HTTP 框架的变迁:从基于开源封装,到开启自研之路;
企业级 HTTP 框架的设计考量和落地思路:破圈、需求提炼、框架科学发展观;
Hertz 核心特点:分层抽象、易用可扩展、自主可控的性能探索;
Hertz 未来的规划和挑战:框架持续打磨、助力 CloudWeGo、服务更多用户。
最后欢迎对 Hertz 感兴趣的同学积极参与到 CloudWeGo 社区中,我们一起完善 Hertz,共同建设 CloudWeGo!
以上内容整理自第七期字节跳动技术沙龙《字节高性能开源微服务框架:CloudWeGo》,获取讲师 PPT 和回放视频,请关注 CloudWeGo 公众号,并在后台回复关键词“一周年”。
GitHub:https://github.com/cloudwego
官网:www.cloudwego.io