思想架构

思想架构

服务保障经验谈之服务熔断

架构思想zkbhj 发表了文章 • 0 个评论 • 118 次浏览 • 2018-12-18 10:56 • 来自相关话题

什么是服务熔断?

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

这种牺牲局部,保全整体的措施就叫做熔断。

如果不采取熔断措施,我们的系统会怎样呢?我们来看一个栗子。

当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:





 
一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务A也变为不可用,整个调用链路被拖垮。




像这种调用链路的连锁故障,叫做雪崩。

正所谓刮骨疗毒,壮士断腕。在这种时候,就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程和刚才所讲的考试策略如出一辙:





 
这里需要解释两点:

1.开启熔断

在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。


2.熔断回复

熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
 
更多服务降级熔断限流,参考:https://www.cnblogs.com/raosha ... .html 查看全部
什么是服务熔断?

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

这种牺牲局部,保全整体的措施就叫做熔断。

如果不采取熔断措施,我们的系统会怎样呢?我们来看一个栗子。

当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:

20180507103456708.png

 
一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务A也变为不可用,整个调用链路被拖垮。
20180507103502138.png

像这种调用链路的连锁故障,叫做雪崩

正所谓刮骨疗毒,壮士断腕。在这种时候,就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程和刚才所讲的考试策略如出一辙:

20180507103507143.png

 
这里需要解释两点:

1.开启熔断

在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。


2.熔断回复

熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
 
更多服务降级熔断限流,参考:https://www.cnblogs.com/raosha ... .html

#线下分享笔记#自如的六层次业务与产品设计方法

总结zkbhj 发表了文章 • 0 个评论 • 397 次浏览 • 2018-07-20 10:18 • 来自相关话题

今天,公司给技术团队组织了一场技术培训,培训的课题是《六层次理论与应用方法》。后来熊帅说这是未来要在技术团队的培训上加大投入力度的一个开始,希望真的能如此。其实在年中绩效评定表里,对上级以及公司需要提供的帮助中我就写了:





 
有了这颗定心丸,下半年就只剩下好好努力了!
 
言归正传,听完分享,还是得总结一下,要不然根本吸收不了多少精华。现在步入正题:
 
首先,分享从一个实际例子开始:

如果有个朋友想要戒烟,请你给点建议,你会给出什么建议?

台下的“观众”纷纷给出了“五花八门”的建议:
给他讲吸烟对身体的危害和经济上的开销;给他看恐怖的视频;让他生孩子……(佩服的不行),后来说生完呢?哈哈,我补了句“再生一个”
 
带着这些建议,分享者将思路拉回到正轨上,抛出了以下几个问题:
他是什么样的人,抽烟多久了?他因为什么开始抽烟,因为什么希望戒烟?他一般什么时候抽烟,习惯是什么样的?
 
是的,很明显,我们所有人,都在未对建议对象进行任何了解的前提下,就对他提出各种各样的建议,这样真的有效吗?

不要做着做着爱上了自己的方案并沉浸其中,忘了最初为什么出发

 
我们有时候就是这样:

客户想要的是一条船过河,而我们做出来的是航空母舰

 
所以,我们需要一个高效的思考与沟通工具,也就是今天的重点:

自如的六层次业务与产品设计方法

 1、课程简介
 
作为产品经理,我们经常会遇到下面的疑问和问题:
跨部门合作好难!感觉提的需求不合理!业务方为什么这么强势?那个需求更重要?需求太多!我的价值是什么?为什么每次都这么急?项目时间太紧!为啥要做这个需求?
资源不够出不了成果?
 
而这个工具就是为了解决上面遇到的这些“棘手问题”!学了这个理论方法之后,要达到下面三个目标:
做更有价值的事情建立更高效的沟通做出令人喜爱的产品
 
“六层次模型”其实是“业务方”和“产品经理”之间的一把宝剑,而“产品经理”和“互联网团队”之间的宝剑是“产品架构和应用架构”,这部分会在未来的分享会上具体讲解。

六层架构应该是业务与产品共同拥有的,一张相同的架构图。

 
PPT上有一张自如整个业务体系对应的“六层次”图例,出于保密要求,不能放在文章里分享,大家可以意淫下。我觉得,透过这个六层次架构图,很清晰的就可以看出来自如在做什么!这也是这个六层次模型的魅力和精华所在!
 
2、六层次架构详解与应用
 
找准用户,是挖掘痛点、设计场景的必要条件
 
是谁:租客、业主、经纪人……
客观描述:用户的客观信息,例如人口统计信息等
群体规模:市场规模,覆盖情况
用户分类:按客观信息,按生命周期分类等
 
比如开场的戒烟例子:用户可能就是30~40岁,10年以上烟龄,白领阶层,月收入超3W,关注健康
 
挖掘最核心的痛点/价值
 
痛点就是,设身处地的去想,真的“痛”
价值就是,设身处地的去想,真的“惊喜”“有意义”
让用户最“愤怒”、“苦闷”的问题
 
比如开场的戒烟例子:工作压力大;对自己的健康很焦虑;意志力不足以抵抗“烟瘾”
 
设计/创造符合生活和实际的场景
 
接地气,符合生活与实际
让用户不再“愤怒”、“苦闷”的真实场景
 
比如开场的戒烟例子:烟瘾犯了的时候;可以有相对健康的替代方案;且可以逐步降低“烟瘾”
 
超出用户期待的产品/服务
首先产品和服务接地气
其次,超出期望就会让用户真的认可
 
比如开场的戒烟例子:电子烟;食品药物;在线互联网心理辅导课程
 
运营(组织、流程、绩效、系统)
运营体系是产品/服务落地的基础支撑
组织、流程、绩效和系统本身也是分析问题的方法
 
组织:研发团队、销售团队、营销团队
流程:研发流程、销售流程、服务流程
绩效:职能绩效、销售绩效
系统:官网、微信号、小程序、销售系统、客户系统……
 
合理有效的资源分配与应用
研发、设计、测试、运营都是资源
市场上可以有偿或无偿获取的信息、产品也是资源
资源有效合理的利用,才能将投入产出最大化
 
 
比如开场的戒烟例子:资金、医药学家、制药工厂、销售渠道、系统平台
 
综上,六层次理论其实就是:
 





3、什么场景使用?或者什么时候能够应用六层次架构?
 
统一跨团队沟通方法,保证目标一致解决日常遇到的问题,做一个项目系统的理解一个业务、产品
 
4、实际举例:
 
  分享中使用了自如搬家刚推出的服务——自如小搬产品进行了举例,用六层次理论来理解公司的自如小搬产品。由于涉及内部资料,不在这里陈述示例。
 最后划重点:
 

你的产品,给谁,解决了什么问题或提供了什么价值

 
而且,六层次理论能教你“从未来看现在”,也就是用未来的视角,解决目前遇到的问题!

至于后来熊帅问我“如果我是一个能够到俄罗斯世界杯决赛去看球的人,最希望做的一件事是什么?”的时候,我第一个想到的确实是带回一些世界杯周边,但是我还是想多了,因为之前在参加邓紫棋演唱会的时候,在演唱会内外最不缺的就是周边——什么T恤衫、水杯、折扇巴拉巴拉,中国人最爱搞这种来赚钱,难道去俄罗斯也还是这一套?事实证明,我想多了!(尴尬……)说实话挺佩服熊帅的,看个球也能把身边的事情投射到这个“六层次业务和产品的设计方法”中。不过这也印证了,这个方法理论的广泛适用性以及它的正确性。
熊帅有一句话说的我觉得很对:
 

一个人工作了3年、5年或者10年,当你问他有什么成长的时候,如果他说“自己从一个学生变成了社会人”,那他基本上没有什么成长。一个人的成长,应该体现在他沉淀和吸收了多少理论和方法。这些理论和方法是骨架,能够给你提供思路和指导,就好比一个个书架,至于这个书架上能够放上什么哪些纪念品、哪些书、哪些“有血有肉”的东西,就要看你的思考有多深,行动有多少!

 
期待更加精彩的技术和理论分享。 查看全部
今天,公司给技术团队组织了一场技术培训,培训的课题是《六层次理论与应用方法》。后来熊帅说这是未来要在技术团队的培训上加大投入力度的一个开始,希望真的能如此。其实在年中绩效评定表里,对上级以及公司需要提供的帮助中我就写了:

QQ截图20180720100050.jpg

 
有了这颗定心丸,下半年就只剩下好好努力了!
 
言归正传,听完分享,还是得总结一下,要不然根本吸收不了多少精华。现在步入正题:
 
首先,分享从一个实际例子开始:


如果有个朋友想要戒烟,请你给点建议,你会给出什么建议?


台下的“观众”纷纷给出了“五花八门”的建议:
  1. 给他讲吸烟对身体的危害和经济上的开销;
  2. 给他看恐怖的视频;
  3. 让他生孩子……(佩服的不行),后来说生完呢?哈哈,我补了句“再生一个”

 
带着这些建议,分享者将思路拉回到正轨上,抛出了以下几个问题:
  1. 他是什么样的人,抽烟多久了?
  2. 他因为什么开始抽烟,因为什么希望戒烟?
  3. 他一般什么时候抽烟,习惯是什么样的?

 
是的,很明显,我们所有人,都在未对建议对象进行任何了解的前提下,就对他提出各种各样的建议,这样真的有效吗?


不要做着做着爱上了自己的方案并沉浸其中,忘了最初为什么出发


 
我们有时候就是这样:


客户想要的是一条船过河,而我们做出来的是航空母舰


 
所以,我们需要一个高效的思考与沟通工具,也就是今天的重点:


自如的六层次业务与产品设计方法


 1、课程简介
 
作为产品经理,我们经常会遇到下面的疑问和问题:
  • 跨部门合作好难!
  • 感觉提的需求不合理!
  • 业务方为什么这么强势?
  • 那个需求更重要?
  • 需求太多!
  • 我的价值是什么?
  • 为什么每次都这么急?
  • 项目时间太紧!
  • 为啥要做这个需求?

  • 资源不够出不了成果?

 
而这个工具就是为了解决上面遇到的这些“棘手问题”!学了这个理论方法之后,要达到下面三个目标:
  1. 做更有价值的事情
  2. 建立更高效的沟通
  3. 做出令人喜爱的产品

 
“六层次模型”其实是“业务方”和“产品经理”之间的一把宝剑,而“产品经理”和“互联网团队”之间的宝剑是“产品架构和应用架构”,这部分会在未来的分享会上具体讲解。


六层架构应该是业务与产品共同拥有的,一张相同的架构图。


 
PPT上有一张自如整个业务体系对应的“六层次”图例,出于保密要求,不能放在文章里分享,大家可以意淫下。我觉得,透过这个六层次架构图,很清晰的就可以看出来自如在做什么!这也是这个六层次模型的魅力和精华所在!
 
2、六层次架构详解与应用
 
找准用户,是挖掘痛点、设计场景的必要条件
 
是谁:租客、业主、经纪人……
客观描述:用户的客观信息,例如人口统计信息等
群体规模:市场规模,覆盖情况
用户分类:按客观信息,按生命周期分类等
 
比如开场的戒烟例子:用户可能就是30~40岁,10年以上烟龄,白领阶层,月收入超3W,关注健康
 
挖掘最核心的痛点/价值
 
痛点就是,设身处地的去想,真的“痛”
价值就是,设身处地的去想,真的“惊喜”“有意义”
让用户最“愤怒”、“苦闷”的问题
 
比如开场的戒烟例子:工作压力大;对自己的健康很焦虑;意志力不足以抵抗“烟瘾”
 
设计/创造符合生活和实际的场景
 
接地气,符合生活与实际
让用户不再“愤怒”、“苦闷”的真实场景
 
比如开场的戒烟例子:烟瘾犯了的时候;可以有相对健康的替代方案;且可以逐步降低“烟瘾”
 
超出用户期待的产品/服务
首先产品和服务接地气
其次,超出期望就会让用户真的认可
 
比如开场的戒烟例子:电子烟;食品药物;在线互联网心理辅导课程
 
运营(组织、流程、绩效、系统)
运营体系是产品/服务落地的基础支撑
组织、流程、绩效和系统本身也是分析问题的方法
 
组织:研发团队、销售团队、营销团队
流程:研发流程、销售流程、服务流程
绩效:职能绩效、销售绩效
系统:官网、微信号、小程序、销售系统、客户系统……
 
合理有效的资源分配与应用
研发、设计、测试、运营都是资源
市场上可以有偿或无偿获取的信息、产品也是资源
资源有效合理的利用,才能将投入产出最大化
 
 
比如开场的戒烟例子:资金、医药学家、制药工厂、销售渠道、系统平台
 
综上,六层次理论其实就是:
 
QQ截图20180720103332.jpg


3、什么场景使用?或者什么时候能够应用六层次架构?
 
  • 统一跨团队沟通方法,保证目标一致
  • 解决日常遇到的问题,做一个项目
  • 系统的理解一个业务、产品

 
4、实际举例:
 
  分享中使用了自如搬家刚推出的服务——自如小搬产品进行了举例,用六层次理论来理解公司的自如小搬产品。由于涉及内部资料,不在这里陈述示例。
 最后划重点:
 


你的产品,给谁,解决了什么问题或提供了什么价值


 
而且,六层次理论能教你“从未来看现在”,也就是用未来的视角,解决目前遇到的问题!

至于后来熊帅问我“如果我是一个能够到俄罗斯世界杯决赛去看球的人,最希望做的一件事是什么?”的时候,我第一个想到的确实是带回一些世界杯周边,但是我还是想多了,因为之前在参加邓紫棋演唱会的时候,在演唱会内外最不缺的就是周边——什么T恤衫、水杯、折扇巴拉巴拉,中国人最爱搞这种来赚钱,难道去俄罗斯也还是这一套?事实证明,我想多了!(尴尬……)说实话挺佩服熊帅的,看个球也能把身边的事情投射到这个“六层次业务和产品的设计方法”中。不过这也印证了,这个方法理论的广泛适用性以及它的正确性。
熊帅有一句话说的我觉得很对:
 


一个人工作了3年、5年或者10年,当你问他有什么成长的时候,如果他说“自己从一个学生变成了社会人”,那他基本上没有什么成长。一个人的成长,应该体现在他沉淀和吸收了多少理论和方法。这些理论和方法是骨架,能够给你提供思路和指导,就好比一个个书架,至于这个书架上能够放上什么哪些纪念品、哪些书、哪些“有血有肉”的东西,就要看你的思考有多深,行动有多少!


 
期待更加精彩的技术和理论分享。

饿了么的PWA升级实践

前端开发zkbhj 发表了文章 • 0 个评论 • 440 次浏览 • 2018-02-03 14:00 • 来自相关话题

自Vue.js在官方推特第一次公开到现在,我们就一直在进行着将饿了么移动端网站升级为 Progressive Web App的工作。直到近日在GoogleI/O 2017上登台亮相,才终于算告一段落。我们非常荣幸能够发布全世界第一个专门面向国内用户的PWA,但更荣幸的是能与 Google、 UC以及腾讯合作,一起推动国内Web与浏览器生态的发展。

多页应用、 Vue.js、 PWA?

对于构建一个希望达到原生应用级别体验的PWA,目前社区里的主流做法都是采用SPA,即单页面应用模型(Single-page App)来组织整个Web应用,业内最有名的几个PWA案例Twitter Lite、 Flipkart Lite、Housing Go 与 Polymer Shop无一例外。

然而饿了么,与很多国内的电商网站一样,青睐多页面应用模型(MPA, Multi-page App)所能带来的一些好处,也因此在一年多前就将移动站从基于AngularJS的单页应用重构为目前的多页应用模型。团队最看重的优点莫过于页面与页面之间的隔离与解耦,这使得我们可以将每个页面当做一个独立的“微服务”来看待,这些服务可以被独立迭代,独立提供给各种第三方的入口嵌入,甚至被不同的团队独立维护。而整个网站则只是各种服务的集合而非一个巨大的整体。

与此同时,我们仍然依赖 Vue.js作为JavaScript框架。 Vue.js除了是React、 AngularJS这种“重型武器”的竞争对手外,其轻量与高性能的优点使得它同样可以作为传统多页应用开发中流行的“jQuery/Zepto/Kissy+模板引擎”技术栈的完美替代。 Vue.js提供的组件系统、声明式与响应式编程更是提升了代码组织、共享、数据流控制、渲染等各个环节的开发效率。 Vue 还是一个渐进式框架,如果网站的复杂度继续提升,我们可以按需、增量地引入Vuex或Vue-Router这些模块。万一哪天又要改回单页呢?(谁知道呢……)

2017年, PWA已经成为Web应用新的风潮。我们决定试试,以我们现有的“Vue.js+多页”架构,能在升级PWA的道路上走多远,达到怎样的效果。

实现“PRPL”模式

“PRPL”(读作“purple”)是Google工程师提出的一种Web应用架构模式,它旨在利用现代Web平台的新技术以大幅优化移动Web的性能与体验,对如何组织与设计高性能的PWA系统提供了一种高层次的抽象。我们并不准备从头重构我们的Web应用,不过我们可以把实现“PRPL”模式作为我们的迁移目标。“PRPL”实际上是“Push/Preload、 Render、 Precache、 Lazy-Load”的缩写,我们接下来会展开介绍它们的具体含义。

Push/Preload,推送/预加载初始URL路由所需的关键资源

无论是HTTP2 Server Push还是,其关键都在于,我们希望提前请求一些隐藏在应用依赖关系(Dependency Graph)较深处的资源,以节省HTTP往返、浏览器解析文档,或脚本执行的时间。比如说,对于一个基于路由进行code splitting的SPA,如果我们可以在Webpack清单、路由等入口代码(entry chunks)被下载与运行之前就把初始URL,即用户访问的入口URL路由所依赖的代码用Server Push推送或进行提前加载。那么当这些资源被真正请求时,它们可能已经下载好并存在缓存中了,这样就加快了初始路由所有依赖的就绪。

在多页应用中,每一个路由本来就只会请求这个路由所需要的资源,并且通常依赖也都比较扁平。饿了么移动站的大部分脚本依赖都是普通的 <script> 元素,因此他们可以在文档解析早期就被浏览器的preloader扫描出来并且开始请求,其效果其实与显式的是一致的,见图1所示。

图1 有无<link rel=“preload”>的效果对比
 
我们还将所有关键的静态资源都伺服在同一域名下(不再做域名散列),以更好地利用HTTP2带来的多路复用(Multiplexing)。同时,我们也在进行着对API进行Server Push的实验。

Render,渲染初始路由,尽快让应用可被交互

既然所有初始路由的依赖都已经就绪,我们就可以尽快开始初始路由的渲染,这有助于提升应用诸如首次渲染时间、可交互时间等指标。多页应用并不使用基于JavaScript的路由,而是传统的HTML跳转机制,所以对于这一部分,多页应用其实不用额外做什么。

Precache,用Service Worker预缓存剩下的路由

这一部分就需要Service Worker的参与了。Service Worker是一个位于浏览器与网络之间的客户端代理,它已可拦截、处理、响应流经的HTTP请求,使得开发者得以从缓存中向Web应用提供资源而闻名。不过, Service Worker其实也可以主动发起 HTTP 请求,在“后台”预请求与预缓存我们未来所需要的资源,见图2所示。

图2 Service Worker预缓存未来所需要的资源
 
我们已经使用Webpack在构建过程中进行.vue编译、文件名哈希等工作,于是我们编写了一个Webpack插件来帮助收集需要缓存的依赖到一个“预缓存清单”中,并使用这个清单在每次构建时生成新的Service Worker文件。在新的Service Worker被激活时,清单里的资源就会被请求与缓存,这其实与SW-Precache 这个库的运行机制非常接近。

实际上,我们只对标记为“关键路由”的路由进行依赖收集。你可以将这些“关键路由”的依赖理解为我们整个应用的“App Shell” 或者说“安装包”。一旦它们都被缓存,或者说成功安装,无论用户是在线离线,我们的Web应用都可以从缓存中直接启动。对于那些并不那么重要的路由,我们则采取在运行时增量缓存的方式。我们使用的SW-Toolbox提供了LRU替换策略与TTL失效机制,可以保证我们的应用不会超过浏览器的缓存配额。

Lazy-Load,按需懒加载、懒实例化剩下的路由

懒加载与懒实例化剩下的路由对于SPA是一件相对麻烦点儿的事情,你需要实现基于路由的code splitting与异步加载。幸运的是,这又是一件不需要多页应用担心的事情,多页应用中的各个路由天生就是分离的。

值得说明的是,无论单页还是多页应用,如果在上一步中,我们已经将这些路由的资源都预先下载与缓存好了,那么懒加载就几乎是瞬时完成的了,这时候我们就只需要付出实例化的代价。

至此,我们对PRPL的四部分含义做了详细说明。有趣的是,我们发现多页应用在实现PRPL这件事甚至比单页还要容易一些。那么结果如何呢?

根据Google推出的Web性能分析工具Lighthouse(v1.6),在模拟的3G网络下,用户的初次访问(无任何缓存)大约在2秒左右达到“可交互”,可以说非常不错,见图3所示。而对于再次访问,由于所有资源都直接来自于Service Worker缓存,页面可以在1秒左右就达到可交互的状态了。
 
图3 Lighthouse跑分结果
 
但是,故事并不是这么简单得就结束了。在实际体验中我们发现,应用在页与页的切换时,仍然存在着非常明显的白屏空隙,见图4所示。由于PWA是全屏运行,白屏对用户体验所带来的负面影响甚至比以往在浏览器内更大。我们不是已经用Service Worker缓存了所有资源了吗,怎么还会这样呢?

图4 从首页点击到发现页,跳转过程中的白屏
 
多页应用的陷阱:重启开销

与SPA不同,在多页应用中,路由的切换是原生的浏览器文档跳转(Navigating across documents),这意味着之前的页面会被完全丢弃而浏览器需要为下一个路由的页面重新执行所有的启动步骤:重新下载资源、重新解析HTML、重新运行JavaScript、重新解码图片、重新布局页面、重新绘制……即使其中的很多步骤本是可以在多个路由之间复用的。这些工作无疑将产生巨大的计算开销,也因此需要付出相当多的时间成本。

图5中为我们的入口页(同时也是最重要的页面)在两倍CPU节流模拟下的Profile数据。即使我们可以将“可交互时间”控制在 1 秒左右,我们的用户仍然会觉得这对于“仅仅切换个标签”来说实在是太慢了。

图5 入口页在两倍CPU节流模拟下的Profile数据

巨大的JavaScript重启开销

根据Profile,我们发现在首次渲染(First Paint)发生之前,大量的时间(900ms)都消耗在了JavaScript的运行上(Evaluate Script)。几乎所有脚本都是阻塞的(Parser-blocking),不过因为所有的UI都是由JavaScript/Vue.js驱动的,倒也不会有性能影响。这900ms中,约一半是消耗在Vue.js运行时、组件、库等依赖的运行上,而另一半则花在了业务组件实例化时Vue.js的启动与渲染上。从软件工程角度来说,我们需要这些抽象,所以这里并不是想责怪JavaScript或是Vue.js所带来的开销。

但是,在SPA中, JavaScript的启动成本是均摊到整个生命周期的:每个脚本都只需要被解析与编译一次,诸如生成Virtual DOM等较重的任务可以只执行一次,像Vue.js的ViewModel或是Virtual DOM这样的大对象也可以被留在内存里复用。可惜在多页应用里就不是这样了,我们每次切换页面都为JavaScript付出了巨大的重启代价。

浏览器的缓存啊,能不能帮帮忙?

能,也不能。

V8提供了代码缓存(code caching),可以将编译后的机器码在本地拷贝一份,这样我们就可以在下次请求同一个脚本时一次省略掉请求、解析、编译的所有工作。而且,对于缓存在Service Worker配套的Cache Storage中的脚本,会在第一次执行后就触发V8的代码缓存,这对于我们的多页切换能提供不少帮助。

另外一个你或许听过的浏览器缓存叫做“进退缓存”, Back-Forward Cache,简称bfcache。浏览器厂商对其的命名各异, Opera称之为Fast History Navigation, Webkit称其为Page Cache。但是思路都一样,就是我们可以让浏览器在跳转时把前一页留存在内存中,保留JavaScript与DOM的状态,而不是全都销毁掉。你可以随便找个传统的多页网站在iOS Safari上试试,无论是通过浏览器的前进后退按钮、手势,还是通过超链接(会有一些不同),基本都可以看到瞬间加载的效果。

Bfcache其实非常适合多页应用。但不幸的是,Chrome由于内存开销与其多进程架构等原因目前并不支持。 Chrome现阶段仅仅只是用了传统的HTTP磁盘缓存,来稍稍简化了一下加载过程而已。对于Chromium内核霸占的Android生态来说,我们没法指望了。

为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
 
为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
图6 在添加骨架屏后,从发现页点回首页的效果

这效果本该很轻松的就能实现,不过实际上我们还费了点功夫。

在构建时使用 Vue 预渲染骨架屏

你可能已经想到了,为了让骨架屏可以被Service Worker缓存,瞬间加载并独立于JavaScript渲染,我们需要把组成骨架屏的HTML标签、 CSS样式与图片资源一并内联至各个路由的静态*.html文件中。

不过,我们并不准备手动编写这些骨架屏。你想啊,如果每次真实组件有迭代(每一个路由对我们来说都是一个Vue.js组件),我们都需要手动去同步每一个变化到骨架屏的话,那实在是太繁琐且难以维护了。好在,骨架屏不过是当数据还未加载进来前,页面的一个空白版本而已。如果我们能将骨架屏实现为真实组件的一个特殊状态——“空状态”的话,从理论上就可以从真实组件中直接渲染出骨架屏来。

而Vue.js的多才多艺就在这时体现出来了,我们真的可以用Vue.js 的服务端渲染模块来实现这个想法,不过不是用在真正的服务器上,而是在构建时用它把组件的空状态预先渲染成字符串并注入到HTML模板中。你需要调整Vue.js组件代码使得它可以在Node上执行,有些页面对DOM/BOM的依赖一时无法轻易去除得,我们目前只好额外编写一个*.shell.vue来暂时绕过这个问题。

关于浏览器的绘制(Painting)

HTML文件中有标签并不意味着这些标签就能立刻被绘制到屏幕上,你必须保证页面的关键渲染路径是为此优化的。很多开发者相信将Script标签放在body的底部就足以保证内容能在脚本执行之前被绘制,这对于能渲染不完整DOM树的浏览器(比如桌面浏览器常见的流式渲染)来说可能是成立的。但移动端的浏览器很可能因为考虑到较慢的硬件、电量消耗等因素并不这么做。不仅如此,即使你曾被告知设为async或defer的脚本就不会阻塞HTML解析了,但这可不意味着浏览 器就一定会在执行它们之前进行渲染。

首先我想澄清的是,根据 HTML 规范 Scripting 章节, async脚本是在其请求完成后立刻运行的,因此它本来就可能阻塞到解析。只有defer(且非内联)与最新的type=module被指定为“一定不会阻塞解析”(不过defer目前也有点小问题……我们稍后会再提到),见图7所示。
图7 具有不同属性的Script脚本对HTML解析的阻塞情况

而更重要的是,一个不阻塞HTML解析的脚本仍然可能阻塞到绘制。我做了一个简化的“最小多页PWA”(Minimal Multi-page PWA,或MMPWA)来测试这个问题:我们在一个async(且确实不阻塞HTML解析)脚本中,生成并渲染1000个列表项,然后测试骨架屏能否在脚本执行之前渲染出来。图8是通过USB Debugging在我的Nexus 5真机上录制的Profile。


图8 通过USB Debugging在Nexus 5真机上录制的Profile

是的,出乎意料吗?首次渲染确实被阻塞到脚本执行结束后才发生。究其原因,如果我们在浏览器还未完成上一次绘制工作之前就过快得进行了DOM操作,我们亲爱的浏览器就只好抛弃所有它已经完成的像素,且一直要等待到DOM操作引起的所有工作结束之后才能重新进行下一次渲染。而这种情况更容易在拥有较慢CPU/GPU的移动设备上出现。

黑魔法:利用setTimeout()让绘制提前

不难发现,骨架屏的绘制与脚本执行实际是一个竞态。大概是Vue.js太快了,我们的骨架屏还是有非常大的概率绘制不出来。于是我们想着如何能让脚本执行慢点,或者说,“懒”点。于是我们想到了一个经典的Hack: setTimeout(callback, 0)。我们试着把MMPWA中的DOM操作(渲染1000个列表)放进setTimeout(callback, 0)里……

当当!首次渲染瞬间就被提前了,见图9所示。如果你熟悉浏览器的事件循环模型(Event Loop)的话,这招Hack其实是通过setTimeout的回调把DOM操作放到了事件循环的任务队列中以避免它在当前循环执行,这样浏览器就得以在主线程空闲时喘息一下(更新一下渲染)了。如果你想亲手试试 MMPWA的话,你可以访问github.com/Huxpro/mmpwa 或huangxuan.me/mmpwa/ ,查看代码与Demo。我把UI设计成了A/B Test的形式并改为渲染5000个列表项来让效果更夸张一些。

图9 利用Hack技术,提前完成骨架屏的绘制

回到饿了么PWA上,我们同样试着把new Vue()放到了setTimeout中。果然,黑魔法再次显灵,骨架屏在每次跳转后都能立刻被渲染。这时的Profile看起来是这样的,见图10所示。

 
图10 为感知体验进行各种优化后的最终Profile

现在,我们在400ms时触发首次渲染(骨架屏),在600ms时完成真实UI的渲染并达到页面的可交互。你可以详细对比下图9和图10所示的优化前后Profile的区别。

被我“defer”的有关defer的Bug

不知道你发现没有,在图10的Profile中,我们仍然有不少脚本是阻塞了HTML解析的。好吧,让我解释一下,由于历史原因,我们确实保留了一部分的阻塞脚本,比如侵入性很强的lib-flexible,我们没法轻易去除它。不过, Profile里的大部分阻塞脚本实际上都设置了defer,我们本以为他们应该在HTML解析完成之后才被执行,结果被Profile打了一脸。

我和Jake Archibald 聊了一下,果然这是Chrome的Bug: defer的脚本被完全缓存时,并没有遵守规范等待解析结束,反而阻塞了解析与渲染。Jake已经提交在crbug上了,一起给它投票吧。

最后,图11是优化后的Lighthouse跑分结果,同样可以看到明显的性能提升。需要说明的是,能影响Lighthouse跑分的因素有很多,所以我建议你以控制变量(跑分用的设备、跑分时的网络环境等)的方式来进行对照实验。

图11 优化后的Lighthouse跑分结果

最后为大家展示下应用的架构示意图,见图12所示。

图12 应用架构示意图

一些感想

多页应用仍然有很长的路要走

Web是一个极其多样化的平台。从静态的博客,到电商网站,再到桌面级的生产力软件,它们全都是Web这个大家庭的第一公民。而我们组织Web应用的方式,也同样只会更多而不会更少:多页、单页、 Universal JavaScript应用、 WebGL,以及可以预见的Web Assembly。不同的技术之间没有贵贱,但是适用场景的差距确是客观存在的。

Jake 曾在 Chrome Dev Summit 2016 上说过“PWA!== SPA”。可是尽管我们已经用上了一系列最新的技术(PRPL、 Service Worker、 App Shell……),我们仍然因为多页应用模型本身的缺陷有着难以逾越的一些障碍。多页应用在未来可能会有“bfcache API”、 Navigation Transition等新的规范以缩小跟SPA的距离,不过我们也必须承认,时至今日,多页应用的局限性也是非常明显的。

而PWA终将带领Web应用进入新的时代

即使我们的多页应用在升级PWA的路上不如单页应用来得那么闪亮,但是PWA背后的想法与技术却实实在在地帮助我们在Web平台上提供了更好的用户体验。

PWA作为下一代 Web 应用模型,其尝试解决的是Web平台本身的根本性问题:对网络与浏览器UI的硬依赖。因此,任何Web应用都可以从中获益,这与你是多页还是单页、面向桌面还是移动端、是用React还是Vue.js无关。或许,它还终将改变用户对移动Web的期待。现如今,谁还觉得桌面端的Web只是个看文档的地方呢?

还是那句老话,让我们的用户,也像我们这般热爱Web吧。

最后,感谢饿了么的王亦斯、任光辉、题叶,Google 的 Michael Yeung、 DevRel 团队, UC浏览器团队,腾讯X5浏览器团队在这次项目中的合作。感谢尤雨溪、陈蒙迪和Jake Archibald 在写作过程中给予我的帮助。
 
原文阅读:https://zhuanlan.zhihu.com/p/27836133 查看全部
自Vue.js在官方推特第一次公开到现在,我们就一直在进行着将饿了么移动端网站升级为 Progressive Web App的工作。直到近日在GoogleI/O 2017上登台亮相,才终于算告一段落。我们非常荣幸能够发布全世界第一个专门面向国内用户的PWA,但更荣幸的是能与 Google、 UC以及腾讯合作,一起推动国内Web与浏览器生态的发展。

多页应用、 Vue.js、 PWA?

对于构建一个希望达到原生应用级别体验的PWA,目前社区里的主流做法都是采用SPA,即单页面应用模型(Single-page App)来组织整个Web应用,业内最有名的几个PWA案例Twitter Lite、 Flipkart Lite、Housing Go 与 Polymer Shop无一例外。

然而饿了么,与很多国内的电商网站一样,青睐多页面应用模型(MPA, Multi-page App)所能带来的一些好处,也因此在一年多前就将移动站从基于AngularJS的单页应用重构为目前的多页应用模型。团队最看重的优点莫过于页面与页面之间的隔离与解耦,这使得我们可以将每个页面当做一个独立的“微服务”来看待,这些服务可以被独立迭代,独立提供给各种第三方的入口嵌入,甚至被不同的团队独立维护。而整个网站则只是各种服务的集合而非一个巨大的整体。

与此同时,我们仍然依赖 Vue.js作为JavaScript框架。 Vue.js除了是React、 AngularJS这种“重型武器”的竞争对手外,其轻量与高性能的优点使得它同样可以作为传统多页应用开发中流行的“jQuery/Zepto/Kissy+模板引擎”技术栈的完美替代。 Vue.js提供的组件系统、声明式与响应式编程更是提升了代码组织、共享、数据流控制、渲染等各个环节的开发效率。 Vue 还是一个渐进式框架,如果网站的复杂度继续提升,我们可以按需、增量地引入Vuex或Vue-Router这些模块。万一哪天又要改回单页呢?(谁知道呢……)

2017年, PWA已经成为Web应用新的风潮。我们决定试试,以我们现有的“Vue.js+多页”架构,能在升级PWA的道路上走多远,达到怎样的效果。

实现“PRPL”模式

“PRPL”(读作“purple”)是Google工程师提出的一种Web应用架构模式,它旨在利用现代Web平台的新技术以大幅优化移动Web的性能与体验,对如何组织与设计高性能的PWA系统提供了一种高层次的抽象。我们并不准备从头重构我们的Web应用,不过我们可以把实现“PRPL”模式作为我们的迁移目标。“PRPL”实际上是“Push/Preload、 Render、 Precache、 Lazy-Load”的缩写,我们接下来会展开介绍它们的具体含义。

Push/Preload,推送/预加载初始URL路由所需的关键资源

无论是HTTP2 Server Push还是,其关键都在于,我们希望提前请求一些隐藏在应用依赖关系(Dependency Graph)较深处的资源,以节省HTTP往返、浏览器解析文档,或脚本执行的时间。比如说,对于一个基于路由进行code splitting的SPA,如果我们可以在Webpack清单、路由等入口代码(entry chunks)被下载与运行之前就把初始URL,即用户访问的入口URL路由所依赖的代码用Server Push推送或进行提前加载。那么当这些资源被真正请求时,它们可能已经下载好并存在缓存中了,这样就加快了初始路由所有依赖的就绪。

在多页应用中,每一个路由本来就只会请求这个路由所需要的资源,并且通常依赖也都比较扁平。饿了么移动站的大部分脚本依赖都是普通的 <script> 元素,因此他们可以在文档解析早期就被浏览器的preloader扫描出来并且开始请求,其效果其实与显式的是一致的,见图1所示。

图1 有无<link rel=“preload”>的效果对比
 
我们还将所有关键的静态资源都伺服在同一域名下(不再做域名散列),以更好地利用HTTP2带来的多路复用(Multiplexing)。同时,我们也在进行着对API进行Server Push的实验。

Render,渲染初始路由,尽快让应用可被交互

既然所有初始路由的依赖都已经就绪,我们就可以尽快开始初始路由的渲染,这有助于提升应用诸如首次渲染时间、可交互时间等指标。多页应用并不使用基于JavaScript的路由,而是传统的HTML跳转机制,所以对于这一部分,多页应用其实不用额外做什么。

Precache,用Service Worker预缓存剩下的路由

这一部分就需要Service Worker的参与了。Service Worker是一个位于浏览器与网络之间的客户端代理,它已可拦截、处理、响应流经的HTTP请求,使得开发者得以从缓存中向Web应用提供资源而闻名。不过, Service Worker其实也可以主动发起 HTTP 请求,在“后台”预请求与预缓存我们未来所需要的资源,见图2所示。

图2 Service Worker预缓存未来所需要的资源
 
我们已经使用Webpack在构建过程中进行.vue编译、文件名哈希等工作,于是我们编写了一个Webpack插件来帮助收集需要缓存的依赖到一个“预缓存清单”中,并使用这个清单在每次构建时生成新的Service Worker文件。在新的Service Worker被激活时,清单里的资源就会被请求与缓存,这其实与SW-Precache 这个库的运行机制非常接近。

实际上,我们只对标记为“关键路由”的路由进行依赖收集。你可以将这些“关键路由”的依赖理解为我们整个应用的“App Shell” 或者说“安装包”。一旦它们都被缓存,或者说成功安装,无论用户是在线离线,我们的Web应用都可以从缓存中直接启动。对于那些并不那么重要的路由,我们则采取在运行时增量缓存的方式。我们使用的SW-Toolbox提供了LRU替换策略与TTL失效机制,可以保证我们的应用不会超过浏览器的缓存配额。

Lazy-Load,按需懒加载、懒实例化剩下的路由

懒加载与懒实例化剩下的路由对于SPA是一件相对麻烦点儿的事情,你需要实现基于路由的code splitting与异步加载。幸运的是,这又是一件不需要多页应用担心的事情,多页应用中的各个路由天生就是分离的。

值得说明的是,无论单页还是多页应用,如果在上一步中,我们已经将这些路由的资源都预先下载与缓存好了,那么懒加载就几乎是瞬时完成的了,这时候我们就只需要付出实例化的代价。

至此,我们对PRPL的四部分含义做了详细说明。有趣的是,我们发现多页应用在实现PRPL这件事甚至比单页还要容易一些。那么结果如何呢?

根据Google推出的Web性能分析工具Lighthouse(v1.6),在模拟的3G网络下,用户的初次访问(无任何缓存)大约在2秒左右达到“可交互”,可以说非常不错,见图3所示。而对于再次访问,由于所有资源都直接来自于Service Worker缓存,页面可以在1秒左右就达到可交互的状态了。
 
图3 Lighthouse跑分结果
 
