推荐系统(工程方向)-abtest平台

本文介绍了AB测试中单层实验存在的流量分配问题,并提出了分层实验的概念,通过域、层和实验的三层结构解决流量饥饿和偏置问题。文章详细阐述了分层实验的实现方案,包括流量实体段、域、层和实验的定义及交互逻辑,并展示了相关代码结构。此外,还介绍了特色实现,如场景级别的流量重新打散和简化发布层流程。最后,提出了校验条件确保实验配置的正确性。
摘要由CSDN通过智能技术生成

一、背景

早期我们进行ab实验,用的是单层实验,即按用户userId/deviceId进行hash取模,打在[0-99]区间,然后配置A:[0-49],B:[50-99]。

v2-c7a9fa75ec046ea11c3189d22842f16d_b.jpg

单层实验有如下问题:

  1. 流量饥饿,如果流量只有2W,需要做20组实验,每组1000个流量太少,导致置信区间太宽,实验结果不可信。
  2. 流量偏置,假设上游的实验把所有的年轻人都获取了,下游的实验,没有年轻人的样本,导致有偏差。

二、分层实验概念

v2-bb828f7997b6afca061aaf72a7bfbfa2_b.jpg

v2-1b848a2341ff356ca78a1c3e748fbfee_b.jpg
abtest架构图(此图网上引用)

参考google分层实验论文

ai.google/research/pubs

分层实验三个概念:

  1. 域:流量的一个大的划分,最上层的流量进来时首先是划分域。(流量实体段)
  2. 层:系统参数的一个子集,一层实验是对一个参数子集的测试。层可以包含域、实验(流量容器)
  3. 实验:具体实验,包括分桶,实验参数,是否对照组等。(流量实体段)

三、实现方案

v2-102b091e68e0c312badbffc44419ef0f_b.jpg
简易架构图


admin UI重要界面

v2-4df24d3934f96cf63a5aba60c91cbf07_b.jpg
项目-场景页面


v2-b721faf1a4cee6f6d84db7a1817b3f9a_b.jpg
域、层、实验编辑页面(抽屉风格,单元素编辑)


v2-e83c0a5dfc0b02175fe650cf5a9f248d_b.jpg
域、层、实验编辑页面(树形编辑风格,多元素编辑)


v2-73758b8c5f0c1f43e2807127113718af_b.jpg
添加白名单页面


v2-c60881f4cbe0ad482efda8ba725b0219_b.jpg
实验效果列表


v2-b3924db6ca0de8a98a917296bee4a79f_b.jpg
B组实验效果


java exp sdk重要代码

Segment类

/**
 * @author itbasketplayer
 * 流量实体段抽象类 包括domain 或者 experiment
 */
@Setter
@Getter
@Slf4j
public abstract class Segment {

    public final static int BUCKET_NUM = 100;//写死100
    private final static String SPLITER = "@";

    private long id;
    private String name;
    private String describe;
    private long startTime;
    private long endTime;
    private int start;
    private int end;
    private long createTime;
    private long updateTime;
    private long rootDomainId;//顶层域id
    private String conditions;//流量过滤条件,json格式,比如:{"broswer": ["IE", "firefox"], "build_version": ["1.8", "1.9"]}
    private Map<String, Set<String>> conditionMap;//条件参数对象,根据conditions生成

    public void setConditions(String conditions) {
        conditionMap = CommonUtil.paramToMapSet(getId(), conditions);
        this.conditions = conditions;
    }

