17. 灰度开关、降级开关、灰度放量 您所在的位置:网站首页 miui125降级改代码 17. 灰度开关、降级开关、灰度放量

17. 灰度开关、降级开关、灰度放量

2024-05-30 15:41| 来源: 网络整理| 查看: 265

文章目录 一、灰度开关、降级开关二、灰度放量

一、灰度开关、降级开关

在日常工作中,我们经常会上线新功能,一般会使用AB实验放量查看效果。但有时候并不需要AB实验,希望直接切换逻辑,比如老接口迁移到新接口,老服务迁移到新服务等,为了在上线初期出现问题时能够及时回滚调用原来的逻辑,灰度开关就派上用场了。

所谓的开关编程其实就是加个if判断,但是可以动态去调整if里面的值,能够随时控制逻辑的走向。开关需要自己编写,自己控制,动态调整值则可以借助于配置中心,改变后实时刷新。 在这里插入图片描述

案例一:新逻辑替换旧逻辑

假设你要对订单详情页面做调整,增加一部分内容或者修改老的逻辑。正常的做法就是直接改掉老逻辑,然后测试,然后上线。

如果测试覆盖了所有的场景,上线后也不会有任何问题。就怕有某些场景遗漏了,导致在测试环境中没有发现的问题,一上线就出问题了。此时你的逻辑已经是最新的了,唯一的解决办法就是回滚应用到之前的版本,回滚是下下策,不到万不得已千万不要做,因为回滚可能带来更严重的问题。

这次发布所有的新功能都丢了 如果执行回滚操作,也就意味着这次发布要上的新功能都没有了(一次上线可能开发了很多功能,开关控制的不过是其中一个小功能,这个小功能有问题则全局回滚,影响面扩大了),如果你的服务拆的够细,可能影响面会稍微小点。

假设服务端回滚了,如果此时客户端已经发布,像H5还好说,也可以回滚,像APP如果用户已经更新到最新版本了,你服务端回滚了,用户一用到新功能就直接报错了。所以请慎重回滚。

所以此时你可能没办法回滚,只能快马加鞭改Bug,然后紧急发布进行修复,玩的就是心跳啊。

开关的作用来啦 在改动旧逻辑的时候,不要直接改,可以在内部采用分版本的方式进行调整。把之前的逻辑定位V1,要改的逻辑定位V2,然后通过开关去切换。

Go伪代码如下:

switch := false // 从配置中心获取开关状态,如果能获取到就用配置中心的,否则用默认值false // XXX 为我们在配置中心设置的key res,err := getConfigFromConfigCenter("XXX") if err == nil { switch = res } if !switch { // 走新改的逻辑 } else { // 走旧逻辑 }

通过增加开关来保证稳定性,默认开关是关闭的,上线后可以走新逻辑,如果新逻辑出现了Bug,立马将开关打开走旧逻辑,不用回滚整个服务,对同期上线的其他功能无任何影响。

案例二:大促前的功能降级 对于很多电商公司来说,大促必不可少。每年都有像周年庆,618,双十一,双十二之类的大促期。

大促的时候,流量也是最高的时候。此时最重要的就是P0级别的核心链路,其他不是很重要的可以降级,以免影响到主链路的功能。

比如订单详情页里面,大部分都是订单的快照信息,可能有个别信息是需要调用其他的接口进行展示的,但这个信息不是必要的,比如商品的评论,此时就可以在调用这个接口的地方加开关,平时关闭,大促之前打开,不进行调用,这样就保证了详情页的稳定性。

如下:在商品大促时,必须保证能够从商品详情服务获取商品图片、价格等信息,从订单服务发起下单等,但是评论服务为非核心服务,即使短暂的不可用,也不会对正常的查看商品和下单产生影响,因此是可以在必要时做降级处理的。 在这里插入图片描述

Go伪代码如下:

switch := false xxxInfo := &XxxInfo{} // 调用商品详情 xxxInfo.Detail = xxxRpc.getxxx(); // 从配置中心获取开关状态,如果能获取到就用配置中心的,否则用默认值false // XXX 为我们在配置中心设置的key res,err := getConfigFromConfigCenter("XXX") if err == nil { switch = res } if !switch { // 降级开关关闭时,可以调用评论服务获取商品评论 xxxInfo.Comment = xxxRpc.getComment() }

具体而言:在 Go 中,实现服务降级通常需要考虑以下几个关键点:

服务降级开关:这是控制是否执行服务降级的关键,当我们检测到系统某些关键指标(如CPU过高,网络延迟等)异常时,我们可以通过开关控制某个或者某些服务的降级。服务降级策略:这是实现服务降级的核心,具体的策略可以是返回固定的值,返回缓存数据,或者直接抛出异常等。

以下是一个简单的 Go 服务降级的例子,降级开关一般需要放到动态配置中心:

