作者简介:致新,OceanBase 数据链路高级研发工程师。
一、ODP 简介
1.1 OceanBase 生态
OceanBase 除了内核中大家相对熟悉的 SQL 模块、事务模块、存储模块等,其周边还有很多生态工具来帮助 OceanBase 服务好用户,比如本章将要介绍的 ODP(原名是OBProxy,现改名为 ODP)。ODP 是 OceanBase 的访问入口,具有代理和中间件的特性。

1.2 ODP 发展历史
ODP 目前的产品特性,除了方案设计外,和产品的发展历史也有关,介绍 ODP 发展历史可以帮助大家更好的理解 ODP 的产品特性。

ODP 的历史主要分成了三个阶段:
1.3 访问入口
ODP 是什么?简单来说,ODP 是数据库的访问入口,OceanBase 的定位是一个分布式数据库,ODP 让用户更简单地访问数据库。
举个例子,我们应用命令行登录 MySQL、Oracle 等产品时,需要指定 IP、port、用户名和密码,而对于分布式数据库 OceanBase 来说,这里存在的问题是如果同时存在三台 OBServer 时如何填写这些信息。ODP 让这一切都变得简单,用户使用时只需要填写 ODP 的 IP 和 port ,就可以正常访问数据库。
ODP 完全兼容 MySQL 协议,目前主要是 5x 系列版本协议,8.0 协议还在适配中。因为协议兼容,所以用户的代码可以使用现有的 MySQL 语言客户端,如 Java 程序可以直接使用 MySQL 的 jdbc 实现。对于访问数据库的代码,使用 ODP 时只需要修改 IP 和 port,其他的代码无需做任何改动就可以访问 OceanBase 数据库。
ODP 作为入口的性能是非常好的,支持多线程利用多核能力、实现异步编程提供高效转发,不会有阻塞操作。ODP 本身非常轻量,无状态不会存取数据,扩展性好。

上图说明了一条 SQL 的访问路径,下面的三个 zone 可以简单地理解成机房,一个机房里部署了两台 OBServer,数据分散在所有的 zone 里面。当执行一条 SQL 时,ODP 可以准确地把这个 SQL 请求发送到有该数据的 OBServer 上。
路由方面,ODP 还支持了 OceanBase 的弱读功能,可以实现读写分离,将读流量发往备副本,实现负载均衡。
1.4 代理模式

关于代理产品的优缺点,下面是我的一些思考:
这里举两个场景,一个是现在比较流行的 Docker、Mesh 化的部署。ODP 本身支持了 sidecar 的部署,可以理解为应用部署在一个 Docker 中,ODP 部署在另一个 Docker 中,两个 Docker 在同一台机器上,这样它是一对一的关系,只需要用户的 Docker 访问旁边的 ODP 的 Docker。
除了这种模式之外,还有另一种可能是集中部署。ODP 前面都会部署一个 LB 组件,如 F5,云上也有专门的 LB 场景。
这两种场景都比较常用,sidecar 部署目前是和云原生一块发展的,蚂蚁内部广泛使用这一种部署方式。LB 部署也很多,阿里公有云上的 ODP 就使用了这种部署,在私有云场景中也经常被采用。
1.5 架构比较

大家做架构比较时,对于单机数据库的代理一般使用直连方式,在生产环境下可能会采用代理组件,如 ProxySQL 会做一些 MySQL 主备之间的切换。关于 OceanBase 的架构,下面和另一款比较知名的开源分布式数据库 TiDB 去进行一些比较。
从 TiDB 的架构上大家可以看到,TiDB 本身是没有代理产品的,LB 后面就直接是 TiDB 的 server ,在 TiDB server 后面是 TiKV。为什么 OceanBase 就有 ODP 这个模块去做代理呢?这和 OBerver 本身的实现有关,虽然大家都是分布式架构,但也有不同。OceanBase 是单机分布式一体化的架构,目前没有做存算分离,它的计算节点和存储数据的节点都在一起。TiDB server是一个 SQL 处理引擎,本身是无状态的,它将请求转发给后面的 TiKV,是存算分离的。
TiDB server 里面实现了路由功能,还做了语法解析、计划生成等。ODP 就比较简单,只实现 SQL 转发,同时保证高性能的转发效率。ODP 都可以保证 SQL 发往的那台 OBServer 是有相关数据的。举个例子,向T1表插入数据,ODP 可以保证把请求发往有 T1 表数据的机器,这样整体性能最好。
另一方面,ODP 在访问核心链路上做了各种性能的优化,可以保证不会成为整个链路上的瓶颈。
最后,代理产品除了为 OceanBase 路由外,它还可以去做一些中间件的能力,比如白名单、分库分表等。整个产品作为独立的组件,安装部署非常方便。
二、ODP 特性介绍
2.1 高性能转发

