查看原文
其他

从ORACLE/MySQL到OceanBase:数据访问代理

MQ4096 数据库技术闲谈 2022-08-27


近期打算写一个【从ORACLE/MySQL到OceanBase】序列。本文是第六篇,介绍分布式数据库连接代理OBProxy。OBProxy的功能和使用绝不像字面上那么简单,所以需要单独说一说。前期直播回放请到:https://cs.enmotech.com/course/play/17 。


数据库监听


理论上,客户端连接数据库的方式有多种,最常用的途径就是数据库监听一个端口,客户端连接这个端口。实际上,ORACLE/MySQL/OceanBase在实现的时候方式各不相同,功能特点也就各不相同。

ORACLE监听


ORACLE是个多进程的软件,其实例是由多个进程和一块共享内存组成。主进程(PMON进程)并不监听任何端口,而是要启动一个独立的Listener进程(tnslsnr)负责监听一个端口(默认1521,可配置,并且可以监听多个端口),客户端都连接这个端口。ORACLE提供了一个配置文件listener.ora用于配置这个监听,提供命令lsnrctl管理这个监听(可以startstopreload)。配置时需要指定监听在哪个IP上,如果有多个IP(一般是还有个VIP)会配置多个。Listener启动后,本机的或者远程的ORACLE实例可以自动将自己动态注册到这个Listener,或者也可以在listener.ora里静态注册(修改配置文件要重启监听)。

ORACLE Listener进程只负责响应客户端连接,然后转发到ORACLE实例里,然后ORACLE实例会创建一个用户进程跟客户端单独进行通信。如果客户端会话结束了,这个用户进程就退出了。ORACLE Listener有负载均衡能力。

MySQL监听


MySQL是单进程软件,在启动的时候就会监听一个端口(默认是3306,可以配置)。MySQL配置文件名默认是my.cnf,MySQL会按一定顺序在多个位置搜索这个配置文件。里面可以配置监听IP(可以是监听本机某个IP或所有IP)和端口。MySQL进程启动后,客户端可以直接连接监听端口并通信,每个会话是进程内部的一个线程。

OceanBase监听


OceanBase集群在每个节点上都会启动一个进程OBServer,也是单进程软件。进程启动后默认会监听两个端口(28812882)。其中2882是用于节点间通信的,2881是对外的连接专用端口。由于OceanBase是分布式数据库,至少有三个节点,所以每个节点都会监听端口2881,对于应用而言要访问的数据只会在某个节点上,应用不想记录那么多IP所以OceanBase提供一个反向代理软件OBProxy,进程启动时会监听端口2883。应用只需要访问这个OBProxy即可,OBProxy会根据应用的SQL决定将请求转发到后端哪个OBServer节点(连接后端的2881端口)。

这里注意,OBProxy并没有类似F5或者LVS之类的负载均衡机制,不提供VIP,其对外是自身的IP,它只做路由转发,有自己的转发规则(根据SQL里访问的表和事务定),绝大部分时候并不是无脑随机转发(负载均衡的常用策略在这里没有)。

数据库返回到应用客户端的数据连接也是走OBProxy回去,OBProxy只做数据转发,不涉及到SQL执行计划解析、运算等逻辑。所以OBProxy是很轻量的无状态的反向代理进程。如果OBProxy不可用,那对业务而言等于OceanBase集群都不可用,因此生产环境会部署多个OBProxy进程。有趣的是,当部署多个OBProxy进程时,需要在前端增加一个负载均衡设备(F5或者LVS) 以提供一个VIP给客户端使用(因为客户端也不想记录和管理那么多的OBProxyIP)。这个前端负载均衡设备的首要作用是解决了OBProxy的高可用问题,其次才是OBProxy之间的负载均衡。


所以应用访问OceanBase数据库时,数据链路是:客户端 <-> 负载均衡 <-> OBProxy <-> OBServer。每个客户端到OBProxy建立连接后,OBProxy会自动按需要建立到后端某个OBServer的连接。不同客户端连接之间,其后端连接也是彼此独立的(不共享)。所以OBProxy里并没有连接池那样的设计。

通过命令:show processlist 可以查看客户端跟OBProxy之间的连接。