但是,故事并不是这么简单得就结束了。在实际体验中我们发现,应用在页与页的切换时,仍然存在着非常明显的白屏空隙,见图4所示。由于PWA是全屏运行,白屏对用户体验所带来的负面影响甚至比以往在浏览器内更大。我们不是已经用Service Worker缓存了所有资源了吗,怎么还会这样呢?

图4 从首页点击到发现页,跳转过程中的白屏
 
多页应用的陷阱:重启开销

与SPA不同,在多页应用中,路由的切换是原生的浏览器文档跳转(Navigating across documents),这意味着之前的页面会被完全丢弃而浏览器需要为下一个路由的页面重新执行所有的启动步骤:重新下载资源、重新解析HTML、重新运行JavaScript、重新解码图片、重新布局页面、重新绘制……即使其中的很多步骤本是可以在多个路由之间复用的。这些工作无疑将产生巨大的计算开销,也因此需要付出相当多的时间成本。

图5中为我们的入口页(同时也是最重要的页面)在两倍CPU节流模拟下的Profile数据。即使我们可以将“可交互时间”控制在 1 秒左右,我们的用户仍然会觉得这对于“仅仅切换个标签”来说实在是太慢了。

图5 入口页在两倍CPU节流模拟下的Profile数据

巨大的JavaScript重启开销

根据Profile,我们发现在首次渲染(First Paint)发生之前,大量的时间(900ms)都消耗在了JavaScript的运行上(Evaluate Script)。几乎所有脚本都是阻塞的(Parser-blocking),不过因为所有的UI都是由JavaScript/Vue.js驱动的,倒也不会有性能影响。这900ms中,约一半是消耗在Vue.js运行时、组件、库等依赖的运行上,而另一半则花在了业务组件实例化时Vue.js的启动与渲染上。从软件工程角度来说,我们需要这些抽象,所以这里并不是想责怪JavaScript或是Vue.js所带来的开销。

但是,在SPA中, JavaScript的启动成本是均摊到整个生命周期的:每个脚本都只需要被解析与编译一次,诸如生成Virtual DOM等较重的任务可以只执行一次,像Vue.js的ViewModel或是Virtual DOM这样的大对象也可以被留在内存里复用。可惜在多页应用里就不是这样了,我们每次切换页面都为JavaScript付出了巨大的重启代价。

浏览器的缓存啊,能不能帮帮忙?

能,也不能。

V8提供了代码缓存(code caching),可以将编译后的机器码在本地拷贝一份,这样我们就可以在下次请求同一个脚本时一次省略掉请求、解析、编译的所有工作。而且,对于缓存在Service Worker配套的Cache Storage中的脚本,会在第一次执行后就触发V8的代码缓存,这对于我们的多页切换能提供不少帮助。

另外一个你或许听过的浏览器缓存叫做“进退缓存”, Back-Forward Cache,简称bfcache。浏览器厂商对其的命名各异, Opera称之为Fast History Navigation, Webkit称其为Page Cache。但是思路都一样,就是我们可以让浏览器在跳转时把前一页留存在内存中,保留JavaScript与DOM的状态,而不是全都销毁掉。你可以随便找个传统的多页网站在iOS Safari上试试,无论是通过浏览器的前进后退按钮、手势,还是通过超链接(会有一些不同),基本都可以看到瞬间加载的效果。

Bfcache其实非常适合多页应用。但不幸的是,Chrome由于内存开销与其多进程架构等原因目前并不支持。 Chrome现阶段仅仅只是用了传统的HTTP磁盘缓存,来稍稍简化了一下加载过程而已。对于Chromium内核霸占的Android生态来说,我们没法指望了。

为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
 
为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
图6 在添加骨架屏后,从发现页点回首页的效果

这效果本该很轻松的就能实现,不过实际上我们还费了点功夫。

在构建时使用 Vue 预渲染骨架屏

你可能已经想到了,为了让骨架屏可以被Service Worker缓存,瞬间加载并独立于JavaScript渲染,我们需要把组成骨架屏的HTML标签、 CSS样式与图片资源一并内联至各个路由的静态*.html文件中。

不过,我们并不准备手动编写这些骨架屏。你想啊,如果每次真实组件有迭代(每一个路由对我们来说都是一个Vue.js组件),我们都需要手动去同步每一个变化到骨架屏的话,那实在是太繁琐且难以维护了。好在,骨架屏不过是当数据还未加载进来前,页面的一个空白版本而已。如果我们能将骨架屏实现为真实组件的一个特殊状态——“空状态”的话,从理论上就可以从真实组件中直接渲染出骨架屏来。

而Vue.js的多才多艺就在这时体现出来了,我们真的可以用Vue.js 的服务端渲染模块来实现这个想法,不过不是用在真正的服务器上,而是在构建时用它把组件的空状态预先渲染成字符串并注入到HTML模板中。你需要调整Vue.js组件代码使得它可以在Node上执行,有些页面对DOM/BOM的依赖一时无法轻易去除得,我们目前只好额外编写一个*.shell.vue来暂时绕过这个问题。

关于浏览器的绘制(Painting)

HTML文件中有标签并不意味着这些标签就能立刻被绘制到屏幕上,你必须保证页面的关键渲染路径是为此优化的。很多开发者相信将Script标签放在body的底部就足以保证内容能在脚本执行之前被绘制,这对于能渲染不完整DOM树的浏览器(比如桌面浏览器常见的流式渲染)来说可能是成立的。但移动端的浏览器很可能因为考虑到较慢的硬件、电量消耗等因素并不这么做。不仅如此,即使你曾被告知设为async或defer的脚本就不会阻塞HTML解析了,但这可不意味着浏览 器就一定会在执行它们之前进行渲染。

首先我想澄清的是,根据 HTML 规范 Scripting 章节, async脚本是在其请求完成后立刻运行的,因此它本来就可能阻塞到解析。只有defer(且非内联)与最新的type=module被指定为“一定不会阻塞解析”(不过defer目前也有点小问题……我们稍后会再提到),见图7所示。
图7 具有不同属性的Script脚本对HTML解析的阻塞情况

而更重要的是,一个不阻塞HTML解析的脚本仍然可能阻塞到绘制。我做了一个简化的“最小多页PWA”(Minimal Multi-page PWA,或MMPWA)来测试这个问题:我们在一个async(且确实不阻塞HTML解析)脚本中,生成并渲染1000个列表项,然后测试骨架屏能否在脚本执行之前渲染出来。图8是通过USB Debugging在我的Nexus 5真机上录制的Profile。


图8 通过USB Debugging在Nexus 5真机上录制的Profile

是的,出乎意料吗?首次渲染确实被阻塞到脚本执行结束后才发生。究其原因,如果我们在浏览器还未完成上一次绘制工作之前就过快得进行了DOM操作,我们亲爱的浏览器就只好抛弃所有它已经完成的像素,且一直要等待到DOM操作引起的所有工作结束之后才能重新进行下一次渲染。而这种情况更容易在拥有较慢CPU/GPU的移动设备上出现。

黑魔法:利用setTimeout()让绘制提前

不难发现,骨架屏的绘制与脚本执行实际是一个竞态。大概是Vue.js太快了,我们的骨架屏还是有非常大的概率绘制不出来。于是我们想着如何能让脚本执行慢点,或者说,“懒”点。于是我们想到了一个经典的Hack: setTimeout(callback, 0)。我们试着把MMPWA中的DOM操作(渲染1000个列表)放进setTimeout(callback, 0)里……

当当!首次渲染瞬间就被提前了,见图9所示。如果你熟悉浏览器的事件循环模型(Event Loop)的话,这招Hack其实是通过setTimeout的回调把DOM操作放到了事件循环的任务队列中以避免它在当前循环执行,这样浏览器就得以在主线程空闲时喘息一下(更新一下渲染)了。如果你想亲手试试 MMPWA的话,你可以访问github.com/Huxpro/mmpwa 或huangxuan.me/mmpwa/ ,查看代码与Demo。我把UI设计成了A/B Test的形式并改为渲染5000个列表项来让效果更夸张一些。

图9 利用Hack技术,提前完成骨架屏的绘制

回到饿了么PWA上,我们同样试着把new Vue()放到了setTimeout中。果然,黑魔法再次显灵,骨架屏在每次跳转后都能立刻被渲染。这时的Profile看起来是这样的,见图10所示。

 
图10 为感知体验进行各种优化后的最终Profile

现在,我们在400ms时触发首次渲染(骨架屏),在600ms时完成真实UI的渲染并达到页面的可交互。你可以详细对比下图9和图10所示的优化前后Profile的区别。

被我“defer”的有关defer的Bug

不知道你发现没有,在图10的Profile中,我们仍然有不少脚本是阻塞了HTML解析的。好吧,让我解释一下,由于历史原因,我们确实保留了一部分的阻塞脚本,比如侵入性很强的lib-flexible,我们没法轻易去除它。不过, Profile里的大部分阻塞脚本实际上都设置了defer,我们本以为他们应该在HTML解析完成之后才被执行,结果被Profile打了一脸。

我和Jake Archibald 聊了一下,果然这是Chrome的Bug: defer的脚本被完全缓存时,并没有遵守规范等待解析结束,反而阻塞了解析与渲染。Jake已经提交在crbug上了,一起给它投票吧。

最后,图11是优化后的Lighthouse跑分结果,同样可以看到明显的性能提升。需要说明的是,能影响Lighthouse跑分的因素有很多,所以我建议你以控制变量(跑分用的设备、跑分时的网络环境等)的方式来进行对照实验。

图11 优化后的Lighthouse跑分结果

最后为大家展示下应用的架构示意图,见图12所示。

图12 应用架构示意图

一些感想

多页应用仍然有很长的路要走

Web是一个极其多样化的平台。从静态的博客,到电商网站,再到桌面级的生产力软件,它们全都是Web这个大家庭的第一公民。而我们组织Web应用的方式,也同样只会更多而不会更少:多页、单页、 Universal JavaScript应用、 WebGL,以及可以预见的Web Assembly。不同的技术之间没有贵贱,但是适用场景的差距确是客观存在的。

Jake 曾在 Chrome Dev Summit 2016 上说过“PWA!== SPA”。可是尽管我们已经用上了一系列最新的技术(PRPL、 Service Worker、 App Shell……),我们仍然因为多页应用模型本身的缺陷有着难以逾越的一些障碍。多页应用在未来可能会有“bfcache API”、 Navigation Transition等新的规范以缩小跟SPA的距离,不过我们也必须承认,时至今日,多页应用的局限性也是非常明显的。

而PWA终将带领Web应用进入新的时代

即使我们的多页应用在升级PWA的路上不如单页应用来得那么闪亮,但是PWA背后的想法与技术却实实在在地帮助我们在Web平台上提供了更好的用户体验。

PWA作为下一代 Web 应用模型,其尝试解决的是Web平台本身的根本性问题:对网络与浏览器UI的硬依赖。因此,任何Web应用都可以从中获益,这与你是多页还是单页、面向桌面还是移动端、是用React还是Vue.js无关。或许,它还终将改变用户对移动Web的期待。现如今,谁还觉得桌面端的Web只是个看文档的地方呢?

还是那句老话,让我们的用户,也像我们这般热爱Web吧。

最后,感谢饿了么的王亦斯、任光辉、题叶,Google 的 Michael Yeung、 DevRel 团队, UC浏览器团队,腾讯X5浏览器团队在这次项目中的合作。感谢尤雨溪、陈蒙迪和Jake Archibald 在写作过程中给予我的帮助。
 
原文阅读:https://zhuanlan.zhihu.com/p/27836133

《架构即未来》中最常用的15个架构原则总结

架构思想zkbhj 发表了文章 • 0 个评论 • 258 次浏览 • 2017-12-25 12:05 • 来自相关话题

《架构即未来》这本书的第12章简单阐述了架构设计的一些常用的原则。这些原则中很多都是在架构一开始的设计中就要考虑进去的,这样在出现任何问题时,我们都能够及时的处理,和把问题影响的范围有效的缩小。否则就像我现在的项目,一开始设计时,考虑的很少,出问题时,没有做到及时的反馈,和缩小影响范围,只能在事故的代价中将所需要的原则添加进来,慢慢完善。

1.N+1设计

要确保任何你所开发的系统在发生故障时,至少有一个冗余的实例。


一个实例确实很危险,当这个实例出现不明原因的问题不能对外服务,需要debug的时候,如果优先debug,那当前实例就要暂停服务直到你找到问题为止。如果你直接重启实例恢复服务,就没有事故现场进行debug了。而这时如果有一个冗余的实例,就可以先让冗余的实例对外服务,事故现场的环境也得以保留。

多个实例来做负载均衡也是一种不错的选择。

2.回滚设计

确保系统可以回滚到以前发布过的任何版本。


以前做游戏的时候经常遇到回滚,有时候是数据库回滚,有时候是服务器端回滚,一般都是回滚到上个版本。

3.禁用设计

能够关闭任何发布的功能。


当一个功能出现严重问题不得不关闭时,如果关闭整个系统代价就有点大了,所有要有单个功能的开关。像商城系统的支付功能就一定要有开关,如果出现比较严重的bug,可以关闭支付而不影响下单。

4.监控设计

在设计阶段就必须要考虑监控,而不是在实施完成之后补充。


如果监控做的好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。

5.设计多活数据中心

不要被一个数据中心的解决方案把自己限制住。


有钱就多建一个,让股东放心。

6.只用成熟的技术

只用确实好用的技术。


不管用什么技术,都要确保是一个成熟的技术。也许某个新技术有众多优点,比如,降低开发成本,提高开发效率,提高可扩展能力,减少终端用户的响应时间。但是,只要这项技术故障率比较高,就绝不能使用。

7.异步设计

只有在绝对必要的时候才进行同步调用。


异步适合并发。

8.无状态系统

只有当业务确实需要的时候,才使用状态。


无状态的系统更利于扩展,更利于做负载均衡。

9.水平扩展非垂直升级

永远不要依赖更大、更快的系统。


微服务是水平扩展的一个例子,不要把所有的功能都集中在一个系统里面。必要的时候把需求分为多个系统,而不是升级原有的系统。

10.设计至少有两个步骤的前瞻性


在扩展性问题发生前考虑好下一步的行动计划。


想的更远一点,就能减少重构的次数。

11.非核心则购买

如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。


云服务这种的就购买好了。

12.使用商品化硬件

在大多数情况下,便宜的是最好的。


硬件这块儿,满足需求即可,在必要的时候增加配置。

13.小构建,小发布,快试错

全部研发要小构建,不断迭代,让系统不断地成长。


小版本的失败率较低,因为失败率与解决方案中的变更数量直接相关。

14.隔离故障

实现隔离故障设计,通过断路保护避免故障传播和交叉影响。


避免多系统之间的互相影响,这个很重要。

15.自动化

设计和构建自动化的过程。如果机器可以做,就不要依赖于人。


人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。
 
原文阅读:https://www.cnblogs.com/cxiaojia/p/6294493.html 查看全部
《架构即未来》这本书的第12章简单阐述了架构设计的一些常用的原则。这些原则中很多都是在架构一开始的设计中就要考虑进去的,这样在出现任何问题时,我们都能够及时的处理,和把问题影响的范围有效的缩小。否则就像我现在的项目,一开始设计时,考虑的很少,出问题时,没有做到及时的反馈,和缩小影响范围,只能在事故的代价中将所需要的原则添加进来,慢慢完善。

1.N+1设计


要确保任何你所开发的系统在发生故障时,至少有一个冗余的实例。



一个实例确实很危险,当这个实例出现不明原因的问题不能对外服务,需要debug的时候,如果优先debug,那当前实例就要暂停服务直到你找到问题为止。如果你直接重启实例恢复服务,就没有事故现场进行debug了。而这时如果有一个冗余的实例,就可以先让冗余的实例对外服务,事故现场的环境也得以保留。

多个实例来做负载均衡也是一种不错的选择。

2.回滚设计


确保系统可以回滚到以前发布过的任何版本。



以前做游戏的时候经常遇到回滚,有时候是数据库回滚,有时候是服务器端回滚,一般都是回滚到上个版本。

3.禁用设计


能够关闭任何发布的功能。



当一个功能出现严重问题不得不关闭时,如果关闭整个系统代价就有点大了,所有要有单个功能的开关。像商城系统的支付功能就一定要有开关,如果出现比较严重的bug,可以关闭支付而不影响下单。

4.监控设计


在设计阶段就必须要考虑监控,而不是在实施完成之后补充。



如果监控做的好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。

5.设计多活数据中心


不要被一个数据中心的解决方案把自己限制住。



有钱就多建一个,让股东放心。

6.只用成熟的技术


只用确实好用的技术。



不管用什么技术,都要确保是一个成熟的技术。也许某个新技术有众多优点,比如,降低开发成本,提高开发效率,提高可扩展能力,减少终端用户的响应时间。但是,只要这项技术故障率比较高,就绝不能使用。

7.异步设计


只有在绝对必要的时候才进行同步调用。



异步适合并发。

8.无状态系统


只有当业务确实需要的时候,才使用状态。



无状态的系统更利于扩展,更利于做负载均衡。

9.水平扩展非垂直升级


永远不要依赖更大、更快的系统。



微服务是水平扩展的一个例子,不要把所有的功能都集中在一个系统里面。必要的时候把需求分为多个系统,而不是升级原有的系统。

10.设计至少有两个步骤的前瞻性



在扩展性问题发生前考虑好下一步的行动计划。



想的更远一点,就能减少重构的次数。

11.非核心则购买


如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。



云服务这种的就购买好了。

12.使用商品化硬件


在大多数情况下,便宜的是最好的。



硬件这块儿,满足需求即可,在必要的时候增加配置。

13.小构建,小发布,快试错


全部研发要小构建,不断迭代,让系统不断地成长。



小版本的失败率较低,因为失败率与解决方案中的变更数量直接相关。

14.隔离故障


实现隔离故障设计,通过断路保护避免故障传播和交叉影响。



避免多系统之间的互相影响,这个很重要。

15.自动化


设计和构建自动化的过程。如果机器可以做,就不要依赖于人。



人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。
 
原文阅读:https://www.cnblogs.com/cxiaojia/p/6294493.html

分布式系统理论基础 - 一致性、2PC和3PC

专业名词zkbhj 发表了文章 • 0 个评论 • 204 次浏览 • 2017-12-23 21:10 • 来自相关话题

狭义的分布式系统指由网络连接的计算机系统,每个节点独立地承担计算或存储任务,节点间通过网络协同工作。广义的分布式系统是一个相对的概念,正如Leslie Lamport所说:

What is a distributed systeme. Distribution is in the eye of the beholder.
To the user sitting at the keyboard, his IBM personal computer is a nondistributed system. 
To a flea crawling around on the circuit board, or to the engineer who designed it, it's very much a distributed system.
 

 
一致性是分布式理论中的根本性问题,近半个世纪以来,科学家们围绕着一致性问题提出了很多理论模型,依据这些理论模型,业界也出现了很多工程实践投影。下面我们从一致性问题、特定条件下解决一致性问题的两种方法(2PC、3PC)入门,了解最基础的分布式系统理论。


一致性(consensus)

何为一致性问题?简单而言,一致性问题就是相互独立的节点之间如何达成一项决议的问题。分布式系统中,进行数据库事务提交(commit transaction)、Leader选举、序列号生成等都会遇到一致性问题。这个问题在我们的日常生活中也很常见,比如牌友怎么商定几点在哪打几圈麻将。
 
假设一个具有N个节点的分布式系统,当其满足以下条件时,我们说这个系统满足一致性: 
全认同(agreement): 所有N个节点都认同一个结果值合法(validity): 该结果必须由N个节点中的节点提出可结束(termination): 决议过程在一定时间内结束,不会无休止地进行下去


