这次我们来聊聊数据库的连接, 因为我觉得这是蛮有内容且蛮重要的一部分内容。首先会从单个的连接池讲起,重点考察下单连接池和数据库的交互情况, 然后探讨下大规模集群下数据库连接会遇到的问题,以及对应的解法。
首先什么是连接池,出现的原因是啥?我们可以从一个标准SQL的生命周期说起, 如果一个SQL要到DB上去执行, 那么首先要建立应用服务器和数据库的一个连接状态,连接建立后数据库会分配一个线程或者进程来调度,完成解析并生成执行计划,然后才进入执行阶段,读取必要的数据到内存并逻辑处理, 最后才通过之前创建的连接发送结果集给到客户端,关掉连接并释放资源,所以连接可以说是应用和DB交互的桥梁和管道,可惜这个桥梁的构建和销毁,对与数据库来说是资源消耗很大的操作,这里会涉及到CPU的运算, 资源的争用,内存的分配, socket的建立等,频繁的创建连接和销毁连接,对数据库来说是不可接受的,所以长连接显然比短连接更适合数据库,这时候就出现了连接池,来对SQL生命周期中连接的创建和销毁这个环节进行优化,有了连接池,就能做到连接复用,维护连接对象,做分配,管理和释放,也就能减少平均连接时间,有了连接池,并加以合理的配置,同时能避免应用创建大量的连接到DB而引发的各种问题,通过请求排队,来缓冲应用对DB的冲击,所以从这个角度看,连接池其实就是排队。
我们可以想象一下对连接池的基本动作,无非就是申请连接,从连接池中获取连接,和业务处理完后,把连接释放回连接池这些动作。在通常情况下一个连接池在启动时会初始化MIN连接数,这时候通往数据库的一部分管道已经建立起来了,你可以通过这些管道,对数据库进行查询和增删改查,如果一个请求申请管道的时候发现有空闲的管道, 那么直接可以拿来用了, 如果所有的管道都在忙,但管道的数量没有达到MAX连接数, 那么不需要等待,直接申请创建一个新的连接,用完了再把他放回去,当发现没有空闲的管道, 并且活跃的管道已经到达MAX连接数了, 那么这时候你只能选择暂时等待, 等待的时间取决于block-timeout, 在这等待期间如果有管道空闲下来, 那么恭喜你,你有机会拿到这个连接, 如果超出等待时间还没有拿到连接,那么就抛出个拿不到连接的异常,连接池基本的逻辑就是这样了,另外的功能无非就是对连接池使用状态的监控,比如一个连接如果空闲下来了,多久没有使用需要被关闭,比如哪些错误情况下需要重新创建一下连接再放入池子,比如如何定时来验证连接是否有效,等等。
刚才提到了连接池的MIN和MAX连接,需要大家的关注,因为连接池是无法感知数据库的运行情况以及负载的,通过经验值或者计算模型,合理的加以设置, 对于应用服务器和数据库来说,都非常的重要,即要能发挥出应用服务器的最大能力,也要能有效利用数据库的连接资源和处理能力, 换句话说不想在有能力处理时让请求在队列中等待,也不想让运行的请求超出DB的处理能力。
我们具体来看一下,如果连接池MIN设置过小的话,在应用业务量突增或者启动时,就可能短时间内产生连接风暴,这对于数据库是不小的冲击,但是如果MIN值设置过大,就会出现数据库连接过剩的情况, 连接一方面超出空闲时间被销毁,而销毁后发现又小于MIN连接数, 又开始创建, 结果就发生循环, 浪费资源浪费电。那如果连接池MAX值设置过大,在极端情况下,当应用发生异常时,会导致连接数被撑到MAX值,有可能导致数据库的连接数被耗尽,或者超出数据库的处理能力,进而导致业务受到影响。并且当连接数被撑到MAX值,在获取连接等待超时的时候,应用的线程池也有可能受到影响,会形成一系列的连锁反应,乃至雪崩。
所以平时有开发同学抱怨连接池的配置不够,让我们加大MAX值, 我都会解释下,能不能加连接要看DB是否还有余量,如果DB还有余量,加连接也许是一种临时的解决办法, 如果DB已经容量不足, 加大MAX会放进更多的请求倒DB,只会让性能变得更差,我们换个角度来做一个数学题,按照连接池默认的配置MAX为6,一百台应用服务器连接一个MySQL ,所以会有600个连接落到数据库,按照一个请求的处理时间1ms的话, 那么一秒钟就能处理1000个请求, 600个连接的话可以处理60w的qps/tps请求了,这时候就已经远远超出单个DB的容量极限了。
也有的同学会说, 那把我block-timeout的时间改长一点, 尽可能的提高拿到连接的概率,岂不是挺好? 不好意思,这个同样不太靠谱,当应用并发很高,大大超过连接池最大值,block-timeout也不能起到缓冲作用,返而会阻塞应用线程,大量的积压线程会导致应用直接挂了。所以这个等待的时间也不是越长越好,而需要从应用的维度去评估一下,并建立好容错机制。
强调了以上两点,细心的同学可能已经发现了,这里面的关键不在别的地方, 而是在于怎么提高响应时间,就是怎么做SQL优化,让事务尽可能的短,怎么进一步做连接复用,提高管道的效率,进而缩短请求的DB服务时间。前面提到过,连接池就是排队论的思想, 我们可以进一步根据little's law 来阐述一下这里面的关系,比如说每秒访问频率是1000 (W), 平均服务的时间2ms(λ), 那么队列的长度 L = λW =0.002*1000 =2, 也就是说队列的长度为2,只要两个连接就能搞定这些需求了,如果我们平均服务的时间缩短到1ms,那么连接池就只需要1个连接就够了,根据little's law ,我们拿到SQL的响应时间,以及请求到达率, 就可以比较简单直观的评估出连接池的大小, 而blocking的时间,也会决定最大等待队列的限制,都可以根据排队论理论做进一步的评估。
这时候连接已经能复用了,连接池的设置也比较合理了,假设SQL的优化上已经没有空间了,这时候应用和DB就应该开始比较流畅的工作了,我们是不是可以高枕无忧, 蒙头睡觉了? 很遗憾,优化是一种毒药,会让人欲罢不能,整个SQL生命周期中有无数的点可以优化(今天主要是跟连接相关的,跟数据库相关的优化以后会单独拎出来扯)。 当我们发现很多情况下执行的都是相同的SQL, 管道虽然已经可以复用了, 但是每一次都把SQL发到数据库上去执行, 都要进行网络交互,数据库还是要重新解析,一遍遍的生成执行计划再执行,代价还是非常高, 同样的SQL是否能预编译掉,省去数据库硬解析的成本呢,或者能否减少网络的交互时间呢?这时候引入了PreparedStatement的概念,只在第一次发送SQL到数据库进行解析,然后就会将有关这个SQL的信息存储到PreparedStatement里面, 这样就可以被同样的SQL语句反复使用了。
对于ORACLE和OB来说,绑定变量下的SQL,使用PreparedStatement能够显著的提高系统的性能,这里面要注意PreparedStatement的对象占用JVM的内存大小,特别是拆分数据源中,曾经发生过JVM内存被撑爆的情况。(JVM内存占用情况=连接总数*PreparedStatementCache设置大小*每个PreparedStatement占用的平均内存) ,在MYSQL数据库中,因为没有绑定变量这个概念,客户端虽然可以设置PreparedStatement,但是在Server端只能在session级别共享一些信息,每个SQL都还是需要进行解析的,所以性能不会有太大的影响, 我们实际的测试也验证了这一论断,目前MySQL官方也在做Server端全局的PreparedStatement,不知道何时能够出来。
再进一步看连接的优化点,数据库的连接都是附带状态的,事务的状态也是维持在连接上的,而一个连接在单位时间内只能处理一个事务请求, 所以需要多个连接来保证并发度,同时数据库(MySQL)也需要创建相应多的线程来绑定这个关系, 那么这个利用率是否足够高呢? 一个连接+一个事务状态+一个线程绑定在一起的状态是否能被打破呢? 比如单连接一次发送多个请求是否可行? 比如连接和(事务状态+线程)的绑定是否能打破, 甚至全部打破?
接下来我们来讲讲大规模集群下的连接问题, 我们拿ICDB集群来举列子,顺便解答下刚才这个问题。记得13年的双十一前夕,ICDB发生性能抖动的问题,把我们惊出了一身冷汗,现在看起来最主要的原因还是大量并发的请求导致MySQL出现抖动。
我们前面讲到过数据库的连接数和实际运行的线程数是两个不同的概念,一个MySQL实例能支撑的连接数可以有很多,受MAX_connections控制,真正的天花板可能在内核的文件句柄,按照一个连接2M来算(默认一个thread创建连接需要分配stack,connect buffer,result buffer,应用层面的连接会更轻量一点),即使有一万个连接所占用的内存也只有20G,Server端能支撑得住。但是要注意的是,这些连接并不都是活跃的,也即在不会同时在运行的,如果DB上运行的活跃连接数过高,线程上下文切换的成本就会很高,DB的响应时间就往往就满足不了业务的需求了,还有即使观察看每秒DB的并发运行线程可能在200左右,但1秒之内请求不是平均分布。在大连接下,很容易出现瞬间运行线程量巨大的情况。问题在于,在瞬间大量并发请求时,也就是活跃的连接数非常大的时候,MySQL对于并发处理的不够好,容易产生性能波动,并持续恶化,进而影响应用响应时间。
所以大并发和多连接,其实是两个问题,可以分开来看,但是这两问题又不能孤立的来解决,多连接的情况下更有可能出现大并发 ,而解了多连接很大程度上也就缓解了高并发的问题,而如果完美的解决了高并发, 也许可能就不需要解多连接了。
为啥需要这么多连接?我们分析下就可以得到, 一个实例的连接数由三个因素决定, 实例的DB数,连接池配置的MAX,以及连数据库的应用机器数量。 假设一个实例有两个DB,有500个应用服务器会去连DB, 连接池的MAX配置是6, 那么这个实例的连接总数就为 2* 500*6 =6000,而数据库连接不断增加很大程度上是受第三个因素的影响,其本质原因还是应用集群规模增大了。
围绕这三个因素做解法,第一个是通过拆分和降低连接池,降低单实例MySQL的连接数,比如原来一个实例上面有两个DB, 通过拆分一个实例只有一个DB, 那么在应用服务器不变的情况下, 连接数就变成1*500*6=3000。
第二个就是提高DB响应时间,这样在系统同样处理能力的情况, 连接池的最大连接可以减少一半,前面little's law 也提到过,响应时间缩短一倍, 同样的处理能力,连接池只要三个连接,这样进一步把连接数减少到 1*500*3 =1500,比如线上的tcbuyer集群的MAX的设置就是2, 肯定比你想象的要小吧。
但是前面两个改进的红利, 很快就会被应用服务器数量的增加给吃掉了,第三个解决办法,也是彻底的解决办法,就是减小应用集群规模,比如采用应用逻辑分组, 甚至单元化部署来解决。单元化并不是为了减少MySQL连接数而做的,但是单元化之后确实可以有效降低连接数 。
前面的三个办法能够有效的解决大连接的问题,但是没有解决高并发的问题,还是可能出现高并发把数据库打垮的问题, 所以我们还是需要第四种方式, 来解决多连接的同时,进一步解决高并发的问题, 这个解法就是文章中间提到的,将(事务状态+线程) 和连接解绑, 方案也比较多,比如增加一层Proxy (这个Proxy位置可以比较灵活), 但是链路复用需要对用户SQL的上下文有依赖, 而且proxy的引入对稳定性和性能有一点的影响,所以不是很推荐。或者第二种办法使用MySQL线程池,就是类似于Oracle的MTS模式,这种方案在我们线上高并发,短事务居多的情况下,是比较合适的,而且直接做到MySQL这一层是最合理的。
所以问题其实就是在高并发时,MySQL需要一个更好的排队策略而已。围绕这个思路,13年的双十一我们采用的是MySQL高低水位限流版,如果出现大量并发请求,通过低水位来排队, 同时通过高水位来削峰限流,即拒绝请求的方式,保证MySQL的响应时间,高水位限流这其实是一种损过载保护, 确保输入不会大于DB的处理能力。到了14年的双十一,我们彻底采用了线程池版本的MySQL,线程跟连接解绑开来,演化成更加合理的等待制排队系统了。
用大家都熟悉的餐厅故事来解释下,假设一家餐厅同时来了100个客人,但餐厅的产能不足,只能同时服务10位客人,MySQL原先的做法是找了100个服务员来接待这100个客人,然后这一百个服务员各种争抢和厨师沟通的机会, 容易乱成一锅粥, 高水位水位限流就是我只最多能让50个客人进来 对后面50个客人说你回去吧, 我伺候不了你们了, 而线程池的做法,就是只有10个服务员, 100个客人都乖乖的排队, 等待分配服务员,保证分配了服务员的客人能够享受餐厅的服务, 这样厨师只要和这十个服务员打交道就可以了, 这样能够减少沟通, 切换, 资源争用的成本。
讲到这里, 相信大家对数据库的连接都有所了解了,还有一块没有涉及到,但是非常关键,就是数据库的RT变化对应用服务的影响, 之前在云化的过程中,直接拿RT的变化推导机器数是有问题的,应用达到瓶颈的时候其实处理线程池通常都还没满,所以有可能是DB RT增加了一点是完全没影响的, 这个事情由大神圭多在牵头,会建立有效的压测模型,这对于提高数据库的水位,探索DB和APP服务器的最佳配比,最终降低成本是非常有意义的,等理清楚了再跟大家一起探讨下。