架构之重构的12条军规

最后更新于:2022-04-01 01:57:20

作者 崔康 【注】架构之重构的12条军规(上)发布以后,一些读者着急要下篇,所以在这里我把上下篇合并成一篇,让大家可以阅读完整版,不用分开看了。 对于开发者来说,架构设计是软件研发过程中最重要的一环,所谓没有图纸,就建不了房子。在遍地App的互联网时代,架构设计有了一些比较成熟的模式,开发者和架构师也可以经常借鉴。 但是,随着应用的不断发展,最初的架构往往面临着各种问题,比如无法满足客户的需求、无法实现应用的扩展、无法实现新的特性等等。在这种情况下,我们如何避免一些坑,尽量比较成功地实现架构的重构,是很多开发者和架构师亟需解决的问题。 ![2015-08-04/55c0321ba5111](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0321ba5111.png) 在这里,跟大家分享一下Uber的工程主管Raffi Krikorian的12条规则,并附上一些解读,希望对大家有所启发。 **确定重构的目的和必要性** 看起来这个规矩有些多余,但是请不要忽略。每一次架构的重构都是“伤筋动骨”,就像做手术一样,即使再成功,也会伤元气,所以决策者们首先要分析架构重构的理由和其他备选方案,明确重构的目的是为了满足业务需求,并且是不得不做的最佳方案,然后再考虑其他问题。 有时候,经过分析就会发现,也许还有其他解决方案,比如增加计算资源,或者重构的目的不是为了业务需求,那就没有必要做了。 检查清单: * 架构重构的原因是什么,是为了满足业务的需要还是只是觉得架构不好看? * 除了架构重构之外,还有其他备选方案吗?是否都分析过这些方案的利弊? **定义“重构完成”的界限** 如果确定要重构,那么要把目标明确下来,也就是重构的边界条件,怎么才算是“完成”了重构,目标要有数据量化,或者有能够测试的办法。这也是一个需求分析的过程,如果需求不明确,那么规格说明书没法写清楚,负责重构的团队也没有明确的目标,不能以重构的时间或者主观的判断为结束的依据。前几天和一朋友聊天,他最近在负责系统的性能优化,也要做一些重构的事情,开始的时候团队的目标不明确,大家不知道优化到什么程度,所以不敢下手。如果目标是提高10%,那么可以从细节处着手;如果是提高50%,那可能要搞大动作才能实现了。后来目标明确之后,团队才找到合适的办法。 检查清单: * 重构的目标可以量化,或者说可以测试吗? * 重构完成的标准是什么?得到业务部门或者领导的认可了吗? **渐进式重构** 现在软件研发最流行的就是快速迭代、持续交付、尽早反馈。这同样可以用在架构的重构上,重构过程的难度不亚于构建一个新产品,所以在设计重构的时候,要引入持续交付的流程,每一个重构步骤或者模块都要快速部署并得到反馈,以便评估重构的效果,及时作出策略调整。有的读者会说,我们的架构重构是釜底抽薪型的,没法渐进,只能一蹴而就。如果是这种情况,可以考虑在另外一套拷贝的系统中做重构,经过谨慎测试之后,将数据和业务迁移过去。 检查清单: * 能否把重构过程分成小的迭代,每一次改进都能尽快得到反馈? * 重构过程中的效果能够定期展示给业务部门或者领导吗? **确定当前的架构状态** 在启动重构之前,团队要对当前的架构状态有清晰的了解,也就是设定好基准,以便评估重构的效果。据我的经验,负责重构的架构师或者开发者,往往还没有搞清楚现有的架构设计,就开始重构了,结果经常出现这样的情况:重构到某个阶段,发现行不通,然后一拍脑袋说,哦,原来这块的架构是这个样的,是为了达到某某业务需求啊,这块不能动,得想别的办法。类似的例子在研发团队中时有发生,也提醒我们要慎重小心。记得有位哲人说过,了解别人很容易,了解自己很难。 检查清单: * 你了解当前的架构设计吗?它的设计初衷和之前的选型方案知道吗? * 你能给架构设定一个基准状态吗? **不要忽略数据** 数据的重要性不言而喻,业务都是以数据流为载体的,所以架构重构的本质就是对于数据流的重构。数据对重构的重要性主要体现在两个方面:在重构设计时,需要考虑业务数据的需求,重构之后的系统对于数据的存储、处理、分析等功能是否有影响;在重构过程中,考虑依靠数据甚至是实际的数据来验证重构的效果,提供评估的支持。 检查清单: * 业务数据的需求在重构设计中有体现吗? * 重构过程中能否通过实际数据来验证效果? **管理好技术债务** 技术债务在平常的软件研发过程中也是比较突出的问题,现在单独拿出来强调是希望提醒开发者们:架构重构往往是为了偿还技术债务,所以请不要在偿还技术债务的过程中制造技术债务了。技术债务就像信用卡一样,会有很高的利息率,就如同给团队留下了大量的帐务开销。组织应该培养一种保证设计质量的文化。应当鼓励重构、同时也应当鼓励持续设计以及其它有关代码质量的实践。在开发时间中应当专门抽出一部分以解决技术债务。如果没有合适的照料,那么真实世界中的代码会变得越来越复杂难懂。 检查清单: * 团队对技术债务有跟踪和备忘录机制吗?还是开发人员可以随意的产生债务? * 针对技术债务有定期的培训、回顾机制吗? **远离那些虚荣的东西(例如使用“热门”的技术栈)** 架构的重构过程应该是以目标为导向,换句话说“注重实效”。对于技术人来说,一个经常被轻视的问题在于,喜欢追逐新鲜的热门技术,这其实是个好事情,说明技术人勇于创新,不断接受新技术。但是对于架构的重构这样的关键性任务来说,是不是新技术并不重要,重要的是能不能实现重构的目标。对于新技术来说,虽然热度大,但是人才储备还不足,大家踩过的坑还不多,积累的失败教训和成功经验还不够,在这种情况下,建议大家不要头脑一热就上马新技术,应该客观冷静地评估新技术和成熟技术对架构重构的影响和效果,以数据和经验来说话,而不要追赶时髦。 检查清单: * 重构的技术选型是否有详实的数据和专家评估? * 选用的技术是否有良好的人才积累和足够的经验支持?你是不是实验小白鼠? * 在技术选型时,是否至少有两个方案待评估?有没有成熟的技术方案? **做好准备面对压力** 这条军规更像是对架构师们的心理建议,软件开发过程中,压力无处不在。对于架构重构来说,压力来源于多个方面:管理层、团队成员、同级部门等等。说白了,架构重构对个人来说往往是一件出力不讨好的事情。和做一个新产品能够取得很高的赞赏相比,重构的成绩往往并不受领导重视,而且出了问题还要承担很大的责任。从软件开发角度看,做新产品是从0到1,而架构重构是从-1到1,复杂性和难度通常更大。因此,重构的负责人要提前做好心理准备,舒缓压力的一个技巧是,设置好里程碑,将重构的成果量化,并且和业务的变化关联起来,定期向利益相关各方同步状态,得到大家的理解和支持。 检查清单: * 架构的重构是否得到了管理层(特别是最高管理层)的支持?他们是否对重构的时间、任务量有直接的认识? * 你的重构计划中是否包含了一些可以量化的成果?是否定期向管理层展示这些成果? **了解业务** 虽然看起来像是一句废话,但是我想Raffi Krikorian特意把这条提出来一定是有理由的。架构重构的最终目的是改进业务,所以对于业务的了解将有助于架构师和技术人确定重构目标的优先级和关键路径。比如,我们需要知道哪些关键业务的架构是不能碰的,哪些业务之间是互相关联的,哪些业务的架构是需要优先重构的.....等等。除了了解业务本身,我们还需要了解“人”,表面上管理层是重构目标的裁决者,但实际上业务部门的人才是。技术人需要了解他们的业务需求,并将其转化为重构目标。通过这种方式,架构重构的意义才能得到具体的体现。 检查清单: * 是否与业务部门就架构重构所能实现的业务目标进行过充分的讨论和确认? * 是否对关键业务和优先重构的业务进行了确认? **做好面对非技术因素的准备** 恩......这又是一个不那么让人舒服的建议。不管你是否愿意相信,技术在架构重构(以及其他很关键的公司决策中)的影响因素中并不是最高的,我们还会涉及到商业利益、管理层偏好、大客户影响、办公室zhengzhi、站队问题等等,对于架构师和技术人来说,这些因素往往不是他们所能掌控的。我们能做的就是,与利益相关者设定重构目标,然后,根据不同的影响因素,调整目标。请记住,不要死扛这个目标,当有人提出不同的意见时,要坦诚地和他们交流,并告知他们如何采纳意见,那么重构目标会有变化,然后让其他利益相关者也知道这些变化。非技术因素的影响是客观存在的,而且从商业层面来说也是合理的,所以对于技术人来说要学会适应。 检查清单: * 当非技术因素影响架构的重构时,你是否对目标做了调整并告知了利益相关各方? * 你是否准备以开放而不是抵制的心态来对待非技术因素的影响? **对于代码质量有所掌握** 这和上篇中所提到的“管理好技术债务”有异曲同工之处。架构的重构对代码质量要求很高,一方面是重构过程对bug的容忍性比新产品的研发更低,另一方面也决定了下一次重构的难易程度。关于代码质量的书籍和文章已经有很多,在这里只想提醒大家一点:代码审查是一个非常好的办法。代码审查是软件开发过程中的必要步骤,既可以帮助被审查者提到代码质量,又可以让审查者加深对产品的理解。不论团队多忙,一定要保证代码提交之前,是经过其他成员审核过的,短期来看会占用团队的时间,长期来看是事半功倍的好事。 检查清单: * 团队成员是否对代码质量有足够的重视?是否有奖惩措施? * 团队内部是否有代码质量的标准文档和审查流程? **让团队做好准备** 这是Raffi Krikorian列举的最后一条军规,是对之前所有建议的总结,我在这里不做解读了,请大家自我感觉吧。 **结尾** 关于架构的重构,Raffi Krikorian给了很好的建议,不过到底有没有效果,还是要实践中检验。尽信书不如无书,来源于实践中的经验是最有价值的,为技术人所用才有意义。
';

Java 20年:转角遇到Go

最后更新于:2022-04-01 01:57:18

作者 郭蕾 1995年,横空出世的Java语言以其颠覆式的特性迅速获得了开发者的关注。跨平台、垃圾回收、面向对象,这在当时都是不可思议的事情,而Java却完美地在一门语言中实现了这一特性。可以说,Java将编程语言设计带领到一个新的高度。20年后的今天,当年的那些新特性已经不再是什么新鲜词。同时,又会有一些新的语言宣称自己有一些颠覆性的特性,其中Go语言就是新语言的一个代表,它部署简单、并发性好,在语言设计上确实优于Java。为了了解Java和Go语言的发展现状与趋势,InfoQ采访了Go语言大牛郝林。 **InfoQ:今年的5月23日是Java的第20岁生日,转眼间,Java已经走过了20年,版本号也已经更新到Java 8。你怎么看Java这门语言?在这20年里,有哪些对你印象比较深刻的Java事件?** > **郝林:**我觉得Java语言一路走来赚足了眼球也惹来了众多非议。就拿它随着Sun公司的没落被流转到Oracle公司来说吧。我记得当时有一大批Java程序员在网上扬言要摒弃Java语言,并且一部分人真的这么做了。但事实证明,Oracle更好地发展了Java。我认为从Java 7开始这门语言相当于迎来了第二春,在发展上增速了不少,各种新鲜特性和类库层出不穷。Java 8给我印象最深刻的就是对Lambda表达式的支持。这使得Java真正地对函数式编程提供了支持。这是质的改变。也终将使Java语言走得更远。 **InfoQ:从版本迭代的角度看,你认为Java的发展经历了哪几个阶段?** > **郝林:**我是从Java 1.3的末期开始接触它的。所以在我看来Java 1.3之前就属于萌芽期吧(虽然那时它已被广泛使用了)。从1.4开始,Java语言有了很多改观,比如NIO、更多的垃圾回收器、性能上的提升、Java EE规范的逐步简化,等等。所以我认为从此Java进入了第一个高速发展期(也许有上一个但我没赶上)。到了Java 6的时候,发展速度其实已经减缓不少了。这也可能是由于Java正处于被交接阶段的缘故。不过,我不得不说,Oracle的调整动作很快,在几乎没有什么断档的情况下,Java的发展又开始“跑”起来了。这也是我在前一个回答中说的“第二春”。 **InfoQ:JVM的普及促使相关周边语言不断涌现,你怎么看这些JVM语言?** > **郝林:**这就是Java真正牛的地方。它不单单是一门语言,更是一个平台。到目前为止,JVM语言已经有很多了,但是发展最好的是Scala。它解决了一些Java在程序开发方面的问题。但是,我认为它的方向有所偏颇。我觉得“简化”往往比“丰富”来得更直接,效果也会更好。相比之下,Clojure语言就做得很好。但是由于它是一个Lisp语言的方言,编码方式和思维方式与Java的面向对象思想相去甚远,所以仅仅被一小部分Java程序员接受。总之,JVM语言让Java更加流行了。它们虽不完美,但却功不可没。 **InfoQ:很多人都在唱衰Java,您能结合Java的发展现状和趋势谈谈Java的前景吗?** > **郝林:**任何一个流行的技术都会有人唱衰,更何况Java已经发展了20年了,中间又经历了种种坎坷。我觉得Java 9又会是一个里程碑式的版本。我很期待。我认为在我可预见的未来Java不会没落。实际上,Java语言在企业级软件领域的霸主地位是不可动摇的。在互联网软件领域,它虽然受到了各种开发成本更低的语言(比如Ruby和Python)的不断侵蚀,但是仍然占有一席之地。这正说明了Java生命力的顽强。不过,相比于Java语言,我更看好Java作为一个平台的前景。 **InfoQ:你什么时候开始接触Go语言的?相比于Java语言,它有哪些优势?** > **郝林:**我接触Go语言实际上并不算早,大约在2013年的上半年。那时候Go语言的版本是1.0,1.1版本正处于开发期。Go语言给我的第一印象就是支持多种编程范式、提供了给力的程序构建和发布工具,以及在并发编程方面的极度简化。在当时,我认为Java语言的不足恰恰就包括了这几个方面。所以我义无返顾的开始学习并使用Go语言。事实证明,Go语言虽属于新兴语言,但它却是一种革新。另外,与Java语言一样,Go语言的向后兼容做的很好。并且,为了以防万一,它提供了一个命令用于自动地把旧版本的Go语言程序源码调整为当前版本的源码。诸如此类的“便捷大法”还有很多。许多在Java世界中只能依靠额外的类库或工具才能完成的事情,在Go语言看来却是手到擒来。当然,这种实实在在的优势也有诞生时间不同的缘故。正是由于Java已经历经了太多,所以在很多方面都很难改变。我觉得这是所有编程语言都应该正视的问题。显然,Go语言的创造者们已经意识到了这一点。 **InfoQ:出色的并发性能是Go语言区别于其他语言的一大特色。相比于Java的并发编程,它有哪些显著性的优势?** > **郝林:**说到并发,Go语言给人们的第一印象就是便捷。在这便捷之下,Go语言权衡了各方面利弊,做了大量的工作,使得我们用极低的开发成本就可以编写出拥有超高运行性能的Go语言并发程序。其中最大的亮点就是,Go语言把“激活”需要并发执行的代码块的操作内置了。我们仅通过一个关键字“go”就可以轻易地完成这项操作。 > > 还记得我们在Java中为此需要编写的代码是多么的冗长吗?侵入式的接口实现声明和类继承声明、复杂的匿名内部类,以及困难重重的线程间协调和调度。这些都是不可忽视的程序开发维护成本。我们在编写和修改这样的并发程序时都要保持头脑和思路的绝对清晰,否则就会埋下祸根,搞出不易察觉和定位的Bug。另一方面,如果透过表象看本质的话,我们就可以看到Go语言为了程序员的方便而做的大量的工作。 > > 笼统地讲,Go语言把对内核线程的使用和调度操作都内置到其运行时系统中了。但是,它远远要比一个线程池复杂得多。Java线程与内核线程之间关系是1:1的。而Go语言的Goroutine(可以看做是Go语言中执行并发代码块的实体)与内核线程之间的关系是M:N的。这让我们可以使用成千上万个Goroutine去执行并发代码块而仅仅耗费极少的内核线程。关于Go并发编程更详细的介绍,大家可以参看我著的“图灵原创”图书《Go并发编程实战》。 **InfoQ:Java和Go语言的使用场景是不是不一样?** > **郝林:**Java语言与Go语言在使用场景方面其实有很多相似之处。例如,它们都适用于服务端程序的构建,并且可以很容易地编写出页面模板文件。又例如,它们在桌面软件方面都比较捉襟见肘。有意思的是,就本身而言,Go语言在适用领域的优势更强,而在不适用领域的劣势也更加明显。优势方面我就不再赘述了,下面说说劣势。比如,用Java编写桌面程序起码还有Swing和JavaFX可选,但是Go语言官方至今还没有一个成熟的解决方案。当然,这仍旧与诞生时间有关。另外,我们还可以用Java语言编写Android应用程序。Go语言目前虽然已经涉足,但还不完美。不过我在这里爆料一下,我很期待能用Go语言编写iOS应用程序。实际上,Go语言在这方面已经有所进展了。总之,两种语言在适用领域方面有所重叠但又有些不同。在很多情况下,我们可以混用这两种语言。 **InfoQ:现在的开发语言特别多,Java、Go、PHP、Rust、Python等,你认为未来语言的发展趋势是怎么样的?** > **郝林:**的确,现在的编程语言层出不穷、多如牛毛。但是编程语言的兴衰是有规律可循的。第一个规律是顺应时代的语言才能有更好的发展。正如Objective-C因iPhone和iPad的诞生而变得火热至极那样。而Java也因Google公司的“横插一足”而在移动程序开发领域占领了制高点。当今的计算机世界正处于“云”的时代,而从处理器的角度看也正处于多核时代。谁能够更好地把握住这些时代标签,谁就会在发展上更具优势。当然,这里说的“把握住”是需要有真功夫的。只喊不练不起任何作用,而且还会遭人唾弃。第二个规律是能够解决问题的语言就是好语言。对于任何场景都是如此。我相信每个技术团队都会在选择编程语言时进行一番权衡。哪种编程语言能更快更好地解决问题(这也涉及到开发和维护成本),它就肯定会胜出。从这方面看,编程语言并没有好坏之分。它们都必有独特的优势和擅长做的事情,否则就根本不会诞生出来了。而问题的解决能力几乎是发展趋势的唯一评判标准。“多快好省”就是选择编程语言的要诀。这也会从侧面预示一个编程语言的发展趋势。说了这么多,我另一个想要表达的意思是:对于它们的未来,我无法预知:)。 ### 受访嘉宾介绍 郝林,软件工程师,从事软件开发工作9年有余。既搞过企业级软件项目,也堆过互联网软件系统。近期在使用和推广Go语言,著有“图灵原创”图书《Go并发编程实战》,以及在线免费教程《Go语言第一课》和《Go命令教程》 。
';

