- 保证离散:确保灰度的范围是离散的,例如如果灰度对象是用户,那么每个年份的用户都可以灰度到最好;针对不同的flag,每次灰度的范围也应该离散,例如flag A和flag B的灰度范围应该不一样;
- 保证一致:在灰度信息不变的情况下,针对同样的业务id每次的灰度结果都能保持一致;
灰度的场景有以下两种:
- 场景一:从整体上保障既定比例的流量灰度即可,针对同个业务id的请求(例如同个userId),不保证一定都是灰度或者都是不灰度;
- 后端存储优化,需要控制1%的流量先使用新的接口;
- ……
- 场景二:需要保障同一个业务id每次的灰度结果是一致的,即针对同样的业务id,要么全灰度、要么全不灰度
- A/B测试;
- 用户端新增功能,需要小部分用户先体验,不可能有时候出现新功能,有时候不出现。应该确保灰度的用户都出现新功能;
- ……
基于以上场景,算法如下:
-
随机算法(针对场景一)
- 生成100以内的随机数;
ThreadLocalRandom.current().nextInt(100)
- 根据生成的随机数进行判断,小于灰度比例则开启灰度,否则不开启。
-
一致性算法(针对场景二)
- 根据flagName计算种子(确保每次的灰度,可以灰度到不同的群体);
- 根据bizId+flagName计算hash(小于100),保证同个业务id每次的执行结果都一致;
- 根据生成的随机数进行判断,小于灰度比例则开启灰度,否则不开启。
/** * 根据业务id和flagName计算hash值 * * @param bizId 业务id * @return 返回计算后小于等于100的哈希值 */ protected long seededHash(String bizId) { // FNV算法,根据flagName生成种子,确保不同的flag可以灰度到不同范围的群体 long seed = FNV.fnv1a_32(getFlagName()); long h = seed % 100L; byte[] bytes = bizId.getBytes(); for (int i = 0; i < bytes.length; i++) { int t = 0xFF & bytes[i]; // 为什么乘以31?因此31在JVM内部做过优化,通过位异5位-1完成,比较高效 h = (h * 31L + t) % 100L; } return h; }