伴鱼技术团队

Technology changes the world

伴鱼开放平台 上线了! 源于实践的解决方案,助力企业成就未来!

在计算机系统设计实践中,我们常常会遇到下图所示架构:

为了解决单个存储器读吞吐无法满足要求的问题,常常需要在存储器上面增加一个或多个缓存。但由于相同的数据被复制到一个或多个地方,就容易引发数据一致性问题。不一致的数据可能出现在同级 Cache 之间 (Cache Coherence) 上下级 Cache 之间。解决这些数据一致性问题的方案可以统称为 Cache Policies。从本质上看,所有 Cache Policies 的设计目的都可以概括为:在增加一级缓存之后,系统看起来和没加缓存的行为一致,但得益于局部性原理,系统的读吞吐量提高、时延减少

本文将探讨三个场景:

  1. Cache Policy In Single-core Processor
  2. Cache Coherence in Multi-core Processor
  3. Cache Policy in Cache/DB Architecture

    ...

    阅读全文 »

简介

本文是分布式系统理论的开山鼻祖、2013 年图灵奖获得者 Lamport 的成名作,也是分布式计算领域杰出论文最佳影响力奖 Dijkstra Prize 的第一篇论文,高达 11692 的引用量(截至 2019/12/08)足以证明其广泛的影响力:

本文主要讨论 3 个话题:

  • 分布式系统中的事件偏序
  • 利用逻辑时钟实现事件偏序
  • 利用逻辑时钟实现事件全序

    ...

    阅读全文 »

早在 2008 年,Google 就已开始分布式调用链追踪的工作,经过两年的打磨后,Dapper 系统问世,并通过这篇文章将其设计公之于众。遗憾的是,Dapper 并不是开源项目,但它的设计理念依然深刻影响到后来的 Jaeger、Zipkin 等开源分布式追踪项目,以及相关的标准 Opentracing、OpenTelemetry。

本文不是原文的精准翻译,而是一次重述和简述,旨在记录分布式调用链追踪要解决的核心问题和潜在解决方案。

Why & Design Goals

云原生环境中,一次请求的处理可能途径多个服务的任意实例,彻底理解系统就需要理解各服务内部的逻辑,理清这些服务之间的关系,甚至有时候还需要了解服务所在物理机的当时状态。系统出现异常时,如果其行为无法被追踪、被理解,就无法为解决异常快速提供线索。

通常这些异常会被监控捕捉,如时延异常、错误日志、程序崩溃,在紧急处理之后,就需要调查案发现场,彻底解决问题。这时候就需要了解每个请求在整个微服务集群内部的行踪。

这就向分布式追踪系统提出了两点要求:

  • 处处部署 (ubiquitous deployment)
  • 持续监控 (continuous monitoring)

如果部署不完全或者监控有间断,就可能有一小部分历史无法被追踪到,从而影响到问题定位的准确度,使得追踪效果大打折扣。

据此,我们提出追踪系统的 3 个主要设计目标:

  1. 低成本 (Low overhead):对服务的性能影响应该能够忽略不计
  2. 对应用透明 (Application-level transparency):应用开发者对追踪系统无感知
  3. 扩展性好 (Scalability):支持部署到所有服务的所有实例上

在此基础上,数据从采集到可以被查询、分析的延迟越小越好,起到的作用也越大、越及时。

How

General Approaches

分布式追踪的设计方案主要可以分为两类:黑盒法(black-box)和标记法(annotation-based):

黑盒法

黑盒法无需任何侵入性代码,只通过统计回归等手段来推测服务之间的关系。它的优势在于无需修改代码,缺点在于记录不准确,且需要大量数据才能够推导出服务间的关系。

标记法

标记法需要为每个请求打标记,并通过一个全局标识符将请求途径的所有服务信息串联,复盘整个链路。标记法记录准确,但它的缺点也很明显,需要将标记代码注入到每个服务中。

在 Google 内部,几乎所有应用都使用相同的 threading model、control flow 和 RPC systems,因此可以将打标记的工作集中在少量的公共库中,同样能够达到对应用透明的效果。

Data Models

通常一个请求在微服务集群中的调用链可以被抽象成树形结构,假设 RequestX 的处理过程如下图所示::

相应调用链追踪的树状结构为:

整棵树称为一个 trace,树上的节点称为 span。每个 span 都记录着 parent id 和 trace id,表明其所属父节点和调用链,其中没有 parent id 的 span 称为 root span,root span 的 id 就是 trace id。

每个 span 都需要记录其开始时间和结束时间,如果应用开发者有记录其它信息的需求,则可以手动增加相应的标记。

Pipeline

Dapper 记录、收集调用链信息的流水线主要分成 3 个阶段:

  1. span 数据写入本地日志文件
  2. dapper daemon 从本地日志文件中收集数据
  3. dapper collectors 将数据写入 Bigtable 的大区仓库 (regional repositories)

