服务容错-熔断策略之断路器hystrix-go

news2025/1/11 14:32:47

文章目录

    • 概要
    • 一、服务熔断
    • 二、断路器模式
    • 三、hystrix-go
        • 3.1、使用
        • 3.2、源码
    • 四、参考

概要

微服务先行者Martin Fowler与James Lewis在文章microservices中指出了微服务的九大特征,其中一个便是容错性设计(Design for failure)。正如文章中提到的,微服务相对于单体服务而言,不同服务之间的通信是经过网络完成的,服务间调用时,上游服务可能随时处于不可用状态(比如崩溃,达到服务最大处理能力等等原因)。

由此会引发一个问题,一个服务点的错误经过层层传递,最终会波及到调用链上的所有服务,这便是雪崩效应,因此如何防止雪崩效应便是微服务架构容错性设计原则的具体实践,否则服务化程度越高,整个系统反而越不稳定。

在实践中有很多容错方案,诸如故障转移、快速失败(服务熔断)、安全失败、沉默失败、故障恢复、负载均衡、重试、限流、服务降级、舱壁隔离等等。这些方案分别从事前(负载均衡、限流、舱壁隔离、服务降级),事中(故障转移、快速失败、安全失败、沉默失败、重试),事后(故障恢复)三个节点提高整个系统的稳定性。

PS:服务降级归到事前,主要是因为服务降级大多数情况下不是在出现错误后才被执行的,在许多场景中,所说的服务降级更多的是指需要主动使服务进入降级逻辑的情况,比如电商预见双11流量高峰、游戏停服更新等。

一、服务熔断

服务熔断策略方案来源于生活中的电路保险丝,电路保险丝遵循一家一个的原则,当该家庭电流增大到一定数值时,其自身熔断而切断电路,保护电视机、冰箱等电器,并不会影响其他家庭的用电。
电路保险丝

同理,可推理到微服务之间的网络调用。
微服务网络调用
如图,当服务C出现异常构,服务B很快会检测到服务C不可用(服务C接口超时或错误等指标满足不可用判定条件),此时服务B不在将请求转发到服务C,而是快速返回错误信息(快速失败)。在一段时间内的后续请求就一直返回失败,稍后当检测到服务C接口调用响应正常后,就会恢复到正常状态。

二、断路器模式

断路器模式是实现熔断策略的具体方案,其本质是接管微服务之间的远程调用请求,断路器会持续监控并统计被调用服务接口返回成功、失败、超时、拒绝等各种结果的指标,当某一个指标满足预设阈值时,断路器就会进入开启状态,后续相应的远程调用请求就会快速返回错误信息,而不会真的对被调用服务发起请求。若干时间后断路器会进入半打开状态,此时断路器会放行一次请求,如果请求正常,则断路器进入关闭状态,否则转入开启状态。

从上面描述来看,断路器是一种有限状态机:
断路器状态变更示意图

  • 关闭状态,此时断路器会放行请求到上游服务,该状态是断路器的初始状态;
  • 开启状态,当断路器统计的某一项指标满足开启条件时就会进入该状态,此时不会放行请求到上游服务,而是快速返回错误信息;
  • 半打开状态,这时一种中间状态,主要是因为断路器要具有故障恢复的能力,所以当进入该状态时,断路器会允许放行一次请求到上游服务。一般是在断路器开启后若干时间后自动进入该状态。

断路器进入半打开状态在实现时并不需要计时器,而是收到请求时检测下是否满足半打开状态(一般是将断路器开启时间与当前时间做比较),是的话就放行该次请求,否则快速返回错误信息。

断路器工作时序图如下:
断路器时序图

三、hystrix-go

hystrix-go是作者从JAVA Netflix的子项目Hystrix翻译过来的,很经典的断路器项目。

3.1、使用

hystrix-go 调用接口有两个:

  • Do:同步调用
func Do(name string, run runFunc, fallback fallbackFunc)
  • Go:异步调用
func Go(name string, run runFunc, fallback fallbackFunc)

hystrix-go配置项:

