客户端禁用Keep-Alive, 服务端开启Keep-Alive,会怎么样?

简介: 最近部署的web程序,服务器上出现不少time_wait的tcp连接状态,占用了tcp端口,花费几天时间排查。

01

非常规的行为形成的短连接


我手上有个项目,由于历史原因,客户端禁用了Keep-Alive,服务端默认开启了Keep-Alive,如此一来协商复用连接失败, 客户端每次请求会使用新的TCP连接, 也就是回退为短连接。


6a2150709ac0dbf31ed8d906df9e5d62.png


客户端强制禁用Keep-Alive


package main
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)
func main() {
    tr := http.Transport{
        DisableKeepAlives: true,
    }
    client := &http.Client{
        Timeout:   10 * time.Second,
        Transport: &tr,
    }
    for {
        requestWithClose(client)
        time.Sleep(time.Second * 1)
    }
}
func requestWithClose(client *http.Client) {
    resp, err := client.Get("http://10.100.219.9:8081")
    if err != nil {
        fmt.Printf("error occurred while fetching page, error: %s", err.Error())
        return
    }
    defer resp.Body.Close()
    c, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("Couldn't parse response body. %+v", err)
    }
    fmt.Println(string(c))
}


web服务端默认开启Keep-Alive


package main
import (
    "fmt"
    "log"
    "net/http"
)
// 根据RemoteAddr 知道客户端使用的持久连接
func IndexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
    w.Write([]byte("ok"))
}
func main() {
    fmt.Printf("Starting server at port 8081\n")
    // net/http 默认开启持久连接
    if err := http.ListenAndServe(":8081", http.HandlerFunc(IndexHandler)); err != nil {
        log.Fatal(err)
    }
}


从服务端的日志看,确实是短连接。


receive a request from: 10.22.34.48:54722 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54724 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54726 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54728 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54731 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54733 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54734 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54738 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54740 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54741 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54743 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54744 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.34.48:54746 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]


02

谁是主动断开方?


我想当然的以为 客户端是主动断开方,被现实啪啪打脸。


某一天服务器上超过300的time_wait报警,告诉我这tmd是服务器主动终断连接。


常规的TCP4次挥手, 主动断开方会进入time_wait状态,等待2MSL后释放占用的SOCKET


d749fbdd1e119a1c9dc7ae3ee4c22da6.png


以下是从服务器上tcpdump抓取的tcp连接信息。


b1152d424c397bb7692f83025a3aa0c6.png


2,3红框显示:


     Server端先发起TCP的FIN消息, 之后Client回应ACK确认收到Server的关闭通知; 之后Client再发FIN消息,告知现在可以关闭了, Server端最后发ACK确认收到,并进入time_wait状态,等待2MSL的时间关闭Socket。


特意指出,红框1表示TCP双端同时关闭[1],此时会在Client,Server同时留下time_wait痕迹,发生概率较小。


03

没有源码说个串串


此种情况是服务端主动关闭,我们翻一翻golang httpServer的源码


http.ListenAndServe(":8081")


server.ListenAndServe()srv.Serve(ln)


go c.serve(connCtx) 使用go协程来处理每个请求


服务器连接处理请求的简略源码如下:


func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    defer func() {
    if !c.hijacked() {
            c.close()   // go协程conn处理请求的协程退出时,主动关闭底层的TCP连接
            c.setState(c.rwc, StateClosed, runHooks)
        }
    }()
  ......
    // HTTP/1.x from here on.
    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()
    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
    for {
        w, err := c.readRequest(ctx)
.....
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest()
        if !w.shouldReuseConnection() {
            if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
                c.closeWriteAndWait()
            }
            return
        }
        c.setState(c.rwc, StateIdle, runHooks)
        c.curReq.Store((*response)(nil))
        if !w.conn.server.doKeepAlives() {
            // We're in shutdown mode. We might've replied
            // to the user without "Connection: close" and
            // they might think they can send another
            // request, but such is life with HTTP/1.1.
            return
        }
        if d := c.server.idleTimeout(); d != 0 {
            c.rwc.SetReadDeadline(time.Now().Add(d))
            if _, err := c.bufr.Peek(4); err != nil {
                return
            }
        }
        c.rwc.SetReadDeadline(time.Time{})
    }
}


我们需要关注


for循环,表示尝试复用该conn,用于处理迎面而来的请求

w.shouldReuseConnection() = false, 表明读取到ClientConnection:Close标头,设置closeAfterReply=true,跳出for循环,协程即将结束,结束之前执行defer函数,defer函数内close该连接


c.close()
......
// Close the connection.
func (c *conn) close() {
 c.finalFlush()
 c.rwc.Close()
}


如果 w.shouldReuseConnection() = true,则将该连接状态置为idle, 并继续走for循环,复用连接,处理后续请求。


04

我的收获


1. TCP 4次挥手的八股文


2.  短连接的效应:主动关闭方会在机器上产生 time_wait状态,需要等待2MSL时间才会关闭SOCKET


3.golang http keep-alive复用tcp连接的源码级分析


4.tcpdump抓包的姿势

相关文章
|
缓存 中间件 测试技术
SOME/IP协议实践指南:精选开发与测试工具解析
SOME/IP协议实践指南:精选开发与测试工具解析
608 0
|
12月前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
218 2
|
12月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
196 3
|
安全 Go 开发者
掌握 Go 语言的依赖关系管理
【8月更文挑战第31天】
217 0
|
JavaScript
【Deepin 20系统】Jupyter notebook解决ValueError: Please install Node.js and npm before continuing installa
文章讨论了在Deepin 20系统上安装Jupyter Notebook的debug插件时出现的"ValueError: Please install Node.js and npm before continuing installation"错误,并提供了使用conda安装Node.js的解决方法。
331 1
|
Linux 网络安全 开发工具
【GitLab私有仓库】在Linux上用Gitlab搭建自己的私有库并配置cpolar内网穿透
【GitLab私有仓库】在Linux上用Gitlab搭建自己的私有库并配置cpolar内网穿透
245 0
|
XML Go 数据格式
【微信公众号开发】基于golang的公众号开发——接入消息自动回复接口
【微信公众号开发】基于golang的公众号开发——接入消息自动回复接口
721 0
|
存储 缓存 小程序
微信小程序-缓存
微信小程序-缓存
259 0
|
Kubernetes 负载均衡 Perl
kube-proxy iptables 模式源码分析
本文主要讲了 kube-proxy iptables 模式的实现,可以看到其中的 iptables 规则是相当复杂的,在实际环境中尽量根据已有服务再来梳理整个 iptables 规则链就比较清楚了,笔者对于 iptables 的知识也是现学的,文中如有不当之处望指正。
1927 0
|
消息中间件 Linux 数据安全/隐私保护
【图解RabbitMQ-4】Docker安装RabbitMQ详细图文过程
【图解RabbitMQ-4】Docker安装RabbitMQ详细图文过程
642 0