引言:
复制表是OceanBase数据库中一种特殊的表,本文将介绍其基本定义与应用方法。复制表,能够在任意一个处于"健康"状态的副本上,获取到数据的最新变更。对于写入操作不频繁,但对读取操作的延迟和负载均衡有较高要求的场景而言,复制表无疑是一个理想的选择。OceanBase 4.x版本在复制表方面相较于旧版进行了多项优化,不仅提升了容灾能力,还增强了写性能,无疑将为用户带来更佳的体验。
为什么需要复制表
一写多读是数据库比较常见的一种部署方式,即一个节点处理所有的写操作,然后以逻辑日志同步的形式异步同步到另外几个读节点,如Amazon Aurora。这种部署方式的好处是可以将读压力分散到多个节点上,有更好的容灾性,并且与客户端物理距离更近的节点读的延迟会更低。
在Oceanbase中,一般采取弱一致性读+多副本的形式来实现这种需求,副本中有一个Leader提供写服务和即时的强一致性读,而其余Follower则可以读取到较旧的已提交数据。弱一致性读是基于异步复制的数据同步方式下的常见选择,Leader在写入时不需要关注Follower的数据回放进度,Follower则可以提供一个一致的旧版本数据的读取。
然而,有一些场景下用户的写入频率很低,对于写入操作的延迟并不敏感。相对的,这部分用户更加关心读操作的延迟和负载均衡,并希望可以及时的读到最新数据。复制表功能就是为了这种场景而设计,在牺牲小部分事务提交性能的前提下复制表可以在任意一个"健康"的Follower上读到最新数据。此处所提到的"健康"是指Follower与Leader之间的网络通畅、回放进度差距不大。
复制表是什么
当创建一个复制表后,所在租户的所有observer内都会创建一个复制表的副本,这些副本中有一个副本会被选为Leader,接受写请求,其余的副本只能接受读请求。
所有的副本都需要向Leader汇报状态,主要是副本的回放进度,即数据同步的进度。一般来说,Follower的回放进度会略落后于Leader,只要落后的幅度没有超过一个阈值,Leader就会认为副本处于"健康"的状态,可以快速回放出Leader上的修改。Leader认为某个副本在一定时间内"健康"后,会授予Follower一段时间的Lease。通俗的说,Leader在接下来的一段时间内"信任"Follower会保持"健康"状态,可以提供强一致性读服务。在这个"信任"期内,Leader的每一个复制表事务提交前都会确认Follower的回放进度。Follower回放出本事务的修改后,Leader才会汇报用户事务提交成功。此时,用户在Follower上可以读到刚刚提交的事务的修改。
OceanBase 4.x 复制表的变化
复制表功能在3.x上就已经存在,而4.x版本OceanBase的架构有了比较大的变化。4.x复制表为了适应单机日志流的新架构进行了重构,构建了基于分区的可读版本号校验以及基于日志流的Lease授予机制,用于保证强一致性读的正确性。
另外,4.x复制表完善了切主不杀事务的能力,在用户或负载均衡发起Leader切换时未提交的复制表事务不会像3.x版本一样无法继续,而是可以在切主后继续执行。相比3.x版本,4.x的复制表也有着更好的写事务性能和更强的容灾能力,副本宕机对于读操作的影响更低。
R副本与广播日志流
R副本指的是只读副本,这种副本不参与一致性协议的投票,无法成为Leader,只是回放Leader产生的日志在本地生成相应的数据。简而言之,R副本只可能提供读服务,不可能提供写服务。
当某个租户的第一个复制表被创建时,同时会创建一个特殊的日志流,称为广播日志流。之后新建的复制表都会创建到广播日志流上。广播日志流与普通日志流的不同之处在于它会自动的在租户内的每个OBServer上都部署一个副本,保证在理想情况下复制表可以在任意一个OBServer上提供强一致性读。
一般来说,我们并不希望参与一致性协议投票的副本过多。参与投票的副本越多,达成多数派需要的时间也就越长。在一个租户内的OBServer较多时,自然不可能让所有的OBServer上的副本都参与投票。因此,广播日志流在不需要参与投票的OBServer上会部署R副本,在需要参与投票的OBServer上部署常规的F副本(FULL副本,即全功能副本)。
R副本的使用与限制
R副本的创建
R副本跟随租户的locality变化而变化。创建租户的locality或者修改租户的locality时,每个zone限制只能指定一个副本,可以是FULL/F或者READONLY/R。举例说明,以下都是合法创建带只读副本租户的语法。
obclient> create resource unit if not exists 2c2g max_cpu 2, memory_size '2G';
Query OK, 0 rows affected (0.01 sec)
obclient> create resource pool tenant_pool unit = "2c2g", unit_num = 2, zone_list = ('z1', 'z2', 'z3');
Query OK, 0 rows affected (0.03 sec)
--创建mysql1租户,指定locality为"F@z1, F@z2, R@z3"
obclient> create tenant mysql1 resource_pool_list=('tenant_pool'), locality = "F@z1, F@z2, R@z3", PRIMARY_ZONE = "z1" SET ob_tcp_invited_nodes='%';
Query OK, 0 rows affected (34.78 sec)
--创建mysql2租户,指定locality为"F@z1, READONLY@z2"
obclient> create tenant mysql2 resource_pool_list=('tenant_pool'), locality = "F@z1, READONLY@z2", PRIMARY_ZONE = "z1" SET ob_tcp_invited_nodes='%';
Query OK, 0 rows affected (34.78 sec)
对于普通日志流来说,每个zone仅能有一个副本,且该副本类型和locality中指定的副本类型匹配。
对于广播日志流来说,每个zone内,除了locality描述的该zone副本类型外,在该zone内其余有该租户unit资源的机器上会各放置一个只读副本。对locality中没有指定的zone不放置任何副本。以上文提到的合法创建租户语句为例:
- 对于mysql1租户来说,广播日志流在zone1会有2个副本(F + R),在zone2会有两个副本(F + R),在zone3会有两个副本(R + R)。
- 对于mysql2租户来说,广播日志流在zone1会有2个副本(F + R),在zone2会有两个副本(R + R),
这里存在一些限制,需要我们了解:
- locality中每个zone只能指定一个副本,不支持all_server的语法。即不支持"F, R@zone1"、"F, R{ALL_SERVER}@zone1"这类的语法。对于广播日志流来说,其副本真实分布允许一个zone内有多个副本。
- sys租户、meta租户没有广播日志流,不支持复制表创建。
- 每个用户租户最多只能有一个广播日志流。
- 不支持广播日志流和普通日志流之间的属性转换。
- 不支持手动删除广播日志流,目前随着租户删除而删除。
如何使用复制表
我们用一个简单的例子说明如何使用一个复制表:
第一步,创建一个Unit:
obclient> create resource unit if not exists 2c2g max_cpu 2, memory_size '2G';
Query OK, 0 rows affected (0.01 sec)
第二步,创建一个Unit Num为2的资源池:
obclient> create resource pool tenant_pool unit = "2c2g", unit_num = 2, zone_list = ('z1', 'z2', 'z3');
Query OK, 0 rows affected (0.03 sec)
第三步,创建租户,指定locality分布:
obclient> create tenant mysql resource_pool_list=('tenant_pool'), locality = "F@z1, F@z2, R@z3", PRIMARY_ZONE = "z1" SET ob_tcp_invited_nodes='%';
Query OK, 0 rows affected (34.78 sec)
第四步,登录到上一步创建的mysql用户租户,创建一个复制表:
obclient> use test;
Database changed
obclient> create table dup_t1(c1 int) duplicate_scope = 'cluster';
Query OK, 0 rows affected (6.57 sec)
第五步,可以查看广播日志流信息,复制表会创建在该日志流上:
obclient> select * from oceanbase.DBA_OB_LS where flag like "%DUPLICATE%";
+-------+--------+--------------+---------------+-------------+---------------------+----------+---------------------+---------------------+-----------+
| LS_ID | STATUS | PRIMARY_ZONE | UNIT_GROUP_ID | LS_GROUP_ID | CREATE_SCN | DROP_SCN | SYNC_SCN | READABLE_SCN | FLAG |
+-------+--------+--------------+---------------+-------------+---------------------+----------+---------------------+---------------------+-----------+
| 1003 | NORMAL | z1;z2 | 0 | 0 | 1683267390195713284 | NULL | 1683337744205408139 | 1683337744205408139 | DUPLICATE |
+-------+--------+--------------+---------------+-------------+---------------------+----------+---------------------+---------------------+-----------+
1 row in set (0.03 sec)
第六步,如果在系统租户下执行,可以查看复制表的副本分布,REPLICA_TYPE说明了副本类型:
obclient> select * from oceanbase.CDB_OB_TABLE_LOCATIONS where table_name = "dup_t1";
+-----------+---------------+------------+----------+------------+----------------+-------------------+------------+---------------+-----------+-------+------+----------------+----------+----------+--------------+-----------------+
| TENANT_ID | DATABASE_NAME | TABLE_NAME | TABLE_ID | TABLE_TYPE | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | DATA_TABLE_ID | TABLET_ID | LS_ID | ZONE | SVR_IP | SVR_PORT | ROLE | REPLICA_TYPE | DUPLICATE_SCOPE |
+-----------+---------------+------------+----------+------------+----------------+-------------------+------------+---------------+-----------+-------+------+----------------+----------+----------+--------------+-----------------+
| 1002 | test | dup_t1 | 500002 | USER TABLE | NULL | NULL | NULL | NULL | 200001 | 1003 | z1 | 11.166.208.181 | 36125 | LEADER | FULL | CLUSTER |
| 1002 | test | dup_t1 | 500002 | USER TABLE | NULL | NULL | NULL | NULL | 200001 | 1003 | z1 | 11.166.86.134 | 36124 | FOLLOWER | READONLY | CLUSTER |
| 1002 | test | dup_t1 | 500002 | USER TABLE | NULL | NULL | NULL | NULL | 200001 | 1003 | z2 | 11.166.208.180 | 36127 | FOLLOWER | FULL | CLUSTER |
| 1002 | test | dup_t1 | 500002 | USER TABLE | NULL | NULL | NULL | NULL | 200001 | 1003 | z2 | 11.166.208.181 | 36126 | FOLLOWER | READONLY | CLUSTER |
| 1002 | test | dup_t1 | 500002 | USER TABLE | NULL | NULL | NULL | NULL | 200001 | 1003 | z3 | 11.166.208.180 | 36128 | FOLLOWER | READONLY | CLUSTER |
| 1002 | test | dup_t1 | 500002 | USER TABLE | NULL | NULL | NULL | NULL | 200001 | 1003 | z3 | 11.166.86.134 | 36129 | FOLLOWER | READONLY | CLUSTER |
+-----------+---------------+------------+----------+------------+----------------+-------------------+------------+---------------+-----------+-------+------+----------------+----------+----------+--------------+-----------------+
6 rows in set (0.28 sec)
如果在MySQL模式的租户下执行),可以查看Leader信任的"健康"副本,以及"信任过期"的时间,仅用作副本状态的参考:
obclient> select svr_ip,svr_port,follower_ip,follower_port,expired_timestamp from oceanbase.__all_virtual_dup_ls_lease_mgr;
+----------------+----------+----------------+---------------+----------------------------+
| svr_ip | svr_port | follower_ip | follower_port | expired_timestamp |
+----------------+----------+----------------+---------------+----------------------------+
| 11.166.208.181 | 36125 | 11.166.86.134 | 36129 | 2023-05-06 09:54:14.986771 |
| 11.166.208.181 | 36125 | 11.166.208.181 | 36126 | 2023-05-06 09:54:13.786119 |
| 11.166.208.181 | 36125 | 11.166.208.180 | 36127 | 2023-05-06 09:54:13.786119 |
| 11.166.208.181 | 36125 | 11.166.208.180 | 36128 | 2023-05-06 09:54:14.986771 |
| 11.166.208.181 | 36125 | 11.166.86.134 | 36124 | 2023-05-06 09:54:14.986771 |
+----------------+----------+----------------+---------------+----------------------------+
5 rows in set (0.02 sec)
当然,在MySQL模式的租户下执行也可以查看复制表分区在各副本上的可读状态:
obclient> select svr_ip,svr_port,tablet_id,ls_state,attribute from oceanbase.__all_virtual_dup_ls_tablets;
+----------------+----------+-----------+----------+-----------+
| svr_ip | svr_port | tablet_id | ls_state | attribute |
+----------------+----------+-----------+----------+-----------+
| 11.166.208.181 | 36126 | 200001 | FOLLOWER | READABLE |
| 11.166.208.181 | 36125 | 200001 | LEADER | READABLE |
| 11.166.86.134 | 36124 | 200001 | FOLLOWER | READABLE |
| 11.166.86.134 | 36129 | 200001 | FOLLOWER | READABLE |
| 11.166.208.180 | 36128 | 200001 | FOLLOWER | READABLE |
| 11.166.208.180 | 36127 | 200001 | FOLLOWER | READABLE |
+----------------+----------+-----------+----------+-----------+
6 rows in set (0.01 sec)
第七步,像普通表一样插入和读写数据。对于一个读请求来说:如果使用Proxy,读请求可能会路由到任意一个OBServer;如果直连OBServer,只要本地副本可读,就会在直连的OBServer上执行读请求。
obclient> insert into dup_t1 values(1);
Query OK, 1 row affected (0.73 sec)
obclient> select * from dup_t1;
+------+
| c1 |
+------+
| 1 |
+------+
1 row in set (0.01 sec)
总结
本文主要讨论了4.x复制表功能的基本概念和使用方式,该功能通过牺牲写操作延迟和增加副本数量的方式使得分布式查询有接近单机查询的性能。在使用方式上,复制表与普通表其实没有特别大的区别。用户日常使用过程中并不需要关心本文描述的复制表可读性相关的检查,复制表功能会自动地寻找"健康"的副本完成用户的读写请求。从4.x版本开始,复制表功能具有了投入生产使用的基本能力,希望本文可以成为用户了解和使用复制表的一个起点。