做者:孙晓光,知乎搜索后端负责人,目前承担知乎搜索后端架构设计以及工程团队的管理工做。曾多年从事私有云相关产品开发工做关注云原生技术,TiKV 项目 Committer。
本文根据孙晓光老师在 TiDB TechDay 2019 北京站上的演讲整理。数据库
本次分享首先将从宏观的角度介绍知乎已读服务的业务场景中的挑战、架构设计思路,而后将从微观的角度介绍其中的关键组件的实现,最后分享在整个过程当中 TiDB 帮助咱们解决了什么样的问题,以及 TiDB 是如何帮助咱们将庞大的系统全面云化,并推动到一个很是理想的状态的。后端
知乎从问答起步,在过去的 8 年中逐步成长为一个大规模的综合性知识内容平台,目前,知乎上有多达 3000 万个问题,共收获了超过 1.3 亿个回答,同时知乎还沉淀了数量众多的文章、电子书以及其余付费内容,目前注册用户数是 2.2 亿,这几个数字仍是蛮惊人的。咱们有 1.3 亿个回答,还有更多的专栏文章,因此如何高效的把用户最感兴趣的优质内容分发他们,就是很是重要的问题。缓存
<center>图 1</center>网络
知乎首页是解决流量分发的一个关键的入口,而已读服务想要帮助知乎首页解决的问题是,如何在首页中给用户推荐感兴趣的内容,同时避免给用户推荐曾经看过的内容。已读服务会将全部知乎站上用户深刻阅读或快速掠过的内容记录下来长期保存,并将这些数据应用于首页推荐信息流和个性化推送的已读过滤。图 2 是一个典型的流程:多线程
<center>图 2</center>架构
当用户打开知乎进入推荐页的时候,系统向首页服务发起请求拉取“用户感兴趣的新内容”,首页根据用户画像,去多个召回队列召回新的候选内容,这些召回的新内容中可能有部分是用户曾经看到过的,因此在分发给用户以前,首页会先把这些内容发给已读服务过滤,而后作进一步加工并最终返回给客户端,其实这个业务流程是很是简单的。并发
<center>图 3</center>运维
这个业务第一个的特色是可用性要求很是高,由于首页多是知乎最重要的流量分发渠道。第二个特色是写入量很是大,峰值每秒写入 40k+ 条记录,每日新增记录近 30 亿条。而且咱们保存数据的时间比较长,按照如今产品设计须要保存三年。整个产品迭代到如今,已经保存了约一万三千亿条记录,按照每个月近一千亿条的记录增加速度,大概两年以后,可能要膨胀到三万亿的数据规模。工具
<center>图 4</center>oop
这个业务的查询端要求也很高。首先,产品吞吐高。用户在线上每次刷新首页,至少要查一次,而且由于有多个召回源和并发的存在,查询吞吐量还可能放大。峰值时间首页每秒大概产生 3 万次独立的已读查询,每次查询平均要查 400 个文档,长尾部分大概 1000 个文档,也就是说,整个系统峰值平均每秒大概处理 1200 万份文档的已读查询。在这样一个吞吐量级下,要求的响应时间还比较严格,要求整个查询响应时间(端到端超时)是 90ms,也就意味着最慢的长尾查询都不能超过 90ms。还有一个特色是,它能够容忍 false positive,意味着有些内容被咱们过滤掉了,可是系统仍然能为用户召回足够多的他们可能感兴趣的内容,只要 false positive rate 被控制在可接受的范围就能够了。
因为知乎首页的重要性,咱们在设计这个系统的时候,考虑了三个设计目标:高可用、高性能、易扩展。首先,若是用户打开知乎首页刷到大量已经看过的内容,这确定不可接受,因此对已读服务的第一个要求是「高可用」。第二个要求是「性能高」,由于业务吞吐高,而且对响应时间要求也很是高。第三点是这个系统在不断演进和发展,业务也在不断的更新迭代,因此系统的「扩展性」很是重要,不能说今天能支撑,明天就支撑不下来了,这是无法接受的。
接下来从这三个方面来介绍咱们具体是如何设计系统架构的。
<center>图 5</center>
当咱们讨论高可用的时候,也意味着咱们已经意识到故障是无时无刻都在发生的,想让系统作到高可用,首先就要有系统化的故障探测机制,检测组件的健康情况,而后设计好每个组件的自愈机制,让它们在故障发生以后能够自动恢复,无需人工干预。最后咱们但愿用必定的机制把这些故障所产生的变化隔离起来,让业务侧尽量对故障的发生和恢复无感知。
<center>图 6</center>
对常见的系统来讲,越核心的组件每每状态越重扩展的代价也越大,层层拦截快速下降须要深刻到核心组件的请求量对提升性能是很是有效的手段。首先咱们经过缓冲分 Slot 的方式来扩展集群所能缓冲的数据规模。接着进一步在 Slot 内经过多副本的方式提高单个 Slot 缓冲数据集的读取吞吐,将大量的请求拦截在系统的缓冲层进行消化。若是请求不可避免的走到了最终的数据库组件上,咱们还能够利用效率较高的压缩来继续下降落到物理设备上的 I/O 压力。
<center>图 7</center>
提高系统扩展性的关键在于减小有状态组件的范围。在路由和服务发现组件的帮助下,系统中的无状态组件能够很是轻松的扩展扩容,因此经过扩大无状态服务的范围,收缩重状态服务的比例,能够显著的帮助咱们提高整个系统的可扩展性。除此以外,若是咱们可以设计一些能够从外部系统恢复状态的弱状态服务,部分替代重状态组件,这样能够压缩重状态组件的比例。随着弱状态组件的扩大和重状态组件的收缩,整个系统的可扩展性能够获得进一步的提高。
在高可用、高性能和易扩展的设计理念下,咱们设计实现了已读服务的架构,图 8 是已读服务的最终架构。
<center>图 8</center>
首先,上层的客户端 API 和 Proxy 是彻底无状态可随时扩展的组件。最底层是存储所有状态数据的 TiDB,中间这些组件都是弱状态的组件,主体是分层的 Redis 缓冲。除了 Redis 缓冲以外,咱们还有一些其余外部组件配合 Redis 保证 Cache 的一致性,这里面的细节会在下一章详述。
从整个系统来看,TiDB 这层自身已经拥有了高可用的能力,它是能够自愈的,系统中无状态的组件很是容易扩展,而有状态的组件中弱状态的部分能够经过 TiDB 中保存的数据恢复,出现故障时也是能够自愈的。此外系统中还有一些组件负责维护缓冲一致性,但它们自身是没有状态的。因此在系统全部组件拥有自愈能力和全局故障监测的前提下,咱们使用 Kubernetes 来管理整个系统,从而在机制上确保整个服务的高可用。
<center>图 9</center>
Proxy 层是无状态的,设计同常见的 Redis 代理类似,从实现角度看也很是简单。首先咱们会基于用户纬度将缓冲拆分红若干 Slot,每一个 Slot 里有多个 Cache 的副本,这些多副本一方面能够提高咱们整个系统的可用性,另一方面也能够分摊同一批数据的读取压力。这里面也有一个问题,就是 Cache 的副本一致性的如何保证?咱们在这里选择的是「会话一致性」,也就是一个用户在一段时间内从同一个入口进来,就会绑定在这一个 Slot 里面的某个副本上,只要没有发生故障,这个会话会维持在上面。
若是一个 Slot 内的某个副本发生故障,Proxy 首先挑这个 Slot 内的其余的副本继续提供服务。更极端的状况下,好比这个 Slot 内全部副本都发生故障,Proxy 能够牺牲系统的性能,把请求打到另一个彻底不相干的一个 Slot 上,这个 Slot 上面没有当前请求对应数据的缓存,并且拿到结果后也不会缓存相应的结果。咱们付出这样的性能代价得到的收益是系统可用性变得更高,即使 Slot 里的全部的副本同时发生故障,依旧不影响系统的可用性。
对于缓冲来讲,很是重要的一点就是如何提高缓冲利用率。
第一点是如何用一样的资源缓冲更大量的数据。在由「用户」和「内容类型」和「内容」所组成的空间中,因为「用户」维度和「内容」维度的基数很是高,都在数亿级别,即便记录数在万亿这样的数量级下,数据在整个三维空间内的分布依然很是稀疏。如图 10 左半部分所示。
<center>图 10</center>
考虑到目前知乎站上沉淀的内容量级巨大,咱们能够容忍 false positive 但依旧为用户召回到足够多可能会感兴趣的内容。基于这样的业务特色,咱们将数据库中存储的原始数据转化为更加致密的 BloomFilter 缓冲起来,这极大的下降了内存的消耗在相同的资源情况下能够缓冲更多的数据,提升缓存的命中率。
提高缓存命中率的方式有不少种,除了前面提到的提高缓存数据密度增长可缓冲的数据量级以外,咱们还能够经过避免没必要要的缓存失效来进一步的提高缓存的效率。
<center>图 11</center>
一方面咱们将缓存设计为 write through cache 使用原地更新缓存的方式来避免 invalidate cache 操做,再配合数据变动订阅咱们能够在不失效缓冲的状况下确保同一份数据的多个缓冲副本能在很短的时间内达成最终一致。
另外一方面得益于 read through 的设计,咱们能够将对同一份数据的多个并发查询请求转化成一次 cache miss 加屡次缓冲读取(图 11 右半部分),进一步提高缓存的命中率下降穿透到底层数据库系统的压力。
接下来再分享一些不单纯和缓冲利用率相关的事情。众所周知,缓冲特别怕冷,一旦冷了, 大量的请求瞬间穿透回数据库,数据库很大几率都会挂掉。在系统扩容或者迭代的状况下,每每须要加入新的缓冲节点,那么如何把新的缓冲节点热起来呢?若是是相似扩容或者滚动升级这种能够控制速度的状况,咱们能够控制开放流量的速度,让新的缓冲节点热起来,但当系统发生故障的时候,咱们就但愿这个节点很是快速的热起来。 因此在咱们这个系统和其余的缓冲系统不大同样的是,当一个新节点启动起来,Cache 是冷的,它会立刻从旁边的 Peer 那边 transfer 一份正在活跃的缓存状态过来,这样就能够很是快的速度热起来,以一个热身的状态去提供线上的服务(如图 12)。
<center>图 12</center>
另外,咱们能够设计分层的缓冲,每一层缓冲能够设计不一样的策略,分别应对不一样层面的问题,如图 13 所示,能够经过 L1 和 L2 分别去解决空间层面的数据热度问题和时间层面的热度问题,经过多层的 Cache 能够逐层的下降穿透到下一层请求的数量,尤为是当咱们发生跨数据中心部署时,对带宽和时延要求很是高,若是有分层的设计,就能够在跨数据中心之间再放一层 Cache,减小在穿透到另一个数据中心的请求数量。
<center>图 13</center>
为了让业务之间不互相影响而且针对不一样业务的数据访问特征选择不一样的缓冲策略,咱们还进一步提供了 Cache 标签隔离的机制来隔离离线写入和多个不一样的业务租户的查询。刚刚说的知乎已读服务数据,在后期已经不仅是给首页提供服务了,还同时为个性化推送提供服务。个性化推送是一个典型的离线任务,在推送内容前去过滤一下用户是否看过。虽然这两个业务访问的数据是同样的,可是它们的访问特征和热点是彻底不同的,相应的缓冲策略也不同的。因而咱们在作分组隔离机制(如图 14),缓冲节点以标签的方式作隔离,不一样的业务使用不一样的缓冲节点,不一样缓冲节点搭配不一样的缓冲策略,达到更高的投入产出比,同时也能隔离各个不一样的租户,防止他们之间互相产生影响。
<center>图 14</center>
<center>图 15</center>
存储方面,咱们最初用的是 MySQL,显然这么大量的数据单机是搞不定的,因此咱们使用了分库分表 + MHA 机制来提高系统的性能并保障系统的高可用,在流量不太大的时候还能忍受,可是在当每个月新增一千亿数据的状况下,咱们内心的不安与日俱增,因此一直在思考怎样让系统可持续发展、可维护,而且开始选择替代方案。这时咱们发现 TiDB 兼容了 MySQL,这对咱们来讲是很是好的一个特色,风险很是小,因而咱们开始作迁移工做。迁移完成后,整个系统最弱的“扩展性”短板就被补齐了。
<center>图 16</center>
如今整个系统都是高可用的,随时能够扩展,并且性能变得更好。图 16 是前两天我取出来的性能指标数据,目前已读服务的流量已达每秒 4 万行记录写入, 3 万独立查询和 1200 万个文档判读,在这样的压力下已读服务响应时间的 P99 和 P999 仍然稳定的维持在 25ms 和 50ms,其实平均时间是远低于这个数据的。这个意义在于已读服务对长尾部分很是敏感,响应时间要很是稳定,由于不能牺牲任何一位用户的体验,对一位用户来讲来讲超时了就是超时了。
最后分享一下咱们从 MySQL 迁移到 TiDB 的过程当中遇到的困难、如何去解决的,以及 TiDB 3.0 发布之后咱们在这个快速迭代的产品上,收获了什么样的红利。
<center>图 17</center>
如今其实整个 TiDB 的数据迁移的生态工具已经很完善,咱们打开 TiDB DM 收集 MySQL 的增量 binlog 先存起来,接着用 TiDB Lightning 快速把历史数据导入到 TiDB 中,当时应该是一万一千亿左右的记录,导入总共用时四天。这个时间仍是很是震撼的,由于若是用逻辑写入的方式至少要花一个月。固然四天也不是不可缩短,那时咱们的硬件资源不是特别充足,选了一批机器,一批数据导完了再导下一批,若是硬件资源够的话,能够导入更快,也就是所谓“高投入高产出”,若是你们有更多的资源,那么应该能够达到更好的效果。在历史数据所有导入完成以后,就须要开启 TiDB DM 的增量同步机制,自动把刚才存下来的历史增量数据和实时增量数据同步到 TiDB 中,并近实时的维持 TiDB 和 MySQL 数据的一致。
在迁移完成以后,咱们就开始小流量的读测试,刚上线的时候其实发现是有问题的,Latency 没法知足要求,刚才介绍了这个业务对 Latency 特别敏感,稍微慢一点就会超时。这时 PingCAP 伙伴们和咱们一块儿不停去调优、适配,解决 Latency 上的问题。图 18 是咱们总结的比较关键的经验。
<center>图 18</center>
第一,咱们把对 Latency 敏感的部分 Query 布了一个独立的 TiDB 隔离开,防止特别大的查询在同一个 TiDB 上影响那些对 Latency 敏感的的 Query。第二,有些 Query 的执行计划选择不是特别理想,咱们也作了一些 SQL Hint,帮助执行引擎选择一个更加合理的执行计划。除此以外,咱们还作了一些更微观的优化,好比说使用低精度的 TSO,还有包括复用 Prepared Statement 进一步减小网络上的 roundtrip,最后达到了很好的效果。
<center>图 19</center>
这个过程当中咱们还作了一些开发的工做,好比 binlog 之间的适配。由于这套系统是靠 binlog 变动下推来维持缓冲副本之间的一致性,因此 binlog 尤其重要。咱们须要把原来 MySQL 的 binlog 改为 TiDB 的 binlog,可是过程当中遇到了一些问题,由于 TiDB 做为一个数据库产品,它的 binlog 要维持全局的有序性的排列,然而在咱们以前的业务中因为分库分表,咱们不关心这个事情,因此咱们作了些调整工做,把以前的 binlog 改为能够用 database 或者 table 来拆分的 binlog,减轻了全局有序的负担,binlog 的吞吐也能知足咱们要求了。同时,PingCAP 伙伴们也作了不少 Drainer 上的优化,目前 Drainer 应该比一两个月前的状态好不少,不管是吞吐仍是 Latency 都能知足咱们如今线上的要求。
最后一点经验是关于资源评估,由于这一点多是咱们当时作得不是特别好的地方。最开始咱们没有特别仔细地想到底要多少资源才能支撑一样的数据。最初用 MySQL 的时候,为了减小运维负担和成本,咱们选择了“1 主 1 从”方式部署 ,而 TiDB 用的 Raft 协议要求至少三个副本,因此资源要作更大的准备,不能期望用一样的资源来支撑一样的业务,必定要提早准备好对应的机器资源。另外,咱们的业务模式是一个很是大的联合主键,这个联合主键在 TiDB 上非聚簇索引,又会致使数据更加庞大,也须要对应准备出更多的机器资源。最后,由于 TiDB 是存储与计算分离的架构,因此网络环境必定要准备好。当这些资源准备好,最后的收益是很是明显的。
在知乎内部采用与已读服务相同的技术架构咱们还支撑了一套用于反做弊的风控类业务。与已读服务极端的历史数据规模不一样,反做弊业务有着更加极端的写入吞吐但只需在线查询最近 48 小时入库的数据(详细对比见图 20)。
<center>图 20</center>
那么 TiDB 3.0 的发布为咱们这两个业务,尤为是为反做弊这个业务,带来什么样的可能呢?
首先咱们来看看已读服务。已读服务写读吞吐也不算小,大概 40k+,TiDB 3.0 的 gRPC Batch Message 和多线程 Raft store,能在这件事情上起到很大的帮助。另外,Latency 这块,我刚才提到了,就是咱们写了很是多 SQL Hint 保证 Query 选到最优的执行计划,TiDB 3.0 有 Plan Management 以后,咱们再遇到执行计划相关的问题就无需调整代码上线,直接利用 Plan Management 进行调整就能够生效了,这是一个很是好用的 feature。
刚才马晓宇老师详细介绍了 TiFlash,在 TiDB DevCon 2019 上第一次听到这个产品的时候就以为特别震撼,你们能够想象一下,一万多亿条的数据能挖掘出多少价值, 可是在以往这种高吞吐的写入和庞大的全量数据规模用传统的 ETL 方式是难以在可行的成本下将数据每日同步到 Hadoop 上进行分析的。而当咱们有 TiFlash,一切就变得有可能了。
<center>图 21</center>
再来看看反做弊业务,它的写入更极端,这时 TiDB 3.0 的 Batch message 和多线程 Raft Store 两个特性可让咱们在更低的硬件配置状况下,达到以前一样的效果。另外反做弊业务写的记录偏大,TiDB 3.0 中包含的新的存储引擎 Titan,就是来解决这个问题的,咱们从 TiDB 3.0.0- rc1 开始就在反做弊业务上将 TiDB 3.0 引入到了生产环境,并在 rc2 发布不久以后开启了 Titan 存储引擎,下图右半部分能够看到 Titan 开启先后的写入/查询 Latency 对比,当时咱们看到这个图的时候都很是很是震撼,这是一个质的变化。
<center>图 22</center>
另外,咱们也使用了 TiDB 3.0 中 Table Partition 这个特性。经过在时间维度拆分 Table Partition,能够控制查询落到最近的 Partition 上,这对查询的时效提高很是明显。
最后简单总结一下咱们开发这套系统以及在迁移到 TiDB 过程当中的收获和思考。
<center>图 23</center>
首先开发任何系统前必定先要理解这个业务特色,对应设计更好的可持续支撑的方案,同时但愿这个架构具备普适性,就像已读服务的架构,除了支撑知乎首页,还能够同时支持反做弊的业务。
另外,咱们大量应用了开源软件,不只一直使用,还会参与必定程度的开发,在这个过程当中咱们也学到了不少东西。因此咱们应该不只以用户的身份参与社区,甚至还能够为社区作更多贡献,一块儿把 TiDB 作的更好、更强。
最后一点,咱们业务系统的设计可能看上去有点过于复杂,但站在今天 Cloud Native 的时代角度,即使是业务系统,咱们也但愿它能像 Cloud Native 产品同样,原生的支持高可用、高性能、易扩展,咱们作业务系统也要以开放的心态去拥抱新技术,Cloud Native from Ground Up。
更多 TiDB 用户实践:https://pingcap.com/cases-cn/