首页 > 其他分享 >系统优化-连接池技术原理与实现

系统优化-连接池技术原理与实现

时间:2022-11-02 11:01:32浏览次数:46  
标签:系统优化 数据库 连接数 sql go 原理 连接 连接池

​背景​

在服务访问的过程中,每一次请求都要建立一次数据库连接。建立连接是一个费时的活动,每次都要花费大约0.05s~1s的时间,而且系统还要分配内存资源。这个时间对于一次或几次数据库操作,或许感觉不出系统有太大的开销。可是对于现在的web应用,存在许多高并发服务,同时有上千上万或更多的并发请求。在这种情况下,频繁的进行数据库连接操作势必占用很多的系统资源,网站的响应速度必定下降,严重的甚至会造成服务器的崩溃。

connect_pool_1

对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄露,最终将不得不重启数据库。还有,这种开发不能控制被创建的连接对象数,系统资源会被毫无顾忌的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

​技术演进​

背景中提到问题的根源就在于对数据库连接资源的低效管理。对于共享资源,有一个著名的设计模式:资源池设计模式。该模式正是为了解决资源的频繁分配、释放所造成的问题。为解决上述问题,可以采用​​数据库连接池技术​​。「数据库连接池」的基本思想就是为数据库连接建立一个“缓冲池”。

connect_pool_2

「连接池基本原理」

  1. 服务启动时建立连接池对象
  2. 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。省略创建连接和销毁连接的过程(TCP连接建立时的三次握手和销毁时的四次握手)
  3. 设定连接池最大连接数来防止系统无尽地与数据库连接。
  4. 通过连接池的管理机制监视数据库的连接的数量、使用情况,为系统开发、测试及性能调整提供依据。
  5. 访问服务完成,释放连接(此时的释放连接,并非真正关闭,而是将其放入空闲队列中。如实际空闲连接数大于初始空闲连接数则释放连接)
  6. 服务停止释放连接池对象

​如何设计一个连接池​

本文例子主要讲解golang数据库组件​​database/sql​​连接池的实现。

go_sql

上图为go sql官方实现逻辑, 源码地址 ​​https://github.com/golang/go/tree/master/src/database/sql​

$ tree $GOROOT/src/database/sql
├── convert.go # scan row
├── convert_test.go
├── ctxutil.go # 判断 ctx,然后执行 prepare/exec/query/close 等操作
├── doc.txt
├── driver
│ ├── driver.go # 定义了实现数据库驱动所需的接口,由 sql 包和具体的驱动包来实现
│ ├── types.go # 数据类型的别名和转换
│ └── types_test.go
├── example_cli_test.go
├── example_service_test.go
├── example_test.go
├── fakedb_test.go
├── sql.go # 关于 SQL 数据库的一些通用接口和类型,包括:连接池、数据类型、连接、事务、状态
└── sql_test.go

DB对象结构​

type DB struct {
// Atomic access only. At top of struct to prevent mis-alignment
// on 32-bit platforms. Of type time.Duration.
waitDuration int64 // 等待新连接的总时间,用于统计

connector driver.Connector // 由数据库驱动实现的连接器
// numClosed is an atomic counter which represents a total number of
// closed connections. Stmt.openStmt checks it before cleaning closed
// connections in Stmt.css.
numClosed uint64 // 关闭的连接数

mu sync.Mutex // 锁
freeConn []*driverConn // 可用连接池
connRequests map[uint64]chan connRequest // 连接请求表,key 是分配的自增键
nextRequest uint64 // 连接请求的自增键
numOpen int // 已经打开 + 即将打开的连接数
// Used to signal the need for new connections
// a goroutine running connectionOpener() reads on this chan and
// maybeOpenNewConnections sends on the chan (one send per needed connection)
// It is closed during db.Close(). The close tells the connectionOpener
// goroutine to exit.
openerCh chan struct{} // 告知 connectionOpener 需要新的连接
resetterCh chan *driverConn // connectionResetter 函数,连接放回连接池的时候会用到
closed bool
dep map[finalCloser]depSet
lastPut map[*driverConn]string // debug 时使用,记录上一个放回的连接
maxIdle int // 连接池大小,默认大小为 2,<= 0 时不使用连接池
maxOpen int // 最大打开的连接数,<= 0 不限制
maxLifetime time.Duration // 一个连接可以被重用的最大时限,也就是它在连接池中的最大存活时间,0 表示可以一直重用
cleanerCh chan struct{} // 告知 connectionCleaner 清理连接
waitCount int64 // 等待的连接总数
maxIdleClosed int64 // 释放连接时,因为连接池已满而被关闭的连接总数
maxLifetimeClosed int64 // 因为超过存活时间而被关闭的连接总数

stop func() // stop cancels the connection opener and the session resetter.
}

driverConn 对象结构​

// driverConn wraps a driver.Conn with a mutex, to
// be held during all calls into the Conn. (including any calls onto
// interfaces returned via that Conn, such as calls on Tx, Stmt,
// Result, Rows)
type driverConn struct {
db *DB // 数据库句柄
createdAt time.Time

sync.Mutex // 锁
ci driver.Conn // 对应具体的连接
closed bool // 是否标记关闭
finalClosed bool // 是否最终关闭
openStmt map[*driverStmt]bool // 在这个连接上打开的状态
lastErr error // connectionResetter 的返回结果

// guarded by db.mu
inUse bool // 连接是否占用
onPut []func() // 连接归还时要运行的函数,在 noteUnusedDriverStatement 添加
dbmuClosed bool // 和 closed 状态一致,但是由锁保护,用于 removeClosedStmtLocked
}

获取连接​

代码地址如下

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
...
}

