从一个简单的SQL查询搞懂Sharding-Proxy核心原理

做者介绍前端

张永伦,京东数科软件开发工程师,Apache ShardingSphere (Incubating) PPMC。2018年2月参与到ShardingSphere项目中,经历了在Apache孵化的整个过程。比较擅长Sharding-Proxy,SQL-Parser,APM和性能测试方向。数据库

分享的内容分为四部分。首先,Sharding-Proxy简介:包括Proxy在ShardingSphere中的定位,Proxy架构和特性的介绍。第二部分,SQL的一辈子:在这里,咱们从一个简单查询SQL的角度,了解Sharding-Proxy内部的运转流程。第三部分,核心原理:会介绍几个不难理解,但对Sharding-Proxy很是重要的原理。最后,性能优化:对于Sharding-Proxy这种应用,它的可用性和性能是很是重要的指标,因此在这里总结一下以前出现的比较有共性的问题。还会介绍一下目前是如何进行性能保障的。后端

1、Sharding-Proxy简介缓存

一、Apache ShardingSphere生态圈

咱们来看一下ShardingSphere的定位。它是一个分布式数据库中间件组成的生态圈,之因此说它是一个生态圈,是由于它整个功能的设计是一个闭环的结构,另外也会给用户提供多种接入方式,来方便用户在生产当中面对不一样的接入需求。如今你们看到的是ShardingSphere的总体架构,它的功能由五大块组成。分别是数据分片,分布式事务,数据库治理,这三块已经上线交付用户使用。管控界面近期也由社区开发完成并捐献给Apache基金会。多模式链接还在规划和开发的进程中。底下是它的接入端,咱们为你们提供了三款产品,分别是Sharding-JDBC,Sharding-Proxy和Sharding-Sidecar。每一款接入端都具有上面全部的功能,三款产品分别知足用户不一样生产场景的需求。性能优化

二、Sharding-Proxy架构

下面进入正题。Sharding-Proxy的定位是透明化的数据库代理,它封装了数据库二进制协议,用于完成对异构语言的支持。目前兼容MySQL和PG,可使用任何兼容MySQL或PG协议的客户端进行访问,好比MySQL命令行、MySQL Workbench、Navicat等等,对DBA更加友好。bash

整个架构能够分为前端、后端和核心组件三部分。前端负责与客户端进行网络通讯,采用的是基于NIO的客户端/服务器框架,在Windows和Mac操做系统下采用NIO 模型,Linux系统自动适配为Epoll模型。通讯的过程当中完成对MySQL协议的编解码。核心组件获得解码的MySQL命令后,开始调用Sharding-Core对SQL进行解析、路由、改写、结果归并等核心功能。后端与真实数据库的交互目前借助于Hikari链接池。服务器

2、SQL的一辈子微信

一、业务场景

你们能够看到,整个ShardingSphere的功能仍是很是多的,短期内确定没法一一讲到。那么咱们就从数据分片,这样一个比较核心,比较基础,又很是重要的功能点来展开跟你们分享。数据分片我以为你们多少都会有所了解,基本原理就是把一个库拆分红多个库,把一个表拆分红多个表。来达到水平扩展的目的。在这个场景里有一个数据库,里面只有一张表,叫t_order。因为它的数据量很大,因此它在底层存储的时候已经进行了拆分。按照这样的分片策略拆分红了两个库,每一个库两张表。Sharding-Proxy帮助用户屏蔽掉了真实的库表,用户的SQL只需针对逻辑库表就能够了。网络

二、MySQL协议解码

SELECT * FROM t_order where user_id = 10 and order_id = 1;复制代码

咱们如今看一下这个SQL是怎么来的,它的真实面目是什么。当链接Proxy的客户端执行一个SQL的时候,咱们能够经过Wireshark在网络上抓到这个包。能够看到,MySQL协议是承载在TCP协议之上的。传输方向是32883端口到3307端口,这个3307是 Proxy的默认端口。MySQL协议也像大多数协议同样遵循TLV原则:架构

  • TYPE: 命令类型——Query

  • LENGTH: 消息长度——58

  • VALUE: 就是这个SQL的ASCII码

Proxy解码出逻辑SQL后,就会当即把它送给解析模块处理。

三、SQL解析

咱们如今看到的是SQL通过解析后生成的抽象语法树。这个语法树是由Antlr自动生成的。

解析过程分为词法解析和语法解析。词法解析器用于将SQL拆解为不可再分的原子符号(好比select, from, t_order, 还有*, =,10),以后语法解析器将SQL转换为抽象语法树。有了这个语法树以后,经过对其遍历,就能够提炼出分片所需的上下文,并标记有可能须要改写的位置。好比user_id和order_id的值要取出来,他们是分片键,决定路由的结果。表t_order的位置要记录下来,改写的时候才能找到。

四、路由

在当前的这个简单分片规则下,真实库的计算方式是 user_id % 2 => 10 % 2,路由到ds_0库。同理,真实表order_id % 2 => 1 % 2,路由到t_order_1。固然,路由的功能不止这么简单。路由引擎支持多种分片策略,包括取模、哈希、范围、标签、时间等等。还支持多种分片接口,包括行表达式、内置规则、自定义类等方式。

五、改写

为何要改写?上面这个面向逻辑库与逻辑表的SQL,并不可以直接在真实的数据库中执行,SQL改写的做用就是把逻辑SQL改写为能够在真实库中正确执行的真实SQL。真实库和真实表咱们以前已经知道了,因此直接把SQL改写为这样。并非全部SQL的改写都这么简单,好比聚合函数怎么改写,包含LIMIT的SQL怎么改写,何时须要补列,这些都是改写引擎须要处理的事情。

六、执行

路由改写完成了,就能够从链接池里取链接执行SQL了。整个执行过程是经过ShardingSphere的执行引擎完成的。执行引擎会根据路由节点的数量和单次查询单库容许的最大链接数,自动决策出是否使用流式结果集。流式结果集会最大程度减小内存的压力。这个真实SQL最后被Proxy发送到MySQL服务器。在MySQL服务器里通过一系列的缓存、解析、优化后,从存储引擎里把结果数据取出来并返回给Proxy。

七、归并

Proxy收到结果数据后,须要对数据进行归并处理。为了方便说明归并,咱们换一个SQL。这个SQL只包含一个分片键user_id,那么它会被路由到ds_0库。因为没有指定order_id,因此会被路由到所有的真实表t_order_0和t_order_1。两个真实表分别存在一条知足条件的数据,那么归并就是将这两条数据合并起来返回给客户端。归并引擎的功能很强大,咱们这个例子属于最简单的遍历归并。还有不少其余的归并场景:好比,排序归并:SQL中存在order by时,须要考虑如何排序代价最小。再好比,分组归并:在SQL中同时存在order by和group by的状况下,须要考虑如何优化内存占用率。

八、MySQL协议编码



获得了最终的归并结果后,Proxy会把这个结果编码成MySQL协议发送给客户端。数据包中包含每一列的描述信息,好比数据库名、表名、字符集、数据类型等等。


还有就是用户可见的结果数据。MySQL命令行客户端收到协议包后,会在终端上打印结果数据。若是使用的是JDBC客户端,JDBC会把全部结果保存到ResultSet里面。以上就是Sharding-Proxy的一个简要流程。

3、核心原理

一、IO & 线程模型

核心原理部分首先介绍一下IO模型和线程模型。能够简单理解上面两个是前端,下面两个是后端。Boss Group至关于Reactor模式中的Reactor。Worker Group至关于模式中的Worker。User Executor Group用于执行MySQL命令。Sharding Execute Engine用于并发访问数据库。你们看到Boss Group和Worker Group应该能猜到前端用的是Netty。因此前端使用IO多路复用处理客户端请求。后端使用的是Hikari链接池,同步请求数据库。若是开启XA事务,状况就会比较特殊。因为Atomiks事务管理器的资源是threadlocal的,因此一次事务的全部SQL必须所有在同一个线程中执行。在执行MySQL命令的时候会单首创建一个线程,并缓存起来。

