前言
分布式环境下应对高并发保证服务稳定,优先级从高到低分别为缓存、限流、降级、熔断,本文重点就讲讲限流这部分。
其实服务降级、熔断本身也是限流的一种,因为它们本质上也是阻断了流量进来,但是本文希望大家可以把限流当做一个单纯的名词来理解,看一下对请求做流控的几种算法及具体实现方式。
为什么要限流
其实很好理解的一个问题,为什么要限流,自然就流量过大了呗,一个对外服务有很多场景都会流量增大:
- 业务用户量不断攀升
- 各种促销
- 网络爬虫
- 恶意刷单
注意这个"大",1000QPS大吗?5000QPS大吗?10000QPS大么?没有答案,因为没有标准,因此,"大"一定是和正常流量相比的大。流量一大,服务器扛不住,扛不住就挂了,挂了没法提供对外服务导致业务直接熔断。怎么办,最直接的办法就是从源头把流量限制下来,例如服务器只有支撑1000QPS的处理能力,那就每秒放1000个请求,自然保证了服务器的稳定,这就是限流。
下面看一下常见的两种限流算法。
漏桶算法
漏桶算法的原理比较简单,水(请求)先进入到漏桶里,人为设置一个最大出水速率,漏桶以<=出水速率的速度出水,当水流入速度过大会直接溢出(拒绝服务):
因此,这个算法的核心为:
- 存下请求
- 匀速处理
- 多于丢弃
因此这是一种强行限制请求速率的方式,但是缺点非常明显,主要有两点:
- 无法面对突发的大流量----比如请求处理速率为1000,容量为5000,来了一波2000/s的请求持续10s,那么后5s的请求将全部直接被丢弃,服务器拒绝服务,但是实际上网络中突发一波大流量尤其是短时间的大流量是非常正常的,超过容量就拒绝,非常简单粗暴
- 无法有效利用网络资源----比如虽然服务器的处理能力是1000/s,但这不是绝对的,这个1000只是一个宏观服务器处理能力的数字,实际上一共5秒,每秒请求量分别为1200、1300、1200、500、800,平均下来qps也是1000/s,但是这个量对服务器来说完全是可以接受的,但是因为限制了速率是1000/s,因此前面的三秒,每秒只能处理掉1000个请求而一共打回了700个请求,白白浪费了服务器资源
所以,通常来说利用漏桶算法来限流,实际场景下用得不多。
令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和限流(Rate Limiting)中最常使用的一种算法,它可用于控制发送到网络上数据的数量并允许突发数据的发送。
从某种意义上来说,令牌桶算法是对漏桶算法的一种改进,主要在于令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用,来看下令牌桶算法的实现原理:
整个的过程是这样的:
- 系统以恒定的速率产生令牌,然后将令牌放入令牌桶中
- 令牌桶有一个容量,当令牌桶满了的时候,再向其中放入的令牌就会被丢弃
- 每次一个请求过来,需要从令牌桶中获取一个令牌,假设有令牌,那么提供服务;假设没有令牌,那么拒绝服务
那么,我们再看一下,为什么令牌桶算法可以防止一定程度的突发流量呢?可以这么理解,假设我们想要的速率是1000QPS,那么往桶中放令牌的速度就是1000个/s,假设第1秒只有800个请求,那意味着第2秒可以容许1200个请求,这就是一定程度突发流量的意思,反之我们看漏桶算法,第一秒只有800个请求,那么全部放过,第二秒这1200个请求将会被打回200个。
注意上面多次提到一定程度这四个字,这也是我认为令牌桶算法最需要注意的一个点。假设还是1000QPS的速率,那么5秒钟放1000个令牌,第1秒钟800个请求过来,第2~4秒没有请求,那么按照令牌桶算法,第5秒钟可以接受4200个请求,但是实际上这已经远远超出了系统的承载能力,因此使用令牌桶算法特别注意设置桶中令牌的上限即可。
总而言之,作为对漏桶算法的改进,令牌桶算法在限流场景下被使用更加广泛。
RateLimiter Wait使用
模拟一下每2秒最多过4个请求:
package main
import (
"context"
"fmt"
"time"
"golang.org/x/time/rate"
)
func main() {
// 第一个参数:每秒往桶中放多少令牌, 第二个参数:桶的容量/ go官方的实现中,初始化后桶中的令牌是满的
l := rate.NewLimiter(2, 4)
for {
l.Wait(context.Background())
fmt.Println(time.Now().Format("04:05.000"))
}
}
输出:
可以看到31:19秒的时候有4个输出,那是因为初始化桶是4个令牌;
以后每秒是2个请求,那是因为每秒投放的令牌是2
RateLimiter allow使用
模拟一下每2秒最多过4个请求:
package main
import (
"fmt"
"time"
"golang.org/x/time/rate"
)
func main() {
// 第一个参数:每秒往桶中放多少令牌, 第二个参数:桶的容量/ go官方的实现中,初始化后桶中的令牌是满的
l := rate.NewLimiter(2, 4)
// 缓存3次后,每秒执行一次
for {
if l.Allow() {
fmt.Println(time.Now().Format("allow 04:05.000"))
} else {
time.Sleep(time.Second / 10)
fmt.Println(time.Now().Format("refuse 04:05.000"))
}
}
}
输出:
可以看到35:24秒的时候有4个allow,那是因为初始化桶是4个令牌;
以后每秒是2个allow,那是因为每秒投放的令牌是2,其他情况都是refuse
RateLimiter 结合go-kit使用
模拟一下每2秒最多过4个请求:
package main
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"context"
"encoding/json"
"github.com/bitly/go-simplejson"
"github.com/go-kit/kit/ratelimit"
httptransport "github.com/go-kit/kit/transport/http"
)
type JsonResponder struct {
Status int `json:"status"`
Code string `json:"code"`
Message string `json:"message"`
Data *simplejson.Json `json:"data"`
}
func AllFactoryReq(Limit ratelimit.Allower) (*JsonResponder, error) {
Url := "http://localhost:8900/erp/v1/factory"
Method := "GET"
UserId := 1
EncodeFunc := func(_ context.Context, r *http.Request, request interface{}) error {
params := request.(url.Values)
r.Body = io.NopCloser(bytes.NewBuffer([]byte(params.Encode())))
return nil
}
DecodeFunc := func(_ context.Context, r *http.Response) (interface{}, error) {
var response JsonResponder
if err := json.NewDecoder(r.Body).Decode(&response); err != nil {
return nil, err
}
response.Status = r.StatusCode
return &response, nil
}
u, err := url.Parse(Url)
if err != nil {
panic(err.(any))
}
var options = []httptransport.ClientOption{
httptransport.SetClient(&http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}),
httptransport.ClientBefore(httptransport.SetRequestHeader("User-Id", strconv.Itoa(UserId))),
}
e := httptransport.NewClient(Method, u, EncodeFunc, DecodeFunc, options...).Endpoint()
e = ratelimit.NewErroringLimiter(Limit)(e)
res, err := e(context.Background(), url.Values{})
if err != nil {
return nil, err
}
return res.(*JsonResponder), nil
}
func main() {
// 第一个参数:每秒往桶中放多少令牌, 第二个参数:桶的容量/ go官方的实现中,初始化后桶中的令牌是满的
lim := NewLimiter(2, 4)
for {
go func() {
_, err := AllFactoryReq(lim)
if err == ratelimit.ErrLimited {
fmt.Printf("%s ErrLimited\n", time.Now().Format("allow 04:05.000"))
} else {
fmt.Printf("%s success\n", time.Now().Format("allow 04:05.000"))
}
}()
time.Sleep(time.Millisecond * 100)
}
}
输出:
可以看到36:54秒的时候有4个allow(因为打印时间和执行之间有误差,所以看起来秒数不对),那是因为初始化桶是4个令牌;
以后每秒是2个allow,那是因为每秒投放的令牌是2,其他情况都是refuse
部分描述引用自: https://www.cnblogs.com/xrq730/p/11025029.html
标签:令牌,RateLimiter,err,golang,算法,限流,time,请求 From: https://www.cnblogs.com/zhanchenjin/p/16997880.html