golang长连接的误用
误用一:忘记读取响应的body
由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接(同时产生大量处于transport.go的readLoop和writeLoop的协程)
在linux下运行下面的代码:
package mainimport ("fmt""html""log""net""net/http""time"
)func startWebserver() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))})go http.ListenAndServe(":8080", nil)}func startLoadTest() {count := 0for {resp, err := http.Get("http://localhost:8080/")if err != nil {panic(fmt.Sprintf("Got error: %v", err))}resp.Body.Close()log.Printf("Finished GET request #%v", count)count += 1}}func main() {startWebserver()startLoadTest()}
在程序运行时另外开一个终端运行下面的命令:
netstat -n | grep -i 8080 | grep -i time_wait | wc -l
你会看到TIME_WAIT数量在持续增长
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
166
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
231
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
293
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
349
解决办法: 读取响应的body
更改startLoadTest()函数,添加下面的代码:
func startLoadTest() {for {...if err != nil {panic(fmt.Sprintf("Got error: %v", err))}io.Copy(ioutil.Discard, resp.Body) // <-- add this lineresp.Body.Close()...}}
现在再次运行netstat -n | grep -i 8080 | grep -i time_wait | wc -l,你会发现TIME_WAIT状态的连接数为0
误用二:空闲连接最大数量设置太小,实际连接数量超过连接池的限制
连接的数量超过连接池的限制导致出现大量TIME_WAIT状态的连接
这种情况时由于持续超过连接池导致许多短连接被打开。
请看下面的代码:
package mainimport ("fmt""html""io""io/ioutil""log""net/http""time"
)func startWebserver() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {time.Sleep(time.Millisecond * 50)fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))})go http.ListenAndServe(":8080", nil)}func startLoadTest() {count := 0for {resp, err := http.Get("http://localhost:8080/")if err != nil {panic(fmt.Sprintf("Got error: %v", err))}io.Copy(ioutil.Discard, resp.Body)resp.Body.Close()log.Printf("Finished GET request #%v", count)count += 1}}func main() {// start a webserver in a goroutinestartWebserver()for i := 0; i < 100; i++ {go startLoadTest()}time.Sleep(time.Second * 2400)}
在另外一个终端运行netstat,尽管响应已经被读取,TIME_WAIT的连接数还是持续增加
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
166
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
231
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
293
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
349
什么是TIME_WAIT状态呢?
就是当我们创建大量短连接时,linux内核的网络栈保持连接处于TIME_WAIT状态,以避免某些问题。
例如:避免来自一个关闭的连接延迟的包被后来的连接所接收。并发连接被用地址,端口,序列号等其他机制所隔离开。
为什么这么多的TIME_WAIT端口?
默认情况下,Golang的http client会做连接池。他会在完成一个连接请求后把连接加到一个空闲的连接池中。如果你想在这个连接空闲超时前发起另外一个http请求,它会复用现有的连接。
这会把总socket连接数保持的低一些,直到连接池满。如果连接池满了,它会创建一个新的连接来发起http请求。
那这个连接池有多大呢?看看transport.go:
var DefaultTransport RoundTripper = &Transport{... MaxIdleConns: 100,IdleConnTimeout: 90 * time.Second,...
}// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2
MaxIdleConns:100设置连接池的大小为100个连接IdleConnTimeOut被设置为90秒,意味着一个连接在连接池里最多保持90秒的空闲时间,超过这个时间将会被移除并关闭DefaultMaxIdleConnsPerHost = 2这个设置意思时尽管整个连接池是100个连接,但是每个host只有2个。
上面的例子中有100个gooutine尝试并发的对同一个主机发起http请求,但是连接池只能存放两个连接。所以,第一轮完成请求时,2个连接保持打开状态。但是剩下的98个连接将会被关闭并进入TIME_WAIT状态。
因为这在一个循环中出现,所以会很快就积累上成千上万的TIME_WAIT状态的连接。最终,会耗尽主机的所有可用端口,从而导致无法打开新的连接。
修复: 增加http client的连接池大小
import (..
)var myClient *http.Clientfunc startWebserver() {... same code as before}func startLoadTest() {... for {resp, err := myClient.Get("http://localhost:8080/") // <-- use a custom client with custom *http.Transport... everything else is the same}}func main() {// Customize the Transport to have larger connection pooldefaultRoundTripper := http.DefaultTransportdefaultTransportPointer, ok := defaultRoundTripper.(*http.Transport)if !ok {panic(fmt.Sprintf("defaultRoundTripper not an *http.Transport"))}defaultTransport := *defaultTransportPointer // dereference it to get a copy of the struct that the pointer points todefaultTransport.MaxIdleConns = 100defaultTransport.MaxIdleConnsPerHost = 100myClient = &http.Client{Transport: &defaultTransport}// start a webserver in a goroutinestartWebserver()for i := 0; i < 100; i++ {go startLoadTest()}time.Sleep(time.Second * 2400)}
当然,如果你的并发要求高,可以把连接池的数量改的更大些。
但是这样没有根本解决问题,因为go的http.Client在连接池被占满并且所有连接都在被使用的时候会创建一个新的连接。
具体可以看代码,http.Client处理请求的核心在用它的transport获取一个连接:
// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {//...省略部分代码// Get the cached or newly-created connection to either the// host (for http or https), the http proxy, or the http proxy// pre-CONNECTed to https server. In any case, we'll be ready// to send it requests.pconn, err := t.getConn(treq, cm) //看这里if err != nil {t.setReqCanceler(req, nil)req.closeBody()return nil, err}var resp *Responseif pconn.alt != nil {// HTTP/2 path.t.setReqCanceler(req, nil) // not cancelable with CancelRequestresp, err = pconn.alt.RoundTrip(req)} else {resp, err = pconn.roundTrip(treq)}if err == nil {return resp, nil}//...省略部分代码}
getConn方法的实现核心如下:
// getConn dials and creates a new persistConn to the target as
// specified in the connectMethod. This includes doing a proxy CONNECT
// and/or setting up TLS. If this doesn't return an error, the persistConn
// is ready to write requests to.
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {req := treq.Requesttrace := treq.tracectx := req.Context()if trace != nil && trace.GetConn != nil {trace.GetConn(cm.addr())}w := &wantConn{cm: cm,key: cm.key(),ctx: ctx,ready: make(chan struct{}, 1),beforeDial: testHookPrePendingDial,afterDial: testHookPostPendingDial,}defer func() {if err != nil {w.cancel(t, err)}}()// Queue for idle connection.if delivered := t.queueForIdleConn(w); delivered { //注意这一行代码,看函数名意思是在Idle连接队列里等待,如果执行成功就拿到一个连接,如果拿不到连接就跳过下面这部分代码pc := w.pc// Trace only for HTTP/1.// HTTP/2 calls trace.GotConn itself.if pc.alt == nil && trace != nil && trace.GotConn != nil {trace.GotConn(pc.gotIdleConnTrace(pc.idleAt))}// set request canceler to some non-nil function so we// can detect whether it was cleared between now and when// we enter roundTript.setReqCanceler(req, func(error) {})return pc, nil}cancelc := make(chan error, 1)t.setReqCanceler(req, func(err error) { cancelc <- err })// Queue for permission to dial.t.queueForDial(w) /拿不到连接就放入等待拨号的队列//...省略部分代码
}
我们再看queueForDial方法的实现:
// queueForDial queues w to wait for permission to begin dialing.
// Once w receives permission to dial, it will do so in a separate goroutine.
func (t *Transport) queueForDial(w *wantConn) {w.beforeDial()if t.MaxConnsPerHost <= 0 { //看这里,如果这个值小于等于0,就直接创建连接了,我们之前没有设置这个选项导致的go t.dialConnFor(w)return}t.connsPerHostMu.Lock()defer t.connsPerHostMu.Unlock()if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {if t.connsPerHost == nil {t.connsPerHost = make(map[connectMethodKey]int)}t.connsPerHost[w.key] = n + 1go t.dialConnFor(w)return}if t.connsPerHostWait == nil {t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)}q := t.connsPerHostWait[w.key]q.cleanFront()q.pushBack(w)t.connsPerHostWait[w.key] = q
}
误用三:没有重用同一个长连接的http.Transport对象
我们的一个服务是用Go写的,在测试的时候发现几个小时之后它就会core掉,而且core的时候没有打出任何堆栈信息,简单分析后发现该服务中的几个HTTP服务的连接数不断增长,而我们的开发机的fd limit只有1024,当该服务所属进程的连接数增长到系统的fd limit的时候,它被操作系统杀掉了。。。
HTTP Connection中连接未被释放的问题在https://groups.google.com/forum/#!topic/golang-nuts/wliZf2_LUag和https://groups.google.com/forum/#!topic/golang-nuts/tACF6RxZ4GQ都有提到。
这个服务中,我们会定期向一个HTTP服务器发起POST请求,因为请求非常不频繁,所以想采用短连接的方式去做。请求代码大概长这样:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
运行这段代码一段时间后会发现,该进程下面有一堆ESTABLISHED状态的连接(用lsof -p pid查看某进程下的所有fd),因为每次DoRequest函数被调用后,都会新建一个TCP连接,如果对端不先关闭该连接(对端发FIN包)的话,我们这边即便是调用了resp.Body.Close()函数仍然不会改变这些处于ESTABLISHED状态的连接。为什么会这样呢?只有去源代码一探究竟了。
Golang的net包中client.go, transport.go, response.go和request.go这几个文件中实现了HTTP Client。当应用层调用client.Do()函数后,transport层会首先找与该请求相关的已经缓存的连接(这个缓存是一个map,map的key是请求方法、请求地址和proxy地址,value是一个叫persistConn的连接描述结构),如果已经有可以复用的旧连接,就会在这个旧连接上发送和接受该HTTP请求,否则会新建一个TCP连接,然后在这个连接上读写数据。当client接受到整个响应后,如果应用层没有
调用response.Body.Close()函数,刚刚传输数据的persistConn就不会被加入到连接缓存中,这样如果您在下次发起HTTP请求的时候,就会重新建立TCP连接,重新分配persistConn结构,这是不调用response.Body.Close()的一个副作用。
如果不调用response.Body.Close()还存在一个问题。如果请求完成后,对端关闭了连接(对端的HTTP服务器向我发送了FIN),如果这边不调用response.Body.Close(),那么可以看到与这个请求相关的TCP连接的状态一直处于CLOSE_WAIT状态(还记得么?CLOSE_WAIT是连接的半开半闭状态,它是收到对方的FIN并且我们也发送了ACK,但是本端还没有发送FIN到对端,如果本段不调用close关闭连接,那么连接将一直处于
CLOSE_WAIT状态,不会被系统回收)。
调用了response.Body.Close()就万无一失了么?上面代码中也调用了body.Close()为什么还会有很多ESTABLISHED状态的连接呢?因为在函数DoRequest()的每次调用中,我们都会新创建transport和client结构,当HTTP请求完成并且接收到响应后,如果对端的HTTP服务器没有关闭连接,那么这个连接会一直处于ESTABLISHED状态。如何解呢?
有两个方法:
第一个方法是用一个全局的client,函数DoRequest()中每次都只在这个全局client上发送数据。但是如果我就想用短连接呢?用方法二。
第二个方法是在transport分配时将它的DisableKeepAlives参数置为true,此时发送的请求头里会包含Connection: close,像下面这样:
| 1 2 3 4 5 6 7 8 9 10 |
|
从transport.go:L908可以看到,当应用层调用resp.Body.Close()时,如果DisableKeepAlives被开启,那么transport自动关闭本端连接。而不将它加入到连接缓存中。
补充一下,在dialTimeout函数中disable tcp连接的keepalive选项是不可行的,它只是设置TCP连接的选项,不会影响到transport中对连接的控制。
| 1 2 3 4 5 6 7 8 9 10 11 |
|
--end--
相关文章:
golang长连接的误用
误用一:忘记读取响应的body 由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接(同时产生大量处于transport.go的readLoop和writeLoop的协程) 在linux下运行下面的代码: package mainimport ("fmt""html"&qu…...
Springboot @Validate @Valid 基于复杂嵌套对象的参数校验示例
Springboot Validate Valid 基于复杂嵌套对象的参数校验示例 复杂对象 Data public class Object1 {Length(max 50,message "长度不能超过50位字符")NotBlank(message "名称不能为空")private String name;NotNull(message "不能为空")pri…...
算力共享下的,分级路由转发报文协议与通告
目录 网络双 SLA 约束 一、双SLA约束的定义与背景 二、双SLA约束的应用场景 三、双SLA约束的管理与实施 四、双SLA约束的优势与挑战 算力共享下的,分级路由转发报文协议与通告 基础设施即服务(IaaS)类 型算力资源 函数即服务(FaaS)类型算力服务 软件即服务(SaaS…...
滚动数组详解
滚动数组详解 何为滚动数组?滚动数组是如何优化空间的?交替滚动例题:来自某某轮廓线DP的题目 自我滚动(~~不如交替~~ 完结!!! ( 宇宙免责任书:我用的是C) 何为滚动数组? 什么是滚动…...
C 语言动态链表
线性结构->顺序存储->动态链表 一、理论部分 从起源中理解事物,就是从本质上理解事物。 -杜勒鲁奇 动态链表是通过结点(Node)的集合来非连续地存储数据,结点之间通过指针相互连接。 动态链表本身就是一种动态分配内存的…...
【Leetcode】二十、记忆化搜索:零钱兑换
文章目录 1、记忆化搜索2、leetcode509:斐波那契数列3、leetcode322:零钱兑换 1、记忆化搜索 也叫备忘录,即把已经计算过的结果存下来,下次再遇到,就直接取,不用重新计算。目的是以减少重复计算。 以前面提…...
json数据格式 继续学习
1.定义 轻量级的数据交互格式,可以按照json数据格式去组织和封装数据。 本质是一个带有特定格式的字符串。 2.功能 负责不同编程语言中的数据传递和交互。 3.json数据格式转化 """ 演示json数据和python字典之间的转换 """ impor…...
gradle 构建项目添加版本信息
gradle 构建项目添加版本信息,打包使用 spring boot 的打包插件 build.gradle 配置文件 bootJar {manifest {attributes(Project-Name: project.name,Project-Version: project.version,"project-Vendor": "XXX Corp","Built-By": &…...
vue3 学习笔记17 -- 基于el-menu封装菜单
vue3 学习笔记17 – 基于el-menu封装菜单 前提条件:组件创建完成 配置路由 // src/router/index.ts import { createRouter, createWebHashHistory } from vue-router import type { RouteRecordRaw } from vue-router export const Layout () > import(/lay…...
使用 Redis 实现验证码、token 的存储,用自定义拦截器完成用户认证、并使用双重拦截器解决 token 刷新的问题
可以看一下我以前做过的笔记:黑马点评 短信登录部分 基于session实现登录流程 1.发送验证码 用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号 如果手机号合法,后台此时生成对应的验…...
反转链表 - 力扣(LeetCode)C语言
206. 反转链表 - 力扣(LeetCode)( 点击前面链接即可查看题目) /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode* reverseList(struct ListNode* head) {if(head NULL)…...
【Linux】进程间通信(1):进程通信概念与匿名管道
人与人之间是如何通信的?举个简单的例子,假如我是月老,我要为素不相识的但又渴望爱情的男女两方牵红线。我需要收集男方的信息告诉女方,收集女方的信息告诉男方,然后由男女双方来决定是否继续。对于他们而言࿰…...
Spring从入门到精通 01
文章目录 1. 依赖注入 (Dependency Injection, DI)2. 面向切面编程 (Aspect-Oriented Programming, AOP)3. 事务管理4. 简化 JDBC 开发5. 集成各种框架和技术6. 模块化和扩展性:主要的 Spring 模块:Core Container:AOP 模块:Data …...
C语言经典习题25
冒泡排序 对一维数组进行升序排序,然后在数组中输入20个数,将排序后的结果打印输出。 #include<stdio.h> #define N 20 int main() {int a[N];int i;for(i0;i<N;i) //初始化数组的数 {scanf("%d",&a);}for(i0;…...
2-47 基于matlab的时域有限差分法(FDTD法)拉夫等效原理进行时谐场外推
基于matlab的时域有限差分法(FDTD法)拉夫等效原理进行时谐场外推。外推边界距离吸收边界的距离、电磁场循环、傅立叶变换提起幅值和相位、各远区剖分点电场、方向系数计算等操作,得出可视化结果。程序已调通,可直接运行。 2-47 时域有限差分法(FDTD法) 拉…...
JupyterNotebook快捷键 自用
COMMAND MODE —————————————————————————————— Up Down cells的上下选择 A B 在上/下方插入cell C V X 复制/粘贴/剪切cell 双击D 删除所选cell Z 恢复被删除的cell 双击I Interrupt中断内核 Shift Enter 运行cell并选择下方 EDIT MODE ———…...
【我的OpenGL学习进阶之旅】讲一讲GL_TEXTURE_2D和GL_TEXTURE_EXTERNAL_OES的区别
在使用OpenGL ES进行图形图像开发时,我们常使用GL_TEXTURE_2D纹理类型,它提供了对标准2D图像的处理能力。这种纹理类型适用于大多数场景,可以用于展示静态贴图、渲染2D图形和进行图像处理等操作。 另外,有时我们需要从Camera或外部视频源读取数据帧并进行处理。这时,我们…...
Makefile 如何将生成的 .o 文件放到指定文件夹
研究了不少文章,我行通了一个,但是也不全,目前只能适用当前文件夹,如果源文件有子文件夹处理不了,还得继续研究。很多人说编译完把O文件移动走或者直接删掉。我想说的是不符合我的要求,移走或者删除O文件&a…...
聊一聊知识图谱结合RAG
因为最近在做一些关于提高公司内部使用的聊天机器人的回答准确率,并且最近微软官方也是开源了一下graphrag的源码,所以想聊一聊这个知识图谱结合rag。 rag在利用私有数据增强大模型回答的领域是一种比较典型的技术,也就是我们提出问题的时候&…...
Java面试锦集 之 一、Java基础(1)
一、Java基础(1) 1.final 关键字的作用? 修饰变量: 一旦被赋值,就不能再被修改,保证了变量值的稳定性。 例: final int NUMBER 10; //之后就不能再改变 NUMBER 的值了。修饰方法:…...
利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...
江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Pinocchio 库详解及其在足式机器人上的应用
Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库,专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性,并提供了一个通用的框架&…...
初探Service服务发现机制
1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能:服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源…...