    public boolean hitExp(String rootDomainName, ExpReq expReq, String layerOrParentDomainName) {
        //1、不使用id+name作为发散因子,是因为admin端实现,在不同环境推送过程中,id会发生改变
        //2、加上expAppName和rootDomainName保证所有项目-场景下流量相互正交
        //3、rootDomainName可以设置rehash
        String rootDomainShuffle = ExpConfigManager.getRehashMap().get(rootDomainName);
        if (rootDomainShuffle != null) {
            rootDomainName = rootDomainName + SPLITER + rootDomainShuffle;
        }
        String shuffle = ExpConfigManager.expAppName + SPLITER + rootDomainName + SPLITER + layerOrParentDomainName;
        String hashKey = ExpConfigManager.DiversionId.UserId.equals(ExpConfigManager.getDiversionId()) ? String.valueOf(expReq.getUserId()) : expReq.getDeviceId();
        int bucketId = CommonUtil.getBucketId(hashKey, shuffle, BUCKET_NUM);
        //filter包含
        if (conditions != null && conditionMap.size() > 0 && !matchCondition(expReq)) {
            return false;
        }
        if (bucketId < start || bucketId > end) {
            return false;
        }
        if (log.isDebugEnabled())
            log.debug("requestId={}`deviceId={}`layerOrParentDomainName={}`expOrDomainId={}`expOrDomainName={}`bucketId={}`start={}`end={}", expReq.getRequestId(), expReq.getDeviceId(), layerOrParentDomainName, id, name, bucketId, start, end);
        return true;
    }

    /**
     * 能执行此方法代表配置了条件
     *
     * @return
     */
    public boolean matchCondition(ExpReq expReq) {
        if (conditionMap == null || conditionMap.size() == 0) {
            log.warn("requestId={}`conditionMap == null", expReq.getRequestId());
            return false;
        }
        Map<String, String> conditionParamMap = expReq.getConditionMap();
        if (conditionParamMap == null || conditionParamMap.size() == 0) {
            return false;
        }

        //从配置维度遍历
        Iterator<Map.Entry<String, String>> it = conditionParamMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            String key = entry.getKey();
            String value = entry.getValue();
            Set<String> set = conditionMap.get(key);
            if (set != null) {
                if (set.contains(value)) {
                    return true;
                }
            }
        }
        return false;
    }
}

Domain类

/**
 * @Description 域中有层,但是层中没有域(google简化版,层中只需配置实验)
 * @Date 2019/5/9 16:36
 * @Created by itbasketplayer
 */
@Setter
@Getter
public class Domain extends Segment {

    private List<Layer> commonLayers;
    private List<Domain> childDomains;
    private long applicationId;
    private long parentId;
    private String defaultParameters;//顶层域,默认参数(json),偏置流量可以使用。默认参数<发布层参数<实验参数

    public boolean isRootDomain() {
        return parentId == -1 ? true : false;
    }

}

Layer类

@Setter
@Getter
public class Layer {

    private List<Experiment> experimentList = new ArrayList<>();
    private long id;
    private String name;
    private long rootDomainId;
    private String describe;
    private long domainId;
    private String launchParameters;
    private boolean launch;
    private long createTime;
    private long updateTime;

}

Experiment类

/**
 * @Description 实验组参数>发布层参数>默认参数
 * @Date 2019/5/17 14:31
 * @Created by itbasketplayer
 */
@Setter
@Getter
@Slf4j
public class Experiment extends Segment {

    private long layerId;//所属层id
    private boolean control;//对照组id
    private String parameters;//参数列表,json格式,任意参数
    private Map<String, String> parameterMap;//实验组参数,根据parameters生成

    public void setParameters(String parameters) {
        parameterMap = CommonUtil.paramToMap(getId(), parameters);
        this.parameters = parameters;
    }

}

ExperimentContext类

@Setter
@Getter
public class ExperimentContext {

    private Map<String, String> defaultParameterMap;
    private List<Map<String, String>> launchParameters = new ArrayList<>();//发布层参数,layer表,launch_parameters字段(json)
    private Map<String, List<Experiment>> userWhitelistMap;//初始化时,查询出userId/deviceId -> 白名单实验组
    private Domain rootDomain;//顶层domain
    private List<Layer> launchLayerList;//所有发布层

