揭秘Sharding-Proxy——面向DBA的数据库中间层

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介:

大家好,我今天想跟大家分享的是Sharding-Sphere的第二个产品Sharding-Proxy。

在上个月亮相的Sharding-Sphere 3.0.0.M1中首次发布了Sharding-Proxy,希望这次分享能够通过几个优化实践,帮助大家管中窥豹,从几个关键细节想象出Sharding-Proxy的全貌。至于更详细的MySQL协议、IO模型、Netty等议题,以后有机会再和大家专题分享。

一、Sharding-Proxy简介

1Sharding-Proxy概览

Sharding-Proxy定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。目前先提供MySQL版本,它可以使用任何兼容MySQL协议的访问客户端操作数据(如:MySQLCommandClient、MySQLWorkbench等),对DBA更加友好。

d47e62d2b349aca45e42305ed6714efbe5ed61d9 对应用程序完全透明,可直接当做MySQL使用;

d47e62d2b349aca45e42305ed6714efbe5ed61d9适用于任何兼容MySQL协议的客户端。

76dbde3d4b1693d9b9255b3c357b5338379483f7

与其他两个产品(Sharding-JDBC、Sharding-Sidecar)的对比:

c912528c7a93fe669fd3322e9ee962b7b9bbe3bd

它们既可以独立使用,也可以相互配合,以不同的架构模型、不同的切入点,实现相同的功能目标。而其核心功能,如数据分片、读写分离、柔性事务等,都是同一套实现代码。

举个例子,对于仅使用Java为开发技术栈的场景,Sharding-JDBC对各种Java的ORM框架支持度非常高,开发人员可以非常便利地将数据分片能力引入到现有的系统中,并将其部署至线上环境运行,而DBA就可以通过部署一个Sharding-Proxy实例,对数据进行查询和管理。

2Sharding-Proxy架构

a130df724b10351f97d8de3a9a75edfb9f5a6101

整个架构可以分为前端、后端和核心组件三部分来看:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 前端(Frontend)负责与客户端进行网络通信,采用的是基于NIO的客户端/服务器框架,在Windows和Mac操作系统下采用NIO模型,Linux系统自动适配为Epoll模型,在通信的过程中完成对MySQL协议的编解码;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 核心组件(Core-module)得到解码的MySQL命令后,开始调用Sharding-Core对SQL进行解析、改写、路由、归并等核心功能;

d47e62d2b349aca45e42305ed6714efbe5ed61d9后端(Backend)与真实数据库的交互暂时借助基于BIO的Hikari连接池。BIO的方式在数据库集群规模很大,或者一主多从的情况下,性能会有所下降。所以未来我们还会提供NIO的方式连接真实数据库。

bda21306c9bf1f73a82a29a05efaf788b7c19283

这种方式下Proxy的吞吐量将得到极大提高,能够有效应对大规模数据库集群。

二、Sharding-Proxy功能细节

1PreparedStatement功能实现

我在Sharding-Sphere的第一个任务就是实现Proxy的PreparedStatement功能。据说这是一个高大上的功能,能够预编译SQL提高查询速度和防止SQL注入攻击等。一次服务端预编译,多次查询,降低SQL编译开销,提升了效率,听起来没毛病。然而在做完之后却发现被坑了,SQL执行效率不但没有提高,甚至用肉眼都能看出来比原始的Statement还要慢。

先抛开Proxy不说,我们通过wireshark抓包看看运行PreparedStatement时MySQL协议是如何交互的。

示例代码如下:

d0ceb298bee8cb7308260b2937adc1b86620955f

代码很容易理解,使用PreparedStatement执行两次查询操作,每次都把参数user_id设置为10。分析抓到的包,JDBC和MySQL之间的协议消息交互如下:

9c78fea98ee5c72e544226d717011ddbd7336d11

JDBC向MySQL进行了两次查询(Query),MySQL返回给JDBC两次结果(Response),第一条消息不是我们期望的PreparedStatement,SELECT里面也没有问号,这就说明prepare没有生效,至少对MySQL服务来说没有生效。

对于这个问题,我想大家心里都有数,是因为JDBC的url没有设置参数useServerPrepStmts=true,这个参数的作用是让MySQL服务进行prepare。没有这个参数就是让JDBC进行prepare,MySQL完全感知不到,是没有什么意义的。接下来我们在url中加上这个参数:

jdbc:mysql://127.0.0.1:3306/demo_ds?useServerPrepStmts=true