在 Bigtable 中,每行数据就是一个 trace,且每行可以有任意列,恰好方便存储不定长的 trace/span 数据。Dapper 向开发者提供相应的 API 和 SDK,方便 Google 的开发者能够据此搭建数据分析工具,定制化地辅助线上问题排查。

Overhead

调用链追踪的主要成本在于:trace generation 和 collection。

Trace generation

在 Dapper 中,生成 root span 需要 204 ns,生成 non-root span 需要 176 ns。这里面相差的部分就是生成 全局唯一 trace id 的时间成本。

在 Dapper 的 runtime library 中,最耗时的操作就是将 trace 信息写入本地磁盘,但考虑到使用批量和异步写入的方式优化,对被跟踪的服务本身的影响就相对削弱了。

Trace collection

Dapper daemon 需要从本地日志文件中读取 trace 信息,然后发送给 Dapper collectors。经过 benchmark 测试验证,Dapper daemon 从未使用超过单核 0.3% 的计算资源,且使用的内存空间极小,可忽略不计。同时 Dapper daemon 在 kernel scheduler 中的优先级被设置为最低,必要时会出让计算资源。

在实践中,平均每个 span 的大小约为 426 字节,经过计算,在生产环境中 Dapper 占用的网络带宽大约为总量的 0.01%。

Effect on production workloads

高吞吐的服务随时都会接收大量的请求,产生大量的 tracing 数据,而这类服务通常又是对性能最敏感的。下表中以 Google 的网页搜索服务集群为例,测量了不同的 trace 采样率对服务本身的影响:

其中 latency 和 throughput 的测量误差分别为 2.5% 和 0.15%。从图中可以发现,尽管调用链追踪带来的性能影响不是很大,但并不能忽略不计,对 trace 数据进行抽样是很有必要的。当抽样率小于 1/16 时,影响范围已经小于误差范围。在实践中,我们设置 1/1024 的抽样率就能收集到足够多的 trace 数据。

除此之外,使用更低的采样率可以让 trace 数据在本地磁盘存活更长的时间,为整个搜集框架争取更大的灵活度。

Adaptive Sampling

调用链追踪的成本与单位时间内收集的 trace 数量成正比。在 Dapper 的首个生产版本中,采用了统一的采样率 1/1024,这种固定采样率不会对高吞吐的在线服务产生不必要的影响。但在这样的采样率下,可能忽略掉一些发生不频繁的重要事件。

因此 Dapper 团队正在研发自适应采样率机制,针对不频繁的重要事件能提高采样率。实际被使用的抽样率会被记录在 trace/span 数据中,帮助后期工具分析。

Additional Sampling

上面介绍的均匀抽样和自适应抽样都是为了减少对被追踪服务本身性能的影响。但 Dapper 本身还需要控制整体抽样数据的规模,在论文发表时,Dapper 在生产环境中每天将产生 1T 的追踪数据;同时 Dapper 的用户希望追踪数据能够保持两周。因此这里存在着存储资源与追踪密度之间的权衡。除此之外,高抽样率也会提升 Dapper collectors、Bigtable 的吞吐量。因此 Dapper 团队引入了额外的一层抽样,来实现全局的、系统级的控制。

实现的思路很简单,将每个 trace id 哈希到 [0, 1],如果哈希值小于给定的抽样系数,则通过;大于则拦截。在实践中,额外的抽样给与 Dapper 团队更强的全局控制力。

Where

General-Purpose Tools

Dapper Depot API

Dapper 向开发者开放一系列 API:

  • Access by trace id
  • Bulk access
  • Indexed access

在实践中,Dapper 发现 (service_name, host_machine, timestamp) 的联合索引恰好能满足大部分开发者的需求。

Dapper user interface

dapper user interface 可以理解为 APM 系统,方便开发者快速对线上问题做根源分析。交互界面和 user story 详见论文。

Experiences

Using Dapper during development

Dapper 主要在以下几个方面帮助开发者改进服务:

  • Performance:通过调用链示意图发现服务瓶颈
  • Correctness:dapper 通过标签帮助开发者发现一些本该访问 master 节点的请求访问了 replicas
  • Understanding:dapper 帮助开发者理解自身系统的依赖,增进对复杂系统的理解,为架构层面优化提供依据
  • Testing:开发者可以通过观察调用链信息测试系统的行为是否符合预期

Addressing long tail latency

Google 内部的一位工程师利用 Dapper 提供的接口来推断服务的关键路径,进而减少服务整体时延。

Inferring service dependencies

服务之间的依赖关系常常是动态变化的,我们基本无法通过扫描配置信息、代码来确定服务之间的依赖关系。 Google 的 “Service Dependencies” 项目正是利用 Dapper 的近期数据来推断依赖关系。

Network usage of different services

在发现网络带宽使用异常时,Dapper 可以辅助开发者锁定到具体的请求。

Layered and Shared Storage Systems

一些公共服务通常很难知道其调用方及各自调用量,Dapper 帮助这些公共服务的维护者更好地了解它们。

References

Dapper

写在前面的话

