连接池设计与实现一——以Golang Http1.1为例

news/2024/11/17 23:39:15/文章来源:https://www.cnblogs.com/hu1056043921/p/18199804

0. 前言

连接池是一个非常重要的开发思想,如http client会构建连接池复用底层TCP连接,使用database/sql的使用也会有连接池的配置。那么代码底层是如何实现连接池的呢?这篇文档将以Golang语言为基础,分析http1.1连接池底层实现

注意:我们仅仅关注连接池设计思想、以及关键源码解读,并不会涉及太多的细节,如果想要了解更多的细节,需要读者自己阅读源码

在开始之前,我们思考如下几个问题

  1. 创建的连接应该放在哪里?数组、链表、channel
  2. 为什么官方都不基于sync.Pool来实现连接池呢?

1. 关键字段分析

在各种连接池中都有几个比较通用的字段,可以先对这几个字段进行初步的了解

MaxIdleConn: 指的是连接池的大小

MaxConn: 指的是客户端最多可以开多少个连接,如果客户端并发很大, MaxIdleConn等于10,MaxConn等于20,此时连接可能存活20个,但是连接池中只会有10个,如果连接池满了,则关闭丢弃、关闭多余的连接

IdleConnTimeout: 一个连接如果超过IdleConnTimeout这个时间没有没重新利用,则会关闭这个连接

2. http client pool设计与源码

Golang版本: go1.19.5 linux/amd64 http1.1部分源码

重要:

这里我仅仅展示的是代码片段,并没有将整个代码展示出来,所以,在看到这篇文档的时候,需要打开你的电脑,打开对应的源码,参考阅读

2.1 关键字段解读

【连接池demo】

func main() {
    // 创建一个HTTP客户端
    client := &http.Client{
        Transport: &http.Transport{
            MaxIdleConns: 1000,
            MaxIdleConnsPerHost: 100,
            MaxConnsPerHost: 100,
        },
    }
    client.Get("https://www.baidu.com")
}

针对每一个server,客户端都会缓存当前server对应的连接池(基于map缓存),http client有几个控制字段

  • MaxIdleConns: 客户端会和各个server建立连接,那么这里就是总连接池大小

  • MaxIdleConnsPerHost:每一个server的连接池大小

  • MaxConnsPerHost:每一个server的最大连接数量

    他们之间的数学关系 MaxIdleConnsPerHost * n <= MaxIdleConns (n 代表服务端域名个数) MaxIdleConnsPerHost <= MaxConnsPerHost

    注意

    为什么在http里面没有设计MaxConns这样一个参数呢?我也没搞懂,如果要实现这样的功能,那么就每一个server创建一个http client

idleConn : 维护连接池里面的连接,是一个map数据类型,key是一个代表一个server,value是一个具体的连接对应的数组

idleconn.pngidleconn.png

【整体概览getConn方法源码】

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
    w := &wantConn{}    // 尝试从连接池中获取连接
    if delivered := t.queueForIdleConn(w); delivered {}    // 连接池中没有连接,则需要自己创建
    t.queueForDial(w)    // 等待连接就绪,针对的是无法从连接池获取连接的情况,才需要在这里等待
    select {
    case <-w.ready:
    }
}

先思考如下几个问题,等我们分析完成所有逻辑之后,再来看看这几个问题

  1. 如果连接池中不存在连接应该如何处理?
  2. 如果达到最大连接数超过了MaxConnsPerHost怎么办?
  3. 连接用完之后,放回到连接池,发现连接池满了,或者达到最大连接了,如何处理?

2.2连接获取逻辑

通过http client发送http请求,会通过如下方法获取连接

// 【go/src/net/http/transport.go】
pconn, err := t.getConn(treq, cm)

这个getConn方法中,会将连接包装成wantConn结构体

w := &wantConn{
    cm:         cm,
    key:        cm.key(),
    ctx:        ctx,
    ready:      make(chan struct{}, 1),
    beforeDial: testHookPrePendingDial,
    afterDial:  testHookPostPendingDial,
}