交互过程变成了这样:

58c8cd113adc96eaf433ce43108658f1962968fd

初看这是一个正确的流程,第1条消息是PreparedStatement,SELECT里也带问号了,通知MySQL对SQL进行预编译;第2条消息MySQL告诉JDBC准备成功;第3条消息JDBC把参数设置为10;第4条消息MySQL返回查询结果。然而到了第5条,JDBC怎么又发了一遍PreparedStatement?

预期应该是以后的每条查询都只是通过ExecuteStatement传参数的值,这样才能达到一次预编译多次运行的效果。

如果每次都“预编译”,那就相当于没有预编译,而且相对于普通查询,还多了两次消息传递的开销:Response(prepareok)和ExecuteStatement(parameter=10)。看来性能的问题就是出在这里了。

像这样使用PreparedStatement还不如不用,一定是哪里搞错了,于是我开始阅读JDBC源代码,终于发现了另一个需要设置的参数——cachePrepStmts。我们加上这个参数看看会不会发生奇迹:

jdbc:mysql://127.0.0.1:3306/demo_ds?useServerPrepStmts=true&cachePrepStmts=true

果然得到了我们预期的消息流程,而且经过测试,速度也比普通查询快了:

96543aec849279b5bb4a7209ee901c5773b6149e

从第5条消息开始,每次查询只传参数值就可以了,终于达到了一次编译多次运行的效果,MySQL的效率得到了提高。而且由于ExecuteStatement只传了参数的值,消息长度上比完整的SQL短了很多,网络IO的效率也得到了提升。

原来cachePrepStmts=true这个参数的意思是告诉JDBC缓存需要prepare的SQL,比如“SELECT*FROMt_orderWHEREuser_id=?”,运行过一次后,下次再运行就跳过PreparedStatement,直接用ExecuteStatement设置参数值。

明白原理后,就知道该怎么优化Proxy了。Proxy采用的是Hikari数据库连接池,在初始化的时候为其设置上面的两个参数:

config.addDataSourceProperty("useServerPrepStmts","true");

config.addDataSourceProperty("cachePrepStmts","true");

这样就保证了Proxy和MySQL服务之间的性能。那么Proxy和Client之间的性能如何保证呢?

Proxy在收到Client的PreparedStatement的时候,并不会把这条消息转发给MySQL,因为SQL里的分片键是问号,Proxy不知道该路由到哪个真实数据库。Proxy收到这条消息后只是缓存了SQL,存储在一个StatementId到SQL的Map里面,等收到ExecuteStatement的时候才真正请求数据库。

这个逻辑在优化前是没问题的,因为每一次查询都是一个新的PreparedStatement流程,ExecuteStatement会把参数类型和参数值告诉客户端。

加上两个参数后,消息内容发生了变化,ExecuteStatement在发送第二次的时候,消息体里只有参数值而没有参数类型,Proxy不知道类型就不能正确的取出值。所以Proxy需要做的优化就是在PreparedStatement开始的时候缓存参数类型。

0fdc93f4661cfadc46bf9d81643827929a6d32fb

完成以上优化后,Client-Proxy和Proxy-MySQL两侧的消息交互都变成了最后这张图的流程,从第9步开始高效查询。

Hikari连接池配置优化

Proxy在初始化的时候,会为每一个真实数据库配置一个Hikari连接池。根据分片规则,SQL被路由到某些真实库,通过Hikari连接得到执行结果,最后Proxy对结果进行归并返回给客户端。那么,数据库连接池到底该设置多大?对于这个众说纷纭的话题,今天该有一个定论了。

你会惊喜的发现,这个问题不是设置“多大”,反而是应该设置“多小”!如果我说执行一个任务,串行比并行更快,是不是有点反直觉?

即使是单核CPU的计算机也能“同时”支持数百个线程。但我们都应该知道这只不过是操作系统用“时间片”玩的一个小花招。事实上,一个CPU核心同一时刻只能执行一个线程,然后操作系统切换上下文,CPU执行另一个线程,如此往复。

一个CPU进行计算的基本规律是,顺序执行任务A和任务B永远比通过时间片“同时”执行A和B要快。一旦线程的数量超过了CPU核心的数量,再增加线程数就只会更慢,而不是更快。一个对Oracle的测试验证了这个观点。

参考链接:http://www.dailymotion.com/video/x2s8uec