技术选型是由技术方向和业务场景 trade-off 决定的,脱离业务场景来说技术选型是没有任何意义的,所以本文只是阐述了伴鱼技术团队数据库选型的过程,这并不是 MySQL、MongoDB 和 TiDB 之间直接的比较,只能说明 TiDB 更适合伴鱼的业务场景和技术规划,另外由于 TiDB 是非常新的数据库技术,所以这也能体现出伴鱼技术团队对新技术的态度、技术后发优势的理解、成本与效率的衡权和技术生态与红利的思考。

为什么放弃 MongoDB

伴鱼是 2015 年成立的,那个时候 NoSQL 还如日中天,关系型数据库为了应付海量的数据只能业务侵入式的分库分表,虽然 Google 在 2012 年发布了 NewSQL 数据库 Spanner 的论文,但是工业界还没有一款可以使用的 NewSQL 数据库,综合当时的各种情况,伴鱼选择的是 MongoDB。

不过,在 2015 年到 2017 年之间,对于伴鱼来说 MongoDB 确实是一个上佳之选,主要有以下几个方面的原因:

  • 开发更高效:公司初期处于探索期,产品迭代非常快,MongoDB 是 NoSQL 数据库,不需要做建库建表等DDL操作,特别在产品快速迭代,需要频繁增减字段的时候就更高效,当然这个也是有代价的,从本质上来说,MongoDB 是读模式,它几乎不检查写入的内容是否合法,对数据 Schema 的解释是在应用程序的代码中,导致写入数据的约束性是没有保证的。
  • 运维更高效:当时公司研发非常少,这段时间整个后端只有两个工程师,没有专职的运维和 DBA ,但是 MongoDB 的单机性能比 MySQL 要高不少,不但对数据库的运维成本要低不少,并且当时除了几个热点库外,其他的库 MongoDB 可以直接扛住流量压力,省去了中间的 Cache 层,让开发和运维都更高效。
  • 有事务需求的场景不多:当时使用的是 MongoDB 2.x 和 3.x,只提供了数据一致性的选择(强一致性、单调一致性和最终一致性)和原子操作,在少数的几个场景,比如交易相关的场景,通过选择强一致性和原子操作,再在应用层实现 MVCC 的机制,可以满足简单的事务需求。

总体来说,在伴鱼产品的探索期,为了效率牺牲一点数据约束性和事务能力是值得的,但是 2017 年底伴鱼产品方向比较明确后,业务场景从探索期转变到快速发展期,对数据库的需求从效率优先转变为效率、事务能力与生态并重:

  • 有事务需求的场景急增:事务场景从最初与钱相关的交易扩展到一些虚拟货币,并且由于并发量的增加,之前没有事务保障的场景出现竞争的情况越来越多,还在应用层通过 MVCC 机制实现简单的事务是非常低效的,并且在应用层实现事务的正确性也是很难保证的。(一个有趣的故事:Jeff Dean曾经说过自己对 Bigtable 最后悔的事情是没有提供跨行事务支持,导致业务就会在上层企图自己搞事务,并且业务实现的分布式事务大部分都是错的,所以在后来的 Spanner 数据库中 Jeff Dean 直接提供了官方分布式事务支持。)
  • 对大数据生态的需求急增:在产品探索期的时候,也有很强的数据分析需求,不过当时数据总量小,在 MongoDB 的隐藏从库中直接分析就足够了,但是产品快速发展期,数据量急剧增加,在 OLTP 数据库中进行 OLAP 操作已经力不从心了。但是通过大数据生态来进行数据分析,对于 MongoDB 来说有一个非常残酷的现实,基本所有的大数据生态都是围绕 MySQL 生态打造的,如果想接入 MongoDB 的数据,意味着需要重新大量造轮子。
  • 对数据约束性的要求更高:由于业务快速的发展,服务可能会出现多人维护和移交的情况,如果存储的数据没有约束,意味着存储的数据 Schema 是不可控的,这很容易让后面参与的工程师崩溃和掉进坑里,这个时候数据的约束性变成是一个更高优秀级的需求,关系数据库的写模式变成更好的选择。

到产品快速发展期,由于业务场景对数据库的需求已经发生了很大的改变,所以在这个时候,伴鱼技术团队开始谨慎思考数据库重新选型的问题,我们理想型的数据库是这样的:

  • 高可用;
  • 高吞吐;
  • 支持ACID事务;
  • 大数据生态友好;
  • 有水平扩张能力,并且尽量做到不侵入业务;

基于上面这些需求,我们开始了数据库的重新选型之路。

初识 TiDB

早在 2015 年的时候我就非常关注分布式数据库,当时已经经历过高并发高 QPS 的场景,利用分布式架构解决无状态高并发高QPS场景是不复杂的,但是分布式存储由于涉及到一致性问题是非常有挑战的,如果还想支持ACID事务那就更难了,所以当时就对分布式数据库技术特别感兴趣,开始关注OceanBase并且收集和研究相关的理论和架构文档。

