亿优百倍|商品数据服务TiDB性能优化
作者|陈彦杰
编辑|林颖
供稿|Marketing Tech Team
本文共6873字,预计阅读时间15分钟
更多干货请关注“eBay技术荟”公众号
导 读
“亿优百倍”是eBay智能营销团队推出的系列文章,分享了在营销商品数据服务系统的架构、设计、代码方面的一些理解和研究。在上期的“亿优百倍|商品数据服务百倍性能优化之路”(点击阅读)里,我们介绍了项目的背景、总体设计和优化路线图。本期“亿优百倍”,我们分享了对MIS的优化方法,以提高了TiDB在eBay平台上使用的性能和稳定性。
1
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平台上使用的性能和稳定性。
2
HTAP带来的性能挑战和解决方案
2.1
问题的发现
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负载关系
(点击可查看大图)
2.2
解决方案
对于上述这个问题,一种比较简单想法是:拉长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协议下有三种角色:
①Leader:负责响应客户端的读写请求;
②Follower:被动地从 Leader同步数据,当Leader失效时会进行选举产生新的Leader;
③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负载关系
(点击可查看大图)
3
TiDB跨数据中心和地域亲和性优化
3.1
背景介绍
3.1.1 时间戳(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 跨数据中心的授时服务器请求
(点击可查看大图)
3.1.2 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的最后带上和查询逻辑无关而和系统有关的时间,这就令人难以理解和维护。
3.2
问题的发现
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之间,并且伴随着极为明显性能抖动。
3.3
解决方案
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)情况下的网络延迟
(点击可查看大图)
4
查询计划稳定性
TiDB和大部分数据库一样,采用了基于成本的查询优化(Cost-Based Optimization,CBO)。TiDB会自动收集每一条记录对应的更改,并据此给出每张表的一些基本信息,比如总行数、字段上的非重复值数量、空值数量、平均长度等。TiDB会根据统计信息生成物理执行计划,这些计划可以通过“explain”查看。
4.1
问题的发现
我们发现在执行的大量查询中,有些查询特别慢。在通过查询执行计划过程中,我们惊讶地发现,对于同一个SQL statement出现了两个执行计划。其中一个执行计划是根据索引查询,而另一个执行计划进行的是扫表查询。当SQL查询不幸使用了扫表查询计划,查询时间会远远大于正常使用索引查询的时间,往往几条扫表SQL查询就会导致整个集群查询性能下降。我们发现出现这种情况的原因是:当没有纳入统计的行数占总行数的比例超过一个阈值(pseudo-estimate-ratio)时,TiDB会认为统计信息不准,转而使用伪估计(Pseudo Estimate)方式。而伪估计得到的结果集大小可能会大幅波动,导致出现索引和扫表两种查询计划。
4.2
解决方案
我们知道统计信息本身也是一张表,这张表需要进行执行收集分析(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,以此来完全禁用伪估计,使用收集分析之前的统计信息来生成物理执行计划。结果证明:由于整体概率分布没有改变,使用没有更新过的统计信息也能非常准确地生成执行计划。有时与其去按照某种算法估计一个值,还不如使用过去的统计信息。
5
其他TiDB的调优
5.1
横向扩展
从工程角度来说,提升性能最简单的方式是增加机器数量。TiDB具有非常好的扩展性,可以通过ansible(3.x)或者tiup(4.x)很好的扩展集群。通过测试我们发现,将TiKV从22台扩展到27台之后(22%),性能提升了约10%到15%。由此可以看出,在实际情况下,TiDB虽然可以进行横向扩展,但性能并不是随着机器数量的增加而线性提升,由于TiDB架构中存在一些全局唯一服务器(如上文说的PD授时),实际增加的性能略小于节点数量的增加,但增加的节点数量依然是提升性能的有效手段。
5.2
磁盘选择
TiDB在线数据查询几乎都是对磁盘的随机读写, 所以对于TiKV节点的磁盘读写有比较高的要求,不仅要求是SSD(最好是NVMe)的磁盘,以满足高并发随机读写的高带宽和低延时需求。由于我们使用的是虚拟机节点,即使是SSD磁盘,性能也不一定能达到TiDB对磁盘的Benchmark要求,所以我们只能从eBay云环境中挑选一些磁盘性能相对比较好的机器作为我们TiDB的节点。另外为了能通过Benchmark,我们更改了Ansible脚本中对于磁盘性 能测试的阈值。
6
总结与展望
本篇通过解决HTAP相互干扰问题、解决跨数据中心数据传输问题、稳定查询计划并对其他方面进行调优,我们将TiDB的性能,在整体延迟P95<50ms的要求下,从最初的5K IPS提升到了200K IPS。
但是,在跨数据中心的优化中,由于目前TiDB在异地多数据中心场景下的局限,我们牺牲了一些灾备能力来提高性能。假设TiDB搭在三个数据中心而不是两个数据中心上,那么禁止TiDB产生跨数据中心的流量会在一个数据中心不可用时失去整个集群的可用性(TiDB搭在两个数据中心上时则没有区别,因为无论如何配置都无法在一个拥有多数节点的数据中心不可用时继续服务);而如果允许TiDB产生跨数据中心的流量,当三中心中的一个发生不可用,整个集群依然可以对外服务。未来,希望能有更好的方案来平衡性能和灾备。
下一期,我们将分享商品数据服务系统中缓存层和代码层面的优化经验,敬请期待!
Reference
[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支付核心账务系统之“展”翅高飞
干货|eBay基于Istio的应用网关的探索和实践
eBay支付账务系统架构解析之“读”一无二
点击阅读原文,一键投递
eBay大量优质职位虚席以待
我们的身边,还缺一个你