降级|熔断|限流详解

限流

本质是对系统的被请求频率以及内部的部分功能的执行频率加以限制,防止因突发的流量激增,导致整个系统不可用。

例如:

web开发中,tomcat默认是200个线程池,当更多的请求到来,没有新的线程能够去处理这个请求,那这个请求将会一直等待在浏览器方。表现的形式是,浏览器一直在转圈(还没超过acceptCount),即使你请求的是一个简单的Hello world。

你可以把这个过程,也看作是限流。本质就是设置一个资源数量上限,超出这个上限的请求,将被缓冲,或者直接失败。

对于高并发场景下的限流来说,它有特殊的含义:它主要是用来保护底层资源的。如果你想要调用某些服务,你需要首先获取调用它的许可。限流一般由服务提供方来提供,对调用方能够做事的能力进行限制。

比如,某个服务为A、B、C都提供了服务,但根据提前申请的流量预估,限制A服务的请求为1000/秒、B服务2000/秒,C服务1w/秒。在同一时刻,某些客户端可能会出现被拒绝的请求,而某些客户端能够正常运行,限流被看作是服务端的自我保护能力。

常见的限流算法有:计数器、漏桶、令牌桶等。但计数器算法无法实现平滑的限流,在实际应用中使用较少。

限流算法

计数器

计数器是一种最简单限流算法,其原理就是:在一段时间间隔内,对请求进行计数,与阀值进行比较判断是否需要限流,每结束一个时间间隔时,都会将计数器清零。
计数器算法存在“时间临界点”缺陷,比如每一分钟限制100个请求,可以在00:00:00-00:00:58秒里面都没有请求,在00:00:59瞬间发送100个请求,这个对于计数器算法来是允许的,然后在00:01:00再次发送100个请求,意味着在短短1s内发送了200个请求,如果量更大呢,系统可能会承受不住瞬间流量,导致系统崩溃。

滑动窗口

滑动窗口算法的思想主要来源于Tcp协议,即将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值,这种实现比固定窗口的流量曲线更加平滑。
对于上述计数器算法存在的不足,对于滑动时间窗口,我们可以把1s的时间窗口划分成10个小窗口,或者想象窗口有10个时间片, 每个时间片统计自己片内100ms的请求数量。每过100ms,都有一个新的时间片加入窗口,早于当前时间1s的时间片滑出窗口,窗口内最多维护10个时间片。
虽然滑动窗口的使用,可以对时间临界问题有一定的优化,但从根本上而言,其实该算法并没有真正地解决固定窗口算法的临界突发流量问题。

漏桶

在介绍漏桶算法前,我们可以回忆一下,一个小学数学上的经典问题,有一个水池,有一个进水口,一个出水口,进水速度为x,出水速度为y,问多久可以把水池注满,相信这个问题大家都不陌生。简单来说漏桶算法,就是这道小学数学题,但是我们并不需要求解,首先想象有一个木桶,桶的容量是固定的。当有请求到来时先放到木桶中,处理请求时则以固定的速度从木桶中取出请求即可,如果木桶已经满了,可以直接返回请求频率超限的错误或进行一些其他处理,算法的好处是,流量一直是以一种均匀的速度在被消费,十分适合某些业务场景。
因为流入请求的速率是不固定的,但流出的速率是恒定的,所以当系统面对突发流量时会有大量的请求失败。这就导致,在类似于电商抢购、微博热点、过年红包等场景中,该算法并不适用。

令牌桶

令牌桶有点像反方向的"漏桶",它是以恒定的速度往木桶里加入令牌,木桶满了则不再加入令牌。服务收到请求时尝试从木桶中取出一个令牌,如果能够得到令牌则继续执行后续的业务逻辑。如果没有得到令牌,直接返回请求频率超限的错误或进行一些其他处理,不继续执行后续的业务逻辑。
同时由于向木桶中添加令牌的速度是恒定的,且木桶的容量是有上限等,所以单位时间内系统能够处理的请求数量也是可控的,从而起到限流的目的。假设加入令牌的速度为 100/s,桶的容量为500,那么在请求比较的少的时候,木桶可以先积攒一些令牌(最多500个)。当有突发流量时,可以一下把木桶内的令牌都取出,也就是500的并发,之后就需要等到有新的令牌加入后,才可以进行新的业务处理了。