    public ExperimentContext(Map<String, String> defaultParameterMap, List<Layer> launchLayerList, Map<String, List<Experiment>> userWhitelistMap, Domain rootDomain) {
        this.defaultParameterMap = defaultParameterMap;
        this.userWhitelistMap = userWhitelistMap;
        this.rootDomain = rootDomain;
        this.launchLayerList = launchLayerList;
        for (Layer layer : launchLayerList) {
            if (StringUtils.isNotBlank(layer.getLaunchParameters())) {
                launchParameters.add(CommonUtil.paramToMap(layer.getId(), layer.getLaunchParameters()));
            }
        }
    }

}

ExpReq类(请求)

@Setter
@Getter
public class ExpReq {

    private String scenesKey;//场景key,代表一个正在进行的场景实验
    private long userId;
    private String deviceId;
    private Map<String, String> conditionMap;//用于condition条件过滤,比如:city:gz
    private String requestId;//唯一标识此次请求

}

ExpRes类(结果)

@Setter
@Getter
public class ExpRes {

    private String hitExps;//命中的实验名称,|分隔,比如:exp1|exp2
    private Map<String, String> expParamMap = new HashMap<>();//命中实验获取到的参数集合,比如:white:red
    private Map<String, Segment> expSegmentMap = new HashMap<>();//命中的domain or exp集合

}

ExpConfigManager类(管理类、入口类)

/**
 * @Description sdk管理类,定时http方式拉取某个app的所有实验配置,并缓存在内存。
 * 同时业务方可调用getExpParam获取用户的实验组和它们的配置
 * @Date 2019/5/14 16:47
 * @Created by yuhaibin
 */
public class ExpConfigManager {

    private static Logger LOG = LoggerFactory.getLogger(ExpConfigManager.class);

    /**
     * 实验配置http查询服务
     */
    private static HttpConfigQueryService httpConfigQueryService;

    //配置实验应用名字
    public static String expAppName = System.getProperty("app.name");
    //配置预发、线上环境,获取脚本里 -Denv 的值
    private static String env = System.getProperty("env", ExpConsts.env.pre).toLowerCase();
    //experiment config api的域名
    private static String expConfigApiDomain = System.getProperty("rec_exp_api_domain");

    private static int status = ExpConsts.env.online.equals(ExpConfigManager.env) ? ExpConsts.status.online : ExpConsts.status.pre;

    /**
     * MurmurHash3经典hash算法,而ABtestBucket自己实现的hash,标准差更小(分桶更均匀)
     */
    public enum HashType {
        MurmurHash3, ABtestBucket
    }

    /**
     * 分流方式,只支持userId和deviceId其一,默认deviceId
     */
    public enum DiversionId {
        UserId, DeviceId
    }

    private static HashType hashType = HashType.MurmurHash3;//默认hash方式
    private static DiversionId diversionId = DiversionId.DeviceId;//默认deviceId

    private static final int CHECK_INTERVAL = 2 * 60;//全量更新时间s
    private static final int UPDATE_CHECK_INTERVAL = 5;//近实时更新时间s

    private final static String EXP_SPLITER = "|";

    private final static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

    private static Map<String, ExperimentContext> rootDomainNameExpCtxMap = new ConcurrentHashMap<>();

    private static Map<String, Domain> rootDomainUpdateMap = new ConcurrentHashMap<>();

    private static Map<String, List<UserExperimentWhitelist>> expWhitelistMap = new ConcurrentHashMap<>();

    private static long userExperimentWhiteListLastUpdateTime = 0L;

    //key=root_domain_name=顶层域名称=场景
    private static Map<String, String> rehashMap = new ConcurrentHashMap<>();

