文章目录

各类限频原理

网上有很多讲解限频原理以及限频原因的,限频常用在接口、服务的流量、并发上,主要是为了合理使用后端资源,防止后端被压垮,雪崩等等。

实现方法

这里使用使用go的ring(环形队列)实现滑动窗口

实现代码

package main

import (
    "fmt"
    "net"
    "os"
    "container/ring"
    "sync/atomic"
    "sync"
    "time"
)

var (
   limitCount int = 10 // 6s限频
   limitBucket int = 6 // 滑动窗口个数
   curCount int32 = 0  // 记录限频数量
   head *ring.Ring     // 环形队列(链表)
)

func main() {
    tcpAddr, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:9090") //获取一个tcpAddr
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr) //监听一个端口
    checkError(err)
    defer listener.Close()
    // 初始化滑动窗口
    head = ring.New(limitBucket)
    for i := 0; i < limitBucket; i++ {
        head.Value = 0
        head = head.Next()
    }
    // 启动执行器
    go func() {
        timer := time.NewTicker(time.Second * 1)
        for range timer.C { // 定时每隔1秒刷新一次滑动窗口数据
            subCount := int32(0 - head.Value.(int))
            newCount := atomic.AddInt32(&curCount, subCount)

            arr := [6]int{}
            for i := 0; i < limitBucket; i++ { // 这里是为了方便打印
                arr[i] = head.Value.(int)
                head = head.Next()
            }
            fmt.Println("move subCount,newCount,arr", subCount, newCount,arr)
            head.Value = 0
            head = head.Next()
        }
    }()

    for {
        conn, err := listener.Accept() // 在此处阻塞,每次来一个请求才往下运行handle函数
        if err != nil {
            fmt.Println(err)
            continue
        }
        go handle(&conn) // 起一个单独的协程处理,有多少个请求,就起多少个协程,协程之间共享同一个全局变量limiting,对其进行原子操作。
    }
}

func handle(conn *net.Conn) {
    defer (*conn).Close()
    n := atomic.AddInt32(&curCount, 1) 
    //fmt.Println("handler n:", n)
    if n > int32(limitCount) { // 超出限频
        atomic.AddInt32(&curCount, -1) // add 1 by atomic,业务处理完毕,放回令牌
        (*conn).Write([]byte("HTTP/1.1 404 NOT FOUND\r\n\r\nError, too many request, please try again."))
    } else {
        mu := sync.Mutex{}
        mu.Lock()
        pos := head.Prev()
        val := pos.Value.(int)
        val++
        pos.Value = val
        mu.Unlock()
        time.Sleep(1 * time.Second) // 假设我们的应用处理业务用了1s的时间
        (*conn).Write([]byte("HTTP/1.1 200 OK\r\n\r\nI can change the world!")) // 业务处理结束后,回复200成功。
    }
}

// 异常报错的处理
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

压测试

另外起一个终端,用golang的boom来做压测。要提前安装boom工具

go get github.com/rakyll/hey
go install github.com/rakyll/hey

进行压测试:

PS D:\Project\go\bin> .\hey.exe -c 6 -n 300 -q 6 -t 80 http://localhost:9090

压测试输出

move subCount,newCount,arr 0 0 [0 0 0 0 0 0]
move subCount,newCount,arr 0 0 [0 0 0 0 0 0]
move subCount,newCount,arr 0 6 [0 0 0 0 0 6]
move subCount,newCount,arr 0 10 [0 0 0 0 6 4]
move subCount,newCount,arr 0 10 [0 0 0 6 4 0]
move subCount,newCount,arr 0 10 [0 0 6 4 0 0]
move subCount,newCount,arr 0 10 [0 6 4 0 0 0]
move subCount,newCount,arr -6 4 [6 4 0 0 0 0]
move subCount,newCount,arr -4 6 [4 0 0 0 0 6]
move subCount,newCount,arr 0 10 [0 0 0 0 6 4]
move subCount,newCount,arr 0 10 [0 0 0 6 4 0]
move subCount,newCount,arr 0 10 [0 0 6 4 0 0]
move subCount,newCount,arr 0 10 [0 6 4 0 0 0]
move subCount,newCount,arr -6 4 [6 4 0 0 0 0]
move subCount,newCount,arr -4 3 [4 0 0 0 0 3]
move subCount,newCount,arr 0 3 [0 0 0 0 3 0]
move subCount,newCount,arr 0 3 [0 0 0 3 0 0]
move subCount,newCount,arr 0 3 [0 0 3 0 0 0]
move subCount,newCount,arr 0 3 [0 3 0 0 0 0]
move subCount,newCount,arr -3 0 [3 0 0 0 0 0]
move subCount,newCount,arr 0 0 [0 0 0 0 0 0]
move subCount,newCount,arr 0 0 [0 0 0 0 0 0]

查看其中数组可以知道每一秒此时滑动窗口的限频值,以及变化。

压测试客户端输出

可以看出压测试服务在12.6秒进行了300次http请求其中有23次正确响应成功,限频测试ok

D:\Project\go\bin> .\hey.exe -c 6 -n 300
-q 6 -t 80 http://localhost:9090

Summary:
  Total:        12.6701 secs
  Slowest:      1.0163 secs
  Fastest:      0.0008 secs
  Average:      0.0789 secs
  Requests/sec: 23.6779


Response time histogram:
  0.001 [1]     |
  0.102 [276]   |■■■■■■■■■■■■■
■■■■■■■■■■■■■■■■■■■■■■
■■■■■
  0.204 [0]     |
  0.305 [0]     |
  0.407 [0]     |
  0.509 [0]     |
  0.610 [0]     |
  0.712 [0]     |
  0.813 [0]     |
  0.915 [0]     |
  1.016 [23]    |■■■


Latency distribution:
  10% in 0.0012 secs
  25% in 0.0014 secs
  50% in 0.0016 secs
  75% in 0.0019 secs
  90% in 0.0071 secs
  95% in 1.0016 secs
  99% in 1.0162 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0020 secs, 0.0008 secs, 1.0163 secs
  DNS-lookup:   0.0012 secs, 0.0001 secs, 0.0150 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0001 secs
  resp wait:    -67.5636 secs, -2902.3027 secs, 1.0014 secs
  resp read:    0.0000 secs, 0.0000 secs, 0.0003 secs

Status code distribution:
  [200] 23 responses
  [404] 277 responses