有人可能会说,决定什么时候在哪搓搓麻将,4个人商量一下就ok,这不很简单吗?

但就这样看似简单的事情,分布式系统实现起来并不轻松,因为它面临着这些问题: 
消息传递异步无序(asynchronous): 现实网络不是一个可靠的信道,存在消息延时、丢失,节点间消息传递做不到同步有序(synchronous)节点宕机(fail-stop): 节点持续宕机,不会恢复节点宕机恢复(fail-recover): 节点宕机一段时间后恢复,在分布式系统中最常见网络分化(network partition): 网络链路出现问题,将N个节点隔离成多个部分拜占庭将军问题(byzantine failure)[2]: 节点或宕机或逻辑失败,甚至不按套路出牌抛出干扰决议的信息
 
我: 老王,今晚7点老地方,搓够48圈不见不散!
……
(第二天凌晨3点) 隔壁老王: 没问题! // 消息延迟
我: ……
----------------------------------------------
我: 小张,今晚7点老地方,搓够48圈不见不散!
小张: No ……
(两小时后……)
小张: No problem! // 宕机节点恢复
我: ……
-----------------------------------------------
我: 老李头,今晚7点老地方,搓够48圈不见不散!
老李: 必须的,guang'chang'wu走起! // 拜占庭将军
(这是要打麻将呢?还是要广场舞?还是一边打麻将一边广场舞……)还能不能一起愉快地玩耍...


 
我们把以上所列的问题称为系统模型(system model),讨论分布式系统理论和工程实践的时候,必先划定模型。例如有以下两种模型: 
异步环境(asynchronous)下,节点宕机(fail-stop)异步环境(asynchronous)下,节点宕机恢复(fail-recover)、网络分化(network partition)

2比1多了节点恢复、网络分化的考量,因而对这两种模型的理论研究和工程解决方案必定是不同的,在还没有明晰所要解决的问题前谈解决方案都是一本正经地耍流氓(这句我喜欢)。

一致性还具备两个属性,一个是强一致(safety),它要求所有节点状态一致、共进退;一个是可用(liveness),它要求分布式系统24*7无间断对外服务。FLP定理(FLP impossibility)已经证明在一个收窄的模型中(异步环境并只存在节点宕机),不能同时满足 safety 和 liveness。

FLP定理是分布式系统理论中的基础理论,正如物理学中的能量守恒定律彻底否定了永动机的存在,FLP定理否定了同时满足safety 和 liveness 的一致性协议的存在。 
工程实践上根据具体的业务场景,或保证强一致(safety),或在节点宕机、网络分化的时候保证可用(liveness)。2PC、3PC是相对简单的解决一致性问题的协议,下面我们就来了解2PC和3PC。
 
2PC

2PC(tow phase commit)两阶段提交,顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):





 
2PC, phase one

在阶段1中,coordinator发起一个提议,分别问询各participant是否接受。





 
2PC, phase two

在阶段2中,coordinator根据participant的反馈,提交或中止事务,如果participant全部同意则提交,只要有一个participant不同意就中止。

在异步环境(asynchronous)并且没有节点宕机(fail-stop)的模型下,2PC可以满足全认同、值合法、可结束,是解决一致性问题的一种协议。但如果再加上节点宕机(fail-recover)的考虑,2PC是否还能解决一致性问题呢?

coordinator如果在发起提议后宕机,那么participant将进入阻塞(block)状态、一直等待coordinator回应以完成该次决议。这时需要另一角色把系统从不可结束的状态中带出来,我们把新增的这一角色叫协调者备份(coordinator watchdog)。coordinator宕机一定时间后,watchdog接替原coordinator工作,通过问询(query) 各participant的状态,决定阶段2是提交还是中止。这也要求 coordinator/participant 记录(logging)历史状态,以备coordinator宕机后watchdog对participant查询、coordinator宕机恢复后重新找回状态。

从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少。

 
3PC

3PC(three phase commit)即三阶段提交,既然2PC可以在异步网络+节点宕机恢复的模型下实现一致性,那还需要3PC做什么,3PC是什么鬼?

 在2PC中一个participant的状态只有它自己和coordinator知晓,假如coordinator提议后自身宕机,在watchdog启用前一个participant又宕机,其他participant就会进入既不能回滚、又不能强制commit的阻塞状态,直到participant宕机恢复。这引出两个疑问:

能不能去掉阻塞,使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态
当次决议中,participant间能不能相互知道对方的状态,又或者participant间根本不依赖对方的状态

相比2PC,3PC增加了一个准备提交(prepare to commit)阶段来解决以上问题:




图片截取自wikipedia

coordinator接收完participant的反馈(vote)之后,进入阶段2,给各个participant发送准备提交(prepare to commit)指令。participant接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort,3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。

participant如果在不同阶段宕机,我们来看看3PC如何应对:

阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务
阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务

因为有了准备提交(prepare to commit)阶段,3PC的事务处理延时也增加了1个RTT,变为3个RTT(propose+precommit+commit),但是它防止participant宕机后整个系统进入阻塞态,增强了系统的可用性,对一些现实业务场景是非常值得的。


小结

以上介绍了分布式系统理论中的部分基础知识,阐述了一致性(consensus)的定义和实现一致性所要面临的问题,最后讨论在异步网络(asynchronous)、节点宕机恢复(fail-recover)模型下2PC、3PC怎么解决一致性问题。
 
原文阅读:https://www.cnblogs.com/bangerlee/p/5268485.html 查看全部
狭义的分布式系统指由网络连接的计算机系统,每个节点独立地承担计算或存储任务,节点间通过网络协同工作。广义的分布式系统是一个相对的概念,正如Leslie Lamport所说:


What is a distributed systeme. Distribution is in the eye of the beholder.
To the user sitting at the keyboard, his IBM personal computer is a nondistributed system. 
To a flea crawling around on the circuit board, or to the engineer who designed it, it's very much a distributed system.
 


 
一致性是分布式理论中的根本性问题,近半个世纪以来,科学家们围绕着一致性问题提出了很多理论模型,依据这些理论模型,业界也出现了很多工程实践投影。下面我们从一致性问题、特定条件下解决一致性问题的两种方法(2PC、3PC)入门,了解最基础的分布式系统理论。


一致性(consensus)

何为一致性问题?简单而言,一致性问题就是相互独立的节点之间如何达成一项决议的问题。分布式系统中,进行数据库事务提交(commit transaction)、Leader选举、序列号生成等都会遇到一致性问题。这个问题在我们的日常生活中也很常见,比如牌友怎么商定几点在哪打几圈麻将。
 
假设一个具有N个节点的分布式系统,当其满足以下条件时,我们说这个系统满足一致性: 
  • 全认同(agreement): 所有N个节点都认同一个结果
  • 值合法(validity): 该结果必须由N个节点中的节点提出
  • 可结束(termination): 决议过程在一定时间内结束,不会无休止地进行下去



有人可能会说,决定什么时候在哪搓搓麻将,4个人商量一下就ok,这不很简单吗?

但就这样看似简单的事情,分布式系统实现起来并不轻松,因为它面临着这些问题: 
  • 消息传递异步无序(asynchronous): 现实网络不是一个可靠的信道,存在消息延时、丢失,节点间消息传递做不到同步有序(synchronous)
  • 节点宕机(fail-stop): 节点持续宕机,不会恢复
  • 节点宕机恢复(fail-recover): 节点宕机一段时间后恢复,在分布式系统中最常见
  • 网络分化(network partition): 网络链路出现问题,将N个节点隔离成多个部分
  • 拜占庭将军问题(byzantine failure)[2]: 节点或宕机或逻辑失败,甚至不按套路出牌抛出干扰决议的信息

 
我: 老王,今晚7点老地方,搓够48圈不见不散!
……
(第二天凌晨3点) 隔壁老王: 没问题! // 消息延迟
我: ……
----------------------------------------------
我: 小张,今晚7点老地方,搓够48圈不见不散!
小张: No ……
(两小时后……)
小张: No problem! // 宕机节点恢复
我: ……
-----------------------------------------------
我: 老李头,今晚7点老地方,搓够48圈不见不散!
老李: 必须的,guang'chang'wu走起! // 拜占庭将军
(这是要打麻将呢?还是要广场舞?还是一边打麻将一边广场舞……)
还能不能一起愉快地玩耍...


 
我们把以上所列的问题称为系统模型(system model),讨论分布式系统理论和工程实践的时候,必先划定模型。例如有以下两种模型: 
  • 异步环境(asynchronous)下,节点宕机(fail-stop)
  • 异步环境(asynchronous)下,节点宕机恢复(fail-recover)、网络分化(network partition)


2比1多了节点恢复、网络分化的考量,因而对这两种模型的理论研究和工程解决方案必定是不同的,在还没有明晰所要解决的问题前谈解决方案都是一本正经地耍流氓(这句我喜欢)

一致性还具备两个属性,一个是强一致(safety),它要求所有节点状态一致、共进退;一个是可用(liveness),它要求分布式系统24*7无间断对外服务。FLP定理(FLP impossibility)已经证明在一个收窄的模型中(异步环境并只存在节点宕机),不能同时满足 safety 和 liveness。

FLP定理是分布式系统理论中的基础理论,正如物理学中的能量守恒定律彻底否定了永动机的存在,FLP定理否定了同时满足safety 和 liveness 的一致性协议的存在。 
工程实践上根据具体的业务场景,或保证强一致(safety),或在节点宕机、网络分化的时候保证可用(liveness)。2PC、3PC是相对简单的解决一致性问题的协议,下面我们就来了解2PC和3PC。
 
2PC

2PC(tow phase commit)两阶段提交,顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):

116770-20160313202532507-1396598167.png

 
2PC, phase one

在阶段1中,coordinator发起一个提议,分别问询各participant是否接受。

116770-20160313203429600-179395429.png

 
2PC, phase two

在阶段2中,coordinator根据participant的反馈,提交或中止事务,如果participant全部同意则提交,只要有一个participant不同意就中止。

在异步环境(asynchronous)并且没有节点宕机(fail-stop)的模型下,2PC可以满足全认同、值合法、可结束,是解决一致性问题的一种协议。但如果再加上节点宕机(fail-recover)的考虑,2PC是否还能解决一致性问题呢?

coordinator如果在发起提议后宕机,那么participant将进入阻塞(block)状态、一直等待coordinator回应以完成该次决议。这时需要另一角色把系统从不可结束的状态中带出来,我们把新增的这一角色叫协调者备份(coordinator watchdog)。coordinator宕机一定时间后,watchdog接替原coordinator工作,通过问询(query) 各participant的状态,决定阶段2是提交还是中止。这也要求 coordinator/participant 记录(logging)历史状态,以备coordinator宕机后watchdog对participant查询、coordinator宕机恢复后重新找回状态。

从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少。

 
3PC

3PC(three phase commit)即三阶段提交,既然2PC可以在异步网络+节点宕机恢复的模型下实现一致性,那还需要3PC做什么,3PC是什么鬼?

 在2PC中一个participant的状态只有它自己和coordinator知晓,假如coordinator提议后自身宕机,在watchdog启用前一个participant又宕机,其他participant就会进入既不能回滚、又不能强制commit的阻塞状态,直到participant宕机恢复。这引出两个疑问:

能不能去掉阻塞,使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态
当次决议中,participant间能不能相互知道对方的状态,又或者participant间根本不依赖对方的状态

相比2PC,3PC增加了一个准备提交(prepare to commit)阶段来解决以上问题:
116770-20160314002734304-489496391.png

图片截取自wikipedia

coordinator接收完participant的反馈(vote)之后,进入阶段2,给各个participant发送准备提交(prepare to commit)指令。participant接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort,3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。

participant如果在不同阶段宕机,我们来看看3PC如何应对:

阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务
阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务

因为有了准备提交(prepare to commit)阶段,3PC的事务处理延时也增加了1个RTT,变为3个RTT(propose+precommit+commit),但是它防止participant宕机后整个系统进入阻塞态,增强了系统的可用性,对一些现实业务场景是非常值得的。


小结

以上介绍了分布式系统理论中的部分基础知识,阐述了一致性(consensus)的定义和实现一致性所要面临的问题,最后讨论在异步网络(asynchronous)、节点宕机恢复(fail-recover)模型下2PC、3PC怎么解决一致性问题。
 
原文阅读:https://www.cnblogs.com/bangerlee/p/5268485.html

微服务架构下的数据一致性保证

架构思想zkbhj 发表了文章 • 0 个评论 • 254 次浏览 • 2017-12-23 19:49 • 来自相关话题

本次学习的主要内容包括:

1.传统使用本地事务和分布式事务保证一致性。

2.传统分布式事务不是微服务中一致性的最佳选择。

3.微服务架构中应满足数据最终一致性原则。

4.微服务架构实现最终一致性的三种模式。

5.对账是最后的终极防线。
 
一、传统使用本地事务和分布式事务保证一致性





 
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。





 
二、传统分布式事务不是微服务中一致性的最佳选择
 
首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如,Neo4j;

基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。

可见在微服务架构中已经不能选择分布式事务了。


三、微服务架构中应满足数据最终一致性原则 
依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性。
 




当然选择了最终一致性,就要保证到最终的这段时间要在用户可接受的范围之内。

那么我们怎么实现最终一致性呢?

四、微服务架构实现最终一致性的三种模式 
从一致性的本质来看,是要保证在一个业务逻辑中包含的服务要么都成功,要么都失败。那我们怎么选择方向呢?保证成功还是保证失败呢?

我们说业务模式决定了我们的选择。实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式。


1) 可靠事件模式


可靠事件模式属于事件驱动架构,当某件重要事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此事件时,就可以完成自己的业务,也可能会引发更多的事件发布。

1. 如订单服务创建一个待支付的订单,发布一个“创建订单”的事件。





 
2.支付服务消费“创建订单”事件,支付完成后发布一个“支付完成”事件。





 
3.订单服务消费“支付完成”事件,订单状态更新为待出库。





 
从而就实现了完成的业务流程。

这个过程可能导致出现不一致的地方在于:某个微服务在更新了业务实体后发布事件却失败;虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;接受事件的微服务重复消费了事件。

可靠事件模式在于保证可靠事件投递和避免重复消费,可靠事件投递定义为(a)每个服务原子性的业务操作和发布事件(b)消息代理确保事件传递至少一次。

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。


2) 补偿模式

为了描述方便,这里先定义两个概念:

业务异常:业务逻辑产生错误的情况,比如账户余额不足、商品库存不足等。

技术异常:非业务逻辑产生的异常,如网络连接异常、网络超时等。


补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是,(1)上海-北京6月19日9点的某某航班,(2)某某酒店住宿3晚,(3)北京-上海6月22日17点火车。在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。





 
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。





 
需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性。

我们应该尽可能通过设计避免采用补偿方式,比如上面的例子中,在预订火车票失败的时候可以提示客户更改其他的时间。


 3) TCC模式(Try-Confirm-Cancel)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
 
 





1) Try:完成所有业务检查

预留必须业务资源


2) Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性


3) Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

整个TCC业务分成两个阶段完成。

 
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。

第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。


五、对账是最后的终极防线

如果有些业务由于瞬时的网络故障或调用超时等问题,通过上文所讲的3种模式一般都能得到很好的解决。但是在当今云计算环境下,很多服务是依赖于外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程。
 
出处:EAII企业架构创新研究院
原文阅读:https://mp.weixin.qq.com/s%3F_ ... 29e86
  查看全部
本次学习的主要内容包括:

1.传统使用本地事务和分布式事务保证一致性。

2.传统分布式事务不是微服务中一致性的最佳选择。

3.微服务架构中应满足数据最终一致性原则。

4.微服务架构实现最终一致性的三种模式。

5.对账是最后的终极防线。
 
一、传统使用本地事务和分布式事务保证一致性

WX20171223-182332@2x.png

 
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。

WX20171223-193729@2x.png

 
二、传统分布式事务不是微服务中一致性的最佳选择
 
首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如,Neo4j;

基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。

可见在微服务架构中已经不能选择分布式事务了。


三、微服务架构中应满足数据最终一致性原则 
依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性。
 
WX20171223-194009@2x.png

当然选择了最终一致性,就要保证到最终的这段时间要在用户可接受的范围之内。

那么我们怎么实现最终一致性呢?

四、微服务架构实现最终一致性的三种模式 
从一致性的本质来看,是要保证在一个业务逻辑中包含的服务要么都成功,要么都失败。那我们怎么选择方向呢?保证成功还是保证失败呢?

我们说业务模式决定了我们的选择。实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式


1) 可靠事件模式


可靠事件模式属于事件驱动架构,当某件重要事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此事件时,就可以完成自己的业务,也可能会引发更多的事件发布。

1. 如订单服务创建一个待支付的订单,发布一个“创建订单”的事件。

WX20171223-194142@2x.png

 
2.支付服务消费“创建订单”事件,支付完成后发布一个“支付完成”事件。

WX20171223-194232@2x.png

 
3.订单服务消费“支付完成”事件,订单状态更新为待出库。

WX20171223-194244@2x.png

 
从而就实现了完成的业务流程。

这个过程可能导致出现不一致的地方在于:某个微服务在更新了业务实体后发布事件却失败;虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;接受事件的微服务重复消费了事件。

可靠事件模式在于保证可靠事件投递和避免重复消费,可靠事件投递定义为(a)每个服务原子性的业务操作和发布事件(b)消息代理确保事件传递至少一次。

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。


2) 补偿模式

为了描述方便,这里先定义两个概念:


业务异常:业务逻辑产生错误的情况,比如账户余额不足、商品库存不足等。

技术异常:非业务逻辑产生的异常,如网络连接异常、网络超时等。



补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是,(1)上海-北京6月19日9点的某某航班,(2)某某酒店住宿3晚,(3)北京-上海6月22日17点火车。在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。

WX20171223-194440@2x.png

 
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。

WX20171223-194454@2x.png

 
需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性。

我们应该尽可能通过设计避免采用补偿方式,比如上面的例子中,在预订火车票失败的时候可以提示客户更改其他的时间。


 3) TCC模式(Try-Confirm-Cancel)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
 
 
WX20171223-194636@2x.png


1) Try:完成所有业务检查

预留必须业务资源


2) Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性


3) Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

整个TCC业务分成两个阶段完成。


 
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。

第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。


五、对账是最后的终极防线

如果有些业务由于瞬时的网络故障或调用超时等问题,通过上文所讲的3种模式一般都能得到很好的解决。但是在当今云计算环境下,很多服务是依赖于外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程。
 
出处:EAII企业架构创新研究院
原文阅读:https://mp.weixin.qq.com/s%3F_ ... 29e86
 

简单理解一致性哈希算法(consistent hashing)

架构思想zkbhj 发表了文章 • 0 个评论 • 291 次浏览 • 2017-11-04 20:14 • 来自相关话题

一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
 
 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
 
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图





 
把数据通过一定的hash算法处理后映射到环上
 
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;





 

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;





 

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
 
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:





 

2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:






    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
 
平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。

以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:





 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:





 
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
 
原文链接:http://blog.csdn.net/cywosp/ar ... 97179 查看全部
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
 
 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
 
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

20140411000507734.png

 
把数据通过一定的hash算法处理后映射到环上
 
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;

20140411000620656.png

 

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;

20140411000853609.png

 

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
 
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:

20140411001033656.png

 

2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:

20140411001211062.png


    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
 
平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。


    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。


以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:

20140411001433375.png

 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:

20140411001540656.png

 
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
 
原文链接:http://blog.csdn.net/cywosp/ar ... 97179

服务保障经验谈之服务熔断

架构思想zkbhj 发表了文章 • 0 个评论 • 118 次浏览 • 2018-12-18 10:56 • 来自相关话题

什么是服务熔断?

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

这种牺牲局部,保全整体的措施就叫做熔断。

