南京做网站的有哪些,手机怎么注册自己的网站,网站文章不显示,小学生做的网站误用一#xff1a;忘记读取响应的body
由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接#xff08;同时产生大量处于transport.go的readLoop和writeLoop的协程#xff09;
在linux下运行下面的代码:
package mainimport (fmthtml忘记读取响应的body
由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接同时产生大量处于transport.go的readLoop和writeLoop的协程
在linux下运行下面的代码:
package mainimport (fmthtmllognetnet/httptime
)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数量在持续增长
rootmyhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
166
rootmyhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
231
rootmyhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
293
rootmyhost:/# 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 (fmthtmlioio/ioutillognet/httptime
)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 Transports
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost 2MaxIdleConns: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, well 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 doesnt 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 func dialTimeout(network, addr string) (net.Conn, error) { return net.DialTimeout(network, addr, time.Second*POST_REMOTE_TIMEOUT) } func DoRequest(URL string) xx, error { transport : http.Transport{ Dial: dialTimeout, } client : http.Client{ Transport: transport, } content : RequestContent{} // fill content here postStr, err : json.Marshal(content) if err ! nil { return nil, err } resp, err : client.Post(URL, application/json, bytes.NewBuffer(postStr)) if err ! nil { return nil, err } defer resp.Body.Close() body, err : ioutil.ReadAll(resp.Body) if err ! nil { return nil, err } // receive body, handle it } 运行这段代码一段时间后会发现该进程下面有一堆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层会首先找与该请求相关的已经缓存的连接这个缓存是一个mapmap的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 : http.Transport{ Dial: dialTimeout, DisableKeepAlives: true, } client : http.Client{ Transport: transport, } // ... 从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 func dialTimeout(network, addr string) (net.Conn, error) { conn, err : net.DialTimeout(network, addr, time.Second*POST_REMOTE_TIMEOUT) if err ! nil { return conn, err } tcp_conn : conn.(*net.TCPConn) tcp_conn.SetKeepAlive(false) return tcp_conn, err }
--end--