当前位置: 首页 > 科技观察

简单了解TiDB架构

时间:2023-03-18 17:37:55 科技观察

1.前言如果你看过我以前的文章,你就会知道我写了很多关于MySQL的文章。从我的Github汇总仓库可以??看到:可能不是很全面。算是对MySQL有了一个浅薄但全面的了解。之前也跟朋友聊过,基于现有的微服务架构,大部分性能瓶颈都不在服务上,因为我们的服务是可以横向扩展的。很多时候,瓶颈是“数据库”。比如,为了减轻MySQL的负担,我们会引入消息队列来降低流量高峰;比如我们会引入Redis来缓存一些不经常变化的数据,减少对MySQL的请求。另一方面,如果业务将大量数据倒入MySQL,没有进行优化,也会影响MySQL的性能。这种情况下就需要分库分表,实现起来比较麻烦。聊着聊着,聊起了分布式数据库。它的数据存储方式类似于RedisCluster。不管你塞给我多少数据,理论上我都能吞下去。这样就不用担心后期数据量大的时候需要分库分表了。正好在逛的时候看到了PingCAP的TiDB,就过来聊聊。2.因为文字通俗易懂,所以更强调存储1.TiDBServer从一个黑盒子开始。在理解之前,我们对TiDB的理解是我们把数据丢进去,TiDB负责存储数据。并且因为是分布式的,理论上只要存储资源足够,无论存储多少数据都可以。我们知道TiDB是支持MySQL的,或者说兼容MySQL的大部分语法。那么我们还是以一条Insert语句为切入点,来探究数据在TiDB中是如何存储的。首先,要执行语句,必须先建立连接。在MySQL中,MySQLServer负责处理客户端连接。TiDB也有相同的角色——TiDBServer。虽然角色相似,但两者之间有很多不同之处。TiDBServer对外暴露MySQL协议,负责SQL分析和优化,最终生成分布式执行计划。MySQL的server层也涉及到SQL的分析和优化,但与MySQL最大的区别在于TiDBServer是无状态的。而MySQLServer是有状态的,因为它与底层存储引擎耦合,部署在同一个节点上,将页面数据缓存在内存中。其实两者可以简单理解为TiDB是一种无状态的服务,可以横向扩展。而MySQL是单一服务,将业务数据缓存在内存中,无法横向扩展。由于TiDBServer的无状态特性,生产中可以启动多个实例,通过负载均衡策略对外提供统一的服务。实际上,TiDB的存储节点是分散部署的。这只是为了方便理解TiDBServer的水平扩展特性。别担心。稍后我们将讨论存储。综上所述,TiDBServer只做了一件事:负责解析SQL,将实际的数据操作转发给存储节点。2.TiKV我们知道,对于MySQL来说,它的存储引擎(大多数情况下)是InnoDB,它存储所用的数据结构是B+树,最终以.ibd文件的形式存储在磁盘上。TiDB呢?TiDB的存储由TiKV处理,TiKV是一个支持事务的分布式KV存储引擎。毕竟是KV存储引擎。用简单的语言来说,这是一个巨大的、有序的Map。但是说到KV存储,很多人可能会想到Redis。数据大部分时间都存储在内存中。甚至Redis也有像RDB、AOF这样的持久化方式。作为分布式数据库,TiKV也不例外。它使用RocksDB引擎实现持久化,并全权负责具体的数据落地。RocksDB是Facebook开源的一个独立的KV存储引擎,使用C++实现。3.索引数据以官网为例给大家看一下。据说TiDB中有这么一张表:那么表中有三行数据:这三行数据中的每一行都会映射成一个键值对:其中t10中的Keyt10代表该表ID为10,r1代表RowID为1的行。由于我们在建表时指定了主键,所以RowID就是主键的值。Value是该行除主键外其他字段的值。上图显示了主键索引。如果是非聚集索引(二级索引)怎么办?例如索引idxAge,在建表语句中为Age列建立二级索引:i1代表ID为1的索引,即当前二级索引,10、20、30为索引列的值Age,最后的1、2、3是对应行的主键ID。从索引的语句部分可以看出,idxAge是一个普通的二级索引,不是唯一索引。因此索引中允许有多个年龄为30的列。但是如果我们有一个唯一索引呢?只用表ID、索引ID和索引值组成Key,这样如果再次插入Age为30的数据,TiKV会发现Key已经存在,可以进行唯一键检测。4.存储细节知道了列数据是如何映射成Map的,我们就可以继续了解存储相关的细节了。从图中我们可以看出一个问题:如果一个TiKV节点挂掉了,这个节点上的数据会不会全部没了?当然不是,TiDB可以是一个金融级高可用的分布式关系型数据库,怎么可能允许这样的事情发生。TiKV在存储数据的时候,会想办法把相同的数据存储在多个TiKV节点上,并使用Raft协议来保证相同数据在多个TiKV节点上的数据一致性。为了便于理解,对上图进行了简化。实际上,一个TiKV中有2个RocksDB。一个用于存储RaftLog,通常称为RaftDB,另一个用于存储用户数据,通常称为KVDB。简单来说,就是会选出其中一条数据作为leader对外提供读写服务,其余的只作为follower来同步leader的数据。当Leader挂掉时,它可以自动进行故障转移,并从Followers中重新选举出新的Leader。看到这里,是不是觉得有点像Kafka?Kafka中的主题是一个逻辑概念。实际上会被划分成多个Partition,分发给多个Broker,选举出一个LeaderPartition对外提供服务。当LeaderPartition失败时,会从FollowerPartiiton中重新选举出一个Leader出来。那么,Kafka中的选举和服务单元是Partition,那么在TiDB中又是什么呢?5.Region答案是Region。刚才说了,TiKV可以理解为一个巨大的Map,Map中某个连续的Key就是一个Region。不同的Region会保存在不同的TiKV上。一个Region有多个副本,每个副本也称为Replica。多个Replica组成一个RaftGroup。按照上面描述的逻辑,会选出某个Replica作为Leader,其余的Replica会选为Follower。而且在写入数据的时候,TiDB会尽量保证Region不会超过一定的大小,目前是96M。当然,超过这个尺寸限制还是有可能的。每个Region可以用左闭右开区间表示,例如[startKey,endKey)。但是不可能让它无限增长吧?所以TiDB设置了一个上限。当Region的大小超过144M(默认)时,TiKV会将其拆分为两个或多个Region,以保证每个Region中的数据均匀分布;同样,当一个Region在短时间内删除大量数据时,它会变得比其他Region小很多,TiKV会合并两个较小的相邻Region。上面已经简单介绍了通用的存储机制和高可用机制。但实际上,还遗留了一个比较大的问题。大家可以结合上图来思考一下。一个查询语句来了,TiDBServer解析之后,它怎么知道要找的数据在哪个Region呢?这个Region在哪个TiKV上?是否要遍历所有TiKV节点?用这样想是不可能的。我刚刚谈到了多个副本。除了知道提供读写服务的LeaderReplica所在的TiKV之外,还需要知道其余的FollowerReplica在哪些实例中,等等。6.PD这就需要引入PD。有了PD“存储相关的细节”,画面就会变成这样:什么是PD?它的全称是PlacementDriver,用于管理整个集群的元数据。您可以将其视为整个集群的控制节点。PD集群本身也支持高可用,至少由3个节点组成。用一个等效的例子应该很容易理解。PD可以大致理解为Zookeeper,或者RocketMQ中的NameServer。Zookeeper就不用多说了,NameServer就是负责管理整个RocketMQ集群元数据的组件。怕大家生气,特意把这两个字加粗了。因为PD不仅负责元数据管理,还负责根据数据分布情况进行合理调度。这是根据数据状态安排的。具体是什么意思?7.调度例如假设每个RaftGroup需要一直维护3个副本,那么当一个RaftGroup的Replica因为网络、机器实例等原因不可用时,Replica的数量降为1。此时时间,当PD检测到时,它会调度并选择合适的机器来补充Replicas。合理删除多余的副本。用一句话概括上面描述的特征:PD会随时保持集群中RaftGroup副本数在期望值。这个可以参考Kubernetes中ReplicaSet的概念,我理解的很相似。或者,当TiDB集群进行存储扩容,将TiKV节点添加到存储集群时,PD会将其他TiKV节点上的Region迁移到新添加的节点上。或者,如果LeaderReplica挂了,PD会从RaftGroup的Replica中选出一个新的Leader。再比如,在热点区域的情况下,并不是所有的区域都会被频繁访问,PD需要对这些热点区域进行负载均衡调度。总结PD的调度行为,你会发现只有三个操作:添加一个Replica,删除一个Replica,在一个RaftGroup的不同副本之间迁移Leader角色。了解了调度操作之后,我们来整体了解一下调度需求。点击TiDB的官方网站以获得一个很好的总结。我整理成一张脑图供大家参考:大部分点没问题,但“控制负载均衡的速度”可能会有些问题。因为TiDB集群在做负载均衡的时候会迁移Region,可以理解为Redis的rehash类似的问题,可能会影响线上服务。8.HeartbeatPD为了调度这些决策,需要控制整个集群的相关数据,比如有多少个TiKV?有多少个筏组?每个RaftGroup的leader在哪里?收集机制。在NameServer中,所有RocketMQBroker都会在NameServer中注册自己并定时发送心跳,Brokers中保存的相关数据也会随着心跳发送到NameServer,更新集群的元数据。PD和TiKV的操作也是类似的。TiKV中有两个组件与PD交互,分别是:RaftGroup的LeaderReplica。TiKV节点本身通过心跳收集数据,更新维护整个集群的元数据,当心跳返回时,返回对应的“调度指令”。值得注意的是,上图中每个TiKV中Raft只连接了一根线。实际上,一个TiKV节点上可能有多个RaftGroupLeaderStore(即TiKV节点本身)心跳,会带来当前节点存储的相关数据,例如磁盘使用情况、Region数量等。通过上报的数据,PD会维护和更新TiKV的状态。PD使用5种状态来标识TiKV的存储,分别是:Up:这个大家都懂,不懂的就别懂(手动doge)Disconnect:如果超过20秒没有心跳,就willbeDown:Disconnect超过max-store-down-time的值后,会变成Down,默认30分钟。这个时候PD会在其他Up的TiKV上补上down的节点上的RegionOffline:通过PDControl手动下线,Store会变为Offline。PD会将节点上的所有Region迁移到其他Store。当所有region迁移完成后,它会变成Tomstone状态的Tombstone:表示完全凉了,可以安全清理了。它官网上的图已经画的很好了,就不重画了。下面的状态机来自TiDB官网:RaftLeader更多的是上报某个Region的当前状态,比如当前Leader的位置,FollowersRegion的数量,离线follower数量,读写速度等,这样,TiDBServer层在解析的时候知道对应LeaderRegion的位置。