西瓜视频基于Hertz的微服务落地实践
01 西瓜视频微服务架构设计
西瓜视频介绍
西瓜视频是一个开眼界、涨知识的视频 App (Informative Video Platform),作为国内领先的中长视频平台,它源源不断地为不同人群提供优质内容,让人们看到更丰富和有深度的世界,收获轻松的获得感,点亮对生活的好奇心。
同时,西瓜视频鼓励多样化创作,帮助人们轻松地向全世界分享视频作品,创造更大的价值。目前平台月活跃创作人超过 320 万,月活跃用户数超过 1.8 亿,日均播放量超过 40 亿,用户平均使用时长超过 100 分钟。
微服务架构设计时关注哪些方面
业务域划分
上面四张图分别对应我们的中视频、电商、长视频和作者侧的业务。当然我们的业务场景远不止这些,但是也反映出了 C 端场景多,业务域划分较细的特点,因此我们在微服务架构设计的时候需要着重考虑服务划分与解耦,在设计期间我们主要遵循两个原则:
单一职责原则:确保每个微服务只负责一个特定的业务功能,避免职责混乱。
领域驱动设计:使用领域驱动设计方法来划分服务边界,确保各个服务独立、可复用。
这样设计之后,可以保证:
业务模块的独立性:每个微服务可以独立开发、测试、部署和扩展,提升了开发效率和系统的灵活性。
技术栈灵活性:不同的微服务可以使用最适合其业务需求的技术栈,不需要统一技术选型。
故障隔离:一个服务的故障不会影响其他服务的运行,提高了系统的可用性和稳定性。
按需扩展:可以根据每个服务的负载情况独立扩展,优化资源使用和成本。
性能
架构设计
上图是西瓜视频整体的微服务架构设计。
从上到下我们分为三层,分别是接入层,业务层和基础组件层。
接入层
不同的分端:包括西瓜 APP,西瓜 PC,TV 端鲜时光以及 M 站。
负载均衡及网关
使用公司内部组件来提供负载均衡和通用网关能力。
API 服务:使用 Hertz 框架的服务。
业务层
消费侧业务,基础业务,和其他的一些互动社区等 RPC 业务,这些服务采用的是 Kitex 框架。
消费侧业务主要面向 C 端用户的场景就是信息流和详情页。
信息流:主要是西瓜 APP 的全部频道模块,包括进入西瓜视频后的首页看到的推荐精选都对应我们的信息流服务。
详情页:我们知道推荐频道更多对应的是一个沉浸式的场景,但是我们的视频也会有其他的入口进入,比如个人主页等场景,这个时候点击一个视频就会进入详情页,相对于沉浸式那种更专注于视频本身播放的体验,详情页会包含更多的视频信息。
推荐系统:这里是一些推荐、广告的服务,主要是提供底层的数据的 id 返回,包含广告混排,推荐排序,广告投放等功能。
打包服务:公司的业务线会比较繁杂,所以随着发展抽象出了打包层,各个业务会有自己数据结构的打包服务,作为最终返回给客户端的数据。这里就包括小视频、中视频、长视频、直播等数据的打包。
互动模块
在这些业务之外,我们还会有其他的一些互动和社区的功能模块。这种都是有公司专门的关系中台、评论中台去维护。同时,我们的业务还会依赖业务用到的存储,像 mysql、redis 等。
基础组件
基础组件,比如语言框架,像前面提到的 Hertz、Kitex 等框架,还有一些日志、监控,配置系统。
02 Hertz 框架介绍
背景
字节跳动从 2014 年开始使用 Golang,2016 年基于 Gin 框架封装了 Ginex。但 Ginex 在迭代受开源 Gin 项目限制、代码混乱膨胀导致维护困难、无法满足性能敏感和功能扩展需求等问题。2020 年部分业务线尝试魔改其他开源框架如 Fasthttp,但带来了分散生产力和巨大维护成本的问题。为解决这些痛点,字节于 2020 年初立项开发自研高性能 Go 框架 Hertz,经过两年多的迭代,Hertz 于 2022 年 6 月正式开源,目前已广泛应用于字节内部逾 1.4 万个服务,支撑日峰值 QPS 超 5000 万,显著降低资源使用和服务延时,接替大量基于 Gin 的存量服务,助力公司降本增效。
为什么选择 Hertz
极致的性能
Hertz 的性能指标是作为一个核心指标开展设计和实现的。Hertz 默认使用自研的高性能网络库 Netpoll,在一些特殊场景相较于 go net,Hertz 在 QPS、时延上均具有一定优势。关于性能数据,大家可以看一下 hertz-benchmark(https://github.com/cloudwego/hertz-benchmark),可以看到 Hertz 的 QPS 和时延指标在和其他三个知名框架对比中已经全面占优,对比 Gin 更是遥遥领先。
并且,框架整体的持续优化会贯穿框架的整个生命周期,持续不断地进行下去。
易用性,开发者友好
在开发过程中,快速写出正确的代码往往是重要的。Hertz 在设计 API 时,考虑到用户的使用习惯,参考业界主流框架使用 API 的方式,并加以优化。在 Hertz 在迭代过程中,积极听取用户意见,持续打磨框架, 比如很多用户希望 Client 也有 Trace 的能力,为此,Hertz Client 支持了中间件能力。Hertz 也提供了命令行工具,一键生成代码,提高框架的易用性。
Hertz 提供了一个简单易用的命令行工具 hz,用户只需提供一个 IDL,根据定义好的接口信息,hz 便可以一键生成项目脚手架,开箱即用使用 Hertz;hz 也提供更新能力,用户的 IDL 如果发生改变,hz 可以更新脚手架。目前 hz 支持了 Thrift 和 Protobuf 两种 IDL 定义。命令行工具内置丰富的选项,可以根据自己的需求使用。
丰富的文档体系
即使是从来没有使用过相关框架的新同学,都能够通过相应的文档,快速上手 Hertz,体验 Hertz 所带来的极致的开发体验。
稳定性
Hertz 对内对外支撑了大量的业务服务,业务选择 Hertz 的一个重要考量就是稳定性。Hertz 也制定了各种措施来保证框架的稳定性:
基于线上流量特征的随机模拟。Hertz 会根据线上真实的流量曲线,通过模拟和重放的方式,在测试环境中复现各种极端的高并发场景,从而验证系统的承载能力和容错能力。
核心 API 全覆盖。Hertz 所有对外提供服务的 API 接口,无一例外都要经过完备的性能测试、压力测试和负载测试,确保在各种极限情况下也能保持稳定和高效。
适配不同的线上部署环境。针对 Hertz 服务的不同场景,会准备不同的线上环境。这些环境硬件资源配置不尽相同,通过在不同环境中反复测试和验证,可以全面评估系统的稳定性和可扩展性。
7*24 小时监控大盘。全天候实时监控系统的各项核心指标,一旦发现异常,能够第一时间定位并通知值班人员介入处理。
严格的发布流程。Hertz 对系统的每一次上线升级都有特别严格的流程要求,必须经过线下测试、验证、代码 review 等多个环节,避免出现变更带来的风险。
低廉的迁移成本
西瓜早期的 API 服务都是基于 Ginex 框架开发的。而 Hertz 立项之初,就将针对存量 Ginex 服务的迁移作为一个高优需要照顾的环节。在一些业务常用 API 上做了许多兼容性的设计。
同时,Hertz 也为存量项目提供了一键迁移的方案 Ginex 项目快速迁移 Hertz 1.x 指南,使用一键迁移工具,用户可能无需/只需改动少量代码即可完成 Gin —> Hertz 项目的迁移。详见:https://www.cloudwego.io/zh/docs/hertz/tutorials/service-migration/#gin
丰富的扩展能力
Hertz 采用了分层设计,提供了较多的接口以及默认的扩展实现,用户也可以自行扩展。Hertz 目前支持了日志、监控、服务注册与发现、网络库和协议等扩展能力,未来有望为用户提供更多的扩展能力。
03 Hertz 迁移过程、踩坑经验
迁移过程
1. 选择要迁移的服务
迁移的第一步就是要先选择要迁移的服务,根据帕累托原则,我们优先找出占据最大比例的成本来源,并处理这些高成本项目,以最大化资源利用效率。根据性能分析平台,我们选择了目前西瓜 CPU 资源消耗最大的两个 api 服务进行迁移,分别对应我们的观看历史、点赞和弹幕的有关服务。
2. SDK 库适配
西瓜业务线对 Ginex 做了封装,提供一个 SDK 给其他 api 服务使用,比如在获取观看历史的接口中,我们会先使用 SDK 中的 newRequestContext 方法来做一些请求上下文的初始化操作。在这个请求上下文中我们会有 ab 实验、设备信息、地理位置信息等上下文的初始化。所以我们第一步就是针对 SDK 库中相应的代码进行适配。在这个适配过程中有一些三方的 SDK 库的依赖,可以直接升级 SDK 对应的 Hertz 版本。
3. 执行一键迁移脚本
Hertz 同学提供了一键迁移脚本,只需在当前服务目录下执行脚本即可完成大部分重复性的改造工作。
脚本通过正则匹配的方式完成大部分重复性替换工作,对于一些 gin、ginex 字样会替换为Hertz 中的相应的实现。
4. 手动修改
Hertz 中间件的设计采用了洋葱模型,洋葱模型是一种中间件流程控制方式,做到核心逻辑和通用逻辑分离。
中间件可以做日志记录、性能统计、安全控制、事务处理、异常处理等场景。
Hertz 预置了几款中间件:
Recovery 中间件:负责处理链路上的 panic
Metrics 中间件:负责请求相关指标上报
对于业务自己实现的特殊中间件需要进行迁移,关于如何实现一个中间件 Hertz 也给出了实例。
以西瓜业务自己的特殊中间件 default_stable 为例,我们会在 header 中将 stable 设置为 1,用于做 SLO 数据统计。这种中间件我们就需要做一些适配工作,这里的话就会面临接口不匹配的问题,可以参考 Gin —> Hertz API 对照表(https://github.com/hertz-contrib/migrate/blob/main/gin_to_hertz.md)进行修改。
踩坑经验
1. Query tag 缺失导致请求参数解析失败
表现
Query 请求参数在服务端没有解析成功,调用 RequestContext 进行参数绑定的时候报错。
原因
参考 Hertz 支持的参数绑定与校验相关功能及用法,不通过 IDL 生成代码时若字段不添加任何 tag 则会遍历所有 tag 并按照优先级绑定参数,添加 tag 则会根据对应的 tag 按照优先级去绑定参数。
Hertz 支持的参数绑定与校验相关功能及用法:https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/binding-and-validate/
请求参数有关的结构体未设置 query tag,导致 query 请求参数在服务端没有解析成功。
解决方式
结构体添加 query tag
通过单元测试提前规避
2. 未配置 looseZero 模式导致绑定数值类型报错
表现
在一些场景下,前端有时候传来的信息只有 key 没有 value,这会导致绑定数值类型的时候报错。
原因
未配置 looseZero 模式
解决方式
04 落地 Hertz 后的收益
上线前后对比发现 CPU 使用率下降约 10%,优化效果明显,详细的性能收益如下:
平均 CPU 核数从 2730 降为 2443
单核 QPS 处理能力提升:10.7%
内存占用率变化—优化前:31.1% ,优化后:29.3%
核心接口 LatencyPct99 下降:10.15%