上篇文章我们介绍了 OBProxy 的连接管理,通过连接管理功能,OBProxy 和 OBServer 联系起来,同时 OBProxy 屏蔽了连接的复杂性,让用户使用起来和单机数据库一样简单。完成接入后,接下来的一个重要功能就是数据路由,这也是大部分用户最关心的功能之一,本文会对其进行详细介绍。
在介绍 OBProxy 的路由原理前,我们先讨论下路由需要考虑的影响因素,方便你更好地理解后面的内容,以及评价一个路由功能的好坏。我将从功能、性能和高可用三个因素展开介绍。
数据路由影响因素
功能因素
你是否考虑过,一个单机数据库的功能如 MySQL 数据库的 Prepared Statements 功能,在分布式系统中该如何实现,比如 PREPARE Statement 和 EXECUTE Statement 该发往哪个节点?
我们以 Prepared Statements 功能为例说明功能对路由的影响。Prepared Statements 执行主要有两个步骤:
-
步骤一:执行 PREPARE 操作,如发送SQL select * from t1 where c1 = ?
-
步骤二:执行 EXECUTE 操作,传递 select 语句使用的数据,并执行 select
步骤二是依赖于步骤一的,我们假设执行情况如图1所示,你就会明白,当 OBServer2 收到步骤二( EXECUTE)的请求后,并不知道步骤一(PREPARE)请求的内容,这时,OBServer2 就会报错(优雅做法)或断连接(粗暴做法)。
图1 PS功能执行情况
对于这种问题,常见的解决办法有两种(见图2)。
-
方法一:记录 PREPARE 路由到的节点 OBSERVER1,EXECUTE 请求继续路由到 OBServer1;该方法实现简单,但无法发挥分布式系统的优势。
-
方法二:在执行 EXECUTE 前,将 PREPARE 的状态同步给 OBServer2,参考连接管理部分的状态同步;该方法需要 OBProxy 同步连接状态,实现复杂,但可以利用分布式系统的优势,目前 OBProxy 采用了该方法。
图2 解决路由失败的两个方法
性能因素
高性能是 OceanBase 数据库的重要特性,路由对性能的影响主要是延迟,即网络通信开销。OBProxy 通过感知数据分布和机器地理位置降低网络通信开销,提高整体性能。
数据分布主要影响执行链路的跳数,OBProxy 路由时直接命中数据所在的节点是最好的。我们以 SQL 语句 select c1 from test为例说明数据分布对性能的影响。如图3所示,t1表的数据分布在 OBServer1 上面,路由方式1直接路由到 OBServer1 ,效率最高;路由方式2发给了一个无t1表数据的 OBServer2,OBServer2 再进行路由转发给 OBServer1,相比方式1性能变差。为了实现路由方式1的路由,OBProxy 需要感知SQL和表数据分布,后文会详细介绍。
图3 OBProxy 的两种路由方式
机器地理位置主要影响网络延迟,当选择了一个远端节点后,SQL执行会变慢,有时网络延迟的时间比数据库执行时间要大很多。我们在阿里云上做了测试,延迟数据如下:
-
杭州同可用区 rtt min/ avg/max/mdev = 0.111/ 0.141/0.433/0.060 ms
-
杭州不同可用区 rtt min/ avg/max/mdev = 1.847/ 2.003/5.840/0.740 ms
可以看到跨可用区后延迟增加接近2ms,对于简单SQL,数据本身执行时间可能才100us左右。因此,对于不同地理位置的机器,OBProxy 选择优先级是:同机房>同城不同机房>不同城市。图4展示了优先选择同城市的OBServer1。
图4 优先选择同城市的OBServer1
高可用因素
高可用因素是指 OceanBase 数据库对机器故障有容忍能力,让故障对应用透明无感知,OBProxy 发现 OBServer 节点故障后,路由时会排除故障节点,选择健康节点,对于正在执行的SQL也有一定的重试能力。高可用涉及故障探测、黑名单机制、重试逻辑等内容。如图5所示,OBProxy 发现OBServer1 故障后,将该节点加入黑名单。路由时从健康节点选择。
图5 OBProxy 发现 OBServer 节点故障的处理逻辑
了解了数据路由的影响因素和路由原则后,我们就可以更高效地进行路由策略设计了。不过,现实情况会复杂很多,原则上我们要实时感知 OBServer 状态、数据分布等,但在工程实践中很难做到,便引发出许多问题。因此,我们在考虑路由时需要兼顾功能、性能和高可用,让 OceanBase 数据库“更好用”。
OBProxy 路由功能
我们知道通过 OBProxy可以访问不同集群的不同租户的不同机器。这也是 OBProxy 可以实现集群路由、租户路由和租户内路由的原因,接下来我将围绕这三部分介绍OBProxy的路由功能。
集群路由
集群路由是指 OBProxy 路由功能支持访问不同的集群,它的关键点在于获取集群名和rslist的映射关系(见图6):
-
对于启动参数指定rslist的启动方式,集群名和rslist的映射关系通过启动参数指定
-
对于指定config_server_url的启动方式,集群名和rslist的映射关系通过访问url获取
需要注意的是,这里的rslist不需要包含所有的集群机器列表,OBProxy 会通过访问内部表获取集群所有机器,一般rslist为RootServer(OceanBase的总控服务)所在的机器。
图6 集群路由步骤
我们从图6中可以看到,OCP 是集群路由时非常重要的一个模块。当出现集群路由问题时,大部分都是 OCP 模块出现了问题,常见等问题有两个。
-
OCP 服务不可用:OBProxy 无法通过 OCP 获取集群名和rslist的映射关系,导致登录失败。
-
OCP 返回错误的结果:如 OBProxy 通过HTTP协议访问OCP,获取结果,结果为JSON格式,如果格式有问题,会导致结果解析失败。
OBProxy 是在用户登录首次访问集群时获取rslist,并保存到内存中,后续再访问该集群,从 OBProxy 的内存中获取就可以了。这里需要注意,当集群内存信息创建好后,OCP再出现问题,即使OBProxy 仍可以正常工作,也要及时排查 OCP 问题。
租户路由
OceanBase 数据库中,一个集群有多个租户,租户路由是指 OBProxy 路由功能支持访问不同的租户。在众多租户中,sys租户比较特殊,类似于管理员租户,和集群管理相关。我们将分开讨论sys租户路由和普通租户路由。
1. sys租户路由
完成集群路由后,我们可以获得集群的rslist,此时 OBProxy 会通过proxyro@sys账号登录rslist中的一台机器,并通过内部表__all_virtual_proxy_server_stat获取集群的所有机器节点。在 OceanBase 数据库的现有实现中,sys在每个节点都有分布,因此,__all_virtual_proxy_server_stat返回的结果也就是sys租户的路由信息。
OBProxy 会15秒访问一次__all_virtual_proxy_server_stat,维护最新的路由信息,这样集群发生节点变更都可以感知到。
除了集群机器列表,OBProxy 还会通过sys租户获取partition分布信息、zone信息、租户信息等。可见sys租户对 OBProxy 非常的重要。
2. 普通租户路由
sys租户的路由信息就是集群的机器列表,但普通租户不同,普通租户路由信息就是租户资源(unit是CPU、内存、磁盘等资源载体,详情可查阅 OceanBase 数据库名词概念)所在的机器。
注意:由于历史原因,查询租户路由信息并不是通过unit相关的表,而是通过特殊表名__all_dummy表示查询租户信息。OBProxy 需要通过内部表__all_virtual_proxy_schema获取租户的机器列表,在访问__all_virtual_proxy_schema时,OBProxy 指定表名(__all_dummy)和指定租户名获取租户的节点信息。图7展示了租户的路由信息。
图7 普通租户路由信息
当获取到租户信息后,OBProxy 会保存在本地内存中,并根据一定策略进行缓存信息的更新。对于 sys 租户,通过15秒一次的拉取任务获得最新的信息;对于普通租户,而刷新频率并不高,普通租户的路由缓存策略如下。
-
创建:首次访问租户时,通过 __all_virtual_proxy_schema获得普通租户路由信息并创建。
-
淘汰:当 OBServer 返回错误码 OB_TENANT_NOT_IN_SERVER时设置缓存失效。
-
更新:当缓存失效后重新访问 __all_virtual_proxy_schema获得普通租户路由信息。
总的来说,在多租户架构下,OBProxy 通过 sys 租户获得元数据信息,sys租户本身路由信息就是集群的机器列表,然后通过元数据信息获得租户的路由信息。通过租户路由功能,OBProxy 支持了 OceanBase 数据库的多租户架构。
租户内路由
租户内路由是指在获取租户的机器列表后,选择合适的节点执行SQL。
对于租户内路由,OBProxy 可以像普通代理如 HAProxy 一样,一个客户端连接对应一个服务端连接,服务端机器从租户机器列表选取,这样功能就会简单很多,但无法满足性能和高可用要求,可见路由影响了连接管理。
租户内路由是路由功能最复杂的部分,主要原因是在想办法提供更好的性能和更高的可用性。我将按照主副本路由、备副本路由、租户机器路由、缓存信息、路由策略、事务路由和常见问题这七部分为你介绍。为什么是这样的顺序呢?你可以在阅读完本文后尝试梳理思维导图,感受更深。
1. 主副本路由
在分布式系统中,为了容灾高可用,会采用多副本机制。副本之间需要保证数据一致性,往往采用Paxos或者Raft算法,在工程实践中,有一个特殊副本,该副本数据最新,并控制数据在副本间的同步,这个副本叫做主副本,其它副本叫做备部分。
由于 OceanBase 数据库只有一个主副本,因此,主副本路由策略就是发往该副本。我们以select c1 from t1为例,介绍主副本路由需要满足的两个条件:
-
SQL语句操作(查询、插入、更新和删除等)实体表,上面例子是t1表。
-
请求必须读到最新数据,即强读(和弱读对应,弱读不要求读到最新数据)。
在 OBProxy 日志中,主副本路由的关键字是ROUTE_TYPE_LEADER。要实现主副本路由,就需要知道访问的分区标识和分区所在的位置。在 OBProxy 的实现中,分为两种情况。第一种情况是单分区表,表只有一个分区,根据表名就可以获得副本位置信息;第二种情况是多分区表,表有多个分区,OBProxy 需要根据SQL中的表名和分区键计算出分区标识,再获取副本位置信息。副本信息包含主副本信息和备副本信息,主副本路由只需要使用主副本信息。
对于多分区路由,涉及分区方式(如hash、range和list)、分区键类型(number、varchar等)、分区算法(如hash算法)、类型转换(如SQL中的值类型和分区键类型不同)等知识点,实现比较复杂,我们以二级分区为例介绍,分为10个步骤(见图8)。
图8 多分区路由流程
2. 备副本路由
了解完主副本路由策略后,你一定想到了备副本路由。备副本路由也需要满足两个条件:SQL语句查询实体表,上面例子是t1表;请求要求弱读即可。
这两个条件和主副本路由需要满足的条件是有区别的:
-
对于条件1,备副本路由只支持查询语句,不支持其他语句,这也是Paxos算法的实现要求。
-
对于条件2,需要主动设置弱读标记 ob_read_consistency=weak,可以通过hint、session等设置。
对于备副本路由,发往主副本和备副本都可以正常工作,因此备副本路由可以选择变多了(对于多个副本选择的问题,请参考下文“路由策略”的内容)。在很多时候,你可能认为备副本路由只能发往备副本,而实际上发往主副本也可以正常工作。
和备副本相关的一个重要话题就是读写分离,请求进行读写分离后,可以降低主副本压力,是一个很好用的功能。OBProxy 也实现了读写分离功能,并在不断打磨细节,在 OceanBase 公有云等场景帮助客户解决了性能问题。
3. 租户机器路由
有些时候我们无法获取主副本或备副本,此时就可以从租户机器中选取一台,这就是租户机器路由。
常见的租户机器路由场景如下:
-
SQL 本身不包含表名,如语句 select 1语句。
-
主副本或者备副本所在的机器有故障。
-
OBProxy 本身功能限制,如复杂 SQL 无法获得表名,无法走副本路由。
通过租户机器路由,OBProxy 将 SQL 发往了租户所在的机器,因此可以保障功能正常。租户机器路由和备副本路由一样,有多个副本可以选择,也存在着路由策略的问题。
4. 缓存信息
主副本路由、备副本路由和租户机器路由都需要通过sys租户查询副本路由信息,为了提升性能和降低对sys租户的压力,OBProxy 对路由信息做了缓存。对于缓存信息,最重要的是时效性。你可以对比sys租户的缓存信息更新,sys租户的缓存信息可以通过定期访问内部表创建和刷新,那么副本路由的缓存信息是否可以采用此策略呢?
答案是:不可以。主要问题是拉取副本路由信息的SQL太多,会对sys租户造成很大的压力。SQL的数量 = 副本的个数 * OBProxy的数量。
OceanBase 数据库可以支持十万、百万分区,分区数极大。OBProxy 的数量受到部署架构影响,部署在应用端形态下,数量也很多。因此,缓存时效性对 OBProxy 是一大难题,使用了过期的缓存信息就会出现大家常说的“路由不准”的问题。那么,怎么保证缓存的时效性呢?
我们先看一下缓存信息在 OBProxy 中的内容。使用root@proxysys账号登录,通过show proxyroute命令可以查看表的缓存信息,如下:
MySQL [(none)]> show proxyroute like 'ob1.hudson tt1 test sbtest1'\G
*************************** 1. row ***************************
cluster_name: ob1.hudson
tenant_name: tt1
database_name: test
table_name: sbtest1
state: AVAIL
partition_num: 1
replica_num: 3
table_id: 1101710651081698
cluster_version: 2
schema_version: 1649196335597728
from_rslist: N
create_time: 2022-04-07 12:41:16
last_valid_time: 2022-04-07 12:41:16
last_access_time: 2022-04-07 12:41:16
last_update_time: 1970-01-01 08:00:00
expire_time: 2022-04-12 12:48:42
relative_expire_time: 2022-04-07 12:40:41
server addr: server[0]=xx.xx.xx.xx:xx,leader,FULL; server[1]=xx.xx.xx.xx:xxfollower,FULL; server[2]=xx.xx.xx.xx:xx,follower,FULL;
这个例子展示了缓存包含的重要信息:集群名、租户名、库名、表名、分区数、副本数、时间、地址信息和缓存状态等。其中,缓存状态是需要我们重点关注的对象,缓存策略都是通过修改状态信息实现的,这些状态影响缓存的刷新机制。缓存信息分为如下5个状态。
-
BUILDING状态:缓存正在创建,需等待创建完成然后使用。
-
AVAIL状态:缓存正常,直接使用即可。
-
DIRTY状态:缓存失效,信息不准确。
-
UPDATING状态:失效的缓存正在更新过程中。
-
DELETED状态:缓存已经备删除,不可以使用,后续会被清理掉。
我们通过修改缓存状态就能刷新缓存状态,从而保证时效性,下面从创建、淘汰和刷新这三方面介绍缓存刷新机制。
-
缓存创建:首次访问分区时,OBProxy 通过查询 sys 租户 的 __all_virtual_proxy_schema获得,指定表名为真实表名,注意和租户路由信息部分区分,创建好后缓存状态为AVAIL 。
-
缓存淘汰:当 OBServer 返回路由不准时(ObServer 会通过OK报文中携带的is_partition_hit字段反馈),OBProxy 修改缓存状态为 DIRTY。
-
缓存刷新:当缓存信息变为 DIRTY状态后,淘汰过期缓存,并重新创建或者更新缓存信息。
目前缓存淘汰主要通过 OBServer 的报文反馈实现,这样就无法实时感知,只有出现一次“错误”路由后,才能刷新,这也是容易出问题一个地方。
5. 路由策略
5.1 路由策略介绍
路由策略用于从多副本中选择出一个合适的副本。这里的多副本,可能来自备副本路由时选择出的多个副本,也有可能是类似select 1 from dual这种语句,使用了租户的路由信息有多个副本(指租户的机器列表)。
路由策略主要有三种:LDC路由、Primary Zone路由和随机路由,路由策略优先级为 Primary Zone路由 > LDC路由 > 随机路由。
1)Primary Zone路由。在多副本选择时,优先发往Primary Zone(租户的属性,Primary Zone指副本的Leader优先分布在Primary Zone中)所在的机器。为什么会有这种路由策略?首先,OceanBase 中常用的高性能部署架构是租户的 Primary Zone 在一台机器,这样可以避免分布式系统的很多网络开销;其次,OBProxy 在主副本路由时,存在找不出表名和计算不出分区的情况,通过Primary Zone路由可以尽量发往主副本。
2)LDC路由是基于地址位置的路由,有两个重要的概念:IDC和Region。IDC表示逻辑机房概念,Region是城市的概念。OBProxy 和 OBServer 都可以设置LDC信息。通过LDC信息,OBProxy 可以确定和 OBServer 的位置关系。当我们设置了LDC信息后,OBProxy就会默认使用LDC路由。
-
OBServer LDC设置。
OBserver 的每个Zone都可以设置Region属性/idc属性,Region通常代表城市的概念,通常设置为城市名(大小写敏感),IDC代表该Zone所处的机房信息,通常设置机房名(小写)。设置SQL如下:
alter system modify zone "z1" set region = "SHANGHAI";
alter system modify zone "z1" set idc = "zue";
select * from __all_virtual_zone_stat;
图9 OBServer LDC设置
-
OBProxy设置LDC。
OBProxy通过配置项或者启动参数设置LDC信息。首先通过-i选项设置启动参数,然后执行配置项:执行SQL语句alter proxyconfig set proxy_idc_name='机房名';通过 OBProxy 执行内部命令show proxyinfo idc;可以检查proxy内部识别的LDC部署情况。
图10 OBProxy设置LDC
你一定在想有没有最佳实践,确实有。如果只有一个机房,LDC用处不大,因为我们认为同机房机器间延迟相同。如果在同机房要使用LDC,需要划分出LDC架构,并设置 OBServer 和 OBProxy 的LDC属性。如果有多个机房,就可以根据机房和城市设置LDC,如公有云杭州可用区I、H机房,它们Region相同,IDC名字不同。
某些特殊情况下,可以通过trick方法设置LDC影响LDC路由,但不太推荐你这么做。
3)随机路由。通过优先级路由后,如果还有多个副本,进行随机路由即可。如未开启Primary Zone路由或者未设置LDC路由,就会直接使用随机路由。
5.2 路由配置和查看
数据路由比较复杂的一个原因是有不同的路由策略,OBProxy默认策略是先进行主副本路由和备副本路由,没有副本则进行租户机器路由。如果只有一个副本被选中,则直接路由,否则根据策略路由。
对于 Primary Zone 路由和 LDC路由,受到配置项控制:
-
enable_primary_zone:为true表示使用 Primary Zone路由策略
-
proxy_idc_name:内容非空(内容为 idc 的名字)表示使用LDC路由
除了现有路由策略,有时我们想使用其它路由策略,可以通过修改配置项proxy_route_policy控制实现,设置后新策略优先级最高。目前我们经常设置的其它路由策略有两种,都和弱读有关。
-
"FOLLOWER_FIRST":优先发往备副本,如果无备副本可用发往主副本。
-
"FOLLOWER_ONLY":只能发往备副本,如果无备副本可用报错。
OBProxy 具体使用了什么路由策略,可以在 OBProxy 的日志中查看关键信息route_type,如ROUTE_TYPE_LEADER表示进行了主副本路由。ROUTE_TYPE_NONPARTITION_UNMERGE_LOCAL情况比较复杂,下面介绍主要关键字含义。
-
PARTITON:选取有副本数据的机器,不区分副本的类型。
-
NONPARTITION:不关心表数据分布,任何租户机器都可以。
-
FOLLOWER:发往备副本。
-
LEADER:发往主副本。
-
UNMERGE:发往不在合并状态的机器。
-
MERGE:发往在合并状态的机器。
-
LOCAL:发往同IDC(机房)机器。
-
REMOTE:发往同城不同IDC(机房)机器。
-
REGION:发往异地的机器。
-
READONLY:发往READONLY属性Zone内机器。
-
READWRITE:发往READWRITE属性Zone内机器。
-
DUP:复制表中,发往复制表所在的机器。
对于该问题,你也可以参考《高性能数据访问中间件 OBProxy(三):问题排查和服务运维》。
6. 事务路由
上面介绍了单个SQL的路由策略,有些功能如事务功能包含一条或多条SQL。对于事务路由,事务的第一条语句受到上述策略影响,后续SQL不再进行路由,直接发往第一条语句发往的节点。
为什么事务路由只能发往第一条语句发往节点?你可以参考数据路由影响因素中的“功能因素”。目前我们还未实现事务状态迁移,所以有此限制。
7. 常见问题
相信当你看到限制要求后,感觉到此处可能有坑。下面我按照本文的叙述顺序说明常见的坑。
-
无法获取表名(主副本路由)
-
SQL太复杂,目前 OBProxy 无法识别所有的SQL语句
-
SQL太长,OBProxy 存储SQL的buffer只有4K,SQL太长不会全解析
-
分区计算失败(主副本路由)
-
OBProxy 无法支持多分区键的计算如range(c1,c2);
-
SQL语句中没有分区键的表达式或者 OBProxy 未提出出来
-
分区键表达式 OBProxy 无法处理,如c1=now(),OBProxy还未支持now函数
-
使用过期缓存(缓存信息)
-
OBProxy 无主动刷新机制
-
OBServer未进行路由反馈(如分布式计划 OBServer 不反馈)
-
配置错误:
-
未设置路由策略为FOLLOWER_FIRST,弱读发往了主副本(备副本路由)
-
未设置LDC路由信息或者信息设置错误导致跨机房或者跨城(路由策略)
总结
数据路由功能点很多,并且功能点之间存在优先级问题,会让路由变得复杂。考虑性能和高可用因素,OBProxy 路由功能还有一些可以完善的地方,如 增强 Parser 能力、支持多分区键路由计算、提高缓存实效性等。我们也在不断提高路由能力,解决大家的问题。