灰度发布
中型的互联⽹公司往往有着以百万计的⽤户,⽽⼤型互联⽹公司的系统则可能要服务千万级甚⾄亿级 的⽤户需求。⼤型系统的请求流⼊往往是源源不断的,任何⻛吹草动,都⼀定会有最终⽤户感受得 到。例如你的系统在上线途中会拒绝⼀些上游过来的请求,⽽这时候依赖你的系统没有做任何容错, 那么这个错误就会⼀直向上抛出,直到触达最终⽤户。形成⼀次对⽤户切切实实的伤害。这种伤害可 能是在⽤户的APP上弹出⼀个让⽤户摸不着头脑的诡异字符串,⽤户只要刷新⼀下⻚⾯就可以忘记这 件事。但也可能会让正在⼼急如焚地和⼏万竞争对⼿同时抢夺秒杀商品的⽤户,因为代码上的⼩问 题,丧失掉了先发优势,与⾃⼰蹲了⼏个⽉的⼼仪产品失之交臂。对⽤户的伤害有多⼤,取决于你的 系统对于你的⽤户来说有多重要。
不管怎么说,在⼤型系统中容错是重要的,能够让系统按百分⽐,分批次到达最终⽤户,也是很重要 的。虽然当今的互联⽹公司系统,名义上会说⾃⼰上线前都经过了充分慎重严格的测试,但就算它们 真得做到了,代码的bug总是在所难免的。即使代码没有bug,分布式服务之间的协作也是可能出现“逻 辑”上的⾮技术问题的。
这时候,灰度发布就显得⾮常重要了,灰度发布也称为⾦丝雀发布,传说17世纪的英国矿井⼯⼈发现 ⾦丝雀对瓦斯⽓体⾮常敏感,瓦斯达到⼀定浓度时,⾦丝雀即会死亡,但⾦丝雀的致死量瓦斯对⼈并 不致死,因此⾦丝雀被⽤来当成他们的瓦斯检测⼯具。互联⽹系统的灰度发布⼀般通过两种⽅式实 现:
1 通过分批次部署实现灰度发布 2 通过业务规则进行灰度发布
对旧的系统进行升级迭代时,第一种用的比较多。新功能上线时,第二种用的比较多。当然,对比较重要的 老功能进行较大幅度的修改时,一般也会选择按业务规则进行发布,因为直接全量开放给所有的用户风险太大。
通过分批次部署实现灰度发布
例如部署在15个实例上,可以把实例分成四组,按照顺序,分别1-2-4-8,保证每次扩展时,大概都是二倍的关系。
为什么是2倍呢?这样保证不管有多少台机器,都不会把组划分的太多。例如1024台机器,实 际上也就只需要1-2-4-8-16-32-64-128-256-512部署⼗次就可以全部部署完毕。
这样我们上线最开始影响到的⽤户在整体⽤户中占的⽐例也不⼤,⽐如1000台机器的服务,我们上线 后如果出现问题,也只影响1/1000的⽤户。如果10组完全平均分,那⼀上线⽴刻就会影响1/10的⽤ 户,1/10的业务出问题,那可能对于公司来说就已经是⼀场不可挽回的事故了。
上线的时候检测日志的变化,如果有明显的逻辑错误,一般错误的日志都会有明显的肉眼可见的增加。 我们通过监控这些曲线就能判断是否有异常发生。
通过业务规则进行灰度发布
常见的灰度发布策略很多,较为简单的需求,例如我们的策略是要按照千分比来划分,那么我们可以使用用户的ID ⼿机号、⽤户设备信息,等等,来⽣成⼀个简单的哈希值,然后再求模,⽤伪代码表示⼀下:
// pass 3/1000 func passed() bool { key := hashFunctions(userID) % 1000 if key <= 2 { return true } return false }
可选规则
常见的灰度发布系统会有下列的规则提供选择:
- 按城市发布
- 按概率发布
- 按百分⽐发布
- 按⽩名单发布
- 按业务线发布
- 按UA发布(APP、Web、PC)
- 按分发渠道发布
因为和公司的业务相关,所以城市、业务线、UA、分发渠道这些都可能会被直接编码在系统⾥,不过 功能其实⼤同⼩异。
按白名单发布比较简单,功能上线时,我们希望只有公司内部的员工和测试人员可以访问到新的功能, 会直接把账号,邮箱写入到白名单,拒绝其他任何账号的访问。
按概率发布则是指实现一个简单的函数:
func isTrue() bool { return true/false according to the rate provided by user }
其可以按照⽤户指定的概率返回 true 或者 false ,当然, true 的概率加 false 的概率应该是 100%。这个函数不需要任何输⼊。
func isTrue(phone string) bool { if hash of phone matches { return true } return false }
这种情况可以按照指定的百分⽐,返回对应的 true 和 false ,和上⾯的单纯按照概率的区别是这⾥ 我们需要调⽤⽅提供给我们⼀个输⼊参数,我们以该输⼊参数作为源来计算哈希,并以哈希后的结果 来求模,并返回结果。这样可以保证同⼀个⽤户的返回结果多次调⽤是⼀致的,在下⾯这种场景下, 必须使⽤这种结果可预期的灰度算法。
如果使用随机的会出现下面的情况
举个具体的例⼦,⽹站的注册环节,可能有两套API,按照⽤户ID进⾏灰度,分别是不同的存取逻辑。 如果存储时使⽤了V1版本的API⽽获取时使⽤V2版本的API,那么就可能出现⽤户注册成功后反⽽返回 注册失败消息的诡异问题。
实现一套灰度发布系统
业务相关的简单灰度
公司内⼀般都会有公共的城市名字和id的映射关系,如果业务只涉及中国国内,那么城市数量不会特别 多,且id可能都在10000范围以内。那么我们只要开辟⼀个⼀万⼤⼩左右的bool数组,就可以满⾜需求 了:
var cityID2Open = [12000]bool{} func init() { readConfig() for i:=0;i<len(cityID2Open);i++ { if city i is opened in configs { cityID2Open[i] = true } } } func isPassed(cityID int) bool { return cityID2Open[cityID] }
如果公司给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 } 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)) }