浅谈 TiDB 初始化系统库过程

【是否原创】是
【首发渠道】TiDB 社区
【正文】

1. init()函数

在golang中,有两个保留函数,一个是main函数,另一个就是init函数。我们都知道,golang里的main函数是程序的入口函数,main函数执行完成之后,整个程序也就执行完成了。但是,golang在执行main之前,会干三件事,第一件事,也是最先做的,就是导入程序中用到的各种依赖包,第二件事,就是做完第一件事情之后,对程序中的变量和常量进行初始化,那么,第三件事,就是在做完前面两件事之后,才会开始的,执行所有包中的init函数。在所有init函数执行完毕之后,才会去执行main函数。

init主要用途就是 初始化,不能使用初始化表达式初始化的,变量。在TiDB源码中,初始化系统库information_schema时,就利用到了init函数。

2. TiDB中表的分类

在TiDB中,将表分为三大类,分别是normal table、virtual table、cluster table。

WXWorkCapture_16203709501830

normal table,也就是物理表,它是持久化存储到TiKV当中的表,主要用于存储用户数据。全数据库共享唯一一份数据,不同的TiDB 读到的数据完全一致。

virtual table,即虚拟表,它只有表结构信息,而不存储数据,当到使用的时候,比如查询这个表的数据的时候,它会从内存中提取相应数据返回,多见于performance_schema,现在仅仅上只有兼容性上的意义。

cluster table,也可以称之为内存表,它是TiDB启动之后,根据表的实际定义,生成的仅存在于TiDB内存中的表,具体可以参考information_schema中的表,像SLOW_QUERY、PROCESSLIST表等。

3. 本地启动调试TiDB

TiDB启动时会默认创建五个SCHEMA,分别是INFORMATION_SCHEMA、METRICS_SCHEMA、PERFORMANCE_SCHEMA、mysql以及test。

其中,TiDB源码每次启动时,都会创建INFORMATION_SCHEMA、METRICS_SCHEMA、PERFORMANCE_SCHEMA,但是却不会每次都去创建mysql以及test,这就与TiDB表的分类有关了。mysql以及test中的表都属于NormalTable,所以这两个schema中的table数据最终是会存储落实在TiKV或者是MockTiKV中,而在本地,第一次编译TiDB源码启动时,TiDB会在本地创建一个MockTiKV存储的文件,一般是存在 ‘///tmp/tikv ’中,那么这些NormalTable就最终就会存储在这个MockTiKV中,那么第二次编译启动TiKV时,则不会再去创建一个新的mysql以及test的SCHEMA,而是直接使用缓存中的SCHEMA。那么剩下的三个SCHEMA,里面的大部分表都属于VirtualTable,只有在INFORMATION_SCHEMA中,以*CLUSER_*开头的表是属于ClusterTable,其余的则都是VirtualTable。这些就与TiDB系统数据库初始化过程息息相关了。

接下来,我们就来看一下,整个TiDB项目源码启动时,都做了什么事情,这里我们要跳出tidb-server/main.go这个文件,去见识更为广阔的天地。这里我们就仅仅拿创建系统SCHEMA的过程来讲一下,至于其余的部分,我们先不做处理。

step1 init过程,init过程中会去创建INFORMATION_SCHEMA,以及METRICS_SCHEMA的系统库。


WXWorkCapture_16203742922275

step2 执行main函数,初始化tidb-sever。main函数中,createStoreAndDomain()会通过session.BootstrapSession()去初始化一个session,而在这个过程中,有一个函数,getStoreBootStrapVersion(),这个函数是用来获取本地MockTiKV的版本信息,默认是0。在第一次在本地初始化启动TiDB的时候,会在工程目录的根目录下创建一个tmp文件夹,用来存储MockTiKV的数据。当这个文件存在时,那么TiDB启动就会获取到这个文件中TiKV的版本信息,目前手上的源码,TiKV版本号是62,而最新的TiDB源码仓库,这个版本号是等于65;如果获取到的版本信息小于62,则会认为,本地TiKV版本老旧,就会去执行upgrade函数,去升级TiKV;但是如果本地没有这样的一个TiKV,那么这个版本就是0,就会去执行bootstrap()。

WXWorkCapture_16203743905039

step3 在我们来先默认本地没有TiKV的缓存副本,就当作第一次启动TiDB。那么,在初始化session的过程中,TiDB就会在createSession()中初始化PERFORMANCE_SCHEMA系统库,然后通过bootstrap()执行DDL以及DML语句,将mysql、test这两个剩余的数据库创建,并且存储进本地的MockTiKV中。如果存在TiKV缓存的话,TiDB就只会去在初始化session的过程中初始化PERFORMANCE_SCHEMA数据库,而不会去执行创建mysql以及test数据库的DDL以及DML语句。



至于TiDB为什么要在这一步进行PERFORMANCE_SCHEMA的初始化,而不是选择直接在init阶段就将初始化的事情做了。猜测:根据源代码的命名方式上来看,一开始是打算这么做的,但是,问题就出在TiDB建立PERFORMANCE_SCHEMA的代码上。TiDB中去创建这个系统数据库里面的表是通过执行SQL语句的方式去做的,那么就会带来一个问题,由于init函数的特性,其执行顺序的不确定,解析SQL语句的模块就不一定就会先运行起来,就不一定能够去正确解析SQL语句。这个时候,就为了保证程序能够顺利启动,就必须就得先初始化plan/core的init函数,也就是必须得保证SQL解析模块是正确运行起来的,这个时候,再通过go中的synv.Once方法,将PERFORMANCE_SCHEMA通过SQL语句的方式建立起来,并且写入到内存中保存,记住,这里是内存,不是负责存储的TiKV,也就是说,PERFORMANCE_SCHEMA中的表同样的全部都属于虚拟表,VirtualTable,不会存储在TiKV中。其实我觉着这里很怪异,在每次启动代码的时候,就都要先去解析一堆SQL语句,解析出来就会得到数据库中的表结构,有了表结构,后续的创建过程就与创建INFORMATION_SCHEMA的方式无异,将表的信息封装进PERFORMANCE_SCHEMA的数据库对象中,再通过registerVirtualTable的方式去创建PERFORMANCE_SCHEMA的表,然后存进内存,而不是通过执行SQL语句的方式,将表存到TiKV中。TiDB执行SQL语句的流程,最终是将语句下发到底层TiKV上去执行,但是对于PERFORMANCE_SCHEMA而言,那一堆建表的SQL语句的作用,就是给解析模块儿来进行解析,得到表结构,然后拿到表结构去将这些表注册成虚拟表,存进内存。这一点,我无法理解为什么要这么干,看源码关于这一块的时候,我看到一堆 ‘CREATE TABLE if not exists performance_schema.table_name’ 静态常量语句的时候,才明白过来。


2赞