测试者把连接池的大小从2048逐渐降低到96,TPS从16163上升到20702,平响从110ms下降到3ms。

当然,也不是那么简单地让连接数等于CPU数就行了,还要考虑网络IO和磁盘IO的影响。当发生IO时,线程被阻塞,此时操作系统可以将那个空闲的CPU核心用于服务其他线程。所以,由于线程总是在I/O上阻塞,我们可以让线程(连接)数比CPU核心多一些,这样能够在同样的时间内完成更多的工作。到底应该多多少呢?PostgreSQL进行了一个benchmark测试:

6b1e573b51caca0b5873c9438d750f7012657d6d

TPS的增长速度从50个连接的时候开始变慢。根据这个结果,PostgreSQL给出了如下公式:

connections=((core_count*2)+effective_spindle_count)

连接数=((核心数*2)+磁盘数)。即使是32核的机器,60多个连接也就够用了。所以,小伙伴们在配置Proxy数据源的时候,不要动不动就写上几百个连接,不仅浪费资源,还会拖慢速度。

结果归并优化

目前Proxy访问真实数据库使用的是JDBC,很快Netty+MySQLProtocol异步访问方式也会上线,两者会并存,由用户选择用哪种方法访问。

在Proxy中使用JDBC的ResultSet会对内存造成非常大的压力。Proxy前端对应m个client,后端又对应n个真实数据库,后端把数据传递给前端client的过程中,数据都需要经过Proxy的内存。如果数据在Proxy内存中呆的时间长了,那么内存就可能被打满,造成服务不可用的后果。所以,ResultSet内存效率可以从两个方向优化,一个是减少数据在Proxy中的停留时间,另一个是限流。

我们先看看优化前Proxy的内存表现。使用5个客户端连接Proxy,每个客户端查询出15万条数据。结果如下图1:

6e2e2115c7e222d71c2d1466484b56d7e650b8d9

(图1)

可以看到,Proxy的内存在一直增长,即时GC也回收不掉的。这是因为ResultSet会阻塞住next(),直到查询回来的所有数据都保存到内存中。这是ResultSet默认提取数据的方式,大量占用内存。

那么,有没有一种方式,让ResultSet收到一条数据就可以立即消费呢?在Connector/J文档中有这样一句话:

If you are working with ResultSets that have a large number of rows or large values and cannot allocate heap space in your JVM for the memory required , you can tell the driver to stream the results back one row at a time.

如果你使用ResultSet遇到查询结果太多,以致堆内存都装不下的情况,你可以指示驱动使用流式结果集,一次返回一条数据。

文档参考链接:

https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

激活这个功能只需在创建Statement实例的时候设置一个参数:

stmt.setFetchSize(Integer.MIN_VALUE);

这样就完成了。这样Proxy就可以在查询指令后立即通过next()消费数据了,数据也可以在下次GC的时候被清理掉。当然,Proxy在对结果做归并的时候,也需要优化成即时归并,而不再是把所有数据都取出来再进行归并,Sharding-Core提供即时归并的接口,这里就不详细介绍了。下面看看优化后的效果,如下图2:

a83a5918d329dd5054d18b9a2e0cad1e26db68e9

(图2)

数据在内存中停留时间缩短,每次GC都回收掉了数据,内存效率大幅提升。看到这里,好像已经大功告成了,然而水还很深,请大家穿上潜水服继续跟我探索。图2是在最理想的情况产生的,即Client从Proxy消费数据的速度,大于等于Proxy从MySQL消费数据的速度。

c748b60bdb4d2cac4948cfaf75106dc81a5502aa

如果Client由于某种原因消费变慢了,或者干脆不消费了,会发生什么呢?通过测试发现,内存使用量直线拉升,比图1更强劲,最后将内存耗尽,Proxy被KO。

下面我们就先搞清楚为什么会发生这种现象,然后介绍对ResultSet的第2个优化:限流。

下图加上了几个主要的缓存:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 SO_RCVBUF/SO_SNDBUF是TCP缓存;

d47e62d2b349aca45e42305ed6714efbe5ed61d9ChannelOutboundBuffer是Netty写缓存。

69482ec3b48e6cfb461e1e07c16fac655cd077fe

当Client阻塞的时候,它的SO_RCVBUF会被瞬间打满,然后通过滑动窗口机制通知Proxy不要再发送数据了,同时Proxy的SO_SNDBUF也会瞬间被Netty打满。

