写点什么

高性能数据访问中间件 OBProxy(四):一文讲透连接管理

  • 2022 年 9 月 08 日
    浙江
  • 本文字数:7820 字

    阅读完需:约 26 分钟

上篇内容我们讲到 OBProxy 的问题排查,将大家在使用 OBProxy 时可能遇到的问题一一分析,并给出经过实践验证的解决方案。从本篇开始,我将介绍 OBProxy 在 OceanBase 分布式架构中的作用和原理,帮助你更透彻地了解 OBProxy,实现“好用”和“用好”。 同时,OBProxy 在上百家企业的持续运行,我们积累了大量的工程实践经验,也将遇到的问题作为案例,伴随 OBProxy 的原理讲解分享给你,供大家参考。


本篇从 OBProxy 的重要特性连接管理讲起,其中,对连接映射关系、session 状态同步等内容,你可以将其与单机数据库比较,体会分布式系统和单机系统的异同。


OBProxy 的连接管理步骤及原理

在正式讲解之前,我先介绍一下本篇内容的技术背景。我们知道,OBProxy 为用户提供了数据库接入和路由功能,用户连接 OBProxy 就可以正常使用 OceanBase 数据库。用户在使用数据库功能时,OBProxy 和 OBServer 进行交互,且交互流程对用户透明,连接管理就是该交互过程中的关键点之一。OBProxy 的连接管理有三个特性:


  1. **代理特性:**OBProxy 既是客户端,也是服务端,还需要保证交互行为符合 MySQL 协议规范。

  2. **功能特性:**OBProxy 实现了很多的连接功能特性,如访问不同集群、不同租户,再如支持主备库、分布式下的 ps 功能,以及兼容 kill、show processlist 等命令。

  3. **高可用特性:**OBProxy 可以处理超时、机器状态变化、网络状态变化等问题,屏蔽后端异常,让用户无感知。


接下来我们按使用 OBProxy 的操作步骤一步一步来讲解连接管理内容。


第一步:用户登录

1. 登录信息

在登录 OBProxy 时,我们需要填写数据库 IP 和 PORT、用户名、密码等信息,对于用户名,OBServer 的格式为 user_name@tenant_name。因为 OBProxy 可以代理不同的集群,所以格式又变为 user_name@tenant_name#cluster_name,字段含义如下:


  • **user_name:**登录的用户名,密码保存在 OBServer 中,OBProxy 只做登录报文转发不做密码校验。

  • **tenant_name:**OBServer 是多租户架构,tenant_name 表示访问的租户名。

  • **cluster_name:**集群名,OBProxy 支持访问多个集群,不同集群通过 cluster_name 区分。


有了这些信息,我们就可以通过 JDBC 驱动、MySQL 命令行、Navicat 等工具连接 OBProxy 访问数据库了。


你可能会疑惑,用户登录时 OBProxy 如何找到对应的机器呢?这就要依赖 OCP 系统(通过 obproxy_config_server_url 配置项指定 OCP 的 url 地址访问),OCP 会保存集群名和集群的机器列表,OBProxy 通过访问 OCP 获取这些信息,整个流程如下图。



有了租户的机器列表,我们就可以进行路由转发了。这里需要注意的是,本系列第二篇介绍的 rslist 启动方式会省略上图中的步骤 2 和步骤 3,这种方式只支持访问一个集群。


2. 登录认证

找到机器后,我们就可以登录认证了,关键信息是用户名和密码,MySQL 协议中(官方图片中展示了 Handshake 相关报文,实际流程中第 2 步结束后服务端需要回复一个 OK 或者 Error 报文给客户端)的交互流程如下图。



OBProxy 作为代理组件,要兼容 MySQL 行为,会有更多步骤。我们以 Java 程序连接数据库为例说明整个流程,在 Java 程序中,登录代码只有一行:


conn = (Connection) DriverManager.getConnection(URL, USER_NAME, PASSWORD);
复制代码


但背后的原理比较复杂,如下图所示。



在完成协议的登录步骤(第 8 步)后,MySQL 协议层的登录交互就结束了,但 JDBC 会发送一些初始化 SQL,也属于登录过程一部分,图中第 9 步表示该过程。初始化 SQL 有多条,内容如下:


set autocommit=1, sql_mode = concat(@@sql_mode,',STRICT_TRANS_TABLES')set names utf8SELECT @@max_allowed_packet,@@system_time_zone,@@time_zone,@@auto_increment_increment,@@tx_isolation AS tx_isolation,@@session.tx_read_only AS tx_read_onlyselect @@version_comment, @@version limit 1
复制代码


