客户端禁用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协议实践指南:精选开发与测试工具解析
778 0
|
弹性计算 负载均衡 容灾
阿里云服务器地域及可用区选择攻略(考虑七大影响因素)
阿里云服务器地域怎么选择?阿里云服务器可用区怎么选择?地域是指云服务器数据中心所在位置,可用区是指同一个地域下电力和网络相互独立的区域
4309 0
阿里云服务器地域及可用区选择攻略(考虑七大影响因素)
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
286 2
|
11月前
|
搜索推荐 NoSQL Java
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
736 17
|
安全 Go 开发者
掌握 Go 语言的依赖关系管理
【8月更文挑战第31天】
276 0
|
Unix Linux Perl
sed删除指定行
sed删除指定行
1742 1
|
XML Go 数据格式
【微信公众号开发】基于golang的公众号开发——接入消息自动回复接口
【微信公众号开发】基于golang的公众号开发——接入消息自动回复接口
863 0
|
存储 缓存 小程序
微信小程序-缓存
微信小程序-缓存
408 0
|
消息中间件 Linux 数据安全/隐私保护
【图解RabbitMQ-4】Docker安装RabbitMQ详细图文过程
【图解RabbitMQ-4】Docker安装RabbitMQ详细图文过程
683 0

热门文章

最新文章