这里面有一个重要的字段ready, 这是一个golang channel,用于通知getConn协程,告诉getConn已经有一个连接准备就绪,你可以获取连接了

// 【go/src/net/http/transport.go】 getConn方法
case <-w.ready:
    // 省略代码...
    return w.pc, w.err

哪些场景代表这个w.ready有事件了呢?

  1. 一个新连接被创建出来,准备就绪了
  2. 一个请求完成,需要将连接要放到连接池,也会优先传递给正在等待使用的客户端

2.2.1 从连接池获取连接

我们先看一下主逻辑,这里只考虑连接池中还有空闲的连接的情况

Transport.queueForIdleConn中获取连接

// 【go/src/net/http/transport.go】 queueForIdleConn方法// 1. 从map中获取当前server的连接池, 然后从连接池中获取连接
if list, ok := t.idleConn[w.key]; ok {
    // ...
    // 连接池中有数据
    for len(list) > 0 && !stop {
        // 这里可以看到拿的是数组最后的一个元素
        pconn := list[len(list)-1]
        // 如果连接可用的话,
        // 会将连接放到wantConn,并且关闭ready,getConn协程会收到通知,获取连接
        delivered = w.tryDeliver(pconn, nil)
    }
}

什么样的连接表示可用呢?

  1. 存活时间没有超过IdleConnTimeout
  2. 底层persistConn没有被关闭

【tryDeliver】逻辑

tryDeliver其实很简单,就是将连接给到wantConn,并且关闭w.readychannel,用于通知getConn去拿连接

func (w *wantConn) tryDeliver(pc *persistConn, err error) bool {
    w.mu.Lock()
    defer w.mu.Unlock()    if w.pc != nil || w.err != nil {
        return false
    }    w.pc = pc
    w.err = err
    if w.pc == nil && w.err == nil {
        panic("net/http: internal error: misuse of tryDeliver")
    }
    close(w.ready)
    return true
}

2.2.2 连接池为空如何处理

如果连接池空了,那么就没办法从连接池中获取连接,这是时候只能去创建一个新的连接,这里暂时不考虑达到最大连接数的情况

//【go/src/net/http/transport.go】 getConn方法// 这里会去创建一个新的连接
t.queueForDial(w)

在t.queueForDial方法中会开一个协程去创建一个连接

go t.dialConnFor(w)func (t *Transport) dialConnFor(w *wantConn) {
    // 创建连接
    pc, err := t.dialConn(w.ctx, w.cm)
    // 这里就和从连接池中获取连接一样了,通知getConn获取连接
    delivered := w.tryDeliver(pc, err)
}

2.2.3 超过最大连接数如何处理

在dialConnFor中会判断连接的数量是否超过MaxConnsPerHost的限制,如果超过了,则不是创建新连接

func (t *Transport) queueForDial(w *wantConn) {
    if t.MaxConnsPerHost <= 0 {
        // 省略创建新连接代码
    }    t.connsPerHostMu.Lock()
    defer t.connsPerHostMu.Unlock()    if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
        // 省略创建新连接代码
    }
    
    // 会将当前key放入等待队列中,当有人释放连接之后,会唤醒getConn获取连接
    q := t.connsPerHostWait[w.key]
    q.cleanFront()
    q.pushBack(w)
    t.connsPerHostWait[w.key] = q
}

那么不创建新连接,那要怎么办呢?我们再回到getConn方法中, 这里会有一个select在等待连接创建(w.ready),或者等到超时(req.Context().Done()),或者等待客户端取消请求件(req.Cancel)

select {
case <-w.ready:
case <-req.Cancel:
case <-req.Context().Done():
case err := <-cancelc:
}

2.3 连接归还

当客户端请求完成之后,会将连接返回给连接池,返回连接池会出现如下几种情况

  1. 有客户端在阻塞等待连接释放(w.ready),会尝试将连接优先给到正在等待的客户端

  2. 没有客户端在等待连接,在优先考虑将连接方法连接池中

  3. 连接池满了,也没有客户端在阻塞等待连接,此时可以将连接关闭了(TCP四次挥手)