// CommandConfig is used to tune circuit settings at runtime
type CommandConfig struct {
	Timeout                int `json:"timeout"`
	MaxConcurrentRequests  int `json:"max_concurrent_requests"`
	RequestVolumeThreshold int `json:"request_volume_threshold"`
	SleepWindow            int `json:"sleep_window"`
	ErrorPercentThreshold  int `json:"error_percent_threshold"`
}
  • MaxConcurrentRequests:请求的并发量,接口并发超过该值也会被归为接口错误(ErrMaxConcurrency);
  • Timeout:请求超时时间,接口响应时间超过该值也会归为接口错误(ErrTimeout);
  • RequestVolumeThreshold:一个窗口(代码里写死的10秒)内的请求数阙值,达到这个阙值才会进入接口错误百分比计算逻辑;
  • ErrorPercentThreshold :设置接口错误(除了ErrMaxConcurrency,ErrTimeout两种错误,接口自身错误也会被计入)的百分比,大于该值断路器就会进入开启状态;
  • SleepWindow:断路器开启后,多久后进入半开启状态。

直接上代码。

import (
	"errors"
	"fmt"
	"github.com/afex/hystrix-go/hystrix"
	"time"
)
var (
	global error
	times  int
)
//模拟远程请求
func mockHttp() error {
	times++
	fmt.Println(times)
	if global != nil {
		return nil
	}
	time.Sleep(2 * time.Second)
	return errors.New("业务出错")
}
const breakFlag = "testBreaker"
func main() {
    hystrix.ConfigureCommand(breakFlag, hystrix.CommandConfig{
		Timeout:                1000, 
		MaxConcurrentRequests:  50,   
		ErrorPercentThreshold:  25,   
		RequestVolumeThreshold: 4,    
		SleepWindow:            1000, 
	})
	//hystrix.SetLogger() //打印断流器内部日志
	for i := 0; i < 10; i++ {
		time.Sleep(time.Millisecond * 400) //给熔断器重试服务时机
		_ = hystrix.Do(breakFlag, func() error {
			return mockHttp()
		}, func(err error) error { //不发生错误不会进入该逻辑的
			if err != nil {
				fmt.Printf("times:%d,断路器检测到错误:%s\n", times, err.Error())
			} else {
				fmt.Printf("times:%d,断路器恢复正常", times)
			}
			global = err
			return nil
		})
	}
	fmt.Println("times:", times)
}

输出如下:

1
times:1,断路器检测到错误:hystrix: timeout
2
3
4
times:4,断路器检测到错误:hystrix: circuit open
times:4,断路器检测到错误:hystrix: circuit open
times:4,断路器检测到错误:hystrix: circuit open
5
6
7
times: 7

分析:
可以看到真正发出的请求是7次,3次是被快速失败了

  1. 第一次请求接口超时;
  2. 第四次请求时,10s内的请求4个了,满足RequestVolumeThreshold配置,此时错误接口个数是1,计算1/4*100等于25,不小于ErrorPercentThreshold配置,断路器进入开启状态;
  3. 第五、六、七次的请求都被快速失败了;
  4. 第八次请求时,满足断路器进入半开启状态的条件(time.Millisecond * 400*3>=SleepWindow),放行本次请求,并且请求响应正常,那么断路器进入关闭状态;
  5. 第九、十次正常。
3.2、源码

Do和Go两个API最终都会进入GoC函数

func GoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) chan error {
	cmd := &command{
		run:      run,
		fallback: fallback,
		start:    time.Now(),
		errChan:  make(chan error, 1),
		finished: make(chan bool, 1),
	}

	circuit, _, err := GetCircuit(name)//获取指标统计器
	if err != nil {
		cmd.errChan <- err
		return cmd.errChan
	}
	cmd.circuit = circuit
	ticketCond := sync.NewCond(cmd)
	ticketChecked := false
	returnTicket := func() {
		cmd.Lock()
		// Avoid releasing before a ticket is acquired.
		for !ticketChecked {
			ticketCond.Wait()
		}
		cmd.circuit.executorPool.Return(cmd.ticket)//执行完之后归还请求令牌
		cmd.Unlock()
	}
	// Shared by the following two goroutines. It ensures only the faster
	// goroutine runs errWithFallback() and reportAllEvent().
	returnOnce := &sync.Once{}
	reportAllEvent := func() {
		err := cmd.circuit.ReportEvent(cmd.events, cmd.start, cmd.runDuration)//上报此次请求时正常还是异常,便于后续进行指标统计
		if err != nil {
			log.Printf(err.Error())
		}
	}

	go func() {
		defer func() { cmd.finished <- true }()

		if !cmd.circuit.AllowRequest() {//统计指标,决定开启、半开启、关闭三个状态的流转
			cmd.Lock()
			// It's safe for another goroutine to go ahead releasing a nil ticket.
			ticketChecked = true
			ticketCond.Signal()
			cmd.Unlock()
			returnOnce.Do(func() {
				returnTicket()
				cmd.errorWithFallback(ctx, ErrCircuitOpen)//上报断路器处于开启状态的错误,不过该错误不会被纳入接口错误指标
				reportAllEvent()
			})
			return
		}
		cmd.Lock()
		select {
		case cmd.ticket = <-circuit.executorPool.Tickets://获取一个请求令牌
			ticketChecked = true
			ticketCond.Signal()
			cmd.Unlock()
		default:  //没有令牌,就表示请求达到并发限制MaxConcurrentRequests配置的值,上报ErrMaxConcurrency错误
			ticketChecked = true
			ticketCond.Signal()
			cmd.Unlock()
			returnOnce.Do(func() {
				returnTicket()
				cmd.errorWithFallback(ctx, ErrMaxConcurrency)
				reportAllEvent()
			})
			return
		}

		runStart := time.Now()
		runErr := run(ctx)  //没有达到限流就发起请求
		returnOnce.Do(func() {
			defer reportAllEvent()
			cmd.runDuration = time.Since(runStart)
			returnTicket()
			if runErr != nil {
				cmd.errorWithFallback(ctx, runErr) //出错就上报业务接口的错误
				return
			}
			cmd.reportEvent("success")//表示请求成功
		})
	}()

	go func() {
		timer := time.NewTimer(getSettings(name).Timeout)//根据Timeout配置起一个定时器
		defer timer.Stop()

		select {
		case <-cmd.finished:  //请求执行完毕
			// returnOnce has been executed in another goroutine
		case <-ctx.Done(): //收集context上下文错误
			returnOnce.Do(func() {
				returnTicket()
				cmd.errorWithFallback(ctx, ctx.Err())
				reportAllEvent()
			})
			return
		case <-timer.C: //标识服务接口超时,上报ErrTimeout错误
			returnOnce.Do(func() {
				returnTicket()
				cmd.errorWithFallback(ctx, ErrTimeout)
				reportAllEvent()
			})
			return
		}
	}()

	return cmd.errChan
}

进入开启状态

func (circuit *CircuitBreaker) AllowRequest() bool {
	return !circuit.IsOpen() || circuit.allowSingleTest()
}
//判断断路器处于关闭状态还是开启状态
func (circuit *CircuitBreaker) IsOpen() bool {
	circuit.mutex.RLock()
	o := circuit.forceOpen || circuit.open
	circuit.mutex.RUnlock()

	if o {
		return true
	}

	if uint64(circuit.metrics.Requests().Sum(time.Now())) < getSettings(circuit.Name).RequestVolumeThreshold {
		return false
	}

	if !circuit.metrics.IsHealthy(time.Now()) {//计算10s内错误请求百分比
		// too many failures, open the circuit
		circuit.setOpen()         //断路器状态为开启状态
		return true
	}

	return false
}

//circuit.metrics.Requests().Sum方法,这里可以看到统计指标的窗口是10s
func (r *Number) Sum(now time.Time) float64 {
	sum := float64(0)

	r.Mutex.RLock()
	defer r.Mutex.RUnlock()

	for timestamp, bucket := range r.Buckets {
		// TODO: configurable rolling window
		if timestamp >= now.Unix()-10 {
			sum += bucket.Value
		}
	}

	return sum
}

断路器半开启状态判断

func (circuit *CircuitBreaker) allowSingleTest() bool {
	circuit.mutex.RLock()
	defer circuit.mutex.RUnlock()

	now := time.Now().UnixNano()
	openedOrLastTestedTime := atomic.LoadInt64(&circuit.openedOrLastTestedTime)
	//如果断路器处于开启状态,且当前时间>断路器开启时间+SleepWindow配置,精确到纳秒,则进入半开启状态
	if circuit.open && now > openedOrLastTestedTime+getSettings(circuit.Name).SleepWindow.Nanoseconds() {
		swapped := atomic.CompareAndSwapInt64(&circuit.openedOrLastTestedTime, openedOrLastTestedTime, now)
		if swapped {
			log.Printf("hystrix-go: allowing single test to possibly close circuit %v", circuit.Name)
		}
		return swapped
	}

	return false
}

恢复为关闭状态