二、流式归并

下面介绍两个早期开发时遇到的问题,实现起来不难,可是原理值得研究一下。这是我在本身电脑上测试的一个场景,使用5个JDBC客户端链接Proxy,每一个客户端查询出15万条数据。能够看到,Proxy的内存在一直增加,即便GC也回收不掉。这是由于在收到所有15万条数据以前,JDBC ResultSet的get()方法是阻塞的。简单说,就是JDBC在没收到全量结果以前,是不让用户读数据的,这是ResultSet的默认提取数据方式。会致使Proxy缓存大量临时数据。那么,有没有一种办法,能让ResultSet一收到结果就当即给用户消费呢?

从Connector/J文档上能够找到两种解决方法:


一种是流式结果集:只要把Statement的FetchSize设置成这个值,JDBC就会使用流式ResultSet处理返回结果,让用户可以一边接收数据一边消费数据,而不须要所有接收完再消费。

另外一种方式是基于游标的流式结果集:设置JDBC参数useCursorFetch=true,表示要使用游标。设置FetchSize,指示每次返回数据的行数。

这种方式效率很低,要谨慎使用。经过抓包能够发现,客户端每次读取数据都会向服务端发送Request Fetch Data这个请求,在网络上的时间开销是很是大的。

Proxy采用的是第一种方案,能够看到内存已经恢复正常了。流式结果集是流式归并的前提条件,流式归并还要知足SQL条件和链接数条件,比较复杂,这里就不详细介绍了。这个内存使用效果是在最理想的状况下产生的,也就是客户端从Proxy消费数据的速度,大于等于Proxy从MySQL消费数据的速度。

三、限流


若是客户端因为某种缘由消费变慢了,或者干脆不消费了,会发生什么呢?经过测试发现,内存使用量直线飙升,比刚才那张图还夸张。咱们来研究下为何会产生这种现象。这里加上了几个主要的缓存,SO_RCVBUF和SO_SNDBUF,他们是TCP的缓存。

ChannelOutboundBuffer是Netty写缓存。在结果数据回传的过程当中,若是Client阻塞,那么他的SO_RCVBUF会瞬间被数据填满,触发TCP的滑动窗口去通知Proxy不要再发送数据了。与此同时,数据就会积压到Proxy端,因此Proxy端的SO_SNDBUF也同时被填满了。Proxy的SO_SNDBUF满了以后,Netty的ChannelOutboundBuffer就会像一个无底洞同样,吞掉全部MySQL发来的数据,由于在默认状况下ChannelOutboundBuffer是无界的。因为Netty在消费,因此Proxy的SO_RCVBUF一直是空的,致使MySQL能够一直把数据发送过来,而Netty则不停的把数据存到ChannelOutboundBuffer,直到内存耗尽。

找到根本缘由以后,咱们须要作的就是当Client阻塞的时候,不让Proxy再接收MySQL的数据。Netty经过水位参数来控制写缓冲区,当buffer大小超太高水位线,咱们就控制Netty再也不往里面写,当buffer大小低于低水位线的时候,才容许写入。设置完水位线后,当ChannelOutboundBuffer满时,Proxy的SO_RCVBUF天然也满了,触发TCP滑动窗口通知MySQL中止发送数据。在这种状况下,Proxy所消耗的内存只是ChannelOutboundBuffer高水位线的大小。咱们的目的就达到了。

四、分布式链路追踪系统


这个很是炫酷的界面来自Skywalking。他监控到了Sharding-Proxy中,执行一条SQL的完整调用链,并且对Proxy的代码没有任何侵入。他是怎么作到的呢?说道对代码没有侵入,你们首先想到的可能就是钩子。没错Skywalking就是使用的Instrument Agent实现的自动探针。目前已经支持的探针达几十种,涵盖了不少主流应用。若是把SkyWalking部署在一个真正的业务系统上,你看到的调用链会更加丰富。核心原理部分就介绍到这里。

