Database connection pool (reproduced)

This time, let's talk about database connections, because I think this is a very informative and important part. First, we will start with a single connection pool, focusing on the interaction between the single connection pool and the database, and then discuss the problems encountered in database connections under large-scale clusters and the corresponding solutions.

First of all, what is a connection pool, and what is the reason for it? We can start from the life cycle of a standard SQL. If a SQL is to be executed on the DB, then a connection state between the application server and the database must first be established. After the connection is established, the database will allocate a thread or process to schedule, complete the parsing and Generate an execution plan, then enter the execution phase, read the necessary data into memory and process it logically, and finally send the result set to the client through the previously created connection, close the connection and release resources, so the connection can be said to be the application and The bridge and pipeline for DB interaction, unfortunately, the construction and destruction of this bridge is a resource-consuming operation for the database, which will involve CPU operation, resource contention, memory allocation, socket establishment, etc. Frequent creation and destruction of connections is unacceptable to the database, so long connections are obviously more suitable for databases than short connections. At this time, connection pools appear to perform connection creation and destruction in the SQL life cycle. Optimization, with the connection pool, you can achieve connection multiplexing, maintain connection objects, do allocation, management and release, and also reduce the average connection time. With the connection pool, you can configure it reasonably and avoid application creation. Various problems caused by a large number of connections to the DB, through request queuing, to buffer the impact of the application on the DB, so from this perspective, the connection pool is actually queuing.

We can imagine the basic actions of the connection pool, which are nothing more than applying for a connection, obtaining a connection from the connection pool, and releasing the connection back to the connection pool after the business is processed. Under normal circumstances, a connection pool will initialize the number of MIN connections when it starts up. At this time, a part of the pipeline leading to the database has been established. You can query the database and add, delete, modify and query through these pipelines. If a request applies for the pipeline When you find that there are idle pipes, you can use them directly. If all pipes are busy, but the number of pipes has not reached the MAX number of connections, you don't need to wait, just apply to create a new connection, and use it up again. He put it back, when it is found that there are no idle pipes, and the active pipes have reached the MAX number of connections, then you can only choose to wait temporarily. The waiting time depends on the block-timeout. During this waiting period, if any pipes are idle , then congratulations, you have the opportunity to get this connection, if the connection is not obtained after the waiting time, then an exception that cannot get the connection is thrown, the basic logic of the connection pool is like this, the other function is nothing more than right Monitoring the usage status of the connection pool, such as if a connection is idle, how long it has not been used and needs to be closed, such as which error conditions need to be re-created and put into the pool, such as how to regularly verify whether the connection is valid, etc.

Just mentioned the MIN and MAX connections of the connection pool, which need everyone's attention, because the connection pool cannot perceive the operation and load of the database, and can be set reasonably through empirical values ​​or calculation models. For application servers and databases, It is very important to be able to exert the maximum capabilities of the application server, and also to effectively utilize the connection resources and processing capabilities of the database. The request exceeds the processing capacity of the 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增加了一点是完全没影响的。

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=327063321&siteId=291194637