
欢迎关注大淘宝技术!这里将为你第一时间分享大淘宝最核心和最NB的技术热点~
新年好!岁末年初,温故而知新。2022年,大淘宝技术公众号一共发布了 248 篇内容,在知乎上做了 80 个精彩回答,在头条、百家、掘金、思否、开源中国等内容阵地都有我们努力布道的身影,也收获了来自社区的各项影响力评选肯定,更开心的是,收到了开发者们的点赞与认同。🍻过去一年,我们尝试在分享的过程中,对自己做过的工作进行系统性的总结和提炼,升华自己对技术深度的理解;更希望能够与同行交流互动,共同关注业务的差异性、技术思考的不同路径、技术的困难挑战以及对未来的思考,彼此成就,共同成长。基于此,我们将一整年的精华内容梳理合并,重磅推出大淘宝技术年货《2022技术人的百宝黑皮书》。在2022年的技术年货中,你将看到以下内容——一、大淘宝各技术栈工程师重新定义和解决问题,分享消费点滴改变背后的技术深意今年我们将业界关注&大淘宝主攻的技术栈做了梳理更新,分为终端技术、服务端技术、3DXR技术、数据算法、音视频与图像技术以及技术质量6个重点篇章,我们深知技术真正在产业中发挥作用,不是去解决一个惊天动地的大问题,而是去打磨10000个琐碎的小问题。例如今年前端和移动端在技术上开始融合,在技术架构、工具体系的研发上都进行了贯通设计;通过融合多模态信息的内容召回模型,更好地兼顾了淘宝推荐的泛化性;深度优化直播的开播规范,视频的处理增强、编码传输、播放渲染等全链路设计……我们尝试用技术创新去探索数字时代下最理想的消费方式和生活方式,让日常消费生活更加美好,也给业界开发者们带来时新的思考和技术分享。二、大淘宝技术工程师们的成长经验沉淀,从幼稚到成熟,就是从不负责任到承担责任的过程技术人做事情,判断力和分寸感很重要。有时候你遇到的困难和问题,可能别人早就经历过、克服过,并沉淀了与之匹配的“判断力”和“分寸感”。最短的学习路径,就是知道它,学会它,实践它。我们梳理了来自大淘宝技术工程师们的一些经验和思考,例如程序员的职业规划路径和职业操守,系统思考业务问题的方法论等,还有他们在多年职场经验中淬炼出的“金玉良言”,给同路人一些参考价值和方向。三、50 余位工程师推荐学习的GitHub项目,入门级经典库or框架,勤学勤练共同成长学习是一个不断精进的过程,没有standard destination。来自大淘宝技术的 50 余位工程师,通过他们自身学习成长和技术精进的经历,给大家推荐了一些GitHub上各领域经典的学习练手项目,覆盖常见常用的库、方案、框架、环境等,作为参考,一起成长。四、2022大淘宝技术顶会 paper 全文,窥见相关领域下一步最新方向按照计算机协会定义的CCF-A类会议和期刊,我们精选出在数据挖掘与机器学习领域、计算机视觉及多媒体等领域里的11篇顶会paper,涵盖了OSDI 2022 、SIGIR 2022 、ICIS 2022等多个国际会议。对相关领域感兴趣的技术从业者或许能窥见到自己这个领域下一步的热门方向。当一项技术逐渐“看不见”的时候,就是这项技术真正走向成熟的时候。当一种氛围逐渐“被习惯”的时候,就是这个氛围真正形成文化的时候。好的技术氛围就是——分享与交流不止于内部团队,不止于业务形态相似,我们期望在不同业态、不同背景的碰撞和交流中,给彼此带来更深的技术理解。一起来阅读《2022技术人的百宝黑皮书》吧!希望小伙伴们在新的一年里收获满满,成长多多。也欢迎大家转给有相同兴趣的同事、朋友,一起切磋,共同成长。部分目录展示如何下载?点击跳转《2022技术人的百宝黑皮书》,点击下载。温馨提示电子书目录均可实现标题跳转,感兴趣的内容点击标题即可一键传输。本书下载无需注册和登录账号,获取链接即可下载文件大小 66M,手机党均可放心下载~
不管是什么行业,无论是工作还是学习,我们经常会思考一个问题:对于目前的职业来说,最重要的能力是什么?今天我们邀请了 4 名淘系技术的工程师,给大家分享一些他们认为最重要的能力,希望能够为你提供一份参考。01 - 淘系技术部 - 繁易对写代码始终充满兴趣,这是我一切的源动力。过往 - 从 HR 专业学生到程序员繁易我是一名半路出家的程序员。高中读的文科,大学选的人力资源管理专业,生活如果按部就班的下去,这个世界或许会多一位叫繁易的 HR 或者猎头,波澜不惊。引导我走上程序员之路的故事也很简单,2014 年刚入学,大一课程少,期末时便在网络上自学起了 C 语言,当时没有电脑就在手机上的 C 语言软件敲着简单的 if/else,就这样我入坑了。往后的半年中,编程的学习也并没有像想象中的那样顺利。学习进度停滞不前。我还清楚的记得当时的自己:“会用20门语言输出Hello World!”,“别人一周就能上手的 BootStrap 我学了四个月” 等诸如此类的事情。虽然整个过程中极其令人丧气,但我始终知道,我对写代码这件事情是抱有极大的兴趣的。在当时同龄的同学已经纷纷在准备考证、找实习之类的事情,而我依旧乐此不疲。我想,如果不是因为真的有兴趣,我大概会放弃编程,选择做一些“更适合”自己的工作。成长 - 从“年轻工程师”到阿里前端技术专家在学写代码半年后,偶然的一个契机,我找到了愿意带我写代码的同学,并磕磕绊绊的完成了第一次 Ajax 操作。那之后的我仿佛开窍了一般,编程世界的大门,终于向我敞开,任我探索和遨游。直到有一天,我遇到了瓶颈。2016 年的前端,技术日新月异,当时的我颇有一些手足无措,总觉得时间不够用。于是在参加当年的 JSConf 时,遇见了当时前端圈的名人贺师俊(Hax),于是便壮着胆上去交流了一番。对话的内容我只记得个大概,在交流结束时我向贺老致谢,他回复我说:“学东西不要有那么强的功利心,而且对于我来说,能帮你们这些年轻的工程师就非常好了。”在那时候我发现,对编程的兴趣已经托着我从学生过渡到大家眼里“年轻的工程师”了,我想我会继续带着兴趣走下去,尽全力去 Coding 就行。时间一晃眼已经过去了 5 年,当年的学生,也从“年轻的工程师”蜕变成了阿里的前端技术专家,身份在变、环境在变、技术在变,只有对代码的兴趣没有改变。开源 - 从 Node.js 使用者到 Node.js Collaborator由兴趣驱动的源动力,往往是纯粹且不掺杂利益的,而在软件世界中,兴趣驱动与开源精神是高度契合的。在 2020 年冬季到来的时候,我遇到了新的瓶颈,想学习更多 Node.js 的知识却不知从何下手。这一次我的选择是参与 Node.js 开源社区的协作。我开始阅读 Node.js 的源码,解决 Github 上的 Issue 等。在事后提名 Node.js Core Collaborator 时,我统计了一下,两个月的时间内我一共提出了 50+ 的 Pull Request。现在回头来看,这些与实际工作“无关的兴趣”,最终支撑着我走的更远,现在的我在 Node.js 架构团队,全职负责着 Node.js 的工作,个人的眼界也开拓了许多。总结 - 兴趣与代码在别人眼里,我是枯坐一整天的“怪人”,在我的脑海中,我是“键新世界(阿里 2018 年校招 Slogan)”的程序员。因为是兴趣,所以获得成长时会快乐,而停滞不前时也不会焦虑与抱怨。对代码的兴趣支撑我走到了现在,我想后面也会是一样。这就是我眼中程序员最重要的能力与我的经历。02 - 淘系技术部 - 宝澜在我的角度看,不同阶段不同场景都会产生问题,他们分别对应不同的能力,需要分析问题根本原因并通过不同手段解决,更考验人的综合能力。作为程序员,你认为最重要的能力是什么?新手刚入门时,需要掌握写代码的能力,熟悉工作后,我们不能仅仅满足于是代码的搬运工,还需要具有深入分析问题和精准解决问题的能力。这个能力不是纵向某一个领域,更类似于思考问题的方式。在项目合作、业务沟通、个人成长、人际交往、绩效考核各类场景,我们都会遇到一个共有的名词”问题“,由于“问题”产生的背景、原因和影响都不同,如何分析问题个根本原因并通过各种手段去解决它,非常考验我们个人的综合素养和能力。为什么你会认为这个能力是最重要的?掌握了这份能力,(你就是勇敢牛牛,不怕困难!)即使以后不是程序员,对我们其他的职业发展方向也会有很多帮助。你通过什么事情认识到这点?总结工作中的规律并将这类经验抽象化。生活中的任何事物都有其生命周期,无论是食物、工作还是代码,都存在开始,发展,结束的过程。而影响我们发展的周期和发展趋势中,最重要的影响因素就是“问题”,我们能否发现阻碍我们发展的“问题”,寻找产生的原因并修复它,决定了我们这个函数是正趋势还是负趋势。你现在是否拥有这样的能力?如果没有,你是怎么学习调整的? 现在我个人还不完全具备这种能力,因为这个能力它是伴随着我们对“未知”的深入了解才逐渐增强的。当我们在一个工作中遇到了困难,没办法突破时,其实往往是因为我们对它的不了解或者是我们对未知事物的恐惧,能我们为了发现这个“问题”寻找解决这个问题的“方法”时,就需要不断学习学习学习,俗话说“知己知彼百战百胜”,了解对手,才有机会战胜对手,才能找到对手的“痛点”和“软肋”,而学习的过程,就是我们寻找问题产生原因的过程,找到"软肋",就是我们找到解决方法的过程。一般我会通过这样一个流程去提升:分析认清问题:先好好审题再答卷,不要停留在“表面”解决寻找已有方案:先看别人怎么解决,如果没有,寻找问题之间的共性并关联起来,产出自己的解决方案;拆解问题:大矛盾自己一个人解决不了,就将其拆解,分清主次,或者寻找能帮助解决问题的人;平时多积累经验,学习别人的思维方式;简历投递渠道阿里拍卖,万亿市场,创新赛道!拍卖技术部-技术质量团队 期待你的加入。联系邮箱:sanxiao@taobao.com03 - 淘系技术部 - 柳千于我而言,最重要的能力是「好奇心」,或者换个词叫「求知欲」、也可以叫「探索精神」。保持好奇心我是一个充满好奇心的人,我喜欢问为什么?在我五年多的职业生涯中,有接近 2/3 时间在做 Cloud IDE 相关的工作,可以说正是好奇心驱使我做了这么多年,以至于有段时间完全忘记了自己本职是一名「前端工程师」。记得那个著名的前端面试题吗?—— 从输入 URL 到页面展示到底发生了什么?我想知道从按下「.」符号到 IDE 弹出提示框之间到底发生了什么? 为什么 VS Code 相比同类技术栈的产品这么快?Refactor 是如何实现的?当你尝试自己去解答某个问题时,一定是将这个问题拆解为很多个小问题,一步一步深入下去,最后再找到答案。重复这个过程很多次会不断的锻炼你的思维模式,也一定会学习到优秀的架构设计、更好的源码阅读与调试技巧、性能优化技巧等等...软件行业每一个看起来很小的点,深入下去都包含着非常复杂的背景和设计。可能有人会问为什么我一个「前端工程师」要了解这些东西,前端不是应该只关心切图画网页吗?还是那句老话,我先是一个工程师,然后才是前端工程师。不设限前端只是软件开发领域中很小的一部分,如果我只是习惯性的将自己的思维限制在「前端」这个领域,那么很难获得真正的成长。保持对其他领域的好奇心与求知欲,不要给自己太多诸如「xxx 工程师不需要知道这些」的限制。前端程序员可以了解游戏领域的知识吗?后端程序员可以了解音视频处理吗?客户端程序员需要了解高并发、容器化吗?如果你只需要一技傍身来解决生存问题,那基本是不需要的。但我还是建议对自己感兴趣的事物保持好奇心,勇于探索各种不了解的领域,在这个过程中不断吸收新的知识、技能,也许收获会比单纯得到一个问题的答案更多。放大到程序员这个职业也是一样,不要将自己陷入程序员的思维定式中,觉得「程序员应该xxx,不应该xxxx」。我认识的许多优秀的工程师涉猎非常广泛,摄影、健身、游戏、B 站 Up 主、视觉设计。我也认识几个优秀的设计师能写一手代码、画 PRD。虽说隔行如隔山,但我相信程序员的学习能力都不会太差,适当的接触其他领域给自己带来的是全方位的成长。04 - 淘系技术部 - 福豆信息技术行业的发展日新月异,每天都会出现新内容,一直保持学习的状态非常重要。保有好奇心的重要性信息技术区别于传统行业,发展很快,新技术日新月异,昨天是传统的统计学习,今天就是深度学习,明天可能就是量子计算。在这个“长江后浪推前浪”的大潮中,保持对新事物的好奇心和探索就显得尤为重要。好奇心可以让我们主动接触新技术,而不是被迫的后知后觉,可以让我们的日常工作变为自身的兴趣,而不会觉得工作是个负担,只为了完成绩效,同样好奇心也可以让我们的工作做的更加出色,不仅仅满足于完成和结束。在我长期学习和工作中,逐渐感觉一直保持对新事物的兴趣可以让我忘掉加班加点的疲劳,觉得一天的思考让自己在项目或者业务上迈出了一小步,会觉得比较有成就感,在一步步推动一件事情发展。比如以前完成第一个研究课题,又或者刚开始工作时接触第一个比较有挑战性的项目时,可以直接参考的解决方案都屈指可数,要把一个方案落地或者完善成论文都不是一蹴而就的。在这个过程中,我遇到过许多想放弃的时刻,明明有更稳健、风险更小的任务,为什么我要偏偏走上这条路,也成了每天反问自己的问题。幸运的是,我仍然保持着本科学生时代对知识的好奇,想想自己在这个探索的过程中还可以满足自己的兴趣,能够提升自己解决问题的能力,并不是仅仅为了完成任务,从而让自己在困难的时候坚持了下去,也在项目的后期更加专注解决问题本身带来的喜悦感和成就感,而不是关注功利性的东西,进而最后让自己满意的看到努力的结果。保持好奇心可以使工作和生活处于一个良性循环的过程,将自己的工作转变为探索未知的兴趣,融入自己的生活一部分,也同时让好好工作真正为生活服务。此外,基于好奇心促成项目的成功,让我对于未来的业务或者项目更加有自信,对后面的新事情更加有激情,不疲倦。每天不忘学习新技术,一点一点积累,也不会被落伍,让大脑时刻进步,掌握学习学习的方法而不仅是完成一项任务。结语如上,没有永恒最重要的能力,只有对于不同阶段来说相对更重要的能力。保持好奇心,保持学习的状态,去面对不同阶段的不同挑战吧。
有前端同学问我们,最近感觉学习遇到了瓶颈,听说尝试操作一些开源项目可以有效提升,想知道有没有什么好玩又有挑战的项目推荐。我们邀请了3位淘系技术前端工程师,精心筛选出 26 个难度层层递进,好玩且实用的前端项目,比如动画制作、文字识别、可视化图表、H5制作等等,甚至还能把云音乐灰色歌曲解锁播放(嘘!)本篇内容按难度分为【初级篇:8个】、【中级篇:9个】、【高级篇:9个】项目,不同水平不同学习瓶颈的同学可按需选择~初级篇这里一般适用于刚入门前端的同学,一般入门前端都是从html和css开始的,应该让这些同学提起对前端的兴趣,因此初级篇选用几个比较有趣的css库来让同学入门前端。animateanimate是一个css动画库,动画内容一般也是我们所熟悉的,例如渐入、渐出、弹跳、旋转、抖动等特效。ppt里面的许多特效在animate都能找到。而且使用简单,只需引入库并添加相关css即可。演示地址:https://animate.style/GitHub地址:https://github.com/animate-css/animate.cssCSS-InspirationCSS-Inspiration涵盖了css的许多常见的特效,例如布局方式、border、伪元素、滤镜、背景3d等。这些都是css里面十分重要的知识点,当我们学习完css基础之后,可以利用此项目来制作一些常用的特效,也可以巩固基础知识。演示地址:https://chokcoco.github.io/CSS-Inspiration/#/GitHub地址:https://github.com/chokcoco/CSS-InspirationBootstrapBootstrap是Twitter推出的一个用于前端开发的开源工具包。它由Twitter的设计师Mark Otto和Jacob Thornton合作开发,是一个CSS/HTML框架。Bootstrap是可以直接应用到我们平常的项目里的,而且非常方便、功能特别多,也是我们常用的前端UI框架。官网:https://getbootstrap.com/GitHub地址:https://github.com/twbs/bootstraptesseract.js一个js ocr识别库,支持包括中英文等许多语言的图片和视频文字识别,底层封装了Tesseract OCR引擎来实现。视频实时ocr:demo: http://tesseract.projectnaptha.comgithub: https://github.com/naptha/tesseract.jsvanillawebprojects这里一共包含20+个项目,包括表单验证、音视频播放器、打字等小应用,有趣且方便学习。github: https://github.com/bradtraversy/vanillawebprojectsAnt Design基于 Ant Design 设计体系的 React UI 组件库, 提供了丰富的组件,基本满足各种开发使用需求。大部分前端项目都会涉及到React / Vue的组件开发,如何开发封装一个组件是成为一个合格前端的第一步,可以通过模仿Ant Design的写法去学习如何开发一个组件,同时学到框架(React / Vue) + HTML + CSS。github: https://github.com/ant-design/ant-design/ (React版本)https://github.com/vueComponent/ant-design-vue/ (Vue版本)Echarts基于 JavaScript 的开源可视化图表库。Echarts使用数据配置的方式轻松的实现可视化,如果想学习数据可视化,不如从学习Echarts的实现开始。github: https://github.com/apache/echartsJavaScript的学习ECMAScript是JavaScript的语言标准,描述了JS的能力,API,基本实现步骤。目前比较通用的标准是ES6,从最初的版本:June 1997,最新的标准已经到了Edition 12 (June 2021)。每个版本都添加 / 修改了新的语法和API, 作为一个前端开发者,应该关注JS的变化,可以查看最新的ECMAScript提案,即将发生的变化。传送门:https://es6.ruanyifeng.com/ ES6的学习ECMAScript提案:https://github.com/tc39/proposals中级篇当有一定css基础之后,前端工程师肯定需要接触一些js。使用js我们可以根据自己的逻辑,让动画产生不同的效果。在逐渐接触到一些大型的项目时,就可以开始学习前端工程化。particles.js当我们浏览网页的时候,会看见一些网站的背景有粒子效果,看完是不是也很好奇它是怎么实现的呢?在particles.js这个库里面,我们可以制造许多粒子效果,用在网页的背景可是相当好看的。演示地址:https://codepen.io/VincentGarreau/pen/pnlsoGitHub地址:https://github.com/VincentGarreau/particles.jsreveal.jsreveal.js是一款网页ppt,界面优美,又支持许多ppt功能,同时也有许多插件满足前端的需求,例如markdown、code等代码高亮插件。文档地址:https://revealjs.comGitHub地址:https://github.com/hakimel/reveal.jsswiper.jsswiper是目前应用广泛的轮播图插件,拥有许多配置,也有许多切换效果,基本可以满足前端开发中轮播图的需求。文档地址:https://www.swiper.com.cn/api/index.htmlGitHub地址:https://github.com/nolimits4web/swiper构建(Build)工具大型项目需要构建工具进行打包,使用插件,转义等,所以学习如何配置构建工具,如何基于构建工具的能力自定义实现某些功能是很重要的。Webpack和Gulp是两种常见的打包工具。Webpack:https://github.com/webpack/webpackGulp: https://github.com/gulpjs/gulpTypeScript现在的前端开发中,对于TypeScript的使用越来越多,尤其是大型迭代项目中,JavaScript不能满足需要,学习TypeScript会是一个必备的技能。github:https://github.com/microsoft/TypeScriptgithub中也有不少翻译的guide book: https://github.com/jkchao/typescript-book-chineseYeomanYeoman是一个能快速生成一个Web APP的脚手架工具,可以通过Yeoman定义常用的模板APP。github: https://github.com/yeoman/yeomanrrweb一个可以录制并回放任意 web 界面中的用户操作的纯前端的库,完全借助浏览器api来实现录制回放,实现的想法很有趣,对需要录制回放用户行为的需求很有帮助。github: https://github.com/rrweb-io/rrwebvite是一个新型前端构建工具,能够显著提升前端开发体验,和webpack等老牌打包工具不同,vite另辟蹊径,开发环境下使用原生esm,让本地开发更快速。代码相较webpack更轻量,且实现逻辑巧妙,比较适合阅读学习。github: https://github.com/vitejs/viteUnblockNeteaseMusic还在因为云音乐因为版权下架了一批你自己喜欢的歌曲,但是又不想单独为了几首歌换平台而烦恼吗,那你可以试试这个库,可以使用 QQ / 虾米 / 百度 / 酷狗 / 酷我 / 咪咕 / JOOX 等音源把云音乐灰色歌曲解锁播放。github: https://github.com/nondanee/UnblockNeteaseMusic高级篇学完基本的js基础后,我们需要学习一些前端常用框架,例如vue、react,这两个框架是我们工作中经常都要用到的,作为一名前端工程师不得不熟悉其中一个框架。掌握了前端工程化之后,就可以做一些有趣的方向,比如前端智能化,Low Code / no Code, serverless 等。vue-admin-template这是一个vue后台管理系统框架,可以让我们轻松地实现一个后台管理系统,界面也比较美观。文档地址:https://panjiachen.github.io/vue-element-admin-site/zh/guide/GitHub地址:https://github.com/PanJiaChen/vue-admin-templateh5-dooringh5-dooring是一款可视化编辑器,底层是用react写的,使用此工具可以让我们快速生成h5页面。同时通过这个平台也能了解到低代码的相关知识。文档地址:http://h5.dooring.cn/docGitHub地址:https://github.com/MrXujiang/h5-Dooringcanvas-special如果大家对图形学感兴趣,可以学一下canvas,canvas-special提供了许多canvas精美的案例供我们学习。GitHub地址:https://github.com/bxm0927/canvas-special node-question-answering基于Node.js的用深度学习模型去回答问题github:https://github.com/huggingface/node-question-answering相关学习资源imgCook通过设计稿生成组件代码的low code工具github: https://github.com/imgcook/imgcookMidwayMidway 是一个适用于构建 Serverless 服务,传统应用、微服务,小程序后端的 Node.js 框架github: https://github.com/midwayjs/midwayLuckysheet一款纯js实现的在线表格库,功能强大,excel的常见能力都支持,对有表格库集成需求或者要实现类似功能的很有帮助。github: https://github.com/mengshukeji/Luckysheetflowchart-fun是一个高效的绘制流程图和思维导图的工具库,输入文字,就能自动生成一个框图,很有意思。不过是基于既定的英文单词串生成框图,可能上手使用会有一定难度。github: https://github.com/tone-row/flowchart-fun我看见你又在点“收藏”了。记得别吃灰,赶紧学起来吧~有问题欢迎与我们交流~
于一名优秀的技术人员来说,究竟是专精一块技术方向,做到深耕其中所向披靡;还是谋求“什么都能略懂一点”的广度,成为一个全方位的人才?这其实是一个职业发展和学习规划路线的问题,许多同学都有这个困扰。今天我们邀请了 4 名淘系技术工程师,结合他们自身在小厂和大厂的经历故事,给大家分享一些他们在技术人员成长中对于【精】和【广】的选择观点,希望能够对你有帮助。01 淘系技术部 - 应用算法 - 立青“更早地认识自己和自己的方向,能更快地帮助我做出成绩。”我想先撇开这个具体的问题,谈一谈一个程序员的技术发展和职业规划。写代码这件事绝大多数人在喜爱的同时,更多的都是当做一个职业来做的,当然也确实有一部分人真的完全当做爱好,例如前段时间的新闻,HashiCorp 的创始人 Mitchell Hashimoto:“顶级凡尔赛CTO辞职:写代码才最快乐!管理只会影响我搞研发”;著名的Linux创始人Linus大神也是出了名的热爱编程。对于这样的追求(土豪),抛弃功利热爱编程,我觉得在编程上完全可以喜欢什么方面就追求什么方面,是广泛的发现兴趣探索兴趣,还是在一个方向上深入挖掘都没有问题,大可以今天做前端,明天搞开发,后天攒算法,一人搞定全栈。但是对于绝大多数人来说,编程更多的是职业发展道路上一个立身的手艺,在众多专业技术方向上挑了一个自己比较喜欢和热爱的。程序员的发展和众多职位的发展一样,每个人都希望自己能够往"上"走:更专业,更能在职场上发挥自己的作用和影响力,从单兵作战做小事,到带队做大一点的事,再到影响一个领域,影响一个行业。这样的发展单单靠自己各方面都懂,都有涉猎,恐怕是不行的。刚毕业的应届同学可以靠自己的知识储备做自己的标签,久经职场的同学必须靠自己在某些领域做出的成绩做自己的军功章。所以我们越早在某些方向做出自己的成绩,对自己的成长和发展是越好的。我本人是做算法的,算法领域有很多大牛在学校期间就已经找准了自己的发展方向,并做出了成绩,例如caffe作者@贾扬清大神,Taichi作者@胡渊鸣等等。他们的成就就需要及早的找到自己的方向+不懈的努力+亿点点的天赋……对我们绝大多数人来说更需要及早的找到一个热爱的,希望精通的方向并做出成绩。我自己的经历其实不是好的榜样,我抱着做机器人的梦想本科学的机械,在学校的时候对什么都感兴趣,既参加过机器人大赛,参加过结构设计大赛,也和同学做过热力学相关的一些机械设计;一个比较有意思的经历是,当时在学校还做过一个上天失败的小卫星。当时学校的微小卫星研究所发射了一颗皮星(体积很小的卫星)之后,在学校办了一个小卫星的比赛,我们做了一个模拟卫星太阳能电池片自动追踪阳光的卫星模型,在答辩前熬夜完成了,然而在最后测试的时候由于线的固定太过粗糙,在运动中扯掉短路了,把电路都烧了……万幸留下了视频支持最后的答辩。对我后来职业选择影响比较大的是临毕业用Kinect做了一个姿态识别控制无人机飞行的项目,初步接触了视觉算法的一些知识。后来研究生就真正的开始做起了机器人的方向,也是研究生时期做的控制算法和视觉算法的经历让我走上了程序员的道路。在这个过程中自己也是对机器人领域中的SLAM方向产生了兴趣,自己恶补了一些知识,并靠着这些最后走上了这个方向的职业。一方面我觉得自己如果能早一些找到自己的方向并且积累起来一些成果,肯定对自己的发展是要好很多的,但是另一方面每个人也确实需要一个认识自己的过程,但这个过程我想还是越快越好。在这个过程中,我们自己的技术发展就像是一棵树,我们尽可以无限的去展开自己的枝叶,多了解一些不同的方向和知识,但一定记住这是为了让自己的枝头长得更高。02 淘系技术部 - 应用算法 - 朔玥“大部分的精力还是要用于巩固自己的长处上,你一定要有竞争力的依凭。”做一件事是要精还是要广?其实相当于赌博里你是要多压,还是要单押。我们的筹码是有限的,当然我们的精力也是有限的,不可能去做所有的选择。那么这时候,问题就变成了如何去组合投资获得最大化的收益。如果你选择去把所有的筹码压在一个选项上,那么你就一定要去承担选择失误带来一切清零的后果。但是我们也知道,如果你选择到一支潜力项,不费吹灰之力就可以赢到盆满钵满。所以孤注一掷,一定对应的高风险。另外一个选项就是广博,它带给我们的好处是平摊风险,但是平摊风险的同时收益也会被稀释掉。比较好的一种做法,应该是两者相结合,在该广博的时候广博,在该专注的时候专注。作为一名算法工程师,我的建议是,广泛的去涉猎相关领域的知识,以及弱相关领域的知识,因为这些知识全都可以作为你自己的储备,作为你专注行业的加成。比如说,你从事图像识别相关算法研究,那么直接的,除了图像相关的专业知识,一些基础的知识例如代数分析等数学理论,计算机原理与体系结构,甚至色彩,摄影美学等知识,都可以为模型设计带来一定的辅助。除基础的知识以外,相关领域比如自然语言处理当中的时序模型就与视频图像分割问题有很多的共通之处;信号处理中很多变换算子能够直接迁移到图像处理当中。这些知识,都可以作为你研究图像算法的加成。又如,你想不到心理学,消费者行为学以及经济学都可以为我们的工业推荐体系赋能。在洞悉了消费者在购物时的选择动机,心理活动以及当下经济趋势后,算法设计者能够依据这些先验知识有聚焦地设计相关的模块,或是作为趋势因子加入算法当中。用一种更柔软的方式将大众认知融入模型,将表象和理论相结合,从而更好地发挥推荐系统的效能。世界上很多知识底层是相通的,不要去抗拒学一些基础的知识,很有可能那将是你未来抓住机遇的契机。但是,大部分的精力还是要用于巩固自己的长处上,你一定要有竞争力的依凭。广泛的涉猎,精准地融会贯通,提取出你需要的那一部分,转化为自己需要的能量。03 淘系技术部 - 前端技术 - 禾鸟“我个人经历觉得,精进一门技术,不管是对于开发还是其他工作,都是重中之重!”当我刚毕业的时候,在一家小厂做Flash游戏开发,由于当时开发人员配比严重不足(qiong),所以不但要用AS3去写前端游戏UI功能,还需要用NodeJS写服务端游戏逻辑、SQL处理数据读写操作、HTML/CSS/JS写后台配置管理页面,且一度在公司没有招到测试的情况下,所有的功能测试只能通过自测来完成。时间一长就有些膨胀了,自我感觉非常好,觉得自己就是全栈,觉得只有更大的舞台才能配得上自己,进入大厂应该是轻而易举的事情,于是开始往网易、阿里、华为一些大厂投简历,Flash游戏开发、H5游戏开发、Web前端开发、NodeJS开发、服务端开发、测试开发各种岗位都投了遍。结果就是被现实狠狠地打了脸,投的简历石沉大海,少有的几个面试也都是一轮游。后来总结反思了一下,在所有面试中,面试官经常会问这么几个问题:XX原理是什么?XX如何实现?对XX你是如何理解的?对于XX功能是否有更好的解决方案?面对这些问题,发现了自己其实对底层原理一窍不通,所有都还是停留在使用层面。看清楚了这个问题之后,我暂时放下了其他域的学习,专注在前端领域的学习,从基础的HTML/CSS/JS入手,到lodash/JQuery等常用工具库的使用,再到Vue/React等主流框架的使用以及原理的学习,以及ES6、TS等学习掌握。等熟练掌握了这些技能之后,再深入到浏览器工作原理、网络通信机制、前端性能优化、稳定性安全保障等知识点的学习。一步一步从搬砖菜鸟变成了熟练搬砖工,然后继续朝着搬砖砖家努力。总的来说,在小厂,老板当然希望能更省钱,巴不得一个人就能干完所有的活。而对于个人而言,精进一门技术,不管是对于开发还是其他工作,都是重中之重!04 淘系技术部 - 移动开发 - 临境“不要把自己当做业务研发的工具人。”这个问题没有标准答案,角度不同,得出的结论会大相径庭。前不久刚从小厂跳到阿里,简单聊下我自己的感受。小厂会更偏重于业务,因此我们更多的是在实现业务方的需求,日常的研发工作也少有接触特别难或者深的内容。这个阶段不是说基础不重要,但深入学习基础知识可能对你业务研发不会有特别明显的提升。曾有段时间我为了学习算法知识跑去刷 leetcode,刷完两三百题后陷入了迷茫。这些刷题获得的算法知识在大部分的业务场景中不太能用上,虽然它对于在编程中边界条件判断、减少低效代码确实起到了一定的帮助,但相较于投入的时间来说性价比略低。同样道理,学习操作系统、计算机网络、计算机组成原理能让你对整个计算机体系有一个更深层次的认识,但工作的人不比在校生,时间比较有限,如果你不是一个特别特别自律的人,投入时间没有看到明显产出很可能会中途放弃。如果想要在工作的同时更深入学习一些计算机基础知识,可以从你正在做或者感兴趣的性能优化入手。不少性能优化手段背后都伴随着深入的知识,比如之前不少大厂分享的针对 iOS 二进制重排优化冷启动的内容。学习过程中一定会碰到自己不了解的知识,一点点去积累,会比盲目的抱着一本砖头书直接啃更容易坚持。另外,如果是业务研发,请一定锻炼自己对业务的理解程度,不要只停留于需求来了做完就完了,把自己当业务的工具人没有对个人成长没有益处。最后,和大家分享一句我和喜欢的话:“书上没有知识,书上只有信息;知识是在特殊的工作和行动中运用信息的能力。”共勉。结语程序员要精还是要广,并不是一个有着标准确定答案的选择题。精于基础,广于工具,熟于业务,永远保持进步和学习的心态,希望各位都能找到最适合自己的技术成长路径。如题,你们的观点是什么呢?留言区欢迎一起讨论。
国际多媒体顶级学术会议(ACM MM2021)论文接收名录公开!淘系技术内容互动算法团队4篇论文入选!恭喜恭喜~国际多媒体学术会议(ACM MM)是计算机学科公认的多媒体领域和计算机视觉领域的国际顶级会议,也是中国计算机学会(CCF)推荐的A类国际学术会议。ACM MM研究领域覆盖图像、视频、音频、人机交互、社交媒体等多个主题,本次 ACM MM2021 一共收到 1,942 篇论文申请,最终入选 542 篇论文(约 27.9% 的接受率)。淘系技术部内容互动算法团队,聚焦机器学习、视觉算法、NLP算法,端侧智能等领域,依托淘系数十亿级的视频数据,业务上支持淘宝直播、逛逛和点淘,有丰富的业务场景和技术方向,不断探索和衍生颠覆型互联网新技术,团队成员来自海内外知名高校,近两年参加CVPR竞赛获得4项冠军,累积在计算机视觉顶会期刊(如CVPR、TPAMI、TIP等)上发表论文10余篇,技术成果获得国家科技进步二等奖。本次 ACM MM2021 会议,该团队一共 4 篇论文被接收,并有相关技术创新点在淘系业务场景中的应用。后文将详细介绍各篇论文创新点以及落地使用。NO.1 题目Understanding Chinese Video and Language via Contrastive Multimodal Pre-Training基于对比多模态预训练理解中文视频和文本作者雷陈奕,罗时现,刘勇,何旺贵,王家忙,王国鑫,唐海红,苗春燕,李厚强论文创新点&对于行业的影响预训练模型在自然语言处理领域、视觉领域乃至多模态领域已经取得了巨大的成功。本文聚焦于多模态领域中的视频-文本的联合预训练策略,尤其针对中文视频和文本。针对视频-文本进行预训练主要存在以下挑战:第一,和静态图像不同,视频拥有动态的时空序列关系,直接将图像-文本的预训练方法移植到视频-文本领域并不足以捕捉这些复杂的关系信息;第二,预训练模型中广泛存在的视频-文本对齐任务和其它基于掩码的重建任务存在冲突;第三,大规模、高质量的中文视频-文本数据集的缺乏限制了预训练模型在中文领域的发展。由此,本文提出一个基于重建和对比学习任务的多模态预训练模型VICTOR,并建立千万数量级的高质量中文视频-文本数据集。VICTOR以Transformer为主体,设计出基于重建和基于对比学习的七个任务训练模型。基于重建的任务包括掩码语言建模、掩玛句子生成、掩码帧序列建模和掩码句子序列建模四个任务,充分捕捉视频和文本的序列信息和交互信息;基于对比的任务包括对偶的视频-文本对齐、视频内的掩码帧对比学习和视频间的掩码帧对比学习三个任务,在避免简单的视频-文本对齐任务会融合不确定的多模态信息的同时,增强视频内的时空信息融合。VICTOR模型拥有上亿级参数,在构造的千万数量级的淘系视频-文本数据集中进行预训练,并在多个下游任务(如视频文本匹配、视频推荐、标题生成)获得了SOTA的性能提升。VICTOR模型的设计和提出,有效促进了预训练在中文视频-文本领域的进展,并可在多个视频相关业务(如视频推荐、视频分类等)广泛应用。Victor 模型的总体框架:包含模型设计和自监督任务论文相关技术在淘系实际场景的应用我们将VICTOR预训练的视频特征应用到内容检索、推荐、分类、直播等多个领域。各个应用场景均对比场景中已服务的Strong Baseline,具体来说:内容推荐--逛逛内容推荐。效率保持稳定下,3天新发内容占比提升22.81%,冷启动UCTR +4.29%, PCTR +4.72%;内容检索--淘宝经验。跨模态检索,保障相关性评测基础下,无结果率由3.23%降至0.95%内容分类--逛逛内容分类。图文分类精度相对提升3.94%(60.97%->63.37%),视频分类精度相对提升7.33%(51.99%->55.80%)物体检测与匹配 -- 直播看点审核。全品类检测精度相对提升4.83%(89%->93.3%),美妆难例检测精度相对提升8.05%(75.8%->81.9%)论文阅读/下载链接https://arxiv.org/abs/2104.09411NO.2题目Pre-training Graph Transformer with MultimodalSide Information for Recommendation用于推荐系统的融合多模态信息的图预训练Transformer作者刘勇,杨粟森,雷陈奕,王国鑫,唐海红,张举勇,孙爱欣,苗春燕论文创新点&对于行业的影响在个性化推荐领域,尤其是短视频的推荐领域,多模态信息发挥着重要的作用。有效利用item的多模态信息,如文本、视觉等信息,可有效提高推荐的性能,缓解冷启动问题。目前存在的融合多模态信息的推荐模型,都是端对端的基于特定任务的模态融合,消耗资源的同时限制了模型的泛化。另外,在推荐领域,item之间存在着各种相关性(如基于标签的语义相关、基于行为的用户兴趣相关等)。为节省资源,提高模型利用率,同时捕捉item之间的相关性,本文提出一种基于多模态信息融合的图预训练框架PMGT,在捕捉item相关性的同时,指导item多模态信息的融合,并且预训练后的item特征,可应用到多种下游任务,避免了在每个特定任务都要重新融合模态信息的资源浪费和时间消耗。PMGT首先根据item的相关信息,构建出一个item多模态图,其中图的节点为item,边反应了item之间的关系(如被相同用户交互过的item建立边),每个节点的特征由item的多模态特征构成。对图中的每个节点,我们设计出高效并行的采样方法MCNSampling,从图中采样出若干与其相关的节点组成节点序列,并使用基于多样性的transformer框架聚合节点特征,缓解模态融合的冗余性。最后使用基于图结构重建的任务和基于节点特征重建的任务指导相关节点融合和节点自身的多模态信息融合。将PMGT在Amazon和MovieLens公开数据集上预训练并测试,和最新的图预训练模型相比,达到SOTA性能。PMGT使用图的方式指导item的多模态信息的融合,并有效捕捉item之间的相关性,让预训练并不局限于item自身,增强了预训练的item特征的表达能力,可适用于多种下游任务和领域。论文相关技术在淘系实际场景的应用在淘系的短视频推荐领域,我们基于短视频tag信息建立拥有400万节点、4亿边的视频多模态图,并将PMGT预训练后的特征直接应用到短视频的召回阶段,7天新内容占比提升7%。之后可将预训练特征应用到排序阶段,甚至其它的业务场景(如视频分类),并且可将PMGT作为基础框架,使用特定的任务微调模型,达到效果的进一步提升。论文阅读/下载链接https://arxiv.org/abs/2010.12284NO.3题目Shape Controllable Virtual Try-on for Underwear Models(SC-VTON:针对内衣模特的形状可控的虚拟试衣系统)作者高鑫,刘振江,冯尊磊,申成吉,欧开日,唐海红,宋明黎论文创新点&对于行业的影响我们提出了一种形状可控的虚拟试衣网络(SC-VTON),针对内衣模特的试衣任务,使用融合了模特和服饰信息的GAT网络来生成形变后的服饰图片。除此之外,我们在SC-VTON中加入控制点来达到服饰的形状控制。更进一步,通过增加Splitting Network和Synthesis Network,我们可以使用服饰-模特的pair对数据优化模型,同时将任务泛化到常规的2D虚拟试衣任务。我们的方法能够做到精准的服饰形状控制。同时与其他方案相比,我们的方案能够生成纹理逼真的高分辨率图片,并且能够在实际应用中落地。这是业内首个将图注意力网络应用到虚拟试衣任务,同时能够做到精准可控的服饰形变。论文相关技术在淘系实际场景的应用服饰是淘系最重要的类目,虚拟试衣作为一种新颖的互动展示方式,为用户带来创新体验,为商家创造新的品牌展示方式。从20年元旦开在手淘拍立淘、扫一扫、云主题等公域场景上线了"虚拟试衣间"产品: 提供不同身材的模特供用户挑选,支持几十万件服饰的在线实时试穿。"虚拟试衣间" PV20-30W,UV10W,二跳页人均停留时长2min,平均试衣件数12件。此外,运营同学利用虚拟试衣产品功能,在微博发起"一天试穿500件奢侈品"话题营销活动,曝光2.4亿,讨论量15.1万,产品得到了商家和线上用户认可。论文阅读/下载链接https://arxiv.org/pdf/2107.13156.pdfNO.4题目TransRefer3D: Entity-and-Relation Aware Transformer for Fine-Grained 3D Visual Grounding(TransRefer3D:基于实体-关系可知的Transformer模型的细粒度3D视觉指代定位)作者何岱岚,赵禹昇,罗钧宇,惠天瑞,黄少飞,张爱喜,刘偲论文创新点&对于行业的影响本文提出了一种基于Transformer的模型来抽取3D场景中物体之间的多模态上下文,从而建模更具判别力的特征来定位被指代物体。该模型的每一层中主要包括两个模块:实体可知的注意力模块。该模块将语言中的实体信息与视觉实体特征相匹配,提取符合语言描述的实体特征;关系可知的注意力模块。该模块将语言中的关系信息与视觉实体间成对的关系特征进行匹配,增强符合关系描述的实体特征。该模型在两个细粒度3D视觉指代定位基准数据集上取得了当前最优效果。论文相关技术在淘系实际场景的应用细粒度3D视觉指代定位任务目前在淘系业务中没有实际应用,未来可在视频结构化信息提取、智能机器人控制和人机交互等方面有广泛的潜在应用场景。本文所提出的模型可以辅助智能机器人更好地理解人类用户的指示语言与视觉信息的对应关系,从而在真实的3D场景中对物体实现准确的定位,为下游的复杂任务提供技术基础。论文阅读/下载链接http://colalab.org/media/paper/mm21_transrefer3d_camera_ready.pdf结语获得以上论文收录的淘宝内容互动算法团队,负责淘宝直播、视频和图文以及评价UGC的内容业务的算法研发,利用前沿的人工智能技术在内容业务上的知识挖掘,理解,认知,表示学习,智能剪辑和内容生成等课题和研究方向来打造阿里巴巴内容算法平台。目前,该团队在大规模多模态预训练模型,多媒体内容的结构化和数字化,融合行业领域运营知识的内容图谱的构建,用户内容消费兴趣表征和认知推荐和内容创意的生成与互动(智能看点,智能摘要,合辑生成,虚拟试衣,3D直播间,虚拟主播等)等技术方向持续深耕。希望通过不断加深对用户在淘宝全域的兴趣理解和实时的感知,在内容领域建立起完善的分类和属性的标签体系,细到物品,场景,人物属性和声音风格,粗到内容类型,拍摄手法,泛化到内容的各层次的表征学习等多粒度的内容认知,实现内容的通用表征学习,提升多媒体内容搜索和推荐匹配的极致效率和体验,让淘宝成为消费者购买决策第一阵地,同时热烈欢迎对课题和方向有兴趣的同学的加入。
前言介绍背景在推荐系统中,分类体系在内容圈选、招稿以及投放的过程中都发挥着重要的作用。产品运营可以借助分类体系来圈选内容,例如统计不同领域的视频的供给和用户行为等,对于内容供给不足但比较重要的类目,可以定向招稿。在投放环节,分类体系可以帮助系统更好的聚焦于用户感兴趣的大行业方向。例如某个用户比较感兴趣的是美食、3C数码、美妆这几个领域,如果我们的分类体系构建的足够细,那么我们可以进一步知道他在3C领域对手机、耳机更感兴趣,而对电脑、相机没有太多兴趣,从而召回用户感兴趣的内容。淘宝视频分类的难度内容分类与商品类目的区别怎么挑西瓜是“美食_食材选择”,怎么吃西瓜是“美食_水果”,演示吃西瓜是“美食_吃播”,对比不同种类的西瓜是“美食测评”,拥有相同商品的视频有可能表示了不同的类型。教如何做西瓜拼盘的视频是“美食_教程”,但挂的商品可能是西瓜刀,所以挂商品类目可能和视频类目差异较大。所以商品类目是无法和视频分类词一一对应起来的,但是能作为一个辅助信息帮助我们进行判断。多模态分类VS文本分类淘系视频很多title和summary是无意义的,例如很多title、summary是“xxx.mp4”、“北京时间xxx”,或者是比较抽象的描述,例如“很多女生都在用”。单纯用这些文本很难进行视频分类。我们统计了训练集和验证集上有意义文本的占比,发现无意义的文本占到了7-10%的比例。模型上的效果也验证了多模态分类的必要性。而且仅仅通过文本进行判断还可能出现较大的偏差,例如title为小米手环四,开箱初体验;summary为我们拿出手环来测试,功能多,有测心率测步数,滑动很流畅不卡顿。从title和summary来看这是一个“开箱测评”类型的视频,但其实这是一个“推荐”视频。视频内容分类需要对视频内容有完整的理解。这些细节都需要从视频画面中提取,而不仅仅是商品信息或者文本就能解决的。短视频分类体系定义构建规则为了能够更好的对用户兴趣领域进行划分,我们希望能构建出粒度较细的分类体系,在新版分类中,我们最终构建出3000+个叶子类目。分类体系的构建建立在以下几个原则的基础上:分类间互斥:分类词之间没有重叠。但是在实际中,不可避免存在一些重叠,例如 美食里的 “烘焙”和“零食小吃”,用“烘焙”的方式做出来售卖的零食,放在两个类目中都有一定的道理。从视觉上可分:从视频画面上可以进行划分。这是在视频内容理解技术和业务需求之间做的一个平衡。很多淘宝视频附带的title、summary有可能是无意义的文本(例如很多title是xxx.mp4)、或者是比较抽象的文本(例如“女神都在用的宝贝”),在缺乏足够的文本辅助信息的情况下,我们需要保证从视频画面上就能进行区分。训练数据制备视频分类的语义层次较高,部分类目的范围定义也比较复杂,我们选择用监督学习的方式构建模型。由外包的同学按照视频内容分领域对淘宝PGC视频进行标注,25个领域总标注了28万的样本。采样空间密度采样可以用较少的样本覆盖尽可能多的空间范围,使用同样数量的训练样本和相同的模型,空间密度采样能比随机采样提供更好的泛化能力。样本不均衡从淘宝视频中随机/空间密度采样挑选样本作为训练集容易造成各个类目上分布很不均衡的情况。例如服饰、母婴、3C是大的领域,而旅游、美甲是小领域,小领域上的分类词很可能会有样本数量不足的情况。为了减少样本不均衡对分类结果的影响:一方面我们利用商品类目和分类词之间的相关性,对少样本类目进行扩展;另一方面我们在训练的时候进行resample:在训练时,模型会接收到两种分布的输入,分别是正常分布和重采样分布。通过同样的特征提取模型后,输出为两个向量。随后模型会进行MIXED的做法,将两个向量融合,再计算loss来优化模型。随着训练的持续,模型的重心会逐渐从正常分支转移到重采样分支。在5个领域的数据上进行实验,内容分类的准确率从64.2%提升到68.8%。主动学习淘宝视频分类本质上是一个open-set,不断会有新类型的视频产出,有新的类目需要加入到分类体系中,为了能处理新的视频类目,我们希望模型能持续的进行小的迭代更新。为此我们构建了主动学习链路,通过密度采样,尽可能减少人工标注的样本量,并使模型能适应淘宝视频整体样本分布的变化。密度采样:用旧版本模型在所有低置信度视频上的embedding向量,进行聚类,按照各个聚类上的样本分布采样得到新训练集。单轮主动学习部分类目效果:多模态融合识别算法整体架构上图是我们视频分类的整体架构,我们对视频相关的信息(视频帧、文本、商品等)提取特征,并构建各个模块的子网络,融合后进行类目的预测。除了视频分类,我们的模型还应用于低质识别、内容择优、经验类视频识别、多模表征、多标签预测等多种任务,并取得了较好的效果。视频多模态融合分类模型结构视频帧模块优化全局特征优化视频内容分析的其中一个难点是从视频帧大量的无语义像素中抽取出高层语义信息。其中一种做法是先把视频帧转化成向量,例如在imagenet上训练的Resnet、inception网络,把视频帧输入到模型中,提取embedding向量。在把视频帧转化成向量后,我们可以用CNN、LSTM、NetVLAD等对视频帧序列进行建模。Nestvlad:在NetVLAD的基础上,我们做了改进,得到NestVLADNetvlad是聚合数据的描述子的算法,通过计算各个描述子到聚类中心的残差的加权和来得到数据的表征。Nextvlad将视频的描述子映射到不同的子空间,然后计算每个子空间的描述子到聚类中心的残差加权和,最后通过一个gate相加来得到数据表征。映射到子空间能大大减少模型参数。Nestvlad将视频描述子映射到不同子空间,然后计算每个子空间的描述子到聚类中心的残差加权和,最后在通过一个r-softmax聚合。此外我们还加入了聚类中心之间的一个正交loss,尝试拉远聚类中心的距离,从而得到更泛化的表达。NeXtVLAD把视频帧向量划分成多个子向量,然后这些子向量在同一组聚类中心上求残差,NestVLAD为每个子向量分配了不同的聚类中心组,并且用原始的特征计算softmax,可以兼顾全局信息。时空特征优化把视频帧转成一维向量所保留的信息量取决于特征提取模型的任务,通常会损失比较多的信息,例如空间关系,物体状态等,其中有些信息是视频分类词任务所需要的。patch上的时空建模可以更好的捕捉各个细节在时间上的变化,以及各个patch之间的relation,保留所有的原始信息。Video Caption在时空建模思路的基础上,我们考虑在Video Caption中对视频中的objects构建时空关系,从而获取较好的objects关系和变化信息。O2T(objects-to-token)模块中首先附加patch的位置,长宽,类型,再从Tokens-to-Token的思想出发,递归的把近邻d的Tokens集成为Token来建模同一帧内object之间的相对位置关系以及同一object的时序关系,以提取object的运动轨迹,几何变化等信息。对比学习另一种特征优化方法是对比学习。淘宝场景下我们有大量的无标注视频,通过对比学习,我们可以学到不同图像之间的差异点,从而也保留了淘宝场景下足够多的信息。目前主流的对比学习有SimCLR、MoCo等,他们都是通过“让数据增强前后的图像embedding向量尽可能接近,不同图像embedding向量尽可能远离”来学习图像的表征。我们也希望能用类似的方法在视频上得到类似的效果:同一个视频的帧/片段尽肯能接近,不同视频的帧/片段尽可能远离,从而学到较好的视频帧/视频片段的表征。文本模块优化BERT类预训练模型在文本分类上取得了很好的效果,我们也考虑把预训练的优点结合到我们的模型中。我们去掉了淘宝视频title、summary中无意义的文本。并把较短的文本concat,得到共计500w段文本,用于RoBERTa的预训练。CNN卷积网络捕获远距离依赖的能力较弱,而RoBERTa多层attention可以很好的建模长依赖。最终单文本情况下比CNN和Transformer有显著的提升。1D CNN卷积模型,捕获远距离依赖较弱RoBERTa由于视频分类词模型是一个多模态,多模块,较为复杂的网络,为了得到较高效率的文本模块,我们对RoBERTa的深度和宽度做了实验对比,发现在分类词任务上深度相比宽度更重要。同时我们使用知识蒸馏的方法来确保小的网络也能得到比较好的效果,用网络较大的teacher给网络较小的student提供额外的训练信息。Teacher网络对比Student网络对比把RoBERTa作为文本子模块加入到大模型中进行finetune时,我们考虑到不同模态在训练的时候需要的learning rate可能不同。我们尝试了以下几种策略:各个模态使用相同的learning rate;RoBERTa模块freeze,finetune过程中参数不更新;RoBERTa模块使用较小的learning rate,效果优于上面两种;根据各个模态上的loss变化来设定learning rate,在这些方法中效果最优是第N步在valid集合上的Loss ;是第N步在train集合上的Loss。视频帧-文本联合模型在VideoBERT,越来越多的视频多模态预训练模型被提出,Uniter就是其中一种多模预训练模型,通过MLM、MFM、VTM、CLS等Loss,构建文本内部、视觉内部、文本与视觉对齐等任务,从而让文本和视觉除了学好自己本身的特征外,互相直接也做了对齐。相比于多个子模块搭建的分类网络,Uniter的文本与视觉之间的相互作用更强,并且利用了大数据预训练,可以更好的学到视频的表征。由于Uniter网络结构较大,很难把Uniter和其他子网络放一起端到端训练,所以我们提取出Uniter的embedding向量加入到模型中去,作为一种特征的补充。预测模块优化特征选择我们尝试了MOE和SE-context gate来对多模态特征进行筛选,选择对下一步分类最有用的特征。MOE:不同模态对各个类目的作用不同,通过多专家决策模块对不同模态的特征进行选择,对相关特征进行增强SE-context gate:不同于MOE,SE-context gate通过特征之间的依赖关系,抑制不相关的特征,在视频分类词中的到了比MOE更好的效果。引入分类词语义信息Attention:我们把分类名转化成word2vec向量,与多模态网络融合后的特征向量做attention,得到最终的特征向量。word2vec具有在全局文本中的side infomation,有助于多模态特征向量更好的了解整体的类目划分空间。噪声样本识别由于视频内容理解是一个语义层级比较高的分类任务。因此在数据标注的时候,难免会遇到标注错误的情况。这会影响到模型的训练。因此进行噪声样本识别是一个关键的步骤。这里我们主要参考了集团内其他团队对噪声的处理《(2019 ICCV) O2U-Net- A Simple Noisy Label Detection Approach for Deep Neural Networks》。对模型学习不好的领域,挑选出loss震荡幅度较大的样本。这些样本可能是噪声,也可能是难样本。通过进一步地清洗数据,能得到更纯净的数据,从而让模型更好地理解数据。实时链路为了更好的为业务提供服务,我们构建了离线链路和实时链路,离线链路是在PAI平台上搭建的daily任务链路,实时链路是在VIP平台MediaFlow上构建原子op和graph。我们和计算平台的同学对视频分解、抽帧做了深度优化,一个淘宝视频的平均处理时长从最初的20多秒减少到4s左右,大部分视频都能在10s以内完成 下载、抽帧、提取特征(帧特征、文本特征、音频特征、OCR、ASR等)、模型预测整个流程。目前实时链路已经在为首猜曼哈顿3分钟链路、内容中台视频审核提供服务。下面上图是离线链路和实时链路整体框架,下图是分类词大graph的原子op和边的示意图。业务应用首猜打散为了提高用户的浏览体验,希望在用户的一个session交互内,给他们推尽可能多不同类目的视频。在同一个session内如果两个推荐的视频是在同一个叶子类目中,则认为这是一个bad session。通过视频分类进行打散是在体感和效率上进行平衡。视频分类从v1.0升级到v2.0版本,在效率持平的情况下,badcase减少了约7%,推荐多样性指标有大幅度提升。首猜冷启动链路模型的embedding向量用于冷启动召回作为一路召回,CRT(4.5895%)指标仅次于fans2c(粉丝召回)。首猜全屏页主链路加入到相关性模型中后,与基准桶相比,实验桶二跳视频相关性问题略有下降,多个视频推荐相同商品问题减少4.12%,有明显的好转。分类模型embedding向量加入到首猜召回模型中作为一路召回,这路召回的CTR(10.53%)相比于其他路有很大的提升。云主题挂载视频为19362个云主题挂载一个相关的 知识教程/好物评测 视频。淘宝短视频和逛逛v2.0分类体系同时迁移到了淘宝内容中台和逛逛等业务场景,叶子类目数量从200左右增加到400+,且算法模型、标注额外增加的工作量很小。v2.0版本叶子类目共计3000+,适用于搜索推荐等算法场景,对于创作者、运营的则可以在v2.0分类词树结构的基础上进行剪裁。我们多模态分类模型在逛逛上也得到了比较好的效果:淘宝经验导航我们使用短视频分类的结果进行视频专题构建,物品+描述角度分类 可以构建一个栏目,通过组织相同物品的不同栏目,构建出一个专题。例如下面两个case,在搜索零食、酒的时候会透出两个专题,两个专题分别包含了多个栏目,例如图中的“零食大比拼”“我吃你看”等。分领域赛马不同领域的视频用户行为存在较大的差异,例如很多用户喜欢看 萌宠_猫狗 的搞笑、软萌视频,但对 萌宠_宠物用品,更多是抱着购买需求浏览视频。点击率、观看时长、成交量等天然存在差异。对不同领域的视频在同一个赛道进行赛马,选择用户行为较好的视频分配更多的流量,对于那些平均用户行为较差的领域的视频很不公平。所以需要分领域赛马选择优质的视频扩量投放。视频分类模型根据视频的内容判断视频的类型,从而可以把视频分到相应的赛道进行公平竞争。多模态分类模的沉淀和推广我们的多模态短视频分类模型可以广泛应用于多种短视频分类任务。例如在淘系场景内,我们使用这个多模态分类模型来解决其他一些task,并取得了较好的结果:Howto视频识别:理解视频帧时间上的变化,判断视频是否是在讲述某些使用经验或者生活经验;精品池视频择优:对文本、视频帧、语音等信息进行分析,判断视频是否是优质视频;PPT视频识别:根据视频画面判断视频是否是PPT类型的视频;总结我们的多模态视频分类模型在多个任务和场景得到了应用,特别在产品运营的招稿、内容圈选、统计分析和搜索推荐的分发效率和用户体验提升上起着重要的作用。在多模态视频分类的优化上,我们分析了模型在任务上的缺陷,从训练样本处理、模态特征优化、特征融合等入手,针对性的做了大量的优化,在各个任务上都取得了不错的效果。后续我们将继续在视频内容理解领域探索,除了多模态视频分类,也会在Vdeo Caption、Video Question Answer、Video Grounding等方向结合业务做深入的研究。Reference视频智能分析平台VIP及其在集团业务的应用 https://topic.atatech.org/articles/154983O2U-Net- A Simple Noisy Label Detection Approach for Deep Neural Networks 2019 ICCVNetVLAD: CNN archi- tecture for weakly supervised place recognition 2016 CVPRActionVLAD: Learning spatio-temporal aggregation for action classification 2017 CVPRGhostVLAD for Set-Based Face Recognition 2019NeXtVLAD: An efficient neural network to aggregate frame-level features for large-scale video classificationBBN: Bilateral-Branch Network with Cumulative Learning for Long-Tailed Visual Recognition 2020 CVPRUNITER: UNiversal Image-TExt Representation Learning 2020 ECCVFASTER Recurrent Networks for Efficient Video Classification 2020 AAAIIs Space-Time Attention All You Need for Video Understanding? 2021 ArxivSelf-supervised Video Retrieval Transformer Network 2021 ArxivA Simple Framework for Contrastive Learning of Visual RepresentationsTing 2020 ICMLMomentum Contrast for Unsupervised Visual Representation Learning 2020 CVPRAn Empirical Study of Training Self-Supervised Visual Transformers 2021 Arxiv
国际多媒体学术会议(ACM MM)是计算机学科公认的多媒体领域和计算机视觉领域的国际顶级会议,也是中国计算机学会(CCF)推荐的A类国际学术会议。ACM MM研究领域覆盖图像、视频、音频、人机交互、社交媒体等多个主题,本次 ACM MM2021 一共收到 1,942 篇论文申请,最终入选 542 篇论文(约 27.9% 的接受率)。淘系技术部内容互动算法团队,聚焦机器学习、视觉算法、NLP算法,端侧智能等领域,依托淘系数十亿级的视频数据,业务上支持淘宝直播、逛逛和点淘,有丰富的业务场景和技术方向,不断探索和衍生颠覆型互联网新技术,团队成员来自海内外知名高校,近两年参加CVPR竞赛获得4项冠军,累积在计算机视觉顶会期刊(如CVPR、TPAMI、TIP等)上发表论文10余篇,技术成果获得国家科技进步二等奖。本次 ACM MM2021 会议,该团队一共 4 篇论文被接收,并有相关技术创新点在淘系业务场景中的应用。后文将详细介绍各篇论文创新点以及落地使用。NO.1题目Understanding Chinese Video and Language via Contrastive Multimodal Pre-Training基于对比多模态预训练理解中文视频和文本作者雷陈奕,罗时现,刘勇,何旺贵,王家忙,王国鑫,唐海红,苗春燕,李厚强论文创新点&对于行业的影响预训练模型在自然语言处理领域、视觉领域乃至多模态领域已经取得了巨大的成功。本文聚焦于多模态领域中的视频-文本的联合预训练策略,尤其针对中文视频和文本。针对视频-文本进行预训练主要存在以下挑战:第一,和静态图像不同,视频拥有动态的时空序列关系,直接将图像-文本的预训练方法移植到视频-文本领域并不足以捕捉这些复杂的关系信息;第二,预训练模型中广泛存在的视频-文本对齐任务和其它基于掩码的重建任务存在冲突;第三,大规模、高质量的中文视频-文本数据集的缺乏限制了预训练模型在中文领域的发展。由此,本文提出一个基于重建和对比学习任务的多模态预训练模型VICTOR,并建立千万数量级的高质量中文视频-文本数据集。VICTOR以Transformer为主体,设计出基于重建和基于对比学习的七个任务训练模型。基于重建的任务包括掩码语言建模、掩玛句子生成、掩码帧序列建模和掩码句子序列建模四个任务,充分捕捉视频和文本的序列信息和交互信息;基于对比的任务包括对偶的视频-文本对齐、视频内的掩码帧对比学习和视频间的掩码帧对比学习三个任务,在避免简单的视频-文本对齐任务会融合不确定的多模态信息的同时,增强视频内的时空信息融合。VICTOR模型拥有上亿级参数,在构造的千万数量级的淘系视频-文本数据集中进行预训练,并在多个下游任务(如视频文本匹配、视频推荐、标题生成)获得了SOTA的性能提升。VICTOR模型的设计和提出,有效促进了预训练在中文视频-文本领域的进展,并可在多个视频相关业务(如视频推荐、视频分类等)广泛应用。Victor 模型的总体框架:包含模型设计和自监督任务论文相关技术在淘系实际场景的应用我们将VICTOR预训练的视频特征应用到内容检索、推荐、分类、直播等多个领域。各个应用场景均对比场景中已服务的Strong Baseline,具体来说:内容推荐--逛逛内容推荐。效率保持稳定下,3天新发内容占比提升22.81%,冷启动UCTR +4.29%, PCTR +4.72%;内容检索--淘宝经验。跨模态检索,保障相关性评测基础下,无结果率由3.23%降至0.95%内容分类--逛逛内容分类。图文分类精度相对提升3.94%(60.97%->63.37%),视频分类精度相对提升7.33%(51.99%->55.80%)物体检测与匹配 -- 直播看点审核。全品类检测精度相对提升4.83%(89%->93.3%),美妆难例检测精度相对提升8.05%(75.8%->81.9%)论文阅读/下载链接https://arxiv.org/abs/2104.09411NO.2题目Pre-training Graph Transformer with MultimodalSide Information for Recommendation用于推荐系统的融合多模态信息的图预训练Transformer作者刘勇,杨粟森,雷陈奕,王国鑫,唐海红,张举勇,孙爱欣,苗春燕论文创新点&对于行业的影响在个性化推荐领域,尤其是短视频的推荐领域,多模态信息发挥着重要的作用。有效利用item的多模态信息,如文本、视觉等信息,可有效提高推荐的性能,缓解冷启动问题。目前存在的融合多模态信息的推荐模型,都是端对端的基于特定任务的模态融合,消耗资源的同时限制了模型的泛化。另外,在推荐领域,item之间存在着各种相关性(如基于标签的语义相关、基于行为的用户兴趣相关等)。为节省资源,提高模型利用率,同时捕捉item之间的相关性,本文提出一种基于多模态信息融合的图预训练框架PMGT,在捕捉item相关性的同时,指导item多模态信息的融合,并且预训练后的item特征,可应用到多种下游任务,避免了在每个特定任务都要重新融合模态信息的资源浪费和时间消耗。PMGT首先根据item的相关信息,构建出一个item多模态图,其中图的节点为item,边反应了item之间的关系(如被相同用户交互过的item建立边),每个节点的特征由item的多模态特征构成。对图中的每个节点,我们设计出高效并行的采样方法MCNSampling,从图中采样出若干与其相关的节点组成节点序列,并使用基于多样性的transformer框架聚合节点特征,缓解模态融合的冗余性。最后使用基于图结构重建的任务和基于节点特征重建的任务指导相关节点融合和节点自身的多模态信息融合。将PMGT在Amazon和MovieLens公开数据集上预训练并测试,和最新的图预训练模型相比,达到SOTA性能。PMGT使用图的方式指导item的多模态信息的融合,并有效捕捉item之间的相关性,让预训练并不局限于item自身,增强了预训练的item特征的表达能力,可适用于多种下游任务和领域。论文相关技术在淘系实际场景的应用在淘系的短视频推荐领域,我们基于短视频tag信息建立拥有400万节点、4亿边的视频多模态图,并将PMGT预训练后的特征直接应用到短视频的召回阶段,7天新内容占比提升7%。之后可将预训练特征应用到排序阶段,甚至其它的业务场景(如视频分类),并且可将PMGT作为基础框架,使用特定的任务微调模型,达到效果的进一步提升。论文阅读/下载链接https://arxiv.org/abs/2010.12284NO.3题目Shape Controllable Virtual Try-on for Underwear Models(SC-VTON:针对内衣模特的形状可控的虚拟试衣系统)作者高鑫,刘振江,冯尊磊,申成吉,欧开日,唐海红,宋明黎论文创新点&对于行业的影响我们提出了一种形状可控的虚拟试衣网络(SC-VTON),针对内衣模特的试衣任务,使用融合了模特和服饰信息的GAT网络来生成形变后的服饰图片。除此之外,我们在SC-VTON中加入控制点来达到服饰的形状控制。更进一步,通过增加Splitting Network和Synthesis Network,我们可以使用服饰-模特的pair对数据优化模型,同时将任务泛化到常规的2D虚拟试衣任务。我们的方法能够做到精准的服饰形状控制。同时与其他方案相比,我们的方案能够生成纹理逼真的高分辨率图片,并且能够在实际应用中落地。这是业内首个将图注意力网络应用到虚拟试衣任务,同时能够做到精准可控的服饰形变。论文相关技术在淘系实际场景的应用服饰是淘系最重要的类目,虚拟试衣作为一种新颖的互动展示方式,为用户带来创新体验,为商家创造新的品牌展示方式。从20年元旦开在手淘拍立淘、扫一扫、云主题等公域场景上线了"虚拟试衣间"产品: 提供不同身材的模特供用户挑选,支持几十万件服饰的在线实时试穿。"虚拟试衣间" PV20-30W,UV10W,二跳页人均停留时长2min,平均试衣件数12件。此外,运营同学利用虚拟试衣产品功能,在微博发起"一天试穿500件奢侈品"话题营销活动,曝光2.4亿,讨论量15.1万,产品得到了商家和线上用户认可。论文阅读/下载链接https://arxiv.org/pdf/2107.13156.pdfNO.4题目TransRefer3D: Entity-and-Relation Aware Transformer for Fine-Grained 3D Visual Grounding(TransRefer3D:基于实体-关系可知的Transformer模型的细粒度3D视觉指代定位)作者何岱岚,赵禹昇,罗钧宇,惠天瑞,黄少飞,张爱喜,刘偲论文创新点&对于行业的影响本文提出了一种基于Transformer的模型来抽取3D场景中物体之间的多模态上下文,从而建模更具判别力的特征来定位被指代物体。该模型的每一层中主要包括两个模块:实体可知的注意力模块。该模块将语言中的实体信息与视觉实体特征相匹配,提取符合语言描述的实体特征;关系可知的注意力模块。该模块将语言中的关系信息与视觉实体间成对的关系特征进行匹配,增强符合关系描述的实体特征。该模型在两个细粒度3D视觉指代定位基准数据集上取得了当前最优效果。论文相关技术在淘系实际场景的应用细粒度3D视觉指代定位任务目前在淘系业务中没有实际应用,未来可在视频结构化信息提取、智能机器人控制和人机交互等方面有广泛的潜在应用场景。本文所提出的模型可以辅助智能机器人更好地理解人类用户的指示语言与视觉信息的对应关系,从而在真实的3D场景中对物体实现准确的定位,为下游的复杂任务提供技术基础。论文阅读/下载链接http://colalab.org/media/paper/mm21_transrefer3d_camera_ready.pdf结语获得以上论文收录的淘宝内容互动算法团队,负责淘宝直播、视频和图文以及评价UGC的内容业务的算法研发,利用前沿的人工智能技术在内容业务上的知识挖掘,理解,认知,表示学习,智能剪辑和内容生成等课题和研究方向来打造阿里巴巴内容算法平台。目前,该团队在大规模多模态预训练模型,多媒体内容的结构化和数字化,融合行业领域运营知识的内容图谱的构建,用户内容消费兴趣表征和认知推荐和内容创意的生成与互动(智能看点,智能摘要,合辑生成,虚拟试衣,3D直播间,虚拟主播等)等技术方向持续深耕。希望通过不断加深对用户在淘宝全域的兴趣理解和实时的感知,在内容领域建立起完善的分类和属性的标签体系,细到物品,场景,人物属性和声音风格,粗到内容类型,拍摄手法,泛化到内容的各层次的表征学习等多粒度的内容认知,实现内容的通用表征学习,提升多媒体内容搜索和推荐匹配的极致效率和体验,让淘宝成为消费者购买决策第一阵地,同时热烈欢迎对课题和方向有兴趣的同学的加入。
于一名优秀的技术人员来说,究竟是专精一块技术方向,做到深耕其中所向披靡;还是谋求“什么都能略懂一点”的广度,成为一个全方位的人才?这其实是一个职业发展和学习规划路线的问题,许多同学都有这个困扰。今天我们邀请了 4 名淘系技术工程师,结合他们自身在小厂和大厂的经历故事,给大家分享一些他们在技术人员成长中对于【精】和【广】的选择观点,希望能够对你有帮助。01 淘系技术部 - 应用算法 - 立青“更早地认识自己和自己的方向,能更快地帮助我做出成绩。”我想先撇开这个具体的问题,谈一谈一个程序员的技术发展和职业规划。写代码这件事绝大多数人在喜爱的同时,更多的都是当做一个职业来做的,当然也确实有一部分人真的完全当做爱好,例如前段时间的新闻,HashiCorp 的创始人 Mitchell Hashimoto:“顶级凡尔赛CTO辞职:写代码才最快乐!管理只会影响我搞研发”;著名的Linux创始人Linus大神也是出了名的热爱编程。对于这样的追求(土豪),抛弃功利热爱编程,我觉得在编程上完全可以喜欢什么方面就追求什么方面,是广泛的发现兴趣探索兴趣,还是在一个方向上深入挖掘都没有问题,大可以今天做前端,明天搞开发,后天攒算法,一人搞定全栈。但是对于绝大多数人来说,编程更多的是职业发展道路上一个立身的手艺,在众多专业技术方向上挑了一个自己比较喜欢和热爱的。程序员的发展和众多职位的发展一样,每个人都希望自己能够往"上"走:更专业,更能在职场上发挥自己的作用和影响力,从单兵作战做小事,到带队做大一点的事,再到影响一个领域,影响一个行业。这样的发展单单靠自己各方面都懂,都有涉猎,恐怕是不行的。刚毕业的应届同学可以靠自己的知识储备做自己的标签,久经职场的同学必须靠自己在某些领域做出的成绩做自己的军功章。所以我们越早在某些方向做出自己的成绩,对自己的成长和发展是越好的。我本人是做算法的,算法领域有很多大牛在学校期间就已经找准了自己的发展方向,并做出了成绩,例如caffe作者@贾扬清大神,Taichi作者@胡渊鸣等等。他们的成就就需要及早的找到自己的方向+不懈的努力+亿点点的天赋……对我们绝大多数人来说更需要及早的找到一个热爱的,希望精通的方向并做出成绩。我自己的经历其实不是好的榜样,我抱着做机器人的梦想本科学的机械,在学校的时候对什么都感兴趣,既参加过机器人大赛,参加过结构设计大赛,也和同学做过热力学相关的一些机械设计;一个比较有意思的经历是,当时在学校还做过一个上天失败的小卫星。当时学校的微小卫星研究所发射了一颗皮星(体积很小的卫星)之后,在学校办了一个小卫星的比赛,我们做了一个模拟卫星太阳能电池片自动追踪阳光的卫星模型,在答辩前熬夜完成了,然而在最后测试的时候由于线的固定太过粗糙,在运动中扯掉短路了,把电路都烧了……万幸留下了视频支持最后的答辩。对我后来职业选择影响比较大的是临毕业用Kinect做了一个姿态识别控制无人机飞行的项目,初步接触了视觉算法的一些知识。后来研究生就真正的开始做起了机器人的方向,也是研究生时期做的控制算法和视觉算法的经历让我走上了程序员的道路。在这个过程中自己也是对机器人领域中的SLAM方向产生了兴趣,自己恶补了一些知识,并靠着这些最后走上了这个方向的职业。一方面我觉得自己如果能早一些找到自己的方向并且积累起来一些成果,肯定对自己的发展是要好很多的,但是另一方面每个人也确实需要一个认识自己的过程,但这个过程我想还是越快越好。在这个过程中,我们自己的技术发展就像是一棵树,我们尽可以无限的去展开自己的枝叶,多了解一些不同的方向和知识,但一定记住这是为了让自己的枝头长得更高。02 淘系技术部 - 应用算法 - 朔玥“大部分的精力还是要用于巩固自己的长处上,你一定要有竞争力的依凭。”做一件事是要精还是要广?其实相当于赌博里你是要多压,还是要单押。我们的筹码是有限的,当然我们的精力也是有限的,不可能去做所有的选择。那么这时候,问题就变成了如何去组合投资获得最大化的收益。如果你选择去把所有的筹码压在一个选项上,那么你就一定要去承担选择失误带来一切清零的后果。但是我们也知道,如果你选择到一支潜力项,不费吹灰之力就可以赢到盆满钵满。所以孤注一掷,一定对应的高风险。另外一个选项就是广博,它带给我们的好处是平摊风险,但是平摊风险的同时收益也会被稀释掉。比较好的一种做法,应该是两者相结合,在该广博的时候广博,在该专注的时候专注。作为一名算法工程师,我的建议是,广泛的去涉猎相关领域的知识,以及弱相关领域的知识,因为这些知识全都可以作为你自己的储备,作为你专注行业的加成。比如说,你从事图像识别相关算法研究,那么直接的,除了图像相关的专业知识,一些基础的知识例如代数分析等数学理论,计算机原理与体系结构,甚至色彩,摄影美学等知识,都可以为模型设计带来一定的辅助。除基础的知识以外,相关领域比如自然语言处理当中的时序模型就与视频图像分割问题有很多的共通之处;信号处理中很多变换算子能够直接迁移到图像处理当中。这些知识,都可以作为你研究图像算法的加成。又如,你想不到心理学,消费者行为学以及经济学都可以为我们的工业推荐体系赋能。在洞悉了消费者在购物时的选择动机,心理活动以及当下经济趋势后,算法设计者能够依据这些先验知识有聚焦地设计相关的模块,或是作为趋势因子加入算法当中。用一种更柔软的方式将大众认知融入模型,将表象和理论相结合,从而更好地发挥推荐系统的效能。世界上很多知识底层是相通的,不要去抗拒学一些基础的知识,很有可能那将是你未来抓住机遇的契机。但是,大部分的精力还是要用于巩固自己的长处上,你一定要有竞争力的依凭。广泛的涉猎,精准地融会贯通,提取出你需要的那一部分,转化为自己需要的能量。03 淘系技术部 - 前端技术 - 禾鸟“我个人经历觉得,精进一门技术,不管是对于开发还是其他工作,都是重中之重!”当我刚毕业的时候,在一家小厂做Flash游戏开发,由于当时开发人员配比严重不足(qiong),所以不但要用AS3去写前端游戏UI功能,还需要用NodeJS写服务端游戏逻辑、SQL处理数据读写操作、HTML/CSS/JS写后台配置管理页面,且一度在公司没有招到测试的情况下,所有的功能测试只能通过自测来完成。时间一长就有些膨胀了,自我感觉非常好,觉得自己就是全栈,觉得只有更大的舞台才能配得上自己,进入大厂应该是轻而易举的事情,于是开始往网易、阿里、华为一些大厂投简历,Flash游戏开发、H5游戏开发、Web前端开发、NodeJS开发、服务端开发、测试开发各种岗位都投了遍。结果就是被现实狠狠地打了脸,投的简历石沉大海,少有的几个面试也都是一轮游。后来总结反思了一下,在所有面试中,面试官经常会问这么几个问题:XX原理是什么?XX如何实现?对XX你是如何理解的?对于XX功能是否有更好的解决方案?面对这些问题,发现了自己其实对底层原理一窍不通,所有都还是停留在使用层面。看清楚了这个问题之后,我暂时放下了其他域的学习,专注在前端领域的学习,从基础的HTML/CSS/JS入手,到lodash/JQuery等常用工具库的使用,再到Vue/React等主流框架的使用以及原理的学习,以及ES6、TS等学习掌握。等熟练掌握了这些技能之后,再深入到浏览器工作原理、网络通信机制、前端性能优化、稳定性安全保障等知识点的学习。一步一步从搬砖菜鸟变成了熟练搬砖工,然后继续朝着搬砖砖家努力。总的来说,在小厂,老板当然希望能更省钱,巴不得一个人就能干完所有的活。而对于个人而言,精进一门技术,不管是对于开发还是其他工作,都是重中之重!04 淘系技术部 - 移动开发 - 临境“不要把自己当做业务研发的工具人。”这个问题没有标准答案,角度不同,得出的结论会大相径庭。前不久刚从小厂跳到阿里,简单聊下我自己的感受。小厂会更偏重于业务,因此我们更多的是在实现业务方的需求,日常的研发工作也少有接触特别难或者深的内容。这个阶段不是说基础不重要,但深入学习基础知识可能对你业务研发不会有特别明显的提升。曾有段时间我为了学习算法知识跑去刷 leetcode,刷完两三百题后陷入了迷茫。这些刷题获得的算法知识在大部分的业务场景中不太能用上,虽然它对于在编程中边界条件判断、减少低效代码确实起到了一定的帮助,但相较于投入的时间来说性价比略低。同样道理,学习操作系统、计算机网络、计算机组成原理能让你对整个计算机体系有一个更深层次的认识,但工作的人不比在校生,时间比较有限,如果你不是一个特别特别自律的人,投入时间没有看到明显产出很可能会中途放弃。如果想要在工作的同时更深入学习一些计算机基础知识,可以从你正在做或者感兴趣的性能优化入手。不少性能优化手段背后都伴随着深入的知识,比如之前不少大厂分享的针对 iOS 二进制重排优化冷启动的内容。学习过程中一定会碰到自己不了解的知识,一点点去积累,会比盲目的抱着一本砖头书直接啃更容易坚持。另外,如果是业务研发,请一定锻炼自己对业务的理解程度,不要只停留于需求来了做完就完了,把自己当业务的工具人没有对个人成长没有益处。最后,和大家分享一句我和喜欢的话:“书上没有知识,书上只有信息;知识是在特殊的工作和行动中运用信息的能力。”共勉。结语程序员要精还是要广,并不是一个有着标准确定答案的选择题。精于基础,广于工具,熟于业务,永远保持进步和学习的心态,希望各位都能找到最适合自己的技术成长路径。如题,你们的观点是什么呢?留言区欢迎一起讨论。
近年来,出现越来越多“自主学习”、“业余提升" 的相关话题。我们经常收到一些同学提问:程序员的工作非常忙碌,如何在繁忙的工作中利用碎片化时间学习或是做自己感兴趣的事情?不管是新的应届生,还是两三年的职场人,甚至工作多年的职场老人都在关心这个问题。到底现在的程序员该如何利用业余时间从而让那个自己达到满意的状态呢?今天我们邀请了 4 名淘系技术的工程师,给大家分享一些他们的业余提升技巧,以及时间的安排,希望能够为你提供一份参考。淘系技术部 | 去来这个话题非常现实,我的方法就是发现工作以外的时间在哪。程序员的一天其实挺忙的,每天能留出的时间可以分几块:通勤时间我的通勤方式是地铁,每天大概需要花40分钟在地铁上,这段时间一般用来读书,一年累计下来能读完2-3本。有些人可能不太爱看书,其实不同类型的书吸引力确实差很多,找到自己感兴趣的书才能培养阅读的习惯。本科的时候我自己也是更爱打游戏不爱看书,在研究生阶段偶然看了一本基督山伯爵,才开始喜欢上阅读的。书里的很多故事不好拍成电视剧或电影,这些有趣的故事能让我在代码之外找到更多乐趣。那之后慢慢开始找书看,在朋友圈看看好友分享的,和同事们也有交流看到的好书。前段时间恰逢建党100周年,组里的几个小伙伴毅安,增群对《毛泽东选集》产生很大兴趣,我也在读中共中央文献研究室出的《毛泽东传》,有很大收获。午休时间这段时间不是很长,吃完饭大概还有半个小时,习惯午睡的同学可以趁机休息会,为下午的工作养足精神。我个人更习惯和同事遛弯交流交流最近生活上的新闻。晚上下班后一周锻炼2-3天,在外跑步跳绳,或在家跟着keep练。没有好身体,心情也不好,其他都干不好,所以对自己的健康还是要多关注一些。IT从业者白天坐的时间太久,用眼时间很长,晚上尽量别玩太久手机。健身是给身体充电,读书是给头脑充电,利用一些更加碎片化的时间我会翻一翻知乎和微信公众号上和人工智能相关的技术专栏,看下行业动态。周末除了吃喝玩乐,继续阅读,健身还有写作。阅读是被动接收信息,我还有一个小目标就是自己写文章,打造个人技术影响力的尝试。去年定了一个小目标,微信公众号的关注超500(超过500就可以加广告,赚流量分成,哈哈)。写了几篇,只吸引了自己朋友圈里的人关注,分析原因:一是内容还不够精彩,二是微信公众号没有公域推荐机制。于是又转到知乎上,通过写文章与答题,目前已经收到1k多粉丝,分析了粉丝来源之后,发现大部分都是来自一两个优质的回答,头部效应很严重,所以再写文章还是要出精品,争取早日一篇自己的10w+。淘系技术部 | 玖伍其实提升自己没有秘籍和诀窍,只要愿意花业余时间去学习,再加上长时间的坚持,就可以成为大神。阅读我个人比较喜欢读书,喜欢读纸质的书,记得刚开始工作的时候,很多东西都不会,只会写CSS切页面,是一名真切图仔,同时自己又特别想成为大神,然后就每天中午吃完饭在工位上看一个小时的书,下班后也会留在公司看两个小时的书再回家,就这样每天中午和晚上一边看书一边写Demo,前期的提升速度还是非常明显的,基本上每天都能感觉到自己学会了新知识。我比较推荐多读一些技术书,特别是纸质书,熟悉我的同学都知道我有非常多的书。一本书从填选题表到最终出版,中间会经历很多步骤,出版社专业的编辑也会和作者一起反复的校验和修改好多遍,上市之后再经过读者的认可,这样一本书的内容质量是非常有保障的。根据经验图灵出版的书质量都非常高。学习资料学习资料非常重要,要阅读高质量的第一手资料,很多时候我们学习某个技术发现怎么都学不会搞不懂时可能不一定是我们笨,也有可能是学习资料有问题。我见过很多文章讲某个技术,即使那个技术我事先已经会了,也确实看不懂文章里在说些什么。我也见过很多文章可能作者自己也不是很懂某个技术,他只是把一些其他文章拼凑起来。不好的学习资料通常内容晦涩难懂且没有把技术讲清楚,而高质量的学习资料通常会很清晰且精准地把一个技术讲透,因为讲解清晰明确,所以学习起来也不会太复杂枯燥。JS框架、库、工具等,我一般会从官网和口碑较好的纸质书籍中学习。基础知识我一般通过阅读高质量的纸质书籍 + 阅读W3C的规范来学习。Web性能领域我通常在Chrome开发者官网和web.dev里的文章来学习。具备一定的基础知识后就可以判断出学习资料的质量,这时候就可以关注一些公众号或者明星程序员来获取一些知识。写作分享除了学习,我还会利用业余时间写文章,做技术分享等,将自己学到的知识分享出去。切身体会,将自己学到的知识分享出去对自己的成长有很大帮助,有时候写文章的过程中会发现自己对某个知识也没有真的学透。而且写作和分享可以让自己学会思考并锻炼思考能力,而思考能力其实很重要。坚持最后,坚持才是最重要的,我们的职业生涯,其实是一场没有终点的长跑比赛,很多人可能想问怎样才能跑得更快,把这场比赛跑赢。其实在这条没有终点的赛道上在短期内快一些没有任何意义。大部分人跑到中途就主动放弃了,这就是为什么大牛那么少。唯一能决定这场比赛输赢的,只有两个字叫“坚持”。在这条赛道上跑赢的,不是那些跑得快的人,而是为数不多坚持跑的人。他们能跑赢,只是因为他们还在跑。书单推荐最后推荐一些书单,全都是我自己看过的觉得非常不错的书。JavaScript相关的书籍:《你不知道的JavaScript》上中下三本、《深入理解ES6》、《JavaScript高级程序设计》CSS相关的书:《CSS世界》(这类书我自己没有看,但我看张鑫旭博客学的CSS,他出版的书我虽然没看,但凭着对作者的信任,而且作者还专门为这本书做了个[官网](https://www.cssworld.cn/)感觉还是蛮用心的,质量应该是可以保障的)JS框架相关的书籍:React相关我没有看过不做推荐,Vue相关的推荐一本:《深入浅出Vue.js》(非广告,内容质量和深度确实是目前市面上最好的一本)。Node.js相关的书籍:《深入浅出Node.js》(只看过一本朴灵大大写的质量还行,别的没看过,所以只推荐这本)。再分享下其他我看过的觉得不错的书:《算法4》、《Web性能权威指南》(作者是前任W3C性能工作组主席,译者是李松峰老师,虽然这本书出版快10年了,但我感觉还是值得一看的)、《重构》、《码农翻身》、《代码整洁之道》、《软技能 - 代码之外的生存指南》、《金字塔原理》。淘系技术部 | 岳溪这个话题非常现实,我的方法就是发现工作以外的时间在哪?程序员的一天其实挺忙的,每天能留出的时间可以分几块业余时间如何分配,如何保持高效的工作状态?首先还是要规律生活,早睡早起,比如晚上尽量逼迫到点就睡,晚上的熬夜必定带来白天的萎靡。保持锻炼,规律健身跑步运动,缓解工作的疲劳,也给工作注入更多的体能资本,健身房的一声大喊,工作的疲劳一扫而尽。业余时间还是要慢下来,品味生活,比如对某一时期的历史感兴趣,不妨系统研究研究,然后结合了解的知识,再制订一个长期的业务学习计划,总之还是要做一些让能力能够专注的事情,给生活多一些愉悦,需要愉悦。工作上多线程切换真的会带来效率提升吗?有时会感觉事情很多,陷入忙碌后容易在多件事情上来回切换,最后的结果经常是一件事都没有做好;普通人真的是不适合多线程工作,有时不妨多想想,要思考,适当的慢下来,分清事情的主次,然后集中精力一件件完成,往往效果会更好;工作中经常会遇到新的领域,新的问题,高大尚的东东,面对新技术如何学习,快速入门?还是要具体问题来分析,面对工作的难题,需要深入理解时,我通常会读读经验帖,找一本好书或资料,以解决问题为目标制订系统学习的分阶段目标,阶段目标可以防止总目标lost。淘系技术部 | 勇剑“业余时间”对自己提升相比“工作中”的提升,不同的地方是,我们可以有选择性的针对自己薄弱的点去提升。那么首先要了解自己有哪些需要提升的地方,才能有针对性的去提升。个人理解,作为技术人员提升的主要方向不外乎技术基础、沟通技巧、推动能力等等。业余时间的话,主要可提升的还是技术基础,这个也比较好去有针对性的学习。自我分析首先需要有自己的一份技术栈大图,有哪些已经掌握的、哪些还不太熟悉、哪些完全不懂的,然后就可以针对自己的薄弱项进行针对性的学习。当然还有一些是我们还完全不知道的技术,这就需要我们经常去关注业界动态了,针对这些,可以视情况去参加一些线下的 meetup,跟大佬面对面的交流。献上我自己的一份个人技术技术大图:透过现象看本质在学习过程中,要注意透过现象去看本质,常说的 What、How、Why 在学习的过程中要常去思考。不给自己设限总结来说就是不设限,尝试去寻找自己的突破点,在自己的技术边界不断拓展,而不是不断的去做一些重复的事情。至于提升的方式那就很多了,首先一手技术资料肯定是各种官方网站,对于各种中间件的学习,看源码是最好的方式(github);想更系统化的学习话,可以读一些书、专业 paper ,都是比较不错的手段;还可以通过一些社区,跟其他人一起学习,交流心得,取长补短。避免直接网上搜索的拿来主义式学习,另外看多少不代表你会多少,付诸实践、产出结果才是我们的最终目标。结语如上,不管是什么职业,我们都需要继续提升。也有很多人觉得不仅现下的生活如一潭死水,工作也没有任何提升。其实不外乎一个原因:想太多,做太少。解决方法很简单,放手去干。
什么是工厂方法模式工厂方法模式(Factory Method Pattern)也被称为多态工厂模式,其定义了一个创建某种产品的接口,但由子类决定要实例化的产品是哪一个,从而把产品的实例化推迟到子类。何时使用工厂方法模式工厂模式一般配合策略模式一起使用,当系统中有多种产品(策略),且每种产品有多个实例时,此时适合使用工厂模式:每种产品对应的工厂提供该产品不同实例的创建功能,从而避免调用方和产品创建逻辑的耦合,完美符合迪米特法则(最少知道原则)。愉快地使用工厂方法模式背景在平常开发中,我们经常会在 Spring 中实现诸如这样的功能:收集某一类具有共同特征的 Bean(都实现了某个接口或者都打上了某个注解等),然后放入容器中(一般是 Map),使用的时候根据 Bean 的标识,来获取到对应的 Bean。比如我之前文章中的 通过表单标识获得表单对应提交处理器的 FormDataHandlerFactory:@Component public class FormDataHandlerFactory { private static final Map<String, FormDataHandler> FORM_DATA_HANDLER_MAP = new HashMap<>(16); /** * 根据表单标识,获取对应的 Handler * * @param formCode 表单标识 * @return 表单对应的 Handler */ public FormDataHandler getHandler(String formCode) { return FORM_DATA_HANDLER_MAP.get(formCode); } @Autowired public void setFormDataHandlers(List<FormDataHandler> handlers) { for (FormDataHandler handler : handlers) { FORM_DATA_HANDLER_MAP.put(handler.getFormCode(), handler); } } }通过表单项类型获得表单项转换器的 FormItemConverterFactory@Component public class FormItemConverterFactory { private static final EnumMap<FormItemTypeEnum, FormItemConverter> CONVERTER_MAP = new EnumMap<>(FormItemTypeEnum.class); /** * 根据表单项类型获得对应的转换器 * * @param type 表单项类型 * @return 表单项转换器 */ public FormItemConverter getConverter(FormItemTypeEnum type) { return CONVERTER_MAP.get(type); } @Autowired public void setConverters(List<FormItemConverter> converters) { for (final FormItemConverter converter : converters) { CONVERTER_MAP.put(converter.getType(), converter); } } }在我见过的系统中,看到过非常多类似的代码,每次需要这样的功能,就是定义一个新的 XxxFactory,甚至还有直接在调用者里面直接写上这些获取对应 Bean 的代码,直接违反 单一原则。在这个时候,其实我们已经趋近于使用工厂方法模式,我们更倾向于称这种 XxxFactory 为简单工厂。不停地使用这种简单工厂的问题在于会导致 重复的代码,因而也就自然而然的违背了 DRY 原则(Don't Repeat Yourself)。虽然重复的代码并不多,但是对于我们 Programmer 来说,写重复的代码无异于往我们脸上吐唾沫 —— 是可忍,孰不可忍!所以接下来基于上面这个场景,我分享一下我目前基于 Spring 实现工厂方法模式的 “最佳套路”(如果你有更好的套路,欢迎赐教和讨论哦)~方案其实设计模式的核心就在于,找出变化的部分,然后对变化进行抽象和封装,从而使得代码能够满足面向对象的基本原则。对于工厂方法模式来说,变化的是产品、工厂,因而我们可以先定义出抽象的产品和抽象的工厂。抽象的产品(策略):public interface Strategy<T> { /** * 获得策略的标识 */ T getId(); }每个产品必须实现 Strategy 接口,代表每个产品必须有一个唯一的标识。抽象的策略工厂:public abstract class StrategyFactory<T, S extends Strategy<T>> implements InitializingBean, ApplicationContextAware { private Map<T, S> strategyMap; private ApplicationContext appContext; /** * 根据策略 id 获得对应的策略的 Bean * * @param id 策略 id * @return 策略的 Bean */ public S getStrategy(T id) { return strategyMap.get(id); } /** * 获取策略的类型(交给子类去实现) * * @return 策略的类型 */ protected abstract Class<S> getStrategyType(); @Override public void afterPropertiesSet() { // 获取 Spring 容器中,所有 S 类型的 Bean Collection<S> strategies = appContext.getBeansOfType(getStrategyType()).values(); strategyMap = Maps.newHashMapWithExpectedSize(strategies.size()); // 将所有 S 类型的 Bean 放入到 strategyMap 中 for (final S strategy : strategies) { T id = strategy.getId(); strategyMap.put(id, strategy); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; } }Spring 容器在启动的时候,会去扫描工厂指定的类型(Class<S>)的 Bean,并将其注册到工厂中(加入到 strategyMap)。所以对于工厂中产品的生产过程,借助 Spring,我们躺好就行。接下来基于我们的抽象产品和抽象工厂,我们重构上面的两个 Factory:通过表单标识获得表单对应提交处理器的 FormDataHandlerFactory @Component public class FormDataHandlerFactory extends StrategyFactory<String, FormDataHandler> { @Override protected Class<FormDataHandler> getStrategyType() { return FormDataHandler.class; } }FormDataHandlerFactory 只需要指定一下其产品类型为 FormDataHandler。当然,FormDataHandler 我们也需要改造一下: public interface FormDataHandler extends Strategy<String> { @Override default String getId() { return getFormCode(); } String getFormCode(); CommonResponse<Object> submit(FormSubmitRequest request); }通过表单项类型获得表单项转换器的 FormItemConverterFactory@Component public class FormItemConverterFactory extends StrategyFactory<FormItemTypeEnum, FormItemConverter> { @Override protected Class<FormItemConverter> getStrategyType() { return FormItemConverter.class; } }此时,FormItemConverterFactory 也只需要指定一下产品的类型,不再会写重复代码。同理,需要改造一下 FormItemConverter:public interface FormItemConverter extends Strategy<FormItemTypeEnum> { @Override default FormItemTypeEnum getId() { return getType(); } FormItemTypeEnum getType(); FormItem convert(FormItemConfig config); }如果这个时候新加一个 通过列表标识获得列表数据拉取器的 ListDataFetcherFactory,那么首先定义出获取列表数据的接口(产品): public interface ListDataFetcher extends Strategy<String> { CommonResponse<JSONObject> fetchData(ListDataFetchRequest request); }然后再实现 ListDataFetcherFactory(工厂):@Component public class ListDataFetcherFactory extends StrategyFactory<String, ListDataFetcher> { @Override protected Class<ListDataFetcher> getStrategyType() { return ListDataFetcher.class; } }通过抽象产品 Strategy 和抽象工厂 StrategyFactory,我们的代码完美符合了 DRY 原则。优化借助反射借助反射,我们还可以使得工厂代码变得更加简单:因为如果父类包含泛型参数,且子类对泛型参数进行了具体化,那么这个具体化的泛型类型,可在运行时获取到。基于这个特性,我们可以改造 StrategyFactory: public abstract class StrategyFactory<T, S extends Strategy<T>> implements InitializingBean, ApplicationContextAware { ... /** * 通过反射获取策略的类型 * * @return 策略的类型 */ protected Class<S> getStrategyType() { // getClass 获取当前运行时实例的类,getGenericSuperclass 获得泛型父类 Type superclass = getClass().getGenericSuperclass(); ParameterizedType pt = (ParameterizedType) superclass; Type[] actualTypeArguments = pt.getActualTypeArguments(); // 获得索引为 1 的实际参数类型,即第二个实际参数的类型 Type actualTypeArgument = actualTypeArguments[1]; @SuppressWarnings("unchecked") Class<S> result = (Class<S>) actualTypeArgument; return result; } ... }那么上面三个 Factory 写起来就更简单了:@Component public class FormDataHandlerFactory extends StrategyFactory<String, FormDataHandler> {}@Component public class FormItemConverterFactory extends StrategyFactory<FormItemTypeEnum, FormItemConverter> {}@Component public class ListDataFetcherFactory extends StrategyFactory<String, ListDataFetcher> {}组合优先于继承上述的方案是通过继承,并借助泛型的反射功能,由子类来指定策略( S getStrategyType)的类型。如果工厂类型较多,那么每次新加一个工厂类,容易导致 “类爆炸”。对于上述的方案,变化的部分就是策略的类型,除了继承,我们还可以通过组合来解决这个变化。修改我们的 StrategyFactory:public class StrategyFactory<T, S extends Strategy<T>> implements InitializingBean, ApplicationContextAware { private final Class<S> strategyType; private Map<T, S> strategyMap; private ApplicationContext appContext; /** * 创建一个策略工厂 * * @param strategyType 策略的类型 */ public StrategyFactory(Class<S> strategyType) { this.strategyType = strategyType; } /** * 根据策略 id 获得对应的策略的 Bean * * @param id 策略 id * @return 策略的 Bean */ public S getStrategy(T id) { return strategyMap.get(id); } @Override public void afterPropertiesSet() { // 获取 Spring 容器中,所有 S 类型的 Bean Collection<S> strategies = appContext.getBeansOfType(strategyType).values(); strategyMap = Maps.newHashMapWithExpectedSize(strategies.size()); // 将 所有 S 类型的 Bean 放入到 strategyMap 中 for (final S strategy : strategies) { T id = strategy.getId(); strategyMap.put(id, strategy); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; } }此时 StrategyFactory 不再是抽象类,并且为 StrategyFactory 引入一个新的属性 strategyType,并且在构造 StrategyFactory 就必须设置当前工厂中的策略(产品)类型。那么对于 FormDataHandlerFactory、FormItemConverterFactory 和 ListDataFetcherFactory,我们不需要再通过继承产生,直接通过配置进行组合即可:@Configuration public class FactoryConfig { @Bean public StrategyFactory<String, FormDataHandler> formDataHandlerFactory() { return new StrategyFactory<>(FormDataHandler.class); } @Bean public StrategyFactory<FormItemTypeEnum, FormItemConverter> formItemConverterFactory() { return new StrategyFactory<>(FormItemConverter.class); } @Bean public StrategyFactory<String, ListDataFetcher> listDataFetcherFactory() { return new StrategyFactory<>(ListDataFetcher.class); } }
什么是模板模式模板模式(Template Pattern) 又叫模板方法模式,其定义了操作的流程,并将流程中的某些步骤延迟到子类中进行实现,使得子类在不改变操作流程的前提下,即可重新定义该操作的某些特定步骤。例如做菜,操作流程一般为 “准备菜”->“放油”->“炒菜”->“调味”->“装盘”,但可能对于不同的菜要放不同类型的油,不同的菜调味方式也可能不一样。何时使用模板模式当一个操作的流程较为复杂,可分为多个步骤,且对于不同的操作实现类,流程步骤相同,只有部分特定步骤才需要自定义,此时可以考虑使用模板模式。如果一个操作不复杂(即只有一个步骤),或者不存在相同的流程,那么应该使用策略模式。从这也可看出模板模式和策略模式的区别:策略模式关注的是多种策略(广度),而模板模式只关注同种策略(相同流程),但是具备多个步骤,且特定步骤可自定义(深度)。愉快地使用模板模式背景我们平台的动态表单在配置表单项的过程中,每新增一个表单项,都要根据表单项的组件类型(例如 单行文本框、下拉选择框)和当前输入的各种配置来转换好对应的 Schema 并保存在 DB 中。一开始,转换的代码逻辑大概是这样的:public class FormItemConverter { /** * 将输入的配置转变为表单项 * * @param config 前端输入的配置 * @return 表单项 */ public FormItem convert(FormItemConfig config) { FormItem formItem = new FormItem(); // 公共的表单项属性 formItem.setTitle(config.getTitle()); formItem.setCode(config.getCode()); formItem.setComponent(config.getComponent()); // 创建表单组件的属性 FormComponentProps props = new FormComponentProps(); formItem.setComponentProps(props); // 公共的组件属性 if (config.isReadOnly()) { props.setReadOnly(true); } FormItemTypeEnum type = config.getType(); // 下拉选择框的特殊属性处理 if (type == ComponentTypeEnum.DROPDOWN_SELECT) { props.setAutoWidth(false); if (config.isMultiple()) { props.setMode("multiple"); } } // 模糊搜索框的特殊属性处理 if (type == ComponentTypeEnum.FUZZY_SEARCH) { formItem.setFuzzySearch(true); props.setAutoWidth(false); } // ... 其他组件的特殊处理 // 创建约束规则 List<FormItemRule> rules = new ArrayList<>(2); formItem.setRules(rules); // 每个表单项都可有的约束规则 if (config.isRequired()) { FormItemRule requiredRule = new FormItemRule(); requiredRule.setRequired(true); requiredRule.setMessage("请输入" + config.getTitle()); rules.add(requiredRule); } // 文本输入框才有的规则 if (type == ComponentTypeEnum.TEXT_INPUT || type == ComponentTypeEnum.TEXT_AREA) { Integer minLength = config.getMinLength(); if (minLength != null && minLength > 0) { FormItemRule minRule = new FormItemRule(); minRule.setMin(minLength); minRule.setMessage("请至少输入 " + minLength + " 个字"); rules.add(minRule); } Integer maxLength = config.getMaxLength(); if (maxLength != null && maxLength > 0) { FormItemRule maxRule = new FormItemRule(); maxRule.setMax(maxLength); maxRule.setMessage("请最多输入 " + maxLength + " 个字"); rules.add(maxRule); } } // ... 其他约束规则 return formItem; } }很明显,这份代码违反了 开闭原则(对扩展开放,对修改关闭):如果此时需要添加一种新的表单项(包含特殊的组件属性),那么不可避免的要修改 convert 方法来进行新表单项的特殊处理。观察上面的代码,将配置转变为表单项 这个操作,满足以下流程:创建表单项,并设置通用的表单项属性,然后再对不同表单项的特殊属性进行处理创建组件属性,处理通用的组件属性,然后再对不同组件的特殊属性进行处理创建约束规则,处理通用的约束规则,然后再对不同表单项的特性约束规则进行处理这不正是符合模板模式的使用场景(操作流程固定,特殊步骤可自定义处理)吗?基于上面这个场景,下面我就分享一下我目前基于 Spring 实现模板模式的 “最佳套路”(如果你有更好的套路,欢迎赐教和讨论哦)~方案定义出模板即首先定义出表单项转换的操作流程,即如下的 convert 方法(使用 final 修饰,确保子类不可修改操作流程):public abstract class FormItemConverter { /** * 子类可处理的表单项类型 */ public abstract FormItemTypeEnum getType(); /** * 将输入的配置转变为表单项的操作流程 * * @param config 前端输入的配置 * @return 表单项 */ public final FormItem convert(FormItemConfig config) { FormItem item = createItem(config); // 表单项创建完成之后,子类如果需要特殊处理,可覆写该方法 afterItemCreate(item, config); FormComponentProps props = createComponentProps(config); item.setComponentProps(props); // 组件属性创建完成之后,子类如果需要特殊处理,可覆写该方法 afterPropsCreate(props, config); List<FormItemRule> rules = createRules(config); item.setRules(rules); // 约束规则创建完成之后,子类如果需要特殊处理,可覆写该方法 afterRulesCreate(rules, config); return item; } /** * 共用逻辑:创建表单项、设置通用的表单项属性 */ private FormItem createItem(FormItemConfig config) { FormItem formItem = new FormItem(); formItem.setCode(config.getCode()); formItem.setTitle(config.getTitle()); formItem.setComponent(config.getComponent()); return formItem; } /** * 表单项创建完成之后,子类如果需要特殊处理,可覆写该方法 */ protected void afterItemCreate(FormItem item, FormItemConfig config) { } /** * 共用逻辑:创建组件属性、设置通用的组件属性 */ private FormComponentProps createComponentProps(FormItemConfig config) { FormComponentProps props = new FormComponentProps(); if (config.isReadOnly()) { props.setReadOnly(true); } if (StringUtils.isNotBlank(config.getPlaceholder())) { props.setPlaceholder(config.getPlaceholder()); } return props; } /** * 组件属性创建完成之后,子类如果需要特殊处理,可覆写该方法 */ protected void afterPropsCreate(FormComponentProps props, FormItemConfig config) { } /** * 共用逻辑:创建约束规则、设置通用的约束规则 */ private List<FormItemRule> createRules(FormItemConfig config) { List<FormItemRule> rules = new ArrayList<>(4); if (config.isRequired()) { FormItemRule requiredRule = new FormItemRule(); requiredRule.setRequired(true); requiredRule.setMessage("请输入" + config.getTitle()); rules.add(requiredRule); } return rules; } /** * 约束规则创建完成之后,子类如果需要特殊处理,可覆写该方法 */ protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) { } }模板的实现针对不同的表单项,对特殊步骤进行自定义处理:/** * 下拉选择框的转换器 */ @Component public class DropdownSelectConverter extends FormItemConverter { @Override public FormItemTypeEnum getType() { return FormItemTypeEnum.DROPDOWN_SELECT; } @Override protected void afterPropsCreate(FormComponentProps props, FormItemConfig config) { props.setAutoWidth(false); if (config.isMultiple()) { props.setMode("multiple"); } } } /** * 模糊搜索框的转换器 */ @Component public class FuzzySearchConverter extends FormItemConverter { @Override public FormItemTypeEnum getType() { return FormItemTypeEnum.FUZZY_SEARCH; } @Override protected void afterItemCreate(FormItem item, FormItemConfig config) { item.setFuzzySearch(true); } @Override protected void afterPropsCreate(FormComponentProps props, FormItemConfig config) { props.setAutoWidth(false); } } /** * 通用文本类转换器 */ public abstract class CommonTextConverter extends FormItemConverter { @Override protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) { Integer minLength = config.getMinLength(); if (minLength != null && minLength > 0) { FormItemRule minRule = new FormItemRule(); minRule.setMin(minLength); minRule.setMessage("请至少输入 " + minLength + " 个字"); rules.add(minRule); } Integer maxLength = config.getMaxLength(); if (maxLength != null && maxLength > 0) { FormItemRule maxRule = new FormItemRule(); maxRule.setMax(maxLength); maxRule.setMessage("请最多输入 " + maxLength + " 个字"); rules.add(maxRule); } } } /** * 单行文本框的转换器 */ @Component public class TextInputConverter extends CommonTextConverter { @Override public FormItemTypeEnum getType() { return FormItemTypeEnum.TEXT_INPUT; } } /** * 多行文本框的转换器 */ @Component public class TextAreaConvertor extends FormItemConverter { @Override public FormItemTypeEnum getType() { return FormItemTypeEnum.TEXT_AREA; } }制作简单工厂 @Component public class FormItemConverterFactory { private static final EnumMap<FormItemTypeEnum, FormItemConverter> CONVERTER_MAP = new EnumMap<>(FormItemTypeEnum.class); /** * 根据表单项类型获得对应的转换器 * * @param type 表单项类型 * @return 表单项转换器 */ public FormItemConverter getConverter(FormItemTypeEnum type) { return CONVERTER_MAP.get(type); } @Autowired public void setConverters(List<FormItemConverter> converters) { for (final FormItemConverter converter : converters) { CONVERTER_MAP.put(converter.getType(), converter); } } }投入使用@Component public class FormItemManagerImpl implements FormItemManager { @Autowired private FormItemConverterFactory converterFactory; @Override public List<FormItem> convertFormItems(JSONArray inputConfigs) { return IntStream.range(0, inputConfigs.size()) .mapToObj(inputConfigs::getJSONObject) .map(this::convertFormItem) .collect(Collectors.toList()); } private FormItem convertFormItem(JSONObject inputConfig) { FormItemConfig itemConfig = inputConfig.toJavaObject(FormItemConfig.class); FormItemConverter converter = converterFactory.getConverter(itemConfig.getType()); if (converter == null) { throw new IllegalArgumentException("不存在转换器:" + itemConfig.getType()); } return converter.convert(itemConfig); } }Factory 只负责获取 Converter,每个 Converter 只负责对应表单项的转换功能,Manager 只负责逻辑编排,从而达到功能上的 “低耦合高内聚”。设想一次扩展此时要加入一种新的表单项 —— 数字选择器(NUMBER_PICKER),它有着特殊的约束条件:最小值和最大值,输入到 FormItemConfig 时分别为 minNumer 和 maxNumber。@Component public class NumberPickerConverter extends FormItemConverter { @Override public FormItemTypeEnum getType() { return FormItemTypeEnum.NUMBER_PICKER; } @Override protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) { Integer minNumber = config.getMinNumber(); // 处理最小值 if (minNumber != null) { FormItemRule minNumRule = new FormItemRule(); minNumRule.setMinimum(minNumber); minNumRule.setMessage("输入数字不能小于 " + minNumber); rules.add(minNumRule); } Integer maxNumber = config.getMaxNumber(); // 处理最大值 if (maxNumber != null) { FormItemRule maxNumRule = new FormItemRule(); maxNumRule.setMaximum(maxNumber); maxNumRule.setMessage("输入数字不能大于 " + maxNumber); rules.add(maxNumRule); } } }此时,我们只需要添加对应的枚举和实现对应的 FormItemConverter,并不需要修改任何逻辑代码,因为 Spring 启动时会自动帮我们处理好 NUMBER_PICKER 和 NumberPickerConverter 的关联关系 —— 完美符合 “开闭原则”。
何时使用代理模式如果想为对象的某些方法做方法逻辑之外的附属功能(例如 打印出入参、处理异常、校验权限),但是又不想(或是无法)将这些功能的代码写到原有方法中,那么可以使用代理模式。愉快地使用代理模式背景刚开始开发模型平台的时候,我们总是会需要一些业务逻辑之外的功能用于调试或者统计,例如这样: public Response processXxxBiz(Request request) { long startTime = System.currentMillis(); try { // 业务逻辑 ...... } catch (Exception ex) { logger.error("processXxxBiz error, request={}", JSON.toJSONString(request), ex) // 生成出错响应 ...... } long costTime = (System.currentMillis() - startTime); // 调用完成后,记录出入参 logger.info("processXxxBiz, costTime={}ms, request={}, response={}", costTime, JSON.toJSONString(request), JSON.toJSONString(response)); }很容易可以看出,打印出入参、记录方法耗时、捕获异常并处理 这些都是和业务没有关系的,业务方法关心的,只应该是 业务逻辑代码 才对。如果不想办法解决,长此以往,坏处就非常明显:违反了 DRY(Don't Repeat Yourself)原则,因为每个业务方法都会包括这些业务逻辑之外的且功能类似的代码违反了 单一职责 原则,业务逻辑代码和附加功能代码杂糅在一起,增加后续维护和扩展的复杂度,且容易导致类爆炸所以,为了不给以后的自己添乱,我就需要一种方式,来解决上面的问题 —— 很明显,我需要的就是代理模式:原对象的方法只需关心业务逻辑,然后由代理对象来处理这些附属功能。在 Spring 中,实现代理模式的方法多种多样,下面分享一下我目前基于 Spring 实现代理模式的 “最佳套路”(如果你有更好的套路,欢迎赐教和讨论哦)~方案大家都听过 Spring 有两大神器 —— IoC 和 AOP。AOP 即面向切面编程(Aspect Oriented Programming):通过预编译方式(CGLib)或者运行期动态代理(JDK Proxy)来实现程序功能代理的技术。在 Spring 中使用代理模式,就是 AOP 的完美应用场景,并且使用注解来进行 AOP 操作已经成为首选,因为注解实在是又方便又好用。我们简单复习下 Spring AOP 的相关概念:Pointcut(切点),指定在什么情况下才执行 AOP,例如方法被打上某个注解的时候JoinPoint(连接点),程序运行中的执行点,例如一个方法的执行或是一个异常的处理;并且在 Spring AOP 中,只有方法连接点Advice(增强),对连接点进行增强(代理):在方法调用前、调用后 或者 抛出异常时,进行额外的处理Aspect(切面),由 Pointcut 和 Advice 组成,可理解为:要在什么情况下(Pointcut)对哪个目标(JoinPoint)做什么样的增强(Advice)复习了 AOP 的概念之后,我们的方案也非常清晰了,对于某个代理场景:先定义好一个注解,然后写好相应的增强处理逻辑建立一个对应的切面,在切面中基于该注解定义切点,并绑定相应的增强处理逻辑对匹配切点的方法(即打上该注解的方法),使用绑定的增强处理逻辑,对其进行增强定义方法增强处理器我们先定义出 ”代理“ 的抽象:方法增强处理器 MethodAdviceHandler 。之后我们定义的每一个注解,都绑定一个对应的 MethodAdviceHandler 的实现类,当目标方法被代理时,由对应的 MethodAdviceHandler 的实现类来处理该方法的代理访问。/** * 方法增强处理器 * * @param <R> 目标方法返回值的类型 */ public interface MethodAdviceHandler<R> { /** * 目标方法执行之前的判断,判断目标方法是否允许执行。默认返回 true,即 默认允许执行 * * @param point 目标方法的连接点 * @return 返回 true 则表示允许调用目标方法;返回 false 则表示禁止调用目标方法。 * 当返回 false 时,此时会先调用 getOnForbid 方法获得被禁止执行时的返回值,然后 * 调用 onComplete 方法结束切面 */ default boolean onBefore(ProceedingJoinPoint point) { return true; } /** * 禁止调用目标方法时(即 onBefore 返回 false),执行该方法获得返回值,默认返回 null * * @param point 目标方法的连接点 * @return 禁止调用目标方法时的返回值 */ default R getOnForbid(ProceedingJoinPoint point) { return null; } /** * 目标方法抛出异常时,执行的动作 * * @param point 目标方法的连接点 * @param e 抛出的异常 */ void onThrow(ProceedingJoinPoint point, Throwable e); /** * 获得抛出异常时的返回值,默认返回 null * * @param point 目标方法的连接点 * @param e 抛出的异常 * @return 抛出异常时的返回值 */ default R getOnThrow(ProceedingJoinPoint point, Throwable e) { return null; } /** * 目标方法完成时,执行的动作 * * @param point 目标方法的连接点 * @param startTime 执行的开始时间 * @param permitted 目标方法是否被允许执行 * @param thrown 目标方法执行时是否抛出异常 * @param result 执行获得的结果 */ default void onComplete(ProceedingJoinPoint point, long startTime, boolean permitted, boolean thrown, Object result) { } }为了方便 MethodAdviceHandler 的使用,我们定义一个抽象类,提供一些常用的方法。 public abstract class BaseMethodAdviceHandler<R> implements MethodAdviceHandler<R> { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 抛出异常时候的默认处理 */ @Override public void onThrow(ProceedingJoinPoint point, Throwable e) { String methodDesc = getMethodDesc(point); Object[] args = point.getArgs(); logger.error("{} 执行时出错,入参={}", methodDesc, JSON.toJSONString(args, true), e); } /** * 获得被代理的方法 * * @param point 连接点 * @return 代理的方法 */ protected Method getTargetMethod(ProceedingJoinPoint point) { // 获得方法签名 Signature signature = point.getSignature(); // Spring AOP 只有方法连接点,所以 Signature 一定是 MethodSignature return ((MethodSignature) signature).getMethod(); } /** * 获得方法描述,目标类名.方法名 * * @param point 连接点 * @return 目标类名.执行方法名 */ protected String getMethodDesc(ProceedingJoinPoint point) { // 获得被代理的类 Object target = point.getTarget(); String className = target.getClass().getSimpleName(); Signature signature = point.getSignature(); String methodName = signature.getName(); return className + "." + methodName; } }定义方法切面的抽象同理,将方法切面的公共逻辑抽取出来,定义出方法切面的抽象 —— 后续每定义一个注解,对应的方法切面继承自这个抽象类就好。/** * 方法切面抽象类,由子类来指定切点和绑定的方法增强处理器的类型 */ public abstract class BaseMethodAspect implements ApplicationContextAware { /** * 切点,通过 @Pointcut 指定相关的注解 */ protected abstract void pointcut(); /** * 对目标方法进行环绕增强处理,子类需通过 pointcut() 方法指定切点 * * @param point 连接点 * @return 方法执行返回值 */ @Around("pointcut()") public Object advice(ProceedingJoinPoint point) { // 获得切面绑定的方法增强处理器的类型 Class<? extends MethodAdviceHandler<?>> handlerType = getAdviceHandlerType(); // 从 Spring 上下文中获得方法增强处理器的实现 Bean MethodAdviceHandler<?> adviceHandler = appContext.getBean(handlerType); // 使用方法增强处理器对目标方法进行增强处理 return advice(point, adviceHandler); } /** * 获得切面绑定的方法增强处理器的类型 */ protected abstract Class<? extends MethodAdviceHandler<?>> getAdviceHandlerType(); /** * 使用方法增强处理器增强被注解的方法 * * @param point 连接点 * @param handler 切面处理器 * @return 方法执行返回值 */ private Object advice(ProceedingJoinPoint point, MethodAdviceHandler<?> handler) { // 执行之前,返回是否被允许执行 boolean permitted = handler.onBefore(point); // 方法返回值 Object result; // 是否抛出了异常 boolean thrown = false; // 开始执行的时间 long startTime = System.currentTimeMillis(); // 目标方法被允许执行 if (permitted) { try { // 执行目标方法 result = point.proceed(); } catch (Throwable e) { // 抛出异常 thrown = true; // 处理异常 handler.onThrow(point, e); // 抛出异常时的返回值 result = handler.getOnThrow(point, e); } } // 目标方法被禁止执行 else { // 禁止执行时的返回值 result = handler.getOnForbid(point); } // 结束 handler.onComplete(point, startTime, permitted, thrown, result); return result; } private ApplicationContext appContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; } }此时,我们基于 AOP 的代理模式小架子就已经搭好了。之所以需要这个小架子,是为了后续新增注解时,能够进行横向的扩展:每次新增一个注解(XxxAnno),只需要实现一个新的方法增强处理器(XxxHandler)和新的方法切面 (XxxAspect),而不会修改现有代码,从而完美符合 对修改关闭,对扩展开放 设计模式理念。下面便让我们基于这个小架子,实现我们的第一个增强功能:方法调用记录(记录方法的出入参和调用时长)。定义一个注解/** * 用于产生调用记录的注解,会记录下方法的出入参、调用时长 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface InvokeRecordAnno { /** * 调用说明 */ String value() default ""; }方法增强处理器的实现 @Component public class InvokeRecordHandler extends BaseMethodAdviceHandler<Object> { /** * 记录方法出入参和调用时长 */ @Override public void onComplete(ProceedingJoinPoint point, long startTime, boolean permitted, boolean thrown, Object result) { String methodDesc = getMethodDesc(point); Object[] args = point.getArgs(); long costTime = System.currentTimeMillis() - startTime; logger.warn("\n{} 执行结束,耗时={}ms,入参={}, 出参={}", methodDesc, costTime, JSON.toJSONString(args, true), JSON.toJSONString(result, true)); } @Override protected String getMethodDesc(ProceedingJoinPoint point) { Method targetMethod = getTargetMethod(point); // 获得方法上的 InvokeRecordAnno InvokeRecordAnno anno = targetMethod.getAnnotation(InvokeRecordAnno.class); String description = anno.value(); // 如果没有指定方法说明,那么使用默认的方法说明 if (StringUtils.isBlank(description)) { description = super.getMethodDesc(point); } return description; } }方法切面的实现@Aspect @Order(1) @Component public class InvokeRecordAspect extends BaseMethodAspect { /** * 指定切点(处理打上 InvokeRecordAnno 的方法) */ @Override @Pointcut("@annotation(xyz.mizhoux.experiment.proxy.anno.InvokeRecordAnno)") protected void pointcut() { } /** * 指定该切面绑定的方法切面处理器为 InvokeRecordHandler */ @Override protected Class<? extends MethodAspectHandler<?>> getHandlerType() { return InvokeRecordHandler.class; } }@Aspect 用来告诉 Spring 这是一个切面,然后 Spring 在启动会时扫描 @Pointcut 匹配的方法,然后对这些目标方法进行织入处理:即使用切面中打上 @Around 的方法来对目标方法进行增强处理。@Order 是用来标记这个切面应该在哪一层,数字越小,则在越外层(越先进入,越后结束) —— 方法调用记录的切面很明显应该在大气层(小编:王者荣耀术语,即最外层),因为方法调用记录的切面应该最后结束,所以我们给一个小点的数字。测试现在我们就可以给开发时想要记录调用信息的方法打上这个注解,然后通过日志来观察目标方法的调用情况。老规矩,弄个 Controller : @RestController @RequestMapping("proxy") public class ProxyTestController { @GetMapping("test") @InvokeRecordAnno("测试代理模式") public Map<String, Object> testProxy(@RequestParam String biz, @RequestParam String param) { Map<String, Object> result = new HashMap<>(4); result.put("id", 123); result.put("nick", "之叶"); return result; } }然后访问:localhost/proxy/test?biz=abc&param=test看出这个输出的那一刻 —— 代理成功 —— 没错,这就是程序猿最幸福的感觉。扩展假设我们要在目标方法抛出异常时进行处理:抛出异常时,把异常信息异步发送到邮箱或者钉钉,然后根据方法的返回值类型,返回相应的错误响应。定义相应的注解/** * 用于异常处理的注解 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ExceptionHandleAnno { } 实现方法增强处理器@Component public class ExceptionHandleHandler extends BaseMethodAdviceHandler<Object> { /** * 抛出异常时的处理 */ @Override public void onThrow(ProceedingJoinPoint point, Throwable e) { super.onThrow(point, e); // 发送异常到邮箱或者钉钉的逻辑 } /** * 抛出异常时的返回值 */ @Override public Object getOnThrow(ProceedingJoinPoint point, Throwable e) { // 获得返回值类型 Class<?> returnType = getTargetMethod(point).getReturnType(); // 如果返回值类型是 Map 或者其子类 if (Map.class.isAssignableFrom(returnType)) { Map<String, Object> result = new HashMap<>(4); result.put("success", false); result.put("message", "调用出错"); return result; } return null; } }如果返回值的类型是个 Map,那么我们就返回调用出错情况下的对应 Map 实例(真实情况一般是返回业务系统中的 Response)。实现方法切面@Aspect @Order(10) @Component public class ExceptionHandleAspect extends BaseMethodAspect { /** * 指定切点(处理打上 ExceptionHandleAnno 的方法) */ @Override @Pointcut("@annotation(xyz.mizhoux.experiment.proxy.anno.ExceptionHandleAnno)") protected void pointcut() { } /** * 指定该切面绑定的方法切面处理器为 ExceptionHandleHandler */ @Override protected Class<? extends MethodAdviceHandler<?>> getAdviceHandlerType() { return ExceptionHandleHandler.class; } } 异常处理一般是非常内层的切面,所以我们将@Order 设置为 10,让 ExceptionHandleAspect 在 InvokeRecordAspect 更内层(即之后进入、之前结束),从而外层的 InvokeRecordAspect 也可以记录到抛出异常时的返回值。修改测试用的方法,加上 @ExceptionHandleAnno:@RestController @RequestMapping("proxy") public class ProxyTestController { @GetMapping("test") @ExceptionHandleAnno @InvokeRecordAnno("测试代理模式") public Map<String, Object> testProxy(@RequestParam String biz, @RequestParam String param) { if (biz.equals("abc")) { throw new IllegalArgumentException("非法的 biz=" + biz); } Map<String, Object> result = new HashMap<>(4); result.put("id", 123); result.put("nick", "之叶"); return result; } }访问:localhost/proxy/test?biz=abc&param=test,异常处理的切面先结束:方法调用记录的切面后结束:没毛病,一切是那么的自然、和谐、美好~思考小编:可以看到抛出异常时, InvokeRecordHandler 的 onThrow 方法没有执行,为什么呢?之叶:因为 InvokeRecordAspect 比 ExceptionHandleAspect 在更外层,外层的 InvokeRecordAspect 在执行时,执行的已经是内层的 ExceptionHandleAspect 代理过的方法,而对应的 ExceptionHandleHandler 已经把异常 “消化” 了,即 ExceptionHandleAspect 代理过的方法已经不会再抛出异常。小编:如果我们要 限制单位时间内方法的调用次数,比如 3s 内用户只能提交表单 1 次,似乎也可以通过这个代理模式的套路来实现。之叶:小场面。首先定义好注解(注解可以包含单位时间、最大调用次数等参数),然后在方法切面处理器的 onBefore 方法里面,使用缓存记录下单位时间内用户的提交次数,如果超出最大调用次数,返回 false,那么目标方法就不被允许调用了;然后在 getOnForbid 的方法里面,返回这种情况下的响应。
1. 案例简介这里举一个简单的常见案例:下单链路。假设我们在做一个checkout接口,需要做各种校验、查询商品信息、调用库存服务扣库存、然后生成订单:一个比较典型的代码如下:@RestController @RequestMapping("/") public class CheckoutController { @Resource private ItemService itemService; @Resource private InventoryService inventoryService; @Resource private OrderRepository orderRepository; @PostMapping("checkout") public Result<OrderDO> checkout(Long itemId, Integer quantity) { // 1) Session管理 Long userId = SessionUtils.getLoggedInUserId(); if (userId <= 0) { return Result.fail("Not Logged In"); } // 2)参数校验 if (itemId <= 0 || quantity <= 0 || quantity >= 1000) { return Result.fail("Invalid Args"); } // 3)外部数据补全 ItemDO item = itemService.getItem(itemId); if (item == null) { return Result.fail("Item Not Found"); } // 4)调用外部服务 boolean withholdSuccess = inventoryService.withhold(itemId, quantity); if (!withholdSuccess) { return Result.fail("Inventory not enough"); } // 5)领域计算 Long cost = item.getPriceInCents() * quantity; // 6)领域对象操作 OrderDO order = new OrderDO(); order.setItemId(itemId); order.setBuyerId(userId); order.setSellerId(item.getSellerId()); order.setCount(quantity); order.setTotalCost(cost); // 7)数据持久化 orderRepository.createOrder(order); // 8)返回 return Result.success(order); } }为什么这种典型的流水账代码在实际应用中会有问题呢?其本质问题是违背了SRP(Single Responsbility Principle)单一职责原则。这段代码里混杂了业务计算、校验逻辑、基础设施、和通信协议等,在未来无论哪一部分的逻辑变更都会直接影响到这段代码,长期当后人不断的在上面叠加新的逻辑时,会造成代码复杂度增加、逻辑分支越来越多,最终造成bug或者没人敢重构的历史包袱。所以我们才需要用DDD的分层思想去重构一下以上的代码,通过不同的代码分层和规范,拆分出逻辑清晰,职责明确的分层和模块,也便于一些通用能力的沉淀。主要的几个步骤分为:分离出独立的Interface接口层,负责处理网络协议相关的逻辑从真实业务场景中,找出具体用例(Use Cases),然后将具体用例通过专用的Command指令、Query查询、和Event事件对象来承接分离出独立的Application应用层,负责业务流程的编排,响应Command、Query和Event。每个应用层的方法应该代表整个业务流程中的一个节点处理一些跨层的横切关注点,如鉴权、异常处理、校验、缓存、日志等下面会针对每个点做详细的解释。2. Interface接口层随着REST和MVC架构的普及,经常能看到开发同学直接在Controller中写业务逻辑,如上面的典型案例,但实际上MVC Controller不是唯一的重灾区。以下的几种常见的代码写法通常都可能包含了同样的问题:HTTP 框架:如Spring MVC框架,Spring Cloud等RPC 框架:如Dubbo、HSF、gRPC等消息队列MQ的“消费者”:比如JMS的 onMessage,RocketMQ的MessageListener等Socket通信:Socket通信的receive、WebSocket的onMessage等文件系统:WatcherService等分布式任务调度:SchedulerX等这些的方法都有一个共同的点就是都有自己的网络协议,而如果我们的业务代码和网络协议混杂在一起,则会直接导致代码跟网络协议绑定,无法被复用。所以,在DDD的分层架构中,我们单独会抽取出来Interface接口层,作为所有对外的门户,将网络协议和业务逻辑解耦。2.1 接口层的组成接口层主要由以下几个功能组成:网络协议的转化:通常这个已经由各种框架给封装掉了,我们需要构建的类要么是被注解的bean,要么是继承了某个接口的bean。统一鉴权:比如在一些需要AppKey+Secret的场景,需要针对某个租户做鉴权的,包括一些加密串的校验Session管理:一般在面向用户的接口或者有登陆态的,通过Session或者RPC上下文可以拿到当前调用的用户,以便传递给下游服务。限流配置:对接口做限流避免大流量打到下游服务前置缓存:针对变更不是很频繁的只读场景,可以前置结果缓存到接口层异常处理:通常在接口层要避免将异常直接暴露给调用端,所以需要在接口层做统一的异常捕获,转化为调用端可以理解的数据格式日志:在接口层打调用日志,用来做统计和debug等。一般微服务框架可能都直接包含了这些功能。当然,如果有一个独立的网关设施/应用,则可以抽离出鉴权、Session、限流、日志等逻辑,但是目前来看API网关也只能解决一部分的功能,即使在有API网关的场景下,应用里独立的接口层还是有必要的。在interface层,鉴权、Session、限流、缓存、日志等都比较直接,只有一个异常处理的点需要重点说下。2.2 返回值和异常处理规范,Result vs Exception注:这部分主要还是面向REST和RPC接口,其他的协议需要根据协议的规范产生返回值。在我见过的一些代码里,接口的返回值比较多样化,有些直接返回DTO甚至DO,另一些返回Result。接口层的核心价值是对外,所以如果只是返回DTO或DO会不可避免的面临异常和错误栈泄漏到使用方的情况,包括错误栈被序列化反序列化的消耗。所以,这里提出一个规范:规范:Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常规范:Application层的所有接口返回值为DTO,不负责处理异常Application层的具体规范等下再讲,在这里先展示Interface层的逻辑。举个例子:@PostMapping("checkout") public Result<OrderDTO> checkout(Long itemId, Integer quantity) { try { CheckoutCommand cmd = new CheckoutCommand(); OrderDTO orderDTO = checkoutService.checkout(cmd); return Result.success(orderDTO); } catch (ConstraintViolationException cve) { // 捕捉一些特殊异常,比如Validation异常 return Result.fail(cve.getMessage()); } catch (Exception e) { // 兜底异常捕获 return Result.fail(e.getMessage()); } }当然,每个接口都要写异常处理逻辑会比较烦,所以可以用AOP做个注解@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ResultHandler { } @Aspect @Component public class ResultAspect { @Around("@annotation(ResultHandler)") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { Object proceed = null; try { proceed = joinPoint.proceed(); } catch (ConstraintViolationException cve) { return Result.fail(cve.getMessage()); } catch (Exception e) { return Result.fail(e.getMessage()); } return proceed; } }然后最终代码则简化为:@PostMapping("checkout") @ResultHandler public Result<OrderDTO> checkout(Long itemId, Integer quantity) { CheckoutCommand cmd = new CheckoutCommand(); OrderDTO orderDTO = checkoutService.checkout(cmd); return Result.success(orderDTO); }2.3 接口层的接口的数量和业务间的隔离在传统REST和RPC的接口规范中,通常一个领域的接口,无论是REST的Resource资源的GET/POST/DELETE,还是RPC的方法,是追求相对固定的,统一的,而且会追求统一个领域的方法放在一个领域的服务或Controller中。但是我发现在实际做业务的过程中,特别是当支撑的上游业务比较多时,刻意去追求接口的统一通常会导致方法中的参数膨胀,或者导致方法的膨胀。举个例子:假设有一个宠物卡和一个亲子卡的业务公用一个开卡服务,但是宠物需要传入宠物类型,亲子的需要传入宝宝年龄。// 可以是RPC Provider 或者 Controller public interface CardService { // 1)统一接口,参数膨胀 Result openCard(int petType, int babyAge); // 2)统一泛化接口,参数语意丢失 Result openCardV2(Map<String, Object> params); // 3)不泛化,同一个类里的接口膨胀 Result openPetCard(int petType); Result openBabyCard(int babyAge); }可以看出来,无论是怎么操作,都有可能导致CardService这个服务未来越来越难以维护,方法越来越多,一个业务的变更有可能会导致整个服务/Controller的变更,最终变得无法维护。我曾经参与过的一个服务,提供了几十个方法,上万行代码,可想而知无论是使用方对接口的理解成本还是对代码的维护成本都是极高的。所以,这里提出另一个规范:规范:一个Interface层的类应该是“小而美”的,应该是面向“一个单一的业务”或“一类同样需求的业务”,需要尽量避免用同一个类承接不同类型业务的需求。基于上面的这个规范,可以发现宠物卡和亲子卡虽然看起来像是类似的需求,但并非是“同样需求”的,可以预见到在未来的某个时刻,这两个业务的需求和需要提供的接口会越走越远,所以需要将这两个接口类拆分开:public interface PetCardService { Result openPetCard(int petType); } public interface BabyCardService { Result openBabyCard(int babyAge); }这个的好处是符合了Single Responsibility Principle单一职责原则,也就是说一个接口类仅仅会因为一个(或一类)业务的变化而变化。一个建议是当一个现有的接口类过度膨胀时,可以考虑对接口类做拆分,拆分原则和SRP一致。也许会有人问,如果按照这种做法,会不会产生大量的接口类,导致代码逻辑重复?答案是不会,因为在DDD分层架构里,接口类的核心作用仅仅是协议层,每类业务的协议可以是不同的,而真实的业务逻辑会沉淀到应用层。也就是说Interface和Application的关系是多对多的:因为业务需求是快速变化的,所以接口层也要跟着快速变化,通过独立的接口层可以避免业务间相互影响,但我们希望相对稳定的是Application层的逻辑。所以我们接下来看一下Application层的一些规范。3. Application层3.1 Application层的组成部分Application层的几个核心类:ApplicationService应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑DTO Assembler:负责将内部领域模型转化为可对外的DTOCommand、Query、Event对象:作为ApplicationService的入参返回的DTO:作为ApplicationService的出参Application层最核心的对象是ApplicationService,它的核心功能是承接“业务流程“。但是在讲ApplicationService的规范之前,必须要先重点的讲几个特殊类型的对象,即Command、Query和Event。3.2 Command、Query、Event对象从本质上来看,这几种对象都是Value Object,但是从语义上来看有比较大的差异:Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)。Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。简单总结下:CommandQueryEvent语意”希望“能触发的操作各种条件的查询已经发生过的事情读/写写只读通常是写返回值DTO 或 BooleanDTO 或 CollectionVoid为什么要用CQE对象?通常在很多代码里,能看到接口上有多个参数,比如上文中的案例:Result<OrderDO> checkout(Long itemId, Integer quantity);如果需要在接口上增加参数,考虑到向前兼容,则需要增加一个方法:Result<OrderDO> checkout(Long itemId, Integer quantity); Result<OrderDO> checkout(Long itemId, Integer quantity, Integer channel);或者常见的查询方法,由于条件的不同导致多个方法:List<OrderDO> queryByItemId(Long itemId); List<OrderDO> queryBySellerId(Long sellerId); List<OrderDO> queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize);可以看出来,传统的接口写法有几个问题:接口膨胀:一个查询条件一个方法难以扩展:每新增一个参数都有可能需要调用方升级难以测试:接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护但是另外一个最重要的问题是:这种类型的参数罗列,本身没有任何业务上的”语意“,只是一堆参数而已,无法明确的表达出来意图。CQE的规范:所以在Application层的接口里,强力建议的一个规范是:规范:ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。唯一可以的例外是根据单一ID查询的情况,可以省略掉一个Query对象的创建按照上面的规范,实现案例是:public interface CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd); List<OrderDTO> query(OrderQuery query); OrderDTO getOrder(Long orderId); // 注意单一ID查询可以不用Query } @Data public class CheckoutCommand { private Long userId; private Long itemId; private Integer quantity; } @Data public class OrderQuery { private Long sellerId; private Long itemId; private int currentPage; private int pageSize; }这个规范的好处是:提升了接口的稳定性、降低低级的重复,并且让接口入参更加语意化。CQE vs DTO从上面的代码能看出来,ApplicationService的入参是CQE对象,但是出参却是一个DTO,从代码格式上来看都是简单的POJO对象,那么他们之间有什么区别呢?CQE:CQE对象是ApplicationService的输入,是有明确的”意图“的,所以这个对象必须保证其”正确性“。DTO:DTO对象只是数据容器,只是为了和外部交互,所以本身不包含任何逻辑,只是贫血对象。但可能最重要的一点:因为CQE是”意图“,所以CQE对象在理论上可以有”无限“个,每个代表不同的意图;但是DTO作为模型数据容器,和模型一一对应,所以是有限的。CQE的校验CQE作为ApplicationService的输入,必须保证其正确性,那么这个校验是放在哪里呢?在最早的代码里,曾经有这样的校验逻辑,当时写在了服务里:if (itemId <= 0 || quantity <= 0 || quantity >= 1000) { return Result.fail("Invalid Args"); }这种代码在日常非常常见,但其最大的问题就是大量的非业务代码混杂在业务代码中,很明显的违背了单一职责原则。但因为当时入参仅仅是简单的int,所以这个逻辑只能出现在服务里。现在当入参改为了CQE之后,我们可以利用java标准JSR303或JSR380的Bean Validation来前置这个校验逻辑。规范:CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现前面的例子可以改造为:@Validated // Spring的注解 public class CheckoutServiceImpl implements CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd) { // 这里@Valid是JSR-303/380的注解 // 如果校验失败会抛异常,在interface层被捕捉 } } @Data public class CheckoutCommand { @NotNull(message = "用户未登陆") private Long userId; @NotNull @Positive(message = "需要是合法的itemId") private Long itemId; @NotNull @Min(value = 1, message = "最少1件") @Max(value = 1000, message = "最多不能超过1000件") private Integer quantity; }这种做法的好处是,让ApplicationService更加清爽,同时各种错误信息可以通过Bean Validation的API做各种个性化定制。避免复用CQE因为CQE是有“意图”和“语意”的,我们需要尽量避免CQE对象的复用,哪怕所有的参数都一样,只要他们的语意不同,尽量还是要用不同的对象。规范:针对于不同语意的指令,要避免CQE对象的复用❌ 反例:一个常见的场景是“Create创建”和“Update更新”,一般来说这两种类型的对象唯一的区别是一个ID,创建没有ID,而更新则有。所以经常能看见有的同学用同一个对象来作为两个方法的入参,唯一区别是ID是否赋值。这个是错误的用法,因为这两个操作的语意完全不一样,他们的校验条件可能也完全不一样,所以不应该复用同一个对象。正确的做法是产出两个对象:public interface CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd); OrderDTO updateOrder(@Valid UpdateOrderCommand cmd); } @Data public class UpdateOrderCommand { @NotNull(message = "用户未登陆") private Long userId; @NotNull(message = "必须要有OrderID") private Long orderId; @NotNull @Positive(message = "需要是合法的itemId") private Long itemId; @NotNull @Min(value = 1, message = "最少1件") @Max(value = 1000, message = "最多不能超过1000件") private Integer quantity; }3.3 ApplicationServiceApplicationService负责了业务流程的编排,是将原有业务流水账代码剥离了校验逻辑、领域计算、持久化等逻辑之后剩余的流程,是“胶水层”代码。参考一个简易的交易流程:在这个案例里可以看出来,交易这个领域一共有5个用例:下单、支付成功、支付失败关单、物流信息更新、关闭订单。这5个用例可以用5个Command/Event对象代替,也就是对应了5个方法。我见过3种ApplicationService的组织形态:1. 一个ApplicationService类是一个完整的业务流程,其中每个方法负责处理一个Use Case。这种的好处是可以完整的收敛整个业务逻辑,从接口类即可对业务逻辑有一定的掌握,适合相对简单的业务流程。坏处就是对于复杂的业务流程会导致一个类的方法过多,有可能代码量过大。这种类型的具体案例如:public interface CheckoutService { // 下单 OrderDTO checkout(@Valid CheckoutCommand cmd); // 支付成功 OrderDTO payReceived(@Valid PaymentReceivedEvent event); // 支付取消 OrderDTO payCanceled(@Valid PaymentCanceledEvent event); // 发货 OrderDTO packageSent(@Valid PackageSentEvent event); // 收货 OrderDTO delivered(@Valid DeliveredEvent event); // 批量查询 List<OrderDTO> query(OrderQuery query); // 单个查询 OrderDTO getOrder(Long orderId); }2. 针对于比较复杂的业务流程,可以通过增加独立的CommandHandler、EventHandler来降低一个类中的代码量:@Component public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> { @Override public OrderDTO handle(CheckoutCommand cmd) { // } } public class CheckoutServiceImpl implements CheckoutService { @Resource private CheckoutCommandHandler checkoutCommandHandler; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { return checkoutCommandHandler.handle(cmd); } }3. 比较激进一点,通过CommandBus、EventBus,直接将指令或事件抛给对应的Handler,EventBus比较常见。具体案例代码如下,通过消息队列收到MQ消息后,生成Event,然后由EventBus做路由到对应的Handler:// Application层 // 在这里框架通常可以根据接口识别到这个负责处理PaymentReceivedEvent // 也可以通过增加注解识别 @Component public class PaymentReceivedHandler implements EventHandler<PaymentReceivedEvent> { @Override public void process(PaymentReceivedEvent event) { // } } // Interface层,这个是RocketMQ的Listener public class OrderMessageListener implements MessageListenerOrderly { @Resource private EventBus eventBus; @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { PaymentReceivedEvent event = new PaymentReceivedEvent(); eventBus.dispatch(event); // 不需要指定消费者 return ConsumeOrderlyStatus.SUCCESS; } }⚠️ 不建议:这种做法可以实现Interface层和某个具体的ApplicationService或Handler的完全静态解藕,在运行时动态dispatch,做的比较好的框架如AxonFramework。虽然看起来很便利,但是根据我们自己业务的实践和踩坑发现,当代码中的CQE对象越来越多,handler越来越复杂时,运行时的dispatch缺乏了静态代码间的关联关系,导致代码很难读懂,特别是当你需要trace一个复杂调用链路时,因为dispatch是运行时的,很难摸清楚具体调用到的对象。所以我们虽然曾经有过这种尝试,但现在已经不建议这么做了。Application Service 是业务流程的封装,不处理业务逻辑虽然之前曾经无数次重复ApplicationService只负责业务流程串联,不负责业务逻辑,但如何判断一段代码到底是业务流程还是逻辑呢?举个之前的例子,最初的代码重构后:@Service @Validated public class CheckoutServiceImpl implements CheckoutService { private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE; @Resource private ItemService itemService; @Resource private InventoryService inventoryService; @Resource private OrderRepository orderRepository; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { ItemDO item = itemService.getItem(cmd.getItemId()); if (item == null) { throw new IllegalArgumentException("Item not found"); } boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity()); if (!withholdSuccess) { throw new IllegalArgumentException("Inventory not enough"); } Order order = new Order(); order.setBuyerId(cmd.getUserId()); order.setSellerId(item.getSellerId()); order.setItemId(item.getItemId()); order.setItemTitle(item.getTitle()); order.setItemUnitPrice(item.getPriceInCents()); order.setCount(cmd.getQuantity()); Order savedOrder = orderRepository.save(order); return orderDtoAssembler.orderToDTO(savedOrder); } }判断是否业务流程的几个点:1、不要有if/else分支逻辑:也就是说代码的Cyclomatic Complexity(循环复杂度)应该尽量等于1通常有分支逻辑的,都代表一些业务判断,应该将逻辑封装到DomainService或者Entity里。但这不代表完全不能有if逻辑,比如,在这段代码里:boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity()); if (!withholdSuccess) { throw new IllegalArgumentException("Inventory not enough"); }虽然CC > 1,但是仅仅代表了中断条件,具体的业务逻辑处理并没有受影响。可以把它看作为Precondition。2、不要有任何计算:在最早的代码里有这个计算:// 5)领域计算 Long cost = item.getPriceInCents() * quantity; order.setTotalCost(cost);通过将这个计算逻辑封装到实体里,避免在ApplicationService里做计算@Data public class Order { private Long itemUnitPrice; private Integer count; // 把原来一个在ApplicationService的计算迁移到Entity里 public Long getTotalCost() { return itemUnitPrice * count; } } order.setItemUnitPrice(item.getPriceInCents()); order.setCount(cmd.getQuantity());3、一些数据的转化可以交给其他对象来做:比如DTO Assembler,将对象间转化的逻辑沉淀在单独的类中,降低ApplicationService的复杂度OrderDTO dto = orderDtoAssembler.orderToDTO(savedOrder);常用的ApplicationService“套路”我们可以看出来,ApplicationService的代码通常有类似的结构:AppService通常不做任何决策(Precondition除外),仅仅是把所有决策交给DomainService或Entity,把跟外部交互的交给Infrastructure接口,如Repository或防腐层。一般的“套路”如下:准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步操作。如果涉及到对多个外部系统(包括自身的DB)都有变更的情况,这个时候通常处在“分布式事务”的场景里,无论是用分布式TX、TCC、还是Saga模式,取决于具体场景的设计,在此处暂时略过。3.4 DTO Assembler一个经常被忽视的问题是 ApplicationService应该返回 Entity 还是 DTO?这里提出一个规范,在DDD分层架构中:ApplicationService应该永远返回DTO而不是Entity为什么呢?构建领域边界:ApplicationService的入参是CQE对象,出参是DTO,这些基本上都属于简单的POJO,来确保Application层的内外互相不影响。降低规则依赖:Entity里面通常会包含业务规则,如果ApplicationService返回Entity,则会导致调用方直接依赖业务规则。如果内部规则变更可能直接影响到外部。通过DTO组合降低成本:Entity是有限的,DTO可以是多个Entity、VO的自由组合,一次性封装成复杂DTO,或者有选择的抽取部分参数封装成DTO可以降低对外的成本。因为我们操作的对象是Entity,但是输出的对象是DTO,这里就需要一个专属类型的对象叫DTO Assembler。DTO Assembler的唯一职责是将一个或多个Entity/VO,转化为DTO。注意:DTO Assembler通常不建议有反操作,也就是不会从DTO到Entity,因为通常一个DTO转化为Entity时是无法保证Entity的准确性的。通常,Entity转DTO是有成本的,无论是代码量还是运行时的操作。手写转换代码容易出错,为了节省代码量用Reflection会造成极大的性能损耗。所以这里我还是不遗余力的推荐MapStruct这个库。MapStruct通过静态编译时代码生成,通过写接口和配置注解就可以生成对应的代码,且因为生成的代码是直接赋值,其性能损耗基本可以忽略不计。通过MapStruct,代码即可简化为:import org.mapstruct.Mapper; @Mapper public interface OrderDtoAssembler { OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class); OrderDTO orderToDTO(Order order); } public class CheckoutServiceImpl implements CheckoutService { private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { // ... Order order = new Order(); // ... Order savedOrder = orderRepository.save(order); return orderDtoAssembler.orderToDTO(savedOrder); } }结合之前的Data Mapper,DTO、Entity和DataObject之间的关系如下图:3.5 Result vs Exception最后,上文曾经提及在Interface层应该返回Result,在Application层应该返回DTO,在这里再次重复提出规范:Application层只返回DTO,可以直接抛异常,不用统一处理。所有调用到的服务也都可以直接抛异常,除非需要特殊处理,否则不需要刻意捕捉异常异常的好处是能明确的知道错误的来源,堆栈等,在Interface层统一捕捉异常是为了避免异常堆栈信息泄漏到API之外,但是在Application层,异常机制仍然是信息量最大,代码结构最清晰的方法,避免了Result的一些常见且繁杂的Result.isSuccess判断。所以在Application层、Domain层,以及Infrastructure层,遇到错误直接抛异常是最合理的方法。3.6 简单讲一下Anti-Corruption Layer防腐层本文仅仅简单描述一下ACL的原理和作用,具体的实施规范可能要等到另外一篇文章。在ApplicationService中,经常会依赖外部服务,从代码层面对外部系统产生了依赖。比如上文中的:ItemDO item = itemService.getItem(cmd.getItemId()); boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());会发现我们的ApplicationService会强依赖ItemService、InventoryService以及ItemDO这个对象。如果任何一个服务的方法变更,或者ItemDO字段变更,都会有可能影响到ApplicationService的代码。也就是说,我们自己的代码会因为强依赖了外部系统的变化而变更,这个在复杂系统中应该是尽量避免的。那么如何做到对外部系统的隔离呢?需要加入ACL防腐层。ACL防腐层的简单原理如下:对于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的VO或DTO类构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类针对外部系统调用,同样的用Facade方法封装外部调用链路无防腐层的情况:有防腐层的情况:具体简单实现,假设所有外部依赖都命名为ExternalXXXService:// 自定义的内部值类 @Data public class ItemDTO { private Long itemId; private Long sellerId; private String title; private Long priceInCents; } // 商品Facade接口 public interface ItemFacade { ItemDTO getItem(Long itemId); } // 商品facade实现 @Service public class ItemFacadeImpl implements ItemFacade { @Resource private ExternalItemService externalItemService; @Override public ItemDTO getItem(Long itemId) { ItemDO itemDO = externalItemService.getItem(itemId); if (itemDO != null) { ItemDTO dto = new ItemDTO(); dto.setItemId(itemDO.getItemId()); dto.setTitle(itemDO.getTitle()); dto.setPriceInCents(itemDO.getPriceInCents()); dto.setSellerId(itemDO.getSellerId()); return dto; } return null; } } // 库存Facade public interface InventoryFacade { boolean withhold(Long itemId, Integer quantity); } @Service public class InventoryFacadeImpl implements InventoryFacade { @Resource private ExternalInventoryService externalInventoryService; @Override public boolean withhold(Long itemId, Integer quantity) { return externalInventoryService.withhold(itemId, quantity); } }通过ACL改造之后,我们ApplicationService的代码改为:@Service public class CheckoutServiceImpl implements CheckoutService { @Resource private ItemFacade itemFacade; @Resource private InventoryFacade inventoryFacade; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { ItemDTO item = itemFacade.getItem(cmd.getItemId()); if (item == null) { throw new IllegalArgumentException("Item not found"); } boolean withholdSuccess = inventoryFacade.withhold(cmd.getItemId(), cmd.getQuantity()); if (!withholdSuccess) { throw new IllegalArgumentException("Inventory not enough"); } // ... } }很显然,这么做的好处是ApplicationService的代码已经完全不再直接依赖外部的类和方法,而是依赖了我们自己内部定义的值类和接口。如果未来外部服务有任何的变更,需要修改的是Facade类和数据转化逻辑,而不需要修改ApplicationService的逻辑。Repository可以认为是一种特殊的ACL,屏蔽了具体数据操作的细节,即使底层数据库结构变更,数据库类型变更,或者加入其他的持久化方式,Repository的接口保持稳定,ApplicationService就能保持不变。在一些理论框架里ACL Facade也被叫做Gateway,含义是一样的。4. Orchestration vs Choreography在本文最后想聊一下复杂业务流程的设计规范。在复杂的业务流程里,我们通常面临两种模式:Orchestration 和 Choreography。很无奈,这两个英文单词的百度翻译/谷歌翻译,都是“编排”,但实际上这两种模式是完全不一样的设计模式。Orchestration的编排(比如SOA/微服务的服务编排Service Orchestration)是我们通常熟悉的用法,Choreography是最近出现了事件驱动架构EDA才慢慢流行起来。网上可能会有其他的翻译,比如编制、编舞、协作等,但感觉都没有真正的把英文单词的意思表达出来,所以为了避免误解,在下文我尽量还是用英文原词。如果谁有更好的翻译方法欢迎联系我。4.1 模式简介Orchestration:通常出现在脑海里的是一个交响乐团(Orchestra,注意这两个词的相似性),如下图。交响乐团的核心是一个唯一的指挥家Conductor,在一个交响乐中,所有的音乐家必须听从Conductor的指挥做操作,不可以独自发挥。所以在Orchestration模式中,所有的流程都是由一个节点或服务触发的。我们常见的业务流程代码,包括调用外部服务,就是Orchestration,由我们的服务统一触发。图片来源:https://insights-images.thoughtworks.com/orchestrator20music_d80f70dfd1857ae9778fee2f8fa9a01d.pngChoreography:通常会出现在脑海的场景是一个舞剧(来自于希腊文的舞蹈,Choros),如下图。其中每个不同的舞蹈家都在做自己的事,但是没有一个中心化的指挥。通过协作配合,每个人做好自己的事,整个舞蹈可以展现出一个完整的、和谐的画面。所以在Choreography模式中,每个服务都是独立的个体,可能会响应外部的一些事件,但整个系统是一个整体。图片来源:https://i.ytimg.com/vi/uQaQrO4hKPQ/maxresdefault.jpg4.2 案例用一个常见的例子:下单后支付并发货如果这个案例是Orchestration,则业务逻辑为:下单时从一个预存的账户里扣取资金,并且生成物流单发货,从图上看是这样的:如果这个案例是Choreography,则业务逻辑为:下单,然后等支付成功事件,然后再发货,类似这样:4.3 模式的区别和选择虽然看起来这两种模式都能达到一样的业务目的,但是在实际开发中他们有巨大的差异:从代码依赖关系来看:Orchestration:涉及到一个服务调用到另外的服务,对于调用方来说,是强依赖的服务提供方。Choreography:每一个服务只是做好自己的事,然后通过事件触发其他的服务,服务之间没有直接调用上的依赖。但要注意的是下游还是会依赖上游的代码(比如事件类),所以可以认为是下游对上游有依赖。从代码灵活性来看:Orchestration:因为服务间的依赖关系是写死的,增加新的业务流程必然需要修改代码。Choreography:因为服务间没有直接调用关系,可以增加或替换服务,而不需要改上游代码。从调用链路来看:Orchestration:是从一个服务主动调用另一个服务,所以是Command-Driven指令驱动的。Choreography:是每个服务被动的被外部事件触发,所以是Event-Driven事件驱动的。从业务职责来看:Orchestration:有主动的调用方(比如:下单服务)。无论下游的依赖是谁,主动的调用方都需要为整个业务流程和结果负责。Choreography:没有主动调用方,每个服务只关心自己的触发条件和结果,没有任何一个服务会为整个业务链路负责总结下来一个比较:OrchestrationChoreography驱动力指令驱动Command-Driven事件驱动Event-Driven调用依赖上游强依赖下游无直接调用依赖但是有代码依赖可以认为是下游依赖上游灵活性较差较高业务职责上游为业务负责无全局责任人另外需要重点明确的:“指令驱动”和“事件驱动”的区别不是“同步”和“异步”。指令可以是同步调用,也可以是异步消息触发(但异步指令不是事件);反过来事件可以是异步消息,但也完全可以是进程内的同步调用。所以指令驱动和事件驱动差异的本质不在于调用方式,而是一件事情是否“已经”发生。所以在日常业务中当你碰到一个需求时,该如何选择是用Orchestration还是Choreography?这里给出两个判断方法:1. 明确依赖的方向:在代码中的依赖是比较明确的:如果你是下游,上游对你无感知,则只能走事件驱动;如果上游必须要对你有感知,则可以走指令驱动。反过来,如果你是上游,需要对下游强依赖,则是指令驱动;如果下游是谁无所谓,则可以走事件驱动。2. 找出业务中的“负责人”:第二种方法是根据业务场景找出其中的“负责人”。比如,如果业务需要通知卖家,下单系统的单一职责不应该为消息通知负责,但订单管理系统需要根据订单状态的推进主动触发消息,所以是这个功能的负责人。在一个复杂业务流程里,通常两个模式都要有,但也很容易设计错误。如果出现依赖关系很奇怪,或者代码里调用链路/负责人梳理不清楚的情况,可以尝试转换一下模式,可能会好很多。哪个模式更好?很显然,没有最好的模式,只有最合适自己业务场景的模式。❌ 反例:最近几年比较流行的Event-Driven Architecture(EDA)事件驱动架构,以及Reactive-Programming响应式编程(比如RxJava),虽然有很多创新,但在一定程度上是“当你有把锤子,所有问题都是钉子”的典型案例。他们对一些基于事件的、流处理的问题有奇效,但如果拿这些框架硬套指令驱动的业务,就会感到代码极其“不协调”,认知成本提高。所以在日常选型中,还是要先根据业务场景梳理出来是哪些流程中的部分是Orchestration,哪些是Choreography,然后再选择相对应的框架。4.4 跟DDD分层架构的关系最后,讲了这么多O vs C,跟DDD有啥关系?很简单:O&C其实是Interface层的关注点,Orchestration = 对外的API,而Choreography = 消息或事件。当你决策了O还是C之后,需要在interface层承接这些“驱动力”。无论O&C如何设计,Application层都“无感知”,因为ApplicationService天生就可以处理Command、Query和Event,至于这些对象怎么来,是Interface层的决策。所以,虽然Orchestration 和 Choreography是两种完全不同的业务设计模式,但最终落到Application层的代码应该是一致的,这也是为什么Application层是“用例”而不是“接口”,是相对稳定的存在。5. 总结只要是做业务的,一定会需要写业务流程和服务编排,但不代表这种代码一定质量差。通过DDD的分层架构里的Interface层和Application层的合理拆分,代码可以变得优雅、灵活,能更快的响应业务但同时又能更好的沉淀。本文主要介绍了一些代码的设计规范,帮助大家掌握一定的技巧。Interface层:职责:主要负责承接网络协议的转化、Session管理等接口数量:避免所谓的统一API,不必人为限制接口类的数量,每个/每类业务对应一套接口即可,接口参数应该符合业务需求,避免大而全的入参接口出参:统一返回Result异常处理:应该捕捉所有异常,避免异常信息的泄漏。可以通过AOP统一处理,避免代码里有大量重复代码。Application层:入参:具像化Command、Query、Event对象作为ApplicationService的入参,唯一可以的例外是单ID查询的场景。CQE的语意化:CQE对象有语意,不同用例之间语意不同,即使参数一样也要避免复用。入参校验:基础校验通过Bean Validation api解决。Spring Validation自带Validation的AOP,也可以自己写AOP。出参:统一返回DTO,而不是Entity或DO。DTO转化:用DTO Assembler负责Entity/VO到DTO的转化。异常处理:不统一捕捉异常,可以随意抛异常。部分Infra层:用ACL防腐层将外部依赖转化为内部代码,隔离外部的影响业务流程设计模式:没有最好的模式,取决于业务场景、依赖关系、以及是否有业务“负责人”。避免拿着锤子找钉子。5.1 前瞻预告CQRS是Application层的一种设计模式,是基于Command和Query分离的一种设计理念,从最简单的对象分离,到目前最复杂的Event-Sourcing。这个topic有很多需要深入的点,也经常可以被用到,特别是结合复杂的Aggregate。后面单独会拉出来讲,标题暂定为《CQRS的7层境界》在当今复杂的微服务开发环境下,依赖外部团队开发的服务是不可避免的,但强耦合带来的成本(无论是变更、代码依赖、甚至Maven Jar包间接依赖)是一个复杂系统长期不可忽视的点。ACL防腐层是一种隔离理念,将外部耦合去除,让内部代码更加纯粹。ACL防腐层可以有很多种,Repository是一种特殊的面相数据持久化的ACL,K8S-sidecar-istio 可以说是一种网络层的ACL,但在Java/Spring里可以有比Istio更高效、更通用的方法,待后文介绍。当你开始用起来DDD时,会发现很多代码模式都非常类似,比如主子订单就是总分模式、类目体系的CPV模式也可以用到一些活动上,ECS模式可以在互动业务上发挥作用等等。后面会尝试总结出一些通用的领域设计模式,他们的设计思路、可以解决的问题类型、以及实践落地的方法。6. 欢迎联系,持续求简历欢迎看到这里的同学给我提任何关于DDD的问题,我会尽可能的回答。文章中的代码案例会稍后申请发布到github上,供大家参考。我的邮箱:guangmiao.lgm@alibaba-inc.com,也可以加我的钉钉号:luangm(殷浩)同时,我们团队也在持续招聘。我团队负责淘系的行业和导购业务,包括天猫和淘宝的四大行业(服饰、快消、消电、家装)以及淘宝的几个大横向业务(企业服务、全球购、有好货等)的日常业务需求和创新业务(3D/AR、360全景视频、搭配、定制、尺码导购、SPU导购等)、前台场(iFashion、全球购、有好货等),以及一些复杂的金融、交易、履约链路(IP撮合、金融服务、交易定制、分销、CPS分佣、服务供应链对接等),总DAU(日均访问用户数)大概3000W左右。我们团队对接了大量的业务形态,从前台导购到后台履约,有极其丰富的应用场景。新的财年我们希望能深入行业,挖掘新的商业模式和履约链路,覆盖一些传统B2C模式无法覆盖到的商业模式,帮助商家在新的赛道成长。欢迎感兴趣的同学加盟。
设计目标标准化:Web Canvas标准主要指的是W3C的Canvas2D和WebGL。标准化的好处一方面是学习成本低,另一方面上层的游戏引擎也可以以很低的适配成本得到复用;跨平台:跨平台主要目地是为了扩宽使用场景、提升研发效率、降低维护成本;跨容器: 由于业务形态的不同,Canvas需要能够跑在多种异构容器上,如小程序、小游戏、小部件、Weex等等;高性能: 正所谓「勿在浮沙筑高台」,上层业务的性能很大程度取决于Canvas的实现;可扩展: 从下文的Canvas分层设计上可以看到,每一层的技术选型都是多样化的,不同场景可能会选择不同的实现方案,因此架构上需要有一定的可扩展性,最好能够做到关键模块可插拔、可替换。Canvas渲染引擎原理概览▐ 工作原理工作原理其实比较简单,一句话就可以说明白。首先封装图形API(OpenGL、Vulkan、Metal...)以支持WebGL和Canvas 2D矢量图渲染能力,对下桥接到不同操作系统和容器之上,对上通过language binding将渲染能力以标准化接口透出到业务容器的JS上下文。举个例子,以下是淘宝小程序容器Canvas组件的渲染流程,省略了「亿」点点细节。Canvas在Android上其实是一个SurfaceView/TextureView,通过同层渲染的方式嵌入到UCWebView中。开发者调用Canvas JS接口,最终会生成一系列的渲染指令送到GPU,渲染结果写入图形缓冲区,在合适时机通过SwapBuffer交换缓冲区,然后操作系统进行图层合成和送显。▐ 分层架构从业务形态上看,不管是小程序、小游戏还是其他容器,实现上都是相似的,如下图所示,通过JSBinding实现标准Canvas接口,开发者可以通过适配在上面跑web游戏引擎(laya、egret、threejs...),下边是JS引擎,这一层可以有不同的技术选型,如老牌的V8、JSC,后起之秀quickjs、hermes等等,在这之下就是Canvas核心实现了,这一层需要分别提供WebGL、Canvas2D的能力。WebGL较为简单,基本与OpenGLES接口一一对应,简单封装即可。Canvas 2D如果要从零开始实现的话相对来说会复杂一些(特别是文字、图片、路径的渲染等),不过技术选型上仍然有很多选择比如cairo、skia、nanovg等等,不管使用哪种方案,只要是硬件渲染,其backend只有vulkan/OpenGLES/metal/Direct3D等几种选择。目前OpenGL使用最为广泛,还可以通过google的Angle项目适配到vulkan/directx等不同backend上。Canvas实现层之下是WAL窗体抽象层,这一层的职责就是为渲染提供宿主环境,通过EGL/EAGL等方式绑定GL上下文与平台窗体系统。下文将对相关模块的实现分别进行介绍。考虑到性能、可移植性等因素,除了与平台/容器桥接的部分需要使用OC/Java等语言实现之外,其余部分基本采用C++实现。JS Binding机制JS引擎通常会抽象出VM、JSContext、JSValue、GlobalObject等概念,VM代表一个JS虚拟机实例,拥有独立的堆栈空间,有点类似进程的概念,不同的VM相互是隔离的(因此在v8中以v8::Isolate命名),一个VM中可以有多个JSContext,JSContext代表一个JS的执行上下文,可以执行JS代码,JSValue代表一个JS值类型,可以是基础数据类型也可以是Object类型,每个JSContext中都会拥有一个GlobalObject对象,GlobalObject在JSContext整个生命周期内,都可以直接进行访问,它默认是可读可写的,因此可以在GlobalObject上绑定属性或者函数等,这样就可以在JSContext执行上下文中访问它们了。要想在JS环境中使用Canvas,需要将Canvas相关接口注入到JS环境,正如Java JNI、Python Binding、Lua Binding等类似,JS引擎也提供了Extension机制,称之为JS Binding,它允许开发者使用c++等语言向JS上下文中注入变量、函数、对象等。// V8函数绑定示例 static void LogCallback(const v8::FunctionCallbackInfo<v8::Value>& args){...} ... // Create a template for the global object and set the // built-in global functions. v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate); global->Set(v8::String::NewFromUtf8(isolate, "log"), v8::FunctionTemplate::New(isolate, LogCallback)); // Each processor gets its own context so different processors // do not affect each other. v8::Persistent<v8::Context> context = v8::Context::New(isolate, nullptr, global);以小程序环境为例,小程序容器初始化时,会分别创建Render和Worker,Render负责界面渲染,Worker负责执行业务逻辑,拥有独立JSContext,Canvas提供了createCanvas()和createOffscreenCanvas() 全局函数需要绑定到该JSContext的GlobalObject上,因此Worker需要有一个时机通知canvas注入API,从小程序视角来看,Worker依赖Canvas显然不合理,因此小程序提供了插件机制,每个插件都是一个动态库,Canvas作为插件先注册到Worker,随后Worker创建之后会扫描一遍插件,依次dlopen每个插件并执行插件的初始化函数,将JSContext作为参数传给插件,这样插件就可以向JSContext中绑定API了。关于JSEngine和Binding有两个需要注意的点(以V8为例):关于线程安全。JSContext通常设计为非线程安全的,需要注意不要在非JS线程中访问JS资源。其次,在V8中一个线程可能有多个JSContext,需要使用v8::Context::Scope切换正确的JSContext;关于Binding对象的生命周期。众所周知,C与JS语言内存管理方式不一样,C需要开发者手动管理内存,JS由虚拟机管理。对于C++ Binding的JS对象的生命周期理论上需要跟普通JS对象一致,因此需要有一种机制,当JS对象被GC回收时,需要通知到C++ Binding对象,以便执行相应的析构函数释放内存。事实上,JS引擎通常会提供让一个JS对象脱离/回归GC管理的机制,且JS对象的生命周期均有钩子函数可以进行监听。V8中有Handle(句柄)的概念,Handle分为LocalHandle、PersistentHandle、Weak PersistentHandle。LocalHandle在栈上分配,由HandleScope控制其作用域,超出作用域即被标记为可释放,PersistentHandle在堆上分配,生命周期长,通常需要开发者显式通过PersistentHandle#Reset的方式释放对象。通过SetWeak函数可以让一个PersistentHandle转为一个Weak PersistentHandle,当没有其他引用指向Weak句柄时就会触发回调,开发者可以在回调中释放内存。最后再讨论下Binding代码如何跨JSEngine的问题。当前主流的JSEngine有V8、JavaScriptCore、QuickJS等,如果需要更换JSEngine的话,Binding代码需要重写,成本有点高(Canvas接口非常多),因此理论上可以再封装一个抽象层,屏蔽不同引擎的差异,对外提供一致接口,基于抽象层编写一次Binding代码,就可以适配到多个JSEngine(使用IDL生成代码是另外一条路),目前我们使用了UC团队提供的JSI SDK适配多JS引擎。平台窗体抽象层设计要想做到跨平台,就需要设计一个抽象的平台胶水层,胶水层的职责是对下屏蔽各个平台间的实现差异,对上为Canvas提供统一的接口操作Surface,封装MakeCurrent、SwapBuffer等行为。实现上可以借鉴Flutter Engine,Flutter Engine的Shell模块对GL胶水层做了较好的封装,可以无缝接入到Android、iOS等主流平台,扩展到新平台比如鸿蒙OS也不在话下。当设计好GL胶水层接口后,分平台进行实现即可。以Android为例,如果想创建一个GL上下文并绘制到屏幕上,必须通过EGL绑定平台窗体环境,即Surface或者是ANativeWindow对象,而能够创建Surface的View只有SurfaceView和TextureView(如果是一个全屏游戏没有其他Native View的话,还可以考虑直接使用NativeActivity,这里先不考虑这种情况),应该如何选择?这里可以从渲染原理上分析下两者的差异再分场景进行决策。先看SurfaceView的渲染流程,简单来说分为如下几个步骤(硬件加速场景):通过SurfaceView申请的Surface创建EGL环境;Surface通过dequeueBuffer向SurfaceFlinger请求一块GraphicBuffer(可理解为一块内存,用于存储绘图数据),随后所有绘制内容都会写到这块Buffer上;当调用EGL swapBuffer之后,会将GraphicBuffer入队到BufferQueue;SurfaceFlinger在下一个VSYNC信号到来时,取GraphicBuffer,进行合成上屏;对比SurfaceView,TextureView的渲染流程更长一些,主要经历以下关键阶段:通过TextureView绑定的SurfaceTexture创建EGL环境;生产端(Surface)通过dequeueBuffer从SurfaceTexture管理的BufferQueue中获得一块GraphicBuffer,后续所有绘制内容都会写到这块Buffer上;当调用EGL swapBuffer之后,会将GraphicBuffer入队到SurfaceTexture内部的BufferQueue;随后TextureView触发frameAvailable,通知系统进行重绘(view#invalidate);系统在下次VSYNC信号到来的时候进行重绘,在UI线程生成DisplayList,然后驱动渲染线程进行真正渲染;渲染线程会将步骤2中的GraphicBuffer作为一张特殊的纹理(GL_TEXTURE_EXTERNAL_OES)上传,与View Hierarchy上其他视图一起通过SurfaceFlinger进行合成;由以上两者的渲染流程对比可发现,SurfaceView的优势是渲染链路短、性能好,但是相比普通的View,没法支持Transform动画,通常全屏的游戏、视频播放器优先选择SurfaceView。而TextureView则弥补了SurfaceView的缺陷,它跟普通的View完全兼容,同样会走HWUI渲染,不过缺陷是内存占用比SurfaceView高,渲染需要在多个线程之间同步整体性能不如SurfaceView。具体如何选择需要分场景来看,以我们为例,我们这边同时支持在SurfaceView和TextureView中渲染,但是由于目前主要服务于淘宝小程序互动业务,而在小程序容器中,需要通过UC提供的WebView同层渲染技术将Canvas嵌入到WebView中,由于业务上需要同时支持全屏和非全屏互动,且需要支持各种CSS效果,因此只能选择EmbedSurface模式,而EmbedSurface不支持SurfaceView,因此我们选择的是TextureView。渲染管线Canvas渲染引擎的核心当然是渲染了,上层的互动业务的性能表现,很大程度取决于Canvas的渲染管线设计是否足够优秀。这一部分会分别讨论Canvas2D/WebGL的渲染管线技术选型及具体的方案设计。▐ Canvas2D Rendering Context基础能力从Canvas2D标准来看,引擎需要提供的原子能力如下:路径绘制,包括直线、矩形、贝塞尔曲线等等;路径填充、描边、裁剪、混合,样式与颜色设置等;图元变换(transform)操作;文本与位图渲染等。软件渲染 VS 硬件渲染软件渲染指的是使用CPU渲染图形,而硬件渲染则是利用GPU。使用GPU的优势一方面是可以降低CPU的使用率,另外GPU的特性(擅长并行计算、浮点数运算等)也使其性能通常会更好。但是GPU在发展的过程中,更多关注的是三维图形的运算,二维矢量图形的渲染似乎关注的较少,因此可以看到像freetype、cairo、skia等早期主要都是使用CPU渲染,虽然khronos组织推出了OpenVG标准,但是也并没有推广开来。目前主流的移动设备都自带GPU,因此对于Canvas2D的技术选型来说,我们更倾向于使用硬件加速的引擎,具体分析可以接着往下看。技术选型Canvas2D的实现成本颇高,从零开始写也不太现实,好在社区中有很多关于Canvas 2D矢量绘制的库,这里仅列举了一部分比较有影响力的,主要从backend、成熟度、移植成本等角度进行评判,详细如下表所示。Cairo和Skia是老牌的2D矢量图形渲染引擎了,成熟度和稳定性都很高,且同时支持软件与硬件渲染(cairo的硬件渲染支持比较晚),性能上通常skia占优(也看具体case),不过体积大的多。nanovg和GCanvas以小而美著称,性能上GCanvas更优秀一点,nanovg需要经过特别的定制与调优,文字渲染也不尽如人意。Blend2D是一个后起之秀,通过引入并发渲染、JIT编译等特性宣称比Caico性能更优,不过目前还在beta阶段,且硬伤是只支持软件渲染,没办法利用GPU硬件能力。最后ejecta项目最早是为了在非浏览器环境支持W3C Canvas标准,有OpenGLES backend,自带JSBinding实现,不过可惜的是现在已无人维护,性能表现也比较一般。我认为技术选型没有最好的方案,只有最适合团队的方案,从实现角度来看,以上列举的方案均可以达到目标,但是没有银弹,选择不同的方案对技术同学的要求、产品的维护成本、性能&稳定性、扩展性等均会产生深远的影响。以我们团队为例,业务形态上看主要服务于淘系互动小程序业务,面向的是淘宝开放平台上的商家、ISV开发者等, 我们对于Canvas渲染引擎最主要的诉求是跨平台渲染一致性、性能、稳定性,因此nanovg、blend2d、ejecta不满足需求。从团队资源的角度看,我们更倾向于使用开箱即用、维护成本低的方案,ejecta、GCanvas不满足需求。最后从组织架构上看,我们团队主要负责手淘跨平台相关产品,其中包括Flutter,而Flutter自带了skia,它同时满足开箱即用、高性能&高可用等特点,而且由于Chromium同样使用了skia,因此渲染一致性也得到了保证,所以复用skia对于我们来说是相对比较优的选择,但与此同时我们的包大小也增大了很多,未来需要持续优化包大小。渲染管线细节这里主要介绍下基于Skia的Canvas 2D渲染流程。JSBinding代码的实现较简单,可以参考chromium Canvas 2D的实现,这里就不展开了。看下渲染的流程,关键步骤如下,其中4~6步与当前Flutter Engine基本保持一致:开发者创建Canvas对象,并通过 Canvas.getContext('2d') 获取2D上下文;通过2D上下文调用Canvas Binding API,内部实际上通过SkCanvas调用Skia的绘图API,不过此时并没有绘制,而是将绘图命令记录下来;当平台层收到Vsync信号时,会调度到JS线程通知到Canvas;Canvas收到信号后,停止记录命令,生成SkPicture对象(其实就是个DisplayList),封装成PictureLayer,添加到LayerTree,发送到GPU线程;GPU线程Rasterizer模块收到LayerTree之后,会拿到Picture对象,交给当前Window Surface关联的SkCanvas;这个SkCanvas先通过Picture回放渲染命令,再根据当前backend选择vulkan、GL或者metal图形API将渲染指令提交到GPU。文字渲染文字渲染其实非常复杂,这里仅作简要介绍。目前字体的事实标准是OpenType和TrueType,它们通过使用贝塞尔曲线的方式定义字体的形状,这样可以保证字体与分辨率无关,可以输出任意大小的文字而不会变形或者模糊。众所周知,OpenGL并没有提供直接的方式用于绘制文字,最容易想到的方式是先在CPU上加载字体文件,光栅化到内存,然后作为GL纹理上传到GPU,目前业界用的最广泛的是 Freetype 库,它可以用来加载字体文件、处理字形,生成光栅化的位图数据。如果每个文字对应一张纹理显然代价非常高,主流的做法是使用 texture atlas 的方式将所有可能用到的文字全部写到一张纹理上,进行缓存,然后根据uv坐标选择正确的文字,有点类似雪碧图。以上还只是文字的渲染,当涉及到多语言、国际化时,情况会变得更加复杂,比如阿拉伯语、印度语中连字(Ligatures)的处理,LTR/RTL布局的处理等,Harfbuzz 库就是专门用来干这个的,可以开箱即用。从Canvas2D的文字API来看,只需要提供文本测量和基本的渲染的能力即可,使用OpenGL+Freetype+Harfbuzz通常就够用了,但是如果是一个GUI应用如Android、Flutter,那么还需要处理断句断行、排版、emoji、字体库管理等逻辑,Android提供了一个minikin库就是用来干这个的,Flutter中的txt模块二次封装了minikin,提供了更友好的API。目前我们的Canvas引擎的文字渲染模块跟Flutter保持一致,直接复用libtxt,使用起来比较简单。上面涉及到的一些库链接如下:Freetype: https://www.freetype.org/Harfbuzz: https://harfbuzz.github.io/minikin: https://android.googlesource.com/platform/frameworks/minikin/flutter txt:https://github.com/flutter/engine/blob/master/third_party/txt位图渲染位图渲染的基本流程是下载图片 -> 图片解码 -> 获得位图像素数据 -> 作为纹理上传GPU -> 渲染位图,拿到像素数据后,就可以上传到GPU作为一张纹理进行渲染。不过由于上传像素数据也是个耗时过程,可以放到独立的线程做,然后通过Share GLContext的方式使用纹理,这也是Flutter目前的做法,Flutter会使用独立的IO线程用于异步上传纹理,通过Share Context与GPU线程共享纹理,与Flutter不一样的是,我们的图片下载和解码直接代理给原生的图片库来做。▐ WebGL Rendering ContextWebGL实现比2D要简单的多,因为WebGL的API基本与OpenGLES一一对应,只需要对OpenGLES API简单进行封装即可。这里不再介绍OpenGL本身的渲染管线,而主要关注下WebGL Binding层的设计,从技术实现上主要分为单线程模型和双线程模型。单线程模型即直接在JS线程发起GL调用,这种方式调用链路最短,在一般场景性能不会有大的问题。但是由于WebGL的API调用与业务逻辑的执行都在JS线程,而某些复杂场景每帧会调用大量的WebGL API,这可能会导致JS线程阻塞。通过profile可以发现,这个场景JS线程的阻塞可能并不在GPU,而是在CPU,原因是JS引擎Binding调用本身的性能损耗也很可观,有一种优化方案是引入Command Buffer优化JSBinding链路损耗,如下图所示。这个方案的思路是这样的,JS侧封装一个虚拟的 WebGLRenderingContext 对象,API与W3C标准一致,但是其实现并不调用Native侧的JSBinding接口,而是按照指定规则对WebGL Call进行编码,存储到ArrayBuffer中,然后在特定时机(如收到VSync信号或者时执行到同步API时)通过一个Binding接口(上图flushCommands)将ArrayBuffer一次性传到Native侧,之后Native对ArrayBuffer中的指令查表、解析,最后执行渲染,这样做可以减少JSBinding的调用频率,假设ArrayBuffer中存储了N条同步指令,那么只需要执行1次Binding调用,减少了(N-1)次Binding调用的耗时,从而提升了整体性能。双线程模型指的是将GL调用转移到独立的渲染线程执行,解放JS线程的压力。具体的做法可以参考chromium GPU Command Buffer(注意这里的Command Buffer与上面提到的解决的并不是同一个问题,不要混淆),思路是这样的,JS线程收到Binding调用后,并不直接提交,而是先encode到Command Buffer(通常使用Ring buffer数据结构)缓存起来,随后在渲染线程中访问CommandBuffer,进行Decode,调用真正的GL命令,双线程模型实现要复杂的多,需要考虑Lock Free&WaitFree、同步、参数拷贝等问题,写的不好可能性能还不如单线程模型。最后再提一句,在chromium中,不仅实现了多线程的WebGL渲染模型,还支持了多进程Command Buffer的模型,使用多进程模型可以有效屏蔽各种硬件兼容性问题,带来更好的稳定性。▐ 离屏渲染离屏Canvas在Web中还是个实验特性,不过因为其实用性,目前主流的小游戏/小程序容器基本都实现了。使用到离屏Canvas的主要是2D的 drawImage 接口以及WebGL的 texImage2D/texSubImage2D 接口,WebGL通常会使用离屏Canvas渲染文本或者做一些游戏场景的预热等等。离屏渲染通常会使用PBuffer或者FBO来实现:PBuffer: 需要通过PBuffer创建新的GL Context,每次渲染都需要切换GL上下文;FBO: FBO是OpenGL提供的能力,通过 glGenFramebuffers 创建FBO,可以绑定并渲染到纹理,并且不需要切换GL上下文,性能通常会更好些(没有做过测试,严格来说也不一定,因为目前移动端GPU主要采用TBR架构,切换FrameBuffer可能会造成Tile Cache失效,导致性能下降)。除了上面两种方案之外,Android上还可以通过SurfaceTexture(本质上是EGLImage)实现离屏渲染,不过这是一种特殊的纹理类型,只能绑到GL_TEXTURE_EXTERNAL_OES上。特别地,对于2D来说,还可以通过CPU软件渲染来间接实现离屏渲染。离屏渲染中比较影响性能的地方是上传离屏Canvas数据到在屏Canvas,如果先readPixels再upload性能会比较差。解决方案是将离屏Canvas渲染到纹理,再通过OpenGL shareContext的方式与在屏Canvas共享纹理。这样,对于在屏Canvas来说就可以直接复用这个纹理了,具体点,对于在屏2D Context的drawImage来说,可以基于该纹理创建texture backend SkImage,然后作为图片上传。对于在屏WebGL Context的texImage2D来说,有几种方式,一种方式提供非标API,调用该API将直接绑定离屏Canvas所对应的纹理,开发者不用自己再创建纹理。另一种方式是texImage2D时,通过FBO拷贝离屏纹理到开发者当前绑定的纹理上。还有一种方式是在texImage2D时,先删除用户当前绑定的纹理,然后再绑定到离屏Canvas所对应的纹理,这种方案有一定使用风险,因为被删除的纹理可能还会被开发者用到。帧同步机制所谓帧同步指的是游戏渲染循环与操作系统的显示子系统(在Android平台即为SurfaceFlinger)和底层硬件之间的同步。众所周知,在GPU加速模式下,我们在屏幕上看到的游戏或者动画需要先在CPU上完成游戏逻辑的运算,然后生成一系列渲染指令,再交由GPU进行渲染,GPU的渲染结果写入FrameBuffer,最终会由显示设备刷新到屏幕。显示设备的刷新频率(即刷新率)通常是固定的,移动设备主流的刷新频率是60HZ,也即每秒刷新60次,但是GPU渲染的速度却是不固定的,它取决于绘制帧的复杂程度。这会导致两个问题,一是帧率不稳定,用户体验差;二是当GPU渲染频率高于刷新频率时,会导致丢帧、抖动或者屏幕tearing的现象。解决这个问题的方案是引入双缓冲和垂直同步(VSYNC),双缓冲指的是准备两块图形缓冲区,BackBuffer给GPU用于渲染,FrontBuffer由显示设备进行显示,这样可以提高系统的吞吐量,提高帧率并减少丢帧的情况。垂直同步是为了协调绘制的步调与屏幕刷新的步调一致,GPU必须等到屏幕完整刷新上一帧之后再进行渲染,因为GPU渲染频率高于刷新率通常是没有意义的。在PC机上早期的垂直同步是用软件模拟的,不过NVIDA和AMD后来分别出了G-SYNC和FreeSync,需要各家的硬件配合。而Android平台上是在Android4.x引入了VSYNC机制,在之后的版本还引入了RenderThread、TripleBuffer(三缓冲)等关键特性,极大提高了Android应用的流畅度。以下是Android平台的渲染模型,一次完整的渲染(GPU加速下)大致会经过如下几个阶段:HWC产生VSYNC事件,分别发给SurfaceFlinger合成进程与App进程;App UI线程(通过Choreographer)收到VSYNC信号后,处理用户输入(input)、动画、视图更新等事件,然后将绘图指令更新到DisplayList中,随后驱动渲染线程执行绘制;渲染线程解析DisplayList,调用hwui/skia绘图模块将渲染指令发给GPU;GPU进行绘制,绘制结果写入图形缓冲区(GraphicBuffer);SurfaceFlinger进程收到VSYNC信号,取图形缓存区内容进行合成;显示设备刷新,屏幕最终显示相应画面;值得注意的是,默认情况下App与SurfaceFlinger同时收到VSYNC信号,App生产第N帧,而SurfaceFlinger合成第N-1帧画面,也即App第N帧产生的数据在第N+1次VSYNC到来时才会显示到屏幕。VSYNC+双缓冲的模型保证了帧率的稳定,但是会导致输出延迟,且并不能解决卡顿、丢帧等问题,当UI线程有耗时操作、渲染场景过于复杂、App内存占用高等等场景就会导致丢帧。丢帧从系统层面上看原因主要是由于CPU/GPU不能在规定的时间内生产帧数据导致SurfaceFlinger只能使用前一帧的数据去合成,Android通过引入VSYNC offset、Triple Buffer等策略进行了一定程度的优化,不过要想帧率流畅主要还是得靠开发者分场景去做针对性的优化。与原生的渲染流程类似,Canvas渲染引擎的绘制流程也是由VSYNC驱动的,在Android平台上可以通过 Choreographer注册VSYNC Callback,当VSYNC信号到来时,就可以执行一次Canvas 2D/WebGL的绘制。以WebGL单线程模型为例,一次绘制过程如下:在JS线程,游戏引擎调用Canvas WebGLContext执行WebGL Binding调用;在Android UI线程,Canvas收到平台VSYNC信号;通过消息队列调度到JS线程,在JS线程遍历Canvas实例,找到所有WebGL渲染上下文;对每个需要执行渲染(dirty)的WebGL上下文执行SwapBuffer;这里其实还涉及到一个问题,如果当前Canvas渲染的内容未发生变化,是否还需要监听VSYNC信号? 这就是所谓的OnDemand Rendering和Continuously Rendering模型。在 OnDemand 模型下,应用层调用了Canvas API就会标记状态为dirty同时向系统请求VSYNC,下一次收到VSYNC callback时执行绘制,而在Continuously 模型下,会一直向系统请求下一次VSYNC,在VSYNC Callback时再去判断是否需要绘制。理论上OnDemand模型更为合理,避免了不必要的通信,功耗更低, 不过Continuously模型实现上更为简单。Android与Flutter均采用了OnDemand模型,而我们则同时支持两种模式。以上仅仅考虑了Canvas自身的渲染流程,在上文窗体环境搭建中,Android平台我们最终选择了TextureView作为Canvas的Render Target,那么在引入了TextureView之后,从操作系统的角度看,宏观的渲染流程又是怎样的呢? 我画了这张图,为简单起见,这里以TextureView Thread代表Canvas的渲染线程。TextureView基于SurfaceTexture,由于没有独立Surface,渲染合成依赖于Android HWUI,TextureView生产完一帧的数据后,还需触发一次view invalidate,再走一次ViewRootImpl#doTraversal流程,因此整体流水线更长,从图上可知,在没有丢帧的情况下,显示也会延迟,第N帧的绘制在第N+2帧才会显示到屏幕上。同时,TextureView下卡顿、丢帧的情况也更为复杂,有时即使FPS很高但是依然感觉卡顿,下面是常见的两种丢帧情况。第一种丢帧情况是第N帧TextureView线程渲染超时,导致错过了N+1帧UI线程的绘制。第二种丢帧情况是UI线程卡顿而TextureView线程渲染较快,导致第N+1帧时UI线程上传的是TextureView第N+1帧的纹理,而第N帧的纹理被忽略掉了。以上可见,在游戏等重渲染场景,SurfaceView是比TextureView更好的选择,另外,分析卡顿往往需要对整个系统的底层机制有较深了解才能顺利解决问题,这对开发者也提出了更高的要求。调试最后讨论下调试的话题。对于Canvas渲染引擎,传统的调试方法如日志、断点调试、systrace对于问题诊断依然十分有用。不过由于引擎会用到Java/OC/C++/JS等语言,调试的链路大大延长,开发者需要根据经验或者对问题的分析进行针对性的调试,有一定的难度。除了使用上面几种方式调试之外,还可以使用一些GPU调试工具辅助,下面简要介绍下。▐ Gapid(Graphic API Debugger)Gapid是Android平台提供的GPU调试工具,功能十分强大,它可以Inspect 任意Android应用的OpenGLES/Vulkan调用,无论是系统的GL上下文(如hwui/skia等)还是应用自己创建的GL上下文都能追踪到,细化到每一帧的话,可以查看该帧所有的Draw Call、GL状态机的运行状态、FrameBuffer内容、创建的Texture、Shader、Program等等。通过这个工具除了可以验证渲染正确性之外,还可以辅助性能调优(如频繁的上下文切换、大纹理的分配等等)、诊断可能发生的GPU内存泄露等等。▐ Snapdragon ProfilerSnapdragon Profiler是高通开发一款GPU调试工具,使用了高通芯片的设备应该都能使用。这个工具也提供了类似的GPU Profiler的工具,可以抓帧分析,不过个人觉得没有gapid好用。除此之外,snapdragon还提供了实时性能分析的功能,可以查看CPU、GPU、网络、FPS、电量等等全方位的性能数据,比Android Studio更强大。有兴趣的同学可以研究下。总结以上基本讲清楚了如何实现一个跨平台Canvas引擎,然而这还只是第一步,还有更多的挑战在前面,比如Canvas与容器层的研发链路、生产链路如何协同? 如何保障线上功能的稳定性?如何管控内存使用?如何优化启动速度等等。另外,对于复杂游戏来说,游戏引擎的使用必不可少,游戏引擎使用Canvas作为渲染接口并不是性能最佳的方案,如果可以将游戏引擎中的通用逻辑下沉,提供更高阶API,势必会对性能带来更大的提升。
首先,尝试分析下题主感到空虚、似懂非懂的原因,从问题描述来看原因可能有以下几方面:▐ 目标不清晰在项目学习之前,是否有认真梳理和思考过,希望通过项目学习到哪些技术、重点需掌握哪些知识点?这些知识点又属于自己技术体系中哪个环节,是需要必须熟练掌握还是了解原理即可?相信只有明确目标之后才有学习侧重点和方向。▐ 学习方法项目学习过程中,是否有带着问题和思考?比如项目核心需要解决的问题场景、使用了哪些技术方案,为什么需要这些技术,方案选择考虑主要有哪些?系统模块这样分层和实现的好处是?这个方法的实现,性能是否可以进一步优化等等。如果只是纯粹跟着视频将项目代码机械敲一遍,我认为跟练习打字没任何区别,写出来的代码也是没有灵魂如行尸走肉。我相信只有结合自己的思考和理解,才可能赋予新的灵魂,做到知其然知其所以然,相关知识点也才能真正转化为自己的技术。▐ 复习与应用纸上得来终觉浅,绝知此事要躬行,相信对编程而言更是如此,唯有实践才能出真知。对项目中学到的相关技术、知识点需要在不同场景反复练习和应用,并对过程中遇到的问题不断总结和反思。其次,回到题主问题,如何吃透一个Java项目?从个人经验来看,大致可以从以下几方面入手:▐ 项目背景了解学习之前,先对项目业务背景和技术体系做大致的了解,这点非常重要,一是为了解项目核心要解决问题域,二是知道系统涉及哪些技术体系,这样在学习之前可以有相关技术知识准备,以便更轻松高效学习。另外,学习完之后也可以清楚知道,什么样问题可以使用什么技术、什么方案来解决、如何解决的。▐ 系统设计文档学习对项目和系统大概了解之后,可以开始对系统设计文档熟悉,建议按照架构文档、概要设计、详细设计方式递进。通过设计文档的学习,可以快速对各系统模块有个框架性认识,知道各系统职责、边界、如何交互、系统核心模型等等。对于设计文档的学习,切不可走马观花,一定要带着问题和思考。比如项目背景中的核心业务问题,架构师是如何转化成技术落地,方案为什么要这样设计,模型为什么要这样抽象,这样做的好处是什么等等?同时,对不理解的问题做需好笔记,以便后续向老师或其他同事请教或讨论等等。▐ 系统熟悉和代码阅读通过设计文档的学习,对系统设计有整体了解之后,接下来就可以结合业务场景、相关问题去看代码如何实现了。不过代码阅读,也需要注意方式方法,切不可陷入代码细节,应该自顶向下、分层分模块的阅读,以先整体、后模块、单功能点的方式层层递进。先快速走读整个代码模块逻辑,然后再精读某个类、方法的实现。代码阅读过程中,建议一边阅读一边整理相关代码模块、流程分支、交互时序,以及类图等,以便更好理解,有些IDE工具也可根据代码自动生成,比如IntelliJ IDEA。代码阅读除了关注具体功能的实现之外,更重要的是需要关心代码设计上的思路和原理、性能考究、设计模式、以及设计原则的应用等。同样,阅读代码注释也非常重要,在研究一个API或方法实现时,先认真阅读代码注释会让你事半功倍,尽可能不要做从代码中反推逻辑和功能的事情。最后,对于核心功能代码建议分模块精读,不明白部分可借助代码调试。然后,对于技术学习这块我给几点个人建议,以供题主参考:▐ 制定学习规划梳理一份适合自己的技术规划,并制定明确的学习路线和计划,让学习更有方向和重点。同样在视频课程的选择上也会更清晰,知道什么样视频该学、什么不该学,也不容易感到迷茫和空虚。如今网上各种学习资料、视频汗牛充栋,学会如何筛选有效、适合自己的信息非常重要。▐ 思考与练习对于技术编程,无捷径可言,思考和练习都非常重要,需要不断学习、思考、实践反复操练。从了解、会用、知原理、优化不断演进。结合学习计划,可以给自己制定不同挑战,比如学习spring可以尝试自己实现一个ioc容器等等。另外,工作或学习过程中遇到的问题,也是你快速提升技术能力的一个好方法,也请珍惜你遇到的每个问题的机会。时间允许的话,也请尽可能去帮助别人解答问题,像stackoverflow就是个非常不错的选择,帮助别人的同时提升自己。▐ 分享与交流保持思考总结的习惯,将学到的技术多与人分享交流,教学相长。多与优秀的程序员一起、多参与优秀的开源项目等。最后,我再以我们团队Dubbo核心开发@哲良 大神的另一开源框架TransmittableThreadLocal(TTL)为例,来讲解下我们该如何学习和快速掌握一个项目。结合上文所述,首先我会将TTL项目相关文档、issues列表认真阅读一遍,让自己对项目能有个大体的认识,并梳理出项目一些关键信息,比如:▐ 核心要解决的问题用于解决「在线程池或线程会被复用情况下,如何解决线程ThreadLocal传值问题」▐ 有哪些典型业务场景分布式跟踪系统或全链路压测(即链路打标)日志收集记录系统上下文Session级Cache应用容器或上层框架跨应用代码给下层SDK传递信息▐ 使用到的技术有线程、线程池、ThreadLocal、InheritableThreadLocal、并发、线程安全等。然后,再结合使用文档编写几个测试demo,通过程序代码练习和框架使用,一步步加深对框架的理解。比如我这里首先会拿TTL与原生JDK InheritableThreadLocal进行不同比较,体验两者的核心区别。public class ThreadLocalTest {public class ThreadLocalTest { private static final AtomicInteger ID_SEQ = new AtomicInteger(); private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1, r -> new Thread(r, "TTL-TEST-" + ID_SEQ.getAndIncrement())); // private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>(); //⑴ 声明TransmittableThreadLocal类型的ThreadLocal //private static ThreadLocal<String> THREAD_LOCAL = new TransmittableThreadLocal<>(); public static void testThreadLocal() throws InterruptedException { try { //doSomething()... THREAD_LOCAL.set("set-task-init-value"); // Runnable task1 = () -> { try { String manTaskCtx = THREAD_LOCAL.get(); System.out.println("task1:" + Thread.currentThread() + ", get ctx:" + manTaskCtx); THREAD_LOCAL.set("task1-set-value"); } finally { THREAD_LOCAL.remove(); } }; EXECUTOR.submit(task1); //doSomething.... TimeUnit.SECONDS.sleep(3); //⑵ 设置期望task2可获取的上下文 THREAD_LOCAL.set("main-task-value"); //⑶ task2的异步任务逻辑中期望获取⑵中的上下文 Runnable task2 = () -> { String manTaskCtx = THREAD_LOCAL.get(); System.out.println("task2:" + Thread.currentThread() + ", get ctx :" + manTaskCtx); }; //⑷ 转换为TransmittableThreadLocal 增强的Runnable //task2 = TtlRunnable.get(task2); EXECUTOR.submit(task2); }finally { THREAD_LOCAL.remove(); } } public static void main(String[] args) throws InterruptedException { testThreadLocal(); } } //InheritableThreadLocal 运行结果: task1:Thread[TTL-TEST-0,5,main], get ctx:set-task-init-value task2:Thread[TTL-TEST-0,5,main], get ctx :null //TransmittableThreadLocal 运行结果 task1:Thread[TTL-TEST-0,5,main], get ctx:set-task-init-value task2:Thread[TTL-TEST-0,5,main], get ctx :main-task-value 通过代码运行结果,我们可以直观看到使用JDK原生InheritableThreadLocal,在task2异步任务中是无法正确获取代码⑵处所设置的上下文参数,只有改用TransmittableThreadLocal之后,程序才如我们预期正常获取。不难发现,由JDK原生ThreadLocal切换到TransmittableThreadLocal,只需要做极少量的代码适配即可。//private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>(); //⑴ 声明TransmittableThreadLocal类型的ThreadLocal private static ThreadLocal<String> THREAD_LOCAL = new TransmittableThreadLocal<>(); ... //⑷ 转换为TransmittableThreadLocal 增强的Runnable task2 = TtlRunnable.get(task2);相信看到这里我们都会不禁想问,为什么只需要简单的更改两行代码,就可以平滑实现上下文透传?TTL框架背后具体都做了哪些工作,到底是怎么实现的呢?相信你和我一样都会比较好奇,也一定有想立马阅读源码一探究竟的冲动。不过,通常这个时候,我并不会一头扎进源码,一般都会先做几项准备工作,一是回到设计文档再仔细的阅读下相关实现方案,把关键流程和原理了解清楚;二是把涉及到的技术体相关的基础知识再复习或学习一遍,以避免由于一些基础知识原理的不了解,导致源码无法深入研究或花费大量精力。像这里如果我对Thread、ThreadLocal、InheritableThreadLocal、线程池等相关知识不熟悉的话,一定会把相关知识先学习一遍,比如ThreadLocal基本原理、底层数据结构、InheritableThreadLocal如何实现父子线程传递等等。假设这里你对这些知识都已掌握,如果不熟悉,网上相关介绍文章也早已是汗牛充栋,你搜索学习下即可。这里我们先带着到底如何实现的这个疑问,一起来探究下核心源码实现。首先把源码clone下来导入IDE,然后结合文档把系统工程结构和各功能模块职责快速熟悉一遍,然后结合文档和Demo找到关键接口和实现类,利用IDE把相关类图结构生成出来,以便快速理解类之间关系。非常不错,TTL整体代码非常精练、命名和包信息描述也都非常规范和清晰,我们可以快速圈出来。从类图中我们可以清晰看到核心关键类TransmittableThreadLocal是从ThreadLocal继承而来,这样的好处是不破坏ThreadLocal原生能力的同时还可增强和扩展自有能力,也可保证业务代码原有互操作性和最小改动。然后结合Demo代码,我们不难发现使用TTL主要有三个步骤,TransmittableThreadLocal声明、set、remove方法的调用。根据整个使用流程和方法调用栈,我们也可以很方便梳理出整个代码处理初始化、调用时序。(这里借用官方原图)通过流程图,我们可以清晰看到TTL核心流程和原理是通过TransmittableThreadLocal.Transmitter 抓取当前线程的所有TTL值并在其他线程进行回放,然后在回放线程执行完业务操作后,再恢复为回放线程原来的TTL值。TransmittableThreadLocal.Transmitter提供了所有TTL值的抓取、回放和恢复方法(即CRR操作): capture方法:抓取线程(线程A)的所有TTL值。 replay方法:在另一个线程(线程B)中,回放在capture方法中抓取的TTL值,并返回 回放前TTL值的备份 restore方法:恢复线程B执行replay方法之前的TTL值(即备份)弄明白核心流程和原理后,我们现在来分析下相关核心代码,在声明TransmittableThreadLocal变量时,我们会发现框架初始化了一个类级别的变量holder用于存储用户设置的所有ttl上下文,也是为了后续执行capture抓取时使用。 // Note about the holder: // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*). // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>. // 2.1 but the WeakHashMap is used as a *Set*: // the value of WeakHashMap is *always* null, and never used. // 2.2 WeakHashMap support *null* value. private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() { @Override protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() { return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(); } @Override protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) { return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue); } }; /** * see {@link InheritableThreadLocal#set} */ @Override public final void set(T value) { if (!disableIgnoreNullValueSemantics && null == value) { // may set null to remove value remove(); } else { super.set(value); addThisToHolder(); } } private void addThisToHolder() { if (!holder.get().containsKey(this)) { holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value. } }结合set方法实现来看,我们会发现holder变量设计的非常巧妙,业务设置的上下文value部分继续复用ThreadLocal原有数据结构ThreadLocalMap来存储( super.set(value));capture的数据源利用holder进行引用存储(addThisToHolder put this)。这样的好处是既可保持ThreadLocal数据存储原有的封装性,又很好实现扩展。除此之外,holder还有其他设计考究,这里抛出来大家可以思考下:为什么holder需要设计成static final类级别变量?ttl变量的存储为什么需要使用WeakHashMap,而不是hashmap或其他?然后我们再来看异步task转换 TtlRunnable.get(task2) 核心代码实现,代码整体实现相对比较简单,get方法是一个静态工厂方法,主要作用是将业务传入的普通Runnable task装饰成TtlRunable类,并在TtlRunable构造方法中进行线程capture动作(具体实现我们后面再分析),然后将结果存储到对象属性capturedRef中。 @Nullable public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) { if (null == runnable) return null; if (runnable instanceof TtlEnhanced) { // avoid redundant decoration, and ensure idempotency if (idempotent) return (TtlRunnable) runnable; else throw new IllegalStateException("Already TtlRunnable!"); } //将入参runnable进行了装饰 return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun); } //...... public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments { private final AtomicReference<Object> capturedRef; private final Runnable runnable; private final boolean releaseTtlValueReferenceAfterRun; private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference<Object>(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } /** * wrap method {@link Runnable#run()}. */ @Override public void run() { final Object captured = capturedRef.get(); if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } final Object backup = replay(captured); try { runnable.run(); } finally { restore(backup); } } //........ }然后是run方法,这也是核心关键的CRR操作了。这里通过模板方法将CRR操作编排在业务逻辑执行的前后了,也即业务逻辑执行前会将capturer的值进行replay恢复,执行后进行复原restore操作。同样这里也有几个问题很值我们思考:capture操作为什么需要放到TtlRunnable构造方法中,而不能在run方法中?代码中使用了哪两个设计模式,使用设计模式的好处是什么?业务执行完之后为什么还需要restore操作?接下来,我们再分别对capture、replay、restore方法实现做个一一分析。首先是capture方法,我们可以看到capture操作整体比较简单,主要是将set操作保存到holder变量中的值进行遍历并以Snapshot结构进行存储返回。 /** * Capture all {@link TransmittableThreadLocal} and registered {@link ThreadLocal} values in the current thread. * * @return the captured {@link TransmittableThreadLocal} values * @since 2.3.0 */ @NonNull public static Object capture() { return new Snapshot(captureTtlValues(), captureThreadLocalValues()); } private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() { HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) { ttl2Value.put(threadLocal, threadLocal.copyValue()); } return ttl2Value; } private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() { final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>(); for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) { final ThreadLocal<Object> threadLocal = entry.getKey(); final TtlCopier<Object> copier = entry.getValue(); threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get())); } return threadLocal2Value; }另一个captureThreadLocalValues,主要是用于将一些已有ThreadLocal中的上下文一起复制,已有ThreadLocal需要通过registerThreadLocal方法来单独注册。相关代码如下: public static class Transmitter { //.... private static volatile WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>> threadLocalHolder = new WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>>(); private static final Object threadLocalHolderUpdateLock = new Object(); //...... public static <T> boolean registerThreadLocal(@NonNull ThreadLocal<T> threadLocal, @NonNull TtlCopier<T> copier, boolean force) { if (threadLocal instanceof TransmittableThreadLocal) { logger.warning("register a TransmittableThreadLocal instance, this is unnecessary!"); return true; } synchronized (threadLocalHolderUpdateLock) { if (!force && threadLocalHolder.containsKey(threadLocal)) return false; WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>> newHolder = new WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>>(threadLocalHolder); newHolder.put((ThreadLocal<Object>) threadLocal, (TtlCopier<Object>) copier); threadLocalHolder = newHolder; return true; } } //...... }这里代码有个非常关键的处理,由于WeakHashMap非线程安全,为了避免并发问题安全加上了synchronized锁操作。这里有可以思考下除了synchronized关键字还有什么保障线程安全的方法。另外,实现threadLocal注册时为已经在锁块中了,为什么还要做new copy重新替换操作,这样做目的是什么?大家可以想想看。最后就是replay和restore方法,整体实现逻辑非常清晰,主要是将captured的值在当前线程ThreadLocal中进行重新赋值初始化,以及业务执行后恢复到原来。这里很佩服作者对不同情况的细致考虑,不是直接将当前holder中的上下文直接备份,而是与之前已capture的内容比较,将业务后set的上下文进行剔除,以免在恢复restore时出现前后不一致的情况。 @NonNull public static Object replay(@NonNull Object captured) { final Snapshot capturedSnapshot = (Snapshot) captured; return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value)); } @NonNull private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) { HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) { TransmittableThreadLocal<Object> threadLocal = iterator.next(); // backup backup.put(threadLocal, threadLocal.get()); // clear the TTL values that is not in captured // avoid the extra TTL values after replay when run task if (!captured.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // set TTL values to captured setTtlValuesTo(captured); // call beforeExecute callback doExecuteCallback(true); return backup; } private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) { for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) { TransmittableThreadLocal<Object> threadLocal = entry.getKey(); threadLocal.set(entry.getValue()); } } public static void restore(@NonNull Object backup) { final Snapshot backupSnapshot = (Snapshot) backup; restoreTtlValues(backupSnapshot.ttl2Value); restoreThreadLocalValues(backupSnapshot.threadLocal2Value); } private static void restoreTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) { // call afterExecute callback doExecuteCallback(false); for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) { TransmittableThreadLocal<Object> threadLocal = iterator.next(); // clear the TTL values that is not in backup // avoid the extra TTL values after restore if (!backup.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // restore TTL values setTtlValuesTo(backup); }核心代码分析完之后,再来简单总结下项目中学习到的知识点:对ThreadLocal、InheritableThreadLocal有了更加系统和深入的理解,包括两者继承关系、底层数据结构ThreadLocalMap与Thread关联关系等。面向gc编程(gc相关)、WeakHashMap(Java对象引用类型强、软、弱等)、线程安全、并发等等设计模式相关,装饰模式、工厂、模板方法、代理等TTL虽然代码量不算多,但短小精悍,也处处体现了作者超高的设计和编程能力,每行代码都值得学习和反复琢磨。我相信通过类似这样的一个项目学习流程下来,把每个环节都能踏踏实实做好,且过程中有贯穿自己思考和理解。相信你一定能把每个项目吃透,并把项目中的每个技术点都牢牢掌握。最后,我所在团队是淘系技术部淘系架构团队,主要在负责一站式serverless研发平台建设,为业务不断提升研发效率和极致体验。平台已平稳支撑淘系互动、淘宝人生、金币庄园、特价版、闲鱼、拍卖、品牌轻店等多个业务的6.18、双11、双12、春晚等多个大促活动。
新特性一览在开始之前,先让我们来一起浏览一下JDK 16版本所带来的17个新特性吧。▐ 本文将解读的新特性357: OpenJDK源代码仓库从Mercurial迁移至Git。努力推动这一改变,将会在版本控制系统元数据大小、可用工具以及托管等方面体现优势。 369: 迁移到GitHub,这个变化是基于OpenJDK源码库迁移至Git的,JDK 16源代码仓库将出现在最流行的程序员社交网站上。 386: 在x64和AArch64架构上,将JDK移植到Alpine Linux和其他使用musl作为其主要C库的Linux发行版。Musl是 ISO C和Posix标准中描述的标准库功能的Linux实现。Alpine Linux由于其镜像小而被广泛应用于云部署、微服务以及容器环境中。Linux版本的Docker容器镜像小于6MB。让Java在此类设置中开箱即用地运行,并允许Tomcat、Jetty、Spring和其它流行的框架在这些环境中工作。通过使用jlink来减少Java运行时的大小,用户可以创建一个更小的镜像,以运行特定的应用程序。 394: instanceof操作符的模式匹配,在JDK 14和JDK 15中都已预览过,将于JDK 16最终确定。模式匹配使程序中的通用逻辑(即从对象中有条件的提取组件)可以更简洁、更安全的表达。 395: 提供Record记录类,作为不可变数据的透明载体。▐ 其他的新特性347: 启用C++ 14语言功能,允许在JDK C++源代码中使用C++ 14功能,并提供有关在HotSpot代码中可以使用哪些功能的具体指导。 376: 将ZGC(可扩展低延迟垃圾收集器)线程堆栈处理从安全点移至并发阶段。ZGC垃圾收集器旨在使HotSpot中的GC暂停和可伸缩性问题成为过去。 380: 添加Unix-Domain Socket Channels,其中Unix-Domain(AF_UNIX)套接字的支持被添加到nio.channels包中的Socket Channel和Server Socket Channel API中。 387: 弹性Metaspace功能可将未使用的HotSpot虚拟机的Class Metadata(Metaspace)占用的内存更迅速的返回给操作系统,从而减少Metaspace的占用并简化Metaspace的代码以降低维护成本。 388: 将JDK移植到Windows/AArch64平台。 389: 孵化阶段的外部链接程序API,支持静态类型的纯Java方式访问本地代码。此计划的目的在于通过用更高级的纯Java开发模式来替换JNI(Java本机接口),以提供与C语言的交互。它的性能将会比JNI更加优越。 390: 基于值的类的警告建议:将原始包装类指定为基于值的类,弃用其构造函数以进行移除,并提示新的弃用警告。在Java平台中对于任何基于值的类的实例进行同步的错误尝试会予以警告。 392: 提供用于打包独立的Java应用程序的jpackage工具。396: 默认情况下,JDK内部结构是强封装的,而关键内部API(例如misc.Unsafe)除外。此计划的目标包括提高JDK的安全性和可维护性,并鼓励开发人员从直接使用内部元素逐渐迁移为使用标准API,这样开发人员和最终用户都可以轻松地升级到 Java 的未来版本。 397: 之前在JDK 15中进行过预览,JDK 16中二次预览的密封类和接口限制了可以扩展或实现它们的类和接口。此计划的目标包括允许类或接口的创建者控制负责实现它的代码,提供比访问修饰符更声明性的方式来限制超类的使用,并通过提供模式分析基础来支持模式匹配的未来发展。 338: 孵化阶段的矢量API(JDK将配备一个孵化器模块),jdk.incubator.vector,以表达在可支持的CPU架构上编译为最佳硬件指令的矢量计算,以实现优于等效标量计算的性能。 393: 孵化阶段的外部存储器访问API,允许Java程序安全的访问Java堆外的外部存储器(包括本地、持久化介质以及托管堆存储器)。 如上新特性前编号为JDK Enhancement Process的标识符,详见文末参考资料立即尝鲜浏览完17个新特性后,我都迫不及待的想尝试一下JDK 16,以及其中一些对工程上有所帮助的特性了。那么先通过JDK官网进行JDK 16候选版下载(http://jdk.java.net/16/)。 由于要方便的在系统中针对多个JDK版本进行切换,可以使用jenv(https://github.com/jenv/jenv)。我们把下载好的JDK16路径添加到jenv,在做如下设置即可使用。jenv add ${JDK16_Path} jenv global openjdk64-16如果一切顺利,那么查看JDK版本时,会有类似如下信息的返回。java -version openjdk version "16"2021-03-16 OpenJDK Runtime Environment (build 16+36-2231) OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)如果你在使用较早的IDEA版本作为开发工具,那么使用JDK 16运行程序时,可能收到如下的错误:Cannot determine path to 'tools.jar' library for 16 (path/to/jdk-16) when running from IDEA, you should update to the latest version.这是由于JDK9对Java运行时做了重构,已删除了rt.jar、tools.jar、dt.jar以及其它各种内部JAR包。而在较早的开发工具通常对这类JAR包有依赖,通过升级IDEA可以解决。到官网获取一个IDEA 2021.1 EAP预发版本(https://www.jetbrains.com/zh-cn/idea/nextversion/)来提前体验(也可以等待2021.3的正式版本)。新特性解读▐ 迁移到GitHub早在2020年9月,OpenJDK已将Github上的jdk仓库作为JDK 16源码的主读取/写入仓库。随着JDK 16的正式发布,这将是OpenJDK在Github上开发完成的初代JDK版本。 而促使将OpenJDK源代码仓库从Mercurial迁移到Git的三个主要原因:版本控制系统元数据,可用工具和可用托管的大小。版本控制元数据大小方面,转换后的存储库的初始原型已显示出版本控制元数据的大小显着减少。例如,使用Git的jdk仓库的.git目录大约为300MB,而使用Mercurial的.hg目录大约为1.2GB。减少元数据可保留本地磁盘空间并减少克隆时间,同时减少传输的数据。可用工具方面,与Mercurial相比,Git可用的工具更多。所有的文本编辑器都可以本地或通过插件实现Git集成。此外,几乎所有的IDE都带有Git集成,包括Eclipse、Visual Studio、IDEA。可用托管方面,有许多选项可用于托管Git仓库,无论是自托管还是作为服务托管。使用外部源码托管提供程序的原因包括性能、与开发人员进行交互的Web API的访问权限控制 以及 蓬勃发展的社区。OpenJDK迁移到Github之后,对于Java开发者而言还是有不少的便利:通过fork一份JDK 16源码仓库(https://github.com/openjdk/jdk),可以一边阅读源代码,一边做笔记并提交,方便持续学习JDK源码。使用Git的upsteam保持JDK源码的更新,同时也保持自我更新。如网速够快,通过Github在线阅读代码的工具Github1s(https://github.com/conwnet/github1s),快速在浏览器中翻阅JDK 16源码(https://github1s.com/openjdk/jdk/releases/tag/jdk-16%2B35)也是非常方便。如果是在IDEA下工作与学习,clone好JDK 16源码,打开Project Structure (command+;),设置Project SDK为JDK 16,并设置Project language level到16。之后就可以愉快的看JDK 16源码了。▐ 将JDK移植到Alpine Linux在云原生时代,个人理解提升效率是第一原则:更小的镜像体积分发时会更加迅速应用程序/容器的启动要迅速这样就能保障系统水平伸缩够快、问题出现时回滚处理够快。另外,出于降低成本考虑,更小的镜像体积内存占用会更小,分发时耗用的资源也更小。 Alpine Linux就是与云原生的提升效率原则契合的一款独立的非商业性的通用Linux发行版。其关注于安全性、简单性和资源效率,围绕musl libc和busybox构建。这使得它比传统的GNU/Linux发行版更小。JDK移植到Alpine Linux后,将允许Tomcat、Jetty、Spring和其它流行的框架在其中工作。用户可以创建一个更小的镜像,以启动、运行特定的应用程序。 提前准备好Docker,我们先构建一个Alpine Linux镜像,然后添加JDK 16,最后运行一个简单的Spring Boot程序来演示一下。▐ 构建Alpine Linux镜像# 获取Alpine Linux镜像 docker pull alpine # 运行镜像 docker run alpine echo'Hello Alpine!'通过docker images命令查看镜像大小会发现,alpine在截止本文完成时,镜像大小仅仅只有5.6MB。相对于debian、ubuntu、centos等系统动则几十甚至上百MB的镜像来说,alpine可是真的小!REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest 7731472c3f2a 7 weeks ago 5.61MB▐ 添加JDK 16OpenJDK通过使用jlink(JEP 282:https://openjdk.java.net/jeps/282)来减少Java运行时的大小,我们可以从DockerHub上获取镜像:16-jdk-alpine(https://hub.docker.com/_/openjdk?tab=tags&page=1&name=16-jdk-alpine&ordering=last_updated)。或者如下Docker命令:docker pull openjdk:16-jdk-alpine▐ 运行Spring Boot先准备一个Spring Boot的FatJar程序,可以从Spring Boot官网获取Hello World!样例程序(https://spring.io/guides/gs/rest-service/)。创建一份Dockerfile,使用openjdk:16-jdk-alpine,并添加Spring Boot程序。FROM openjdk:16-jdk-alpine VOLUME /tmp ARG JAR_FILE ADD ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]▐ 构建并运行# 构建镜像,设置JAR_FILE参数指向Spring Boot程序Jar包路径 docker build --build-argJAR_FILE=target/rest-service-0.0.1-SNAPSHOT.jar -t alpine-jdk16-app:latest . # 查看镜像 docker images # 根据镜像,启动容器运行# -d参数 后台运行# -p参数 Spring Boot默认端口8080,映射到容器端口8080 docker run -d-p8080:8080 alpine-jdk16-app:latest # 查看容器运行 docker ps # 验证成功之后可以停止容器 docker stop${CONTAINER_ID} # 访问应用 curl-w'\n' http://127.0.0.1:8080/greeting?name=jdk16至此,通过Alpine Linux系统带JDK 16运行时的Spring Boot已经启动并可以正常的访问了。Alpine系统JDK 16镜像大小约为321MB。相比Oracle官方的Linux版本镜像的467MB,减少30%+。记录类从JDK 14开始提供了Record记录类的预览特性,这一特性将成为JDK 16的一项永久性特性。Record记录类作为不可变数据的透明载体,其是为了回应有关Java过于冗长拘谨的抱怨。此计划的目标包括设计一个表示简单值集合的面向对象的构造函数,帮助开发人员专注于对不可变数据的建模而不是扩展行为,自动实现数据驱动的方法(例如 equals() 和 属性的访问器)。 通过较新版IDEA可以创建此类型:声明Record记录类后,几乎不需要添加额外的代码,一组隐式声明让其代码书写很简洁:隐式声明了属性隐式声明了构造器隐式声明了equals()、hashCode()、toString()隐式声明了属性的访问器,访问器名称与属性同名public record Point(int x, int y) {}Record记录类支持Local Classes特性,那么当需要临时使用Record的时候,就可以非常方便的定义与使用:List<Merchant>findTopMerchants(List<Merchant> merchants, int month) { // Local record record MerchantSales(Merchant merchant, double sales) {} // 使用MerchantSales Record类临时包装merchant和sales,方便做处理。 return merchants.stream() .map(merchant ->new MerchantSales(merchant, computeSales(merchant, month))) .sorted((m1, m2) ->Double.compare(m2.sales(), m1.sales())) .map(MerchantSales::merchant) .collect(toList()); }Record记录类将可以代替Tuple、Pair等之前在JDK之外的工具库提供的元组功能,在与下面将介绍的模式匹配特性配合,可使代码将变得非常简洁。▐ 模式匹配从JDK 14开始引入了一种模式匹配的预览特性,这一特性也将成为JDK 16的一项永久性特性。因此虽然JDK 16是个短期版本,也不妨碍我们在未来的JDK版本中继续使用模式匹配特性。 模式匹配的现阶段仅限于一种模式(类型模式)和一种语言构造(instanceof),但这只是完整特性的一部分。即便如此,我们也已经获得了一个显著的好处:冗余的强制转换消失了,消除了冗余的代码,使更重要的代码得到了更清晰的关注,同时消除了隐藏bug的地方。 举个例子:我们在开发中当需要解析对象会用到类似如下的方式if (obj instanceofString) { String s = (String) obj; ... }使用模式匹配后的等价代码:if (obj instanceofString s) { // 通过使用模式匹配可以直接使用s局部变量 ... }代码看起来是不是整洁了许多。使用instanceof获取对象类型是一种条件提取形式,在获得到对象类型之后,总是要将对象强制转换为该类型。以前在instanceof之后必须进行显式类型转换,这是一种繁琐的操作,而融合这些操作的好处不仅仅是为了简洁,它还消除了一个常见的错误来源:在剪切和粘贴instanceof及强制转换代码,容易在修改了 instanceof的类型之后忘记修改强制转换类型,这就给了漏洞一个藏身之处。通过instanceof的模式匹配消除了这个问题,我们还可以消灭所有这种类型的bug。 另一个需要经常的做此类“先检测后强制转换”的地方是equals方法。再来看一个例子:publicbooleanequals(Object o) { if (!(o instanceof Point)) returnfalse; Point other = (Point) o; return x == other.x && y == other.y; }使用模式匹配后的等价代码:publicbooleanequals(Object o) { return (o instanceof Point other) && x == other.x && y == other.y; }这段代码起到同样的效果,但更简单直接,因为我们可以只使用一个复合布尔表达式来表达一个等价的条件,而不是使用控制流语句。 模式匹配的绑定变量(如上代码例子中 obj instanceof String s的s就是一个绑定变量)除了特殊的声明位置以外,其作用域也与"普通"局部变量有所不同。比如我们可以这样写:if (a instanceof Point p) { // p is in scope ... } else { // p not in scope here } // p not in scope here if (b instanceof Point p) { // Sure! ... }这样特殊的作用域让我们能够在if-else的多分支情况下,自由的重新声明绑定变量,也考虑未来在switch中的case也是如此便利。如:if (x instanceofInteger num) { ... } elseif (x instanceofLong num) { ... } elseif (x instanceofDouble num) { ... }如果模式匹配可以消除Java代码中99%的强制类型转换操作,那么它肯定会很流行。但还不仅限于此,随着时间的推移,将会出现其他类型的模式,它们可以进行更复杂的条件提取,使用更复杂的方式来组合模式,以及提供其他可以使用模式的构造:比如switch,甚至是catch,再加上目前已永久支持的Record类以及在预览中的密封类等相关特性,模式匹配未来一定能够大大简化我们编写的代码。尾声本文从JDK 16版本所带来的17个新特性中抽取对工程工作和学习比较有帮助的几个特性展开解读,快速了解了这些特性。 大部分的企业或者项目还在使用JDK 8(其依然占据JDK市场的80%,绝对的主流),源于JDK 8的超豪华新特性,如函数式接口、Lambda表达式、方法引用 / 构造器引用、更强的Steam API、接口的增强、Optional、JVM中Metaspace取代PermGen空间等等。 我们也能够看到Java为了跟上当下技术更迭的快节奏,不断的推陈出新。从JDK 9开始,Java版本的发布改为每6个月一次,JDK 11是长期支持版本以及下半年将发布的JDK 17。JDK 9~JDK15也不乏一些重要的新特性,如JDK 9 模块系统、JShell交互式命令行JDK 10 局部变量类型推断JDK 11 ZGC试用、HTTP Client API、Steam等增强JDK 12 switch表达式扩展、增加基于JMH的一套微基准套件JDK 13 Socket API 重构、文本块(多行文本)JDK 14 更有价值的NPE错误信息、JDK 16特性的部分预览JDK 15 密封类、Record类等JDK 16特性的预览希望这种快速版本迭代的策略能够让Java保持持续的活力,能够让开发者使用的更高效、更健壮!参考资料JDK 16 的状态、发布计划与新特性(http://openjdk.java.net/projects/jdk/16/)JDK 16: The new features in Java 16(https://www.infoworld.com/article/3569150/jdk-16-the-new-features-in-java-16.html)Java源代码仓库迁移到Github(https://www.infoworld.com/article/3569068/javas-move-to-github-set-for-september.html)在Alpine + OpenJDK镜像中运行Spring Boot(https://blogs.oracle.com/developers/running-spring-boot-in-a-docker-container-on-openjdk,-oracle-jdk,-zulu-on-alpine-linux,-oracle-linux,-ubuntu)JEP 394: Pattern Matching for instanceof(https://openjdk.java.net/jeps/394)JEP 395: Records(https://openjdk.java.net/jeps/395)JEP 397: Sealed Classes (Second Preview)(https://openjdk.java.net/jeps/397) 加入我们 欢迎加入淘系架构团队,团队成员大牛云集,有阿里移动中间件的创始人员、Dubbo核心成员、更有一群热爱技术,期望用技术推动业务的小伙伴。 淘系架构团队,推进淘系(淘宝、天猫等)架构升级,致力于为淘系、整个集团提供基础核心能力、产品与解决方案:业务高可用的解决方案与核心能力(精细化流量管控Marconi平台:为业务提供自适应流控、隔离与熔断的柔性高可用解决方案,站点高可用:故障自愈、多机房与异地容灾与快速切流恢复新一代的业务研发模式FaaS(一站式函数研发Gaia平台)下一代网络协议QUIC实现与落地移动中间件(API网关MTop、接入层AServer、消息/推送、配置中心等等)期待一起参与加入淘系基础平台的建设~ 简历投递至📮:泽彬 zebin.xuzb@alibaba-inc.com(淘系架构-应用架构Leader)作者:熊政(八风)来源:淘系技术公众号
tidevice 是阿里的内部的一个小组用来做 iOS 自动化用的工具,通过逆向iOS通信协议,使用纯Python实现。目前淘宝和其他部分事业部已经全面使用了该技术,进行iOS应用的性能采集,UI自动化。注:这里的被测应用无需做任何修改,使用不再局限于Mac上。开源地址:https://github.com/alibaba/taobao-iphone-devicetidevice可以帮你做什么呢?应用安装,启动,停止,查看启动 WDA(WebDriverAgent)(注:该操作不依赖xcodebuild,可跨平台使用)运行UITests (跨平台使用)性能采集(类似 PerfDog)截图、syslog采集 等等熟悉libimobiledevice工具集的同学可能知道大部分上面提到的功能,为了方便日常的使用tidevice对libimobiledevice中已有的功能也重新实现了一遍(比如 截图,看日志,应用安装),这样有tidevice你就可以完成日常所有相关的操作了。除了这些众所周知的功能,tidevice还可以完成WDA的启动,iOS设备的性能采集。可能有人不知道WDA为何物,这里简单的介绍一下。WDA全名WebDriverAgent是facebook推出的可以实现黑盒iOS自动化的项目。该项目作为一个App运行在iOS手机上,被测应用不需要做任何的更改(比如接入sdk),进行无侵入的测试。唯一不方便的是手机必须连接上Mac电脑,并使用Mac上才有的xcodebuild才能将WDA这个App运行起来,这也导致其推广起来比较困难。文章中会重点介绍一下tidevice 如何完成WDA的启动。原理比较简单:tidevice通过模拟xcodebuild与手机进行通信,向手机发送特定的指令,来启动WDA,从而可以脱离Mac的限制,能够在Linux、Windows上运行起来iOS自动化,而在tidevice出现之前,这些是无法做到的。安装因为是Python项目,一条命令即可完成安装 pip3 install -U tidevice安装完成后,先执行几个命令测试一下tidevice version 查看tidevice版本tidevice list 查看已经连接上的iPhone设备常用命令安装应用tidevice install example.ipa 通过URL安装应用 (实际使用时网址要改成正确的)tidevice install http://example.org/demo.ipa 应用启动tidevice launch com.apple.Preferences 截图tidevice screenshot screenshot.jpg 查看系统日志tidevice syslog 其他常用的命令帮助命令查看,基本上常用的命令都有了tidevice -h启动WebDriverAgent目前iOS的黑盒自动化,最流行的方法是通过WDA来实现的。在tidevice出现之前,WDA只能通过xcodebuild来启动,而运行xcodebuild则必须有一台Mac才行。tidevice没有通过xcodebuild,而是通过usbmuxd直接跟手机上的服务进行直接通信完成手机上WDA的启动。usbmux在不同的平台都有开源的实现,所以tidevice不仅能在Mac上运行,也能在Linux、Windows上运行。关于usbmux通信协议这部分,苹果当然不会告诉直接告诉我们。不过因为usbmux本身就是socket套接字,所以我们可以直接截获其中的内容,然后根据开源界已有的成果,其中大部分的内容已经被破解了。先用xcodebuild完成一次WDA的启动,然后找到关键的通信内容,再用python来模拟回放一遍,就可以抛弃xcodebuild不用了。 相对于xcodebuild启动,tidevice因为通信内容更精简,所以启动速度更快(2s左右),另外也更稳定。 说了一堆理论,我们看一下怎么使用的吧前提条件数据线将 iPhone 手机连接到 PC 上手机上已经有WebDriverAgent这个 App 了。这个可以通过 xcode 编译源码安装,也可以用开发者证书重签名的 WebDriverAgent.ipa 安装到手机。Linux和Windows因为默认没有usbmux这个服务,提前安装一下就可以。可以参考这个issue:https://github.com/alibaba/taobao-iphone-device/issues/7 前提条件OK了的话,像下图这样执行命令就可以将WDA启动起来了。 而验证WDA是否工作最简单的办法就是打开浏览器,网站:http://localhost:8100/status能看到下面的输出说明WDA工作正常了或者也可以直接使用Appium调度运行 UITests有些用户的开发能力可能比较强,习惯直接用OC或者Swift直接写UI自动化用例。通过tidevice也支持的。 网上找了一个OC写的XCTest UITests demo项目https://github.com/FeiHuang93/XCTest-Demo使用xcode编译安装到手机上之后,有两个应用testXCTestUITests 执行测试的应用testXCTest 被测应用使用下面的命令执行即可tidevice xctest --bundle-id philhuang.testXCTestUITests.xctrunner --target-bundle-id philhuang.testXCTest将被测应用和执行测试应用打包成ipa后,就可以在多个的手机上运行了。总结现在这个项目在阿里内部目前用的还不错,希望欢迎多多试用反馈。如果好用的话,希望可以留下你的Starhttps://github.com/alibaba/taobao-iphone-device
▐ Flutter2.0整场活动中,最令人激动的就是Flutter2.0的发布了。那么Flutter2.0主要带来了哪些新的特性呢?一句话总结,Flutter2.0最大的变化是除了之前已经处于stable渠道的移动设备支持外,桌面和Web支持也正式宣布进入stable渠道。▐ Dart2.12独一无二的应用构建能力集合可移植性Dart的高效编译器可以生成针对x86&ARM的机器码,以及针对Web优化过的JS。其广泛支持了各种目标: 移动设备、桌面PC、后端应用以及更多。高开发效率Dart提供的HotReload特性,支持快速的,可交互的开发体验,不论是原生设备还是Web应用均如此。Dart也提供丰富的对象用于应用开发,包括Isolate模型,async/await并发处理,以及事件驱动的开发模式。健壮Dart的健全空安全类型系统可以在编译期捕获错误,这一切高度可伸缩可信赖,并被用于支持大量的应用,如高度重要的Google Ads,Google Assistant,运行长达长达十年以上健全的空安全健全的空安全是自从Dart2.0引入健全类型系统后,Dart语言的一大改进。空安全进一步增强了类型系统,使得开发者可以去捕获空错误,这也是应用崩溃的一大常见原因。通过引入空安全机制,开发者可以在开发期捕获空错误,避免线上崩溃。以下是健全空安全的几大原则默认非空: 对于类型系统的根本性变化默认使用non-nullable 增量迁移到空安全针对以下代码,空安全将带来显著的机器码减小: int age = 0; } int getAge(Animal a) { return a.age; }持续改善已有功能GooglePay包大小通过针对Flutter的优化大小降低了14%针对不同的输入,UTF8Decoder最快可以加速20x用于集成Dart和C的FFIDart FFI使得你可以利用已有的C库代码,这样不仅提高了可移植性,也可以在性能敏感的场景下充分利用高度调优的C代码。Dart2.12中,FFI已经脱离beta阶段,被认为是stable,可线上使用。并新增了以下特性。按值传递结构体结构体嵌套自动生成FFI绑定Dart语言的下一步类型别名三相移位操作符通用元信息注解静态元编程▐ WebFlutterWeb正式进入stable渠道。随着这一初始stable的发布,已有Flutter代码Web平台支持将进入一个新的阶段,换句话说,当你使用Flutter2.0创建App时,Web只是一个新增的设备目标。借助Web平台的诸多能力,Flutter构建了可用于富交互Web应用的基础。Flutter For Web(FFW)主要聚焦于高性能及高保真的渲染性能。除过HTML渲染后端外,FFW也新增了一个CanvasKit的渲染后端。以及诸多针对Web的特性,比如Link Widget,使得你的应用在浏览器中的运行可以真的感觉是一个Web应用。在这一阶段,FFW主要聚焦以下应用场景:Progressive Web Apps(PWA) Single Page apps(SPA) 将已有Flutter移动应用扩展到Web侧架构设计整个框架使用Dart编写,总计约70w行的Flutter框架核心代码针对所有平台是一致的,不论是mobile,desktop还是现在的web.你既可以使用dartdevc或者dart2js将代码编译成javascript,进而运行在服务器上。鉴于Dart本身可以将整个Flutter框架编译成JS,将Flutter运行在Web上的核心问题就是将移动应用的底层C++渲染引擎替换成对应的Web平台API。Flutter并不是简单地将Widget编译成对应的HTML元素。相反,其Web引擎提供了两种渲染后端选择,HTML后端用于降低包大小,并提供广泛的兼容度。CanvasKit后端使用了WebAssembly和WebGL来在浏览器Canvas上渲染Skia绘图指令, 具有更高的性能和组件密度,但增加了大约2M的下载包大小。你可以通过以下命令来指定渲染后端。--web-renderer html--web-render canvaskit稳定的Web支持ShowcaseRiveRive, 是一个用于创建自定义动画的工具,使用Flutter For Web来重构其代码,并已经beta可用。https://rive.appFlutter PlasmaFlutter Plasma展示了一个运行在Safari, Firefox, Edge和Chrome上的Flutter Demo。https://flutterplasma.deviRobotiRobot教育使用Flutter开发了iRobot Coding App,通过将其在Web可用,提供了随处可用的针对任何人的代码学习体验。https://code.irobot.com/#/MobiMoi Mobiili, 一个现代移动虚拟网络运营商,近期使用Flutter发布了他们的Web应用。https://www.moi.fiWeb的相关特性自定义URL策略新的Link Widget基于Canvas的文本度量和渲染文本交互(选择,拷贝,粘贴等)支持桌面表单因子展望CanvasKit的进一步支持,比如CORS图片PWA的全离线支持文本渲染以及功能插件生态系统的完善▐ 桌面支持Flutter Desktop也正式进入stable渠道,即初始发布状态。Canonical正在同Flutter合作以将Flutter引入桌面,工程师们正在开发代码并且将其部署到Linux上。对其而言,各种各样的硬件配置下提供稳定可靠并且优美的体验是至关重要的。再往后看,Flutter将是后续Canonical桌面以及移动应用开发的默认选择。文本编辑体验鼠标输入体验ScrollbarIME支持桌面额外功能支持更新的文档支持以将应用发布到特定应用商店▐ 折叠设备支持微软在持续扩大其对于Flutter的支持。除了在Flutter Engine中持续贡献高质量的Windows支持外,微软正在增加对于新的可折叠Android设备的引擎支持。这些设备引入了新的设计模型,App既可扩展其内容,也可充分利用多屏特性提供side-by-side的体验。https://flutter.gskinner.com此外,gskinner开发的Folio App,很好地诠释了Flutter在多平台上的运行。通过一套代码,不论是在小,中等还是大的屏幕上,Flutter均可处理好触摸,键盘和鼠标输入,并同平台的特性适应良好(比如Web上的链接以及桌面上的菜单)。▐ 嵌入式设备支持丰田公司,宣布了他们计划提供市场上最好的机动车上的数字体验,通过使用Flutter来构建娱乐信息系统。使用Flutter标志着同以前车载软件完全不同的开发体验。Toyota之所以使用Flutter,是因为以下的原因:高性能和AOT一致性智能手机层的触摸机制人类工程学从客户反馈中快速迭代▐ 工具链FlutterFix如今有超过50w的Flutter开发者,我们所面临的的设备平台也越来越多。当框架变得成熟,越来越大的时候,我们越来越需要去避免对于框架的修改,不要去破坏愈发庞大的代码库。然而,为了持续改善Flutter,我们也需要能够去对API做Breaking修改,问题来了,如何去持续改善FlutterAPI而不阻断开发者体验呢?我们提供了FlutterFix。Flutter Fix包含了以下特性:dart fix——新提供的命令行选项dart fix可用于查找哪些API已经被废弃,如何去更新这些API。提供可供fix的选项IDE插件集成从而可以通过选择完成修改DevToolsIDE插件可帮助开发者清零问题,即便是DevTools还未启动。通过点击按钮,即可快速找到引发问题的Widget。目前仅支持Layout Overflow异常,但DevTools团队计划去涵盖所有的常见类型异常。轻易发现高分辨率的图片,跟踪降低过度的包大小与内存使用Inspector新增对于固定Layout的展示能力内存视图更快,小,易于使用日志Tab增加搜索与过滤功能在DevTools启动前即可跟踪日志▐ 社区与生态数据Flutter1.0发布至今已经两年有余,Flutter共计关闭了24541个Issue,合并了来自765个贡献者的17039个PR。目前共计有50w+的Flutter开发者,超过15w的Flutter应用。目前有15k的针对Flutter和Dart的Package,这其中包括了亚马逊,微软,Adobe,阿里巴巴,eBay,Square等公司,也要报关键包诸如Lottie, Sentry, SVG,以及Flutter Favorite推荐的sign_in_with_apple, google_fonts, geolocator和sqlite.▐ 其他Add2App中的多引擎实例过去,额外的Flutter引擎创建会造成同第一个实例同样的内存开销。在Flutter2.0上,我们将这一内存开销减少到了每个实例180KB,降低了99%之多。由此,我们推荐在你的原生App中去使用多个Flutter引擎实例。DartPad升级到支持Flutter2.0▐ AskFlutter圆桌参与这场圆桌的成员有:Andrew Brogdon(主持人)、Eric Seidel(Flutter负责人)、Ian Hickson(Flutter技术负责人)、 Mariam Hasnany(FlutterForWeb PM)、Frank van Puffelen(开发者项目工程师)。以下针对一些重要的问题做了摘录:主持人什么时候FlutterWeb可以供生产环境使用?M好消息是,今天Flutter Web正式在stable渠道可用。你无需enable任何flags,即可将Web作为Flutter应用的目标设备。也就意味着,对于任何已经有Flutter Web App的开发者,你现在可以用stable渠道来构建你的应用,如果你是要新开发Flutter Web Apps,快来检出把。主持人什么是Flutter Web的理想用户场景呢?M这真的是一个好问题,随着这次标志性的发布,我们已经聚焦在构建富交互Web应用的基础。如果你已经有一个Flutter移动应用,你现在可以使用同样的代码来构建其Web版本,然后把你的用户群基础扩展到整个Web应用。不仅如此,这对于构建PWA或者SPAde应用来说也是一个额外的优势。这些应用通常使用大量的动态内容,交互UI,我们通常认为这三种是最适合的。当然为了支持文档为中心的页面我们还需要很多工作去做。比如,传统的HTML页面有很多的文本,静态的内容。所以我想现在我们的确已经很适合来开发Web应用。主持人目前在Github上有超过8200个Open Issues、Flutter似乎有些人手不足,有没有计划加以改善这种问题或者你们的优先级是什么?I是的,我们的确有这么多,正如Netlinx(提问者)问道的,我们有8000多处于Open状态的Issues, 但重要的是我们正在尽力去解决他们,比如去年我们在Github上收到了15000多Issues,我们也关闭了15000issues。我们对修复和解决bug的比例还是很高兴的。这一数字表征了我们有多少用户。越多人使用,就有越多的bug提出。我们解决issue的数字是一个贡献者多少的指标,我们很多贡献者。在Github的Flutter Hacker组里,我们有超过200人。一半以上是谷歌员工,大部分贡献者并不是。事实上,部分Flutter Team的人是来自开源项目的,他们可能是微软或者Canonical,或者是使用自己时间的志愿者。不同的人投入的时间不同,他们都给这个项目做出了共享,希望我们可以解决更多的问题,让Netlinx高兴。主持人Flutter Web什么时候会废弃URL中的#?M这是一个很好的问题。让我们从为什么它的存在开始,今天我们有时候要使用hash URL策略,这是当我们初始这个Web引擎的时候决定的。也就是说当你有Flutter具名路由的时候,我们基本上是初始化这些路由作为hash的一部分,添加到URL上。随着今天Stable的发布,我们有了新的办法来自定义URL,从URL中丢弃hash。这样你就可以按照自己的方式来构建URL,配置其余的子URL,实现deep link或者说同朋友来一起分享。社区中也有一个叫做URL Strategy的插件,它实现了我们在文档中的指南,以一种很简单的方式。主持人Flutter依然有很多Mac M1上的兼容性问题,你们是否在加以解决还是说我需要买一个旧的Intel Mac?E我想说你不需要买一个新电脑,我建议你今天再试试。Flutter2.0上有很多针对M1的优化。其实我们也是同社区同一时间知道M1新发布的这类信息的。当天我们就订购了一个M1的开发机并用它开始工作。我们将其分为了三个桶,用来运行App, 工具链以及开发工具。据我所知,前两个桶其运行都是良好的。当然随着Flutter2.0的正式发布,如果你遇到了M1或者其他的问题,我们想听到你的反馈。正如Ian所说,我们每天有很多Issues,我们想去尽快去解决他们。因此,快去试试Flutter2.0吧,我想它应该会工作良好的,而且也会持续工作很好,因为我们会做更多代码修改。IM1有意思的是它几乎是一个全新的平台,因为我们以前从来没有用ARM作为host。今天我们发布Web和Desktop,但是,实际上,Apple Silicon是苹果自己的平台,我们要去支持。虽然我们现在已经支持当前release的macOS,但是依然有大量的工作要去做。主持人Flutter Dart团队是否计划去提供针对App开发的官方指南?类似Android的Jetpack?I我要笑了,因为你把这个问题丢给我似乎你不知道这个答案一样。我们已经讨论了好几个礼拜了,是的,我们有,实际上,我想或者是今天或者是很快,我们会发一个新的模板到Flutter master分支,这个模板基本上就是这个问题的答案。如何去使用最佳实践来创建和应用,状态复原等等?不仅是这个问题的答案。编程的核心在于针对这些问题有很多的差别,不同的App有不同的需要,我们希望这样特定的模板可以真的帮助到大家,我们也希望后续可以有针对不同架构类型的模板。可能你更喜欢redux而不是我们在模板中使用的。这个今天不会随着Flutter2.0发布,但是我想会在未来几个月的stable版本中发出。主持人空安全是否会破坏已有App? 是否有一些内容需要被迁移?E这个问题是你能够去迁移。甚至有个工具可以使用。我想应该叫做dart fix,你可以在你的代码库上运行它,将会帮助你去将代码改成Null aware.I如果你关注了更早的Keynote,我们有一章是关于他如何工作的。这并不会产生破坏性,你首先要确保你的依赖都已经顺利迁移。如果你的依赖没有迁移,对你来说迁移自己的代码将很困难。这是可能的,但会变的低效。所以,如你所知,如果你有一个包还没有迁移,去让这个包的开发者完成迁移。即便不迁移,也不会有特别的破坏性。正如Keynote提到的,这是为什么我们不把这次发布叫做Dart3.它是向后兼容的。同其他语言一样,Dart空安全里,你可以决定使用哪个版本,Dart2.0或者更高。我们在Flutter Sample仓库里经历过这些,我们观察例子,看看有多少依赖。对他们进行排序,随着Flutter2.0,我们事实上已经处理完了Sample仓库。另一个我们部署空安全方式的好处是,你可以同时编译空安全和非空安全,编译器自己会使用空安全优化。他可以在编译空安全代码的时候知道类型。当到了非空安全代码边界的时候,他会添加判空逻辑。我们称之为非健全空安全。如果我没记错的话,因而你的代码可以在混合模式下执行,也是可以的。主持人Flutter是否适合3D渲染?E我来回答吧。我们是把Flutter作为2D系统构建的。其实也有很多人用它来做3D工作。我们提供的API可以用来在一个屏幕上绘制2D对象。要支持3D,人们可以自行创建2.5D或者3D对象,然后通过纹理这样的方式嵌入Flutter.有很多人就这么做。事实上Keynote中,我想就提到了Wallace & Gromit app。它里面就有2D和3D内容。将二者混在一起是可行的,但是再说一遍,Flutter是针对2D体验设计的。主持人Flutter对于桌面的支持怎么样?I是的,Flutter Desktop如今已经在stable渠道可用,尽管我们不认为它是完全stable了,我们支持macOS, Windows和Linux现在。还有什么呢?我们还没有提供你可能需要的所有必须特性,例如,我们目前还没有支持多窗口,尽管这已经在开发了。我们还有很多努力。支持基本的单窗口App是非常稳定的。我自己就写了一个数独应用,运行在Mac上,工作很棒。E是的,我想多说一些,我喜欢Flutter Desktop,并且他已经在stable渠道可用,他的开发体验很棒。你只需要打开它,Flutter自己就正常运行了,这种工作体验很棒。试试吧,给我们写反馈,我想说,对于我,Flutter Web和Desktop公共的部分很棒。主持人什么时候首次打开App动画卡顿的问题可以得到解决?E这是一个普遍的问题,特别是最近几周,我在Reddit上写了很长的帖子,我也正在写一个更长的博客。我想说的是性能一直是Flutter最基础的一个衡量,当我们五六年前讨论这个项目的时候,我们就在说先谈论性能。在我们的任务列表中,性能是排名第一的。这不仅仅是文字,我们通过各种方式去保证这一点。所有的提交都要首先通过各种各样的性能测试,包括所有平台。我们一直在追求性能优化,每天都是如此。尤其当面对首次启动的动画卡顿的问题,我们意识到这个问题已经有一段时间了,尤其是iOS上。过去的一年这个问题在某些场景下愈加恶化。当从OpenGL迁移到Metal的时候,我们不能够在去缓存Shaders,你必须使用GPU去产生这些像素。不论如何,我们已经充分意识到有这样的问题,正在努力去解决,很多人力投入其中。Ian就在攻坚这些问题。I是的,我一直在关注这些卡顿的issues,这是我现在非常关注的问题。你可以看看Github Probject188, 我内心里一直记着这个数字因为我经常打开它。那里有所有相关的问题。你能做到最好的事情就是,如同我们之前讨论的,如果你遇到应用卡顿,请提一个bug,带上复现代码,包括显示卡顿的视频,以及时间线的trace以说明你的应用在视频中具体在干什么。这是目前对于我们来说最有帮助的了,我们可以去研究特定的Case,他们并不都是因为同样的原因造成的。即便是Shaders的原因,也不一定是因为同样的Shaders。所有这些不同的bug将会被以不同的问题加以解决。主持人谷歌打算如何在内部使用Flutter Web?E我不能,你知道的,谈论其他团队的计划,我能说的是有很多团队正常尝试使用Flutter Web. Flutter Web今天刚刚来到stable渠道,我们也正在给内部团队类似的指引,知道的,我们依然在解决各种问题。所以今天并没有什么能宣布的,但是我可以期待有更多的对于Flutter Web的使用。我致力于这个工程技术,认为这是一种更好的方式来写一次代码,可以运行在各个平台上。我们已经看到很多谷歌团队接受了Flutter的这一策略,我想Flutter会继续来到更多的应用场景的。I我们已经看到内部的很多工具使用Flutter For Web.当这些工具背后没有一个大的团队的时候,他们需要一些有用且可以高效开发的工具。比如,在Flutter团队里,我们使用Flutter Web来开发一些内部工具,用于把公共的Flutter代码迁移到如GooglePay这样的内部仓库中,以及供其他团队使用。这些工具都是使用Flutter写的。主持人Dart何时支持WebAssembly?I这意味着很多问题,我们实际上已经在Flutter For Web中使用WebAssembly了。Mariam可能可以谈更多,简单来说,我们有其他两部分WebAssembly和Dart相关。一个是是否直接把Dart编译成WebAssembly,另一个是,是否可使用已经编译成WebAssembly的代码并且将其同Dart链接。对于第二问题而言,把WebAssembly连接到Dart,我想有一个包已经可以做到这一点。虽然不是最方便的方式,但是是可行的。至于将Dart编译成WebAssembly,目前还是不可行的。这需要WebAssembly去实现一些尚不成熟的特性,WebAssembly GC,多线程等等。我们对此很感兴趣,我想WebAssembly有潜力在未来几年真的成为一种统一的互操作语言。MFlutter Web目前有两个渲染后端,我们默认使用HTML。HTML+DOM+CSS后端这种方式来渲染应用,但我们也在尝试使用CanvasKit来进行渲染。今天我们已经稳定下来,你可以使用CanvasKit,它采用了WebAssembly和WebGLS来渲染App,以在浏览器中替代Skia.针对这两个不同的渲染后端,我们也有一些叫做auto的内容。他可以针对不同的环境来选择渲染器,在桌面中使用CanvasKit,在移动浏览器中使用HTML,以便充分采用两者的优点。主持人同React,Angular相比你怎么看Flutter Web?E我首先想到的是,Flutter Web,我们只是在Canvas中绘制,我们认为是直通GPU/CPU,我想这是同React和Angular很大的不同。I我总是很犹豫去把Flutter同别的技术去做对比,因为每一个都有有效的用户场景。我不想去提React说,React这里好,那里不好。这完全取决于React,我们很高兴能沟通这些其他技术并存,我们也希望整个社区作为一个整体可以写出指南说,你知道什么样的场景下Flutter很适合,什么场景下Flutter For Web很适合,什么样的场景下React很适合等等。主持人你认为我们应该使用哪个渠道?IStage主要的区别是,stable同其他渠道的差别,我们会把fix pick到stable channel。因此你可以看到stable channel每次更新变化都很小。这一点不会发生在dev分支上。我们不会检查dev渠道,如果dev出了问题,他会被在trunk上修掉,然后我们重新会在未来几天生成一次dev.这也不会发生在主线上因为我们一直在主线开发。这里就有风险,越近的代码,越容易有我们没有捕获i的问题。当然,他们最终都会被修复,这是一个权衡。▐ 结论这是个令人振奋的发布,至此我们可以说Flutter真正做到了以应用为中心,全平台的支持。不论是面向移动,还是面向桌面,或者是Web,Flutter都做到了产品级可用。面对日趋激烈的业务竞争,其可显著降低开发成本与人员不足/不均衡的问题,提供更稳定一致的用户体验。但从另一个方面讲,国内市场普遍面临的Legacy System的问题,目前看从官方渠道并没有一个解决方案。尤其是对于桌面端的问题,Windows XP,Win32这样的应用场景下,以及FlutterWeb性能体验兼容度的问题,业务方还是需要一定的备选方案。其他的诸如移动设备性能,包大小,动态性,浏览器兼容度,目前原理上本身已经不是问题,只是为了性能,大小,体验考虑,还是需要做更多的深入细致的优化工作。▐ 结论参考资料What’s New in Flutter 2.0Announcing Dart 2.12Flutter web support hits the stable milestoneLanguage design funnelFlutter design docFlutter Engage YoutubeFlutter FolioAnnouncing Flutter support for foldable devices
技术人不可错过的年度重磅百宝书来了! 复制该链接到浏览器完成下载或分享:https://developer.aliyun.com/topic/download?id=1080 作为阿里巴巴新零售技术的王牌军,基于淘系丰富的商业和业务形态,淘系技术在大前端、音视频、端智能、用户增长、客户端架构、服务端架构、云原生、技术质量、以及AI类等技术领域有着丰富的思考和沉淀。 淘系技术将2020一整年的精华内容梳理合并,重磅推出【淘系技术2020技术年货】。在这本书中,你将看到: 各技术栈下时新前沿的技术讲解与方法技巧 淘系技术大牛的职场成长经验&学习问答实录 年度精选技术人员必读书单 淘系经典开源项目介绍 2020淘系顶会 paper 全文 点击此处立即下载《技术人的百宝黑皮书》 目录 本书内容页数 1500+,全部内容将近 40w 字。作为一群年轻热情有活力的技术er,我们深知,输入重要,输出同样重要。我们热衷于分享和交流,不止于内部团队,不止于业务形态相似,我们期望在不同业态、不同背景的碰撞和交流中,给彼此带来更深的技术理解。 我们怀抱着开放自由的交流心态,希望小伙伴们在新的一年里收获满满,成长多多。 也欢迎大家转给有相同兴趣的同事、朋友,一起切磋,共同成长。 复制该链接到浏览器完成下载或分享:https://developer.aliyun.com/topic/download?id=1080 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
2007年,Jeff Atwood 提出了一个著名的观点, 戏谑又似认真地称其为 Atwood's Law(https://blog.codinghorror.com/the-principle-of-least-power/):any application that can be written in JavaScript, will eventually be written in JavaScript. 时间快速穿行13年到今天,仿佛在印证戏言成真:在互联网软件工业的疆域上,以ECMAScript 为圆点朝各个方向射出一箭,凡目力所及的范围内,皆似洒落上了这一箭之威。 而在阿里,也像在诠释着这些上述断言,前端技术如初生牛犊般从蛮荒时代的PC Web 踏入到多个领域,在许多重要战场中发挥着重要作用。 本文,简单分享几个前端领域在阿里的应用场景,附带一些我对前端技术领域的一些思考,期待能够和众多的行业同仁们有交流互动的机会。 从 Web 互动到媒体互动 在早期,Web上的互动是为提升页面氛围作为附庸而存在的,为此有一个专用词“网页特效”作为代称。 曾经很长一段时间,互联网上存在大量的特效代码库、特效网站专门服务于开发者。而这些特效的基础原理就是通过 Javascript来变换样式和操作DOM实现的。 随着标准组织和浏览器厂商的不断努力,现代化互动的基础开始成型。除了硬件性能提升外,HTML5/CSS3,Canvas、WebGL让互动的开发显得更为标准、更高效可行,而非各种原理古怪、性能堪忧的Hack技巧。这也让在Web上实现大型互动成为可能性 - 可是要知道,曾几何时,Flash几乎是“双十一狂欢城” 唯一的选择。 今天,互动产品及对应的互动前端技术早已成为各大互联网公司的标配: 在技术上:继承了Web的优势,能够调整迭代,无需发版,天生跨端的同时还能兼具不错的性能。 在商业形态上:更游戏化的互动包括不限于“蚂蚁森林”、“淘宝人生”、“天猫农场”等类社交游戏产品使“人与人”、“人与平台”之间的互动具备了更好的可玩性、用户黏性,从而具备了更高的商业价值。 2020年,互动技术也成为阿里经济体前端技术的重点发力方向。 以淘系互动技术为例,它构建在一个大型、完备的前端基座上:一体化的工程、构建、容器、框架、发布系统、渲染引擎。互动前端技术的核心简单地大概分为三部份: (互动)框架:基于游戏领域的通用构架,自底到顶的分层实现-Render/Render OBJ/Design Pattern/Utils,具备加载器、ECS、场景、插件化扩展等基础能力。 (互动)素材中心:接收并处理互动展示层所需要的资源并输出成型的互动素材,并通过SaaS化服务进行管理。 (互动)研发平台:面向互动生产者的工作平台,它具备包括不限于编码、拼装、编排在内的构建能力。 基于这套互动技术体系,冷冰冰的商业化产品开始具备了越来越多的趣味性和创意体验。 当互联网基础设施不断完善,硬件与带宽成本持续降低,直播/短视频逐渐形成获取用户时间的主流产品形态,也成为人&人、人&机互动的新场景。 传统的解决方案是在视频媒体上“遮盖”一层Web页面,内嵌在页面中的互动(如领取红包)和视频内容没有事实上的关联,而是通过预置的逻辑进行触发,“伪装”成视频与互动同步的用户体感。这种方式成本低廉,但代价则是牺牲了创意和灵活性,缺少想像空间,没有未来。 而媒体智能互动则是更合理的解决方案:通过智能化手段进行手势、表情等识别,互动素材与效果均与视频内容强关联,并在视频流上完成素材渲染。 从实时渲染角度来看,核心技术环节在:图像采集、数据处理、算法识别、渲染计算、端渲染展示 从研发生产角度来看,关键流程在:玩法生产、应用管理、玩法使用、端渲染展示 上述每个节点都涉及到各个关联技术及工具/产品,正是这些能力组成了媒体智能的技术体系。 从长远来看,无论在互动的自身形态、输入方式、承载媒介还是玩法创意,都必将有长足地发展,对于前端体系的从业人员,只要持续关注用户&终端、熟悉业务、不断学习必定会获得长足的技术竞争力和创造力! 搭建,不止于提效 过去几年,我在负责面向消费者端的搭建体系:把形形色色、千奇百怪的页面都看作组件们的集合,然后用极致简单的搭积木方式将它进行可视化组装。 如果拿冰山举例,我们尽量让用户们(运营、开发者)只看到露出海面的那一段,把概念和实操尽一切可能地简化,而被屏蔽在冰山下面的东西,包括了一系列的交付 高度被抽象的 界面+数据描述标准 ,我们称之为模块; 兼顾性能和灵活性的 客户端+服务端渲染架构 ; 离线的、简洁的 零代码可视化平台; 提供线上服务的 页面渲染引擎 + 数据网关; 有了这些交付物(以及基于它构建的大型生态),前端、设计师、后端(多数时候甚至不需要)围绕着高度可被复用的产品模块即可进行页面组装,将业务发布上线。这个方案简单又高效。以至于在很长时间,这套构架最终产生成了数以百万计的各种页面,其中包括不限于双十一,我们在阿里系各种App看到的很多页面都是基于这样的方案出来的。 然而这并不是终点。 其实道理也很简单:当效率上达到一定临界点后(通常是边际效应开始递减),对应的技术方案都越界碰触到另一个领域。 搭建技术也一样 - 一个业务的运行通常不是搭建一个页面就完成了 - 它涉及到整个业务的执行周期。好比一个线下商场要做一场年货活动,上架商品 这个任务只是工作中的一部份,其他包括不限于选商家、选货品、制造氛围、顾客动线都是必须得完成的工作。 而线上业务的运行亦如此,除了“搭建页面” 这个看似简单的动作外,还有各种上下游环节,包括不限于 - 数据哪来(选品)、以什么规则展示(算法千人千面)、抵达什么用户(人群规则)、界面是怎样的(新式交互)、在哪儿展示(跨端渲染)、浏览体验是否又快又好(性能&质量)、业务效果如何(数据看板)等等,每个环节都或多或少地关联着前端相关技术。 基于此,我们根据业务的实际需要拓展了系统的功能边界。慢慢地,原本的面向效率、被高度抽象化的工具系统成为了一个解决具象业务的产品。 在这个转变过程中,原本核心工作是完成前台页面的前端程序员,逐渐成为了一个面向业务的技术工程师,而其工作职责从原来的高效交付扩大到了方方面面,技术视野、技术能力都得到了很大的提升。这时,我们也很难单纯用技术栈来界定这位程序员是前端抑或是后端工程师。 相信在未来,这样的前端工程师会越来越多,也会成为前端发展的一个方向。 技术栈从来不是技术人的桎梏! Serverless,孕育新的生产关系 还是得谈谈这个话题,如果说越发完善的研发工程体系在提升交付整备质量、周全的性能故障监控系统在改进交付效果,那么 Serverless 就是在尝试着在最合适的环节来优化生产关系。 想必很多初入 CRUD 阶段的前端同学都尝试写过一个形如 Todo、blog-like等应用,基于广受各类教程推荐的 Express/Koa+MongoDB+ReactJS/VueJS 套件方案实现。 然而写完应用后的踌躇满志在遇到技术面试/业务应用时则一片茫然:怎么和预想的不一样? - 性能调试、高可用、容量规划、多应用调用、数据库优化、跨栈中间件等等都是未曾太多考虑的问题,但若稍加深入思考,无需很久就进入新一阶段的灵魂拷问:为什么我要使用Node而不是其他工业化程度更高的语言技术栈? 而云原生下的Serverless 让多数前端们开始解除束缚: 便捷可弹的BaaS服务 弱/低运维成本 现代化的函数计算 高速交付 无用赘谈太多的优势,只需缓解运维成本、稳定可弹的计算资源,在完备地BaaS下,离终端用户足够近的前端们就能快速的进行多栈开发,而这则很大机会带来的生产关系的变革,重新定义前后端的边界 - 把原来以 BFF 层为界碑的研发模式重新刷新一遍。 在这个基础上,一部份的前端职能会发生深刻的变化,他们为成为离业务更近的产品工程师,既可以实施商业小程序/轻应用,也能主导一场营销大促、也能驱动一个新业务场景的诞生。 当然,围绕着这种生产关系的周边生态,如职业定义、招聘要求、未来前景、企业人才规划都会随之发生变化。 也期待着这一天的到来。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
作者|豆豆、定源 都说憋大招需要时间的积累,一位刚踏出校园入职阿里巴巴淘系技术质量才一年多的新同学,凭什么登上测试行业最高讲台之一的MTSC大会主会场做分享?他是怎么做到的?让我们来看看他的成长故事吧。 ▐ 自我介绍 大家好,我叫黄俊,阿里花名豆豆。从毕业那刻起变加入了阿里巴巴淘系技术质量团队,有幸加入到团队AI智能化测试方向的研究探索中。经过这一年多的成长,从对业务的融入、理解、思考,到质量建设上的构思、试错、突破,我受益良多。感谢于良师益友的老板指导,还有一对一师兄的辅导,更有一群志同道合的同学一起并肩作战,与善人居,如入芝兰之室,久而不闻其香,超nice的团队决定了我的现在。 对于即将毕业或者想来阿里做测试开发行业的新同学,如果你还在迷茫或者有疑问,不妨继续往下看看我的心路历程: 初入职场的迷茫 主动出击,找到切入点 下钻深入思考 聚焦并落地拿结果 ▐ 初入职场,偶遇丝迷茫 还记得你的第一个测试开发场景吗?这里附上我的第一个需求&质量建设: 新人时期,我的第一个接手需求是手淘消息的搜索(如上图所示);第一次完整走完流程的需求是手淘消息的智能push,包含了评审、排期、开发、联调、提测、测试、灰度、bugfix、全量等等;第一个质量保障工具是手淘消息客户端日志的上报。 总的来说,我的新人时期,在不断学习识别业务需求的过程中,时刻洞察质量建设突破点,逐渐完成了从学生到职场的转变。不过随着业务的深入,虽然可以在相关业务上独当一面,但是如何找到测试技术和业务结合的切入点?我不禁陷入了迷茫。现在回头来看,这应该是很多测试开发的新人都会遇到的一个问题。 好在这一时期并没有太长,之后我开始多和师兄、主管、团队老同学的交流,主动走出去和不同部门的同学交流,阿里真的是一个很大的舞台,高手如林,我的眼界和思维被迅速打开,经过和师兄以及主管的反复对焦,我开始逐渐找到适合自己的场景。 ▐ 主动出击,寻找点切入 在经过一段时间的适应和转变后,自身对整体消息场景已经有了一定的基础见解,对切入点的感觉开始变得懵懵懂懂,渴望着将一些新的技术融入于现有的业务场景,便踏上了寻找可以将想法落地的场景之路。 19年的春季,正值手淘消息全链路监控排查体系之际打造如火如荼之际,也是我第一次接触测试右移的道路,在和消息测试团队的师兄师姐们一起研发的同时,逐步开始沉淀通用能力,例如诸多系统间错综复杂的链路如何秒级定位能力、统一降级、统一采样等等。 随后在团队重点研究的AIOps领域,确定了多个研究方向,而其中的“智能异常检测”方向,在当时还是新兴领域,集团还没有特别好的解决方案,而异常检测本身其实是实际业务中的一项关键性需求。在Devops领域,指标监控是系统稳定性的关键,也是监控/运维平台成功的关键,成功的异常检测有助于发现异常后及时提醒用户采取相应的措施,避免更大故障的发生。 通过对国内外现状展开调研,并与达摩院同事进行了交流与合作,对智能异常检测行业现状的了解和摸底,我们的研究内容开始逐步有了线索。虽然这个任务技术挑战很大,也没有太多可以参考的先例,但是我觉得可以发挥我有算法基础喜欢技术钻研的优势,便义无反顾地ALL in到这个任务的研究探索中,并且在过程中我发现自己找到了技术和业务结合可以落地的切入点。 找到切入点并不容易,结合对阿里“六脉神剑”价值观的践行,阶段性总结一下有以下几个方面的成长: 1、经济基础决定上层建筑,业务理解决定质量建设。 2、不要怕折腾,多沟通,少情绪,有耐心。 3、Keep Learning,不为失败找借口,只为成功找方法,全力以赴拿结果。 ▐ 深入思考,聚集智能化监控 在测试右移智能化的道路上,逐步找到切入点后,便开始在这个领域深入研究。如果将一个产品的上线一分为二,左移是往测试之前的开发阶段移,右移则是往上线之后移,这就要求测试开发同学需要做好指标监控。指标的监控关乎稳定性,但随着数据量的增加,加上指标的复杂周期性和模式变化的动态性,基于传统监控的阈值/同比环比的规则难以适用,而且复杂的领域知识导致为每条指标配置相应的规则费时费力,无法应用在大规模数据监控上。 在很多时候,给我的感触是,右移比左移更具有挑战性。因此,我们迫切希望传统监控向智能化监控进行转型,基于团队前期对海量系统日志数据实时计算的基础上,我通过结合百余种实时数据,尝试了20多种异常检测算法,一个全新的智能化异常检测平台呼之欲出。 然而聚焦和深入到智能异常检测方向的研究并非一帆风顺,中间经历了不少曲折克服了不少困难,比如: 场景1、在面临百余种数据类型时,当现有的业务数据跑出一个还不错的模型,而换在另外一种业务数据效果就大打折扣,如何保障这么多数据类型都有一个不错的效果? 解决思路:随着基于对业务数据的深入理解,基于团队前期对海量系统日志数据实时计算的基础上,结合百余种实时数据,尝试了20多种异常检测算,逐渐探索出对于业务数据的算法场景化能力。 场景2、当面临大量业务指标的接入,或者是一个全新场景,没有那么多时间来训练拟合该指标的模型,如何保障业务可以顺利地快速接入使用? 解决思路:提前将场景化的基础模型作为快速接入的选项,并结合无监督学习,配合完成新指标的接入使用,随后再进行逐步迭代,以适应新指标的快速接入。 场景3、当在实时检测有了一定的沉淀能力后,越来越多的业务方反馈,Holmes的报警能不能再提前一点? 解决思路:真实的业务现状使我们体会到单有检测能力是远远不够的,晚一分钟发现问题,往往也是致命的,致使我们踏上了预测的研究道路。 经过不懈的努力,智能化异常检测平台(Holmes)终于落地,并且在淘系核心战役巴拿马项目上线以及双十一大促备战的过程中发挥了作用。下面就展开简单介绍一下这个平台: ▐ 聚焦落地,福尔摩斯(Holmes)-- 淘系智能化监控平台 Holmes是一款智能化、轻量级、易接入、可扩展的异常检测平台,使用基于AI的异常检测算法,替代传统的规则监控方案。解决规则告警系统准确率低、时效性低、规则配置复杂与耗费人力等诸多问题。 特点: 学习历史数据,分析当前指标曲线趋势是否异常 基于以往数据,进行预测未来指标走势 优势: 算法检测代替规则检测 告警准确率高 更早发现异常情况 可适应业务发展带来的趋势变化 解决的异常场景: MSTC大会-主会场分享 基于Holmes的落地,我和师兄董福铭(吾铭)一起在MTSC 2020大会申报了议题《手淘AIOPS实战-消息全链路智能监控》,结合Holmes平台的实战做经验分享。 介绍如何通过SDK实现应用内链路日志聚合、采样率控制、统一降级开关等功能,打通客户端到服务端链路,实现IM端到端秒级排查。通过实时计算实现消息核心指标到达率/时延的实时监控。使用AI检测算法,替代传统的规则监控方案,解决规则告警准确率低、时效性低、规则配置复杂与耗费人力等诸多问题。通过NLP进行舆情智能分类,并结合全链路数据对预警问题进行分析定位,打造全链路智能监控排查平台。 ▐ Holmes异常检测平台 配置化指标接入流程 通过4步简单配置进行指标的接入和算法选择,轻松开启智能异常检测。 基本算法概览 机器学习上有一个定理,叫没有免费午餐定理(No Free Lunch,NFL),表示的是在机器学习上,不存在一种通用的最优的算法可以解决所有问题,因此在面对各类数据时,Holmes将算法场景化。 在实时检测方面,集成了无监督学习和有监督学习,主要运用了高斯分布、STL、孤立森林、XGBoost等; 在数据预测方面,集成了LSTM、Prophet、三次指数平滑等。 实践效果 目前Holmes异常检测平台已经在集团内部开放接入和运行,支持集团内常用数据源,帮助接入业务方的开发测试同学构建智能监控体系,减少繁琐的规则配置,有效提高了线上质量监控的覆盖率。今年的多次大促期间,Holmes的准确性方面也进一步得到验证,有效保障了大促的稳定性质量。 覆盖应用:淘宝、千牛、优酷、钉钉、淘宝直播、闲鱼等 接入指标:核心业务指标 300+ 提前预警:有效提前预警线上问题 50+ 算法调用:5000W+ 展望 Holmes异常检测平台是淘系技术质量团队打造,在智能化测试领域的一次实践,未来我们希望利用AI算法实现业务全方位智能化监控和问题定位。覆盖更多的数据类型,打造通用的算法模型,同时我们也在全链路监控排查、智能舆情处理等多方面进行探索,期待后续跟大家分享。 ▐ 主管点评 豆豆同学19年4月份硕士毕业后,加入阿里巴巴淘系技术质量团队,仅仅通过一年多的时间就成长为团队技术骨干之一。针对豆豆同学的特点,以及团队将19年的整体打法做了分解,将团队AIOps领域探索的智能化异常检测(智能化监控中的重要组成部分)安排给豆豆。对他有两个期望:一方面期望这个能力可以有纵深的下钻,作为整个IM消息测试中台中测试右移的主要武器之一。另一方面也期望可以横向扩展,未来逐步在阿里集团内进行推广使用。作为主管也明白这个技术探索对新人来说有一定的落地风险,当时并没有太多可以参考的先例,而且对同学的AI算法能力和工程化落地能力均有很大的挑战。这也是豆豆加入淘系技术质量团队后的第一个测试技术创新开发任务。 面对压力和挑战,我们很欣喜地发现,豆豆同学并没有被压垮。他迎难而上,查阅了大量业界和阿里集团的资料,调研开源算法、阅读论文,结合师兄吾铭的指导和消息测试团队的反复讨论,豆豆不断优化和完善技术方案,先后克服了多项技术难题。最终Holmes智能异常检测平台在手淘消息场景落地使用,为第一年阿里生涯交出一份漂亮的答卷。新财年,豆豆继续发力,不仅扩大了算法支持的场景个数、准确率,还多次提前预警线上问题,并将支持的业务系统从淘系扩大到了阿里集团多个BU,受到广泛好评。 对于豆豆来说,精彩才刚刚开始,Holmes的落地只是一个起点。今年双十一之后,一个新的保密技术创新项目又开始酝酿,豆豆同学届时又会憋一个什么样的大招?让我们拭目以待! 这里也简单展开一下,新同学除了学习基本功之外,新同学也可以在师兄和主管的引导下,逐步养成思考的好习惯。比如,如何让自己和团队产生更多的链接(多看多沟通多思考)?如何与所在团队的大方向结合(埋头干活的同时也抬头看路)?如何站在团队研究基础之上进一步构建自己的创新产品(借力)?等等,我觉得是新同学们可以思考方式上去做一些转变的。 豆豆同学只是整个淘系技术质量团队(极测)中的一位典型代表,这样优秀的新同学在我们部门还有很多。我们有非常体系化的极测新人培训课程,以及阿里非常著名的“百阿”新人培训,淘系技术质量团队(极测)会针对每一位入职的新人安排一对一的师兄指导,也会针对同学的特点设计和规划职业发展方向。在部门技术驱动的理念下,不仅会对同学的业务理解培养,同时还会在技术创新上给予同学相应的发力场景和落地辅导。结合上一对一师兄和主管在日常工作、生活中对具体的问题和项目实战中的点滴进行具体指导,相信新加入的同学们一定也会像豆豆一样快速成长! MTSC2020中国互联网测试开发大会深圳站现场 淘系技术部-(极测)质量团队-诚招英才 我们负责所有淘系业务的质量保障。在这里:可以经历双十一等超大并发场景;可以接触到全链路压测、海量的数据处理、人工智能推荐算法等领域;可以学习到业界最前沿的测试技术、一对一指导的师兄辅导机制、定期而丰富的技术分享;还可以与层层选拔的各路优秀同学共同战斗,共同成长!我们以技术驱动,构建业界领先的质量体系。 我们的使命是让测试更简单,让研发更高效 ,让用户体验更极致。 还在等什么,快来加入我们吧!AI智能化测试新赛道,你就是下一个明日之星!邮件发送至:dingyuan.jh@alibaba-inc.com
自 2019 年 4 月在 Github 开源以来,淘系技术部-端智能团队自研的 MNN 推理引擎,因为其高性能、易用性以及优秀兼容性受到不少开发者的支持和喜爱。我们也把这份支持化作不断前进的动力,仅最近半年就推出了包括但不限于如下的诸多亮眼特性: 几何计算。通过 MNN 自研的“硬核”技术,将多后端算子实现这项枯燥但必要的工作成本大幅降低。正是基于这项技术,MNN 重写了目前所有的硬件后端。引入几何计算之后 GPU 后端算子的覆盖率大幅增加,在GPU 后端性能普遍获得约 20% 提升,并新支持了 TensorRT 和 CUDA 后端。 基于 Transformer 结构的 ASR 模型的支持。为了应对这类大量涉及 Control Flow、Dynamic Shape和Zero Shape等特性的模型结构,MNN 在框架层面进行了大幅度重构从而对其进行支持和完善。 关于 MNN 的强大,能说的还有很多,但我们不想再一次通过秀肌肉来证明 MNN 的领先独到。相反,我们希望通过这篇文章来说明更重要的一件事:来自开发者的声音,我们听见了。 在 MNN 开源的这一年多里,随着 MNN 被越来越多的开发者、企业所了解并使用,我们与社区之间的交流也愈加紧密频繁。在这之中,有一类的呼声经常被提起: “MNN 很强了,就是希望 iOS / Android Demo 更多一点。Python 还是有点不熟悉” “教教我们这些小白怎么上手呗。MNN 能干些啥呀~” “五子棋牛逼,支持五神!老板我也想搞端智能!” 没错,很多人对机器学习的陌生来自于未知,而正是因为这个未知让大家想象不到能用 MNN 实现什么。 所以我们在想,如果用一门更熟悉的语言,带大家走入端智能的大门,为自己的职业生涯开辟一道新的口子,这样是不是对大家更有帮助? 今天,对着广大的移动开发者,我们要大声的宣布:MNN for Swift 正式来啦!伴随着这个项目一同发布的,还有系列实践性教程 -《MNN x Swift 机器学习实战》。 通过这两个项目,希望能给各位带来清晰的端智能学习路径。 能从课程中收获什么 相信大家都对网上质量参差不齐、没有实际干货的课程感到深恶痛绝。所以在设计系列教程的时候,我们首要考虑的两个要素就是: 免费 硬核技术和动手实践结合 基于此,我们会以每两周一次的方式进行 5 次的系列课程教学,结合 MNN 工作台 AI For Everyone 的低实践门槛,带来值得期待的知识分享。 ✿ 系列直播预告 插段广告,如果你还不知道 MNN 工作台是什么,那就赶快前往 MNN 官网 www.mnn.zone 去下载吧。MNN 工作台是淘系技术部 - 端智能团队在今年 10 月份对外公测的一站式端侧 AI 平台。它是集低门槛预训练模版、开箱即用算法集、多端一键部署于一体的机器学习工具箱,通过 MNN 工作台,每个人都可以在几分钟内完成模型训练并部署到手机上运行看到应用效果。 整体的课程大纲如下: Introduce to MNN For Swift 介绍移动端机器学习现状 MNN For Swift 整体概览 MNN For Swift 实现原理 C++ 至 C 到 Swift 开发流程的最佳实践 MNN For Swift API 设计思路 MNN For Swift 进阶 - 玩转 Swift 自定义操作符 玩转 MNN 工作台 For Swit (一)- 模型预测 MNN For Swift 推理 API 使用介绍 手把手玩转 MNIST 手写数字预测 高级进阶 - 从 MNN 工作台获取更多高级模型 玩转 MNN 工作台 For Swift(二)- 模型训练 MNN For Swift 训练 API 使用介绍 用 MNN Swift 构建 MINST 数字识别模型 高级进阶 - 通过 MNN 工作台训练更多模型 MNN For Swift 应用实战 OCR - 光学字符识别简介 MNN 工作台 OCR 开箱即用模型简介 使用 MNN For Swift 部署 OCR 模型 完整应用案例展示 怎么样?看完大纲后是不是对使用 MNN For Swift 进行机器学习充满了好奇?那就敬请期待我们后续的课程吧! 当然有人会问:“付出那么多,你们想从这个课程中收获什么?” 很简单,我们希望通过这个课程让大家了解端智能是什么、如何把端智能和自身的日常工作进行结合。对那些积极参与《MNN x Swift》系列课程的朋友,如果您对 MNN 和 Swift 有什么独到的见解或者建议,也会邀请您参与到我们的直播中,共同打造 MNN For Swift 的社区生态! 只有更多人一起来玩端智能,这个新兴的领域才能受到更多的关注、获得更长足的发展。 Why Swift 最后我们还想来谈谈为什么 MNN 会选择在这个时间点支持 Swift。 一直以来,因为其强大的社区活力和易用的特性,Python 始终把控着机器学习社区语言的头把交椅(虽然 Julia 也发展的很迅猛)。Tensorflow、PyTorch 以及最新推出的 MNN 工作台等主流的机器学习框架或工具更是和 Python 这门语言紧紧的交织在一起。 但是将 Python 搬到移动端上却不是一件非常容易的事,引用 Tensorflow 对于移动端应用 Python 的观点来看: 部署麻烦,运行时依赖太多。 没有编译期类型检查。这导致很多错误要到运行时才能发现,在移动端,这些不可忽视的错误常常导致严重的应用崩溃等重大用户体验问题。 性能太差,并发困难。而机器学习模型对算力的贪婪需求,迫切需要靠并发缓解。 而自 2014 年 WWDC 正式发布之后,Swift 已经逐渐成为了苹果开发者生态中的主流开发语言。作为一门比 Objective-C 更加现代化、更加安全的编程语言,Swift 已经获得了国内外广大开发者的喜爱。同时,应用 Swift 可以让我们“免费”享受到苹果工程师持续不断的性能、稳定性优化成果。 更重要的一点是,当我们基于 Swift 实践了部分机器学习开发工作后,我们惊讶的发现,Swift 竟然在机器学习领域有着与 Python 相媲美的表达特性。 用如下一段 Python 和 Swift 的 MNN 编程片段进行简单对比: Python 代码: # F = MNN.expr input_data = F.placeholder([1, 3, image_height, image_width], F.NCHW, F.float) input_data.write(image_data) input_data = F.convert(input_data, F.NC4HW4) outputs = ocr_det_net.forward([input_data])[0] outputs = F.convert(outputs, F.NCHW) data = outputs.read() 同样的代码在 Swift 中: var input_data = Expr.placeholder(shape: [1, 3, image_height, image_width], dataFormat: .NCHW, dtype: Float.self) input_data.write(data: image_data) input_data = Expr.convert(input: input_data, format: .NC4HW4) var output = ocr_det_net.onForward(inputs: [input_data])[0] output = Expr.convert(input: output, format: .NCHW) let data = output.read() 毫不夸张的说,如果不加以提示,可能根本不会感受到二者的异同,可见二者在语法表达上十分接近。 除了同样充分的表达性以外,Swift 在移动开发领域天然的优势(苹果大力支持)以及语言自身的安全特性都让 Swift 比起 Python 而言更适合移动端机器学习。 这也是我们为什么下定决心要开展 MNN For Swift 的重要原因。 在内部项目中,我们已经用 MNN For Swift 与 SwiftUI 完成了机器学习应用的编写,91% 的代码均为 Swift。由此可见 Swift 在移动端机器学习领域是能让开发者快速上手,降低开发者的开发门槛的一门优秀语言。所以不要犹豫,赶紧把 MNN For Swift 学起来。 结语 纸上得来终觉浅,绝知此事要躬行。不要再为自己每天还在糊 UI、画 Label 、组装 TableView 而感到焦虑。通过《MNN x Swift 机器学习实战》,我们希望让大家感受到深度学习不止是从事算法专业人员的“独门武器”、也不是大厂宣传秀肌肉的利器,而是让所有爱好技术的人都能参与实践的自我提升手段。 也希望借助 MNN For Swift 项目及系列课程,让大家感受到 MNN 积极拥抱社区、响应开发者呼声的热情与决心,给开发者们缓解一丝冬季的焦虑。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
背景 “今年的双11是全球极大内容电商场的超级爆发,消费者、技术、内容与商业生态之间每一秒都在产生激烈共振,实时性、复杂性和持续峰值的叠加令其成为全球技术顶峰。2020年双11,阿里巴巴峰值交易达到了58.3万笔/秒,其背后的商家链路也承受着史无前例的压力”阿里巴巴副总裁汤兴如此描述今年的双11。 阿里商家业务域涉及集团近20条业务线,100多个场景,400+链路,部分业务深度融入在导购、交易链路中。商家链路业务涵盖淘系的上千万商家及数百家核心三方服务商的伙伴。以往由于平台和商家IT基础设施存在“水位差”,平台很难帮助商家进行系统的改造升级和全链路验收。随着阿里面向商家与生态伙伴推出了新一代数字化基建体系,借助云原生技术引擎与云IT治理能力,帮助商家与生态伙伴重构系统的高可用基线,使得越来越多的商家具备了和阿里同等量级的超大规模数据处理能力。 常规备战中各个业务依靠单链路压测,缺少互通联动,缺少全局把控,容易有质量盲点,隐患容易被忽视,风险较高。商家链路出现问题,直接影响商家生产经营能力,对商家和消费者造成重大的体验伤害。业内的全链路压测普遍面向C端场景,B端场景往往重视度不够。商家端的场景结构极其庞杂,涉及内部系统还涉及众多三方系统,开展全链路压测面临着诸多挑战。为有效预防风险,提升各个业务以及三方合作伙伴系统稳定性,提升用户体验,今年我们克服了重重困难首次面向商家链路开展了全链路压测。 挑战 ▐ 核心难点 商家业务涵盖商家工具、消息、多媒体、算法等多种业务形态,各应用间调用错综复杂,如何理清核心链路的依赖,保障核心链路不遗漏是开展压测的首要挑战。 压测实施上,如何模拟数百个场景流量的叠加耦合,并留好应对突发情况的操作空间,且在同一时间批量将流量拉到峰值是压测执行的一大挑战。 商家业务涉及众多合作伙伴服务商的系统,阿里生态的复杂性,使得接入阿里平台的服务商系统架构存在异构化、多样化、性能差异大等多方面技术挑战,每个服务商对于稳定性的认知存在差异。 ▐ 解决方案 针对上述挑战,我们的核心解决方案为: 统一流程规范标准、压测组织协同、统一验收复盘。 工具支撑压测一体化。 压测涵盖预案、限流、演练、监控等内容,模拟大促真实情况。 全链路压测方案 1、统一流程规范 各个业务参与全链路压测明确准入准出标准。全链路压测的基本准入原则:每个场景单链路压测通过后才能进入全链路压测。准出标准包括:系统水位、流量比例、响应时间、缓存命中率、限流、jvm指标等。服务商系统与阿里系统标准一致。 流程上统一进行场景review、链路评审、全链路压测以及复盘,复盘中的问题同步优化解决。 2、工具支撑一体化 在集团已有的压测能力基础上,我们重点在商家链路分析,流量评估,压测模型、结果校验自动化等方面建设一站式压测工具,解决商家全链路压测的核心问题。 链路分析 压测场景选取的基本原则: 大流量场景 核心链路场景 复杂业务链路场景 流量扩散场景。 参照以上原则,在我们在集团中间件支持的基础上,开发了链路分析场景推荐工具,用人工+工具check双保险的方式进行链路模型生成。示意原理如下图,应用A某接口的一次调用,我们可以获取其下游应用的trace,访问的数据库或者缓存,并且分析出1次调用对下游流量的放大。 链路示意图 其工作流程如下: 统计流量调用的服务,存储等指标,获取对下游的调用次数,得到对下游请求的放大。 获取流量大于某阈值的入口链路。 获取调用链路深度或者依赖调用大于某阈值的链路。 获取翻倍调用大于某阈值的链路。 流量评估 常规流量评估通常是根据监控和上游调用来评估接口流量,评估比较理想化,容易产生误差。如果前期流量预估不充分,会导致压测无法达到目标或远超目标值,靠压测来发现这些问题成本太高。对此我们引入了深度机器学习,利用算法能力来做智能预测,更加精确的进行流量评估。其主要的工作原理如下: 抓取应用的出入口流量、RT、QPS、错误率、应用系统水位等信息。 应用深度机器学习,对基础数据进行分析学习,产生该应用的算法模型。该模型可以根据入口流量,预估出口流量、应用系统水位等信息。效果如图所示。 接口流量模拟对比图 其核心思想为: 算法每天根据线上真实数据持续学习,持续优化,生成可靠模型。 用户输入接口预估流量,平台输出预估的下游接口流量、存储QPS、机器数和系统水位等信息。 模型构建 结合业务场景,我们建立了两种压测模型:0点模型和非0点模型。0点模型涉及场景在大促0点流量达到峰值,非0点模型场景峰值则在其他时间达到峰值,压测模型的设计不是简单的覆盖业务,还需要考虑各个业务之间的流量耦合。有上下游关系的链路,下游的压测流量也是来自上游。除了考虑商家域内的流量耦合,还要考虑集团其他域的流量耦合。因此,与之配合的还有非常多的压测预案,用以控制压测流量的准入准出。 有流量的耦合就要考虑耦合流量不足的情况,不是每次压测上游都会有充足流量打到下游。在保障能够耦合上游流量的同时,下游业务自身还要具备流量补充能力,当来自上游的流量不足时,自身可以补充足够的流量,满足自身系统的验证。 3、压测执行 压测数据准备采用淘系技术质量部自研凤凰平台[见附1]录制线上流量做“平移”,场景数据更加真实有效。录制化也提升了数据准备的效率,一次录制多次使用。场景管理上做到了1人1键执行所有的场景,极大的节约人力。每次全链路压测商家和交易、导购都会同步开展,0点流量全部打到100%,以验证跨业务域的流量依赖和叠加的情况。从压测开始到流量拉到100%时间非常有限,在压测初期,这个过程中通常会伴随着⾮常多的问题。这些问题在前期准备过程中很难发现,主要问题有以下⼏类: 压⼒分布不均。 模型紧急调整。 数据⽂件紧急修正。 个别压测机异常。 受压测管控影响,服务调⽤受限。 压测任务紧急管控。 执行⼈员需要⼀边控制不受影响的业务继续按计划加压,同时需要迅速对出现的问题作出判断,给出解决方案。对于能够快速修正的场景,⾸先将其从压测活动中单独摘掉,调整完成后重新关联到压测任务中。对于⽆法快速修正的场景,要迅速协调业务同学进⾏单链路操作。受上述问题影响,现场需要制定不同的应对策略,主要包括以下几种情况: 压测过程中某个场景需要紧急卸载压⼒,其他场景保持。 压测过程中某个场景没有达到⽬标,但系统资源已经出现瓶颈,需要保持压⼒⽔位排查问题,其余业务继续加压。 压⼒拉到100%个别业务没达到预期需要继续增加压⼒。 这⼏类问题在前期压测过程中出现频率⾮常⾼,如果前期准备不到位会阻塞压测节奏。 此外全链路压测具备从客户端(成功率、错误量、舆情等)到服务器(接口成功率、中间件成功率、水位等)的完整监控体系,避免出现只关注到服务可用性而忽视客户端及用户实际体验受损的盲区。 在数百个商家全链路压测场景中,消息、开放、小程序是几个典型的非常规压测场景,压测实施难度大,接下来重点介绍一下这几个场景的全链路压测实践。 ▐ 核心场景1: IM消息体系的端到端全链路压测 传统服务端压测通常是http或者tcp类型的短链接压测,也就是客户端请求一次,拿到返回后,连接就关闭了。但是这种方式并不适用于IM消息系统的压测,IM的系统与服务端建立的是长连接,用户A发送消息给用户B的时候,用户B是被动收到服务器的推送,而不是主动拉取数据,这个模式对压测的实施非常有挑战。 为了解决这个问题,我们基于NIO开发了手淘和千牛的瘦客户端,模拟真实用户与服务端保持长连接在服务端,并将部分业务逻辑做了参数化,集成到了瘦客户端中。比如瘦客户端在收到消息推送后,回复ACK,表示收到了消息,并且根据已读比例对该消息进行已读设置。 长连接瘦客户端 消息链路压测需要模拟客户端登录后保持长连接,目前业内常规压测工具对消息场景都不适用,需要独立开发工具。核心方案如下: 开发瘦客户端集成到压测引擎中模拟端到端的场景 模拟消息回复 模拟消息已阅读 模拟消息漫游 压测引擎部署到CDN节点上模拟真实用户收发消息 创建长连接模拟用户真实保持长连接在线 长连接压测架构图 全链路消息业务打通 消息全链路除了包含消息上下行,消息已读未读,消息内容模型(文本、卡片、多媒体消息),消息漫游等基础消息业务,也在集团二方和合作伙伴三方中衍生出了如机器人智能辅助、订单核对,修改地址,智能客服等消息业务。 消息业务图 由于消息业务的复杂性,导致我们在压测的时候需要协调各个业务方的压测数据,来调整实际压测时各业务方的压力。例如某个账号开通了A业务也开通了B业务,发送给该账号的消息走到了两个业务,导致流量叠加,最后的压测结果不准确。为了解决这一问题,我们统一收口了消息业务压测脚本,通过统一分配账号,通过统一开通业务,统一发起流量等方法,解决了流量叠加互相干扰以及各业务线脚本无法复用等一系列问题。 ▐ 核心场景2: 三方服务商全链路压测 淘系电商业务是一个覆盖消费者、平台、商家、三方服务商的生态链路。借助平台开放的接口,三方服务商可以开发出客户管理、订单管理、互动营销等方向的电商支持工具,帮助商家提升运营效率。要实现商家全链路保障,就需要驱动并赋能三方服务商一起参与到全链路压测中来。 淘系电商生态交互图 订单推送全链路压测 在涉及三方的众多场景中最核心的是订单场景,订单推送全链路覆盖了消费者在淘宝下单后到收到货物的整个过程。链路涵盖多个内外部系统,直接关系到消费者的购物体验,压测重要性不言而喻。 订单推送全链路交互图 订单推送全链路压测面临以下痛点: 服务商数量众多,没有统一的压测工具。 压测订单模型构造难度大、准确性差 压测结果收集困难,数据分析工作量大、标准不统一。 为此我们开发了订单推送全链路压测平台赋能服务商,平台具有以下特点: 基于历史订单数据一键生成压测模型,方便、精准 用户只需指定历史订单的起止时间,系统会自动分析该时间段内的线上订单数据,并根据订单的买家、优惠、商品等信息模拟生成压测订单模型。 订单推送压测模型 压测结果数据自动收集、分析,全面、直观 工具会自动从监控数据中收集相关压测指标,在报告分析阶段跟历史数据进行比对、根据压测标准计算出压测质量分、产出压测结论、给出优化建议。 订单推送压测报告生成示意图 支持Service Mesh技术,生产环境压测更简单、安全 压测平台支持云原生的Service Mesh技术,对于生产环境压测改造只需修改POD节点的相关配置,无需对系统业务代码进行改造,大大降低生产环境压测成本,同时减少代码改造可能带来的安全风险 ▐ 核心场景3:三方小程序压测 小程序是淘系在APP端开放的重要形式,由于三方小程序集成在手淘、千牛等淘系APP中,其稳定性关系到淘系APP的稳定和用户体验。受技术和安全上的限制,三方服务商无法自主对小程序接口进行线上压测,为此我们开发了小程序压测平台为服务商伙伴们提供高效、易用的官方压测工具。 三方小程序压测平台使用简单、易上手、自动化程度高,大大降低了技术门槛和服务商的压测成本,其核心能力如下: 系统根据线上接口流量自动分析、创建、下发压测任务。 服务商只需编辑压测参数,执行压测任务即可。 压测通过并提交报告后,系统会根据压测流量值设定限流保护值。 三方小程序压测流程图 此外,我们还组织了头部核心服务商联合预演。通过全链路压测,模拟业务峰值流量,演练了各种应急预案和沟通机制,以应对大促中可能出现的异常和突发状况,形成了压测、封网、预演的完整保障链路: 在全链路压测的同时对核心三方小程序进行压测,模拟更真实的双十一环境。 在压测的同时进行异常情况演练:人工模拟引入问题、故障。 对问题、故障进行紧急处理,演练沟通、协同机制和应急处理流程。 总结 全链路压测期间总计发现130+内部系统问题,大促前所有问题都得到了有效解决。整个大促期间商家业务系统0故障、商家问题反馈较往年降低了50%,商家以及消费者感受到了如丝般顺滑,捍卫了商家利益与体验。 一百多家核心的ISV参与了全链路压测验收,全链路压测帮助商家与生态伙伴重构了系统的高可用基线,使商家在信息处理和数据处理的水平上,和淘宝拉到同一个水位,使得商家具备了和阿里一样的超大规模数据应用能力,帮助商家系统发现并修复上千个性能问题,在效率和能力上大幅度提升商家系统稳定性。 未来我们计划将在三个方面持续优化,为商家和合作伙伴提供更优质的服务: 1、基于算法能力的智能化压测。 2、场景、预案、限流、压测结果等全方位的自动化分析。 3、多维度监控、海量告警信息的智能分析定位处理。 附1: 凤凰生态利用JVM-Sandbox提供的统一底层能力,大家奇思妙想产生模块原子能力生态,提供开放、快速的模块开发、管理、鉴权、部署能力(魔方平台),通过对模块能力的组合,衍生出录制回放、故障注入、强弱依赖梳理、系统Mock、快速问题定位、测试质量评估等上层产品模块(凤凰平台2.0),并对各个环节产生的数据、标准能力都开放API和能力,在业务回归、攻防演练、架构治理等领域输出方案,致力于快速、高效的提升系统整体稳定性。 开源地址: https://github.com/alibaba/jvm-sandbox-repeater?spm=ata.13261165.0.0.11ee30bfW4qEoF JVM-Sandbox属于基于Instrumentation的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截。 开源地址: https://github.com/alibaba/JVM-Sandbox?spm=ata.13261165.0.0.5a094b01By8WRH 淘系技术部-质量团队-诚招英才 负责保障整个淘宝和天猫主站的业务质量,在这里有丰富多样的业务场景和技术挑战。在这里你能够了解世界级双十一是如何保障的,面对双十一海量峰值流量,沉淀最具挑战的大促稳定性保障产品,在这里可以为过亿DAU的互动产品保驾护航,感受行业最复杂的营销玩法技术魅力,在这里可以近距离了解目前炙手可热的内容电商,见识李佳琦、薇娅等是怎样成为带货红人,在这里还可以探索大数据驱动下的前线业务和增长策略,在3D、AI、5G等新技术加持下打造一个个电商领域的新赛道。 在这里你还会和一群优秀的伙伴共事,这里有对技术的极致追求,使用业界最前沿的研发技术和理念,创新质量保障的方法、工具和平台,提升研发测试效能,不断完善用户体验,以技术驱动,构建业界领先的质量体系。我们的任何一点优化改进都将使数亿用户受益,期待你的加入,欢迎加入我们一起共同打造淘系的技术质量!联系方式:dahai@taobao.com 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
技术的成熟度源自大规模的实践,在 Java 领域,阿里将自身的实践源源不断的反哺给微服务技术体系;在 Node.js 领域,阿里正掀起了前所未有的前端革命浪潮,将实践反哺给 Serverless 技术体系,并逐渐拓展到其他多语言体系和后端 BaaS上。 Serverless 云研发平台作为阿里巴巴集团前端委员会发起的一体化云研发平台,底层基于函数计算 FC,是整个 Node Serverless 体系中的研发入口,承接了淘宝、飞猪、ICBU、考拉、高德、文娱等研发、交付和运维工作。目前,集团已经有上千位前端和客户端的工程师使用 Serverless 云研发平台进行业务的开发工作,包括但不限于营销导购、中后台、行业前台等规模化场景。 从今年双 11 整体的大盘数据来看, 仅淘系 Node Serverless 的支撑流量就已经从去年的 2K QPS 峰值增加到今年的 30K QPS 峰值,峰值流量增加了近15倍,集团整体更加是从近 5.8K QPS 到达今年的 50K QPS峰值。 解决方案上,我们定制了面向更多场景的能力,包括考拉 Dart 解决方案的落地,以及一些面向导购的模型驱动解决方案;运维上,我们优化了大促态和日常态流程,让开发者在应对更高 QPS 规模时,精力花费降低至少 50%;在研发体验侧,打造解决方案体系,降低研发门槛,支持前端快速入场,提升研发效率 39%;在底层 Serverless 基座上,我们适配了多个 Serverless 平台,支持多平台的实时切换,以应对单一平台的不确定性。 本文将介绍 Serverless 云研发平台是通过提供哪些能力保障各租户业务的快速开发和安全交付的。 研发的本质 大家可能都在「人员协同、服务可靠性」上支付着高额的人力成本,但研发的本质是交付「业务功能」。 今天,我们从传统的「前端开发者」慢慢走向「应用研发者」,摸爬滚打不容易,除了需要去思考「什么是真正的按需付费」、「弹性」等底层运维相关的命题之外,还需要去考虑「研发效能」相关命题,这也是为什么有更高效的协同模式、组织关系的变化,甚至整个前后端协同的生产关系都在发生变化的原因,今天我们谈「云端一体」,本质是从用户的视角去思考问题,用更高效的方式去解决业务问题。 如今,软件开发对于成本的控制要求越来越高,单位时间的产能会慢慢成为衡量一个团队是否高效的标准。 因此从研发的本质,我们来看看 Serverless 云研发平台要解决的命题: 让业务开发变轻,聚焦业务逻辑; 让业务开发变快,提升产研效率; 让基础设施变厚,提升稳定性。 Serverless 云研发平台架构图 Serverless 方案定制能力来完善云端一体研发者市场,提供开发者更多选择、打造云端一体的研发集成闭环来提供业务更快的交付速度、以及业务低成本的使用基础 BaaS 服务能力以及业务 BaaS 成为研发平台的核心抓手。 Serverless 研发平台 ▐ Serverless 业务解决方案 我们定义的解决方案 :即解决某一横向或纵向领域的,贯穿创建、研发、交付、运维阶段的一系列能力的集合。为什么当时需要定义解决方案的定制能力,核心原因是面向今天云端一体化的场景,不同事业部的业务同学有着不同的定制需求。 我们调研了几个事业部,包含 AE 、考拉、淘系等,起初的 Serverless 云研发平台的定制开发能力偏弱,无法很好的承接业务诉求,我们需要让平台有一定的开放定制能力,例如淘系面向研发面板的 low code 的定制能力,考拉面向函数的资损风险等级和应用风险等级录入等需求。 但是开放能力会涉及创建、研发、交付、运维这几个阶段,每个过程能提供什么定制能力、开放到什么程度是要由平台根据收集到的需求和平台自身管控要求去综合考虑的,所谓「人挪活,树挪死」,结构化了几个关键能力之后 Serverless 云研发平台开放解决方案的定制能力在当时多个租户的调研下产生了。 上图为结构化几个可定制节点以及多个场景的调研情况 通过上图结构化的信息,我们定义了解决方案元数据相关信息,示例为中后台一体化解决方案相关元数据信息。 { "name": "ICE-FaaS", "display_name": "Web 端一体化", "description": "传统 Web 一体化解决方案,解决中后台开发需求(ICE、React等),同时支撑中后台前端页面和 FaaS 的研发", "owner": "*", "generator": { "id": 30 }, "depserver": [], "page": {}, "widget": {}, "baas": {}, "ide_plugin": ["midway-helper"], "checkConfig": { "cf": true, "cr": true, "fone": true }, "flow": { "id": 1 }, "ops": { "resource": [{ "type": "faas" }, { "type": "assets" }] } } 截止目前,Serverless 云研发平台通过共建一共沉淀了 14 个解决方案,包括 5 个通用解决方案和 9 个面向不同租户的定制化解决方案。 接下去介绍 3 个典型的解决方案。 一体化解决方案 一体化应用解决方案是基于 Midway Hooks 提供的上层业务云端一体解决方案,借助 Serverless + Hooks + “零” API 调用的特性,开发者在研发流程中仅需关注业务逻辑,即可高效完成应用的交付。 一体化应用在使用时,具有诸多的优势: 易于开发,前后端同仓库,无缝融合一体开发 易于部署,前后端一同发布与部署 易于维护,后端代码使用Serverless 部署,运维难度低 而在开发时,我们也提供了诸多的功能来帮助开发者加速研发。 “零 API 调用” Hooks 支持 在阿里内部,我们提供了中后台一体化与搭建模块一体化两种解决方案。其中,中后台一体化应用在内部已经落地了 300+ 应用,快速且高效的支撑了各个 BU 的中后台需求。 淘系模型驱动解决方案 模型驱动是淘宝导购业务开发过程中沉淀的一种开发方式,面向导购大量的召回补全展现需求。通过配置面板,将模型、数据来源、插件配置组合,最终生成业务逻辑代码,供业务消费。 整个操作面板的核心关注点在右侧的流程画布上,我们希望使用固定的流程来解决这一类业务问题,这些逻辑遵从预定义的操作路径。在云市场轻应用外包介入开发的模式中,由内部同学生成物料,外包同学开发模块和选择业务字段并串联流程,帮助内部同学节省了大量流程串联和模块联调成本,相比传统的开发方式整体提效10%左右。这也是一种创新的协同模式,物料丰富后会有更大的提升空间。 数据源(召回) --> 模型(补全) --> 扩展逻辑(插件) 模型驱动解决方案在淘宝很好的解决了业务问题,但是面临更多的场景需要的是更加灵活的模板定制能力,因此未来模型驱动会在灵活的模板配置化上发力、对节点物料的沉淀上建立更加完善的机制、支持Web IDE等插件,并在更多的场景上支持业务的落地,让不同的业务场景可以更加便利的建立自己的“三板斧”。 考拉 Dart 一体化解决方案 考拉大前端自 2020 年 3 月份开始尝试 Flutter 的应用,部分客户端和前端同学均参与进 Flutter 的开发,对于 Dart 相对熟悉,所以 Dart 一体化解决方案最初目的主要是考虑帮客户端同学解决开发提效的问题。考拉之前主要在使 Node.js Runtime 的 Serverless 方案,相比于 Java Script,Dart 对于客户端同学也更友好一些,同时也不断有客户端同学提出 Dart Serverless 的诉求。 在函数计算 FC 研发团队的帮助下,考拉基于 Dart Runtime 的前期测试版本,快速完成了考拉 App 今日活动 Tab 的改造重构,并已于 9 月底灰度上线。10 月中下旬,基于 Dart Runtime 开始和 DEF 平台对接,最终 DEF Serverless 创建面板,会透出 Dart 纯函数解决方案,目前和 FC 侧基本流程已调通,即将上线 Dart 的纯函数解决方案。 除了已上线的 Dart Ast 生成服务,考拉将基于 Dart Serverless 方案推出更多的业务场景,如 App 端数据模型的动态下发、业务逻辑的动态配置、Flutter 动态化尝试,以及 App 跨端搭建能力等。 除了以上 3 个解决方案,ICBU 团队研发的 EaaS 微应用级别的解决方案,天猫行业团队研发的面向轻店场景的原生小程序一体化 解决方案等,这里不展开一一介绍了。 ▐ 函数稳定性保障 最开始的时候,我们关注的重点是如何用 Node 完成业务逻辑,比如数据怎么组织、 Java 二方包怎么调用、怎么结合阿拉丁链路、线上 bug 怎么快速修复。现在有了这么多线上运行的业务,我们关注的重点已经从怎么完成业务需求,转变成如何高效地、稳定地完成业务需求。 线上稳定性,本质上是对问题的治理。从问题出发,可以分为以下几个主要环节:预防问题、发现问题、定位问题和解决问题。 在预防问题上,要尽可能降低问题发生的概率和缩小影响面,做好上线卡口,以及做好对应的预案。发现问题上要尽可能实现全链路监控,以及实现合理有效的报警分发机制。定位问题上,要尽可能缩短问题的定位时间,在报警元信息的基础上,做一些机器的辅助分析,关联上下文,从而做到半自动定位或提供更多有逻辑的上下文,来缩短人为定位问题的时间。在解决问题上,要保证解决方案的有效,安全以及快速。 大促稳定性保障手段 大促场景下, C 端场景需要重保,以下的稳定性保障手段经历数次大促压测,同时越是大促态,整个稳定性保障也愈发紧张。 稳定性是保障了,但是在之前我们是对照上述的文档完成上线流程的,流程冗长无比,最终并沉淀成一个作战手册,同时这些内容无法和应用关联,离散在文档角落,整个过程「又臭又长」。 上线流程 -> 作战手册一体化 因此,Serverless 研发平台上希望规范化整个流程,从从 强弱依赖梳理 -> 预案配置 -> 监控报警订阅 -> 单链路压测 -> 作战手册生成,记录所有函数上线过程,流程可追溯,文档可沉淀;另外预案、压测、监控等流程做到半自动化,减少上线时间。我们将每个流程节点定义成一个 SOP 单元,这样根据业务特性可以进行 SOP 流程的随意组装。 发布SOP流程 通过半自动化流程生产的作战手册,函数和作战手册关联的硬盘化记录方式,并结合自动限流和下游依赖分析以及预案生产,例如:通过预发流量录制的回放,自动分析出函数下游的强弱依赖,并录入强依赖负责人,方便出现线上问题的时候可以第一时间找到负责人排查问题;根据不同租户对单元化的需求,平台可以帮助用户进行多机房、多单元部署实现异地多活。这些都能够让业务的大促态变得更轻松一些。 淘系业务作战手册 专家应急响应 为解决线上问题定位慢的痛点,平台还提供了应急响应系统,当函数成功率降低触发报警时,平台会自动拉取函数以及下游多项数据信息,进行错误分析,快速产出错误报告推送给函数开发者。并引导开发者回到研发平台进行切流、执行预案等止血操作。例如,下游服务强依赖服务A成功率下降,导致函数自身成功率下降,需要联系服务A负责同学。 ▐ 租户运维 平台上的每个租户都有对应的租户管理员,对各自租户的函数稳定性负责,包括租户下函数的单元化部署规则、大促管控、自建网关配置、容器额度、租户私有解决方案等,为此平台提供了一系列运维工具。 租户大盘 帮助管理员更好的观测到租户下函数的服务质量,和容器额度使用状况,提供函数错误率和 RT 黑榜,并且每周都会有治理周报推送给管理员,帮助其更好的进行运维其租户下的函数。 函数盘点 帮助管理员细致的观测每个函数线上运行的具体状态,包括函数线上存在的版本、容器数量、 Runtime 版本、灰度、单元部署状况,甚至可以观测到函数部署是否均衡。 大促管控 平台还提供针对大促态的运维管控能力,管理员可以将租户下参与大促的函数服务一键切换到大促态,进行大促态的额外配置,比如大促容量配置,Broker 侧限流,网关侧统一监控预案等能力,保障大促的稳定。 一些思考 Serverless 云研发平台后续将在提升用户正向和逆向流程的效率上继续演进,L1 是希望让用户低成本的上手,L2 是希望让用户低成本的进行研发,让前端往应用研发更进一步。 以下是基于用户正向研发链路耗时统计的一些分析: 技术方案产出的时间较久,占比整体研发周期 5%,核心原因是服务物料难以检索以及服务可用性难以评估,领域模型沉淀不足;FaaS 整体研发占比 25%~30% ;模型驱动等可视化编排在物料准备完备的情况下,能够提效,但是不具备规模化场景;联调耗时较久占整体成本 20% 左右,过度依赖预发环境,据统计,完成一个项目需要部署 50 次;压测成本依然存在,平台熟悉成本过高。 当然还有监控运维逆向链路的一些分析: 报警分发不准确,因现在无法区分报警是底层框架和上层业务的问题,所以往往需要架构组和业务同学的共同介入;定位问题效率低,如失败率报警,可能是底层架构的问题也有可能是下游的问题,还有可能是机房或者自身的问题,往往需要去多个平台逐一排查;缺乏对服务质量的统计或整体认知;缺乏能针对 80% 线上问题的排查和解决的标准化流程,依赖用户对问题的定位和解决能力。 最后 Serverless 云研发平台经过这半年多的蜕变,已经从简单的解决工程链路的平台演进成一个面向研发、上线、运维的全生命周期研发平台,后续要解决的命题会集中在用户低门槛上。 希望我们在 Serverless 上的实践和探索,能给业内其他公司带去一些启发,让路上的障碍变少,让应用的研发变轻。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
近几年,关注端智能方向的公司越来越多,一些头部公司在端智能上有了新的探索,并且取得了不错的效果,端智能逐渐成为驱动移动 App 业务创新的核⼼推动⼒之⼀。在推进端智能的过程中,会遇到哪些挑战?核心解决思路是什么? 近日,阿里巴巴淘系技术部资深技术专家吕承飞(花名:吕行)受 InfoQ 邀请,和大家聊一聊端智能在淘系的应用,以及双十一背后的那些技术挑战。并将于12 月 6-7 日的 QCon 全球软件开发大会(深圳站)“云端一体化移动开发”专题中进行《淘系端智能技术建设和业务创新》的分享,后续现场内容将通过淘系技术微信公众号整理发布。 嘉宾简介: 吕承飞(花名:吕行)阿里巴巴资深无线开发专家,在移动开发,超级 App 架构,端侧 AI 等方面有深入思考和实践经验。2011 年毕业加入百度,经历百度输入法 0 到 1 研发过程。2013 年加入淘系,经历手机淘宝超级 App 技术演化完整过程,主导淘宝 iOS 架构升级、架构治理、稳定性和性能等相关工作。2017 年开始端侧智能方向探索,构建开源端侧推理引擎 MNN,端计算框架 Walle,AR 技术框架和美妆 AR 等创新应用。 以下是采访实录: InfoQ:吕行老师您好,非常荣幸能够采访您。您从 2017 年开始探索端侧智能方向,这 3 年来,您认为端智能的发展有哪些变化? 吕行:宏观层面,端智能应用从探索尝试到逐步展开,在未来,必定会成为商业应用以及业务创新的核心技术推动力之一。具体来看,业界端智能的发展可以从以下三个角度来看: 从技术角度看,解决的问题是逐步递进的。从最初模型运行基础问题,再到效率和规模应用问题,具体包括:算法模型如何在端侧运行?算法模型如何快速迭代部署?如何降低端 AI 技术门槛实现普及应用? 从算法角度看,端侧算法不断成熟和完善。从最初的人脸检测,到人体姿态、手势、OCR 等逐步成熟。除视觉模型外,像搜索推荐深度模型、语音 ASR 模型和 NLP 模型在端侧运行也逐步变得可能,比如:我们今年基于 MNN 实现了移动端实时语音识别方案,并且在双 11 淘宝直播"一猜到底"活动中取得很好的业务效果。 从应用角度看,整体应用范围不断拓展和深入。从最初单点场景比如淘宝拍立淘场景,到多 App 和多场景全面铺开,不完全统计,阿里基于 MNN 的端智能应用已经超过 30 个。 InfoQ:淘系端智能的发展主要经历了哪几个阶段? 吕行:淘系端侧 AI 应用流程如上所示,每个节点都存在诸多问题,过去 3 年我们一直在解决中,主要经历了以下 3 个阶段: 端侧推理引擎阶段:端智能首先要解决算法模型在端侧运行问题,否则一切无从谈起,推理引擎就是端智能应用皇冠上的明珠,这个阶段我们做了端侧推理引擎 MNN,实现模型在端侧高效运行。 算法模型服务阶段:端智能要在业务落地除了算法模型运行之外,前后还涉及模型转换、更新发布、版本管理、运维监控等工作,这个阶段我们做了端 AI 服务端解决算法模型发布更新问题。特别地,算法任务除模型外,还涉及前后处理代码,因此我们构建了基于 PythonVM 的算法任务运行时容器,让算法同学编写 Python 任务实现快速迭代。 端 AI 研发范式阶段:端智能规模化应用过程中需要体系化解决研发迭代全链路问题。一方面,端智能应用落地需要算法开发和移动开发通力协作,但是两者之间天然存在 GAP,完全依赖口头沟通,协作效率存在较大问题;另一方面,AI 应用场景具有长尾和碎片化特征,诸多场景因为缺少专业算法支持导致没有落地,而且由于缺少统一技术建设导致已被应用的方案难以沉淀和复用;因此,我们构建『端 AI 研发范式』,具体由 MNN 工作台、MNN 运行时、端 AI 服务端构成。其核心思想:一是解耦算法和移动开发,让算法开发独立迭代;二是降低 AI 门槛,让 AI 成为普通开发的有力武器解决业务问题。我将在本次 QCon 会议分享相关内容细节。 InfoQ:淘系技术在推进端智能落地过程中,遇到过哪些困难,您认为最大的挑战是什么?最终是如何解决的? 吕行:淘系丰富的业务场景历来是培育创新技术的沃土,端智能的整体技术和应用实践一直走在行业前列,我们有开源推理引擎 MNN 还有开放的 MNN 工作台等。目前淘系已经有 25+ 应用场景,65+ 算法模型在日常运行,每天推理运行次数超过百亿次,覆盖商品搜索推荐、用户触达、拍立淘、直播等核心场景,经历 3 次双 11 考验并取得巨大业务价值。整体应用可以大致分成如下几类: 视觉类,主要在拍立淘、淘宝直播、拍摄工具、评价等场景应用。 推荐类,主要在首页信息流、购后、详情等各种推荐场景。 触达类,主要在 Push、消息、各业务弹框等场景应用。 语音类,主要在淘宝直播、智能降噪等场景应用。 到目前为止,最大挑战还是推理引擎 MNN 的挑战,比如: 移动端设备和系统碎片化; 移动端算力和资源有限; 视觉、语音等多样化的算法模型 …… 如何解决上述挑战,这里我就不细说了,我会在 QCon 深圳 2020 会议上重点分享其核心解决思路。 InfoQ:在刚刚过去的双 11 中,端智能在实际应用过程中有哪些突出表现?可以结合实际案例来展开聊聊吗? 吕行:端智能已经逐渐从尝试应用变成驱动业务创新的核心推动力之一,在双 11 的热点业务场景都能看到相关应用。今年大热的直播场景也有不少应用。依托于淘系自研的 MNN,淘宝直播间推出 “语音猜价格”挑战,观众在直播间也能实现语音交互,动动嘴就可以响应主播发出的猜产品价格的任务。端智能极大的提升了直播内容的交互可玩性和内容理解准确性。 基于端 AI 技术实现精准的用户感知能力,双 11 流量高峰阶段,充分发挥端侧的算力和数据优势,大幅提升主动触达用户的体验和效果,仅在 11 月 1 日当天,端侧 AI 决策运行了 277 亿次。 通过对用户行为的实时感知和意图识别做商品列表重排和智能刷新,在淘系信息流等场景大规模应用,DPV 和 GMV 都获得了较大提升。 InfoQ:可以简单聊聊 MNN 的下一步计划吗? 吕行:其实推理引擎 MNN 的本质是做这样一件事情,即实现 [不同种类模型] 在 [不同异构设备] 上 [最高效运行]。这里有三个关键点,我们持续在演进和探索。 支持不同种类模型,从支持 CV、Data 算法模型到支持 ASR、NLP 算法模型,最近 MNN 在控制流、动态图等方面都有了很多完善和升级,新支持 Transformer 等网络模型。 支持不同异构设备,从支持客户端 CPU ARMv7/64/v8.2 到 GPU OpenCL/Vulkan/Metal 等都不断在演进和完善,MNN 也开始支持服务端 Intel x86/NVIDIA GPU 推理,提供云 - 端一体的统一推理服务。针对每个异构设备都需要实现和优化所有 OP 导致开发成本过高问题,我们创新性提出几何计算架构方案,将 OP 数目收敛到约 20 个核心算子,做到低成本覆盖各个异构后端,MNN 应该是行业覆盖异构后端最多支持算子最全的推理引擎。 实现最高效运行,高性能一直是 MNN 核心优势之一,在行业也有广泛认同。具体优化思路包括离线的模型压缩、图融合等方式进行优化,在线的通过汇编、SIMD/ 并行化、矩阵算法、调度等方式进行优化。另外,MNN 与 PAI 合作实现训练、量化到 MNN 部署云端一体化方案,新增稀疏剪枝、Overflow-Aware 量化等压缩方案。 MNN 在上述三个方向会持续演进,但从整个端智能应用链路来看,MNN 只是解决算法模型在端侧高效运行的单点问题。目前,我们正从 MNN 单点技术往端智能技术体系化和产品化方向迈进,如前所述构建端 AI 研发范式,通过 MNN 工作台解决算法模型部署过程中的转换、优化、调试、发布等问题,甚至做到让算法开发独立迭代。MNN 工作台目前正在免费对外公测中,有兴趣同学可以访问我们官网 www.mnn.zone 下载体验。 InfoQ:您认为未来移动领域还有哪些值得关注的技术方向? 吕行:技术进展跟业务发展还是强相关的,随着直播业务快速发展,多媒体技术应该有比较多发展,我自己更多关注端智能相关的一些东西: AR+ 端 AI+3D 我觉得这几个技术结合可以做出很多有意思的应用,其中 AR 提供了虚实结合的场景能力,端侧 AI 提供 AR 中的交互能⼒,3D 模型 /AR 素材提供内容供给,5G 网络提供了大资源包的网络传输能⼒。目前这几个技术都不算成熟,比如难以实现低成本且高质量 3D 建模。另外,手机也不是 AR 应用最适合的载体,可以期待一下后续消费级的 AR 眼镜。 端云协同的智能 目前云端做训练,客户端做推理,端云结合还比较浅层。我们也在做端上训练的探索,以及构建一套分布式的端云协同智能系统,实现用户个性化理解,保护数据隐私,以及节省云端成本。
淘宝正在进行产品升级。 内测版淘宝主要有以下三方面变化,“微淘”升级为“订阅”,和“推荐”并列在首页展示;猜你喜欢进入首页第一屏,首页更加信息流化;最为重要的更改,毫无疑问是将此前淘宝内的买家秀、洋淘、问大家等内容板块聚合在“逛逛”内。 “逛逛”未来会拿走淘宝首页的一级入口,位于菜单栏的第二栏,成为淘宝的中心化内容平台。这足以窥见淘宝对逛逛的重视程度。当然“逛逛”也并非新人,而是此前淘宝内本身就很丰富的消费内容的整合。 阿里巴巴副总裁汤兴(花名:平畴)受邀接受Tech星球采访时表示:“逛逛是第一次我们在这个位置上放了一个中心化的内容场,而且至少在未来很长一段时间里,它是淘宝最重要的内容中心。” 淘宝不同功能入口和优先级的变化,背后其实是流量分配以及产品内部逻辑的调整。让作为消费内容平台的逛逛拿走重要入口,隐藏的是淘宝怎样的变化? 淘宝里散落各处的内容有了集中的机会 淘宝的内容化方向由来已久。 单是近3年内,淘宝就推出了包括淘宝直播、微淘、洋淘、淘宝短视频、淘宝头条等频道,淘宝的“逛”的心智也明显强于别的电商平台。但各类内容一直分散在产品各处。“(过去)我们没有中心化的内容场,现在非常明确地在各个场之间做了正确的切割。”平畴说。 阿里巴巴副总裁平畴 这种切割的基本逻辑是,手机淘宝首页被设置为“效率转化场”,更多是商品的客观介绍和商品说明书。无论是“推荐”还是“订阅”,看的都是进店转化、商品转化和权益领取带来的销售转化。 而逛逛承担的,则是让淘系生态内的商家、品牌、达人、普通用户分享自己的生活方式、价值主张,构建品牌力,建立自己对其他消费者的人设。 过去,淘宝上虽分散有买家秀、洋淘、微淘等内容板块,并不具备集中展示和积攒多样内容的平台。 以洋淘为例,洋淘前身“洋葱盒子”在2019年就已推出。作为内容社区产品,洋淘用频道的方式首次承载了一部分淘宝用户分享的诉求,做了买家秀的社区,但入口隐蔽,内容也相对分散。微淘中尽管也有种草信息,但与商家的促销、上新、活动等内容混杂在一起。 而脱胎于这些产品的逛逛,则整合了淘宝上分散四处的内容。体验后可发现,逛逛内,分为关注流及推荐流,为双信息流展示。目前可按照个人兴趣标签对内容进行基础选择。发布内容形式包括照片、视频。 逛逛是一个生活分享的社区,基于真实消费的商品评价,消费分享,个人生活方式的内容平台。而且这与其它内容平台差别很大。 从发展的逻辑上,像小红书等都是先有的内容分享,再接入的电商,由内容引出交易。淘宝则是先存在交易,才有的基于消费而来的内容,逛逛要做的,就是对平台中的优质交易评价内容和消费主张等去中心化的内容,做聚合和分发,从而再次促进交易。 淘宝产品负责人千城透露了一个数据:在淘系内,每个月有一亿多的用户做UGC内容的生产,形成数亿次的用户内容消费。淘宝消费者已经形成了很明显的圈层化,有2000个细分市场。 这意味着,淘宝生态内,本身就具备内容生产的能力。而在逛逛中,分散的UGC内容、细分的消费圈层内容会得到更集中的展示和分发。 在淘宝生态中,有包括商家、品牌、主播、用户等的生态角色。对这些角色来说,除了买卖需求外,建立个人影响力、品牌心智,个人人设的需求通常在淘宝之外的平台得到更多的满足。 不得不提的是,李佳琦就曾有过在淘宝直播内粉丝增长到达瓶颈的境遇。当时,他们的策略是:将直播中的内容二次剪辑在抖音等短视频平台进行内容传播,并持续输出系列高信息度、带有人格人设特点的内容。到淘宝站外寻找流量,突破圈层。 但逛逛的出现,就成了为主播、品牌、商家等角色提供在站内积攒粉丝,建立更丰富和个性化的人设、构建品牌影响力的渠道。逛逛和订阅的升级都给了更多积累粉丝的方式。 从10月开始面向商家、达人及部分用户内测,目前的界面并非最终形式。未来,直播、图文、视频内容,商品链接都可以在逛逛内进行一站式的打通,所关注用户一旦开始直播,就将会在关注流中置顶显示。同时,还将配备移动端的内容生产工具。基于不同角色,逛逛还开发了不同的内容号。 今天的淘宝是一个拥有8亿用户的产品,基于用户长期消费和浏览的习惯,淘宝已然是一个不可忽视的巨型流量池,大多数社交产品都无法企及。而淘宝,把首页第二栏的位置的关键流量位交给了逛逛。 不一样的内容 早在3年前,阿里巴巴集团 CEO 张勇就曾公开表示:社区化、内容化和本地生活化发展将是淘宝未来的三大方向。 这3年中,淘宝做了大量相关的尝试和更新。无论是千人千面的猜你喜欢,还是推出社区化产品,做大量内容方向的尝试,本质上,最终的目的都是为了把合适的产品卖给合适的人。 相比出于娱乐的目的,在包括抖音、快手、小红书等内容平台被种草好物,淘宝本身就是更交易导向的产品,大多数用户逛淘宝更多是出于购买的需求。这就意味着,在内容的种草力同等的情况下,在淘宝上,用户对会购买的行为有基础预设,消费链路就会短很多。 从7月开始筹备,到10月推出,对市场来说,逛逛还是一个崭新的品牌。内容平台做启动,通常需要专业内容生产者的加入和示范,逛逛从小红书、B站、抖音引入了不同的MCN机构,在原有的内容上进行扩充。 对于市场上已经成熟,有内容基础、商业能力的MCN机构来说,如何做粉丝积累和变现是关键的问题。 千城受采访时表示:逛逛为专业内容的从业者提供了很多产品上能力,比如快速入驻,直接查看内容运营的数据和链路,商业化上有接洽商单,智能对接平台里客户的能力,以及高佣商品池等都会开放给创作者。 在当下的消费内容中,绝大多数专业生产内容的达人、MCN通过用户互动数量,展现内容的内容力,商业模式是直接从品牌方处获得收益。 也就是说,当下的内容消费更多是一门to B的生意,拿商单再做内容,用点赞、评论和转发的数量衡量内容的优质程度。这种模式的一个问题是,转化很难具体量化,无论是直播还是短视频带货,几乎很少有人选择纯佣金的方式。 但基于淘宝的体系,佣金化的种草已经是一门成熟的生意,大量淘宝客靠着佣金过活。在微博、微信上,有大量在各自领域有影响力的人士通过纯佣金收益的方式对淘宝的商品进行种草。 逛逛还通过淘系多年积攒的供应链和品牌资源,为用户提供以纯佣金为导向的专属商品供应中心。这意味着,未来,创作者可以主动挑选内容,主动做内容,向别人安利商品,让内容变成一门to c的生意。 在淘宝的产品升级发布会上,淘宝还宣布,将启动“有光计划”,设立10亿奖励基金,帮助创作者更好成长。未来,逛逛会扶持1万名年收入过10万的个人创作者,以及1000位年收入过百万的机构创作者。 千城说,未来也会引入内容创作者的体系,不同类型的用户有不同的激励方式。比如普通用户晒买家秀,其他用户看了这个买家秀产生了购买,用户也会有平台的激励。 淘宝需要内容平台 长期来看,淘宝做逛逛为平台内的商家和消费者提供了一个拉近关系、建立信任的内容平台。无论是线下交易,还是基于电商平台,亦或是通过当下盛行的短视频、直播等进行种草交易,都需要基于基础信任的基础。 以直播为例,李佳琦、薇娅为首的头部主播,在直播时之所以上一个链接卖空一个链接,背后支撑庞大交易量的就是消费者对他们的信任——基于严选的商品供应链和相较市面上的同款商品更低的价格。 “无论是通过朋友圈的推荐,还是通过对主播、博主和达人的信任,而且它的产生的关系比原来商业的关系更加牢固,且效率更高。”平畴认为,信任一直是淘宝发展的内在逻辑。原来更多是通过商品,通过我们的平台的算法进行人货的匹配,通过平台背书来构建信任关系。而现在,社会经济发展过程当中,很多人的购买决策会转向人和人之间的信任。 QuestMobile电商粉丝经济洞察报告中提到,年轻用户,特别是90后、00后购物越来越看重商品以外的附加价值:商品背后的人格属性,购物过程中的娱乐性、互动性,在购物中通过明星/KOL引导,能够很好地满足这些需求。 实际上,这也是淘宝这次产品升级的一个重要出发点。“消费者对商品的信任关系的变化构成了整个交易的最后的起始点和本质:淘宝如何帮助商家建立跟消费者之间的信任,帮助消费者建立跟商品之间的信任”,平畴解释说。 淘宝继续升级内容的背后,看中的正是基于人的内容对于时下消费决策的影响力,以及内容平台对于一个人更为全面和多维度的人设塑造。正因此,升级之后,每一位用户都将拥有一个属于自己的个人主页,展示自己的消费习惯及主张。 逛逛不会直接露出关联商品链接,也不强制要求所有内容都附带商品。同时,关注按钮被隐藏很深,需要进入用户主页才能进行关注。 他们希望的是,用户看到单一内容产生关注念头时,可以在主页浏览更多内容后再做进一步的决策。逛逛的关注会促使交易,带来直接的收益,在这样的状况下,“只有出于信任的动机去关注,粉丝质量才是真正牢靠的”,平畴说。 一个有意思的细节是,现在,在逛逛上,普通用户发带链接的商品必须是一年之内购买的,促使保证评价的真实性,用淘宝上真实的消费行为作为背书。这也是淘宝逛逛有别于其它平台的特征,千城认为,这种真是消费带来的分享和评价,是淘宝的独特文化基因。 千城的发现是,各种数据链路表明,纯素人消费者产生的内容,不管是浏览转化还是成交转化的效率都并不输于专业的内容生产者所产生的内容。“因为素人有真实的交易,活生生的人产生的背书,因为有了信任,才带来了更好的效果。” 这两年中,内容行业面临根本性变化。平畴的感受是,消费决策正在慢慢往富媒体的方向上转移。 2016年,淘宝提出内容化方向时,用户消费习惯还是以电商平台上的评价、推荐顺序为主。这两年中,内容的兴起促使消费者的消费决策路径有了转变。通过图文、短视频等方式传播的社交口碑,社交电商、直播电商的兴起,消费者选品的路径在这个过程中不断发生变化。 在这四年的尝试中,他们得到了两个验证:富媒体的方式消费者可以接受;“内容种草类和营销类的内容放在一起,消费者是不work的”。 由此,淘宝迎来了一次升级,将逛逛成为淘宝消费内容的中心,首页“订阅”成为商家自运营阵地,推荐为品牌、商家提供曝光。作为重要的内容阵地,逛逛这本“内容书”,才刚刚起草了卷首语,正处于未完成、尚未释放能量的阶段。
淘宝的图片访问,有98%的流量都走了CDN缓存,只有2%会回源到源站,节省了大量的服务器资源。 但是,如果在用户访问高峰期,图片内容大批量发生变化,大量用户的访问就会穿透cdn,对源站造成巨大的压力。 今年双11,淘宝鹿班的主图价格表达升级项目,就面临了这种挑战,让我们看看是如何解决的吧。 CDN工作原理 内容分发网络(Content Delivery Network,简称CDN)是建立并覆盖在承载网之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。 CDN应用广泛,支持多种行业、多种场景内容加速,例如:图片小文件、大文件下载、视音频点播、直播流媒体、全站加速、安全加速。 借用阿里云官网的例子,来简单介绍CDN的工作原理。 假设通过CDN加速的域名为www.a.com,接入CDN网络,开始使用加速服务后,当终端用户(北京)发起HTTP请求时,处理流程如下: 1、当终端用户(北京)向www.a.com下的指定资源发起请求时,首先向LDNS(本地DNS)发起域名解析请求。 2、LDNS检查缓存中是否有www.a.com的IP地址记录。如果有,则直接返回给终端用户;如果没有,则向授权DNS查询。 3、当授权DNS解析www.a.com时,返回域名CNAME www.a.tbcdn.com对应IP地址。 4、域名解析请求发送至阿里云DNS调度系统,并为请求分配最佳节点IP地址。 5、LDNS获取DNS返回的解析IP地址。 6、用户获取解析IP地址。 7、用户向获取的IP地址发起对该资源的访问请求。 如果该IP地址对应的节点已缓存该资源,则会将数据直接返回给用户,例如,图中步骤7和8,请求结束。 如果该IP地址对应的节点未缓存该资源,则节点向源站发起对该资源的请求。获取资源后,结合用户自定义配置的缓存策略,将资源缓存至节点,例如,图中的北京节点,并返回给用户,请求结束。 从这个例子可以了解到: 1、CDN的加速资源是跟域名绑定的。 2、通过域名访问资源,首先是通过DNS分查找离用户最近的CDN节点(边缘服务器)的IP 3、通过IP访问实际资源时,如果CDN上并没有缓存资源,则会到源站请求资源,并缓存到CDN节点上,这样,用户下一次访问时,该CDN节点就会有对应资源的缓存了。 淘宝鹿班图片业务背景 商品的主图贯穿整个导购和交易链路,相比文字,图片更能吸引眼球,主图对消费者的购物决策有很大的影响。主图上表达的内容各式各样,但其中一定少不了的一定是价格的表达。 长期以来,主图上的价格表达都是商家自己维护,商品价格发生变化后,手动去换图。这样做,会带来3个问题: 1、价格的准确性:商家手动填写的图片价格,跟实际的购买价可能不一致,造成不好的用户体验。 2、价格更新的及时性:有时候,由于优惠券/品类券的生效失效,会导致商品的价格变化会很频繁,商家根本来不及换图。 3、商家的操作成本:手动修改图片的价格,成本还是很高的,需要通过ps等软件修改图片,重新上传,编辑商品。 今年双11,淘系技术部-鹿班团队,试图通过技术手段来解决这些问题。当商品价格发生变化后,系统自动计算新的价格,自动合成图片,然后更新商品主图。 我们知道,淘宝网有上亿的商品,光大促商品就有几千万,因此,价格变化导致的图片变化频率非常高。最高的就是在双11的0点,全部大促商品的价格都会由日常价格变成大促价格。 这就意味着,大促高峰期,有上千万的图片刚生成就会被用户访问。那这个情况会产生什么问题呢,让我们先了解下淘宝的图片空间和CDN的架构,就清楚了。 淘宝图片空间和CDN的架构 淘宝整个图片的访问链路有三级缓存(客户端本地、CDN L1、CDN L2),所有图片都持久化的存储到OSS中。真正处理图片的是img-picasso系统,它的功能比较复杂,包括从OSS读取文件,对图片尺寸进行缩放,编解码,所以机器成本比较高。 CDN的缓存分成2级,合理的分配L1和L2的比例,一方面,可以通过一致性hash的手段,在同等资源的情况下,缓存更多内容,提升整体缓存命中率;另一方面,可以平衡计算和IO,充分利用不同配置的机器的能力。 用户访问图片的过程如下: 用户通过手机淘宝来搜索商品或者查看宝贝详情。 详情/搜索/推荐通过调用商品中心返回商品的图片URL。 客户端本地如果有该图片的缓存,则直接渲染图片,否则执行下一步。 从CDN L1回源图片,如果L1有该图片的缓存,则客户端渲染图片,同时缓存到本地,如果L1没有缓存,则执行下一步。 从CDN L2回源图片,如果L2有该图片的缓存,则客户端渲染图片,同时CDN L1及客户端缓存图片内容,如果CDN L2没有缓存该图片,则执行下一步。 从图片空间回源图片,图片空间会从OSS拉取图片源文件,按要求进行尺寸缩放,然后执行编解码,返回客户端能够支持的图片内容,之后客户端就可以渲染图片,同时CDN的L1、L2以及客户端都会缓存图片内容。 频繁换图带来的技术挑战 当商品的价格发生变化时,我们会使用新的价格重新合成图片,更新商品中心中存储的图片URL。这样会带来2个问题: 1、CDN及手机淘宝原本缓存的图片内容失效了,用户访问图片会全部回源到img-picasso。 2、由于更改了商品的字段,交易的核心应用(购物车和商品中心)的缓存也失效了,用户浏览及购物时,对商品的访问会走到db。 源站img-picasso处理图片,以及查询商品DB,都是非常消耗资源的。CDN及商品的缓存命中率降低后,对源站img-picsasso以及db会产生巨大的压力。 拿CDN缓存为例,简单计算一下,CDN平时的命中率是98%,假设命中率降低1个点,对源站的压力就会增加1/3(原本承担2%的流量,现在需要承担3%的流量),意味着img-picasso需要扩容1/3。如果全网一半的图片都同时变化,cdn的命中率降到50%,对img-picasso的访问量就会增加25倍,这个扩容成本肯定没法接受。 解决这2个问题,对应的有2个办法: 1、改图保持图片URL不变,可以避免商品链路的缓存失效。 2、在访问高峰到来之前,提前预热图片到CDN,可以避免CDN缓存失效对源站的压力。 下面,介绍下我们具体是怎么做到这2点的。 频繁换图的应对方案 ▐ 改图保持图片URL不变 图片内容发生变化时,执行下面2个操作: 1、更新OSS内容:使用新的图片内容替换OSS中老的图片内容 2、刷新CDN缓存:清除CDN之前缓存的图片内容 这样,用户再次访问图片时,发现CDN没有缓存,就会回源到img-picasso,从OSS拉取新的图片内容。 由于图片URL没有变化,就不必去更新商品中心的图片链接,这样商品链路的缓存可以保持不变。 在真正实施这个方案的过程中,遇到了几个问题,简单跟大家分享下: OSS三地同步 淘宝的图片空间,承载了淘系所有图片的上下行稳定性保障,为了保障高可用,一份资源会存储到三地OSS。图片上传时,默认只上传一地,利用OSS的能力,自动同步到另外两地。 但是使用URL不变方案,CDN缓存已经清除完成后,如果另外2地的OSS还未同步完成,用户访问后,就会回源到旧的图片内容,发现图片内容没有变化。 针对该问题,我们将异步同步OSS软链的模式,改成三地同步建软链,三地都返回成功后,再去清除CDN缓存,这就保证了用户访问的图片一定是最新的内容。 图片尺寸收敛 同一张商品图片会用于不同的场景坑位展现,不同的坑位对图片的尺寸有不同的要求。为此,图片空间提供了一项功能,可以方便的生成不同尺寸的缩率图。只需要访问图片时,给图片增加不同的后缀,img-picasso源站就可以按要求进行图片进行缩放。 由于历史原因,之前对缩放的尺寸种类没有限制,导致CDN上的图片后缀格式多达2400种+,TOP6格式覆盖率46%,TOP15格式覆盖率64%。这意味着,一张图片,在cdn上最多可能有2400+个不同的url,当图片内容变化后,要把这些缓存全部清掉,才能保证所有用户看到的图片都是新内容。 为了解决这个问题,我们对域名格式进行了收敛。 图片空间对于图片质量压缩参数的规则如下: 图片质量参数常见有一下8种形式:Q90、Q75、Q50、Q30、q90、q75、q50、q30 图片锐化参数常见有一下3种形式:s100,s150,s200 我们重新将图片质量定义为高质量图片和低质量图片,收敛格式为 q90 和 p50s150 这样,就可以把2000多种格式收敛到6种主要格式,CDN清除缓存才变得可行。 多副本清除CDN缓存 通过图片尺寸收敛,每张图片只需要清除6个不同的url就可以了,那能不能进一步提升刷新效率呢? 为此,阿里云CDN为我们提供了多副本刷新的解决方案:每种不同后缀的图片,作为图片的一个副本,在CDN的swift层增加一层KV结构,存储url和不同副本的映射关系,清除缓存时,可以通过该结构找到所有副本,实现快速清除所有副本。这样,每张图片,我们只需要调用一次CDN清除缓存接口就可以了,极大提升了CDN缓存刷新效率。 图片域名收敛 淘系的图片域名有300多种,主要有下面2个原因: 1、图片完整的链接太长,所以存储时经常只存最后一段,业务自己来拼域名,很多业务就自己申请了一个图片域名来拼。 2、PC时代,浏览器对同一域名下的并发请求数是有限制的,不同浏览器不一样,一般6个左右。为了突破该限制,一些业务就会申请多个域名,随机的拼不同的域名。 前面我们讲过,CDN的缓存是跟域名绑定的,不管是缓存命中还是缓存清除,都只能针对一个域名。 我们显然不可能改一张图,就去对300个域名调用CDN刷新。于是我们考虑对图片域名进行收敛,使得用户对图片的访问都路由到同一个域名,我们希望将所有的图片访问统一收敛到picasso.alicdn.com,具体实现方式如下: 1、对于手淘和猫客客户端,图片访问都收口在图片库,我们推进图片库进行改造,符合一定规则的url,统一收敛到picasso.alicdn.com,实现了域名的一刀切。 2、对于PC浏览器端,就比较麻烦了,没有统一收口的地方。我们只能退而求其次,针对访问最多的6大域名,在cdn上配置域名转发规则,重定向到picasso域名。 通过这种方式,我们实现了全网99%以上的图片访问流量都路由到picasso域名,图片内容发生变化时,通过清除picasso域名的cdn缓存,就能保证基本所有的场景都能看到新的图片内容。 客户端及浏览器缓存 通过多副本和图片域名收敛,cdn的缓存问题得到了解决。但在cdn之上,用户图片访问首先是来自客户端或者浏览器,这里也会有一层缓存。 大家知道,浏览器的缓存都遵循标准的http max-age协议,指定该header后,到了时间图片就会失效,访问到新的图片。所以我们可以在源站img-picasso回源给cdn时,添加max-age协议头,值为1分钟,cdn会原封不动的透给浏览器,这样浏览器就可以实现1分钟内图片缓存失效,重新到cdn拉新的图片资源。 对于手机淘宝客户端,我们在原有的LRU缓存机制之上,另外支持标准的http协议。这样,手机淘宝也实现了1分钟内图片缓存失效。 ▐ 提前预热CDN图片 通过改图保持图片URL不变,我们解决了改图对商品链路缓存的影响。但是,图片变化时,虽然URL没有变,但我们清除了CDN缓存,导致用户访问时还是会回源到img-picasso源站,所以对图片源站的压力依然存在。 我们发现,商品的价格变化大部分发生在大促节奏变化的时刻,基于这个特点,我们通过提前合成图片,提前预热到CDN,可以实现图片切换瞬间生效,同时对源站没有压力。具体方案如下: 提前合成多波段图片:我们知道大促期间商家集中换图的时间点后,按这些时间点把图片的展示分成多个波段,每个波段图片提前合成,并提前将图片URL写入到商品中心扩展结构中。 图片访问路由:营销系统根据配置的大促氛围切换计划,告诉鹿班图片二方包,当前是哪个波段,鹿班根据当前波段及场景,返回正确的图片URL给各个场景。 图片渲染:各个场景拿到图片URL后,结合自身的业务逻辑,决定是否要展现该图片。 CDN图片预热:为了避免图片集中切换时,把源站击垮,我们会在集中切换前把这些冷图片内容预热到CDN。 波段内图片变化:提前合成各个波段图片后,商家可能会临时发券/改价,导致商品价格再次变化,对于这类换图需求,为了避免更新商品中心的图片URL,我们通过本文上一章节刷CDN缓存的方式实现。 总结和展望 CDN技术广泛应用于互联网的各个场景,如今的CDN服务商,都提供了非常简单的业务接入方式,而且CDN的费用每年都在降低,这一切使得CDN的接入和使用成本越来越低。 本文通过淘宝图片业务的例子,为大家阐述了使用CDN过程中可能遇到的问题和解决思路。 淘宝的图片业务除了访问量大,还会面临更新频繁的问题。图片的频繁更新,一方面会由于商品上的图片url变化,导致商品缓存失效,另一方面会大幅降低CDN的图片访问缓存命中率。 针对图片url变化导致商品缓存失效的问题,我们通过刷新cdn缓存,用户访问时重新回源的方式,实现了改图保持图片url不变,这个过程中了,我们解决了一些列的问题,包括:OSS三地同步更新、图片尺寸收敛、图片域名收敛、客户端及浏览器本地缓存。 针对改图降低CDN图片缓存命中率的问题,我们根据业务的特点,提前合成不同波段的图片,并预热到CDN,保障了源站的安全。 目前,淘宝上用户看到的图片,都是提前合成好的。未来,我们考虑在用户访问图片时,实时合成图片。通过这项技术,可以实时感知业务更多的实时信息,可以根据这些信息,在图片上合成当前用户或者环境更匹配的文案/元素等内容,给用户带来更多的惊喜。 当然,实时合图也会面临更多的挑战,如:计算能力、合图性能。此外,对于CDN而言,由于每次用户访问的内容是临时合成的,CDN的缓存策略也是一个很大的挑战。 淘系技术部-鹿班团队-诚招英才 技术来驱动业务!!!淘系技术部鹿班团队,长期聚焦在图片及视频领域,通过技术创新,提升商家的经营效率及用户的体验,如果你对图片或者视频技术感兴趣,或者希望接触到高并发的工程系统,希望通过code改变世界,欢迎加入我们!!!zhaoming.ywt@taobao.com 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
本文作者:刘博文(Berwin),花名“玖五”,畅销书《深入浅出Vue.js》作者、知名技术博主、讲师、阿里巴巴淘系技术部前端技术专家,现负责淘系618、双11等超大型营销活动主会场的终端渲染架构。 回想起年初刚来杭州那会,是疫情正严重的时候,那时候刚来杭州要住半个月的酒店,然后才能进入阿里巴巴溪西园区(后续简称”园区”),时间过得飞快,一晃已经来杭州半年了,这半年经历了很多,也学到了很多,写一篇文章总结下这半年来自己的成长。 勇于挑战权威 要勇于挑战权威,发现现有技术体系的问题,并解决它。 记得当时刚来杭州时,心情是非常忐忑的,对未来非常憧憬,能和那么多很厉害的工程师一起工作是一件特别爽的事,再加上我们团队是做双十一大促会场的,技术人都知道双十一对工程师来说意味着什么。入职后,一大堆技术名词和各种技术体系铺面而来也确实让我感受到了技术的强大,所以就一直以学习的心态在了解和接触现有的技术体系。 进入园区后第二个月就开启了618战役,感谢主管墨冥的信任,当时我承担了一个非常重要的专项PM(Project Manager),它在整个618战役里都算是风险和挑战都非常高的专项,也是因为这件事干的还不错,上线后非常稳定,因此我获得了618战役奖励优秀PM的一个“厉害了Work哥 - 此时此刻非我莫属”奖,奖状在淘宝楼里挂了三四个月,入职不到3个月就获奖应该算是比较值得自豪的事了。 说这个当然不只是为了显摆,就在我以为自己表现非常好的时候到了转正面试的时间,虽然通过了试用期,但得到的反馈是对我的表现没有超出预期,我的执行力虽然很强,但我“没有对现有技术体系带来变化”。 换句话说,招我来的目的不是来当资源的,618战役虽然打的不错,但说实话换个人上去又能差到哪里去?大家对我的期望是对现有比较成熟的体系带来变革。 那怎么对现有体系带来变革?经过大家的引导和我自己的思考,答案是:”发现现有体系的问题“。我刚来觉得这里技术体系特别牛,加上沉淀了这么多年的双十一,已经是比较成熟的技术,觉得这是一个权威,不可能有问题,所以一直抱着膜拜的想法在了解和学习现有体系,所以这就是问题所在。 这时候我学会的最深刻的一个成长是:“要勇于挑战权威,发现现有技术体系的问题,并解决它。” 身为PM如何推进事情 感谢主管的培养和信任,给了我很多试错空间,入职到现在这半年时间从第一次当PM到现在,犯了很多错误,在一次次犯错后也学习到了很多当Project Manager的知识,本小节将这些成长总结起来分享给大家。 ▐ 解决合作阻碍 在推进项目时,有时会遇到一些阻碍,例如:不配合,难合作,主动性差等问题。绝大部分能来到阿里工作的同学不会是“能力”或“态度”问题,所以大部分阻碍可以归结于: 1、信息不一致 2、目标不一致 3、优先级不一致 解决障碍,就是在解决以上这三点,信息不一致可以靠“沟通”和“换位思考”解决,而“目标不一致”和“优先级不一致”可以靠“上升”解决,只要事情足够重要,给出几种解决方案,上升到某个级别问题一定会被按照最合理的方案解决。 ▐ 建立自己的权威 入职大概三个月左右的时候,我发现PD(产品经理,在阿里巴巴我们称为 Product Design,即:“产品设计”)在与我合作的过程中对我是有点不信任的,例如:相同的答案PD会相信我师兄和主管、我给了方案后PD会再去找我主管询问是否有更好的方案等。这也让我有了很多成长,当时我还特地去向二级主管请教一些方法。 拉人佐证自己的判断 案例一:在没有建立起自己的权威前,很多时候PD并不相信我给出的方案。那么这种情况解决方案是:假设有3种方案,将各自的利弊信息同步给PD后,如果都不满意,可以拉其他人,例如拉自己的主管进来,佐证自己的判断。(但前提是自己要做好功课,先和主管沟通好达成一致,再将信息同步给PD)。 案例二:假设现有两种方案各有利弊,完美的方案需要团队A配合,那么可以拉着自己的PD去和团队A的PD谈(前提是要先和自己的PD达成一致,并且以自己为主导拉PD只是来佐证自己的判断),一方面向团队A的PD佐证自己提的需求是重要的,另一方面也可以向自己的PD佐证这个需求确实没那么简单。如果事情推进遇到阻塞,回到“解决合作阻碍”小节,最终问题一定会被解决。 拉主管进来是为了佐证自己的判断,而不是和PD一起把复杂问题抛出去。 拉主管佐证要先沟通好达成一致,不是突然把主管拉进来佐证自己,避免主管的判断和自己不一致当场被打脸 资源不足而PD说我都要怎么办? 先和主管达成一致,客观评估需求是否真的重要到需要拉其他同学来帮忙开发,在一个更宏观的角度评估哪个需求优先级更高,然后再给PD同步结论,如果不接受拉主管进来还是相同的结论,再不接受再拉主管的主管也是相同的结论。权威就是在日常这样一点点建立起来的。 与PD合作的艺术 回到最初的问题,PD为什么要找我主管寻求帮助? 1、她想把事情更好地推进下去 2、她觉得找主管会有更令她满意的方案 本质上是我还没有建立起自己的权威,另外PD是信息弱势方,所以如果我给出的方案她不满意时,她会去找她更信任的人寻求帮助看是否有更好的方案,那么如果这时真得到了一个更满意的方案,那么她会觉得这招管用下回还会再去,这时候我的权威就会崩塌。 解决方案是:提前做功课给到PD的信息永远是权威的判断,在不信任自己时拉人佐证自己的判断是对的,或者和PD一起去找其他人继续推进&解决问题,逐渐让PD意识到即便是找了其他更信任的人也会得到相同的结论,我的结论就是正确且权威的。 积累自己的信用 作为新同学,可能会发现同一件事,同一个解法,PD经常会不信任新同学。 这里日常工作本质上是累积信用和消费信用的过程,解决方案是:在日常工作中一点点累积自己的信用,当机会来临要勇于消费信用去推动事情,这也是体现“此时此刻,非我莫属”的阿里巴巴价值观。 当然,打了败仗,信用也会消耗,经常打败仗即便不是新同学也很难让人信任,所以还是要靠自己的本事来积累信用并建立权威。 ▐ 项目风险同步 这半年时间,关于风险同步我犯过两次错误,这两次错误也分别让我学到了两种关于项目风险的经验。 **风险同步:Case 1** 事情发生在今年的88大促,当时我为会场底层渲染架构全新升级了一版,在一个极短的时间用新开发的2.0追上了正在运行的1.0的大部分功能,然后在88大促切换到2.0,可以类比在天上给飞机换引擎。切换2.0后如预料中的一样,出了一些小问题。 问题在于,给飞机换引擎这个动作和行为,我没有通知给业务方,导致出了问题的时候,业务方很惊讶,为什么之前一直好好的这次突然出了这么多问题?业务方对这个事没有任何预期。 在这件事上,我学到的是:在做一件事时,要通知到所有可能会因为这件事而受到影响的人,把自己的计划,方案,风险等信息完全同步给可能会受到影响的人,好处是: 人多力量大,如果方案确实不成熟,有漏洞大家可以一起完善提高稳定性 大家都知道这件事,而且计划、方案、风险、预案都得到了大家的认可,即使真的出问题也不会给大家“惊喜” 风险同步:Case 2 事情的背景是,有一次我负责一件事,这件事在过程中我发现进度有延迟的风险,但当时我选择了自己抗下来,加加班,赶赶进度。后面我低估了这件事的严重性,导致最后实在扛不住了才暴露风险,紧急加人解决了这件事,虽然这件事没引起问题,但是这个最后临门一脚才暴露风险这个行为是不对的。 通过这件事,我也学会了如何做事是对的(感谢主管孜孜不倦的教导),暴露风险不是懦弱和能力不行的体现,暴露风险是一个PM的专业素质,不要自己硬扛风险和压力,过程中有风险需要帮助应及时提,避免到最后扛不住才将风险暴露出来。 ▐ 误区:不敢上升和暴露风险 新人入职都会进入一个误区: 1、为什么我负责的项目,大家都在争论不休,是不是我能力不行? 2、为什么我负责的项目,好多事我都解决不了需要靠更高级别的同学来拍板,是不是我能力不行? 3、为什么我负责的项目,又又又又有风险了,是不是我能力不行? 现在我可以很明确的告诉大家,不是,完全不是! 推进项目受阻有很多原因,绝大部分都不是自己这个位置能解决的,上升是非常高效的解决方案。 项目遇到风险也是同样的道理,除了确实是自己能力导致的风险以外,绝大部分风险都是客观存在的事实,和自己能力没关系,即时且充分暴露风险寻求资源解决风险才是王道,这反而是一名专业的PM应该具备的基本素质。 PM的职业素养 PM的目标只有一个:“确保项目按时保质上线”,但过程也同样重要,阿里巴巴有句土话我很喜欢:没有过程的结果是“垃圾”,没有结果的过程是“放屁”。 在推进项目的过程中,一名合格的PM需要具备的基本素养是: 拥有Owner的心态 做关键技术决策 充分暴露风险 调动能调动的一切力量 内心时刻铭记一句团队内广为流传的名言:所有关于事的困难可以靠坚持解决,所有关于人的困难可以靠换位思考解决。 ▐ 关于情绪控制 项目复杂且生产关系也复杂的时候,会遇到各种困难和阻塞。沟通工作时很容易情绪失控,但身为一名合格的PM,任何时候,不应该被情绪控制自己的判断。 任何时候,都应该基于客观事实理性分析问题,不应该带有主观的执念。 这也是未来我要加强训练的一点,过去这半年,我经常情绪失控,未来我会克服这一点,做一名专业的Project Manager。 ▐ 关于方案评估 评估某个方案是否可行,不应该只是评估技术上是否可行,还要考虑按照这个方案推进后,会带来怎样的影响。 关于这点我曾犯过一次错,在技术方案的评审上,我只判断了技术的可行性就同意了某个方案,但是我没有考虑按照这个解决方案推进后会带来很大的其他影响。后面我师兄及时制止了这件事的发生,但是已经答应了PD按照某个方案推进结果又反悔,对于我这种新人甚至是我们团队在PD心中,都是非常消耗信用的一件事,“积累信用可能需要好久,但消耗信用,仅仅只是一次无意间的失误”,要珍惜自己的信用,“信用”,才是PM推进事情时的通行证。 ▐ 避免无意中踢皮球的情况 客户(运营、产品、其他人)有疑问来向自己咨询的时候,即便不是自己负责的域,也不要直接和客户说你去找谁谁谁。正确的做法是先把问题揽下来,然后团队内部找对应的同学拉个群解决。 我就曾遇到过这种被踢皮球的情况,我有疑问找了A,A说让我找B,B让我找C,C说他不负责这事,然后我直接找了他们共同的主管,问题解决。 我知道他们不是故意踢皮球,但在我找不到他们1号位是谁的时候,用这种方式对我来说是最快速解决问题的方案。这首先对于客户的体感不好,另一个是我向他们共同的主管寻求帮助后,他们的体感也不好。所以,要有owner心态,“让业务方幸福,让主管信任”。 前端工程师的职业素养 前面说了很多关于PM我犯的错和收获到的成长,那么作为一名前端工程师,这半年也收获了一些成长。 ▐ 做一名有“标签”的人 要做一名有标签,有特色,有影响力的人,在公司工作了一段时间后,在人们心中不应该只是一名“前端工程师”,应该是一名XXX的前端工程师。 标签应该自己去努力获取,有两个标签是我现在在努力获取的:“双十一前端PM” 和 “不到30岁的P8”。 ▐ 让事情因为自己而与众不同 一个灵魂拷问:今天我负责的事,我做完和其他人做完有什么不一样?今天我作为Project Manager负责某个项目,如果换做其他人来,会有什么区别? 今天获奖也好,得到一个好绩效也好,真的是因为自己做得好,还是因为主管把自己放在这个位置得到了更多的资源所以做得好?如果把其他人放到这个位置,和自己会有什么区别?这是一个值得思考的问题。 一个特别大的误区是:认为自己技术好,所以比其他人做的好。今天能进阿里的同学技术上都不会差,再加上大部分工作都不是去造火箭,所以 “技术好 !== 拿到好结果”,技术好会增加拿到好结果的概率,但不是一定能拿到好结果。 所以,要让自己负责的项目,因为PM是自己,而变得不一样。要让自己的团队,因为自己的存在,而变得不一样。 ▐ 学会换位思考 换位到更高维度思考,很多时候不理解的事就理解了。换位到合作伙伴的维度思考,就理解他为什么会不配合,难合作,主动性差。换位到客户的角度思考,就理解她为什么会不信任自己。 还是那句话:所有关于事的困难可以靠坚持解决,所有关于人的困难可以靠换位思考解决。 ▐ 沟通的艺术 这是有一次和主管聊天中学到的,和人沟通,一定要学会聆听,解决冲突或问题时,第一步是“聆听”,先聆听,充分了解信息后,再基于客观事实把事摊开了,并基于客观事实讲述正确的做法是什么,然后再去指点哪些地方可能不足。 如果不聆听就做判断,试想在没有得到足够信息输入时就做判断,判断真的客观么?还是自己主观上有倾向?就算自己对情况完全了解,不需要输入也可以做判断,那信息的输出方会不会认为自己做的判断不够客观?因为自己都没有听他说的是什么就做判断,他一定会质疑自己的判断是否公正。 总结 半年来,成长远不止这些,还是感谢舒文把我带到这个团队,赐予机遇和指导。感谢主管墨冥这半年来不断地言传身教并给予机会试错,相信未来,我会在实战中承担更大的职责,相信未来,我会让我们团队因为我的存在变得不一样。 更多: 玖五Twitter:https://twitter.com/jiuwu_lbw玖五博客:https://github.com/berwin/Blog/issues玖五知乎:https://www.zhihu.com/people/berwin-95/ 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
本篇为设计模式第二篇,第一篇可见设计模式最佳套路 —— 愉快地使用策略模式 管道模式(Pipeline Pattern) 是责任链模式(Chain of Responsibility Pattern)的常用变体之一。在管道模式中,管道扮演着流水线的角色,将数据传递到一个加工处理序列中,数据在每个步骤中被加工处理后,传递到下一个步骤进行加工处理,直到全部步骤处理完毕。 PS:纯的责任链模式在链上只会有一个处理器用于处理数据,而管道模式上多个处理器都会处理数据。 何时使用管道模式 任务代码较为复杂,需要拆分为多个子步骤时,尤其是后续可能在任意位置添加新的子步骤、删除旧的子步骤、交换子步骤顺序,可以考虑使用管道模式。 愉快地使用管道模式 背景回放 最开始做模型平台的时候,创建模型实例的功能,包括:“输入数据校验 -> 根据输入创建模型实例 -> 保存模型实例到相关 DB 表”总共三个步骤,也不算复杂,所以当时的代码大概是这样的: public class ModelServiceImpl implements ModelService { /** * 提交模型(构建模型实例) */ public CommonReponse<Long> buildModelInstance(InstanceBuildRequest request) { // 输入数据校验 validateInput(request); // 根据输入创建模型实例 ModelInstance instance = createModelInstance(request); // 保存实例到相关 DB 表 saveInstance(instance); } } 然而没有过多久,我们发现表单输入数据的格式并不完全符合模型的输入要求,于是我们要加入 “表单数据的预处理”。这功能还没动手呢,又有业务方提出自己也存在需要对数据进行处理的情况(比如根据商家的表单输入,生成一些其他业务数据作为模型输入)。 所以在 “输入数据校验” 之后,还需要加入 “表单输入输出预处理” 和 “业务方自定义数据处理(可选)”。这个时候我就面临一个选择:是否继续通过在 buildModelInstance 中加入新的方法来实现这些新的处理步骤?好处就是可以当下偷懒,但是坏处呢: 1、ModelService 应该只用来接收 HSF 请求,而不应该承载业务逻辑,如果将 提交模型 的逻辑都写在这个类当中,违反了 单一职责,而且后面会导致 类代码爆炸 2、将来每加入一个新的处理步骤或者删除某个步骤,我就要修改 buildModelInstance 这个本应该非常内聚的方法,违反了 开闭原则 所以,为了不给以后的自己挖坑,我觉得要思考一个万全的方案。这个时候,我小脑袋花开始飞转,突然闪过了 Netty 中的 ChannelPipeline —— 对哦,管道模式,不就正是我需要的嘛! 管道模式的实现方式也是多种多样,接下来基于前面的背景,我分享一下我目前基于 Spring 实现管道模式的 “最佳套路”(如果你有更好的套路,欢迎赐教,一起讨论哦)。 定义管道处理的上下文 /** * 传递到管道的上下文 */ @Getter @Setter public class PipelineContext { /** * 处理开始时间 */ private LocalDateTime startTime; /** * 处理结束时间 */ private LocalDateTime endTime; /** * 获取数据名称 */ public String getName() { return this.getClass().getSimpleName(); } } 定义上下文处理器 /** * 管道中的上下文处理器 */ public interface ContextHandler<T extends PipelineContext> { /** * 处理输入的上下文数据 * * @param context 处理时的上下文数据 * @return 返回 true 则表示由下一个 ContextHandler 继续处理,返回 false 则表示处理结束 */ boolean handle(T context); } 为了方便说明,我们现在先定义出最早版 【提交模型逻辑】 的上下文和相关处理器: /** * 模型实例构建的上下文 */ @Getter @Setter public class InstanceBuildContext extends PipelineContext { /** * 模型 id */ private Long modelId; /** * 用户 id */ private long userId; /** * 表单输入 */ private Map<String, Object> formInput; /** * 保存模型实例完成后,记录下 id */ private Long instanceId; /** * 模型创建出错时的错误信息 */ private String errorMsg; // 其他参数 @Override public String getName() { return "模型实例构建上下文"; } } 处理器 - 输入数据校验: @Component public class InputDataPreChecker implements ContextHandler<InstanceBuildContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(InstanceBuildContext context) { logger.info("--输入数据校验--"); Map<String, Object> formInput = context.getFormInput(); if (MapUtils.isEmpty(formInput)) { context.setErrorMsg("表单输入数据不能为空"); return false; } String instanceName = (String) formInput.get("instanceName"); if (StringUtils.isBlank(instanceName)) { context.setErrorMsg("表单输入数据必须包含实例名称"); return false; } return true; } } 处理器 - 根据输入创建模型实例: @Component public class ModelInstanceCreator implements ContextHandler<InstanceBuildContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(InstanceBuildContext context) { logger.info("--根据输入数据创建模型实例--"); // 假装创建模型实例 return true; } } 处理器 - 保存模型实例到相关DB表: @Component public class ModelInstanceSaver implements ContextHandler<InstanceBuildContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(InstanceBuildContext context) { logger.info("--保存模型实例到相关DB表--"); // 假装保存模型实例 return true; } } 到这里,有个问题就出现了:应该使用什么样的方式,将同一种 Context 的 ContextHandler 串联为管道呢?思考一下: 1、给 ContextHandler 加一个 setNext 方法,每个实现类必须指定其下一个处理器。缺点也很明显,如果在当前管道中间加入一个新的 ContextHandler,那么要势必要修改前一个 ContextHandler 的 setNext 方法;另外,代码是写给人阅读的,这样做没法一眼就直观的知道整个管道的处理链路,还要进入到每个相关的 ContextHandler 中去查看才知道。 2、给 ContextHandler 加上 @Order 注解,根据 @Order 中给定的数字来确定每个 ContextHandler 的序列,一开始时每个数字间隔的可以大些(比如 10、20、30),后续加入新的 ContextHandler 时,可以指定数字为 (11、21、31)这种,那么可以避免上面方案中要修改代码的问题,但是仍然无法避免要进入每个相关的 ContextHandler 中去查看才能知道管道处理链路的问题。 3、提前写好一份路由表,指定好 ”Context -> 管道“ 的映射(管道用 List 来表示),以及管道中处理器的顺序 。Spring 来根据这份路由表,在启动时就构建好一个 Map,Map 的键为 Context 的类型,值为 管道(即 List)。这样的话,如果想知道每个管道的处理链路,直接看这份路由表就行,一目了然。缺点嘛,就是每次加入新的 ContextHandler 时,这份路由表也需要在对应管道上进行小改动 —— 但是如果能让阅读代码更清晰,我觉得这样的修改是值得的、可接受的~ 构建管道路由表 基于 Spring 的 Java Bean 配置,我们可以很方便的构建管道的路由表: /** * 管道路由的配置 */ @Configuration public class PipelineRouteConfig implements ApplicationContextAware { /** * 数据类型->管道中处理器类型列表 的路由 */ private static final Map<Class<? extends PipelineContext>, List<Class<? extends ContextHandler<? extends PipelineContext>>>> PIPELINE_ROUTE_MAP = new HashMap<>(4); /* * 在这里配置各种上下文类型对应的处理管道:键为上下文类型,值为处理器类型的列表 */ static { PIPELINE_ROUTE_MAP.put(InstanceBuildContext.class, Arrays.asList( InputDataPreChecker.class, ModelInstanceCreator.class, ModelInstanceSaver.class )); // 将来其他 Context 的管道配置 } /** * 在 Spring 启动时,根据路由表生成对应的管道映射关系 */ @Bean("pipelineRouteMap") public Map<Class<? extends PipelineContext>, List<? extends ContextHandler<? extends PipelineContext>>> getHandlerPipelineMap() { return PIPELINE_ROUTE_MAP.entrySet() .stream() .collect(Collectors.toMap(Map.Entry::getKey, this::toPipeline)); } /** * 根据给定的管道中 ContextHandler 的类型的列表,构建管道 */ private List<? extends ContextHandler<? extends PipelineContext>> toPipeline( Map.Entry<Class<? extends PipelineContext>, List<Class<? extends ContextHandler<? extends PipelineContext>>>> entry) { return entry.getValue() .stream() .map(appContext::getBean) .collect(Collectors.toList()); } private ApplicationContext appContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; } } 定义管道执行器 最后一步,定义管道执行器。管道执行器 根据传入的上下文数据的类型,找到其对应的管道,然后将上下文数据放入管道中去进行处理。 /** * 管道执行器 */ @Component public class PipelineExecutor { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 引用 PipelineRouteConfig 中的 pipelineRouteMap */ @Resource private Map<Class<? extends PipelineContext>, List<? extends ContextHandler<? super PipelineContext>>> pipelineRouteMap; /** * 同步处理输入的上下文数据<br/> * 如果处理时上下文数据流通到最后一个处理器且最后一个处理器返回 true,则返回 true,否则返回 false * * @param context 输入的上下文数据 * @return 处理过程中管道是否畅通,畅通返回 true,不畅通返回 false */ public boolean acceptSync(PipelineContext context) { Objects.requireNonNull(context, "上下文数据不能为 null"); // 拿到数据类型 Class<? extends PipelineContext> dataType = context.getClass(); // 获取数据处理管道 List<? extends ContextHandler<? super PipelineContext>> pipeline = pipelineRouteMap.get(dataType); if (CollectionUtils.isEmpty(pipeline)) { logger.error("{} 的管道为空", dataType.getSimpleName()); return false; } // 管道是否畅通 boolean lastSuccess = true; for (ContextHandler<? super PipelineContext> handler : pipeline) { try { // 当前处理器处理数据,并返回是否继续向下处理 lastSuccess = handler.handle(context); } catch (Throwable ex) { lastSuccess = false; logger.error("[{}] 处理异常,handler={}", context.getName(), handler.getClass().getSimpleName(), ex); } // 不再向下处理 if (!lastSuccess) { break; } } return lastSuccess; } } 使用管道模式 此时,我们可以将最开始的 buildModelInstance 修改为: public CommonResponse<Long> buildModelInstance(InstanceBuildRequest request) { InstanceBuildContext data = createPipelineData(request); boolean success = pipelineExecutor.acceptSync(data); // 创建模型实例成功 if (success) { return CommonResponse.success(data.getInstanceId()); } logger.error("创建模式实例失败:{}", data.getErrorMsg()); return CommonResponse.failed(data.getErrorMsg()); } 我们模拟一下模型实例的创建过程: 参数正常时: 参数出错时: 这个时候我们再为 InstanceBuildContext 加入新的两个 ContextHandler:FormInputPreprocessor(表单输入数据预处理) 和 BizSideCustomProcessor(业务方自定义数据处理)。 @Component public class FormInputPreprocessor implements ContextHandler<InstanceBuildContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(InstanceBuildContext context) { logger.info("--表单输入数据预处理--"); // 假装进行表单输入数据预处理 return true; } } @Component public class BizSideCustomProcessor implements ContextHandler<InstanceBuildContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(InstanceBuildContext context) { logger.info("--业务方自定义数据处理--"); // 先判断是否存在自定义数据处理,如果没有,直接返回 true // 调用业务方的自定义的表单数据处理 return true; } } 此时 buildModelInstance 不需要做任何修改,我们只需要在 “路由表” 里面,将这两个 ContextHandler 加入到 InstanceBuildContext 关联的管道中,Spring 启动的时候,会自动帮我们构建好每种 Context 对应的管道: 加入新的处理器 再模拟一下模型实例的创建过程: 异步处理 管道执行器 PipelineExecutor 中,acceptSync 是个同步的方法。 小蜜:看名字你就知道你悄悄埋伏笔了。 对于步骤繁多的任务,很多时候我们更需要的是异步处理,比如某些耗时长的定时任务。管道处理异步化非常的简单,我们先定义一个线程池,比如: <!-- 专门用于执行管道任务的线程池 --> <bean id="pipelineThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <property name="corePoolSize" value="4" /> <!-- 核心线程数 --> <property name="maxPoolSize" value="8" /> <!-- 最大线程数 --> <property name="keepAliveSeconds" value="960" /> <!-- 线程最大空闲时间/秒(根据管道使用情况指定)--> <property name="queueCapacity" value="256" /> <!-- 任务队列大小(根据管道使用情况指定)--> <property name="threadNamePrefix" value="pipelineThreadPool" /> <property name="rejectedExecutionHandler"> <bean class="java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy" /> </property> </bean> 然后在 PipelineExecutor 中加入异步处理的方法: /** * 管道线程池 */ @Resource private ThreadPoolTaskExecutor pipelineThreadPool; /** * 异步处理输入的上下文数据 * * @param context 上下文数据 * @param callback 处理完成的回调 */ public void acceptAsync(PipelineContext context, BiConsumer<PipelineContext, Boolean> callback) { pipelineThreadPool.execute(() -> { boolean success = acceptSync(context); if (callback != null) { callback.accept(context, success); } }); } 通用处理 比如我们想记录下每次管道处理的时间,以及在处理前和处理后都打印相关的日志。那么我们可以提供两个通用的 ContextHandler,分别放在每个管道的头和尾: @Component public class CommonHeadHandler implements ContextHandler<PipelineContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(PipelineContext context) { logger.info("管道开始执行:context={}", JSON.toJSONString(context)); // 设置开始时间 context.setStartTime(LocalDateTime.now()); return true; } } @Component public class CommonTailHandler implements ContextHandler<PipelineContext> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean handle(PipelineContext context) { // 设置处理结束时间 context.setEndTime(LocalDateTime.now()); logger.info("管道执行完毕:context={}", JSON.toJSONString(context)); return true; } } 通用头、尾处理器可以在路由表里面放置,但是每次新加一种 PipelineContext 都要加一次,好像没有必要 —— 我们直接修改下 管道执行器 PipelineExecutor 中的 acceptSync 方法: @Component public class PipelineExecutor { ...... @Autowired private CommonHeadHandler commonHeadHandler; @Autowired private CommonTailHandler commonTailHandler; public boolean acceptSync(PipelineContext context) { ...... // 【通用头处理器】处理 commonHeadHandler.handle(context); // 管道是否畅通 boolean lastSuccess = true; for (ContextHandler<? super PipelineContext> handler : pipeline) { try { // 当前处理器处理数据,并返回是否继续向下处理 lastSuccess = handler.handle(context); } catch (Throwable ex) { lastSuccess = false; logger.error("[{}] 处理异常,handler={}", context.getName(), handler.getClass().getSimpleName(), ex); } // 不再向下处理 if (!lastSuccess) { break; } } // 【通用尾处理器】处理 commonTailHandler.handle(context); return lastSuccess; } } 总结 通过管道模式,我们大幅降低了系统的耦合度和提升了内聚程度与扩展性: ModelService 只负责处理 HSF 请求,不用关心具体的业务逻辑 PipelineExecutor 只做执行工作,不用关心具体的管道细节 每个 ContextHandler 只负责自己那部分的业务逻辑,不需要知道管道的结构,与其他ContextHandler 的业务逻辑解耦 新增、删除 或者 交换子步骤时,都只需要操作路由表的配置,而不要修改原来的调用代码 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
过去一年淘宝直播快速发展,截止2020年9月底,80个淘宝直播产业基地在全国落地开花,从农村走出10万农民主播,直播真正意义上成为帮助商家和消费者完成交易的利器,同时通过各种互动玩法让直播购物变得有趣好玩。在2020年双11开始阶段,淘宝直播App升级了18年直播答题「点题成金」的玩法,推出「一猜到底」新玩法。如果说传统的直播答题是「选择题」,一猜到底的玩法更像是几万人同时在线的「抢答题」,将答题方式从文字选择升级成语音抢答,给出猜中价格高低提示,让用户增加了更多的参与的乐趣。 为了实现比肩综艺现场的直播竞猜体验,我们一次压上了由达摩院语音实验室、阿里云PAI团队、淘系技术直播App和端智能MNN团队组成的全明星阵容,通力协作之下,一举实现了工业界首个用于直播的移动端语音识别。 业务流程和技术挑战 「一猜到底」整体玩法链路如上图所示,主播口播开始后,用户需要在人数和时间未满前,按住按钮,通过语音报出价格,系统通过本地语音识别能力进行识别和结果比对,提示用户所报价格“过高”还是“过低”,直到答对或者超时结束。在每一关有限的作答时间内,用户往往需要多次竞答,才能逼近商品的真实价格。于是,实时语音识别能不能准确且快速地识别用户的报价,直接决定了「一猜到底」的成败。 不同于一般的语音识别应用,一场顶流的淘宝直播,可以聚集百万乃至千万的用户围观。这么多用户同时进行语音识别,会出现非常多的请求,如果采用云端识别对服务压力和服务质量都有非常大的挑战。项目开始时实验了端侧和云侧识别的两种方案,发现云侧方案难以支撑这样的活动,最终选择了端侧方案,确定端侧识别方案之后,发现也不是康庄大道,主要存在以下技术难点: * 高精度高性能的本地语音识别 目前行业比较成熟的是服务端的语音识别方案,完全照搬服务端方案到移动端也不现实,需要创建一套适合移动端运行的语音识别方案。同时,直播场景下的语音答题噪声较大,对语音识别的准确度要求较高,语音识别速度也会对用户的答题速度造成巨大影响。 * 语音模型和资源包体积过大 考虑到活动特性,端侧的语音识别引擎需要内置在包内,而且越小越好。经过客户端研发评估,如何做到15MB以内甚至更小的语音模型是关键,因此需要极致的模型压缩能力支持。 * 端侧资源有限,性能压力大 直播场景本身就已经很占用资源,叠加直播场景下做语音识别,对语音识别过程中的CPU、内存占用,都有很大的要求,高性能的推理和优化成为模型落地的最大拦路虎。 移动端实时语音识别技术大揭秘 阿里达摩院语音实验室早在2015年就研发出了第一代移动端离线语音识别方案,近来结合PAI模型压缩、MNN高性能推理引擎,实现了移动端离线和流式端到端语音识别方案,满足语音指令、语音识别、实时翻译等场景需求。根据「一猜到底」项目需求,我们选取"基于SAN-M的离线端到端语音识别"方案,通过极致的模型压缩和性能优化,最终实现模型大小小于15MB、内存占用低于60MB、1s语料识别快于50ms的高性能方案。 ▐ 基于SAN-M的离线端到端语音识别 目前,最具代表性的离线端到端语音识别模型LAS[1]和Transformer[2]都是基于Attention-Encoder-Decoder的。LAS采用基于BLSTM的Encoder和基于单向LSTM的Decoder;而Transformer则采用Multi-head Self-Attention模块组建Encoder网络,采用Masked Multi-head Self-Attention组建Decoder网络。 在公开评测任务集上,Transformer较LAS在性能上有优势,同时由于采用了Multi-head,训练并行化效率更高。我们分析了Self-Attention和DFSMN memory block[3,4]之间的关联性:Self-Attention可以理解为采用了context-dependent系数进行全局建模,而DFSMN的memory block则采用了context-independent系数进行局部建模。对于语音识别,局部声学建模和全局语义建模都非常重要,因此我们提出了如下图所示的SAN-M模型结构,高效地融合了Self-Attention和DFSMN memory block。 SAN-M模块如上左图所示,将Self-Attention和DFSMN memory block融合一个模块,有效的结合了Self-Attention的全局长时建模能力和memory block的局部长时建模能力。基于SAN-M模块构建了如上右图的Encoder-Decoder离线语音识别系统(SAN-M-E2E-ASR),并在开源的1000小时AISHELL-2中文识别任务中获得了当前该任务的最优性能(CER=5.61%);在工业量级的2万小时中文识别任务中,该系统也显著优于我们之前线上的CTC系统和标准Transformer系统。 针对本次识别场景,我们最终实现了不到40MB的端到端模型,而识别性能则可以媲美上一代整体超过100GB大小的云端DFSMN-CTC系统。我们在finetune数据上进行了不同维度的挑选和搭配,并做了不同策略的数据扩增来覆盖多样的识别情况。针对模型输出的token,也进行了一定压缩,并拉低了与本次任务无关的token概率来降低误识别率。在ITN模块,我们采用精小的FST(Finite State Transducer)来实现规则网络,用状态转移来实现文字到阿拉伯数字的转换,通过边上权重来控制其转换方向,并在简略读法、谐音、容错上也做了一系列路径优化。 ▐ 基于PAI-MNN云端一体化模型压缩 虽然达摩院语音实验室通过定制化语音识别模型设计,将原有的170MB模型裁剪至不到40MB,但是考虑到移动端的资源情况,我们还需要通过PAI-MNN云端一体化模型压缩方案,进一步将模型基本无损地压缩到15MB以内。 从训练、模型压缩到优化部署的PAI-MNN云端一体方案 PAI混合精度量化流程 PAI混合精度量化流程 上图显示了PAI团队 (PAI: Platform of A. I. in Alibaba)研发的无数据标注干预的自动混合精度量化流程(Label-free AMP Pipeline, AMP: Automatic Mixed Precision),包括量化误差预补偿、离线标定、量化噪声分析与混合精度决策四个阶段,主要创新点包括: 支持端到端Transformer的离线后量化: PAI团队的后量化方法,引入了循环张量探针,以支持端到端Transformer的离线后量化。 相比于拆图量化、量化训练等,端到端后量化具备快捷、高效的优势; 集成了丰富的后量化策略,为后量化的精度鲁棒性提供了坚实保证,基本策略包括: KL算法的改进,能够有效减少输入/输出张量的量化噪声; EasyQuant(参考文献 [5])的使用,可进一步减少输入/输出张量的量化误差,尤其能改善INT7等更低精度量化的效果; Bias Correction(参考文献 [6])通过补偿网络权重的量化偏差(均值与方差的偏差),以减少权重量化噪声;同时对Bias Correction的适当改进,增强了对SAN-M ASR模型的补偿效果; ADMM(参考文献 [7])亦可优化权重量化参数,减少权重量化噪声;也适当改进了ADMM的使用,从而在交替方向迭代范围内,确保权重量化误差最小; Weight Adjustment(参考文献 [8])在Kernel weight按Per-tensor量化时,通过Per-channel形式的等价均衡变换,可以减少Weight量化误差。 无Label干预的混合精度量化流程: 该流程从模型输入到混合精度决策,无需数据标注(Label)的干预,简洁易用、快捷有效; 量化误差按逐层统计,且能准确反映每个网络层的量化敏感度,为混合精度(INT8/FP32混合)决策提供了可靠基础; 通过控制回退的网络层数,可选择出精度与模型容量折中最佳的帕累托最优解,完成多目标优化; 生成的混合精度量化表,能够对接移动端推理框架MNN,以生成低延迟、高推理精度的运行时推理引擎;从而构成了完整的工具链路,即从混合精度量化、到移动端的推理部署; AMP Pipeline不仅适用于移动端,也适用于CPU/GPU优化部署,体现了PAI云端一体的优势所在。 基于PAI AMP Pipeline,有效实现了SAN-M模型的离线后量化(PTQ: Post-training Quantization)。为了保持算法模型识别精度,经AMP INT8量化之后(回退3个Op,分类层保留为FP32实现)。 为了解决压缩率的问题,MNN模型转换和优化工具对回退的算子统一使用权重8bit存储、float计算的方式进行优化,进一步压缩模型大小。通过一套统一格式的模型压缩文件,经过PAI AMC优化的模型可以顺滑无缝地转换到MNN的格式。 MNN模型转换工具基于现有的图优化流程,根据该模型压缩文件将float模型转换成MNN模型的同时完成离线量化,具体过程如下: 根据量化表中提供的tensor name,在TensorFlow的计算图中生产和消费该tensor的边上同时插入一个自定义的量化和反量化算子。 将TensorFlow的计算图转换成MNN的计算图,其中自定义的量化和反量化算子转换成MNN量化(FloatToInt8)和反量化(Int8ToFloat)算子。 算子融合:将支持量化的算子、输入的反量化算子和输出的量化算子融合成一个Int8的算子。 最后消除成对的MNN量化和反量化算子。 最终,SAN-M模型在众包测试集上的WER绝对损失低于0.1%、SER绝对损失低于0.5%、理论压缩比约为3.19倍。 ▐ 基于MNN推理引擎的实时高性能计算 为了在移动端上实现实时的端到端语音识别模型推理计算,MNN在全链路上做了诸多优化。 端到端语音识别模型基于Transformer结构,包含一个对输入音频特征编码的Encoder和一个自回归解码的Decoder。这类模型结构要求MNN支持Control Flow、Dynamic Shape和Zero Shape等特性,因此,MNN首先在框架层面对这些特性进行了支持和完善: MNN重构了Control Flow支持方案,提供用户透明的functional control flow实现,并支持了TensorFlow 1.x的控制流模型转换,为用户提供一站式的部署体验。 对于Dynamic Shape的支持,MNN将整图按照动态形状算子划分为多个分段子图。在代码层面,一个子图对应一个Module,Module支持嵌套,即整图被表达为一个由Module组成的调用树,树的每个叶子节点可以使用一个Session来执行,Session每次执行前resize,重新进行shape推理和分配内存。 Zero Shape指的是模型中某些Tensor的shape存在0值,比如 (1, 0, 256),这种情况大多是为了给while-loop中某些循环变量提供初始值而引入的。MNN在形状推理和执行逻辑上对Zero Shape进行了支持。 之后,MNN根据达摩院模型新增了LayerNorm Fuse、Constant Folding、重复Reshape算子消除等图优化方法。图优化之后的计算图更容易和其他优化方法组合使用,比如,Constant Folding后MatMul的一个输入可能被替换成一个Constant节点,因此就可以转换成FullyConnected或Conv1x1进行加速,并且也更容易利用模型压缩方法对权重进行量化。 而后,语音模型的耗时重点仍然是矩阵乘法。MNN通过更优矩阵乘分块、基于 NC4HW4 布局优化前后内存布局转化、Strassen 算法改进等策略,优化了整体的卷积和矩阵乘的性能,ARM 架构上性能提高了 10%-20% ,保障了语音模型的高效运行。 同时,MNN最新提出的几何计算机制也在实时语音识别起到了重要作用。几何计算是MNN为了解决设备碎片化问题而提出的一种新机制,其核心在于把坐标映射标准化,以便统一实现与优化。在几何计算的支持下,我们可以较简单地合并相邻的纯形变算子,从而降低访存需求,提升模型运行性能。 最后,在PAI-MNN云端一体化模型压缩的加持下,我们利用量化表和有限回退机制,在精度损失可控的前提下,进一步降低了移动端上的计算总量。 RTF (real time factor),即实时率,表示识别一秒钟音频需要的耗时。 在这一系列组合拳之下,我们才最终在目标设备上,将RTF(real time factor)降低到了目标值0.02以下,从而实现实时语音识别,让「一猜到底」得以走到每一个用户的面前。 总结与展望 通过这次项目合作,基于高性能推理引擎MNN,结合一流的语音模型设计和模型压缩技术,我们已经能在移动端上实现实时的语音识别,并通过了双11核心场景的考验。除了上述离线端到端语音识别之外,我们还实现了更复杂的流式端到端语音识别,能够低延迟地流式输出识别结果,可以应用到语音实时翻译等场景。 在硬件算力、模型设计、模型压缩和推理引擎飞速发展的共同推动下,CV、Data的AI应用场景已经日趋成熟,ASR、NLP的规模化应用也已指日可待,端侧AI的应用场景仍在持续发酵。 淘系技术部端智能团队,基于淘系丰富业务场景,持续进行端侧AI技术建设和业务创新实践。除开源推理引擎MNN之外,我们将上百次AI应用开发实践中积累的经验沉淀为MNN 工作台。MNN工作台将极大降低AI应用门槛,将AI研发的效率提升数十倍,让“技术小白”也能快速上手,轻松设计自己的AI应用。MNN工作台已于近日正式对公众开放,赶快进入MNN官网下载体验吧。 MNN官网:http://www.mnn.zone 参考:[1] Chan W, Jaitly N, Le Q, et al. Listen, attend and spell: A neural network for large vocabulary conversational speech recognition[C]//2016 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). IEEE, 2016: 4960-4964. [2] Vaswani, Ashish, et al. "Attention is all you need." Advances in neural information processing systems. 2017. [3] Zhang S, Lei M, Yan Z, et al. Deep-fsmn for large vocabulary continuous speech recognition[C]//2018 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). IEEE, 2018: 5869-5873. [4] Zhang S, Lei M, Liu Y, et al. Investigation of modeling units for mandarin speech recognition using dfsmn-ctc-smbr[C]//ICASSP 2019-2019 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). IEEE, 2019: 7085-7089. [5] Di Wu, Qi Tang, Yongle Zhao, Ming Zhang, Ying Fu, Debing Zhang, "EasyQuant: Post-training Quantization via Scale Optimization", arXiv preprint 2006.16669, 2020. [6] Ron Banner, Yury Nahshan, Elad Hoffer, Daniel Soudry, "Post-training 4-bit quantization of convolution networks for rapid-deployment", arXiv preprint 1810.05723, 2018. [7] Cong Leng, Hao Li, Shenghuo Zhu, Rong Jin, "Extremely Low Bit Neural Network: Squeeze the Last Bit Out with ADMM", arXiv preprint 1707.09870, 2017. [8] Markus Nagel, Mart van Baalen, Tijmen Blankevoort, Max Welling, "Data-Free Quantization Through Weight Equalization and Bias Correction", arXiv preprint 1906.04721, 2019. 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
作者|周密(之叶) 策略模式(Strategy Pattern)定义了一组策略,分别在不同类中封装起来,每种策略都可以根据当前场景相互替换,从而使策略的变化可以独立于操作者。比如我们要去某个地方,会根据距离的不同(或者是根据手头经济状况)来选择不同的出行方式(共享单车、坐公交、滴滴打车等等),这些出行方式即不同的策略。 何时使用策略模式 阿里开发规约-编程规约-控制语句-第六条 :超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现。相信大家都见过这种代码: if (conditionA) { 逻辑1 } else if (conditionB) { 逻辑2 } else if (conditionC) { 逻辑3 } else { 逻辑4 } 这种代码虽然写起来简单,但是很明显违反了面向对象的 2 个基本原则: 单一职责原则(一个类应该只有一个发生变化的原因):因为之后修改任何一个逻辑,当前类都会被修改 开闭原则(对扩展开放,对修改关闭):如果此时需要添加(删除)某个逻辑,那么不可避免的要修改原来的代码 因为违反了以上两个原则,尤其是当 if-else 块中的代码量比较大时,后续代码的扩展和维护就会逐渐变得非常困难且容易出错,使用卫语句也同样避免不了以上两个问题。因此根据我的经验,得出一个我个人认为比较好的实践: if-else 不超过 2 层,块中代码 1~5 行,直接写到块中,否则封装为方法 if-else 超过 2 层,但块中的代码不超过 3 行,尽量使用卫语句 if-else 超过 2 层,且块中代码超过 3 行,尽量使用策略模式 愉快地使用策略模式 在 Spring 中,实现策略模式的方法多种多样,下面我分享一下我目前实现策略模式的 “最佳套路”(如果你有更好的套路,欢迎赐教,一起讨论哦)。 没时间解释了快上车 ▐ 需求背景 我们平台的动态表单,之前专门用于模型输入的提交。现在业务方希望对表单能力进行开放,除了可用于模型提交,还可以用于业务方指定功能的提交(方式设计为绑定一个 HSF 泛化服务,HSF 即淘系内部的 RPC 框架)。加上我们在配置表单时的 “预览模式” 下的提交,那么表单目前便有以下三种提交类型: 预览表单时的提交 模型输入时的提交 绑定 HSF 时的提交 现在,有请我的 “最佳套路” 上场。 ▐ 第一步,定义策略接口 首先定义策略的接口,包括两个方法: 1、获取策略类型的方法 2、处理策略逻辑的方法 /** * 表单提交处理器 */ public interface FormSubmitHandler<R extends Serializable> { /** * 获得提交类型(返回值也可以使用已经存在的枚举类) * * @return 提交类型 */ String getSubmitType(); /** * 处理表单提交请求 * * @param request 请求 * @return 响应,left 为返回给前端的提示信息,right 为业务值 */ CommonPairResponse<String, R> handleSubmit(FormSubmitRequest request); } /** * 表单提交的请求 */ @Getter @Setter public class FormSubmitRequest { /** * 提交类型 * * @see FormSubmitHandler#getSubmitType() */ private String submitType; /** * 用户 id */ private Long userId; /** * 表单提交的值 */ private Map<String, Object> formInput; // 其他属性 } 其中,FormSubmitHandler 的 getSubmitType 方法用来获取表单的提交类型(即策略类型),用于根据客户端传递的参数直接获取到对应的策略实现;客户端传递的相关参数都被封装为 FormSubmitRequest,传递给 handleSubmit 进行处理。 ▐ 第二步,相关策略实现 预览表单时的提交 @Component public class FormPreviewSubmitHandler implements FormSubmitHandler<Serializable> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public String getSubmitType() { return "preview"; } @Override public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) { logger.info("预览模式提交:userId={}, formInput={}", request.getUserId(), request.getFormInput()); return CommonPairResponse.success("预览模式提交数据成功!", null); } } 模型输入时的提交 @Component public class FormModelSubmitHandler implements FormSubmitHandler<Long> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public String getSubmitType() { return "model"; } @Override public CommonPairResponse<String, Long> handleSubmit(FormSubmitRequest request) { logger.info("模型提交:userId={}, formInput={}", request.getUserId(), request.getFormInput()); // 模型创建成功后获得模型的 id Long modelId = createModel(request); return CommonPairResponse.success("模型提交成功!", modelId); } private Long createModel(FormSubmitRequest request) { // 创建模型的逻辑 return 123L; } } HSF 模式的提交 @Component public class FormHsfSubmitHandler implements FormSubmitHandler<Serializable> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public String getSubmitType() { return "hsf"; } @Override public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) { logger.info("HSF 模式提交:userId={}, formInput={}", request.getUserId(), request.getFormInput()); // 进行 HSF 泛化调用,获得业务方返回的提示信息和业务数据 CommonPairResponse<String, Serializable> response = hsfSubmitData(request); return response; } ... } ▐ 第三步,建立策略的简单工厂 @Component public class FormSubmitHandlerFactory implements InitializingBean, ApplicationContextAware { private static final Map<String, FormSubmitHandler<Serializable>> FORM_SUBMIT_HANDLER_MAP = new HashMap<>(8); private ApplicationContext appContext; /** * 根据提交类型获取对应的处理器 * * @param submitType 提交类型 * @return 提交类型对应的处理器 */ public FormSubmitHandler<Serializable> getHandler(String submitType) { return FORM_SUBMIT_HANDLER_MAP.get(submitType); } @Override public void afterPropertiesSet() { // 将 Spring 容器中所有的 FormSubmitHandler 注册到 FORM_SUBMIT_HANDLER_MAP appContext.getBeansOfType(FormSubmitHandler.class) .values() .forEach(handler -> FORM_SUBMIT_HANDLER_MAP.put(handler.getSubmitType(), handler)); } @Override public void setApplicationContext(@NonNull ApplicationContext applicationContext) { appContext = applicationContext; } } 我们让 FormSubmitHandlerFactory 实现 InitializingBean 接口,在 afterPropertiesSet 方法中,基于 Spring 容器将所有 FormSubmitHandler 自动注册到 FORM_SUBMIT_HANDLER_MAP,从而 Spring 容器启动完成后, getHandler 方法可以直接通过 submitType 来获取对应的表单提交处理器。 ▐ 第四步,使用 & 测试 在表单服务中,我们通过 FormSubmitHandlerFactory 来获取对应的表单提交处理器,从而处理不同类型的提交: @Service public class FormServiceImpl implements FormService { @Autowired private FormSubmitHandlerFactory submitHandlerFactory; public CommonPairResponse<String, Serializable> submitForm(@NonNull FormSubmitRequest request) { String submitType = request.getSubmitType(); // 根据 submitType 找到对应的提交处理器 FormSubmitHandler<Serializable> submitHandler = submitHandlerFactory.getHandler(submitType); // 判断 submitType 对应的 handler 是否存在 if (submitHandler == null) { return CommonPairResponse.failure("非法的提交类型: " + submitType); } // 处理提交 return submitHandler.handleSubmit(request); } } Factory 只负责获取 Handler,Handler 只负责处理具体的提交,Service 只负责逻辑编排,从而达到功能上的 “低耦合高内聚”。 写一个简单的 Controller: @RestController public class SimpleController { @Autowired private FormService formService; @PostMapping("/form/submit") public CommonPairResponse<String, Serializable> submitForm(@RequestParam String submitType, @RequestParam String formInputJson) { JSONObject formInput = JSON.parseObject(formInputJson); FormSubmitRequest request = new FormSubmitRequest(); request.setUserId(123456L); request.setSubmitType(submitType); request.setFormInput(formInput); return formService.submitForm(request); } } 最后来个简单的测试: 我感觉到了,这就是非常流畅的感觉~ ▐ 设想一次扩展 如果我们需要加入一个新的策略,比如绑定 FaaS 函数的提交,我们只需要添加一个新的策略实现即可: @Component public class FormFaasSubmitHandler implements FormSubmitHandler<Serializable> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public String getSubmitType() { return "faas"; } @Override public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) { logger.info("FaaS 模式的提交:userId={}, formInput={}", request.getUserId(), request.getFormInput()); // 进行 FaaS 函数调用,并获得业务方返回的提示信息和业务数据 CommonPairResponse<String, Serializable> response = faasSubmitData(request); return response; } ... } 此时不需要修改任何代码,因为 Spring 容器重启时会自动将 FormFaasSubmitHandler 注册到 FormSubmitHandlerFactory 中 —— 面向 Spring 编程,太香惹~
今年双11的整体节奏从之前的“光棍节”变为“双节棍”,具体业务上也有很多变化和调整,应了阿里的土话“唯一不变的是变化”。面对这些变化,是挑战也是机会,我们要做的就是,“既要”高效支撑保障业务先赢,“又要”确保体验和稳定性带给用户极致体验,“还要”追求创新让前端持续演进。为了实现“既要、又要、还要”,包括技术方案、流程机制、人员组织等各方面都进行了大量的设计和保障。最终第一次双峰的双11圆满结束,淘系前端也实现了自己的目标,包括应用大量优化手段和创新方案带来业务转化提升;将FaaS、PHA、ESR等技术应用在更多场景,分别向服务端、客户端、CDN节点进一步拓展了前端的能力和边界;应用视觉还原、一体化研发等提升研发效率,大幅缓解资源瓶颈等等。下面会整体介绍一下淘系前端在今年双11的思考和沉淀,希望对大家有所助益。后续也会有各个专项的系列文章,希望大家持续关注。 变化 & 挑战 今年的双11,首先感受到的就是源源不断的变化。 单峰变双峰:双11从之前的一个波段变成今年的两个波段,大促的三个阶段预售、预热、正式也都对应的翻倍。首先带来的是研发工作量的大幅增加,在时间排期不变、工作量增加、人员不变的情况高效的完成需求研发是第一重挑战;其次面对6个阶段的状态变化,如何保持准确切换、稳定运行、体验流畅是在双峰期间要重点保障的内容;最后面对超过20天的超长作战期,安全生产、人员状态保持、快速反应都需要有强力的组织和机制进行保障。 图:双11节奏 首页大改版:最新的淘宝首页首屏内容有颠覆性的变化,比如首屏内容简化,推荐提前,频道作为内容嵌入推荐等。各个业务在缺少固定的流量入口的情况下,包括运营策略、产品策略、设计方案、技术方案都需要积极调整。同时在各个场景的推荐能力也需要持续增强,今年双11通过将坑位数扩展到1000+,理论可达无限扩坑;通过智能UI提升用户点击率。 图:手淘版本对比 业务变化:业务创新和新玩法层出不穷,包括mini详情、旗舰店、价格表达、笔笔返、芝麻购等在内的很多业务都是全新的表达、颠覆式的升级。即是业务上新的尝试,在技术上也要解决架构选型、对账、一致性表达、排期等问题。 做好本职 首先要做的就是做好本职工作,保障需求研发和稳定性。需求研发方面,我们通过D2C实现了大部分UI模块自动开发、通过建设Eva互动体系降低互动研发成本、通过Serverless的一体化研发提升研发和运维效率,使前端不再成为资源瓶颈。稳定性上,也通过一系列机制和工具体系进行保障。同时增加一块大家平时可能不太关注的资损防控的策略和方案。 ▐ D2C研发提效 去年双11我们设立了研发效率专项,核心就是通过 设计稿生成代码(Design to Code, D2C)平台 Imgcook 来提升研发效率。最终在去年的双11大促会中承接了 78.94% 的新增模块代码自动生成,代码可用率达到 79.34%。 今年前端智能化助力前端研发模式升级,数个 BU 共建前端设计稿识别算法模型和数据集,设计稿生成代码技术体系全面升级,如对 UI 多态、直播视频组件、循环的智能识别增强等。今年双11会场承接了 90.4% 的新模块代码智能生成,代码可用率达到 79.26%(对比去年升级设计稿智能检查能力,视觉稿无需人工辅助调整)。得益于D2C的研发提效,今年并没有出现往年借调资源投入会场开发的情况。相比传统模块开发模式,使用设计稿生成代码技术后编码效率(模块复杂度和研发耗时比值)提升68%,固定人力单位时间模块需求吞吐量增加约 1.5 倍。 图:D2C操作流程 ▐ 互动研发升级 在电商领域,互动是一个重要的用户增长方案,在提升用户黏性、活跃以及拉新上都发挥着重要的作用。今年双11,淘系互动团队推出了“超级星秀猫”,我们不盖楼、不开车,全民参与养猫出道,3只风格各异的萌猫咪一经问世,瞬间俘获了无数消费者的心。通过 EVA 互动体系一整套解决方案,大幅提升研发效率,支撑全民养猫猫在手淘、猫客、支付宝等多个 APP 互通。借助客户端能力及 EVA 互动体系将性能与内存良好控制,让多数用户体验高清稳定的互动,实现 0 故障及秒开,同时星秀猫参与人数再创新高。后续的系列文章将具体阐述淘系互动前端团队是如何做到双11互动又快又好又稳,内容涵盖互动基础、EVA研发体系和全局稳定性方案3个方面。 图:互动效果图 ▐ Node FaaS一体研发 Serverless云+端研发模式通过打通页面代码和服务代码进行一体研发,使得前端可以从前台页面到后端服务完整支撑,节省中间沟通和联调成本。在天猫榜单以及V榜的落地,使得双11 Node FaaS 相关业务整体研发效率提升38.89%。行业导购双11需求也在云+端的新模式下支撑外包快速入场,使得整体提效约20%。 ▐ 稳定性保障 稳定性保障贯穿从项目启动到结束的整个双11周期,下面从几个重点方面进行简单的介绍: 变化评估:每年的双11都是站在巨人的肩膀上,都经过了上一次双11的考验。主要的风险就变成新增的部分以及变化的部分,这里的变化既包括技术上的变化也包含人员上的变化。要做到对变化的充分评估,在99大促进行验证,并且保证99大促后不再进行变化,以一个稳定的状态迎接双11。 压测:首先要进行流量评估,借鉴去年数据的同时结合今年的变化,进行相应的机器、带宽等资源准备。完成单线路压测,保证在预估流量模型下,自己的服务和上下游都能够运转正常。进行全链路压测,核心验证在0点高峰时各业务并发的情况的运转情况,尤其是一些底层公共服务,以及优先级的保障情况。 兜底&预案:兜底一般指在大流量或其他不可控因素的情况下,如何将用户体验和业务损失降低到最小。预案需要评估可能遇到的各种情况,以及对应的处理方案。 验收:功能预演,按照用户的所有使用路径进行操作,目前这个工作仍是人工。时间穿越,将页面和系统的状态都调整为活动时间来验证,需要打通上下游的各个系统并形成联动。机型验收,基本分为高端机、中端机、低端机,分别进行验收,很多业务都需要针对低端机做功能降级。稳定性验收,单独页面的性能和稳定性各自保障,但业务叠加后很可能存在问题,尤其像会场、互动、直播、旗舰店等内存消耗大户,互相之间都有引流,切换后很难保证,需要整体全链路验收。 变更&应急:历次的故障数据表明,大部分的问题都是由于变更导致的,如何做好变更管控尤为重要。根据时间分为弱管控、强管控期;根据业务等级分为集团核心应用、BU核心应用、非核心应用等;建立变更的CR和审批的机制。应急主要指在核心活动期间,问题、舆情、故障等流转机制,针对问题发现、定位问题、修复问题时间作出要求,不同等级如何决策作出安排。 监控:淘系前端持续进行监控能力的建设和升级。需要保障大促高峰的可用性以及报警的实时性,覆盖所有的业务场景。针对越来越复杂的场景,需要端到端的监控和数据分析平台。灰度过程缺少度量和定点监控。根据这些问题和需求,jstracker提供了安全生产的整体解决方案,打造端到端的前端监控与数据分析平台,打造实时监控、多端覆盖、数据分析、智能化的数据平台。同时根据页面情况、错误日志、源站数据、FaaS日志等打造了双11的前端数据大盘。 ▐ 资损防控 一直以来前端资损防控是平台非常薄弱的一环,前端触发的资损和舆情问题不在少数。之前全靠开发同学的经验和意识来保证,缺少体系化的资损防控能力。去年开始组织了团队层面的集中筛查和人工预演,对人力和时间的消耗非常巨大,并且很难保证质量并进行积累和沉淀。所以为了能有一种成本更低、预防效果更好的方式进行资损防控,2020年 S1 伊始,就重点对资防做相关产品化的设计和实现。同时今年也重点增加了商家、运营中后台侧的资损防控。 我们将资损防控氛围了三个阶段,研发阶段、操作阶段、运行阶段。研发阶段给存在资损风险的仓库打标,将常规的价格、优惠、默认文案等case进行枚举,通过静态扫描、UI测试用例扫描等方式进行防控。操作阶段,主要是指商家、运营进行优惠、权益等设置的阶段,通过表达方式统一(避免5折、0.5折造成理解差异)、二次确认、限定边界值、低价预警等进行防控。运行阶段有快照对比、服务端数据对账等方式,运行阶段的防控相对滞后,发现时很大概率已经造成实际的影响。 然而,目前仍是预防为主,不能百分之百保障没有资损故障发生,接下来我们还在构思链路级别的、生产环境上的防控手段,建设一些告警和自动止血为平台保驾护航。 业务价值 做好本职的基础上,我们希望给业务带来增量价值。本章从会场性能优化提升转化、基础链路新方案提升转化、唤端技术定制策略提升精准率、智能UI为不同人群提供不通过UI提升点击等4个方面来介绍。 ▐ 性能提升 会场是每年双11的主角之一,会场的用户体验自然也是每年最关注的点。在日趋复杂的业务需求下,如何保障我们的用户体验不劣化甚至能更优化是永恒的命题。今年分别使用了预渲染方案和SSR方案进行优化,首先是重新定义了秒开的标准,从原来的前端时间升级到从用户点击经过跳转到页面可视的时间,增加了客户端路由、webview启动等时间,使体验的衡量更贴近用户真实的体感。覆盖了包括主会场、行业会场、外投会场等数十个场景。 预渲染 预渲染是在今年双11会场中使用的技术方案,用于提升用户打开会场的体验。将原有H5页面渲染流程中的WebView的初始化、页面资源加载、部分JS的执行等耗时的操作,提前执行,在离屏状态下完成页面“渲染”。当用户真正点击进入会场的时候,复用这个提前“渲染”的页面,大大节省打开会场的时间。用户打开会场的整体平均耗时缩短了200ms~700ms左右,秒开率提升10%-14%。优化对中低端机绝对收益更高,已实现在低端机上实现秒开会场。让用户逛会场体验更流畅了,尤其中低端手机效果更加明显。在后续的文章也会讲述包括预渲染、数据快照、并行请求等性能优化方面的实践与思考。 图:中低端机型预渲染效果对比图 SSR 今年在不改变现有架构,不改变业务的前提下,在会场上使用了 ServerSideRendering 技术,将秒开率提高到了新的高度(82.6%);在用户体验得到优化的同时,业务指标如点击率等也有明显的增长,带来了不错的业务价值。后续的系列文章汇中会详细介绍前端在解决工程化、业务效果评估上的具体实践与方法论;服务端在解决前端模块代码于服务端执行、隔离和性能优化上的思考和方案。 图:中低端机型 SSR 效果对比图 基础链路 基础链路是电商核心的链路,包含首页、商品详情、微详情、交易(下单、订单、购物车、支付成功)、信息流、我的淘宝等基础业务。现有的技术方案是手淘内使用Native版本,追求极致的体验和稳定性;站外流量、包括支付宝在内的阿里系App使用H5版本,追求灵活性和可用性。随着支付宝容器化体系的完善,在其他App中的内聚,基础链路新的容器化版本具备了孵化的土壤;同时H5的一些弊端,比如资源都在远端、Native能力使用限制等也可以得到优化。 借助之前的“新奥创”和“DinamicX”方案(主要解决业务定制以及安卓、iOS、H5的三端一致,实现一处开发、三端生效),容器化版本得以快速扩展,实现四端一致。性能数据上,加载时间对比H5版本有2s的提升,基本达成秒开的目标;业务数据上,容器化版本对比H5版本UV转化率提升70+%。 目前已覆盖支付宝、特价版、优酷、高德、淘小铺、一淘等App,以及通过百川SDK集成在众多外部媒体App。业务上也接入了每日必抢、大牌直降、淘宝特价、淘宝直播、百川媒体、优酷、小铺、轻店、花呗等业务。 唤端技术 随着流量见顶、电商大战进一步升级,如何做好用户增长是各大公司必须完成的命题。用户增长涉及的面非常广泛,今年淘系前端聚焦在唤端技术,即外部流量拉起手淘App的技术体系。唤端技术的门槛很低,简单到只需要拼一个类似 URL 的 scheme 就可以触发唤端。唤端技术又很复杂,不同的渠道、不同的OS、不同的 App 都有可能针对唤端协议有限制,并有各种各样的兼容性问题;唤端链路中不同业务可能都有自己的业务定制需求,例如参数的透传;唤端链路的效率更是被关注的核心点,不同场景不同业务在效率上可能都不一样,因此还需要对唤端效果进行监测和对比。为了解决这些复杂的问题,我们在唤端技术上进行了又一次升级,建设了可定制的唤端策略,打造了详细的唤端AB测试链路。从本次双11 的效果看,不同场景下的唤端效率(唤端成功率)相对提升了 25~40%不等。 图:唤端策略图 智能UI 随着移动互联网和推荐系统的发展,人和商品的精准匹配为业务带来了效率的大幅提升。越来越多的精细化手段逐渐应用于个性化推荐领域,比如场景化推荐、人群定投技术等。同时商品的信息比以往任何时候都要丰富(买家秀,品牌背书,无忧购服务等),不同的用户对于内容的UI表达有着差异化的诉求,因此通过为不同人群找到合适的UI表达一定能带来业务效果的提升。 项目的最早期,我们通过AB实验直接定量测试,明确了相同的UI方案在不同的场景会产生差异,不同的UI方案在相同场景下也会产生差异。也就是说,针对不同场景使用不同方案是有意义的。2020年双11大促我们第一次大规模采用智能UI产品化方案落地了多个前端模块,包括猜你喜欢模块、商品模块、店铺模块等,覆盖了双11的预售和正式开卖阶段,承受了流量洪峰的考验,且带来了稳定的增长。覆盖300多个会场,最高的会场PV点击率相对提升10%+。 技术升级 伴随业界的技术演进和业务的发展,我们在技术上相比去年也有了新的尝试和迭代升级,其中典型的包括FaaS的深度使用、PHA渐进式的体验增强、边缘节点渲染的应用等。 ▐ FaaS Serverless,一块深水的坚冰,逐步从深海付出了水面,阿里淘系从去年在大促实践开始,逐渐将 Serverless 应用到前端领域方方面面。今年双11首先是在覆盖场景方面,FaaS从淘宝行业拓展到会场和营销业务,业务的复杂度得到极大的丰富。能力进一步提升,支撑的业务量级也从2k QPS提升到5W QPS,CPU水位从去年的高 QPS 规模时,精力花费降低约50%。在研发体验方面,打造解决方案体系,单元保障、大促管控、专家系统、函数盘点等能力,运维提效约50%。在研发体验方面,打造解决方案体系,降低研发门槛,支持外包快速入场。 ▐ PHA PHA 全称 Progressive Hybrid App,是提升 Hybrid 体验的一种应用框架。提升页面加载速度和交互体验的渐进式 Web 应用,使用 PHA 开发的应用本质上没有脱离前端开发和 W3C 标准,但依然拥有原生应用的特性和体验。或许你有想到 PWA,但 PHA 有比 PWA 更强的 UI 能力和更快的加载速度。目前已经在手淘、特价版、Lazada、CBU 等多个客户端落地,支持了618、双11等多个大促。PHA联合客户端、前端团队、数据分析团队,跨栈协同,在性能优化方向上也做了很多优化工作,梳理全链路性能埋点、定义新的性能口径(从用户点击到可视),使用了预加载、预渲染、资源加速下载、离线资源等优化手段。 ▐ ESR 现在的渲染节点主要是在终端或是服务端,对应CSR(Client Sider Rendering)和SSR(Server Side Rendering),分别有适用的场景以及优势和弊端。现在借助阿里云的能力可将渲染转移到CDN节点,也就是我们要介绍的ESR(Edge Side Rendering),即能为前端提供渲染能力,同时也能将大量CDN机器上的计算资源利用起来。 阿里云推出了CDN轻量编程环境——EdgeRoutine,这为我们提供了一个新的尝试方向。我们可以在CDN节点去做提前渲染的事情。CDN的访问策略是会去寻找离用户最近的节点,就像快递运输的最后一公里一样,总会派送到离客户最近的分拨点。这么看来页面的网络调度时长是非常有优化空间的。并且我们还可以利用CDN节点资源共享的特性,将部分数据缓存到CDN节点上,减少远程的数据请求。 这套方案对于数据刷新率不高、访问量极大的页面,ESR搭配CDN的缓存能力是非常适合用的。以达人页为例,首屏时间约能提升50%左右。现在ER的技术才刚刚起步,应用场景比较局限,能力上还有很多不足,体系也需要不断地建设,但这个新技术为前端提供了更多可能,需要我们不停的去探索和完善。 ▐ 双11 PM 初体验 双11作为电商年度最核心的节日,各方面投入的力度和资源都是最大的。作为参加过8次双11的老兵,作为前端PM是第一次,有很多不一样的感受。 复杂:首先是业务上,有双11定制和特有的主会场、主互动、猫晚等,还有淘系内部本身就有导购、行业、营销、直播等数十个业务,同时联动支付宝、优酷、本地生活、阿里妈妈、菜鸟等多个集团BU,与商家、ISV、物流、媒体等的协同和合作。技术上同样复杂,前端的页面从开发、搭建、源站、CDN的全部链路,以及Node FaaS的容器、中间件、容量准备、流量调配、机房部署等。管中窥豹,对于整个体系的认知还需要更进一步的探索。 流程:双11作为电商业务每年的大考,已经摸索出一套成熟的流程机制。包括人员的组成、沟通机制、时间排期、组织保障等各个方面都有很细致的机制进行保障。 协同:双11是非常好的节点,可以让各团队、各岗位、各BU之间形成联动,集中力量将如此庞大的体系进一步完善。很多技术的升级和突破都是在双11落地并进一步推广的。这次预渲染的方案就是客户端和前端紧密协同,在很短的时间内实现并验证的。 多维:看问题的视角可以更多维,不同技术岗位视角,全链路视角,业务的视角。以一次变更的审批为例,之前更多关注变更的代码实现上,对上下游的影响、对稳定性的影响、对业务的影响、是否引入新的风险、影响的范围等等都需要进行综合衡量,做一个判断往往需要从中进行取舍,而不单单是技术上的1和0。 招兵买马 最后的最后,招聘贴! 淘系前端由淘宝前端、天猫前端、手淘前端三大前端团队融合而成,在业务上负责淘宝、天猫的所有业务,如:双11&双12大促、聚划算、天猫新品、有好货等营销导购产品、淘宝直播&短视频业务、商业千牛以及开发、用户增长、互动&游戏等等,囊括了新零售域下最复杂、最多形态的业务场景;在技术上在前端工程、多端架构、Node架构、互动架构等基础体系上有着深厚的沉淀,同时在多媒体、前端智能化、云手机等新兴体系上布局&发展,在搭建&投放、小程序开放、工作台等应用体系上直接助力业务。 网址:https://fed.taobao.org/ 邮箱:taobao-fed-zhaopin@list.alibaba-inc.com 职位:前端开发专家-杭州/北京、端架构 TL、前端技术专家(IDE方向)、前端技术专家(Node.JS)、互动技术专家、Web多媒体领域专家-杭州/广州、云手机解决方案架构师、中后台领域架构师、用户增长领域专家、投放技术高级专家、软硬件技术专家、开发者平台产品经理。
你是否想过,1秒之内,电商直播间里会发生什么? 阿里巴巴集团副总裁、双11集团技术总指挥汤兴透露,2020年双11期间,淘宝直播单个直播间同时在线人数超过200万人,“这意味着,我们能够在1秒之内,把主播的声音和画面,以及商品信息,同步给分布在全国各地的200万消费者。” 1秒要做到极致体验,不仅是对技术极限的追求,更关乎以直播为代表的内容电商成败,正如汤兴在近日媒体会上所提出的问题:“当消费者听到主播说秒杀的时候,别人已经把商品抢光了,他会是什么感受?”这1秒在此刻变得无比重要。 因此,平台必须在1秒内将主播声音、画面和商品信息同步给分布范围极广的百万级消费者,确保后者获得一致的、实时的、高水平的音视频体验,以及商品交易(尤其是秒杀)的可信度。与此同时,同样的问题也在进入直播间、完成购买交易、抢红包/点赞为代表的大规模实时高并行度的内容电商互动中存在。 你是否想过,1秒之内,电商直播间里会发生什么? 阿里巴巴集团副总裁、双11集团技术总指挥汤兴透露,2020年双11期间,淘宝直播单个直播间同时在线人数超过200万人,“这意味着,我们能够在1秒之内,把主播的声音和画面,以及商品信息,同步给分布在全国各地的200万消费者。” 1秒要做到极致体验,不仅是对技术极限的追求,更关乎以直播为代表的内容电商成败,正如汤兴在近日媒体会上所提出的问题:“当消费者听到主播说秒杀的时候,别人已经把商品抢光了,他会是什么感受?”这1秒在此刻变得无比重要。 因此,平台必须在1秒内将主播声音、画面和商品信息同步给分布范围极广的百万级消费者,确保后者获得一致的、实时的、高水平的音视频体验,以及商品交易(尤其是秒杀)的可信度。与此同时,同样的问题也在进入直播间、完成购买交易、抢红包/点赞为代表的大规模实时高并行度的内容电商互动中存在。 阿里巴巴集团副总裁 、 双11集团技术总指挥 汤兴 汤兴:直播间里的每1秒都是技术人的骄傲 据汤兴介绍,为了进一步优化消费者体验,降低延时,淘系技术非常重视在音视频技术领域的投入,涉及领域包括音视频通话、低延迟直播、S265编解码器、实时智能调度、自学习参与体系等多个方面,特别是在2020年双11 将GRTN 新一代多媒体传输网络“首次大规模应用于直播电商带货”。 GRTN由淘系技术联合阿里云等阿里巴巴经济体内技术团队共建,旨在通过对传输网络层的优化从而降低直播延时。目前将直播延时由过去的3-5秒降低到1秒以内,极低的互动延时带来了直播内容和互动体验上质的提升,在消费者和主播之间形成了更好的互动效果。 “在构建底层音视频技术的能力底座的同时,淘系技术同时在创造内容电商领域极为重要的智能引擎,通过算法为消费者、商家、商品和主播构建精准理解、智能匹配的四方关系。”汤兴谈到。 他表示,传统的电商算法只考虑商品和消费者、商家和消费者之间的关系,并彼此割裂,但淘系技术团队构建的智能引擎,包括了消费者与内容匹配、 内容与商品匹配、商品与商家理解、主播(达人)与商品匹配的多维度关系,“实现人货场和主播之间匹配效率的提升,提高了内容电商场,尤其是直播电商带货的效率,也让消费者买到最心动的商品。” 体验、智能、互动、质量是内容电商主要发展方向 汤兴认为,对于商家和消费者来说,双11是全球超大规模内容电商场的全面爆发,但对于技术人来说,它是一个融合了导购、交易、内容、互动的综合技术场,实时性、复杂性、高峰值的叠加驱动其同时成为全球内容电商场的技术顶峰,而每1秒的都能提供优质体验,是技术人的骄傲。 最后,在提到内容电商的趋势时,汤兴认为:“未来五年,所有的内容场都会商业化,所有的商业场都会内容化。”淘系技术正在利用支撑图文、视频、直播等业务的内容中台,支撑阿里巴巴经济体向内容化方向发展的需求和趋势。 阿里巴巴集团副总裁 、 双11集团技术总指挥 汤兴 汤兴:直播间里的每1秒都是技术人的骄傲 据汤兴介绍,为了进一步优化消费者体验,降低延时,淘系技术非常重视在音视频技术领域的投入,涉及领域包括音视频通话、低延迟直播、S265编解码器、实时智能调度、自学习参与体系等多个方面,特别是在2020年双11 将GRTN 新一代多媒体传输网络“首次大规模应用于直播电商带货”。 GRTN由淘系技术联合阿里云等阿里巴巴经济体内技术团队共建,旨在通过对传输网络层的优化从而降低直播延时。目前将直播延时由过去的3-5秒降低到1秒以内,极低的互动延时带来了直播内容和互动体验上质的提升,在消费者和主播之间形成了更好的互动效果。 “在构建底层音视频技术的能力底座的同时,淘系技术同时在创造内容电商领域极为重要的智能引擎,通过算法为消费者、商家、商品和主播构建精准理解、智能匹配的四方关系。”汤兴谈到。 他表示,传统的电商算法只考虑商品和消费者、商家和消费者之间的关系,并彼此割裂,但淘系技术团队构建的智能引擎,包括了消费者与内容匹配、 内容与商品匹配、商品与商家理解、主播(达人)与商品匹配的多维度关系,“实现人货场和主播之间匹配效率的提升,提高了内容电商场,尤其是直播电商带货的效率,也让消费者买到最心动的商品。” 体验、智能、互动、质量是内容电商主要发展方向 汤兴认为,对于商家和消费者来说,双11是全球超大规模内容电商场的全面爆发,但对于技术人来说,它是一个融合了导购、交易、内容、互动的综合技术场,实时性、复杂性、高峰值的叠加驱动其同时成为全球内容电商场的技术顶峰,而每1秒的都能提供优质体验,是技术人的骄傲。 最后,在提到内容电商的趋势时,汤兴认为:“未来五年,所有的内容场都会商业化,所有的商业场都会内容化。”淘系技术正在利用支撑图文、视频、直播等业务的内容中台,支撑阿里巴巴经济体向内容化方向发展的需求和趋势。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
“ 今年的双11是全球极大内容电商场的超级爆发,消费者、技术、内容与商业生态之间每一秒都在产生激烈共振,实时性、复杂性和持续峰值的叠加令其成为全球技术顶峰。我们的使命就是让技术成为双11坚如磐石的稳态,让消费者感受到更顺滑的体验。” 汤兴说。 阿里巴巴副总裁 双11技术总指挥 汤兴(平畴) 今年是第12个双11,作为双11的新一轮的元年,不仅仅是每年最大的购物节狂欢节,更是2020年最大的一次内需爆发。4982亿,双11再创记录。这背后是阿里巴巴经济体的数字化建设支撑了全社会的双11,支撑了如此大体量的双节棍。快跟着小编来了解下今年双11最新最快的核心技术解读。 淘宝直播,缔造全球内容电商场的技术顶峰 “今年天猫双11是最为特殊的一年。”淘宝天猫总裁蒋凡说,作为疫情之后全球最大的消费季,很多商家对“双11”有更强的期待。从10月21日,淘宝直播强势开启今年双11的预售环节,淘宝直播俨然成为商家在内容营销媒体上的第一选择。那从技术上,淘系直播技术系统如何做能够支持在线高并发,同时还能保障主播和用户之间的互动实时性、交易的准确性? ▐ 淘系自研全球实时传输网络——GRTN GRTN是新一代多媒体传输网络,实现了流量、算力的智能调度,通过大数据计算,汇聚CDN及客户端数据,采用自动化控制理论,自动控制CDN带宽。同时淘宝直播的内容生产实现了端+云智能调度能力,依据端的性能、云的容量,动态调整生产位置,降低资源成本,保障了系统稳定性。 相较于其他直播平台的3~5秒延迟,GRTN让淘宝直播的延时控制在1秒以内。今年双11是GRTN首次大规模应用于淘宝直播电商,极低的互动延时带来互动体验的质的提升。从而实现在千万级在线并发情况下,仍能够让主播自信的喊出“123,开抢!” 同时,基于GRTN动态容量伸缩的特性,保障了我们在直播带宽消耗快速增长的同时,成本同步下降30%。 淘系内容中台服务阿里巴巴集团10+业务 ▐ 端侧智能助力直播创新玩法 随着手机端侧算力迅速发展,极大的提升了直播内容的交互可玩性和内容理解准确性。依托于淘系自研并开源的MNN端侧推理引擎,淘宝直播间推出 “语音猜价格”挑战,观众在直播间也能实现语音交互,动动嘴就可以响应主播发出的猜产品价格任务。 淘宝直播的观众遍布全国,口音差异较大,对识别算法提出了很高的要求;同时,如果使用Siri等基于云端的语音识别技术,那本就无法满足直播场景强互动的识别时延,在一场直播动辄百万、千万的观众数量面前,将会雪上加霜,端侧识别引擎同样面临巨大挑战。为了实现实时语音交互,此次淘宝直播采用了全新的移动端语音识别方案。基于全新的网络结构,快语速、重口音等问题,高难度场景中的语音识别错误率降低近三成;同时,推理引擎MNN也针对语音识别模型做了重点优化,达成50毫秒内完成语音交互过程,速度高达传统语音识别的20倍。 平台与商家共同完成数智化运营 ▐ 数字化新基建成就史上最大规模商家全链路压测 以往由于平台和商家IT基础设施存在“水位差”,平台很难帮助商家进行系统的改造升级。随着阿里面向商家与生态伙伴推出新一代数字化基建体系,借助云原生技术引擎、可持续全链路压测与云IT治理能力等平台成熟的技术,帮助商家与生态伙伴重构系统的高可用基线,使得越来越多的商家具备了和阿里一样的互联网级超大规模数据应用系统能力。 在双11备战期间,淘系技术团队通过帮助100多家ERP服务商与品牌商家实施订单洪峰全链路压测,使得商家系统每秒能处理的订单规模上万,这在传统模式下是完全无法想象的;通过向商家与生态伙伴提供新的IT治理手段与工具,帮助商家系统发现并修复上千个数据访问性能问题;在购后履约阶段,通过大数据技术应用,让今年双11每一笔订单都能更为有效地对接物流和仓储调度,提升消费者收件体验和时效的同时,帮助商家进一步降低了发货成本,发货效率提升20%以上;以上一系列的技术升级,赋能百万商家,覆盖90%以上的双11订单,为今年双11核心交易链路的平稳运行保驾护航。 在过去,我们将阿里的全链路压测称为大促备战的“秘密武器”,放眼未来,阿里巴巴通过电商与云原生技术的进一步融合,将更好形成与百万商家的数据化实时协同,为实现真正的线上线下零售一体化、智能化经营,奠定了良好的系统基础。 ▐ 智能新品孵化,助力商家快速增长 新品作为每个品牌/商家的增长核心驱动力之一,在过去,传统新品的研发主要痛点有两个:新品的研发周期长,新品的研发成功率低。目前在天猫上,已经有超过1600多家品牌商,通过新品平台来尝试进行趋势、测款、试销、量产、诊断、引爆等全流程智能化。整体的新品研制流程能比过去缩短60%。 淘系新品平台通过整合业界趋势生态机构和时尚媒体,结合阿里巴巴平台海量数据,将抽象的趋势语言结构化和数字化。并采用NLP新词发现,图像识别等智能技术,为趋势构建了体系化的识别和预测模型,快准全的跟踪和预测未来趋势。在此基础上,构建完整的市场分析和评估体系,将趋势转化为生意机会。与品牌商共建品牌知识库,并通过线上的精准人群测款调研能力、新品试销诊断,帮助品牌商快速调整新品策略,赋能商家新品研发打造极致爆款。以产品化+ISV的方式赋能行业和商家,提升其对新品开发的确定性,降低试错成本。 淘系技术投入建设新品平台已有两年多,致力于构建从研发新品到打爆新品的一体化新品技术体系,也已经促成犀牛智造、躺平智造等柔性制造模式更快落地。 3D场景购“利器”助推线上家装业爆发 ▐ 我们“盖”了一栋100层的大楼 这个双11,天猫盖了一座100层的3D虚拟家装城,包含几万套家装样板间,可以360度无死角呈现家居全景。消费者躺在家里,点击手机屏幕就能挑选自己喜爱的家装产品,体验实景逛街的感觉。与此同时,还可以自主搭配、一键购物。当然,还可同步享受品牌商家双11的优惠促销价格,以及线下门店的售后服务。 在这座100层大楼应用了5G、VR、AR技术的应用,驱动3D样板间2.0的体验升级。 在双11前夕,淘系技术联合躺平设计家共同研发并推出了AI智能样板间。在11月3日的技术沟通会上,双11技术总指挥汤兴说:“通过AI智能样板间,可以帮助商家可以在3秒钟之内,设计出一套3D样板间,仅仅在今年双11期间,就为客户节省了近1亿元设计费。” AI智能样板间通过不断学习前期积累的大量优秀设计案例,能将最新潮前沿的设计方案呈现给消费者。值得一提的是,智能样板间与淘宝主流量渠道进行了打通,能够为商家带来精准流量,通过大数据技术,实现对投放设计的实时追踪,及时感知消费偏好,实现千人千面投放。 ▐ 3D样板间虚拟直播,碰撞新火花 同时今年双11,“3D样板间+直播”再升级,首次采用绿幕技术,帮助商家和主播在虚拟的样板间中进行直播。这背后则是淘系技术与躺平设计家在3D购物技术领域带来的新突破,即—将3D样板间技术与绿幕直播技术进行融合,使消费者在观看主播讲解时,仿佛置身于实景样板间之中。 主播在前面讲解,可以利用绿幕“抠像”技术换成3D样板间的背景。如此,置身绿幕的主播,在直播镜头里,却仿佛置身在实景样板间中。而绿幕技术和3D样板间加持后,主播可以随时切换不同的场景,瞬间“平移”至其他样板间,为商家大大节约了成本。 左图直播背景是绿幕,右图观众视角的背景是3D样板间 仅11月1日,就有上万名消费者进入商家直播间,体验在线3D样板间虚拟直播的新形式。在此期间,用户平均逛样板间的时长达到42秒,单个UV的价值较日常提升5倍多。 技术君说: 每一年的双11,零点交易的系统数字都在不断提升,而淘系技术的突破和创新远不止与此,我们将持续打造全球领先的线上新零售技术平台。
任何一款开源软件都不会是完美的。 使用过程中遇到问题,有的人会说,这是语言本身的限制;有的人会寻求临时性的解决方案;还有的索性换个技术栈。 他不一样,打从2013年开始,只要是他遇到的库,使用过程中遇到问题,他会执着地去社区提 pull request 。 一点一滴,就像一个拿着刻刀的工匠,逐渐打磨出完善精密的形状。 那年被提名为 Node.js Colloborator 的时候,他还只是个创业公司的小码农。 如今,他已是前端智能化开源项目 Pipcook 的负责人。 雷姆表示:“前端是一群非常有想象力的开发者,我希望能更多帮助他们发挥这种能力。” ▐ 游戏、留级与编程:误打误撞的前端 点进他的 GitHub 首页,赫然会被头像里那个笑得谦逊而温柔的动漫女孩吸引,浅蓝色的头发轻柔地飘扬开,仿佛给整个博客页面吹过一阵轻风。 那是他最喜欢的动漫角色,雷姆。也是他在阿里的花名,寓意着坚强和温柔。 成为一名前端工程师,是他从未想过的。 和天才少年从小接触电脑和代码的套路不同,雷姆是学无机非金属材料与工程的,从大二起,他无心上课,沉迷游戏,甚至因为学分不够被留级了。从08级1班降到09级1班,以往甚少管学习的父母也急的展开了促膝长谈。 故事依然没有走向 GPA 爆表逆袭的一面。 雷姆喜欢上一个女孩,女孩想做一个网站开店。于是他开始自学 flash,然后学 DW,学 ActionScript 和 jQuery,学着学着有点上头,他就把做网站的事情给忘了…… 后来,他就做了前端。 ▐ 左手公司,右手社区:不要给自己设限 雷姆的第一份工作在北京一家创业公司,去了之后才发现并不是前端岗位,主要职责是用 Node.js 写服务器,做邮件推送系统。 创业公司比较辛苦,变化也多,遇到的困难超出自己的领域是很常见的事情。 反而是在社区里的雷姆,可以专心致志写自己喜欢的技术,所获得的认同感和成就感远高于工作。 雷姆所负责的项目,上线第一天由于缺少严谨的测试和审核流程,在用户端出现了比较严重的问题。“我觉得无论作为开发,还是测试,仿佛是墨菲定律的实践典范,你觉得不会发生的,就真的会来。” 正逢沮丧的雷姆,机缘巧合在微博上认识了一位年龄 50+,在银行做账务类工作的老师。这位老师业余爱好编程,同样的工作,别人做 6 个小时,他自己通过编程的方式 2 个小时就能完成,从而有更多时间去学习更多的东西。 “从他的经历,我感觉到平时的抱怨是自己没有真的想方设法去解决自己的问题。” 遇到超出范围的问题的时候,不要用“我只是一个前端”来逃避。单纯地聚焦在这个问题上,从0开始看它,不给自己设限,随时准备好在在编程领域学新的东西,慢慢地,就会越懂越多。 这是雷姆最早学会的程序员职场成长法则。 与此同时,他从未停歇在 GitHub、知乎等地的输出和耕耘,作为 Node.js Collaborator 的贡献者,他收获了2.2k 深度用户粉丝,是大家心中低调的大神。 (雷姆式习惯谦虚:不不不,我不是大神) ▐ “做一件有利于所有前端工程师的事儿” 雷姆的职业生涯兜兜转转,他曾抱着“一边写码、一边旅游”的梦想做了兼职外包工程师,却因为旅游区 WIFI 和位置不好容易断网,又回归了全职;他曾因为“丈母娘喜欢大公司名气”来到了阿里巴巴机器人公司,又由于项目终止和转岗,做出了新的职业选择。 “那时候,我不知道自己应该做什么。”离开了阿里,这是雷姆一直在思考的事情。 这种困惑并不是他要不要成为一个前端,而是成为一个前端是为了什么。 技术并非是一种冷漠的推动生产力的力量,它不仅解决了时代下的生产效率问题,财务利润问题等,对于技术背后的个体而言,他们每推进的一行代码,是如何与自身追求的理想价值相关联的,这是技术背后的动力和温度。 一开始,雷姆只是把前端当做一份工作。渐渐地,他真真切切地在 coding 的领域收获了乐趣。无论多晚下班,他每天都会花时间泡在社区和开发者交流。 “我发现,国内外的前端都是一群非常有想象力的开发者。因为前端的技术相对比较轻,更容易让开发者实现自己想做的东西。” 彼时,谷歌的深度学习框架 tensorflow 正在流行,雷姆尝试着在社区写一个 Node.js 的版本。尽管这个版本的 tensorflow-nodejs 只能做预测使用,但依然引起了 Node.js 创始人 Ry 以及 jQuery 的作者等不少大牛的关注。 但由于 tensorflow 胶水层的代码是用 Python写的,越往深,几乎全部沉淀在 Python 这边。雷姆发现,这样写下去,几乎等同于重造,只好搁置了。 2 年后,雷姆机缘巧合认识了阿里淘系的甄子老师,甄子提出,“能不能直接从 JS 调用到 Python ?” 听到这个想法的雷姆瞬间进入了亢奋状态,当晚便开始自己的捣鼓尝试。 “越写越觉得正确,真的可以把这个生态打通!” 认同甄子老师理念的雷姆,再次回到了阿里,加入了 FX Team,一支致力于探索和实践前端智能化领域的队伍。 雷姆:左二 这一次,他已经非常知道自己的方向了。 “这就是我想做的事情,我希望架桥,我希望降低前端开发者的学习成本,以更低的门槛,去最大化地释放他们的想象力。” ▐ 开源项目 Pipcook —— “前端工程师的智能化” 知乎上有人质疑,前端非要和 AI 绑定在一起,纯属异想天开的炫技。 “前端要和市场沟通,它是主观的、动态的,非要搞个什么智能化,就变得简单而机械了。” 不同于理所当然可以智能化的后端,在这个新概念的领域里,大部分人都无法理解,也觉得无法落地。 对于雷姆和开源项目 Pipcook 而言,我们不是做前端的智能化,而是前端工程师的智能化。 “它不是说这个前端页面里有多少智能化的东西在里面,而是我们国家有多少人有使用机器学习去解决问题的能力,Pipcook 就是为此而存在的。” 前端社区可以通过复用 Python,来补充自己生态中的不足,实现从前端工程师向机器学习工程师的转型的第一步。它是一座桥,连接了前端和机器学习。就像雷姆最喜欢的 Node.js 当年,为前端工程师搭起了一座通往服务端的桥。 现在的 Pipcook,依然不是最终最完美的连接方式,雷姆和他团队的伙伴们依然在不断尝试,突破新的思考角度,突破新的编程方式,希望找到最适合前端进入机器学习的方式。 而前端智能化的落地场景,并不诞生于开源技术的圈定和规划,而是社区所有充满了想象力和创造力的前端开发者,将会如何利用这样新的思考问题的角度,把二者生态连接起来,以更有效的方式,去解决更多的问题。 “就是这种慢慢找到目的和答案的感觉,让我越来越兴奋。” 除了 Pipcook 的项目,雷姆在阿里淘系最有价值的收获,是找到了技术的立足点。 在社区,技术要受欢迎,要黑科技,要酷,要的是大呼精妙的过瘾和爽感;在淘系,由于业务场景的复杂和丰富,有时候雷姆从技术角度出发觉得理所当然要做的 ABCD 4件事,会被甄子老师一遍又一遍问,目的是什么,出发点在哪里,如何让业务提效或者减少人力…… 技术不再是一颗螺丝钉,雷姆开始学着用体系化的视角去看待产品和技术问题,这种将思考的珍珠串成连贯线路的感觉,让雷姆觉得自己的代码不飘了,有了真切的立足点,是他工作中另一个亢奋的源泉。 ▐ 结语 聊起社区和技术的时候,雷姆的语速总是不自觉地加快,气息里都是神采飞扬。 聊起生活里的琐碎,雷姆习惯性慢半拍,歪着脑袋想好一会儿才能依稀记起那些时间和地点。 一如他喜欢的动漫角色,妖尾里的纳兹,灼眼的夏娜,他们都是有着清晰理想目标的元气少年少女,雷姆将自己满满的元气和干劲儿交给了 coding ,这样的幸福无可比拟。 他的爱、细心和浪漫同样交给了 coding 。他曾经将自己的名字和夫人的名字(Yorkie & Babeee),悄悄咪咪写进测试用例里,作为一种镌刻,长长久久将名字留在了 GitHub 里,被无数志同道合的人不经意阅读与祝福。 “真正的工程师,内心应该是开放而包容的,他们不会纠结于某些概念学不动、要不要学,而是通过实践和参与,取百家之所长,让自己的代码或者软件,能烙下历史的烙印,无悔于工程师三个字。” 这是雷姆对 coding 的尊重和执着。 雷姆的 GitHub :https://github.com/yorkie 附:Pipcook 项目介绍 1.项目地址:https://github.com/alibaba/pipcook 2.项目介绍 可实现什么:提供前端可用的视觉和 NLP 能力 可用于哪些场景:Design2Code 运行环境:mac、linux 开发语言:JavaScript、Python 开源协议:Apache 2.0 架构图:https://github.com/alibaba/pipcook/issues/30 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
前言 目前5G新基建正在迈入起航阶段,5G相比目前广泛使用的4G,它具有更高的速率,更大的容量,同时延迟更低,可靠性更高。在5G时代,视频得益于网络带宽的提升,未来将成为主流的传播媒介。越来越多的业务和应用将视频化,直播化。大量互动的内容将通过5G以低延时的方式以视频的形式传输。 5G将对视频分辨率和清晰度提出越来越高的要求。淘宝作为一个数亿级用户的短视频与直播平台,业务多样,两端用户分布广,设备和网络情况复杂,给多媒体内容存储和分发带来巨大挑战。在内容生产过程中把控好质量和成本,在内容分发和消费过程中确保用户体验,是当前面临的主要问题。为了解决这个问题,我们有两个优化目标,一是在画质不变的前提下降码率,二是在码率不变的前提下提升画面质量。 淘宝直播高清低延时系统架构 在降码率上,我们自研高效编码器,升级播放架构,添加智能ROI,场景编码,智能码控等工具,有效地降低了视频码率带宽。在这些技术中,高效的编码器能够在质量不变的前提下显著降低码率;场景编码能够根据不同的画面内容配置合适的编码参数;ROI挑出画面中人眼比较关注的区域交给编码器重点编码;智能码控根据人眼主观特性,消除因为超过人眼阈值而浪费的码字。 在画质上,我们使用前处理增强,去噪,超分高动态范围等算法提高生产内容的观感质量;在体验优化上,通过低延时编码技术,在降低了编码延迟的同时损失很小的码率,增加观众和主播的体验。 围绕着提高问题发现、问题处理效率的出发点,具备数据采集、存储、异常事件收集、智能告警、告警数据运营、可编码诊断平台、故障自动化处理、变更联动等能力。我们搭建了一套基于淘宝直播的全链路监控体系,从音频,视频,网络这三个方面入手去解决目前淘宝直播全链路的现有问题以及将来可能出现的问题。不断去优化整套高画质低延时系统。 与此同时,我们建立了客观质量和主观质量评价体系,采用vmaf,psnr,ssim这一系列的指标作为客观质量评价。针对海量无源场景,我们还基于cnn建立了无源评价模型,保证无源场景下质量评价的准确性。以这些有效的评价手段来确保“画质不变”,并监控线上视频质量。 端上窄带高清 ▐ S265编码器 带宽成本是视频服务中非常重的基础设施成本,如何在保证视频质量的前提下降低成本是整个链路中至关重要的一环。相机采集到的视频数字信号,数据带宽通常都非常高,以720p 25fps为例,带宽高达263.67Mbps,很难存储和发送。好在视频图像内部,帧与帧之间存在非常高的相关性,采用压缩技术去除相关性后,可以将带宽降低到原来的100-400倍。视频压缩标准主要由ISO(国际标准组织)制定的MPEG系列和ITU(国际电信联盟)主导的H.26X系列,每隔十年时间,视频压缩标准升级带来的压缩率会提升一倍。 h265作为比h264更新一代的视频压缩标准,提供了更灵活的编码结构和划分方式,并在在运动补偿、运动矢量预测、帧内预测、变换、去块滤波、熵编码等方面进行了大量改进与优化,得益于这些新的编码工具和特色技术,相同画质下最高可以比H.264节省一半码率,为了在不牺牲画质的前提下节省码率,h265成为我们首选的编码标准。 Ali265是淘宝自研的高性能H.265编码器,对比业界开源的X265可实现BDrate20%以上的增益,对比X264则有40%以上的增益。目前已在淘宝直播,优酷视频,阿里郎会议,VMate,UC云盘等业务中上线使用。 Ali265 JCTVC class B~F sequence 淘宝直播技术团队联合阿里云团队开发了s265编码器,对比业界常用的开源软件X265,1pass单遍编码在相同psnr指标下: veryslow速度档次有28%码率节省。 medium速度档次有36%码率节省。 crf模式与abr模式节约的码率接近。 S265编码从码率控制,编码工具两个方向优化编码质量,并从快速算法,工程算法两方面引入速度优化算法。 码率控制 为了进一步提高压缩质量,在编码器框架标准一定的情况下,编码器算法优化主要的方向是找到策略选择出最优的编码方法和编码参数,从而获得更好的码率节约。合理分配码率是编码器的一个重要工作,码率控制的目标是把码字分配到更有价值的地方,从而在目标码率下使得编码失真降到最低,或者在失真固定的前提下使码率降到最低。码控需要解决两个经典问题,一是帧级码控和块级码控根据目标码率来分配每个GOP、帧、编码块的码字数量,二是块内编码时以最合理的方式把这些码字分配到每个编码块中。 在帧级别码控中,传统方法统计所有已编码帧的长期复杂度,根据长期复杂度及当前码率之间的比例计算出QP,这样一来,QP对帧复杂度越来越不敏感,导致编码质量下降或码率过剩。特别是在计算首帧qp时,以往算法采用了一个只和当前码率有关的经验值。我们基于cutree理论准确估计预分析长度中ipb帧的码率占比和预期编码大小,从而在编码前获得更准确的量化系数。 块级码控分配则受时域cutree和空域AQ影响。在时域上IBP帧的重要性是明显不同的,被后续帧参考的块,不仅影响自已本身的质量,还会影响到后续帧的质量,因此被参考更多的块需要进行高质量编码。cutree算法根据帧内预测代价和帧间预测代价计算信息的传递比例,算出当前块对后续序列的影响程度,进而调整qp偏移。但考虑到在不同的噪声能量,运动强度,纹理边缘强度,以及编码参数下,不同参考块的调节为后续帧的节约比例是不一样的, 所以s265通过参数训练的方法,获得多个因素对传递效率的影响,得到一个更准确的信息传递比递,从而更合理地在时域上分配码率。 cutree传递过程 另一方面,空域上各块之间的重要程度也是不一样的。人眼是视频的最终观察者,从人类视觉系统出发,不同的块在人眼中的视觉冗余不相同,比如人眼存在视觉掩蔽效果,它对显著纹理和强边缘附近的噪声不敏感,将码率更多分配向人眼敏感的平坦区域,可以得到更好的主观质量。在编码器中,我们通过计算块的方差能量及边缘能量作为块的代价,研究不同块能量和人眼感知程度之间的关系,估计出块间码率配分对人眼注意力的影响,合理分配码率到更重要的纹理块,提高视频感知编码效率。 编码工具 在编码工具上,S265对传统的场景切换检测,帧类型决策,SAO,DEBLOCK,两遍编码,RDOQ等编码工具算法做了改进,并实现一批编码工具。 比如在参考帧模块,有较多的工具可以提高参考效率。首先长期参考帧和广义B帧等帧类型可以提高预测质量,长期参考帧针对背景很少发生变化的直播场景,它有效减少信息经过多帧传递带来的损失,引用长期参考帧可将平均EV提高大概0.25dB。而传统P帧改为广义b帧,采用双向预测取代单向预测从而降低噪声,光照变化,采样误差等预测残差源。 在扩充了帧类型后,我们基于参考强度做IBP帧帧类型决策。然后在minigop内部,我们使用金字塔结构的参考关系,得到比传统结构获得更短的参考距离。最后,在管理和选择参考帧时,我们考虑到静止块和运动块的区别,静止块倾向于参考质量高的帧,运动块倾向于参考时间近的帧,所以针对场景筛选出这两种类型的参考帧能得到更好的参考质量。 速度优化 HEVC编码器带来了编码效率的提升,但很多新的编码工具都存在计算复杂度过高的问题。因此,优化编码器速度,在高端机上能打开更多的编码工具,搜索更大的编码模式空间。进一步提升编码质量。在低端机上则能降低CPU发烫和编码卡顿的现象。 HEVC可以将图像块从64x64划分到4x4,同时块的类型模式激增,备选的编码模式数量是h264的数倍,块划分及模式决策因此成为一个重要的瓶颈。 所以在RDO中,减少CU划分层级的搜索次数,筛选出一些必要的层级是减少计算量的重要手段。首先利用时间和空间相关性,可以从参考块获取到一些先验信息,再结合本块的运动信息和纹理信息,分析预判出当前块CU层级的最大估计层级和最小估计层级。其次,在决策过程中的提前跳出策略也可以大幅降低计算量,我们根据图像纹理的平坦程度,或者各种模式下的rdcost对比,提前跳出当前的模式遍历。而在一些图像非线性的场景,我们通过CNN深度学习模型辅助决策模式。 进入决策模块的内部,同样存在大量复杂的计算。帧内预测存在35种模式,我们可以通过贝叶斯理论,求出最简单的几种模式后,估计出最佳模式最可能出现的位置,从而为帧内模式筛选过程提升一倍速度并将损失控制在0.01db。另外,帧间预测的运动搜索是从参考帧寻找最佳匹配块的过程,它的分像素搜索需要做7抽头或者8抽头的插值滤波,计算量很大。我们所以可以利用整像素的信息建立二元二次误差平面方程,估算最佳分像素点的位置,避免了分像素的完整搜索过程。 在评价模式的优劣时通常采用rdcost作为模式的代价,它需要计算编码比特数和编码失真。这就需要将编码系数进行熵编码计算码流长度,同时还要将编码系数变换回时域求失真。为了降低rdcost的计算量,我们采用了失真和码率的线性估计算法,包括两个部分,其一是量化误差能量在频域计算,利用IDCT变换的能量不变性,计算量化余数的平方和估计失真,其二是建立编码系数特征信息和码流大小之间的线性关系,直接从系数特征信息估计出熵编码的大小。通过这个方法可以跳模式代价计算的熵编码过程以及,反变换,反量化,重建,SSE等过程。节约了大量的计算。 在rdo之外,我们还改进了slicetype决策算法,动态拉格朗日因子调整算法,快速deblock和sao决策等。 在工程优化方面我们也添加了多项优化,首先是C函数优化,我们通过优化流程逻辑,拆分特殊路径,合并分支,查表,循环优化等方法给rdoq模块,系数解析,deblock等模块带来了接近一倍的提升;其次针对密集计算的函数我们simd化并优化汇编代码的执行速度。 s265经过快速算法与工程两个层次上的优化,我们为HEVC编码带来了明显的性能提升。从而在低端iphone上实现720P 30帧每秒的实时编码。 ▐ 智能码控 智能码控是淘宝自研的码率控制算法,普通ABR或CBR码率控制为了追求目标码率,在低复杂度场景浪费了大量码率,根据人眼主观质量模型,当psnr高于一定阈值后,再提高质量,人眼无法察觉,只会消耗过多码字。我们使用机器学习方法,根据17种历史编码信息和待编码帧的复杂度,预估出待编码帧在质量阈值以上的量化系数,并限定在ABR目标码率以下,确保每个帧都能以最合适的码率编码。 经过淘宝直播线上验证,可达到15%的省流,在钉钉直播中使用更是节省了52%的带宽并降低了62%的推流侧卡顿。 ▐ 场景编码 由于当前淘宝直播种类的丰富性,各种场景下的纹理,光照,背景,运动程度都是不一样的。户外主播经常走动,画面帧变化幅度频率高。美妆主播大多坐在室内,光照基本上比较偏亮。珠宝类主播主要是拍摄物品,画面多静止不动。面对形形色色的直播场景,单一的编码器配置并不能满足当前淘宝直播的需求,开启或关闭某些编码工具对视频编码效果影响不一致,如何针对内容选择最佳参数成为业界研究的方向。 在此需求下,我们提出了基于不同场景的编码参数配置策略。首先,我们通过多个深度学习与机器学习模型对数万条各种内容的直播视频进行了数据训练分类,包含两个大的特征维度,分别是语义特征和信号特征。语义特征包含:主播分级,商品特征,环境特征,声音特征,时域空域RoI。信号特征包含:运动特征,纹理特征,噪声特征,亮度特征。通过对不同特征种类的视频集,我们单独使用大规模服务器集进行最佳编码参数搜索,自动化高效地搜索到适合当前视频编码的最佳编码参数组合,在提升画质的同时能尽可能地减少码率消耗。并最终根据编码参数集进行聚类分为多个参数配置项。 在主播需要推流的时候,首先进行标准的编码参数配置进行推流。收集一定的数据之后,我们将得到的视频语义特征和信号特征送入自适应决策引擎,通过里面的深度神经网络进行视频分类,决策出当前视频应该下发的编码参数配置,然后我们将新的参数配置重新送入编码器进行新的推流,以此优化使主播获得当前情况下最优质的视频编码。通过此方法,我们在淘宝直播里面获得了7-10%的BDrate收益。在淘拍场景下获得了40%的BDrate收益。 ▐ 低延时编码 在直播中,低时延意味着高效率和优质体验。试想以下场景: 场景一:当主播展示下一个商品后,10秒才收到上一个的商品的提问。 场景二:钉钉课堂直播中,老师提问后迟迟得不到学生的反馈,浪费部分时间。 这些场景给用户带来糟糕的体验,使得直播卖货、直播课堂效率低下。当5G普及,会带来更低的时延,带来更好的体验,但是当下还是4G为主,降低时延有很有必要。 端到端延迟主要分布在采集、编码、传输、转码、分发、播放等各个部分,这部分主要优化编码延迟。编码延迟又分为多线程导致的延迟、缓存帧数延迟、B帧数带来的延迟等。其中编码延迟最大的一部分来源于编码器缓存,通过分析编码前的缓存图像,可以大大的增加编码效率。如果粗暴的降低编码器缓存,可以实现较低的延迟,但是质量损失比较高。所有产生了一种想法,能不能用较少的缓存去模拟较长的缓存的效果?通过分析cutree的原理,结合统计lookahead长度跟传递代价的关系,可以发现缓存长度跟传递代价很强的线性关系,如下图所示: 根据场景可以用不同的预测模型变种,最终实现用较短的lookahead模拟较长的lookahead的效果,测试在直播素材中lookahead4优化后比优化前可以节省13.5%的码率,有效的降低了编码延迟,结果示意图如下。 同时,在之前的测试中发现,该优化对场景不敏感,运动简单场景和运动复杂场景提升同样有效。 过去一年,我们采用前述优化,将265码流在画质不变的前提下,将码率从1.4M下降到800K。 ▐ 画质增强 在淘宝直播的场景中,大主播有自己的专业设备与团队,直播出来的视频与音频都是比较高质量的。但是针对中小主播,用户的行为不可控。因此产生的结果就是很多中小主播产生的视频质量比较低,收获的观众数量也比较少。针对这种情况,我们选取了用户习惯产生最严重的几种情况,对这一类主播进行了画质增加的,显著提升了用户的直播体验。 下面介绍一些已经有的应用效果。 去抖: 现代编码器能够较好的处理平坦纹理和平移运动,前者通过帧内预测来消除空间相关性,后者通过运动搜索来消除帧与帧之间的时间相关性。但是在视频采集过程中,由于摄像机抖动产生的视频帧抖动,编码器不能够很好的处理;由于抖动剧烈的一般是中小主播,且携带的设备比较老旧,我们考虑从采集源来改善视频帧,最终在这里我们采用相机路径平滑算法来去除视频帧中的抖动。 去噪: 视频直播在灯光不太理想的情况下,摄像头采集的画面会产生明显的飞蚊噪声和高斯白噪声,严重影响用户对视频内容的感受,这种情况下,有必要对视频进行降噪。 现有的很多优秀的云端去噪算法,其实对于移动端来说采用深度学习的方法就不合适。虽然现在有很多移动端深度学习框架,但是毕竟还没有跟机器是配得非常好,针对很多中低端的手机其实跑不动这种生成模型的。基于此,我们在移动端主要是考虑效率,那么我们就采了基于维纳滤波的时域降噪算法方式来实现,进行训练和优化。 超分: 针对一些小微主播,录播设备只能支持360p,最终观众端看到的视频会通过插值等传统方法进行放大为720p。这样获得的视频帧难免产生模糊效果,影响直播观感。得益于深度学习在移动端的优化,我们在部分高端机实现了移动端视频帧的实时超分。 在众多的网络架构中,我们最终选择了性能最佳的FSRCN方案,网络的架构图如下所示。在训练过程中,我们精选了1W+淘宝个品类的高清大图,结合业界的高清开源数据集,利用样本增强技术,训练了5000轮左右的模型达到收敛效果。此外,为了消除图像分块带来的边界效应,我们做了图像重叠合并的操作,在增加部分计算时间的情况下带来了更好的超分效果。 为了在手机端实时运行,避免占用过多资源,我们优化反卷积计算,并针对人眼视觉特性,对强纹理和静止区域部分像素进行超分,以此大幅提高移动端的效率。 ▐ 4K实战 淘宝直播与中国电信浙江公司合作了全国首场5G电商直播,利用5g直播下的超高清画质和超低时延给消费者更好的线上购物体验。消费者除了可以看到直播间的整体情况,还能够远程自主对局部进行放大,非常清晰地看到商品的细节信息。让线上购物具有逼真的线下现场购物感受。 未来凭借5G通信技术与极致的编码优化和画质优化的结合,能给直播带货体系带来深层次的改变。4K,VR,AR等技术的成熟将会给直播场景带来更多产品和更好的体验,带来更多新型的应用场景,沉浸式体验和全画幅体验使用互动感和真实感更强。通过技术的赋能,淘宝直播间的可看性越来越强,淘宝直播所能覆盖的场景、所能覆盖的领域也会越来越广。 低延迟传输 ▐ 低延迟播放器 常规播放器的延迟分析。目前基于TCP的直播传输技术主要有 HLS和RTMP/HTTP-FLV两个协议,其中HLS直播的延迟一般在10秒以上,HTTP-FLV直播的延迟一般在6到9秒,从推流、cdn分发到播放的整个直播链路看,延迟的大头来自播放端。在播放器中,几乎每个线程都有自己的缓冲区,这些缓冲区的作用是平滑整个播放链路的抖动,它们的大小决定了播放过程中的播放延迟和播放的流畅性。VideoBuffer和AudioBuffer用来存放待解码的音视频 packet,该缓冲区是为了平滑网络的抖动,推流、CDN传输和播放下载的抖动都会堆积到播放端,这是常规播放器延迟最大的一个产生点,为提升直播的整体流畅度,缓冲区延迟一般在5秒以上。 基于TCP的媒体传输并不适用于低延迟直播场景,主要原因如下: 重传慢,TCP追求的是完全可靠性和顺序性,丢包后会持续重传直至该包被确认,否则后续包也不会被上层接收,且重传超时时间一般200ms,会造成接收侧帧抖动。 上层无法针对优化,TCP拥塞控制和 Qos 策略在操作系统内核层实现。 拥塞判断不准确,基于丢包的拥塞控制跟实际网络情况不符,丢包并不等于拥塞,也会造成发送链路 bufferbloat,链路RTT增大。 我们的低延迟传输SDK是基于WebRTC打造的,使用了WebRTC的几个核心模块,包括 RTP/RTCP、FEC、NACK、NetEQ、JitterBuffer、音视频同步、拥塞控制等。NetEQ和JitterBuffer分别是音频和视频的网络抖动缓存区,这是传输SDK延迟最大的一个产生点。RTP over UDP能够更好地对抗公网的丢包,结合自适应缓存和Qos优化,确保直播整体流畅度的条件下,我们的JitterBuffer的缓冲区延迟能够控制在700毫秒以下,直播观看延迟在1秒左右。 播放器对低延迟传输SDK的接入适配。我们对低延迟传输模块封装了FFmpeg的扩展demuxer,将支持低延时传输协议的demuxer注册到FFmpeg,播放器通过FFmpeg打开网络连接读取数据,这种接入方案基本不影响播放器原有逻辑,对播放器改动较小,主要改动点如下: 缓冲区大小控制。使用低延迟传输协议拉流时,网络的抖动缓冲区是底层传输模块的JitterBuffer,播放器层的JitterBuffer的缓存应设置为0秒,否则会引入多余的延迟。 卡顿统计修改。一般播放器根据缓冲区水位大小判断卡顿事件,当缓冲区为空或持续空一段时间,这会导播放画面卡顿,同时触发卡顿事件,播放器的JitterBuffer被低延迟传输SDK接管后,卡顿事件也应该由低延迟传输SDK触发。 音频解码流程。从NetEQ获取的音频已经是PCM数据了,播放器读取的音频数据可直接渲染,如果音频使用硬解,可能会出现解码兼容问题,现象是听不到声音,使用FFmpeg软解也是可以兼容的。 ▐ 低延迟服务器 低延迟传输是一个综合性的问题,要从整体入手,不仅要从设计上考虑,还需要客户端,服务器,数据系统紧密配合。从传输协议设计上采用rtp/rtcp方案。基于udp半可靠传输,技术成熟,更加适合音视频场景。难点在于既要降卡顿,也要降延迟。我们使用的整体算法策略如下: 拥塞控制 拥塞控制gcc&bbr算法针对直播场景深度优化,同时兼顾秒开和延迟。 分层丢帧 基于B帧的SVC算法和丢gop策略在网络拥塞时保证快速降低码率,解决拥塞。 重传控制 重传控制既要抑制重传风暴,也要保障快速重传。 平滑发送优化 平滑发送策略防止网络突发,平滑流量。同时针对秒开场景深度定制。重新设计发送机制和算法,发送性能大大提高。 秒开优化 服务器和端配合的多种秒开策略,保证极速开播。淘宝直播大盘平均秒开率94%以上。 信令优化 从信令设计上采用rtcp app私有协议,和音视频传输使用一个socket连接。建联协议更加精简,保障 1RTT 快速给出媒体数据。 除此之外还进行了大量策略到算法上的改进和优化。上面整体策略,基于数据驱动,针对场景不断迭代优化。 ▐ 端到端全链路分段统计 我们设计的端到端延迟分段统计系统,既能统计单次播放的延迟,也能统计每个阶段延迟。不依赖ntp时间,适合超大规模网络。通过分析不同平台推流端,服务器,播放器各个阶段的延迟情况,大盘展示出来,可以针对专项做优化。 面向未来 伴随着5G网络的提速,主播侧到用户侧的延时将会越来越短;移动端本身的性能提升,各种画质增强,图像渲染技术也会慢慢硬件化。移动端的深度学习模型也逐渐变得轻量化。这使得学术界各种越来越先进的创新也得以工程化。近期淘宝直播推出的智能虚拟主播也圈起了一波粉。将来,越来越有趣的玩法也会逐渐推出,使淘宝直播不再仅仅只限于“卖货“。更可能有更多有趣的玩法,例如:主播观众实时游戏互动,虚拟主播完全代替真人直播,观众沉浸式地通过VR或者AR进行直播购物。这些都会慢慢变成现实。淘宝直播的技术将来会为用户带来越来越丰富的直播购物体验。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
前言 天猫优品导购归因链路负责天猫优品订单导购判定工作,目前支撑了天猫优品权益券导购、普通导购和淘花导购等多种导购类型。随着业务迭代,现有导购归因链路在维护性、扩展性和可读性等方面存在明显不足,代码复杂性不断攀升,历史代码债务逐步积累。 为解决上述问题,开展了天猫优品导购归因链路技术重构工作。进一步地,在导购归因重构基础上,作为对“属性-分类-执行”问题的产品化思考与实践,提出了一种通用归因技术组件 ACE 。ACE 组件基于属性校验器、分类器和执行器三层模型解决了属性分类的通用性问题,具有良好的扩展性和代码语义。 本文以天猫优品导购归因重构为背景,阐述了一种基于 ACE 组件的订单归类技术方案。 技术痛点 伴随业务快速上下线,现有天猫优品导购归因链路在不断迭代过程中逐渐积累历史代码债务,应用代码存在复杂性高、扩展性低、可读性差等问题。 ▐ 事务脚本编程 事务脚本编程导致代码复杂性攀升。事务脚本型代码可能我们每天都在写,我们有时在用一门面向对象语言写着面向过程代码,基于 IF-ELSE 等条件判断语句快速堆砌业务代码。每新增一行业务代码,也许就新增了一行代码债务,应用代码复杂性逐步攀升。 图 1 :代码复杂性的演变 ▐ 违背开闭原则 违背开闭原则导致可维护性差。每次业务需求迭代,都在原有业务代码基础上修改或新增逻辑。我们很难知道历史代码哪些地方埋了坑,最好的方式就是尽量避免改动它。实际上,对于大部分业务代码,很难保证在新增需求时完全不需要改动原有代码逻辑。 ▐ 缺失架构设计 缺失架构设计导致可扩展性差。业务型技术团队常常面临业务需求急、开发周期短等问题,为支撑新业务快速上线,有时会采取最快的方式满足业务诉求。然而,最快的方式往往缺失架构设计,只为满足单一需求,对后续迭代并不友好。随着源源不断的新需求,应用代码很快陷入破窗效应,可扩展性越来越差,代码债务不断积累。 ▐ 业务逻辑复杂 复杂业务逻辑导致代码可读性差。看到下面这段代码,可能很难理解满足哪些属性是导购订单。这样的代码在业务型技术团队很常见,我们带着业务需求打开应用代码,却发现连原有代码所表示的业务含义都难以理解。代码可读性对业务型技术团队尤为重要,因为代码往往隐藏着业务含义,复杂的业务场景加上晦涩难懂的应用代码无疑是雪上加霜。 图 2 :晦涩难懂的业务代码逻辑 重构目标 为解决上述技术痛点,结合天猫优品导购业务发展背景,制定以下重构目标。 ▐ 精简导购归因链路,清理过时业务逻辑 业务发展存在不断试错的过程,应用代码伴随着业务不断迭代。有些业务代码虽然早已过时,且由于团队开发人员流动,谁也不敢轻易删除历史代码。代码上线容易下线难,代码愈发臃肿。因此,有必要精简现有天猫优品导购归因逻辑,清理过时业务逻辑。 ▐ 抽象业务模型,向后兼容业务发展 结合现有业务场景,抽象业务模型,支持后续业务轻量化迭代。通过抽象业务模型,可降低应用代码复杂度与业务场景复杂度的强相关性,甚至实现同一模型支撑多种不同业务场景。 ▐ 完善业务优先级决策,规则统一收口 规范业务优先级决策,统一收口业务优先级规则,便于后续代码维护和业务迭代。优先级决策是一种很常见的业务规则。如何用一行代码描述所有业务优先级,而不是将业务优先级判断散落在应用的多处地方? ▐ 提升代码可读性,代码语义即业务语义 借助通用业务模型,赋予代码更丰富的语义,提升代码可读性。代码可读性对业务型技术团队尤其重要,看懂代码即看懂业务规则,可极大减少沟通成本,提升开发效率。 技术方案 基于现有技术痛点和重构目标,首先抽象业务模型,然后设计了一种通用归因技术组件 ACE,最后将 ACE 应用于天猫优品导购归因链路。 ▐ 业务模型 以导购归因为例,导购归因旨在判断一个订单存在哪种类型的有效导购行为。有效性定义可概括为两个方面:一是满足或过滤某些属性,二是满足业务优先级规则。 导购归因是订单归类和优先级决策的组合,具体概括为以下四个步骤: Step 1 :导购订单必须满足或不满足某些属性 例如,天猫优品导购订单必须满足天猫优品商品等属性,且不满足(过滤)本地履约订单等属性。 Step 2 :不同属性组合成不同类型导购订单 例如,权益券导购订单 = 天猫优品商品订单 + 权益券订单 + ... + 非本地履约订单 + 非定向优惠订单。 Step 3 :不同导购订单类型存在不同业务优先级 根据优先级规则决策哪种类型导购订单有效。例如,权益券导购订单优先级高于普通导购订单。 Step 4 :根据归因结果执行不同处理流程 例如,订单判定为导购订单,执行落库、打标、消息推送等流程。 进一步地,导购归因可抽象为“属性-分类-执行”问题,抽象模型如下: 属性校验器(Attributor):表示一种属性。校验是否满足某个属性,支持原子或组合属性。 分类器(Classifier):表示一种类型。绑定一个或多个属性校验器,校验是否满足某些属性组合。分类器可分为嵌套分类器(NestedClassifier)和原子分类器(AtomicClassifier)。例如,Classifier 1 需要满足多个 Attributor,Classifier 4 需要满足 Classifier 1 和 Classifier 2。 执行器(Executor):表示一种类型对应的执行策略。绑定一个分类器,负责对某种类型执行处理。 图 3 :模型层次结构 Attributor-Classifier-Executor ▐ 归因组件 基于现有业务场景,抽象了一种“属性-分类-执行”的技术模型。在通用模型基础上,设计了一种归因组件 ACE 。ACE 是 Attributor - Classifier - Executor 的缩写,旨在通过属性校验器(Attributor)、分类器(Classifier)和执行器(Executor)三层模型解决属性分类的通用性问题。 整体设计 ACE 对外暴露统一服务接口 AceWorker,AceWorker 接收外部传入参数(归因场景 + 归因对象),根据归因场景获取分类器,并判断归因对象是否满足该分类器。分类器是 ACE 的核心,绑定了一个或多个属性校验器,并对应唯一的执行器。 图 4 :ACE 整体设计 详细设计 ACE 组件由 ACE 注解、ACE 工厂容器、ACE 初始化和 ACE 服务入口组成,详细设计如图 5 所示。 ACE 注解 基于易用性考虑,ACE 提供三种注解 @Attributor、@Classifier 和 @Executor 用于声明 ACE 组件,分别对应属性校验器、分类器和执行器。 @Attributor:声明一个属性校验器,属性校验器名称唯一。 @Classifier:声明一个分类器,分类器名称唯一。@Classifier 提供 matcher、filter 和 priority 三种属性,matcher 用于指定该分类器需满足的属性校验器列表,filter 用于指定该分类器需过滤的属性校验器列表,priority 用于指定该分类器绑定的原子分类器的优先级规则。 @Executor:声明一个执行器,每个分类器对应一个执行器,执行器名称需与分类器名称一致。 ACE 工厂容器 AceFactory 是 ACE 的工厂容器,负责管理所有定义的 ACE 组件,包括属性校验器集合、分类器集合及其绑定的属性校验器集合、执行器集合。根据 ACE 组件名称可直接从 AceFactory 获取对应的 ACE 组件。 ACE 初始化 借助 AceInitService 初始化 ACE 组件,应用启动时 AceInitService 自动解析 ACE 注解,并将 ACE 组件注册到 ACE 工厂容器。 ACE 服务入口 AceWorker 是 ACE 的服务入口,负责对外提供 ACE 通用服务,如属性校验 attribute、分类 classify 和执行 execute 。 图 5 :ACE 详细设计 示例 1)定义属性校验器 定义属性校验器 A ,判断是否满足属性 A 。 /** * 属性校验器示例 ATTRIBUTOR_A * @author haoyu.chy * @date 2020/9/5 */ @Attributor(name = "ATTRIBUTOR_A") public class AttributorA implements IAttributor { @Override public AceResult attribute(AceContext aceContext) { if (满足属性A) { return new AceResult(true); } return new AceResult(false); } } 2)定义分类器 定义原子分类器 CLASSIFIER_X(绑定属性校验器 ATTRIBUTOR_A ),判定是否满足类型 X 。 /** * 原子分类器示例 * matcher:需匹配的属性 * filter:需过滤的属性 * @author haoyu.chy * @date 2020/9/5 */ @Classifier(name = "CLASSIFIER_X", matcher = "ATTRIBUTOR_A", filter = "") public class ClassifierX implements AtomicClassifier { } 定义嵌套分类器 CLASSIFIER_NEST ,绑定原子分类器 CLASSIFIER_X 和 CLASSIFIER_Y ,CLASSIFIER_X 优先级高于 CLASSIFIER_Y 。 /** * 嵌套分类器示例 * priority:表示绑定的原子分类器的优先级规则 * @author haoyu.chy * @date 2020/9/5 */ @Classifier(name = "CLASSIFIER_NEST", priority = "CLASSIFIER_X,CLASSIFIER_Y") public class Classifier_Nest implements NestedClassifier { } 3)定义执行器 定义原子分类器 CLASSIFIER_X 对应的执行器 ExecutorX 。 /** * 执行器示例 * 注:执行器名称对应分类器名称 * @author haoyu.chy * @date 2020/9/5 */ @Executor(name = "CLASSIFIER_X") public class ExecutorX implements IExecutor { @Override public AceResult execute(AceContext aceContext) { // do something ... return new AceResult(); } } 4)定义服务入口 定义归因服务入口,指定归因场景 aceScene 和归因对象 aceObject,借助 AceWorker 完成归因工作。 /** * 归因服务入口示例 * @author haoyu.chy * @date 2020/9/5 */ public class SimpleService { public static void main(String[] args) { // 归因场景(例如,CLASSIFIER_NEST) String aceScene = "CLASSIFIER_NEST"; // 被归因对象(例如,订单号) Long aceObject = 1234567890L; // 初试化上下文 AceContext<Long> aceContext = AceContext.of(aceScene, aceObject); // 执行归因流程 AceResult aceResult = AceWorker.getInstance().classify(aceContext); } } ▐ 天猫优品导购归因 天猫优品导购订单类型举例: 权益券导购订单:订单存在权益券使用记录 天猫优品普通导购订单:天猫优品商品且最近一次导购记录为普通导购 天猫优品淘花导购订单:天猫优品商品且最近一次导购记录为淘花导购 不同类型订单有不同优先级,业务规则如下: 优先级规则:权益券导购优先级最高,普通导购和淘花导购优先级并列(最近原则) 互斥规则:天猫优品定向优惠订单、本地履约订单、代购订单优先级高于所有类型导购订单 基于 ACE 组件,重构天猫优品导购归因技术链路,设计天猫优品导购归因流程如图 6 。 图 6 :天猫优品导购归因流程 Step 1 :定义属性校验器 属性校验器相互独立,表示是否满足某种属性。例如,定义属性校验器(Attributor):天猫优品商品标、权益导购券、普通导购记录、淘花导购记录、定向优惠、本地履约等。 Step 2:定义原子分类器 原子分类器绑定多个属性校验器,表示是否满足某种类型。例如,定义分类器(Classifier):权益券导购订单(COUPON_GUIDE)、普通导购订单(NORMAL_GUIDE)和淘花导购订单(SUPERB_GUIDE)。其中,权益券导购订单(COUPON_GUIDE)绑定权益导购券属性,过滤定向优惠和本地履约等属性。 注:图 6 实线表示满足,虚线表示过滤 Step 3:定义嵌套分类器 嵌套分类器绑定多个原子分类器,一般用于优先级决策场景,按前后顺序匹配第一个有效分类器。例如,定义嵌套分类器(GUIDE_ORDER),其绑定原子分类器(COUPON_GUIDE、NORMAL_GUIDE 和 SUPERB_GUIDE),优先级从前往后。 Step 4:定义执行器 每个原子分类器对应一个执行器。如果某个分类器有效,则执行对应的执行器。 Step 5:定义服务入口 定义 ACE 组件后,只需指定归因场景(对应分类器名称)和归因对象,即可使用归因服务。例如,归因场景为导购归因(GUIDE_ORDER),归因对象为某个订单,则表示对某个订单执行导购归因。 基于 ACE 组件,定义属性校验器、分类器和执行器,封装服务接口。天猫优品导购归因代码框架如图 7 。 图 7 :天猫优品导购归因代码框架 效果分析 ▐ 遵循 SOLID 原则 单一责任原则(SRP):每个 ACE 组件只负责一种职责。Attributor 只判断是否满足某种属性,Classifier 只判断是否满足某种类型,Executor 只对某种类型执行处理。 开放关闭原则(OCP):ACE 组件之间相互独立。新增属性或分类无需修改原有组件,只需定义一种新的属性校验器或分类器即可,完全无需改动原有代码。 里氏替换原则(LSP):父类可用子类替代,子类只扩展父类方法,不重写父类方法。ACE 基于接口实现,不重写已实现方法。 接口隔离原则(ISP):子类不被迫依赖它不需要的方法。ACE 组件接口相互独立,且只提供唯一方法。 依赖倒置原则(DIP):ACE 组件面向接口编程,基于 ACE 工厂容器实现依赖注入,组件间不存在直接依赖关系。 举例: 如图 8 所示,新增导购类型(优盟导购),只需新增分类器 Classifier(UM_GUIDE),并绑定相关属性 Attributor(优盟),关联执行器 Executor(优盟导购)。借助 ACE 组件可无侵入性地新增业务类型,完全无需改动原有代码。 图 8 :新增导购类型(优盟导购) ▐ 代码结构优化 代码结构从原有的「纵向+多出口」转变成「横向+单出口」。从代码维护角度,单出口程序更利于维护,代码(方法)出口统一收口到一处地方,代码逻辑一目了然。 图 9 :横向单出口的代码结构 总结 本文提出了一种通用归因技术组件 ACE,并将其应用于天猫优品导购归因技术链路。ACE 组件以属性校验器、分类器和执行器三层模型为核心,规范化定义某种类型需满足的属性组合及其对应的执行策略。 ACE 组件借鉴了策略模式思想,不同分类器对应不同执行器(策略),但只有分类器有效时,相应执行器(策略)才会被执行。分类器支持组装式关联多个属性校验器,同一属性校验器可被多个分类器复用,具有较好的灵活性和扩展性。此外,ACE 组件可将代码逻辑结构化,提升应用代码的可读性。 思考 一周岁技术新人的非严谨思考 “业务需求的局部性原理” 大家都听过计算机系统的局部性原理,其实业务需求也存在“局部性原理”。小到多打一行日志、多传一个参数,大到多留一个扩展点、抽象一个服务,这都在为后续需求留余地。写代码时多做一步,不写一次性代码,也许反而能减少后续工作量。 “应用代码的破窗效应” 在实际需求开发过程中,我们往往会参考原有代码实现。代码风格或结构设计是具有传染性的,糟糕的代码风格和架构设计会使应用陷入破窗效应。一个不成熟的思考,是否绝大部分应用都难逃破窗效应?即使前期有较好的架构设计,但由于业务发展和人员流动,原有架构约束依然有可能过时或被忽略。 “业务团队的技术挑战” 在集团的“关怀”下,业务型技术团队的技术越做越轻,业务越做越重。开箱即用的中间件,让业务团队变得“没有技术挑战”。刚参加工作的这一年,常常焦虑个人技术成长。回过头想,对于业务团队而言,技术挑战也许在广不在深。大多数情况下,技术型团队或许在和机器打交道,考验技术深度与钻研能力;业务型团队则在和商业打交道,考验技术架构与抽象能力。孰好孰坏似乎没有绝对答案。 淘系技术部-天猫优品团队 阿里集团新零售战略板块的重要一环,围绕消费电子/家装等领域,以天猫优品数字化门店为核心,沉淀一套从供给端到消费端的全链路数字化解决方案。 如您在寻求新的工作机会,欢迎投递简历至:haoyu.chy@alibaba-inc.com ,期待您的加入! 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
背景 闲鱼前端页面的性能常常被人念叨,凡跳转、必跳鱼 的印象深入人心,部分页面甚至需要跳四五下才能打开,最近我们对闲鱼前端页面系统性的做了些优化,由于闲鱼前端技术栈相对多元,不同栈技术原理各不相同,优化方案也有所差异,本文主要介绍目前闲鱼占比较重的 Weex 页面的优化过程。闲鱼 Weex 页面多以前端渲染为主,其打开过程与 Web 页面略微相近,大致分为以下几个阶段:我们将「从开始加载(navigationStart)到屏幕首次 paint(绘制)像素内容」的这段时间称为 白屏时间(FP),将「从开始加载(navigationStart)到首屏内容全部渲染完成」的这段时间称为 首屏时间(FSP),受限于统计口径,目前 Weex 下的首屏时间是不包含图片下载及后续过程的。 优化前后 我们拿闲鱼的直播频道页和玩家频道页作为参考,通过录屏的方式看下优化前后的对比: 通过录屏分帧的方式我们统计了下这两个频道页在不同系统不同机型下的首屏时长:可以看到,优化前 iOS、Android 主流机型上的首屏时间都要超过 2s,低端机甚至要 3-5s,优化后各机型的首屏时间均大幅下降,低端机首屏时长控制到了 2s 内,中高端机近乎直开。 拆解分析 确定优化方案前我们对现有的 Weex 页面做了拆解分析,从结果来看,以下几个因素对首屏时间的影响较大: Bundle 体积:不仅影响 Bundle 加载时长,同时也影响 Bundle 的解析执行耗时(低端机尤为明显) 首屏数据请求:页面渲染必须在首屏数据请求返回后,接口耗时直接影响首屏时间 首屏渲染范围:首屏渲染量直接影响渲染时长(低端机尤为明显) 优化方案 基于上面的分析调研,我们初步把优化方案定为四层: 按照预期优化效果,Weex 页面的打开过程是这样的:体现在上述的四层结构中,主要包含以下几个优化点: Bundle 离线 具体实现是将 Weex Bundle 以资源包为单位、以 URL 前缀为索引,通过一定的更新策略离线到客户端本地,之前的更新策略主要有 访问后安装、启动安装** 两种,对应的更新时机如下:这套机制在容器层有统一的方案支撑,但是包命中率一直不高(25% - 55%),导致最终效果差强人意,分析后发现默认的更新策略(访问后安装)与页面回访率强相关,闲鱼的前端页面大都是频道导购型的页面,回访率天然不高,所以包命中率相对应也不会高。本次优化主要是对更新策略进行了扩展,增加了 “闲时安装” 的更新策略:会在定时更新期间主动安装,如果安装后未使用,则会在一周之后淘汰;如果一周内使用过,则进入常规的更新淘汰机制(一个月未使用淘汰)。在 “闲时安装” 的更新策略上线后,包命中率大幅提升(稳定后 90% 左右),页面性能也得到了显著提升:不依赖首屏接口渲染的页面甚至可以达到「直开」: 数据预取 传统的首屏数据请求都是在 Bundle 解析完以后发起的,首屏数据返回后渲染页面,是个典型的串行过程。本次优化中我们把这个串行的过程并行化了: 将首屏请求的配置序列化以后作为参数配置到了 URL 上,同时支持一些动态替换的参数(譬如经纬度、城市等参数); 在 navigationStart 的时候由客户端提取首屏请求配置,然后发起请求,并将结果以特定的 Hash Key(通过首屏配置生成的)作为索引存储到本地; 在业务层真正发起首屏请求的时候会通过 Hash Key 进行比对,命中后将数据取出来返回给业务层; 时序图如下: 特殊情况下的时序图:具体的技术细节本文不再赘述,数据预取的优化策略上线后,首屏时间也得到了一定程度的提升,如下(iOS 侧由于各优化策略并行上线,没能做到单一变量采集性能数据,暂以 Android 侧为参考):Bundle 离线、数据预取 的优化策略上线后,部分页面在中高端机型上逼近「直开」: 渐进式首屏 渐进式首屏解决的是「最后一公里」的问题,因为在上了「离线包」和「数据预取」的方案后,我们发现:页面首屏时间一定程度上还是受限于首屏接口请求耗时,该方案就是为了降低用户侧的白屏等待时长,具体从以下三个方面着手: 以接口请求配置生成的索引对接口数据进行缓存 当用户首次进入时,以骨架屏占位来等待业务数据加载; 当用户非首次进入时,会根据接口请求配置生成的索引在本地缓存中查找缓存数据,并完成首屏渲染,同时并行发送接口请求,待新数据返回后,触发页面更新,完成最终渲染; 低端机降级方案 为了用户体验能够更好,在此我们尝试了低端机降级优化方案。以直播频道为例: 只对首屏 Tab 做缓存数据占位优化 减少了低端机上首屏渲染展示数据量 图片渲染效果优化 渐进式首屏带来的一个问题是界面更新时的闪动(特别是图片占大篇幅的时候),为了优化此问题,我们将图片从加载到出现的过程改为了渐显过渡,一定程度上消除了图片闪动的生硬感。 按需渲染 渲染页面作为首屏链路中的一环,不同技术栈、不同设备环境下,在页面首屏时间中也会有不同的占比。类Weex、RN 通过前端脚本映射原生组件的技术方案,渲染路径总结起来是:渲染前端 Virtual DOM -> 映射为 Native 指令 -> 将指令传输到 Native 侧 -> Native 执行指令完成渲染。在前三个步骤上,较重的业务逻辑或不合理的代码通常会带来较长的计算、通信耗时,中低端机器上尤为明显。通过按需渲染可以有效解决这一问题。按需渲染主要思路是通过只渲染首屏可见视图来最小化首屏渲染耗时。本次优化中,主要针对以下几个场景做了按需渲染: 多 Tab 情况下,对于有性能要求的非首屏 tab 页,做数据预加载、页面懒渲染处理 对带/不带回收机制的长列表做首屏只渲染可见条目,剩余懒渲染处理。可减少带回收机制列表的脚本计算、通信耗时,减少不带回收机制列表的全链路渲染耗时。 自建或使用轻量级组件替换非必要的重量级组件,如: xSlider。 优化上线后,鱼塘广场页中低端机型的首屏性能有了部分数据上的提升:低端机上优化前端渲染阶段对比: Bundle 瘦身 **Bundle 体积一方面直接影响 Bundle 下载时间,另一方面也会影响 Android 端的渲染性能(耗时随 Bundle 体积增加 1-2ms/KB),我们在 Bundle 体积上的优化方案较为常规,包括: 通过 Webpack Bundle Analyzer 分析依赖,减少同 npm 包不同版本依赖 抽象公共模块,提高代码复用率 重构基础工具类库,支持按需加载打包 总结 闲鱼前端的性能优化暂时告一段落,优化过程中沉淀了较多的通用能力,像 Bundle 离线、数据预取、渐进式首屏等等,这些能力在后续会有更大的发挥空间,一些能力也会变得更加智能,譬如目前的数据预取是在 navigationStart 的时候发起的,这个时机已经比传统的页面加载时的时机提前了许多,但其实还可以更加提前,譬如可以在闲鱼客户端中常驻个 TaskSchedule,专门用来处理数据预取的 Task,同时可以结合用户的访问习惯做智能数据预取。在前端性能要求越来越高的背景下,传统的 Web 加载流程已无法再满足性能优化的需要,所以出现了各种新兴容器 + 配套能力,所以下一代容器的标准形态应该是什么样的?
开门见山,这是一段可以搞崩掉服务器的代码片段,如果你的代码也这样,那一定要注意啦~ try { obj = JSON.parse(data); } catch (err) { // ignore } 你肯定很好奇,这段看似平淡的代码片段究竟是怎样搞崩掉服务器的? 这是一个"真实"的故事,就发生在几天前...... 某晚一办公大楼警铃大作,电话那头某应用函数报告某应用系统异常, 从监控上看到,内存增长呈现阶梯式爆炸式增长,短短几个小时就消耗完了系统内存。 内存监控 咋一看,这是普通的不能再普通的内存泄漏问题,这对训练有素的士兵们已经不算什么。按照常规方法,取heapdump进行分析,占用最多的对象一般都能分析个八九不离十了。 但是 。。。 heapdump竟然看不出什么。。。只看到一个影子,一个吃了几百兆内存的影子,这是什么鬼? Heapdump 此时,报警还在持续,办公室报警声不断,但又非常安静,弥漫着诡异的气氛。 监控上,应用一个个逼近系统极限,OOM一个个成为尸体,但是都留下相同的影子 。。。 时间在一分一秒的过去,"我们必须尽快抓到'影子',好给大家一个交代",数班长急促的声音透露着坚定。 "'影子'可能有个代号script_list,但是我们目前掌握的就只有那么多信息了",Y说到。 Y是班里最牛的信息兵,他有着最敏锐的洞察力,并掌握着最精准的信息,但是这一次,他也感到困惑。 "M,你跟我立刻去一趟基地,我们要进去抓'影子'" 班长说。"是,长官"。 作为特种兵的M,平时就接受了缺少粮食、缺少装备的高强度训练,他可以在极简的配置下,执行最底层的特殊任务。 M近照 "如果'影子'是个人,他应该还在基地里",M说。 "你能找到他么?",班长问。 "能!他只能从指定的门进去,并且注册登记,吃成这么胖,应该很容易被发现。" "如果是妖呢?" "下次好莱坞的电影可以用这个做题材,这是人类历史上首次捉到妖", 班长一脚踢向了M,"少TM扯淡,走!" "带上这个,或许会用到。" 临走时,P塞给了M一卷图纸。走的匆忙,M也没来得及看一眼,就丢在了包里。 一卷图纸 班长和M离开了办公大楼,去往基地。 基地在不远的地方,门口有门卫守护,但是地方很大,要在基地找到'影子'并不是容易的事情。 基地内戒备森严,并还有巡逻的卫兵,巡视着基地内各个房间,并清理一些不必要的垃圾出来。 基地已经运作了很多很多年,可能有过一些异类后来被清理了,但是从来没有遇到过'妖怪'? 到了基地, "M,你进去吧,我还有个会议要参加,要给排长作简报,等你好消息哟~", "是,长官",M背着包就进了基地。 M的包里除了P塞的图纸,还有gdb和llnode两个工具。"真实的师傅领进门",M心里默想。 gdb 用来定位和分析v8/node的c++实现,大部分没啥用,但有把叉子总比啥都没有的强。 llnode 用来定位和分析v8的object,虽然绝大部分都是unkown,但能看个东西总比眼瞎的强。 基地内被分割了很多个营地,每个营地都有自己独立的管理人员。M面临的第一个问题,是如何找到各营地的管理人员,因为管理人员通常不固定在一个地方,而且他又没有电话号码可以联系。 但是每一个营地在建设的时候,都保留了一个设计图纸,里面标注了这个营地营长的办公室。 "P给我塞的难道是营地图纸",M嘀咕着, 拿出图纸一看,真的是Isolate第一营地的地方标注,他径直走了进去。 关于进程内存中定位Isolate node支持多个Isolate,通过 node::per_process::v8_platform.platform_.per_isolate_ 可以获取到所有v8::Isolatenode binary会在固定内存的地址上存放了一些很重要的数据用以分析,比如下面的v8_platform 00000000029ae600 B node::per_process::v8_platform 除此之外还有 nodedbg、v8dbg开头的常量符号用于mdb(Modular Debugger), 被收进llnode中,用来给v8和node定位corefile,也被称作 postmortem (验尸)。 "长管,我是NODE特种兵M,请问您是Isolate的营长么?" "我是" "我受上级命令,来调查一个叫'影子'的人,这个人很危险关系到人民的利益,影响到群众用TB了" "'影子'?从来没听过这个人",营长一脸困惑 "这个人可能很胖,你能给我讲一下我怎么能查到所有的人,我相信我能找到他" "可以是可以,你得这样来 。。。",营长给M讲了一下营地的结构。 原来营地分为很多个区域, 新兵区,刚来的新兵都在这个区域进行训练,有些新兵呆满2年就退伍转业了,有些新兵则可能留在部队晋升到老兵区了。 老兵区,老兵通常有着更丰富的经验,并且比新兵更加沉稳,愿意效忠,退伍意愿并不强烈。 还有器械区,摆放了各种武器,虽然武器最后会分发给各个士兵,但是都存放在这里。 Node内存 "每一个人,每一把枪,都在账本上有登记,你也可以查看宿舍和仓库。我现在带你去见H长官",营长说。 H长官负责所有营地的人或物件的管理,任何进出都需由H长官许可。 Isolate->heap_ 管理了v8所有的对象。 在H长官的带领下,M检查了新兵区和老兵区的登记,没有发现任何异样,完全没有异常体重的人。 M走进了大型器械仓库,看到一个超级大的架子, "这是什么?",M问道, "这是武器架,任何武器都存放在这个架子上,每个武器存放一格"。 "这有多长",M接着问道, "700多m", "你们有多少武器" "10w件", "那要这么大的架子么?",M表示疑问。 从 0xbec56a80138 - 0xbec81f55660,存放了一个LargeObject,占用了726M内存空间 M拿出了GDB仔细检查了这个架子,发现700m的架子上,只有头上和中间部分集中摆放了一些武器,其余部分都是空的。 "为什么会这样?",M问H长官。 "这是按规定的,我们有一个账本,记录了进来的武器,每次进来一件,我就会从架子上分配一个格子,如果没有格子了,我就问上级需求一个新的架子。我们这里需求很大,你看,现在已经分配到66626945格了。" "那些取出的武器呢?",M问 "放心,GC卫兵会来清点的,如果架子后面都是空的,他会标注最后一个有武器的格子,然后我会从下一个空格子分配。这个系统已经运作很久了,从来没有出过问题",H长官有些不耐烦。 "这个架子有代号么?", "有,叫script_list"。 中间 (0x00002090 - 0x056dc5c0), ( 0x056dc610 - 0x1fc48d60) 都是0x0000000000000003(v8空指针) 空洞占了绝大多内存空间,由于v8指针压缩技术的存在,写脏的页面导致很大的内存开销。每一个js都会创建一个script添加到script_list上。 听到这,M已经理解为啥这个营地需要那么多的架子存放武器了。 因为只要架子后面有一把武器没有被拿走,新来的武器只能存放在他的后面。所以这个架子已经接到700多m,并且600多m都是空的。 M走到架子中间,随手拿起中部架子上第一部武器,是一把手枪。M拿出了LLNODE,仔细检查了这把手枪。M注意到手枪上面印有"[object Object]"的字样。 这个script含有特征字符 "[object Object]"和3个smi数字(1,2,6596938),但无法判断是什么script "这是谁的枪?",M问道, "士兵使用不同的枪械,这种类型的'[object Object]'手枪属于很多个兵种,一排二排都是,但是不知道具体谁的。",长官答道, M拍了拍上面的灰尘说,"这把枪应该很久没有人来拿过了,要不现在开始,所有的入库都需要检查一下,看看谁还有这把手枪?" 没过多久,有个叫Json士兵来到架子前,M用GDB查看了他的手枪,上面写着"[object Object]"。 "有个长官让我更换这个枪的枪托,我更换时发现这个枪托根本拆不开,按照部队规定我就给送到这里来了",Json解释到, "这把是不是也你的?",M问道, "可能是我上次忘了吧,", Json答道, "你们有没有流程记录送到这里的枪械,然后会全部取回么?",M问道, "没有,忙起来就忘了"。 利用gdb的数据断点,可以捕获向script_list添加script的调用栈。 这个捕获的调用栈显示了在处理JSON异常时,会向v8::script_list增加script,并且这个script含有特征字符串"[object Object]"。 JS的代码呢?你没看错,就是片头的范例。 M拨通了数班长的电话,"我找到'影子'了"。 几个月后, node基地从v12.18.2开始,对script_list的入库,都采用了新账本来管理这些入库的武器, 那些freed的格子都被填满了武器。 终—— 这是一个复合型的内存泄漏案例。 v8::script_list的实现是在WeakArrayList的末尾添加新的script,并在执行完成之后由GC回收缩短队列, JSON.parse()在遇到异常时,会有少量的内存泄漏并可能遗留script的对象在script_list中, 泄漏的script对象造成了v8::script_list出现空洞而无法回缩,从而放大了对内存的消耗。 node-v12.18.2以前所有的v12版本都受这个问题影响。 但v10不受这个问题影响。 最后:如你的应用已经遇到类似的内存泄漏问题,请尽快升级到最新的nodejs或alinode。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
阿里巴巴淘系技术开源轻量级深度学习推理引擎 MNN 以来,得到了业界的普遍认可 —— 数十家企业成为 MNN 的用户,不乏有一线科技企业;此外,MNN 还与上海交大等一线院校展开合作,深度连接产学研。 除 MNN 引擎建设外,MNN 团队还深度参与了阿里内部的端AI应用实践。AI 应用门槛高、算法模型部署链路长是长期困扰我们的问题。为了解决这两个问题,我们将上百次实践中长期积累的方案沉淀为一站式 AI 研发范式 —— MNN 工作台。 ☞ 赶快进入官网:http://www.mnn.zone/ 下载MNN工作台吧 今天正式推出 MNN 工作台,这将极大降低AI应用门槛、将AI研发的效率提升数十倍,让“技术小白”也能快速上手,轻松设计自己的AI应用。 要打通AI应用的“任督二脉”,需要“十八般武艺” —— 你需要同时熟悉AI和业务,要搜得到数据、啃得了论文、改得动模型,末了还得会移动开发,打通业务流程。道阻且长,任一环节出了问题,都可能断送AI应用探索之路。 市面上其实不乏有一些云端模型训练平台,但大多只在流程中的数据和模型上下功夫。相较之下,MNN 工作台是第一个同时应对 AI 应用启蒙、无门槛训练和一键多端部署的一站式 AI 平台。 AI 应用启蒙最好的办法是参考行业的最佳实践,并且动手玩一玩。 MNN 工作台中,提供了人像分割、文字识别等主流应用的实例,无需训练就可以直接使用。通过 MNN 工作台,可以方便地在电脑和手机上体验AI的效果。AI 的视觉效果会直接叠加在相机视频流上,改变相机机位,就可以看到效果的变化。同时,还可以通过开关、滑块等来调节算法参数,同样可以实时观察到调整的影响。随手改、随心玩的即时体验,可以帮忙用户理解算法,打开想象的空间,更好地寻找AI和业务的结合。 在帮助用户无门槛训练自己的AI算法模型方面,MNN 工作台也下足了功夫。 MNN 工作台支持图像分类、文本分类等模型的训练,而且,所有的操作都可以通过图形化界面来完成。用户不需要了解模型训练的细节,只需要按照提示,提供训练所需的数据,就可以完成模型的定制。同时,MNN 工作台使用了迁移学习技术,用户仅仅需要少量的训练数据,就可以训练出效果上佳的专属模型。MNN 工作台还提供了自动化标注工具和手工标注工具,来进一步简化训练数据的准备工作。最后,在模型训练完成时,MNN 工作台还会提供模型测试报告,涵盖模型的大小、性能、精度信息,辅助决策。 应用多端部署方面,MNN 工作台为电脑和手机都提供了强大的 Python 运行环境 —— 三端一致的开发体验。 但更友好的,还是 MNN 工作台上所有的模型,都可以在电脑和手机上直接体验。是的,无需任何一行代码,试玩模型或训练模型都有对应的、已经编写好的代码!如果你需要修改算法实现,我们也提供了 Numpy、OpenCV 等常用库,尽可能降低图片、数据处理的成本。同时,在工作台上,扫码真机调试,断点、console 一应俱全,更有多端文件实时同步这样的黑科技等待你的解锁。 好礼相送 加入钉钉群,并安装MNN工作台,前50位开发者即可获得淘系技术专属大礼包!!!(tips:安装后要第一时间截图发到钉群哦~) 还等什么呢?快来扫码进入官网,下载体验吧。只要你有想法、有创意,下一个引爆全场的,就是你了! 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
前言 3D-FRONT是由阿里巴巴淘系技术部(赵斌强、付欢、贾荣飞、蔡博文等),西蒙弗雷泽大学(张皓教授),中科院计算所(高林、张凌霄)合作开源的大型3D场景数据集,其填补了学界3D场景布局数据的空白。这对于推进学术研究与工业落地紧密结合,以及3D视觉与图形学未来发展有着重大意义。 中科院计算所博导高林、淘系技术部高级算法专家贾荣飞 代表现场领奖 Chinagraph (中国计算机图形学大会)是由中国计算机学会、中国自动化学会、中国图学学会、中国图象图形学会、中国系统仿真学会、香港多媒体及图像计算学会于1996年发起主办,迄今已举办12届,是中国计算机图形学界最高级别的学术会议。 大会今年首次设立图形开源数据集奖。奖项的目的是为了表彰向公众提供计算机图形学相关的开源数据集的华人学者、企业及学生。“图形开源数据集奖”颁发给阿里淘系技术,表彰了3D-FRONT帮助他人能够快速验证已有方法,推动领域内新技术的发展,并为提升计算机图形学领域的影响力做出重要贡献。 3D-FRONT 3D场景理解是打造数字化世界的基础核心研究课题,且依赖与大量数据进行相关模型训练。然而,学界开源场景数据严重缺乏,特别是具有高质量布局与室内设计的数据集。导致3D场景的研究与远无法满足现实工业落地需求。这是当下学界与业界亟须解决的问题。阿里巴巴作为世界级电商巨头,其官方家装家居设计平台 --- “躺平设计家”积累了海量高质量家居设计方案。以这些真实专业场景设计为基础,阿里巴巴淘系技术部结合3D人工智能技术,打造了场景数字化营销,推出了智能设计搭配服务,创造了大量精美场景布局与设计数据,并基于此组织了3D-FRONT开源项目。 3D-FRONT (3D-FRONT: 3D Furnished Rooms withlayOuts and semaNTics) 包含6,813不同的真实户型,户型由51,708房间组成。这些房间可以被细分成28种类型,其中19,775房间含有人工验证过的精美室内设计信息。房间内的高质量模型来自于阿里巴巴大型家具模型开源项目3D-FUTURE (3D FUrniture shapes with TextURE)。 阿里巴巴智能设计业务落地 阿里巴巴智能设计在多个家居业务发挥着核心的作用,包括躺平轻应用智能创作,淘宝购后猜你喜欢,智能客服推荐,ICBU 3D Virtual Home等。在3D Virtual Home会场业务中,阿里巴巴淘系技术部利用智能室内设计技术生成了近8000套精美户型设计,在导购会场浏览时长提升了3倍,带动大家装行业的实收交易额同比增长130%。 基于场景数据集的AI智能设计产品——3D鲁班,依托躺平设计家超百万的真实尖货商品模型库,平台能快速组合商家的真实商品模型并根据客户行为洞察呈现出“千人千面”AI智能设计样板间。未来,将有越来越多的家居品牌商家会因此受益。 3D-FRONT自开源以来,受到学界广泛关注,并持续吸引者国内外顶尖机构与企业,例如斯坦福,清华,谷歌,Facebook等,在该数据集上开展学术研究。阿里巴巴将持续维护并提升3D-FRONT场景开源项目,以推动3D视觉与几何领域内新技术的发展与革新。3D-FRONT数据集主页:https://tianchi.aliyun.com/specials/promotion/alibaba-3d-scene-dataset 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
前言 2019年5月,淘系技术开源了深度学习推理引擎MNN,加入到人工智能开源社区中来。自打开源起,MNN就一直是开源社区的性能标杆之一,是众多后来者的挑战的目标。这固然是来自行业的认可,却也拉开了推理引擎间的性能军备竞赛。这或多或少,背离了我们的初心 —— 人工智能只是工具,靠比拼工具在一时一地的优劣,不能帮助我们繁荣AI应用生态。 帮助MNN不断成长的,除了我们在性能上的不断压榨,还有众多场景的哺育。MNN支撑着阿里巴巴众多的人工智能应用,从电商购物到视频直播,从手机应用到智能汽车;同时,也有越来越多的企业选择MNN,通过人工智能来提升工作能效、优化用户体验。是这些应用,而不是PPT上的性能数据,在你我不经意之间,改变了我们的生活。 为了走出性能内卷怪圈,为了给生态引一泓清流,MNN会陆续为你介绍我们在人工智能应用上的探索。这里的「我们」,不只是阿里巴巴,也包括身为MNN用户的你。 优酷体育:宅家街舞AI私教?! 今年的疫情对许多行业都产生了深远的影响,这其中就包括运动健身。宅家胡吃海塞,不能去健身房挥汗如雨,沉积的脂肪都堆在了大腿和肚腩上。俗话说,三月不减肥,四月徒伤悲,五月不减肥,六月徒伤悲…就算宅家,也不能被封印在床头!生命!在于运动! 然后你看了看家里的跑步机。 是这样? 或者是这样? 除了器械,家庭健身需要专业的运动指导,更需要优质的内容,让运动本身不再乏味和无趣。优酷体育AI操房,作为中国首家家庭智能操房平台,连接了硬件产商、健身机构和运动达人,通过游戏互动,带你玩转运动 —— AI操房通过摄像头捕捉用户动作,通过算法实现动作量化分析,再结合上游戏化的方式,把反馈投屏到电视上,让用户的跳操不再枯燥乏味。 为了指导用户做出动作并打分,我们要先根据原始视频设定动作示意和打分标准。运营同学会在AI操房的后台,从原始视频中选出关键帧,根据视频骨骼点信息生成动作示意图和描述文件。 在进入操练前,动作描述文件会和视频一同下载下来。在视频播放的同时,摄像头会捕捉用户的动作。关键帧的前后一段时间都会作为对应动作的得分区间。我们会在得分区间内持续比较用户和视频的动作差异,给出miss、good、perfect等反馈,并打出分数。区间内的最高得分会保留为动作的最终得分,参与总得分的计算。 这样,就算宅在家里,也能练起街舞了。谁说下一届「这!就是街舞」的冠军,就不能是你呢? 整套方案基于平台化思维,分为前端用户流程和动作编辑后台,整套流程完全开放,可以提供给健身机构或者KOL,产出定制化的AI操房,创造更多的玩法。整套方案还可以无缝迁移到OTT,为OTT用户提供服务。 有意向与优酷体育进行合作的智能硬件产商、科技和AI算法平台、健身机构和健身内容创作者们,请发邮件至:zr162261@alibaba-inc.com ,与优酷体育共创AI健身操房,做大!做强! 陌陌:直播互动「心」姿势 陌陌在人脸识别、人脸关键点、表情识别、手势识别、身体关键点算法上,有长足的积累。在直播中,陌陌基于人脸识别、人脸关键点,加上渲染、磨皮、美白等技术,为主播实现美颜、眼妆、贴纸等一系列特效;在拍摄器里,陌陌基于人脸关键点,来识别表情和睁闭眼,实现了一系列的特效玩法,比如眨眼识别的眨眼星星特效、嘟嘴识别的吹蒲公英、吹泡泡等。 最初,陌陌并没有使用MNN推理引擎,由于客户端上人脸检测、人脸关键点等模型推理和特效渲染都需要占用大量CPU和内存,CPU和内存成为了更多模型和特效应用的瓶颈。在全面升级到MNN推理引擎之后,推理速度和内存占用上都有了明显的优化。这样,客户端上就可以放心添加更多功能模块了。 为了增加主播和粉丝的互动,让直播更加有趣,陌陌在直播中推出了送礼物比心的互动玩法 —— 当粉丝给主播送出礼物后,主播可以做出比心手势表示对粉丝的感谢,在客户端识别出手势后,会触发粉丝送礼物的特效。 技术小哥哥素颜出镜比心 功能上线后,使用过的主播都夸效果好,直播间的粉丝数有明显的提升,主播和粉丝之间的互动更多了,粉丝刷礼物也更积极。 后续,陌陌还会在直播间和拍摄器里设计出更多好玩的特效和互动,期待你的体验~ 智能应用长征 人工智能可以优化原有流程的体验,也可以开创出全新的玩法,但目前,智能应用的开发流程还很长,整体门槛比较高。 要打造一款有用、有趣的人工智能应用,你需要对人工智能和业务场景都有必要的认知,才能将有效融合两者。同时,你需要为模型训练收集大量的数据,并对数据做出必要的清洗和标注。之后,在众多模型结构中找到适合业务的,经历漫长的训练、验证迭代,得到模型后还需要做必要的优化、压缩。 产出模型并不是端侧AI应用的终点,恰恰是起点。以视觉类应用为例,可能90%的工作量都在模型训练之后 —— 适配iOS、Android的相机输入,图片增强特征、裁切、转换,推理结果过滤、提炼,结合物料渲染上屏。工程链路很长,涉及到的移动端编程、图片处理、渲染绘制,还分别要求不同的专业技能。 # 预告 如何降低人工智能应用的门槛、提升人工智能的研发效率,想了解淘系的实践经验吗? 号外:MNN官网全新上线,下周,将有重磅消息推出,敬请期待哦~也可以进入MNN官网,提前尝鲜。 也可以添加淘大橙微信(TaoTech001)随时获得最新资讯!!! 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
中国计算机学会 (CCF) 主办的「2020中国计算机大会」(简称:CNCC)将于10月22-24日在杭州未来科技城学术交流中心举办。 10月23日下午,阿里巴巴淘系技术部将在CNCC2020和大家分享阿里巴巴新零售内容AI平台创新与实践。 随着4G的普及和5G的推出,内容消费的诉求越来越受到人们的重视。2019年互联网趋势报告指出在移动互联网行业整体增速放缓的大背景下,短视频行业异军突起,成为“行业黑洞”抢夺用户时间,尽管移动互联网人口红利见顶,新的增长点难以寻觅,但中国短视频人均使用时长及头部短视频平台日均活跃用户均持续增长。 在淘宝,短视频业务一直以来都是非常重要的业务,是淘宝app从单一的商品导购app走向商品导购+内容消费的多元化app的关键所在。现如今淘宝每年新增内容数达数十亿,其中视频数占比持续提升,预计到2022年视频的占比会超过50%。如何对规模如此庞大的视频进行内容化理解,高效赋能视频运营和个性化分发变得极为关键。 因此,针对视频内容理解技术,我们将重点构建一个面向视频的层次化、精细的标签体系和算法,为新生产的视频提供冷启动能力,提升分发的效率,另一方面构建视觉内容和文本语义之间的跨模态检索算法,根据用户的检索,提供更加优质的视频内容,提高用户的消费体验。 下面橙子就来和大家揭晓——本场论坛的演讲嘉宾和议题。 淘宝直播端上窄带高清技术 演讲时间:13:50-14:20 演讲嘉宾:王立波(庄恕),阿里巴巴淘系技术部高级算法专家,毕业于上海交通大学数学系,现为淘宝直播音视频算法负责人,从事视频压缩,图像处理,语音增强方向的研究,参与完成的项目《编码摄像关键技术及应用》获得2019年国家科技进步二等奖。 演讲内容:在淘宝直播大规模实时系统中,如何通过底层算法及技术架构的升级,实现高画质,高音质,低延时直播,在确保用户体验的前提下,实现成本的大幅缩减。 5G时代的国际视频标准化最新动态 演讲时间:14:20-14:50 演讲嘉宾:叶琰,阿里巴巴(美国)达摩院研究员,负责前沿视频技术研发和高性能视频编解码硬件及软件实现。她的团队代表阿里巴巴在各个视频标准组织积极进行技术推进,包括国际视频标准(ITU-T/VCEG 与 ISO/IEC/MPEG),国家视频标准(AVS),以及业界视频标准联盟(AOM)等。她参与了多项视频编解码与流媒体的国际标准制定工作,包括 H.266/VVC,H.265/HEVC,SHVC,MV-HEVC,HEVC SCC,H.264/SVC,MPEG PCC,MPEG LCEVC,MPEG DASH,MPEG OMAF和MPEG CMAF 等,并曾就任多项国际标准的编辑。她是美国国家标准组织INCITS L3.1的主席, IEEE 的高级会员。她在中国科技大学获得本科及硕士学位,在美国加州大学获得博士学位。 演讲内容:随着5G时代的到来,超高清4K/8K视频,AR/VR/MR浸入式视频,以及机器视频等全新的视频消费形式将很快走入人们的日常生活中。为了能够高效低延时地传输处理这些海量的视频数据,必须要有最先进的视频编解码技术提供底座支撑。国际视频标准组织ISO/IEC MPEG和ITU-T近几年陆续出台多项相关的国际视频标准,其中包括最近刚刚出炉的下一代视频标准H.266/VVC,浸入式媒体标准系列MPEG-I,以及MPEG正在探索的机器视频编码。这个演讲将回顾VVC的发展历程以及VVC标准的压缩性能和它所能提供的各种灵活易用的功能,并一起展望AR/VR等浸入式视频内容在5G网上的未来。 内容AI升级:视频分析与生成 演讲时间:14:50-15:20 演讲嘉宾:潘攀(启磐),阿里巴巴达摩院资深算法专家,负责电商的视觉技术研发,服务于拍立淘,淘宝直播等应用场景。他博士毕业于美国伊利诺伊大学芝加哥分校,研究领域包括深度学习和计算机视觉等。他曾先后在美国三菱研究院和北京富士通研发中心从事视觉技术研发工作。 演讲内容:近年来围绕电商升级,电商内容从之前的图像/文字,走向了更富模态的直播和短视频。面临新的内容形式和新的业务形态,视觉技术也需要进行升级。比如通过升级图像的分析能力到视频,我们可以精确解析出视频出现的实体和关键属性。再比如通过结合三维和生成技术,我们创新得研发了虚拟主播这个新的产品形态。本次演讲会描述伴随内容升级的视频分析和生成技术,以及这些技术在阿里巴巴的各种应用。 深度学习在端侧AI的发展之路 演讲时间:15:20-15:50 演讲嘉宾:李晓波(篱悠),阿里巴巴淘系技术资深算法专家,2009年北大硕士毕业加入阿里巴巴,先后在B2B、阿里云、手淘等BU任职。目前在手淘负责多媒体算法部门,支持淘宝直播和短视频等业务。 演讲内容:随着技术的迭代更新,新的媒体形式不断推陈出新。短视频/直播、VR、AR、3D、MR等新兴的媒体技术不断出新,方新未艾。那么做为电商购物场景的手淘,在业务发展的过程中又是如何利用这些新兴技术来为用户带来更好的购物体验呢? 精彩内容将在10月23日(今天)13:30-16:00线上同步直播。 可扫描下方二维码,进行线上直播观看。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
作者简介:珑晴——淘系技术部前端技术专家,16 年校招实习转正进入的阿里,当时是在聚划算前端团队,随着业务变化一路从聚划算到天猫至今加入淘系技术部,负责日常活动营销的同时,也多次参与大促会场&互动的研发,在支持业务的过程中不断挖掘提炼创新,最终完成从前端小白到前端技术专家的蜕变。 本文,将结合笔者的 4 年,从以下几点具体谈谈我的前端成长之路。 职业经历 关键技能 一些反思 职业经历 ▐ 职场初探-实习的那几个月 首先是实习期,当时是北邮的一个师兄内推来的杭州这边。实习期间,主要负责 2 块东西, XList 和周末淘宝, XList 是聚划算之前的一个解决无尽滚动的框架,当时主要还是在熟悉团队的工具链、看源码,顺便给其他业务打打杂。 这个时期的重点是团队融入和兴趣探索,对实习生本身能力要求并不高,所以这个阶段的同学们不要过于焦虑,重点还是看自己的个人意愿与团队的匹配度来进一步决定是否要继续待在这样的团队。 ▐ 新人菜鸟-入职第1年 然后是入职第 1 年,我的菜鸟时期。我入职的时候,刚好赶上聚划算整个在做品牌升级,当时存在一个商家素材不规范的问题,亟需解决以提升用户体验。 我就在师兄的指导下,设计了这么一个基于 PSD 解析的规范化合图方案,这是技术方案的简图。 在 PSD 解析的基础上,我还提供了一个可视化界面给设计师调控实现配置生成图片,界面是参考 sketch 实现的。 这个方案在聚划算整个实施之后,也取得了很好的业务结果,覆盖了 90% 的业务线,平均每天合图超过 1w 张。而这个工具,也是当时团队内部第一个完整的 node 应用。 总的来说,第 1 年是新人时期,在这段时间,要开始学习识别业务需求,能进行方案设计,推动个人技术栈的成型。这个时期,我们完成的是从学生到社会人身份上的转变,在做业务的同时,一定要时不时的抬抬头,把自己做的东西拿出来跟大家分享,既增进了团队对自己的了解,也促进了自己对知识的进一步总结概括。 ▐ 独当一面-升级打怪 接下来,就是作为新人成功 landing 之后的升级打怪了。这个时期,我给自己的定义是独当一面。这里主要结合我个人做的另一个工具阐述。 坑位研发,是我们电商前端工程师日常开发过程中绕不开的事情,这是一些常见的商品坑位。 受之前做的配置生成图片工具的启发,我萌生了用编辑器产出布局的想法。 然后,我就做了一个坑位可视化开发工具,左边是编辑器界面,右边是消费者侧渲染的 demo。通过这个工具,设计人员简单复制粘贴、拖拖拽拽就可以生成一个新坑位,极大地降低了 UI 的开发成本。 这是当时在会场上的应用情况,做到了业务变化的分钟级响应。 但是,工具在推广过程中,也遇到了一些问题,包括如下 不支持存在事件交互、数据处理等逻辑的场景 运行时解析方式在端上性能不佳 脱离正常的研发流程 。。。 当然,这些问题也正好给了我独立 owner 且重构产品的机会。于是,我结合了当时天猫的研发体系,推出了融入开发体系的模块可视化研发方案,即 2.0 版本。这是 2.0 版本的完整链路图。 下图是 2.0 版本上线之后取得的结果,也是在那一年拿到了年度 3.75,并成功晋升。 从我个人角度,独当一面这个时期的重点是在能游刃有余的完成分发到自己的工作之外,建设核心能力、寻求突破。这个时期,已经对业务有了较为深刻的了解,可以从零到无设计一块东西,技术上能够对接业务方,技术之外要能做项目管理,协同多方有效完成任务。 关键技能 接下来,我将重点介绍下我过去 3 年多解锁的一些关键技能。 ▐ 技能一:跳出技术视角 第 1 个关键技能是跳出技术视角。不少开发都有类似【我只是一个技术,不应该参与 QA、PM 的工作】的想法,实际上也确实有人在技术路线越走越精深,并得到了认可。然而,凡事都有个但是,很多人技术水平非常高,但一直得不到认可,这种现象更为常见。尤其对于我们前端来说,很难彻底脱离上下游、脱离业务方来完成工作,甚至除了 QA 和 PM 的工作之外,还需要向业务方 “推销” 自己的工具。一个更加 “全面” 的前端才能更加顺风顺水。那么,怎么做到更加全面呢?可以从以下几个方面入手,比如从 why 出发做事情,做业务的同学,关注业务数据、跟进线上问题,做技术产品的同学,提供出色的产品服务文档。 ▐ 技能二:追求极致 第 2 个关键技能是追求极致,也可以理解为我们常常称赞的匠人精神。以我为例,我入职第一年就做了个在线编辑器,在完成基础功能的同时,还增加了快捷键、辅助线、自动吸附等功能,不断打磨编辑器的产品体验,因此也吸引了很多小伙伴的加入,共同把这个产品推广到更多的业务域。 ▐ 技能三:结构化的表达 第 3 个关键技能是结构化的表达。俗话说的好,酒香也怕巷子深,结构化的表达,是我们在完成事情的前提下,更好地让别人(比如你的老板)get 到你的产出及价值。结构化的表达离不开结构化的思维方式,大家可以看看金字塔原理这本书。除此之外,在建立结构化思维的过程中,好的画图工具也能实现事半功倍的效果。 既然是复盘,也来做下自我批判,也是对大家的一些建议 一些反思 ▐ 反思一:No 三点一线 程序员长时间工作、加班,三点一线的奔波,对身体伤害其实蛮大的。身体是革命的本钱,要保持一个良好的健身习惯,不需要很频繁或者很专业的锻炼,其实每周 1 次的健身房养成习惯就好。保持一个良好的身体状态,工作时其实也能更有精力。 ▐ 反思二:Keep learning 经常会有人说,做业务一直在 CRUD,感觉工作几年下来没什么成长。诚然,有意思、有挑战的工作确实不多见,更多的是一些琐碎的、重复的工作。所以,在工作之外,也需要花一些时间,关注下业内资讯、新闻,看看书、写写博客、参与一下 GitHub 感兴趣的项目。keep learning,保持对新技术的关注~ 好书推荐 最后向大家推荐《凤凰项目》这本书,他是以小说的形式,讲述了一个凌乱的无可救药的运维项目组,是如何一步步达成最后高效且舒心的工作状态。通过该书,能学会有效的自我、团队、项目管理方法,让开发不再深陷于无尽的业务交付。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
随着iOS14 系统的更新, iOS 系统的隐私保护上了一个新的台阶,用户的隐私得到了更加好的保护;iOS14 系统出现了很多新的特性,Widget 可以让用户的桌面更加丰富,定制型更加强;Clips 可以让用户在无需安装应用的情况下体验应用; Swift 语言进一步发展,将进一步促进原生技术的发展 ——彭玉堂(巴格) 点击此处立即下载>>《iOS开发者必读资讯》 亮点内容抢先看 一、详解 WWDC 20 SwiftUI 的重大改变及核心优势 6月23日凌晨 1 点,苹果 WWDC20 开发者大会在线上以主题演讲的方式,在 Apple Park 进行直播。23-26日,苹果公开了 100 多个面向开发者的视频,内容涵盖Swift / SwiftUI 、App Clips、Widgets、Privacy & Security 等等方面。对于开发者和程序员来说,我们有哪些新发现和新思考?点击了解>> 详解 WWDC 20 SwiftUI 的重大改变及核心优势 二、iOS14 隐私适配及部分解决方案 在刚刚结束的线上 WWDC 2020 发布会上苹果向我们展示了新的 iOS14 系统。iOS14 的适配,很重要的一环就集中在用户隐私和安全方面。 在 iOS13 及以前,当用户首次访问应用程序时,会被要求开放大量权限,比如相册、定位、联系人,实际上该应用可能仅仅需要一个选择图片功能,却被要求开放整个照片库的权限,这确实是不合理的。对于相册,在 iOS14 中引入了 “LimitedPhotos Library” 的概念,用户可以授予应用访问其一部分的照片,对于应用来说,仅能读取到用户选择让应用来读取的照片,让我们看到了 Apple 对于用户隐私的尊重。这仅仅是一部分,在iOS14 中,可以看到诸多类似的保护用户隐私的措施,也需要我们升级适配。 最近在调研 iOS14的适配方案,本文主要分享一下 iOS14 上对于隐私授权的变更和部分适配方案,欢迎补充指正。iOS14 隐私适配及部分解决方案 Metal新特性:大幅度提升iOS端性能 作为较早在客户端侧选择Flutter方案的技术团队,性能和用户体验一直是闲鱼技术团队在开发中比较关注的点。而Metal这样的直接操作GPU的底层接口无疑会给闲鱼技术团队突破性能瓶颈提供一些新的思路。 本文将会详细阐述一下这次大会Metal相关的新特性,以及对于闲鱼技术和整个淘系技术来说,这些新特性带来了哪些技术启发与思考。Metal新特性:大幅度提升iOS端性能 四、Swift 5.3 又更新了什么新奇爽快的语法? Swift 在 WWDC14 正式发布到 2019,经过 5 年的不断迭代,这其中经历了标准库变动,语法的增减。首先使用 Swift 作为开发语言的开发者们都苦不堪言,戏称《Swift 从入门到重学》,几乎每一年 Swift 都会迎来比较大的改动,甚至 API 都发生了变化。 WWDC 19 苹果发布了 Swift 5.0,苹果终于宣布 Swift 的 ABI 稳定。这标志着 Swift 这门语言已经趋于稳定,在 2019 至 2020 的迭代中,Swift 5.2 也做到了模块稳定,之前的大修大改已经不会在出现了。Swift 5.3 又更新了什么新奇爽快的语法? 五、Apple Widget:下一个顶级流量入口? 2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这可谓是一次可以载入史册的发布会,宣布了 ARM 架构 Mac 芯片、软硬件的生态大统一、iOS 14 系统界面大改等一系列激动人心的消息。当然,最让我感兴趣的就是让 iOS 界面大改的 Widget 了。过去几年,iOS 的桌面交互体验可谓是一言难尽,Widget 的加入无疑是一次比较大的破局。在看发布会的时候,我的脑海里就浮现出一个问题:“这会是下一个互联网公司竞争的流量入口吗?”Apple Widget:下一个顶级流量入口? 六、Swift 5.3的进化:语法、标准库、调试能力大幅提升 Swift 从 5.0 的 ABI 稳定到5.1 的模块稳定,Swift 终于不是《Swift 入门到重学》了。本次 WWDC2020,Swift 5.3 正式发布,Swift 依旧朝着安全、高效、易读的方向持续发力,不断的在改进语法,增强代码的表达能力和易用性。因为 Swift 的模块稳定,SPM 现在也支持了二进制模块的分发,逐渐完善的社区生态也在不断拓宽 Swift 可以涉足的领域,而不仅仅是在 Apple 平台之上。Swift 5.3的进化:语法、标准库、调试能力大幅提升 七、WWDC:无线网络优化实践,带来哪些启发? 网络技术作为互联网应用赖以存在的技术基础,速度与安全永远是其核心使命,本次WWDC的网络类topic涵盖内容基本还是围绕这两个点来展开。本次WWDC网络类session在基础网络技术上譬如新协议、新算法方面着墨并不多;也未提出新的类似NSURLSession / Network.framework之类的新网络组件。站在应用视角,本次WWDC网络类session可分为两大类: 无线网络体验优化实践在系统层面的标准化; 本地网络应用的权限管控增强。 在第一类议题中,我们看到很多已经在手淘中的类似实践,或标准或自研,说明手淘在网络技术的开发与应用上还是较为深入和前沿的,基本走在全球业界前列。根据我们手淘的业务特点,笔者重点关注第一类session,并简单探讨该新技术可以我们带来什么样启发和变化。WWDC:无线网络优化实践,带来哪些启发? 藏经阁系列精品保证 本书为阿里云开发者社区 “藏经阁” 系列图书。开发者技术精品“藏经阁”,超全阿里系电子书开放下载,覆盖Java、物联网、云原生、前端、大数据、开源等技术领域,深度分享阿里工程师实践精华,顶级技术内容一键获取:https://developer.aliyun.com/ebook 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
在C语言中,有些由内存需要程序员在代码中进行手动回收,但是在Java中,没有这样的声明式操作。有没有人有去想过,Java到底做了什么可以自动进行垃圾回收呢?Java中的垃圾回收,是一点都不需要程序员关心,万无一失的吗? 本文将从:Jvm中的垃圾收集器和内存分配策略。虚拟机中对已经死亡的对象都有哪些垃圾回收是算法,两部分和大家谈谈Java虚拟机的垃圾收集器与内存分配策略。 重垃圾收集器和内存分配策略 垃圾收集(Garbage Collection,GC),并不是随着Java一起诞生的。GC的历史比Java来得更加久远,早在1960年的时候,MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考GC需要完成的三件事情:哪些内存需要回收?什么时候回收?如何回收? 在经过半个世纪的发展后,对于这三个问题的答案越来越清晰,总结成就是:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。 在Java程序编写的过程中,我们可以知道代码的逻辑是怎样的,但是具体的分支只有在运行过程中才能知道。而这部分的内存分配和回收也是动态进行的,垃圾收集器主要关注的就是这部分内存。 那么实际中,一个需要解决的问题就是,如何判断对象是否存活,对于不再存活的对象,进行垃圾回收。 在经过漫长的发展后,目前主要有下面几种算法来进行对象存活判断。 ▐ 引用计数算法 算法的定义为:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。 这是实现简单,且效率非常高效的一种算法。在redis、python的虚拟机、FlashPlayer等应用中,也都有采用这样的算法。但是Java中并没有采用这样的算法实现,主要原因是其存在相互循环引用的问题。 简单来说,A对象引用B对象,B对象引用A对象的情况下。A和B互相引用,于是他们的计数器都不会为0,于是GC收集器便就永远无法回收他们。 ▐ 根搜索算法 算法的定义为:通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,或者说不可达的时候,则证明此对象不可用。 在Java语言中,可以作为GC Roots的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中的引用的对象。 方法区中的类静态属性引用的对象。 方法区中的常量引用的对象。 本地方法栈中JNI(即一般说的Native方法)的引用的对象 ▐ 引用 在早期的JDK定义中,引用的定义为,如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但这样的定义方式过于纯粹,一个对象只有两种状态,即被引用或者没有被引用两种。对于一些缓存类型的数据,则显得有些鸡肋,更无法体现内存分配的价值。 之后JDK对于引用进行了概念扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。 ▐ 是否死亡 在根搜索算法中,在GCRoots没有可以到达的引用链之后,就一定会“死亡”吗?其实也不一定,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。 当这个对象需要执行finalize()方法时,这个对象会被放置在一个名为F-Queue的队列中,并稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里的“执行”是虚拟机会触发这个方法,但并不一定会等待它运行结束。因为如果对象在finalize()方法中死循环或者超长时间执行,可能导致F-Queue队列中的其他对象永久处于等待状态,甚至可能导致内存回收系统奔溃。 finalize()方法是对象可以存活的最后一次机会,在这里可以将自己和引用链上的任何一个对象建立关联即可,否则就会进入到垃圾回收的系统中。但finalize()依旧是一种充满不确定性的方法,在诞生之初亦是为了C/C++程序员的更容易接受的一种妥协,推荐目前的try-finally方法处理更加优雅,也更安全可靠。 接着我们一起来看看虚拟机中对已经死亡的对象都有哪些垃圾回收是算法。 ▐ 标记-清除算法 标记-清除算法(Mark-Sweep)可以说应该是最基础的收集算法了。从字面意思很好理解,算法的过程分为标记过程和清楚过程。首先标记出所有需要回收的对象,在标记完了之后,对标记对象进行统一的回收工作。哪些对象需要标记,哪些对象不需要标记,这个再上一篇文章中进行了详细的介绍,可以回顾再了解下。 这个算法的缺点也非常明显,内存中的被标记的数据不一定都是连续,因此标记清楚之后,内存中会产生大量的内存碎片,碎片的存在也会导致在后续分配较大对象时候找不到足够的连续空间,导致内存不足。还有一个问题,便是标记和清楚的效率都不高。 但之所以说这是最基础的收集算法,是因为后续是算法基本上都是由此改进得来的。 ▐ 复制算法 为了解决效率问题,诞生了一种叫复制(Copying)的算法。该算法将可以用的内存空间划分为两大块,每次只使用其中的一块。当这块内存使用完了之后,就将还存活的对象复制到另一块空间中去。这样就不需要考虑内存碎片的问题,只需要移动堆顶指针,按顺序分配内存即可,简单高效。同样缺点也很明显,这样做了之后很明显,我们只能使用内存中的一半内存。代价还是比较高。 那么目前的虚拟机新生代中,就采用了这种回收算法。新生代的空间相对较小,内存空间由Eden,和两块Survivor空间组成,分配比例为8:1:1,也就是最多只有10%的空间是处于空闲的。当进行回收时,将新生代的Eden和其中一块的Survivor中的还存活的对象一次性拷贝到另一块Survivor的空间上,然后清理掉Eden和刚才用过的Survivor的空间。如果当Survivor的无法存放时候,就会进入老年代存放。 ▐ 标记-整理算法 复制算法在对象存活较高的时候,就会执行较多的复制操作,从而降低整体的回收效率,还有存在50%的空间浪费。基于这种情况,有人对标记-清楚算法进行改进,从而衍生出标记-整理(Mark-Compact)算法。 这种算法的标记过程和”标记-清楚“算法一致,不同的是标记完成之后,让所有存活的对象都移动到内存的一端,然后清理掉边界外面的内存。 ▐ 分代收集算法 当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。 哪些垃圾回收是算法? ▐ 垃圾收集器 收集算法是用以支撑内存回收的理论,在虚拟机中对应的具体实现就是垃圾收集器。不同的厂商和开发者,可以依据自己的应用特点来实现对应的收集器,因此不同版本之间的收集器可能存在较大的差别。以下收集器内容摘录自参考书籍《深入理解Java虚拟机》 ▐ Serial垃圾收集器 Serial是最基本、历史最悠久的垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。 Serial是一个单线程的收集器,它不仅仅只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。 ▐ ParNew垃圾收集器 ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。 ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。 ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。 ▐ Parallel Scavenge收集器 Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 Parallel Scavenge收集器提供了两个参数用于精准控制吞吐量: XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,是一个大于0的毫秒数。 XX:GCTimeRation:直接设置吞吐量大小,是一个大于0小于100的整数,也就是程序运行时间占总时间的比率,默认值是99,即垃圾收集运行最大1%(1/(1+99))的垃圾收集时间。 Parallel Scavenge是吞吐量优先的垃圾收集器,它还提供一个参数:-XX:+UseAdaptiveSizePolicy,这是个开关参数,打开之后就不需要手动指定新生代大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、新生代晋升年老代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以达到最大吞吐量,这种方式称为GC自适应调节策略,自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。 ▐ Serial Old收集器 Serial Old是Serial垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。 在Server模式下,主要有两个用途: 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。 作为年老代中使用CMS收集器的后备垃圾收集方案。 ▐ Parallel Old收集器 Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。 ▐ CMS收集器 Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验,CMS收集器是Sun HotSpot虚拟机中第一款真正意义上并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时工作。 CMS工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下4个阶段: 初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。 并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。 CMS收集器有以下三个不足: CMS收集器对CPU资源非常敏感,其默认启动的收集线程数=(CPU数量+3)/4,在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。 CMS无法处理浮动垃圾(Floating Garbage),可能会导致Concurrent ModeFailure失败而导致另一次Full GC。由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。CMS垃圾收集器不能像其他垃圾收集器那样等待年老代机会完全被填满之后再进行收集,需要预留一部分空间供并发收集时的使用,可以通过参数-XX:CMSInitiatingOccupancyFraction来设置年老代空间达到多少的百分比时触发CMS进行垃圾收集,默认是68%。 如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次ConcurrentMode Failure失败,此时虚拟机将启动预备方案,使用Serial Old收集器重新进行年老代垃圾回收。 CMS收集器是基于标记-清除算法,因此不可避免会产生大量不连续的内存碎片,如果无法找到一块足够大的连续内存存放对象时,将会触发因此Full GC。CMS提供一个开关参数-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理,内存整理会使得垃圾收集停顿时间变长,CMS提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理。 ▐ G1收集器 Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与CMS收集器,G1收集器两个最突出的改进是: 基于标记-整理算法,不产生内存碎片。 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。 区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。 总结 其实相对于C和C++语言,Java程序员依赖JVM的强大内存管理能力,已经不再需要对内存进行分配或者释放等操作。所以Java程序员往往很少关注内存中潜在的泄露和溢出等问题。但当这个问题出现时候,如果对虚拟机内存管理机制没有足够多的掌握,会难以定位和解决问题。去了解虚拟机的发展历程以及现有的管理机制,可以更好地理解为什么这样设计,同样能提高自己的问题解决能力。 淘系技术部-天猫奢侈品团队 我们是一支支撑天猫奢侈品、品牌客户、淘宝心选等大店数据化经营解决方案的技术团队,依托于阿里大中台推动品牌经营解决方案升级,不断提升客户经营的效率,持续提升业务价值赋能业务。 如果您有兴趣可讲简历发至:gangmin.zgm@alibaba-inc.com,期待您的加入! 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
作者|侯超群(初类)编辑|橙子君出品|阿里巴巴新零售淘系技术 前言: 在大数据和算力的助力下,深度学习掀起了一波浪潮,在许多领域取得了显著的成绩。以监督学习为主的深度学习方法,往往期望能够拥有大量的标注样本进行训练,模型能够学到更多有价值的知识(如下左图展示了3组常见的图像分类数据集,拥有上万的标注样本)。然而,实际应用场景的标注样本严重稀缺。并且,标注大量样本将产生昂贵的标注成本(如下右图所示,标注一张X射线图需要5分钟和30元左右的成本,一张CT图需要20分钟和70元的成本)。在庞大而复杂的淘系电商场景中,类似的需求比比皆是:例如,咸鱼&躺平和洋淘等社区内容的治理,拍立淘的以图搜图,服饰分类(例如,iFashion)等场景都存在标注样本严重稀缺的问题。综上,在实际应用场景中,如何“在模型达到目标性能的前提下,尽可能地减少标注成本”是一项亟需解决的挑战。 主动学习作为机器学习的一个子领域,旨在以尽可能少的标注样本达到模型的目标性能,广泛应用于实际需求中。本文的定位是主动学习方法的入门篇,主要介绍的内容包括:1)详细地介绍主动学习的基础知识;2)简要地介绍主动学习在学术界的研究现状;3)主动学习实践部分将简单介绍几个图像分类的案例;4)文末将给出本文的参考文献和相关资料。 主动学习的基本知识 主动学习的概念和基本流程 主动学习是什么:Burr Settles[1] 的文章《Active Learning Literature Survey》详细地介绍了主动学习:“主动学习是机器学习的一个子领域,在统计学领域也叫查询学习或最优实验设计”。主动学习方法尝试解决样本的标注瓶颈,通过主动优先选择最有价值的未标注样本进行标注,以尽可能少的标注样本达到模型的预期性能。1.1.2、主动学习的基本流程:如下图所示,主动学习方法是一个迭代式的交互训练过程,主要由五个核心部分组成,包括:未标注样本池(unlabeled pool,记为U)、筛选策略(select queries,记为Q)、相关领域的标注者(human annotator,记为S),标注数据集(labeled training set,记为L),目标模型(machine learning model,记为G)。主动学习将上述五个部分组合到同一个流程中,并通过如下图所示的顺序,以不断迭代的训练方式更新模型性能、未标注样本池和标注数据集,直到目标模型达到预设的性能或者不再提供标注数据为止。其中,在每次迭代过程中,已标注样本的数量不断增加,模型的性能也随之提升(理想情况)。在实际应用中,应尽可能保证标注者的准确率,缓解模型在训练初期学偏(此处特指错误标注的样本导致)的情况。 主动学习和被动学习、半监督学习的关系: 主动学习和被动学习:如下图(a)所示,红色实线表示理想情况下模型性能随着训练标注样本数量的增多而无限地提升。实际情况下往往是如下图(b)的红色实线所示,模型的性能不是随着标注数据量的增多而无限地提升。此外,每个模型都会有与之对应的瓶颈性能(peak performance),研究者通过增加训练数据以及调参使之不断逼近瓶颈性能。主动学习核心解决的问题正是如何使用尽可能少的标注数据达到模型的瓶颈性能,从而减少不必要的标注成本。如下图(b)的蓝色虚线所示,主动学习根据合适的策略筛选出最具有价值的样本优先标注并给模型训练,从而以更少的标注样本达到模型的瓶颈性能。 主动学习和半监督学习:在机器学习领域中,根据是否需要样本的标签信息可分为“监督学习”和“无监督学习”。此外,同时利用未标注样本和标注样本进行机器学习的算法可进一步归纳为三类:半监督学习、直推式学习和主动学习 。文献[21]简要介绍了主动学习与半监督学习的异同点:“半监督学习和主动学习都是从未标记样例中挑选部分价值量高的样例标注后补充到已标记样例集中来提高分类器精度,降低领域专家的工作量。但二者的学习方式不同:半监督学习一般不需要人工参与,是通过具有一定分类精度的基准分类器实现对未标注样例的自动标注;而主动学习有别于半监督学习的特点之一就是需要将挑选出的高价值样例进行人工准确标注。半监督学习通过用计算机进行自动或半自动标注代替人工标注,虽然有效降低了标注代价,但其标注结果依赖于用部分已标注样例训练出的基准分类器的分类精度,因此并不能保证标注结果完全正确。相比而言,主动学习挑选的样本是人工标注,尽可能引入最少的错误类标”。值得一提的是,目前已有许多研究者尝试将主动学习和半监督学习进行结合,并取得了不错的效果(本文暂不详细展开介绍,留给下一篇章)。 主动学习的基本策略 样本的筛选策略直接关系到模型能够节约标注成本的程度。例如,使用不确定性策略比随机采样策略能够节约更多的标注样本[4,5] 。因为随机采样策略既没有利用到模型的预测信息,也没有利用到大量未标注样本池的结构信息,仅凭随机采样决定优先标注的样本。而不确定性策略通过与模型的预测信息进行交互,优先筛选出相对当前模型最有价值的样本。本节将围绕部分经典的筛选策略展开讨论。 (1)随机采样策略(Random Sampling,RS): RS 不需要跟模型的预测结果做任何交互,直接通过随机数从未标注样本池筛选出一批样本给专家标注,常作为主动学习算法中最基础的对比实验。 (2)不确定性策略(Uncertainty Strategy,US): US 假设最靠近分类超平面的样本相对分类器具有较丰富的信息量,根据当前模型对样本的预测值筛选出最不确定的样本。US 包含了一些基础的衡量指标:1)最不确定指标(Least Confidence,LC)将预测概率的最大值的相反数作为样本的不确定性分数。2)边缘采样(Margin Sampling,MS)认为距离分类超平面越近的样本具有越高的不确定性,常与 SVM 结合并用于解决二分类任务,但在多分类任务上的表现不佳。3)多类别不确定采样(Multi-Class Level Uncertainty,MCLU)是 MS 在多分类问题上的扩展,MCLU 选择离分类界面最远的两个样本,并将它们的距离差值作为评判标准。MCLU 能够在混合类别区域中筛选出最不确信度的样本,如式(2.3)所示。其中,xj 表示被选中的样本,C 表示样本 xi 所属的类别集合,c+ 表示最大预测概率对应的类别,f (xi, c) 表示样本 xi 到分类超平面的距离。4)熵值最大化(Maximize Entropy,ME)优先筛选具有更大熵值的样本,熵值可以通过计算得到,其中 pi 表示第 i 个类别的预测值。5)样本最优次优类别(Best vs Second Best, BvSB)[79]主要是针对多分类问题的一种衡量指标,并且能够缓解 ME 在多分类问题上效果不佳的情况。BvSB 只考虑样本预测值最大的两个类别,忽略了其他预测类别的影响,从而在多分类问题上的效果更佳。 (3)委员会投票(Query by Committee,QBC):QBC[31]是一种基于版本空间缩减的采样策略,核心思想是优先选择能够最大程度缩减版本空间的未标记样本。QBC 包括两个基本步骤:1)使用多个模型构成委员会;2)委员会中所有的模型依次对未标注样本进行预测并优先筛选出投票最不一致的样本进行标注。由于 QBC 在实际应用的过程中需要训练若干个模型,导致具有较高的计算复杂度。基于此,熵值装袋算法(Entropy Query-By-Bagging,EQB)[80]和自适应不一致最大化(Adaptive Maximize Disagree,AMD)被提出并缓解了计算复杂度问题。其中,EQB 同时引入了 bagging 继承方法以及 bootstrap 采样;AMD 主要针对高维数据,将特征空间划分为一定数量的子集并构造委员会。 (4)其他经典的策略:梯度长度期望(Expected Gradient Length,EGL) 策略根据未标注样本对当前模型的影响程度优先筛选出对模型影响最大的样本;EGL [4] 是代表性方法之一,能够应用在任意基于梯度下降方法的模型中。方差最小(Variance Reduction,VR)策略通过减少输出方差能够降低模型的泛化误差[81,82];Ji 等[82]提出了一种基于图的 VR 衡量指标的主动学习方法,通过将所有未标注样本构建在同一个图中,每个样本分布在图中每个结点上。紧接着,通过调和高斯随机场分类器直接预测未标注样本所属的标签;在优化的过程中,通过挑选一组未标注样本进行预测并获得对应的预测类别,使得未标注样本的预测类别方差最小。 主动学习的扩展方法 近年来,主动学习策略在很多实际应用场景中取得显著的效果。但同时也存在一些亟需解决的挑战。例如,不确定性策略只关注样本的不确定性,在BMAL(批量式主动学习方法,每次迭代筛选出N>1的样本数量)场景下会产生大量具有冗余信息的样本。因此,仅使用单一的策略尚未能最大程度地节约标注成本。本节将围绕本文的核心工作简要地介绍几种主动学习的扩展方法。 (1)组合多种基本策略的主动学习方法:组合策略将多个基本策略以互补的方式进行融合,广泛应用于图像分类任务中[36,37,38,83]。其中,Li 等[36]基于概率分类模型提出一种自适应的组合策略框架。Li 等[36]通过信息密度指标(Information Density Measure)将未标注样本的信息考虑在内,弥补了不确定性策略的不足。如算法 2-2所示,该算法能够自然地扩展到更多的组合策略。 (2)结合半监督学习(Semi-Supervised Learning)的主动学习方法:自训练(Self-training)算法作为半监督学习的一种基础方法,其核心步骤如算法2-3所示。由于自训练算法在训练过程中会根据模型的预测信息,挑选合适的样本及其对应的预测标签加入训练集,而且初始化少量的标注样本能够保证模型的初始性能,因此初始化训练环节对其后续的学习过程至关重要。半监督学习算法需要解决的挑战之一是:在训练的过程中容易引入大量的噪声样本,导致模型学习不到正确的信息。部分研究员们通过构建多个分类器的协同训练算法缓解噪声样本,如Co-Training[84] 和 Tri-Training[85]。 (3)结合生成对抗网络的主动学习方法:生成对抗网络(Generative Adversarial Networks,GAN)模型以无监督的训练方式对大量未标注样本进行训练,并通过生成器产生新的样本。经典的 GAN[15] 主要包括生成器和判别器等两个核心部分,两者以互相博弈的方式进行对抗训练,直到两者达到一个动态均衡的状态。GAN 的目标函数如式(2.4)所示,其中,V(G,D)=Ex∼Pdata [logD(x)]+Ex∼PG [log(1−D(x))] 表示数据真实分布 x ∼ Pdata 与生成模型得到的分布 x ∼ PG 之间的差异。文献[19,50]将生成器和主动学习策略进行融合并构建目标函数,通过解决优化问题控制生成器产生的样本。 主动学习方法的基本评价指标 本文侧重介绍主动学习方法在保证不损失模型准确率的情况下,节约标注成本的性能,评价指标如下式所示。其中,SavedRate 表示主动学习方法相对于全样本训练减少的标注成本;ExpertAnnotated 表示当模型达到预定的目标性能时专家标注的样本数量;Full Samples 表示当前数据集提供的未标注样本数量,即全样本训练时所使用的标注样本数量。本文涉及的实验会先进行全样本训练,并分别记录最佳验证集准确率作为主动学习相关算法的目标准确率。例如,在某组数据集中使用 AlexNet 模型对Full Samples张标注图像进行训练,记录训练过程中最佳的验证准确率(Best accuracy)并将其作为主动学习的目标准确率(Target accuracy);随后,模型通过迭代过程不断提升性能,当达到目标准确率时,记录专家所标注的样本数量 ExpertAnnotated;此时,就可以算出SavedRate 的值,即该方法能够节约多少标注成本。此外,我们也会将主动学习方法与一些常见的方法进行比较,比如 RS 策略常用于基准对比实验(baseline)。 早期的主动学习面临的挑战及其解决方案 多类分类问题:在处理多类分类问题时,基于 Margin Sampling 的样例选择标准忽略了样例可能属于其他类别的信息,因此所选样例质量较差。基于熵的方法“基于不确定性的主动学习算法研究(2011)”虽考虑了样例从属于每个类别的概率,但在多类分类问题中,样例的熵也会受到那些不重要类别的干扰。文献“Multi-class active learning for image classification(2009)”提出了基于最优标号和次优标号的准则(BvSB),考虑样例所属概率最高的前2个类别,忽略剩余类别对样例选择标准产生的干扰。文献“基于主动学习和半监督学习的多类图像分类(2011)”将BvSB和带约束的自学习(Constrained self-training,CST)引入到基于SVM的图像分类中,显著提高了分类精度。 样本中的孤立点:若选择样例时能综合考虑样其代表性和不确定性,通常可避免采集到孤立点。文献“Active Learning by querying informative and representative examples(2010)”中提出了一种综合利用聚类信息和分类间隔的样例选择方法;文献“Active Learning using a Variational Dirichlet Processing model for pre-clustering and classification of underwater stereo imagery(2011)”提出了一种利用预聚类协助选择代表性样例的主动学习方法;文献“Dual strategy active learning(2007)”利用样例的不确定性及其先验分布密度进行样例选择以获取优质样例;文献“基于样本不确定性和代表性相结合的可控主动学习算法研究 (2009)”将样例的分布密度作为度量样例代表性的指标,结合以熵作为不确定性指标,提出了一种基于密度熵的样例选择策略,有效解决了孤立点问题给样例选择质量造成的影响。 训练集样本冗余:如下图所示,蓝色圆圈所表示的新训练样本中,样例1与分类超平面的距离比样例2近,根据 BvSB 准则应当挑选样例1进行标注并补充到训练集中;但紧挨着样例1的绿色样例 a 已经在训练集中,此时若再加入样例1则对分类界面影响甚微。相比而言,将样例2补充到训练集中,对当前分类模型的训练贡献度更大。通过上述分析可知,主动学习中的样例选择度量主要分为2种:1)不确定性度量;2)差异性度量或代表性度量。样例的不确定性一般可通过计算其信息熵获得,样例的代表性通常可根据其是否在聚类中心判断,而样例的差异性则可通过计算余弦相似度(基于采样策略的主动学习算法研究进展,2012)或用高斯核函数(基于多特征融合的中文评论情感分类算法,2015)获得。 不平衡数据集:文献“一种新的SVM主动学习算法及其在障碍物检测中的应用(2009)”提出 KSVMactive 主动学习算法;文献“基于主动学习的加权支持向量机的分类(2009)”提出了改进的加权支持向量机模型;文献“基于专家委员会的主动学习算法研究(2010)”提出了基于SVM超平面位置校正的主动学习算法。 主动学习的研究现状 本节将围绕如下要点对主动学习方法的研究现状展开讨论,包括:1)基于未标注样本池的主动学习策略;2)批量式主动学习方法,侧重于组合式策略以及引入聚类算法的主动学习方法;3)半监督主动学习方法;4)结合生成对抗网络的主动学习方法。此外,主动学习方法在近几年的进展不仅局限于上述归类的方法,本节将其总结在“其他主流的主动学习方法”(本文涉及的参考文献,都可以通过文末的参考文献提供的链接中获取)。 (1)主动学习方法概述:主动学习作为机器学习的一个子领域,核心思想是通过一些启发式策略找到相对最具有“价值”的训练样本,使得模型能够以尽可能少的标注样本达到甚至超过预期的效果。主动学习的概念是Simon[23]在1974年提出。随后,主动学习方法在许多领域中层出不穷,并进一步被归纳为生成式成员查询(Membership Query Synthesis)、流式主动学习方法(Stream-Based Selective Sampling)和基于未标注样本池的主动学习方法(Pool-Based Sampling)等经典的场景[4]。Angluin等[24]于1988年提出了生成式成员查询场景,模型通过预设的条件控制生成新的样本并向标注专家询问标签;由于当时生成模型的能力有限,并且无法较好的控制生成所需的样本,因此这类方法的应用范围未被推广。Atlas等[25]在1990提出了基于数据流的方法,模型按照顺序依次判断是否需要对样本进行标记。由于基于数据流的方法不需要将所有样本统一放在池中,因此适用于存储空间较小以及处理能力有限的情况(如,应用到移动设备),但存在最大的缺陷是无法获取样本的结构分布。相较之下,基于未标注样本池的主动学习方法[26]将大量未标注样本构成未标注样本池,通过设计样本筛选策略从未标注样本池中筛选出最有“价值”的样本优先进行标注。此外,伴随着互联网的热潮以及数据采集技术的不断提升,很多领域能够以廉价的成本获取大量的未标注数据。因此,基于未标注样本池的主动学习方法最流行并且广泛应用于不同的领域中,在机器学习和数据挖掘的应用中处于非常重要的地位。 (2)基于未标注样本池的主动学习方法:样本筛选策略的质量直接影响到基于未标注样本池的主动学习方法的效果。目前,一些手工设计策略不断被提出并应用到主动学习方法中,如不确定性策略和代表性策略。文献[27,28]通过计算信息熵(entropy)表示最不确定的样本。文献[12,29,30]使用SVM作为目标分类器,通过选择距离支持向量最近的样本作为最不确定的样本。Seung等[31]首次提出了基于委员会的筛选算法(Query-by-Committee,QBC),首先训练了一组分类器组成委员会。紧接着,以委员投票的方式决定筛选哪个样本作为最不确定的样本。随后,一些基于QBC的改进方法不断被提出:例如,Breiman等[32]基于Bagging提出的Query-by-Bagging(QBBAG)以及Mamitsuka等[33]基于Boosting提出的Query-by-Boosting(QBB)。对于样本的代表性策略,文献[34,35]通过使用未标注样本的先验密度(PriorDensity)作为不确定性指标的权重,从而达到利用未标注样本的目的。Settles等[28]提出一种相似的框架,使用cosine距离衡量信息密度(InformationDensity)。 (3)批量式主动学习(BatchModeActiveLearning,BMAL)方法:目前,大多数主动学习方法存在一个共同的问题:串行地筛选样本,即每次迭代选择一个样本进行标注,这种方式非常低效且无法满足大多数实际需求。在实际应用中,往往需要以分布式的形式并行处理,多名标注专家同时在不同的环境下标注样本。BMAL旨在每次迭代中能够产生一批未标注样本,同时提供给多名标注者,从而极大地提升了应用效率。BMAL的发展历程中,起初,有研究尝试将很多不同的预测模型应用到不同的策略中。但他们在筛选样本时,只使用了单一的不确定性指标或者多样性指标的主动选择策略,导致所挑选的样本中存在大量的冗余信息,从而造成了额外的标注成本。基于此,Li等[36]提出一种新颖的自适应组合式的样本筛选策略,将不确定性策略和信息密度指标进行结合。在每次迭代中,通过自适应地调整两种策略的权重,从而选择最具有“价值”的样本给专家标注,并在三组图像分类数据集上验证了所提出方法的有效性。Gu等[37]提出了一种面向多分类的BMAL,通过组合不确定性策略和多样性策略,并在两组图像分类的数据集上进行验证,实验结果表明该方法能够挑选出同时满足最不确定性和最具多样性的样本。Zhou等[38]通过组合不确定性指标和多样性指标,同时引入了迁移学习和数据增强等技术,提出了AIFT方法并将其应用到医疗图像领域,验证了该方法至少能够减少一半的标注成本。Cardoso等[39]在传统BMAL的基础上提出了一种排序批量式主动学习方法(RBMAL),通过生成一个优化过的排序表决定样本被标注的优先级。RBMAL避免了标注专家频繁等待被选中的未标注样本,实验结果表明RBMAL能够在保证甚至提升模型性能的条件下显著地减少标注成本。此外,为了更加充分利用大量未标注样本的信息,有研究员[40,41,42]尝试将聚类算法引入主动学习中。然而,目前大多数聚类方法都是先通过手工提取特征再聚类,在很大一定程度上局限于特征的质量。我们尝试将卷积自编码聚类算法[43]应用到BMAL中,通过将特征提取和聚类算法以端到端的形式整合到同一个模型里(本文暂不展开介绍)。从而既能够提升聚类性能,又能够利用卷积神经网络的优势处理更复杂的图像。 (4)半监督主动学习方法:半监督学习能够在少量标注成本的情况下训练模型,通过挑选出预测结果较明确的样本并由模型直接给标签,但是容易产生噪声标签。而主动学习则是挑选预测结果最不确定的样本给专家标注,能够保证标签质量。因此,半监督学习方法和主动学习方法的结合能够在一定程度上互补优缺。1998年,McCallumzy等[44]首次组合了QBC和期望最大化(EM)算法,使用朴素贝叶斯方法作为分类器并在文本分类任务上进行实验。随后,Muslea等[45]提出了一种QBC的改进方法,联合测试方法(Co-Testing),通过分别在不同视角训练的两个分类器共同筛选样本给专家标注,并将其与联合期望最大化(Co-EM)算法结合。Zhou等[46]尝试将Co-Testing和Co-Training方法进行结合并在图像检索任务中验证了算法的优势。此外,文献[47,48,49]组合了不确定性策略和自学习方法(Self-Training)。上述方法将半监督学习和主动学习巧妙地结合,充分利用各自的优势并弥补不足,取得了显著的成绩。然而,目前的半监督主动学习方法尚未对噪声样本进行有效地处理,因此仍会对模型造成不小的影响。 (5)结合生成对抗网络的主动学习方法:GANs对提升主动学习方法的样本筛选效率具有重要的意义。文献[19,50]将主动学习策略结合生成器构建目标函数,通过解决优化问题使得生成器直接生成目标样本,提升了筛选样本的效率。Huijser等[20]首先使用GAN沿着与当前分类器决策边界垂直的方向生成一批样本。紧接着,通过可视化从生成的样本中找出类别发生改变的位置,并将其加入待标注样本集。最后,通过大量的图像分类实验验证了该方法的有效性。此外,除了图像分类任务以外,主动学习方法与GAN的结合也广泛应用到其他领域中,例如离群点检测[21]。 (6)其他主流的主动学习方法:Huang等[51]提出一种针对深度神经网络的主动学习方法,能够用更少的标记样本将预训练好的深度模型迁移到不同的任务上,从而降低深度神经网络的学习代价。Huang等[52]提出一种结合主动学习和矩阵补全技术的方法,能够在特征缺失严重的情况下有效利用标记信息,节省特征提取代价。Chu等[53]认为应用在不同数据集上的主动学习策略存在有效的经验,并且这些经验可以被迁移到其他数据集中进而提升模型或者策略的性能。作者尝试将模型迁移到不同的数据集中,实验部分证明了当前大多数策略不仅存在有效的经验,而且经验能够被迁移到不同的数据集中,并提升特征学习任务的性能。 (7)NAS + Active Learning:最后,值得一提的是,考虑到上述归纳的主动学习方法中,任务模型是根据先验知识从现成的模型中筛选,即模型的网络结构是固定的。存在如下缺陷:1)很多领域没有现成的模型可用,例如医疗图像领域;2)在前期的迭代过程中,标注样本量较少,固定网络结构(通常会比较复杂一点)的模型可能会陷入过拟合。如下图所示,Geifman 等人首次尝试将NAS应用到主动学习方法中,使得模型的网络结构能够自适应新增的标注数据。实验结果表明,加入NAS后的主动学习方法的效率显著地优于固定网络结构的主动学习方法。 主动学习实践:牛刀小试 主动学习如何减少标注样本的简单案例 如下图所示,文献《Active Learning Literature Survey》提供了一个基于 pool-based的主动学习案例。其中,数据集(toy data)是从高斯分布产生的400个样本,任务是2分类问题(每个类有200个样本),如(a)图所示将这些数据映射在2D特征空间上;图(b)使用了逻辑回归模型,通过训练随机选择的30个标注样本,得到70%的验证精度,蓝色线表示决策边界(decision boundary);图(c)同样使用逻辑回归模型,但训练的30个标注样本是通过主动学习策略(uncertain strategy)选择而来,达到90%的验证精度。这个简单的案例体现了引入主动学习策略所带来的效果,使用30个标注样本能够提升20%的精度。值得注意的是,上述2分类的样本分别200个,样本数据非常平衡。但是在实际应用中,分类样本数据比例往往不能达到1:1,相关领域的研究者正在尝试解决这类问题。 图像分类数据集的实践 如算法2-1所示给出了“基于为标注样本池的主动学习方法”,本文也在第一部分详细地介绍了主动学习的基本流程,此处不再赘述。 本文分享的实践部分,按照算法2-1分别对MNIST、Cifar-10和Dog-Cat三个数据集进行实验(分类模型使用了AlexNet,深度学习框架使用了PyTorch)。如下表所示,在MNIST数据集的实验中(train_num=55000, val_num = 10000):1)使用全部5.5万的训练数据直接训练模型,在1万个验证集得到的准确率为98.99%;2)使用主动学习的不确定性策略(Uncertainty Strategy),只需要5000张标注样本,在相同的1万个验证集得到的准确率就达到99.14%。此外,将训练好的模型对剩余的50000(55000-5000)张样本进行预测,得到99.70% 的效果。由此可见,仅仅使用不确定性策略在MNIST数据集上,就能够显著地减少大量的标注成本。 值得注意的是,表中所示的三组图像分类数据集acc_left_active_samples 的准确率都很高。这部分样本表示未被主动学习策略筛选中的样本,即当前模型已经具备识别这部分样本的能力。因此,当模型在训练数据集下的准确率达到 99.4% 时,使用当前模型对 acc_left_active_samples 这部分样本进行预测的精度也同样在 99.378% 左右,甚至更高。 问题1:主动学习为什么有时还能提升分类模型的准确率?杨文柱等人发表的“主动学习算法研究进展”给出的解释是:标注样本可能存在低质量的样本,会降低模型的鲁棒性(模型过渡拟合噪声点)。如何高效地筛选出具有高分类贡献度的无类标样例进行标注,并补充到已有训练集中逐步提高分类器精度与鲁棒性是主动学习亟待解决的关键问题。 问题2:不确定性策略具体怎么实现?重点关注每个样本预测结果的最大概率值:p_pred_max。我们初步认为 p_pred_max>0.5 的情况表示当前模型对该样本有个确定的分类结果(此处分类结果的正确与否不重要);反之,当前模型对该样本的判断结果模棱两可,标记为hard sample;比如:模型进行第一次预测,得到10个概率值,取其最大的概率 p_pred_max; 对P(real lable) < p_threshold(此处的10分类任务取p_threshold=0.5)的样本进行排序,取前N个样本加入集合train_samples中; 淘系商品的二分类问题 背景:商品的单包装和多包装属性影响着客户对商品价格的认知。比如:有些多包装属性的标价较高,但实际单价可能已经很划算了,而客户误将多包装的价格认为是单价,导致购买意向降低。因此区分出商品的包装属性对提高客户购买意向和优化商品价格分布具有较大的实际意义。对于此问题,有多种不同的解决方案。其中,基于图像的分类方法能够直接的区分出商品的单/多包装属性。然而,监督学习需要大量的标注样本,众多品类将产生大量的标注需求,如何能够显著地减少标注代价也同样具有重大的意义。因此,我们尝试将主动学习方法应用图像分类中,解决单包装和多包装的二分类问题。如下图所示,我们分别对比了随机筛选策略和不确定策略。实验结果表明,引入不确定性策略主动筛选样本显著地减少了标注成本。 此外,我们尝试了更加复杂的模型(DesNet121),提高模型学习能力的同时,也带来了更多训练时长的弊端。但总体的分类精度提升了3pt。同时,我们也分别在AlexNet和DenseNet121等模型上验证了模型预训练带来的效率。 参考文献 本文涉及的参考文献较多,由于篇幅问题,参考文献详见:•https://blog.csdn.net/Houchaoqun_XMU/article/details/103094113•https://blog.csdn.net/Houchaoqun_XMU/article/details/96210160 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
对于任何技术栈,都会有一个绕不过去的坎,那就是性能优化,而对于如何进行性能优化,最重要的前提就是需要知道具体的耗时分布,要知道耗时分布,就得打点(时间戳),一般的性能打点都是一些散点,比较凌乱,而本文要讲的 Tracing 则是性能打点的一种非常优雅的实现,它以瀑布流的形式呈现,非常直观,还有一个更直观的名字叫 火焰图 Tracing 顾名思义 —— 追踪每段耗时分布。 背景 上面这张图是 Flutter Engine 初始化过程中的一部分流程,非常直观的反应了执行流程中每个阶段的耗时分布。 Tracing 是 Chrome 开发者工具中强大的性能分析工具之一,它能记录 Chrome 所有进程间的各种活动。例如能记录每个进程中每个线程里 C++ 或者 JavaScript 方法的调用栈/耗时,不仅仅如此,还能看到视图 Layer 之间的层级关系,相关文档介绍 The Trace Event Profiling Tool (about:tracing)。 本文会专注在 Flutter Engine 中 Tracing 原理与实践,会分为原理篇与实践篇,原理篇会涉及到具体实现,实践篇主要包括如何使用、分析、定制。 ⚠️:Flutter 中用 Timeline 这个词代替了 Tracing,Flutter Devtool 也提供了 Timeline 工具(展示的就是 Tracing 结构的信息)。这两个词是一个对等的概念,下文提到的 Timeline 可以和 Tracing 对等。 原理篇 整个 Timeline 的过程主要包括初始化 Timeline 与记录 Tracing 信息两个部分。 ▐ 初始化 Timeline 初始化 Timeline 包括四个过程:注册 Flag、设置 Flag、TimelineStream 初始化、Timeline 初始化。注册 Flag Flutter 中会注册非常多的 Flag 用于各种功能标记,对于 Timeline/Tracing 功能就是 timeline_streams 标实,具体如下: /path/to/engine/src/third_party/dart/runtime/vm/timeline.cc // 执行宏定义 DEFINE_FLAG(charp, timeline_streams, NULL, "Comma separated list of timeline streams to record. " "Valid values: all, API, Compiler, CompilerVerbose, Dart, " "Debugger, Embedder, GC, Isolate, and VM."); // 展开后: charp FLAG_timeline_streams = Flags::Register_charp(&FLAG_timeline_streams, 'timeline_streams', NULL, "Comma separated list of timeline streams to record. " "Valid values: all, API, Compiler, CompilerVerbose, Dart, " "Debugger, Embedder, GC, Isolate, and VM."); 其中 charp 为 typedef const char* charp; 真正执行的函数如下: /path/to/engine/src/third_party/dart/runtime/vm/flags.cc const char* Flags::Register_charp(charp* addr, const char* name, const char* default_value, const char* comment) { ASSERT(Lookup(name) == NULL); Flag* flag = new Flag(name, comment, addr, Flag::kString); AddFlag(flag); return default_value; } 其中 addr_ 是一个 union 成员,初始值为当前注册函数的默认值为 NULL,即 FLAG_timeline_streams 初始值为 NULL。 注册 Flag 的过程就是定义了 FLAG_timeline_streams 标记。 设置 Flag 在 Flutter Engine 初始化的过程中,可以进行 DartVm 参数的透传,例如 —trace-startup,这个参数就可以记录启动时 Tracing 信息,会由如下方法进行设置: path/to/engine/src/flutter/runtime/dart_vm.cc char* flags_error = Dart_SetVMFlags(args.size(), args.data()); 最终调用方法: /path/to/engine/src/third_party/dart/runtime/vm/flags.cc char* Flags::ProcessCommandLineFlags(int number_of_vm_flags, const char** vm_flags) { ... while ((i < number_of_vm_flags) && IsValidFlag(vm_flags[i], kPrefix, kPrefixLen)) { const char* option = vm_flags[i] + kPrefixLen; Parse(option); i++; } ... } 这里主要会进行 Flag 的有效性验证,关键步骤为 Parse 方法中的 SetFlagFromString bool Flags::SetFlagFromString(Flag* flag, const char* argument) { ASSERT(!flag->IsUnrecognized()); switch (flag->type_) { ... case Flag::kString: { *flag->charp_ptr_ = argument == NULL ? NULL : strdup(argument); break; } .... } flag->changed_ = true; return true; } 会针对不同 Flag Type 设置不同变量,而这些变量是一个 union 结构体,如下: union { void* addr_; bool* bool_ptr_; int* int_ptr_; uint64_t* uint64_ptr_; charp* charp_ptr_; FlagHandler flag_handler_; OptionHandler option_handler_; } 根据 union 的特性,针对不同的 Flag Type,会得到不同值类型,可见之前定义的 FLAG_timeline_streams 值最终就会设置成透传的值。例如 —trace_startup 对应的值为 Compiler,Dart,Debugger,Embedder,GC,Isolate,VM。 设置 Flag 的过程就是具体设置了之前定义的 FLAG_timeline_streams 值。 TimelineStream 初始化 在 FLAG_timeline_streams 中非常多的类型值,每种都定义了不同的 Stream,初始化过程包括三个步骤:Declare Stream(申明)、Get Stream(获取)、Define Stream(定义)。 ✎ Declare Stream /path/to/engine/src/third_party/dart/runtime/vm/timeline.h // stream 申明 #define TIMELINE_STREAM_DECLARE(name, fuchsia_name) \ static TimelineStream stream_##name##_; TIMELINE_STREAM_LIST(TIMELINE_STREAM_DECLARE) #undef TIMELINE_STREAM_DECLARE // 展开后 static TimelineStream stream_API_; static TimelineStream stream_Compiler_; static TimelineStream stream_Dart_; static TimelineStream stream_Embedder_; .... Flutter Engine 中的 Timeline 信息为 stream_Embedder_,其它的 Timeline 也包括 Dart 层、API 层等等,本文主要会关注在 stream_Embedder_。 ✎ Get Stream /path/to/engine/src/third_party/dart/runtime/vm/timeline.h // 获取 Stream #define TIMELINE_STREAM_ACCESSOR(name, fuchsia_name) \ static TimelineStream* Get##name##Stream() { return &stream_##name##_; } TIMELINE_STREAM_LIST(TIMELINE_STREAM_ACCESSOR) #undef TIMELINE_STREAM_ACCESSOR // 展开后 static TimelineStream* GetAPIStream() { return &stream_API_; } static TimelineStream* GetDartStream() { return &stream_Dart_; } static TimelineStream* GetEmbedderStream() { return &stream_Embedder_; } ... 设置了相应的静态获取方法。 Define Stream /path/to/engine/src/third_party/dart/runtime/vm/timeline.cc #define TIMELINE_STREAM_DEFINE(name, fuchsia_name) \ TimelineStream Timeline::stream_##name##_(#name, fuchsia_name, false); TIMELINE_STREAM_LIST(TIMELINE_STREAM_DEFINE) #undef TIMELINE_STREAM_DEFINE // 展开后 TimelineStream Timeline::stream_API_("API", "dart:api", false); TimelineStream Timeline::stream_Dart_("Dart", "dart:dart", false); TimelineStream Timeline::stream_Embedder_("Embedder", "dart:embedder", false); ... Timeline 初始化 void Timeline::Init() { ASSERT(recorder_ == NULL); recorder_ = CreateTimelineRecorder(); ASSERT(recorder_ != NULL); enabled_streams_ = GetEnabledByDefaultTimelineStreams(); // Global overrides. #define TIMELINE_STREAM_FLAG_DEFAULT(name, fuchsia_name) \ stream_##name##_.set_enabled(HasStream(enabled_streams_, #name)); TIMELINE_STREAM_LIST(TIMELINE_STREAM_FLAG_DEFAULT) #undef TIMELINE_STREAM_FLAG_DEFAULT } 1、通过 CreateTimelineRecorder 创建 TimelineEventRecorder,如果需要获取启动 Tracing 信息会创建 TimelineEventEndlessRecorder,会记录无上限的 Trace 信息。2、设置刚才创建的一系列 TimelineStream 实例的 set_enable 函数,后续在进行 Timeline 记录的时候都会查询是否 enable。 ▐ 记录 Timeline 信息 上一部分主要讲了 Timeline 初始化准备的各种信息变量,这部分主要会讲记录 Tracing 信息的过程。 记录 Tracing 信息有非常多的调用方法,包括记录同步事件(TRACE_EVENT)、异步事件(TRACE_EVENT_ASYNC)、事件流(TRACE_FLOW_)。以下讲同步事件的调用过程,其他事件整个流程基本类似。 同步事件包括 TRACE_EVENT0 、TRACE_EVENT1、TRACE_EVENT2 等,以 TRACE_EVENT0 调用为例: { TRACE_EVENT0("flutter", "Shell::CreateWithSnapshots"); } // 展开后 ::fml::tracing::TraceEvent0("flutter", "Shell::CreateWithSnapshots"); ::fml::tracing::ScopedInstantEnd __trace_end___LINE__("Shell::CreateWithSnapshots"); 主要包括两个部分: 记录阶段 TraceEvent0,记录当前信息 标记结束 ScopedInstantEnd ,一般在作用域析构时调用 TraceEvent0 TraceEvent0 最终会调用如下方法: path/to/engine/src/third_party/dart/runtime/vm/dart_api_impl.cc DART_EXPORT void Dart_TimelineEvent(const char* label, int64_t timestamp0, int64_t timestamp1_or_async_id, Dart_Timeline_Event_Type type, intptr_t argument_count, const char** argument_names, const char** argument_values) { ... TimelineStream* stream = Timeline::GetEmbedderStream(); ASSERT(stream != NULL); TimelineEvent* event = stream->StartEvent(); ... switch (type) { case Dart_Timeline_Event_Begin: event->Begin(label, timestamp0); break; case Dart_Timeline_Event_End: event->End(label, timestamp0); break; ... } ... event->Complete(); } 整个过程主要包括四个阶段: TimelineStream::StartEvent:生成 TimelineEvent,其中Timeline::GetEmbedderStream() 即为初始化阶段的 stream_Embedder_。 TimelineEvent::Begin/End:记录起始、结束的时间等信息 TimelineEvent::Complete:完成当前记录 TimelineEventBlock::Finish:上报记录的信息 ✎ TimelineStream::StartEvent stream->StartEvent() 最终会调用如下方法产生 TimelineEvent: /path/to/engine/src/third_party/dart/runtime/vm/timeline.cc TimelineEvent* TimelineEventRecorder::ThreadBlockStartEvent() { // Grab the current thread. OSThread* thread = OSThread::Current(); ASSERT(thread != NULL); Mutex* thread_block_lock = thread->timeline_block_lock(); ... thread_block_lock->Lock(); // 会一直持有,直到调用 CompleteEvent() ... TimelineEventBlock* thread_block = thread->timeline_block(); if ((thread_block != NULL) && thread_block->IsFull()) { MutexLocker ml(&lock_); // Thread has a block and it is full: // 1) Mark it as finished. thread_block->Finish(); // 2) Allocate a new block. thread_block = GetNewBlockLocked(); thread->set_timeline_block(thread_block); } else if (thread_block == NULL) { MutexLocker ml(&lock_); // Thread has no block. Attempt to allocate one. thread_block = GetNewBlockLocked(); thread->set_timeline_block(thread_block); } if (thread_block != NULL) { // NOTE: We are exiting this function with the thread's block lock held. ASSERT(!thread_block->IsFull()); TimelineEvent* event = thread_block->StartEvent(); return event; } .... thread_block_lock->Unlock(); return NULL; } 1、首先会调用线程锁,一直持有本次记录过程,直到调用 CompleteEvent()。2、如果没有 TimelineEventBlock ,则首先会创建一个,并记录在当前线程中。3、如果 TimelineEventBlock 满了,会先 Finish (见下文分析),再创建一个新的,并记录。4、最后都会在 TimelineEventBlock 中创建一个新的 TimelineEvent,每个 TimelineEventBlock 创建的 TimelineEvent 会有数量限制,最多为 64 个。 ⚠️:如果为 TimelineEventEndlessRecorder,则会无限创建 TimelineEventBlock,否则会有数量限制。 ✎ TimelineEvent::Begin/End /path/to/engine/src/third_party/dart/runtime/vm/timeline.cc void TimelineEvent::Begin(const char* label, int64_t micros, int64_t thread_micros) { Init(kBegin, label); set_timestamp0(micros); set_thread_timestamp0(thread_micros); } 这些阶段主要是记录具体的信息,包括:1、Init: 记录事件标签名,事情类型(kBegin,kEnd),End 一般会在作用域析构时调用(下面会分析)。2、micros: 记录系统启动后运行的时间戳。 3、thread_micros: 记录该线程CPU运行的时间戳。 ✎ TimelineEvent::Complete 最终调用方法如下: /path/to/engine/src/third_party/dart/runtime/vm/timeline.cc void TimelineEventRecorder::ThreadBlockCompleteEvent(TimelineEvent* event) { ... // Grab the current thread. OSThread* thread = OSThread::Current(); ASSERT(thread != NULL); // Unlock the thread's block lock. Mutex* thread_block_lock = thread->timeline_block_lock(); ... thread_block_lock->Unlock(); } 一次记录结束后会调用 Complete 方法,并最终会释放一开始 Lock 的同步锁。 ✎ TimelineEventBlock::Finish 在 TimelineStream::StartEvent 中创建的TimelineEventBlock 提到,默认最多是 64 个,满了之后会调用 Finsih 方法。 void TimelineEventBlock::Finish() { ... in_use_ = false; #ifndef PRODUCT if (Service::timeline_stream.enabled()) { ServiceEvent service_event(NULL, ServiceEvent::kTimelineEvents); service_event.set_timeline_event_block(this); Service::HandleEvent(&service_event); } #endif } 最终会将事件信息发送给 ServiceIsolate 来处理,关于 ServiceIsolate 简单可以理解为后端服务,是由 Dart VM 初始化的时候创建的, DevTool 显示的信息(包括 Tracing 信息)都会和 ServiceIsolate 通信获取。 ScopedInstantEnd class ScopedInstantEnd { public: ScopedInstantEnd(const char* str) : label_(str) {} ~ScopedInstantEnd() { TraceEventEnd(label_); } private: const char* label_; FML_DISALLOW_COPY_AND_ASSIGN(ScopedInstantEnd); }; 可以看到析构函数中会调用 TraceEventEnd 方法,也就是说离开了作用域就会调用 TraceEventEnd 方法,而 TraceEventEnd 方法最终调用的就是 TimelineEvent::End 阶段进行信息记录。 以上就是整体的 Tracing 信息的路由过程,实现上使用了大量的宏,宏在开发阶段还是方便实现,不过对于阅读源码来说会有一定的障碍,不能直观的进行代码搜索查找。 实践篇 主要介绍 Timeline 的使用、启动性能分析、有用的 Debug 参数介绍、以及添加自定义 Tracing 节点。 ▐ Timeline 使用 Timeline 的使用在官方文档中已经有详细的说明,Using the Timeline view - Flutter 直接看文档即可。 ▐ 启动性能分析 Timeline 工具仅仅只能分析 Flutter 页面启动之后的运行时情况,整个 Flutter 的启动过程完全是无法分析的,而启动/初始化过程也是比较关键的一环。 对于启动性能分析,官方文档描述甚少,目前只发现了这一处,Measuring app startup time - Flutter。 启动性能分析包括三个步骤:添加启动性能参数、获取 Tracing 信息、分析。 添加启动参数 只有添加了特定的参数后才能获取启动时 Tracing 信息。 ✎ Flutter App 场景 flutter run --trace-startup --profile 主要是通过 flutter cli 命令行参数运行 Flutter App,最终会在当前目录下生成 build/start_up_info.json 文件。 可惜的是这个文件只产出了四个关键的 Timestamp,远远达不到能够分析的地步,跟进 Flutter Tools 源码后,关键源码如下: path/to/flutter/packages/flutter_tools/lib/src/tracing.dart /// Download the startup trace information from the given observatory client and /// store it to build/start_up_info.json. Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async { final Tracing tracing = Tracing(observatory); final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline( awaitFirstFrame: awaitFirstFrame, ); ...... final Map<String, dynamic> traceInfo = <String, dynamic>{ 'engineEnterTimestampMicros': engineEnterTimestampMicros, }; ...... traceInfo['timeToFrameworkInitMicros'] = timeToFrameworkInitMicros; ...... traceInfo['timeToFirstFrameRasterizedMicros'] = firstFrameRasterizedTimestampMicros - engineEnterTimestampMicros; ...... traceInfo['timeToFirstFrameMicros'] = timeToFirstFrameMicros; ...... traceInfo['timeAfterFrameworkInitMicros'] = firstFrameBuiltTimestampMicros - frameworkInitTimestampMicros; ...... traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo)); } 可以看到关键的四个 Timestamp 被保存在 Map 进行输出到文件,最关键的一点是整个 timeline 数据其实都已经拿到了,于是可以进行如下改造: /// Download the startup trace information from the given observatory client and /// store it to build/start_up_info.json. Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async { final Tracing tracing = Tracing(observatory); final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline( awaitFirstFrame: awaitFirstFrame, ); ...... // 原来的 start_up_info.json 生成 traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo)); ...... // 新增 start_up_trace_events.json 生成 final String traceEventsFilePath = globals.fs.path.join(getBuildDirectory(), 'start_up_trace_events.json'); final File traceEventsFile = globals.fs.file(traceEventsFilePath); final List<Map<String, dynamic>> events = List<Map<String, dynamic>>.from((timeline['traceEvents'] as List<dynamic>).cast<Map<String, dynamic>>()); traceEventsFile.writeAsStringSync(toPrettyJson(events)); } 改造后会在当前目录下生成 build/start_up_trace_events.json 文件,并通过 chrome://tracing 打开查看。有一个注意点,在改动 flutter tools 代码后,需要重新生成 flutter command ,具体可以看文档。The flutter tool · flutter/flutter Wiki · GitHub 上面这个场景对于整个 Flutter App 来讲是完全可以进行启动性能分析了,但是对于 Add to App 的场景还是无法满足,因为这种场景无法通过 flutter cli 来进行参数透传。 **✎ Add To App 场景**对于这种场景,需要通过 Platform 层去透传参数。 Android Android 侧参数透传方法如下: path/to/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java public FlutterEngine( @NonNull Context context, @NonNull FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI, @NonNull PlatformViewsController platformViewsController, @Nullable String[] dartVmArgs, boolean automaticallyRegisterPlugins) { ...... } 通过实例化 FlutterEngine 时的构造参数 dartVmArgs 中添加 --trace-startup 即可。 new FlutterEngine(mPlatform.getApplication().getApplicationContext(), FlutterLoader.getInstance(),new FlutterJNI(),new String[]{"--trace-startup"},true); iOS iOS 侧通过源码查看,对应的 FlutterEngine.mm 的构造参数中是没有对应的 dartVmArgs 参数透传。真正参数转换的地方如下: path/to/engine/src/flutter/shell/platform/darwin/common/command_line.mm fml::CommandLine CommandLineFromNSProcessInfo() { std::vector<std::string> args_vector; for (NSString* arg in [NSProcessInfo processInfo].arguments) { args_vector.emplace_back(arg.UTF8String); } return fml::CommandLineFromIterators(args_vector.begin(), args_vector.end()); } 通过 [NSProcessInfo processInfo].arguments 拿的命令行参数,无法通过自定义加入参数实现,对于从 XCode 启动 App 的可以通过编辑 schema 添加参数实现,示例如下: 但是绝大多数情况下,不会通过 XCode 来启动 App,因此还是需要通过修改 Engine 代码来实现参数传递。对此提了 PR 来支持 dartVm 参数的透传。 path/to/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm - (instancetype)initWithDartVmArgs:(nullable NSArray<NSString*>*)args { return [self initWithPrecompiledDartBundle:nil dartVmArgs:args]; } 初始化 FlutterEngine.mm 中可以通过如下方式初始化: _dartProject = [[FlutterDartProject alloc] initWithPrecompiledDartBundle:dartBundle dartVmArgs:@[@"--trace-startup"]]; _engine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:_dartProject allowHeadlessExecution:YES]; Android Systrace 对于 Android 设备来讲,还可以用 Android 独有的 Systrace 来看,不需要改任何 Flutter 相关的参数。 相关参考文档:Understanding Systrace | Android Open Source ProjectOverview of system tracing | Android Developers 获取 Tracing 文件 添加了启动参数之后,需要有工具进行查看,Flutter 默认提供的 DevTool 默认就能进行查看,按如下步骤: 拿到启动后的 Observatory 地址。通过 flutter attach --debug-uri=observatory_url attach 到对应的服务,会生成一个 debugger/profiler 地址。打开 debugger/profiler 地址后就是 Fluuter 默认的 DevTool 工具,点击 timeline 按钮即可打开 Tracing 内容。 分析 Tracing 文件 关于 Tracing 工具的使用可以查看相关 Chrome 文档, The Trace Event Profiling Tool (about:tracing)。 展示的信息比较直观,对于启动性能分析,能非常直观的看到各个部分的耗时情况,下图是 Flutter 启动时 iOS 上的各个耗时阶段的大致分布,图的左边,可以看到各个阶段执行对应的线程。 ▐ Debug 参数 上面介绍了如何获取 Tracing 的方法,生成的 Tracing 耗时分布主要包括各个阶段的耗时,但是还并不是包含所有的阶段,介绍两个有用的 Debug 参数,其他相关参数参考文档 [Debug flags: performance - Flutter](链接地址https://flutter.dev/docs/testing/code-debugging?spm=ata.13261165.0.0.32ca24d41mrBFF#debug-flags-performance)debugProfilePaintsEnabled path/to/flutter/packages/flutter/lib/src/rendering/debug.dart bool debugProfilePaintsEnabled = false; 这个参数会在渲染 Paint 阶段,显示所有 Paint 时节点的遍历情况,可以根据这些信息查看是否有无用的节点 Paint debugProfileBuildsEnabled path/to/flutter/packages/flutter/lib/src/widgets/debug.dart bool debugProfileBuildsEnabled = false; 这个参数会在 Widget Build 阶段,显示所有 Widget 节点 Build 时的遍历情况,可以根据这些信息查看是否有无用的节点 Build。 上图把 build、paint 阶段的过程全都显示出来了,有了这些信息后,还需要结合自身的业务逻辑分析 Widget Build/Paint 是否合理,是否执行了无用的操作,然后进行优化。 自定义 Tracing 节点 对于默认没有打点的地方,如果自己需要查看其耗时,则可以自行进行打点。例如需要查看创建 IOSContext 的耗时,则可以进行如下打点: std::unique_ptr<IOSContext> IOSContext::Create(IOSRenderingAPI rendering_api) { TRACE_EVENT0("flutter", "IOSContext::Create"); ...... FML_CHECK(false); return nullptr; } 最终会反应在 Tracing 上,如下图: 后记 本文主要分析了 Tracing 在 Flutter 上的实现以及一些实践,Tracing 是 Chrome 实现的一种标准格式,任何技术栈的性能分析都可以生成这种标准格式,然后利用现成的 Chrome DevTool 工具打开即可分析,非常直观,能启到事半功倍的效果。 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
如今不再是传统的前端开发时代,云 + 端赋予了前端新的能力,新的使命。从传统的服务,到端侧,小程序,亦或者从线下走上云端,前端始终走在业务和技术的浪潮之巅,在这其中,技术总是在迭代和变革,也总是会有不同的声音以及对应的问题。 你是否在传统开发和 Serverless 中犹豫不决,对上云抱有怀疑?是否在开发项目的前端代码和后端代码中切换觉得特别繁琐?是否在开发中后台时觉得能力不足,需要申请服务器配合?是否在小程序开发中碰到资源调用,管理繁琐的问题? 淘系前端技术,我们即将开放新能力,让这些问题都得到解答,就来拭目以待吧。 9月23日淘系技术特此举办【云+端】开源产品线上发布会!和大咖连线,共同探索云+端的未来。 直播看点
8月20日,TypeScript 4.0 正式发布了( Announcing TypeScript 4.0 ),虽然没有重大的变更和特性,可以看做是 3.9 版本正常迭代,不过 Daniel 也在公告中说了:对于初学者而言,现在是最好的上手时机。 In fact, if you’re new to the language, now is the best time to start using it. 确实 TS 在经过了几年的发展后,使用 TS 的团队也越来越多,更重要的是 TS 的生态越来越完备,非常多的库、框架等都支持了类型系统甚至直接用 TS 重写,现在开始使用 TS 就能够直接享受整个技术生态带来的开发效率提升。回归到业务,我们团队最近也确实在开始用 TS 来进行类库的开发,所以结合官方文档和社区文档并从新人学习的角度梳理了一份包含大部分 TS 核心概念的学习手册。本文主要是做减法,梳理出核心的点,能够先用起来,然后按工作需要找一些点逐个进行深入学习。 背景 因为日常工作会使用页面搭建系统来生成很多前端页面,所以会开发很多的楼层模块配合搭建系统使用,最近模块是基于 Rax 开发的,后续会支持越来越多的投放渠道:web、weex、淘宝小程序、支付宝小程序等等,为了兼容约来越多的渠道,很多功能被抽象成了一个个小的类库,在类库中去兼容各个渠道,从而让模块中的业务代码保持尽量的只有清晰的业务逻辑。 但是随着支持渠道的增多,不可避免的导致类库在不同的渠道支持的特性不一致,比如 web 中,类库A 支持三个参数甲、乙、丙,而在小程序中类库A 仅支持两个参数甲、乙,所以丙这个参数要设计成可选参数,在类似的场景变多之后,这些类库的文档说明成了很重要的工作,同时在 IDE 中写代码时如果有类型系统能自动告诉开发人员这个函数支持哪些参数就更好了,所以我们准备对类库进行 TS 的重写来提高我们的生产效率,也为后续 TS 在团队内的落地打一个好基础。 什么是 TypeScript 简单的说 TypeScript 是 JavaScript 一个超集,能够编译成 JavaScript 代码 其核心能力是在代码编写过程中提供了类型支持,以及在编译过程中进行类型校验 先说一下 JS 的现状: 1、在 JS 中的变量本身是没有类型,变量可以接受任意不同类型的值,同时可以访问任意属性,属性不存在无非是返回 undefined 2、JS 也是有类型的,但是 JS 的类型是和值绑定的,是值的类型,用 typeof 判断变量类型其实是判断当前值的类型 // JavaScript var a = 123 typeof a // "number" a = 'sdf' typeof a // "string" a = { name: 'Tom' } a = function () { return true } a.xxx // undefined TS 做的事情就是给变量加上类型限制 1、限制在变量赋值的时候必须提供类型匹配的值 2、限制变量只能访问所绑定的类型中存在的属性和方法 举个简单的例子,如下是一段能够正常执行的 JS 代码: let a = 100 if (a.length !== undefined) { console.log(a.length) } else { console.log('no length') } 直接用 TS 来重写上面的代码,把变量 a 的类型设置为 number 在 TS 中给变量设置类型的语法是 【 : Type 】 类型注解 let a: number = 100 if (a.length !== undefined) { // error TS2339: Property 'length' does not exist on type 'number'. console.log(a.length) } else { console.log('no length') } 但是如果直接对这个 TS 代码进行编译会报错,因为当变量被限制了类型之后,就无法访问该类型中不存在的属性或方法。 那再来写一段能正常执行的 TS let a: string = 'hello' console.log(a.length) 编译成 JS 后的代码为 var a = 'hello' console.log(a.length) 可以发现 : string 这个类型限制编译之后是不存在的,只在编译时进行类型校验。 当 TS 源码最终被编译成 JS 后,是不会产生任何类型代码的,所以在运行时自然也不存在类型校验。 也就是说,假设一个项目,用 TS 来写,哼哧哼哧加上各种类型检验,项目测试通过部署到线上之后 最后运行在客户端的代码和我直接用 JS 来写的代码是一样的,写了很多额外的类型代码,竟然是为了保证能顺利编译成原来的代码 ▐ TypeScript 的作用 那 TS 的作用究竟是什么呢,主要是以下三点: 将类型系统看作为文档,在代码结构相对复杂的场景中比较适用,本质上就是良好的注释。 配合 IDE,有更好的代码自动补全功能。 配合 IDE,在代码编写的过程中就能进行一些代码校验。例如在一些 if 内部的类型错误,JS 需要执行到了对应代码才能发现错误,而 TS 在写代码的过程中就能发现部分错误,代码交付质量相对高一些,不过对于逻辑错误,TS 当然也是无法识别的。 TypeScript 类型梳理 分两类来介绍 TS 的类型系统: JS 中现有的值类型在 TS 中对应如何去限制变量 TS 中拓展的类型,这些类型同样只在编译时存在,编译之后运行时所赋的值其实也是 JS 现有的值类型 下文中会穿插一些类似 [ xx ] 这样的标题,这是在列举介绍 TS 类型的过程中插入介绍的 TS 概念 ▐ JS 中现有的值类型如何绑定到变量 使用语法:类型注解【 : Type 】 布尔值 let isDone: boolean = false 数值 let age: number = 18 字符串 let name: string = 'jiangmo' 空值 function alertName(): void { // 用 : void 来表示函数没有返回值 alert('My name is Tom') } Null 和 Undefined let u: undefined = undefined let n: null = null // 注意:和所有静态类型的语言一样,TS 中不同类型的变量也无法相互赋值 age = isDone // error TS2322: Type 'false' is not assignable to type 'number'. // 但是因为 undefined 和 null 是所有类型的子类型,所以可以赋值给任意类型的变量 age = n // ok [ 类型推论 ] 如果没有明确的指定类型,那么 TypeScript 会依照类型推论的规则推断出一个类型 例如:定义变量的时候同时进行赋值,那么 TS 会自动推断出变量类型,无需类型注解 let age = 18 // 等价于 let age: number = 18 // 所以上面代码中的类型声明其实都可以省略 // 但是如果定义的时候没有赋值,不管之后有没有赋值,则这个变量完全不会被类型检查(被推断成了 any 类型) let x x = 'seven' x = 7 // 所以这个时候应该显示的声明类型 let x: number x = 7 继续列举类型 数组的类型 语法是 【 Type[] 】 let nameList: string[] = ['Tom', 'Jerry'] let ageList: number[] = [5, 6, 20] 对象的类型 接口 (interface) 用于描述对象的类型 interface Person { // 自定义的类型名称,一般首字母大写 name: string age: number } let tom: Person = { name: 'Tom', age: 25, } 函数的类型 以函数表达式为例 ( 函数声明定义的函数也是用类似的 参数注解 语法来进行类型约束 ) // JavaScript const sum = function (x, y) { return x + y } TS 中有多种语法来定义函数类型 直接约束出入参类型 const sum = function (x: number, y: number): number { return x + y } 单独给 sum 变量设置类型 const sum: (x: number, y: number) => number = function (x, y) { return x + y } 这里如果把函数类型直接提取出来用并起一个自定义的类型名,代码会更美观,也易复用。 利用 类型别名 可以给 TS 类型重命名 [ 类型别名 ] 类型别名的语法是 【 type 自定义的类型名称 = Type 】 type MySum = (x: number, y: number) => number const sum: MySum = function (x, y) { return x + y } 回到函数类型 1、用接口定义函数的类型 interface MySum { (a: number, b: number): number } const sum: MySum = function (x, y) { return x + y } 函数类型介绍完了,最后额外补充一下函数类型怎么 定义剩余参数的类型 以及 如何设置默认参数。 const sum = function (x: number = 1, y: number = 2, ...args: number[]): number { return x + y } 类的类型 和函数类型的语法相似,直接在 ES6 语法中用【 : Type 】类型注解 和 参数注解 语法给类的属性和方法设置类型 class Animal { name: string // 这一行表示声明实例属性 name constructor(name: string) { this.name = name } sayHi(): string { return `My name is ${this.name}` } } let a: Animal = new Animal('Jack') // : Animal 约束了变量 a 必须是 Animal 类的实例 console.log(a.sayHi()) // My name is Jack 顺便值得一提的是,除了类型支持以外,TS 也拓展了 class 的语法特性 新增了三种访问修饰符 public、 private、 protected 和只读属性关键字 readonly 以及 abstract 抽象类 这里就不展开了,有需要的再去查阅一下官方文档即可 内置对象和内置方法 JavaScript 中有很多内置对象和工具函数,TS 自带其对应的类型定义 很多内置对象可以直接在 TypeScript 中当做定义好了的类型来使用 let e: Error = new Error('Error occurred') let d: Date = new Date() let r: RegExp = /[a-z]/ let body: HTMLElement = document.body 一些内置的方法,TS 也补充了类型定义,配合 IDE 在编写代码的时候也能得到 TS 的参数提示。 Math.pow(2, '3') // error TS2345: Argument of type '"3"' is not assignable to parameter of type 'number'. ▐ TS 中拓展的类型 任意值 any 与其说 any 是 JS 中不存在的类型,不如说原本 JS 中的变量只有一个类型就是 any 任意值 any 的特点: any 类型的变量可以赋值给任何别的类型,这一点和 null 与 undefined 相同任何类型都可以赋值给 any 类型的变量 在任意值上访问任何属性都是允许的 let a: any = 123 a = '123' // ok let n: number[] = a // ok a.foo && a.foo() // ok 所以 any 是万金油,也是和 TS 进行类型约束的目的是相违背的,要尽量避免使用 any。 联合类型 类型中的或操作,在列出的类型里满足其中一个即可 let x: string | number = 1 x = '1' 不过联合类型有一个额外约束: 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。 let x: string | number = 1 x = '1' x.length // 这里能访问到 length ,因为 TS 能确定此时 x 是 string 类型 // 下面这个例子就会报错 function getLength(something: string | number): number { return something.length // error TS2339: Property 'length' does not exist on type 'string | number'. } 两种解决思路 让 TS 能够自行推断出具体类型 function getLength(something: string | number): number { if (typeof something === 'string') { // TS 能识别 typeof 语句 return something.length // 所以在这个 if 分支里, something 的类型被推断为 string } else { return 0 } } 利用 类型断言,手动强制修改现有类型 function getLength(something: string | number): number { return (something as string).length // 不过这样做实际上代码是有问题的,所以用断言的时候要小心 } [ 类型断言 ] 用来手动指定一个值的类型,语法 【 value as Type 】 用类型断言修改类型时的限制: 1、联合类型可以被断言为其中一个类型 2、父类可以被断言为子类 3、任何类型都可以被断言为 any 4、any 可以被断言为任何类型 总结成一条规律就是:要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可。 ✎ 双重断言 利用利用上述 3 和 4 两条规则,可以强制把一个值改为任意其他类型 **let a = 3(a as any) as string).split // ok** 如果说断言有风险,那双重断言就是在反复横跳了 字符串字面量类型 用来约束取值只能是某几个字符串中的一个 type EventNames = 'click' | 'scroll' | 'mousemove' function handleEvent(ele: Element, event: EventNames) { // do something } 注意,只有一个字符串也是字符串字面量类型 type MyType = 'hello' 虽然一般不会手动设置这样的类型,不过类型推论经常会推断出这种类型。 比如某次编译报错提示为:Argument of type '"foo"' is not assignable to parameter of type 'number'. 提示中的 type '"foo"' 一般就是根据字符串 'foo' 推断出来的字符串字面量类型。 元组 类似 Python 中的元组,可以看做是固定长度和元素类型的数组 let man: [string, number] = ['Tom', 25] // 不过 TS 中的元组支持越界 // 当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型 man.push('male') 枚举 用于取值被限定在一定范围内的场景,可以替代 JS 中用字面量来定义一个对象作为字典的场景 enum Directions { Up, Down, Left, Right, } let d: Directions = Directions.Left 这里看到 Directions.Left 直接把类型当做一个值来用了。 不是说类型是用于【 : Type 】类型注解 语法来约束变量,编译之后类型代码都会被删除吗? 为了解释这个问题,我们先来来看看单纯的类型代码会被编译成什么。 首先以一个联合类型举例 type MyType = string | number | boolean 编译结果: // 不会产生任何 JS 代码 再来看看枚举类型会被编译成什么 enum Directions { Up, Down, Left, Right, } console.log(Directions) 编译结果: var Directions ;(function (Directions) { Directions[(Directions['Up'] = 0)] = 'Up' Directions[(Directions['Down'] = 1)] = 'Down' Directions[(Directions['Left'] = 2)] = 'Left' Directions[(Directions['Right'] = 3)] = 'Right' })(Directions || (Directions = {})) console.log(Directions) /* 运行时 log 出来的 Directions 变量如下 { '0': 'Up', '1': 'Down', '2': 'Left', '3': 'Right', Up: 0, Down: 1, Left: 2, Right: 3 } */ 这怎么理解呢? let d: Directions = Directions.Left 其实这一行代码中,前一个 Directions 表示类型,后一个 Directions 表示值。 即 Directions 是一个值和类型的“复合体”,在不同的语法中具象化为值或者类型。 其实有办法可以把类型部分从 Directions 中抽离出来。 enum Directions { Up, Down, Left, Right, } type MyDirections = Directions console.log(MyDirections) // error TS2693: 'MyDirections' only refers to a type, but is being used as a value here. 此时 MyDirections 就是一个纯粹的类型,不能当做一个值来使用。 其实之前介绍的函数类型、类类型等声明中,也存在这样的值与类型的“复合体” const sum = function (x: number, y: number = 5): number { return x + y } console.log(sum) // [Function: sum] type MySum = typeof sum // 注意,剥离出来的函数类型是不会带有默认参数的,因为默认参数其实是函数的特性,和类型系统无关 const f: MySum = (a, b) => 3 // ok console.log(MySum) // error TS2693: 'MySum' only refers to a type, but is being used as a value here. 然后再回到枚举。 ✎ 字符串枚举 用字符串字面量初始化枚举成员,在实际使用过程中很常见 enum Directions { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', } console.log(Directions.Up === 'UP') // true ✎ 常数枚举 用 const enum 定义的枚举类型 和普通枚举的区别就是对应的值也会在编译阶段被删除,只会留下枚举成员的值 const enum Directions { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', } let d = Directions.Left // 如果取消注释下面这行代码,编译会报错 // console.log(Directions) // error TS2475: 'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query. 编译结果: var d = 'LEFT' /* Left */ 泛型 其实泛型并不是一种具体的类型,而是在定义函数、接口或类的类型时的的拓展特性。 泛型是类型系统里的 “函数” ,通过传入具体的 类型参数 来得到一个具体的类型,从而达到复用类型代码的目的 假设一个场景,某个函数的入参类型为 number | string ,并且出参类型和入参相同 先尝试用联合类型来约束出入参 type MyFunc = (x: number | string) => number | string 但是 MyFunc 无法表示出参类型和入参相同,即入参是 number 的时候出参也是 number。 在这个场景下,可以利用泛型来定义出多个类似的函数类型。 泛型函数 表示声明了一个 类型参数,在定义类型的时候 T 就可以作为一个类型来使用 类型参数也可以定义多个,比如 function GenericFunc<T>(arg: T): T { return arg } // 这里的 GenericFunc<number> 是表示的是一个函数值,同时将类型参数 T 赋值为 number let n = GenericFunc<number>(1) // n 可以通过类型推论得出类型为 :number // 进一步,利用 泛型约束 ,限制出入参为 number | string type MyType = number | string function GenericFunc<T extends MyType>(arg: T): T { // extends MyType 表示类型参数 T 符合 MyType 类型定义的形状 return arg } let s = GenericFunc<string>('qq') let b = GenericFunc<boolean>(false) // error TS2344: Type 'boolean' does not satisfy the constraint 'string | number'. 泛型接口 用 泛型接口 来定义函数类型 interface GenericFn<T> { (arg: T): T } // 定义一个泛型函数作为函数实现 function identity<T>(arg: T): T { return arg } // 使用泛型时传入一个类型来使 类型参数 变成具体的类型 // <number> 表示 T 此时就是 number 类型,GenericFn<number> 类似是 “函数调用” 并返回了一个具体的类型 (这里是一个函数类型) const myNumberFn: GenericFn<number> = identity const myStringFn: GenericFn<string> = identity let n = myNumberFn(1) // n 可以通过类型推论得出类型为 :number let s = myStringFn('string') // s 可以通过类型推论得出类型为 :string 对比上述的 泛型函数 和 泛型接口,有一个区别: 给泛型函数传参之后得到的是一个函数值,而不是类型 // GenericFunc 是上面定义的泛型函数 type G = GenericFunc<string> // error TS2749: 'GenericFunc' refers to a value, but is being used as a type here. 而泛型接口传参之后得到的是一个类型,而不是函数值 // GenericFn 是上面定义的泛型接口 type G = GenericFn<number> // ok GenericFn<number>() // error TS2693: 'GenericFn' only refers to a type, but is being used as a value here. 泛型类 用 泛型类 来定义类的类型 class GenericClass<T> { zeroValue: T constructor(a: T) { this.zeroValue = a } } let instance = new GenericClass<number>(1) // 等价于 let instance: GenericClass<number> = new GenericClass(1) // 因为有类型推论,所以可以简写成 let instance = new GenericClass(1) ✎ 内置的数组泛型 TS 中内置类一个数组泛型 Array,传入类型参数后会返回对应的数组类型 // 数组的类型之前是用 【 Type[] 】 语法来表示的 let list: number[] = [1, 2, 3] // 现在也可以这么表示 let list: Array<number> = [1, 2, 3] [ 声明合并 ] 上面那个场景,某个函数的入参类型为 number | string ,并且出参类型和入参相同,其实不用泛型也可以用函数重载来实现 ✎ 函数的合并 即函数声明的合并,即函数重载 TS 中的重载并不是真正意义上的重载,只是在根据不同的实参类型,从上而下挑选出一个具体的函数类型来使用 function func(x: number): number function func(x: string): string function func(x: any): any { // 这里定义的函数类型 (x: any): any 会被覆盖失效 return x.length } let n = func(1) // n 可以通过类型推论得出类型为 :number let s = func('1') // s 可以通过类型推论得出类型为 :string // 需要注意的是,如上重载之后只剩下两种函数类型,调用时的入参要么是 number 要么是 string,无法传入其他类型的值 let b = func(true) // error /* - error TS2769: No overload matches this call. Overload 1 of 2, '(x: number): number', gave the following error. Argument of type 'true' is not assignable to parameter of type 'number'. Overload 2 of 2, '(x: string): string', gave the following error. Argument of type 'true' is not assignable to parameter of type 'string'. */ ✎ 接口的合并 接口中方法的合并和函数的合并相同,但是 属性的合并要求类型必须唯一 interface Alarm { price: number alert(s: string): string } interface Alarm { weight: number alert(s: string, n: number): string } // 相当于 interface Alarm { price: number weight: number alert(s: string): string alert(s: string, n: number): string } 声明文件 以 .d.ts 结尾的文件 声明文件里面 100% 全部都是纯类型的声明,不会编译出任何 JS 代码 一般来说,TS 会解析项目中所有的 *.ts 以及 .d.ts 结尾的文件,从而获取其中这些类型声明。 使用场景:作为一个第三方类库的开发者 假如你的库是用 TypeScript 写的,但是最终你的库分发出去的时候要编译成 JS。 否则这个类库就只能给 TS 项目来使用了,因为没有使用 TS 的项目没法直接引用 .ts 文件。 但是编译成 JS 有一个问题就是类型代码都被删除了之后,对于 TS 项目的使用方来说就没法继承 TS 的三大优势 (文档、自动补全、类型校验) 所以需要在类库的编译产出文件中保留一些 .d.ts 类型声明文件,和编译出来的 JS 文件中导出的函数、类等进行一一匹配。 这样 JS 文件和类型文件分离之后,你的类库就可以同时被 JS 项目和 TS 项目引用了。 假如你的库是并没有使用 TypeScript 来编写,那就需要额外有人给这个库写单独的声明文件。 比如 jQuery 并不是用 TS 来写的,但是你可以安装单独的 TS 类型包来实现补全这个包的类型系统。 npm install @types/jquery --save-dev **感谢&全文引用:** TypeScript 入门教程 TypeScript 官方手册 TypeScript Handbook 关注「淘系技术」微信公众号,一个有温度有内容的技术社区~
2023年01月
2021年09月
2021年08月
2021年04月
2021年03月
2021年01月
2020年12月
2020年11月
2020年10月
2020年09月