func (circuit *CircuitBreaker) ReportEvent(eventTypes []string, start time.Time, runDuration time.Duration) error {
	if len(eventTypes) == 0 {
		return fmt.Errorf("no event types sent for metrics")
	}

	circuit.mutex.RLock()
	o := circuit.open
	circuit.mutex.RUnlock()
	if eventTypes[0] == "success" && o {//此次请求成功,且断路器处于开启状态,则将断路器转为关闭状态
		circuit.setClose()
	}

	//省略代码...
	return nil
}

四、参考

1]:服务治理:熔断器介绍以及hystrix-go的使用
2]:Microservices

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宋仕强论道之华强北精神和文化(二十一)

华强北的精神会内化再提炼和升华成为华强北文化&#xff0c;在外部会流传下去和传播开来。在事实上的行动层面&#xff0c;就是华强北人的思维方式和行为习惯&#xff0c;即见到机会就奋不顾身敢闯敢赌&#xff0c;在看似没有机会的时候拼出机会&#xff0c;和经济学家哈耶克企…

【OSG案例详细分析与讲解】之四:【3D动画场景】

文章目录 一、【3D动画场景】前言 二、【3D动画场景】实现效果 三、【3D动画场景】创建动画路径 1、实现目的 2、创建动画路径步骤 3、核心代码 4、知识要点 5、AnimationPath详讲 四、【3D动画场景】创建基础模型 1、实现目的 2、创建基础模型步骤 3、核心代码 4、知识要点 5、…

openssl3.2 - 官方dmeo学习 - server-arg.c

文章目录 openssl3.2 - 官方dmeo学习 - server-arg.c概述笔记备注END openssl3.2 - 官方dmeo学习 - server-arg.c 概述 TLS服务器, 等客户端来连接; 如果客户端断开了, 通过释放bio来释放客户端socket, 然后继续通过bio读来aceept. 笔记 对于开源工程, 不可能有作者那么熟悉…

uniapp 字母索引列表插件(组件版) Ba-SortList

简介&#xff08;下载地址&#xff09; Ba-SortList 是一款字母索引列表组件版插件&#xff0c;可自定义样式&#xff0c;支持首字母字母检索、首字检索、搜索等等&#xff1b;支持点击事件。 支持首字母字母检索支持首字检索支持搜索支持点击事件支持长按事件支持在uniapp界…

VMware vSphere运维管理手册

