数据库系统的正常运转离不开元数据(Meta Data),例如表的模式(结构)信息、系统中数据的统计信息、系统的运行状态信息等,OceanBase当然也不例外。OceanBase中将元数据也按照表的形式进行组织和管理,这些存放元数据的表被称为系统表 。系统表本质上也和普通的用户表一样,可以通过SQL语句进行增删改查等操作,但系统表的操作只能由特殊的通道完成。
OceanBase的系统表统一存放在系统(SYS)租户中,未区分不同租户产生的元数据,这些系统表中都有一个tenant_id列用来标识该条元数据的归属,这些系统表的名称都以“__all”为前缀。同时,为了方便各租户使用自己的元数据,在各租户中也定义有一些从系统表导出的视图,这些视图的名称都以“__tenant”为前缀。很明显,这些视图都是系统表的简单行列子集视图,可以直接在其上进行修改操作,因此它们也被归入到“系统表”的类别之中。此外,为了方便对元数据的查看,OceanBase还提供了一些比较复杂的只读视图,它们被称为“虚拟表”,其名称以“__all_virtual”或者“__tenant_virtual”为前缀。
1.核心系统表
在众多的系统表中,有一类“一等公民”,它们是其他系统表能够存在的前提,因此可以称为核心系统表。由于系统表本身也需要有元数据来描述其结构,因此存放表结构信息的系统表地位自然会超然于普通系统表之上。
(1)__all_core_table
__all_core_table记载着系统中核心系统表的元数据,其结构如表4.1所示。这些信息是RootService启动所需的必要信息,例如其中表名为__all_table_v2且行号为5的行共有73个,每一行都描述了系统表__all_table_v2中的一个列和列值,而其中列“table_name”的值正好是“__all_table_v2”,说明这73行描述的是系统表__all_table_v2作为一个表的基本元数据。只有获得了这些信息,系统的各个模块才能用它们去解释从__all_table_v2获得的信息,从而得到普通表的模式。由于__all_core_table在元数据中的基础地位,OceanBase内部也把它称为“一号表”(THE ONE)。
表4.1 __all_core_table系统表结构
(2)__all_root_table
__all_root_table记载了表的分区和副本信息,其结构如表4.2所示。
表4.2 __all_root_table系统表结构(局部)
(续)
(3)__all_table
__all_table中记载着所有表(__all_core_table、__all_root_table、__all_table本身不包括在内)的表级元数据,其结构如表4.3所示。为了保持向下兼容,OceanBase系统中还可能会出现另一个版本的__all_table,即__all_table_v2,当集群的版本低于2.2.1时,表的元数据放在__all_table中,对于2.2.1及以上的版本则使用__all_table_v2。
表4.3 __all_table系统表结构(局部)
(4)__all_column
一个表的元数据不仅仅是表自身的描述信息,还应包括表中各列的描述数据,这些数据存放在系统表__all_column中,表中的每一列在__all_column中都有一行,其结构如表4.4所示。
表4.4 __all_column系统表结构(局部)
事实上,表的全部元数据不仅有__all_table和__all_column中的部分,还包括若干围绕这两者的系统表中的数据,例如__all_constraint存放着各种约束,__all_collation存放着__all_column.collation_type中引用的排序规则的信息。
(5)__all_database
__all_database存放着租户中所有方案(Schema)的元数据,每一个方案对应着其中的一行,其结构如表4.5所示。值得注意的是,在OceanBase中Schema的概念和Database(数据库)等同,因此这个系统表名中包含了database。
表4.5 __all_database系统表结构
(续)
(6)__all_tablegroup
__all_tablegroup中存储着所有的表组(Table Group)信息,系统中每个表组对应其中一行,其结构如表4.6所示。
表4.6 __all_tablegroup系统表结构(局部)
(7)__all_tenant
__all_tenant中存储着所有的租户信息,系统中每个租户对应其中一行,其结构如表4.7所示。
表4.7 __all_tenant系统表结构
(8)__all_ddl_operation
__all_ddl_operation中收集着所有执行过的DDL操作的信息,其结构如表4.8所示。该表的schema_version是主键,这是因为每一次DDL操作都会导致整个集群的模式中发生或多或少的改变(例如列结构或表结构改变),为了让系统中不同时间开始的操作能使用到合适的模式信息,OceanBase每次成功执行DDL操作后都会将模式版本增加,因此可以认为每一个DDL操作都有一个唯一的模式版本(Schema Version)。
表4.8 __all_ddl_operation系统表结构
2.系统表初始化
一个OceanBase集群第一次被启动时,需要首先进行自举操作(Bootstrap)形成初始的系统表结构并且将集群中各个服务器节点加入到集群之中,通常这一动作是由OBD发起。
OBD在启动集群时会通过检查节点数据目录的clog子目录是否存在来判断是否需要进行自举动作,如果需要进行自举,则OBD会向集群发送一系列的SQL命令完成自举:
1)OBD首先会发送一个BOOTSTRAP命令进行基本的自举,发送的BOOTSTRAP命令示例如下:
上述语句中指定的ZONE列表也被称为根服务(Root Service,RS)列表。
2)基本自举完成后,OBD还会发出若干ADD SERVER命令将RS列表中的多个节点注册到集群中:
OBD连接的节点(服务器列表中的第一个)收到BOOTSTRAP命令之后,会按照SQL引擎中的流程进行SQL的解析、执行(见第5章),负责执行BOOTSTRAP命令的执行器部件是ObBootstrapExecutor::execute()方法,它将会通过RPC端口向当前连接节点发送代码为OB_BOOTSTRAP的RPC请求。
自举的RPC请求最终会由目标节点上的ObService::bootstrap()方法处理,该方法分为两个阶段:①预备阶段,通过ObPreBootstrap::prepare_bootstrap()为自举工作做准备,其核心任务是创建一号表(__all_core_table);②自举阶段,建立其他系统表,由ObBootstrap::execute_bootstrap()接手处理。ObBootstrap::execute_bootstrap()的流程如图4.2所示。
图4.2 集群自举过程
1)复查自举状态:执行自举之前还会再次准确地检查集群是否已经完成过自举,其方法是通过多版本模式服务(见4.1.2节)中的模式服务获取集群的模式版本,然后将其与默认版本号OB_CORE_SCHEMA_VERSION(值为1)比较,只有相等才表示集群中尚未完成自举。
2)RS列表检查:在多ZONE部署模式中,检查RS列表中的每一台服务器声明是否有重复的ZONE定义。
3)检查RS列表服务器状态:将传入的RS列表加入模式服务中的服务器管理器(Server Manager),然后逐一检查各服务器的活跃情况,事实上这里并不会实时测试服务器是否有响应,而是依赖于服务器管理器中记录的服务器状态进行判断,服务器管理器中的服务器状态则是通过各服务器向RootService的心跳报告收集得到。
4)设置自举状态:准确来说,这一步骤的目的是让整个集群处于一种“正在进行自举”的状态,通过将模式服务中的is_in_bootstrap_属性置为true来实现,这期间所有对于模式的访问都会被暂停。
5)构造模式:OceanBase系统表的模式都被硬编码在ObInnerTableSchema类中,其中有多个名称为“XXX_schema”的方法,每一个方法都能构造出系统表XXX的模式信息,表示成一个ObSchemaTable对象。每一个方法都被分成两个阶段。
①构造ObSchemaTable对象,并填充相应系统表的基本信息,例如所属租户ID(固定为SYS租户)、表类型、表ID、表名等。
②用ADD_COLUMN_SCHEMA_TS_T逐一把该系统表的列定义加入ObSchemaTable对象。
6)模式排序:对上一步构造的众多ObSchemaTable对象进行排序,这个过程中采用了TableIdCompare类对左右两个ObSchemaTable对象进行比较,判断的规则是:
①左右都不是系统索引,则ID小的对象排在前面。
②左右都是系统索引,则数据表(基表)ID小的排前面。
③只有左边是系统索引,如果左边基表就是右边表,则左边的对象排前面,否则数据表ID小的排前面。
④只有右边是系统索引,如果右边基表就是左边表,则右边的对象排前面,否则数据表ID小的排前面。
7)广播系统模式:系统表对于各节点的操作都起着关键作用,因此每个节点都应该保存有系统表信息,这一步骤将会把整理好的ObSchemaTable对象广播给RS列表中的每一台服务器。
8)创建模式:这一步骤是真正创建系统表的地方,待创建的系统表将会以32个为一批分配创建,最终将通过ObTableSqlService::create_table()方法完成每一个系统表的创建。
作为多节点构成的分布式数据库系统,OceanBase集群中每一个节点上的操作都需要访问模式数据,为了更好地服务各节点上的操作,OceanBase基于模式的相对稳定性设计了一套多副本的模式管理方案:各节点上都缓存有模式数据的副本,但对于模式的修改则由RootService所在的节点实施,在完成模式修改之后由RootService将新的模式版本通知其他节点,它们将会刷新各自的模式缓存。
为了便于系统中其他模块使用模式信息,OceanBase基于节点上的模式副本包装了一套模式服务来为其他模块服务。由于系统运行中会由于DDL操作导致模式版本发生变化,不同时刻开始的操作(事务)将会看到(需要)不同版本的模式信息,这套模式服务准确来说应该被称为“多版本模式服务”。
OceanBase的多版本模式服务被实现为ObMultiVersionSchemaService类,在ObServer对象初始化过程中会调用ObServer::init_schema()方法初始化一个ObMultiVersionSchemaService实例并置于ObServer实例的schema_service_属性中。
多版本模式服务为系统中其他模块提供元数据服务,其他模块可以从模式服务获得两种形态的模式,如图4.3所示。
1)完整模式(Full Schema):包含数据库对象的完整模式信息,由名为“Ob###Schema”的类表达,其中###是数据库对象的类型名,例如图4.3中的ObDatabaseSchema表示数据库(Database)的模式。
2)简单模式(Simple Schema):仅包括完整模式中的核心部分,由名为“ObSimple###Schema”的类表达。从图4.3可以看到,简单模式中仅保留了数据库对象全局性或者关键性的信息(如数据库的ID、名称),而更细致的如字符集、排序规则等信息则仅保留在完整模式中。
图4.3 完整模式和简单模式
模式信息在系统中会被频繁访问,这样的信息如果能常驻在内存中当然是最理想的情况。对于完整模式的形式来说,其体量相对较大,如果直接将完整的模式维持在缓存中会耗费较多内存空间,而且其体量会随着数据库对象数的增加而增长。因此,OceanBase中采取的方案是仅将模式信息中最核心的部分(即简单模式)常驻在缓存中,完整版本的模式信息根据需要载入非常驻内存,当内存不足时完整模式会被自动淘汰。
多版本模式服务提供了ObSchemaGetterGuard类作为节点上的其他模块访问模式数据的入口。
ObSchemaGetterGuard对各类数据库对象的模式都分别公开了读取接口,命名规则是“get_###_schema”,其中###是小写形式的数据库对象类型,例如数据库模式的访问接口是get_database_schema方法。对于每一种数据库对象,其读取方法通过重载形成了不同的版本,其他模块通过ObSchemaGetterGuard可以访问其简单模式或者完整模式,例如get_database_schema方法就有两类版本,分别通过ObSimpleDatabaseSchema和ObDatabaseSchema类型的输出参数向调用者返回数据库的简单模式和完整模式。
外部模块使用ObSchemaGetterGuard对象时,可以通过指定其tenant_id_属性来限定其可访问的模式范围。如果tenant_id_属性值是一个合法的租户ID,则ObSchemaGetterGuard对象是租户级Guard,通过该对象只能访问该租户的简单和完整模式,否则ObSchemaGetterGuard是集群级Guard,通过该对象能访问整个集群中全部的模式。
利用ObSchemaGetterGuard获得的数据库对象的模式信息都是形如ObDatabaseSchema的对象,它们本质上是多版本模式服务管辖的模式缓存(见4.1.4节)中某个版本的缓存数据。因此,外部模块不会孤立地修改缓存中的模式,而是在通过DDL语句修改数据库对象时同步地在模式缓存中产生新版本的模式。不过,这种动作只会发生在RootService所在的节点上,其他节点上需要通过模式刷新(见4.1.5节)才能获得新版本的模式。
为了实现对模式的修改,OceanBase在多版本模式服务中提供了ObSchemaService类作为DDL命令操纵模式的接口。ObSchemaService是一个接口类,目前它仅有一个实现:ObSchemaServiceSQLImpl类。
ObSchemaServiceSQLImpl的作用是根据外部模块的调用,返回操纵相应数据库对象的SQL服务类(ObDDLSqlService的子类,如图4.4中的ObDatabaseSqlService)的对象,外部模块再利用SQL服务对象的方法完成DDL操作。图4.5中给出了CREATE DATABASE语句执行过程中涉及的DDL服务部分:主RootService所在节点收到创建数据库的RP C请求之后将会交给其create_database方法处理,其中关于模式部分的处理最终会进入多版本模式服务的ObSchemaService(实际是一个ObSchemaServiceSQLImpl实例)中,进而通过get_database_sql_service方法取得数据库对象的DDL服务对象(ObDatabaseSqlService),最后调用其insert_database方法将新建数据库的模式信息 加入多版本模式服务管辖的模式缓存中。
图4.4 DDL服务类层次
图4.5 CREATE DATABASE执行过程中的模式服务
模式数据是整个数据库系统运行期间会频繁访问的信息,为了避免反复地从持久化存储中读出系统表数据,OceanBase在多版本模式服务中设置了模式缓存,被访问过的模式被驻留在位于内存中的缓存区域用于加速后续的模式访问。由于OceanBase的分布式数据库特性,集群中每个节点上都会有访问模式数据的需求,因此每个节点上都有自己的模式缓存。虽然多个节点上的模式缓存形成了多副本,但整个集群中只有RootService节点才能通过执行DDL语句修改模式信息,这些缓存之间实际是一主多从的关系,非RootService节点上的缓存会随着RootService节点的缓存变化而刷新,因此不会出现缓存不一致的问题。
模式缓存分为两部分,第一部分是由ObSchemaCache描述的完整模式缓存,其逻辑结构如图4.6所示。ObSchemaCache用于管理通过ObSchemaGetterGuard产生的完整模式,通过ObSchemaGetterGuard取完整模式时会优先在ObSchemaCache中查找,如果能命中则直接从缓存中返回完整模式,否则会构造SQL从系统表中读取元组并构造所需的完整模式。在ObSchemaCache内部又分为两个组成部分:
1)sys_cache_:是一个ObHashMap类型的Hash表,顾名思义,sys_cache_中缓存着系统级的模式数据,例如核心表、系统表等的模式。sys_cache_中缓存的信息一直常驻内存,不会因为存储空间而被替换出缓存。
2)cache_:是一个KVCache类型的KV存储,它管理着非核心数据库对象的模式,或者说sys_cache_以外的所有模式数据都被缓存在cache_中。与sys_cache_不同的是,cache_中的模式数据有可能因为存储空间不足的原因被换出缓存。
在ObSchemaCache中,每一个模式都由一个ObSchemaCacheKey唯一标识,其中包括了模式类型(schema_type_)、模式ID(schema_id_)以及模式版本(schema_version_)。被缓存的对象是ObSchema,它根据模式类型可以具体化为ObTenantSchema、ObDatabase-Schema等。
图4.6 ObSchemaCache缓存逻辑结构
模式缓存的第二大组成部分由ObSchemaMgrCache表达,其逻辑结构如图4.7所示。ObSchemaMgrCache中缓存着ObSchemaMgr对象,相较于缓存在ObSchemaCache中的模式对象,ObSchemaMgr更像是从另一个“视角”对模式数据的组织,一个ObSchemaMgr对象可以被看成是一个特定租户在特定模式版本下的模式数据,而ObSchemaCache则是按照数据库对象来组织模式数据。此外,从图4.7中DatabaseInfos的定义可以看到,ObSchemaMgr中仅缓存简单版本的模式。
图4.7 ObSchemaMgrCache缓存逻辑结构
ObSchemaMgrCache中对ObSchemaMgr对象的管理相对简单粗放:采用了一个ObSche-maMgrItem数组(schema_mgr_items_属性)管理ObSchemaMgr对象及其引用计数。尽管Ob-SchemaMgrCache初始化时传入的参数指定了schema_mgr_items_中缓存的ObSchemaMgr对象的最大数量,但该数组实际仍然按最大硬上限(8192个元素,由常量属性MAX_SCHEMA_SLOT_NUM定义)分配空间决定。因此ObSchemaMgrCache中缓存的ObSchemaMgr数量有限,发生缓存替换时会将引用数为零的ObSchemaMgr对象换出。ObSchemaMgrItem中的引用数记录着ObSchemaMgr被使用的次数,通过ObSchemaGetterGuard获取模式数据时若能从ObSchemaMgrCache找到对应版本的ObSchemaMgr,则会加引用计数,用完ObSchemaGetterG-uard析构时反向减少引用计数,其目的是确保使用ObSchemaGetterGuard期间对应的ObSche-maMgr不会被淘汰。
如4.1.2节中所述,OceanBase数据库集群中各个节点上都有自己的模式缓存,当主RootService节点上执行DDL操作修改模式之后,其他节点上的模式缓存需要在适当的时机进行刷新。模式缓存的刷新主要分为主动刷新和被动刷新。
1.主动刷新
RootServer执行完DDL操作并且更新自身的模式缓存时,会产生新的模式版本号。模式版本号可以看成是一种流水号,新的模式版本号是从前一个版本号加1形成。产生新的模式版本号之后,RootServer并不采用广播的方式通知其他节点,而是等待其他节点报告心跳(续租)时随着响应信息返回给这些节点。如图4.8所示(箭头上的数字代表步骤序号),集群中的每一个节点上的OBServer都会定期向RootServer上的主RootService发送RPC请求更新租约(同时也充当心跳包),RootService中的renew_lease方法会处理续租请求。在完成对该节点的状态更新之后,renew_lease方法会向发起请求的OBServer返回一个响应包LeaseResponse,响应包中包含有RootServer上的最新模式版本号。OBServer收到租约响应包之后会由ObHeartBeatProcess的do_heartbeat_event方法处理,其中会根据租约响应包的最新模式版本号尝试调用ObServerSchemaUpdater::try_reload_schema()进行模式重载,即将刷新任务包装成一个类型为REFRESH的ObServerSchemaTask任务放入任务队列中等待处理线程异步执行,而模式的刷新最终会被路由到该节点的多版本模式服务的refresh_and_add_schema方法中。
图4.8 更新租约时主动刷新模式
模式的主动刷新仅为所在节点载入新版本的SchemaMgr,即简单形式的模式。而完整模式的刷新则要依赖被动刷新方式。
2.被动刷新
当其他模块想要获取完整模式(会指定其模式版本)时,如果所在节点模式缓存中无法找到对应版本的完整模式,会实时触发SQL从系统表构造指定版本的完整模式,并放入到当前节点的模式缓存中。
严格来说,上述模式刷新方式其实并不符合“刷新”一词的语义,因为OceanBase采用的是多版本并发控制,模式的变动并不是通过“就地”(In-place)修改的方式体现,而是形成一个新版本的完整模式。因此,所谓的模式“刷新”实际是将之前没有访问过的其他版本的模式加入到模式缓存中。两种不同形式的模式中,由于简单模式的使用会更加频繁,因此对简单模式的“刷新”采用更为激进的主动刷新方式。