暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

TiDB 在 eBay丨亿优百倍 : 商品数据服务 TiDB 性能优化

PingCAP 2022-01-13
853


作者丨陈彦杰

编辑丨林颖

供稿丨eBay 技术荟

导读

“亿优百倍” 是 eBay 智能营销团队推出的系列文章,分享了在营销商品数据服务系统的架构、设计、代码方面的一些理解和研究。在上期的 “亿优百倍丨商品数据服务百倍性能优化之路” 里,我们介绍了项目的背景、总体设计和优化路线图。本期 “亿优百倍”,我们分享了对 MIS 的优化方法,以提高了 TiDB 在 eBay 平台上使用的性能和稳定性。

TiDB 简介

在 2020 之前,我们主要使用 NoSQL 和关系型数据库来存储数据。NoSQL 数据库有着不错的性能和扩展性,但是很少有完备的二级索引支持;而关系型数据库有完整的索引支持,但是扩展性有限,并且商业版本的关系型数据库往往成本高昂。另外,我们有大量数据存储在 Hadoop 上,需要进行线上线下同步,并使用 Spark 处理。Spark 和常见数据库进行数据同步时,通常使用 JDBC(Java Database Connectivity)作为接口,但由于 JDBC 本身的限制,在进行超大数据并发时会成为严重的带宽瓶颈。

从 2020 年开始,我们尝试使用 TiDB,代替 NoSQL 和关系型数据库,并尝试使用 TiDB 来构建 MIS 的核心存储。

TiDB 是一个 HTAP(混合事务/分析处理,Hybrid Transactional/Analytical Processing)数据库,它可以同时服务于批处理数据和事务型数据查询。TiDB 包含三个核心组件 —— PD、TiDB 和 TiKV
  • PD(Placement Driver)是有状态元数据节点,可以存储元数据并为集群授时。PD 收集集群状态信息,并负责集群调度。

  • TiDB 服务器(区分于TiDB集群)是无状态查询节点,负责接受客户端请求,将 SQL 查询转换成 TiKV 能接受的 Key 查询,并负责事务处理。

  • TiKV 是有状态存储节点,它将所有数据以 Key-Value 的形式存储在底层的 RocksDB[1](一个性能优异的单机 NoSQL 数据库)中。TiKV 使用了两个 RocksDB 实例,分别存储查询数据和日志数据。因此TiKV底层数据的存储形式和大部分 NoSQL 数据库一样,都是 SSTable。SSTable 的特性之一就是 Key 有序,这个特性决定了 TiDB 数据分片的模式 —— 将连续 Key 分段,而不是按 Key 做哈希。另外,TiDB 数据管理的基本单位是 Region(即 Key 连续的数据块)。由于数据块是以 SSTable 形式实现,而且一个 Region 内部本身就是有序的,TiDB 只要保证 Region 相互之间是有序的,就可以得到一个全局有序的数据集。



除了 TiDB 三个核心组件之外,还有使用 Spark 查询 TiDB 的组件 —— TiSpark。TiSpark 是 Spark 连接器,可以绕过 JDBC 性能的限制,直接读写 TiKV 底层数据,这极大地提升了读写性能。

以下是 TiDB 整体架构图,感兴趣的朋友可以进一步查看官网上对存储[2],计算[3],调度[4]的详细介绍。

图 1 TiDB 整体架构

我们目前拥有一个生产集群,包含 3 个 PD 节点、25 个 TiDB 节点和 27 个 TiKV 节点。TiDB 集群中存储的数据集有 30 亿左右,字段数量大约有 70 个。我们在 TiDB 使用过程中主要在 HTAP、跨数据中心和地域亲和性、查询计划稳定性等方面对 TiDB 进行调优,以提高 TiDB 在 eBay 平台上使用的性能和稳定性。

HTAP 带来的性能挑战和解决方案

问题的发现

TiDB 从一开始就支持混合在线离线数据处理,它可以同时支持 Spark 这种批处理查询和 JDBC 这种在线查询的需求。在 MIS 的系统中,我们对每日数据校正的任务需要通过 Spark 读全量 TiDB 数据,并且和 Hadoop 上的商品快照(Snapshot)数据作对比从而进行误差校正。而在实践中我们发现,当 Spark 查询和 JDBC 查询集中在同一个时间段时,查询延迟会大幅上升,并且其性能会剧烈抖动。Spark 会在 1 小时内读取超过 20 亿行数据,达到 600K IPS。在这种情况下其他查询延迟 P95 会从平均 30ms 左右上升到 800ms。由于所有的在线和离线查询都会请求 TiKV 数据,这就导致当大量在线和离线查询都堆积到 TiKV 时,会产生 HTAP 干扰的情况,从而严重影响性能。下图展示了 JDBC 延迟与 TiKV 负载关系,从图中可以发现:在有 Spark 任务时 TiKV 负载大幅上升,相应的 TiDB 查询时间剧增。
图 2 JDBC 延迟与 TiKV 负载关系