ODP 的第一个特性一定是高性能的,在网络多一跳的情况下,我们一定要保证转发效率足够高,不会成为系统的瓶颈。ODP 在设计实现上是多线程加异步的编程模型。在性能测试中,我们在阿里云 ECS.g7.8xlarge 机型上测试了 sysbench的point_selec t场景,得到了下面这组数据。大家可以看到第三列,三机部署的情况下 ODP 做到100万的转化只需要30个核左右,所以它的单核转发效率是特别高的。

ODP 是怎么实现高性能的呢?上图简单地介绍了 ODP 的线程模型。ODP 的每个线程基本上可以认为是一样的,各自独立地去做转发任务,线程之间基本没有共享数据,可以充分地利用多核的能力。
代码实现上有两套逻辑,一个是 IO 逻辑,即 TCP 流的转发;另一个是业务逻辑,进行数据转发和处理的流程。这两个逻辑都是在同一个线程里完成的。
关于 ODP 的性能,一方面需要保证线程数,如果设置了 ODP 线程数是8,那我们最多只能利用8哥 CPU 核。所以要合理的设置工作线程数。不建议设置线程数超过机器本身 CPU 的核数,这样就会存在两个或多个线程抢占同一个 CPU,性能反而会下降。另一方面是我们可以保证把任务均匀地分发到每个线程里,这样做到每个核的负载均衡。

前面讲到了线程模型和我们的测试数据。我再从代码优化和整体性能去说一下。代码层面,针对主路径做了很多优化,提高程序执行效率。另一方面,为了保证性能不回退,我们实现了核心场景的性能回归。比如 TPCC 场景、蚂蚁内部的核心交易支付场景等。
从整体性能来看,ODP 不追求本身的性能最优,而是保证整个链路的性能最优。举例说明,来了一条 SQL 以后,一种转发策略是简单的把这条 SQL 转发给一台 OBServer ;另一种策略就是对这个 SQL 做解析,比如提取表名获取这个表的分布,再把 SQL 转发给有相关数据的 OBServer 。在 ODP 层面,第二种方式相比第一种方式多了 SQL 的解析以及获取 table、table 数据分布的逻辑。ODP 虽然需要牺牲 CPU 去计算这套逻辑,但根据实际测试场景的表现,第二种情况的整体性能要明显优于第一种。这就是所说的全链路最优。

简单介绍一下 ODP 性能问题的分析。ODP 前面连着 LB 或业务的 App,后面连着 OBServer ,所以一定要全链路去分析性能问题。ODP 和 OBServer 经过很多年的场景的锻炼,整体的性能还是非常稳定的。当确定是 ODP 的问题时,大家可以用 top 命令或 perf 命令去查看具体的性能。比如通过 top 命令,如果 ODP 和 OBServer 部署在一起,那么 ODP 的 CPU消耗是要小于 OBServer 的;使用 perf 命令查看 ODP 有没有热点函数,基本上 ODP 每个函数的消耗都没有超过5%的。性能优化是很复杂的问题,需要从全局把控。
2.2 连接管理

ODP 能够使得访问 OBServer 足够简单,首先介绍一下登录信息和登录流程方面的内容。
OceanBase 本身的架构非常复杂,从用户名就可以看出,如完整名字为 username@tenant_name#cluster_name:cluster_id,username 是连 MySQL 数据库时的用户名,tenantname 是访问的租户名,因为 OceanBase 本身是多租户的,cluster_name 指定访问的集群。ODP 不仅可以代理同一个集群,还可以代理多个集群,即通过一个 ODP 不仅可以访问一个集群下的不同租户,还可以访问多个集群下的不同租户。cluster_id 和 OceanBase 的主备库形态有关,比如0代表主库,1代表备库。
ODP 对 MySQL 协议完全兼容,目前是兼容的 5.7 版本,8.0 版本协议正在做相关的适配工作。基本上使用开源的 MySQL 的客户端都可以直接访问 ODP ,但是如果是 Oracle 模式,还是必须用 OceanBase 的专有客户端。
最后再解释一下 ODP 是怎么做到可以代理不同集群的不同租户的。一种方式是 rslist 的方式,OceanBase 中包含一个 rootserver 组件,把 rootserver 的地址配给 ODP 就可以。但是这种方式的问题是 ODP 只能访问一个集群了。另一种方式就是通过 OCP 组件的 URL ,OCP 保存不同集群的所有的 rslist 信息,这样就可以访问不同的集群了。
OceanBase 的整个登录流程还是非常复杂的,首先进行 MySQL 协议的握手,在建立连接后客户端会主动发一些 SQL,比如 set names UTF8 ,告诉数据库传输的数据都是 UTF8 编码的。OceanBase 比这个还要再复杂一些,因为 ODP 本身也会发一些内部的 SQL,比如查询集群机器列表,因为访问每个集群时,需要拿到这个信息再去把这个请求转发给对应的机器。
整个登录流程比较复杂,但一般是没有什么问题的。特殊场景下,比如说某条 SQL 突然执行慢了,很可能会导致登录失败,这是本身复杂导致的。

