1、背景
作为代理服务,系统需要同时管理不同的用户、管理不同数据库实例的连接。但是每一个连接在创建的时候,就必须指定其用户名和数据库实例,以及一些其它会话状态例如客户端编码、时区等,此后在该连接执行任何SQL的时候,当需要这些信息的时候就会使用这些预设值。在这种情况下,如果将用户建立的连接与数据库连接一一对应,那么问题不大,在此基础上可以实现高可用、主备切换。
但是,仅仅实现高可用和主备切换,对于现代应用程序的性能要求来说是远远不够的。首先,就像先前所说的,连接就是一种稀缺资源,每个用户连接对应一个数据库连接是一种浪费,连接过多会造成数据库的响应能力下降;其次如果用户连接与数据库连接一一对应,是没有办法实现负载均衡、读写分离这两个功能的,因为你不能要求用户在处理读写请求的时候分别使用多个数据库连接,也不能让用户去选择负载较低的数据库节点提交请求,因为这样会大大提升用户使用数据库的困难,最好的方法是由代理服务调配,用户可以对其中的逻辑完全无感知。
如果代理服务需要实现:1、提高连接利用率,用更少的连接处理更多的请求;2、实现负载均衡和读写分离。那么它们的共同要求就是:连接可以复用,可以在适合的时候使用不同的数据库连接代替上一次执行请求使用的连接,通俗的说就是一个连接池。但是就像之前所说的,连接池建立的连接,已经跟用户名、数据库实例、客户端编码、时区绑定了,那么它们又如何能复用呢,不同的用户在使用数据库的时候,这些信息都有可能是不一样的,而且是很可能不一样。
对于此类问题,业界也有一些的解决方案:
在代理服务中为每个数据库节点创建一个连接池,然后在每次请求之前,都先选择一个适合的连接,读连接或者是写连接,在执行之前先进行状态同步,然后再执行。这样连接就可以复用了,一个代理服务只要创建少量的数据库连接,即可支持大量的客户端请求。
2、解决方案
本方案在代理服务中为每个节点的每一种状态创建一个连接池,在每次请求之前,系统都会根据请求的信息和连接的状态选择一个合适的连接池,并从中获取一个连接进行操作。这样,用户拿到连接后,不需要进行状态同步,直接就可以复用连接进行操作,一个代理服务只要创建少量的数据库连接,即可支持大量的客户端请求。
然而管理多个连接池的时候,空闲的连接也会更多,因此本方案在多连接池管理中的连接动态调配中进行了特殊的设计,以提高连接的在各个连接池中的调配效率。连接状态组合虽然多,但是实际使用中依然是非常有限的,一般一个应用程序可能存在的状态也就3、4个,所以为每个状态配置一个连接池不会造成太大的负担。
基于此数据结构可以在保证连接池高性能、连接高利用率的前提下,解决不同的数据库连接的会话状态差异问题:
每个连接池使用一个线程安全的双向链表作为基础数据结构,每个节点保持一个连接,并记录其上次使用的时间。当用户申请连接的时候,从队首取出一个连接交给用户,如果不存在可用的连接时,根据连接状态创建一个并交给用户。当用户归还连接的时候,将连接归还到队首,并更新其上次使用时间。如果连接的状态发生了修改,直接归还到对应状态的连接池中。此外还有一个守护线程,根据上次使用时间从队尾将空闲的连接逐步移除和关闭。如果连接池的借出连接数归零了则可以直接回收整个连接池,以避免代理服务数据变得臃肿。在这样的数据结构下,可以很容易推断得知其存在以下特性:
1、大部分的请求都会由少数几个连接的不间断工作完成;
2、空闲的连接(>1s)会及时得到释放,避免占用数据库的连接资源;
3、不需要在执行请求前对数据库连接进行额外的恢复操作,用户可以直接拿到适合状态的连接;
4、由于存在1s的空闲时间缓冲,即使在并发执行数量浮动较大的情况,连接池性能仍然会保持平稳。
3、优点
本方案提出一种数据库连接动态调配的方法,与常用的连接复用方案相比,在连接池管理方面变更为复杂,但是省略了连接状态同步环节,提升了请求处理性能。
在常用方案中,在每次请求执行之前,都必须要对连接状态进行同步,例如用户名、数据库实例、客户端编码、时区等信息的同步,这是毫无疑问会降低请求处理性能的一个操作。