在上文中我们将单体应用迁移到微服务架构的过程中,通过采用Consul作为服务注册与发现中心以及Jaeger实施链路追踪后,结合Docker Compose进行服务编排与启动,已经为微服务架构打下了坚实的基础。为了进一步提升服务的可用性、稳定性和容错性。我们这文将引入服务治理中的熔断、降级和限流策略。在服务入口或关键服务调用处实施请求限流,并定义降级逻辑,当达到预设阈值时,优先保证核心服务的稳定运行,牺牲非关键服务或功能,以维持整体服务的稳定,从而实现一个暴打校园选课服务。
能学到什么
- 如何使用go-sentinel
- 针对于参数级别的限流控制
- 如何进行熔断与降级
涉及技术
- Sentinel
- Circuit-broke(熔断)
- Limiter(限流)
架构设计
文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
服务治理
Sentinel的介绍和使用
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
官网地址:sentinelguard.io/zh-cn/docs/…文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
由于go-sentinel的使用文章不多这里作者也是摸着石头过河的,若有什么不对的地方还请指点指点。这里我们只使用到了sentinel组件的熔断降级、热点流量防护机制。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
熔断降级
熔断机制好比电路中的保险丝,其核心思想是在服务调用过程中,当检测到某个服务的错误率超过预设阈值或响应时间过长时,自动“打开”熔断器,即停止对这个服务的进一步调用,转而直接返回一个错误响应或默认结果给客户端,避免因为反复尝试失败的调用而占用系统资源,甚至拖垮整个系统。这里可以进行进一步的了解一下熔断器模型、熔断状态机。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
目的:快速隔离故障服务,防止故障扩散,保护系统免受雪崩效应的影响。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
在sentinel支持多种熔断策略:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
- 慢调用比例策略:可以认为是一种针对于请求时长的慢调。
- 错误比例策略 :错误率比例很好理解达到比例就行熔断。
- 错误计数策略(本文使用)
错误计数策略
本文在何地使用熔断机制?,为什么使用错误计数策略不使用其他策略呢?文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
回顾一下我们这个demo最主要的功能就是选课和退课功能,其中选课操作都是进行一个预扣减库存的操作然后丢入消息队列进行异步处理。其实请求生命周期非常的短暂,且计算操作都是才内存层面上的,这里就可以pass掉慢调用比例策略,在来看我们使用Redis的Lua脚本的原子性来避免并发问题,但在Lua脚本中存在大量的对Redis操作命令,且针对于单线程写操作,在突发大量请求涌入的情况下可能对redis单服务器造成压力导致“雪崩效应”。在这里我们可以进行熔断,我们选择了错误计数策略,对于错误比例策略来说最好是计算一个合适的比例,于是就不进行考虑了。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
如何使用
这里我们针对Lua脚本的执行进行使用文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15899.html
初始化 sentinel
进行初始化sentinel,可以使用默认配置或者是来源于外部的配置。
go
// init sentinel
if err := sentinel.InitWithConfigFile("./sentinel.yml"); err != nil {
logger.Logger.Error("sentinel init error for: %v", zap.Error(err))
panic(err)
}
配置文件
yaml
version: "v1"
sentinel:
app:
name: select-course
type: 0
log:
dir: "./logs/csp" // 指定日志目录,这里后期可以进行提供prometheus指标统计使用
pid: false
metric:
maxFileCount: 14
flushIntervalSec: 1 // 刷新指标间隔时间
stat:
system:
collectIntervalMs: 1000 // 收集指标间隔时间
注册和加载熔断器
go
var (
BreakError = errors.New("触发熔断")
)
// ErrorCountRules 熔断规则: 错误次数
var ErrorCountRules = []*circuitbreaker.Rule{
{
//executeLuaScript
Resource: "executeLuaScript", // 资源名称,推荐调用方法调用名称,或者保护段名称。
Strategy: circuitbreaker.ErrorCount, // 基于错误即使
RetryTimeoutMs: 3000,
MinRequestAmount: 10,
StatIntervalMs: 5000,
StatSlidingWindowBucketCount: 10,
Threshold: 5, // 5次错误 10次请求其中有5次错误,触发熔断
},
}
// StateChangeTestListener 熔断器
type StateChangeTestListener struct {
}
func (s *StateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {
fmt.Println("熔断关闭")
}
func (s *StateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {
fmt.Println("opening")
}
func (s *StateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {
fmt.Println("熔断恢复")
}
// load breaker
circuitbreaker.RegisterStateChangeListeners(StateChangeTestListener{}) //注册回调方法,三种状态出发的回调
// 加载策略
if _, err = circuitbreaker.LoadRules(breaker.ErrorCountRules); err != nil {
logger.Logger.Error("breaker init error for: %v", zap.Error(err))
panic(err)
}
如果触发了回调可以进行进一步的操作,例如通知运维,记录日志监控、服务降级、流量切换等动作。这里就只简单的打印触发信息。
熔断 埋点
go
func executeLuaScript(ctx context.Context, rdb *redis.Client, script *redis.Script, keys []string, args ...interface{}) (int64, error) {
// sentinel
entry, errBroke := sentinel.Entry("executeLuaScript") //这里需要指定刚刚设置的熔断名称
// 如果errBroke非nil,则触发熔断,进行快速失败(降级,静态返回)
if errBroke != nil {
Logger.Warn("open circuit breaker", zap.Error(errBroke))
return 0, breaker.BreakError
}
defer entry.Exit() // 顺利通过
// 模拟执行错误,进行熔断踩点。
if TestRandoBroke && rand.Int()%2 == 0 {
sentinel.TraceError(entry, errors.New("biz error"))
Logger.Warn("circuit breaker")
return 0, errors.New("模拟错误")
}
val, err := script.Run(ctx, rdb, keys, args...).Result()
if err != nil {
sentinel.TraceError(entry, err)
Logger.Warn("执行lua脚本失败", zap.Error(err))
return 0, err
}
return val.(int64), nil
}
测试
这里模拟100个用户进行选课,进行抛出异常进入埋点出发熔断机制。可以看到这里进行调用了OnTransformToOpen
回调,当进入了open状态,熔断器会进行通过适量的请求去尝试是否可恢复状态。
小结
其实触发了熔断机制即熔断器处于open状态,这里就有点降级的意思,进行快速错误,静态返回的功效。
热点参数流控
在使用热点参数流控前我是使用了sentinel的流量控制机制。 其实这两者差别还是蛮大的,可以来说流量控制机制是针对某个接口的总访问量限制,热点参数流控可以细粒度到针对某个参数级别的控制。针对于选课的场景下使用热点参数流控是无疑的,针对某个用户标识进行选课控制,防止用户选课期间使用恶意工具进行暴力选课,造成服务死亡。对于流量控制机制单个用户大量请求造成其他用户排不上的情况。
其实这里热点参数流控应该放入在网关层的,这样可以避免过多的无用调用服务
热点参数流控支持多种策略
- 基于请求数控制热点参数:并发数
- 基于并发数控制热点参数:QPS
本文选择的是基于QPS的并发数控制热点参数策略(基于某个用户的请求频率限制)
加载流控策略
这里需要注意的是ParamIndex
需要针对埋点传入的索引,也就是针对某个字段进行限制。这里并没有放入到选课的功能里而是放入到GetMyCourses
方法里。
go
var LimitRules = []*hotspot.Rule{
{
Resource: "GetMyCourses",
MetricType: hotspot.QPS, // 基于QPS
ControlBehavior: hotspot.Reject, //直接拒绝,
ParamIndex: 0, // 0:第一个参数
Threshold: 50, // 每秒请求数不超过50,针对某个参数
DurationInSec: 1, // 1:统计时间间隔
},
}
// load limiter
if _, err := hotspot.LoadRules(limiter.LimitRules); err != nil {
logger.Logger.Error("limiter init error for: %v", zap.Error(err))
panic(err)
}
埋点
值得注意的是sentinel.WithArgs(request.UserId)
,这里需要携带流控参数,参数类型为any
,也就是说可以传入一个结构体,结构体可以包含用户ID和课程ID。那么可以说精确到针对于某个用户选的课程请求进行流控了。参数可以传多个,但是需要匹配到刚刚设置的规则的ParamIndex
进行参数流控
go
// limiter
e, b := sentinel.Entry("GetMyCourses", sentinel.WithArgs(request.UserId))
if b != nil {
// 触发流控
fmt.Println("GetMyCourses: LimitTrigger")
// 对该用户进行限流
return &course.GetMyCoursesResponse{
StatusMsg: code.LimitTriggerMsg,
StatusCode: code.LimitTrigger,
}, nil
}
defer e.Exit()
测试
这里进行模拟单个用户1秒内发送50个请求,可以看到并没有触发LimitTrigger
流控策略
这里进行模拟单个用户1秒内发送51个请求,可以触发LimitTrigger
流控策略
总结
在本次内容回顾中,我们探讨了如何在微服务架构下,利用go-sentinel来实现服务治理的关键措施——熔断降级和热点参数流控,以此增强系统的稳定性和鲁棒性。
- 熔断降级的实施:
- 目的:快速隔离故障服务,防止雪崩效应。
- 策略:采用错误计数策略,当错误请求达到一定比例时触发熔断。
- 热点参数流控:
- 目的:对特定参数(如用户ID)的访问进行细粒度控制,防止科技用户过载行为(这里可以进行恶意检查关小黑屋)。
- 策略:基于QPS的并发数控制,限制单个参数的请求频率。
- 实践:定义流控规则,加载至Sentinel,并在相关函数中通过埋点参数实现流控。
评论