字节跳动 订阅
北京字节跳动科技有限公司,成立于2012年3月,是最早将人工智能应用于移动互联网场景的科技企业之一,是中国北京的一家信息科技公司,地址位于北京市海淀区知春路甲48号 [1]  。公司以建设“全球创作与交流平台”为愿景。 [2]  字节跳动的全球化布局始于2015年 [3]  ,“技术出海”是字节跳动全球化发展的核心战略 [4]  。字节跳动人工智能实验室成立于2016年 [5]  ,旨在针对人工智能相关领域的长期性和开放性问题进行探索,帮助公司实现对未来发展的构想 [6]  。其独立研发的“今日头条”客户端,通过海量信息采集、深度数据挖掘和用户行为分析,为用户智能推荐个性化信息,从而开创了一种全新的新闻阅读模式。 展开全文
北京字节跳动科技有限公司,成立于2012年3月,是最早将人工智能应用于移动互联网场景的科技企业之一,是中国北京的一家信息科技公司,地址位于北京市海淀区知春路甲48号 [1]  。公司以建设“全球创作与交流平台”为愿景。 [2]  字节跳动的全球化布局始于2015年 [3]  ,“技术出海”是字节跳动全球化发展的核心战略 [4]  。字节跳动人工智能实验室成立于2016年 [5]  ,旨在针对人工智能相关领域的长期性和开放性问题进行探索,帮助公司实现对未来发展的构想 [6]  。其独立研发的“今日头条”客户端,通过海量信息采集、深度数据挖掘和用户行为分析,为用户智能推荐个性化信息,从而开创了一种全新的新闻阅读模式。
信息
公司名称
北京字节跳动科技有限公司
所属行业
信息科技
年营业额
1400余亿元(2019年) [9]
主要产品
今日头条,西瓜视频,抖音
总部地点
北京市海淀区知春路甲48号2号楼10A室 [7]
公司类型
有限责任公司 [7]
经营范围
技术开发、技术推广、技术转让、技术咨询、技术服务等 [7]
成立时间
2012年3月
创始人
张一鸣 [8]
北京字节跳动科技有限公司发展历程
2012年3月,北京字节跳动科技有限公司成立。 [10]  2012年8月,字节跳动旗舰产品今日头条1.0版本上线 [11]  ;2016年3月,字节跳动设立人工智能实验室 [11]  ;2018年8月8日,北京字节跳动科技有限公司已经启动新一轮股权融资,对该公司的估值最高或达750亿美元。 [12]  2018年8月,北京市文化市场行政执法总队对“今日头条”“西瓜视频”开办单位北京字节跳动科技有限公司存在的问题作出警告和罚款的行政处罚。 [13]  2018年10月24日,字节跳动本轮Pre-IPO融资已经完成,投前估值达到750亿美元。 [14]  2019年6月11日,字节跳动入选“2019福布斯中国最具创新力企业榜”。 [15-16]  2019年8月10日,上线头条搜索。 [17]  2019年9月,字节跳动完成对互动百科的收购,持股100%。 [18]  2020年3月,字节跳动旗下新成立全资子公司北京博学互联教育科技有限公司,持续在教育领域进行探索。 [19]  2020年5月,字节跳动在伦敦TikTok欧洲业务中心。 [20]  2020年6月11日,深圳今日头条有限公司以底价10.82亿元,竞得深圳南山区后海总部基地一宗商业用地,标的代号为T107-0090。 [21] 
收起全文
精华内容
下载资源
问答
  • 2020注定是不平凡的一年,在这特殊的一年里,字节跳动技术团队依旧在技术人身边,分享字节跳动的技术实践。本年度字节跳动技术团队共发布了50篇技术干货,其中许多都受到读者的喜爱。值此元旦...

    2020 注定是不平凡的一年,在这特殊的一年里,字节跳动技术团队依旧在技术人身边,分享字节跳动的技术实践。

    本年度字节跳动技术团队共发布了50篇技术干货,其中许多都受到读者的喜爱。值此元旦佳节,我们精选出了其中最受大家欢迎的10 篇文章,供大家回顾,点击文章标题即可阅读全文。

    TOP 1:字节跳动混沌工程实践总结

    #基础架构 #混沌工程

    混沌工程是通过故障注入的方式帮助系统寻找薄弱点,从而提高系统的稳定性。随着微服务、云原生相关技术的发展,分布式系统已经流行在业界各处,但因此也带来了复杂度急剧上升、故障发生难以预测后果、难以避免与验证等挑战。而混沌工程正是通过故障注入等方式为切入点,帮助解决以上问题。本文讨论了字节跳动引入混沌工程以来的相关实践,希望能提供一些参考。

    TOP 2:字节跳动自研万亿级图数据库 & 图计算实践

    #基础架构 #图数据库 #ByteGraph

    2019 年,Gartner 将图列为 2019 年十大数据和分析趋势之一,字节跳动在面对把海量内容推荐给海量用户的业务挑战中,也大量采用图技术。本文将对字节跳动自研的分布式图数据库和图计算专用引擎做深度解析和分享,展示新技术是如何解决业务问题,影响几亿互联网用户的产品体验。

    TOP 3:今日头条品质优化 - 图文详情页秒开实践

    #客户端 #性能优化

    作为一个内容类应用,看新闻读资讯一直是头条用户的核心需求,页面的打开速度直接关系到用户使用头条的核心体验,在头条中,为了更多的承载足够丰富的样式和逻辑下保持多端体验的统一,详情页的内容我们是通过 WebView 来承载的,但 WebView 本身的性能相比 Native 来说比较差,因此,今日头条技术团队一直致力于优化详情页的加载速度。

    经过不断的优化,目前今日头条中详情页在线上的打开体验,从肉眼上基本已经感知不到加载过程。在接下来这篇文章里,我们会逐步拆解和介绍我们对详情页加载优化的思路和实践。

    TOP 4:“���”引发的线上事故

    #Go #服务端

    本文对一起依赖升级 + 异常数据引发的线上事故进行回顾和总结。得出如下反思:

    1. 服务重大版本更新,至少在线下跑一周。

    2. 有问题,第一时间回滚。

    3. 对于工具的使用要规范。如不要随意更改 vendor 文件夹的内容而不同步更新vendor.json文件,反之亦然。

    4. 因为go mod的版本选择以及不遵守开源规范的第三方库作者会让使用者不知不觉、被动地引入一些难以发现的问题。可以使用go mod vendor代替,如果要锁死版本的话,使用 replace。

    TOP 5:iOS性能优化实践:头条抖音如何实现OOM崩溃率下降50%+

    #客户端 #iOS #OOM

    iOS OOM 崩溃在生产环境中的归因一直是困扰业界已久的疑难问题,字节跳动旗下的头条、抖音等产品也面临同样的问题。

    在字节跳动性能与稳定性保障团队的研发实践中,我们自研了一款基于内存快照技术并且可应用于生产环境中的 OOM 归因方案——线上 Memory Graph。基于此方案,3 个月内头条抖音 OOM 崩溃率下降 50%+。

    本文主要分享下该解决方案的技术背景,技术原理以及使用方式,旨在为这个疑难问题提供一种新的解决思路。

    TOP 6:字节跳动在 Go 网络库上的实践

    #Go #基础架构

    RPC 框架作为研发体系中重要的一环,承载了几乎所有的服务流量。本文将简单介绍字节跳动自研网络库 netpoll 的设计及实践;以及实际遇到的问题和解决思路,希望能为大家提供一些参考。

    TOP 7:YARN 在字节跳动的优化与实践

    #YARN #基础架构

    本文从利用率提升、多负载场景优化、稳定性提升、异地多活四个方面介绍了字节跳动在四年来对 Hadoop YARN 进行的一系列的优化,以及生产环境中的实践经验。

    TOP 8:Fastbot:行进中的智能 Monkey

    #测试 Fastbot

    字节自研测试工具 Fastbot,是基于模型的智能 GUI 测试服务。我们分拆服务至客户端和服务端,客户端做 GUI 监听和动作注入,服务端做模型构建和算法决策,使用强化学习研发多种智能搜索算法,有效避免 GUI 测试局部死循环问题,大幅提升测试覆盖率。

    TOP 9:抖音包大小优化-资源优化

    #Android #性能优化 #包大小

    随着业务的快速迭代,抖音 Android 端的包大小爆发式增长。包大小直接影响到下载转化率、推广成本、运行内存和安装时间等因素,因此对 apk 进行瘦身是一件很有必要且收益很大的事情。apk 主要由 dex、resource、asserts、native libraries 和 meta-data 组成,针对每一部分,都可以专项去做包大小优化。

    其中,资源在 apk 包体积中占比很大,针对资源进行优化是包大小优化中很重要的部分。本着追求极致的原则,本文详细阐述抖音 Android 端针对资源部分的优化措施。

    TOP 10:抖音 Android 性能优化系列:Java 内存优化篇

    #Android #性能优化 #JAVA #OOM

    内存作为计算机程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(out of memory)崩溃。抖音作为一款用户使用广泛的产品,需要在各种机器资源上保持优秀的流畅性和稳定性,内存优化是必须要重视的环节。

    本文从抖音 Java OOM 内存优化的治理实践出发,尝试给大家分享一下抖音团队关于 Java 内存优化中的一些思考,包括工具建设、优化方法论。

    互动有奖

    在评论区说出 2020 你最难忘的技术故事。

    截止 2021 年 1 月 4 日 0 点,评论区点赞第 1 名可获得精美新春养生壶,第 2-10 名可获得字节跳动技术学院抱枕。快来参与吧!‍


    欢迎关注「 字节跳动技术团队 」

    简历投递联系邮箱「 tech@bytedance.com 」

     点击阅读原文,快来加入我们吧!

    展开全文
  • 字节跳动开源云原生机器学习平台 Klever

    万次阅读 多人点赞 2020-12-24 11:46:45
    字节跳动基础架构团队基于火山引擎机器学习平台 Clever 及其丰富的行业落地经验,推出开源项目 Klever,以工程化的方式降低智能技术落地门槛,助力企业快速打造智能业务。 项目地址 | kleveross 近年来,智能技术...

    在这里插入图片描述

    字节跳动基础架构团队基于火山引擎机器学习平台 Clever 及其丰富的行业落地经验,推出开源项目 Klever,以工程化的方式降低智能技术落地门槛,助力企业快速打造智能业务。

    项目地址 | kleveross

    近年来,智能技术不论是在学术界还是工业界都取得了突破性进展。机器学习、深度学习开始在各行各业扮演重要角色:业务上,帮助企业优化运营、提高效率、改善客户体验;管理上,参与后台自动化运营,完成数据处理和提取等任务。

    然而,随着越来越多企业开始尝试落地智能技术,一个严峻的问题也逐渐暴露出来:从算法技术选型到模型最终上线,这个过程涉及大量工程化任务对接。算法工程师们掌握着丰富的先进算法,但算法能力的实现通常离不开底层计算资源和系统架构的支撑,如何实现从开发、模型训练、模型管理、模型服务全链路高效、敏捷、自动化管理,进而实现企业的智能化转型,仍是当前智能技术领域亟待解决的问题之一。

    开源机器学习平台 Klever

    针对上述问题,字节跳动基础架构团队多年来就智能技术的工程化问题进行了长期探索。
    2020 年,字节跳动旗下的数字服务与智能科技品牌火山引擎携我们的技术实践落地国内某金融机构,使其模型上线效率提升了 10 倍,GPU 资源使用率提高 50%,自主创新能力大幅提高。

    这类落地最佳实践让我们深刻认识到了智能技术对企业业务持续增长的重要性,也让我们了解到缺乏工程化工具已经成为当下企业应用智能技术的一大掣肘。为此,我们决定推出开源机器学习平台 Klever。

    Klever 是一个支持 OCI(Open Container Initiative)标准存储训练模型、支持在线模型服务部署的云原生机器学习平台。算法科学家可以使用 Klever 进行模型管理模型解析模型转换模型服务,它已经解决了智能技术落地流程中的如下问题:

    • 模型的管理和分发
    • 模型解析和转换
    • 在线模型服务部署和管理

    同时,基于字节跳动在机器学习和云原生开源社区的技术积累,Klever 提供强大、通用的开源技术标准,方便企业无缝迁移线上应用。未来,它还将进一步支持模型开发、模型训练等一系列智能模型开发和管理流程,降低技术落地门槛,助力企业快速打造智能业务、全面实现智能化转型。

    Klever 概述

    系统架构

    Klever 有四个自研发的组件,并依赖三个开源组件:

    • ormb:模型打包、解压、上传、下载工具(点击了解 ormb)
    • model-registry:模型仓库及模型服务 API 管理层
    • modeljob-operator:ModelJob controller,管理模型解析、模型转换任务
    • klever-web:前端组件
    • Istio:开源服务网格组件,模型服务通过 Istio 对外暴露模型服务地址,实现模型服务按内容分流和按比例分流
    • Harbor:模型底层存储组件,对模型配置和模型文件进行分层存储
    • Seldon Core:开源模型服务管理的 Seldon Deployment CRD 的 controller,通过 SeldonDeployment CR 实现模型服务的管理
      在这里插入图片描述

    使用场景

    如前所述,目前机器学习平台 Klever 率先实现的是从模型仓库到模型服务的自动化管理,具体来说,它可以支持以下两种应用场景:

    • 模型管理
      开发的模型在团队内外、公司内外通过 ormb + Harbor 进行管理和分发
      用户如果有现成的模型文件,但是不知道如何构建模型服务,那么可通过将模型导入系统,一键部署模型服务
      用户可导入模型,获取模型的输入输出等模型内部信息

    • 模型服务
      支持简单模型服务和基于流量比例及内容分流的高级模型服务
      用户可通过构建自定义镜像的方式支持自定义模型服务
      支持 GPU 和 非 GPU 模式部署模型服务

    Klever 特性

    Klever 有三大特性。

    首先,通过与 Harbor 结合,它可以满足 OCI 标准的模型仓库管理,用户可以像使用 Docker 管理镜像一样管理机器学习模型。

    其次,整个系统可通过容器化的方式部署在 Kubernetes 容器管理平台之上,用户无需管理模型解析、模型转换、模型服务实际运行在哪台物理机之上,系统会自动调度和运行资源充足的机器,并在模型服务负载较高时自动弹性伸缩。

    最后,由于机器学习在不同训练过程中往往使用不同的数据集,会产生不同的模型,Klever 支持多种模型服务运行时,可将产生的模型用于提供生产环境可用的在线服务。

    在这里插入图片描述

    模型管理

    ORMB 命令行管理

    ORMB 是 Klever 下的一个命令行管理工具子项目,可以像 Docker 管理镜像一样管理模型。它支持 OCI 标准,可以对模型文件和模型属性进行分层存储管理。

    在这里插入图片描述

    支持的命令类型:

    执行 ormb save 保存模型时,我们需要按照如下目录格式管理模型,其中 ormbfile.yaml 文件格式请参考 spec-v1alpha1.md

    在这里插入图片描述

    模型解析与转换

    Klever 通过自动 ModelJob CR 进行模型解析和模型转换任务的管理,模型解析任务需要设置模型的格式及模型的 URI,模型转换任务则需要设置模型源模型格式、目标模型格式及模型 URI。

    在这里插入图片描述
    在这里插入图片描述

    模型解析和转换的执行通过自定义脚本的方式实现,每种类型的模型解析和模型转换任务都有对应的脚本,脚本在 github.com/kleveross/klever-model-registry/tree/master/scripts 下集中管理。解析内容主要包括 inputs、outputs、operators。任务通过 status 的 message 和 phase 字段可以返回给用户任务当前执行的状态,并在异常时返回异常信息。

    模型拉取

    模型解析和转换任务执行之前,需要将模型文件拉取到 Container 中并重新组织为模型服务器需要的目录格式,此过程通过扩展 ORMB 实现了一个 ormb-stororage-initializer 容器,该容器作为任务的 initContainer 存在,它将模型 pull & export 到 /mnt/input 目录下,模型解析和转换任务的容器和 initContainer 共享 /mnt/input 挂载点使用下载的模型。

    支持的模型格式

    在这里插入图片描述

    模型服务

    Klever 基于 Seldon-Core 实现模型服务,创建模型服务会首先创建一个Seldon Deployment,并在其Init Container中通过 ormb-storage-initializer 下载模型。
    若模型为PMML格式,将使用 OpenScoring 镜像启动服务;若模型为其他 Triton Server 支持的模型格式,将使用 Triton Server 镜像启动服务.镜像中会自动通过ormbfile.yaml中的信息生成 Triton Server 所需要的 config.pbtxt 文件。

    模型推理运行时

    当前 Klever 已支持通过 Nvidia Triton Server 模型服务器部署深度学习模型服务,通过 OpenScoring 部署 PMML 格式的传统机器学习模型。我们还在完善对 Spark MLlib、MLFlow 及 XGBoost 模型服务器的支持,会在近期完成。

    在这里插入图片描述

    支持的模型格式

    • SavedModel
    • ONNX
    • GraphDef
    • NetDef
    • TensorRT with GPU
    • TorchScript
    • PMML

    服务访问方式

    部署为模型服务之后,用户如何通过 API 接口进行模型服务的访问呢?对于 Nvidia Triton Server 渲染的模型服务,访问地址为:

    在这里插入图片描述

    对于 OpenScoring 渲染的模型服务,访问地址为:

    在这里插入图片描述

    其中,servingName 为模型服务的名称,在创建模型服务的时候需要指定该名称。

    部署

    讲到这里,大家是否已经迫不及待地想安装和使用 Klever 了呢?Klever 提供一键部署安装的脚本,同时也支持详细的部署文档,你只需要有一个 Kubernetes 环境即可安装部署。

    在这里插入图片描述

    RoadMap

    当前,在字节跳动内部,我们仍在基于各类实践完善云原生机器学习工程化平台的构建想法,丰富 Klever 的功能和内涵。在外部市场,火山引擎推出的商业化版机器学习平台 Clever 已在金融、制造、零售、能源等行业拥有成熟的解决方案,为各行业头部客户业务的持续增长提供赋能,也为 Klever 开源提供重要实战经验。

    当前 Klever 仅支持模型文件及模型元数据的基本属性的存储和管理,以及支持在线模型服务的功能。但是 Klever 的使命远不止于此,未来它将完成从训练到模型、再到模型服务的全链路管理。

    • 权限管理
    • 更完善的模型解析和转换
    • 模型服务器
    • 支持 XGboost
    • 支持 MLflow
    • 支持 Spark MLlib
    • 支持推理图
    • 支持多种发布策略,例如灰度发布、金丝雀发布等
    • 更完善的分流策略
    • 边缘部署

    为了实现这一蓝图,我们在此呼吁开源社区开发者的力量,欢迎更多志同道合的朋友加入我们!

    相关链接

    https://github.com/kleveross/ormb

    https://github.com/kleveross/klever-model-registry

    https://github.com/kleveross/klever-web

    https://github.com/istio/istio

    https://github.com/goharbor/harbor

    https://github.com/SeldonIO/seldon-core


    欢迎关注「 字节跳动技术团队 」

    简历投递联系邮箱「 tech@bytedance.com 」

    展开全文
  • 本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...

    本文选自“字节跳动基础架构实践”系列文章。

    “字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经验与教训,与各位技术同学一起交流成长。

    KiteX 自 2020.04 正式发布以来,公司内部服务数量 8k+,QPS 过亿。经过持续迭代,KiteX 在吞吐和延迟表现上都取得了显著收益。本文将简单分享一些较有成效的优化方向,希望为大家提供参考。

    前言

    KiteX 是字节跳动框架组研发的下一代高性能、强可扩展性的 Go RPC 框架。除具备丰富的服务治理特性外,相比其他框架还有以下特点:集成了自研的网络库 Netpoll;支持多消息协议(Thrift、Protobuf)和多交互方式(Ping-Pong、Oneway、 Streaming);提供了更加灵活可扩展的代码生成器。

    目前公司内主要业务线都已经大范围使用 KiteX,据统计当前接入服务数量多达 8k。KiteX 推出后,我们一直在不断地优化性能,本文将分享我们在 Netpoll 和 序列化方面的优化工作。

    自研网络库 Netpoll 优化

    自研的基于 epoll 的网络库 —— Netpoll,在性能方面有了较为显著的优化。测试数据表明,当前版本(2020.12) 相比于上次分享时(2020.05),吞吐能力 ↑30%,延迟 AVG ↓25%,TP99 ↓67%,性能已远超官方 net 库。以下,我们将分享两点显著提升性能的方案。

    epoll_wait 调度延迟优化

    Netpoll 在刚发布时,遇到了延迟 AVG 较低,但 TP99 较高的问题。经过认真研究 epoll_wait,我们发现结合 polling 和 event trigger 两种模式,并优化调度策略,可以显著降低延迟。

    首先我们来看 Go 官方提供的 syscall.EpollWait 方法:

    func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
    

    这里共提供 3 个参数,分别表示 epoll 的 fd、回调事件、等待时间,其中只有 msec 是动态可调的。

    通常情况下,我们主动调用 EpollWait 都会设置 msec=-1,即无限等待事件到来。事实上不少开源网络库也是这么做的。但是我们研究发现,msec=-1 并不是最优解。

    epoll_wait 内核源码(如下) 表明,msec=-1 比 msec=0 增加了 fetch_events 检查,因此耗时更长。

    static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
                       int maxevents, long timeout)
    {
        ...
        if (timeout > 0) {
           ...
        } else if (timeout == 0) {
            ...
            goto send_events;
        }
    
    fetch_events:
        ...
        if (eavail)
            goto send_events;
    
    send_events:
        ...
    

    Benchmark 表明,在有事件触发的情况下,msec=0 比 msec=-1 调用要快 18% 左右,因此在频繁事件触发场景下,使用 msec=0 调用明显是更优的。

    而在无事件触发的场景下,使用 msec=0 显然会造成无限轮询,空耗大量资源。

    综合考虑后,我们更希望在有事件触发时,使用 msec=0 调用,而在无事件时,使用 msec=-1 来减少轮询开销。伪代码如下:

    var msec = -1
    for {
       n, err = syscall.EpollWait(epfd, events, msec)
       if n <= 0 {
          msec = -1
          continue
       }
       msec = 0
       ...
    }
    

    那么这样就可以了吗?事实证明优化效果并不明显。

    我们再做思考:

    msec=0 仅单次调用耗时减少 50ns,影响太小,如果想要进一步优化,必须要在调度逻辑上做出调整。

    进一步思考:

    上述伪代码中,当无事件触发,调整 msec=-1 时,直接 continue 会立即再次执行 EpollWait,而由于无事件,msec=-1,当前 goroutine 会 block 并被 P 切换。但是被动切换效率较低,如果我们在 continue 前主动为 P 切换 goroutine,则可以节约时间。因此我们将上述伪代码改为如下:

    var msec = -1
    for {
       n, err = syscall.EpollWait(epfd, events, msec)
       if n <= 0 {
          msec = -1
          runtime.Gosched()
          continue
       }
       msec = 0
       ...
    }
    

    测试表明,调整代码后,吞吐量 ↑12%,TP99 ↓64%,获得了显著的延迟收益。

    合理利用 unsafe.Pointer

    继续研究 epoll_wait,我们发现 Go 官方对外提供的 syscall.EpollWait 和 runtime 自用的 epollwait 是不同的版本,即两者使用了不同的 EpollEvent。以下我们展示两者的区别:

    // @syscall
    type EpollEvent struct {
       Events uint32
       Fd     int32
       Pad    int32
    }
    // @runtime
    type epollevent struct {
       events uint32
       data   [8]byte // unaligned uintptr
    }
    

    我们看到,runtime 使用的 epollevent 是系统层 epoll 定义的原始结构;而对外版本则对其做了封装,将 epoll_data(epollevent.data) 拆分为固定的两字段:Fd 和 Pad。那么 runtime 又是如何使用的呢?在源码里我们看到这样的逻辑:

    *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
    
    pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
    

    显然,runtime 使用 epoll_data(&ev.data) 直接存储了 fd 对应结构体(pollDesc)的指针,这样在事件触发时,可以直接找到结构体对象,并执行相应逻辑。而对外版本则由于只能获得封装后的 Fd 参数,因此需要引入额外的 Map 来增删改查结构体对象,这样性能肯定相差很多。

    所以我们果断抛弃了 syscall.EpollWait,转而仿照 runtime 自行设计了 EpollWait 调用,同样采用 unsafe.Pointer 存取结构体对象。测试表明,该方案下 吞吐量 ↑10%,TP99 ↓10%,获得了较为明显的收益。

    Thrift 序列化/反序列化优化

    序列化是指把数据结构或对象转换成字节序列的过程,反序列化则是相反的过程。RPC 在通信时需要约定好序列化协议,client 在发送请求前进行序列化,字节序列通过网络传输到 server,server 再反序列进行逻辑处理,完成一次 RPC 请求。Thrift 支持 Binary、Compact 和 JSON 序列化协议。目前公司内部使用的基本都是 Binary,这里只介绍 Binary 协议。

    Binary 采用 TLV 编码实现,即每个字段都由 TLV 结构来描述,TLV 意为:Type 类型, Lenght 长度,Value 值,Value 也可以是个 TLV 结构,其中 Type 和 Length 的长度固定,Value 的长度则由 Length 的值决定。TLV 编码结构简单清晰,并且扩展性较好,但是由于增加了 Type 和 Length,有额外的内存开销,特别是在大部分字段都是基本类型的情况下有不小的空间浪费。

    序列化和反序列的性能优化从大的方面来看可以从空间和时间两个维度进行优化。从兼容已有的 Binary 协议来看,空间上的优化似乎不太可行,只能从时间维度进行优化,包括:

    1. 减少内存操作次数,包括内存分配和拷贝,尽量预分配内存,减少不必要的开销;

    2. 减少函数调用次数,比如可调整代码结构和 inline 等手段进行优化;

    调研

    根据 go_serialization_benchmarks 的压测数据,我们找到了一些性能卓越的序列化方案进行调研,希望能够对我们的优化工作有所启发。

    通过对 protobuf、gogoprotobuf 和 Cap'n Proto 的分析,我们得出以下结论:

    1. 网络传输中出于 IO 的考虑,都会尽量压缩传输数据,protobuf 采用了 Varint 编码在大部分场景中都有着不错的压缩效果;

    2. gogoprotobuf 采用预计算方式,在序列化时能够减少内存分配次数,进而减少了内存分配带来的系统调用、锁和 GC 等代价;

    3. Cap'n Proto 直接操作 buffer,也是减少了内存分配和内存拷贝(少了中间的数据结构),并且在 struct pointer 的设计中把固定长度类型数据和非固定长度类型数据分开处理,针对固定长度类型可以快速处理;

    从兼容性考虑,不可能改变现有的 TLV 编码格式,因此数据压缩不太现实,但是 2 和 3 对我们的优化工作是有启发的,事实上我们也是采取了类似的思路。

    思路

    减少内存操作

    buffer 管理

    无论是序列化还是反序列化,都是从一块内存拷贝数据到另一块内存,这就涉及到内存分配和内存拷贝操作,尽量避免内存操作可以减少不必要的系统调用、锁和 GC 等开销。

    事实上 KiteX 已经提供了 LinkBuffer 用于 buffer 的管理,LinkBuffer 设计上采用链式结构,由多个 block 组成,其中 block 是大小固定的内存块,构建对象池维护空闲 block,由此复用 block,减少内存占用和 GC。

    刚开始我们简单地采用 sync.Pool 来复用 netpoll 的 LinkBufferNode,但是这样仍然无法解决对于大包场景下的内存复用(大的 Node 不能回收,否则会导致内存泄漏)。目前我们改成了维护一组 sync.Pool,每组中的 buffer size 都不同,新建 block 时根据最接近所需 size 的 pool 中去获取,这样可以尽可能复用内存,从测试来看内存分配和 GC 优化效果明显。

    string / binary 零拷贝

    对于有一些业务,比如视频相关的业务,会在请求或者返回中有一个很大的 Binary 二进制数据代表了处理后的视频或者图片数据,同时会有一些业务会返回很大的 String(如全文信息等)。这种场景下,我们通过火焰图看到的热点都在数据的 copy 上,那我们就想了,我们是否可以减少这种拷贝呢?

    答案是肯定的。既然我们底层使用的 Buffer 是个链表,那么就可以很容易地在链表中间插入一个节点。

    我们就采用了类似的思想,当序列化的过程中遇到了 string 或者 binary 的时候, 将这个节点的 buffer 分成两段,在中间原地插入用户的 string / binary 对应的 buffer,这样可以避免大的 string / binary 的拷贝了。

    这里再介绍一下,如果我们直接用 []byte(string) 去转换一个 string 到 []byte 的话实际上是会发生一次拷贝的,原因是 Go 的设计中 string 是 immutable 的但是 []byte 是 mutable 的,所以这么转换的时候会拷贝一次;如果要不拷贝转换的话,就需要用到 unsafe 了:

    func StringToSliceByte(s string) []byte {
       l := len(s)
       return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
          Data: (*(*reflect.StringHeader)(unsafe.Pointer(&s))).Data,
          Len:  l,
          Cap:  l,
       }))
    }
    

    这段代码的意思是,先把 string 的地址拿到,再拼装上一个 slice byte 的 header,这样就可以不拷贝数据而将 string 转换成 []byte 了,不过要注意这样生成的 []byte 不可写,否则行为未定义。

    预计算

    线上存在某些服务有大包传输的场景,这种场景下会引入不小的序列化 / 反序列化开销。一般大包都是容器类型的大小非常大导致的,如果能够提前计算出 buffer,一些 O(n) 的操作就能降到 O(1),减少了函数调用次数,在大包场景下也大量减少了内存分配的次数,带来的收益是可观的。

    基本类型

    如果容器元素为基本类型(bool, byte, i16, i32, i64, double)的话,由于基本类型大小固定,在序列化时是可以提前计算出总的大小,并且一次性分配足够的 buffer,O(n) 的 malloc 操作次数可以降到 O(1),从而大量减少了 malloc 的次数,同理在反序列化时可以减少 next 的操作次数。

    struct 字段重排

    上面的优化只能针对容器元素类型为基本类型的有效,那么对于元素类型为 struct 的是否也能优化呢?答案是肯定的。

    沿用上面的思路,假如 struct 中如果存在基本类型的 field,也可以预先计算出这些 field 的大小,在序列化时为这些 field 提前分配 buffer,写的时候也把这些 field 顺序统一放到前面写,这样也能在一定程度上减少 malloc 的次数。

    一次性计算

    上面提到的是基本类型的优化,如果在序列化时,先遍历一遍 request 所有 field,便可以计算得到整个 request 的大小,提前分配好 buffer,在序列化和反序列时直接操作 buffer,这样对于非基本类型也能有优化效果。

    定义新的 codec 接口:

    type thriftMsgFastCodec interface {
       BLength() int // count length of whole req/resp
       FastWrite(buf []byte) int
       FastRead(buf []byte) (int, error)
    }
    

    在 Marshal 和 Unmarshal 接口中做相应改造:

    func (c thriftCodec) Marshal(ctx context.Context, message remote.Message, out remote.ByteBuffer) error {
        ...
        if msg, ok := data.(thriftMsgFastCodec); ok {
           msgBeginLen := bthrift.Binary.MessageBeginLength(methodName, thrift.TMessageType(msgType), int32(seqID))
           msgEndLen := bthrift.Binary.MessageEndLength()
           buf, err := out.Malloc(msgBeginLen + msg.BLength() + msgEndLen)// malloc once
           if err != nil {
              return perrors.NewProtocolErrorWithMsg(fmt.Sprintf("thrift marshal, Malloc failed: %s", err.Error()))
           }
           offset := bthrift.Binary.WriteMessageBegin(buf, methodName, thrift.TMessageType(msgType), int32(seqID))
           offset += msg.FastWrite(buf[offset:])
           bthrift.Binary.WriteMessageEnd(buf[offset:])
           return nil
        }
        ...
    }
    
    func (c thriftCodec) Unmarshal(ctx context.Context, message remote.Message, in remote.ByteBuffer) error {
        ...
        data := message.Data()
    if msg, ok := data.(thriftMsgFastCodec); ok && message.PayloadLen() != 0 {
       msgBeginLen := bthrift.Binary.MessageBeginLength(methodName, msgType, seqID)
       buf, err := tProt.next(message.PayloadLen() - msgBeginLen - bthrift.Binary.MessageEndLength()) // next once
       if err != nil {
          return remote.NewTransError(remote.PROTOCOL_ERROR, err.Error())
       }
       _, err = msg.FastRead(buf)
       if err != nil {
          return remote.NewTransError(remote.PROTOCOL_ERROR, err.Error())
       }
       err = tProt.ReadMessageEnd()
       if err != nil {
          return remote.NewTransError(remote.PROTOCOL_ERROR, err.Error())
       }
       tProt.Recycle()
       return err
       }
       ...
    }
    

    生成代码中也做相应改造:

    func (p *Demo) BLength() int {
            l := 0
            l += bthrift.Binary.StructBeginLength("Demo")
            if p != nil {
                    l += p.field1Length()
                    l += p.field2Length()
                    l += p.field3Length()
        ...
            }
            l += bthrift.Binary.FieldStopLength()
            l += bthrift.Binary.StructEndLength()
            return l
    }
    
    func (p *Demo) FastWrite(buf []byte) int {
            offset := 0
            offset += bthrift.Binary.WriteStructBegin(buf[offset:], "Demo")
            if p != nil {
                    offset += p.fastWriteField2(buf[offset:])
                    offset += p.fastWriteField4(buf[offset:])
                    offset += p.fastWriteField1(buf[offset:])
                    offset += p.fastWriteField3(buf[offset:])
            }
            offset += bthrift.Binary.WriteFieldStop(buf[offset:])
            offset += bthrift.Binary.WriteStructEnd(buf[offset:])
            return offset
    }
    

    使用 SIMD 优化 Thrift 编码

    公司内广泛使用 list<i64/i32> 类型来承载 ID 列表,并且 list<i64/i32> 的编码方式十分符合向量化的规律,于是我们用了 SIMD 来优化 list<i64/i32> 的编码过程。

    我们使用了 avx2,优化后的结果比较显著,在大数据量下针对 i64 可以提升 6 倍性能,针对 i32 可以提升 12 倍性能;在小数据量下提升更明显,针对 i64 可以提升 10 倍,针对 i32 可以提升 20 倍。

    减少函数调用

    inline

    inline 是在编译期间将一个函数调用原地展开,替换成这个函数的实现,它可以减少函数调用的开销以提高程序的性能。

    在 Go 中并不是所有函数都能 inline,使用参数-gflags="-m"运行进程,可显示被 inline 的函数。以下几种情况无法内联:

    1. 包含循环的函数;

    2. 包含以下内容的函数:闭包调用,select,for,defer,go 关键字创建的协程;

    3. 超过一定长度的函数,默认情况下当解析 AST 时,Go 申请了 80 个节点作为内联的预算。每个节点都会消耗一个预算。比如,a = a + 1 这行代码包含了 5 个节点:AS, NAME, ADD, NAME, LITERAL。当一个函数的开销超过了这个预算,就无法内联。

    编译时通过指定参数-l可以指定编译器对代码内联的强度(go 1.9+),不过这里不推荐大家使用,在我们的测试场景下是 buggy 的,无法正常运行:

    // The debug['l'] flag controls the aggressiveness. Note that main() swaps level 0 and 1, making 1 the default and -l disable. Additional levels (beyond -l) may be buggy and are not supported.
    //      0: disabled
    //      1: 80-nodes leaf functions, oneliners, panic, lazy typechecking (default)
    //      2: (unassigned)
    //      3: (unassigned)
    //      4: allow non-leaf functions
    

    内联虽然可以减少函数调用的开销,但是也可能因为存在重复代码,从而导致 CPU 缓存命中率降低,所以并不能盲目追求过度的内联,需要结合 profile 结果来具体分析。

    go test -gcflags='-m=2' -v -test.run TestNewCodec 2>&1 | grep "function too complex" | wc -l
    48
    
    go test -gcflags='-m=2 -l=4' -v -test.run TestNewCodec 2>&1 | grep "function too complex" | wc -l
    25
    
    

    从上面的输出结果可以看出,加强内联程度确实减少了一些"function too complex",看下 benchmark 结果:

    上面开启最高程度的内联强度,确实消除了不少因为“function too complex”带来无法内联的函数,但是压测结果显示收益不太明显。

    测试结果

    我们构建了基准测试来对比优化前后的性能,下面是测试结果。

    环境:Go 1.13.5 darwin/amd64 on a 2.5 GHz Intel Core i7 16GB

    小包

    data size: 20KB

    大包

    data size: 6MB

    无拷贝序列化

    在一些 request 和 response 数据较大的服务中,序列化和反序列化的代价较高,有两种优化思路:

    1. 如前文所述进行序列化和反序列化的优化

    2. 以无拷贝序列化的方式进行调用

    调研

    通过无拷贝序列化进行 RPC 调用,最早出自 Kenton Varda 的 Cap'n Proto 项目,Cap'n Proto 提供了一套数据交换格式和对应的编解码库。

    Cap'n Proto 本质上是开辟一个 bytes slice 作为 buffer ,所有对数据结构的读写操作都是直接读写 buffer,读写完成后,在头部添加一些 buffer 的信息就可以直接发送,对端收到后即可读取,因为没有 Go 语言结构体作为中间存储,所有无需序列化这个步骤,反序列化亦然。

    简单总结下 Cap'n Proto 的特点:

    1. 所有数据的读写都是在一段连续内存中

    2. 将序列化操作前置,在数据 Get/Set 的同时进行编解码

    3. 在数据交换格式中,通过 pointer(数据存储位置的 offset)机制,使得数据可以存储在连续内存的任意位置,进而使得结构体中的数据可以以任意顺序读写

      1. 对于结构体的固定大小字段,通过重新排列,使得这些字段存储在一块连续内存中

      2. 对于结构体的不定大小字段(如 list),则通过一个固定大小的 pointer 来表示,pointer 中存储了包括数据位置在内的一些信息

    首先 Cap'n Proto 没有 Go 语言结构体作为中间载体,得以减少一次拷贝,然后 Cap'n Proto 是在一段连续内存上进行操作,编码数据的读写可以一次完成,因为这两个原因,使得 Cap' Proto 的性能表现优秀。

    下面是相同数据结构下 Thrift 和 Cap'n Proto 的 Benchmark,考虑到 Cap'n Proto 是将编解码操作前置了,所以对比的是包括数据初始化在内的完整过程,即结构体数据初始化+(序列化)+写入 buffer +从 buffer 读出+(反序列化)+从结构体读出数据。

    struct MyTest {
        1: i64 Num,
        2: Ano Ano,
        3: list<i64> Nums, // 长度131072 大小1MB
    }
    
    struct Ano {
        1: i64 Num,
    }
    

    (反序列化)+读出数据,视包大小,Cap'n Proto 性能大约是 Thrift 的 8-9 倍。写入数据+(序列化),视包大小,Cap'n Proto 性能大约是 Thrift 的 2-8 倍。整体性能 Cap' Proto 性能大约是 Thrift 的 4-8 倍。

    前面说了 Cap'n Proto 的优势,下面总结一下 Cap'n Proto 存在的一些问题:

    1. Cap'n Proto 的连续内存存储这一特性带来的一个问题:当对不定大小数据进行 resize ,且需要的空间大于原有空间时,只能在后面重新分配一块空间,导致原来数据的空间成为了一个无法去掉的 hole 。这个问题随着调用链路的不断 resize 会越来越严重,要解决只能在整个链路上严格约束:尽量避免对不定大小字段的 resize ,当不得不 resize 的时候,重新构建一个结构体并对数据进行深拷贝。

    2. Cap'n Proto 因为没有 Go 语言结构体作为中间载体,使得所有的字段都只能通过接口进行读写,用户体验较差。

    Thrift 协议兼容的无拷贝序列化

    Cap'n Proto 为了更好更高效地支持无拷贝序列化,使用了一套自研的编解码格式,但在现在 Thrift 和 ProtoBuf 占主流的环境中难以铺开。为了能在协议兼容的同时获得无拷贝序列化的性能,我们开始了 Thrift 协议兼容的无拷贝序列化的探索。

    Cap'n Proto 作为无拷贝序列化的标杆,那么我们就看看 Cap'n Proto 上的优化能否应用到 Thrift 上:

    1. 自然是无拷贝序列化的核心,不使用 Go 语言结构体作为中间载体,减少一次拷贝。此优化点是协议无关的,能够适用于任何已有的协议,自然也能和 Thrift 协议兼容,但是从 Cap'n Proto 的使用上来看,用户体验还需要仔细打磨一下。

    2. Cap'n Proto 是在一段连续内存上进行操作,编码数据的读写可以一次完成。Cap'n Proto 得以在连续内存上操作的原因:有 pointer 机制,数据可以存储在任意位置,允许字段可以以任意顺序写入而不影响解码。但是一方面,在连续内存上容易因为误操作,导致在 resize 的时候留下 hole,另一方面,Thrift 没有类似于 pointer 的机制,故而对数据布局有着更严格的要求。这里有两个思路:

      1. 坚持在连续内存上进行操作,并对用户使用提出严格要求:1. resize 操作必须重新构建数据结构 2. 当存在结构体嵌套时,对字段写入顺序有着严格要求(可以想象为把一个存在嵌套的结构体从外往里展开,写入时需要按展开顺序写入),且因为 Binary 等 TLV 编码的关系,在每个嵌套开始写入时,需要用户主动声明(如 StartWriteFieldX)。

      2. 不完全在连续内存上操作,局部内存连续,可变字段则单独分配一块内存,既然内存不是完全连续的,自然也无法做到一次写操作便完成输出。为了尽可能接近一次写完数据的性能,我们采取了一种链式 buffer 的方案,一方面当可变字段 resize 时只需替换链式 buffer 的一个节点,无需像 Cap'n Proto 一样重新构建结构体,另一方面在需要输出时无需像 Thrift 一样需要感知实际的结构,只要把整个链路上的 buffer 写入即可。

    先总结下目前确定的两个点:1. 不使用 Go 语言结构体作为中间载体,通过接口直接操作底层内存,在 Get/Set 时完成编解码 2. 通过链式 buffer 存储数据

    然后让我们看下目前还有待解决的问题:

    1. 不使用 Go 语言结构体后带来的用户体验劣化

      1. 解决方案:改善 Get/Set 接口的使用体验,尽可能做到和 Go 语言结构体同等的易用

    2. Cap'n Proto 的 Binary Format 是针对无拷贝序列化场景专门设计的,虽然每次 Get 时都会进行一次解码,但是解码代价非常小。而 Thrift 的协议(以 Binary 为例),没有类似于 pointer 的机制,当存在多个不定大小字段或者存在嵌套时,必须顺序解析而无法直接通过计算偏移拿到字段数据所在的位置,而每次 Get 都进行顺序解析的代价过于高昂。

      1. 解决方案:我们在表示结构体的时候,除了记录结构体的 buffer 节点,还加了一个索引,里面记录了每个不定大小字段开始的 buffer 节点的指针。

    下面是目前的无拷贝序列化方案与 FastRead/Write,在 4 核下的极限性能对比测试:

    测试结果概述:

    1. 小包场景,无序列化性能表现较差,约为 FastWrite/FastRead 的 85%。

    2. 大包场景,无序列化性能表现较好,4K 以上的包较 FastWrite/FastRead 提升 7%-40%。

    后记

    希望以上的分享能够对社区有所帮助。同时,我们也在尝试 share memory-based IPC、io_uring、tcp zero copy 、RDMA 等,更好地提升 KiteX 性能;重点优化同机、同容器的通讯场景。欢迎各位感兴趣的同学加入我们,共同建设 Go 语言生态!

    参考资料

    1. https://github.com/alecthomas/go_serialization_benchmarks

    2. https://capnproto.org/

    3. https://software.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/compiler-reference/intrinsics/intrinsics-for-intel-advanced-vector-extensions-2/intrinsics-for-shuffle-operations-1/mm256-shuffle-epi8.html

    字节跳动基础架构团队

    字节跳动基础架构团队是支撑字节跳动旗下包括抖音、今日头条、西瓜视频、火山小视频在内的多款亿级规模用户产品平稳运行的重要团队,为字节跳动及旗下业务的快速稳定发展提供了保证和推动力。

    公司内,基础架构团队主要负责字节跳动私有云建设,管理数以万计服务器规模的集群,负责数万台计算/存储混合部署和在线/离线混合部署,支持若干 EB 海量数据的稳定存储。

    文化上,团队积极拥抱开源和创新的软硬件架构。我们长期招聘基础架构方向的同学,具体可参见   job.bytedance.com (文末“阅读原文”),感兴趣可以联系邮箱: tech@bytedance.com ,邮件标题: 姓名 - 工作年限 - 基础架构 。



    欢迎关注「 字节跳动技术团队 」

    简历投递联系邮箱「 tech@bytedance.com 」

     点击阅读原文,快来加入我们吧!

    展开全文
  • 本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...

    本文选自“字节跳动基础架构实践”系列文章。

    “字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经验与教训,与各位技术同学一起交流成长。

    线上流量引流线下环境是一个通用需求,广泛应用于功能测试、压测等场景。本文介绍了引流系统在字节跳动的发展过程和系统设计,希望能给大家带来一点新的思考和收获。

    1. 背景

    AB Test (diff 测试) 是在互联网行业中比较常用的验证方法,例如 Google 通过 AB 实验针对广告和推荐的效果做验证,Twitter 研发了 Diffy ,把 Diff 验证能力应用到了 API 接口的质量保障上。通常 AB Test 有两种形式,一种是线上多个服务版本,通过接入侧分流 AB 来做实验,但对于广告这类场景,一旦某个模型有问题,就会造成资损。另外一个模式是通过线上的流量复制回放到内部环境,这种方式对于生产是绝对安全的,例如 Twitter 的 AB 验证服务 diffy 就是走的这个模式。今天字节内部推荐,广告,等很多业务线都是通过线上流量实时回放的模式做 AB 实验。为了满足业务的需求,我们自研了一套线上流量录制回放系统 ByteCopy 来支撑业务的海量流量吞吐和不断产生的对于流量录制回放的新需求。

    这篇文章会从业务场景、系统架构、问题分析等几个方面来介绍 ByteCopy 这套系统的演进过程。

    2. 基于 TCPCopy 构建第一代引流系统

    2.1 业务需求

    刚开始业务的需求还是比较简单的,就是希望在业务方部署好了目标服务 (HTTP 和 RPC) 后,引流系统可以把对应线上生产的流量复制一份并转发过去,并且整个引流过程可以灵活管控,只在需要流量的时候开启。

    2.2 系统选型

    从引流自身来看,主要分为 2 种类型,主路复制和旁路复制,我们分别来分析一下这两种模式的优劣。

    2.2.1 主路复制

    主路复制是指在调用链中进行流量复制:一种是在业务逻辑中进行流量复制,如在调用 API/RPC 过程中,由业务方编写代码逻辑,记录 request / response 信息内容;另外一种是在框架(如使用 Dubbo、Service Mesh 等网络框架)处理逻辑中进行复制。

    优点

    • 可以高度结合业务逻辑,实现细粒度定制化流量复制,比如可以只针对某个用户的流量进行复制,可以最大程度上提升引流源上的有效流量采集比。

    缺点

    • 业务逻辑与引流逻辑耦合度较高,功能上相互影响。

    • 每个请求都需要进行额外引流处理,对业务流程存在性能影响。

    适用场景

    • 对于流量有细粒度筛选要求的,或与业务逻辑有关,可以选择主路复制;如 Service Mesh 中根据染色标记,进行流量复制。

    2.2.2 旁路复制

    对比主路复制,旁路复制突出了业务无感知的特点,一般是由第三方服务在网络协议栈中,监听复制流量

    优点

    • 与业务解耦,可以独立部署升级引流模块,业务方无需关注引流功能实现;通过在协议栈底层进行流量复制,性能较好。

    缺点

    • 4 层网卡层面的网络包抓取后,仍需要进行数据包的重组和解析,需要额外的消耗计算资源。

    • 往往需要全量抓包解析再进行筛选,无法结合业务逻辑进行定制化的采样。

    开源方案 TCPCopy

    虽然 Linux 提供了 libpcap 这样的底层 packet capture 库,不过本着快速交付业务需求的目标,我们选择了开源的 TCPCopy 来作为整个引流系统的核心基础。TCPCopy 在这里就不多介绍,只在下面附上一张简单的架构图,其中 TCPCopy 和 Intercept 是 TCPCopy 的两个组件,相关细节感兴趣的同学可以自行查找资料。

    TCPCopy 的主要优势:

    • 协议无感知,可以透明转发,能够支持基于 TCP 的任意应用层协议,如 MySQL,Kafka,Redis 等

    • 实时转发,延时较低

    • 可以保留原始请求 IP 端口信息,测试服务器可用于统计

    同时,也具有以下不足:

    • 无法动态添加多个下游服务器

    • 由于透明转发,不做协议解析,无法发现数据异常,如部分 TCP 包丢失,测试服务器将收到不完整的数据;此外,也无法对应用层数据进行筛选和修改进行修改

    • 核心组件设计时未进行多线程设计,处理能力存在瓶颈

    • 需要修改 iptables 来丢弃下游服务的回包,用在生产或公共的测试环境存在较大风险

    为了满足字节的需求,我们在整体架构上引入了一些其他组件来弥补 TCPCopy 自身的不足。

    2.3 系统架构

    为了解决 TCPCopy 存在的不足,我们在通过 TCPCopy 直接进行流量转发的方案基础上又进行了一些优化。

    首先,在 TCPCopy 和被测服务之间额外引入了七层代理进行数据转发。七层代理本身可以校验请求的完整性,避免不完整的请求被转发到测试服务对干扰测试造成干扰。

    此外,我们将七层代理和 TCPCopy 的 intercept 组件部署在一批专用于流量转发的服务器上,进行转发任务时只需要修改这批服务器的 iptables ,而被测服务只需在测试机上正常运行,不用进行额外配置,因此可以尽量避免修改 iptables 带来的风险。

    为了能够更好地融入公司的技术生态系统,同时支持更丰富的测试场景,我们还专门实现了一个用于测试的七层代理。具体加入了以下能力:

    • 接入了公司的服务发现框架,被测实例只需注册指定的服务名,就可以收到代理发送的流量。因此流量可以被同时转发到多个被测实例,也可以动态地添加或删除被测实例

    • 支持流量过滤。从收到的流量中筛选出指定方法的流量进行转发。比如可以过滤掉转发流量中包含写操作的流量,从而避免对存储造成污染

    • 引入流控机制。支持对转发的流量进行限速,以及通过将收到的请求多次重复发送实现加压,从而支持简单的压测场景

    最后,为了让引流功能变得易用,我们把 TCPCopy 的两个组件,以及我们的七层代理进行了封装,打包成了一个平台提供给用户。用户只需要指定引流源和被测服务的 IP 和端口,以及引流任务的持续时间,即可进行一次引流测试。

    线上的整体架构大概如下图所示,用户提交任务后,我们会负责进行各个组件的调度、配置和部署,将流量从线上转发到用户的待测实例。

    2.5 存在的问题

    随着规模的逐渐发展和更多用户场景的提出,这套架构也逐渐暴露出了一些问题。

    2.5.1 TCPCopy 存在性能问题

    TCPCopy 在实现上没有进行多线程的设计,因此实际的转发吞吐能力较为有限,对于一些高带宽的测试场景无法很好地支持。

    2.5.2 现有实现无法支持响应录制等更多场景

    TCPCopy 定位只是请求复制转发工具,只会复制线上流量的请求部分,而不会复制线上流量的响应。我们接到了一些想要对线上流量进行分析的用户的需求,他们希望能够同时收集线上流量的请求和响应,TCPCopy 没法支持这类场景。

    3. 自研 ByteCopy,开启海量流量和复杂业务场景的支持

    前面提到了第一代引流系统存在一些性能和灵活性的问题,与此同时业务也提出了一些新的需求,例如支持 MySQL 协议,支持历史流量的存储和回放等。考虑到在现有的 TCPCopy 的架构上很难做扩展,所以我们考虑推翻现有架构,重新构建字节新一代的引流系统 - ByteCopy (寓意是复制线上每一个字节)

    在以上演进的基础上,我们可以按职责把七层流量复制大致分解为下面三个模块

    • 流量采集

    • 流量解析

    • 流量应用

    针对 3 个模块我们分别展开介绍

    3.1 流量采集

    流量采集模块会依据服务部署的平台以不同方式拉起,如在 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 实例这种冗余部署。我们的做法是把用户的请求规格和采集模块的实际部署解耦,用户提交一个规格请求后,会先和已有的规格合并,得到一个最小部署方案,然后更新部署状态。

    3.2 流量解析

    引流源采集上来的原始流量还是第四层协议,为了支持一些更复杂的功能,比如过滤,多路输出,历史流量存储,流量查询及流量可视化等等,我们需要将四层流量解析到七层。字节跳动内部服务使用得比较多的协议是 Thrift 和 HTTP ,这两个根据协议规范即可很好地完成解析。

    流量解析有一个难点是判断流量的边界,区别于 HTTP/2 等的 Pipeline 连接复用传输形式,Thrift 和 HTTP/1.X 在单条连接上严格按照请求-响应对来进行传输,因此可以通过请求和响应的切换分隔出完整的请求或响应流量。

    3.3 流量应用

    对于线上采集的流量,不同用户会有不同的业务用途,如压测平台可能希望将流量先持久化到 Kafka,然后利用 Kafka 的高吞吐发压;有些研发同学只是简单从线上引一份流量转发到自己的开发环境做新特性测试;有些同学希望转发 QPS 能达到一定水位以实现压测的目的;还有的是特定流量会触发线上 coredump ,他们希望把这段流量录制下来线下 debug 等等。针对不同的场景,我们实现了若干流量输出形式。

    下面会着重介绍转发和存储。

    3.3.1 转发

    结构如上图,emitter 会在 zookeeper 上注册自身,scheduler 感知到 emitter 节点信息,将任务根据各个 emitter 节点的标签和统计信息过滤/打分,然后调度到最合适的节点上。这里有个问题是为什么不直接使用无状态服务,由每个 emitter 实例均等地转发,而采用 sharding 方案,主要是基于下面几点考虑:

    1. 如果每个任务均摊到所有实例上执行,那每个实例需要和全部下游 endpoint 建立连接,在海量任务下的 goroutine、连接、内存等资源占用是不可接受的

    2. 通过将单个任务的处理收敛到少数实例上,提高了对单个 endpoint 的请求密度,从而避免因为连接 idle 时间过长被对端 close,复用了连接。

    由于 emitter 对性能比较敏感,我们为此也做了很多优化,比如使用了 fasthttp 的 goroutine 池避免频繁申请 goroutine,对连接的 reader/writer 对象池化,动态调节每个 endpoint 的工作线程数量以自适应用户指定 QPS,避免 goroutine 浪费及闲置长连接退化成短连接,全程无锁化,通过 channel+select 做线程同步和数据传递等等。

    3.3.2 存储

    存储分为了两层,数据层和索引层,采用双写模型,并有定时任务从数据层纠错索引层保证两者的最终一致性。存储需 要支持回放和查询两种语义,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 等等,查看特定请求的内容。其他场景功能还有待进一步发掘。

    3.4 业务场景支持

    3.4.1 支持通用的 diff 能力

    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 的,即使是一个写的请求,也可以做到对于线上没有任何影响。

    4. 未来展望

    4.1 更精准的流量采集

    前面提到,在进行出口流量的采集时,会对下游依赖服务的所有实例的 IP:端口 进行抓包。而实际的生产环境中,同一台服务器上,可能会部署具有相同下游依赖的多个服务,只依赖四层数据,无法判断抓到的数据到底来自哪一个服务,会造成抓包、处理和转发流程中都会存在资源浪费的问题。目前来看基于网卡抓包的方案应该没法很好地解决这个问题,我们也在尝试探索一些其他的流量采集的方案,比如探索用 ebpf 进行进程级别的流量采集。

    4.2 引流回放系统的重新定义

    现阶段我们的引流回放系统只会根据用户的配置被动进行流量采集,而为了及时拿到流量进行测试,用户一般都会选择实时引流进行测试。而实际上并不是所有的场景都一定需要实时的流量进行测试,我们在规划逐步将引流回放系统从一个按照用户要求进行流量转发回放的工具,转变为一个线上复制流量的取用的平台。

    在流量存储能力的基础上,对于有测试需求的服务,平台主动错峰、定时发起流量录制任务,从而维护一个不断更新的流量池,用户需要流量时直接从流量池中取用,这样一来,既可以尽量避免引流操作对和线上业务抢占计算资源,也可以使得流量的可用性更高。

    4.3 特定场景下的流量存储优化

    随着基于流量录制回放的上层应用的完善,为了更多的业务方便接入试用,我们正在考虑朝着常态化的引流去演进。这个势必对我们的流量存储带来新的挑战,无论是的数据规模,存储形态以及查询性能。我们希望可以基于现有架构的存储系统,构建流量存储的解决方案,支持海量数据吞吐的同时,能够支持点查(基于 TraceId ),和 time-range scan 等多种复杂的高性能查询方式。另外我们也在积极和安全团队合作,确保相关核心流量数据在存储时候能够实现脱敏,同时不断强化对于流量存储使用的安全审计。

    5. 总结

    到今天为止,ByteCopy 系统已经支撑了字节绝大部分业务线在不同场景下的各种引流需求, 我们一直在努力丰富 ByteCopy 的功能场景,不断提升系统稳定性和吞吐容量,此外我们也在积极构建 ByteMock 等自研的研发组件,通过和 ByteCopy 形成组合拳,解锁生产流量在研发活动中更多的使用场景,帮助业务团队更好地去构建各种有趣的产品。

    更多分享

    字节跳动表格存储中的事务

    iOS 大解密:玄之又玄的 KVO

    今日头条 Android '秒' 级编译速度优化

    字节跳动分布式表格存储系统的演进

    字节跳动基础架构团队

    字节跳动基础架构团队是支撑字节跳动旗下包括抖音、今日头条、西瓜视频、火山小视频在内的多款亿级规模用户产品平稳运行的重要团队,为字节跳动及旗下业务的快速稳定发展提供了保证和推动力。

    公司内,基础架构团队主要负责字节跳动私有云建设,管理数以万计服务器规模的集群,负责数万台计算/存储混合部署和在线/离线混合部署,支持若干 EB 海量数据的稳定存储。

    文化上,团队积极拥抱开源和创新的软硬件架构。我们长期招聘基础架构方向的同学,具体可参见  job.bytedance.com (文末“阅读原文”),感兴趣可以联系邮箱  guoxinyu.0372@bytedance.com 。。


    欢迎关注「字节跳动技术团队」

     点击阅读原文,快来加入我们吧!

    展开全文
  • 本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...
  • 字节跳动上班有多累?

    万次阅读 多人点赞 2020-06-07 12:09:06
    字节跳动上班有多累?前言面试邀约面试过程一面的出乎意料二面的游刃有余三面的压力测试立FLAG入职字节一年 人员三年临近崩溃第一次崩溃第二次崩溃第三次崩溃最新状态 前言      大部分的人都渴望能进入字节跳动...
  • 字节跳动10万节点HDFS集群多机房架构演进之路

    千次阅读 多人点赞 2021-07-03 00:39:15
    字节跳动采用的是横跨多个机房的联邦大集群部署模式,即 HDFS 只有一个集群,这个集群有多个 nameservice,但是底层的 DN 是横跨 A/B/C 3 个机房的 ,由于社区版 HDFS 没有机房感知相关的支持,因此字节跳动 HDFS ...
  • 本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...
  • 字节跳动在 Go 网络库上的实践

    万次阅读 2020-05-11 10:01:44
    本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...
  • 本文讨论了微前端在字节跳动的应用情况,内容主要分析了微前端具体落地的步骤和两年来的使用情况。其中分析的部分主要讲到一些实际问题和我们的应对,落地情况强调了实现的过程。特别讲到很多在我们观...
  • 字节跳动全链路压测(Rhino)的实践

    万次阅读 2020-10-12 09:58:18
    我们广泛吸收了各大互联网公司的全链路压测建设经验,并基于字节跳动业务需求,设计开发了一个全链路压测系统 Rhino。 2.1 架构图 Rhino 平台作为公司级的全链路压测平台,它的目标是对全公司所有业务,提供单服务、...
  • 毕业前的校招季,Lucas投递了字节跳动图形图像算法方向的岗位,在收割了数家国际大厂的算法offer后,因为看好抖音的发展、热爱这个产品,选择加入字节跳动。 Lucas的校招经验: 我那年的笔试时长大约3个小时,有6、...
  • 字节跳动视频编解码面经

    万次阅读 多人点赞 2019-09-27 16:26:17
    三四月份投了字节跳动的实习(图形图像岗位),然后hr打电话过来问了一下会不会opengl,c++,shador,当时只会一点c++,其他两个都不会,也就直接被拒了。 七月初内推了字节跳动的提前批,因为内推没有具体的岗位,...
  • 大三实习生,字节跳动面经分享,已拿Offer

    万次阅读 多人点赞 2020-03-29 21:32:51
    其实准备字节跳动的面试还是需要着重准备基础部分(数据结构、算法、操作系统、计算机网络),另外对做算法题的要求很高,如果在看到题目的时候能直接给出这个题目的最优时空复杂度解法,面试官对你的评价会很高。...
  • 字节跳动 EB 级 HDFS 实践

    万次阅读 2019-12-31 09:47:14
    本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...
  • 字节跳动表格存储中的事务

    千次阅读 2020-08-03 09:55:00
    本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经...
  • 字节跳动面经(一、二、三+大boss+hr面)

    万次阅读 多人点赞 2019-07-18 16:15:08
    先介绍一下,本科和研究生都不是计算机专业,现在是学通信,然后做图像处理,可能面试官看我不是科班出身没有问太多计算机相关的问题,因为第一次找工作,字节的游戏专场又是最早开始的,就投递了,投递的是游戏测试...
  • 本文主要围绕 MQ-Hive 场景,针对目前字节跳动内已有解决方案的痛点,提出基于 Flink 的实时解决方案,并介绍新方案在字节跳动内部的使用现状。 已有方案及痛点 字节跳动内已有解决方案如下图所示,主要分了两个...
  • 今天,字节跳动正式宣布开源 CloudWeGo。这是一套以 Go 语言为核心、专注于微服务通信与治理的中间件集合,具有高性能、可扩展、高可靠的特点。项目地址:https://github....
  • 字节跳动在代码覆盖率监测平台基础上,建立了SmartEye-智能化精准中台,帮助研发团队快速定位代码变更影响域,精准推荐测试用例/自动化巡检任务,为业务减负,为研发团队提效。 背景 在日常的研发活动中,我们经常...
  • 从2012到2021,今天的字节跳动已经走过了9周年,员工人数也已经突破了10W。 不过,回想九年前的创业早期,整个公司只有十几个人,探索方向期间,创始团队一口气做了包括今日头条在内的12款App,但支持这么多...
  • YARN 在字节跳动的优化与实践

    千次阅读 2020-06-23 10:02:56
    导读:本文从利用率提升、多负载场景优化、稳定性提升、异地多活四个方面介绍了字节跳动在四年来对 Hadoop YARN 进行的一系列的优化,以及生产环境中的实践经验。1.YARN 简介1...
  • 「互联网」职级、薪酬、绩效全认知 第5期1、全球员工总数字节的员工数量目前超过5万人2、岗位职级字节跳动的职级研发序列一共10级:字节跳动应届生一般是1-1级,中级工程师是1-...
  • 字节跳动AI-LAB智能语音团队对RNN-T模型做了较多尝试和探索,通过数据的不断积累、算法的持续优化,基于RNN-T模型的自研端到端语音识别系统获得了显著优于CTC模型的性能,被应用到了会议转录、同传等多个业务中,都...
  • 本文选自“字节跳动基础架构实践”系列文章。“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队...
  • 活动简介12 月 19 日,GTC 智能增长技术专场,火山引擎将以「智能增长」为主题,为大家带来字节跳动在机器学习领域沉淀的技术经验,智能平台、数据智能、语音识别、联邦学习等场景的前沿应...
  • MBox 是字节跳动抖音基础技术、Client Infra-DevOps根据移动端研发出现的现状与问题,结合移动端研发工具相关实践经验,自研的一款面向移动端开发者的研发工具链产品。目前,M...
  • 我们找到了通过校招入职字节跳动的算法、后端、客户端、前端、大数据、测试方向的6位技术同学,分享他们校招中遇到的难题和拿到offer的独家秘笈。 来一起看看他们是如何过关斩将,拿到字节跳动offer的吧。 ...
  • 这是字节跳动公司内部产品体系使用的官方图标库,如今可以免费商用了。 关于IconPark 看看官方描述: IconPark图标库是一个通过技术驱动矢量图标样式的开源图标库,可以实现根据单一SVG源文件变换出多种主题, 具备...
  • 已经拿到小米、网易、爱奇艺、百度与字节跳动的offer,并在12月签下字节跳动(我会说字节跳动是我第一次面试而且一发命中吗?)。在入职后与面试官带着我去认识团队成员时,介绍道:你问她什么问题她都会。 所以本文...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 46,507
精华内容 18,602
关键字:

字节跳动