在了解 ODP 的产品行为时需要从分布式角度考虑。大家使用 MySQL 时直接使用客户端连接,只需要考虑一个连接方向。但 ODP 场景就比较复杂,上图中的 OBProxy 是我们的 ODP进程名,对于一个客户端的连接,目前的实现是会和 server 建立多个连接的,形成一对多的关系,此时就要考虑和客户的连接、和 server 连接的问题。
假设分布式系统中一个 server 发生了宕机、重启等各种情况,ODP 一般都会对这各种场景会做一些相关的处理,屏蔽 server 的故障,客户端和 ODP 的连接还是继续保持,这样就可以保证客户端和 ODP 之间连接的稳定。
一个 ODP 和 OBServer 之间连接,它只属于一个客户端和 ODP 的连接,不会做共享复用。比如说有两个客户端,他同时访问同一个 OBServer,他这时候会建两个连接,而不是说共用同一个连接。目前我们的实现是这样的,后续会实现连接池的功能。
最后一个再介绍一下这个状态同步的功能。假设用户主动发生了发送 set autocommit=0,ODP 需要对所有的 OBServer 实现状态同步。Set autocommit=0 这条 SQL 只会发往一台 OBServer ,执行完成后将结果返回给客户端。当下一次访问另一台 OBServer 时,需要由 ODP 保证 set autocommit=0,实现 ODP 和 OBServer 之间的状态同步。

后面就是举几个问题,让大家更深入地了解一下连接管理部分。
举例来说,如果客户端和 ODP 的网络慢,ODP 和 OBServer 的网络是正常的,但是对于用户的体验都是一样的,整个访问都是慢的,这里只能去分段去排查。
在实际场景中主要还是要大家都积极地配合,OBServer 提供相关的数据,客户端本身的数据也非常的关键,比如执行时间以及执行的 SQL 。比如说大家用 jdbc ,jdbc 本身的超时参数可能会把连接断掉,如果应用可以提供给我们时间点和具体的 SQL,可以帮助我们去分析问题。

再讲解一下 PS 模式的实现,了解一下 ODP 对整个分布式状态管理的复杂性。PS 模式直连比较简单,第一个请求是发送 prepare 请求,select * from T where A=?,下一个请求就是 execute 的请求,把相关数据带回来。
ODP 需要实现最佳路由,将请求发往数据所在的机器。假设表T是分区表,数据分布在多台 OBServer 上。用户执行第一个 execute 时,发现数据在 OBServer1 这台机器上,就可以把 prepare 请求发给 OBServer1 机器,再发送 execute 请求。用户在发送第二个 execute 请求时发现数据在 OBServer2 ,ODP 不能简单地把这个请求发往了发往 OBServer2,一定要把之前的 prepare 请求发过去,不然就出现了 OBServer 没有 prepare 就 execute 的情况。
2.3 超时管理

一些超时参数会影响相关的行为。
ob_query_timeout 设置为10秒时,如果一条 SQL 在10秒后没有执行完成,OceanBase 会返回一个错误。如果10秒内 OBServer 没有给 ODP 返回这个错误,ODP 这时候就比较难处理。因为在10秒后,这条 SQL 要么执行成功,要么返回错误,但是现在没有获得任何请求。ODP 在这里进行了兼容处理,专门提供一个额外增加20秒的配置项,如果比如说10秒+20 秒后还没有任何返回请求就会断开连接,因为这种状态是异常的。
2.4 数据路由