后来同事推荐说有个叫TiDB的数据库,目前有些公司也在用了,反馈也不错,所以我们决定调研一下。TiDB 官网的文档做的非常友好,不论是理论还是架构的文章都非常齐全,几乎一口气就把所有的文章都看了一遍(当时文章比现在要少),完备的理论支持、优雅的架构设计、与 Google Spanner 一脉相承的设计思路让我们对 TiDB 的前景非常看好,并且功能上完全满足我们的要求,所以当时就决定长期关注 TiDB,并且准备进行初步验证。

初步验证

通过调研我们发现,TiDB 是通过 raft 协议来保证多副本数据的一致性( ACID 中的 C ),通过 2PC 协议来保证事务的原子性( ACID 的 A ),通过乐观锁加 MVCC 来实现可重复读的事务隔离级别( ACID 中 I ),这意味着 TiDB 每一次事务的成本是比 MySQL 要高很多的,特别是有事务冲突的时候(乐观锁的原因),所以性能是需要验证的关键点。

当时伴鱼所有的业务都部署在阿里云上(现在有自建机房),就直接在阿里云上面按 TiDB 的配置要求购买了机器,当时安装的是 TiDB 1.x 的版本。因为 TiDB 官网已经有 Sysbench 的压力测试数据,这个性能数据是符合我们需求的,所以决定对我们的业务场景进行一次完全模拟的长期测试: 伴鱼 IM 的并发比较高,并且采用写扩散的设计,对数据库的要求会比较高,所以适合进行验证。通过对 IM 业务的 inbox 表进行双写,业务同步写 MongoDB 和异步写 TiDB,业务读只读 MongoDB,这样如果 TiDB 有问题也不会影响线上业务。

在低峰期对 IM 业务开启双写后,TiDB 监控的 999 线和 99 线还满足要求,但是所有 TiKV 节点的 io 使用率一直在 90% 附近波动,这个如果到高峰期是绝对会有问题的,通过重读 TiKV 的配置,在修改配置项 sync-log = false 后,TiKV 的 io 使用率维持在 5% 以下,当天的高峰期也一切正常,没有出现问题。

在这之后,我们对 IM 的双写观察 2-3 个月的时间,在确认一切正常后,再将同步读写都修改为 TiDB 并且异步写 MongoDB,一切正常并且持续观察。

sync-log 配置是控制 TiKV 数据多副本进行 raft 协议同步的时候,如果 sync-log=false,则内存中处理完成就返回 ack,对于 3 副本来说,单节点故障是不会丢失数据的,同一个 raft 集的 2 个副本同时故障可能会出现丢数据的情况,这个问题除了金融等对数据安全性要求非常高的场景外,其他的业务场景是可以接受的,并且 MySQL 等其他数据库的集群方案在 master 节点故障的时候问题更大。

深度交流

在前面初步验证 TiDB 的过程中,一个看似很严重的问题但是调整一个配置就可以解决,这让我们发现了我们对 TiDB 的理解和控制力还不够,在对每一个配置都进行理解研究外,还有一些我们非常关心的问题但没有官方答案。如果对这些问题没有官方答案,那么我们直接使用 TiDB 就是有很大风险的,所以我们决定和 TiDB 团队进行一次深度的交流。

我们当时非常关心的问题列表为:

  • TiKV的线性扩展能力怎么样?
  • 两地三中心架构,TiDB 可以容忍数据中心之间的延迟是多少?
  • 目前业界 TiDB 最大的一个集群的 TiKV 和 TiDB 的节点数、数据量、QPS 最高是多少?
  • TiDB 哪一些配置是需要特别关注和调整的?

收集了大概 20 多个问题,得益于伴鱼和 TiDB 都在北京,离得还非常近,在线上联系上并且约好时间后,和 TiDB 进行了第一次深度的交流。

大概是2018年上半年的一天,我和 TiDB 的 3-4 个同事聊了一整个上午,基本都是我将收集到的问题一个个抛出来,大家一起讨论。整个交流过程解答了很多我们关心的问题,也了解到当前业界对TiDB的使用情况,大大增强了我们对 TiDB 的信心,对于数据库的选型来说,这是非常关键的事情。

特别感谢当时一起交流的 TiDB 同事:房晓乐以及另外 2-3 位我不知道名字的同学(非常抱歉)。

为什么不选择 MySQL

经过对 TiDB 的调研、试用和深入交流后,在传统的关系型数据库 MySQL 和 NewSQL 数据库 TiDB 之间,我们需要做出自己的选择了,这不仅仅是两个数据库之间的选择,这其实也体现了伴鱼对新技术的态度、技术后发优势的理解、成本与效率的衡权和技术生态与红利的思考。

对新技术的态度