​https://github.com/golang/go/blob/master/src/database/sql/sql.go#L1131-L1246​

从连接池中获取连接时首先要对整个连接池加锁,如果连接池已经事先被关掉了,直接返回 errDBClosed 错误。如果连接池无恙,将会评估连接请求是否取消或过期。

尽可能优先使用空闲的连接而不是新建一条连接(这也是连接池存在的意义)。看一下是否还剩下空闲连接,如果还有余粮,就取第 0 条连接出来, 然后左移所有连接填补空位。这里对连接本身操作都会上锁。

如果没有空闲连接了,而且已打开的 + 即将打开的连接数超过了限定的最大打开的连接数,就要发送一条连接请求然后排队(不会新建连接)。等待排队期间同时监听连接请求是否取消或过期,如果此时连接被取消很不巧正好有连接来了,就将连接放回连接池中;如果等着等着连接来了, 会先检查这个连接的上一次会话是否被重置(擦屁股),确认没问题就用这条连接。

如果还没到限定的最大打开的连接数,会新建一个连接,代码在​​https://github.com/golang/go/blob/574c286607015297e35b7c02c793038fd827e59b/src/database/sql/sql.go#L1031-L1047​

// Assumes db.mu is locked.
// If there are connRequests and the connection limit hasn't been reached,
// then tell the connectionOpener to open new connections.
// 如果有连接请求,并且没有达到连接数的限制,告知 connectionOpener 打开新的连接
func (db *DB) maybeOpenNewConnections() {
...
}

连接池使用技巧​

  1. 连接池默认大小 defaultMaxIdleConns 连接池默认大小为 2 连接池太小会导致有太多方生方死的连接 maxIdleClosed 增长会很快
  2. 连接池状态 DBStats 定期获取 DBStats 了解连接池的基本信息
  3. 并发安全 连接池是并发安全的,但连接不是。

如果使用同一个连接,在一个事务里面,不要使用多个 goroutine 去操作这个连接。

  1. 连接失效 如果连接是客户端主动关闭的,那会在写包的时候返回 ErrBadConn,连接池会在重试次数内获取新的连接 如果连接是服务器主动关闭的,客户端并不知道,拿到连接后写包不会报错,但是在读服务器的 response 包 的时候会有 unexpected EOF 错误。4.1 设置 maxLifetime DB 定期清理连接池中的过期连接 如果没有设置 maxLifetime,表示连接池中的连接可以一直复用,如果服务器关闭了这条连接, 连接池是不知道的,返回给客户端的是一条已经关闭的连接。

获取数据库服务器的 wait_timeout,然后设置 maxLifetime 比这个数值小 10s 左右。

4.2 检查连接的有效性 MySQL 推荐在获取连接时、回池时、定期检查

​优势​

1. 资源重用

由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上, 另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。

2. 更快的系统响应速度

数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

3. 新的资源分配手段

对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术。设置某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。

4. 统一的连接管理,避免数据库连接泄漏

在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄漏。




标签:系统优化,数据库,连接数,sql,go,原理,连接,连接池
From: https://blog.51cto.com/u_12162833/5816062

相关文章

  • MySQL-深入浅出锁分类及实现原理
    ​背景​数据库是一个多用户并发使用的共享资源。当多个并发读写数据时,在数据库中就会产生多个事务同时读写同一数据的情况。若对并发操作不加控制就可能会读取和存储不正......
  • vue实战-深入响应式数据原理
    本文将带大家快速过一遍Vue数据响应式原理,解析源码,学习设计思路,循序渐进。数据初始化_init在我们执行newVue创建实例时,会调用如下构造函数,在该函数内部调用this._init(......
  • vue源码分析-插槽原理
    Vue组件的另一个重要概念是插槽,它允许你以一种不同于严格的父子关系的方式组合组件。插槽为你提供了一个将内容放置到新位置或使组件更通用的出口。这一节将围绕官网对插......
  • 深入分析React-Scheduler原理
    关键词:reactreact-schedulerscheduler时间切片任务调度workLoop背景本文所有关于React源码的讨论,基于Reactv17.0.2版本。文章背景工作中一直有在用React......
  • 经常被问到的react-router实现原理详解
    在单页面应用如日中天发展的过程中,备受关注的少了前端路由。而且还经常会被xxx面试官问到,什么是前端路由,它的原理的是什么,它是怎么实现,跳转不刷新页面的...一大堆为什么,......
  • JSONP的原理和封装
    jsonp是一种跨域通信的手段,它的原理其实很简单:首先是利用script标签的src属性来实现跨域。通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现......
  • vue中key有什么作用(key的内部原理)
    虚拟DOM的key的作用:key是虚拟DOM对象的标识,当状态中的数据发生变化的时候,vue会根据新的数据生成新的虚拟DOM,随后vue进新虚拟DOM与旧虚拟DOM的差异比较(1)旧虚拟......
  • gp map的底层实现原理
    gomap的底层实现是hashtable,根据key查找vlue的时间负责度是O(1)先通过哈希算法得出哈希值对算出来的哈希值进行对槽位总数取模找到对应槽位如果冲突多的话,需要以......
  • autoreleas实现原理
    静态库和动态库的区别1、形式上静态库是.a和.framework。动态库是.dylib和.framework,xcode8为.tbd,本质是.dylib2、使用上:静态库,链接时,会被完整的复制到可执行......
  • tensorflow2从入门到精通——自编码器系列原理以及实现
    自编码器无监督学习,常用于数据去噪,数据降维、图像重构无监督学习监督学习样本数据对应着标签,称之为有监督学习难点绝大部分标签为人工标注有些数据对于标注者的先验......