如果不采取熔断措施,我们的系统会怎样呢?我们来看一个栗子。

当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:





 
一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务A也变为不可用,整个调用链路被拖垮。




像这种调用链路的连锁故障,叫做雪崩。

正所谓刮骨疗毒,壮士断腕。在这种时候,就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程和刚才所讲的考试策略如出一辙:





 
这里需要解释两点:

1.开启熔断

在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。


2.熔断回复

熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
 
更多服务降级熔断限流,参考:https://www.cnblogs.com/raosha ... .html 查看全部
什么是服务熔断?

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

这种牺牲局部,保全整体的措施就叫做熔断。

如果不采取熔断措施,我们的系统会怎样呢?我们来看一个栗子。

当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:

20180507103456708.png

 
一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务A也变为不可用,整个调用链路被拖垮。
20180507103502138.png

像这种调用链路的连锁故障,叫做雪崩

正所谓刮骨疗毒,壮士断腕。在这种时候,就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程和刚才所讲的考试策略如出一辙:

20180507103507143.png

 
这里需要解释两点:

1.开启熔断

在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。


2.熔断回复

熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
 
更多服务降级熔断限流,参考:https://www.cnblogs.com/raosha ... .html

#线下分享笔记#自如的六层次业务与产品设计方法

总结zkbhj 发表了文章 • 0 个评论 • 397 次浏览 • 2018-07-20 10:18 • 来自相关话题

今天,公司给技术团队组织了一场技术培训,培训的课题是《六层次理论与应用方法》。后来熊帅说这是未来要在技术团队的培训上加大投入力度的一个开始,希望真的能如此。其实在年中绩效评定表里,对上级以及公司需要提供的帮助中我就写了:





 
有了这颗定心丸,下半年就只剩下好好努力了!
 
言归正传,听完分享,还是得总结一下,要不然根本吸收不了多少精华。现在步入正题:
 
首先,分享从一个实际例子开始:

如果有个朋友想要戒烟,请你给点建议,你会给出什么建议?

台下的“观众”纷纷给出了“五花八门”的建议:
给他讲吸烟对身体的危害和经济上的开销;给他看恐怖的视频;让他生孩子……(佩服的不行),后来说生完呢?哈哈,我补了句“再生一个”
 
带着这些建议,分享者将思路拉回到正轨上,抛出了以下几个问题:
他是什么样的人,抽烟多久了?他因为什么开始抽烟,因为什么希望戒烟?他一般什么时候抽烟,习惯是什么样的?
 
是的,很明显,我们所有人,都在未对建议对象进行任何了解的前提下,就对他提出各种各样的建议,这样真的有效吗?

不要做着做着爱上了自己的方案并沉浸其中,忘了最初为什么出发

 
我们有时候就是这样:

客户想要的是一条船过河,而我们做出来的是航空母舰

 
所以,我们需要一个高效的思考与沟通工具,也就是今天的重点:

自如的六层次业务与产品设计方法

 1、课程简介
 
作为产品经理,我们经常会遇到下面的疑问和问题:
跨部门合作好难!感觉提的需求不合理!业务方为什么这么强势?那个需求更重要?需求太多!我的价值是什么?为什么每次都这么急?项目时间太紧!为啥要做这个需求?
资源不够出不了成果?
 
而这个工具就是为了解决上面遇到的这些“棘手问题”!学了这个理论方法之后,要达到下面三个目标:
做更有价值的事情建立更高效的沟通做出令人喜爱的产品
 
“六层次模型”其实是“业务方”和“产品经理”之间的一把宝剑,而“产品经理”和“互联网团队”之间的宝剑是“产品架构和应用架构”,这部分会在未来的分享会上具体讲解。

六层架构应该是业务与产品共同拥有的,一张相同的架构图。

 
PPT上有一张自如整个业务体系对应的“六层次”图例,出于保密要求,不能放在文章里分享,大家可以意淫下。我觉得,透过这个六层次架构图,很清晰的就可以看出来自如在做什么!这也是这个六层次模型的魅力和精华所在!
 
2、六层次架构详解与应用
 
找准用户,是挖掘痛点、设计场景的必要条件
 
是谁:租客、业主、经纪人……
客观描述:用户的客观信息,例如人口统计信息等
群体规模:市场规模,覆盖情况
用户分类:按客观信息,按生命周期分类等
 
比如开场的戒烟例子:用户可能就是30~40岁,10年以上烟龄,白领阶层,月收入超3W,关注健康
 
挖掘最核心的痛点/价值
 
痛点就是,设身处地的去想,真的“痛”
价值就是,设身处地的去想,真的“惊喜”“有意义”
让用户最“愤怒”、“苦闷”的问题
 
比如开场的戒烟例子:工作压力大;对自己的健康很焦虑;意志力不足以抵抗“烟瘾”
 
设计/创造符合生活和实际的场景
 
接地气,符合生活与实际
让用户不再“愤怒”、“苦闷”的真实场景
 
比如开场的戒烟例子:烟瘾犯了的时候;可以有相对健康的替代方案;且可以逐步降低“烟瘾”
 
超出用户期待的产品/服务
首先产品和服务接地气
其次,超出期望就会让用户真的认可
 
比如开场的戒烟例子:电子烟;食品药物;在线互联网心理辅导课程
 
运营(组织、流程、绩效、系统)
运营体系是产品/服务落地的基础支撑
组织、流程、绩效和系统本身也是分析问题的方法
 
组织:研发团队、销售团队、营销团队
流程:研发流程、销售流程、服务流程
绩效:职能绩效、销售绩效
系统:官网、微信号、小程序、销售系统、客户系统……
 
合理有效的资源分配与应用
研发、设计、测试、运营都是资源
市场上可以有偿或无偿获取的信息、产品也是资源
资源有效合理的利用,才能将投入产出最大化
 
 
比如开场的戒烟例子:资金、医药学家、制药工厂、销售渠道、系统平台
 
综上,六层次理论其实就是:
 





3、什么场景使用?或者什么时候能够应用六层次架构?
 
统一跨团队沟通方法,保证目标一致解决日常遇到的问题,做一个项目系统的理解一个业务、产品
 
4、实际举例:
 
  分享中使用了自如搬家刚推出的服务——自如小搬产品进行了举例,用六层次理论来理解公司的自如小搬产品。由于涉及内部资料,不在这里陈述示例。
 最后划重点:
 

你的产品,给谁,解决了什么问题或提供了什么价值

 
而且,六层次理论能教你“从未来看现在”,也就是用未来的视角,解决目前遇到的问题!

至于后来熊帅问我“如果我是一个能够到俄罗斯世界杯决赛去看球的人,最希望做的一件事是什么?”的时候,我第一个想到的确实是带回一些世界杯周边,但是我还是想多了,因为之前在参加邓紫棋演唱会的时候,在演唱会内外最不缺的就是周边——什么T恤衫、水杯、折扇巴拉巴拉,中国人最爱搞这种来赚钱,难道去俄罗斯也还是这一套?事实证明,我想多了!(尴尬……)说实话挺佩服熊帅的,看个球也能把身边的事情投射到这个“六层次业务和产品的设计方法”中。不过这也印证了,这个方法理论的广泛适用性以及它的正确性。
熊帅有一句话说的我觉得很对:
 

一个人工作了3年、5年或者10年,当你问他有什么成长的时候,如果他说“自己从一个学生变成了社会人”,那他基本上没有什么成长。一个人的成长,应该体现在他沉淀和吸收了多少理论和方法。这些理论和方法是骨架,能够给你提供思路和指导,就好比一个个书架,至于这个书架上能够放上什么哪些纪念品、哪些书、哪些“有血有肉”的东西,就要看你的思考有多深,行动有多少!

 
期待更加精彩的技术和理论分享。 查看全部
今天,公司给技术团队组织了一场技术培训,培训的课题是《六层次理论与应用方法》。后来熊帅说这是未来要在技术团队的培训上加大投入力度的一个开始,希望真的能如此。其实在年中绩效评定表里,对上级以及公司需要提供的帮助中我就写了:

QQ截图20180720100050.jpg

 
有了这颗定心丸,下半年就只剩下好好努力了!
 
言归正传,听完分享,还是得总结一下,要不然根本吸收不了多少精华。现在步入正题:
 
首先,分享从一个实际例子开始:


如果有个朋友想要戒烟,请你给点建议,你会给出什么建议?


台下的“观众”纷纷给出了“五花八门”的建议:
  1. 给他讲吸烟对身体的危害和经济上的开销;
  2. 给他看恐怖的视频;
  3. 让他生孩子……(佩服的不行),后来说生完呢?哈哈,我补了句“再生一个”

 
带着这些建议,分享者将思路拉回到正轨上,抛出了以下几个问题:
  1. 他是什么样的人,抽烟多久了?
  2. 他因为什么开始抽烟,因为什么希望戒烟?
  3. 他一般什么时候抽烟,习惯是什么样的?

 
是的,很明显,我们所有人,都在未对建议对象进行任何了解的前提下,就对他提出各种各样的建议,这样真的有效吗?


不要做着做着爱上了自己的方案并沉浸其中,忘了最初为什么出发


 
我们有时候就是这样:


客户想要的是一条船过河,而我们做出来的是航空母舰


 
所以,我们需要一个高效的思考与沟通工具,也就是今天的重点:


自如的六层次业务与产品设计方法


 1、课程简介
 
作为产品经理,我们经常会遇到下面的疑问和问题:
  • 跨部门合作好难!
  • 感觉提的需求不合理!
  • 业务方为什么这么强势?
  • 那个需求更重要?
  • 需求太多!
  • 我的价值是什么?
  • 为什么每次都这么急?
  • 项目时间太紧!
  • 为啥要做这个需求?

  • 资源不够出不了成果?

 
而这个工具就是为了解决上面遇到的这些“棘手问题”!学了这个理论方法之后,要达到下面三个目标:
  1. 做更有价值的事情
  2. 建立更高效的沟通
  3. 做出令人喜爱的产品

 
“六层次模型”其实是“业务方”和“产品经理”之间的一把宝剑,而“产品经理”和“互联网团队”之间的宝剑是“产品架构和应用架构”,这部分会在未来的分享会上具体讲解。


六层架构应该是业务与产品共同拥有的,一张相同的架构图。


 
PPT上有一张自如整个业务体系对应的“六层次”图例,出于保密要求,不能放在文章里分享,大家可以意淫下。我觉得,透过这个六层次架构图,很清晰的就可以看出来自如在做什么!这也是这个六层次模型的魅力和精华所在!
 
2、六层次架构详解与应用
 
找准用户,是挖掘痛点、设计场景的必要条件
 
是谁:租客、业主、经纪人……
客观描述:用户的客观信息,例如人口统计信息等
群体规模:市场规模,覆盖情况
用户分类:按客观信息,按生命周期分类等
 
比如开场的戒烟例子:用户可能就是30~40岁,10年以上烟龄,白领阶层,月收入超3W,关注健康
 
挖掘最核心的痛点/价值
 
痛点就是,设身处地的去想,真的“痛”
价值就是,设身处地的去想,真的“惊喜”“有意义”
让用户最“愤怒”、“苦闷”的问题
 
比如开场的戒烟例子:工作压力大;对自己的健康很焦虑;意志力不足以抵抗“烟瘾”
 
设计/创造符合生活和实际的场景
 
接地气,符合生活与实际
让用户不再“愤怒”、“苦闷”的真实场景
 
比如开场的戒烟例子:烟瘾犯了的时候;可以有相对健康的替代方案;且可以逐步降低“烟瘾”
 
超出用户期待的产品/服务
首先产品和服务接地气
其次,超出期望就会让用户真的认可
 
比如开场的戒烟例子:电子烟;食品药物;在线互联网心理辅导课程
 
运营(组织、流程、绩效、系统)
运营体系是产品/服务落地的基础支撑
组织、流程、绩效和系统本身也是分析问题的方法
 
组织:研发团队、销售团队、营销团队
流程:研发流程、销售流程、服务流程
绩效:职能绩效、销售绩效
系统:官网、微信号、小程序、销售系统、客户系统……
 
合理有效的资源分配与应用
研发、设计、测试、运营都是资源
市场上可以有偿或无偿获取的信息、产品也是资源
资源有效合理的利用,才能将投入产出最大化
 
 
比如开场的戒烟例子:资金、医药学家、制药工厂、销售渠道、系统平台
 
综上,六层次理论其实就是:
 
QQ截图20180720103332.jpg


3、什么场景使用?或者什么时候能够应用六层次架构?
 
  • 统一跨团队沟通方法,保证目标一致
  • 解决日常遇到的问题,做一个项目
  • 系统的理解一个业务、产品

 
4、实际举例:
 
  分享中使用了自如搬家刚推出的服务——自如小搬产品进行了举例,用六层次理论来理解公司的自如小搬产品。由于涉及内部资料,不在这里陈述示例。
 最后划重点:
 


你的产品,给谁,解决了什么问题或提供了什么价值


 
而且,六层次理论能教你“从未来看现在”,也就是用未来的视角,解决目前遇到的问题!

至于后来熊帅问我“如果我是一个能够到俄罗斯世界杯决赛去看球的人,最希望做的一件事是什么?”的时候,我第一个想到的确实是带回一些世界杯周边,但是我还是想多了,因为之前在参加邓紫棋演唱会的时候,在演唱会内外最不缺的就是周边——什么T恤衫、水杯、折扇巴拉巴拉,中国人最爱搞这种来赚钱,难道去俄罗斯也还是这一套?事实证明,我想多了!(尴尬……)说实话挺佩服熊帅的,看个球也能把身边的事情投射到这个“六层次业务和产品的设计方法”中。不过这也印证了,这个方法理论的广泛适用性以及它的正确性。
熊帅有一句话说的我觉得很对:
 


一个人工作了3年、5年或者10年,当你问他有什么成长的时候,如果他说“自己从一个学生变成了社会人”,那他基本上没有什么成长。一个人的成长,应该体现在他沉淀和吸收了多少理论和方法。这些理论和方法是骨架,能够给你提供思路和指导,就好比一个个书架,至于这个书架上能够放上什么哪些纪念品、哪些书、哪些“有血有肉”的东西,就要看你的思考有多深,行动有多少!


 
期待更加精彩的技术和理论分享。

饿了么的PWA升级实践

前端开发zkbhj 发表了文章 • 0 个评论 • 440 次浏览 • 2018-02-03 14:00 • 来自相关话题

自Vue.js在官方推特第一次公开到现在,我们就一直在进行着将饿了么移动端网站升级为 Progressive Web App的工作。直到近日在GoogleI/O 2017上登台亮相,才终于算告一段落。我们非常荣幸能够发布全世界第一个专门面向国内用户的PWA,但更荣幸的是能与 Google、 UC以及腾讯合作,一起推动国内Web与浏览器生态的发展。

多页应用、 Vue.js、 PWA?

对于构建一个希望达到原生应用级别体验的PWA,目前社区里的主流做法都是采用SPA,即单页面应用模型(Single-page App)来组织整个Web应用,业内最有名的几个PWA案例Twitter Lite、 Flipkart Lite、Housing Go 与 Polymer Shop无一例外。

然而饿了么,与很多国内的电商网站一样,青睐多页面应用模型(MPA, Multi-page App)所能带来的一些好处,也因此在一年多前就将移动站从基于AngularJS的单页应用重构为目前的多页应用模型。团队最看重的优点莫过于页面与页面之间的隔离与解耦,这使得我们可以将每个页面当做一个独立的“微服务”来看待,这些服务可以被独立迭代,独立提供给各种第三方的入口嵌入,甚至被不同的团队独立维护。而整个网站则只是各种服务的集合而非一个巨大的整体。

与此同时,我们仍然依赖 Vue.js作为JavaScript框架。 Vue.js除了是React、 AngularJS这种“重型武器”的竞争对手外,其轻量与高性能的优点使得它同样可以作为传统多页应用开发中流行的“jQuery/Zepto/Kissy+模板引擎”技术栈的完美替代。 Vue.js提供的组件系统、声明式与响应式编程更是提升了代码组织、共享、数据流控制、渲染等各个环节的开发效率。 Vue 还是一个渐进式框架,如果网站的复杂度继续提升,我们可以按需、增量地引入Vuex或Vue-Router这些模块。万一哪天又要改回单页呢?(谁知道呢……)

2017年, PWA已经成为Web应用新的风潮。我们决定试试,以我们现有的“Vue.js+多页”架构,能在升级PWA的道路上走多远,达到怎样的效果。

实现“PRPL”模式

“PRPL”(读作“purple”)是Google工程师提出的一种Web应用架构模式,它旨在利用现代Web平台的新技术以大幅优化移动Web的性能与体验,对如何组织与设计高性能的PWA系统提供了一种高层次的抽象。我们并不准备从头重构我们的Web应用,不过我们可以把实现“PRPL”模式作为我们的迁移目标。“PRPL”实际上是“Push/Preload、 Render、 Precache、 Lazy-Load”的缩写,我们接下来会展开介绍它们的具体含义。

Push/Preload,推送/预加载初始URL路由所需的关键资源

无论是HTTP2 Server Push还是,其关键都在于,我们希望提前请求一些隐藏在应用依赖关系(Dependency Graph)较深处的资源,以节省HTTP往返、浏览器解析文档,或脚本执行的时间。比如说,对于一个基于路由进行code splitting的SPA,如果我们可以在Webpack清单、路由等入口代码(entry chunks)被下载与运行之前就把初始URL,即用户访问的入口URL路由所依赖的代码用Server Push推送或进行提前加载。那么当这些资源被真正请求时,它们可能已经下载好并存在缓存中了,这样就加快了初始路由所有依赖的就绪。

在多页应用中,每一个路由本来就只会请求这个路由所需要的资源,并且通常依赖也都比较扁平。饿了么移动站的大部分脚本依赖都是普通的 <script> 元素,因此他们可以在文档解析早期就被浏览器的preloader扫描出来并且开始请求,其效果其实与显式的是一致的,见图1所示。

图1 有无<link rel=“preload”>的效果对比
 
我们还将所有关键的静态资源都伺服在同一域名下(不再做域名散列),以更好地利用HTTP2带来的多路复用(Multiplexing)。同时,我们也在进行着对API进行Server Push的实验。

Render,渲染初始路由,尽快让应用可被交互

既然所有初始路由的依赖都已经就绪,我们就可以尽快开始初始路由的渲染,这有助于提升应用诸如首次渲染时间、可交互时间等指标。多页应用并不使用基于JavaScript的路由,而是传统的HTML跳转机制,所以对于这一部分,多页应用其实不用额外做什么。

Precache,用Service Worker预缓存剩下的路由

这一部分就需要Service Worker的参与了。Service Worker是一个位于浏览器与网络之间的客户端代理,它已可拦截、处理、响应流经的HTTP请求,使得开发者得以从缓存中向Web应用提供资源而闻名。不过, Service Worker其实也可以主动发起 HTTP 请求,在“后台”预请求与预缓存我们未来所需要的资源,见图2所示。

图2 Service Worker预缓存未来所需要的资源
 
我们已经使用Webpack在构建过程中进行.vue编译、文件名哈希等工作,于是我们编写了一个Webpack插件来帮助收集需要缓存的依赖到一个“预缓存清单”中,并使用这个清单在每次构建时生成新的Service Worker文件。在新的Service Worker被激活时,清单里的资源就会被请求与缓存,这其实与SW-Precache 这个库的运行机制非常接近。

实际上,我们只对标记为“关键路由”的路由进行依赖收集。你可以将这些“关键路由”的依赖理解为我们整个应用的“App Shell” 或者说“安装包”。一旦它们都被缓存,或者说成功安装,无论用户是在线离线,我们的Web应用都可以从缓存中直接启动。对于那些并不那么重要的路由,我们则采取在运行时增量缓存的方式。我们使用的SW-Toolbox提供了LRU替换策略与TTL失效机制,可以保证我们的应用不会超过浏览器的缓存配额。