4、性能优化

一、代码

我在这里总结了一下常见的性能问题,有想提交代码的同窗能够重点留意一下,避免出现相同的问题。

第一类是代码类问题。第一个例子:

你们看一下这个函数,有什么问题吗?入参若是是LinkedList,可能会产生什么后果?LinkedList是链表,若是用下标get,时间复杂度是O(n)。循环n次,整个时间复杂度是O(n方)。换成for each遍历遍历就能够了。

这个以前有社区的同窗测试过,50000个元素,执行时间相差1000多倍。

第二个例子:

若是把Properties当作整个系统的全局变量来使用,系统中大量并发调用getProperty()。Properties是Hashtable的子类,Hashtable的读写都是同步的。因此并发读会被串行处理。使用其余数据保存全局变量,好比ConcurrentHashMap。

二、额外非预期SQL

第二类是额外非预期SQL。第一个例子:

最近有一个PR,在每次执行SQL的时候调用了JDBC 的这个接口提取用户名。JDBC会利用select user()这个SQL,到数据库去查询用户名。致使在不知情的状况下,你认为执行了一个SQL,实际上执行了两个。优化方式就是把UserName缓存。性能能够提高32%。

第二个例子:

另一个非预期SQL问题与咱们以前使用的流式ResultSet有关。

这是我从JDBC 5.1.0的Release Notes上截的图。每建立一个流式ResultSet,JDBC都会设置一次网络超时时间。致使网络上有大量的 SET net_write_timeout 请求。


解决方式是手动把这个参数的值设为0。通过测试,性能提高33%。可见额外SQL对性能影响很是大,你们必定要注意。

三、IO & 系统调用

第三类涉及到比较底层的IO和系统调用。

Netty中发送数据分为两步:write()和flush()。Write()把数据写入netty的缓存中,flush()把数据发送出去。以前的实现是,每次从MySQL收到一条结果数据,就马上flush一次把它发送到客户端。这样的用法效率很低。诺曼在《Netty In Action》里提到要尽可能减小flush()的调用次数,由于系统调用的代价很是高。

其实不止系统调用代价高,网络利用率也很低。咱们能够算一下,TCP每次发送数据,各类包头加起来要54字节(MAC头:14字节 + IP头:20字节 + TCP头:20字节),可能比你真正传输的数据都多。频繁的flush,单位时间内传输的有效数据确定会变小。优化方式是多条结果调用一次flush(),性能提高达到50%。

四、全路由

最后一类是全路由问题。(这张图是京东数科内部的链路监控工具SGM,是对接京东白条现场的截图)

原本预期路由到单个节点的SQL,被路由到了n个节点。通常是由解析问题引发的。致使响应时间指数级增加。定位这类问题可使用Java性能监控工具定位,例如JMC、JProfiler。或者APM工具SkyWalking、SGM等等。利用他们跟踪路由相关的函数,检查调用次数是否正确。

五、测试

为了可以即时发现性能问题,咱们会对每一个合并的PR进行性能测试,把问题定位在最小的范围内。


好比此次构建,性能明显降低,经过构建序号就能找到Github的提交记录。

与性能测试不一样,压力测试侧重于真实业务,天天进行长时间的压测,保证当天代码的稳定性。

目前天天压测10小时左右,像内存泄漏这种问题就可以测出来。这个压测的页面之后会放到官网上。你们提完PR后,次日就能够看到效果。

Sharding-Proxy在经历了一年多的磨练后已经走向成熟,开始逐渐被部署到生产环境使用。欢迎感兴趣的小伙伴试用,使用过程当中有什么问题能够加微信群讨论。若是你们有什么想法、意见和建议,也欢迎留言与咱们交流,更欢迎加入到ShardingSphere的开源项目中。

相关文章
相关标签/搜索