适用版本:VMware vSphere 7.0 VMware vSphere 是 VMware 的虚拟化平台,可将数据中心转换为包括 CPU、存储和网络资源的聚合计算基础架构。vSphere 将这些基础架构作为一个统一的运行环境进行管理,并为您提供工具来管理加入该环境的数据中心。 ![[Pasted image 20231212132…

数据库原理与应用期末复习试卷1

数据库原理与应用期末复习试卷1 一.单项选择题 数据库系统是采用了数据库技术的计算机系统&#xff0c;由系统数据库&#xff0c;数据库管理系统&#xff0c;应用系统和&#xff08;C&#xff09;组成。 ​ A.系统分析员 B.程序员 C.数据库管理员 D.操作员 数据库系统的体系…

YOLOv5改进 | 检测头篇 | DynamicHead支持检测和分割(不同于网上版本,全网首发)

一、本文介绍 本文给大家带来的改进机制是DynamicHead(Dyhead),这个检测头由微软提出的一种名为“动态头”的新型检测头,用于统一尺度感知、空间感知和任务感知。网络上关于该检测头我查了一些有一些魔改的版本,但是我觉得其已经改变了该检测头的本质,因为往往一些细节上才…

解决STM32F7系列芯片TIM无法触发ADC采样的问题

我在测试STM32F746 ADC DMA TIM 做AD采样时候发现 使用cubeMX 库生成的代码无法进入DMA中断&#xff0c;发现官方勘误手册有做解释&#xff0c;需要打开DAC时钟。如下 如上图&#xff0c;在ADC初始化代码中加入 __HAL_RCC_DAC_CLK_ENABLE();

统一密钥管理在信息安全领域有什么作用

统一密钥管理在信息安全领域中至关重要。它可以确保密钥的安全性、保密性和可用性&#xff0c;同时降低开发、维护和管理的成本。 对于没有KMS(密钥管理服务)管理系统的公司&#xff0c;密钥的本地化管理可能导致密钥分散在代码、配置文件中&#xff0c;缺乏统一管理&#xff0…

999合1超级简易文字菜单组件

引言 超级简易文字菜单组件 大家好&#xff0c;相信大家都玩过那个999合1的游戏&#xff0c;特别是那个菜单。 在游戏开发中&#xff0c;往往会有形式各异的游戏菜单&#xff0c;游戏菜单和菜单一样主要是给玩家选择开始游戏、设置、排行榜等功能的。 本文将分享一下笔者在…

【MMC子系统】四、MMC控制器驱动层

我的圈子&#xff1a; 高级工程师聚集地 我是董哥&#xff0c;高级嵌入式软件开发工程师&#xff0c;从事嵌入式Linux驱动开发和系统开发&#xff0c;曾就职于世界500强企业&#xff01; 创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01; …

网络服务DHCP与DNS

一 DHCP的工作原理&#xff08;租约过程&#xff09; 分类 1&#xff09;自动分配&#xff1a;分配到一个IP地址后永久使用 &#xff08;2&#xff09;手动分配&#xff1a;由DHCP服务器管理员指定IP&#xff08;打印机、报销系统&#xff09;把mac地址和ip地址做一个一一对…

猫粮对比:性价比高的主食冻干猫粮推荐

虽然很多铲屎官可能认为给猫咪喂猫粮就足够了&#xff0c;但实际上猫咪对蛋白质的需求很高&#xff0c;并且作为肉食动物&#xff0c;它们更喜欢肉的味道。而冻干猫粮是采用低温和真空干燥处理技术将鲜肉制成&#xff0c;去除水分并保持蛋白质等营养物质不变性&#xff0c;同时…

办公自动应用,HR大屏可视化模板

大家可以进行资料下载。 完整的案例。 AIGC ChatGPT 职场案例 AI 绘画 与 短视频制作 PowerBI 商业智能 68集 数据库Mysql 8.0 54集 数据库Oracle 21C 142集 Office 2021实战应用 Python 数据分析实战&#xff0c; ETL Informatica 数据仓库案例实战 Excel 2021实操 100集&a…

premiere简约大气3D动画logo片头Pr模板Mogrt免费下载

Premiere简约大气3D动画logo片头pr模板mogrt下载&#xff0c;无需插件&#xff0c;高清分辨率&#xff0c;易于自定义&#xff0c;包括教程&#xff0c;不包括音频和图像。免费下载&#xff1a;https://prmuban.com/37065.html

路由黑洞和黑洞路由的区别

路由黑洞&#xff1a; 路由黑洞是一种现象&#xff0c;一般是在网络边界做汇总回程路由的时候产生的一种不太愿意出现的现象&#xff0c;就是汇总的时候有时会有一些不在内网中存在的网段&#xff0c;但是又包含在汇总后的网段中&#xff0c;如果在这个汇总的边界设备上同时还配…

【JVM】本地方法接口 Native Interface

一、JNI简介 JVM本地方法接口&#xff08;Java Native Interface&#xff0c;JNI&#xff09;是一种允许Java代码调用本地方法&#xff08;如C或C编写的方法&#xff09;的机制。这种技术通常用于实现高性能的计算密集型任务&#xff0c;或者与底层系统库进行交互。 二、JNI组…

西门子S7-1200与S7-300PLC的九大不同点

S7-1200作为新推出的紧凑型控制器&#xff0c;其产品定位在原有的SIMATIC S7-200和S7-300之间&#xff0c;它与S7-300的区别主要体现在硬件、通信、工程、存储器、功能块、计数器、定时器、工艺功能等方面。 一、硬件的区别 在硬件扩展方面&#xff0c;S7-300的主机架多支持八…

现代密码学 考点复盘

现代密码学 考点汇总&#xff08;上&#xff09; 写在最前面考试范围一、给一个简单的方案&#xff0c;判断是否cca安全二、随机预言机模型之下的简单应用 考试题目1.证明CBC方案是CPA安全的2. 证明哈希函数的抗碰撞性3. CBC-MAC安全&#xff1a;证明CPA安全的对称密钥加密方案…

Digital Audio (HDMI)未插入 用Hdmi连接电脑 显示高清数字音频未插入 win10电脑没声音,喇叭上一个叉❌

先说结论&#xff0c;出现这些问题的原因&#xff1a; 未插入音频设备或者硬件问题&#xff08;10%&#xff09;设置错误&#xff0c;未使用显示器音频 &#xff08;30%&#xff09;音频驱动不兼容或者没有驱动&#xff08;50%&#xff09;其他驱动有问题 &#xff08;10%&…