伴鱼对新技术的态度是非常积极的,如果业务场景需要的新技术我们都会去了解它、研究它和掌握它,我们相信我们对新技术趋势的判断能力和掌控能力,所以在 TiDB 和 MySQL 的选型的过程中,MySQL 确实是非常稳的选择,并且对我们的需求目前都有现成的解决方案,比如高可用,比如水平扩展能力,只不过不是非常优雅的解决方案,但是 TiDB 无论是理论层面和架构层面都比 MySQL 高出一个时代(MySQL 是面向单机数据库设计的,是这个领域非常优秀的数据库,只是现在伴鱼想要解决的是单机无法存储的海量数据场景,在这个维度上比较确实 TiDB 更好一些,但是这并不是 MySQL 的问题,是因为它们的设计目标不同而已),但是稳定性和成熟度会比 MySQL 要差一些,这个时候,我们选择相信我们对 NewSQL 技术方向的判断力和掌控力,相信 TiDB 的进化能力,相信时间站在我们这边,让子弹再飞一会。

技术后发优势的理解

伴鱼在之前用的数据库是 MongoDB,MySQL 和 TiDB 都没有用过,如果我们判断 TiDB 更面向未来的数据库,那么我们是先从 MySQL 开始,走一遍 MySQL 的道路,在后面可预见的未来再迁移到 TiDB 上来;还是直接深入研究和掌握 TiDB,直接 All in TiDB?

初创公司在技术沉淀和积累上是远远不及一些成熟公司的,这些沉淀和积累就是成熟公司在技术上的先发优势,当技术没有出现变革的时候我们没有选择,但是当技术正出现重大变革的时候,如果我们还做同样的技术选型,那么也需要花同样的时间和成本才能达到成熟公司的水平,然后等大家都开始迁移到新的技术上的时候,这些技术沉淀和积累就可能会变成技术债务。

所以初创公司应该去预判技术趋势,选择面向未来的技术,在技术上弯道超车,避免自己的技术债务,这个是伴鱼技术团队对技术后发优势的理解。

成本与效率的衡权

成本和效率是技术选型绕不过的关键点,对于数据库来说更是如此,因为数据库需要的机器等资源成本会占总资源成本的很大一部分,所以伴鱼技术团队在 TiDB 和 MySQL 做选择的时候,对成本与效率进行了深度的评估。

Unix 哲学是经过时间和实践的锤炼设计原则,很多时候也是伴鱼技术团队的实践原则,比如 Rule of Economy :「宁花机器一分,不花程序员一秒」。在技术选型上,我们是总是期望基础软件做更多的事情,业务研发做更少的事情,如果业务研发需要在业务层去做原本基础软件应该做好的事情,那其实就是基础软件的抽象泄漏了。如果基础软件抽象泄漏,必然会导致业务层重复去解决这个问题,这个其实是非常大的隐性成本。

MySQL 相比较 TiDB 而言,集群的高可用和大表需要分库分表其实就是 MySQL 在面对当前需求的抽象泄漏,MySQL 的集群高可用需要 DBA 和基础架构团队花成本去解决,MySQL 的大表分库分表方案需要 DBA、基础架构团队和业务研发团队花成本去解决,只不过这些都是隐性成本,不像在搭建数据库集群的时候,TiDB 比 MySQL 可能需要更多的机器来的简单直接,所以很容易被忽略了。

所以,对于成本与效率的衡权,伴鱼技术团队更关注工程师的效率,更关注工程师的心情(在业务上层重复解决一些底层软件抽象泄漏的问题是很影响心情的),更关注隐性成本,而不仅仅是账面明显可以比较的资源数字,特别是在机器越来越便宜,人才越来越值钱的趋势下。

技术生态与红利的思考

选择一个技术,其实也是选择了这个技术的生态,如果技术生态完善,做事情往往会事半功倍,极大地提高研发效率。TiDB 在这个方面做的非常好,全面兼容 MySQL 协议,让 TiDB 的用户享受到 NewSQL 的能力的同时也享受到 MySQL 的生态,这个是非常正确的决定,MySQL 生态是几十年的积累,不是一朝一夕可以做到的。

另一方面,在选择面向未来、优雅高效的解决方案,还是选择成熟的但不够优雅和高效的解决方案,如果选择成熟的解决方案,对技术的掌控会比较高,但是会在效率方面持续的进行付出;如果选择面向未来的解决方案,需要花时间和精力来掌握新技术,但是新技术会优雅和高效的解决问题,我们认为这个就是技术的红利。比如对于大表的解决方案,MySQL 提供的解决方案是分库分表,业务研发和 DBA 一起配合非常低效地解决这个问题,但是对于 NewSQL 的 TiDB,单表几乎可以理解为无限大的(业界已经存在 100 亿以上的表),从根本上解决了这个问题。现在伴鱼的大表都从 MongoDB 迁移到 TiDB 上面,业务研发和 DBA 不再为数据的增加而不停地进行分库分表,这个就是巨大的技术红利。

所以,基于上面的一些讨论与思考,伴鱼决定 All in TiDB,MongoDB 不再增加新的库和表,正在使用 MongoDB 的业务继续使用,并且对 MongoDB 上的大表进行有计划的迁移,避免进行分库分表操作。

踩过的坑