微服务架构下的熔断框架:hystrix-go

快速安装

go get -u github.com/afex/hystrix-go/hystrix

快速使用

hystrix-go真的是开箱即用,使用还是比较简单的,主要分为两个步骤:

  • 配置熔断规则,否则将使用默认配置。可以调用的方法

func Configure(cmds map[string]CommandConfig) 

func ConfigureCommand(name string, config CommandConfig)

Configure方法内部也是调用的ConfigureCommand方法,就是传参数不一样,根据自己的代码风格选择。

  • 定义依赖于外部系统的应用程序逻辑 - runFunc 和服务中断期间执行的逻辑代码 - fallbackFunc,可以调用的方法:

func Go(name string, run runFunc, fallback fallbackFunc) // 内部调用Goc方法

func GoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) 

func Do(name string, run runFunc, fallback fallbackFunc) // 内部调用的是Doc方法

func DoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) // 内部调用Goc方法,处理了异步过程

GoDo的区别在于异步还是同步,Do方法在调用Doc方法内处理了异步过程,他们最终都是调用的Goc方法

完整示例:

package main

import (
    "errors"
    "fmt"
    "github.com/afex/hystrix-go/hystrix"
    "net/http"
    "sync"
    "testing"
    "time"
)

func TestHystrixRequestWithErr(t *testing.T) {
    hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
        Timeout:                1000, // 超时时间1秒
        MaxConcurrentRequests:  20,   // 最大并发数量20
        SleepWindow:            1000, // 窗口时间1秒,熔断开启1秒后尝试重试
        RequestVolumeThreshold: 5,    // 10秒钟请求数量超过5次,启动熔断器判断
        ErrorPercentThreshold:  50,   // 请求数超过5并且错误率达到百分之50,开启熔断
    })
    wg := new(sync.WaitGroup)

    // 模拟并发10次请求,5次返回err,导致熔断器开启
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                if i%2 == 0 {
                    return errors.New("测试错误!")
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为开启状态
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 睡眠1秒,转换为半开状态,并发请求10次,应该会有一个goroutine真正去请求,返回成功,其它请求直接走fallback逻辑
    time.Sleep(1 * time.Second)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 熔断器已经由半开转为关闭状态,请求应该全部成功
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")
}

func TestHystrixRequestWithOverLimit(t *testing.T) {
    hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
        Timeout:                1000,  // 超时时间1秒
        MaxConcurrentRequests:  20,    // 最大并发数量20
        SleepWindow:            10000, // 窗口时间1秒,熔断开启10秒后尝试重试
        RequestVolumeThreshold: 5,     // 10秒钟请求数量超过5次,启动熔断器判断
        ErrorPercentThreshold:  50,    // 请求数超过5并且错误率达到百分之50,开启熔断
    })

    wg := new(sync.WaitGroup)
    // 模拟并发大于20
    for i := 0; i < 40; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为开启状态
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为半开启状态
    time.Sleep(10 * time.Second)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为关闭状态
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")
}

