引言
上篇内容我讲到 OBProxy 的问题排查,将你在使用 OBProxy 时可能遇到的问题一一分析,并给出经过实践验证的解决方案。从本篇开始,我将介绍 OBProxy 在OceanBase分布式架构中的作用和原理,帮助你更透彻地了解OBProxy,实现“好用”和“用好”。同时,OBProxy 在上百家企业的持续运行,我积累了大量的工程实践经验,也将遇到的问题作为案例,伴随 OBProxy 的原理讲解分享给你,供你参考。
本篇从 OBProxy 的重要特性连接管理讲起,其中,对连接映射关系、session状态同步等内容,你可以将其与单机数据库比较,体会分布式系统和单机系统的异同。
OBProxy的连接管理步骤及原理
在正式讲解之前,我先介绍一下本篇内容的技术背景。我们知道,OBProxy 为用户提供了数据库接入和路由功能,用户连接 OBProxy 就可以正常使用OceanBase数据库。用户在使用数据库功能时,OBProxy和OBServer进行交互,且交互流程对用户透明,连接管理就是该交互过程中的关键点之一。
OBProxy的连接管理有三个特性:
-
代理特性:OBProxy 既是客户端,也是服务端,还需要保证交互行为符合MySQL协议规范。
-
功能特性:OBProxy实现了很多的连接功能特性,如访问不同集群、不同租户,再如支持主备库、分布式下的ps功能,以及兼容kill、show processlist等命令。
-
高可用特性: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 utf8
SELECT @@max_allowed_packet,@@system_time_zone,@@time_zone,@@auto_increment_increment,@@tx_isolation AS tx_isolation,@@session.tx_read_only AS tx_read_only
select @@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 和 keepalive
server_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 failure
The 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经历多年实践,解决了很多连接管理的问题,也欢迎大家学习交流。