func (t *Transport) putOrCloseIdleConn(pconn *persistConn) {
    // 如果无法放到连接池,或者连接没办法被复用,则关闭连接
    if err := t.tryPutIdleConn(pconn); err != nil {
        pconn.close(err)
    }
}

将连接发送给正在等待的客户端

// 【tryPutIdleConn】
// 根据key查找是否有正在等待的客户端
if q, ok := t.idleConnWait[key]; ok {
    if pconn.alt == nil {
        for q.len() > 0 {
            // 从队列的头部获取一个wantConn
            w := q.popFront()
            // 将准备好的连接发送给等待的客户端
            if w.tryDeliver(pconn, nil) {
                done = true
                break
            }
        }
    } 
}
// 【tryPutIdleConn】idles := t.idleConn[key]
// 连接池满了,直接返回错误
if len(idles) >= t.maxIdleConnsPerHost() {
    return errTooManyIdleHost
}
// 连接池还没满,则将连接返回给连接池
t.idleConn[key] = append(idles, pconn) 

3. 其他细节

MaxIdleConns是如何管理所有的连接的?

每一个server对应一个连接池,然后MaxIdleConns是管理所有server的连接池的,如果超过了,也是需要移除的

// 【tryPutIdleConn】
t.idleConn[key] = append(idles, pconn)
// 把连接放到lru缓存
t.idleLRU.add(pconn)
// 如果发现总连接t.idleLRU.len() > t.MaxIdleConns, 此时会移除LRU中最后那个元素
if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns {
    oldest := t.idleLRU.removeOldest()
    oldest.close(errTooManyIdle)
    // 移除连接
    // 1. 从idleLRU缓存中移除
    // 2. 从idleConn[key]移除对应server的连接
    t.removeIdleConnLocked(oldest)
}

4. 总结

连接池.PNG连接池.PNG

关于 【连接获取】【连接归还】 的逻辑,可以通过上面的一张图总结

我们回到开始的问题

  1. 创建的连接应该放在哪里?数组、链表、channel

Golang的连接池大部分都会放到slice中(http连接池、数据库连接池),然后使用互斥锁保证其并发安全,像mongoDB的连接池是使用链表实现的。当然如果想要使用channel实现,也是可以的

  1. 为什么官方都不基于sync.Pool来实现连接池呢?
  1. sync.Pool没有固定大小,连接池需要

  2. 会被垃圾回收清理,清理的时候没有任何通知。连接池不能由垃圾回收管理,而应该由用户明确回收和管理

  3. sync.Pool更多被使用在对象池、内存池的管理

本篇文章,主要是基于Golang http1.1连接池的源码分析了,连接池的设计原理。但是本文更注重关键源码以及关键原理的解读,如果读者想要看懂本篇文章,还是需要对照相关代码进行对照阅读的。下一篇,我将手把手写一个连接池。

5. 扩展

  • 如果看懂了这篇文档,那么你可以尝试看一下database/sql中连接池的设计与实现么?

  • 更进一步设计属于自己的一个连接池

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/708461.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring 面向切面编程AOP 详细讲解

1. Spring 面向切面编程AOP 详细讲解 @目录1. Spring 面向切面编程AOP 详细讲解每博一文案2. AOP介绍说明2.1 AOP的七大术语2.2 AOP 当中的 切点表达式3. 使用Spring 对 AOP 的实现使用3.1 准备工作3.2 Spring 基于AspectJ的AOP注解式开发3.2.1 实现步骤3.2.2 各个通知类型的说…

vulnhub - NYX: 1

标准攻击链vulnhub - NYX: 1 描述 这是一个简单的盒子,非常基本的东西。 它是基于vmware的,我不知道它是否可以在VB上运行,如果你愿意的话可以测试一下。 /home/$user/user.txt 和 /root/root.txt 下有 2 个标志。 信息收集 NYX靶ip:192.168.157.159 nmap -sT -sV -sC -O -p…