其中IdOBProxy发给客户端的连接唯一ID。Host是客户端的ip


通过命令:show full processlist 可以查看该OBProxy和所有后端OBServer之间的连接。

其中IdOBServer发给其客户端(可能是OBProxy或者其他OBServer节点)的连接唯一性ID。Host是发起连接方的IPIp是被连接的OBServerip


然而,如果对OBProxy的了解只是这些还远远不够。所有的分布式产品在性能方面都有个特征,跨节点请求过多时会降低性能。OceanBase也不例外。在后面分析性能的环节我们就需要分析每个具体的SQL请求的路径。所以我们还需要了解每个SQL的路由规则。

OceanBase数据的分布特点


前面说了OBProxy进程路由是根据SQL访问的表和事务来定的,并不是随意的。上图中OBProxy进程之所以可能跟所有OBServer节点都有连接是因为其访问的数据可能分布在任意节点上。在分析OceanBase每个节点上的具体的数据(即分区,partition),一定要记得分布式数据库有两个机制在起作用:数据分片(sharding)和数据冗余(replication)。所以你看到的每个具体的分区(partition),它既是业务数据的一个分片(数据的子集),也是这个分片的一个副本(意味着默认在其他节点还有至少2个副本,不同点在于它们的角色)。详情请参考前文《分布式数据库选型——数据水平拆分方案》和《揭秘OceanBase的弹性伸缩和负载均衡原理》。

所以上图进一步细化为下图。


OceanBase的负载均衡原理是通过调整Leader副本的分布来间接改变各个节点的负载实现的。

OBProxy的路由规则


第一次接触路由规则


OceanBase的SQL在执行时,默认读写分区时都只会在该分区的Leader副本上。所以不管前面这个请求被路由到哪个OBServer节点,其最终读写访问的一定是该Leader副本所在的节点。这里要补充的是OBServer自身也是有路由的能力(前面提到OBServer的端口2881是可以对外直接提供访问,只是不推荐直连后端OBServer而已)。

上图给人的一个感觉是当业务要访问t1(p0)时,OBProxy就路由到节点OBServer01,访问t3(p1)时,OBProxy就路由到节点OBServer03。然而实际并不总是这样。这是由分布式数据库的特点决定的。如果每次SQL都是一个独立的事务,那么OBProxy转发就是按上图规则来。但是如果一个事务里有多个SQL的时候,或者一个SQL会访问到多个分区(如有表连接等),OBProxy只会选择路由到其中一个OBServer节点。之所以这样是因为OBProxy的功能只有路由,并没有保持事务的一致性能力和数据运算能力。当转发到一个OBServer节点之后,如果事务还要访问到其他节点的数据,那就由该节点再发起一个跨节点的请求。这就是OceanBase里跨节点请求的由来。

SQL执行计划分类

SQL的特点和OBProxy的路由规则导致SQL执行计划有三类:

  1. 本地执行:SQL在本地执行。

  2. 远程执行:SQL会发往其他节点执行。

  3. 分布式执行:SQL的部分运算需要从其他节点获取数据,节点间可以并行。




在上图这个场景里,三个SQL是在一个事务里的,OBProxy会路由到事务的第一个SQL里的表的Leader副本所在节点,事务里的其他SQL后续都会发往这个节点。这就导致了第2个访问表t1的SQL是远程执行,并且第三个表连接SQL是分布式执行。跨节点的请求,在性能上会比本地执行要慢一点。在压力测试过程中,要重点分析SQL的执行计划类型。

对于使用者而言,可以从OceanBase的SQL审计视图gv$sql_audit里找到每个执行过的SQL的执行计划类型(具体是gv$sql_audit.plan_type值,1表示本地执行,2表示远程执行,3表示分布式执行)。

当然这个也会体现在SQL的执行计划展示上。命令是:explain。这个以后再专门介绍。

完整的路由规则


路由选择是OBProxy的核心功能,这里面的规则实际还是比较复杂的。
首先路由逻辑如下图:


SQL在解析的时候只需要解析出SQL中的数据库名/表名/Hint即可,而事务的第一个有SQL往往会决定整个事务的请求被发往哪个OBServer节点。

