
好好学习,天天向上。
2020阿里巴巴双11技术专题精彩内容持续更新中戳我前往 今年天猫双 11「光棍节」变成了「双节棍」(2020 年双 11 代号,11 月 1 日至 3 日第一波,11 月 11 日第二波)。战线、周期比往年拉得更长,但令人意外的是,交易峰值不降反升,今年订单创建峰值高达 58.3 万笔/秒。 从 2009 年第一届双 11 算起,双 11 已经走过十一个年头。对普通消费者而言,对双 11 的体感可能仍是大幅优惠,当然还有打开界面、浏览挑选商品、直播互动、付款、收货的全链路体验一年一年更好。 但对阿里巴巴来说,双 11 越来越不像是个「市场活动」,给社会造了一个「节」之后,阿里巴巴搞双 11 自己的目标是什么? 其实,每一年的双 11 就像阿里巴巴内部组织的大型「科技运动会」,过去很多新型业务、新技术为了迎战双 11 被迫生长、锻炼出来,并被迅速催熟,成为阿里巴巴未来的业务支柱。同时,每一次技术层面的迭代、升级,都将阿里巴巴推向全新的商业空间,这可能是双 11 的另外一面。 双 11 期间,极客公园创始人张鹏与程立进行了一场访谈,双 11 背后不为人知的阿里巴巴视角,以及阿里巴巴对未来技术及商业走向的判断与设计,在访谈里逐渐浮出水面。 以下为编辑、整理后的对话内容: 程立(左)与极客公园创始人张鹏(右)对话 张鹏:为什么今年要搞成「双节棍」? 程立:主要是站在消费者体验的角度考虑。以前总要在那一天夜里去抢购,虽然很有过节的「仪式感」,但对大家来说时间太紧张,购物体验其实不够好。 今年手淘界面进行了大幅度改版,我们强化了首页信息流推荐,这背后是阿里认知算法体系的大升级,除了能帮用户把兴趣点缩小至具体商品,还能帮用户发现更多有趣的信息。「双节棍」和 AI 技术的结合,主要是希望用户能有更多逛的感觉。另外,这次在很多淘宝天猫的直播中,AI 支撑了许多主播与消费者互动聊天的场景,让用户有非常好的沉浸式体验。我们甚至复刻出一个虚拟主播,为消费者提供 24 小时服务。 张鹏:每一年双 11 阿里都会有些自己看中的变化和突破,今年双 11 最令阿里巴巴兴奋的核心变化是什么? 程立:尽管今年我们准备了更高的峰值能力,但交易峰值不再是主要看点了。我觉得今年的看点主要在技术创新上。去年我们叫双 11 核心系统「全面上云」,今年双 11 是「云上」的第一年,今年的云和去年的云有很大不同,有些技术是第一次使用。 可以理解为,去年商业侧的创新刚刚起步,规模不大,今年成为主流了。比如,在数字化供应链方面,我们开始支持线上线下新零售的真正融合,比如今年特别看重多层次履约的效果,也就是前端根据消费者需求,选择仓储物流发货的最优链路,将商品最快捷地送达消费者手中。 张鹏:去年是上云,今年是云上,怎么理解这件事? 程立:「云上」是指将阿里巴巴过去十一年,将双 11 场景中锻炼出来的技术,通过云这样的开放平台汇聚起来,不仅能够服务自己,还可以服务我们的合作伙伴、商家。云作为一个公共性的底座,使我们与商家之间协同不存在技术障碍。 比如,今年菜鸟的核心系统全部在云上,云上的网络平台使之具备了日均 10 亿次包裹的处理能力,突破了物流行业的技术极限。菜鸟的 IoT 物联网设备从仓库到配置也全部部署在云上,让无人配送成为可能。 再比如,数据中台成为云上开放的能力,阿里云数据中台提供的「全域数据中台解决方案」,可以帮助商家在线上及线下完成从数据到业务的充分融合,提升全域运营、新品创新、智能组货、供应链升级的整体效率。九阳就通过阿里云数据中台将一款新品的 ROI(投资回报率)提升了 322%。 云的定义也在变化,它成为了商业领域数字化的底座和基础,不再单指传统云计算了。新的云让商业效率提升和技术创新变得更加简单。 程立在 2020 年天猫双 11 全球狂欢季 张鹏:阿里「云上」的双 11,最终要通往哪里?要解决什么问题? 程立:在阿里,我们以十一年为一个轮回思考双 11,今年是双 11 新轮回的起点。 上一个轮回我们解决的是技术上的沟沟坎坎,下一个轮回留给我们最大的问题是商业创新接下来怎么继续突破。 各种技术迭代发展到今年,世界数字化的基础技术其实都已准备到位了,我们想把过去十一年技术和商业的创新有效组合起来,形成一个「数字原生商业操作系统」。 就像前面说的,现在不是局部解决问题的时代了,要运用新的技术,创造新的效率提升,需要整个商业链条一起前进。这个时候以一个「操作系统」作为基础,对所有环节的商业创新都特别有意义。这是阿里很想干的事情。 张鹏:是不是可以这样理解,之前是通过技术工具一个一个地解决问题,大家都是各自为战,见招拆招。现在是要以「云」的形态,构建一个系统,重新搭建一个更好的数字化商业的地基,然后再大家一起起更高的楼。 程立:没错。现在大家都已经发现数字化商业的「新大陆」了,但是不知道怎么开发,独立摸索既不现实,效率也低。 就像电刚刚被发明时,人类一开始只知道照明场景,却不知道电真正广泛应用后,带来的颠覆将远远超出人们的想象力。数字技术也是类似的,商业世界仅停留在原先见招拆招的思考层面是不够的,现在我们需要重新理解一个新技术带来的数字原生世界。 阿里巴巴的使命一直都是让天下没有难做的生意,数字原生商业操作系统将是阿里巴巴接下来的新起点。这样的「商业操作系统」,叠加更优秀的技术、更多的创新,将更高效、更全面地解决问题。一方面,我们让商业要素的资源调度更高效,另一方面,我们让应用场景变得更简单,两者结合将产生巨大潜力。 张鹏:今年是你上任阿里巴巴 CTO 后第一个双 11,作为技术「老大」,面对这样一个全国人民都关注的节点,是什么心情? 程立:作为当事人不可能不紧张,毕竟每年双 11 不容有失。但其实我们也挺兴奋的,每年双 11 也是一个非常难得的技术创新舞台。 往年的压力在于峰值能不能顶住,用户体验是否「丝般顺滑」。今年面对的更大挑战在于「双节棍」新玩法。过去双 11 的模式是提早把业务需求固定下来,严阵以待,调试到位,来迎接那一个晚上的峰值节点,这就像打固定靶。但今年改成了移动靶,我们甚至需要在双 11 进程中变更系统,承接新的业务创新需求。从固定靶到移动靶,挑战更大,这是以前不敢做的事情,某种程度上,恰恰是变成「云上」之后,我们才能从过去的束缚中解放出来。不迈出这一步,把业务的创新空间打开,我们的综合能力得不到提升。 程立:双 11 的技术挑战进入新的历史阶段 张鹏:回顾过去十一年的双 11 有什么阶段划分吗?中间闯过了哪些关? 程立:2009 年第一次双 11,就是淘宝商城临时决定搞个活动,技术侧还不太有感觉。 2010 年我们掉以轻心了,结果双 11 流量一下子涨了好几倍,突然发现服务器根本不够用,我当时在支付宝,被逼着把系统一再瘦身,只留下核心的支付链路,总算扛过了那次交易洪峰。 那一年以后,双 11 不再只是市场部门的活动,它对阿里底层技术提升也开始具有重大意义。 比如,2010 年,为了技术底盘和架构能支撑互联网海量流量,阿里巴巴启动「去 IOE」;2013 年阿里巴巴最后一台小型机下线,意味着我们支撑高并发交易的能力再也没有天花板。 2013 年,为提升数据算力能力,阿里巴巴提出「飞天 5K」项目;2014 年,「登月」工程将阿里内部数据汇聚在一个平台上。这两个瓶颈打开后,阿里巴巴在基础算力方面的限制基本消除了。 2015 年,双 11 又上了一个新台阶。移动互联网极大加快了商业创新,此时阿里提出了「中台」战略,打通底层人货场、实现数据驱动的中台,让商业创新变得更加简单明了,能更快地开枝散叶。 2017 年,成立达摩院也是很关键的节点。围绕未来数字时代的智能化能力,阿里巴巴形成了包括算力、算法、数据、IOT、区块链等领域的全新布局。此后,阿里双 11 进入全面智能化驱动的时代。 到了 2019 年,作为前面十一年所有节点的收官、总结,阿里巴巴提出全面上云,云正式成为包括中台在内的所有阿里巴巴核心系统的承载平台。我担任阿里巴巴 CTO 后,开始了对「云上」的思考。让我从技术角度总结过去十一年双 11,我觉得大概是这样一个历程。 张鹏:听起来,双 11 就是阿里巴巴的「运动会」,每年都琢磨着更高、更快、更强,压力之下,你们反而被往前推着上了一个一个新的台阶? 程立:客观地说,在阿里巴巴双 11 的历程中,有几次管理层提出了不同的意见。为了双 11 这一天的大卖和流量峰值,我们对技术的投入那么大,这笔账到底划算不划算?到后来没有人再怀疑这个问题。 比如,不做全链路压力测试的话,根本不具备迎接新时代日常流量的能力。双 11 的「变态」变成未来的「常态」,倒逼我们做各种创新,倒逼我们的云成为最强大的云。不处于当时的极限压力之下,你不可能突破常规去思考,一个典型例子是,为了支撑双 11 的流量,支付宝一个不到 100 人的团队,研发出可代替甲骨文数据库的 OceanBase 数据库。 没有双 11 的巨大「压强」,很多重大创新不可能发生。 张鹏:是否可以打个比方?过去双 11 就像人类在地上百米冲刺,人体肌肉能力有极限,再提升也就是百分之一秒级别了。但如果用数字化的发动机,通过天上飞来解决,就有秒级的提升空间,这就是要用商业创新问题,最终要用系统重构来解决的意义吧。 程立:是的,这个总结特别形象。阿里巴巴特别强调在一个新的维度内做商业和技术的联合设计创新,这是一种根本性的重构。如果商业只是把技术当作工具,或者技术只是把自己当作资源,是不可能形成任何「大设计」的,必须要进入更深层次的思考、突破。 程立(左)与极客公园创始人张鹏(右)对话 张鹏:阿里创始人是文科生,但技术体系反而做得特别厚重,敢于突破高难度、高风险的问题,为什么? 程立:跟阿里巴巴背后的基因有关。我们一直说要天马行空、脚踏实地。一方面,要非常有理想,另一方面,有理想后必须要落实、执行好。我认为阿里巴巴一直有这样的两拨人存在,且高度互信,阿里巴巴很好地将这两股力量融合在一起。过去很长一段时间内,阿里巴巴并不是技术最好的公司,但一步步脚踏实地地往前走,我们慢慢成为技术最好的公司。 张鹏:从战略眼光到落地执行,要形成闭环往往都挺难的。既要有宏大颗粒度的梦想,又要有中等颗粒度的世界观,还要在每个小颗粒度上落地实现,一个组织怎么才能同时做到这一点呢? 程立:这个确实不容易,有时候确实要靠积累。从我个人角度来说,其实我当年就是从「小颗粒度」的一线工程师做起的。当时阿里巴巴的评分体系,3.5 是中,3.25 是差,我刚转为架构师评分就是 3.25,差点就放弃了。后来慢慢地才熬过去,开始带小团队、大团队,培养自己的领导力,拓宽自己的专业面。 两年多前,公司让我负责国际业务,这个领域对我来说完全陌生。我又开始思考客户价值,思考公司与世界的关系,思考如何建设面向未来的全球化的支付体系,这些思考是完全从不同层面打开的。 正当我打算在这个方向完成转型时,公司突然说你回来做技术吧。再回来,看待技术的视角已经和过去不一样了。 张鹏:这个过程给你的世界观或者说对技术的理解,带来了什么核心的不一样? 程立:我能够看通技术与商业如何联合设计,能理解背后的全链条逻辑了。过去在前线做过这些事情很重要,不然只是纸上谈兵,切身做过后就知道技术和商业是一体的。现在我会在商业一侧看待技术的实际价值,且会从它的未来形态、好的商业模式角度去思考。但有时候也不能是纯商业角度,毕竟有些技术短期内看不到商业价值。这是两者结合对人和组织最大的考验。 张鹏:很好奇,阿里一方面是一家工程师聚集的科技公司,另一方面一直有自己很强也很独特的文化,比如「新六脉神剑」这种高度概括的东西。很多技术人喜欢实、甚过虚,这两种文化在阿里能兼容吗? 程立:可以很兼容,不冲突。我原来就是一个典型的码农,六脉神剑、六大价值观,对我来说,都是非常简单的道理,比如要敬业、要团队合作、要客户第一。当你慢慢做到更高层次,变成架构师的时候,会慢慢琢磨到这些价值观的深度,比如要权衡利弊、平衡团队关系、处理边界问题时,联想到「因为信任,所以简单」。越到高层面,价值观越起到关键作用。 当然我们为了让技术同学更容易理解,我们把公司的「新六脉」做了针对技术人的「适配解读」。比如有一条「此时此刻,非我莫属」,我们翻译成「Yes we can」,任何挑战、不可能,我们都能把它变成可能。再比如「唯一不变的是变化」,我们技术人员翻译成「Think Big,Go Deep」。翻译成技术人的语言后,价值观就有了技术人的特质。 程立在阿里巴巴工作园区 张鹏:我很好奇阿里巴巴技术和业务创新的机制到底是什么?会是赛马机制吗? 程立:阿里巴巴内部创新非常多,每个成功创新的背后还有很多不成功的创新,大家看到的可能都是成功的创新。我认为成功的创新背后不在于技术是否最先进,而是能否解决客户和行业真正的痛点。 创新机制上竞、合两种机制都有。阿里巴巴内部有两种力量,一种是从顶层统一愿景驱动的创新,可以看到是一条非常清晰的创新线,我们有很强的组织能力,可以将大目标逐层分解到每个团队,去合力完成创新。第二种是阿里巴巴内生的创新,由每个人能动性地触发,最后生长出来,比如聚划算、Oceanbase 数据库。至于如何选择竞或合,阿里巴巴内部会针对不同业务和业务的不同阶段,来匹配不同机制。 张鹏:世界级的科技公司都会有自己的技术阵地和技术高地。那么阿里巴巴是如何定义自己的技术阵地和技术高地的? 程立:阿里巴巴在早些年就把技术价值观与技术愿景非常清晰、明确地定义成:技术创造新商业。我们不想复刻任何一家公司,「技术创造新商业」是我们非常核心的一点。这些年围绕这一核心技术价值观,我们确定了前台、中台、底层技术架构,同时,商业倒逼技术与技术驱动商业创新的两条动线都转动起来了。阿里体系的完整性,以及它的动态性、多元性,让商业创新变得更丰富、更高效,这是阿里技术阵地独特的地方。 阿里巴巴未来的技术布局有两个方向,一个是数字原生时代的商业创新,其中有大量技术与商业联合创新的场景。我不认为它完全是商业,而是技术与商业的结合。 第二是围绕整个数字时代最核心的基础算力、算法,数据技术展开布局,这也是我们要重点投入的。 张鹏:作为阿里的 CTO,你怎样理解技术的终极目标? 程立:在阿里,技术就是商业的一部分,或者商业就是技术的一部分,两者是一体的。所以,于我而言,技术的终极目标就是实现阿里巴巴数字经济体的愿景,构建一个由数字技术支撑的经济体。这个庞大的系统工程,未来将成为全社会数字商业的一部分,这件事让我很兴奋。 在这个过程中,技术人肯定也有自己的好奇心,我也会关注下一代计算架构是怎样的,比如量子计算能不能突破?通用 AI 能不能实现?这些问题,可能在下一个十年会有答案,我对此充满好奇。或许,满足人类好奇心,去探索和突破,也是技术的意义吧。 更多精彩戳我前往2020阿里巴巴双11技术专题
11月11日0点刚过26秒,天猫双11的订单创建峰值就达到58.3万笔/秒,阿里云又一次扛住全球最大规模流量洪峰!58.3万笔/秒,这一数字是2009年第一次天猫双11的1457倍。 数字的背后,隐藏着阿里巴巴很多不为人知的技术创新和突破,乃至世界顶尖技术。今天,阿里妹就来和大家分享阿里巴巴天猫双11背后的黑科技! 天猫双11十二年,阿里商业操作系统迈向数字原生时代 全社会参与的2020双11,已成为全球超大规模数字创新工程,支撑2020双11的,是全新的阿里巴巴数字原生商业操作系统。 数字原生,包含云原生、AI原生、区块链原生、IoT原生、5G原生等新技术。数字原生商业操作系统,夯实了以云计算为代表的基础设施层,打通了业务、数据、智能、协同在内的数字创新中台层,进而实现上层全链路商业要素的全面在线化与数字化。数字原生商业操作系统,以工具化和产品化的能力加速商业要素流通,实现了标准化、模块化组装的技术搭建方式,不仅能极大提升效率,还将为社会打开更多创新空间,让更多商业新物种涌现。 2020天猫双11业务的核心系统完成了从“上云”到“云上”的架构升级 全面实现云原生上云,并通过大规模使用包括容器服务ACK、数据库PolarDB、Redis、消息RocketMQ、微服务 EDAS、监控ARMS等在内的云原生产品,获得成本、稳定性和研发运维效率提升的红利。与此同时,双11大促的业务场景也成为阿里云云原生技术与产品优势的锤炼场,为阿里云客户创造更大价值。数据库核心全面云原生化,资源利用率提高60%。 阿里巴巴发布2020天猫双11十大前沿技术 2020双11是史上最具科技含量的一届双11。大规模运用于2020双11的十大前沿技术中,包括正式上岗的物流机器人小蛮驴、支持214种语言的直播实时翻译、一年省电7000万度的液冷数据中心、全面云原生化的阿里核心系统…既有基于数字技术的原生商业创新,也有引领时代的基础技术突破。 十大前沿技术中,商业AI深入生产制造、购物消费、物流配送各个环节,让今年双11走向智能化。在生产制造环节,“工业视觉AI”已经在多家纺织服装工厂上线,可自动完成原料、坯布、成品布、成衣全生产环节的质检工作,小到头发丝直径10分之一大小的瑕疵都能精准识别,识别准确率90%以上,远超人类水平,效率大幅提升5倍,推动双11生产制造提速。 直播间里的每一秒都是技术人的骄傲 平台必须在1秒内将主播声音、画面和商品信息同步给分布范围极广的百万级消费者,确保后者获得一致的、实时的、高水平的音视频体验,以及商品交易(尤其是秒杀)的可信度。与此同时,同样的问题也在进入直播间、完成购买交易、抢红包/点赞为代表的大规模实时高并行度的内容电商互动中存在。 为了进一步优化消费者体验,降低延时,阿里巴巴非常重视在音视频技术领域的投入,涉及领域包括音视频通话、低延迟直播、S265编解码器、实时智能调度、自学习参与体系等多个方面,特别是在2020双11 将GRTN 新一代多媒体传输网络“首次大规模应用于直播电商带货”。将直播延时由过去的3-5秒降低到1秒以内,极低的互动延时带来了直播内容和互动体验上质的提升,在消费者和主播之间形成了更好的互动效果。 AI虚拟主播现身淘宝直播间,上岗天猫双11 双11前夕,阿里AI虚拟主播现身淘宝直播间,替代真人主播“上岗”双11。该虚拟主播形神兼备,声音、情绪、动作逼近真人,不仅能听会说,与千万观众对答如流,还会跳舞、Rap,完成各种复杂动作。 认知智能引擎助力消费者更好地选选选,新货好货成爆款! 今年双11前夕,淘宝首页大幅改版。在信息流、搜索、聚划算、会场和直播等用户场景,智能计算调用量日均已高达数千亿次,消费者兴趣宽度显著拓展,各类商家机会趋于多样,新商品孵化周期正在缩短。 此外,各类基础智能科技已在淘宝大规模应用,日调用量也在数千亿次。在视觉AI领域,拍立淘目前支持数亿商品对应的图片和视频检索;自然语言学习(NLP)、实时机器翻译、语义识别等技术,也在店小蜜、实时翻译、商品评价分析等关键链路使用。 在信息搜索、推荐和营销领域,传统机器学习机制会不断拟合用户行为。认知智能则能提供更好的发现性、丰富性。智能技术像水一样在阿里经济体流淌,认知智能引擎助力消费者更好地选选选,新货好货成爆款! 手机淘宝无障碍再升级,听障用户体验直播实时字幕 直播已成为热搜的新宠儿,为了让听障用户平等地使用这种购物社交方式,手淘信息无障碍团队联合淘系技术直播团队和达摩院语音实验室,基于ASR技术,实时将主播语音转换成实时字幕显示在手机上。让听障朋友第一次“听”到了李佳琦的“OMG!买它!买它!”。 业务平台:史上最多业务通过中台参与天猫双11,助力大促如丝般顺滑 为保障双峰之下的交易链路平稳顺滑,高效稳定,阿里巴巴业务平台通过全链路系统优化和保障预案,持续提升任务异步调度性能,降低资源成本。在助力淘宝天猫主站实现红包合并、直播秒杀、淘宝零钱等大促新玩法的基础上,还顺利支持第一次通过中台参与双11的本地生活、银泰、考拉等业务实现经济体大促业务共振,提升消费者整体购物体验。 同时,基于产品化的思路,今年业务平台还将大促能力数字化、自动化、和智能化的开放给更多业务部门,使得各前台业务团队可以精准核算大促容量需求,许多部门只需一个人,就能够张罗起大促数据预热等关键大促工作,大幅降低了人员和时间的投入。 全新动能:新一代数据计算处理框架——“流批一体” 今年双11,阿里数据中台将通过最新的数据技术和更深刻的数据理解为业务创新提供动能。其中,新一代数据计算处理框架——“流批一体”将在内部小二端大规模应用。 过去双11期间,阿里小二们想要进行诸如“跟往年相比哪些商品和渠道的表现更好?”等多维度、大批量的深度分析,这些看似简单的运营分析,背后涉及到的是大量数据计算和多维交叉分析,这些都需要大量的时间成本来实现。新一代的数据技术处理框架——“流批一体”,实现了“全链路、全维度、全实时”的数据处理分析,可以帮助阿里小二们及时发现问题、分析问题,实现实时运营决策。 “流批一体”在技术上,实现了哪怕是多个计算处理模式,也只需要撰写一套代码就能兼容。在计算速度上比其他框架快1倍、查询快4倍,给小二们搭建数据报表提升了4-10倍的速度。同时,由于“一体化”的特性,能实现实时与离线数据的完全一致。 在今年史上数据量最大的情况下,新一代数据计算处理框架——“流批一体”直接节省了一半的资源成本,真正实现了“快、准、省”。 MaxCompute以国内最大规模的计算能力,为阿里经济体天猫双11保驾护航 MaxCompute支持双11场景下的海量数据高并发计算,预计2020计算同比增长超50%。其中,单日数据计算量将在2017年300PB、2018年600PB、2019年900PB的持续增长之后,在今年双11突破EB级。单日计算任务将在2017年300万、2018年500万、2019年900万之后,超过千万。 云原生实时数仓首次在天猫双11核心数据场景落地 实时计算Flink+ MaxCompute交互式分析(Hologres)实时数仓实现商业全链路实时化,毫秒级海量数据处理能力,为商家和消费者带来了更加智能的消费体验。为应对双11大促流量洪峰提供了强有力的保障,支持的业务包括双11实时直播间、智能推荐、阿里妈妈数据平台、国际站数据平台、菜鸟数据平台、友盟+全域数据分析、CCO智能客服、新零售数据平台、考拉、饿了么等业务。 前端智能化设计稿生成代码,助力十余BU智能研发, 自动生成90.4%双十一会场新模块 前端智能化助力前端研发模式升级,设计稿生成代码技术体系升级(如对UI多态、直播视频组件、循环的智能识别增强等),多个部门共建前端设计稿识别算法模型和数据集,双11会场大规模应用,营销模块研发链路产品化升级,双11会场有90.4% (高于去年)新模块的代码是自动生成的;尽可能去除设计稿约束,增加约束智能检查等升级,无人工辅助的情况智能生成的代码被保留发布上线的代码占比79.26%;相比传统模块开发模式,固定人力单位时间模块需求吞吐量增加约1.5倍,使用设计稿生成代码技术后编码效率(模块代码复杂度和研发耗时的比值)提升68%。 AlibabaWood助力天猫双11 AlibabaWood是一款高效智能制作商品短视频的工具,它具备便捷、灵活、智能3个核心特点。AlibabaWood可帮助用户1分钟即可生成1个商品短视频,将极大缩短用户的处理时间,提供用户的工作效率。 该工具由阿里巴巴达摩院人机自然交互实验室、机器智能技术人工智能中心DesignAI、阿里云智能业务联手打造,融合电商视频设计与人工智能,能够对商品内容进行智能理解,然后自动为商品编写剧本,添加镜头,书写文案,并搭配风格匹配的音乐,自动剪辑出具备故事性的电商短视频。 智能客服会打“视频电话”了,提供多模态交互服务 热线小蜜2020双11的外呼目标是当天外呼规模1000w+,期间日均300w+。呼入目标是全月满意度平均60%,日均转人工量5.4w。 热线小蜜今年双11上线了多模态交互服务能力,能够接听用户的视频电话了。针对大促满意度低的活动介绍、购买指南等场景,热线小蜜试点音画同步多模态交互服务。这项功能将对部分用户开放,用户在手淘等应用上选择多模态呼叫,就能跟机器人建立视频聊天,视频界面主要用来展示与聊天话题相关的内容,比如当用户问询大促活动规则、红包使用方法等等,小蜜可以对照画面,手把手教用户找到对应的接口,告知使用规则,大大缓解过去语音电话效率低的问题。 小蜜的多模态升级是基于达摩院的语音主动对话平台SPD(spoken positive dialogue)平台打造,借助达摩院的多模态对话交互能力实现。能够拨打视频电话的智能客服,未来应用空间广泛,包括在线电商导购、私人生活助理等等。 “有电的地方就有阿里巴巴内网” 今年双11,企业智能事业部的网络工程师们自研了一个5G盒子。在没有网络的情况下,只需要插上盒子,员工就能连接到无线网络,而且是内网环境,不需要部署宽带,也不用员工在进行连接内网的操作。一场活动可节省数万成本。此外,通过该盒子,还可以直接进行无线投屏等操作,进一步提升办公体验与效率。目前这一设备已运用在天猫直播盛典等双11大型活动现场。 天猫双11网络保障:首次运用空中“光缆” 在阿里西溪园区首次使用目前业界最先进的无线电超高频传输技术进行网络灾备,在各作战室的上空拉起了一条条“无线光缆”,再也不怕路面线路被挖断或者老鼠咬断,确保小二们的网络稳定传输。 数字供应链多层级跨境仓发网络构建 双十一前,数字供应链事业部为Lazada搭建了多层次跨境仓发供应链解决方案,商家可以按品类特性灵活选择(国内中心仓、区域自贸仓eWTP、本地仓FBL)备货,覆盖东南亚六国消费群体,系统自动根据消费者地址及库存水位选择合适的仓库自动履约发货,集成菜鸟和Lazada头程、海关、干线、落地配能力,帮助商家解决跨境仓储物流挑战,提升消费者服务时效水平。 国内首家企业联合海关完成全链路压测,重点保障进口保税高峰值通关稳定性 本次双11,菜鸟做为国内首家企业联合海关实现全链路压测方案的落地,通过模拟峰值,海关内部系统完成10+升级优化。11月1日上午6时33分,海关全链路整体稳定,消费者当天购买的双11进口订单,已经有1000W单顺利清关,这一速度比2019年双11当天大幅提前了2小时14分,提速25%,这些包裹已经陆续进入配送环节,最快可以当日送达。 从中国数字化到全球数字化,菜鸟为东南亚11国装上“物流大脑”迎战天猫双11 今年双11,菜鸟的数字化物流技术首次走向全球。除中国外,菜鸟还在印尼、马来西亚、新加坡、越南、泰国、菲律宾、巴基斯坦、尼泊尔、缅甸、孟加拉国、斯里兰卡等11个国家,建设统一的物流技术中台,为东南亚物流装上了“眼睛”和“大脑”。 全国150城装配了菜鸟创新IoT装备,为行业赋予了降本增效的新生产力 本次双11,菜鸟IoT迎来了第一次大规模助力行业数字化的升级。在仓储、快递、国际、零售通等物流场景,配置了数万台LEMO系列产品,预计期间将支持亿级包裹的生产操作。在驿站场景,无人取件机、小盒、自提柜等产品覆盖150城,为消费者取件节省15万小时。同时,上万台的裹裹寄件机,将为消费者提供全新的自助寄件体验。 小蛮驴上岗了!阿里物流机器人将承包浙大菜鸟驿站天猫双11包裹配送 出生不到两个月,阿里巴巴的物流机器人小蛮驴就上岗营业了。10月30日,由小蛮驴领衔的22个物流机器人进入浙江大学紫金港校区。今年双11,阿里将在浙大打造全球首个纯机器人送货点位,由机器人承担浙大菜鸟驿站3万多件包裹的送货上门服务。
AI 集群旨在获得更高的 AI 集成算力,扮演着“承上启下“的角色,“承上”是指向 AI 应用提供的有效集成算力,高集成算力是支撑 AI 大模型和海量数据量的使能能力。而“启下”是指通过集群的计算、网络、存储平衡设计来充分发挥 AI 计算芯片能力,例如访存或网络瓶颈都会导致较低的 AI 芯片效率。AI 集群设计的关键在于:1)单机:计算优化,软硬件协同优化发挥 AI 加速器有效算力。2)多机:通信优化,最小化多机数据交换引入的计算效率损耗。我们将从单机加速和集群加速(多机)两个方面介绍相关技术和系统。 首先,我们将从计算和互联两个技术点对阿里现有的技术栈进行全面的阐述。 计算技术 神龙服务器和神龙虚拟化技术 在阿里云神龙硬件平台下,虚拟化架构也做了相应的升级,使计算虚拟化部分的架构更加清晰简洁,让虚拟机能提供接近物理机的性能。如图所示,神龙服务器架构的主要特点是:I/O 链路从传统的软件实现转变为硬件和直通设备来实现,存储虚拟化、网络虚拟化都在 MOC 卡上来实现;同时将管控系统、监控程序等都下沉到 MOC 卡上。在提供计算服务的物理机上,只运行阿里云自己裁剪的 Linux 操作系统和轻量化的虚拟机监控器。总的来说,神龙硬件平台的底座,加上轻量化的宿主机 OS,再加上轻量化的虚拟机监控器,就组成了神龙架构下的轻薄且高效的新一代虚拟化平台。 GPU CPU 执行 AI 计算往往并不能达到最优的性价比,因此,具有海量并行计算能力、能够加速 AI 计算的 AI 芯片应运而生。当前最具代表的是 GPU、FPGA 和 AI ASIC 芯片。GPU 仍然是当前最成熟也是最广泛使用的加速器,阿里巴巴上层框架针对 GPU 做了大量的编译优化工作。GPU 在阿里巴巴得到了广泛的部署,也是云上 AI 算力售卖的主力,我们已经能做到基于 GPU 的云产品与最新一代 GPU 同步发布。在云上 GPU 的安全性,可运维性,用户体验上我们都走在业界的前列。在于通用计算可运维性在 GPU 虚拟化场景下的热升级能力,居于业界第一;是业界首个发布基于 SRIOV 的 GPU 热迁移技术预研的云厂商。在业界首个实现基于 GRID 的 vGPU 技术在云上输出,引导了 vGPU 云化的技术趋势,并且为 5G 时代的云游戏铺垫了 GPU 计算基础设施。 GPU 的训练芯片一直引领着 GPU 技术发展的趋势,除了基础 FP32 算力的高速增长之外,通过精度的变化大幅度提高算力,比如 Tensorcore 是另外一个算力提升趋势;另外,由于多卡,多机的通信的需求,GPU 的通信经历了 PCIE P2P 技术,基于 NVLink 的高速通信技术,以及通过 RDMA 网络的 GPUDirect RDMA 技术。而在阿里云上,由于多租户之间需要进行算力共享,在不同的通信模式下,如何进行算力分割和通信的隔离,是一个阿里云一直研究的技术,包括最新的基于 NVSwitch 的 NVLink 全连接场景下的可编程拓扑分割技术等。 FPGA FPGA 器件自诞生之初,就以高度灵活的可编程性提供类 ASIC 的性能和能效比而广泛应用于有线和无线通信、航空航天、医疗电子和汽车电子等领域。但是,相比 CPU 和 GPU,FPGA 的开发周期较长(尽管只有 ASIC 开发周期的一半乃至三分之一)、开发和使用门槛较高,使得 FPGA 的开发人员远远少于 CPU 和 GPU 的开发人员,同时应用范围和知名度也受到了很大的限制。在 FPGA 上,我们具备了有更高的定制和自研能力,阿里云与 AIS 联合研发的业界第一款单卡双芯片的 Xilinx FPGA 板卡,在板卡和 HDK 层面实现了技术自主创新的能力。 舜天平台:FPGA 即服务 (FaaS) 云上的 FPGA 实例做了丰富的功能输出,阿里云 FaaS(FPGA as a Service)舜天平台在云端提供统一硬件平台与中间件,可大大降低加速器的开发与部署成本。第三方 ISV 加速器 IP 可以迅速形成服务提供给用户,消除加速技术与最终用户的硬件壁垒。用户则能够在无需了解底层硬件的情况下,直接按需使用加速服务。为了给加速器提供方和使用方提供更加高效、统一的开发及部署平台,FaaS 舜天平台提供两大开发套件:HDK 和 SDK。FaaS 的逻辑架构图如下图所示: FaaS FPGA 逻辑架构图 阿里云 FaaS 舜天平台支持最全面的 DMA 技术,包括:DMA、XDMA 和 QDMA;同一架构支持 RTL 和 HLS 开发、验证与测试;全球唯一的同一软件架构同时支持两大 FPGA 厂商 Xilinx 和 Intel 的云厂商。全面、过硬、兼容性好,并且能够利用 PR 技术进行动态热升级的Shell技术使得 FaaS 舜天平台成为阿里集团 FPGA 异构加速业务的基础设施,完全适配了集团的所有已经引入的 FPGA 器件,已经成功服务手淘、优酷、蚂蚁和云安全几大业务板块。 阿里云 FaaS 平台架构图 AliDNN 与 GPU 环境下,单向的软件适配硬件不同,FPGA 和阿里自研 NPU 给了我们定义硬件的机会,可以根据业务特征进行深度的软硬件优化。AliDNN 是一款基于 FPGA 的指令集、加速器、SDK 和编译器全栈自研的深度学习加速引擎。指令集加编译器设计为 AliDNN 提供了充分的灵活性。深度学习框架 (TensorFlow,Caffe 等)可以直接调用 AliDNN 引擎,编译器(震旦)将深度学习模型编译成加速器指令进行计算。算法, runtime,编译器和加速器的全栈软硬件协调优化,使得 AliDNN 拥有极致的效率和性能。AliDNN 提供了高吞吐、低延迟的深度学习推理服务。 NPU AliNPU(含光 800)更是分析阿里集团内部的人工智能应用场景需求,确定了以 CNN 模型为主做了深度的优化,同时支持一些通用模型,比如 RNN 类模型等。这是针对特定深度学习算法领域做特别的优化,把相关应用的性价比提高到极致,正式如此,含光 800 性价比远超竞品,成为全球最强 AI 推理芯片。 互联技术 RDMA RDMA 是目前业界最受欢迎的高性能网络技术,能大大节约数据传输时间及 CPU 开销,被公认为是提升人工智能、科学计算、分布式存储性能的关键技术。阿里巴巴基于全新的 HAIL 网络架构并结合自研交换机,打造了从主机网络、消息中间件、盘古分布式存储、网络运营到产品运营的完整技术体系,实现了数十个数据中心的全球最大规模 RDMA 网络部署,远超亚马逊、微软等主要云厂商。这张全球最大规模的数据中心“高速网”使得集群极大地突破了传输速度瓶颈,有效地支撑了云盘 ESSD、云超算 SCC、机器学习 PAI、 云原生数据库 POLARDB 这些广受欢迎的创新产品,并助力电商数据库从容应对双十一峰值流量考验。同时,可以跨 POD 的 lossy RDMA 技术已经在阿里巴巴进入实验测试阶段,届时将进一步扩大 RDMA 的适应范围。 EXSPARCL通信库 自研的EXSPARCL(Extremely Scalable and high Performance Alibaba gRoup Communication Library)集合通信库提供通用的集合通信功能,同时兼容 NVIDIA 的 NCCL。ExSparcl 专门优化支持大规模 AI 集群的高速互联架构和多网卡特性, 充分利用设备之间的互联带宽,保证通信和业务性能的线性扩展。通过对集群/主机物理互联的拓扑感知,和网络路由的最优选择,实现创新的无拥塞算法,确保节点内和节点间的高速通信。例如针对 SCC 训练集群架构,实现的Rank重映射 Havling-Doubling 算法,可以保障集合通信过程中没有因为路径冲突而产生的拥塞排队,在大规模环境中对比 NVDIA 公司的 NCCL 通信库,实现集合通信性能 (AllReduce/AllGather) 的数倍提升,对业务性能的提升也非常明显。此外,拓扑感知的特性可以用于故障规避,大大增强网络的可用性,详见:https://www.qbitai.com/2020/03/11987.html。 飞天AI加速工具(AIACC) 飞天 AI 加速工具通过统一的框架同时支持了 Tensorflow,PyTorch,MXNET,Caffe 这 4 种主流的 AI 计算框架的分布式性能加速,并且针对 VPC 网络和 RDMA 网络都做了很深入的性能优化,在不同场景不同训练规模下可以提升 1~10 倍的训练性能。同时, AIACC 和各 AI 计算框架是解耦的,一方面可以轻松支持各 AI 计算框架社区版本的向前迭代,另一方面,用户使用各 AI 计算框架实现的模型、算法代码基本上不用修改,就可以很轻松的获得性能加速。 FaaS 舜天可以支持 FPGA 芯片高速互联 (最高可达 600G) 阿里云与 AIS 联合研发的业界第一款单卡双芯片的 FPGA 板卡,AliFPGAx2,两块 FPGA 之间的 Serdes 的带宽可以高达 600G,在一台服务器上的两块 AliFPGAx2 的板卡,还可以通过光缆互联与高速 Serdes 互联。于此同时 FaaS F3 提供给用户强大的互联拓扑结构,方便用户搭建 FPGA 集群,实现 FPGA 互联,并且通过 FaaS 舜天的 Shell 提供高速 DMA 的软硬件支持。可以支持单卡 2 芯片互联,双卡 4 芯片互联和 8 卡 16 芯片互联,并且互联的通道可以通过软件灵活配置硬件的隔离分割,在云用户不同的拓扑需求之间进行互联和隔离。下图是典型的 2 卡 4 芯片互联拓扑结构。 在此基础上,阿里巴巴开发了适合不同应用场景的异构超算集群。 SCC:通用和异构超算集群 高性能计算是采用低延迟高带宽的计算节点构成并行计算集群,通过并行计算(Parallel computing)实现对浮点密集型科学和工程模型包括AI深度模型的求解。作为计算节点的神龙裸金属服务器通过 RoCE 网卡实现节点间计算的高速 MPI 通信,通过神龙 MOC 卡实现和 VPC,IO 和网络,云盘的互联。从而在输出超算水平的超级算力的同时,保持所有节点的“云原生”的弹性和统一运维。我们还开发了弹性高性能计算 E-HPC PaaS 平台,作为高性能计算软件栈创建和管理 SCC 集群,同时对有能力自建 HPC 平台的客户直接输出 SCC 集群。 SCC-训练集群:大规模AI训练集群 针对 AI 大规模训练的算力需求,阿里巴巴开展了从硬件到算法的一体化设计。从性能角度,集群设计的核心之一就是通过提升加速器间数据交互的能力,降低非计算开销占比,进而实现算力的规模线性扩展。因此,异构集群系统以优化通信为突破口,对服务器和整机网络架构进行重新定义,先从硬件上解除通信瓶颈,再通过软硬件协同,将增强的通信能力发挥出来,该部分的工作内部代号 EFlops,实现了 AI 训练的线性加速,相关成果在顶级学术会议 HPCA2020 上发布(https://www.csdn.net/article/a/2020-03-03/15988517),目前建设的系统单集群 AI 算力可达 500 PFlops(FP16 性能)。 此外,液冷是推动AI集群架构演进的另一股力量。由于功耗限制,当前一个机柜仅能容纳两台或四台 8 卡服务器,计算密度难以提升,而液冷则可以打破这个限制,一个 Tank 即可容纳 100 多块 GPU 卡,省电省地省光纤,提升稳定性。相应地,集群架构根据浸没式液冷 Tank 的尺寸和出线特点进行了重新设计,这一工作已进入实验阶段。 再次,在获得极致性能的同时,我们也兼顾在计算成本方面的优化,以及通用与异构算力解耦之后的灵活性。 异构硬件层虚拟化 阿里 IaaS 的 GPU 虚拟化技术是 AI 计算力上云的基础。阿里 IaaS 在对现有开源 GPU 虚拟化技术之上进行了二次开发,使得现有 GPU 虚拟化方案可以适合公有云对安全性,高可靠性与可监控等关键功能的提升。 在公有云 GPU 服务器的安全隔离方面,阿里云异构 IaaS 层完成了从实例内部的驱动的初步安全过滤,宿主机虚拟化层对 GPU 特权指令的过滤拦截,宿主机 PCIe 协议成的容错处理等三层立体防护体系,确保客户实例不受攻击,也没有可能攻击其他客户。 当前主流异构虚拟化还是以设备直通的方式存在,并在基础之上逐渐演化出 SRIOV, vGPU 分片虚拟化等最新技术。阿里 IaaS 依托现有 GPU 虚拟化技术完成了 AI 集群 (GPU/FPGA) 的云上规模化和产品输出。GPU 虚拟化走过了漫长的发展路程;经历了直通虚拟化,SRIOV 与 vGPU 的分片虚拟化,Intel 的 GVT-G 技术等等。 在通用计算时代,虚拟化的引入主要是为了提高 CPU 利用率,但是随着热升级,热迁移技术的引入,把计算资源的安全性,可靠性推到新的高度。在 GPU 资源利用率提升,计算资源碎片整理上,GPU 虚拟化技术也扮演着重要的角色。 在公有云 GPU 服务器的监控方面,阿里云异构 IaaS 层做了 GPU 相关的云监控方案,可以实时取得当前 GPU 的运行状态与温度,显存用量等信息,并可以自定义多种监控模式。GPU 自定义监控与 GPU 云监控插件。 在公有云 GPU 服务器的高可用性方面,阿里云异构 IaaS 层开发并部署了特有的 GPU 服务器热升级与热迁移功能。使得当实例所在宿主机需要系统软件更新/硬件维修等运维操作时,可以让客户无感的完成升级更新,从而保障了客户业务的稳定性与连续性。基于 SRIOV 和 GRID vGPU 的热迁移能力上,阿里云都作为业界第一梯队领先竞对。 异构 GPU 容器支持(cGPU 容器 AI 算力隔离技术) 云原生已经成为业内云服务的一个趋势,如何能够在云原生上支持异构计算,同时在单个 GPU 上可以运行多个容器并进行隔离,对此业界也做了很多探索。Nvidia vGPU, Nvidia MPS, 友商的 vCUDA 方案,都为用户更小颗粒度的使用 GPU 提供了可能。阿里云 GPU 团队推出了昊天 cGPU 方案,相比其他方案,这是一个颠覆性的创新。业内常用方案是通过替换 CUDA 库实现拦截,需要对静态链接的程序重新编译,同时 CUDA 升级时也需要适配新版本;而昊天 cGPU 在做到算力调度与显存隔离的同时,也做到了无需替换 CUDA 静态库,动态库,无需重新编译,CUDA,cuDNN 等版本随时升级无需适配。 cGPU 为自主研发的宿主机内核驱动。它的好处在于: 适配开源标准的 Kubernetes 和 NVidia Docker 方案用户侧透明。AI 应用无需重编译,执行无需 cuda 库替换针对 Nvidia 设备的底层操作更加稳定和收敛,而 CUDA 层的 API 变化多端,同时一些 Cudnn 非开放的 API 也不容易捕获。同时支持 GPU 的显存和算力隔离 现在基于阿里云 GPU 团队的 cGPU 昊天方案和容器服务的 GPU 共享调度双剑合璧, 可以打造低成本,可靠,用户友好的规模化 GPU 调度和隔离方案。 软件池化 EAIS.EI:资源调度层池化 EAIS 通过软件池化的方式将 CPU 核心数和后端异构加速设备解耦,前端的纯 CPU ECS可以动态挂载或者卸载后端异构加速设备,前端 ECS 和后端异构加速设备间通过加密的 gRPC 协议通信。后端加速设备可以包括 GPU,FPGA,NPU 等异构加速器,并且通过软件池化的方式进行统一调度和管理。 HARP:Runtime 层池化 做加速机群池化的主要目的,是想提高机群资源利用率。机群管理如 K8s 对加速资源管理手段非常有限,一般都是以直通(pass-through)的方式,以整卡的力度来分配。再者 CPU 和加速资源以整机形式,绑定资源分配,在实际应用中有很大资源浪费。而数据中心呈现去中心化发展趋势(disaggregation),从物理机到虚拟机,到虚拟网卡,到分布式存储,整机的各个部件逐渐以专用机柜的形式,以虚拟化,可配置(configurable)的方式通过网络组装。对比磁盘,加速资源跟通用计算之间联系更紧密,但是随着技术的进步,加速资源单独组机柜,通过网络或者其他互联(interconnect)的形式,对通用计算(CPU)加速,成为一个技术趋势。 Heterogenous accelerator resource pooling (HARP),目前以 GPU 为主要加速资源,将来会扩展到 NPU 和其他加速硬件。通过在用户程序和 driver 之间,加一个中间层(目前为软件,将来可扩展为硬件),来实现加速资源的虚拟化,动态地为用户分配本地或者远程的加速资源,从而更好的管理和利用加速资源。 HARP 实现方式的优势为: 对上层应用透明,对运行环境无要求(物理机、容器、VM)均可 同时支持本地和远程的加速资源,本地模式下与直接使用 GPU 性能无差异 可以通过控制 API 来比较轻量化地在单卡复用情形下显存、计算资源控制 可以对上层应用隐藏底层硬件细节,自动化一些底层配置(本地物理 GPU/由 PCIE-switch 连接的另一台 host 上的物理机/vGPU/下一代 GPU 支持/Compute Instance) 可以实现一些额外的 profiling 功能,甚至生成一些可以 replay 的 trace HARP 资源池需要支持其他各种加速芯片,因此希望能建立统一的接口。支持该接口的芯片只需要花少量的工作,就能接入资源池调度系统。因此我们联合上海交大和清华大学,以及寒武纪等芯片厂商,建立中国异构资源池技术标准产业联盟。 硬件层池化 作为软件池化技术的升级,利用自研或者三方的硬件插卡,通过机架或小规模跨机架的高速总线互联技术,对通用计算器与多种加速器进行配比解耦,达到中等规模加速器池化和任意的加速器的灵活组合。同时在可靠性,可运维性,加速卡硬件故障处理 SLA 上提供更好的服务。 远程资源 - 本地访问 最后,基于在“核高基”领域的技术研发,奠定了今天阿里云的 IaaS 及 PaaS 服务具备了以下特点: 以产品多样性,打造性价比极致的算力供给的硬件算力基础设施为设计目标。 以软硬协同为主的技术研发和布局形成我们的差异化竞争力和技术抓手。 从单加速器分片算力共享到大规模集群算力的弹性算力能力的交付能力是一个核心需要打造的能力。 SLA 服务能力具备差异性,高可靠性。
引言 俗话说有多少米,就下多大锅。在特征体系构建上,我们已经准备了很多米了,并且在线性模型 FTRL 上拿到了一些甜头。下一阶段我们换了锅,对模型进行了升级,从线性模型转为 end-to-end 的深度模型,并进行了多个版本的迭代,包括 pure deep 模型(Pure Adaptive L2 Model,PALM),引入实时点击和实时未点击行为(FeedBack-PALM,FB-PALM),引入全网序列特征(Global Local Attention-PALM,GLA),并拿到了一些收益。下面对这个阶段的技术细节进行介绍。 问题分析 固定的特征体系下精巧的模型结构也能显著发挥现有特征的潜力。end-to-end 的深度模型一方面可以隐式和显示的进行特征交叉增加特征的表达能力。另一方面可以很 flexible 的引入真实行为序列特征等复杂结构特征。但是任何事情都有两面性,end-to-end 的深度模型相较于 FTRL 虽然对特征组合要求较少,但是对特征的筛选要求精细,数值型特征以及 id 类特征的选择和处理方法都会对最后的模型效果起决定性的作用。另外模型泛化性,训练过拟合问题以及模型复杂度和线上 rt 的关系也是需要关注的问题。经过多个版本的迭代,包括对现有的多种点击率深度积木模型的复现,引入用户实时点击和未点击 set 特征,引入用户全网序列特征,引入宝贝一阶近邻和预训练向量,加入 time-aware attention 等。下面分阶段进行介绍,包括面对的问题,模型结构,模型离线/在线效果,以及一些思考。 模型迭代 PALM (Pure Adaptive L2 Model) 模型 问题 在新的特征体系下,我们在 FTRL 上拿到不错的结果,很自然的想法是把 FTRL 上的特征复用到 Wide and deep 模型上,然后增加隐式高阶交叉的 deep 侧,来在原来的基础上增加模型表达能力。但是经过几轮的调试,离线指标一直和 FTRL 相比微正向,这不符合对 deep 模型的期望。后来发现把 wide 侧上的特征慢慢迁移到 deep 侧,包括 id 特征,数值特征,命中特征等,离线评测指标涨幅较大,后面也沿用这个思路,采用 pure deep 模型,将所有的特征都迁移到 deep 侧,并拿到了一些收益。但是 pure deep 模型和 wide and deep 模型相比,非常容易过拟合,并且对数值特征以及命中特征的处理方式有一定的要求。其中对离线指标提升较明显的几个点如下: 1)宝贝 id,用户 id,trigger id 等高维 id 类特征需要谨慎加入。航旅这种低频场景,这种高维 id 特征分布一般长尾较严重,大部分 id 的训练数据非常少,噪声较多,模型容易过拟合。在尝试中,正则以及 dropout 等常用抑制过拟合的方式一直没有较好的效果。后续借鉴 DIN 中介绍的 adaptive l2 regularization 的方式,挑选了一批类似用户id这种高维稀疏 id 类特征做动态正则。对正则系数做适当调整后,模型训练正常,全量数据过 5 个 epoch 也不会出现过拟合现象。 2)命中特征(lookup 特征)在目前的 rtp fg 过程中如果没有命中的话,是不存在这个特征结果的,也没有默认值。反映到模型中,如果没有命中,那么 dense 类型输出为 0,id 类型经过 embedding 操作之后输出全零的向量。在神经网络这种对数据分布敏感的模型中,我们考虑了两种使用方式。第一种是采用 dense 类型,命中之后有大于零的值,没有命中的话为零,符合正常数据分布。第二种是 id 类型,命中之后输出一个 id,经过 embedding 操作之后输出均值为零的向量,没有命中的话输出全零向量,也是符合正常数据分布的。经过试验第二种优于第一种,但是第二种方式 embedding 的维度不适合选择太大,因为命中特征本身较稀疏,输出全零向量的可能性较多,会影响模型训练过程。 3)Warm-up+Adam+learning rate decay 的方式相较于其他优化方法对离线指标的提升较大,非常值得一试。 4)batch normalization 在模型训练中起了非常大的作用。由于 deep 侧包括大量的高阶 look up 特征和归一化之后的数值型特征,离散化之后的数值型 id 特征以及普通 id 类特征的 embedding 结果,输入到网络中的数值非常不规范。后面发现 batch normalization 能有效规范这种多种来源的特征组合输入,保证网络的正常收敛。经过尝试在 embedding 层之后加前置 BN,后续接 fully_connected + BN + relu 的这种形式在离线指标上表现最好。 模型结构 最终的网络结构如图: 图中的模型细节就不累述了。该模型的 loss function 和后续的迭代版本都是采用 pointwise 的形式。离线评测都采用同样时间区间的训练数据的 T + 1 评测 AUC。 模型效果分析 同样的时间窗口,采用 30 天数据训练,T + 1 评测,离线指标如下,提升还是很明显的。 效果 上线实验观察 4 天,相较于新特征的 FTRL,uctr 平均提升 3.2%,pctr 平均提升 3.2%。 FB-PALM (FeedBack-PALM) 模型 问题 前一个版本在 pure deep 模型上拿到不错的收益。很自然的想法是尝试各种点击率深度积木模型,比如 DCN,DeepFM,XDeepFM,PNN,Autoint,FGCNN 等,但是经过几版实现以及调优,发现这些 model 结构和上一个版本相比提升微弱,如下图。本来以为是模型复杂度高了,于是对于 FGCNN 多训练了 20k 步,发现模型处于收敛状态。猜想原因为目前 pure deep 模型中已经包含了大量高阶特征,特征不变的情况下,特征的隐式高阶组合已经足够了,显示的高阶组合带来的收益较少。 但是模型还是要继续迭代的,于是切换了思路,通过引入原始特征体系中没有的更多复杂结构特征来实现模型的性能提升。这一版主要添加了用户短期全网实时宝贝点击序列以及用户短期场景内宝贝曝光未点击序列特征,序列中的宝贝考虑 Id,目的地,类目, Poi,Tag 以及行为类型和行为次数等属性。模型结构上没有太多的创新,对于两组序列特征,以待推荐宝贝的 Id,目的地,类目,Poi,Tag 综合起来作为 query,对两组序列进行 attention pooling。然后将 pooling 的结果加入 Pure deep 的输入层,其他结构不变。其中对离线指标提升较明显的几个点如下: 1)对于序列特征中的宝贝并不是考虑越多的属性效果会更好,选择的属性需要覆盖度高,不然序列中太多的属性默认值会导致模型训练不佳。 2)这里对于两组序列,因为同样采用宝贝 id 特征,所以为了防止过拟合,也加入了动态正则的技巧。但是这里需要注意的是虽然待推荐宝贝 id,点击宝贝 id 以及曝光未点击宝贝 id 都是宝贝 id,但是由于来源不同,id 出现的频次也会不同,因此这三类 id 都采用自己各自出现的频次分布进行动态正则。另外这三类宝贝 id 也都采用不同的 embedding matrix,避免正则之间的影响。 模型结构 最终的网络结构如图: 下面对模型的核心部分进行详细描述: 1)对于宝贝维度,模型存在三类信息,包括点击宝贝行为,曝光未点击的宝贝行为以及待推荐宝贝。除了宝贝id以外,我们将行为类型,目的地,类目,tag,poi 等 side information 加入模型中。这里融合采用 concat 的方式。令待推荐宝贝的多个特征经过 embedding 之后得到的向量分别为:,其中 L 为特征的类型数,则可以得到待推荐宝贝的表达为: 类似的可以得到点击宝贝行为以及曝光未点击宝贝行为的表达为 和 ,其中 M 和 N 分别为点击宝贝行为和曝光未点击宝贝行为的个数。 2)通过待推荐宝贝作为 query,对点击宝贝行为以及曝光未点击宝贝行为进行 attention pooling。这里由于维度不一致的原因,采用复杂度较高的加性 attention。attention 的过程如下,其中函数 H 为多层前馈神经网络。这里以点击宝贝行为为例,曝光未点击宝贝行为类似。需要注意的是由于这两类序列信息意义相差较大,所以两个 attention pooling 操作参数是不 sharing 的。 3)最后将点击宝贝行为以及曝光未点击宝贝行为的 pooling 结果 和 与 pure deep 模型的输入层特征进行 concat 一起送入到多层前馈神经网络,输出最后的打分 logit,并进行相应 loss 计算。 模型效果分析 同样的时间窗口,采用 30 天数据训练,T + 1 评测,离线指标如下,提升还是很明显的。 效果 上线实验观察7天,包括三天正向桶以及四天反向桶,相较于 pure deep 模型,uctr 平均提升 1.0%,pctr 平均提升 1.5%。 GLA(Global Local Attention-PALM) 模型 问题 上一个版本主要添加了用户短期全网实时宝贝点击序列以及用户短期场景内宝贝曝光未点击序列特征,并拿到了不错的效果。但是分析特征组成以及模型结构之后,发现还存在一些不足: 1)宝贝行为序列只能覆盖一部分航旅用户,另外一部分航旅用户没有宝贝相关行为,只有机票、火车票、酒店等行业下的行为,但是目前模型中考虑的序列还没有 cover 这部分用户。 2)上一版模型对宝贝点击序列以及宝贝曝光未点击序列只是做了加性 attention pooling 的操作,只考虑了 query(待推荐宝贝)和序列中的每一个元素的相关性。我们还应该考虑序列中每一个元素之间的相关性。 针对上述不足,在这一版本迭代过程中: 1)加入用户全网行为序列,包括机票,火车票,酒店,宝贝等行业。需要注意的是在实践过程中,发现全网行为序列中的 id 属性维度会造成模型的过拟合现象,加入动态正则之后还是没能缓解。分析原因有可能是 id 属性在机票、火车票、酒店以及宝贝的混合序列中分布太杂乱,因此去除 id 属性,只考虑覆盖度高的目的地、类目、Poi、Tag 以及行为类型等属性。用户全网行为序列的 pooling 方式采用在意图模型中积累的 Multi-CNN + attention 的方式,具体细节可参考飞猪用户旅行意图。 2)对宝贝点击序列以及宝贝曝光未点击序列的 pooling 方式,我们采用 transformer + attention 的形式,在原来加性 attention 的上一层,我们先通过 transformer 对序列中个体之间的相关性通过 self-attention 进行描述,然后再通过 attention 进行 pooling。当然和上一个版本一样宝贝点击序列以及宝贝曝光未点击序列的 pooling network 的参数是不 sharing 的。 模型结构 最终的网络结构如图: 下面对模型的核心部分进行详细描述,一些通用性的模块的细节不详细介绍。 1)对于用户全网行为序列,我们只考虑行为类型,目的地,类目,tag,poi 等覆盖度高的粗粒度属性,不考虑 id 这一维属性,防止过拟合。融合的方式采用 concat 的方式。 2)用户全网行为序列的 pooling 方式采用 Multi-CNN + attention 的方式。通过 Multi-CNN 抽取不同区间范围内的局部特征(units),同时考虑 local 和 global 信息。区间越小,更加关注于局部,区间越大,更加关注于全局。比如区间为 1 时,考虑的是 point level。unit 中包含多种目的地类型,包括酒店,宝贝,火车票,机票等。通过 d 个形状为 d ∗ h 的 filters 对输入 进行步长为1的卷积,卷积方式为 SAME。经过 m 次类似的操作,每一次操作filter的形状为 ,最后输出为 m 个和输入形状一致的序列向量,。因为混合序列的形式,序列长度过长,如果采用RNN的形式一方面计算量大,另一方面长时依赖较弱。然后复用上一版模型中的加性 attention 对 Multi-CNN 抽取出来的多组序列进行 attention pooling,并将 pooling 的结果和 pure deep 模型的输入层 concat。需要注意的是这里多组序列的 attention pooling 参数是共享的。 对于点击宝贝行为,曝光未点击的宝贝行为的 pooling 过程,在 attention 之前添加一层 transformer 操作,通过 self-attention 来捕获序列内个体之间的关系,然后再进行加性 attention pooling。其他的操作不变。 模型效果分析 同样的时间窗口,采用 30 天数据训练,T + 1 评测,离线指标如下,提升还是很明显的。这里和上一版模型的离线结果图在 AUC 范围上有所不同是因为切换了时间窗口。 效果 上线实验观察,相较于 FB-PALM 模型,uctr 平均提升 1.0%,pctr 平均提升 3.0%。 其他的尝试 宝贝的一阶近邻信息和宝贝 pretrain embedding 前面后两版模型是在用户侧增加了复杂序列特征,而宝贝侧一直很薄弱。因此考虑增加宝贝的一阶近邻信息和宝贝的 pretrain embedding 向量。一阶近邻通过统计历史一年用户 session 行为中宝贝的共现关系来获得。而宝贝的 pretrain embedding 向量包括文本向量,图像向量以及基于随机游走产出的 deepwalk 向量。实验下来单独增加这些 trick 和 pure deep 模型相比能拿到很大的提升,但是叠加到后续的模型版本上效果不明显。应该需要再探索更加精细的融合方式,而不是直接都 concat 到输入层。两次实验结果如下,确实很微弱,因此没有上线验证。 time-aware attention 前几个版本的模型中 time 的信息都是通过 side information 的方式加入,类似于 pos embedding。但是 time 的信息在行为序列刻画过程中十分重要。调研了一些方法,包括 time-LSTM 等,但是由于 RNN 的计算复杂度太高,也放弃了。后续尝试将 time 的信息后置,直接加到 attention 的过程中,在离线表现上有一些收益,但是也不明显,暂时没有上线,后续这个也是值得探索的。 未来展望 目前飞猪首页猜你喜欢数据来源非常多,如何在不同数据来源的混合数据集下学习出在多场景下都很 solid 的模型是值得探索的。 目前几版模型在特征显示交叉的模型结构上没有拿到多少收益,后续需要探索这个问题,从特征体系,到模型结构,到数据来源上一起分析这个问题。
很少有一家公司,会像阿里一样要求技术人员也要懂业务的。来阿里之前,我呆过不少公司,通常的做法是业务提需求,技术干活,这个时候,技术就是个资源。在阿里,我花了很长时间才适应过来,相信很多同学也和当初的我有同样的困惑:已经有产品经理的角色了,大家各司其职不就好了,为什么我一个做技术的,要狗拿耗子呢?答案很简单:只有了解业务,才能从技术的角度想到业务方不曾想到的地方;只有了解到业务背后的 Why ,才能从全局的视角去规划技术的未来。 从 0 和 1 说起 我的花名叫林异,那我就从 0 和 1 说起吧。于公司而言,业务是 1,其它都是 0;于技术团队而言,技术是 1,其它都是 0。只要这家公司还要从事商业活动,业务就是第一的,业务不行大家就得散伙,所以要业务先赢。技术团队核心要做的是支撑业务、促进业务、引领业务,层层递进。对于大多数技术团队来说,都是以支撑业务为主,少量的促进业务,而引领业务则是少之又少了。这样听起来是否大多数技术团队只需要认真做好技术就可以了呢,其实并不是这样,如果公司这个大 1 不存在了,技术这个小 1 还有什么意义呢?退一万步讲,即使我们只考虑技术,但如果我们不了解业务,我们又如何去规划技术支撑当下及未来的业务呢? 以终为始 在做技术规划的时候,如果我们只看到当前存在的问题,就容易头痛医头脚痛医脚。我们需要结合未来几年业务上可能的变化以及技术的发展,来做出我们的技术规划。 毛主席教导我们: 不要只见树木,不见森林 ICBU 的前端掌门人耿霄常说: 要以终为始,看到我们的终局是什么 以 ICBU 无线前端的技术规划为例,我从业务趋势与技术趋势两个维度,来判断未来几年我们的技术上将发生什么变化。再结合当下的业务规划,来确定当前的技术重点。而当下的技术项目推进,又要为未来的技术规划埋下伏笔,这样在逐步的演进过程中,我们才能将点最终连成片,形成立体化的技术规划。 业务趋势 我认为,要看 ICBU 的业务趋势,就一定要看阿里巴巴是如何起来的。可以说,中国加入 WTO (主要是对美贸易) + 人口红利 + 信息不对称成就了阿里巴巴,C 类这一趴我们今天且按下不表,但就 B 类跨境电商而言,当前的内外部环境都已经发生很大的变化。 从外部环境看,世界格局走向两强对抗,修昔底德陷阱在所难免,贸易保护主义抬头,低端制造被迫向东南亚转移。从内部环境看,人口红利正在消失,中国是否能跳出中等收入陷阱仍然是个未知数。纵观世界历史,跳出中等收入陷阱主要有两个手段——对外输出或者技术革命,所以才会有“一带一路”和“中国制造 2025”。 由此可见,世界格局的变化 + 人口红利不再(成本上升)可能导致产业外移,原来我们依靠世界工厂地位而形成的垄断优势不再,所以我们就必须有更多的全球卖家加入进来。中美摩擦相当长一段时间内可能会成为常态,外贸企业势必寻找新的出路,例如向印度、东盟、非洲等国家和地区出口,所以我们需要由原来重点看美国买家转变为看全球买家。 过去国际站一直是一个黄页模式 (Souring),有人戏称之为一夜情模式,这几年国际站开始由 Souring 转向 Trading。传统模式下,买家通过 Alibaba.com 找到卖家,以后他们发生什么事情可能就和阿里巴巴无关了。这种模式有几个弊端,第一,我们的营收模式有限,主要靠卖家会员费和广告费;第二,我们无法沉淀交易数据,提供的用户价值太薄,用户粘性不足。 我们更应该看到,B 类贸易形式正在发生巨大的变化,传统的分销模式正在分崩离析,千禧一代开始主导世界,他们更愿意跳过中间商直接从源头进货,当资金流(例如用区块链提升国际结算的速度)和物流(例如菜鸟的 72 小时全球物流网)等基础设施得到改善后,这个趋势会更明显。另一方面,新生代更加强调个性,传统的大规模生产将会被柔性生产代替。未来我们的客户主要分为两个部分,一种是传统的大贸,这类主要是以采购工业品、原料、OEM、ODM 为主,另一种是以轻订制或现货组成的快速交易市场。前者交易链路太长,与线下链路耦合太紧,后者可能会逐渐形成在线交易的主要来源。 传统跨境 B 类贸易的用户画像是朝九晚五的办公族,用手机去找商找品洽谈完全无法想像。但如果 B 类贸易的用户结构发生了变化呢,当便利店杂货店健身房的老板们逐渐变多,他们甚至可能没有电脑,忙时偷闲掏出手机就能完成一单交易。 诚然,这几年我们的 App 一直在强势增长,但与 C 类业务不同是,在相当长一段时间内,我们恐怕无法 All in 无线直接以 App 为主。B 类的操作非常复杂,PC 还是重要战场;但对于快速交易市场的用户,App 无疑是最好的触达方式;M 站仍然是我们重要的流量来源,欧美一些国家的用户习惯使用 M 站这种轻量的方式,第三世界国家的用户由于网络和设备较差,也会愿意选择直接使用 M 站。 技术趋势 技术上也在发生巨大的变化,5G 技术会加速地区的不平衡,新技术的出现必定带来新的交互形式,移动设备上的 VR/AR 等能力再结合 5G,未来人们的沟通交流将会有质的改变,我们正在下一场技术革命临界点。5G 技术还可能会让部分国家或地区跳过 PC 直接进入到高速的无线时代,例如印度和非洲,而这些国家或地区可能会是未来 B 类业务新的增长引擎。全球化走到后面就是本地化,再加上新技术的出现,就要求我们要有强大差异化的能力,包括内容、运营甚至交易流程等。 端的发展也值得关注,苹果已经发布了 iPadOS,未来 PC 将会成为专业人士的工具,有没有可能未来有很多小店主,直接使用平板在阿里巴巴就完成了整个采购交易。在多端多屏的问题上,Flutter 的出现让 iOS 和 Android 可以使用同一套代码开发,Flutter For Web 已经出现,未来是否有可能直接使用 Flutter 就能进行开发?随着基础技术与 Web 的发展,Weex 未来会否是一个比较尴尬的位置,是要拥抱 Web 还是继续走 Weex? 小结 结合技术趋势及业务趋势,我认为“差异化”和“端技术融合”将可能会是我们未来发力的重点。但是否意味着我们现在就要全力 Focus 在这两件事情上呢?答案是否定的,我们还要结合当下的业务规划与问题,来判断最紧急最重要的事情是什么。但是,我们在解决当前问题的同时,也要花一小部分的精力为未来埋下伏笔。 立足当下,着眼未来 立足当下 业务上而言,当下的环境要求我们必须要走出去寻找突破口,而走出去一个很重要的点就是提升买家体验,国际 B 类市场总体上还是买方市场,对于一个买方市场来说,必须要有买家的繁荣才会带来双边的繁荣。要提升买家体验的手段有很多,而非前端莫属的则是性能这个突破口。 在无线前端团队,我进一步把性能优化的重点锁定在 App 内的 Weex 这个场景。虽然我们团队同时要兼顾 M 站和 App 两个端,但 App 端无论是业务转化还是用户留存都要比 M 站好太多。M 站虽然流量大但转化低,核心原因是由于链路断层及流量结构导致的,性能有影响但并非主因。 国际站的无线是一个全力奔跑的业务,在奔跑的过程中,自然而然选择了“短平快”这种方式来支撑业务。这种“短平快”的模式带来的弊端就是技术体系众多,技术体系众多又容易带来质量隐患,所以在去年我们还有两个重点是要解除稳定性的隐患及逐步做技术收敛。 着眼未来 资源永远是有限的,业务需要前行,技术需要成长,我们需要做出选择。我的选择就是以性能为核心突破口,在稳定性方面先保障卡得住、能发现、可灰度,逐步做好技术收敛。 有人戏言,做技术就像戴着镣铐跳舞,满足业务当下的需求可能就耗掉我们全部的精力了。但与此同时,我们又要考虑到未来的业务和技术发展趋势,布局整个技术产品。可以提前部署但不超前部署,可以分段布局但不盲目入局。 结语 作为技术方,我们不仅要了解业务策略与打法,也要去思考这些策略与打法背后的Why,这样我们才能从更全局的视角去思考与规划技术的未来。只看看清未来,我们才能清楚地知道当下我们要做什么。
一 前言 最近十年来,随着移动互联网和智能设备的兴起,越来越多的数据被沉淀到各大公司的应用平台之上,这些包含大量用户特征和行为日志的数据被海量地存储起来,先经过统计分析与特征样本提取,然后再经过训练就会产出相应的业务算法模型,这些模型就像智能的机器人,它可以精准地识别和预测用户的行为和意图。 如果把数据作为一种资源的话,互联网公司与传统公司有着本质的不同,它不是资源的消耗者,而是资源的生产者,在平台运营的过程中不停地在创造新的数据资源,并且随着平台的使用时长和频率的增加,这些资源也在指数级地增长。平台通过使用这些数据和模型,又反过来带来更好的用户体验和商业价值。2016 年,AlphaGo,一个基于深度神经网络的围棋人工智能程序,第一次战胜围棋世界冠军李世石。这个由谷歌(Google)旗下 DeepMind 公司开发的算法模型,背后使用的数据正是人类棋手所有的历史棋谱数据。 阿里的搜索、推荐和广告也是非常典型的大数据应用的场景(高维稀疏业务场景),在谈如何测试之前我们需要先了解一下平台处理数据的工程技术背景。搜索、推荐、广告系统在工程架构和数据处理流程上比较相近,一般分为离线系统和在线系统两部分,见下图 1(在线广告系统一般性架构,刘鹏《计算广告》)。离线系统负责数据处理与算法模型的建模与训练,而在线系统主要用以处理用户的实时请求。在线系统会使用离线系统训练产出的模型,用以实时的在线预测,例如预估点击率。 用户在访问手机淘宝或者其他 app 的时候会产生大量的行为数据,包括用户的浏览、搜索、点击、购买、评价、停留时长等,加上商家商品维度的各类数据(广告还需要增加广告主维度的数据),这些数据经过采集过滤处理之后再经过特征提取之后生成了模型所需的样本数据,样本数据在机器学习训练平台上经过离线训练之后就可以产生用以在线服务的各类算法模型(例如深度兴趣演化网络 DIEN、Tree-based Deep Model、大规模图表示学习、基于分类兴趣的动态相似用户向量召回模型、等等)。在线系统中最主要的功能是数据的检索和在线预测服务,一般使用信息检索的相关技术,例如数据的正倒排索引、时序存储等。 搜索推荐广告系统在使用了上述维度的大数据,经过深度学习之后,成为一个千人千面的个性化系统。对于不同的用户请求,每次展现的商品和推荐的自然结果和商业结果都不尽相同,即便是同一个用户在不同的时刻得到的结果也会随着用户的实时行为的不同而改变,这些背后都是数据和算法模型的魔力。 图1 在线广告系统一般性架构图 二 大数据应用测试质量域的六大挑战 在思考搜索推荐广告系统是如何测试的之前,我们首先要定义问题域,即要解决的测试问题是什么,我们的思路从以下几个方向展开。 1 功能性测试与验证 除了正常的请求与响应的检查之外,大数据的“大”主要体现在数据的完整性和丰富性。一个搜索推荐引擎的好坏很大程度上取决于其内容是否足够丰富,召回是否足够多样。另外,算法带来搜索推荐结果的不确性,但也给我们的测试验证工作造成了麻烦。所以,数据的完整性和不确定性校验也是功能测试的要点。 2 数据更新的实时性如何测试 总所周知,对于一个搜索或者广告的在线计算引擎,其内部的数据在不停地发生更新,或者出于商家在商品信息上的变更,也可能是因为广告主在创意甚至投放计划上的变化,这些更新需要实时反馈在投放引擎,否则会出现信息不一致甚至错误。如何测试和验证这些变更的及时性,即保证一定的并发带宽又保证更新链路的响应时间,这是需要测试重点关注的一个问题。 3 数据请求响应的及时性如何测试 在线服务都要求低延迟,每次 query 服务端需要在几十毫秒内给出响应结果,而整个服务端的拓扑会有大概 30 多个不同模块构成。如何测试后端服务的性能和容量就变得至关重要。 4 算法的效果如何验证 搜索推荐甚至广告的返回结果需要与用户的需求和兴趣匹配,这样才会保证更高的点击率与成交转化,但如何验证这种需求与结果的相关性,或者如何测试一个算法的效果,这是一个非常有趣且有挑战的话题。 5 AI 算法系统的线上稳定性如何保证 线下发布之前的测试是对代码的测试验收,并随着缺陷的发现与修复,提升的是代码质量。而线上的稳定性运营是为了提升系统运行的稳定性,解决的问题是:即便是一个代码质量一般的系统,如何通过技术运维的方法来提升系统的高可用性与鲁棒性,并降低线上故障的频次与影响,这一部分也被称为线上技术风险领域。 6 工程效率方向 这是对以上几个部分的补充,甚至是对整个工程研发体系在效率上的补充。质量与效率是一对孪生兄弟,也是同一个硬币的两面,如何平衡好两者之间的关系是一个难题,质量优先还是效率优先,不同的产品发展阶段有不同的侧重点。我们的工程效率,力在解决 DevOps 研发工具链路,用以提升研发的工程生产力。 自此,我们基本定义完毕大数据应用的测试问题的六大领域,有些领域已经超过了传统的测试与质量的范畴,但这也正是大数据应用给我们带来的独特质量挑战,下面我们围绕这六个问题展开讲一讲。 三 大数据应用测试六个问题的解法 1 AI 应用的功能性测试验证 功能测试主要分三块:端到端的用户交互测试、在线工程的功能测试、离线算法系统的功能测试。 1) 端到端的用户交互测试 这是涉及到搜索推荐广告系统的用户交互部分的测试验证,既包括买家端(手机淘宝、天猫 app 和优酷 app 等)的用户体验和逻辑功能的验证,也包括针对广告主和商家的客户管理平台(Business Platform)上业务流程逻辑的校验,涉及广告主在广告创意创作、投放计划设定、计费结算等方面的测试。端到端的测试保证了我们最终交付给用户和客户使用的产品质量。端上的测试技术和工具,主要涉及端到端(native/h5)app/web 上的 UI 自动化、安全、性能稳定性(monkey test/crash 率)、流量(弱网络)、耗电量、兼容性和适配,在集团其他团队的测试技术和开源技术体系的基础上我们做了一些改进和创新,例如将富媒体智能化验证引入客户端自动化测试,完成图像对比、文字 OCR、局部特征匹配、边缘检测、基于关键帧的视频验证(组件动画、贴片视频)等,解决了广告推荐在客户端上所有展现形式的验证问题。另外,针对 Java 接口和客户端 SDK 的测试,除了常规的 API Service 级别测试之外,在数据流量回放的基础上使用对比测试的方法,在接口对比、DB 对比、文件对比、流量对比的使用上有一些不错的质量效果。端到端的测试验证,由于 UI 的改版速度非常快,测试策略上我们把自动化的重点放在接口层面,UI 的 automation 只是简单的逻辑验证,全量的 UI 验证回归(功能逻辑和样式体验)还是通过手动测试,这里我们使用了不少的外包测试服务作为补充。 2) 在线工程系统的测试 这一部分是整个系统的功能测试的重点。搜索推荐广告系统,本质上是数据管理的系统,数据包括商品维度、用户维度、商家和广告主维度的数据。把大量的数据按照一定的数据结构存储在机器内存之中,提供召回、预估、融合等服务,这些都是在线工程要去解决的问题。这部分的功能测试,基本原理是通过发送 Request/Query 请求串、验证 Response 结果的模式,在此基础上使用了比较多提升测试用例生成效率和执行效率的技术。基于可视化、智能化等技术(智能用例生成、智能回归、失败智能归因、精准测试覆盖、功能 A/B 测试),把测试方法论融入其中,解决了大规模异构的在线工程功能测试 case 编写成本高、debug 难、回归效率低的问题。搜索推荐广告的在线服务工程基本上由 20 - 30 个不同的在线模块组成,测试这些在线服务模块极其消耗时间,用例的编写效率和回归运行效率是优化的主要目标,在这个方向上,我们在用例生成方面通过用例膨胀和推荐技术、基于遗传算法动态生成有效测试用例、在用例执行阶段使用动态编排的回归技术,通过这些技术极大地提升了在线模块的功能测试的覆盖率。 此外,我们比较多地使用线上的 Query 做对比测试的方法,用以验证功能变更的差异,分析即将发布的系统与实际线上系统之间的结果一致率和数据分布可以很好地找到系统的功能性问题。在线上测试领域,除了对比测试,我们把近期 Top-N 的 Query 在线上定时做巡检监察,一方面起到功能监控的作用,另一方面 Query 量级到一定程度(例如最近一周 80% 的长尾 Query),可以很轻松地验证引擎数据的完整性和多样性。最后,这一部分的测试策略也需要强调一下,由于算法的逻辑(例如召回和排序的业务逻辑)非常复杂,涉及不同的业务和分层模型,这些逻辑是算法工程师直接设计实现的,所以算法逻辑的测试用例的设计和执行也是由算法工程师来做,只有他们最清楚模型的功能逻辑和如何变化。结合着线上 debug 系统的使用,算法工程师可以很清楚目前线上运行的算法和线下即将上线的算法之间的逻辑差异,测试用例也就比较容易编写。测试工程师在其中主要负责整个测试框架和工具环境的搭建,以及基本测试用例的编写与运行。这个测试策略的调整,在本文最后关于测试未来的预判部分也有介绍。 3) 离线系统的测试,或者算法工程测试 从数据流程的角度看,算法工程包括算法模型的建模流程和模型训练上线两部分,从特征提取、样本生成、模型训练、在线预测,整个pipeline离线流程到在线的过程中如何验证特征样本的质量和模型的质量。所以算法测试的关键在于三个地方: 样本特征质量的评估 模型质量的评估 模型在线预估服务的质量保障 a 和 b 涉及数据质量与特征功效放在一起在第四部分介绍,主要使用数据质量的各种指标来评估质量。 这里重点说一下 c,算法在线预估服务上线前的测试,因为其涉及到模型最终服务的质量,比较重要。我们这里使用了一种小样本离线在线打分对比的方法,可以最终比较全面地验证模型上线质量的问题。详细过程是:在模型上线正式服务之前,需要对模型做测试验证,除了准备常规的 test 数据集,我们单独分离出一部分样本集,称之为小样本数据集,通过小样本数据集在线系统的得分与离线分数的对比的差异,验证模型的在线服务质量,这种小样本打分实际上也提供了类似于灰度验证的能力。流程见下图 2。 图2 小样本测试 关于离线系统的测试,我们同时在深度学习训练平台的质量保障上也做了一些探索,目前深度学习平台质量主要面临三大难点: 于种种复杂状况,在集群上训练的模型存在训练失败的风险,如何提前预警深度学习平台当前存在的潜在风险。 由于神经网络天然局部最优解基因和 Tensorflow Batch 的设计思路,每次训练的模型,如何保障它是否满足上线的质量要求。 如何验证在大规模数据集和分布式系统下深度学习平台提供的各种深度学习功能的准确性。 针对这三大问题,我们尝试了三个解法: 实验预跑法,设计特别的模型和训练数据,15 分钟内训练完毕。可以快速发现和定位训练平台的问题,在大规模的生产模型正式训练之前就发现问题。 Model on Model 的模型验证法,把模型生产的中间数据指标(除 auc 之外,还包括神经元激活率、梯度在各层传到均方差等)透传加工建模,监控生产模型的质量。 Model Based 功能校验法,针对性地设计样本格式和测试模型网络,使模型 variable 的理论值能够精确计算出,根据训练模型的结果验证平台的质量。 2 数据更新的实时性如何测试的问题 这一部分主要包含两个子问题: 1) 引擎数据的实时更新链路的测试 对于一个实时更新链路,从上游的数据源/数据表(TT/MetaQ/ODPS,阿里的消息中间件与离线数据表)读取数据,经过 Streaming 计算平台(Bayes 引擎、Blink 等,阿里的实时计算平台)的实时计算任务处理产出引擎可以接受的更新消息,引擎在收到此类消息之后再做数据的更新操作。这个链路主要验证的点在于: 数据的正确性验证 数据的一致性验证 数据的时效性验证 数据的并发性能测试 在这几个问题的解决上,我们使用了流式数据实时对比、全量对比可以解决数据的正确性和一致性验证的问题;数据的时效性更多地依赖计算平台底层的资源来保证毫秒级别的更新速度,我们这里通过记录更新时间戳来验证更新的时效性;性能测试通过伪造上游数据和流量复制来验证整个链路的响应时间和并发处理能力。 2) 模型的实时更新(Online Deep Learning)链路如何测试 为了拿到实时行为带来的算法收益,Online Deep Learning(ODL)最近两年兴起,用户实时行为特征数据也需要实时地训练到模型之中,在 10-15 分钟的时间间隔里,在线服务的模型会被更新替代,留给模型验证的时间最多只有 10 分钟的时间,这是 ODL 带来的质量挑战。解这个问题,我们有两个方法同时工作。 (1)最主要的方法是建立 ODL 全链路质量指标监控体系,这里的链路是指从样本构建到在线预测的全链路,包括样本域的指标校验和训练域指标校验。 指标选取上主要看是否跟效果相关联,例如对于 ctr 预估方向,可以计算测试集上的 auc、gauc(Group auc,分组后的 auc)、score_avg(模型打分均值)等指标,甚至计算train_auc & test_auc,pctr & actual_ctr 之间的差值(前者看是否过拟合,后者看打分准度)是不是在一个合理的范围内。这里也有一个关键的点在于测试集的选取,我们建议测试集除了取下一个时间窗口的数据(用未见的数据测试模型的泛化性能),还可以包含从过去一段时间(比如一周)的数据里面随机抽样的一部分数据(用已见但全面的数据测试模型是否跑偏)。同时,这也降低了局部的异常测试样本对评估指标带来的扰动影响。 (2)除了指标体系之外,我们设计了一个离线仿真的系统,在模型正式上线之前在仿真环境模拟打分校验。 简单来说就是把需要上线的模型,在线下测试环境利用线上流量通过在线服务的组件打分模块进行一个提前的预打分,在这个打分过程中出现任何错误都算校验不通过,打分正常的模型再对分数进行均值和分布的校验,打分校验不通过会直接拒绝上线。通过以上两种方案,结合样本与模型的监控与拦截,可以极大概率低降低 ODL 的质量风险。 3 性能压测 对于由离线、在线两部分构成的AI系统,在线是响应的是用户实时访问请求,对响应时间要求更高,在线系统的性能是这一部分的重点。离线的性能很大程度上取决于训练平台在计算资源方面的调度使用,我们一般通过简单的源头数据复制来做验证。对于在线系统,分为读场景和写场景的性能测试,写场景的性能在第二部分实时更新链路的时效性部分已有介绍,这里主要讲一下在线场景的读场景的性能容量测试。 在线系统一般由二三十个不同的引擎模块组成,引擎里的数据 Data 与测试 Query 的不同都会极大的影响性能测试结果,同时也由于维护线下的性能测试环境与线上环境的数据同步工作需要极大的代价,我们目前的策略都是选择在线上的某个生产集群里做性能容量测试。对于可以处理几十万 QPS(Query Per Second)的在线系统,难点在于如何精准控制产生如此量级的并发 Query,使用简单的多线程或多进程的控制已经无法解决,我们在这里使用了一个爬山算法(梯度多伦迭代爬山法)来做流量的精准控制,背后是上百台的压力测试机器递增式地探测系统性能水位。 另外一个建设的方向是整个压测流程的自动化以及执行上的无人值守,从基于场景的线上 Query 的自动选取、到压力生成、再到均值漂移算法的系统自动化校验工作,整个压测流程会如行云流水一般的按照预设自动完成。配合着集群之间的切流,可以做到白+黑(白天夜间)的日常压测,对线上水位和性能瓶颈的分析带来了极大的便利。 4 效果的测试与评估 这是大数据应用算法的重头戏,由于算法的效果涉及到搜索广告业务的直接受益(Revenue & GMV),我们在这个方向上也有比较大的投入,分为以下几个子方向。 1) 特征与样本的质量与功效评估 通过对特征质量(有无数据及分布问题),以及特征效用(对算法有无价值)两个角度出发,在特征指标计算上找到一些比较重要的指标:缺失率占比、高频取值、分布变化、取值相关性等。同时,在训练和评估过程中大量中间指标与模型效果能产生因果关系,通过系统的分析建模张量、梯度、权重和更新量,能够对算法调优、问题定位起到辅助决策作用。 而且,通过改进 AUC 算法,分析 ROC、PR、预估分布等更多评估指标,能够更全面的评估模型效果。随着数据量级的增加,最近两年我们在建模和训练过程中使用了千亿参数、万亿样本,Graph Deep Learning 也进入百亿点千亿边的阶段,在如此浩瀚的数据海洋里,如何可视化特征样本以及上述的各种指标成为一个难点,我们在 Google 开源的 Tensorboard 的基础上做了大量的优化与改进,帮助算法工程师在数据指标的可视化、训练过程的调试、深度模型的可解释性上给与了较好的支持。 2) 在线流量实验 算法项目在正式上线之前,模型需要在实验环境中引入真实的线上流量进行效果测试和调优,在第一代基于Google分层实验架构的在线分层实验(原理 Google 论文“Overlapping Experiment Infrastructure More, Better, Faster Experimentation”)的基础上,我们在并发实验、参数管理、参数间相互覆盖、实验质量缺乏保障、实验调试能力缺失、实验扩展能力不足等方面做了很多的改进,极大地提升了流量的并发复用与安全机制,达到真正的生产实验的目的。在效果上,通过在线实验平台引入的真实流量的验证,使得模型的效果质量得到极大的保障。 3) 数据效果评测 这里分两块:相关性评测与效果评测。相关性是相关性模型的一个很重要的评估指标,我们主要通过数据评测来解决,通过对搜索展示结果的指标评测,可以得到每次搜索结果的相关性分数,细分指标包括:经典衡量指标 CSAT (Customer Satisfaction,包括非常满意、满意、一般、不满意、非常不满意)、净推荐值 NPS (Net Promoter Score,由贝恩咨询企业客户忠诚度业务的创始人 Fred Reichheld 在 2003 年提出,它通过测量用户的推荐意愿,从而了解用户的忠诚度)、CES (Customer Effort Score,“客户费力度”是让用户评价使用某产品/服务来解决问题的困难程度)、HEART 框架(来源于 Google,从愉悦度 Happiness、Engagement 参与度、Adoption 接受度、Retention 留存率、Task success 任务完成度)。 效果评估方面,我们采用了数据统计与分析的方法。在一个算法模型真正全量投入服务之前,我们需要准确地验证这个模型的服务效果。除了第一部分介绍的离在线对比之外,我们需要更加客观的数据指标来加以佐证。这里我们采用了真实流量的 A/B 实验测试的方法,给即将发布的模型导入线上 5% 的流量,评估这 5% 流量和基准桶的效果对比,从用户体验(相关性)、平台收益、客户价值三个维度做各自实际指标的分析,根据用户的相关性评测结果、平台的收入或者 GMV、客户的 ROI 等几个方面来观测一个新模型对于买家、平台、卖家的潜在影响到底是什么,并给最终的业务决策提供必要的数据支撑。流量从 5% 到 10%,再到 20% 以及 50%,在这个灰度逐渐加大至全量的过程中,无论是功能问题、还是性能的问题,甚至效果的问题都会被探测到,这种方法进一步降低了重大风险的发生。这是一个数据统计分析与技术的融合的方案,与本文所介绍的其他技术方法不同,比较独特,但效果甚佳。 5 线上稳定性 与其他业务的稳定性建设类似,通过发布三板斧(灰度、监控、回滚)来解决发布过程的质量,通过线上的容灾演练、故障注入与演练(我们也是集团开源的混沌工程 Monkey King 的 C++ 版本的提供者)、安全红蓝对抗攻防来提升系统线上的稳定性和可用性。另外在 AI Ops 和 Service Mesh 为基础的运维管控方向上,我们正在向着智能运维、数据透视分析、自动切流、自动扩缩容等方向努力。我们预测结合 Service Mesh 技术理念在 C++ 在线服务的演进,系统会具备对业务应用无侵入的流量标定及变更标定的能力,也就能够实现流量调度能力和隔离的能力。另外,红蓝攻防也将进一步发展,自动化、流程化将逐步成为混沌工程实施的标准形式。由于这一部分尚处于起步阶段,这里不再过多介绍还没有实现的内容,但我们判定这个方向大有可为,与传统运维工作不同,更接近 Google 的 SRE(Site Reliability Engineering)理念。 6 AI 应用的工程效能 主要解决在测试阶段和研发阶段提升效率的问题,这个方向上我们以 DevOps 工具链建设为主,在开发、测试、工程发布、模型发布(模型 debug 定位)、客户反馈(体感评估、众测、客户问题 debug)整个研发闭环所使用到的工具方面的建设。在我们设想的 DevOps 的场景下,开发同学通过使用这些工具可以独立完成需求的开发测试发布及客户反馈的处理。鉴于这个方向与测试本身关系不大,篇幅原因,这里也略过。 四 大数据应用测试的未来 至此,关于大数据应用测试的几个主要问题的解法已经介绍完毕。关于大数据应用测试的未来,我也有一些自己初步的判断。 1 后端服务测试的工具化 这涉及到服务端的测试转型问题,我们的判断是后端服务类型的测试不再需要专职的测试人员,开发工程师在使用合理的测试工具的情况下可以更加高效地完成测试任务。专职的测试团队,未来会更多地专注于偏前端与用户交互方面产品质量的把控,跟产品经理一样,需要从用户的角度思考产品质量的问题,产品的交付与交互的验证是这个方向的重点。多数的服务端的测试工作都是可以自动化的,且很多 service 级别的验证也只有通过自动化这种方式才能验证。相比较测试同学,开发同学在 API 级别的自动化代码开发方面能力会更强,更重要的是开发同学自己做测试会减少测试同学与开发同学之间的大量往返沟通的成本,而这个成本是整个发布环节中占比较大的部分。再者,第一部分介绍过,算法工程师在业务逻辑的理解上更加清晰。 所以,我们更希望后端的测试工作由工程或者算法工程师独立完成,在这种新的生产关系模式下,测试同学更加专注于测试工具的研发,包括自动化测试框架、测试环境部署工具、测试数据构造与生成、发布冒烟测试工具、持续集成与部署等。这种模式也是目前 Google 一直在使用的测试模式,我们今年在这个方向下尝试了转型,在质量变化和效率提升方面这两方面效果还不错。作为国内互联网公司的率先进行的测试转型之路,相信可以给到各位同行一些借鉴。这里需要强调一点的是,虽然测试团队在这个方向上做了转型,但后端测试这个事情还是需要继续做,只是测试任务的执行主体变成了开发工程师,本文介绍的大量后端测试的技术和方向还会继续存在。后端服务类测试团队转型,除了效能工具之外,第五部分的线上稳定性的建设是一个非常好的方向。 2 测试的线上化,既 TIP(Test In Production) 这个概念大概十年前由微软的工程师提出。TIP 是未来测试方法上的一个方向,主要的考虑是以下三点。 1)一方面由于线下测试环境与真实线上环境总是存在一些差异,或者消除这种差异需要较大的持续成本,导致测试结论不够置信。使用最多的就是性能测试或容量测试,后端服务的拓扑非常复杂,且许多模块都有可扩展性,带有不同的数据对性能测试的结果也有很大的影响,测试环境与生产环境的这种不同会带来测试结果的巨大差异。另外,目前的生产集群都是异地多活,在夜里或者流量低谷的时候,单个集群就可以承担起所有流量请求,剩下的集群可以很方便地用来压测,这也给我们在线上做性能测试带来了可能性。最具典型的代表就是阿里的双十一全链路压测,今年基本上都是在白加黑的模式下完成的。 2)另外,许多真实的演练测试也只能在线上系统进行,例如安全攻防和故障注入与演练,在线下测试环境是无法做到的。 3)最后,从质量的最终结果上看,不管是发布前的线下测试,还是发布后的线上稳定性建设,其目的都是为了减少系统故障的发生,把这两部分融合在一起,针对最终的线上故障的减少去做目标优化工作,可以最大程度地节约和利用人力资源。我们判断,线下测试与线上稳定性的融合必将是一个历史趋势,这一领域统称为技术风险的防控。 3 测试技术的智能化 见图 3。类似对自动驾驶的分级,智能化测试也有不同的成熟度模型,从人工测试、自动化、辅助智能测试、高度智能测试。机器智能是一个工具,在测试的不同阶段都有其应用的场景,测试数据和用例设计阶段、测试用例回归执行阶段、测试结果的检验阶段、线上的指标异常检测诸多技术风险领域都可以用到不同的算法和模型。智能化测试是发展到一定阶段的产物,前提条件就是数字化,自动化测试是比较简单一种数字化。没有做到数字化或者自动化,其实是没有智能分析与优化的诉求的。 另外,在算法的使用上,一些简单算法的使用可能会有不错的效果,比较复杂的深度学习甚至强化学习算法的效果反而一般,原因或者难点在两个地方,一个是特征提取和建模比较困难,第二个原因是测试运行的样本与反馈的缺失。但无论如何,运用最新的算法技术去优化不同的测试阶段的效率问题,是未来的一个方向。但我们同时判断,完全的高度智能测试与无人驾驶一样,目前还不成熟,主要不在算法与模型,而是测试数据的不足。 图3 测试技术的智能化 阿里的搜索推荐与广告系统的质量建设之路,经过近 10 年的不断发展,在许多前辈的不断努力付出之下,才能在如此众多的细分领域方向上开花结果,本文所介绍的方法也都浓缩在内部的工具兵器之中,后面我们的想法还是逐渐开源,回馈社区。限于篇幅,内容又较杂多,很多技术细节这里并没有办法细细展开。如果想了解更多,后继可以关注即将由阿里经济体技术质量小组主导出版的测试书籍《阿里巴巴测试之道》(电子工业出版社,暂定书名),本文所介绍的大数据AI算法应用测试被收录在第六章。如果还是需要更加详尽的了解,也欢迎大家加入我们的团队或者开源社区,一起在以上的几个方向做更加深入的研究与建设。
一 什么是 Kubernetes? 我们来看一下什么是 Kubernetes。这部分内容我会从四个角度来跟大家分享一下我的看法。 1 未来什么样 这是一张未来大部分公司后端 IT 基础设施的架构图。简单来说,以后所有公司的 IT 基础设施都会部署在云上。用户会基于 Kubernetes 把底层云资源分割成具体的集群单元,给不同的业务使用。而随着业务微服务化的深入,服务网格这样的服务治理逻辑会变得跟下边两层一样,成为基础设施的范畴。 目前,阿里基本上所有的业务都跑在云上。而其中大约有一半的业务已经迁移到了自己定制 Kubernetes 集群上。另外据我了解,阿里计划今年完成 100% 的基于 Kubernetes 集群的业务部署。 而服务网格这块,在阿里的一些部门,像蚂蚁金服,其实已经有线上业务在用了。大家可以通过蚂蚁一些同学的分享来了解他们的实践过程。 虽然这张图里的观点可能有点绝对,但是目前这个趋势是非常明显的。所以未来几年, Kubernetes 肯定会变成像 Linux 一样的,作为集群的操作系统无处不在。 2 Kubernetes 与操作系统 这是一张传统的操作系统和 Kubernetes 的比较图。大家都知道,作为一个传统的操作系统,像 Linux 或者 Windows,它们扮演的角色,就是底层硬件的 一个抽象层。它们向下管理计算机的硬件,像内存或 CPU,然后把底层硬件抽象成一些易用的接口,用这些接口,向上对应用层提供支持。 而 Kubernetes 呢,我们也可以把它理解为一个操作系统。这个操作系统说白了也是一个抽象层,它向下管理的硬件,不是内存或者 CPU 这种硬件,而是多台计算机组成的集群,这些计算机本身就是普通的单机系统,有自己的操作系统和硬件。Kubernetes 把这些计算机当成一个资源池来 统一管理,向上对应用提供支撑。 这里的应用比较特别,就是这些应用都是容器化的应用。如果对容器不太了解的同学,可以简单把这些应用,理解为一个应用安装文件。安装文件打包了所有的依赖库,比如 libc 这些。这些应用不会依赖底层操作系统的库文件来运行。 3 Kubernetes 与 Google 运维解密 上图中,左边是一个 Kubernetes 集群,右边是一本非常有名的书,就是 Google 运维解密这本书。相信很多人都看过这本书,而且有很多公司目前也在实践这本书里的方法。包括故障管理,运维排班等。 Kubernetes 和这本书的关系,我们可以把他们比作剑法和气功的关系。不知道这里有多少人看过笑傲江湖。笑傲江湖里的华山派分两个派别,气宗和剑宗。气宗注重气功修炼,而剑宗更强调剑法的精妙。实际上气宗和剑宗的分家,是因为华山派两个弟子偷学一本葵花宝典,两个人各记了一部分,最终因为观点分歧分成了两派。 Kubernetes 实际上源自 Google 的集群自动化管理和调度系统 Borg,也就是这本书里讲的运维方法所针对的对象。Borg 系统和书里讲的各种运维方法可以看做是一件事情的两个方面。如果一个公司只去学习他们的运维方法,比如开了 SRE 的职位,而不懂这套方法所管理的系统的话,那其实就是学习葵花宝典,但是只学了一部分。 Borg 因为是 Google 内部的系统,所以我们一般人是看不到的,而 Kubernetes 基本上继承了 Borg 在集群自动化管理方面非常核心的一些理念。所以如果大家看了这本书,觉得很厉害,或者在实践这本书里的方法,那大家一定要深入理解下 Kubernetes。 4 技术演进史 早期的时候,我们做一个网站后端,可能只需要把所有的模块放在一个可执行文件里,就像上图一样,我们有 UI、数据和业务三个模块,这三个模块被编译成一个可执行文件,跑在一台服务器上。 但是随着业务量的大幅增长,我们没有办法,通过升级服务器配置的方式来扩容。这时候我们就必须去做微服务化了。 微服务化会把单体应用拆分成低耦合的小应用。这些应用各自负责一块业务,然后每个应用的实例独占一台服务器,它们之间通过网络互相调用。 这里最关键的是,我们可以通过增加实例个数,来对小应用做横向扩容。这就解决了单台服务器无法扩容的问题。 微服务之后会出现一个问题,就是一个实例占用一台服务器的问题。这种部署方式,资源的浪费其实是比较严重的。这时我们自然会想到,把这些实例混部到底层服务器上。 但是混部会引入两个新问题,一个是依赖库兼容性问题。这些应用依赖的库文件版本可能完全不一样,安装到一个操作系统里,必然会出问题。另一个问题就是应用调度和集群资源管理的问题。 比如一个新的应用被创建出来,我们需要考虑这个应用被调度到哪台服务器,调度上去之后资源够不够用这些问题。 这里的依赖库兼容性问题,是靠容器化来解决的,也就是每个应用自带依赖库,只跟其他应用共享内核。而调度和资源管理就是 Kubernetes 所解决的问题。 顺便提一句,我们可能会因为,集群里混部的应用太多,这些应用关系错综复杂,而没有办法去排查一些像请求响应慢这样的问题。所以类似服务网格这类服务治理的技术,肯定会成为下一个趋势。 二 怎么学习 Kubernetes? 1 Kubernetes 学习难点 总体来说,Kubernetes 之所以门槛比较高,比较难学习,一个是因为它的技术栈非常深,包括了内核,虚拟化,容器,软件定义网络 SDN,存储,安全,甚至可信计算等,绝对可以称得上全栈技术。 同时 Kubernetes 在云环境的实现,肯定会牵扯到非常多的云产品,比如在阿里云上,我们的 Kubernetes 集群用到了 ECS 云服务器,VPC 虚拟网络,负载均衡,安全组,日志服务,云监控,中间件产品像 ahas 和 arms,服务网格,弹性伸缩等等大量云产品。 最后,因为 Kubernetes 是一个通用的计算平台,所以它会被用到各种业务场景中去,比如数据库。据我所知,像我们的 PolarDB Box 一体机就是计划基于 Kubernetes 搭建。另外还有边缘计算,机器学习,流计算等等。 2 了解、动手、思考 基于我个人的经验,学习 Kubernetes,我们需要从了解、动手、以及思考三个方面去把握。 了解其实很重要,特别是了解技术的演进史,以及技术的全景图。 我们需要知道各种技术的演进历史,比如容器技术是怎么从 chroot 这个命令发展而来的,以及技术演进背后要解决的问题是什么,只有知道技术的演进史和发展的动力,我们才能对未来技术方向有自己的判断。 同时我们需要了解技术全景,对 Kubernetes 来说,我们需要了解整个云原生技术栈,包括容器,CICD,微服务、服务网格这些,知道 Kubernetes 在整个技术栈里所处的位置。 除了这些基本的背景知识以外,学习 Kubernetes 技术,动手实践是非常关键的。 从我和大量工程师一起解决问题的经验来说,很多人其实是不会去深入研究技术细节的。我们经常开玩笑说工程师有两种,一种是 search engineer,就是搜索工程师,一种是 research engineer,就是研究工程师。很多工程师遇到问题,google 一把,如果搜不到答案,就直接开工单了。这样是很难深入理解一个技术的。 最后就是怎么去思考,怎么去总结了。我个人的经验是,我们需要在理解技术细节之后,不断的问自己,细节的背后,有没有什么更本质的东西。也就是我们要把复杂的细节看简单,然后找出普通的模式出来。 下边我用两个例子来具体解释一下上边的方法。 3 用冰箱来理解集群控制器 第一个例子是关于集群控制器的。我们在学习 Kubernetes 的时候会听到几个概念,像声明式 API,Operator,面向终态设计等。这些概念本质上 都是在讲一件事情,就是控制器模式。 我们怎么来理解 Kubernetes 的控制器呢?上面这张图是一个经典的 Kubernetes 架构图,这张图里有集群管控节点和工作节点,管控节点上有中心数据库,API Server,调度器及一些控制器。 中心数据库是集群的核心存储系统,API Server 是集群的管控入口,调度器负责把应用调度到资源充沛的节点上。而控制器是我们这里要说的重点。控制器的作用,我们用一句话概括,就是“让梦想照进现实”。从这个意义上来讲,我自己也经常扮演控制器的角色,我女儿如果说,爸爸我要吃冰激凌,那我女儿就是集群的用户,我就是负责把她这个愿望实现的人,就是控制器。 除了管控节点以外,Kubernetes 集群有很多工作节点,这些节点都部署了 Kubelet 和 Proxy 这两个代理。Kubelet 负责管理工作节点,包括应用在节点上启动和停止之类的工作。Proxy 负责把服务的定义落实成具体的 iptables 或者 ipvs 规则。这里服务的概念,其实简单来说,就是利用 iptables 或者 ipvs 来实现负载均衡。 如果我们从控制器的角度来看第一张图的话,我们就会得到第二张图。也就是说,集群实际上就包括一个数据库,一个集群入口,以及很多个控制器。这些组件,包括调度器,Kubelet 以及 Proxy,实际上都是不断的去观察集群里各种资源的定义,然后把这些定义落实成具体的配置,比如容器启动或 iptables 配置。 从控制器的角度观察 Kubernetes 的时候,我们其实得到了 Kubernetes 最根本的一个原理了。就是控制器模式。 其实控制器模式在我们生活中无处不在的,这里我拿冰箱做个例子。我们在控制冰箱的时候,并不会直接去控制冰箱里的制冷系统或者照明系统。我们打开冰箱的时候,里边的灯会打开,我们在设置了想要的温度之后,就算我们不在家,制冷系统也会一直保持这个温度。这背后就是因为有控制器模式在起作用。 4 为什么删除不掉命名空间 第二个例子,我们来看一个真实问题的排查过程。这个问题是一个命名空间不能被删除的问题。问题稍微有点复杂,我们一步一步来看。 命名空间是 Kubernetes 集群的 一个收纳盒机制,就像这里的第一张图片一样。这个盒子就是命名空间,它里边收纳了橡皮和铅笔。 命名空间可以被创建或者删除。我们经常会遇到不能删除命名空间的问题。遇到这个问题,我们如果完全不知道怎么排查。第一步我们可能会想到,研究一下 API Server 是怎么处理这个删除操作的,因为 API Server 就是集群的 管理入口。 API Server 本身是一个应用,我们可以通过提升这个应用的日志级别,来深入理解它的操作流程。在这个问题里,我们会发现,API Server 收到删除命令,但是就没有其他信息了。 这里我们需要稍微理解下命名空间的删除过程,用户在删除命名空间的时候,其实命名空间并不会被直接删除掉,而会被改成“删除中”的状态。这个时候命名空间控制器就会看到这个状态。 为了理解命名空间控制器的行为,我们同样可以把控制器的日志级别提高来查看详细的日志。这个时候呢,我们会发现,控制器正在尝试去获取所有的 API 分组。 到这里我们需要去理解两个事情。一个是为什么删除命名空间,控制器会去获取 API 分组。第二个是 API 分组到底是什么。 我们先看第二个问题,API 分组到底是什么。简单来说,API 分组就是集群 API 的分类机制,比如网络相关的 API 就在 networking 这个组里。而通过网络 API 分组创建出来的资源就属于这个组。 那为什么命名空间控制器会去获取 API 分组呢?是因为在删除命名空间的时候,控制器需要删除命名空间里的所有资源。这个操作不像我们删除文件夹一样,会把里边的文件都一起删掉。 命名空间收纳了资源,实际上是这些资源用类似索引的机制,指向了这个命名空间。集群只有遍历所有的 API 分组,找出指向这个命名空间的所有资源,才能逐个把它们删除掉。 而遍历 API 组这个操作呢,会使得集群的 API Server 和它的扩展进行通信。这是因为 API Server 的扩展,也可以实现一部分 API 分组。所以想知道被删除的命名空间里是不是有包括这个扩展定义的资源,API Server 就必须和扩展通信。 到这一步之后,问题实际上变成 API Server 和他的扩展之间通信的问题。也就是删除资源的问题就变成了网络问题。 阿里云的 Kubernetes 集群,是在 VPC 网络,也就是虚拟局域网上创建的。默认情况下, VPC 的只认识 VPC 网段的地址,而集群里边的容器,一般会使用和 VPC 不同的网段。比如 VPC 使用 172 网段,那容器可能就使用 192 网段。 我们通过在 VPC 的路由表里,增加容器网段的路由项,可以让容器使用 VPC 网络进行通信。 在右下角这张图,我们有两个集群节点,他们的地址是 172 网段,那我们给路由表里增加 192 网段的路由项,就可以让 VPC 把发给容器的数据转发到正确的节点上,再由节点发给具体的容器。 而这里的路由项,是在节点加入集群的时候,由路由控制器来添加的。路由控制器在发现有新节点加入集群之后,会立刻做出反应,给路由表里增加一条路由项。 添加路由项这个操作,其实是对 VPC 的一次操作。这个操作是需要使用一定的授权的,这是因为这个操作跟线下一台机器访问云上资源是差不多的,肯定需要授权。 这里路由控制器使用的授权,是以 RAM 角色的方式绑定到路由控制器所在的集群节点上的。而这个 RAM 角色,正常会有一系列的授权规则。 最后,我们通过检查,发现用户修改了授权规则,所以导致了这个问题。
《Java 开发手册》始发于阿里巴巴内部规约,涵盖编程规约、异常日志、单元测试、安全规约等七大维度。从 2017 年上线至今整整四年,共发布了七个版本,在全球 Java 开发者共同努力下,这本手册已经成为业界普遍遵循的开发规范,感谢大家一直和我们在码出高效、码出质量的路上并肩同行。 本文将介绍每个版本手册更新的亮点,文末可以下载所有版本的合集。 第七版:泰山版《Java 开发手册》 更新日期:2020/04/22 更新亮点: 新增 5 条日期时间规约 新增 2 条表别名 sql 规约 新增统一错误码规约 新增的规约细则如下: OOP 规约 1.【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。 2.【推荐】当某个方法的代码行数超过 10 行时,return / throw 等中断逻辑的右大括号后加一个空行。 说明:这样做逻辑清晰,有利于代码阅读时重点关注。 3.【推荐】在类中删除未使用的任何字段和方法;在方法中删除未使用的任何参数声明与内部变量。 4.【强制】三目运算符 condition? 表达式1 : 表达式2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。 说明:以下两种场景会触发类型对齐的拆箱操作: 表达式 1 或表达式 2 的值只要有一个是原始类型。 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。 日期时间 1.【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。 说明:日期格式中的这两对字母表意如下: 表示月份是大写的 M; 表示分钟则是小写的 m; 24 小时制的是大写的 H; 12 小时制的则是小写的 h。 2.【强制】不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3) java.sql.Timestamp。 说明:第 1 个不记录时间,getHours() 抛出异常;第 2 个不记录日期, getYear() 抛出异常;第 3 个在构造方法 super((time/1000)*1000),fastTime 和 nanos 分开存储秒和纳秒信息。 3.【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。 4.【推荐】避免公历闰年 2 月问题。闰年的 2 月份有 29 天,一年后的那一天不可能是 2 月 29 日。 5.【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar等日期相关类的月份 month 取值在 0-11 之间。 说明:参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January. 集合处理 1.【强制】判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式。 说明:前者的时间复杂度为 O(1),而且可读性更好。 2.【强制】在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要使 用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key 值时会抛出 IllegalStateException 异常。 说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。 3.【强制】在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。 说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断: if (value == null || remappingFunction == null) throw newNullPointerException(); SQL 规约 1.【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。 说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。 2.【推荐】SQL 语句中表的别名前加 as,并且以 t1、t2、t3、... 的顺序依次命名。 说明:1)别名可以是表的简称,或者是根据表出现的顺序,以 t1、t2、t3 的方式命名。2)别名前加 as 使别名更容易识别。 3.【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。说明:如果对象里某些 get 方法被重写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。 4.【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace() 打印异常堆栈。 说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。 其他 1.【强制】避免用 Apache Beanutils 进行属性的 copy。 说明:Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。 安全规约 1.【强制】URL 外部重定向传入的目标地址必须执行白名单过滤。 二方库规约 1.【推荐】不要使用不稳定的工具包或者 Utils 类。 说明:不稳定指的是提供方无法做到向下兼容,在编译阶段正常,但在运行时产生异常,因此,尽量使用业界稳定的二方工具包。 设计规约 1.【参考】可扩展性的本质是找到系统的变化点,并隔离变化点。 说明:世间众多设计模式其实就是一种设计模式即隔离变化点的模式。 2.【参考】代码即文档的观点是错误的,清晰的代码只是文档的某个片断,而不是全部。 说明:代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题是需要相应的文档来完整地呈现的。 错误码统一 统一错误码,就是统一度量衡,为你的应用与服务的稳定保驾护航,烦恼清空,快乐回家。泰山版新近出炉的错误码具有快速溯源、简单易记、沟通标准化三大优势。错误码为字符串类型,共 5 位,分成两个部分:错误产生来源和四位数字编号。错误产生来源分为 A/B/C,以当前代码运行视角来进行判定,A 表示错误来源于用户,比如请求参数错误,用户安装版本过低等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题。优秀的错误码可以迅速知道他们是怎么来滴,从哪儿来滴,来干啥滴。同时俺们的错误码具有三级结构,分为一级宏观错误码、二级宏观错误码、三级宏观错误码,这样的方案更加可扩展,有弹性,更多详细规则,见手册的附件的《错误码参考列表》。 Java 开发手册所有历史版本 第 6 版:华山版《Java 开发手册》 更新日期:2019/06/13 更新亮点: 新增 21 条设计规约 修改描述 112 处 完善若干处示例 第 5 版:1.4.0 版 《阿里巴巴 Java 开发手册》 更新日期:2018/06/06 更新亮点:新增 16 条设计规约 第 4 版:1.3.0 版 《阿里巴巴 Java 开发手册》 更新日期:2017/09/19 更新亮点:增加单元测试规约 第 3 版:1.2.0 版 《阿里巴巴 Java 开发手册》 更新日期:2017/05/20 更新亮点:公开征集意见后修正版本 第 2 版:1.1.0 版 《阿里巴巴 Java 开发手册》 更新日期:2017/02/27 更新亮点:增加前言和专有名词说明,修正部分描述 第 1 版:1.0.0 版 《阿里巴巴 Java 开发手册》 更新日期:2016/12/07 更新亮点:首次向业界开放
一 线上常见问题定位 常见问题 1:CPU 利用率高 CPU 使用率是衡量系统繁忙程度的重要指标,一般情况下单纯的 CPU 高并没有问题,它代表系统正在不断的处理我们的任务,但是如果 CPU 过高,导致任务处理不过来,从而引起 load 高,这个是非常危险需要关注的。 CPU 使用率的安全值没有一个标准值,取决于你的系统是计算密集型还是 IO 密集型,一般计算密集型应用 CPU 使用率偏高 load 偏低,IO 密集型相反。 问题原因及定位: 1 频繁 FullGC/YongGC 查看 gc 日志 jstat -gcutil pid 查看内存使用和 gc 情况 2 代码消耗,如死循环,md5 等内存态操作 1)arthas (已开源:https://github.com/alibaba/arthas) thread -n 5 查看 CPU 使用率最高的前 5 个线程(包含堆栈,第二部分有详解) 2)jstack 查找 ps -ef | grep java 找到 Java 进程 id top -Hp pid 找到使用 CPU 最高的线程 printf ‘0x%x’ tid 线程 id 转化 16 进制 jstack pid | grep tid 找到线程堆栈 ps:输入“1”可查看每个 CPU 的情况,之前有团队遇到单个 CPU 被中间件绑定导致 CPU 飚高的 case。 常见问题 2:load 高 load 指单位时间内活跃进程数,包含运行态(runnable 和 running)和不可中断态( IO、内核态锁)。关键字是运行态和不可中断态,运行态可以联想到 Java 线程的 6 种状态,如下,线程 new 之后处于 NEW 状态,执行 start 进入 runnable 等待 CPU 调度,因此如果 CPU 很忙会导致 runnable 进程数增加;不可中断态主要包含网络 IO、磁盘 IO 以及内核态的锁,如 synchronized 等。 问题原因及定位: 1 CPU 利用率高,可运行态进程数多 排查方法见常见问题一 2 iowait,等待 IO vmstat 查看 blocked 进程状况 jstack -l pid | grep BLOCKED 查看阻塞态线程堆栈 3 等待内核态锁,如 synchronized jstack -l pid | grep BLOCKED 查看阻塞态线程堆栈 profiler dump 线程栈,分析线程持锁情况 常见问题 3:持续 FullGC 在了解 FullGC 原因之前,先花一点时间回顾下 jvm 的内存相关知识: 内存模型 新 new 的对象放在 Eden 区,当 Eden 区满之后进行一次 MinorGC,并将存活的对象放入 S0; 当下一次 Eden 区满的时候,再次进行 MinorGC,并将存活的对象和 S0 的对象放入S1(S0 和 S1 始终有一个是空的); 依次循环直到 S0 或者 S1 快满的时候将对象放入 old 区,依次,直到 old 区满进行 FullGC。 jdk1.7 之前 Java 类信息、常量池、静态变量存储在 Perm 永久代,类的原数据和静态变量在类加载的时候放入 Perm 区,类卸载的时候清理;在 1.8 中,MetaSpace 代替 Perm 区,使用本地内存,常量池和静态变量放入堆区,一定程度上解决了在运行时生成或加载大量类造成的 FullGC,如反射、代理、groovy 等。 回收器 年轻代常用 ParNew,复制算法,多线程并行; 老年代常用 CMS,标记清除算法(会产生内存碎片),并发收集(收集过程中有用户线程产生对象)。 关键常用参数 CMSInitiatingOccupancyFraction 表示老年代使用率达到多少时进行 FullGC; UseCMSCompactAtFullCollection 表示在进行 FullGC 之后进行老年代内存整理,避免产生内存碎片。 问题原因及定位: 1 prommotion failed 从S区晋升的对象在老年代也放不下导致 FullGC(fgc 回收无效则抛 OOM)。 1)survivor 区太小,对象过早进入老年代。 jstat -gcutil pid 1000 观察内存运行情况; jinfo pid 查看 SurvivorRatio 参数; 2)大对象分配,没有足够的内存。 日志查找关键字 “allocating large”; profiler 查看内存概况大对象分布; 3)old 区存在大量对象。 实例数量前十的类:jmap -histo pid | sort -n -r -k 2 | head -10 实例容量前十的类:jmap -histo pid | sort -n -r -k 3 | head -10 dump 堆,profiler 分析对象占用情况 2 concurrent mode failed 在 CMS GC 过程中业务线程将对象放入老年代(并发收集的特点)内存不足。详细原因: 1)fgc 触发比例过大,导致老年代占用过多,并发收集时用户线程持续产生对象导致达到触发 FGC 比例。 jinfo 查看 CMSInitiatingOccupancyFraction 参数,一般 70~80 即可 2)老年代存在内存碎片。 jinfo 查看 UseCMSCompactAtFullCollection 参数,在 FullGC 后整理内存 常见问题 4:线程池满 Java 线程池以有界队列的线程池为例,当新任务提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求。如果正在运行的线程数等于 corePoolSize 时,则新任务被添加到队列中,直到队列满。当队列满了后,会继续开辟新线程来处理任务,但不超过 maximumPoolSize。当任务队列满了并且已开辟了最大线程数,此时又来了新任务,ThreadPoolExecutor 会拒绝服务。 问题原因及定位: 1 下游 RT 高,超时时间不合理 业务监控 sunfire eagleeye 2 数据库慢 sql 或者数据库死锁 日志关键字 “Deadlock found when trying to get lock” Jstack 或 zprofiler 查看阻塞态线程 3 Java 代码死锁 jstack –l pid | grep -i –E 'BLOCKED | deadlock' dump thread 通过 zprofiler 分析阻塞线程和持锁情况 常见问题 5:NoSuchMethodException 问题原因及定位: 1 jar 包冲突 java 在装载一个目录下所有 jar 包时,它加载的顺序完全取决于操作系统。 mvn dependency:tree 分析报错方法所在的 jar 包版本,留下新的 arthas:sc -d ClassName XX:+TraceClassLoading 2 同类问题 ClassNotFoundException NoClassDefFoundError ClassCastException 二 常用工具介绍 常用命令 1 tail -f 跟踪文件 2 grep -i 忽略大小写 -v 反转查找 -E 扩展正则表达式 :grep -E 'pattern1|pattern2' filename 3 pgm -b 开启并发 -p 指定并发数 -A 开启 askpass 4 awk -F 指定分隔符:awk -F “|” '{print $1}‘ | sort -r | uniq -c 5 sed 时间段匹配:sed '/2020-03-02 10:00:00/,/2020-03-02 11:00:00/p' filename arthas 阿里巴巴开源 Java 诊断工具(开源地址:https://github.com/alibaba/arthas),基于 javaAgent 方式,使用 Instrumentation 方式修改字节码方式进行 Java 应用诊断。 基础功能介绍 dashboard:系统实时数据面板, 可查看线程,内存,gc 等信息 thread:jvm 线程堆栈信息,如查看最繁忙的前 n 线程 getstatic:获取静态属性值,如 getstatic className attrName 可用于查看线上开关真实值 sc:查看 jvm 已加载类信息,可用于排查 jar 包冲突 sm:查看 jvm 已加载类的方法信息 jad:反编译 jvm 加载类信息,排查代码逻辑没执行原因 watch:观测方法执行数据,包含出入参,异常等; watch xxxClass xxxMethod " {params, throwExp} " -e -x 2 watch xxxClass xxxMethod "{params,returnObj}" "params[0].sellerId.equals('189')" -x 2 watch xxxClass xxxMethod sendMsg '@com.taobao.eagleeye.EagleEye@getTraceId()' trace:方法内部调用时长,并输出每个节点的耗时,用于性能分析 tt:用于记录方法,并做回放 三 常见问题恢复 1 线程池满 rpc 框架线程池满 高 RT 接口进行线程数限流 应用内线程池满 重启可短暂缓解,具体还得看问题原因 2 CPU 高,load 高 单机置换或重启,可短暂缓解,恢复看具体原因 集群高且流量大幅增加,扩容,恢复看具体原因 3 下游 RT 高 限流 降级 4 数据库 死锁 kill 进程 慢 sql sql 限流 线上问题的排查是一个积累的过程,只有了解问题背后的原理才能更快速的定位和恢复,除此之外更需要有一些趁手的工具来辅助排查,从而降低整个团队问题定位和快恢的门槛。
背景 19 年 6 月左右,我发布过一篇文章《Bit 初体验》 。在梳理这篇文章的过程中,我可以说深度体验了一把 bit 所提出的概念和做法,就像一颗种子种在我的脑海中,一开始我觉得这东西没什么。 我还记得我第一次与我的同事分享 bit 后,他说: emm,虽然你讲了这么多,但是我觉得好像没有那么...有体感? 感觉没什么卵用? 啊,emm,既然你说了,就像你说的。我觉得我们现在如果引入 bit 会不会对我们的日常工作带来很多额外的工作量。 这种反应很正常,我是在 18 年初,就在 Vue 的官网见到过 bit ,当时我点进去大致浏览过一下。我当时的感受就是,没什么卵用,无非就是 " 前端垂直领域的 git "。对国内的支持情况还不咋地,连一篇像样的中文文档都找不到。 在我们的团队中一下子直接切换到 bit 的工作流,这确实不现实,在公司有那么多的基础建设都不知道 bit 这么个玩意。 但是,bit 的做法和概念,却是非常非常有价值和可以借鉴的! 所以,我想做一件事情,一步一步的把 bit 的玩法用我们熟悉的方式引入进来甚至有所延伸扩展,让大家认同其中的好处和价值。 认识组件 随着近些年”微前端“概念的不断酝酿,越来越多的团队开始着手将自己的业务处理为不同组件,然后通过一些微前端做法,编排到一个业务页面中去。 那么对于组件的维护就会变得越来越重要。所以,先来看看现在大多数团队是怎么维护组件的吧! 大库型,Antd、Element 标准的大库型 一次型,完全业务组件,用完一次再也不维护 高复用型,一看就应该单独封装以后给其他人用,比如:视频播放器 项目融合型,与业务项目在一起,混合 store,不分你我 我暂时能想到的就这几种类型的组件,如果你的团队也在维护自己的一套组件库,那么应该很容易理解我上面所说的。 我相信,既然这么做了,肯定有这么做的理由和好处,没有人会闲着没事找麻烦做不是,那么这些做法都有什么好处和痛点呢?我从几个方面入手分别分析一下。 方便、快捷 组件嘛,当然是最快能跑起来,最方便能看到效果最好咯。就这点来讲,还有什么比直接在业务项目里撸组件更快的方式吗!? 现在用个展示的面板,立马去 components 目录撸一个。 数据?不是有 store 吗?引入进来不就拿到数据了! 所见即所得,现在改完马上看到页面上的效果!无法反驳.. 这么看确实开发这个组件是好快了,但是从整个业务需求实现来看,这么做真的是最快的吗?如果这样的做法是最快捷的,那为什么那么多团队在强调沉淀、封装、抽象呢? 其实很多组件当时看起来,这辈子就只可能用一次,不用封装。可是往往交互稿过来的时候就会发现,这个样式好像我在哪里见过。然后去各种业务项目里一顿翻,哇终于找到了,复制过来发现各种爆红,定睛一看,store??? 所以,聪明的团队早已洞察这一切,让我们把组件都维护到同一个地方,然后大家写好文档,用的时候从库里面取就可以了,有 Bug 的话统一修复就是,棒 ! 可维护性 于是乎,大家便如火如荼的开始的组件抽象,组件整改的浩大工程。 一开始,一般会有一个团队中较为靠谱、能力突出的小伙子(嗯?怎么是我?)去把 Webpack、Babel、TypeScript、SassLess、目录结构、单元测试结构、代码规范、Review 规范、发布规范这些梳理好,然后写一个标准的组件出来,最后再强调一下大家一定要按照规范认真维护组件,书写文档,编写单元测试。 从维护性上来讲,大家把组件都写在一个库里面,然后再用到的项目中直接引入,业务上的问题逐渐被分为组件问题还是项目问题,甚至有些需求可以用这个交互在组件库中有相似的,用那个组件就可以了,来反驳产品和设计 。 就在大家用的不亦乐乎的时候,有一天发现,呀,我们的组件库怎么打包出来有 10M 啊 ! 然后找一个靠谱、能力突出的小伙子(没错又是我)就去查了下,这个库是谁引入的?这个组件不是已经有一个了吗?lodash 不是这么用的呀!这个组件是干什么的,怎么没文档? 面对上百个业务组件,只能感叹一声业务迭代的可真快啊。 所以,大库维护固然有大库维护的好处和适用场景,大家能够有这样的抽象思维已经是技术上的突破了,现在只是遇到了另外一个问题,解它! 组件大小、加载性能 接触 Webpack 的一些周边工具,比如 analyzer 很容易可找出具体是什么包”霸占“了这么多的流量。 发现原来组件包中还有一些个组件,看上去不应该放在大库中进行维护,比如那种一次性组件,二次封装型组件。 因为这种组件可能会引入一个很大的第三方依赖,比如视频播放器、Banner Swiper 等。 对于这样的组件,最好的处理方式应该是创建一个独立的仓库,封装完善后,写好 README,发布至内网 NPM,供业务项目使用。 But you know ,这样做成本太高,如果有时间的话,我肯定.....balabala...(一般来说,如果你对程序员说一个更好的方案时,除非这个方案他有参与设计,否则大部分回复都是这样 ) 当然组件大小这方面也可以通过很多其他方式解决,比如:异步加载,NPM 引入,按需加载等等啦...那么,让我们谈谈下面另外一个很重要、又很容易被忽略的部分吧。 组件说明及可索引性 老板,我们今年沉淀了组件 200+,其中有几个组件写的特别好,同时支撑了 20+ 项目。 哇,这么棒!来给我看看你们写的组件长什么样?啊,这,这样来看看我们做的这个页面,这个页面里面用了这几个组件,balabala ... 设计:听说你们已经沉淀了 200+ 组件,能给我们看看有哪些组件吗?我们在下次设计的时候可以参考这些组件进行输出,减少沟通成本。 前端:@所有人 这个组件我们库里面有吗?有,CascadeSelect。哦,怎么用的?有文档吗?.......看下源码吧。well... 组件的说明及可索引性,其实仅次于组件的可用性,甚至更高。 试想下如果今天你写了个巨牛的组件,复用性、接口设计和交互设计都非常棒,但是你有什么渠道能让大家一下子就知道吗,难道你要专门为此拉大家开个会?来今天占用大家1个小时的宝贵时间,介绍下我今天写的巨牛组件。 反过来想,如果我在写组件的时候,反正我这个组件也没啥亮点,别人应该也不会用到,就不用补充文档了吧,应该也没人会知道。哦豁,丸蛋 索引组件,来给大家分享一张图: 如果有一天你团队的组件库也能像这样,一板一眼有图有真相,那该是多么幸福和享受的一件事情! 我也知道这样好啊!谁不知道!如果我有时间,我肯定会....balabala... 所以你的意思是让我们每写一个组件不但要补充文档,还要补充用法说明,还要截图!? 对,还要单独建库,还要考虑配置 Webpack、Babel、TypeScript、SassLess、目录结构、单元测试结构、代码规范、Review 规范、发布规范这些 最*实践 说这么多呢,主要是想带读者们一起思考,也是我写作的风格(喜欢讲故事),大部分内容其实是前端er 都会遇到的问题。 接下就进入正题,说了一大堆问题,总得有点办法来解决吧! 先看看 bit 是怎么做的吧,bit 首先自身有一定的编译能力,内置了 Webpack 及一些插件式 loader 来解决 React、Vue 等编译问题。 对于我们团队来说,都是使用 React,所以咱们就先从一个编译 React 的脚手架开始。 如果把每一个组件都作为单独的 NPM 项目发布,首先要考虑的是,前端一系列的编译环境。如果我有 N 个前端组件项目,每个前端组件库的 Webpack、babel 这些都需要重复配置,那真是要头大的事情,我只是想写一个组件而已,为什么要考虑这些。所以我们的脚手架首先要具备一些基础的编译命令。 啊对了,脚手架还没有名字,那就暂时叫它:comp 吧 comp new:处理按照模板新建一个标准组件 初始化一个标准组件项目结构,所有接入所有 comp 命令 初始化 Git 仓库 初始化 CI/CD 云构建配置 comp start:处理日常开发,附加单个组件展示及调试能力 comp watch:处理 babel 及 scss 监听编译,用于 npm link 场景 comp babel:处理编译 npm 包 comp dev:处理监听编译 umd 包,用于代理调试 comp build:处理最后编译过程 webpack 编译 UMD 包 Babel 包 CICD 过程中自动截图组件 CICD 过程中自动生成 README 其他 Hook comp test:处理 jest 单元测试 那么等组件初始化以后,目录结构就长这样: 项目结构中没有任何 webpackbabel 配置,当然如果有特殊配置需求,可以建立 comp.config.js 进行配置(此处借鉴很多已有的 cli 处理方式)。 这样处理的好处是,在项目初始化后,用户能见到的目录结构非常清晰明了,项目中不有很多允许配置的地方,所有组件的编译环境基本可以保证统一。 这都是些非常基础的功能,当然又是不可缺少的部分,这些基础命令我就不详细介绍了,重点在后面。 通过这几个问题来介绍功能: 你平时开发组件的流程是什么样子? 平时,一般就是根据设计稿,切分到组件后。 然后去创建组件,最后通过项目引入,一边看着一边开发啊。 你开发组件的时候对于你提供的 Props 是如何验证的? 最简单的给一个 mock 看看效果呗。 或者写一个单元测试? 那写 Mock 的过程算不算是在写 Usage 呢? 这个,应该也算吧,但是这些都是散落在各个项目里面,有些 mock 验证完就删掉了。 谁会闲的没事在开发的时候把这些补充到 README 里面去啊。 为什么他们不写文档? 这还用说?因为懒呗? 那你为啥不写?emm,那是因为....写文档这事儿吧,写了不一定有人看,还费时间呀!业务需求那么多!我要是有时间的话,我肯定....balabala... OK,那我们来看下一个问题 一个好的组件文档需要那几部分? 开发组件背景,注意事项啥的,这个没啥太大的必要,有的组件需要的话就补充下,没有的话就不用补充。 主要需要的一些介绍有 :用法,Props 入参,最好能有个截图,要是有个 Demo,那简直完美! 还有安装、开发、编译的命令介绍得有吧。 锦上添花的话最好还能有几个 badge,介绍下源码是 TypeScript,下载量多少。 但是,要补充这些文档是在太麻烦了,要一个一个整理,Props 这些信息,用的人可以在组件里面找到啊,我都有些注释和类型定义的呀! 完成一轮心灵拷问之后,就会发现在整个组件的开发过程中,开发者本人之所以对这个组件这么清楚,是因为开发者其实已经为自己写过一份 README 了。 用法:组件开发过程中需要看到效果,写过一些 mock 数据,已经知道什么样的 props 传进去会产生什么样的效果。 Props 入参:组件有哪些 Props,所代表的含义是什么,每个 Props 入参类型是什么,已经在 TypeScript 的 Interface 及注释中有体现。 截图:有 mock 数据还不知道长什么样?已经看过 N 多遍了。 Demo:根据用法和组件定义,开发调试的就是 Demo。 有了这四个最重要的介绍后,相信大部分开发者也都能知道这个组件是怎么个情况了。 所以,如果我们能把上面这些数据都收集到,是不是就可以利用脚本 自动生成 README 文档 了呢? 用法 / Usage 要收集用法其实很简单,如果让组件有独立开发的能力,不就可以保留这些 Usage 的 Mock 数据了吗? 有些人可能没理解我说的”组件独立开发的能力“是什么意思,我解释一下下:我们平时开发一个组件,一般都是把这个组件放置于某个页面中,一遍调试页面一遍调试组件。独立开发组件的意思是,直接启动一个页面,可以看到组件的样子,这个页面展示的就是围绕组件的所有信息。 所以在脚手架中,只要在 docs.ts 中书写需要调试组件相关的 mock 数据,页面就可以展示出组件的样子,同时这些 mock 数据可以保留作为 README 文档数据。 export default function(Component: typeof IComponent, mountNode) { /** DOCS_START 请将Demo生成方法都写在以下区块内,用于生成README及Riddle **/ ReactDOM.render( <Component navigation={true} pagination={true} autoplay={true} dataSource={[ { href: 'http://xxxxxxxx', image: 'https://xxxxxxx.cdn.com/tfs/TB1jHkBmNv1gK0jSZFFXXb0sXXa-1440-343.png', }, { image: 'https://xxxxxxx.cdn.com/tfs/TB1Y_XacrY1gK0jSZTEXXXDQVXa-1416-813.png', }, ]} />, mountNode, ); /** DOCS_END **/ } 另外,如果保证这份 demo 的接口输出统一规范,还可以支持直接生成在 CodePen,Riddle 这些在线编辑的代码内容。 试想下,你的 README 中如果出现一段 :点击立即体验 ,跳转过去后可以在线编辑实时看到效果,那对于任何看到你组件的同学来说都是一种享受 组件参数 / Props 要收集这部分数据就比较复杂了,我们需要深入分析 TypeScript AST 语法树,提取出其中组件 props 的类型以及对于Interface的注释内容。 经过一番 github,终于找到可以实现一个可以处理这件事情的小众库:react-docgen-typescript(https://github.com/styleguidist/react-docgen-typescript)。 在开发过程中,因为对一些注释及类型输出与我预期的不太一样,所以我 fork 后做了一些修改,已经可以完成对一个完整组件的 Props 做分析后输出一份 typefile.json 。 同样的,通过基于该能力,可以扩展为 webpack 插件 react-docgen-typescript-loader(https://github.com/strothj/react-docgen-typescript-loader),为组件的静态属性中添加 __docInfo 属性,来声明其属性内容,于是组件开发过程变可以实现以下效果: 截图 / Preview 有了组件,有了 Demo,还愁没有截图吗? 直接在构建过程中用 puppeteer ,读取运行 docs.ts 渲染出组件,进行截图,然后随着云构建 CD 过程发到 CDN,就完事了! 最后,README 中加入一些特殊标记,在云构建过程中进行 README 替换生成就可以啦!并不会影响 README 本身要叮嘱的内容。 最后,Duang !一份完整,漂亮,详细的文档就生成好了,整个过程我们并没有特意写过什么 README 方面的内容,一切都是非常轻松标准的进行输出。 结语 在上面的一整套复杂的过程中,看上去最后好像我只得到了一个自动生成 README 的功能。但实际上呢,其实 README 只是一个顺带的产物。 整个过程中,我已经拿到了这个组件的所有我想要拿到的数据,它的 Props,Usage,Preview,版本信息,包名,甚至构建过程会同步发布该组件的 UMD CDN 包及 NPM 包。 接下来,就可以围绕这些数据和工具,建立和扩展很多功能和平台。 举几个栗子: 建立一个 bit 一样的,组件平台,把团队内的组件收集起来,统一在平台展示及索引 根据拿到 Props 类型信息做可视化的搭建平台,把 Props 的传参直接交给用户设置,根据不同数据类型提供不同的 Form Setter 看似组件都分布在不同的库中,却可以通过组件 cli 做统一的构建处理 非常轻松接入 微前端 框架,因为所有组件的发布构建都是标准的构建协议 通过统计组件发布次数,下载次数,关联 bug 数评估代码质量 目前在我们团队,已经使用该工具产出 100+ 的可用组件,并且发布组件已经成功接入到我们已有的可视化编辑器中。 看一眼结合可视化设置面板后的效果吧: 我发现只要实现过程中,没有给开发者带来太多的工作量,又能带来实时可以看到的效果,开发者会很乐意为那些 Props 做一番解释和修饰 。 我们团队目前产出的组件看起来一片通透,整齐明了。 我是一个热爱生活的前端工程师!Yooh!
一 修复版《士兵突击》,帧享 60 帧的《重生》,你看了吗? 回想 10 年前看士兵突击的时候,29 寸电视机绝对是“大电视”。但是今天 40 寸、60 寸已经非常普遍,甚至很多家庭直接使用投影仪看剧。尺寸越来越大,我们对清晰度的要求就越高。 如何让视频更清晰?今年年初优酷联合内容制作侧、终端消费侧的产业链合作伙伴共同推出了一套超高清解决方案——帧享,从内容创作、超高清重制、终端渲染等各个环节严格把关, 精益求精,真正呈现出 “每一帧,都享受”的高水准视听效果。 《重生》是优酷重磅打造的悬疑题材热剧,很多的镜头和情节处理都非常适合叠加我们的高帧率和声场技术,所以,我们通过视频超分辨率、视频插帧、SDR 转 HDR,立体声增强等技术手段,让画面细节更细腻、让动态场景更顺滑,让声场更立体,真正做到身临其境的超爽视听体验,最重要的实现普惠,让千元安卓机也同样流畅看高清。 大家都知道,优酷创立的时间很早,视频库中有很多老视频资源,而老视频资源中存在的最普遍问题就是噪声和模糊。我们通过高清修复技术,实现批量去噪、去模糊、去划痕、去闪烁、去抖动、高帧率等能力,修补之外重新调色,用工业化方式解决老片修复问题。2006 年单机工作室修复一部两小时左右的电影需要数十天甚至上百天的时间,现在通过异构计算平台,修复一部经典电影到 4K 仅要 1 小时。比如 80 后的集体记忆:还珠格格、士兵突击、家有儿女等都在修复后换新颜。 二 用优酷追剧为什么不卡? 视频高清了,如何不卡顿?视频清晰度的选择那么多,如何选择刚刚好的模式?从地跌的“移动”网络到家的 Wi-Fi 稳定网络,如何能不麻烦的做转换? “智能档”要解决的关键问题就是“高清不卡”,并且自动匹配合适的清晰度,这背后是自适应码率技术的支持。码率自适应技术,并不是新技术,在学术界早已有非常多的论文。但这样一个成熟“学术”技术,在真正大规模落地过程中,遇到了很多问题和挑战: 第一, 国内用户对这一功能很陌生,甚至觉得比较“傻”; 第二, 用户评判“体验好”的标准比较主观,流畅和高清的平衡点难把握; 第三, 公开的算法框架的实际效果不理想。原因是特征纬度单薄,对实际细节考虑少。 在技术策略之外,我们主要想分享,如何将成熟的学术算法落地到工程业务场景: 第一,抓住算法框架的核心点,不要太在乎结构性,要看算法解决的核心问题的切入点,和你要解决的是不是一个问题,是不是能借鉴; 第二,与大数据有关的算法,一定要关注好数据集的质和量,结合自身业务,积累高质量的大量数据; 第三,算法效果的度量标准,结合业务场景来看,尤其是那些非标准化、不好量化的场景,避免生硬的套用已有标准,毕竟你才是对问题最了解的人; 第四,像 AB 测试、大数据 Pipeline 等工程系统能力,确实对产品技术的迭代效率提升是非常大的。 三 前方高能!基于人脸识别的跟随弹幕来了 要高清不卡,还要参与互动。在追剧时,第一好看是剧情,第二好看是弹幕,而且有些剧里弹幕甚至比剧情还要精彩,比如上过热搜的出自《东宫》的那一句“谈恋爱吗?灭你全族的那种”。正是由于这些神一般的网友频频曝出精句,让某些剧集精彩程度翻了几倍。 喜欢用优酷看视频发弹幕的同学应该已经发现,很多剧都上线了基于 AI 人脸识别的跟随弹幕,以往的普通弹幕或高级弹幕都是在播放器顶端自右向左以跑马灯式的效果展示,而这种跟随弹幕是以气泡样式挂在人物头像旁边,随着人物移动而移动。这种跟随弹幕可玩性更高,有才网友可发挥余地更大。 结合人物动作的玩法 结合人物所处场景的玩法 自编自导人物对话 从几个视频 demo 中可以看出,相比普通弹幕,这种跟随弹幕是以一种类似剧中人物的内心 OS 的方式展示出来的,与视频无割离感,更有趣更新颖更精彩,有更多玩法。 人脸跟随弹幕的架构分成算法侧、服务端、客户端三层: 首先,算法侧按每秒 25 帧的频率进行视频抽帧,对每一帧进行人脸识别,配合人脸跟踪和平滑处理,生成每一帧的人脸元数据; 其次,服务端将多个帧的人脸元数据通过降噪、防抖、合并后组合成一组组的人脸组数据,将该数据与跟随弹幕数据一起下发给客户端; 最后,客户端在互动 SDK 中将每组人脸数据生成一个脚本,脚本中完成弹幕跟随该人脸轨迹的移动而移动。 剧中的人脸数据如果只应用在跟随弹幕中就大材小用了,下一步我们准备把带有人脸数据和人体数据的脚本做为基本脚本,后面除了跟随弹幕脚本,还会有弹幕穿人脚本等等。后续客户端这部分架构可能会调整,方便大家通过外部注入等方式,构建自己想要的脚本。 四 让你自然美的实时直播美颜技术 看高清、发弹幕还是不过瘾?那就来直播。在经历了在家上课,在家办公之后,非常多的一线教师、职员、程序员都开启了直播之路。打开摄影头,心里有点发慌?实时直播美颜技术,让你1秒变美,而且是自然的美。 为达成人脸美颜效果,在技术上我们主要通过以下四个关键步骤来实现: 其中,脸部美型处理主要包括脸型调整和脸部器官调整,核心步骤是基于人脸关键点通过图像形变的形式来实现脸部各器官的形状调整。图像形变算法主要是局部扭曲算法和三角剖分,局部扭曲算法一般包括局部缩放、局部平移、局部旋转等,如大眼功能即可通过局部缩放来实现。三角剖分的方法则是通过对三角网顶点进行平移,再将平移后的顶点更新到对应的纹理坐标,通过 openGL 或者 D3D 进行绘制渲染,从而实现整个关联三角网的变形。具体的脸部美型效果如下图所示: 在性能方面,在 iphone 6 Plus 等中低端机型上,可实现 720p 24fps 实时人脸美颜; 在效果方面,通过对皮肤的处理,可使人脸皮肤达到白皙细腻的效果,同时主播可按照自己的喜好对脸部的任意器官进行调整。 目前人脸美颜功能已在来疯直播(移动端和 PC 端)、优来播移动端及淘宝直播 PC 端落地,来疯移动端主播日均开播人数实现一倍增长。具体的人脸美颜效果如下图所示: 五 优酷酷看体验:你猜老胡是卧底吗? 细心的观众已经发现,优酷视频中出现了很多有人情味的“黑科技“,比如百科 tips、角色伴侣、剧情竞猜等,让用户“边看剧边互动”,这就是优酷的酷看模式。酷看模式在移动端采用了多路流的同屏展示、智能平滑切换、精准同步和动态化渲染等技术。其中动态化渲染、子母屏和多路流同步播放是酷看模式在端侧的核心能力,能够做到多路流、多机位视频帧级同步播放。 六 不知道看什么?多模态搜索来帮忙 与中午吃什么一样伤脑筋,用户打开视频平台,常常面临一个难题是 “不知道看什么、不知道如何搜索”,针对这个痛点,文娱人工智能平台提出了基于多轮对话式搜索系统。 交互式搜索系统采用模块化的设计思路,按照分层逻辑结构,分为应用技术层、核心技术层和基础数据层。应用技术层主要包括是自然语言理解(NLU)和对话技术,其中 NLU 包括意图理解(Intent Understanding)技术和成分分析((Slot Filling)技术;对话技术包括对话管理(DM)以及对话生成(NLG)。核心技术层包括知识图谱(Knowledge Graph)的构建和推理应用。基础数据层是基于视觉技术的智能媒资库。 用户在视频这个垂直领域,意图和属性槽相对比较明确,整体以有限状态机的方法为基础,基础动作迁移状态以人工设计动作为主;模型的方法作为泛化能力,解决不确定场景的理解。 系统对话流程如下: 用户说“我想看类似宫心计的电视剧”,系统通过语音识别(ASR)和自然语言理解(NLU)技术理解分析用户想看‘宫斗剧’,通过检索反馈给用户‘宫斗剧’相关电视剧,并通过自然语言生成(NLG)技术主动和用户作进一步的交互,得到用户想看‘孙俪’主演的需求后,系统基于多轮对话管理(DM)技术将前后两轮的用户综合理解,向搜索引擎发起再次检索实现多轮交互。 七 视频物体分割:在视频中随意“抠图” 视频物体分割(Video Object Segmentation,简称 VOS ),顾名思义就是从视频所有图像中把感兴趣的物体区域完整的分割出来。为了方便大家的理解,先给出一个我们自己的视频物体分割的结果。 阿里文娱摩酷实验室从 2019 年 3 月底开始从事半监督和交互式视频物体分割算法的研究。 2019 年 5 月,我们完成一版基础的半监督视频物体分割算法和交互式视频物体分割解决方案,并以此参加了 DAVIS Challenge on Video Object Segmentation 2019,在交互式视频物体分割赛道获得第四名。 我们提出的 VOS with robust tracking 策略,可以较大幅度的提高基础算法的鲁棒性。在 Davis 2017 验证集上,我们交互式视频物体分割算法 J&F @ 60s 准确率从 3 月底的 0.353 提高到 5 月初的 0.761。现在,我们的半监督视频物体分割算法也达到了 J&F = 0.763。可以说,在这个集合上我们的结果已经接近业界一流水准。一些分割结果示例如下: 八 数据反映的是真实的观看体验吗? 看基于人类视觉感知的视频体验评价体系怎么回答: 随着 4K 电视、HDR 技术、multi-view、free-viewpoint video、360 视频、虚拟现实 VR、增强现实 AR 以及混合现实 MR 的发展,Qualinet 定义的 QoE 的概念可以无差别的直接应用于这些多媒体载体上,所以在业界被广泛采用并认定其为标准定义。 为什么要做质量评价?因为用户的观看体验永远是第一位。而在整个视频从获取,处理,压缩,传输到最后解码,增强,播放的 pipeline 中,每一个阶段视频质量的评估可以指导和优化相对应的算法实现,进而实现每一个阶段算法效果的提升,最终导致用户观看体验的提升。这是我们的终极目标。 阿里文娱摩酷实验室依据 ITU 国际标准,搭建了自己的主观测试平台。 摩酷实验室主观测试流程 5G 的到来势必颠覆用户的观看习惯和体验。目前已经出现的新型多媒体技术,比如 Light-field Imaging, AR, VR, 360 VR, MR, High Dynamic Range (HDR), Free-viewpoint video, 以及 Autostereoscopic 3D 将会是未来 5G 时代的主流。以提高用户多维度的感知体验为目的下一代视频内容生成,视频压缩,视频增强,depth estimation, view synthesis 等技术势必需要质量评价方法来做监控。同时,这其中有可能产生的会引发观众视觉疲劳等危害身体健康的视频更需要质量评价方法去做前期评估预警。 九 竖屏看热剧如何实现? 近两年,随着竖版视频的流行和播放转化效率,用户对竖版视频的消费需求越来越旺盛。针对这一需求,优酷将基于机器视觉的视频裁剪技术应用于视频二次生产和智能封面图生成业务中,智能裁剪技术主要应用于以多人或者单人为主体的场景,我们将目标检测,跟踪,识别等技术进行创新和结合,开发了完整的视频智能裁剪技术链路,面对实际业务中的主体标定,视频帧间抖动,视频黑边填充等问题针对性的研发了算法解决方案,可以根据不同的业务场景将各算法可插拔的配置进主裁剪 pipeline 中,视频智能裁剪技术的研发给内容行业的素材自动化制作,剪辑作品的视觉效果和制作成本降低等方面都带来了大幅度的提升。 在视频智能裁剪技术链路中,我们研发了前处理模块(包含镜头切分, 画面尺寸判定,黑边检测裁剪等),主体标定模块,主体追踪模块和后处理模块(包含画质增强,字幕/logo 检测,画面内容修补等)。 目前视频智能裁剪技术生产的视频和封面图广泛应用于优酷的各个场景,我们对视频智能裁剪算法栈进行了整体性能优化,达到处理时间仅 1:2 视频时长,目前该技术累计对优酷综艺:演技派、这就是街舞、这就是灌篮;优酷剧集:陆战之王、天雷一部之春花秋月、微微一笑很倾城等百部 OGC 进行裁剪服务,裁剪后的竖版视频用于抖音,微博等外渠宣发和站内投放,同时主体标定算法服务于搜索双列封面图转竖项目,镜头平滑算法服务于弹幕人脸项目。 十 推荐如何又好又准? 如何为用户推荐真正想看的视频,这离不开对视频内容的理解,在个性化视频推荐中,为了对视频的内容进行表征,一种常用的方法是给视频打上多个标签,每个标签代表了一个视频中的主要元素。优酷过去的标签算法主要依赖于文本分析,当视频的文本元信息(标题、描述、评论等)对主题的描述不明确时,我们常常无法分析视频内容。为了解决这一问题,我们采用文本、封面图、音频、视频多种模态信息对视频进行多标签分类,大大提高了建模的准确率。从而提升推荐成功率。 多模态视频多标签分类结果示例 当然,音视频模态面临的一个严重问题是对于知识的提取能力有限,文本模态对于实体有更好的提取与推断能力。在下面的例子中,音视频模态会以更高的权重推断「古装剧」和「历史剧」,而文本模态则会推断「虎啸龙吟」与「司马懿」,最终的融合模型则可以融合两者的优势获得更完整的推断结果。
背景 1991 年,James Gosling 带领团队开始了一个叫"Oak"的项目,这个就是 Java 的前身。1995 年,Java1.0 发布。“Write once, run anywhere"这句 Java 口号想必大家耳熟能详。Java 刚开始出现的时候主要面向 Interactive Television 领域,直至后来几年的发展,当时的 SUN(后来在 2010 年被 Oracle 收购)一度想用 Java 来打造桌面的网络操作系统,取代当时如日中天的 Windows。不过 Java 后来的发展,不曾想虽未在桌面领域内取得多大的建树,出乎意料地,却在企业级应用领域开花结果,占据了如今几乎统治的地位。失之東隅,却收之桑榆。 JavaSE 开源现状 Sun 在 2006 年的 Java One 大会上,宣布 Java 技术开源,随后 2006 年底在 GPL 协议下发布 HotSpot 以及 javac,这是 Java 发展中的里程碑事件。阿里巴巴最早在 2012 签署 OCA,并参与到了 OpenJDK 的开发。 OpenJDK 是 JavaSE 开源的 Reference Implementation。在 JavaOne 2017 的 Keynote 上 (2018 年 JavaOne 被 Oracle 重命名为 CodeOne),Oracle 承诺将开源所有的 OracleJDK 里包含的商业实现功能[1]。 在 2018 年发布的 Java11, Oracle 已经让 OpenJDK 和 Oracle JDK 两者的二进制文件在功能上尽可能相互接近,尽管 OpenJDK 与 OracleJDK 两者在一些选项之间仍然存在一些差异[2]。 另外,除了 OpenJDK 这条主线,在最近的几年里,Java 基础技术的开源有愈演愈烈趋势:2017 年,IBM 将内部使用 20 多年之久的 J9 虚拟机开源,并贡献到 Eclipse Foundation, 而随后 2018 年,Oracle 开源 GraalVM 1.0,其核心包含用 Java 写的Just-in-Time compiler/Graal, SubstrateVM 以及支持多语言解释器的 Truffle 框架。各个企业开源的主要动机,想通过开源构建并受益于一个更为强大的语言生态系统。 云 + 开源结合在一起,使得普通开发者以较低的门槛获得一流工具(链)的使用和体验,任何一家企业都可以像任何大型组织一样,使用的相同技术(democratizing),这是开发者的黄金时代。 Java is Still Free: 你该选择什么样的 JDK? Java 仍然免费,但随着 OracleJDK License 变化开始转向收费,OpenJDK 会逐渐取代 OracleJDK 成为市场主流,这点也可以从 JVM 2020 生态报告中看出趋势:OracleJDK 从前一年的 70% 的开发者选择使用率降到 2020 年的 34%。 OracleJDK 收费,在客观上也加剧了 OpenJDK 生态的碎片化趋势,出现了包括 Alibaba Dragonwell 在内的多个基于 OpenJDK 的可选实现。 企业在选择使用那个 Java Vendor 的 JDK 版本时,几个方面的考虑因素可以参考: 安全与稳定:是否会及时同步上游的最新更新,包括安全补丁,关键的问题修复等。 JavaSE 标准兼容 :是否与标准 Java 兼容。 性能与效率:是否可以在问题诊断,性能调优方面提供有效的工具支持,帮助一线的开发同学高效地解决 Java 问题。在 JVM,到 JDK (Class library) 层面,是否有面向企业业务场景的优化特性,可以帮助提升资源的利用率,生产系统的稳定性等等。 快速的新技术采纳:伴随收费,Oracle 管理 Java 版本生命周期采用了 Long Term Support(LTS) 的概念,Oracle 每三年会指定一个 LTS 的 Java 版本, Java 8/11 都是 LTS 版本。大部分企业,尤其是大中型企业很难跟上 Java 每六个月一发布的节奏,像 Java 12,13 这样的 Feature Release(FR) 版本。那么问题来了,如果你选择 Stay 在 LTS 版本上,比如 Java 11,在新版本 (Java11+) 发布的 JVM/JDK 技术,是否可以在不升级的情况下,提前享受这些技术红利? 这里分享下 Alibaba Dragonwell 在这些方面的计划与思考。 Alibaba Dragonwell 是阿里巴巴内部广泛使用的 AJDK (AlibabaJDK) 的开源版本,Alibaba Dragonwell 作为基石,支撑了阿里经济体内几乎所有的 Java 业务,经过了双 11 等大促的考验。Alibaba Dragonwell 主要针对的场景是数据中心大规模 Java 应用部署情况下,Java 应用稳定性、效率以及性能的优化与提高。 2019 年 3 月阿里开源 Alibaba Dragonwll 8.0.0,我们也一直正在践行开源时候的承诺,AJDK 内部使用的特性在逐步开源。到刚刚发布 Alibaba Dragonwell 8.3.3,我们已经开源了 JWarmup,ElasticHeap,多租户,JFR 等众多功能,协程 Wisp 2.0,GCIH 等也在开源的规划上。 同时,Alibaba Dragonwell 作为 OpenJDK 的下游,每个发行版都会同步上游最新更新,包括安全更新,问题修复等,并经过阿里内部大规模的应用集群测试。 在新技术 Adoption 方面,Alibaba Dragonwell 目前发布和维护了 Java 8,11 两个 LTS 版本,阿里 JVM 团队会根据实际业务状况,移植 Java11+ 的相关功能到 Java 8 和 11 两个版本,这样 Alibaba Dragonwell 用户可以在不跟进 Java 12,13 等这些 FR 版本的情况下,提前享受这些功能带来的技术红利。 OpenJDK技术趋势 纵观 Java 技术 20 多年的发展,始终围绕着两大主题:Productivity 以及 Performance。在很多情况下,Java 在设计上 Productivity 是优于 Performance 考虑的。Java 引入的 Garbage Collector 把程序员从复杂的内存管理中解脱出来,但在另一方面 Java 应用始终困扰于 GC 暂停时间的影响。Java 基于栈式虚拟机的中间字节码设计,很好地抽象了不同平台 (Intel, ARM 等) 的差异性,同时通过 Just-in-Time (JIT) 编译技术,解决的 Java 应用 peak 性能, 但在另一方面 JIT 不可避免引入了 Warmup 的代价,正常情况下 Java 程序永远需要先 load class,解释执行,然后再到高度优化的代码执行。 如果从 JVM 视角来总结梳理下目前 OpenJDK 社区正在发生,孵化的相关技术,主要从工具,GC,编译器,以及 Runtime 四个方面进行一个主要概括: JFR/JMC Oracle 从 Java 11 开源了其之前一直作为商业功能的 JFR,JFR 是功能强大的 Java 应用问题诊断与性能剖析工具。阿里巴巴也是作为主要的贡献者,与社区包括 RedHat 等,一起将 JFR 移植到了 OpenJDK 8, 预计 2020 年 7 月即将发布的 OpenJDK 8u262 (Java8) 将会默认带有 JFR 功能,这样 Java 8 的用户可以基于这个版本免费使用 JFR 功能。 ZGC/Shandoath 无论是 Oracle 在 Java 11 发布的 ZGC,还是 RedHat 已经做了好几年的 Shandoath,都实现了 concurrent copy GC,解决 Large Heap 情况下的 GC 停机性能。ZGC 最新状态,在 9 月份即将发布的 JDK 15,ZGC 将从 Experimental 功能变为生产可用 [3] 。实际上,在 AJDK 11 上,阿里巴巴团队 JVM 团队已经做了大量 Java 11+ 到 Java 11 的 ZGC 移植工作,以及相关问题修复,2019 年双 11 和阿里数据库团队一起,让数据库应用运行在 ZGC 上,100+ GB Heap 情况下 GC 暂停时间可以保持在 <10ms 以内, 详细讨论参考[4]。 Graal 用 Java 开发的新一代 Just-in-Time 编译技术,用来替代目前 HostSot JVM 的 C1/C2 编译器,OpenJDK 上的 Ahead-of-Time (AOT) 技术也是基于 Graal 编译器开发。 Loom OpenJDK 社区协程项目,对应于 AJDK 的 Wisp 2.0 实现,详细讨论可以参考[5]。 进击的 Java:面向未来演进 2020,站在一个全新的节点上,本文也从三个大的方面 Cloud Native, AI,以及多语言生态三个方面展望下未来的发展,有些讨论本身是超越 Java 本身的。 面向 Cloud Native 的语言进化 云原生时代,软件的交付方式发生的根本性变化。以 Java 为例,在之前 Java 开发者交付的是应用本身,具体体现在以 "jar", "war" 的形式交付, 而云原生则是以 Container 为交付单位的: 在运行方面,面向 Cloud Native 的应用要求: Reactive Always Watching Extreme low memory footprint Quick boot time Java 语言作为企业计算,互联网领域的王者,拥有一致性,丰富的构建在 Java 语言之上的生态系统, 丰富的三方库,多样的 Serviceability 支持等,随着云时代应用微服务化,Serverless,这些新的架构逐渐触及到了 Java 程序速度提升的天花板 —— Java 自身的启动运行开销。 在 Cloud Native 这个新的上下文里, 我们谈论语言的进化,绝不仅仅限于运行时,编译器层面, 新的计算形态一定伴随着编程模型的变革,这涉及围绕程序语言的 Library,Framework,Tools 等一系列配套的改革。从目前业界来看,也有不少的项目正在发生:配合 GraalVM/SVM (Java 静态编译技术) 的下一代编程框架 Quarkus, Micronaut, 以及 Helidon,Quarkus 更是提出了“container first” ,他们提倡的分层的 lightweight uber-jar 的概念正是符合了 container 交付这一趋势。而 Red Hat 的 Java 团队与 OS 团队合作的"Checkpoint Restore Fast Start-up"技术 (AZul 在 JVM 技术峰会 '2019 上也提出过类似的想法) 则是在更加底层的技术栈上解决 Java 快速拉起问题。 在 Java for Cloud Native 方向,我们也开展了相关研发工作。Java 是静态语言,但是包含了大量的动态特性,包括反射,Class Loading,Bytecode Instrument (BCI) 等等,这些动态特性本质上都是违反 GraalVM/SVM 所要求的 Closed-World Assumption (CWA) 原则,这也是导致传统跑在 JVM 的 Java 应用不容易在 SVM 编译运行的主要原因。阿里巴巴 JVM 团队对 AJDK 做了静态化裁剪,务求在 Java 静/动态特性之间找到一个确定的边界,从 JDK 的层面为 Java 静态编译提供可能性。同时向上,与蚂蚁中间团队合作,定义面向静态编译的 Java 编程模型,通过编程框架来约束 - Java 应用的开发是面向静态编译友好的。我们静态编译了基于蚂蚁开源中间件 SOFAStack 构建的服务注册中心 Meta 节点应用,相较于传统 的运行在 JVM上,性能有量级的提升:服务启动时间降低了 17 倍,可执行文件大小降低了 3.4 倍,运行时内存降低了一半。详见[6]。 AI 的兴起,编程语言异构计算的新挑战 2005 年,时任 Intel CTO 的 Justin Rattner,说过 “We are at the cusp of a transition to multicore, multithreaded architectures”, 在前后的十几年中, 编程语言与编译器领域一直在努力面向 parallel architectural paradigm 做优化探索。随着 AI这些年的兴起, 不同的时间节点,相似的场景,面向 FPGA/GPU 异构计算场景,对编程语言与编译器领域提出了新的挑战。 除了传统 Compiler 诸如 IBM XL Compilers, Intel Compilers 等做的 Automatic Parallelizing 工作,在极致性能探索方面,基于多面体模型 (polytope model) 的编译优化技术作为解决程序并行化、数据局部性优化的一种手段,成为编译优化领域的研究热点。 而在 Parallel Languages 层面,对 C&C++ 开发人员,CUDA 的出现降低了 GPU 的编程门槛,但 GPU 和 CPU 两种硬件模型本质区别,导致过高的开发成本,需要学习和了解更多底层硬件细节,还更不用说更高级语言的开发语言像 Java 等所面临的底层硬件模型与高级语言之间巨大的 GAP。 在 Java 领域,最早在 JVM 技术峰会 '2014,AMD 曾经分享过他们的 Sumatra 项目,尝试实现 JVM 与 Heterogeneous System Architecture 目标硬件交互。而在最近,由 The University of Manchester 发起的 TornadoVM 项目,实现包含:一个 Just-in-Time 编译,支持从 Java bytecode 到 OpenCL 的映射,一个优化的运行时引擎,以及可以保持 Java 堆和异构设备堆内存一致性的内存管理器。TornadoVM 的目标是开发人员不需要了解 GPU 编程语言或者相关的 GPU 体系结构知识就可以编写面向异构的并行程序。TornadoVM 可以透明地运行在 AMD GPUs, NVIDIA GPUs, Intel integrated GPUs 以及 multi-core CPUs 上。 在通用 CPU 领域, OpenJDK 社区的 Vector API 项目 (Panama 的子项目),依赖 CPU 的 SIMD 指令,获得计算性能的成倍提升,Vector API 在大数据,AI 计算也有非常广的应用场景。阿里 JVM 团队把 Vector API 移植到了 AJDK 11,后续会开源到 Alibaba Dragonwell,分享下我们获得的基础性能数据: 时间 (单位: milliseconds) 越短,性能越好 Polyglot Programing,链接多语言生态 Polyglot Programming 并不是一个新的概念。在 Managed Runtime 领域, 2017 年 IBM 开源 Open Managed Runtime(OMR), 以及 2018 年 Oracle 开源 Truffle/Graal 技术。OMR 和 Graal 技术让开发人员实现一个新的语言成本大幅下降。前者 OMR 以 C、C++ 组件的形式提供了 Garbage Collection (GC), Just-in-Time (JIT) 以及 Reliability, availability and serviceability (RAS,工具)等, 开发人员可以依赖这些组件,通过 'glue' 的方式基于这些组件实现自己的高性能语言。而后者 Truffle/Graal, Truffle 是一个依赖 AST parser 实现新的语言的 Java 框架,本质上是将你的新的语言映射到 JVM 世界。不同于 Scala, JRuby 这些围绕 JVM 生态本身构建的语言,他们本质是还是 Java, 无论是 OMR, 还是 Truffle/Graal,他们都提供了生产级的 GC,JIT,以及 RAS 服务支持,新开发的语言完全不需要再重新实现这些底层技术。 从业界来看,面向特定领域的 Domain Specific Language (DSL) 语言已经有向这些技术迁移的趋势,高盛正在与 Graal 社区合作,把他们的 DSL 迁移到 Graal 上。另外 Ruby/OMR, Python/Graal, JS/Graal,WASM/Graal 等这些真正链接不同语言生态的项目,也正在迅速发展起来。 回到 AJDK, Graal 已经在 AJDK 8 开始支持, JS/Graal 这样成熟的技术,已经在阿里内部业务上线。 最后 Java 是一项二十多年前被发明出来的技术,她历经磨难,几易其主,但却历久弥新。这篇报告旨在为 Java 的开发者们梳理下目前的 Java 技术现状,以及讨论在云,AI 等这些重要领域内 Java 技术的演进趋势。在介绍的相关部分,我们也穿插了阿里的一些工程实践。作为世界上最大的 Java 用户之一,我们也一直在探索把前沿的 Java 技术,通过在阿里丰富的业务场景的试验,真正把这些技术应用于真实的生产环境。我们也非常乐于分享和贡献 Java 领域的经验、实践与技术洞见,共同促进 Java 的发展。 参考 [1]https://www.infoq.com/news/2017/10/javaone-opening/[2]https://www.oracle.com/technetwork/java/javase/11-relnote-issues-5012449.html#Diffs[3]https://openjdk.java.net/jeps/377[4]https://mp.weixin.qq.com/s/FQpvT5wIy9xwhX2jHMU7aw [5]https://mp.weixin.qq.com/s/K1us6aH-gjHsWGhQ3SulFg[6]https://www.infoq.cn/article/uzHpEbpMwiYd85jYslka
潜力修炼一年之久的《Java 开发手册(泰山版)》今天发布!此次共计新增 34 条规约,修改描述 90 处,其中错误码规则更是第一次提出完整的解决方案,大家参考错误码示例表,欢迎大家下载与阅读! PC端下载地址:https://developer.aliyun.com/topic/download?id=12 也可点击下方链接直接下载: 点击立即下载:《Java 开发手册(泰山版)》 “荡胸生层云,决眦入归鸟”,《Java 开发手册(泰山版)》正式和大家伙见面了!秉承着“码出高效,码出质量”的一贯愿景,泰山小哥跳着欢快的步伐向大家走来了,以下是他的独白: 自华山版现身武林以来,大家都叫泰山泰山快现身,但是一开始我是拒绝的,不能因为你说来,我就马上来,直到我遇到了 stream.Collectors 类下 toMap() 方法的坑,转化逻辑对我等键盘侠来说应该是手到擒来,谁知一脚深坑踩进了无底洞,两个相同 key 的转化时就已经撂挑子了。怎奈祸不单行,toMap() 大哥在进行值为 null 的转化逻辑时突然的空指针异常,又一次给了我沉痛一击,于是我悔悟,我惶恐,我连夜买着站票赶来见大家,欲知详情如何,请速速下载《Java 开发手册(泰山版)》。 错误码一统中原 夜色中的火车驶过泰安站,我朦胧欲睡时,边上的 HTTP 小哥传来一个声音“嘿,兄弟,前方道口 404 信号灯了”,我一脸懵逼状,然后他就开始嘲笑我们后端程序之间纷繁复杂又各自为政的错误码定义,这让我十分没有面子。但是今天!这样局面就要被终结!你还在为杂乱的错误码而烦恼么?统一错误码,就是统一度量衡,为你的应用与服务的稳定保驾护航,烦恼清空,快乐回家。 泰山版新近出炉的错误码具有快速溯源、简单易记、沟通标准化三大优势。错误码为字符串类型,共 5 位,分成两个部分:错误产生来源和四位数字编号。错误产生来源分为A/B/C,以当前代码运行视角来进行判定: A 表示错误来源于用户,比如请求参数错误,用户安装版本过低等问题。 B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题。 C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题。 优秀的错误码可以迅速知道他们是怎么来滴,从哪儿来滴,来干啥滴。同时俺们的错误码具有三级结构,分为一级宏观错误码、二级宏观错误码、三级宏观错误码,你想要的,泰山我全部都有。 闰年多一天多一丝烦恼 蔡国庆叔叔曾唱过“一年有三百六十五个日出,我送你三百六十五个祝福”,当闰年的时候,我们就会有一天收不到蔡叔叔的祝福了。虽前有“千年虫”之鉴,但好多朋友却开始在代码中玩儿起了“闰年虫”,一个写死为 365 天的年份天数,在闰年里让大家尝尽了悲伤痛楚,比如,缓存有效期 = 3652460*60 秒,结果提前一天所有缓存同时失效,高并发访问时数据库就跪了。对此,泰山我不禁要问了,LocalDate 它不香么,你要的年份、天数,它通通拥有,听哥的,以后这坑啊,咱不踩。 表的别名 “在这表的世界里,为什么我不配拥有一个简洁的江湖头衔?”SQL 老弟常常对我说这日子他过得憋屈。 “泰山大哥,你知道么?我在一个多表操作的 SQL 中大吼一声行者孙,竟然活生生蹦出了俩,劈头盖脸那就是给我一顿胖揍。”多表连接操作的时候,当一个相同的列名出现在多个表的时候就是会如此,多个行者孙打架斗殴的场面屡见不鲜。我们能做的就是用表的别名(或表名)来给这些小猴子般的操作列加上限定,不然花果山的猴子就会跟果子岭的猴子起冲突,让我等键盘侠沦落于无穷的 BUG 地狱中,不得超脱。 代码无空行憋得慌 遥望东海,当年我也是代码届一蛟龙,可上九天揽月,可下五洋抓鳖,但是有些仁兄的百余行一站到底式的瀑布式写法还是让我玉面小蛟龙感受到了窒息的滋味。代码和人一样,憋久了也是要换气的,未来你的代码要被很多人阅读和修改,因此,不要让阅读者感到憋闷和窒息,也是好的代码应该做的事情,当某个方法的代码行数超过 10 行时,return / throw 等中断逻辑的右大括号后加一个空行。合适的换行能够让阅读者获得喘息的机会,更能让代码之间的组织方式更加的协调。 三目运算符的任性 三目运算符这个外表看起来老实巴交的孩子却不是个好相处之辈,竟然也玩起了儿时类型转化的游戏,让我等好汉折了腰。这是咋回事儿呢?原来在三目运算符中: condition ? 表达式 1 : 表达式 2 表达式 1 和 2 在涉及算术计算或数据类型转换时,会触发自动拆箱。拆箱操作本来没有问题,但是当其中的操作数为 null 值时,一个大大的 NPE 就砸到了我滴脸上,泰山我不能让兄弟们前赴后继踩进深坑里,跌得鼻青脸肿,下面就复原下现场,大家回家思考一下为啥抛出 NPE 异常: Integer a = 1; Integer b = 2; Integer c = null; Boolean flag = false; Integer result = (flag ? a * b : c); 寄语未来 本次泰山版是一次全新的革新,涉及到集合转化函数式接口中的深坑、年份天数写死所带来的问题、SQL 多表操作未加表名对字段限制惨案、以及统一错误码能够为我们带来的巨大好处,更多的细节与精彩我们可以在泰山版手册中细细品读。这些年来我们同全球有着相同极致追求的开发者一起,不断的完善和迭代着 Java 开发手册,用技术情怀去做一件帮助所有 Java 开发者们避免踩坑。因为相信,所以看见,未来我们将一起继续为全球的 Java 开发者们服务,致力于消灭各类程序中的疑难病症。 手册中每一条规约的背后都有一段或悲伤或遗憾的故事,将前人的历史经验沉淀下来,让后面的人的路走的更平坦。我们于小中见大,于细微处见真章。“见渺小之物必细查其纹理”,可能也是我辈技术人独有的浪漫,small is powerful, small is beautiful。 再次感谢各位全球开发者,本次《Java 开发手册(泰山版)》共计新增 34 条规约,修改描述 90 处,其中错误码规则更是第一次提出完整的解决方案,欢迎大家下载与阅读。下一个版本“嵩山版”,咱们少林寺见真功夫。 请戳这里下载历史开发手册!同时完成七天打卡还有机会赢社区周边!https://developer.aliyun.com/topic/java2020
虽然目前越来越多的国产优秀技术产品走出了国门,但是对于众领域的开发者来说,对国外的各种基础资源依赖还是非常的强。所以,一些网络基本技能一直都是我们需要掌握的,然而速度和稳定性问题一直都在困扰着大家。 所以,今天就给众 Java 开发者推荐两个提速神器: Maven 的国内镜像 Spring 的国内脚手架 Maven 的国内镜像 官网地址:http://maven.aliyun.com/ 对于每一个 Java 开发来说,拉不到依赖的情况一定都有碰到过。所以,强烈建议大家在安装开发环境的时候,就把阿里的国内镜像配置上,省去日后各种不必要的麻烦。 配置方法很简单,只需要找到 Maven 的 setting.xml 文件,然后在镜像配置中加入阿里云的仓库配置,具体如下: <mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors> Spring 的国内脚手架 官网地址:https://start.aliyun.com/ Spring 的国内脚手架是近日阿里巴巴中间件发布不久的定制版 Spring Initializr,归功于亲切的中文与流畅的速度,被广大开发者关注与热传。 使用 Spring 的 Java 开发者可以很方便的通过该界面创建一个基础的 Spring Boot 项目,并引入你所需要的各种 Spring Boot Starter 组件或者 Spring Cloud 的各种功能。 除此之外,其实我们也可以将它用于 IDEA 的新建工程中,来提速原来的创建方式。操作很简单,只需要下面两步: 第一步:先从菜单中选择 “File” -> “New” -> “Project...” 第二步:左侧项目类型中选择 “Spring Initializr”,右侧可以看到模式使用的是 Spring 官方的脚手架地址,我们只需要选择 custom,然后填入 https://start.aliyun.com/,点击 “Next”! 大功告成!在后续的创建指引中与原有的操作一模一样,但是项目的组件的元数据获取与生成过程已经不再通过远在国外的官网地址,而改为了离我们更近、服务更好的 start.aliyun.com 了。 这些环境都搞定了,是不是很爽?爽了那么就可以开始学习了!
一 背景 客户端日常开发中经常遇到各种低效痛点,比如开发 UI 界面时,开发、设计同学走查 UI 基本靠眼,不易于发现问题;设计同学想修改一个 UI 元素,除非是原开发者,其他同学不知道相关的 UI 类和 UI 布局,定位代码费时费力;再如出现 bug 时无法在 bug 现场获取数据定位问题,debug 依赖电脑联调,缺少独立便捷的 debug 工具。 对此优酷开发了啄幕鸟 iOS 提效工具平台,在端上整合各种高效 debug 工具,不依赖电脑联调,直接获取 App 运行时数据,快速定位问题,提高开发测试效率。 啄幕鸟项目现已开源,欢迎接入,共建共享。 二 啄幕鸟简介 啄幕鸟,即手机屏幕上的啄木鸟,森林里的啄木鸟抓虫子,优酷的啄幕鸟抓 bug。 啄幕鸟提效工具平台集合了 UI 检查、对象查看、方法监听、po 命令执行、JSON 抓包等十多个开发工具,不依赖电脑联调,直接获取运行时数据,快速定位 bug,简便易用,零侵入、零依赖、易接入、易扩展。 啄幕鸟架构图 啄幕鸟界面截图 1 啄幕鸟架构 啄幕鸟使用插件化架构,每个工具作为插件接入到啄幕鸟基础服务当中,各个插件相互独立,同时支持外部插件注册、定制等,啄幕鸟还提供了一些通用功能模块,如系统分享面板、屏幕日志等,方便各插件使用。 2 基础服务 基础服务包括生命周期管理,插件加载、注册、运行,资源管理,本地化等基础能力。 3 公共模块 啄幕鸟公共模块包括分享面板、图文预览、屏幕折线图、屏幕日志四部分,随着工具开发,越来越多的通用能力会总结到公共模块中。 1)分享面板封装了系统分享功能,方便各个工具导出信息,啄幕鸟中的文本、图片皆支持分享面板导出。 2)图文预览用以全屏查看文本、图片。 3)屏幕折线图方便插件直观的显示数据,啄幕鸟中性能插件使用了屏幕折线图显示性能数据。 4)屏幕日志模块方便各插件在 App 内显示日志、接受用户输入,屏幕日志界面大小可调,支持日志显示、用户输入、搜索、正则表达式过滤等功能,系统信息工具直接使用了屏幕日志显示信息: 系统信息插件中直接使用了屏幕日志显示信息 三 主要工具介绍 1 UI 检查工具 UI 检查包含控件拾取和测距条两个工具,控件拾取会根据手指在屏幕上的点击坐标,递归遍历 View 层级,获取包含触点坐标的最靠前的 UI 控件,并显示控件的类名、 frame、字体、图片 URL 等信息,方便地获取、导出运行时数据;测距条工具会在屏幕上添加大小、位置可控的 View 作为测量标尺,作为控件拾取的补充,对于某些不能通过控件拾取查看的大小间距,如行间距等,可以使用测距条测量。 控件拾取、测距条截图 2 In-App-Debug 工具 iOS 开发主要使用 LLDB 的断点、指令等进行 Debug,依赖电脑联调,In-App-Debug 即不依赖电脑,使用 App 内的 debug 工具获取运行时数据,帮助定位问题,啄幕鸟提供了多种查看运行时数据的方式:对象查看、方法监听、po 命令和 JSON 抓包,帮助在 bug 现场定位问题,In-App-Debug 工具都利用了 objective-C 的运行时特性,将运行时特性工具化,形成一能力多功能的开发工具,如方法监听既可以用于定位 bug,也可以监听网络方法抓取网络数据,其他工具也都有多种用途,可以极大提高 debug 效率。 1)对象查看 App 中所有的对象通过继承、代理、属性等关系,可以看作一个或多个连通图。从一个对象开始,可以利用运行时特性获取连通图里任一个对象的属性、成员变量,获取运行时数据,以定位问题。双击控件拾取的信息区即可打开对象查看,对象查看会显示拾取对象的属性、成员变量列表,点击对象即可查看它的属性,层层查找即可查看到每一个相关的对象,并可以通过命令读取对象 key-path-value、执行 po 命令等。 查看某 UILabel 对象、使用 k 命令获取圆角值 UI 检查、对象查看 Demo 2)方法监听 对象查看提供了获取 App 静态数据的方式,而方法监听提供了获取动态数据的方式,输入监听命令即可监听任意 OC 方法的调用,输出调用参数和返回值,用以查看代码逻辑是否正常执行,关键方法是否调用,监听网络方法以在 App 内抓包等。 监听设置自动息屏方法并显示日志 方法监听 Demo 方法监听利用了 OC 的消息转发机制,通过 hook 监听对象消息转发的相关方法,最后可以在 ykwoodpecker_forwardInvocation 方法中收到封装了被监听方法调用参数和返回值的 NSInvocation,即可获取 target、selector 等参数数组,根据参数编码规则解析相应的参数,最后修改 NSInvocation 的 selector 为指向原方法的 ykwoodpecker_selector,即可调用原方法获取返回值,输出日志。 使用方法监听需要输入类名、方法名、keypath 等参数,故采用了命令行进行交互,扩展性好,配合命令配置,输入也较方便。命令使用统一格式,<命令名缩写><空格><命令参数><空格><命令参数>,如监听命令 L className methodName,KVC 取值命令 k keyPath,调用栈查看命令 k callStack,所有命令详见工程 README。为方便输入,命令可通过后台配置,一键输入,命令配置采用如下格式的 JSON,可在啄幕鸟初始化时指定配置 JSON 的获取地址,推荐在 https://github.com/ZimWoodpecker/WoodpeckerCmdSource 工程中建立配置,方便命令共享。 后台命令配置格式 3)po 命令 po 命令是 iOS 开发中最常用的 debug 命令,po 命令工具会解析输入字符串,获取输入的方法名、参数等,动态调用所输入命令,并显示返回信息。 App 中执行 po 命令 4)JSON 抓包 使用方法监听抓包略有不便,数据量较大时会引起卡顿,因此提供了更方便的 JSON 抓包工具,通过监听 NSJSONSerialization 的 JSON 解析方法实现抓包。 JSON 抓包工具截图 四 更多功能 随着日常使用,啄幕鸟中增加了更多功能: 系统信息:查看系统名称、版本、屏幕、UA 等信息,支持添加业务方信息。 SandBox:查看沙盒文件,导出文件等。 Defaults:查看、新增、删除 User Defaults。 清除数据:清除所有沙盒数据,包括 User Default。 UI 对比:支持将设计图导入到 App 中进行对比,并可画线、标注需修改的地方,方便 UI 走查。 查看图片资源、Bundle 资源:查看 App 中的图片资源与 Bundle 目录内容。 Crash:查看本地 crash 日志。 触点显示:显示手指触控,方面录屏时显示触控操作。 性能插件:查看 CPU、内存占用率,帧率,网络流量等。 其他业务方注册的插件:环境切换、埋点、实验、调试工具等。 啄幕鸟工具插件截图 五 扩展开发 啄幕鸟使用插件化架构,新插件扩展方便,部分插件也支持功能扩展。一个类只需实现插件协议方法即可注册为插件,可定制插件分组、分组显示位置、插件名称、icon、插件显示位置等,简单方便,高可定制。控件拾取、系统信息等插件也支持功能扩展,通过监听相关系统通知可以获取拾取到的 View 等事件,并显示自定义信息,具体参见工程 README。 六 快速接入 啄幕鸟推出以来深受欢迎,已成为产品、设计、开发、测试日常互怼居家旅行必备之工具,啄幕鸟不依赖优酷、阿里或其它第三方库和数据,主要功能皆通过系统 API 或 hook 方式实现,没有使用 + load / initialize 方法,不开启啄幕鸟不会执行任何代码,简单安全,零侵入,零依赖。 啄幕鸟现已开源,是 AIOSO 的子项目之一,支持 iOS 8.0 及以上,阿里巴巴集团内外使用pod YKWoodpecker 即可接入。 Get Started: pod 'YKWoodpecker' 一行代码打开啄幕鸟: #import "YKWoodpecker.h" // 显示啄幕鸟入口 [[YKWoodpeckerManager shareInstance] show]; 更多介绍详见工程 README,Github 地址:https://github.com/alibaba/youku-sdk-tool-woodpecker 。
我认为,热爱是一直努力的理由。 四个体感 体感一 :学生时代正确的定位,会让进入职场的自己事半功倍 1998 - 2005 年,我在浙江大学就读电子工程,因为自己对电子工程的热爱和成绩优异,我本科保送研究生,研究方向是电路与系统,简单点就是硬件 + 嵌入式软件开发,因此自己天生就跨了学科,既有硬件电路设计,又有嵌入式软件开发。2005 年硕士毕业后,来到上海,加入了华为公司,从事基站硬件开发,也就是电子工程师一枚,负责单板设计,以及基站日常 bug 维护。从自己的经历发现,一个工科生掌握一两门不同学科的技能是可以让自己胜任更多有挑战的工作,也可以更容易脱颖而出。在华为,我就一边负责硬件单板设计,一边还参与其他部门的微码(类似汇编级别编程语言)开发,既做了硬件,也懂软件,在职业生涯的早期就给自己定位为综合性人才,为后续自己的技术发展奠定了很好的基础。 体感二:清楚了解自己的目标,并为之努力 2006 年 9 月份,我很自然的提出了离职,因为我一直都觉得自己不喜欢做一颗螺丝钉,华为的通信系统项目少则 100 人,多则 1000 人联合开发,每个人在其中都是一颗螺丝钉,我觉得这不是我想要的状态。其实现在看来,这样的想法很幼稚,且不说大系统中做好一颗螺丝钉是对自己的历练,就说任何小公司的小项目也都是需要团队合作,都是需要先做好螺丝钉,才有机会成长为整个项目的 Leader 。因此项目不论大小,关键是自己在其中是否有成长,是否可以学习到新知识?在我离职后,华为给在职员工派发了股份,如果一直待到现在,年收入也是让人羡慕的,这次和华为的擦肩而过,估计今生也不会再有机会和它有缘了,只能祝福还在华为的同事们。 体感三:创业是一种经历,成功是在于克服了多少困难,而不是取得了什么结果 从华为离职后,我加盟了一家手机设计公司——希姆通,它和龙旗并称为当时功能手机(Feature Phone)的黄埔军校,其中有大量的人离职后都开了自己的公司,做起了 boss ,也取得了丰厚的回报和巨大的成功。我当时加入希姆通的部门是模块事业部,做的是 IoT(物联网)通讯模组,主要聚焦远距离通信技术,比如 2G,3G 等等。所以其实我和功能手机的风口很近,但是不在风口中,我们部门不温不火的发展着,我也不温不火的一直待着,从工程师一路做到了硬件总监, 6 年时光也就匆匆过去了,回头看,自己的成长并不多,自己的技术视野,特别是商业感觉都没有特别进步,因此我有点蠢蠢欲动,在 2012 年 9 月份,我终于下定决心离职创业,并拿着公司给的 N+1 ,忐忑的下海创业,其实这个时候,我手头只有一个蓝牙防丢器的 demo ,对如何商业化运作,没有任何经验。我只好拿着自己精心制作的 PPT 寻找 VC (风险投资),参加了很多路演,一直没有拿到投资,而这个时候,我已经待业 1 个月,我突然有点对自己前途不知何去何从的感觉。很幸运的是,一直和自己联络了 3 个月的一家风投公司最终投资了我,我也就开始了注册公司,招聘合伙人的工作,其实合伙人也就是项目早期的合作伙伴和同学这些人,当时没有考虑商业化,也没有经验,作为老板,“找人,找钱,找方向”,犯了两个错误,找人的时候没有找营销市场合伙人,这个为后来的生意拓展留下了祸根,毕竟一个公司是要靠销售获取利润的,后来自己勉为其难做了销售,虽然也做到了不错的销售业绩,但是和科班专业的销售合伙人相比,还是欠点火候。还有一个错误是找方向,离职希姆通时候自己捣鼓的蓝牙防丢器,其实属于智能硬件,但是这个产品比较鸡肋,不是一个刚需产品,因此这个方向并不能成就大的事业,所以方向选择格局不够,很容易碰到事业的天花板,虽然我们在国内是做的最好的,但是也不能改变鸡肋产品的结局,始终无法使公司步入正轨。如果当时选择和蓝牙相似的 WiFi (都是 2.4G 频段的无线通信技术)创业,现在也可能就是另一个博联(比我们晚半年时间的另一家 WiFi 智能插座创业公司,我和他们 boss 也是朋友,还曾经一起路演过,后来他们多次融资,日子过得挺红火的)。 经过 5 年多的折腾,中间也转换方向,做过技术外包, IoT 教育,但是折腾来折腾去总是没有找到一个有“比较优势”的方向,我也只能在不断关公司,开公司中度过了失败的 5 年多的时间,最终在 2017 年底,我打算结束创业,开始新的职业生涯。 体感四 :不断地学习才能不停的进步 2017 年圣诞节,是一个周一,我满怀创业失败后的惆怅和对互联网公司的好奇进入了阿里巴巴西溪园区报道,职业生涯从来没有想象过会有互联网公司这一站,记得一个多月前,面试官问我,“你之前的职业生涯为什么从来没有考虑过互联网公司”,当时的我深思了 30 秒,对啊,我为什么从来没有考虑过加入互联网公司呢?大学毕业, 2005 年 - 2006 年,我在华为工作了一年半,做基站硬件+嵌入式开发。2006 年 - 2012 年,我安安静静的在手机 IDH (方案设计公司)竟然呆了 6 年,我自己都好奇,我这么不安分的人怎么会在一家公司待了 6 年。2012 年,我下海创业,当时是蓝牙 BLE 和智能硬件兴起的时间节点,我赶上了这波风口,可惜没有把握住,2017 年底,我灰心地结束了创业公司的所有业务。在 2016 到 2017 的一年时间里,我看了一年的书,对,是一年的书,至少 100 多本,涵盖了历史,地理,数学,人工智能,经济,股票,管理,也花了一年时间思考了人生,事业,未来。好了,不扯了,个人职业生涯确实和互联网公司没有交集,唯一的交集是,自己的创新小公司曾经拿到了奇虎 360 公司的投资而已,也得到了 360 的 ZHY 的会见,促膝长谈过 4 回。 刚到阿里,头衔高级技术专家,负责蓝牙 mesh 整个方案的技术落地。一个人,一杆枪,和产品经理 XJ 搭档,开始宏大的 IoT 蓝牙 mesh 商业化落地,是一个 0 到 1 的项目,对我来说,有挑战,但是不难,因为过去的 5 年多我就是一直在 0 到 1 的路途中,所以已经习惯了。说到挑战,不得不说在这么大的平台,如何开展工作,我似乎有点陌生,就像入职 6 个月后,我的一个产品经理朋友 XC 评价我说,“崮德,你这 5 年多的创业经历让你缺失了一大段大公司工作的经验”,确实,刚入职 1 个月,我对如何开展工作还是有点蒙圈。当然作为一个连续创业者,这些挑战也是一次学习的过程。因此我先整理了一个工作规划,也就是我觉得蓝牙 mesh 技术落地需要配置哪些技术人才,比如硬件工程师,驱动工程师, App 工程师,需要多少预算,等等。一周时间后,我做了一个方案,比如蓝牙 mesh 在天猫精灵生态的位置和意义,蓝牙 mesh 模块如何做,要多少人,费用多少等等,然后又花了一周时间和领导们汇报了自己的想法,并和跨部门的领导也做了沟通,算是对齐了目标。 接下来就是蓝牙 mesh 方案细节输出,我静下心来,在蓝牙官网下载了蓝牙 mesh 的协议,一看有 3 份文档,大约 600 多页,英语版本。看英语版本资料是我强项,但是我知道这个过程必须连贯,绝对不能被经常打断,因此我花了 2 周时间,每天上班就是看文档,其他什么都不做,喝水,上厕所,看文档,做摘要,然后还是喝水,上厕所,看文档,做摘要。2 周后,我已经吸收整份协议的 80% 精华,根据 28 原则,剩下的 20% 可能要占用 80% 的时间才能完成,因此现在继续花时间性价比不高,可以后续用到再研究,但是不影响对整个技术架构的理解。这个时候,我开始了软件架构的设计,输出相应的架构文档,并规划了未来新员工的工作。 开展工作的一大原则,是需要学会借力打力,不要什么都自己做,要善于利用外部资源。因此在这次蓝牙 mesh 软件架构的输出中,我发现和我们合作的芯片公司LFK,有一个技术大拿 TN ,更早接触了蓝牙 mesh 技术(蓝牙 SIG 组织 2017 年中推出该技术),比我更了解该技术,因此我在做技术架构的时候,把 20% 不懂的部分,通过和 TN 的不断邮件,电话,钉钉沟通,用了 2 周的时间把这些知识点补足(还是 28 原则,大概补足了 20% 中的 80% ,剩下的 4% 在后续的工作中不断补充),也完成了技术架构的初稿,并发给 TN 审核,并得到了很好的反馈。 说句题外话,通过以上的描述,如果作为职场新人,需要掌握一个新技术,不管是哪个行业的新技术,一定要用 20% 的时间快速掌握 80% 的知识点,然后就可以开展工作,后续的 20% 知识点,可能会占用 80% 的时间掌握,可以拆散打乱到平时工作中继续学习,或者请教专业人士慢慢补足,没有必要为了追求完美持续学习,而是要转身开始开展工作或者开始其他方向的学习。而在 15 年的职场经历中,我深刻的领悟到危机感、结构化思考、演讲力、商业化思考的重要性。 关于危机感 这么多年的从业经验,让我不得不思考自己是如何在打工和创业的环境中一直存活下来,最近也和自己的一个下属聊他手头一个重要项目,谈到了其中一个 issue 该如何解决,突然让我很有感想,我和他说,我觉得自己一直能够存活下来,应该归功于自己的危机感,这个危机感不是无病呻吟地焦虑,而是一种未雨绸缪,是一种全局观的思考方式,一种能在万花丛中找到那点绿的思考方式。 我觉得一个工程师,一定要敬畏手头的工作,因为如果自己的一个疏忽,可能就会导致一个硬件或者软件事故,影响很多很多的用户。但是工程师的工作是千头万绪,有很多很多问题需要解决,那该怎么找到其中的关键点呢?我觉得吧,一定要时不时的停下手头的工作,环顾下自己的工作,看看里面哪些问题是只要自己个人投入足够时间就可以解决的,哪些问题是需要多方协调,需要其他资源投入才可以解决的,特别是对于第二种情况,一定要有危机感,一定要有警惕性和前瞻性,因为这种问题,可能就是我说的关键点,需要提高优先级来解决。 或者我们经常会听到这样的悖论,我们每天有很多重要而紧急的事情,我的时间管理该怎么做?其实,如果你有很好的危机感,或者前瞻性,及时规划重要而不紧急的事情,那么你接下来就不太容易碰到重要而紧急的事情。比如阿里有年度考评,需要做很多 PPT 和自评,这些事情平时做肯定是重要而不紧急,平时多收集汇报的素材,并记录下来,那么等到年度考评的事情,可以快速和轻松的做好。如果平时不收集素材,到了年度考评的时候,你就要花很多时间回忆自己一年都有哪些亮点,该怎么总结自己的一年工作,这样就把一个重要而不紧急的事情变成一个重要而紧急的事情。一个优秀工程师,有一个很重要的标准,那就是他不太有重要而紧急的事情,每天大部分时间,他都在处理重要而不紧急的事情,这样他的工作和学习都是非常的闲庭信步。打个比方,类似下棋走一步看三步,这个刚开始很难,只要你对项目的方方面面多思考,对技术保持前瞻性思考,你就慢慢会有这种能力。 还有,就是可以在睡觉前,上班途中,下班路上,多回顾下自己平时的工作,移除掉细枝末节,然后把几个大的功能块提出来好好思考下,他们互相有什么关联,需要什么前置工作,会调用哪些资源,有什么表面风险,注意,我这里提到的是表面风险,因为,深层次风险是很难仅仅通过大脑思考就能暴露的,他需要在你具体工作中才会暴露,这个也就是凸显了这样的思考重要性,因为你通过思考,可以有强烈危机感,可以 sense 到表面风险,然后 focus 在这些问题解决中,不断挖掘深层次风险,及早排除地雷。 我和很多人说过,一个优秀工程师和一个平庸工程师的最大区别就是,谁有火眼金睛及时识别到问题在哪里,危机在哪里,及时提前准备解决这些危机,而不是在问题发生后,不知所措,两眼一抹黑,自乱阵脚,毫无解决思路。 有人会说,我就是没有危机感,怎么解决呢?我觉得吧,你可以多看看一些美剧,其中很多狗血桥段,会不断给你制造危机场面,不断让你 Suprised ,不断让你觉得桥段的设计巧妙,不断让你看到日常生活中不太看到的异常情况。慢慢的,你会越来越有危机感。 结构化思考 阿里巴巴的产品经理特别喜欢用 Xmind 来做脑图,从这个工作工具的频繁使用可以看出,阿里巴巴人员是喜欢做结构化思考的,也就是自顶向下或者自底向上的思考,或者 MECE ( Mutually Exclusive Collectively Exhaustive, 中文意思是“相互独立,完全穷尽”)思考。刚开始我也不习惯而且也不屑于这样思考问题,我的习惯是跳跃式思考,而且经常沾沾自喜,觉得这样的人才有创造力,后来通过 3 个月的接触,学习,阅读相关资料,我慢慢觉得脑图确实可以补充跳跃式思考的漏洞,特别是考虑问题更加全面了,这个思考方法不是否定跳跃式思维,而是重要的补充。我们做创造性工作,确实特别需要跳跃式思维,而且这个也是人类社会进步的重要思考方法,但是如果没有结构化思考能力,则人类的很多具体细节工作会丢三落四,不太容易把事情做好,做优秀。这个是我来阿里 4 个月后的体验,特别是我在做商业模式思考的时候特别有用,比如推广一个产品有 4P 原则,即 Product,Price,Place,Promotion 。分析一个具体问题可以有 4W1H 原则,即 What,Why,When,Where 以及 How ,或者 2W1H 原则:What,How 和 Why 。如果我们思考问题的时候,多多使用这种结构化思考方式,我们考虑问题会更加全面。又比如,我最近在考虑一个商业模式,就是如何推广天猫精灵智能硬件套件,我的方法就是直接打开 Xmind 工具,然后开始绘制脑图,刚开始不需要尽善尽美,先把一些跳跃式思维丢进去,然后不断从结构化思考方式丰富内容,最后出来的效果如下图所示: 上图中一级目录是标题,二级目录包括:痛点,为什么买?怎么卖?卖什么?价值,这些方面是一个结构化思考的过程,或者是宏观的思考,而相应展开的三级、四级目录的细节是微观的思考,可以使用跳跃性思维。而且这个模板产生后,后续考虑问题,可以直接调取这个模板,把一级目录的标题修改为你需要卖的其他产品,然后保留二级目录不变,继续修改三级目录、四级目录(可以选择性使用跳跃性思考)。因此可以大大提高工作效率,效果特别好。 演讲力 工程师都有两种截然不同的想法,一种想法是,自己做的东西其实挺微不足道的,没有必要特意告诉别人或者当众演讲来阐述自己的工作内容。另外一种想法是,自己做的东西特别牛逼,和大众讲怕大众听不懂。对于第一种想法,需要提醒工程师的是,其实工作职责只是分工的不同,没有贵贱之分,对自己做的工作一定不要觉得微不足道,其实它很可能是一个重要项目的关键单元,因此自己需要时不时考虑下自己的工作在整个项目中的作用、地位或者意义,如果经常能这样考虑问题,也就不会觉得自己的工作是微不足道了,因为,任何存在的工作一定是有价值的,最坏情况下,如果找不到自己工作的意义和价值,那有可能确实可以考虑换工作或者换公司了。所以我们基于工作是重要的,有价值的这个前提,看看前面提到的第二种想法,也就是觉得自己的工作特别牛逼,特别专业,没有办法和大众沟通,甚至给大众做演讲。这里,我分享一个自己的案例,刚来阿里巴巴的时候,我负责天猫精灵 IoT (物联网,Internet of Things )这个方向,我觉得 IoT 这个工作特别重要,战略意义也很大,我需要把自己的工作让更多同学知道。因此我在入职阿里巴巴 3 个月后,就主动发起了一个公开演讲(不是领导或者 HR 安排的),我的想法是,我需要把我的工作的意义,内容,价值告诉大家,通过公开演讲影响大家对 IoT 的看法。但是在做具体 PPT 的时候,我发现要让不同岗位的人(很多不具备技术背景)听懂技术演讲是一个不容易的事情,因此我决定要先把 IoT 的概念,作用讲清楚,这里我就用了做 PPT 的一个技巧“一图二表三文字”,就是尽量多做一些图表来展示 IoT 的概念,比如什么是 IoT ,和互联网,移动互联网的关系,比如 IoT 的具体应用案例是哪些, IoT 的架构分层分为哪三层,等等不一而足。通过图形,脑图,案例图,把这些抽象概念用“人话”讲出来,也就是通过通俗易懂的方式,深入浅出(特别是大量使用打比方,举例子的方式)地讲出来。当时就收到了很好的效果,也展现了自己的技术能力,并和听众达成了共识:IoT 这个工作在部门中是这样的战略地位。 我觉得做技术的人,很多时候会完全沉迷在技术研究中,当然这个无可厚非,也是他们的本职工作,但是如果自己的工作只能孤芳自赏,那么成就感会相对不那么足,如果可以“与民同乐”,“独乐乐,不如同乐”的方式通过演讲分享给大家,并注意分享的方式(一定是深入浅出,通俗易懂,绝对不要认为自己懂的简单的东西大家也一样精通,反而要讲的越浅显越好,也不要一次讲太多的知识点),那么不仅仅得到了大家的认可,也让大家更加了解自己做的工作的意义,那样的话,成就感会大大加倍。另外,领导很多时候也不太可能完全懂工程师做的技术细节,通过公开演讲,也是向领导展现自己的技术能力的机会。另外,技术人员如果要上升到技术管理,演讲力也是一个重要的考察指标,有很好演讲力,很好表达力的工程师,说明这个人,既有技术功底,又有宏观抽象能力,并可以从非技术角度思考问题,绝对是技术管理人员的好人选。 商业思考 在阿里工作, P7 及以上的员工,是要求有商业 sense 的,当然不是说 P7 以下的员工不需要商业 sense,只是他们由于工作经验少一些,工作范围窄一些,知识结构不全面,可能比较难有好的商业 sense,但是也不排除特例,我认识的一个同学 BS,他虽然只有 P6,但是经常和我聊如何做好商业,就说明 BS 还是有商业 sense 的,或者他在刻意培养自己的商业 sense,这个对他的职业发展特别有好处。反过来,有些 P7 的同学,特别是技术同学,会特别关注在技术层面,很少关注商业问题,这个也就局限了自己的职业发展。 我由于来阿里之前,有过 5 年多的创业经历,所以我很自然地具备了一些商业 sense,或者我的血液中就流淌着商业 sense,毕竟我已经经历过太多的公司创立和倒闭的过程,无时无刻都有很多危机感,这也是我比较很多长久待在大公司的同学的优势之一。我觉得很多时候一定要刻意培养下自己的危机感,有适当的危机感会逼迫自己更多考虑公司怎么赚钱这个事情。 说到商业思考,我不得不提醒很多人,这个东西要么自己去经历(创业),要么多看书学习(间接经验),还有就是多和优秀的人聊天。多听听他讲做一个事情的更深层次的思考。不要只是看表面问题,而是要多多思考为什么他们是这么做的,有什么深层原因。
点击阅读上篇:从方法到思维:什么是应用逻辑架构的正确姿势? 五 架构的基本约束 架构约束分成了基本约束和业务约束: 逻辑架构基本约束:是软件工程领域常见的各种软件设计原则。 逻辑架构的职责约束:是模块,子模块,模型的职责相关约束,尤其是核心的模型和核心主模块是在一定时间内是比较稳定的,所以此时对其定义它的约束范围是有助于这段时间内的研发的效率的。 各种架构的非业务功能性约束,如稳定性,性能,成本等等。 而本文讲到的约束基本是逻辑架构上约束,如果考虑业务约束,我们还必须要考虑我们的面向的客户是什么群体之类的约束,如果缺少这样的约束,在设计产品时可能会走偏。 5.1 常见的软件设计原则 单一职责原则(SCP)(参考 grasp 原则) 开闭原则(OCP) 子类替换原则 依赖倒置原则(DIP) 接口隔离原则(ISP) 组合聚合复用原则(CARP) 迪米特法则(LoD) 以上这些原则都是判断标准,那么是用什么方法论来实现软件可以帮助我们的软件符合这些原则的呢?答:设计模式。 5.2 常见设计模式 这里有两个非常重要的关键词:判断标准 + 实现方法,这里判断标准是软件设计原则,实现方法设计模式。 作为一个常年在软件行业摸爬滚打的人,设计模式和设计原则应该是较为熟悉的,或者说常用的设计模式和设计原则都是比较熟悉的。但是大部分书籍讲到的是模块内部如何使用设计模式,并没有重点强调逻辑架构中模块之间如何使用设计模式来让逻辑架构遵循软件设计原则。 而我们设计或者推导逻辑架构时,主要就是用设计模式等方法来让逻辑架构中的各模块之间的关系,以及模块内部的子模块之间的关系符合软件设计原则。 5.3 关于模块 如何用设计模式来让模块间的集成符合软件设计原则,从而降低维护和扩展的成本。架构中的模块之间,模块和子模块,子模块和子模块要遵守软件设计的相关约束。如何遵守呢,领域建模和设计模式是两个具体的方法。 即使不考虑模块之间边界和约束,光考虑模块内部的设计,软件设计原则和设计模式就已然是我们软件工程师的必修课。再加上模块之间的依赖或者边界更加需要软件设计原则和设计模式,那它们的地位就更加神圣不可替代。值得不断的深入学习,实践,思考和总结,这也是为设计逻辑架构打基础,架构师必修课。 虽然我们一开始总是从滥用开始,不过没关系,一开始要做到不偏不倚总是很难的,慢慢的我们就可以窥见的其中的奥妙。 5.4 具体技术在某些特定场景下的约束 这是具体的技术在某个特定场景下的约束: Web 研发常见的规约,比如说重复提交,事务,多版本。 MySQL 的在高并发场景下的使用规约,比如说各种分库分表的规则,索引规则等等。 高并发相关系统中的相关约束,比如说幂等控制,并发控制,缓存策略,线程使用,锁粒度,各种循环内调用远程接口或数据库等等。 其他。 总的来说,这里的这些约束更偏向于物理架构上的约束,这里还是提前描述一下。同时每个物理架构要解决的问题不一样,导致它们要遵守的计算机科学与技术上的约束是不一样的,这是架构师们要整理,并倡导执行的。 5.5 逻辑架构中的业务属性约束 前面讲到的是软件研发领域的基本约束,这些基本约束在高粒度模块中一般很少被提及,高粒度模块之间的约束关系是根据业务中的思维概念提炼而来,比如电商中提炼出订单,营销活动,商品等等核心概念和核心域,对这些核心概念进行定义,以确定它们之间的关系和边界,从而形成技术上的统一业务约束。 同理,任何一个领域应该都存在这样的约束,只是这样的约束并不是一层不变的,尤其是在业务系统中,业务理解发生了变化,这样的约束也会随之变化,而且业务中约束的目的是驱动业务更好的前进的重要保障。 我们拿国家这个架构来做简单的解读,读了十年历史,大概总结出的一个国家级别主要架构约束是这样的: 历史上不同时期的国家治理有不同的架构(三省是顶层模块,六部是二级模块,然后依次做模块分解,直到一村,一户,这户可以看最是领域模型)和规约。西周和东周的春秋时期靠的是周公旦制作的礼和乐作为国家架构的约束,到了战国时期,礼崩乐坏,百家争鸣,最终以统一国家为目标的法(这个法和保障民生的法是两回事)成为秦国的架构约束,得以让他成功统一六国,但是很快这种法的约束又带来了副作用,于是汉朝建立,确定孔子的儒家伦理道德作为国家架构的主要约束。 然而这种以伦理和道德为主的架构约束对王朝的前 100 年 - 150 年是非常有效的,但是随着时间的发展,这样的约束会越来越弱,约束变弱则利益集团会不断的让架构中的模块边界变的模糊,有些模块的利益变的更大,有些模块的利益更小了,而且依赖关系变的混乱,从而使整体架构的利益受到影响,同时由于利益牵绊太深很少有一个总架构师有能力扭转乾坤。最终于就会被另外一个王朝所洗牌,新的王朝会重新建立架构,重新设定模块间的边界和依赖,同时还是以道德和伦理作为主要的约束。这种局面从汉朝开始周而复始了 2000 年。 不管怎么说,一个符合时宜的架构约束是有利于架构向前发展的,而不符合时宜的约束反而是制约者架构发展的。各种内耗等情况应运而生,最终阻碍了业务向前拓展。 5.6 约束小结 纵上所述,模块间约束无处不在,技术上的约束是最最容易看懂的。越是细粒度模块的约束,我们越容易学习和理解,比如软件设计的原则等等,越是高粒度的模块的约束,越抽象。需要对业务有深刻理解,对组织有深刻的理解,甚至对社会有深刻的理解。 六 逻辑架构复用 6.1 复用 件复用包含很多内容,比如说设计的复用,文档的复用,代码的复用等等。在本章节中的复用特指代码的复用。 复用的收益 提炼的目的是实现复用,复用的目标收益是: 软件的研发效率的提升 研发成本下降 软件质量的提升 等等 复用的分类 对于复用,我从业务功能和非业务功能的角度来分了一下类,如下: 1)一种是跟业务无关的一些可复用的内容,这些内容存在于基础架构的每一个层次,但是还不能归属于逻辑架构,而且业务技术无关的复用不是本文讨论的重点,所以本文不会重点阐述 MVC 的设计思想是如何在不同的 Web 应用中得以复用的。 框架的复用,spring, mybatis 之类的,对于框架的研究,业界从来没有停止过脚步 数据结构,算法,网络,等封装库,比如 Apache 和 Google 的各种封装库 中间件(RPC,Queue,cache 等)及各种存储,监控报警等基础设施 ORM,IOC,AOP,MVC,BPM,Rule Engine 等等对应的框架,这些都是和业务无关的复用 等等 2)还有一种是跟业务相关的可复用内容,它的产生取决于抽象能力和技术功底,比如: 系统模型复用:营销活动中存在各种规则,那么这些规则应该如何抽象以达到可以被复用的程度呢?比如我们将规则中的节点可以抽象成单独的算子,比如说满足某个条件,执行某个优惠动作,那么满足和某个优惠动作都可以抽象成算子(在 UMP 中被称为元数据,我们也沿袭了这一叫法)这些算子可以被复用且随意组合,以形成新的活动规则。 流程的复用,比如每种电商平台,都需要有交易流程,包括信息流,资金流,那么天猫,淘宝,聚划算等的交易流程是否可以复用,如果可以应该如何复用,是否可以将相同的和不同的环节区别对待,以实现可复用性。 计算模型 & 框架的复用,比如说营销中的叠加互斥计算模型,session 包的复用,特定业务中的测试框架的复用。 业务模块复用的形式(物理架构中要考虑的内容) 具体的复用形式本质上来说是物理架构中要考虑的内容,这里捎带提一下。 1)二方库形式 提炼成二方库,谁使用谁依赖这个二方库,这种情况又分成了两个子类: 纯逻辑,没有数据的存储等,其计算完全依靠调用者传入的数据,比如说某个业务场景的规则引擎,某个业务工具包等。 有负责数据的存储,比如说在二方库中直连另外一个服务(也可以看做胖客户端),或者直接连接数据库,这种方式在网站早期比较常见。 2)服务化形式 下沉成服务,通过接口对外暴露,技术手段多种多样,比如说 HSF,SOFA 对外暴露,或者 HTTP 对外暴露等,但是这里的重点不是在使用什么样的技术手段,而是暴露的服务中应该包含哪些内容(有多少客户,他们的需求的共性是什么,我们的业务本质是什么,根据这些内容来设计我们需要暴露的服务,然后在考虑我们接口的规范。至于使用什么样的服务容器之类的内容基础设施架构同学会重点来考量,我们需要需要学习和理解,但是我们的重点还是在前两个,即服务到底是什么,以及服务接口的规范是什么,在这两个上苦下功夫,对业务线的同学拿结果以及个人成长都有莫大的帮助) 3)展示组件 还有我们前端的各种可复用的展示组件的设计,比如说 TMF 的可复用组件等等。 逻辑架构中的可复用模块的落地表现形式优劣 跟业务无关的可以复用内容我们在本文中暂不讨论,本文中我们讨论一下跟业务相关的跨模块复用的两种情况,以及这两种情况之间的异同: 在跟业务相关的跨模块可复用情况中,慢慢的大家都以后者(下沉成服务)作为主要的表现形式,原因有便于发布,变更影响小,等等。虽然后者在调用时有一些远程开销,但是得益于 RPC 简洁的二进制协议(CPU Time 的下降)和日益变小的 RTT(RT 的下降)及日益增加的带宽,其远程开销的代价渐渐变得不那么显眼,甚至可以忽视。 那么是不是后者是不是可以代替前者呢?也并不是这样,有的场景下前者是不能用后者来代替的,比如说通过业务流程的提炼抽象而得来的业务二方库,这个是无法通过服务化来代替的,反而这种情况下,往往是服务化+二方库同时出现,起到一个很好的复用的作用。 所以在业务线的应用逻辑架构中,复用的重点即在提炼出共同的特性(模型上,流程上,计算模型上等),然后以二方库或者服务化应用的方式来进行落地。那么如何在逻辑架构中提炼出共同特性呢? 6.2 抽象和提炼 抽象和提炼基本上会从下面几个点出发: 有类似的模型或者属性 有类似的流程 有类似的数据结构和算法 我相信很多人都有过这样的经验。由此可见提炼就是阴阳调和: 抽象与架构:对业务的理解,根据领域建模的方法和设计模式产生领域模型抽象和流程抽象,或者计算模型的抽象等等,然后根据这些抽象设计出合理的架构,并让架构健康的向前迭代。 计算机科学与技术:对技术深度的把控,包括编程语言,各种框架,SDK,多线程,数据结构,各种网络编程包,各种 xx 引擎(如规则引擎,流程引擎等等)。 而这些都需要工程师们对领域建模和设计模式的抽象技术,以及对相对的技术特性等计算机技术的深入掌握。这里需要强调光知道领域建模和设计模式等是不够的,不同的技术选型特性不一样,会导致在抽象的实现时产生不同的差别。 在复用这件事情上,抽象技术和计算机技术两手抓,两手都要硬。如果用中国古代传统思想来比喻,那可能可以用阴来比喻抽象技术,阳来比喻计算机技术。 尤其是阴,总是给人捉摸不定的感觉,但是如何深入学习,坚持实践总结,我们就会发现阴原来也是有具体方法论的,但是这个具体方法又不是看看书就能学会的,它对知行合一的要求更高。 从学习的步骤来说,一般的过程都是先从阳(计算机科学与技术)开始,因为先从阴(抽象和架构技术)开始没有阳作为支撑是很难把阴融会贯通的。而且最终要达到的是阴阳调和。如果我们过于偏重阴或者过于偏重阳,都会导致阴阳失调,大概就是这个意思。 来到数据部门之后,我发现已经不能用阴阳来形容我们要学的领域了,现在我们搞的比较多的是统计分析和机器学习(统计分析和机器学习有交集,也有区别),所以目前对我们团队来说,我们的同学有三门学科是必须要掌握的: 计算机科学与技术 抽象与架构 统计分析与机器学习 我最近一年看的比较多的是统计分析,有同学钉钉我问道:怎么连你也放弃领域建模了。我没放弃,领域建模是抽象和架构的重要方法(但不是唯一的方法,演绎和归纳也是,自顶向下分解也是),工程技术同学是不能放弃的。学习统计分析及统计学习是因为统计学习 + 计算机科学与技术可以更好的解决工程领域遇到的问题,这也是各条线的工程师需要掌握的技能。 6.3 复用小结 复用是软件中一个非常重要的学问,里面结合了抽象技术和计算机技术,而抽象技术还依赖于对业务的理解程度,所以此非一日之功,需要长时间的锻炼才能有所小成。 当然,有时候即使在技术上可以抽象提炼,但是由于组织架构的问题也会让这样的提炼无法落地,或者这里并不是一个稳定的结构从而导致经常调整,带来的结果是提炼的投入产出比比较小,从而导致无法提炼,这些这里就不详细写了。 七 逻辑架构分层 7.1 分层的分类 工程骨架分层 分层几乎是从每个工程师入门的时候都会接触到的一个普世的概念,在一些书籍里,分层有的被称之为 tier,有的被称之为 layer,比如说 OSI 分层模型是用的 Layer 这个词。而在一些文章里讲到架构时用的是 tier 这个词,当你去查看 wiki 的时候,那就更晕了,因为 wiki 离 tier 和 layer 是混在一起讲的。 谈到分层,各种教科书中分层无不拿出景点的 3 个层次来阐述分层问题,如下: presentation layer business layer data layer 然后还有扩展出 service layer,这些在工程骨架中非常常见,我们几乎从来没有见过不分层的工程骨架,所以当我们讨论架构分层的时候,很多人脑海里第一映像就是工程骨架中的分层。 工程骨架的分层的一个重要目的是:成为代码组织结构的约束,防止代码混乱不堪。 逻辑架构分层 但是我们讲的逻辑架构分层不是指工程骨架分层,为什么不是?首先来看一下逻辑架构的特点: 源于业务概念架构(源于业务分析),保留了业务概念架构中大多数的业务功能模块,但是又会通过对技术的提炼从而比业务概念架构更加复杂。 逻辑架构中上下左右模块之间存在依赖关系,所以确定模块依赖关系是一个非常重要的话题。 逻辑架构是分片的,一般来说同一个层次会存在多个模块,像兄弟一样。 根据这个特点,我们可以模糊的看出逻辑架构的分层主要是逻辑架构中各模块的调用关系,甚至更偏向从模块职责的角度来进行归纳从而得出层次。 这种分层的目的是:对同一类职责的模块进行职责上的约束,此时还不一定有代码的存在。 者的区别 这么看来这两个分层是有着本质的区别: 目的上:逻辑架构中的分层是逻辑架构中各模块间的依赖层次关系,以及模块的再抽象。而项目骨架中的分层是代码的组织形式的一种约束。 形式上:某个逻辑架构中某个层次上的应用内部依然是存在工程骨架分层的,比如说购物车模块依赖了营销模块和商品模块,他们在逻辑架构上可能是不同的层次,但是购物车,营销内部的工程骨架上依然进行presentation, business, repository 之类的分层。 也许有的同学会说了,再大的架构(就比如说某个 BU 的逻辑架构),我也可以将最靠近用户的模块划分成 presentation layer,中间的所有模块都划分为 business layer,最下面的我都划分成 presentation layer。没错,你可以这样做,但是这样做基本没有任何意义,不能带来指导作用,失去的分层的目的。 7.2 分层的案例 在文件系统或者网络协议上,也有各种层次的封装。如下图所示: 这个图中每个模块在不同阶段都有不同阶段要解决的问题,然后每个模块都可以分解,产生更细粒度的模块,这里重点是让大家了解到什么是逻辑架构中模块的分层。 宏观上来看,处于上层的模块会依赖处于下层的模块 同一层的模块有时候也会产生依赖关系 在层次上可以用箭头标注数据流或者调用流 不过这些都不是问题,问题是什么呢? 问题是我们必须时刻知道,目前我们在不同层次的这些模块存在哪些问题,以及不同层次在解决什么问题。比如说上述的操作系统中文件系统和协议分层中,最底层的是跟硬件打交道,能够精准的控制硬件,中间是对操作系统的用户暴露的,更简单易用,上层是针对应用来使用,解决特定领域的问题,不同的层次做了不同的抽象,也是在解决不同的问题。 7.3 某些领域建模书籍之中的分层 很多人及一些书中,谈分层必谈工程骨架的分层,这个分层和架构中的分层是两回事,如果我们在谈架构,那么我们要避免把重心放到项目骨架的分层上。 比如说领域建模的相关书籍中,经常会讲到 service, domain, repository 之流,这个些概念处于架构中的什么位置,我们应该什么时候去关心这些概念? 如图所示,在细粒度模块内部,按照纯技术职责来进行划分时,我们将之撸成 service, model, repository,integration 之流的工程骨架,值得注意的是工程骨架的划分层次和具体的业务逻辑架构是没有关系的,他更偏技术,他的职责是对代码做一个高层次的组织和管理。 按照正常流程,系统模型产出之后,应该紧接着考虑模块的设定,依赖,规约,但是很不幸,很多书籍和资料都把 service, model, repository,integration 这部分分层的内容作为了领域的建模的最重要的重点之一。 某些书里的这种观点是不符合实际工作流程的,实际工作时,在领域模型之后我们先考虑的是架构中的各个模块的位置和职责,以及模块内部的子模块,模块之间的关系,以及整体的约束等等(请参考文章开头对架构的定义)。具体表现就是我们在逻辑架构图中不会去画什么 service, model, repository, integration 之类的层。 工程骨架的分层在细粒度模块内部,这是基础设施架构的一部分,也许是你手头目前最重要的部分,但是对于整个应用逻辑架构来说不是最核心的部分也不是最需要先考虑的内容。也就是说,即使你不分 service, model 之类的,对应用逻辑架构中模块的职责划分也是没有影响的。 同时我也见过一些项目,应用逻辑架构比较明确了,但是在落地到物理架构时,把逻辑架构中的所有模块都放在 service 包里,而且没有再分包,这就不合适了,逻辑架构中的模块完全没有落地。 所以我现在在我们部门的项目中,坚决避免将 service, model, repository,integration 之类的放到最高层来考虑。而是将逻辑架构的设计切切实实的落地,这样根据逻辑架构,我们就能看到的我们具体的应用,和应用内包的组织情况。 再次强调:逻辑架构中的分层不是指 service, model,repository, integration 之流的分层,而是指功能模块的分层。如果不了解业务,如果不了解业务概念模块,如果没有业务概念架构,我们是很难做出合理的应用逻辑架构的(当然也包括逻辑架构中模块的分层),撇开业务特征直接谈逻辑架构的分层是不行的。 模块的职责确定之后,模块之间的依赖也必须要确定,然后模块对外暴露接口需要定义规范和技术实现的手段。比如,如果是 restful 接口,那么应该是什么样的规范对外定义,如果是内部的服务的接口,应该是什么样的规范。由于本文篇幅所限,此处不进行详述,前者可参考各大平台的开放接口,后者可参考各 BU 内部服务调用的相关规范,如果没有,那说需要制定一个统一的规范。 八 逻辑架构是分粒度的 8.1 逻辑架构颗粒度树 这里我引入了一个新概念:逻辑架构颗粒度树。 刚刚讲的都是 2 维上的架构,我们可以看到,架构推导是有方法的,而且如果对方法进行提炼,就是横和竖的问题,但是正如我们开始讲到的,架构也可以是 3 维的,那就是在二维的模块中存在各种粒度子模块或者父模块。 如果非要打个比喻的话,那么下面的宇宙星神合体是一个大的架构: 里面分成了很多小模块,比如物质飞船,探测器等等,而每个小模块又是有很多基础模块构成,宇宙星神合体中只有 3 个层次,及基础部件,模块,及最终的星神合体,它们代表着不同的粒度。而对于业务复杂的架构来说,粒度会更多,层次就会更多。这取决于N个研发资源投入在某个模块上的效率最高,而这个 N 在某个阶段的技术限制下应该是一个比较稳定的值! 抽象一下,模块在不同粒度上,可以整成这么一棵树: 逻辑架构粒度树的 3 条原则: 纵向上,任何一层次的模块的职责,都必须是下一层职责的概括 横向上,同一层次的模块职责属于同一范畴 横向上,同一层次的模块的边界清晰 上述的树形结构,只能描绘出模块和父模块,子模块的关系,但是不能完整的描绘出模块之间的关系,是处于同一层次,还是处在不同层次(就是前面提到的应用逻辑架构中横的问题和竖的问题)。 那么用什么样的图形既可以生动的表达出模块和父模块,及子模块的关系,又能表达出不同模块之间的关系呢,我想了很久,也没有想到一个更容易理解的图形,最后产出了下面这么一幅图: 图中有三层,但是现实生活中可能超过三层,也可能低于三层。我们能归纳的层次越高,那可能我们接触的东西就越宽广,越精深。 这里有一个严肃的话题需要提一下:是不是一线工程师不用考虑逻辑架构问题?当然不是,任何一个同学,你手头的工作都是跟架构相关的,你负责模块可能也存在子模块,而且必定会存在父模块,出于工作,你必须要理解不断迭代你模块中的设计,同时随着能力的成长,你必须要关注你的父模块,父模块的父模块,日积月累,你可以 hold 住的模块粒度会越来越大,你的职责和能力要求会越来越大。 8.2 模块颗粒度树落地情况 在下述架构模块颗粒度树中,并没有模块和模块之间的依赖关系,这里只是为了概要的说明模块落地到物理架构中的一个演变过程,而具体的案例我们放在后面的文章中来进行阐述。 模块树上的这些不同粒度的模块,在具体落地成物理架构时,可以是不同的形式,如下: 可能是子包 可能是顶层的包 可能是应用 可能是一组应用的集合,负责某种职责 也可能是某个平台(如营销平台,商品中心等) 更有可能更大层次的平台,比如 B2C 为什么会出现这种情况呢,因为不同的模块在业务的发展的不同时期: 模块中的逻辑的复杂度不一样 模块的粒度本身也在发生变化 那么我们来看看一个网站从小到大的逻辑架构模块树落地的变化情况。 小型业务逻辑架构的模块树落地情况 很显然,这里是一个电商网站起步时候的样子,所有模块都有模有样,只是模块中的逻辑比较简单,这些模块都以包的形式存在于一个应用之中,这个应用是一个大泥球。但是由于模块的职责划分合理,粒度的治理也比较符合发展要求,所以这样的应用在分拆成分布式的时候阻力会比较小。而那些模块职责不合理的大泥球应用,随着业务的发展,要分拆成分布式应用,阻力就大很多。 中型业务逻辑架构的模块树落地情况 这是一个度过初期阶段的电商网站,营销,商品,交易模块等已经成型,而且得益之前的模块划分,架构师可以很快的将初期的多个顶级包,分拆出来,变成多个应用。 大型业务逻辑架构的模块树落地情况 到了这个时期,已经是一个大型电商网站的样子了,营销平台内部已经分拆出了多个应用,得益于上一阶段中各模块职责的合理分配,所以架构师将在将物理架构进化成这个样子的时候,力气不需要花在逻辑架构的治理上,可以把精力集中投入到物理架构及基础设施架构的建设上,比如同城容灾,异地多活等等。 8.3 再发展成巨型的架构呢? 中台概念抽象 我不知道,比如中台是不是,要把电商业务中所有的相对稳定的核心抽象出来,可能是领域模型,可能是业务流程转换而成的系统流程,可能是一个计算模型或者算法等等。然后变化的内容(前台)可以依托于这个大的核心概念快速的迭代。如果需要图形化来做概要的理解,我想应该是这样的: 一旦要做一个中台,那么以为着这个中台对前台来说就是一个技术产品,则要考虑如下几个方面的内容: 稳定性性能的要求是极高的,需要有体系化稳定性和性能体系 产品运营是非常重要的,到售前,到售后有一个完整的流程 目的就是要提高客户的生产效率。 是否存在中台的判断依据是什么? 多个业务线有无重复的流程抽象,有无重复的领域模型抽象,有无重复的计算模型(数据结构和算法)等等,有无重复的辅助性设施。他们是否在重复建设,等等。 在演变的过程中变化是什么呢?需要的是学习能力,沟通能力,协调资源的能力,领导力,影响力,评估人的能力和用人的能力等等。这些能力需要涉及的范围都从一个小的组织向一个更大的组织前进。 大音希声,大象无形,不管如何发展,基础的规律都还是不变的。 8.4 逻辑模块落地的相关考量维度 当一个逻辑模块要落地时,我们如何判断一个模块落地成包,还是应用等等,有很多判断的维度,比如: 效率(多少人维护一个应用效率最高) 到底多少人的团队协作效率最高?作为一个应用,在技术不断进步的情况下(比如说新的容器之类的),或者要面对的业务的复杂度不同的情况下,同时可维护的人数也是不一样的,具体目前变化到多少,目前基本是靠经验,然后遇到问题再调整,根据主管的经验不断调整和优化,以达到一个适合当前阶段的最优值,目前我自己这边大概5人左右一个攻坚小组,遇到更大问题域,那就拆解。 稳定性 强弱依赖 核心与非核心分离 性能 QPS,包括模块内部的技术实现,是使用多线程还是协程,容量评估,到压测,等等,里面大量的内容。光是线程这一节就有需要研究很久最大 QPS 推导及同步异步问题: RT,减少 wait time? 减少 cpu time?从浏览器,到网络,到服务器,到存储等每个环节,比如说网络上有一个重要的公式:BDP = BD * RTT,把这个公式背后的相关知识点搞清楚,那么网络优化的很多方法的理论依据我们就搞清楚了。 这里面,效率,稳定性,性能是最影响逻辑架构落地成物理架构的三大主要因素。 九 全文总结 9.1 架构的定义和价值 我们在文章的开头对架构两个字给出了一个官方定义,然后按照笔者自己对架构的理解又对架构进行了分类,在架构分类中,出现了产品功能架构,业务架构,应用逻辑架构,应用物理架构等等。 不同的架构都是在解释不同的问题,比如: 产品功能架构强调的是功能模块能力,受众是最终使用产品的用户等。 业务架构是对业务的一种分析和理解,用来如何更好的构建产品,受众是产品的同学和技术同学。 应用逻辑架构强调的是研发时,各逻辑模块的职责,受众是研发的同学及架构师。 正确分析出当前的场合(受众和目的)应该用什么样的架构来阐述我们的意图是非常重要的。 同时我们可以看到小到一个mis系统,大到整个阿里,都可以用架构的角度来解释,架构中出现的各种中台,后台,各种框架等等其实都是架构方法产出的结果。系统大小不一样,抽象的方法是类似的。 架构产生之后,随着业务的迭代,架构不治理,模块职责和依赖,层次不清晰,约束不明确。稳定性,性能,成本都受到影响。积弊越久,回头越难,有时候不得不重头来过。 9.2 自底向上重度依赖于演绎和归纳 为了避免推倒重造的问题发生,我们需要不断的自底向上的方式来修正架构,修正其实是在做局部的模块重构,谈到修正,具体的方法是由这里就不得正视归纳和演绎的重要性了,而这里的演绎和归纳是抽象的核心概括。 自底向上的推导的重点在于演绎和归纳,越是底层的越是要使用演绎的方法,越是高层的越是使用归纳。 这两种方法应该什么时候使用?显然当我们的目标(比如说业务目标)或者结论是非常高粒度的时候,需要分解,那么使用自顶向下的推导,在规划未来时一般会用到类似的自顶向下的方法,产出我们宏观结论。 而如果是产品方案已经明确,程序员需要理解这个业务需求,并根据产品方案推导出架构,此时一般使用自底向上的方法,而领域建模就是这种自底向上的分析方法。 对于自底向上的分析方法,如果提炼一下关键词,会得到如下两个关键词: 演绎 演绎就是逻辑推导,越是底层的,越需要演绎: 从用例到业务模型就属于演绎 从业务模型到系统模型也属于演绎 根据目前的问题,推导出要实施某种稳定性措施,这是也是演绎 归纳 这里的归纳是根据事物的某个维度来进行归类,越是高层的,越需要归纳: 问题空间模块划分属于归纳 逻辑架构中有部分也属于归纳 根据一堆稳定性问题,归纳出,事前,事中,事后都需要做对应的操作,是就是根据时间维度来进行归纳。 关于归纳,我们前面已经做了大量的讲解,所以这里我们重点阐述一下演绎: 1)我们从对业务的理解,演绎出用例,从用例演绎抽象出业务概念模型,从业务概念演绎抽象出系统模型,从系统模型演绎抽象出物理存储模型。这是一个从 A 推导出 B,从 B 推导出 C,从 C 推导出 D,从 D 推导出E的过程,而在 B,C,D 上又有很多逻辑分支。推导出的层次越深,逻辑分支越广(保障每层的准确度的基础上),一般来说实力越强。 2)我们从对业务的理解,演绎出用例,从用例演绎出业务流程,再从业务流程演绎抽象成系统流程,然后再演绎成数据流。这是也是一个从 A 推导出 B,从 B 推导出 C',从 C' 推导成 D',从 D' 推导出 E' 的过程。这个推导过程比如有方法论辅助,否则逻辑的深度和广度都会受到影响。 总的来说:演绎推导的层次越深,分支逻辑越多,越能穿透迷雾,看问题就越透彻,说明功力越深厚。 打个比喻就是:对应相同品种的树来说,小树的根系和大树的根系在地下深入的长度和广度是完全不一样的,人的逻辑能力大抵也是如此。 其实我们工作中很多时候都在使用演绎和归纳,只是我们不知道我们在使用这类方法,看到这篇文章之后也许可以给大家带去一些思考,看清楚我们自己以前的工作到底是如何使用演绎和归纳的,以及如何改进以前的方法。 9.3 逻辑架构的自底向上推演 除了自底向上的通用思考方法之外,我们还必须要了解计算机领域的相关技能和套路才能产出合适的结果: 这张图是有严密的逻辑路径的,每个步骤的输入,都是上个步骤的输出。更关键的是这个张图是有顺序的,做架构要从上往下做,不可自顾自,不可撇开业务闭门造车。 为什么前面我们问题空间领域模型聊了这么多,原因就是问题空间的领域建模其实是分析阶段,如果分析阶段我们没有做正确,那么设计阶段我们能做正确的可能性是非常小的。 分析阶段,我们得出了正确的分析产出,那么我们在设计阶段,又根据合理正确的方法论,我们就可以得到合理正确的应用逻辑架构。 同时我们可以看出领域建模是抽象和架构的重要方法,但不是唯一的方法,因为归纳和演绎也是抽象及架构的重要方法,自顶向下推演也是架构的重要方法。 这套方法论的关键性总结应该是这样的: 1)架构问题是我们工作中常见的问题,我们要注意识别并定义架构中的问题 2)业务概念模型的产出是通过具体的方法演绎出来的 3)业务概念架构的产出是通过具体的方法归纳出来的 4)系统模型和数据模型的产出是通过具体的方法演绎出来的 5)应用逻辑架构的产出是通过对前面的产出归纳和演绎出来的 架构内模块的构建,模块的依赖关系,及约束 模块的粒度,父子模块的归纳 提炼可复用模块 纯技术模块的产生 逻辑架构的分层 物理架构的演进受逻辑架构的影响 研究业界现有的技术架构 6)应用逻辑架构推导所使用的归纳和演绎方法涉及到很多具体的知识 最重要的是这个过程是不断迭代的,这句话比什么都重要,只有运动着的架构,没有静止的架构。有的架构运动时进行不断的重构和调整,所以经久不衰,有的架构缺乏这样的自我否定机制,最终走向衰败。 PS:由于我架构水平,写作水平,及认知所限,有很多我自己都不知道自己不知道的规律和事实存在。所以文中整体思路和细节之处不免存在纰漏之处,还望大家不吝指出,谢谢。
那些年 早在闲鱼使用 Flutter 之初,图片就是我们核心关注和重点优化的功能。图片展示体验的好坏会对闲鱼用户的使用体验产生巨大影响。你们是否也曾遇到过: 图片加载内存占用过多? 使用 Flutter 以后本地资源重复,利用率不高? 混合方案下 Flutter 原生图片加载效率不高? 针对上述问题,从第一版 Flutter 业务上线开始,闲鱼对图片框架的优化就从未停止。从开始的原生优化,到后面黑科技的外接纹理;从内存占用,到包大小;文本会逐一介绍。希望其中的优化思路和手段,能给大家带去一些启发。 原生模式 从技术层面看图片加载,其实简单来说,追求的是无非是加载的效率的最大化——用尽可能小的资源成本,尽可能快地加载尽可能多的图片。 闲鱼图片的第一个版本其实基本上是纯原生的方案。如果你不想魔改很多底层的逻辑,原生方案肯定是最简单和经济的方案。原生方案的功能模块如下: 如果你啥都没做直接上了,那么你可能会发现效果并没有达到你预期的那么美好。那么如果从原生的方案入手,我们有哪些具体的优化手段呢? 设置图片缓存 没错猜对了,是缓存。对于图片加载,最能想到的方案就是使用缓存。首先原生 Image 的组件是支持自定义图片缓存的,具体的实现类是 ImageCache。ImageCache 的设置维度是两个方向: 缓存图片的张数。通过 maximumSize 设置。默认是 1000 张。 缓存空间的大小。通过 maximumSizeBytes 来设置。默认值 100M。相比张数的限制,其实大小的设置方式更加符合我们的最终的预期。 通过合理设置 ImageCache 的大小,能充分利用缓存机制加速图片加载。不仅如此,闲鱼在这个点上还做了额外两个重要优化: 低端手机适配 在上线以后,我们陆续收到线上舆情的反馈,发现全部机型设置同一个缓存大小的做法并非最优。特别是大缓存设置在低端机器上面,不仅会出现体验变差,甚至还会影响稳定性。基于实际情况,我们实现了一个能从 Native 侧获取机器基础信息的 Flutter 插件。通过获取的信息,我们根据不同手机的配置设置不同的缓存策略。在低端机器上面适当降低图片缓存的大小,同时在高端手机上将其适当放大。这样能在不同配置的手机上获取最优的缓存性能。 磁盘缓存 熟悉 APP 开发的同学都知道,成熟的图片加载框架一般都有多级缓存。除了常见的内存缓存,一般都会配置一个文件缓存。从加载效率上来说,是通过空间换时间,提升加载速度。从稳定性来说,这又不会过分占用宝贵的内存资源,出现 OOM。但是可惜的是,Flutter 自带的图片加载框架并没有独立的磁盘缓存。所以我们在原生方案的基础上扩展了磁盘缓存能力。 在具体的架构实现上,我们并没有完全自己撸一个磁盘缓存。我们的策略还是复用现有能力。首先我们将 Native 图片加载框架的磁盘缓存的功能通过接口暴露出来。然后通过桥接的方式,将 Native 磁盘缓存能力嫁接到 Flutter 层。Flutter 侧进行图片加载的时候,如果内存没有命中,就去磁盘缓存中进行二次搜索。如果都没有命中才会走网络请求。 通过增加磁盘缓存,Flutter 图片加载效率进一步提升。 设置 CDN 优化 CDN 优化是另一个非常重要图片优化手段。CDN 优化的效率提升主要是:最小化传输图片的大小。常见策略包括: 根据显示大小裁剪 简单来说,你要加载图片的真实尺寸,可能会大于你实际展示窗口的大小。那么你就没必要加载完整大图,你只需要加载一个能覆盖窗口大小的图片即可。通过这种方式,裁剪掉不需要的部分,就能最小化传输图片的大小。从端侧角度来说,一来可以提升加载速度,二来可以降低内存占用。 适当压缩图片大小 这里主要是根据实际情况增加图片压缩的比例。在不影响显示效果的情况下,通过压缩进一步降低图片的大小。 图片格式 建议优先使用 webp 这样格式,图片资源相对小。Flutter 原生支持 webp(包括动图)。这里特别强调一下 webp 动图不仅大小要比 gif 小很多,而且还对透明效果有更好的支持。webp 动图是 gif 方案比较理想的一种替代方案。 基于上述原因,闲鱼图片框架在 Flutter 侧实现了一套 CDN 尺寸匹配的算法。通过该算法,请求图片会根据实际显示的大小,自动匹配到最合适的尺寸上并适当压缩。如果图片格式允许,图片尽可能转化成 webp 格式下发。这样 CDN 图片的传输就能尽可能高效。 其他优化 除了上面的策略,Flutter 还有一些其他的手段可以优化图片的性能。 图片预加载 如果你想在展示的图片的尽可能的快,官方也提供了一套预加载的机制:precacheImage。precacheImage 能预先将图片加载到内存,真正使用的时候就能秒出了。 Element 复用优化 其实这个算是一个 Flutter 通用的优化方案。复写 didWidgetUpdate 方案,通过比较前后两次 widget 中针对图片的描述是否一致,来决定是否重新渲染 Element。这样能避免同一个图片,不必要的反复渲染。 长列表优化 一般情况下,Listview 是 flutter 最为常见的滚动容器。在 Listview 中的性能好坏,直接影响最终的用户体验。 Flutter 的 Listview 跟 Native 的实现思路并不相同。其最大的特点是有一个 viewPort 的概念。超出 viewPort 的部分会被强制回收掉。 基于上述的原理,我们有两点建议: 1)cell 拆分 尽量避免大型的 cell 出现,这样能大幅降低 cell 频繁创建过程中的性能损耗。其实这里影响的不仅仅是图片加载过程。文字,视频等其他组件也都应该避免 cell 过于复杂导致的性能问题。 2)合理使用缓冲区 ListView 可以通过设置 cacheExtent 来设置预先加载的内容大小。通过预先加载可以提升 view 渲染的速度。但是这个值需要合理设置,并非越大越好。因为预加载缓存越大,对页面整体内存的压力就越大。 该方案的不足 这里需要客观指出:如果是一个纯 Flutter APP,原生方案是完善,够用的。但是如果从混合 APP 的角度来说,有如下两个缺陷: 1)无法复用 Native 图片加载能力 毫无疑问,原生的图片方案是完全独立的图片加载方案。对于一个混合 APP 来说,原生方案和 Native 的图片框架相互独立,能力无法复用。例如 CDN 裁剪 & 压缩等能力需要重复建设。特别是 Native一些独特的图片解码能力,Flutter 就很难使用。这会造成 APP 范围内的图片格式的支持不统一。 2)内存性能不足 从整个 APP 的视角来说,采用原生图片方案的情况下,其实我们维护了两个大的缓存池:一个是 Native 的图片缓存,一个是 Flutter 侧的图片缓存。两个缓存无法互通,这无疑是一个巨大的浪费。特别是对内存的峰值内存性能产生了非常大的压力。 打通 Native 经过多轮优化,基于原生的方案已经获得了非常大的性能提升。但是整个 APP 的内存水位线依然比较高(特别是 Ios 端)。现实的压力迫使我们继续对图片框架进行更深度的优化。基于上述原生方案缺点的分析,我们有了一个大胆的想法:能否完全复用 Native 的图片加载能力? 外接纹理 怎样打通 Flutter 和 Native 的图片能力?我们想到了外接纹理。外接纹理并非是 Flutter 自有的技术,它是音视频领域常用的一种性能优化手段。 这个阶段我们基于 shared-Context 的方案实现了 Flutter 和 Native 的纹理外接。通过该方案,Flutter 可以通过共享纹理的方式,拿到 Native 图片库加载好的图片并展示。为了实现这个纹理共享的通道,我们对 engine 层做了深度定制。细节过程如下: 该方案不仅打通了 Native 和 Flutter 的图片架构,整个过程图片加载的性能也得到了优化。 外接纹理是闲鱼图片方案的一次大跨越。通过该技术,我们不仅实现图片方案的本地能力复用,而且还能实现视频能力的纹理外接。这避免了大量重复的建设,提升了整个 APP 的性能。 多页面内存优化 这个优化策略真真是被逼出来的。在对线上数据分析以后,我们发现 Flutter 页面栈有一个非常有意思的特点:多页面栈情况下,底层的页面不会被释放。即便是在内存非常紧张的情况下,也不会执行回收。这样就会导致一个问题:随着页面的增多,内存消耗会线性增长。这里占比最高的就是图片资源的占比了。 是不是可以在页面处于页面栈底层的时候直接回收掉该页面内的图片呢? 在这个想法的驱动下,我们对图片架构进行了新一轮的优化。整个图片框架中的图片都会监听页面栈的变化。当方发现自己已经处于非栈顶的时候,就自动回收掉对应的图片纹理释放资源。这种方案能使图片占用的内存大小不会随着页面数的变多呈现持续线性增长。原理如下: 需要注意的是:这个阶段页面判断位置其实是需要页面栈(具体来说就是混合栈)提供额外的接口来实现的。系统之间的耦合相对较高。 意外收获:包大小 打通 Native 和 Flutter 侧图片框架以后,我们发现了一个意外收获:Native 和 Flutter 可以共用本地图片资源了。也就是说,我们不再需要将相同的图片资源在 Flutter 和 Native 侧各保留一份了。这样能大幅提升本地资源的复用率,从而降低整体的包大小。基于这个方案,我们实现了一套资源管理的功能,脚本能自动同步不同端的本地图片资源。通过这样提升本地资源利用率,降低包大小。 其他优化——PlaceHolder 强化 原生的 Image 是没有 PlaceHolder 功能的。如果想用原生方案的话,需要使用 FadeInImage。针对闲鱼的场景我们有很多定制,所以我们自己实现了一套 PlaceHolder 的机制。 从核心功能上来说,我们引入了加载状态的概念分为: 未初始化 加载中 加载完成 针对不同的状态,可以细粒度的控制 PlaceHolder 的展示逻辑。 整体架构 该方案的不足 毕竟改了 engine 随着闲鱼业务的不断推进,engine 的升级的成本是我们必须要考虑的事情。能否不改 engine 实现同样的功能是我们核心的述求(PS:我承认我们是贪心的)。 通道性能还有优化空间 外接纹理的方案需要通过桥的方式跟 Native 的能力做通信。这里包括图片请求的传递和图片加载各种状态的同步。特别是在 listview 快速滑动的时候,通过桥发送的数据量还是可观的。当前方案每个图片加载时都会单独进行桥的调用。在图片数量比较多的情况下,这显然会是一个瓶颈。 耦合过多 在实现图片回收方案的时候,目前方案需要栈提供是否在栈底层的接口。这里就产生方案耦合,很难抽象出一个独立干净的图片加载方案。 Clean & Efficient 时间来到了 2020 年,随着对 Flutter 基础能力理解的逐步深入,我们实现了一个整体方案更优的图片框架。 无侵入外接纹理 外接纹理可以不用修改 engine 么?答案是肯定的。 其实 Flutter 是提供了官方的外接纹理方案的。 而且 Native 操作的 texture 和 Flutter 侧显示的 texture 在底层是同一对象,并没有产生额外的数据 copy。这样就保证了纹理共享的足够高效。那为什么闲鱼之前会单独基于 shared-Context 自己实现一套呢?1.12 版本之前,官方 Ios 的外接纹理方案有性能问题。每次渲染的过程中(不管纹理是否有更新)都会频繁获取 CVPixelBuffer,造成不必要的性能损耗(过程有加锁损耗)。该问题已经在 1.12 版本中修复(官方 commit 地址),这样官方方案也足够满足需求。在这样的背景下,我们重新启用官方方案来实现外接纹理功能。 独立的内存优化 之前提到过,老版本的基于页面栈的图片资源回收需要强依赖栈功能的接口。一方面产生了不必要的依赖,更重要的是,整体方案无法独立成通用方案。为了解决这个问题,我们对 Flutter 底层进行了深入的研究。我们发现 Flutter 的 layer 层可以稳定感知到页面栈的变化。 然后每个页面通过 context 获取的 router 对象作为标识对一个页面中的所有的图片对象进行重新组织。所有获取到同一个 router 对象的标识成同一个页面。这样就能以页面为单位对所有的图片进行管理。整体上通过 LRU 的算法来模拟虚拟页面栈结构。这样就能对栈底页面的图片资源实现回收了。 其他优化 通道的高度复用 首先我们以一帧为单位对这一帧中的图片请求进行聚合,然后在一次通道请求中传递给 Native 的图片加载框架。这样能避免频繁的桥调用。特别在快速滚动等场景下优化效果尤为明显。 高效的纹理复用 使用外接纹理进行图片加载以后,我们发现复用纹理可以进一步提升性能。举一个简单的场景。我们知道电商场景中,商品展示经常会有标签,打底图这样的图片。这类图片往往在不同的商品上会出现大量重复。这时候,可以将已经渲染好的纹理,直接复用给不同的显示组件。这样能进一步优化 GPU 内存的占用,避免重复创建。为了精确对纹理进行管理,我们引入了引用计数的算法来管理纹理的复用。通过这些方案,我们实现了纹理跨页面高效复用。 此外,我们将纹理和请求的映射关系移动到了 Flutter 侧。这样能在最短路径上完成纹理的复用,进一步减少了桥的通信的压力。 整体架构 优化效果 由于最新的版本目前还在灰度,具体数据后续会写文跟大家详细介绍。下属数据主要以方案二为主。 内存优化 通过打通 Native,相比于首次上线版本,在显示效果不变的情况下,Ios 的 abort 率降低 25%,用户体验明显提升。 多页面栈内存优化** 多页面栈的内存优化,在多页面场景下对内存优化作用明显。我们做了一个极限试验效果如下(测试环境,非闲鱼 APP): 可见多页面栈的优化,可以将多 Flutter 页面的内存占用控制得更好。 包大小减少 通过接入外接纹理,本地资源得到了更好的复用,包大小降低 1M。早期闲鱼接入 Flutter,会以改造现有页面为切入点。资源重复情况比较严重,但是随着闲鱼 Flutter 新业务越来越多。Flutter 和 Native 的重复资源越来越少。外接纹理对包大小的影响已经逐步变弱。 后续计划 这是一场没有尽头的旅行,我们对闲鱼图片的优化还会持续。特别是我们最新的方案,受限篇幅,本文只是做了初步介绍。更多技术细节,包括测试数据,我们随后还会专门写文继续给大家做介绍。方案完善以后,我们也会逐步开源。
前言 中国有句老话叫"事不过三",指一个人犯了同样的错误,一次两次还可以原谅,再多就不可原谅了。写代码也是如此,同一个代码“坑”,踩第一次叫"长了经验",踩第二次叫"加深印象",踩第三次叫"不长记性",踩三次以上就叫"不可救药"。在本文中,笔者总结了一些 Java 坑,描述了问题现象,进行了问题分析,给出了避坑方法。希望大家在日常工作中,遇到了这类 Java 坑,能够提前避让开来。 1 对象比较方法 JDK 1.7 提供的 Objects.equals 方法,非常方便地实现了对象的比较,有效地避免了繁琐的空指针检查。 问题现象 在 JDK1.7 之前,在判断一个短整型、整型、长整型包装数据类型与常量是否相等时,我们一般这样写: Short shortValue = (short)12345; System.out.println(shortValue == 12345); // true Integer intValue = 12345; System.out.println(intValue == 12345); // true Long longValue = 12345L; System.out.println(longValue == 12345); // true 从 JDK1.7 之后,提供了 Objects.equals 方法,并推荐使用函数式编程,更改代码如下: Short shortValue = (short)12345; System.out.println(Objects.equals(shortValue, 12345)); // false Integer intValue = 12345; System.out.println(Objects.equals(intValue, 12345)); // true Long longValue = 12345L; System.out.println(Objects.equals(longValue, 12345)); // false 为什么直接把 == 替换为 Objects.equals 方法就会导致输出结果不一样? 问题分析 通过反编译第一段代码,我们得到语句 System.out.println(shortValue == 12345); 的字节码指令如下: getstatic java.lang.System.out : java.io.PrintStream [22] aload_1 [shortValue] invokevirtual java.lang.Short.shortValue() : short [28] sipush 12345 if_icmpne 24 iconst_1 goto 25 iconst_0 invokevirtual java.io.PrintStream.println(boolean) : void [32] 原来,编译器会判断包装数据类型对应的基本数据类型,并采用这个基本数据类型的指令进行比较(比如上面字节码指令中的 sipush 和 if_icmpne 等),相当于编译器自动对常量进行了数据类型的强制转化。 为什么采用 Objects.equals 方法后,编译器不自动对常量进行数据类型的强制转化?通过反编译第二段代码,我们得到语句 System.out.println(Objects.equals(shortValue, 12345)); 的字节码指令如下: getstatic java.lang.System.out : java.io.PrintStream [22] aload_1 [shortValue] sipush 12345 invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer [28] invokestatic java.util.Objects.equals(java.lang.Object, java.lang.Object) : boolean [33] invokevirtual java.io.PrintStream.println(boolean) : void [39] 原来,编译器根据字面意思,认为常量 12345 默认基本数据类型是 int,所以会自动转化为包装数据类型 Integer。 在 Java 语言中,整数的默认数据类型是 int,小数的默认数据类型是 double。 通过分析 Objects.equals 方法的源代码可知:语句 System.out.println(Objects.equals(shortValue, 12345)),因为 Objects.equals 的两个参数对象类型不一致,一个是包装数据类型 Short,另一个是包装数据类型 Integer,所以最终的比较结果必然是false;而语句 System.out.println(Objects.equals(intValue, 12345)),因为 Objects.equals 的两个参数对象类型一致,都是包装数据类型 Integer 且取值相同,所以最终的比较结果必然是 true。 避坑方法 1)保持良好的编码习惯,避免数据类型的自动转化 为了避免数据类型自动转化,更科学的写法是直接声明常量为对应的基本数据类型。 第一段代码可以这样写: Short shortValue = (short)12345; System.out.println(shortValue == (short)12345); // true Integer intValue = 12345; System.out.println(intValue == 12345); // true Long longValue = 12345L; System.out.println(longValue == 12345L); // true 第二段代码可以这样写: Short shortValue = (short)12345; System.out.println(Objects.equals(shortValue, (short)12345)); // true Integer intValue = 12345; System.out.println(Objects.equals(intValue, 12345)); // true Long longValue = 12345L; System.out.println(Objects.equals(longValue, 12345L)); // true 2)借助开发工具或插件,及早地发现数据类型不匹配问题 在 Eclipse 的问题窗口中,我们会看到这样的提示: Unlikely argument type for equals(): int seems to be unrelated to Short Unlikely argument type for equals(): int seems to be unrelated to Long 3)进行常规性单元测试,尽量把问题发现在研发阶段 “勿以善小而不为”,不要因为改动很小就不需要进行单元测试了,往往 Bug 都出现在自己过度自信的代码中。像这种问题,只要进行一次单元测试,是完全可以发现问题的。 注意:进行必要单元测试,适用于以下所有案例,所以下文不再累述。 2 三元表达式拆包 三元表达式是 Java 编码中的一个固定语法格式: 条件表达式?表达式1:表达式2 三元表达式的逻辑为:如果条件表达式成立,则执行表达式 1,否则执行表达式 2。 问题现象 boolean condition = false; Double value1 = 1.0D; Double value2 = 2.0D; Double value3 = null; Double result = condition ? value1 * value2 : value3; // 抛出空指针异常 当条件表达式 condition 等于 false 时,直接把 Double 对象 value3 赋值给 Double 对象 result,按道理没有任何问题,为什么会抛出空指针异常? 问题分析 通过反编译代码,我们得到语句: Double result = condition ? value1 * value2 : value3; 的字节码指令如下: iload_1 [condition] ifeq 33 aload_2 [value1] invokevirtual java.lang.Double.doubleValue() : double [24] aload_3 [value2] invokevirtual java.lang.Double.doubleValue() : double [24] dmul goto 38 aload 4 [value3] invokevirtual java.lang.Double.doubleValue() : double [24] invokestatic java.lang.Double.valueOf(double) : java.lang.Double [16] astore 5 [result] 在第 9 行,加载 Double 对象 value 3 到操作数栈中;在第 10 行,调用 Double 对象 value 3 的 doubleValue 方法。这个时候,由于 value 3 是空对象 null,调用 doubleValue 方法必然抛出抛出空指针异常。但是,为什么要把空对象 value 3 转化为基础数据类型 double 呢? 查阅相关资料,得到三元表达式的类型转化规则: 1)若两个表达式类型相同,返回值类型为该类型; 2)若两个表达式类型不同,但类型不可转换,返回值类型为 Object 类型; 3)若两个表达式类型不同,但类型可以转化,先把包装数据类型转化为基本数据类型,然后按照基本数据类型的转换规则 (byte < short(char)< int < long < float < double) 来转化,返回值类型为优先级最高的基本数据类型。 根据规则分析,表达式 1(value1 * value2)的类型为基础数据类型 double,表达式 2(value 3)的类型为包装数据类型 Double,根据三元表达式的类型转化规则判断,最终的表达式类型为基础数据类型 double。所以,当条件表达式 condition 为 false 时,需要把空 Double 对象 value 3 转化为基础数据类型 double,于是就调用了 value 3 的 doubleValue 方法进行拆包,当然会抛出空指针异常。 避坑方法 1)尽量避免使用三元表达式,可以采用 if-else 语句代替 如果三元表达式中有包装数据类型的算术计算,可以考虑利用 if-else 语句代替。改写代码如下: if (condition) { result = value1 * value2; } else { result = value3; } 2)尽量使用基本数据类型,避免包装数据类型的拆装包 如果在三元表达式中有算术计算,尽量使用基本数据类型,避免包装数据类型的拆装包。改写代码如下: boolean condition = false; double value1 = 1.0D; double value2 = 2.0D; double value3 = 3.0D; double result = condition ? value1 * value2 : value3; 3 泛型对象赋值 Java 泛型是 JDK 1.5 中引入的一个新特性,其本质是参数化类型,即把数据类型做为一个参数使用。 问题现象 在做用户数据分页查询时,因为笔误编写了如下代码: 1)PageDataVO.java /** 分页数据VO类 */ @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class PageDataVO<T> { /** 总共数量 */ private Long totalCount; /** 数据列表 */ private List<T> dataList; } 2)UserDAO.java /** 用户DAO接口 */ @Mapper public interface UserDAO { /** 统计用户数量 */ public Long countUser(@Param("query") UserQueryVO query); /** 查询用户信息 */ public List<UserDO> queryUser(@Param("query") UserQueryVO query);} 3)UserService.java /** 用户服务类 */@Service public class UserService { /** 用户DAO */ @Autowired private UserDAO userDAO; /** 查询用户信息 */ public PageDataVO<UserVO> queryUser(UserQueryVO query) { List<UserDO> dataList = null; Long totalCount = userDAO.countUser(query); if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) { dataList = userDAO.queryUser(query); } return new PageDataVO(totalCount, dataList); } } 以上代码没有任何编译问题,但是却把 UserDO 中一些涉密字段返回给前端。细心的读者可能已经发现了,在 UserService 类的 queryUser 方法的语句 return new PageDataVO(totalCount, dataList); 中,我们把 List 对象 dataList 赋值给了 PageDataVO 的 List 字段 dataList。 问题是:为什么开发工具不报编译错误啦? 问题分析 由于历史原因,参数化类型和原始类型需要兼容。我们以 ArrayList 举例子,来看看如何兼容的。 以前的写法: ArrayList list = new ArrayList(); 现在的写法: ArrayList<String> list = new ArrayList<String>(); 考虑到与以前的代码兼容,各种对象引用之间传值,必然会出现以下的情况: // 第一种情况 ArrayList list1 = new ArrayList<String>(); // 第二种情况 ArrayList<String> list2 = new ArrayList(); 所以,Java 编译器对以上两种类型进行了兼容,不会出现编译错误,但会出现编译告警。但是,我的开发工具在编译时真没出现过告警。 再来分析我们遇到的问题,实际上同时命中了两种情况: 1)把 List 对象赋值给 List,命中了第一种情况; 2)把 PageDataVO 对象赋值给 PageDataVO,命中了第二种情况。 最终的效果就是:我们神奇地把 List 对象赋值给了 List。 问题的根源就是:我们在初始化 PageDataVO 对象时,没有要求强制进行类型检查。 避坑方法 1)在初始化泛型对象时,推荐使用 diamond 语法 在《 Java 开发手册》中,有这么一条推荐规则: 【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。 说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。 正例: // <> diamond 方式 HashMap<String, String> userCache = new HashMap<>(16); // 全省略方式ArrayList<User> users = new ArrayList(10); 其实,初始化泛型对象时,全省略是不推荐的。这样会避免类型检查,从而造成上面的问题。 在初始化泛型对象时,推荐使用 diamond 语法,代码如下: return new PageDataVO<>(totalCount, dataList); 现在,在 Eclipse 的问题窗口中,我们会看到这样的错误: Cannot infer type arguments for PageDataVO<> 于是,我们就知道忘记把 List 对象转化为 List 对象了。 4 泛型属性拷贝 Spring 的 BeanUtils.copyProperties 方法,是一个很好用的属性拷贝工具方法。 问题现象 根据数据库开发规范,数据库表格必须包含 id,gmt_create,gmt_modified 三个字段。其中,id 这个字段,可能根据数据量不同,采用 int 或 long 类型。 首先,定义了一个 BaseDO 基类: /** 基础DO类 */ @Getter @Setter @ToString public class BaseDO<T> { private T id; private Date gmtCreate; private Date gmtModified;} 针对 user 表,定义了一个 UserDO 类: /** 用户DO */ @Getter @Setter @ToString public static class UserDO extends BaseDO<Long> { private String name; private String description; } 对于查询接口,定义了一个 UserVO 类: /** 用户VO类 */ @Getter @Setter @ToString public static class UserVO { private Long id; private String name; private String description; } 实现查询用户服务接口,实现代码如下: /** 用户服务类 */ @Service public class UserService { /** 用户DAO */ @Autowired private UserDAO userDAO; /** 查询用户 */ public List<UserVO> queryUser(UserQueryVO query) { // 查询用户信息 List<UserDO> userDOList = userDAO.queryUser(query); if (CollectionUtils.isEmpty()) { return Collections.emptyList(); } // 转化用户列表 List<UserVO> userVOList = new ArrayList<>(userDOList.size()); for (UserDO userDO : userDOList) { UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); } // 返回用户列表 return userVOList; } } 通过测试,我们会发现一个问题——调用查询用户服务接口,用户 ID 的值并没有返回。 [{"description":"This is a tester.","name":"tester"},...] 问题分析 通过 Debug 模式运行,进入到 BeanUtils.copyProperties 工具方法内部,得到以下内容: 原来,UserDO 类的 getId 方法返回类型不是 Long 类型,而是被泛型还原成了 Object 类型。而下面的 ClassUtils.isAssignable 工具方法,判断是否能够把 Object 类型赋值给 Long 类型,当然会返回false导致不能进行属性拷贝。 为什么作者不考虑"先获取属性值,再判断能否赋值”?建议代码如下: Object value = readMethod.invoke(source); if (Objects.nonNull(value) && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], value.getClass())) { ... // 赋值相关代码 } 避坑方法 1)不要盲目地相信第三方工具包,任何工具包都有可能存在问题 在 Java 中,存在很多第三方工具包,比如:Apache 的 commons-lang3、commons-collections,Google 的 guava……都是很好用的第三方工具包。但是,不要盲目地相信第三方工具包,任何工具包都有可能存在问题。 2)如果需要拷贝的属性较少,可以手动编码进行属性拷贝 用 BeanUtils.copyProperties 反射拷贝属性,主要优点是节省了代码量,主要缺点是导致程序性能下降。所以,如果需要拷贝的属性较少,可以手动编码进行属性拷贝。 5 Set 对象排重 在 Java 语言中,Set 数据结构可以用于对象排重,常见的 Set 类有 HashSet、LinkedHashSet 等。 问题现象 编写了一个城市辅助类,从 CSV 文件中读取城市数据: /** 城市辅助类 */ @Slf4j public class CityHelper { /** 读取城市 */ public static Collection<City> readCities(String fileName) { try (FileInputStream stream = new FileInputStream(fileName); InputStreamReader reader = new InputStreamReader(stream, "GBK"); CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT.withHeader())) { Set<City> citySet = new HashSet<>(1024); Iterator<CSVRecord> iterator = parser.iterator(); while (iterator.hasNext()) { citySet.add(parseCity(iterator.next())); } return citySet; } catch (IOException e) { log.warn("读取所有城市异常", e); } return Collections.emptyList(); } /** 解析城市 */ private static City parseCity(CSVRecord record) { City city = new City(); city.setCode(record.get(0)); city.setName(record.get(1)); return city; } /** 城市类 */ @Getter @Setter @ToString private static class City { /** 城市编码 */ private String code; /** 城市名称 */ private String name; } } 代码中使用 HashSet 数据结构,目的是为了避免城市数据重复,对读取的城市数据进行强制排重。 当输入文件内容如下时: 编码,名称 010,北京 020,广州 010,北京 解析后的 JSON 结果如下: [{"code":"010","name":"北京"},{"code":"020","name":"广州"},{"code":"010","name":"北京"}] 但是,并没有对城市“北京”进行排重。 问题分析 当向集合 Set 中增加对象时,首先集合计算要增加对象的 hashCode,根据该值来得到一个位置用来存放当前对象。如在该位置没有一个对象存在的话,那么集合 Set 认为该对象在集合中不存在,直接增加进去。如果在该位置有一个对象存在的话,接着将准备增加到集合中的对象与该位置上的对象进行 equals 方法比较:如果该 equals 方法返回 false,那么集合认为集合中不存在该对象,就把该对象放在这个对象之后;如果 equals 方法返回 true,那么就认为集合中已经存在该对象了,就不会再将该对象增加到集合中了。所以,在哈希表中判断两个元素是否重复要使用到 hashCode 方法和 equals 方法。hashCode 方法决定数据在表中的存储位置,而 equals 方法判断表中是否存在相同的数据。 分析上面的问题,由于没有重写 City 类的 hashCode 方法和 equals 方法,就会采用 Object 类的 hashCode 方法和 equals 方法。其实现如下: public native int hashCode(); public boolean equals(Object obj) { return (this == obj); } 可以看出:Object 类的 hashCode 方法是一个本地方法,返回的是对象地址;Object 类的 equals 方法只比较对象是否相等。所以,对于两条完全一样的北京数据,由于在解析时初始化了不同的 City 对象,导致 hashCode 方法和 equals 方法值都不一样,必然被 Set 认为是不同的对象,所以没有进行排重。 那么,我们就重写把 City 类的 hashCode 方法和 equals 方法,代码如下: /** 城市类 */ @Getter @Setter @ToString private static class City { /** 城市编码 */ private String code; /** 城市名称 */ private String name; /** 判断相等 */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (Objects.isNull(obj)) { return false; } if (obj.getClass() != this.getClass()) { return false; } return Objects.equals(this.code, ((City)obj).code); } /** 哈希编码 */ @Override public int hashCode() { return Objects.hashCode(this.code); } } 重新支持测试程序,解析后的 JSON 结果如下: [{"code":"010","name":"北京"},{"code":"020","name":"广州"}] 结果正确,已经对城市“北京”进行排重。 避坑方法 1)当确定数据唯一时,可以使用 List 代替 Set 当确定解析的城市数据唯一时,就没有必要进行排重操作,可以直接使用 List 来存储。 List<City> citySet = new ArrayList<>(1024); Iterator<CSVRecord> iterator = parser.iterator(); while (iterator.hasNext()) { citySet.add(parseCity(iterator.next())); } return citySet; 2)当确定数据不唯一时,可以使用 Map 代替 Set 当确定解析的城市数据不唯一时,需要安装城市名称进行排重操作,可以直接使用 Map 进行存储。为什么不建议实现 City 类的 hashCode 方法,再采用 HashSet 来实现排重呢?首先,不希望把业务逻辑放在模型 DO 类中;其次,把排重字段放在代码中,便于代码的阅读、理解和维护。 Map<String, City> cityMap = new HashMap<>(1024); Iterator<CSVRecord> iterator = parser.iterator(); while (iterator.hasNext()) { City city = parseCity(iterator.next()); cityMap.put(city.getCode(), city); } return cityMap.values(); 3)遵循 Java 语言规范,重写 hashCode 方法和 equals 方法 不重写 hashCode 方法和 equals 方法的自定义类不应该在 Set 中使用。 6 公有方法代理 SpringCGLIB 代理生成的代理类是一个继承被代理类,通过重写被代理类中的非 final 的方法实现代理。所以,SpringCGLIB 代理的类不能是 final 类,代理的方法也不能是 final 方法,这是由继承机制限制的。 问题现象 这里举例一个简单的例子,只有超级用户才有删除公司的权限,并且所有服务函数被 AOP 拦截处理异常。例子代码如下: 1)UserService.java /** 用户服务类 */ @Service public class UserService { /** 超级用户 */ private User superUser; /** 设置超级用户 */ public void setSuperUser(User superUser) { this.superUser = superUser; } /** 获取超级用户 */ public final User getSuperUser() { return this.superUser; } } 2)CompanyService.java /** 公司服务类 */ @Service public class CompanyService { /** 公司DAO */ @Autowired private CompanyDAO companyDAO; /** 用户服务 */ @Autowired private UserService userService; /** 删除公司 */ public void deleteCompany(Long companyId, Long operatorId) { // 设置超级用户 userService.setSuperUser(new User(0L, "admin", "超级用户")); // 验证超级用户 if (!Objects.equals(operatorId, userService.getSuperUser().getId())) { throw new ExampleException("只有超级用户才能删除公司"); } // 删除公司信息 companyDAO.delete(companyId, operatorId); } } 当我们调用 CompanyService 的 deleteCompany 方法时,居然也抛出空指针异常 (NullPointerException),因为调用 UserService 类的 getSuperUser 方法获取的超级用户为 null。但是,我们在 CompanyService 类的 deleteCompany 方法中,每次都通过 UserService 类的 setSuperUser 方法强制指定了超级用户,按道理通过 UserService 类的 getSuperUser 方法获取到的超级用户不应该为 null。其实,这个问题也是由 AOP 代理导致的。 问题分析 使用 SpringCGLIB 代理类时,Spring 会创建一个名为 UserService$$EnhancerBySpringCGLIB$$???????? 的代理类。反编译这个代理类,得到以下主要代码: public class UserService$$EnhancerBySpringCGLIB$$a2c3b345 extends UserService implements SpringProxy, Advised, Factory { ...... public final void setSuperUser(User var1) { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if (var10000 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } if (var10000 != null) { var10000.intercept(this, CGLIB$setSuperUser$0$Method, new Object[]{var1}, CGLIB$setSuperUser$0$Proxy); } else { super.setSuperUser(var1); } } ...... } 可以看出,这个代理类继承了 UserService 类,只代理了 setSuperUser 方法,但是没有代理 getSuperUser 方法。所以,当我们调用 setSuperUser 方法时,设置的是原始对象实例的 superUser 字段值;而当我们调用 getSuperUser 方法时,获取的是代理对象实例的 superUser 字段值。如果把这两个方法的 final 修饰符互换,同样存在获取超级用户为 null 的问题。 避坑方法 1)严格遵循 CGLIB 代理规范,被代理的类和方法不要加 final 修饰符 严格遵循 CGLIB 代理规范,被代理的类和方法不要加 final 修饰符,避免动态代理操作对象实例不同(原始对象实例和代理对象实例),从而导致数据不一致或空指针问题。 2)缩小 CGLIB 代理类的范围,能不用被代理的类就不要被代理 缩小 CGLIB 代理类的范围,能不用被代理的类就不要被代理,即可以节省内存开销,又可以提高函数调用效率。 7 公有字段代理 在 fastjson 强制升级到 1.2.60 时踩过一个坑,作者为了开发快速,在 ParseConfig 中定义了: public class ParseConfig { public final SymbolTable symbolTable = new SymbolTable(4096); ...... } 在我们的项目中继承了该类,同时又被 AOP 动态代理了,于是一行代码引起了一场“血案”。 问题现象 仍然使用上章的例子,但是把获取、设置方法删除,定义了一个公有字段。例子代码如下: 1)UserService.java /** 用户服务类 */ @Service public class UserService { /** 超级用户 */ public final User superUser = new User(0L, "admin", "超级用户"); ...... } 2)CompanyService.java /** 公司服务类 */ @Service public class CompanyService { /** 公司DAO */ @Autowired private CompanyDAO companyDAO; /** 用户服务 */ @Autowired private UserService userService; /** 删除公司 */ public void deleteCompany(Long companyId, Long operatorId) { // 验证超级用户 if (!Objects.equals(operatorId, userService.superUser.getId())) { throw new ExampleException("只有超级用户才能删除公司"); } // 删除公司信息 companyDAO.delete(companyId, operatorId); } } 当我们调用 CompanyService 的 deleteCompany 方法时,居然抛出空指针异常 (NullPointerException)。经过调试打印,发现是 UserService 的 superUser 变量为 null。如果把代理删除,就不会出现空指针异常,说明这个问题是由 AOP 代理导致的。 问题分析 使用 SpringCGLIB 代理类时,Spring 会创建一个名为 UserService$$EnhancerBySpringCGLIB$$???????? 的代理类。这个代理类继承了 UserService 类,并覆盖了 UserService 类中的所有非 final 的 public 的方法。但是,这个代理类并不调用 super 基类的方法;相反,它会创建的一个成员 userService 并指向原始的 UserService 类对象实例。现在,内存中存在两个对象实例:一个是原始的 UserService 对象实例,另一个指向 UserService 的代理对象实例。这个代理类只是一个虚拟代理,它继承了 UserService 类,并且具有与 UserService 相同的字段,但是它从来不会去初始化和使用它们。所以,一但通过这个代理类对象实例获取公有成员变量时,将返回一个默认值 null。 避坑方法 1)当确定字段不可变时,可以定义为公有静态常量 当确定字段不可变时,可以定义为公有静态常量,并用类名称 + 字段名称访问。类名称 + 字段名称访问公有静态常量,与类实例的动态代理无关。 2)当确定字段不可变时,可以定义为私有成员变量 当确定字段不可变时,可以定义为私有成员变量,提供一个公有 Getter 方法获取该变量值。当该类实例被动态代理时,代理方法会调用被代理的 Getter 方法,从而返回被代理类的成员变量值。 3)遵循 JavaBean 编码规范,不要定义公有成员变量 遵循 JavaBean 编码规范,不要定义公有成员变量。JavaBean 规范如下: JavaBean 类必须是一个公共类,并将其访问属性设置为 public,如:public class User{......} JavaBean 类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器 一个 JavaBean 类不应有公共实例变量,类变量都为 private,如:private Integer id; 属性应该通过一组 getter / setter 方法来访问 后记 最后,推荐大家阅读一下《Java 开发手册》,这本手册让我受益匪浅。只要学习理解了《Java 开发手册》,就能在日常的 Java 开发工作中,避免踩到很多常识性的 Java 坑。
背景 阿里云有位工程师叫朴灵,热爱开源,是活跃在 Github 上的国内技术大牛之一。在阿里工作 6 年之际,朴灵产生了离职的想法,打算去一家创业公司再战高峰。走之前,朴灵做了一些研究工作,他发现阿里云在功能和产品上可以说是一流的云计算厂商,是创业公司的首选,但由于过去的业务中写过大量的 Node.js SDK,对开发者体验有着自己的体感,他觉得在开发者体验关怀上,阿里云做得还不够好。来自一个热血工程师最朴素的想法,自己何不先留下来,去把这件事情做好,于是,朴灵加入了阿里云开放平台负责 SDK 业务,期间,他和团队研发了专利 TeaDSL,下面朴灵将分享 TeaDSL 如何解决多语言 SDK 的问题。 使用 OpenAPI 的痛苦 在过去,我们经常说的 OpenAPI,通常的做法是,开发好服务端的接口,然后在文档里简单写几个参数描述,就直接丢给客户去用。反正我是开发好了,我这里是好的,客户能不能用起来我是不用管的。 图 1 第一代的 OpenAPI 通常仅由简单的文档及实际的接口构成 然而接下来的问题就来了。首先,文档上写得不清不楚的参数,没有试过,完全不知道它到底能不能 Work。其次,OpenAPI 总得有一定的权限认证吧,那么总得有一个签名啥的,每个客户都要写一遍,关键是总是没法写对。再次,不同的客户所使用的编程语言不一样,得把接口重新包装才能用。 总算费心费力调通了接口,以为可以高枕无忧的时候,咋接口老是报错,网络连不上,返回的数据不对,诸如此类。再往后,OpenAPI 可能总是要发生一点变化什么的,总是出现一些数据结构发生变化,不兼容之类的问题。 一个 OpenAPI 到最后,不光是用户使用起来觉得很气,作为维护者也是很艰难的。当公布一个 OpenAPI 后,第一步给出简单的文档后,会发现除了要把参数详情写得越来越完善准确外,还得给出签名算法,让不同语言的开发者来接入。然而给出签名算法后,会发现只有一些开发者能顺利完成,大部分的开发者只能眼巴巴地请你帮忙提供一个 SDK。好吧,那就提供一下我最拿手的 Java 语言的签名,提供一个核心 SDK 呗。 图 2 第二代的 OpenAPI 会有 SDK 的实现,但仅有少许的语言支持 随着这个 OpenAPI 接口的用户越来越多,一个客户说我要用 C++ 来对接你,另一个客户说我要用 Python 来对接你,于是,我一个 Java 程序员,怎么就要写那么多语言的 SDK 呢。没有办法,如果不提供良好的 SDK,客户说,没有 IDE 提示呢,我怎么写代码呢。 总而言之,在 OpenAPI 的应用过程中,一件简单的事情,会变得非常复杂: 需要提供良好的 API 文档,作为最基本的要求 需要提供 SDK,保障开发者的编码体验,封装细节,代码提示等 需要提供 Code Sample,更理解接口的使用效果 如果有 CLI 就更好了,这样连 bash 脚本写起来也更方便 如果没有 Test Cases 作为日常的持续集成,接口质量可能存在问题 上面这些要求,如果加上多种编程语言的条件,就会演变为一件细碎而又繁多的体力活。并且这中间不能有任何的变动,因为仅仅是一点点的 OpenAPI 变动,就需要连带整个下游发生变化。如果一个地方没有保持一致,那么客户问题就会出现。 图 3 当用户量变多,OpenAPI 的提供者需要提供完善的工具及更多的编程语言支持 通常为了解决此类的问题,以及 OpenAPI 的诸如签名校验,限流,生成 SDK、文档等等,业界通常会使用 API 网关来承担这些横向的责任。 然而,作为笔者所在的环境下,会发现,我们身边的网关有点多。于是不同的网关有不同的风格,不同的签名算法,不同的序列化格式。于是上述的过程要根据不同网关的数量,进行翻倍: 图 4 当一个企业变得庞大时,不同风格的 OpenAPI 及网关都会出现 当我们在抱怨使用不同产品的 OpenAPI/SDK 体验不一致,文档不对,Demo 出错等等问题时,真不是因为做这些事情太难,而是太多,太琐碎。一件简单的事情,需要做一百次,也就不是简单的事情了。 TeaDSL 的解决之道 TeaDSL 是由阿里云开放平台 SDK 团队主导设计的一门领域特定语言。主要用于解决如下问题: 通过一门中间语言,可以支持不同风格的网关。即使网关下的 OpenAPI 风格各异,也能一致地表达到。 可以通过翻译的能力,实现对不同编程语言的代码生成。也就是可以基于统一的中间表达,生成多语言的 SDK。 基于中间表达,我们可以将一组 OpenAPI 视为一个 library,因此可以在这个基础上实现 OpenAPI 接口的 Code Sample 编写。进而实现多语言的 Code Sample 统一生成。 因此 TeaDSL 的核心能力就是通过一种中间语法来描述 OpenAPI,提供类似编程语言的能力,来将 OpenAPI、SDK、Code Sample 等场景及语言有机地结合在一起。 在没有 TeaDSL 之前,对于不同的网关,我们要为它制定独立的工作流程,即从 OpenAPI 定义到不同语言的 SDK 生成,是独特的。换一个新的网关风格,就要重新实现这套流程。 图 5 M 个网关都要支持 N 种编程语言,整个工作量是 M * N 的关系 而具有 TeaDSL 后,我们则形成一个中间层。可以将原来的工作收敛起来,我们仅需要关注不同的网关到 TeaDSL 的转换工作,以及 TeaDSL 到各个编程语言的生成工作。 图 6 经过中间层的隔离,整个工作量变为 M + N 的关系 也就是说,TeaDSL 是在做一件 M * N 到 M + N 的工作。当网关越多,支持的编程语言越多,收益则越大。 一旦这个中间层建立起来,整个 OpenAPI 的应用形式都可以基于它来构建。比如,编写一个 OpenAPI 的 Code Sample,Test Case 等。 接下来简单介绍 TeaDSL 是如何实现支持任意风格的网关和多种编程语言的。 如何支持任意风格的网关 对于不同的 API 网关,或者不同产品的 OpenAPI 而言,它们之间的风格可能都千差万别,因此在很大的程度上,每种风格的 OpenAPI 都有它自己的元数据定义格式。为了减少网关、风格带来的差异化,业界主要推动的方式是尽量采用标准的定义格式。比如 Swagger 就是其中的佼佼者,它依托于 OpenAPI Specification ,以 RESTful 风格的 OpenAPI 作为基准,形成了一套业界标准。 但这个世界就是这样不完美,我们现有的大量 OpenAPI 并不是 RESTful 风格的。这导致很多的产品现存的 OpenAPI 在文档、SDK等场景下,无法使用上 Swagger 这样强大的生态工具链。 为了解决这些问题,我们需要进行两步操作: 设立一套新的标准,来包容不同风格的 OpenAPI以这套新的标准,来建设生态工具链 如果完成这两个步骤,那么现实世界上的每一个 OpenAPI,RESTful 或者非 RESTful 的,不需要做任何迁移,也能具有强大的工具链支持。 新标准的设计 通过我们的研究发现,无论 OpenAPI 的参数是如何组成的,传输是 JSON,还是 XML,乃至自定义协议,OpenAPI 都是基于 HTTP 协议栈进行提供的。也就是说,万变不离其宗的是 HTTP 协议本身。因此我们确立的基本模型是这样的: { protocol: string, // http or https port: number, // tcp port host: string, // domain request: { method: string, // http method pathname: string, // path name query: map[string]string, // query string headers: map[string]string, // request headers body: readable // request body }, response: { statusCode: number, // http method statusMessage: string, // path name headers: map[string]string, // response headers body: readable // response body }, } 对于不同风格的 OpenAPI 而言,就像不同风格的建筑,它们的建筑材料都几乎相同,只是施工手法,组合形式不一样而已。我们看到的 OpenAPI 风格差异,实质则是序列化过程不同而带来的不同。我们序列化过程和数据模型分离,将用户更直观的数据结构提取出来。 比如从用户角度出发,一个数据模型是更直观的事物: model User { username: string, age: number } 在不同的网关下,它的传输形式可能是 JSON,也可能是 XML,但最终都是 readable,也就是可读的字节流。 toJSON(user: User): string toXML(user: User): string 最终的结果就是: __request.body = toJSON(user); __request.body = toXML(user); 更进一步的过程是,我们会将一个 OpenAPI 的请求/响应包装为一个类似于编程代码的方法: api getUser(username: string): User { __request.method = 'GET'; __request.pathname = `/users/${username}`; __request.headers = { host = 'hostname', }; } returns { var body = readAsJSON(__response.body); return body; } 尽管上面的代码不能实际运行,但大致也看出来我们包容不同的网关、风格的办法如下: 以 request / response 也就是 HTTP 协议作为核心模型 通过引入一些方法,如 toJSON / toXML / readAsJSON 等方法来分离数据结构和序列化过程 将整个过程包装成方法 这些方法在不同的编程语言下具有不同的实现,但我们只要定义好统一的签名,就能确保一致性: function toXML(data: $Model): string; function toJSON(data: $Model): string; 以上就是 TeaDSL 如何实现支持任意网关的方案。整个过程相对抽象,网关间的那些具有差异化的风格,统统交给这些方法去实现,留下来的就只有数据结构。 如何支持不同的编程语言 如果只是能通过一种描述方式来描述不同的 OpenAPI 调用过程,只是完成了一半的工作。另一半的工作是如何将这种描述语言落地到不同的编程语言下。在过去,我们支持不同的编程语言,主要是基于模版的形式来生成不同语言的实际代码。但这对我们来说仍然还有一些不足之处: 模版的生成方式相对生硬,实现起来容易,但维护起来不那么灵活 生成出来的代码容易带来命名冲突,语法错误等 从上面的形式也看到,这个方案,被我们设计成了一种 DSL 代码。因此它是具有自己的词法、语法、语义规则的,在生成目标编程语言代码之前,会有一套自身的校验。DSL 的这些能力是模版所不具备的。 可能对于别的场合,采用 DSL 的形式并不多见。但对于前端工程师而言,这些年已经见的较多了:CoffeeScript、Babel、JSX、TypeScript 等等。为此我们参考了诸多编程语言的设计,最终形成了自己的一套语法。并借鉴编译器领域的转译方式,因此我们可以在模型一致的情况,生成到各种不同的编程语言下。 整个 TeaDSL 的处理流程如下: 最终我们支持多种编程语言的场景主要有 3 个: 基本的多种语言的 SDK OpenAPI 相关的多种语言的 Code Sample OpenAPI 相关的多种语言的 Test Case 通过中间语言的强校验,生成到多种目标场景,可以解决编程语言支持不全面的问题。同时也大幅节约 OpenAPI 维护者的精力成本,不需要反复手工地编写不同编程语言下的 Code Sample。随着对不同编程语言的支持逐步完善,这些中间 TeaDSL 代码不需要任何操作,即可自动支持到新的编程语言下。 总结 TeaDSL 的主要能力是支持到不同风格的 OpenAPI,同时支持多语言的 SDK、Code Sample 目标生成。最终的目的仍然是打通从 OpenAPI 定义到文档、到 SDK、CLI 等 OpenAPI 使用场景下的一致性。提供给用户更统一、专业、一致的使用体验。同时也大幅降低 OpenAPI 提供者用来支持用户的成本,通过自动化的方式,节省精力的同时,还减少人为参与时导致的错误。 目前 TeaDSL 在阿里云的一些 SDK 上已经有所应用,如:https://github.com/aliyun/aliyun-ccp 。阿里云开放平台在持续努力提升它的整个工具支持生态,以期望能建成比 Swagger 更适配的生态体系。
介绍 阿里巴巴近期开源了面向图神经网络(GNN)的框架 Graph-Learn(GL,原 AliGraph)。框架由阿里内部团队研发,研发同学分别来自计算平台事业部 - PAI 团队,新零售智能引擎事业群-智能计算实验室,以及安全部-数据与算法团队。 GL 旨在降低 GNN 应用落地的成本,加速整个 GNN 生态的迭代。阿里内部早在几年前就开始了 GNN 相关的探索,从研究到实际落地积累了很多有价值的经验,我们把这些经验通过 GL 逐渐传递出来,希望能对相关从业者有所帮助。 GL 面向工业场景而设计,为当下主流 GNN 算法提供了基础运行框架。由于起源于工业界,GL 天然支持大规模图数据、异构图、属性图等十分必要但棘手、而当下深度学习框架(TensorFlow、PyTorch 等)又不擅长的问题。同时,考虑到上层 NN 极具面向业务定制化的特点,GL 支持与任意 python 接口的深度学习框架结合。GL 框架轻便灵活,内部模块都预留了充足的扩展空间,方便基于不同的场景进行定制。同时,GL 内置了基于 TensorFlow 实现的各类 GNN 模型和编程接口,供复用和参考。 项目地址: https://github.com/alibaba/graph-learn 设计理念 GNN 是当下 AI 领域中非常热门的子领域,被研究者给予了很高的期待。在一切皆向量的深度学习中,我们希望融入更多的知识,使得深度学习由感知学习迈向认知学习。人类的知识在计算机中依托于图结构而存在,这也是为什么要融合 Graph 和 Neural Network 的缘由。 从深度学习过渡到 GNN,作为开发者,我们深知落地一个 GNN 算法的难点在哪里,阻碍 GNN 被大范围应用的环节是什么。我们也知道 GNN 虽热,但也并不像每个人说的那么好,应用场景的拓宽、算法理论的变化、编程范式的变化都可能带来平台的变化甚至颠覆,面对这些不确定性,平台该做什么。 我们将这些摸爬滚打的经验积累下来,融入到 GL,无论是对 GL 的直接用户,还是参考 GL 做类似系统设计的同学,希望有所助益。只有更多人了解 GNN,玩的起 GNN,由感知到认知的跨越才不会是一句空话。 GL 遵循轻便易用的原则,充分保留内部子模块的扩展性,并兼容开源生态。概括来讲包括:轻量可移植,模块可扩展,接口可复用,生态可兼容。 轻量可移植 和主流深度学习框架一样,GL 平台代码由 C 写成,Linux 系统下,所有支持 C11 的编译器都可以对源码进行编译打包,所依赖的几个外部库也都是开源社区广为人知的。第一次编译过程由于下载外部依赖,需要大概几分钟,后续每次开发编译打包只需秒级,部署成本很低。GL 可运行于物理机,也可运行于 Docker 内,也可基于阿里云的 ACK 服务一键拉起,确保分布式的机器间网络连通即可。 模块可扩展 系统内部实现高度模块化,每个模块都可独立扩展。可扩展给系统足够的弹性,以适应未来发展的不确定性,以及和不同运行环境的适配。可扩展也为广大开发者提供自由伸缩的空间,快速进行原型验证,而不局限于系统当前提供的功能。 存储模块,抽象了 FileSystem 和 Storage 两层,如需扩展自定义存储,一般只需实现 FileSystem 接口即可,或者通过实现 Storage 接口直接外接其他图存储系统。Partition 模块,定义了数据如何在分布式 Server 间分布,以及一个分布式计算请求如何正确的转发,新的 Partition 策略(例如某种更适合业务场景的图切分)只需扩展该模块即可。计算模块,由一个个算子组成,算子可自定义。算子被框架分布式执行,每个算子定义的计算根据指定的Partition策略进行转发,并可通过资源的形式访问全局存储。目前内置的算子种类包括:Sampling,Negative Sampling,Aggregation,Graph Traverse,Graph Query,Graph Update。RPC 模块,解耦于其他模块,只负责请求的收发,很容易对接其他 RPC 框架。Naming 模块,用于分布式地址发现,可方便对接不同的调度环境。 接口可复用 接口可复用体现在两个方面:向后兼容与功能扩展。兼容性不言而喻,很多开发者受困于版本更新后的接口兼容问题。功能扩展,一般的做法是新增 API,意味着在用户能关注到的基础上带来学习成本。新增 API 对用户的困扰相对小于兼容性,但 API 增加也带来庞大的系统维护成本。在 GNN 快速迭代的当下,功能扩展几乎时刻都在发生,比如新增一种图采样算法,而向后兼容更是由于应用场景和算法更迭带来的变化而不容易做到,我们尽可能使得当这些变化发生时,对用户的损失最小。 GL 通过提高 User Interface 的抽象程度,来降低上述风险。我们发现很多 GNN 研究者都有图的背景,因为对 Gremlin 或多或少有了解。Gremlin 是一种抽象的图查询语言,Gremlin 之于 Graph,类似 SQL 之于 Table。GL 设计了一套 Gremlin-Like 的 python 接口,接口内部会翻译成各种算子实现,每新增一种算子,只需改变接口对应的参数即可。例如,用户随机采样某种顶点的邻居,当新增一种采样算法时,只需改动如下: import graphlearn as gl g = gl.Graph() # sample 10 neighbors for each node in this batch by random sampler g.V("vertex_type").shuffle().batch(512).outV("edge_type").sample(10).by("random") # sample 10 neighbors for each node in this batch by new sampler g.V("vertex_type").shuffle().batch(512).outV("edge_type").sample(10).by("new") 在 GNN 模型编程接口方面,我们也进行了一定程度的抽象,且提供了诸多可供参考示例。 生态可兼容 GL 提供 python 形式的用户接口,结果以 NumPy 形式呈现,易于上手。此外,GL 可与当下主流的深度学习框架,如 TensorFlow、PyTorch 等配套使用,丰富上层 NN 的表达能力。在一个 e2e 的 GNN 应用场景中,GL 和深度学习框架之间有良好的互补关系,把计算交给擅长的框架,Graph->GL,Numeric->TensorFlow、PyTorch,这也是我们一贯的原则。 取得成果 GL 已在阿里集团内数十个场景落地,包括搜索推荐、安全风控、新零售、知识图谱等。GL 日常任务数据为规模达百亿级边、十亿级顶点的异构图,且包含百余个混合类型属性。相比之前通过大数据计算任务(如 Map-Reduce)把图数据处理成可供深度学习框架使用的样本的方式,每个模型日均可节省万 CPU 时(core x hour)算力、百 TB 存储,从 GNN 算法开发到上线的周期被缩短到原来的 1/3,而且带来了显著业务效果的提升。此外,GL 获得 2019 世界人工智能大会 SAIL 先锋奖。 应用案例 我们以在安全风控中的各种典型场景为例,来看 GL 的应用效果。阿里巴巴安全部数据与算法团队一直致力于与黑灰产进行对抗,保障用户在淘宝、天猫、闲鱼等相关平台上的使用体验和切身利益。面对各类黑灰产,已经研究出了一系列的算法武器,图神经网络(GNN)是其中重要的防控技术之一。GNN 作为近年来新兴的技术,不仅能考虑节点本身的属性,还能同时考虑到网络结构特征,进而刻画黑灰产的关系、团伙以及产业链信息,在风控场景中取得了广泛的应用和效果增益。将 GNN 应用到风控场景中是一件十分有挑战的事情,我们面临的图结构往往有着以下两个特点: 高度异构:节点和边都丰富多样 数据规模巨大:很多图结构都是亿级节点、数十亿甚至上百亿的边 垃圾注册识别 淘宝每天新注册的用户中,正常用户占绝大多数,但也有许多黑灰产用户伪装成正常用户,企图获取一个账号从而进行刷单、垃圾评论等等活动,我们称这些账户为“垃圾账户”。“垃圾账户”如果注册通过,就可能从事各种危害活动,因此在注册时将其识别并删除非常有必要。我们通过手机号,设备信息,ip 地址等多种关系构建账户与账户之间的连接关系,基于 graph-learn 构建账户和账户之间的同构图以刻画账户的新表征,垃圾注册图模型目前线上已稳定运行近1年,相比于单纯使用账户的特征,每日额外识别 10-15% 的垃圾账号,保持着相当高的识别准确率。 淘系假货识别 阿里巴巴对于知识产权的保护一直都在进行大量的努力,也取得了十分显著的成果。但是仍然有极少部分卖家在淘宝上售卖假货商品,这一直是我们深恶痛绝的。为此,除了应用假货商品本身的特征以外,我们仔细甄别了假货商品和售假卖家之间的多种关系,比如售假卖家之间的团伙关系,物流等产业链关系,并且通过这些关系构建商家-商品之间的异构图,基于 graph-learn 开发的淘宝假货图模型目前已在服饰类、鞋类、首饰等诸多大类中落地,相比于直接使用商品、商家的特征信息,图模型能够额外识别 10% 以上的假货商品。 闲鱼垃圾评论识别 闲鱼是目前国内最大的二手商品交易平台,买卖家可以在商品下面评论进行沟通和询问,但其中也有黑灰产会在商品下面留下一些涉嫌广告、欺诈、假货甚至违禁的评论,影响了用户的使用体验,也给用户带来了风险。为了识别闲鱼上的垃圾评论,我们结合业务特点,自主设计了基于异构图卷积网络的反垃圾系统—— GAS,相对于单节点的深度模型,能够在同样准确率情况下获得16%的覆盖率提升,同时我们将该项目中的方法进行总结,所著文章《Spam Review Detection with Graph Convolutional Networks》发表在信息检索领域顶级会议 CIKM2019 上,并斩获最佳应用论文奖。 恶意评价识别 恶意评价包括评价要挟,同行攻击和虚假评价等多种类型,在淘宝平台上一直是困扰商家的主要问题之一。和传统图模型相比,异构图神经网络通过聚合不同子图的方式消除主观上对强弱边的判断,能够通过图间融合的方式融合不同强度的边信息。在淘系恶意评价的场景上,基于 graph-learn 开发的恶意评价图模型优化了整体数据准备流程,提高训练效率,上线后已稳定运行近半年,日均额外识别7%以上的恶意评价,优化了商家的营商体验。 “职业吃货”识别 淘宝平台上还有一部分“职业吃货”存在,他们在淘宝、饿了吗等平台疯狂下单,收货后却立即申请“仅退款”而拒不退货,继而以职业投诉等方式威逼商家妥协,这就是典型的“职业吃货”行为。对这种滥用淘宝会员权利,损害平台正常运营秩序的人,我们通过各种媒介关系基于 graph-learn 构建“职业吃货”图模型,相对于 GBDT 的传统模型,“职业吃货”图模型在相同准确率的情况下,额外识别了 15% 的恶意买家,保障了商家在平台上的权益。 发表文献 AliGraph: A Comprehensive Graph Neural Network Platform. VLDB, 2019. Representation Learning for Attributed Multiplex Heterogeneous Network. KDD, 2019. Is a Single Vector Enough? Exploring Node Polysemy for Network Embedding. KDD, 2019. Towards Knowledge-Based Personalized Product Description Generation in E-commerce. KDD, 2019. Large Scale Evolving Graphs with Burst Detection. IJCAI, 2019. Hierarchical Representation Learning for Bipartite Graphs. IJCAI, 2019. Cognitive Graph for Multi-Hop Reading Comprehension at Scale. ACL, 2019. 未来规划 围绕 GL,我们未来会在以下几个方面投入精力。GL 良好的扩展性,也使得未来存在更多的想象空间。 新硬件 以图像为代表的应用催化了 GPU 的发展,可以预见,Graph 作为实际生产中最广泛的数据格式,与 NN 的结合,也会引发更多从硬件角度追求卓越性能的思考。目前阿里内部已经在这方面开始了探索。 新算法 近年来 GNN 算法相关的 paper 主要在 GCN 框架下做延伸,编程方式比较固定,目前或多或少遇到了一些瓶颈。我们也开始在算法理论上尝试做一些创新,GL 的易用和可扩展会助力这一过程。 新业务 GNN 覆盖的业务非常广,也会带来很多意想不到的效果。当前落地的应用主要集中在了少数大公司内,还没有普及开来。GL 的开源也希望能够助力相关从业者,共同扩大 GNN 的生态。
什么是全双工自然对话? 如果孙悟空的金箍棒加入天猫精灵语音交互的支持,那么孙悟空在获得金箍棒后会怎样给他的猴儿们演示呢? 在一问一答方式中,孙悟空首先需要唤醒金箍棒,然后再发出大一些的指令,金箍棒在收到指令后会变粗变高并回复已经变大。如果孙悟空要想让金箍棒再大一些,需要再次唤醒金箍棒并发出指令。每次想让金箍棒大一些,孙悟空就要重复这个过程。 在连续对话方式中,孙悟空也要先唤醒金箍棒,然后在发出大一些的指令,金箍棒也会在收到指令后会变粗变高并回复已经变大。如果孙悟空要想金箍棒再大一些,可以选择等待金箍棒的回复结束,然后直接说出大一些的指令。接下来每次想让金箍棒大一些,孙悟空只需要等待金箍棒回复结束,然后直接说出大一些的指令即可。 在自然对话模式中,孙悟空先唤醒金箍棒,然后发出大一些的指令,金箍棒在收到指令后会直接变粗变高。接下来,如果孙悟空想金箍棒在大一些,直接说出大一些指令即可,不需要再次唤醒,也不需要等待回复结束。 天猫精灵有孙悟空演示金箍棒的场景吗?答案是肯定的,例如音量控制就非常相似。那么天猫精灵的全双工自然对话是怎么实现的呢? 技术方案 设备端:负责听和说。主要解决什么时候听,有没有听到语音,听到的语音有多长;什么时候说,以及说什么。 语音识别:即 ASR,将用户的语音识别成文本,并提取声音的特征。 语义理解:即 NLU,负责理解用户说了什么并转换成机器可读的信息。 语音合成:即 TTS,负责将文本转换成语音。 对话管理:即 DM,根据语义理解的结果和会话的上下文信息调用各种服务完成用户的请求。 人机交互识别:根据 ASR 输出的声学特征判断收到语音是不是用户和音箱说的话。 设备端交互管理 用户打开自然对话后,服务端在对用户进行应答的同时会下发自然对话收音指令到设备端。 设备端收到自然对话收音指令后,进入自然对话状态并开始收音。 在自然对话状态下,设备端检测到用户说话时开始发起自然对话交互请求;检测到用户说话结束时结束请求,并上报自然对话状态的持续时长和用户的说话时长到服务端。 设备端收到服务端应答,但是应答中不包含自然对话收音指令时,如果当前处于自然对话状态则退出自然对话。 自然对话收音指令中包含收音的持续时长,如果在这个时长内没有检测到声音也会退出自然对话。 设备端播放管理 在用户交互过程中,需要根据用户的意图决定被打断的播放动作是否需要继续。比如,用户在播放音乐时说“收藏这首歌”,那么在执行用户指令后需要继续恢复播放音乐;而用户在收听天气时问了算术,那么在执行用户的指令后不需要继续播放天气。 所以我们把音箱播放的类型分成 3 类,即需要恢复的播放、不需要恢复的播放以及提示音,并根据当前的播放类型和即将播放的类型来确定是否保存播放状态。 持续时长 用户不会一直和音箱交互,持续的收音和处理会极大的增加云端的处理负担,也没有必要,因此我们需要对自然对话的持续时长进行限制。但是用户和用户交互的时长又不是确定的,因此我们采取了滑窗的计时方式,即每一次用户交互后,我们都会重新计时,等待用户进行交互。 人机交互识别 在自然对话持续的过程中,用户可能在与其它人聊天,我们需要识别出来,并且做到用户无感知。在实际实践过程中,我们以语音识别时提取的声学特征为输入,通过深度学习算法来判断用户是否在与与音箱进行交互。如果发现请求不是用户和精灵的交互,精灵不会做出响应。 会话流 在自然对话持续的过程中,语义理解服务会记住会话的历史,并结合历史来处理用户的请求,通过这种方式,用户的多次连续交互就可以形成一个会话流,与用户的交互也更加自然。例如,用户询问今天说“今天天气怎么样”,精灵会回复今天的天气,如果用户再询问明天的天气,那么用户不用说“明天天气怎么样”,而只需要说“明天呢”。 总结 全双工自然对话是一个系统的工程,涉及到从设备端的前端信号处理、设备端语音交互管理、到云端网关、语音识别、语义理解等整个语音交互链路,涉及到部门多个团队的协作,项目的顺利推进离不开各个团队的通力协作,充分体现了“因为信任所以简单”和“此时此刻非我莫属”的价值观。 全双工自然对话上线后受到了用户的广泛好评,自然对话的用户只要使用精灵,几乎都会使用这个功能来与天猫精灵交互,而这部分用户的活跃度也比其它用户更高。 当然,在提升用户交互体验的道路上,全双工自然对话不是终点,还有很多可以提升的地方,我们会继续努力,为用户提供更好的交互体验。 感谢 衷心感谢参与项目的所有同学,有些同学甚至为此取消了国庆假期的安排。在大家的共同努力下,短短一个多月时间内就完成了全链路方案的实现和落地,让用户可以更早的体验到精灵的黑科技。
一切都是为了更顺滑! 五福已五年陈。除去第一年采用咻一咻方案来完成集福,还未引入图像识福的方案外,今年已是 AR 扫福链路作为识福入口的第四年。有的同学会问,我去年就已经觉得扫福非常顺畅、识别非常准确了,为什么今年还要继续升级模型呢?这让我想起逍遥子在双十一战场上的一句话,"今年双 11,我最关心的不是销售数字,而是技术峰值"。而对应在扫五福的场景上,如何让用户在 AR 识福的体验上更加顺滑、如何在极低配的手机客户端上依旧可以流畅地扫出福来、如何将端上计算的覆盖率触达更多的用户甚至接近100%,这是我们每年必须要去刷新、去突破的方向。 往年的沉淀 在过去三年扫福的视觉算法侧,我们可以总结为两次突破。第一次突破,来自 18 年 xNN 深度学习引擎的引入,支持了识福深度学习网络在端上的运算,显著降低云端服务压力的同时,更大幅提升了识别算法的精度;第二次突破,来自 19 年云端服务完成了 xNN - x86 版本的升级,使得在端云结合的方案背景下,真正地实现端云一体,端上与云上模型完全统一。技术上的突破,带来体感交互更加流畅、识别结果更加准确,就像下面的动图,如果 17 年他可能还是位"漏洞挖掘专家",但在 18 年以后再想"偷袭",恐怕就很难了。 今年的突破 今年为了在用户体验侧、研发流程侧更加顺滑,我们引入了 AutoML。不仅是对训练超参与网络结构的搜索,也是对于整个模型研发流程的自动化。原因可归为如下两点: 网络性能阶越式提升,人工成本高 今年在历年基础上,为了端侧计算触达更多用户、端上资源消耗更低、识别效果更准,我们需要网络模型性能完成进一步的阶越式提升。然而当历年模型已经达到不错的基准水平时,再想单靠人工设计,使得模型性能、精度大幅提升,是需要付出更大的精力与人工成本的。 KA 商户需求、舆情需得到更快速响应 随着 AR 扫商业化的丰富,对于福字识别链路的模型结果,也要快速兼容与满足 KA 商户定向识别的个性化需求。并且对于扫福这种全民参与的活动,遇到识别舆情,快速迭代模型的应急能力,是对于扫福这种活动必须具备的能力。我们需要尽可能地缩短模型迭代的周期,所有的操作、时延、训练时间都是标准可控的,强化模型迭代的应急快反能力。 网络结构设计的自动化 AutoML 能力特性 我们采用了 xNN-Cloud 平台提供的 AutoML 能力,来完成今年福字模型的结构搜索。那么它必须具备如下一些特性: 1)是开箱即用的。在原生的网络结构代码上,无需改造很多,即可使得网络支持搜索。 2)是能够兼顾到端侧指标的。在比较不同搜索实验结果时,不局限于精度,计算量大小 / 参数量大小 / 耗时等因素,也要一同考虑进来。 3)用户交互是友好的。用户只需要关心搜索过程是否符合预期,搜索结果指标是否可以快速比较,而具体的资源调度、GPU 使用等用户无需关心。 下图展示了在 xNN - Cloud 上的一个 AutoML 任务搜索过程的流程图示例,以及指标结果在搜索过程中的动态变化过程。 网络与空间设计 识别链路 检测模型先定位候选目标区域,再由识别模型,来判别区域是否为福。由于检测与识别网络分开,从而遇到定制识别需求(比如识别马老师福)需要模型快速迁移的场景里,可以维持检测模型稳定,只需要在识别模型上进行迁移训练,而不用连带检测网络一同更新。因此总体上,我们有检测与识别两个模型,均需要进行 AutoML 结构搜索。 引入先验 AutoML 搜索,如果训练资源足够多、时间跨度允许足够长,我们会愿意给予机器尽可能多的自由,从而可以在更复杂的空间内进行搜索。然而对于扫福这种业务场景,每年从各部门抽同学组成一个临时的集体,到模型参与测试联调,无非一个月左右的时间。而在这么短的时间内,需要对两个网络完成结构搜索。并且还要保证参数量、计算量比往年模型更小,精度比往年更高。因此,我们对于网络框架上,基于在其他相似业务积累的经验,做了一些先验上的选择。 检测模型 我们从单阶段检测器(SSD)结构出发,增加了 Pyramid Pooling Network(PPN) 的结构,用于在不增加额外参数的情况下,快速获取不同尺度空间上的特征。并且在框位置/类别的回归分支上,使用了权值共享卷积,来将不同尺度上的信息特征,使用相同的权重进行学习,使网络能够对不同尺度下的目标,都有较好的表现。在此基础上,我们以 MobileNet V1 结构为初始 base network,搜索各层的通道宽度以及卷积核大小,并且为了预测 head 可以与 base network feature 匹配的更好, shared tower 回归层的通道宽度,也加入到搜索空间当中。从而使得网络的特征提取与预测 head,在福字检测这个场景均达到最佳适配。 识别模型 我们从 MobileNet V2 结构出发,并且参考 EfficientNet 的方式,在一个较小的网络下搜索,然后通过网络宽度、深度、输入大小的 scaling,来使得基于小模型上搜索出的优秀结构,可以快速扩充模型的容量。并且 Google 的 Searching for MobilenetV3,也是以搜索的方式在 MobileNet V2 基础上,将移动端识别模型做到更加极致。后文也给出了我们 AutoML 的结果与如果直接使用 MobileNet V3 结构的结果比较。下图中展示了,最终结构搜索得到的检测与识别网络结构。 搜索目标 伴随着 AutoML 的搜索过程,会涉及每个 Trial 的结果好坏比较。从而选择相对更优的结构,去进一步在此基础上搜索或者训练。比较的目标基准,在扫福这个任务上,我们要考虑的不仅仅是精度。因为模型最终要在客户端运行,模型的计算量 (FLOPS)、模型的参数量大小 (Model Size),也会成为我们必须关注的指标。因此,在指定比较基准时,我们设计了一个将多种因素同时考虑进来的方式,来对比 AutoML 过程中的不同 Trail。其中 T_f 和 T_s 是期望的目标值,w_f 和 w_s 是用来控制 FLOPS、ModelSize 与 Accuracy 之间的一个 trade-off。 搜索策略 由于搜索资源一定是有限的,因此我们选用了资源消耗、搜索效率上相对最优的 Hyperband 算法。该算法让前序搜索的 Trial,先训练较少步数后比较目标函数,然后从中逐级选择较优的结构,再使用更大的步数进行训练,从而能在搜索结构的训练步数和整体资源之间找到一个平衡。同时为了防止陷入一定概率的局部最优解,在选择首次随机的参数空间时,也会采用多级随机选择。从而整体来看,在保证随机性的同时,也保证了较优结构训练更大步数,最终得到收敛情况最好的模型。 在搜索算法实现上,xNN-Cloud 与蚂蚁人工智能部 ALPS 团队合作,完成了 ALPS-AutoML 框架 Python SDK 的集成,其中超参搜索算法部分,支持 grid、random、bayesian、racos 等各种超参搜索算法;也支持 hyperband、medianstop 等 EarlyStopping 机制;支持灵活的超参数组合配置。同时 ALPS-AutoML 在机器学习其他场景,也支持 xgboost 模型的元学习、高效的自动特征工程等非常丰富的自动机器学习能力。 模型性能 速度更快,大小更小,精度更高 安卓客户端上以 19 年的 top50 机型、ios 客户端上以 19 年的 top20 机型,来作为对比两年推理耗时的基准。从去年和今年的真实线上环境数据来看,安卓平台上相比去年降低了50%以上,平均耗时达到了 100 毫秒内;ios 上降低了 30% 以上,平均耗时40毫秒以内。模型大小上,较去年进一步减少 80K。在耗时、参数大小都比去年更低的情况下,精度较去年进一步提升了 1.6%。这些均来自于在 AutoML 搜索过程中,对于精度、参数量、计算量的综合考虑,使得搜索得到的最终模型结构,可以在这三方面,均得到兼顾。 识别模型 vs Mobilenet-v3 我们也将搜索出的识别网络结构,与 Mobilenet - v3 结构,在福字识别场景做了对比。可以看到当计算量相差不大时(mnv3-small-065),我们 NAS 得到的参数量大小只有 mn-v3 的结构的 1/10。然而对于集五福这种对端上资源大小,也有非常严格限制的业务场景上,原生 mn-v3 的模型大小明显是不合适的。而如果为了尽量减小mn-v3的模型大小,将一些 advanced blocks 舍去,并且将宽度倍率进一步降低时 (mnv3-small-minimalistic-025),模型计算量的确会进一步减少,但是精度上会引来非常明显的下降。因此,这也进一步证明了福字识别 AutoML 搜索结果,对于精度、计算量、参数大小综合考虑的有效性、以及网络对于业务场景的更高适配性。 P.S. 上述表格中展示的识别精度,为线下 9 万张测试用例在 threshold = 0.9 下的图像级精度。在业务场景实际调用时,是基于视频流识别的方案,会有多帧结果的融合与校验,使得总体的线上识别精度几乎趋近于 1。在整个扫福活动期间,没有引起任何规模化的识福舆情的产生与传播。 算法研发流程的自动化 平台基础 xNN-Cloud 提供了一站式视觉算法研发的能力,提供了丰富的功能与算法开发特性(如上图),这也给扫福模型研发流程的自动化,提供了巨大的便利与帮助。 算法开发 内置标准的分类、检测、OCR 等算法包模板,且配合参数配置,无需任务额外代码的情况下,就可以训练得到优质的模型。有定制优化需求时,仅需完成 Common API 的几个抽象接口开发,即可将自定义的网络结构、损失函数、优化方法等,应用在模型训练当中。 模型压缩 配置化的模型压缩能力,极大降低用户感知模型压缩能力的成本。集成的 xQueeze 模型压缩工具链,提供强大的自动剪枝、模型量化、定点化的能力。 数据预览 不局限于看标注结果,同时可以结合模型在数据集的评测结果,做到更多维展示。如分类任务,可以快速查看预测类别与 GroundTruth 类别之间的交叉结果;检测任务,能够直观展示多组预测结果与 GroundTruth 之间的 IoU 情况。 多机多卡 尽可能降低用户使用多机多卡能力的复杂度。通过接入 Kubeflow MPI Operator,帮助用户轻松运行分布式训练。不管在哪种算法场景类型上,用户仅需要选择卡数,即可使用上多机多卡的训练能力,而无需将额外精力放在代码如何在多机上运行起来的、各卡上的梯度是如何汇总更新。 模型评测 整个扫福期间,自动化训练产出上千个模型。如何快速地从这些模型中,选择出较优的那个,是能够大大减少时间成本的。通过评测,可以观察到在独立测试集上的精度结果、PR 曲线等。而且可以将多个模型的结果,放置在同一个面板当中。 真机测试 模型要在客户端侧运行,除了模型大小以外,也需要耗时、CPU/内存占用等指标,来判断模型是否满足端上的运行性能需求。通过接入 EdgePerf 与云测平台,快速实现真机上的模型实测,提供真实、直观的运行性能指标。 模型快反 五福活动期间,如果模型上线之后,根据用户实际扫福的情况,发现需要增加对于定向类别的识别。并且产品还希望在 1 小时内模型能上线。从数据组织到模型训练到格式转换再到模型评测,如果有一步不小心人为操作疏漏了,会有很大风险耽误模型的更新上线时间。然而借助将整个数据准备、训练、评测流程,全部平台化、自动化,那么可想到的犯错机会,与人为离线训练相比,将大大降低! 从而完成在特殊业务需求下的模型快反。 在扫福模型上线之前,我们在 xNN-Cloud 模拟了根据定向图片类型,进行模型快反的训练流程。只需要将定向识别的数据集上传之后,克隆之前的模拟训练任务,并且将新的数据集添加进来,再加上平台提供的多机多卡的能力,从而最终模型训练过程,可以控制在 15 分钟以内。不仅保证了模型结构、大小与预期完全相符,识别结果上也在保持原先其他类别不受影响的前提下,增加了对定向图片的识别。整个过程全部在掌控之中,不再有人为失误的可能,只需要交给平台。这个能力也在鼠年马老师福、商家特定福识别的迁移训练上,得到了应用。在很短的时间内,就在基准模型基础上,迭代出了最终上线的版本,完成模型的快反。 结语 扫福模型的训练,依托于 xNN-Cloud 平台,今年在节省大量人力的情况下,产出了比往年都要更优秀的模型。这离不开 xNN-Cloud 平台提供的 AutoML 与自动化训练的能力。目前 xNN-Cloud 也在将真机测试与搜索过程直接打通,用最接近真实环境的运行指标,指导 AutoML 的搜索。 更多关于 xNN-Cloud 平台,可点击阅读:不写一行代码,完成机器视觉算法的研发
本文作者:搜索推荐事业部认知图谱团队 Xusheng Luo, Luxin Liu, Yonghua Yang, Le Bo, Yuanpeng Cao, Jinhang Wu, Qiang Li, Keping Yang and Kenny Q. Zhu 背景 近年来电商搜索、推荐算法已经取得了长足的进步,但面对用户多样化的需求,目前的电商体验依然还称不上“智能”。多年来,我们的搜索引擎在引导用户如何输入关键字才能更快地找到需要的商品,而这种基于关键字的搜索,适用于对明确清楚具体商品的用户。但很多时候,用户面临的往往是一些问题或场景,如“举办一场户外烧烤”需要哪些工具?在淘宝上购买什么商品能有效“预防家里的老人走失”?他们需要更多的“知识”来帮助他们决策。而在商品推荐中,重复推荐、买过了又推荐、推荐缺少新意等问题也是经常为人诟病。当前的推荐系统更多的是从用户历史行为出发,通过 i2i 等手段来召回商品,而不是真正从建模用户需求出发。 深究这些问题背后的原因,其根源在于电商技术所依赖的底层数据,缺少对于用户需求的刻画。具体来讲,目前淘宝用于管理商品的体系,是一套基于类目 - 属性 - 属性值(CPV,Category-Property-Value)的体系,它缺乏必要的知识广度和深度,去描述和理解各类用户需求,从而导致基于此的搜索、推荐算法在认知真实的用户需求时产生了语义的隔阂,从而限制了用户体验的进一步提升。 为了打破这个隔阂,让电商搜索、推荐算法更好地认知用户需求,我们提出建设一种新的电商知识图谱,将用户需求显式地表达成图中的节点,构建一个以用户需求节点为中心的概念图谱,链接用户需求、知识、常识、商品和内容的大规模语义网络:阿里巴巴电商认知图谱(Alibaba E-commerce Cognitive Concept Net),简称 AliCoCo。我们希望 AliCoCo 能为电商领域的用户理解、知识理解、商品和内容理解提供统一的数据基础。经过两年的努力,我们已经完成了整体的结构设计和核心数据的建设,并在电商搜索、推荐等多个具体的业务场景落地,取得了不错的效果,提升了用户体验。 AliCoCo 如下图所示,AliCoCo 是一个概念图谱,主要由四部分构成: 电商概念层(E-commerce Concepts) 原子概念层(Primitive Concepts) 分类体系(Taxonomy) 商品层 (Items) 在电商概念层(E-commerce Concepts),作为 AliCoCo 最大的创新点,我们将用户需求显式地用一个符合人话的短语表示为图中的节点,如“户外烧烤(outdoor barbecue)”、“儿童保暖(keep warm for kids)”等,并称之为“电商概念”。用户需求虽然一直被提及,但在电商领域,还未被正式地定义过。在很多下游应用(如推荐系统)的工作中,常常用类目或品类节点(商品的分类)作为用户需求的表达。但用户需求是远不止于这些的,很多场合下,用户面临的是一个“场景”或者“问题”,他们并不知道具体什么商品可以帮助解决,因此我们将用户需求的定义进一步泛化为电商概念,具体详见下文章节。所有用于表示用户需求的电商概念组成了这一层。 在原子概念层(Primitive Concepts),我们为了更好地理解上面讲到的电商概念(即用户需求),我们将这些短语进行拆解细化到词粒度,用这些细粒度的词来更系统地描述用户需求,这些细粒度的词称为“原子概念”。如对于电商概念“户外烧烤”而言,它可以被表示成“动作:烧烤 & 地点:户外 & 天气:晴”,这里的“烧烤”、“户外”和“晴”都是原子概念。所有原子概念组成了这一层。 在分类体系(Taxonomy)中,为了更好地管理上述的原子概念,我们构建了一个描述大千世界基本概念的分类体系,它不局限于电商领域,但目前是为电商领域的概念理解所服务。在这一层中,我们定义了诸如“时间”、“地点”、“动作”、“功能”、“品类”、“IP”等一级分类(class),并在每个分类下继续细分出子分类,形成一颗树形结构。在每个分类中,包含了分类的实例(instance),即原子概念,如上述的“烧烤”、“户外”和“晴”就分属于“动作 - 消耗性动作”、“地点 - 公共空间”和“时间 - 天气”。同时,不同分类之间有不同的关系(relation),如“品类 - 服饰 - 服装 - 裤子”和“时间 - 季节”之间定义了一个“适用于(季节)”的关系。因此,相应的会有一条三元组实例:<棉裤,适用于,冬季>。 如果将上述的分类体系和原子概念层合起来,实际上可以看做一个相对完整的本体(Ontology),它和 Freebase、DBpedia 等大家熟知的开放领域的知识图谱非常相似,唯一的区别是我们的实例不仅有实体(entity),还包括了大量的概念(concept)。而相比 Probase,ConceptNet 等概念图谱,我们又定义了一套完整的类型系统(type system)。 在商品(内容)层,阿里巴巴平台上数十亿的商品和内容,将会和电商概念、原子概念层进行关联。如和“户外烧烤”相关联的商品可能会包括烧烤架、炭火、食材等等。但这里要注意的一点是,有些商品可以关联到“户外烧烤”这个电商概念,但不一定可以和相应的原子概念“户外”直接关联。对于商品来说,电商概念像是这个商品会被用于的某个场景,而原子概念更像是细粒度的属性,用于刻画商品的特性。 综上所述,在 AliCoCo 的体系中,用户需求被表达成短语级别的电商概念。在这之下,有一套定义完备的分类体系和原子概念实例去描述所有的电商概念。最后,电商平台上的所有商品都会和电商概念或是原子概念相关联。下面,我们详细介绍每一层的细节以及在构建过程中所遇到的算法问题。 分类体系(Taxonomy) AliCoCo 的分类体系是一个巨大的树形结构,包含了百万级别的原子概念实例。由于分类体系的构建,对专家知识的要求非常高,并且这部分的设计对于整个知识体系都至关重要,因此我们人工定义了约 20 个一级分类(下图),其中专为电商领域所设计的有:“品类”、“图案”、“功能”、“材质”、“花色”、“形状”、“气味”、“口味”。每个一级分类还会继续细分为二级、三级,直至叶子分类,其中对于电商领域最为重要的“品类”包含了约800个叶子分类。诸如“时间”、“地点”、“受众”、“IP”等分类和开放领域的知识图谱可以交融,如“IP”中包含了大量的明星、运动员、电影、音乐等。 原子概念层 (Primitive Concepts) 在原子概念层,我们希望这些细粒度的词能够去完整地描述所有的用户需求,这是用于组成电商概念的基础,在这一层,我们主要讨论两个问题: 原子概念词汇的挖掘 原子概念之间的上下位关系构建 词汇挖掘 在定义好分类体系之后,一般有两种方式快速扩充分类下的实例(词汇)。第一种是融合多种来源的结构化数据,这种方法采用的技术通常是本体对齐(ontology matching),在实践过程中,我们主要采用规则+人工映射的方式将不同来源的结构化数据对齐到我们的分类体系进行词汇的融合。第二种是通过在大规模的语料上进行自动挖掘来补充分类下的词汇,这里我们将其定义为序列标注任务,并采用基于 BiLSTM+CRF [1] 的模型来挖掘发现分类下的新词。由于叶子分类的数量过于庞大,我们使用一级分类作为label,先对词汇进行粗粒度的挖掘。 上图为 BiLSTM+CRF 模型的简单示意,BiLSTM(双向 LSTM)层用于捕捉句子上下位的语义特征,而 CRF(条件随机场)层则用于捕捉当前词的 label 和前后词 label 之间的相关性。而在模型挖掘得到可能属于某个分类的新词之后,后续还会经由众包投放审核、外包质检等人工把关环节,最终才会入库成为真正的原子概念。不同的原子概念可能拥有相同的名字,但分属不同的类别,代表了不同的语义,每个原子概念有一个 ID,这也是 AliCoCo 未来可以用概念消歧的基础。 上下位关系构建 在某个一级分类下的词汇挖掘到一定量后,我们需要继续将所有词汇分到不同层次的类别中去,这个过程可以抽象成为一个上下位关系发现(hypernym discovery)的过程:给定一个下位词,在词表中找到其可能的上位词。我们采用基于 pattern 的无监督方法和基于 projection learning 的监督方法两种方式结合来完成上下位关系的构建。 Pattern based 基于 pattern 的方式 [2] 是最直观且准确率最高的方法,通过归纳和发现一些可用于判断上下位关系的 pattern,从文本句子中直接抽取上下位词对。典型的 pattern 如“XX,一种XX”、“XX,包括XX”等。但这种方式的缺点是默认上下位词对在句子中必须共现,会影响召回。此外,利用中文的一些特点,我们可以用过“XX裤”一定是“裤子”等来自动构建起一批置信度较高的上下位关系。 Projection learning Projection learning 的方式是给定一个下位词 embedding 和上位词 embedding ,有监督去学习一个映射函数 ,使得 和 尽可能地接近。这方面有很多前人的工作 [3, 4],其中有一些工作会先将不同的词进行聚类,在每个类别上分别学习不同的映射,取得了较好的效果。具体地,我们学习一个打分函数,用于表征一对候选词之间的上下位关系强弱,并使用多个 matrix 来模拟不同维度的特征(隐式的聚类),其中第 k个 score 计算如:。最后将 k 个 score 过一层全连接得到最终的probability:。之后我们采用交叉熵损失函数进行训练。模型中使用的预训练的词向量是在前面提到的电商语料上用 word2vec 进行训练的。同时,我们针对部分品类词在语料中出现较为稀疏的问题,用 ALaCarte embedding [5] 进行了强化,其主要思想是学习一个映射关系矩阵 ,利用稀疏词 周围的 context 的 embeddings 之和 对其进行表征: 而 可以通过利用语料中所有的词 进行训练得到: Active learning 模型产出候选和众外包审核是一个同时进行的过程,人工审核的数据可以不断反哺强化模型。因此,我们在迭代的过程中,考虑用 active learning 来进一步提升效率,降低人工审核的成本。我们采用了一种 uncertainty and high confidence (UCS) 的 sampling strategy,除了考虑模型难以判断正负的样例之外(预测值接近 0.5),我们还额外添加了一定比例的高置信度判正的样例一起送标,这是因为在上下位关系的判别中,很容易被诸如同义或者相关关系所干扰,尤其在前期样本数量少且质量不一,以及负采样不均衡的情况下,模型对于区分相关关系和上下位的表现不是太好。而通过人工标注纠正这样的判断错误,可以及时惩罚这一类的误判。实验表明这样的策略可以帮助我们减少 35% 的人力成本。 电商概念层 (E-commerce Concepts) 在电商概念层,每一个节点代表了一种购物需求,这种购物需求可以用至少一个原子概念来描述。我们首先介绍电商概念的定义,然后介绍电商概念是如何被挖掘和生成的,最后介绍电商概念和原子概念之间的链接。 电商概念的定义 我们定义一个符合标准的电商概念,需要满足以下要求: 1)有消费需求 即一个电商概念必须可以让人很自然地联想到一系列商品,反例如“蓝色天空”、“母鸡下蛋”等就不是电商概念。 2)通顺 反例如“仔细妈咪肥皂”等就不是电商概念。 3)合理 即一个电商概念必须符合人类常识,反例如“欧式韩风窗帘”、“儿童性感连衣裙”等就不是电商概念,因为一个窗帘不可能即是欧式还是韩风的,而我们通常不会用性感去修饰一件儿童的连衣裙。 4)指向明确 即一个电商概念必须有明确的受众,反例如“儿童宝宝辅食”等就不是电商概念,因为儿童的辅食和宝宝的辅食差别较大,会造成用户的疑惑。 5)无错别字 反例如“印渡神油”等。 电商概念的生成 我们采用一个两阶段的方式来生成电商概念:首先我们用两种不同的方式生成大量的候选,然后用一个判别模型来过滤那些不满足我们的标准的候选。 候选生成 候选生成有两种方式,一种是从文本语料中去挖掘可能的短语,这里我们采用了 AutoPhrase [6] 在大规模的语料上进行挖掘,语料包括电商生态内的 query log,商品的标题、评论,还有很多达人商户写的购物攻略等。另一种方式是用词粒度的原子概念进行组合生成短语粒度的电商概念。我们挖掘并人工审核了一些 pattern 来赋值生成,部分 pattern 如下图所示: 我们可以通过“[事件]用的功能”这个 pattern 来生成“旅游用的保暖帽子”这样的电商概念。而这些 pattern 可以和下面的判别过程结合,通过迭代的方式来进行不断地挖掘和补充。 电商概念判别 判断一个候选短语是否满足电商概念的要求,最大的挑战是上文提到的第三点,即“合理”,要符合人的常识。其他一些要求我们可以通过字级别或是词级别的语言模型就能过滤掉大部分的 badcase,但常识错误的识别对机器来说是非常困难的。此外,电商概念判别任务中的候选短语又严重缺少上下文信息,进一步增加了判别的难度。 为了解决这个难题,我们设计了一种知识增强的判别模型(如下图所示),整体是一个 Wide&Deep [7] 的结构。在 Deep 侧,我们利用字级别和词级别的 BiLSTM 来提取特征,同时对于词级别的输入,我们还加入了一些词性特征如 POS tag 和 NER label 等。为了进行知识增强来辅助常识理解,我们将部分词链接到 Wikipedia 上,如“性感”就可以找到对应的页面。然后将页面上的 gloss(通常是一段简单的介绍)用 Doc2vec [8] 的方式进行 encode 得到知识表达。在经过 self-attention + max-pooling 之后将两者融合。在 Wide 侧,我们主要计算了 concept 的一些统计特征,包括了 BERT [9] 语言模型产出的 ppl 值。最后,通过一个全连接层我们得到最终衡量一个候选短语是否符合电商概念要求的分数。 我们希望模型能辅助我们过滤掉大量的 badcase,此后我们对模型判别正确的电商概念通过众包投放审核和外包多轮质检的方式来保证数据质量。同时,审核入库的数据会继续迭代地帮助模型进一步提高准确率。 和原子概念的链接 对于那些通过从原子概念组合而得到的电商概念,它们天然地和原子概念关联了起来,但对于那些从文本中直接挖掘得到的短语概念,我们需要进一步将它们和原子概念层进行链接,以便更好地去理解和描述这些用户需求。回顾前文提到的电商概念“户外烧烤”,我们需要预测“户外”是一个“地点”,“烧烤”是一个“动作”。但“烧烤”在我们的体系中也有可能是一个“电影”,所以这里的难点在于如何进行消歧。我们把这个任务定义为一个短文本的 NER 任务,由于电商概念普遍只有 2-3 个词组成,缺少上下文也让这个任务具有挑战。 为了解决这个问题,我们设计了一种文本增强的方式,对短文本中待链接的词进行外部上下文的补充,用以为消歧带来额外的信息辅助。模型如上图所示,左边部分是比较常规的特征抽取,右边是一个信息增强的模块。我们将目标词映射到高质量的外部文本中,通过 doc2vec 将其周边的上下文信息 encode 成 embedding,最终最为额外的输入融合到最终的表达中。此外,由于部分电商概念中的原子概念可以属于多个类型,如“乡村半身裙”中的“乡村”,既可以是“地点”,也可以是“风格”。因此我们将 CRF 层改为 Fuzzy-CRF [10],用以建模多个正确的 label 序列: 商品关联 (Item Association) 在构建完原子概念和电商概念层之后,最重要的是将电商平台上的所有商品进行关联。前面提到原子概念更像是属性,因而我们更关注商品与电商概念的关联,因为后者表达的是一个用户需求,常常有着较为复杂的语义。此外,电商概念与商品的关联不能直接从对应的原子概念到商品的关联组合得到,因为会出现“语义漂移”的问题。例如“户外烧烤”所需要的商品,往往和属性“户外”没有任何关系。我们将这个问题抽象为一个语义匹配(semantic match)[11, 12] 的问题,因为现阶段我们暂时只用到商品侧标题的信息(实际上商品是一个多模态的结构,有着非常丰富的文本、图像甚至越来越多的商品开始有了短视频的介绍)。这个任务最大的挑战依旧在于我们的电商概念非常简短,直接进行匹配,往往会遇到诸如某些不那么重要词对结果产生了巨大的影响等问题。 针对上述难点,我们在语义匹配模型上引入了一些必要的外部知识来提升性能。具体模型如上图所示,除了常规的特征抽取,attention 注意力机制模块等建模商品和电商概念之间的关联外,我们主要做了两个地方的增强: 1)引入了电商概念对应的原子概念的特征表达,增加了类型等结构化信息。 2)引入外部 Wikipedia gloss,对部分词进行知识增强,以更好地建立与商品之间的关联。引入这些知识带来的典型的优点,例如在关联“中秋节送礼”时,可以把不包含中秋节字样的的月饼类商品给排上来。 应用 目前,AliCoCo 已经基本完成了 1.0 版本的建设,共包含 2.8m 的原子概念,5.3m 的电商概念,超过千亿级别的关系。淘宝天猫上超过 98% 的商品均已纳入到 AliCoCo 的体系之中,平均每个商品关联了 14 个原子概念和 135 个电商概念。通过对用户需求的统计,相较于之前的商品管理体系,AliCoCo 对于搜索 query 中用户需求的覆盖从35%提升到了 75%。 AliCoCo 已经支持了阿里巴巴集团核心电商的多个业务应用,这里我们主要介绍在电商搜索和推荐上已经落地的、正在进行的,以及将要进行的一些应用。 电商搜索 相关性是搜索引擎的核心问题,其最大的挑战在于用户输入的 query 和商品端之间存在语义隔阂。AliCoCo 中已经为大量的原子概念和电商概念关联了相应的商品,为商品理解提供了从用户视角出发的大量标签,同时 AliCoCo 包含了大量的同义和上下位关系,这些数据帮助了搜索相关性取得显著的提升,从而进一步改善了用户体验。 语义搜索和自动问答一直人们对于搜索引擎的梦想。在电商的场景中,我们可以充分发挥 AliCoCo 的优势,当用户搜索命中电商概念的时候,通过一个知识卡片的形式透出该电商概念下多样化的商品,类似 Google 的知识图谱帮助搜索引擎在用户检索一些实体时透出知识卡片。如上图(a)所示,当用户在淘宝搜索“烘焙”时,命中了相应的电商概念“烘焙工具”,于是会透出一个卡片,上面的商品按照不同品类来进行排序展示。此外,我们还可以透出一些对于烘焙知识的文字解释用于辅助用户进行决策。而电商场景中的自动问答,更多出现在语音交互的场景中,我们可以在家里问天猫精灵“周末要组织一场户外烧烤,我需要准备哪些东西?”,AliCoCo 可以为这样的场景提供底层知识的支持。 电商推荐 目前电商推荐主要以商品推荐的形式为主,但为了满足用户丰富多样的购物需求,我们也需要为用户做一些主题式的推荐,让用户能够明显感知到推荐系统能更人性化地在满足其购物需求。AliCoCo 中的电商概念,正是为了表达用户需求,同时 2-3 个词的长度也非常适合直接推送给用户。如上图(b)中所示,在手机淘宝首页信息流推荐中,我们在商品坑位之间插入了以电商概念为主题的知识卡片,当用户点击卡片时,就会跳到相应的页面,展示该电商概念下的商品。这个应用目前已经稳定运行了超过一年,满足了用户多样化的推荐需求,进一步提升了用户的满意度。 此外,电商概念简短的文字也非常适合用作推荐理由展示在商品坑位中,进一步吸引用户,如上图(c)所示。AliCoCo 为可解释的推荐提供了数据基础。 总结 为了支持电商技术从个性化时代全面迈入认知智能时代,我们投入了巨大的心血和努力探索并构建了全新一代的电商知识图谱 AliCoCo,目前 AliCoCo 已成为阿里巴巴电商核心引擎的底层基础,赋能搜索、推荐、广告等电商核心业务。同时,通过海量的线上用户反馈,AliCoCo 也在不断地对其自身的结构和数据进行补充与完善,形成了一个良性生长的循环。对于 AliCoCo 2.0 的方向,我们未来考虑: 1)继续补充大量电商常识性关系,如将电商概念与原子概念的链接,从短文本 NER 扩展成属性推理任务,我们需要为“男孩T恤”预测出“季节:夏天”,尽管“夏天”没有出现在文本之中,这样的购物常识对于进一步理解用户需求、改善购物体验是非常有帮助的。 2)将电商概念和商品之间的关系建模为概率分,根据分数来进一步将用户体感进行分层,让用户有更明显的感知。 3)AliCoCo 将响应集团国际化和本地化的战略,朝着多语言(multi-lingual)和餐饮等方向进行探索。 参考文献 [1] Zhiheng Huang, Wei Xu, and Kai Yu. 2015. Bidirectional LSTM-CRF models for sequence tagging. arXiv (2015). [2] Marti A. Hearst. 1992. Automatic acquisition of hyponyms from large text corpora. In Proceedings of the 14th conference on Computational linguistics - Volume 2, volume 2, pages 539–545. Association for Computational Linguistics. [3] Josuke Yamane, Tomoya Takatani, Hitoshi Yamada, Makoto Miwa, and Yutaka Sasaki. 2016. Distributional hypernym generation by jointly learning clus- ters and projections. In Proceedings of COLING 2016, the 26th International Conference on Compu- tational Linguistics: Technical Papers, pages 1871– 1879. [4] Dmitry Ustalov, Nikolay Arefyev, Chris Biemann, and Alexander Panchenko. 2017. Negative sampling improves hypernymy extraction based on projection learning. In Proceedings of the 15th Conference of the European Chapter of the Association for Compu- tational Linguistics: Volume 2, Short Papers, pages 543–550, Valencia, Spain. Association for Compu- tational Linguistics. [5] Khodak, Mikhail, et al. "A la carte embedding: Cheap but effective induction of semantic feature vectors." arXiv preprint arXiv:1805.05388 (2018). [6] Jingbo Shang, Jialu Liu, Meng Jiang, Xiang Ren, Clare R Voss, and Jiawei Han. 2018. Automated phrase mining from massive text corpora. IEEE Transactions on Knowledge and Data Engineering 30, 10 (2018), 1825–1837. [7] Heng-Tze Cheng, Levent Koc, Jeremiah Harmsen, Tal Shaked, Tushar Chandra, Hrishi Aradhye, Glen Anderson, Greg Corrado, Wei Chai, Mustafa Ispir, et al. 2016. Wide & deep learning for recommender systems. In Proceedings of the 1st Workshop on Deep Learning for Rec- ommender Systems. ACM, 7–10. [8] Quoc Le and Tomas Mikolov. 2014. Distributed representations of sen- tences and documents. In International conference on machine learning. 1188–1196. [9] Jacob Devlin, Ming-Wei Chang, Kenton Lee, and Kristina Toutanova. 2018. Bert: Pre-training of deep bidirectional transformers for language understanding. arXiv preprint arXiv:1810.04805 (2018). [10] Jingbo Shang, Liyuan Liu, Xiang Ren, Xiaotao Gu, Teng Ren, and Jiawei Han. 2018. Learning named entity tagger using domain-speci c dictionary. arXiv preprint arXiv:1809.03599 (2018). [11] Po-Sen Huang, Xiaodong He, Jianfeng Gao, Li Deng, Alex Acero, and Larry Heck. 2013. Learning deep structured semantic models for web search using clickthrough data. In Proceedings of the 22nd ACM international conference on Information & Knowledge Management. ACM, 2333–2338. [12] Liang Pang, Yanyan Lan, Jiafeng Guo, Jun Xu, Shengxian Wan, and Xueqi Cheng. 2016. Text matching as image recognition. In Thirtieth AAAI Conference on Arti cial Intelligence.
1.背景 先来解释一下本文中出现的专有名词。 情报:是一种文本、图片或视频等信息,用来解决高德地图生产或者导航中的具体问题,本质上是指与道路或交通相关的知识或事实,通过一定空间和时间通知给特定用户。 用户反馈:是指用户借助一定的媒介,对所使用的软件等提供一些反馈信息,包括情报、建议和投诉等。 典型的用户反馈类型和选项如下图所示: 2.问题及解法 用户反馈的方式可以通过手机的 Amap 端、PC 端等进行上报,上报时选择一些选择项以及文本描述来报告问题,以下是一个用户反馈的示例,其中问题来源、大类型、子类型和道路名称是选择项,用户描述是填写项,一般为比较短的文本。这些也是我们可以使用的主要特征。 每个用户在上报了问题之后,均希望在第一时间内问题能够得到解决并及时收到反馈。但是高德每天的用户反馈量级在几十万,要想达到及时反馈这个目标非常的不容易。 针对这些用户反馈信息,当前的整体流程是先采用规则进行分类,其中与道路相关的每条反馈都要经过人工核实,找到用户上报的问题类型和问题发生的地点,及时更新道路数据,作用于导航。 具体一条反馈的操作需要经过情报识别、情报定位、情报验证等环节: 1) 情报识别主要是判断问题类型即给情报打标签 分析用户上报的信息包括问题来源、大类型、子类型和用户描述等 查看上传的图片资料,包括手机自动截图和用户拍照 2) 情报定位主要是找到问题发生的位置信息即定位坐标 分析用户反馈问题时戳的位置点即戳点的有效性 查看用户上报问题时车辆行驶的位置即自车位置 分析用户使用高德软件过程中的规划和实走轨迹等日志信息 3) 情报验证:通过以上两步确定了情报标签和位置坐标,此环节需要验证情报标签(含道路名称) 分析影像和大数据热力图或路网基础数据 查看用户上传的资料和采集的多媒体图片资料 整个业务处理流程如下图所示: 在处理用户反馈问题整个过程秉持的原则是完全相信用户的问题存在。若用户上报的信息不足以判断问题类型和问题发生地点,则会尽量通过用户规划和实走轨迹等日志信息进行推理得出偏向用户的结论。 目前整个用户反馈问题处理流程存在的主要问题有:规则分发准确率低,人工核实流程复杂、技能要求高且效率低,去无效误杀严重等。 为了解决以上问题,我们希望引入机器学习的方法,以数据驱动的方式提高作业能力。在目标具体实现的探索过程中,我们首先对业务进行拆解及层级化分类,其次使用算法来替代规则进行情报分类,再次工程化拆解人工核实作业流程为情报识别、情报定位和情报验证等步骤,实现单人单技能快速作业,最后将工程化拆解后的情报识别步骤使用算法实现其自动化。 3.机器学习解题 3.1 业务梳理与流程层级化拆解 原始的用户反馈问题经由规则分类后,再进行人工情报识别、定位和验证,最终确认问题及其所在是属于近百种小分类项中的哪一个,进而确定上一级分类以及整个层级的对应关系。 由此可以看出,整个问题处理流程只有一个步骤,处理过程相当复杂,对人工的技能要求很高,且效率低下。而且一千个人眼中就有一千个哈姆雷特,个人的主观性也会影响对问题的判断。 针对这种情况,我们对原有业务流程进行梳理和拆解,希望能够利用机器学习和流程自动化等方式解决其中某些环节,提升整体问题处理的效率。 首先进行有效情报和无效情报的分类即去无效,接着将整个流程拆解为六个层级,包括业务一级、业务二级、业务三级、情报识别、情报定位和情报验证。 如上图所示,拆解后的前三个级别为情报分类环节,只有后三个级别需要部分人工干预,其他级别均直接自动化处理。这样通过层级化、自动化和专人专职等方法极大地简化了问题同时提高了效率。 3.2 业务与模型适配 我们可以看到用户反馈中既有选择项又有输入项,其中选择项如问题来源等都是有默认值的,需要点击后选择相应细分项,用户不一定有耐心仔细选择,有耐心的用户可能会由于不知道具体分类标准而无法选择正确的分类。而用户描述,是需要用户手动输入的内容,是用户表达真实意图的主要途径,是一条用户反馈当中最有价值的内容。 用户描述一般分为三种情况:无描述、有描述但无意义的、有描述且有意义的。前两种称之为无效描述,后一种称之为有效描述。 根据业务拆解结果,业务流程第一步即为去无效,在这之后,我们将有效、无效描述的用户反馈进行区分,分别建立相应的流程进行处理。 1) 有效描述的用户反馈,逐级分类,第一级分为数据、产品、转发三类,其中产品和转发两类直接进行自动化处理,数据类别会在第二级中分为道路和专题,专题是指非道路类的限行、步导、骑行等。 2) 无效描述的用户反馈,进行同样的分类,并走一样的流程,但是样本集和模型是不同的,并且最后没有算法处理的步骤,直接走人工或者规则处理。 3) 最终根据实际业务需要进行层层拆解后形成了下图所示的业务与模型适配的结构。 由以上分析可见,情报分类和情报识别均为多分类的文本分类问题,我们针对各自不同的数据特点,进行相应的操作: 情报分类,每一级类别虽不同,但是模型架构却是可以复用的,只需要有针对性的做微小改动即可。且有以前人工核实过(包含情报识别、情报定位、情报验证等过程)具有最终结果作为分类标签的历史数据集作为真值,样本集获得相对容易。 情报识别,其分类标签是在情报验证之前的中间结果,只能进行人工标注,并且需要在保证线上正常生产的前提下,尽量分配人力进行标注,资源非常有限。所以我们先在情报分类数据集上做 Finetuning 来训练模型。然后等人工标注样本量积累到一定量级后再进行情报识别上的应用。 3.3 模型选择 首先,将非结构化的文本用户描述表示成向量形式即向量空间模型,传统的做法是直接使用离散特征 one-hot 表示,即用 tf-idf 值表示词,维度为词典大小。但是这种表示方式当统计样本数量比较大时就会出现数据稀疏和维度爆炸的问题。 为了避免类似问题,以及更好的体现词语之间的关系如语义相近、语序相邻等,我们使用 word embedding 的方式表示,即 Mikolov 提出的 word2vec 模型,此模型可以通过词的上下文结构信息,将词的语义映射到一个固定的向量空间中,其在向量空间上的相似度可以表示出文本语义上的相似度,本质上可以看作是语境特征的一种抽象表示。 其次,也是最重要的就是模型选择,相对于传统的统计学习方法复杂的特征工程步骤,深度学习方法更受青睐,NLP 中最常用的是循环神经网络 RNN,RNN 将状态在自身网络中循环传递,相对于前馈神经网络可以接受更广泛的时间序列结构输入,更好的表达上下文信息,但是其在训练过程中会出现梯度消失或梯度爆炸等问题,而长短时记忆网络 LSTM 可以很好的解决这个问题。 3.4 模型架构 将每个用户反馈情报的词向量结果作为 LSTM 的输入,接着将 LSTM 的最后一个单元的结果作为文本特征,与其他用户选择项问题一起 merge 后作为模型输入,然后经过全连接层后使用 softmax 作为输出层进行分类,得到的 0~1 之间的实数即为分类的依据。多分类的网络架构如下图所示: 4.实战经验总结 理清业务逻辑、确定解题步骤、确认样本标注排期并跑通了初版的模型后,我们觉得终于可以松一口气,问题应该已经解决过半了,剩下的就是做做模型调参和优化、坐等样本积累,训练完模型就可以轻松上线了。 但实际情况却是面临着比预想更多的问题和困难,训练数据量不够、单个模型效果不好、超参设置不理想等问题接踵而至,漫长而艰难的优化和迭代过程才刚刚开始。 4.1 Fine-tuning 选定了模型之后,情报识别首先面临的问题是样本量严重不足,我们采用 Fine-tuning 的办法将网络上已经训练过的模型略加修改后再进行训练,用以提升模型的效果,随着人工标注样本逐渐增加,在不同大小的数据集上都可以取得大约 3 个百分点的提升。 4.2 调参 模型的调参是个修炼内功炼制金丹的过程,实际上取得的效果却不一定好。我们一共进行了近 30 组的调参实验,得出了以下饱含血泪的宝贵经验: 1) 初始化,一定要做的,我们选择 SVD 初始化 2) dropout 也是一定要用的,有效防止过拟合,还有 Ensemble的作用。对于 LSTM,dropout 的位置要放到 LSTM 之前,尤其是 bidirectional LSTM 是一定要这么做的,否则直接过拟合。 3) 关于优化算法的选择,我们尝试了 Adam、RMSprop、SGD、AdaDelta 等,实际上RMSprop和Adam效果相差不多,但基于Adam可以认为是RMSprop 和 Momentum 的结合,最终选择了Adam。 4) batch size 一般从 128 左右开始调整,但并不是越大越好。对于不同的数据集一定也要试试 batch size为 64 的情况,没准儿会有惊喜。 5) 最后一条,一定要记住的一条,尽量对数据做 shuffle。 4.3 Ensemble 针对单个模型精度不够的问题,我们采用 Ensemble 方式解决,进行了多组试验后,最终选定了不同参数设定时训练得到的最好模型中的5个通过投票的方式做 Ensemble,整体准确率比单个最优模型提高 1.5 个百分点。 另外为了优化模型效果,后续还尝试了模型方面的调整比如双向 LSTM 和不同的 Padding 方式,经过对比发现在情报识别中差异不大,经分析是每个用户描述问题的方式不同且分布差异不明显所致。 4.4 置信度区分 当情报识别多分类模型本身的结构优化和调参都达到一定瓶颈后,发现模型最终的效果离自动化有一定的差距,原因是特征不全且某些特征工程化提取的准确率有限、类别不均衡、单个类别的样本数量不多等。 为了更好的实现算法落地,我们尝试进行类别内的置信度区分,主要使用了置信度模型和按类别设定阈值两种办法,最终选择了简单高效的按类别设定阈值的方法。 置信度模型是利用分类模型的标签输出结果作为输入,每个标签的样本集重新分为训练集和验证集做二分类,训练后得到置信度模型,应用高置信的结果。 在置信度模型实验中,尝试了 Binary 和 Weighted Crossentropy、Ensemble 的方式进行置信度模型实验,Weighted Crossentropy 的公式为: 为了避免溢出,将公式改为: 其中,表示: 实验的结果是 Binary 方式没有明显效果提升,Ensemble 在 95% 置信度上取得了较高的召回率,但是没有达到 98% 置信度的模型。 借鉴了情报分类算法模型落地时按照各个类别设定不同 softmax 阈值的方式做高置信判断即按类别设定阈值的方式,在情报识别中也使用类似的方法,取得的效果超过了之前做的高置信模型效果,所以最终选择了此种方式,这部分可以很大地提高作业员的作业效率。同时为了减少作业员的操作复杂性,我们还提供了低置信部分的 top N 推荐,最大程度节省作业时间。 5. 算法效果及应用成果 5.1 情报分类 算法效果:根据实际的应用需求,情报分类算法的最终效果产品类准确率 96% 以上、数据类召回率可达 99%。 应用成果:与其他策略共同作用,整体自动化率大幅提升。在通过规则优化后实际应用中取得的效果,作业人员大幅度减少,单位作业成本降低 4/5,解决了用户反馈后端处理的瓶颈。 5.2 情报识别 算法效果:根据使用时高置信部分走自动化,低置信走人工进行标注的策略,情报识别算法的最终效果是有效描述准确率 96% 以上。 应用成果:完成情报标签分类模型接入平台后通过对高低置信标签的不同处理,最终提升作业人员效率 30% 以上。 6. 总结与展望 通过此项目我们形成了一套有效解决复杂业务问题的方法论,同时积累了关于 NLP 算法与业务紧密结合解题的实战经验。目前这些方法与经验已在其他项目中很好的付诸实施,并且在持续的积累和完善中。在不断提升用户满意度的前提下,尽可能的高效自动化的处理问题,将产品的每一个细节争取做到极致, 是我们前进的原动力和坚持不懈的目标。
当我做完设计相关的培训分享过后,有同学来问我:如何才能快速提升自己的设计能力?我觉得这个问题非常有代表性,代表了一大波程序猿在艰辛的修炼路上的心声。现将我对这个问题的思考、心得体会分享出来,供大家参考,也欢迎提出不同的意见与看法,共同探讨。 一 编码历练 代码行经验是个非常重要的东西,当你还没有 1 万行代码经验的时候,你来问如何提升设计能力的问题,我只能告诉你不要太纠结,理论看看就好,老老实实写代码吧。 据说,一个程序员平均每天码代码的速度是 200~300 行,你可能会说,我一天怎么也要写上 1000 行吧,别忘了,当你码完代码后,你还需要测试、调试、优化、BUG Fix,这些时间你没法一直码代码的。 编码规范就不多说了,如果你的代码还是杂乱无章的状态,就先别谈什么设计与架构了,我会觉得有点扯淡。 另外,作为代码洁癖患者,推荐大家不要把代码写完后,批量格式化处理,或者手工再去整理代码,而是每敲一个字符上去,它都是符合规范的。习惯真的很重要,有时在招聘面试的时候,我真想添加一个环节,现场编写程序完成一个简单但容易出错的任务。 二 理论学习 简单说就是看书,看博客,你所能得到的资源,质量高的就行。例如:《重构 - 改善既有代码的设计》、《敏捷软件开发:原则、模式与实践》、《UML 和模式应用》、"面向对象设计原则"(五大原则)、《设计模式》等。 《设计模式》这本书是很古老的一本书了,只有短短 200 页,但是,这是最难看懂的一本书,一个月都可能看不完(看小说的话,200 页 3 个小时也许就看完了吧),而且就算看完了,也不会全看懂,很可能看懂不超过 30%。看不懂没关系,看了就行,不用太纠结,这不能说明什么问题。 另外,我想说一下,多线程技术是程序员必须掌握的,而且需要理解透彻,现在的高级技术例如 GCD,会掩盖你对多线程理解不足的问题,因为使用实在太简单了。别说你没写过多线程依然完成了复杂的项目,更别说你随手写出的多线程代码好像也没出什么问题啊,把你的代码给我,我写个 Demo 让它出错乃至崩溃,如果我做不到,恭喜你。 三 实践 现在,你已经具备了一定的编码经验,而且已经学习了足够的理论知识,接下来就是真正练手的时候了。好好反复思考你学习的这些理论知识,要如何运用到项目中去,身体力行的去实践,一定要把那些理论搞清楚,用于指导你的实践,收起从前的自信,首先否定自己以前的做法,保证每次做出的东西相比以前是有进步有改进的。 四 重温理论 你已经能看到自己的进步了,发现比以前做的更好了,但是总感觉还不够,好像有瓶颈似的,恭喜你,我已经能看到你未来的潜力了。 重新拿起书本,重温一遍之前看的似懂非懂的东西,你会发现之前没弄懂的东西,现在豁然开朗了,不再是那种难于理解的晦涩感了。就算是以前你觉得已经弄懂的,也再看一遍,通常会有新的收获。 五 再实践 这个阶段,你已经掌握了较多的东西了,不但实践经验丰富,各种理论也能手到擒来了,但是你发现你的设计依然不够专业。而且你回过头去看你以前写的代码,你会惊讶:天啊,这是谁写的代码,怎么能这样干!然后。。。我就不多说了,你已经进入了自省的阶段,掌握了适合自己的学习方法,再要学习什么新东西,都不再是个事。 六 总结 先别太得意(不信?那你去做一堂讲座看看),你需要总结了,总结自己的学习方法,总结项目经验,总结设计理论知识。 如果你能有自己独到的理解,而不是停留在只会使用成熟的设计模式什么的,能根据自己的经验教训总结一些设计原则出来,那自然是极好的。 七 分享 分享是最好的学习催化剂,当你要准备一场培训分享的时候,你会发现你先前以为已经理解的东西其实并没有真正理解透彻,因为你无法把它讲清楚,实际上就是研究不够,这时会迫使你去重新深入学习,融汇贯通,然后你才敢走上讲台。否则当别人提问的时候,你根本回答不上来。 以上,便是我认为的程序员修炼道路的必经阶段。 然后,我再说说其他对提升非常重要的几点: 养成先设计,再编码的习惯 几乎所有的程序员,一开始都不太愿意写文档,也不太愿意去精心设计,拿到需求总是忍不住那双躁动的手,总觉得敲在键盘上,一行一行的代码飙出来,才有成就感,才是正确的工作姿势。 没讨论清楚不要编码,不然你一定会返工。 设计重于编码,接口重于实现 制定接口的过程,本身就是设计过程,接口一定要反复推敲,尽量做减法而不是加法,在能满足需求的情况下越简单越好。 另外,不要一个人冥思苦想,先简单做一个雏形出来,然后拿去找使用方沟通,直到对方满意为止。 不要完全根据使用需求去设计接口,参考 MVVM,ViewModel 就是根据 View 的需要而对 Model 进行的再封装,不能将这些接口直接设计到 Model 中。 不盲从设计模式 设计模式只是一种解决问题的套路方法,你也可以有自己的方法,当然设计模式如果用好了,会让你的设计显得专业与优雅,毕竟前辈们的心血结晶。但是滥用的话,也会导致更严重的问题,甚至可能成为灾难。个人觉得面向对象设计原则更加重要,有些原则是必须遵守的(如单向依赖、SRP 等),而设计模式本身都是遵守这些原则的,有些模式就是为了遵循某原则而设计出来的。 抽象不是万能的,在适当的地方使用,需要仔细推敲。当有更好的方案不用抽象就能解决问题时,尽量避免抽象,笔者见过太多的抽象过火过度设计的案例了,增加了太多维护成本,还不如按照最自然的方式去写。 空杯心态,向身边的同学学习,站在巨人的肩上,站在别人的肩上 有人提意见,先收下它(无论接受与否)。 很多程序猿,也都有一个毛病,就是觉得自己技术牛的不行,不愿意接受别人的意见,尤其是否定意见(文人相轻)。 而无论是理论的学习,还是编码实践,向身边的同学学习将是对自己影响最大的(三人行,必有我师),比刻意参加相关培训要有用的多。 我自己就经常在跟团队同学的讨论中获益,当百思不得解的时候,把问题抛出来讨论一下,通常都能得到一个最佳方案。 另外,跟团队其他人讨论还有一个好处,就是当你的设计有妥协,有些不专业的时候,别人看到代码也不会产生质疑,因为他参与了讨论的,你不用花那么多时间去做解释。 设计期间就一定要找他人讨论,我一直比较反对一个人把设计做完了,把文档写完了,然后才找大家开个评审会那种模式,虽然也有效果,但是效果真的达不到极致,大家没有参与到设计中来,通过一场会议的时间理解不一定有那么深,最关键的是,如果设计有些问题的时候,但也不是致命问题,难道还让打回重新设计么? 等前期讨论足够后,大家都知道你的思路与方案,而且最后也有设计文档,当其他人来阅读你的代码的时候,根本无需你再指引,今后的工作交接都不是很需要了,何乐而不为呢? 最后,我想在此呼吁一下,当你去修改维护别人的代码时,最好找模块负责人做深入的讨论沟通,让他明白你的需求以及你的方案,请他帮忙评估方案是否可行,是否会踩坑、埋坑等。这样我们的项目才不会出现坏味道蔓延,而如果你恰好是某模块负责人,请行使你的权力,拒绝有问题的不符合要求的代码提交入库。 大家共勉。
微前端的场景域 在选择一个微前端方案之前,常常需要思考这样一个问题,我们为什么需要微前端。通常对微前端的诉求有两个方面,一是工程上的价值,二是产品上的价值。 对于工程上的价值,可以从一个三年陈的项目来看,如下所示, commit 的记录显示,第一次提交是 2016 年 8 月。 依赖树 dependencies: 打包体积: 虽然这个三年陈的项目看上去版本比较低,但仍然是相对主流的全家桶方案。这样一个乐观的项目,在真实的场景中经过三年的时间,也不实用了。因为开发的时间比较长,并且人员流动也比较大,会导致一些祖传的代码出现,其次,在技术上不能及时的升级,导致开发体验变得很差,例如打包的时间就超过三分钟。也有可能在不经意间依赖一些不兼容的框架,导致项目无法升级。种种原因,最后很有可能变成一个遗产项目。 对于产品体验上的问题,例如下图所示,要完成一个跳多个控制台任务,在过程中发现每个控制台视觉不统一、流程出现断点以及重复加载或认证的问题导致整个产品的体验较差。 微前端的定义 Techniques, strategies and recipes for building a modern web app with multiple teams using different JavaScript frameworks.—— Micro Frontends。 以上是 Micro Frontends 网站对微前端的定义。意思是所谓微前端就是一种多个团队之间可以使用不同技术构建一个现代化 web 的技术手段以及方法策略。其中的关键字是多团队、采用不同的技术栈以及现代化的 web。微前端的思路继承自微服务的思想。 微前端的架构图 如图,其中上层为统一共享的拼接层,主要做一些基础信息的加载,和对来自不同团队不同技术栈的客户端在运行时动态组成一个完整的 SPA 应用,以及生命周期的调度和事件的管理。总之,微前端是将微服务概念做了一个很好的延伸和实现。 在具体实践中,衡量一个微前端方案是否是可利用的,需要满足以下几个条件: 技术栈无关性,不仅指子应用之间使用多个不同的框架,也指在使用同一个框架时,有可能在一个长的时间跨度下,由于框架的不兼容的升级,导致应用被锁死的情况。 开发、发布及部署独立,要求子应用和主应用做到工程上的解耦和独立。 应用隔离的能力,是指需要考虑如何不干扰到原来子应用的开发模式和部署模式的情况下,做好运行时的样式隔离、JS 隔离以及异常隔离等。 以上几点是基于工程价值方面考虑的。此外,也需要动态组合的能力,是基于产品价值方面考虑的。 落地的关键问题 微前端架构中的技术选择 按架构类型区分,常规 web 应用的架构类型分为两种,一种是 MPA,另一种是 SPA。如上图所示为 2017 年各云产品控制台架构调研,除了 google cloud 之外,大部分的云厂商都使用 MPA 架构。MPA 的优点在于部署简单,具备独立开发和独立部署的特性。但是,它的缺点是完成一个任务要跳到多个控制台,并且每个控制台又是重复刷新的。而 SPA 能极大保证多个任务之间串联的流畅性,但问题是通常一个 SPA 是一个技术栈的应用,很难共存多个技术栈方案的选型。SPA 和 MPA 都是微前端方案的基础选型,但是也都存在各自的问题。 单实例,一个运行时只有一个 APP Actived 多实例,一个运行同时有多个 APP Actived 按运行时特性区分,微前端包含两个类别,一类是单实例,另一类是多实例。单实例场景如上图中左侧,通常是一个页面级别的组合,例如一个运行时只有一个 App 被激活。多实例场景如上图右侧,像一个组件或者是容器级别的应用,运行时可以做到多个应用被同时激活。这两种模式都有自己适应的场景和优势。微前端架构的核心诉求是实现能支持自由组合的微前端架构,将其他的 SPA 应用以及其他组件级别的应用自由的组合到平台中。那么,如何选择 SPA 和 MPA 以及单实例和多实例是一个问题,我们是否能探索出一种方案,将 SPA 和 MPA 工程上的特点结合起来,同时兼顾多实例和单实例运行时的场景来实现。 技术细节上的决策 为了实现上述的方案,在技术细节上的决策需要注意以下问题: 如何做到子应用之间的技术无关。 如何设计路由和应用导入。 如何做到应用隔离。 基础应用之间资源的处理以及跨应用间通信的选择。 对于如何做到子应用之间的技术无关问题,我们是通过协议来解决的。如下代码所示的方式,就可以完成子应用的导入。如果子应用接入时做了一些框架上的耦合或者依赖一个具体实现库的机制,就一定会存在与实现库版本耦合的可能,不利于整个微前端生态的统一和融合。 export async function bootstrap() { console.log('react app bootstraped') ; } export async function mount(props) { console.log(props) ; ReactDOM.render(<App/>, document.getElementById('react15Root')); } export async function unmount() { ReactDOM.unmountComponentAtNode(document.getElementById('react15Root') ) ; } 如下所示是一个与具体框架实现相耦合的例子(反例): //主应用 import React from 'react' ; import ReactDOM from 'react-dom'; import MicroFrontend from 'micro-frontend'; ReactDOM.render(<MicroFrontend base="/app1" entry="//localhost/a.js">); //子应用 window.microFrontends = { app:{...} , reduxStore:{...} , globals:(...) , } 对于路由的问题,如下图所示。这样一条访问链路后,刷新当前 URL 通常情况下会发生什么? 正常访问一个站点,经过一番操作之后,进入到站点的列表页,路由会变大很复杂,但如果是一个微前端用户,刷新一下页面会出现 404 的情况。解决思路是将 404 路由 fallback 到一个异步注册的子应用路由机制上。 对于应用导入方式的选择,比较常见的方案是 Config Entry。通过在主应用中注册子应用依赖哪些 JS。这种方案一目了然,但是最大的问题是 ConfigEntry 的方式很难描述出一个子应用真实的应用数据信息。真实的子应用会有一些 title 信息,依赖容器 ID 节点信息,渲染时会依赖节点做渲染,如果只配 JS 和 CSS,那么很多信息是会丢失的,有可能会导致间接上的依赖。 <html> <head> <title>sub app</title> <link rel="stylesheet" href="//localhost/app.css"> </head> <body> <main id="root"></main> <script src="//localhost/base. js"> </body> </html> <script> import React from 'react' import ReactDOM from 'react-dom' ReactDOM.render(<App/>, document.getElementById('root') ) </script> 另外一种方案是 HTML Entry,直接配 html,因为 html 本身就是一个完整的应用的 manifest,包含依赖的信息。HTML Entry 的优点是接入应用的信息可以得到完整的保留,接入应用地址只需配一次,子应用的原始开发模式得到完整保留,因为子应用接入只需要告知主应用 html 在哪,包括在不接入主应用时独立的打开。它的缺点是将解析的消耗留给了运行时。而 Config Entry 相较于 HTML Entry 减少了运行时的解析消耗。Config Entry 的缺点是主应用需配置完整的子应用信息,包含初始 DOM 信息、js/css 资源地址等。 registerMicroApps([ { name: 'react app' // index.html 本身就是一个完整的应用的 manifest entry: '//localhost: 8080/index.html', render, activeRule: '/react' } ]) ; 对于样式隔离问题,例如 BEM,每个子应用在写样式之前要加一些前缀,做一些隔离,但是这个做法并不推荐。相对而言,CSS Module 更简单高效,也更智能化,是比较推荐的方式,但是也存在着问题。而 Web Components 看上去很不错,但在实践过程中也会发生一些问题。 例如在 Web Components 渲染的流程中出现了问题,如下图所示。 在 antd 中提供了全局的 API,可以提前设置好所有的弹框的 container,但是也不是每个组件库都能像 antd 一样完成度那么高。 蚂蚁所采用的解决方案是做动态的加载和卸载样式表,如下图所示,这种方案是很有效的。 对于 JS 隔离,蚂蚁提出了 JS Sandbox 机制,如上图所示,其中 bootstrap、mount及 unmount 生命周期是子应用需要暴露出来的,因为子应用的整个生命周期都是被主应用所管理的,所以可以在主应用中给子应用插入各种拦截的机制,也可以捕获到子应用在加载期间做了哪些全局上的修改。在 unmount 时,可以将全局上的副作用全部手动移除掉,同时也可以实现在重新进来时,将上次忘记卸载的副作用重建一遍,因为需要保证下次进来时能完整回复到与上次一致的上下文。 对于资源加载问题,在微前端方案中存在一个典型的问题,如果子应用比较多,就会存在之间重复依赖的场景。解决方案是在主应用中主动的依赖基础框架,然后子应用保守的将基础的依赖处理掉,但是,这个机制里存在一个问题,如果子应用中既有 react 15 又有 react 16,这时主应用该如何做?蚂蚁的方案是在主应用中维护一个语义化版本的映射表,在运行时分析当前的子应用,最后可以决定真实运行时真正的消费到哪一个基础框架的版本,可以实现真正运行时的依赖系统,也能解决子应用多版本共存时依赖去从的问题,能确保最大程度的依赖复用。 基于 props 以单向数据流的方式传递给子应用: export function mount(props) { ReactDOM.render( <App {...props}/>, container ) } 基于浏览器原生事件做通信: //主应用 window.dispathEvent( new CustomEvent('master:collapse-menu'), {detail: {collapsed:true} } ) //子应用 window.addEventLister( 'master:collapse-menu', event => console.log(event.detail.collapsed) ) 对于应用之间数据共享及通信的问题,蚂蚁提出了两个原则,第一个原则是基于 props 以单向数据流的方式传递给子应用。第二个原则是基于浏览器原生事件做跨业务之间的通信。 在真实的生产实践中,蚂蚁总结出了几点经验及建议:兄弟节点间通信以主应用作为消息总线,不建议自己封装的 Pub/Sub 机制,也不推荐直接基于某一状态管理库做数据通信。 蚂蚁在实践中做的性能优化,包括异步样式导致闪烁问题的解决以及预加载问题的解决。 异步样式导致的闪烁问题: 预加载: export function prefetch(entry: Entry, fetch?: Fetch) { const requestIdleCallback = window.requestIdleCallback || noop; requestIdleCallback(async () => { const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, { fetch } ); requestIdleCallback(getExternalStyleSheets) ; requestIdleCallback(getExternalScripts) ; }) ; } 如图所示为微前端方案涉及到的技术点,本文分享了图中三分之二的内容。 在蚂蚁金服做了大量关于微前端方案之后,总结了衡量一个微前端方案是否友好的两个标准,第一个标准是技术无关,也是微前端最核心的特性,不论是子应用还是主应用都应该做到框架不感知。第二个标准是接入友好,子应用接入应该像接入一个 iframe 一样轻松自然。 蚂蚁的微前端落地的实践成果 蚂蚁内部基于微前端基础架构提出了一体化上云解决方案,称为 OneX,是一个基础的平台,它可以将各种流程和工具串联,其价值体现在品牌、产品和技术方面。品牌价值指的是统一的界面框架、UI、交互形成了蚂蚁金服科技品牌心智。 OneNav + OneConsole + TechUI + OneAPI + Bigfish 下图所示为蚂蚁的一个真实应用的例子,除了中间接入的产品是自己控制之外,其他内容都是由平台提供,这样,如论是一个三年陈项目还是新做的项目,在基本的视觉上可以做到统一。 产品价值指的是产品具有自由组合能力。之前的产品是多个产品、多个站点的控制台,而现在只需要一个控制台,将多个产品自由的组合,这样,可以在商业上有更多的相应空间以及更多自由的搭配。基于这样的系统也可以做一些全局性的事情,例如埋点、用户的转化跟踪业务。 技术价值指的是研发上的提效。经过微前端的改造后,蚂蚁可以将大型的系统解耦成可以独立开发的并行的小型的系统,这些小型系统可以交给别的团队或者使用可视化的系统去实现,最后在运行时只需要将他们集成起来。 在技术价值方面也可以实现交付上的提效,只需要在某一个环境的任意一个环境中做平台上的接入,应用就可以做到在多余的环境中不改代码,直接运行。 下图为阿里云刚上市的一个产品例子,其中包括 15 个来自不同团队的应用进行维护,它的特点是并不是单独为阿里云而设计的,之前在蚂蚁也有运行,只不过在阿里云中做了动态的组合。OneTour 微应用组件主要解决的是在多个产品控制台之间自由切换导致流程割裂的问题。 蚂蚁微前端的落地成果包括:有 70+ 线上应用接入(阿里云 + 蚂蚁云 + 专有云),最复杂一个控制台同时集成 15 个应用,并且有 4+ 不同技术栈,以及开发到发布上线全链路的自动化支持,一云入驻多云运行。 基于以上技术上的成果,蚂蚁沉淀了自己的微前端方案并开源。 基于以上技术上的成果,蚂蚁沉淀了自己的微前端方案并开源。qiankun 是框架无关的微前端内核,umi-plugin-qiankun 是基于 umi 应用的 qiankun 插件,方便 umi 应用通过修改配置的方式变身成为一个微前端系统。基于上述实践的检验和内部落地结果来看,在大规模中后台应用场景下,微前端架构是一个值得尝试的方案。 qiankun:https://github.com/umijs/qiankun/ umi-plugin-qiankun:https://github.com/umijs/umi-plugin-qiankun
前言 开发人员经常会面临下面一些场景: 新人入职,需要学习已有系统,作为 landing 的一部分,如何学习? 被拉过去参与一个陌生系统的迭代开发或者系统维护(bugfix),如何快速上手? 同事离职或转岗,需要把系统交接给你,怎么去接?内心 os:这是一口锅吗? 这样的场景多了,就需要去梳理常见问题以及应对方法,方便后续遇到类似场景可以快速应对。本文总结熟悉系统主要分三部分:业务学习、技术学习、实战。每部分会梳理一些在学习过程中需要解答的问题,这些问题随着经验的积累需要逐步补充完善。 业务学习 业务学习就是从业务角度去学习系统,我们需要了解系统的客户是谁、使用人是谁、带来了什么价值,系统提供了哪些功能等。不清楚业务,就等于不知道系统在干什么。技术是为业务落地而服务,清楚了业务才知道怎样用技术更好地服务业务,所以业务学习是熟悉一个系统的首要任务。这块主要的学习方式有跟产品、运营、开发沟通,学习产品设计文档文档、PRD、自己使用系统,还有一些常见图,如产品功能架构图、业务流程图、功能树,用例图等。 常见问题: 系统所在行业的情况是怎样? 系统的目标用户是谁?比如是给公司高层做决策用?给运营或客服用?还是互联网用户用? 平均有多少人在使用?高峰期多有少人在用? 系统有什么业务价值?有哪些指标可以衡量系统业务价值? 系统有哪些功能模块? 系统有哪些领域概念?梳理下系统的领域模型。 系统的关键业务流程有哪些?关键业务流程是怎样? 系统的非功能性需求有哪些?如性能、质量、扩展性、安全性等。 系统未来的发展规划是怎样? 技术学习 技术学习主要学习系统的架构、如何实现、系统的运维等。描述一个系统的架构有五视图方法论,五视图分别是:逻辑架构、开发架构、运行架构、物理架构、数据架构。 逻辑架构 逻辑架构着重考虑功能需求,系统应当向用户提供什么样的服务,关注点主要是行为或职责的划分。常用表达图形,静态图有包图、类图、对象图,动态图有序列图、协作图、状态图、活动图。逻辑架构的核心设计任务是模块划分、接口定义、领域模型细化。 常见问题: 有哪些子系统或模块?系统之间是什么样的关系? 对外上下游接口有哪些?对接人是谁? 关键业务流程怎么实现的?用类图、序列图等方式表达出来。 开发架构 开发架构关主要关注系统源代码、第三方SDK、使用的框架、中间件、工具包。 常见问题: 代码在哪? 包怎么划分的?怎么分层?如 mvc、controller-service-dao。 用了什么框架?如 ssh、dubbo。 用了哪些工具包?如 apache commons、guava。 用了哪些中间件?如 metaq、tair、schedulerX、Diamond。 依赖哪些平台?如权限平台、流程引擎等。 运行架构 运行架构的着重考虑运行期质量属性,关注点是系统的并发、同步、通信等问题,这势必涉及到进程、线程、对象等运行时概念,以及相关的并发、同步、通信等。 常见问题: 系统能支撑多少 qps ?峰值 qps 多少? 与上下游系统怎么交互的?rpc?http?同步还是异步? 物理架构 物理架构的设计着重考虑安装和部署需求,关注点是目标程序及其依赖的运行库和系统软件最终如何安装或部署到物理机器,以及如何部署机器和网络来配合软件系统的可靠性、可伸缩性、持续可用性、性能和安全性等要求。 常见问题: 系统如何发布部署?有哪些部署环境? 系统有多少台机器? 系统部署怎么部署的?关注接入层,部署方式,如集群部署、分布式部署等。 有没有容器化? 有没有多机房部署? 数据架构 数据架构的设计着重考虑数据需求,关注点是持久化数据的存储方案,不仅包括实体及实体关系数据存储格式,还可能包括数据传递、数据复制、数据同步等策略。 常见问题: 数据存储在哪?用了什么数据库,如 oracle、mysql。 梳理 E-R 图。 数据量有多少?是否有分库分表? 用了哪些 nosql 库? 有哪些数据同步任务? 大数据框架的使用情况如何? 系统运维 系统运维重点关注什么时候会出问题,出了问题怎么解决。 常见问题: 什么时间容易出问题?比如电商双十一,对系统的压力很大,这时候很容易出问题。 对关键功能是否有监控?需要看系统有配置了哪些报警项,监控了哪些方面。 出了问题怎么解决?日志在哪?是否有全链路跟踪?是否有一些紧急操作,比如开关配置、降级、限流配置。 系统有哪些坑?找开发同学回顾历史问题,以免踩坑。通过同事总结的 case,或者与负责的产品、运营、技术与了解。系统总会有一些坑,需要把这些坑填上。历史代码经过多次迭代总会导致复杂度高(分支、嵌套、循环很多),存在设计漏洞,性能隐患等,很难维护,这些就需要我们去重构了。记住有一句话:填的坑越大,能力越大。 运营、客服反馈的常见问题有哪些? 实践 熟悉了系统的业务和技术后,就要实战了,通过实战进一步加深对系统的熟悉程度。实践可以通过做需求、修 bug、重构等方式,亲自动手编码、调试、测试、上线。 总结 已有系统通常经历了从 0 到 N 的建设过程,熟悉系统其实是一个逆向推导过程,也是一个学习架构、阅读源码的过程。在学习的过程中最好能带上思考,比如为什么要这么设计,为什么要用这个中间件?是否有更好的编码方式?哪些地方可以优化等,以此达到一个深入熟悉的过程。
一 背景 1.1 架构中的问题识别 需求分析,架构实现,(新需求,架构改动)* n = 推倒重来。 这个过程是一个循环往复的过程,有的产品每年都会推倒重来一次。 而这个过程是如何造成的呢?原因之一是每次迭代过程中都没有用正确的架构方法来进行迭代造成的,就像在歪楼上继续加盖楼层一样,最终还是会倒塌(不过这个原因并不是唯一的原因,其他原因留到后续文章中阐述)。 这真是一个悲伤的故事,但是又是一个时常发生的故事。或者说我们大多数人都经历过的场景。 要解决这个问题,那就需要在每次迭代中,都需要用正确的姿势对不对?要用对姿势其中有一个重要的原因是架构。就像一幢大楼,架构设计得越有问题,这幢大楼被重造的可能性就越大。 这里正确的姿势到底是什么姿势?接下来本文会阐述一整套架构方法论,该方法论中包含了详细的架构推导逻辑,帮助我们在工作中在各个粒度,各个层次做好架构工作。 我们后续的文章中将会着重阐述如何通过自底向上以及自顶向下的两种架构思考方式来解决这些问题,但是在那之前,我们还是先来聊聊什么叫“架构”。 1.2 什么是架构? 大概是在 11 年前左右,在土豆网做广告平台,同时也做视频 CDN 的相关事情,当时做一个服务,基础架构是 lighttpd + squid + tomcat,将静态资源分离到 httpd,get 请求使用 squid 缓存,智能路由使用 HTTP post 请求,并让 tomcat 提供服务,当时就觉得这就是架构。再后来,做了视频 CDN 相关的基础建设的工作,就觉得这就是做架构,关键那个时候也没有人告诉我们什么架构,自己不知道自己不知道。 再后来慢慢成长,又去做了几年中间件(包括高性能 RPC 和 JSR-170),然后就觉得这也是做架构。当时也没有前辈跟我讲什么是架构,那个时候的我对架构是没有体系化认知的,都是凭着感觉做的,是不知道自己不知道。 再后来,来到了阿里做应用研发和架构了,发现业务开发中也包含了各种方法论,而以前看过的建模相关的资料,在中间件等基础设施上也没有太大的感觉,反而在业务技术领域发挥出了巨大的光芒。也发现越靠近用户的架构,随着企业的慢慢壮大会变得越来越重要。这个时候的我对架构认知是知道自己不知道了。 既然知道自己不知道了,那么就是要追寻它,曾经我和不少业务的研发同学讨论过架构是什么,撇去基础设施架构和物理架构等视角不谈(这些视角聊起来也是篇幅很长的),我挑应用逻辑架构并从几个角度来尝试描述一下: 1)从架构的总原则的角度:尽可能简单(在当前场景下要尽可能简单便于扩展和维护),但是不能太简单(相对而言太过于简单可能在场景上有所遗漏). 2)从架构的目的角度来考虑:既要解决过去的问题,也要解决现在的问题,还能适度解决未来的问题,这些问题既包含技术问题,更包含业务问题。 3)从形态之 2 维的角度来考虑:架构就是横的问题,和竖的问题。横就是分层,竖就是分区,横竖都有抽象的事情要做。 4)从形态之 3 维的角度来考虑:架构是三维的,在 x 轴和 y 轴上有横竖的问题,在z轴上还有粒度的问题。 5)从时间轴的角度来考虑:架构不是一层不变的,是随着业务的发展在不断变化的。 可以看出,虽然我试图从以上几个视角对架构进行了描述,但是显然这些描述都是见仁见智的观点,是从某个角度来看架构。内心里我觉得自己提炼的高度是不够的,实践中的总结必须和业界的知识结合起来,我必须学习前人已经总结的体系。于是在不断的搜集资料的过程中,我发现在 ISO/IEC 42010:20072 中对架构有如下定义: The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution. 这个最顶层抽象我个人觉得非常到位,根据这个定义,显然,我们在架构中需要: 职责明确的模块或者组件 组件直接的关联关系非常明确 需要有约束和指导原则 这个架构的定义很简练,很实在。小到一个玩具,大到一个国家的运作都可以隐含着这样的内容。 但这是一个广义上定义的架构,经过一些总结思考,我觉得实际上具体到我们日常的工作中,在不同的层次,会有更加精细化的架构分类。 1.3 架构有哪些分类 在工作中我遇到不同职位的人从不同的角度来描述架构,但是我们鲜有能达成共识的,刚开始我也不知道为啥讨论不到一块去,后来经过一段时间的纠结和深入仔细的思考后,我发现很多时候大家描述的架构都不是同一个角度的东西,于是我尝试从如下几个角度划分架构的类别,以帮助我们在不同的场景和不同的人聊天时大家可以聚焦,明确我们到底是在讨论哪种架构,以提升沟通效率,并尽快达成共识,目前这个划分已经在我们团队基本达成共识。 值得注意的是,不管下面哪种分类的架构,都符合上一节总的架构的定义:模块(组件)+ 关系 + 约束 & 原则。 1. 产品功能架构 这个是产品经理最喜欢讲的架构,一般来说,讲我们有什么功能的时候,产品功能架构描述的是能做什么,受众群体一般是使用产品的同学。如果我们做软件设计时,不应该产出这玩意,而是应该产出应用逻辑架构和应用物理架构。但是一旦我们要对外宣讲我们的产品,比如我们的接口有啥用,应该怎么用,这个时候我们讲的应该是产品功能架构。 目的:指导用户使用产品,所以模块的聚合是从用户视角出发的 受众:使用产品的人 包含的内容:阐述产品功能模块的能力:比如一辆汽车,方向盘有什么功能,方向盘的按钮上各区域的功能是什么,仪表盘分成哪些功能模块,每个功能模块有什么作用,油门踏板有什么作用,刹车踏板有什么作用。但是也不排除有些高阶用户需要明确知道变速箱的齿比等信息,所以在产品功能架构图上也可以描绘出来。 命名:这里命名需要考虑如何取一个吸引人的名字(同时又能表达产品的能力)来吸引我们的用户前来使用,比如说以前经常有产品套用“纳米”,又有产品套用“绿色”等等。 2. 业务能力架构 用来分析业务,业务概念架构是指拥有哪些业务模块,且各自的能力是什么,这张图有助于我们分析和理解业务需求,也有利于产品经理分析业务。所以业务概念架构和业务概念模型都是用在分析阶段。 目的:研发人员和业务人员理解业务内在的概念和联系。 受众:研发人员和业务人员,主要是给规划业务的人使用。 包含的内容:业务能力,能力中的子能力。 3. 应用逻辑架构 软件设计本身,模块,粒度,职责,复用,等等,在讲解软件设计的时候,使用的是这个架构图,这个架构图是通过系统模型和业务概念架构推导而来。所以系统模型和应用逻辑架构都是用在软件设计阶段。 目的:指导软件的研发。 受众:研发人员,各层级架构师,各层级技术管理者。 包含的内容:阐述架构中各模块的职责:如系统模型,技术模块,技术模块的关系,技术模块的核心抽象,如何用设计模式来让架构符合软件设计原则,等等。如果拿汽车举例,那就是发动机模块中包含了哪些子模块(活塞,曲轴,连杆,缸体,缸盖,等等)发动机模块和变速箱模块之间的关联关系是什么,如何协同工作,和底盘的关联关系是什么,如何协同工作。发动机,底盘,变速箱,电子系统在整辆汽车中的职责,关系,约束是什么。这些都是用来指导汽车研发的。而不是指导用户如何使用这辆汽车的。 命名:这里的命名需要朴实无华,精准的描述出职责,华而不实反而让技术的同学无法理解这到底是什么玩意,导致实施的时候职责放错地方,挖下大坑让后人来填。 4. 应用物理(部署)架构 软件部署时的架构,这张图推导自应用逻辑架构,推导时重点逻辑架构如何落地,比如使用何种微服务容器,逻辑架构的模块落地时应该是 package,还是应用,也有可能是一组应用,是不是要跨机房部署,甚至跨国部署等等。还需要考虑稳定性,性能,成本等话题。 5. 基础设施架构 选择什么样的中间件,存储,监控,报警,等等。 6. 等等 1.4 能力和职责的区别 在日常的架构讨论中,有的同学经常谈架构的能力,有的同学经常谈架构的职责,那么能力和职责有什么区别?跟产品的同学打交道多了之后,发现产品同学很多都是讲能力,后来技术的同学也开始讲能力,而通常我们架构的同学原来讲的都是职责,两者有什么区别呢,我说说自己的理解: 1. 能力(产品功能模块的能力) 是指一个产品能做什么,比如中台本身是一个产品,对使用中台的同学来说,我们应该讲中台的能力(其实是在讲中台这个产品的能力)。所以讲能力是讲给架构的使用者或者其他想了解的人来听的。 2. 职责(逻辑架构中各模块的职责) 是指架构内模块的职责,用来指导开发,比如中台研发的同学,应该讲架构的职责,依赖,约束。所以讲职责是讲给研发的同学,讲给域内的架构师,讲给域内的管理者来听的,总的来说就是讲给架构的实现者来说的。 简单来说就是:能力是指产品的能力,职责是指架构内部的职责。如果架构本身也是一个产品需对外输出(如中台,或者其他技术框架作为产品输出),则对外输出时,我们应该讲这个技术产品的能力(这个时候技术的同学也就开始讲能力了)。所以当我们讨论问题的时候,如果有的人在谈产品能力,有人在谈架构内部职责,那么显然已经不是在讨论同一个话题了,请大家务必注意区分这种情况,差之毫厘,谬以千里,鸡同鸭讲啊。 比如说两个模块 A 和 B,职责不一样,但是依赖了相同的二方库。那我们不能说某个职责在这个二方库里。这个二方库作为某个独立的技术小产品,提供了某些能力。但是履行职责的还是 A 模块或者 B 模块。 1.5 应用逻辑架构的地位 正如前面我们描述的架构分类所描述,有些架构和具体业务是无关的,有些架构是和具体业务息息相关的,比如说应用逻辑架构就是和业务息息相关,它来源于业务的抽象,甚至我们可以说:它是业务线技术架构设计中第一份产出。 既然他是首要的产出,我们就必须要考虑应用逻辑架构中应该包含的三类主题: 模块 依赖 约束 绝大部分的架构问题都可以归纳成这三类主题,这些主题包含哪些内容呢?这就是本文接下来要介绍的内容,应用逻辑架构的设计不需要拍脑袋,是通过科学的方法体系推导出来的。 二 架构的两种推导思路 架构的产出总的来说有两种方式,一种是自顶向下的方式来推导架构,一种是自底向上的推导方式,而且两种方式往往是相互结合来产出最合适的结果。而在业务线的同学,可能接触最多的是自底向上的推导的方式,自底向上的推导的方式也是本文中要重点讲解的架构推导方式。 2.1 自顶向下的架构推导 自顶向下的推导的关键问题在问题定义,如果问题没有被准确的定义,那么自顶向下就无法推导出正确的结果。假设问题被准确的定义了,如何自顶向下推导呢? 2.2 自底向上的架构推导 我们在业务线做开发的同学,每天肯定跟很多需求打交道,这些需求哪里来的?基本上有这三种产出: 有些是来自产品方一拍脑袋产生的灵感 有些是对数据进行了详细的分析产出的产品策略 有些是当前产品中暴露的一个个问题 产品方的这些详细的需求来了之后,我们是如何应对的呢?我们首先和产品方一起讨论产品方案的合理性,在产品方案合理的基础上,我们来开始识别用例,开始了一系列软件工程领域方面的措施。其整体过程如下图所示: 自底向上推导逻辑架构就是最右边代表的那条曲线。 这里基本上就是本文接下来要重点阐述的:如何自底向上推导应用逻辑架构,这个过程就是一个抽象和架构的过程。 那么我们从整体方法论的介绍开始,采用总分总的结构,下面这张图就是应用逻辑架构自底向上的推导路径,这个推导路径是有序的,每个步骤都包含了大量的操作技巧,前一步做好,后一步才有可能得出正确的结果。 这张图中有几个重点: 1)软件研发分成了两个阶段: 分析阶段,也是我们常说的问题空间领域建模,关键的一步是业务概念模型的输出,而业务概念模型输出的前置条件是从需求中分解出合理的用例集合。 设计阶段,也是我们常说的解决方案空间建模,以及应用逻辑架构。 2)图中存在了箭头这个东西,说明了我们做架构推导的主要的思维路径,也说明做架构不需要拍脑袋,都是根据严密的逻辑推导出来的。 这个严密逻辑基本是一个自底向上的推导过程,底层的模型是通过建模方法演绎出来,逻辑架构中的各个模块是通过归纳的方法推导出来。那么: 什么地方应该用演绎,什么地方应该用归纳呢? 使用演绎的时候应该使用何种具体方法呢? 使用归纳的时候应该使用何种具体方法呢? 我们再留一个悬念,后面再讲。 不管是演绎还是归纳,都是抽象工作的一部分,而且都需要素材,这里的素材就是我们对需求,对业务的理解,以及对技术的深度广度的把握。没有素材,方法论掌握再好也得不出结果。 素材哪里来呢? 业务素材的来源大部分是你需要解决问题所在的领域,比如我们在电商领域,那么我们就要多搜集电商领域的业务知识。如果我们在数据领域,自然要多搜集数据业务的相关知识,以及我们前文中讲到的技术类的相关知识。 而技术素材是要求我们在技术领域不断的钻研,不断的扩展边界,深度不断增加,广度也不断增加。所以对于架构师来说计算机科学与技术是绝对要不断精进的。 2.3 两个方法的区别 自顶向下推导的一个前置条件就是你需要知道猪长什么样,在架构上就是你需要知道这个架构的原来是是什么样子的,解决什么问题的。如果都不知道猪长什么样,那么就无从判断猪是不是适合当宠物了。此处需要有一定的业务领域理解力和领域经验(包含:客户的问题和痛点是什么,怎么分析出来的,当前的架构方案是什么,当前的架构方案是如何解决这个问题的,未来的架构方案如何更好的解决这个问题)。 而自底向上推导则没有这个问题,因为是看着猪来做推导的,知道猪的细节,这个细节的特点如何演绎,如何归纳,最后得出结论。 所以当我们不熟悉一个大的业务的时候,我们自顶向下推导架构的难度是极大的,几乎不能完成。不了解业务或技术情况时定义出来的问题也未必是一个被正确定义的问题,容易给人造成一个印象:瞎指挥。 这个时候如何在没有知识背景的情况下快速落地就得自底向上的来推导架构。在自底向上的过程中慢慢熟悉业务。 但是如果工作中每每都是纯粹的自底向上的推导架构,是无法帮助我们来做技术的前瞻性布局的,此时架构师的成长就遇到的瓶颈,所以此时又要使用自顶向下的架构推导方式。 综上所述,不管是自底向上,还是自顶向下,都是架构师需要掌握的技能。 三 自底向上的架构方法:业务概念架构推导 这部分内容,我在 ICBU,村淘,一达通,菜鸟,AE 现场分享过。尤其是在 AE,一达通和菜鸟,相关的同学都拿出了当时的纠结大家很久的难题,我们一起使用了这样的方法很快就分析出了业务概念模型,并且对模块进行了简要的划分,形成概要的业务概念架构。经过大量的实战,效果是非常明显的。 3.1 模型的 3 个层次 在这里,我把一些常见的概念集中起来,便于大家统一概念: 1)业务概念模型,问题空间领域模型,信息模型是同样的意思,这个层次上的实体我们称之为概念实体,这部分内容是用在需求和业务分析上的,讨论业务概念模型时完全不需要考虑软件的实现,这个过程是一个分析过程,即使不做软件研发,做其他的研发,类似的分析过程也应该是有的。 2)系统模型,解决方案空间领域模型,逻辑模型是同样的意思,这个层次上的实体,我们称之为系统实体,或者逻辑实体,就是各种类,这个是用在软件设计和软件研发上的。 3)存储模型,数据模型,物理模型,在这里也是同样的意思,这个层次上的实体,我们称之为数据实体,或者物理实体,也是用在软件设计上。 这 3 个层次其实是从 3 个角度在看待问题,他们之间是自上而下的转换的关系,这里尤其要注意的两个词是:逻辑的,顺序的推导。 这些不同层次的模型是应用逻辑架构的基础!!! 3.2 模型的推导 3.2.1 用例集合推导概念模型 根据用例集合推导业务概念模型 根据用例中的动词和量词推导业务概念模型的关联关系 在特定的边界内根据模型的职责归纳子域 重要!重要!重要!这里业务概念模型如果没有分析正确,那么下面要搞清楚是不容易的,这个分析部分是软件逻辑架构设计的基础。 这个环节需要我们理解业务,更需要我们掌握问题空间建模这一严谨的方法论,这样我们才能推导出合理的模型,整个过程是非常严谨的,非常符合逻辑的。 我在各 BU 分享现场做的多次实战演练之所以能成功的快速帮助同学们梳理出前面花一两个月都没有理出的模型,完全是因为于现场的同学对业务的理解(因为讨论之前我完全没有了解过对方的业务)和这套方法论(隐含在我的提问方式中)。所以说对业务的理解和方法论,两者缺一不可。 3.2.2 对业务概念模型进行归纳 在模型产出之后,我们要对模型进行归纳。 什么叫归纳? 归纳的意思是将所有的结果和想法合并,变成一种思维概念。或者让某个模型归属于某个已经存在的思维概念。且这些模型或者模块的职责不能超越这个高层次思维概念的边界。 为什么要归纳? 其实是为了保证相近的职责模型聚拢在一起从而保证职责的高内聚,同时明确出来的两个子域的边界,保证模块和模块之间的低耦合。 对业务概念模型的归纳有助于做业务需求分析时判断高内聚和低耦合,而且在系统模型上,对系统模型进行分类也有助于做应用逻辑架构中模块的高内聚和低耦合,但是应用逻辑架构的不止高内聚和低耦合,还有其他让职责单一的方法,这些后面的章节会做介绍。 3.2.3 按职责来进行归纳 接下来我们来讲讲业务概念模型到业务概念架构判断方法: 1)通过名词定义来进行归纳思维概念 如果多个模型都在围绕某个名词,那么我们倾向将这个名词提炼出来。产品在设计时,基本上我们已经能够得一个粗略的业务模块划分,但是这个粗略的划分是不一定是合理: 一是有可能我们的理解是不到位的,导致用错了名词,这个我们前面的文章中也提到过了。 二是这个结果也只是一个粗略的结果,需要进一步精化。 2)通过内聚的度量公式来进行归纳 业务模型图中,模型和模型连线(连线就是模型和模型连接线)数量除以模型的梳理得到的值比较大的,那么我们可以看做是内聚,这些连线比较紧密我们趋向将其放到一个模块中,连线不是那么密切的,我们趋向于将它们放置在不同的模块中。然后我们再观察 连线数 / 模型数 观察内聚度量是高了还是低了,通过这样的方式归纳完成之后,我们再来通过度量公式来度量各模块的内聚和耦合程度。 3)其他归纳方式 如果我们划分出了基本模块,发现还有一些模型不确定应该放到哪些模块中,我们还可以使用创建者原则和信息专家原则来判断应该将该模型归纳如哪个模块。 比如说,对存储系统进行系统建模,表和字段的关系在业务概念模型中是1对n的关系(在系统模型中是组合关系,强生命周期依赖,但是这里我们还没有到讨论应用逻辑架构的时候,只是在推导业务概念架构),此时将字段放到另外一个模块显然不合适,原因是根据创建者原则。 当我们不清楚把字段模型放到哪个模块的时候,我们可以看看字段这个模型是由谁创建的。 根据这条原则显然这里是表创建了字段,没有表对象,就没有字段对象,所以根据这条原则,我们就倾向于把字段模型放到表所在的模块中。 重点:失去了最底层合理且正确的演绎,上层的归纳掌握的再好,也很难得出合理的结果。 我们来看看归纳之后的效果示意图: 图中的 A1,A2,A3,A4 之类是示意图,表示 A 模块内部还存在子模块,当然我们其实是先推导出子模块,然后对子模块再次进行高级别归纳,形成父模块。 父模块层级再进行归纳,就形成了祖父模块,或者再向上形成曾祖父模块等等。粒度越大的模块,一般都对应更大的组织,越存在跨团队沟通,所以划清边界的要求就越高。 3.3 业务流程 除了业务模型之外,业务流程也是我们需要总结并明确的地方,这个地方主要明确的就是边界和异常分支等等,尤其是异常分支非常重要,很多业务方案的设计中对异常分支的考量是不重复的,这需要工程师对业务方案提出挑战,以明确业务方案中的各种流程的异常分支。 3.4 业务概念架构总结 我们工作中常见的推导有两种方式,一种是自顶向下推导,一种是自底向上推导,显然,两种推导使用的方法是不一样的。细心的读者会发现,其实我们刚刚说的问题空间领域模型和边界分析这套方法就是自底向上的演绎和归纳方法。 四 基础逻辑架构推导(软件设计阶段) 前面我们讲到了业务分析阶段,也是问题空间建模和问题空间业务概念架构梳理,业务分析阶段和软件没有任何关系。但本文中它是软件设计的前置条件,没有 get 到点的同学,请务必再把前一章仔细阅读。 接下来我们来讲讲软件设计阶段我们需要产出的应用逻辑架构。 4.1 再谈逻辑架构特性 文章开头讲到了逻辑架构的相关特点,我们回顾一下: 应用逻辑架构的作用:我们把前面那个例子再搬过来:如果拿汽车举例,那就是发动机模块中包含了哪些子模块(活塞,曲轴,连杆,缸体,缸盖,等等)发动机模块和变速箱模块之间的关联关系是什么,和底盘的关联关系是什么,发动机,底盘,变速箱,电子系统在整辆汽车中的职责,关系,约束是什么。这些都是用来指导汽车研发的。而不是指导用户如何使用这辆汽车的。 目的:所以系统模型和应用逻辑架构都是用在软件设计阶段,其目的是用来指导软件的研发。 受众:逻辑架构的受众有哪些呢?一般是这些人:研发人员,各层级架构师,各层级技术管理者,总的来说他们都是架构的设计者和实现者。 这里还是请大家务必要跟产品功能架构区分开来,它们的受众和目标是不一样的。 4.2 基础逻辑架构的推导概要 在文章开头的图中,我们讲到应用逻辑架构来源于系统模型,数据模型,业务概念架构,还有流程,如下图所示。 接下来,我们分别从三个角度来阐述逻辑架构的生成: 业务概念架构 模型(系统模型和数据模型) 流程(系统调用流和数据流) 看到很多同学画的图没有区分出调用流和数据流,经常造成误解,造成沟通效率下降,甚至不能够准确的说明问题。所以在画图的时候,一定要注意区分调用流和数据流。 接下来就根据业务概念架构和系统模型及流程来推导一下应用架构(逻辑架构)。我们来看一下一个简单的逻辑架构构成的 gif 示意图: 从这张图中,我们可以看出应用逻辑架构是如何一步步被构成的,整个过程存在以下关键点: 1)在业务概念架构的基础上推演应用逻辑架构。 2)根据流程和系统模型来完善应用逻辑架构。 3)横向提炼模块的问题:要实现业务模块,需要什么非业务模块的支撑,比如监控,报警,配置等等,而这部分内容往往还是可复用的。在上述动画中,可以理解成移动到最右侧的部分,当然可以移动到左侧,只是动画中没有体现出来。 4)纵向提炼模块问题:有类似职责的模块在技术实现上是否可以提炼成可复用内容,提炼的结果可能是: 独立的服务复用,在上述动画中,可以理解成最下方。 或者二方库复用,在上述动画中,可以理解成最左或者最右侧。 等 5)还有一些模块是为了支撑性能或者稳定性的,并非是从业务概念模型提炼而来,如图中深蓝色的模块。 最终,出现的逻辑架构是分层的和分片的逻辑架构,下面我们来一步步阐述这个过程。 4.3 根据业务概念架构推演 业务概念架构图产出之后,基本上,我们逻辑架构的初步模型就具备了。所以我们可以理解成,第一步就是把业务概念架构直接先搬到应用逻辑架构中来,此处就不用多阐述了。 啰嗦两句:尤其是较为顶层的粗粒度业务架构,一个是自顶向下分解得来,一个是自底向上演绎和归纳得来。而自顶向下分解尤其考验人对业务的理解能力,如果对业务理解不透彻,那很难产出合理的粗粒度业务概念架构。 4.4 根据系统流程进行推演模块 当业务概念架构产出之后,逻辑架构的骨架初成,接下来就是在这个框架上去填充内容。第一步就是根据流程来进行模块划分。 总结一下,这里的方法就是,先根据业务流程,分解出系统时序图,根据时序图开始对模块进行归纳,从而得到粒度更大的模块。 这是粒度比较细的根据流程划分模块的案例,在粒度更大的流程,此方法同样适用,看大家是工作在何种粒度上。 通过流程来进行推导是我们日常工作必不可少的一部分,尤其当很多场景的流程具有业务共同点时,那么可以考虑提炼出这些业务共同点,以提升研发的效率。 4.5 非业务线系统根据流程推导模块案例 除了对流程进行归纳之外,我们还可以对系统模型进行归纳。我们知道,业务概念模型一般可以直接转换为系统模型,但是系统模型并不只是业务领域相关的模型,比如查询模型是一个经常出现的,这在 OLTP 的场景十分常见,而在 OLAP 的场景简直就是顶梁柱。非常常见的就是 SQL parser 模块,下图是 spark 体系中 SQL SQL 的主要流程和对应的模型,根据这个模型我们基本上也可以梳理出模块: 根据这个流程,我们发现了什么?我们发现了 spark 中是这样分模块的(这里面的模块已经落地成 package 了): 所以说按照业务流程转换成的系统流程来推导模块是非常重要的手段。 除此之外需要还需要强调的是,流程和模块一样,也是有粒度的,相同粒度的流程节点放在一起才更加容易推导出合理的架构模块。至于什么叫相同粒度,请参考一下《金字塔原理》。 流程的粒度很重要,粒度粒度粒度,请重视流程的粒度。 4.6 根据性能 & 稳定性 & 成本等进行提炼模块 前面讲的都是从业务的角度来阐述架构的推导,接下来我们从计算机科学与技术的角度来阐述一下这些非功能性模块的推导,这里拿性能来举个例子吧。 数据分析的报表场景降低 RT 的方案 在一些数据分析产品中,绩效监控及报表展示是一个非常重要的场景,这个场景下的数据量是比较大的,为了降低 RT,我们不得不通过 ETL 对数据进行预计算,将原有的大表清洗成聚合之后的小表,以加快查询的速度。这样做的缺点是每次进行报表的修改,就要进行相关的ETL逻辑,高时间和人力成本,高性能。 为了把高时间和人力成本 & 高性能转换成低成本&高性能,我们需要把人工操作转换成自动操作,把 ETL 的过程去除。 第一个选择是将一个大表的数据存储到另外一个支持大数据下高性能的查询引擎,这样就极大的减少了 ETL 的操作,但是这样就带来一个问题,就是大数据量下把数据从 ODPS 导入到某个 ROLAP 的查询引擎中是比较耗时的,而且每次查询需要进行在海量数据中进行大量的 scan,但实际上获取的数据量并不大。这样的查询的 RT 依然需要亚秒级。 第二个选择是根据报表的定义,自动的将判断出用户需要查询什么结果,将查询结果提前计算出来,然后只把这些少量的预计算后的结果导入到 ROLAP 引擎中(具体请参考 apache 开源项目 Kylin)。然后在报表的场景下,查询的 RT 下降到了百毫秒级。 显然我们要实现第二种方式,这个时候在业务功能没有增加的情况下,我们必须要增加一个模块,在我们的产品中,我们称之为 intelligent cube,因为我们这里引入了机器学习算法对 cube 的构建进行了预测,无需或者只需非常少量的人为参与。 最后导致逻辑架构中有部分是来自业务概念架构推导而来,有部分是系统流程推导而来,有部分是因为性能 & 成本的需要产生的设计。 注意:理论上来讲,逻辑架构上需要指出模块之间的依赖关系,只是如果这样,不是特别美观,所以就根据上下和左右的位置来大概描述模块之间的关系了。 这两个案例基本可以说明,根据性能 & 成本 & 稳定性推导出来的模块也是逻辑架构组成的重要部分。 但是这个还只是一个场景一个场景来解决 RT 问题,虽然 icube 自己内部是有个体系的,但是通过这样的方式来解决 RT 问题对于整个架构来说也是自底向上构建的一个环节。在下一篇文章中,我们将会阐述相同的案例,但是思路是自顶向下来构建性能领域的体系化架构。同样一个事情,用不同的思路来做,对总目标的帮助是不一样的,而且两个方法是互补的,谁都少不了。 这样的模块是如何得来的呢? 看上去我们都已经知道了系统中有不少类似的纯技术相关的模块,但是这些模块内部是如何设计出来的呢? 一般来说有如下方法帮助我们做这些模块的内部设计: 1)调查业界的开源技术类产品中是否有类似功能的,比如预计算在业界有 kylin,而星环等专业大数据公司也都有自己的 cube 预计算产品。 2)查阅业界相关的论文,比如说在预计算领域就已经研究了几十年,计算机发展的不同阶段有不同的论文,网上一搜一大堆,不断研究,必对工作有帮助。 3)多关注业界的牛人,看看他们在想什么,说什么,参加参加相关的会议。 4)自己通过逻辑和数据结构 & 算法推导出来。 如果每次都只通过自己的逻辑和自己已经掌握的知识来进行方案的推导是不够的,一个是我们的技能有时候和事情是不匹配的,但是我们往往不知道这样的事实的存在,所以此时一定要虚心学习,请教他人,扩展自己的知识边界,才能做出更好的方案和技术决策。 4.7 应用逻辑架构推导小结 根据上文所述,基本上应用逻辑架构的推导有 4 个子路径,他们分别是: 业务概念架构:业务概念架构来自于业务概念模型和业务流程 系统模型:来自于业务概念模型 系统流程:来自业务流程 非功能性的系统支撑:来自对性能,稳定性,成本的需要 每个子路径中都存在相关的具体方法。 如果真的要想学习东西,而且想学的更快更深入,就要关注自己如何集中注意力,要思考自己的思考方式,研究自己的研究方式。 说明:以上是本文的上篇,关注阿里技术,后续我们将继续推出本文的下篇,继续讨论架构的基本约束、逻辑架构的复用以及逻辑架构分层的问题。
在 Git Rev News #48 期的 LightReading 中有一篇文章(地址:https://hacker-tools.github.io/version-control/) 写的不错,不仅干货满满而且还附带了操作视频。其中的内容不仅覆盖了很多 Git 使用上的基础知识,也从使用角度上解答了很多刚接触 Git 的开发者的疑问。为了便于读者理解,我在翻译的同时也添加了一些内容。以下为正文部分。本文内容较长,建议收藏慢慢学习。 担忧 很多人怕使用 Git,我个人觉得主要可能是两部分的原因: 没接触过:平时接触的代码还托管在 SVN 或 CVS 等工具上。 不太熟悉:可能对 Git 的使用还不太熟悉和全面,导致了在使用 git 时步步为营。 Never Be Afraid To Try Something New. 代码对于开发者是劳作成果的结晶,对于公司而言是核心资产,有一些担忧也是正常的。但 Git 也并没有我们想象中的那么复杂,需要让我们每次使用都心有余悸,其实我们只需要稍微花一点时间尝试多多了解它,在很多时候你会发现,非但 Git 不会让你产生担忧,而且会让自己的交付过程更加高效。 Version Control 谈及 Git 就不得不提到版本控制,我们不妨先来看下版本控制是做什么的,这将有助于后续对 Git 的理解。 当你在工作中面对的是一些经常变化的文档、代码等交付物的时候,考虑如何去追踪和记录这些 changes 就变得非常重要,原因可能是:对于频繁改动和改进的交付物,非常有必要去记录下每次变更的内容,每次记录的内容汇成了一段修改的历史,有了历史我们才知道我们曾经做了什么。 记录的历史中必须要包含一些重要的信息,这样追溯才变得有意义,比如: Who:是谁执行的变更? When:什么时候做出的变更? What:这次变更做了什么事情? 最好可以支持撤销变更,不让某一个提交的严重问题,去污染整个提交历史。 版本控制系统(VCS: Version Control System),正会为你提供这种记录和追溯变更的能力。 大多数的 VCS 支持在多个使用者之间共享变更的提交历史,这从实质上让团队协同变为了可能,简单说来就是: 你可以看到我的变更提交。 我也可以看到你的变更提交。 如果双方都进行了变更提交,也可以以某种方式方法进行比对和合并,最终作出统一的变更版本。 VCS 历经多年的发展,目前业界中有许多 VCS 工具可供我们选择。在本文中,我们将会针对目前最流行的 Git 来介绍。 Git 是黑魔法么? 刚接触 Git 时,Git 确实有让人觉得有点像黑魔法一样神秘,但是又有哪个技术不是这样呢?当我们了解其基本的数据结构结构后,会发现 Git 从使用角度来讲其实并不复杂,我们甚至可以更进一步的学习 Git 的一些优良的软件设计理论,从中获益。首先,让我们先从 commit 说起。 git object commit 提交对象(git commit object):每一个提交在 Git 中都通过 git commit object 存储,对象具有一个全局唯一的名称,叫做 revision hash。它的名字是由 SHA-1 算法生成,形如"998622294a6c520db718867354bf98348ae3c7e2",我们通常会取其缩写方便使用,如"9986222"。 对象构成:commit 对象包含了 author + commit message 的基本信息。 对象存储:git commit object 保存一次变更提交内的所有变更内容,而不是增量变化的数据 delta (很多人都理解错了这一点),所以 Git 对于每次改动存储的都是全部状态的数据。 大对象存储:因对于大文件的修改和存储,同样也是存储全部状态的数据,所以可能会影响 Git 使用时的性能(glfs 可以改进这一点)。 提交树:多个 commit 对象会组成一个提交树,它让我们可以轻松的追溯 commit 的历史,也能对比树上 commit 与 commit 之间的变更差异。 git commit 练习 让我们通过实战来帮助理解,第一步我们来初始化一个 repository(Git 仓库),默认初始化之后仓库是空的,其中既没有保存任何文本内容也没有附带任何提交: $ git init hackers $ cd hackers $ git status 第二步,让我们来看下执行过后 Git 给出的输出内容,它会指引我们进行进一步的了解: ➜ hackers git:(master) git status On branch master No commits yet nothing to commit (create/copy files anduse "git add" to track) 1)output 1: On branch master 对于刚刚创建空仓库来说,master 是我们的默认分支,一个 Git 仓库下可以有很多分支 (branches),具体某一个分支的命名可以完全由你自己决定,通常会起便于理解的名字,如果用 hash 号的话肯定不是一个好主意。 branches 是一种引用 (ref),他们指向了一个确定的 commit hash 号,这样我们就可以明确我们的分支当前的内容。 除了 branches 引用以外,还有一种引用叫做 tags,相信大家也不会陌生。 master 通常被我们更加熟知,因为大多数的分支开发模式都是用 master 来指向“最新”的 commit。 On branch master 代表着我们当前是在 master 分支下操作,所以每次当我们在提交新的 commit 时,Git 会自动将 master 指向我们新的 commit,当工作在其他分支上时,同理。 有一个很特殊的 ref 名称叫做 "HEAD",它指向我们当前正在操作的 branches 或 tags (正常工作时),其命名上非常容易理解,表示当前的引用状态。 通过 git branch (或 git tag) 命令你可以灵活的操作和修改 branches 或 tags。 2)output 2:No commits yet 对于空仓库来说,目前我们还没有进行任意的提交。 nothing to commit (create/copy files anduse "git add" to track) output 中提示我们需要使用 git add 命令,说到这里就必须要提到暂存或索引 (stage),那么如何去理解暂存呢? 暂存 一个文件从改动到提交到 Git 仓库,需要经历三个状态: 工作区:工作区指的是我们本地工作的目录,比如我们可以在刚才创建的 hackers 目录下新增一个 readme 文件,readme 文件这时只是本地文件系统上的修改,还未存储到 Git。 暂存(索引)区:暂存实际上是将我们本地文件系统的改动转化为 Git 的对象存储的过程。 仓库:git commit 后将提交对象存储到 Git 仓库。 git add 的帮助文档中很详细的解释了暂存这一过程: This command updates the index using thecurrent content found in the working tree, to prepare the content stagedfor the next commit. git add 命令将更新暂存区,为接下来的提交做准备。 It typically adds the current content ofexisting paths as a whole, but with some options it can also be used toadd content with only part of the changes made to the working tree filesapplied, or remove paths that do not exist in the working tree anymore. The "index" holds a snapshot ofthe content of the working tree, and it is this snapshot that is taken as thecontents of the next commit. 暂存区的 index 保存的是改动的完整文件和目录的快照 (非 delta)。 Thus after making any changes to theworking tree, and before running the commit command, you must use the addcommand to add any new or modified files to the index. 暂存是我们将改动提交到 git 仓库之前必须经历的状态。 对 Git 暂存有一定了解后,其相关操作的使用其实也非常简单,简要的说明如下: 1)暂存区操作 通过 git add 命令将改动暂存。 可以使用 git add -p 来依次暂存每一个文件改动,过程中我们可以灵活选择文件。中的变更内容,从而决定哪些改动暂存。 如果 git add 不会暂存被 ignore 的文件改动。 通过 git rm 命令,我们可以删除文件的同时将其从暂存区中剔除。 2)暂存区修正 通过 git reset 命令进行修正,可以先将暂存区的内容清空,在使用 git add -p 命令对改动 review 和暂存。 这个过程不会对你的文件进行任何修改操作,只是 Git 会认为目前没有改动需要被提交 。 如果我们想分阶段(or 分文件)进行 reset,可以使用 git reset FILE 或 git reset -p 命令。 3)暂存区状态 可以用 git diff --staged 依次检查暂存区内每一个文件的修改。 用 git diff 查看剩余的还未暂存内容的修改。 Just Commit! 当你对需要修改的内容和范围满意时,你就可以将暂存区的内容进行 commit 了,命令为:git commit。 如果你觉得需要把所有当前工作空间的修改全部 commit,可以执行 git commit -a ,这相当于先执行 git add 后执行 git commit,将暂存和提交的指令合二为一,这对于一些开发者来说是很高效的,但是如果提交过大这样做通常不合适。 我们建议一个提交中只做一件事,这在符合单一职责的同时,也可以让我们明确的知道每一个 commit 中做了一件什么事情而不是多个事情。所以通常我们的使用习惯都是执行 git add -p 来 review 我们将要暂存内容是否合理?是否需要更细的拆分提交?这些优秀的工程实践,将会让代码库中的 commits 更加优雅。 ok,我们已经在不知不觉中了解了很多内容,我们来回顾下,它们包括了: commit 包含的信息? commit 是如何表示的? 暂存区是什么?如何全部添加、一次添加、删除、查询和修正? 如何将暂存区的改动内容 commit? 不要做大提交,一个提交只做一件事。 附带的,在了解 commit 过程中我们知道了从本地改动到提交到 Git 仓库,经历的几个关键的状态: 工作区 (Working Directory) 暂存区 (Index) Git 仓库 (Git Repo) 下图为上述过程中各个状态的转换过程: 本地改动文件时,此时还仅仅是工作区内的改动 当执行 git add 之后,工作区内的改动被索引在暂存区 当执行 git commit 之后,暂存区的内容对象将会存储在 Git 仓库中,并执行更新 HEAD 指向等后续操作,这样就完成了引用与提交、提交与改动快照的——对应了。 正是因为 Git 本身对于这几个区域(状态)的设计,为 Git 在本地开发过程带来了灵活的管理空间。我们可以根据自己的情况,自由的选择哪些改动暂存、哪些暂存的改动可以 commit、commit 可以关联到那个引用,从而进一步与其他人进行协同。 提交之后 我们已经有了一个 commit,现在我们可以围绕 commit 做更多有趣的事情: 查看 commit 历史:git log (或 git log --oneline)。 在 commit 中查看改动的 diff:git log -p。 查看 ref 与提交的关联关系,如当前 master 指向的 commit: git show master 。 检出覆盖:git checkout NAME(如果 NAME 是一个具体的提交哈希值时,Git 会认为状态是 “detached (分离的)”,因为 git checkout 过程中重要的一步是将 HEAD 指向那个分支的最后一次 commit。所以如果这样做,将意味着没有分支在引用此提交,所以若我们这时候进行提交的话,没有人会知道它们的存在)。 使用 git revert NAME 来对 commit 进行反转操作。 使用 git diff NAME 将旧版本与当前版本进行比较,查看 diff。 使用 git log NAME 查看指定区间的提交。 使用 git reset NAME 进行提交重置操作。 使用 git reset --hard NAME:将所有文件的状态强制重置为 NAME 的状态,使用上需要小心。 引用基本操作 引用 (refs) 包含两种分别是 branches 和 tags, 我们接下来简单介绍下相关操作: git branch b 命令可以让我们创建一个名称为 b 的分支。 当我们创建了一个 b 分支后,这也相当于意味着 b 的指向就是 HEAD 对应的commit。 我们可以先在 b 分支上创建一个新的 commit A ,然后假如切回 master 分支上,这时再提交了一个新的 commit B,那么 master 和 HEAD 将会指向了新的commit __B,而 b 分支指向的还是原来的 commit A。 git checkout b 可以切换到b分支上,切换后新的提交都会在b分支上,理所应当。 git checkout master 切换回 master 后,b 分支的提交也不会带回 master 上,分支隔离。 分支上提交隔离的设计,可以让我们非常轻松的切换我们的修改,非常方便的做各类测试。 tags 的名称不会改变,而且它们有自己的描述信息 (比如可以作为 release note 以及标记发布的版本号等)。 做好你的提交 可能很多人的提交历史是长这个样子的: commit 14: add feature x – maybe even witha commit message about x! commit 13: forgot to add file commit 12: fix bug commit 11: typo commit 10: typo2 commit 9: actually fix commit 8: actually actually fix commit 7: tests pass commit 6: fix example code commit 5: typo commit 4: x commit 3: x commit 2: x commit 1: x 单就 Git 而言,这看上去是没有问题而且合法的,但对于那些对你修改感兴趣的人(很可能是未来的你!),这样的提交在信息在追溯历史时可能并没有多大帮助。但是如果你的提交已经长成这个样子,我们该怎么办? 没关系,Git 有办法可以弥补这一些: git commit --amend 我们可以将新的改动提交到当前最近的提交上,比如你发现少改了什么,但是又不想多出一个提交时会很有用。 如果我们认为我们的提交信息写的并不好,我要修改修改,这也是一种办法,但是并不是最好的办法。 这个操作会更改先前的提交,并为其提供新的 hash 值。 git rebase -i HEAD~13 这个命令非常强大,可以说是 Git 提交管理的神器,此命令含义是我们可以针对之前的 13 次的提交在 VI 环境中进行重新修改设计: 操作选项 p 意味着保持原样什么都不做,我们可以通过 vim 中编辑提交的顺序,使其在提交树上生效。 操作选项 r:我们可以修改提交信息,这种方式比 commit --amend 要好的多,因为不会新生成一个 commit。 操作选项 e:我们可以修改 commit,比如新增或者删除某些文件改动。 操作选项 s:我们可以将这个提交与其上一次的提交进行合并,并重新编辑提交信息。 操作选项 f:f代表着 "fixup"。例如我们如果想针对之前一个老的提交进行 fixup,又不想做一次新的提交破坏提交树的历史的逻辑含义,可以采用这种方式,这种处理方式非常优雅。 关于 Git 版本控制的一个常见功能是允许多个人对一组文件进行更改,而不会互相影响。或者更确切地说,为了确保如果他们不会踩到彼此的脚趾,不会在提交代码到服务端时偷偷的覆盖彼此的变化。 在 Git 中我们如何保证这一点呢? Git 与 SVN 不同,Git 不存在本地文件存在 lock 的情况,这是一种避免出现写作问题的方式,但是并不方便,而 Git 与 SVN 最大的不同在于它是一个分布式 VCS,这意味着: 每个人都有整个存储库的本地副本(其中不仅包含了自己的,也包含了其他人的提交到仓库的所有内容)。 一些 VCS 是集中式的(例如 SVN):服务器具有所有提交,而客户端只有他们“已检出”的文件。所以基本上在本地我们只有当前文件,每次涉及本地不存在的文件操作时,都需要访问服务端进行进一步交互。 每一个本地副本都可以当作服务端对外提供 Git 服务。 我们可以用 git push 推送本地内容到任意我们有权限的 Git 远端仓库。 不管是集团的 force、Github、Gitlab 等工具,其实本质上都是提供的 Git 仓库存储的相关服务,在这一点上其实并没有特别之处,针对 Git 本身和其协议上是透明的。 SVN,图片出自 git-scm Git,图片出自 git-scm Git 冲突解决 冲突的产生几乎是不可避免的,当冲突产生时你需要将一个分支中的更改与另一个分支中的更改合并,对应 Git 的命令为 git merge NAME ,一般过程如下: 找到 HEAD 和 NAME 的一个共同祖先 (common base)。 尝试将这些 NAME 到共同祖先之间的修改合并到 HEAD 上。 新创建一个 merge commit 对象,包含所有的这些变更内容。 HEAD 指向这个新的 merge commit。 Git 将会保证这个过程改动不会丢失,另外一个命令你可能会比较熟悉,那就是 git pull 命令,git pull 命令实际上包含了 git merge 的过程,具体过程为: git fetch REMOTE git merge REMOTE/BRANCH 和 git push 一样,有的时候需要先设置 "tracking"(-u) ,这样可以将本地和远程的分支一一对应。 如果每次 merge 都如此顺利,那肯定是非常完美的,但有时候你会发现在合并时产生了冲突文件,这时候也不用担心,如何处理冲突的简要介绍如下: 冲突只是因为 Git 不清楚你最终要合并后的文本是什么样子,这是很正常的情况。 产生冲突时,Git 会中断合并操作,并指导你解决好所有的冲突文件。 打开你的冲突文件,找到 <<<<<<< ,这是你需要开始处理冲突的地方,然后找到 =======,等号上面的内容是 HEAD 到共同祖先之间的改动,等号下面是 NAME 到共同祖先之间的改动。用 git mergetool 通常是比较好的选择,当然现在大多数 IDE 都集成了不错的冲突解决工具。 当你把冲突全部解决完毕,请用 git add. 来暂存这些改动吧。 最后进行 git commit,如果你想放弃当前修改重新解决可以使用 git merge --abort ,非常方便。 当你完成了以上这些艰巨的任务,最后 git push 吧! push 失败? 排除掉远端的 Git 服务存在问题以外,我们 push 失败的大多数原因都是因为我们在工作的内容其他人也在工作的关系。 Git 是这样判断的: 1)会判断 REMOTE 的当前 commit 是不是你当前正在 pushing commit 的祖先。 2)如果是的话,代表你的提交是相对比较新的,push 是可以成功的 (fast-forwarding)。 3)否则 push 失败并提示你其他人已经在你 push 之前执行更新 (push is rejected)。 当发生“push is rejected”后我们的几个处理方法如下: 使用 git pull 合并远程的最新更改(git pull 相当于 git fetch + git merge)。 使用 --force 强制推送本地变化到远端引用进行覆盖,需要注意的是 这种覆盖操作可能会丢失其他人的提交内容。 可以使用 --force-with-lease 参数,这样只有远端的 ref 自上次从 fetch 后没有改变时才会强制进行更改,否则“reject the push”,这样的操作更安全,是一种非常推荐使用的方式。 如果 rebase 操作了本地的一些提交,而这些提交之前已经 push 过了的话,你可能需要进行 force push 了,可以想象看为什么? 本文只是选取部分 Git 基本命令进行介绍,目的是抛砖引玉,让大家对 Git 有一个基本的认识。当我们深入挖掘 Git 时,你会发现它本身有着如此多优秀的设计理念,值得我们学习和探究。 不要让 Git 成为你认知领域的黑魔法,而是让 Git 成为你掌握的魔法。
《喧哗与骚动》是我喜欢的作家威廉·福克纳的一部小说,小说用多个家庭成员的意识流,从不同的视角描绘了一家三代的悲剧。这部小说有意思的地方在于:对于同样一件事情,从不同人跳跃的意识中能看到迥然相异的景象。 今天大家理解 Serverless 也有点这个意思,因此我以此为题,展开分析。文章只代表作者本人观点。 Serverless is like teenage sex 不知道大家有没有听过这样的话: Big data is like teenage sex: Everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims they are doing it. 我们把 Big data 换一下: AI is like teenage sex: Everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims they are doing it. 我们把 AI 换成 Serverless: Serverless is like teenage sex: Everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims they are doing it. 从中可以总结出以下几点: 所有人都在说 Serverless; 几乎没人知道怎么落地 Serverless; 但是大家都觉得其他人在大力做 Serverless; 所以大家都宣称自己在做 Serverless。 Serverless 和很多词如微服务一样,是没有精确定义的,也没有事实的标准。什么是事实标准?Kubernetes 是事实标准;对 Java 程序员来说 Spring Boot / Spring Cloud 是事实标准。 事实标准就是一种思想/方法论得到了广泛落地,占领了市场。落地通常意味着两个点: 它是开放(开源)的。因此不会有 vendor lock-in,所有人可以放心用; 有大量的成功案例。很多人将其用到关键的商业系统中,因此得到了广泛验证。 今天 Serverless/FaaS 领域有这个东西吗?还没有。 Serverless 的愿景 下面是来自 Google Trends 的一个图,其中红色是 Microservices,蓝色是 Serverless。 从 2016 年 AWS 发布 Lambda 以来,全世界的开发者和云厂商对 Serverless 的热情在不断高涨,这说明大家对 Serverless 所描绘的愿景都非常 buy in。这个愿景是什么呢? 愿景是无服务器?但工程师们都知道服务器本质上是存在的,最多是加一层抽象,让我们看不到服务器,但它依旧很好的发挥作用。 我个人觉得有关 Serverless 愿景,描绘最清楚的是一个比喻,这个比喻来自 UC Berkeley 在今年 2 月发表的那篇论文: 简单来说就是:我们今天对云资源的操作方式,就类似于几十年前早期程序员写汇编的方式。 如果你没写过/学过汇编语言,或者已经忘了汇编语言,我特地找了本书拍了一段内容下来: 是不是对图中的这些寄存器、栈、程序计数器、以及相关的汇编指令感到很陌生了?如果让你用这样的语言写业务逻辑,那效率必然会变得非常低。 幸好我们有 Java,Go,JavaScript 这样的高级语言,而这些高级语言还配套了相关的编译器/虚拟机,编译器/虚拟机能够高效地把面向业务的高级语言翻译成面向机器的汇编/机器码。 今天,虽然基本的计算机体系结构没有发生本质的变化,但我们的程序所运行的环境,相比较 20 年前,已经发生了本质的变化。20 年前的程序大都跑在单机上,今天我们的程序都要为了跑在云上而设计了。 为了让程序跑在云上,我们就需要配套的工作,包括云资源(容器、缓存、队列)的申请和回收、包括弹性伸缩的控制,等等。这些事情和业务逻辑没有任何关系,但研发/运维同学却为此花费了大量的时间。 我想做一个不太成熟的类比: 单机时代,操作系统管理了硬件资源,贴着资源层,高级语言让程序员描述业务,贴着业务层,编译器/VM 把高级语言翻译成机器码,交给操作系统; 今天的云时代,资源的单位不再是 CPU、内存、硬盘了,而是容器、分布式队列、分布式缓存、分布式文件系统。 云上的 OS 这个角色,基本上可以说是被 Kubernetes 生态给占了,那么云上的编译器/VM 呢?开发语言和框架呢?好像还没有。 今天我们把应用程序往云上搬的时候(a.k.a Cloud Native),往往都会做两件事情: 第一是把巨型应用拆小,微服务化; 第二就是摇身一变成为 yaml 工程师,写很多 yaml 文件来管理云上的资源。 本质上大家都在把面向单机体系架构编写的应用程序,硬搬到云体系架构上。我认为这里存在两个巨大的 gap,这两个 gap 在图中用灰色的框表示了: 1 编程语言和框架 目前主流的编程语言基本都是假设单机体系架构运行的,面对分布式问题的时候,再叠一层框架上去。其对应的资源也依旧停留在单机体系结构的那些资源上(当然这里是有例外的,比如 erlang/OTP 天生就是为分布式设计的)。 云时代,首先基本的资源单位发生了变化,从原来的 cpu、内存变成了容器、函数、分布式队列等等;其次,云天生分布式,因此单机时代大行其道的同步模型就不再适合。 2 编译器 程序员不应该花大量时间去写 yaml 文件,这些面向资源的 yaml 文件应该是由机器生成的,我称之为云编译器,高级编程语言用来表达业务的领域模型和逻辑,云编译器负责将语言编译成资源描述。 我个人很看好 Erlang 的 Actor 模型,这个模型在其他语言上也有实现,例如语法参考 Ruby 并运行在 Erlang OTP 上的 Elixir,JVM 上的 Akka,以及 .NET 上的 Orleans。 不同于其他语言的设计,Actor 模型从一开始就是基于分布式的前提做的设计,因此这种模型如果把其对应的资源管理换成纯粹的云资源管理,我觉得是有极大可行性的。 如果用一句话来总结,我觉得 Serverless 的愿景应该是: Write locally, compile to the cloud. 大家在忙什么 除了抬头看天,说了一大堆美好的愿景,还得低头走路,先看看这条路上其他人在做什么。我整理了一下最近一年 Serverless 领域行业发生的一些比较重要的事件,建议大家打开简单看下《Serverless 领域近一年行业发展回顾》这篇文章。 为了能够稍微清晰一点地去看这一大堆的产品和技术,我简单的把 Serverless 领域做的事情分了三个层,自下而上分别是资源层、DevOps 层和框架及运行时层。 资源层关注的是资源(如容器)的生命周期管理,以及安全隔离。这里是 Kubernetes 的天下,Firecracker,gVisor 等产品在做轻量级安全沙箱。这一层关注的是如何能够更快地生产资源,以及保证好安全性。 DevOps 层关注的是变更管理、流量调配以及弹性伸缩,还包括基于事件模型和云生态打通。这一层的核心目标是如何把运维这件事情给做没了(NoOps)。虽然所有云厂商都有自己的产品(各种 FaaS),但是我个人比较看好 Knative 这个开源产品,原因有二: 第一是其模型非常完备; 第二是其生态发展非常迅速和健康。很有可能未来所有云厂商都要去兼容 Knative 的标准,就像今天所有云厂商都在兼容 Kubernetes 一样。 以下是 Knative 近一年的贡献者及贡献数量的增长情况,数据来自演讲「Knative a Year Later: Serverless, Kubernetes and You」。 框架和运行时层呢,由于个人经验所限,我看的仅仅是 Java 领域,其实核心的还是在解决 Java 应用程序启动慢的问题(GraalVM)。当然框架如何避免 vendor lock-in 也很重要,谁都怕被一家云厂商绑定,怕换个云厂商要改代码,这方面主要是 Spring Cloud Function 在做。 刚需在哪里 产品想要成功,需要有核心竞争力,这个核心竞争力往往就是,你解决了一个用户很头疼、但其他产品没有解决的问题。我姑且把这样的问题称为用户的刚需。那么 Serverless 能解决哪些用户的什么刚需呢?我先对用户做一些简单的分析: 很多技术产品基本都是经历了如下四个阶段: 初创期 一个小团队围绕新的业务做试错,从无到有,技术上什么能快速上线用什么。 这个时候团队规模很小,可能两三个人,所有代码放在一个应用内,不需要分布式,不需要隔离。 成熟期 业务成功了,用户在不断增多,业务也变得越来越复杂。 这个时候团队的规模增长到数十到上百人,团队还处在一个部门,相互之间有足够的信任,沟通带宽也有足够的保证。一个应用的模式已经不能满足协作的需要,架构师开始做应用拆分,系统成了分布式的,按照业务的划分做了进程级别的隔离。 平台期 业务太成功了,就希望把已经沉淀的能力赋能给其他类似的业务。 相比较于成熟期,这时候有了一些新的变化。首先是参与开发的人数增长得更多了,往往是数百上千;其次大多数参与开发的成员已经不再是核心产品团队的成员,他们往往在不同部门了,相互之间的信任已经大大减弱,沟通带宽也开始显著变窄。 由于核心团队对于其他部门的开发缺乏组织管控能力,因此技术上的隔离要求被提上优先级,以避免平台上的开发者不小心拖垮平台本身。 伴随着隔离,成本的问题也被提上日常,当平台上数百个插件和平台本身跑在同一个进程内的时候,资源天然是被复用的,只要模糊地计算下整体即可;当数百个插件被隔离到独立的容器中运行的时候,他们的资源占用就需要额外的调度系统去控制和优化。 云产品期 平台太成功了,就希望做成云服务,赋能社会上类似的业务,发挥更大的价值。 如果说在平台期,隔离还只是个重要但非必须的要求的话(很多平台就没有真正做好隔离),云产品期的产品必须具备非常强的隔离能力。 平台期做隔离最大的诉求是稳定性(不被平台上的开发者搞垮整个平台),而云产品期做隔离的最大诉求是安全性。 正如图中所示,产品上的开发者已经和产品团队不在一个组织了,而且这样的开发者还可能是恶意的,因此除了容器的隔离,还需要虚拟机级别的隔离,网络的隔离等等。 随着技术产品由小长大,不断成功,参与的开发者不断增长,核心团队对这些开发者的控制力越来越弱,沟通带宽不断缩减,信任不断降低,进而导致了稳定性和安全的风险不断上升,这就要求隔离能力不断加强。而随着隔离的引入,以及使用资源的不断增长,成本就成了一个不得不面对的问题,为了更优地分配资源,解决成本问题,就对调度提出了要求。 因此,对于处在平台期和云产品期的产品来说,技术上的隔离能力及调度能力是他们的刚需。 框架和运行时的创新 前面所说的刚需都是集中在稳定性、安全性及资源成本的角度来讨论的。除此之外我们还需要讨论另外一个话题,那就是开发效率,而开发效率具体到技术是体现在框架上的。 我们可以进一步的把框架分成两类: 1)面向技术问题提升开发效率的框架 如 Spring 通过依赖注入解决对象组装问题;HSF 解决分布式同步通讯问题;RocketMQ 解决分布式异步通讯问题;Hystrix 解决分布式通讯引入的网络不可靠问题等等。通过使用这些框架,技术的天然复杂度在很大程度被屏蔽掉了。 2)面向业务问题提升开发效率的框架 阿里的很多业务平台团队都会根据自己的场景(如交易、店铺、供应链)开发业务型框架,赋能开发快速迭代业务。 通常,面向技术问题的框架会有一个团队研发,而面向业务问题的框架则由各类业务平台团队提供,这再一次证明了康威定律的正确性。康威定律翻译成中国的土话差不多就是“屁股决定脑袋”,技术型团队不愿意碰业务问题,而业务平台团队的框架在解决技术问题方面也显得没有技术团队专业,最终的结果是:两种框架割裂得比较厉害。 大家可能听过这么一个故事: 有一条恶龙,每年要求村庄献祭一个处女,每年这个村庄都会有一个少年英雄去与恶龙搏斗,但无人生还。又一个英雄出发时,有人悄悄尾随。龙穴铺满金银财宝,英雄用剑刺死恶龙,然后坐在尸身上,看着闪烁的珠宝,慢慢地长出鳞片、尾巴和触角,最终变成恶龙。 虽然看起来很夸张,但在我看来,这一定程度上体现了一些大中型研发组织主流框架的现状:这些框架在组织发展的历史上发挥了极其重要的作用,然而到了今天,随着云服务不断地成熟,大家都在提云原生,都基于云在构建业务系统的时候,需要框架还在强制用户绑定语言(如 Java),还没做好服务化,把逻辑塞进用户的应用中。有的甚至要求用户的代码必须部署到平台的巨型应用中。 这些限制短期内实现了业务目标,交付了业务价值,但从长期看基本上浇灭了业务开发做框架创新的热情,他们更习惯于等待“位于正确定位的团队”去解决问题,而“处于正确定位的团队”同学呢,可能一时半会还没感受到那些问题。 不出意外的话,专注组织内短期业务价值的框架,被推到云上、推到社区、面向更普适通用诉求的时候,获得的认可就会差很多。 传统的框架和运行时,只管理单机层面的资源,而当所有人都用云服务构建自身业务的时候,框架和运行时需要管理的就不再是单机资源,而是云资源了。 在这方面行业里已经有了不少产品,比较知名的有 Terraform 和 Pulumi,但我觉得还不够,我觉得理想的云原生框架应该是这样的: 能够帮助开发屏蔽云资源的管理。开发都不喜欢像写汇编一样写 yaml,因此框架需要负责资源的分配、回收,编排等等; 纯异步的,事件驱动的。这是云天生的分布式特性决定的,如果编程语言范式还是同步的模型,这个框架就没法实现了; 没有 vendor lock-in。不绑定实际的云厂商,唯有厂商中立的开发框架才能被广泛使用,框架定义了编程 API,具体的厂商可以提供相关的 driver; 同时具备云资源管理和大规模软件开发必须的编程范式。这里的编程范式可能描述不当,但我找不到更好的词,面向对象设计是最主流的编程范式,Spring 就是围绕这个编程范式展开的。在一个框架中解决两个问题,会给开发极好的体验。 小结 Serverless 这个领域看起来极其美好,一旦深入去做了才发现实际非常复杂。这个复杂体现在涉及的工程技术比较广,也体现在用户的期望差异很大,更体现在大家对未来的判断还有很大的差异。 在和团队一起深入这个领域的时候,我也需要不断整理自己的所闻所见、所思所想,因此我计划产出一系列文章,拿出来和大家分享,和大家探讨,这是第一篇,有兴趣的同学可以一起讨论。
2018年7月,Google 发布了 Knative。 Knative 是一个基于 Kubernetes 的开源 Serverless 框架,具备构建容器、流量调配、弹性伸缩、零实例、函数事件等能力。Knative 背后主要有 Google,Redhat,Pivotal 和 IBM 等公司参与。值得留意的是,Knative 的社区发展非常迅速,截止到 2019年4月,已经有超过50家公司参与,超过400贡献者。 2018年12月,AWS 发布了 Firecracker。 Firecracker 是一个开源的虚拟化技术,面向基于函数的服务,创建和管控安全的、多租户的容器。Firecracker 的目标是把传统虚拟机安全性和隔离型,和容器的诉求和资源效率结合起来。类似的产品还有 Kata Container 和 gVisor。 2019年1月,InfoQ 发布架构和设计趋势报告。 报告指出,「虽然当前 serverless 这个词可能还比较模糊,但是它驱动了行业更多地关注事件驱动的系统设计,以及更多地自动化底层操作系统的关注点。」 2019年2月, Jonas Bonér (Akka的创始人)指出 ,目前 serverless 的编程模型还是限制在无状态的函数上,即 FaaS,这限制了 serverless 能支持的用例:https://thenewstack.io/serverless-needs-a-bolder-stateful-vision/。 2019年2月,UC Berkeley 发布了 Serverless Computing 报告。 报告阐述了 Serverless Computing 的动力,分析了当前 Serverless 技术的优劣,以及这一领域目前遇到的问题和机会。 2019年3月,Red Hat 发布了 Quarkus。 Quarkus 是一个开源的,Kubernetes 原生的 Java 框架,适配 GraalVM 和 OpenJDK HotSpot。较之于传统的 Java 应用,使用 Quarkus 编写的 Java 应用程序在启动时间和内存消耗上有较大的改进。 2019年3月,Mozilla 宣布了 WASI,WebAssembly 的系统接口。 WASI 的目标是让 WebAssembly 代码运行在所有设备上、机器和操作系统上。WebAssembly 原来主要是为 web 客户端设计的,而现在 Mozilla 想将其扩展到其他地方,例如数据中心的服务端和 IoT 设备。 2019年3月,Pivotal 发布了 Spring Cloud Function 2.1.0.M1。Spring Cloud Function 是一层对于 Serverless 平台的抽象,基于 Spring Boot,推崇面向函数的编程模型。Spring Cloud Function 目前支持 AWS Lambda,微软 Azure 和 Apache OpenWhisk。 2019年4月,Google 发布了 Cloud Run 。Google Cloud 是一个托管式的计算平台,用户可以部署无状态的容器,容器可以接受 http 请求,按实际请求次数收费。Cloud Run 能够管理好基础设施,包括自动弹性和缩容到零实例。Cloud Run 基于开源 Knative 标准构建,用户的容器运行在安全容器 Gvisor 中。 2019年5月,Oracle 发布 GraalVM 19.0。 GraalVM 是一个通用的应用虚拟机,除了支持 JVM 语言外,还支持 JS, Python, Ruby, R 等其他语言,它可以通过 AOT 技术将应用编译成本地镜像,以提升启动时间、降低内存消耗。 2019年5月,Spring 核心开发 Juergen Hoeller 分享了 Spring 5.2 & 5.3 的 Roadmap。 其中包含了 Spring 5.2 的启动时间优化,以及 Spring 5.3 对 GraalVM Native Images 的兼容。
为什么是 Flutter 集团内已有越来越多的业务和团队开始尝试 Flutter 技术栈,从闲鱼的一支独秀引领潮流,到如今淘宝特价版、盒马、优酷、飞猪等 BU 业务相继入局,Flutter 的业务应用在集团内也已经逐渐形成趋势。那么,是什么原因让集团内越来越多的开发者选择拥抱 Flutter 技术栈?Flutter 的哪些优势吸引了集团 Native 开发者们通过 Flutter 开发并交付业务? 从技术上看,个人认为 Flutter 最核心的 3 个特点最为吸引开发者: 极高的开发与交付效率,良好的开发体验 优秀的跨多端多平台能力 极强的 UI 表现力 开发效率 从集团电商业务属性出发,业务响应效率及其背后的研发效率从来都是最为重要的指标。在保证体验的前提下,尽可能的提高研发效率,就意味着更高的生产力。传统的 Native 业务研发 iOS/Android 双端需要分别投入,研发成本高,端差异性大且依赖端侧发版,这也是为什么集团电商业务的活动类技术栈一直较为依赖前端体系,从 H5 到 Weex 到小程序,很大程度上就是在追求研发和交付效率以及灵活性。 如今Flutter很好的解决了跨端一致性问题,一套代码无差异的同时跑在 iOS 与 Android 两端;开发体验基本接近前端,支持on device的Hot Reload,去年年底Flutter又推出了在 Android Studio 中通过插件实现实时预览并支持交互的 Hot UI 能力,以及 Layout Explorer 可视化布局,让 Flutter 的开发效率和前端效率基本持平。 跨多端多平台 电商业务发展到当前阶段,已经不再仅仅局限于移动端场景,越来越多的业务需求对跨端跨平台性提出了更高的要求。钉钉/千牛桌面端应用场景,甚至天猫精灵、线下门店等业务场景,从长远看都需要一个比 Web 性能一致性更好适配成本更低的多端方案。目前跨多端技术方案主要依赖于浏览器和前端体系,但浏览器本身的沙盒属性、与系统较低的结合度、以及在低端设备上较差的性能都降低了研发效率和用户体验,提高了业务的交付门槛。可以说目前集团内的跨多端多平台方案是实质缺失的。 Flutter 从设计上就天然支持多平台开发,它的底层基于 Skia 跨平台图形引擎,向上构建出了一整套平台无关的渲染体系和事件处理体系,并紧贴 Native 研发模式自定义了基于 widgets 的声明 + 响应式编程范式,对系统能力依赖度低,并具备出色的跨平台还原度;支持多平台也是 Flutter 的战略目标之一,目前除了 iOS 和 Android,官方宣布支持的平台有 Mac、Windows 和 Web,Linux 也在开发中,它的技术特性也让将 Flutter 移植到 Linux based IoT 平台上成本很低,同时 Flutter 还是未来 Google 的下一代操作系统Fuschia的官方应用研发框架。可以说 Flutter 已经具备了成为下一代跨多端多平台研发模式的一切条件,围绕 Flutter 建立集团的多端多平台研发体系是非常可行的选择。 UI 表现力 电商业务重体验,重交互,尤其对于流量精细化运营场景,富交互的游戏化表现方式已经成为流量促活的重要手段。在 UI 表现力方面,前端体系一直具备着优势,通过 CSS3 强大的动画能力,开发者可以非常容易的实现复杂的动画效果和交互体验,而基于 Native UI,需要借助各种动画特效三方库,双端开发体验不一致,实现复杂且交付效率低。Flutter 很好的解决了这个问题,从补间(Tween)动画、基于物理属性的动画,到相对复杂的页面间 Hero 动画、parallax 交错动画等特效,Flutter 都可以跨平台低成本的高效实现,可以帮助开发者快速高效低成本的开发出极为炫酷的 UI,非常适合电商领域重 UI 视觉交互的各类场景,帮助业务构建出富有表现力的页面。 Flutter 体系化建设现状 目前集团内有多个业务 BU 均已开始尝试应用 Flutter 技术栈,涵盖了从电商详情业务、导购频道,到 Feeds 流、游戏化交互以及国际化等多个业务场景。目前 Flutter 技术在集团应用的痛点在于,研发基础设施的中台基建不够完善,研发支撑能力与数据运维能力未实现标准化,集团 Flutter 开发者生态还未完全拉通,暂时未能形成合力。这些问题将是我们后面在集团层面建设 Flutter 技术体系的重点。 另一方面从行业趋势上看,Flutter 技术已经成为越来越多行业伙伴重点投入的技术建设方向。字节跳动、美团等公司均建设了自己的 Flutter 工程化体系,并服务了各自的业务场景;腾讯也基于 Flutter 在多个 App 上进行了应用尝试,并在 Flutter 渲染能力服务小程序的场景下做了有益探索。行业伙伴们在 Flutter 技术上的投入力度和决心,一方面让我们对 Flutter 技术的应用前景和社区更有信心,另一方面也让我们感到联合集团各方力量共建 Flutter 生态的必要性和紧迫性。 手淘的尝试和思考 最后,简单讲一下我们从 18 年到现在在 Flutter 上做的探索和思考。手淘从 18 年 10 月开始探索 Flutter 渲染引擎应用在小程序场景;19 年下半年开始建设 Flutter 基础能力,并服务了淘宝特价版业务,在引擎、图片库、内存优化和加载性能等关键技术上做了沉淀;同时通过对 Flutter 的引擎改造,封装出 Flutter 2D Canvas 能力,向上支持小程序 Canvas 组件及小游戏引擎,服务 2D/2.5D 游戏化业务,并在业务场景中落地。在这个过程中,我们也沉淀了解决内存问题和图片问题等方案,以及 Flutter 技术与 Web 技术的对比与思考,取得了一定的技术及业务价值。 通过这些尝试,加深了我们对 Flutter 技术的掌控力和理解。在我们看来,Flutter 的横空出世,完全可以被看作吹响了 Native 体系复兴的号角。在保持 Native 性能优势的前提下,Flutter 带来了优秀的跨端一致性、贴近前端的研发效率,以及强大的 UI 表现力,为集团业务使用 Native 技术栈带来了新的可能。 从业务应用上看,Flutter 目前带来的最大价值是研发效率的提升。在基建和 native 扩展能力完备的前提下,开发基于 Flutter 的纯 Dart 业务的人效比之前各端分别开发的效率提高了接近 2 倍,单位时间内的需求响应能力也相应提高了接近 2 倍,目前已在闲鱼和特价版业务开发中得到了很好的工程化验证。从适应场景看,Flutter 目前比较适合承载富图文内容,如详情、Feeds 流、用户主页等常规业务开发,以及 2D/2.5D 游戏场景以及富动效业务。Flutter 通过单端技术栈可以同时满足以前需要 iOS、Android 以及前端技术栈分别负责的业务场景,甚至可以通过端云一体化的开发模式使用 Dart 负责一部分服务端业务逻辑开发,可以帮助业务团队拓展业务边界的同时,实现前后端研发能力闭环。 Flutter 目前的限制在于,动态性能力及前期的投入成本。前期投入成本主要指技术学习与团队研发模式升级的成本,涉及到技术路线选择,是我们和每个业务团队需要一起思考和判断的,这里不展开谈;动态性能力是 Flutter 的相对短板,目前能够通过 Flutter 模板化技术实现基于模板的组件级动态化能力,但基于性能、审核及对原生 Flutter 体系的侵入性等多种因素,目前还不能去直接实现 UI + 逻辑动态化能力。Flutter Web 方案虽然不存在审核限制,但受限于浏览器 DOM API 与 widgets 体系的差异性,目前仍旧存在较严重的性能瓶颈和渲染差异性,仅可作为降级的备用方案,暂时无法作为动态化的主要实现方案。未来在动态化方向的探索也将是个长期的博弈过程。如果后面我们可以解决好 Flutter 动态化的问题,那么 Flutter 完全有机会成为集团业务的核心研发模式之一。 综上我们认为,入局 Flutter 的时机已成熟,合力共建 Flutter 集团移动生态,这件事情大有可为。 AliFlutter - 阿里集团的 Flutter 体系化建设 在这样的背景下,经济体移动技术小组今年也将端侧架构治理的重点方向放在了 Flutter 上。移动技术小组从战略角度提出了 AliFlutter 项目,目标非常明确: 从经济体层面拉通 Flutter 体系建设,打造 Flutter 的公共技术基础设施,制定 Flutter 容器、中间件与 API 标准,建设 Flutter 研发支撑与数据运维能力,复用关键技术; 联合各BU构建经济体 Flutter 技术社区,沉淀共享集团 Flutter 技术及业务组件、能力与解决方案,服务集团 Flutter 业务,共建集团 Flutter 技术生态; 在经济体层面构建 Flutter 的对外影响力,联合各BU一致对外,打造阿里在行业内的Flutter技术制高点。 为经济体的 Flutter 技术体系“建基础、育社区、扛大旗”,我们责无旁贷。 未来 AliFlutter 的整体架构如下所示: AliFlutter 建设的第一步,就是要构建集团的 Flutter 基础设施、提供公共容器与组件、研发支撑服务与标准化研发流程,为集团的 Flutter 业务提供一个基础的 Flutter 公共研发服务能力,搭建好技术共建共享的基础和平台。 第二步,我们要服务好 Flutter 业务应用,探索业务应用模式与 Flutter 技术特性的结合点,并在过程中打磨技术,形成针对业务特点的解决方案与技术沉淀,真正盘活集团内的 Flutter 社区共建氛围与生态。 第三步,面向未来,解决好 Flutter 应用的几个核心关键问题:跨端与交互能力、业务研发效率与业务交付效率,通过技术赋能业务,让 Flutter 真正成为集团移动业务的核心研发模式。 接下来,就详细讲一讲每个阶段 AliFlutter 所做的工作和面向未来的思考。 基础设施建设 从 19 年 10 月 AliFlutter 项目启动开始到现在,我们基本构建起了一套 Flutter 的公共基础设施,包括 Artifacts 与 pub 库,Flutter 标准容器与 API 标准,并实现了 Flutter 的构建打包自动化,定义了标准的引擎定制工作流与业务研发工作流。目前基础设施已经具备支撑集团 Flutter 业务研发的能力,并支持各 BU 按需定制。 Artifacts 仓库 产物服务器主要是为了配合引擎定制,加速通过 Flutter 命令获取 Engine 中间产物的后端服务,便于统一 Flutter 开发者的工作环境。开发者可通过设置 FLUTTER_STORAGE_BASE_URL 来将 Flutter 工具链获取 artifacts 的地址指向该服务,同时通过 namespace 便可实现定制化的获取 artifacts 的能力以及内网加速服务。 Namespace 设计为区分不同 BU 的引擎产物,同时提供了公共 namespace 来存储公共产物,确保定制性和公共能力的按需配置;若后端存储上不存在需要获取的产物地址,则会触发从 Flutter 官方镜像做一次获取并缓存在服务端。 各 BU 可按需定制引擎,并按规定的路径格式上传至自己 namespace 中,即可实现从 namespace 中获取定制版本的引擎中间产物。 pub 仓库 类似于 Node.js 世界的 npm,Flutter 使用 pub 来管理三方组件与依赖。考虑到易用性、安全性等需求,为了管理集团内的公共二方组件,我们也搭建了内网环境的 Flutter pub 库。 该库的目标是成为集团的 pub 发布平台,管理集团内所有二、三方 pub package。用户可通过设置 PUB_HOSTED_URL 指向内部地址,来实现通过 Flutter 工具链获取配置以及发布二方 pub 的能力,用户也可以通过 Web portal 的方式访问 pub 库并查询已发布的 pub 组件。 容器、中间件与 API 对于业务的接入而言,现阶段核心要解决的问题就是提供一个统一的 Flutter 运行时容器,以及一系列集团标准化移动中间件的 Flutter 封装与 API 能力,并提供集团标准的插件扩展方式供业务方独立开发业务功能。 鉴于集团应用基本上均以混合栈为主,我们将 FlutterBoost 作为 Flutter 容器混合栈的基础,并配合集团标准路由与导航中间件提供了统一的混合栈路由导航能力,业务通过标准路由注册即可快速实现 Flutter 页面和 Native 页面的混合导航能力。 容器通过对接高可用平台,提供了初始化性能埋点与 Crash 数据上报等标准监控能力,为 Flutter 业务的技术性能分析和问题排查提供了基础。 集团移动端积累了一整套的标准中间件体系,包括网络库、图片库、push 消息、配置下发、数据采集与监控等一系列基础能力,在 Flutter 体系内无缝使用移动中间件能力对于业务是刚需;同时,小程序体系建设过程中形成的一系列标准 API,也很大程度上实现了一个完整的小程序运行环境的底层能力抽象,对于 Flutter 体系标准化的访问系统能力,实现平台无关的跨端能力是个非常好的补充。我们联合淘宝中间件团队与小程序团队,对基础中间件和小程序 API 实现做了 Flutter 侧的封装与标准化,未来也将对 Flutter 中间件和 API 能力进行系统支持。 标准化 Flutter 构建 由于 Flutter 研发体系较新,且构建 Pipeline 相较传统的移动构建流程又存在一定特殊性,产物构建配置复杂耗时长易出错,给 Flutter 业务的构建和发版带来了很大阻碍。因此我们也联合研发支撑部的同学,以插件的形式实现了 Flutter 脚本化的构建流程,支持双端自动整包打包和 Flutter Module 打包。 目前 AliFlutter 的构建流程默认使用 AliFlutter 的 Flutter 仓库以及集团内部 pub 仓库,引擎产物也统一按配置从 artifacts 仓库获取,较好的实现了支持 Flutter 业务的自动化构建需求。 业务应用 在夯实 Flutter 集团共建基础之后,第二步,我们 AliFlutter 在业务应用方面也做了大量工作。一方面通过原生 Flutter 的工程化能力持续服务淘系与集团业务;另一方面通过 Flutter Canvas 项目服务了小程序场景及游戏化场景下的互动业务。 淘系与集团业务支撑 目前淘宝特价版已完成详情业务的 Flutter 改造并上线,采用 Flutter 使业务在需求节奏不变的情况下人力投入减少一半,对缓解业务研发压力起到了明显的作用;同时应用的整体性能和稳定性与 Native 基本持平。后续特价版将基于 Flutter 继续拓展业务改造范围,并沉淀基于 Flutter 的业务域解决方案。 目前盒马、ICBU 、优酷也基于 AliFlutter 进行了容器接入升级与业务适配,盒马依托闲鱼的 Flutter 游戏引擎实现了盒马小镇业务,ICBU 在主链路相关页面使用了 Flutter,优酷则基于 Flutter 实现了会员订单页等场景。同时我们也在和钉钉及 Google 一起探索 Flutter 桌面端的解决方案。 Flutter Canvas 在电商活动营销中互动场景日益增多,对性能要求持续提升的前提下,如何提供一个高性能且稳定的 Canvas 基础能力服务好富交互的互动场景就成为了一个重点的课题。 在小程序场景中 Canvas 作为承载互动游戏的主要能力发挥了重要作用。然而受限于小程序架构下 app context 和 page context 的隔离设计,存在从 app worker 到 page renderer 的通信瓶颈,无法充分发挥出 web canvas 的性能,如果有一个 native 版的 canvas 实现将可直接在 native 层对接 app worker,降低通信成本,充分发挥 Canvas 的性能。 Flutter 底层基于 Skia,其性能和移动端复杂异构机型的适配性均得到过长期的检验,且 Flutter 基于浏览器的设计实现了一条平台无关的渲染管线,并对浏览器实现做了极大的简化,提供了很好的可靠性和性能。那么如果能够将这条渲染管线直接用于向业务容器提供 Canvas 能力,通过 binding 方式直接向小程序和小游戏容器提供与 Web Canvas 一致的标准 API,一方面可以复用 Flutter 的底层能力,为非 Dart 环境提供渲染支持,另一方面可以借助 Flutter 简化高效的渲染管线实现提供更好的渲染性能。 目前 Flutter Canvas 已落地手机淘宝,并在小程序运动银行业务进行了灰度试点,初步具备了承载小程序 Canvas 业务的能力;其性能在 Android 低端机上的表现有优势,可以作为 Web Canvas 方案的有益补充。 未来 Flutter Canvas 一方面将借助 Flutter 渲染管线的跨平台与高性能特点,以及 Flutter 对 Vulkan 和 Metal 的适配支持,在移动端获得更好的适配性以及性能;同时将继续实现 3D API,支撑未来互动类的业务应用。 未来建设 扎根业务之后,接下来的第三步,我们要紧贴 Flutter 体系在阿里集团未来的建设目标,持续回答好 Flutter 面向未来建设路径中的几个关键问题。那么首先,Flutter 体系在阿里集团的建设目标应该是什么?个人以为:Flutter 应成为阿里集团未来跨多端多平台的核心业务研发模式之一。 那么,我们目前离这个目标还有多大差距?在我看来,如果要想让 Flutter 成为业务的核心研发模式,那么必须解决好跨端能力、交互能力、业务研发效率以及业务交付效率四个核心问题。 从跨端能力看,Flutter 虽然已具备了很好的跨多端能力与高还原度,但涉及到平台能力时,仍然需要通过各端扩展实现,还未形成小程序体系这样的标准化的容器和 API 封装能力。那么如何更好的解决 Flutter 的容器化问题,让业务不感知平台差异性? 从交互能力看,Flutter 如何利用好自身交互能力的优势,在提供媲美前端的富交互体验的同时,降低 Native 富交互特性开发的门槛,真正吸引 Native 开发者使用 Flutter 技术开发业务? 从业务研发效率看,虽然 Flutter 的 Hot Reload/Hot UI 机制已经让开发 Native 页面的效率追上了前端,但在工程解耦方面仍然有很大提升空间,目前还无法高效的支持多业务团队并行开发;另一方面如何与现今流行的 Serverless 能力结合,实现端云一体研发模式,使业务实现研发闭环,也需要实践的检验。 从业务交付效率看,目前 Flutter 仍属于 Native 方案,依赖端侧发版,交付效率低,无法很好的承载电商系灵活性和实效性的需求;那么如何解决 Flutter 的动态化,帮助业务实现快速迭代? 解决好这几个问题,才能真正让 Flutter 成为集团移动业务的核心研发模式,为集团业务研发带来一个飞跃性的提升。下面讲讲我们在这几个方向的思考和探索。 提升跨端能力:Flutter 容器化 从工程角度看,虽然 Flutter 通过 Skia 跨平台图形渲染和自建事件体系基本实现了对宿主平台的最小依赖,但对于平台侧能力,目前 Flutter 还未也没有必要从应用框架角度做到一个统一的抽象,这就需要我们根据业务的诉求和特点进行有选择的封装。小程序 API 就做了一个非常好的示范,目前阿里小程序体系提供的 API 达到了 200+,很好的对移动端的 UI、多媒体、文件缓存、网络、设备能力、数据安全以及业务相关能力进行了封装,让业务开发者在小程序侧针对 API 进行系统能力调用,无需关心平台实现。 因此 AliFlutter 容器接下来的规划就是从工程体系的角度,提供一套标准化的 API 能力,以规范并抽象移动端的端基础能力,使业务尽量少甚至不关心平台差异性,专注于业务;同时借助标准化 API 的能力,实现跨多端多平台部署。 从移动端架构角度看,各个时期的跨平台方案都对 API 能力有着共同的诉求,从 H5 到 Weex,再到后面的小程序,以及 Flutter 等容器环境,进行了多轮的 API 重复建设,造成了缺少 API 接口的标准化定义,以及缺少实现层统一管控的现状。如果能够在 API 的 native 实现上做到接口统一,再通过各个容器分别提供接口供业务使用,可以更好的做到实现收口,并在统一实现层跨容器实现对系统资源的统一调度、管控和度量。 提升交互能力:UI + 游戏引擎 前文提到过,Flutter 目前最大的价值在于研发效率的提升,这是吸引业务团队应用 Flutter 技术的起点;但仅仅依靠研发提效还远远不够,当通过各种工程化手段解决好当前研发痛点,提升研发效率之后,如何说服业务继续使用 Flutter 体系进行业务开发?Flutter 带来的长远价值在哪里?个人认为,这个落脚点应该在通过游戏交互能力的泛化,打破 UI 与游戏引擎的边界,用游戏化的方式创造更有表现力的交互体验,创造新的业务玩法和价值。 大家知道传统的 UI 和游戏引擎是相互独立的两个体系,在 H5 应用中,往往是通过 DOM 或者上层应用框架做 UI,通过建立在 canvas 上的 H5 游戏引擎实现游戏能力。如果在游戏应用中有 UI 的需求,解决方案一般是自建一套简单的 UI 体系与事件体系,通过自绘的方式在游戏中叠加 UI,独立游戏引擎亦是如此。Flutter 从技术原理上看更像是一个建立在 Skia 图形库上的游戏引擎,它通过细粒度的 widgets 设计向上构建 UI 系统;同样得益于这样的细粒度设计,我们也完全可以直接通过 widgets 能力组合出一个完整的游戏引擎,提供 Game,Scene 及 Sprite 动效等 widgets 并扩展对应的 elements 和 render objects,并与UI体系共用一套事件处理机制、分层与渲染合成机制。这样做就相当于打破了原来 H5 中 DOM UI 和 Canvas 游戏的边界,让两个体系在 widgets 概念下完美融合起来,通过游戏引擎的能力赋能 UI 实现更多以前 UI 体系难以低成本实现的动效效果(比如一只小盒马一口吃掉了一个订单组件等等)。我们相信,这个方向的探索将会进一步释放 Flutter 的技术潜力,带来更多的业务可玩性与创造性,解放产品和设计的想象力,为业务创造更多价值。 提升研发效率:工程解耦与端云一体化 Flutter 工程解耦 前端体系的研发效率很大程度上来自于基于URI的统一路由体系带来的页面间解耦性,与页面内基于 Web API 的标准化带来的内聚性。然而目前的 Flutter 研发模式,仍需要多个业务团队工作在同一个工程下,互相之间存在源码依赖,未来如果跨业务团队大规模应用 Flutter 技术,必将拖慢业务的研发效率。 从工程解耦角度看,目前 AliFlutter 容器通过混合栈与标准路由能力基本实现了页面研发的解耦,未来的容器化建设通过提供小程序对等的 API 能力封装,业务对平台无感知,能够让我们有机会解耦业务研发,实现与小程序开发接近的研发体验和效率。 理想的方案是,业务可以从业务维度创建一个独立的 Dart 工程,只包含业务相关的页面和逻辑代码,通过 Flutter 的 Hot UI 开发页面,通过 IDE 提供的基于 Flutter Web 的能力本地预览工程并调试 API 与系统调用,完成研发期工作;也可生成预览二维码,使用预装有 AliFlutter SDK 环境的宿主应用扫码预览;研发与构建链路分离,云端主动拉取业务仓库代码参与整包构建。以此达到类似小程序研发方式的前端研发体验,同时实现业务间的研发解耦和并行发布,提高业务的交付效率。 端云一体化 如今 Serverless 概念越来越多的受到业务研发的重视和应用,集团在新一代的端云一体化研发模式上的探索这一年多来也做的如火如荼。结合轻量级容器环境、多语言支持能力与统一的 API 服务端编程,端侧同学可以很容易的使用客户端语言如 Java、JS、Swift 甚至 Dart 来开发服务端业务能力,实现服务编排、服务端 FaaS 业务逻辑与 API 自动生成,达到前后端工程体系归一,业务研发闭环的效果。 目前闲鱼在 Dart FaaS 云端一体化的探索走在了前面,通过集团的容器规范实现了 Dart function 容器,并联合服务端为部分业务需要的领域服务抽象出来 BaaS 层(存储、消息队列等),并封装了面向前端的 BFF(Backend for Frontend)能力层,使移动端开发者可以很容易的使用 Dart 封装 FaaS 业务逻辑,同时进行移动端和服务端 FaaS 开发,大大提高了业务研发效率。通过将原有的端侧请求接口、组装数据并转换为 ViewModel 的逻辑都后移到了服务端,经过字段映射与页面编排,移动端可直接获取 ViewModel 并刷新页面,通过 BinderAction 双向交互状态数据,有效屏蔽了通信细节,提高了开发效率。 云端一体化除了带来了研发与协同效率的提升,同时也重塑了生产关系,使端侧业务从只关注端侧体验和逻辑的开发角色,变成能够向业务整体结果负责;同时也让服务端更专注于领域服务的沉淀。Flutter 良好的跨端特性,能够屏蔽掉端差异化,配合 Flutter 容器化改造,更近一步的简化了业务的全链路研发模式。未来如何在 FaaS 模式下,沉淀出一套可以服务集团业务研发的通用端侧和服务端通信调度框架,让集团 Flutter 开发者和业务都能共享到 Serverless 技术和云端一体化提效的红利,是需要我们共同去探索和定义的新问题。 提升交付效率:Flutter 动态化 实现动态化是交付效率提升的重要方式。对于电商强运营强时效性特性来说,动态化几乎是一个必备的诉求,但从技术上看也是一个非常敏感的需求,是一个在系统厂商平台管控下长期博弈的过程。在我看来,动态化技术需要解决的核心问题,是在保证业务发布确定性的前提下,寻求技术性能、业务迭代效率与灵活性三者之间合理的平衡点。下面讲讲我们在 Flutter 动态化上的两个思路与尝试:Flutter 模板化方案与 Web on Flutter。 Flutter 模板化方案 动态模板化方案是集团内较为成熟的一套基于 Native 技术的模板化方案,专注于UI模板渲染,没有执行逻辑和运行时环境,目前被广泛应用于电商系的一些核心业务场景。同时该方案提供了配套的组件平台,支持在线模板编辑、预览、测试及发布整套流程,结合发布平台形成了一整套完善的业务开发生态闭环。 在 Flutter 体系下,目前闲鱼团队依据标准模板协议在 Flutter 侧实现了一套 Dart 版 SDK,通过模板下发实现了 Flutter 端的轻量级动态化组件编排能力;并通过一年多的迭代逐步解决了渲染性能与渲染一致性问题,较好的赋能了 Flutter 业务的组件动态化能力。 那么,未来 Flutter 与动态模板化方案有没有更好的结合点?答案是肯定的。从 DSL 角度看,目前模板的写法基本上来自 Android XML,对组件开发者尤其是非 Android 开发者不够友好,具备一定的学习成本;而 Flutter 的结构与属性均可通过 widgets 表达,可以极为灵活且平台中立的用声明式代码构建 UI 并绑定数据,易于开发者编写组件并通过 Flutter 框架独立调试与测试;在移动端运行时,可以按需按场景翻译成 native 组件或 Flutter 组件。未来我们也将持续在这个方向上做探索。 Web on Flutter 相比贴近 Native 研发模式的模板组件化渲染方案,Web on Flutter 希望通过类 H5 的 DSL + JS,借助 Flutter 的渲染能力,实现贴前端技术栈的动态化能力。 目前基于 Web 渲染的小程序方案存在启动耗时高,渲染性能距原生UI有较大差距等性能问题,这些问题很大程度上都源自于浏览器引擎的设计历史包袱(渲染管线复杂、 CSS multi-pass layout 及 legacy 实现等),以及 JS 到 Native 通信效率低(bridge)。Flutter 的设计思路源自浏览器,一方面直接吸收了前端框架近年来的演进成就,原生支持了声明式-响应式编程范式,提高了移动端的研发效率;另一方面, Flutter 紧贴 native 开发模式有限定义了 UI 结构、布局与渲染的必要元素,在满足 native UI 开发模式的前提下简化了能力定义与布局算法(single-pass layout 与 repaint boundary 等概念),很大程度上简化了渲染管线的复杂度,直接为 Flutter 带来了接近原生的性能体验。同时,Flutter 的 widgets 设计巧妙,结构_布局_属性等基础元素均使用 widgets 表达,且可通过基础 widgets 的组合来构成复杂组件,这种细粒度 + 组合能力设计让 Flutter 有极强的表现力,并具备向上对接各种研发模式的可能性。 因此,通过 widgets 组合小程序 DSL,支持小程序 CSS 有限集,实现渲染层替换浏览器引擎,并对接 JS 引擎支持 JS 执行能力,是一个将 Flutter 应用于小程序生态的合理探索方向。相比把开发者惯坏了的浏览器,这种方案在 CSS 的能力支持上必然是受限的,无法满足所有 CSS3 标准的实现,更多通过紧贴 Flutter widgets 的现有能力以及必要的 widgets 扩展,在不破坏 single-pass layout 的前提下组合出 CSS 能力;但从 Flutter 原生开发的角度看,只要 Flutter 现有的原生能力能够满足业务需求,那么受限的 CSS 实现也一样可以提供和 Flutter 对等的能力解决业务问题。同时,通过受限的 CSS 可以换来与 Flutter 相当的高性能,与基于 Web 的实现相比,在性能上带来了质变。 我们在 18 年底通过一个内部项目实现了这个思路的原型,通过使用 C++ 重写 Flutter 的 widgets、rendering、painting 及事件管理等 Dart framework 中的中低层能力,并在 widget 上用 C++ 实现了 CSSOM + DSL -> Widgets 的响应式框架,直接从 C++ 层提供 render 实现,将传统由 JS 承担的模板展开、tree-diff 计算与渲染工作交给 C++ 层,通过 Flutter 提供的 Widgets tree 到 RenderObjects tree 的 diff 能力实现,显著提高了性能。从实现的简单的 demo 看,相对小程序的 web 渲染性能有了大幅的提升。 这种方案的问题在于和 Google 代码库分裂后的长期维护性。打破重重阻碍,Flutter 和 Web 生态如何对接?这篇文章对集团内在这个方向上尝试的几种思路做了较为全面的对比,未来我们也将继续在这个方向上做深入和持续的探索。 总结与展望 Flutter 体系化建设目前在淘系刚刚起步,仍然有大量工作需要去做,我们在基础设施、工程化以及通过 Flutter 定义和收敛业务域研发模式上做的许多建设性的事情,正朝着把 Flutter 打造为统一移动应用基础研发框架,助力业务回归移动端研发模式这个大目标一点点迈进。 在移动技术小组启动 AliFlutter 项目之前,闲鱼技术部已经在 Flutter 技术建设上做了大量探索和投入,一方面通过 Flutter 技术赋能业务提效升级,拿到了很好的业务成绩;另一方面沉淀 Flutter 的技术与业务解决方案,并通过开源反哺社区,在海内外 Flutter 技术社区中建立了显著的技术影响力与领导力,也涌现出一众 Flutter 技术专家。 接下来 AliFlutter 的重点任务,就是要和闲鱼、财富等先驱应用者以及盒马、钉钉、飞猪、优酷、饿了么、CBU 等 Flutter 的实践者一起,在集团层面把 Flutter 的场子建起来,把集团 Flutter 生态拉起来,让技术和经验能够共同沉淀和分享,一起来把 Flutter 技术体系在阿里的应用生态内做大做强,真正成为集团业务的核心研发模式,并让每个参与者都能从中受益。 我们一直坚信 Flutter 技术的先进性和应用前景,未来我们将继续立足淘系服务集团业务,和集团开发者携手前行,在 Flutter 这个技术方向上坚定的走下去。
自从提倡 SOA 架构风格以来,个人觉得软件架构并未有特别突破的变革,主要是在 SOA 面向服务架构风格基础上不断演化迭代,基于服务的 EA 明确分层架构也好,微服务也罢,都是在面向服务架构基础上的适应不同的场景的迭代升级。 我先抛出一个观点,我觉得服务化架构的本质,和西方教育界深受影响的古希腊哲学家苏格拉底的“产婆术”的教育思想本质上是非常相通的:苏格拉底的“产婆术”思想强调教育是一个“接生”的过程,教师就是“接生婆”,人们之所以接受教育是为了寻找“原我”以不断完善自身。也就是教育的目的在于唤醒而不再于塑造。同理服务化架构的本质也不仅仅在采用什么样的技术框架实现和塑造,更重要的是在于通过不停地在共创中反问、反思、反省等方式进行对业务的本质的不断追溯、抽象、综合归纳演绎,我们的每一个架构师都是服务化架构的接生婆,我们的使命是建立真正反映业务本质并驱动业务不断向前的架构。 我们是否足够深入理解业务的本质,做了足够的归纳演绎以及综合抽象,是否清晰的反应到了我们的服务化的根基:业务模型、域模型以及平台公共语义模型上?这是我们每一个参与服务化的每一个产品、架构师、TL 和核心开发同学需要回答的第一个根本问题。 定义 面向服务的架构(SOA):SOA 是一种架构风格,致力于将业务功能保持一致的服务(系统服务,应用服务,技术服务)作为设计、构建和编排组合业务流程以及解决方案的基本单元。 目的 我们采用 SOA 的架构是为了什么呢? 为了更好的复用?为了更好的责任切分?为了接口和实现的分离,提升灵活性和隔离性?还是为了更好的接口分类和管理? 以上说法其实都没错,但是面向服务化的架构 SOA 的目的远远超过接口技术细节的设计与定义,其核心的关注点在于服务的业务内容以及内涵,而不仅仅是如何设计和实现。 同时,SOA 更多的也不是如何构建一个服务,任何人都可以很容易地创建一个服务,这并不是 SOA 的核心挑战,而是如何赋能企业构建有业务价值意义的完整业务语义的服务集合。 面向服务的架构致力于在企业内的不同的业务环境内,建设业务功能驱动的服务,从而将服务组装成有价值、更高级别的业务流程和解决方案平台。 面向服务的架构的真正的价值体现在当可重用的服务被灵活组合、编排在一起来构建敏捷的、灵活的业务流程,其中敏捷体现在服务可以快速调整,独立演化;灵活性体现在服务由于其业务功能定义明确,边界清晰且功能内聚性强,同时服务具备各自独立完整生命周期,可被灵活组装。 如果面向服务架构能为企业提供了重大的价值,那么这些价值通过什么来体现的呢? 价值体现 行为一致性 面向服务的架构允许我们为业务流程、任务或者决策拥有唯一的共同的入口,也就是,不管服务访问的路径如何,服务给业务提供的业务行为都是一致的。 数据一致性 面向服务的架构允许我们为业务数据信息提供单一的访问入口,也就是它提供给业务一致的、企业内部共识的公用数据访问。 模块化及敏捷性 面向服务的架构 SOA 为业务功能、业务决策和业务信息的模块化提供了非常好的机制。同时,在模块化实现好的情况下,这些模块可以在多个业务流程和场景中被灵活复用和重新组合,从而为业务竞争力和创造性提供灵活性和敏捷度支持。 功能与数据的解耦 面向服务的架构 SOA 提供了业务功能和信息集成的同时,减少了他们之间的依赖和耦合性。也就是,独立的业务功能单元,应用系统,可以一起协同工作,同时各自又具备各自的演进计划,生命周期和业务目标。 高度可管理性 SOA 提供给我们通过定义服务水平协定在服务模块粒度支撑我们的业务目标,我们可以不断的设定、监控和优化调整组件,应用以及系统所承载服务的考核。 其中行为一致性和数据一致性作为服务的核心价值根基。 服务 一、定义 首先我们先定义一下服务是什么? 服务是通过服务契约的方式来提供业务功能的独立单元,同时受服务契约所明确管理。 服务是设计、构建和编排组合一个完整业务实体中业务解决方案的基础单元。服务契约指定了服务消费方和提供方之间所有的交互约定,包括: 服务接口 接口文档 服务策略 服务质量 服务可用性 性能 那我们经常听到模块、组件等其他的软件构件,服务和他们有什么区别呢?其中最核心的区别在于服务本身是被明确管理的,其服务质量和性能是通过服务水平协定(SLA)被明确管理的,而模块以及组件并无此约束。此外,服务的全生命周期包含从设计、部署到增强升级和维护都是可管理的。 举例(下列内容仅做示例展示用,非适用于严格场景): 补货计算服 服务策略 服务质量 性能要求 补货建议量计算服务 针对行业下商家/供应商维度的入仓货品补货建议计算 在销量预测符合分布要求且满足准确率水平要求的情况下,根据缺货率服务水平要求的产生的补货建议量符合业务期望的周转天数 10W + 货品 * 30 仓,品+仓补货及建议量 <= 30min 订单创建服务 包含购物车下单+立即下单场景,满足所有优惠计算后的订单生成 订单创建成功率 99.999999999% 峰值支撑:100w 单/s 二、服务构成 服务自身主要包含两个主要方面,第一方面也是服务最核心的方面就是服务的接口,另外一方面则是服务的实现。服务非常好的实现了接口和实现的分离。 1)服务接口 服务接口指定了服务的操作,也就是服务是做什么的(What),操作的输入输出参数,以及用来约定如何使用和提供这些能力的协议。 服务通常包含围绕着一个核心的业务功能操作以及相关联的操作。例如补货建议计算服务中核心的操作是生成货品+仓维度的补货建议单,其他相关操作包含查询补货建议单相关销量预测操作,查询补货建议单对应计划库存操作。 服务 核心功能操作 关联操作 补货建议计算服务 品+仓维度补货建议计算 补货建议单对应销量预测查询 补货建议单对应计划库存操作 2)服务实现 服务实现指的是服务如何通过其明确定义的接口提供其能力。服务实现可以通过以下方式实现: 完全基于编码实现 基于其他服务的编排而成 基于已有应用适配封装而成 以上情况混合实现 核心点是服务如何被实现的对于服务消费方来说是透明的,服务消费方仅仅需要关心的是服务是做什么的,而不是如何被实现的。 服务可以提供在保持服务接口或者行为约定不改变的情况下,提供根据不同的行业不同场景提供各种不同的实现。 服务实现在保持服务接口或者行为约定不改变的情况下,可以自由进行升级和切换。服务实现既可以是静态的更新升级,也可以使动态路由实时切换实现,如对应到不同的行业以及不同的业务场景的自动实现切换。 不管服务实现如何升级或者按需自动路由切换,只用服务的行为和契约不会发生改变,用户也就是服务的消费者根本不会感知到任何不同。 我们可以把服务接口想象成室内普通电源国标插口,服务策略为室内非防水情况下适用,服务契约想象成 24X7 的 220v 电压供电能力(其中 180V~250V 50Hz 是质量要求,24x7 稳定性要求,电流供给 <= 10A 是性能要求),此国标插座(服务提供方)可以给包含与此接口匹配且符合契约的任何电器(消费方)交互并提供供电能力,支持其运转。 服务接口定义了交互的的风格和细节,而服务的实现定义了一个特定的服务提供方或者特定的业务实现如何提供其能力。 这种类似连接点/插口的设计极大的方便了更松耦合的业务功能解决方案。 三、服务接口与服务实现的逻辑构成 服务接口与实现的构成也有两个重要的不同方面,分别是执行功能的方法和执行的信息数据。换句话说,一个服务是由一个业务服务操作集合以及对应操作的输入输出的抽象业务服务数据模型组成。这层业务服务数据模型是企业业务层次或者平台业务层次的业务实体的抽象,独立于底层数据存储与实现。此业务数据模型是和各子域密切相关联,但是超越各子域以上的,在完整的业务线或者平台层次上达成一致的业务数据模型,也就是说在各子域之间达成共识且约定的严格明确的公共模型,主要用于平台业务流程中不同域服务的交互,是平台层次统一的业务语言,我把它暂时称为平台业务数据模型。 此平台业务数据模型通常需要包含平台统一语义的业务术语表,平台各域核心实体表,平台各域核心实体交互图等。 接口与实现的逻辑构成: 1)服务操作 服务操作声明定义了这个操作的输入以及输出参数。 2)平台业务实体模型 平台业务实体模型描述了服务中输入输出数据的结构以及含义。服务接口中的信息和服务实现中逻辑数据之间的差异是至关重要的。 在服务接口层次上,最重要的是信息必须在业务服务之间进行交互来赋能业务流程并完成业务流程。这些信息必须在参与流程的所有业务服务间达成一致且在服务之间通用,也就是平台层次所有服务公用且标准的业务实体模型,同时此业务实体模型必须在平台业务语义上明确且完成,确保可以支撑平台所有端到端的业务。此平台层级的业务实体模型并不是一蹴而就的,但是可以随着平台的重心变化不断迭代完善成型的。 然而不同的是,从内部来看,很多服务在各自实现的子域内部都有这些信息的不同的超集,可能潜在的存在不同的数据格式。幸运的是,我们不需要感知也不需要在所有关联服务的相关子域实体模型上达成共识,即使不是不可能,但是也不太现实。与之相反,服务接口和服务实现的分离设计允许非常方便的进行平台业务实体模型和服务所在子域领域模型进行映射转换。 3)服务接口最后一个重要的方面就是服务水平协议 SLA。服务水平 SLA 协议指定了服务的的两个重要方面的指标,分别是业务上的指标和技术上的指标: 技术指标:响应时间RT,并发吞吐量 Throughput,可用性 Availability,可靠性 Reliablity。 业务指标:完成的业务功能的质量或者完成度,如产生的补货建议是否满足业务预期的周转缺货KPI要求:周转下降 10 天,缺货率下降5%。 服务化分层架构 理解服务化分层架构,首先要对 TOGAF Meta-Model 有个清晰的理解,从元模型可以看出业务服务和业务流程的上承业务,下启系统平台的核心作用,一定要深刻理解业务服务和业务流程在企业架构中的重要性,下面我把我翻译后画的版本给大家放在这里,给大家做个参考,TOGAF 不多做解释,如有需要,大家可以交流,后面有时间尝试写下我对 TOGAF 的学习和理解。 通常情况下,我们会按照不同行业的不同的业务流程去搭建系统,如供应链最初在大家电 3W 行业孕育,我们按照 3W 的行业和业务场景搭建了平台商家相适应的计划系统;后续自营行业又根据自己的行业也搭建了自营的计划系统;后续小电数码、国际以及其他业务快速发展,跟随业务快跑的同时,也各自建立的各自的业务流程。在这个过程中,BPM 为建造不同的业务系统提供非常好的抽象支撑,但是经常的结果是,BPM 被用作构建了更高层抽象的,也更高效的,但是却是烟囱式的应用,而不没有更好的贡献更多的支撑到整体上能快速应对业务变化而更灵活,更敏捷的业务平台或者系统。 而这正是面向服务的架构中业务规则以及决策作为服务要发挥更大作用的地方。面向服务的架构允许我们将特定业务流程中的业务规则和业务决策抽象分离出来变成业务规则或者决策服务,这些规则和决策服务就可以被灵活应用到不同的业务流程中,从而这些服务可以被统一管理和演化升级。 BPM + SOA 一起提供了支撑企业架构的完美组合。BPM 提供更高层抽象定义业务流程的能力,以及与流程相关联的重要监控和管理能力;业务服务提供了支撑业务流程的核心的功能、决策以及信息。面向服务的架构则提供能力将服务组合在一起来支撑和创建灵活且敏捷的端到端的企业业务。如果只有 BPM 而没有 SOA 对于创建单独的业务应用或许非常有用,但是通常是创建的烟囱式的应用,很难扩展到企业内或者平台内不同的业务线。如果只有 SOA 而没有BPM虽然可以创建可重用且一致性高的服务,但是缺少将这些服务快速搭建业务流程并支撑端到端业务的能力,也无法支撑建立具有竞争力且可以随着外部竞争环境进行敏捷反应的业务。 下图显示了一个建议的的封层服务化架构图,各分层如下: 端到端业务流程 业务流程是按照一定业务规则决定的顺序执行的业务操作组成。高层级的业务功能,通常跨越应用域或者业务线。通常由行业开发团队开发,此行业开发团队可以具备明确的实现组织结构,也可以由跨团队的相关域共同组成虚线团队。例如,电商业务中,用户选购下单交互流程;供应链业务中的补货调拨计划流程等。 平台业务服务 高度模块化的业务功能单元,由不同类型的子域服务组合编排而来,可作为业务流程的编排单元。跨行业通用的业务服务可由功能所在核心域开发团队编排开发,行业内通用的业务服务可以由行业开发团队负责编排开发。例如,补货审批服务 子域服务 平台各功能子域提供的服务,对平台可见,用于平台业务服务的组合编排,也可以作为更高层的业务流程编排的基础单元。子域服务通常由平台各子域开发团队负责开发。例如,销量计划服务,补货建议计算服务。 子域基础服务 用于支撑各功能子域服务的基础服务,对子域可见,对平台不可见,用于子域服务的编排。 子域基础服务通常由平台各子域开发团队负责开发。例如,入仓决策服务,计划单据服务,计划库存服务等。 基础子域服务 或称为基础业务域服务,提供平台基础业务服务,为各个功能子域或平台业务服务提供基础业务功能及数据服务。例如:商家服务,货品服务,库存服务等。 基础架构服务层 提供不同层次所公用的基础架构服务,如用用户管理,权限管理,操作审计等等。 我们通常按照上述分层结构来描述平台架构或者企业内部架构,看上去好像层次结构清晰明了,但是却是不完整的,因为此面向服务的架构描述缺失了平台系统架构中一个核心部分,暨信息及信息模型分层,这一点非常之关键,往往会决定架构的成功与否。 为了使架构更完整同时也更真实,我们需要添加对应的完整信息抽象(实体模型 or 领域模型): 核心单据模型 端到端业务流程中操作的核心单据,承载业务核心价值的信息单元模型,例如,销售订单,采购订单,补货计划单等。此模型通常是平台公共语义模型的核心子集。 平台公共语义模型 定义了平台层业务流程、业务服务交互数据。在平台层面或企业层面,端到端业务流程中交互信息的公共语义模型,此模型不仅对平台业务流程中交互的各实体进行了明确的定义,而且包含了业务流程中所需要的完整的业务语义实体,同时各业务语义实体边界明确,责任清晰。核心单据模型通常是平台公共语义模型的子集。平台公共语义模型包含下层子域的对外服务实体子集,按照端到端的完整平台业务语义,可由平台各功能子域模型所共享给平台的核心实体子集有机整合而成,也可由平台业务模型全新定义,或者从 TOP-DOWN 以及 BOTTOM-UP 两个方向共同融合而成。需要注意的是此模型必然是无法一蹴而就,需要经过无数迭代而不断完善,但其一定是不可或缺的。平台的诸多架构决策和不断演化完善需要基于此模型来进行。 子域领域模型 平台各功能子域的领域模型,用于驱动各功能子域的应用系统设计和开发。子域领域模型需要保持动态稳定,通过防腐层同所依赖的外域或者外部服务进行隔离,防止外部服务污染子域内的核心业务语义,同时保持域内业务功能灵活可控。子域领域模型仅通过其对外服务实体子集对外可见,其余对外不可见。 跨域映射模型 用于各子域领域模型实现对外部模型的防腐依赖。 基础架构服务层 提供不同层次所公用的基础架构信息模型,如用户模型,权限模型等。 信息架构模型框架 现在来讨论下服务化分层架构重视度并不太高的另一个重要侧面:信息架构,之所以说信息架构非常之重要,是因为信息架构与服务化架构是一个密不可分的完整的整体。我对信息架构模型进行了分层划分,下面从 TOP_DOWN 方向来讨论不同的分层模型。 Level 0:战略与决策模型(高层战略视角) 这层次模型用于定义企业的战略方向和商业目的,从而定义了企业内任何系统平台开发的方向和终局。这必然作为企业内任何系统平台开发的基本背景和基调,影响任何系统平台开发项目的中长期目标定义和终局设定。 Level 1:商业模式(业务线 owner 视角) 这层模型从业务线 owner 的视角,用运营主体的业务术语描述其商业模式的本质,包括其整体结构,业务流程,以及组织结构等。 Level 2:业务抽象概念模型 这层模型从业务架构的视角用信息化的方式对单个业务线或者多个业务线的业务进行抽象。Level 1 描述是对于企业业务来说有意义的东西或者事情,而 Level 2 则给予这些有意义的东西以更严格且清晰的定义,明确其内涵以及外延并体系化,同时根据不同行业线的业务内容进行提取抽象,抽象出共性的内容,用于更高效灵活的描述和定义业务 。 Level 1 描述的是业务运营人员所感知的业务流程,Level 2 不仅描述了这些业务流程,更重要的是抽象并描述了了这些业务流程所应该包含的底层业务功能。 同样的,Level 1 描述对企业业务来讲所有重要的东西,Level 2 描述的是组织想要管理的信息后面最根本的内容。Level 1 描述的事情是 Level 2 定义的基本实体的实际业务中对应的样本或事例。 简而言之,Level 2 是 Level 1 的抽象(Abstraction)与综合(Synthesis)。 为了达成这一视图,必须要仔细分析和归纳,有时候需要演绎的方式来定义出隐藏在企业业务运营主体视图下根本结构和内容。 Level 3:平台公共语义模型 Level 3 层公共语义模型同 Level 2 层业务概念模型保持紧密一致,在此基础上增加了服务化视角的语义。Level 3 公共语义模型描述的内容是在必须在平台层业务服务间共享的具有一致语义的业务实体和信息,是平台层一致的共享信息模型。这层模型用于描述平台层服务接口交互的共享信息,基于平台完整业务语义下所有服务所公共数据的标准化视图模型。简而言之,平台公共语义模型,定义了业务平台层次基本业务服务语义,是平台各业务服务之间,平台业务流程和平台业务服务交互的统一语言。 Level 4:域模型 Level 4 层域模型定位于平台各子域的领域模型/实体模型,用于对各子域的核心业务功能进行抽象。域模型是平台各子域的标准模型,不仅明确定义的各子域功能服务暨服务接口的语义,同时也包含各子域内服务实现中的关键实体的定义。域模型从整体上来说是平台各子域的私有模型,除了服务语义外整体不对外可视。公共信息中的服务视图是域模型的子集。 域模型核心用于除了用于暴露到平台子域的业务服务设计与实现外,同时也用于驱动域内服务功能的设计和实现。 域模型是需要保持动态稳定的,除非域内业务发生本质变化,域模型应该是相对稳定的。域模型稳定性最大的敌人是外部的依赖,如何不受外部依赖的侵蚀而逐渐腐败,域防腐层存在的最主要原因。子域防腐层维护外部依赖服务和子域模型之间的动态映射,维护域模型的独立性,保护域模型不受有害侵蚀。 域模型我理解基本和我们通常谈的领域模型基本接近,对于各域内业务的抽象,驱动各域技术设计方案设计和实现,至于具体的模型表现形式,采用基于亚里士多德的物质本源的思想(“Material Cause,Formal Cause,Efficient Cause,Final Cause" —> 实体+属性+关系)的ER图,还是基于我们老祖宗老子道家思想("人法地、地法天、天法道、道法自然" —> 实体+行为)的思想的领域驱动 DDD 的方式,个人认为各有伯仲,组中能清楚表达出业务本质即可,后面单独写一篇抽象建模的文章聊一下这两种不同的思想。 Level 5:实现模型 此层模型为开发者视角的实现模型,也就是我们系统实现核心的对象模型,是我们系统落地的基石。 设计服务 我们初步了解的什么是服务,以及什么是服务化的分层?那如何设计服务以及服务化架构呢?下面给出基本步骤和方案。 一、理解整体背景 首先,我们要理解服务化架构的整体背景。我们必须理解我们所支撑的业务和业务根本驱动力以及所有的业务流程,业务场景以及业务用例;同时对于平台系统,我们还必须理解公司的战略所赋予平台的使命是什么?我们平台中长期的目标是什么?平台的终局是什么?这些组合和在一起才是服务化架构的完整的上下文背景。这些必须要反映到我们的业务模型、平台公共语义模型和各域模型中去。 然后,我们需要提出并回答如下问题: 我们当前支撑的是什么样的业务?(业务模型) 这个业务或者这些业务的中长期目标和短期目标分别是什么? 平台的短中长期目标是什么?平台的终局是什么? 上述目标是否存在冲突,如何平衡和取舍? 实现这些目标,需要完成什么样的成果? 这些成果如何衡量? 取得这些成果,需要什么样的能力和信息? 实现这些能力需要什么样的流程、服务、实体以及规则 现有的服务、应用或者系统提供了那些基本能力和信息? 前面六个问题描述了整体的架构需求(包括业务和平台),而剩下的问题则描述了整个服务化架构的上下文以及引入了服务目录库的需求。我们服务不能只从单个服务的角度来看,而必须从整个服务集合的角度来反应完整的业务语义和平台语义。我们的服务集合也就是服务目录库必须具备完整的上下文语义,必须能识别出: 整体的上下文背景,包括完整的业务语义和平台语义。 服务职责范围 关联的服务的分组 服务的类型和角色 服务目录库的设计必须支持两个主要的设计时目标: 第一个目标是要提供一种机制来帮助理解服务整体的上下文背景,用于更好的服务选择及更高效的服务重用。特别是,这个服务实现了什么样的责任,以及如何和其他的服务相关联。 第二个目标是要提供一种机制来识别一个特定服务的责任边界,用来指引服务的实现。这是一个非常关键的点,特别是在避免服务的功能和数据重复上非常重要,不仅仅是避免重复建设,更核心的是要以此保证业务功能和数据的一致性。 服务目录库中的服务可以按照服务类型以及服务角色来进行组织。服务类型请参照服务化分层架构内容里的描述;服务角色包含任务服务角色、实体服务角色和决策服务角色,请参照后面小节描述。 二、服务设计原则 面向服务化的架构的其中一个成功的关键是创建一个具备完整业务语义的服务集合以便于可以方便一起进行组合编排来支撑不同的业务流程以及丰富的业务场景。 我们经常谈论各功能域要提供松耦合的服务,是因为服务间的松耦合是非常重要的,特别是通过减少服务间的依赖以便于服务可以在不同的场景中被复用,以及可以起到隔离变更影响的作用。但是如何才能尽可能的实现这个目标呢? 首先我们来看下对于服务最重要点是什么?首先就是这个服务提供了什么样的业务功能,其次这个服务对业务有价值的数据产生了那些影响。从这两个点上我们就可以比较容易得出两种类型的耦合在服务接口设计中是特别重要的: 数据依赖 功能依赖 举例来说明下: 交易服务协调所有的活动,然后依赖其他服务来帮助完成流程。交易服务依赖于或者说耦合于用户服务,商品服务,库存服务,营销服务、订单服务以及支付服务等。 为啥交易服务没有实现所有的功能? 首先是因为我们想在其他高级别流程或者服务中重用底层的能力。 第二是交易服务服务并不负责用户服务,商品服务,库存服务,营销服务、订单服务以及支付服务。交易服务只是使用它们,而不是负责实现它们。 用户服务被用作管理客户信息访问,它具有唯一的责任来提供、维护和更新客户信息,这样做的目的是为了可以在任何需要访问客户数据服务的地方重用客户服务。比代码重用更重要的是隔离或者是集中式访问客户信息,因为只有唯一的路径访问数据,数据就总是一致的,真正实现 Source Of Truth。因此,尽管有很多服务包含交易服务,购物车,订单历史等服务需要访问客户服务,通过松耦合的这种模式去管理这些依赖是比较容易被理解的。 通过创建服务来执行用户管理,商品管理,库存管理,以及营销管理等,就可以在任何可以用到的地方,执行保持一致性的这些业务功能。 敲黑板:好的服务设计并不仅仅是关注重用性,更重要的是要提供一致性,既包含功能一致性,也包含数据一致性。 那么下一个问题是你如何决定有哪些服务以及这些服务分别是什么呢?同样,你用功能分解和信息隔离组合在一起来决定服务有哪些并且各自是什么? 对线上交易功能的分解引导去识别用户、商品、库存、营销、订单以及支付等相关功能服务。 对信息的隔离引导我们去识别用户和商品等作为交易订单中的共享信息。 面向服务的架构中服务设计的问题需要跨越多个以致于所有的流程中来一起考虑。 因此,服务设计原则基本原则如下: 避免服务间的功能重复 避免服务间的功能缺失 避免数据重复 实现数据的协同访问 具备统一、一致的方式来执行给定的功能 在服务化设计中,如何实现上述的这些原则呢?答案是提出并回答如下问题: 谁负责这个功能? 这个功能在哪里被用到的? 谁负责管理这些指定的数据? 谁负责定义和实现那些特别的业务规则 流程中的哪个步骤具备执行这个任务所需要的特定的知识 这些问题的答案会帮你来识别如下信息: 服务应该做什么? 服务对什么负责? 同样重要的是,识别服务不应该做什么,而应该依赖其他的服务来支撑 三、服务颗粒度与类型 我们通常设计服务时候一个很大的疑惑是我的服务到底要设计成什么样的颗粒度,应该更粗粒度一些,还是更细粒度一些?答案是:没有一个统一正确的服务颗粒度标准。那怎么办?我如何设计我的服务的颗粒度呢?虽然没有统一的标准,但是我们可以依赖下面的因素来决定合适的服务粒度: 谁是服务的潜在消费方?其他服务,业务流程还是外部合作方? 服务在哪里被消费,通过什么样的路径被消费,也就是服务的拓扑结构是什么? 服务的性能要求是什么? 服务预期的业务范围或者边界是什么? 在几乎任何复杂的环境或者系统平台中,我们可以预期到多种多样类型的服务。这些服务具有不同的类型和颗粒度,可以参考服务化分层中的内容,也可以见下面的描述: 端到端的业务流程 业务流程通常跨越整个企业或者平台多个业务域,通常是由底层服务构建而成 平台业务服务 业务服务是最粗粒度的服务,业务服务提供高度抽象的,组合的业务功能给到平台或者企业。业务服务的功能和数据同业务流程所需要的业务语义紧密结合。数据整合服务在这个层次提供端到端的业务流程所需要的整合后的数据。 子域服务 子域服务是中等粒度的,他们提供特别针对于每个业务子域的业务相关服务,被本域内的不同业务服务所使用,但是未必暴露出子域外 子域基础服务 子域基础服务通常是最小粒度的服务,他们提供更低层次的服务,用来提供子域内子域业务功能的基本功能支撑 基础子域服务 子域基础服务通常也提供教小粒度的服务,用于支撑上层业务功能服务的业务功能完整实现。 基础架构服务层 基础架构提供了在更高层级服务构建中细粒度的能力,独立于任何业务域。这些服务需要和业务相关明确区分开来,例如安全认证,权限管理以及纯粹技术编排服务。 四、服务角色 独立于服务的粒度,职责范围以及服务创建以外的另外一个重要考量或者说是侧面是:服务在服务组合或者流程编排中所承担的角色是什么? 那么怎么来区分不同的角色呢?我们使用关注点隔离的架构原则。例如,我们在构建应用中就使用了将数据同逻辑隔离作为重要的概念。这样不仅提供了不同关注点解耦的可能以及机会,而且允许采用不同的方式,在不同的地方来实现这些不同的关注点。 对业务流程进行单独管理的BPM就是一个非常好的例子,BPM作为另外一个关注点分离的例子,将业务流程方案从其他逻辑中分离出来,可以使工作流程可以在一个特定的层次或者环境内进行执行和管理, 这样就可以实现通过快速的建立新的流程模型来快速响应业务的变化。同时面向服务的架构SOA提供了将业务服务作为构建业务流程的基础构件的功能。业务规则系统BRMS同样也作为一个关注点分离的例子,将业务规则或者业务决策从其他应用逻辑中区分开来,这样业务规则和业务决策也可以在一个特定的层次被执行和管理,从而就可以很容易的被变更来支持新的业务需求。这里,业务规则以及决策服务也是面向服务的机构来暴露出规则和决策服务来支撑规则和决策与业务流程的分离。 通常我们通过较粗粒度的来定义三大类服务角色来构建不同的服务层次: 任务服务角色 任务服务通常实现一个完整的业务功能,既可以是基本业务功能,也可以是复杂的业务功能,如计算某个货品在某个仓的补货量,或者一个简单的业务校验,如此货品在此仓是否可补。 此服务类型颗粒度范围较广,包含从独立的子域基础服务到大的平台业务服务都可以具有任务服务角色,更小颗粒度的服务倾向于具有更通用的目的,更大的可重用的潜力。业务服务几乎总是承担任务服务的角色,通常是小颗粒度服务较大的组合,可以被设计成支持一个或者更多特定的流程。因此这些服务通常在跨业务流程中广泛复用的潜力更低。但是也是正常的,因为他们通常是有其他可重用的服务组成的。 通常,具有业务角色的服务是主动服务,通过主动行为来提供价值 实体服务角色 主要管理访问业务实体的服务具有这个角色。业务实体的例子如用户、类目、商品、价格、库存、购物车,主要对应主要的业务信息。实体通常是中到大型实体,倾向于独立于任何特定的业务流程,而可做为多个不同业务流程的组成部分。具有实体服务角色的服务通常通过适配和提供需要的信息来实现任务的方式来支撑任务服务。实体服务通常都具备较大的重用的潜力。 规则 / 决策服务角色 规则 / 决策服务是通过执行业务规则来提供业务决策的服务,如补货计划自动审核服务。 规则 / 决策服务通常用作对复杂问题进行判断或者支持变化频繁的业务规则,如复杂且多变的审核规则等。 规则 / 决策服务通常为小到中等大小颗粒度,通常用来组装成更大的服务。规则/决策服务是可以不同层次不同类型的服务,包括平台业务服务,子域服务,子域基础服务等,但是通常情况下规则/决策服服务也来支撑这些服务类型。 我们通过组合这些不同类型的服务角色来提供灵活的业务能力,从而用来支持业务流程内的活动。我们提供了一些基本原则来帮助我们进行服务组合以便于帮我们减少依赖,限制耦合以及最大化灵活性。 服务层次以及组合基本原则: 业务流程的任务通过任务服务实现,业务流程路由的核心规则由规则/决策服务来提供,而不是定义在流程网关内。这一块内容后续详细说明。 更高层次的任务为核心的业务服务由其他更小的服务组成 服务依赖严格单向原则,上层服务可以依赖下层次服务以及同一层次服务,但是下层服务不可以依赖上层服务 一个任务服务可以组合规则/决策服务、实体服务以及其他任务服务 但是一个实体服务不允许直接调用其他实体服务 现在我们可以通过丰富的流程,实体和决策服务的集合,可以创建新的不同的服务组合,把规则的灵活可变的好处同服务化架构的模块化,灵活性以及重用性结合起来作为业务系统平台级别的基本架构方式。 服务化如何成功? 一、大规划 大的规划首先要明确 2-3 年内的服务化的目标。大的规划切记事无巨细,而是根据长期规划设定明确的指导性原则和要求,在体系化的基础上鼓励协同和创新。 二、小目标 服务化不应该是运动式的大跃进推进,而应该是坚持试点、推广、总结、扩大试点,从而由点到面,逐步落实的方法,由各域根据规划的体系化要求,再各自情况暨各自成熟度来设定各自服务化目标,制定一个个小目标,快速迭代,敏捷式的总结推进。 三、真共识 建立共识的根本是要讲清楚服务化的目标、架构、设计、开发背后的清楚的逻辑,让每个人想的清楚,听的明白。 四、接地气 接地气同达成共识一样,要用朴素的工程师语言讲清楚目标和逻辑,而不是拿各种看上去非常光鲜亮丽的各种名词来充当台面,讲的人解释不清楚,听得人一头雾水,没有体系化逻辑来支撑落地,最终很难达到服务化真正的目标的。 五、结硬寨 服务化是一个庞大的,迭代的,渐进的体系化工程,不是快闪战,不是突袭战,是场持久战,一定要有曾国藩的“结硬寨,打呆仗”的耐心和准备,踏踏实实落地迭代推进,小步快跑,在坚持体系化思考的基础上进行持续总结改进,通过一个接一个战斗,一个小胜利接一个小胜利,一个战役接一个战役不停的攻城略地的基础上逐渐迈向成功。 六、Think Fast & Slow 一句话,高效的方式就是慢想、快干。我们不一定缺少高执行力的人,但是一定缺少能独立思考并体系化行事的人。
本文将主要围绕以下四个方面展开: AI 技术背景 自然语言处理 语音技术 机器视觉 一、AI 技术背景介绍 目前的 AI 技术都是以深度学习为基础,而深度学习完成如此复杂的学习过程需要两个条件,首先需要大量的数据,深度学习非常依赖数据挖掘技术,用于产生大量有效的训练数据。此外,深度学习还需要优化算法,因为深度学习要在非常复杂的网络中找到最好的模型,用于匹配数据。在最基础的深度学习模型上,有三个主要的领域,既图像视觉、语音交互和自然语言处理。其中,图像视觉是由图像处理和理解、自然人识别、视频编解码和内容分析、三维视觉等技术组成。语音交互是由语音识别、语音合成、语音硬件技术等组成。自然语言处理包括自然语言应用技术、语义理解计算、翻译基础计算等技术。所有这些技术组成了人工智能技术。综上而言,人工智能是由深度学习和机器学习组成的。 1 机器学习 机器学习的目标是利用有限的样本对未知的目标函数求近似。任何机器学习模型都有三个 component 组成,首先确定要学习的函数空间、然后确定使用的数据,用哪些训练数据拟合机器学习模型,最后是找到优化算法,让机器从函数空间中学习到最好的模型,即最佳匹配数据的模型。 2 深度学习 机器学习是考虑所有可能的函数,而深度学习只考虑一个特殊类的函数,神经网络。在数据方面,深度学习的数据要求比普通模型的要求要高很多。在大数据支持的前提下,才能够真正发挥深度学习的作用。传统的优化只是做凸优化,而在深度学习场景中要处理非凸优化。因此,深度学习在三个 component 中都会遇到非常大的挑战。首先,神经网络构成的函数空间非常不清楚。其次,由于大数据的复杂性,训练数据的难度比传统机器学习的难度要大。最后,非凸优化无论在理论或实践层面都没有很成型的模版。所以业界为了找到最佳的实践也在做很多的实验研究。 3 人工智能发展的关键 人工智能发展主要包含两个关键点。首先可以利用大量丰富的“活”数据。利用“活”数据的应用有很多,如 2016 年谷歌的 AlphaGo 战胜了围棋世界冠军。另外,AI 技术具备强大的计算能力,如目前非常火的自动驾驶技术,Google 的 Waymo 可以在非常长的距离下无需人为干预的进行自动驾驶。但是这些技术早在 20 多年以前都有所实践,在 1995 年,Backgammon 通过和自己下 1.5 万盘棋,成为了世界冠军。在 1994 年,Alvin 以每小时 70 英里的速度从美国的东海岸开到了西海岸。相比这 20 多年的发展,本质上的不同点是数据的数量级和计算能力的提升。如人脸识别技术现在都需要上亿级别的训练数据,而以前只有几百万张的数据。传统的 AI 技术要依靠很多的 GPU 才能得到比较好的模型效果。 自然语言处理 1 自然语言处理模型 自然语言处理也有着很长的历史,以前叫计算语言学。传统的计算语言学方法使用统计学的语言概率模型构建自然语言模型。如下图中的“中国鼓励民营企业家投资国家基础建设”,这一句话可以被解析为一个语言树,分为主语、谓语、宾语、动词和名词等内容。也就是利用语言树表达这句话的语法结构。另外,传统的自然语言中常用的技术叫统计语言模型。如下图中的拼音串 “ta shi yan jiu sheng wu de” 可以有多种可能的汉子串表达,人为判断的话应该是最后一条“他是研究生物的”。实际上,人类大脑中通过大量阅读会形成一个概念图表,知道哪些表达是可能发生的,形成了一种统计语言模型。最典型的统计语言模型是 Bi-gram 模型,计算一个词之后可能出现的词的概率。但传统的计算语言学方法存在模型欠精准,文本处理效果一般等弊端。 鉴于传统方法的局限,深度学习可以用于自然语言处理中,其中最成功的的模型叫深度语言模型。与传统方法的区别在于它将所有词的上下文信息用张量表示,还可以双向表示,即对未来和过去都做预测。此外,深度语言模型利用了 Transformer 结构,可以更好的捕捉词和词之间的关系。 自然语言模型 - 问题应用 问答应用传统的方式是常见问答对(FAQ)和知识图谱(KBQA)。如下图中的例子,问答对是一个数据库,包含问题和回答。这种方式相对保守,且编辑问答对要求人对相应的 domain 有比较深的理解,很难扩大领域,且冷启动慢。为了解决此问题,随之出现了机器阅读理解的技术,它可以直接自动从文档中找到匹配问题的答案,通过深度语言模型将问题和文档转化为语义向量,从而找到最后的匹配答案。 目前问答应用广泛应用于各大企业,如阿里小蜜,闲鱼卖家助理,每天帮助百万级的买家自动获取商品和活动信息。 2 自然语言处理 - 机器翻译 另外一个比较成熟的 AI 技术的应用是机器翻译。传统的翻译模型叫统计机器翻译模型(SMT),如下图左侧,从翻译结果来看,统计机器翻译模型容易产生错译,整体流畅度差,包含语法错误。引入深度学习的神经网络机器学习模型(NMT)的结果错译少,流畅度也比较高,符合英文的语法规则。 下图中可以看到,Google Brain 对神经网络做了一个评估报告,其中 phrase based 翻译模型达到的效果有限,而基于神经网络的翻译模型有了明显的提升。同时,在阿里巴巴业务中机器翻译也得到了广泛应用,如电商场景中对商品信息的翻译,钉钉 AI 翻译等。但是因为钉钉的信息都是比较随意的表达,所以钉钉 AI 翻译在未来还有很大的进步空间。 三、语音技术 语音技术在很长一段时间内都被想象成是编码的技术,将文字编译成语音信号。而语音识别的过程是属于解码的过程。 通常语音识别有两种模型,语言模型(Language Model)和声学模型(Acoustic Model)。语言模型主要的场景是预测某词或词序列的概率。声学模型预测通过词 W 的发音生成特征 X 的概率。 1 语音识别 混合语音识别系统 传统的混合语音识别系统叫 GMM-HMM,GMM 用于声学模型, HMM 用于语言模型。即使在语音识别领域大家做了很长一段时间的努力,但还是无法达到人类语音识别水平。到了 2009 年之后,基于深度学习的语音识别系统开始发展, 2017 年微软声称它们的语音识别系统比传统的语音识别系统有了明显的提升,甚至比人类的语音识别水平更好。 传统的混合语音识别系统包含独立优化的声学模型,语言模型和语言学家设计的发音词典。不难发现,传统的语音识别系统的构建流程非常繁琐,它需要多个 component 并行开发,各个模型都是独立优化的,导致最终的优化效果不尽人意。 端到端的语音识别系统 基于传统的语音识别系统遇到的问题,端到端的语音识别系统中将声学模型、解码器、语言模型、发音词典都结合在一起,统一进行开发和优化,使得效果达到最优。实际的实验结果明端到端语音识别系统可以进一步降低识别 20+% 的错误率。此外,模型的达到会大大缩小,可以达到传统语音识别模型的几十分之一。而且端到端的语音识别系统还可以在云上发挥作用。 2 语音合成 语音合成大概分为几个 component。首先是前端的文本分析,进行词的拆分,识别 break,这些会构成语言信息。之后,传到后端通过声学模型产生声波。 语音合成历史 语音合成技术从最早的 GMM,到 2000 年的 HMM,再到 2013 年,基于深度学习的模型。而到了 2016 年,WaveNet 相较于之前的模型,在语音质量上有了质的飞跃。2017 年出现了端到端的语音合成模型。2018 年阿里巴巴的 Knowledge-aware Neural 模型不仅能够产生很好的音质,还实现了大规模的模型压缩和计算效率的提升,可以实时产生有效的合成语音。 语音合成一直存在一个较大的 borderline,即定制化成本非常高。通常传统的语音定制则需要专业的发言人,还要在录音棚中录制,人工精准的标注,而且需要大量的数据,一般大于 1 个小时。而如今,语音合成需要在个性化声音定制方面做一些尝试,任何的普通人只要通过手机进行录制,即便在噪声环境下,也可以完成个性化的声音定制。如可以将车内导航系统的语音换成家人的声音。 3 多模态语音交互方案 当人和人对话时,不只是在听声音,而是通过视觉和听觉结合起来理解对方表达的意思。未来的语音交互系统中,还需要将多模态交互方案引入进来。目前的语音识别系统在嘈杂环境下的效果还是不尽人意的,在地铁等嘈杂的公共环境中还是会遇到较大的挑战。阿里达摩院希望将语音识别和机器视觉进行结合,采用多模态的人机交互技术将语音识别与计算机视觉结合的方式,让机器人看着对方,听对方说话,就可以在嘈杂的环境中精准识别用户发出的声音。 举一个例子,假如在地铁站买票,和卖票机器进行对话,同时因为后面也排着很多人,他们也会说话。这时通过视觉的方式,可以判断哪一个人脸更大,从而识别买票的人说的话。下图展示了基于人脸特征监督信息的目标说话人语音分离主要算法处理流程。最后是提出的音视觉特征输入和基于音视觉融和的信源掩码估计模型。 音视觉融合技术应用 音视觉融合技术已经在很多生活场景中得到广泛应用。覆盖了上海的主要交通枢纽,如地铁站,虹桥火车站、上海火车站、上海南站、虹桥机场和浦东机场等。从 2018 年 3 月至今累计服务旅客超百万人。此外,2018 年 9 月杭州云栖大会上达摩院和肯德基合作的基于多模态技术的智能点餐机在 3 天内完成了4500 单。2019 年 8 月钉钉推出了搭载多模态交互技术的智能办公硬件新品 M 25,可以在嘈杂的声音环境下使得交互更加有效。 四、视觉技术 1 图像搜索 视觉技术中最核心的就是图像搜索的识别,同样也经历了很长的发展过程。在早期的 90 年代初期是基于全局信息的底层特征进行搜索,如将图像颜色的信息做分布,但这种方法的精度非常糟糕,如 ImageNet Top 5 只达到了 30%。到 2000 年初,大家开始基于局部的特征编码特征做图像的搜索和识别,精度达到了 70%。但是其中局部信息都是由人工确定,如果出现人没有见过的特征,则无法有效提取。到了 2010 年左右,大家开始使用深度学习的技术,自动的提取局部信息特征,从而精度达到了 92%,使得图像搜索技术完全可以应用于商业场景。 图像搜索和识别发展历程 目前,图像搜索面临的挑战主要有三点,首先数据越来越多,10 亿级别的训练数据。同时还要处理上亿级别的分类。而且模型的复杂度也越来越高。 为了解决以上挑战,阿里推出了九鼎,一种大规模 AI 训练引擎。九鼎是大规模训练载体和专家系统,涵盖了视觉、NLP 等领域。九鼎由两部分组成,首先是通讯,因为所有大规模训练都需要多级多卡,如何有效的通过多级多卡提升模型的训练,减少通讯的代价是较为重要的问题。另外一部分是优化算法部分,如何做好分布式的优化问题同样也是目前遇到的较大的挑战。这种大规模训练引擎可以处理大规模数据的分类并达到很好的训练效果。ImageNet ResNet50 可以在 2.8 分钟内就可以训练完成。若处理 1 亿级的 ID,10 亿级别的图片分类可以在 7 天内训练完成。 图像搜索应用 图像搜索在实际生活场景中被广泛应用。目前,拍立淘可以处理超大规模的图像识别和搜索任务,其中有 4 亿+ 的商品,30 亿+ 的图片,和 2000 万+ 的活跃用户。可以识别 3000 万+ 的实体,覆盖了 SKU 商品,动物,植物,车辆等。 天巡是用于遥感图像识别分析的应用,可以进行大规模的遥感影像训练,拖动遥感图像的路网提取,地物分类,新增建筑物识别,违章建筑识别等任务。 2 图像分割 图像分割指的是从一张图中将 object 分割出来。传统的图像分割方法是如下图左侧,分割成很多像素,看每个像素之间的相似度,相似的像素聚合起来一些区域,再输出。但传统的图像分割技术无法学习到语义的信息,只能知道图中的 object,但不知道 object 是什么物体。另外,因为采用了无监督的学习,在分割边角时精度不高。 而基于深度学习的分割技术基于监督学习,将很多训练样本接入技术中。同时还可以得到分割的结果和分类的结果,理解每个像素的实例归属。而且在大规模的数据前提下,编码器和解码器模型可以精细的分割 object 的边缘。 图像分割应用 阿里将图像分割技术应用于淘系的全类目商品中,可以自动生成商品白底图,提速商品发布。 另外,还可以用于服饰的素材拼图场景中,商家会提供模特素材,利用分割技术,将模特身上的服饰进行分割,自由组合搭配。 3 模型压缩 目前,深度学习技术已经广泛应用于多种行业中,同时也遇到了很多挑战。首先,深度学习模型越来越复杂,其中计算量在不断增长,达到了 20G FLOPS+,还有不断增加的连接。模型变大那就需要较大的 memory 进行存储,找到合适的 Device 将是非常困难的事情。即使有了 Device,模型也需要跑很长的时间。此时,模型的压缩技术就显得非常重要,它可以将几十 G 的模型压缩到几十 M,用户可以在任何的 Device 上运行模型,无需等待很长时间。 模型压缩已经发展了很长时间。如下图中的模型,可以将模型中不重要的边去掉,进行稀疏化。然后对模型的边进行量化,给不同的权重。最后对模型进行分支,改变结构。FPGA 的加速方案可以在相同 QPS 条件下,相对 GPU 提速 170 倍(RESNet-18 仅需 174us)。 模型压缩本质上是改变模型的结构。模型结构的选择是比较难的问题,它不是一个普通的优化问题,不同结构之间是一个离散的空间。阿里提出出的 cargotainer 方法,可以更快速的获取准确的 pseudo gradient,在 2019 年 ICCV 大会举办的低功能耗图像识别挑战(Low-Power Image Recognition)中获得了冠军。 模型压缩技术的应用 基于 FPGA 的解决方案在盒马自助收银机得到了应用,利用机器视觉方法识别是否漏扫商品,GPU 成本缩小到 1/2。同时自研的高效检测算法,可以在 1 秒内完成多种行为分析任务,扫码动作分类准确度达 90% 以上。场景分类准确度达 95% 以上。 4 目标检测 另外,机器视觉技术可以应用与视频信息的结构化任务中,检测目标物体,跟踪识别。目标检测和跟踪识别任务主要的处理流程如下图,对视频进行解码,目标检测,目标跟踪,高维特征提取,属性提取,存储为结构化数据。 目标检测技术也出现了很长一段时间,传统的检测方法是 HoG,DPM 等,依靠 Handcrafted 特征,即人工选择特征。这种方法的问题在于鲁棒性差,无法泛化,计算量冗余度高。而现在也出现了很多基于深度学习的目标检测方法,如 Faster RCNN、SSD、RetinaNet、FCOS 等。它们的优点是机器可以替代人工识别的特征,可以对物体的尺寸,外观的变化更加鲁棒,泛化性能好。如下图中的折线图,可以发现从 2008 年到 2019 年,从较低的准确度(大约 20%)提升到了 83% 左右。 5 目标跟踪 目标识别出来后还要进行跟踪。在目标跟踪中遇到的挑战是人是动态的,在行动的过程中会被其它物体或人遮挡,这档过程中会丢失目标,如下图中的红色衣服的人会被紫色衣服的人遮挡。传统的方法是根据 position 进行匹配,但在上述拥挤场景中,预测位置难以精确,匹配很容易出现错误。而基于深度学习的方法是抽取 appearance feature 进行匹配,预测结果更加鲁棒。 目标检测和跟踪应用 目标跟踪的应用场景一般在新零售场景中。购物中心和品牌门店需要对客流及场内行为进行深入洞察,构建线下人、货和场地的数据关联。提升线下运营的管理效率,提升消费者体验,最终促进业务的增长。 另外,目标跟踪技术用于案发场景下。但因为案发场景中视频都是非常长时间的内容,难以人为检查识别。那么能否将整个 24 小时的信息浓缩后在几分钟内看完。其中需要利用目标检测和目标跟踪的技术,识别人和物,跟踪轨迹。将不同时间的轨迹一起播放,如果对某一个或某一类轨迹感兴趣可以点击进去,看到这类的视频内容,大大减少了观看视频的时间。 总结 可以发现 AI 技术的发展离不开大量数据的支持,因此目前的人工智能技术还是以数据为驱动。如机器翻译和专业的翻译人员相比,人在翻译的时候并不是完全以数据为驱动,不需要阅读上亿的数据,更多是基于 Knowledge 的方法,高效的处理已有的信息。所以在未来,如何让机器从 Data Driven Approach 走向 Knowledge Based Approach 还需要进行不断的探索和努力。
前言 最近 CodeReview(代码评审,又叫代码审查,下称 CR)心态相当的平和,代码是一个讲道理的东西,是就是,否就否。在 CR 时,沟通特别轻松,问题讨论也特别聚焦,因为它是量化和定向的。CR 的过程不是恃强凌弱,也不是一言堂,大家看着代码,当是一种灵魂的交流,那么每一次的 CR 也是同事间提升和谐度的一种方式。优良的 CR 传统可以体现团队温度,体现高年级同学传帮带的技术文化。平时,大家抬头看 PRD,低头写代码,很少有时间静心气闲地交流一下业务流程、业务逻辑、业务未来扩展,在 CR 时,往往可以反复被讨论到。有些问题是 CR 阶段群智群力发现的。人的一个能力,不是解决了问题,也不是发现了问题,而是利用某种手段预知了问题。曾经有段代码,我觉得取反逻辑生涩难懂,反复修改之后,发现写代码的小伙伴是错误的领会了业务意图。 提升技术质量、促进人才成长、培养技术情怀这些口号我们今天先放一边,聊聊最近 CR 的切身体会。CR 不是互相看天书,而是产生天天看书的感觉,每一段写得好,写得不好的代码都是一本书,好的代码希望见贤思齐,差的代码希望见不贤而内自省也。总之,CR 是一种修行,也是一种自我积累,苦涩的是看到惨不忍堵的代码,心里说:我去!有意思的是看到优雅的代码,心里也说:我去! 业务跑得这么快,没时间 CR 这是一个很大的谎言,不要为自己的丑代码找个华丽的借口,没有时间好好 CR,总有时间焦头烂额地处理故障和投诉。时间老人是公平的,我一直认为某个同学在工位上噼里啪啦打字,就是说明他干活快,通过团队打字比赛,发现其中 20% 在按 BACKSPACE 键。业务跑得快,代码写得快,可能写的是一堆没有营养甚至是有毒的代码。我们需要追求的是 CR 的效能,而不是逃避 CR。CR 是一种修行,对于双方都是一样的收获。因为如果想象成一个摊派任务,抵触情绪总会油然而生。业务跑得快,也得两腿是健康的, CR 就是让业务持续快的一个小医生。要不然,今天冲刺 100 米,明天就嗝 P,这样的不正常的业务节奏对公司的中长远发展肯定是弊大于利。 代码是讲道理的 我觉得靠烧香来保佑代码不出问题时,保平安往往也是暂时的。牛叉的代码,就是在小流量、单线程没有问题,在高流量、高并发时还是没有问题,你的限流,你的容灾,你的降级各种导弹防御系统一样自动打开并正确地发挥价值。很多人的思维觉得,代码只要在场景和逻辑上没有问题就行,那是因为夜路走得不够多,还没有碰到鬼。代码是讲道理的,就像有一个同学说 >= 比 > 更加慢,那只是我们的潜意识猜测,经过深达编译层的分析,发现两个指令几乎是完全一样。其实凭我们的想象,那也是一个位运算级别的操作,从左向右比,如果一处有 1,另一个没有 1,那么前者一定是更大。没有无缘无故的爱,没有无缘无故的恨,一切的故障总是代码的字里行间。我们需要做的,就是读懂她,用好她,写好她。如果代码任性闯祸,那么是我们不懂代码的心思。 每一行代码的存在是有意义的 更加严格地说,每一个字符的存在都应该是有意义的。如果某行代码的存在完全是可有可无的,这个时候,我们考虑过 JVM 的感受吗?凭白无故地要编译这些字节码,然后栈进栈出的忙活一阵子,然后告诉它,你的劳动是没有任何价值的。比如: Boolean assetFlag = Boolean.true; 这里都已经明确地给给出来显示的初始值,可是在调用端,居然还有这样的判断: if ( assetFlag != null && assetFlag == true) {...} 什么情况下为 null 值啊?另外参数在框架里已经做了值的判断,那么下边又是 n 行,对所有参数重新判断一遍,是对我们的代码有多少不自信,还是对框架不自信?每一行的代码,相当于生命,它的存在一定是有意义的,一定是能够被执行到并且能够为实际的业务负责的。 我们比拼的不是代码行数 在 CR 过程中,发现有些方法,重复用到一段逻辑,这段逻辑如果不抽取出来成为一个方法,未来的修改就成了一个必须多点全部修改的大坑,稍有不慎,容易遗漏。重复代码在提交行数上,似乎挺壮观的。如果在同样的效果上,3 行代码能够实现功能的价值,就不应该用 4 行来实现。我们经常说晒出代码行数,并非是单纯地鼓励代码行数多,而是提倡大家去写代码,写优质的代码,优质的代码一定是少即是多的原则。代码的实现,不要像鲁迅先生说的一样:懒婆娘的裹脚布又臭又长。 用户视角的成功与失败 在交付时,调用服务失败,然后返回前台一个空列表,那么前端业务的展示是后台数据正常,这个人不拥有数据列表,这明明是对数据的一种曲解。所以,后台调用服务失败,就应该明确告诉前台,服务出错了,这个用户有没有数据。系统出错的信息给用户看,合适吗?不合适。前后端的用户交界面上,往往飞着两类信息:错误码、错误信息。这样够了吗?用户提示需要额外地再给出来,往往根据不同的错误码,有不同的用户提示,可能是一个多对多的关系。多个错误码,提示给用户的信息:请输入必填项。多个用户信息,可能也对应一个错误码。一般来说后台承包这三者的联动关系,json 串推送给前端时,前端拿来主义即可。 有重复使用的量一定要找个地方集中隔离 不管是变量,还是常量,工具类,如果是多个地方同时用到,那么如果硬编码在代码或者沉淀在包里,未来一定是一个灾难。比如,一个组装 SQL 语句的代码,到处都是"from"、"where"、"limit",都是这类语句直接写死在代码中,注意问题来了,这些单词前后都需要加空格。有时候在复制粘粘时,发现少了一个空格,出现的问题,往往是致命的。再比如,一个互相约定的分隔符“###”,定义在本类中 private String,这明显是两个共同遵守的常量,单独定义的结果就是容易造成不匹配。隔离的目的是复用它,保护程序地正常运行,易于维护。 单测没必要代码 CR 单测有时候感觉像是阑尾,有或没有感觉都是无关紧急,这是错误的观点。单测感觉就是一个任务。你写单测了吗?写了。单测是否需要 MOCK,是否进行边界值测试,是否用例覆盖到业务场景,这都也是 CR 的一部分。单测写得好,BUG 肯定少。需要调试来查找错误时,往往是一种对异常处理机制的侮辱 良好的日志和异常机制,是不应该出现调试的。打日志和抛异常,一定要把上下文给出来,否则,等于在毁灭命案现场,把后边处理问题的人,往歪路上带。别人传一个参数进来,发现是 null,立马抛出来一个参数异常提示,然后也不返回哪一个参数是 null,这在调用参数很多的情况下,简直就是字谜游戏一样。到底是抛异常,还是抛错误码?我不管抛什么,反正错了什么东西,都应该透明出来。到底是抛受检异常,还是非受检异常,我只想说,没有充足的理由,不要乱抛受检异常。异常抛出时,一定要自己消化干净,告诉别人说我的方法签名抛的是 AbcException,实际运行中,代码某个地方直接抛出 EfgException,这也是不负责任的。 多个 return 的语句,概率高的一定先进行判定 if(condition1) return; if(condition2) return; if(condition3) return; 那么需要评估一下 condition 1/2/3 出现概率的大小,概念大的在最前边,尽可能快地进行 return,不需要进行后续无谓的匹配。不要总觉得计算机跑得快,不差这点蝇头小利的,这种思维,和《南辕北辙》里的寓义一样的吗? 吝啬空行 感觉空行是廉价的,到处乱扔是一种;另一种是感觉空行是昂贵的,舍不得用,这种情况更多见。50 行代码没有一个空行,就像英语 50 句话,没有任何标点符号一样。既然标点符号起到隔断和语义区分作用,我们的空行不是同一个道理吗?在以下情形: 在方法的 return、break、continue 这种断开性语句后必须是空行。 在不同语义块之间。 循环之前和之后一般有空行。 命名太随意 代码有两件事情比较头疼:命名和循环。人如其名,如果不是它干的活,名字却是一副道貌岸然,太容易把人带偏了,一个中国人如果取名叫赵 C,一个女孩子如果取名叫石敢当,第一印象生生地给扭曲了。英语不好的同学,要么用错英文单词,要么翻词典,整出一个专八的词汇,任何人都不认得这个单词,在 CR 时,还需要打开在线翻译时的命名,绝对不是好命名。当然如果在线翻译都翻不出来的时候,那更头疼。如果表意错误,那更要命。 注释是电影的旁白 电影的旁白:1)信息量大。2)适时出现。就像 《Star Wars》 里开始的一段一样,如果不交代那些背景,可能进入正片是一脸懵逼的。在代码上不需要写正确的废话,名字取得好,自然是自解释的。在嵌套循环中,或者在复杂条件分支中,往往是需要讲明白的。另外,添加业务背景信息,以及执行频率,执行条件,甚至维护者注意点,都是注释的重要理由。识别到哪里要写注释,也是一个对业务的阅读能力,而不是代码阅读能力。 满天飞的函数式编程好吗? 不好。如果一个 stream 后边的调用超过 5 个,我觉得你是为了炫耀,因为别人不敢改这段代码,体现出来你的不可替代性。这种 10 行都是函数式编程的方式,就像让人在水里憋气超过 10 分钟不能换气一样难受,有点缺氧的感觉。函数式编程调试困难,难于上青天,如果没有办法修改调试器的话,那只有委屈那些喜欢天马行空写一串函数式语句的同学了。如下图,个人反对这种直接 return 一个长链路的处理结果:
最近在一个项目中,因为涉及很多状态的流转,我们选择使用状态机引擎来表达状态流转。因为状态机 DSL(Domain Specific Languages)带来的表达能力,相比较于 if-else 的代码,要更优雅更容易理解。另一方面,状态机很简单,不像流程引擎那么华而不实。 一开始我们选用了一个开源的状态机引擎,但我觉得不好用,就自己写了一个能满足我们要求的简洁版状态机,这样比较 KISS(Keep It Simple and Stupid)。 作为 COLA 开源的一部分,我已经将该状态机(cola-statemachine)开源,你可以访问获取:https://github.com/alibaba/COLA 在实现状态机的过程中,有幸看到Martin Fowler写的《Domain Specific Languages》。书中的内容让我对 DSL 有了不一样的认知。 这也是为什么会有这篇文章的原因,希望你看完这边文章以后,可以对什么是 DSL、如何使用 DSL、如何使用状态机都能有一个不一样的体会。 DSL 在介绍如何实现状态机之前,不妨让我们先来看一下什么是 DSL,在 Martin Fowler 的《Domain Specific Languages》书中。开篇就是以 State Machine 来作为引子介绍 DSL 的。有时间的话,强烈建议你去读读这本书。没时间的话,看看下面的内容也能掌握个大概了。 下面就让我提炼一下书中的内容,带大家深入了解下 DSL。 什么是 DSL DSL 是一种工具,它的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。 这种清晰并非只是审美追求。一段代码越容易看懂,就越容易发现错误,也就越容易对系统进行修改。因此,我们鼓励变量名要有意义,文档要写清楚,代码结构要写清晰。基于同样的理由,我们应该也鼓励采用 DSL。 按照定义来说,DSL 是针对某一特定领域,具有受限表达性的一种计算机程序设计语言。这一定义包含 3 个关键元素: 语言性(language nature):DSL 是一种程序设计语言,因此它必须具备连贯的表达能力——不管是一个表达式还是多个表达式组合在一起。 受限的表达性(limited expressiveness):通用程序设计语言提供广泛的能力:支持各种数据、控制,以及抽象结构。这些能力很有用,但也会让语言难于学习和使用。DSL 只支持特定领域所需要特性的最小集。使用 DSL,无法构建一个完整的系统,相反,却可以解决系统某一方面的问题。 针对领域(domain focus):只有在一个明确的小领域下,这种能力有限的语言才会有用。这个领域才使得这种语言值得使用。 比如正则表达式: /\d{3}-\d{3}-\d{4}/ 就是一个典型的 DSL,解决的是字符串匹配这个特定领域的问题。 DSL 的分类 按照类型,DSL 可以分为三类:内部 DSL(Internal DSL)、外部 DSL(External DSL)、以及语言工作台(Language Workbench)。 Internal DSL 是一种通用语言的特定用法。用内部 DSL 写成的脚本是一段合法的程序,但是它具有特定的风格,而且只用到了语言的一部分特性,用于处理整个系统一个小方面的问题。用这种 DSL 写出的程序有一种自定义语言的风格,与其所使用的宿主语言有所区别。例如我们的状态机就是 Internal DSL,它不支持脚本配置,使用的时候还是 Java 语言,但并不妨碍它也是 DSL。 builder.externalTransition() .from(States.STATE1) .to(States.STATE2) .on(Events.EVENT1) .when(checkCondition()) .perform(doAction()); External DSL 是一种“不同于应用系统主要使用语言”的语言。外部 DSL 通常采用自定义语法,不过选择其他语言的语法也很常见(XML 就是一个常见选 择)。比如像 Struts 和 Hibernate 这样的系统所使用的 XML 配置文件。 Workbench 是一个专用的 IDE,简单点说,工作台是 DSL 的产品化和可视化形态。 三个类别 DSL 从前往后是有一种递进关系,Internal DSL 最简单,实现成本也低,但是不支持“外部配置”。Workbench 不仅实现了配置化,还实现了可视化,但是实现成本也最高。他们的关系如下图所示: 不同 DSL 该如何选择 几种 DSL 类型各有各的使用场景,选择的时候,可以这样去做一个判断。 Internal DSL:假如你只是为了增加代码的可理解性,不需要做外部配置,我建议使用 Internal DSL,简单、方便、直观。 External DSL:如果你需要在 Runtime 的时候进行配置,或者配置完,不想重新部署代码,可以考虑这种方式。比如,你有一个规则引擎,希望增加一条规则的时候,不需要重复发布代码,那么可以考虑 External。 Workbench:配置也好,DSL Script 也好,这东西对用户不够友好。比如在淘宝,各种针对商品的活动和管控规则非常复杂,变化也快。我们需要一个给运营提供一个 workbench,让他们自己设置各种规则,并及时生效。这时的 workbench 将会非常有用。 总而言之,在合适的地方用合适的解决方案,不能一招鲜吃遍天。就像最臭名昭著的 DSL —— 流程引擎,就属于那种严重的被滥用和过渡设计的典型,是把简单的问题复杂化的典型。 最好不要无端增加复杂性。然而,想做简单也不是一件容易的事,特别是在大公司,我们不仅要写代码,还要能沉淀“NB 的技术”,最好是那种可以把老板说的一愣一愣的技术,就像尼古拉斯在《反脆弱》里面说的: 在现代生活中,简单的做法一直难以实现,因为它有违某些努力寻求复杂化以证明其工作合理性的人所秉持的精神。 Fluent Interfaces 在编写软件库的时候,我们有两种选择。一种是提供 Command-Query API,另一种是 Fluent Interfaces。比如 Mockito 的 API : when(mockedList.get(anyInt())).thenReturn("element") 就是一种典型连贯接口的用法。 连贯接口(fluent interfaces)是实现 Internal DSL 的重要方式,为什么这么说呢? 因为 Fluent 的这种连贯性带来的可读性和可理解的提升,其本质不仅仅是在提供 API,更是一种领域语言,是一种 Internal DSL。 比如 Mockito 的 API: when(mockedList.get(anyInt())).thenReturn("element") 就非常适合用 Fluent 的形式,实际上,它也是单元测试这个特定领域的 DSL。 如果把这个 Fluent 换成是 Command-Query API,将很难表达出测试框架的领域。 String element = mockedList.get(anyInt()); boolean isExpected = "element".equals(element); 这里需要注意的是,连贯接口不仅仅可以提供类似于 method chaining 和 builder 模式的方法级联调用,比如 OkHttpClient 中的 Builder: OkHttpClient.Builder builder=new OkHttpClient.Builder(); OkHttpClient okHttpClient=builder .readTimeout(5*1000, TimeUnit.SECONDS) .writeTimeout(5*1000, TimeUnit.SECONDS) .connectTimeout(5*1000, TimeUnit.SECONDS) 他更重要的作用是,限定方法调用的顺序。比如,在构建状态机的时候,我们只有在调用了 from 方法后,才能调用 to 方法,Builder 模式没有这个功能。 怎么做呢?我们可以使用 Builder 和 Fluent 接口结合起来的方式来实现,下面的状态机实现部分,我会进一步介绍。 状态机 好的,关于 DSL 的知识我就介绍这么多。接下来,让我们看看应该如何实现一个 Internal DSL 的状态机引擎。 状态机选型 我反对滥用流程引擎,但并不排斥状态机,主要有以下两个原因: 首先,状态机的实现可以非常的轻量,最简单的状态机用一个 Enum 就能实现,基本是零成本。 其次,使用状态机的 DSL 来表达状态的流转,语义会更加清晰,会增强代码的可读性和可维护性。 然而,我们的业务场景虽然也不是特别复杂,但还是超出了 Enum 仅支持线性状态流转的范畴。因此不得不先向外看看。 开源状态机太复杂 和流程引擎一样,开源的状态机引擎不可谓不多,我着重看了两个状态机引擎的实现,一个是 Spring Statemachine,一个是 Squirrel statemachine。这是目前在 github 上的 Top 2 的状态机实现,他们的优点是功能很完备,缺点也是功能很完备。 当然,这也不能怪开源软件的作者,你好不容易开源一个项目,至少要把 UML State Machine 上罗列的功能点都支持掉吧。 就我们的项目而言(其实大部分项目都是如此),我实在不需要那么多状态机的高级玩法:比如状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等。 开源状态机性能差 除此之外,还有一个我不能容忍的问题是,这些开源的状态机都是有状态的(Stateful)的,表面上来看,状态机理所当然是应该维持状态的。但是深入想一下,这种状态性并不是必须的,因为有状态,状态机的实例就不是线程安全的,而我们的应用服务器是分布式多线程的,所以在每一次状态机在接受请求的时候,都不得不重新 build 一个新的状态机实例。 以电商交易为例,用户下单后,我们调用状态机实例将状态改为“Order Placed”。当用户支付订单的时候,可能是另一个线程,也可能是另一台服务器,所以我们必须重新创建一个状态机实例。因为原来的 instance 不是线程安全的。 这种 new instance per request 的做法,耗电不说。倘若状态机的构建很复杂,QPS 又很高的话,肯定会遇到性能问题。 鉴于复杂性和性能(公司电费)的考虑,我们决定自己实现一个状态机引擎,设计的目标很明确,有两个要求: 简洁的仅支持状态流转的状态机,不需要支持嵌套、并行等高级玩法。 状态机本身需要是 Stateless(无状态)的,这样一个 Singleton Instance 就能服务所有的状态流转请求了。 状态机实现 状态机领域模型 鉴于我们的诉求是实现一个仅支持简单状态流转的状态机,该状态机的核心概念如下图所示,主要包括: State:状态 Event:事件,状态由事件触发,引起变化 Transition:流转,表示从一个状态到另一个状态 External Transition:外部流转,两个不同状态之间的流转 Internal Transition:内部流转,同一个状态之间的流转 Condition:条件,表示是否允许到达某个状态 Action:动作,到达某个状态之后,可以做什么 StateMachine:状态机 整个状态机的核心语义模型(Semantic Model)也很简单,就是如下图所示: Note:这里之所以叫 Semantic Model,用的是《DSL》书里的术语,你也可以理解为是状态机的领域模型。Martin 用 Semantic 这个词,是想说,外部的 DSL script 代表语法(Syntax),里面的 model 代表语义(Semantic),我觉得这个隐喻还是很恰当的。 OK,状态机语义模型的核心代码如下所示: //StateMachine public class StateMachineImpl<S,E,C> implements StateMachine<S, E, C> { private String machineId; private final Map<S, State<S,E,C>> stateMap; ... } //State public class StateImpl<S,E,C> implements State<S,E,C> { protected final S stateId; private Map<E, Transition<S, E,C>> transitions = new HashMap<>(); ... } //Transition public class TransitionImpl<S,E,C> implements Transition<S,E,C> { private State<S, E, C> source; private State<S, E, C> target; private E event; private Condition<C> condition; private Action<S,E,C> action; ... } 状态机的 Fluent API 实际上,我用来写 Builder 和 Fluent Interface 的代码甚至比核心代码还要多,比如我们的 TransitionBuilder 是这样写的 class TransitionBuilderImpl<S,E,C> implements ExternalTransitionBuilder<S,E,C>, InternalTransitionBuilder<S,E,C>, From<S,E,C>, On<S,E,C>, To<S,E,C> { ... @Override public From<S, E, C> from(S stateId) { source = StateHelper.getState(stateMap,stateId); return this; } @Override public To<S, E, C> to(S stateId) { target = StateHelper.getState(stateMap, stateId); return this; } ... } 通过这种 Fluent Interface 的方式,我们确保了 Fluent 调用的顺序,如下图所示,在 externalTransition 的后面你只能调用 from,在 from 的后面你只能调用 to,从而保证了状态机构建的语义正确性和连贯性。 状态机的无状态设计 至此,状态机的核心模型和 Fluent 接口我已经介绍完了。我们还需要解决一个性能问题,也就是我前面说的,要把状态机变成无状态的。 分析一下市面上的开源状态机引擎,不难发现,它们之所以有状态,主要是在状态机里面维护了两个状态:初始状态(initial state)和当前状态(current state),如果我们能把这两个实例变量去掉的话,就可以实现无状态,从而实现一个状态机只需要有一个 instance 就够了。 关键是这两个状态可以不要吗?当然可以,唯一的副作用是,我们没办法获取到状态机 instance 的 current state。然而,我也不需要知道,因为我们使用状态机,仅仅是接受一下 source state,check 一下 condition,execute 一下 action,然后返回 target state 而已。它只是实现了一个状态流转的 DSL 表达,仅此而已,全程操作完全可以是无状态的。 采用了无状态设计之后,我们就可以使用一个状态机 Instance 来响应所有的请求了,性能会大大的提升。 使用状态机 状态机的实现很简单,同样,他的使用也不难。如下面的代码所示,它展现了 cola 状态机支持的全部三种 transition 方式。 StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create(); //external transition builder.externalTransition() .from(States.STATE1) .to(States.STATE2) .on(Events.EVENT1) .when(checkCondition()) .perform(doAction()); //internal transition builder.internalTransition() .within(States.STATE2) .on(Events.INTERNAL_EVENT) .when(checkCondition()) .perform(doAction()); //external transitions builder.externalTransitions() .fromAmong(States.STATE1, States.STATE2, States.STATE3) .to(States.STATE4) .on(Events.EVENT4) .when(checkCondition()) .perform(doAction()); builder.build(machineId); 可以看到,这种 Internal DSL 的状态机显著的提升了代码的可读性和可理解性。特别是在相对复杂的业务状态流转中,比如下图就是我们用 cola-statemachine 生成的我们实际项目中的 plantUML 图。如果没有状态机的支持,像这样的业务代码将会很难看懂和维护。 这就是 DSL 的核心价值——更加清晰地表达系统中,某一部分的设计意图和业务语义。当然 External DSL 所带来的可配置性和灵活性也很有价值,只是 cola-statemachine 还没有支持,原因很简单,暂时用不上。
1. 背景 从本质上可以知道,语言就是一套逻辑符号,这意味着NLP处理的输入是高度抽象并且离散的符号,它跳过了感知的过程,而直接关注各种抽象概念,语义和逻辑推理。正是由于NLP涉及到高层语义、记忆、知识抽象以及逻辑推理等复杂的认知特性,导致基于数据驱动和统计学习的深度学习模型在NLP领域遇到了比较大的瓶颈。可以不夸张的说,NLP中复杂的认知特性已经完全超出了深度学习的能力边界。那如何打破这个魔咒,突破深度学习的能力边界,从而实现感知智能到认知智能的关键跨越呢? 这正是本文需要探索的原因所在,可能的一条出路就是通过对非结构化的数据(如业务的数据,产品的数据,行业领域的数据)进行整合和知识蒸馏,从而变成结构化的业务知识,结构化产品的知识,结构化的行业领域知识,在这些结构化的知识的基础上,再运用深度学习的模型进行推理,实现知识驱动,再进一步进阶到基于推理的驱动,这样就会形成结构化的知识推理引擎,从而提高整个智能系统的认知能力。那知识图谱就是将非结构的数据进行提炼归纳成结构化的知识的基础设施,图神经网络GNN就是在知识图谱基础设施上的推理模型。一句话概括就是:用不确定的眼光看待世界,再用确定的结构化知识来消除这种不确定性。 2.知识图谱 在介绍知识图谱之前,先得弄清楚什么是知识?知识是从大量有意义的数据中归纳总结出来的,是从有意义数据中压缩、提炼,从而形成有价值的规律。比如,天文学家日夜观察各种行星的位置,及对应的时间,这些都是观察的数据,但是牛顿从这些观察的数据中发现了万有引力定律,这就是知识。就像后来的天文学家运用牛顿的万有引力定律这个有价值的知识,发现了更多的未知星体和宇宙的奥秘,知识也将大大的加强智能系统的认知能力,也将使智能系统走向更深的未知领域。知识图谱就是对知识进行存储,表示,抽取,融合,推理的基础设施。 建设一个知识图谱系统,需要包括:知识建模、知识获取、知识融合、知识存储、知识模型挖掘和知识应用6大部分: 1、知识schema建模:构建多层级知识体系,将抽象的知识、属性、关联关系等信息,进行定义、组织、管理,转化成现实的知识库。 2、知识抽取:将不同来源、不同结构的数据转化成图谱数据,包括结构化数据、半结构化数据(解析)、知识标引、知识推理等,保障数据的有效性和完整性。 3、知识融合:由于知识图谱中的知识来源广泛,存在知识质量良莠不齐、来自不同数据源的知识重复、知识间的关联不够明确等问题,所以必须要进行知识的融合。知识融合是高层次的知识组织,使来自不同知识源的知识在同一框架规范下进行异构数据整合、消歧、加工、推理验证、更新等步骤,达到数据、信息、方法、经验以及人的思想的融合,形成高质量的知识库。 4、知识存储:根据业务特点以及知识规模选择合适的存储方式将融合后的知识进行持久化保存。 5、知识模型挖掘:知识的分布式表示学习,通过图挖掘相关算法进行知识推理出新知识,关联规则挖掘一些隐藏知识。 6、知识应用:为已构建知识图谱提供图谱检索、知识计算、图谱可视化等分析与应用能力。并提供各类知识运算的API,包含图谱基础应用类、图结构分析类、图谱语义应用类、自然语言处理类、图数据获取类、图谱统计类等等。 说这么多知识图谱的概念,可能这些概念有些抽象,这里给出一个实际的关务hscode领域的知识图谱的例子: 3.图神经网络GNN 知识图谱将按照欧式空间分布的的文本、图片、时间序列等数据进行归纳融合,提炼出了按照非欧空间的图结构来存储结构化知识。图结构的复杂性对传统的深度学习算法提出了重大挑战,主要是因为非欧空间的图结构数据是不规则的。每个图都有无固定数量的节点,同时图中的每个节点都有不同数量的邻居节点,这就导致传统深度学习的卷积操作不能在图结构上有效的计算。同时,传统深度学习算法的一个核心假设是样本实例彼此独立,如两张关于猫的图片是完全独立的。然而,对于图结构数据来说,情况并非如此,图中的节点通过边的连接信息,使节点之间有机的组合起来,从而天然构造了功能强大的结构性feature。另外,业界公认的传统的深度学习的一大软肋是无法有效的进行因果推理,只能进行某种意义上的统计相关性推理,这就大大降低了智能系统的认知能力。针对上述传统深度学习算法在图结构数据和因果推理上的天然软肋,业界最近兴起了针对图结构数据建模和因果推理的新方向-图神经网络GNN。 3.1 图卷积网络GCN基本原理 图卷积神经网络GCN是目前最重要的图神经网络,本文落地的图神经网络也是基于图卷积神经网络GCN。图卷积神经网络GCN本质上是基于Message-Passing的信息传递式的通用框架,是由多层的图卷积操作组成,每一个图卷积层仅处理一阶邻域信息,通过叠加若干图卷积层可以实现多阶邻域的信息传递。基于Message-Passing的图神经网络有以下三个基本公式构成: 对应的符号解释如下: 几乎所有的GNN模型的底层运行机制都是基于上述三个公式,只不过不同的AGGREGATE,COMBINE,READOUT的实现策略不同,导致演化成了GCN,GAT,GraphSAGE等不同类型的图神经网络。 3.2 图卷积网络GCN的AGGREGATE计算方式 图卷积网络GCN中的AGGREGATE是将GCN的每一层通过邻接矩阵A和特征向量 ,相乘得到每个顶点邻居特征的汇总,然后再乘上一个参数矩阵 , 加上激活函数σ,做一次非线性变换得到聚合邻接顶点特征的矩阵 。基本公式如下: 1. 是图卷积网络GCN中第l层的特征向量,其中 是输入特征。 2. 是图卷积网络GCN每一层的参数矩阵。 3. 是图Graph的邻接矩阵加上每个图节点的自旋单位矩阵。 4. 是图Graph邻接矩阵 的度矩阵。 上面是一般GCN的AGGREGATE策略,但是这样的AGGREGATE策略是的transductive learning的方式,需要把所有节点都参与训练才能得到图中节点的特征表示,无法快速得到新节点的特征表示。为了解决这个问题,参考文献[1]中的GraphSage利用采样(Sample)部分结点的方式进行学习,学习到K个聚合AGGREGATE函数,GraphSage的重点就放在了AGGREGATE函数的设计上。它可以是不带参数的, , 也可以是带参数的如等神经网络。下图是参考文献[1]GraphSage的AGGREGATE函数的学习过程: 3.3 图卷积网络GCN的COMBINE计算方式 图卷积网络GCN中的COMBINE的计算方式一般就是将第k层节点通过AGGREGATE学到的向量和第K-1层已经学习到的节点向量进行CONCAT,然后在CONCAT后的向量加上一层神经网络Dense层即可。GCN中的COMBINE采用concate就是将两个原始特征直接拼接,让网络去学习,在学习过程中确定最佳的方式去融合特征,这样保证特征在融合过程中信息不会损失。 3.4 图卷积网络GCN的READOUT计算方式 图读出操作(READOUT)就是用来生成整个图表示的,综合图中所有节点的特征向量最终抽象出整体图的特征表示。GCN的图读出操作目前有基于统计的方法与基于学习的方法两种。 基于统计的方法 统计的方法实现图读出操作一般常用的是sum,max,average来求整个图的抽象表示,这些统计的好处就是简单,不给整体的图神经网络模型带来额外的参数,但是sum,max,average带来的不利因素也是显然易见的,这些统计操作会对高维特征进行压缩,使每一维上数据的分布特性被完全抹除了,带来了信息的损失比较大。 基于学习的方法 基于统计的方法的不足是无法参数化同时带来信息损失,这样难以表示结点到图向量的这个“复杂”过程。基于学习的方法就是希望用神经网络来拟合这个过程。目前在参考文献[2]来自斯坦福等大学的研究者提出了DIFFPOOL,这是一个可以分层和端到端的方式应用于不同图神经网络的可微图池化模块,DIFFPOOL 可以有效的学习出整体图的层级表征。DIFFPOOL 可以以非均匀的方式将节点坍缩(collapse)成软簇,并且倾向于将密集连接的子图坍缩成簇。因为 GNN 可以有效地在密集的、类似团的子图(直径较小)上传递信息,因此在这样密集的子图上池化所有节点不太可能会损失结构信息。一句话概括,DIFFPOOL不希望图中各个结点一次性得到整体图的向量表示,而是希望通过一个逐渐压缩信息的过程,来得到最终图的表示,如参考文献[2]下图所示: 研究者通过可视化图神经网络不同层的簇分配研究了 DIFFPOOL 学习有意义节点簇的程度。参考文献[2]中的DIFFPOOL 的层级聚合分布的图展示了一个来自 COLLAB 数据集的图形在第一层和第二层节点分配的可视化图,图中节点颜色表示属于哪个聚合簇。 4.基于知识图谱的图神经网络在hscode产品归类中的落地 hscode是《商品名称及编码协调制度》的简称。编码协调制度由国际海关理事会制定,英文名称为The Harmonization System Code (HS-Code),是对各种不同产品出入境应征/应退关税税率进行量化管理的制度。hscode是各国海关、商品出入境管理机构确认商品类别、进行商品分类管理、审核关税标准、检验商品品质指标的科学系统的国际贸易商品分类体系。hscode总共22大类98章,前6位编码国际通用,后面的编码由各国/地区根据实际情况自行扩展,我国海关现行的是10位海关编码。hscode归类是一个非常特殊的NLP场景,在一般的NLP场景中如果根据文本语义匹配在top3能召回的话,那表明NLP的效果处理的还不错。但是由于hscode归类在通关过程中具有确定汇率,确定监管条件等等关键作用,导致了hscode需要更严的准确率,这也是和其他业务场景产品归类最大不同的地方,必须确保top1的准确性。举个具体的例子,大家可能更有体感一些。 通过上面的例子,我们发现从传统NLP文本语义相似度上来说,上面几条文本的语义匹配相似度非常高,但是hscode需要根据不同的申报要素(例如上图中的"是否液态要素","额定容量")的具体业务知识来进行精细化推理才能得出正确的hscode。 4.1 基于传统深度学习的NLP模型在hscode归类中的瓶颈 在hscode归类中,基于传统深度学习的NLP模型架构图如下: 传统深度学习的NLP模型主要包括以下几部分: 1、计算hscode质点向量:基于词向量平均池化的kmeans聚类算法计算hscode质点向量, hscode的向量表达能力和抗干扰能力。 2、hscode层级分类器:通过两层的层级分类器先进行粗排,选出候选的hscode。 3、细排阶段的语义推理:基于hscode产品归类领域特定的知识形态最终选择了基于BiLSTM + Attention的Encoder-Decoder语义推理模型。 基于传统深度学习的NLP模型已经在线上hscode归类场景中运行了,通过业务同学对线上真实客户原始提交的归类样本2223条进行评估,以下准确率为:算法预测HsCode top1的准确率,结果如下: 未修改:业务审核小二对客户原始输入信息没有做任何改动。 编码品名未修改:业务对品名未修改,但是修改了商品其他归类申报要素属性。 其他修改:其他修改的情况比较复杂,由于hscode归类本身是个很复杂专业的领域,客户原始提交的信息不能满足海关申报的规范,业务小二需要对客户原始提交的信息进行修改。 评估结果详细分析:由于基于传统深度学习NLP模型的训练数据都是来自业务小二审核后的规范样本上进行训练,这就可以看出在客户原始输入信息未修改那部分,传统深度学习NLP模型的准确率和算法测试集上的准确率差相差不多为89.3%,同时在品名未修改,但是其他归类要素修改了的情况,传统深度学习NLP模型的准确率在87.7%,证明了传统深度学习NLP模型有一定程度上的泛化性。但是在第三种其他修改情况,即客户原始提交信息不满足海关规范,传统的深度学习NLP模型的准确率只有17.3%。这样在总体样本上的准确率只有59.3%(计算公式为:0.31 X 89.3% + 0.28 X 87.7% + 0.41 X 17.3% = 59.3%)。 通过上面的分析我们发现,传统深度学习NLP模型在第三种其他修改情况(客户原始提交的信息不能满足海关申报的规范)下,准确率非常低,我们对第三种其他修改情况下的具体badcase进行了一定的拆解,主要原因如下: 1、申报要素不缺失,但是客户输入的具体申报要素值那些不规范,这种不规范如下:客户原始输入是行业术语"来令片"(其实是摩擦片),然而在nlp语料里没有这些行业领域知识,故传统深度NLP模型无法解决这些case。 2、申报要素不缺失,但是由于申报要素值需要进行一些逻辑计算,如 客户输入"含棉75%,羊毛10%,纤维15%",而当棉的成分<60%是hscode A,含棉成分>60%是hscode B,这种有逻辑计算的nlp问题,传统的深度NLP模型也无法解决这些case。 3、申报要素用户输入过多,带来太多的噪声。传统深度NLP模型不能有效抓取核心申报要素的结构性信息,容易被多余的噪声带偏。 4、hscode体系中有太多了其他,除非等托底编码,就是除了hscode A、hscode B、hscode C以外的情况都是hscode D,这种估计也需要一定的推理才能解决这个问题。 5、申报要素缺失,客户没有输入完整的hscode申报要素,从而导致不能正确归类。这个估计任何模型都无效。 上面的第1点和第2点是缺少结构化知识用于模型推理,第3,4点是不能有效的抓取结构化的特征进行因果逻辑推理,因此我们落地了基于知识图谱的GCN模型来尝试解决上述的问题。 4.2 hscode归类场景中知识图谱的知识schema建模 知识图谱的元数据schema信息定义非常重要。设计之初要既考虑本体之间的关系,还要考虑本体schema维度变化。hscode产品归类知识引擎的schema如下: 4.3 基于知识图谱的GCN模型的整体算法架构 1、在hscode领域,底层知识图谱的构建的实体是品名,具体申报要素值,申报要素对应行业领域的属性值,图谱中的边是不同申报要素的key,这样底层的图结构是个不同关系边组成的异构图。同时由于同一个商品,申报要素值的不同会导致对应不同的hscode(label),因此这里hscode(label)会标记在以品名,申报要素值构建的子图上,以这样的子图作为预测的实体节点。 2、在hscode领域,知识图谱的每个实体节点存储的是经过向量平均池化后的word2vec语义向量,这样也使这个图谱具有语义泛化性,这和以前每个知识图谱每个节点存储在以字面文本为主的实体有所不同。 3、在hscode领域,存储在知识图谱中的结构性知识需要经过图中节点文本语义特征,图中不同边的特征,图的结构特征等一起融合转化成GCN模型的embeding输入层。 4、在hscode领域,图神经网络的邻居节点的聚合机制如下: 具体的AGG策略如下: 4.5 落地效果 从4.1的详细分析可以看到基于传统深度学习的NLP模型在hscode线上的准确率为:准确率只有59.3%(计算公式为:0.31 X 89.3% + 0.28 X 87.7% + 0.41 X 17.3% = 59.3%)。基于知识图谱的GCN模型取了2017-01-01 到 2020-01-08之间的所有客户原始提交数据进行训练,另取了2020-01-09到2020-01-12的5687条线上真实的客户原始提交的数据作为测试数据,业务评估的结果是:基于知识图谱的GCN模型的准确率到达了76%,相比以前传统深度学习的NLP模型准确率提升了16.7% ,证明了基于知识图谱的GCN模型具有更好的容错性。 5.实验 目前设立了2种对比实验,对比的指标是在测试集上的准确率。1种是图神经网络GCN的图level READOUT策略sum和average的对比,2种是底层知识图谱的图结构做一些改变,一种是较简单的星形结构,另一种是复杂的图结构。 图神经网络GCN的图level READOUT策略sum和average的对比 在保持知识图谱的GCN模型其他参数不变的情况下,将GCN的图level READOUT策略由sum改成average,观察在测试样本上的准确率: 底层知识图谱的图结构做一些改变,一种是较简单的星形结构,另一种是复杂的图结构 在保持知识图谱的GCN模型的参数都相同的情况下,将底层的知识图谱的存储结构一种是简单的星形结构,即只有品名-申报要素发生边的关联,申报要素之间不产生边的关系,另一种知识图谱的存储结构是复杂的图结构,除了品名和申报要素产生边的关联,敏感申报要素之间也产生边的关系,属性值和申报要素之间同样产生边的关联。 通过实验可以发现,知识图谱的底层结构化知识越丰富,基于知识图谱的GCN模型的准确率有会有相应的提升。改变整个图神经网络模型的READOUT策略对模型的准确率提升也一定的帮助。接下来会进一步做更多的实验,来充分挖掘基于知识图谱的GCN模型在NLP中的潜力。 6.未来思考 1、在很多业务领域,人工梳理了大量的业务规则知识,这些异构的规则知识如何抽取融合进知识图谱,进一步提升知识图谱的结构化的推理能力。业务梳理的多阶逻辑规则,如何用图谱进行存储。业务梳理的人工规则类似于规则树组织形式,将与、或、非等原子逻辑命题有机的组织在一起,这里如何将业务梳理的多阶逻辑规则树,抽象出实体和关系,从而转化成图谱结构,也是个未来需要攻坚的难题。 2、规则如何与图神经网络进行有效的融合。比如hscode领域还沉淀了大量的人工规则,这些规则是宝贵的知识财富,这些规则如果作为teacher-network,去指导hscode归类任务这个student-network,将能大大的提升hscode领域的精确性。规则这个teacher-network相当于起到指导和约束作用,在规则teacher-network学习出的各个规则的约束子空间更利于语义推理。这里规则如何转化成teacher-network,进而与知识图谱的图神经网络结合也是一个重要的优化方向。 3、目前的知识图谱还主要是基于文本构建的,真正完善的知识图谱应该是个多模态的结构化知识,比如除了文本,还应该有图片,语音等多模态信息,只有多模态的结构化知识,才能进一步推动整个智能系统的认知能力。 参考文献 [1]Inductive Representation Learning on Large Graphs, https://arxiv.org/abs/1706.02216 [2]Hierarchical Graph Representation Learning with Differentiable Pooling, https://arxiv.org/abs/1806.08804 [3]https://www.cnblogs.com/SivilTaram/p/graph_neural_network_3.html [4]https://zhuanlan.zhihu.com/p/68064309 [5]https://zhuanlan.zhihu.com/p/37057052
前言 支付宝视觉售货柜项目是蚂蚁IOT的重要产品,用户通过人脸识别打开货柜门,挑选出想要购买的商品后关门,视觉识别算法通过对比开门前后的商品变化判断出用户购买了哪些商品,自动完成结算。“开门即取,关门即走”的体验给用户带来了极大方便。 图1 3D合成的百岁山矿泉水 图 2 支付宝视觉售货柜 在本场景中,由于货品的高密度摆放,视觉货柜所拍摄的图像中商品之间遮挡非常严重,算法需要根据非常有限的图像片段判断是哪个商品。同时算法需要不断迭代以支撑源源不断的上新需求。这就需要我们不仅要采集足够多的数据以解决各种情况,而且要能在很短的时间内及时输出新品的训练数据,否则算法模型的泛化能力将大打折扣。3D合成数据技术为该项目提升了3倍以上的上新速度,降低了70%以上的成本,大大缩短了商品上新周期。同时避免了人工打标带来的质量不稳定,保障了训练数据的高质量,将因人工标注数据不可靠造成的风险降低了90%以上。图3是这个方案的流程图。Part1对商品建模,并赋予精确的纹理和材质,Part2对场景进行参数化建模仿真模拟各种各样可能出现的情况,Part3对场景的每个情况进行渲染获取最终训练数据。 图3 3D数据合成流程 Part 1 全材质3D重建 3D重建是利用技术手段对某个物体进行自动几何重建,以及纹理与材质的建模。这个过程有别于3D建模师手K的过程,可快速准确的恢复某个物体的真实几何和外观信息。3D重建需要重建的信息包括几何和外观两个部分。当前3D重建难以解决的物体是一些反光、透明等材质,尤其是各种材质杂糅在一起的物体。这个难题横旦在项目的初期,是无论后面走哪条技术路线都需要攻克的难题。 项目组经过艰苦技术攻关自研了一套全材质3D建模方案,该方案结合了结构光扫描技术与基于图像特征匹配的多目几何重建技术,通过扫描和3D特征匹配的方法实现了全材质物体的3D重建,攻克了业界难题。使用全材质3D重建技术方案可在5-10分钟左右的时间精确重建一个商品的完整几何信息以及初步的外观信息。下面是若干个3D重建示例。 图4重建的3D模型 在获得3D几何信息和初步外观信息之后,可根据实际商品的外观对3D模型不同部位赋予准确材质模型,这个过程称之为材质重建。一般来讲特定应用场景的商品材质种类是相对有限的,可根据不同业务场景建立一个特定材质库,根据3D模型的初步外观信息赋予相应的材质。实际上商品的外观与材质之间的专家经验是可以通过网络学习到的,一些研究工作如:开放环境材质估计、 形状与SV-BRDF估计 表明即便是在商品3D模型未知、采集环境开放的时候,我们仍然可以学习到材质模型与图像特征的对应关系。 Part2 参数化场景 我们通过全材质3D重建技术对场景进行建模,之后需要针对场景分布的各种可能性进行基于物理的模拟。在参数化场景部分,我们也需要对场景进行3D建模。场景的建模是对所渲染3D模型所处的环境进行3D建模,包括了场景3D重建和光源建模两个部分。场景3D重建的过程可以是自动化的使用如扫描仪,或者根据多目几何原理使用Structure-from-Motion进行三维重建。而光源重建则是对环境的光源进行建模,使得渲染出来的图像与实际拍摄的图像在外观上融合度较好。 光照估计 在渲染流程中,光照对渲染结果的影响至关重要,因此场景参数化需要对光照进行精确的描述。通常来讲,一个场景中的光源往往构成复杂,需要对直接光源的数量、色温、光源形状、乃至频谱范围等进行准确建模,如果场景中有类似液晶显示屏等光源,还需要针对光源的偏振态和频率进行建模,工作量很大且很难自动化。 图5 HDR合成与渲染结果 这里我们采用了HDRI技术对光源进行重建,该方案是一个简单有效的光源重建和渲染技术,被广泛采用于电影制作中实现与真实场景融合度很高的渲染图像。该技术是一种基于图像的渲染技术,即采集并合成一张高动态范围图像作为光源进行渲染。可以看到这样的光源渲染出来的结果在高光表现方面较好。 场景建模和物理碰撞检测引擎 在视觉货柜项目中,我们所面料的场景是一个采用视觉识别技术完成商品交易的无人货柜。货柜需要频繁上新品,且商品之间遮挡严重。商家为了更有效的利用货柜会密集摆放很多商品,很多商品漏出来的画面非常有限,而视觉识别需要检测并识别出所有目标。这就要求视觉算法同学除了想法设法提高模型泛化能力之外,也需要准备充分多样性的数据,尽可能全的覆盖到各种遮挡关系,同时需要覆盖到每个可能出现的商品。 在参数化场景的过程中,我们使用重力模型、随机力模型等对场景施加变化,并对场景中的各个物体进行碰撞检测和模拟,使得场景中的物体分布接近真实状态。下面这个视频示意如何对倒瓶等异常情况进行仿真模拟。 图6物理碰撞模拟 Part3 照片级渲染 3D合成数据方案的核心问题是怎样使得渲染出来的图像看起来像照片,而不是人眼看上去很真实就够了。我们需要渲染域与实拍域尽量接近才能真正起到训练数据的作用。一般意义上的渲染场景存在所谓too perfect的问题,也就是说渲染出来的图像看上去可能已经非常真实,与人眼实际看到的样子很接近,但却与摄像头实际拍摄的图片不同。作为喂给机器学习模型的训练数据,我们要求最终输出的图片需要复现这些瑕疵,实现所谓的照片级渲染(Photo-realistic rendering)。 我们尝试了两种思路实现照片级渲染。一种思路是数据驱动的方法,先采集大量实拍图,之后通过GAN、域迁移、域自适应等方法将渲染域的图像迁移至实拍域。另一种思路是成像模拟的方法,在渲染流程前中后期分别模拟各种摄像头成像的影响,比如渲染过程中根据场景深度不同模拟散焦模糊,对渲染图像卷积同一模糊算子实现因低分辨率引起的镜头模糊等。 图7渲染图、域迁移图与实拍图 图7为采用第一种思路实现的效果。将渲染图、迁移图和实拍图的对比,我们看到迁移图可以较好的实现与镜头相关的图像特征迁移效果,同时也会存在一些artifacts。此外,作为数据驱动的技术,域迁移的过程可控性较弱,获得好结果的前提是需要有与真实场景分布接近的实拍数据,导致数据采集成本较高。 不同于上面的数据驱动算法,成像模拟采用纯模拟的方式合成训练数据,可控性强,且效果无天花板,但实现的技术较为复杂。我们采用电影级渲染引擎,并自研了光学摄像头模拟器,实现了一系列因镜头、光电传感器、以及ISP图像处理单元的模拟,消除了许多引起渲染域与实拍域差距的因素。下图为成像模拟实现的效果。 图8成像模拟结果 写在最后 在实践中,我们发现3D合成数据可以很好的解决许多计算机视觉任务,尤其是在一些无法很好获取ground truth的任务中具有非常好的落地前景。毕竟人工智能的目的是代替重复低效的人工,而如果用于训练的数据收集和标注仍然大量依赖人工的话,有时就不免落入到所谓“有多少人工就有多少智能“的尴尬境地。 同时我们也必须看到目前的3D合成数据方案有诸多挑战。首先,不能完全依赖合成数据,总会有一些模拟不到的场景。其次,合成数据方案比较适合标注成本高的任务,对于一些标注成本不高的任务反而会增加成本,比如人脸检测、物体识别分类等任务。再次,一些技术难点,如低成本实现动态场景模拟等尚需进一步攻克。
“云办公”让企业向移动化转型升级迎来一场实战考验,对于多数传统企业而言,需求沟通、研发效率、测试保障、发布质量、运维稳定、运营分析等各个环节都充满了挑战。阿里的同学亮出“云办公”高效率、协同化、流程化的“杀手锏”,利用移动研发平台EMAS助力远程研发协同,仅用5天时间便完成手机淘宝“三八国际女王节”新版本全链路发布。“居家办公”也能如此高效?经过复盘与梳理,本文将深度揭秘手机淘宝新版本开发流程,探索阿里工程师在这5天“云办公”中的速度与激情。 2月25日:远程研发,只需1天 许多业内小伙伴开启远程研发办公后惊呼:一线上,全乱了。而阿里工程师仅用1天的实践就证明了移动研发平台EMAS的强大功能和硬核技术。 视频晨会,产品经理“淘小二”完成需求部署,客户端开发“叮叮”同学便迅速开启了手机淘宝“三八国际女王节”版本视频直播模块功能的开发。在移动研发平台EMAS上新建项目、添加模块、输入代码、构建手机淘宝客户端,最后扫码安装、自测验证,整个流程规范而高效。与此同时,系统配置的自动化测试流水线也开始默默运行起来。 2.2月25日.png 移动研发平台EMAS为远程研发提供全方位的功能支撑和技术优势。研发环境方面,平台提供架构治理方案、规范交付产物、标准化基础配置信息;研发管理方面,平台支持Android和iOS客户端构建以及模块构建;平台侧管理工程方面,提供打包签名、安全加固、构建加速、持续集成等一体化功能。研发协作方面,研发同学只需要在平台上简单操作几个按钮就可以独立输出自己的开发模块,相互协作又互不干扰。1天时间,开发产出,研发同学竟然“提前下班”了,移动研发平台EMAS可谓功不可没。 2月26日:自动测试,安全覆盖 研发与测试是软件新版本开发的两大核心命脉。研发同学保障新模块、新功能的及时发布,而测试环节则如同野马的缰绳,全面保障版本质量。 依托于移动研发平台EMAS的硬核技术,手机淘宝的研发流程中配置了自动化真机适配测试和静态代码扫描,构建成功后,流水线自动触发真机测试任务,并对构建产物进行编码规范的自动化检测。测试阶段提供Crash数据、智能Monkey服务、结合流程的卡口配置等功能,适配通过率超过95%。 版本测试,自动化和全覆盖才是王道。通过平台中真机适配、性能测试等自动化工具,迅速发现APP中的各类隐患,包括APP崩溃、各类兼容性问题、功能性问题、性能问题等。自动化测试覆盖发现问题、定位问题、解决问题等全链路,让阿里工程师将速度与安全集于一身。 2月27日:灰度发布,灵活可控 经过两天的“奋战”,阿里的同学已经顺利完成远程研发与自动化测试,接下来是最为关键的发布计划。 手机淘宝“三八国际女王节”新版本按照灰度策略推送到用户手中,移动研发平台EMAS提供的客户端灰度发布功能,采用多维度灰度策略,实现发布过程直观可控,再次为新版本的安全稳定保驾护航。 客户端灰度发布,针对品牌、机型、操作系统、灰度人数等维度选取部分用户进行多批次缓慢放量,在此过程中进行稳定性数据监控、实时收集用户体验反馈等操作,进而决定是否继续放大新版本投放范围。经过多轮灰度后,版本的质量、稳定性、用户体验都得到了充分的验证,便一鼓作气升级全量发布。 客户端灰度发布过程 2月28日:轻松运维,快速响应 随着版本逐步放量,越来越多的用户更新了版本,运维同学神经紧绷,时刻关注新版本线上稳定性和用户舆情反馈情况,准备迎接任何突发状况。 监控版本稳定性、Crash率、操作响应时长、用户的使用反馈,每一部分都与版本上线后的使用情况密切相关,复杂的分析对比,精密的实时监控,听起来并不容易,但实现起来只需要一位“叮叮”一台电脑,一束阳光和一个系统,如果有杯咖啡就更好了~ 都说,没有什么云淡风轻,只因有人替你负重前行。分担大量数据处理工作的实力派伙伴就是它,移动研发平台EMAS。 基于“EMAS端到端解决方案DP2”的秒级数据处理能力,高可用监控实现实时智能告警,从端上数据采集到产出报表,打通整条数据链路,让移动端高可用数据触手可得。崩溃分析提供双端Crash监控、聚合查看、Crash原因定位、告警配置等服务。 版本正式发布2小时后,“叮叮”关注到某条Crash 的占比超过了50%,一度引起告警,于是他跟其他几位研发同学一起通过数据跟踪和分析,迅速排查、定位问题。通过热修复发布补丁,在用户无感知的情况下迅速修复问题。从Crash数据出现异常升高到恢复正常,前后也不过4个小时。 趁着阳光正好,我们喝杯咖啡吧! 3月2日:精准运营,智慧数据 项目进行第5天,步入运营阶段,“三八节”活动的准备紧锣密鼓的展开,突破了研发、测试、发布阶段的多重验证后,即将迎来压抑已久的“春宅”购物欲,如何分析用户行为?如何进行用户分层?如何评估运营策略是否准确? 运营同学此时正襟危坐,静待花开,所有的疑问都需要数据的支撑和论证。 移动研发平台EMAS中的移动数据模型轻松反馈了运营同学每一步实施方案的价值,结合数据分析,用户行为轨迹、成单率、复购率一目了然,而用户漏斗模型也展示了产品路径上的优化项,产品经理开始思考下一个版本的优化方案,怎样让用户更快地找到更心仪的宝贝,体验最优质的线上购物体验和服务。 新版本的优势劣势,运营策略的正确与否,新老用户的喜好变迁,一切都在大数据的眼中,包括每位小伙伴的汗水和努力。 伴随着移动研发平台EMAS,阿里工程师们在短短一周时间且足不出户的情况下就实现了一个客户端新版本的研发、测试、灰度、发布、监控、运维、运营等一系列工作,这就是阿里系高质高效的产品生命周期。 先别急着惊叹,在阿里工程师眼中,对这样的项目速度早已司空见惯。 有这样一套流程体系、工具平台、项目机制等“组合相助”,阿里集团几乎所有的开发任务都顺利完成,项目计划有条不紊地展开。移动研发平台EMAS聚焦移动研发领域,实践了Mobile DevOps的文化、流程、理念、工具,在阿里集团内部广泛使用,包括手机淘宝、天猫、钉钉、优酷、盒马、飞猪、菜鸟、天猫精灵等上百个客户端的日常研发、发布版本等工作。 一个微小的变动就能轻松上热搜的项目在阿里也许从立项到上线仅仅是你写一份方案的时间,不仅是敏捷开发,更是智慧开发。移动研发平台EMAS就是阿里高效的秘籍,为阿里工程师们的智慧赋予了价值和意义。 阿里期望把近十年在移动互联网行业沉淀的DevOps研发支撑能力、移动App基础中间件能力开放共用,通过自动化流程让需求、开发、构建、测试、发布、监控等一体化业务价值交付链路更好更快更稳定,帮助传统企业快速完成业务移动化的转型升级目标。 让我们用新的武器来打破远程低效的焦虑,试着效仿阿里工程师的方式,将你的团队赋能,将你的产品赋予生机吧。
一、整体策略思考 1. 理解问题 首先,面对问题需要理解这个问题的场景,搞清问题背后的原因到底是什么,这对于解决问题来说是关键前提条件。那么就需要对问题相关的干系人进行沟通,换位思考,以寻求最优解决方案。 2. 明确目的 遇到问题不可怕,可怕的是并不知道这个问题到底带来什么影响,只是主观上认为这是问题,那就无从评估其严重性,结果就是很大可能花了大精力只解决了一个很小的问题,没有带来实际的价值。所以,也要在解决问题之前,思考清楚目的是什么,为什么要这么做,成本和收益关系如何,再做下一步执行决策。 3. 沟通引导 出现问题往往是因为双方立场不同、目标不一致而导致的,所以想要更好地解决问题,就要从目标一致的视角出发去与当事人沟通,在相互尊重、尽可能说清双方的困难并引导相互理解的基础上,使双方能够达成共赢的目标,再去交流解决方案直到达成协议会更加高效。 4. 赋能组织 将遇到过的问题总结经验,举一反三,从点到面去形成可复制的方法论,在团队内外进行分享与交流,帮助更多的团队解决实际问题。 二、问题解法及实施 上面讲的是一些面对问题时思考的通用方法,在理清思路后,下面就来看看那几个常见问题的解法吧! 1. 明确业务规划 当团队反馈不太了解业务规划时,这是一个需要特别警示的信息,因为当一个团队没有明确方向的时候,后续的执行也会出现很大偏差,导致组织的低效。其背后的原因可能是管理层信息下达不够,也可能是团队之间的目标并没有横向拉通只局限于关注自己这部分事情而导致。 解决方案可以是通过定期召开业务规划会,聚焦季度或半年内核心事件、目标及资源分配原则,各团队TL在会上充分达成一致,会后坚决执行,将策略拆解成产品需求迭代进行规范化管理。建议可与BI团队协同,在团队定期会议上反馈业务目标进展及近期上线的核心产品功能数据效果,做到有规划、有反馈,这是建立团队间信任的一个非常好的方式。此外,也可以通过OKR目标管理的方式,使上下游协同的团队在目标层面进行横向拉通,彼此在共同的目标下制定策略方案,定期回顾或调整目标以确保团队整体方向在主线上正常运行。 2. 客户交付周期提效 客户交付周期是指产品需求从评审到发布的周期时间。一般提出“客户交付周期比较长”的问题时,可能并不是周期长短的问题,而是在整个产品迭代生命周期过程中协同不顺畅的问题导致了体感上觉得周期长,所以也需要具体问题具体分析。 常见的解决方案是通过梳理产品需求评审到发布的关键里程碑,定义里程碑的标准,制定版本迭代的计划与节奏,界定职责分工,让各团队养成相对稳定的节奏,步调一致,以此来帮助上下游更高效地协同。产品迭代生命周期关键里程碑通常为评审、设计、开发、测试、集成(代码冻结封版)、发布,可根据团队现状和具体问题进行更加细化的标准制定。 需要注意的是,当一个产品迭代的里程碑规划好后,同时也要关注前后迭代的上下并行情况,不合理的并行会导致更加低效的交付,可以参考以下两个关键逻辑来减轻并行对团队的影响: 1)当前迭代启动开发时,下一个迭代可启动评审(这里的评审指的是只需TL层参加的初次评审会,明确需求价值及技术可行性,不需一线同学参加。经过初次评审会的需求则进入设计阶段,在召开细节评审会后确定需求排期)。 2)当前迭代完成集成(代码冻结封板)时,下一个迭代可启动开发。 迭代并行里程碑规划示意图 所以,当多个产品迭代里程碑规划好后,各团队会明确自己在不同阶段要做的事情,养成迭代节奏感,再通过制定协同机制,监督执行过程,记录关键效能数据及分析问题的手段来不断提升客户交付周期效率。 3. 维护协同秩序 针对前面讲到关键里程碑的规划,那么问题来了,里程碑规划后,如何使团队很好地执行而不是多次被延期呢?举个例子,大家都知道高铁的乘车规则,提前10-15分钟集中进站检票,按时发车,迟到的人只能承担退改签的成本。同理,为了能够保障关键里程碑按时完成交付,除了制定规则机制外,还需要借助平台去进行系统化管理,以确保机制是可执行、可落地的。 常见的工具有: 1)产品需求管理平台 主要用于需求管理及缺陷管理,可通过冻结需求池来管理变更,也可配合看板建立开发任务跟进研发测试过程,并自动生成数据报表。 2)研发集成与发布平台 主要用于APP开发项目管理、版本集成管理及发布管理,通过平台卡口管理质量及过程效率,对于质量不达标或晚于集成冻结时间的需求,触发上级审批流程加以管控,同时度量大盘可生成版本维度的数据报告。 三、效果提升 通过机制和平台组合拳,统一团队阶段性目标,以上在团队试行二至三个产品迭代后,上述问题得到了很大的改善效果,以近期赋能的大麦网APP客户端为例,经过改进,可在产品规划的方向上明确每个版本迭代的需求范围,APP版本固定在每三周发布一次,同时也改善了集成质量,模块一次集成成功率从50%提升至80%,团队整体对问题影响面的评估意识有所提升,不再一有问题就阻碍发布,连续多个版本紧急集成次数为零,灰度发布周期也从3天下降到0.5天。 四、关键沉淀 最后,简单整理了产品迭代生命周期的关键里程碑目的及主要标准,细节之处可以根据不同的组织进行个性化定制,关键在于从问题出发,通过有数据支撑的不断过程改进,使团队之间更加信任彼此,高效协同,切实帮助组织提效。 产品迭代生命周期关键里程碑目标及标准: 启动 需求规划 目的 明确业务近期重点、核心目标及资源分配原则 工具 产品规划会议(各团队TL层参加) 准入 产品规划文档 准出 结论明确的会议纪要 产品内审 目的 明确单个产品迭代内的需求及优先级 工具 产品需求管理平台 准入 需求与规划方向一致,目标或价值明确 准出 由产品负责人负责审核及输出需求优先级 计划 需求初评 目的 对需求价值各团队TL达成一致,初步评估技术可行性 工具 初评会议(各团队TL参加) 准入 需求目标或价值明确,具备Demo或线框图 准出 价值及可行性达成一致的会议纪要 交互设计 目的 按需求初评范围完成交互及视觉设计 可行性 需求细评 目的 对需求实现各团队达成一致 工具 细评会议(相关团队一线同学参加) 准入 需求交互及视觉稿,完整的PRD文档 准出 通过细评的需求列表及完善的会议纪要 确定排期 目的 细评后由技术TL在1-2个工作日反馈确认排期 执行 需求研发 目的 按排期完成指定需求的研发,确保自测质量,按时提测 需求测试 目的 按排期完成指定需求的测试,确保需求质量 集成(代码)冻结 目的 对代码变更提交集成限期完成,以保证按时发布 灰度发布 目的 用于少量用户更新升级,收集问题后快速修复 正式发布 目的 将通过灰度发布验证的安装包,交付至全量用户进行更新升级 监控 目标进展 目的 针对每个需求进行上线后的目标追踪及反馈,引导业务调整策略 关键里程碑 目的 对关键里程碑进展及效果进行过程监控,确保按时、按质交付 过程质量及效率 目的 对过程质量及效率监控,记录问题,指引改进 收尾 度量报告 目的 用数据反馈结果目标趋势及过程问题,分析原因,指引改进 复盘改进 目的 聚焦问题,改进过程 工具 复盘会议(各团队TL参加) 准入 阶段性度量报告 准出 改进措施、期限及责任人
前言 与传统工业优化不同,多智能体系统中每个机器人互相替代性很强,流程是非线性的,导致系统效率很难直接建模。一般通过调整任务分配与移动路径,优化总任务距离来间接逼近系统效率。但我们在实践中发现,任务距离与系统效率并不强相关。由于成本的限制,机器人数量往往是有限的,当针对任务距离进行优化时,会导致部分作业人员过于繁忙,而部分作业人员无事可做的问题。 因此我们结合了菜鸟柔性自动化实验室在多智能体系统的实践与反思,于论文《Idle Time Optimization for Target Assignment and Path Finding in Sortation Centers》中提出了基于工作站空闲时间的优化模型,关注如何最大化人的能力,从而推动整个系统达到更高的效率。我们对工作站的工作时间进行了离散化切分,模拟了机器人排队与等待的情况,并通过一套统一的网络流模型获得机器人与工作站的分配策略,以及机器人集群的路径规划,提升了系统产能。 论文地址:https://aaai.org/Papers/AAAI/2020GB/AAAI-KouN.3001.pdf 应用场景 基于多智能体集群的自动化技术方案的兴起和发展,促进了现代物流业的发展和全球化,代表着物流与供应链行业未来的一个主要方向。在阿里巴巴旗下菜鸟网络以及其合作伙伴的仓储和分拨中心有着成百上千的机器人在工作,实现包裹高效安全的到达用户手上。 图1. 机器人集群在分拨中心进行包裹分拨 图1是一个机器人分拨中心,几百个机器人在快速的把大量的包裹根据城市分拨,帮助干线物流网络更高效的运输包裹。机器人分拨中心的核心是三部分:工作站(Station),机器人(Agent)以及道口(Sorting Bin)。机器人会自动行驶到工作站领取包裹,通过自动扫码,然后再将包裹运输到对应的目的地道口,此时机器人会将包裹投进道口,从而完成包裹分拨。如何让这几百个机器人高效的运转,使得包裹可以更加快速的到达用户手中。这里要值得思考的是,一般性的会认为让这些机器人总的行驶路线最短就会使得整个分拨中心的效率最高。然而并不是这样,我们会看输入和输出,输入是所有的包裹,输出是各个道口中的包裹。受限于大量的包裹以及有限的机器人,仅仅是去优化路线最短并不能最大产出,这样就会存在部分工作站机器人排队而另一些工作站缺乏机器人的情况,在输入部分就已经限制了整个系统的产能。所以我们的目标是最小化所有工作站的空闲时间,来达到最大化系统产能的目的。下面会介绍如何建模来解决最小化空闲时间的问题。 问题建模 我们将上图机器人分拨中心模式进行抽象成如下图2所示,这样可以方便的引入多智能体路径规划的研究,其中核心三要素分别是橙色的工作站,绿色的机器人以及蓝色的道口。 图2.分拨过程,橙色节点为工作站,绿色节点是机器人,蓝色节点为分拨道口(每个道口对应了一个目的地结合) 要完成最小化工作站空闲时间,其核心是解决两个问题: 每个机器人去哪个工作站接包裹,即任务分配(Task Assignment)问题; 接完包裹后每个机器人按照什么路线运行到目的地道口,每个机器人可以视为一个智能体,即多智能体路径规划(Multi-Agent Path Finding, MAPF)问题。 这两个问题合在一起在学术上定义为TAPF问题。解决单次的任务分配和路径规划问题,我们定义为一个单次的TAPF问题。那么顺理成章的对于上述的自动化分拨中心持续作业的场景,可以抽象成Lifelong TAPF问题。接下来我们给出TAPF的定义。在给定的如下3个条件: 一个全连接的无向图G(V,E), N个Station:, M个Agent:, TAPF会找到一个分配方案,这个分配方案表示即为每个Agent去哪个Station,同时会为所有的Agents找到没有冲突的路径使得可以更快的到达各自的工作站。 当每个Agent到达其目的地Station点后,Station将需要T的时间将包裹处理到Agent上的时间。因此如果给定一个时间窗口[0, KT),那么我们可以设定每个工作站的操作时间为K个工作时间片: [0,T), [T,2T),..., [(K-1)T,KT),且每个Station仅允许Agent的到达时间为kT,其中k=0,1,...,K-1。基于以上,我们认为当一个Agent在kT时刻到达其目的地工作站时,则这个工作站在时间段[kT,(k+1)T)内将会被占用。 对于Life-Long TPAF问题,那就不是仅仅计算一次任务分配和多智能体路径规划问题。其本质就是不断的计算并更新每个Agent的分配方案和路径,这样对于上述场景中即是,每个机器人在运行过程中都在调整其目的地工作站和运行的路线,最终达到最小化工作站空闲时间最大化分拨中心产能的目的。 目标函数:基于以上定义,我们可以定义:在一个给定时间段内,最小化总的空闲时间,即为在这段时间内所有工作站的空闲时间之和。 示例说明:在后面的章节中,我们将用如下示例来详细解释每一种模型。 图3. 问题示例 两个Agent: 于时刻0从A出发, 于时刻1于C出发; 两个Station: 位于E点,其位置用 表示, 位于F点,其位置用 表示; 那么假设给定时间范围是[0,6),工作站的处理时间T=2,我们可以看到一个最优的TAPF的解决方案是给 分配工作站 ,且其路线为; 分配到工作站 ,且其路线为,null表示0时刻 不在地图中。这样两个工作站的工作时间段均为[4,6),得到的目标函数即总的空闲时间为8。 ITO-空闲时间优化 图4. ITO模型,边上标记(cost, capacity),为简洁起见(cost=0,capacity=1)的边没有标记 为优化空闲时间,如图4所示,我们建立了ITO(Idle Time Optimization)网络流模型。每一条边有两个属性(cost, capacity),cost代表了每单位流量经过这条边需要付出的代价,capacity代表这条边能承载多少单位的流量。为简洁起见,在图中我们省略(cost=0,capacity=1)的边。 我们对每一个 建立了一个对应的蓝色节点,对每个 建立一个矩形Station子结构。Station子结构根据时间轴展开成K个离散的时间段,每个时间段[kT,(k+1)T)用节点 表示,这样可以方便的考虑每个时间段的工作情况。Agent与Station子结构之间是一个全连通的二分图,表示每个Agent都能被指派到任意一个Station并占用一个对应的工作时间段。 图5. Agent节点与Station子结构的链接 图5解释了Agent节点与Station子结构的链接细节。对于每一组,我们可以估算 到达 的时间,如果这个时间段是[kT,(k+1)T),那么 可以在 时间段开始排队,并填补之后任意一个时间段的空缺,排队的特性我们通过 与 之间的链接实现。 最后,为使得整个系统的空闲时间最少,我们希望找到一个工作站指派使得工作站时间段尽可能被占用。因此我们以Agent节点为流的入口,每个Agent分配一个单位流量,以工作站时间段为出口,每个工作时间段最多流出一个单位流量。这样每个时间段只能被一个Agent独占,每个Agent也只能占用一个时间段。这个网络流模型的最大流解即是使整个系统空闲时间最少的Agent-Station分配。当我们得出分配方案后,再通过MAPF算法求得无冲突的Agent路径,就可以按照该路径来控制调度整个多智能体集群。 图6. 示例的ITO模型 图6是前文示例对应的ITO模型,两个Agent的预测到达时间都是在第3个时间段,粗边是最大流的解,对应匹配为与。 PITO-结合路径规划的空闲时间优化 由于ITO将Station分配与路径规划分开考虑,其效果高度依赖于基于到达时间预测的精确程度。为了避免这个依赖,我们基于ITO设计了PITO(Path Finding with ITO),它将ITO与匿名MAPF网络流模型(Anonymous MAPF flow network, Yu and LaValle 2013)相结合,通过一个统一个网络流模型,同时计算得出Station分配与Agent路径。 图7. PITO模型 PITO的构造过程如图7所示由两部分构成。左侧MAPF网络用于计算生成路径信息,右侧ITO网络用于生成Station分配结果。 对于任何一个地图中的点 ,我们根据时间轴将其扩展到每一时刻t,t时刻的u用一个紫色节点 表示。对于每一个 ,我们创建一个紧随其后的绿色辅助节点 。由于 与 之间只有一条capacity为1的边,任何时刻t最多只会有一个单位的流通过u,从而避免了多个Agent同时到达u而相撞。我们在这里并没有在网络结构中设计避免在边上相撞,而是采用了一个小技巧,如果有两个Agent在一条边上相撞,则令他们在当前点等待,并交换两者的Station分配与后面的路径。由于Agent是匿名的,交换Station分配与后面的路径并不会影响空闲时间,从而达到了简化网络、加速求解的目的。 ITO网络和前一章的构造方式基本相同,我们不再将Agent与Station子结构直接相连,而是采用让Agent通过MAPF直接“走到”Station的方式。每个Station 有其真实位置 。对于每个工作时间段[kT,(k+1)T),我们从辅助节点 连接一条边到对应的工作站时间段 ,从而允许Agent在到达 时可以占用其对应工作时间段。最后我们根据Agent的起始时间与起始位置,将它们连接到对应的节点上。 图8. 示例的PITO模型 图8是前文示例对应的PITO模型,蓝色粗边相连的节点展示了Agent的对应路径以及Station的分配结果。 Lifelong优化 这一章我们讨论如何将ITO与PITO应用到Lifelong的优化中。前面我们讨论了如何利用ITO与PITO求解One-Shot问题的Station分配与路径规划,每个Agent只需要去Station一次。但在现实场景中我们更关心的是一个动态的过程,Agent不断往返工作站与倾倒口。因此在每经过 的一个时间窗口,我们会重新根据场上情况重算为Agent分配Station并规划路径。 但为了让上个时间窗口的结果能够更好的为下一个时间窗口留出优化空间,Agent最好能占用更早工作时间段。我们通过增加惩罚节点P来达到这个目的。如图4和8,我们在ITO与PITO中增加了一个红色惩罚节点P,将它们转化为一个最小费用最大流的问题。P拥有足够大的流量并且跟所有的时间段相连但cost不为0,如果一个工作时间段没有Agent能够占据,就会产生一个从P到该时间段的等同于cost的惩罚。为了让Agent尽可能占据前面的时间段,我们用一个随时间单调递减的函数 来表示P到 的cost,比如采用线性递减或者指数递减函数。从而当空闲时间相等时,解会倾向于将空闲时间放在后面。 我们将Lifelong版的ITO与PITO(带时间窗口W与惩罚节点P)称为ITO-L与PITO-L。 实验分析 基于以上提出的的ITO-L和PITO-L算法以及我们给出3种对比算法,共5种算法框架进行Life-Long TAPF的实验对比。5种算法框架分别如下: 1)H(Inf)-L 采用Hungarian算法将所有agents按照总距离最近的方式统一分配到工作站,解决Task Assignment问题,然后采用改进的PBS算法求解MAPF问题;随着系统的运行不断重复实时的计算直至时间窗口结束。 2)H(1)-L 采用Hungarian算法重复计算[M/N]次将所有agents分配到工作站,解决Task Assignment问题;然后采用改进的PBS算法求解MAPF问题,随着系统的运行不断重复实时的计算直至时间窗口结束。 3)H(Q)-L 采用Hungarian算法重复计算[M/(NQ)]次将所有agents分配到工作站,解决Task Assignment问题;然后采用改进的PBS算法求解MAPF问题。可以发现H(1)-L和H(Inf)-L分别对应Q=1和Q=∞,随着系统的运行不断重复实时的计算直至时间窗口结束。 4)ITO-L 采用Primal Dual算法求解Min Cost Max Flow问题,解决Task Assignment问题;然后采用改进的PBS算法求解MAPF问题,随着系统的运行不断重复实时的计算直至时间窗口结束。 5)PITO-L 采用Primal Dual算法求解可直接得到TAPF问题的解,同时解决Task Assignment和MAPF问题,随着系统的运行不断重复实时的计算直至时间窗口结束。 下面将介绍我们采用的两个实验平台。 Agent Simulator 图9. 模拟实验中两个分拨中心的地图 Agent simulator是指在我们随机生成的分拨中心业务模式的地图集合上(如图9所示),其中橙色代表工作站,蓝色表示道口,Agent未标明在上述地图中。采用我们设计的仿真框架来模拟系统的运行,核心参数分别是: 工作站处理包裹时间,T = 10; 时间窗口范围为[0,600],即KT = 600,K = 60; 每次重复计算的时间间隔30,即W = 30; Q = M/N + 5; Industrial Simulator 图10. 分拨中心场地的2D布局,绿点为Station,灰点为不可达区域,黑点为道口 我们将ITO-L算法和H(Q)-L算法应用到上述应用场景的分拨中心的调度系统中;其中Task Assignment问题分别用对应的算法解决,实际中的路径规划采用centralized A*算法求解以及解决deadlock问题。我们将实际分拨中心的地图抽象抽象成如图10所示。核心参数分别如下: T = 4; K = 75; Q = 15; 数据结果 Agent Simulator的实验结果如下图11与12所示: 图11. 变化Agent数量的空闲时间趋势 图12. 相对H(Inf)-L算法,各算法对于总产能的提升比例 Industrial Simulator的实验结果如图13所示,其中全场分布的绿色方块表示带着包裹的机器人,全场分布的蓝色表示空载的机器人。 图13. Industrial Simulator运行截图,以及ITO-L与H(Q)-L在Industrial Simulator的表现 从以上数据结果我们可以看到: 1)在自测平台上,无论是ITO-L还是PITO-L表现的要比其他算法好,最小化空闲时间带来的产能提升超过10%,且我们知道10%的分拨能力的提升对一个持续运行的业务系统来说已经是比较好的表现。 2)在实际分拨中心的系统仿真中,我们的产能提升也可以达到11%,表明了我们所设计的算法的扩展性和实用性。 结束语 本文是研究任务分配,多智能体路径规划以及两者结合在一起的TAPF问题的一次成功尝试。我们的核心是针对当前物流行业前沿的自动化方案机器人作业模式的研究,分析其核心点Life-Long TAPF问题,首次提出了最小化工作站空闲时间的思想,来优化提高系统的效率。并在此基础上设计了两种算法框架ITO-L和PITO-L来求解Lifelong TAPF问题,达到最小化工作站空闲时间的目的。在实验部分,我们分别采用了Agent Simulator和Industrial Simulator两种仿真平台来验证所设计的两种算法。实验数据表明在两个测试平台上我们所提出的算法在系统产能上均可以有10%以上的提升,而对于一个长期运行的自动化作业系统来说,10%的提升已经是一个不错的表现,可以让大量的包裹更加高效快速的到达用户手中,对保证和提高物流时效、加速物流自动化的发展起到了积极的作用。本文主要表达,针对物流自动化机器人作业模式,我们在优化方向上和算法设计上的一些思考和做的事情。后续我们将继续分析行业问题,设计和改进算法来进一步加速算法应用来提高系统效率。
什么是机器人工厂? 2016年开始,世界进入“Chatbot时代”,科技行业巨头也纷纷发布了各自在Chatbot领域的相关产品,例如苹果Siri、微软小冰、百度小度、阿里的天猫精灵等等。会话机器人的出现带来的是用户与计算机之间交互方式的变革,从以计算机为中心的一串有序的操作指令,发展到以用户为中心的自然语言会话AI。 机器人工厂就是在“会话AI First”浪潮中应运而生的Chatbot智能机器人孵化平台。有了机器人工厂,任何人只需一分钟就可以搭建一个专属的会话机器人,不懂NLP技术、搞不定编程各种问题都不存在的! 机器人工厂整体架构图如下所示,下面将围绕产品场景和核心能力两大块做详细介绍。 用于哪些产品场景? 机器人工厂的应用场景主要分为答疑、运营、运维三大类。 AI+智能答疑 答疑是三种场景中最常见的。用户对商家的产品问询、员工对企业的规章制度,总会有大量共性重复的问题需要解答,将这些共性的问题抽取出来整理成问答对沉淀在机器人工厂平台上,答疑机器人就可以帮你解答大部分问题,既省时省力,就降低了人工成本。比如:机器人工厂为云栖大会提供了大会引导机器人。 AI+内容场景 运营场景主要是通过机器人将消息准确快速的统一触达到所有用户。比如,小明运营着天南海北的100个商家群,只需机器人工厂一次配置消息内容、发送时间,就可以准时触达100个商家群。轻松解决了人工操作100次的低效与时延。 AI+日常运维 运维场景就更厉害了,机器人不仅会回答问题还能够执行命令。比如,你在阿里云上购买了一台服务器,跑了一些任务,可以让机器人帮你查询任务执行的状态,发现异常,终止任务等等。“用户提问 — 理解指令——调用服务(执行指令)—— 返回答案”,是运维场景的链路。 还有更多新奇的玩法儿,比如结合语音文字互转技术,与阿里云通信、菜鸟驿站一起打造了智能外呼机器人。调查问卷、电话回访、上门服务确认是否在家等等场景都可以由机器人完成,机器人一天可以打出的电话可是多了好几倍。 有哪些核心能力? 介绍了这么多,你应该对机器人工厂可以做什么有了一些基本认识。下面将为你介绍机器人工厂有哪些核心能力,到底怎样玩转起来。 智能问答 智能会话机器人的看家本领就是QA问答。意图和实体是两个最基本的概念。意图由用户输入、动作、回应三部分组成。其中用户输入定义了用户问题;回应定义了对应的答案;动作非必选,定义了理解用户意图后需要去执行一系列指令。实体作用于用户输入,将用户输入中的结构化信息抽取出来,高效地解决了存在大量类似意图匹配场景的问题。例如,“杭州2020-03-08的天气怎么样?”杭州可以抽取成一个枚举实体,2020-03-08可以抽取成一个正则实体,动作可以定义为调用一个根据城市和时间去查询天气的服务,最后回应返回天气信息。 上述例子说明,QA问答首先要做到对用户意图的精准理解。机器人工厂底层有一套完备的算法框架,传统机器学习算法与基于深度神经网络的自然语言处理算法相结合,离线的特征提取模型训练与在线的实时预测相结合,纯文本的FAQ意图匹配与基于实体槽位的意图匹配相结合,从而提高意图匹配的准确率。这里不做详细展开,后续会有专题文章介绍。 语料管理 会话机器人的智能程度在一定意义上取决于它所理解的语料的丰富性。但绝大部分的知识都是以非结构化的文本形式沉淀下来,而非会话机器人所能理解的一问一答的意图形式。因此,新创建的会话机器人怎样快速构建语料具有智能问答的能力?机器人工厂提供了三种方式解决冷启动问题,单个机器人应用内,通过语料爬取(自动化)与批量导入(人工)和预置意图(系统公共语料)丰富语料;在多个应用之间实现语料共享(应用拼装)。 语料爬取 语料爬取是指自动地将用户已有非结构化的知识库或文档,通过机器阅读和理解能力,抽取整理成会话机器人可以理解的问答对的形式。语料爬取不仅可以代替人工录入快速丰富会话机器人的语料,而且极大的降低了知识库对接机器人工厂的迁移成本。目前,机器人工厂平台80%的语料都由语料爬取生成。此外,还支持人工的将excel或json格式的语料,批量导入自动生成意图。 预置意图 预置意图是机器人工厂将用户高频、通用的场景下沉到平台层面,使其可以赋能所有平台上的机器人应用,增强QA问答能力。例如,闲聊、查询天气、查询值班等等。用户只需在平台上勾选启用,即可使自己的机器人拥有回答这些问题的能力。 语料共享 语料共享是指不同会话机器人之间相互复用语料的能力。例如,所有银泰百货的会员手册都一致,但不同的店打折促销活动不同。机器人工厂支持将通用普适的语料创建一个机器人A,各自差异的语料分别创建各自的机器人,但大家都共享复用机器人A的语料。语料共享能够提高语料的复用率,让用户更专注差异化的部分。 在线编程 上文提到过意图是由“用户输入——动作——回应”三部分组成,其中动作定义了理解用户意图后需要去执行一系列指令。通常动作会通过HTTP请求的方式去调用用户自定义的一个服务。但发现经常会遇到以下问题: 若用户已有服务接口,会遇到服务格式不适配;机器人工厂的特殊处理逻辑与业务逻辑强耦合等问题。 若用户没有服务接口,需要开发、部署、联调、发布一系列流程,还会遇到机器、网络、环境等问题,如果线上验证失败,上面步骤需要重新来过。 新增一些具有时效性的临时功能,都需要牵一发而动全身。 为了解决上述问题,机器人工厂与阿里云计算平台的在线开发平台AppStudio合作开发出基于AppStudio的在线服务开发IDE,为用户提供云上在线编程平台,帮助你打通下游的服务实现数据查询,指令执行,知识库检索,内容推荐等功能。可以为你提供: 灵活性:支持在线编程,自定义业务逻辑、安全性校验等,与业务系统本身解耦; 开放性:可以引入需要依赖的sdk,支持odps、hsf等服务; 简便性:封装了基本类和openApi便于开发; 即时性:不依赖任何发布系统,随改随生效; 共享性:支持协同编辑开发,代码共享; 调试:支持在线debug、服务测试等功能; 未来已来 机器人工厂在阿里巴巴集团内经过2年的发展与打磨,已经孵化2w+机器人,服务44w+用户。连续2年参加云栖大会让我们感受到用户对智能机器人的强烈诉求,2020年机器人工厂正式发布公有云版本。最后,献上机器人工厂为飞天大数据开发平台DataWorks打造的一键答疑机器人的Demo。
目前企业的数仓建设大多是离线一套,实时一套。业务要求低延时的使用实时数仓;业务复杂的使用离线数仓。架构十分复杂,需要使用很多系统和计算框架,这就要求企业储备多方面的人才,导致人才成本较高,且出了问题难以排查,终端用户也需要熟悉多种语法。本文分析目前的数仓架构,探索离线和实时数仓是否能放在一起考虑,探索Flink的统一架构是否能解决大部分问题。 数仓架构 数据仓库可以分为三层:ODS(原始数据层)、DW(数据仓库层)、ADS(应用数据层)。 1. ODS (Operation Data Store) 层 从日志或者业务DB传输过来的原始数据,传统的离线数仓做法也有直接用CDC (Change Data Capture) 工具周期同步到数仓里面。用一套统一的Kafka来承接这个角色,可以让数据更实时的落入数仓,也可以在这一层统一实时和离线的。 2. DW (Data warehouse) 层 DW层一般也分为DWD层和DWS层: DWD (Data warehouse detail) 层:明细数据层,这一层的数据应该是经过清洗的,干净的、准确的数据,它包含的信息和ODS层相同,但是它遵循数仓和数据库的标准Schema定义。 DWS (Data warehouse service) 层:汇总数据层,这一层可能经过了轻度的聚合,可能是星型或雪花模型的结构数据,这一层已经做了一些业务层的计算,用户可以基于这一层,计算出数据服务所需数据。 3. ADS (Application Data Store) 层 和DWS不同的是,这一层直接面向用户的数据服务,不需要再次计算,已经是最终需要的数据。 主要分为两条链路: 业务DB和日志 -> Kafka -> 实时数仓 (Kafka + Dim维表) -> BI DB -> 数据服务 业务DB和日志 -> Kafka -> 离线数仓 (Hive metastore + HDFS) -> BI DB -> 数据服务 主流的数仓架构仍然是Lambda架构,Lambda架构虽然复杂,但是它能覆盖业务上需要的场景,对业务来说,是最灵活的方式。 Lambda架构分为两条链路: 传统离线数据具有稳定、计算复杂、灵活的优点,运行批计算,保证T+1的报表产生和灵活的Ad-hoc查询。 实时数仓提供低延时的数据服务,传统的离线数仓往往都是T+1的延时,这导致分析人员没法做一些实时化的决策,而实时数仓整条链路的延迟最低甚至可以做到秒级,这不但加快了分析和决策,而且也给更多的业务带来了可能,比如实时化的监控报警。Flink的强项是实时计算、流计算,而Kafka是实时数仓存储的核心。 上图标出了1-9条边,每条边代表数据的转换,就是大数据的计算,本文后续将分析这些边,探索Flink在其中可以发挥的作用。 Flink一栈式计算 元数据 先说下元数据的管理,离线数仓有Hive metastore来管理元数据,但是单纯的Kafka不具备元数据管理的能力,这里推荐两种做法: 1)Confluent schema registry 搭建起schema registry服务后,通过confluent的url即可获取到表的schema信息,对于上百个字段的表,它可以省编写Flink作业时的很多事,后续Flink也正在把它的schema推断功能结合Confluent schema registry。但是它仍然省不掉创建表的过程,用户也需要填写Confluent对应的URL。 2)Catalog 目前Flink内置已提供了HiveCatalog,Kafka的表可以直接集成到Hive metastore中,用户在SQL中可以直接使用这些表。但是Kafka的start-offset一些场景需要灵活的配置,为此,Flink也正在提供 LIKE [1] 和 Table Hints [2] 等手段来解决。 Flink中离线数仓和实时数仓都使用Hive Catalog: use catalog my_hive; -- build streaming database and tables; create database stream_db; use stream_db; create table order_table ( id long, amount double, user_id long, status string, ts timestamp, … -- 可能还有几十个字段 ts_day string, ts_hour string ) with ( ‘connector.type’ = ‘kafka’, … -- Kafka table相关配置 ); -- build batch database and tables; create database batch_db; use batch_db; create table order_table like stream_db.order_table (excluding options) partitioned by (ts_day, ts_hour) with ( ‘connector.type’ = ‘hive’, … -- Hive table相关配置 ); 使用Catalog,后续的计算可以完全复用批和流,提供相同的体验。 数仓导入 计算①和⑤分别是实时数仓的导入和离线数仓的导入,近来,更加实时的离线数仓导入越来越成为数据仓库的常规做法,Flink的导入可以让离线数仓的数据更实时化。 以前主要通过DataStream + StreamingFileSink的方式进行导入,但是不支持ORC和无法更新HMS。 Flink streaming integrate Hive后,提供Hive的streaming sink [3],用SQL的方式会更方便灵活,使用SQL的内置函数和UDF,而且流和批可以复用,运行两个流计算作业。 insert into [stream_db.|batch_db.]order_table select … from log_table; 数据处理 计算②和⑥分别是实时数仓和离线数仓的中间数据处理,这里面主要有三种计算: ETL:和数据导入一样,批流没有区别。 维表Join:维表补字段是很常见的数仓操作,离线数仓中基本都是直接Join Hive表即可,但是Streaming作业却有些不同,下文将详细描述。 Aggregation:Streaming作业在这些有状态的计算中,产生的不是一次确定的值,而可能是不断变化的值。 维表Join 与离线计算不同,离线计算只用关心某个时间点的维表数据,而Streaming的作业持续运行,所以它关注的不能只是静态数据,需要是动态的维表。 另外为了Join的效率,streaming作业往往是join一个数据库表,而不仅仅是Hive表。 例子: -- stream 维表 use stream_db; create table user_info ( user_id long, age int, address, primary key(user_id) ) with ( ‘connector.type’ = ‘jdbc’, ... ); -- 将离线数仓的维表导入实时数仓中 insert into user_info select * from batch_db.user_info; -- 维表Join,SQL批流复用 insert into order_with_user_age select * from order_table join user_info for system_time as of order_table.proctime on user_info.user_id = user_info.user_id; 这里有个非常麻烦的事情,那就是在实时数仓中,需要按时周期调度更新维表到实时维表数据库中,那能不能直接Join离线数仓的Hive维表呢?目前社区也正在开发Hive维表,它有哪些挑战: Hive维表太大,放不进Cache中: 考虑Shuffle by key,分布式的维表Join,减少单- 并发Cache的数据量 考虑将维表数据放入State中 维表更新问题: 简单的方案是TTL过期 复杂一些的方案是实现Hive streaming source,并结合Flink的watermark机制 有状态计算和数据导出 例子: select age, avg(amount) from order_with_user_age group by age; 一句简单的聚合SQL,它在批计算和流计算的执行模式是完全不同的。 Streaming的聚合和离线计算的聚合最大的不同在于它是一个动态表[4],它的输出是在持续变化的。动态表的概念简单来说,一个streaming的count,它的输出是由输入来驱动的,而不是像batch一样,获取全部输入后才会输出,所以,它的结果是动态变化的: 如果在SQL内部,Flink内部的retract机制会保证SQL 的结果的与批一样。 如果是外部的存储,这给sink带来了挑战。 有状态计算后的输出: 如果sink是一个可更新的数据库,比如HBase/Redis/JDBC,那这看起来不是问题,我们只需要不断的去更新就好了。 但是如果是不可更新的存储呢,我们没有办法去更新原本的数据。为此,Flink提出了Changelog的支持[5],想内置支持这种sink,输出特定Schema的数据,让下游消费者也能很好的work起来。 例子: -- batch:计算完成后,一次性输出到mysql中,同key只有一个数据 -- streaming:mysql里面的数据不断更新,不断变化 insert into mysql_table select age, avg(amount) from order_with_user_age group by age; -- batch: 同key只有一个数据,append即可 insert into hive_table select age, avg(amount) from order_with_user_age group by age; -- streaming: kafka里面的数据不断append,并且多出一列,来表示这是upsert的消息,后续的Flink消费会自动做出机制来处理upsert insert into kafka_table select age, avg(amount) from order_with_user_age group by age; AD-HOC与OLAP 离线数仓可以进行计算⑨,对明细数据或者汇总数据都可以进行ad-hoc的查询,可以让数据分析师进行灵活的查询。 目前实时数仓一个比较大的缺点是不能Ad-hoc查询,因为它本身没有保存历史数据,Kafka可能可以保存3天以上的数据,但是一是存储成本高、二是查询效率也不好。 一个思路是提供OLAP数据库的批流统一Sink组件: Druid sink Doris sink Clickhouse sink HBase/Phoenix sink 总结 本文从目前的Lambda架构出发,分析了Flink一栈式数仓计算方案的能力,本文中一些Flink新功能还在快速迭代演进中,随着不断的探索和实践,希望朝着计算一体化的方向逐渐推进,将来的数仓架构希望能真正统一用户的离线和实时,提供统一的体验: 统一元数据 统一SQL开发 统一数据导入与导出 将来考虑统一存储 参考 [1]https://cwiki.apache.org/confluence/display/FLINK/FLIP-110%3A+Support+LIKE+clause+in+CREATE+TABLE [2]https://cwiki.apache.org/confluence/display/FLINK/FLIP-113%3A+Supports+Table+Hints [3]https://cwiki.apache.org/confluence/display/FLINK/FLIP-115%3A+Filesystem+connector+in+Table
阿里文娱有专门的搜索引擎算法团队做相关的探索。可你知道吗?在算法业务背后,面对数亿的视频数据,还有一支技艺高超的测试团队,在保障庞大数据系统的数据质量。 阿里文娱测试开发专家熙闫将通过本文讲述阿里文娱是如何构建实时数据质量保障体系,进而提升搜索引擎数据全面、快速、准确效果的,希望对大家有启发。 一、背景 优酷视频搜索是文娱分发场的最核心入口之一,数据源多、业务逻辑复杂,尤其是实时系统的质量保障是一个巨大挑战。如何保障数据质量,如何衡量数据变化对业务的影响?本文会做详细解答。 二、现状分析 搜索数据流程如下图所示,从内容生产到生成索引经历了复杂的数据处理流程,中间表多达千余张,实时数据消费即消失,难以追踪和复现。 从上图可以看出,整个系统以实时流模式为数据流通主体,业务层面按实体类型打平,入口统一分层解耦,极大的增加了业务的实时性和稳定性。但是另一方面,这种庞大的流式计算和数据业务系统给质量保障带来了巨大的挑战,从0到1建设实时数据的质量保障体系,同时保证数据对搜索引擎业务的平滑过渡是我们面临的考验。 三、实时数据质量保障体系方案 质量保障需要透过现象看本质。通过对架构和业务的分析,可以发现整个流式计算的业务系统有几个关键点:流式计算、数据服务、全链路、数据业务(包括搜索引擎的索引和摘要)。整体的质量诉求可以归类为: 基础数据内容质量的保障 流式链路的数据正确性和及时性保障 数据变化对业务效果的非负向的保障 结合线上、线下、全链路闭环的理论体系去设计我们的整体质量保障方案,如下图所示: 四、线下质量 1.实时dump 数据测试包含链路节点比对、时效性、正确性、一致性、可用性等方面,依托于阿里技术资源设计实时dump的方案如图: 2.数据一致性 一致性主要是指每个链路节点消费的一致性,重点在于整体链路的各个节点的数据处理消费情况保持一致,通过对数据消费的分时分频率的比对完成一致性验证。方案如下图: 我们采取不同的数据流频率输送给实时链路进行消费,利用各层的dump机制进行数据dump,然后取不同的抽样间隔对dump数据计算分析,分为三种不同的数据频率模式: natural-flow:自然消费的数据流,是源于线上真实的数据消息通道,即自然频率的数据消费,以该模式进行测试更贴合实际业务情景 high-frequency:高频数据流,采用超出真实峰值或者其他设定值的数据频次输送给实时消费链路,在压测或者检测链路稳定性中是一个常用的测试策略; low-frequency:低频数据流,采用明显低于真实值或者特定的低频次数据输送给实时消费链路。如果数据链路中有基于数据量的批量处理策略会暴露的比较明显,比如批量处理的阈值是100,那么在业务低峰时很有可能达不到策略阈值,这批数据就会迟迟不更新,这个批量处理策略可能不是合理。同时低频次的消费对于实时链路处理的一些资源、链接的最低可用度这些层面的检查也是有意义的。 3.数据正确性 数据正确性是对于数据内容的具体值的检查,总体原则是: 首先,高优保障影响用户体验的数据; 其次,保障业务层直接使用的核心业务相关的数据内容; 再次,中间层的核心业务相关数据由于不对外露出,会转换成业务引擎需要的最终层的业务数据。所以中间层我们采用通用的规则和业务规则来做基础数据质量保障,同时对上下游数据内容变化进行diff对比,保障整个流程处理的准确性。 4.数据可用性 数据可用性指的是数据链路生产的最终数据是能够安全合理使用的,包括存储、查询的读写效率、数据安全读写、对不同的使用方提供的数据使用保持一致性等。 可用性保障主要关注数据的存储、查询、数据协议(数据结构)三个大的维度,衡量的标准重点关注三个方面: 易读写:数据的结构化存储和写入必须是高效合理的; 服务一致:数据在结构化存储后,对外提供的服务有很多种,比如PB协议、API、SDK等,需要根据业务去考量。比如SDK、PB等对外提供使用的方式会涉及协议版本,不同的版本可能数据结构不一致导致对外使用的数据不一致性; 安全可靠:重点关注存储稳定、可靠、高效,兼顾效率和稳定性,同时更要关注安全性,防范随意改写数据、恶意dump等严重影响线上数据使用安全的风险。 5.时效性 由于实时链路的流式特性和多实体多次更新的特性,在测试时效性时核心问题有两点: 如何去跟踪确定一条唯一的消息在整个链路的消费情况; 如何低成本获取每个节点过程的数据链路时间。 我们抽象出一个trace+wraper的流式trace模型如下图: 获取链路过程的每个节点的时间,包括传输时间和处理时间。对于track-wraper需要约定统一的track规范和格式,并且保证这部分的信息对业务数据没有影响,没有增加大的性能开销。如下图,我们最终的信息中经过trace&track-wraper带出来的trak-info,采用json格式方便track-info的扩展性。 这样就很容易去获取到任意信息,计算每个节点的时间。 我们也可以通过抽样计算一些统计指标衡量时效: 对于时效性有明显异常的数据可以筛选出来,进行持续优化。 6.性能测试 实时数据链路本质是一套全链路数据计算服务,所以我们也需要测试它的性能情况。 第一步,我们先具体化全链路的待测系统服务。 包括两部分的性能,Bigku的反查服务,即HSF服务,再就是blink的计算链路节点。 第二步,准备数据和工具。 压测需要的业务数据就是消息。数据准备有两种方式,一种是尽可能模拟真实的消息数据,我们只要获取消息内容进行程序自动模拟即可;另外一种会采用更真实的业务数据dump引流,进行流量回放。 由于数据链路的特性,对压测链路施压就是转成发送消息数据,那么如何控制数据发送呢?有两种方式: 第一种我们开发一个发送消息的服务接口,转变成常规的接口服务压测,然后可以采用阿里的任何压测工具,整个测试就变成常规的性能测试; 第二种我们可以利用blink消息回追的机制,重复消费历史消息进行压测,不过这种方法有弊端,无法控制消息的频率。 7.压测和指标收集 根据业务情况来收集指标,指标包括服务本身的指标和资源指标,可以参考我们的部分性能测试报告示例(数据有截断): 五、线上质量 1.服务稳定性保障 稳定性包括两个层面,一是实时计算任务链路的每个节点的稳定性,二是内置服务的稳定性。 2.实时计算 由于实时计算采用全blink的计算方式,我们可以利用blink系统本身的特性来做任务的监控。每个节点的任务都需要配置稳定性指标的监控,包括rps、delay、failover等。效果示例如下: 3.实体服务 实体服务是HSF服务,采用阿里统一的监控平台来完成整体服务能力的监控,示例如图: 整体指标包含以下内容: 4.数据消费保障 在数据消费层面,重点关注每个链路层级的消费能力和异常情况。基于积累的track-report能力进行数据统计,结合平台完备的基础能力来完成消费保障。分为两层: 核心层:消息出口的实体消息统计监控,包括整体数量和消息内容分类统计监控。如图示例: 中间层:包括每个实体消息处理的accept,处理逻辑层的success、fail、skip指标,便于我们实时知晓每个链路层收到的消息、成功处理、错误和合理异常等消费能力情况。如图示例: 5.数据内容保障 数据内容层,建设综合数据更新、数据内容检查、业务效果三位一体的精准数据检查,达到数据生产、消费、可用性的闭环检测,如图所示: 从图中可以看出,我们数据内容保障分为三部分: sampler:抽样器,通过blink实时消费消息在链路中抽取待测数据,通常是只抽取数据ID;抽样策略分间隔和随机两种。间隔策略就是取固定时间间隔的特定数据进行检查;随机则根据一定的随机算法策略来抽样数据进行检查。 data-monitor:是做数据内容检查,包括更新时效性和数据特征属性检查。 effect-monitor:数据正常更新之后,对在线业务实时产生的效果影响进行检查,检查的核心点包括搜索的两大基本效果——召回和排序,以及用户体验相关的数据属性的检查。 部分数据实时效果示例图: 6.实时干预与自动修复 实时干预通道,如下图: 实时干预系统会根据不同的干预需求,对消息内容和干预机制进行消息组装和通道分发。 当主通道业务链路正常时,若需要强制更新一个ID维度的数据,只需要输入ID走正常主链路更新即可。 当需要强制干预某些具体的数据内容到指定的消息通道时,则可进行数据内容级别的更详细的精准干预。 急强制干预,是指当主链路中间层处理有较大延迟或者完全阻塞时,会造成下游业务层数据无法正常获取输入。通过主逻辑全copy的机制建立了一个VIP的消息通道,通过VIP通道去直接干预出口消息,保证业务数据正常能进行优先更新。 六、质量效能 效能层面主要指:研发能快速自测上线,线上问题能高效排查定位这两个维度,以期达到保证快速迭代、节省人力投入的目标。所以我们提供了实时debug和实时全链路trace透视两大提效体系。 1.实时debug 实时debug是基于实时消息通道能力和debug机制建立的一套服务,在研发自测、问题复现等场景有很大用途,可以通过debug模式详细了解链路的业务层处理细节,业务层只需要按数据需求自主定制debug内容,无需其他接入成本,具备很强的通用性和扩展性。 平台效果图: 填入节目ID,发送消息就会自动进入实时debug模式。 同时还配备了指定消息内容的专家模式,方便研发进行单独的消息内容制定化测试和干预。 2.全链路trace 我们提炼了一个全链路实时trace的通用模型,同时做更精细定制化的trace机制。结合实时业务链路逻辑视图,来看下trace的系统实现: 链路层视角,目前整体分为4个业务块,数据流按顺序进行展示: 1)bigku_service 展示了当时消息的镜像数据。 2)mid_show_f 为算法层面的基础特征,即一级特征,包含了业务信息和系统信息(工程关注的指标数据,主要用来指导优化)。 3)sum_video_f 和 ogc属于搜索链路上的数据,一般在节目里面会有一些较为复杂的截断逻辑,通过字典表的形式提供数据层的透视视角,可以看到链路的全部信息。 七、产品体验实时自动化保障 我们在实时数据内容质量方面做了融合效果监控的质量方案,建立了实时发现问题、实时定位、实时修复的闭环链路效果保障体系,起到了很好的效果。体系方案如下图: 后记 数据是算法的生命之源,保障好数据质量,提高优质内容的分发效率,既能留住用户,也能让用户在疫情这段特殊的日子里看到高质量的视频内容。数据质量道阻且长,未来会深入每个节点和逻辑,探索海量数据和用户体感的关系,能对算法业务发展有帮助作用,也能为广大用户感受到文娱带来的精神滋养。
人工智能算法 目前,AI(人工智能)已经成为科技产业大趋势。各个行业都与“AI”密切相关。与AI相关的领域如下图所示,其中包括与AI强相关的领域和AI间接赋能的的领域。那么究竟什么是人工智能、人工智能的应用以及人工智能系统将在之后一一介绍。 人工智能发展的80年,实现了从图灵测试到全民换脸。机器是通过人工智能像人一样来回答问题、创作或者计算分析的,在一些领域,计算机已经能够做的和人一样优秀。例如在2019年网络上的“全民换脸”都是基于人工智能中的深度学习及神经网络等技术的广泛应用的结果。 目前,人们生活中以及工业生产中都有很多“AI”技术的应用,用来代替人类的工作。例如比较流行的“ELON MUSK’S”能够模拟人的大脑工作。但随着人工智能的快速发展,也出现了一些对人工智能的反思和一些“假冒AI”。 人工智能AI在发展过程中面临了一系列的事件,其中有比较严重的假冒伪劣AI骗取2亿融资的事件。那么人工智能究竟是什么以及它的用途主要有哪些是接下来要重点讨论的问题。 在学术界,人工智能的定义也有所差异。人工智能是接受输入的信息,通过信息的整理判断,像人一样对输入的信息做出一系列理性行为和决策。它的主要特征就是“理性的行动”。 在这个“感知”到“决策”的反馈中,如何感知外部世界信息成为人工智能能否去行动的关键。既然是要模拟人的大脑,那人去感知的过程其实是一个认识和学习的过程。那也就是人工智能中“深度学习”所要解决的问题。 深度学习 只有将外部信息(视频,文字,口令等)转换成机器语言才能被人工智能所接受并作出反应。这个问题的思考早在人工智能初期就被科学家所考虑和研究。 在这之后人们开始讨论如何通过视觉感知来完成信息的输入,并做了很多研究。2012年,加拿大多伦多大学的ImageNet竞赛冠军获得者Hinton和他的学生Alex Krizhevsky设计的。也是在那年之后,更多的更深的神经网络被提出,比如优秀的vgg,GoogLeNet。这对于传统的机器学习分类算法而言,已经相当的出色。 AlexNet开始的深度学习历程 通俗的说,就是在大量的物体中,准确地识别我们指令中需要的物体。这个模型的应用使得图像识别领域取得了突飞猛进的发展,并被广泛应用。 神经网络这种分层学习的模式跟我们人类大脑一样,随着不断地学习,神经网络也变得越来越复杂。假设要在百万级的图片信息中找已标注的信息“猫”,然后把编辑好的视觉网络的模型在一个非常大的数据集中训练。通过模型的迭代实现更复杂的训练。 目前,比较普遍使用的“RestNet模型”,深度在一百多层,并加入了一些最新的科研成果,例如最下面图中如拱桥部分的快速链接,可以有效快速的训练如此深的网络。最终解决视觉领域的“感知”问题。 阿里云:智慧航空机坪管理 通过人工智能来识别机种,登机门,机场车辆,并与实际地图结合起来,以及了解飞机在飞行过程中的运行轨迹等等,这些信息都可以作为输入的信息来通过人工智能管理,使得机场运转更加快捷和高效。 上面所说的,深度学习是感知的一个重要形式和方法。深度学习算法主要组成: 数据标注 算法模型开发 高性能分布式训练 模型调优 模型部署 人工智能在“感知”之后,另一个需要做的就是“决策”。深度学习是一个黑箱操作,能够很好地学习和感知外部信息,但是不能给出反馈及如何解释自己感知的问题究竟是什么原因。那就需要“决策”来分析和反馈。 传统机器学习的榜样是决策树算法和逻辑回归。例如,银行发放贷款的过程就是一个权衡各方面因素之后的一个决策过程。可以通过决策树的形式,进行“Yes”或“No”的判断来最终决定是否发放贷款。而逻辑回归,指的是两类数据之间的相互关系,通过数学的方式精确求解。 其实,深度学习和机器学习是一种互补的状态。深度学习非常好地解决了感知的问题(计算机视觉,语音等等),可以用神经网络的架构来解决非常多的“感知”的问题,但它需要解释这些感知的东西。而传统机器学习则没有这么人性化的感知功能,但它的模型相对较小,我们可以直接解释(例如金融,风控等)。 人工智能很早便被应用在广告领域中。早在宋朝就有广告,用来帮助来招揽生意。 目前比较典型的广告场景是淘宝广告。厂家首先通过消费者个人的浏览信息了解用户的喜好是什么,然后再通过智能推荐系统来推送消费者所搜索的相关产品。这样的一些智能算法的广泛应用使得用户的信息浏览更加高效和精细化。 无论是感知还是决策,都和算法相关。 感知。与深度学习算法相关,涉及到数据标注、算法模型开发、高性能分布式训练、性能调优、模型部署等。 决策。传统机器学习算法以及深度学习算法相关,涉及到行业行为数据采集、结构化/非结构化数据处理、数据和算法的组合建模、算法开发训练和调优、模型部署和实时训练反馈等。 人工智能系统 在算法发展迅猛的今天,相应的基础设施支持也显得尤为重要,这就需要人工智能系统的支持。构建人工智能或者机器学习系统的两个不可或缺的因素是算法和算力,算法创新的背后是算力的突破。 截止到2019年,人工智能对于算力的需求如下图所示。相较于AlphaGo Zero,AlexNet对于算力的需求已经有了30万倍的增长。这种情况下解决算法迭代和算法落地的问题,给系统提出了更高的要求。 AlexNet在2013年的时候所谓的系统如下图所示,简单的一台机器加GPU,当时的训练成本大约是七天每天500瓦,也就是业务模型的迭代周期是一周左右。 在业务需要飞速发展的今天,比如广告推荐,一周的模型迭代周期是远不能满足需求的。因此,目前越来越多的人关注如何通过大规模集群或者芯片的方式来为人工智能系统提供更好的算力。MIT在2014年的时候做了一个对比,一个人在一分钟内大概可以处理77张图片,单个GPU相同的时间内可以处理230张,尽管单个GPU的处理速度与人的处理速度相差不大,但是其可以通过GPU集群的方式实现更大规模更快速的计算,比如下图中512个GPU的集群,可以在一分钟内处理60000张图片。 人工智能系统在设计的过程中需要关注怎么样做高性能存储,怎么样实现机器之间的快速通信,怎么样保持分布式集群的稳定性。今天,阿里云内部有一个Eflops平台,可以实现三钟内1018次的计算,耗电128千瓦每分钟。这是在2015年以前是无法想象的能力,这一能力的实现主要归功于大规模集群,还有系统底层芯片的伸缩性。 目前国内很多家企业致力于更高性能芯片的研发,阿里也不例外。2019年,阿里发布了全球最高性能的AI推理芯片含光800,并在城市大脑和航空大脑的实际测试场景中进行了测试,峰值性能可以达到将近80万张图片每秒,这与上一代的芯片相比,实现了40倍左右的性能提升。 系统复杂度上升后,会带来一系列的问题,包括软件复杂度、硬件复杂度、资源管理复杂度、调度效率复杂度、全系统优化复杂度,这在系统发展过程中是比较共性的挑战。 需要强调的是,AI集群不等于通用集群。AI在做训练的时候需要子任务周期性同步,不同机器之间需要有高性能的通信,很多时基于GPU或NPU专用部件。不同的计算模型,不同的交互模式目前对于AI训练有比较大的挑战。 阿里的各种业务场景都可以用到AI,因此可以通过AI实践打磨平台设计,比如手淘-拍立淘的百万分类模型、淘宝网的语音+NLP和阿里妈妈广告推荐等。 打磨后的飞天AI平台分为三层,从最底层的基础硬件,到中间的训练和推理框架,再到开发平台。对于AI平台来讲很重要的平台有以下三个: 轻量级AI开发平台:帮助算法和数据科学家实现一键式开发、调试部署 AI和大数据协同开发平台:帮助更加迅速地开发面向大数据型业务的系统 AI推理服务平台:解决推理需要的计算资源问题、模型训练、部署和效果监测 以上三个平台支撑了算法API的输出和垂直领域平台以及大脑的解决方案。 深度学习领域,斯坦福大学推出了一个名为DAWNBench的测试基准,相比于之前的最有结果,阿里云机器学习实现了性能百分之十左右的优化。 AI技术能力在今天对于提升资产利用率、解决不同场景需求具有重要意义。综合的AI技术能力主要涉及以下几方面: 基础硬件:用于提供通用的算力以及AI所需要的计算能力,通过IaaS提供云的能力 AI云服务:最基础的PaaS层,通过容易拉起的软硬件环境向绝大多数用户提供适合AI的算力高性能计算:提供核心AI计算引擎加速 AI系统框架:提供AI计算模式的完整抽象以及跨体系结构的建模迭代和部署 AI托管平台:提升算法研发共享部署和输出的效率,以及具有用户粘性的开发平台 智能计算和数据计算 AI是智能计算,大数据领域是数据计算,二者是相辅相成不可或缺的关系。 数据支撑AI 刚才提到的算法和算力背后需要大量数据的支撑,数据是体现算法和算力价值的重要部分。 下图分别展示了2005年和2013年教皇登基的场景。当前手机互联网的发展导致了数据的指数型增长,这也可以给深度学习带来性能的提升。 1998年的一个小系统MNIST的训练数据仅有 10MB,2009年的ImageNet有200G,2017年的WebVision有3TB,而典型的产品视觉系统有1PB。海量的数据帮助阿里几乎线性地提升其性能。 举一个的生活中的场景来说明数据量对于性能的提升作用。在X光片医学识别领域,有研究显示,医生在X光片上识别病症的效果和其所看过的X光片数量成正比。看的越多,正确率越高。同理,目前的医疗引擎系统可以通过大规模的计算机系统训练更多的数据,实现更加精准的医疗识别。 AI驱动大数据走向智能化 下图展示了Forum对大数据领域做的趋势总结,当前大数据领域需要提取更多的信息,要实现实时的计算,实现AI平台和在线预测等,都体现了大数据走向智能化的趋势。 多个数据源不同类型的数据,如结构化、半结构化和非结构化,落到数仓后如何发挥其价值,答案是智能计算。以广告推荐场景为例,数据源是用户在淘宝上的点击、浏览和购买行为数据,通过数据集成离线或实时同步、离线或实时ETL的方式将其落到数仓中,再通过数仓或数据湖的解决方案生成各种数据模型对数据进行训练,最后通过数据服务的方式对训练结果进行输出。可以发现,该过程中对于数据的理解和使用方式开始变得智能化。 几年前的HTAP,包括OLTP和OLAP两部分,OLAP可以进一步分解为大数据的分析,离线、实时分析,基于数据量的不同选择不同的引擎。而目前数据服务也变得越来越重要,在一些智能客服场景中,需要依赖数据提炼模型,来做实时人工智能推理服务和应用,因此如何把analytics和service结合也很关键。这也是现在考虑在做的HSAP,通过人工智能驱动离线、实时数仓数据价值提取,通过数据服务推送给用户。 阿里在自己本身的应用中沉淀出了AI加持的大数据方法论和解决方案,在双十一大促中的离线计算(批处理)、实时计算(流计算)、交互式分析和图计算等场景,和飞天AI平台相结合,为用户提供了AI加持的完整的新一代飞天大数据产品。 大数据和AI一样,也非常注重性能。2019年阿里云大数据平台MaxCompute和EMR分别在TPC上的计算性能和性价比优势明显。具体测试结果如下图所示。 阿里的阿里小蜜目前为用户提供了智能化的的语音客服交互方式,其应用了深度学习和智能感知的AI技术,同时需要和背后的大数据业务系统紧密联系,如物流、用户数据等,才能实现最后的智能化效果。 那么作为一个企业应该如何拥抱AI呢。简单来讲,人工智能需要落地,应该从应用需求出发,逐渐追求技术创新,就像爱迪生发明电灯一样。通过云提供低成本、到高性能和高稳定性的基础设施,但关键应该明确需求是什么。 前面几年,AI一直在做算法的创新,做Demo,但这是远远不够的。 AI算法只是系统中的一环,怎样收集数据,获取有用特征,怎样进行验证,怎样进行过程管理、资源管理等等,都是企业在拥抱AI需要考虑的问题。 AI不是万能的,但是忽略AI是万万不能的。当企业拥抱AI的时候,最重要的还是从业务出发。随着数据量越来越大,算法越来越多,核心是需要建立懂业务的数据工程师、算法工程师的队伍,这是当前智能化企业致胜的关键。而前面提到的算法、算力和数据,都可以利用目前云上提供的服务和解决方案来实现,其可以帮助企业更快速的实现AI的落地。
工业互联网是消费互联网的下半场 各位同学,我叫丁险峰,花名柯镇。我是阿里云IoT事业部的首席科学家,负责阿里云物联网平台的建设,以及工业互联网平台的建设。 首先我想阐述一下,开源对于工业互联网的价值。 开源对于工业互联网这一个庞大工程是极其重要的。为什么要建设工业互联网?工业互联网的内容是什么?工业互联网的本质是数字化人类积累了200多年来的工业知识,以软件的形式或者数据的形式放在互联网上。原来的数字化图书馆是把文字数字化了,如果说工业领域的书的话,那么把物理知识、化学知识、数学公式等都用计算机的文字来表达,但还不是一段可以被调用的计算代码,但是人获取知识以后,还要编程序去控制机器。过去20年的互联网主要还是解决信息的存储、处理、传播的问题,而未来20年人类将更深入一层,把这些数学公式,仿真模型代码化,所以工业互联网将是一个巨大的工程,人类拥有的工业肯定不止360行业,可能就是一个轴承行业(机械的重要部件)就可以分成360行,有很多很多种人类已经建立的模型,这些人类的知识能不能被重复利用呢?上一个互联网时代是可以共享模型的,数学公司的,但是需要copy的,传播的能力严重被限制了。但是一旦这个模型建立了一个SaaS,那么任何人只要输入参数就可以重复利用这个人类知识的结晶了。显然人类的知识的利用率将会被大大地放大了,人类从1万年的农业革命以来的协作再一次被大幅度提升了。 既然我们不仅仅要建立一个数字的四库全书,而是要建立一个四库全书的代码库,这样巨大的工作,有几种工作方式,一是请乾隆皇帝再世,我们可以搜罗全中国,乃至全世界的专家来贡献代码,那将是非常昂贵的过程。第二种方式是学习专利体系,谁贡献了,谁就得到专利,那么大量的已经过期的专利的模型就没有人来贡献了。而且人类共同拥有的知识或者模型,应该属于人类共同所有,而不是被少数人或者公司所有,他们写的代码应该得到报酬,但是知识和模型应该是免费的。既然这么庞大的知识数字化工程,是人类劳动力的一次大跃升,那么全人类参与进来才是最为有意思的。而且很多知识模型应该给整个人类分享,那么首先应该开源,开源意味着分享给整个人类。其次只有开源,整个人类都来贡献,才能穷尽人类200年来积累的工业知识。当然现在人类还在不断的发明创造的过程之中,很多公司有的新发明还是可以不开源,客户可以通过付费来获取使用权。 接下来我们探讨下工业互联网和物联网之间的差别。物联网是一个比较通用的名词,而工业互联网是用物联网把人、机、料、法、环数字化以后连在了一起,然后基于这些资源来优化整个生产过程,从而实现更快速敏捷地响应大规模定制化的需求。 过去20年主要是消费互联网,它是拉近了商品和消费者之间的关系。而下面的20年是工业互联网或者说工业4.0,我认为是消费互联网的下半场,其目的是要拉近制造系统和消费者之间的关系。消费互联网是先有生产计划,消费者没有对产品需求的定义权,只有等产品做出来,才能够去购买。而工业互联网要解决的问题,是让整个制造系统能够非常敏捷地响应消费者的定制化需求,要让消费者参与到制造的过程当中。 任何一个时代,都是由技术驱动的。比如因为有了牛顿的三大定律,后来瓦特才发明蒸汽机,才有了机械化时代;有了麦克斯韦方程,才会有爱迪生发明了发电机、灯泡,有了电气化时代;有人发明了半导体,从电子管到晶体管再到集成电路,才会引起信息化时代的蓬勃发展。 过去的十年里,移动互联网背后也是由一个庞大的技术浪潮所驱动的。智能手机背后的技术是什么呢?是4G技术。芯片技术达到了一定的程度,才能够支撑你做出一个智能手机,从而才点燃了移动互联网时代。那么下面一个时代,我们首先要讲它是一个什么样的技术时代,然后再讲它是一个什么样的产业时代。 解决1万亿“物”的信息交换 相对过去的4G时代,我们现在更多地是讲5G或者是物联网时代,这个时代的技术驱动力是什么呢?不仅仅是像4G一样去解决100亿人的信息交换问题,而是物联网加上5G,来解决将近1万亿物的信息交换问题。4G时代很好的解决了人类信息的交换问题,而无论是5G还是我一会要讲到的Ipv6,其技术本身就是为了解决物的信息交换问题。 从Bits、Farmes,到Packets、Messages……最后是未来有participant x和participant y,有可能是1万亿物里面有100亿人,不再是两个人在交换信息,而是有可能一个人和一个灯泡,或者一个人和一辆车,或者是你的车跟你的家在交换信息。下一个时代的技术驱动力,是来自于物联网的技术。 一旦建立了万物互联,最重要的一件事情就是我们能够建立一个物理世界抽象层,把每一个物体从物理世界映射到数字世界。有了这个数字世界,计算机就能够发挥它强大的计算能力,来处理大数据、小数据或者时序数据,就能够再一次大幅提升人类生活方方面面的效率。 有机构预测,到2025年或者是2030年,物联网、AI、5G等技术将会对人类社会产生11万亿美元的价值。其中制造业预估会产生1.2万亿美元到3.7万亿美元的价值,比其他的行业都要大。制造业本身就是人类的第二大产业,在中国也是占据40%左右的GDP。所以哪怕1%的影响,也会产生几千亿元的利润,再乘以估值,那就是几万亿元的估值差别。 在PC时代,中国的参与度是比较小的。芯片主要是英特尔,软件主要是微软、甲骨文这样的公司。中国只有联想这一家公司,占到了全世界分工的20%。移动互联网时代呢,我们中国有华为、Oppo、vivo、小米等等公司,占据了手机硬件设备出货量的50%。芯片上也取得了长足的进展,像海思和展讯,目前已经占到了整个手机芯片出货量的将近20%。而移动时代的软件也就是APP,无论是电商软件、搜索软件还是社交软件,中国已经是仅次于美国的第二大APP市场,也是接近世界市场的20%。 进入下一个时代,物联网时代,将会需要大量的传感器和连接器,或者有连接的传感器。预测,到2030年,中国将会设计制造世界上80%~90%的物联网硬件设备和50%的物联网相关的芯片。 这里我们要提出一个问题,我们能做世界上50%的硬件, 50%的物联网的芯片,我们能不能做世界上50%的处理这些物联网数据的SaaS呢?现在还是一个巨大的问号,美日欧在SaaS的生产上还是要比中国还要强大得多,所以我也要跟各位同仁说一声,革命尚未成功,同志仍需努力。 5G与IPV6 讲完了大的趋势,接下来重点要讲一下物联网相关的最重要的几个技术。我不是要讲技术怎么做,而是要讲为什么说这些技术是为物联网而生的。 第一个最重要的技术,我认为是5G。5G不仅仅是4G的延伸,4G的延伸其实只有一个点,叫做移动增强带宽。现在我们的手机可以视频通话,移动增强带宽无非是让视频通话可能达到3D视频通话。5G的海量机器类通讯,和超高可靠低时延通讯,其实是为了解决物联网设备的问题。 5G有三大重点功能,第一个是增强移动宽带,它主要是解决高清视频、AR/VR的人机交互。第二个是大连接低功耗机器通信,主要是解决比如家里的电表、水表、气表这些低功耗/超低功耗的连接。这些表计的密度也非常的高,所以要把通讯切分成很多细密度的信道,也要让通讯保持超低功耗、超低成本。而第三个低时延高可靠连接,主要是为了工业高精度控制、自动驾驶这类的应用。一个运转中的机械臂或者一辆行驶中的自动驾驶汽车,它的通讯要求非常低的时延,而且一定要传达,所以必须是超高可靠的。 5G既然是为物联网所生,那么它其实不再需要一个非常宽广的移动通讯的手段,所以未来私有网络是非常重要的,低成本的基于云化的核心网是非常重要的。阿里巴巴也投入了基于云化的5G核心网,以及MEC移动边缘计算单元,在端上新增了5G协议栈,来帮助客户做智能家居、智能园区、智能城市和智能工业这些方向。 然后为什么要做IPV6?它是一个网络拓扑的革命。不需要二手中转,也不需要P2P打洞,两个物联网的设备之间就可以非常好地自由地通讯。每一个设备也不需要躲在网关后面,就可以升级到网络世界的一等公民。而且因为都是直接IP, Ipv6网络可以降低10~30%的网络延时。 那么5G是解决信道容量的问题,IPV6是解决IP地址的问题,从而能够让世界变成一张扁平的网络。阿里巴巴跟合作伙伴一起,提供了很多IPV6的技术,不管你是通过LoRa基站、边缘网关、私有云接入,或者是通过Wi-Fi、蓝牙接入,我们都能够提供IPV6的接入方式。 然后我们会在公有云、私有云和专有云上面提供IPV6的接入,也在海外各地提供IPV6的接入。我们现在主要是提供IPV6的接入,未来会提供IPV6的拓扑,再未来我们会提供IPV6 M2M的能力给客户。 室外定位技术与GIS 定位技术也是物联网时代非常重要的一个技术。在物联网时代,每一个物都需要一个时空的基准。无论是飞机、车辆、手机还是物联网设备,移动的或者不移动的,都需要准确的位置。所以物联网时代的定位技术,需要比移动互联网时代更加精确、低功耗,高覆盖。从室内到室外,需要多种无线技术、传感技术以及先进的融合算法。 目前手机的定位技术主要有几类。第一类是GNSS,包括GPS、北斗或者伽利略等,还有各种各样的滤波算法和融合算法,以及一些网络辅助化的定位技术。第二大类是网络定位技术,比如全球IP定位,全球基站定位,全球wifi定位,逆地理编码服务,运动状态检测等,还包括一些离线定位的服务。还有一类就是传感器辅助,比如行人航迹推算或者导航航迹推算,在没有GPS或网络的情况下,还能够继续推算人或者车的轨迹。最后一类是地图匹配导航,你可以在云上,也可以在端上做地图匹配,比如一个轨迹,显然跟地图上的某条路是匹配的,就可以把你的定位漂移给拉回来。 车载的定位技术也分成几类,包括VIO/SLAM、语义定位、多传感器融合以及高精地图技术。在手机定位技术的基础上,发展车载的定位技术和导航技术。未来车载的定位,一定比手机的定位要求覆盖更精准,更低的覆盖面积,更多的信息特征。 定位技术和数据技术,未来会形成很多数据的闭环。定位技术和数据服务之间相互进行不断的校准,然后让整个物联网,比如说城市和空间里面地图数据和定位技术越来越准确,原来因为GPS漂移产生的一些误差,也因为大量的众包数据而得到了校准。 讲完了室外定位技术,我也略微讲一下跟定位技术配合的GIS技术。GIS技术是一种软件,主要是把地图信息,道路信息,还有高层信息等等联合起来。GIS技术主要有几大组成部分,包括数据采集、管理、分析和应用。 GIS技术经过几十年的发展,从萌芽期基本上进入了成熟期。GIS和IoT的关系,将会变成IoT提供实时数据,GIS提供数据的采集和分析引擎能力,这些能力会帮助物联网的应用进行更好的处理和展示。比如一个智能城市的管理里面,IoT提供大量的动态数据,比如人、车、物的数据,雨雪天气的数据,还有很多视频的数据,然后GIS提供大量时空基准的数据,这样就能够帮助解决很多商业的应用,比如城市雨洪,污染扩散,综合管网,城市沉降等等。 室内定位技术 我们下面说说对物联网很重要的室内定位技术。常见的几种室内定位技术有WiFi、蓝牙和ZigBee的三角定位,还有地磁的定位技术等。 绝大部分的定位技术都是用多边定位,包括WiFi、蓝牙RSSI、超声波、UWB、ZiBee技术等,现在也有新的蓝牙5.2的AOA技术、RFID技术,能够为室内定位提供更加精准的定位技术。 其他的一些定位技术,比如说指纹定位技术,室内的惯性导航定位,以及三维视觉的成像定位,比如说室内机器人用的激光雷达、毫米波、超声波,这样的定位技术,未来应用在工厂里面,像自动上料、仓储运输、物流送料等等,都是让工业4.0能够得到应用的重要的基础技术。毫无疑问,室内定位将会是多种传感器或者是多种技术的融合,从而提供室内机器人等物体的定位技术,成为工业4.0的一个基础产品。 讲到室内定位,也会讲到一些室内三维坐标的信息,室内三维坐标的信息主要是通过BIM软件,能够把一个楼的这个管廊啊,墙面啊等等三维数据,都描述在一个数据库里面,就像GIS描绘室外的地理信息系统一样。 GIS信息、IoT信息、定位信息放在一起,就可以基于室内定位技术和室内的时空技术,通过IoT的数据平台,把设备的实时数据输入到BIM(Building Information Modeling)系统里面,把人员事件等都在数字世界里面展现出来,从而能够进行一些数据的分析,然后解决很多场景问题,比如说消防疏散的紧急预案,商场里的人员行为分析,人流热力图,甚至哪个地方要多开点暖风,都能够有实时在线的理解。 所以说把物理世界和数字世界用室内定位提供时空基准,然后加上BIM信息,能够把这个数据分析和展现出来,从而能够建立一个这个更好的物联网的应用。前面讲的这几个技术,分别解决了不同的问题,让我们能够准确的描述一个数字世界。 区块链、数字孪生体 接下来我们简单聊一下区块链,区块链到其实是一个什么意思?打个比方你去买房子,原来你需要一个账本,记录这个交易和合约的条款,必须有一个非常可信的第三方,能够把银行、经理人和你自己的钱,都放在一个账本里面,来创建一个交易。但是呢,这个账本持有者是可以篡改它的。 为了让账本不可篡改,你可以怎么办呢?首先每一笔记录加上签名和时间戳,并记录到账本,要改一个记录,就要把前面的所有的记录都改掉,那么在交易了几百次以后,这个交易记录就很难去篡改了。 其次是要多处备份,交易中的每一个人都有一份自己的账本副本,放在物联网里面,就是每个人都有一个边缘计算节点。要改一个账本容易,但是改多个账本是比较难的,从而把信任移到了网络上去中心化,用群体的共识机制来建立信任。 物联网天然的有这样一个技术,因为基本上每个房子都有一个路由器,都有一个边缘计算节点,每个小区都有一个边缘服务器,都可以成为数据交易的账本。比如说有一个人调用了你家里的灯,他调用了多少次,用了多少度电,都会被记录下来,他是无法抵赖的。 阿里云基于蚂蚁区块链,开发了IoT的技术栈,包括数据层、网络层、共识层、合约层、访控层和应用层。我们用边缘IoT联盟链分布式账本网络来采集数据,放到IoT联盟链管理微服务集群里,从而支撑各种各样的区块链物联网的应用。比如说物流服务、仓储服务、金融、保险等等这些数据的消费方,来做数据的交换。 工业互联网还有一个蛮重要的事情就是数字孪生体,它的好处是能够让我们更好地进行人机交互。数字孪生体有很多的应用,例如可视化维护、设备状态可视化、远程互动交互、设备三维操作指南等。比方说采集了一个水泵的数据之后,可以把它的状态实时地叠加到你眼睛看见的这个水泵上面,其他人也能够远程地帮助你识别这里面有些什么问题,从而辅助前线的运维人员变得更加高效。 结语 4G时代是移动互联网时代,最主要的应用是在mobile、social、local。那么物联网时代产生的最有价值的应用是什么?我认为是工业互联网。前面我们讲了,5G解决了通讯大容量的问题,IPV6解决了IP的问题,然后室外定位和室内定位解决了时空基准的问题,区块链解决了数据保护和信任问题。再加上数字孪生体,在数字世界建立了一个又一个digital entity(数字人)。基于这几大技术,我认为未来的爆发,指向最大的一个方向就是工业制造技术。
1、当我们在谈产品化时,我们想的是同一个概念吗? 为了更好地理解这个问题,首先要解释“系统、产品、商品”的定义。 我不太想用百科上的通用定义,如:商品是用于交换的劳动产品,这对我们今天的话题没有指导意义,我尝试用更贴近我们日常工作上下文的方式来给出定义。 系统的定义:各种离散功能组成的功能集合体。 产品的定义:有使用价值且封装良好的可复用功能集合体。 商品的定义:以交易为目的的,有使用价值且封装良好的可复用功能集合体。 举个例子:我用各种零件制作了一个计时系统,具有计时的功能。给身边的小伙伴使用没问题,但拿到市场上去,就会被吐槽的体无完肤,“太丑了,感觉好复杂”。 所以我奋发图强,给这个计时系统加上了好看的表盘和表带,封装成一个颜值高,易操作的产品。看上去专业多了,然后拿到市场上去,就会有人来询盘,多少钱啊老板? 于是我给这个产品定了个价格,就成了一个商品。 由此可见,系统可以转化为产品,产品也可以转化为商品。 系统转化为产品的过程就是产品化。产品转化为商品的过程就是商业化。从系统到产品再到商品,是复杂性逐渐降低,体验逐渐提升的过程。 2、我们开发人员天天做的东西,是不是产品? 我觉得大部分内部系统开发团队做的都是介于系统和产品之间的一种形态。很难将我们现在手头上的几个应用称之为产品。 如果一个应用只能在一个特定场景给一个特定客户使用,这是一个系统,并不是一个产品。 产品应该是能快速复制给多个客户使用的。比如:法务、采购、HRM、财务,等用于公司内部运营的应用。可以说是业务能力的集合体,一般满足内部运营都没什么问题。但拿到外面的市场上去绕一圈,晒一晒,不一定有竞争力。 更不必说这些系统耦合了很多公司内部的特殊逻辑,依赖了内部的组件,牵一发而动全身,很难直接复制出去给另一个公司使用。所以,公司内的很多系统,为了成为真正的“产品”,纷纷开始做产品化的改造。 比如,公司内部项目协作管理平台AONE是很好用的一个系统,但不能说是一个成熟可复制的产品,因此AONE经过产品化的改造,有了云上版本“云效”。 HSF是很好的技术中间件,在公司内部使用广泛,但拿到云上售卖还是得经过产品化封装为EDAS产品。 3、什么团队需要做产品化? 如果你觉得你做了几年的系统,积累了较多有价值的业务能力,在行业里也有竞争力,自信除了现有的使用者,还愿意且有能力服务更多客户的话。你就需要考虑下产品化的事情了。 例子:X产品本来是公司内部使用的办公协作系统,如今产品化向市场开放。 4、开发团队希望将维护的系统产品化,该怎么做? 我手上维护的BUC、SSO、ACL、VDS,这些都是集团内使用广泛的系统。 最近一直在做这些系统的产品化,封装成一个产品叫做MOZI(墨子),两年来,目前这个产品已经服务了集团经济体生态20多个BU的200多个业务;同时作为基础能力输出到数字政府领域,已经在多个业务领域证明了产品价值。 我从我们自己产品化的经验来讲,如果手头已经有个系统,在这个基础上想做产品化的话,比较粗略的分,开发团队做产品化具体可以分为以下几个步骤: 1)产品能力的积累和建设; 2)低成本的可快速复制; 3)优化用户体验。 产品能力的积累和建设 比较容易理解,我们需要提升产品的使用价值,使用价值是客户来衡量的。对标业界竞品,有差距的补全差距,有优势的巩固优势。这个过程是持续改进的。 低成本的可快速复制 产品要有能力快速服务不止一家客户,这里客户使用方式分为saas方式和专有化交付方式,saas方式就必须要支持多租户的能力,专有化交付方式的话,就需要尽可能的降低产品交付的成本,包括需要占用的服务器数量,依赖的三方软件等。 去掉不必要的功能,提供最小模块功能集,最好可以让客户自定义功能集,仅对使用的功能付费。 优化用户体验 这个没啥说的,现在这个时代,颜值就是正义,"dont make me think"。优化视觉,优化交互,优化体验这三件事情要持续打磨。 5、产品化需要多少投入? 产品化是个持续改进,无限接近完美的过程,而不是一蹴而就,从0到1的变化。 技术部做了个A工单系统,给公司内部的客服人员使用。虽然界面很丑,bug一堆,但独此一家,别无分店,客服只能一边嫌弃着一边继续用。 另一个技术部也做了个B工单系统,在产品质量和用户体验上深度打磨,推出以后,口碑越来越好,用户越来越多,大量客服纷纷弃前者而来。 从用户视角看,B工单系统的使用价值比A工单系统高。 换句话说,B工单系统的产品化程度比较高。B比A更像产品。如下图所示,B比A的产品化成熟度更高。 但须知“文无第一,武无第二“,B也没法知道自己的优势能保留多久,也许马上会有其他产品化程度更高的C产品出现。为了保持领先,不得不持续更新自己的产品功能和体验。 6、产品化的建议路径是什么? 当然完整的产品化路径并不仅仅是开发的事情,而是PD、UED、运营、开发、交付、商业一起参与的大工程。 我尝试梳理了下从0到1做产品化的一般路径,不一定对,以后可能还会补充和删减。 1)定义客户和用户,给出客户画像; 2)定义客户需求痛点以及产品主要解决的问题; 3)定义产品核心功能和护城河; 4)明确价值和市场定位; 5)建设产品能力:面向客户; 6)建设配置能力:面向交付; 7)建设开发工具:面向开发(插件生态,小程序生态); 8)降低成本,快速可复制; 9)优化和打磨用户体验; 10)定义商业模式和盈利模式(可选); 11)定义计费方案(可选); 12)建设标杆应用(对于平台类产品适用,如宜搭); 13)联合行业领袖建立标准(头部玩家适用,如office)。 7、终极问题:如何做一个优秀的互联网产品? 既然是终极问题了,我没法给出标准答案了,欢迎大家在留言区互动、探讨。
背景 以电商场景优化用户点击为例,推荐系统的任务是从海量的候选商品中选出用户最感兴趣且最可能点击的商品。为了提升检索的效率,通常分为两阶段来检索。召回/候选生成(Matching/Candidate Generation)阶段根据U2I相关性从整个候选集中筛选出少量的候选商品(比如1000个),常用协同过滤方法。排序(Ranking)阶段根据排序模型预估这小部分候选商品的CTR,排序后展示给用户。 推荐系统中CTR预估的重要性不言而喻,其中个性化是提升CTR模型效果的关键。本文介绍一种全新的排序模型,主要的思想是融合Match中的协同过滤思想,在Rank模型中表征U2I的相关性,从而提升模型的个性化能力,并取得不俗的效果。 搜索场景中用户通过输入搜索词显式地表达用户的意图,而推荐场景中没有这种显式获取用户意图的方式。用户的意图往往隐藏在用户行为序列中,可以说用户行为序列就是推荐中的query。因此,对用户行为序列进行建模来抽取其中的用户意图就非常重要。DIN[1]以及DIEN[2]等后续工作关注用户兴趣的表征以提升模型效果,而我们的工作在此基础上又往前走了一步,关注U2I相关性的表征。U2I相关性可以直接衡量用户对目标商品的偏好强度。可以理解成从用户特征(用户兴趣表征)到U2I交叉特征(U2I相关性表征)的升级。 表征U2I相关性很容易想到召回中的协同过滤(CF)。I2I CF是工业界最常见的方法,预计算I2I的相似度,然后根据用户的行为和I2I相似度间接得到U2I相关性。因子分解(factorization)的方法更加直接,通过用户表征和商品表征的内积直接得到U2I相关性,这里暂且称这种方法为U2I CF。最近有一些深度学习的方法进入到相关领域:比如I2I CF中有NAIS[7],用attention机制区分用户行为的重要性,和DIN[1]的做法相似;U2I CF中有DNN4YouTube[3],把召回建模成大规模多分类问题,也就是常说的DeepMatch。DeepMatch可以看做factorization技术的非线性泛化。我们根据协同过滤中的U2I CF和I2I CF分别构建了两个子网络来表征U2I相关性。 模型介绍 DMR(Deep Match to Rank)模型的网络结构如图所示。仅仅依靠MLP隐式的特征交叉很难捕捉到U2I的相关性。对于输入到MLP中的U2I交叉特征,除了手工构建的U2I交叉特征,我们通过User-to-Item子网络和Item-to-Item子网络来表征U2I相关性,进一步提升模型的表达能力。 User-to-Item网络 受factorization方法的启发,我们用user representation和item representation的内积来表征U2I相关性,这可以看做是一种显式的特征交叉。user representation根据用户行为特征得到,一种简单的方法是做average pooling,即把每个行为特征看得同等重要。我们考虑到行为时间等context特征对行为重要性的区分度,采用attention机制,以位置编码(positional encoding,参考Transformer[4])等context特征作为query去适应性地学习每个行为的权重。其中位置编码行为序列按时间顺序排列后的编号,表达行为时间的远近。公式如下: 其中是第t个position embedding,是第t个用户行为的特征向量,,是学习参数,是第t个用户行为的归一化权重。通过weighted sum pooling,得到定长的特征向量,然后通过全连接层进行非线性变化得到user representation,以匹配item representation的维度。最终的user representation可以定义为: 其中函数代表非线性变换,输入维度,输出维度, 是第t个用户行为的带权的特征向量。 目标item representation直接通过embedding lookup得到,这个embedding矩阵是单独的一个矩阵,用于输出端,和输入端的item使用的embedding矩阵V不同(类似于word2vec[6]中一个单词有输入和输出两种表征)。有了user representation和item representation,我们用内积来表征U2I相关性: 我们希望r越大代表相关性越强,从而对CTR预测有正向的效果。然后从反向传播的角度考虑,仅仅通过点击label的监督很难学出这样的效果。另外,embedding矩阵的学习完全依赖于唯一的相关性单元r。基于以上两点,我们提出了DeepMatch网络(即图中最后侧的Auxiliary Match Network),引入用户行为作为label监督User-to-Item网络的学习。 DeepMatch网络的任务是根据前T−1个行为预测第T个行为,是一个大规模多分类任务,有多少候选商品就有多个分类。根据上述用户表征的形式,我们可以获得前T−1个用户行为对应的user representation,记作。用户在发生这T−1个行为后,下一个点击商品j的概率可以用softmax函数来定义: 其中第j个商品的(输出)表征。目标商品的输出表征实际上就是softmax层的参数。以交叉熵为损失函数,我们有以下损失: 其中代表第I个样本的第j个商品的label,是相应的预测结果,K是不同的分类数,也就是商品数。当且仅当商品j是用户行为序列中的第T个行为。考虑到softmax的计算量太大,正比于商品总数K,采用negative sampling的方法来简化计算,损失变为如下形式: 其中是sigmoid 函数,是正样本,是负样本,k是采用的负样本数,远小于总体商品总数K。DeepMatch的loss会加到MLP最终的分类loss上。DeepMatch网络会促使更大的内积r代表更强的相关性,从而帮助模型的训练。实际上,User-to-Item Network是Ranking模型和Matching模型以统一的方式进行联合训练。这和简单地将召回阶段的match_type、match_score等特征加入到排序模型中不同。召回阶段通常是多路召回,不同召回方式的分数不在同一个metric下,无法直接比较(比如swing和DeepMatch的分数不能直接比较)。DMR通过User-to-Item网络能够针对任意给定的目标商品表征U2I相关性,且可以相互比较。 Item-to-Item网络 User-to-Item网络通过内积直接表征U2I相关性,而Item-to-Item网络通过计算I2I相似度间接表征U2I相关性。回忆一下DIN[1]等模型中的target attention,即以目标商品为query对用户行为序列做attention,区分出行为的重要程度。我们可以把它理解成一个I2I的相似度计算,和目标商品更相似的用户行为商品获得更高的权重,从而主导pooling后的特征向量。基于这样的理解,我们将所有的权重(softmax归一化之前)求和就得到了另一种U2I相关性表达。公式如下: Item-to-Item网络使用additive attention[5]形式计算,区别于User-to-Item的内积形式,可以让增强表征能力。 除了U2I相关性表征,Item-to-Item网络也将target attention后的用户表征输入到MLP中。DMR如果没有U2I相关性表征以及positional encoding,则和DIN[1]模型基本相同。 实验 我们在阿里妈妈的公开数据集,以及1688为你推荐的生产数据集上做了一系列实验,验证模型整体的效果并且探索某个模块对模型的影响。 离线实验 线上实验 我们在1688为你推荐上线DMR模型,对比模型是DIN[1](我们上一个版本的CTR模型),CTR相对提升5.5%,DPV相对提升12.8%,目前已经全量。 总结&展望 我们的论文Deep Match to Rank Model for Personalized Click-Through Rate Prediction被AAAI-20以oral的形式录用,paper原文地址:https://github.com/lvze92/DMR DMR提供了一个Matching和Ranking联合训练的框架,U2I相关性表征的模块可以很容易嵌到现有的CTR模型中,相当于在你原来的模型上加了一些有效的特征。我们后续的CTR模型迭代会基于DMR的框架不断加入新的改进。 我们是阿里巴巴CBU技术部算法团队,招推荐算法工程师,欢迎投递简历至lvze.lz@alibaba-inc.com 主要参考文献:[1] Deep Interest Network for Click-Through Rate Prediction - KDD18 [2] Deep Interest Evolution Network for Click-Through Rate Prediction - AAAI19 [3] Deep Neural Networks for YouTube Recommendations - ResSys16 [4] Attention Is All You Need - NIPS17 [5] Neural Machine Translation by Jointly Learning to Align and Translate - ICLR15 [6] Distributed Representations of Words and Phrases and their Compositionality - NIPS13 [7] NAIS - Neural Attentive Item Similarity Model for Recommendation - TKDE18
01 概 述 近日,Tair团队的一篇论文——HotRing: A Hotspot-Aware In-Memory Key-Value Store 被FAST'20 Research Track接收 (USENIX Conference on File and Storage Techniques (FAST),CCF A类会议,存储领域顶会,2020年接受率16%)。 HotRing是Tair团队的创新性纯内存KV存储引擎设计。其引擎吞吐性能可达600M ops/s,与目前最快的KVS系统相比,可实现2.58倍的性能提升。HotRing最重要的创新点是:极大的提升了KVS引擎对于热点访问的承载能力。这对于KVS系统的稳定性以及成本控制尤为关键。 为了方便大家更通俗全面的理解这篇论文,本文将从阿里巴巴的双十一零点峰值讲起,介绍峰值下数据库整体架构所面临的热点问题,再介绍Tair团队在解决热点方面一次次的优化提升,最后介绍Tair的创新性引擎HotRing。 02 背 景 零点峰值 2019年天猫双11再次刷新世界纪录,零点的订单峰值达到54.4万笔/秒。有订单就涉及到交易,有交易就需要数据库的事务保证,因此阿里巴巴数据库将在这时面临巨大的冲击。 现实往往更加严峻,在业务方面,一次订单随着业务逻辑在后端会放大为数十次的访问;在客户方面,大量的客户只是疯狂的访问,并没有生成订单。因此,在双11的零点峰值,业务实际的访问量级是10亿次/秒。 Tair作为高并发分布式的KVS系统,在这时发挥了重要作用。如下面的逻辑图所示,Tair作为数据库的分布式缓存系统,缓存了大量的热点数据(例如商品,库存,风控信息等),为数据库抵挡了巨大的访问量。2019年双11,Tair的峰值访问为9.92亿次/秒。 热点问题 在业务层面,热点问题很好理解,最典型的就是双十一零点秒杀。这会导致数据访问呈现严重倾斜的幂律分布。 我们分析了多种业务的数据访问分布,如下图所示,大量的数据访问只集中在少部分的热点数据中,若用离散幂率分布(Zipfian)刻画,其θ参数约为1.22。相似地,Facebook的一篇论文同样也展示了近似的数据访问分布(参考论文[3])。 直观上可以用下图来解释。以苹果新手机发售举例。手机的库存等信息只存在KVS的一个节点中。当新手机发售后,大量的果粉疯狂进行抢购下单,业务的访问量基本都聚集在这一个节点上。节点可能无法承载大量的热点访问,进而引发系统崩溃,严重影响用户体验。 热点优化 为了保证双十一丝般顺滑的购物体验,Tair针对热点问题进行了多层优化: 客户端缓存:通过预先标记热点,设置客户端层面的缓存。以上图来理解,就是将访问在业务层面返回,直接减小了KVS系统的负载压力。 热点散列技术:通过将热点数据备份到多个KVS节点上,分摊热点访问。以少量成本的资源与系统开销,换取了成倍的系统承载力。 RCU无锁引擎:通过采用Read-Copy-Update的方式,实现内存KV引擎的无锁化(lock-free)访问(参考论文[1,2])。成倍提升KVS引擎的性能,进而提高热点的承载力。 HotRing:在RCU无锁引擎基础上,我们进行索引结构的热点感知设计,提出了一种名为HotRing的新型热点感知内存KVS。HotRing可动态识别热点,并实时的进行索引结构的无锁调整,对于幂律分布场景实现成倍的引擎性能提升。 经过十年的技术沉淀,我们已将集团Tair数据库的缓存技术释放到云上,普惠大众,即“阿里云Redis企业版”。 03 HotRing 现有技术 现有的内存KVS引擎通常采用链式哈希作为索引,结构如下图所示。首先,根据数据的键值(k)计算其哈希值h(k),对应到哈希表(Hash table)的某个头指针(Headi)。根据头指针遍历相应的冲突链(Collision Chain)的所有数据(Item),通过键值比较,找到目标数据。如果目标数据不在冲突链中(read miss),则可在冲突链头部插入该数据。 在链式哈希索引结构中,访问位于冲突链尾部的数据,需要经过更多的索引跳数,即更多次的内存访问。很直观的想法是,如果可以将热点数据放置在冲突链头部,那么系统对于热点数据的访问将会有更快的响应速度。 但是,数据在冲突链中的位置由数据的插入顺序决定,这和数据的冷热程度是互相独立的。因此,如图所示,热点数据(Hot Item)在冲突链中的位置是完全均匀分布。 设计挑战 理想的设计也很直观,就是将所有热点数据移动到冲突链的头部。但有两方面因素使得这个问题非常难解。一方面,数据的热度是动态变化的,必须实现动态的热点感知保证热点时效性。另一方面,内存KVS的引擎性能是很敏感的(一次访问的时延通常是100ns量级),必须实现无锁的热点感知维持引擎的高并发与高吞吐特性。 HotRing整体设计 HotRing在传统链式哈希索引基础上,实现了有序环式哈希索引设计。如下图所示,将冲突链首尾连接形式冲突环,保证头指针指向任何一个item都可以遍历环上所有数据。然后,HotRing通过lock-free移动头指针,动态指向热度较高的item(或根据算法计算出的最优item位置),使得访问热点数据可以更快的返回。 下面通过如下4方面进行介绍: 设计1:为什么要实现为有序环? 设计2:如何动态识别热点并调整头指针? 设计3:如何保证无锁的并发访问? 设计4:如何根据热点数据量的动态变化进行无锁rehash? 设计1——有序环 实现环式哈希索引后,第一个问题是要保证查询的正确性。若为无序环,当一个read miss操作遍历冲突环时,它需要一个标志来判断遍历何时终止,否则会形式死循环。但是在环上,所有数据都会动态变化(更新或删除),头指针同样也会动态移动,没有标志可以作为遍历的终止判断。 利用key排序可以解决这个问题,若目标key介于连续两个item的key之间,说明为read miss操作,即可终止返回。由于实际系统中,数据key的大小通常为10~100B,比较会带来巨大的开销。哈希结构利用tag来减少key的比较开销。 如下图所示,tag是哈希值的一部分,每个key计算的哈希值,前k位用来哈希表的定位,后n-k位作为冲突链中进一步区分key的标志。为了减小排序开销,我们构建字典序:order = (tag, key)。先根据tag进行排序,tag相同再根据key进行排序。 下图比较了HotRing与传统链式哈希。以itemB举例,链式哈希需要遍历所有数据才能返回read miss。而HotRing在访问itemA与C后,即可确认B read miss。因此针对read miss操作,链式哈希需要遍历整个冲突链;而HotRing利用字典序,不仅可以正确终止,且平均只需遍历1/2冲突环。 设计2——动态识别与调整 HotRing实现了两种策略来实现周期性的热点识别与调整。每R次访问为一个周期(R通常设置为5),第R次访问的线程将进行头指针的调整。两种策略如下: 随机移动策略:每R次访问,移动头指针指向第R次访问的item。若已经指向该item,则头指针不移动。该策略的优势是, 不需要额外的元数据开销,且不需要采样过程,响应速度极快。 采样分析策略:每R次访问,尝试启动对应冲突环的采样,统计item的访问频率。若第R次访问的item已经是头指针指向的item,则不启动采样。 采样所需的元数据结构如下图所示,分别在头指针处设置Total Counter,记录该环的访问总次数,每个item设置Counter记录该item的访问次数。因为内存指针需要分配64bits,但实际系统地址索引只使用其中的48bits。我们使用剩余16bits设置标志位(例如Total Counter、Counter等),保证不会增加额外的元数据开销。该策略的优势是,通过采样分析,可以计算选出最优的头指针位置,稳态时性能表现更优。 这一部分的细节设计有很多: 采样分析策略如何选出最优位置; 针对RCU更新操作的采样优化, 热点继承防止冷启动。 本文不再详细描述,有兴趣请参考HotRing论文。 设计3——无锁并发访问 Tair的RCU无锁引擎是HotRing的设计基础。参考论文[1,2]对如何实现无锁链表进行了详细讲解,后续的所有无锁设计基本都沿用了他们的策略。有兴趣可以读一下。这里我们举一个典型的并发示例进行介绍。 如下图所示,在链A->B->D上,线程1进行插入C的操作,同时线程2进行RCU更新B的操作,尝试更新为B'。线程1修改B的指针指向C,完成插入。而线程2修改A的指针指向B'完成更新。两个线程并发修改不同的内存,均可成功返回。但是这时遍历整条链(A->B'->D),将发现C无法被遍历到,导致正确性问题。 解决措施是利用上图(Item Format)中的Occupied标志位。当线程2更新B时,首先需要将B的Occupied标志位置位。线程1插入C需要修改B的指针(Next Item Address),若发现Occupied标志位已置位,则需要重新遍历链表,尝试插入。通过使并发操作竞争修改同一内存地址,保证并发操作的正确性。 利用相同原理,我们保证了头指针移动操作,与CRUD操作的并发正确性。因此实现了HotRing的无锁并发访问。 设计4——适应热点数据量的无锁rehash 如背景所述,对于极端的幂率分布场景,大量的数据访问只集中在少部分的热点数据中。因此只要保证热点数据可以位于头指针位置,冲突环即使很长,对于引擎的性能表现并不影响。引擎性能的降低,必然是因为冲突环上存在多个热点。因此HotRing设计了适应热点数据量的无锁rehash策略来解决这一问题。 HotRing利用访问所需平均内存访问次数(access overhead)来替代传统rehash策略的负载因子(load factor)。在幂率分布场景,若每个冲突环只有一个热点,HotRing可以保证access overhead < 2,即平均每次访问所需内存访问次数小于2。因此设定access overhead阈值为2,当大于2时,触发rehash。 rehash过程分为3步进行,结合上面4图进行说明,图一为哈希表,哈希值在rehash前后的变化。剩余三图为rehash三个过程。 初始化(Initialization):首先,HotRing创建一个后台rehash线程。该线程创建2倍空间的新哈希表,通过复用tag的最高一位来进行索引。因此,新表中将会有两个头指针与旧表中的一个头指针对应。HotRing根据tag范围对数据进行划分。假设tag最大值为T,tag范围为[0,T),则两个新的头指针对应tag范围为[0,T/2)和[T/2,T)。同时,rahash线程创建一个rehash节点(包含两个空数据的子item节点),子item节点分别对应两个新头指针。HotRing利用item中的Rehash标志位识别rehash节点的子item节点。 分裂(Split):在分裂阶段,rehash线程通过将rehash节点的两个子item节点插入环中完成环的分裂。如图(Split)所示,因为itemB和E是tag的范围边界,所以子item节点分别插入到itemB和E之前。完成两个插入操作后,新哈希表将激活,所有的访问都将通过新哈希表进行访问。到目前为止,已经在逻辑上将冲突环一分为二。当我们查找数据时,最多只需要扫描一半的item。 删除(Deletion):删除阶段需要做一些首尾工作,包括旧哈希表的回收。以及rehash节点的删除回收。这里需要强调,分裂阶段和删除阶段间,必须有一个RCU静默期(transition period)。该静默期保证所有从旧哈希表进入的访问均已经返回。否则,直接回收旧哈希表可能导致并发错误。 04 总 结 内存键值存储系统由于高性能、易扩展等特性在云存储服务中广泛使用。其通常作为必不可少的缓存组件,以解决持久化存储系统或分布式存储系统中的热点问题。 但分析发现,内存KVS内部的热点问题更加严重,其数据访问分布同样服从幂律分布,且访问倾斜愈加严重。现有的内存KVS缺乏热点优化意识,部分数据节点可能无法承载大量的热点访问,进而引发系统崩溃,严重影响用户体验。 在本论文中,我们进行索引结构的热点感知设计,提出了一种名为HotRing的新型热点感知内存KVS,针对幂率分布的热点场景进行大量优化。HotRing可动态识别热点,并实时的进行索引结构的无锁调整,进而提供高并发高性能的无锁化访问。 与传统的内存KVS索引相比,HotRing采用轻量级的热点识别策略,且没有增加元数据存储开销。但在幂律分布的应用场景中,HotRing的引擎吞吐性能可达600M ops/s,与目前最快KVS相比,可实现2.58倍的性能提升。 参考 [1] John D Valois. Lock-free linked lists using compare-and-swap. (PODC 1995) [2] Timothy L Harris. A Pragmatic Implementation of Non-blocking Linked-lists. (DISC 2001) [3] Berk Atikoglu. Workload Analysis of a Large- Scale Key-Value Store. (SIGMETRICS 2012)
2020年11月
2020年04月
2020年03月