Lazy-Load,按需懒加载、懒实例化剩下的路由

懒加载与懒实例化剩下的路由对于SPA是一件相对麻烦点儿的事情,你需要实现基于路由的code splitting与异步加载。幸运的是,这又是一件不需要多页应用担心的事情,多页应用中的各个路由天生就是分离的。

值得说明的是,无论单页还是多页应用,如果在上一步中,我们已经将这些路由的资源都预先下载与缓存好了,那么懒加载就几乎是瞬时完成的了,这时候我们就只需要付出实例化的代价。

至此,我们对PRPL的四部分含义做了详细说明。有趣的是,我们发现多页应用在实现PRPL这件事甚至比单页还要容易一些。那么结果如何呢?

根据Google推出的Web性能分析工具Lighthouse(v1.6),在模拟的3G网络下,用户的初次访问(无任何缓存)大约在2秒左右达到“可交互”,可以说非常不错,见图3所示。而对于再次访问,由于所有资源都直接来自于Service Worker缓存,页面可以在1秒左右就达到可交互的状态了。
 
图3 Lighthouse跑分结果
 
但是,故事并不是这么简单得就结束了。在实际体验中我们发现,应用在页与页的切换时,仍然存在着非常明显的白屏空隙,见图4所示。由于PWA是全屏运行,白屏对用户体验所带来的负面影响甚至比以往在浏览器内更大。我们不是已经用Service Worker缓存了所有资源了吗,怎么还会这样呢?

图4 从首页点击到发现页,跳转过程中的白屏
 
多页应用的陷阱:重启开销

与SPA不同,在多页应用中,路由的切换是原生的浏览器文档跳转(Navigating across documents),这意味着之前的页面会被完全丢弃而浏览器需要为下一个路由的页面重新执行所有的启动步骤:重新下载资源、重新解析HTML、重新运行JavaScript、重新解码图片、重新布局页面、重新绘制……即使其中的很多步骤本是可以在多个路由之间复用的。这些工作无疑将产生巨大的计算开销,也因此需要付出相当多的时间成本。

图5中为我们的入口页(同时也是最重要的页面)在两倍CPU节流模拟下的Profile数据。即使我们可以将“可交互时间”控制在 1 秒左右,我们的用户仍然会觉得这对于“仅仅切换个标签”来说实在是太慢了。

图5 入口页在两倍CPU节流模拟下的Profile数据

巨大的JavaScript重启开销

根据Profile,我们发现在首次渲染(First Paint)发生之前,大量的时间(900ms)都消耗在了JavaScript的运行上(Evaluate Script)。几乎所有脚本都是阻塞的(Parser-blocking),不过因为所有的UI都是由JavaScript/Vue.js驱动的,倒也不会有性能影响。这900ms中,约一半是消耗在Vue.js运行时、组件、库等依赖的运行上,而另一半则花在了业务组件实例化时Vue.js的启动与渲染上。从软件工程角度来说,我们需要这些抽象,所以这里并不是想责怪JavaScript或是Vue.js所带来的开销。

但是,在SPA中, JavaScript的启动成本是均摊到整个生命周期的:每个脚本都只需要被解析与编译一次,诸如生成Virtual DOM等较重的任务可以只执行一次,像Vue.js的ViewModel或是Virtual DOM这样的大对象也可以被留在内存里复用。可惜在多页应用里就不是这样了,我们每次切换页面都为JavaScript付出了巨大的重启代价。

浏览器的缓存啊,能不能帮帮忙?

能,也不能。

V8提供了代码缓存(code caching),可以将编译后的机器码在本地拷贝一份,这样我们就可以在下次请求同一个脚本时一次省略掉请求、解析、编译的所有工作。而且,对于缓存在Service Worker配套的Cache Storage中的脚本,会在第一次执行后就触发V8的代码缓存,这对于我们的多页切换能提供不少帮助。

另外一个你或许听过的浏览器缓存叫做“进退缓存”, Back-Forward Cache,简称bfcache。浏览器厂商对其的命名各异, Opera称之为Fast History Navigation, Webkit称其为Page Cache。但是思路都一样,就是我们可以让浏览器在跳转时把前一页留存在内存中,保留JavaScript与DOM的状态,而不是全都销毁掉。你可以随便找个传统的多页网站在iOS Safari上试试,无论是通过浏览器的前进后退按钮、手势,还是通过超链接(会有一些不同),基本都可以看到瞬间加载的效果。

Bfcache其实非常适合多页应用。但不幸的是,Chrome由于内存开销与其多进程架构等原因目前并不支持。 Chrome现阶段仅仅只是用了传统的HTTP磁盘缓存,来稍稍简化了一下加载过程而已。对于Chromium内核霸占的Android生态来说,我们没法指望了。

为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
 
为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
图6 在添加骨架屏后,从发现页点回首页的效果

这效果本该很轻松的就能实现,不过实际上我们还费了点功夫。

在构建时使用 Vue 预渲染骨架屏

你可能已经想到了,为了让骨架屏可以被Service Worker缓存,瞬间加载并独立于JavaScript渲染,我们需要把组成骨架屏的HTML标签、 CSS样式与图片资源一并内联至各个路由的静态*.html文件中。

不过,我们并不准备手动编写这些骨架屏。你想啊,如果每次真实组件有迭代(每一个路由对我们来说都是一个Vue.js组件),我们都需要手动去同步每一个变化到骨架屏的话,那实在是太繁琐且难以维护了。好在,骨架屏不过是当数据还未加载进来前,页面的一个空白版本而已。如果我们能将骨架屏实现为真实组件的一个特殊状态——“空状态”的话,从理论上就可以从真实组件中直接渲染出骨架屏来。

而Vue.js的多才多艺就在这时体现出来了,我们真的可以用Vue.js 的服务端渲染模块来实现这个想法,不过不是用在真正的服务器上,而是在构建时用它把组件的空状态预先渲染成字符串并注入到HTML模板中。你需要调整Vue.js组件代码使得它可以在Node上执行,有些页面对DOM/BOM的依赖一时无法轻易去除得,我们目前只好额外编写一个*.shell.vue来暂时绕过这个问题。

关于浏览器的绘制(Painting)

HTML文件中有标签并不意味着这些标签就能立刻被绘制到屏幕上,你必须保证页面的关键渲染路径是为此优化的。很多开发者相信将Script标签放在body的底部就足以保证内容能在脚本执行之前被绘制,这对于能渲染不完整DOM树的浏览器(比如桌面浏览器常见的流式渲染)来说可能是成立的。但移动端的浏览器很可能因为考虑到较慢的硬件、电量消耗等因素并不这么做。不仅如此,即使你曾被告知设为async或defer的脚本就不会阻塞HTML解析了,但这可不意味着浏览 器就一定会在执行它们之前进行渲染。

首先我想澄清的是,根据 HTML 规范 Scripting 章节, async脚本是在其请求完成后立刻运行的,因此它本来就可能阻塞到解析。只有defer(且非内联)与最新的type=module被指定为“一定不会阻塞解析”(不过defer目前也有点小问题……我们稍后会再提到),见图7所示。
图7 具有不同属性的Script脚本对HTML解析的阻塞情况

而更重要的是,一个不阻塞HTML解析的脚本仍然可能阻塞到绘制。我做了一个简化的“最小多页PWA”(Minimal Multi-page PWA,或MMPWA)来测试这个问题:我们在一个async(且确实不阻塞HTML解析)脚本中,生成并渲染1000个列表项,然后测试骨架屏能否在脚本执行之前渲染出来。图8是通过USB Debugging在我的Nexus 5真机上录制的Profile。


图8 通过USB Debugging在Nexus 5真机上录制的Profile

是的,出乎意料吗?首次渲染确实被阻塞到脚本执行结束后才发生。究其原因,如果我们在浏览器还未完成上一次绘制工作之前就过快得进行了DOM操作,我们亲爱的浏览器就只好抛弃所有它已经完成的像素,且一直要等待到DOM操作引起的所有工作结束之后才能重新进行下一次渲染。而这种情况更容易在拥有较慢CPU/GPU的移动设备上出现。

黑魔法:利用setTimeout()让绘制提前

不难发现,骨架屏的绘制与脚本执行实际是一个竞态。大概是Vue.js太快了,我们的骨架屏还是有非常大的概率绘制不出来。于是我们想着如何能让脚本执行慢点,或者说,“懒”点。于是我们想到了一个经典的Hack: setTimeout(callback, 0)。我们试着把MMPWA中的DOM操作(渲染1000个列表)放进setTimeout(callback, 0)里……

当当!首次渲染瞬间就被提前了,见图9所示。如果你熟悉浏览器的事件循环模型(Event Loop)的话,这招Hack其实是通过setTimeout的回调把DOM操作放到了事件循环的任务队列中以避免它在当前循环执行,这样浏览器就得以在主线程空闲时喘息一下(更新一下渲染)了。如果你想亲手试试 MMPWA的话,你可以访问github.com/Huxpro/mmpwa 或huangxuan.me/mmpwa/ ,查看代码与Demo。我把UI设计成了A/B Test的形式并改为渲染5000个列表项来让效果更夸张一些。

图9 利用Hack技术,提前完成骨架屏的绘制

回到饿了么PWA上,我们同样试着把new Vue()放到了setTimeout中。果然,黑魔法再次显灵,骨架屏在每次跳转后都能立刻被渲染。这时的Profile看起来是这样的,见图10所示。

 
图10 为感知体验进行各种优化后的最终Profile

现在,我们在400ms时触发首次渲染(骨架屏),在600ms时完成真实UI的渲染并达到页面的可交互。你可以详细对比下图9和图10所示的优化前后Profile的区别。

被我“defer”的有关defer的Bug

不知道你发现没有,在图10的Profile中,我们仍然有不少脚本是阻塞了HTML解析的。好吧,让我解释一下,由于历史原因,我们确实保留了一部分的阻塞脚本,比如侵入性很强的lib-flexible,我们没法轻易去除它。不过, Profile里的大部分阻塞脚本实际上都设置了defer,我们本以为他们应该在HTML解析完成之后才被执行,结果被Profile打了一脸。

我和Jake Archibald 聊了一下,果然这是Chrome的Bug: defer的脚本被完全缓存时,并没有遵守规范等待解析结束,反而阻塞了解析与渲染。Jake已经提交在crbug上了,一起给它投票吧。

最后,图11是优化后的Lighthouse跑分结果,同样可以看到明显的性能提升。需要说明的是,能影响Lighthouse跑分的因素有很多,所以我建议你以控制变量(跑分用的设备、跑分时的网络环境等)的方式来进行对照实验。

图11 优化后的Lighthouse跑分结果

最后为大家展示下应用的架构示意图,见图12所示。

图12 应用架构示意图

一些感想

多页应用仍然有很长的路要走

Web是一个极其多样化的平台。从静态的博客,到电商网站,再到桌面级的生产力软件,它们全都是Web这个大家庭的第一公民。而我们组织Web应用的方式,也同样只会更多而不会更少:多页、单页、 Universal JavaScript应用、 WebGL,以及可以预见的Web Assembly。不同的技术之间没有贵贱,但是适用场景的差距确是客观存在的。

Jake 曾在 Chrome Dev Summit 2016 上说过“PWA!== SPA”。可是尽管我们已经用上了一系列最新的技术(PRPL、 Service Worker、 App Shell……),我们仍然因为多页应用模型本身的缺陷有着难以逾越的一些障碍。多页应用在未来可能会有“bfcache API”、 Navigation Transition等新的规范以缩小跟SPA的距离,不过我们也必须承认,时至今日,多页应用的局限性也是非常明显的。

而PWA终将带领Web应用进入新的时代

即使我们的多页应用在升级PWA的路上不如单页应用来得那么闪亮,但是PWA背后的想法与技术却实实在在地帮助我们在Web平台上提供了更好的用户体验。

PWA作为下一代 Web 应用模型,其尝试解决的是Web平台本身的根本性问题:对网络与浏览器UI的硬依赖。因此,任何Web应用都可以从中获益,这与你是多页还是单页、面向桌面还是移动端、是用React还是Vue.js无关。或许,它还终将改变用户对移动Web的期待。现如今,谁还觉得桌面端的Web只是个看文档的地方呢?

还是那句老话,让我们的用户,也像我们这般热爱Web吧。

最后,感谢饿了么的王亦斯、任光辉、题叶,Google 的 Michael Yeung、 DevRel 团队, UC浏览器团队,腾讯X5浏览器团队在这次项目中的合作。感谢尤雨溪、陈蒙迪和Jake Archibald 在写作过程中给予我的帮助。
 
原文阅读:https://zhuanlan.zhihu.com/p/27836133 查看全部
自Vue.js在官方推特第一次公开到现在,我们就一直在进行着将饿了么移动端网站升级为 Progressive Web App的工作。直到近日在GoogleI/O 2017上登台亮相,才终于算告一段落。我们非常荣幸能够发布全世界第一个专门面向国内用户的PWA,但更荣幸的是能与 Google、 UC以及腾讯合作,一起推动国内Web与浏览器生态的发展。

多页应用、 Vue.js、 PWA?

对于构建一个希望达到原生应用级别体验的PWA,目前社区里的主流做法都是采用SPA,即单页面应用模型(Single-page App)来组织整个Web应用,业内最有名的几个PWA案例Twitter Lite、 Flipkart Lite、Housing Go 与 Polymer Shop无一例外。

然而饿了么,与很多国内的电商网站一样,青睐多页面应用模型(MPA, Multi-page App)所能带来的一些好处,也因此在一年多前就将移动站从基于AngularJS的单页应用重构为目前的多页应用模型。团队最看重的优点莫过于页面与页面之间的隔离与解耦,这使得我们可以将每个页面当做一个独立的“微服务”来看待,这些服务可以被独立迭代,独立提供给各种第三方的入口嵌入,甚至被不同的团队独立维护。而整个网站则只是各种服务的集合而非一个巨大的整体。

与此同时,我们仍然依赖 Vue.js作为JavaScript框架。 Vue.js除了是React、 AngularJS这种“重型武器”的竞争对手外,其轻量与高性能的优点使得它同样可以作为传统多页应用开发中流行的“jQuery/Zepto/Kissy+模板引擎”技术栈的完美替代。 Vue.js提供的组件系统、声明式与响应式编程更是提升了代码组织、共享、数据流控制、渲染等各个环节的开发效率。 Vue 还是一个渐进式框架,如果网站的复杂度继续提升,我们可以按需、增量地引入Vuex或Vue-Router这些模块。万一哪天又要改回单页呢?(谁知道呢……)

2017年, PWA已经成为Web应用新的风潮。我们决定试试,以我们现有的“Vue.js+多页”架构,能在升级PWA的道路上走多远,达到怎样的效果。

实现“PRPL”模式

“PRPL”(读作“purple”)是Google工程师提出的一种Web应用架构模式,它旨在利用现代Web平台的新技术以大幅优化移动Web的性能与体验,对如何组织与设计高性能的PWA系统提供了一种高层次的抽象。我们并不准备从头重构我们的Web应用,不过我们可以把实现“PRPL”模式作为我们的迁移目标。“PRPL”实际上是“Push/Preload、 Render、 Precache、 Lazy-Load”的缩写,我们接下来会展开介绍它们的具体含义。

Push/Preload,推送/预加载初始URL路由所需的关键资源

无论是HTTP2 Server Push还是,其关键都在于,我们希望提前请求一些隐藏在应用依赖关系(Dependency Graph)较深处的资源,以节省HTTP往返、浏览器解析文档,或脚本执行的时间。比如说,对于一个基于路由进行code splitting的SPA,如果我们可以在Webpack清单、路由等入口代码(entry chunks)被下载与运行之前就把初始URL,即用户访问的入口URL路由所依赖的代码用Server Push推送或进行提前加载。那么当这些资源被真正请求时,它们可能已经下载好并存在缓存中了,这样就加快了初始路由所有依赖的就绪。

在多页应用中,每一个路由本来就只会请求这个路由所需要的资源,并且通常依赖也都比较扁平。饿了么移动站的大部分脚本依赖都是普通的 <script> 元素,因此他们可以在文档解析早期就被浏览器的preloader扫描出来并且开始请求,其效果其实与显式的是一致的,见图1所示。

图1 有无<link rel=“preload”>的效果对比
 
我们还将所有关键的静态资源都伺服在同一域名下(不再做域名散列),以更好地利用HTTP2带来的多路复用(Multiplexing)。同时,我们也在进行着对API进行Server Push的实验。

Render,渲染初始路由,尽快让应用可被交互

既然所有初始路由的依赖都已经就绪,我们就可以尽快开始初始路由的渲染,这有助于提升应用诸如首次渲染时间、可交互时间等指标。多页应用并不使用基于JavaScript的路由,而是传统的HTML跳转机制,所以对于这一部分,多页应用其实不用额外做什么。

Precache,用Service Worker预缓存剩下的路由

这一部分就需要Service Worker的参与了。Service Worker是一个位于浏览器与网络之间的客户端代理,它已可拦截、处理、响应流经的HTTP请求,使得开发者得以从缓存中向Web应用提供资源而闻名。不过, Service Worker其实也可以主动发起 HTTP 请求,在“后台”预请求与预缓存我们未来所需要的资源,见图2所示。

图2 Service Worker预缓存未来所需要的资源
 
我们已经使用Webpack在构建过程中进行.vue编译、文件名哈希等工作,于是我们编写了一个Webpack插件来帮助收集需要缓存的依赖到一个“预缓存清单”中,并使用这个清单在每次构建时生成新的Service Worker文件。在新的Service Worker被激活时,清单里的资源就会被请求与缓存,这其实与SW-Precache 这个库的运行机制非常接近。

实际上,我们只对标记为“关键路由”的路由进行依赖收集。你可以将这些“关键路由”的依赖理解为我们整个应用的“App Shell” 或者说“安装包”。一旦它们都被缓存,或者说成功安装,无论用户是在线离线,我们的Web应用都可以从缓存中直接启动。对于那些并不那么重要的路由,我们则采取在运行时增量缓存的方式。我们使用的SW-Toolbox提供了LRU替换策略与TTL失效机制,可以保证我们的应用不会超过浏览器的缓存配额。

Lazy-Load,按需懒加载、懒实例化剩下的路由

懒加载与懒实例化剩下的路由对于SPA是一件相对麻烦点儿的事情,你需要实现基于路由的code splitting与异步加载。幸运的是,这又是一件不需要多页应用担心的事情,多页应用中的各个路由天生就是分离的。

值得说明的是,无论单页还是多页应用,如果在上一步中,我们已经将这些路由的资源都预先下载与缓存好了,那么懒加载就几乎是瞬时完成的了,这时候我们就只需要付出实例化的代价。

至此,我们对PRPL的四部分含义做了详细说明。有趣的是,我们发现多页应用在实现PRPL这件事甚至比单页还要容易一些。那么结果如何呢?

根据Google推出的Web性能分析工具Lighthouse(v1.6),在模拟的3G网络下,用户的初次访问(无任何缓存)大约在2秒左右达到“可交互”,可以说非常不错,见图3所示。而对于再次访问,由于所有资源都直接来自于Service Worker缓存,页面可以在1秒左右就达到可交互的状态了。
 
图3 Lighthouse跑分结果
 
但是,故事并不是这么简单得就结束了。在实际体验中我们发现,应用在页与页的切换时,仍然存在着非常明显的白屏空隙,见图4所示。由于PWA是全屏运行,白屏对用户体验所带来的负面影响甚至比以往在浏览器内更大。我们不是已经用Service Worker缓存了所有资源了吗,怎么还会这样呢?