在完全掌握一项新技术前,享受新技术的红利是有代价的,特别是伴鱼在 TiDB 比较早期的时候就决定All in,这很考验技术团队的学习和进化能力、新技术的社区和官方提供的技术支持的能力。在 TiDB 这件事情上,伴鱼技术团队和 TiDB 的技术支持团队都做的非常优秀了,但是我们从 TiDB 1.x 到目前的 3.x 的过程中依然还是踩了一些的坑:

优化器选择索引问题

  • 单表数据 30W+,查询请求并发约 10+,某次业务上线,新增一个索引后,导致原有的查询索引选择错误,TiKV 实例所在机器 cpu 迅速被打满,引发故障。
  • 线上某张大表,请求量比较大,偶尔出现个别条件走不到索引,导致全表扫描,从而引发接口响应时间的抖动,影响业务。
  • 线上某张 14 亿的大表,查询条件区分度很高,某天出现特定条件突然走不到索引,导致全表扫描,引发故障。后面经过 TiDB 同学排查,系 bug 导致。

优化器选择索引问题,TiDB 从 1.x 到 3.x 的过程中,优化器表现越来越好,同时伴鱼 DBA 团队通过性能监控和慢日志监控提前快速地发现问题,并且对大表采用强制索引的方式避免隐患,目前这个问题已经比较彻底的解决了。

大数据同步问题

  • 为了进行数据分析,我们把上游各 TiDB 集群的数据通过 Pump / Drainer 汇聚到一个 TiDB 集群供大数据分析使用,在使用过程中,遇到数据不一致、数据同步慢和编码不一致导致同步失败等问题。

随着伴鱼的 DBA 团队深度研究 TiDB 并且和 TiDB 的同学进行持续的深入沟通,目前对 TiDB 的掌控力越来越强,大数据同步问题目前已经得到解决。

现在的情况

现在伴鱼有 10 套 TiDB 数据库,110+ 数据库实例,6 个 TPS 过万核心集群,999 线基本维持在 16ms 左右,响应时间和稳定性都达到预期。从目前的情况来看,伴鱼选择 TiDB 是一次非常正确的选择,我们在数据库技术方面弯道超车,避免了对 MySQL 技术的重复建设与积累,享受了 NewSQL 数据库 TiDB 在高可用和水平扩展等方面的技术红利,大大提高了业务研发和 DBA 的工作效率。当然,这是伴鱼技术团队(特别是DBA)和 TiDB 技术团队共同努力的结果。

这里还要特别提一点,TiDB 每一次版本升级都会带来惊喜,这是一个可以持续享受的技术红利。

写在后面的话

目前,在摩尔定律失效、业务的高可用要求和成本优化等综合的大环境下,分布式架构是技术潮流的大势所趋,流量路由策略加多副本部署(微服务是其中的一种架构形式)解决了无状态服务的分布式架构问题,Redis Cluster 和 Codis 等方案解决了缓存的分布式架构问题,Kubernetes 完成了操作系统的分布式进化,数据库领域自然也不会例外,它的分布式架构趋势一定是不可阻挡的。要特别说明一下,这里所说的解决问题是指系统性的解决问题,MySQL 业务侵入式的分库分表确实是一个可以解决问题的分布式架构方案,但是需要业务研发配合一个业务场景一个业务场景的去解决,这就不能称之为系统性的解决方案,因为在解决这个问题方式上,业务侵入式的分库分表方案将本应由数据库处理好的大表抽象泄漏给业务层了,在这个问题上,我们认为 NewSQL 是一个系统性的解决方案,而 TiDB 就是当下非常不错的一个选择。

另外还需要说明一点的是,这是一篇数据库选型的文章,所以只记录了与之相关的内容,比如详细描述了伴鱼技术团队在将数据库迁移到 TiDB 后踩的坑,因为这是我们数据库选型 TiDB 付出的代价,所以一定要详细记录;没有记录在使用其他数据库踩的坑,这并不代表我们没有踩到,比如在使用 MongoDB 的过程中也踩过一些坑,但是因为这并不是我们决定重新做数据库选型的原因(决定重新选型的原因见文章「为什么放弃 MongoDB」部分),所以就没有在文章中记录。

参考

前言

随着用户规模迅速扩张,业务场景越来越多,对消息产品功能要求高,如死信队列、延迟队列、 消费重置、消息查看等;写消息的延迟和稳定性要求也越来越高。 如有相对更加丰富的功能,可以节省很多时间,同时提升业务开发效率。

Kafka 方案及其痛点

之前,我们采用 Apache Kafka 作为消息平台, 为了让业务在高峰期(晚上八点到十点)不受影响,我们根据消息业务量的大小, 分别搭建了不同的集群。对于一些业务场景的需求, 比如需要重置 offset 来消费过去几天的消息,使用 Kafka 需要停掉消费者才可以进行, 这种方式对大量在线业务非常不利,只能采用重写消息或者一些不太灵活的方式来实现, 极大降低了使用体验。

我们在使用 Kafka 集群过程中,主要遇到以下问题:

...

阅读全文 »

前言