解决方案

对于上述这个问题,一种比较简单想法是:拉长 Spark 读取 TiDB 的时间, 以降低读取 TiDB 的速率。但这种方法只能相对降低延迟,无法从根本解决干扰问题。如果 Spark 读取 TiDB 的时间过长,就会导致一致性问题,使得 Spark 读取到的 TiDB 数据可能已经不是最新的。另外 Spark 读取 TiDB 时需要有合适的时间戳(TSO),如果时间戳超过了 GC Safepoint,读取会被 TiDB 拒绝而导致报错。

那么如何解决这种 HTAP 干扰的问题呢?官方其实已经给出了答案。

从 TiDB 4.x 开始,官方推出 TiFlash[5] 作为列式存储引擎。相比于 TiKV 封装了 RocksDB,TiFlash 封装了 Clickhouse,把数据从 TiKV 同步到 TiFlash。

在 TiDB 的数据模型中,Region 是一个逻辑上的概念,是一个 Key 连续的数据段。但物理上,一份 Region 会有多个副本,每个副本被称为 Peer,这些 Peer 用 Raft 协议组成 Group,分布在 TiDB 集群中。

Peer 在 Raft 协议下有三种角色:

  1. Leader:负责响应客户端的读写请求;
  2. Follower:被动地从 Leader 同步数据,当 Leader 失效时会进行选举产生新的 Leader;
  3. Learner:只参与同步 raft log 而不参与投票。




在 TiDB 3.x 阶段,Learner 只短暂存在于添加副本的中间步骤。进入 TiDB 4.x 之后,TiFlash 内所有 Peer 全部作为 Learner 加入 Raft Group,只同步数据而不响应客户端的读写请求。这样既不影响现有的 Peer,又使 TiFlash 可以同步 TiKV 数据。Spark 任务可以选择读取 TiFlash,直接请求列数据,不影响 TiKV 中在线查询的请求。

我们搭建了测试环境,对 TiDB 4.x 进行充分测试。我们发现使用 Spark 查询时,读取 TiFlash可以比 TiKV 快 3 倍,同时对在线查询没有影响。我们将 TiDB 生产环境升级到 4.x 版本,并且添加 TiFlash 组件。生产环境和测试环境得到的结果基本一致,在 Spark 读取 TiDB 时,所有的请求都会读取 TiFlash,而且读取 TiKV 的 JDBC 请求不受任何影响。如图 3 所示,(对比图 2)TiDB 的负载只出现在 TiFlash 中,并且查询延时在有 Spark 任务时变得非常稳定。延迟从之前最高 800ms 下降到 30ms,IPS 从之前全部读取 TiKV 时的 5K,提升到分别读取 TiKV 和 TiFlash 时的 30K。

图 3 JDBC 延迟与 TiFlash 负载关系


TiDB 跨数据中心和地域亲和性优化

背景介绍

时间戳(TSO)

在 TiDB 中,全局时间戳(TSO)是一个非常重要的概念,它有两个核心应用场景:事务处理(Transaction)和多版本并发控制(MVCC)。

TiDB 使用 Percolator 分布式事务模型。Percolator 采用的是一种两阶段提交(two phase commit)的方式,分别为预写阶段(Pre_write)和提交阶段(Commit)。预写阶段需要一个开始时间戳,作为当前事务的开始版本号。当所有预写完成之后,TiDB 获取提交时间戳,将事务状态存储在 TiKV 上。

TiDB 对多版本并发控制以 ‘Key+ 时间戳’ 作为主键,因此需要获取一个全局单调递增的时间戳。该时间戳将作为 Key 的版本号,通过 Seek 的方式拿到当前最高版本的 Key,也就是 Key 上最新的数据。

在工业界,分布式环境下的时间戳获取主要有非集中授时和集中授时两种。

非集中授时的代表是 Google Spanner 和 CockroachDB,前者采用硬件级别的时间授时机制(原子钟 + GPS),再加上算法层面的控制,将授时延迟控制在 1ms - 7ms 之内;而后者采用 NTP(Network Time Protocol)加算法进行授时,但会牺牲了一些事务上功能。

