本文选自“字节跳动基础架构实践”系列文章。
“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经验与教训,与各位技术同学一起交流成长。
2019 年,Gartner 将图列为 2019 年十大数据和分析趋势之一,字节跳动在面对把海量内容推荐给海量用户的业务挑战中,也大量采用图技术。本文将对字节跳动自研的分布式图数据库和图计算专用引擎做深度解析和分享,展示新技术是如何解决业务问题,影响几亿互联网用户的产品体验。
1. 图状结构数据广泛存在
字节跳动的所有产品的大部分业务数据,几乎都可以归入到以下三种:
用户信息、用户和用户的关系(关注、好友等);
内容(视频、文章、广告等);
用户和内容的联系(点赞、评论、转发、点击广告等)。
这三种数据关联在一起,形成图状(Graph)结构数据。

为了满足 social graph 的在线增删改查场景,字节跳动自研了分布式图存储系统——ByteGraph。针对上述图状结构数据,ByteGraph 支持有向属性图数据模型,支持 Gremlin 查询语言,支持灵活丰富的写入和查询接口,读写吞吐可扩展到千万 QPS,延迟毫秒级。目前,ByteGraph 支持了头条、抖音、 TikTok、西瓜、火山等几乎字节跳动全部产品线,遍布全球机房。在这篇文章中,将从适用场景、内部架构、关键问题分析几个方面作深入介绍。
ByteGraph 主要用于在线 OLTP 场景,而在离线场景下,图数据的分析和计算需求也逐渐显现。2019 年年初,Gartner 数据与分析峰会上将图列为 2019 年十大数据和分析趋势之一,预计全球图分析应用将以每年 100% 的速度迅猛增长,2020 年将达到 80 亿美元。因此,我们团队同时也开启了在离线图计算场景的支持和实践。
下面会从图数据库和图计算两个部分,分别来介绍字节跳动在这方面的一些工作。
2. 自研图数据库(ByteGraph)介绍
从数据模型角度看,图数据库内部数据是有向属性图,其基本元素是 Graph 中的点(Vertex)、边(Edge)以及其上附着的属性;作为一个工具,图数据对外提供的接口都是围绕这些元素展开。
图数据库本质也是一个存储系统,它和常见的 KV 存储系统、MySQL 存储系统的相比主要区别在于目标数据的逻辑关系不同和访问模式不同,对于数据内在关系是图模型以及在图上游走类和模式匹配类的查询,比如社交关系查询,图数据库会有更大的性能优势和更加简洁高效的接口。
2.1 为什么不选择开源图数据库
图数据库在 90 年代出现,直到最近几年在数据爆炸的大趋势下快速发展,百花齐放;但目前比较成熟的大部分都是面对传统行业较小的数据集和较低的访问吞吐场景,比如开源的 Neo4j 是单机架构;因此,在互联网场景下,通常都是基于已有的基础设施定制系统:比如 Facebook 基于 MySQL 系统封装了 Social Graph 系统 TAO,几乎承载了 Facebook 所有数据逻辑;Linkedln 在 KV 之上构建了 Social Graph 服务;微博是基于 Redis 构建了粉丝和关注关系。
字节跳动的 Graph 在线存储场景, 其需求也是有自身特点的,可以总结为:
海量数据存储:百亿点、万亿边的数据规模;并且图符合幂律分布,比如少量大 V 粉丝达到几千万;
海量吞吐:最大集群 QPS 达到数千万;
低延迟:要求访问延迟 pct99 需要限制在毫秒级;
读多写少:读流量是写流量的接近百倍之多;
轻量查询多,重量查询少:90%查询是图上二度以内查询;
容灾架构演进:要能支持字节跳动城域网、广域网、洲际网络之间主备容灾、异地多活等不同容灾部署方案。
事实上,我们调研过了很多业界系统, 这个主题可以再单独分享一篇文章。但是,面对字节跳动世界级的海量数据和海量并发请求,用万亿级分布式存储、千万高并发、低延迟、稳定可控这三个条件一起去筛选,业界在线上被验证稳定可信赖的开源图存储系统基本没有满足的了;另外,对于一个承载公司核心数据的重要的基础设施,是值得长期投入并且深度掌控的。
因此,我们在 18 年 8 月份,开始从第一行代码开始踏上图数据库的漫漫征程,从解决一个最核心的抖音社交关系问题入手,逐渐演变为支持有向属性图数据模型、支持写入原子性、部分 Gremlin 图查询语言的通用图数据库系统,在公司所有产品体系落地,我们称之为 ByteGraph。下面,会从数据模型、系统架构等几个部分,由浅入深和大家分享我们的工作。
2.2 ByteGraph 的数据模型和 API
数据模型
就像我们在使用 SQL 数据库时,先要完成数据库 Schema 以及范式设计一样,ByteGraph 也需要用户完成类似的数据模型抽象,但图的数据抽象更加简单,基本上是把数据之间的关系“翻译”成有向属性图,我们称之为“构图”过程。
比如在前面提到的,如果想把用户关系存入 ByteGraph,第一步就是需要把用户抽象为点,第二步把"关注关系”、“好友关系”抽象为边就完全搞定了。下面,我们就从代码层面介绍下点边的数据类型。
点(Vertex)
点是图数据库的基本元素,通常反映的是静态信息。在 ByteGraph 中,点包含以下字段:
- 点的id(uint64_t): 比如用户id作为一个点
- 点的type(uint32_t): 比如appID作为点的type
- 点的属性(KV 对):比如 'name': string,'age': int, 'gender': male,等自定义属性
- [id, type]唯一定义一个点
边(Edge)
一条边由两个点和点之间的边的类型组成,边可以描述点之间的关系,比如用户 A 关注了用户 B ,可以用以下字段来描述:
- 两个点(Vertex): 比如用户A和用户B
- 边的类型(string): 比如“关注”
- 边的时间戳(uint64_t):这个t值是业务自定义含义的,比如可以用于记录关注发生的时间戳
- 边属性(KV对):比如'ts_us': int64 描述关系创建时间的属性,以及其他用户自定义属性
边的方向
在 ByteGraph 的数据模型中,边是有方向的,目前支持 3 种边的方向:
- 正向边:如 A 关注 B(A -> B)
- 反向边:如 B 被 A 关注(B <- A)
- 双向边:如 A 与 B 是好友(A <-> B)
场景使用伪码举例
构图完毕后,我们就可以把业务逻辑通过 Gremlin 查询语言来实现了;为便于大家理解,我们列举几种典型的场景为例。
场景一:记录关注关系 A 关注 B
// 创建用户A和B,可以使用 .property('name', 'Alice') 语句添加用户属性
g.addV().property("type", A.type).property("id", A.id)
g.addV().property("type", B.type).property("id", B.id)
// 创建关注关系 A -> B,其中addE("关注")中指定了边的类型信息,from和to分别指定起点和终点,
g.addE("关注").from(A.id, A.type).to(B.id, B.type).property("ts_us", now)
场景二:查询 A 关注的且关注了 C 的所有用户
用户 A 进入用户 C 的详情页面,想看看 A 和 C 之间的二度中间节点有哪些,比如 A->B,B->C,B 则为中间节点。
// where()表示对于上一个step的每个执行结果,执行子查询过滤条件,只保留关注了C的用户。
g.V().has("type", A.type).has("id", A.id).out("关注").where(out("关注").has("type", C.type).has("id", C.id).count().is(gte(1)))
场景三:查询 A 的好友的好友(二度关系)

最低0.47元/天 解锁文章
2万+





