架构师 (2014年6月刊). pdf

patrick002

贡献于2015-03-15

字数:0 关键词: 软件架构

2 2014 年 6 月《架构师》 卷首语 程序员的理想 (InfoQ 编辑注:本期内容原定用作 5 月刊内容,但 5 月刊由于各种原因未能完成制作, 因此作为 6 月刊内容发布。) 4 月的最后一个周末,大连的连一团队策划了一场 TEDx 活动,活动的主题是理想主义。我 也作为团队的一员,做了一些微不足道的工作。本来差一点可以邀请 InfoQ 中文站的泰稳 同学来和大家做一场关于理想主义的分享,但非常不凑巧的是,活动的日期正好是 QCon 北 京的举办日期,所以未能成行。 在那次活动中,多位来自于各个领域的讲师和大家分享了他们心中的理想,那么对于我们这 些程序员来说,心中的理想又是什么呢? 有人会说,我的理想就是能够用程序来解决世间一切问题。管他什么业务流程,销售、采购、 财务、生产、合同流转、公文审批,只要是用了我写的系统,一切都可以高效、高质,在极 其流畅的情况下顺利完成。各种各样的业务人员,都可以没事儿就休息,把工作交给系统做 就好啦。 有人会说,我的理想就是能够实现软件行业的世界大同。我们可以按照别人的需要编写程序, 而不需要靠这个养家糊口,只是用来帮助别人完成难以完成的工作,实现自己的人生价值。 而且,当我们需要的时候,就能够找到需要的各种软件来使用,而不需要因为那而花费任何 金钱。这似乎就是 IT 世界中的共产主义。 还有人会说,我没有那么高的理想,我就希望我们这群技术人能够远离各种政治,能够靠着 自己的技术赢得丰富的回报,可以衣食无忧,写自己最喜欢写的程序,读自己最喜欢读的书, 每天都快快乐乐,和家人朋友一起幸福地生活。 然而,还有句话叫做,理想是丰满的,而现实是骨感的。 所以,我们还是需要承认,计算机、系统、编程语言都是工具,最终是为业务服务的。很多 事情并非能够靠系统来解决,至少在我们还没有实现黑客帝国中的情境,每个人都通过终端 接入到大大的网络中之前,还是要靠真正意义上的人来做。我们能够做到的只是,在一些情 况下,帮助人们解决机械化的工作,或者帮忙存储海量的数据、做大规模的分析和计算,然 而,人工智能还没有发达到足够的高度,我们还需要依赖于人来做这一切。 所以,我们还是需要靠编写各种各样的系统和程序来维持生计,在需要使用他人编写的软件 时,还是需要花费一些金钱。尽管现在已经有很多开源的软件,但并不足以满足我们的需要; 你也可能说现在有很多游戏是免费的,但其中可能会隐藏的内购项目,想要真正玩得过瘾, 还是需要花费大量资金。 所以,我们还是要承认,有人的地方就有政治,很多时候,技术人还是会处于不利的地位, 有时候,还是需要为了赚取能够满足我们的衣食住行各个方面所需的费用而努力工作。甚至 于,有些时候太忙了,没有时间和家人、朋友相处。 尽管如此,拥有理想还是非常好的事情,至少那是我们的奋斗目标所在。如果都没有了理想, 那么又怎么能够体会到风雨后彩虹的美丽? 投稿信箱:editors@cn.infoq.com 3 那么,回过头来说说,我自己的理想是什么呢。 曾经的理想是做好程序员的工作,把程序尽量写得漂亮,没有 bug,能够很好地完成业务客 户所需要的功能。 后来的理想是能够和业务人员一些完美地协作,不仅帮助他们用计算机手段解决问题,而且 能够和他们一起制定各种制度,完善各种各样的需求。 现在的理想,是希望自己可以把多年来积累的知识和经验,以更好地形式分享给大家,更好 地影响更多人做积极的改变。并且也让更多人受到自己的影响,把各自的宝贵经验都分享出 来,大家一起提高进步。 似乎现在的理想真的是任重而道远,但我想只要确定了目标,并为之坚持不懈的努力,终究 会成功,不是吗? 亲爱的读者朋友们,你的理想又是什么呢? 本期主编:侯伯薇 4 2014 年 6 月《架构师》 目录 卷首语 2 | 程序员的理想 人物 | People 6 | 左耳朵耗子谈云计算:拼的就是运维 12 | Javascript 高性能动画与页面渲染 30 | 对话 Facebook 人工智能实验室主任、深度学习专家 Yann LeCun 36 | NoSQL、JSON 和时间序列数据管理 : Anuj Sahni 访谈 专题 | Topic 38 | 本期专题:支付宝的测试 40 | 支付宝分布式事务测试方案 44 | 支付宝的性能测试 56 | 数据设计测试分析方案 推荐文章 | Article 62 | 一秒钟法则:来自腾讯无线研发的经验分享 68 | 聊聊并发——生产者消费者模式 特别专栏 | Column 74 | Node.js 软肋之回调大坑 86 | 快乐 Node 码农的十个习惯 避开那些坑 | Void 90 | 修复 bug 与解决问题——从敏捷到精益 96 | 不要就这么放弃了 SQL 投稿信箱:editors@cn.infoq.com 5 6 2014 年 6 月《架构师》 人物 | People 左耳朵耗子谈云计算:拼的就是运维 作者 陈皓 本文根据 InfoQ 中文站跟陈皓(@ 左耳朵耗子)在 2014 年 3 月的一次聊天内容整理而成。 在沟通中,陈皓分享了自己对云计算的理解,包括云计算为什么会分三层,实现一个云平台 的难点在什么地方,运维之于云计算的重要性,电商云为什么有价值等。 嘉宾简介 陈皓(@ 左耳朵耗子),CoolShell.cn 博主。15 年软件开发相关工作经验,8 年以上项 目和团队管理经验。擅长底层技术架构,团队建设,软件工程,软件研发咨询,以及全球软 件团队协作管理。对高性能,高可用性,分布式,高并发,以及大规模数据处理系统有一些 经验和心得。喜欢关注底层技术平台和互联网行业应用。技术擅长 C/C++/Java 和 Unix/ Linux/Windows。曾于 Amazon 中国任研发经理,负责电子商务全球化业务(全球开店) 和全球库存预测系统的研发。曾在阿里巴巴北京研发中心、商家业务部曾任资深专家一职, 负责电商云平台、开放平台,云监控和电商多媒体平台。现在阿里巴巴核心系统专家组从事 阿里核心系统和阿里云 ECS 相关的虚拟化平台的开发工作。 对云计算的定义 云计算其实跟 PC 机有一样的概念,有 CPU、硬盘、操作系统、应用软件。云计算的计算节点(虚 拟机)就是 PC 中的 CPU,数据缓存服务就是 PC 的内存,存储节点就是 PC 的硬盘,提供数 据服务,让数据不丢、高可用,PC 中的控制器就是云计算的控制系统。PC 机的硬件上面要 有操作系统。操作系统很大一块是给开发人员提供系统的 API 接口,提供系统监控以看运 行情况,并且还要有系统管理——如用户账号的权限管理、备份恢复等等。操作系统上面要 有应用软件,这样才能服务于最终用户,应用软件就是真正落地的业务,这样才会有用户; 有了用户,整个体系就运转起来了。 这就是工程师说的 stack,也就是我们听到的 IaaS、PaaS、SaaS 三个层。IaaS 层就像 PC 机的基础硬件加驱动程序,PaaS 层就像 PC 机上的操作系统——把基础硬件抽象、包起 来并屏蔽硬件和硬件驱动细节、调度基础硬件,而 SaaS 层就是 PC 机里的应用软件。另外, 我们还得给开发人员提供各种开发框架、类库和开发环境,这就是为什么AWS还做通知、消息、 工作流,这是用于粘合操作系统和业务层的,比如可以让你方便地做水平扩展和分布式。云 计算自然也会像 PC 机一样,三个层上都会有用于控制和管理的系统。这就是为什么云计算 会做成这个样子,其实计算机的发展就在这个圈子里绕。 其实,最终用户基本并不关心你 CPU 用的啥,存储用的是啥,你用什么框架开发,他们 关心更多的是可以解决什么问题,有什么样的用户体验。像以前 Windows 用户体验之所 以比 Linux 好,就是因为应用层用的舒服;而 Linux 对开发者的用户体验比 Windows 好,就是因为其开放和可以让开发人员更灵活、更自由。我们可以看到 SaaS 层上有的像 SalesForce、Dropbox、Evernote、Netflix 这样的给最终用户的服务,他们更倾向于 最终用户和业务。 说到底,云计算的 IaaS、PaaS、SaaS 最后那个 S 都是 Service。就是说,无论你云计 算长成什么样,都得要向用户提供“服务”而不仅仅是软硬件和各种资源。 投稿信箱:editors@cn.infoq.com 7 云计算的技术难点 到今天,云计算的工业实现已经不太难了。现在有开源软件 KVM 和 Xen,这两个东西基本 把虚拟化搞定;而 OpenStack 则把管理、控制系统搞定,也很成熟。PaaS 也有相应的开源, 比如 OpenShift,而 Java 里也有 N 多的中间件框架和技术。另外分布式文件系统 GFS/ TFS,分布式计算系统 Hadoop/Hbase 等等,分布式的东西都不神秘了。技术的实现在以 前可能是问题,现在不是了。 对于云计算工程方面,现在最难的是运维。管 100 台、1 万台还是 100 万台机器,那是完 全不同的。机器少你可以用人管理,机器多是不可能靠人的。运维系统不属于功能性的东西, 用户看不见,所以这是被大家严重低估的东西。只要你做大了,就必然要在运维系统上做文 章。数据中心 / 云计算拼的就是运维能力。 为什么我说运维比较复杂,原因有这么几个。 一方面,云计算要用廉价设备取代那些昂贵的解决方案。所谓互联网的文化就是屌丝文化, 屌丝就是便宜,互联网就是要用便宜的东西搭建出高质量的东西,硬件和资源一定不会走高 端路线——比如 EMC、IBM 小型机、SGI 超级计算机等等,你如果用它去搭建云计算,成本 太贵。用廉价的解决方案代替昂贵的解决方案是整个计算机发展史中到今天唯一不变的事情。 所以如果你要让夏利车跑出奔驰车的感觉,你需要自己动手做很多事,搭建一个智能的系统。 用廉价的东西做出高质量的东西,运维好廉价的设备其实是云计算工程里最大的挑战。 另一方面,因为你机器多了,然后你用的又不是昂贵的硬件,所以故障就变成了常态,硬盘、 主板、网络天天坏。所以,没什么好想的,运维就必须要跟上。云计算的目标是在故障成为 常态的情况下保证高可用——也就是我们所说的,你服务的可用性是 3 个 9、4 个 9 还是 5 个 9。 最后,这一大堆机器和设备都放在一起,你的安全就是一个挑战,一方面是 Security,另 一方面是 Safety,保证数十台数百台的设备的安全还好说,但是对于数万数十万台的设计, 就没有那么简单了。 所以,面对这样的难题,人是无法搞得定的,你只能依靠技术来管理和运维整个平台。比如 必须有监控系统。这跟操作系统一样,对资源的管理,对网络流量、CPU 利用率、进程、内 存等等的状态肯定要全部收集的。收集整个集群各种节点的状态,是必然每个云计算都有的, 都是大同小异的。 然后,你还要找到可用性更好的节点,这需要有一些故障自检的功能。比如阿里云就遇到过 磁盘用到一定时候就会莫名其妙的不稳定,有些磁盘的 I/O 会变慢。变慢的原因有可是硬 盘不行了,于是硬盘控制器可能因为 CRC 校验出错需要要多读几次,这就好比 TCP 的包传 过来,数据出错了,需要重新传。在这种硬盘处理半死不活的状态时,你肯定是需要一个自 动检测或自动发现的程序去监控这种事情,当这个磁盘可能不行了,标记成坏磁盘,别用它, 到别的磁盘上读复本去。我们要有故障自动检测、预测的措施,才能驱动故障,而不是被动 响应故障,用户体验才会好。换句话说,我们需要自动化的、主动的运维。 为了数据的高可用性,你只能使用数据冗余,写多份到不同的节点——工业界标准写三份是 安全。然而,你做了冗余,又有数据一致性问题。为了解决冗余带来的一致性问题,才有了 paxos 的投票玩法,大家投票这个能不能改,于是你就需要一个强大的控制系统来控制这 些东西。 另外,公有云人来人往,里面的资源和服务今天用明天不用,有分配有释放,有冻结,你还 8 2014 年 6 月《架构师》 要搞一个资源管理系统来管理这些资源的生命状态。还有权限管理,就像 AWS 的 IAM 一样, 如果没有像 AWS 的 IAM 权限管理系统,AWS 可能会不会像今天这样有很多大的公司来用。 企业级的云平台,你需要有企业级的运维和管理能力。 云计算的门槛 为啥云计算有这么多开源的东西,却不是人人都能做?我觉得有以下原因: 一方面,这就跟盖楼一样。盖楼的技术没什么难的(当然,盖高楼是很难的),但是你没地 你怎么盖?我觉得云计算也一样,带宽的价格贵得就像土地的价格。其实云计算跟房地产一 样,要占地、占机房、占带宽。如果能把中国所有的机房、机柜、带宽资源都买了,你就不 用做云计算了,卖土地就够了——因为这些是有限的。最简单的例子,IP 地址是有限的。 你有带宽、有机房,但是如果你没有 IP,这就不好玩了。尤其是你要提供 CDN 服务,这个 就更明显,因为有多少物理节点直接决定你的 CDN 服务质量。 另一方面,正如前面所说的,运维是件很难的事,运维这个事并不是一般人能搞的事。没有 足够的场景、经验和时间,这种能力很难出现。 从用户的角度来说呢,云计算是一种服务,你需要对用户企业内的解决方案要有很好的了解, 这样才能提高很好的服务。能提供“好服务”的通常都是把自己真正当成用户公司。 这跟做汽车一样,底层做引擎、轮子、油箱、控制系统,给你弄一堆零件,上层可以拼装。 PaaS 相当于给你一个很快可以打造成的汽车的工作台。而 SaaS 就是成品——两厢、三厢、 卡车、轿车,最终用户要的是这个。后面什么 Xen、存储、分布式,跟我一毛钱关系没有, 我就要知道汽车是安全的,性能好的,省油的,不会抛锚、耐用的,千万别速度快了或者坡 度大了或是别的怎么样就失灵了。 卖汽车也是卖服务。造出汽车来,并不代表你搞定这个事了。如果没有公路、没有加油站、 没有 4s 店、没有交通管理、规则等等,你要么用不了,要么就是乱七八糟。不能只让用户 在那看着你的汽车好牛啊,但是用户不知道怎么用。所以说,云计算最终旁边必须要有一套 服务设施,而这套服务设施也是今天被人低估的。 云计算有两个东西我觉得是被人低估的,一个是运维,一个是那堆服务。做服务的需要有生 态环境,有人帮你做。所以做云计算要落地并不简单。 这跟 IBM 一样。IBM 有段时间也是快不行了,他们的 CEO 写了一本《谁说大象不能跳舞》, 讲 IBM 的转型,从卖硬件的转成卖服务、解决方案,有流程、咨询,顺便卖硬件,带着一 堆系统集成商一起玩。我给你解决方案,谁来实现呢,就是集成商帮你,然后顺便把硬件卖 给你。一样。未来是什么样,历史上已经有了。你看,要干那么多事,而且还不是用人堆就 可以堆出来的。这就是云计算的门槛。 总之,云计算是需要吃自己的狗食才能吃出来的,绝不是像手机上的 Apps 一样,你想一想、 试一试就能搞出来的,你首先需要让自己有这样的场景,有这样的经历,你才可能会有这样 的经验和能力。 云计算的市场细分 市场细分必然是市场来驱动的。市场变化太快,说不清楚,不过大的方向应该会是这样的: 有类是需要玩计算密集型的(比如大数据计算、网络游戏),有类是需要玩 IO 密集型的(比 如视频网站),有类就是为了建网站的(比如电子商务、门户网站、无线),有类是为了数 投稿信箱:editors@cn.infoq.com 9 据安全和保密的(比如金融数据)。 从更高的层面来看,社会也需要分工。有的人卖土地,有的人卖房子,有的人装修,有的人 是中介。我相信没人愿意把所有的赌注都押在一个地方。云计算也是一样。上面也说过,无 论 IaaS、PaaS、SaaS,后面的 S 都是 service,本质上都是提供服务。所以,我认为, 市场的细分本质上就是服务的细分。 看看历史我们知道,细分永远是跟着行业走的,也是跟着业务走的,所以,在业务层会出现 更多的细分。 对阿里云产业细分的看法 政府云、金融云不太清楚,不过我很清楚电商云——就是我之前负责的聚石塔。聚石塔时间 不长,2012 年 9 月正式上线,去年是大发展的一年,作为垂直云解决的很好。天猫和淘宝 做的都是下单前的东西,下单后,商家每天处理好几百单,需要做订单合并、筛选,有的商 家规模不大但订单很多。海尔有 ERP,这些商家没有,但是每天也 1000 多单,如果没有信 息化的系统,人肉是处理不了的,必然要有 ERP 系统处理订单。另外还要管理用户,给用 户做营销、发展忠实用户。总之,都是卖东西以后的事情。咋办? 淘宝天猫给了一堆开放 API,你可以调我的 API 接入,在你那边有 ISV 帮你做一套东西远 程访问淘宝 API,把订单拉过去,仓库进货了之后,通过 API 把库存改一下,就可以连起来了。 天猫用户下单,到他的系统、他的仓库,他就发货了,仓库补完货,在他的系统里一改,自 动就到天猫店了。这是电子信息化。 但是一到双十一就受不了:订单量太大。正好云平台出现了,再怎么样,阿里的运维能力也 要比你商家的要强吧。你看,聚石塔卖的是服务,不是主机。另外是数据安全:商家的系统 天天被黑客盯着,如果我们把用户信息都给商家,不是所有的商家的系统安全都做得很好, 内部的人插个什么 U 盘,上面一堆木马,数据就被偷走了。偷走了之后,别人还说是阿里搞 丢的,这当然不行。所以,我们又要开放,还要保证安全,聚石塔这个云平台就这样出来的: 你来我这儿,我才开放给你,因为安全很重要。 保证性能和安全也是商家的利益诉求也在里面,商家也不希望用户数据被偷,他也希望双 十一能抗住。 另外,很多商家自己不会做,所以要 ISV(第三方软件开发商)来做,所以这个是卖解决方 案,跟 IBM 很相似。银行要一套系统,IBM 提供硬件和解决方案,系统集成商来帮银行写 代码和集成系统。聚石塔也很像,聚石塔提供 API、ECS、数据库,第三方的 ISV 进来帮商 家集成一个系统。这是很经典的也是很传统的 IBM 的玩法,只不过是玩在了云端。 你看,这也是做自己的长项做出来的细分市场。所以说,吃自己的狗食很重要。 对 PaaS 的看法 无论是 Google 的 GAE 还是新浪的 SAE 都是给个容器,给个容器的好处是不用管数据连接、 CPU 什么,程序一传就能用,什么水平扩展都不用管。不爽的是,一个是在编程上限制太多: AppEngine 总会阉割很多系统相关的功能,比如 Java、PHP、Python 的很多系统调用都 阉割了,因为如果给你这些系统调用,你就可以突破沙箱;另一个是有故障的时候:技术人 遇到问题都恨不得自己上去解决,想看看后面在忙啥,但是看不到,很无助,只能等你解决, 就看你的人解决的好不好、快不快。所以如果 IaaS 没做好,运维、故障自动处理、迁移没 做好,出了问题用户只能干瞪眼,PaaS 必然不好用。当然 IaaS 层也有这个问题,但是至 10 2014 年 6 月《架构师》 少你还可以登到机器上看一看,大不了重启一下。像 AWS,你重启一下就跑到别的物理机, 问题也许就解决了。 其实,对于 PaaS 中间这层的确很尴尬。怎么解决?我觉得还是要依赖某种业务场景。单纯 一个平台要阉割很多功能,搞得用户不舒服,还不如干脆一步到位,根据业务场景给一个编 程框架。比如 SAE 可以就做微博 app,上来就调 API,数据库都 ready;或者微信如果做 个 PaaS,上面只玩微信公众平台上的东西,也可以。我觉得 PaaS 层更贴合业务会更成功。 给新浪微博做个插件,你去买个 VM、买数据库?这种时候很需要 PaaS。我觉得 PaaS 层要 成功就要贴近业务场景。比如:腾讯的风铃系统(虽然不知道企业帐号看见是什么样的), 就做无线建站,这样多好。干巴巴的 PaaS 有点高不成低不就。 对 SDN 的看法 SDN 其意图是想改变目前超级复杂的网络结构。意图是挺好的。想一想,如果以后我家的网 络不用因为买个新的路由器都要重新设计一把,只要一次设置,然后对所有的路由器都通过, 的确是挺方便的,这点对企业非常好。不过,不知道在操作上怎么做,也许会从企业内部开 始这场革命,这个不得而知。 就像开车一样,机械式的方向盘和刹车油门系统这么多年都没什么变化,也提过很多更好更 高科技的解决方案,但是传统还是这样延续下来了。所以,SDN 真不知道未来会怎么样。总 之,一个老的事物到一个新的事物需要有一个过程,这个过程中会出现很多过渡产品或是过 渡方案,如果没有这些过渡产品和方案,也就没法达到新的事物。未来是什么样,无法预知。 对私有云的看法 私有云跟公有云,都会存在。这跟人一样,私人生活和公众生活都会需要的。大公司有 1 万、 2 万人,这么多数据,要存,需要一个很稳定的解决方案。要稳定可以买 IBM,但是贵。云 计算出来说,我可以写三份,但他不想上公有云,我的数据放在别人那里,总感觉不好的, 所以有了私有云做物理隔离,他觉得安全。 安全这个词对应两个英文,security 和 safety,其实 security 和 safety 不一样: 云计算解决 safety,保证数据不丢;宁可数据丢也不让人看到,那是 security。比如私 人照片我更愿意存家里,有一个小的云存储,所有设备同步,跟老家父母同步,这样比较好。 放公网很恐怖。 一定会有公司不愿意上云的,比如金融方面的企业,他们觉得互联网不安全,他们要的更多 的是安全。在公网上你的系统的安全攻防能力都要跟上,但如果物理不通的话就不用考虑的 太复杂。企业内部私有云肯定有市场。你看,好些企业内部目前还被 EMC、IBM 所垄断着呢。 计算机发展史就是廉价的东西取代昂贵的东西,所以私有云一定没问题,而降低私有云的运 维复杂度、提供一个或多个方便的运维系统和工具就是重中之重中。其中,SDN 之类的东西 肯定会是其中一个很重要的一块。 另外,还是那句话,云就是服务,只要提供了好的服务,无论公有还是私有都是会有价值的。 感谢杨赛对本文的策划。 原文:http://www.infoq.com/cn/articles/chenhao-on-cloud 投稿信箱:editors@cn.infoq.com 11 ArchSummit International Architect Summit 2014.07.18-19 中国·深圳 万科国际会议中心 全球架构师峰会2014 - 阿里交易平台的架构演变及并行化实践 - 京东应用架构设计与治理 - 第三方支付风控系统的演进过程 - 腾讯数据中心技术与运维发展 - 百度贴吧服务端架构变迁史 - 陌陌高可用即时通讯架构 - 智能路由器的产品定义与架构 腾讯、百度、小米、阿里、京东、去哪儿、陌陌、晨星、IBM、顺丰...... 移动互联网、云计算、大数据、电商、运维、智能硬件、游戏、SNS、金融。 1个峰会、9大专题、36场演讲 演讲内容早知道 涵盖热门领域 非常NB的嘉宾 议题提交开放,折扣购票启动,详情查阅大会官网 咨询电话:010-89880682 会务咨询:arch@cn.infoq.com 大会官网:www.archsummit.com 12 2014 年 6 月《架构师》 Javascript 高性能动画与页面渲染 作者 李光毅 No setTimeout, No setInterval 如果你不得不使用 setTimeout 或者 setInterval 来实现动画,那么原因只能是你需要 精确的控制动画。但我认为至少在现在这个时间点,高级浏览器、甚至手机浏览器的普及程 度足够让你有理由有条件在实现动画时使用更高效的方式。 什么是高效 页面是每一帧变化都是系统绘制出来的 (GPU 或者 CPU)。但这种绘制又和 PC 游戏的绘制 不同,它的最高绘制频率受限于显示器的刷新频率 ( 而非显卡 ),所以大多数情况下最高的 绘制频率只能是每秒 60 帧 (frame per second,以下用 fps 简称 ),对应于显示器的 60Hz。60fps 是一个最理想的状态,在日常对页面性能的测试中,60fps 也是一个重要的 指标,the closer the better。在 Chrome 的调试工具中,有不少工具都是用于衡量 当前帧数: 接下来的工作中,我们将会用到这些工具,来实时查看我们页面的性能。 60fps 是动力也是压力,因为它意味着我们只有 16.7 毫秒 (1000 / 60) 来绘制每一帧。 如果使用 setTimeout 或者 setInterval( 以下统称为 timer) 来控制绘制,问题就来了。 首先,Timer 计算延时的精确度不够。延时的计算依靠的是浏览器的内置时钟,而时钟的 精确度又取决于时钟更新的频率 (Timer resolution)。IE8 及其之前的 IE 版本更新间 隔为 15.6 毫秒。假设你设定的 setTimeout 延迟为 16.7ms,那么它要更新两个 15.6 毫 秒才会该触发延时。这也意味着无故延迟了 15.6 x 2 - 16.7 = 14.5 毫秒。 16.7ms DELAY: |------------| CLOCK: |----------|----------| 15.6ms 15.6ms 所以即使你给 setTimeout 设定的延时为 0ms,它也不会立即触发。目前 Chrome 与 IE9+ 浏览器的更新频率都为 4ms( 如果你使用的是笔记本电脑,并且在使用电池而非电源 的模式下,为了节省资源,浏览器会将更新频率切换至于系统时间相同,也就意味着更新频 率更低 )。 退一步说,假使 timer resolution 能够达到 16.7ms,它还要面临一个异步队列的问题。 因为异步的关系 setTimeout 中的回调函数并非立即执行,而是需要加入等待队列中。但 投稿信箱:editors@cn.infoq.com 13 问题是,如果在等待延迟触发的过程中,有新的同步脚本需要执行,那么同步脚本不会排在 timer 的回调之后,而是立即执行,比如下面这段代码: function runForSeconds(s) { var start = +new Date(); while (start + s * 1000 > (+new Date())) {} } document.body.addEventListener("click", function () { runForSeconds(10); }, false); setTimeout(function () { console.log("Done!"); }, 1000 * 3); 如果在等待触发延迟的 3 秒过程中,有人点击了 body,那么回调还是准时在 3s 完成时触 发吗?当然不能,它会等待 10s,同步函数总是优先于异步函数: 等待 3 秒延迟 | 1s | 2s | 3s |--->console.log("Done!"); 经过 2 秒 |----1s----|----2s----| |--->console.log("Done!"); 点击 body 后 以为是这样:|----1s----|----2s----|----3s----|--->console.log("Done!")--->|------- -----------10s----------------| 其实是这样:|----1s----|----2s----|------------------10s----------------|--- >console.log("Done!"); John Resign 有三篇关于 Timer 性能与准确性的文章 : 1.Accuracy of JavaScript Time, 2.Analyzing Timer Performance, 3.How JavaScript Timers Work。从文章中可以看到 Timer 在不同平台浏览器与操作系统下的一些问题。 再退一步说,假设 timer resolution 能够达到 16.7ms,并且假设异步函数不会被延后, 使用 timer 控制的动画还是有不尽如人意的地方。这也就是下一节要说的问题。 垂直同步问题 这里请再允许我引入另一个常量 60——屏幕的刷新率 60Hz。 60Hz 和 60fps 有什么关系?没有任何关系。fps 代表 GPU 渲染画面的频率,Hz 代表显示 器刷新屏幕的频率。一幅静态图片,你可以说这副图片的 fps 是 0 帧 / 秒,但绝对不能说 此时屏幕的刷新率是 0Hz,也就是说刷新率不随图像内容的变化而变化。游戏也好浏览器也 好,我们谈到掉帧,是指 GPU 渲染画面频率降低。比如跌落到 30fps 甚至 20fps,但因为 视觉暂留原理,我们看到的画面仍然是运动和连贯的。 接上一节,我们假设每一次 timer 都不会有延时,也不会被同步函数干扰,甚至能把时间 缩短至 16ms,那么会发生什么呢: 14 2014 年 6 月《架构师》 在 22 秒处发生了丢帧。 如果把延迟时间缩的更短,丢失的帧数也就更多: 实际情况会比以上想象的复杂的多。即使你能给出一个固定的延时,解决 60Hz 屏幕下丢帧 问题,那么其他刷新频率的显示器应该怎么办,要知道不同设备、甚至相同设备在不同电池 状态下的屏幕刷新率都不尽相同。 以上同时还忽略了屏幕刷新画面的时间成本。问题产生于 GPU 渲染画面的频率和屏幕刷新 频率的不一致:如果 GPU 渲染出一帧画面的时间比显示器刷新一张画面的时间要短 ( 更快 ), 那么当显示器还没有刷新完一张图片时,GPU 渲染出的另一张图片已经送达并覆盖了前一张, 导致屏幕上画面的撕裂,也就是是上半部分是前一张图片,下半部分是后一张图片: PC 游戏中解决这个问题的方法是开启垂直同 步 (v-sync),也就是让 GPU 妥协,GPU 渲 染图片必须在屏幕两次刷新之间,且必须等 待屏幕发出的垂直同步信号。但这样同样也 是要付出代价的:降低了 GPU 的输出频率, 也就降低了画面的帧数。以至于你在玩需要 高帧数运行的游戏时 ( 比如竞速、第一人称 射击 ) 感觉到“顿卡”,因为掉帧。 投稿信箱:editors@cn.infoq.com 15 requestAnimationFrame 在这里不谈 requestAnimationFrame( 以下简称 rAF) 用法,具体请参考 MDN:Window. requestAnimationFrame()。我们来具体谈谈 rAF 所解决的问题。 从上一节我们可以总结出实现平滑动画的两个因素 1. 时机 (Frame Timing): 新的一帧准备好的时机 2. 成本 (Frame Budget): 渲染新的一帧需要多长的时间 这个 Native API 把我们从纠结于多久刷新的一次的困境中解救出来 ( 其实 rAF 也不关心 距离下次屏幕刷新页面还需要多久 )。当我们调用这个函数的时候,我们告诉它需要做两件 事: 1. 我们需要新的一帧;2. 当你渲染新的一帧时需要执行我传给你的回调函数 那么它解决了我们上面描述的第一个问题,产生新的一帧的时机。 那么第二个问题呢。不,它无能为力。比如可以对比下面两个页面: DEMO DEMO-FIXED 对比两个页面的源码,你会发现只有一处不同: // animation loop function update(timestamp) { for(var m = 0; m < movers.length; m++) { // DEMO 版本 //movers[m].style.left = ((Math.sin(movers[m].offsetTop + timestamp/1000)+1) * 500) + 'px'; // FIXED 版本 movers[m].style.left = ((Math.sin(m + timestamp/1000)+1) * 500) + 'px'; } rAF(update); }; rAF(update); DEMO 版本之所以慢的原因是,在修改每一个物体的 left 值时,会请求这个物体的 offsetTop 值。这是一个非常耗时的 reflow 操作 ( 具体还有哪些耗时的 reflow 操作可以参 考这篇 : How (not) to trigger a layout in WebKit)。这一点从 Chrome 调试 工具中可以看出来 ( 截图中的某些功能需要在 Chrome canary 版本中才可启用 ) 16 2014 年 6 月《架构师》 未矫正的版本 可见大部分时间都花在了 rendering 上,而矫正之后的版本: rendering 时间大大减少了 但如果你的回调函数耗时真的很严重,rAF 还是可以为你做一些什么的。比如当它发现无法 维持 60fps 的频率时,它会把频率降低到 30fps,至少能够保持帧数的稳定,保持动画的 连贯。 使用 rAF 推迟代码 没有什么是万能的,面对上面的情况,我们需要对代码进行组织和优化。 看看下面这样一段代码: 投稿信箱:editors@cn.infoq.com 17 function jank(second) { var start = +new Date(); while (start + second * 1000 > (+new Date())) {} } div.style.backgroundColor = "red"; // some long run task jank(5); div.style.backgroundColor = "blue"; 无论在任何的浏览器中运行上面的代码,你都不会看到 div 变为红色,页面通常会在假死 5 秒,然后容器变为蓝色。这是因为浏览器的始终只有一个线程在运行 ( 可以这么理解,因为 js 引擎与 UI 引擎互斥 )。虽然你告诉浏览器此时 div 背景颜色应该为红色,但是它此时 还在执行脚本,无法调用 UI 线程。 有了这个前提,我们接下来看这段代码: var div = document.getElementById("foo"); var currentWidth = div.innerWidth; div.style.backgroundColor = "blue"; // do some "long running" task, like sorting data 这个时候我们不仅仅需要更新背景颜色,还需要获取容器的宽度。可以想象它的执行顺序如 下: 当我们请求 innerWidth 一类的属性时,浏览器会以为我们马上需要,于是它会立即更新 容器的样式 ( 通常浏览器会攒着一批,等待时机一次性的 repaint,以便节省性能 ),并 把计算的结果告诉我们。这通常是性能消耗量大的工作。 但如果我们并非立即需要得到结果呢? 上面的代码有两处不足: 18 2014 年 6 月《架构师》 1. 更新背景颜色的代码过于提前,根据前一个例子,我们知道,即使在这里告知了浏览器我 需要更新背景颜色,浏览器至少也要等到 js 运行完毕才能调用 UI 线程; 2. 假设后面部分的 long runing 代码会启动一些异步代码,比如 setTimeout 或者 Ajax 请求又或者 web-worker,那应该尽早为妙。 综上所述,如果我们不是那么迫切的需要知道 innerWidth,我们可以使用 rAF 推迟这部 分代码的发生: requestAnimationFrame(function(){ var el = document.getElementById("foo"); var currentWidth = el.innerWidth; el.style.backgroundColor = "blue"; // ... }); // do some "long running" task, like sorting data 可见即使我们在这里没有使用到动画,但仍然可以使用 rAF 优化我们的代码。执行的顺序 会变成: 在这里 rAF 的用法变成了:把代码推迟到下一帧执行。 有时候我们需要把代码推迟的更远,比如这个样子: 投稿信箱:editors@cn.infoq.com 19 再比如我们想要一个效果分两步执行:1.div 的 display 变为 block;2. div 的 top 值缩短移动到某处。如果这两项操作都放入同一帧中的话,浏览器会同时把这两项更改应用 于容器,在同一帧内。于是我们需要两帧把这两项操作区分开来: requestAnimationFrame(function(){ el.style.display = "block"; requestAnimationFrame(function(){ // fire off a CSS transition on its `top` property el.style.top = "300px"; }); }); 这样的写法好像有些不太讲究,Kyle Simpson 有一个开源项目 h5ive,它把上面的用法 封装了起来,并且提供了 API。实现起来非常简单,摘一段代码瞧瞧: function qID(){ var id; do { id = Math.floor(Math.random() * 1E9); } while (id in q_ids); return id; } function queue(cb) { var qid = qID(); q_ids[qid] = rAF(function(){ delete q_ids[qid]; cb.apply(publicAPI,arguments); }); return qid; } function queueAfter(cb) { var qid; qid = queue(function(){ // do our own rAF call here because we want to re-use the same `qid` for both frames q_ids[qid] = rAF(function(){ 20 2014 年 6 月《架构师》 delete q_ids[qid]; cb.apply(publicAPI,arguments); }); }); return qid; } 使用方法: // 插入下一帧 id1 = aFrame.queue(function(){ text = document.createTextNode("##"); body.appendChild(text); }); // 插入下下一帧 id2 = aFrame.queueAfter(function(){ text = document.createTextNode("!!"); body.appendChild(text); }); 使用 rAF 解耦代码 先从一个 2011 年 twitter 遇到的 bug 说起。 当时 twitter 加入了一个新功能:“无限滚动”。也就是当页面滚至底部的时候,去加载更 多的 twitter: $(window).bind('scroll', function () { if (nearBottomOfPage()) { // load more tweets ... } }); 但是在这个功能上线之后,发现了一个严重的 bug:经过几次滚动到最底部之后,滚动就会 变得奇慢无比。 经过排查发现,原来是一条语句引起的:$details.find(".details-pane-outer"); 这还不是真正的罪魁祸首,真正的原因是因为他们将使用的 jQuery 类库从 1.4.2 升级到 了 1.4.4 版。而这 jQuery 其中一个重要的升级是把 Sizzle 的上下文选择器全部替换为 了 querySelectorAll。但是这个接口原实现使用的是 getElementsByClassName。 虽然 querySelectorAll 在大部分情况下性能还是不错的。但在通过 Class 名称选 择元素这一项是占了下风。有两个对比测试可以看出来:1. querySelectorAll v getElementsByClassName 2.jQuery Simple Selector 通过这个 bug,John Resig 给出了一条 ( 实际上是两条,但是今天只取与我们话题有关 的 ) 非常重要的建议 It’s a very, very, bad idea to attach handlers to the window scroll event. 他想表达的意思是,像 scroll,resize 这一类的事件会非常频繁的触发,如果把太多的 代码放进这一类的回调函数中,会延迟页面的滚动,甚至造成无法响应。所以应该把这一类 投稿信箱:editors@cn.infoq.com 21 代码分离出来,放在一个 timer 中,有间隔的去检查是否滚动,再做适当的处理。比如如 下代码: var didScroll = false; $(window).scroll(function() { didScroll = true; }); setInterval(function() { if ( didScroll ) { didScroll = false; // Check your page position and then // Load in more results } }, 250) 这样的作法类似于 Nicholas 将需要长时间运算的循环分解为“片”来进行运算: // 具体可以参考他写的《Javascript 高级程序设计》 // 也可以参考他的这篇博客:http://www.nczonline.net/blog/2009/01/13/speed-up-your- javascript-part-1/ function chunk(array, process, context){ var items = array.concat(); //clone the array setTimeout(function(){ var item = items.shift(); process.call(context, item); if (items.length > 0){ setTimeout(arguments.callee, 100); } }, 100); } 原理其实是一样的,为了优化性能、为了防止浏览器假死,将需要长时间运行的代码分解为 小段执行,能够使浏览器有时间响应其他的请求。 回到 rAF 上来,其实 rAF 也可以完成相同的功能。比如最初的滚动代码是这样: function onScroll() { update(); } function update() { // assume domElements has been declared for(var i = 0; i < domElements.length; i++) { // read offset of DOM elements // to determine visibility - a reflow // then apply some CSS classes // to the visible items - a repaint } } window.addEventListener('scroll', onScroll, false); 22 2014 年 6 月《架构师》 这是很典型的反例:每一次滚动都需要遍历所有元素,而且每一次遍历都会引起 reflow 和 repaint。接下来我们要做的事情就是把这些费时的代码从 update 中解耦出来。 首先我们仍然需要给 scroll 事件添加回调函数,用于记录滚动的情况,以方便其他函数的 查询: var latestKnownScrollY = 0; function onScroll() { latestKnownScrollY = window.scrollY; } 接下来把分离出来的 repaint 或者 reflow 操作全部放入一个 update 函数中,并且使用 rAF 进行调用: function update() { requestAnimationFrame(update); var currentScrollY = latestKnownScrollY; // read offset of DOM elements // and compare to the currentScrollY value // then apply some CSS classes // to the visible items } // kick off requestAnimationFrame(update); 其实解耦的目的已经达到了,但还需要做一些优化,比如不能让 update 无限执行下去,需 要设标志位来控制它的执行: var latestKnownScrollY = 0, ticking = false; function onScroll() { latestKnownScrollY = window.scrollY; requestTick(); } function requestTick() { if(!ticking) { requestAnimationFrame(update); } ticking = true; } 并且我们始终只需要一个 rAF 实例的存在,也不允许无限次的 update 下去,于是我们还 需要一个出口: 投稿信箱:editors@cn.infoq.com 23 function update() { // reset the tick so we can // capture the next onScroll ticking = false; var currentScrollY = latestKnownScrollY; // read offset of DOM elements // and compare to the currentScrollY value // then apply some CSS classes // to the visible items } // kick off - no longer needed! Woo. // update(); 理解 Layer Kyle Simpson 说: Rule of thumb: don’t do in JS what you can do in CSS. 如以上所说,即使使用 rAF,还是会有诸多的不便。我们还有一个选择是使用 css 动画: 虽然浏览器中 UI 线程与 js 线程是互斥,但这一点对 css 动画不成立。 在这里不聊 css 动画的用法。css 动画运用的是什么原理来提升浏览器性能的。 首先我们看看淘宝首页的焦点图: 我想提出一个问题,为什么明明可以使用 translate 2d 去实现的动画,它要用 3d 去实 现呢? 我不是淘宝的员工,但我的第一猜测这么做的原因是为了使用 translate3d hack。 简单来说如果你给一个元素添加上了 -webkit-transform: translateZ(0); 或者 -webkit-transform: translate3d(0,0,0); 属性,那么你就等于告诉了浏览器 用 GPU 来渲染该层,与一般的 CPU 渲染相比,提升了速度和性能。( 我很确定这么做会在 24 2014 年 6 月《架构师》 Chrome 中启用了硬件加速,但在其他平台不做保证。就我得到的资料而言,在大多数浏览 器比如 Firefox、Safari 也是适用的 )。 但这样的说法其实并不准确,至少在现在的 Chrome 版本中这算不上一个 hack。因为默认 渲染所有的网页时都会经过 GPU。那么这么做还有必要吗?有。在理解原理之前,你必须先 了解一个层 (Layer) 的概念。 html 在浏览器中会被转化为 DOM 树,DOM 树的每一个节点都会转化为 RenderObject, 多个 RenderObject 可能又会对应一个或多个 RenderLayer。浏览器渲染的流程如下: 1. 获取 DOM 并将其分割为多个层 (RenderLayer) 2. 将每个层栅格化,并独立的绘制进位图中 3. 将这些位图作为纹理上传至 GPU 4. 复合多个层来生成最终的屏幕图像 ( 终极 layer)。 这和游戏中的 3D 渲染类似,虽然我们看到的是一个立体的人物,但这个人物的皮肤是由不 同的图片“贴”和“拼”上去的。网页比此还多了一个步骤,虽然最终的网页是由多个位图层合成的, 但我们看到的只是一个复印版,最终只有一个层。当然有的层是无法拼合的,比如 flash。 以爱奇艺的一个播放页 (http://www.iqiyi.com/v_19rrgyhg0s.html) 为例,我们 可以利用 Chrome 的 Layer 面板 ( 默认不启用,需要手动开启 ) 查看页面上所有的层: 我们可以看到页面上由如下层组成: 投稿信箱:editors@cn.infoq.com 25 OK,那么问题来了。 假设我现在想改变一个容器的样式 ( 可以看做动画的一个步骤 ),并且是一种最糟糕的情况, 改变它的长和宽——为什么说改变长和宽是最糟糕的情况呢,通常改变一个物体的样式需要 以下四个步骤: 任何属性的改变都导致浏览器重新计算容器的样式,比如你改变的是容器的尺寸或者位置 (reflow),那么首先影响的就是容器的尺寸和位置 ( 也影响了与它相关的父节点自己点相 邻节点的位置等 ),接下来浏览器还需要对容器重新绘制 (repaint);但如果你改变的只 是容器的背景颜色等无关容器尺寸的属性,那么便省去了第一步计算位置的时间。也就是说 如果改变属性在瀑布图中开始的越早 ( 越往上 ),那么影响就越大,效率就越低。reflow 和 repaint 会导致所有受影响节点所在 layer 的位图重绘,反复执行上面的过程,导致效率 降低。 为了把代价降到最低,当然最好只留下 compositing layer 这一个步骤即可。假设当我 们改变一个容器的样式时,影响的只是它自己,并且还无需重绘,直接通过在 GPU 中改变 纹理的属性来改变样式,岂不是更好?这当然是可以实现的,前提是你有自己的 layer 这也是上面硬件加速 hack 的原理,也是 css 动画的原理——给元素创建自己 layer,而 非与页面上大部分的元素共用 layer。 什么样的元素才能创建自己 layer 呢?在 Chrome 中至少要符合以下条件之一: • Layer has 3D or perspective transform CSS properties( 有 3D 元素的属 性 ) • Layer is used by

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 2 金币 [ 分享文档获得金币 ] 1 人已下载

下载文档

相关文档