因为SQL的种类很多,所以这里判断规则也很多。比如说能正确解析并发送的SQL有:

示例1: begin; select * from t1; commit;
示例2: set @@autocommit = 1; insert into t1 values(); set @@autocommit = 0;
示例3: select * from t1; insert into t2 values;
示例4: set @@ob_trx_timeout = 10000000; begin; select * from t1; commit;

下面这种SQL会发送到上一个事务SQL所发送的OBServer节点。

示例5: create table t1 (id int primary key); create table t2 (id int primary key);

上面说的规则是举例,具体以文档为主,可能会随着OBProxy的版本而不同。可以实际测试观察gv$sql_audit。注意每次测试都要是开启新的事务。


观察SQL执行细节示例:

select /*+ read_consistency(weak) query_timeout(1000000000) */ usec_to_time(request_time) req_time, svr_Ip, sid, client_ip, tenant_id,tenant_name,user_name,db_name, query_sql, affected_rows,ret_code,plan_type
from gv$sql_audit
where tenant_id > 1 and request_time >= time_to_usec('2019-07-24 19:15:00') and user_name in ('testuser') and sid=2147717786
order by request_time desc
limit 100;

一个比较好的建议是

  1. 将一个事务的所有语句(最好只有begin/set/commit和DML)放在一个multiple stmtement里面。当然如果DML很多的情况下还是要分开发送。SQL文本长度也不宜太长,否则解析会耗内存和有可能执行计划不被缓存。

  2. DDL和DML不要在同一个multiple stmtement里面。


在OceanBase里,分区表的情况会复杂一点。因为一个分区表的不同分区的leader副本是有可能分布在不同节点上。所以针对分区表的路由是要看SQL里访问的分区是哪个。这个就让情况复杂一点了,因为不是每个SQL都会带上分区键(虽然我们总是那么建议用户的)。目前支持带分区键条件的路由,也支持指定分区名的SQL路由。

当然即使带上了分区键,也可能不是等值条件,比如说是范围查询、向量比较等。有些OBProxy暂时并不支持。比如说不支持有多列的range column分区和key分区,或者暂不支持分区列的向量比较、函数/表达式运算等。不支持指的是可能路由到不正确的后端OBServer节点。路由问题是所有分布式数据库的共性问题,不同产品能力会有差异。

上图在确定路由规则时,提到一个LDC规则。这个是OceanBase多机房部署时应用多活设计相关的一个规则。也是OBProxy能力强大的一个地方。下面单独解释。

LDC规则


现在又回到一个单SQL的路由规则场景,不考虑事务。默认规则是SQL会发往某个分区的Leader副本所在节点。这个特性叫强一致性读。OceanBase还提供一种弱一致性读特性,就是允许读取分区的Follower副本(即备副本)。这个比较像传统数据库架构里应用读取只读备库,以实现读写分离的效果。弱一致性读第一个场景就是用于做读写分离用的。

不一样的是,OceanBase至少是三副本架构,至少有2个以上的备副本,那么应该读取哪个备副本呢。这个就有很大的发挥余地。可以随机取,也可以根据实际机房部署情况就近取。就近指的是取应用所在的机房或者城市,以规避应用跨机房读取数据库或者跨城市读取数据库这种现象(这个对应用性能也有损害,道理跟分布式数据库内部跨节点请求同理)。

在实现这个之前,首先需要在OceanBase集群里作一个管理设置,就是要定义每个Zone分属哪些IDC以前我们说Zone是逻辑单位,可以是机房,可以是机柜。那么这里就要具体指明了。

下面是一个设置Zone的LDC相关信息。手动搭建的OceanBase集群需要设置一下。

alter system modify zone "zone1" set region='HZ';
alter system modify zone "zone1" set idc='idc1';
alter system modify zone "zone2" set region='HZ';
alter system modify zone "zone2" set idc='idc2';
alter system modify zone "zone3" set region='SH';
alter system modify zone "zone3" set idc='idc3';
alter system modify zone "zone4" set region='SZ';
alter system modify zone "zone4" set idc='idc4';
alter system modify zone "zone4" set zone_type='ReadOnly';