集中授时的代表是 TiDB,TiDB 将一台 PD 节点作为全局唯一的授时服务器,所有时间戳请求都会集中到这一个节点。这样做的好处是使 TiDB 能完整支持事务处理需求,并且无需硬件支持。但缺点也很明显,当我们需要在多个数据中心做灾备或者服务跨数据中心的流量请求时,授时服务器的请求就会跨数据中心,延时时间为 10ms - 20ms 不等。另外,由于跨数据中心网络请求更容易受到网络波动的影响,导致跨数据中心的授时服务器请求不稳定。

图 4 跨数据中心的授时服务器请求

Leader 读取

前面已经说过,在 TiDB 的数据模型中,Region 有多个副本,这些副本分布在所有 TiKV 节点上,只有副本中的 Leader 节点负责响应客户端的读写请求。当 Leader 分布在多个数据中心时,TiDB 节点访问 Region 时必然需要跨数据中心读取 Region 的 Leader。请求量越大,读取 Leader 的网络延迟开销影响越大。

TiDB 之所以一定要求只从 Leader 读写数据,这是为了保证强一致性。虽然 TiDB 从 4.x 开始支持 Follower 读取,但是 Follower 读取之前,先要向 Leader 请求时间戳,保证 Follower 和 Leader 之间版本一致,然后再从 Follower 读取数据。这相当于多了一次网络开销,与在跨数据中心的网络环境下和直接读取 Leader 需要的网络开销没有太大改善,所以并没有解决跨数据中心的问题。

TiDB 本身支持读取历史数据,因此有一种变通的办法可以不用经过 Leader,直接从 Follower 读取该节点最新数据。但这种方法也有两个问题:一个是原来读取 Leader 可以保证强一致性,而读取 Follower 的最新数据只能保证最终一致性,一致性保证降低;二是从实际操作角度上说,直接读取 Follower 最新数据是读取历史数据的一个变种,需要显式地带上历史数据的时间戳,因此实际查询中需要在 SQL 的最后带上和查询逻辑无关而和系统有关的时间,这就令人难以理解和维护。

问题的发现

eBay 拥有多个数据中心。当我们最初使用 TiDB 时,并没有注意到将 TiDB 部署在多个数据中心会影响 SQL 查询的性能。因此我们最初将 TiDB 的各个节点平均部署在多个数据中心,用以服务来自多个数据中心的流量,并用多个数据中心作灾备。

在问题发现前,我们先来理解一下 Grafana 仪表盘上 PD 客户端中 PD TSO RPC Duration(如下图所示)的含义:该图表现了获取时间戳过程中的纯网络开销。P99 在平时不超过 5ms,而高峰时不超过 10ms。读取 Region 和获取时间戳的网络开销基本相同。

图 5 获取时间戳的网络开销

通过理解上图的含义,我们在后来的回溯中发现,当跨数据中心的请求出现时,纯网络开销延迟在低谷时期(IPS < 1K)P99 大约是 20ms。一旦 IPS 上升到 10K,P99 会达到 60ms,由此带来的整体查询延迟会超过 200ms。IPS 越高,网络抖动造成的延迟上升越明显。在跨数据中心的网络条件下,能满足我们延迟需求的 IPS 最高在 25K 到 30K 之间,并且伴随着极为明显性能抖动。

解决方案

TiDB 集群搭建在两个数据中心上,以下简称为 A 中心和 B 中心。为了缩小网络延迟开销,提升 IPS 必须去除影响读写性能的跨数据中心网络延迟开销,同时保证内部系统的性能和稳定性。跨数据中心的请求存在于 PD、TiDB 和 TiKV 之间,主要由时间戳请求和 Region 读取造成。因此,我们的目标是让获取时间戳和读取 Region 这两个操作只发生在本地数据中心,以消除跨数据中心的操作。

我们选择 A 中心作为主中心,将授时服务器和 Region Leader 集中在 A 中心,服务于外部请求;而 B 中心只作为同步备份中心,包含 PD Follower 和 Region Follower,不对外服务。B 中心只有当 A 中心完全不可用时才会起到灾备作用。A 中心和 B 中心的具体配置方法如下图所示:

图 6 两数据中心架构