图4 从首页点击到发现页,跳转过程中的白屏
 
多页应用的陷阱:重启开销

与SPA不同,在多页应用中,路由的切换是原生的浏览器文档跳转(Navigating across documents),这意味着之前的页面会被完全丢弃而浏览器需要为下一个路由的页面重新执行所有的启动步骤:重新下载资源、重新解析HTML、重新运行JavaScript、重新解码图片、重新布局页面、重新绘制……即使其中的很多步骤本是可以在多个路由之间复用的。这些工作无疑将产生巨大的计算开销,也因此需要付出相当多的时间成本。

图5中为我们的入口页(同时也是最重要的页面)在两倍CPU节流模拟下的Profile数据。即使我们可以将“可交互时间”控制在 1 秒左右,我们的用户仍然会觉得这对于“仅仅切换个标签”来说实在是太慢了。

图5 入口页在两倍CPU节流模拟下的Profile数据

巨大的JavaScript重启开销

根据Profile,我们发现在首次渲染(First Paint)发生之前,大量的时间(900ms)都消耗在了JavaScript的运行上(Evaluate Script)。几乎所有脚本都是阻塞的(Parser-blocking),不过因为所有的UI都是由JavaScript/Vue.js驱动的,倒也不会有性能影响。这900ms中,约一半是消耗在Vue.js运行时、组件、库等依赖的运行上,而另一半则花在了业务组件实例化时Vue.js的启动与渲染上。从软件工程角度来说,我们需要这些抽象,所以这里并不是想责怪JavaScript或是Vue.js所带来的开销。

但是,在SPA中, JavaScript的启动成本是均摊到整个生命周期的:每个脚本都只需要被解析与编译一次,诸如生成Virtual DOM等较重的任务可以只执行一次,像Vue.js的ViewModel或是Virtual DOM这样的大对象也可以被留在内存里复用。可惜在多页应用里就不是这样了,我们每次切换页面都为JavaScript付出了巨大的重启代价。

浏览器的缓存啊,能不能帮帮忙?

能,也不能。

V8提供了代码缓存(code caching),可以将编译后的机器码在本地拷贝一份,这样我们就可以在下次请求同一个脚本时一次省略掉请求、解析、编译的所有工作。而且,对于缓存在Service Worker配套的Cache Storage中的脚本,会在第一次执行后就触发V8的代码缓存,这对于我们的多页切换能提供不少帮助。

另外一个你或许听过的浏览器缓存叫做“进退缓存”, Back-Forward Cache,简称bfcache。浏览器厂商对其的命名各异, Opera称之为Fast History Navigation, Webkit称其为Page Cache。但是思路都一样,就是我们可以让浏览器在跳转时把前一页留存在内存中,保留JavaScript与DOM的状态,而不是全都销毁掉。你可以随便找个传统的多页网站在iOS Safari上试试,无论是通过浏览器的前进后退按钮、手势,还是通过超链接(会有一些不同),基本都可以看到瞬间加载的效果。

Bfcache其实非常适合多页应用。但不幸的是,Chrome由于内存开销与其多进程架构等原因目前并不支持。 Chrome现阶段仅仅只是用了传统的HTTP磁盘缓存,来稍稍简化了一下加载过程而已。对于Chromium内核霸占的Android生态来说,我们没法指望了。

为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
 
为“感知体验”奋斗

尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的DOM节点数以节省Virtual DOM的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。

Chrome产品经理Owen写过一篇Reactive Web Design: The secret to building web apps that feel amazing,谈到两种改进感知体验的手段:一是使用骨架屏(Skeleton Screen)来实现瞬间加载;二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。

为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证UI是稳定、连续、有响应的。我录了两个视频放在Youtube上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果。最终效果如图6所示。
图6 在添加骨架屏后,从发现页点回首页的效果

这效果本该很轻松的就能实现,不过实际上我们还费了点功夫。

在构建时使用 Vue 预渲染骨架屏

你可能已经想到了,为了让骨架屏可以被Service Worker缓存,瞬间加载并独立于JavaScript渲染,我们需要把组成骨架屏的HTML标签、 CSS样式与图片资源一并内联至各个路由的静态*.html文件中。

不过,我们并不准备手动编写这些骨架屏。你想啊,如果每次真实组件有迭代(每一个路由对我们来说都是一个Vue.js组件),我们都需要手动去同步每一个变化到骨架屏的话,那实在是太繁琐且难以维护了。好在,骨架屏不过是当数据还未加载进来前,页面的一个空白版本而已。如果我们能将骨架屏实现为真实组件的一个特殊状态——“空状态”的话,从理论上就可以从真实组件中直接渲染出骨架屏来。

而Vue.js的多才多艺就在这时体现出来了,我们真的可以用Vue.js 的服务端渲染模块来实现这个想法,不过不是用在真正的服务器上,而是在构建时用它把组件的空状态预先渲染成字符串并注入到HTML模板中。你需要调整Vue.js组件代码使得它可以在Node上执行,有些页面对DOM/BOM的依赖一时无法轻易去除得,我们目前只好额外编写一个*.shell.vue来暂时绕过这个问题。

关于浏览器的绘制(Painting)

HTML文件中有标签并不意味着这些标签就能立刻被绘制到屏幕上,你必须保证页面的关键渲染路径是为此优化的。很多开发者相信将Script标签放在body的底部就足以保证内容能在脚本执行之前被绘制,这对于能渲染不完整DOM树的浏览器(比如桌面浏览器常见的流式渲染)来说可能是成立的。但移动端的浏览器很可能因为考虑到较慢的硬件、电量消耗等因素并不这么做。不仅如此,即使你曾被告知设为async或defer的脚本就不会阻塞HTML解析了,但这可不意味着浏览 器就一定会在执行它们之前进行渲染。

首先我想澄清的是,根据 HTML 规范 Scripting 章节, async脚本是在其请求完成后立刻运行的,因此它本来就可能阻塞到解析。只有defer(且非内联)与最新的type=module被指定为“一定不会阻塞解析”(不过defer目前也有点小问题……我们稍后会再提到),见图7所示。
图7 具有不同属性的Script脚本对HTML解析的阻塞情况

而更重要的是,一个不阻塞HTML解析的脚本仍然可能阻塞到绘制。我做了一个简化的“最小多页PWA”(Minimal Multi-page PWA,或MMPWA)来测试这个问题:我们在一个async(且确实不阻塞HTML解析)脚本中,生成并渲染1000个列表项,然后测试骨架屏能否在脚本执行之前渲染出来。图8是通过USB Debugging在我的Nexus 5真机上录制的Profile。


图8 通过USB Debugging在Nexus 5真机上录制的Profile

是的,出乎意料吗?首次渲染确实被阻塞到脚本执行结束后才发生。究其原因,如果我们在浏览器还未完成上一次绘制工作之前就过快得进行了DOM操作,我们亲爱的浏览器就只好抛弃所有它已经完成的像素,且一直要等待到DOM操作引起的所有工作结束之后才能重新进行下一次渲染。而这种情况更容易在拥有较慢CPU/GPU的移动设备上出现。

黑魔法:利用setTimeout()让绘制提前

不难发现,骨架屏的绘制与脚本执行实际是一个竞态。大概是Vue.js太快了,我们的骨架屏还是有非常大的概率绘制不出来。于是我们想着如何能让脚本执行慢点,或者说,“懒”点。于是我们想到了一个经典的Hack: setTimeout(callback, 0)。我们试着把MMPWA中的DOM操作(渲染1000个列表)放进setTimeout(callback, 0)里……

当当!首次渲染瞬间就被提前了,见图9所示。如果你熟悉浏览器的事件循环模型(Event Loop)的话,这招Hack其实是通过setTimeout的回调把DOM操作放到了事件循环的任务队列中以避免它在当前循环执行,这样浏览器就得以在主线程空闲时喘息一下(更新一下渲染)了。如果你想亲手试试 MMPWA的话,你可以访问github.com/Huxpro/mmpwa 或huangxuan.me/mmpwa/ ,查看代码与Demo。我把UI设计成了A/B Test的形式并改为渲染5000个列表项来让效果更夸张一些。

图9 利用Hack技术,提前完成骨架屏的绘制

回到饿了么PWA上,我们同样试着把new Vue()放到了setTimeout中。果然,黑魔法再次显灵,骨架屏在每次跳转后都能立刻被渲染。这时的Profile看起来是这样的,见图10所示。

 
图10 为感知体验进行各种优化后的最终Profile

现在,我们在400ms时触发首次渲染(骨架屏),在600ms时完成真实UI的渲染并达到页面的可交互。你可以详细对比下图9和图10所示的优化前后Profile的区别。

被我“defer”的有关defer的Bug

不知道你发现没有,在图10的Profile中,我们仍然有不少脚本是阻塞了HTML解析的。好吧,让我解释一下,由于历史原因,我们确实保留了一部分的阻塞脚本,比如侵入性很强的lib-flexible,我们没法轻易去除它。不过, Profile里的大部分阻塞脚本实际上都设置了defer,我们本以为他们应该在HTML解析完成之后才被执行,结果被Profile打了一脸。

我和Jake Archibald 聊了一下,果然这是Chrome的Bug: defer的脚本被完全缓存时,并没有遵守规范等待解析结束,反而阻塞了解析与渲染。Jake已经提交在crbug上了,一起给它投票吧。

最后,图11是优化后的Lighthouse跑分结果,同样可以看到明显的性能提升。需要说明的是,能影响Lighthouse跑分的因素有很多,所以我建议你以控制变量(跑分用的设备、跑分时的网络环境等)的方式来进行对照实验。

图11 优化后的Lighthouse跑分结果

最后为大家展示下应用的架构示意图,见图12所示。

图12 应用架构示意图

一些感想

多页应用仍然有很长的路要走

Web是一个极其多样化的平台。从静态的博客,到电商网站,再到桌面级的生产力软件,它们全都是Web这个大家庭的第一公民。而我们组织Web应用的方式,也同样只会更多而不会更少:多页、单页、 Universal JavaScript应用、 WebGL,以及可以预见的Web Assembly。不同的技术之间没有贵贱,但是适用场景的差距确是客观存在的。

Jake 曾在 Chrome Dev Summit 2016 上说过“PWA!== SPA”。可是尽管我们已经用上了一系列最新的技术(PRPL、 Service Worker、 App Shell……),我们仍然因为多页应用模型本身的缺陷有着难以逾越的一些障碍。多页应用在未来可能会有“bfcache API”、 Navigation Transition等新的规范以缩小跟SPA的距离,不过我们也必须承认,时至今日,多页应用的局限性也是非常明显的。

而PWA终将带领Web应用进入新的时代

即使我们的多页应用在升级PWA的路上不如单页应用来得那么闪亮,但是PWA背后的想法与技术却实实在在地帮助我们在Web平台上提供了更好的用户体验。

PWA作为下一代 Web 应用模型,其尝试解决的是Web平台本身的根本性问题:对网络与浏览器UI的硬依赖。因此,任何Web应用都可以从中获益,这与你是多页还是单页、面向桌面还是移动端、是用React还是Vue.js无关。或许,它还终将改变用户对移动Web的期待。现如今,谁还觉得桌面端的Web只是个看文档的地方呢?

还是那句老话,让我们的用户,也像我们这般热爱Web吧。

最后,感谢饿了么的王亦斯、任光辉、题叶,Google 的 Michael Yeung、 DevRel 团队, UC浏览器团队,腾讯X5浏览器团队在这次项目中的合作。感谢尤雨溪、陈蒙迪和Jake Archibald 在写作过程中给予我的帮助。
 
原文阅读:https://zhuanlan.zhihu.com/p/27836133

《架构即未来》中最常用的15个架构原则总结

架构思想zkbhj 发表了文章 • 0 个评论 • 258 次浏览 • 2017-12-25 12:05 • 来自相关话题

《架构即未来》这本书的第12章简单阐述了架构设计的一些常用的原则。这些原则中很多都是在架构一开始的设计中就要考虑进去的,这样在出现任何问题时,我们都能够及时的处理,和把问题影响的范围有效的缩小。否则就像我现在的项目,一开始设计时,考虑的很少,出问题时,没有做到及时的反馈,和缩小影响范围,只能在事故的代价中将所需要的原则添加进来,慢慢完善。

1.N+1设计

要确保任何你所开发的系统在发生故障时,至少有一个冗余的实例。


一个实例确实很危险,当这个实例出现不明原因的问题不能对外服务,需要debug的时候,如果优先debug,那当前实例就要暂停服务直到你找到问题为止。如果你直接重启实例恢复服务,就没有事故现场进行debug了。而这时如果有一个冗余的实例,就可以先让冗余的实例对外服务,事故现场的环境也得以保留。

多个实例来做负载均衡也是一种不错的选择。

2.回滚设计

确保系统可以回滚到以前发布过的任何版本。


以前做游戏的时候经常遇到回滚,有时候是数据库回滚,有时候是服务器端回滚,一般都是回滚到上个版本。

3.禁用设计

能够关闭任何发布的功能。


当一个功能出现严重问题不得不关闭时,如果关闭整个系统代价就有点大了,所有要有单个功能的开关。像商城系统的支付功能就一定要有开关,如果出现比较严重的bug,可以关闭支付而不影响下单。

4.监控设计

在设计阶段就必须要考虑监控,而不是在实施完成之后补充。


如果监控做的好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。

5.设计多活数据中心

不要被一个数据中心的解决方案把自己限制住。


有钱就多建一个,让股东放心。

6.只用成熟的技术

只用确实好用的技术。


不管用什么技术,都要确保是一个成熟的技术。也许某个新技术有众多优点,比如,降低开发成本,提高开发效率,提高可扩展能力,减少终端用户的响应时间。但是,只要这项技术故障率比较高,就绝不能使用。

7.异步设计

只有在绝对必要的时候才进行同步调用。


异步适合并发。

8.无状态系统

只有当业务确实需要的时候,才使用状态。


无状态的系统更利于扩展,更利于做负载均衡。

9.水平扩展非垂直升级

永远不要依赖更大、更快的系统。


微服务是水平扩展的一个例子,不要把所有的功能都集中在一个系统里面。必要的时候把需求分为多个系统,而不是升级原有的系统。

10.设计至少有两个步骤的前瞻性


在扩展性问题发生前考虑好下一步的行动计划。


想的更远一点,就能减少重构的次数。

11.非核心则购买

如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。


云服务这种的就购买好了。

12.使用商品化硬件

在大多数情况下,便宜的是最好的。


硬件这块儿,满足需求即可,在必要的时候增加配置。

13.小构建,小发布,快试错

全部研发要小构建,不断迭代,让系统不断地成长。


小版本的失败率较低,因为失败率与解决方案中的变更数量直接相关。

14.隔离故障

实现隔离故障设计,通过断路保护避免故障传播和交叉影响。


避免多系统之间的互相影响,这个很重要。

15.自动化

设计和构建自动化的过程。如果机器可以做,就不要依赖于人。


人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。
 
原文阅读:https://www.cnblogs.com/cxiaojia/p/6294493.html 查看全部
《架构即未来》这本书的第12章简单阐述了架构设计的一些常用的原则。这些原则中很多都是在架构一开始的设计中就要考虑进去的,这样在出现任何问题时,我们都能够及时的处理,和把问题影响的范围有效的缩小。否则就像我现在的项目,一开始设计时,考虑的很少,出问题时,没有做到及时的反馈,和缩小影响范围,只能在事故的代价中将所需要的原则添加进来,慢慢完善。

1.N+1设计


要确保任何你所开发的系统在发生故障时,至少有一个冗余的实例。



一个实例确实很危险,当这个实例出现不明原因的问题不能对外服务,需要debug的时候,如果优先debug,那当前实例就要暂停服务直到你找到问题为止。如果你直接重启实例恢复服务,就没有事故现场进行debug了。而这时如果有一个冗余的实例,就可以先让冗余的实例对外服务,事故现场的环境也得以保留。

多个实例来做负载均衡也是一种不错的选择。

2.回滚设计


确保系统可以回滚到以前发布过的任何版本。



以前做游戏的时候经常遇到回滚,有时候是数据库回滚,有时候是服务器端回滚,一般都是回滚到上个版本。

3.禁用设计


能够关闭任何发布的功能。



当一个功能出现严重问题不得不关闭时,如果关闭整个系统代价就有点大了,所有要有单个功能的开关。像商城系统的支付功能就一定要有开关,如果出现比较严重的bug,可以关闭支付而不影响下单。

4.监控设计


在设计阶段就必须要考虑监控,而不是在实施完成之后补充。



如果监控做的好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。

5.设计多活数据中心


不要被一个数据中心的解决方案把自己限制住。



有钱就多建一个,让股东放心。

6.只用成熟的技术


只用确实好用的技术。



不管用什么技术,都要确保是一个成熟的技术。也许某个新技术有众多优点,比如,降低开发成本,提高开发效率,提高可扩展能力,减少终端用户的响应时间。但是,只要这项技术故障率比较高,就绝不能使用。

7.异步设计


只有在绝对必要的时候才进行同步调用。



异步适合并发。

8.无状态系统


只有当业务确实需要的时候,才使用状态。



无状态的系统更利于扩展,更利于做负载均衡。

9.水平扩展非垂直升级


永远不要依赖更大、更快的系统。



微服务是水平扩展的一个例子,不要把所有的功能都集中在一个系统里面。必要的时候把需求分为多个系统,而不是升级原有的系统。

10.设计至少有两个步骤的前瞻性



在扩展性问题发生前考虑好下一步的行动计划。



想的更远一点,就能减少重构的次数。

11.非核心则购买


如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。



云服务这种的就购买好了。

12.使用商品化硬件


在大多数情况下,便宜的是最好的。



硬件这块儿,满足需求即可,在必要的时候增加配置。

13.小构建,小发布,快试错


全部研发要小构建,不断迭代,让系统不断地成长。



小版本的失败率较低,因为失败率与解决方案中的变更数量直接相关。

14.隔离故障


实现隔离故障设计,通过断路保护避免故障传播和交叉影响。



避免多系统之间的互相影响,这个很重要。

15.自动化


设计和构建自动化的过程。如果机器可以做,就不要依赖于人。



人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。
 
原文阅读:https://www.cnblogs.com/cxiaojia/p/6294493.html

分布式系统理论基础 - 一致性、2PC和3PC

专业名词zkbhj 发表了文章 • 0 个评论 • 204 次浏览 • 2017-12-23 21:10 • 来自相关话题

狭义的分布式系统指由网络连接的计算机系统,每个节点独立地承担计算或存储任务,节点间通过网络协同工作。广义的分布式系统是一个相对的概念,正如Leslie Lamport所说:

What is a distributed systeme. Distribution is in the eye of the beholder.
To the user sitting at the keyboard, his IBM personal computer is a nondistributed system. 
To a flea crawling around on the circuit board, or to the engineer who designed it, it's very much a distributed system.
 

 
一致性是分布式理论中的根本性问题,近半个世纪以来,科学家们围绕着一致性问题提出了很多理论模型,依据这些理论模型,业界也出现了很多工程实践投影。下面我们从一致性问题、特定条件下解决一致性问题的两种方法(2PC、3PC)入门,了解最基础的分布式系统理论。


一致性(consensus)

何为一致性问题?简单而言,一致性问题就是相互独立的节点之间如何达成一项决议的问题。分布式系统中,进行数据库事务提交(commit transaction)、Leader选举、序列号生成等都会遇到一致性问题。这个问题在我们的日常生活中也很常见,比如牌友怎么商定几点在哪打几圈麻将。
 
假设一个具有N个节点的分布式系统,当其满足以下条件时,我们说这个系统满足一致性: 
全认同(agreement): 所有N个节点都认同一个结果值合法(validity): 该结果必须由N个节点中的节点提出可结束(termination): 决议过程在一定时间内结束,不会无休止地进行下去