Proxy的SO_SNDBUF满了之后,Netty的ChannelOutboundBuffer就会像一个无底洞一样,吞掉所有MySQL发来的数据,因为在默认情况下ChannelOutboundBuffer是无界的。

由于有用户(Netty)在消费,所以Proxy的SO_RCVBUF一直有空间,导致MySQL会一直发送数据,而Netty则不停的把数据存到ChannelOutboundBuffer,直到内存耗尽。

搞清原理之后就知道,我们的目标就是当Client阻塞的时候,Proxy不再接收MySQL的数据。

Netty通过水位参数WRITE_BUFFER_WATER_MARK来控制写缓冲区:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 当Buffer大小超过高水位线,我们就控制Netty不让再往里面写,当Buffer大小低于低水位线的时候,才允许写入;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 当ChannelOutboundBuffer满时,Proxy的SO_RCVBUF被打满,通知MySQL停止发送数据。

所以,在这种情况下,Proxy所消耗的内存只是ChannelOutboundBuffer高水位线的大小。

Proxy的两种模式

在即将发布的Sharding-Sphere3.0.0.M2版本中,Proxy会加入两种代理模式的配置:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 MEMORY_STRICTLY:Proxy会保持一个数据库中所有被路由到的表的连接,这种方式的好处是利用流式ResultSet来节省内存。
d47e62d2b349aca45e42305ed6714efbe5ed61d9 CONNECTION_STRICTLY:代理在取出ResultSet中的所有数据后会释放连接,同时,内存的消耗将会增加。

简单可以理解为,如果你想消耗更小的内存,就用MEMORY_STRICTLY模式;如果你想消耗更少的连接,就用CONNECTION_STRICTLY模式。

MEMORY_STRICTLY的原理其实就是我们前面介绍的内容,优点已经说过了。它带来的一个副作用是,流式ResultSet需要保持对数据库的连接,必须与所有路由到的真实表成功建立连接后,才能够进行即时归并,进而返回结果给客户端。

假设数据库设置max_user_connections=80,而该库被路由到的表是100个,那么无论如何也不可能同时建立100个连接,也就无法归并返回结果。

CONNECTION_STRICTLY就是为了解决以上问题而存在的。不使用流式ResultSet,内存消耗增加。但该模式不需要保持与数据库的连接,每次取出ResultSet内的全量数据后即可释放连接。

还是刚才的例子max_user_connections=80,而该库被路由到的表是100个。Proxy会先建立80个连接查询数据,另外20个连接请求被缓存在连接池队列中,随着前面查询的完成,这20个请求会陆续成功连接数据库。

如果你对这个配置还感到迷惑,那么记住一句话,只有当max_user_connections小于该库可能被路由到的最大表数量时,才使用CONNECTION_STRICTLY。

三、小结

Sharding-Sphere自2016年开源以来不断精进和发展,被越来越多企业和个人所使用,同时也为我们提供了重要的成功案例。未来,我们将不断优化当前的特性,陆续推出大家关注的柔性事务、数据治理等更多新特性。如果大家有什么想法、意见和建议,也欢迎留言与我们交流,更欢迎加入到Sharding-Sphere的开源项目中:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 https://github.com/sharding-sphere/sharding-sphere/
d47e62d2b349aca45e42305ed6714efbe5ed61d9 https://gitee.com/sharding-sphere/sharding-sphere/
Q&A

Q1:Sidecar是干什么的?

A1:Sharding-Sidecar是Sharding-Sphere的第三个产品,目前仍在规划中。定位为Kubernetes或Mesos的云原生数据库代理,以DaemonSet的形式代理所有对数据库的访问。

Q2:“问如果你对这个配置还感到迷惑,那么记住一句话,只有当max_user_connections小于该库可能被路由到的最大表数量时,才使用CONNECTION_STRICTLY。”这句话是不是说反了?

A2:CONNECTION_STRICTLY就是为了省连接的。max_user_connections小,所以用CONNECTION_STRICTLY模式。

Q3:stmt.setFetchSize(custom_size);设置的场景使用类似Mybatis之类的框架查询大量数据到内存,一般就是放到一个list中,然后处理。内存还是会占用。

A3:这种情况占的是客户端的内存,不会影响Proxy。

Q4:问出现查询大数据量到内存的场景,是不是只能使用原生JDBC,查一批处理一批,不放内存?

A4:Mybatis是在客户端控制的,不影响Proxy。

直播回放链接

