池化技术
池概念
- 池的预分配:
服务器的硬件资源“充裕”,那么提高性能的方式是以空间换时间: 池其实是一组资源的集合, 这组资源在服务器启动之初就被完全创建好并初始化,称为静态资源分配。
- 池的利用: 当服务器开始处理客户请求时,如果它需要相关资源就可以直接从池中获取,无需动态分配; 处理完后可以放回池中,而不需要系统调用释放资源,池相当于服务器管理系统资源的应用层设施
- 如何预分配多少资源?
- 预分配足够多的资源。即针对每个可能的客户连接都分配必要的资源;不过这个会导致资源浪费。
- 预分配一定的资源,随后不够之后再动态分配。
优缺点
- 优点: 避免了频繁创建分配系统资源,避免了内核的频繁访问
- 缺点: 实现肯定会复杂了,容易出错; 因为动态分配的控制,动态分配多少,维持池的资源数目在一个什么范围?何时进行回收都是一个问题。
池的分类
各种Pool
- 内存池:
- 内存池通常用于socket的接收缓存和发送缓冲。 对于HTTP请求预先分配一个比如5K字节的缓冲区是合理的。
- 当客户请求长度大于缓冲区可以选择丢弃或动态扩大。
- 线程池: 线程池就是并发处理,比如处理新的request就会直接从池子中取出线程实例来处理,而无需pthread_create
- 进程池:同上,无需fork。
- 连接池:
- WHAT: 这个通常特指数据库的连接池,因为这是属于服务器内部或服务器与db之间的永久连接;每个逻辑单元都需要频繁访问这个db。
- WHY:打开一个数据库连接是非常昂贵的操作,因为网络session、验证、权限校验等逻辑会比较重。一旦关闭了再次建立会造成较大的损耗,所以一般都会搞个连接池来进行复用。
- HOW: 预先创建一组连接集合。当逻辑单元要访问db,直接从连接池取出一个连接的实例并使用;完成访问后返还连接给连接池。
Pool的基础实现
实现线程池的基础: thread safe -->
go的线程安全实现
- 实现一个线程池的结构体,
- 需要有互斥锁来保障读写的并发安全,另一个是有conns来存储连接。
- conns 其实是** chan net.Conn**类型
type channelPool struct {
// storage for our net.Conn connections
mu sync.Mutex
conns chan net.Conn
// net.Conn generator
factory Factory
}
- 创建连接池:
- 工厂方法用于创建连接,是大于0的并填充池。
- 指定了initialCap 以及 maximum cap并基于bufferChannel来存放连接~
- 利用channel来传递connection的好处是线程安全,没有conn的时候会阻塞,不会造成panic。
func NewChannelPool(initialCap, maxCap int, factory Factory) (Pool, error) {
if initialCap < 0 || maxCap <= 0 || initialCap > maxCap {
return nil, errors.New("invalid capacity settings")
}
c := &channelPool{
conns: make(chan net.Conn, maxCap),
factory: factory,
}
// create initial connections, if something goes wrong,
// just close the pool error out.
for i := 0; i < initialCap; i++ {
conn, err := factory()
if err != nil {
c.Close()
return nil, fmt.Errorf("factory is not able to fill the pool: %s", err)
}
c.conns <- conn
}
return c, nil
}
- 获取连接
- 获取Conn的时候必须是用互斥锁保护conn,否则会造成并发读写出错。
- 如果没有connection,就会创建一个新的connection;
func (c *channelPool)Get() (net.Conn, error) {
conns, factory := c.getConnsAndFactory()
if conns == nil { return nil, ErrClosed}
select {
case conn := <- conns:
if conn == nil {
return nil , ErrClosed
}
return c.wrapConn(conn),nil default:
conn, err := factory()
if err = nil { return nil, err }
return c.wrapConn(conn), nil
}
}
func (c *channelPool) getConnsAndFactory() (chan net.Conn, Factory) {
c.mu.Lock()
conns := c.conns
factory := c.factory
c.mu.Unlock()
return conns, factory
}
- 归还连接
将连接放回池子,如果pool是满的或closed,那么conn会被关闭。
func (c *channelPool) put (conn net.Conn) error {
if conn == nil { return errors.New("Connection is nil, rejecting") }
c.mu.RLock()
defer c.mu.RUnlock()
if c.conns == nil { return conn.Close() }
select {
case c.conns <- conn:
return nil
default:
return conn.Close()
}
}
- 关闭连接
- 关闭连接池必须上锁,因为要对结构体里面的conns进行写。即标记为nil。
- 同时调用close( chan ) 即关闭通道。那么其他想要读取这个channel的方法就会返回nil。
func (p *channelPool)Close() error {
c.mu.Lock()
c.conns = nil
c.factory = nil
c.mu.Unlock()
if conns == nil {return }
close(conns)
for conn := range conns { conn.Close() }
}
总结点
- 连接池的最大数量要有限制,如果线程池空的话我们默认返回一个新连接;一旦并发量高就会不断新建连接,很容易造成too much connection
- 最大的connection可以约定,然后空闲的时候也希望维护一定的空闲连接 idleNum。
- 问题 如何解决连接长期idle的问题? 另外是如何解决重复建立扩张新连接的问题? --- 待完善
数据库连接池
基础实现原理
连接池保持连接活跃,所以连接可以稍后再请求。
- client请求,检查pool是否有连接
- 有连接就直接返回给client
- 否则创建新的连接
- 新的请求来,是关闭连接的请求,这时放到连接池里面;
- 稍后的某个时间真正地被关闭。
连接池的动态变化过程
基础实现
public class ConnectionPool where T : IDisposable, new()
{
private ConcurrentStack mPool = new ConcurrentStack();
public T Pop()
{
if (!mPool.TryPop(out T item))
{
item = new T();
}
return item;
}
public void Push(T item)
{
mPool.Push(item);
}
}
可以基于栈来实现,栈存的是一个连接对象,Pop和Push就是弹出连接和压入连接。
栈实现
- 增加最大限制
最大连接数量的限制,即当连接数过多,就需要判断。
- 负载满了
负载满了返回空的话,sleep不是好的做法,直接抛异常也是问题;最好的做法是事件驱动,即通过事件来通知, 本质是实现一个队列,负载满了后Push到队列里面,
sql.DB连接池
标准库提供了一个通用的数据库连接池,通过MaxOpenConns和MaxIdelConn控制最大连接数和最大IDEL连接数。
用法:
db.Open()
db.DB().SetMaxIdleConns(c.Gorm.MaxIdleConns)
db.DB().SetMaxOpenConns(c.Gorm.MaxOpenConns)
db.DB().SetConnMaxLifetime(time.Duration(c.Gorm.MaxLifetime) * time.Second)
源代码:
type DB struct {
// Atomic access only. At top of struct to prevent mis-alignment
waitDuration int64 // Total time waited for new connections.
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 // protects following fields
freeConn []*driverConn
connRequests map[uint64]chan connRequest
nextRequest uint64 // Next key to use in connRequests.
numOpen int
openerCh chan struct{}
closed bool
dep map[finalCloser]depSet
lastPut map[*driverConn]string // stacktrace of last conn's put; debug only
maxIdleCount int // zero means defaultMaxIdleConns; negative means 0
maxOpen int // <= 0 means unlimited
maxLifetime time.Duration // maximum amount of time a connection may be reused
maxIdleTime time.Duration // maximum amount of time a connection may be idle before being closed
cleanerCh chan struct{}
waitCount int64 // Total number of connections waited for.
maxIdleClosed int64 // Total number of connections closed due to idle count.
maxIdleTimeClosed int64 // Total number of connections closed due to idle time.
maxLifetimeClosed int64 // Total number of connections closed due to max connection lifetime limit.
}
- freeConn: 是一是空闲的连接切片 conn(基于[]*driver.Conn)
- maxIdle : 最大空闲连接数。
- numOpen: 已打开的连接以及等待打开的连接(pending open conn)用于表示需要新连接的信号;
1. 打开连接:
- Open实际并没有建立实际的数据库连接, openNewConnection在goroutine中运行;
- 使用mutex进行锁保护,对db .numOpen进行处理, 因此open方法是并发安全的。
- 维护池子中的idle connection, 只要有出错,即连接fail 或者 连接已关闭 就要把numOpen--。
func (db *DB) openNewConnection(ctx context.Context) {
// maybeOpenNewConnections has already executed db.numOpen++ before it sent
ci, err := db.connector.Connect(ctx)
db.mu.Lock()
defer db.mu.Unlock()
if db.closed {
if err == nil { ci.Close()}
db.numOpen--
return
}
if err != nil {
db.numOpen--
db.putConnDBLocked(nil, err)
db.maybeOpenNewConnections()
return
}
dc := &driverConn{ // }
if db.putConnDBLocked(dc, err) {
db.addDepLocked(dc, dc)
} else {
db.numOpen--
ci.Close()
}
}
打开连接,就需要把请求数减少,增加新的numOpen标志。
func (db *DB) maybeOpenNewConnections() {
numRequests := len(db.connRequests)
if db.maxOpen > 0 {
numCanOpen := db.maxOpen - db.numOpen
if numRequests > numCanOpen {
numRequests = numCanOpen
}
}
for numRequests > 0 {
db.numOpen++ // optimistically
numRequests--
if db.closed {
return
}
db.openerCh <- struct{}{}
}
}
2.设置空闲连接或最大连接数
func (db *DB) SetMaxOpenConns(n int) {
db.mu.Lock()
db.maxOpen = n
if n < 0 { db.maxOpen = 0 }
syncMaxIdle := db.maxOpen > 0 && db.maxIdleConnsLocked() > db.maxOpen
db.mu.Unlock()
if syncMaxIdle {
db.SetMaxIdleConns(n)
}
}
freeConn : 连接池内部存储连接的结构 freeConn;
3.获取连接
- 实际在Query里面调用conn()
- FreeConn这个切片有空闲的话就left Pop出列(加锁操作)。
- connRequest (本质是map)
- 1) 一个类似排队机制的 connRequest,当空闲的连接为空时,这边将会新建一个request并且开始等待。(可以解决等待)
- 2) 往connRequest插入自己的号牌,插入号码牌之后这边就不需要阻塞等待继续往下
- 3) Context取消操作时,从connRequest这个map取走自己的号码牌
// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
// 先判断db是否已经关闭。
db.mu.Lock()
if db.closed {
db.mu.Unlock()
return nil, errDBClosed
}
// 注意检测context是否已经被超时等原因被取消。
select {
default:
case <-ctx.Done():
db.mu.Unlock()
return nil, ctx.Err()
}
lifetime := db.maxLifetime
// 这边如果在freeConn这个切片有空闲连接的话,就left pop一个出列。注意的是,这边因为是切片操作,所以需要前面需要加锁且获取后进行解锁操作。同时判断返回的连接是否已经过期。
numFree := len(db.freeConn)
if strategy == cachedOrNewConn && numFree > 0 {
conn := db.freeConn[0]
copy(db.freeConn, db.freeConn[1:])
db.freeConn = db.freeConn[:numFree-1]
conn.inUse = true
db.mu.Unlock()
if conn.expired(lifetime) {
conn.Close()
return nil, driver.ErrBadConn
}
// Lock around reading lastErr to ensure the session resetter finished.
conn.Lock()
err := conn.lastErr
conn.Unlock()
if err == driver.ErrBadConn {
conn.Close()
return nil, driver.ErrBadConn
}
return conn, nil
}
// 这边就是等候获取连接的重点了。当空闲的连接为空的时候,这边将会新建一个request(的等待连接 的请求)并且开始等待
if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
// 下面的动作相当于往connRequests这个map插入自己的号码牌。
// 插入号码牌之后这边就不需要阻塞等待继续往下走逻辑。
req := make(chan connRequest, 1)
reqKey := db.nextRequestKeyLocked()
db.connRequests[reqKey] = req
db.waitCount++
db.mu.Unlock()
waitStart := time.Now()
// Timeout the connection request with the context.
select {
case <-ctx.Done():
// context取消操作的时候,记得从connRequests这个map取走自己的号码牌。
db.mu.Lock()
delete(db.connRequests, reqKey)
db.mu.Unlock()
atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
select {
default:
case ret, ok := <-req:
// 这边值得注意了,因为现在已经被context取消了。但是刚刚放了自己的号码牌进去排队里面。意思是说不定已经发了连接了,所以得注意归还!
if ok && ret.conn != nil {
db.putConn(ret.conn, ret.err, false)
}
}
return nil, ctx.Err()
case ret, ok := <-req:
// 下面是已经获得连接后的操作了。检测一下获得连接的状况。因为有可能已经过期了等等。
atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
if !ok { return nil, errDBClosed }
if ret.err == nil && ret.conn.expired(lifetime) {
ret.conn.Close()
return nil, driver.ErrBadConn
}
if ret.conn == nil {
return nil, ret.err
}
ret.conn.Lock()
err := ret.conn.lastErr
ret.conn.Unlock()
if err == driver.ErrBadConn {
ret.conn.Close()
return nil, driver.ErrBadConn
}
return ret.conn, ret.err
}
}
// 下面就是如果上面说的限制情况不存在,可以创建先连接时候,要做的创建连接操作了。
db.numOpen++ // optimistically
db.mu.Unlock()
ci, err := db.connector.Connect(ctx)
if err != nil {
db.mu.Lock()
db.numOpen-- // correct for earlier optimism
db.maybeOpenNewConnections()
db.mu.Unlock()
return nil, err
}
db.mu.Lock()
dc := &driverConn{
db: db,
createdAt: nowFunc(),
ci: ci,
inUse: true,
}
db.addDepLocked(dc, dc)
db.mu.Unlock()
return dc, nil
}
4. 释放连接:
返回连接,如果有在排队轮换的请求就
- 判断特殊边界:比如已关闭,numOpen大于maxOpen
- 从connRequests这个map里面随机抽一个在排队的其请求,取出后还给源节点,不需要归还池子。
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
if db.closed {
return false
}
if db.maxOpen > 0 && db.numOpen > db.maxOpen {
return false
}
if c := len(db.connRequests); c > 0 {
var req chan connRequest
var reqKey uint64
for reqKey, req = range db.connRequests {
break
}
delete(db.connRequests, reqKey) // Remove from pending requests.
if err == nil {
dc.inUse = true
}
//连接给这个正常排队的连接。
req <- connRequest{
conn: dc,
err: err,
}
return true
} else if err == nil && !db.closed {
if db.maxIdleConnsLocked() > len(db.freeConn) {
db.freeConn = append(db.freeConn, dc)
db.startCleanerLocked()
return true
}
db.maxIdleClosed++
}
return false
}
解决重要问题:
- 解决too many connection : 避免连接太多的情况,主要就是靠--》 connRequst 连接请求队列的机制
- 连接健康问题检查 :
- 获得连接前: 即连接是否已关闭,是否超时(超过context或db的
maxLifeTime) 具体是用conn.expired方法 |db.Closed关闭状态 - 获得连接后超时:
atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))关闭状态:ret, ok <-req , errDBClosed
- 获得连接前: 即连接是否已关闭,是否超时(超过context或db的
网络连接线程池
基础原理
服务端启动,先按固定大小预创建多个线程,当有新连接建立时,往连接字队列里设置这个新描述字,线程池里的线程负责从连接队列取出连接描述字
- Cond: 条件变量,多个线程需要交互下,用来线程间同步。
- Mutex:互斥锁
// 定义一个队列
typedef struct {
int number; // 队列里的描述字最大个数
int *fd; // 这是一个数组指针
int front; // 当前队列的头位置
int rear; // 当前队列的尾位置
pthread_mutex_t mutex; // 锁
pthread_cond_t cond; // 条件变量
} block_queue;
// 初始化队列
void block_queue_init(block_queue *blockQueue, int number) {
blockQueue->number = number;
blockQueue->fd = calloc(number, sizeof(int));
blockQueue->front = blockQueue->rear = 0;
pthread_mutex_init(&blockQueue->mutex, NULL);
pthread_cond_init(&blockQueue->cond, NULL);
}
// 往队列里放置一个描述字 fd
void block_queue_push(block_queue *blockQueue, int fd) {
// 一定要先加锁,因为有多个线程需要读写队列
pthread_mutex_lock(&blockQueue->mutex);
// 将描述字放到队列尾的位置
blockQueue->fd[blockQueue->rear] = fd;
// 如果已经到最后,重置尾的位置
if (++blockQueue->rear == blockQueue->number) {
blockQueue->rear = 0;
}
printf("push fd %d", fd);
// 通知其他等待读的线程,有新的连接字等待处理
pthread_cond_signal(&blockQueue->cond);
// 解锁
pthread_mutex_unlock(&blockQueue->mutex);
}
// 从队列里读出描述字进行处理
int block_queue_pop(block_queue *blockQueue) {
// 加锁
pthread_mutex_lock(&blockQueue->mutex);
// 判断队列里没有新的连接字可以处理,就一直条件等待,直到有新的连接字入队列
while (blockQueue->front == blockQueue->rear)
pthread_cond_wait(&blockQueue->cond, &blockQueue->mutex);
// 取出队列头的连接字
int fd = blockQueue->fd[blockQueue->front];
// 如果已经到最后,重置头的位置
if (++blockQueue->front == blockQueue->number) {
blockQueue->front = 0;
}
printf("pop fd %d", fd);
// 解锁
pthread_mutex_unlock(&blockQueue->mutex);
// 返回连接字
return fd;
}
服务端使用线程池技术:
int main(int c, char **v) {
int listener_fd = tcp_server_listen(SERV_PORT);
block_queue blockQueue;
block_queue_init(&blockQueue, BLOCK_QUEUE_SIZE);
thread_array = calloc(THREAD_NUMBER, sizeof(Thread));
int i;
for (i = 0; i < THREAD_NUMBER; i++) {
pthread_create(&(thread_array[i].thread_tid), NULL, &thread_run, (void *) &blockQueue);
}
while (1) {
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listener_fd, (struct sockaddr *) &ss, &slen);
//....
}
return 0;
}
主程序变得非常简洁。
- 预创建了多个线程,组成了一个线程池。
- 新连接建立后,将连接描述字加入到队列中。
- 工作线程的功能:从连接队列中取出描述符,并进行服务处理
TCP workerpool实现
goroutine is cheap, 通常可以每个传入的请求生成一个goroutine。
理由
- 但规模上,非零成本可能为瓶颈。 或者你可能想限制服务器的并发性
- 因为并非所有机器都是平等的。
这两种情况下,工作池,是下一个合乎逻辑的步骤 -- 可以通过创建并发工作池来分摊goroutine成本。
工作池
由于工作池重用 Goroutines,则内存的扩张也可以得到缓解,每个Goroutine会自己维护4K左右的栈内存。
// workerResetThreshold defines how often the stack must be reset. Every N
// requests, by spawning a new Goroutine in its place, a worker can reset its
// stack so that large stacks don't live in memory forever. 2^16 should allow
// each Goroutine stack to live for at least a few seconds in a typical
// workload (assuming a QPS of a few thousand requests/sec).
const workerResetThreshold = 1 << 16
type request struct {
// whatever
}
type server struct {
workerChannels []chan *request
}
func (s *server) worker(ch chan *request) {
// To make sure all server workers don't reset at the same time, choose a
// random number of iterations before resetting.
threshold := workerResetThreshold + rand.Intn(workerResetThreshold)
for completed := 0; completed < threshold; completed++ {
req, ok := <-ch
if !ok {
return
}
s.handleSingleRequest(req)
}
// Restart in a new Goroutine.
go s.worker(ch)
}
func (s *server) initWorkers(numWorkers int) {
s.workerChannels = make([]chan *request, numWorkers)
for i := 0; i < numWorkers; i++ {
// One channel per worker reduces contention.
s.workerChannels[i] = make(chan *request)
go s.worker(s.workerChannels[i])
}
}
func (s *server) stopWorkers() {
for _, ch := range s.workerChannels {
close(ch)
}
}
func (s *server) handleSingleRequest(req *request) {
log.Printf("processing req=%v\n", req)
}
func (s *server) listenAndHandleForever() {
for counter := 0; ; counter++ {
req := listenForRequest()
select {
case s.workerChannels[counter % len(s.workerChannels)] <- req:
default:
// TODO: If this workers is busy, fall back to spawning a Goroutine. Or
// find a different worker. Or dynamically increase the number of workers.
// Or just reject the request.
}
}
}
func newServer() *server {
s := &server{}
s.initWorkers(16)
return s
}
func main() {
s := newServer()
s.listenAndHandleForever()
}