分类: 编程语言

通过WPeMatico自动添加.

  • Go编程模式 : 泛型编程

    Go编程模式 : 泛型编程

    Go语言的1.17版本发布了,其中开始正式支持泛型了。虽然还有一些限制(比如,不能把泛型函数export),但是,可以体验了。我的这个《Go编程模式》的系列终于有了真正的泛型编程了,再也不需要使用反射或是go generation这些难用的技术了。周末的时候,我把Go 1.17下载下来,然后,体验了一下泛型编程,还是很不错的。下面,就让我们来看一下Go的泛型编程。(注:不过,如果你对泛型编程的重要性还不是很了解的话,你可以先看一下之前的这篇文章《Go编程模式:Go Generation》,然后再读一下《Go编程模式:MapReduce》)

    本文是全系列中第10 / 10篇:Go编程模式

    初探

    我们先来看一个简单的示例:

    package main
    
    import "fmt"
    
    func print[T any] (arr []T) {
      for _, v := range arr {
        fmt.Print(v)
        fmt.Print(" ")
      }
      fmt.Println("")
    }
    
    func main() {
      strs := []string{"Hello", "World",  "Generics"}
      decs := []float64{3.14, 1.14, 1.618, 2.718 }
      nums := []int{2,4,6,8}
    
      print(strs)
      print(decs)
      print(nums)
    }

    上面这个例子中,有一个 print() 函数,这个函数就是想输出数组的值,如果没有泛型的话,这个函数需要写出 int 版,float版,string 版,以及我们的自定义类型(struct)的版本。现在好了,有了泛型的支持后,我们可以使用 [T any] 这样的方式来声明一个泛型类型(有点像C++的 typename T),然后面都使用 T 来声明变量就好。

    上面这个示例中,我们泛型的 print() 支持了三种类型的适配—— int型,float64型,和 string型。要让这段程序跑起来需要在编译行上加上 -gcflags=-G=3编译参数(这个编译参数会在1.18版上成为默认参数),如下所示:

    $ go run -gcflags=-G=3 ./main.go

    有了个操作以后,我们就可以写一些标准的算法了,比如,一个查找的算法

    func find[T comparable] (arr []T, elem T) int {
      for i, v := range arr {
        if  v == elem {
          return i
        }
      }
      return -1
    }

    我们注意到,我们没有使用 [T any]的形式,而是使用 [T comparable]的形式,comparable是一个接口类型,其约束了我们的类型需要支持 == 的操作, 不然就会有类型不对的编译错误。上面的这个 find() 函数同样可以使用于 int, float64或是string类型。

    从上面的这两个小程序来看,Go语言的泛型已基本可用了,只不过,还有三个问题:

    • 一个是 fmt.Printf()中的泛型类型是 %v 还不够好,不能像c++ iostream重载 >> 来获得程序自定义的输出。
    • 另外一个是,go不支持操作符重载,所以,你也很难在泛型算法中使用“泛型操作符”如:== 等
    • 最后一个是,上面的 find() 算法依赖于“数组”,对于hash-table、tree、graph、link等数据结构还要重写。也就是说,没有一个像C++ STL那样的一个泛型迭代器(这其中的一部分工作当然也需要通过重载操作符(如:++ 来实现)

    不过,这个已经很好了,让我们来看一下,可以干哪些事了。

    数据结构

    Stack 栈

    编程支持泛型最大的优势就是可以实现类型无关的数据结构了。下面,我们用Slices这个结构体来实现一个Stack的数结构。

    首先,我们可以定义一个泛型的Stack

    type stack [T any] []T

    看上去很简单,还是 [T any] ,然后 []T 就是一个数组,接下来就是实现这个数据结构的各种方法了。下面的代码实现了 push()pop()top()len()print()这几个方法,这几个方法和 C++的STL中的 Stack很类似。(注:目前Go的泛型函数不支持 export,所以只能使用第一个字符是小写的函数名)

    func (s *stack[T]) push(elem T) {
      *s = append(*s, elem)
    }
    
    func (s *stack[T]) pop() {
      if len(*s) > 0 {
        *s = (*s)[:len(*s)-1]
      } 
    }
    func (s *stack[T]) top() *T{
      if len(*s) > 0 {
        return &(*s)[len(*s)-1]
      } 
      return nil
    }
    
    func (s *stack[T]) len() int{
      return len(*s)
    }
    
    func (s *stack[T]) print() {
      for _, elem := range *s {
        fmt.Print(elem)
        fmt.Print(" ")
      }
      fmt.Println("")
    }

    上面的这个例子还是比较简单的,不过在实现的过程中,对于一个如果栈为空,那么 top()要么返回error要么返回空值,在这个地方卡了一下。因为,之前,我们返回的“空”值,要么是 int 的0,要么是 string 的 “”,然而在泛型的T下,这个值就不容易搞了。也就是说,除了类型泛型后,还需要有一些“值的泛型”(注:在C++中,如果你要用一个空栈进行 top() 操作,你会得到一个 segmentation fault),所以,这里我们返回的是一个指针,这样可以判断一下指针是否为空。

    下面是如何使用这个stack的代码。

    func main() {
    
      ss := stack[string]{}
      ss.push("Hello")
      ss.push("Hao")
      ss.push("Chen")
      ss.print()
      fmt.Printf("stack top is - %vn", *(ss.top()))
      ss.pop()
      ss.pop()
      ss.print()
    
      
      ns := stack[int]{}
      ns.push(10)
      ns.push(20)
      ns.print()
      ns.pop()
      ns.print()
      *ns.top() += 1
      ns.print()
      ns.pop()
      fmt.Printf("stack top is - %vn", ns.top())
    
    }

     

    LinkList 双向链表

    下面我们再来看一个双向链表的实现。下面这个实现中实现了 这几个方法:

    • add() – 从头插入一个数据结点
    • push() – 从尾插入一个数据结点
    • del() – 删除一个结点(因为需要比较,所以使用了 compareable 的泛型)
    • print() – 从头遍历一个链表,并输出值。
    type node[T comparable] struct {
      data T
      prev *node[T]
      next *node[T]
    }
    
    type list[T comparable] struct {
      head, tail *node[T]
      len int
    }
    
    func (l *list[T]) isEmpty() bool {
      return l.head == nil && l.tail == nil
    }
    
    func (l *list[T]) add(data T) {
      n := &node[T] {
        data : data,
        prev : nil,
        next : l.head,
      }
      if l.isEmpty() {
        l.head = n
        l.tail = n
      }
      l.head.prev = n
      l.head = n
    }
    
    func (l *list[T]) push(data T) { 
      n := &node[T] {
        data : data,
        prev : l.tail,
        next : nil,
      }
      if l.isEmpty() {
        l.head = n
        l.tail = n
      }
      l.tail.next = n
      l.tail = n
    }
    
    func (l *list[T]) del(data T) { 
      for p := l.head; p != nil; p = p.next {
        if data == p.data {
          
          if p == l.head {
            l.head = p.next
          }
          if p == l.tail {
            l.tail = p.prev
          }
          if p.prev != nil {
            p.prev.next = p.next
          }
          if p.next != nil {
            p.next.prev = p.prev
          }
          return 
        }
      } 
    }
    
    func (l *list[T]) print() {
      if l.isEmpty() {
        fmt.Println("the link list is empty.")
        return 
      }
      for p := l.head; p != nil; p = p.next {
        fmt.Printf("[%v] -> ", p.data)
      }
      fmt.Println("nil")
    }

    上面这个代码都是一些比较常规的链表操作,学过链表数据结构的同学应该都不陌生,使用的代码也不难,如下所示,都很简单,看代码就好了。

    func main(){
      var l = list[int]{}
      l.add(1)
      l.add(2)
      l.push(3)
      l.push(4)
      l.add(5)
      l.print() //[5] -> [2] -> [1] -> [3] -> [4] -> nil
      l.del(5)
      l.del(1)
      l.del(4)
      l.print() //[2] -> [3] -> nil
      
    }

    函数式范型

    接下来,我们就要来看一下我们函数式编程的三大件 map()reduce()filter() 在之前的《Go编程模式:Map-Reduce》文章中,我们可以看到要实现这样的泛型,需要用到反射,代码复杂到完全读不懂。下面来看一下真正的泛型版本。

    泛型Map
    func gMap[T1 any, T2 any] (arr []T1, f func(T1) T2) []T2 {
      result := make([]T2, len(arr))
      for i, elem := range arr {
        result[i] = f(elem)
      }
      return result
    }

    在上面的这个 map函数中我使用了两个类型 – T1T2

    • T1 – 是需要处理数据的类型
    • T2 – 是处理后的数据类型

    T1T2 可以一样,也可以不一样。

    我们还有一个函数参数 –  func(T1) T2 意味着,进入的是 T1 类型的,出来的是 T2 类型的。

    然后,整个函数返回的是一个 []T2

    好的,我们来看一下怎么使用这个map函数:

    nums := []int {0,1,2,3,4,5,6,7,8,9}
    squares := gMap(nums, func (elem int) int {
      return elem * elem
    })
    print(squares)  //0 1 4 9 16 25 36 49 64 81 
    
    strs := []string{"Hao", "Chen", "MegaEase"}
    upstrs := gMap(strs, func(s string) string  {
      return strings.ToUpper(s)
    })
    print(upstrs) // HAO CHEN MEGAEASE 
    
    
    dict := []string{"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}
    strs =  gMap(nums, func (elem int) string  {
      return  dict[elem]
    })
    print(strs) // 零 壹 贰 叁 肆 伍 陆 柒 捌 玖
    泛型 Reduce

    接下来,我们再来看一下我们的Reduce函数,reduce函数是把一堆数据合成一个。

    func gReduce[T1 any, T2 any] (arr []T1, init T2, f func(T2, T1) T2) T2 {
      result := init
      for _, elem := range arr {
        result = f(result, elem)
      }
      return result
    }

    函数实现起来很简单,但是感觉不是很优雅。

    • 也是有两个类型 T1T2,前者是输出数据的类型,后者是佃出数据的类型。
    • 因为要合成一个数据,所以需要有这个数据的初始值 init,是 T2 类型
    • 而自定义函数 func(T2, T1) T2,会把这个init值传给用户,然后用户处理完后再返回出来。

    下面是一个使用上的示例——求一个数组的和

    nums := []int {0,1,2,3,4,5,6,7,8,9}
    sum := gReduce(nums, 0, func (result, elem int) int  {
        return result + elem
    })
    fmt.Printf("Sum = %d n", sum)
    泛型 filter

    filter函数主要是用来做过滤的,把数据中一些符合条件(filter in)或是不符合条件(filter out)的数据过滤出来,下面是相关的代码示例

    func gFilter[T any] (arr []T, in bool, f func(T) bool) []T {
      result := []T{}
      for _, elem := range arr {
        choose := f(elem)
        if (in && choose) || (!in && !choose) {
          result = append(result, elem)
        }
      }
      return result
    }
    
    func gFilterIn[T any] (arr []T, f func(T) bool) []T {
      return gFilter(arr, true, f)
    }
    
    func gFilterOut[T any] (arr []T, f func(T) bool) []T {
      return gFilter(arr, false, f)
    }

    其中,用户需要提从一个 bool 的函数,我们会把数据传给用户,然后用户只需要告诉我行还是不行,于是我们就会返回一个过滤好的数组给用户。

    比如,我们想把数组中所有的奇数过滤出来

    nums := []int {0,1,2,3,4,5,6,7,8,9}
    odds := gFilterIn(nums, func (elem int) bool  {
        return elem % 2 == 1
    })
    print(odds)

    业务示例

    正如《Go编程模式:Map-Reduce》中的那个业务示例,我们在这里再做一遍。

    首先,我们先声明一个员工对象和相关的数据

    type Employee struct {
      Name     string
      Age      int
      Vacation int
      Salary   float32
    }
    
    var employees = []Employee{
      {"Hao", 44, 0, 8000.5},
      {"Bob", 34, 10, 5000.5},
      {"Alice", 23, 5, 9000.0},
      {"Jack", 26, 0, 4000.0},
      {"Tom", 48, 9, 7500.75},
      {"Marry", 29, 0, 6000.0},
      {"Mike", 32, 8, 4000.3},
    }

    然后,我们想统一下所有员工的薪水,我们就可以使用前面的reduce函数

    total_pay := gReduce(employees, 0.0, func(result float32, e Employee) float32 {
      return result + e.Salary
    })
    fmt.Printf("Total Salary: %0.2fn", total_pay) // Total Salary: 43502.05

    我们函数这个 gReduce 函数有点啰嗦,还需要传一个初始值,在用户自己的函数中,还要关心 result 我们还是来定义一个更好的版本。

    一般来说,我们用 reduce 函数大多时候基本上是统计求和或是数个数,所以,是不是我们可以定义的更为直接一些?比如下面的这个 CountIf(),就比上面的 Reduce 干净了很多。

    func gCountIf[T any](arr []T, f func(T) bool) int {
      cnt := 0
      for _, elem := range arr {
        if f(elem) {
          cnt += 1
        }
      }
      return cnt;
    }

    我们做求和,我们也可以写一个Sum的泛型。

    • 处理 T 类型的数据,返回 U类型的结果
    • 然后,用户只需要给我一个需要统计的 TU 类型的数据就可以了。

    代码如下所示:

    type Sumable interface {
      type int, int8, int16, int32, int64,
            uint, uint8, uint16, uint32, uint64,
            float32, float64
    }
    
    func gSum[T any, U Sumable](arr []T, f func(T) U) U {
      var sum U
      for _, elem := range arr {
        sum += f(elem)
      }
      return sum
    }

    上面的代码我们动用了一个叫 Sumable 的接口,其限定了 U 类型,只能是 Sumable里的那些类型,也就是整型或浮点型,这个支持可以让我们的泛型代码更健壮一些。

    于是,我们就可以完成下面的事了。

    1)统计年龄大于40岁的员工数

    old := gCountIf(employees, func (e Employee) bool  {
        return e.Age > 40
    })
    fmt.Printf("old people(>40): %dn", old) 
    // ld people(>40): 2

    2)统计薪水超过 6000元的员工数

    high_pay := gCountIf(employees, func(e Employee) bool {
      return e.Salary >= 6000
    })
    fmt.Printf("High Salary people(>6k): %dn", high_pay) 
    //High Salary people(>6k): 4

    3)统计年龄小于30岁的员工的薪水

    younger_pay := gSum(employees, func(e Employee) float32 {
      if e.Age < 30 {
          return e.Salary
      } 
      return 0
    })
    fmt.Printf("Total Salary of Young People: %0.2fn", younger_pay)
    //Total Salary of Young People: 19000.00

    4)统计全员的休假天数

    total_vacation := gSum(employees, func(e Employee) int {
      return e.Vacation
    })
    fmt.Printf("Total Vacation: %d day(s)n", total_vacation)
    //Total Vacation: 32 day(s)

    5)把没有休假的员工过滤出来

    no_vacation := gFilterIn(employees, func(e Employee) bool {
      return e.Vacation == 0
    })
    print(no_vacation)
    //{Hao 44 0 8000.5} {Jack 26 0 4000} {Marry 29 0 6000}

    怎么样,你大概了解了泛型编程的意义了吧。

    (全文完)

    (转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

    The post Go编程模式 : 泛型编程 first appeared on 酷 壳 – CoolShell.

  • 从一次经历谈 TIME_WAIT 的那些事

    从一次经历谈 TIME_WAIT 的那些事

    今天来讲一讲TCP 的 TIME_WAIT 的问题。这个问题尽人皆知,不过,这次遇到的是不太一样的场景,前两天也解决了,正好写篇文章,顺便把 TIME_WAIT 的那些事都说一说。对了,这个场景,跟我开源的探活小工具 EaseProbe 有关,我先说说这个场景里的问题,然后,顺着这个场景跟大家好好说一下这个事。

    问题背景

    先说一下背景,EaseProbe 是一个轻量独立的用来探活服务健康状况的小工具,支持http/tcp/shell/ssh/tls/host以及各种中间件的探活,然后,直接发送通知到主流的IM上,如:Slack/Telegram/Discrod/Email/Team,包括国内的企业微信/钉钉/飞书, 非常好用,用过的人都说好

    这个探活工具在每次探活的时候,必须要从头开始建立整个网络链接,也就是说,需要从头开始进行DNS查询,建立TCP链接,然后进行通信,再关闭链接。这里,我们不会设置 TCP 的 KeepAlive 重用链接,因为探活工具除了要探活所远端的服务,还要探活整个网络的情况,所以,每次探活都需要从新来过,这样才能捕捉得到整个链路的情况。

    但是,这样不断的新建链接和关闭链接,根据TCP的状态机,我们知道这会导致在探测端这边出现的 TIME_WAIT 的 TCP 链接,根据 TCP 协议的定义,这个 TIME_WAIT 需要等待 2倍的MSL 时间,TCP 链接都会被系统回收,在回收之前,这个链接会占用系统的资源,主要是两个资源,一个是文件描述符,这个还好,可以调整,另一个则是端口号,这个是没法调整的,因为作为发起请求的client来说,在对同一个IP上理论上你只有64K的端口号号可用(实际上系统默认只有近30K,从32,768 到 60,999 一共 60999+1-32768=28,232,你可以通过 sysctl net.ipv4.ip_local_port_range 查看  ),如果 TIME_WAIT 过多,会导致TCP无法建立链接,还会因为资源消耗太多导致整个程序甚至整个系统异常。

    试想,如果我们以 10秒为周期探测10K的结点,如果TIME_WAIT的超时时间是120秒,那么在第60秒后,等着超时的 TIME_WAIT 我们就有可能把某个IP的端口基本用完了,就算还行,系统也有些问题。(注意:我们不仅仅只是TCP,还有HTTP协议,所以,大家不要觉得TCP的四元组只要目标地址不一样就好了,一方面,我们探的是域名,需要访问DNS服务,所以,DNS服务一般是一台服务器,还有,因为HTTPS一般是探API,而且会有网关代理API,所以链接会到同一个网关上。另外就算还可以建出站连接,但是本地程序会因为端口耗尽无法bind了。所以,现实情况并不会像理论情况那样只要四元组不冲突,端口就不会耗尽)

    为什么要 TIME_WAIT

    那么,为什么TCP在 TIME_WAIT 上要等待一个2MSL的时间?

    以前写过篇比较宏观的《TCP的那些事》(上篇下篇),这个访问在“上篇”里讲过,这里再说一次,TCP 断链接的时候,会有下面这个来来回回的过程。

    我们来看主动断链接的最后一个状态 TIME_WAIT 后就不需要等待对端回 ack了,而是进入了超时状态。这主要是因为,在网络上,如果要知道我们发出的数据被对方收到了,那我们就需要对方发来一个确认的Ack信息,那问题来了,对方怎么知道自己发出去的ack,被收到了?难道还要再ack一下,这样ack来ack回的,那什么谁也不要玩了……是的,这就是比较著名的【两将军问题】——两个将军需要在一个不稳定的信道上达成对敌攻击时间的协商,A向B派出信鸽,我们明早8点进攻,A怎么知道B收到了信?那需要B向A派出信鸽,ack说我收到了,明早8点开干。但是,B怎么知道A会收到自己的确认信?是不是还要A再确认一下?这样无穷无尽的确认导致这个问题是没有完美解的(我们在《分布式事务》一文中说过这个问题,这里不再重述)

    所以,我们只能等一个我们认为最大小时来解决两件个问题:

    1) 为了 防止来自一个连接的延迟段被依赖于相同四元组(源地址、源端口、目标地址、目标端口)的稍后连接接受(被接受后,就会被马上断掉,TCP状态机紊乱)。虽然,可以通过指定 TCP 的 sequence number 一定范围内才能被接受。但这也只是让问题发生的概率低了一些,对于一个吞吐量大的的应用来说,依然能够出现问题,尤其是在具有大接收窗口的快速连接上。RFC 1337详细解释了当 TIME-WAIT状态不足时会发生什么。TIME-WAIT以下是如果不缩短状态可以避免的示例:

    duplicate segment
    由于缩短的 TIME-WAIT 状态,后续的 TCP 段已在不相关的连接中被接受(来源

     

    2)另一个目的是确保远端已经关闭了连接。当最后一个ACK​​ 丢失时,对端保持该LAST-ACK状态。在没有TIME-WAIT状态的情况下,可以重新打开连接,而远程端仍然认为先前的连接有效。当它收到一个SYN段(并且序列号匹配)时,它将以RST应答,因为它不期望这样的段。新连接将因错误而中止:

     

    last ack
    如果远端因为最后一个 ACK​​ 丢失而停留在 LAST-ACK 状态,则打开具有相同四元组的新连接将不起作用 (来源

    TIME_WAIT 的这个超时时间的值如下所示:

    • 在 macOS 上是15秒, sysctl net.inet.tcp | grep net.inet.tcp.msl
    • 在 Linux 上是 60秒 cat /proc/sys/net/ipv4/tcp_fin_timeout

    解决方案

    要解决这个问题,网上一般会有下面这些解法

    • 把这个超时间调小一些,这样就可以把TCP 的端口号回收的快一些。但是也不能太小,如果流量很大的话,TIME_WAIT一样会被耗尽。
    • 设置上 tcp_tw_reuse 。RFC 1323提出了一组 TCP 扩展来提高高带宽路径的性能。除其他外,它定义了一个新的 TCP 选项,带有两个四字节时间戳字段。第一个是发送选项的 TCP 时间戳的当前值,而第二个是从远程主机接收到的最新时间戳。如果新时间戳严格大于为前一个连接记录的最新时间戳。Linux 将重用该状态下的现有 TIME_WAIT 连接用于出站的链接。也就是说,这个参数对于入站连接是没有任何用图的。
    • 设置上 tcp_tw_recycle 。 这个参数同样依赖于时间戳选项,但会影响进站和出站链接。这个参数会影响NAT环境,也就是一个公司里的所有员工用一个IP地址访问外网的情况。在这种情况下,时间戳条件将禁止在这个公网IP后面的所有设备在一分钟内连接,因为它们不共享相同的时间戳时钟。毫无疑问,禁用此选项要好得多,因为它会导致 难以检测诊断问题。(注:从 Linux 4.10 (commit 95a22caee396 ) 开始,Linux 将为每个连接随机化时间戳偏移量,从而使该选项完全失效,无论有无NAT。它已从 Linux 4.12中完全删除)

    对于服务器来说,上述的三个访问都不能解决服务器的 TIME_WAIT 过多的问题,真正解决问题的就是——不作死就不会死,也就是说,服务器不要主动断链接,而设置上KeepAlive后,让客户端主动断链接,这样服务端只会有CLOSE_WAIT

    但是对于用于建立出站连接的探活的 EaseProbe来说,设置上 tcp_tw_reuse 就可以重用 TIME_WAIT 了,但是这依然无法解决 TIME_WAIT 过多的问题。

    然后,过了几天后,我忽然想起来以前在《UNIX 网络编程》上有看到过一个Socket的参数,叫 <code>SO_LINGER,我的编程生涯中从来没有使用过这个设置,这个参数主要是为了延尽关闭来用的,也就是说你应用调用 close()函数时,如果还有数据没有发送完成,则需要等一个延时时间来让数据发完,但是,如果你把延时设置为 0  时,Socket就丢弃数据,并向对方发送一个 RST 来终止连接,因为走的是 RST 包,所以就不会有 TIME_WAIT 了。

    这个东西在服务器端永远不要设置,不然,你的客户端就总是看到 TCP 链接错误 “connnection reset by peer”,但是这个参数对于 EaseProbe 的客户来说,简直是太完美了,当EaseProbe 探测完后,直接 reset connection, 即不会有功能上的问题,也不会影响服务器,更不会有烦人的 TIME_WAIT 问题。

    Go 实际操作

    在 Golang的标准库代码里,net.TCPConn 有个方法 SetLinger()可以完成这个事,使用起来也比较简单:

    conn, _ := net.DialTimeout("tcp", t.Host, t.Timeout())
    
    if tcpCon, ok := conn.(*net.TCPConn); ok {
        tcpCon.SetLinger(0)
    }

    你需要把一个 net.Conn  转型成 net.TCPConn,然后就可以调用方法了。

    但是对于Golang 的标准库中的 HTTP 对象来说,就有点麻烦了,Golang的 http 库把底层的这边连接对象全都包装成私有变量了,你在外面根本获取不到。这篇《How to Set Go net/http Socket Options – setsockopt() example 》中给出了下面的方法:

    dialer := &net.Dialer{
        Control: func(network, address string, conn syscall.RawConn) error {
            var operr error
            if err := conn.Control(func(fd uintptr) {
                operr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.TCP_QUICKACK, 1)
            }); err != nil {
                return err
            }
            return operr
        },
    }
    
    client := &http.Client{
        Transport: &http.Transport{
            DialContext: dialer.DialContext,
        },
    }

    上面这个方法非常的低层,需要直接使用setsocketopt这样的系统调用,我其实,还是想使用 TCPConn.SetLinger(0) 来完成这个事,即然都被封装好了,最好还是别破坏封闭性碰底层的东西。

    经过Golang http包的源码阅读和摸索,我使用了下面的方法:

    client := &http.Client{
        Timeout: h.Timeout(),
        Transport: &http.Transport{
          TLSClientConfig:   tls,
          DisableKeepAlives: true,
          DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            d := net.Dialer{Timeout: h.Timeout()}
            conn, err := d.DialContext(ctx, network, addr)
            if err != nil {
              return nil, err
            }
            tcpConn, ok := conn.(*net.TCPConn)
            if ok {
              tcpConn.SetLinger(0)
              return tcpConn, nil
            }
            return conn, nil
          },
        },
      }

    然后,我找来了全球 T0p 100W的域名,然后在AWS上开了一台服务器,用脚本生成了 TOP 10K 和 20K 的网站来以5s, 10s, 30s, 60s的间隔进行探活,搞到Cloudflare 的 1.1.1.1 DNS 时不时就把我拉黑,最后的测试结果也非常不错,根本 没有 TIME_WAIT 的链接,相关的测试方法、测试数据和测试报告可以参看:Benchmark Report

    总结

    下面是几点总结

    • TIME_WAIT 是一个TCP 协议完整性的手段,虽然会有一定的副作用,但是这个设计是非常关键的,最好不要妥协掉。
    • 永远不要使用  tcp_tw_recycle ,这个参数是个巨龙,破坏力极大。
    • 服务器端永远不要使用  SO_LINGER(0),而且使用 tcp_tw_reuse 对服务端意义不大,因为它只对出站流量有用。
    • 在服务端上最好不要主动断链接,设置好KeepAlive,重用链接,让客户端主动断链接。
    • 在客户端上可以使用 tcp_tw_reuse  和 SO_LINGER(0)

    最后强烈推荐阅读这篇文章 – Coping with the TCP TIME-WAIT state on busy Linux servers

    (全文完)

    (转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

    The post 从一次经历谈 TIME_WAIT 的那些事 first appeared on 酷 壳 – CoolShell.