上面 SQL 执行完成后,Java 程序就可以发送业务 SQL 了。


3. 常见登录问题

现在的你已经明白了 OBProxy 的登录原理,就可以在登录失败时快速地定位和解决问题了。常见的问题及原因如下:


  • 用户名或密码错误,可以通过直连 OBServer 确定。

  • OCP 故障,无法拉取集群机器列表,可以通过 curl 命令访问 OCP 的 url 确定。

  • OBServer 执行获取机器列表或初始化 SQL 失败,需要日志排查定位。

  • 超过最大连接数设置或不在白名单中,后文将为你详细解读。


对于上述问题,你可以通过本系列第三篇文章提到的查看 obproxy_error.log 方式解决。


第二步:连接管理

登录成功后,客户端<->OBProxy<->OBServer 之间的网络连接便建立起来,此时 OBProxy 只是和其中一台 OBServer 建立了连接。随着 SQL 请求的到来,如果路由到新的 OBServer,会和新的 OBServer 建立连接。在此过程中涉及连接的映射关系、状态同步和连接功能特性,接下来一一解读。

1. 连接的映射关系

连接映射主要讲客户端连接和服务端连接之间的关系,我们先从一个客户端连接说起。当客户端和 OBProxy 建立一个连接后,OBProxy 会和后面 N 个 OBServer 建立连接,整个关系如下图所示。



可以看到,OBProxy 按需和两台 OBServer 建立了连接。这两个连接只属于这一个客户端连接,不会被其他客户端连接复用。连接映射的关键点就是需要用 id 标识出每一个连接并记录 id 之间的映射关系,我们可以将上图抽象成模型:App<-----[proxy_sessid1]---->OBProxy<---[server_sessid1]----->OBServer1<---[server_sessid3]----->OBServer3 这样我们就可以用 proxy_sessid 唯一标识 App 和 OBProxy 之间的连接,用 server_sessid 唯一标识 OBProxy 和 OBServer 之间的连接。当 SQL 执行错误、执行慢等情况出现,会将映射关系打印到日志中,这样就将 App 和 OBServer 关联起来,进行全链路问题定位。


2. 状态同步

一个客户端连接对应多个服务端连接,要保证执行结果的正确性,就要求多个服务端连接的 session 状态是一致的。那么,状态不同步会导致什么问题?我们举个反例,假设用户执行下面的 SQL 命令:


set autocommit=1;insert into t1 values(1);insert into t2 values(2);
复制代码


执行过程如下:


  • set autocommit=0 发给 OBServer1

  • insert into t1 values(1)发给 OBServer1

  • 进行连接切换,insert into t2 values(2)发给 OBServer2



对于第三条 SQL,OBProxy 和 OBServer2 的连接并未同步连接状态 autocommit=1,这样就可能导致第三条语句 insert into t2 并未提交事务。正确的步骤是 OBProxy 在给 OBServer2 发送 INSERT SQL 前,先同步 autocommit 变量的值。OBProxy 通过版本号机制解决了状态同步的问题,实现了 database、session variables、last_insert_id、ps prepare 语句的状态同步,保证功能的正确性。


3. 连接功能特性

和单机数据库不同,OBProxy 改变了连接的映射关系为 M:N,因此有些连接功能需要做额外处理。举个例子,用户通过 show processlist 查看连接数,此时他希望看到的是客户端和 OBProxy 之间的连接数,而不是 OBProxy 和 OBServer 之间的连接数。下面我们对常见的连接功能展开详细介绍。**连接粘性。**OBProxy 还未实现所有功能的状态同步,如事务状态、临时表状态、cursor 状态等。对于这些功能,OBProxy 只会将后续请求都发往状态开始的节点,这样就不需要进行状态同步,而缺点是无法充分发挥分布式系统的优势。因此,我们根据功能重要程度,逐步支持相关功能的分布式化。**show processlist 和 kill 命令配套使用。**show processlist 用于展示客户端和服务端之间的连接,对于 OBProxy,show processlist 只展示客户端和 OBProxy 之间的连接,不展示 OBProxy 和 OBServer 之间的连接。kill 命令用于杀死一个客户端连接,客户端连接关闭后,OBProxy 也会关闭对应的服务端连接。对于 OBProxy 的 kill 命令,需要先获取对应的 id,如下图的 Id 列内容(show proxysession 和 show processlist 功能类似,show proxysession 是 OBProxy 专属命令)。