Vue3学习之项目工程初步了解

1.介绍项目工程,如在vscode中下图

因更懂你而更热爱,智慧PC助力当代每一位“霞客”行

“大丈夫当朝碧海而暮苍梧”,从小就立志游历天下的徐霞客,自公元1613年5月19日这天开始了一生的披荆斩棘。三十年时间素履而往,所有足迹凝练出了一本《徐霞客游记》,在地理学、地质学、文学、历史学中都占有重要一席。因热爱而出发,如果他生在当下,一定是中国最大的旅游博…

Pyqt6PySide6 事件与事件的处理函数

什么是事件? 事件是程序收到外界的输入,处于某种状态时自动发送的信号。事件有固定的类型,每种类型有自己的处理函数,用户只要重写这些函数,即可达到特定的目的。通过事件可以用一个控件监测另外一个控件,并可过滤被监测控件发出的事件。 事件的类型与处理函数 事件的概念…

C语言编程题练习 (从初识到入门再到进阶)修正版

C语言编程题练习 📔(从初识到入门再到进阶) 小飞机 🛬#include<stdio.h>int main() {printf(" ** \n");printf(" ** \n");printf("***************\n");printf("***************\n");printf("…

生物医学顶刊论文(JBHI-2024):TransFOL:药物相互作用中复杂关系推理的逻辑查询模型

(2024.5.17)JBHI-TransFOL:药物相互作用中复杂关系推理的逻辑查询模型 论文题目:TransFOL: A Logical Query Model for Complex Relational Reasoning in Drug-Drug Interaction 论文期刊:Journal of Biomedical and Health Informatics (JBHI) 论文地址:https://ieeexplor…

2024-05-18:用go语言,给定一个从 0 开始的字符串 s,以及两个子字符串 a 和 b,还有一个整数 k。 定义一个“美丽下标”,当满足以下条件时: 1.找到字符串 a 在字符串 s 中的位

2024-05-18:用go语言,给定一个从 0 开始的字符串 s,以及两个子字符串 a 和 b,还有一个整数 k。 定义一个“美丽下标”,当满足以下条件时: 1.找到字符串 a 在字符串 s 中的位置,且该位置范围为 0 <= i <= s.length - a.length。 2.找到字符串 b 在字符串 s 中的位置…

Windows 环境多服务文件同步

多服务器文件同步的目标是将 SSCMS 系统生成的站点文件以及图片、附件等站点资源文件同步至独立服务器并对外提供访问,以避免直接将 SSCMS 系统暴露在外网。 我们推荐使用免费开源的 rsync 软件进行跨服务器文件同步,除了 rsync 软件之外,您也可以使用其他第三方软件进行文件…

【Python】强化学习SARSA走迷宫

之前有实现Q-Learning走迷宫,本篇实现SARSA走迷宫。 Q-Learning是一种off-policy算法,当前步采取的决策action不直接作用于环境生成下一次state,而是选择最优的奖励来更新Q表。 更新公式:SARSA是一种on-policy算法,当前步采取的策略action既直接作用于环境生成新的state,…

编译mmdetection3d时,无root权限下为虚拟环境单独创建CUDA版本

在跑一些深度学习代码的时候,如果需要使用mmdetection3d框架,下载的pytorch的cudatoolkit最好需要和本机的cuda版本是一样的,即输入nvcc -V命令后显示的版本一样。 但是如果是在学校里,一般是服务器管理员装的cuda驱动是啥版本,cudatoolkit就是啥版本,且非root用户改变不…

原型设计工具

当下主流的原型设计工具有Axure、Figma、Pixso、墨刀。 一.Axure Axure,全称叫做 Axure RP 。Axure是一款功能强大的原型设计工具,被广泛用于用户体验(UX)和用户界面(UI)设计。这算是目前原型图领域最常用的工具,有着丰富的交互设计功能,可创建各种高保真、低保真的交互…