func TestHystrixRequestWithTimeout(t *testing.T) {
    hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
        Timeout:                1000,  // 超时时间1秒
        MaxConcurrentRequests:  20,    // 最大并发数量20
        SleepWindow:            10000, // 窗口时间10秒,熔断开启1秒后尝试重试
        RequestVolumeThreshold: 5,     // 10秒钟请求数量超过5次,启动熔断器判断
        ErrorPercentThreshold:  50,    // 请求数超过5并且错误率达到百分之50,开启熔断
    })

    wg := new(sync.WaitGroup)
    // 模拟请求超时
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                if i%2 == 0 {
                    time.Sleep(1 * time.Second)
                    return nil
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为开启状态
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为半开启状态
    time.Sleep(10 * time.Second)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")

    // 继续模拟10次请求,熔断器应该为关闭状态
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("请求失败, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
    fmt.Println("----------------")
}
Sentinel-Go实现熔断

示例:gin中使用sentinel在middleware实现限流

func FlowControl(c *gin.Context) {
	//初始化sentinel
	err := sentinel.InitDefault()
	if err != nil {
		log.Fatalf("初始化sentinel失败:%v", err)
	}
	//配置限流规则 可以根据resource配置多个规则
	_, err = flow.LoadRules([]*flow.Rule{

		//rule1规则:1000ms内最多处理2个请求,多余的直接拒绝
		{
			Resource:               "rule1",     //规则的名称
			TokenCalculateStrategy: flow.Direct, //当前流量控制器的Token计算策略。Direct表示直接使用字段 Threshold 作为阈值;WarmUp表示使用预热方式计算Token的阈值。
			ControlBehavior:        flow.Reject, //表示流量控制器的控制策略;Reject表示超过阈值直接拒绝,Throttling表示匀速排队。
			Threshold:              2,           //表示流控阈值;如果字段 StatIntervalInMs 是1000(
			// 也就是1秒),那么Threshold就表示QPS,流量控制器也就会依据资源的QPS来做流控。
			StatIntervalInMs: 1000, //StatIntervalInMs 和 Threshold 这两个字段,这两个字段决定了流量控制器的灵敏度。以 Direct + Reject 的流控策略为例,流量控制器的行为就是在 StatIntervalInMs 周期内,允许的最大请求数量是Threshold。比如如果 StatIntervalInMs 是 10000,Threshold 是10000,那么流量控制器的行为就是控制该资源10s内运行最多10000次访问。
		},
	})
	if err != nil {
		log.Fatalf("初始化sentinel加载限流规则失败:%v", err)
	}
	//最终的限流实现通过这个方法实现
	e, b := sentinel.Entry("rule1", sentinel.WithTrafficType(base.Inbound))
	if b != nil {
		log.Println("被限流了\n")
		c.JSON(403, gin.H{"code": 403, "msg": "限流了"})
	} else {
		log.Println("未限流\n")
		c.Next()
		e.Exit()
	}
}

通过jmeter发送20个请求,并查看结果

服务降级

当流量出现激增,触发限流,那么对于那些系统暂时不想或无法处理的“流量”,我们该如何处理呢?

服务降级本质提供降低系统正常运行所能提供的功能数,亦或是降低某些功能完成的完整度(质量)。

熔断

熔断本质是在流量过大时(或下游服务出现问题时),可以自动断开与下游服务的交互,并可以通过自我诊断下游系统的错误是否已经修正,或上游流量是否减少至正常水平,来恢复自我恢复。

降级一般而言指的是我们自身的系统出现了故障而降级。而熔断一般是指依赖的外部接口出现故障的情况断绝和外部接口的关系。

例如:

例如你的A服务里面的一个功能依赖B服务,这时候B服务出问题了,返回的很慢。这种情况可能会因为这么一个功能而拖慢了A服务里面的所有功能,因此我们这时候就需要熔断!即当发现A要调用这B时就直接返回错误(或者返回其他默认值啊啥的),就不去请求B了。我这还是举了两个服务的调用,有些那真的是一环扣一环,出问题不熔断,那真的是会雪崩。

总结:

  • 限流 规定一个上限,流量超过系统承载能力时,会直接拒绝服务
  • 熔断 不因底层旁路应用的故障,造成系统雪崩
  • 降级 从请求入口,大范围的灭掉过载请求
  • 预热 给系统一些启动预热时间,加载缓存,避免资源死锁

简单来讲,只要流量不进系统,什么都好说,降级的作用;一旦流量进入系统,就要接受系统内一系列规则的制约,其中限流是最直接的手段,将请求拦在外面。虽然用户的请求失败了,但我的系统还能活;而熔断,很容易让三流功能影响主要功能,所以要在合适的时候打开它;至于预热,不过是在爱情火花前的一系列前戏,直到服务的巅峰状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值