如果将互联网应用比喻成冲浪的话, 可能需要先学会在“池”中游泳。
引子
AI赋能万物,老码农的伙伴们也曾经开发了一个基于图数据库的知识问答系统,在压力测试的时候发现随着并发数的增加,响应的时延明显变长,看时延分布,是应用程序与图数据库之间的交互时延过长。结构不做调整,优化图数据库后,发现在并发量上来之后,效果仍不明显。
看代码,观察ELK中的日志,发现了问题所在————高并发时连接的创建时间较长。时间所限,替换为httpclient的连接池,post 和 get都采用池中的连接,性能问题迎刃而解。
在编程的世界里,经常会遇到连接池,那连接池到底是什么呢?
什么是池
池,一种资源抽象的形象化说法。编程世界中的池是一组资源, 可以随时使用, 但不随时地创建和释放。
资源池(resource pool)被认为是一种设计模式,这里的资源主要是指系统资源, 这些资源不专属于某个进程或内部资源。客户端向池请求资源, 并使用返回的资源进行指定的操作。当客户端使用完资源后, 会把资源放回池中而不是释放或丢弃掉。
任何技术都有自己的应用边界,池作为一种资源使用技术,典型的使用情形是:
- 当获取资源的成本较高的时候
- 当请求资源的频率很高且使用资源总数较低的时候
- 当面对性能问题,涉及到处理时间延迟的时候
池中的资源主要有两类:需要系统调用(system call) 的系统资源,或主演需要网络通信的远程资源, 如数据库连接、套接字连接、线程和内存分配等等。池中的资源一般不包括像字体库或图片等大的数据对象, 那些资源的存储一般是通过是数据缓存或数据库技术实现的。由于资源池的存在, 从池中获取资源所需的时间变成了可预知的,从而在一定程度上解决性能的问题。
根据资源的类型,资源池一般包括连接池、线程池和内存池。
连接池
连接池是创建和管理一个网络连接资源池的技术,这些连接一般预先准备好被任何需要它们的线程或者进程使用。
网络连接根据连接的生命周期可以粗略的分为两种:长链接和短链接。就web应用而言,短连接就是一般的http请求,长连接如websocket。
短链接适合大部分应用。对于远程方法的执行时间远大于连接创建时间(看网络情况大约为数毫秒)的时候,其连接创建时间可以被忽略,此时短连接策略基本不会有较大性能损失。另外,对于非频繁调用火灾对延迟时间不敏感的服务也适合使用短连接策略。
对于高并发或者高吞吐量的应用,网络连接的创建消耗是很大的,对于这种应用应该使用长连接策略的连接池实现。
连接池中的几个常用参数
在各种连接池的实现中,常用的参数一般有:连接数相关,连接时间相关,有效性相关。
连接数
设计一个连接池,要确定池中的连接数量,包括最小空闲连接数,***空闲连接数,连接池***持有连接数。当然连接数可以变化,动态缩放,确定每次增加/减少的连接数量。
连接的有效性
保证连接池中的连接有效性,相当于增加了连接心跳的检测。同时,还有从池中获取客户端接口时的有效性,将客户端接口归还连接池时的有效性,当配置或实现了相关的管理服务,可以通过管理工具观察连接池的使用情况。例如对于Java的应用,如果配置了JMX服务的话,可以通过JMX管理工具观察Java连接池的状态。
连接有效性测试可以减少长连接失效造成的远程调用失败,对于那些对连接失效而造成的调用失败很敏感的服务,可以开启各种合适的连接有效性测试策略来保障所取得的客户端是连接正常的。
时间相关参数
为了保持池中连接的有效性,空闲连接检测时间也就是心跳间隔,这往往取决于业务使用连接池的场景。另外,还有从连接池中获取连接的***等待时间,一般地默认为-1,即无可用连接会抛出异常,当设为0时表示无穷大。
网络通信连接池
网络通信的连接池主要节省创建TCP连接的时间,从而降低了请求的总处理时间。客户端为每个服务端实例维护一个连接池。如果连接池中有空闲连接,则复用这个连接。如果连接池中没有空闲连接,则会建立一个新的TCP连接或者等待池中出现空闲的连接。
当客户端使用池中连接处理完一个请求时,如果连接池中的空闲连接数小于连接池的大小,则将当前使用的连接放入连接池。 如果连接池中的空闲连接数大于等于连接池的大小,则关闭当前使用的连接。
面向http短连接的连接池,服务端支持keepalive时才有效,如果服务端关闭keepalive,则效果等同于短连接,就没有连接池的作用了。
同理,如果连接池的大小设置为0,也等同于短连接的方式。服务端支持Keepalive的时候,可以减少CPU和内存的使用,允许请求和应答的HTTP管道化,减少了后续请求的延迟,报告错误也无需关闭TCP连接。
一般地,对于延迟敏感的业务,可以使用连接池机制。
数据库连接池
开头的例子是一个数据库连接池。数据库连接池也可以理解为维护数据库连接的缓存, 以便在需要对数据库的请求时可以重用连接。
为每个用户打开和维护数据库连接需要消耗大量的资源,而数据库连接池用于提高数据库中执行命令的性能,减少了用户必须等待的时间。在数据库连接池中, 创建连接后将其放入池中, 再次使用, 不必重新建立新的连接。如果所有的连接都被使用, 则创建新的连接并被添加到池中。
基于 web 的应用程序和企业应用程序一般都使用应用服务器来处理连接池。当页面需要访问数据库时, 只需使用池中的现有连接, 并且只在池中没有空闲连接的情况下建立新连接。这减少了连接到数据库响应单个请求的开销,需要频繁访问数据库的本地应用程序也可以从数据库连接池中受益。
一些库不仅实现了数据库连接池还实现了相关的 SQL 查询池, 简化了数据库操作密集型应中连接池的实现。Java中常用的数据库连接池有:DBCP 、C3P0、BoneCP、Proxool、DBPool、XAPool、Primrose、SmartPool、MiniConnectionPoolManager及Druid等。
通过对连接池进行配置, 对最小连接、***连接和空闲连接的数量加以限制, 可以优化在特定场景和特定环境中数据库连接池的性能。
端上的连接池
由于互联网尤其是广域网中的速度非可控性,特别是移动互联网(基于3G/4G)的速度的不确定性,在端上的应用也将连接池作为一种重要的技术手段。
以Chrome浏览器为例,其网络库采取连接池的方式管理连接的建立、分配以及释放,当请求可以直接从连接池中获取复用连接时,可以减少建立连接的时间消耗。除了websoket连接池之外,包含三种类型的连接池:
- TransportClientSocketPool
- SSLClientSocketPool
- SOCKSClientSocketPool
其中TransportClientSocketPool为低层连接池,SSLClientSocketPool和SOCKSClientSocketPool为高层连接池,高层连接池包含低层连接池或其他高层连接池的对象,这三种连接池类可以组合出多种连接池对象。打开chrome://net-internals/#sockets 可以看到浏览器当前的连接状态。
在app中,连接池同样被广泛采用,主流的网络通信库都支持连接池,例如Okhttp。平台层也是如此,例如Android 平台中的binder 连接池。
线程池
在计算机编程中, 线程池是实现计算机程序中并发执行的软件设计方式。线程池维护多个线程, 等待监督程序为并发执行分配任务。通过维护一个线程池, 可以提高性能, 避免执行延迟。可用线程的数量取决于程序可用的计算资源, 如并行处理器、核心、内存和网络套接字。
一个常见的线程执行任务调度方法是同步队列, 称为任务队列。池中的线程将等待任务从队列中移除, 并在执行完成后将其放置到已完成的任务队列中。线程池的大小是为执行任务而保留的线程数,通常是一个可调参数, 调整它可以以优化程序性能。
线程池对于为每个任务创建一个新线程的主要好处是线程创建和销毁开销仅限于初始创建池, 这可能导致更好的性能和更好的系统稳定性。通常情况下,创建和销毁一个线程及其相关资源是一个费时的过程。
然而, 池中的线程数量过多, 会浪费内存, 并且在可运行的线程之间切换上下文也可能会引发性能问题。一个socket连接到另一个网络主机, 可能需要许多 CPU 周期, 可以将socket与在多个网络事务中使用的线程联系起来, 可以更有效地维护它。
根据等待任务的数量, 可以在应用程序的生存期间动态调整线程数。例如, 如果许多网页同时发出请求的时候, web 服务器可以添加线程, 当请求逐渐减少时可以删除线程。
线程池使用中需要注意的问题:
- 创建太多的线程会浪费资源
- 关注创建了但未使用的线程
- 销毁了大量线程后又化费较多的时间来重新创建它们
- 创建线程过于缓慢可能导致客户端性能变差
- 销毁线程过于缓慢可能会饿死其他的处理流程
内存池
内存池, 是使用池来进行内存管理, 使动态内存分配时达到 malloc 或者 new 的效果。由于内存碎片的存在,一个有效的方案是预先分配一些内存大小相同的内存块,许多实时操作系统都适用了内存池。一种简单的内存池实现如下图所示:
对于内存池的应用而言,可以通过以下方式分配、访问和释放内存:
- 从池中分配内存时,函数将确定所需块的池。如果该池的所有区块已被保留,则该函数试图在下一个较大的池中找到一个。分配的内存块用句柄表示。
- 获取分配内存的访问指针
- 释放以前分配的内存块
内存池将句柄划分为池索引、内存块索引以及版本, 从而在内部解释句柄。池和内存块索引允许使用句柄快速访问对应的块, 而在每个新分配中增量的版本允许检测已经释放内存块的句柄。
内存池允许使用恒定的执行时间来分配内存。数千个对象在池中的内存释放只是一个操作, 而不是一个一个的Free。内存池也可以采用树状结构, 应用于特殊的编程行为, 如循环,递归等。固定大小的块内存池不需要为每个块分配元数据存储, 不需要描述分配块的大小等特性。
内存池还可用于对象, 在这种情况下,对象本身没有外部资源, 只占用内存, 已经创建了的对象避免了对象创建时的内存分配。当对象创建成本较高时, 对象池是有用的, 但在某些情况下, 这种简单的对象池可能并不有效, 实际上还可能会降低性能。
小结
池是一种资源共享和复用的技术,把管理的理念引入到编程世界中。从基础的内存池,到线程池,再到各种连接池,根据应用场景还可以继续细分,如句柄池,缓存池.....几乎涵盖了互联网应用的大部分角落。如果将互联网成冲浪的话, 可能需要先学会在池中游泳吧。
【本文来自51CTO专栏作者“老曹”的原创文章,作者微信公众号:喔家ArchiSelf,id:wrieless-com】