    static {
        String expConfigApiUrl;
        if (StringUtils.isNotBlank(expConfigApiDomain)) {
            expConfigApiUrl = ExpConsts.HTTP_PROTOCAL + expConfigApiDomain + ExpConsts.EXP_ROOT_PATH;
        } else {
            if ("test".equals(env) || "office".equals(env)) {
                expConfigApiUrl = ExpConsts.HTTP_PROTOCAL + ExpConsts.HTTP_CONFIG_TEST_DOMAIN + ExpConsts.EXP_ROOT_PATH;
            } else if ("pre".equals(env)) {
                expConfigApiUrl = ExpConsts.HTTP_PROTOCAL + ExpConsts.HTTP_CONFIG_PRE_DOMAIN + ExpConsts.EXP_ROOT_PATH;
            } else {
                expConfigApiUrl = ExpConsts.HTTP_PROTOCAL + ExpConsts.HTTP_CONFIG_PRO_DOMAIN + ExpConsts.EXP_ROOT_PATH;
            }
        }
        httpConfigQueryService = new HttpConfigQueryService(expConfigApiUrl);
        //特殊处理_pre或者_test结尾的app.name
        if (ExpConsts.env.pre.equals(env)) {
            if (expAppName.endsWith("_pre")) {
                expAppName = expAppName.substring(0, expAppName.lastIndexOf("_pre"));
            } else if (expAppName.endsWith("_test")) {
                expAppName = expAppName.substring(0, expAppName.lastIndexOf("_test"));
            }
        }
        LOG.info("expConfigApiUrl={} `expAppName={}", expConfigApiUrl, expAppName);

        reload();
        executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                reload();
            }
        }, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.SECONDS);
        executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                update();
            }
        }, UPDATE_CHECK_INTERVAL, UPDATE_CHECK_INTERVAL, TimeUnit.SECONDS);
    }

    public static void register() {
        LOG.info("register...");
    }

    public static boolean hasExp(ExpReq expReq) {
        return rootDomainNameExpCtxMap.containsKey(expReq.getScenesKey());
    }

    /**
     * 获取所有实验参数
     *
     * @param expReq
     * @return
     */
    public static Map<String, String> getExpParam(ExpReq expReq) {
        Map<String, String> resultMap = new HashMap<>();
        ExpRes expRes = getAllExps(expReq);
        if (expRes != null && expRes.getExpParamMap() != null) {
            return expRes.getExpParamMap();
        }
        return resultMap;
    }

    /**
     * 获取命中的实验
     *
     * @param expReq
     * @return
     */
    public static String getHitExps(ExpReq expReq) {
        String hitExps = null;
        ExpRes expRes = getAllExps(expReq);
        if (expRes != null) {
            return expRes.getHitExps();
        }
        return hitExps;
    }

    /**
     * 重要方法
     * 每次请求调用
     * 获取所有实验
     *
     * @param expReq
     * @return
     */
    private static ExpRes getAllExps(ExpReq expReq) {
        ExpRes expRes = new ExpRes();
        try {
            List<Segment> hitSements = new ArrayList<>();
            ExperimentContext expCtx = null;
            if (!rootDomainNameExpCtxMap.containsKey(expReq.getScenesKey())) {
                LOG.warn("requestId={}`sceneKey={} has no any exps", expReq.getRequestId(), expReq.getScenesKey());
                return null;
            } else {
                expCtx = rootDomainNameExpCtxMap.get(expReq.getScenesKey());
                boolean hitUserWhitelist = hitUserWhitelist(expCtx, expReq, hitSements);
                if (!hitUserWhitelist) {
                    //从rootDomain开始递归
                    hitByDomainRecursive(expCtx.getRootDomain().getName(), expCtx.getRootDomain(), expReq, hitSements);
                }
            }
            //上面处理完,得出一批命中的实验,做下面的处理
            StringBuilder sb = new StringBuilder();
            Map<String, String> expAllMap = new HashMap<>();
            if (hitSements.size() > 0) {
                for (Segment segment : hitSements) {
                    //保存所有命中experiment,不保存domain
                    if (segment instanceof Experiment) {
                        expRes.getExpSegmentMap().put(segment.getName(), segment);
                        sb.append(segment.getName()).append(EXP_SPLITER);
                        //命中实验,保存参数
                        Map<String, String> expParamMap = ((Experiment) segment).getParameterMap();
                        //LOG.info("requestId={}`deviceId={}`expName={}`expParamMap={}", expReq.getRequestId(), expReq.getDeviceId(), segment.getName(), expParamMap);
                        if (expParamMap != null && expParamMap.size() > 0) {
                            Iterator<Map.Entry<String, String>> it = expParamMap.entrySet().iterator();
                            while (it.hasNext()) {
                                Map.Entry<String, String> next = it.next();
                                if (expAllMap.containsKey(next.getKey())) {
                                    //validate已验证,这里改成warn
                                    LOG.warn("requestId={}`experiment param key={} in multi common layers!", expReq.getRequestId(), next.getKey());
                                }
                                expAllMap.put(next.getKey(), next.getValue());
                            }
                        }
                    }
                }
                if (sb.toString() != null && sb.toString().endsWith(EXP_SPLITER)) {
                    expRes.setHitExps(sb.toString().substring(0, sb.toString().length() - 1));
                }

            }
            //default<launch<exp
            expRes.getExpParamMap().putAll(expCtx.getDefaultParameterMap());
            Map<String, String> launchParameterMap = new HashMap<>();
            for (Map<String, String> map : expCtx.getLaunchParameters()) {
                for (Map.Entry<String, String> entry : map.entrySet()) {
                    if (launchParameterMap.containsKey(entry.getKey())) {
                        //validate已验证,这里改成warn
                        LOG.warn("requestId={}`launch param key={} in multi launch layers!", expReq.getRequestId(), entry.getKey());
                    }
                    launchParameterMap.put(entry.getKey(), entry.getValue());
                }
            }
            if (launchParameterMap.size() > 0) {
                expRes.getExpParamMap().putAll(launchParameterMap);
            }
            expRes.getExpParamMap().putAll(expAllMap);
        } catch (Exception e) {
            LOG.error("", e);
        }
        return expRes;
    }

    /**
     * 全部加载
     */
    private static void reload() {
        long initStartTime = System.currentTimeMillis();
        try {
            Map<String, ExperimentContext> newRootDomainNameExpCtxMap = new ConcurrentHashMap<>();
            List<Domain> rootDomainList = httpConfigQueryService.getRootDomains(expAppName, status);
            List<String> rootDomainNameList = new ArrayList<>();
            for (Domain domain : rootDomainList) {
                rootDomainNameList.add(domain.getName());
            }
            //必须在rootDomain查询完成之后,设置白名单到全局
            List<UserExperimentWhitelist> userExperimentWhitelistList = httpConfigQueryService.getAllUserExperimentWhitelistList(expAppName, rootDomainNameList);
            if (userExperimentWhitelistList.size() > 0) {
                //记录实验更新时间,并更新expWhitelistMap
                userExperimentWhiteListLastUpdateTime = userExperimentWhitelistList.get(0).getUpdateTime();
                expWhitelistMap = getAllUserExperimentWhitelistMap(userExperimentWhitelistList);
            }
            //从每个顶层域开始构建环境,并更新newRootDomainNameExpCtxMap
            if (rootDomainList == null || rootDomainList.size() == 0) {
                LOG.warn("{} can't find rootDomainList from db!", expAppName);
            } else {
                for (Domain domain : rootDomainList) {
                    //记录域->更新时间
                    rootDomainUpdateMap.put(domain.getName(), domain);
                    initOneDomain(domain, newRootDomainNameExpCtxMap);
                }
            }
            if (newRootDomainNameExpCtxMap.size() > 0) {
                rootDomainNameExpCtxMap = newRootDomainNameExpCtxMap;
            }
        } catch (Exception e) {
            LOG.error("", e);
        }
        LOG.info("reload spends [{}]ms", System.currentTimeMillis() - initStartTime);
    }

    /**
     * 更新函数
     */
    private static void update() {
        //顶层域更新
        List<Domain> rootDomainList = httpConfigQueryService.getRootDomains(expAppName, status);
        List<String> rootDomainNameList = new ArrayList<>();
        for (Domain domain : rootDomainList) {
            rootDomainNameList.add(domain.getName());
        }
        //必须在rootDomain查询完成之后,设置白名单到全局
        List<UserExperimentWhitelist> userExperimentWhitelistList = httpConfigQueryService.getAllUserExperimentWhitelistList(expAppName, rootDomainNameList);
        if (userExperimentWhitelistList.size() > 0) {
            boolean needUpdate = false;
            for (UserExperimentWhitelist userExperimentWhitelist : userExperimentWhitelistList) {
                if (userExperimentWhitelist.getUpdateTime() > userExperimentWhiteListLastUpdateTime) {
                    needUpdate = true;
                    break;
                }
            }
            if (needUpdate) {
                LOG.info("update expWhitelist!");
                userExperimentWhiteListLastUpdateTime = userExperimentWhitelistList.get(0).getUpdateTime();
                expWhitelistMap = getAllUserExperimentWhitelistMap(userExperimentWhitelistList);
                if (rootDomainList != null && rootDomainList.size() > 0) {
                    for (Domain domain : rootDomainList) {
                        Map<String, List<Experiment>> userWhitelistMap = initOneDomainWhitelist(domain, expWhitelistMap.get(domain.getName()));
                        ExperimentContext experimentContext = rootDomainNameExpCtxMap.get(domain.getName());
                        if (experimentContext != null) {
                            experimentContext.setUserWhitelistMap(userWhitelistMap);
                        }
                    }
                }
            }
        }
        //场景更新
        if (rootDomainList != null && rootDomainList.size() > 0) {
            for (Domain domain : rootDomainList) {
                boolean needUpdate = false;
                if (rootDomainUpdateMap.containsKey(domain.getName())) {
                    //更新
                    if (domain.getUpdateTime() > rootDomainUpdateMap.get(domain.getName()).getUpdateTime()) {
                        needUpdate = true;
                    }
                } else {
                    //新增
                    needUpdate = true;
                }
                if (needUpdate) {
                    //记录域->更新时间
                    rootDomainUpdateMap.put(domain.getName(), domain);
                    initOneDomain(domain, rootDomainNameExpCtxMap);
                    LOG.info("update id={}`name={}`updateTime={}", domain.getId(), domain.getName(), domain.getUpdateTime());
                }

            }
        }
    }

    /**
     * 获取所有配置的白名单
     * root
     *
     * @return
     */
    public static Map<String, List<UserExperimentWhitelist>> getAllUserExperimentWhitelistMap(List<UserExperimentWhitelist> resultList) {
        Map<String, List<UserExperimentWhitelist>> resultMap = new ConcurrentHashMap<>();
        if (resultList == null) {
            return resultMap;
        }
        for (UserExperimentWhitelist expWhitelist : resultList) {
            if (expWhitelist.isDisabled()) {
                continue;
            }
            if (resultMap.containsKey(expWhitelist.getRootDomainName())) {
                resultMap.get(expWhitelist.getRootDomainName()).add(expWhitelist);
            } else {
                List<UserExperimentWhitelist> list = new ArrayList<>();
                list.add(expWhitelist);
                resultMap.put(expWhitelist.getRootDomainName(), list);
            }
        }
        return resultMap;
    }

    /**
     * 初始化一个域下的白名单
     *
     * @param rootDomain
     * @param userExperimentWhitelists
     * @return
     */
    private static Map<String, List<Experiment>> initOneDomainWhitelist(Domain rootDomain, List<UserExperimentWhitelist> userExperimentWhitelists) {
        Map<String, List<Experiment>> userWhitelistMap = new HashMap<>();
        if (userExperimentWhitelists != null && userExperimentWhitelists.size() > 0) {
            for (UserExperimentWhitelist whitelist : userExperimentWhitelists) {
                try {
                    if (whitelist.getExperimnetNames() != null && whitelist.getExperimnetNames().length() > 0) {
                        String[] expNameArr = whitelist.getExperimnetNames().split(",");
                        List<String> expNameList = new ArrayList<>();
                        for (String expName : expNameArr) {
                            expNameList.add(expName);
                        }
                        if (expNameList.size() > 0) {
                            List<Experiment> whiteExperimentList = httpConfigQueryService.getExperimentsInNames(rootDomain.getId(), expNameList);
                            if (whiteExperimentList != null && whiteExperimentList.size() > 0) {
                                userWhitelistMap.put(whitelist.getUserKey(), whiteExperimentList);
                            }
                        }
                    }
                } catch (Exception e) {
                    LOG.error("", e);
                }
            }
        }
        return userWhitelistMap;
    }

    /**
     * 初始化一个域,并装进新的域环境map中
     *
     * @param rootDomain
     * @param newRootDomainNameExpCtxMap
     */
    private static void initOneDomain(Domain rootDomain, Map<String, ExperimentContext> newRootDomainNameExpCtxMap) {
        //顶层域,获取默认参数,必须要有,否则抛错并忽略此root_domains
        Map<String, String> defaultParameterMap = CommonUtil.paramToMap(rootDomain.getId(), rootDomain.getDefaultParameters());
        if (defaultParameterMap == null || defaultParameterMap.size() == 0) {
            LOG.error("rootDomainId={}`name={}`defaultParameters={} can't paramToMap", rootDomain.getId(), rootDomain.getName(), rootDomain.getDefaultParameters());
            return;
        }
        //顶层域,获取发布层参数
        List<Layer> launchLayerList = httpConfigQueryService.getLaunchLayersByRootDomainId(rootDomain.getId());
        //获取用户配置白名单实验列表
        Map<String, List<Experiment>> userWhitelistMap = initOneDomainWhitelist(rootDomain, expWhitelistMap.get(rootDomain.getName()));
        //从顶层域构建整个实验
        buildDomainRecursive(rootDomain);
        //组装一个场景对应的一套实验环境
        ExperimentContext experimentContext = new ExperimentContext(defaultParameterMap, launchLayerList, userWhitelistMap, rootDomain);
        newRootDomainNameExpCtxMap.put(rootDomain.getName(), experimentContext);
        //校验
        ValidateService validateService = new ValidateService(experimentContext);
        validateService.validate();
        LOG.info("nowRootDomainSize={}`rootDomainName={}`approved={}`ExperimentContext={}", rootDomainNameExpCtxMap.size(), rootDomain.getName(), validateService.isApproved(), validateService.getEcsb().toString());
    }

    /**
     * 每次请求调用
     * 递归遍历域,层和子域,命中的实验放在hitSements
     *
     * @param currentDomain
     * @param expReq
     * @param hitSements
     */
    private static void hitByDomainRecursive(String rootDomainName, Domain currentDomain, ExpReq expReq, List<Segment> hitSements) {
        if (currentDomain.getCommonLayers() != null && currentDomain.getCommonLayers().size() > 0) {
            for (Layer layer : currentDomain.getCommonLayers()) {
                if (layer.getExperimentList() != null && layer.getExperimentList().size() > 0) {
                    for (Experiment exp : layer.getExperimentList()) {
                        if (exp.hitExp(rootDomainName, expReq, layer.getName())) {
                            hitSements.add(exp);
                        }
                    }
                }
            }
        }
        if (currentDomain.getChildDomains() != null && currentDomain.getChildDomains().size() > 0) {
            for (Domain domain : currentDomain.getChildDomains()) {
                if (domain.hitExp(rootDomainName, expReq, currentDomain.getName())) {
                    //只记录命中的实验?
                    hitSements.add(domain);
                    //继续递归子domain
                    hitByDomainRecursive(rootDomainName, domain, expReq, hitSements);
                }
            }
        }
    }

    /**
     * 初始化调用
     * 递归构建一个域,及其子域和层
     *
     * @param domain
     */
    private static void buildDomainRecursive(Domain domain) {
        //获取domain下所有普通层
        List<Layer> layerList = httpConfigQueryService.getCommonLayersByDomainId(domain.getId());
        if (layerList.size() > 0) {
            domain.setCommonLayers(layerList);
        }
        //获取domain下所有域
        List<Domain> domainList = httpConfigQueryService.getDomainsByParentId(domain.getId());
        if (domainList == null || domainList.size() == 0) {
            return;
        }
        domain.setChildDomains(domainList);
        for (Domain childDomain : domainList) {
            buildDomainRecursive(childDomain);
        }
    }

    /**
     * 每次请求调用
     * 是否命中用户白名单,命中把实验加入到hitSegments
     *
     * @param expCtx
     * @param expReq
     * @param hitSements
     * @return
     */
    private static boolean hitUserWhitelist(ExperimentContext expCtx, ExpReq expReq, List<Segment> hitSements) {
        if (expCtx.getUserWhitelistMap() != null) {
            //必须转成String类型的userId【优先】
            String userId = String.valueOf(expReq.getUserId());
            if (expCtx.getUserWhitelistMap().containsKey(userId)) {
                List<Experiment> whitelistExpList = expCtx.getUserWhitelistMap().get(userId);
                if (whitelistExpList != null) {
                    for (Experiment experiment : whitelistExpList) {
                        hitSements.add(experiment);
                    }
                }
                if (hitSements.size() > 0) {
                    LOG.info("requestId={}`userId={}`deviceId={}`hitUserWhitelist={}", expReq.getRequestId(), expReq.getUserId(), expReq.getDeviceId(), hitSements.size());
                    return true;
                }
            }
            //deviceId其次
            if (expCtx.getUserWhitelistMap().containsKey(expReq.getDeviceId())) {
                List<Experiment> whitelistExpList = expCtx.getUserWhitelistMap().get(expReq.getDeviceId());
                if (whitelistExpList != null) {
                    for (Experiment experiment : whitelistExpList) {
                        hitSements.add(experiment);
                    }
                }
                if (hitSements.size() > 0) {
                    LOG.info("requestId={}`userId={}`deviceId={}`hitUserWhitelist={}", expReq.getRequestId(), expReq.getUserId(), expReq.getDeviceId(), hitSements.size());
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @param hashType
     */
    public static void setHashType(HashType hashType) {
        ExpConfigManager.hashType = hashType;
    }

    /**
     * @param diversionId
     */
    public static void setDiversionId(DiversionId diversionId) {
        ExpConfigManager.diversionId = diversionId;
    }

    public static HashType getHashType() {
        return hashType;
    }

    public static DiversionId getDiversionId() {
        return diversionId;
    }

    public static void setRehash(String scene, String shuffle) {
        if (scene != null && scene.length() > 0 && shuffle != null && shuffle.length() > 0) {
            rehashMap.put(scene, shuffle);
        }
    }

    public static Map<String, String> getRehashMap() {
        return rehashMap;
    }
}


四、特色

实现上基本参考google论文,做了一些简化

  1. 层只包含实验,不包含域,使用上非常简单,不用考虑层套域,域套层的复杂问题场景。
  2. diversion type做成静态变量,初始就选好(我们用设备号deviceId)。google论文的diversion type可以有userId,cookie,random等,并且每个experiment要配置一个diversion type。
  3. 场景级别rehash(必要时支持层rehash),为避免流量天然不均,提供场景级别rehash以达到流量重新打散,避免流量惯性问题。
  4. 基本不用发布层,直接用默认参数替代发布层参数,简化上线流程。
  5. 每层流量区间[0-99],不用1000的区间,简化实验流量配置。
  6. 与策略平台打通。itbasketplayer:推荐系统(工程方向)-策略平台

五、5个校验条件

  1. 域、层、实验的名称不能重复
  2. 一个实验参数,不能同时出现在多个实验层(可同时出现在发布层)
  3. 一个实验参数,不能同时出现在多个发布层(可同时出现在实验层)
  4. 一个实验参数,如果出现在实验层或发布层,那一定要出现在顶层域的默认参数中(每个实验参数必须有兜底)
  5. 每个域(子域)、每个层(实验)的流量加起来必须=100,且不重叠
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值