OceanBase 本身也是有数据路由能力的,假设T1表的数据在 OBServer1,如果这条 SQL 发往另一台 OBServer 的话,另一台 OBServer 会做转发。但是目前 ODP 的转化性能是最高的。
路由除了数据分布以外还需要考虑更多的因素。
路由因素这里举了三个比较重要的类,路由本身是和业务相关的,其他点暂时就不列举了。ODP 路由的主要流程可以概括成四个情况:
另一个是分区表的路由,分区表本身有很多的 leader 副本,需要根据用户的 SQL算出具体的数据存放在哪个 leader 副本后才能发往正确的数据,与非分区表路由表相比增加了分区计算的步骤。
路由是我们 ODP 处理的最多的问题之一,因为其中的每一个流程都存在着非常复杂的情况。

另一个问题是 parser 目前最多解析4K的数据,如果用户写了特别长的一个数据,比如写了一个16K的 SQL 请求,那很可能是拿取不到的。我们目前正在完善对复杂 SQL 语句的支持,但也需要考虑与快速解析的性能的权衡。对于 SQL 长度的问题,为了提取表名很可能需要把整个 SQL 全部保存下来才能进行分析,此时如果同时并发很多大请求,内存消耗会特别大。这也是我们保留目前设计的原因。
路由缓存被动更新的另一个例子是弹性问题。比如说双11大促的时候,突然需要爬表做副本重新分布等操作,此时由于 ODP 缓存没有主动更新,所以需要大流量地去触发 ODP的更新机制。ODP 虽然没有主动更新,但是对于错误的路由提供了反馈机制,因此可以触发所有表的反馈机制去完成更新操作。
此外,高可用机制下,对机器进行变更后可能会路由到不符合预期的机器,有可能就是缓存信息没有更新。
2.5 路由缓存

2.6 LDC 路由

ODP 可以配置 LDC 路由策略,配置后可将位置信息与 OBServer 的状态信息纳入考虑。
2.7 Primary Zone 路由

分布式数据库可以把数据做分区打散,充分利用每一台机器的资源,但是也存在明显的问题,如无论如何优化,分布式事务的执行效率不会高于本机的执行效率。OBServer 为了性能场景,提出了 Primary Zone 的概念。
2.8 高可用

前述两点反应的延迟在于性能方面,另一方面是高可用的关于黑名单的处理。高可用中有30秒恢复或者45秒内恢复的说法。ODP 一方面从 OBServer 处查询获取信息,另一方面通过真正执行获取反馈信息,建立一套复杂的重试或黑名单相关的策略来完成高可用。
2.9 专有协议

ODP 的另一个杀手锏是协议。一方面,一般应用和 ODP 兼容 MySQL ,利于生态的扩展。另一方面 ODP 和 OBServer 对协议做了扩充。比如在数据安全方面,可以校验数据的 CRC 保证数据传输的准确性,进行 SSL 传输加密。能够支持 Oracle 模式的数据类型和交互模型。从监控诊断方面来说,可以进行全链路监控,记录监控信息。在状态同步方面,解决了 O BServer 和 ODP 之间如何高效的进行传输同步的问题。
2.10 中间件特性

ODP 支持很多中间件特性。
三、ODP 实践
3.1 安装部署

ODP 的二进制名为 OBproxy ,编译后约占 400M ,除去符号表约占 30M,核心链路的函数有200多个。用户可从源码进行编译,或者通过开源的相关工具或者生产环境,通过 OCP 进行安装部署。OBproxy 足够轻量,对 CPU 核数没有要求,启动内存在百兆级别,在普通的 Linux 机器均可启动。守护脚本 obproxyd.sh 通过 Linux 的 nc 命令尝试连接 ODP,如果端口连接不同,对 ODP 进行重新拉取,可以秒级拉取恢复高可用。
如上图的截图中,显示了安装好后,ODP 的目录。bin 目录存放二进制文件,etc 目录存放配置文件,log 可供用户查询日志相关。

ODP 的部署有两种方式。

3.2 监控

系统提供了 SQL 化的接口命令查看 ODP 的状态。例如,用户可以通过 MySQL 的命令行连接管理员账号,然后通过特定的命令查看例如 ODP 的内存使用等。此外,ODP 支持普罗米修斯系统,可以将数据传输给普罗米修斯系统。最后,监控比较关键的日志,如慢 SQL 、错误 SQL、SQL 统计日志,以及 SQL 审计日志,监控指标包括 QPS、TPS、连接数、响应时间、错误数等。
3.3 运维

