ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

《Go语言高级编程》灰度发布与 A/B Test 实战:分批部署、业务规则与 murmurhash 实现

《Go语言高级编程》灰度发布与 A/B Test 实战:分批部署、业务规则与 murmurhash 实现 《Go语言高级编程》灰度发布与 A/B Test 实战分批部署、业务规则与 murmurhash 实现【免费下载链接】advanced-go-programming-book:books: 《Go语言高级编程》开源图书涵盖CGO、Go汇编语言、RPC实现、Protobuf插件实现、Web框架实现、分布式系统等高阶主题(完稿)项目地址: https://gitcode.com/gh_mirrors/ad/advanced-go-programming-book灰度发布金丝雀发布Canary Release是现代大型互联网系统上线新功能时降低风险的核心手段它允许新版本按批次或按业务规则只触达一部分用户从而把故障影响面控制在最小范围内。本文以《Go语言高级编程》开源图书第 5 章 ch5-09-gated-launch.md 为骨架结合仓库源码与测试思路系统讲解分批次部署与业务规则两类灰度策略的工程实现并给出基于 Go 的哈希取模、murmurhash 选型 benchmark 与分布均匀性验证方法读完即可在自己的服务中落地一套可复用的灰度判断逻辑。为什么大型系统必须灰度发布中型的互联网公司往往有着以百万计的用户而大型互联网公司的系统则可能要服务千万级甚至亿级的用户需求。大型系统的请求流入往往是源源不断的任何风吹草动都一定会有最终用户感受得到。例如系统在上线途中会拒绝一些上游过来的请求而这时候依赖它的系统没有做任何容错那么这个错误就会一直向上抛出直到触达最终用户——可能是用户 APP 上一个让人摸不着头脑的诡异字符串也可能让正在和几万竞争对手同时抢购秒杀商品的用户因代码问题错失心仪产品。对用户的伤害有多大取决于你的系统对于用户来说有多重要。即使在名义上经过了充分严格的测试代码的 bug 总是难以避免即便代码没有 bug分布式服务之间的协作也可能出现“逻辑”上的非技术问题。因此在大型系统中容错是重要的能够让系统按百分比、分批次地到达最终用户同样重要。“灰度发布”也称“金丝雀发布”其典故来自 17 世纪的英国矿井金丝雀对瓦斯气体非常敏感瓦斯达到一定浓度时金丝雀会先于矿工死亡从而充当瓦斯检测工具。互联网系统的灰度发布一般通过两种方式实现通过分批次部署实现灰度发布——常用于对旧功能进行升级迭代通过业务规则进行灰度发布——常用于新功能上线或对重要老功能进行较大幅度修改时直接全量开放风险太大。5.9.1 分批次部署按 1-2-4-8 等比扩量假如服务部署在 15 个实例物理机或容器上可以将其分为四组按先后顺序分别有 1、2、4、8 台机器保证每次扩量都约为二倍关系如下图所示。图 5-20 分组部署为什么要用 2 倍扩量这样能够保证不管有多少台机器都不会把组划分得太多。例如 1024 台机器也只需要 1-2-4-8-16-32-64-128-256-512 部署十次就可以全部部署完毕。这种策略的核心价值在于控制初始影响面1000 台机器的服务上线后如果出现问题第一批只影响 1/1000 的用户如果 10 组完全平均分一上线立刻就会影响 1/10 的用户——1/10 的业务出问题对公司来说可能已经是不可挽回的事故。上线期间的观察手法最有效的是查看程序的错误日志——较明显的逻辑错误一般错误日志的滚动速度都会有肉眼可见的增加。这些错误也可以通过 metrics 一类系统上报给公司内的监控系统因此上线过程中也可以通过观察监控曲线判断是否有异常发生。如果发现异常情况首先要做的自然就是回滚。5.9.2 通过业务规则进行灰度发布业务规则灰度最常见的需求是按千分比发布。可以用用户 ID、手机号、用户设备信息等生成一个简单的哈希值再求模伪代码如下// pass 3/1000 func passed() bool { key : hashFunctions(userID) % 1000 if key 2 { return true } return false }5.9.2.1 常见的可选规则常见的灰度发布系统会提供下列规则供选择按城市发布按概率发布按百分比发布按白名单发布按业务线发布按 UA 发布APP、Web、PC按分发渠道发布因为与公司业务相关城市、业务线、UA、分发渠道这些条件可能会被直接编码在系统里但功能其实大同小异。按白名单发布最简单功能上线时可能希望只有公司内部员工和测试人员可以访问新功能直接把账号、邮箱写入白名单拒绝其它任何账号的访问。按概率发布指实现一个简单函数按用户指定的概率返回true或false两者概率之和为 100%函数不需要任何输入func isTrue() bool { return true/false according to the rate provided by user }按百分比发布则是实现下面这样的函数以调用方提供的输入参数如手机号为源计算哈希、以哈希结果求模并返回结果func isTrue(phone string) bool { if hash of phone matches { return true } return false }与单纯按概率发布的区别在于以输入参数为源计算哈希可以保证同一个用户的返回结果多次调用是一致的。在下面这种场景下必须使用这种结果可预期的灰度算法。set 与 get 必须命中同一版本可预期灰度的必要性考虑“先 set 然后马上 get”的时序。如果采用可预期的哈希灰度写和读会稳定地落在同一版本的 API 上流程正常图 5-21 先 set 然后马上 get正常如果采用随机策略则可能出现写与读走到不同版本 API 的诡异问题图 5-22 先 set 然后马上 get异常举个具体的例子网站的注册环节可能有两套 API按照用户 ID 进行灰度且两套 API 的存取逻辑不同。如果存储时使用了 V1 版本的 API而获取时使用了 V2 版本的 API就可能出现用户注册成功后反而返回注册失败消息的诡异问题。这正是灰度场景中必须保证“结果可预期”同一 key 恒走同一版本的原因。5.9.3 如何实现一套灰度发布系统提供给用户的接口大致分为两类与业务绑定的简单灰度判断逻辑以及输入稍复杂的哈希灰度。下面分别看如何实现。5.9.3.1 业务相关的简单灰度按城市发布数组实现公司内一般都有公共的城市名字和 ID 的映射关系。如果业务只涉及国内城市数量不会特别多且 ID 可能都在 10000 以内那么只要开辟一个一万大小左右的 bool 数组即可var cityID2Open [12000]bool{} func init() { readConfig() for i:0;ilen(cityID2Open);i { if city i is opened in configs { cityID2Open[i] true } } } func isPassed(cityID int) bool { return cityID2Open[cityID] }按城市发布map 实现如果公司给 cityID 赋的值比较大可以考虑用 map 存储映射关系。map 的查询比数组稍慢但扩展更灵活var cityID2Open map[int]struct{}{} func init() { readConfig() for _, city : range openCities { cityID2Open[city] struct{}{} } } func isPassed(cityID int) bool { if _, ok : cityID2Open[cityID]; ok { return true } return false }按白名单、按业务线、按 UA、按分发渠道发布本质上与按城市发布相同这里不再赘述。按概率发布稍微特殊一些但不考虑输入时实现也很简单。注意初始化随机种子func init() { rand.Seed(time.Now().UnixNano()) } // rate 为 0~100 func isPassed(rate int) bool { if rate 100 { return true } if rate 0 rand.Int(100) rate { return true } return false }这里有一个值得注意的实现细节当rate 100时直接返回true当rate 0时rand.Int(100) rate恒成立、返回false边界行为清晰。但随机函数天然不具备可预期性因此它只适用于对“同一用户多次调用结果一致”没有要求的场景例如某些运营活动类的全局开关。5.9.3.2 哈希算法选型为什么灰度场景更偏爱 murmurhash求哈希可用的算法非常多比如 md5、crc32、sha1 等等。但灰度场景的目的只是给数据做映射并不希望因为计算哈希消耗过多的 CPU所以业界使用较多的算法是 murmurhash。下面是标准库 md5、sha1 与开源 murmur3 实现的简单 benchmark。先定义四个哈希函数package main import ( crypto/md5 crypto/sha1 github.com/spaolacci/murmur3 ) var str hello world func md5Hash() [16]byte { return md5.Sum([]byte(str)) } func sha1Hash() [20]byte { return sha1.Sum([]byte(str)) } func murmur32() uint32 { return murmur3.Sum32([]byte(str)) } func murmur64() uint64 { return murmur3.Sum64([]byte(str)) }为这些算法写基准测试package main import testing func BenchmarkMD5(b *testing.B) { for i : 0; i b.N; i { md5Hash() } } func BenchmarkSHA1(b *testing.B) { for i : 0; i b.N; i { sha1Hash() } } func BenchmarkMurmurHash32(b *testing.B) { for i : 0; i b.N; i { murmur32() } } func BenchmarkMurmurHash64(b *testing.B) { for i : 0; i b.N; i { murmur64() } }运行go test -bench.的效果~/t/g/hash_bench git:master ❯❯❯ go test -bench. goos: darwin goarch: amd64 BenchmarkMD5-4 10000000 180 ns/op BenchmarkSHA1-4 10000000 211 ns/op BenchmarkMurmurHash32-4 50000000 25.7 ns/op BenchmarkMurmurHash64-4 20000000 66.2 ns/op PASS ok _/Users/caochunhui/test/go/hash_bench 7.050s可见murmurhash 相比其它算法有三倍以上的性能提升MurmurHash32 约 25.7 ns/op仅为 MD5 的约 1/7、SHA1 的约 1/8。显然做灰度分流本质是一种按 key 的负载均衡时用 murmurhash 要比 md5 和 sha1 都好。这些年社区里还涌现了另外一些更高效的哈希算法感兴趣的读者可以自行调研。需要说明的是上述 benchmark 结果来自原文档作者在特定机器darwin/amd64上的实测具体数值会随硬件与 Go 版本变化读者可基于文中代码在自己的环境中复测。5.9.3.3 分布是否均匀灰度效果的第二重校验对于哈希算法除了性能还要考虑哈希后的值是否分布均匀——如果分布不均匀自然也起不到均匀灰度的效果。以 murmur3 为例以 15810000000 开头造一千万个和手机号类似的数字将计算后的哈希值分十个桶观察计数是否均匀package main import ( fmt github.com/spaolacci/murmur3 ) var bucketSize 10 func main() { var bucketMap map[uint64]int{} for i : 15000000000; i 1500000000010000000; i { hashInt : murmur64(fmt.Sprint(i)) % uint64(bucketSize) bucketMap[hashInt] } fmt.Println(bucketMap) } func murmur64(p string) uint64 { return murmur3.Sum64([]byte(p)) }执行结果map[7:999475 5:1000359 1:999945 6:1000200 3:1000193 9:1000765 2:1000044 \ 4:1000343 8:1000823 0:997853]十个桶的计数都落在 9978531000823 之间偏差都在 1/100 以内可以接受。读者在调研其它算法、判断其是否适合做灰度发布时也应该从本节提到的性能和均衡度两方面对其进行考察。从文档到工程灰度逻辑的落地要点结合原文档的内容与 Go 工程实践落地一套灰度系统时值得关注以下几点灰度判定必须可预期按百分比/城市等规则灰度时一律以稳定标识用户 ID、手机号、设备 ID作为哈希输入保证同一用户多次请求稳定命中同一版本避免“先 set V1、后 get V2”类数据不一致问题。配置驱动无论是 bool 数组、map 还是白名单都应在init()或启动阶段从配置加载原文档中的readConfig()使灰度开关可以热更新而不需要重新发版。哈希选型按“性能 均衡度”双指标灰度分流请求路径上的热点操作优先选用 murmur3 这类非加密但高速且分布均匀的哈希必要时用本文的 benchmark 与分桶实验验证后再上线。分批部署与监控回滚配套1-2-4-8 等比放量 错误日志/metrics 曲线观察 异常即回滚是分批部署灰度安全性的三根支柱。小结本文完整梳理了《Go语言高级编程》第 5.9 节“灰度发布和 A/B test”的核心内容先解释了大型系统中灰度发布的必要性再分别给出分批次部署1-2-4-8 等比扩量、监控与回滚与业务规则灰度城市、概率、百分比、白名单、业务线、UA、分发渠道两类策略最后深入到实现层面——数组/map 驱动的城市灰度、基于rand的概率灰度以及 murmurhash 的 benchmark 与分布均匀性验证。掌握这些方法你就能在自己的 Go 服务中实现一套性能足够、结果可预期、可配置驱动的灰度发布能力。延伸阅读本文属于《Go语言高级编程》第 5 章“Go 和 Web”同章还讨论了请求路由ch5-02-router.md、中间件ch5-03-middleware.md、服务流量限制ch5-06-ratelimit.md与大型 Web 项目分层ch5-07-layout-of-web-project.md配合阅读可以构建更完整的 Web 服务稳定性体系。【免费下载链接】advanced-go-programming-book:books: 《Go语言高级编程》开源图书涵盖CGO、Go汇编语言、RPC实现、Protobuf插件实现、Web框架实现、分布式系统等高阶主题(完稿)项目地址: https://gitcode.com/gh_mirrors/ad/advanced-go-programming-book创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表