package main import ( "fmt" "math/rand" "time" ) // 定义服务接口 type Service interface { DoSomething() (string, error) } // 正常服务的实现 type NormalService struct{} func (s *NormalService) DoSomething() (string, error) { return "I am normal service", nil } // 降级服务的实现 type DegradedService struct{} func (s *DegradedService) DoSomething() (string, error) { // 在这里你可以返回一些默认值或者缓存数据,或者抛出一个已知的异常 return "I am degraded service", nil } var ( // 服务降级开关 DegradeService bool ) func main() { normalService := &NormalService{} degradedService := &DegradedService{} // 模拟系统状态 go func() { for { // 随机改变服务降级开关状态 DegradeService = rand.Intn(2) == 1 fmt.Println("Service degrade status:", DegradeService) time.Sleep(1 * time.Second) } }() for { var service Service if DegradeService { service = degradedService } else { service = normalService } result, err := service.DoSomething() if err != nil { fmt.Printf("Error: %v\n", err) continue } fmt.Println("Result:", result) time.Sleep(1 * time.Second) } }

在这个例子中,我们定义了一个 Service 接口以及两个实现这个接口的服务:一个正常服务 NormalService 以及一个降级服务 DegradedService。我们在主函数 main 中,根据服务降级开关的状态来选择使用哪一个服务。我们还模拟了系统状态,会随机改变服务降级开关的状态。

这只是一个最基础的示例,实际中可能会有更加复杂的场景和需求,如需要精细控制哪些服务需要降级,哪些请求需要降级,以及如何进行降级等,可能需要引入一些开源的服务降级和熔断框架,如 Hystrix、Sentinel 等。

开关注意点 开关虽然很有用,但是凡事有利也有弊。当项目中出现了很多的开关之后,对于代码的可读性比较差,特别是对于新同学来说,这么多开关,可能也不知道干嘛的,所以第一点就是注释一定要写清楚。

然后对于那些保证上线稳定性的开关,在上线后过一点时间,功能稳定了,就应该及时删除开关的逻辑,提高代码可读性。对于降级的开关,还是要保留的。

开关总结 大家在工作中肯定会遇到一些场景需要用开关去做一些事情,特别是在上线新功能,或者改老功能,或者重构,或者迁移的时候。 有了开关,上线不慌,遇到问题可以及时切换开关状态进行回滚。

二、灰度放量

上面介绍了开关,但是开关还是属于一刀切的那种,如果流量特别大的情况下,影响面还是挺大的,所以此时可能需要用到另外一种方式,灰度放量。

当我们发布新功能时,需要尽可能降低因新功能发布所导致的线上风险,通常会采取灰度放量的方式将新功能逐步发布给用户。在具体实施灰度放量时,我们可以根据业务需求选择相应的放量规则,常见如按白名单放量(如仅 QA 可见)、按特定人群属性放量(如仅某个城市的用户可见)亦或是按用户百分比放量。

1、随机百分比放量,针对允许用户可一会是走新逻辑,一会又可以走旧逻辑的情况

该场景下,可以使用一种简单的策略,即为每一个请求分配一个0到100之间的随机数,然后根据这个随机数的值决定是否将请求引导到新的特性。

以下是如何在 Go 中实现这个策略的一个例子:

package main import ( "fmt" "math/rand" "time" ) // 定义处理请求的接口 type Handler interface { HandleRequest() } // 新特性处理请求的实现 type NewFeatureHandler struct { } func (f *NewFeatureHandler) HandleRequest() { fmt.Println("New feature handling request") } // 老特性处理请求的实现 type OldFeatureHandler struct { } func (f *OldFeatureHandler) HandleRequest() { fmt.Println("Old feature handling request") } func main() { // 设置随机数种子 rand.Seed(time.Now().UnixNano()) newHandler := &NewFeatureHandler{} oldHandler := &OldFeatureHandler{} // 此处设置的是灰度放量的百分比,这个10以及下面的100可以放到配置中心动态配置,逐步加大放量人群 percentage := 10 for i := 0; i newHandler.HandleRequest() } else { oldHandler.HandleRequest() } } }

在這個例子中,我们创建了两个处理请求的类型:NewFeatureHandler和 OldFeatureHandler,他们都实现了 Handler 接口。 在主函数 main 中,我们为每一个请求生成一个0到100之间的随机数。如果这个随机数小于我们设置的百分比值(这里是10),那么我们就将请求交给 NewFeatureHandler 处理;否则,我们将请求交给 OldFeatureHandler 处理。

这个策略是最基础的,适用于简单的场景。在实际中,可能需要复杂的灰度策略,例如基于用户属性的灰度放量,基于请求频次的灰度放量等,这涉及到如何在系统中集成这些策略,可能需要使用到负载均衡框架如 Nginx 或者一些服务网格解决方案如 Istio 等。