1、PD
上述介绍过 PD 是元数据管理和授时服务器。所有的 PD 节点共同组成了 Raft Group,而 Raft Group 内同一时间只有一个 Leader 节点,其余的都是 Follower 节点。只有 Leader 节点作为 PD 的服务端点,而 Follower 只负责同步。因此所有的 Region 位置请求以及时间戳请求都会请求同一个 PD 节点。我们将三个 PD 节点中的两个放在 A 中心(随机选择其中一个作为 Leader 节点,另一个作为 Follower 以防 Leader 下线),另一个放在 B 中心(只有当 A 中心整体不可用时该节点才会作为 Leader,以保证数据不丢)。

2、TiDB
TiDB 是无状态查询节点,由于 B 中心不对外服务,因此不保留 TiDB 节点,将其全部放在 A 中心。

3、TiKV
TiKV 存储着所有数据,数据以 Region 为划分。TiDB 可以承受的最大副本失效个数为 “(副本数/2) -1”。我们目前将副本数量设为 7,TiDB 可以在 3 个 TiKV 节点同时失效的情况依然保持可用状态。数据副本数量越多可靠性越高,但同时由于写副本增加,性能也会受到越多影响。我们将 27 个 TiKV 中的 18 个节点放在 A 中心,9 个节点放在 B 中心。同时在布局规则中设置 A 中心 6 个副本,这些副本有机会成为 Leader,而 B 中心保留 1 个副本,不能作为 Leader,只能作为 Follower 同步数据。

如下图 TiDB 内部的监控可以看到,经过优化后,当所有 Leader 节点都在同一数据中心时,授时请求 P99 延迟在 4ms 左右,并且当 IPS 持续上升到 200K 时,延迟也不会超过 10ms。对比之前在 IPS 在 10K 时 P99 达到 60ms,延迟有非常明显的下降。由于网络延迟下降并且保持稳定,查询延迟 P95 下降到 30ms,在 IPS 200K 时不超过 50ms。
图 7 QPS 20K(IPS 200K)情况下的网络延迟

查询计划稳定性

TiDB 和大部分数据库一样,采用了基于成本的查询优化(Cost-Based Optimization,CBO)。TiDB 会自动收集每一条记录对应的更改,并据此给出每张表的一些基本信息,比如总行数、字段上的非重复值数量、空值数量、平均长度等。TiDB 会根据统计信息生成物理执行计划,这些计划可以通过 “explain” 查看。

问题的发现

我们发现在执行的大量查询中,有些查询特别慢。在通过查询执行计划过程中,我们惊讶地发现,对于同一个 SQL statement 出现了两个执行计划。其中一个执行计划是根据索引查询,而另一个执行计划进行的是扫表查询。当 SQL 查询不幸使用了扫表查询计划,查询时间会远远大于正常使用索引查询的时间,往往几条扫表 SQL 查询就会导致整个集群查询性能下降。我们发现出现这种情况的原因是:当没有纳入统计的行数占总行数的比例超过一个阈值(pseudo-estimate-ratio)时,TiDB 会认为统计信息不准,转而使用伪估计(Pseudo Estimate)方式。而伪估计得到的结果集大小可能会大幅波动,导致出现索引和扫表两种查询计划。

解决方案

我们知道统计信息本身也是一张表,这张表需要进行执行收集分析(analyze)才会更新。TiDB 本身会根据更改的行数与总量的比例(tidb_auto_analyze_ratio)进行自动收集。但实践中我们发现,一方面自动收集的触发不是很稳定,有时自动收集的比例已经达到,但自动收集分析的行为并没有如期被触发。另一方面收集分析的行为是一个相对来说比较重的操作,如果自动收集和业务高峰正好撞车,就会容易导致业务不稳定。所以,在实践中通常建议使用 Crontab 定时进行收集分析。

但通过上述定时收集分析统计信息并没有完全解决我们的问题。由于我们更新非常频繁(平峰  4K IPS,高峰 16K IPS),可能会出现某一段时间更新行数暴涨而定时收集分析还未开始的情况。这时由于没有纳入统计的记录超过了阈值,导致依然会触发伪估计。一旦伪估计得出扫表执行计划,问题就会出现。

TiDB 官方已经意识到由于统计信息不准会导致查询计划出现扫表的问题,因此推出了执行计划绑定(SQL Binding)的方式。TiDB 可以将一个 SQL 绑定到一个等价但带索引提示(Index Hint)的 SQL 上,所有和这个 SQL 使用同一种 statement 的 SQL 都会被绑定到这个带索引提示的 SQL 上。这样就可以绕过统计信息,直接使用绑定的物理计划查询。

