你们好,拓海(https://github.com/tuohai666)今天为你们分享Sharding-Sphere推出的重磅产品:Sharding-Proxy!在以前闪亮登场的Sharding-Sphere 3.0.0.M1中,首次发布了Sharding-Proxy,这个新产品到底表现如何呢?此次但愿经过几个优化实践,让你们管中窥豹,从几个细节的点可以想象出Sharding-Proxy的全貌。更详细的MySQL协议、IO模型、Netty等议题,之后有机会再和你们专题分享。html
Sharding-Proxy是Sharding-Sphere的第二个产品。它定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。目前先提供MySQL版本,它可使用任何兼容MySQL协议的访问客户端(如:MySQL Command Client, MySQL Workbench等)操做数据,对DBA更加友好。前端
与其余两个产品(Sharding-JDBC、Sharding-Sidecar)对比:mysql
|
Sharding-JDBCgit |
Sharding-Proxygithub |
Sharding-Sidecarsql |
数据库数据库 |
任意后端 |
MySQL缓存 |
MySQL服务器 |
链接消耗数 |
高 |
低 |
高 |
异构语言 |
仅Java |
任意 |
任意 |
性能 |
损耗低 |
损耗略高 |
损耗低 |
无中心化 |
是 |
否 |
是 |
静态入口 |
无 |
有 |
无 |
它们既能够独立使用,也能够相互配合,以不一样的架构模型、不一样的切入点,实现相同的功能目标,而其核心功能,如数据分片、读写分离、柔性事务等,都是同一套实现代码。举个例子,对于仅使用 Java 为开发技术栈的场景,Sharding-JDBC 对各类 Java 的 ORM 框架支持度很是高,开发人员能够很是便利地将数据分片能力引入到现有的系统中,并将其部署至线上环境运行,而 DBA 能够经过部署一个 Sharding-Proxy 实例,对数据进行查询和管理。
整个架构能够分为前端、后端和核心组件三部分来看。前端(Frontend)负责与客户端进行网络通讯,采用的是基于NIO的客户端/服务器框架,在Windows和Mac操做系统下采用NIO 模型,Linux系统自动适配为Epoll模型。通讯的过程当中完成对MySQL协议的编解码。核心组件(Core-module)获得解码的MySQL命令后,开始调用Sharding-Core对SQL进行解析、改写、路由、归并等核心功能。后端(Backend)与真实数据库的交互暂时借助基于BIO的Hikari链接池。BIO的方式在数据库集群规模很大,或者一主多从的状况下,性能会有所降低。因此将来咱们还会提供NIO的方式链接真实数据库。
这种方式下Proxy的吞吐量将获得极大提升,可以有效应对大规模数据库集群。
我在Sharding-Sphere的第一个任务就是实现Proxy的PreparedStatement功能,听说这是一个高大上的功能,可以预编译SQL提升查询速度和防止SQL注入攻击什么的。一次服务端预编译,屡次查询,下降SQL编译开销,提高了效率,听起来没毛病。然而在作完以后却发现被坑,SQL执行效率不但没有提升,甚至用肉眼都能看出来比原始的Statement还要慢。
先抛开Proxy不说,咱们经过wireshark抓包看看运行PreparedStatement的时候MySQL协议是如何交互的。
示例代码以下:
1 for (int i = 0; i < 2; i++) { 2 String sql = "SELECT * FROM t_order WHERE user_id=?"; 3 try ( 4 Connection connection = dataSource.getConnection(); 5 PreparedStatement preparedStatement = connection.prepareStatement(sql)) { 6 preparedStatement.setInt(1, 10); 7 ResultSet resultSet = preparedStatement.executeQuery(); 8 while (resultSet.next()) { 9 ... 10 } 11 } 12 }
代码很容易理解,使用PreparedStatement执行两次查询操做,每次都把参数user_id设置为10。分析抓到的包,JDBC和MySQL之间的协议消息交互以下:
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
交互过程变成了这样:
初看这是一个正确的流程,第1条消息是PreparedStatement,SELECT里也带问号了,通知MySQL对SQL进行预编译。第2条消息MySQL告诉JDBC准备成功。第3条消息JDBC把参数设置为10。第4条消息MySQL返回查询结果。然而到了第5条,JDBC怎么又发了一遍PreparedStatement?预期应该是之后的每条查询都只是经过ExecuteStatement传参数的值,这样才能达到一次预编译屡次运行的效果。若是每次都“预编译”,那就至关于没有预编译,并且相对于普通查询,还多了两次消息传递的开销:Response(prepare ok)和ExecuteStatement(parameter = 10)。看来性能的问题就是出在这里了。
像这样使用PreparedStatement还不如不用,必定是哪里搞错了,因而拓海开始阅读JDBC源代码,终于发现了另外一个须要设置的参数:cachePrepStmts。咱们加上这个参数看看会不会发生奇迹:
jdbc:mysql://127.0.0.1:3306/demo_ds?useServerPrepStmts=true&cachePrepStmts=true
果真获得了咱们预期的消息流程,并且通过测试,速度也比普通查询快了:
从第5条消息开始,每次查询只传参数值就能够了,终于达到了一次编译屡次运行的效果,MySQL的效率获得了提升。并且因为ExecuteStatement只传了参数的值,消息长度上比完整的SQL短了不少,网络IO的效率也获得了提高。原来cachePrepStmts=true这个参数的意思是告诉JDBC缓存须要prepare的SQL,好比"SELECT * FROM t_order WHERE user_id=?",运行过一次后,下次再运行就跳过PreparedStatement,直接用ExecuteStatement设置参数值。
明白原理后,就知道该怎么优化Proxy了。Proxy采用的是Hikari数据库链接池,在初始化的时候为其设置上面的两个参数:
1 config.addDataSourceProperty("useServerPrepStmts", "true"); 2 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开始的时候缓存参数类型。
完成以上优化后,Client-Proxy和Proxy-MySQL两侧的消息交互都变成了最后这张图的流程,从第9步开始高效查询。
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测试:
TPS的增加速度从50个链接的时候开始变慢。根据这个结果,PostgreSQL给出了以下公式: connections = ((core_count * 2) + effective_spindle_count)
链接数 = ((核心数 * 2) + 磁盘数)。即便是32核的机器,60多个链接也就够用了。因此,小伙伴们在配置Proxy数据源的时候,不要动不动就写上几百个链接,不只浪费资源,还会拖慢速度。
目前Proxy访问真实数据库使用的是JDBC,很快Netty + MySQL Protocol异步访问方式也会上线,二者会并存,由用户选择用哪一种方法访问。
在Proxy中使用JDBC的ResultSet会对内存形成很是大的压力。Proxy前端对应m个client,后端又对应n个真实数据库,后端把数据传递给前端client的过程当中,数据都须要通过Proxy的内存。若是数据在Proxy内存中呆的时间长了,那么内存就可能被打满,形成服务不可用的后果。因此,ResultSet内存效率能够从两个方向优化,一个是减小数据在Proxy中的停留时间,另外一个是限流。
咱们先看看优化前Proxy的内存表现。使用5个客户端链接Proxy,每一个客户端查询出15万条数据。结果以下图,之后简称图1。
能够看到,Proxy的内存在一直增加,即时GC也回收不掉的。这是由于ResultSet会阻塞住next(),直到查询回来的全部数据都保存到内存中。这是ResultSet默认提取数据的方式,大量占用内存。那么,有没有一种方式,让ResultSet收到一条数据就能够当即消费呢?在Connector/J文档(https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html)中有这样一句话: 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遇到查询结果太多,以至堆内存都装不下的状况,你能够指示驱动使用流式结果集,一次返回一条数据。激活这个功能只需在建立Statement实例的时候设置一个参数:
stmt.setFetchSize(Integer.MIN_VALUE);
这样就完成了。这样Proxy就能够在查询指令后当即经过next()消费数据了,数据也能够在下次GC的时候被清理掉。固然,Proxy在对结果作归并的时候,也须要优化成即时归并,而再也不是把全部数据都取出来再进行归并,Sharding-Core提供即时归并的接口,这里就不详细介绍了。下面看看优化后的效果,如下简称图2。
数据在内存中停留时间缩短,每次GC都回收掉了数据,内存效率大幅提高。看到这里,好像已经大功告成了,然而水还很深,请你们穿上潜水服继续跟我探索。图2是在最理想的状况产生的,即Client从Proxy消费数据的速度,大于等于Proxy从MySQL消费数据的速度。
若是Client因为某种缘由消费变慢了,或者干脆不消费了,会发生什么呢?经过测试发现,内存使用量直线拉升,比图1更强劲,最后将内存耗尽,Proxy被KO。下面咱们就先搞清楚为何会发生这种现象,而后介绍对ResultSet的第2个优化:限流。下图加上了几个主要的缓存,SO_RCVBUF/ SO_SNDBUF是TCP缓存、ChannelOutboundBuffer是Netty写缓存。
当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高水位线的大小。
在即将发布的Sharding-Sphere 3.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开源以来,不断精进、不断发展,被愈来愈多的企业和我的承认:在Github上收获5000+的star,1900+forks,60+的各大公司企业使用它,为Sharding-Sphere提供了重要的成功案例。此外,愈来愈多的企业伙伴和我的也加入到Sharding-Sphere的开源项目中,为它的成长和发展贡献了巨大力量。
将来,咱们将不断优化当前的特性,精益求精;同时,你们关注的柔性事务、数据治理等更多新特性也会陆续登场。Sharding-Sidecar也将成为云原生的数据库中间件!
愿全部有识之士能加入咱们,一同描绘Sharding-Sidecar的新将来!
愿正在阅读的你也能助咱们一臂之力,转载分享文章、加入关注咱们!
Sharding-Sphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这3款相互独立的产品组成。他们均提供标准化的数据分片、读写分离、柔性事务和数据治理功能,可适用于如Java同构、异构语言、容器、云原生等各类多样化的应用场景。
亦步亦趋,开源不易,您对咱们最大支持,就是在github上留下一个star。
项目地址:
https://github.com/sharding-sphere/sharding-sphere/
https://gitee.com/sharding-sphere/sharding-sphere/
更多信息请浏览官网: