小米商城服务治理之客户端熔断器(Google SRE客户端熔断器)

目录

前言

一、什么是Google SRE熔断器

二、Google SRE 熔断器的工作流程:

三、客户端熔断器  (google SRE 熔断器) golang GRPC 实现

四、客户端熔断器 (google SRE 熔断器) golang GRPC单元测试


大家可以关注个人博客:xingxing – Web Developer from Somewhere   有关后端问题探讨

前言

当某个用户超过资源配额时,后端任务应该迅速拒绝该请求,返回一个“用户配额不足”类型的错误,该回复应该比真正处理该请求所消耗的资源少得多。然而,这种逻辑其实不适用于所有请求。例如,拒绝一个执行简单内存查询的请求可能跟实际执行该请求消耗内存差不多(因为这里主要的消耗是在应用层协议解析中,结果的产生部分很简单)。

就算在某些情况下,拒绝请求可以节省大量资源,发送这些拒绝回复仍然会消耗一定数量的资源。如果拒绝回复的数量也很多,这些资源消耗可能也十分可观。在这种情况下,有可能该后端在忙着不停地发送拒绝回复时一样会进人过载状态。

那么客户端截流机制就可以解决这个问题,也就是Google SRE

一、什么是Google SRE熔断器

是否可以做到在熔断器 Open 状态下(但是后端未 Shutdown)仍然可以放行少部分流量呢?Google SRE 熔断器提供了一种算法:客户端自适应限流(client-side throttling)。

解决的办法就是客户端自行限制请求速度,限制生成请求的数量,超过这个数量的请求直接在本地回复失败,而不会真正发送到服务端。

该算法统计的指标依赖如下两种,每个客户端记录过去两分钟内的以下信息(一般代码中以滑动窗口实现)。

  • requests:客户端请求总量

    • 注:The number of requests attempted by the application layer(at the client, on top of the adaptive throttling system)

  • accepts:成功的请求总量 - 被 accepted 的量

    • 注:The number of requests accepted by the backend

二、Google SRE 熔断器的工作流程:

  • 在通常情况下(无错误发生时) requests == accepts ;

  • 当后端出现异常情况时,accepts 的数量会逐渐小于 requests;

  • 当后端持续异常时,客户端可以继续发送请求直到 requests = K∗accepts,一旦超过这个值,客户端就启动自适应限流机制,新产生的请求在本地会被概率(以下称为p)丢弃;

  • 当客户端主动丢弃请求时,requests 值会一直增大,在某个时间点会超过 K∗accepts,使 p 计算出来的值大于 0,此时客户端会以此概率对请求做主动丢弃;

  • 当后端逐渐恢复时,accepts 增加,(同时 requests 值也会增加,但是由于 K 的关系,K*accepts的放大倍数更快),使得 (requests − K×accepts) / (requests + 1) 变为负数,从而 p == 0,客户端自适应限流结束。

客户端请求被拒绝的概率(Client request rejection probability,以下简称为 p)

p 基于如下公式计算(其中 K 为倍率 - multiplier,常用的值为 2)。

  • 当 requests − K∗accepts <= 0 时,p == 0,客户端不会主动丢弃请求;

  • 反之, p 会随着 accepts 值的变小而增加,即成功接受的请求数越少,本地丢弃请求的概率就越高。

客户端可以发送请求直到 requests = K∗accepts, 一旦超过限制, 按照 p 进行截流。

对于后端而言,调整 K 值可以使得自适应限流算法适配不同的服务场景

  • 降低 K 值会使自适应限流算法更加激进(允许客户端在算法启动时拒绝更多本地请求);

  • 增加 K 值会使自适应限流算法变得保守一些(允许服务端在算法启动时尝试接收更多的请求,与上面相反)。

熔断本质上是一种快速失败策略。旨在通过及时中断失败或超时的操作,防止资源过度消耗和请求堆积,从而避免服务因小问题而引发的雪崩效应。

三、客户端熔断器  (google SRE 熔断器) golang GRPC 实现

我们要考虑几个问题,第一个问题用哪种算法去做统计呢,我感觉用滑动窗口去统计比较合适,因为滑动窗口是统计一个周期内的请求以及响应.用户的响应也是随着周期性的变化的,这样就可以周期性的统计。

第二个问题是此算法在什么时候执行呢,就拿GRPC 来说,当然是拦截器呢,在发送后端服务请求的时候前就要去看是否要熔断,避免错误的请求发送到后端。

type googleSlide struct {
	sreSlide *list.List
	//滑动窗口大小
	interval int64
	mutex    sync.Mutex
	//客户端成功请求量的系数
	k float64
}

type slideVal struct {
	//客户端请求时间
	time int64
	//客户端的总请求量
	req float64
	//客户端成功请求量
	accept float64
}

type SlideValOptions func(val *slideVal)

func NewSlideval(options ...SlideValOptions) *slideVal {
	t := &slideVal{
		time: time.Now().UnixNano(),
	}
	for _, option := range options {
		option(t)
	}
	return t
}

func WithReqOption(req float64) SlideValOptions {
	return func(val *slideVal) {
		val.req = req
	}
}

func WithAcceptReqOption(accept float64) SlideValOptions {
	return func(val *slideVal) {
		val.accept = accept
	}
}

func NewGoogleSlide(interval time.Duration, k float64) *googleSlide {
	return &googleSlide{
		sreSlide: list.New(),
		interval: interval.Nanoseconds(),
		k:        k,
	}
}

func (g *googleSlide) Sre() grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		g.mutex.Lock()
		now := time.Now().UnixNano()
		front := g.sreSlide.Front()
		//调整滑动窗口
		for front != nil && front.Value.(*slideVal).time+g.interval < now {
			g.sreSlide.Remove(front)
			front = g.sreSlide.Front()
		}
		var r, accept float64
		front = g.sreSlide.Front()
		//当前滑动窗口下的请求和成功请求量的统计
		for front != nil {
			t := front.Value.(*slideVal)
			r += t.req
			accept += t.accept
			front = front.Next()
		}
		//客户端请求被拒绝的概率((requests − K×accepts) / (requests + 1))
		tail := (r - g.k*accept) / (r + 1)
		if tail > 0 {
			g.mutex.Unlock()
			return errors.New("request is fail")
		}
		g.sreSlide.PushBack(NewSlideval(WithReqOption(1)))
		err := invoker(ctx, method, req, req, cc, opts...)
		if err == nil {
			g.sreSlide.PushBack(NewSlideval(WithAcceptReqOption(1)))
		}
		g.mutex.Unlock()
		return err
	}
}

四、客户端熔断器 (google SRE 熔断器) golang GRPC单元测试

模拟客户端请求,handler 是正常的请求,handler1是返回有问题的请求,2 客户端熔断器的参数. 此值越小越激进,对服务端错误的容忍越小.

测试用例我说明下:

network is fail 是模拟服务端返回的错误,是要调用服务端,此时并不会限制,随着服务恢复,整个请求逐渐正常。

 request is fail 是熔断器返回的,不会调用服务端的,直接返回错误。这就是熔断器的魅力所在。

func TestGoogleSre(t *testing.T) {
   slide := NewGoogleSlide(5*time.Second, 2)
   builder := slide.Sre()
   // 模拟服务端正常的请求
   handler := func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
      return nil
   }
   //模拟服务端出问题
   handler1 := func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
      return errors.New("network is fail")
   }
   err := builder(context.Background(), "/test/a", &gen.GetByIdReq{}, &gen.GetByIDResp{}, nil, handler)
   assert.NoError(t, err)
   err = builder(context.Background(), "/test/a", &gen.GetByIdReq{}, &gen.GetByIDResp{}, nil, handler1)
   assert.Equal(t, err, errors.New("network is fail"))
   err = builder(context.Background(), "/test/a", &gen.GetByIdReq{}, &gen.GetByIDResp{}, nil, handler1)
   assert.Equal(t, err, errors.New("network is fail"))
   err = builder(context.Background(), "/test/a", &gen.GetByIdReq{}, &gen.GetByIDResp{}, nil, handler1)
   assert.Equal(t, err, errors.New("request is fail"))
   err = builder(context.Background(), "/test/a", &gen.GetByIdReq{}, &gen.GetByIDResp{}, nil, handler1)
   assert.Equal(t, err, errors.New("request is fail"))
   time.Sleep(5 * time.Second)
   err = builder(context.Background(), "/test/a", &gen.GetByIdReq{}, &gen.GetByIDResp{}, nil, handler)
   assert.NoError(t, err)
}

首先感谢《google SRE 》以及 腾讯微服务治理相关文章为我提供了深入的思考以及总结

代码或者测试用例如果有异议,请和我留言,大家一起探讨

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/363441.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【笔试常见易错选择题01】else、表达式、二维数组、%m.ns、%m.nf、常量指针和指针常量、宏定义、传参、数组越界、位段

1. 下列main()函数执行后的结果为&#xff08;&#xff09; int func(){ int i, j, k 0; for(i 0, j -1;j 0;i, j){ k; } return k; } int main(){cout << (func());return 0; }A. -1 B. 0 C. 1 D. 2 判断为赋值语句&#xff0c;j等于0 0为假不进循环 选B. 2. 下面程…

【日常总结】宝塔中 Gitlab服务器 forbidden

一、场景 二、问题 三、原因 四、解决方案 五、实战 Stage 1&#xff1a;打开 /etc/gitlab/gitlab.rb&#xff0c;并编辑 Stage 2&#xff1a;重启gitlab服务 Stage 3&#xff1a;测试&#xff08;打开girlab网页&#xff09; 六、后续 一、场景 公司更换新电脑 服务…

引用与常引用

1.定义 下面写法定义了一个引用&#xff0c;并将它进行初始化为引用某个变量。 类型名 & 引用名 某变量名&#xff1b; int n 4; int & r n; // r引用了n&#xff0c;r的类型是int & 某个变量的引用&#xff0c;等价于这个变量&#xff0c;相当于该变量…

共用体与枚举法,链表的学习

结构体注意事项&#xff1a; 1.结构体类型可以定义在main函数里面&#xff0c;但是此时的作用域就被限定在该函数中 2.结构体的的的定义的形式&#xff1a;a.先定义类型&#xff0c;后定义变量-----struct stu s b.定义类型的同时&#xff0c;定义了变量&#xff1a;struct…

C语言系列-联合

&#x1f308;个人主页: 会编程的果子君 ​&#x1f4ab;个人格言:“成为自己未来的主人~” 目录 联合体 联合体类型的声明 联合体的特点 相同成员的结构体和联合体对比 联合体大小的计算 联合的一个练习 联合体 联合体类型的声明 像结构体一样&#xff0c;联合体也是由…

幻兽帕鲁专用服务器搭建教程分享(手把手教学)

想要快速搭建幻兽帕鲁服务器&#xff0c;我们只需要参考以下教程即可轻易完成幻兽帕鲁服务器的搭建部署&#xff0c;与其他专用服务器游戏一样&#xff0c;可以让您和朋友在一个相对独立、稳定且私密的云端跨境中进行游戏&#xff0c;以获得更好、更流畅的游戏体验。 幻兽帕鲁游…

CYUSB3065配置CMOS

目录 目标Eclipse使用流程1.首先安装开发环境2.在SDK目录下找到开发工具Eclipse&#xff0c;并打开3.编译固件4.进入烧录模式5.烧录6.如何调试&#xff1f;串口调试GDB调试 配置CMOSCMOS转接电路两个参数新建CX3工程 当前问题 目标 这是Cypress的一个支持MIPI和UVC的模块&…

SAP同步异常5:继续处理获利能力异常WW291

Short text of error message: 派生过程中系统错误: CE01000-WW291 在ABAP辞典中不存在 Long text of error message: 诊断 带 CE01000-WW291 名字段派生参考的定制设置。这个字段在 ABAP 字典中不 存在。 系统响应 因为在定制中有矛盾&#xff0c;所以系统的程序崩溃。 步骤 …

类和对象-下篇

文章目录 一、再谈构造函数1. 构造函数体赋值2. 初始化列表3. explicit关键字 二、static成员1. 概念2. 特性 三、友元1. 友元函数2. 友元类 四、 内部类1. 概念2. 特性 五、再次理解封装 一、再谈构造函数 1. 构造函数体赋值 在创建对象时&#xff0c;编译器通过调用构造函数…

BFS的基本应用——flood fill 最短路

bfs的核心就是一层一层往外扩展&#xff0c;在bfs中会用到一个队列&#xff0c;又由于是一层一层往外扩展的&#xff0c;故而可以用来求连通的问题&#xff0c;同时相当于每次往外扩展的边权都是1&#xff0c;所以这个队列是有序的&#xff0c;相当于dijkstra算法中的优先队列&…

vue3学习——初始化项目及配置

初始化项目 环境 node 16pnpm 8.0.0 命令 pnpm create vite进行以下选择 &#x1f447; – 项目名 – VUe – Ts – cd/目录 – pnpm run dev 浏览器自动打开 package.json 配置eslint 安装依赖包 pnpm i eslint -D npx eslint --init // 生成配置文件进行以下选择 &a…

SpringBoot 使用WebSocket功能

实现步骤&#xff1a; 1.导入WebSocket坐标。 在pom.xml中增加依赖项&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>2.编写WebSocket配…

Redis学习——高级篇⑨

Redis学习——高级篇⑨ Redis7高级之Redlock算法和Redisson的使用&#xff08;十&#xff09; 10.1 Redlock 红锁算法1.解决手写分布式锁的单点故障问题2.设计理念3. 解决方案 10.2 Redisson进行代码改造10.3 多机案例&#xff08;解决单点故障&#xff09;10.4 R…

vue3使用is动态切换组件报错Vue received a Component which was made a reactive object.

vue3使用is动态切换组件&#xff0c;activeComponent用ref定义报错 Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef ins…

代码随想录算法训练营29期|day36任务以及具体安排

第八章 贪心算法 part05 435. 无重叠区间 class Solution {public int eraseOverlapIntervals(int[][] intervals) {Arrays.sort(intervals, (a,b)-> {return Integer.compare(a[0],b[0]);});if(intervals.length 1) return 0;int result 0;for(int i 1 ; i < interva…

结构体与共用体——共用体——C语言——day16

昨天介绍了下结构体&#xff0c;今天主要介绍共用体&#xff0c;枚举 共用体 概念&#xff1a;有时需要使几种不同类型的变量存放到同一段内存单元中。 例如&#xff0c;可把一个整型变量、一个字符型变量、一个浮点型变量放在同一个地址开始的内存单元中 。以上三个变量在内…

Python系列-字典

&#x1f308;个人主页: 会编程的果子君 ​&#x1f4ab;个人格言:“成为自己未来的主人~” ​ 目录 ​ 字典是什么 创建字典 查找key 新增/修改元素 删除元素 遍历字典元素 取出所有的key和value 合成的key类型 ​编辑 小结 字典是什么 字典是一种存储键值对的结…

云打印怎么收费?云打印需要付费吗?

随着云打印概念的火热发展&#xff0c;很多有打印需求的App或者个人用户都想使用易绘创云打印服务。那么易绘创云打印怎么收费&#xff1f;云打印需要付费吗&#xff1f;今天就带大家来了解一下。 云打印怎么收费&#xff1f;云打印需要付费吗&#xff1f; 很多有打印需求的小…

Linux网络状态查看与防火墙管理

网络状态查看 netstat [选项] Netstat是一款命令行工具&#xff0c;用于显示Linux系统中网络的状态信息&#xff0c;可以显示网络连接、路由表、连接的数据统计等信息。 使用 选项 -a&#xff1a;显示所有选项&#xff0c;包括监听和未监听的端口。 -t&#xff1a;仅显示tc…

IS-IS的LSP分片扩展

原理 IS-IS通过泛洪LSP来宣告链路状态信息,由于一个LSP能够承载的信息量有限,IS-IS将对LSP进行分片。每个LSP分片由产生该LSP的结点或伪结点的SystemID、PseudnodeID(普通LSP中该值为0,Pseudonode LSP中该值为非0)、LSPNumber(LSP分片号)组合起来唯一标识,由于LSPNumb…