有人可能会说,决定什么时候在哪搓搓麻将,4个人商量一下就ok,这不很简单吗?

但就这样看似简单的事情,分布式系统实现起来并不轻松,因为它面临着这些问题: 
消息传递异步无序(asynchronous): 现实网络不是一个可靠的信道,存在消息延时、丢失,节点间消息传递做不到同步有序(synchronous)节点宕机(fail-stop): 节点持续宕机,不会恢复节点宕机恢复(fail-recover): 节点宕机一段时间后恢复,在分布式系统中最常见网络分化(network partition): 网络链路出现问题,将N个节点隔离成多个部分拜占庭将军问题(byzantine failure)[2]: 节点或宕机或逻辑失败,甚至不按套路出牌抛出干扰决议的信息
 
我: 老王,今晚7点老地方,搓够48圈不见不散!
……
(第二天凌晨3点) 隔壁老王: 没问题! // 消息延迟
我: ……
----------------------------------------------
我: 小张,今晚7点老地方,搓够48圈不见不散!
小张: No ……
(两小时后……)
小张: No problem! // 宕机节点恢复
我: ……
-----------------------------------------------
我: 老李头,今晚7点老地方,搓够48圈不见不散!
老李: 必须的,guang'chang'wu走起! // 拜占庭将军
(这是要打麻将呢?还是要广场舞?还是一边打麻将一边广场舞……)还能不能一起愉快地玩耍...


 
我们把以上所列的问题称为系统模型(system model),讨论分布式系统理论和工程实践的时候,必先划定模型。例如有以下两种模型: 
异步环境(asynchronous)下,节点宕机(fail-stop)异步环境(asynchronous)下,节点宕机恢复(fail-recover)、网络分化(network partition)

2比1多了节点恢复、网络分化的考量,因而对这两种模型的理论研究和工程解决方案必定是不同的,在还没有明晰所要解决的问题前谈解决方案都是一本正经地耍流氓(这句我喜欢)。

一致性还具备两个属性,一个是强一致(safety),它要求所有节点状态一致、共进退;一个是可用(liveness),它要求分布式系统24*7无间断对外服务。FLP定理(FLP impossibility)已经证明在一个收窄的模型中(异步环境并只存在节点宕机),不能同时满足 safety 和 liveness。

FLP定理是分布式系统理论中的基础理论,正如物理学中的能量守恒定律彻底否定了永动机的存在,FLP定理否定了同时满足safety 和 liveness 的一致性协议的存在。 
工程实践上根据具体的业务场景,或保证强一致(safety),或在节点宕机、网络分化的时候保证可用(liveness)。2PC、3PC是相对简单的解决一致性问题的协议,下面我们就来了解2PC和3PC。
 
2PC

2PC(tow phase commit)两阶段提交,顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):





 
2PC, phase one

在阶段1中,coordinator发起一个提议,分别问询各participant是否接受。





 
2PC, phase two

在阶段2中,coordinator根据participant的反馈,提交或中止事务,如果participant全部同意则提交,只要有一个participant不同意就中止。

在异步环境(asynchronous)并且没有节点宕机(fail-stop)的模型下,2PC可以满足全认同、值合法、可结束,是解决一致性问题的一种协议。但如果再加上节点宕机(fail-recover)的考虑,2PC是否还能解决一致性问题呢?

coordinator如果在发起提议后宕机,那么participant将进入阻塞(block)状态、一直等待coordinator回应以完成该次决议。这时需要另一角色把系统从不可结束的状态中带出来,我们把新增的这一角色叫协调者备份(coordinator watchdog)。coordinator宕机一定时间后,watchdog接替原coordinator工作,通过问询(query) 各participant的状态,决定阶段2是提交还是中止。这也要求 coordinator/participant 记录(logging)历史状态,以备coordinator宕机后watchdog对participant查询、coordinator宕机恢复后重新找回状态。

从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少。

 
3PC

3PC(three phase commit)即三阶段提交,既然2PC可以在异步网络+节点宕机恢复的模型下实现一致性,那还需要3PC做什么,3PC是什么鬼?

 在2PC中一个participant的状态只有它自己和coordinator知晓,假如coordinator提议后自身宕机,在watchdog启用前一个participant又宕机,其他participant就会进入既不能回滚、又不能强制commit的阻塞状态,直到participant宕机恢复。这引出两个疑问:

能不能去掉阻塞,使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态
当次决议中,participant间能不能相互知道对方的状态,又或者participant间根本不依赖对方的状态

相比2PC,3PC增加了一个准备提交(prepare to commit)阶段来解决以上问题:




图片截取自wikipedia

coordinator接收完participant的反馈(vote)之后,进入阶段2,给各个participant发送准备提交(prepare to commit)指令。participant接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort,3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。

participant如果在不同阶段宕机,我们来看看3PC如何应对:

阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务
阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务

因为有了准备提交(prepare to commit)阶段,3PC的事务处理延时也增加了1个RTT,变为3个RTT(propose+precommit+commit),但是它防止participant宕机后整个系统进入阻塞态,增强了系统的可用性,对一些现实业务场景是非常值得的。


小结

以上介绍了分布式系统理论中的部分基础知识,阐述了一致性(consensus)的定义和实现一致性所要面临的问题,最后讨论在异步网络(asynchronous)、节点宕机恢复(fail-recover)模型下2PC、3PC怎么解决一致性问题。
 
原文阅读:https://www.cnblogs.com/bangerlee/p/5268485.html 查看全部
狭义的分布式系统指由网络连接的计算机系统,每个节点独立地承担计算或存储任务,节点间通过网络协同工作。广义的分布式系统是一个相对的概念,正如Leslie Lamport所说:


What is a distributed systeme. Distribution is in the eye of the beholder.
To the user sitting at the keyboard, his IBM personal computer is a nondistributed system. 
To a flea crawling around on the circuit board, or to the engineer who designed it, it's very much a distributed system.
 


 
一致性是分布式理论中的根本性问题,近半个世纪以来,科学家们围绕着一致性问题提出了很多理论模型,依据这些理论模型,业界也出现了很多工程实践投影。下面我们从一致性问题、特定条件下解决一致性问题的两种方法(2PC、3PC)入门,了解最基础的分布式系统理论。


一致性(consensus)

何为一致性问题?简单而言,一致性问题就是相互独立的节点之间如何达成一项决议的问题。分布式系统中,进行数据库事务提交(commit transaction)、Leader选举、序列号生成等都会遇到一致性问题。这个问题在我们的日常生活中也很常见,比如牌友怎么商定几点在哪打几圈麻将。
 
假设一个具有N个节点的分布式系统,当其满足以下条件时,我们说这个系统满足一致性: 
  • 全认同(agreement): 所有N个节点都认同一个结果
  • 值合法(validity): 该结果必须由N个节点中的节点提出
  • 可结束(termination): 决议过程在一定时间内结束,不会无休止地进行下去



有人可能会说,决定什么时候在哪搓搓麻将,4个人商量一下就ok,这不很简单吗?

但就这样看似简单的事情,分布式系统实现起来并不轻松,因为它面临着这些问题: 
  • 消息传递异步无序(asynchronous): 现实网络不是一个可靠的信道,存在消息延时、丢失,节点间消息传递做不到同步有序(synchronous)
  • 节点宕机(fail-stop): 节点持续宕机,不会恢复
  • 节点宕机恢复(fail-recover): 节点宕机一段时间后恢复,在分布式系统中最常见
  • 网络分化(network partition): 网络链路出现问题,将N个节点隔离成多个部分
  • 拜占庭将军问题(byzantine failure)[2]: 节点或宕机或逻辑失败,甚至不按套路出牌抛出干扰决议的信息

 
我: 老王,今晚7点老地方,搓够48圈不见不散!
……
(第二天凌晨3点) 隔壁老王: 没问题! // 消息延迟
我: ……
----------------------------------------------
我: 小张,今晚7点老地方,搓够48圈不见不散!
小张: No ……
(两小时后……)
小张: No problem! // 宕机节点恢复
我: ……
-----------------------------------------------
我: 老李头,今晚7点老地方,搓够48圈不见不散!
老李: 必须的,guang'chang'wu走起! // 拜占庭将军
(这是要打麻将呢?还是要广场舞?还是一边打麻将一边广场舞……)
还能不能一起愉快地玩耍...


 
我们把以上所列的问题称为系统模型(system model),讨论分布式系统理论和工程实践的时候,必先划定模型。例如有以下两种模型: 
  • 异步环境(asynchronous)下,节点宕机(fail-stop)
  • 异步环境(asynchronous)下,节点宕机恢复(fail-recover)、网络分化(network partition)


2比1多了节点恢复、网络分化的考量,因而对这两种模型的理论研究和工程解决方案必定是不同的,在还没有明晰所要解决的问题前谈解决方案都是一本正经地耍流氓(这句我喜欢)

一致性还具备两个属性,一个是强一致(safety),它要求所有节点状态一致、共进退;一个是可用(liveness),它要求分布式系统24*7无间断对外服务。FLP定理(FLP impossibility)已经证明在一个收窄的模型中(异步环境并只存在节点宕机),不能同时满足 safety 和 liveness。

FLP定理是分布式系统理论中的基础理论,正如物理学中的能量守恒定律彻底否定了永动机的存在,FLP定理否定了同时满足safety 和 liveness 的一致性协议的存在。 
工程实践上根据具体的业务场景,或保证强一致(safety),或在节点宕机、网络分化的时候保证可用(liveness)。2PC、3PC是相对简单的解决一致性问题的协议,下面我们就来了解2PC和3PC。
 
2PC

2PC(tow phase commit)两阶段提交,顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):

116770-20160313202532507-1396598167.png

 
2PC, phase one

在阶段1中,coordinator发起一个提议,分别问询各participant是否接受。

116770-20160313203429600-179395429.png

 
2PC, phase two

在阶段2中,coordinator根据participant的反馈,提交或中止事务,如果participant全部同意则提交,只要有一个participant不同意就中止。

在异步环境(asynchronous)并且没有节点宕机(fail-stop)的模型下,2PC可以满足全认同、值合法、可结束,是解决一致性问题的一种协议。但如果再加上节点宕机(fail-recover)的考虑,2PC是否还能解决一致性问题呢?

coordinator如果在发起提议后宕机,那么participant将进入阻塞(block)状态、一直等待coordinator回应以完成该次决议。这时需要另一角色把系统从不可结束的状态中带出来,我们把新增的这一角色叫协调者备份(coordinator watchdog)。coordinator宕机一定时间后,watchdog接替原coordinator工作,通过问询(query) 各participant的状态,决定阶段2是提交还是中止。这也要求 coordinator/participant 记录(logging)历史状态,以备coordinator宕机后watchdog对participant查询、coordinator宕机恢复后重新找回状态。

从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少。

 
3PC

3PC(three phase commit)即三阶段提交,既然2PC可以在异步网络+节点宕机恢复的模型下实现一致性,那还需要3PC做什么,3PC是什么鬼?

 在2PC中一个participant的状态只有它自己和coordinator知晓,假如coordinator提议后自身宕机,在watchdog启用前一个participant又宕机,其他participant就会进入既不能回滚、又不能强制commit的阻塞状态,直到participant宕机恢复。这引出两个疑问:

能不能去掉阻塞,使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态
当次决议中,participant间能不能相互知道对方的状态,又或者participant间根本不依赖对方的状态

相比2PC,3PC增加了一个准备提交(prepare to commit)阶段来解决以上问题:
116770-20160314002734304-489496391.png

图片截取自wikipedia

coordinator接收完participant的反馈(vote)之后,进入阶段2,给各个participant发送准备提交(prepare to commit)指令。participant接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort,3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。

participant如果在不同阶段宕机,我们来看看3PC如何应对:

阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务
阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务

因为有了准备提交(prepare to commit)阶段,3PC的事务处理延时也增加了1个RTT,变为3个RTT(propose+precommit+commit),但是它防止participant宕机后整个系统进入阻塞态,增强了系统的可用性,对一些现实业务场景是非常值得的。


小结

以上介绍了分布式系统理论中的部分基础知识,阐述了一致性(consensus)的定义和实现一致性所要面临的问题,最后讨论在异步网络(asynchronous)、节点宕机恢复(fail-recover)模型下2PC、3PC怎么解决一致性问题。
 
原文阅读:https://www.cnblogs.com/bangerlee/p/5268485.html

微服务架构下的数据一致性保证

架构思想zkbhj 发表了文章 • 0 个评论 • 254 次浏览 • 2017-12-23 19:49 • 来自相关话题

本次学习的主要内容包括:

1.传统使用本地事务和分布式事务保证一致性。

2.传统分布式事务不是微服务中一致性的最佳选择。

3.微服务架构中应满足数据最终一致性原则。

4.微服务架构实现最终一致性的三种模式。

5.对账是最后的终极防线。
 
一、传统使用本地事务和分布式事务保证一致性





 
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。





 
二、传统分布式事务不是微服务中一致性的最佳选择
 
首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如,Neo4j;

基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。

可见在微服务架构中已经不能选择分布式事务了。


三、微服务架构中应满足数据最终一致性原则 
依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性。
 




当然选择了最终一致性,就要保证到最终的这段时间要在用户可接受的范围之内。

那么我们怎么实现最终一致性呢?

四、微服务架构实现最终一致性的三种模式 
从一致性的本质来看,是要保证在一个业务逻辑中包含的服务要么都成功,要么都失败。那我们怎么选择方向呢?保证成功还是保证失败呢?

我们说业务模式决定了我们的选择。实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式。


1) 可靠事件模式


可靠事件模式属于事件驱动架构,当某件重要事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此事件时,就可以完成自己的业务,也可能会引发更多的事件发布。

1. 如订单服务创建一个待支付的订单,发布一个“创建订单”的事件。





 
2.支付服务消费“创建订单”事件,支付完成后发布一个“支付完成”事件。





 
3.订单服务消费“支付完成”事件,订单状态更新为待出库。





 
从而就实现了完成的业务流程。

这个过程可能导致出现不一致的地方在于:某个微服务在更新了业务实体后发布事件却失败;虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;接受事件的微服务重复消费了事件。

可靠事件模式在于保证可靠事件投递和避免重复消费,可靠事件投递定义为(a)每个服务原子性的业务操作和发布事件(b)消息代理确保事件传递至少一次。

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。


2) 补偿模式

为了描述方便,这里先定义两个概念:

业务异常:业务逻辑产生错误的情况,比如账户余额不足、商品库存不足等。

技术异常:非业务逻辑产生的异常,如网络连接异常、网络超时等。


补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是,(1)上海-北京6月19日9点的某某航班,(2)某某酒店住宿3晚,(3)北京-上海6月22日17点火车。在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。





 
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。





 
需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性。

我们应该尽可能通过设计避免采用补偿方式,比如上面的例子中,在预订火车票失败的时候可以提示客户更改其他的时间。


 3) TCC模式(Try-Confirm-Cancel)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
 
 





1) Try:完成所有业务检查

预留必须业务资源


2) Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性


3) Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

整个TCC业务分成两个阶段完成。

 
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。

第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。


五、对账是最后的终极防线

如果有些业务由于瞬时的网络故障或调用超时等问题,通过上文所讲的3种模式一般都能得到很好的解决。但是在当今云计算环境下,很多服务是依赖于外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程。
 
出处:EAII企业架构创新研究院
原文阅读:https://mp.weixin.qq.com/s%3F_ ... 29e86
  查看全部
本次学习的主要内容包括:

1.传统使用本地事务和分布式事务保证一致性。

2.传统分布式事务不是微服务中一致性的最佳选择。

3.微服务架构中应满足数据最终一致性原则。

4.微服务架构实现最终一致性的三种模式。

5.对账是最后的终极防线。
 
一、传统使用本地事务和分布式事务保证一致性

WX20171223-182332@2x.png

 
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。

WX20171223-193729@2x.png

 
二、传统分布式事务不是微服务中一致性的最佳选择
 
首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如,Neo4j;

基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。

可见在微服务架构中已经不能选择分布式事务了。


三、微服务架构中应满足数据最终一致性原则 
依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性。
 
WX20171223-194009@2x.png

当然选择了最终一致性,就要保证到最终的这段时间要在用户可接受的范围之内。

那么我们怎么实现最终一致性呢?

四、微服务架构实现最终一致性的三种模式 
从一致性的本质来看,是要保证在一个业务逻辑中包含的服务要么都成功,要么都失败。那我们怎么选择方向呢?保证成功还是保证失败呢?

我们说业务模式决定了我们的选择。实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式


1) 可靠事件模式


可靠事件模式属于事件驱动架构,当某件重要事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此事件时,就可以完成自己的业务,也可能会引发更多的事件发布。

1. 如订单服务创建一个待支付的订单,发布一个“创建订单”的事件。

WX20171223-194142@2x.png

 
2.支付服务消费“创建订单”事件,支付完成后发布一个“支付完成”事件。

WX20171223-194232@2x.png

 
3.订单服务消费“支付完成”事件,订单状态更新为待出库。

WX20171223-194244@2x.png

 
从而就实现了完成的业务流程。

这个过程可能导致出现不一致的地方在于:某个微服务在更新了业务实体后发布事件却失败;虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;接受事件的微服务重复消费了事件。

可靠事件模式在于保证可靠事件投递和避免重复消费,可靠事件投递定义为(a)每个服务原子性的业务操作和发布事件(b)消息代理确保事件传递至少一次。

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。


2) 补偿模式

为了描述方便,这里先定义两个概念:


业务异常:业务逻辑产生错误的情况,比如账户余额不足、商品库存不足等。

技术异常:非业务逻辑产生的异常,如网络连接异常、网络超时等。



补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是,(1)上海-北京6月19日9点的某某航班,(2)某某酒店住宿3晚,(3)北京-上海6月22日17点火车。在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。

WX20171223-194440@2x.png

 
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。

WX20171223-194454@2x.png

 
需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性。

我们应该尽可能通过设计避免采用补偿方式,比如上面的例子中,在预订火车票失败的时候可以提示客户更改其他的时间。


 3) TCC模式(Try-Confirm-Cancel)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
 
 
WX20171223-194636@2x.png


1) Try:完成所有业务检查

预留必须业务资源


2) Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性


3) Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

整个TCC业务分成两个阶段完成。


 
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。

第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。


五、对账是最后的终极防线

如果有些业务由于瞬时的网络故障或调用超时等问题,通过上文所讲的3种模式一般都能得到很好的解决。但是在当今云计算环境下,很多服务是依赖于外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程。
 
出处:EAII企业架构创新研究院
原文阅读:https://mp.weixin.qq.com/s%3F_ ... 29e86
 

简单理解一致性哈希算法(consistent hashing)

架构思想zkbhj 发表了文章 • 0 个评论 • 291 次浏览 • 2017-11-04 20:14 • 来自相关话题

一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
 
 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
 
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图





 
把数据通过一定的hash算法处理后映射到环上
 
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;





 

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;





 

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
 
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:





 

2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:






    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
 
平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。

以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:





 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:





 
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
 
原文链接:http://blog.csdn.net/cywosp/ar ... 97179 查看全部
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
 
 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
 
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

20140411000507734.png

 
把数据通过一定的hash算法处理后映射到环上
 
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;

20140411000620656.png

 

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;

20140411000853609.png

 

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
 
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:

20140411001033656.png

 

2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:

20140411001211062.png


    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
 
平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。


    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。


以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:

20140411001433375.png

 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:

20140411001540656.png

 
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
 
原文链接:http://blog.csdn.net/cywosp/ar ... 97179
  集中记录了编程开发过程中可以被用到的思想和架构,用于指导以后的编程更加规范高效。