2、基于用户百分比放量,一个用户从命中放量访问新特性起就应该一直是出于放量中,能够访问到新特性

当我们选择将功能以用户百分比放量时,会先将功能发布给小部分用户(如1%),此时即便出现问题影响也相对可控,如观察没有问题后逐步扩大需要放量的用户百分比,实现从少量到全量平滑过渡的上线。与上面随机百分比放量不同的地方在于,不是随机生成数字,而是通过用户属性,如用户ID、用户年龄或者用户地区等 hash 后求模得到一个数字,因为同一内容hash后每次得到的结果相同,放量比例又是在不断增大的,所以用户命中放量后,加大放量力度也会一直是命中放量状态的。

此时可能我们会在配置中心做如下配置

{ "greyRate" : 10, // 当前放量份数 "greyMax":1000, // 总切分份数 "greyObjectIds" : [] // 灰度白名单,一定命中灰度,一般用于测试账号 }

注:上诉配置即为将总体分为1000份,放量10份,即放量比例为10/1000 = 1%,放量时,如用用户uid控制,则为hash(uid) % greyMax < greyRate即为命中放量的用户。此外,当总流量非常大时,切分份数也可以更细,如切为100000份等,然后放量就是10/100000 =0.01%,相对总量上亿用户的话,0.01%其实也会影响不少人啦。

还有一种非常常用的方式则是直接认定总份数为1000,然后只需要在配置中心指定放量份数即可,当然,还可以包含黑名单,如下

// ControlGrayRule 灰度控制规则, 按字段优先级逐个判断规则, 未命中任何规则默认不灰度 type ControlGrayRule struct { GlobalSwitch bool `json:"global_switch"` // 优先级1: 全局灰度开关, false-不允许灰度, 止损时一键关闭灰度 BlackList []string `json:"black_list"` // 优先级2: 灰度黑名单 WhiteList []string `json:"white_list"` // 优先级3: 灰度白名单 GrayThousandRate uint32 `json:"gray_thousand_rate"` // 优先级3: 灰度比例, 千分比, 0-1000 }

Go代码如下,该代码可以直接作为工具包使用

package gray import ( "context" "google.golang.org/appengine/log" "hash/fnv" ) // ControlGrayRule 灰度控制规则, 按字段优先级逐个判断规则, 未命中任何规则默认不灰度 type ControlGrayRule struct { GlobalSwitch bool `json:"global_switch"` // 优先级1: 全局灰度开关, false-不允许灰度, 止损时一键关闭灰度 BlackList []string `json:"black_list"` // 优先级2: 灰度黑名单 WhiteList []string `json:"white_list"` // 优先级3: 灰度白名单 GrayThousandRate uint32 `json:"gray_thousand_rate"` // 优先级3: 灰度比例, 千分比, 0-1000 } // IsHitGray 基于ID的灰度控制, 命中灰度返回true func IsHitGray(ctx context.Context, grayID string, rule *ControlGrayRule) bool { // 全局灰度开关关闭, 不灰度 if !rule.GlobalSwitch { log.Infof(ctx, "[IsHitGray] gray global switch is close, not gray: grayID=%s", grayID) return false } // 命中黑名单, 不灰度 if containsString(rule.BlackList, grayID) { log.Infof(ctx, "[IsHitGray] hit black list, not gray: grayID=%s", grayID) return false } // 命中白名单, 进行灰度 if containsString(rule.WhiteList, grayID) { log.Infof(ctx, "[IsHitGray] hit white list, will gray: grayID=%s", grayID) return true } // 命中灰度比例, 进行灰度 grayHash, err := hashStringToInt(grayID) if err != nil { // 实际上不会触发,因为 hash.Hash32 的 Write 方法不会返回 err return false } log.Infof(ctx, "[IsHitGray] grayID=%s, grayHash=%d, GrayThousandRate=%d", grayID, grayHash, rule.GrayThousandRate) if grayHash%1000 h := fnv.New32a() _, err := h.Write([]byte(s)) if err != nil { return 0, err } return h.Sum32(), nil } func containsString(list []string, userId string) bool { for _, val := range list { if val == userId { return true } } return false }

放量总结 灰度按百分比放量是一种软件开发中常用的功能发布方法,它可以帮助提高软件可靠性,提高用户体验,在实施时也需要注意几个方面:

确定放量目标:首先需要确定放量的目标,例如增加多少百分比的数据量。这个目标需要根据实际情况进行制定,例如需要考虑数据量的大小、计算资源的限制等因素。

确定放量规则:你需要确定在放量过程中,哪些功能会被启用,哪些功能会被禁用。你可以根据开发进度、测试结果和市场需求等因素来确定放量规则。

监控放量过程:在实施放量操作时,需要监控放量过程,以确保放量结果的稳定性和可靠性。如果出现异常情况,需要及时采取措施进行调整。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有