随着电商行业的快速发展,用户对于在线购物体验的要求也在不断提高。特别是在服装领域,用户希望能够更加直观地了解商品的实际穿着效果。为此,淘宝试衣项目应运而生,通过AI技术为用户提供个性化的试衣体验。本文将详细介绍淘宝试衣项目的背景、现状、合作场景以及技术实现,特别是如何通过定时任务驱动的试衣素材高效写入IC(商品中心)拓展结构,提升用户体验和转化率。
背景
▐现状服饰属于非标商品,用户无法直观的通过模特上身图去预估自己的真实上身结果;在详情页内,用户主要通过卖家秀、问大家等获取真实参考;因此部分数据不全的商品内,用户难以快速决策发生购买。
▐淘宝试衣已经做了什么扩大女装商品覆盖:例如连衣裙、上装等支持同商品的不同sku的试穿,提供分类目的不同上身效果的衣长指南优化多版模特,使其更加真实自然;支持多种身型模特以及用户照片试穿优化试衣效果及清晰度▐淘宝试衣在其他合作场景LAZADA详情:为没有模特上身图的商家替换东南亚模特试穿的详情主图淘宝详情、购物车等:试衣标志,为有需求的用户提供试衣体验BC消息:咨询界面为客户实时提供试穿入口,让用户看到真实试穿效果▐淘宝试衣x详情场景的承接演变:原沉浸式试衣间形态在详情场景下不适应的问题:
1. 沉浸式试衣间要做页面跳转,跳出详情,阻断交易链路
2. 沉浸式试衣间内的商品来自推荐和衣橱,不适用于详情场景
3. 用户实时试穿,需要等待,影响用户体验,受限于显卡资源无法承受详情这么大的qps
为了让更多的用户能够直面接触淘宝试衣以及带给用户更好的试衣体验;我们与详情进行合作:
1. 在详情页增加ai试衣锚点,让用户在主图区域直观看到ai试衣效果;
2. 算法预跑不同身形下的试衣效果数据,服务端使用离线任务将其写入ic拓展结构,用于锚点试衣效果展示,解决详情场景下请求过多导致显卡资源不足问题
3. 用户同样可在商品主图区域直接触发试衣浮层,使用不同的模特或使用自身照片,选择不同的sku进行试穿;
淘宝试衣 x 详情页关于模特的位置以及外观形态等方面正在优化中,近期上线新模特。
▐目标试衣视角:详情渠道来访用户提升xw(依赖于与详情的商议条件人群,建议卖家秀及评价数据为0的女装商品)详情渠道用户人均试穿n次详情视角:通过在商品主图为用户提供更个性化的试穿效果图,帮助用户快速下决策,提升该部分用户的在详情的笔数转化率a%当然在上线后我们也是发现了很多不足,无论是模特还是页面的排版上都有很多不合理的地方,我们正在对这一部分进行优化,下面先看一下本次合作相关链路,稍后进入正题:网格化任务写ic。
淘宝试衣 x 详情合作主链路
此次完成的离线任务主要支持的工作是将商品的ai试衣信息写入ic拓展结构。
试衣素材写入IC
试衣素材写ic实现方式:odps数据预处理 + ScheduleX网格化任务
▐1. 高性能(高效)目标性能可横向扩展(在不考虑上下游依赖的情况下,增加机器可提升性能),现阶段目标实现百万级商品在小时内完成保障打标成功率达到99.9%任务失败重跑断点续传任务执行进度及结果可视化目标实现于效率优化部分 4子任务分发以及处理部分效率优化
▐2. odps数据预处理多行数据整合:由于上游提供的数据为:商品下的每一张图片对应一条数据,每个商品平均5张图片,则一个商品会产生约5条数据。
不进行预处理的情况且假设请求速率不变的情况下,在调用离线任务时,每条数据去进行ic写入,请求量级上,qps会被放大5倍;另外在代码逻辑上不能直接采用覆盖更新ic试衣拓展结构的方式,而是每条数据都需要先查询ic进行校验后追加数据,这种情况下,qps又被放大2倍;
代码内存中聚合实现成本高且灵活性差,那么在现实场景下会导致每秒的商品处理量会急剧降低,因此我在对ic进行数据写入之前对odps同商品数据进行了整合操作。
在上游提供的原数据表中一个item存在多条试穿数据,我需要先将该数据按item维度进行Group BY后,再使用WM_CONCAT()与CONCAT()将其中每条数据的相应字段整合到一个名为extend_info的字段中进行汇总,其中包含多条试穿数据,为了在离线任务中能够更好的进行对象格式的转换,需要将其组装为json格式;最终,实现数据的正确获取以及整合,存储至新表中。
▐3. 实现数据的正确获取及处理(ScheduleX网格化任务)关于离线任务写ic拓展结构的主要流程如下:
在网格化任务处理过程中,主要包含三大部分:子任务的分发、子任务的正确处理、执行结果汇总。
@Overridepublic ProcessResult process(final JobContext jobContext) throws Exception { // 处理master任务 if (isRootTask(jobContext)) { return processRootTask(jobContext); } // 处理分发子任务 if (StringUtils.equals(jobContext.getTaskName(), SUB_TASK_NAME)) { return processDressOfflineDataWritingIcTask(jobContext); } return new ProcessResult(true);}// 执行结果汇总@Overridepublic ProcessResult reduce(final JobContext jobContext) throws Exception {}子任务分发在dts平台进行任务运行时,选择了关键信息参数传递的方式:
/** * 组装离线任务上下文 * * @param jobContext 任务基本信息 * @param context 离线任务上下文 */ public void assembleContextParam(final JobContext jobContext, final DressWritingIcTaskContext context) { final JSONObject params; try { params = JSON.parseObject(jobContext.getInstanceParameters()); } catch (Exception e) { throw new RuntimeException(e); } context.setUpdateType(TaskUpdateTypeEnum.parse(params.getString(UPDATE_TYPE))); context.setTableName(params.getString(ODPS_TABLE)); context.setProjectName(params.getString(ODPS_PROJECT)); context.setPartition(params.getString(PARTITION)); context.setTaskId(jobContext.getTaskId()); context.setJobInstanceId(jobContext.getJobInstanceId()); }子任务分发主流程:
子任务处理@Overridepublic ProcessResult process(final JobContext jobContext) throws Exception { // 处理master任务 if (isRootTask(jobContext)) { return processRootTask(jobContext); } // 处理分发子任务 if (StringUtils.equals(jobContext.getTaskName(), SUB_TASK_NAME)) { return processDressOfflineDataWritingIcTask(jobContext); } return new ProcessResult(true);}关于子任务的处理部分,在任务分发的时候就会指定分发子任务的标识:
这样在子任务处理流程中,拿到对应标识的任务上下文处理即可;
/** * 子任务处理主流程 * * @param jobContext 子任务基本信息 * @return 子任务处理结果 */ private ProcessResult processdressWritingIcTask(final JobContext jobContext) { // 1.获取子任务上下文 final DressWritingIcTaskContext dataWritingIcTask = (DressWritingIcTaskContext)jobContext.getTask(); // 2.处理子任务 final TaskUpdateResult taskUpdateResult = processRecordsByPage(dataWritingIcTask); // 3.返回任务处理结果 return new ProcessResult(true, JSONObject.toJSONString(taskUpdateResult)); }执行结果汇总/** * 结果汇总发送钉钉以及群机器人通知主流程 * * @param jobContext 任务相关数据 */ @Override public ProcessResult reduce(final JobContext jobContext) { final TaskUpdateResult processResult = dressWritingIcProcessManager.getSuccessCountFromProcessResult(jobContext); // 数据组装... // 发送钉钉通知 }/** * 子任务处理结果统计 * * @param jobContext reduce中获取任务基本信息及结果容器 * @return 汇总更新结果 */ public TaskUpdateResult getSuccessCountFromProcessResult(JobContext jobContext) { TaskUpdateResult taskUpdateResult = new TaskUpdateResult(); for (String value : jobContext.getTaskResults().values()) { if (StringUtils.isNotBlank(value)) { try { // 对子任务执行结果中需要采集的数据进行整合 // ... } catch (Exception e) { LoggerUtil.error(logger, e, "Parse taskUpdateResult failed,value:", value); } } } return taskUpdateResult; }通过reduce方法,获取各个子任务的执行结果,进行汇总后组装数据发送钉钉通知等。
子任务分发以及处理部分效率优化
▐线上的机器配置关于资源利用情况:
根据每条数据的数据量去计算,百万条数据均分至线上机器后,每台机器内存占用约为2.5%,由此可以看出,离线任务的卡点不在资源配置上吗,所以我们后续专注于如何提高运行效率。
▐任务处理效率对比下面将第一版主流程图以及优化后的主流程图进行对比:
优化前主流程这一版代码一开始时并没有实现匀速请求,请求不断的发出,在预发测试时,触发了ic限流,catch(BlockException e)不能正常捕获,ic限流后异常类被sentinel重新包装返回,因为没有注意到这一点,当时也是排查了很久。
虽然修改后能够实现数据的正确处理以及匀速请求,但是因为单线程循环处理方式以及分页不合理,以及限流后直接失败等因素,导致数据处理速度较慢、成功率不达标;另外在实现匀速请求后,sentinel的限流就失去了他的作用,因此进行了代码优化。
优化后主流程目标:
通过网格化任务,实现百万级淘宝试衣商品打标在小时内完成,同时保障打标成功率达到99.9%。
结果:
实现百万条数据在半小时内处理完毕,并且保障打标成功率99.9%;
实现断点续传,执行进度和结果可监测等。
初始方案产生的问题:
任务锁范围为整个任务,过程中异常导致锁无法释放;子任务分页采用Switch配置,没有其他分页策略;分页不合理,小数据量任务耗时较高;任务执行采用多机器单线程方式执行;机器资源没有得到充分利用,qps较低,处理缓慢。最终没有实现小时内打标百万的目标,打标时间超时。
相关优化方式:优化点一:减小任务锁范围,增加任务锁释放失败重试;
优化点二:子任务分页
此前得知,单线程时请求更新耗平均约为40ms,那么单机单线程qps约为25qps,ic商品中心规定qps千级别以下请求不需要考虑限流问题,因为我们规定单机限流100,线上机器共10台,虽然理论值1000qps,实则可能小于1000qps; 进行子任务分发时,单机限流100,则规定单机4线程并行处理数据,以一分钟单机处理最大数据量为界限:60 * 1000ms /30 * 4 = 8000 条数据,则switch配置的分页阈值为8000; 上述提到的40ms是大部分请求下的执行耗时,30ms是较快请求下的执行耗时。 如果odps数据总量totalCount <= 在线机器采用默认分页可以处理的数据量(默认单机分页*在线机器数) 则单机任务处理量为默认界限值, 任务数量由总数量/分页获取; 否则(超过界限值的情况下): 采用数据总量/机器数获取单机分页值,任务数量为机器数量采用这样的策略可以使任务分页更加合理,另外,如果按照之前的逻辑,我们每次请求任务时都需要临时去修改switch分页值;每次都要走审批流程,也是不小的时间花费。
优化点三:线程池任务分页,线程池四个子线程任务分页时,直接采用均匀分页策略;
无论在线程任务分页还是在子任务分发时分页,都要考虑到最后一个任务的数据量问题,最后一个任务数据量应为:总数据量 - 前面分页已经分发掉的数据量,而不是直接使用分页值,这样可以避免数据重复更新,从而避免增加更新耗时。
优化点四:取消通过记录请求时间以及sleep实现匀速请求的方法,直接采用sentinel的限流配置,当触发限流后进入队列排队等待;之前的匀速请求是为了尽量避免触发限流,新的策略是触发限流进行等待,实则本质上还是匀速的思想,将请求时间进行平均处理;
另外如果限流等待后还是重新触发了限流,那么会进入代码中的本地限流重试,重试最大次数为3次,3次均失败后,此条数据更新失败;(当然,在这里也可以选择使用消息实现限流重试,因为本地限流已经能够满足基本需求,所以在此没有接入消息)
高性能目标实现1. 通过上述优化后的处理速度:(实现目标一、目标二)odps预处理有效降低下游qps消耗;
在实际运行时,由于ic请求rt波动等原因,时间在预估时间误差范围内;另外,目前打标成功率为99.9%,成功率受ic限流触发以及重试结果影响。
其次采用优化后的分页,也可以加快小数据量的商品放量速度。
2. 断点续传(实现目标三):断点续传是用于记录任务执行偏移量;防止机器异常重启等情况导致任务重跑时,任务重新从0开始执行,重复更新增加执行耗时和资源浪费等。
采用tair记录的方式,在每轮数据更新结束后会对value进行更新,如果机器重跑,校验指定key,于记录位置继续执行该任务。
3. 增加报警、执行人奥格消息通知、群机器人通知等(实现目标四)执行过程可监测:
可以通过Schedulex任务控制台观察执行进度数据写入结果以及异常打trace日志,已做失败超阈值报警写数据以及写失败记录同样回流至odps表,利用odps周期任务能力整合记录,使数据都有迹可循执行结果通过钉钉消息的形式发送至相关人员展望
随着在改善试衣效果和模特效果、增加商品覆盖和业务场景覆盖上的不断努力,淘宝试衣已经进入一个新的阶段,在此次淘宝试衣x详情合作上线后,我们也发现了一些不足之处:模特的真实性,详情页主图构图效果等...,我们也会在后续的工作中不断完成这一部分的优化改进,争取进一步提升AI试衣的产品力,为用户提供更好的购物体验。
在我认为的理想世界中,未来的淘宝试衣不只是贴图的形式出现,那么试衣能不能做的更加立体更加多元化,也是我们不断在追求的方向,你可以想象到一个悬浮人物在详情页不同商品间搭配试衣的场景吗?这是我对AI试衣最初的展望;我也同算法同学讨论过,目前无法实现,但是我们会向着给予用户最佳体验的方向不断进化。
团队介绍
我们是淘天集团-用户运营技术团队,一支懂用户,技术驱动的年轻队伍,立足于体系化打造业界领先的用户增长基础设施,承担着捍卫电商主板块增长的重要使命,以媒体外投平台、ABTest平台、用户运营平台为代表的基础设施赋能阿里集团用户增长,坚持以用户为中心,通过技术创新提升用户全生命周期体验,持续为用户创造价值。团队技术氛围浓厚,倡导创新和工程师文化,积极探索新技术,推动用户增长业务高效稳健发展。