https://m.qlchat.com/topic/details?topicId=2000001395952730&minimal=1


原文发布时间为:2018-06-20

本文作者:张永伦

本文来自云栖社区合作伙伴“DBAplus社群”,了解相关信息可以关注“DBAplus社群”。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
数据库 SQL Cloud Native
DBA福利:云原生分布式数据库 PolarDB-X 操作指南
【6月更文挑战第2天】简述如何使用PXD工具一键安装PolarDB-X操作
72074 10
|
4月前
|
缓存 NoSQL 数据库
救命!DBA找上门了,数据库cpu飙到60%
这篇文章讲述了作者如何通过优化数据库查询和引入二级缓存架构,成功解决了数据库CPU使用率规律性飙升的问题,提高了系统稳定性。
救命!DBA找上门了,数据库cpu飙到60%
|
7月前
|
SQL BI 数据库
数据库慢查询:DBA的挑战与应对策略
NineData的慢查询分析功能。它可以自动采集并记录数据库中的所有慢查询,比较亮眼的是它通过对每一条慢查询进行性能诊断,最终提供优化建议,包含添加或修改索引、调整表结构等,同时还可以根据业务类型配置SQL开发规范,配置完成后,系统还会基于这些规范诊断慢SQL。
1062 2
数据库慢查询:DBA的挑战与应对策略
|
7月前
|
存储 SQL 关系型数据库
探索数据库的世界:DB、DBMS、DBA、DBS的全面介绍
探索数据库的世界:DB、DBMS、DBA、DBS的全面介绍
977 0
|
数据库 数据库管理 OceanBase
OceanBase数据库中,角色的权限是由数据库管理员(DBA)设置的
OceanBase数据库中,角色的权限是由数据库管理员(DBA)设置的
164 1
|
运维 关系型数据库 MySQL
MySQL DBA的必备参考,两位数据库资深专家呕心沥血之作
互联网发展至今,开源软件已经深入人心,并且受到广泛的支持和响应,很多公司在使用开源软件的同时也输出了一些好的开源产品。MySQL 作为当今世界.上最受欢迎的开源数据库产品之一,在很多互联网企业里起到了不可或缺的作用。由于MySQL的诸多特性,比如开源免费、灵活、轻量简单且越来越多的企业开始使用MySQL,在业界诞生了一大批相关从业者,他们研究MySQL的原理,探讨MySQL的架构,完善MySQL的运维,丰富MySQL的工具,促进MySQL的发展,我们称这些人为MySQL DBA,而本人也是其中之一,深感荣幸。
|
运维 关系型数据库 MySQL
GitHub无抗手!MySQL DBA攻坚指南一出,阿里数据库专家都解脱了
大家可能并不觉得,数据库其实非常重要,每个业内巨头,每个成熟的互联网产品都有多个数据库系统,能保证大量并发场景下不出错,并非易事。尤其是银行、电商、电信、电力、航空等实时交易重要的环境中,可靠的数据库是重中之重,稳定压倒一切。
|
SQL 人工智能 运维
ChatGPT4高分通过数据库系统工程师(DBA)认证
ChatGPT 已经通过了美国MBA、法律和医学考试,**ChatGPT 又通过了数据库系统工程师考试。** 这次考试使用了2021年的真实试卷,上午的笔试题全部为选择题,总分75分(45分通过)。
457 2
ChatGPT4高分通过数据库系统工程师(DBA)认证
阿里大牛撰写"星耀级"MySQL DBA工作笔记!颠覆我对数据库的认知
MySQL是开源数据库方向的典型代表,它拥有成熟的生态体系,同时在可靠性、性能、易用性方面表现出色,它的发展历程见证了互联网的兴衰与成长。 在 DeveloperWeek 上曾发起一个调查,超过 3/5 的受访者使用 SQL,其中MySQL 以 38.9% 的使用率高居榜首,其后依次是 MongoDB(24.6%)、PostgreSQL(17.4%)、Redis(8.4%)和 Cassandra(3.0%)。毫无疑问,以MySQL为主的开源技术生态正变得越来越流行。同样的,对我们掌握MySQL也迫在眉睫。那么,接下来,就让小轩带领大家来深入了解MySQL吧!
|
存储 运维 Oracle
新数据库时代,DBA 发展之路该如何选择
新数据库时代,DBA 发展之路该如何选择
408 0
新数据库时代,DBA 发展之路该如何选择
下一篇
DataWorks