我们提供一个管理员的账号—— root@proxysys。连接这个账号,请求并不会发往 OBServer ,并且整套命令和 MySQL 协议完全兼容,用户可以用 MySQL 命令行或者用 jdbc 程序来运维 ODP。

这里介绍两个内部账号。第一个是运维管理员。第二个是一个 sys 租户的域账号,这个账号是 ODP 访问 OBServer 的内部数据的账号。现在 ODP 访问 OBServer 采用了 SQL 的接口,目前通过这个账号来实现的。
3.4 ODP 问题分析

ODP 如果出现了问题,该如何分析?
3.4 ODP 日志介绍

ODP 的日志相对来说非常规范,基本的种类如 digest 日志是审计,slow 日志是慢 SQL,error 日志是错误 SQL,stat 日志是 SQL 的统计信息。相关 SQL 的关键信息在这里简单列举:如时间、租户、SQL 、错误码等。

这里介绍一下相关问题的处理思路:

该问题答案为 BC。
A 选项正确。MySQL 模式下,用户采用 MySQL 的驱动程序即可。唯一需要注意的是,建议用户使用5.71相关的版本,不要使用太新或者太旧的版本。
B 选项错误。ODP 无法保证路由 100%正确,可能会有各种各样复杂的原因导致路有错误。
C 选项错误。如果 OBproxy 和 OBserver 之间的连接断了,ODP 会主动地去对这种错误去进行容错。但是如果客户端与 ODP 之间的连接断了,比如用户在 jdbc 中设置 socket timeout 等于 5 秒,但实际的执行时间是15秒,那么 jdbc 程序就会把 TCP 连接断掉,此时 OBproxy 和 OBserver 的连接也不再需要,这个连接也会断掉。
D 选项正确。
四、Q&A
这里区别在于产品名字叫 ODP,二进制程序的名字叫 OBproxy 。针对第二个问题,首先我们是 OceanBase 公司的产品,支持 OceanBase 的分库分表。针对 RDS 和 MySQL 的分库分表实现过,基本上是兼容的,但是后面用得比较少,建议大家使用 OceanBase 的分库分表。
这个场景可以理解为,比如 App 的一个 SQL 执行了 10 分钟,它不想再执行了。但是没有触发 OBserver 的 query timeout,因此 SQL 还在执行,此时用户想把这个 query 给关掉。然后 MySQL 驱动再新建一个连接,此时它这个请求很可能就会发往别的 proxy,所以这种情况下我们建议先确定这个执行很长时间 SQL 发往哪个ODP了,然后去主动避免 lvs 连接该OBproxy,再去杀死该进程。整体的操作比较复杂,因为这个新建的连接 lvs 会把请求发送给另一个 ODP,可能会导致杀错 session 或者很可能是一个错误的 session ID,然后报错。
这里有两种情况,首先路由会先去缓存拿,如果这个缓存本身是错的,但是 ODP 不知道,还是会主动把请求根据缓存去发送,发送以后 OBserver 才会告诉 ODP 这个缓存失效了,我们会设计标记缓存失效的信息。第二种情况,如果用户第二次 SQL 来时,因为缓存信息失效了,此时我们支持先刷新缓存,然后再去路由,然后发往仲裁机器。目前如果是 31x 版本,有个配置项去控制,如果发现路由缓存错了,可以先进行刷新。其实大部分场景下,如果不做频繁的扩缩容,一般不会出现这种问题。如果是做机器的运维,也建议可以先通过 SQL 在业务低峰进行刷新,让缓存变成最新版本。
如果说 OBserver 发生了切主,OBserver 会进行转发,而不会直接导致 SQL 的失败。但如果 SQL 执行时间超过了 jdbc 的超时时间,jdbc 会主动 kill,这时才会导致 SQL 失败。举一个 OBserver 切主的场景,比如每日合并,对于每日合并的机器,ODP 会去做容错,SQL 失败需要进行详细的分析。
全链路跟踪,一方面需要业务方配合。比如蚂蚁内部,业务 SQL中 OceanBase 只是 hint ,用户可以写一些自己的 hint ,传入 tracert,获得相关的全链路信息。另一方面比如说像开源用户或者是商业用户,很可能得不到别的业务方的配合,所以目前我们主动去 jdbc 中进行设置,来做到全链路跟踪。目前我们还没有完全实现全链路跟踪,但是相关的产品正在研发中。
欢迎广大 OceanBase 爱好者、用户和客户随时与我们联系、反馈,方式如下:

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70005215/viewspace-2883557/ ,如需转载,请注明出处,否则将追究法律责任。