但不幸的是我们的业务并不适合这种方案,因为绑定执行计划需要固定 AND/OR/NOT 这种逻辑关系,上游 SQL 查询中会带不定长度的 OR 条件,用于查询多个 Item 的属性,Item 数量最少是 1,最大可能有 100。每个 Item 数量都需要绑定一种执行计划,也就是说如果我们采用这种方案,绑定的执行计划会超过 100 种。如果将来查询的属性发生变动,这些执行计划绑定都需要更改。这显然是我们无法接受的维护成本。还有一种办法是我们将同一层级的 OR 逻辑合并成 IN 逻辑,这样所有的 Item 查询就可以使用同一个绑定。但这样就是对特定场景的补丁,不符合通用性原则,并且在架构上会额外多出一层逻辑,增加未来的维护成本,因此该方法也不适合我们。

为了彻底解决这个问题,我们回顾业务场景,发现虽然我们的更新量非常大,但整体数据分布并不会大幅改变,因此使用更新之前的统计信息从概率分布的角度上并没有区别。因此我们将伪估计触发阈值(pseudo-estimate-ratio)设为 1,以此来完全禁用伪估计,使用收集分析之前的统计信息来生成物理执行计划。结果证明:由于整体概率分布没有改变,使用没有更新过的统计信息也能非常准确地生成执行计划。有时与其去按照某种算法估计一个值,还不如使用过去的统计信息。

其他 TiDB 的调优

横向扩展

从工程角度来说,提升性能最简单的方式是增加机器数量。TiDB 具有非常好的扩展性,可以通过 ansible (3.x) 或者 tiup (4.x) 很好的扩展集群。通过测试我们发现,将 TiKV 从 22 台扩展到 27 台之后(22%),性能提升了约 10% 到 15%。由此可以看出,在实际情况下,TiDB 虽然可以进行横向扩展,但性能并不是随着机器数量的增加而线性提升,由于 TiDB 架构中存在一些全局唯一服务器(如上文说的 PD 授时),实际增加的性能略小于节点数量的增加,但增加的节点数量依然是提升性能的有效手段。

磁盘选择

TiDB 在线数据查询几乎都是对磁盘的随机读写, 所以对于 TiKV 节点的磁盘读写有比较高的要求,不仅要求是 SSD(最好是 NVMe)的磁盘,以满足高并发随机读写的高带宽和低延时需求。由于我们使用的是虚拟机节点,即使是 SSD 磁盘,性能也不一定能达到 TiDB 对磁盘的 Benchmark 要求,所以我们只能从 eBay 云环境中挑选一些磁盘性能相对比较好的机器作为我们 TiDB 的节点。另外为了能通过 Benchmark,我们更改了 Ansible 脚本中对于磁盘性能测试的阈值。

总结与展望

本篇通过解决 HTAP 相互干扰问题、解决跨数据中心数据传输问题、稳定查询计划并对其他方面进行调优,我们将 TiDB 的性能,在整体延迟 P95 < 50ms 的要求下,从最初的 5K IPS 提升到了 200K IPS。

但是,在跨数据中心的优化中,由于目前 TiDB 在异地多数据中心场景下的局限,我们牺牲了一些灾备能力来提高性能。假设 TiDB 搭在三个数据中心而不是两个数据中心上,那么禁止 TiDB 产生跨数据中心的流量会在一个数据中心不可用时失去整个集群的可用性(TiDB 搭在两个数据中心上时则没有区别,因为无论如何配置都无法在一个拥有多数节点的数据中心不可用时继续服务);而如果允许 TiDB 产生跨数据中心的流量,当三中心中的一个发生不可用,整个集群依然可以对外服务。未来,希望能有更好的方案来平衡性能和灾备。

下一期,我们将分享商品数据服务系统中缓存层和代码层面的优化经验,敬请期待!

参考资料:

[1]http://rocksdb.org

[2]https://docs.pingcap.com/zh/tidb/stable/tidb-storage

[3]https://docs.pingcap.com/zh/tidb/stable/tidb-computing

[4]https://docs.pingcap.com/zh/tidb/stable/tidb-scheduling

[5]https://docs.pingcap.com/zh/tidb/v4.0/tiflash-overview

本文版权和/或知识产权归 eBay Inc 所有。如需引述,请和我们联系 DL-eBay-mkt-mis-pub@ebay.com。本文旨在进行学术探讨交流,如您认为某些信息侵犯您的合法权益,请联系我们 DL-eBay-mkt-mis-pub@ebay.com,并在通知中列明国家法律法规要求的必要信息,我们在收到您的通知后将根据国家法律法规尽快采取措施。

文章转载自PingCAP,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论