连锁故障是由于正反馈循序导致问题不断扩大的故障。一个连锁故障通常是由于整个系统中一个很小的部分出现故障而引发,进而导致系统其它部分也出现故障。比如某一个服务的一个实例出现故障,导致负载均衡将该实例摘除而引起其它实例负载升高,最终该服务的所有实例像多米诺骨牌一样一个一个全部出现故障。

那么,一个正常运行的服务是怎么发生连锁故障的呢?

服务器过载

服务器过载是指服务器只能处理一定 QPS 的请求,当发往该服务器的 QPS 超出后,由于资源部够等原因导致崩溃、超时或者出现其他的异常情况,结果导致服务器成功处理的请求远远不及正常情况可处理的 QPS 。这种成功处理请求能力的下降可能会导致服务实例的崩溃等异常情况,当服务崩溃后,负载均衡器会将请求发送给其他的集群,使得其他的集群的实例也出现过载的情况,从而造成整个服务过载的故障。一个过程通常非常快,因为负载均衡器的响应速度通常是非常快的。

资源耗尽

资源耗尽会导致高延迟、高错误率或者低质量的回复发生。而这些问题不断导致负载上升直至过载,从而发生连锁故障。下面来分析不同资源耗尽对服务器产生的影响:

cpu 资源不足一般会导致请求变慢,有以下几种情况:

  • 正在处理的请求数量上升,这会导致同一时间服务器必须同时处理更多的请求,也将会导致其他的资源的需求上涨,包括内存,线程,文件描述符等等资源的上涨;
  • 正在等待处理的队列过长,这会导致请求的延迟上升,并且队列过长也会导致内存使用量上升;
  • 线程卡住,如果一个线程由于等待某一个锁而无法处理请求,可能服务器无法在合理的时间内处理健康检查请求而被重启;
  • cpu 死锁或者请求卡住,由于 cpu 死锁或者请求卡住,导致健康检查无法通过而被重启;
  • 由于 cpu 资源不足导致响应变慢引起 rpc 超时,而 rpc 超时可能会导致客户端的重试,造成系统的过载;
  • cpu 缓存效率下降,cpu 使用率越高,导致任务被分配到多个 cpu 核心上的几率越大,从而导致 cpu 核心的本地缓存失效,进而降低 cpu 处理的效率;

内存资源不足会导致以下的情况发生:

  • 任务崩溃,内存不足可能会导致任务被系统 oom 或者自身逻辑导致服务崩溃;
  • gc 速率加快,导致 cpu 使用率上升,cpu 使用率上升导致请求变慢,进一步导致内存上升。(gc 死亡螺旋)
  • 缓存命中率下降,可用内存的减少会导致缓存命中率的降低,导致向后端发送更多的 rpc,可能会导致后端服务过载;

线程不足会导致以下情况的发生:

  • 导致请求错误,这可能会导致客户端的重试,造成系统的过载;
  • 导致健康检查失败而被重启;
  • 增加的线程会消耗更多的内存;
  • 极端情况下会导致进程 id 不足;

文件描述符不足会导致以下情况的发生:

  • 导致无法建立新的网络连接导致请求错误;
  • 导致健康检查失败而被重启;

服务不可用

当资源耗尽导致服务的崩溃,比如内存耗尽等等,一个服务实例不可用,比如崩溃等等。由于负载均衡会自动忽略不健康的实例,导致其他健康实例的负载升高,从而导致连锁故障。

怎么避免连锁故障

那么怎么来应对连锁故障呢?一般来说可以采用下面的方法来避免连锁故障,按优先级排列为:

  • 进行压力测试,测试服务器的极限,同时测试过载情况下的失败模式;
  • 提供降级结果;
  • 在过载情况下服务主动拒绝请求;
  • 在反向代理层,针对请求的特性进行数量限制(ip),防止 ddos 攻击;
  • 在负载均衡层。在服务进入全局过载时进入主动丢弃请求。
  • 服务自身避免负载均衡的随机抖动导致过载;
  • 进行容量规范,容量规范只能减少连锁故障的可能性,不能避免连锁故障;

避免连锁故障的具体的策略为:

队列管理

提前规划好请求队列容量,当队列满时服务器主动拒绝新的请求。另外在服务器过载的时候,后进先出的队列模式比先入先出要好。

流量抛弃

流量抛弃有两个方式:

  • 服务器流量抛弃:在服务器临近过载时,主动抛弃一定量的负载。比如 cpu 达到一定得使用率、内存达到一定得使用率或者服务器请求队列容量达到最大值的时候,服务器端可以对一些流量直接抛弃。
  • 客户端流量抛弃:反向代理或者负载均衡层在系统快进入或者已经进入连锁故障的情况下,直接抛弃一部分流量。

流量抛弃可以和一些策略进行结合,比如请求的优先级,用户的优先级等等。同时流量抛弃和截止时间配合起来效果非常不错。

优雅降级

优雅降级是在接受该请求的情况下,通过降低回复的质量来大幅较少服务器的计算量。流量抛弃已经让服务器直接少处理了很多请求,但是对于已经接受的请求,服务器是需要处理的,这个时候如果有优雅降级机制,能大大较少服务器的计算量,并且能一定程度的保证用户体验。

