本文选自“字节跳动基础架构实践”系列文章。
“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经验与教训,与各位技术同学一起交流成长。
线上流量引流线下环境是一个通用需求,广泛应用于功能测试、压测等场景。本文介绍了引流系统在字节跳动的发展过程和系统设计,希望能给大家带来一点新的思考和收获。
AB Test (diff 测试) 是在互联网行业中比较常用的验证方法,例如 google 通过 AB 实验针对广告和推荐的效果做验证,Twitter 研发了 Diffy ,把 Diff 验证能力应用到了 API 接口的质量保障上。通常 AB Test 有两种形式,一种是线上多个服务版本,通过接入侧分流 AB 来做实验,但对于广告这类场景,一旦某个模型有问题,就会造成资损。另外一个模式是通过线上的流量复制回放到内部环境,这种方式对于生产是绝对安全的,例如 Twitter 的 AB 验证服务 diffy 就是走的这个模式。今天字节内部推荐,广告,等很多业务线都是通过线上流量实时回放的模式做 AB 实验。为了满足业务的需求,我们自研了一套线上流量录制回放系统 ByteCopy 来支撑业务的海量流量吞吐和不断产生的对于流量录制回放的新需求。
这篇文章会从业务场景、系统架构、问题分析等几个方面来介绍 ByteCopy 这套系统的演进过程。
刚开始业务的需求还是比较简单的,就是希望在业务方部署好了目标服务 (HTTP 和 RPC) 后,引流系统可以把对应线上生产的流量复制一份并转发过去,并且整个引流过程可以灵活管控,只在需要流量的时候开启。
从引流自身来看,主要分为 2 种类型,主路复制和旁路复制,我们分别来分析一下这两种模式的优劣。
主路复制是指在调用链中进行流量复制:一种是在业务逻辑中进行流量复制,如在调用 API/RPC 过程中,由业务方编写代码逻辑,记录 request / response 信息内容;另外一种是在框架(如使用 Dubbo、Service Mesh 等网络框架)处理逻辑中进行复制。
对比主路复制,旁路复制突出了业务无感知的特点,一般是由第三方服务在网络协议栈中,监听复制流量
虽然 linux 提供了 libpcap 这样的底层 packet capture 库,不过本着快速交付业务需求的目标,我们选择了开源的 TCPCopy 来作为整个引流系统的核心基础。TCPCopy 在这里就不多介绍,只在下面附上一张简单的架构图,其中 TCPCopy 和 Intercept 是 TCPCopy 的两个组件,相关细节感兴趣的同学可以自行查找资料。
TCPCopy 的主要优势:
同时,也具有以下不足:
为了满足字节的需求,我们在整体架构上引入了一些其他组件来弥补 TCPCopy 自身的不足。
为了解决 TCPCopy 存在的不足,我们在通过 TCPCopy 直接进行流量转发的方案基础上又进行了一些优化。
首先,在 TCPCopy 和被测服务之间额外引入了七层代理进行数据转发。七层代理本身可以校验请求的完整性,避免不完整的请求被转发到测试服务对干扰测试造成干扰。
此外,我们将七层代理和 TCPCopy 的 intercept 组件部署在一批专用于流量转发的服务器上,进行转发任务时只需要修改这批服务器的 iptables ,而被测服务只需在测试机上正常运行,不用进行额外配置,因此可以尽量避免修改 iptables 带来的风险。
为了能够更好地融入公司的技术生态系统,同时支持更丰富的测试场景,我们还专门实现了一个用于测试的七层代理。具体加入了以下能力:
最后,为了让引流功能变得易用,我们把 TCPCopy 的两个组件,以及我们的七层代理进行了封装,打包成了一个平台提供给用户。用户只需要指定引流源和被测服务的 IP 和端口,以及引流任务的持续时间,即可进行一次引流测试。
线上的整体架构大概如下图所示,用户提交任务后,我们会负责进行各个组件的调度、配置和部署,将流量从线上转发到用户的待测实例。
随着规模的逐渐发展和更多用户场景的提出,这套架构也逐渐暴露出了一些问题。
TCPCopy 在实现上没有进行多线程的设计,因此实际的转发吞吐能力较为有限,对于一些高带宽的测试场景无法很好地支持。
TCPCopy 定位只是请求复制转发工具,只会复制线上流量的请求部分,而不会复制线上流量的响应。我们接到了一些想要对线上流量进行分析的用户的需求,他们希望能够同时收集线上流量的请求和响应,TCPCopy 没法支持这类场景。
前面提到了第一代引流系统存在一些性能和灵活性的问题,与此同时业务也提出了一些新的需求,例如支持 MySQL 协议,支持历史流量的存储和回放等。考虑到在现有的 TCPCopy 的架构上很难做扩展,所以我们考虑推翻现有架构,重新构建字节新一代的引流系统 - ByteCopy (寓意是复制线上每一个字节)
在以上演进的基础上,我们可以按职责把七层流量复制大致分解为下面三个模块
针对 3 个模块我们分别展开介绍
流量采集模块会依据服务部署的平台以不同方式拉起,如在 Kubernetes 会由 Mesh Agent 唤起,使用 libpcap 监听特定端口流量,在用户态重组 TCP 层包,batch 发送至 Kafka。
默认场景下,流量采集模块只会对被采集的服务监听的 IP 和端口进行抓包。此外,为了提供出口流量采集(即采集某一服务对其下游依赖发的调用)的能力,流量采集模块还对接了公司的服务发现框架。在需要对出口流量进行采集时,流量采集模块会查询下游依赖服务所有实例的 IP 和端口,对这部分流量也进行采集。(这一方案存在一定问题,后续会详细介绍)
由于流量采集进程和应用进程是部署在同个 Docker 实例或物理机里,业务会对流量采集模块的资源占用比较敏感,我们除了在代码层优化,还会用 cgroups 对资源使用做硬性限制。
此外流量采集平台是多租户设计,对一个服务可能同时存在多个用户的不同规格的采集需求,如用户 A 希望采集 env1 环境 5% 实例流量,用户 B 希望采集 env1 环境 1 个实例的流量及 env2 环境 1 个实例的流量,如果简单地独立处理用户 A 和 B 的请求,会出现 env1 环境部署 5%+1 实例 env2 部署 1 实例这种冗余部署。我们的做法是把用户的请求规格和采集模块的实际部署解耦,用户提交一个规格请求后,会先和已有的规格合并,得到一个最小部署方案,然后更新部署状态。
引流源采集上来的原始流量还是第四层协议,为了支持一些更复杂的功能,比如过滤,多路输出,历史流量存储,流量查询及流量可视化等等,我们需要将四层流量解析到七层。字节跳动内部服务使用得比较多的协议是 Thrift 和 HTTP ,这两个根据协议规范即可很好地完成解析。
流量解析有一个难点是判断流量的边界,区别于 HTTP/2 等的 Pipeline 连接复用传输形式,Thrift 和 HTTP/1.X 在单条连接上严格按照请求-响应对来进行传输,因此可以通过请求和响应的切换分隔出完整的请求或响应流量。
对于线上采集的流量,不同用户会有不同的业务用途,如压测平台可能希望将流量先持久化到 Kafka,然后利用 Kafka 的高吞吐发压;有些研发同学只是简单从线上引一份流量转发到自己的开发环境做新特性测试;有些同学希望转发 QPS 能达到一定水位以实现压测的目的;还有的是特定流量会触发线上 coredump ,他们希望把这段流量录制下来线下 debug 等等。针对不同的场景,我们实现了若干流量输出形式。
下面会着重介绍转发和存储。
结构如上图,emitter 会在 zookeeper 上注册自身,scheduler 感知到 emitter 节点信息,将任务根据各个 emitter 节点的标签和统计信息过滤/打分,然后调度到最合适的节点上。这里有个问题是为什么不直接使用无状态服务,由每个 emitter 实例均等地转发,而采用 sharding 方案,主要是基于下面几点考虑:
由于 emitter 对性能比较敏感,我们为此也做了很多优化,比如使用了 fasthttp 的 goroutine 池避免频繁申请 goroutine,对连接的 reader/writer 对象池化,动态调节每个 endpoint 的工作线程数量以自适应用户指定 QPS,避免 goroutine 浪费及闲置长连接退化成短连接,全程无锁化,通过 channel+select 做线程同步和数据传递等等。
存储分为了两层,数据层和索引层,采用双写模型,并有定时任务从数据层纠错索引层保证两者的最终一致性。存储需 要支持回放和查询两种语义,Data Layer 抽象成了一个支持 KV 查询,按 Key 有序,大容量的存储模型,Index Layer 是 Multi-index->Key 映射模型,通过这两层即可满足流量查询+回放的需求。所以 Data Layer 和 Index Layer 底层实现是模块化的,只需符合上述模型并实现模型定义 API。
Data Layer 的理想数据结构是 LSM tree,写入性能出色,对于流量回放场景,需要按 key 有序扫描流量记录,因为 LSM 满足按 key 的局部性和有序性,可以充分利用 page cache 和磁盘顺序读达到较高回放性能。分布式 LSM Tree 业界比较成熟的开源产品是 HBase,字节跳动内部也有对标产品 Bytable,我们同时实现了基于这两个引擎的 Data Layer,经过一系列性能 benchmark 我们选择了 Bytable 作为 Data Layer 实现。
Index Layer 使用了 ES 的能力,因而可以支持用户的复合条件查询,我们会预先内置一些查询索引,如源服务,目标服务,方法名,traceid 等等,流量查询目前的使用场景一个是作为服务 mock 的数据源,可以屏蔽掉功能测试或者 diff 中不必要的外部依赖,还有一个功能是流量可视化,用户通过请求时间,traceid 等等,查看特定请求的内容。其他场景功能还有待进一步发掘。
Diff 验证是互联网公司在快速迭代下保持产品质量的一个利器,类似 Twtiter 的 Diffy 项目,都是通过线上流量的录制回放来实现。但是它的适用场景也很有限,因为是直接在生产环境上通过 AB 环境做回放,无法支持写的流量。虽然阿里巴巴的 doom 平台可以解决写场景的回放隔离问题,但是它是在应用程序中通过 AOP 来实现的,强绑定 JAVA 生态。
通过 ByteCopy 的无侵入引流和流量存储回放能力,结合我们自研的 ByteMock 组件,我们提供了面向业务的无侵入 diff 解决方案,并解决了写隔离的问题。
在一个生产环境下的 (A,B,C) 链路中,通过 ByteCopy 实现了针对每一跳 (request, response) 的采集,在做 A 的 diff 验证的时候,通过 ByteCopy 实现对于 A 服务请求的回放,同时,基于 ByteMock 来实现对于服务 B 的 mock,并支持针对一个 trace 的 response 回放 (依赖 ByteCopy 中流量存储,实现精准的回放)。因为 B 是 mock 的,即使是一个写的请求,也可以做到对于线上没有任何影响。
前面提到,在进行出口流量的采集时,会对下游依赖服务的所有实例的 IP:端口 进行抓包。而实际的生产环境中,同一台服务器上,可能会部署具有相同下游依赖的多个服务,只依赖四层数据,无法判断抓到的数据到底来自哪一个服务,会造成抓包、处理和转发流程中都会存在资源浪费的问题。目前来看基于网卡抓包的方案应该没法很好地解决这个问题,我们也在尝试探索一些其他的流量采集的方案,比如探索用 ebpf 进行进程级别的流量采集。
现阶段我们的引流回放系统只会根据用户的配置被动进行流量采集,而为了及时拿到流量进行测试,用户一般都会选择实时引流进行测试。而实际上并不是所有的场景都一定需要实时的流量进行测试,我们在规划逐步将引流回放系统从一个按照用户要求进行流量转发回放的工具,转变为一个线上复制流量的取用的平台。
在流量存储能力的基础上,对于有测试需求的服务,平台主动错峰、定时发起流量录制任务,从而维护一个不断更新的流量池,用户需要流量时直接从流量池中取用,这样一来,既可以尽量避免引流操作对和线上业务抢占计算资源,也可以使得流量的可用性更高。
随着基于流量录制回放的上层应用的完善,为了更多的业务方便接入试用,我们正在考虑朝着常态化的引流去演进。这个势必对我们的流量存储带来新的挑战,无论是的数据规模,存储形态以及查询性能。我们希望可以基于现有架构的存储系统,构建流量存储的解决方案,支持海量数据吞吐的同时,能够支持点查(基于 TraceId ),和 time-range scan 等多种复杂的高性能查询方式。另外我们也在积极和安全团队合作,确保相关核心流量数据在存储时候能够实现脱敏,同时不断强化对于流量存储使用的安全审计。
到今天为止,ByteCopy 系统已经支撑了字节绝大部分业务线在不同场景下的各种引流需求, 我们一直在努力丰富 ByteCopy 的功能场景,不断提升系统稳定性和吞吐容量,此外我们也在积极构建 ByteMock 等自研的研发组件,通过和 ByteCopy 形成组合拳,解锁生产流量在研发活动中更多的使用场景,帮助业务团队更好地去构建各种有趣的产品。
字节跳动基础架构团队是支撑字节跳动旗下包括抖音、今日头条、西瓜视频、火山小视频在内的多款亿级规模用户产品平稳运行的重要团队,为字节跳动及旗下业务的快速稳定发展提供了保证和推动力。
公司内,基础架构团队主要负责字节跳动私有云建设,管理数以万计服务器规模的集群,负责数万台计算/存储混合部署和在线/离线混合部署,支持若干 EB 海量数据的稳定存储。
文化上,团队积极拥抱开源和创新的软硬件架构。我们长期招聘基础架构方向的同学,具体可参见 job.bytedance.com (文末“阅读原文”),感兴趣可以联系邮箱 guoxinyu.0372@bytedance.com 。