**负载均衡影响。**因为 OBProxy 对 show processlist 和 kill 命令做了处理,所以 show processlist 和 kill 命令只有都发往同一台 OBProxy 才能正常工作。在公有云等环境,OBProxy 前面有负载均衡,负载均衡后面挂在多个 OBProxy 上,此时,如果执行 show prcesslist 和 kill 命令是两个不同的连接,负载均衡组件可能将请求发往不同的 OBProxy,在这种情况下,我们最好不要使用相关命令。介绍完 OBProxy 的连接映射关系、状态同步和连接功能特性的技术原理后,你可能会觉得如果用 HAProxy 等普通代理,很多地方就会简单很多,比如:


  • 用户连接普通代理后,普通代理只会和一台 OBServer 建立连接,连接映射是 1:1 的管理。

  • 普通代理的连接映射为 1:1 的关系,就不需要做状态同步,连接功能也不需要特殊处理,直接转发即可。


那么,为什么还推荐使用 OBProxy,它的优势在哪里呢?


  • 普通代理无法做高可用容灾,OBProxy 通过内部表、错误码等信息,会探测出 OBServer 状态(升级、宕机等),对 OBServer 节点进行拉黑和洗白。

  • 普通代理无法充分发挥 OceanBase 数据库的性能,连接建立后,普通代理无法将 SQL 发往其他 OBServer 节点,有时执行链路会更长。OBProxy 可以精准路由,并实现读写分离等特性。

  • 普通代理功能有限,无法适配 OceanBase 主备库、复制表、LDC 架构等特性。


以上就是使用 OBProxy 的步骤及其原理,如果想了解更深层次的连接原理知识,我们需要追溯到 TCP 协议。因为 OBProxy 的连接基于 TCP 协议,了解 TCP 协议的连接机制就能更透彻的掌握连接管理的功能及连接问题定位等知识。


延伸知识点 1:基于 TCP 协议的连接机制

TCP 参数

OBProxy 在代码层面设置了 TCP 的 no_delay 和 keepalive 属性,以保证低延迟、高可用等特性。


  • no_delay 属性通过禁用 TCP Nagle 算法解决延迟问题。在 Linux 的网络栈中默认启用 Nagle 算法,用于解决网络报文小分组出现,但会导致网络报文发送延迟。我们曾在生产环境中遇到 TCP 未禁用 Nagle 算法的情况,导致一条 SQL 发送耗时 40ms 左右,这是不满足业务要求的。

  • keepalive 属性用于故障探测。及时发现机器故障,将无效的连接关闭,是 TCP 层高可用一部分。


这两个属性可以通过 OBProxy 的配置项进行配置,推荐配置如下:


## 和OBServer的tcp连接设置sock_option_flag_out = 3;  --  这是个二进制位参数,bit 0 表示否是启用 no_delay,bit 1 表示是否启用 keepalive。3的二进制是 11,表示启用 no_delay 和 keepaliveserver_tcp_keepidle = 5;  --  启动keepalive探活前的idle时间,5秒。server_tcp_keepintvl = 5;  -- 两个keepalive探活包之间的时间间隔,5秒server_tcp_keepcnt = 2;  --  最多发送多少个keepalive包,2个。最长5+5*2=15秒发现dead_socket。
## 和客户端的tcp连接设置client_sock_option_flag_out = 3; -- 同上client_tcp_keepidle = 5; -- 同上client_tcp_keepintvl = 5; -- 同上client_tcp_keepcnt = 2; -- 同上
复制代码


我们在生产环境中已经验证了 no_delay 和 keepalive 属性,你可以放心使用。对于其他 TCP 参数,我们直接使用默认值,还没有调参的需求。


超时参数

我们在排查问题时,偶尔会遇到 TCP 连接断了的情况,但无法确定是客户端断连还是服务端断连。根据经验判断,往往是超时机制触发的。这里针对几种超时机制介绍一下全链路解决方案。

1.JDBC 超时

(1)socketTimeout

socketTimeout 是 Java socket 的超时时间,指的是业务程序和后端数据库之间 TCP 通信的超时时间,如果业务程序发送一个 MySQL Packet 后,超过 socketTimeout 的时间还没有从后端数据库收到 Response 报文,这时候 JDBC 会抛出异常,程序执行失败。socketTimeout 单位是毫秒,可以通过以下方式设置:


jdbc:mysql://$ip:$port/$database?socketTimeout=60000
复制代码

(2) connectTimeout

connectTimeout 是业务程序使用 jdbc 跟后端数据库建立 TCP 连接的超时时间,也相当于是在 connect 阶段的 socketTimeout,如果 TCP 连接超过这个时间没有建成功,jdbc 会抛出异常。connectTimeout 的单位是毫秒,可以通过以下方式设置:


jdbc:mysql://$ip:$port/$database?socketTimeout=60000&connecTimeout=5000
复制代码

(3)queryTimeout

queryTimeout 是业务程序执行 SQL 时,jdbc 设置的本地的超时时间。在业务调用 jdbc 的接口执行 SQL 时,jdbc 内部会开启这个超时机制,超过 quertTimeout 没有执行结束,jdbc 会在当前连接上发送一个 kill query 给后端数据库,并给上层业务抛一个 MySQLTimeoutException。queryTimeout 单位是秒,可以通过以下方式进行设置:


1、通过jdbc的Statement.setQueryTimeout接口来设置    int queryTimeout = 10;java.sql.Statement stmt = connection.CreateStatement();stmt.setQueryTimeout(queryTimeout);
复制代码


这里需要特别说明的是,如果要使用 queryTimeout,建议设置一个比 socketTimeout 小的值,否则会先触发网络超时从而导致断连接。但我们并不推荐使用 queryTimeout,你可以使用 OceanBase 数据库的 ob_query_timeout 属性。

(4)JDBC 超时实践

JDBC 重要的几个超时参数,均可以设置到连接池的 ConnectionProperties 中,或者 JDBC URL 上:



这些参数是我们根据蚂蚁内部 OLTP 业务总结而来,供你参考。


2.OceanBase 数据库超时

OceanBase 数据库有自己特定的超时参数,这些参数和 JDBC 的不同在于 OceanBase 触发超时后的行为是返回 ERROR 报文而不是断连接

(1) ob_query_timeout

ob_query_timeout 是 OBServer SQL 级别的超时参数,可以通过 hint 或者 session 变量设置,表示执行一个 SQL 超时时间。当业务的 SQL 在数据库执行时间超过 ob_query_timeout 设置的值后,OBServer 会返回一个 ERROR 包给客户端,错误码是 4012。 ob_query_timeout 的默认值是 10000000(10s), 单位是微秒。可以通过以下几种方式设置:


// Java程序示例
// 1、通过sql设置session变量或者global变量,设置完成之后,当前连接生效stmt.execute("set @@ob_query_timeout = 10000000");stmt.execute("set @@global.ob_query_timeout = 10000000");
// 2、通过在业务sql中添加hint设置,当前sql生效String sql = "/*+ QUERY_TIMEOUT(10000000)*/ select count(*) from XXX";stmt.execute(sql);
复制代码

(2)ob_trx_timeout

ob_trx_timeout 是 OBServer 的事务超时时间,是一个 session 变量,当一个事务执行超过 ob_trx_timeout 还没有结束时,OBServer 同样会返回 4012 的 ERROR 报文给客户端。如果超时发生时,事务还没有提交,那么 OBServer 会回滚这个事务;如果发生在 commit 阶段,由于事务状态未定,OBServer 不会主动回滚它。ob_trx_timeout 默认值是 100000000(100s),单位是微秒,可以通过以下方式设置:


// Java程序示例
// 通过sql设置session变量或者global变量,设置完成之后,当前连接生效stmt.execute("set @@ob_trx_timeout = 100000000");stmt.execute("set @@global.ob_trx_timeout = 100000000");
复制代码

(3)兼容 MySQL 超时

除了上述 OceanBase 数据库特有的超时参数,OceanBase 数据库还兼容了 MySQL 的超时参数,可以参考 MySQL 超时文档,MySQL 的超时参数会导致断连接,有三个参数。


  • **wait_timeout:**空闲 session 时间,默认为 8 个小时,超过该时间未发送任何 SQL 就会断连。

  • **net_read_timeout:**等待从连接上读取数据的超时时间。超时会导致连接关闭。

  • **net_write_timeout:**等待从连接上写入数据的超时时间。超时会导致连接关闭。


因为大部分超时机制触发后会直接关闭连接,并且不打印超时日志,所以不了解超时机制的话很难确定 TCP 断连的原因,但了解后通过观察执行时间或调整参数基本就能确定问题。OBProxy 作为代理层,并没有增加超时机制,不会让机制更加复杂,同时,OBProxy 会感知数据库超时参数的设置,让整体表现符合超时机制。


延伸知识点 2:常用网络工具

在很多时候我们需要了解系统网络行为,从而更好地解决问题。比如,在 OBProxy 日志中出现了 Connect Error,我们就可以通过 ping 命令进一步证实两台机器之间网络不通。接下来为你介绍一些常用的网络工具。