重试控制

  • 使用随机化,指数型递增的重试周期,防止重试风暴;
  • 限制每个请求的重试次数,防止在服务器过载的情况下,出现重试出错,出错重试导致连锁故障;
  • 考虑全局重试预算。比如每个服务每分钟只容许重试 60 次,重全局的角度控制重试的范围和力度;
  • 不要在多个层数上重试,一个高层的请求可能会导致各层的重试,所以重试的时候,一定要明确在一个层面重试,防止多层重试导致重试的放大;
  • 使用明确的错误码,将可重试错误和不可重试错误分开,不可重试的错误一定不要重试。一般来说临时错误是可以重试的,非临时错误或者服务过载的时候,就不应该再进行重试;

请求延迟和截止时间

在顶层给每一个请求增加一个截止时间,并且在每一层进行传递,同时每一层在请求之前进行检查,过期的请求直接抛弃。在服务器过载的情况下,请求的延迟会加大,请求会在队列中排队等待很长的时间,比如一个请求等待 30s 后才开始执行,但是对于客户端来说,用户早已经放弃等待该请求的结果了,所以这对这样的请求继续执行是没有意义的,只会浪费服务器的计算资源,进一步加速了连锁故障。所以对于这样的请求,应该在请求前直接抛弃,将服务器的计算资源应用在其他有意义的请求上面;

保持调用栈永远向下

同层通信容易导致分布式死锁,比如一个服务实例 a 由于线程池没有空闲线程而将请求挂起,这个时候如果实例 b 将将请求转发到实例 a 而导致实例 b 线程的消耗,在最坏的情况下可能会导致连锁故障的发生。一个比较好的方式是将同层通信的逻辑转交给客户端来处理,比如一个前端需要后后端通信,但是猜错了后端服务,这个时候后端服务应该返回正确的后端服务,让客户端再次发起请求,而不是直接代理请求到正确的后端服务。

连锁故障测试

测试直到出现故障,再继续测试,通过测试发现连锁故障出现的原因;并且也应该测试非关键性的后端,确保它们的不可用不会影响到系统中的其他关键组件,比如它们会不会影响请求的时延,会不会导致正常请求的超时等等。

总结

当一个系统过载的时候,一定需要牺牲一些东西的,这样比尝试继续请求而导致所有请求都不能正常服务要好。理解这些临界点以及超过这些临界点后系统的行为模式,是我们避免连锁故障必须掌握的。

一般来说,我们为了降低服务背景错误率或者优化稳定状态的改变反而会让服务更容易出现事故。比如在请求失败时的重试、负载自动转移、自动杀掉不健康的服务器、增加缓存提高性能或者降低延迟等等这些手段都是为了优化正常情况下服务器的性能,但是这也会提高大规模服务故障的几率。所以一点要小心评估这些改变!

本文将主要介绍伴鱼在TiDB数据库中间件平台建设过程中的实践与思考。

背景

伴鱼是一家在线教育公司,目前正处于业务高速增长期。伴鱼从2018年开始使用TiDB分布式数据库作为核心关系型数据库,在生产环境已经大规模使用,现有10+套TiDB集群,服务于绘本、口语、中台等核心业务线。

在技术架构层面,我司采用了微服务架构,各个服务之间通过通过自研的服务治理框架实现相互访问。对TiDB数据库的访问也有相应的SDK封装。SDK集成了数据库路由的功能,通过集群名和表名即可获得访问对应数据库的权限,而无须关注用户名密码等连接相关配置。这种数据库访问方式确实为微服务研发同学提供了便利,但是随着数据规模增大、公司业务线逐渐复杂,这种方式暴露出越来越多的问题。

第一,缺少访问控制机制。只要能拿到相应的集群名和表名,即可获取对应账号信息访问数据库,实际在公司内部也确实存在多个微服务通过同一个路由访问数据库的情况。当出现数据库相关问题时,难以快速定位相关服务。

第二,难以实现数据库统一接入。SDK路由查询获取TiDB集群对应的SLB地址,直接使用后端TiDB账号信息访问数据库。统一接入能力重度依赖SDK,对原生数据库客户端支持较差。

第三,兼容性、灵活性差。目前只能通过内部SDK访问数据库。如果使用原生客户端,需要向DBA提特殊申请,获取集群地址和账号信息,而这样又会失去SDK提供的统一监控、熔断等治理能力。

我们希望借助数据库中间件解决以上问题,然而经过调研发现,目前的开源数据库中间件大多面向MySQL、PostgreSQL等传统数据库,以提供分库分表功能为主,而我们所需要的统一接入、多租户、数据库治理等功能相对比较薄弱,而且对接其他内部系统时的二次开发成本也比较高。因此我们决定研发一套面向TiDB分布式数据库的中间件平台,一站式解决数据库管理和治理的问题。本文将从整体架构、功能实现、未来规划这几个方面,详细介绍伴鱼数据库中间件建设历程。

...

阅读全文 »