观点|Opinion

最后更新于:2022-04-01 01:57:16

';

戏(细)说Executor框架线程池任务执行全过程(下)

最后更新于:2022-04-01 01:57:14

作者 张超盟 [上一篇文章](http://www.infoq.com/cn/articles/executor-framework-thread-pool-task-execution-part-01)中通过引入的一个例子介绍了在Executor框架下,提交一个任务的过程,这个过程就像我们老大的老大要找个老大来执行一个任务那样简单。并通过剖析ExecutorService的一种经典实现ThreadPoolExecutor来分析接收任务的主要逻辑,发现ThreadPoolExecutor的工作思路和我们带项目的老大的工作思路完全一致。在本文中我们将继续后面的步骤,着重描述下任务执行的过程和任务执行结果获取的过程。会很容易发现,这个过程我们更加熟悉,因为正是每天我们工作的过程。除了ThreadPoolExecutor的内部类Worker外,对执行内容和执行结果封装的FutureTask的表现是这部分着重需要了解的。 为了连贯期间,内容的编号延续上篇。 ### 2. 任务执行 其实应该说是任务被执行,任务是宾语。动宾结构:execute the task,执行任务,无论写成英文还是中文似乎都是这样。那么主语是是who呢?明显不是调用submit的那位(线程),那是哪位呢?上篇介绍ThreadPoolExecutor主要属性时提到其中有一个HashSet workers的集合,我们有说明这里存储的就是线程池的工作队列的集合,队列的对象是Worker类型的工作线程,是ThreadPoolExecutor的一个内部类,实现了Runnable接口: ~~~ private final class Worker implements Runnable ~~~ 8)  看作业线程干什么当然是看它的run方法在干什么。如我们所料,作业线程就是在一直调用getTask方法获取任务,然后调用 runTask(task)方法执行任务。看到没有,是在while循环里面,就是不干完不罢休的意思!在加班干活的苦逼的朋友们,有没有遇见战友的亲切感觉? ~~~ public void run() { try { Runnable task = firstTask; //循环从线程池的任务队列获取任务 while (task != null || (task = getTask()) != null) { //执行任务 runTask(task); task = null; } } finally { workerDone(this); } } ~~~ 然后简单看下getTask和runTask(task)方法的内容。 9) getTask方法是ThreadPoolExecutor提供给其内部类Worker的的方法。作用就是一个,从任务队列中取任务,源源不断地输出任务。有没有想到老大手里拿的总是满满当当的project,也是源源不断的。 ~~~ Runnable getTask() { for (;;) { //从任务队列的头部取任务 r = workQueue.take(); return r; } } ~~~ 10) runTask(Runnable task)是工作线程Worker真正处理拿到的每个具体任务。看到这里才可用确认我们的猜想,之前提到[[y1]](http://infoqhelp.sinaapp.com/architectgen#_msocom_1) 的“执行任务”这个动宾结构前面的主语正是这些Worker呀。唠叨了半天(看主要方法都看到了整整第10个了),前面都是派活,这里才是干活。和我们的工作何其相似!老大(LD),老大的老大(LD^2),老大的老大(LD^n) 非常辛苦,花了很多时间、精力在会议室、在project上想着怎么生成和安排任务,然而真的轮到咱哥们干活,可能花了不少时间,但看看流程就是这么简单。**三个大字:“****Just do it****”。** ~~~ private void runTask(Runnable task) { //调用任务的run方法,即在Worker线程中执行Task内定义内容。 task.run(); } ~~~ 需要注意的地方出现了,调用的其实是task的run方法。看下FutureTask的run方法做了什么事情。 这里插入一个FutureTask的类图。可以看到FutureTask实现了RunnableFuture接口,所以FutureTask即有Runnable接口的run方法来定义任务内容,也有Future接口中定义的get、cancel等方法来控制任务执行和获取执行结果。Runnable接口自不用说,Future接口的伟大设计,就是使得实现该接口的对象可以阻塞线程直到任务执行完毕,也可以取消任务执行,检测任务是执行完毕还是被取消了。想想在之前我们使用Thread.join()或者Thread.join(long millis)等待任务结束是多么苦涩啊。 FutureTask内部定义了一个Sync的内部类,继承自AQS,来维护任务状态。关于AQS的设计思路,可以参照参考Doug Lea大师的原著[_The java_._util_._concurrent Synchronizer Framework_](http://gee.cs.oswego.edu/dl/papers/aqs.pdf)。 ![2015-08-04/55c030e76f19f](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c030e76f19f.jpg) 11) 和其他的同步工具类一样,FutureTask的主要工作内容也是委托给其定义的内部类Sync来完成。 ~~~ public void run() { //调用Sync的对应方法 sync.innerRun(); } ~~~ 12)   FutureTask.Sync.innerRun(),这样做的目的就是为了维护任务执行的状态,只有当执行完后才能够获得任务执行结果。在该方法中,首先设置执行状态为RUNNING只有判断任务的状态是运行状态,才调用任务内封装的回调,并且在执行完成后设置回调的返回值到FutureTask的result变量上。在FutureTask中,innerRun等每个“写”方法都会首先修改状态位,在后续会看到innerGet等“读”方法会先判断状态,然后才能决定后续的操作是否可以继续。下图是FutureTask.Sync中几个重要状态的流转情况,和其他的同步工具类一样,状态位使用的也是父类AQS的state属性。 ![2015-08-04/55c030f1cc7e6](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c030f1cc7e6.png) ~~~ void innerRun() { //通过对AQS的状态位state的判断来判断任务的状态是运行状态,则调用任务内封装的回调,并且设置回调的返回值 if (getState() == RUNNING) innerSet(callable.call()); } void innerSet(V v) { for (;;) { int s = getState(); //设置运行状态为完成,并且把回调额执行结果设置给result变量 if (compareAndSetState(s, RAN)) { result = v; releaseShared(0); done(); return; } } ~~~ 至此工作线程执行Task就结束了。提交的任务是由Worker工作线程执行,正是在该线程上调用Task中定义的任务内容,即封装的Callable回调,并设置执行结果。下面就是最重要的部分:调用者如何获取执行的结果。让你加班那么久,总得把成果交出来吧。老大在等,因为老大的老大在等! ### 3. 获取执行结果 前面说过,对于老大的老大这样的使用者来说,获取执行结果这个过程总是最容易的事情,只需调用FutureTask的get()方法即可。该方法是在Future接口中就定义的。get方法的作用就是等待执行结果。(Waits if necessary for the computation to complete, and then retrieves its result.)Future这个接口命名得真好,虽然是在未来,但是定义有一个get()方法,总是“可以掌控的未来,总是有收获的未来!”实现该接口的FutureTask也应该是这个意思,在未来要完成的任务,但是一样要有结果哦。 13)  FutureTask的get方法同样委托给Sync来执行。和该方法类似,还有一个V get(long timeout, TimeUnit unit),可以配置超时时间。 ~~~ public V get() throws InterruptedException, ExecutionException { return sync.innerGet(); } ~~~ 14)  在Sync的 innerGet方法中,调用AQS父类定义的获取共享锁的方法acquireSharedInterruptibly来等待执行完成。如果执行完成了则可以继续执行后面的代码,返回result结果,否则如果还未完成,则阻塞线程等待执行完成。[[bd2]](http://infoqhelp.sinaapp.com/architectgen#_msocom_2) 再大的老大要想获得结果也得等老子干完了才行!可以看到调用FutureTask的get方法,进而调用到该方法的一定是想要执行结果的线程,一般应该就是提交Task的线程,而这个任务的执行是在Worker的工作线程上,通过AQS来保证执行完毕才能获取执行结果。该方法中acquireSharedInterruptibly是AQS父类中定义的获取共享锁的方法,但是到底满足什么条件可以成功获取共享锁,这是Sync的tryAcquireShared方法内定义的。[[bd3]](http://infoqhelp.sinaapp.com/architectgen#_msocom_3) 具体说来,innerIsDone用来判断是否执行完毕,如果执行完毕则向下执行,返回result即可;如果判断未完成,则调用AQS的doAcquireSharedInterruptibly来挂起当前线程,一直到满足条件。这种思路在其他的几种同步工具类[Semaphore](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html)、[CountDownLatch](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CountDownLatch.html)、[ReentrantLock](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantLock.html)、[ReentrantReadWriteLock](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html)也广泛使用。借助AQS框架,在获取锁时,先判断当前状态是否允许获取锁,若是允许则获取锁,否则获取不成功。获取不成功则会阻塞,进入阻塞队列。而释放锁时,一般会修改状态位,唤醒队列中的阻塞线程。每个同步工具类的自定义同步器都继承自AQS父类,是否可以获取锁根据同步类自身的功能要求覆盖AQS对应的try前缀方法,这些方法在AQS父类中都是只有定义没有内容。可以参照《[源码剖析AQS在几个同步工具类中的使用](http://www.idouba.net/sync-implementation-by-aqs/)》来详细了解。 突然想到想想那些被称为老大的,是不是整个career流程就是只干两件事情:submit a task, then wait and get the result。不对,还有一件事情,不是等待,而是催。“完了没,完了没?schedule很紧的,抓点紧啊,要不要适当加点班啊……” ~~~ V innerGet() throws InterruptedException, ExecutionException { //获得锁,表示执行完毕,才能获得后执行结果,否则阻塞等待执行完成再获取执行结果 acquireSharedInterruptibly(0); return result; } protected int tryAcquireShared(int ignore) { return innerIsDone()? 1 : -1; } ~~~ 至此,获得执行结果,圆满完成任务! 老大的老大,拍着咱们老大的肩膀(或者深情的抚摸着咱们老大唏嘘胡茬的脸庞)说:“亲,你这活干的漂亮!”而隔壁桌座位的几个兄弟,刚熬了几个晚上加班交付完这波task后,发现任务队列里又有新任务了,俺们老大又从他的另外一个老大手里接来的任务了。每个人都按照这样的角色进行着,依照这样的角色安排和谐愉快地进行着。。。 选择合适的任务执行服务,如可以根据需要选择ThreadPoolExecutor还是ScheduledThreadPoolExecutor,并定制ExecutorService的配置。 定义好任务的工作内容和结果类型,提交任务,等待任务的执行结果 | 角色名 | 任务用户 | 任务管理者 | 任务执行者 | |--|---|---|---| | **角色属性** | 任务的甲方 | 任务的乙方 | 乙方的工具 | | **角色说明** | 接收提交的任务;维护执行服务内部管理;配置工作线程执行任务 | 每个工作线程一直从任务执行服务获取待执行的任务,保证任务完成后返回执行结果 | | | **Executor中对应** | 创建获取ExecutorService、并提交Task的外部接口 | ExecutorService的各种实现。 | 执行服务内定义的配套的Worker线程。如ThreadPoolExecutor.Worker | | **主要接口方法** | submit(Callable task) | execute(Runnable command) | runTask(Runnable task) | | **现实角色映射** | 手里有活的大老大 | 领人干活的老大 | 真正干活的码农 | | **主要工作伪代码** | taskService = createService() future=taskService.submitTask() future.get() | executeTask() { addTask() createThread() } | while(ture) { getTask() runTask() } | ## 四、 总结 从时序图上看主要的几个角色是这样配合完成任务提交、任务执行、获取执行结果这几个步骤的。 ![2015-08-04/55c0311d30d59](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0311d30d59.png) 1. 外面需要提交任务的角色(如例子中老大的老大),首先创建一个任务执行服务ExecutorService,一般使用工具类Executors的若干个工厂方法 创建不同特征的线程池ThreadPoolExecutor,例子中是使用newFixedThreadPool方法创建有n个固定工作线程的线程池。 2. 线程池是专门负责从外面接活的老大。把任务封装成一个FutureTask对象,并根据输入定义好要获得结果的类型,就可以submit任务了。 3. 线程池就像我们团队里管人管项目的老大,各个都有一套娴熟、有效的办法来对付输入的任务和手下干活的兄弟一样,内部有一套比较完整、细致的任务管理办法,工作线程管理办法,以便应付输入的任务。这些逻辑全部在其execute方法中体现。 4. 线程池接收输入的task,根据需要创建工作线程,启动工作线程来执行task。 5. 工作线程在其run方法中一直循环,从线程池领取可以执行的task,调用task的run方法执行task内定义的任务。 6. FutureTask的run方法中调用其内部类Sync的innerRun方法来执行封装的具体任务,并把任务的执行结果返回给FutureTask的result变量。 7. 当提及任务的角色调用FutureTask的get方法获取执行结果时,Sync的innerGet方法被调用。根据任务的执行状态判断,任务执行完毕则返回执行结果;未执行完毕则等待。 还记得我们费了半天劲试图找出任务执行时那个动宾结构的主语吗?从示例上看更像是线程池在向外提供任务执行的服务。就像我们的老大在代表我们接收任务、执行任务、提交执行结果。明显我们这些真正的Worker成了延伸,有点搞不懂到底我们是主语,还是主语延伸的工具,就像定义ThreadPoolExecutor的内部类Worker一样。我们只是工具,不是主语,是状语: execute the task by workers。突然想到毛主席当年的“数风流人物,还看今朝”,说的应该是这些Worker的劳苦大众吧,怎么都今朝这么久了,俺们这些Woker们还是风流不起来呢?风骚的作者居然在上面严肃的时序图上加了个风骚的小星星,向同行的Worker们致敬!
';

戏(细)说Executor框架线程池任务执行全过程(上)

最后更新于:2022-04-01 01:57:11

作者 张超盟 ## 一、前言 1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。 经过这样的封装,对于使用者来说,提交任务获取结果的过程大大简化,调用者直接从提交的地方就可以等待获取执行结果。而封装最大的效果是使得真正执行任务的线程们变得不为人知。有没有觉得这个场景似曾相识?我们工作中当老大的老大(且称作LD^2)把一个任务交给我们老大(LD)的时候,到底是LD自己干,还是转过身来拉来一帮苦逼的兄弟加班加点干,那LD^2是不管的。LD^2只用把人描述清楚提及给LD,然后喝着咖啡等着收LD的report即可。等LD一封邮件非常优雅地报告LD^2report结果时,实际操作中是码农A和码农B干了一个月,还是码农ABCDE加班干了一个礼拜,大多是不用体现的。这套机制的优点就是LD^2找个合适的LD出来提交任务即可,接口友好有效,不用为具体怎么干费神费力。 ## 二、 一个最简单的例子 看上去这个执行过程是这个样子。调用这段代码的是老大的老大了,他所需要干的所有事情就是找到一个合适的老大(如下面例子中laodaA就荣幸地被选中了),提交任务就好了。 ~~~ // 一个有7个作业线程的线程池,老大的老大找到一个管7个人的小团队的老大 ExecutorService laodaA = Executors.newFixedThreadPool(7); //提交作业给老大,作业内容封装在Callable中,约定好了输出的类型是String。 String outputs = laoda.submit( new Callable<String>() { public String call() throws Exception { return "I am a task, which submited by the so called laoda, and run by those anonymous workers"; } //提交后就等着结果吧,到底是手下7个作业中谁领到任务了,老大是不关心的。 }).get(); System.out.println(outputs); ~~~ 使用上非常简单,其实只有两行语句来完成所有功能:创建一个线程池,提交任务并等待获取执行结果。 例子中生成线程池采用了工具类Executors的静态方法。除了newFixedThreadPool可以生成固定大小的线程池,newCachedThreadPool可以生成一个无界、可以自动回收的线程池,newSingleThreadScheduledExecutor可以生成一个单个线程的线程池。newScheduledThreadPool还可以生成支持周期任务的线程池。一般用户场景下各种不同设置要求的线程池都可以这样生成,不用自己new一个线程池出来。 ## 三、代码剖析 这套机制怎么用,上面两句语句就做到了,非常方便和友好。但是submit的task是怎么被执行的?是谁执行的?如何做到在调用的时候只有等待执行结束才能get到结果。这些都是1.5之后Executor接口下的线程池、Future接口下的可获得执行结果的的任务,配合AQS和原有的Runnable来做到的。在下文中我们尝试通过剖析每部分的代码来了解Task提交,Task执行,获取Task执行结果等几个主要步骤。为了控制篇幅,突出主要逻辑,文章中引用的代码片段去掉了异常捕获、非主要条件判断、非主要操作。文中只是以最常用的ThreadPoolExecutor线程池举例,其实ExecutorService接口下定义了很多功能丰富的其他类型,有各自的特点,但风格类似。本文重点是介绍任务提交的过程,过程中涉及的ExecutorService、ThreadPoolExecutor、AQS、Future、FutureTask等只会介绍该过程中用到的内容,不会对每个类都详细展开。 ### 1、 任务提交 从类图上可以看到,接口ExecutorService继承自Executor。不像Executor中只定义了一个方法来执行任务,在ExecutorService中,正如其名字暗示的一样,定义了一个服务,定义了完整的线程池的行为,可以接受提交任务、执行任务、关闭服务。抽象类AbstractExecutorService类实现了ExecutorService接口,也实现了接口定义的默认行为。 ![2015-08-04/55c030a8c9627](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c030a8c9627.jpg) AbstractExecutorService任务提交的submit方法有三个实现。第一个接收一个Runnable的Task,没有执行结果;第二个是两个参数:一个任务,一个执行结果;第三个一个Callable,本身就包含执任务内容和执行结果。 submit方法的返回结果是Future类型,调用该接口定义的get方法即可获得执行结果。 **V get() 方法的返回值类型V是在提交任务时就约定好了的。** 除了submit任务的方法外,作为对服务的管理,在ExecutorService接口中还定义了服务的关闭方法shutdown和shutdownNow方法,可以平缓或者立即关闭执行服务,实现该方法的子类根据自身特征支持该定义。在ThreadPoolExecutor中,维护了RUNNING、SHUTDOWN、STOP、TERMINATED四种状态来实现对线程池的管理。线程池的完整运行机制不是本文的重点,重点还是关注submit过程中的逻辑。 1) 看AbstractExecutorService中代码提交部分,构造好一个FutureTask对象后,调用execute()方法执行任务。我们知道这个方法是顶级接口Executor中定义的最重要的方法。。FutureTask类型实现了Runnable接口,因此满足Executor中execute()方法的约定。同时比较有意思的是,该对象在execute执行后,就又作为submit方法的返回值返回,因为FutureTask同时又实现了Future接口,满足Future接口的约定。 ~~~ public <T> Future<T> submit(Callable<T> task) { if (task == null) throw new NullPointerException(); RunnableFuture<T> ftask = newTaskFor(task); execute(ftask); return ftask; } ~~~ 2) Submit传入的参数都被封装成了FutureTask类型来execute的,对应前面三个不同的参数类型都会封装成FutureTask。 ~~~ protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { return new FutureTask<T>(callable); } ~~~ 3) Executor接口中定义的execute方法的作用就是执行提交的任务,该方法在抽象类AbstractExecutorService中没有实现,留到子类中实现。我们观察下子类ThreadPoolExecutor,使用最广泛的线程池如何来execute那些submit的任务的。这个方法看着比较简单,但是线程池什么时候创建新的作业线程来处理任务,什么时候只接收任务不创建作业线程,另外什么时候拒绝任务。线程池的接收任务、维护工作线程的策略都要在其中体现。 作为必要的预备知识,先补充下ThreadPoolExecutor有两个最重要的集合属性,分别是存储接收任务的任务队列和用来干活的作业集合。 ~~~ //任务队列 private final BlockingQueue<Runnable> workQueue; //作业线程集合 private final HashSet<Worker> workers = new HashSet<Worker>(); ~~~ 其中阻塞队列workQueue是来存储待执行的任务的,在构造线程池时可以选择满足该BlockingQueue 接口定义的SynchronousQueue、LinkedBlockingQueue或者DelayedWorkQueue等不同阻塞队列来实现不同特征的线程池。 关注下execute(Runnable command)方法中调用到的addIfUnderCorePoolSize,workQueue.offer(command) , ensureQueuedTaskHandled(command),addIfUnderMaximumPoolSize(command)这几个操作。尤其几个名字较长的private方法,把方法名的驼峰式的单词分开,加上对方法上下文的了解就能理解其功能。 因为前面说到的几个方法在里面即是操作,又返回一个布尔值,影响后面的逻辑,所以不大方便在方法体中为每条语句加注释来说明,需要大致关联起来看。所以首先需要把execute方法的主要逻辑说明下,再看其中各自方法的作用。 * 如果线程池的状态是RUNNING,线程池的大小小于配置的核心线程数,说明还可以创建新线程,则启动新的线程执行这个任务。 * 如果线程池的状态是RUNNING ,线程池的大小小于配置的最大线程数,并且任务队列已经满了,说明现有线程已经不能支持当前的任务了,并且线程池还有继续扩充的空间,就可以创建一个新的线程来处理提交的任务。 * 如果线程池的状态是RUNNING,当前线程池的大小大于等于配置的核心线程数,说明根据配置当前的线程数已经够用,不用创建新线程,只需把任务加入任务队列即可。如果任务队列不满,则提交的任务在任务队列中等待处理;如果任务队列满了则需要考虑是否要扩展线程池的容量。 * 当线程池已经关闭或者上面的条件都不能满足时,则进行拒绝策略,拒绝策略在RejectedExecutionHandler接口中定义,可以有多种不同的实现。 上面其实也是对最主要思路的解析,详细展开可能还会更复杂。简单梳理下思路:构建线程池时定义了一个额定大小,当线程池内工作线程数小于额定大小,有新任务进来就创建新工作线程,如果超过该阈值,则一般就不创建了,只是把接收任务加到任务队列里面。但是如果任务队列里的任务实在太多了,那还是要申请额外的工作线程来帮忙。如果还是不够用就拒绝服务。这个场景其实也是每天我们工作中会碰到的场景。我们管人的老大,手里都有一定HC(Head Count),当上面老大有活分下来,手里人不够,但是不超过HC,我们就自己招人;如果超过了还是忙不过来,那就向上门老大申请借调人手来帮忙;如果还是干不完,那就没办法了,新任务咱就不接了。 ~~~ public void execute(Runnable command) { if (command == null) throw new NullPointerException(); if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated } } ~~~ 4) addIfUnderCorePoolSize方法检查如果当前线程池的大小小于配置的核心线程数,说明还可以创建新线程,则启动新的线程执行这个任务。 ~~~ private boolean addIfUnderCorePoolSize(Runnable firstTask) { Thread t = null; //如果当前线程池的大小小于配置的核心线程数,说明还可以创建新线程 if (poolSize < corePoolSize && runState == RUNNING) // 则启动新的线程执行这个任务 t = addThread(firstTask); return t != null; } ~~~ 5)  和上一个方法类似,addIfUnderMaximumPoolSize检查如果线程池的大小小于配置的最大线程数,并且任务队列已经满了(就是execute方法试图把当前线程加入任务队列时不成功),说明现有线程已经不能支持当前的任务了,但线程池还有继续扩充的空间,就可以创建一个新的线程来处理提交的任务。 ~~~ private boolean addIfUnderMaximumPoolSize(Runnable firstTask) { // 如果线程池的大小小于配置的最大线程数,并且任务队列已经满了(就 是execute方法中试图把当前线程加入任务队列workQueue.offer(command)时候不成功 ),说明现有线程已经不能支持当前的任务了,但线程池还有继续扩充的空间 if (poolSize < maximumPoolSize && runState == RUNNING) //就可以创建一个新的线程来处理提交的任务 t = addThread(firstTask); return t != null; } ~~~ 6)  在ensureQueuedTaskHandled方法中,判断如果当前状态不是RUNING,则当前任务不加入到任务队列中,判断如果状态是停止,线程数小于允许的最大数,且任务队列还不空,则加入一个新的工作线程到线程池来帮助处理还未处理完的任务。 ~~~ private void ensureQueuedTaskHandled(Runnable command) { // 如果当前状态不是RUNING,则当前任务不加入到任务队列中,判断如 果状态是停止,线程数小于允许的最大数,且任务队列还不空 if (state < STOP && poolSize < Math.max(corePoolSize, 1) && !workQueue.isEmpty()) //则加入一个新的工作线程到线程池来帮助处理还未处理完的任务 t = addThread(null); if (reject) reject(command); } ~~~ 7)   在前面方法中都会调用adThread方法创建一个工作线程,差别是创建的有些工作线程上面关联接收到的任务firstTask,有些没有。该方法为当前接收到的任务firstTask创建Worker,并将Worker添加到作业集合HashSet workers中,并启动作业。 ~~~ private Thread addThread(Runnable firstTask) { //为当前接收到的任务firstTask创建Worker Worker w = new Worker(firstTask); Thread t = threadFactory.newThread(w); w.thread = t; //将Worker添加到作业集合HashSet<Worker> workers中,并启动作业 workers.add(w); t.start(); return t; } ~~~ 至此,任务提交过程简单描述完毕,并介绍了任务提交后ExecutorService框架下线程池的主要应对逻辑,其实就是接收任务,根据需要创建或者维护管理线程。 维护这些工作线程干什么用?先不用看后面的代码,想想我们老大每月辛苦地把老板丰厚的薪水递到我们手里,定期还要领着大家出去happy下,又是定期的关心下个人生活,所有做的这些都是为什么呢?木讷的代码工不往这边使劲动脑子,但是猜还是能猜的到的,就让干活呗。本文想着重表达细节,诸如线程池里的Worker是怎么工作的,Task到底是不是在这些工作线程中执行的,如何保证执行完成后,外面等待任务的老大拿到想要结果,我们将在[下篇文章](http://www.infoq.com/cn/articles/executor-framework-thread-pool-task-execution-part-02)中详细介绍。 ## 作者简介 **张超盟**,an ExTrender,‍CS数据管理方向工学硕士。与妻儿蜗居于钱江畔,就职一初创安全公司任数据服务团队负责人,做数据(存储、挖掘、服务)方面研发。爱数据,爱代码,爱技术,爱豆吧!([idouba.net](http://idouba.net/))。
';

深入浅出Mesos(四):Mesos的资源分配

最后更新于:2022-04-01 01:57:09

作者 韩陆 【编者按】Mesos是Apache下的开源分布式资源管理框架,它被称为是分布式系统的内核。Mesos最初是由加州大学伯克利分校的AMPLab开发的,后在Twitter得到广泛使用。InfoQ接下来将会策划系列文章来为读者剖析Mesos。本文是整个系列的第一篇,简单介绍了Mesos的背景、历史以及架构。 注:本文翻译自[Cloud Architect Musings](http://cloudarchitectmusings.com/2015/03/23/apache-mesos-the-true-os-for-the-software-defined-data-center/),InfoQ中文站在获得作者授权的基础上对文章进行了翻译。 ![2015-08-04/55c02c852d0d9](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02c852d0d9.jpg) Apache Mesos能够成为最优秀的数据中心资源管理器的一个重要功能是面对各种类型的应用,它具备像交警一样的疏导能力。本文将深入Mesos的资源分配内部,探讨Mesos是如何根据客户应用需求,平衡公平资源共享的。在开始之前,如果读者还没有阅读这个系列的前序文章,建议首先阅读它们。第一篇是[Mesos的概述](http://www.infoq.com/cn/articles/analyse-mesos-part-01),第二篇是[两级架构的说明](http://www.infoq.com/cn/articles/analyse-mesos-part-02),第三篇是[数据存储和容错](http://www.infoq.com/cn/articles/analyse-mesos-part-03)。 我们将探讨Mesos的资源分配模块,看看它是如何确定将什么样的资源邀约发送给具体哪个Framework,以及在必要时如何回收资源。让我们先来回顾一下Mesos的任务调度过程: ![2015-08-04/55c02c8e0ecff](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02c8e0ecff.jpg) 从前面提到的[两级架构的说明](http://www.infoq.com/cn/articles/analyse-mesos-part-02)一文中我们知道,Mesos Master代理任务的调度首先从Slave节点收集有关可用资源的信息,然后以资源邀约的形式,将这些资源提供给注册其上的Framework。 Framework可以根据是否符合任务对资源的约束,选择接受或拒绝资源邀约。一旦资源邀约被接受,Framework将与Master协作调度任务,并在数据中心的相应Slave节点上运行任务。 如何作出资源邀约的决定是由资源分配模块实现的,该模块存在于Master之中。资源分配模块确定Framework接受资源邀约的顺序,与此同时,确保在本性贪婪的Framework之间公平地共享资源。在同质环境中,比如Hadoop集群,使用最多的公平份额分配算法之一是最大最小公平算法(max-min fairness)。[最大最小公平算法](http://en.wikipedia.org/wiki/Max-min_fairness)算法将最小的资源分配最大化,并将其提供给用户,确保每个用户都能获得公平的资源份额,以满足其需求所需的资源;一个简单的例子能够说明其工作原理,请参考[最大最小公平份额算法页面](http://www.ece.rutgers.edu/~marsic/Teaching/CCN/minmax-fairsh.html)的示例1。如前所述,在同质环境下,这通常能够很好地运行。同质环境下的资源需求几乎没有波动,所涉及的资源类型包括CPU、内存、网络带宽和I/O。然而,在跨数据中心调度资源并且是异构的资源需求时,资源分配将会更加困难。例如,当用户A的每个任务需要1核CPU、4GB内存,而用户B的每个任务需要3核CPU、1GB内存时,如何提供合适的公平份额分配策略?当用户A的任务是内存密集型,而用户B的任务是CPU密集型时,如何公平地为其分配一揽子资源? 因为Mesos是专门管理异构环境中的资源,所以它实现了一个可插拔的资源分配模块架构,将特定部署最适合的分配策略和算法交给用户去实现。例如,用户可以实现加权的最大最小公平性算法,让指定的Framework相对于其它的Framework获得更多的资源。默认情况下,Mesos包括一个严格优先级的资源分配模块和一个改良的公平份额资源分配模块。严格优先级模块实现的算法给定Framework的优先级,使其总是接收并接受足以满足其任务要求的资源邀约。这保证了关键应用在Mesos中限制动态资源份额上的开销,但是会潜在其他Framework饥饿的情况。 由于这些原因,大多数用户默认使用DRF(主导资源公平算法 Dominant Resource Fairness),这是Mesos中更适合异质环境的改良公平份额算法。 DRF和Mesos一样出自Berkeley AMPLab团队,并且作为Mesos的默认资源分配策略实现编码。 读者可以从[此处](https://www.cs.berkeley.edu/~alig/papers/drf.pdf)和[此处](http://www.eecs.berkeley.edu/Pubs/TechRpts/2010/EECS-2010-55.pdfhttp://www.eecs.berkeley.edu/Pubs/TechRpts/2010/EECS-2010-55.pdf)阅读DRF的原始论文。在本文中,我将总结其中要点并提供一些例子,相信这样会更清晰地解读DRF。让我们开始揭秘之旅。 DRF的目标是确保每一个用户,即Mesos中的Framework,在异质环境中能够接收到其最需资源的公平份额。为了掌握DRF,我们需要了解主导资源(dominant resource)和主导份额(dominant share)的概念。Framework的主导资源是其最需的资源类型(CPU、内存等),在资源邀约中以可用资源百分比的形式展示。例如,对于计算密集型的任务,它的Framework的主导资源是CPU,而依赖于在内存中计算的任务,它的Framework的主导资源是内存。因为资源是分配给Framework的,所以DRF会跟踪每个Framework拥有的资源类型的份额百分比;Framework拥有的全部资源类型份额中占最高百分比的就是Framework的主导份额。DRF算法会使用所有已注册的Framework来计算主导份额,以确保每个Framework能接收到其主导资源的公平份额。 概念过于抽象了吧?让我们用一个例子来说明。假设我们有一个资源邀约,包含9核CPU和18GB的内存。Framework 1运行任务需要(1核CPU、4GB内存),Framework 2运行任务需要(3核CPU、1GB内存) Framework 1的每个任务会消耗CPU总数的1/9、内存总数的2/9,因此Framework 1的主导资源是内存。同样,Framework 2的每个任务会CPU总数的1/3、内存总数的1/18,因此Framework 2的主导资源是CPU。DRF会尝试为每个Framework提供等量的主导资源,作为他们的主导份额。在这个例子中,DRF将协同Framework做如下分配:Framework 1有三个任务,总分配为(3核CPU、12GB内存),Framework 2有两个任务,总分配为(6核CPU、2GB内存)。 此时,每个Framework的主导资源(Framework 1的内存和Framework 2的CPU)最终得到相同的主导份额(2/3或67%),这样提供给两个Framework后,将没有足够的可用资源运行其他任务。需要注意的是,如果Framework 1中仅有两个任务需要被运行,那么Framework 2以及其他已注册的Framework将收到的所有剩余的资源。 ![2015-08-04/55c02c9a40be3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02c9a40be3.png) 那么,DRF是怎样计算而产生上述结果的呢?如前所述,DRF分配模块跟踪分配给每个Framework的资源和每个框架的主导份额。每次,DRF以所有Framework中运行的任务中最低的主导份额作为资源邀约发送给Framework。如果有足够的可用资源来运行它的任务,Framework将接受这个邀约。通过前面引述的DRF论文中的示例,我们来贯穿DRF算法的每个步骤。为了简单起见,示例将不考虑短任务完成后,资源被释放回资源池中这一因素,我们假设每个Framework会有无限数量的任务要运行,并认为每个资源邀约都会被接受。 回顾上述示例,假设有一个资源邀约包含9核CPU和18GB内存。Framework 1运行的任务需要(1核CPU、4GB内存),Framework 2运行的任务需要(3核CPU、2GB内存)。Framework 1的任务会消耗CPU总数的1/9、内存总数的2/9,Framework 1的主导资源是内存。同样,Framework 2的每个任务会CPU总数的1/3、内存总数的1/18,Framework 2的主导资源是CPU。 ![2015-08-04/55c02ca106690](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02ca106690.jpg) 上面表中的每一行提供了以下信息: * Framework chosen——收到最新资源邀约的Framework。 * Resource Shares——给定时间内Framework接受的资源总数,包括CPU和内存,以占资源总量的比例表示。 * Dominant Share(主导份额)——给定时间内Framework主导资源占总份额的比例,以占资源总量的比例表示。 * Dominant Share %(主导份额百分比)——给定时间内Framework主导资源占总份额的百分比,以占资源总量的百分比表示。 * CPU Total Allocation——给定时间内接受的所有Framework的总CPU资源。 * RAM Total Allocation——给定时间内接受的所有Framework的总内存资源。 注意,每个行中的最低主导份额以粗体字显示,以便查找。 最初,两个Framework的主导份额是0%,我们假设DRF首先选择的是Framework 2,当然我们也可以假设Framework 1,但是最终的结果是一样的。 1. Framework 2接收份额并运行任务,使其主导资源成为CPU,主导份额增加至33%。 2. 由于Framework 1的主导份额维持在0%,它接收共享并运行任务,主导份额的主导资源(内存)增加至22%。 3. 由于Framework 1仍具有较低的主导份额,它接收下一个共享并运行任务,增加其主导份额至44%。 4. 然后DRF将资源邀约发送给Framework 2,因为它现在拥有更低的主导份额。 5. 该过程继续进行,直到由于缺乏可用资源,不能运行新的任务。在这种情况下,CPU资源已经饱和。 6. 然后该过程将使用一组新的资源邀约重复进行。 需要注意的是,可以创建一个资源分配模块,使用加权的DRF使其偏向某个Framework或某组Framework。如前面所提到的,也可以创建一些自定义模块来提供组织特定的分配策略。 一般情况下,现在大多数的任务是短暂的,Mesos能够等待任务完成并重新分配资源。然而,集群上也可以跑长时间运行的任务,这些任务用于处理挂起作业或行为不当的Framework。 值得注意的是,在当资源释放的速度不够快的情况下,资源分配模块具有撤销任务的能力。Mesos尝试如此撤销任务:向执行器发送请求结束指定的任务,并给出一个宽限期让执行器清理该任务。如果执行器不响应请求,分配模块就结束该执行器及其上的所有任务。 分配策略可以实现为,通过提供与Framework相关的保证配置,来阻止对指定任务的撤销。如果Framework低于保证配置,Mesos将不能结束该Framework的任务。 我们还需了解更多关于Mesos资源分配的知识,但是我将戛然而止。接下来,我要说点不同的东西,是关于Mesos社区的。我相信这是一个值得考虑的重要话题,因为开源不仅包括技术,还包括社区。 说完社区,我将会写一些关于Mesos的安装和Framework的创建和使用的,逐步指导的教程。在一番实操教学的文章之后,我会回来做一些更深入的话题,比如Framework与Master是如何互动的,Mesos如何跨多个数据中心工作等。 与往常一样,我鼓励读者提供反馈,特别是关于如果我打标的地方,如果你发现哪里不对,请反馈给我。我非全知,虚心求教,所以非常期待读者的校正和启示。我们也可以在[twitter](https://twitter.com/hui_kenneth)上沟通,请关注 @hui_kenneth。
';

深入浅出Mesos(三):持久化存储和容错

最后更新于:2022-04-01 01:57:07

作者 韩陆 > 【编者按】Mesos是Apache下的开源分布式资源管理框架,它被称为是分布式系统的内核。Mesos最初是由加州大学伯克利分校的AMPLab开发的,后在Twitter得到广泛使用。InfoQ接下来将会策划系列文章来为读者剖析Mesos。本文是整个系列的第一篇,简单介绍了Mesos的背景、历史以及架构。 注:本文翻译自[Cloud Architect Musings](http://cloudarchitectmusings.com/2015/03/23/apache-mesos-the-true-os-for-the-software-defined-data-center/),InfoQ中文站在获得作者授权的基础上对文章进行了翻译。 * * * 在深入浅出Mesos系列的[第一篇文章](http://www.infoq.com/cn/articles/analyse-mesos-part-01)中,我对相关的技术做了简要概述,在[第二篇](http://www.infoq.com/cn/articles/analyse-mesos-part-02)文章中,我深入介绍了Mesos的架构。完成第二篇文章之后,我本想开始着手写一篇Mesos如何处理资源分配的文章。不过,我收到一些读者的反馈,于是决定在谈资源分配之前,先完成这篇关于Mesos持久化存储和容错的文章。 ## 持久化存储的问题 ![2015-08-04/55c02975db1b7](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02975db1b7.png) 正如我在前文中讨论过的,使用Mesos的主要好处是可以在同一组计算节点集合上运行多种类型的应用程序(调度以及通过Framework初始化任务)。这些任务使用隔离模块(目前是某些类型的容器技术)从实际节点中抽象出来,以便它们可以根据需要在不同的节点上移动和重新启动。 由此我们会思考一个问题,Mesos是如何处理持久化存储的呢?如果我在运行一个数据库作业,Mesos如何确保当任务被调度时,分配的节点可以访问其所需的数据?如图所示,在Hindman的示例中,使用Hadoop文件系统(HDFS)作为Mesos的持久层,这是HDFS常见的使用方式,也是Mesos的执行器传递分配指定任务的配置数据给Slave经常使用的方式。实际上,Mesos的持久化存储可以使用多种类型的文件系统,HDFS只是其中之一,但也是Mesos最经常使用的,它使得Mesos具备了与高性能计算的亲缘关系。其实Mesos可以有多种选择来处理持久化存储的问题: * **分布式文件系统**。如上所述,Mesos可以使用DFS(比如HDFS或者Lustre)来保证数据可以被Mesos集群中的每个节点访问。这种方式的缺点是会有网络延迟,对于某些应用程序来说,这样的网络文件系统或许并不适合。 * **使用数据存储复制的本地文件系统**。另一种方法是利用应用程序级别的复制来确保数据可被多个节点访问。提供数据存储复制的应用程序可以是NoSQL数据库,比如Cassandra和MongoDB。这种方式的优点是不再需要考虑网络延迟问题。缺点是必须配置Mesos,使特定的任务只运行在持有复制数据的节点上,因为你不会希望数据中心的所有节点都复制相同的数据。为此,可以使用一个Framework,静态地为其预留特定的节点作为复制数据的存储。 ![2015-08-04/55c02b1190028](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02b1190028.png) * **不使用复制的本地文件系统**。也可以将持久化数据存储在指定节点的文件系统上,并且将该节点预留给指定的应用程序。和前面的选择一样,可以静态地为指定应用程序预留节点,但此时只能预留给单个节点而不是节点集合。后面两种显然不是理想的选择,因为实质上都需要创建静态分区。然而,在不允许延时或者应用程序不能复制它的数据存储等特殊情况下,我们需要这样的选择。 Mesos项目还在发展中,它会定期增加新功能。现在我已经发现了两个可以帮助解决持久化存储问题的新特性: * **动态预留**。Framework可以使用这个功能框架保留指定的资源,比如持久化存储,以便在需要启动另一个任务时,资源邀约只会发送给那个Framework。这可以在单节点和节点集合中结合使用Framework配置,访问永久化数据存储。关于这个建议的功能的更多信息可以从[此处](https://issues.apache.org/jira/browse/MESOS-2018)获得。 * **持久化卷**。该功能可以创建一个卷,作为Slave节点上任务的一部分被启动,即使在任务完成后其持久化依然存在。Mesos为需要访问相同的数据后续任务,提供在可以访问该持久化卷的节点集合上相同的Framework来初始化。关于这个建议的功能的更多信息可以从[此处](https://issues.apache.org/jira/browse/MESOS-1554)获得。 ## 容错 接下来,我们来谈谈Mesos在其协议栈上是如何提供容错能力的。恕我直言,Mesos的优势之一便是将容错设计到架构之中,并以可扩展的分布式系统的方式来实现。 * **Master**。故障处理机制和特定的架构设计实现了Master的容错。 ![2015-08-04/55c02b1e1af33](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02b1e1af33.png) 首先,Mesos决定使用热备份(hot-standby)设计来实现Master节点集合。正如Tomas Barton对上图的说明,一个Master节点与多个备用(standby)节点运行在同一集群中,并由开源软件Zookeeper来监控。Zookeeper会监控Master集群中所有的节点,并在Master节点发生故障时管理新Master的选举。建议的节点总数是5个,实际上,生产环境至少需要3个Master节点。 Mesos决定将Master设计为持有软件状态,这意味着当Master节点发生故障时,其状态可以很快地在新选举的Master节点上重建。 Mesos的状态信息实际上驻留在Framework调度器和Slave节点集合之中。当一个新的Master当选后,Zookeeper会通知Framework和选举后的Slave节点集合,以便使其在新的Master上注册。彼时,新的 Master可以根据Framework和Slave节点集合发送过来的信息,重建内部状态。 * **Framework调度器**。Framework调度器的容错是通过Framework将调度器注册2份或者更多份到Master来实现。当一个调度器发生故障时,Master会通知另一个调度来接管。需要注意的是Framework自身负责实现调度器之间共享状态的机制。 * **Slave**。Mesos实现了Slave的恢复功能,当Slave节点上的进程失败时,可以让执行器/任务继续运行,并为那个Slave进程重新连接那台Slave节点上运行的执行器/任务。当任务执行时,Slave会将任务的监测点元数据存入本地磁盘。如果Slave进程失败,任务会继续运行,当Master重新启动Slave进程后,因为此时没有可以响应的消息,所以重新启动的Slave进程会使用检查点数据来恢复状态,并重新与执行器/任务连接。 如下情况则截然不同,计算节点上Slave正常运行而任务执行失败。在此,Master负责监控所有Slave节点的状态。 ![2015-08-04/55c02b4176e7c](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02b4176e7c.png) 当计算节点/Slave节点无法响应多个连续的消息后,Master会从可用资源的列表中删除该节点,并会尝试关闭该节点。 ![2015-08-04/55c02b4b207fc](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c02b4b207fc.png) 然后,Master会向分配任务的Framework调度器汇报执行器/任务失败,并允许调度器根据其配置策略做任务失败处理。通常情况下,Framework会重新启动任务到新的Slave节点,假设它接收并接受来自Master的相应的资源邀约。 * **执行器/任务**。与计算节点/Slave节点故障类似,Master会向分配任务的Framework调度器汇报执行器/任务失败,并允许调度器根据其配置策略在任务失败时做出相应的处理。通常情况下,Framework在接收并接受来自Master的相应的资源邀约后,会在新的Slave节点上重新启动任务。 ## 结论 在接下来的文章中,我将更深入到资源分配模块。同时,我非常期待读者的反馈,特别是关于如果我打标的地方,如果你发现哪里不对,请反馈给我。我非全知,虚心求教,所以期待读者的校正和启示。我也会在[twitter](https://twitter.com/hui_kenneth)响应你的反馈,请关注 @hui_kenneth。
';

专题|Topic

最后更新于:2022-04-01 01:57:04

';

高可用可伸缩架构实用经验谈

最后更新于:2022-04-01 01:57:02

作者 李道兵 移动互联网、云计算和大数据的成熟和发展,让更多的好想法得以在很短的时间内实现为产品。此时,如果用户需求抓得准,用户数量将很可能获得爆发式增长,而不需要像以往一样需要精心运营几年的时间。然而用户数量的快速增长(尤其是短时间内的爆发式增长),通常会让应用开发者有些吃不消,不得不面临一些严峻的技术挑战:如何避免因为单台机器当机导致服务不可用;如何避免在服务容量不足时,用户体验下降,等等。在系统构建之初就采用高可用和可伸缩架构,将能有效避免这些问题。 如何构建高可用和可伸缩架构呢?七牛云存储首席架构师李道兵在3月22的「开发者最佳实践日」第十期沙龙活动上给出了自己的想法。他结合自己多年的实践经验,针对一些不太复杂的业务场景,从入口层、业务层、缓存层和数据库层四个层面细致讲述了如何构建高可用和可伸缩系统。希望大家读完这篇文章,能觉得高可用和可伸缩不是一个高不可攀的东西,投入不高的成本就能在项目早期把高可用和可伸缩纳入架构设计之中。 ## 如何实现高可用 ### 入口层 入口层,通常指Nginx和Apache等层面的东西,负责应用(不管是Web应用还是移动应用)的服务入口。我们通常会将服务定位在一个IP,如果这个IP对应的服务器当机了,那么用户的访问肯定会中断。此时,可以用keepalived来实现入口层的高可用。例如,机器A 的IP是 1.2.3.4,机器 B 的 IP 是 1.2.3.5, 那么再申请一个 IP 1.2.3.6(称为⼼跳IP), 平时绑定在机器A上,如果A当机,IP会自动绑定在机器B上;如果B当机,IP会自动绑定在机器A上。对于这种形式,我们将DNS绑定到心跳IP上,即可实现入口层的高可用。 但这个方案有一点小问题。第一,它的切换可能会有一到两秒的中断,也就是说,如果不是要求到非常严格的毫秒级就不会有问题。第二,对入口的机器会有些浪费,因为买了两台机器的入口,可能就只有一台机器用上。对一些长连接的应用可能会导致服务中断,这时候就需要客户端做配合做一些重新创建连接的工作。简单来说,对于比较普通的业务来说,这个方案就能解决一部分问题。 这里要注意,keepalived在使用上会有一些限制。 * 两台机器必须在同一个网段,不是在同一个网段,没有办法实现互相抢IP。 * 内网服务也可以做心跳,但需要注意的是,以前为了安全我们会把内网服务绑定在内网IP上,避免出现安全问题。但为了使用keepalived,必须监听在所有IP上(如果监听在心跳IP上,那么机器没有持有该IP时,服务无法启动),简单的方案是启用 iptables, 避免内网服务被外网访问。 * 服务器利用率下降,这时可以考虑做混合部署来改善这一点。 比较常见的一个错误是,如果有两台机器,两个公网IP,DNS上把域名同时定位到两个IP,就觉得已经做了高可用了。这完全不是高可用,因为如果一台机器当机,那么就有一半左右的用户无法访问。 除了keepalive,lvs也能用来解决入口层的高可用问题。不过,与keepalived相比,lvs会更复杂一些,门槛也会高一些。 ### 业务层 业务层通常是由PHP、Java、Python、Go等写的逻辑代码构成的,需要依赖于后台数据库及一些缓存层面的东西。如何实现业务层的高可用呢?最核心的就是,业务层不要有状态,将状态分散到缓存层和数据库。目前大家通常喜欢将以下几种数据放入业务层。 第一个是session,即用户登录相关的数据,但好的做法是将session放在数据库里,或者一个比较稳定的缓存系统中。 第二个是缓存,在访问数据库时,如果一个查询很慢,就希望将这些结果暂时放到进程里,下次再做查询时就不用再访问数据库了。这种做法带来的问题是,当业务层服务器不只一台时,数据很难做到一致,从缓存拿到的数据就可能是错误的。。 一个简单的原则就是业务层不要有状态。在业务层没有状态时,一台业务层服务器当掉了之后,Nginx/Apache会自动将所有的请求打到另外一台业务层的服务器上。由于没有状态,两台服务器没有任何差异,所以用户完全感受不到。如果把session放在业务层里面的话,那么面临的问题是,这个用户以前是登录在一台机器上的,这个进程死掉后,用户就会被登出了。 友情提醒:有一段时间比较流行cookie session,就是将session中的数据加密之后放在客户的cookie里,然后下发到客户端,这样也能做到与服务端完全无状态。但这里面有很多坑,如果能绕过这些坑就可以这样使用。第一个坑是怎么保证加密的密钥不泄露,一旦泄露就意味着攻击者可以伪造任何人的身份。第二个坑是重放攻击,如何避免别人通过保存 cookie 去不停地尝试的验证码,当然也还有其他一些攻击手段。如果没有好办法解决这两方面的问题,那么cookie session尽量慎用。最好是将session放在一个性能比较好的数据库中。如果数据库性能不行,那么将session放在缓存中也比放在cookie里要好一点。 ### 缓存层 非常简单的架构里是没有缓存这个概念的。但在访问量上来之后,MySQL之类的数据库扛不住了,比如在SATA盘里跑MySQL,QPS到达200、300甚至500时,MySQL的性能会大幅下降,这时就可以考虑用缓存层来挡住绝大部分服务请求,提升系统整体的容量。 缓存层做高可用一个简单的方法就是,将缓存层分得细一点儿。比如说,缓存层就一台机器的话,那么这台机器当了以后,所有应用层的压力就会往数据库里压,数据库扛不住的话,整个网站(或应用)就会随之当掉。而如果缓存层分在四台机器上的话,每台只有四分之一,这台机器当掉了以后,也只有总访问量的四分之一会压在数据库上面,数据库能扛住的话,网站就能很稳定地等到缓存层重新起来。在实践中,四分之一显然是不够的,我们会将它分得更细,以保证单台缓存当机后数据库还能撑得住即可。在中小规模下,缓存层和业务层可以混合部署,这样可以节省机器。 ### 数据库层 在数据库层面实现高可用,通常是在软件层面来做。例如,MySQL有主从模式(Master-Slave),还有主主模式(Master-Master)都能满足需求。MongoDB也有ReplicaSet的概念,基本都能满足大家的需求。 总之,要想实现高可用,需要做到这几点:入口层做心跳,业务层服务器无状态,缓存层减小粒度,数据库做一个主从模式。对于这种模式来讲,我们做的高可用不需要太多服务器,这些东西都可以同时部署在两台服务器上。这时,两台服务器就能满足早期的高可用需求了。任何一台服务器当机用户完全无感知。 ## 如何实现可伸缩 ### 入口层 在入口层实现伸缩性,可以通过直接水平扩机器,然后DNS加IP来实现。但需要注意,尽管一个域名解析到几十个IP没有问题,但是很多浏览器客户端只会使用前几个IP,部分域名供应商对此有优化(如每次返回的IP顺序随机),但这个优化效果不稳定。 推荐的做法是使用少量的Nginx机器作为入口,业务服务器隐藏在内网(HTTP类型的业务这种方式居多)。另外,也可以把所有IP下发到客户端,然后在客户端做一些调度(特别是非HTTP型的业务,如游戏、直播)。 ### 业务层 业务层的伸缩性如何实现?与做高可用时的解决方案一样,要实现业务层的伸缩性,保证无状态是很好的手段。此外,加机器继续水平部署即可。 ### 缓存层 比较麻烦的是缓存层的伸缩性,最简单粗暴的方式是什么呢?趁着半夜量比较低的时候,把整个缓存层全部下线,然后上线新的缓存层。新的缓存层启动起来之后,再等这些缓存慢慢预热。当然这里一个要求,你的数据库能抗住低估期的请求量。如果扛不住呢?取决于缓存类型,下面我们先可以将缓存的类型区分一下。 * 强一致性缓存:无法接受从缓存拿到错误的数据 (比如用户余额,或者会被下游继续缓存这种情形) * 弱一致性缓存:能接受在一段时间内从缓存拿到错误的数据 (比如微博的转发数)。 * 不变型缓存:缓存key对应的value不会变更 (比如从SHA1推出来的密码, 或者其他复杂公式的计算结果)。 那什么缓存类型伸缩性比较好呢?弱一致性和不变型缓存的扩容很方便,用一致性Hash即可;强一致性情况稍微复杂一些,稍后再讲。使用一致性Hash,而不用简单Hash的原因是缓存的失效率。如果缓存从9台扩容到10台,简单Hash 情况下90%的缓存会马上失效,而如果使用一致性Hash情况,只有10%的缓存会失效。 那么,强一致性缓存会有什么问题?第一个问题是,缓存客户端的配置更新时间会有微小的差异,在这个时间窗内有可能会拿到过期的数据。第二个问题是,如果扩容之后再裁撤节点,会拿到脏数据。比如 a 这个key之前在机器1,扩容后在机器2,数据更新了,但裁撤节点后key回到机器1,这时候就会拿到脏数据。 要解决问题2比较简单,要么保持永不减少节点,要么节点调整间隔大于数据的有效时间。问题1可以用如下的步骤来解决: 1. 两套hash配置都更新到客户端,但仍然使用旧配置; 2. 逐个客户端改为只有两套hash结果一致的情况下会使用缓存,其余情况从数据库读,但写入缓存; 3. 逐个客户端通知使用新配置。 Memcache 设计得比较早,导致在伸缩性高可用方面的考虑得不太周到。Redis 在这方面有不少改进,特别是 @ngaut 团队基于 redis 开发了 codis 这个软件,一次性地解决了缓存层的绝大部分问题。推荐大家考察一下。 ### 数据库 在数据库层面实现伸缩,方法很多,文档也很多,此处不做过多赘述。大致方法为:水平拆分、垂直拆分和定期滚动。 总之,我们可以在入口层、业务层面、缓存层和数据库层四个层面,使用刚才介绍的方法和技术实现系统高可用和可伸缩性。具体为:在入口层用心跳来做到高可用,用平行部署来伸缩;在业务层做到服务无状态;在缓存层,可以减小一些粒度,以方便实现高可用,使用一致性Hash将有助于实现缓存层的伸缩性;数据库层的主从模式能解决高可用问题,拆分和滚动能解决可伸缩问题。 ![2015-08-04/55c0292a9c730](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0292a9c730.png) 本文中分享的这些技巧和方法,主要想帮助不太复杂的业务场景或者中小型应用快速搭建起高可用可伸缩的系统。关于如何构建高可用和可伸缩系统还有很多更为细节的点和实践经验值得探讨,望以后能与大家做更充分的交流。
';

序列化和反序列化

最后更新于:2022-04-01 01:57:00

作者 刘丁 ## 简介 文章作者服务于美团推荐与个性化组,该组致力于为美团用户提供每天billion级别的高质量个性化推荐以及排序服务。从Terabyte级别的用户行为数据,到Gigabyte级别的Deal/Poi数据;从对实时性要求毫秒以内的用户实时地理位置数据,到定期后台job数据,推荐与重排序系统需要多种类型的数据服务。推荐与重排序系统客户包括各种内部服务、美团客户端、美团网站。为了提供高质量的数据服务,为了实现与上下游各系统进行良好的对接,序列化和反序列化的选型往往是我们做系统设计的一个重要考虑因素。 本文内容按如下方式组织: * 第一部分给出了序列化和反序列化的定义,以及其在通讯协议中所处的位置; * 第二部分从使用者的角度探讨了序列化协议的一些特性; * 第三部分描述在具体的实施过程中典型的序列化组件,并与数据库组建进行了类比; * 第四部分分别讲解了目前常见的几种序列化协议的特性,应用场景,并对相关组件进行举例; * 最后一部分,基于各种协议的特性,以及相关benchmark数据,给出了作者的技术选型建议。 ## 一、定义以及相关概念 互联网的产生带来了机器间通讯的需求,而互联通讯的双方需要采用约定的协议,序列化和反序列化属于通讯协议的一部分。通讯协议往往采用分层模型,不同模型每层的功能定义以及颗粒度不同,例如:TCP/IP协议是一个四层协议,而OSI模型却是七层协议模型。在OSI七层协议模型中展现层(Presentation Layer)的主要功能是把应用层的对象转换成一段连续的二进制串,或者反过来,把二进制串转换成应用层的对象--这两个功能就是序列化和反序列化。一般而言,TCP/IP协议的应用层对应与OSI七层协议模型的应用层,展示层和会话层,所以序列化协议属于TCP/IP协议应用层的一部分。本文对序列化协议的讲解主要基于OSI七层协议模型。 * 序列化: 将数据结构或对象转换成二进制串的过程。 * 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。 ### 数据结构、对象与二进制串 不同的计算机语言中,数据结构,对象以及二进制串的表示方式并不相同。 数据结构和对象:对于类似Java这种完全面向对象的语言,工程师所操作的一切都是对象(Object),来自于类的实例化。在Java语言中最接近数据结构的概念,就是POJO(Plain Old Java Object)或者Javabean--那些只有setter/getter方法的类。而在C二进制串:序列化所生成的二进制串指的是存储在内存中的一块数据。C语言的字符串可以直接被传输层使用,因为其本质上就是以'0'结尾的存储在内存中的二进制串。在Java语言里面,二进制串的概念容易和String混淆。实际上String 是Java的一等公民,是一种特殊对象(Object)。对于跨语言间的通讯,序列化后的数据当然不能是某种语言的特殊数据类型。二进制串在Java里面所指的是byte[],byte是Java的8中原生数据类型之一(Primitive data types)。 ## 二、序列化协议特性 每种序列化协议都有优点和缺点,它们在设计之初有自己独特的应用场景。在系统设计的过程中,需要考虑序列化需求的方方面面,综合对比各种序列化协议的特性,最终给出一个折衷的方案。 ### 通用性 通用性有两个层面的意义。 1. 技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低了。 2. 流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一方面,流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。 ### 强健性/鲁棒性 以下两个方面的原因会导致协议不够强健。 1. 成熟度不够,一个协议从制定到实施,到最后成熟往往是一个漫长的阶段。协议的强健性依赖于大量而全面的测试,对于致力于提供高质量服务的系统,采用处于测试阶段的序列化协议会带来很高的风险。 2. 语言/平台的不公平性。为了支持跨语言、跨平台的功能,序列化协议的制定者需要做大量的工作;但是,当所支持的语言或者平台之间存在难以调和的特性的时候,协议制定者需要做一个艰难的决定--支持更多人使用的语言/平台,亦或支持更多的语言/平台而放弃某个特性。当协议的制定者决定为某种语言或平台提供更多支持的时候,对于使用者而言,协议的强健性就被牺牲了。 ### 可调试性/可读性 序列化和反序列化的数据正确性和业务正确性的调试往往需要很长的时间,良好的调试机制会大大提高开发效率。序列化后的二进制串往往不具备人眼可读性,为了验证序列化结果的正确性,写入方不得同时撰写反序列化程序,或提供一个查询平台--这比较费时;另一方面,如果读取方未能成功实现反序列化,这将给问题查找带来了很大的挑战--难以定位是由于自身的反序列化程序的bug所导致还是由于写入方序列化后的错误数据所导致。对于跨公司间的调试,由于以下原因,问题会显得更严重。 1. 支持不到位,跨公司调试在问题出现后可能得不到及时的支持,这大大延长了调试周期。 2. 访问限制,调试阶段的查询平台未必对外公开,这增加了读取方的验证难度。 如果序列化后的数据人眼可读,这将大大提高调试效率, XML和JSON就具有人眼可读的优点。 ### 性能 性能包括两个方面,时间复杂度和空间复杂度。 1. 空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。 2. 时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。 ### 可扩展性/兼容性 移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。 ### 安全性/访问限制 在序列化选型的过程中,安全性的考虑往往发生在跨局域网访问的场景。当通讯发生在公司之间或者跨机房的时候,出于安全的考虑,对于跨局域网的访问往往被限制为基于HTTP/HTTPS的80和443端口。如果使用的序列化协议没有兼容而成熟的HTTP传输层框架支持,可能会导致以下三种结果之一: 1. 因为访问限制而降低服务可用性; 2. 被迫重新实现安全协议而导致实施成本大大提高; 3. 开放更多的防火墙端口和协议访问,而牺牲安全性。 ## 三、序列化和反序列化的组件 典型的序列化和反序列化过程往往需要如下组件。 * IDL(Interface description language)文件:参与通讯的各方需要对通讯的内容需要做相关的约定(Specifications)。为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用IDL撰写的协议约定称之为IDL文件。 * IDL Compiler:IDL文件中约定的内容为了在各语言和平台可见,需要有一个编译器,将IDL文件转换成各语言对应的动态库。 * Stub/Skeleton Lib:负责序列化和反序列化的工作代码。Stub是一段部署在分布式系统客户端的代码,一方面接收应用层的参数,并对其序列化后通过底层协议栈发送到服务端,另一方面接收服务端序列化后的结果数据,反序列化后交给客户端应用层;Skeleton部署在服务端,其功能与Stub相反,从传输层接收序列化参数,反序列化后交给服务端应用层,并将应用层的执行结果序列化后最终传送给客户端Stub。 * Client/Server:指的是应用层程序代码,他们面对的是IDL所生存的特定语言的class或struct。 * 底层协议栈和互联网:序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递。 ![2015-08-04/55c0287cda54c](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0287cda54c.jpg) ### 序列化组件与数据库访问组件的对比 数据库访问对于很多工程师来说相对熟悉,所用到的组件也相对容易理解。下表类比了序列化过程中用到的部分组件和数据库访问组件的对应关系,以便于大家更好的把握序列化相关组件的概念。 |序列化组件|数据库组件|说明| |---|--|--| |IDL|DDL|用于建表或者模型的语言| |DL file|DB Schema|表创建文件或模型文件| |Stub/Skeleton|lib O/R mapping|将class和Table或者数据模型进行映射| ## 四、几种常见的序列化和反序列化协议 互联网早期的序列化协议主要有COM和CORBA。 COM主要用于Windows平台,并没有真正实现跨平台,另外COM的序列化的原理利用了编译器中虚表,使得其学习成本巨大(想一下这个场景, 工程师需要是简单的序列化协议,但却要先掌握语言编译器)。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。 CORBA是早期比较好的实现了跨平台,跨语言的序列化协议。COBRA的主要问题是参与方过多带来的版本过多,版本之间兼容性较差,以及使用复杂晦涩。这些政治经济,技术实现以及早期设计不成熟的问题,最终导致COBRA的渐渐消亡。J2SE 1.3之后的版本提供了基于CORBA协议的RMI-IIOP技术,这使得Java开发者可以采用纯粹的Java语言进行CORBA的开发。 这里主要介绍和对比几种当下比较流行的序列化协议,包括XML、JSON、Protobuf、Thrift和Avro。 ### 一个例子 如前所述,序列化和反序列化的出现往往晦涩而隐蔽,与其他概念之间往往相互包容。为了更好了让大家理解序列化和反序列化的相关概念在每种协议里面的具体实现,我们将一个例子穿插在各种序列化协议讲解中。在该例子中,我们希望将一个用户信息在多个系统里面进行传递;在应用层,如果采用Java语言,所面对的类对象如下所示: ~~~ class Address { private String city; private String postcode; private String street; } public class UserInfo { private Integer userid; private String name; private List<address> address; } </address> ~~~ ### XML&SOAP XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。 XML历史悠久,其1.0版本早在1998年就形成标准,并被广泛使用至今。XML的最初产生目标是对互联网文档(Document)进行标记,所以它的设计理念中就包含了对于人和机器都具备可读性。 但是,当这种标记文档的设计被用来序列化对象的时候,就显得冗长而复杂(Verbose and Complex)。 XML本质上是一种描述语言,并且具有自我描述(Self-describing)的属性,所以XML自身就被用于XML序列化的IDL。 标准的XML描述格式有两种:DTD(Document Type Definition)和XSD(XML Schema Definition)。作为一种人眼可读(Human-readable)的描述语言,XML被广泛使用在配置文件中,例如O/R mapping、 Spring Bean Configuration File 等。 SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议。SOAP在互联网影响如此大,以至于我们给基于SOAP的解决方案一个特定的名称--Web service。SOAP虽然可以支持多种传输层协议,不过SOAP最常见的使用方式还是XML+HTTP。SOAP协议的主要接口描述语言(IDL)是WSDL(Web Service Description Language)。SOAP具有安全、可扩展、跨语言、跨平台并支持多种传输层协议。如果不考虑跨平台和跨语言的需求,XML的在某些语言里面具有非常简单易用的序列化使用方法,无需IDL文件和第三方编译器, 例如Java+XStream。 #### 自我描述与递归 SOAP是一种采用XML进行序列化和反序列化的协议,它的IDL是WSDL. 而WSDL的描述文件是XSD,而XSD自身是一种XML文件。 这里产生了一种有趣的在数学上称之为“递归”的问题,这种现象往往发生在一些具有自我属性(Self-description)的事物上。 #### IDL文件举例 采用WSDL描述上述用户基本信息的例子如下: ~~~ <xsd:complexType name='Address'> <xsd:attribute name='city' type='xsd:string' /> <xsd:attribute name='postcode' type='xsd:string' /> <xsd:attribute name='street' type='xsd:string' /> </xsd:complexType> <xsd:complexType name='UserInfo'> <xsd:sequence> <xsd:element name='address' type='tns:Address'/> <xsd:element name='address1' type='tns:Address'/>  </xsd:sequence> <xsd:attribute name='userid' type='xsd:int' /> <xsd:attribute name='name' type='xsd:string' />  </xsd:complexTyp> ~~~ #### 典型应用场景和非应用场景 SOAP协议具有广泛的群众基础,基于HTTP的传输协议使得其在穿越防火墙时具有良好安全特性,XML所具有的人眼可读(Human-readable)特性使得其具有出众的可调试性,互联网带宽的日益剧增也大大弥补了其空间开销大(Verbose)的缺点。对于在公司之间传输数据量相对小或者实时性要求相对低(例如秒级别)的服务是一个好的选择。由于XML的额外空间开销大,序列化之后的数据量剧增,对于数据量巨大序列持久化应用常景,这意味着巨大的内存和磁盘开销,不太适合XML。另外,XML的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在ms级别的服务,不推荐使用。WSDL虽然具备了描述对象的能力,SOAP的S代表的也是simple,但是SOAP的使用绝对不简单。对于习惯于面向对象编程的用户,WSDL文件不直观。 ### JSON(Javascript Object Notation) JSON起源于弱类型语言Javascript, 它的产生来自于一种称之为"Associative array"的概念,其本质是就是采用"Attribute-value"的方式来描述对象。实际上在Javascript和PHP等弱类型语言中,类的描述方式就是Associative array。JSON的如下优点,使得它快速成为最广泛使用的序列化协议之一。 1. 这种Associative array格式非常符合工程师对对象的理解。 2. 它保持了XML的人眼可读(Human-readable)的优点。 3. 相对于XML而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML所产生序列化之后文件的大小接近JSON的两倍。[http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity](http://infoqhelp.sinaapp.com/architectgen#) 4. 它具备Javascript的先天性支持,所以被广泛应用于Web browser的应用常景中,是Ajax的事实标准协议。 5. 与XML相比,其协议比较简单,解析速度比较快。 6. 松散的Associative array使得其具有良好的可扩展性和兼容性。 #### IDL悖论 JSON实在是太简单了,或者说太像各种语言里面的类了,所以采用JSON进行序列化不需要IDL。这实在是太神奇了,存在一种天然的序列化协议,自身就实现了跨语言和跨平台。然而事实没有那么神奇,之所以产生这种假象,来自于两个原因。 1. Associative array在弱类型语言里面就是类的概念,在PHP和Javascript里面Associative array就是其class的实际实现方式,所以在这些弱类型语言里面,JSON得到了非常良好的支持。 2. IDL的目的是撰写IDL文件,而IDL文件被IDL Compiler编译后能够产生一些代码(Stub/Skeleton),而这些代码是真正负责相应的序列化和反序列化工作的组件。 但是由于Associative array和一般语言里面的class太像了,他们之间形成了一一对应关系,这就使得我们可以采用一套标准的代码进行相应的转化。对于自身支持Associative array的弱类型语言,语言自身就具备操作JSON序列化后的数据的能力;对于Java这强类型语言,可以采用反射的方式统一解决,例如Google提供的Gson。 #### 典型应用场景和非应用场景 JSON在很多应用场景中可以替代XML,更简洁并且解析速度更快。典型应用场景包括: 1. 公司之间传输数据量相对小,实时性要求相对低(例如秒级别)的服务。 2. 基于Web browser的Ajax请求。 3. 由于JSON具有非常强的前后兼容性,对于接口经常发生变化,并对可调式性要求高的场景,例如Mobile app与服务端的通讯。 4. 由于JSON的典型应用场景是JSON+HTTP,适合跨防火墙访问。总的来说,采用JSON进行序列化的额外空间开销比较大,对于大数据量服务或持久化,这意味着巨大的内存和磁盘开销,这种场景不适合。没有统一可用的IDL降低了对参与方的约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便,延长开发周期。 由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能要求为ms级别,不建议使用。 #### IDL文件举例 以下是UserInfo序列化之后的一个例子: ~~~ {"userid":1,"name":"messi","address":[{"city":"北京","postcode":"1000000","street":"wangjingdonglu"}]} ~~~ ### Thrift Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是,Thrift并不仅仅是序列化协议,而是一个RPC框架。相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案;但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。 #### 典型应用场景和非应用场景 对于需求为高性能,分布式的RPC服务,Thrift是一个优秀的解决方案。它支持众多语言和丰富的数据类型,并对于数据字段的增删具有较强的兼容性。所以非常适用于作为公司内部的面向服务构建(SOA)的标准RPC框架。 不过Thrift的文档相对比较缺乏,目前使用的群众基础相对较少。另外由于其Server是基于自身的Socket服务,所以在跨防火墙访问时,安全是一个顾虑,所以在公司间进行通讯时需要谨慎。 另外Thrift序列化之后的数据是Binary数组,不具有可读性,调试代码时相对困难。最后,由于Thrift的序列化和框架紧耦合,无法支持向持久层直接读写数据,所以不适合做数据持久化序列化协议。 #### IDL文件举例 ~~~ struct Address {  1: required string city; 2: optional string postcode; 3: optional string street; }  struct UserInfo {  1: required string userid; 2: required i32 name; 3: optional list<address> address; } </address> ~~~ ### Protobuf Protobuf具备了优秀的序列化协议的所需的众多典型特征。 1. 标准的IDL和IDL编译器,这使得其对工程师非常友好。 2. 序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。 3. 解析速度非常快,比对应的XML快约20-100倍。 4. 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。 Protobuf是一个纯粹的展示层协议,可以和各种传输层协议一起使用;Protobuf的文档也非常完善。 但是由于Protobuf产生于Google,所以目前其仅仅支持Java、C#### 典型应用场景和非应用场景 Protobuf具有广泛的用户基础,空间开销小以及高解析性能是其亮点,非常适合于公司内部的对性能要求高的RPC调用。由于Protobuf提供了标准的IDL以及对应的编译器,其IDL文件是参与各方的非常强的业务约束,另外,Protobuf与传输层无关,采用HTTP具有良好的跨防火墙的访问属性,所以Protobuf也适用于公司间对性能要求比较高的场景。由于其解析性能高,序列化后数据量相对少,非常适合应用层对象的持久化场景。 它的主要问题在于其所支持的语言相对较少,另外由于没有绑定的标准底层传输层协议,在公司间进行传输层协议的调试工作相对麻烦。 #### IDL文件举例 ~~~ message Address { required string city=1; optional string postcode=2; optional string street=3; } message UserInfo { required string userid=1; required string name=2; repeated Address address=3; } ~~~ ### Avro Avro的产生解决了JSON的冗长和没有IDL的问题,Avro属于Apache Hadoop的一个子项目。 Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,JSON格式方便测试阶段的调试。 Avro支持的数据类型非常丰富,包括C#### 典型应用场景和非应用场景 Avro解析性能高并且序列化之后的数据非常简洁,比较适合于高性能的序列化服务。 由于Avro目前非JSON格式的IDL处于实验阶段,而JSON格式的IDL对于习惯于静态类型语言的工程师来说不直观。 #### IDL文件举例 ~~~ protocol Userservice {   record Address {    string city;    string postcode;    string street;   }     record UserInfo {    string name;    int userid;    array<Address> address = [];   } } ~~~ 所对应的JSON Schema格式如下: ~~~ { "protocol" : "Userservice", "namespace" : "org.apache.avro.ipc.specific", "version" : "1.0.5", "types" : [ { "type" : "record", "name" : "Address", "fields" : [ { "name" : "city", "type" : "string" }, { "name" : "postcode", "type" : "string" }, { "name" : "street", "type" : "string" } ] }, { "type" : "record", "name" : "UserInfo", "fields" : [ { "name" : "name", "type" : "string" }, { "name" : "userid", "type" : "int" }, { "name" : "address", "type" : { "type" : "array", "items" : "Address" }, "default" : [ ] } ] } ], "messages" : { } } ~~~ ## 五、Benchmark以及选型建议 ### Benchmark 以下数据来自[https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking](http://infoqhelp.sinaapp.com/%5Bhttps://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking%5D)。 #### 解析性能 ![2015-08-04/55c028c6be392](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c028c6be392.png) #### 序列化之空间开销 ![2015-08-04/55c028cdacbb4](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c028cdacbb4.png) 从上图可得出如下结论: 1. XML序列化(Xstream)无论在性能和简洁性上比较差; 2. Thrift与Protobuf相比在时空开销方面都有一定的劣势; 3. Protobuf和Avro在两方面表现都非常优越。 ### 选型建议 以上描述的五种序列化和反序列化协议都各自具有相应的特点,适用于不同的场景。 1. 对于公司间的系统调用,如果性能要求在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。 2. 基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是非常不错的选择。 3. 对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。 4. 当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的竞争关系。 5. 对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在Hadoop子项目里,Avro会是更好的选择。 6. 由于Avro的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro是更好的选择。 7. 对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。 8. 如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。 9. 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。 ## 参考文献 1. [http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity](http://infoqhelp.sinaapp.com/%5Bhttp://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity%5D) 2. [https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking](https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking) 3. [http://en.wikipedia.org/wiki/Serialization](http://en.wikipedia.org/wiki/Serialization) 4. [http://en.wikipedia.org/wiki/Soap](http://en.wikipedia.org/wiki/Soap) 5. [http://en.wikipedia.org/wiki/XML](http://infoqhelp.sinaapp.com/%5Bhttp://en.wikipedia.org/wiki/XML%5D) 6. [http://en.wikipedia.org/wiki/JSON](http://en.wikipedia.org/wiki/JSON) 7. [http://avro.apache.org/](http://avro.apache.org/) 8. [http://www.oracle.com/technetwork/java/rmi-iiop-139743.html](http://www.oracle.com/technetwork/java/rmi-iiop-139743.html)
';

推荐文章|Article

最后更新于:2022-04-01 01:56:57

';

深入解析和反思携程宕机事件

最后更新于:2022-04-01 01:56:55

作者 智锦 携程网宕机事件还在持续,截止28号晚上8点,携程首页还是指向一个静态页面,所有动态网页都访问不了。关于事故根源,网上众说纷纭。作为互联网运维老兵,尝试分析原因,谈谈我的看法。 ## 宕机原因分析 网上有各种说法,有说是数据库数据和备份数据被物理删除的。也有说是各个节点的业务代码被删除,现在重新在部署。也有说是误操作,导致业务不可用,还有说是黑客攻击甚至是内部员工恶意破坏的。 先说一下最早传出来的“数据库物理删除”,其实这个提法就很不专业,应该是第一个传播者,试图强调问题之严重和恢复之困难,所以用了一个普通电脑用户比较熟悉的“物理删除”的概念。实际上,任何一个网站的数据库,都分为本地高可用备份、异地热备、磁带冷备三道防线,相应的数据库管理员、操作系统管理员、存储管理员三者的权限是分离的,磁带备份的数据甚至是保存在银行的地下金库中的。从理论上而言,很难有一个人能把所有的备份数据都删除,更不用说这个绘声绘色的物理删除了。 第二个则是黑客攻击和内部员工破坏的说法,这个说法能满足一些围观者猎奇的心理,因此也传播的比较快。但理性分析,可能性也不大。黑客讲究的是潜伏和隐蔽,做这种事等于是在做自杀性攻击。而内部员工也不太可能,我还是相信携程的运维人员的操守和职业素养,在刑法的威慑下,除非像“法航飞行员撞山”那种极个别案列,正常情况下不太可能出现人为恶意的可能性。 从现象上看,确实是携程的应用程序和数据库都被删除。我分析,最大的可能还是运维人员在正常的批量操作时出现了误操作。我猜测的版本是:携程网被“乌云”曝光了一个安全漏洞,漏洞涉及到了大部分应用服务器和数据库服务器;运维人员在使用pssh这样的批量操作执行修复漏洞的脚本时,无意中写错了删除命令的对象,发生了无差别的全局删除,所有的应用服务器和数据库服务器都受到了影响。这个段子在运维圈子中作为笑话流传了很多年,没想到居然真的有这样一天。 ## 为什么恢复的如此缓慢? 从上午11点传出故障,到晚上8点,携程网站一直没能恢复。所以很多朋友很疑惑:“为什么网站恢复的如此缓慢?是不是数据库没有备份了?”这也是那个“数据库物理删除”的说法很流行的一个根源。实际上这个还是普通用户,把网站的备份和恢复理解成了类似我们的笔记本的系统备份和恢复的场景,认为只有有备份在,很快就能导入和恢复应用。 实际上大型网站,远不是像把几台应用和数据库服务器那么简单。看似很久都没有变化的一个网站,后台是一个由SOA(面向服务)架构组成的庞大服务器集群,看似简单的一个页面背后由成百上千个应用子系统组成,每个子系统又包括若干台应用和数据库服务器,大家可以理解为每一个从首页跳转过去的二级域名都是一个独立的应用子系统。这上千的个应用子系统,平时真正经常发布和变更的,可能就是不到20%的核心子系统,而且发布时都是做加法,很少完全重新部署一个应用。 在平时的运维过程中,对于常见的故障都会有应急预案。但像携程这次所有系统包括数据库都需要重新部署的极端情况,显然不可能在应急预案的范畴中。在仓促上阵应急的情况下,技术方案的评估和选择问题,不同技术岗位之间的管理协调的问题,不同应用系统之间的耦合和依赖关系,还有很多平时欠下的技术债都集中爆发了,更不用说很多不常用的子系统,可能上线之后就没人动过,一时半会都找不到能处理的人。更要命的是,网站的核心系统,可能会写死依赖了这个平时根本没人关注的应用,想绕开边缘应用只恢复核心业务都做不到。更别说在这样的高压之下,各种噪音和干扰很多,运维工程师的反应也没有平时灵敏。 简单的说,就算所有代码和数据库的备份都存在,想要快速恢复业务,甚至比从0开始重新搭建一个携程更困难。携程的工程师今天肯定是一个不眠夜。乐观的估计,要是能在24小时之内恢复核心业务,就已经非常厉害了。 天下运维是一家。携程的同行加油,尽快度过难关! ## 故障根源反思:黑盒运维之殇 携程的这次事件,不管原因是什么,都会成为IT运维历史上的一个标志性事件。相信之后所有的IT企业和技术人员,都会去认真的反思,总结经验教训。但我相信,不同的人在不同的位置上,看到的东西可能是截然相反的,甚至可能会有不少企业的管理者受到误导,开始制定更严格的规章制度,严犯运维人员再犯事。在此,我想表明一下我的态度:这是一个由运维引发的问题,但真正的根源其实不仅仅在运维,预防和治理更应该从整个企业的治理入手。 长久以来,在所有的企业中,运维部门的地位都是很边缘化的。企业的管理者会觉得运维部门是成本部门,只要能支撑业务就行。业务部门只负责提业务需求,开发部门只管做功能的开发,很多非功能性的问题无人重视,只能靠运维人员肩挑人扛到处救火,可以认为是运维部门靠自己的血肉之躯实现了业务部门的信息化。在这样的场景下,不光企业的管理者不知道该如何评价运维的价值,甚至很多运维从业者都不知道自己除了到处救火外真正应该关注什么,当然也没有时间和精力去思考。 在上文的情况下,传统的运维人员实际上是所谓的“黑盒运维”,不断的去做重复性的操作,时间长了之后,只知道自己管理的服务器能正常对外服务,但是却不知道里面应用的依赖关系,哪些配置是有效配置、哪些是无效配置,只敢加配置,不敢删配置,欠的技术债越来越多。在这样的情况下,遇到这次携程的极端案列,需要完整的重建系统时候,就很容易一筹莫展了。 对于这样的故障,我认为真正有效的根源解决做法是从黑盒运维走向白盒运维。和Puppet这样的运维工具理念一致,运维的核心和难点其实是配置管理,运维人员只有真正的清楚所管理的系统的功能和配置,才能从根源上解决到处救火疲于奔命的情况,也才能真正的杜绝今天携程这样的事件重现,从根本上解决运维的问题。 从黑盒运维走向白盒运维,再进一步实现DevOps(开发运维衔接)和软件定义数据中心,就是所谓的运维2.0了。很显然,这个单靠运维部门自身是做不到的,需要每一个企业的管理者、业务部门、开发部门去思考。因此,我希望今天这个事件,不要简单的让运维来背黑锅,而是让大家真正的从中得到教训和启示。
';

Node.js与io.js那些事儿

最后更新于:2022-04-01 01:56:53

作者 朴灵 去年12月,多位重量级Node.js开发者不满Joyent对Node.js的管理,自立门户创建了io.js。io.js的发展速度非常快,先是于2015年1月份发布了1.0版本,并且很快就达到了2.0版本,社区非常活跃。而[最近io.js社区又宣布](http://www.infoq.com/cn/news/2015/05/nodejs-iojs),这两个项目将合并到Node基金会下,并暂时由“Node.js和io.js核心技术团队联合监督”运营。本文将聊一聊Node.js项目的一些历史情况,与io.js项目之间的恩怨纠葛,他们将来的发展去向。希望能从历史的层面去了解这个开源项目在运营模式上是如何演变和发展的。 ## Node.js项目的由来 自从JavaScript被Brendan Eich创造出来后,除了应用在浏览器中作为重要的补充外,人类从来就没有放弃过将JavaScript应用到服务端的想法。这些努力从livewired项目(1994年12月)开始,就从来没有停止过。如果你不知道livewired,那应该知道ASP中可以使用JScript语言(1996年),或者Rhino。但直到2009年,这些服务端JavaScript技术与同样应用在服务端的Java、PHP相比,显得相对失色。 谈到Node.js的由来,不可避免要聊到它的创始人Ryan Dahl。在2009年时,服务端JavaScript迎来了它的拐点,因为Ryan Dahl带来了Node.js,在那之后Node.js将服务端JavaScript带入了新的境地,大量的JavaScript在GitHub上被贡献出来,大量的JavaScript模块出现,出现了真正的繁荣。 Node.js不是凭空出现的项目,也不是某个Web前端工程师为了完成将JavaScript应用到服务端的理想而在实验室里捣鼓出来的。它的出现主要归功于Ryan Dahl历时多年的研究,以及一个恰到好处的节点。2008年V8随着Chrome浏览器的出世,JavaScript脚本语言的执行效率得到质的提升,这给Ryan Dahl带来新的启示,他原本的研究工作与V8之间碰撞出火花,于是带来了一个基于事件的高性能Web服务器。 ![2015-08-04/55c027acda649](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c027acda649.png) 上图为Node.js创始人Ryan Dahl。 Ryan Dahl的经历比较奇特,他并非科班出身的开发者,在2004年的时候他还在纽约的罗彻斯特大学数学系读博士,期间有研究一些分形、分类以及p-adic分析,这些都跟开源和编程没啥关系。2006年,也许是厌倦了读博的无聊,他产生了『世界那么大,我想去看看』的念头,做出了退学的决定,然后一个人来到智利的Valparaiso小镇。那时候他尚不知道找一个什么样的工作来糊口,期间他曾熬夜做了一些不切实际的研究,如如何通过云进行通信。下面是这个阶段他产出的中间产物,与后来苹果发布的iCloud似乎有那么点相似。 ![2015-08-04/55c027ca9e84c](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c027ca9e84c.png) 从那起,Ryan Dahl不知道是否因为生活的关系,他开始学习网站开发了,走上了码农的道路。那时候Ruby on Rails很火,他也不例外的学习了它。从那时候开始,Ryan Dahl的生活方式就是接项目,然后去客户的地方工作,在他眼中,拿工资和上班其实就是去那里旅行。此后他去过很多地方,如阿根廷的布宜诺斯艾利斯、德国的科隆、奥地利的维也纳。 Ryan Dahl经过两年的工作后,成为了高性能Web服务器的专家,从接开发应用到变成专门帮客户解决性能问题的专家。期间他开始写一些开源项目帮助客户解决Web服务器的高并发性能问题,尝试过的语言有Ruby、C、Lua。当然这些尝试都最终失败了,只有其中通过C写的HTTP服务库libebb项目略有起色,基本上算作libuv的前身。这些失败各有各的原因,Ruby因为虚拟机性能太烂而无法解决根本问题,C代码的性能高,但是让业务通过C进行开发显然是不太现实的事情,Lua则是已有的同步I/O导致无法发挥性能优势。虽然经历了失败,但Ryan Dahl大致的感觉到了解决问题的关键是要通过事件驱动和异步I/O来达成目的。 在他快绝望的时候,V8引擎来了。V8满足他关于高性能Web服务器的想象: 1. 没有历史包袱,没有同步I/O。不会出现一个同步I/O导致事件循环性能急剧降低的情况。 2. V8性能足够好,远远比Python、Ruby等其他脚本语言的引擎快。 3. JavaScript语言的闭包特性非常方便,比C中的回调函数好用。 于是在2009年的2月,按新的想法他提交了项目的第一行代码,这个项目的名字最终被定名为“node”。 2009年5月,Ryan Dahl正式向外界宣布他做的这个项目。2009年底,Ryan Dahl在柏林举行的JSConf EU会议上发表关于Node.js的演讲,之后Node.js逐渐流行于世。 以上就是Node.js项目的由来,是一个专注于实现高性能Web服务器优化的专家,几经探索,几经挫折后,遇到V8而诞生的项目。 ## Node.js项目的组织架构和管理模式 Node.js随着JSConf EU会议等形式的宣传下,一家位于硅谷的创业公司注意到了该项目。这家公司就是Joyent,主要从事云计算和数据分析等。Joyent意识到Node.js项目的价值,决定赞助这个项目。Ryan Dahl于2010年加入该公司,全职负责Node.js项目的开发。此时Node.js项目进入了它生命历程里的第二个阶段:从个人项目变成一个公司组织下的项目。 这个阶段可以从2010年Ryan Dahl加入Joyent开始到2014年底Mikeal Rogers发起Node Forward结束,Node的版本也发展到了v0.11。这个时期,IT业中的大多数企业都关注过Node.js项目,如微软甚至对于Node.js对Windows的移植方面做过重要的贡献。 这个时期可以的组织架构和管理模式可以总结为“Gatekeeper + Joyent”模式。 Gatekeeper的身份类似于项目的技术负责人,对技术方向的把握是有绝对权威。历任的Gatekeeper为:Ryan Dahl、Isaac Z. Schlueter、Timothy J Fontaine,均是在Node.js社区具有很高威望的贡献者。项目的法律方面则由Joyent负责,Joyent注册了“Node.js”这个商标,使用其相关内容需要得到法律授权(如笔者《深入浅出Node.js》上使用了Node.js的Logo,当时是通过邮件的形式得到过授权)。技术方面除了Gatekeeper外,还有部分core contributor。core contributor除了贡献重要feature外,帮助项目进行日常的patch提交处理,协助review代码和合并代码。项目中知名的core contributor有Ben Noordhuis,Bert Belder、Fedor Indutny、Trevor Norris、Nathan Rajlich等,这些人大多来自Joyent公司之外,他们有各自负责的重要模块。Gatekeeper除了要做core contributor的事情外,还要决定版本的发布等日常事情。 Node.js成为Joyent公司的项目后,Joyent公司对该项目的贡献非常大,也没有过多的干涉Node.js社区的发展,还投入了较多资源发展它,如Ryan Dahl、Isaac Z. Schlueter、Timothy J Fontaine等都是Joyent的全职员工。 ## Node.js社区的分裂 “Gatekeeper + Joyent”模式运作到2013年的时候都还工作良好,蜜月期大概中止于第二任Gatekeeper Isaac Z. Schlueter离开Joyent自行创建npm inc.公司时期。前两任Gatekeeper期间,Node.js的版本迭代都保持了较高的频率,大约每个月会发布一个小版本。在Isaac Z. Schlueter卸任Gatekeeper之后,Node.js的贡献频率开始下降,主要的代码提交主要来自社区的提交,代码的版本下降到三个月才能发布一个小版本。社区一直期待的1.0版本迟迟不能发布。这个时期Node.js属于非常活跃的时期,但是对于Node.js内核而言却进展缓慢。技术方向上似乎是有些不明朗,一方面期待内核稳定下来,一方面又不能满足社区对新feature的渴望(如ES6的特性迟迟无法引入)。 第三任的Gatekeeper Timothy J Fontaine本人也意识到这个问题。从他上任开始,主要的工作方向就是解决该问题。他主要工作是Node on the road活动,通过一系列活动来向一些大企业用户获取他们使用Node.js的反馈。通过一些调研,他做了个决定,取消了贡献者的CLA签证,让任何人可以贡献代码。 尽管Timothy J Fontaine的做法对Node.js本身是好的,但是事情没有得到更好的改善。这时候Node.js项目对社区贡献的patch处理速度已经非常缓慢,经常活跃的core contributor只有Fedor Indutny、Trevor Norris。另外还发生了[人称代词](http://www.infoq.com/cn/news/2013/12/the-power-of-a-pronoun)的事件,导致Node.js/libuv项目中非常重要的贡献者Ben Noordhuis离开core contributor列表,这件事情被上升到道德层面,迎来了不少人的谩骂。其中Joyent的前任CEO甚至还致信表示如果是他的员工,会进行开除处理。这致使Node.js项目的活跃度更低。Node.js的进展缓慢甚至让社区的知名geek TJ Holowaychuk都选择离开Node.js而投入Go语言的怀抱。 可以总结这个时期是“Gatekeeper + Joyent”模式的末期。Joyent对于项目的不作为和其他层面对社区其他成员的干预,导致项目进展十分缓慢,用蜗牛的速度来形容一点也不为过。尽管Timothy J Fontaine试图挽回些什么,也有一些行为来试图重新激活这个项目的活力,但是已经为时已晚。 这时一个社区里非常有威望的人出现了,他就是Mikeal Rogers。Mikeal Rogers的威望不是建立在他对Node.js项目代码的贡献上,他的威望主要来自于request模块和JSConf会议。其中JSConf是JavaScript社区最顶级的会议,他是主要发起人。 在2014年8月,以Mikeal Rogers为首,几个重要core contributor一起发起了一个叫做“Node forword”的组织。该组织致力于发起一个由社区自己驱动来提升Node、JavaScript和整个生态的项目。 ![2015-08-04/55c027d980671](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c027d980671.png) “Node forword”可以视作是io.js的前身。这些core contributor们在“Node forword”上工作了一段时间,后来因为可能涉及到Node这个商标问题,Fedor Indutny愤而fork了Node.js,改名为io.js,宣告了Node.js社区的正式分裂。 简单点来说这件事情主要在于社区贡献者们对于Joyent公司的不满,导致这些主要贡献者们想通过一个更开放的模式进行协作。复杂点来说这是公司开源项目管理模式的问题所在,当社区方向和公司方向一致时,必然对大家都有好处,形同蜜月期,但当两者步骤不一致时,分歧则会暴露出来。这点在Node.js项目的后期表现得极为明显,社区觉得项目进展缓慢,而Joyent公司的管理层则认为他稳定可靠。 ## io.js与Node.js advisory board 在“Node Forward”的进展期间,社区成员们一起沟通出了一个基本的开放的管理模式。这个模式在io.js期间得到体现。 ![2015-08-04/55c027e133cbf](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c027e133cbf.png) io.js的开放管理模式主要体现在以下方面: * 不再有Gatekeeper。取而代之的是TC(Technical Committee),也就是技术委员会。技术委员会基本上是由那些有很多代码贡献的core contributor组成,他们来决定技术的方向、项目管理和流程、贡献的原则、管理附加的合作者等。当有分歧产生时(如引入feature),采用投票的方式来决定,遵循少数服从多数的简单原则。基本上原来由一个人担任的Gatekeeper现在由一个技术委员会来执行。如果要添加一个新成员为TC成员,需要由一位现任的TC成员提议。每个公司在TC中的成员不能超过总成员的1/3。 * 引入Collaborators。代码仓库的维护不仅仅局限在几个core contributor手中,而是引入Collaborators,也就是合作者。可以理解为有了更多的core contributor。 * TC会议。之前的的沟通方式主要是分布式的,大家通过GitHub的issue列表进行沟通。这种模式容易堆积问题,社区的意见被接受和得到处理取决于core contributor的情况。io.js会每周举行TC会议,会议的内容主要就issue讨论、解决问题、工作进展等。会议通过Google Hangout远程进行,由TC赞同的委任主席主持。会议视频会发布在YouTube上,会议记录会提交为文档放在代码仓库中。 * 成立工作组。在项目中成立一些细分的工作组,工作组负责细分方向上的工作推进。 io.js项目从fork之后,于2015-01-14发布了v1.0.0版本。自此io.js一发不可收拾,以周为单位发布新的版本,目前已经发布到2.0.2。io.js项目与Node.js的不同在行为上主要体现在以下方面: * 新功能的激进。io.js尽管在架构层面依然保持着Node.js的样子(由Ryan Dahl时确立),但是对于ECMAScript 6持拥抱态度。过去在Node.js中需要通过flag来启用的新功能,io.js中不再需要这些flag。当然不用flag的前提是V8觉得这个feature已经稳定的情况下。一旦最新的Chrome采用了新版本的V8,io.js保持很快的跟进速度。 * 版本迭代。io.js保持了较高频率的迭代,以底层API改变作为大版本的划分,但对于小的改进,保持每周一个版本的频率。只要是改进,io.js项目的TC和Collaborators都非常欢迎,大到具体feature或bug,小到文档改进都可以被接受,并很快放出版本。 * issue反馈。Node.js的重要的贡献者们都在io.js上工作,Node.js和io.js项目的问题反馈速度几乎一致,但是问题处理速度上面io.js以迅捷著称,基本在2-3天内必然有响应,而Node.js则需要1个礼拜才有回复。 基本上而言原本应该属于Node.js项目的活力现在都在io.js项目这里。如果没有其他事情的发生,io.js可以算作社区驱动开源项目的成功案例了。 当然,尽管在Node.js这边进展缓慢,但Joyent方面还是做出了他们的努力。在“Node Forward”讨论期间,Joyent成立了临时的Node.js顾问委员会[https://nodejs.org/about/advisory-board/](https://nodejs.org/about/advisory-board/)。顾问委员会的主要目标与“Node Forward”的想法比较类似,想借助顾问委员会的形式来产出打造一个更加开放的管理模式,以找到办法来平衡所有成员的需要,为各方提供一个平台来投入资源到Node.js项目。 顾问委员会中邀请了很多重要的Contributor和一些Node.js重度用户的参与。开了几次会议来进行探讨和制定新的管理模式。于是就出现了一边是io.js如火如荼发布版本,Joyent这边则是开会讨论的情况。顾问委员会调研了IBM(Eclipse)、Linux基金会、Apache等,决定成立Node.js基金会的形式。 ## io.js与Node.js基金会 时间来到2015年1月,临时委员会正式发布通告决定将Node.js项目迁移到基金会,并决定跟io.js之间进行和解。简单点来说Node.js方面除了版本的进展比较缓慢外,确实是在制定一个新的模式来确保Node.js项目的下一步发展,Joyent公司本着开放的原则,也做出相当大的让步,保持着较为和谐的状态。 然而io.js动作太快,代码的进展程度远远快于Node.js项目,和解的讨论从2月开始讨论,到5月才做出决定。这时io.js已经发布了它的2.0版本。 最终的结论是Node.js项目和io.js项目都将加入Node.js基金会。Node.js基金会的模式与io.js较为相似,但是更为健全。Mikeal Rogers在他的一篇名为[《Growing Up》](https://medium.com/node-js-javascript/growing-up-27d6cc8b7c53)的文章中提到io.js项目需要一个基金会的原因。 io.js项目在技术方面的成熟度显然要比最初的Gatekeeper时代要更为先进,给予贡献者更多的管理权利。然而在市场和法律方面,还略显幼稚。最终无论是顾问委员会,还是io.js都选定以基金会的形式存在。这个基金会参考Linux基金会的形式,由董事会和技术委员会组成,董事会负责市场和法律方面的事务,技术委员会负责技术方向。 ![2015-08-04/55c027ec119b3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c027ec119b3.png) 就像《三国演义》所述:天下大势,合久必分,分久必合。Node.js项目也从Joyent公司的怀里走出来,成长为基金会的形式,进入这个项目生命周期里第三个阶段。 ## 后续 从io.js的分裂到Node.js基金会,从外人看起来似乎如一场闹剧一般,然而这个过程中可以看到一个开源项目自身的成长。尽管io.js将归于Node.js基金会,像一个离家出走的孩子又回家一般,它的出走可能要被人忘记,但从当初的出发点来说,这场战役,io.js其实是赢家。穷则思变、不破不立是对Joyent较为恰当的形容。如果Joyent能提前想到这些,则不会有社区分裂的事情发生。 Node.js处于停滞状态的开发和io.js的活跃情况之间,目前免不了大量的Merge工作。作为和解的条件之一,Node.js基金会之后Node版本的发布将基于目前io.js的进展来进行。后续的合并工作示意如下: ~~~ now (io.js) v2.0 : v2.x | | : | v0.10.x /--------------:-----------------\ Node.js 2.0 ____|____/ : \______|_____ \ : / \--------------:-----------------/ | | : | | (node.js) v0.12.x : v0.13.x v0.14.x ~~~ 在未完成合并之前,io.js会继续保持发布。Node.js的下个大版本跨过1.0,直接到2.0。 io.js项目的TC将被邀请加入Node.js基金会的TC,毕竟两者在技术管理方面达成了一致。基金会将在黄金和白银会员中选举出董事、技术委员会成员中选举出技术委员主席。 对于成为Node.js基金会成员方面,企业可以通过赞助的方式注册成为会员。 ## 总结 一个开源项目成长起来之后,就不再是当初创始人个人维护的那个样子了。Node.js项目的发展可以说展现了一个开源项目是如何成长蜕变成成熟项目的。当然我们现在说Node.js基金会是成功的还为时尚早,但是祝福它。 ## 参考文档 * [https://github.com/joyent/node/issues/9295](https://github.com/joyent/node/issues/9295) * [https://github.com/iojs/io.js/issues/978](https://github.com/iojs/io.js/issues/978) * [https://github.com/iojs/io.js/issues/1336](https://github.com/iojs/io.js/issues/1336) * [https://github.com/iojs/io.js/issues/1416](https://github.com/iojs/io.js/issues/1416) * [https://github.com/iojs/io.js/labels/meta](https://github.com/iojs/io.js/labels/meta) * [http://blog.nodejs.org/2015/05/08/transitions/](http://blog.nodejs.org/2015/05/08/transitions/) * [http://blog.nodejs.org/2015/05/08/next-chapter/](http://blog.nodejs.org/2015/05/08/next-chapter/) * [https://github.com/iojs/io.js/issues/1664](https://github.com/iojs/io.js/issues/1664) * [http://tinyclouds.org/nodeconf2012.pdf](http://tinyclouds.org/nodeconf2012.pdf) * [https://www.joyent.com/blog/introducing-the-nodejs-foundation](https://www.joyent.com/blog/introducing-the-nodejs-foundation) * [http://blog.nodejs.org/2015/05/15/node-leaders-are-building-an-open-foundation/](http://blog.nodejs.org/2015/05/15/node-leaders-are-building-an-open-foundation/) * [https://medium.com/node-js-javascript/growing-up-27d6cc8b7c53](https://medium.com/node-js-javascript/growing-up-27d6cc8b7c53)
';

热点 | Hot

最后更新于:2022-04-01 01:56:51

';

卷首语

最后更新于:2022-04-01 01:56:48

# Dart语言的未来在哪里? Google推出的Go语言这两年火的是一塌糊涂,而同样是亲爹生的Dart语言这些年却一直不温不火。什么是Dart语言了?我先来简单解释下: > Dart是Google于2011年发布的一门开源编程语言,目标是为开发现代Web程序提供结构化但又不乏灵活性的编程语言,其实就是弥补JavaScript的不足。Dart在JavaScript语言的基础上,改进了编程效率和执行性能,大幅度减少了编程的复杂性。相比JavaScript,Dart语言更加简单和高效,它支持类和接口,是一门纯面向对象的语言。Dart在动态语言的基础上,结合了静态语言的优点,有很多不错的特性,比如可选类型、并发编程、工厂构造函数、级联调用。Dart代码可以用两种不同方式执行:一是通过原生的虚拟机(可以集成到浏览器);另一种则是通过Google的Dart2js编译器将Dart代码转换为JavaScript代码,然后再执行。 从发布之初,Dart语言要做的就是颠覆JavaScript,确实,JavaScript这门语言缺陷有很多,不过这也可以理解,因为JavaScript从设计到发布仅有几个月的时间,可以说非常仓促。而Dart语言在设计时借鉴了很多现代语言的思路,它在性能、易用性等方面都远远超过了JavaScript。但从现在的情况来看,Dart语言似乎并没有发展起来。这从最近的新闻里就能看出来,4月,谷歌确认他们不会再将Dart VM集成到Chrome中,也就是说,要使用Dart语言替换JavaScript几乎不可能,因为现在用户只能使用编译为JavaScript的方式使用Dart。 也许Google对Dart语言的定位早有了变化,所以才宣布在Chrome中放弃Dart。Android应用基本都是使用Java创建的,这俩还打过不少官司,Google也是吃了不少哑巴亏。5月初,Google发布了跨平台框架:Sky。Sky基于Dart语言编写,因为Dart本身就是与平台无关的,所以Sky的目标是跨平台。最近比较火的跨平台框架是react-native,Sky其实和React差不多,或者说是参考了React的设计哲学,只不过一个使用JavaScript,一个使用Dart。 当然,问题又回来了,有了React,为什么还要用Sky?JavaScript已经获得了各个平台的支持,所以React推广起来也不费事,但Dart又面临的同样的问题,其它平台会支持Dart吗? 不管怎么样,Dart终是迈出了属于自己的[一大步](https://www.youtube.com/watch?t=133&v=PnIWl33YMwA)。
';