ping 和 telnet 命令

ping 和 telnet 命令简单好用,ping 命令可以确定两台机器之间的网络是否连通,并且可以了解网络之间的延迟,这对解决很多问题都非常有用。比如,延迟几十毫秒,判定为路由到了其他城市的机房,这在 oltp 业务中是不符合预期的。telnet 命令可以进一步确定机器端口是否在监听,从而发现服务程序未启动、程序 core 掉等问题。

抓包工具

当问题涉及多个模块的协议交互时,只分析日志无法获得更多有用信息,此时可以使用 tcpdump 工具获取网络真实情况。举个例子,如果在 OBProxy 日志看到数据库执行 SQL 慢,而在 OBServer 看到 SQL 执行很快,此时就会怀疑网络问题,只有根据 TCP 报文才能确定是否是网络问题。标准部署中,OBProxy 的端口是 2881,OBServer 的端口是 2883,如果抓 OBProxy 和 OBServer 的交互报文,可以在 OBProxy 的机器执行:


sudo tcpdump -i any port 2881 -w xxx.cap
复制代码


TCP 报文就会保存在 xxx.cap 中。得到该文件拷贝到本地机器使用 wireshark 软件分析即可。除了上面介绍的命令,还有 ifconfig、ss、lsof、curl 等命令都非常有用,网络上资料很多,此处就不作更多介绍了。总之,在分布式系统中,网络问题很常见。了解背后的原因,需要对操作系统、数据库系统、网络工具都非常熟悉,因此增加了判断难度。上述内容对这些知识点都做了详细介绍,帮助你更好地了解分布式数据库系统。


延伸知识点 3:连接管理的常见问题

虽然连接管理的知识点很多,但当出现连接问题后,客户端报错基本都是 Communications link failure。这里关键点就是通过“关键信息”将 TCP 的两端联系起来,获得更多有用信息去进一步分析。如“关键信息”是 SQL 和时间点,那么就可以去 obproxy_error.log 中去查看,找到日志记录后,可以根据本系列第三篇文章介绍的内容去进一步排查。这里我们举两个例子说明。例子 1 客户端报错:


--- Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failureThe last packet successfully received from the server was 70,810 milliseconds ago. The last packet sent successfully to the server was 5,005 milliseconds ago.
复制代码


分析报错信息,可以看到客户端上次从服务端收到请求是 70810ms 之前,客户端上次往服务端发送数据是 5005ms(5s)之前。这是在执行 SQL 过程中出现了问题,一般是 Java 本身设置了 sockettimeout=5s 导致的,这可以向应用开发者确认。例子 2 客户端报错:


--- Cause: com.alipay.oceanbase.obproxy.druid.pool.GetConnectionTimeoutException: get connection timeout, maxWait=500ms, maxCount=10, currentCount=11,
复制代码


可以看到这是个 Druid 连接池的报错,获取连接超时。此时 SQL 还没有执行,但从连接池已经拿不到连接了,问题原因就比较复杂。一般原因有:


  • 连接池本身配置连接数太少不够用,参考 maxCount 的值,一般调大可以解决。

  • 有慢 SQL 导致连接没有及时归还,这种问题就需要排查哪些 SQL 执行慢。


可见报错只是一种表象,很难一下就确定根本原因,需要收集更多信息去深入排查。


总结

连接管理涉及的综合知识点多,涉及的模块(包括驱动、OBProxy 和 OBServer)也多,因此变得复杂,但这也是微服务、分布式数据库的共同“痛点”。本文我们从用户登录讲起,分别介绍了连接管理、网络技术和常见问题,将相关原理和实践介绍给你。OBProxy 经历多年实践,解决了很多连接管理的问题,n 也欢迎大家学习交流。


课后互动

上期互动答案

小明想查看所有经过 OBProxy 的 SQL,请问有什么办法?答:obproxy_digest.log 是审计日志,记录了所有执行时间大于 query_digest_time_threshold 的 SQL,因此可以修改 query_digest_time_threshold 的值为 1us,就可以在 obproxy_digest.log 中看到所有经过 OBProxy 的日志了。


本期互动问题

JDBC 和 OBProxy 建立了一个连接,晚上 OBProxy 被 kill 掉重启,请问此时 JDBC 会抛出异常吗?欢迎你在问答区发帖讨论,下篇文章揭晓答案。

用户头像

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

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

评论

发布
暂无评论
高性能数据访问中间件 OBProxy(四):一文讲透连接管理_OceanBase 数据库_InfoQ写作社区