背景与介绍
SQL限流,顾名思义,是一种对SQL的查询速度进行限制的能力。一般情况下,我们希望SQL查询语句在数据库上的执行速度越快越好,然而数据库的资源有限,在CPU、IO、内存等某一项资源达到上限时,查询在并发执行时会有激烈的资源争抢,这时查询会有因为资源不足而出现超时,影响用户业务。这时用户首先想到的是升配数据库实例,计算资源不足则增加计算节点,IO资源不足则增加存储节点分摊IO开销,以此增加资源上限。
如上图所示,PolarDB-X包含计算节点(CN)和存储节点(DN),由于计算节点是无状态的,增加计算节点操作可以在短时间内完成,然而增加存储节点,涉及到数据迁移,无法快速完成。当无法快速增加资源上限时,我们想到的是如何进行合理的资源分配让现有的存量资源发挥出最大的价值,简单概括为:让高优先级甚至直接影响核心业务的查询能够获得足够资源,而低优先级甚至可以不在这时执行的查询获得较少的资源。PolarDB-X正是以SQL限流的形式提供了这样的能力,让用户能够在核心业务的查询受到其他边缘业务查询排挤的情况下,快速采取措施限制边缘业务的查询,恢复核心业务,及时止血。
小实验:SQL限流的关键作用
接下来使用一个小实验来展现SQL限流的关键作用。
首先在阿里云的官网上购买一个入门级的PolarDB-X 2.0的实例,因为笔者需要长期测试使用,商品类型上选择了“包年包月”,读者若想体验可选“按量付费”,体验完后记得及时释放。
接着对PolarDB-X实例进行实验操作,操作过程由三个阶段组成:核心业务流量访问、边缘业务流量访问、开启SQL限流。
第一阶段,对PolarDB-X施加TPC-C流量。TPC-C流量主要包含5种事务的发起:订单创建(New-Order)、订单支付(Payment)、发货(Delivery)、订单状态查询(Order-Status)、库存状态查询(Stock-Level),以此模拟一个电商平台的核心系统。
第二阶段,从另外的施压机并发发起IO-Bound的查询操作,查询语句如下:
select * from bmsql_oorder as bubububuverybadsql limit 10000;
以此模拟,边缘业务流量对数据库的访问,对核心业务查询形成资源争抢。
第三阶段,在发现核心业务流量收到影响之后,用户在SQL日志和show full processlist命令的执行结果中双双发现一条非核心业务而且费IO资源的查询语句。果断采取措施,使用PolarDB-X的SQL限流能力对包含关键字"bubububuverybadsql"的SELECT查询进行限流,限流方式为禁止执行这样的SQL,使其查询返回错误,以此屏蔽边缘业务流量。
通过监控查看实验结果,QPS和RT的监控如下:
在第一阶段,核心业务流量正常访问,QPS处于高位,RT较小;在第二阶段,由于费资源的边缘业务流量的影响,QPS急剧下降,RT上升,核心业务受到重大影响;在第三阶段开始的时间点,运用SQL限流大法后,QPS上升至正常水位,RT下降,核心业务恢复正常。
功能简介
我们参考业界数据库的SQL限流功能,结合PolarDB-X自身特点,开发了具有简洁易用的交互接口、多样的限流策略、平均匹配复杂度O(1)、节点级限流实例级监控的SQL限流能力,可解用户燃眉之急。
简洁易用的交互接口
如果要对一种特定模式的SQL进行限流,用户可以通过SQL接口创建限流规则来指定需要限流的SQL的特征,以及限流行为(对匹配上的SQL采取的措施)。SQL语法中,我们使用CCL_RULE或者CCL_RULES语法关键字来代表限流操作。CREATE、DROP、SHOW开头的SQL限流语法用于创建、删除、查看SQL限流规则。语法简洁易懂,方便用户快速上手,而且不易出错。
多样的限流策略
我们提供丰富的SQL限流的匹配维度,可让用户尽可能精准地匹配到需要限流的SQL。
例子
继续以上文小实验的SQL为例,如果想要禁止执行这样的SQL,首先分析这条的SQL所具有的特征:
特征名称 |
特征值 |
用户名 |
ccltest |
客户端IP |
未知 |
数据库 |
tpcc100 |
表 |
bmsql_oorder |
独特的关键词 |
bubububuverybadsql |
语句类型 |
SELECT |
如果我们希望匹配到的SQL直接返回错误,则创建限流规则并尝试执行这样的SQL,结果如下:
PolarDB-X在提高限流精准度上进行了深入探索。使用关键词匹配时,需要指定该SQL独特的关键词或者关键词组合,来避免错误地对其他的SQL实施了限流,当无法拿到完整的单条SQL时,可用关键词进行有效限流。另外,PolarDB-X允许指定SQL模版编号(可从explain、show full processlist和SQL日志中得到)进一步提高SQL限流精准度。
有时,我们不希望这样比较耗资源的SQL被一直返回错误,而是希望获得执行结果,但能允许执行得慢一点。PolarDB-X允许用户在限流规则中,指定查询并发度(MAX_CONCURRENCY)、等待队列最大长度(WAIT_QUEUE_SIZE)、最大等待时间(WAIT_TIMEOUT)。每个查询进入执行状态都会占用一个并发度,退出时释放并发度,而当并发度不够的时候,查询进入等待队列,当超出等待队列长度或者等待超时,查询都会返回错误。
平均匹配复杂度O(1)
匹配方式
限流规则存在优先级,后创建的优先级高于先创建的,匹配时按优先级从高到到低进行匹配,匹配到一个限流规则即返回结果,不再匹配剩余的限流规则。PolarDB-X在SHOW CCL_RULES指令的返回结果中指出了现有限流规则的优先级。
性能优化
假设限流规则的数量为N,每个限流规则都采用关键词匹配,单个限流规则里的平均关键词数量为M,关键词长度是个常量,则限流规则的匹配复杂度为图片。我们做了实验,如果Naive地进行挨个匹配,当限流规则数量为73个,每个规则有21个关键词,关键词长度为6个字符,相比没有限流规则时,数据库性能下降15.8%。我们使用空间换时间的方式对匹配开销进行了优化,使得匹配开销降为常量,经测试发现:规则匹配开销几乎没对数据库性能产生影响。
接下来对优化点进行简单介绍:
- 用户匹配维度优化。
当客户端登录PolarDB-X时,PolarDB-X内部会分配一个连接对象给该客户端,每个对象有一个全实例唯一的ConnId用于标示该连接,因此这个ConnId可以映射到该连接对象上不可变的信息,比如user@host。因此,限流模块发现某个查询因为用户名而无法匹配到任何一个限流规则时,之后都不再匹配该连接上的查询。
- 关键词匹配内转为模版编号匹配。
如上图所示,当一个关键词匹配的限流规则匹配上一个查询时,而且查询中的参数值没有匹配到关键词,我们则认为是该SQL的模版匹配上限流规则,可使用元组(TemplateId,Schema,User,Host)作为Key,将匹配结果缓存。下次同一用户名,同一查询来的时候,可直接返回匹配到的规则。
假设一个限流规则的匹配关键词组为 ["polardbx1", "polardbx2"], 当前符合该关键词组的SQL有:
1. select `name` as polardbx1, `age` as polardbx2 from test where id = 1; 2. select * from test where `name` = 'polardbx1' or `name` = 'polardbx2';
第一个查询语句匹配结果会被缓存,而第二个查询语句因为是参数值匹配中了限流规则中的关键词,匹配结果不会被缓存。
- 无法匹配到规则的查询语句的优化
假设一个限流规则的匹配关键词组为 ["polardbx1", "polardbx2"],当前在执行的SQL有:
a1. select `name` as polardbx1 from test where id = 1; a2. select `name` as polardbx1 from test where id = 2; b1. select `name` as polardbx1 from test where `name` = 'polardbx1'; b2. select `name` as polardbx1 from test where `name` = 'polardbx2';
如上所示,查询语句a1和a2,b1和b2具有相同的SQL模版。匹配时,与2中类似,元组(TemplateId,Schema,User,Host)作为Key,将匹配结果缓存,因此他们在缓存匹配结果时会使用相同的CacheKey。
以a1和a2为例,他们的SQL模版都是:
a. select `name` as polardbx1 from test where id = ?;
参数值分别为 1 和 2。假设创建完限流规则后,a1语句先于a2执行,执行a1时,限流模块缓存该模版不匹配的结果,当a2再来执行时,我们只需要对判断a1与a2唯一变动,即参数值,是否能对匹配结果产生影响即可,显然a2的参数值为2,不是任何限流规则的关键词,因此直接返回不匹配的结果。
然而,当b1先于b2执行的时候,b2的参数值'polardbx2'可能会影响匹配结果,我们对b2仍然进行完整的匹配,最后返回匹配到的规则。
为提高效率,我们根据所有规则的关键词会预先建立一个BloomFilter,以此快速判断一个参数值是否可能为某个限流规则里的关键词。
节点级限流实例级监控
PolarDB-X限流规则中的并发度、等待队列长度都是针对单个节点的参数,假设限流规则并发度为P,实例计算节点个数为K,则整个实例的并发度最大为KP。
为什么限流规则参数不针对整个实例?
从PolarDB-X架构中可以看出,资源是跟着节点走的,节点越多资源越多,使用节点级限流时,节点增加或减少不会改变限流行为的合理性。另外,由于PolarDB-X的负载均衡机制是针对连接数,将连接数均匀分布在各个计算节点上,我们能通过以下的伪代码构造出负载不均衡的场景:
connPoola = [] connPoolb = [] for i in range(1000): connPoola.push(createConnection('jdbc:mysql:10.10.10.9:3306/a')) connPoolb.push(createConnection('jdbc:mysql:10.10.10.9:3306/n')) use connPoola for IO Bound Work use connPoolb for core business
使用以上创建连接池的方式,当计算节点个数正好为2且SLB采用常见的轮询方式的时候,connPoola和connPoolb将分别连接到各自的节点上。生产环境中,如果多个不同业务功能的连接池并发创建,也可能会产生相似的效果。此时如果针对connPoola中的查询进行限流,如果限流参数是针对实例级的,限流行为则与会与预想的大相径庭。
使用实例级参数的好处是,用户评估整个实例的限流效果时无需关心节点个数。现实是用户目前可以从PolarDB-X控制台上感知到节点个数,可以方便地评估出整个实例的限流效果。
综合考虑,使用节点级限流更为合理。
为方便用户监控实例状况,和显示上的简洁性,使用SHOW CCL_RULES指令查看限流规则的时候,限流效果聚合了多个计算节点上的信息。
总结与展望
本文首先介绍了SQL限流的使用场景,它可通过限制边缘业务查询,留出资源来为核心业务保驾护航。接着是功能简介,PolarDB-X结合自身云原生分布式的特点,提供了具有简洁易用的交互接口、多样的限流策略、平均复杂度O(1)、节点级限流实例级监控的SQL限流能力。
目前,当查询匹配到限流规则进入等待队列时,仍然占用线程池里的一条线程,未来我们将会让出线程,提高资源利用率。我们亦将沿着数据库智能化的演进方向,探索如何根据workload自动为用户创建限流规则。