select * from __all_zone where name in ('idc','region','zone_type');


然后针对每个OBProxy设置LDC属性。如果不设置就是路由的时候不考虑LDC规则。

OBProxy自身的参数里proxy_idc_name是用于设置该OBProxy进程的LDC偏好。具体可以设置到机房或者Zone信息。

alter proxyconfig set proxy_idc_name='zone3';

alter proxyconfig set proxy_idc_name='idc3';


此外在客户端连接里,还需要设置一个Follower副本路由变量ob_route_policy默认值是READONLY_ZONE_FIRST意思是如果有只读Zone,则备副本优先选择只读副本;否则就选择proxy_idc_name设置的Zone里的OBServer节点。

MySQL [sysbenchtest]> show global variables like 'ob_route_policy';
+-----------------+---------------------+
| Variable_name | Value |
+-----------------+---------------------+
| ob_route_policy | READONLY_ZONE_FIRST |
+-----------------+---------------------+
1 row in set (0.01 sec)

这个变量是可以在会话级别设置覆盖全局设置。此前的proxy_idc_name也可以(不过写法有点特殊)。

set @proxy_idc_name='zone2';select /*+ read_consistency(weak)*/ * from t1 where id=3;

set session ob_route_policy='only_readonly_zone'; select /*+ read_consistency(weak)*/ * from t1 where id=3;

关于只读副本,是专门用于读写分离的一个Zone,里面全部是备副本,但是不参与投票。读写分离是个有趣的话题,并不像字面上那么简单,这个以后再专门分享。

这里只需要记住,不管是读备副本或者只读副本,都需要设置弱一致性读才可以。对业务来说可能不想在每个SQL里都加上弱一致性读HINT(/*+ read_consistency(weak)*/),那么可以在会话级别统一设置,但是不要在租户级别设置。


OBProxy对高可用的重要性


在数据库高可用设计方案里,都会包含两部分。一个是数据库自身的高可用(即通常说的主备切换部分),另外一部分就是客户端路由切换。第一部分传统的有ORACLE的Dataguard,MySQL的Mastr-Slave同步等,第二部分通常是用VIP、域名技术,或者中间件环节自己维护对应的连接映射并切换映射。

OceanBase的数据至少三副本,以副本为单位会独立自动选举出一个新的主副本提供服务。如果主备副本角色切换了,OBProxy会及时从OBServer节点那里获取反馈并自动维护后端连接。这个过程对应用是透明的,即数据库端主备副本切换了,应用不需要改IP或者重新连接。 OBProxy在判断一个OBServer节点不可用后为将其放入黑名单,避免后续请求再次放入这个节点。同时又会定期去探测黑名单里的节点可用性,以在故障节点恢复服务后尽快将其纳入到路由选择里。

有时候我们也把这个能力称为OBProxy的路由容灾能力。依然要提一下,OBProxy自身的高可用目前是通过部署多个OBProxy再借助前端F5/LVS等软硬件实现。


总结


OBProxy的核心能力就是路由和容灾(容灾是指数据库主备副本切换后OBProxy路由自动切换,后端换连接对应用透明),基本的规则就是强一致性读(读写访问Leader副本),一个事务的SQL会以第一个有实体表的SQL的表的Leader副本所在节点为路由目标节点,由此可能带来分布式执行计划和远程执行计划。在性能压测的时候需要重点关注SQL的执行计划类型。

OceanBase的负载均衡原理是通过调整Leader副本的分布来间接改变各个节点的负载实现的,这其中OBProxy的路由在打配合。OBProxy自身并没有负载均衡能力。多个OBProxy之间的负载均衡(还有高可用)又是靠前端F5或LVS提供。

OceanBase还持弱一致性读,可以在租户级别、会话级别或者SQL上设置。弱一致性读默认会随机选择一个备副本。通过OBProxyLDC变量和路由变量可以实现备副本选择时就近选择,或者专门选择只读Zone里的只读副本。弱一致性读可以实现读写分离,只读副本可以降低主副本负载,这些操作对业务都不需要特别改动。

推荐阅读



更多分享敬请关注公众号:obpilot,对OB有兴趣还可以发送消息“加好友”进一步沟通。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存