微信扫一扫识物是典型的“离线写,在线读”的业务,业务数据的存储和检索库的构建都是在离线环节完成。我们通过爬虫系统收录了小程序生态下的商品图片,下载后进行检测抠图,提取检索特征,最终构建成检索库交付到线上环境。这篇文章将主要介绍这一部分的工作。
识物是以图像或视频作为输入,用以挖掘微信生态下商品、物品等有价值等信息。这里我们基本覆盖了微信全量优质小程序电商,涵盖上亿商品SKU,聚合了微信内的搜一搜、搜狗等资讯,最终聚合后呈现给用户。百度识图和阿里拍立淘也是基于该技术发展而来。
工程上,识物工作主要可以分为三块,如图1所示:
算法侧主要是对检测模型和多类目的检索模型等持续炼丹,检测模型需要返回图片中物品的准确位置;检索模型需要保证同款物品的特征表达越近越好。
识物是典型的“离线写,在线读”的业务,业务数据的存储和检索库的构建都是在离线环节完成。我们通过爬虫系统收录了小程序生态下的商品图片,下载后进行检测抠图,提取检索特征,最终构建成检索库 交付到线上环境。这篇文章将主要介绍这一部分的工作。
3.在线部署
算法模型和离线生成的检索库最终完成部署,对外服务。用户识物时,检索库会召回一批相似物品,再经过一系列复杂的精排、过滤逻辑,最终返回用户看到的结果。
数据版本主要分为两类,一是算法模型版本,我们有10+种业务模型,平均每周有2-3个模型迭代升级。二是检索库版本,在模型不迭代的情况下,每天有新数据的合并,即增量迭代;而每次算法模型变更,特征表达发生改变,需要根据新特征重新构建检索库,即全量迭代。
在高频的版本变更场景下,如何兼顾灵活性与安全性。
目前我们收录的图片数为10亿左右,平均每天新增1500w。除了图片数量多,任务的流程也很多,如图片下载、目标检测、特征提取等任务,每个任务每天都是千万级的数据处理量。
如何高效的处理数据,提升业务的迭代效率。
随着业务的发展,简单的业务流程已经不能满足我们日益复杂的业务需求。为了提升业务指标,我们可能还需要图片质量,文本语义,死链、下架商品的过滤等任务。
如何在流程日益变多的情况下,不导致整个系统的臃肿。
离线工程属于重流程的业务,数据从产生和落地将经历九九八十一环,任何一环出错都会导致结果有问题。发现问题的时间越晚,修复的成本越高,对业务的影响越难以估计。
如何科学的监控和管理数据质量,使系统有良好的可维护性。
这里有多种维度的数据版本,例如模型版本,特征版本,检索库版本等,上游环节的版本变更将引发后续环节的变更,最终都将导致检索库版本变更。
在我们的业务场景下,检索库的迭代是高频操作,正常情况下每天会增量更新,而模型的变更又会引发检索库全量更新。数据量级上,我们的全量图像是亿级别的,按类目分库后每个类目也是千万级。
我们调研了业界内主要用于图像检索的技术,如图3所示。综合考虑后,我们选取了灵活性更强、相对内存占用更小的的faiss-ivf作为我们的索引库构建算法。
对于每天的增量数据,我们每天对每个类目(10+个类目)都会构造一个对应当天数据检索库。每个类目的全量检索库是由N天的检索库合并生成(faiss-ivf特性),2000w的数据合并仅需要4分钟。基于这样的设计,使得我们可以灵活的选取时间窗口的范围,如图3所示了窗口为2的合并方法。
这样的好处是,如果某天数据发现有问题,只需要修复当天数据后再进行合并即可;如果需要丢弃某些数据,如旧数据,合并时不选取即可。
前面我们讲到,模型变更最终都将引发检索库的全量迭代,这里的模型有检测模型和**检索特征模型。**新检索库上线时,本质上是新旧数据的过渡,一般实现新旧数据的切换都会设计复杂的系统来保证数据一致性。
2.2.1 检测模型变更
这种场景下的检索库变更,严格上来讲我们并没有实现新旧数据的一致性,我们只是通过简单的方法使得即使新旧数据同时存在也不影响用户的体验**。
这里主要涉及到如何构建我们的映射关系,我们为每次检测出的结果都赋予一个唯一的单调递增id。替换模型后,同一张图片的检测结果会变化。可能抠图的位置有变化、可能会扣取不同的物品、可能会扣取多个物品。
如图5所示,检索库v1里只有上衣,对应检索id为1;变更检测模型后,检索库v2可以同时检测出上衣和下衣,对应检索id为2,3。这样在线模块可以逐步更新检索库,线上同时存在新旧检索库也没有影响,如果请求落到旧库返回1,落到新库返回2,但最终都将返回正确的结果,结果上是一致的。
2.2.2 检索特征模型变更
这种场景下的检索库变更则复杂许多,检索库存的特征来自于检索特征模型。检索模型变更后,同一个物品图片的特征表达完全不同,维度甚至也发生了变化,如图6所示。
我们需要同步变更检索特征模型服务和新检索库,通过双buffer的方式实现新旧数据的共存,而且要实现严格的路由协议来保证同一个请求在同版本的特征检索服务和检索库中完成。
在开发过程中,算法需要交付各种模型给离线和在线,离线生成的检索库也需要交付给在线,数据版本的迭代也需要考虑版本的可回退性。为了解耦多方之间的依赖,且避免在同步过程中直接操作文件带来的风险,设计了一套数据版本管理系统。
如图7所示,资源发布者上传资源到该系统,并附带对应业务、版本号及md5。资源使用者只需要理解对应业务当前的版本号,版本管理系统会返回对应的资源文件。线上实际使用时,在线模块会定期轮训某业务对应数据版本文件的md5和本地文件md5是否一致,不一致则会拉取最新的文件,拉取完成后校验md5是否一致,最终实现更新。
在业务模型或检索库需要回退时,只需修改配置文件,重启服务即可。
目标检测、检索特征提取等是典型的图像深度学习任务,业界内有caffe、pytorch、tensorflow、tensorRT等多种深度学习框架,有的框架不能保证向上兼容。而我们负责炼丹的同学第一要务是追求效果指标,在尝试各种奇淫巧技时练出来的丹通常并不能和微信的线上环境很好的兼容。
简而言之,在重算法的工程系统中,不仅有业务代码的更新,还有工程环境的迭代。这非常适合使用docker来封装和迭代业务环境。 通过docker化部署,我们可以更方便的引入更多开源组件来支撑业务,也可以让我们在一些框架选型上更加灵活。
就我们自己的业务场景而言,我们还可以利用微信深度学习任务平台(yard)的计算资源,这部分属于公用资源,需要抢占式使用。yard也是docker化去执行任务。这为我们业务可以借助yard公用资源作为临时扩容worker节点做了很好的铺垫。
我们每天平均有1500w增量数据,全量为十亿级别的数据。单机必然无法满足处理的实效性,唯有分布式计算才能满足要求。
正如mapreduce,map阶段的工作我们需要对数据进行拆分。这里对拆分原则除了平均外,还考虑了拆分后到数据的运行时间。如果拆分太细GPU的运行效率会降低,拆分太粗会导致错误修复的时间成本变大。我们让每个拆分后的任务都尽量控制在1小时内完成,最终拆分的粒度为每个包10w左右。
拆分后的数据进行并行计算相当于reduce阶段,这里的重点是如何将拆分后的数据分发到多台机进行计算。此外,我们还希望公用资源空闲时可以非常灵活的进行扩容接入,提高并发处理能力。
我们结合zookeeper的分布式锁特性,实现了一套可靠分布式任务队列。worker采用拉模式拉取队列的任务。这样的优点是伸缩性好,可以灵活的增加和减少yard的机器资源。如图8,当新worker接入后,从队列中拉到任务直接执行,可以实现秒级的扩容。
对于我们的场景,任务需要被可靠消费,这里的可靠包含来两层含义。
第一是避免任务被重复消费,我们借助zookeeper的保活锁,锁通过心跳保持活性。如图9中第1,2时刻,worker拿到队列里的任务抢锁成功才可执行;如果出现机器宕机,如图9第三3时刻,锁会自动释放。
第二是完整消费,我们在task被完全消费结束后才删掉队列里的对应task,如时刻4的task2。时刻3由于机器宕机,task1并未被完整消费,因此依旧存在,后续可被继续消费。
理论上讲,我们的消费模式属于至少一次消费(at least once),极端情况下,如果worker执行完任务还没有回传状态时宕机,那任务仍处于未成功消费,仍可能被后续worker消费。这里需要保证任务的幂等性。
引入公用计算资源提升了我们的处理能力,但同时也给我们带来了一些小问题。例如,公用集群的机器配置比我们自己集群要好很多,为了使不同集群都能发挥最大的GPU性能,我们支持不同集群使用不同的全局参数配置。而且公用集群和文件系统不在同一个idc,导致网络IO时间过长,降低了GPU利用时间,我们在公用集群的同idc实现了一套文件预拉取系统,根据任务队列中存在的任务,提前同步待消费文件到同idc的文件缓存系统。
为了提高GPU利用率,我们还做了大量的工程优化,这里就不展开叙述了。基于分布式计算的框架,极大提升了我们的计算效率。拿计算效率最低的目标检测任务举例,目前我们集群的处理能力可达到5600w图/天,如果加上公用计算资源,可以达到1.2亿图/天(集群12台P4双卡,公用集群yard-g7a集群平均10个双卡,深度学习框架使用的tensorRT)。
虽然我们每天有1500w左右的原始图片,但最终符合录入检索库的商品仅有一半不到。因为为了确保检索数据的质量,我们会在多个维度做数据过滤。现在我们的图片从下载到建库一共会经历30+种中间任务,图10仅展示了主要的任务流程模型。
随着任务的增多,尤其是许多任务间存在着复杂的依赖关系,每个任务都不是一个独立的个体,每个任务的成败都将影响最终的结果。为了更好的管理每个任务的状态,梳理任务间的依赖,使得工程的复杂度不随任务变多而变大,我们自研了一套任务调度系统。
调度系统主要由以下几个部分组成:
容灾性上,调度系统相关的模块均是多机多园区部署,只要不是某个模块完全挂掉,整套任务调度都可以正常执行。
对于每天的例行任务,实效性并不敏感,早几个小时或晚几个小时对业务影响不大。但GPU资源是是十分宝贵的,我们将部分GPU机器和在线GPU服务合并部署。结合在线流量屏蔽策略,实现高峰时期资源借给在线服务,低峰时期运行离线任务。
如图12所示,其为一台参与离线任务闲时调度的在线模块,我们拟定每天0点-7点的低峰时间为离线运行时间,7点-24点的高峰时间为在线模块服务时间。最大限度的利用了宝贵的机器资源。
前面做的工作保证了我们以任务为粒度的工程可靠性,但任务的成功并不能保证业务数据是完整的,如数据丢失、代码逻辑有问题等。为了监控数据维度上的业务质量,我们基于ELK搭建了一套数据系统,主要用于收集重要的基础数据、业务数据、运行结果等。
我们曾在几次版本迭代过程中,发现数据出错,但发现时已经付出了极高的时间代价。因此我们希望在任意时刻都能观察离线系统的运作是否正常,数据的流转是否符合预期。出现问题后可以及时干预修正,降低错误成本。
我们对涉及数据流转的核心任务都做了数据结果上报,这样子我们可以通过数据漏斗发现是否出现问题。这个问题在全量数据重跑的时候尤其重要。图13展示了项目中核心任务的数据情况。
上图看上去是每天任务级的数据监控,但实际上我们我们的设计是扩展到了**每次任务级(这里定义为planid),**既可以是每天,也可以是每次覆盖多天的重跑。 我们按图14的字段上报业务的运行结果,前4个字段组成联合唯一索引,planid作为区分每次运行的逻辑字段。这样即使同一个任务在不同时期运行结果是不同的,我们也能区分每一次运行后,真实的数据结果。这个设计在保证每次大版本数据迭代时,对于把控数据整体运行质量十分重要也十分有效。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。