揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)

Posted DBAplus社群

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)相关的知识,希望对你有一定的参考价值。


本文根据DBAplus社群第151期线上分享整理而成,文末还有好书送哦~


讲师介绍
揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)

张永伦

京东金融运维部高级软件工程师


曾在传统行业工作多年,从事基础软件开发工作。后投身互联网,在京东金融开始了爬虫生涯,感叹互联网数据量之大,但心中仍对偏底层的软件感兴趣。今年有幸加入到Sharding-Sphere,能够做自己感兴趣的事情,希望以后多做些工作,提升自己,回报社会。


分享大纲

1、Sharding-Proxy简介

  • Sharding-Proxy概览

  • Sharding-Proxy架构

2、Sharding-Proxy功能细节

  • PreparedStatement功能实现

  • Hikari连接池配置优化

  • 结果归并优化

  • Proxy的两种模式

3、小结


大家好,我今天想跟大家分享的是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更加友好。


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

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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


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


2Sharding-Proxy架构


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


  • 前端(Frontend)负责与客户端进行网络通信,采用的是基于NIO的客户端/服务器框架,在Windows和Mac操作系统下采用NIO模型,Linux系统自动适配为Epoll模型,在通信的过程中完成对MySQL协议的编解码;

  • 核心组件(Core-module)得到解码的MySQL命令后,开始调用Sharding-Core对SQL进行解析、改写、路由、归并等核心功能;

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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


二、Sharding-Proxy功能细节


1PreparedStatement功能实现


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


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


示例代码如下:


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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

交互过程变成了这样:


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


初看这是一个正确的流程,第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

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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


从第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开始的时候缓存参数类型。


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


2Hikari连接池配置优化


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测试:


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


connections=((core_count*2)+effective_spindle_count)


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


3结果归并优化


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


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


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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)

(图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:


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)

(图2)


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


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


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


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


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


  • SO_RCVBUF/SO_SNDBUF是TCP缓存;

  • ChannelOutboundBuffer是Netty写缓存。


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


当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来控制写缓冲区:


  • 当Buffer大小超过高水位线,我们就控制Netty不让再往里面写,当Buffer大小低于低水位线的时候,才允许写入;

  • 当ChannelOutboundBuffer满时,Proxy的SO_RCVBUF被打满,通知MySQL停止发送数据。


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


4Proxy的两种模式


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


  • MEMORY_STRICTLY:Proxy会保持一个数据库中所有被路由到的表的连接,这种方式的好处是利用流式ResultSet来节省内存。

  • 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的开源项目中:

  • https://github.com/sharding-sphere/sharding-sphere/

  • 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


彩蛋来了

在本文微信订阅号(dbaplus)评论区留下足以引起共鸣的真知灼见,小编将在本文发布后的隔天中午12点选出留言最精彩的一位读者,送出以下书籍一本~

注:同一个月里,已获赠者将不可重复拿书。


揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)

特别鸣谢图灵社区为活动提供图书赞助。

揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)


想了解关于Sharding-Sphere的更多细节

不妨来“2018 DAMS中国数据资产管理峰会”

听听京东金融数据研发负责人张亮老师的解析

扫描以下二维码,查看峰会详情及报名↓

以上是关于揭秘Sharding-Proxy——面向DBA的数据库中间层(有彩蛋)的主要内容,如果未能解决你的问题,请参考以下文章

Oracle内核系列3-揭秘ASM磁盘头信息

揭秘!10+位DBA大神测评华为云DDS增强版实感

揭秘Oracle数据库truncate原理

Sharding-Proxy 源码分析

Sharding-Proxy核心原理与性能优化

data block address