ab测试/灰度下发探索

ab测试

Posted by River on December 18, 2019

前言

最近转战到移动中台建设,接到设计ab测试/灰度的方案设计紧急任务,设计过程中点滴记录在此。

整体架构图

A/B测试系统架构图

设计边界

1.用户画像数据库的查询QPS峰值不超过2000
2.关联测试深度不超过3层

请求参数

public class Req {

    /**
     * 灰度包安装成功埋点用的,option
     */
    private String testId;

    /**
     * 分区端的识别码必须有
     */
    private String appCode;

    /**
     * app版本号,必须有
     */
    private String version;

    /**
     * 用户id,必须有
     */
    private String userId;

    /**
     * 从端侧的sdk获取,option
     */
    private List<String> finishTestIds;

    /**
     * iOS设备需要传,true表示安装了test flight
     */
    private Boolean installTestFlight;
}

服务处理

以下所有的代码均是伪代码

主体服务逻辑
 // 读取正在运行的测试
 List<TestConfig> list = readRunningTest(abReq);
 
 // 分桶的集合
 List<Bucket> resp = new ArrayList<>();
 
 //获取每一个测试的分桶结果
 for (TestConfigEntity testConfig : list) {
      runABTestConfig(abReq,resp, testConfig);
 }
 
 return resp;
分桶算法主体实现
 private BucketConfigEntity runABTestConfig(TestReq abReq,  List<Bucket> resp, TestConfigEntity testConfig) {
 
        // 检查版本号       
        boolean match = compare(testConfig.getAppVersion(), appVersion);    
                
        if(match){
            // 检查最大用户数限制,可以用redis记录
            boolean userCountmatch = checkSucceedUserCount(testConfig);
            
            if(userCountmatch){
                // 检查是否符合圈选目标用户
                boolean exist = queryUserId(abReq.getUserId(), testConfig.getGroupId());
                
                if(exist){
                    // highway哈希算法取模
                    long hashCode = HighwayHash.hash64(testConfig.getTestId() + abReq.getUserId());
                    long mod = Math.abs(hashCode % 100);
                    // 分层分流
                    Bucket bucket = calculateBucketId(bucketConfigEntities, mod,
                            abReq, resp);
                    // redis埋点锁库存
                    increaseAbUserCount(testConfig.getTestId(), AbConfig.Ab_REACH_ID);
                }
            }
        }
 }
ci前端添加新建一个A/B测试主体结构
  public class AddTestConfigReq {

    /**
     * 测试名称
     */
    private String testName;

    /**
     * 区分端
     */
    private String appCode;

    /**
     * 端的名称
     */
    private String appName;

    /**
     * 0表示iOS,1代表Android
     */
    private Integer platform;

    /**
     * 最低支持到的app版本
     */
    private String appVersion;

    /**
     * 最大参与用户数
     */
    private Integer userMaxNumber;

    /**
     * 向上依赖的关联测试
     */
    private String parentTestId;

    /**
     * 关联测试下的某个桶
     */
    private String relatedBucketId;

    /**
     * true表示灰度,false表示ab测试
     */
    private Boolean grey;

    /**
     * 扩展配置的jsonObject对象,灰度时候使用
     */
    private String jsonTag;

    /**
     * 用户圈选条件的id
     */
    private Long groupId;

    /**
     * 创建人的名字
     */
    private String creator;

    /**
     * 发布比例默认是100
     */
    private Integer publishPercent;

    /**
     * 分桶配置,灰度的时候没有,option
     */
    private List<BucketConfigReq> bucketConfigList;

    /**
     * 业务参数,option
     */
    private List<ParamItem>  paramsTypeReqList;
}

public class BucketConfigReq {

    /**
     * 分桶的名称
     */
    private String bucketName;

    /**
     * 分桶百分比,90就表示90%
     */
    private int  percent;
}

一些注意点

  1. ci上新建A/B测试一定要有工单审批,灰度流程控制可以放在ci的server去实现;
  2. 灰度的时候iOS的test fight的一万人数机制是针对单个app的,不是一个group一万;
  3. 消除一万限制的时候,参数记得传全面,需要把app下所有的group数据都清空;
  4. 关联测试返回测试id的时候最好用子测试id,不要根测试id