写点什么

高性能数据访问中间件 OBProxy(五):一文讲透数据路由

  • 2022 年 9 月 27 日
    浙江
  • 本文字数:8432 字

    阅读完需:约 28 分钟

上篇文章我们介绍了 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 就会报错(优雅做法)或断连接(粗暴做法)。



<br />图 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 故障后,将该节点加入黑名单。路由时从健康节点选择。



<br />图 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. 租户机器路由

有些时候我们无法获取主副本或备副本,此时就可以从租户机器中选取一台,这就是租户机器路由。<br />常见的租户机器路由场景如下:


  • 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:42relative_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;



<br />图 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 能力、支持多分区键路由计算、提高缓存实效性等。我们也在不断提高路由能力,解决大家的问题。

课后互动

上期互动答案

问:JDBC 和 OBProxy 建立了一个连接,晚上 OBProxy 被 kill 掉重启,请问此时 JDBC 会抛出异常吗?


答:不会的。因为 JDBC 不感知 tcp 异常,所以只有在真正使用连接时才会发现连接出现问题,这种问题就会比较难排查。大家可以看 JDBC 的异常日志会打印上次发给 Server 的请求时间和上次从 Server 接收到请求的时间,如果接收请求的时间大于发送请求时间,那么上次请求应该是正常完成,可能就是上来就拿到了一个坏连接。

本期互动问题

问:对于一个有表名的强读请求,如果 SQL 长度为 7K,那么可能会如何路由?


欢迎你在问答区发帖讨论,下篇文章揭晓答案。

用户头像

企业级原生分布式数据库 2020.05.06 加入

github:https://github.com/oceanbase/oceanbase 欢迎大家

评论

发布
暂无评论
高性能数据访问中间件 OBProxy(五):一文讲透数据路由_OceanBase 数据库_InfoQ写作社区