一、背景
早期我们进行ab实验,用的是单层实验,即按用户userId/deviceId进行hash取模,打在[0-99]区间,然后配置A:[0-49],B:[50-99]。
单层实验有如下问题:
- 流量饥饿,如果流量只有2W,需要做20组实验,每组1000个流量太少,导致置信区间太宽,实验结果不可信。
- 流量偏置,假设上游的实验把所有的年轻人都获取了,下游的实验,没有年轻人的样本,导致有偏差。
二、分层实验概念
参考google分层实验论文
https://ai.google/research/pubs/pub36500
分层实验三个概念:
- 域:流量的一个大的划分,最上层的流量进来时首先是划分域。(流量实体段)
- 层:系统参数的一个子集,一层实验是对一个参数子集的测试。层可以包含域、实验(流量容器)
- 实验:具体实验,包括分桶,实验参数,是否对照组等。(流量实体段)
三、实现方案
admin UI重要界面
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论文,做了一些简化
- 层只包含实验,不包含域,使用上非常简单,不用考虑层套域,域套层的复杂问题场景。
- diversion type做成静态变量,初始就选好(我们用设备号deviceId)。google论文的diversion type可以有userId,cookie,random等,并且每个experiment要配置一个diversion type。
- 场景级别rehash(必要时支持层rehash),为避免流量天然不均,提供场景级别rehash以达到流量重新打散,避免流量惯性问题。
- 基本不用发布层,直接用默认参数替代发布层参数,简化上线流程。
- 每层流量区间[0-99],不用1000的区间,简化实验流量配置。
- 与策略平台打通。itbasketplayer:推荐系统(工程方向)-策略平台
五、5个校验条件
- 域、层、实验的名称不能重复
- 一个实验参数,不能同时出现在多个实验层(可同时出现在发布层)
- 一个实验参数,不能同时出现在多个发布层(可同时出现在实验层)
- 一个实验参数,如果出现在实验层或发布层,那一定要出现在顶层域的默认参数中(每个实验参数必须有兜底)
- 每个域(子域)、每个层(实验)的流量加起来必须=100,且不重叠