first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 _导读 _ 什么是“The Fenix Project”
你好,我是周志明。
在开篇词中我在介绍“探索与实践”这个模块的时候提到会带你开发不同架构的Fenixs Bookstore。你是不是还不清楚这个项目是啥以及为什么要做这么一个项目。所以这一讲我要再和你说一说这门课的来源这样你就会更清楚为什么会这么设计了。
因为我一直看重“布道与分享”对梳理、扎实知识体系的重大作用所以便萌生了把自己这十几年软件开发工作中用到的架构知识进行梳理并以文字的形式分享出来的想法。于是2019年底我就开始了这项浩大的工程在GitHub写了一部叫做《软件架构探索The Fenix Project》的开源文档。
后来我又和极客时间的编辑讨论,为了让更多的开发者能从中收益,让他们可以相对轻松地跟着我一起进行这次的软件架构探索之旅,所以就再一次整理成了“图文+音频”的形式。
后面,我也会基于这个开源文档再出版一本纸质图书。如果你在这门课更新的过程中,分享了优质的留言并被课程编辑展示了出来,我也会送你一本有我亲笔签名的书。
其实“软件架构探索”的意思是清晰的但乍一听到“The Fenix Project”是不是还很难判断这门课到底要做什么呢。确实如此所以接下来我们就先从“Phoenix”这个词说起吧。
软件架构探索
“Phoenix”的字面意思就是“凤凰”或者是“不死鸟”这个词我们东方人不太常用但它在西方的软件工程读物尤其是关于Agile、DevOps话题的作品中经常会出现。
比如说软件工程小说《The Phoenix Project》就讲述了徘徊在死亡边缘的Phoenix项目在精益方法下浴火重生的故事马丁 · 福勒Martin Fowler对《Continuous Delivery》持续交付的诠释里也多次提到过“Phoenix Server”取其能够“涅槃重生”之意与“Snowflake Server”取其“世界上没有相同的两片雪花”之意的优劣比对。
The Phoenix Project
也许是东西方文化差异的原因,尽管我们东方人会说“失败是成功之母”,但骨子里还是更注重一次就能把事做对、做好,尽量别出乱子;而西方人则要“更看得开”一些,把出错看作是正常、甚至是必须的发展过程,只要出了问题能够兜底,能重回正轨就好。
其实在软件工程的世界里,任何产品的研发,只要时间尺度足够长,人就总会疏忽犯错,代码就总会带有缺陷,电脑就总会宕机崩溃,网络就总会堵塞中断……
所以如果一项工程需要大量的人员共同去研发,并保证它们分布在网络中的大量服务器节点能够同时运行,那么随着项目规模的增大、运作时间变长,它必然会受到墨菲定律的无情打击。
Murphys Law: Anything that can go wrong will go wrong.-
墨菲定律:凡事只要有可能出错,那就一定会出错。
这样问题就来了:为了得到高质量的软件产品,我们是应该把精力更多地集中在提升每一个人员、过程、产出物的能力和质量上,还是放在整体流程和架构上?
这里我先给一个“和稀泥”式的回答:这两者都重要。前者重术,后者重道;前者更多与编码能力相关,后者更多与软件架构相关;前者主要由开发者个体水平决定,后者主要由技术决策者水平决定。
但是,我也必须要强调这个问题的另外一面:这两者的理解路径和抽象程度是不一样的。
如何学习一项具体的语言、框架、工具比如Java、Spring、Vue.js等等都是相对具象的不论其蕴含的内容多少、复杂程度的高低它们至少是能看得见、摸得着的。
而如何学习某一种风格的架构方法,比如单体、微服务、服务网格、无服务、云原生,等等,则是相对抽象的,谈论它们可能要面临着“一百个人眼中有一百个哈姆雷特”的困境。
所以,探讨这方面的话题,要想言之有物,就不能只是单纯的经验陈述了。
那么我就想,回到这些架构根本的出发点和问题上,带你一起真正去使用这些不同风格的架构方法,来实现某些需求、解决某些问题,然后在实践中观察它们的异同优劣,这会是一种很好的,也许是最好的学习方式。
可靠的系统
我们接着前面提出的“人与系统”的探讨,再来思考一个问题:构建一个大规模但依然可靠的软件系统,是否可行?
看到这个问题,你的第一感觉可能会认为有点荒谬:废话。如果这个事情从理论上来说就根本不可能的话,那我们这些做软件开发的还在瞎忙活些什么呢?
但你再仔细想想,根据“墨菲定律”和在“大规模”这个前提下,在做软件开发时,你一定会遇到各种不靠谱的人员、代码、硬件、网络等因素。那你从中就能得出一个听起来很符合逻辑直觉的推论:如果一项工作,要经过多个“不靠谱”的过程相互协作来完成,其中的误差应该会不断地累积叠加,导致最终结果必然不能收敛稳定才对。
这个问题,也并不是杞人忧天、庸人自扰式的瞎操心,计算机之父冯 · 诺依曼John von Neumann在1940年代末期就曾经花了大约两年的时间来研究这个问题并且得出了一门理论《自复制自动机》Theory of Self-Reproducing Automata这个理论以机器应该如何从基本的部件中构造出与自身相同的另一台机器引出。
他的目的并不是想单纯地模拟或者理解生物体的自我复制,也并不是简单地想制造自我复制的计算机,而就是想回答一个理论问题:如何用一些不可靠的部件来构造出一个可靠的系统。
当时自复制机的艺术表示
所以说,自复制机恰好就是一个最好的、用不可靠部件构造的可靠系统的例子。这里的“不可靠部件”,你可以理解为构成生命的大量细胞、甚至是分子。由于热力学扰动、生物复制差错等因素的干扰,这些分子本身并不可靠。
但是,生命系统之所以可靠的本质,恰恰是因为它可以使用不可靠的部件来完成遗传迭代。这其中的关键点,便是承认细胞、分子等这些零部件可能会出错,某个具体的零部件可能会崩溃消亡,但在存续生命的微生态系统中,一定会有其后代的出现,重新代替该零部件的作用,以维持系统的整体稳定。
因而在这个微生态里每一个部件都可以看作是一只不死鸟Phoenix它会老迈而之后又能涅槃重生。
虽然几乎是在计算机诞生的同时计算机科学家就开始研究如何构造可靠的软件系统并且得到了“像Phoenix一样迭代的生态才是可靠的”明确的结论但是软件架构却不是一蹴而就地直接照这个结论去设计。原因也很简单因为软件架构有一个逐渐演进的过程。
架构的演进
软件架构风格从大型机Mainframe发展到了多层单体架构Monolithic到分布式Distributed到微服务Microservices到服务网格Service Mesh到无服务Serverless……你能发现在技术架构上确实呈现出“从大到小”的发展趋势。
这几年微服务兴起后,出现了各类文章去总结、去赞美它的各种好处,比如简化了部署、逻辑拆分更清晰、便于技术异构、易于伸缩拓展应对更高的性能,等等。没错,这些都是微服务架构非常重要的优点,也是企业去搭建微服务的动力。
可是,如果不拘泥于特定系统或特定的某个问题,我们从更宏观的角度来看,前面所列举的这些好处,都只能算是“锦上添花”、是让系统“活得更好”的动因,肯定比不上系统如何“确保生存”的需求来得更关键、本质。
在我看来,架构演变最重要的驱动力,或者说产生这种“从大到小”趋势的最根本的驱动力,始终都是为了方便某个服务能够顺利地“死去”与“重生”而设计的。个体服务的生死更迭,是关系到整个系统能否可靠续存的关键因素。
我举个例子。假设某个企业中应用的是单体架构的Java系统它的更新、升级都必须要有固定的停机计划必须在特定的时间窗口内才能按时开始而且必须按时结束。如果出现了非计划之中的宕机那就是生产事故。
但是软件的缺陷不会遵循领导定下的停机计划来“安排时间出错”所以为了应对缺陷与变化做到不停机地检修Java曾经搞出了OSGi和JVMTI Instrumentation等这样复杂的HotSwap方案以实现给奔跑中的汽车更换轮胎这种匪夷所思却又无可奈何的需求。
而在微服务架构的视角下所谓的系统检修只不过是一次在线服务更新而已先停掉1/3的机器升级新的软件版本再有条不紊地导流、测试、做金丝雀发布一切都是显得如此理所当然而在无服务架构的视角下我们甚至都不可能去关心服务所运行的基础设施甚至连机器是哪台都不用知道停机升级什么的就根本无从谈起了。
流水不腐,有老朽、有消亡、有重生、有更迭,才是正常生态的运作合理规律。
那么你来设想一下如果你的系统中每个部件都符合“Phoenix”的特性哪怕其中的某些部件采用了极不靠谱的程序代码哪怕存在严重的内存泄漏问题最多只能服务三分钟就一定会崩溃。而即便这样只要在整体架构设计中有恰当的、自动化的错误熔断、服务淘汰和重建的机制那在系统外部来观察它在整体上仍然有可能表现出稳定和健壮的服务能力。
铺垫到这里我就可以给你解释清楚到底什么是“The Fenix Project”了。
为什么叫做“The Fenix Project”
你应该也知道在企业软件开发的历史中当发布一项新技术的时候常常会有伴以该技术开发的“宠物店PetStore”作为演示的传统如J2EE PetStore、.NET PetShop、Spring PetClinic等
所以在课程里我在带你做不同架构风格的演示时也希望能遵循这个传统。不过无奈的是我从来没养过宠物于是就改行开了书店Fenixs Bookstore里面出售了几本我写过的书算是夹带了一点私货这样也避免了在使用素材时可能产生的版权问题。
另外尽管我相信没有人会误解但我还是想多强调一句Oracle、Microsoft、Pivotal等公司设计宠物店的目的绝不是为了日后能在网上贩卖小猫小狗他们只是在纯粹地演示技术。
所以说你也不要以“实现这种学生毕业设计复杂度的需求却引入如此规模的架构或框架纯属大炮打苍蝇肯定是过度设计”的眼光来看待这个“Fenixs Bookstore”项目。
相反,如果可能的话,我会在有新的技术、框架发布出来的时候,持续更新,以恰当的形式添加到项目的不同版本中,让它的技术栈越来越复杂。我希望把这些新的、不断发展的知识,融合进已有的知识框架之中,让自己学习、理解、思考,然后将这些技术连同自己的观点看法,分享给你。
说到这儿我和“Fenix”这个名字还有一段奇妙的缘分。在二十多年前我就开始用“IcyFenix”这个网名了。这个名字来源于暴雪公司的即时战略游戏《星际争霸》里面有一个Protoss普罗托斯英雄叫Fenix菲尼克斯。就像这个名字所预示的那样Fenix曾经是Zealot狂热者牺牲后以Dragoon龙骑兵的形式重生带领Protoss与刀锋女王Kerrigan凯瑞甘继续抗争。
所以既然我们要开始一段关于“Phoenix”的代码与故事那便叫它“The Fenix Project”如何
好了,现在你对这门课程的设计和讲解思路就已经非常了解了,那么,你是否也制定了一些学习计划?不妨分享出来,让我们一起开启这趟软件架构的学习之旅。

View File

@@ -0,0 +1,71 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 _ 如何构建一个可靠的分布式系统?
你好,我是周志明,一名纯粹的软件开发者。之所以说“纯粹”,是因为我想强调,研究技术、编写程序不仅是我养家糊口的技能,也是我最大的兴趣爱好。
在十几年的职业生涯里,我有过很多种不同的标签。比如说,企业管理者、实验室研究员、技术布道师、计算机作家,等等。它们都有一个共同点,就是强调要把自己的想法、理念分享出来,影响他人。
所以你很可能是从《深入理解Java虚拟机》这一类技术书籍知道我的名字的。确实我之前出版过7本计算机技术类的书籍还写过两部开源文档。特别开心的是这些原创技术书的口碑和销量都得到了大家的认可其中有四本书在豆瓣上的评分还超过了9分。现在分享与布道这件事儿也成了我在技术与代码之外的另一个兴趣当然它也给我带来了很多机会和荣誉。
不过除了分享和布道,其实最贴合我的标签,还是程序员。对我来说,这也是我最看重的身份。十几年间,我从一个面向业务逻辑与局部功能编码的程序员,逐步成长为了一名对系统全局负责的技术架构师。现在,我主要是在做一些大型企业级软件的研发工作,也参与和主导过多个国家级的软件项目。
那么,作为一名架构师,在软件研发的过程中,最难的事儿,其实并不是如何解决具体某个缺陷、如何提升某段代码的性能,而是如何才能让一系列来自不同开发者、不同厂商、不同版本、不同语言、质量也良莠不齐的软件模块,在不同的物理硬件和拓扑结构随时变动的网络环境中,依然能保证可靠的运行质量。
显然,这并不是一个研发过程的管理问题。一套“靠谱”的软件系统,尤其是大型的、分布式的软件系统,很难指望只依靠团队成员的个人能力水平,或者依靠质量管理流程来达成。
在我看来,这是一个系统性的、架构层面的问题,最终还是要在技术和架构中去解决。而这也正是我要在这门课中跟你一起探讨的主题:如何构建一个可靠的分布式系统。
我是怎么规划课程的?
那么为了能够讨论清楚这个话题我把课程划分成了以下5个模块。
演进中的架构:我会借着讨论历史之名,从全局性的视角,帮你梳理微服务发展历程中出现的大量技术名词、概念,让你了解这些技术的时代背景和探索过程,帮你在后续的课程讲解中,更容易去深入理解软件架构设计的本质。
架构师的视角:我不会局限在某种架构的通用技巧,而是会带你系统性地了解在做架构设计的时候,架构师都应该思考哪些问题、可以选择哪些主流的解决方案和行业标准做法,以及这些主流方案都有什么优缺点、会给架构设计带来什么影响,等等。这样一来,我们才可以把“架构设计”这样比较抽象的工作具体化、具象化。
分布式的基石:我会聚焦在分布式架构,和你探讨分布式带来的问题与应对策略。我会带你剖析分布式架构中出现的一系列问题,比如服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通讯、事务处理等,有哪些解决思路、方法和常见工具。
不可变基础设施:我会按照云原生时代“基础设施即代码”的新思路,带你深入理解基础设施不变性的目的、原理与实现途径,和你一起去体会用代码和用基础设施,来解决分布式问题的差异,让你能够理解不可变基础设施的内涵,便于在实际工作中做运维、程序升级和部署等工作。
探索与实践我会带你一起开发不同架构的Fenixs Bookstore“导读”这一讲会具体介绍这个项目并看看在不同环境下都应该怎么部署。这个模块的定位是“实战”为了保证学习效果我特意没有安排音频所以建议你一定要自己动手去实操。
因为我相信,如果你是一名驾驶初学者,最合理的学习路径应该是先把汽车发动,然后慢慢行驶起来,而不是马上从“引擎动力原理”“变速箱构造”入手,去设法深刻地了解一台汽车。学习计算机技术也是同样的道理。所以在“探索与实践”模块,我会先带你从运行程序开始,看看效果,然后再搭建好开发、调试环境。
说到这里,我一定要和你说说怎么学习这门课,才能保证最好的效果。
你要怎么学习这门课?
如果你已经是一名系统架构师或者高级开发工程师了,那这门课程就非常适合你。通过跟随学习,你会知道,在软件设计、架构工作中,都需要考虑哪些因素、需要解决哪些问题、有哪些行业标准的解决方案。而如果你是个刚入行不久的程序员,那你可以把这门课程作为一个概念名词的速查手册。
很多内容对你来说可能是全新的,甚至会颠覆你过去的一些认知。而这门课程的好处就是,在不同的技术水平阶段,你都会找到不同的使用方法。具体怎么做呢?
第一步,先完整地跟着课程的节奏学习一遍。你可以先去串一下各种技术名词和架构理论概念,拓展一下视野,去看看大型的架构项目是怎么搭建的,涨涨见识,不一定要求自己深入地理解和记住每一讲的内容。
第二步,根据自己当前的情况,按图索骥寻找对应的章节深入学习并实践。
第三步,当你有了一定的实践经验之后,再来重新学习对应的章节,看看自己曾经的理解是否有遗漏或者有偏差,或者看看我的内容是否还有不完善的地方,真正将知识变成自己的认知。
写在最后
最后,我想说的是,我在极客时间上开设这门课程,既是为了分享与技术布道,也是为了借这个机会,系统性地整理自己的知识,查缺补漏,将它们都融入既有的知识框架之中。
我一直认为,技术人员的成长是有“捷径”的,做技术不仅要去看、去读、去想、去用,更要去写、去说。
把自己“认为掌握了的”知识给叙述出来,能够写得条理清晰,讲得理直气壮;能够让别人听得明白,释去心中疑惑;能够把自己的观点交给别人审视,乃至质疑。在这个过程之中,就会挖掘出很多潜藏在“已知”背后的“未知”。
那么既然如此,我也非常希望,你能够在学习课程的过程当中,也记录下自己的所学所得、所思所想,然后分享给我和其他同学。在分享的过程中,相信我们一定都会有所收获。
所以在这里,我也想发起一个活动:在课程更新的过程中,分享出你的学习心得、实践感悟等等,或者也可以分享出你自己在架构设计中的实践经历、遇到的坑以及避坑的经验。在期中和期末时,课程编辑会甄选出优秀的留言分享内容,专门做一个展示模块,最后还会送出这门课程的纸质版图书。
另外,我还会从中挑选一些比较有代表性的留言做针对性点评,期待在留言区看到你的身影呀!欢迎你的踊跃参与,这也是给自己增添一份学习动力~
OK最后的最后我还想正式认识一下你。你可以在留言区里做个自我介绍和我聊聊你目前对于软件架构设计的最大难点在哪或者你也可以聊聊你对软件架构都有哪些独特的思考和体验欢迎在留言区和我交流讨论。
好了,让我们正式开始学习之旅吧!

View File

@@ -0,0 +1,109 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 _ 原始分布式时代Unix设计哲学下的服务探索
你好,我是周志明。欢迎你来到“软件架构课”,从今天开始,我们就进入课程的第一个模块“演进中的架构”。
架构并不是被“发明”出来的,而是持续进化的结果。所以在这一模块中,我会借着讨论历史之名,从全局性的视角,来带你一起梳理下微服务的发展历程中,出现的大量技术名词、概念。
我会和你一起去分析,它们都是什么、取代了什么,以及为什么能够在技术发展的斗争中取得成功,为什么会成为软件架构不可或缺的支撑;又或者它们为什么会失败,为什么会逐渐被我们遗忘。
了解了这些技术的时代背景和探索过程,在后续的课程中,我再去讲解它们的原理、它们是如何解决问题的时候,你就能与它们当初的设计思想产生共鸣,能更容易深入理解其本质了。
今天这一讲让我们先把时间拨回到半个世纪之前一起来探讨下计算机最开始进入公众视野的时候在Unix设计哲学的指导下分布式架构的第一次服务化探索的得与失。
Unix的分布式设计哲学-
Simplicity of both the interface and the implementation are more important than any other attributes of the system — including correctness, consistency, and completeness.-
保持接口与实现的简单性,比系统的任何其他属性,包括准确性、一致性和完整性,都来得更加重要。-
—— Richard P. GabrielThe Rise of Worse is Better1991
分布式架构的目标是使用多个独立的分布式服务,来共同构建一个更大型的系统。不过,可能跟绝大多数人心中的认知有点儿差异,分布式系统的设想和它实际的尝试,反而要比你今天所了解的大型单体系统出现的时间更早。
在20世纪70年代末到80年代初计算机科学刚经历了从以大型机为主到向以微型机为主的蜕变计算机也逐渐从一种存在于研究机构、实验室当中的科研设备转变为了存在于商业企业中的生产设备甚至是面向家庭、个人用户的娱乐设备。
这个时候的微型计算机系统通常具有16位寻址能力、不足5MHz兆赫时钟频率的处理器和128KB左右的内存地址空间。比如说著名的英特尔处理器的鼻祖Intel 8086处理器就是在1978年研制成功流行于80年代中期的甚至一直到90年代初期还在生产销售。
不过,因为当时的计算机硬件的运算处理能力还相当薄弱,已经直接妨碍了单台计算机上信息系统软件能够达到的最大规模。所以,为了突破硬件算力的限制,各个高校、研究机构、软硬件厂商,都开始分头探索,想看看到底能不能使用多台计算机共同协作,来支撑同一套软件系统的运行。
这个阶段其实是对分布式架构最原始的探索与研究。你可能会觉得奇怪,计算机科学这个技术发展一日千里的领域,半个世纪之前的研究对今天还能有什么指导意义?那个时候探索的分布式如果是可行的,又怎么会拖到今时今日,软件系统才逐步进入微服务时代?
然而并非如此从结果来看历史局限决定了它不可能一蹴而就地解决分布式的难题但仅从过程来看这个阶段的探索可以称得上是硕果累累、成绩斐然。因为在这个时期提出的很多技术、概念对Unix系统后续的发展甚至是对今天计算机科学的很多领域都产生了巨大而深远的影响直接带动了后续的软件架构演化进程。
我们看一些比较熟悉的例子吧。
比如惠普公司及后来被惠普收购的Apollo在80年代初期提出的网络运算架构Network Computing ArchitectureNCA就可以说是未来远程服务调用的雏形。
再比如,卡内基 · 梅隆大学提出的AFS文件系统Andrew File System可以看作是分布式文件系统的最早实现顺便一提Andrew的意思是纪念Andrew Carnegie和Andrew Mellon
再比如麻省理工学院提出的Kerberos协议是服务认证和访问控制ACL的基础性协议是分布式服务安全性的重要支撑目前包括Windows和macOS在内的众多操作系统的登录、认证功能等等都会利用到这个协议。
而为了避免Unix系统的版本战争在分布式领域中重演负责制定Unix系统技术标准的开放软件基金会Open Software FoundationOSF也就是后来的“国际开放标准组织”就邀请了各个主要的研究厂商一起参与共同制订了“分布式运算环境”Distributed Computing EnvironmentDCE的分布式技术体系。
DCE包括了一整套完整的分布式服务组件的规范与实现。
比如源自NCA的远程服务调用规范Remote Procedure CallRPC在当时被称为是DCE/RPC跟后来不局限于Unix系统的、基于通用TCP/IP协议的远程服务标准ONC RPC一起被认为是现代RPC的共同鼻祖这是Sun公司向互联网工程任务组提交的源自AFS的分布式文件系统Distributed File SystemDFS规范在当时被称为DCE/DFS源自Kerberos的服务认证规范还有时间服务、命名与目录服务就连现在程序中很常用的通用唯一识别符UUID也是在DCE中发明出来的。
因为OSF本身的背景它是一个由Unix开发者组成的Unix标准化组织所以在当时研究这些分布式技术通常都会有一个预设的重要原则也就是在实现分布式环境中的服务调用、资源访问、数据存储等操作的时候要尽可能地透明化、简单化让开发人员不用去过于关注他们访问的方法或者是要知道其他资源是位于本地还是远程。
这样的主旨呢确实非常符合Unix设计哲学有过几个版本的不同说法这里我指的是Common Lisp作者Richard P. Gabriel提出的简单优先“Worse is Better”原则但这个目标其实是过于理想化了它存在一些在当时根本不可能完美解决的技术困难。
“调用远程方法”与“调用本地方法”尽管只是两字之差但要是想能同时兼顾到简单、透明、性能、正确、鲁棒Robust、一致的目标的话两者的复杂度就完全不能相提并论了。
我们先不说,远程方法是不可能做到像本地方法一样,能用内联等传统编译原理中的优化算法,来提升程序运行速度的,光是“远程”二字带来的网络环境下的新问题。
比如说,远程的服务在哪里(服务发现)、有多少个(负载均衡)、网络出现分区、超时或者服务出错了怎么办(熔断、隔离、降级)、方法的参数与返回结果如何表示(序列化协议)、如何传输(传输协议)、服务权限如何管理(认证、授权)、如何保证通信安全(网络安全层)、如何令调用不同机器的服务能返回相同的结果(分布式数据一致性)等一系列问题,就需要设计者耗费大量的心思。
那么面对重重的困难与压力DCE不仅从零开始、从无到有地回答了其中大部分问题构建出了大量的分布式基础组件与协议而且它还真的尽力去做到了相对意义的“透明”。
比如说你在DFS上访问文件如果不考虑性能上的差异的话就很难感受到它与本地磁盘文件系统有什么不同。可是一旦考虑性能上的差异分布式和本地的鸿沟是无比深刻的这是数量级上的差距是不可调和的。
尤其是在那个年代,在机器硬件的限制下,开发者为了让程序在运行效率上可以接受,就只有在方法本身的运行时间很长,可以相对忽略远程调用成本时的情况下,才去考虑使用分布式。如果方法本身的运行时长不够,就要人为地用各种奇技淫巧来刻意构造出这样的场景,比如可能会将几个原本毫无关系的方法打包到一个方法内,一块进行远程调用。
一方面刻意构造长时间运行的方法这本身就与使用分布式来突破硬件算力、提升性能的初衷相互矛盾需要我们小心平衡另一方面此时的开发人员实际上仍然必须无时无刻地都要意识到自己是在编写分布式的程序不能随随便便地踏过本地与远程的界限让软件系统的设计向性能做出妥协让DCE“尽量简单透明”的努力几乎全部付诸东流。
因为本地与远程,无论是从编码、部署,还是从运行效率的角度上看,都有着天壤之别,所以在设计一个能运作良好的分布式应用的时候,就变得需要极高的编程技巧和各方面的知识来作为支撑,这个时候,反而是人员本身对软件规模的约束,超过机器算力上的约束了。
对DCE的研究呢算得上是计算机科学中第一次有组织领导、有标准可循、有巨大投入的分布式计算的尝试。但无论是DCE还是稍后出现的CORBACommon ObjectRequest Broker Architecture公共对象请求代理体系结构我们从结果来看都不能说它们取得了成功。
因为把一个系统直接拆分到不同的机器之中,这样做带来的服务的发现、跟踪、通讯、容错、隔离、配置、传输、数据一致性和编码复杂度等方面的问题,所付出的代价远远超过了分布式所取得的收益。
而亲身经历过那个年代的计算机科学家、IBM院士凯尔 · 布朗Kyle Brown在事后曾经评价道“这次尝试最大的收获就是对RPC、DFS等概念的开创以及得到了一个价值千金的教训某个功能能够进行分布式并不意味着它就应该进行分布式强行追求透明的分布式操作只会自寻苦果”。
原始分布式时代的教训-
Just because something can be distributed doesnt mean it should be distributed. Trying to make a distributed call act like a local call always ends in tears.-
某个功能能够进行分布式,并不意味着它就应该进行分布式,强行追求透明的分布式操作,只会自寻苦果。-
—— Kyle BrownIBM FellowBeyond buzzwords: A brief history of microservices patterns2016
其实从设计角度来看以上的结论是有违Unix哲学的但这也是在当时的现实情况下不得不做出的让步。在当时计算机科学面前有两条通往更大规模软件系统的道路一条路是尽快提升单机的处理能力以避免分布式的种种问题另一条路是找到更完美的解决方案来应对如何构筑分布式系统的问题。
在20世纪80年代正是摩尔定律开始稳定发挥作用的黄金时期微型计算机的性能以每两年就增长一倍的惊人速度在提升硬件算力束缚软件规模的链条很快就松动了我们用单台或者几台计算机就可以作为服务器来支撑大型信息系统的运作了信息系统进入了单体时代而且在未来很长的一段时间内单体系统都将是软件架构的主流。
不过尽管如此对于另外一条路径也就是对分布式计算、远程服务调用的探索开发者们也从没有中断过。关于远程服务调用这个关键问题的历史、发展与现状我还会在服务设计风格的“远程服务调用”部分第7~10讲以现代RPC和RESTful为主角来进行更详细的讲述。而对于在原始分布式时代中遭遇到的其他问题我也还会在软件架构演进的后面几个时代里反复提起它们。
小结
今天我给你介绍了计算机科学对分布式和服务化的第一次探索着重分析了这次探索的主旨思想也就是追求简单、符合Unix哲学的分布式系统以及它当时所面临的困难比如在捉襟见肘的运算能力、网络带宽下设计不得不做出的妥协。
在这个过程中我们接触到了DCE、CORBA等早期的分布式基础架构。其中许多的技术比如远程服务调用、分布式文件系统、Kerberos认证协议等。如果你对这些技术觉得还有点陌生、或者还有很多疑惑没有关系我还会在后面的课程中为你着重介绍。
原始分布式时代提出的构建“符合Unix的设计哲学的”以及“如同本地调用一般简单透明的”分布式系统的这个目标是软件开发者对分布式系统最初的美好愿景。不过迫于现实它会在一定时期内被妥协、被舍弃分布式将会经过一段越来越复杂的发展进程。
但是,到了三十多年以后的今天,随着微服务的逐渐成熟完善,成为大型软件的主流架构风格以后,这个美好的愿景终将还是会重新被开发者拾起。
一课一思
Richard P. Gabriel提出的Unix设计哲学中写到“保持接口与实现的简单性比系统的任何其他属性包括准确性、一致性和完整性都来得更加重要。”
现在你来思考一下今天以微服务为代表的分布式系统是如何看待“简单”的欢迎在留言区分享你的见解我也将会在第5讲“后微服务时代”中带你一起重新审视这个问题。
好,这节课就到这里。如果你身边也有想要或者必须要了解架构的演进的朋友,欢迎你把这一讲的内容分享给她/他。

View File

@@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 _ 单体系统时代:应用最广泛的架构风格
你好,我是周志明。今天,我们一起来探索单体系统时代。
这一讲,我会带你去了解以单体架构构建的软件系统,都有哪些优势和缺点,还有哪些容易让人产生错误理解的误区。在探索的过程中,你可以同时思考一下,为什么单体架构能够在相当长的时间里成为软件架构的主流风格,然后再对比下我在这一讲最后给出答案。
大型单体系统
单体架构是绝大部分软件开发者都学习和实践过的一种软件架构很多介绍微服务的图书和技术资料中也常常会把这种架构形式的应用称作“巨石系统”Monolithic Application
在整个软件架构演进的历史进程里,单体架构是出现时间最早、应用范围最广、使用人数最多、统治历史最长的一种架构风格。但“单体”这个名称,却是从微服务开始流行之后,才“事后追认”所形成的概念。在这之前,并没有多少人会把“单体”看成一种架构。
如果你去查找软件架构的开发资料,可以轻轻松松找到很多以微服务为主题的图书和文章,但却很难能找到专门教我们怎么开发单体系统的任何形式的材料。
这一方面体现了单体架构本身的简单性;另一方面也体现出,在相当长的时间里,我们都已经习惯了,软件架构就应该是单体这种样子的。
那在剖析单体架构之前呢,我们有必要先搞清楚一个思维误区,那就是单体架构是落后的系统架构风格,最终会被微服务所取代。
因为在许多微服务的研究资料里,单体系统往往是以“反派角色”的身份登场的,比如著名的微服务入门书《微服务架构设计模式》,第一章的名字就是“逃离单体的地狱”。而这些材料所讲的单体系统,其实都有一个没有明说的隐含定语:“大型的单体系统”。
对于小型系统,也就是用单台机器就足以支撑其良好运行的系统来说,这样的单体不仅易于开发、易于测试、易于部署,而且因为各个功能、模块、方法的调用过程,都是在进程内调用的,不会发生进程间通讯,所以程序的运行效率也要比分布式系统更高,完全不应该被贴上“反派角色”的标签。要我说的话,反倒是那些爱赶技术潮流,却不顾需求现状的微服务吹捧者更像是个反派。
进程间通讯Inter-Process CommunicationIPC。RPC属于IPC的一种特例但请注意这里两个“PC”不是同个单词的缩写关于IPC与RPC的知识在“远程服务调用”这个小章节中我会详细讲解。
所以当我们在讨论单体系统的缺陷的时候必须基于软件的性能需求超过了单机软件的开发人员规模明显超过了“2 Pizza Teams”范畴的前提下这样才有讨论的价值。那么在咱们课程后续讨论中我所说的单体都应该是特指的“大型的单体系统”。
也正因如此,在这一讲的开篇中“单体是出现最早的架构风格”,跟我在上一讲介绍“原始分布式时代”时,在开篇中提到的“使用多个独立的分布式服务共同构建一个更大型系统的设想与实际尝试,反而要比今天你所了解的大型单体系统出现的时间更早”,这两句话实际上并没有矛盾的地方。
可拆分的单体系统
好了,回到主题,接下来我就带你来详细、深入地了解一下单体系统,看看“巨石系统”为何仍然是可以拆分的。
尽管“Monolithic”这个词语本身的意思“巨石”确实是带有一些“不可拆分”的隐含意味但我们也不能简单粗暴地把单体系统在维基百科上的定义“All in One Piece”翻译成“铁板一块”它其实更接近于自给自足Self-Contained的含义。
单体系统-
Monolith means composed all in one piece. The Monolithic application describes a single-tiered software application in which different components combined into a single program from a single platform.-
—— Monolithic ApplicationWikipedia
当然了,这种“铁板一块”的译法也不全是段子。我相信肯定有一部分人说起单体架构、巨石系统的缺点,脑海中闪过的第一印象就是“不可拆分”,难以扩展,所以它才不能支撑起越来越大的软件规模。这种想法我觉得其实是有失偏颇的,至少不完整。
我为什么会这么判断呢?
因为从纵向角度来看,在现代信息系统中,我从来没有见到过实际的生产环境里,有哪个大型的系统是完全不分层的。
分层架构Layered Architecture已经是现在几乎所有的信息系统建设中都普遍认可、普遍采用的软件设计方法了。无论是单体还是微服务或者是其他架构风格都会对代码进行纵向拆分收到的外部请求会在各层之间以不同形式的数据结构进行流转传递在触及到最末端的数据库后依次返回响应。
那么对于单体架构来说在这个意义上的“可拆分”单体其实完全不会展露出丝毫的弱势反而还可能因为更容易开发、部署、测试而更加便捷。比如说当前市面上所有主流的IDE如Intellij IDEA、Eclipse等都对单体架构最为友好。IDE提供的代码分析、重构能力以及对编译结果的自动化部署和调试能力都是主要面向单体架构而设计的。
来自OReilly的开放文档《Software Architecture Patterns》
而在横向角度的“可拆分”上,单体架构也可以支持按照技术、功能、职责等角度,把软件拆分为各种模块,以便重用和团队管理。
实际上单体系统并不意味着就只能有一个整体的程序封装形式如果有需要它完全可以由多个JAR、WAR、DLL、Assembly或者其他模块格式来构成。
即使是从横向扩展Scale Horizontally的角度来衡量如果我们要在负载均衡器之后同时部署若干个单体系统的副本以达到分摊流量压力的效果那么基于单体架构也是轻而易举就可以实现的。
非独立的单体
不过,在“拆分”这方面,单体系统的真正缺陷实际上并不在于要如何拆分,而在于拆分之后,它会存在隔离与自治能力上的欠缺。
在单体架构中,所有的代码都运行在同一个进程空间之内,所有模块、方法的调用也都不需要考虑网络分区、对象复制这些麻烦事儿,也不担心因为数据交换而造成性能的损失。
可是,在获得了进程内调用的简单、高效这些好处的同时,也就意味着,如果在单体架构中,有任何一部分的代码出现了缺陷,过度消耗进程空间内的公共资源,那所造成的影响就是全局性的、难以隔离的。
我们要怎么理解这个问题呢?
首先,一旦架构中出现了内存泄漏、线程爆炸、阻塞、死循环等问题,就都将会影响到整个程序的运行,而不仅仅是某一个功能、模块本身的正常运作;而如果消耗的是某些更高层次的公共资源,比如端口占用过多或者数据库连接池泄漏,还将会波及到整台机器,甚至是集群中其他单体副本的正常工作。
此外同样是因为所有代码都共享着同一个进程空间如果代码无法隔离那也就意味着我们无法做到单独停止、更新、升级某一部分代码因为不可能有“停掉半个进程重启1/4个进程”这样不合逻辑的操作。所以从动态可维护性的角度来说单体系统也是有所不足的对于程序升级、修改缺陷这样的工作我们往往需要制定专门的停机更新计划而且做灰度发布也相对会更加复杂。
补充这里我说的“代码无法隔离无法做到单独停止、更新……”其实严谨来说还是有办法的比如可以使用OSGi这种运行时模块化框架只是会很别扭、很复杂。
这里就涉及到一个需要权衡的问题:如果说共享同一进程获得简单、高效这些优势的代价,是损失了各个功能模块的自治、隔离能力,那这两者孰轻孰重呢?这个问题很有代表性,我们还可以换个角度思考一下,它的潜台词其实是在比较微服务、单体架构哪种更好用、优秀?
在我看来,“好用和优秀”不一定是绝对的。我们看一个例子吧。
比如说,沃尔玛将超市分为仓储部、采购部、安保部、库存管理部、巡检部、质量管理部、市场营销部,等等,来划清职责,明确边界,让管理能力可以支持企业的成长规模;但如果你家楼下开的小卖部,爸、妈加儿子,再算上看家的中华田园犬小黄,一共也就只有四名员工,也去追求“先进管理”,来划分仓储部、采购部、库存管理部……的话,那纯粹是给自己找麻烦。
在单体架构下,哪怕是信息系统中两个毫无关联的子系统,我们也都必须部署到一起。当系统规模小的时候,这是个优势;但当系统规模扩大、程序需要修改的时候,相应的部署成本、技术升级时的迁移成本,都会变得非常高。
就拿沃尔玛例子来说,也就是当公司规模比较小的时候,让安保部和质检部两个不相干的部门在同一栋大楼中办公,算是节约资源。但当公司的人数增加了,办公室已经变得拥挤不堪的时候,我们也最多只能在楼顶加盖新楼层(相当于增强硬件性能),而不能让安保、质检分开地方办公,这才是缺陷所在。
另外,由于隔离能力的缺失,除了会带来难以阻断错误传播、不便于动态更新程序的问题,还会给带来难以技术异构等困难。
技术异构:后面在介绍微服务时,我会提到马丁 · 福勒Martin Fowler提出的9个特征技术异构就是其中之一。它的意思是说允许系统的每个模块自由选择不一样的程序语言、不一样的编程框架等技术栈去实现。单体系统的技术栈异构不是一定做不到比如JNI就可以让Java混用C/C++,但是这也是很麻烦的事,是迫不得已下的选择。
不过在我看来我们提到的这些问题还不是我们今天以微服务去代替单体系统的根本原因。我认为最根本的原因是单体系统并不兼容“Phoenix”的特性。
单体这种架构风格潜在的观念是希望系统的每一个部件甚至每一处代码都尽量可靠不出、少出错误致力于构筑一个7×24小时不间断的可靠系统。
这种观念在小规模软件上能运作良好但当系统越来越大的时候交付一个可靠的单体系统就会变得越来越有挑战性。就像我在导读《什么是“The Fenix Project”》中所说的正是随着软件架构的不断演进我们构建可靠系统的观念开始从“追求尽量不出错”转变为了正视“出错是必然”。实际上这才是微服务架构能够挑战并且能逐步开始代替运作了几十年的单体架构的根本驱动力。
不过即使是为了允许程序出错为了获得隔离、自治的能力为了可以技术异构等目标也并不意味着一定要依靠微服务架构。在新旧世纪之交人们曾经探索过几种服务的拆分方法把一个大的单体系统拆分为若干个更小的、不运行在同一个进程的独立服务这些服务拆分的方法后来导致了面向服务架构Service-Oriented Architecture的一段兴盛期我们把它称作是“SOA时代”。
下一讲呢,我就带你来一起探索这个架构时代的特点与得失,你可以期待一下。
小结
这节课,我们一起学习了单体架构。单体作为迄今为止使用人数最多的一种软件架构风格,自身必定是有可取之处的,比如说,易于分层、易于开发、易于部署测试、进程内的高效交互,等等,这些都是单体架构的优点。
可是今天以微服务为代表的分布式架构的呼声如此之高也同样说明单体至少在当今的某些领域中存在一些关键性的问题而我在前面提到的自治、隔离谈到的单体不兼容“Phoenix”的特性这些也正是单体系统的缺陷所在也是我在下一讲中要讲的SOA、微服务等分布式架构的核心目标所在。
一课一思
上一讲,我在给你介绍原始分布式时代的时候,提到过软件系统发展的道路有两条,一条是“分布式”路径,而单体架构是“不分布式”这条路径所演化的结果。经过这节课的讨论,我们已经知道了单体最终并不会被微服务所取代,未来它仍然会长期存在。
那么,在了解了它的演化历史的基础上,请你思考一下:未来的单体系统将会朝着怎样的方向发展呢?欢迎在留言区分享你的见解。
在下一讲中我会给你介绍另一条“分布式”路径下的架构风格SOA架构。不过到了这一模块的最后一讲时我还将重新回到原点讨论这两条架构演化路径的未来。到时候你可以看看你思考的答案是否跟我观察到的一致。
好,这节课就到这里,如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,140 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 _ SOA时代成功理论与失败实践
你好,我是周志明。
SOA架构是第一次被广泛使用过的、通过分布式服务来构建信息系统的工程实践。它有完善的理论和工具可以说它解决了分布式系统中几乎所有主要的技术问题。
但遗憾的是虽然SOA架构曾经被视为更大规模的软件发展的方向但它最终还是没能成为一种普适的软件架构。
所以今天我们就来探索一下SOA架构一起来找找它没能成为普适的软件架构的原因。通过这一讲你能从中体会到SOA的设计思想与原则理解它为什么不能成功。
三种代表性的服务拆分架构模式
在上一讲,我曾经提到过,为了对大型的单体系统进行拆分,让每一个子系统都能独立地部署、运行、更新,开发者们尝试了很多种方案。
所以在介绍SOA架构模式之前我还要先带你学习三种比较有代表性的服务拆分的架构模式。这些架构是SOA演化过程的中间产物你也可以理解为它们是SOA架构出现的必要前提。
烟囱式架构Information Silo Architecture
第一种架构模式是烟囱式架构。
信息烟囱也被叫做信息孤岛Information Island使用这种架构的系统呢也被称为孤岛式信息系统或者烟囱式信息系统。这种信息系统完全不会跟其他相关的信息系统之间进行互操作或者是进行协调工作。
那你就会发现,这样的系统其实并没有什么“架构设计”可言。你还记不记得,我在上一讲中举的那个“企业与部门”的例子?如果两个部门真的完全不会发生任何交互,那我们就并没有什么理由,一定要强迫他们必须在一栋楼里办公。
所以,两个不发生交互的信息系统,让它们使用独立的数据库、服务器,就可以完成拆分了。
而唯一的问题,也是这个架构模式的致命问题,那就是:企业中真的存在完全不发生交互的部门吗?
对于两个信息系统来说,哪怕真的毫无业务往来关系,但系统的人员、组织、权限等主数据,会是完全独立、没有任何重叠的吗?这样“独立拆分”“老死不相往来”的系统,显然不可能是企业所希望见到的。
微内核架构Microkernel Architecture
第二种是微内核架构它也被称为插件式架构Plug-in Architecture
既然在烟囱式架构中我们说两个没有业务往来关系的系统也可能需要共享人员、组织、权限等一些公共的主数据那就不妨把这些主数据连同其他可能被各个子系统使用到的公共服务、数据、资源都集中到一块成为一个被所有业务系统共同依赖的核心系统Kernel也称为Core System
这样的话具体的业务系统就能以插件模块Plug-in Modules的形式存在了就可以为整个系统提供可扩展的、灵活的、天然隔离的功能特性。
来自OReilly的开放文档《Software Architecture Patterns》
以更高层次的抽象程度来看,任何计算机系统都是由各种架构的软件互相配合来实现各种功能的,这一讲我介绍的各种架构模式,一般都可以看作是整个系统的一种插件。对于产品型应用程序来说,如果我们想将新特性或者功能及时加入系统,微内核架构会是一个不错的选择。
微内核架构也可以嵌入到其它架构模式之中,通过插件的方式,来提供逐步演化的功能和增量开发。所以,如果你准备实现一个能够支持二次开发的软件系统,微内核就是一种良好的架构模式。
不过,微内核架构也有它的局限和使用前提,它会假设系统中各个插件模块之间是互不认识的(不可预知系统会安装哪些模块),这些插件会访问内核中一些公共的资源,但不会发生直接交互。
可是无论是在企业信息系统还是在互联网在许多场景中这一假设都不成立。比如说你要建设一个购物网站支付子系统和用户子系统是独立的但当交易发生时支付子系统可能需要从用户子系统中得到是否是VIP、银行账号等信息而用户子系统也可能要从支付子系统中获取交易金额等数据来维护用户积分。
所以,我们必须找到一个办法,它既能拆分出独立的系统,也能让拆分后的子系统之间可以顺畅地互相调用通讯。
事件驱动架构Event-Driven Architecture
那么,为了能让子系统之间互相通讯,事件驱动架构就应运而生了。
这种架构模式的运作方案是在子系统之间建立一套事件队列管道Event Queues来自系统外部的消息将以事件的形式发送到管道中各个子系统可以从管道里获取自己感兴趣、可以处理的事件消息也可以为事件新增或者是修改其中的附加信息甚至还可以自己发布一些新的事件到管道队列中去。
这样一来,每一个消息的处理者都是独立的、高度解耦的,但它又能与其他处理者(如果存在该消息处理者的话)通过事件管道来进行互动。
来自OReilly的开放文档《Software Architecture Patterns》
那么当系统演化至事件驱动架构的时候我在原始分布式时代这一讲的结尾中提到的第二条通往大规模软件的路径也就是仍然在并行发展的远程服务调用就迎来了SOAP协议的诞生我在后面第7~10讲分享远程服务调用的时候还会给你详细介绍它你到时可以再次印证一下这一讲的内容
此时“面向服务的架构”Service Oriented ArchitectureSOA就已经有了登上软件架构舞台所需要的全部前置条件了。
SOA架构时代的探索
SOA的概念最早是由Gartner公司在1994年提出的。当时的SOA还不具备发展的条件直到2006年情况才有所变化IBM、Oracle、SAP等公司共同成立了OSOA联盟Open Service Oriented Architecture来联合制定和推进SOA相关行业标准。
到2007年在结构化资讯标准促进组织Organization for the Advancement of Structured Information StandardsOASIS的倡议与支持下OSOA就由一个软件厂商组成的松散联盟转变为了一个制定行业标准的国际组织。它联合OASIS共同新成立了Open CSA组织Open Composite Services Architecture也就是SOA的“官方管理机构”。
当软件架构发展至SOA时代的时候其中的许多概念、思想都已经能在今天的微服务中找到对应的身影了。比如说服务之间的松散耦合、注册、发现、治理、隔离、编排等等都是微服务架构中耳熟能详的概念了也大多是在分布式服务刚被提出的时候就已经可以预见到的困难。
所以SOA就针对这些问题乃至于针对“软件开发”这件事儿本身进行了更具体、更系统的探索。
更具体
“更具体”体现在尽管SOA本身还是属于一种抽象概念而不是特指某一种具体的技术但它比单体架构和烟囱式架构、微内核架构、事件驱动架构都要更具可操作性细节也充实了很多。所以我们已经不能简单地把SOA看作是一种架构风格了而是可以称之为一套软件架构的基础平台了。
那,我们怎么理解“基础平台”这个概念呢?在我看来,主要是下面几个方面:
SOA拥有领导制定技术标准的组织Open CSA
SOA具有清晰的软件设计的指导原则比如服务的封装性、自治、松耦合、可重用、可组合、无状态等等
SOA架构明确了采用SOAP作为远程调用的协议依靠SOAP协议族WSDL、UDDI和一大票WS-*协议)来完成服务的发布、发现和治理;
SOA架构会利用一个被称为是企业服务总线Enterprise Service BusESB的消息管道来实现各个子系统之间的通讯交互这就让各个服务间在ESB的调度下不需要相互依赖就可以实现相互通讯既带来了服务松耦合的好处也为以后可以进一步实现业务流程编排Business Process ManagementBPM提供了基础
SOA架构使用了服务数据对象Service Data ObjectSDO来访问和表示数据使用服务组件架构Service Component ArchitectureSCA来定义服务封装的形式和服务运行的容器
……
在这一整套成体系、可以互相精密协作的技术组件的支持下我们从技术可行性的角度来评判的话SOA实际上就可以算是成功地解决了分布式环境下出现的诸如服务注册、发现、隔离、治理等主要技术问题了。
更系统
这里我说的“更系统”指的是SOA的宏大理想。因为SOA最根本的目标就是希望能够总结出一套自上而下的软件研发方法论让企业只需要跟着它的思路就能够一揽子解决掉软件开发过程中的全套问题。比如如何挖掘需求、如何将需求分解为业务能力、如何编排已有服务、如何开发测试部署新的功能等等。
如果这个目标真的能够达成,那么软件开发就有可能从此迈进工业化大生产的阶段。你可以试想一下,如果有一天,你在写符合客户需求的软件时,就像写八股文一样有迹可循、有法可依,那对你来说或许很无趣,但这肯定可以大幅提升整个社会实施信息化的效率。
SOA在21世纪最初的十年里曾经盛行一时有IBM等一众巨头为其摇旗呐喊吸引了不少软件开发商尤其是企业级软件开发商的跟随但最终却还是偃旗息鼓沉寂了下去。
原因也很简单开发信息系统毕竟不是写八股文SOA架构过于严谨精密的流程与理论导致了软件开发的全过程都需要有懂得复杂概念的专业人员才能够驾驭。从SOA诞生的那一天起就已经注定了它只能是少数系统的阳春白雪式的精致奢侈品它可以实现多个异构大型系统之间的复杂集成交互却很难作为一种具有广泛普适性的软件架构风格来推广。
我在后面第7~10讲介绍远程服务调用时我还会为你介绍Web Service的兴起与衰落。Web Service之所以被逐渐边缘化最本质的原因就是过于严格的规范定义给架构带来了过度的复杂性。
而构建在Web Service基础之上的ESB、BPM、SCA、SDO等诸多的上层建筑就进一步加剧了这种复杂性。
SOA最终没有获得成功的致命伤其实跟当年的EJBEnterprise JavaBean企业级JavaBean的失败如出一辙。
尽管在当时EJB有Sun Microsystems被甲骨文收购和IBM等一众巨头在背后力挺希望能把它发展成一套面向信息系统的编程范式但它仍然被以Spring、Hibernate为代表的“草根框架”给打败了。可见任何事物一旦脱离了人民群众最终都会淹没在群众的海洋之中就连信息技术也不曾例外过。
最后当你读到这一段的时候你不妨再重新思考下我们这一讲的开头提到的“如何使用多个独立的分布式服务共同构建一个更大型系统”这个问题再回顾下“原始分布式时代”这一讲中Unix DCE提出的分布式服务的主旨“让开发人员不必关心服务是远程还是本地都能够透明地调用服务或者访问资源”。
经过了三十年的技术发展信息系统经历了巨石、烟囱、微内核、事件驱动、SOA等架构模式应用受架构复杂度的牵绊却是越来越大距离“透明”二字已经越来越远了。这是否算不自觉间忘记了当年的初心呢
接下来我们要探索的微服务时代,似乎正是带着这样自省式的问句而开启的。
小结
这一讲我带你学习了解了SOA架构重点了解了从原始分布式架构、单体架构演进到SOA架构这段过程中的一些中间产物如烟囱式架构、微内核架构、事件驱动架构等。
另外我之所以带你解构SOA架构就是要帮助你弄清楚它成功的部分比如它是如何提出了哪些技术、解决问题的方法论是什么它是如何看待分布式、乃至是如何看待软件开发的你也要弄清楚它失败的部分要清楚为什么SOA在众多软件业巨头的推动下仍然没能成为软件开发者所普遍接受的普适的软件开发方法。这是你了解和掌握推动架构时代演进原因的重要方式。
一课一思
你是否使用过SOA的方法论来开发软件系统呢无论有还是没有作为一个软件开发者你是否愿意软件开发向着工业化方向发展让软件类似工业产品制造那样可以在规范、理论、工具、技术的支持下以流水线的方式生产出来
欢迎在留言区分享你的见解。如果你身边的朋友也对SOA架构的成功与失败感兴趣希望你能把今天的内容分享给TA。

View File

@@ -0,0 +1,150 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 _ 微服务时代SOA的革命者
你好,我是周志明。这一讲,我继续来带你探索软件架构中的微服务时代。
其实“微服务”这个词儿Peter Rodgers博士在2005年的云计算博览会Web Services Edge 2005就已经提出和使用了。当时的说法是“Micro-Web-Service”指的是一种专注于单一职责的、与语言无关的、细粒度的Web服务Granular Web Services
“微服务”这个词并不是Peter Rodgers直接凭空创造出来的概念。最开始的微服务可以说是在SOA发展的同时被催生出来的产物就像是EJB在推广的过程中催生出了Spring和Hibernate框架那样。这一阶段的微服务是作为SOA的一种轻量化的补救方案而被提出来的。
到今天为止在英文版的维基百科上人们仍然是把微服务定义成了SOA的一个变种。所以微服务在诞生和最初的发展阶段跟SOA、Web Service这些概念有所牵扯也是完全可以理解的。
What is microservices-
Microservices is a software development technique — a variant of the service-oriented architecture SOA structural style.-
—— WikipediaMicroservices
但我们现在再来看,维基百科对微服务的定义,其实已经有些过时了。至于为什么这样说,就是我在这一讲中要和你解释的了。
在微服务的概念被提出后将近10年的时间里面它都没有受到太多人的追捧。毕竟如果只是对现有的SOA架构的修修补补确实难以唤起广大技术人员的更多激情。
不过也是在这10年的时间里微服务本身其实一直在思考、蜕变。
2012年在波兰克拉科夫举行的“33rd Degree Conference”大会上Thoughtworks首席咨询师James Lewis做了题为《Microservices - Java, the Unix Way》的主题演讲。其中他提到了单一服务职责、康威定律、自动扩展、领域驱动设计等原则却只字未提SOA反而号召大家应该重拾Unix的设计哲学As Well Behaved Unix Services。这一点跟我在上一讲中所说的“初心与自省”可以说是一个意思。
微服务已经迫不及待地要脱离SOA的附庸想要成为一种独立的架构风格也许它还将会是SOA的革命者找到一条能被广大开发者普遍接受且愿意接受的、实现服务化系统的目标。
微服务真正崛起是在2014年。相信我们大多数程序员也是从Martin Fowler和James Lewis合写的文章“Microservices: a definition of this new architectural term”里面第一次了解到微服务的。这篇文章虽然不是最早提出“微服务”这个概念的但却是真正丰富的、广为人知的和可操作的微服务指南。也就是说这篇文章才是微服务的真正起源。
这篇文章定义了现代微服务的概念:微服务是一种通过多个小型服务的组合,来构建单个应用的架构风格,这些服务会围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言、不同的数据存储技术、运行在不同的进程之中。服务会采取轻量级的通讯机制和自动化的部署机制,来实现通讯与运维。
此外,在这篇论文中,作者还列举出了微服务的九个核心的业务与技术特征。接下来,我就一一解读为你解读下,希望你可以从中领悟到,微服务在团队、开发、运维等一系列研发过程中的核心思想。
第一围绕业务能力构建Organized around Business Capabilities
这个核心技术特征,实际上再次强调了康威定律的重要性。它的意思是,有怎样的结构、规模和能力的团队,就会产生出对应结构、规模、能力的产品。这个结论不是某个团队、某个公司遇到的巧合,而是必然的演化结果。
如果本应该归属同一个产品内的功能,被划分在了不同的团队当中,那就必然会产生大量的跨团队沟通协作,而跨越团队边界,无论是在管理、沟通,还是在工作安排上,都会产生更高的成本。高效的团队,自然会针对这个情况进行改进,而当团队和产品磨合调节稳定了之后,就会拥有一致的结构。
第二分散治理Decentralized Governance
这个技术特征,表达的是“谁家孩子谁来管”。微服务对应的开发团队,有着直接对服务运行质量负责的责任,也应该有着不受外界干预,掌控服务各个方面的权力,可以选择与其他服务异构的技术来实现自己的服务。
这一点在真正实践的时候其实多少都会留点儿宽松的处理余地。因为大多数的公司都不会在某一个服务用Java另一个用Python下一个用Golang而是通常都会统一主流语言甚至会有统一的技术栈或专有的技术平台。
微服务不提倡也并不反对这种“统一”它只负责提供和维护基础技术栈的团队有被各方依赖的觉悟要有“经常被凌晨3点的闹钟吵醒”的心理准备就好。
微服务更加强调的是在确实有必要进行技术异构的时候一个开发团队应该能有选择“不统一”的权利。比如说我们不应该强迫用Node.js去开发报表页面要做人工智能计算的时候也可以选择用Python等等。
第三通过服务来实现独立自治的组件Componentization via Services
这里Martin Fowler与James Lewis之所以强调要通过“服务”Service而不是“类库”Library来构建组件是因为类库是在编译期静态链接到程序中的会通过本地调用来提供功能而服务是进程外组件它是通过远程调用来提供功能的。在第2讲中我们已经分析过尽管远程服务有更高昂的调用成本但这是为组件带来隔离与自治能力的必要代价。
第四产品化思维Products not Projects
产品化思维的意思就是,我们要避免把软件研发看作是要去完成某种功能,而要把它当做是一种持续改进、提升的过程。比如,我们不应该把运维看作就是运维团队的事,把开发看作就是开发团队的事。
开发团队应该为软件产品的整个生命周期负责。开发者不仅应该知道软件是如何开发的,还应该知道它会如何运作、用户如何反馈,乃至售后支持工作是怎样进行的。这里服务的用户,不一定是最终用户,也可能是消费这个服务的另外一个服务。
以前在单体的架构模式下程序的规模决定了我们无法让全部的开发人员都关注到一个完整的产品在组织中会有开发、运维、支持等细致分工的成员他们只关注于自己的一块工作。但在微服务下我们可以让团队中的每一位成员都具有产品化思维。因为在“2 Pizza Teams”的团队规模下每一个人都了解全过程是完全有可能实现的。
第五数据去中心化Decentralized Data Management
微服务这种架构模式也明确地提倡,数据应该按领域来分散管理、更新、维护和存储。
在单体服务中,通常一个系统的各个功能模块会使用同一个数据库,虽然这种中心化的存储确实天生就更容易避免一致性的问题,但是,同一个数据实体在不同服务的视角里,它的抽象形态往往也是不同的。
比如Bookstore应用中的书本在销售领域中关注的是价格在仓储领域中关注的是库存数量在商品展示领域中关注的是书籍的介绍信息。如果是作为中心化的存储那么这里所有的领域都必须修改和映射到同一个实体之中就会导致不同的服务之间可能会互相产生影响从而丧失了各自的独立性。
另外,尽管在分布式中,我们要想处理好一致性的问题也很困难,很多时候都没法使用传统的事务处理来保证不出现一致性问题。但是两害相权取其轻,一致性问题这些必要的代价是值得付出的。
第六轻量级通讯机制Smart Endpoints and Dumb Pipes
这个弱管道Dumb Pipes机制可以说几乎算是在直接指名道姓地反对ESB、BPM和SOAP等复杂的通讯机制。
ESB可以处理消息的编码加工、业务规则转换等BPM可以集中编排企业的业务服务SOAP有几十个WS-*协议族在处理事务、一致性、认证授权等一系列工作。这些构筑在通讯管道上的功能,也许在某个系统中的确有一部分服务是需要的,但对于另外更多的服务来说是强加进来的负担。
如果服务需要上面的某一种功能或能力那就应该在服务自己的Endpoint端点上解决而不是在通讯管道上一揽子处理。
微服务提倡的是类似于经典Unix过滤器那样简单直接的通讯方式。比如说RESTful风格的通讯在微服务中就是比较适合的。
第七容错性设计Design for Failure
容错性设计,是指软件架构不再虚幻地追求服务永远稳定,而是接受服务总会出错的现实。
这个技术特征要求,在微服务的设计中,有自动的机制能够对其依赖的服务进行快速故障检测,在持续出错的时候进行隔离,在服务恢复的时候重新联通。所以“断路器”这类设施,对实际生产环境的微服务来说,并不是可选的外围组件,而是一个必须的支撑点。如果没有容错性的设计,系统很容易就会因为一两个服务的崩溃带来的雪崩效应而被淹没。
我想说的是可靠系统完全可以由会出错的服务来组成这是微服务最大的价值所在也是咱们这门课的开篇导读标题中“The Fenix Project”的含义。
第八演进式设计Evolutionary Design
容错性设计承认服务会出错,而演进式设计则是承认服务会被报废淘汰。
一个良好设计的服务,应该是能够报废的,而不是期望得到长久的发展。如果一个系统中出现不可更改、无可替代的服务,这并不能说明这个服务有多么重要,反而是系统设计上脆弱的表现。微服务带来的独立、自治,也是在反对这种脆弱性。
第九基础设施自动化Infrastructure Automation
基础设施自动化如CI/CD的长足发展大大降低了构建、发布、运维工作的复杂性。
由于微服务架构下,运维的服务数量比起单体架构来说,要有数量级的增长,所以使用微服务的团队,会更加依赖于基础设施的自动化。毕竟,人工是无法运维成百上千,乃至成千上万级别的服务的。
到这里通过我的解读你是不是已经大概理解了微服务核心的业务和技术特征了以上9个特征是一个合理的微服务系统展示出来的内、外在表现它能够指导你该如何应用微服务架构却不必作为一种强加于系统中的束缚来看待。
“Microservices: a definition of this new architectural term”一文中对微服务特征的描写已经非常具体了除定义了微服务是什么还专门申明了微服务不是什么微服务不是SOA的衍生品应该明确地与SOA划清界线不再贴上任何SOA的标签。
这样一来,微服务才算是一种真正丰满、独立、具体的架构风格,为它在未来的几年时间里,如同明星一般闪耀崛起于技术舞台奠定了坚实的基础。
Microservices and SOA-
This common manifestation of SOA has led some microservice advocates to reject the SOA label entirely, although others consider microservices to be one form of SOA , perhaps service orientation done right. Either way, the fact that SOA means such different things means its valuable to have a term that more crisply defines this architectural style.-
由于与SOA具有一致的表现形式这让微服务的支持者更加迫切地拒绝再被打上SOA的标签。一些人坚持认为微服务就是SOA的一种变体尽管仅从面向服务这个角度来考虑这个观点可以说也是正确的。但无论如何从整体上看SOA与微服务都是两种不同的东西。也因此使用一个别的名称来简明地定义这种架构风格就显得非常有必要了。-
—— Martin Fowler / James LewisMicroservices
从上面我对微服务的定义和特征的解读当中你还可以明显地感觉到微服务追求的是更加自由的架构风格它摒弃了SOA中几乎所有可以抛弃的约束和规定提倡以“实践标准”代替“规范标准”。
可是如果没有了统一的规范和约束以前SOA解决的那些分布式服务的问题不又都重新出现了吗
没错,的确如此。服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通讯、事务处理等问题,在微服务中,都不再会有统一的解决方案。
即使我们只讨论Java范围内会使用到的微服务那么光一个服务间通讯的问题可以列入候选清单的解决方案就有很多很多。比如RMISun/Oracle、ThriftFacebook、Dubbo阿里巴巴、gRPCGoogle、Motan2新浪、FinagleTwitter、brpc百度、ArvoHadoop、JSON-RPC、REST等等。
再来举个例子光一个服务发现问题我们可以选择的解决方案就有EurekaNetflix、ConsulHashiCorp、Nacos阿里巴巴、ZooKeeperApache、etcdCoreOS、CoreDNSCNCF等等。
其他领域的情况也很类似。总之,完全就是“八仙过海,各显神通”的局面。
所以说微服务所带来的自由是一把双刃开锋的宝剑。当软件架构者拿起这把宝剑的时候它的一刃指向的是SOA定下的复杂技术标准而在将选择的权力夺回的同一时刻另外一刃也正朝向着自己映出冷冷的寒光。
小结
在微服务时代中软件研发本身的复杂度应该说是有所降低一个简单服务并不见得就会同时面临分布式中所有的问题也就没有必要背上SOA那百宝袋般沉重的技术包袱。微服务架构下我们需要解决什么问题就引入什么工具团队熟悉什么技术就使用什么框架。
此外像Spring Cloud这样的胶水式的全家桶工具集通过一致的接口、声明和配置进一步屏蔽了源自于具体工具、框架的复杂性降低了在不同工具、框架之间切换的成本。所以作为一个普通的服务开发者作为一个“螺丝钉”式的程序员微服务架构对我们来说是很友善的。
可是,微服务对架构者来说却是满满的恶意,因为它对架构能力的要求可以说是史无前例。要知道,技术架构者的第一职责就是做决策权衡,有利有弊才需要决策,有取有舍才需要权衡。如果架构者本身的知识面不足以覆盖所需要决策的内容,不清楚其中的利弊,也就不可避免地会陷入选择困难症的困境之中。
总而言之,微服务时代充满着自由的气息,也充斥着迷茫的选择。软件架构不会止步于自由,微服务仍然不可能是架构探索的终点。如果有下一个时代,我希望信息系统能同时拥有微服务的自由权利,围绕业务能力构建自己的服务而不受技术规范管束,但同时又不必承担自行解决分布式问题的代价。管他什么利弊权衡!小孩子才做选择题,成年人全部都要!
一课一思
思考一下你所负责的产品是不是基于微服务的如果是它符合微服务的9个特征吗如果不是你的产品适合微服务架构吗你所在的企业、团队适合引入微服务吗
欢迎在留言区分享你的答案。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,110 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 _ 后微服务时代:跨越软件与硬件之间的界限
你好,我是周志明。今天,我们一起来探索后微服务时代。
在开始探讨这一讲的主题之前呢我想先跟你讨论一个问题。我们都知道在微服务架构中会面临一些必须解决的问题比如注册发现、跟踪治理、负载均衡、传输通讯等。但这些问题其实在SOA时代甚至可以说自原始分布式时代就一直存在了。既然只要是分布式系统就没办法完全避免这些问题那我们就回过头来想一下这些问题一定要由分布式系统自己来解决吗
既然这样,那我们就先不去纠结到底是用微服务还是什么别的架构,直接看看面对这些问题,现在最常见的解决方法是怎样的:
如果某个系统需要伸缩扩容,我们通常会购买新的服务器,多部署几套副本实例来分担压力;
如果某个系统需要解决负载均衡的问题,我们通常会布置负载均衡器,并选择恰当的均衡算法来分流;
如果需要解决安全传输的问题我们通常会布置TLS传输链路配置好CA证书以保证通讯不被窃听篡改
如果需要解决服务发现的问题我们通常会设置DNS服务器让服务访问依赖稳定的记录名而不是易变的IP地址等等。
所以你会发现,计算机科学经过了这么多年的发展,这些问题已经大多都有了专职化的基础设施来帮助解决了。
那么,在微服务时代,我们之所以不得不在应用服务层面,而不是基础设施层面去解决这些分布式问题,完全是因为由硬件构成的基础设施,跟不上由软件构成的应用服务的灵活性。这其实是一种无奈之举。
软件可以做到只使用键盘就能拆分出不同的服务只通过拷贝、启动就能够伸缩扩容服务。那么硬件难道也可以通过敲键盘就变出相应的应用服务器、负载均衡器、DNS服务器、网络链路等等的这些设施吗好像也可以啊
到这里你是不是已经知道了注册发现、跟踪治理等等问题的解决依靠的就是虚拟化技术和容器化技术。我们也就明白了微服务时代所取得的成就本身就离不开以Docker为代表的早期容器化技术的巨大贡献。
不知道你注意到没有,在这之前,我从来没有提起过“容器”二字。其实,这并不是我想刻意冷落它,而是因为早期的容器只是被简单地视为一种可快速启动的服务运行环境,使用它的目的是方便程序的分发部署。所以,早期阶段针对单个服务的容器,并没有真正参与到分布式问题的解决之中。
尽管2014年微服务真正崛起的时候Docker Swarm2013年和Apache Mesos2012年就已经存在了更早之前也出现过软件定义网络Software-Defined NetworkingSDN、软件定义存储Software-Defined StorageSDS等技术但是被业界广泛认可、普遍采用的通过虚拟化的基础设施去解决分布式架构问题的方案应该要从2017年Kubernetes赢得容器战争的胜利开始算起。
2017年可以说是容器生态发展历史中具有里程碑意义的一年。
在这一年长期作为Docker竞争对手的RKT容器一派的领导者CoreOS宣布放弃了自己的容器管理系统Fleet未来将会把所有容器管理功能转移到Kubernetes之上去实现。
在这一年容器管理领域的独角兽Rancher Labs宣布放弃其内置了数年的容器管理系统Cattle提出了“All-in-Kubernetes”战略从2.0版本开始把1.x版本能够支持多种容器管理工具的Rancher“升级”为只支持Kubernetes一种的容器管理系统。
在这一年Kubernetes的主要竞争者Apache Mesos在9月正式宣布了“Kubernetes on Mesos”集成计划开始由竞争关系转为了对Kubernetes提供支持使其能够与Mesos的其他一级框架如HDFS、Spark和Chros等进行集群资源动态共享、分配与隔离。
在这一年Kubernetes的最大竞争者Docker Swarm的母公司Docker终于在10月被迫宣布Docker要同时支持Swarm与Kubernetes两套容器管理系统也就是承认了Kubernetes的统治地位。
至此这场已经持续了三、四年时间以Docker Swarm、Apache Mesos与Kubernetes为主要竞争者的“容器战争”终于有了明确结果。可以说Kubernetes最后从众多的容器管理系统中脱颖而出、“登基加冕”就代表了容器发展中一个时代的结束。而且我可以说它带来的容器间网络、服务、负载均衡、配置等虚拟化基础设施也将会是开启下一个软件架构发展新纪元的钥匙。
我为什么会这么肯定呢?
针对同一个分布式服务的问题对比下Spring Cloud中提供的应用层面的解决方案以及Kubernetes中提供的基础设施层面的解决方案你就可以明白其中缘由了。
虽然Spring Cloud和Kubernetes的出发点不同解决问题的方法和效果也不一样但不容忽视的是Kubernetes的确提供了一条全新的、前途更加广阔的解题思路。
我说的“前途广阔”,不仅仅是一句恭维赞赏的客气话。当虚拟化的基础设施,开始从单个服务的容器发展到由多个容器构成的服务集群,以及集群所需的所有通讯、存储设施的时候,软件与硬件的界限就开始模糊了。
一旦硬件能够跟得上软件的灵活性,那么这些与业务无关的技术问题,便很可能从软件的层面剥离出来,在硬件的基础设施之内就被悄悄解决掉,让软件可以只专注于业务,真正“围绕业务能力构建”团队与产品。那么原来只能从软件层面解决的分布式架构问题,于是有了另外一种解法:应用代码与基础设施软硬一体,合力应对。
这样一来在DCE中未能实现的“透明的分布式应用”就成为了可能Martin Fowler设想的“凤凰服务器”就成为了可能Chad Fowler提出的“不可变基础设施”也会成为可能。
没错,我们借此就来到了现在媒体文章中常说的“云原生”时代。这样理解下来,“云原生”这个概念,是不是没那么抽象了。
云原生时代追求的目标,跟此前微服务时代中追求的目标相比,并没有什么本质的改变,它们都是通过一系列小型服务去构建大型系统。在服务架构演进的历史进程中,我更愿意把“云原生时代”称为“后微服务时代”。
不过还有一点值得注意的是前面我说Kubernetes成为了容器战争的胜利者标志着后微服务时代的开端但Kubernetes其实并没有完美地解决全部的分布式问题。
这里所说的“不完美”的意思是仅从功能灵活强大这点来看Kubernetes反而还不如之前的Spring Cloud方案。这是因为有一些问题处于应用系统与基础设施的边缘我们很难能完全在基础设施的层面中去精细化地解决掉它们。
给你举个例子微服务A调用了微服务B中发布的两个服务我们称之为B1和B2假设B1表现正常但B2出现了持续的500错那在达到一定的阈值之后我们就应该对B2进行熔断以避免产生雪崩效应。如果我们仅在基础设施的层面来做处理这就会遇到一个两难问题也就是切断A到B的网络通路会影响到B1的正常调用而不切断的话则会持续受到B2的错误影响。
这种问题在通过Spring Cloud这类应用代码实现的微服务中其实并不难处理反正是使用代码或者配置来解决问题只要合乎逻辑我们想做什么功能都是可以的只是会受限于开发人员的想象力与技术能力。但基础设施是针对整个容器来做整体管理的它的粒度就相对粗犷。
实际上类似的情况不仅仅会在断路器上出现服务的监控、认证、授权、安全、负载均衡等功能都有细化管理的需求。比如服务调用时的负载均衡往往需要根据流量特征调整负载均衡的层次、算法等而DNS尽管能实现一定程度的负载均衡但它通常并不能满足这些额外的需求。
所以为了解决这一类问题微服务基础设施很快就进行了第二次进化引入在今天被我们叫做是“服务网格”Service Mesh的“边车代理模式”Sidecar Proxy
所谓的“边车”,是指一种带挎斗的三轮摩托,我小时候还算常见,现在基本就只在抗日神剧中才会看到了。
具体到咱们现在的语境里“边车”的意思是微服务基础设施会由系统自动地在服务的资源容器指Kubernetes的Pod中注入一个通讯代理服务器相当于那个挎斗用类似网络安全里中间人攻击的方式进行流量劫持在应用毫无感知的情况下悄悄接管掉应用的所有对外通讯。
这个代理除了会实现正常的服务调用以外(称为数据平面通讯),同时还接受来自控制器的指令(称为控制平面通讯),根据控制平面中的配置,分析数据平面通讯的内容,以实现熔断、认证、度量、监控、负载均衡等各种附加功能。
这样,就实现了既不需要在应用层面附带额外的代码,也提供了几乎不亚于应用代码的精细管理能力的目的。
来自Istio的配置文档图中的Mixer在Istio 1.5之后已经取消,这里仅作示意)
虽然,我们很难从概念上,来判定一个与应用系统运行于同一资源容器之内的代理服务,到底应该算是软件,还是算作基础设施,但只要它对应用是透明的,不需要改动任何软件代码就可以实现服务治理,这就足够了。
小结
今天,我带着你一起游览了后微服务时代,一起了解了容器化技术兴起对软件架构、软件开发的改变,并一起探讨了微服务如何通过虚拟化基础设施,来解决分布式问题的办法,即今天服务网格中的“边车代理模式”。
服务网格在2018年才火了起来到今天它仍然是一个新潮的概念Istio和Envoy的发展时间还很短仍然没有完全成熟甚至连Kubernetes也还算是个新生事物以它开源的日期来计算
但我相信未来几年Kubernetes将会成为服务器端标准的运行环境如同在此之前的Linux一样服务网格将会成为微服务之间通讯交互的主流模式它会把“选择什么通讯协议”“如何做认证授权”之类的技术问题隔离于应用软件之外取代今天的Spring Cloud全家桶中的大部分组件的功能。这是最理想的Smart Endpoints解决方案微服务只需要考虑业务本身的逻辑就行了。
上帝的归上帝,凯撒的归凯撒,业务与技术完全分离,远程与本地完全透明,我想也许这就是分布式架构最好的时代吧。
一课一思
分布式架构发展到服务网格后,真的是到达“最好的时代”了吗?软件架构的发展不太可能真的就此止步,你认为今天的云原生还有哪些主要矛盾,下一次软件架构的进化将会主要解决什么问题?
欢迎你在留言区分享你的看法。如果你觉得有收获,也欢迎你把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,74 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 _ 无服务时代:“不分布式”云端系统的起点
你好,我是周志明。今天是探索“演进中的架构”的最后一讲,我们来聊聊最近一两年才开始兴起的“无服务架构”。
我们都知道,分布式架构出现的最初目的,是要解决单台机器的性能成为整个软件系统的瓶颈的问题。后来随着技术的演进,容错能力、技术异构、职责划分等其他因素,也都成了分布式架构要考虑的问题。但不可否认的是,获得更好的性能,仍然在架构设计中占有非常大的比重。
在前面几讲我们也说,分布式架构也会引入一些新问题(比如服务的安全、容错,分布式事务的一致性),因此对软件开发这件事儿来说,不去做分布式无疑是最简单的。如果单台服务器的性能可以是无限的,那架构演进的结果,肯定会跟今天不一样。不管是分布式和容器化,还是微服务,恐怕都未必会出现了,最起码不会是今天的模样。
当然了绝对意义上的无限性能肯定是不存在的但相对意义上的无限性能其实已经实现了云计算的成功落地就可以说明这一点。对基于云计算的软件系统来说无论用户有多少、逻辑如何复杂AWS、阿里云等云服务提供商都能在算力上满足系统对性能的需求只要你能为这种无限的性能支付得起对应的代价。
在工业界2012年iron.io公司率先提出了“无服务”Serverless应该翻译为“无服务器”才合适但现在用“无服务”已形成习惯了的概念2014年开始AWS发布了命名为Lambda的商业化无服务应用并在后续的几年里逐步得到了开发者的认可发展成目前世界上最大的无服务的运行平台到了2019年中国的阿里云、腾讯云等厂商也发布了无服务的产品。“无服务”成了近期技术圈里的“新网红”之一。
我们再看看学术界对无服务的态度。在2009年云计算刚提出的时候UC Berkeley大学就发表了一篇论文“Above the Clouds: A Berkeley View of Cloud Computing”文中预言的云计算的价值、演进和普及在过去的十年2009~2019年里一一得到了验证。十年之后的2019年UC Berkeley的第二篇命名风格相同的论文“Cloud Programming Simplified: A Berkeley View on Serverless Computing”再次预言“无服务将会成为日后云计算的主流方式”。
由此可见,主流学术界也同样认可无服务是未来的一个发展方向。
虽然工业界和学术界在“无服务”这件事儿上都取得了些成果但是到今天“无服务”也还没有一个特别权威的定义。不过这也不是什么问题毕竟它没有我们前面讲到的微服务、SOA等各种架构那么复杂它最大的卖点就是简单只涉及了后端设施Backend和函数Function两块内容。
后端设施是指数据库、消息队列、日志、存储等这一类用于支撑业务逻辑运行但本身无业务含义的技术组件。这些后端设施都运行在云中也就是无服务中的“后端即服务”Backend as a ServiceBaaS
函数指的就是业务逻辑代码。这里函数的概念与粒度都已经和程序编码角度的函数非常接近了区别就在于无服务中的函数运行在云端不必考虑算力问题和容量规划从技术角度可以不考虑但从计费的角度来看你还是要掂量一下自己的钱包够不够用也就是无服务中的“函数即服务”Function as a ServiceFaaS
无服务的愿景是让开发者只需要纯粹地关注业务:一是,不用考虑技术组件,因为后端的技术组件是现成的,可以直接取用,没有采购、版权和选型的烦恼;二是,不需要考虑如何部署,因为部署过程完全是托管到云端的,由云端自动完成;三是,不需要考虑算力,因为有整个数据中心的支撑,算力可以认为是无限的;四是,也不需要操心运维,维护系统持续地平稳运行是云服务商的责任,而不再是开发者的责任。
你看这是不是就像从汇编语言发展到高级语言后开发者不用再去关注寄存器、信号、中断等与机器底层相关的细节没错儿UC Berkeley的论文“Cloud Programming Simplified: A Berkeley View on Serverless Computing”中就是这样描述无服务给生产力带来的极大解放的。
不过,无服务架构的远期前景也许很美好,但我自己对无服务中短期内的发展,并没有那么乐观。为什么这么说呢?
与单体架构、微服务架构不同,无服务架构天生的一些特点,比如冷启动、 无状态、运行时间有限制等等,决定了它不是一种具有普适性的架构模式。除非是有重大变革,否则它也很难具备普适性。
一方面对一些适合的应用来说使用无服务架构确实能够降低开发和运维环节的成本比如不需要交互的离线大规模计算又比如多数Web资讯类网站、小程序、公共API服务、移动应用服务端等都跟无服务架构擅长的短链接、无状态、适合事件驱动的交互形式很契合。
但另一方面,对于那些信息管理系统、网络游戏等应用来说,又或者说对所有具有业务逻辑复杂、依赖服务端状态、响应速度要求较高、需要长连接等特征的应用来说,无服务架构至少在目前来看并不是最合适的。
这是因为无服务天生“无限算力”的假设就决定了它必须要按使用量函数运算的时间和内存来计费以控制消耗算力的规模所以函数不会一直以活动状态常驻服务器只有请求到了才会开始运行。这导致了函数不便于依赖服务端状态也导致了函数会有冷启动时间响应的性能不可能会太好目前无服务的云函数冷启动过程大概是在百毫秒级别对于Java这类启动性能差的应用甚至能到秒级
但无论如何云计算毕竟是大势所趋今天信息系统建设的概念和观念在较长尺度的“明天”都是会转变成适应云端的。我并不怀疑Serverless+API的这种设计方式随着云计算的持续发展将会成为一种主流的软件架构形式无服务到时候也应该会有更广阔的应用空间。
如果说微服务架构是分布式系统这条路当前所能做到的极致,那无服务架构,也许就是“不分布式”的云端系统这条路的起点。
虽然在顺序上,我把“无服务”安排到了“微服务”和“云原生”时代之后,但它们并没有继承替代关系。我之所以要强调这一点,是为了避免你可能会从两者的名称和安排顺序的角度,产生“无服务比微服务更加先进”的错误想法。我相信,软件开发的未来,不会只存在某一种“最先进的”架构风格,而是会有多种具有针对性的架构风格并存。这才是软件产业更有生命力的形态。
我同样也相信,软件开发的未来,多种架构风格将会融合互补,“分布式”与“不分布式”的边界将会逐渐模糊,两条路线将会在云端的数据中心交汇。
今天我们已经能初步看见一些使用无服务的云函数去实现微服务架构的苗头了所以把无服务作为技术层面的架构把微服务视为应用层面的架构这样的组合使用也是完全合理可行的。比如根据腾讯公开的资料企业微信、QQ小程序、腾讯新闻等产品就是使用自己的无服务框架构成的微服务系统。以后无论是通过物理机、虚拟机、容器或者是无服务云函数都会是微服务实现方案的一个候选项。
小结
今天是架构演进历史的最后一讲如第1讲的开篇所说我们谈历史重点不在考古而是要借历史之名来理解每种架构出现的意义以及被淘汰的原因。这样我们才能更好地解决今天遇到的各种实际的问题看清楚未来架构演进的发展道路。
对于架构演进的未来2014年的时候Martin Fowler和James Lewis在《Microservices》的结束语中分享的观点是他们对于微服务日后能否被大范围地推广最多只能持谨慎的乐观态度。无服务方兴未艾的今天与那时微服务的情况十分相近我对无服务日后的推广也是持有谨慎的乐观态度。软件开发的最大挑战就在于只能在不完备的信息下决定当前要处理的问题。
时至今日,我们依然很难预想在架构演进之路的前方,微服务和无服务之后,还会出现什么形式的架构风格,这也正契合了图灵的那句名言:尽管目光所及之处,只是不远的前方,即使如此,依然可以看到那里有许多值得去完成的工作在等待我们。
We can only see a short distance ahead, but we can see plenty there that needs to be done.-
尽管目光所及之处,只是不远的前方,即使如此,依然可以看到那里有许多值得去完成的工作在等待我们。-
—— Alan Turing, Computing Machinery and Intelligence, 1950
一课一思
在这一讲之前,你是否了解、接触过无服务架构?无服务目前在中国处于起步的发展阶段,阿里云、腾讯云的无服务计算框架,都给了普通用户相当大的免费额度,你愿意去试一下吗?
欢迎你在留言区分享你的看法。如果你觉得有收获,也欢迎你把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,161 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 _ 远程服务调用(上):从本地方法到远程方法的桥梁
你好,我是周志明。从今天这一讲开始,我们就进入了课程的第二个模块:架构师的视角。
“架构师”这个词,其实指向非常宽泛,你可以说做企业战略设计的是架构师,也可以说做业务流程分析的是架构师。而在这门课程中,我所针对的架构师视角,特指软件系统中技术模型的系统设计者。在这个模块当中,我会带你系统性地了解,在做架构设计的时候,架构师都应该思考哪些问题、可以选择哪些主流的解决方案和行业标准做法,以及这些主流方案都有什么优缺点、会给架构设计带来什么影响,等等。
理解了架构师的这些职责,你对“架构设计”这种听起来就很抽象的工作,是不是有个更具体的认识了?
从今天开始我会花两讲的时间和你一起学习“远程服务调用Remote Procedure CallRPC”这个话题。我会尽可能地从根源到现状、从表现到本质为你解释清楚RPC的一些常见的问题。
那今天我们就先从“什么是RPC”开始一起去学习“远程服务”这个构建分布式系统的最基本的前置条件看看它是如何出现、如何发展的以及当前业界的主流实现手段。
其实RPC这个词儿在计算机科学中已经有超过40年的历史了肯定不是一个新概念。但是直到今天我们还是会在知乎等网站上看到很多人提问“什么是RPC”“如何评价某某RPC技术”“RPC好还是REST好仍然“每天”都有新的不同形状的RPC轮子被发明出来仍然有层出不穷的文章去比对Google gRPC、Facebook Thrift等各个厂家的RPC技术的优劣。
像计算机科学这种知识快速更迭的领域一项40岁高龄的技术能有如此的关注度可以说是相当稀罕的现象了。那为什么会出现这种现象呢
我分析了其中的原因一方面可能是微服务风潮带来的热度另一方面也不得不承认作为开发者我们很多人对RPC本身可以解决什么问题、如何解决这些问题、为什么要这样解决都或多或少存在些认知模糊的情况。
那接下来我就给你详细解读一下关于RPC的各种分歧和普遍的错误认知。
进程间通讯
尽管今天的大多数RPC技术已经不再追求“与本地方法调用一致”这个目标了但不可否认的是RPC出现的最初目的就是为了让计算机能够跟调用本地方法一样去调用远程方法。所以我们先来看一下在本地方法调用的时候都会发生些什么。
我们先通过下面这段Java风格的伪代码来定义几个概念
// 调用者Caller main()
// 被调用者Callee println()
// 调用点Call Site 发生方法调用的指令流位置
// 调用参数Parameter 由Caller传递给Callee的数据即“hello world”
// 返回值Retval 由Callee传递给Caller的数据如果方法正常完成返回值是void否则是对应的异常
public static void main(String[] args) {
System.out.println(“hello world”);
}
通过这段伪代码你可以发现在完全不考虑编译器优化的前提下程序运行至调用println()这一行的时候,计算机(物理机或者虚拟机)会做以下这些事情:
传递方法参数将字符串hello world的引用压栈。
确定方法版本根据println()方法的签名确定它的执行版本其实并不是一个简单的过程不管是编译时的静态解析也好还是运行时的动态分派也好程序都必须根据某些语言规范中明确定义的原则找到明确的被调用者Callee。这里的“明确”是指唯一的一个Callee或者有严格优先级的多个Callee比如不同的重载版本。我曾在《深入理解Java虚拟机》中用一整章介绍过这个过程。如果你感兴趣的话可以去深入了解一下。
执行被调方法从栈中获得Parameter以此为输入执行Callee内部的逻辑。
返回执行结果将Callee的执行结果压栈并将指令流恢复到Call Site处继续向下执行。
接下来我们就需要考虑一下当println()方法不在当前进程的内存地址空间中,会出现什么问题。不难想到,此时至少面临两个直接的障碍:
第一个障碍前面的第一步和第四步所做的传递参数、传回结果都依赖于栈内存的帮助如果Caller与Callee分属不同的进程就不会拥有相同的栈内存那么在Caller进程的内存中将参数压栈对于Callee进程的执行毫无意义。
第二个障碍第二步的方法版本选择依赖于语言规则的定义而如果Caller与Callee不是同一种语言实现的程序方法版本选择就将是一项模糊的不可知行为。
所以为了简化我们暂时忽略第二个障碍假设Caller与Callee是使用同一种语言实现的先来解决两个进程之间如何交换数据的问题这件事情在计算机科学中被称为“进程间通讯”Inter-Process CommunicationIPC。那么我们可以考虑的解决办法就有以下几种
第一管道Pipe或具名管道Named Pipe
管道其实类似于两个进程间的桥梁,用于进程间传递少量的字符流或字节流。普通管道可用于有亲缘关系进程间的通信(由一个进程启动的另外一个进程);而具名管道摆脱了普通管道没有名字的限制,除了具有普通管道所具有的功能以外,它还允许无亲缘关系进程间的通信。
管道典型的应用就是命令行中的“ | ”操作符比如说命令“ps -ef | grep java” ,就是管道操作符“ | ”将ps命令的标准输出通过管道连接到grep命令的标准输入上。
第二信号Signal
信号是用来通知目标进程有某种事件发生的。除了用于进程间通信外信号还可以被进程发送给进程自身。信号的典型应用是kill命令比如“kill -9 pid”意思就是由Shell进程向指定PID的进程发送SIGKILL信号。
第三信号量Semaphore
信号量是用于两个进程之间同步协作的手段相当于操作系统提供的一个特殊变量。我们可以在信号量上进行wait()和notify()操作。
第四消息队列Message Queue
前面所说的这三种方式只适合传递少量信息而POSIX标准中有定义“消息队列”用于进程间通讯的方法。也就是说进程可以向队列中添加消息而被赋予读权限的进程则可以从队列中消费消息。消息队列就克服了信号承载信息量少、管道只能用于无格式字节流以及缓冲区大小受限等缺点 ,但实时性相对受限。
第五共享内存Shared Memory
允许多个进程可以访问同一块内存空间,这是效率最高的进程间通讯形式。进程的内存地址空间是独立隔离的,但操作系统提供了让进程主动创建、映射、分离、控制某一块内存的接口。由于内存是多进程共享的,所以往往会与其它通信机制,如信号量等结合使用,来达到进程间的同步及互斥。
第六本地套接字接口IPC Socket
消息队列和共享内存这两种方式,只适合单机多进程间的通讯。而套接字接口,是更为普适的进程间通信机制,可用于不同机器之间的进程通信。
套接字Socket起初是由Unix系统的BSD分支开发出来的但现在已经移植到所有的Unix和Linux系统上了。基于效率考虑当仅限于本机进程间通讯的时候套接字接口是被优化过的不会经过网络协议栈不需要打包拆包、计算校验和、维护序号和应答等操作只是简单地将应用层数据从一个进程拷贝到另一个进程这种进程间通讯方式有个专有的名称Unix Domain Socket又叫做IPC Socket。
通信的成本
我之所以花这么多篇幅来介绍IPC的手段是因为计算机科学家们最初的想法就是将RPC作为IPC的一种特例来看待其实现在分类上这么说也仍然合适只是在具体操作手段上不会这么做了
这里我们需要特别关注的是最后一种基于套接字接口的通讯方式IPC Socket。因为它不仅适用于本地相同机器的不同进程间通讯而且因为Socket是网络栈的统一接口它也理所当然地能支持基于网络的跨机器、跨进程的通讯。比如Linux系统的图形化界面中X Window服务器和GUI程序之间的交互就是由这套机制来实现的。
此外这样做还有一个看起来无比诱人的好处。因为IPC Socket是操作系统提供的标准接口所以它完全有可能把远程方法调用的通讯细节隐藏在操作系统底层从应用层面上来看可以做到远程调用与本地方法调用几乎完全一致。
事实上,在原始分布式时代的初期确实是奔着这个目标去做的,但这种透明的调用形式反而让程序员们误以为通信是无成本的,从而被滥用,以至于显著降低了分布式系统的性能。
1987年当“透明的RPC调用”一度成为主流范式的时候安德鲁 · 塔能鲍姆Andrew Tanenbaum教授曾发表了一篇论文“A Critique of the Remote Procedure Call Paradigm”对这种透明的RPC范式提出了一系列质问
两个进程通讯,谁作为服务端,谁作为客户端?
怎样进行异常处理?异常该如何让调用者获知?
服务端出现多线程竞争之后怎么办?
如何提高网络利用的效率,比如连接是否可被多个请求复用以减少开销?是否支持多播?
参数、返回值如何表示?应该有怎样的字节序?
如何保证网络的可靠性,比如调用期间某个链接忽然断开了怎么办?
服务端发送请求后,收不到回复该怎么办?
……
论文的中心观点是:把本地调用与远程调用当作一样的来处理,是犯了方向性的错误,把系统间的调用做成透明的,反而会增加程序员工作的复杂度。
此后几年关于RPC应该如何发展、如何实现的论文层出不穷有支持的也有反对有冷静分析的也有狂热唾骂的但历史逐渐证明了Andrew Tanenbaum的预言是正确的。
最终1994年至1997年间由ACM和Sun的院士Peter Deutsch、套接字接口发明者Bill Joy、Java之父James Gosling等众多在Sun Microsystems工作的大佬们共同总结了通过网络进行分布式运算的八宗罪8 Fallacies of Distributed Computing
网络是可靠的The network is reliable
延迟是不存在的Latency is zero
带宽是无限的Bandwidth is infinite
网络是安全的The network is secure
拓扑结构是一成不变的Topology doesnt change
总会有一个管理员There is one administrator
不考虑传输成本Transport cost is zero
网络是同质化的The network is homogeneous
这八宗罪被认为是程序员在网络编程中经常忽略的八大问题潜台词就是如果远程服务调用要弄透明化的话就必须为这些罪过买单。这算是给RPC能否等同于IPC来实现暂时定下了一个具有公信力的结论。
到这时为止RPC应该是一种高层次的或者说语言层次的特征而不是像IPC那样是低层次的或者说系统层次的特征就成为了工业界、学术界的主流观点。
在1980年代初期传奇的施乐Palo Alto研究中心发布了基于Cedar语言的RPC框架Lupine并实现了世界上第一个基于RPC的商业应用Courier。这里施乐PARC定义的“远程服务调用”的概念就是符合上面针对RPC的结论的。所以尽管此前已经有用其他名词指代RPC的操作我们也一般认为RPC的概念最早是由施乐公司所提出的。
首次提出远程服务调用的定义-
Remote procedure call is the synchronous language-level transfer of control between programs in address spaces whose primary communication is a narrow channel.-
—— Bruce Jay NelsonRemote Procedure CallXerox PARC1981
到这里我们就可以得出RPC的定义了RPC是一种语言级别的通讯协议它允许运行于一台计算机上的程序以某种管道作为通讯媒介即某种传输协议的网络去调用另外一个地址空间通常为网络上的另外一台计算机
小结
这一讲我们讨论了RPC的起源、概念以及它发展上的一些分歧。以此为基础我们才能更好地理解后面几讲要学习的内容包括RPC本身要解决的三大问题、RPC框架的现状与发展以及它与REST的区别。
RPC以模拟进程间方法调用为起点许多思想和概念都借鉴的是IPC因此这一讲我也介绍了IPC中的一些关键概念和实现方法。但是RPC原本想照着IPC的发展思路却在实现层面上遇到了很大的困难。RPC作为一种跨网络的通讯手段能否无视通讯的成本去迁就编程和设计的原则这一点从几十年前的DCE开始直到今天学术界、工业界都还有争议。
在下一讲我会和你一起学习在RPC的定义提出之后工业界中出现过的、著名的RPC协议以及当今常用的各种RPC框架学习它们的共性也就是它们都必须解决哪几个问题各自以什么为关注点以及为何不会出现“完美的”RPC框架。
一课一思
“远程方法不应该无视通讯成本”这个观点从性能的角度来看是有益的但从简单的角度看则是有害的。在现代的软件系统开发中你用过什么RPC框架吗它们有没有把“像本地方法一样调用远程方法”作为卖点
欢迎在留言区分享你的答案。如果你觉得有收获,也欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,199 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 _ 远程服务调用如何选择适合自己的RPC框架
你好,我是周志明。
上一讲我们主要是从学术的角度出发一起学习了RPC概念的形成过程。今天这一讲我会带你从技术的角度出发去看看工业界在RPC这个领域曾经出现过的各种协议以及时至今日还在层出不穷的各种框架。你会从中了解到RPC要解决什么问题以及如何选择适合自己的RPC框架。
RPC框架要解决的三个基本问题
在第1讲“原始分布式时代”中我曾提到过在80年代中后期惠普和Apollo提出了网络运算架构Network Computing ArchitectureNCA的设想并随后在DCE项目中发展成了在Unix系统下的远程服务调用框架DCE/RPC。
这是历史上第一次对分布式有组织的探索尝试。因为DCE本身是基于Unix操作系统的所以DCE/RPC也仅面向于Unix系统程序之间的通用。
补充这句话其实不全对微软COM/DCOM的前身MS RPC就是DCE的一种变体版本而它就可以在Windows系统中使用。
在1988年Sun Microsystems起草并向互联网工程任务组Internet Engineering Task ForceIETF提交了RFC 1050规范此规范中设计了一套面向广域网或混合网络环境的、基于TCP/IP网络的、支持C语言的RPC协议后来也被称为是ONC RPCOpen Network Computing RPC/Sun RPC
这两个RPC协议就可以算是如今各种RPC协议的鼻祖了。从它们开始一直到接下来的这几十年所有流行过的RPC协议都不外乎通过各种手段来解决三个基本问题
如何表示数据?
如何传递数据?
如何表示方法?
接下来,我们分别看看是如何解决的吧。
如何表示数据?
这里的数据包括了传递给方法的参数,以及方法的返回值。无论是将参数传递给另外一个进程,还是从另外一个进程中取回执行结果,都会涉及应该如何表示的问题。
针对进程内的方法调用,我们使用程序语言内置的和程序员自定义的数据类型,就很容易解决数据表示的问题了;而远程方法调用,则可能面临交互双方分属不同程序语言的情况。
所以即使是只支持同一种语言的RPC协议在不同硬件指令集、不同操作系统下也完全可能有不一样的表现细节比如数据宽度、字节序的差异等。
行之有效的做法,是将交互双方涉及的数据,转换为某种事先约定好的中立数据流格式来传输,将数据流转换回不同语言中对应的数据类型来使用。这个过程说起来比较拗口,但相信你一定很熟悉它,这其实就是序列化与反序列化。
每种RPC协议都应该有对应的序列化协议比如
ONC RPC的External Data Representation XDR
CORBA的Common Data RepresentationCDR
Java RMI的Java Object Serialization Stream Protocol
gRPC的Protocol Buffers
Web Service的XML Serialization
众多轻量级RPC支持的JSON Serialization
……
如何传递数据?
准确地说如何传递数据是指如何通过网络在两个服务Endpoint之间相互操作、交换数据。这里“传递数据”通常指的是应用层协议实际传输一般是基于标准的TCP、UDP等传输层协议来完成的。
两个服务交互不是只扔个序列化数据流来表示参数和结果就行了,诸如异常、超时、安全、认证、授权、事务等信息,都可能存在双方交换信息的需求。
在计算机科学中专门有一个“Wire Protocol”用来表示两个Endpoint之间交换这类数据的行为。常见的Wire Protocol有以下几种
Java RMI的Java Remote Message ProtocolJRMP也支持RMI-IIOP
CORBA的Internet Inter ORB ProtocolIIOP是GIOP协议在IP协议上的实现版本
DDS的Real Time Publish Subscribe ProtocolRTPS
Web Service的Simple Object Access ProtocolSOAP
如果要求足够简单双方都是HTTP Endpoint直接使用HTTP也可以如JSON-RPC
……
如何表示方法?
“如何表示方法”,这在本地方法调用中其实也不成问题,因为编译器或者解释器会根据语言规范,把调用的方法转换为进程地址空间中方法入口位置的指针。
不过一旦考虑到不同语言,这件事儿又麻烦起来了,因为每门语言的方法签名都可能有所差别,所以,针对“如何表示一个方法”和“如何找到这些方法”这两个问题,我们还是得有个统一的标准。
这个标准做起来其实可以很简单只要给程序中的每个方法都规定一个通用的又绝对不会重复的编号在调用的时候直接传这个编号就可以找到对应的方法。这种听起来无比寒碜的办法还真的就是DCE/RPC最初准备的解决方案。
虽然最后DCE还是弄出了一套跟语言无关的接口描述语言Interface Description LanguageIDL成为了此后许多RPC参考或依赖的基础如CORBA的OMG IDL但那个唯一的“绝不重复”的编码方案UUID却意外地流行了起来已经被广泛应用到了程序开发的方方面面。
这类用于表示方法的协议还有:
Android的Android Interface Definition LanguageAIDL
CORBA的OMG Interface Definition LanguageOMG IDL
Web Service的Web Service Description LanguageWSDL
JSON-RPC的JSON Web Service ProtocolJSON-WSP
……
你看如何表示数据、如何传递数据、如何表示方法这三个RPC中的基本问题都可以在本地方法调用中找到对应的操作。RPC的思想始于本地方法调用尽管它早就不再追求要跟本地方法调用的实现完全一样了但RPC的发展仍然带有本地方法调用的深刻烙印。因此我们在理解PRC的本质时比较轻松的方式是以它和本地调用的联系来对比着理解。
理解了RPC要解决的三个基本问题以后我们接着来看一下现代的RPC框架都为我们提供了哪些可选的解决方案以及为什么今天会有这么多的RPC框架在并行发展。
统一的RPC
DCE/RPC与ONC RPC都有很浓厚的Unix痕迹所以它们其实并没有真正地在Unix系统以外大规模流行过而且它们还有一个“大问题”只支持传递值而不支持传递对象ONC RPC的XDR的序列化器能用于序列化结构体但结构体毕竟不是对象。这两个RPC协议都是面向C语言设计的根本就没有对象的概念。
而90年代正好又是面向对象编程Object-Oriented ProgrammingOOP风头正盛的年代所以在1991年对象管理组织Object Management GroupOMG便发布了跨进程、面向异构语言的、支持面向对象的服务调用协议CORBA 1.0Common Object Request Broker Architecture
CORBA 1.0和1.1版本只提供了对C和C++的支持而到了末代的CORBA 3.0版本不仅支持了C、C++、Java、Object Pascal、Python、Ruby等多种主流编程语言还支持了Smalltalk、Lisp、Ada、COBOL等已经“半截入土”的非主流语言阵营不可谓不强大。
可以这么说CORBA是一套由国际标准组织牵头、由多个软件提供商共同参与的分布式规范。在当时只有微软私有的DCOM的影响力可以稍微跟CORBA抗衡一下。但是与DCE一样DCOM也受限于操作系统不过比DCE厉害的是DCOM能跨语言哟。所以能够同时支持跨系统、跨语言的CORBA其实原本是最有机会统一RPC这个细分领域的竞争者。
但很无奈的是CORBA并没有抓住这个机会。一方面CORBA本身的设计实在是太过于啰嗦和繁琐了甚至有些规定简直到了荒谬的程度。比如说我们要写一个对象请求代理ORB这是CORBA中的关键概念大概要200行代码其中大概有170行是纯粹无用的废话这句带有鞭尸性质的得罪人的评价不是我说的是CORBA的首席科学家Michi Henning在文章《The Rise and Fall of CORBA》中自己说的
另一方面为CORBA制定规范的专家逐渐脱离实际了所以做出的CORBA规范非常晦涩难懂导致各家语言的厂商都有自己的解读结果弄出来的CORBA实现互不兼容实在是对CORBA号称支持众多异构语言的莫大讽刺。这也间接造就了后来W3C Web Service的出现。
所以Web Service一出现CORBA就在这场竞争中犹如十八路诸侯讨董卓互乱阵脚、一触即溃局面可以说是惨败无比。最终下场就是CORBA和DCOM一起被扫进了计算机历史的博物馆中而Web Service获得了一统RPC的大好机会。
1998年XML 1.0发布并成为了万维网联盟World Wide Web ConsortiumW3C的推荐标准。1999年末以XML为基础的SOAP 1.0Simple Object Access Protocol规范的发布代表着一种被称为“Web Service”的全新RPC协议的诞生。
Web Service是由微软和DevelopMentor公司共同起草的远程服务协议随后被提交给W3C并通过投票成为了国际标准。所以Web Service也被称为是W3C Web Service。
Web Service采用了XML作为远程过程调用的序列化、接口描述、服务发现等所有编码的载体当时XML是计算机工业最新的银弹只要是定义为XML的东西几乎就都被认为是好的风头一时无两连微软自己都主动宣布放弃DCOM迅速转投Web Service的怀抱。
交给W3C管理后Web Service再没有天生属于哪家公司的烙印商业运作非常成功很受市场欢迎大量的厂商都想分一杯羹。但从技术角度来看它设计得也并不优秀甚至同样可以说是有显著缺陷。
对于开发者而言Web Service的一大缺点就是过于严格的数据和接口定义所带来的性能问题。
虽然Web Service吸取了CORBA的教训不再需要程序员手工去编写对象的描述和服务代理了但是XML作为一门描述性语言本身的信息密度就很低都不用与二进制协议比与今天的JSON或YAML比一下就知道了。同时Web Service是一个跨语言的RPC协议这使得一个简单的字段为了在不同语言中不会产生歧义要以XML描述去清楚的话往往比原本存储这个字段值的空间多出十几倍、几十倍乃至上百倍。
这个特点就导致了要想使用Web Service就必须要有专门的客户端去调用和解析SOAP内容也需要专门的服务去部署如Java中的Apache Axis/CXF更关键的是这导致了每一次数据交互都包含大量的冗余信息性能非常差。
如果只是需要客户端、传输性能差也就算了又不是不能用。既然选择了XML来获得自描述能力也就代表着没打算把性能放到第一位。但是Web Service还有另外一点原罪贪婪。
“贪婪”是指它希望在一套协议上一揽子解决分布式计算中可能遇到的所有问题。这导致Web Service生出了一整个家族的协议出来。
Web Service协议家族中除它本身包括了的SOAP、WSDL、UDDI协议之外还有一堆以WS-*命名的子功能协议,来解决事务、一致性、事件、通知、业务描述、安全、防重放等问题。这些几乎数不清个数的家族协议,对开发者来说学习负担极其沉重。结果就是,得罪惨了开发者,谁爱用谁用去。
当程序员们对Web Service的热情迅速燃起又逐渐冷却之后也不禁开始反思那些面向透明的、简单的RPC协议如DCE/RPC、DCOM、Java RMI要么依赖于操作系统要么依赖于特定语言总有一些先天约束那些面向通用的、普适的RPC协议如CORBA就无法逃过使用复杂性的困扰而那些意图通过技术手段来屏蔽复杂性的RPC协议如Web Service又不免受到性能问题的束缚。
简单、普适和高性能,似乎真的难以同时满足。
分裂的RPC
由于一直没有一个能同时满足以上简单、普适和高性能的“完美RPC协议”因此远程服务器调用这个小小的领域就逐渐进入了群雄混战、百家争鸣的“战国时代”距离“统一”越来越远并一直延续至今。
我们看看相继出现过的RPC协议/框架就能明白了RMISun/Oracle、ThriftFacebook/Apache、Dubbo阿里巴巴/Apache、gRPCGoogle、Motan2新浪、FinagleTwitter、brpc百度、.NET Remoting微软、ArvoHadoop、JSON-RPC 2.0公开规范JSON-RPC工作组……
这些RPC的功能、特点都不太一样有的是某种语言私有有的能支持跨越多门语言有的运行在HTTP协议之上有的能直接运行于TCP/UDP之上但没有哪一款是“最完美的RPC”。据此我们也可以发现一个规律任何一款具有生命力的RPC框架都不再去追求大而全的“完美”而是会找到一个独特的点作为主要的发展方向。
我们看几个典型的发展方向:
朝着面向对象发展。这条线的缘由在于在分布式系统中开发者们不再满足于RPC带来的面向过程的编码方式而是希望能够进行跨进程的面向对象编程。因此这条线还有一个别名叫作分布式对象Distributed Object它的代表有RMI、.NET Remoting。当然了之前的CORBA和DCOM也可以归入这一类。
朝着性能发展代表为gRPC和Thrift。决定RPC性能主要就两个因素序列化效率和信息密度。序列化效率很好理解序列化输出结果的容量越小速度越快效率自然越高信息密度则取决于协议中有效荷载Payload所占总传输数据的比例大小使用传输协议的层次越高信息密度就越低SOAP使用XML拙劣的性能表现就是前车之鉴。gRPC和Thrift都有自己优秀的专有序列化器而在传输协议方面gRPC是基于HTTP/2的支持多路复用和Header压缩Thrift则直接基于传输层的TCP协议来实现省去了额外的应用层协议的开销。
朝着简化发展代表为JSON-RPC。要是说选出功能最强、速度最快的RPC可能会有争议但要选出哪个功能弱的、速度慢的JSON-RPC肯定会是候选人之一。它牺牲了功能和效率换来的是协议的简单。也就是说JSON-RPC的接口与格式的通用性很好尤其适合用在Web浏览器这类一般不会有额外协议、客户端支持的应用场合。
……
经历了RPC框架的“战国时代”开发者们终于认可了不同的RPC框架所提供的不同特性或多或少是互相矛盾的很难有某一种框架说“我全部都要”。
要把面向对象那套全搬过来就注定不会太简单比如建Stub、Skeleton就很烦了即使由IDL生成也很麻烦功能多起来协议就要弄得复杂效率一般就会受影响要简单易用那很多事情就必须遵循约定而不是配置才行要重视效率那就需要采用二进制的序列化器和较底层的传输协议支持的语言范围容易受限。
也正是因为每一种RPC框架都有不完美的地方才会有新的RPC轮子不断出现。
而到了最近几年RPC框架有明显朝着更高层次不仅仅负责调用远程服务还管理远程服务与插件化方向发展的趋势不再选择自己去解决表示数据、传递数据和表示方法这三个问题而是将全部或者一部分问题设计为扩展点实现核心能力的可配置再辅以外围功能如负载均衡、服务注册、可观察性等方面的支持。这一类框架的代表有Facebook的Thrift和阿里的Dubbo现在两者都是Apache的
尤其是断更多年后重启的Dubbo表现得更为明显它默认有自己的传输协议Dubbo协议同时也支持其他协议它默认采用Hessian 2作为序列化器如果你有JSON的需求可以替换为Fastjson如果你对性能有更高的需求可以替换为Kryo、FST、Protocol Buffers等如果你不想依赖其他包直接使用JDK自带的序列化器也可以。这种设计就在一定程度上缓解了RPC框架必须取舍难以完美的缺憾。
小结
今天我们一起学习了RPC协议在工业界的发展包括它要解决的三个基本问题以及层出不穷的RPC协议/框架。
表示数据、传递数据和表示方法是RPC必须解决的三大基本问题。要解决这些问题可以有很多方案这也是RPC协议/框架出现群雄混战局面的一个原因。
出现这种分裂局面的另一个原因,是简单的框架很难能达到功能强大的要求。
功能强大的框架往往要在传输中加入额外的负载和控制措施,导致传输性能降低,而如果既想要高性能,又想要强功能,这就必然要依赖大量的技巧去实现,进而也就导致了框架会变得过于复杂,这就决定了不可能有一个“完美”的框架同时满足简单、普适和高性能这三个要求。
认识到这一点后一个RPC框架要想取得成功就要选择一个发展方向能够非常好地满足某一方面的需求。因此我们也就有了朝着面向对象发展、朝着性能发展和朝着简化发展这三条线。
以上就是这一讲我要和你分享的RPC在工业界的发展成果了。这也是你在日后工作中选择RPC实现方案的一个参考。
最后我再和你分享一点我的心得。我在讲到DCOM、CORBA、Web Service的失败的时候虽然说我的口吻多少有一些戏谑但我们得明确一点这些框架即使没有成功但作为早期的探索先驱并没有什么应该被讽刺的地方。而且其后续的发展都称得上是知耻后勇反而值得我们的掌声赞赏。
比如说到CORBA的消亡OMG痛定思痛之后提出了基于RTPS协议栈的“数据分发服务”商业标准Data Distribution ServiceDDS“商业”就是要付费使用的意思。这个标准现在主要用在物联网领域能够做到微秒级延时还能支持大规模并发通讯。
再比如说到DCOM的失败和Web Service的衰落微软在它们的基础上推出了.NET WCFWindows Communication FoundationWindows通信基础
.NET WCF的优势主要有两点一是把REST、TCP、SOAP等不同形式的调用自动封装为了完全一致的、如同本地方法调用一般的程序接口二是依靠自家的“地表最强IDE”Visual Studio把工作量减少到只需要指定一个远程服务地址就可以获取服务描述、绑定各种特性如安全传输、自动生成客户端调用代码甚至还能选择同步还是异步之类细节的程度。
虽然.NET WCF只支持.NET平台而且也是采用XML语言描述但使用体验真的是非常畅快足够挽回Web Service得罪开发者丢掉的全部印象分。
一课一思
我们通过两讲学习了RPC在学术界和工业界的发展后再回过头来思考一个问题开发一个分布式系统是不是就一定要用RPC呢
我提供给你一个分析思路吧。RPC的三大问题源自对本地方法调用的类比模拟如果我们把思维从“方法调用”的约束中挣脱那参数与结果如何表示、方法如何表示、数据如何传递这些问题都会海阔天空拥有焕然一新的视角。但是我们写程序真的可能不面向方法来编程吗
这就是我在下一讲准备跟你探讨的话题了。现在你可以先自己思考一下,欢迎在留言区分享你的看法。另外,如果觉得有收获,也非常欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,226 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 _ RESTful服务从面向过程编程到面向资源编程
你好我是周志明。前面两节课我们学习了远程方法调用RPC今天我们接着学习另一种主流的远程服务访问风格RESTful服务。
REST与RPC的对比
很多人都会拿REST来跟RPC对比优劣其实无论是思想上、概念上还是使用范围上REST与RPC都不完全一样它们在本质上并不是同一个类型的东西充其量只算是有一些相似在应用中会有一部分功能重合的地方。
REST与RPC在思想上存在差异的核心是抽象的目标不一样也就是面向资源的编程思想与面向过程的编程思想之间的区别。
面向过程编程和面向对象编程想必你应该都听说过但什么是面向资源编程呢这个问题等我一会儿介绍完REST的特征之后再回头细说。
那么二者在概念上的不同是指REST并不是一种远程服务调用协议甚至我们可以把定语也去掉它就不是一种协议。
因为协议都带有一定的规范性和强制性最起码也该有个规约文档比如JSON-RPC它哪怕再简单也要有个《JSON-RPC Specification》来规定协议的格式细节、异常、响应码等信息。但是REST并没有定义这些内容虽然它有一些指导原则但实际上并不受任何强制的约束。
经常会有人批评说某个系统接口“设计得不够RESTful”其实这句话本身就有些争议。因为REST只能说是一种风格而不是规范、协议并且能完全达到REST所有指导原则的系统也是很少见的。这个问题我们会在下一讲中详细讨论。
至于使用范围上REST与RPC作为主流的两种远程调用方式在使用上确实有重合之处但重合的区域有多大就见仁见智了。
上一节课我提到了当前的RPC协议框架各有侧重点并且列举了RPC的一些典型发展方向比如分布式对象、提升调用效率、简化调用复杂性等等。
其中的分布式对象这一条线的应用对于REST就可以说是毫无关系而能够重视远程服务调用效率的应用场景就基本上已经排除了REST应用得最多的供浏览器端消费的远程服务。因为以浏览器作为前端对于传输协议、序列化器这两点都没有什么选择权哪怕想要更高效率也有心无力。
而在移动端、桌面端或者分布式服务端的节点之间通讯这一块REST虽然照样有宽阔的用武之地只要支持HTTP就可以用于任何语言之间的交互不过使用REST的前提是网络没有成为性能上的瓶颈。但是在需要追求传输效率的场景里REST提升传输效率的潜力有限死磕REST又想要好的网络性能一般不会有好的效果。
另外对于追求简化调用的场景我在前面提到的浏览器端就属于这一类的典型在众多RPC里也就JSON-RPC有机会与REST竞争其他RPC协议与框架哪怕是能够支持HTTP协议哪怕提供了JavaScript版本的客户端如gRPC-Web也只是具备前端使用的理论可行性很少能看到有实际项目把它们真的用到浏览器上的。
可是尽管有着种种不同REST跟RPC还是产生了很频繁的比较与争论这两种分别面向资源和面向过程的远程调用方式就像当年面向对象与面向过程的编程思想一样非得分出个高低不可。
理解REST
REST概念的提出来自于罗伊·菲尔丁Roy Fielding在2000年发表的博士论文《Architectural Styles and the Design of Network-based Software Architectures》架构风格与网络的软件架构设计。这篇文章的确是REST的源头但我们也不能忽略Fielding的身份和他之前的工作背景这对理解REST的设计思想也是非常重要的。
首先Fielding是一名很优秀的软件工程师他是Apache服务器的核心开发者后来成为了著名的Apache软件基金会的联合创始人同时Fielding也是HTTP 1.0协议1996年发布的专家组成员后来还成为了HTTP 1.1协议1999年发布的负责人。
HTTP 1.1协议设计得非常成功以至于在发布后长达十年的时间里都没有多少人认为有修订的必要。而用来指导设计HTTP 1.1协议的理论和思想最初是以备忘录的形式在专家组成员之间交流这个备忘录其实就是REST的雏形。
那么从时间上看当起草完HTTP 1.1协议之后Fielding就回到了加州大学欧文分校继续攻读博士学位。然后到了第二年也就是2000年Fielding更为系统、严谨地阐述了这套理论框架并且以这套理论框架为基础导出了一种新的编程风格他把这种风格命名为了我们今天所熟知的REST即“表征状态转移”Representational State Transfer的缩写。
不过哪怕是对编程和网络都很熟悉的同学单从“表征状态转移”这个标题上看也不太可能直接弄明白什么叫“表征”、啥东西的“状态”、从哪“转移”到哪。虽然在论文当中Fielding有论述过这些概念但他写得确实非常晦涩不想读英文的话你可以参考一下中文翻译版本
这里呢我推荐你一种比较容易理解REST思想的方式就是你先去理解什么是HTTP再配合一些实际例子来进行类比你就会发现“REST”实际上是“HTT”Hyper Text Transfer超文本传输的进一步抽象它们就像是接口与实现类之间的关系。
HTTP中使用的“超文本”一词是美国社会学家泰德·H·尼尔森Theodor Holm Nelson在1967年于《Brief Words on the Hypertext》一文里提出的这里引用的是他本人在1992年修正后的定义
Hypertext-
By now the word “hypertext” has become generally accepted for branching and responding text, but the corresponding word “hypermedia”, meaning complexes of branching and responding graphics, movies and sound as well as text is much less used. Instead they use the strange term “interactive multimedia”: this is four syllables longer, and does not express the idea of extending hypertext.-
—— Theodor Holm Nelson Literary Machines, 1992
可以看到“超文本或超媒体”指的是一种“能够对操作进行判断和响应的文本或声音、图像等这个概念在上个世纪60年代提出的时候应该还属于科幻的范畴但是到了今天我们已经完全接受了它互联网中的一段文字可以点击、可以触发脚本执行、可以调用服务端都已经非常平常毫不稀奇了。
所以接下来我们就尝试着从理解“超文本”的含义开始根据一个具体的阅读文章的例子来理解一下什么是“表征”以及REST中的其他关键概念。
资源Resource
假设你现在正在阅读一篇名为《REST设计风格》的文章这篇文章的内容本身可以将其视作是某种信息、数据我们称之为“资源”。无论你是在网上看的网页还是打印出来看的文字稿或者是在电脑屏幕上阅读、手机上浏览尽管它呈现出来的样子都不一样但其中的信息是不变的你阅读的仍是同一个“资源”。
表征Representation
当你通过电脑浏览器阅读这篇文章的时候浏览器会向服务端发出请求“我需要这个资源的HTML格式”那么服务端向浏览器返回的这个HTML就被称之为“表征”你通过其他方式拿到了文章的PDF、Markdown、RSS等其他形式的版本它们也同样是一个资源的多种表征。可见“表征”这个概念是指信息与用户交互时的表示形式这跟应用分层中我们常说的“表示层”Presentation Layer的语义其实是一致的。
状态State
当你读完了这篇文章,想再接着看看下一篇文章的内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”,这样服务器才能正确回应,那么这类在特定语境中才能产生的上下文信息就被称为“状态”。
这里我们要注意有状态Stateful还是无状态Stateless都是只相对于服务端来说的服务器要完成“取下一篇”的请求要么是自己记住用户的状态这个用户现在阅读的是哪一篇文章这是有状态要么是客户端来记住状态在请求的时候明确告诉服务器我正在阅读某某文章现在要读下一篇这是无状态
转移Transfer
要知道,无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。
通过这个“阅读文章”的例子对资源等概念进行通俗的解释现在你应该就能理解REST所说的“表征状态转移”的含义了。
那么借着这个例子的上下文我再给你介绍几个现在不涉及但在后面解读REST的6大核心特征时要用到的概念名词
第一个统一接口Uniform Interface
在了解这个概念之前,我们先来思考一个问题,前面所说的“服务器通过某种方式”,让表征状态发生转移,具体指的是什么方式呢?
如果你现在正在使用Web端来学习这一讲的内容你可以看到页面的左半部分有下一讲或者是下面几讲的URI超链接地址这是服务端在渲染这讲内容时就预置好的点击它让页面跳转到下一讲就是所谓“某种方式”的其中一种方式不过若下一讲还未更新出来时你只能看到之前的课程内容道理其实也差不多
现在我们其实并不会对点击超链接网页出现跳转而感到奇怪但你再细想一下URI的含义是统一资源标识符是一个名词那它如何能表达出“转移”这个动作的含义呢
答案是HTTP协议中已经提前约定好了一套“统一接口”它包括GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七种基本操作任何一个支持HTTP协议的服务器都会遵守这套规定对特定的URI采取这些操作服务器就会触发相应的表征状态转移。
第二个超文本驱动Hypertext Driven
尽管表征状态转移是由浏览器主动向服务器发出请求所引发的该请求导致了“在浏览器屏幕上显示出了下一篇文章的内容”这个结果的出现。但是你我都清楚这不可能真的是浏览器的主动意图浏览器是根据用户输入的URI地址来找到服务器给予的首页超文本内容通过超文本内部的链接导航到了这篇文章阅读结束时也是通过超文本内部的链接再导航到下一篇。
浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。
第三个自描述消息Self-Descriptive Messages
前面我们知道了资源的表征可能存在多种不同形态因此在传输给浏览器的消息中应当有明确的信息来告知客户端该消息的类型以及该如何处理这条消息。一种被广泛采用的自描述方法是在名为“Content-Type”的HTTP Header中标识出互联网媒体类型MIME type比如“Content-Type : application/json; charset=utf-8”就说明了该资源会以JSON的格式返回请使用UTF-8字符集进行处理。
除了以上列出的这些看名字不容易弄懂的概念外在理解REST的时候你还要注意一个常见的误区。Fielding在提出REST时所谈论的范围是“架构风格与网络的软件架构设计”Architectural Styles and Design of Network-based Software Architectures而不是现在被人们所狭义理解的一种“远程服务设计风格”。
这两者的范围差别,就好比这门课程所谈论的话题“软件架构”与这个小章节所谈论的话题“访问远程服务”的关系那样,前者是后者的一个很大的超集。尽管考虑到这节课的主题和多数人的关注点,我们确实是会以“远程服务设计风格”作为讨论的重点,但至少我们要知道它们在范围上的差别。
RESTful风格的系统特征
OK理解了前面解读的这些概念以后现在我们就可以开始讨论面向资源的编程思想以及Fielding所提出的具体的软件架构设计特征了。Fielding认为一套理想的、完全满足REST的系统应该满足以下六个特征。
服务端与客户端分离Client-Server
现在,有越来越多的开发者认可,分离开用户界面和数据存储所关注的逻辑,有助于提高用户界面跨平台的可移植性。
以前完全基于服务端控制和渲染如JSF这类框架的实际用户现在已经很少见了。另外在服务端进行界面控制Controller通过服务端或者客户端的模版渲染引擎来进行界面渲染的框架如Struts、SpringMVC这类也受到了颇大的冲击。而推动这个局面发展的主要原因实际上跟REST的关系并不大随着前端技术从ES规范到语言实现到前端框架等近年来的高速发展前端表达能力的大幅度加强才是真正的幕后推手。
此外由于前端的日渐强势现在还流行起由前端代码反过来驱动服务端进行渲染的SSRServer-Side Rendering技术在Serverless、SEO等场景中已经占领了一块领地。
无状态Stateless
这是REST的一条关键原则部分开发者在做服务接口规划时觉得RESTful风格的API怎么设计都别扭一个很可能的原因就是服务端持有着比较重的状态。
REST希望服务器能不负责维护状态每一次从客户端发送的请求中应该包括所有必要的上下文信息会话信息也由客户端保存维护服务器端依据客户端传递的状态信息来进行业务处理并且驱动整个应用的状态变迁。
至于客户端承担状态维护职责后的认证、授权等各方面的可信问题,都会有针对性的解决方案(这部分内容,我会在后面讲解安全架构时展开介绍)。
但必须承认的现状是,目前大多数的系统是达不到这个要求的,越复杂、越大型的系统越是如此。服务端无状态可以在分布式环境中获得很高价值的好处,但大型系统的上下文状态数量,完全可能膨胀到,客户端在每次发送请求时,根本无法全部囊括系统里所有必要的上下文信息。在服务端的内存、会话、数据库或者缓存等地方,持有一定的状态是一种现实情况,而且会是长期存在、被广泛使用的主流方案。
可缓存Cacheability
前面我们提到的无状态服务,虽然提升了系统的可见性、可靠性和可伸缩性,但也降低了系统的网络性。这句话通俗的解释就是,某个功能使用有状态的架构只需要一次请求就能完成,而无状态的服务则可能会需要多个请求,或者在请求中带有冗余的信息。
所以为了缓解这个矛盾REST希望软件系统能够像万维网一样客户端和中间的通讯传递者代理可以将部分服务端的应答缓存起来。当然应答中必须明确或者间接地表明本身是否可以进行缓存以避免客户端在将来进行请求的时候得到过时的数据。
运作良好的缓存机制可以减少客户端、服务器之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。
分层系统Layered System
这里所指的并不是表示层、服务层、持久层这种意义上的分层,而是指客户端一般不需要知道是否直接连接到了最终的服务器,或者是连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制,提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。
统一接口Uniform Interface
REST希望开发者面向资源编程希望软件系统设计的重点放在抽象系统该有哪些资源上而不是抽象系统该有哪些行为服务上。
这个特征你可以类比计算机中对文件管理的操作。我们知道管理文件可能会进行创建、修改、删除、移动等操作这些操作数量是可数的而且对所有文件都是固定的、统一的。如果面向资源来设计系统同样会具有类似的操作特征由于REST并没有设计新的协议所以这些操作都借用了HTTP协议中固有的操作命令来完成。
统一接口也是REST最容易陷入争论的地方基于网络的软件系统到底是面向资源更好还是面向服务更合适这件事情在很长的时间里恐怕都不会有个定论也许永远都没有。但是有一个已经基本清晰的结论是面向资源编程的抽象程度通常更高。
抽象程度高有好处但也有坏处。坏处是往往距离人类的思维方式更远,而好处是往往通用程度会更好。
不过这样来诠释REST大概本身就挺抽象的你可能不太好理解我还是举个例子来说明。
几乎每个系统都有登录和注销功能如果你理解成登录对应于login()、注销对应于logout()这样两个独立服务这是“符合人类思维”的如果你理解成登录是PUT Session注销是DELETE Session这样你只需要设计一种“Session资源”即可满足需求甚至以后对Session的其他需求如查询登录用户的信息就是GET Session而已其他操作如修改用户信息等等都可以被这同一套设计囊括在内这便是“抽象程度更高”带来的好处。
而如果你想要在架构设计中合理恰当地利用统一接口Fielding给出了三个建议第一系统要能做到每次请求中都包含资源的ID所有操作均通过资源ID来进行第二每个资源都应该是自描述的消息第三通过超文本来驱动应用状态的转移。
按需代码Code-On-Demand
按需代码被Fielding列为了一条可选原则原因其实并非是它特别难以达到更多是出于必要性和性价比的实际考虑。按需代码是指任何按照客户端如浏览器的请求将可执行的软件程序从服务器发送到客户端的技术。它赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。
举个具体例子以前的Java Applet技术、今天的WebAssembly等都属于典型的按需代码蕴含着具体执行逻辑的代码存放在了服务端只有当客户端请求了某个Java Applet之后代码才会被传输并在客户端机器中运行结束后通常也会随即在客户端中被销毁掉。
到这里REST中的主要概念与思想原则就介绍完了那么现在我们再回过头来讨论一下这节课开篇中提出的REST与RPC在思想上的差异。
REST与RPC在思想上的差异
我在前面提到REST的基本思想是面向资源来抽象问题它与此前流行的面向过程的编程思想在抽象主体上有本质的差别。
在REST提出以前人们设计分布式系统服务的唯一方案就只有RPCRPC是将本地的方法调用思路迁移到远程方法调用上开发者是围绕着“远程方法”去设计两个系统间的交互的比如CORBA、RMI、DCOM等等。
这样做的坏处不仅是“如何在异构系统间表示一个方法”“如何获得接口能够提供的方法清单”都成了需要专门协议去解决的问题RPC的三大基本问题之一更在于服务的每个方法都是不同的服务使用者必须逐个学习才能正确地使用它们。Google在《Google API Design Guide》中曾经写下这样一段话
Traditionally, people design RPC APIs in terms of API interfaces and methods, such as CORBA and Windows COM. As time goes by, more and more interfaces and methods are introduced. The end result can be an overwhelming number of interfaces and methods, each of them different from the others. Developers have to learn each one carefully in order to use it correctly, which can be both time consuming and error prone.-
以前人们面向方法去设计RPC API比如CORBA和DCOM随着时间推移接口与方法越来越多却又各不相同开发人员必须了解每一个方法才能正确使用它们这样既耗时又容易出错。-
—— Google API Design Guide, 2017
而REST提出以资源为主体进行服务设计的风格就为它带来了不少好处。我举几个典型例子。
第一,降低了服务接口的学习成本。
统一接口是REST的重要标志它把对资源的标准操作都映射到了标准的HTTP方法上去这些方法对每个资源的语义都是一致的我们不需要刻意学习更不会有什么Interface Description Language之类的协议存在。
第二,资源天然具有集合与层次结构。
以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。
我举个例子。你可以想像一个商城用户中心的接口设计:用户资源会拥有多个不同的下级的资源,比如若干条短消息资源、一份用户资料资源、一部购物车资源,而购物车中又会有自己的下级资源,比如多本书籍资源。
这样你就很容易在程序接口中构造出这些资源的集合关系与层次关系而且能符合人们长期在单机或网络环境中管理数据的直觉。我相信你并不需要专门去阅读接口说明书也能轻易推断出获取用户icyfenix的购物车中第2本书的REST接口应该表示为
GET /users/icyfenix/cart/2
第三REST绑定于HTTP协议。
面向资源编程并不是必须构筑在HTTP之上但对于REST来说这是优点也是缺点。
因为HTTP本来就是面向资源而设计的网络协议纯粹只用HTTP而不是SOAP over HTTP那样在再构筑协议带来的好处是不需要再去考虑RPC中的Wire Protocol问题了REST可以复用HTTP协议中已经定义的语义和相关基础支持来解决。HTTP协议已经有效运作了30年与其相关的技术基础设施已是千锤百炼无比成熟。而它的坏处自然就是当你想去考虑那些HTTP不提供的特性时就束手无策了。
小结
在这节课中虽然我列举了一些面向资源的优点但我并非要证明它比面向过程、面向对象更优秀。是否选用REST的API设计风格需要权衡的是你的需求场景、你团队的设计以及开发人员是否能够适应面向资源的思想来设计软件、来编写代码。
在互联网中面向资源来进行网络传输是这三十年来HTTP协议精心培养出来的用户习惯如果开发者能够适应REST不太符合人类思维习惯的抽象方式那REST通常能够更好地匹配在HTTP基础上构建的互联网在效率与扩展性方面也会有可观的收益。
一课一思
与远端服务通讯除了以资源为中心的REST和以方法为中心的RPC外还有基于长连接、消息管道等其他方式想一想你还知道哪些通讯手段它们能解决什么问题有什么应用场景
欢迎给我留言,分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,320 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 _ RESTful服务如何评价服务是否RESTful
你好,我是周志明。
上一节课我们一起学习了REST的思想、概念和指导原则等今天我们把重心放在REST的实践上把目光聚焦到具体如何设计REST服务接口上。这样我们也就能回答上节课提出的问题“如何评价服务是否RESTful”了。
Richardson成熟度模型
“RESTful Web APIs”和“RESTful Web Services”的作者伦纳德 · 理查德森Leonard Richardson曾提出过一个衡量“服务有多么REST”的Richardson成熟度模型Richardson Maturity ModelRMM。这个模型的一个用处是方便那些原本不使用REST的服务能够逐步导入REST。
Richardson将服务接口按照“REST的程度”从低到高分为0至3共4级
The Swamp of Plain Old XML完全不REST。另外关于POX这个说法SOAP表示感觉有被冒犯到。
Resources开始引入资源的概念。
HTTP Verbs引入统一接口映射到HTTP协议的方法上。
Hypermedia Controls在咱们课程里面的说法是“超文本驱动”在Fielding论文里的说法是Hypertext as the Engine of Application StateHATEOAS都说的是同一件事情。
REST成熟度模型
接下来,我们通过马丁 · 福勒Martin Fowler的关于RMM的文章中的实际例子原文是XML写的我简化了一下来看看四种不同程度的REST反应到实际API是怎样的。
假设,你是一名软件工程师,接到需求(也被我尽量简化了)的用户故事是这样的:现在要开发一个医生预约系统,病人通过这个系统,可以知道自己熟悉的医生在指定日期是否有空闲时间,以方便预约就诊。
第0级成熟度The Swamp of Plain Old XML
医院开放了一个/appointmentService的Web API传入日期、医生姓名作为参数就可以得到该时间段、该医生的空闲时间。
这个API的一次HTTP调用如下所示
POST /appointmentService?action=query HTTP/1.1
{date: "2020-03-04", doctor: "mjones"}
在接收到请求之后,服务器会传回一个包含所需信息的结果:
HTTP/1.1 200 OK
[
{start:"14:00", end: "14:50", doctor: "mjones"},
{start:"16:00", end: "16:50", doctor: "mjones"}
]
得到了医生空闲的结果后我觉得14:00的时间比较合适于是预约确认并提交了我的基本信息
POST /appointmentService?action=comfirm HTTP/1.1
{
appointment: {date: "2020-03-04", start:"14:00", doctor: "mjones"},
patient: {name: xx, age: 30, ……}
}
如果预约成功,那我能够收到一个预约成功的响应:
HTTP/1.1 200 OK
{
code: 0,
message: "Successful confirmation of appointment"
}
如果发生了问题,比如有人在我前面抢先预约了,那么我会在响应中收到某种错误信息:
HTTP/1.1 200 OK
{
code: 1
message: "doctor not available"
}
到此,整个预约服务就完成了,可以说是直接明了。
在这个方案里我们采用的是非常直观的基于RPC风格的服务设计看似是很轻松地解决了所有问题但真的是这样吗
第1级成熟度Resources
实际上你可以发现第0级是RPC的风格所以如果需求永远不会变化也不会增加那它完全可以良好地工作下去。但是如果你不想为预约医生之外的其他操作、为获取空闲时间之外的其他信息去编写额外的方法或者改动现有方法的接口那就应该考虑一下如何使用REST来抽象资源。
通往REST的第一步是引入资源的概念在API中最基本的体现就是它会围绕着资源而不是过程来设计服务。说得直白一点你可以理解为服务的Endpoint应该是一个名词而不是动词。此外每次请求中都应包含资源ID所有操作均通过资源ID来进行。
POST /doctors/mjones HTTP/1.1
{date: "2020-03-04"}
然后服务器传回一个包含了ID的信息。注意ID是资源的唯一编号有ID即代表“医生的档期”被视为一种资源
HTTP/1.1 200 OK
[
{id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},
{id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}
]
我还是觉得14:00的时间比较合适于是又预约确认并提交了我的基本信息
POST /schedules/1234 HTTP/1.1
{name: xx, age: 30, ……}
后面预约成功或者失败的响应消息在这个级别里面与之前一致,就不重复了。
比起第0级第1级的服务抽象程度有所提高但至少还有三个问题并没有解决
一是,只处理了查询和预约,如果我临时想换个时间要调整预约,或者我的病忽然好了想删除预约,这都需要提供新的服务接口。
二是处理结果响应时只能靠着结果中的code、message这些字段做分支判断每一套服务都要设计可能发生错误的code。而这很难考虑全面而且也不利于对某些通用的错误做统一处理。
三是并没有考虑认证授权等安全方面的内容。比如要求只有登录过的用户才允许查询医生的档期再比如某些医生可能只对VIP开放需要特定级别的病人才能预约等等。
这三个问题其实都可以通过引入统一接口Uniform Interface来解决。接下来我们就来到了第2级。
第2级成熟度HTTP Verbs
前面说到第1级中遗留的这三个问题都可以靠引入统一接口来解决而HTTP协议的标准方法便是最常接触到的统一接口。
HTTP协议的标准方法是经过精心设计的它几乎涵盖了资源可能遇到的所有操作场景这其实更取决于架构师的抽象能力
那么REST的做法是
针对预约变更的问题,把不同业务需求抽象为对资源的增加、修改、删除等操作来解决;
针对响应代码的问题使用HTTP协议的Status Code可以涵盖大多数资源操作可能出现的异常而且也是可以自定义扩展的
针对安全性的问题依靠HTTP Header中携带的额外认证、授权信息来解决这个在实战中并没有体现你可以去看看后面第23~28讲中关于安全架构的相关内容
按这个思路我们在获取医生档期时应该使用具有查询语义的GET操作来完成
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
然后,服务器会传回一个包含了所需信息的结果:
HTTP/1.1 200 OK
[
{id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},
{id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}
]
我还是觉得14:00的时间比较合适于是就预约确认并提交了我的基本信息用来创建预约。这是符合POST的语义的
POST /schedules/1234 HTTP/1.1
{name: xx, age: 30, ……}
如果预约成功,那我能够收到一个预约成功的响应:
HTTP/1.1 201 Created
Successful confirmation of appointment
否则,我会在响应中收到某种错误信息:
HTTP/1.1 409 Conflict
doctor not available
目前绝大多数的系统能够达到的REST级别也就是第2级了。不过这种方案还不够完美最主要的一个问题是我们如何知道预约mjones医生的档期需要访问“/schedules/1234”这个服务Endpoint
第3级成熟度Hypermedia Controls
或许你第一眼看到这个问题会说这当然是程序写的啊我为什么会问这么奇怪的问题。但问题是REST并不认同这种已烙在程序员脑海中许久的想法。
RMM中的第3级成熟度Hypermedia Controls、Fielding论文中的HATEOAS和现在提得比较多的超文本驱动其实都是希望能达到这样一种效果除了第一个请求是由你在浏览器地址栏输入的信息所驱动的之外其他的请求都应该能够自己描述清楚后续可能发生的状态转移由超文本自身来驱动。
所以,当你输入了查询命令后:
GET /doctors/mjones/schedule?date=2020-03-04&statu s=open HTTP/1.1
服务器传回的响应信息应该包括如何预约档期、如何了解医生信息等可能的后续操作:
HTTP/1.1 200 OK
{
schedules[
{
id: 1234, start:"14:00", end: "14:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/1234"}
]
},
{
id: 5678, start:"16:00", end: "16:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/5678"}
]
}
],
links: [
{rel: "doctor info", href: "/doctors/mjones/info"}
]
}
如果做到了第3级REST那么服务端的API和客户端就可以做到完全解耦了。这样一来你再想要调整服务数量或者同一个服务做API升级将会变得非常简单。
至此我们已经学完了REST的相关知识了解了REST的一些优点然而凡事总有两面下面我们来看一看REST经常收到非议的方面。
REST的不足与争议
第一个有争议的问题是面向资源的编程思想只适合做CRUD只有面向过程、面向对象编程才能处理真正复杂的业务逻辑。
这是我们在实践REST时遇到的最多的一个问题。有这个争议的原因也很简单HTTP的4个最基础的命令POST、GET、PUT和DELETE很容易让人联想到CRUD操作因此在脑海中就自然产生了直接的对应。
REST涵盖的范围当然远不止于此。不过要说POST、GET、PUT和DELETE对应于CRUD其实也没什么不对只是我们必须泛化地去理解这个CRUD它们涵盖了信息在客户端与服务端之间流动的几种主要方式比如POST、GET、PUT等标准方法所有基于网络的操作逻辑都可以通过解决“信息在服务端与客户端之间如何流动”这个问题来理解有的场景里比较直观而另一些场景中可能比较抽象。
针对那些比较抽象的场景如果确实不好把HTTP方法映射为资源的所需操作REST也并不会刻板地要求一定要做映射。这时用户可以使用自定义方法按Google推荐的REST API风格来拓展HTTP标准方法。
自定义方法应该放在资源路径末尾嵌入冒号加自定义动词的后缀。比如我将删除操作映射到标准DELETE方法上此外还要提供一个恢复删除的API那它可能会被设计为
POST /user/user_id/cart/book_id:undelete
要实现恢复删除一个完全可行的设计是设计一个回收站的资源在那里保留还能被恢复的商品我们把恢复删除看作是对这个资源的某个状态值的修改映射到PUT或者PATCH方法上。
最后,我要再重复一遍,面向资源的编程思想与另外两种主流编程(面向过程和面向对象编程)思想,只是抽象问题时所处的立场不同,只有选择问题,没有高下之分:
面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为了符合计算机世界中主流的交互方式。
面向对象编程时,为什么要将数据和行为统一起来、封装成对象?当然是为了符合现实世界的主流交互方式。
面向资源编程时,为什么要将数据(资源)作为抽象的主体,把行为看作是统一的接口?当然是为了符合网络世界的主流的交互方式。
第二个有争议的问题是REST与HTTP完全绑定不适用于要求高性能传输的场景中。
其实我在很大程度上赞同这个观点但我并不认为这是REST的缺陷因为锤子不能当扳手用并不是锤子的质量有问题。
面向资源编程与协议无关但是REST特指Fielding论文中所定义的REST而不是泛指面向资源的思想的确依赖着HTTP协议的标准方法、状态码和协议头等各个方面。
我们也知道HTTP是应用层协议而不是传输层协议如果我们只是把HTTP用作传输是不恰当的SOAP再次感觉有被冒犯到。因此对于需要直接控制传输如二进制细节/编码形式/报文格式/连接方式等细节的场景REST确实不合适。这些场景往往存在于服务集群的内部节点之间这也是我在上一讲提到的虽然REST和RPC的应用场景的确有所重合但重合的范围有多大就是见仁见智的事情了。
第三个有争议的问题是REST不利于事务支持。
其实这个问题首先要看我们怎么去理解“事务Transaction”这个概念了。
如果“事务”指的是数据库那种狭义的刚性ACID事务那分布式系统本身跟它之间就是有矛盾的CAP不可兼得。这是分布式的问题而不是REST的问题。
如果“事务”是指通过服务协议或架构在分布式服务中获得对多个数据同时提交的统一协调能力2PC/3PC比如WS-AtomicTransaction和WS-Coordination这样的功能性协议那REST确实不支持。假如你已经理解了这样做的代价仍决定要这样做的话Web Service是比较好的选择。
如果“事务”是指希望保证数据的最终一致性说明你已经放弃刚性事务了。这才是分布式系统中的主流使用REST肯定不会有什么阻碍更谈不上“不利于”事务支持当然对于最终一致性的问题REST本身并没有提供什么帮助而是完全取决于你系统的事务设计。我们在讲解事务处理的课程章节中会再详细讨论
第四个有争议的问题是REST没有传输可靠性支持。
是的REST并没有提供对传输可靠性的支持。在HTTP中你发送出去一个请求通常会收到一个与之相对的响应比如HTTP/1.1 200 OK或者HTTP/1.1 404 Not Found等。但是如果你没有收到任何响应那就无法确定消息到底是没有发送出去还是没有从服务端返回回来。这其中的关键差别是服务端到底是否被触发了某些处理
应对传输可靠性最简单粗暴的做法就是把消息再重发一遍。这种简单处理能够成立的前提是服务具有幂等性Idempotency也就是说服务被重复执行多次的效果与执行一次是相等的。
HTTP协议要求GET、PUT和DELETE操作应该具有幂等性我们把REST服务映射到这些方法时也应该保证幂等性。
对于POST方法曾经有过一些专门的提案比如POE、POST Once Exactly但并未得到IETF的通过。对于POST的重复提交浏览器会出现相应警告比如Chrome中会有“确认重新提交表单”的提示。而服务端就应该做预校验如果发现可能重复就返回HTTP/1.1 425 Too Early。
另外Web Service中有WS-ReliableMessaging功能协议用来支持消息可靠投递。类似的REST因为没有采用额外的Wire Protocol所以除了缺少对事务、可靠传输的支持外一定还可以在WS-*协议中找到很多REST不支持的特性。
第五个有争议的问题是REST缺乏对资源进行“部分”和“批量”的处理能力。
这个观点我是认同的而且我认为这很可能是未来面向资源的思想和API设计风格的发展方向。
REST开创了面向资源的服务风格却肯定不完美。以HTTP协议为基础虽然给REST带来了极大的便捷不需要额外协议不需要重复解决一堆基础网络问题等等但也成了束缚REST的无形牢笼。
关于HTTP协议对REST的束缚我会通过具体的例子和你解释。
第一种束缚就是缺少对资源的“部分”操作的支持。有些时候我们只是想获得某个用户的姓名RPC风格中可以设计一个“getUsernameById”的服务返回一个字符串。尽管这种服务的通用性实在称不上“设计”二字但确实可以工作。而要是采用REST风格的话你需要向服务端请求整个用户对象然后丢弃掉返回结果中的其他属性这就是一种请求冗余Overfetching
REST的应对手段是通过位于中间节点或客户端缓存来缓解。但这治标不治本因为这个问题的根源在于HTTP协议对请求资源完全没有结构化的描述能力但有的是非结构化的部分内容获取能力也就是今天多用于端点续传的Range Header所以返回资源的哪些内容、以什么数据类型返回等等都不可能得到协议层面的支持。如果要实现这种能力你就只能自己在GET方法的Endpoint上设计各种参数。
而与此相对的缺陷也是HTTP协议对REST的第二种束缚是对资源的“批量”操作的支持。有时候我们不得不为此而专门设计一些抽象的资源才能应对。
比如我们要把某个用户的昵称增加一个“VIP”前缀那提交一个PUT请求修改这个用户的昵称就可以了。但如果我们要给1000个用户的昵称加“VIP”前缀时就不得不先创建一个比如名为“VIP-Modify-Task”任务资源把1000个用户的ID交给这个任务最后驱动任务进入执行状态如果真去调用1000次PUT等浏览器回应我们HTTP/1.1 429 Too Many Requests的时候老板就要发飙了
又比如我们在网店买东西的时候下单、冻结库存、支付、加积分、扣减库存这一系列步骤会涉及多个资源的变化这时候我们就得创建一种“事务”的抽象资源或者用某种具体的资源比如“结算单”贯穿网购这个过程的始终每次操作其他资源时都带着事务或者结算单的ID。对于HTTP协议来说由于它的无状态性相对来说不适用于并非不能够处理这类业务场景。
要解决批量操作这类问题目前一种从理论上看还比较优秀的解决方案是GraphQL但实际使用人数并不多。GraphQL是由Facebook提出并开源的一种面向资源API的数据查询语言。它和SQL一样挂了个“查询语言”的名字但其实CRUD都能做。
相对于依赖HTTP无协议的REST来说GraphQL是另一种“有协议”地、更彻底地面向资源的服务方式。但是凡事都有两面离开了HTTPGraphQL又面临着几乎所有RPC框架都会遇到的如何推广交互接口的问题。
小结
介绍REST服务的两节课里面我们学习了REST的思想内涵讲解了RESTful系统的6个核心特征以及如何衡量RESTful程度的RMM成熟度同时也讨论了REST的争议与不足。
在软件行业发展的初期,程序编写都是以算法为核心的,程序员会把数据和过程分别作为独立的部分来考虑,数据代表问题空间中的客体,程序代码则用于处理这些数据。这种直接站在计算机的角度去抽象问题和解决问题的思维方式,就是面向过程的编程思想。
与此类似,后来出现的面向对象的编程思想,则是站在现实世界的角度去抽象和解决问题。它把数据和行为都看作是对象的一部分,以方便程序员用符合现实世界的思维方式,来编写和组织程序。
我们今天再去看这两种编程思想,虽然它们出现的时间有先后,但在人类使用计算机语言来处理数据的工作中,无论用哪种思维来抽象问题都是合乎逻辑的。
经过了20世纪90年代末到21世纪初期面向对象编程的火热如今站在网络角度考虑如何对内封装逻辑、对外重用服务的新思想也就是面向资源的编程思想又成为了新的受追捧的对象。
面向资源编程这种思想是把问题空间中的数据对象作为抽象的主体把解决问题时从输入数据到输出结果的处理过程看作是一个数据资源的状态不断发生变换而导致的结果。这符合目前网络主流的交互方式也因此REST常常被看作是为基于网络的分布式系统量身定做的交互方式。
一课一思
从第7到10讲我们通过四节课学习了RPC和REST两种远程服务的设计风格。你更倾向于哪一种呢你觉得未来这两种风格会如何发展呢
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,159 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 _ 本地事务如何实现原子性和持久性?
你好,我是周志明。
在接下来的五节课里,我们将会一起讨论软件开发中另一个常见的话题:事务处理。
事务处理几乎是每一个信息系统中都会涉及到的问题它存在的意义就是保证系统中的数据是正确的不同数据间不会产生矛盾也就是保证数据状态的一致性Consistency
关于一致性,我这里先做个说明。“一致性”在数据科学中有严肃定义,并且有多种细分类型的概念。这里我们重点关注的是数据库状态的一致性,它跟课程后面第三个模块“分布式的基石”当中,即将要讨论的分布式共识算法时所说的一致性,是不一样的,具体的差别我们会在第三个模块中探讨。
说回数据库状态的一致性,理论上,要达成这个目标需要三方面的共同努力:
原子性Atomic在同一项业务处理过程中事务保证了多个对数据的修改要么同时成功要么一起被撤销。
隔离性Isolation在不同的业务处理过程中事务保证了各自业务正在读、写的数据互相独立不会彼此影响。
持久性Durability事务应当保证所有被成功提交的数据修改都能够正确地被持久化不丢失数据。
以上就是事务的“ACID”的概念提法。我自己对这种已经形成习惯的“ACID”的提法是不太认同的因为这四种特性并不正交A、I、D是手段C是目的完全是为了拼凑个单词缩写才弄到一块去误导的弊端已经超过了易于传播的好处。所以明确了这一点也就明确了我们今天的讨论就是要聚焦在事务处理的A、I、D上。
那接下来,我们先来看看事务处理的场景。
事务场景
事务的概念最初是源于数据库,但今天的信息系统中,所有需要保证数据正确性(一致性)的场景下,包括但不限于数据库、缓存、事务内存、消息、队列、对象文件存储等等,都有可能会涉及到事务处理。
当一个服务只操作一个数据源的时候通过A、I、D来获得一致性是相对容易的但当一个服务涉及到多个不同的数据源甚至多个不同服务同时涉及到多个不同的数据源时这件事情就变得很困难有时需要付出很大、甚至是不切实际的代价因此业界探索过许多其他方案在确保可操作的前提下获得尽可能高的一致性保障。由此事务处理才从一个具体操作上的“编程问题”上升成一个需要仔细权衡的“架构问题”。
人们在探索这些事务方案的过程中,产生了许多新的思路和概念,有一些概念看上去并不那么直观,因此,在接下来的这几节课中,我会带着你,一起探索同一个事例在不同的事务方案中的不同处理,以此来贯穿、理顺这些概念。
场景事例
我先来给你介绍下具体的事例。
Fenixs Bookstore是一个在线书店。一份商品成功售出需要确保以下三件事情被正确地处理
用户的账号扣减相应的商品款项;
商品仓库中扣减库存,将商品标识为待配送状态;
商家的账号增加相应的商品款项。
接下来,我将逐一介绍在“单个服务使用单个数据源”“单个服务使用多个数据源”“多个服务使用单个数据源”以及“多个服务使用多个数据源”的不同场景下,我们可以采用哪些手段来保证以上场景实例的正确性。
今天这一讲,我们先来看“单个服务使用单个数据源”,也就是本地事务场景。
本地事务
本地事务Local Transactions其实应该翻译成“局部事务”才好与第13讲中要讲解的“全局事务”对应起来。不过现在“本地事务”的译法似乎已经成为主流我们就不去纠结名称了。
本地事务是指仅操作特定单一事务资源的、不需要“全局事务管理器”进行协调的事务。如果这个定义你现在不能理解的话,不妨暂且先放下,等学完“全局事务”这个小章节后再回过头来想想。
本地事务是最基础的一种事务处理方案通常只适用于单个服务使用单个数据源的场景它是直接依赖于数据源通常是数据库系统本身的事务能力来工作的。在程序代码层面我们最多只能对事务接口做一层标准化的包装如JDBC接口并不能深入参与到事务的运作过程当中。
事务的开启、终止、提交、回滚、嵌套、设置隔离级别、乃至与应用代码贴近的传播方式全部都要依赖底层数据库的支持这一点与后面的14、15两讲中要介绍的XA、TCC、SAGA等主要靠应用程序代码来实现的事务有着十分明显的区别到时你可以跟今天所讲的内容相互对照下
我举个具体的例子假设你的代码调用了JDBC中的Transaction::rollback()方法方法的成功执行并不代表事务就已经被成功回滚如果数据表采用引擎的是MyISAM那rollback()方法便是一项没有意义的空操作。因此我们要想深入地讨论本地事务便不得不越过应用代码的层次去了解一些数据库本身的事务实现原理弄明白传统数据库管理系统是如何实现ACID的。
ARIES理论
如今研究事务的实现原理必定会追溯到ARIES理论Algorithms for Recovery and Isolation Exploiting Semantics基于语义的恢复与隔离算法。起这拗口的名字应该多少也有些拼凑“ARIES”这个单词的目的跟ACID一样的恶趣味
虽然我们不能说所有的数据库都实现了ARIES理论但现代的主流关系型数据库Oracle、Microsoft SQLServer、MySQL-InnoDB、IBM DB2、PostgreSQL等等在事务实现上都深受该理论的影响。
上世纪90年代IBM Almaden研究院总结了研发原型数据库系统“IBM System R”的经验发表了ARIES理论中最主要的三篇论文这里先给你介绍两篇。《ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging》着重解决了事务的ACID三个属性中原子性A和持久性D在算法层面上应当如何实现而另一篇《ARIES/KVL: A Key-Value Locking Method for Concurrency Control of Multiaction Transactions Operating on B-Tree Indexes》则是现代数据库隔离性I奠基式的文章。
我们先从原子性和持久性说起。至于隔离性,在下一节课中我们再接着展开介绍。
实现原子性和持久性
原子性和持久性在事务里是密切相关的两个属性,原子性保证了事务的多个操作要么都生效要么都不生效,不会存在中间状态;持久性保证了一旦事务生效,就不会再因为任何原因而导致其修改的内容被撤销或丢失。
显而易见数据必须要成功写入磁盘、磁带等持久化存储器后才能拥有持久性只存储在内存中的数据一旦遇到程序忽然崩溃、数据库崩溃、操作系统崩溃机器突然断电宕机后面我们都统称为崩溃Crash等情况就会丢失。实现原子性和持久性所面临的困难是“写入磁盘”这个操作不会是原子的不仅有“写入”与“未写入”还客观地存在着“正在写”的中间状态。
按照上面我们列出的示例场景从Fenixs Bookstore购买一本书需要修改三个数据在用户账户中减去货款、在商家账户中增加货款、在商品仓库中标记一本书为配送状态由于写入存在中间状态可能发生以下情形
未提交事务:程序还没修改完三个数据,数据库已经将其中一个或两个数据的变动写入了磁盘,此时出现崩溃,一旦重启之后,数据库必须要有办法得知崩溃前发生过一次不完整的购物操作,将已经修改过的数据从磁盘中恢复成没有改过的样子,以保证原子性。
已提交事务:程序已经修改完三个数据,数据库还未将全部三个数据的变动都写入到磁盘,此时出现崩溃,一旦重启之后,数据库必须要有办法得知崩溃前发生过一次完整的购物操作,将还没来得及写入磁盘的那部分数据重新写入,以保证持久性。
这种数据恢复操作被称为崩溃恢复Crash Recovery也有称作Failure Recovery或Transaction Recovery。为了能够顺利地完成崩溃恢复在磁盘中写数据就不能像程序修改内存中变量值那样直接改变某表某行某列的某个值必须将修改数据这个操作所需的全部信息比如修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值等等以日志的形式日志特指仅进行顺序追加的文件写入方式这是最高效的写入方式先记录到磁盘中。
只有在日志记录全部都安全落盘见到代表事务成功提交的“Commit Record”后数据库才会根据日志上的信息对真正的数据进行修改修改完成后在日志中加入一条“End Record”表示事务已完成持久化这种事务实现方法被称为“Commit Logging”。
额外知识Shadow Paging-
通过日志实现事务的原子性和持久性是当今的主流方案但并非唯一的选择。除日志外还有另外一种称为“Shadow Paging”有中文资料翻译为“影子分页”的事务实现机制常用的轻量级数据库SQLite Version 3采用的就是Shadow Paging。-
Shadow Paging的大体思路是对数据的变动会写到硬盘的数据中但并不是直接就地修改原先的数据而是先将数据复制一份副本保留原数据修改副本数据。在事务过程中被修改的数据会同时存在两份一份修改前的数据一份是修改后的数据这也是“影子”Shadow这个名字的由来。-
当事务成功提交所有数据的修改都成功持久化之后最后一步要修改数据的引用指针将引用从原数据改为新复制出来修改后的副本最后的“修改指针”这个操作将被认为是原子操作所以Shadow Paging也可以保证原子性和持久性。-
Shadow Paging相对简单但涉及到隔离性与锁时Shadow Paging实现的事务并发能力相对有限因此在高性能的数据库中应用不多。
Commit Logging保障数据持久性、原子性的原理并不难想明白。
首先日志一旦成功写入Commit Record那整个事务就是成功的即使修改数据时崩溃了重启后根据已经写入磁盘的日志信息恢复现场、继续修改数据即可这保证了持久性。
其次如果日志没有写入成功就发生崩溃系统重启后会看到一部分没有Commit Record的日志那将这部分日志标记为回滚状态即可整个事务就像完全没有发生过一样这保证了原子性。
Commit Logging实现事务简单清晰也有一些数据库就是采用Commit Logging机制来实现事务的较具代表性的是阿里的OceanBase。但是Commit Logging存在一个巨大的缺陷所有对数据的真实修改都必须发生在事务提交、日志写入了Commit Record之后即使事务提交前磁盘I/O有足够空闲、即使某个事务修改的数据量非常庞大占用大量的内存缓冲无论何种理由都决不允许在事务提交之前就开始修改磁盘上的数据这一点对提升数据库的性能是很不利的。
为了解决这个缺陷前面提到的ARIES理论终于可以登场了。ARIES提出了“Write-Ahead Logging”的日志改进方案其名字里所谓的“提前写入”Write-Ahead就是允许在事务提交之前提前写入变动数据的意思。
Write-Ahead Logging先将何时写入变动数据按照事务提交时点为界分为了FORCE和STEAL两类
FORCE当事务提交后要求变动数据必须同时完成写入则称为FORCE如果不强制变动数据必须同时完成写入则称为NO-FORCE。现实中绝大多数数据库采用的都是NO-FORCE策略只要有了日志变动数据随时可以持久化从优化磁盘I/O性能考虑没有必要强制数据写入立即进行。
STEAL在事务提交前允许变动数据提前写入则称为STEAL不允许则称为NO-STEAL。从优化磁盘I/O性能考虑允许数据提前写入有利于利用空闲I/O资源也有利于节省数据库缓存区的内存。
Commit Logging允许NO-FORCE但不允许STEAL。因为假如事务提交前就有部分变动数据写入磁盘那一旦事务要回滚或者发生了崩溃这些提前写入的变动数据就都成了错误。
Write-Ahead Logging允许NO-FORCE也允许STEAL它给出的解决办法是增加了另一种称为Undo Log的日志。当变动数据写入磁盘前必须先记录Undo Log写明修改哪个位置的数据、从什么值改成什么值以便在事务回滚或者崩溃恢复时根据Undo Log对提前写入的数据变动进行擦除。
Undo Log现在一般被翻译为“回滚日志”此前记录的用于崩溃恢复时重演数据变动的日志就相应被命名为Redo Log一般翻译为“重做日志”。
由于Undo Log的加入Write-Ahead Logging在崩溃恢复时会以此经历以下三个阶段
分析阶段Analysis该阶段从最后一次检查点Checkpoint可理解为在这个点之前所有应该持久化的变动都已安全落盘开始扫描日志找出所有没有End Record的事务组成待恢复的事务集合一般包括Transaction Table和Dirty Page Table
重做阶段Redo该阶段依据分析阶段中产生的待恢复的事务集合来重演历史Repeat History找出所有包含Commit Record的日志将它们写入磁盘写入完成后增加一条End Record然后移除出待恢复事务集合。
回滚阶段Undo该阶段处理经过分析、重做阶段后剩余的恢复事务集合此时剩下的都是需要回滚的事务被称为Loser根据Undo Log中的信息回滚这些事务。
重做阶段和回滚阶段的操作都应该设计为幂等的。而为了追求高性能以上三个阶段都无可避免地会涉及到非常繁琐的概念和细节如Redo Log、Undo Log的具体数据结构等这里我们就不展开讲了如果想要继续学习前面讲到的那两篇论文就是学习的最佳途径。
Write-Ahead Logging是ARIES理论的一部分整套ARIES拥有严谨、高性能等很多的优点但这些也是以复杂性为代价的。
数据库按照“是否允许FORCE和STEAL”可以产生四种组合从优化磁盘I/O的角度看NO-FORCE加STEAL组合的性能无疑是最高的从算法实现与日志的角度看NO-FORCE加STEAL组合的复杂度无疑是最高的。
这四种组合与Undo Log、Redo Log之间的具体关系如下图所示
小结
今天这节课我们学习了经典ARIES理论下实现本地事务中原子性与持久性的方法。通过写入日志来保证原子性和持久性是业界的主流做法这个做法最困难的一点就是如何处理日志“写入中”的中间状态才能既保证严谨也能够高效。
ARIES理论提出了Write-Ahead Logging式的日志写入方法通过分析、重做、回滚三个阶段实现了STEAL、NO-FORCE从而实现了既高效又严谨的日志记录与故障恢复。
一课一思
为什么Logging会成为实现原子性、持久性的主流做法Shadow Paging等其他方法的不足之处是什么
欢迎在留言区分享你的见解。如果你身边的朋友也对实现本地事务中原子性与持久性的方法感兴趣欢迎你把今天的内容分享给TA我们一起交流探讨。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,151 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 _ 本地事务如何实现隔离性?
你好。我是周志明。
今天我们接着上一节课的话题,继续来探讨数据库如何实现隔离性。
隔离性保证了每个事务各自读、写的数据互相独立,不会彼此影响。只从定义上,我们就能感觉到隔离性肯定与并发密切相关。如果没有并发,所有事务全都是串行的,那就不需要任何隔离,或者说这样的访问具备了天然的隔离性。
但在现实情况中不可能没有并发,要在并发下实现串行的数据访问,该怎样做?几乎所有程序员都会回答到:加锁同步呀!现代数据库都提供了以下三种锁:
写锁Write Lock也叫做排他锁eXclusive Lock简写为X-Lock只有持有写锁的事务才能对数据进行写入操作数据加持着写锁时其他事务不能写入数据也不能施加读锁。
读锁Read Lock也叫做共享锁Shared Lock简写为S-Lock多个事务可以对同一个数据添加多个读锁数据被加上读锁后就不能再被加上写锁所以其他事务不能对该数据进行写入但仍然可以读取。对于持有读锁的事务如果该数据只有一个事务加了读锁那可以直接将其升级为写锁然后写入数据。
范围锁Range Lock对于某个范围直接加排他锁在这个范围内的数据不能被读取也不能被写入。如下语句是典型的加范围锁的例子
SELECT * FROM books WHERE price < 100 FOR UPDATE;
请注意范围不能写入一批数据不能写入的差别也就是我们不要把范围锁理解成一组排他锁的集合加了范围锁后不仅无法修改该范围内已有的数据也不能在该范围内新增或删除任何数据这是一组排他锁的集合无法做到的
本地事务的四种隔离级别
了解了这三种锁的概念之后如果我们要继续探讨数据库是如何实现隔离性的那就得先理解事务的隔离级别接下来我就按照隔离强度从高到低来给你一一介绍
可串行化
串行化访问提供了强度最高的隔离性ANSI/ISO SQL-92中定义的最高等级的隔离级别便是可串行化Serializable)。
可串行化比较符合普通程序员对数据竞争加锁的理解如果不考虑性能优化的话对事务所有读写的数据全都加上读锁写锁和范围锁即可这种可串行化的实现方案称为Two-Phase Lock)。
但数据库不考虑性能肯定是不行的并发控制理论Concurrency Control决定了隔离程度与并发能力是相互抵触的隔离程度越高并发访问时的吞吐量就越低现代数据库一定会提供除可串行化以外的其他隔离级别供用户使用让用户调节隔离级别的选项这样做的根本目的是让用户可以调节数据库的加锁方式取得隔离性与吞吐量之间的平衡
可重复读
可串行化的下一个隔离级别是可重复读Repeatable Read)。可重复读的意思就是对事务所涉及到的数据加读锁和写锁并且一直持续到事务结束但不再加范围锁
可重复读比可串行化弱化的地方在于幻读问题Phantom Reads它是指在事务执行的过程中两个完全相同的范围查询得到了不同的结果集比如我现在准备统计一下Fenixs Bookstore中售价小于100元的书有多少本就可以执行以下第一条SQL语句
SELECT count(1) FROM books WHERE price < 100 /* 时间顺序1事务 T1 */
INSERT INTO books(name,price) VALUES ('深入理解Java虚拟机',90) /* 时间顺序2事务 T2 */
SELECT count(1) FROM books WHERE price < 100 /* 时间顺序3事务 T1 */
那么根据前面对范围锁读锁和写锁的定义我们可以知道假如这条SQL语句在同一个事务中重复执行了两次并且这两次执行之间恰好有另外一个事务在数据库中插入了一本小于100元的书籍这是当前隔离级别允许的操作那这两次相同的查询就会得到不一样的结果原因就是可重复读没有范围锁来禁止在该范围内插入新的数据
这就是一个事务遭到其他事务影响隔离性被破坏的表现
这里我要提醒你注意一个地方我这里的介绍实际上是以ARIES理论作为讨论目标的而具体的数据库并不一定要完全遵照着这个理论去实现
我给你举个例子MySQL/InnoDB的默认隔离级别是可重复读但它在只读事务中就可以完全避免幻读问题
比如在前面这个例子中事务T1只有查询语句它是一个只读事务所以这个例子里出现的幻读问题在MySQL中并不会出现但在读写事务中MySQL仍然会出现幻读问题比如例子中的事务T1如果在其他事务插入新书后不是重新查询一次数量而是要把所有小于100元的书全部改名那就依然会受到新插入书籍的影响
读已提交
可重复读的下一个隔离级别是读已提交Read Committed)。读已提交对事务涉及到的数据加的写锁会一直持续到事务结束但加的读锁在查询操作完成后就马上会释放
读已提交比可重复读弱化的地方在于不可重复读问题Non-Repeatable Reads它是指在事务执行过程中对同一行数据的两次查询得到了不同的结果
比如说现在我要获取Fenixs Bookstore中深入理解Java虚拟机这本书的售价同样让程序执行了两条SQL语句而在这两条语句执行之间恰好有另外一个事务修改了这本书的价格从90元调整到了110元如下所示
SELECT * FROM books WHERE id = 1; /* 时间顺序1事务 T1 */
UPDATE books SET price = 110 WHERE ID = 1; COMMIT; /* 时间顺序2事务 T2 */
SELECT * FROM books WHERE id = 1; COMMIT; /* 时间顺序3事务 T1 */
所以到这里你其实也会发现如果隔离级别是读已提交那么这两次重复执行的查询结果也会不一样原因是读已提交的隔离级别缺乏贯穿整个事务周期的读锁无法禁止读取过的数据发生变化而此时事务T2中的更新语句可以马上提供成功这也是一个事务遭到其他事务影响隔离性被破坏的表现
不过假如隔离级别是可重复读的话由于数据已被事务T1施加了读锁并且读取后不会马上释放所以事务T2无法获取到写锁更新就会被阻塞直至事务T1被提交或回滚后才能提交
读未提交
读已提交的下一个级别是读未提交Read Uncommitted)。读未提交对事务涉及到的数据只加写锁这会一直持续到事务结束但完全不加读锁
读未提交比读已提交弱化的地方在于脏读问题Dirty Reads它是指在事务执行的过程中一个事务读取到了另一个事务未提交的数据
比如说我觉得深入理解Java虚拟机从90元涨价到110元是损害消费者利益的行为又执行了一条更新语句把价格改回了90元而在我提交事务之前同事过来告诉我这并不是随便涨价的而是印刷成本上升导致的按90元卖要亏本于是我随即回滚了事务那么在这个场景下程序执行的SQL语句是这样的
SELECT * FROM books WHERE id = 1; /* 时间顺序1事务 T1 */
/* 注意没有COMMIT */
UPDATE books SET price = 90 WHERE ID = 1; /* 时间顺序2事务 T2 */
/* 这条SELECT模拟购书的操作的逻辑 */
SELECT * FROM books WHERE id = 1; /* 时间顺序3事务 T1 */
ROLLBACK; /* 时间顺序4事务 T2 */
不过在我修改完价格之后事务T1已经按90元的价格卖出了几本出现这个问题的原因就在于读未提交在数据上完全不加读锁这反而令它能读到其他事务加了写锁的数据也就是我前面所说的事务T1中两条查询语句得到的结果并不相同
这里你可能会有点疑问,“为什么完全不加读锁反而令它能读到其他事务加了写锁的数据”,这句话中的反而代表的是什么意思呢不理解也没关系我们再来重新读一遍写锁的定义写锁禁止其他事务施加读锁而不是禁止事务读取数据
所以说如果事务T1读取数据时根本就不用去加读锁的话就会导致事务T2未提交的数据也能马上就被事务T1所读到这同样是一个事务遭到其他事务影响隔离性被破坏的表现
那么这里我们假设隔离级别是读已提交的话由于事务T2持有数据的写锁所以事务T1的第二次查询就无法获得读锁而读已提交级别是要求先加读锁后读数据的所以T1中的查询就会被阻塞直到事务T2被提交或者回滚后才能得到结果
理论上还有更低的隔离级别就是完全不隔离”,即读写锁都不加读未提交会有脏读问题但不会有脏写问题Dirty Write即一个事务没提交之前的修改可以被另外一个事务的修改覆盖掉脏写已经不单纯是隔离性上的问题了它会导致事务的原子性都无法实现所以一般隔离级别不会包括它会把读未提交看作是最低级的隔离级别
这四种隔离级别属于数据库的基础知识多数大学的计算机课程应该都会讲到但不少教材资料都把它们当作数据库的某种固有设定来进行讲解导致很多人只能对这些现象死记硬背其实不同隔离级别以及幻读脏读等问题都只是表面现象它们是各种锁在不同加锁时间上组合应用所产生的结果锁才是根本的原因
除了锁之外以上对四种隔离级别的介绍还有一个共同特点就是一个事务在读数据过程中受另外一个写数据的事务影响而破坏了隔离性针对这种一个事务读+另一个事务写的隔离问题有一种名为多版本并发控制”(Multi-Version Concurrency ControlMVCC的无锁优化方案被主流的商业数据库广泛采用
接下来我们就一起讨论下MVCC
MVCC的基础原理
MVCC是一种读取优化策略它的无锁是特指读取时不需要加锁MVCC的基本思路是对数据库的任何修改都不会直接覆盖之前的数据而是产生一个新版副本与老版本共存以此达到读取时可以完全不加锁的目的
这句话里的版本是个关键词你不妨将其理解为数据库中每一行记录都存在两个看不见的字段CREATE_VERSION和DELETE_VERSION这两个字段记录的值都是事务ID事务ID是一个全局严格递增的数值然后
数据被插入时CREATE_VERSION记录插入数据的事务IDDELETE_VERSION为空
数据被删除时DELETE_VERSION记录删除数据的事务IDCREATE_VERSION为空
数据被修改时将修改视为删除旧数据插入新数据”,即先将原有数据复制一份原有数据的DELETE_VERSION记录修改数据的事务IDCREATE_VERSION为空复制出来的新数据的CREATE_VERSION记录修改数据的事务IDDELETE_VERSION为空
此时当有另外一个事务要读取这些发生了变化的数据时会根据隔离级别来决定到底应该读取哪个版本的数据
隔离级别是可重复读总是读取CREATE_VERSION小于或等于当前事务ID的记录在这个前提下如果数据仍有多个版本则取最新事务ID最大
隔离级别是读已提交总是取最新的版本即可即最近被Commit的那个版本的数据记录
另外两个隔离级别都没有必要用到MVCC读未提交直接修改原始数据即可其他事务查看数据的时候立刻可以查看到根本无需版本字段可串行化本来的语义就是要阻塞其他事务的读取操作而MVCC是做读取时无锁优化的自然就不会放到一起用
MVCC是只针对+场景的优化如果是两个事务同时修改数据+的情况那就没有多少优化的空间了加锁几乎是唯一可行的解决方案
稍微有点讨论余地的是乐观加锁”(Optimistic Locking悲观加锁”(Pessimistic Locking对此我们还可以根据实际情况去商量一下
前面我介绍的加锁都属于悲观加锁策略也就是数据库认为如果不先做加锁再访问数据就肯定会出现问题与之相对的乐观加锁策略认为事务之间数据存在竞争是偶然情况没有竞争才是普遍情况这样就不应该一开始就加锁而是应当出现竞争时再找补救措施这种思路被称为乐观并发控制”(Optimistic Concurrency ControlOCC这一点我就不再展开了不过提醒一句不要迷信什么乐观锁要比悲观锁更快的说法这纯粹看竞争的剧烈程度如果竞争剧烈的话乐观锁反而会更慢
小结
今天的内容再加上上一讲这两节课我们总结了本地事务中原子性持久性和隔离性的实现模式如果你是后端程序员只要你实际开发过用于生产的软件系统几乎一定会使用过本地事务
但在Spring等框架的声明式事务的简化下对多数程序员来说事务可能仅仅是一个注解一种概念却未必真正理解它们的原理和运作希望通过这两节课的学习你能对这些常用却不常为人所注意到的知识点有更进一步的理解
一课一思
现在大多数系统都把本地事务控制在底层在系统特定分层中开启和结束对普通开发人员尽量透明你在开发时会考虑事务吗你认为以上透明式的事务管理是否合适普通开发人员是否应该意识到事务的存在
欢迎在留言区分享你的见解如果你身边的朋友也对实现本地事务中隔离性的方法感兴趣欢迎你把今天的内容分享给TA我们一起交流探讨
感谢你的阅读我们下一讲再见

View File

@@ -0,0 +1,185 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 _ 全局事务和共享事务是如何实现的?
你好我是周志明。今天我们一起来学习全局事务Global Transactions和共享事务Share Transactions的原理与实现。
其实,相对于我们前两节课学习的本地事务,全局事务和共享事务的使用频率已经很低了。但这两种事务类型,是分布式事务(下一讲要学习)的中间形式,起到的是承上启下的作用。
所以,我们还是有必要去理解它们的实现方式,这样才能更透彻地理解事务处理这个话题。
接下来,我们就从全局事务学起吧。
全局事务
与本地事务相对的是全局事务一些资料中也会称之为外部事务External Transactions。在今天这一讲我会给全局事务做个限定一种适用于单个服务使用多个数据源场景的事务解决方案。
需要注意的是理论上真正的全局事务是没有“单个服务”这个约束的它本来就是DTPDistributed Transaction Processing模型中的概念。那我为什么要在这一讲给它做个限定呢
这是因为,我们今天要学习的内容,也就是一种在分布式环境中仍追求强一致性的事务处理方案,在多节点互相调用彼此服务的场景(比如现在的微服务)中是非常不合适的。从目前的情况来看,这种方案几乎只实际应用在了单服务多数据源的场景中。
为了避免与我们下一讲要学习的放弃了ACID的弱一致性事务处理方式混淆所以我在这一讲缩减了全局事务所指的范围对于涉及多服务多数据源的事务我将其称为“分布式事务”。
XA协议
为了解决分布式事务的一致性问题1991年的时候X/Open组织后来并入了The Open Group提出了一套叫做X/Open XAXA是eXtended Architecture的缩写的事务处理框架。这个框架的核心内容是定义了全局的事务管理器Transaction Manager用于协调全局事务和局部的资源管理器Resource Manager用于驱动本地事务之间的通讯接口。
XA接口是双向的是一个事务管理器和多个资源管理器之间通信的桥梁通过协调多个数据源的动作保持一致来实现全局事务的统一提交或者统一回滚。现在我们在Java代码中还偶尔能看见的XADataSource、XAResource等名字其实都是源于XA接口。
这里你要注意的是XA并不是Java规范因为当时还没有Java而是一套通用的技术规范。Java后来专门定义了一套全局事务处理标准也就是我们熟知的JTAJSR 907 Java Transaction API接口。它有两个最主要的接口
事务管理器的接口javax.transaction.TransactionManager这套接口是给Java EE服务器提供容器事务由容器自动负责事务管理使用的。另外它还提供了另外一套javax.transaction.UserTransaction接口用于给程序员通过程序代码手动开启、提交和回滚事务。
满足XA规范的资源定义接口javax.transaction.xa.XAResource。任何资源JDBC、JMS等如果需要支持JTA只要实现XAResource接口中的方法就可以了。
JTA原本是Java EE中的技术一般情况下应该由JBoss、WebSphere、WebLogic这些Java EE容器来提供支持但现在Bittronix、Atomikos和JBossTM以前叫Arjuna都以JAR包的形式实现了JTA的接口也就是JOTMJava Open Transaction Manager。有了JOTM的支持我们就可以在Tomcat、Jetty这样的Java SE环境下使用JTA了。
我们在第11讲讲解本地事务的时候设计了一个Fenixs Bookstore在线书店场景。一份商品成功售出需要确保以下三件事情被正确地处理
用户的账号扣减相应的商品款项;
商品仓库中扣减库存,将商品标识为待配送状态;
商家的账号增加相应的商品款项。
现在,我们对这个示例场景做另外一种假设:如果书店的用户、商家、仓库分别处于不同的数据库中,其他条件不变,那会发生什么变化呢?
如果我们以声明式事务来编码的话,那与本地事务看起来可能没什么区别,都是标个@Transactional注解而已,但如果是以编程式事务来实现的话,在写法上就有差异了。我们具体看看:
public void buyBook(PaymentBill bill) {
userTransaction.begin();
warehouseTransaction.begin();
businessTransaction.begin();
try {
userAccountService.pay(bill.getMoney());
warehouseService.deliver(bill.getItems());
businessAccountService.receipt(bill.getMoney());
userTransaction.commit();
warehouseTransaction.commit();
businessTransaction.commit();
} catch(Exception e) {
userTransaction.rollback();
warehouseTransaction.rollback();
businessTransaction.rollback();
}
}
两段式提交
代码上能看出程序的目的是要做三次事务提交,但实际代码并不能这样写。为什么呢?
我们可以试想一下如果程序运行到businessTransaction.commit()中出现错误会跳转到catch块中继续执行这时候userTransaction和warehouseTransaction已经提交了再去调用rollback()方法已经无济于事。因为这会导致一部分数据被提交,另一部分被回滚,无法保证整个事务的一致性。
为了解决这个问题XA将事务提交拆分成了两阶段过程也就是准备阶段和提交阶段。
准备阶段又叫做投票阶段。在这一阶段协调者询问事务的所有参与者是否准备好提交如果已经准备好提交回复Prepared否则回复Non-Prepared。
这里的“准备”操作其实和我们通常理解的“准备”不太一样对于数据库来说准备操作是在重做日志中记录全部事务提交操作所要做的内容它与本地事务中真正提交的区别只是暂不写入最后一条Commit Record。这意味着在做完数据持久化后并不会立即释放隔离性也就是仍继续持有锁维持数据对其他非事务内观察者的隔离状态。
提交阶段又叫做执行阶段协调者如果在准备阶段收到所有事务参与者回复的Prepared消息就会首先在本地持久化事务状态为Commit然后向所有参与者发送Commit指令所有参与者立即执行提交操作否则任意一个参与者回复了Non-Prepared消息或任意一个参与者超时未回复协调者都会将自己的事务状态持久化为“Abort”之后向所有参与者发送Abort指令参与者立即执行回滚操作。
对于数据库来说提交阶段的提交操作是相对轻量的仅仅是持久化一条Commit Record而已通常能够快速完成。回滚阶段则相对耗时收到Abort指令时需要根据回滚日志清理已提交的数据这可能是相对重负载操作。
“准备”和“提交”这两个过程被称为“两段式提交”2 Phase Commit2PC协议。那么使用了两阶段提交协议就一定可以成功保证一致性吗也不是的它还需要两个前提条件。
第一必须假设网络在提交阶段这个短时间内是可靠的即提交阶段不会丢失消息。同时也假设网络通讯在全过程都不会出现误差即可以丢失后消息但不会传递错误的消息XA的设计目标并不是解决诸如拜占庭将军一类的问题。
两段式提交中投票阶段失败了可以补救(回滚),而提交阶段失败了无法补救(不再改变提交或回滚的结果,只能等崩溃的节点重新恢复),因而提交阶段的耗时应尽可能短,这也是为了尽量控制网络风险的考虑。
第二,必须假设因为网络分区、机器崩溃或者其他原因而导致失联的节点最终能够恢复,不会永久性地处于失联状态。由于在准备阶段已经写入了完整的重做日志,所以当失联机器一旦恢复,就能够从日志中找出已准备妥当但并未提交的事务数据,再向协调者查询该事务的状态,确定下一步应该进行提交还是回滚操作。
到这里,我还要给你澄清一个概念。我们前面提到的协调者和参与者,通常都是由数据库自己来扮演的,不需要应用程序介入,应用程序相对于数据库来说只扮演客户端的角色。
两段式提交的原理很简单,也不难实现,但有三个非常明显的缺点。
单点问题协调者在两段提交中具有举足轻重的作用协调者等待参与者回复时可以有超时机制允许参与者宕机但参与者等待协调者指令时无法做超时处理。一旦协调者宕机所有参与者都会受到影响。如果协调者一直没有恢复没有正常发送Commit或者Rollback的指令那所有参与者都必须一直等待。
性能问题两段提交过程中所有参与者相当于被绑定成为一个统一调度的整体期间要经过两次远程服务调用、三次数据持久化准备阶段写重做日志协调者做状态持久化提交阶段在日志写入Commit Record整个过程将持续到参与者集群中最慢的那一个处理操作结束为止。这就决定了两段式提交的性能通常都比较差。
一致性风险:当网络稳定性和宕机恢复能力的假设不成立时,两段式提交可能会出现一致性问题。
宕机恢复能力这一点无需多说。1985年Fischer、Lynch、Paterson用定理被称为FLP不可能原理在分布式中与CAP定理齐名证明了如果宕机最后不能恢复那就不存在任何一种分布式协议可以正确地达成一致性结果。
我们重点看看网络稳定性带来的一致性风险。尽管提交阶段时间很短但仍是明确存在的危险期。如果协调者在发出准备指令后根据各个参与者发回的信息确定事务状态是可以提交的协调者就会先持久化事务状态并提交自己的事务。如果这时候网络忽然断开了无法再通过网络向所有参与者发出Commit指令的话就会导致部分数据协调者的已提交但部分数据参与者的既未提交也没办法回滚导致数据不一致。
三段式提交
为了解决两段式提交的单点问题、性能问题和数据一致性问题“三段式提交”3 Phase Commit3PC协议出现了。但是三段式提交也并没有解决一致性问题。
这是为什么呢?别着急,接下来我就具体和你分析下其中的缘由,以及了解三段式提交是否真正解决了单点问题和性能问题。
三段式提交把原本的两段式提交的准备阶段再细分为两个阶段分别称为CanCommit、PreCommit把提交阶段改为DoCommit阶段。其中新增的CanCommit是一个询问阶段协调者让每个参与的数据库根据自身状态评估该事务是否有可能顺利完成。
将准备阶段一分为二的理由是,这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,这时候涉及的数据资源都会被锁住。如果此时某一个参与者无法完成提交,相当于所有的参与者都做了一轮无用功。
所以,增加一轮询问阶段,如果都得到了正面的响应,那事务能够成功提交的把握就比较大了,也意味着因某个参与者提交时发生崩溃而导致全部回滚的风险相对变小了。
因此,在事务需要回滚的场景中,三段式的性能通常要比两段式好很多,但在事务能够正常提交的场景中,两段式和三段式提交的性能都很差,三段式因为多了一次询问,性能还要更差一些。
同样地也是因为询问阶段使得事务失败回滚的概率变小了所以在三段式提交中如果协调者在PreCommit阶段开始之后发生了宕机参与者没有能等到DoCommit的消息的话默认的操作策略将是提交事务而不是回滚事务或者持续等待。你看这就相当于避免了协调者的单点问题。
三段式提交的操作时序如下图所示。
可以看出,三段式提交对单点问题和回滚时的性能问题有所改善,但是对一致性风险问题并未有任何改进,甚至是增加了面临的一致性风险。为什么这么说呢?
我们看一个例子。比如进入PreCommit阶段之后协调者发出的指令不是Ack而是Abort而此时因为网络问题有部分参与者直至超时都没能收到协调者的Abort指令的话这些参与者将会错误地提交事务这就产生了不同参与者之间数据不一致的问题。
共享事务
与全局事务的单个服务使用多个数据源正好相反,共享事务是指多个服务共用同一个数据源。
这里,我要再强调一次“数据源”与“数据库”的区别:数据源是指提供数据的逻辑设备,不必与物理设备一一对应。
在部署应用集群时最常采用的模式是将同一套程序部署到多个中间件服务器上构成多个副本实例来分担流量压力。它们虽然连接了同一个数据库但每个节点配有自己的专属数据源通常是中间件以JNDI的形式开放给程序代码使用。
这种情况下,所有副本实例的数据访问都是完全独立的,并没有任何交集,每个节点使用的仍是最简单的本地事务。但是有些场景下,多个服务之间是有业务交集的,它们可能会共用一个数据源,共享事务也有可能成为专门针对这种业务场景的一种解决方案。
举个例子。在Fenixs Bookstore的场景事例中假设用户账户、商家账户和商品仓库都存储在同一个数据库里面但用户、商户和仓库每个领域部署了独立的微服务。此时一次购书的业务操作将贯穿三个微服务而且都要在数据库中修改数据。
如果我们直接将不同数据源视为不同的数据库,那我们完全可以用全局事务或者下一讲要学习的分布式事务来实现。不过,针对每个数据源连接的都是同一个物理数据库的特例,共享事务可能是另一条可以提高性能、降低复杂度的途径,当然这也很有可能是一个伪需求。
一种理论可行的方案是直接让各个服务共享数据库连接。同一个应用进程中的不同持久化工具JDBC、ORM、JMS等共享数据库连接并不困难一些中间件服务器比如WebSphere就内置了“可共享连接”功能来专门支持共享数据库的连接。
但这种“共享”的前提是数据源的使用者都在同一个进程内。由于数据库连接的基础是网络连接它是与IP地址和端口号绑定的字面意义上的“不同服务节点共享数据库连接”很难做到。所以为了实现共享事务就必须新增一个中间角色也就是交易服务器。无论是用户服务、商家服务还是仓库服务它们都要通过同一台交易服务器来与数据库打交道。
如果将交易服务器的对外接口实现为满足JDBC规范那它完全可以看作一个独立于各个服务的远程数据库连接池或者直接作为数据库代理来看待。此时三个服务所发出的交易请求就有可能做到由交易服务器上的同一个数据库连接通过本地事务的方式完成。
比如交易服务器根据不同服务节点传来的同一个事务ID使用同一个数据库连接来处理跨越多个服务的交易事务。
之所以强调理论可行,是因为这个方案,其实是与实际生产系统中的压力方向相悖的。一个服务集群里,数据库才是压力最大、最不容易伸缩拓展的重灾区。
所以现实中只有类似ProxySQL和MaxScale这样用于对多个数据库实例做负载均衡的数据库代理而几乎没有反过来代理一个数据库为多个应用提供事务协调的交易服务代理。
这也是为什么说它更有可能是个伪需求的原因。如果你有充足理由让多个微服务去共享数据库,那就必须找到更加站得住脚的理由,来向团队解释拆分微服务的目的是什么。
让多个微服务去共享一个数据库这个方案其实还有另一种应用形式使用消息队列服务器来代替交易服务器用户、商家、仓库的服务操作业务时通过消息将所有对数据库的改动传送到消息队列服务器然后通过消息的消费者来统一处理实现由本地事务保障的持久化操作。这就是“单个数据库的消息驱动更新”Message-Driven Update of a Single Database
“共享事务”这种叫法以及我们刚刚讲到的通过交易服务器或者通过消息驱动来更新单个数据库这两种处理方式在实际应用中并不常见也几乎没有相应的成功案例能够查到的资料几乎都来源于十多年前Spring的核心开发者Dave Syer的文章“Distributed Transactions in Spring, with and without XA”。
正如我在这一讲的开头所说,我把共享事务和本地事务、全局事务、分布式事务并列成为四大事务类型,更多的考虑到事务演进过程的完备性,也是为了方便你理解这三种事务类型。同时,拆分微服务后仍然共享数据库的案例,我们经常会在实践中看到,但我个人仍旧不赞同将共享事务看作是一种常规的解决方案。
小结
这节课我们学习了全局事务和共享事务的实现方式。目前共享事务确实已经很少见了但是全局事务中的两段式提交和三段式提交模式仍然会在一些多数据源的场景中用到Java的JTA事务也仍然有一定规模的用户群体。
两段式提交和三段式提交仍然追求ACID的强一致性这个目标不仅给它带来了很高的复杂度而且吞吐量和使用效果上也不佳。因此现在系统设计的主流已经变成了不追求ACID而是强调BASE的弱一致性事务这就是我们要在下一讲学习的分布式事务了。
一课一思
你开发过的系统使用过全局事务和共享事务吗?你当时是如何实现这些事务的呢?
欢迎在留言区分享你的答案。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,163 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 _ 分布式事务之可靠消息队列
你好,我是周志明。
前面几节课,我们谈论了事务处理中的本地事务(单个服务、单个数据源)、全局事务(单个服务、多个数据源)和共享事务(多个服务、单个数据源),这一讲我们将聚焦于事务处理中最复杂的分布式事务(多个服务、多个数据源)。
在开始展开介绍之前我想先给你强调一下这里所说的分布式事务Distributed Transactions跟DTP模型中所指的“分布式事务”的含义是不一样的DTP模型所指的“分布式”是相对于数据源而言的并不涉及服务这部分内容我们在上节课已经讨论过了而这里的“分布式”是相对于服务而言的它特指的是多个服务同时访问多个数据源的事务处理机制严谨地说它更应该被称为“在分布式服务环境下的事务处理机制”。
其实在上一讲我们就提到过为了解决分布式事务的一致性问题1991年X/Open组织提出了一套XA的事务处理架构。在2000年以前人们还寄希望于这套事务处理架构能良好地应用在分布式环境中。不过很遗憾这个美好的愿望今天已经被CAP理论彻底地击碎了。
那么为什么会出现这种局面呢就让我们从CAP与ACID的矛盾开始说起吧。
CAP与ACID之间的矛盾
CAP理论又叫Brewer理论这是加州大学伯克利分校的埃里克 · 布鲁尔Eric Brewer教授在2000年7月“ACM分布式计算原理研讨会PODC”上提出的一个猜想。
CAP理论原稿那时候还只是猜想
然后到了2002年麻省理工学院的赛斯 · 吉尔伯特Seth Gilbert和南希 · 林奇Nancy Lynch就以严谨的数学推理证明了这个CAP猜想。在这之后CAP理论就正式成为了分布式计算领域公认的著名定理。
这个定理里,描述了一个分布式的系统中,当涉及到共享数据问题时,以下三个特性最多只能满足其中两个:
一致性Consistency代表在任何时刻、任何分布式节点中我们所看到的数据都是没有矛盾的。这与第11讲所提到的ACID中的C是相同的单词但它们又有不同的定义分别指Replication的一致性和数据库状态的一致性。在分布式事务中ACID的C要以满足CAP中的C为前提。
可用性Availability代表系统不间断地提供服务的能力。
分区容忍性Partition Tolerance代表分布式环境中当部分节点因网络原因而彼此失联即与其他节点形成“网络分区”系统仍能正确地提供服务的能力。
当然单纯只看这个概念的话CAP是比较抽象的我还是以第11讲开头所列的事例场景来说明一下这三种特性对分布式系统来说都意味着什么。
事例场景Fenixs Bookstore是一个在线书店。一份商品成功售出需要确保以下三件事情被正确地处理
用户的账号扣减相应的商品款项;
商品仓库中扣减库存,将商品标识为待配送状态;
商家的账号增加相应的商品款项。
假设Fenixs Bookstore的服务拓扑如下图所示一个来自最终用户的交易请求将交由账号、商家和仓库服务集群中的某一个节点来完成响应
Fenixs Bookstore的服务拓扑
你可以看到,在这套系统中,每一个单独的服务节点都有着自己的数据库。
假设某次交易请求分别由“账号节点1”“商家节点2”“仓库节点N”来进行响应当用户购买一件价值100元的商品后账号节点1首先应该给用户账号扣减100元货款。
账号节点1在自己的数据库扣减100元是很容易的但它还要把这次交易变动告知账号节点2到N以及确保能正确变更商家和仓库集群其他账号节点中的关联数据。那么此时我们可能会面临以下几种情况
如果该变动信息没有及时同步给其他账号节点,那么当用户购买其他商品时,会被分配给另一个节点处理,因为没有及时同步,此时系统会看到用户账户上有不正确的余额,从而错误地发生了原本无法进行的交易。此为一致性问题。
如果因为要把该变动信息同步给其他账号节点,就必须暂停对该用户的交易服务,直到数据同步一致后再重新恢复,那么当用户在下一次购买商品时,可能会因为系统暂时无法提供服务而被拒绝交易。此为可用性问题。
如果由于账号服务集群中某一部分节点,因出现网络问题,无法正常与另一部分节点交换账号变动信息,那么此时的服务集群中,无论哪一部分节点对外提供的服务,都可能是不正确的,我们需要考虑能否接受由于部分节点之间的连接中断,而影响整个集群的正确性的情况。此为分区容忍性问题。
以上还只是涉及到了账号服务集群自身的CAP问题而对于整个Bookstore站点来说它更是面临着来自于账号、商家和仓库服务集群带来的CAP问题。
比如,用户账号扣款后,由于没有及时通知仓库服务,导致另一次交易中看到仓库中有不正确的库存数据而发生了超售。再比如,因为仓库中某个商品的交易正在进行当中,为了同步用户、商家和仓库此时的交易变动,而暂时锁定了该商品的交易服务,导致了可用性问题,等等。
不过既然CAP理论已经有了数学证明也成为了业界公认的计算定理我们就不去讨论为何CAP特性会存在不可兼得的问题了直接来分析下在实际的应用场景中我们要如何权衡取舍CAP然后看看这些不同取舍都会带来哪些问题。
如果放弃分区容错性CA without P
这意味着,我们将假设节点之间的通讯永远是可靠的。可是永远可靠的通讯在分布式系统中必定是不成立的,这不是你想不想的问题,而是网络分区现象始终会存在。
在现实场景中主流的RDBMS关系数据库管理系统集群通常就是采用放弃分区容错性的工作模式。以Oracle的RAC集群为例它的每一个节点都有自己的SGA系统全局区、重做日志、回滚日志等但各个节点是共享磁盘中的同一份数据文件和控制文件的也就是说RAC集群是通过共享磁盘的方式来避免网络分区的出现。
如果放弃可用性CP without A
这意味着我们将假设一旦发生分区节点之间的信息同步时间可以无限制地延长那么这个问题就相当于退化到了上一讲所讨论的全局事务的场景之中即一个系统可以使用多个数据源。我们可以通过2PC/3PC等手段同时获得分区容错性和一致性。
在现实中除了DTP模型的分布式数据库事务外著名的HBase也是属于CP系统。以它的集群为例假如某个RegionServer宕机了这个RegionServer持有的所有键值范围都将离线直到数据恢复过程完成为止这个时间通常会是很长的。
如果放弃一致性AP without C
这意味着,我们将假设一旦发生分区,节点之间所提供的数据可能不一致。
AP系统目前是分布式系统设计的主流选择大多数的NoSQL库和支持分布式的缓存都是AP系统。因为P是分布式网络的天然属性你不想要也无法丢弃而A通常是建设分布式的目的如果可用性随着节点数量增加反而降低的话很多分布式系统可能就没有存在的价值了除非银行这些涉及到金钱交易的服务宁可中断也不能出错
以Redis集群为例如果某个Redis节点出现网络分区那也不妨碍每个节点仍然会以自己本地的数据对外提供服务。但这时有可能出现这种情况即请求分配到不同节点时返回给客户端的是不同的数据。
那么看到这里,你是否感受到了一丝无奈?这个小章节所讨论的话题“事务”,原本的目的就是要获得“一致性”。而在分布式环境中,“一致性”却不得不成为了通常被牺牲、被放弃的那一项属性。
但无论如何我们建设信息系统终究还是要保证操作结果在最终被交付的时候是正确的。为此人们又重新给一致性下了定义把前面我们在CAP、ACID中讨论的一致性称为“强一致性**”Strong Consistency有时也称为“线性一致性”Linearizability而把牺牲了C的AP系统又要尽可能获得正确的结果的行为称为追求“弱一致性**”。
不过,如果单纯只说“弱一致性”,那其实就是“不保证一致性”的意思……人类语言这东西真是博大精深。
所以,在弱一致性中,人们又总结出了一种特例,叫做“[最终一致性](https://en.wikipedia.org/wiki/Eventual_consistency)”Eventual Consistency。它是指如果数据在一段时间内没有被另外的操作所更改那它最终将会达到与强一致性过程相同的结果有时候面向最终一致性的算法也被称为“乐观复制算法”。
那么,在“分布式事务”中,我们的设计目标同样也不得不从获得强一致性,降低为获得“最终一致性”,在这个意义上,其实“事务”一词的含义也已经被拓宽了。
除了本地事务、全局事务和分布式事务以外还有一种对于不同事务的叫法那就是针对追求ACID的事务我们称之为“刚性事务”。而在接下来和下一讲中我将要介绍的几种分布式事务的常见做法会统称为“柔性事务”。
这一讲我们先来讨论下,可靠消息队列这种分布式事务的实现方式。
可靠事件队列
前面提到的最终一致性的概念是由eBay的系统架构师丹 · 普利切特Dan Pritchett在2008年发表于ACM的论文“Base: An Acid Alternative”中提出的。
这篇文章中总结了一种独立于ACID获得的强一致性之外的途径即通过BASE来达成一致性目的最终一致性就是其中的“E”。
BASE这个提法比ACID凑缩写的痕迹更重不过因为有ACID vs BASE酸 vs 碱这个朗朗上口的梗这篇文章传播得足够快。在这里我就不多谈BASE中的概念了但这篇论文本身作为最终一致性的概念起源并系统性地总结了一种在分布式事务的技术手段还是非常有价值的。
下面我们继续以Fenixs Bookstore的事例场景来解释下丹 · 普利切特提出的“可靠事件队列”的具体做法,下图为操作时序:
可靠事件队列时序图
我们按照顺序,一步步来解读一下。
第一步最终用户向Fenixs Bookstore发送交易请求购买一本价值100元的《深入理解Java虚拟机》。
第二步Fenixs Bookstore应该对用户账户扣款、商家账户收款、库存商品出库这三个操作有一个出错概率的先验评估根据出错概率的大小来安排它们的操作顺序这个一般体现在程序代码中有一些大型系统也可能动态排序。比如最有可能出错的地方是用户购买了但是系统不同意扣款或者是账户余额不足其次是商品库存不足最后是商家收款一般收款不会遇到什么意外。那么这个顺序就应该是最容易出错的最先进行账户扣款 → 仓库出库 → 商家收款。
第三步账户服务进行扣款业务如果扣款成功就在自己的数据库建立一张消息表里面存入一条消息“事务IDUUID扣款100元状态已完成仓库出库《深入理解Java虚拟机》1本状态进行中某商家收款100元状态进行中”。注意这个步骤中“扣款业务”和“写入消息”是依靠同一个本地事务写入自身数据库的。
第四步,系统建立一个消息服务,定时轮询消息表,将状态是“进行中”的消息同时发送到库存和商家服务节点中去。
这时候可能会产生以下几种情况:
商家和仓库服务成功完成了收款和出库工作,向用户账户服务器返回执行结果,用户账户服务把消息状态从“进行中”更新为“已完成”。整个事务宣告顺利结束,达到最终一致性的状态。
商家或仓库服务有某些或全部因网络原因未能收到来自用户账户服务的消息。此时由于用户账户服务器中存储的消息状态一直处于“进行中”所以消息服务器将在每次轮询的时候持续地向对应的服务重复发送消息。这个步骤的可重复性就决定了所有被消息服务器发送的消息都必须具备幂等性。通常我们的设计是让消息带上一个唯一的事务ID以保证一个事务中的出库、收款动作只会被处理一次。
商家或仓库服务有某个或全部无法完成工作。比如仓库发现《深入理解Java虚拟机》没有库存了此时仍然是持续自动重发消息直至操作成功比如补充了库存或者被人工介入为止。
商家和仓库服务成功完成了收款和出库工作,但回复的应答消息因网络原因丢失。此时,用户账户服务仍会重新发出下一条消息,但因消息幂等,所以不会导致重复出库和收款,只会导致商家、仓库服务器重新发送一条应答消息。此过程会一直重复,直至双方网络恢复。
也有一些支持分布式事务的消息框架如RocketMQ原生就支持分布式事务操作这时候前面提到的情况2、4也可以交给消息框架来保障。
前面这种靠着持续重试来保证可靠性的操作在计算机中就非常常见它有个专门的名字叫做“最大努力交付”Best-Effort Delivery比如TCP协议中的可靠性保障就属于最大努力交付。
而“可靠事件队列”有一种更普通的形式被称为“最大努力一次提交”Best-Effort 1PC意思就是系统会把最有可能出错的业务以本地事务的方式完成后通过不断重试的方式不限于消息系统来促使同个事务的其他关联业务完成。
小结
这节课我第一次引入了CAP定理希望你能通过事务处理的上下文场景去理解它。这套理论不仅是在事务处理中而且在一致性、共识乃至整个分布式所有涉及到数据的知识点中都有重要的应用后面讲到分布式共识算法、微服务中多种基础设施等内容的时候我们还会多次涉及到它。
除了可靠事件队列之外下一讲我还会给你介绍TCC和SAGA这两种主流的实现方式它们都有各自的优缺点和应用场景。分布式系统中不存在放之四海皆准的万能事务解决方案针对具体场景选择合适的解决方案达到一致性与可用性之间的最佳平衡是我们作为一名设计者必须具备的技能。
一课一思
请你思考一下为什么XA事务很少在分布式环境下直接应用会有什么代价而这节课介绍的“可靠事件队列”的事务实现方式又会有什么代价欢迎给我留言分享你的思考和见解。
如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,166 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 _ 分布式事务之TCC与SAGA
你好,我是周志明。
今天我们接着上一节课的话题继续讨论另外两种主流的分布式事务实现方式TCC和SAGA。
TCC事务的实现过程
TCCTry-Confirm-Cancel是除可靠消息队列以外的另一种常见的分布式事务机制它是由数据库专家帕特 · 赫兰德Pat Helland在2007年撰写的论文《Life beyond Distributed Transactions: An Apostates Opinion》中提出的。
在上一讲我给你介绍了可靠消息队列的实现原理虽然它也能保证最终的结果是相对可靠的过程也足够简单相对于TCC来说但现在你已经知道可靠消息队列的整个实现过程完全没有任何隔离性可言。
虽然在有些业务中有没有隔离性不是很重要比如说搜索系统。但在有些业务中一旦缺乏了隔离性就会带来许多麻烦。比如说前几讲我一直引用的Fenixs Bookstore在线书店的场景事例中如果缺乏了隔离性就会带来一个显而易见的问题超售。
事例场景Fenixs Bookstore是一个在线书店。一份商品成功售出需要确保以下三件事情被正确地处理
用户的账号扣减相应的商品款项;
商品仓库中扣减库存,将商品标识为待配送状态;
商家的账号增加相应的商品款项。
也就是说,在书店的业务场景下,很有可能会出现这样的情况:两个客户在短时间内都成功购买了同一件商品,而且他们各自购买的数量都不超过目前的库存,但他们购买的数量之和,却超过了库存。
如果这件事情是发生在刚性事务且隔离级别足够的情况下其实是可以完全避免的。比如我前面提到的“超售”场景就需要“可重复读”Repeatable Read的隔离级别以保证后面提交的事务会因为无法获得锁而导致失败。但用可靠消息队列就无法保证这一点了。我在第12讲中已经给你介绍过数据库本地事务的相关知识你可以再去回顾复习下。
所以如果业务需要隔离我们通常就应该重点考虑TCC方案它天生适合用于需要强隔离性的分布式事务中。
在具体实现上TCC的操作其实有点儿麻烦和复杂它是一种业务侵入性较强的事务方案要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。另外你看名字也能看出来TCC的实现过程分为了三个阶段
Try尝试执行阶段完成所有业务可执行性的检查保障一致性并且预留好事务需要用到的所有业务资源保障隔离性
Confirm确认执行阶段不进行任何业务检查直接使用Try阶段准备的资源来完成业务处理。注意Confirm阶段可能会重复执行因此需要满足幂等性。
Cancel取消执行阶段释放Try阶段预留的业务资源。注意Cancel阶段也可能会重复执行因此也需要满足幂等性。
那么根据Fenixs Bookstore在线书店的场景事例TCC的执行过程应该是这样的
第一步最终用户向Fenixs Bookstore发送交易请求购买一本价值100元的《深入理解Java虚拟机》。
第二步创建事务生成事务ID记录在活动日志中进入Try阶段
用户服务检查业务可行性可行的话把该用户的100元设置为“冻结”状态通知下一步进入Confirm阶段不可行的话通知下一步进入Cancel阶段。
仓库服务检查业务可行性可行的话将该仓库的1本《深入理解Java虚拟机》设置为“冻结”状态通知下一步进入Confirm阶段不可行的话通知下一步进入Cancel阶段。
商家服务:检查业务可行性,不需要冻结资源。
第三步如果第二步中所有业务都反馈业务可行就将活动日志中的状态记录为Confirm进入Confirm阶段
用户服务完成业务操作扣减被冻结的100元
仓库服务完成业务操作标记那1本冻结的书为出库状态扣减相应库存
商家服务完成业务操作收款100元
第四步如果第三步的操作全部完成了事务就会宣告正常结束。而如果第三步中的任何一方出现了异常不论是业务异常还是网络异常都将会根据活动日志中的记录来重复执行该服务的Confirm操作即进行“最大努力交付”。
第五步如果是在第二步有任意一方反馈业务不可行或是任意一方出现了超时就将活动日志的状态记录为Cancel进入Cancel阶段
用户服务取消业务操作释放被冻结的100元
仓库服务取消业务操作释放被冻结的1本书
商家服务:取消业务操作(大哭一场后安慰商家谋生不易)。
第六步如果第五步全部完成了事务就会宣告以失败回滚结束。而如果第五步中的任何一方出现了异常不论是业务异常还是网络异常也都将会根据活动日志中的记录来重复执行该服务的Cancel操作即进行“最大努力交付”。
那么你从上述的操作执行过程中可以发现TCC其实有点类似于2PC的准备阶段和提交阶段但TCC是位于用户代码层面而不是在基础设施层面这就为它的实现带来了较高的灵活性我们可以根据需要设计资源锁定的粒度。
另外TCC在业务执行的时候只操作预留资源几乎不会涉及到锁和资源的争用所以它具有很高的性能潜力。
但是由于TCC的业务侵入性比较高需要开发编码配合在一定程度上增加了不少工作量也就给我们带来了一些使用上的弊端那就是我们需要投入更高的开发成本和更换事务实现方案的替换成本。
所以通常我们并不会完全靠裸编码来实现TCC而是会基于某些分布式事务中间件如阿里开源的Seata来完成以尽量减轻一些编码工作量。
现在你就已经知道了TCC事务具有较强的隔离性能够有效避免“超售”的问题而且它的性能可以说是包括可靠消息队列在内的几种柔性事务模式中最高的。但是TCC仍然不能满足所有的业务场景。
我在前面也提到了TCC最主要的限制是它的业务侵入性很强但并不是指由此给开发编码带来的工作量而是指它所要求的技术可控性上的约束。
比如说我们把这个书店的场景事例修改一下由于中国网络支付日益盛行在书店系统中现在用户和商家可以选择不再开设充值账号至少不会强求一定要先从银行充值到系统中才能进行消费而是允许在购物时直接通过U盾或扫码支付在银行账户中划转货款。
这个需求完全符合我们现在支付的习惯但这也给系统的事务设计增加了额外的限制如果用户、商家的账户余额由银行管理的话其操作权限和数据结构就不可能再随心所欲地自行定义了通常也就无法完成冻结款项、解冻、扣减这样的操作因为银行一般不会配合你的操作。所以在TCC的执行过程中第一步Try阶段往往就已经无法施行了。
那么我们就只能考虑采用另外一种柔性事务方案SAGA事务。
SAGA事务基于数据补偿代替回滚的解决思路
SAGA事务模式的历史十分悠久比分布式事务的概念提出还要更早。SAGA的意思是“长篇故事、长篇记叙、一长串事件”它起源于1987年普林斯顿大学的赫克托 · 加西亚 · 莫利纳Hector Garcia Molina和肯尼斯 · 麦克米伦Kenneth Salem在ACM发表的一篇论文《SAGAS》这就是论文的全名
文中提出了一种如何提升“长时间事务”Long Lived Transaction运作效率的方法大致思路是把一个大事务分解为可以交错运行的一系列子事务的集合。原本提出SAGA的目的是为了避免大事务长时间锁定数据库的资源后来才逐渐发展成将一个分布式环境中的大事务分解为一系列本地事务的设计模式。
SAGA由两部分操作组成。
一部分是把大事务拆分成若干个小事务将整个分布式事务T分解为n个子事务我们命名为T1T2TiTn。每个子事务都应该、或者能被看作是原子行为。如果分布式事务T能够正常提交那么它对数据的影响最终一致性就应该与连续按顺序成功提交子事务Ti等价。
另一部分是为每一个子事务设计对应的补偿动作我们命名为C1C2CiCn。Ti与Ci必须满足以下条件
Ti与Ci都具备幂等性
Ti与Ci满足交换律Commutative即不管是先执行Ti还是先执行Ci效果都是一样的
Ci必须能成功提交即不考虑Ci本身提交失败被回滚的情况如果出现就必须持续重试直至成功或者要人工介入。
如果T1到Tn均成功提交那么事务就可以顺利完成。否则我们就要采取以下两种恢复策略之一
正向恢复Forward Recovery如果Ti事务提交失败则一直对Ti进行重试直至成功为止最大努力交付。这种恢复方式不需要补偿适用于事务最终都要成功的场景比如在别人的银行账号中扣了款就一定要给别人发货。正向恢复的执行模式为T1T2Ti失败Ti重试Ti+1Tn。
反向恢复Backward Recovery如果Ti事务提交失败则一直执行Ci对Ti进行补偿直至成功为止最大努力交付。这里要求Ci必须在持续重试后执行成功。反向恢复的执行模式为T1T2Ti失败Ci补偿C2C1。
所以你能发现与TCC相比SAGA不需要为资源设计冻结状态和撤销冻结的操作补偿操作往往要比冻结操作容易实现得多。
我给你举个例子。我在前面提到的账户余额直接在银行维护的场景从银行划转货款到Fenixs Bookstore系统中这步是经由用户支付操作扫码或U盾来促使银行提供服务如果后续业务操作失败尽管我们无法要求银行撤销掉之前的用户转账操作但是作为补偿措施我们让Fenixs Bookstore系统将货款转回到用户账上却是完全可行的。
SAGA必须保证所有子事务都能够提交或者补偿但SAGA系统本身也有可能会崩溃所以它必须设计成与数据库类似的日志机制被称为SAGA Log以保证系统恢复后可以追踪到子事务的执行情况比如执行都到哪一步或者补偿到哪一步了。
另外你还要注意,尽管补偿操作通常比冻结/撤销更容易实现但要保证正向、反向恢复过程能严谨地进行也需要你花费不少的工夫。比如你可能需要通过服务编排、可靠事件队列等方式来完成。所以SAGA事务通常也不会直接靠裸编码来实现一般也是在事务中间件的基础上完成。我前面提到的Seata就同样支持SAGA事务模式。
还有SAGA基于数据补偿来代替回滚的思路也可以应用在其他事务方案上。举个例子阿里的GTSGlobal Transaction ServiceSeata由GTS开源而来所提出的“AT事务模式”就是这样的一种应用。
另一种应用模式AT事务
从整体上看AT事务是参照了XA两段提交协议来实现的但针对XA 2PC的缺陷即在准备阶段必须等待所有数据源都返回成功后协调者才能统一发出Commit命令而导致的[木桶效应](https://en.wikipedia.org/wiki/Liebigs_law_of_the_minimum)所有涉及到的锁和资源都需要等到最慢的事务完成后才能统一释放AT事务也设计了针对性的解决方案。
它大致的做法是在业务数据提交时自动拦截所有SQL分别保存SQL对数据修改前后结果的快照生成行锁通过本地事务一起提交到操作的数据源中这就相当于自动记录了重做和回滚日志。
如果分布式事务成功提交了那么我们后续只需清理每个数据源中对应的日志数据即可而如果分布式事务需要回滚就要根据日志数据自动产生用于补偿的“逆向SQL”。
所以基于这种补偿方式分布式事务中所涉及的每一个数据源都可以单独提交然后立刻释放锁和资源。AT事务这种异步提交的模式相比2PC极大地提升了系统的吞吐量水平。而使用的代价就是大幅度地牺牲了隔离性甚至直接影响到了原子性。因为在缺乏隔离性的前提下以补偿代替回滚不一定总能成功。
比如当在本地事务提交之后、分布式事务完成之前该数据被补偿之前又被其他操作修改过即出现了脏写Dirty Wirte而这个时候一旦出现分布式事务需要回滚就不可能再通过自动的逆向SQL来实现补偿只能由人工介入处理了。
一般来说,对于脏写我们是一定要避免的,所有传统关系数据库在最低的隔离级别上,都仍然要加锁以避免脏写。因为脏写情况一旦发生,人工其实也很难进行有效处理。
所以GTS增加了一个“全局锁”Global Lock的机制来实现写隔离要求本地事务提交之前一定要先拿到针对修改记录的全局锁后才允许提交而在没有获得全局锁之前就必须一直等待。
这种设计以牺牲一定性能为代价,避免了在两个分布式事务中,数据被同一个本地事务改写的情况,从而避免了脏写。
另外在读隔离方面AT事务默认的隔离级别是读未提交Read Uncommitted这意味着可能会产生脏读Dirty Read。读隔离也可以采用全局锁的方案来解决但直接阻塞读取的话我们要付出的代价就非常大了一般并不会这样做。
所以到这里,你其实能发现,分布式事务中并没有能一揽子包治百病的解决办法,你只有因地制宜地选用合适的事务处理方案,才是唯一有效的做法。
小结
通过上一讲和今天这节课的学习我们已经知道CAP定理决定了C与A不可兼得传统的ACID强一致性在分布式环境中要想能保证一致性C就不得不牺牲可用性A。那么这个时候随着分布式系统中节点数量的增加整个系统发生服务中断的概率和时间都会随之增长。
所以我们只能退而求其次把“最终一致性”作为分布式架构下事务处理的目标。在这两节课中我给你介绍的可靠事件队列、TCC和SAGA都是实现最终一致性的三种主流模式。
一课一思
请你思考并对比可靠事件队列、TCC和SAGA三种事务实现的优缺点然后来总结一下它们各自适用的场景。
欢迎在留言区分享你的思考和见解。 如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢阅读,我们下一讲再见。

View File

@@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 _ 域名解析系统优化HTTP性能的第一步
你好,我是周志明。从今天这节课开始,我们一起来学习下,如何引导流量分配到最合适的系统部件中进行响应。
那么在正式开始学习之前,我们先来了解下所谓的透明多级分流系统的定义。
理解透明多级分流系统的设计原则
我们都知道用户在使用信息系统的过程中请求首先是从浏览器出发在DNS的指引下找到系统的入口然后经过了网关、负载均衡器、缓存、服务集群等一系列设施最后接触到了系统末端存储于数据库服务器中的信息然后再逐级返回到用户的浏览器之中。
这个过程需要经过许许多多的技术部件。那么作为系统的设计者,我们应该意识到不同的设施、部件在系统中,都具有各自不同的价值:
有一些部件位于客户端或网络的边缘能够迅速响应用户的请求避免给后方的I/O与CPU带来压力典型的如本地缓存、内容分发网络、反向代理等。
有一些部件的处理能力能够线性拓展,易于伸缩,可以通过使用较小的代价堆叠机器,来获得与用户数量相匹配的并发性能,并且应尽量作为业务逻辑的主要载体,典型的如集群中能够自动扩缩的服务节点。
有一些部件的稳定服务,对系统运行具有全局性的影响,要时刻保持着容错备份,维护着高可用性,典型的如服务注册中心、配置中心。
有一些设施是天生的单点部件,只能依靠升级机器本身的网络、存储和运算性能来提升处理能力,比如位于系统入口的路由、网关或者负载均衡器(它们都可以做集群,但一次网络请求中无可避免至少有一个是单点的部件)、位于请求调用链末端的传统关系数据库等,都是典型的容易形成单点部件。
所以,在对系统进行流量规划时,我们需要充分理解这些部件的价值差异。这里,我认为有两个简单、普适的原则,能指导我们进行设计。
第一个原则是尽可能减少单点部件,如果某些单点是无可避免的,则应尽最大限度减少到达单点部件的流量。
用户的请求在系统中往往会有多个部件都能够处理响应比如要获取一张存储在数据库的用户头像图片浏览器缓存、内容分发网络、反向代理、Web服务器、文件服务器、数据库等都可能会提供这张图片。
所以,恰如其分地引导请求分流至最合适的组件中,避免绝大多数流量汇集到单点部件(如数据库),同时依然能够、或者在绝大多数时候能够保证处理结果的准确性,在单点系统出现故障时,仍能自动而迅速地实施补救措施,这便是系统架构中多级分流的意义。
那么,缓存、节流、主备、负载均衡等措施,就都是为了达成该目标所采用的工具与手段,而高可用架构、高并发架构,则是通过该原则所获得的价值。
许多介绍架构设计的资料呢,都会以“高可用、高并发架构”为主题,主要聚焦于流量到达服务端后,如何构建强大的服务端集群来应对。
而在这个小章节中,我们是以“透明多级分流系统”为主题,聚焦于流量从客户端发出,到达服务端处理节点前的过程,并会去了解在这个过程中对流量削峰填谷的基础设施与通用组件。
第二个原则是奥卡姆剃刀原则,它更为关键。
奥卡姆剃刀原则-
Entities should not be multiplied without necessity.-
如无必要,勿增实体。-
—— Occams RazorWilliam of Ockham
作为一个架构设计者,你应该对前面提到的多级分流的手段,有一个全面的理解与充分的准备。同时你也要清晰地意识到,这些设施并不是越多越好,在实际构建系统的时候,你要在有明确需求、真正有必要的时候,再去考虑部署它们。
因为并不是每一个系统都要追求高并发、高可用的,从系统的用户量、峰值流量和团队本身的技术与运维能力出发,来考虑如何布置这些设施,才是最合理的做法。
在能满足需求的前提下,最简单的系统就是最好的系统。
所以在这个章节的第一节课当中我们就先来学习一个相对简单但又是全世界最大规模的查询系统即DNS域名解析查询系统我们一起来看看它是如何实现多级分流的。
DNS的工作原理
我们都知道DNS的作用是把便于人类理解的域名地址转换为便于计算机处理的IP地址。
说到DNS我想到了一件事你可能会觉得有点儿好笑我在刚接触计算机网络有一小段时间以后一直都把DNS想像成是一个部署在世界上某个神秘机房里的大型电话本式的翻译服务。直到后来当我第一次了解到DNS的工作原理也知道了世界根域名服务器的ZONE文件只有2MB大小甚至可以打印出来物理备份的时候我就对DNS系统的设计惊叹得不得了。
域名解析对于大多数信息系统尤其是基于互联网的系统来说是必不可少的组件可是现在想想它在我们的开发工作里其实根本没有特别高的存在感通常它都是不会受到重点关注的设施。这就导致了很多程序员还不太了解DNS本身的工作过程以及它对系统流量能够施加的影响而且DNS本身就堪称是示范性的透明多级分流系统非常符合我们这个章节的主题也很值得我们去借鉴。
无论是使用浏览器还是在程序代码中访问某个网址域名如果没有缓存的话都会先经过DNS服务器的解析翻译找到域名对应的IP地址才能开始通讯。
后面我就以www.icyfenix.com.cn为例吧。这项操作是操作系统自动完成的一般不需要用户程序的介入。
不过DNS服务器并不是一次性地把www.icyfenix.com.cn直接解析成IP地址的这个解析需要经历一个递归的过程。
首先DNS会把域名还原为“www.icyfenix.com.cn.”。注意这里最后多了一个点“.”,它是“.root”的意思。早期的域名都必须得带这个点DNS才能正确解析不过现在几乎所有的操作系统、DNS服务器都可以自动补上结尾的点号了。
然后DNS就开始进行解析了。它的解析步骤是这样的
第一步客户端先检查本地的DNS缓存查看是否存在并且是存活着的该域名的地址记录。
DNS是以存活时间Time to LiveTTL来衡量缓存的有效情况的因此如果某个域名改变了IP地址它也无法去通知缓存了该地址的机器来更新或失效掉缓存只能依靠TTL超期后重新获取来保证一致性。后续每一级DNS查询的过程都会有类似的缓存查询操作所以我就不重复说了。
第二步客户端将地址发送给本机操作系统中配置的本地DNSLocal DNS。这个本地DNS服务器可以由用户手工设置也可以在DHCP分配时或者在拨号时从PPP服务器中自动获取。
第三步本地DNS收到查询请求后会按照“是否有www.icyfenix.com.cn的权威服务器”→“是否有icyfenix.com.cn的权威服务器”→“是否有com.cn的权威服务器”→“是否有cn的权威服务器”的顺序依次查询自己的地址记录。如果都没有查询到本地DNS就会一直找到最后点号代表的根域名服务器为止。
这个步骤里涉及了两个重要名词,你需要好好掌握:
权威域名服务器Authoritative DNS是指负责翻译特定域名的DNS服务器“权威”的意思就是说服务器决定了这个域名应该翻译出怎样的结果。DNS翻译域名的时候不需要像查电话本一样刻板地一对一翻译它可以根据来访机器、网络链路、服务内容等各种信息玩出很多花样。权威DNS的灵活应用在后面的内容分发网络、服务发现等课程内容中都还会涉及到。
根域名服务器Root DNS是指固定的、无需查询的顶级域名Top-Level Domain服务器可以默认为它们已内置在操作系统代码之中。全世界一共有13组根域名服务器注意并不是13台每一组根域名都通过任播的方式建立了一大群镜像根据维基百科的数据迄今已经超过1000台根域名服务器的镜像了之所以有13这个数字的限制是因为DNS主要是采用UDP传输协议在需要稳定性保证的时候也可以采用TCP来进行数据交换的未分片的UDP数据包在IPv4下最大有效值为512字节最多可以存放13组地址记录。
第四步现在假设本地DNS是全新的上面不存在任何域名的权威服务器记录所以当DNS查询请求按步骤3的顺序一直查到根域名服务器之后它将会得到“cn的权威服务器”的地址记录然后通过“cn的权威服务器”得到“com.cn的权威服务器”的地址记录以此类推最后找到能够解释www.icyfenix.com.cn的权威服务器地址。
第五步通过“www.icyfenix.com.cn的权威服务器”查询www.icyfenix.com.cn的地址记录。这里的地址记录并不一定就是指IP地址在RFC规范中有定义的地址记录类型已经多达几十种比如IPv4下的IP地址为A记录IPv6下的AAAA记录、主机别名CNAME记录等等。
我给你举一个例子。假设一个域名下配置了多条不同的A记录此时权威服务器就可以根据自己的策略来进行选择典型的应用是智能线路根据访问者所处的不同地区如华北、华南、东北、不同服务商如电信、联通、移动等因素来确定返回最合适的A记录将访问者路由到最合适的数据中心达到智能加速的目的。
可见DNS系统多级分流的设计就让DNS系统能够经受住全球网络流量不间断的冲击但它也并不是没有缺点。
典型的问题就是响应速度会受影响。在极端情况(如各级服务器均无缓存)下,域名解析可能会导致每个域名都必须递归多次才能查询到结果,显著影响传输的响应速度。
如下图所示你可以看到其高达310毫秒的DNS查询速度
首次DNS请求耗时
所以为了避免产生这种问题专门就有一种前端优化手段叫做DNS预取DNS Prefetching如果网站后续要使用来自于其他域的资源那就在网页加载时便生成一个link请求促使浏览器提前对该域名进行预解释如下所示
<link rel="dns-prefetch" href="//domain.not-icyfenx.cn">
而另一种可能更严重的缺陷就是DNS的分级查询意味着每一级都有可能受到中间人攻击的威胁产生被劫持的风险。
我们应该都知道要攻陷位于递归链条顶层的如根域名服务器cn权威服务器服务器和链路是非常困难的它们都有很专业的安全防护措施。但很多位于递归链底层的、或者来自本地运营商的Local DNS服务器安全防护就相对松懈甚至不少地区的运行商自己就会主动进行劫持专门返回一个错的IP通过在这个IP上代理用户请求以便给特定类型的资源主要是HTML注入广告进行牟利。
所以针对这种情况最近几年出现了一种新的DNS工作模式HTTPDNS也称为DNS over HTTPSDoH。它把原本的DNS解析服务开放为一个基于HTTPS协议的查询服务替代基于UDP传输协议的DNS域名解析通过程序代替操作系统直接从权威DNS或者可靠Local DNS获取解析数据从而绕过传统Local DNS。
这种做法的好处是完全免去了“中间商赚差价”的环节不再惧怕底层的域名劫持能有效避免Local DNS不可靠导致的域名生效缓慢、来源IP不准确、产生的智能线路切换错误等问题。
小结
这节课作为“透明多级分流系统”的第一讲我给你介绍了这个名字的意义与来由。在开发过程中没有太多存在感的DNS系统其实就很符合透明和多级分流的特点。所以我也以此为例给你简要介绍了它的工作原理。
根据请求从浏览器发出到最终查询或修改数据库的信息除了DNS以外还会有客户端浏览器、网络传输链路、内容分发网络、负载均衡器和缓存中间件这些位于服务器、数据库之外的组件可以帮助分担流量便于我们构建出更加高并发、高可用的系统。在后面的几节课中我们就会逐一来探讨它们的工作原理。
一课一思
思考一下,你开发的系统中,有使用过哪些分流手段?欢迎给我留言,分享你的做法。
如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,215 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 _ 客户端缓存是如何帮助服务器分担流量的?
你好,我是周志明。这节课,我们继续来讨论透明多级分流系统中,最靠近用户一侧的分流部件:浏览器的客户端缓存。
当万维网刚刚出现的时候浏览器的缓存机制差不多就已经存在了。在HTTP协议设计之初人们便确定了服务端与客户端之间“无状态”Stateless的交互原则即要求客户端的每次请求是独立的每次请求无法感知、也不能依赖另一个请求的存在这既简化了HTTP服务器的设计也为它的水平扩展能力留下了广阔的空间。
但无状态并不是只有好的一面。因为客户端的每次请求都是独立的,服务端不会保存之前请求的状态和资源,所以也不可避免地导致它会携带重复的数据,造成网络性能的降低。
那么HTTP协议针对这个问题的解决方案就是客户端缓存。从HTTP/1.0到1.1、再到2.0版本的演进中逐步形成了现在被称为“状态缓存”、“强制缓存”或简称为“强缓存”和“协商缓存”这三种HTTP缓存机制。
这其中的状态缓存是指不经过服务器客户端直接根据缓存信息来判断目标网站的状态。以前只有301/Moved Permanently永久重定向这一种后来在RFC6797中增加了HSTSHTTP Strict Transport Security机制用来避免依赖301/302跳转HTTPS时可能产生的降级中间人劫持问题在第28、29讲中我还会展开讲解这个问题这也属于另一种状态缓存。
因为状态缓存涉及的内容只有这么一点,所以后面,我们就只聚焦在强制缓存与协商缓存这两种机制的探讨上。
下面我们就先来看看强制缓存的实现机制吧。
实现强制缓存机制的两类Headers
就像“强制缓存”这个名字一样它对一致性问题的处理策略十分直接假设在某个时间点内比如服务器收到响应后的10分钟内资源的内容和状态一定不会被改变因此客户端可以不需要经过任何请求在该时间点到来之前一直持有和使用该资源的本地缓存副本。
根据约定,在浏览器的地址输入、页面链接跳转、新开窗口、前进和后退中,强制缓存都可以生效,但在用户主动刷新页面时应当自动失效。
在HTTP协议中设置了两类可以实现强制缓存的Headers标头Expires和Cache-Control。
第一类Expires
Expires是HTTP/1.0协议中开始提供的Header后面跟随了一个截止时间参数。当服务器返回某个资源时如果带有该Header的话就意味着服务器承诺在截止时间之前资源不会发生变动浏览器可直接缓存该数据不再重新发请求。我们直接来看一个Expires头的示例程序
HTTP/1.1 200 OK
Expires: Wed, 8 Apr 2020 07:28:00 GMT
那么你能看到Expires设计得非常直观易懂但它考虑得其实并不周全。我给你简单举几个例子。
受限于客户端的本地时间
比如,在收到响应后,客户端修改了本地时间,将时间点前后调整了几分钟,这就可能会造成缓存提前失效或超期持有。
无法处理涉及到用户身份的私有资源
比如合理的做法是某些资源被登录用户缓存在了自己的浏览器上。但如果被代理服务器或者内容分发网络CDN缓存起来就可能会被其他未认证的用户获取。
无法描述“不缓存”的语义
比如一般浏览器为了提高性能往往会自动在当次会话中缓存某些MINE类型的资源这会造成设计者不希望缓存的资源无法被及时更新。而在HTTP/1.0的设计中Expires并没有考虑这方面的需求导致无法强制浏览器不允许缓存某个资源。
所以以前为了实现这类功能我们通常不得不使用脚本或者手工在资源后面增加时间戳如“xx.js?t=1586359920”“xx.jpg?t=1586359350”来保证每次资源都会重新获取。
不过关于“不缓存”的语义在HTTP/1.0中其实是预留了“Pragma: no-cache”来表达的但在HTTP/1.0中并没有确切地描述Pragma参数的具体行为随后它就被HTTP/1.1中出现过的Cache-Control给替代了。
现在尽管主流的浏览器也通常都会支持Pragma但它的行为仍然是不确定的实际上并没有什么使用价值。而Cache-Control的出现进一步压缩了Pragma的生存空间。所以接下来我们就一起来看看它是如何支持强制缓存机制的实现的。
第二类Cache-Control
Cache-Control是HTTP/1.1协议中定义的强制缓存Header它的语义比起Expires来说就丰富了很多。而如果Cache-Control和Expires同时存在并且语义存在冲突比如Expires与max-age / s-maxage冲突的话IETF规定必须以Cache-Control为准。
同样这里我们也看看Cache-Control的使用示例
HTTP/1.1 200 OK
Cache-Control: max-age=600
那么你能看到这里的示例中使用的参数是max-age。实际上在客户端的请求Header或服务器的响应Header中Cache-Control都可以存在它定义了一系列的参数并且允许自行扩展即不在标准RFC协议中由浏览器自行支持的参数。Cache-Control标准的参数主要包括6种下面我就带你一一了解下。
max-age和s-maxage
在前面的示例中你会发现max-age后面跟随了一个数字它是以秒为单位的表明相对于请求时间在Date Header中会注明请求时间多少秒以内缓存是有效的资源不需要重新从服务器中获取。这个相对时间就避免了Expires中采用的绝对时间可能受客户端时钟影响的问题。
另一个类似的参数是s-maxage其中的“s”是“Share”的缩写意味着“共享缓存”的有效时间即允许被CDN、代理等持有的缓存有效时间这个参数主要是用来提示CDN这类服务器如何对缓存进行失效。
public和private
这一类参数表明了是否涉及到用户身份的私有资源。如果是public就意味着资源可以被代理、CDN等缓存如果是private就意味着只能由用户的客户端进行私有缓存。
no-cache和no-store
no-cache表明该资源不应该被缓存哪怕是同一个会话中对同一个URL地址的请求也必须从服务端获取从而令强制缓存完全失效但此时的协商缓存机制依然是生效的no-store不强制会话中是否重复获取相同的URL资源但它禁止浏览器、CDN等以任何形式保存该资源。
no-transform
no-transform禁止资源以任何形式被修改。比如某些CDN、透明代理支持自动GZip压缩图片或文本以提升网络性能而no-transform就禁止了这样的行为它要求Content-Encoding、Content-Range、Content-Type均不允许进行任何形式的修改。
min-fresh和only-if-cached
这两个参数是仅用于客户端的请求Header。min-fresh后续跟随了一个以秒为单位的数字用于建议服务器能返回一个不少于该时间的缓存资源即包含max-age且不少于min-fresh的数字only-if-cached表示服务器希望客户端不要发送请求只使用缓存来进行响应若缓存不能命中就直接返回503/Service Unavailable错误。
must-revalidate和proxy-revalidate
must-revalidate表示在资源过期后一定要从服务器中进行获取即超过了max-age的时间后就等同于no-cache的行为proxy-revalidate用于提示代理、CDN等设备资源过期后的缓存行为除对象不同外语义与must-revalidate完全一致。
好了,现在你应该就已经理解了强制缓存的实现机制了。但是,强制缓存是基于时效性的,无论是人还是服务器,在大多数情况下,其实都没有什么把握去承诺某项资源多久不会发生变化。
所以,接下来我就要给你介绍另一种基于变化检测的缓存机制,也就是协商缓存。它在处理一致性问题上,比强制缓存会有更好的表现。不过它需要一次变化检测的交互开销,在性能上就会略差一些。
协商缓存的两种变动检查机制
那么在开始了解协商缓存的实现机制之前你要先注意一个地方就是在HTTP中协商缓存与强制缓存并没有互斥性这两套机制是并行工作的。
比如说当强制缓存存在时客户端可以直接从强制缓存中返回资源无需进行变动检查而当强制缓存超过时效或者被禁止no-cache / must-revalidate协商缓存也仍然可以正常工作。
协商缓存有两种变动检查机制一种是根据资源的修改时间进行检查另一种是根据资源唯一标识是否发生变化来进行检查。它们都是靠一组成对出现的请求、响应Header来实现的。
根据资源的修改时间进行检查
我们先来看看根据资源的修改时间进行检查的协商缓存机制。它的语义中包含了两种标准参数Last-Modified和If-Modified-Since。
Last-Modified是服务器的响应Header用来告诉客户端这个资源的最后修改时间。
而对于带有这个Header的资源当客户端需要再次请求时会通过If-Modified-Since把之前收到的资源最后修改时间发送回服务端。
如果此时服务端发现资源在该时间后没有被修改过就只要返回一个304/Not Modified的响应即可无需附带消息体从而达到了节省流量的目的
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT
而如果此时服务端发现资源在该时间之后有变动就会返回200/OK的完整响应在消息体中包含最新的资源。
HTTP/1.1 200 OK
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT
Content
根据资源唯一标识是否发生变化来进行检查
我们再来看看“根据资源唯一标识是否发生变化来进行检查”的协商缓存机制。它的语义中也包含了两种标准参数Etag和If-None-Match。
Etag是服务器的响应Header用于告诉客户端这个资源的唯一标识。HTTP服务器可以根据自己的意愿来选择如何生成这个标识比如Apache服务器的Etag值就默认是对文件的索引节点INode、大小和最后修改时间进行哈希计算后而得到的。
然后对于带有这个Header的资源当客户端需要再次请求时就会通过If-None-Match把之前收到的资源唯一标识发送回服务端。
如果此时服务端计算后发现资源的唯一标识与上传回来的一致就说明资源没有被修改过同样也只需要返回一个304/Not Modified的响应即可无需附带消息体达到节省流量的目的
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"
而如果此时服务端发现资源的唯一标识有变动也一样会返回200/OK的完整响应在消息体中包含最新的资源。
HTTP/1.1 200 OK
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"
Content
另外我还想强调的是Etag是HTTP中一致性最强的缓存机制。
为什么会这么说呢?我直接给你举个例子。
前面我提到的Last-Modified参数它标注的最后修改只能精确到秒级而如果某些文件在一秒钟以内被修改多次的话它就不能准确标注文件的修改时间了又或者如果某些文件会被定期生成可能内容上并没有任何变化但Last-Modified却改变了导致文件无法有效使用缓存。而这些情况Last-Modified都有可能产生资源一致性的问题只能使用Etag解决。
但是Etag又是HTTP中性能最差的缓存机制。这个“最差”体现在每次请求时服务端都必须对资源进行哈希计算这比起简单获取一下修改时间开销要大了很多。
所以Etag和Last-Modified是允许一起使用的服务器会优先验证Etag在Etag一致的情况下再去对比Last-Modified这是为了防止有一些HTTP服务器没有把文件修改日期纳入哈希范围内。
HTTP的内容协商机制
那到这里为止HTTP的协商缓存机制就已经能很好地处理通过URL获取单个资源的场景了。不过你可能要问了为什么要强调“单个资源”呢
我们知道在HTTP协议的设计中一个URL地址是有可能提供多份不同版本的资源的比如说一段文字的不同语言版本一个文件的不同编码格式版本一份数据的不同压缩方式版本等等。因此针对请求的缓存机制也必须能够提供对应的支持。
所以针对这种情况HTTP协议设计了以Accept*Accept、Accept-Language、Accept-Charset、Accept-Encoding开头的一套请求Header以及对应的以Content-*Content-Language、Content-Type、Content-Encoding开头的响应Header。这些Headers被称为HTTP的内容协商机制。
那么与之对应的对于一个URL能够获取多个资源的场景中缓存同样也需要有明确的标识来获知它要根据什么内容来对同一个URL返回给用户正确的资源。这个就是Vary Header的作用Vary后面应该跟随一组其他Header的名字比如说
HTTP/1.1 200 OK
Vary: Accept, User-Agent
这里你要知道这个响应的含义是应该根据MINE类型和浏览器类型来缓存资源另外服务端在获取资源时也需要根据请求Header中对应的字段来筛选出适合的资源版本。
根据约定协商缓存不仅可以在浏览器的地址输入、页面链接跳转、新开窗口、前进、后退中生效而且在用户主动刷新页面F5时也同样是生效的。只有用户强制刷新Ctrl+F5或者明确禁用缓存比如在DevTools中设定时才会失效此时客户端向服务端发出的请求会自动带有“Cache-Control: no-cache”。
小结
HTTP协议以“无状态”作为基本的交互原则那么由此而来的资源重复访问问题就需要通过网络链路中的缓存来解决了。现在你也已经知道客户端缓存具体包括了“状态缓存”、“强制缓存”和“协商缓存”三类。
这节课,我们详细分析了强制缓存和协商缓存的工作原理。利用好客户端的缓存,能够节省大量网络流量,这是为后端系统分流,以实现更高并发的第一步。
一课一思
除了在HTTP协议中规定的缓存机制你还使用过其他客户端的缓存方式吗比如说浏览器WebAPI中的Cache Storage和Application Cache或者你是否通过LocalStorage、SessionStorage、IndexedDB、WebSQL等等去缓存信息
欢迎在留言区分享你的做法和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,277 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 _ 传输链路优化HTTP传输速度的小技巧
你好,我是周志明。
在经过了客户端缓存的节流和DNS服务的解析指引以后程序发出的请求流量就正式离开了客户端踏上以服务器为目的地的旅途了。而这个过程就是我们今天这节课要讨论的主角传输链路。
以优化链路传输为目的的前端设计原则未来或许不再适用
可能不少人的第一直觉都会认为,传输链路是完全不受开发者控制的因素,觉得网络路由跳点的数量、运营商铺设线路的质量,已经决定了线路带宽的大小、速率的高低。不过事实并非如此,程序发出的请求能否与应用层、传输层协议提倡的方式相匹配,对传输的效率也会有非常大的影响。
最容易体现出这点的就是那些前端网页的优化技巧。我们只要简单搜索一下就能找到很多以优化链路传输为目的的前端设计原则比如经典的雅虎YSlow-23条规则中就涵盖了很多与传输相关的内容。
下面我来给你简单举几个例子。
Minimize HTTP Requests
即减少请求数量:对于客户端发出的请求,服务器每次都需要建立通信链路进行数据传输,这些开销很昂贵,所以减少请求的数量,就可以有效地提高访问性能。如果你是做前端开发的,那你可能就听说过下面这几种减少请求数量的手段:
a. 雪碧图CSS Sprites-
b. CSS、JS文件合并/内联Concatenation / Inline-
c. 分段文档Multipart Document-
d. 媒体图片、音频内联Data Base64 URI-
e. 合并Ajax请求Batch Ajax Request-
f. ……
Split Components Across Domains
即扩大并发请求数现代浏览器Chrome、Firefox一般可以为每个域名支持6个IE为8-13个并发请求。如果你希望更快地加载大量图片或其他资源就需要进行域名分片Domain Sharding将图片同步到不同主机或者同一个主机的不同域名上。
GZip Components
即启用压缩传输:启用压缩能够大幅度减少需要在网络上传输内容的大小,节省网络流量。
Avoid Redirects
即避免页面重定向当页面发生了重定向就会延迟整个文档的传输。在HTML文档到达之前页面中不会呈现任何东西会降低用户的体验。
Put Stylesheets at the TopPut Scripts at the Bottom
即按重要性调节资源优先级将重要的、马上就要使用的、对客户端展示影响大的资源放在HTML的头部以便优先下载。
……
这些原则在今天暂且仍算得上是有一定价值,但如果在若干年后的未来再回头看它们,大概率其中的多数原则已经成了奇技淫巧,有些甚至成了反模式。
我为什么这么说呢这是因为HTTP协议还在持续发展从上世纪90年代的HTTP/1.0和HTTP/1.1到2015年发布的HTTP/2再到2019年的HTTP/3由于HTTP协议本身的变化造成了“适合HTTP传输的请求”的特征也在不断变化。
那么接下来,我们就来看看这里所说的特征变化指的都是什么。
连接数优化
我们知道HTTP特指HTTP/3以前是以TCP为传输层的应用层协议但HTTP over TCP这种搭配只能说是TCP目前在互联网中的统治性地位所造就的结果而不能说它们两者配合工作就是合适的。
为啥呢一方面你可以回想一下平常你在上网的时候平均每个页面停留的时间都有多长然后你也可以大概估算一下每个页面中包含的资源HTML、JS、CSS、图片等数量由此你其实就可以大致总结出HTTP传输对象的主要特征了那就是数量多、时间短、资源小、切换快。
另一方面TCP协议要求必须在三次握手完成之后才能开始数据传输这是一个可能高达“百毫秒”为计时尺度的事件另外TCP还有慢启动的特性这就会导致通信双方在刚刚建立连接时的传输速度是最低的后面再逐步加速直至稳定。
由于TCP协议本身是面向长时间、大数据传输来设计的所以只有在一段较长的时间尺度内TCP协议才能展现出稳定性和可靠性的优势不会因为建立连接的成本太高成为了使用瓶颈。
所以我才说HTTP over TCP这种搭配在目标特征上确实是有矛盾的以至于HTTP/1.x时代大量短而小的TCP连接导致了网络性能的瓶颈。
开发Tricks的使用困境
那么为了缓解HTTP与TCP之间的矛盾聪明的程序员们一面致力于减少发出的请求数量另一面也在致力于增加客户端到服务端的连接数量。这就是我在前面提到的Yslow规则中“Minimize HTTP Requests”与“Split Components Across Domains”两条优化措施的根本依据所在。
由此可见通过前端开发者的各种Tricks确实能够减少TCP连接数量的消耗这是有数据统计作为支撑的。
HTTP Archive就对最近5年来数百万个URL地址进行了采样并得出了一个结论页面平均请求没有改变的情况下桌面端下降3.8%移动端上升1.4%TCP连接正在持续且幅度较大地下降桌面端下降36.4%移动端下降28.6%)。我们一起来具体看看这个变化:
HTTP平均请求数量70余个没有明显变化
TCP连接数量约15个有明显下降趋势
但是这些开发Tricks除了可以节省TCP连接以外其实也给我们带来了不少的副作用比如说
如果你用CSS Sprites合并了多张图片就意味着在任何场景下哪怕你只用到了其中一张小图也必须要完整地加载整个大图片或者哪怕只有一张小图需要修改也都会导致整个大图的缓存失效。类似的样式、脚本等其他文件的合并也会造成同样的问题。
如果你使用了媒体内嵌除了要承受Base64编码导致传输容量膨胀1/3的代价以外Base64以8 bit表示6 bit数据也会无法有效利用缓存。
如果你合并了异步请求,就会导致所有请求的返回时间,都要受最慢的那个请求拖累,页面整体的响应速度会下降。
如果你把图片放到了不同子域下面将会导致更大的DNS解析负担而且浏览器对两个不同子域下的同一图片必须持有两份缓存这也使得缓存效率下降。
……
所以我们也不难看出一旦需要使用者通过各种Tricks来解决基于技术根基而出现的各种问题时就会导致TA再也没办法摆脱“两害相权取其轻”的困境。否则这就不是Tricks而是会成为一种标准的设计模式了。
连接复用技术的优势和缺陷
实际上HTTP的设计者们也不是没有尝试过在协议层面去解决连接成本过高的问题即使是HTTP协议的最初版本指HTTP/1.0忽略非正式的HTTP/0.9版本就已经支持HTTP/1.0中不是默认开启的HTTP/1.1中变为默认连接复用技术了也就是今天我们所熟知的持久连接Persistent Connection或者叫连接Keep-Alive机制。
它的原理是让客户端对同一个域名长期持有一个或多个不会用完即断的TCP连接。典型做法是在客户端维护一个FIFO队列每次取完数据之后的一段时间内不自动断开连接以便获取下一个资源时可以直接复用避免创建TCP连接的成本。
但是连接复用技术依然是不完美的最明显的副作用就是“队首阻塞”Head-of-Line Blocking问题。
我们来设想一下这样的场景浏览器有10个资源需要从服务器中获取这个时候它把10个资源放入队列入列顺序只能是按照浏览器预见这些资源的先后顺序来决定。但是如果这10个资源中的第1个就让服务器陷入了长时间运算状态会发生怎样的状况呢
答案是当它的请求被发送到服务端之后服务端就会开始计算而在运算结果出来之前TCP连接中并没有任何数据返回此时后面的9个资源都必须阻塞等待。
虽然说服务端可以并行处理另外9个请求比如第1个是复杂运算请求消耗CPU资源第2个是数据库访问消耗数据库资源第3个是访问某张图片消耗磁盘I/O资源等等这就很适合并行。
但问题是这些请求的处理结果无法及时发回给客户端服务端也不能哪个请求先完成就返回哪个更不可能把所有要返回的资源混杂到一起交叉传输。这是因为只使用一个TCP连接来传输多个资源的话一旦顺序乱了客户端就很难区分清楚哪个数据包归属哪个资源了。
因此在2014年IETF发布的RFC 7230中提出了名为“HTTP管道”HTTP Pipelining复用技术试图在HTTP服务器中也建立类似客户端的FIFO队列让客户端一次性将所有要请求的资源名单全部发给服务端由服务端来安排返回顺序管理传输队列。
不过,无论队列维护是在服务端还是在客户端,其实都无法完全避免队首阻塞的问题。这是因为很难在真正传输之前,完全精确地评估出传输队列中每一项的时间成本,但由于服务端能够较为准确地评估资源消耗情况,这样确实能更紧凑地安排资源传输,保证队列中两项工作之间尽量减少空隙,甚至可能做到并行化传输,从而提升链路传输的效率。
可是因为HTTP管道需要多方共同支持协调起来相当复杂因此推广得并不算成功。
后来队首阻塞问题一直持续到HTTP/2发布后才算是被比较完美地解决了。
解决方案HTTP/2的多路复用技术
在HTTP/1.x中HTTP请求就是传输过程中最小粒度的信息单位了所以如果将多个请求切碎再混杂在一块传输客户端势必难以分辨重组出有效信息。
而在HTTP/2中Frame才是最小粒度的信息单位它可以用来描述各种数据比如请求的Headers、Body或者是用来做控制标识如打开流、关闭流。
这里我说的流Stream是一个逻辑上的数据通道概念每个帧都附带有一个流ID以标识这个帧属于哪个流。这样在同一个TCP连接中传输的多个数据帧就可以根据流ID轻易区分出来在客户端就能毫不费力地将不同流中的数据重组出不同HTTP请求和响应报文来。
这项设计是HTTP/2的最重要的技术特征之一被称为HTTP/2 多路复用HTTP/2 Multiplexing技术。
HTTP/2的多路复用
这样有了多路复用的支持HTTP/2就可以对每个域名只维持一个TCP连接One Connection Per Origin来以任意顺序传输任意数量的资源。这样就既减轻了服务器的连接压力开发者也不用去考虑域名分片这种事情来突破浏览器对每个域名最多6个连接数的限制了。
而更重要的是没有了TCP连接数的压力客户端就不需要再刻意压缩HTTP请求了所有通过合并、内联文件无论是图片、样式、脚本以减少请求数的需求都不再成立甚至反而是徒增副作用的反模式。
当然我说这是反模式可能还会有一些前端开发者不同意觉得HTTP请求少一些总是好的。减少请求数量最起码还减少了传输中耗费的Headers。
这里我们必须要先承认一个事实在HTTP传输中Headers占传输成本的比重是相当地大对于许多小资源甚至可能出现Headers的容量比Body的还要大以至于在HTTP/2中必须专门考虑如何进行Header压缩的问题。
但实际上有这样几个因素就决定了即使是通过合并资源文件来减少请求数对节省Headers成本来说也并没有太大的帮助
Header的传输成本在Ajax尤其是只返回少量数据的请求请求中可能是比重很大的开销但在图片、样式、脚本这些静态资源的请求中一般并不占主要比重。
在HTTP/2中Header压缩的原理是基于字典编码的信息复用简而言之是同一个连接上产生的请求和响应越多动态字典积累得越全头部压缩效果也就越好。所以HTTP/2是单域名单连接的机制合并资源和域名分片反而对性能提升不利。
与HTTP/1.x相反HTTP/2本身反而变得更适合传输小资源了比如传输1000张10K的小图HTTP/2要比HTTP/1.x快但传输10张1000K的大图则应该HTTP/1.x会更快。这其中有TCP连接数量相当于多点下载的影响更多的是由于TCP协议可靠传输机制导致的一个错误的TCP包会导致所有的流都必须等待这个包重传成功而这个问题就是HTTP/3.0要解决的目标了。因此把小文件合并成大文件在HTTP/2下是毫无好处的。
传输压缩
了解了TCP连接数的各种优化机制之后我们再来讨论下在链路优化中除缓存、连接之外的另一个重要话题压缩。同时这也可以解决我们之前遗留的一个问题如何不以断开TCP连接为标志来判断资源已传输完毕
HTTP很早就支持了GZip压缩因为HTTP传输的主要内容比如HTML、CSS、Script等主要是文本数据因此对于文本数据启用压缩的收益是非常高的传输数据量一般会降至原有的20%左右。而对于那些不适合压缩的资源Web服务器则能根据MIME类型来自动判断是否对响应进行压缩。这样对于已经采用过压缩算法存储的资源比如JPEG、PNG图片就不会被二次压缩空耗性能了。
不过大概没有多少人想过压缩其实跟我们前面提到的用于节约TCP的持久连接机制是存在冲突的。
在网络时代的早期,服务器的处理能力还很薄弱,为了启用压缩,会把静态资源预先压缩为.gz文件的形式给存放起来。当客户端可以接受压缩版本的资源时请求的Header中包含Accept-Encoding: gzip就返回压缩后的版本响应的Header中包含Content-Encoding: gzip否则就返回未压缩的原版。这种方式被称为“静态预压缩”Static Precompression
而现代的Web服务器处理能力有了大幅提升已经没有人再采用这种麻烦的预压缩方式了都是由服务器对符合条件的请求在即将输出时进行“即时压缩”On-The-Fly Compression整个压缩过程全部在内存的数据流中完成不必等资源压缩完成再返回响应这样可以显著提高“首字节时间”Time To First ByteTTFB改善Web性能体验。
而这个过程中唯一不好的地方就是服务器再也没有办法给出Content-Length这个响应Header了。因为输出Header时服务器还不知道压缩后资源的确切大小。
那看到这里,你想明白即时压缩与持久连接的冲突在哪了吗?
实际上持久连接机制不再依靠TCP连接是否关闭来判断资源请求是否结束了。它会重用同一个连接以便向同一个域名请求多个资源。这样客户端就必须要有除了关闭连接之外的其他机制来判断一个资源什么时候才算是传递完毕。
这个机制最初在HTTP/1.0时就只有Content-Length即靠着请求Header中明确给出资源的长度传输到达该长度即宣告一个资源的传输已经结束。不过由于启用即时压缩后就无法给出Content-Length了如果是HTTP/1.0的话持久连接和即时压缩只能二选其一。事实上在HTTP/1.0中这两者都支持,却默认都是不启用的。
另外依靠Content-Length来判断传输结束的缺陷不仅只有即时压缩这一种场景对于动态内容Ajax、PHP、JSP等输出等应用场景服务器也同样无法事先得知Content-Length。
不过HTTP/1.1版本中已经修复了这个缺陷并增加了另一种“分块传输编码”Chunked Transfer Encoding的资源结束判断机制彻底解决了Content-Length与持久连接的冲突问题。
分块编码的工作原理相当简单在响应Header中加入“Transfer-Encoding: chunked”之后就代表这个响应报文将采用分块编码。此时报文中的Body需要改为用一系列“分块”来传输。每个分块包含十六进制的长度值和对应长度的数据内容长度值独占一行数据从下一行开始。最后以一个长度值为0的分块来表示资源结束。
给你举个具体的例子(例子来自于维基百科,为便于观察,只分块,未压缩):
HTTP/1.1 200 OK
Date: Sat, 11 Apr 2020 04:44:00 GMT
Transfer-Encoding: chunked
Connection: keep-alive
25
This is the data in the first chunk
1C
and this is the second one
3
con
8
sequence
0
根据分块长度就可以知道前两个分块包含显式的回车换行符CRLF即\r\n字符
"This is the data in the first chunk\r\n" (37 字符 => 十六进制: 0x25)
"and this is the second one\r\n" (28 字符 => 十六进制: 0x1C)
"con" (3 字符 => 十六进制: 0x03)
"sequence" (8 字符 => 十六进制: 0x08)
所以解码后的内容为:
This is the data in the first chunk
and this is the second one
consequence
另外这里你要知道的是一般来说Web服务器给出的数据分块大小应该但并不强制是一致的而不是像这个例子一样随意。
HTTP/1.1通过分块传输解决了即时压缩与持久连接并存的问题到了HTTP/2由于多路复用和单域名单连接的设计已经不需要再刻意去强调持久连接机制了但数据压缩仍然有节约传输带宽的重要价值。
快速UDP网络连接
OK那么到这里我们还需要明确一件事情就是HTTP是应用层协议而不是传输层协议它的设计原本并不应该过多地考虑底层的传输细节。从职责上来讲持久连接、多路复用、分块编码这些能力已经或多或少超过了应用层的范畴。
所以说要想从根本上改进HTTP就必须直接替换掉HTTP over TCP的根基即TCP传输协议这便是最新一代HTTP/3协议的设计重点。
推动替换TCP协议的先驱者并不是IETF而是Google公司。目前世界上只有Google公司具有这样的能力这并不是因为Google的技术实力雄厚而是由于它同时持有着占浏览器市场70%份额的Chrome浏览器与占移动领域半壁江山的Android操作系统。
2013年Google在它的服务器如Google.com、YouTube.com等及Chrome浏览器上同时启用了名为“快速UDP网络连接”Quick UDP Internet ConnectionsQUIC的全新传输协议。
在2015年Google将QUIC提交给了IETF并在IETF的推动下对QUIC进行重新规范化为以示区别业界习惯将此前的版本称为gQUIC规范化后的版本称为iQUIC使其不仅能满足HTTP传输协议日后还能支持SMTP、DNS、SSH、Telnet、NTP等多种其他上层协议。
2018年末IETF正式批准了HTTP over QUIC使用HTTP/3的版本号将其确立为最新一代的互联网标准。
那么你从QUIC的名字上就能看出它会以UDP协议作为基础。UDP协议没有丢包自动重传的特性因此QUIC的可靠传输能力并不是由底层协议提供的而是完全由自己来实现。由QUIC自己实现的好处是能对每个流能做单独的控制如果在一个流中发生错误协议栈仍然可以独立地继续为其他流提供服务。
这个特性对提高易出错链路的性能方面非常有用。因为在大多数情况下TCP协议接到数据包丢失或损坏通知之前可能已经收到了大量的正确数据但是在纠正错误之前其他的正常请求都会等待甚至被重发。这也是前面我在讲连接数优化的时候提到HTTP/2没能解决传输大文件慢的根本原因。
此外QUIC的另一个设计目标是面向移动设备的专门支持。
以前TCP、UDP传输协议在设计的时候根本不可能设想到今天移动设备盛行的场景因此肯定不会有任何专门的支持。而QUIC在移动设备上的优势就体现在网络切换时的响应速度上比如当移动设备在不同WiFi热点之间切换或者从WiFi切换到移动网络时如果使用TCP协议现存的所有连接都必定会超时、中断然后根据需要重新创建。这个过程会带来很高的延迟因为超时和重新握手都需要大量的时间。
为此QUIC提出了连接标识符的概念该标识符可以唯一地标识客户端与服务器之间的连接而无需依靠IP地址。这样在切换网络后只需向服务端发送一个包含此标识符的数据包就可以重用既有的连接。因为即使用户的IP地址发生了变化原始连接标识符依然是有效的。
到现在无论是TCP协议还是HTTP协议都已经存在了数十年的时间。它们积累了大量用户的同时也承载了很重的技术惯性。就算是Google和IETF要是想把HTTP从TCP迁移出去也不是件容易的事儿。最主要的一个问题就是互联网基础设施中的许多中间设备都只面向TCP协议去建造也只对UDP做很基础的支持有的甚至会完全阻止UDP的流量。
所以Google在Chromium的网络协议栈中同时启用了QUIC和传统TCP连接并在QUIC连接失败时能以零延迟回退到TCP连接尽可能让用户无感知地、逐步地扩大QUIC的使用面。
根据W3Techs的数据截至2020年10月全球已经有48.9%的网站支持了HTTP/2协议按照维基百科中的记录这个数字在2019年6月份的时候还只是36.5%。另外在HTTP/3方面今天也已经有了7.2%的使用比例。
由此我们可以肯定地说,目前网络链路传输这个领域正处于新旧交替的时代,许多既有的设备、程序、知识,都会在未来几年的时间里出现重大更新。
小结
这节课我们一起了解了HTTP传输的优化技巧。HTTP的意思就是超文本传输协议但它并不是只将内容顺利传输到客户端就算完成任务了其中如何做到高效、无状态也是很重要的目标。
另外你还要记住的是在HTTP/2之前要想在应用一侧优化传输就必须要同时在其他方面付出相应的成本而HTTP/2中的多路复用、头压缩等改进项就从根本上给出了传输优化的解决方案。
一课一思
HTTP/2和HTTP/3已分别在2015年、2018年末成为正式标准你参与的项目是否有使用到这些新的标准呢对此你有什么思考和发现
欢迎给我留言,分享你的看法。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,219 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 _ 如何利用内容分发网络来提高网络性能?
你好,我是周志明。
前面几讲中我给你介绍了客户端缓存、域名解析、链路优化这三种与客户端关系较密切的传输优化机制。这节课我们来讨论一个针对这三种机制的经典综合运用案例内容分发网络CDNContent Distribution Network或Content Delivery Network
内容分发网络是一种十分古老的应用你应该也听说过它的名字多少知道它是用来做什么的。简单理解的话CDN其实就是做“内容分销”工作的。
我给你举个例子吧。假设我们把某个互联网系统比喻为一家开门营业的企业那内容分发网络就是它遍布世界各地的分支销售机构。如果一位客户要买一块CPU我们要是订机票飞到美国Intel总部去采购那肯定是不合适的到本地电脑城找个装机铺才是正常人的做法。所以在这个场景里内容分发网络就相当于电脑城那吆喝着CPU三十块钱一斤的本地经销商。
然后,内容分发网络又是一种十分透明的应用,一般不需要我们参与它的工作过程。所以我想,如果你没有自己亲身使用和专门研究过,那可能就不太清楚它是如何为互联网站点分流的,也不太会注意到它的工作原理是什么。
实际上,内容分发网络的工作过程,主要涉及到路由解析、内容分发、负载均衡和它所能支持的应用内容四个方面。今天这节课,我们先来了解内容分发网络可以解决哪些网络传输问题,也就是先着重探讨除负载均衡以外的其他三个方面的工作。在下一讲中,我会专门跟你讨论负载均衡的内容。
好,如果忽略其他影响服务质量的因素,仅从网络传输的角度来看,一个互联网系统的速度快慢,主要取决于以下四点因素:
网站服务器接入网络运营商的链路所能提供的出口带宽。
用户客户端接入网络运营商的链路所能提供的入口带宽。
从网站到用户之间,经过的不同运营商之间互联节点的带宽。一般来说,两个运营商之间只有固定的若干个点是互通的,所有跨运营商之间的交互都要经过这些点。
从网站到用户之间的物理链路传输时延。你要是爱打游戏的话应该就很清楚了延迟Ping值通常比带宽更重要。
以上四个网络问题,除了第二个只能由用户掏腰包,装个更好的宽带才能够解决之外,其余三个都能通过内容分发网络来改善。
所以说,一个运作良好的内容分发网络,能为互联网系统解决跨运营商、跨地域物理距离所导致的时延问题,也能给网站流量带宽起到分流、减负的作用。
举个例子如果没有遍布全国乃至全世界的阿里云CDN网络支持哪怕把整个杭州所有网民的上网权利都剥夺了把带宽全部让给淘宝的机房恐怕也撑不住双十一全国甚至是全球用户的疯狂围殴。
那么接下来我们就从CDN工作流程的第一步“路由解析”开始来全面了解下CDN是如何进行网络加速的。
路由解析
在第16讲我给你介绍DNS域名解析的时候提到过翻译域名不需要像查电话本一样刻板地一对一翻译DNS可以根据来访机器、网络链路、服务内容等各种信息玩出很多花样。
而内容分发网络将用户请求路由到它的资源服务器上其实就是依靠DNS服务器来实现的。
那么根据我们现在对DNS域名解析的了解一次没有内容分发网络参与的用户访问它的解析过程应该是这样的
即查询icyfenix.cn的请求发送至本地DNS后会递归查询直至找到能够解析icyfenix.cn地址的权威DNS服务器最终把解析结果返回给浏览器。
而有了内容分发网络的介入,这个解析过程会发生什么变化呢?
我们不妨先来看一段对网站“icyfenix.cn”进行DNS查询的真实应答记录这个网站就是通过国内的内容分发网络来给位于GitHub Pages上的静态页面加速的。
通过dig或者host命令我们就能很方便地得到DNS服务器的返回结果结果中头4个IP的城市地址是我手工加入的后面的其他记录就不一个一个查了如下所示
$ dig icyfenix.cn
; <<>> DiG 9.11.3-1ubuntu1.8-Ubuntu <<>> icyfenix.cn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60630
;; flags: qr rd ra; QUERY: 1, ANSWER: 17, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;icyfenix.cn. IN A
;; ANSWER SECTION:
icyfenix.cn. 600 IN CNAME icyfenix.cn.cdn.dnsv1.com.
icyfenix.cn.cdn.dnsv1.com. 599 IN CNAME 4yi4q4z6.dispatch.spcdntip.com.
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 101.71.72.192 #浙江宁波市
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 113.200.16.234 #陕西省榆林市
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 116.95.25.196 #内蒙古自治区呼和浩特市
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 116.178.66.65 #新疆维吾尔自治区乌鲁木齐市
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 118.212.234.144
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 211.91.160.228
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 211.97.73.224
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 218.11.8.232
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 221.204.166.70
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 14.204.74.140
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 43.242.166.88
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 59.80.39.110
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 59.83.204.12
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 59.83.204.14
4yi4q4z6.dispatch.spcdntip.com. 60 IN A 59.83.218.235
;; Query time: 74 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Sat Apr 11 22:33:56 CST 2020
;; MSG SIZE rcvd: 152
那么根据这个解析信息我们可以知道DNS服务为icyfenix.cn的查询结果先返回了一个CNAME记录icyfenxi.cn.cdn.dnsv1.com”,服务器在递归查询该CNAME时候返回了另一个看起来更奇怪的CNAME4yi4q4z6.dispatch.spcdntip.com”。继续查询后这个CNAME返回了十几个位于全国不同地区的A记录
很明显这些A记录就是分布在全国各地存有本站缓存的CDN节点由此我们就能清晰地了解到CDN路由解析的具体工作过程了
架设好icyfenix.cn的服务器后将服务器的IP地址在你的CDN服务商上注册为源站”,注册后你会得到一个CNAME也就是这个例子当中的icyfenxi.cn.cdn.dnsv1.com”。
接着将得到的CNAME在你购买域名的DNS服务商上注册为一条CNAME记录
当第一位用户来访问你的站点时会首先发生一次未命中缓存的DNS查询域名服务商解析出CNAME后会返回给本地DNS到这里后续的链路解析的主导权就开始由内容分发网络的调度服务接管了
本地DNS查询CNAME时由于能解析该CNAME的权威服务器只有CDN服务商所架设的权威DNS这个DNS服务会根据一定的均衡策略和参数比如拓扑结构容量时延等等在全国各地能提供服务的CDN缓存节点中挑选一个最适合的把它的IP替换成源站的IP地址然后返回给本地DNS
浏览器从本地DNS拿到了IP地址后就会把该IP当作源站服务器来进行访问此时该IP的CDN节点上可能有也可能没有缓存过源站的资源这一点我们马上会在讲内容分发的部分展开讨论)。
最后经过内容分发后的CDN节点就有能力代替源站向用户提供所请求的资源了
那么把前面解析的这个步骤反映在时序图上会是什么样子的呢你可以参考我在这里给出的图例然后对比一下我在前面所给出的没有CDN参与的时序图看看它们都有什么不同之处
好了现在我们就已经了解了CDN中路由解析的工作流程了下面我们一起来看看CDN加速的核心内容分发
内容分发
我们已经知道在DNS服务器的协助下无论是对用户还是服务器内容分发网络都可以是完全透明的在两者都不知情的情况下由CDN的缓存节点接管用户向服务器发出的资源请求
但随之而来的问题就是缓存节点中必须要有用户想要请求的资源副本才可能代替源站来响应用户请求而这里面又包括了两个子问题:“如何获取源站资源如何管理更新资源”。
所以CDN是如何解决这两个问题的呢
首先对于如何获取源站资源这个问题CDN获取源站资源的过程被称为内容分发”,“内容分发网络的名字也正是由此而来的可见这是CDN的核心价值
那么在内容分发的过程中我们可以采取两种主流的内容分发方式
第一种主动分发Push
顾名思义主动分发就是由源站主动发起将内容从源站或者其他资源库推送到用户边缘的各个CDN缓存节点上这个推送的操作没有什么业界标准可循我们可以采用任何传输方式如HTTPFTPP2P等)、任何推送策略如满足特定条件定时人工等)、任何推送时间只要与我后面要说的更新策略相匹配即可
不过你要注意由于主动分发通常需要源站CDN服务双方提供的程序API接口层面的配合所以它对源站并不是透明的只对用户一侧单向透明
另外主动分发的方式一般是用于网站要预载大量资源的场景比如双十一之前的一段时间内淘宝京东等各个网络商城就会开始把未来活动中需要用到的资源推送到CDN缓存节点中特别常用的资源甚至会直接缓存到你的手机App的存储空间或者浏览器的localStorage上
第二种被动回源Pull
被动回源就是指由用户访问所触发的全自动双向透明的资源缓存过程当某个资源首次被用户请求的时候CDN缓存节点如果发现自己没有该资源就会实时从源站中获取这时资源的响应时间可粗略认为是资源从源站到CDN缓存节点的时间再加上资源从CDN发送到用户的时间之和
所以被动回源的首次访问通常是比较慢的但由于CDN的网络条件一般远高于普通用户并不一定就会比用户直接访问源站更慢不适合应用于数据量较大的资源
但是被动回源也有优点就是它可以做到完全的双向透明不需要源站在程序上做任何的配合使用起来非常方便
这种分发方式是小型站点使用CDN服务的主流选择如果你不是自建CDN而是购买阿里云腾讯云的CDN服务的站点它们多数采用的就是这种方式
其次对于CDN如何管理更新资源这个问题同样也没有统一的标准可言尽管在HTTP协议中关于缓存的Header定义中确实是有对CDN这类共享缓存的一些指引性参数比如Cache-Control的s-maxage但是否要遵循完全取决于CDN本身的实现策略
而且更令人感到无奈的是由于大多数网站的开发和运维人员并不十分了解HTTP缓存机制所以就导致了如果CDN完全照着HTTP Headers来控制缓存失效和更新效果反而会更差而且还可能会引发其他的问题所以CDN缓存的管理没有通用的准则
现在最常见的管理更新资源的做法是超时被动失效与手工主动失效相结合
超时失效是指给予缓存资源一定的生存期超过了生存期就在下次请求时重新被动回源一次而手工失效是指CDN服务商一般会给程序调用提供失效缓存的接口在网站更新时由持续集成的流水线自动调用该接口来实现缓存更新比如icyfenix.cn就是依靠Travis-CI的持续集成服务来触发CDN失效和重新预热的
CDN应用
内容分发网络最初是为了快速分发静态资源而设计的但今天的CDN能做到的事情已经远远超越了开始建设时的目标所以下面我想带你来了解一下现在的CDN都可以做到什么它都有哪些应用这里我先说明一下我不会把CDN的各种应用全部展开细说而是只做个简要地列举说明我的目的是想帮你建立一个总体的关于CDN的认知只要理解和掌握了CDN的原理相信你也能发掘出许多这里没有列举的应用
加速静态资源
这是CDN本职工作
安全防御
在广义上你可以把CDN看作是你网站的堡垒机源站只对CDN提供服务然后由CDN来服务外界的其他用户这样恶意攻击者就不容易直接威胁源站CDN对防御某些攻击手段如DDoS攻击等尤其有效
但你也需要注意的是把安全性都寄托在CDN上本身其实是不安全的一旦源站的真实IP被泄露就会面临很高的风险
协议升级
不少CDN提供商都同时对接代售CA的SSL证书服务这样就可以实现源站是HTTP协议的而对外开放的网站是基于HTTPS的
同理这样的做法也可以实现源站到CDN是HTTP/1.x协议而CDN提供的外部服务是HTTP/2或HTTP/3协议或者是实现源站是基于IPv4网络的CDN提供的外部服务支持IPv6网络等等
状态缓存
在第17讲我介绍客户端缓存的时候简要提到了状态缓存的实现机制即不经过服务器客户端直接根据缓存信息来判断目标网站的状态而CDN不仅可以缓存源站的资源还可以缓存源站的状态比如源站的301/302转向就可以缓存起来让客户端直接跳转还可以通过CDN开启HSTS通过CDN进行OCSP装订来加速SSL证书访问等等
另外有一些情况下我们甚至可以配置CDN对任意状态码如404进行一定时间的缓存以减轻源站压力但这个操作你要慎重注意要在网站状态发生改变时去及时刷新缓存
修改资源
CDN可以在给用户返回资源的时候修改它的任何内容以实现不同的目的比如说可以对源站未压缩的资源自动压缩并修改Content-Encoding以节省用户的网络带宽消耗可以针对源站未启用客户端缓存的内容加上缓存Header来自动启用客户端缓存可以修改CORS的相关Header给源站不支持跨域的资源提供跨域能力等等
访问控制
CDN可以实现IP黑/白名单功能比如根据不同的来访IP提供不同的响应结果根据IP的访问流量来实现QoS控制根据HTTP的Referer来实现防盗链等等
注入功能
CDN可以在不修改源站代码的前提下为源站注入各种功能举个例子下图是国际CDN巨头CloudFlare提供的Google AnalyticsPACEHardenize等第三方应用这些原本需要在源站中注入代码的应用在CDN下都可以做到无需修改源站任何代码即可使用
小结
CDN是一种已经存在了很长时间也被人们广泛应用的分流系统它能为互联网系统提供性能上的加速也能帮助增强许多功能比如说我今天所讲的安全防御资源修改功能注入等等
而且这一切又实现得极为透明可以完全不需要我们这样的开发者来配合甚至可以在我们不知情的情况下完成以至于CDN没什么存在感虽然我们可能都说听过它但却没有真正了解过它所以学完了这一讲你应该就对CDN有更全面的理解了
另外CDN本身就是透明多级分流系统的一个优秀范例我希望你不仅可以学会CDN本身的功能与运作原理而且可以在实际的工作中将这种透明多级分流的思路应用于不同的场景构建出更加健壮能应对更大流量的系统
一课一思
除了我们已经介绍到的DNS和CDN你还了解软件业界里哪些常见的系统符合透明多级分流的特征呢欢迎在留言区分享出来
如果你觉得有收获也欢迎把今天的内容分享给更多的朋友感谢你的阅读我们下一讲再见

View File

@@ -0,0 +1,261 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 _ 常见的四层负载均衡的工作模式是怎样的?
你好,我是周志明。
在上节课我们学习了利用CDN来加速网络性能的工作内容包括路由解析、内容分发、负载均衡和它所支持的应用。其中负载均衡是相对独立的内容它不仅在CDN方面有应用在大量软件系统的生产部署中也都离不开负载均衡器的支持。所以今天这节课我们就一起来了解下负载均衡器的作用与原理。
在互联网时代的早期,网站流量还比较小,业务也比较简单,使用单台服务器基本就可以满足访问的需要了。但时至今日,互联网也好,企业级也好,一般实际用于生产的系统,几乎都离不开集群部署了。
一方面,不管是采用单体架构多副本部署还是微服务架构,也不管是为了实现高可用还是为了获得高性能,信息系统都需要利用多台机器来扩展服务能力,希望用户的请求不管连接到哪台机器上,都能得到相同的处理。
另一方面,如何构建和调度服务集群这件事情,又必须对用户一侧保持足够的透明,即使请求背后是由一千台、一万台机器来共同响应的,这也都不是用户会关心的事情,用户需要记住的只有一个域名地址而已。
那么这里承担了调度后方的多台机器以统一的接口对外提供服务的技术组件就是“负载均衡器”Load Balancer了。
真正的大型系统的负载均衡过程往往是多级的。比如在各地建有多个机房或者是机房有不同网络链路入口的大型互联网站然后它们会从DNS解析开始通过“域名” → “CNAME” → “负载调度服务” → “就近的数据中心入口”的路径先根据IP地址或者其他条件将来访地用户分配到一个合适的数据中心当中然后才到了我们马上要讨论的各式负载均衡。
这里我先跟你说明一下在DNS层面的负载均衡的工作模式与我在前几讲中介绍的DNS智能线路、内容分发网络等的工作原理是类似的它们之间的差别只是数据中心能提供的不仅是缓存而是全方位的服务能力。所以这种负载均衡的工作模式我就不再重复介绍了后面我们主要聚焦在讨论网络请求进入数据中心入口之后的其他级次的负载均衡。
好,那么接下来,我们就先从负载均衡的实现形式开始了解吧。
负载均衡的两种形式
实际上,无论我们在网关内部建立了多少级的负载均衡,从形式上来说都可以分为两种:四层负载均衡和七层负载均衡。
那么,在详细介绍它们是什么、如何工作之前,我们先来建立两个总体的、概念性的印象:
四层负载均衡的优势是性能高,七层负载均衡的优势是功能强。
做多级混合负载均衡,通常应该是低层的负载均衡在前,高层的负载均衡在后(你可以先想一想为什么?)。
这里我们所说的“四层”“七层”一般指的是经典的OSI七层模型中第四层传输层和第七层应用层。你可以参考下面表格中给出的维基百科上对OSI七层模型的介绍我们在后面还会多次使用它。
另外我还想说明一点就是现在人们所说的“四层负载均衡”其实是多种均衡器工作模式的统称。“四层”的意思是说这些工作模式的共同特点是都维持着同一个TCP连接而不是说它就只工作在第四层。
事实上这些模式主要都是工作在二层数据链路层可以改写MAC地址和三层上网络层可以改写IP地址单纯只处理第四层传输层可以改写TCP、UDP等协议的内容和端口的数据无法做到负载均衡的转发因为OSI的下面三层是媒体层Media Layers上面四层是主机层Host Layers。所以既然流量都已经到达目标主机上了也就谈不上什么流量转发最多只能做代理了。
但出于习惯和方便,现在几乎所有的资料都把它们统称为四层负载均衡,这里我也就遵循习惯,同样称呼它为四层负载均衡。而如果你在某些资料上,看见“二层负载均衡”“三层负载均衡”的表述,这就真的是在描述它们工作的层次了,和我这里讲的“四层负载均衡”并不是同一类意思。
常见的四层负载均衡的工作模式
好,回到我们这一讲的重点上来,我们一起来看看几种常见的四层负载均衡的工作模式都是怎样的。
数据链路层负载均衡
这里你可以参考前面的OSI模型表格数据链路层传输的内容是数据帧Frame比如我们常见的以太网帧、ADSL宽带的PPP帧等。当然了在我们所讨论的具体上下文里探究的目标必定就是以太网帧了。按照IEEE 802.3标准最典型的1500 Bytes MTU的以太网帧结构如下表所示
阅读链接补充:-
802.1Q 标签-
以太类型-
冗余校验-
帧间距
在这个帧结构中其他数据项的含义你可以暂时不去理会只需要注意到“MAC目标地址”和“MAC源地址”两项即可。
我们知道每一块网卡都有独立的MAC地址而以太帧上的这两个地址告诉了交换机此帧应该是从连接在交换机上的哪个端口的网卡发出送至哪块网卡的。
数据链路层负载均衡所做的工作是修改请求的数据帧中的MAC目标地址让用户原本是发送给负载均衡器的请求的数据帧被二层交换机根据新的MAC目标地址转发到服务器集群中对应的服务器后面都叫做“真实服务器”Real Server的网卡上这样真实服务器就获得了一个原本目标并不是发送给它的数据帧。
由于二层负载均衡器在转发请求过程中只修改了帧的MAC目标地址不涉及更上层协议没有修改Payload的数据所以在更上层第三层看来所有数据都是没有被改变过的。
这是因为第三层的数据包也就是IP数据包中包含了源客户端和目标均衡器的IP地址只有真实服务器保证自己的IP地址与数据包中的目标IP地址一致这个数据包才能被正确处理。
所以我们在使用这种负载均衡模式的时候需要把真实物理服务器集群所有机器的虚拟IP地址Virtual IP AddressVIP配置成跟负载均衡器的虚拟IP一样这样经均衡器转发后的数据包就能在真实服务器中顺利地使用。
另外也正是因为实际处理请求的真实物理服务器IP和数据请求中的目的IP是一致的所以响应结果就不再需要通过负载均衡服务器进行地址交换我们可以把响应结果的数据包直接从真实服务器返回给用户的客户端避免负载均衡器网卡带宽成为瓶颈所以数据链路层的负载均衡效率是相当高的。
整个请求到响应的过程如下图所示:
那么这里你就可以发现数据链路层负载均衡的工作模式是只有请求会经过负载均衡器而服务的响应不需要从负载均衡器原路返回整个请求、转发、响应的链路形成了一个“三角关系”。所以这种负载均衡模式也被很形象地称为“三角传输模式”Direct Server ReturnDSR也有人叫它是“单臂模式”Single Legged Mode或者“直接路由”Direct Routing
不过,虽然数据链路层负载均衡的效率很高,但它并不适用于所有的场合。除了那些需要感知应用层协议信息的负载均衡场景它无法胜任外(所有的四层负载均衡器都无法胜任,这个我后面介绍七层负载均衡器时会一并解释),在网络一侧受到的约束也很大。
原因是二层负载均衡器直接改写目标MAC地址的工作原理决定了它与真实服务器的通讯必须是二层可达的。通俗地说就是它们必须位于同一个子网当中无法跨VLAN。
所以,这个优势(效率高)和劣势(不能跨子网)就共同决定了,数据链路层负载均衡最适合用来做数据中心的第一级均衡设备,用来连接其他的下级负载均衡器。
好,我们再来看看第二种常见的四层负载均衡工作模式:网络层负载均衡。
网络层负载均衡
根据OSI七层模型我们可以知道在第三层网络层传输的单位是分组数据包Packets这是一种在分组交换网络Packet Switching NetworkPSN中传输的结构化数据单位。
我拿IP协议来给你举个例子吧。一个IP数据包由Headers和Payload两部分组成Headers长度最大为60 Bytes它是由20 Bytes的固定数据和最长不超过40 Bytes的可选数据组成的。按照IPv4标准一个典型的分组数据包的Headers部分的结构是这样的
同样我们也不需要过多关注表中的其他信息只要知道在IP分组数据包的Headers带有源和目标的IP地址即可。
源和目标IP地址代表了“数据是从分组交换网络中的哪台机器发送到哪台机器的”所以我们就可以沿用与二层改写MAC地址相似的思路通过改变这里面的IP地址来实现数据包的转发。
具体有两种常见的修改方式:
第一种是保持原来的数据包不变新创建一个数据包把原来数据包的Headers和Payload整体作为另一个新的数据包的Payload在这个新数据包的Headers中写入真实服务器的IP作为目标地址然后把它发送出去。
如此经过三层交换机的转发当真实服务器收到数据包后就必须在接收入口处设计一个针对性的拆包机制把由负载均衡器自动添加的那层Headers扔掉还原出原来的数据包来进行使用。
这样真实服务器就同样拿到了一个原本不是发给它目标IP不是它的数据包从而达到了流量转发的目的。
在那个时候还没有流行起“禁止套娃”的梗所以设计者给这种“套娃式”的传输起名为“IP隧道”IP Tunnel传输也是相当的形象了。
当然尽管因为要封装新的数据包IP隧道的转发模式比起直接路由的模式效率会有所下降但因为它并没有修改原有数据包中的任何信息所以IP隧道的转发模式仍然具备三角传输的特性即负载均衡器转发来的请求可以由真实服务器去直接应答无需再经过均衡器原路返回。
而且因为IP隧道工作在网络层所以可以跨越VLAN也就摆脱了我前面所讲的直接路由模式中网络侧的约束。现在我们来看看这种转发模式从请求到响应的具体过程
不过这种转发模式也有缺点就是它要求真实服务器必须得支持“IP隧道协议”IP Encapsulation也就是它得学会自己拆包扔掉一层Headers。当然这个其实并不是什么大问题现在几乎所有的Linux系统都支持IP隧道协议。
可另一个问题是这种模式仍然必须通过专门的配置必须保证所有的真实服务器与均衡器有着相同的虚拟IP地址。因为真实服务器器在回复该数据包的时候需要使用这个虚拟IP作为响应数据包的源地址这样客户端在收到这个数据包的时候才能正确解析。
这个限制就相对麻烦了一些它跟“透明”的原则冲突了需由系统管理员去专门介入。而且并不是在任何情况下我们都可以对服务器进行虚拟IP的配置的尤其是当有好几个服务共用一台物理服务器的时候。
那么在这种情况下我们就必须考虑第二种改变目标数据包的方式直接把数据包Headers中的目标地址改掉修改后原本由用户发给均衡器的数据包也会被三层交换机转发送到真实服务器的网卡上而且因为没有经过IP隧道的额外包装也就无需再拆包了。
但问题是这种模式是修改了目标IP地址才到达真实服务器的而如果真实服务器直接把应答包发回给客户端的话这个应答数据包的源IP是真实服务器的IP也就是均衡器修改以后的IP地址那么客户端就不可能认识该IP自然也就无法再正常处理这个应答了。
因此我们只能让应答流量继续回到负载均衡负载均衡把应答包的源IP改回自己的IP然后再发给客户端这样才能保证客户端与真实服务器之间正常通信。
如果你对网络知识有些了解的话肯定会觉得这种处理似曾相识这不就是在家里、公司、学校上网的时候由一台路由器带着一群内网机器上网的“网络地址转换”Network Address TranslationNAT操作吗
这种负载均衡的模式的确就被称为NAT模式。此时负载均衡器就是充当了家里、公司、学校的上网路由器的作用。
NAT模式的负载均衡器运维起来也十分简单只要机器把自己的网关地址设置为均衡器地址就不需要再进行任何额外设置了。
我们来看看这种工作模式从请求到响应的过程:
不过这里你还要知道的是在流量压力比较大的时候NAT模式的负载均衡会带来较大的性能损失比起直接路由和IP隧道模式甚至会出现数量级上的下降。
这个问题也是显而易见的因为由负载均衡器代表整个服务集群来进行应答各个服务器的响应数据都会互相争抢均衡器的出口带宽。这就好比在家里用NAT上网的话如果有人在下载你打游戏可能就会觉得卡顿是一个道理此时整个系统的瓶颈很容易就出现在负载均衡器上。
不过还有一种更加彻底的NAT模式就是均衡器在转发时不仅修改目标IP地址连源IP地址也一起改了这样源地址就改成了均衡器自己的IP。这种方式被叫做Source NATSNAT
这样做的好处是真实服务器连网关都不需要配置了,它能让应答流量经过正常的三层路由,回到负载均衡器上,做到了彻底的透明。
但它的缺点是由于做了SNAT真实服务器处理请求时就无法拿到客户端的IP地址了在真实服务器的视角看来所有的流量都来自于负载均衡器这样有一些需要根据目标IP进行控制的业务逻辑就无法进行了。
应用层负载均衡
前面我介绍的两种四层负载均衡的工作模式都属于“转发”即直接将承载着TCP报文的底层数据格式IP数据包或以太网帧转发到真实服务器上此时客户端到响应请求的真实服务器维持着同一条TCP通道。
但工作在四层之后的负载均衡模式就无法再进行转发了只能进行代理。此时正式服务器、负载均衡器、客户端三者之间是由两条独立的TCP通道来维持通讯的。
那么,转发与代理之间的具体区别是怎样的呢?我们来看一个图例:
首先,“代理”这个词,根据“哪一方能感知到”的原则,可以分为“正向代理”“反向代理”和“透明代理”三类。
正向代理就是我们通常简称的代理,意思就是在客户端设置的、代表客户端与服务器通讯的代理服务。它是客户端可知,而对服务器是透明的。
反向代理是指设置在服务器这一侧,代表真实服务器来与客户端通讯的代理服务。此时它对客户端来说是透明的。
透明代理是指对双方都透明的,配置在网络中间设备上的代理服务。比如,架设在路由器上的透明翻墙代理。
那么根据这个定义很显然七层负载均衡器就属于反向代理中的一种如果只论网络性能七层负载均衡器肯定是无论如何比不过四层负载均衡器的。毕竟它比四层负载均衡器至少要多一轮TCP握手还有着跟NAT转发模式一样的带宽问题而且通常要耗费更多的CPU因为可用的解析规则远比四层丰富。
所以说,如果你要用七层负载均衡器去做下载站、视频站这种流量应用,一定是不合适的,起码它不能作为第一级均衡器。
但是,如果网站的性能瓶颈并不在于网络性能,而是要论整个服务集群对外所体现出来的服务性能,七层负载均衡器就有它的用武之地了。这里,七层负载均衡器的底气就来源于,它是工作在应用层的,可以感知应用层通讯的具体内容,往往能够做出更明智的决策,玩出更多的花样来。
我举个生活中的例子。
四层负载均衡器就像是银行的自助排号机,转发效率高且不知疲倦,每一个达到银行的客户都可以根据排号机的顺序,选择对应的窗口接受服务;而七层负载均衡器就像银行的大堂经理,他会先确认客户需要办理的业务,再安排排号。这样,办理理财、存取款等业务的客户,可以根据银行内部资源得到统一的协调处理,加快客户业务办理流程;而有些无需柜台办理的业务,由大堂经理直接就可以解决了。
比如说,反向代理的工作模式就能够实现静态资源缓存,对于静态资源的请求就可以在反向代理上直接返回,无需转发到真实服务器。
这里关于代理的工作模式,相信你应该是比较熟悉的,所以这里关于七层负载均衡器的具体工作过程我就不详细展开了。下面我来列举一些七层代理可以实现的功能,让你能对它“功能强大”有个直观的感受:
在上一讲我介绍CDN应用的时候就提到过所有CDN可以做的缓存方面的工作就是除去CDN根据物理位置就近返回这种优化链路的工作外七层负载均衡器全都可以实现比如静态资源缓存、协议升级、安全防护、访问控制等等。
七层负载均衡器可以实现更智能化的路由。比如根据Session路由以实现亲和性的集群根据URL路由实现专职化服务此时就相当于网关的职责甚至根据用户身份路由实现对部分用户的特殊服务如某些站点的贵宾服务器等等。
某些安全攻击可以由七层负载均衡器来抵御。比如一种常见的DDoS手段是SYN Flood攻击即攻击者控制众多客户端使用虚假IP地址对同一目标大量发送SYN报文。从技术原理上看因为四层负载均衡器无法感知上层协议的内容这些SYN攻击都会被转发到后端的真实服务器上而在七层负载均衡器下这些SYN攻击自然就会在负载均衡设备上被过滤掉不会影响到后面服务器的正常运行。类似地我们也可以在七层负载均衡器上设定多种策略比如过滤特定报文以防御如SQL注入等应用层面的特定攻击手段。
很多微服务架构的系统中链路治理措施都需要在七层中进行比如服务降级、熔断、异常注入等等。我举个例子一台服务器只有出现物理层面或者系统层面的故障导致无法应答TCP请求才能被四层负载均衡器感知到进而剔除出服务集群而如果一台服务器能够应答只是一直在报500错那四层负载均衡器对此是完全无能为力的只能由七层负载均衡器来解决。
均衡策略与实现
好,现在你应该也能知道,负载均衡的两大职责是“选择谁来处理用户请求”和“将用户请求转发过去”。那么到这里为止,我们只介绍了后者,即请求的转发或代理过程。
而“选择谁来处理用户请求”是指均衡器所采取的均衡策略,这一块因为涉及的均衡算法太多,我就不一一展开介绍了。所以接下来,我想从功能和应用的角度,来给你介绍一些常见的均衡策略,你可以在自己的实践当中根据实际需求去配置选择。
轮循均衡Round Robin
即每一次来自网络的请求会轮流分配给内部中的服务器从1到N然后重新开始。这种均衡算法适用于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。
权重轮循均衡Weighted Round Robin
即根据服务器的不同处理能力给每个服务器分配不同的权值使其能够接受相应权值数的服务请求。比如服务器A的权值被设计成1B的权值是3C的权值是6则服务器A、B、C将分别接收到10%、30、60的服务请求。这种均衡算法能确保高性能的服务器得到更多的使用率避免低性能的服务器负载过重。
随机均衡Random
即把来自客户端的请求随机分配给内部中的多个服务器。这种均衡算法在数据足够大的场景下,能达到相对均衡的分布。
权重随机均衡Weighted Random
这种均衡算法类似于权重轮循算法,不过在处理请求分担的时候,它是个随机选择的过程。
一致性哈希均衡Consistency Hash
即根据请求中的某些数据可以是MAC、IP地址也可以是更上层协议中的某些参数信息作为特征值来计算需要落在哪些节点上算法一般会保证同一个特征值每次都一定落在相同的服务器上。这里一致性的意思就是保证当服务集群的某个真实服务器出现故障的时候只影响该服务器的哈希而不会导致整个服务集群的哈希键值重新分布。
响应速度均衡Response Time
即负载均衡设备对内部各服务器发出一个探测请求如Ping然后根据内部中各服务器对探测请求的最快响应时间来决定哪一台服务器来响应客户端的服务请求。这种均衡算法能比较好地反映服务器的当前运行状态但要注意这里的最快响应时间仅仅指的是负载均衡设备与服务器间的最快响应时间而不是客户端与服务器间的最快响应时间。
最少连接数均衡Least Connection
客户端的每一次请求服务在服务器停留的时间可能会有比较大的差异。那么随着工作时间加长如果采用简单的轮循或者随机均衡算法每一台服务器上的连接进程可能会产生极大的不平衡并没有达到真正的负载均衡。所以最少连接数均衡算法就会对内部中需要负载的每一台服务器都有一个数据记录也就是记录当前该服务器正在处理的连接数量当有新的服务连接请求时就把当前请求分配给连接数最少的服务器使均衡更加符合实际情况负载也能更加均衡。这种均衡算法适合长时间处理的请求服务比如FTP传输。
…………
另外,从实现角度来看,负载均衡器的实现有“软件均衡器”和“硬件均衡器”两类。
在软件均衡器方面又分为直接建设在操作系统内核的均衡器和应用程序形式的均衡器两种。前者的代表是LVSLinux Virtual Server后者的代表有Nginx、HAProxy、KeepAlived等等前者的性能会更好因为它不需要在内核空间和应用空间中来回复制数据包而后者的优势是选择广泛使用方便功能不受限于内核版本。
在硬件均衡器方面往往会直接采用应用专用集成电路Application Specific Integrated CircuitASIC来实现。因为它有专用处理芯片的支持可以避免操作系统层面的损耗从而能够达到最高的性能。这类的代表就是著名的F5和A10公司的负载均衡产品。
小结
这节课,我给你介绍了数据链路层负载均衡和网络层负载均衡的基本原理。对于一个普通的开发人员来说,可能平常不太接触这些偏向底层网络的知识,但如果你要对软件系统工作有全局的把握,进阶成为一名架构人员,那么即使不会去实际参与网络拓扑设计与运维,至少也必须理解它们的工作原理,这是系统做流量和容量规划的必要基础。
一课一思
请你思考一下:为什么负载均衡不能只在某一个网络层次中完成,而是要进行多级混合的负载均衡?另外做多级混合负载均衡,为什么应该是低层的负载均衡在前,高层的负载均衡在后?
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,284 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 _ 服务端缓存的三种属性
你好,我是周志明。
在透明多级分流系统这个小章节中我们的研究思路是以流量从客户端中发出开始以流量到达服务器集群中真正处理业务的节点作为结束一起探索了在这个过程中与业务无关的一些通用组件包括DNS、CDN、客户端缓存等等。
实际上,服务端缓存也是一种通用的技术组件,它主要用于减少多个客户端相同的资源请求,缓解或降低服务器的负载压力。所以,说它是一种分流手段也是很合理的。
另外,我们其实很难界定服务端缓存到底算不算与业务逻辑无关,因为服务端缓存通常是在代码中被显式调用的,这就很难说它是“透明分流”了。但是,服务端缓存作为流量到达服务端实际处理逻辑之前的最后一道防御线,把它作为这个小章节的最后一讲,倒也是合适的。
所以这节课,我就带你来了解下服务端缓存的相关知识点,你可以从中理解和掌握缓存的三种常见属性,然后灵活运用在自己的软件开发当中。
好,接下来,我们就从引入缓存的价值开始学起吧。
为系统引入缓存的理由
关于服务端缓存,首先你需要明确的问题是,在为你的系统引入缓存之前,它是否真的需要缓存呢?
我们很多人可能都会有意无意地把硬件里那种常用于区分不同产品档次、“多多益善”的缓存如CPU L1/2/3缓存、磁盘缓存等等代入到软件开发中去。但实际上这两者的差别是很大的。毕竟服务端缓存是程序的一部分而硬件缓存是一种硬件对软件运行效率的优化手段。
在软件开发中,引入缓存的负面作用要明显大于硬件的缓存。主要有这样几个原因:
从开发角度来说引入缓存会提高系统的复杂度因为你要考虑缓存的失效、更新、一致性等问题硬件缓存也有这些问题只是不需要由你来考虑主流的ISA也都没有提供任何直接操作缓存的指令
从运维角度来说,缓存会掩盖掉一些缺陷,让问题在更久的时间以后,出现在距离发生现场更远的位置上;
从安全角度来说,缓存可能泄漏某些保密数据,这也是容易受到攻击的薄弱点。
那么,冒着前面提到的这种种风险,你还是想要给系统引入缓存,是为了什么呢?其实无外乎有两种理由。
第一种为了缓解CPU压力而做缓存。
比如说把方法运行结果存储起来、把原本要实时计算的内容提前算好、把一些公用的数据进行复用等等这些引入缓存的做法都可以节省CPU算力顺带提升响应性能。
第二种为了缓解I/O压力而做缓存。
比如说,通过引入缓存,把原本对网络、磁盘等较慢介质的读写访问,变为对内存等较快介质的访问;把原本对单点部件(如数据库)的读写访问,变为对可扩缩部件(如缓存中间件)的访问,等等,也顺带提升了响应性能。
这里请你注意缓存虽然是典型的以空间换时间来提升性能的手段但它的出发点是缓解CPU和I/O资源在峰值流量下的压力“顺带”而非“专门”地提升响应性能。
所以我的言外之意就是如果你可以通过增强CPU、I/O本身的性能比如扩展服务器的数量来满足需要的话那升级硬件往往是更好的解决方案。即使需要你掏腰包多花一点儿钱那通常也比引入缓存带来的风险更低。
这样,当你有了使用服务端缓存的明确目的后,下一步就是要如何选择缓存了。所以接下来,我们就一起讨论一下,设计或者选择缓存时要考虑哪些方面的属性。
缓存属性
其实不少软件系统最初的缓存功能都是以HashMap或者ConcurrentHashMap为起点开始的演进的。当我们在开发中发现系统中某些资源的构建成本比较高而这些资源又有被重复使用的可能性那很自然就会产生“循环再利用”的想法把它们放到Map容器中下次需要时取出重用避免重新构建。这种原始朴素的复用就是最基本的缓存了。
不过,一旦我们专门把“缓存”看作是一项技术基础设施,一旦它有了通用、高效、可统计、可管理等方面的需求,那么我们需要考虑的因素就会变得复杂起来了。通常我们在设计或者选择缓存时,至少需要考虑以下四个维度的属性:
吞吐量缓存的吞吐量使用OPS值每秒操作数Operations per Secondops/s来衡量它反映了对缓存进行并发读、写操作的效率即缓存本身的工作效率高低。
命中率:缓存的命中率即成功从缓存中返回结果次数与总请求次数的比值,它反映了引入缓存的价值高低,命中率越低,引入缓存的收益越小,价值越低。
扩展功能:缓存除了基本读写功能外,还提供了一些额外的管理功能,比如最大容量、失效时间、失效事件、命中率统计,等等。
分布式支持:缓存可以分为“进程内缓存”和“分布式缓存”两大类,前者只为节点本身提供服务,无网络访问操作,速度快但缓存的数据不能在各个服务节点中共享。后者则相反。
在今天这节课,我们就先来探讨下前三个属性(下一讲我们会重点讨论分布式缓存)。
吞吐量
首先你要知道缓存的吞吐量只在并发场景中才有统计的意义因为不考虑并发的话即使是最原始的、以HashMap实现的缓存访问效率也已经是常量时间复杂度即O(1)。其中主要涉及到碰撞、扩容等场景的处理,这些都是属于数据结构基础知识,我就不展开讲了。
但HashMap并不是线程安全的容器如果要让它在多线程并发下能正确地工作就要用Collections.synchronizedMap进行包装这相当于给Map接口的所有访问方法都自动加上了全局锁或者我们也可以改用ConcurrentHashMap来实现这相当于给Map的访问分段加锁从JDK 8起已取消分段加锁改为CAS+Synchronized锁单个元素
而无论采用怎样的实现方法,线程安全措施都会带来一定的吞吐量损失。
所以进一步说如果我们只比较吞吐量完全不去考虑命中率、淘汰策略、缓存统计、过期失效等功能该如何实现那也不必去选择哪种缓存容器更好了JDK 8改进之后的ConcurrentHashMap基本上就是你能找到的吞吐量最高的缓存容器了。
可是,在很多场景里,前面提到的这些功能至少有一两项是必须的,我们不可能完全不考虑。所以,这就涉及到了不同缓存方案的权衡问题。
根据Caffeine给出的一组目前业界主流进程内缓存的实现方案其中包括了Caffeine、ConcurrentLinkedHashMap、LinkedHashMap、Guava Cache、Ehcache和Infinispan Embedded等缓存组件库的对比。从它们在8线程、75%读操作、25%写操作下的吞吐量表现Benchmarks来看各种缓存组件库的性能差异还是十分明显的最高与最低相差了足有一个数量级你可以参考下图
8线程、75%读、25%写的吞吐量比较
其中你可以发现在这种并发读写的场景中吞吐量会受多方面因素的共同影响。比如说怎样设计数据结构以尽可能避免数据竞争、存在竞争风险时怎样处理同步主要有使用锁实现的悲观同步和使用CAS实现的乐观同步、如何避免伪共享现象False Sharing这也算是典型的用缓存提升开发复杂度的例子发生等等。
其中的第一点,“尽可能避免数据竞争”是最关键的。因为无论我们如何实现同步,都不会比直接不需要同步更快。
那么下面我就以Caffeine为例来给你介绍一些缓存如何避免竞争、提高吞吐量的设计方法。
我们知道,缓存中最主要的数据竞争来源于读取数据的同时,也会伴随着对数据状态的写入操作,而写入数据的同时,也会伴随着数据状态的读取操作。
比如说读取数据时服务器要同时更新数据的最近访问时间和访问计数器的状态后面讲命中率时会提到为了追求高效程序可能不会记录时间和次数比如通过调整链表顺序来表达时间先后、通过Sketch结构来表达热度高低以实现缓存的淘汰策略又或者在读取时服务器要同时判断数据的超期时间等信息以实现失效重加载等其他扩展功能。
那么,针对前面所讲的伴随读写操作而来的状态维护,我们可以选择两种处理思路。
一种是以Guava Cache为代表的同步处理机制。即在访问数据时一并完成缓存淘汰、统计、失效等状态变更操作通过分段加锁等优化手段来尽量减少数据竞争。
另一种是以Caffeine为代表的异步日志提交机制。这种机制参考了经典的数据库设计理论它把对数据的读、写过程看作是日志即对数据的操作指令的提交过程。
尽管日志也涉及到了写入操作而有并发的数据变更就必然面临着锁竞争。但是异步提交的日志已经将原本在Map内的锁转移到了日志的追加写操作上日志里腾挪优化的余地就比在Map中要大得多。
另外在Caffeine的实现中还设有专门的环形缓存区Ring Buffer也常称作Circular Buffer来记录由于数据读取而产生的状态变动日志。而且为了进一步减少数据竞争Caffeine给每条线程对线程取Hash哈希值相同的使用同一个缓冲区都设置了一个专用的环形缓冲。
额外知识:环形缓冲-
所谓环形缓冲并不是Caffeine的专有概念它是一种拥有读、写两个指针的数据复用结构在计算机科学中有非常广泛的应用。-
我给你举个具体例子。比如说一台计算机通过键盘输入并通过CPU读取“HELLO WIKIPEDIA”这个长14字节的单词那么通常就需要一个至少14字节以上的缓冲区才行。-
但如果是环形缓冲结构,读取和写入就应当一起进行,在读取指针之前的位置都可以重复使用。理想情况下,只要读取指针不落后于写入指针一整圈,这个缓冲区就可以持续工作下去,就能容纳无限多个新字符。否则,就必须阻塞写入操作,去等待读取清空缓冲区。
环形缓存区工作原理
然后从Caffeine读取数据时数据本身会在其内部的ConcurrentHashMap中直接返回而数据的状态信息变更就存入了环形缓冲中由后台线程异步处理。-
而如果异步处理的速度跟不上状态变更的速度,导致缓冲区满了,那此后接收的状态的变更信息就会直接被丢弃掉,直到缓冲区重新有了富余。-
所以通过环形缓冲和容忍有损失的状态变更Caffeine大幅降低了由于数据读取而导致的垃圾收集和锁竞争因而Caffeine的读取性能几乎能与ConcurrentHashMap的读取性能相同。-
另外你要知道在向Caffeine写入数据时还要求要使用传统的有界队列ArrayQueue来存放状态变更信息写入带来的状态变更是无损的不允许丢失任何状态。这是考虑到许多状态的默认值必须通过写入操作来完成初始化因此写入会有一定的性能损失。根据Caffeine官方给出的数据相比ConcurrentHashMapCaffeine在写入时大约会慢10%左右。
好,说完了吞吐量,我们接着来看看缓存的第二个属性:命中率。
命中率与淘汰策略
有限的物理存储,决定了任何缓存的容量都不可能是无限的,所以缓存需要在消耗空间与节约时间之间取得平衡,这就要求缓存必须能够自动、或者由人工淘汰掉缓存中的低价值数据。不过,由人工管理的缓存淘汰主要取决于开发者如何编码,不能一概而论,所以这里我们就只讨论由缓存自动进行淘汰的情况。
这里我所说的“缓存如何自动地实现淘汰低价值目标”,现在也被称之为缓存的淘汰策略,或者是替换策略、清理策略。
那么,在缓存实现自动淘汰低价值数据的容器之前,我们首先要定义,怎样的数据才算是“低价值”的数据。
由于缓存的通用性,这个问题的答案必须是与具体业务逻辑无关的,所以我们只能从缓存工作过程中收集到的统计结果,来确定数据是否有价值。这个通用的统计结果包括但不限于数据何时进入缓存、被使用过多少次、最近什么时候被使用,等等。
这就由此决定了,一旦确定了选择何种统计数据,以及如何通用地、自动地判定缓存中每个数据价值高低,也就相当于决定了缓存的淘汰策略是如何实现的。
那么目前,最基础的淘汰策略实现方案主要有三种,我来一一给你介绍下。
第一种FIFOFirst In First Out
即优先淘汰最早进入被缓存的数据。FIFO的实现十分简单但一般来说它并不是优秀的淘汰策略因为越是频繁被用到的数据往往越会早早地被存入缓存之中。所以如果采用这种淘汰策略很可能会大幅降低缓存的命中率。
第二种LRULeast Recent Used
即优先淘汰最久未被使用访问过的数据。LRU通常会采用HashMap加LinkedList的双重结构如LinkedHashMap来实现。也就是它以HashMap来提供访问接口保证常量时间复杂度的读取性能以LinkedList的链表元素顺序来表示数据的时间顺序在每次缓存命中时把返回对象调整到LinkedList开头每次缓存淘汰时从链表末端开始清理数据。
所以你也能发现对大多数的缓存场景来说LRU都明显要比FIFO策略合理尤其适合用来处理短时间内频繁访问的热点对象。但相反它的问题是如果一些热点数据在系统中经常被频繁访问但最近一段时间因为某种原因未被访问过那么这时这些热点数据依然要面临淘汰的命运LRU依然可能错误淘汰掉价值更高的数据。
第三种LFULeast Frequently Used
即优先淘汰最不经常使用的数据。LFU会给每个数据添加一个访问计数器每访问一次就加1当需要淘汰数据的时候就清理计数器数值最小的那批数据。
LFU可以解决前面LRU中热点数据间隔一段时间不访问就被淘汰的问题但同时它又引入了两个新的问题。
第一个问题是需要对每个缓存的数据专门去维护一个计数器,每次访问都要更新,在前面讲“吞吐量”的时候,我也解释了这样做会带来高昂的维护开销;第二个问题是不便于处理随时间变化的热度变化,比如某个曾经频繁访问的数据现在不需要了,它也很难自动被清理出缓存。
可见,缓存淘汰策略会直接影响缓存的命中率,没有一种策略是完美的、能够满足全部系统所需的。
不过随着淘汰算法的发展近几年的确出现了许多相对性能要更好、也更为复杂的新算法。下面我就以LFU分支为例针对它存在的这两个问题给你讲讲近年来提出的TinyLFU和W-TinyLFU算法都分别带来了什么样的优化效果。
TinyLFUTiny Least Frequently Used
TinyLFU是LFU的改进版本。为了缓解LFU每次访问都要修改计数器所带来的性能负担TinyLFU首先采用Sketch结构来分析访问数据。
所谓的Sketch它实际上是统计学中的概念即指用少量的样本数据来估计全体数据的特征。这种做法显然牺牲了一定程度的准确性但是只要样本数据与全体数据具有相同的概率分布Sketch得出的结论仍不失为一种在高效与准确之间做好权衡的有效结论。
所以借助CountMin Sketch算法可以看作是布隆过滤器的一种等价变种结构TinyLFU可以用相对小得多的记录频率和空间来近似地找出缓存中的低价值数据。
另外为了解决LFU不便于处理随时间变化的热度变化问题TinyLFU采用了基于“滑动时间窗”在第38讲中我们会更详细地分析这种算法的热度衰减算法。简单理解就是每隔一段时间便会把计数器的数值减半以此解决“旧热点”数据难以清除的问题。
W-TinyLFUWindows-TinyLFU
W-TinyLFU又是TinyLFU的改进版本。TinyLFU在实现减少计数器维护频率的同时也带来了无法很好地应对稀疏突发访问的问题。
所谓的稀疏突发访问是指有一些绝对频率较小但突发访问频率很高的数据比如某些运维性质的任务也许一天、一周只会在特定时间运行一次其余时间都不会用到那么此时TinyLFU就很难让这类元素通过Sketch的过滤因为它们无法在运行期间积累到足够高的频率。
而应对短时间的突发访问是LRU的强项因此W-TinyLFU就结合了LRU和LFU两者的优点。从整体上看它是LFU策略从局部实现上看它又是LRU策略。
怎么理解这个“整体”和“局部”呢?
W-TinyLFU的具体做法是把新记录暂时放入一个名为Window Cache的前端LRU缓存里面让这些对象可以在Window Cache中累积热度如果能通过TinyLFU的过滤器再进入名为Main Cache的主缓存中存储。
主缓存根据数据的访问频繁程度分为了不同的段LFU策略实际上W-TinyLFU只分了两段但单独某一段从局部来看又是基于LRU策略去实现的称为Segmented LRU。每当前一段缓存满了之后就会将低价值数据淘汰到后一段中去存储直至最后一段也满了之后该数据就彻底清理出缓存。
当然只靠这种简单的、有限的介绍你不一定能完全理解TinyLFU和W-TinyLFU的工作原理但是你肯定能看出来这些改进算法比起原来基础版本的LFU要复杂许多。
有时候,为了取得理想的效果,采用较为复杂的淘汰策略只是不得已的选择。
除了W-TinyLFU之外Caffeine官方还制定了另外两种高级淘汰策略ARCAdaptive Replacement Cache和LIRSLow Inter-Reference Recency Set。这里你可以看看这三种新的淘汰策略与基础的LFU策略之间的命中率对比
几种淘汰算法在搜索场景下的命中率对比
在搜索场景中三种高级策略的命中率比较为接近于理想曲线Optimal而LRU则差距最远。另外在Caffeine官方给出的数据库、网站、分析类等应用场景中这几种策略之间的绝对差距也不完全一样但相对排名基本上没有改变最基础的淘汰策略的命中率是最低的。如果你对其他缓存淘汰策略感兴趣的话可以参考维基百科中对Cache Replacement Policies的介绍。
好,最后我们再来看看服务端缓存的第三种属性,也就是它提供的一些额外的管理功能。
扩展功能
一般来说一套标准的Map接口或者是来自JSR 107的javax.cache.Cache接口就可以满足缓存访问的基本需要不过在“访问”之外专业的缓存往往还会提供很多额外的功能。
加载器
许多缓存都有“CacheLoader”之类的设计加载器可以让缓存从只能被动存储外部放入的数据变为能够主动通过加载器去加载指定Key值的数据加载器也是实现自动刷新功能的基础前提。
淘汰策略
有的缓存淘汰策略是固定的,也有一些缓存可以支持用户根据自己的需要,来选择不同的淘汰策略。
失效策略
失效策略就是要求缓存的数据在一定时间后自动失效(移除出缓存)或者自动刷新(使用加载器重新加载)。
事件通知
缓存可能会提供一些事件监听器让你在数据状态变动如失效、刷新、移除时进行一些额外操作。有的缓存还提供了对缓存数据本身的监视能力Watch功能
并发级别
对于通过分段加锁来实现的缓存以Guava Cache为代表往往会提供并发级别的设置。
这里你可以简单地理解为缓存内部是使用多个Map来分段存储数据的并发级别就用于计算出使用Map的数量。如果这个参数设置过大会引入更多的Map你需要额外维护这些Map而导致更大的时间和空间上的开销而如果设置过小又会导致在访问时产生线程阻塞因为多个线程更新同一个ConcurrentMap的同一个值时会产生锁竞争。
容量控制
缓存通常都支持指定初始容量和最大容量。设定初始容量的目的是减少扩容频率这与Map接口本身的初始容量含义是一致的而最大容量类似于控制Java堆的-Xmx参数当缓存接近最大容量时会自动清理掉低价值的数据。
引用方式
Java语言支持将数据设置为软引用或者弱引用而提供引用方式的设置就是为了将缓存与Java虚拟机的垃圾收集机制联系起来。
统计信息
缓存框架会提供诸如缓存命中率、平均加载时间、自动回收计数等统计信息。
持久化
也就是支持将缓存的内容存储到数据库或者磁盘中。进程内缓存提供持久化功能的作用不是太大,但分布式缓存大多都会考虑提供持久化功能。
小结
今天这节课,我给你介绍了缓存的三项属性:吞吐量、命中率和扩展功能。为了便于你回顾知识点,我把目前几款主流的进程内缓存方案整理成了一个表格,供你参考。
那么总的来说,表格里的四类就基本囊括了目前主流的进程内缓存方案。希望通过这节课的学习,你能够掌握服务端缓存的原理,能够独立分析各种缓存框架所提供的功能属性,明白它们有什么影响,有什么收益和代价。
一课一思
在这节课的开篇我就提到了缓存并非多多益善,引用它有收益也有风险。那么请你思考一下,缓存可能存在什么风险弊端?欢迎在留言区分享你的见解。这也是我们下一节课的主要话题。
好,感谢你的阅读,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。就到这里,我们下一讲再见。

View File

@@ -0,0 +1,199 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 _ 分布式缓存如何与本地缓存配合,提高系统性能?
你好,我是周志明。
今天,我们接着上节课服务端缓存的话题,继续来学习下分布式缓存的实现形式、与本地缓存搭配使用的方法,以及一起来了解下,在实际使用缓存的过程中,可能会存在的各种风险和应对手段。
分布式缓存
首先通过上节课的学习,现在我们已经知道了,服务端缓存可以分为“进程内缓存”和“分布式缓存”两大类。相比缓存数据在进程内存中读写的速度,一旦涉及到了网络访问,那么由网络传输、数据复制、序列化和反序列化等操作所导致的延迟,就要比内存访问高得多。
所以,对于分布式缓存来说,处理与网络有关的操作是影响吞吐量的主要因素,这也是比淘汰策略、扩展功能更重要的关注点。
而这就决定了尽管也有Ehcache、Infinispan这类能同时支持分布式部署和进程内嵌部署的缓存方案但在通常情况下进程内缓存和分布式缓存在选型时会有完全不同的候选对象和考察点。
所以说,我们在决定使用哪种分布式缓存之前,必须先确认好自己的需求是什么。
那么接下来,我们就从两个不同的需求场景出发,看看都可以选择哪些分布式缓存方案。我们先从数据访问的需求场景开始了解吧。
复制式缓存与集中式缓存
从访问的角度来说,如果是频繁更新但很少读取的数据,正常是不会有人把它拿去做缓存的,因为这样做没有收益。
然后,对于很少更新但频繁读取的数据,理论上更适合做复制式缓存;而对于更新和读取都较为频繁的数据,理论上就更适合做集中式缓存。
所以在这里,我就针对这两种比较通用的缓存形式,给你介绍一下二者之间的差别,以及各自具有代表性的产品。
复制式缓存
对于复制式缓存你可以看作是“能够支持分布式的进程内缓存”它的工作原理与Session复制类似缓存中的所有数据在分布式集群的每个节点里面都存有一份副本当读取数据时无需网络访问直接从当前节点的进程内存中返回因此理论上可以做到与进程内缓存一样高的读取性能而当数据发生变化的时候就必须遵循复制协议将变更同步到集群的每个节点中这时复制性能会随着节点的增加呈现平方级下降变更数据的代价就会变得十分高昂。
复制式缓存的代表是JBossCache这是JBoss针对企业级集群设计的缓存方案它可以支持JTA事务依靠JGroup进行集群节点间数据同步。
以JBossCache为典型的复制式缓存曾经有过一段短暂的兴盛期但是在今天我们基本上已经很难再见到使用这种缓存形式的大型信息系统了。
为什么今天JBossCache会被淘汰掉呢
主要是因为JBossCache的写入性能实在是差到了不堪入目的程度它在小规模集群中同步数据还算是差强人意但在大规模集群下动辄就会因为网络同步的速度跟不上写入速度进而导致在内存中累计大量待重发对象最终引发OutOfMemory崩溃。如果我们对JBossCache没有足够了解的话稍有不慎就会被埋进坑里。
后来为了缓解复制式同步的写入效率问题JBossCache的继任者Infinispan提供了另一种分布式同步模式。它允许用户配置数据需要复制的副本数量比如集群中有八个节点我们可以要求每个数据只保存四份副本这样就降低了复制数据时的网络负担。
此时缓存的总容量就相当于是传统复制模式的一倍如果要访问的数据在本地缓存中没有存储Infinispan完全有能力感知网络的拓扑结构知道应该到哪些节点中寻找数据。
集中式缓存
集中式缓存是目前分布式缓存的主流形式。集中式缓存的读、写都需要网络访问,它的好处是不会随着集群节点数量的增加而产生额外的负担,而坏处自然是读、写都不可能再达到进程内缓存那样的高性能。
集中式缓存还有一个必须提到的关键特点,那就是它与使用缓存的应用分处在独立的进程空间中。
这样做的好处是它能够为异构语言提供服务比如用C语言编写的Memcached完全可以毫无障碍地为Java语言编写的应用提供缓存服务但坏处是如果要缓存像对象这种复杂类型的话基本上就只能靠序列化来支撑具体语言的类型系统了支持Hash类型的缓存可以部分模拟对象类型。这样就不仅产生了序列化的成本还很容易导致传输成本的大幅增加。
我举个例子假设某个有100个字段的大对象变更了其中1个字段的值通常缓存也不得不把整个对象的所有内容重新序列化传输出去才能实现更新。所以一般集中式缓存更提倡直接缓存原始数据类型而不是对象。
相比之下JBossCache则通过它的字节码自审Introspection功能和树状存储结构TreeCache做到了自动跟踪、处理对象的部分变动。如果用户修改了对象中某些字段的数据缓存就只会同步对象中真正变更的那部分数据。
不过现在因为Redis在集中式缓存中处于统治地位已经打败了Memcached和其他集中式缓存框架成为了集中式缓存的首选甚至可以说成为了分布式缓存的首选几乎到了不用管读取、写入哪种操作更频繁都可以无脑上Redis的程度。
也正是因为如此前面我在说到哪些数据适合用复制式缓存、哪些数据适合用集中式缓存的时候我都加了个拗口的“理论上”。尽管Redis最初设计的本意是NoSQL数据库而不是专门用来做缓存的可今天它确实已经成为许多分布式系统中不可或缺的基础设施被广泛用作缓存的实现方案。
而另一方面,访问缓存不仅仅要考虑如何快速取到数据,还需要考虑取到的是否是正确的数据,缓存的数据质量是另一个重要的考量因素。
从数据一致性的角度来说,缓存本身也有集群部署的需求。所以在理论上,我们需要好好考虑一下,如果不同的节点取到的缓存数据不一样,我们是否可以接受。比如说,我们刚刚放入缓存中的数据,另外一个节点马上访问发现未能读到;或者刚刚更新缓存中的数据,另外一个节点访问时,在短时间内读取到的仍是旧的数据,等等。
那么根据分布式缓存集群是否能保证数据一致性我们可以将它分为AP和CP两种类型在“分布式事务”中已经介绍过CAP各自的含义
你可以发现这里我又说的是“理论上”这是因为我们在实际开发中通常不太会使用缓存来处理追求强一致性的数据。当然我们是可以这样做但其实没必要可类比MESI等缓存一致性协议
给你举个例子。Redis集群就是典型的AP式它具有高性能、高可用等特点但它却并不保证强一致性。而能够保证强一致性的ZooKeeper、Doozerd、Etcd等分布式协调框架我们可通常不会把它们当作“缓存框架”来使用这些分布式协调框架的吞吐量相对Redis来说是非常有限的。不过ZooKeeper、Doozerd、Etcd倒是常跟Redis和其他分布式缓存搭配工作用来实现其中的通知、协调、队列、分布式锁等功能。
透明多级缓存
那到这里你也能发现分布式缓存与进程内缓存各有所长也有各有局限它们是互补的而不是竞争的关系。所以如果你有需要完全可以同时互相搭配进程内缓存和分布式缓存来构成透明多级缓存Transparent Multilevel CacheTMC
这里,我们先不去考虑“透明”这个词的定义是啥,单看“多级缓存”的话,倒还很好理解。
它的意思就是,使用进程内缓存做一级缓存,分布式缓存做二级缓存,如果能在一级缓存中查询到结果就直接返回,否则就到二级缓存中去查询;再将二级缓存中的结果回填到一级缓存,以后再访问该数据就没有网络请求了。
而如果二级缓存也查询不到,就发起对最终数据源的查询,将结果回填到一、二级缓存中去。
不过,尽管多级缓存结合了进程内缓存和分布式缓存的优点,但它的代码侵入性较大,需要由开发者承担多次查询、多次回填的工作,也不便于管理,像是超时、刷新等策略,都要设置多遍,数据更新更是麻烦,很容易会出现各个节点的一级缓存、二级缓存里的数据互相不一致的问题。
所以,我们必须“透明”地解决这些问题,多级缓存才具有实用的价值。
一种常见的设计原则,就是变更以分布式缓存中的数据为准,访问以进程内缓存的数据优先。
大致做法是当数据发生变动时在集群内发送推送通知简单点的话可以采用Redis的PUB/SUB求严谨的话可以引入ZooKeeper或Etcd来处理让各个节点的一级缓存自动失效掉相应数据。
然后,当访问缓存时,缓存框架提供统一封装好的一、二级缓存联合查询接口,接口外部只查询一次,接口内部自动实现优先查询一级缓存。如果没有获取到数据,就再自动查询二级缓存。
缓存风险
OK现在你也对不同需求场景下的不同分布式缓存实现方案有大概的了解了。而在上一节课开头我提到过缓存并不是多多益善它有利也有弊是要真正到必要的时候才去考虑的解决方案。因此接下来我就带你详细了解一下使用缓存的各种常见风险和注意事项以及应对风险的方法。
缓存穿透
我们知道引入缓存的目的是为了缓解CPU或者I/O的压力比如对数据库做缓存大部分流量都从缓存中直接返回只有缓存未能命中的数据请求才会流到数据库中数据库压力自然就减小了。
但是如果查询的数据在数据库中根本不存在的话,缓存里自然也不会有。这样,这类请求的流量每次都不会命中,每次都会触及到末端的数据库,缓存自然也就起不到缓解压力的作用了。那么,这种查询不存在数据的现象,就被称为缓存穿透。
缓存穿透有可能是业务逻辑本身就存在的固有问题,也有可能是被恶意攻击的所导致的。所以,为了解决缓存穿透,我们一般会采取下面两种办法:
对于业务逻辑本身就不能避免的缓存穿透
我们可以约定在一定时间内对返回为空的Key值依然进行缓存注意是正常返回但是结果为空不要把抛异常的也当作空值来缓存了这样在一段时间内缓存就最多被穿透一次。
如果后续业务在数据库中对该Key值插入了新记录那我们就应当在插入之后主动清理掉缓存的Key值。如果业务时效性允许的话也可以设置一个较短的超时时间来自动处理缓存。
对于恶意攻击导致的缓存穿透
针对这种原因我们通常会在缓存之前设置一个布隆过滤器来解决。所谓的恶意攻击是指请求者刻意构造数据库中肯定不存在的Key值然后发送大量请求进行查询。而布隆过滤器是用最小的代价来判断某个元素是否存在于某个集合的办法。
如果布隆过滤器给出的判定结果是请求的数据不存在,那就直接返回即可,连缓存都不必去查。虽然维护布隆过滤器本身需要一定的成本,但比起攻击造成的资源损耗,还是比较值得的。
缓存击穿
我们都知道,缓存的基本工作原理是首次从真实数据源加载数据,完成加载后回填入缓存,以后其他相同的请求就从缓存中获取数据,缓解数据源的压力。
但是,如果缓存中的某些热点数据忽然因为某种原因失效了,比如典型地由于超期而失效,而此时又有多个针对该数据的请求同时发送过来,那么这些请求就会全部未能命中缓存,都到达真实数据源中去,导致其压力剧增。这种现象,就被称为缓存击穿。
所以,要如何避免缓存击穿问题呢?我们通常可以采取这样两种办法:
加锁同步。以请求该数据的Key值为锁这样就只有第一个请求可以流入到真实的数据源中其他线程采取阻塞或重试策略。如果是进程内缓存出现了问题施加普通互斥锁就可以了如果是分布式缓存中出现的问题就施加分布式锁这样数据源就不会同时收到大量针对同一个数据的请求了。
热点数据由代码来手动管理。缓存击穿是只针对热点数据被自动失效才引发的问题,所以对于这类数据,我们可以直接通过代码来有计划地完成更新、失效,避免由缓存的策略自动管理。
缓存雪崩
现在我们了解了,缓存击穿是针对单个热点数据失效,由大量请求击穿缓存而给真实数据源带来了压力。
而另一种可能更普遍的情况,是不需要针对单个热点数据的大量请求,而是由于大批不同的数据在短时间内一起失效,导致了这些数据的请求都击穿了缓存,到达数据源,这同样也会令数据源在短时间内压力剧增。
那么,之所以会出现这种情况,往往是因为系统有专门的缓存预热功能,也可能是因为,大量的公共数据都是由某一次冷操作加载的,这样都可能会出现由此载入缓存的大批数据具有相同的过期时间,在同一时刻一起失效。
还有一种情况是缓存服务由于某些原因崩溃后重启,此时也会造成大量数据同时失效。那么以上出现的这种现象,就被称为缓存雪崩。
而要避免缓存雪崩的问题,我们通常可以采取这三种办法:
提升缓存系统可用性,建设分布式缓存的集群。
启用透明多级缓存,各个服务节点的一级缓存中的数据,通常会具有不一样的加载时间,这样做也就分散了它们的过期时间。
将缓存的生存期从固定时间改为一个时间段内的随机时间比如原本是一个小时过期那可以在缓存不同数据时设置生存期为55分钟到65分钟之间的某个随机时间。
缓存污染
所谓的缓存污染是指,缓存中的数据与真实数据源中的数据不一致的现象。尽管我在前面有说过,缓存通常不追求强一致性,但这显然不能等同于,缓存和数据源间连最终的一致性都可以不要求了。
缓存污染多数是因为开发者更新缓存不规范造成的。比如说,你从缓存中获得了某个对象,更新了对象的属性,但最后因为某些原因,比如后续业务发生异常回滚了,最终没有成功写入到数据库,此时缓存的数据是新的,而数据库中的数据是旧的。
所以为了尽可能地提高使用缓存时的一致性人们已经总结了不少更新缓存时可以遵循的设计模式比如Cache Aside、Read/Write Through、Write Behind Caching等等。
这里我想给你介绍下Cache Aside模式因为这种设计模式最简单成本也最低。它的主要内容只有两条
读数据时,先读缓存,缓存没有的话,再读数据源,然后将数据放入缓存,再响应请求。
写数据时,先写数据源,然后失效(而不是更新)掉缓存。
在读数据方面,一般不会有什么出错的余地。但是写数据时,我有必要专门给你强调两点。
一个是先后顺序一定要先数据源后缓存。你试想一下,如果采用先失效缓存后写数据源的顺序,那一定会存在一段时间内缓存已经删除完毕,但数据源还未修改完成的情况。此时新的查询请求到来,缓存未能命中,就会直接流到真实数据源中。
这样,请求读到的数据依然是旧数据,随后又重新回填到缓存中。而当数据源修改完成后,结果就成了数据在数据源中是新的,在缓存中是老的,两者就会有不一致的情况。
二个是应当失效缓存,而不是尝试去更新缓存。这很容易理解,如果去更新缓存,更新过程中数据源又被其他请求再次修改的话,缓存又要面临处理多次赋值的复杂时序问题。所以直接失效缓存,等下次用到该数据时自动回填,期间数据源中的值无论被改了多少次,都不会造成任何影响。
不过Cache Aside模式依然也不能保证在一致性上绝对不出问题否则我们就不需要设计出Paxos这样复杂的共识算法了。采用Cache Aside模式典型的出错场景就是如果某个数据是从未被缓存过的请求会直接流到真实数据源中如果数据源中的写操作发生在查询请求之后结果回填到缓存之前也会出现缓存中回填的内容与数据库的实际数据不一致的情况。
但是出现这种情况的概率实际上是很低的Cache Aside模式仍然是以低成本更新缓存并且获得相对可靠结果的解决方案。
小结
今天这一讲,我着重给你介绍了两种主要的分布式缓存形式,分别是复制式缓存和集中式缓存。其中我强调了,在选择使用不同缓存方案的时候,你需要注意对读效率和写效率,以及对访问效率和数据质量之间的权衡。而在实际的应用场景中,你其实可以考虑选择将两种缓存结合使用,构成透明多级缓存,以此达到各取所长的目的。
最后,在为系统引入缓存的时候,你还要特别注意可能会出现的风险问题,比如说缓存穿透、缓存击穿、缓存雪崩、缓存污染,等等。如果你对这些可能出现的风险问题有了一定的准备和应对方案,那么可以说,你基本上算是对服务端缓存建立了基本的整体认知了。
一课一思
不知道你还记不记得在第16讲中我提出过一个观点“能满足需求的前提下最简单的系统就是最好的系统”。现在你已经学完了“透明多级分流系统”这个小章节的所有内容那么你对这个判定有什么新的看法吗?
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,274 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 _ 认证:系统如何正确分辨操作用户的真实身份?
你好,我是周志明。
我们应该都很清楚,对于软件研发来说,即使只限定在“软件架构设计”这个语境下,系统安全仍然是一个很大的话题。它不仅包括“防御系统被黑客攻击”这样狭隘的安全,还包括一些与管理、运维、审计等领域主导的相关安全性问题,比如说安全备份与恢复、安全审计、防治病毒,等等。
不过在这门课程里,我们的关注重点并不会放在以上这些内容上,我们所谈论的软件架构安全,主要包括(但不限于)以下这些问题的具体解决方案:
认证Authentication系统如何正确分辨出操作用户的真实身份
授权( Authorization系统如何控制一个用户该看到哪些数据、能操作哪些功能
凭证Credentials系统如何保证它与用户之间的承诺是双方当时真实意图的体现是准确、完整且不可抵赖的
保密Confidentiality系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用
传输Transport Security系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充
验证Verification系统如何确保提交到每项服务中的数据是合乎规则的不会对系统稳定性、数据一致性、正确性产生风险
由于跟安全相关的问题,一般都不会给架构设计直接创造价值,而且解决起来又很繁琐复杂、费时费力,所以可能会经常性地被一部分开发人员给有意无意地忽略掉。
不过庆幸的是,这些问题基本上也都是与具体系统、具体业务无关的通用性问题,这就意味着它们往往会存在一些业界通行的、已经被验证过是行之有效的解决方案,乃至已经形成了行业标准,不需要我们再从头去构思如何解决。
所以在“安全架构”这个小章节里我会花六讲的时间围绕系统安全的标准方案带你逐一探讨以上这些问题的处理办法并会以Fenixs Bookstore作为案例实践。而出于方便你进行动手实操的目的我不会在课程中直接贴出大段的项目代码当然必要的代码示例还是会有的所以我建议你要结合着从Fenixs Bookstore的GitHub仓库中获取的示例代码来进行学习。
好,那么今天这节课,我们就从“认证”这个话题开始,一起来解决“系统如何正确分辨操作用户的真实身份”这个问题。
什么是认证?
认证Authentication、授权Authorization和凭证Credentials这三项可以说是一个系统中最基础的安全设计了哪怕是再简陋的信息系统大概也不可能忽略掉“用户登录”这个功能。
信息系统在为用户提供服务之前,总是希望先弄清楚“你是谁?”(认证)、“你能干什么?”(授权)以及“你如何证明?”(凭证)这三个基本问题的答案。然而,认证、授权与凭证这三个基本问题,又并不像部分开发者认为的那样,只是一个“系统登录”功能而已,仅仅是校验一下用户名、密码是否正确这么简单。
账户和权限信息作为一种必须最大限度保障安全和隐私,同时又要兼顾各个系统模块、甚至是系统间共享访问的基础主数据,它的存储、管理与使用都面临一系列复杂的问题。
因此对于某些大规模的信息系统账户和权限的管理往往要由专门的基础设施来负责比如微软的活动目录Active DirectoryAD或者轻量目录访问协议Lightweight Directory Access ProtocolLDAP跨系统的共享使用问题甚至还会用到区块链技术来解决。
另外,还有一个不少人会先入为主的认知偏差:尽管“认证”是解决“你是谁?”的问题,但这里的“你”并不一定是个人(真不是在骂你),也很有可能是指外部的代码,即第三方的类库或者服务。
因为最初在计算机软件当中对代码认证的重要程度甚至要高于对最终用户的认证比如早期的Java系统里安全中的认证默认是特指“代码级安全”即你是否信任要在你的电脑中运行的代码。
这是由Java当时的主要应用形式Java Applets所决定的类加载器从远端下载一段字节码以Applets的形式在用户的浏览器中运行由于Java的语言操控计算机资源的能力要远远强于JavaScript所以系统必须要先确保这些代码不会损害用户的计算机否则就谁都不敢去用。
这一阶段的安全观念就催生了现在仍然存在于Java技术体系中的“安全管理器”java.lang.SecurityManager、“代码权限许可”java.lang.RuntimePermission等概念。到了现在系统对外部类库和服务的认证需求依然很普遍但相比起五花八门的最终用户认证来说代码认证的研究发展方向已经很固定了基本上都是统一到证书签名上。
不过在咱们这节课里对认证的探究范围只限于对最终用户的认证。关于对代码的认证我会安排在“分布式的基石”模块中的第40讲“服务安全”来讲解。
好,那么在理解了什么是认证、界定了认证的范围之后,我们接下来看一下软件工业界是如何进行认证的。
认证的标准
在世纪之交Java迎来了Web时代的辉煌互联网的迅速兴起促使Java进入了快速发展时期。这时候基于HTML和JavaScript的超文本Web应用就迅速超过了“Java 2时代”之前的Java Applets应用B/S系统对最终用户认证的需求使得“安全认证”的重点逐渐从“代码级安全”转为了“用户级安全”即你是否信任正在操作的用户。
在1999年随J2EE 1.2它是J2EE的首个版本为了与J2SE同步初始版本号直接就是1.2一起发布的Servlet 2.2中添加了一系列用于认证的API主要包括了两部分内容
标准方面添加了四种内置的、不可扩展的认证方案即Client-Cert、Basic、Digest和Form。
实现方面添加了与认证和授权相关的一套程序接口比如HttpServletRequest::isUserInRole()、HttpServletRequest::getUserPrincipal()等方法。
到这儿你可能会觉得这都是一项发布超过20年的老旧技术了为啥还要专门提一嘴呢这是因为我希望从它包含的两部分内容中引出一个架构安全性的经验原则以标准规范为指导、以标准接口去实现。
因为安全涉及的问题很麻烦但它的解决方案也相当的成熟。对于99%的系统来说,在安全上不去做轮子,不去想发明创造,严格遵循标准就是最恰当的安全设计。
然后我之所以引用J2EE 1.2对安全的改进还有一个原因就是它内置支持的Basic、Digest、Form和Client-Cert四种认证方案都很有代表性刚好分别覆盖了通讯信道、协议和内容层面的认证这三种层面的认证又涵盖了主流的三种认证方式下面我们分别来看看它们各自的含义和应用场景
通讯信道上的认证你和我建立通讯连接之前要先证明你是谁。在网络传输Network场景中的典型是基于SSL/TLS传输安全层的认证。
通讯协议上的认证你请求获取我的资源之前要先证明你是谁。在互联网Internet场景中的典型是基于HTTP协议的认证。
通讯内容上的认证你使用我提供的服务之前要先证明你是谁。在万维网World Wide Web场景中的典型是基于Web内容的认证。
关于第一点“信道上的认证”由于它涉及的内容较多又与后面第28、29讲要介绍的微服务安全方面的话题关系密切所以这节课我就不展开讲了而且J2EE中的Client-Cert其实并不是用于TLS的以它引出TLS并不合适
那么接下来,我们就针对后两种认证方式,来看看它们各自都有什么样的实现特点和工作流程。
基于通讯协议HTTP认证
前面我在介绍J2EE 1.2这项老技术的时候已经提前用到了一个技术名词“认证方案”Authentication Schemes。它是指生成用户身份凭证的某种方法这个概念最初是来源于HTTP协议的认证框架Authentication Framework
IETF在RFC 7235中定义了HTTP协议的通用认证框架要求所有支持HTTP协议的服务器当未授权的用户意图访问服务端保护区域的资源时应返回401 Unauthorized的状态码同时要在响应报文头里附带以下两个分别代表网页认证和代理认证的Header之一告知客户端应该采取哪种方式产生能代表访问者身份的凭证信息
WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>
而在接收到该响应后客户端必须遵循服务端指定的认证方案在请求资源的报文头中加入身份凭证信息服务端核实通过后才会允许该请求正常返回否则将返回403 Forbidden。其中请求报文头要包含以下Header项之一
Authorization: <认证方案> <凭证内容>
Proxy-Authorization: <认证方案> <凭证内容>
由此我们其实可以发现HTTP认证框架提出的认证方案是希望能把认证“要产生身份凭证”的目的与“具体如何产生凭证”的实现给分开来。无论客户端是通过生物信息指纹、人脸、用户密码、数字证书还是其他方式来生成凭证都是属于如何生成凭证的具体实现都可以包容在HTTP协议预设的框架之内。
HTTP认证框架的工作流程如下面的时序图所示
不过只有这种概念性的介绍你可能还是会觉得有点儿枯燥和抽象接下来我就以最基础的认证方案HTTP Basic Authentication为例来给你解释下认证具体是如何工作的。
HTTP Basic认证是一种以演示为目的的认证方案在一些不要求安全性的场合也有实际应用比如你家里的路由器登录有可能就是这种认证方式。
Basic认证产生用户身份凭证的方法是让用户输入用户名和密码经过Base64编码“加密”后作为身份凭证。比如请求资源“GET/admin”后浏览器会收到服务端如下响应
HTTP/1.1 401 Unauthorized
Date: Mon, 24 Feb 2020 16:50:53 GMT
WWW-Authenticate: Basic realm="example from icyfenix.cn"
此时浏览器必须询问最终用户要求提供用户名和密码并会弹出类似下图所示的HTTP Basic认证窗口
然后用户在对话框中输入密码信息比如输入用户名“icyfenix”密码123456浏览器会将字符串“icyfenix:123456”编码为“aWN5ZmVuaXg6MTIzNDU2”然后发送给服务端HTTP请求如下所示
GET /admin HTTP/1.1
Authorization: Basic aWN5ZmVuaXg6MTIzNDU2
服务端接收到请求,解码后检查用户名和密码是否合法,如果合法就允许返回/admin的资源否则就返回403 Forbidden禁止下一步操作。
这里要注意一点Base64只是一种编码方式而并不是任何形式的加密所以Basic认证的风险是显而易见的它只能是一种以演示为主要目的的认证方案。
那么除Basic认证外IETF还定义了很多种可用于实际生产环境的认证方案比如
DigestRFC 7616HTTP摘要认证你可以把它看作是Basic认证的改良版本针对Base64明文发送的风险Digest认证把用户名和密码加盐一个被称为Nonce的变化值作为盐值再通过MD5/SHA等哈希算法取摘要发送出去。这种认证方式依然是不安全的无论客户端使用何种加密算法加密无论是否采用了Nonce这样的动态盐值去抵御重放和冒认当遇到中间人攻击时依然存在显著的安全风险。在第27“保密”一讲中我还会跟你具体讨论加解密方面的问题。
BearerRFC 6750基于OAuth 2.0规范来完成认证OAuth 2.0是一个同时涉及到认证与授权的协议。在下节课讲解“授权”的时候我会详细介绍OAuth 2.0。
HOBARFC 7486 HOBA是HTTP Origin-Bound Authentication的缩写这是一种基于自签名证书的认证方案。基于数字证书的信任关系主要有两类模型一类是采用CACertification Authority层次结构的模型由CA中心签发证书另一种是以IETF的Token Binding协议为基础的OBCOrigin Bound Certificates自签名证书模型。同样在后面讲“传输”的时候我会给你详细介绍数字证书。
还有在HTTP认证框架中认证方案是允许自行扩展的也并不要求一定要由RFC规范来定义只要用户代理User Agent通常是浏览器泛指任何使用HTTP协议的程序能够识别这种私有的认证方案即可。
因此,很多厂商也扩展了自己的认证方案,比如:
AWS4-HMAC-SHA256相当简单粗暴的名字就是亚马逊AWS基于HMAC-SHA256哈希算法的认证。
NTLM / Negotiate这是微软公司NT LAN ManagerNTLM用到的两种认证方式。
Windows Live ID这个顾名思义即可。
Twitter Basic一个不存在的网站所改良的HTTP基础认证。
……
说完了基于通讯协议的认证方案我们再来看看基于通讯内容的Web认证是如何实现的。
基于通讯内容Web认证
IETF为HTTP认证框架设计了可插拔Pluggable的认证方案原本是希望能涌现出各式各样的认证方案去支持不同的应用场景。尽管前面我也列举了一些还算常用的认证方案但目前的信息系统尤其是在系统对终端用户的认证场景中直接采用HTTP认证框架的比例其实是非常低的。
这也不难理解HTTP是“超文本传输协议”传输协议的根本职责是把资源从服务端传输到客户端至于资源具体是什么内容只能由客户端自行解析驱动。所以说以HTTP协议为基础的认证框架也只能面向传输协议而不是具体传输内容来设计。
如果用户想要从服务器中下载文件弹出一个HTTP服务器的对话框让用户登录是可以接受的但如果用户访问信息系统中的具体服务身份认证肯定希望是由系统本身的功能去完成的而不是由HTTP服务器来负责认证。
那么这种依靠内容而不是传输协议来实现的认证方式在万维网里就被称为“Web认证”由于在实现形式上登录表单占了绝对的主流因此它通常也被称为“表单认证”Form Authentication
实际上直到2019年之前表单认证都没有什么行业标准可循表单长什么样子、其中的用户字段、密码字段、验证码字段、是否要在客户端加密、采用何种方式加密、接受表单的服务地址是什么等等都完全由服务端与客户端的开发者自行协商决定。
可“没有标准的约束”,反倒成了表单认证的一大优点,表单认证允许我们做出五花八门的页面,各种程序语言、框架或开发者本身,都可以自行决定认证的全套交互细节。
到这里你可能要说了,在前面讲认证标准的时候,我说“遵循规范、别造轮子就是最恰当的安全”,这里我又把表单认证的高自由度说成是一大优点,好话都让我给说全了。
其实啊,我提倡用标准规范去解决安全领域的共性问题,这条原则完全没有必要与界面是否美观合理、操作流程是否灵活便捷这些应用需求对立起来。
比如,想要支持密码或扫码等多种登录方式、想要支持图形验证码来驱逐爬虫与机器人、想要支持在登录表单提交之前进行必要的表单校验,等等,这些需求都很具体,不具备写入标准规范的通用性,但它们都具备足够的合理性,应当在实现层面去满足。
同时,如何控制权限保证不产生越权操作、如何传输信息保证内容不被窃听篡改、如何加密敏感内容保证即使泄漏也不被逆推出明文,等等,这些问题也已经有了通行的解决方案,明确定义在规范之中,因此也应当在架构层面去遵循。
所以说表单认证与HTTP认证不见得是完全对立的它们分别有不同的关注点可以结合使用。就以Fenixs Bootstore的登录功能为例这个项目的页面表单是一个自行设计的Vue.js页面但认证的整个交互过程就遵循了OAuth 2.0规范的密码模式来完成。
2019年3月万维网联盟批准了由FIDOFast IDentity Online一个安全、开放、防钓鱼、无密码认证标准的联盟领导起草的世界首份Web内容认证的标准“WebAuthn”在这节课里我们只讨论WebAuthn不会涉及CTAP、U2F和UAF。如果你的思维很严谨的话可能又会觉得奇怪和矛盾了不是才说了Web表单长什么样、要不要验证码、登录表单是否在客户端校验等等是十分具体的需求不太可能定义在规范上的吗
确实如此所以WebAuthn彻底抛弃了传统的密码登录方式改为直接采用生物识别指纹、人脸、虹膜、声纹或者实体密钥以USB、蓝牙、NFC连接的物理密钥容器来作为身份凭证从根本上消灭了用户输入错误产生的校验需求以及防止机器人模拟产生的验证码需求等问题甚至连表单界面都可能省略掉所以这个规范不关注界面该是什么样子、要不要验证码、是否要前端校验等这些问题。
不过由于WebAuthn相对比较复杂在学习后面的内容之前我建议如果你的设备和环境允许的话可以先在GitHub网站的2FA认证功能中实际体验一下通过WebAuthn完成的两段式登录。
在硬件方面需要你用带有TouchBar的MacBook或者其他支持指纹、FaceID验证的手机均可现在应该在售的移动设备基本都带有生物识别的装置了。在软件方面直至iOS13.6iPhone和iPad都不支持WebAuthn但Android和macOS系统中的Chrome以及Windows的Edge浏览器都已经可以正常使用WebAuthn了。
WebAuthn规范涵盖了“注册”与“认证”两大流程我先来介绍下注册流程的大致步骤
用户进入系统的注册页面这个页面的格式、内容和用户注册时需要填写的信息都不包含在WebAuthn标准的定义范围内。
当用户填写完信息点击“提交注册信息”的按钮后服务端先暂存用户提交的数据生成一个随机字符串规范中称为Challenge和用户的UserID在规范中称作凭证ID返回给客户端。
客户端的WebAuthn API接收到Challenge和UserID把这些信息发送给验证器Authenticator这个验证器你可以理解为用户设备上TouchBar、FaceID、实体密钥等认证设备的统一接口。
验证器提示用户进行验证如果你的机器支持多种认证设备还会提示用户选择一个想要使用的设备。验证的结果是生成一个密钥对公钥和私钥验证器自己存储好私钥、用户信息以及当前的域名。然后使用私钥对Challenge进行签名并将签名结果、UserID和公钥一起返回给客户端。
浏览器将验证器返回的结果转发给服务器。
服务器核验信息检查UserID与之前发送的是否一致并对比用公钥解密后得到的结果与之前发送的Challenge是否一致一致即表明注册通过服务端存储该UserID对应的公钥。
你可以参考一下这个注册步骤的时序图:
登录流程其实跟注册流程差不多,如果你理解了注册流程,登录就比较简单了,大致可以分为这样几个步骤:
用户访问登录页面,填入用户名后即可点击登录按钮。
服务器返回随机字符串Challenge、用户UserID。
浏览器将Challenge和UserID转发给验证器。
验证器提示用户进行认证操作。由于在注册阶段验证器已经存储了该域名的私钥和用户信息所以如果域名和用户都相同的话就不需要生成密钥对了直接以存储的私钥加密Challenge然后返回给浏览器。
服务端接收到浏览器转发来的被私钥加密的Challenge以此前注册时存储的公钥进行解密如果解密成功则宣告登录成功。
WebAuthn采用非对称加密的公钥、私钥替代传统的密码这是非常理想的认证方案。私钥是保密的只有验证器需要知道它连用户本人都不需要知道也就没有人为泄漏的可能公钥是公开的可以被任何人看到或存储。
另外,公钥可用于验证私钥生成的签名,但不能用来签名,除了得知私钥外,没有其他途径能够生成可被公钥验证为有效的签名,这样服务器就可以通过公钥是否能够解密,来判断最终用户的身份是否合法。
而且WebAuthn还一揽子地解决了传统密码在网络传输上的风险在“保密”一节课中我们还会讲到无论密码是否在客户端进行加密、如何加密对防御中间人攻击来说都是没有意义的。
更值得夸赞的是WebAuthn还为登录过程带来了极大的便捷性不仅注册和验证的用户体验十分优秀而且彻底避免了用户在一个网站上泄漏密码所有使用相同密码的网站都受到攻击的问题这个优点可以让用户不需要再为每个网站想不同的密码。
当然现在的WebAuthn还很年轻普及率暂时还很有限但我相信几年之内它必定会发展成Web认证的主流方式被大多数网站和系统所支持。
认证的实现
OK在了解了业界标准的认证规范以后我们再来看看在Java技术体系内通常都是如何实现安全认证的。
Java其实也有自己的认证规范第一个系统性的Java认证规范发布于Java 1.3时代Sun公司提出了同时面向代码级安全和用户级安全的认证授权服务JAASJava Authentication and Authorization Service1.3处于扩展包中1.4纳入标准包。不过尽管JAAS已经开始照顾了最终用户的认证但相对而言该规范中代码级安全仍然占更主要的地位。
可能今天用过、甚至是听过JAAS的Java程序员都已经不多了但是这个规范提出了很多在今天仍然活跃于主流Java安全框架中的概念。比如说一般把用户存放在“Principal”之中、密码存在“Credentials”之中、登录后从安全上下文“Context”中获取状态等常见的安全概念都可以追溯到这一时期所定下的API
LoginModule javax.security.auth.spi.LoginModule
LoginContext javax.security.auth.login.LoginContext
Subject javax.security.auth.Subject
Principal java.security.Principal
Credentialsjavax.security.auth.Destroyable、javax.security.auth.Refreshable
可是虽然JAAS开创了这些沿用至今的安全概念但其规范本身实质上并没有得到广泛的应用。我认为主要有两大原因。
一方面是由于JAAS同时面向代码级和用户级的安全机制使得它过度复杂化难以推广。在这个问题上Java社区一直有做持续的增强和补救比如Java EE 6中的JASPIC、Java EE 8中的EE Security
JSR 115Java Authorization Contract for ContainersJACC
JSR 196Java Authentication Service Provider Interface for ContainersJASPIC
JSR 375 Java EE Security APIEE Security
而另一方面也可能是更重要的一个原因就是在21世纪的第一个十年里以“With EJB”为口号、以WebSphere、Jboss等为代表J2EE容器环境与以“Without EJB”为口号、以Spring、Hibernate等为代表的轻量化开发框架产生了激烈的竞争结果是后者获得了全面胜利。
这个结果就导致了依赖于容器安全的JAAS无法得到大多数人的认可。在今时今日实际活跃于Java安全领域的是两个私有的私有的意思是不由JSR所规范的即没有java/javax.*作为包名的的安全框架Apache Shiro和Spring Security。
那么相较而言Shiro更加便捷易用而Spring Security的功能则要复杂强大一些。因此在后面课程中要介绍的Fenixs Bookstore项目中无论是单体架构、还是微服务架构我都选择了Spring Security作为安全框架这个选择与功能、性能之类的考量没什么关系就只是因为Spring Boot、Spring Cloud全家桶的缘故这里我不打算罗列代码来介绍Shiro与Spring Security的具体使用如果你感兴趣可以参考Fenixs Bookstore的源码仓库
只从目标上来看,两个安全框架提供的功能都很类似,大致包括以下四类:
认证功能以HTTP协议中定义的各种认证、表单等认证方式确认用户身份这也是这节课所探讨的主要话题。
安全上下文:用户获得认证之后,要开放一些接口,让应用可以得知该用户的基本资料、用户拥有的权限、角色,等等。
授权功能:判断并控制认证后的用户对什么资源拥有哪些操作许可,这部分内容我会在下一节课讲“授权”时介绍。
密码的存储与验证:密码是烫手的山芋,不管是存储、传输还是验证,都应该谨慎处理,这部分内容我会放到“保密”一讲去具体讨论。
小结
这节课我们了解了信道、协议和内容这三种主要标准化认证类型的其中两种分别是HTTP认证协议和Web认证内容。现在你应该就很清楚HTTP认证和Web认证的特点了那就是认证的载体不一样决定了认证的形式和功能范围都有不同。
另外我还给你介绍了它们各自的工作流程,其中你要关注的重点是认证框架的整体的运作,不必一下子陷入到具体的认证方案上去。
除此之外我还介绍了认证标准在Java中的落地实现。在Java技术体系中原本也有自己的认证标准与实现那就是依赖于JAAS的面向代码级和用户级的安全机制不过目前应用更广泛的反而是两个私有的安全框架这又是一个官方标准被民间草根框架击败的例子可见软件中设计必须贴近实际用户才能达到实用的效果。
一课一思
我相信你公司的系统一定也会使用用户登录功能,那么它是标准化的认证吗?是如何实现的呢?
欢迎给我留言,分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,225 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 _ 授权(上):系统如何确保授权的过程可靠?
你好,我是周志明。
在上节课,我们探讨了信息系统中关于安全认证的相关话题,它主要解决的是“你是谁”的问题。那么今天我们要探讨的授权话题,是要解决“你能干什么”的问题。
“授权”这个概念通常伴随着“认证”“审计”“账号”一同出现被合称为AAAAAuthentication、Authorization、Audit、Account。授权行为在程序中的应用也是非常广泛的我们给某个类或某个方法设置范围控制符如public、protected、private、 ),本质上也是一种授权(访问控制)行为。
而在安全领域中,我们所谈论的授权就更要具体一些,它通常涉及到以下两个相对独立的问题:
确保授权的过程可靠
对于单一系统来说,授权的过程是比较容易做到可控的,以前在很多语境上提到授权,实质上讲的都是访问控制,理论上两者是应该分开的。
而在涉及多方的系统中授权过程则是一个比较困难但必须要严肃对待的问题如何既让第三方系统能够访问到所需的资源又能保证其不泄露用户的敏感数据现在常用的多方授权协议主要有OAuth 2.0和SAML 2.0(两个协议涵盖的功能并不是直接对等的)。
确保授权的结果可控
授权的结果是用于对程序功能或者资源的访问控制Access Control。现在已形成理论体系的权限控制模型有很多比如自主访问控制Discretionary Access ControlDAC、强制访问控制Mandatory Access ControlMAC、基于属性的访问控制Attribute-Based Access ControlABAC还有最为常用的基于角色的访问控制Role-Based Access ControlRBAC
所以在接下来的两节课中我们将会围绕前面这两个问题分别以Fenixs Bookstore中用到的OAuth 2.0和RBAC为例去探讨软件业界中授权的标准协议与实现。
下面我们就先来看看OAuth 2.0的具体工作流程是什么样的吧。
OAuth 2.0解决的是第三方服务中涉及的安全授权问题
OAuth 2.0是一种相对复杂繁琐的认证授权协议。它是在RFC 6749中定义的国际标准RFC 6749正文的第一句就阐明了OAuth 2.0是面向于解决第三方应用Third-Party Application的认证授权协议。
如果你的系统并不涉及到第三方比如单体架构的Fenixs Bookstore中就既不为第三方提供服务也不使用第三方的服务那引入OAuth 2.0其实就没必要。
这里我为什么要强调第三方呢?在多方系统授权的过程中,具体会有什么问题,需要专门制定一个标准协议来解决呢?
我举个现实的例子来给你解释一下。“The Fenix Project”这部文档的官方网站它的建设和更新的大致流程是我以Markdown形式写好了某篇文章上传到由GitHub提供的代码仓库接着由Travis-CI提供的持续集成服务会检测到该仓库发生了变化触发一次Vuepress编译活动生成目录和静态的HTML页面然后推送回GitHub Pages再触发国内的CDN缓存刷新。
如果要想保证这个过程能顺利进行就存在一系列必须要解决的授权问题Travis-CI只有得到了我的明确授权GitHub才能同意它读取我代码仓库中的内容。问题是它该如何获得我的授权呢
一种最简单粗暴的方案是把我的用户账号和密码都告诉Travis-CI但这显然会导致下面这些问题
密码泄漏如果Travis-CI被黑客攻破将导致我的GitHub的密码也同时被泄漏。
访问范围Travis-CI将有能力读取、修改、删除、更新我放在GitHub上的所有代码仓库而我并不希望它能够修改删除文件。
授权回收只有修改密码才能回收我授予给Travis-CI的权限可是我在GitHub的密码只有一个授权的应用除了Travis-CI之外却还有许多修改了就意味着所有别的第三方的应用程序会全部失效。
那么前面列举的这些问题也正是OAuth 2.0所要解决的问题尤其是要求第三方系统在没有支持HTTPS传输安全的环境下依然能够解决这些问题这可不是件容易的事情。
因此OAuth 2.0给出了很多种解决办法这些办法的共同特征是以令牌Token代替用户密码作为授权的凭证。有了令牌之后哪怕令牌被泄漏也不会导致密码的泄漏令牌上可以设定访问资源的范围以及时效性每个应用都持有独立的令牌哪个失效都不会波及其他。
这样一下子前面提出的三个问题就都解决了,有了一层令牌之后,整个授权的流程如下图所示:
这个时序图里涉及到了OAuth 2.0中的几个关键术语,我们根据前面的例子,一起来解读下它们的含义,这对理解后面要介绍的几种授权模式非常重要:
第三方应用Third-Party Application需要得到授权访问我资源的那个应用即此场景中的“Travis-CI”。
授权服务器Authorization Server能够根据我的意愿提供授权授权之前肯定已经进行了必要的认证过程但它与授权可以没有直接关系的服务器即此场景中的“GitHub”。
资源服务器Resource Server能够提供第三方应用所需资源的服务器它与认证服务可以是相同的服务器也可以是不同的服务器即此场景中的“我的代码仓库”。
资源所有者Resource Owner 拥有授权权限的人,即此场景中的“我”。
操作代理User Agent指用户用来访问服务器的工具对于人类用户来说这个通常是指浏览器。但在微服务中一个服务经常会作为另一个服务的用户此时指的可能就是HttpClient、RPCClient或者其他访问途径。
OAuth 2.0的认证流程
当然,“用令牌代替密码”确实是解决问题的好方法,但这充其量只能算个思路,距离可实施的步骤还是不够具体。所以,时序图中的“要求/同意授权”“要求/同意发放令牌”“要求/同意开放资源”这几个服务请求、响应要如何设计,就是执行步骤的关键了。
对此OAuth 2.0一共提出了四种不同的授权方式这是我为什么说OAuth 2.0较为复杂繁琐的其中一个原因),分别为:
授权码模式Authorization Code
简化模式Implicit
密码模式Resource Owner Password Credentials
客户端模式Client Credentials
接下来我们就一一来解读下这四种授权方式的具体流程以此理解OAuth 2.0是如何实现多方系统中相对安全、相对可控的授权的。
授权码模式
授权码模式是四种模式中最严luōsuō它考虑到了几乎所有敏感信息泄露的预防和后果。我们来看看这种模式的具体步骤
这里你要注意在开始进行授权过程之前第三方应用要先到授权服务器上进行注册。所谓的注册是指第三方应用向认证服务器提供一个域名地址然后从授权服务器中获取ClientID和ClientSecret以便能够顺利完成如下的授权过程
第三方应用将资源所有者用户导向授权服务器的授权页面并向授权服务器提供ClientID及用户同意授权后的回调URI这是第一次客户端页面转向。
授权服务器根据ClientID确认第三方应用的身份用户在授权服务器中决定是否同意向该身份的应用进行授权。注意用户认证的过程未定义在此步骤中在此之前就应该已经完成。
如果用户同意授权授权服务器将转向第三方应用在第1步调用中提供的回调URI并附带上一个授权码和获取令牌的地址作为参数这是第二次客户端页面转向。
第三方应用通过回调地址收到授权码然后将授权码与自己的ClientSecret一起作为参数通过服务器向授权服务器提供的获取令牌的服务地址发起请求换取令牌。该服务器的地址应该与注册时提供的域名处于同一个域中。
授权服务器核对授权码和ClientSecret确认无误后向第三方应用授予令牌。令牌可以是一个或者两个其中必定要有的是访问令牌Access Token可选的是刷新令牌Refresh Token。访问令牌用于到资源服务器获取资源有效期较短刷新令牌用于在访问令牌失效后重新获取有效期较长。
资源服务器根据访问令牌所允许的权限,向第三方应用提供资源。
由此你也能看到这个过程设计已经考虑到了几乎所有合理的意外情况。这里我再给你举几个容易遇到的意外情况的例子以便你能够更好地理解为何OAuth 2.0要这样设计:
会不会有其他应用冒充第三方应用骗取授权?
ClientID代表一个第三方应用的“用户名”这项信息是可以完全公开的。但ClientSecret应当只有应用自己才知道这个代表了第三方应用的“密码”。在第5步发放令牌时调用者必须能够提供ClientSecret才能成功完成。只要第三方应用妥善保管好ClientSecret就没有人能够冒充它。
为什么要先发放授权码,再用授权码换令牌?
这是因为客户端转向通常就是一次HTTP 302重定向对于用户是可见的。换言之授权码可能会暴露给用户以及用户机器上的其他程序但由于用户并没有ClientSecret光有授权码也无法换取到令牌所以就避免了令牌在传输转向过程中被泄漏的风险。
为什么要设计一个时限较长的刷新令牌和时限较短的访问令牌?不能直接把访问令牌的时间调长吗?-
-
这是为了缓解OAuth 2.0在实际应用中的一个主要缺陷。因为通常情况下,访问令牌一旦发放,除非超过了令牌中的有效期,否则很难有其他方式让它失效。所以访问令牌的时效性一般会设计得比较短,比如几个小时,如果还需要继续用,那就定期用刷新令牌去更新,授权服务器可以在更新过程中决定是否还要继续给予授权。至于为什么说很难让它失效,我们将放到下一讲“凭证”中去解释。
不过尽管授权码模式是很严谨的但它并不够好用这不仅仅体现在它那繁复的调用过程上还体现在它对第三方应用提出了一个“貌似不难”的要求第三方应用必须有应用服务器因为第4步要发起服务端转向而且要求服务端的地址必须与注册时提供的地址在同一个域内。
你不要觉得要求一个系统要有应用服务器是天经地义理所当然的事情“The Fenix Project”这部文档的官方网站就没有任何应用服务器的支持里面使用到了Gittalk作为每篇文章的留言板它对GitHub来说照样是第三方应用需要OAuth 2.0授权来解决。
除了基于浏览器的应用外现在越来越普遍的是移动或桌面端的客户端Web应用Client-Side Web Applications比如现在大量的基于Cordova、Electron、Node-Webkit.js的PWA应用它们都不会有应用服务器的支持。
正是因为有这样的实际需求就引出了OAuth 2.0的第二种授权模式:隐式授权。
隐式授权
隐式授权省略掉了通过授权码换取令牌的步骤整个授权过程都不需要服务端的支持一步到位。而使用的代价是在隐式授权中授权服务器不会再去验证第三方应用的身份因为已经没有应用服务器了ClientSecret没有人保管就没有存在的意义了。
但隐式授权中的授权服务器还是会限制第三方应用的回调URI地址必须与注册时提供的域名一致虽然有可能会被DNS污染之类的攻击所攻破但这也算是它尽可能地努力了一下吧。同样的原因隐式授权也不能避免令牌暴露给资源所有者不能避免用户机器上可能意图不轨的其他程序、HTTP的中间人攻击等风险。
隐式授权的调用时序图如下图所示后面展示的几种授权模式时序图中我就不再画出资源访问部分的内容了就是前面授权码图例中opt框里的那一部分以便更聚焦重点
你可以发现,在这个交互过程里,隐式模式与授权码模式的显著区别是授权服务器在得到用户授权后,直接返回了访问令牌,这很明显会降低授权的安全性。
但OAuth 2.0仍然在尽可能地努力做到相对安全,比如前面我提到在隐式授权中,尽管不需要用到服务端,但仍然需要在注册时提供回调域名,此时会要求该域名与接受令牌的服务处于同一个域内。此外,同样基于安全考虑,在隐式模式中也明确禁止发放刷新令牌。
还有一点在RFC 6749对隐式授权的描述中特别强调了令牌必须是“通过Fragment带回”的。如果你对超文本协议没有多少了解的话可能还不知道Fragment是个什么东西我们来看一下它的英文释义。
Fragment-
In computer hypertext, a fragment identifier is a string of characters that refers to a resource that is subordinate to another, primary resource. The primary resource is identified by a Uniform Resource Identifier (URI), and the fragment identifier points to the subordinate resource.-
——URI FragmentWikipedia
要是你看完后还是觉得概念不好理解的话我就简单告诉你Fragment就是地址中“#”号后面的部分,比如这个地址:
http://bookstore.icyfenix.cn/#/detail/1
后面的/detail/1便是Fragment这个语法是在RFC 3986中定义的。该规范中解释了这是用于客户端定位的URI从属资源比如在HTML中就可以使用Fragment来做文档内的跳转而不会发起服务端请求。
此外RFC 3986还规定了如果浏览器对一个带有Fragment的地址发出Ajax请求那Fragment是不会跟随请求被发送到服务端的只能在客户端通过Script脚本来读取。
所以,隐式授权巧妙地利用这个特性,尽最大努力地避免了令牌从操作代理到第三方服务之间的链路,存在被攻击而泄露出去的可能性。
至于认证服务器到操作代理之间的这一段链路的安全则只能通过TLS即HTTPS来保证不会受到中间人攻击了我们可以要求认证服务器必须都是启用HTTPS的但无法要求第三方应用同样都支持HTTPS。
密码模式
前面所说的授权码模式和隐式模式都属于纯粹的授权模式,它们与认证没有直接的联系,如何认证用户的真实身份,跟如何进行授权是两个互相独立的过程。但在密码模式里,认证和授权就被整合成了同一个过程。
密码模式原本的设计意图是,仅限于在用户对第三方应用是高度可信任的场景中使用,因为用户需要把密码明文提供给第三方应用,第三方以此向授权服务器获取令牌。
这种高度可信的第三方是非常罕见的尽管在介绍OAuth 2.0的材料中,经常举的例子是“操作系统作为第三方应用向授权服务器申请资源”,但真实应用中极少遇到这样的情况,合理性依然十分有限。
我认为,如果要采用密码模式,那“第三方”属性就必须弱化,把“第三方”看作是系统中与授权服务器相对独立的子模块,在物理上独立于授权服务器部署,但是在逻辑上与授权服务器仍同属一个系统。这样把认证和授权一并完成的密码模式,才会有合理的应用场景。
比如说Fenixs Bookstore就直接采用了密码模式将认证和授权统一到一个过程中完成尽管Fenixs Bookstore中的Frontend工程和Account工程都能直接接触到用户名和密码但它们事实上都是整个系统的一部分在这个前提下密码模式才具有可用性关于分布式系统各个服务之间的信任关系我会在“零信任网络”与“服务安全”两讲中和你作进一步讨论
这样,理解了密码模式的用途,你再去看它的调用过程就很简单了,也就是第三方应用拿着用户名和密码向授权服务器换令牌而已。具体的时序如下图所示:
此外你还要明确一件事在密码模式下“如何保障安全”的职责无法由OAuth 2.0来承担只能由用户和第三方应用来自行保障尽管OAuth 2.0在规范中强调到“此模式下,第三方应用不得保存用户的密码”,但这并没有任何技术上的约束力。
OK我们再来看看OAuth 2.0的最后一种授权模式:客户端模式。
客户端模式
客户端模式是四种模式中最简单的,它只涉及到两个主体:第三方应用和授权服务器。如果我们严谨一点,现在叫“第三方应用”其实已经不合适了,因为已经没有了“第二方”的存在,资源所有者、操作代理在客户端模式中都是不必出现的。甚至严格来说,叫“授权”都已经不太恰当,毕竟资源所有者都没有了,也就不会有谁授予谁权限的过程。
那么,客户端模式就是指第三方应用(考虑到前后统一,我们还是继续沿用这个称呼)以自己的名义,向授权服务器申请资源许可。这种模式通常用于管理操作或者自动处理类型的场景中。
举个具体例子。比如我开了一家叫Fenixs Bookstore的书店因为小本经营不像京东那样全国多个仓库可以调货因此我必须保证只要客户成功购买书店就必须有货可发不允许超卖。但问题是经常有顾客下了订单又拖着不付款导致部分货物处于冻结状态。
所以Fenixs Bookstore中有一个订单清理的定时服务自动清理超过两分钟还未付款的订单。在这个场景里订单肯定是属于下单用户自己的资源如果把订单清理服务看作是一个独立的第三方应用的话它是不可能向下单用户去申请授权来删掉订单的而是应该直接以自己的名义向授权服务器申请一个能清理所有用户订单的授权。那么这个客户端模式的时序就会是这样的
在微服务架构中,其实并不提倡同一个系统的各服务间有默认的信任关系,所以服务之间的调用也需要先进行认证授权,然后才能通讯。
那么此时客户端模式便是一种常用的服务间认证授权的解决方案。Spring Cloud版本的Fenixs Bookstore就是采用这种方案来保证微服务之间的合法调用的而Istio版本的Fenixs Bookstore则启用了双向mTLS通讯使用客户端证书来保障安全。它们可作为上一节课我介绍认证时提到的“通讯信道认证”和“通讯内容认证”的例子你要是感兴趣可以对比一下这两种方式的差异优劣。
此外在OAuth 2.0中呢还有一种与客户端模式类似的授权模式在RFC 8628中定义为“设备码模式”Device Code这里我顺带提一下。
设备码模式用于在无输入的情况下区分设备是否被许可使用,典型的应用就是手机锁网解锁(锁网在国内较少,但在国外很常见)或者设备激活(比如某游戏机注册到某个游戏平台)的过程。它的时序如下图所示:
这里你可以记着采用设备码模式在进行验证时设备需要从授权服务器获取一个URI地址和一个用户码然后需要用户手动或设备自动地到验证URI中输入用户码。在这个过程中设备会一直循环尝试去获取令牌直到拿到令牌或者用户码过期为止。
小结
这节课我们学习了如何使用OAuth 2.0来解决涉及到多方系统调用时可靠授权的问题并详细了解了OAuth 2.0协议的授权码模式、隐式授权模式、密码模式和客户端模式的工作流程。
实际上,无论是哪一种授权模式,它们都属于保障授权过程可靠的实现方案。那么,系统要如何确保授权的结果可控呢?别着急,在下节课中,我就来给你揭晓答案。
一课一思
OAuth 2.0的核心思想是令牌代替密码,令牌是我们讲“凭证”这节课的主角,在这里你能否先想象一下,所谓的“令牌”应该是一种怎样的数据结构?它有什么特点?有什么必须的信息?
欢迎在留言区分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 _ 授权(下):系统如何确保授权的结果可控?
你好,我是周志明。今天,我们接着上一讲的话题,继续来探究关于授权的第二个核心问题:系统如何确保授权的结果可控?
在上节课的开篇我提到了授权的结果是用于对程序功能或者资源的访问控制Access Control并且也给你介绍了一种最为常用的权限控制模型RBAC基于角色的访问控制Role-Based Access Control
那么这节课我就来和你聊聊这种访问控制模型的概念、原理和一些要注意的问题。希望你能在理解了RBAC是如何运作的之后将其灵活运用在自己实际工作中关于功能、数据权限的管理上而且这也是为后面学习Kubernetes的权限控制、服务安全等内容提前做的铺垫工作。
接下来我们就从RBAC的几个基础概念开始学起吧。
RBAC的基础概念
首先我们要明确所有的访问控制模型实质上都是在解决同一个问题User拥有什么权限Authority去操作Operation哪些资源Resource
这个问题初看起来并不太难一种直观的解决方案就是在用户对象上设定一些权限当用户使用资源时检查是否有对应的操作权限即可。很多著名的安全框架比如Spring Security的访问控制本质上就是支持这么做的。
不过,这种把权限直接关联在用户身上的简单设计,在复杂系统上确实会导致一些比较繁琐的问题。
你可以试想一下,如果某个系统涉及到成百上千的资源,又有成千上万的用户,一旦两者搅合到一起,要为每个用户访问每个资源都分配合适的权限,就必定会导致巨大的操作量和极高的出错概率。
而这也正是RBAC所关注的核心问题。
RBAC模型在业界有很多种说法其中最具系统性且得到了普遍认可的说法是美国乔治梅森George Mason大学信息安全技术实验室提出的RBAC96模型。
为了避免对每一个用户设定权限RBAC将权限从用户身上剥离改为绑定到“角色”Role“权限控制”这项工作就可以具体化成针对“角色拥有操作哪些资源的许可”这个逻辑表达式的值是否为真的求解过程。
这个逻辑表达式中涉及的关键概念有用户、角色、资源等我画了张图你可以参考图中展示的RBAC主要元素之间的关系
其中你可能发现了除了前面提到的用户、角色和资源以外图上还出现了一个新的名词“许可”Permission
许可其实是抽象权限的具象化体现。权限在RBAC系统中的含义是“允许何种操作作用于哪些资源之上”这句话的具体实例即为“许可”。
提出许可这个概念的目的,其实跟提出角色的目的是完全一致的,只是许可会更抽象。角色为的是解耦用户与权限之间的多对多关系,而许可为的是解耦操作与资源之间的多对多关系。比如说,不同的数据都能够有增、删、改等操作,而如果把操作与数据搅和在一起,也会面临前面我提到的权限配置膨胀的问题。
不过现在,你可能快被这些概念、逻辑给绕晕了。没事儿,我再给你举个更具体的例子,帮你理清这一堆名词之间的关系。
想像一下某个论文管理系统的UserStory中与访问控制相关的Backlog可能会是这样描述的
Backlog周同学User是某SCI杂志的审稿人Role职责之一是在系统中审核论文Authority。在审稿过程Session当他认为某篇论文Resource达到了可以公开发表的标准时就会在后台点击“通过”按钮Operation来完成审核。
所以在这个Backlog中“给论文点击通过按钮”就是一种许可它是“审核论文”这项权限的具象化体现。现在你是不是就清楚一些了
另外我还想强调的是采用RBAC的角色、资源等概念不仅是为了简化配置操作通过设定这些概念之间的关系与约束还是很多关键的安全原则和设计原则的实现基础。下面我们就从计算机安全中的“最小特权原则”Least Privilege开始来了解一下吧。
RBAC的概念间关系
在RBAC模型中角色拥有许可的数量是根据完成该角色工作职责所需的最小权限所赋予的。
最典型的例子是操作系统权限管理中的用户组。即根据对不同角色的职责分工如管理员Administrator、系统用户System、验证用户Authenticated Users、普通用户Users、来宾用户Guests分配其各自的权限。这样就既保证了用户能够正常工作也避免了用户出现越权操作的风险。
而当用户的职责发生变化时,在系统中就体现为它所隶属的角色被改变,比如将“普通用户角色”改变为“管理员角色”,就可以迅速让该用户具备管理员的多个细分权限,降低权限分配错误的风险。
另外RBAC还允许定义不同角色之间的关联与约束关系以此进一步强化它的抽象描述能力。
比如说不同的角色之间可以有继承性典型的就是RBAC-1模型的角色权限继承关系。
我举个例子。如果要描述开发经理应该和开发人员一样具有代码提交的权限,描述开发人员应该和任何公司的员工一样具有食堂就餐的权限,那么我们就可以直接把食堂就餐的权限赋予到公司员工的角色上,把代码提交的权限赋予到开发人员的角色上,再让开发人员的角色从公司员工派生,开发经理的角色从开发人员中派生即可。
另外不同的角色之间也可以具有互斥性典型的就是RBAC-2模型的角色职责分离关系。互斥性要求当权限被赋予角色时、或角色被赋予用户时应该遵循的强制性职责分离规定。
我举个例子。角色的互斥约束可以限制同一用户,只能分配到一组互斥角色集合中至多一个角色,比如不能让同一名员工既当会计,也当出纳,否则资金安全无法保证。而角色的基数约束可以限制某一个用户拥有的最大角色数目,比如不能让同一名员工全部包揽产品、设计、开发、测试等工作,否则产品质量无法保证。
OK现在我们就了解了通过RBAC建立的用户、角色等概念并且也定义了它们之间的关联与约束关系其实这些都属于RBAC中“Role Based”范畴的内容而RB只是手段是为了AC这个目的服务的。
所以接下来我们就一起来看看“Access Control”范畴的内容也就是RBAC的访问控制。
RBAC的访问控制
建立访问控制模型的基本目的就是为了管理垂直权限和水平权限。垂直权限即功能权限,比如前面提到的审稿编辑有通过审核的权限、开发经理有代码提交的权限、出纳有从账户提取资金的权限,这一类某个角色完成某项操作的许可,都可以直接翻译为功能权限。
由于实际应用与权限模型具有高度对应的关系因此把权限从具体的应用中抽离出来放到通用的模型中是相对容易的Spring Security、Apache Shiro等权限框架就是这样的抽象产物大多数系统都能采用这些权限框架来管理功能权限。
那么与此相对要管理水平权限也就是数据权限的话则要困难得多。比如用户A、B都属于同一个角色但它们各自在系统中产生的数据完全有可能是私有的A访问或删除了B的数据也照样属于越权。
一般来说,数据权限是很难抽象与通用的,仅在角色层面进行控制并不能满足全部业务的需要。很多时候,数据权限必须具体到用户,甚至具体管理到发生数据的某一行、某一列之上,因此数据权限基本上只能由信息系统自主来完成,并不存在能放之四海皆准的通用数据权限框架。
Spring Security的RBAC实现
在课程后面要介绍的“不可变基础设施”的模块里其中要讲解的一个“重要角色”Kubernetes也是完全遵循RBAC模型来进行服务访问控制的。Fenixs Bookstore所使用的Spring Security也参考了但并没有完全遵循RBAC来设计它的访问控制功能所以这里我就以Spring Security为例给你简要介绍一下RBAC的实现。
在Spring Security的设计里用户和角色都可以拥有权限比如在它的HttpSecurity接口就同时有着hasRole()和hasAuthority()方法如果你是刚接触Spring Security的设计的话可能会混淆它们之间的关系。那么下面我们就直接来看看Spring Security的访问控制模型是长什么样子的你也可以去对比一下前面的RBAC的关系图
从实现角度来看Spring Security中Role和Authority的差异很小它们完全共享同一套存储结构唯一的差别就只是Role会在存储时自动带上“ROLE_”前缀罢了。
但从使用角度来看Role和Authority的差异可以很大用户可以自行决定系统中到底Permission只能对应到角色身上还是可以让用户也拥有某些角色中没有的权限。
你应该会觉得这一点好像不符合RBAC的思想但我个人认为这是一种创新而非破坏在Spring Security的文档上也说的很清楚这取决于你自己如何使用。
The core difference between these two指Role和Authority is the semantics we attach to how we use the feature. For the framework, the difference is minimal and it basically deals with these in exactly the same way.
这样我们通过RBAC就很容易控制最终用户在广义和精细级别上能够做什么我们可以指定用户是管理员、专家用户或者只是普通用户并使角色和访问权限与组织中员工的身份职位保持一致仅根据需要为员工完成工作的最低限度来分配权限。
这些都是人们通过设计大量的软件系统、长时间积累下来的实践经验,将这些经验运用在软件产品上,绝大多数情况下都要比自己发明创造一个新的轮子更加安全。
小结
针对如何确保授权的结果可控的问题这节课我们学习了一种最常用的解决方案基于角色的访问控制RBAC
其中在Role-Based部分我通过一些例子给你介绍了以角色为中心的一系列概念以及这些概念之间的关系与约束在Access Control部分我还介绍了垂直和水平权限的控制的差异也以Spring Security为例带你了解了它的大致运作过程。你需要记住以下几个核心要点
所有的访问控制模型实质上都是在解决同一个问题User拥有什么权限Authority去操作Operation哪些资源Resource
为避免对每一个用户设定权限RBAC提出了角色和许可等概念角色为的是解耦用户与权限之间的多对多关系而许可为的是解耦操作与资源之间的多对多关系。
建立访问控制模型的基本目的就是为了管理垂直权限和水平权限。垂直权限即功能权限,水平权限则是数据权限,它很难抽象与通用。
一课一思
你是否使用过RBAC或者是其他权限控制模型你是自己实现的还是基于现成框架实现的
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给其他的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,241 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 _ 凭证:系统如何保证与用户之间的承诺是准确完整且不可抵赖的?
你好,我是周志明。
在第24讲我给你介绍OAuth 2.0协议的时候提到过每一种授权模式的最终目标都是拿到访问令牌但我并没有讲拿回来的令牌应该长什么样子反而还挖了一些坑没有填即为什么说OAuth 2.0的一个主要缺陷是令牌难以主动失效。
所以这节课我们要讨论的主角就是令牌了。我会带你了解令牌的结构、原理与实现让你明确系统是如何保证它和用户之间的承诺是双方当时意图的体现、是准确完整且不可抵赖的另外我还会跟你一起看看如果不使用OAuth 2.0的话,通过最传统的状态管理机制的方式,系统要如何完成认证和授权。
那接下来我们就先来看看HTTP协议中最传统的状态管理机制Cookie-Session是如何运作的吧。
Cookie-SessionHTTP的状态管理机制
我们应该都知道HTTP协议是一种无状态的传输协议也就是协议对事务处理没有上下文的记忆能力每一个请求都是完全独立的。但是我想肯定很多人都没有意识到HTTP协议无状态的重要性。
为什么这么说呢假如你做了一个简单的网页其中包含了1个HTML、2个Script脚本、3个CSS还有10张图片那么这个网页要想成功地展示在用户屏幕前就需要完成16次与服务器的交互来获取这些资源。
但是因为网络传输等各种因素的影响服务器发送的顺序与客户端请求的先后并没有必然的联系所以按照可能出现的响应顺序理论上最多会有P(16,16) = 20,922,789,888,000种可能性。
所以我们可以试想一下如果HTTP协议不是设计成无状态的这16次请求每一个都有依赖关联先调用哪一个、先返回哪一个都会对结果产生影响的话那么服务器与客户端交互的协调工作会有多么复杂。
可是HTTP协议的无状态特性又有悖于我们最常见的网络应用场景典型的就是认证授权毕竟系统总得要获知用户身份才能提供合适的服务。因此我们也希望HTTP能有一种手段让服务器至少有办法区分出发送请求的用户是谁。
所以为了实现这个目的RFC 6265规范就定义了HTTP的状态管理机制在HTTP协议中增加了Set-Cookie指令。
这个指令的含义是以键值对的方式向客户端发送一组信息在此后一段时间内的每次HTTP请求中这组信息会附带着名为Cookie的Header重新发回给服务端以便服务器区分来自不同客户端的请求。
我们直接来看一个典型的Set-Cookie指令具体是怎么做的
Set-Cookie: id=icyfenix; Expires=Wed, 21 Feb 2020 07:28:00 GMT; Secure; HttpOnly
服务器在收到该指令以后客户端再对同一个域的请求中就会自动附带有键值对信息“id=icyfenix”比如说
GET /index.html HTTP/2.0
Host: icyfenix.cn
Cookie: id=icyfenix
那么根据每次请求传到服务端的Cookie服务器就能分辨出请求来自于哪一个用户。由于Cookie是放在请求头上的属于额外的传输负担不应该携带过多的内容而且放在Cookie中传输也并不安全容易被中间人窃取或篡改所以在实际情况中通常是不会像这个例子一样设置“id=icyfenix”这样的明文信息的。
一般来说系统会把状态信息保存在服务端而在Cookie里只传输一个无字面意义的、不重复的字符串通常习惯上是以sessionid或者jsessionid为名。然后服务器拿这个字符串为Key在内存中开辟一块空间以Key/Entity的结构来存储每一个在线用户的上下文状态再辅以一些超时自动清理之类的管理措施。
这种服务端的状态管理机制就是今天我们非常熟悉的Session。Cookie-Session也就是最传统的但在今天依然广泛应用于大量系统中的、由服务端与客户端联动来完成的状态管理机制。
Cookie-Session的方案在安全架构的系统当中其实是占有一定天生优势的因为状态信息都存储于服务器只要依靠客户端的同源策略和HTTPS的传输层安全保证Cookie中的键值不被窃取而出现被冒认身份的情况就能完全规避掉信息在传输过程中被泄露和篡改的风险。
Cookie-Session方案另一大优点是服务端有主动的状态管理能力可以根据自己的意愿随时修改、清除任意的上下文信息比如很轻易就能实现强制某用户下线这样的功能。
不过Cookie-Session在单节点的单体服务环境中确实是最合适的方案但当服务器需要具备水平扩展服务能力要部署集群时就有点儿麻烦了。
因为Session存储在服务器的内存中那么当服务器水平拓展成多节点时我们在设计时就必须在以下三种方案中选择其一
要么就牺牲集群的一致性Consistency让均衡器采用亲和式的负载均衡算法。比如根据用户IP或者Session来分配节点每一个特定用户发出的所有请求都一直被分配到其中某一个节点来提供服务每个节点都不重复地保存着一部分用户的状态如果这个节点崩溃了里面的用户状态便完全丢失。
要么就牺牲集群的可用性Availability让各个节点之间采用复制式的Session每一个节点中的Session变动都会发送到组播地址的其他服务器上这样即使某个节点崩溃了也不会中断某个用户的服务。但Session之间组播复制的同步代价比较高昂节点越多时同步成本就越高。
要么就牺牲集群的分区容错性Partition Tolerance让普通的服务节点中不再保留状态将上下文集中放在一个所有服务节点都能访问到的数据节点中进行存储。此时的矛盾是数据节点就成为了单点一旦数据节点损坏或出现网络分区整个集群都不能再提供服务。
通过第14讲内容的学习现在我们已经知道了只要在分布式系统中共享信息CAP就不可兼得所以分布式环境中的状态管理一定会受到CAP的局限无论怎样都不可能完美。
但如果,我们只是解决分布式下的认证授权问题,并顺带解决少量状态的问题,就不一定只能依靠共享信息去实现了。
我说这句话的言外之意是想先提醒一下你接下来我要给你介绍的JWT令牌跟Cookie-Session并不是完全对等的解决方案它只用来处理认证授权问题是Cookie-Session在认证授权问题上的替代品充其量能携带少量非敏感的信息。而我们不能说JWT要比Cookie-Session更加先进它也更不可能全面取代Cookie-Session机制。
JWT解决认证授权问题的无状态方案
现在我们知道了Cookie-Session机制在分布式环境下会遇到CAP不可兼得的问题而在多方系统中也就更不可能谈什么Session层面的数据共享了哪怕服务端之间能共享数据客户端的Cookie也没法跨域。
所以,我们不得不重新捡起最初被抛弃的思路:当服务器存在多个,客户端只有一个时,那就把状态信息存储在客户端,每次随着请求发回服务器中去。可是奇怪了,前面我才说过,这样做的缺点是无法携带大量信息,而且有泄露和篡改的安全风险。
其实啊信息量受限的问题目前并没有太好的解决办法但是要确保信息不被中间人篡改还是可以实现的JWT便是这个问题的标准答案。
JWTJSON Web Token定义于RFC 7519标准之中是目前广泛使用的一种令牌格式尤其经常与OAuth 2.0配合应用于分布式的、涉及多方的应用系统中。
那么在介绍JWT的具体构成之前我们先来直观地看一下它是什么样子的
这个示意图来源于JWT官网数据则是我随意编的。图上右边的JSON结构是JWT令牌中携带的信息左边的字符串呈现了JWT令牌的本体。它最常见的使用方式是附在名为Authorization的Header发送给服务端其前缀在RFC 6750中被规定为Bearer。
如果你没有忘记第23讲“认证方案”与第24讲“OAuth 2.0”的知识内容那么当你在看到Authorization这个Header与Bearer这个前缀的时候就应该能意识到它是HTTP认证框架中的OAuth 2.0认证方案。下面的示例代码就展示了一次采用JWT令牌的HTTP实际请求
GET /restful/products/1 HTTP/1.1
Host: icyfenix.cn
Connection: keep-alive
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpY3lmZW5peCIsInNjb3BlIjpbIkFMTCJdLCJleHAiOjE1ODQ5NDg5NDcsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwianRpIjoiOWQ3NzU4NmEtM2Y0Zi00Y2JiLTk5MjQtZmUyZjc3ZGZhMzNkIiwiY2xpZW50X2lkIjoiYm9va3N0b3JlX2Zyb250ZW5kIiwidXNlcm5hbWUiOiJpY3lmZW5peCJ9.539WMzbjv63wBtx4ytYYw_Fo1ECG_9vsgAn8bheflL8
另外我还要跟你强调的是在前面的令牌结构示意图中右边的状态信息是对令牌使用Base64URL转码后得到的明文请你特别注意它是明文。
毕竟JWT只解决防篡改的问题并不解决防泄露的问题所以令牌默认是不加密的。尽管你自己要加密的话也并不难做到接收时自行解密即可但这样做其实没有太大的意义具体原因我这里先卖个关子下一节课我讲“保密”的时候再给你详细解释。
JWT令牌的三部分结构
那么从前面给出的明文中你已经知道JWT令牌是以JSON结构毕竟名字就叫JSON Web Token存储的其结构总体上可以划分为三个部分每个部分用点号“ . ”分隔开。
令牌的第一部分是令牌头Header其内容如下所示
{
"alg": "HS256",
"typ": "JWT"
}
这里你可以看到它描述了令牌的类型统一为typ:JWT以及令牌签名的算法示例中HS256为HMAC SHA256算法的缩写其他各种系统支持的签名算法你可以参考JWT官网。
额外知识:散列消息认证码-
在这一讲及后面其他关于安全的课程内容中你会经常看到在某种哈希算法前出现“HMAC”的前缀这是指散列消息认证码Hash-based Message Authentication CodeHMAC。你可以简单将它理解为一种带有密钥的哈希摘要算法实现形式上通常是把密钥以加盐方式混入与内容一起做哈希摘要。-
HMAC哈希与普通哈希算法的差别是普通的哈希算法通过Hash函数结果易变性保证了原有内容未被篡改而HMAC不仅保证了内容未被篡改过还保证了该哈希确实是由密钥的持有人所生成的。
令牌的第二部分是负载Payload这是令牌真正需要向服务端传递的信息。针对认证问题负载至少应该包含能够告知服务端“这个用户是谁”的信息针对授权问题令牌至少应该包含能够告知服务端“这个用户拥有什么角色/权限”的信息。
JWT的负载部分是可以完全自定义的我们可以根据具体要解决的问题设计自己所需要的信息只是总容量不能太大毕竟它受HTTP Header大小的限制。下面我们来看一个JWT负载的例子
{
"username": "icyfenix",
"authorities": [
"ROLE_USER",
"ROLE_ADMIN"
],
"scope": [
"ALL"
],
"exp": 1584948947,
"jti": "9d77586a-3f4f-4cbb-9924-fe2f77dfa33d",
"client_id": "bookstore_frontend"
}
另外JWT在RFC 7519标准中推荐非强制约束了七项声明名称Claim Name如果你在设计令牌时需要用到这些内容我建议其字段名要与官方的保持一致
issIssuer签发人。
expExpiration Time令牌过期时间。
subSubject主题。
aud Audience令牌受众。
nbf Not Before令牌生效时间。
iat Issued At令牌签发时间。
jti JWT ID令牌编号。
补充除此之外在RFC 8225、RFC 8417、RFC 8485等规范文档以及OpenID等协议当中都定义有约定好公有含义的名称内容比较多我就不贴出来了你可以参考IANA JSON Web Token Registry。
令牌的第三部分是签名Signature。签名的意思是使用在对象头中公开的特定签名算法通过特定的密钥Secret由服务器进行保密不能公开对前面两部分内容进行加密计算产生签名值。
这里我们继续以前面例子里使用的JWT默认的HMAC SHA256算法为例它会通过以下公式产生签名值
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)
签名的意义在于,它可以确保负载中的信息是可信的、没有被篡改的,也没有在传输过程中丢失任何信息。因为被签名的内容哪怕是发生了一个字节的变动,也会导致整个签名发生显著变化。
此外由于签名这件事情只能由认证授权服务器完成只有它知道Secret任何人都无法在篡改后重新计算出合法的签名值所以服务端才能够完全信任客户端传上来的JWT中的负载信息。
JWT默认的签名算法HMAC SHA256是一种带密钥的哈希摘要算法加密与验证过程都只能由中心化的授权服务来提供所以这种方式一般只适合于授权服务与应用服务处于同一个进程中的单体应用。
另外在多方系统或者是授权服务与资源服务分离的分布式应用当中通常会采用非对称加密算法来进行签名。这时候除了授权服务端持有的可以用于签名的私钥以外还会对其他服务器公开一个公钥公开方式一般遵循JSON Web Key规范。
不过这个公钥不能用来签名但它能被其他服务用于验证签名是否由私钥所签发的。这样其他服务器就也能不依赖授权服务器、无需远程通讯即可独立判断JWT令牌中的信息的真伪了。
在后面课程会展示的Fenixs Bookstore的单体服务版本中我们采用了默认的HMAC SHA256算法来加密签名而在Istio服务网格版本里终端用户认证会由服务网格的基础设施来完成此时就改用了非对称加密的RSA SHA256算法来进行签名。如果你还想更深入地了解凭证安全到时不妨对比一下这两部分的代码。更多关于哈希摘要、对称和非对称加密的讨论我将会在“传输”这个小章节中继续展开介绍。
JWT令牌的缺陷
现在我们知道JWT令牌是多方系统中的一种优秀的凭证载体它不需要任何一个服务节点保留任何一点状态信息就能够保障认证服务与用户之间的承诺是双方当时真实意图的体现是准确、完整、不可篡改且不可抵赖的。
同时由于JWT本身可以携带少量信息这十分有利于RESTful API的设计比较容易地做成无状态服务我们在做水平扩展时就不需要像前面Cookie-Session方案那样考虑如何部署的问题了。在现实应用中也确实有一些项目直接采用JWT来承载上下文信息以此实现完全无状态的服务端这样就可以获得任意加入或移除服务节点的巨大便利天然具有完美的水平扩缩能力。
比如在调试Fenixs Bookstore的代码时你随时都可以重启服务在重启后客户端仍然能毫无感知地继续操作流程而对于有状态的系统就必须通过重新登录、进行前置业务操作来为服务端重建状态。尽管在大型系统中只使用JWT来维护上下文状态服务端完全不持有状态是不太现实的不过将热点的服务单独抽离出来做成无状态仍然是一种有效提升系统吞吐能力的架构技巧。
但是JWT也并不是一种完美的解决方案它存在着以下几个经常被提及的缺点
令牌难以主动失效
JWT令牌一旦签发理论上就和认证服务器没有什么瓜葛了在到期之前就会始终有效除非我们在服务器部署额外的逻辑去处理失效问题而这对某些管理功能的实现是很不利的。比如说一种十分常见的需求是要求一个用户只能在一台设备上登录在B设备登录后之前已经登录过的A设备就应该自动退出。
如果我们采用JWT就必须设计一个“黑名单”的额外逻辑把要主动失效的令牌集中存储起来而无论这个黑名单是实现在Session、Redis还是数据库当中都会让服务退化成有状态服务这就降低了JWT本身的价值。但在使用JWT时设置黑名单依然是很常见的做法需要维护的黑名单一般是很小的状态量因此在许多场景中还是有存在价值的。
相对更容易遭受重放攻击
这里首先我要说明Cookie-Session也是有重放攻击问题的只是因为Session中的数据控制在服务端手上应对重放攻击会相对主动一些。
但是要在JWT层面解决重放攻击就需要付出比较大的代价了因为无论是加入全局序列号HTTPS协议的思路、Nonce字符串HTTP Digest验证的思路、挑战应答码当下网银动态令牌的思路、还是缩短令牌有效期强制频繁刷新令牌在真正应用起来时都很麻烦。
而真要处理重放攻击的话我建议的解决方案是在信道层次比如启用HTTPS上解决而不提倡在服务层次比如在令牌或接口其他参数上增加额外逻辑上解决。
只能携带相当有限的数据
HTTP协议并没有强制约束Header的最大长度但是各种服务器、浏览器都会有自己的约束比如Tomcat就要求Header最大不超过8KB而在Nginx中则默认为4KB。所以在令牌中存储过多的数据不仅耗费传输带宽还有额外的出错风险。
必须考虑令牌在客户端如何存储
严谨地说这个并不是JWT的问题而是系统设计的问题。如果在授权之后操作完关掉浏览器就结束了那把令牌放到内存里面压根不考虑持久化其实才是最理想的方案。
但并不是谁都能忍受一个网站关闭之后下次就一定强制要重新登录的。这样的话你想想客户端该把令牌存放到哪里呢CookielocalStorage还是Indexed DB它们都有泄露的可能而令牌一旦泄露别人就可以冒充用户的身份做任何事情。
无状态也不总是好的
这个其实不也是JWT的问题。如果不能想像无状态会有什么不好的话我给你提个需求请基于无状态JWT的方案做一个在线用户实时统计功能。兄弟难搞哦。
小结
Cookie-Session机制是为HTTP量身定做的经典凭证实现方案它曾经为信息系统解决过无数问题。不过随着微服务的流行分布式系统变得越来越主流因此由于分布式下共享数据的CAP矛盾就导致了Cookie-Session在一些场景中遇到了C与A难以取舍的情况。
而无状态的JWT方案在合适的场景下确实可以带来实实在在的好处它可以让服务端水平扩容变得异常容易不用担心Session复制的效率问题也不用担心Session挂掉后整个集群全部无法正常工作的问题。
然而场景二字仍然是关键词脱离了具体场景我们就很难说哪种凭证方案更好或者更坏在这节课中我也特别强调了JWT的几个缺点。你要记住权衡才是架构设计中最关键的地方。
一课一思
这节课我给你介绍了Cookie-Session和JWT两种最常见的凭证实现除此之外你还知道其他凭证的实现方案吗它们都有什么应用场景和优缺点
欢迎给我留言,分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,184 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 _ 保密:系统如何保证敏感数据无法被内外部人员窃取滥用?
你好,我是周志明。这节课,我们来讨论在信息系统中,一个一直非常受人关注的安全性议题:保密。
保密是加密和解密的统称,意思就是以某种特殊的算法改变原有的信息数据,使得未授权的用户即使获得了已加密的信息,但因为不知道解密的方法,或者就算知晓解密的算法、但缺少解密所需的必要信息,所以仍然无法了解数据的真实内容。
那么,根据需要保密信息所处的不同环节,我们可以将其划分为“信息在客户端时的保密”“信息在传输时的保密”和“信息在服务端时的保密”三类,或者也可以进一步概括为“端的保密”和“链路的保密”两类。
这里,我们先把最复杂、最有效,但是又最早就有了标准解决方案的“传输”环节单独拿出来,放到后面两讲中展开探讨。在今天的这节课当中,我们只讨论两个端的环节,即在客户端和服务端中的信息保密问题。
保密的强度
好,首先我们要知道,保密是有成本的,追求越高的安全等级,我们就要付出越多的工作量与算力消耗。就连国家保密法都会把秘密信息划分为秘密、机密、绝密三级来区别对待,可见即使是信息安全,也应该有所取舍。
那么接下来,我就以用户登录为例,给你列举几种不同强度的保密手段,看看它们的防御关注点与弱点分别都是什么。这里你需要注意的是,以下提及到的不同保密手段,并不一定就是正确的做法,只是为了强调保密手段是有成本、有不同的强度的。
以摘要代替明文
如果密码本身比较复杂,那么一次简单的哈希摘要就至少可以保证,即使在传输过程中有信息泄露,也不会被逆推出原信息;即使密码在一个系统中泄露了,也不至于威胁到其他系统的使用。但这种处理不能防止弱密码被彩虹表攻击所破解。
先加盐值再做哈希是应对弱密码的常用方法
盐值可以替弱密码建立一道防御屏障,在一定程度上可以防御已有的彩虹表攻击。但它并不能阻止加密结果被监听、窃取后,攻击者直接发送加密结果给服务端进行冒认。
将盐值变为动态值能有效防止冒认
如果每次向服务端传输时,密码都掺入了动态的盐值,让每次加密的结果都不一样,那么即使传输给服务端的加密结果被窃取了,攻击者也不能冒用来进行另一次调用。不过,尽管在双方通讯均可能泄露的前提下,协商出只有通讯双方才知道的保密信息是完全可行的(后面两讲介绍“传输安全层”时会提到),但这样协商出盐值的过程将变得极为复杂,而且每次协商只能保护一次操作,因而也很难阻止攻击者对其他服务的重放攻击。
加入动态令牌防止重放攻击
我们可以给服务加入动态令牌,这样在网关或其他流量公共位置建立校验逻辑,服务端愿意付出在集群中分发令牌信息等代价的前提下,就可以做到防止重放攻击。但这种手段的弱点是,依然不能抵御传输过程中被嗅探而泄露信息的问题。
启用HTTPS来应对因嗅探而导致的信息泄露问题
启用HTTPS可以防御链路上的恶意嗅探也能在通讯层面解决重放攻击的问题。但是它依然有因客户端被攻破而产生伪造根证书的风险、因服务端被攻破产生证书泄露被中间人冒认的风险、因CRL更新不及时或者OCSP Soft-fail产生吊销证书被冒用的风险以及因TLS的版本过低或密码学套件选用不当产生加密强度不足的风险。
进一步提升保密强度的不同手段
为了抵御前面提到的这种种风险我们还要进一步提升保密强度。比如说银行会使用独立于客户端的存储证书的物理设备俗称的U盾来避免根证书被客户端中的恶意程序窃取伪造当大型网站涉及到账号、金钱等操作时会使用双重验证开辟出一条独立于网络的信息通道如手机验证码、电子邮件来显著提高冒认的难度甚至一些关键企业如国家电网或机构如军事机构会专门建设遍布全国各地的、与公网物理隔离的专用内部网络来保障通讯安全。
现在,通过了解以上这些逐步升级的保密措施,你应该能对“更高的安全强度同时也意味着要付出更多的代价”,有更加具体的理解了,并不是任何一个网站、系统、服务都需要无限拔高的安全性。
也许这个时候,你还会好奇另一个问题:安全的强度有尽头吗?存不存在某种绝对安全的保密方式?
答案可能会出乎你的意料确实是有的。信息论之父香农就严格证明了一次性密码One Time Password的绝对安全性。
但是使用一次性密码必须有个前提,就是我们已经提前安全地把密码或密码列表传达给了对方。比如说,你给朋友人肉送去一本存储了完全随机密码的密码本,然后每次使用其中一条密码来进行加密通讯,用完一条丢弃一条。这样理论上可以做到绝对的安全,但显然这种绝对安全对于互联网来说没有任何的可行性。
所以下面,我们就来看一下在互联网中,信息在客户端的加密是否有必要和有价值。
客户端加密的意义
其实客户端在用户登录、注册一类场景里是否需要对密码进行加密这个问题一直存有争议。而我的观点很明确为了保证信息不被黑客窃取而去做客户端加密其实没有太大意义对绝大多数的信息系统来说启用HTTPS可以说是唯一的实际可行的方案。但是为了保证密码不在服务端被滥用而在客户端就开始加密的做法还是很有意义的。
现在,大网站被拖库的事情层出不穷,密码明文被写入数据库、被输出到日志中之类的事情也屡见不鲜。所以在做系统设计的时候,我们就应该把明文密码这种东西当成是最烫手的山芋来看待,越早消灭掉越好。毕竟把一个潜在的炸弹从客户端运到服务端,对绝大多数系统来说都没有必要。
那我为什么会说,客户端加密对防御泄密没有意义呢?原因是网络通讯并不是由发送方和接收方点对点进行的,客户端无法决定用户送出的信息能不能到达服务端,或者会经过怎样的路径到达服务端,所以在传输链路必定是不安全的前提下,无论客户端做什么防御措施,最终都会沦为“马其诺防线”。
此外前面我还多次提到过中间人攻击即攻击者它是指通过劫持掉客户端到服务端之间的某个节点包括但不限于代理通过HTTP代理返回赝品、路由器通过路由导向赝品、DNS服务直接将机器的DNS查询结果替换为赝品地址来给你访问的页面或服务注入恶意的代码。极端情况下甚至可能把你要访问的服务或页面整个给取代掉此时不管你在页面上设计了多么精巧严密的加密措施也都不会有保护作用。而攻击者只需劫持路由器或者是在局域网内的其他机器上释放ARP病毒便有可能做到这一点。
额外知识中间人攻击Man-in-the-Middle AttackMitM-
在消息发出方和接收方之间拦截双方通讯。我们用写信来做个类比:你给朋友写了一封信,而邮递员可以拆开看你寄出去的信,甚至把信的内容改掉,然后重新封起来,再寄出去给你的朋友。朋友收到信之后给你回信,邮递员又可以拆开看,看完随便改,改完封好再送到你手上。你全程都不知道自己寄出去的信和收到的信都经过邮递员这个“中间人”转手和处理。换句话说,对于你和你朋友来讲,邮递员这个“中间人”角色是不可见的。
当然了,对于“不应把明文传递到服务端”的这个观点,很多人也会有一些不同的意见。比如其中一种保存明文密码的理由是为了便于客户端做动态加盐,因为这样需要服务端先存储明文,或者是存储某种盐值/密钥固定的加密结果,才能每次用新的盐值重新加密,然后与客户端传上来的加密结果进行比对。
而对此我的看法是这种每次从服务端请求动态盐值在客户端加盐传输的做法通常都得不偿失因为客户端无论是否动态加盐都不可能代替HTTPS。真正防御性的密码加密存储确实应该在服务端中进行但这是为了防御服务端被攻破而批量泄露密码的风险并不是为了增强传输过程的安全性。
那么,在服务端是如何处理信息的保密问题的呢?
密码的存储和验证
接下来我就以Fenixs Bookstore中的真实代码为例给你介绍一下针对一个普通安全强度的信息系统密码要如何从客户端传输到服务端然后存储进数据库。
这里的“普通安全强度”的意思是在具有一定保密安全性的同时避免消耗过多的运算资源这样验证起来也相对便捷。毕竟对多数信息系统来说只要配合一定的密码规则约束比如密码要求长度、特殊字符等等再配合HTTPS传输就已经足够防御大多数风险了。即使是用户采用了弱密码、客户端通讯被监听、服务端被拖库、泄露了存储的密文和盐值等问题同时发生也能够最大限度地避免用户明文密码被逆推出来。
下面我们就先来看看在Fenixs Bookstore中密码是如何创建出来的。
首先用户在客户端注册输入明文密码123456。
password = 123456
然后客户端对用户密码进行简单哈希摘要我们可选的算法有MD2/4/5、SHA1/256/512、BCrypt、PBKDF1/2等等。这里为了突出“简单”的哈希摘要我故意没有排除掉MD系这些已经有了高效碰撞手段的算法。
client_hash = MD5(password) // e10adc3949ba59abbe56e057f20f883e
接着,为了防御彩虹表攻击,我们应进行加盐处理,客户端加盐只需要取固定的字符串即可,如果实在不安心,可以使用伪动态的盐值(“伪动态”是指服务端不需要额外通讯就可以得到的信息,比如由日期或用户名等自然变化的内容,加上固定字符串构成)。
client_hash = MD5(MD5(password) + salt) // SALT = $2a$10$o5L.dWYEjZjaejOmN3x4Qu
现在我们假设攻击者截获了客户端发出的信息得到了摘要结果和采用的盐值那攻击者就可以枚举遍历所有8位字符以内“8位”只是举个例子反正就是指弱密码你如果拿1024位随机字符当密码用加不加盐彩虹表都跟你没什么关系的弱密码然后对每个密码再加盐计算就得到了一个针对固定盐值的对照彩虹表。
所以为了应对这种暴力破解,我并不提倡在盐值上做动态化,更理想的方式是引入慢哈希函数来解决。
慢哈希函数是指这个函数的执行时间是可以调节的哈希函数它通常是以控制调用次数来实现的。BCrypt算法就是一种典型的慢哈希函数它在做哈希计算时接受盐值Salt和执行成本Cost两个参数代码层面Cost一般是混入在Salt中比如上面例子中的Salt就是混入了10轮运算的盐值10轮的意思是2的10次方哈希Cost参数是放在指数上的最大取值就31
那么如果我们控制BCrypt的执行时间大概是0.1秒完成一次哈希计算的话按照1秒生成10个哈希值的速度要算完所有的10位大小写字母和数字组成的弱密码就大概需要P(62,10)/(360024365)/0.1=1,237,204,169年的时间。
client_hash = BCrypt(MD5(password) + salt) // MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2
接下来我们要做的就只是防御服务端被拖库后针对固定盐值的批量彩虹表攻击。具体做法是为每一个密码指客户端传来的哈希值产生一个随机的盐值。我建议采用“密码学安全伪随机数生成器”Cryptographically Secure Pseudo-Random Number GeneratorCSPRNG来生成一个长度与哈希值相等的随机字符串。
对于Java语言来说从Java SE 7开始就提供了java.security.SecureRandom类用于支持CSPRNG字符串生成。
SecureRandom random = new SecureRandom();
byte server_salt[] = new byte[36];
random.nextBytes(server_salt); // tq2pdxrblkbgp8vt8kbdpmzdh1w8bex
好,我们继续进行这个密码的创建过程。我们把动态盐值混入客户端传来的哈希值,再做一次哈希,产生出最终的密文,并和上一步随机生成的盐值一起写入到同一条数据库记录中(由于慢哈希算法会占用大量的处理器资源,所以我并不推荐在服务端中采用)。
不过如果你在学习课程后面的实战模块时阅读了Fenixs Bookstore的源码就会发现这步依然采用了Spring Security 5中的BcryptPasswordEncoder。但是请注意它默认构造函数中的Cost参数值为-1经转换后实际只进行了2的10次方=1024次计算所以不会对服务端造成额外的压力。
另外你还可以看到代码中并没有显式地传入CSPRNG生成的盐值这是因为BCryptPasswordEncoder本身就会自动调用CSPRNG产生盐值并将该盐值输出在结果的前32位之中所以也不需要专门在数据库中设计存储盐值字段。
这个过程我们用伪代码来表示一下:
server_hash = SHA256(client_hash + server_salt); // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
DB.save(server_hash, server_salt);
到这里,你会发现这个加密存储的过程其实相对比较复杂,但是运算压力最大的过程(慢哈希)是在客户端完成的,对服务端的压力很小,也不用怕因网络通讯被截获而导致明文密码泄露的问题。
OK等密码存储完之后后面验证的过程就跟加密的操作是类似的我们简单了解下这个步骤就可以了
首先在客户端用户在登录页面中输入密码明文123456经过与注册相同的加密过程向服务端传输加密后的结果。
authentication_hash = MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2
然后,在服务端,接收到客户端传输上来的哈希值,从数据库中取出登录用户对应的密文和盐值,采用相同的哈希算法,针对客户端传来的哈希值、服务端存储的盐值计算摘要结果。
result = SHA256(authentication_hash + server_salt); // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
最后,比较上一步的结果和数据库储存的哈希值是否相同,如果相同就说明密码正确,反之密码错误。
authentication = compare(result, server_hash) // yes
小结
这节课我们其实讨论了两个观点:
第一个观点是,安全并不是一个非此即彼的二元选项,它是连续值,而不是安全与不安全的问题。
第二个观点是,你要明确在信息系统里,客户端加密、服务端解密两项操作的意义是什么。
另外,针对“如何取得相对安全与良好性能之间平衡”这个问题,也是你在进行架构设计时必须权衡取舍的。
一课一思
这节课,我们讨论到了客户端对敏感信息加密后,传输是否有意义的话题。请说说你对这个问题的看法吧。
欢迎在留言区分享你的见解。如果你觉得有收获,也欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 _ 传输(上):传输安全的基础,摘要、加密与签名
你好,我是周志明。今天我们接着上节课的话题,一起来探讨下信息在传输时的保密问题。
其实在前面几讲中,我已经为传输安全层挖下了不少坑,比如说:
基于信道的认证是怎样实现的?
为什么说HTTPS是绝大部分信息系统防御通讯被窃听和篡改的唯一可行手段
传输安全层难道不也是一种自动化的加密吗?
为什么说客户端如何加密都不能代替HTTPS呢
所以接下来,我会花两讲的时间,通过“假设链路上的安全得不到保障,攻击者要如何摧毁之前在认证、授权、凭证、保密中所提到的种种安全机制”这个场景,来给你讲解传输层安全所要解决的问题,同时这也能给你解答前面提到的这些问题。
另外,在上节课的开篇里我也提到过,安全架构中的传输环节是最复杂、最有效,但又是最早就有了标准解决方案的,它包含了许多今天在开发中经常听说,但却不为多数开发人员所知的细节,比如传输安全中的摘要、加密与签名,以及数字证书与传输安全层,等等。那么今天这一讲,我们就先来理清系统安全中,摘要、加密与签名这三种行为的异同之处。
哈希摘要的特点和作用
现在让我们先从JWT令牌的一小段“题外话”来引出这一讲要讨论的话题吧。
你应该已经知道JWT令牌携带信息的可信度源于它是被签名过的信息所以是不可篡改的是令牌签发者真实意图的体现。然而你是否了解过签名具体做了什么呢为什么有签名就能够让负载中的信息变得不可篡改和不可抵赖呢
要解释数字签名Digital Signature的话就必须先从密码学算法的另外两种基础应用“摘要”和“加密”说起。
摘要也被叫做是数字摘要Digital Digest或数字指纹Digital Fingerprint。在JWT令牌中默认的签名信息就是通过令牌头中指定的哈希算法HMAC SHA256针对令牌头、负载和密钥所计算出来的摘要值。
我们来看一个例子:
signature = SHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)
理想的哈希算法都具备这样两个特性:
一是易变性。这是指算法的输入端发生了任何一点细微变动都会引发雪崩效应Avalanche Effect导致输出端的结果产生极大的变化。
这个特性经常被用来做校验,以此保护信息在传输的过程中不会被篡改。比如在互联网下载大文件,通常都会附有一个哈希校验码,用来确保下载下来的文件没有因网络或其他原因,与原文件产生任何偏差。
二是不可逆性。要知道,摘要的过程是单向的,我们不可能从摘要的结果中,逆向还原出输入值来。
其实这点只要你具备初中数学知识就能想明白世间的信息有无穷多种而不管摘要结果的位数是32、128还是512 Bit即使它再大也总归是个有限的数字所以输入数据与输出的摘要结果必然不是一一对应的关系。
可以想想看如果我把一部电影进行了摘要形成256 Bit的哈希值应该不会有人指望能从这个哈希值中还原出一部电影的。
实际上现在我们偶尔还能听到MD5、SHA1或其他哈希算法被破解了的新闻这里的“破解”并不是“解密”的意思而是指找到了该算法的高效率碰撞方法它能够在合理的时间内生成一个摘要结果为指定内容的输入比特流但它并不能代表这个碰撞产生的比特流就会是原来的输入源。
所以通过这两个特性,我们能发现,摘要的意义就是在源信息不泄露的前提下辨别其真伪。易变性保证了,从公开的特征上就可以甄别出摘要结果是否来自于源信息;而不可逆性保证了,从公开的特征并不会暴露出源信息。这跟我们今天用来做身份识别的指纹、面容和虹膜的生物特征是具有高度可比性的。
摘要与加密和签名的本质区别
另外在一些场合中摘要也会被借用来做加密如上节课“保密”中介绍的慢哈希Bcrypt算法和签名如第26讲中提到JWT签名中的HMAC SHA256算法。但从严格意义上看摘要与这两者是有本质的区别的。
加密与摘要的本质区别就在于,摘要是不可逆的,而加密是可逆的,逆过程就是解密。
在经典密码学时代,加密的安全主要是依靠机密性来保证的,也就是依靠保护加密算法或算法的执行参数不被泄露,来保障信息的安全。
而现代密码学并不依靠机密性,加解密算法都是完全公开的,信息的安全是建立在特定问题的计算复杂度之上。具体来说,就是算法根据输入端计算输出结果,这里耗费的算力资源很小;但根据输出端的结果反过来推算原本的输入,耗费的算力就极其庞大。
一个经常被用来说明计算复杂度的例子就是大数的质因数分解我们可以轻而易举地以O(nlogn)的复杂度)计算出两个大素数的乘积:
97667323933 * 128764321253 = 12576066674829627448049
我们知道根据算术基本定理质因数的分解形式是唯一的而且示例前面的计算条件中给出的运算因子已经是质数了所以12,576,066,674,829,627,448,049的分解形式就只有一种即上面给出的唯一答案。
然而如何对大数进行质因数分解其实到今天都还没有找到多项式时间的算法甚至我们都无法确切地知道这个问题属于哪个复杂度类Complexity Class
所以说,尽管这个加密过程理论上一定是可逆的,但实际上的算力差异决定了其逆过程根本无法实现。
24位十进制数的因数分解完全在现代计算机的暴力处理能力范围内这里只是举例。但目前很多计算机科学家都相信大数分解问题就是一种P!=NP的证例尽管也并没有人能证明它一定不存在多项式时间的解法。除了质因数分解外离散对数和椭圆曲线也是具备实用性的复杂问题。
那既然我们提到了密码学,下面我们就来了解下密码学中最重要的应用,信息加密算法,一起来学习、理解下加密是如何保护信息不被泄露的。
加密算法的两大类型:对称与非对称
在现代密码学算法中,根据加密与解密是否采用了同一个密钥,将算法分为了对称加密和非对称加密两大类型。这两类算法各有明确的优劣势与应用场景。
首先我们来看看对称加密算法。
对称加密的缺点显而易见:因为加密和解密都使用相同的密钥,那么当通讯的成员数量增加时,为了保证两两通讯都能采用独立的密钥,密钥数量就要与成员数量的平方成正比,这必然就会面临密钥管理的难题。
而更尴尬的难题是,当通讯双方原本就不存在安全的信道时,如何才能把一个只能让通讯双方才能知道的密钥传输给对方?而如果有通道可以安全地传输密钥,那为何不使用现有的通道传输信息呢?这个“蛋鸡悖论”曾经在很长的时间里,严重阻碍了密码学在真实世界中的推广应用。
因此在1970年代中后期出现的非对称加密算法就从根本上解决了密钥分发的难题。
非对称加密算法把密钥分成了公钥和私钥,公钥可以完全公开,无需安全传输的保证。私钥由用户自行保管,不参与任何通讯传输。这两个密钥谁加密、谁解密,就构成了两种不同的用途:
公钥加密,私钥解密,这种就是加密,用于向私钥所有者发送信息,这个信息可能被他人篡改,但是无法被他人得知。举个例子,如果甲想给乙发一个安全保密的数据,那么应该甲乙各自有一个私钥,甲先用乙的公钥加密这段数据,再用自己的私钥加密这段加密后的数据,最后再发给乙。这样确保了内容既不会被读取,也不能被篡改。
私钥加密,公钥解密,这种就是签名,用于让所有公钥所有者验证私钥所有者的身份,并能用来防止私钥所有者发布的内容被篡改。但是它不用来保证内容不被他人获得。
这两种用途理论上肯定是成立的,但在现实中一般却不成立,因为单靠非对称加密算法,既做不了加密也做不了签名。原因是,不管加密还是解密,非对称加密算法的计算复杂度都相当高,性能比对称加密要差上好几个数量级(不是好几倍)。
要知道,加解密的性能不仅影响运行速度,还导致了现行的非对称加密算法都没有支持分组加密模式。分组的意思就是,由于明文长度与密钥长度在安全上具有相关性,通俗地说就是多长的密钥决定了它能加密多长的明文,如果明文太短就需要进行填充,太长就需要进行分组。
这也就是说,因为非对称加密本身的效率所限,难以支持分组,所以主流的非对称加密算法都只能加密不超过密钥长度的数据,这就决定了非对称加密不能直接用于大量数据的加密。
所以在加密方面,现在一般会结合对称与非对称加密的优点,通过混合加密来保护信道传输的安全。具体是怎么做的呢?
通常我们的做法是,用非对称加密来安全地传递少量数据给通讯的另一方,然后再以这些数据为密钥,采用对称加密来安全高效地大量加密传输数据。这种由多种加密算法组合的应用形式,就被称为“密码学套件”,非对称加密在这个场景中发挥的作用被称为“密钥协商”。
然后,在签名方面,现在一般会结合摘要与非对称加密的优点,通过对摘要结果做加密的形式来保证签名的适用性。由于对任何长度的输入源做摘要之后,都能得到固定长度的结果,所以只要对摘要的结果进行签名,就相当于对整个输入源进行了背书,这样就能保证一旦内容遭到了篡改,摘要结果就会变化,签名也就马上失效了。
这里,我也汇总了前面提到的这三种与密码学相关的应用,你可以参考下表格,去深入理解它们的主要特征、用途和局限性:
那么现在让我们再回到开篇中提到的关于JWT令牌的几个问题上来有了哈希摘要、对称和非对称加密JWT令牌的签名就能保证负载中的信息不可篡改、不可抵赖吗
其实还是不行的,在这个场景里,数字签名的安全性仍然存在一个致命的漏洞:公钥虽然是公开的,但在网络世界里,“公开”具体是一种什么操作?如何保证每一个获取公钥的服务,拿到的公钥就是授权服务器所希望它拿到的?
在网络传输是不可信任的前提下,公钥在网络传输的过程中可能已经被篡改了,所以如果获取公钥的网络请求被攻击者截获并篡改,返回了攻击者自己的公钥,那以后攻击者就可以用自己的私钥来签名,让资源服务器无条件信任它的所有行为了。
也就是说,如果是在现实世界中公开公钥,我们可以通过打电话、发邮件、发短信、登报纸、同时发布在多个网站上等多种网络通讯之外的途径来达成。但在程序与网络的世界中,就必须找到一种可信任的公开方法,而且这种方法不能依赖加密来实现,否则又会陷入到蛋鸡的问题之中。
小结
今天,我们从哈希摘要、对称加密和非对称加密这三种安全架构中常见的保密操作说起,一起学习了摘要、加密、签名这三种现代密码学算法基础应用的主要用途和区别差异。
首先我们要明确的是哈希是不可逆的它不能解密并不是加密算法只是一些场景把它当作加密算法使用。哈希的特点是易变性输入发生1Bit变动就可能导致输出结果50%的内容发生改变无论输入长度多少都有长度固定的输出2的N次幂。所以这些特点决定了哈希的主要应用是做摘要用来保证原文未被修改。
而加密是现代密码学算法的关键应用,对称加密的设计难度比较小,执行速度快,加密明文长度不受限制,这些特点就决定了对于大量数据的加密传输,目前都是靠对称加密来完成的。但是对称加密难以解决如何把密钥传递给对方的问题,因而出现了非对称加密,它的特点是加密和解密使用的是不同的密钥,但是性能和加密明文的长度都受限。
一课一思
请你思考一下本节课最后提出的这个问题,即数字签名需要分发公钥,但在网络世界里,“公开”具体是一种什么操作呢?如何保证每一个获取公钥的服务,拿到的公钥就是授权服务器所希望它拿到的?欢迎给我留言。这个问题的答案呢,我们也会在下一节课中进行探讨。
好,感谢你的阅读,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。就到这里,我们下一讲再见。

View File

@@ -0,0 +1,197 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
29 _ 传输(下):数字证书与传输安全层
你好,我是周志明。
上节课,我们花了很多时间来学习传输安全层中的摘要、加密和签名的主要用途和差别,在最后,我给你留了一个问题:数字签名需要分发公钥,但在网络世界里,“公开”具体是一种什么操作?如何保证每一个获取公钥的服务,拿到的公钥就是授权服务器所希望它拿到的呢?在网络中一切皆不可信任的假设前提下,任何传输都有可能被篡改,那这个问题能够解决吗?
答案其实是可以的,这就是数字证书要解决的问题。
所以接下来,我们就先从数字证书如何达成共同信任开始说起,一起来了解下在传输安全的过程中,数字证书与传输安全层的相关实现细节。
如何通过数字证书达成共同信任?
有了哈希摘要、对称和非对称加密之后,签名还是无法保证负载中的信息不可篡改、不可抵赖。所以,当我们无法以“签名”的手段来达成信任时,就只能求助于其他途径。
现在,你不妨想想真实的世界中,我们是如何达成信任的。其实不外乎以下这两种:
基于共同私密信息的信任-
比如某个陌生号码找你,说是你的老同学,生病了要找你借钱。你能够信任他的方式是向对方询问一些你们两个应该知道,而且只有你们两个知道的私密信息,如果对方能够回答上来,他有可能真的是你的老同学,否则他十有八九就是个诈骗犯。
基于权威公证人的信任-
如果有个陌生人找你,说他是警察,让你把存款转到他们的安全账号上。你能够信任他的方式是去一趟公安局,如果公安局担保他确实是个警察,那他有可能真的是警察,否则他也十有八九就是个诈骗犯。
那回到网络世界中我们其实并不能假设授权服务器和资源服务器是互相认识的所以通常不太会采用第一种方式。而第二种就是目前保证公钥可信分发的标准这个标准有一个名字公开密钥基础设施Public Key InfrastructurePKI
额外知识公开密钥基础设施Public Key InfrastructurePKI-
又称公开密钥基础架构、公钥基础建设、公钥基础设施、公开密码匙基础建设或公钥基础架构,是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。-
密码学上公开密钥基础建设借着数字证书认证中心Certificate AuthorityCA将用户的个人身份跟公开密钥链接在一起。对每个证书中心用户的身份必须是唯一的。链接关系通过注册和发布过程创建取决于担保级别链接关系可能由CA的各种软件或在人为监督下完成。PKI的确定链接关系的这一角色称为注册管理中心Registration AuthorityRA。RA确保公开密钥和个人身份链接可以防抵赖。
咱们不必纠缠于PKI概念上的内容只要知道里面定义的“数字证书认证中心”就相当于前面例子中“权威公证人”的角色它是负责发放和管理数字证书的权威机构。
当然任何人包括你我也都可以签发证书只是不权威罢了而CA作为受信任的第三方就承担了公钥体系中公钥的合法性检验的责任。
可是这里和现实世界仍然有一些区别。在现实世界里你去找的公安局大楼不太可能是剧场布景冒认的而网络世界里在假设所有网络传输都有可能被截获冒认的前提下“去CA中心进行认证”本身也是一种网络操作。那你就要问了这跟之前的“去获取公钥”的操作在本质上不是没什么差别吗
其实还是有差别的世界上的公钥成千上万、不可枚举而权威的CA中心则应该是可数的。“可数的”就意味着可以不通过网络而是在浏览器与操作系统出厂时就预置好或者是提前就安装好如银行的证书。比如说下图就是我机器上现存的根证书
那到这里其实就出现了我们这节课要探讨的主角之一证书Certificate
证书是权威CA中心对特定公钥信息的一种公证载体你也可以理解为是权威CA对特定公钥未被篡改的签名背书。由于客户的机器上已经预置了这些权威CA中心本身的证书可以叫做CA证书或者根证书这样就让我们在不依靠网络的前提下使用根证书里面的公钥信息对其所签发的证书中的签名进行确认。
所以到这里,我们就终于打破了鸡生蛋、蛋生鸡的循环,使得整套数字签名体系有了坚实的逻辑基础。
PKI中采用的证书格式是X.509标准格式,它定义了证书中应该包含哪些信息,并描述了这些信息是如何编码的。其中最关键的,就是认证机构的数字签名和公钥信息两项内容。
那么下面我们就通过一个标准X.509格式的CA证书的例子来看看一个数字证书具体都包含了哪些内容。
第一是版本号Version它会指出该证书使用了哪种版本的X.509标准版本1、版本2或是版本3。版本号会影响证书中的一些特定信息在这个例子当中目前的版本为3。
Version: 3 (0x2)
第二是序列号Serial Number这是由证书颁发者分配的本证书的唯一标识符。
Serial Number: 04:00:00:00:00:01:15:4b:5a:c3:94
第三是签名算法标识符Signature Algorithm ID它是签证书的算法标识由对象标识符加上相关的参数组成用于说明本证书所用的数字签名算法。比如SHA1和RSA的对象标识符就用来说明该数字签名是利用RSA对SHA1的摘要结果进行加密。
Signature Algorithm: sha1WithRSAEncryption
第四是认证机构的数字签名Certificate Signature这是使用证书发布者私钥生成的签名以确保这个证书在发放之后没有被篡改过。
第五是认证机构Issuer Name 即证书颁发者的可识别名。
Issuer: C=BE, O=GlobalSign nv-sa, CN=GlobalSign Organization Validation CA - SHA256 - G2
第六是有效期限Validity Period 即证书起始日期和时间以及终止日期和时间,意为指明证书在这两个时间内有效。
Validity
Not Before: Nov 21 08:00:00 2020 GMT
Not After : Nov 22 07:59:59 2021 GMT
第七是主题信息Subject证书持有人唯一的标识符Distinguished Name这个名字在整个互联网上应该是唯一的通常使用的是网站的域名。
Subject: C=CN, ST=GuangDong, L=Zhuhai, O=Awosome-Fenix, CN=*.icyfenix.cn
第八是公钥信息Public-Key 它包括了证书持有人的公钥、算法(指明密钥属于哪种密码系统)的标识符和其他相关的密钥参数。
那么到此为止数字签名的安全性其实已经可以完全自洽了但相信你大概也已经感受到了这条信任链的复杂与繁琐如果从确定加密算法到生成密钥、公钥分发、CA认证、核验公钥、签名、验证每一个步骤都要由最终用户来完成的话这种意义的“安全”估计只能一直是存于实验室中的阳春白雪。
所以,如何把这套繁琐的技术体系,自动化地应用于无处不在的网络通讯之中,就是接下来我们要讨论的主题了。
传输安全层是如何隐藏繁琐的安全过程的?
在计算机科学里,隔离复杂性的最有效手段(没有之一)就是分层,如果一层不够就再加一层,这点在网络中更是体现得淋漓尽致。
OSI模型、TCP/IP模型从物理特性比特流开始将网络逐层封装隔离到了HTTP协议这种面向应用的协议里使用者就已经不会去关心网卡/交换机是如何处理数据帧、MAC地址的了也不会去关心ARP如何做地址转换不会去关心IP寻址、TCP传输控制等细节。
那么想要在网络世界中让用户无感知地实现安全通讯最合理的做法就是在传输层之上、应用层之下加入专门的安全层来实现。这样对上层原本是基于HTTP的Web应用来说甚至都察觉不到有什么影响。
而且构建传输安全层这个想法几乎可以说是和万维网的历史一样长早在1994年就已经有公司开始着手去实践了
1994年网景Netscape公司开发了SSL协议Secure Sockets Layer的1.0版这是构建传输安全层的起源但是SSL 1.0从未正式对外发布过。
1995年Netscape把SSL升级到2.0版,正式对外发布,但是刚刚发布不久,就被发现有严重漏洞,所以并未大规模使用。
1996年修补好漏洞的SSL 3.0对外发布这个版本得到了广泛的应用很快成为Web网络安全层的事实标准。
1999年互联网标准化组织接替网景公司将SSL改名为TLSTransport Layer Security随即就形成了传输安全层的国际标准。第一个正式的版本是RFC 2246定义的TLS 1.0该版TLS的生命周期极长直到2020年3月主流浏览器Chrome、Firefox、IE、Safari才刚刚宣布同时停止TLS 1.0/1.1的支持。而讽刺的是由于停止后许多政府网站被无法被浏览此时又正值新冠病毒的爆发期Firefox紧急发布公告宣布撤回该改动因此目前TLS 1.0的生命还在顽强延续。
2006年TLS的第一个升级版1.1发布RFC 4346但它除了增加对CBC攻击的保护外几乎没有任何改变沦为了被遗忘的孩子当时也很少有人会使用TLS 1.1甚至TLS 1.1根本都没有被提出过有啥已知的协议漏洞。
2008年TLS 1.1发布2年之后TLS 1.2标准发布RFC 5246迄今超过90%的互联网HTTPS流量都是由TLS 1.2所支持的,现在我们仍在使用的浏览器几乎都完美支持了该协议。
2018年最新的TLS 1.3RFC 8446发布比起前面版本相对温和的升级TLS 1.3做出了一些激烈的改动修改了从1.0起一直没有大变化的两轮四次2-RTT握手首次连接仅需一轮1-RTT握手即可完成在有连接复用支持的时候甚至可以把TLS 1.2原本的1-RTT下降到0-RTT显著提升了访问速度。
那么接下来我就以现在被广泛使用的TLS 1.2为例,给你介绍一下传输安全层是如何保障所有信息都是第三方无法窃听(加密传输)、无法篡改(一旦篡改通讯算法会立刻发现)、无法冒充(证书验证身份)的。
TLS 1.2在传输之前的握手过程中,一共需要进行上下两轮、共计四次的通讯。我们来看一下这个握手过程的时序图:
下面,我们就一一来详细解读一下这个过程。
第一步客户端请求Client Hello
客户端向服务器请求进行加密通讯,在这个请求里面,它会以明文的形式,向服务端提供以下信息:
支持的协议版本比如TLS 1.2。但是你要注意1.0至3.0分别代表了SSL1.0至3.0而TLS1.0则是3.1一直到TLS1.3的3.4。
一个客户端生成的32 Bytes随机数。这个随机数将稍后用于产生加密的密钥。
一个可选的SessionID。注意你不要和前面的Cookie-Session机制混淆了这个SessionID是指传输安全层的Session它是为了TLS的连接复用而设计的。
一系列支持的密码学算法套件。比如TLS_RSA_WITH_AES_128_GCM_SHA256代表着密钥交换算法是RSA加密算法是AES128-GCM消息认证码算法是SHA256。
一系列支持的数据压缩算法。
其他可扩展的信息。为了保证协议的稳定后续对协议的功能扩展大多都是添加到这个变长结构中。比如TLS 1.0中由于发送的数据并不包含服务器的域名地址导致了一台服务器只能安装一张数字证书这对虚拟主机来说就很不方便所以从TLS 1.1起就增加了名为“Server Name”的扩展信息以便一台服务器给不同的站点安装不同的证书。
第二步服务器回应Server Hello
服务器接收到客户端的通讯请求后,如果客户端声明支持的协议版本和加密算法组合,与服务端相匹配的话,就向客户端发出回应。如果不匹配,将会返回一个握手失败的警告提示。这次回应同样是以明文发送的,主要包括以下信息:
服务端确认使用的TLS协议版本。
第二个32 Bytes的随机数稍后用于产生加密的密钥。
一个SessionID以后可通过连接复用减少一轮握手。
服务端在列表中选定的密码学算法套件。
服务端在列表中选定的数据压缩方法。
其他可扩展的信息。
如果协商出的加密算法组合是依赖证书认证的服务端还要发送出自己的X.509证书,而证书中的公钥是什么,也必须根据协商的加密算法组合来决定。
密钥协商消息这部分内容对于不同的密码学套件有着不同的价值。比如对于ECDH + anon这样的密钥协商算法组合来说基于椭圆曲线的ECDH算法可以在双方通讯都公开的情况下协商出一组只有通讯双方知道的密钥就不需要依赖证书中的公钥而是通过Server Key Exchange消息协商出密钥。
第三步客户端确认Client Handshake Finished
由于密码学套件的组合复杂多样这里我就只用RSA算法作为密钥交换算法来给你举个例子介绍下客户端确认的后续过程。
首先,客户端在收到服务器应答后,要先验证服务器的证书合法性。然后,如果证书不是可信机构颁布的,或者是证书中的信息存在问题,比如域名与实际域名不一致、或证书已经过期、或通过在线证书状态协议得知证书已被吊销,等等,这都会向访问者显示一个“证书不可信任”的警告,由用户自行选择是否还要继续通信。
而如果证书没有问题,客户端就会从证书中取出服务器的公钥,并向服务器发送以下信息:
客户端证书可选。部分服务端并不是面向全公众的而是只对特定的客户端提供服务此时客户端就需要发送它自身的证书来证明身份。如果不发送或者验证不通过服务端可自行决定是否要继续握手或者返回一个握手失败的信息。客户端需要证书的TLS通讯也被称为“双向TLS”Mutual TLS常简写为mTLS这是云原生基础设施的主要认证方法也是基于信道认证的最主流形式。
第三个32 Bytes的随机数这个随机数不再是明文发送而是以服务端传过来的公钥加密的它被称为PreMasterSecret将与前两次发送的随机数一起根据特定算法计算出48 Bytes的MasterSecret这个MasterSecret也就是为后续内容传输时的对称加密算法所采用的私钥。
编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供服务器校验。
第四步服务端确认Server Handshake Finished
服务端向客户端回应最后的确认通知,包括以下信息:
编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供客户端校验。
那么到这里整个TLS握手阶段就宣告完成一个安全的连接就成功建立了。你要知道每一个连接建立的时候客户端和服务端都会通过上面的握手过程协商出许多信息比如一个只有双方才知道的随机产生的密钥、传输过程中要采用的对称加密算法例子中的AES128、压缩算法等此后该连接的通讯将使用此密钥和加密算法进行加密、解密和压缩。
这种处理方式对上层协议的功能上完全透明的在传输性能上会有下降但在功能上完全不会感知到有TLS的存在。建立在这层安全传输层之上的HTTP协议就被称为“HTTP Over SSL/TLS”也即是我们所熟知的HTTPS。
另外从上面握手协商的过程中我们还可以得知HTTPS并非不是只有“启用了HTTPS”和“未启用HTTPS”的差别采用不同的协议版本、不同的密码学套件、证书是否有效、服务端/客户端对面对无效证书时的处理策略如何都会导致不同HTTPS站点的安全强度的不同。因此并不能说只要启用了HTTPS就必定能够安枕无忧。
小结
今天我们通过在网络中如何安全分发公钥这个问题引出了如何通过数字证书达成共同信任、如何通过PKI体系来签发数字证书。在了解了数字证书的工作原理后你还要了解的重点是如何通过传输安全层把繁琐的安全过程隐藏起来让开发者不需要时刻注意到那些麻烦而又琐碎的安全细节。
一课一思
除了TLS你还知道数字证书有什么具体应用吗欢迎给我留言分享你的思考。
如果你觉得有收获,也欢迎你把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,203 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
30 _ 验证:系统如何确保提交给服务的数据是安全的?
你好,我是周志明。今天是安全架构这个小章节的最后一讲,我们来讨论下“验证”这个话题,一起来看看,关于“系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险”这个问题的具体解决方案。
数据验证也很重要
数据验证与程序如何编码是密切相关的,你在做开发的时候可能都不会把它归入安全的范畴之中。但你细想一下,如果说关注“你是谁”(认证)、“你能做什么”(授权)等问题是很合理的安全,那么关注“你做的对不对”(验证)不也同样合理吗?
首先,从数量上来讲,因为数据验证不严谨而导致的安全问题,要比其他安全攻击所导致的问题多得多;其次,从风险上来讲,由于数据质量而导致的安全问题,要承受的风险可能有高有低,可当我们真的遇到了高风险的数据问题,面临的损失不一定就比被黑客拖库来得小。
当然不可否认的是,相比其他富有挑战性的安全措施,比如说,防御与攻击之间精彩的缠斗需要人们综合运用数学、心理、社会工程和计算机等跨学科知识,数据验证这项常规工作确实有点儿无聊。在日常的开发工作当中,它会贯穿于代码的各个层次,我们每个人肯定都写过。
但是,这种常见的代码反而是迫切需要被架构约束的。
这里我们要先明确一个要点:缺失的校验会影响数据质量,而过度的校验也不会让系统更加健壮,反而在某种意义上会制造垃圾代码,甚至还会有副作用。
我们来看看下面这个实际的段子:
前 端: 提交一份用户数据(姓名:某, 性别:男, 爱好:女, 签名:xxx, 手机:xxx, 邮箱:null
控制器: 发现邮箱是空的抛ValidationException("邮箱没填")
前 端: 已修改,重新提交
安 全: 发送验证码时发现手机号少一位抛RemoteInvokeException("无法发送验证码")
前 端: 已修改,重新提交
服务层: 邮箱怎么有重复啊抛BusinessRuntimeException("不允许开小号")
前 端: 已修改,重新提交
持久层: 签名字段超长了插不进去抛SQLException("插入数据库失败SQLxxx")
…… ……
前 端: 你们这些坑管挖不管埋的后端,各种异常都往前抛!
用 户: 这系统牙膏厂生产的?
你应该也知道,最基础的数据问题可以在前端做表单校验来处理,但服务端验证肯定也是要做的。那么在看完了前面这个段子以后,你可以想一想,服务端应该在哪一层去做校验呢?我想你可能会得出这样的答案:
在Controller层做在Service层不做。理由是从Service开始会有同级重用当出现ServiceA.foo(params)调用ServiceB.bar(params)的时候就会对params重复校验两次。
在Service层做在Controller层不做。理由是无业务含义的格式校验已经在前端表单验证处理过了而有业务含义的校验不应该放在Controller中毕竟页面控制器的职责是管理页面流不该承载业务。
在Controller、Service层各做各的。Controller做格式校验Service层做业务校验听起来很合理但这其实就是前面段子中被嘲笑的行为。
还有其他一些意见,比如说在持久层做校验,理由是这是最终入口,把守好写入数据库的质量最重要。
这样的讨论大概是不会有一个统一、正确的结论的但是在Java里确实是有验证的标准做法即Java Bean Validation。这也是我比较提倡的做法那就是把校验行为从分层中剥离出来不是在哪一层做而是在Bean上做。
Java Bean Validation
从2009年JSR 303的1.0到2013年JSR 349更新的1.1到目前最新的2017年发布的JSR 380Java定义了Bean验证的全套规范。这种单独将验证提取、封装的做法可以让我们获得不少好处
对于无业务含义的格式验证,可以做到预置。
对于有业务含义的业务验证可以做到重用。一个Bean作为参数或返回值被用于多个方法的情况是很常见的因此针对Bean做校验就比针对方法做校验更有价值这样利于我们集中管理比如统一认证的异常体系、统一做国际化、统一给客户端的返回格式等等。
避免对输入数据的防御污染到业务代码。如果你的代码里面有很多像是下面这样的条件判断,就应该考虑重构了:
// 一些已执行的逻辑
if (someParam == null) {
throw new RuntimeExcetpion("客官不可以!")
}
利于多个校验器统一执行,统一返回校验结果,避免用户踩地雷、挤牙膏式的试错体验。
据我所知国内的项目使用Bean Validation的并不少见但大多数程序员都只使用到它的Built-In Constraint来做一些与业务逻辑无关的通用校验也就是下面展示的这堆注解看类名我们基本上就能明白它们的含义了
@Null@NotNull@AssertTrue@AssertFalse@Min@Max@DecimalMin@DecimalMax@Negative@NegativeOrZero@Positive@PositiveOrZeor@Szie@Digits@Pass@PassOrPresent@Future@FutureOrPresent@Pattern@NotEmpty@NotBlank@Email
不过我们要知道与业务相关的校验往往才是最复杂的校验。而把简单的校验交给Bean Validation把复杂的校验留给自己这简直是买椟还珠故事的程序员版本。其实以Bean Validation的标准方式来做业务校验是非常优雅的。
接下来我就用Fenixs Bookstore项目在用户资源上的两个方法“创建新用户”和“更新用户信息”来给你举个例子
/**
* 创建新的用户
*/
@POST
public Response createUser(@Valid @UniqueAccount Account user) {
return CommonResponse.op(() -> service.createAccount(user));
}
/**
* 更新用户信息
*/
@PUT
@CacheEvict(key = "#user.username")
public Response updateUser(@Valid @AuthenticatedAccount @NotConflictAccount Account user) {
return CommonResponse.op(() -> service.updateAccount(user));
}
这里你要注意其中的三个自定义校验注解,它们的含义分别是:
@UniqueAccount:传入的用户对象必须是唯一的,不与数据库中任何已有用户的名称、手机、邮箱产生重复。
@AuthenticatedAccount:传入的用户对象必须与当前登录的用户一致。
@NotConflictAccount:传入的用户对象中的信息与其他用户是无冲突的,比如将一个注册用户的邮箱,修改成与另外一个已存在的注册用户一致的值,这便是冲突。
这里的需求我们其实很容易就能想明白:当注册新用户时,要约束其不与任何已有用户的关键信息重复;而当用户修改自己的信息时,只能与自己的信息重复,而且只能修改当前登录用户的信息。
这些约束规则不仅仅是为这两个方法服务它们还可能会在用户资源中的其他入口被使用到乃至在其他分层的代码中被使用到。而在Bean上做校验我们就能一揽子地覆盖前面提到的这些使用场景。
现在我们来看前面代码中用到的三个自定义注解对应校验器的实现类:
public static class AuthenticatedAccountValidator extends AccountValidation<AuthenticatedAccount> {
public void initialize(AuthenticatedAccount constraintAnnotation) {
predicate = c -> {
AuthenticAccount loginUser = (AuthenticAccount) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return c.getId().equals(loginUser.getId());
};
}
}
public static class UniqueAccountValidator extends AccountValidation<UniqueAccount> {
public void initialize(UniqueAccount constraintAnnotation) {
predicate = c -> !repository.existsByUsernameOrEmailOrTelephone(c.getUsername(), c.getEmail(), c.getTelephone());
}
}
public static class NotConflictAccountValidator extends AccountValidation<NotConflictAccount> {
public void initialize(NotConflictAccount constraintAnnotation) {
predicate = c -> {
Collection<Account> collection = repository.findByUsernameOrEmailOrTelephone(c.getUsername(), c.getEmail(), c.getTelephone());
// 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
};
}
}
这样,业务校验就可以和业务逻辑完全分离开,在需要校验时,你可以用@Valid注解自动触发,或者通过代码手动触发执行。你可以根据自己项目的要求,把这些注解应用于控制器、服务层、持久层等任何层次的代码之中。
而且采用Bean Validation也便于我们统一处理校验结果不满足时的提示信息。比如提供默认值、提供国际化支持这里没做、提供统一的客户端返回格式创建一个用于ConstraintViolationException的异常处理器来实现以及批量执行全部校验避免出现开篇那个段子中挤牙膏的尴尬情况。
除此之外对于Bean与Bean校验器我还想给你两条关于编码的建议。
第一条建议是,要对校验项预置好默认的提示信息,这样当校验不通过时,用户能获得明确的修正提示。这里你可以参考下面的代码示例:
/**
* 表示一个用户的信息是无冲突的
*
* “无冲突”是指该用户的敏感信息与其他用户不重合,比如将一个注册用户的邮箱,修改成与另外一个已存在的注册用户一致的值,这便是冲突
**/
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = AccountValidation.NotConflictAccountValidator.class)
public @interface NotConflictAccount {
String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第二条建议是要把不带业务含义的格式校验注解放到Bean的类定义之上把带业务逻辑的校验放到Bean的类定义的外面。
这两者的区别是,放在类定义中的注解能够自动运行,而放到类外面的业务校验需要像前面的示例代码那样,明确标出注解时才会运行。比如用户账号实体中的部分代码为:
public class Account extends BaseEntity {
@NotEmpty(message = "用户不允许为空")
private String username;
@NotEmpty(message = "用户姓名不允许为空")
private String name;
private String avatar;
@Pattern(regexp = "1\\d{10}", message = "手机号格式不正确")
private String telephone;
@Email(message = "邮箱格式不正确")
private String email
}
你可以发现这些校验注解都直接放在了类定义中每次执行校验的时候它们都会被运行。因为Bean Validation是Java的标准规范它执行的频率可能比编写代码的程序所预想的更高比如使用Hibernate来做持久化时便会自动执行Data Object上的校验注解。
对于那些不带业务含义的注解,在运行时是不需要其他外部资源的参与的,它们不会调用远程服务、访问数据库,这种校验重复执行实际上并没有什么成本。
但带业务逻辑的校验,通常就需要外部资源参与执行,这不仅仅是多消耗一点时间和运算资源的问题,因为我们很难保证依赖的每个服务都是幂等的,重复执行校验很可能会带来额外的副作用。因此应该放到外面,让使用者自行判断是否要触发。
另外还有一些“需要触发一部分校验”的非典型情况比如“新增”操作A需要执行全部校验规则“修改”操作B中希望不校验某个字段“删除”操作C中希望改变某一条校验规则这个时候我们就要启用分组校验来处理设计一套“新增”“修改”“删除”这样的标识类置入到校验注解的groups参数中去实现。
小结
这节课算是JSR 380 Bean Validation的小科普Bean验证器在Java中存在的时间已经超过了十年应用范围也非常广泛但现在还是有很多的信息系统选择自己制造轮子去解决数据验证的问题而且做的也并没有Bean验证器好。
所以在这节课里我给你总结了正确使用Bean验证器的一些最佳实践涉及到不少具体的代码建议你好好结合着代码进行学习和实践。在课程后面的实战模块中我还会给你具体展示Fenixs Bookstore的工程代码到时你也可以结合着该模块一起学习印证或增强实战学习的效果。
一课一思
你开发的系统是依靠Bean验证器完成数据验证的吗如果不是那么你的系统、或者是你知道的系统是如何做校验的呢你认为效果如何
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,219 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
31 _ 分布式共识想用好分布式框架先学会Paxos算法吧
你好,我是周志明。从这节课起,我会用两讲带你学习分布式共识算法。
可靠与可用、共识与一致
在正式开始探讨分布式环境面临的各种技术问题和解决方案之前,我们先把目光从工业界转到学术界,学习两三种具有代表性的分布式共识算法,为后续分布式环境中操作共享数据打好理论基础。
我们先从一个最简单、最常见的场景开始:如果你有一份很重要的数据,要确保它长期存储在电脑上不会丢失,你会怎么做?
这不是什么脑筋急转弯的古怪问题,答案就是去买几块磁盘,把数据在不同磁盘上多备份几个副本。
假设一块磁盘每年损坏的概率是5%那把文件复制到另一块磁盘上备份后数据丢失的概率就变成了0.25%两块磁盘同时损坏才会导致数据丢失。以此类推使用三块磁盘存储数据丢失的概率就是0.00125%使用四块则是0.0000625%。换句话说使用四块磁盘来保存同一份数据就已经保证了这份数据在一年内有超过99.9999%的概率是安全可靠的。
那对应到软件系统里,保障系统可靠性的方法,与拿几个磁盘备份并没有什么本质区别。
单个节点的系统宕机导致无法访问数据的原因可能有很多比如程序运行出错、硬件损坏、网络分区、电源故障等等一年中出现系统宕机的概率也许比5%还要大。这就决定了软件系统也必须有多台机器能够拥有一致的数据副本,才有可能对外提供可靠的服务。
但是,在软件系统里,要保障系统的可用性,面临的困难与磁盘备份却又有着本质的区别。
其中的原因也很好理解:磁盘之间是孤立的不需要互相通讯,备份数据是静态的,初始化后状态就不会发生改变,由人工进行的文件复制操作,很容易就能保证数据在各个备份盘中是一致的。但是,到了分布式系统里面,我们就必须考虑动态的数据如何在不可靠的网络通讯条件下,依然能在各个节点之间正确复制的问题。
现在,我们来修改下要讨论的场景:如果你有一份会随时变动的数据,要确保它能正确地存储在网络中几台不同的机器上,你会怎么做?
这时,你最容易想到的答案一定是“数据同步”:每当数据有变化,就把变化情况在各个节点间的复制看成是一种事务性的操作,只有系统里的每一台机器都反馈成功地完成磁盘写入后,数据的变化才能宣布成功。
我们在第13讲学过的2PC、3PC就可以实现这种同步操作。同步的一种真实应用场景是数据库的主从全同步复制Fully Synchronous Replication。比如MySQL Cluster进行全同步复制时所有Slave节点的Binlog都完成写入后Master的事务才会进行提交。
不过这里有一个显而易见的缺陷尽管可以确保Master和Slave中的数据是绝对一致的但任何一个Slave节点、因为任何原因未响应都会阻塞整个事务。也就是说每增加一个Slave节点整个系统的可用性风险都会增加一分。
以同步为代表的数据复制方法叫做状态转移State Transfer。这类方法属于比较符合人类思维的可靠性保障手段但通常要以牺牲可用性为代价。
但是我们在建设分布式系统的时候往往不能承受这样的代价。对于一些关键系统来说在必须保障数据正确可靠的前提下对可用性的要求也非常苛刻。比如系统要保证数据要达到99.999999%可靠性同时也要达到99.999%可用的程度。
这就引出了第三个问题:如果你有一份会随时变动的数据,要确保它正确地存储于网络中的几台不同机器之上,并且要尽可能保证数据是随时可用的,你会怎么做?
系统高可用和高可靠之间的矛盾是由于增加机器数量反而降低了可用性带来的。为缓解这个矛盾在分布式系统里主流的数据复制方法是以操作转移Operation Transfer为基础的。我们想要改变数据的状态除了直接将目标状态赋予它之外还有另一种常用的方法就是通过某种操作把源状态转换为目标状态。
能够使用确定的操作促使状态间产生确定的转移结果的计算模型在计算机科学中被称为状态机State Machine
状态机有一个特性:任何初始状态一样的状态机,如果执行的命令序列一样,那么最终达到的状态也一样。在这里我们可以这么理解这个特性,要让多台机器的最终状态一致,只要确保它们的初始状态和接收到的操作指令都是完全一致的就可以。
无论这个操作指令是新增、修改、删除或者其他任何可能的程序行为,都可以理解为要将一连串的操作日志正确地广播给各个分布式节点。
广播指令与指令执行期间允许系统内部状态存在不一致的情况也就是不要求所有节点的每一条指令都是同时开始、同步完成的只要求在此期间的内部状态不能被外部观察到且当操作指令序列执行完成的时候所有节点的最终的状态是一致的。这种模型就是状态机复制State Machine Replication
在分布式环境下,考虑到网络分区现象是不可能消除的,而且可以不必去追求系统内所有节点在任何情况下的数据状态都一致,所以采用的是“少数服从多数”的原则。
也就是说一旦系统中超过半数的节点完成了状态的转换就可以认为数据的变化已经被正确地存储在了系统当中。这样就可以容忍少数通常是不超过半数的节点失联使得增加机器数量可以用来提升系统整体的可用性。在分布式中这种思想被叫做Quorum机制。
根据这些讨论,我们需要设计出一种算法,能够让分布式系统内部可以暂时容忍存在不同的状态,但最终能够保证大多数节点的状态能够达成一致;同时,能够让分布式系统在外部看来,始终表现出整体一致的结果。
这个让系统各节点不受局部的网络分区、机器崩溃、执行性能或者其他因素影响能最终表现出整体一致的过程就是各个节点的协商共识Consensus
这里需要注意的是共识Consensus与一致性Consistency是有区别的一致性指的是数据不同副本之间的差异而共识是指达成一致性的方法与过程。
由于翻译的关系很多中文资料把Consensus同样翻译为一致性导致网络上大量的“二手中文资料”都混淆了这两个概念。以后你再看到“分布式一致性算法”的时候应该就知道它指的其实是“Distributed Consensus Algorithm”了。
好了我们继续来学习分布式中的共识算法。说到这里我们就不得不提Paxos算法了。
Paxos算法
Paxos算法是由Leslie Lamport就是大名鼎鼎的LaTeX中的“La”提出的一种基于消息传递的协商共识算法。现在Paxos算法已经成了分布式系统最重要的理论基础几乎就是“共识”这两字的代名词了。
这个极高的评价来自提出Raft算法的论文“In Search of an Understandable Consensus Algorithm”更是显得分量十足。
关于Paxos在分布式共识算法中的地位还有这么一种说法
There is only one consensus protocol, and thats “Paxos” — all other approaches are just broken versions of Paxos.-
世界上只有一种共识协议就是Paxos其他所有共识算法都是Paxos的退化版本。-
—— Mike BurrowsInventor of Google Chubby
虽然我认为“世界上只有Paxos一种分布式共识算法”的说法有些夸张但是如果没有Paxos那后续的Raft、ZAB等算法ZooKeeper、etcd这些分布式协调框架Hadoop、Consul这些在此基础上的各类分布式应用都很可能会延后好几年面世。
但Paxos算法从被第一次提出到成为分布式系统最重要的理论基础可谓是经历了一番波折。我们来具体看看。
Paxos算法的诞生之路
为了解释清楚Paxos算法Lamport虚构了一个叫做“Paxos”的希腊城邦这个城邦按照民主制度制定法律却又不存在一个中心化的专职立法机构而是靠着“兼职议会”Part-Time Parliament来完成立法。这就无法保证所有城邦居民都能够及时地了解新的法律提案也无法保证居民会及时为提案投票。
Paxos算法的目标就是让城邦能够在每一位居民都不承诺一定会及时参与的情况下依然可以按照少数服从多数的原则最终达成一致意见。但是Paxos算法并不考虑拜占庭将军问题也就是假设信息可能丢失也可能延迟但不会被错误传递。
Lamport在1990年首次发表了Paxos算法选的论文题目就是“The Part-Time Parliament”。由于这个算法本身非常复杂希腊城邦的比喻使得描述更为晦涩难懂。所以这篇论文的三个审稿人一致要求Lamport删掉希腊城邦的故事。这就让Lamport非常不爽然后干脆就撤稿不发了所以Paxos刚刚被提出的时候并没有引起什么反响。
到了八年之后也就是1998年Lamport把这篇论文重新整理后投到了“ACM Transactions on Computer Systems”。这次论文成功发表Lamport的名气也确实吸引了一些人去研究但是并没有多少人能看懂他到底在说什么。
时间又过去了三年来到了2001年Lamport认为前两次没有引起什么反响是因为同行们无法理解他以“希腊城邦”来讲故事的幽默感。所以他第三次以“Paxos Made Simple”为题在“SIGACT News”杂志上发表这篇论文的时候终于放弃了“希腊城邦”的比喻尽可能用他认为简单直接、可读性较强的方式去介绍Paxos算法。这次的情况虽然比前两次要好一些但以Paxos本应获得的重视程度来说依然只能算是应者寥寥。
这一段听起来跟网络段子一般的经历被Lamport以自嘲的形式放到了他的个人网站上。尽管我们作为后辈应该尊重Lamport老爷子但是当我翻开“Paxos Made Simple”这篇论文见到只有“The Paxos algorithm, when presented in plain English, is very simple.”这一句话的“摘要”时还是忍不住在心里怀疑Lamport这样写论文是不是在恶搞审稿人和读者在嘲讽“你们这些愚蠢的人类”。
虽然Lamport本人连发三篇文章都没能让大多数同行理解Paxos但当时间来到了2006年Google的Chubby、Megastore和Spanner等分布式系统都使用Paxos解决了分布式共识的问题并将其整理成正式的论文发表。
之后得益于Google的行业影响力再加上Chubby的作者Mike Burrows那略显夸张但足够吸引眼球的评价推波助澜Paxos算法一夜间成为计算机科学分布式这条分支中最炙手可热网红概念开始被学术界众人争相研究。
2013年因为对分布式系统的杰出理论贡献Lamport获得了2013年的图灵奖。随后才有了Paxos在区块链、分布式系统、云计算等多个领域大放异彩的故事。其实这样充分说明了在技术圈里即使再有本事也还是需要好好包装一下。
讲完段子吃过西瓜希望你没有被这些对Paxos的“困难”做的铺垫所吓倒反正又不让你马上去实现它。
其实说难不难如果放弃些许严谨性并简化掉最繁琐的分支细节和特殊情况的话Paxos是完全可以去通俗地理解的。毕竟Lamport在论文中只用两段话就描述“清楚”了Paxos的工作流程。
下面我们来正式学习Paxos算法在本小节中Paxos指的都是最早的Basic Paxos算法
Paxos算法的工作流程
Paxos算法将分布式系统中的节点分为提案节点、决策节点和记录节点三类。
提案节点称为Proposer提出对某个值进行设置操作的节点设置值这个行为就是提案Proposal。值一旦设置成功就是不会丢失也不可变的。
需要注意的是Paxos是典型的基于操作转移模型而非状态转移模型来设计的算法所以这里的“设置值”不要类比成程序中变量的赋值操作而应该类比成日志记录操作。因此我在后面介绍Raft算法时就索性直接把“提案”叫做“日志”了。
决策节点称为Acceptor是应答提案的节点决定该提案是否可被投票、是否可被接受。提案一旦得到过半数决策节点的接受就意味着这个提案被批准Accept。提案被批准就意味着该值不能再被更改也不会丢失且最终所有节点都会接受它。
记录节点被称为Learner不参与提案也不参与决策只是单纯地从提案、决策节点中学习已经达成共识的提案。比如少数派节点从网络分区中恢复时将会进入这种状态。
在使用Paxos算法的分布式系统里所有的节点都是平等的它们都可以承担以上某一种或者多种角色。
不过为了便于确保有明确的多数派决策节点的数量应该被设定为奇数个且在系统初始化时网络中每个节点都知道整个网络所有决策节点的数量、地址等信息。另外在分布式环境下如果说各个节点“就某个值提案达成一致”代表的意思就是“不存在某个时刻有一个值为A另一个时刻这个值又为B的情景”。
而如果要解决这个问题的复杂度,主要会受到下面两个因素的共同影响:
系统内部各个节点间的通讯是不可靠的。不论对于系统中企图设置数据的提案节点,抑或决定是否批准设置操作的决策节点来说,它们发出、收到的信息可能延迟送达、也可能会丢失,但不去考虑消息有传递错误的情况。
系统外部各个用户访问是可并发的。如果系统只会有一个用户或者每次只对系统进行串行访问那单纯地应用Quorum机制少数节点服从多数节点就已经足以保证值被正确地读写了。
第一点“系统内部各个节点间的通讯是不可靠的”,是网络通讯中客观存在的现象,也是所有共识算法都要重点解决的问题。所以我们重点看下第二点“系统外部各个用户访问是可并发的”,即“分布式环境下并发操作的共享数据”问题。
为了方便理解,我们可以先不考虑是不是在分布式的环境下,只考虑并发操作。
假设有一个变量i当前在系统中存储的数值为2同时有外部请求A、B分别对系统发送操作指令“把i的值加1”和“把i的值乘3”。如果不加任何并发控制的话将可能得到“(2+1)×3=9”和“2×3+1=7”这两种结果。因此对同一个变量的并发修改必须先加锁后操作不能让A、B的请求被交替处理。这可以说是程序设计的基本常识了。
但是,在分布式的环境下,还要同时考虑到分布式系统内,可能在任何时刻出现的通讯故障。如果一个节点在取得锁之后、在释放锁之前发生崩溃失联,就会导致整个操作被无限期的等待所阻塞。因此,算法中的加锁,就不完全等同于并发控制中以互斥量来实现的加锁,还必须提供一个其他节点能抢占锁的机制,以避免因通讯问题而出现死锁的问题。
我们继续看Paxos算法是怎么解决并发操作带来的竞争的。
Paxos算法包括“准备Prepare”和“批准Accept”两个阶段。
第一阶段“准备”Prepare就相当于抢占锁的过程。如果某个提案节点准备发起提案必须先向所有的决策节点广播一个许可申请称为Prepare请求。提案节点的Prepare请求中会附带一个全局唯一的数字n作为提案ID决策节点收到后会给提案节点两个承诺和一个应答。
其中两个承诺是指承诺不会再接受提案ID小于或等于n的Prepare请求承诺不会再接受提案ID小于n的Accept请求。
一个应答是指在不违背以前作出的承诺的前提下回复已经批准过的提案中ID最大的那个提案所设定的值和提案ID如果该值从来没有被任何提案设定过则返回空值。如果违反此前做出的承诺也就是说收到的提案ID并不是决策节点收到过的最大的那就可以直接不理会这个Prepare请求。
当提案节点收到了多数派决策节点的应答称为Promise应答可以开始第二阶段“批准”Accept过程。这时有两种可能的结果
如果提案节点发现所有响应的决策节点此前都没有批准过这个值即为空就说明它是第一个设置值的节点可以随意地决定要设定的值并将自己选定的值与提案ID构成一个二元组(id, value)再次广播给全部的决策节点称为Accept请求
如果提案节点发现响应的决策节点中已经有至少一个节点的应答中包含有值了那它就不能够随意取值了必须无条件地从应答中找出提案ID最大的那个值并接受构成一个二元组(id, maxAcceptValue)然后再次广播给全部的决策节点称为Accept请求
当每一个决策节点收到Accept请求时都会在不违背以前作出的承诺的前提下接收并持久化当前提案ID和提案附带的值。如果违反此前做出的承诺即收到的提案ID并不是决策节点收到过的最大的那允许直接对此Accept请求不予理会。
当提案节点收到了多数派决策节点的应答称为Accepted应答协商结束共识决议形成将形成的决议发送给所有记录节点进行学习。整个过程的时序图如下所示
到这里整个Paxos算法的工作流程就结束了。
如果你之前没有专门学习过分布式的知识那学到这里你的感受很可能是操作过程中的每一步都能看懂但还是不能对Paxos算法究竟是如何解决协商共识这个问题形成具体的认知。
所以接下来我就不局限于抽象的算法步骤以一个更具体例子来讲解Paxos。这个例子以及其中使用的图片都来源于“Implementing Replicated Logs with Paxos”。
借助一个例子来理解Paxos算法
在这个例子中,我们只讨论正常通讯的场景,不会涉及网络分区。
假设一个分布式系统有五个节点分别是S1、S2、S3、S4和S5全部节点都同时扮演着提案节点和决策节点的角色。此时有两个并发的请求希望将同一个值分别设定为X由S1作为提案节点提出和Y由S5作为提案节点提出我们用P代表准备阶段、用A代表批准阶段这时候可能发生下面四种情况。
情况一比如S1选定的提案ID是3.1全局唯一ID加上节点编号先取得了多数派决策节点的Promise和Accepted应答此时S5选定的提案ID是4.5发起Prepare请求收到的多数派应答中至少会包含1个此前应答过S1的决策节点假设是S3。
那么S3提供的Promise中必将包含S1已设定好的值XS5就必须无条件地用X代替Y作为自己提案的值。由此整个系统对“取值为X”这个事实达成了一致。如下图所示
情况二事实上对于情况一X被选定为最终值是必然结果。但从图中可以看出X被选定为最终值并不是一定要多数派的共同批准而只取决于S5提案时Promise应答中是否已经包含了批准过X的决策节点。
比如下图所示S5发起提案的Prepare请求时X并未获得多数派批准但由于S3已经批准的关系最终共识的结果仍然是X。
情况三当然另外一种可能的结果是S5提案时Promise应答中并未包含批准过X的决策节点。
比如应答S5提案时节点S1已经批准了X节点S2、S3未批准但返回了Promise应答此时S5以更大的提案ID获得了S3、S4和S5的Promise。这三个节点均未批准过任何值那么S3将不会再接受来自S1的Accept请求因为它的提案ID已经不是最大的了。所以这三个节点将批准Y的取值整个系统最终会对“取值为Y”达成一致。
情况四从情况三可以推导出另一种极端的情况如果两个提案节点交替使用更大的提案ID使得准备阶段成功但是批准阶段失败的话这个过程理论上可以无限持续下去形成活锁Live Lock。在算法实现中会引入随机超时时间来避免活锁的产生。
到这里我们就又通过一个例子算是通俗地学习了一遍Paxos算法。
虽然Paxos是以复杂著称的算法但我们上面学习的是基于Basic Paxos、以正常流程未出现网络分区等异常、以通俗的方式讲解的Paxos算法并没有涉及到严谨的逻辑和数学原理也没有讨论Paxos的推导证明过程。所以这对于大多数不从事算法研究的技术人员来说理解起来应该也不会太过困难。
Basic Paxos的价值在于开拓了分布式共识算法的发展思路但因为它有如下缺陷一般不会直接用于实践Basic Paxos只能对单个值形成决议并且决议的形成至少需要两次网络请求和应答准备和批准阶段各一次高并发情况下将产生较大的网络开销极端情况下甚至可能形成活锁。
总之Basic Paxos是一种很学术化、对工业化并不友好的算法现在几乎只用来做理论研究。实际的应用都是基于Multi Paxos和Fast Paxos算法的在下一讲我们就会了解Multi Paxos以及和它理论等价的几个算法比如Raft、ZAB等算法。
小结
今天这节课,我们从分布式系统的高可靠与高可用的矛盾开始,首先学习了分布式共识算法的含义,以及为什么需要这种算法。我们也明确了“共识”这个词,在这个上下文中所指含义,就是“各个分布式节点针对于某个取值达成一致”。
其次我们了解了Basic Paxos算法发表的一些背景历史以及这种算法的主要工作流程。尽管我们很少有机会去研究或者实现分布式共识算法但理解它的基本原理是我们日后理解和使用etcd、ZooKeeper等分布式框架的重要基础。
一课一思
今天这节课我们一起学习了Paxos算法在正常情况下的工作流程。你认为Paxos在工程上需要考虑哪些异常情况呢实现的难度在哪里呢
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,190 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
32 _ 分布式共识Multi Paxos、Raft与Gossip分布式领域的基石
你好,我是周志明,这节课我们继续学习分布式共识算法。
在上节课的最后我通过一个批准阶段重复失败例子和你介绍了Basic Paxos的活锁问题两个提案节点互不相让地提出自己的提案抢占同一个值的修改权限导致整个系统在持续性地“反复横跳”从外部看就像是被锁住了。
同时我还讲过一个观点分布式共识的复杂性主要来源于网络的不可靠、请求的可并发这两大因素。活锁问题和许多Basic Paxos异常场景中所遭遇的麻烦都可以看作是源于任何一个提案节点都能够完全平等地、与其他节点并发地提出提案而带来的复杂问题。
为此Lamport专门设计“专门设计”的意思是在Paxos的论文中Lamport随意提了几句可以这么做了一种Paxos的改进版本“Multi Paxos”算法希望能够找到一种两全其美的办法既不破坏Paxos中“众节点平等”的原则又能在提案节点中实现主次之分限制每个节点都有不受控的提案权利。
这两个目标听起来似乎是矛盾的,但现实世界中的选举,就很符合这种在平等节点中挑选意见领袖的情景。
Multi Paxos
Multi Paxos对Basic Paxos的核心改进是增加了“选主”的过程
提案节点会通过定时轮询(心跳),确定当前网络中的所有节点里是否存在一个主提案节点;
一旦没有发现主节点存在节点就会在心跳超时后使用Basic Paxos中定义的准备、批准的两轮网络交互过程向所有其他节点广播自己希望竞选主节点的请求希望整个分布式系统对“由我作为主节点”这件事情协商达成一致共识
如果得到了决策节点中多数派的批准,便宣告竞选成功。
当选主完成之后除非主节点失联会发起重新竞选否则就只有主节点本身才能够提出提案。此时无论哪个提案节点接收到客户端的操作请求都会将请求转发给主节点来完成提案而主节点提案的时候也就无需再次经过准备过程因为可以视作是经过选举时的那一次准备之后后续的提案都是对相同提案ID的一连串的批准过程。
我们也可以通俗地理解为:选主过后,就不会再有其他节点与它竞争,相当于是处于无并发的环境当中进行的有序操作,所以此时系统中要对某个值达成一致,只需要进行一次批准的交互即可。具体如下序列所示:
你可能会注意到,二元组(id, value)已经变成了三元组(id, i, value),这是因为需要给主节点增加一个“任期编号”,这个编号必须是严格单调递增的,以应付主节点陷入网络分区后重新恢复,但另外一部分节点仍然有多数派,且已经完成了重新选主的情况,此时必须以任期编号大的主节点为准。
从整体来看当节点有了选主机制的支持后就可以进一步简化节点角色不必区分提案节点、决策节点和记录节点了可以统称为“节点”节点只有主Leader和从Follower的区别。此时的协商共识的时序图如下
在这个理解的基础上,我们换一个角度来重新思考“分布式系统中如何对某个值达成一致”这个问题,可以把它分为下面三个子问题来考虑:
如何选主Leader Election
如何把数据复制到各个节点上Entity Replication
如何保证过程是安全的Safety
可以证明(具体证明就不列在这里了,感兴趣的读者可参考结尾给出的论文),当这三个问题同时被解决时,就等价于达成共识。
接下来,我们分别看下这三个子问题如何解决。
关于“如何选主”虽然选主问题会涉及到许多工程上的细节比如心跳、随机超时、并行竞选等但从原理上来说只要你能够理解Paxos算法的操作步骤就不会有啥问题了。因为选主问题的本质仅仅是分布式系统对“谁来当主节点”这件事情的达成的共识而已。我们上节课其实就已经解决了“分布式系统该如何对一件事情达成共识”这个问题。
我们继续来解决数据Paxos中的提案、Raft中的日志在网络各节点间的复制问题。
在正常情况下当客户端向主节点发起一个操作请求后比如提出“将某个值设置为X”数据复制的过程为
主节点将X写入自己的变更日志但先不提交接着把变更X的信息在下一次心跳包中广播给所有的从节点并要求从节点回复“确认收到”的消息
从节点收到信息后,将操作写入自己的变更日志,然后给主节点发送“确认签收”的消息;
主节点收到过半数的签收消息后,提交自己的变更、应答客户端并且给从节点广播“可以提交”的消息;
从节点收到提交消息后提交自己的变更,数据在节点间的复制宣告完成。
那异常情况下的数据复制问题怎么解决呢?
网络出现了分区部分节点失联但只要仍能正常工作的节点数量能够满足多数派过半数的要求分布式系统就仍然可以正常工作。假设有S1、S2、S3、S4和S5共5个节点我们来看下数据复制过程。
假设由于网络故障形成了S1、S2和S3、S4、S5两个分区。
一段时间后S3、S4、S5三个节点中的某一个节点比如S3最先达到心跳超时的阈值获知当前分区中已经不存在主节点了于是S3向所有节点发出自己要竞选的广播并收到了S4、S5节点的批准响应加上自己一共三票竞选成功。此时系统中同时存在S1和S3两个主节点但由于网络分区它们都不知道对方的存在。
这种情况下,客户端发起操作请求的话,可能出现这么两种情况:
第一种如果客户端连接到了S1、S2中的一个都将由S1处理但由于操作只能获得最多两个节点的响应无法构成多数派的批准所以任何变更都无法成功提交。
第二种如果客户端连接到了S3、S4、S5中的一个都将由S3处理此时操作可以获得最多三个节点的响应构成多数派的批准变更就是有效的可以被提交也就是说系统可以继续提供服务。
事实上,这两种“如果”的场景同时出现的机会非常少。为什么呢?网络分区是由软、硬件或者网络故障引起的,内部网络出现了分区,但两个分区都能和外部网络的客户端正常通讯的情况,极为少见。更多的场景是,算法能容忍网络里下线了一部分节点,针对咱们这个例子来说,如果下线了两个节点系统可以正常工作,但下线了三个节点的话,剩余的两个节点也不可能继续提供服务了。
假设现在故障恢复,分区解除,五个节点可以重新通讯了:
S1和S3都向所有节点发送心跳包从它们的心跳中可以得知S3的任期编号更大、是最新的所以五个节点均只承认S3是唯一的主节点。
S1、S2回滚它们所有未被提交的变更。
S1、S2从主节点发送的心跳包中获得它们失联期间发生的所有变更将变更提交写入本地磁盘。
此时分布式系统各节点的状态达成最终一致。
到这里,第二个问题“数据在网络节点间的复制问题”也就解决了。我们继续看第三个问题,如何保证过程是安全的。
你可能要说了,选主和数据复制这两个问题都是很具体的行为,但“安全”这个表述很模糊啊,怎么判断什么是安全或者不安全呢?
要想搞明白这个问题我们需要先看下Safety和Liveness这两个术语。
在专业资料中Safety和Liveness通常会被翻译为“协定性”和“终止性”。它们也是由Lamport最先提出的定义是
协定性Safety所有的坏事都不会发生Something “bad” will never happen
终止性Liveness所有的好事都终将发生但不知道是啥时候Something “good” will must happen, but we dont know when
这种就算解释了你也看不明白的定义是不是很符合Lamport老爷子一贯的写作风格我也是无奈地摊手苦笑。不过没关系我们不用去纠结严谨的定义可以通过例子来理解它们的具体含义。
还是以选主问题为例Safety保证了选主的结果一定是有且只有唯一的一个主节点不可能同时出现两个主节点而Liveness则要保证选主过程是一定可以在某个时刻能够结束的。
我们再回想一下活锁的内容的话可以发现在Liveness这个属性上选主问题是存在理论上的瑕疵的可能会由于活锁而导致一直无法选出明确的主节点。所以Raft论文中只写了对Safety的保证但由于工程实现上的处理现实中是几乎不可能会出现终止性的问题。
最后以上这种把共识问题分解为“Leader Election”、“Entity Replication”和“Safety”三个问题来思考、解决的解题思路就是咱们这一节标题中的“Raft算法”。
《一种可以让人理解的共识算法》In Search of an Understandable Consensus Algorithm这篇论文提出了Raft算法并获得了USENIX ATC 2014大会的Best Paper更是成为了日后etcd、LogCabin、Consul等重要分布式程序的实现基础。ZooKeeper的ZAB算法和Raft的思路也非常类似这些算法都被认为是与Multi Paxos的等价派生实现。
Gossip协议
Paxos、Raft、ZAB等分布式算法经常会被称作是“强一致性”的分布式共识协议其实这样的描述扣细节概念的话是很别扭的会有语病嫌疑但我们都明白它的意思其实是在说“尽管系统内部节点可以存在不一致的状态但从系统外部看来不一致的情况并不会被观察到所以整体上看系统是强一致性的”。
与它们相对的,还有另一类被冠以“最终一致性”的分布式共识协议,这表明系统中不一致的状态有可能会在一定时间内被外部直接观察到。
一种典型而且非常常见的最终一致的分布式系统就是DNS系统在各节点缓存的TTL到期之前都有可能与真实的域名翻译结果存在不一致。
还有一种很有代表性的“最终一致性”的分布式共识协议那就是Gossip协议。Gossip协议主要应用在比特币网络和许多重要的分布式框架比如Consul的跨数据中心同步中。
Gossip最早是由施乐公司 Palo Alto研究中心在论文“Epidemic Algorithms for Replicated Database Maintenance”中提出的是一种用于分布式数据库在多节点间复制同步数据的算法。
扩展施乐公司Xerox现在可能很多人不了解施乐了或只把施乐当一家复印产品公司看待。其实施乐是计算机许多关键技术的鼻祖是图形界面的发明者、以太网的发明者、激光打印机的发明者、MVC架构的提出者、RPC的提出者、BMP格式的提出者……
从论文题目中可以看出最初它是被称作“流行病算法”Epidemic Algorithm但因为不太雅观Gossip这个名字会更普遍。另外你可能还会听到有人把它叫做“流言算法”“八卦算法”“瘟疫算法”等。其实这些名字都是很形象化的描述反映了Gossip的特点要同步的信息如同流言一般传播、病毒一般扩散。
按照习惯我也会把Gossip叫做“共识协议”但首先必须强调它所解决的问题并不是直接与Paxos、Raft这些共识算法等价的只是基于Gossip之上可以通过某些方法去实现与Paxos、Raft相类似的目标而已。
一个最典型的例子是比特币网络中使用到了Gossip协议用来在各个分布式节点中互相同步区块头和区块体的信息。这是整个网络能够正常交换信息的基础但并不能称作共识。比特币使用工作量证明Proof of WorkPoW来对“这个区块由谁来记账”这一件事儿在全网达成共识。这个目标才可以认为与Paxos、Raft的目标是一致的。
接下来我们一起学习下Gossip的具体工作过程。其实和Paxos、Raft等算法相比Gossip的过程可以说是十分简单了可以看作是两个步骤的简单循环
如果有某一项信息需要在整个网络中的所有节点中传播那从信息源开始选择一个固定的传播周期比如1秒随机选择与它相连接的k个节点称为Fan-Out来传播消息。
如果一个节点收到消息后发现这条消息之前没有收到过就会在下一个周期内把这条消息发送给除了给它发消息的那个节点之外的相邻的k个节点直到网络中所有节点都收到了这条消息。尽管这个过程需要一定的时间但理论上网络的所有节点最终都会拥有相同的消息。
Gossip传播过程的示意图如下所示
Gossip传播示意图
根据示意图和Gossip的过程描述我们很容易发现Gossip对网络节点的连通性和稳定性几乎没有任何要求表现在两个方面
它一开始就将某些节点只能与一部分节点部分连通Partially Connected Network而不是全连通网络Fully Connected Network作为前提
能够容忍网络上节点的随意地增加或者减少、随意地宕机或者重启,新增加或者重启的节点的状态,最终会与其他节点同步达成一致。
也就是说Gossip把网络上所有节点都视为平等而普通的一员没有中心化节点或者主节点的概念。这些特点使得Gossip具有极强的鲁棒性而且非常适合在公众互联网WAN中应用。
同时我们也很容易发现Gossip协议有两个缺点。
第一个缺点是,消息是通过多个轮次的散播而到达全网的,因此必然会存在各节点状态不一致的情况。而且,因为是随机选取的发送消息的节点,所以尽管可以在整体上测算出统计学意义上的传播速率,但我们还是没办法准确估计出单条消息的传播,需要多久才能达成全网一致。
第二个缺点是消息的冗余。这也是因为随机选取发送消息的节点,会不可避免地存在消息重复发送给同一节点的情况。这种冗余会增加网络的传输压力,也会给消息节点带来额外的处理负载。
达到一致性耗费的时间与网络传播中消息冗余量这两个缺点存在一定的对立关系如果要改善其中一个就会恶化另外一个。由此Gossip传播消息时有两种可能的方式反熵Anti-Entropy和传谣Rumor-Mongering。这两个名字听起来都挺文艺的我们具体分析下。
Entropy这个概念在生活中很少见但在科学中却很常用它代表的是事物的混乱程度。反熵就是反混乱的意思它把提升网络各个节点之间的相似度作为目标。
所以,在反熵模式下,为了达成全网各节点的完全一致的目标,会同步节点的全部数据,来消除各节点之间的差异。但是,在节点本身就会发生变动的前提下,这个目标将使得整个网络中消息的数量非常庞大,给网络带来巨大的传输开销。
而传谣模式是以传播消息为目标,仅仅发送新到达节点的数据,即只对外发送变更信息,这样消息数据量将显著缩减,网络开销也相对较小。
小结
对于普通开发者来说,分布式共识算法这两讲的内容理解起来还是有些困难的,因为算法更接近研究而不是研发的范畴。
但是理解Paxos算法对深入理解许多分布式工具比如HDFS、ZooKeeper、etcd、Consul等的工作原理是无可回避的基础。虽然Paxos不直接应用于工业界但它的变体算法比如我们今天学习的Multi Paxos、Raft算法以及今天我们没有提到的ZAB等算法都是分布式领域中的基石。
一课一思
结合自己了解的某一款分布式框架,你可以总结下共识算法具体在其中解决了什么问题吗?
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,180 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
33 _ 服务发现如何做到持续维护服务地址在动态运维中的时效性?
你好,我是周志明。
前面的两节课我们已经学习了与分布式相关的算法和理论掌握了一致性、共识、Paxos等共识算法为了解分布式环境中的操作共享数据打好了理论基础。那么从这一讲开始我们就来一起了解下在使用分布式服务构造大型系统的过程中都可能会遇到哪些问题以及针对这些问题都可以选择哪些解决方案。
好,那在正式开始学习之前呢,让我们先来思考一个问题:为什么在微服务应用中,需要引入服务发现呢?它的意义是什么?
服务发现解耦对位置的依赖
事实上,服务发现的意义是解耦程序对服务具体位置的依赖,对于分布式应用来说,服务发现不是可选项,而是必须的。
要理解分布式中的服务发现那不妨先以单机程序中的类库来类比因为类库概念的普及让计算机实现了通过位于不同模块的方法调用来组装复用指令序列的目的打开了软件达到更大规模的一扇大门。无论是编译期链接的C/CPP还是运行期链接的Java都要通过链接器Linker把代码里的符号引用转换为模块入口或进程内存地址的直接引用。
而服务概念的普及让计算机可以通过分布于网络中的不同机器互相协作来复用功能这是软件发展规模的第二次飞跃。此时如何确定目标方法的确切位置便是与编译链接有着等同意义的问题解决该问题的过程就被叫做“服务发现”Service Discovery
通过服务来实现组件-
Microservice architectures will use libraries, but their primary way of componentizing their own software is by breaking down into services.-
微服务架构也会使用到类库,但构成软件系统组件的主要方式是将其拆分为一个个服务。-
—— Martin Fowler / James Lewis, Microservices, 2014
所有的远程服务调用都是使用“全限定名Fully Qualified Domain NameFQDN、端口号、服务标识”构成的三元组来确定一个远程服务的精确坐标的。全限定名代表了网络中某台主机的精确位置端口代表了主机上某一个提供服务的程序服务标识则代表了该程序所提供的一个方法接口。
其中“全限定名、端口号”的含义在各种远程服务中都一致而“服务标识”则与具体的应用层协议相关它可以是多样的比如HTTP的远程服务标识是URL地址RMI的远程服务标识是Stub类中的方法SOAP的远程服务标识是WSDL中的定义等等。
也正是因为远程服务的多样性,导致了“服务发现”也会有两种不同的理解。
一种是以UDDI为代表的“百科全书式”的服务发现。上到提供服务的企业信息企业实体、联系地址、分类目录等下到服务的程序接口细节方法名称、参数、返回值、技术规范等它们都在服务发现的管辖范围之内。
另一种是类似于DNS这样的“门牌号码式”的服务发现。这种服务发现只满足从某个代表服务提供者的全限定名到服务实际主机IP地址的翻译转换。它并不关心服务具体是哪个厂家提供的也不关心服务有几个方法各自都由什么参数所构成它默认这些细节信息服务消费者本身就是了解的。此时服务坐标就可以退化为简单的“全限定名+端口号”。
现如今,主要是后一种服务发现占主流地位,所以咱们这节课要探讨的服务发现,如无说明,都是指的后者。
在前面讲“透明多级分流系统”这个小章节的时候我提到过原本程序只依赖DNS把一个全限定名翻译为一个或者多个IP地址或者SRV等其他记录就可以实现服务发现了后来的负载均衡器实质上也承担了一部分服务发现的职责指外部IP地址到各个服务内部实际IP的转换。我们也已经详细解析过这种方式在软件追求不间断长时间运行的时代是很合适的。
但随着微服务的逐渐流行服务的非正常宕机重启和正常的上下线操作变得更加频繁仅靠着DNS服务器和负载均衡器等基础设施就显得逐渐有些疲于应对无法跟上服务变动的步伐了。
因此人们开始尝试使用ZooKeeper这样的分布式K/V框架通过软件自身来完成服务注册与发现。ZooKeeper曾短暂统治过远程服务发现是微服务早期对服务发现的主流选择但毕竟ZooKeeper是很底层的分布式工具用户自己还需要做相当多的工作才能满足服务发现的需求。
那到了2014年在Netflix内部经受过长时间实际考验的、专门用于服务发现的Eureka宣布了开源并很快被纳入Spring Cloud成为Spring默认的远程服务发现的解决方案。从此Java程序员就无需再在服务注册这件事情上花费太多的力气。
然后到2018年Spring Cloud Eureka进入维护模式以后HashiCorp的Consul和阿里巴巴的Nacos就很快从Eureka手上接过传承的衣钵。此时的服务发现框架已经发展得相当成熟考虑到了几乎方方面面的问题比如可以支持通过DNS或者HTTP请求进行符号与实际地址的转换支持各种各样的服务健康检查方式支持集中配置、K/V存储、跨数据中心的数据交换等多种功能可以说是以应用自身去解决服务发现的一个顶峰。
而如今,云原生时代来临,基础设施的灵活性得到了大幅度地增强,最初使用基础设施来透明化地做服务发现的方式,又重新被人们所重视了,如何在基础设施和网络协议层面,对应用尽可能无感知、尽可能方便地实现服务发现,便是目前一个主要的发展方向。
接下来,我们就具体来看看服务发现的三个关键的子问题,并一起探讨、对比下最常见的用作服务发现的几种形式,以此让你了解服务发现中,可用性与可靠性之间的关系和权衡。
服务发现要解决注册、维护和发现三大功能问题
那么,第一个问题就是,“服务发现”具体是指进行过什么操作呢?我认为,这里面其实包含了三个必须的过程:
服务的注册Service Registration
当服务启动的时候它应该通过某些形式比如调用API、产生事件消息、在ZooKeeper/Etcd的指定位置记录、存入数据库等等把自己的坐标信息通知给服务注册中心这个过程可能由应用程序来完成比如Spring Cloud的@EnableDiscoveryClient注解也可能是由容器框架比如Kubernetes来完成。
服务的维护Service Maintaining
尽管服务发现框架通常都有提供下线机制但并没有什么办法保证每次服务都能优雅地下线Graceful Shutdown而不是由于宕机、断网等原因突然失联。所以服务发现框架就必须要自己去保证所维护的服务列表的正确性以避免告知消费者服务的坐标后得到的服务却不能使用的尴尬情况。
现在的服务发现框架一般都可以支持多种协议HTTP、TCP等、多种方式长连接、心跳、探针、进程状态等来监控服务是否健康存活然后把不健康的服务自动下线。
服务的发现Service Discovery
这里所说的发现是狭义的它特指消费者从服务发现框架中把一个符号比如Eureka中的ServiceID、Nacos中的服务名、或者通用的FDQN转换为服务实际坐标的过程这个过程现在一般是通过HTTP API请求或者是通过DNS Lookup操作来完成的还有一些相对少用的方式如Kubernetes也支持注入环境变量
当然我提到的这三点只是列举了服务发现中必须要进行的过程除此之外它还是会有一些可选的功能的比如在服务发现时进行的负载均衡、流量管控、K/V存储、元数据管理、业务分组等等这些功能都属于具体服务发现框架的功能细节这里就不再展开了。
下面我们来讨论另一个很常见的问题。
不知道你有没有观察过很多谈论服务发现的文章总是无可避免地会先扯到“CP”还是“AP”的问题上。那么为什么服务发现对CAP如此关注、如此敏感呢
其实,我们可以从服务发现在整个系统中所处的角色,来着手分析这个问题。
在概念模型中,服务中心所处的地位是这样的:提供者在服务发现中注册、续约和下线自己的真实坐标,消费者根据某种符号从服务发现中获取到真实坐标,它们都可以看作是系统中平等的微服务。我们来看看这个概念模型示意图:
不过,在真实的系统中,服务发现的地位还是有一些特殊,我们还不能把它完全看作是一个普通的服务。为啥呢?
这是因为,服务发现是整个系统中,其他所有服务都直接依赖的最基础的服务(类似相同待遇的大概就数配置中心了,现在服务发现框架也开始同时提供配置中心的功能,以避免配置中心又去专门搞出一集群的节点来),几乎没有办法在业务层面进行容错处理。而服务注册中心一旦崩溃,整个系统都会受到波及和影响,因此我们必须尽最大可能,在技术层面上保证系统的可用性。
所以,在分布式系统中,服务注册中心一般会以内部小集群的方式进行部署,提供三个或者五个节点(通常最多七个,一般也不会更多了,否则日志复制的开销太高)来保证高可用性。你可以看看下面给出的这个例子:
另外这里你还要注意一点就是这个图例中各服务发现节点之间的“Replicate”字样。
作为用户我们当然希望服务注册一直可用、永远健康的同时也能够在访问每一个节点中都取到一致的数据而这两个需求就构成了CAP矛盾。
我拿前面提到的最有代表性的Eureka和Consul来举个例子。
这里我以AP、CP两种取舍作为选择维度Consul采用的是Raft协议要求多数派节点写入成功后服务的注册或变动才算完成这就严格地保证了在集群外部读取到的服务发现结果一定是一致的Eureka的各个节点间采用异步复制来交换服务注册信息服务注册或变动时并不需要等待信息在其他节点复制完成而是马上在该服务发现节点就宣告可见但其他节点是否可见并不保证
实际上,这两点差异带来的影响并不在于服务注册的快慢(当然,快慢确实是有差别),而在于你如何看待以下这件事情:
假设系统形成了A、B两个网络分区后A区的服务只能从区域内的服务发现节点获取到A区的服务坐标B区的服务只能取到在B区的服务坐标这对你的系统会有什么影响
如果这件事情对你并没有什么影响甚至有可能还是有益的那你就应该倾向于选择AP的服务发现。比如假设A、B就是不同的机房是机房间的网络交换机导致服务发现集群出现的分区问题但每个分区中的服务仍然能独立提供完整且正确的服务能力此时尽管不是有意而为但网络分区在事实上避免了跨机房的服务请求反而还带来了服务调用链路优化的效果。
如果这件事情可能对你影响非常大甚至可能带来比整个系统宕机更坏的结果那你就应该倾向于选择CP的服务发现。比如系统中大量依赖了集中式缓存、消息总线、或者其他有状态的服务一旦这些服务全部或者部分被分隔到某一个分区中会对整个系统的操作正确性产生直接影响的话那与其搞出一堆数据错误还不如停机来得痛快。
除此之外,在服务发现的过程中,对系统的可用性和可靠性的取舍不同,对服务发现框架的具体实现也有着决定性的影响。接下来,我们就具体来了解下几类不同的服务发现的实现形式。
服务发现需要有效权衡一致性与可用性的矛盾
数据一致性与服务可用性之间的矛盾是分布式系统永恒的话题。而在服务发现这个场景里,权衡的主要关注点是一旦出现分区所带来的后果,其他在系统正常运行过程中,出现的速度问题都是次要的。
所以最后,我们再来讨论一个很“务实”的话题:现在那么多的服务发现框架,哪一款最好呢?或者说我们应该如何挑选最适合的呢?
实际上,现在直接以服务发现、服务注册中心为目标,或者间接用来实现这个目标的方式主要有以下三类:
第一类在分布式K/V存储框架上自己实现的服务发现
这类的代表是ZooKeeper、Doozerd、Etcd。这些K/V框架提供了分布式环境下读写操作的共识保证Etcd采用的是我们学习过的Raft算法ZooKeeper采用的是ZAB算法一种Multi Paxos的派生算法所以采用这种方案就不必纠结CP还是AP的问题了它们都是CP的。
这类框架的宣传语中往往会主动提及“高可用性”它们的潜台词其实是“在保证一致性和分区容错性的前提下尽最大努力实现最高的可用性”比如Etcd的宣传语就是“高可用的集中配置和服务发现”Highly-Available Key Value Store for Shared Configuration and Service Discovery
这些K/V框架的另一个共同特点是在整体较高复杂度的架构和算法的外部维持着极为简单的应用接口只有基本的CRUD和Watch等少量API所以我们如果要在上面完成功能齐全的服务发现有很多基础的能力比如服务如何注册、如何做健康检查等等都必须自己实现因此现在一般也只有“大厂”才会直接基于这些框架去做服务发现了。
第二类以基础设施主要是指DNS服务器来实现服务发现
这类的代表是SkyDNS、CoreDNS。在Kubernetes 1.3之前的版本是使用SkyDNS作为默认的DNS服务它的工作原理是从API Server中监听集群服务的变化然后根据服务生成NS、SRV等DNS记录存放到Etcd中kubelet会在每个Pod内部设置DNS服务的地址作为SkyDNS的地址在需要调用服务时只需查询DNS把域名转换成IP列表便可实现分布式的服务发现。
而在Kubernetes 1.3之后SkyDNS不再是默认的DNS服务器也不再使用Etcd存储记录而是只将DNS记录存储在内存中的KubeDNS代替到了1.11版就更推荐采用扩展性很强的CoreDNS此时我们可以通过各种插件来决定是否要采用Etcd存储、重定向、定制DNS记录、记录日志等等。
那么采用这种方案的话是CP还是AP就取决于后端采用何种存储如果是基于Etcd实现的那自然是CP的如果是基于内存异步复制的方案实现的那就是AP的。
也就是说以基础设施来做服务发现好处是对应用透明任何语言、框架、工具都肯定是支持HTTP、DNS的所以完全不受程序技术选型的约束。但它的坏处是透明的并不一定简单你必须自己考虑如何去做客户端负载均衡、如何调用远程方法等这些问题而且必须遵循或者说受限于这些基础设施本身所采用的实现机制。
比如在服务健康检查里服务的缓存期限就必须采用TTLTime to Live来决定这是DNS协议所规定的如果想改用KeepAlive长连接来实时判断服务是否存活就很麻烦。
第三类:专门用于服务发现的框架和工具
这类的代表是Eureka、Consul和Nacos。-
这一类框架中你可以自己决定是CP还是AP的问题比如CP的Consul、AP的Eureka还有同时支持CP和AP的NacosNacos采用类Raft协议做的CP采用自研的Distro协议做的AP注意这里的“同时”是“都支持”的意思它们必须二取其一不是说CAP全能满足
另外还有很重要一点是它们对应用并不是透明的。尽管Consul、Nacos也支持基于DNS的服务发现尽管这些框架都基本上做到了以声明代替编码比如在Spring Cloud中只改动pom.xml、配置文件和注解即可实现但它们依然是应用程序有感知的。所以或多或少还需要考虑你所用的程序语言、技术框架的集成问题。
但这一点其实并不见得就是坏处比如采用Eureka做服务注册那在远程调用服务时你就可以用OpenFeign做客户端写个声明式接口就能跑相当能偷懒在做负载均衡时你就可以采用Ribbon做客户端要想换均衡算法的话改个配置就成这些“不透明”实际上都为编码开发带来了一定的便捷而前提是你选用的语言和框架要支持。如果你的老板提出要在Rust上用Eureka那你就只能无奈叹息了原本这里我写的是Node、Go、Python等查了一下这些居然都有非官方的Eureka客户端用的人多就是有好处啊
小结
微服务架构中的一个重要设计原则是“通过服务来实现独立自治的组件”Componentization via Services微服务强调通过“服务”Service而不是“类库”Library来构建组件这是因为两者具有很大的差别类库是在编译期静态链接到程序中的通过本地调用来提供功能而服务是进程外组件通过远程调用来提供功能。
在这节课中我们共同了解了服务发现在微服务架构中的意义它是将固定的代表服务的标识符转化为动态的真实服务地址并持续维护这些地址在动态运维过程中的时效性。因此为了完成这个目标服务发现需要解决注册、维护和发现三大功能问题并且需要妥善权衡分布式环境下一致性与可用性之间的矛盾由此便派生出了以DNS、专有服务等不同形式AP和CP两种不同权衡取向的实现方案。
而且,基于服务来构建程序,这也迫使微服务在复杂性与执行性能方面作出了极大的让步,而换来的收益就是软件系统“整体”与“部分”的物理层面的真正的隔离。
一课一思
使用DNS来做服务发现是最符合传统的做法这也是现代虚拟化容器编排系统如Kubernetes所提供的方案。
那么请你思考一下为何有了DNS还会出现Eureka、Consul这些专有的服务发现框架后者有哪些能力是前者无法提供的呢你是否有办法在以DNS为代表的虚拟化基础设施中解决服务发现问题而无需使用在应用层面提供的框架、类库呢
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,179 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 _ 路由凭什么作为微服务网关的基础职能?
你好,我是周志明。这节课我们要探讨的话题是微服务中的入口:网关。
网关Gateway这个词我们应该都很熟悉了它在计算机科学中尤其是计算机网络中十分常见主要是用来表示位于内部区域边缘与外界进行交互的某个物理或逻辑设备比如你家里的路由器就属于家庭内网与互联网之间的网关。
在单体架构下,我们一般不太强调“网关”这个概念,因为给各个单体系统的副本分发流量的负载均衡器,实质上就承担着内部服务与外部调用之间的网关角色。
不过在微服务环境中,网关的存在感就极大地增强了,甚至成为微服务集群中必不可少的设施之一。
其中原因并不难理解。你可以想想看,在微服务架构下,每个服务节点都由不同的团队负责,它们有自己独立的、各不相同的能力,所以如果服务集群没有一个统一对外交互的代理人角色,那外部的服务消费者就必须知道所有微服务在集群中的精确坐标(上一讲我介绍过“坐标”的概念)。
这样,消费者不仅会受到服务集群的网络限制(不能确保集群中每个节点都有外网连接)、安全限制(不仅是服务节点的安全,外部自身也会受到如浏览器同源策略的约束)、依赖限制(服务坐标这类信息不属于对外接口承诺的内容,随时可能变动,不应该依赖它),就算是我们自己也不可能愿意记住每一个服务的坐标位置来编写代码。
所以微服务中网关的首要职责就是以统一的地址对外提供服务将外部访问这个地址的流量根据适当的规则路由到内部集群中正确的服务节点之上。也正是因为这样微服务中的网关也常被称为“服务网关”或者“API网关”。
可见,微服务的网关首先应该是个路由器,在满足此前提的基础上,网关还可以根据需要作为流量过滤器来使用,以提供某些额外的可选的功能。比如安全、认证、授权、限流、监控、缓存,等等。
简而言之:
网关 = 路由器(基础职能) + 过滤器(可选职能)
在“路由”这个基础职能里,服务网关主要考虑的是能够支持路由的“网络层次与协议”和“性能与可用性”两方面的因素。那么接下来,我们就围绕这两方面因素来讲解路由的原理与知识点。
网络层次与协议
在第20讲“负载均衡器”中我曾给你介绍过四层流量转发与七层流量代理这里所说的“层次”就是指OSI七层协议中的层次更具体的话其实就是“四层”和“七层”的意思。
仅从技术实现的角度来看对于路由流量这项工作负载均衡器与服务网关的实现是没有什么差别的很多服务网关本身就是基于老牌的负载均衡器来实现的比如Nginx、HAProxy对应的Ingress Controller等等而从路由目的这个角度来看负载均衡器与服务网关的区别在于前者是为了根据均衡算法对流量进行平均地路由后者是为了根据流量中的某种特征进行正确地路由。
也就是说,网关必须能够识别流量中的特征,这意味着网关能够支持的网络层次、通讯协议的数量,将会直接限制后端服务节点能够选择的服务通讯方式:
如果服务集群只提供如Etcd这类直接基于TCP访问的服务那就可以只部署四层网关以TCP报文中的源地址、目标地址为特征进行路由
如果服务集群要提供HTTP服务的话就必须部署一个七层网关根据HTTP的URL、Header等信息为特征进行路由
如果服务集群要提供更上层的WebSocket、SOAP等服务那就必须要求网关同样能够支持这些上层协议才能从中提取到特征。
我们直接来看一个例子吧。
这里是一段基于SpringCloud实现的Fenixs Bootstore中用到的Netflix Zuul网关的配置。Zuul是HTTP网关“/restful/accounts/**”和“/restful/pay/**”是HTTP中URL的特征而配置中的“serviceId”就是路由的目标服务。
routes:
account:
path: /restful/accounts/**
serviceId: account
stripPrefix: false
sensitiveHeaders: "*"
payment:
path: /restful/pay/**
serviceId: payment
stripPrefix: false
sensitiveHeaders: "*"
最后呢我还想给你一个提醒现在围绕微服务的各种技术都处于快速发展期我其实并不提倡你针对每一种框架本身去记忆配置的细节也就是你并不需要去纠结前面给出的这些配置的确切写法、每个指令的含义。因为如果你从根本上理解了网关的原理那你参考一下技术手册很容易就能够将前面给出的这些信息改写成Kubernetes Ingress Controller、Istio VirtualServer或者是其他服务网关所需的配置形式。
OK 我们再来了解下服务网关的另一个能够支持路由的重要因素:性能与可用性。
性能与可用性
性能与可用性是网关的一大关注点。因为网关是所有服务对外的总出口是流量必经之地所以网关的路由性能是全局的、系统性的如果某个服务经过网关路由会有10毫秒的性能损失就意味着整个系统所有服务的性能都会降低10毫秒。
网关的性能与它的工作模式和自身实现都有关系但毫无疑问工作模式是最主要的。如果网关能够采用三角传输模式DSR即数据链路层负载均衡模式原理上就决定了性能一定会比代理模式来的强DSR、代理等都是负载均衡的基础知识你可以去回顾复习一下
不过因为今天REST和JSON-RPC等基于HTTP协议的接口形式在对外部提供的服务中占绝对主流的地位所以我们这里所讨论的服务网关默认都必须支持七层路由这样通常就默认无法转发只能采用代理模式。
那么在这个前提约束下网关的性能就主要取决于它们是如何代理网络请求的也就是它们的网络I/O模型了。既然如此下面我们就一起来了解下网络I/O的基础知识剖析下网络I/O模型的工作原理借此也掌握不同网关的特点与性能差异。
网络I/O的基础知识
在套接字接口的抽象下网络I/O的本质其实是Socket的读取Socket在操作系统接口中被抽象为了数据流而网络I/O就可以理解为是对流的操作。
对于每一次网络访问,从远程主机返回的数据会先存放到操作系统内核的缓冲区中,然后再从内核的缓冲区,复制到应用程序的地址空间,所以当一次网络请求发生后,就会按顺序经历“等待数据从远程主机到达缓冲区”和“将数据从缓冲区拷贝到应用程序地址空间”两个阶段。
那么根据完成这两个阶段的不同方法我们可以把网络I/O模型总结为两类、五种模型。两类是指同步I/O与异步I/O五种是指在同步I/O中又划分出了阻塞I/O、非阻塞I/O、多路复用I/O和信号驱动I/O四种细分模型。
同步就是指调用端发出请求之后在得到结果之前必须一直等待与之相对的就是异步在发出调用请求之后将立即返回不会马上得到处理结果这个结果将通过状态变化和回调来通知给调用者。而阻塞和非阻塞I/O针对请求处理的过程就是指在收到调用请求、返回结果之前当前处理线程是否会被挂起。
当然,这种概念上的讲述估计你也不好理解,所以下面我就以“你如何领到盒饭”这个情景,来给你类比解释一下:
异步I/OAsynchronous I/O
这就好比你在某团外卖订了个盒饭付款之后你自己该干嘛还干嘛去饭做好了骑手自然会到门口打电话通知你。所以说异步I/O中数据到达缓冲区后不需要由调用进程主动进行从缓冲区复制数据的操作而是在复制完成后由操作系统向线程发送信号所以它一定是非阻塞的。
同步I/OSynchronous I/O
这就好比你自己去饭堂打饭,这时可能有以下几种情形发生:
阻塞I/OBlocking I/O
你去到饭堂发现饭还没做好你也干不了别的只能打个瞌睡线程休眠直到饭做好。阻塞I/O是最直观的I/O模型逻辑清晰也比较节省CPU资源但缺点就是线程休眠所带来的上下文切换这是一种需要切换到内核态的重负载操作不应当频繁进行。
非阻塞I/ONon-Blocking I/O
你去到饭堂发现饭还没做好你就回去了然后每隔3分钟来一次饭堂看饭做好了没直到饭做好。非阻塞I/O能够避免线程休眠对于一些很快就能返回结果的请求非阻塞I/O可以节省上下文切换的消耗但是对于较长时间才能返回的请求非阻塞I/O反而白白浪费了CPU资源所以目前并不常用。
多路复用I/OMultiplexing I/O
多路复用I/O本质上是阻塞I/O的一种但是它的好处是可以在同一条阻塞线程上处理多个不同端口的监听。可以类比这样一个情景你是活雷锋代表整个宿舍去饭堂打饭去到饭堂发现饭还没做好还是继续打瞌睡不过哪个舍友的饭好了你就马上把那份饭送回去然后继续打着瞌睡哼着歌等待其他的饭做好。多路复用I/O是目前的高并发网络应用的主流它下面还可以细分select、epoll、kqueue等不同实现。
信号驱动I/OSignal-Driven I/O
你去到饭堂发现饭还没做好但你跟厨师熟跟他说饭做好了叫你然后回去该干嘛干嘛等收到厨师通知后你把饭从饭堂拿回宿舍。这里厨师的通知就是那个“信号”信号驱动I/O与异步I/O的区别是“从缓冲区获取数据”这个步骤的处理前者收到的通知是可以开始进行复制操作了也就是你要自己把饭从饭堂拿回宿舍在复制完成之前线程处于阻塞状态所以它仍属于同步I/O操作而后者收到的通知是复制操作已经完成即外卖小哥已经把饭送到了。
那么显而易见异步I/O模型是最方便的毕竟能叫外卖谁愿意跑饭堂啊但前提是你学校里得让送美团外卖。所以异步I/O受限于操作系统Windows NT内核早在3.5以后就通过IOCP实现了真正的异步I/O模型。而Linux系统下是在Linux Kernel 2.6才首次引入目前也还并不完善因此在Linux下实现高并发网络编程时仍然是以多路复用I/O模型模式为主。
网关的性能考量
回到服务网关的话题上现在我们掌握了网络I/O模型的知识就可以在理论上定性分析不同网关的性能差异了。
服务网关处理一次请求代理时包含了两组网络操作分别是“作为服务端对外部请求的应答”和“作为客户端对内部服务的调用”。理论上这两组网络操作可以采用不同的网络I/O模型去完成但一般来说并没有必要这样做。
为什么呢我以Zuul网关来给你举个例子。
在Zuul 1.0时它采用的是阻塞I/O模型来进行最经典的“一条线程对应一个连接”Thread-per-Connection的方式来代理流量而采用阻塞I/O就意味着它会有线程休眠就有上下文切换的成本。
所以如果后端服务普遍属于计算密集型CPU Bound可以通俗理解为服务耗时比较长主要消耗在CPU上这种模式能够节省网关的CPU资源但如果后端服务普遍都是I/O密集型I/O Bound可以理解服务都很快返回主要消耗在I/O上它就会由于频繁的上下文切换而降低性能。
那么到了Zuul的2.0版本最大的改进就是基于Netty Server实现了异步I/O模型来处理请求大幅度减少了线程数获得了更高的性能和更低的延迟。根据Netflix官方自己给出的数据Zuul 2.0大约要比Zuul 1.0快20%左右。当然还有一些网关我们也可以自行配置或者根据环境选择不同的网络I/O模型典型的就是Nginx可以支持在配置文件中指定select、poll、epoll、kqueue等并发模型。
不过网关的性能高低一般只能去定性分析要想定量地说哪一种网关性能最高、高多少是很难的就像我们都认可Chrome要比IE快但具体要快上多少我们很难说得清楚。
所以尽管我上面引用了Netflix官方对Zuul两个版本的量化对比网络上也有不少关于各种网关的性能对比数据但要是脱离具体应用场景去定量地比较不同网关的性能差异还是难以令人信服毕竟不同的测试环境、后端服务都会直接影响结果。
网关的可用性考量
OK我们还有一点要关注的就是网关的可用性问题。任何系统的网络调用过程中都至少会有一个单点存在这是由用户只通过唯一的一个地址去访问系统决定的。即使是淘宝、亚马逊这样全球多数据中心部署的大型系统给多数据中心翻译地址的权威DNS服务器也可以认为是它的单点。
而对于更普遍的小型系统(小型是相对淘宝这些而言)来说,作为后端对外服务代理人角色的网关,经常被看作是系统的入口,往往很容易成为网络访问中的单点。这时候,它的可用性就尤为重要。
另外,由于网关具有唯一性,它不像之前讲服务发现时的那些注册中心一样,可以直接做个集群,随便访问哪一台都可以解决问题。所以针对这个情况,在网关的可用性方面,我们应该考虑到以下几点:
网关应尽可能轻量。尽管网关作为服务集群统一的出入口,可以很方便地做安全、认证、授权、限流、监控等功能,但在给网关附加这些能力时,我们还是要仔细权衡,取得功能性与可用性之间的平衡,不然过度增加网关的职责是很危险的。
网关选型时应该尽可能选择较成熟的产品实现。比如Nginx Ingress Controller、KONG、Zuul等等这些经受过长期考验的产品我们不能一味只考虑性能选择最新的产品毕竟性能与可用性之间的平衡也需要做好权衡。
在需要高可用的生产环境中应当考虑在网关之前部署负载均衡器或者等价路由器ECMP让那些更成熟健壮的往往是硬件物理设备的设施去充当整个系统的入口地址这样网关就可以很方便地设置多路扩展了。
这里我提到了网关的唯一性、高可用与扩展所以我顺带也说一下近年来随着微服务一起火起来的概念“BFF”Backends for Frontends
这个概念目前还没有权威的中文翻译,不过在我们讨论的上下文里,它的意思可以理解为,网关不必为所有的前端提供无差别的服务,而是应该针对不同的前端,聚合不同的服务,提供不同的接口和网络访问协议支持。
比如运行于浏览器的Web程序由于浏览器一般只支持HTTP协议服务网关就应该提供REST等基于HTTP协议的服务但同时我们也可以针对运行于桌面系统的程序部署另外一套网关它能与Web网关有完全不同的技术选型能提供基于更高性能协议如gRPC的接口来获得更好的体验。
所以这个概念要表达的就是,在网关这种边缘节点上,针对同样的后端集群,裁剪、适配、聚合出适应不一样的前端服务,有助于后端的稳定,也有助于前端的赋能。
小结
这节课我们主要探讨的话题是网关,但我只给你介绍了网关的路由职能,其他可以在网关上实现的限流、容错、安全、认证等等的过滤职能,在课程中都有专门的讲解,所以这里我们就不展开了。
那么在路由方面因为现在我们所讨论的服务网关默认都必须支持七层路由通常就默认无法转发只能采用代理模式。因此你要掌握这样一个核心知识点在必须支持七层路由的前提下网关的性能主要取决于它们是如何代理网络请求的也就是说你要了解它们的网络I/O模型。现在在学习了典型的网络I/O模型的工作原理之后希望你在后面的学习或者实践过程当中看到网关的I/O模型你就能够对它的特点与性能有个大致的判断。
一课一思
这节课的最后我给你介绍了网关路由职能在BFF方面的应用。那么除了BFF之外你还用网关来做什么另外如今微服务兴起在网关这个概念独立之前你觉得这项功能是如何实现的呢
欢迎给我留言,分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,176 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
35 _ 如何在客户端实现服务的负载均衡?
你好,我是周志明。这节课我们来学习客户端负载均衡的实现原理。
在正式开始讨论之前,我们先来区分清楚几个容易混淆的概念,分别是前面两讲中我介绍过的服务发现、网关路由,以及这节课要探讨的负载均衡,还有在下一讲中将会介绍的调用容错。这几个技术名词都带有“从服务集群中寻找到一个合适的服务来调用”的含义,那么它们之间的差别都体现在哪呢?下面我就通过一个具体的案例场景来给你说明一下。
理解服务发现、网关路由、负载均衡、调用容错的具体区别
假设你目前身处广东要上Fenixs Bookstore购买一本书。在程序业务逻辑里购书的其中一个关键步骤是调用商品出库服务来完成货物准备在代码中该服务的调用请求为
PATCH https://warehouse:8080/restful/stockpile/3
{amount: -1}
假设Fenixs Bookstore是个大书店在北京、武汉、广州的机房均部署有服务集群那么此时按顺序会发生以下事件
首先是将warehouse这个服务名称转换为了恰当的服务地址。
注意这里的“恰当”是个宽泛的描述一种典型的“恰当”就是因为调用请求来自于广东DNS层面的负载均衡就会优先分配给传输距离最短的广州机房来应答。
其实按常理来说这次出库服务的调用应该是集群内的流量而不是用户浏览器直接发出的请求。所以尽管结果都一样但更接近实际的情况应该是用户访问首页时已经被DNS服务器分配到了广州机房请求出库服务时应优先选择同机房的服务进行调用此时该服务的调用请求就变为
PATCH https://guangzhou-ip-wan:8080/restful/stockpile/3
然后广州机房的服务网关将该请求与配置中的特征进行比对由URL中的“/restful/stockpile/**”得知该请求访问的是商品出库服务。因此将请求的IP地址转换为内网中warehouse服务集群的入口地址
PATCH https://warehouse-gz-lan:8080/restful/stockpile/3
由于集群中部署有多个warehouse服务所以在收到调用请求后负载均衡器要在多个服务中根据某种标准或均衡策略可能是随机挑选也可能是按顺序轮询或者是选择此前调用次数最少那个等等找出要响应本次调用的服务我们称其为warehouse-gz-lan-node1。
PATCH https://warehouse-gz-lan-node1:8080/restful/stockpile/3
接着访问warehouse-gz-lan-node1服务没有返回需要的结果而是抛出了500错。
HTTP/1.1 500 Internal Server Error
那么根据预置的故障转移Failover策略负载均衡器会进行重试把调用分配给能够提供该服务的其他节点我们称其为warehouse-gz-lan-node2。
PATCH https://warehouse-gz-lan-node2:8080/restful/stockpile/3
最后warehouse-gz-lan-node2服务返回商品出库成功。
HTTP/1.1 200 OK
所以从整体上看前面步骤中的1、2、3、5就分别对应了服务发现、网关路由、负载均衡和调用容错而从细节上看其中的部分职责又是有交叉的并不是注册中心就只关心服务发现网关只关心路由均衡器只关心负载均衡。
比如步骤1服务发现的过程中“根据请求来源的物理位置来分配机房”这个操作本质上是根据请求中的特征地理位置来进行流量分发这其实是一种路由行为。在实际的系统中DNS服务器DNS智能线路、服务注册中心如Eureka等框架中的Region、Zone概念或者负载均衡器可用区负载均衡如AWS的NLB或Envoy的Region、Zone、Sub-zone里都有可能实现路由功能。
而且除此之外,你有没有感觉到这个网络调用的过程好像过于繁琐了,一个从广州机房内网发出的服务请求,绕到了网络边缘的网关、负载均衡器这些设施上,然后再被分配回内网中另外一个服务去响应。这不仅消耗了带宽,降低了性能,也增加了链路上的风险和运维的复杂度。
可是,如果流量不经过这些设施,它们相应的职责就无法发挥作用了:如果不经过负载均衡器的话,甚至连请求应该具体交给哪一个服务去处理都无法确定。
所以既然如此,我们有什么办法可以简化这个调用过程吗?这就需要用到客户端负载均衡器了。
客户端负载均衡器的工作原理
我们知道对于任何一个大型系统来说负载均衡器都是必不可少的设施。以前负载均衡器大多只部署在整个服务集群的前端将用户的请求分流到各个服务进行处理这种经典的部署形式现在被称为集中式的负载均衡在第20讲中我已经给你介绍过这种经典的负载均衡的工作原理你可以去复习一下
而随着微服务的日渐流行,服务集群收到的请求来源就不再局限于外部了,越来越多的访问请求是由集群内部的某个服务发起,由集群内部的另一个服务进行响应的。对于这类流量的负载均衡,既有的方案依然是可行的,但针对内部流量的特点,直接在服务集群内部消化掉,肯定是更合理、更受开发者青睐的办法。
由此一种全新的、独立位于每个服务前端的、分散式的负载均衡方式正逐渐变得流行起来这就是本节课我们要讨论的主角客户端负载均衡器Client-Side Load Balancer
客户端负载均衡器的理念提出以后此前的集中式负载均衡器也有了一个方便与它对比的名字“服务端负载均衡器”Server-Side Load Balancer
从上图中,你其实能够清晰地看到这两种均衡器的关键差别所在:服务端负载均衡器是集中式的,同时为多个节点提供服务,而客户端负载均衡器是和服务实例一一对应的,而且与服务实例并存于同一个进程之内。这能为它带来很多好处,比如说:
均衡器与服务之间的信息交换是进程内的方法调用,不存在任何额外的网络开销。
客户端均衡器不依赖集群边缘的设施,所有内部流量都仅在服务集群的内部循环,避免出现前面提到的,集群内部流量要“绕场一周”的尴尬局面。
分散式的均衡器意味着它天然避免了集中式的单点问题它的带宽资源将不会像集中式均衡器那样敏感这在以七层均衡器为绝对主流、不能通过IP隧道和三角传输这样的方式来节省带宽的微服务环境中显得更具优势。
客户端均衡器要更加灵活,能够针对每一个服务实例单独设置均衡策略等参数。比如访问某个服务是否需要具备亲和性,选择服务的策略是随机、轮询、加权还是最小连接,等等,都可以单独设置而不影响其他服务。
……
但是你也要清楚,客户端均衡器并不是银弹,它的缺点同样是不少的:
它与服务运行于同一个进程之内就意味着它的选型要受到服务所使用的编程语言的限制。比如用Golang开发的微服务就不太可能搭配Spring Cloud Load Balancer来一起使用因为要为每种语言都实现对应的能够支持复杂网络情况的均衡器是非常难的。客户端均衡器的这个缺陷其实有违于微服务中技术异构不应受到限制的原则。
从个体服务来看由于客户端均衡器与服务实例是共用一个进程均衡器的稳定性会直接影响整个服务进程的稳定性而消耗的CPU、内存等资源也同样会影响到服务的可用资源。从集群整体来看在服务数量达到成千乃至上万的规模时客户端均衡器消耗的资源总量是相当可观的。
由于请求的来源可能是来自集群中任意一个服务节点,而不再是统一来自集中式均衡器,这就会导致内部网络安全和信任关系变得复杂,当入侵者攻破任何一个服务时,都会更容易通过该服务突破集群中的其他部分。
我们知道,服务集群的拓扑关系是动态的,每一个客户端均衡器必须持续跟踪其他服务的健康状况,以实现上线新服务、下线旧服务、自动剔除失败的服务、自动重连恢复的服务等均衡器必须具备的功能。由于这些操作都需要通过访问服务注册中心来完成,因此数量庞大的客户端均衡器需要一直持续轮询服务注册中心,这也会为它带来不小的负担。
……
代理客户端负载均衡器
在Java领域客户端负载均衡器中最具代表性的产品就是Netflix Ribbon和Spring Cloud Load Balancer了随着微服务的流行它们在Java微服务中已经积聚了相当可观的使用者。
而到了最近两三年服务网格Service Mesh开始盛行另一种被称为“代理客户端负载均衡器”Proxy Client-Side Load Balancer后面就简称“代理均衡器”的客户端均衡器变体形式开始引起不同编程语言的微服务开发者的共同关注因为它解决了此前客户端均衡器的大部分缺陷。
代理均衡器对此前的客户端负载均衡器的改进,其实就是将原本嵌入在服务进程中的均衡器提取出来,放到边车代理中去实现,它的流量关系如下图所示:
这里你可以发现虽然代理均衡器与服务实例不再是进程内通讯而是通过虚拟化网络进行数据交换的数据要经过操作系统的协议栈要进行打包拆包、计算校验和、维护序列号等网络数据的收发等步骤Envoy中支持使用Unix Domain Socket来进一步避免这种消耗流量比起之前的客户端均衡器确实多经历了一次代理过程。
不过Kubernetes严格保证了同一个Pod中的容器不会跨越不同的节点相同Pod中的容器共享同一个网络和Linux UTS名称空间因此代理均衡器与服务实例的交互仍然要比真正的网络交互高效且稳定得多代价很小。但它从服务进程中分离出来的收益则是非常明显的
代理均衡器不再受编程语言的限制。比如说要发展一个支持Java、Golang、Python等所有微服务应用的通用代理均衡器就具有很高的性价比。集中不同编程语言的使用者的力量也更容易打造出能面对复杂网络情况的、高效健壮的均衡器。即使退一步说独立于服务进程的均衡器也不会因为自身的稳定性而影响到服务进程的稳定。
在服务拓扑感知方面,代理均衡器也要更有优势。由于边车代理接受控制平面的统一管理,当服务节点拓扑关系发生变化时,控制平面就会主动向边车代理发送更新服务清单的控制指令,这避免了此前客户端均衡器必须长期主动轮询服务注册中心所造成的浪费。
在安全性、可观测性上由于边车代理都是一致的实现有利于在服务间建立双向TLS通讯也有利于对整个调用链路给出更详细的统计信息。
……
所以总体而言边车代理这种通过同一个Pod的独立容器实现的负载均衡器就是目前处理微服务集群内部流量最理想的方式。只是服务网格本身仍然是初生事物它还不够成熟对程序员的操作系统、网络和运维方面的知识要求也比较高但我们有理由相信随着时间的推移未来服务网格将会是微服务的主流通讯方式。
地域与区域
OK最后我想再和你探讨一个与负载均衡相关但又不仅仅只涉及到负载均衡的概念地域与区域。
不知道你有没有注意到在与微服务相关的许多设施中都带有Region、Zone参数比如前面我提到过的服务注册中心Eureka的Region、Zone边车代理Envoy中的Region、Zone、Sub-zone等等。如果你有云计算IaaS的使用经历你也会发现几乎所有的云计算设备都有类似的概念。
实际上Region和Zone是公有云计算先驱亚马逊AWS提出的概念我们分别来看看它们的含义。
Region是地域的意思比如华北、东北、华东、华南这些都是地域范围。
面向全球或全国的大型系统的服务集群,往往都会部署在多个不同的地域,就像是这节课开篇我设计的那个案例场景一样,它就是通过不同地域的机房,来缩短用户与服务器之间的物理距离,提升响应速度的;而对于小型系统来说,地域一般就只在异地容灾时才会涉及到。
这里你需要注意的是,不同的地域之间是没有内网连接的,所有流量都只能经过公众互联网相连,如果微服务的流量跨越了地域,实际上就跟调用外部服务商提供的互联网服务没有任何差别了。所以,集群内部流量是不会跨地域的,服务发现、负载均衡器也默认是不会支持跨地域的服务发现和负载均衡。
Zone是区域的意思它是可用区域Availability Zones的简称。区域的意思是在地理上位于同一地域内但电力和网络是互相独立的物理区域比如在华东的上海、杭州、苏州的不同机房就是同一个地域的几个可用区域。
同一个地域的区域之间具有内网连接,流量不占用公网带宽,因此区域是微服务集群内,流量能够触及的最大范围。但是,你的应用是只部署在同一区域内,还是部署到几个可用区域中,主要取决于你是否有做异地双活的需求,以及对网络延时的容忍程度。
如果你追求高可用,比如希望系统即使在某个地区发生电力或者骨干网络中断时,仍然可用,那你可以考虑将系统部署在多个区域中。这里你要注意异地容灾和异地双活的差别:容灾是非实时的同步,而双活是实时或者准实时的,跨地域或者跨区域做容灾都可以,但只能一般只能跨区域做双活,当然你也可以将它们结合起来同时使用,即“两地三中心”模式。
如果你追求低延迟比如对时间有高要求的SLA应用或者网络游戏服务器等那就应该考虑将系统部署在同一个区域中。因为尽管内网连接不受限于公网带宽但毕竟机房之间的专线容量也是有限的难以跟机房内部的交换机相比延时也会受物理距离、网络跃点等因素的影响。
当然可用区域对应于城市级别的区域范围在一些场景中仍然是过大了一些即使是同一个区域中的机房也可能存在不同的具有差异的子网络所以部分的微服务框架也提供了Group、Sub-zone等做进一步的细分控制。这些参数的意思通常是加权或优先访问同一个子区域的服务但如果子区域中没有合适的还是会访问到可用区域中的其他服务。
另外地域和区域原本是云计算中的概念对于一些中小型的微服务系统尤其是非互联网的企业信息系统来说很多仍然没有使用云计算设施只部署在某个专有机房内部只为特定人群提供服务这就不需要涉及地理上的地域、区域的概念了。这时你完全可以自己灵活延伸拓展Region、Zone参数的含义达到优化虚拟化基础设施流量的目的。
比如说将框架的这类设置与Kubernetes的标签、选择器相配合实现内部服务请求其他服务时优先使用同一个Node中提供的服务进行应答以降低真实的网络消耗。
小结
这节课,我们从“如何将流量转发到最恰当的服务”开始,讨论了客户端负载均衡器出现的背景、它与服务端负载均衡器之间的差异,以及通过代理来实现客户端负载均衡器的差别。
最后,借助本节课建立的上下文场景,我还给你介绍了地域与可用区域的概念。这些概念不仅在购买、使用云计算服务时会用到,还直接影响到了应用程序中路由、负载均衡的请求分发形式。
一课一思
你认为“负载均衡”这件事情,是负责设计程序的架构师、负责实现程序的开发者,还是负责部署程序的运维人员为主去考虑的呢?
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,112 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
36 _ 面对程序故障,我们该做些什么?
你好,我是周志明。接下来的两节课,我们一起来学习服务的容错性设计这个话题。
“容错性设计”Design for Failure是微服务的另一个核心原则也是我在这门课中反复强调的开发观念的转变。
不过,虽然已经有了一定的心理准备,但在首次将微服务架构引入实际生产系统时,在服务发现、网关路由等支持下,踏出了服务化的第一步以后,我们还是很可能会经历一段阵痛期。随着拆分出的服务越来越多,随之而来的,我们也会面临以下两个问题的困扰:
某一个服务的崩溃,会导致所有用到这个服务的其他服务都无法正常工作,一个点的错误经过层层传递,最终波及到调用链上与此有关的所有服务,这便是雪崩效应。如何防止雪崩效应,便是微服务架构容错性设计原则的具体实践,否则服务化程度越高,整个系统反而越不稳定。
服务虽然没有崩溃,但由于处理能力有限,面临超过预期的突发请求时,大部分请求直至超时都无法完成处理。这种现象产生的后果跟交通堵塞是类似的,如果一开始没有得到及时地治理,后面就会需要很长时间才能使全部服务都恢复正常。
这两个问题,就是“流量治理”这个话题要解决的了。在这个小章节,我们将围绕着如何解决这两个问题,提出服务容错、流量控制、服务质量管理等一系列解决方案。
当然了,这些措施并不是孤立的,它们相互之间存在很多联系,其中的许多功能还必须与咱们之前学习过的服务注册中心、服务网关、负载均衡器配合才能实现。理清楚了这些技术措施背后的逻辑链条,我们就找到了理解它们工作原理的捷径。
接下来,我们就从服务容错这个解决方案学起吧。
服务容错
Martin Fowler与James Lewis提出的“微服务的九个核心特征”是构建微服务系统的指导性原则但不是技术规范并没有严格的约束力。在实际构建系统时候其中多数特征可能会有或多或少的妥协比如分散治理、数据去中心化、轻量级通讯机制、演进式设计等等。但也有一些特征是无法做出妥协的其中典型的就是今天我们讨论的主题容错性设计。
容错性设计不能妥协的原因在于,分布式系统的本质是不可靠的,一个大的服务集群中,程序可能崩溃、节点可能宕机、网络可能中断,这些“意外情况”其实全部都在“意料之中”。原本信息系统设计成分布式架构的主要动力之一,就是提升系统的可用性,最低限度也必须保证将原有系统重构为分布式架构之后,可用性不出现倒退才行。
如果说,服务集群中出现任何一点差错都能让系统面临“千里之堤溃于蚁穴”的风险,那么分布式恐怕就根本没有机会成为一种可用的系统架构形式了。
容错策略
那在实践中怎么落实容错性设计这条原则呢?除了思想观念上转变过来,正视程序必然是会出错的,对它进行有计划的防御之外,我们还必须了解一些常用的容错策略和容错设计模式,来指导具体的设计与编码实践。
那怎么理解容错策略和容错设计模式呢?其实,容错策略,指的是“面对故障,我们该做些什么”;而容错设计模式,指的是“要实现某种容错策略,我们该如何去做”。
所以接下来我们先一起学习7种常见的容错策略包括故障转移、快速失败、安全失败、沉默失败、故障恢复、并行调用和广播调用然后下一讲我们再学习几种被实践证明有效的服务容错设计模式。
第一种容错策略是故障转移Failover
高可用的服务集群中,多数的服务,尤其是那些经常被其他服务依赖的关键路径上的服务,都会部署多个副本。这些副本可能部署在不同的节点(避免节点宕机)、不同的网络交换机(避免网络分区),甚至是不同的可用区(避免整个地区发生灾害或电力、骨干网故障)中。
故障转移是指,如果调用的服务器出现故障,系统不会立即向调用者返回失败结果,而是自动切换到其他服务副本,尝试其他副本能否返回成功调用的结果,从而保证了整体的高可用性。
故障转移的容错策略应该有一定的调用次数限制,比如允许最多重试三个服务,如果都发生报错,那还是会返回调用失败。引入调用次数的限制,不仅是因为重试有执行成本,更是因为过度的重试反而可能让系统处于更加不利的状况。
我们看一个例子。现在有Service A → Service B → Service C这么一条调用链。假设A的超时阈值为100毫秒而B调用C需要60毫秒然后不幸失败了这时候做故障转移其实已经没有太大意义了。因为即使下一次调用能够返回正确结果也很可能同样需要耗费60毫秒的时间时间总和就已经超过了Service A的超时阈值。所以在这种情况下故障转移反而对系统是不利的。
第二种容错策略是快速失败Failfast
有一些业务场景是不允许做故障转移的,因为故障转移策略能够实施的前提,是服务具有幂等性。那对于非幂等的服务,重复调用就可能产生脏数据,引起的麻烦远大于单纯的某次服务调用失败。这时候,就应该把快速失败作为首选的容错策略。
比如,在支付场景中,需要调用银行的扣款接口,如果该接口返回的结果是网络异常,那程序很难判断到底是扣款指令发送给银行时出现的网络异常,还是银行扣款后给服务返回结果时出现的网络异常。为了避免重复扣款,此时最恰当的方案就是尽快让服务报错并抛出异常,坚决避免重试,由调用者自行处理。
第三种容错策略是安全失败Failsafe
在一个调用链路中的服务通常也有主路和旁路之分并不见得每个服务都是不可或缺的属于旁路逻辑的一个显著特点是服务失败了也不影响核心业务的正确性。比如开发基于Spring管理的应用程序时通过扩展点、事件或者AOP注入的逻辑往往就属于旁路逻辑典型的有审计、日志、调试信息等等。
属于旁路逻辑的另一个显著特征是,后续处理不会依赖其返回值,或者它的返回值是什么都不会影响后续处理的结果。比如只是将返回值记录到数据库,并不使用它参与最终结果的运算。
对这类逻辑,一种理想的容错策略是,即使旁路逻辑调用失败了,也当作正确来返回,如果需要返回值的话,系统就自动返回一个符合要求的数据类型的对应零值,然后自动记录一条服务调用出错的日志备查即可。这种容错策略,被称为安全失败。
第四种容错策略是沉默失败Failsilent
如果大量的请求需要等到超时(或者长时间处理后)才宣告失败,很容易因为某个远程服务的请求堆积而消耗大量的线程、内存、网络等资源,进而影响到整个系统的稳定性。
面对这种情况,一种合理的失败策略是当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,并将错误隔离开来,避免对系统其他部分产生影响。这种容错策略,就被称为沉默失败。
第五种容错策略是故障恢复Failback
故障恢复一般不单独存在,而是作为其他容错策略的补充措施。在微服务管理框架中,如果设置容错策略为故障恢复的话,通常默认会采用快速失败加上故障恢复的策略组合。
故障恢复是指,当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。
从这个定义中也可以看出,故障恢复策略,一方面是尽力促使失败的调用最终能够被正常执行,另一方面也可以为服务注册中心和负载均衡器及时提供服务恢复的通知信息。很显然,故障恢复也要求服务必须具备幂等性,由于它的重试是后台异步进行,即使最后调用成功了,原来的请求也早已经响应完毕。所以,故障恢复策略一般用于对实时性要求不高的主路逻辑,也适合处理那些不需要返回值的旁路逻辑。
为了避免在内存中的异步调用任务堆积,故障恢复与故障转移一样,也应该有最大重试次数的限制。
故障转移、快速失败、安全失败、沉默失败和故障恢复这5种容错策略的英文都是以“Fail”开头的它们有一个共同点都是针对调用失败时如何进行弥补的。接下来咱们要学习的并行调用和广播调用这两种策略则是在调用之前就开始考虑如何获得最大的成功概率。
第六种容错策略是并行调用Forking
并行调用策略,是指一开始就同时向多个服务副本发起调用,只要有其中任何一个返回成功,那调用便宣告成功。这种策略是在一些关键场景中,使用更高的执行成本换取执行时间和成功概率的策略。
这种处理思路,其实对应的就是,咱们在日常生活中,对一些重要环节采取的“双重保险”或者“多重保险”的处理思路。
第七种容错策略是广播调用Broadcast
广播调用与并行调用是相对应的,都是同时发起多个调用,但并行调用是任何一个调用结果返回成功便宣告成功,而广播调用则是要求所有的请求全部都成功,才算是成功。
也就是说,对于广播调用来说,任何一个服务提供者出现异常都算调用失败。因此,广播调用通常被用于实现“刷新分布式缓存”这类的操作。
小结
今天这一讲我们学习了故障转移、快速失败、安全失败、沉默失败、故障恢复、并行调用和广播调用一共7种容错策略。
其实,容错策略并不是计算机领域独有的,在交通、能源、航天等非常多的领域都有容错性设计,也会使用到上面这些策略,并在自己的行业领域中进行解读与延伸。
这里我把今天讲到的7种容错策略进行了一次对比梳理你可以在下面的表格中看到它们的优缺点和应用场景。
一课一思
你在实际工作中使用过哪种容错策略,是要用它来解决什么问题的呢?你能试着说说它是如何实现的吗?
欢迎在留言区分享你的答案。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,179 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
37 _ 要实现某种容错策略,我们该怎么做?
你好,我是周志明。今天,我们继续学习服务容错的实现方法。
在上一讲,我们首先界定了容错策略和容错设计模式这两个概念:容错策略,指的是“面对故障,我们该做些什么”;而容错设计模式,指的是“要实现某种容错策略,我们该如何去做”。
然后我们讲了7种常见的容错策略包括故障转移、快速失败、安全失败、沉默失败、故障恢复、并行调用和广播调用。
那么为了实现各种各样的容错策略,开发人员总结出了一些被实践证明有效的服务容错设计模式。这些设计模式,包括了这一讲我们要学习的,微服务中常见的断路器模式、舱壁隔离模式和超时重试模式等,以及我们下一讲要学习的流量控制模式,比如滑动时间窗模式、漏桶模式、令牌桶模式,等等。
我们先来学习断路器模式。
断路器模式
断路器模式是微服务架构中最基础的容错设计模式以至于像Hystrix这种服务治理工具我们往往会忽略了它的服务隔离、请求合并、请求缓存等其他服务治理职能直接把它叫做微服务断路器或者熔断器。这下你明白断路器模式为啥是“最基础”的容错设计模式了吧也明白为啥我们要首先学习这种模式了吧。
断路器模式最开始是由迈克尔 · 尼加德Michael Nygard在“Release It!”这本书中提出来的,后来又因为马丁 · 福勒Martin Fowler的文章“Circuit Breaker”而广为人知。
其实,断路器的思路很简单,就是通过代理(断路器对象)来一对一(一个远程服务对应一个断路器对象)地接管服务调用者的远程请求。那怎么实现的呢?
断路器会持续监控并统计服务返回的成功、失败、超时、拒绝等各种结果当出现故障失败、超时、拒绝的次数达到断路器的阈值时它的状态就自动变为“OPEN”。之后这个断路器代理的远程访问都将直接返回调用失败而不会发出真正的远程服务请求。
通过断路器对远程服务进行熔断,就可以避免因为持续的失败或拒绝而消耗资源,因为持续的超时而堆积请求,最终可以避免雪崩效应的出现。由此可见,断路器本质上是快速失败策略的一种实现方式。
断路器模式的工作过程,可以用下面的序列图来表示:
从调用序列来看,断路器就是一种有限状态机,断路器模式就是根据自身的状态变化,自动调整代理请求策略的过程。
断路器一般可以设置为CLOSED、OPEN和HALF OPEN三种状态。
CLOSED表示断路器关闭此时的远程请求会真正发送给服务提供者。断路器刚刚建立时默认处于这种状态此后将持续监视远程请求的数量和执行结果决定是否要进入OPEN状态。
OPEN表示断路器开启此时不会进行远程请求直接给服务调用者返回调用失败的信息以实现快速失败策略。
HALF OPEN是一种中间状态。断路器必须带有自动的故障恢复能力当进入OPEN状态一段时间以后将“自动”一般是由下一次请求而不是计时器触发的所以这里的自动是带引号的切换到HALF OPEN状态。在中间状态下会放行一次远程调用然后根据这次调用的结果成功与否转换为CLOSED或者OPEN状态来实现断路器的弹性恢复。
CLOSED、OPEN和HALF OPEN这三种状态的转换逻辑和条件如下图所示
断路器的状态转换逻辑图片引自“Application Resiliency Using Netflix Hystrix”
OPEN和CLOSED状态的含义是十分清晰的和我们日常生活中电路的断路器并没有什么差别值得讨论的是这两种状态的转换条件是什么
最简单直接的方案是只要遇到一次调用失败那就默认以后所有的调用都会接着失败断路器直接进入OPEN状态。但这样做的效果非常差虽然避免了故障扩散和请求堆积却使得在外部看来系统表现的极其不稳定。
那怎么解决这个问题呢一个可行的办法是当一次调用失败后如果还同时满足下面两个条件断路器的状态就变为OPEN
一段时间比如10秒以内请求数量达到一定阈值比如20个请求。这个条件的意思是如果请求本身就很少那就用不着断路器介入。
一段时间比如10秒以内请求的故障率发生失败、超时、拒绝的统计比例到达一定阈值比如50%)。这个条件的意思是,如果请求本身都能正确返回,也用不着断路器介入。
括号中举例的数值10秒、20个请求、50%是Netflix Hystrix的默认值。其他服务治理的工具比如Resilience4j、Envoy等也有类似的设置你可以在它们的帮助文档中找到对应的默认值。
借着断路器的上下文,我再顺带讲一下服务治理中两个常见的易混淆概念:服务熔断和服务降级之间的联系与差别。
断路器做的事情是自动进行服务熔断,属于一种快速失败的容错策略的实现方法。在快速失败策略明确反馈了故障信息给上游服务以后,上游服务必须能够主动处理调用失败的后果,而不是坐视故障扩散。这里的“处理”,指的就是一种典型的服务降级逻辑,降级逻辑可以是,但不应该只是,把异常信息抛到用户界面去,而应该尽力想办法通过其他路径解决问题,比如把原本要处理的业务记录下来,留待以后重新处理是最低限度的通用降级逻辑。
举个例子:你女朋友有事儿想召唤你,打你手机没人接,响了几声气冲冲地挂断后(快速失败),又打了你另外三个不同朋友的手机号(故障转移),都还是没能找到你(重试超过阈值)。这时候她生气地在微信上给你留言“三分钟不回电话就分手”,以此来与你取得联系。在这个不是太吉利的故事里,女朋友给你留言这个行为便是服务降级逻辑。
服务降级不一定是在出现错误后才被动执行的,我们在很多场景中谈论的降级更可能是指,需要主动迫使服务进入降级逻辑的情况。比如,出于应对可预见的峰值流量,或者是系统检修等原因,要关闭系统部分功能或关闭部分旁路服务,这时候就有可能会主动迫使这些服务降级。当然,此时服务降级就不一定是出于服务容错的目的了,更可能是属于下一讲我们要学习的流量控制的范畴。
舱壁隔离模式
了解了服务熔断和服务降级以后,我们再来看看微服务治理中常听见的另一概念:服务隔离。
舱壁隔离模式,是常用的实现服务隔离的设计模式。“舱壁”这个词来自造船业,它原本的意思是设计舰船时,要在每个区域设计独立的水密舱室,一旦某个舱室进水,也只会影响到这个舱室中的货物,而不至于让整艘舰艇沉没。
那对应到分布式系统中,服务隔离,就是避免某一个远程服务的局部失败影响到全局,而设置的一种止损方案。这种思想,对应的就是容错策略中的失败静默策略。那为什么会有一个服务失败会影响全局的事情发生呢?
咱们在刚刚学习断路器模式时,把调用外部服务的故障分为了失败、拒绝和超时三大类。
其中“超时”引起的故障尤其容易给调用者带来全局性的风险。这是因为目前主流的网络访问大多是基于TPR并发模型Thread per Request来实现的只要请求一直不结束无论是以成功结束还是以失败结束就要一直占用着某个线程不能释放。而线程是典型的整个系统的全局性资源尤其是在Java这类将线程映射为操作系统内核线程来实现的语言环境中。
我们来看一个更具体的场景。
当分布式系统依赖的某个服务比如“服务I”发生了超时那在高流量的访问下或者更具体点假设平均1秒钟内会调用这个服务50次就意味着该服务如果长时间不结束的话每秒会有50条用户线程被阻塞。
如果这样的访问量一直持续按照Tomcat默认的HTTP超时时间20秒来计算的话20秒内将会阻塞掉1000条用户线程。此后才陆续会有用户线程因超时被释放出来回归Tomcat的全局线程池中。
通常情况下Java应用的线程池最大只会设置为200~400这就意味着从外部来看此时系统的所有服务已经全面瘫痪而不仅仅是只有涉及到“服务I”的功能不可用。因为Tomcat已经没有任何空余的线程来为其他请求提供服务了。
由于某个外部服务导致的阻塞图片来自Hystrix使用文档
要解决这类问题,本质上就是要控制单个服务的最大连接数。一种可行的解决办法是为每个服务单独设立线程池,这些线程池默认不预置活动线程,只用来控制单个服务的最大连接数。
比如对出问题的“服务I”设置了一个最大线程数为5的线程池这时候它的超时故障就只会最多阻塞5条用户线程而不至于影响全局。此时其他不依赖“服务I”的用户线程依然能够正常对外提供服务如下图所示。
通过线程池将阻塞限制在一定范围内图片来自Hystrix使用文档
使用局部的线程池来控制服务的最大连接数有很多好处比如当服务出问题时能够隔离影响当服务恢复后还可以通过清理掉局部线程池瞬间恢复该服务的调用。而如果是Tomcat的全局线程池被占满再恢复就会非常麻烦。
但是局部线程池有一个显著的弱点那就是它额外增加了CPU的开销每个独立的线程池都要进行排队、调度和下文切换工作。根据Netflix官方给出的数据一旦启用Hystrix线程池来进行服务隔离每次服务调用大概会增加3~10毫秒的延时。如果调用链中有20次远程服务调用的话那每次请求就要多付出60毫秒至200毫秒的代价来换取服务隔离的安全保障。
为应对这种情况还有一种更轻量的控制服务最大连接数的办法那就是信号量机制Semaphore
如果不考虑清理线程池、客户端主动中断线程这些额外的功能,仅仅是为了控制单个服务并发调用的最大次数的话,我们可以只为每个远程服务维护一个线程安全的计数器,并不需要建立局部线程池。
具体做法是当服务开始调用时计数器加1服务返回结果后计数器减1一旦计数器的值超过设置的阈值就立即开始限流在回落到阈值范围之前都不再允许请求了。因为不需要承担线程的排队、调度和切换工作所以单纯维护一个作为计数器的信号量的性能损耗相对于局部线程池来说几乎可以忽略不计。
以上介绍的是从微观的、服务调用的角度应用舱壁隔离设计模式实际上舱壁隔离模式还可以在更高层、更宏观的场景中使用不按调用线程而是按功能、按子系统、按用户类型等条件来隔离资源都是可以的。比如根据用户等级、用户是否是VIP、用户来访的地域等各种因素将请求分流到独立的服务实例去这样即使某一个实例完全崩溃了也只是影响到其中某一部分的用户把波及范围尽可能控制住。
一般来说我们会选择将服务层面的隔离实现在服务调用端或者边车代理上将系统层面的隔离实现在DNS或者网关处。
到这里,我们回顾下已经学习了哪几种安全策略的实现方式:
使用断路器模式实现快速失败策略;
使用舱壁隔离模式实现静默失败策略;
在断路器中的案例中提到的主动对非关键的旁路服务进行降级,也可以算作是安全失败策略的一种体现。
再对照着我们上一讲学习的7种常见安全策略来说我们还剩下故障转移和故障恢复两种策略的实现没有学习。接下来我就以重试模式和你介绍这两种容错策略的主流实现方案。
重试模式
故障转移和故障恢复这两种策略都需要对服务进行重复调用,差别就在于这些重复调用有可能是同步的,也可能是后台异步进行;有可能会重复调用同一个服务,也可能会调用服务的其他副本。但是,无论具体是通过怎样的方式调用、调用的服务实例是否相同,都可以归结为重试设计模式的应用范畴。
重试模式适合解决系统中的瞬时故障简单地说就是有可能自己恢复Resilient称为自愈也叫做回弹性的临时性失灵比如网络抖动、服务的临时过载比如返回了503 Bad Gateway错误这些都属于瞬时故障。
重试模式实现起来并不难即使完全不考虑框架的支持程序员自己编写十几行代码也能完成。也正因为实现起来简单使用重试模式面临的最大风险就是滥用。那怎么避免滥用呢在实践中我们判断是否应该且是否能够对一个服务进行重试时要看是否同时满足下面4个条件。
第一,仅在主路逻辑的关键服务上进行同步的重试。也就是说,如果不是关键的服务,一般不要把重试作为首选的容错方案,尤其不应该进行同步重试。
第二仅对由瞬时故障导致的失败进行重试。尽管要做到精确判定一个故障是否属于可自愈的瞬时故障并不容易但我们至少可以从HTTP的状态码上获得一些初步的结论。比如当发出的请求收到了401 Unauthorized响应时说明服务本身是可用的只是你没有权限调用这时候再去重试就没有什么意义。
功能完善的服务治理工具会提供具体的重试策略配置如Envoy的Retry Policy可以根据包括HTTP响应码在内的各种具体条件来设置不同的重试参数。
第三,仅对具备幂等性的服务进行重试。你可能会说了,如果服务调用者和提供者不属于同一个团队,那服务是否幂等,其实也是一个难以精确判断的问题。这个问题确实存在,但我们还是可以找到一些总体上通用的原则来帮助我们做判断。
比如RESTful服务中的POST请求是非幂等的GET、HEAD、OPTIONS、TRACE请求应该被设计成幂等的因为它们不会改变资源状态PUT请求一般也是幂等的因为n个PUT请求会覆盖相同的资源n-1次DELETE请求也可看作是幂等的同一个资源首次删除会得到200 OK响应此后应该得到204 No Content响应。
这些都是HTTP协议中定义的通用的指导原则虽然对于具体服务如何实现并无强制约束力但建设系统时遵循业界惯例本身就是一种良好的习惯。
第四,重试必须有明确的终止条件,常用的终止条件有超时终止和次数终止两种。
超时终止。其实,超时机制并不限于重试策略,所有涉及远程调用的服务都应该有超时机制来避免无限期的等待。这里我只是强调重试模式更应该配合超时机制来使用,否则重试对系统很可能是有害的。在介绍故障转移策略时,我已经举过类似的例子,你可以再去看一下。
次数终止。重试必须要有一定限度不能无限制地做下去通常是重试2~5次。因为重试不仅会给调用者带来负担对服务提供者来说也同样是负担所以我们要避免把重试次数设得太大。此外如果服务提供者返回的响应头中带有Retry-After的话尽管它没有强制约束力我们也应该充分尊重服务端的要求做个“有礼貌”的调用者。
另外,由于重试模式可以在网络链路的多个环节中去实现,比如在客户端发起调用时自动重试、网关中自动重试、负载均衡器中自动重试,等等,而且现在的微服务框架都足够便捷,只需设置一两个开关参数,就可以开启对某个服务、甚至是全部服务的重试机制了。
所以,如果你没有太多经验的话,可能根本就意识不到其中会带来多大的负担。
这里我给你举个具体的例子一套基于Netflix OSS建设的微服务系统如果同时在Zuul、Feign和Ribbon上都打开了重试功能且不考虑重试被超时终止的话那总重试次数就相当于它们的重试次数的乘积。假设按它们都重试4次且Ribbon可以转移4个服务副本来计算的话理论上最多会产生高达4×4×4×4=256次调用请求。
小结
熔断、隔离、重试、降级、超时等概念,都是建立具有韧性的微服务系统的必须的保障措施。那么就目前来说,这些措施的正确运作,主要还是依靠开发人员对服务逻辑的了解,以及根据运维人员的经验去静态地调整、配置参数和阈值。
但是面对能够自动扩缩Auto Scale的大型分布式系统静态的配置越来越难以起到良好的效果。
所以这就要求,系统不仅要有能力可以自动地根据服务负载来调整服务器的数量规模,同时还要有能力根据服务调用的统计结果,或者启发式搜索的结果来自动变更容错策略和参数。当然,目前这方面的研究,还处于各大厂商在内部分头摸索的初级阶段,不过这正是服务治理未来的重要发展方向之一。
这节课我给你介绍的容错策略和容错设计模式,最终目的都是为了避免服务集群中,某个节点的故障导致整个系统发生雪崩效应。
但我们要知道,仅仅做到容错,只让故障不扩散是远远不够的,我们还希望系统或者至少系统的核心功能能够表现出最佳的响应的能力,不受或少受硬件资源、网络带宽和系统中一两个缓慢服务的拖累。在下一节课,我们还将会面向如何解决集群中的短板效应,去讨论服务质量、流量管控等话题。
一课一思
服务容错一般由底层的服务治理框架来负责实现,你使用过哪些服务治理框架呢?能对比一下这些框架的优缺点吗?
欢迎在留言区分享你的答案。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,247 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 _ 限流的目标与模式
你好,我是周志明。
在前面两讲中,我们了解了分布式服务中的容错机制,这是分布式服务调用中必须考虑的因素。今天这节课,我们接着来学习分布式服务中另一个常见的机制:限流。
任何一个系统的运算、存储、网络资源都不是无限的,当系统资源不足以支撑外部超过预期的突发流量时,就应该要有取舍,建立面对超额流量自我保护的机制,而这个机制就是微服务中常说的“限流”。
限流的目标
在介绍限流具体的实现细节之前,我们先来做一道小学三年级难度的四则运算场景应用题:
已知条件:-
系统中一个业务操作需要调用10个服务协作来完成该业务操作的总超时时间是10秒每个服务的处理时间平均是0.5秒集群中每个服务均部署了20个实例副本。-
求解以下问题:-
(1)单个用户访问,完成一次业务操作,需要耗费系统多少处理器时间?-
答0.5 × 10 = 5 Sec CPU Time-
(2)集群中每个服务每秒最大能处理多少个请求?-
答:(1 ÷ 0.5) × 20 = 40 QPS-
(3)假设不考虑顺序且请求分发是均衡的,在保证不超时的前提下,整个集群能持续承受最多每秒多少笔业务操作?-
答40 × 10 ÷ 5 = 80 TPS-
(4)如果集群在一段时间内持续收到100 TPS的业务请求会出现什么情况-
答:这就超纲了小学水平,得看你们家架构师的本事了。
前三个问题都很基础我就不说了对于最后一个问题如果仍然按照小学生的解题思路最大处理能力为80 TPS的系统遇到100 TPS的请求应该能完成其中的80 TPS也就是只有20 TPS的请求失败或被拒绝才对。然而这其实是最理想的情况也是我们追求的目标。
事实上如果不做任何处理的话更可能出现的结果是这100个请求中的每一个都开始了处理但是大部分请求都只完成了10次服务调用中的8次或者9次然后就是超时没有然后了。
其实在很多的微服务应用中多数服务调用都是白白浪费掉的没有几个请求能够走完整笔业务操作。比如早期的12306系统就明显存在这样的问题全国人民都上去抢票的结果就是全国人民谁都买不上票。
那么,为了避免这种状况出现,一个健壮的系统就需要做到恰当的流量控制,更具体地说,需要妥善解决以下三个问题:
依据什么限流?
要不要控制流量、要控制哪些流量、控制力度要有多大,等等,这些操作都没法在系统设计阶段静态地给出确定的结论,必须根据系统此前一段时间的运行状况,甚至未来一段时间的预测情况来动态决定。
具体如何限流?
要想解决系统具体是如何做到允许一部分请求能够通行,而另外一部分流量实行受控制的失败降级的问题,就必须要了解和掌握常用的服务限流算法和设计模式。
超额流量如何处理?
超额流量可以有不同的处理策略也许会直接返回失败如429 Too Many Requests或者被迫使它们进入降级逻辑这种策略被称为否决式限流也可能是让请求排队等待暂时阻塞一段时间后继续处理这种则被称为阻塞式限流。
流量统计指标
那么,要做好流量控制,首先就要弄清楚到底哪些指标能反映系统的流量压力大小。
相比较而言,容错的统计指标是明确的,容错的触发条件基本上只取决于请求的故障率,发生失败、拒绝与超时都算作故障。但限流的统计指标就不那么明确了,所以这里我们要先搞明白一个问题:限流中的“流”到底指什么呢?
要解答这个问题,我们得先梳理清楚经常用于衡量服务流量压力,但又比较容易混淆的三个指标的定义:
每秒事务数Transactions per SecondTPS
TPS是衡量信息系统吞吐量的最终标准。“事务”可以理解为一个逻辑上具备原子性的业务操作。比如你在Fenixs Bookstore买了一本书要进行支付这个“支付”就是一笔业务操作无论支付成功还是不成功这个操作在逻辑上就是原子的即逻辑上不可能让你买本书可以成功支付前面200页但失败了后面300页。
每秒请求数Hits per SecondHPS
HPS是指每秒从客户端发向服务端的请求数这里你要把Hits理解为Requests而不是Clicks国内某些翻译把它理解为“每秒点击数”多少有点望文生义的嫌疑。如果只要一个请求就能完成一笔业务那HPS与TPS是等价的但在一些场景里尤其常见于网页中一笔业务可能需要多次请求才能完成。
比如你在Fenixs Bookstore买了一本书要进行支付尽管在操作逻辑上它是原子的但在技术实现上除非你是直接在银行开的商城中购物能够直接扣款否则这个操作就很难在一次请求里完成总要经过显示支付二维码、扫码付款、校验支付是否成功等过程中间不可避免地会发生多次请求。
每秒查询数Queries per SecondQPS
QPS是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求那QPS和HPS是等价的但在分布式系统中一个请求的响应往往要由后台多个服务节点共同协作来完成。
比如你在Fenixs Bookstore买了一本书要进行支付在扫描支付二维码时尽管客户端只发送了一个请求但在这背后服务端很可能需要向仓储服务确认库存信息避免超卖、向支付服务发送指令划转货款、向用户服务修改用户的购物积分等等这里面每次的内部访问都要消耗掉一次或多次查询数。
总体来说以上这三点都是基于调用计数的指标而在整体目标上我们当然最希望能够基于TPS来限流因为信息系统最终是为人类用户提供服务的用户并不关心业务到底是由多少个请求、多少个后台查询共同协作来实现的。
但是,系统的业务五花八门,不同的业务操作对系统的压力往往差异巨大,不具备可比性;而更关键的是,流量控制是针对用户实际操作场景来限流的,这不同于压力测试场景中无间隙(最多有些集合点)的全自动化操作,真实业务操作的耗时会无可避免地受限于用户交互带来的不确定性。比如前面例子中的“扫描支付二维码”这个步骤,如果用户在掏出手机扫描二维码前,先顺便回了两条短信,那整个付款操作就要持续更长时间。
那么此时如果按照业务开始时计数器加1业务结束时计数器减1通过限制最大TPS来限流的话就不能准确地反映出系统所承受的压力了。所以直接针对TPS来限流实际上是很难操作的。
目前来说主流系统大多倾向于使用HPS作为首选的限流指标因为它相对容易观察统计而且能够在一定程度上反映系统当前以及接下来一段时间的压力。
但你要知道的是限流指标并不存在任何必须遵循的权威法则根据系统的实际需要哪怕完全不选择基于调用计数的指标都是有可能的。我举个简单的例子下载、视频、直播等I/O密集型系统往往会把每次请求和响应报文的大小作为限流指标而不是调用次数。
比如说只允许单位时间通过100MB的流量再比如网络游戏等基于长连接的应用可能会把登录用户数作为限流指标热门的网游往往超过一定用户数就会让你在登录前排队等候。
限流设计模式
与容错模式类似,对于具体如何进行限流,业界内也有一些常见、常用、被实践证明有效的设计模式可以参考使用,包括流量计数器、滑动时间窗、漏桶和令牌桶这四种,下面我们一起来看看。
流量计数器模式
我们最容易想到的一种做限流的方法就是设置一个计算器根据当前时刻的流量计数结果是否超过阈值来决定是否限流。比如在前面的小学场景应用题中我们计算得出了该系统能承受的最大持续流量是80 TPS那我们就可以控制任何一秒内发现超过80次的业务请求就直接拒绝掉超额部分。
这种做法很直观,而且有些简单的限流就是这么实现的,但它并不严谨,比如说以下两个结论,就很可能出乎你对限流算法的意料之外:
即使每一秒的统计流量都没有超过80 TPS也不能说明系统没有遇到过大于80 TPS的流量压力。
你可以想像这样一个场景如果系统连续两秒都收到了60 TPS的访问请求但这两个60 TPS请求分别是前1秒里的后0.5秒以及后1秒中的前0.5秒所发生的。这样虽然每个周期的流量都不超过80 TPS请求的阈值但是系统确实是曾经在1秒内发生了超过阈值的120 TPS请求。
即使连续若干秒的统计流量都超过了80 TPS也不能说明流量压力就一定超过了系统的承受能力。
你同样可以想像这样一个场景如果10秒的时间片段中前3秒的TPS平均值到了100而后7秒的平均值是30左右此时系统是否能够处理完这些请求而不产生超时失败
答案是可以的因为条件中给出的超时时间是10秒而最慢的请求也能在8秒左右处理完毕。如果只基于固定时间周期来控制请求阈值为80 TPS反而会误杀一部分请求造成部分请求出现原本不必要的失败。
由此可见,流量计数器模式缺陷的根源在于,它只是针对时间点进行离散的统计。因此为了弥补该缺陷,一种名为“滑动时间窗”的限流模式就被设计了出来,它可以实现平滑的基于时间片段的统计。
滑动时间窗模式
滑动窗口算法Sliding Window Algorithm在计算机科学的很多领域中都有成功的应用比如编译原理中的窥孔优化Peephole Optimization、TCP协议的阻塞控制Congestion Control等都使用到了滑动窗口算法。而对分布式系统来说无论是服务容错中对服务响应结果的统计还是流量控制中对服务请求数量的统计也都经常要用到滑动窗口算法。
关于这个算法的运作过程,现在你可以充分发挥下想象力,在脑海中构造这样一个场景:在不断向前流淌的时间轴上,漂浮着一个固定大小的窗口,窗口与时间一起平滑地向前滚动。在任何时刻,我们静态地通过窗口内观察到的信息,都等价于一段长度与窗口大小相等、动态流动中的时间片段的信息。由于窗口观察的目标都是时间轴,所以它就被称为形象地称为“滑动时间窗模式”。
我再举个更具体的例子假如我们准备观察的时间片段为10秒并以1秒作为统计精度的话那可以设定一个长度为10的数组实际设计中通常是以双头队列来实现的这里简化一下和一个每秒触发1次的定时器。
现在假设我们准备通过统计结果进行限流和容错并定下限流阈值是最近10秒内收到的外部请求不要超过500个服务熔断的阈值是最近10秒内的故障率不超过50%那么在每个数组元素中下图中称为Buckets就应该存储请求的总数实际是通过明细相加得到的和其中成功、失败、超时、拒绝的明细数具体如下图所示
滑动窗口模式示意图片来自Hystrix使用文档
补充这里虽然引用了Hystrix文档的图片但Hystrix实际上是基于RxJava实现的RxJava的响应式编程思路与我下面描述的步骤差异比较大。我的本意并不是去讨论某一款流量治理工具的具体实现细节因此这里你可以将以下描述的步骤作为原理来理解。
这样当频率固定为每秒1次的定时器被唤醒时它应该完成以下几项工作这也就是滑动时间窗的工作过程
将数组最后一位的元素丢弃掉,并把所有元素都后移一位,然后在数组第一个插入一个新的空元素。这个步骤即为“滑动窗口”。
将计数器中所有统计信息写入到第一位的空元素中。
对数组中所有元素进行统计,并复位清空计数器数据供下一个统计周期使用。
所以简而言之,滑动时间窗口模式的限流完全解决了流量计数器的缺陷,它可以保证在任意时间片段内,只需经过简单的调用计数比较,就能控制住请求次数一定不会超过限流的阈值,在单机限流或者分布式服务单点网关中的限流中很常用。
不过,这种限流模式也有一些缺点,它通常只适用于否决式限流,对于超过阈值的流量就必须强制失败或降级,很难进行阻塞等待处理,也就很难在细粒度上对流量曲线进行整形,起不到削峰填谷的作用。而接下来我要介绍的这两种限流模式,就适用于阻塞式限流的处理策略。
我们先来看看漏桶模式。
漏桶模式
在计算机网络中专门有一个术语“流量整形”Traffic Shaping用来描述如何限制网络设备的流量突变使得网络报文以比较均匀的速度向外发送。流量整形通常都需要用到缓冲区来实现当报文的发送速度过快时首先在缓冲区中暂存然后在控制算法的调节下均匀地发送这些被缓冲的报文。
可以发现这里我提到了控制算法常用的控制算法有漏桶算法Leaky Bucket Algorithm和令牌桶算法Token Bucket Algorithm两种这两种算法的思路截然相反但达到的效果又是相似的。
所谓漏桶可以理解为就是你在小学做应用题时一定遇到过的那个奇怪的水池“一个水池每秒以X升速度注水同时又以Y升速度出水问水池啥时候装满”。
那么针对限流模式的话,你可以把“请求”想像成是“水”,水来了都先放进池子里,水池同时又以额定的速度出水,让请求进入系统中。这样,如果一段时间内注水过快的话,水池还能充当缓冲区,让出水口的速度不至于过快。
不过,由于请求总是有超时时间的,所以缓冲区的大小也必须是有限度的,当注水速度持续超过出水速度一段时间以后,水池终究会被灌满。此时,从网络的流量整形的角度看,就体现为部分数据包被丢弃;而从信息系统的角度看,就体现为有部分请求会遭遇失败和降级。
另外漏桶在代码实现上也非常简单它其实就是一个以请求对象作为元素的先入先出队列FIFO Queue队列长度就相当于漏桶的大小当队列已满时就拒绝新的请求进入。漏桶实现起来很容易比较困难的地方只在于如何确定漏桶的两个参数桶的大小和水的流出速率。
首先是桶的大小。如果桶设置得太大,那服务依然可能遭遇流量过大的冲击,不能完全发挥限流的作用;如果设置得太小,那很可能就会误杀掉一部分正常的请求,这种情况与流量计数器模式中举过的例子是一样的。
而流出速率在漏桶算法中一般是个固定值,这对于开篇我提到的那个场景应用题中,固定拓扑结构的服务是很合适的;但同时你也应该明白,那是经过最大限度简化的场景,现实世界里系统的处理速度,往往会受到其内部拓扑结构变化和动态伸缩的影响。所以,能够支持变动请求处理速率的令牌桶算法,可能往往会是更受我们青睐的选择。
令牌桶模式
如果说漏桶是小学应用题中的奇怪水池,那令牌桶就是你去银行办事时摆在门口的那台排队取号机。它与漏桶一样都是基于缓冲区的限流算法,只是方向刚好相反:漏桶是从水池里往系统出水,令牌桶则是系统往排队机中放入令牌。
令牌桶模式具体是如何实现的呢?我来举个例子。
假设我们要限制系统在X秒内的最大请求次数不超过Y那我们可以每间隔X/Y时间就往桶中放一个令牌当有请求进来时首先要从桶中取得一个准入的令牌然后才能进入系统处理。任何时候一旦请求进入桶中发现没有令牌可取了就应该马上失败或进入服务降级逻辑。
与漏桶类似,令牌桶同样有最大容量,这意味着当系统比较空闲的时候,桶中的令牌累积到一定程度就不再无限增加,而预存在桶中的令牌便是请求最大缓冲的余量。
这里可能说得有些抽象,你可以转化为以下步骤来指导程序编码:
让系统以一个由限流目标决定的速率向桶中注入令牌比如要控制系统的访问不超过100次速率即设定为1/100=10毫秒。
桶中最多可以存放N个令牌N的具体数量是由超时时间和服务处理能力共同决定的。如果桶已满第N+1个进入的令牌就会被丢弃掉。
请求到时会先从桶中取走1个令牌如果桶已空就进入降级逻辑。
总体来说,令牌桶模式的实现看似可能比较复杂,每间隔固定时间,我们就要把新的令牌放到桶中,但其实我们并不需要真的用一个专用线程或者定时器来做这件事情,只要在令牌中增加一个时间戳记录,每次获取令牌前,比较一下时间戳与当前时间,就可以轻易计算出这段时间需要放多少令牌进去,然后一次性放完全部令牌即可,所以真正编码时并不会显得很复杂。
分布式限流
那么,在理解了实践可用的几种限流模式之后,我们接着再向实际的信息系统前进一步,一起来讨论下分布式系统中的限流问题。
此前,我们讨论的种种限流算法和模式全部是针对整个系统的限流,总是有意无意地假设或默认系统只提供一种业务操作,或者所有业务操作的消耗都是等价的,并不涉及不同业务请求进入系统的服务集群后,分别会调用哪些服务、每个服务节点处理能力有何差别等问题。
另外,这些限流算法直接使用在单体架构的集群上确实是完全可行的,但到了微服务架构下,它们就最多只能应用于集群最入口处的网关上,对整个服务集群进行流量控制,而无法细粒度地管理流量在内部微服务节点中的流转情况。
所以,我们把前面介绍的限流模式都统称为单机限流,把能够精细控制分布式集群中每个服务消耗量的限流算法称为分布式限流。
你可能要问,这两种限流算法在实现上的核心差别是什么呢?
答案是,要看二者是如何管理限流的统计指标的。
单机限流很好办,指标都是存储在服务的内存当中;而分布式限流的目的是要让各个服务节点的协同限流。无论是将限流功能封装为专门的远程服务,还是在系统采用的分布式框架中有专门的限流支持,都需要把每个服务节点的内存中的统计数据给开放出来,让全局的限流服务可以访问到才行。
一种常见的简单分布式限流方法是将所有服务的统计结果都存入集中式缓存如Redis以实现在集群内的共享并通过分布式锁、信号量等机制解决这些数据在读写访问时的并发控制问题。
那么由此我们也能得出一个结论,在可以共享统计数据的前提下,原本用于单机的限流模式,理论上也是可以应用于分布式环境中的,可是它的代价也显而易见:每次服务调用都必须要额外增加一次网络开销,所以这种方法的效率肯定是不高的,当流量压力大的时候,限流本身反倒会显著降低系统的处理能力。
这也就是说,只要集中式存储统计信息,就不可避免地会产生网络开销。因此为了缓解这里产生的性能损耗,一种可以考虑的办法是在令牌桶限流模式的基础上,进行“货币化改造”改造。即不把令牌看作是只有准入和不准入的“通行证”,而把它看作是数值形式的“货币额度”。
具体是什么意思呢也就是当请求进入集群时首先在API网关处领取到一定数额的“货币”为了体现不同等级用户重要性的差别他们的额度可以有所差异比如让VIP用户的额度更高甚至是无限的。
这里我们将用户A的额度表示为QuanityA。由于任何一个服务在响应请求时都需要消耗集群中一定量的处理资源所以在访问每个服务时都要求消耗一定量的“货币”。
假设服务X要消耗的额度表示为CostX那当用户A访问了N个服务以后他剩余的额度LimitN就会表示为
LimitN = QuanityA - ∑NCostX
此时我们可以把剩余额度LimitN作为内部限流的指标规定在任何时候只要剩余额度LimitN小于等于0时就不再允许访问其他服务了。另外这时还必须先发生一次网络请求重新向令牌桶申请一次额度成功后才能继续访问不成功则进入降级逻辑。除此之外的任何时刻即LimitN不为0时都无需额外的网络访问因为计算LimitN是完全可以在本地完成的。
这种基于额度的限流方案,对限流的精确度会有一定的影响,比如可能存在业务操作已经进行了一部分服务调用,却无法从令牌桶中再获取到新额度,因“资金链断裂”而导致业务操作失败的情况。这种失败的代价是比较高昂的,它白白浪费了部分已经完成了的服务资源,但总体来说,它仍然是一种在并发性能和限流效果上,都相对折衷可行的分布式限流方案。
小结
这节课,我带你学习了限流的目标与指标这两项概念性的内容,现在你可以根据系统的服务和流量特征,来事先做好系统开发设计中针对流量的规划问题了。
另外,我还带你重点学习了单机限流的流量计数器、滑动时间窗、漏桶和令牌桶这四种实现模式,也了解了如何将单机限流升级为分布式限流的实现方案。你要注意的地方是,对于分布式系统容错的设计,是必须要有且无法妥协的措施。但限流与容错不一样,做分布式限流从不追求“越彻底越好”,我们往往需要权衡方案的代价与收益。
一课一思
请介绍一下你接触的生产系统中,都采用了什么样的流量治理手段或者框架?欢迎给我留言,分享你的答案。
如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
39 _ 如何构建零信任网络安全?
你好,我是周志明。
在学完第4讲的课程之后现在我们知道了微服务的核心技术特征之一是分散治理Decentralized Governance这表明了微服务并不追求统一的技术平台而是提倡让团队有自由选择的权利不受制于语言和技术框架。
在开发阶段构建服务时分散治理打破了由技术栈带来的约束它带来的好处是不言自明的。但在运维阶段部署服务时尤其是在考量起安全问题时由Java、Golang、Python、Node.js等多种语言和框架共同组成的微服务系统出现安全漏洞的概率肯定要比只采用其中某种语言、某种框架所构建的单体系统更高。
于是,为了避免由于单个服务节点出现漏洞被攻击者突破,进而导致整个系统和内网都遭到入侵,我们就必须打破一些传统的安全观念,以此来构筑更加可靠的服务间通讯机制。
基于边界的安全模型
长期以来主流的网络安全观念都比较提倡根据某类与宿主机相关的特征比如机器所处的位置或者机器的IP地址、子网等等把网络划分为不同的区域不同的区域对应不同的风险级别和允许访问的网络资源权限把安全防护措施集中部署在各个区域的边界之上重点关注跨区域的网络流量。
现在我们所熟知的VPN、DMZ、防火墙、内网、外网等概念可以说都是因此而生的这种安全模型今天也被叫做是基于边界的安全模型Perimeter-Based Security Model简称“边界安全”
边界安全是完全合情合理的做法,在“安全架构”这个小章节中,我就强调过安全不可能是绝对的,我们必须在可用性和安全性之间做好权衡和取舍。不然我们想想,把一台“服务器”的网线拔掉、电源关掉,不让它对外提供服务,那它肯定是最安全的。
另外,边界安全着重检查的是经过网络区域边界的流量,而对可信任区域(内网)内部机器之间的流量,会给予直接信任、或者至少是较为宽松的处理策略,这样就减小了安全设施对整个应用系统复杂度的影响,以及网络传输性能的额外损耗,所以它当然是很合理的。
可是,今天单纯的边界安全,已经不能满足大规模微服务系统技术异构和节点膨胀的发展需要了。
这是因为边界安全的核心问题在于,边界上的防御措施即使自身能做到永远滴水不漏、牢不可破,也很难保证内网中它所尽力保护的某一台服务器不会成为“猪队友”,一旦“可信的”网络区域中的某台服务器被攻陷,那边界安全措施就成了马其诺防线,攻击者很快就能以一台机器为跳板,侵入到整个内网。
实际上,这是边界安全的基因所决定的固有缺陷,从边界安全被提出的第一天起,这就已经是可以预料到的问题了。不过在微服务时代,我们已经转变了开发观念,承认服务了总是会出错的,那么现在我们也必须转变安全观念,承认一定会有被攻陷的服务。
为此,我们就需要寻找到与之匹配的新的网络安全模型。
零信任安全模型
2010年Forrester Research的首席分析师约翰 · 金德维格John Kindervag提出了零信任安全模型的概念Zero-Trust Security Model后面简称“零信任安全”最初提出时它是叫“零信任架构”Zero-Trust Architecture这个概念在当时并没有引发太大的关注但随着微服务架构的日渐兴盛越来越多的开发和运维人员注意到零信任安全模型与微服务所追求的安全目标是高度吻合的。
零信任安全的中心思想是不应当以某种固有特征来自动信任任何流量,除非明确得到了能代表请求来源(不一定是人,更可能是另一台服务)的身份凭证,否则一律不会有默认的信任关系。
在2019年Google发表了一篇在安全与研发领域里都备受关注的论文《BeyondProd: A New Approach to Cloud-Native Security》BeyondCorp和BeyondProd是谷歌最新一代安全框架的名字其中详细列举了传统的基于边界的网络安全模型与云原生时代下基于零信任网络的安全模型之间的差异并描述了要完成边界安全模型到零信任安全模型的迁移所要实现的具体需求点这里我把它翻译、整理了出来你可以参考下
这个表格已经系统地阐述了零信任安全在微服务、云原生环境中的具体落地过程了后续的整篇论文除了介绍Google自己的实现框架外就是以此为主线来展开论述的但是这个表格还是过于简单论文原文也写得比较分散晦涩所以这里我就根据自己的理解给你展开介绍一下其中的主要观点以此让你进一步理解零信任安全的含义。
零信任网络不等同于放弃在边界上的保护设施
虽然像防火墙这样的位于网络边界的设施是属于边界安全而不是零信任安全的概念但它仍然是一种提升安全性的有效且必要的做法。在微服务集群的前端部署防火墙把内部服务节点间的流量与来自互联网的流量隔离开这种做法无论何时都是值得提倡的因为这样至少能够让内部服务避开来自互联网未经授权流量的饱和攻击比如最典型的DDoS拒绝服务攻击。
身份只来源于服务
我们知道传统应用一般是部署在特定的服务器上的这些机器的IP、MAC地址很少会发生变化此时系统的拓扑状态是相对静态的。基于这个前提安全策略才会使用IP地址、主机名等作为身份标识符Identifiers无条件信任具有特性身份表示的服务。
而如今的微服务系统尤其是云原生环境中的微服务系统其虚拟化基础设施已经得到了大范围的应用这就会导致服务所部署的IP地址、服务实例的数量随时都可能发生变化。因此身份只能来源于服务本身所能够出示的身份凭证通常是数字证书而不再是服务所在的IP地址、主机名或者其他特征。
服务之间也没有固有的信任关系
这点决定了只有已知的、明确授权的调用者才能访问服务,从而阻止攻击者通过某个服务节点中的代码漏洞来越权调用到其他服务。
另外,如果某个服务节点被成功入侵,这个原则也能阻止攻击者扩大其入侵范围。这个其实比较类似于微服务设计模式中,使用断路器、舱壁隔离实现容错来避免雪崩效应,在安全方面,我们也应当采用这种“互不信任”的模式来隔离入侵危害的影响范围。
集中、共享的安全策略实施点
这点与微服务的“分散治理”刚好相反微服务提倡每个服务独立地负责自身所有的功能性与非功能性需求。而Google这个观点相当于为分散治理原则做了一个补充分散治理但涉及安全的非功能性需求如身份管理、安全传输层、数据安全层最好除外。
一方面要写出高度安全的代码非常不容易为此付出的精力甚至可能远高于业务逻辑本身。如果你有兴趣阅读基于Spring Cloud的Fenixs Bookstore的源码很容易就会发现在Security工程中的代码量是该项目中所有微服务中最多的。
更重要的是另一方面,也就是如果让服务各自处理安全问题,很容易会出现实现不一致、或者出现漏洞时要反复修改多处地方的情况,而且还有一些安全问题,如果不立足于全局是很难彻底解决的(在下节课面向具体操作实践的“服务安全”中我还会详细讲述)。
所以Google明确提出应该有集中式的“安全策略实施点”原文中称为Choke Points安全需求应该从微服务的应用代码下沉至云原生的基础设施里这也就契合了论文的标题“Cloud-Native Security”。
受信的机器运行来源已知的代码
这条原则就限制了服务只能使用认证过的代码和配置,并且只能运行在认证过的环境中。
分布式软件系统除了促使软件架构发生了重大变化之外对软件的发布流程也有很大的改变使其严重依赖持续集成与持续部署Continuous Integration / Continuous DeliveryCI/CD。从开发人员编写代码到自动化测试到自动集成再到漏洞扫描最后发布上线这整套CI/CD流程被称作“软件供应链”Software Supply Chain
可是安全问题并不仅仅局限于软件运行阶段。我举个例子之前造成过很大影响的XCodeGhost风波就是针对软件供应链的攻击事件它是在编译阶段把恶意代码嵌入到软件当中只要安装了此软件的用户就可能触发恶意代码。
因此也是为了避免这样的事件再发生,零信任安全针对软件供应链的每一步,都加入了安全控制策略。
自动化、标准化的变更管理
这点也是提倡要通过基础设施,而不是应用代码去实现安全功能的另一个重要理由。如果将安全放在应用上,由于应用本身的分散治理,这决定了安全也必然是难以统一和标准化的,而做不到标准化就意味着做不到自动化。
相反,一套独立于应用的安全基础设施,就可以让运维人员轻松地了解基础设施变更对安全性的影响,并且可以在几乎不影响生产环境的情况下,发布安全补丁程序。
强隔离性的工作负载
“工作负载”的概念贯穿了Google内部的Borg系统与后来的Kubernetes系统它是指在虚拟化技术支持下运行的一组能够协同提供服务的镜像。下个模块我介绍云原生基础设施的时候会详细介绍容器化这里我先给你说明一个要点就是容器化仅仅是虚拟化的一个子集。
实际上,容器比起传统虚拟机的隔离能力是有所降低的,这种设计对性能非常有利,却对安全相对不利,因此在强调安全性的应用里,会有专门关注强隔离性的容器运行工具出现。
Google的零信任安全实践
Google认为零信任安全模型的最终目标是实现整个基础设施之上的自动化安全控制服务所需的安全能力可以与服务自身一起以相同方式自动进行伸缩扩展。
对于程序来说做到安全是日常风险是例外Secure by Default and Insecure by Exception对于人类来说做到袖手旁观是日常主动干预是例外Human Actions Should Be by Exception, Not Routine
这的确是很美好的愿景,只是这种“喊口号”式的目标,在软件发展史上也提出过很多次,却一直难以真正达成,其中的原因我在开篇其实就提到过,安全不可能是绝对的,而是有成本的。
那么你其实也能很明显地发现,之所以在今天这节课,我们才真正严肃地讨论零信任网络模型,并不是因为它本身有多么巧妙、有什么此前没有想到的好办法,而是因为它受制于前面我提到的边界安全模型的“合理之处”,即“安全设施对整个应用系统复杂度的影响,以及网络传输性能的额外损耗”。
那到底零信任安全要实现这个目标的代价是什么呢会有多大这里我根据Google论文的观点来回答下这个问题为了保护服务集群内的代码与基础设施Google设计了一系列的内部工具才最终得以实现前面所说的那些安全原则
为了在网络边界上保护内部服务免受DDoS攻击设计了名为Google Front End名字意为“最终用户访问请求的终点”的边缘代理负责保证此后所有流量都在TLS之上传输并自动将流量路由到适合的可用区域之中。
为了强制身份只来源于服务设计了名为Application Layer Transport Security应用层传输安全的服务认证机制这是一个用于双向认证和传输加密的系统它可以自动把服务与它的身份标识符绑定使得所有服务间流量都不必再使用服务名称、主机IP来判断对方的身份。
为了确保服务间不再有默认的信任关系设计了Service Access Policy服务访问策略来管理一个服务向另一个服务发起请求时需要提供的认证、鉴权和审计策略并支持全局视角的访问控制与分析以达成“集中、共享的安全策略实施点”这条原则。
为了实现仅以受信的机器运行来源已知的代码设计了名为Binary Authorization二进制授权的部署时检查机制确保在软件供应链的每一个阶段都符合内部安全检查策略并对此进行授权与鉴权。同时谷歌设计了名为Host Integrity宿主机完整性的机器安全启动程序在创建宿主机时自动验证包括BIOS、BMC、Bootloader和操作系统内核的数字签名。
为了工作负载能够具有强隔离性设计了名为gVisor的轻量级虚拟化方案。这个方案与此前由Intel发起的Kata Containers的思路异曲同工目的都是解决容器共享操作系统内核而导致隔离性不足的安全缺陷它们的做法也都是为每个容器提供了一个独立的虚拟Linux内核。比如gVisor是用Golang实现了一个名为Sentry的、能够提供传统操作系统内核能力的进程。严格来说无论是gVisor还是Kata Containers尽管都披着容器运行时的外衣但它们在本质上都是轻量级虚拟机。
到这里作为一名普通的软件开发者你在看完Google关于零信任安全的论文或者学习完我这些简要的转述了解到即使是Google也必须要花费如此庞大的精力才能做到零信任安全那你最有可能的感受大概不是对零信任安全心生向往而是准备对它挥手告别了。
其实哪怕不需要开发、不需要购买免费把前面Google开发的安全组件赠送给我们大多数的开发团队恐怕也没有足够的运维能力。
小结
在微服务时代以前,传统的软件系统与研发模式的确是很难承受零信任安全模型的代价的,只有到了云原生时代,虚拟化的基础设施长足发展,能将复杂性隐藏于基础设施之内,开发者不需要为了达成每一条安全原则,而专门开发或引入可感知的安全设施;只有容器与虚拟化网络的性能足够高,在可以弥补安全隔离与安全通讯的额外损耗的前提下,零信任网络的安全模型才有它生根发芽的土壤。
另外,零信任安全引入了比边界安全更细致、更复杂的安全措施的同时,也强调自动与透明的重要性。这既要保证系统各个微服务之间能安全通讯,同时也不削弱微服务架构本身的设计原则,比如集中式的安全并不抵触于分散治理原则,安全机制并不影响服务的自动伸缩和有效的封装,等等。
总而言之,只有零信任安全的成本在开发与运维上都是可接受的,它才不会变成仅仅具备理论可行性的“大饼”,不会给软件带来额外的负担。
当然,如何构建零信任网络安全是一个非常大而且比较前沿的话题,在下一节课,我会从实践的角度出发,更具体、更量化地给你展示零信任安全模型的价值与权衡。
一课一思
你认为零信任安全所付出的代价与收益是什么?在实践中,你应该如何权衡两者的权重呢?
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,300 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
40 _ 如何实现零信任网络下安全的服务访问?
你好,我是周志明。
在上节课“零信任网络安全”当中我们探讨了与微服务运作特点相适应的零信任安全模型。今天这节课我们会从实践和编码的角度出发一起来了解在前微服务时代以Spring Cloud为例和云原生时代以Kubernetes with Istio为例零信任网络分别是如何实现安全传输、认证和授权的。
这里我要说明的是,由于这节课是面向实践的,必然会涉及到具体代码,为了便于讲解,在课程中我只贴出了少量的核心代码片段,所以我建议你在开始学习这节课之前,先去浏览一下这两个样例工程的代码,以便获得更好的学习效果。
建立信任
首先我们要知道,零信任网络里不存在默认的信任关系,一切服务调用、资源访问成功与否,都需要以调用者与提供者间已建立的信任关系为前提。
之前我们在第23讲也讨论过真实世界里能够达成信任的基本途径不外乎基于共同私密信息的信任和基于权威公证人的信任两种而在网络世界里因为客户端和服务端之间一般没有什么共同私密信息所以真正能采用的就只能是基于权威公证人的信任它有个标准的名字公开密钥基础设施Public Key InfrastructurePKI
这里你可以先记住一个要点PKI是构建传输安全层Transport Layer SecurityTLS的必要基础。
在任何网络设施都不可信任的假设前提下无论是DNS服务器、代理服务器、负载均衡器还是路由器传输路径上的每一个节点都有可能监听或者篡改通讯双方传输的信息。那么要保证通讯过程不受到中间人攻击的威胁唯一具备可行性的方案是启用TLS对传输通道本身进行加密让发送者发出的内容只有接受者可以解密。
建立TLS传输说起来好像并不复杂只要在部署服务器时预置好CA根证书以后用该CA为部署的服务签发TLS证书就行了。
但落到实际操作上,这个事情就属于典型的“必须集中在基础设施中自动进行的安全策略实施点”,毕竟面对数量庞大且能够自动扩缩的服务节点,依赖运维人员手工去部署和轮换根证书,肯定是很难持续做好的。
而除了随服务节点动态扩缩而来的运维压力外微服务中TLS认证的频次也很明显地高于传统的应用。比起公众互联网中主流单向的TLS认证在零信任网络中往往要启用双向TLS认证Mutual TLS Authentication常简写为mTLS也就是不仅要确认服务端的身份还需要确认调用者的身份。
单向TLS认证只需要服务端提供证书客户端通过服务端证书验证服务器的身份但服务器并不验证客户端的身份。单向TLS用于公开的服务即任何客户端都被允许连接到服务进行访问它保护的重点是客户端免遭冒牌服务器的欺骗。
双向TLS认证客户端、服务端双方都要提供证书双方各自通过对方提供的证书来验证对方的身份。双向TLS用于私密的服务即服务只允许特定身份的客户端访问它除了保护客户端不连接到冒牌服务器外也保护服务端不遭到非法用户的越权访问。
另外对于前面提到的围绕TLS而展开的密钥生成、证书分发、签名请求Certificate Signing RequestCSR、更新轮换等等这其实是一套操作起来非常繁琐的流程稍有疏忽就会产生安全漏洞。所以尽管它在理论上可行但实践中如果没有自动化的基础设施的支持仅靠应用程序和运维人员的努力是很难成功实施零信任安全模型的。
那么接下来我们就结合Fenixs Bookstore的代码聚焦于“认证”和“授权”这两个最基本的安全需求来看看它们在微服务架构下有或者没有基础设施支持的时候各自都是如何实现的。
我们先来看看认证。
认证
根据认证的目标对象我们可以把认证分为两种类型一种是以机器作为认证对象即访问服务的流量来源是另外一个服务这被叫做服务认证Peer Authentication直译过来是“节点认证”另一种是以人类作为认证对象即访问服务的流量来自于最终用户这被叫做请求认证Request Authentication
当然,无论是哪一种认证,无论有没有基础设施的支持,它们都要有可行的方案来确定服务调用者的身份,只有建立起信任关系才能调用服务。
好,下面我们来了解下服务认证的相关实现机制。
服务认证
Istio版本的Fenixs Bookstore采用了双向TLS认证作为服务调用双方的身份认证手段。得益于Istio提供的基础设施的支持我们不需要Google Front End、Application Layer Transport Security这些安全组件也不需要部署PKI和CA甚至无需改动任何代码就可以启用mTLS认证。
不过Istio毕竟是新生事物如果你要在自己的生产系统中准备启用mTLS还是要先想一下是否整个服务集群的全部节点都受Istio管理如果每一个服务提供者、调用者都会受到Istio的管理那mTLS就是最理想的认证方案。你只需要参考以下简单的PeerAuthentication CRD配置就可以对某个Kubernetes名称空间范围内的所有流量启用mTLS
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: authentication-mtls
namespace: bookstore-servicemesh
spec:
mtls:
mode: STRICT
不过如果你的分布式系统还没有达到完全云原生的程度其中还存在部分不受Istio管理即未注入Sidecar的服务端或者客户端这是很常见的你也可以将mTLS传输声明为“宽容模式”Permissive Mode
宽容模式的含义是受Istio管理的服务会允许同时接受纯文本和mTLS两种流量。纯文本流量只用来和那些不受Istio管理的节点进行交互你需要自行想办法解决纯文本流量的认证问题而对于服务网格内部的流量就可以使用mTLS认证。
这里你要知道的是宽容模式为普通微服务向服务网格迁移提供了良好的灵活性让运维人员能够逐个给服务进行mTLS升级。甚至在原本没有启用mTLS的服务中启用mTLS时可以不中断现存已经建立的纯文本传输连接完全不会被最终用户感知到。
这样一旦所有服务都完成迁移就可以把整个系统设置为严格TLS模式即前面代码中的mode: STRICT。
在Spring Cloud版本的Fenixs Bookstore里因为没有基础设施的支持一切认证工作就不得不在应用层面去实现。我选择的方案是借用OAtuh 2.0协议的客户端模式来进行认证的,其大体思路有如下两步。
第一步每一个要调用服务的客户端都与认证服务器约定好一组只有自己知道的密钥Client Secret这个约定过程应该是由运维人员在线下自行完成通过参数传给服务而不是由开发人员在源码或配置文件中直接设定。我在演示工程的代码注释中也专门强调了这点以免你被示例代码中包含密钥的做法所误导。
这个密钥其实就是客户端的身份证明客户端在调用服务时会先使用该密钥向认证服务器申请到JWT令牌然后通过令牌证明自己的身份最后访问服务。
你可以看看下面给出的代码示例它定义了五个客户端其中四个是集群内部的微服务均使用客户端模式并且注明了授权范围是SERVICE授权范围在下面介绍授权中会用到示例中的第一个是前端代码的微服务它使用密码模式授权范围是BROWSER。
/**
* 客户端列表
*/
private static final List<Client> clients = Arrays.asList(
new Client("bookstore_frontend", "bookstore_secret", new String[]{GrantType.PASSWORD, GrantType.REFRESH_TOKEN}, new String[]{Scope.BROWSER}),
// 微服务一共有Security微服务、Account微服务、Warehouse微服务、Payment微服务四个客户端
// 如果正式使用这部分信息应该做成可以配置的以便快速增加微服务的类型。clientSecret也不应该出现在源码中应由外部配置传入
new Client("account", "account_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}),
new Client("warehouse", "warehouse_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}),
new Client("payment", "payment_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}),
new Client("security", "security_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE})
);
第二步每一个对外提供服务的服务端都扮演着OAuth 2.0中的资源服务器的角色它们都声明为要求提供客户端模式的凭证如以下代码所示。客户端要调用受保护的服务就必须先出示能证明调用者身份的JWT令牌否则就会遭到拒绝。这个操作本质上是授权的过程但它在授权过程中其实已经实现了服务的身份认证。
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
而且由于每一个微服务都同时具有服务端和客户端两种身份它们既消费其他服务也提供服务供别人消费所以在每个微服务中都应该要包含以上这些代码放在公共infrastructure工程里
另外Spring Security提供的过滤器会自动拦截请求驱动认证、授权检查的执行以及申请和验证JWT令牌等操作无论是在开发期对程序员还是在运行期对用户都能做到相对透明。
不过尽管如此,这样的做法仍然是一种应用层面的、不加密传输的解决方案。为什么呢?
前面我提到在零信任网络中面对可能的中间人攻击TLS是唯一可行的办法。其实我的言下之意是即使应用层的认证能在一定程度上保护服务不被身份不明的客户端越权调用但是如果内容在传输途中被监听、篡改或者被攻击者拿到了JWT令牌之后冒认调用者的身份去调用其他服务应用层的认证就无法防御了。
所以简而言之,这种方案并不适用于零信任安全模型,只有在默认内网节点间具备信任关系的边界安全模型上,才能良好工作。
好,我们再来说说请求认证。
请求认证
对于来自最终用户的请求认证Istio版本的Fenixs Bookstore仍然能做到单纯依靠基础设施解决问题整个认证过程不需要应用程序参与JWT令牌还是在应用中生成的因为Fenixs Bookstore并没有使用独立的用户认证服务器只有应用本身才拥有用户信息
当来自最终用户的请求进入服务网格时Istio会自动根据配置中的JWKSJSON Web Key Set来验证令牌的合法性如果令牌没有被篡改过且在有效期内就信任Payload中的用户身份并从令牌的Iss字段中获得Principal。关于Iss、Principals等概念我在安全架构这个小章节中都介绍过了你可以去回顾复习一下第23到30讲。而JWKS倒是之前从没有提到过它代表了一个密钥仓库。
我们知道在分布式系统中JWT要采用非对称的签名算法RSA SHA256、ECDSA SHA256等默认的HMAC SHA256属于对称加密认证服务器使用私钥对Payload进行签名资源服务器使用公钥对签名进行验证。
而常与JWT配合使用的JWKJSON Web Key就是一种存储密钥的纯文本格式在功能上它和JKSJava Key Storage、P12Predecessor of PKCS#12、PEMPrivacy Enhanced Mail这些常见的密钥格式并没有什么本质上的差别。
所以顾名思义JWKS就是一组JWK的集合。支持JWKS的系统能通过JWT令牌Header中的KIDKey ID自动匹配出应该使用哪个JWK来验证签名。
以下是Istio版本的Fenixs Bookstore中的用户认证配置。其中jwks字段配的就是JWKS全文实际生产中并不推荐这样做应该使用jwkUri来配置一个JWKS地址以方便密钥轮换根据这里配置的密钥信息Istio就能够验证请求中附带的JWT是否合法。
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: authentication-jwt-token
namespace: bookstore-servicemesh
spec:
jwtRules:
- issuer: "[email protected]"
# Envoy默认只认“Bearer”作为JWT前缀之前其他地方用的都是小写这里专门兼容一下
fromHeaders:
- name: Authorization
prefix: "bearer "
# 在rsa-key目录下放了用来生成这个JWKS的证书最初是用java keytool生成的jks格式一般转jwks都是用pkcs12或者pem格式为方便使用也一起附带了
jwks: |
{
"keys": [
{
"e": "AQAB",
"kid": "bookstore-jwt-kid",
"kty": "RSA",
"n": "i-htQPOTvNMccJjOkCAzd3YlqBElURzkaeRLDoJYskyU59JdGO-p_q4JEH0DZOM2BbonGI4lIHFkiZLO4IBBZ5j2P7U6QYURt6-AyjS6RGw9v_wFdIRlyBI9D3EO7u8rCA4RktBLPavfEc5BwYX2Vb9wX6N63tV48cP1CoGU0GtIq9HTqbEQs5KVmme5n4XOuzxQ6B2AGaPBJgdq_K0ZWDkXiqPz6921X3oiNYPCQ22bvFxb4yFX8ZfbxeYc-1rN7PaUsK009qOx-qRenHpWgPVfagMbNYkm0TOHNOWXqukxE-soCDI_Nc--1khWCmQ9E2B82ap7IXsVBAnBIaV9WQ"
}
]
}
forwardOriginalToken: true
而Spring Cloud版本的Fenixs Bookstore就要稍微麻烦一些它依然是采用JWT令牌作为用户身份凭证的载体认证过程依然在Spring Security的过滤器里中自动完成。不过因为这节课我们讨论的重点不在Spring Security的过滤器工作原理所以它的详细过程就不展开了只简单说说其主要路径过滤器→令牌服务→令牌实现。
既然如此Spring Security已经做好了认证所需的绝大部分工作那么真正要开发者去编写的代码就是令牌的具体实现即代码中名为“RSA256PublicJWTAccessToken”的实现类。
它的作用是加载Resource目录下的公钥证书public.cert实在是怕“抄作业不改名字”的行为我再一次强调不要将密码、密钥、证书这类敏感信息打包到程序中示例代码只是为了演示实际生产应该由运维人员管理密钥验证请求中的JWT令牌是否合法。
@Named
public class RSA256PublicJWTAccessToken extends JWTAccessToken {
RSA256PublicJWTAccessToken(UserDetailsService userDetailsService) throws IOException {
super(userDetailsService);
Resource resource = new ClassPathResource("public.cert");
String publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
setVerifierKey(publicKey);
}
}
如果JWT令牌合法Spring Security的过滤器就会放行调用请求并从令牌中提取出Principals放到自己的安全上下文中即“SecurityContextHolder.getContext()”)。
在开发实际项目的时候你可以根据需要自行决定Principals的具体形式比如既可以像Istio中那样直接从令牌中取出来以字符串的形式原样存放节省一些数据库或者缓存的查询开销也可以统一做些额外的转换处理以方便后续业务使用比如将Principals转换为系统中的用户对象。
Fenixs Bookstore的转换操作是在JWT令牌的父类JWTAccessToken中完成的。所以可见尽管由应用自己来做请求验证会有一定的代码量和侵入性但同时自由度确实也会更高一些。
这里为了方便不同版本实现之间的对比在Istio版本中我保留了Spring Security自动从令牌转换Principals为用户对象的逻辑因此就必须在YAML中包含forwardOriginalToken: true的配置告诉Istio验证完JWT令牌后不要丢弃掉请求中的Authorization Header而是要原样转发给后面的服务处理。
授权
那么经过认证之后合法的调用者就有了可信任的身份此时就不再需要区分调用者到底是机器服务还是人类最终用户只需要根据其身份角色来进行权限访问控制就行即我们常说的RBAC。
不过为了更便于理解Fenixs Bookstore提供的示例代码仍然沿用此前的思路分别针对来自“服务”和“用户”的流量来控制权限和访问范围。
举个具体例子。如果我们准备把一部分微服务看作是私有服务,限制它只接受来自集群内部其他服务的请求,把另外一部分微服务看作是公共服务,允许它可以接受来自集群外部的最终用户发出的请求;又或者,我们想要控制一部分服务只允许移动应用调用,另外一部分服务只允许浏览器调用。
那么一种可行的方案就是为不同的调用场景设立角色进行授权控制另一种常用的方案是做BFF网关
我们还是以Istio和Spring Cloud版本的Fenixs Bookstore为例。
在Istio版本的Fenixs Bookstore中通过以下文稿这里给出的配置就限制了来自bookstore-servicemesh名空间的内部流量只允许访问accounts、products、pay和settlements四个端点的GET、POST、PUT、PATCH方法而对于来自istio-system名空间Istio Ingress Gateway所在的名空间的外部流量就不作限制直接放行。
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: authorization-peer
namespace: bookstore-servicemesh
spec:
action: ALLOW
rules:
- from:
- source:
namespaces: ["bookstore-servicemesh"]
to:
- operation:
paths:
- /restful/accounts/*
- /restful/products*
- /restful/pay/*
- /restful/settlements*
methods: ["GET","POST","PUT","PATCH"]
- from:
- source:
namespaces: ["istio-system"]
但针对外部的请求不来自bookstore-servicemesh名空间的流量又进行了另外一层控制如果请求中没有包含有效的登录信息就限制不允许访问accounts、pay和settlements三个端点如以下配置所示
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: authorization-request
namespace: bookstore-servicemesh
spec:
action: DENY
rules:
- from:
- source:
notRequestPrincipals: ["*"]
notNamespaces: ["bookstore-servicemesh"]
to:
- operation:
paths:
- /restful/accounts/*
- /restful/pay/*
- /restful/settlements*
由此可见Istio已经提供了比较完善的目标匹配工具比如前面配置中用到的源from、目标to以及没有用到的条件匹配when还有其他像是通配符、IP、端口、名空间、JWT字段等等。
当然了要说灵活和功能强大它肯定还是不可能跟在应用中由代码实现的授权相媲美但对绝大多数场景来说已经够用了。在便捷性、安全性、无侵入、统一管理等方面Istio这种在基础设施上实现授权的方案显然要更具优势。
而在Spring Cloud版本的Fenixs Bookstore中授权控制自然还是使用Spring Security、通过应用程序代码来实现的。
常见的Spring Security授权方法有两种。
第一种是使用它的ExpressionUrlAuthorizationConfigurer也就是类似下面编码所示的写法来进行集中配置。这个写法跟前面在Istio的AuthorizationPolicy CRD中的写法在体验上是比较相似的也是几乎所有Spring Security资料中都会介绍的最主流的方式比较适合对批量端点进行控制不过在Fenixs Bookstore的示例代码中并没有采用没有什么特别理由就是我的个人习惯而已
http.authorizeRequests()
.antMatchers("/restful/accounts/**").hasScope(Scope.BROWSER)
.antMatchers("/restful/pay/**").hasScope(Scope.SERVICE)
第二种写法就是下面的示例代码中采用的方法了。它是通过Spring的全局方法级安全Global Method Security以及JSR 250的@RolesAllowed注解来做授权控制
这种写法对代码的侵入性更强,需要以注解的形式分散写到每个服务甚至是每个方法中,但好处是能以更方便的形式做出更加精细的控制效果。比如,要控制服务中某个方法,只允许来自服务或者来自浏览器的调用,那直接在该方法上标注@PreAuthorize注解即可而且它还支持SpEL表达式来做条件。
表达式中用到的SERVICE、BROWSER代表的是授权范围就是在声明客户端列表时传入的具体你可以参考这节课开头声明客户端列表的代码清单。
/**
* 根据用户名称获取用户详情
*/
@GET
@Path("/{username}")
@Cacheable(key = "#username")
@PreAuthorize("#oauth2.hasAnyScope('SERVICE','BROWSER')")
public Account getUser(@PathParam("username") String username) {
return service.findAccountByUsername(username);
}
/**
* 创建新的用户
*/
@POST
@CacheEvict(key = "#user.username")
@PreAuthorize("#oauth2.hasAnyScope('BROWSER')")
public Response createUser(@Valid @UniqueAccount Account user) {
return CommonResponse.op(() -> service.createAccount(user));
}
小结
这节课里,我们尝试以程序代码和基础设施两种方式,去实现功能类似的认证与授权,通过这两者的对比,探讨了在微服务架构下,应该如何把业界的安全技术标准引入并实际落地,实现零信任网络下安全的服务访问。
由此我们也得出了一个基本的结论在以应用代码为主去实现安全需求的微服务系统中是很难真正落地零信任安全的这不仅仅是由于安全需求所带来的庞大开发、管理如密钥轮换和建设如PKI、CA的工作量更是因为这种方式很难符合上节课所提到的零信任安全中“集中、共享的安全策略实施点”“自动化、标准化的变更管理”等基本特征。
但另一方面我们也必须看到现在以代码去解决微服务非功能性需求的方案是很主流的像Spring Cloud这些方案在未来的很长一段时间里都会是信息系统重点考虑的微服务框架。因此去学习、了解如何通过代码尽最大可能地去保证服务之间的安全通讯仍然非常有必要。
一课一思
有人说在未来,零信任安全模型很可能会取代边界安全模型,成为微服务间通讯的标准安全观念,你认为这个判断是否会实现呢?或者你是否觉得这只是存在于理论上的美好期望?
欢迎在留言区分享你的答案。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,119 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
41 _ 分布式架构中的可观测到底说的是什么?
你好,我是周志明。从这节课开始,我们将会花四节课的时间去学习“可观测性”方面的知识。
在以前,可观测性并不是软件设计中要重点考虑的问题,甚至很长时间里,人们也并没有把这种属性与可并发性、可用性、安全性等并列,作为系统的非功能属性之一,直到微服务与云原生时代的来临。
对于单体系统来说,可观测性可能确实是附属的边缘属性,但对于分布式系统来说,可观测性就是不可或缺的了。为什么呢?别着急,接下来我就跟你详细说道说道。
可观测性的概念
好,首先呢,我们来了解下可观测性的含义和特点。
随着分布式架构逐渐成为了架构设计的主流可观测性Observability一词也日益被人频繁地提起。
最初它是与可控制性Controllability一起由匈牙利数学家鲁道夫 · 卡尔曼Rudolf E. Kálmán针对线性动态控制系统提出的一组对偶属性。可观测性原本的含义是“可以由系统的外部输出推断其内部状态的程度”。
在学术界,“可观测性”这个名词其实是最近几年才从控制理论中借用的舶来概念,不过实际上,计算机科学中关于可观测性的研究内容已经有了很多年的实践积累。通常,人们会把可观测性分解为三个更具体的方向进行研究,分别是:日志收集、链路追踪和聚合度量。
这三个方向各有侧重,但又不是完全独立的,因为它们天然就有重合或者可以结合的地方。
在2017年的分布式追踪峰会2017 Distributed Tracing Summit结束后彼得 · 波本Peter Bourgon撰写了总结文章《Metrics, Tracing, and Logging》就系统地阐述了这三者的定义、特征以及它们之间的关系与差异受到了业界的广泛认可。
日志、追踪、度量的目标与结合
假如你平时只开发单体系统,从来没有接触过分布式系统的观测工作,那你可能就只熟悉日志这一项工作,对追踪和度量会相对比较陌生。
然而按照彼得 · 波本Peter Bourgon给出的定义来看尽管在分布式系统中追踪和度量的必要性和复杂程度确实比单体系统时要更高但是在单体时代你肯定已经接触过这三项工作了只是并没有意识到而已。
你可能会想进一步了解这三项工作的具体含义,想知道为什么要这样划分,下面我来给你简单介绍一下它们各自的特征,你就能明白其中的原因了:
日志Logging
我们都知道,日志的职责是记录离散事件,通过这些记录事后分析出程序的行为,比如曾经调用过什么方法、曾经操作过哪些数据,等等。通常,打印日志被认为是程序中最简单的工作之一,你在调试问题的时候,可能也经历过这样的情景“当初这里记得打点日志就好了”,可见这就是一项举手之劳的任务。
当然输出日志的确很容易但收集和分析日志却可能会很复杂面对成千上万的集群节点、面对迅速滚动的事件信息、面对数以TB计算的文本传输与归集都并不简单。对大多数程序员来说分析日志也许就是最常遇见、也最有实践可行性的“大数据系统”了。
追踪Tracing
在单体系统时代追踪的范畴基本只局限于栈追踪Stack Tracing。比如说你在调试程序的时候在IDE打个断点看到的Call Stack视图上的内容便是跟踪在编写代码时处理异常调用了Exception::printStackTrace()方法,它输出的堆栈信息也是追踪。
而在微服务时代追踪就不只局限于调用栈了一个外部请求需要内部若干服务的联动响应这时候完整的调用轨迹就会跨越多个服务会同时包括服务间的网络传输信息与各个服务内部的调用堆栈信息。因此分布式系统中的追踪在国内通常被称为“全链路追踪”后面我就直接称“链路追踪”了许多资料中也把它叫做是“分布式追踪”Distributed Tracing
追踪的主要目的是排查故障,比如分析调用链的哪一部分、哪个方法出现错误或阻塞,输入输出是否符合预期,等等。
度量Metrics
度量是指对系统中某一类信息的统计聚合。比如,证券市场的每一只股票都会定期公布财务报表,通过财报上的营收、净利、毛利、资产、负载等等一系列数据,来体现过去一个财务周期中公司的经营状况,这就是一种信息聚合。
Java天生自带有一种基本的度量就是由虚拟机直接提供的JMXJava Management eXtensions度量像是内存大小、各分代的用量、峰值的线程数、垃圾收集的吞吐量、频率等等这些数据信息都可以从JMX中获得。
度量的主要目的是监控Monitoring和预警Alert比如说当某些度量指标达到了风险阈值时就触发事件以便自动处理或者提醒管理员介入。
那到这里,你应该也就知道为什么在单体系统中,除了接触过日志之外,其实也同样接触过其他两项工作了,因为追踪和度量本来就是我们调试和监控程序时的常用手段。
好,说完了学术界对于可观测性的定义和研究,下面我们来看看对于工业界,在云原生时代下,这三个方向都有哪些新的发展。
工业界的遥测产品
在工业界,目前针对可观测性的产品已经是一片红海,经过多年的角逐,日志、度量两个领域的胜利者算是基本尘埃落定了。
一方面在日志领域日志收集和分析大多被统一到了Elastic StackELK技术栈上如果说未来还能出现什么变化的话也就是其中的Logstash能看到有被Fluentd取代的趋势让ELK变成EFK但整套Elastic Stack技术栈的地位已经是相当稳固了。
而在度量方面跟随着Kubernetes统一容器编排的步伐Prometheus也击败了度量领域里以Zabbix为代表的众多前辈即将成为云原生时代度量监控的事实标准。虽然从市场角度来说Prometheus还没有达到Kubernetes那种“拔剑四顾举世无敌”的程度但是从社区活跃度上看Prometheus已经占有了绝对的优势在Google和CNCF的推动下未来前途可期。
额外知识Kubernetes与Prometheus的关系-
Kubernetes是CNCF第一个孵化成功的项目Prometheus是CNCF第二个孵化成功的项目。-
Kubernetes起源于Google的编排系统BorgPrometheus起源于Google为Borg做的度量监控系统BorgMon。
不过,追踪方面的情况与日志、度量有所不同,追踪是与具体网络协议、程序语言密切相关的。
我们在收集日志时不必关心这段日志是由Java程序输出的还是由Golang程序输出的对程序来说它们就只是一段非结构化文本而已同理度量对程序来说也只是一个个聚合的数据指标而已。
但链路追踪就不一样了各个服务之间是使用HTTP还是gRPC来进行通信会直接影响到追踪的实现各个服务是使用Java、Golang还是Node.js来编写也会直接影响到进程内调用栈的追踪方式。
所以,这就决定了追踪工具本身有较强的侵入性,通常是以插件式的探针来实现的;这也决定了在追踪领域很难出现一家独大的情况,通常要有多种产品来针对不同的语言和网络。
最近几年各种链路追踪产品层出不穷市面上主流的工具既有像Datadog这样的一揽子商业方案也有像AWS X-Ray和Google Stackdriver Trace这样的云计算厂商产品还有像SkyWalking、Zipkin、Jaeger这样来自开源社区的优秀产品。
日志、追踪、度量的相关产品
这里我给出的示意图是CNCF Interactive Landscape中列出的日志、追踪、度量领域的著名产品。其实这里很多不同领域的产品是跨界的比如ELK可以通过Metricbeat来实现度量的功能Apache SkyWalking的探针就可以同时支持度量和追踪两方面的数据来源由OpenTracing进化而来OpenTelemetry更是融合了日志、追踪、度量三者所长有望成为三者兼备的统一可观测性的解决方案。在后面关于可观测性的三节课里我也会紧扣每个领域中最具统治性的产品给你做一个详细的介绍。
小结
这节课,我们了解了可观测性的概念、特征与现状,并明确了在今天,可观测性一般会被分成事件日志、链路追踪和聚合度量三个主题方向进行探讨和研究。你可以记住以下几个核心要点:
事件日志的职责是记录离散事件,通过这些记录事后分析出程序的行为;
追踪的主要目的是排查故障,比如分析调用链的哪一部分、哪个方法出现错误或阻塞,输入输出是否符合预期;
度量是指对系统中某一类信息的统计聚合,主要目的是监控和预警,当某些度量指标达到风险阈值时就触发事件,以便自动处理或者提醒管理员介入。
另外,事件日志、链路追踪和聚合度量这三个主题也是未来三节课我们要学习的主角,到时你也可以与这节课的学习内容相互印证。
一课一思
尽管“可观测性”今天已经被提升到了与“可用性”“可并发性”等同等的高度,但实际是否如此呢?在你的公司设计软件系统的时候,可观测性的考虑权重有多大?
欢迎在留言区分享你的见解。好,感谢你的阅读,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。就到这里,我们下一讲再见。

View File

@@ -0,0 +1,230 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
42 _ 分析日志真的没那么简单
你好,我是周志明。
在上节课明确了可观测性的概念、特征与现状之后,我们知道了可观测性一般会被分成三种具体的表现形式,分别是日志、追踪和度量。那么这节课,我们就来讨论其中最普遍的形式:事件日志。
日志主要是用来记录系统运行期间发生过的离散事件。我想应该没有哪一个生产系统会缺少日志功能,不过我也相信,没有多少人会把日志看作是多关键的功能。它就像是阳光与空气,不可或缺但又不太被人重视。
除此之外我想在座的很多人也都会说日志很简单其实这是在说“打印日志”这个操作简单。打印日志的目的是为了日后能从中得到有价值的信息而今天只要是稍微复杂点的系统尤其是复杂的分布式系统就很难只依靠tail、grep、awk来从日志中挖掘信息了往往还要有专门的全局查询和可视化功能。
此时,从打印日志到分析查询之间,还隔着收集、缓冲、聚合、加工、索引、存储等若干个步骤,如下图所示:
日志处理过程
而这一整个链条中会涉及到大量需要我们注意的细节其复杂性并不亚于任何一项技术或业务功能的实现。所以接下来我就以这个日志的处理过程为主线以最成熟的Elastic Stack技术栈为例子给你介绍该链条每个步骤的目的与方法。
好,下面我们就先来了解下日志处理中的输出工作。
输出
要是说好的日志能像文章一样,让人读起来身心舒畅,这话肯定有夸大的成分,不过好的日志应该能做到像“流水账”一样,可以毫无遗漏地记录信息,格式统一,内容恰当。其中,“恰当”是一个难点,它要求日志不应该过多,也不应该过少。
这里的“多与少”一般不针对输出的日志行数。尽管我听说过最夸张的系统有单节点INFO级别下每天的日志都能以TB计算这样的是代码有问题的给网络与磁盘I/O带来了不小的压力但我通常不会用数量来衡量日志是否恰当。
我所说的“恰当”,是指日志中不该出现的内容不要有,而该有的不要少。具体是什么意思呢?下面我就分别给你举几个例子。
不该出现的内容不要有
首先,我们来看看有哪些常见的“不应该有”的日志内容:
避免打印敏感信息
不用专门去提醒,我们肯定都知道不该把密码、银行账号、身份证件等这些敏感信息打到日志里,但我就见过不止一个系统的日志中,能直接找到这些信息。一旦这些敏感信息随日志流到了后续的索引、存储、归档等步骤后,清理起来就会非常麻烦。
不过日志中应该要包含必要的非敏感信息比如当前用户的ID最好是内部ID避免登录名或者用户名称有些系统就直接用MDCMapped Diagnostic Context把用户ID自动打印在Pattern Layout上。
避免引用慢操作
要知道,日志中打印的信息应该是在上下文中可以直接取到的,而如果当前的上下文中根本没有这项数据,需要专门调用远程服务或者从数据库中获取,又或者要通过大量计算才能取到的话,那我们就应该先考虑下,这项信息放到日志中是不是必要且恰当的。
避免打印追踪诊断信息
即日志中不要打印方法输入参数、输出结果、方法执行时长之类的调试信息。
这个观点其实是反直觉的不少公司甚至会提倡把这一点作为最佳实践但是我仍然坚持把它归入反模式中。这是因为日志的职责是记录事件而追踪诊断应该由追踪系统去处理哪怕贵公司完全没有开发追踪诊断方面功能的打算我也建议使用BTrace或者Arthas这类“On-The-Fly”的工具来解决。
还有,我之所以将其归为反模式,也是因为前面所说的敏感信息、慢操作等主要源头,就是这些原本想用于调试的日志。
比如当前方法入口参数有个User对象如果要输出这个对象的话常见的做法是将它序列化成JSON字符串然后打到日志里。那么这个时候User里面的Password字段、BankCard字段就很容易被暴露出来。
再比如当前方法的返回值是个Map我们在开发期的调试数据只做了三五个Entity然后觉得遍历一下把具体内容打到日志里面没什么问题。而到了生产期这个Map里面有可能存放了成千上万个Entity那么这时候打印日志就相当于引用慢操作了。
避免误导他人
你可能也知道,在日志中给以后调试除错的人挖坑是十分恶劣却又常见的行为。不过我觉得大部分人并不是专门要去误导别人,很可能只是无意识地这样做了。
比如明明已经在逻辑中妥善处理好了某个异常只是偏习惯性地调用printStackTrace()方法,把堆栈打到日志中,那么一旦这个方法附近出现问题,由其他人来除错的话,就很容易会盯着这段堆栈去找线索,从而浪费大量时间。
……
该出现的内容不要少
然后,日志中不该缺少的内容也“不应该少”,这里我同样给你举几个建议应该输出到日志中的内容的例子:
处理请求时的TraceID
当服务收到请求时如果该请求没有附带TraceID就应该自动生成唯一的TraceID来对请求进行标记并使用MDC自动输出到日志。TraceID会贯穿整条调用链目的是通过它把请求在分布式系统各个服务中的执行过程给串联起来。TraceID通常也会随着请求的响应返回到客户端如果响应内容出现了异常用户就能通过此ID快速找到与问题相关的日志。
这个TraceID其实是链路追踪里的概念类似的还有用于标识进程内调用状况的SpanID在Java程序中这些都可以用Spring Cloud Sleuth来自动生成下一讲我还会提到
另外尽管TraceID在分布式追踪系统中会发挥最大的作用但对单体系统来说将TraceID记录到日志并返回给最终用户对快速定位错误也仍然十分有价值。
系统运行过程中的关键事件
我们都知道,日志的职责就是记录事件,包括系统进行了哪些操作、发生了哪些与预期不符的情况、在运行期间出现了哪些未能处理的异常或警告、定期自动执行的各种任务,等等,这些都应该在日志中完整地记录下来。
那么原则上,程序中发生的事件只要有价值,就应该去记录,但我们还是要判断清楚事件的重要程度,选定相匹配的日志的级别。至于如何快速处理大量日志,这是后面的步骤需要考虑的问题,如果输出日志实在太频繁,以至于影响到了性能,就应该由运维人员去调整全局或单个类的日志级别来解决。
启动时输出配置信息
与避免输出诊断信息不同,对于系统启动时或者检测到配置中心变化时更新的配置,就应该把非敏感的配置信息输出到日志中,比如连接的数据库、临时目录的路径等等,因为初始化配置的逻辑一般只会执行一次,不便于诊断时复现,所以应该输出到日志中。
……
总而言之,日志输出是程序中非常普遍的行为,我们要把握好“不应该有”和“不应该少”这两个关键点。接下来,我们继续学习日志处理分析链路中,关于收集和缓冲这两个步骤。
收集与缓冲
我们知道,写日志是在服务节点中进行的,但我们不可能在每个节点都单独建设日志查询功能。这不是资源或工作量的问题,而是分布式系统处理一个请求要跨越多个服务节点,因此为了能看到跨节点的全部日志,就要有能覆盖整个链路的全局日志系统。
那么这个需求就决定了当每个节点输出日志到文件后就必须要把日志文件统一收集起来集中存储、索引这一步由Elasticsearch来负责由此便催生出了专门的日志收集器。
最初ELKElastic Stack中的日志收集与下面要讲的加工聚合的职责都是由Logstash来承担的。Logstash既部署在各个节点中作为收集的客户端Shipper也同时有独立部署的节点扮演归集转换日志的服务端Master。毕竟Logstash有良好的插件化设计而收集、转换、输出都支持插件化定制所以它应对多重角色本身并没有什么困难。
但问题是Logstash与它的插件是基于JRuby编写的要跑在单独的Java虚拟机进程上而且Logstash默认的堆大小就到了1GB。对于归集部分Master来说这种消耗当然不算什么问题但作为每个节点都要部署的日志收集器这样的消耗就显得太过负重了。
所以后来Elastic.co公司就把所有需要在服务节点中处理的工作整理成了以Libbeat为核心的Beats框架并使用Golang重写了一个功能较少却更轻量高效的日志收集器这就是今天流行的Filebeat。
现在的Beats已经是一个很大的家族了除了Filebeat外Elastic.co还提供用于收集Linux审计数据的Auditbeat、用于无服务计算架构的Functionbeat、用于心跳检测的Heartbeat、用于聚合度量的Metricbeat、用于收集Linux Systemd Journald日志的Journalbeat、用于收集Windows事件日志的Winlogbeat用于网络包嗅探的Packetbeat等等。
而如果再算上大量由社区维护的Community Beats那几乎是你能想象到的数据都可以被收集到以至于ELK在一定程度上也可以代替度量和追踪系统实现它们的部分职能。
这对于中小型分布式系统来说是很便利的,但对于大型系统,我建议还是让专业的工具去做专业的事情。
还有一点你要知道,日志收集器不仅要保证能覆盖全部数据来源,还要尽力保证日志数据的连续性,这其实是不太容易做到的。为啥呢?
我给你举个例子。像淘宝这类大型的互联网系统每天的日志量超过了10,000TB10PB量级日志收集器的部署实例数能达到百万量级那么此时归集到系统中的日志要想与实际产生的日志保持绝对的一致性是非常困难的我们也不应该为此付出过高的成本。
所以换言之,日志的处理分析其实并不追求绝对的完整精确,只追求在代价可承受的范围内,尽可能地保证较高的数据质量。
一种最常用的缓解压力的做法是将日志接收者从Logstash和Elasticsearch转移至抗压能力更强的队列缓存。比如在Logstash之前架设一个Kafka或者Redis作为缓冲层当面对突发流量Logstash或Elasticsearch的处理能力出现瓶颈时就自动削峰填谷这样甚至当它们短时间停顿也不会丢失日志数据。
加工与聚合
那么在将日志集中收集之后以及存入Elasticsearch之前我们一般还要对它们进行加工转换和聚合处理这一步通常就要使用到前面我提过的Logstash。
这是因为日志是非结构化数据一行日志中通常会包含多项信息如果不做处理那在Elasticsearch就只能以全文检索的原始方式去使用日志这样既不利于统计对比也不利于条件过滤。
举个具体例子下面是一行Nginx服务器的Access Log代表了一次页面访问操作
14.123.255.234 - - [19/Feb/2020:00:12:11 +0800] "GET /index.html HTTP/1.1" 200 1314 "https://icyfenix.cn" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
在这一行日志里面包含了下表所列的10项独立数据项
所以也就是说Logstash的基本职能是把日志行中的非结构化数据通过Grok表达式语法转换为表格那样的结构化数据。而在进行结构化的同时它还可能会根据需要调用其他插件来完成时间处理统一时间格式、类型转换如字符串、数值的转换、查询归类比如将IP地址根据地理信息库按省市归类等各种额外处理的工作然后以JSON格式输出到Elasticsearch中这是最普遍的输出形式Logstash输出也有很多插件可以具体定制不同的格式
如此一来有了这些经过Logstash转换已经结构化了的日志Elasticsearch便可针对不同的数据项来建立索引进行条件查询、统计、聚合等操作了。
而提到聚合这也是Logstash的另一个常见职能。
我们已经知道日志中存储的是离散事件离散的意思就是每个事件都是相互独立的比如有10个用户访问服务他们操作所产生的事件都会在日志中分别记录。
那么如果想从离散的日志中获得统计信息比如想知道这些用户中正常返回200 OK的有多少、出现异常的500 Internal Server Error的有多少再生成个可视化统计图表一种解决方案是通过Elasticsearch本身的处理能力做实时的聚合统计。这是一种很便捷的方式不过要消耗Elasticsearch服务器的运算资源。
另一种解决方案是在收集日志后自动生成某些常用的、固定的聚合指标这种聚合就会在Logstash中通过聚合插件来完成。
这两种聚合方式都有不少的实际应用,前者一般用于应对即席查询,后者更多是用于应对固定查询。
存储与查询
OK经过了前面收集、缓冲、加工、聚合之后的日志数据现在就终于可以放入Elasticsearch中索引存储了。
可以说Elasticsearch是整个Elastic Stack技术栈的核心。其他步骤的工具比如Filebeat、Logstash、Kibana等都有替代品有自由选择的余地唯独Elasticsearch在日志分析这方面完全没有什么值得一提的竞争者几乎就是解决这个问题的唯一答案。
这样的结果肯定与Elasticsearch本身就是一款优秀的产品有关然而更关键的是Elasticsearch的优势正好与日志分析的需求完美契合我们可以根据以下三个角度进行观察
从数据特征的角度看
日志是典型的基于时间的数据流,但它与其他时间数据流,比如你的新浪微博、微信朋友圈这种社交网络数据又稍微有点儿区别:日志虽然增长速度很快,但已经写入的数据几乎没有再发生变动的可能。
由此可见日志的数据特征就决定了所有用于日志分析的Elasticsearch都会使用时间范围作为索引比如根据实际数据量的大小可能是按月、按周或者按日、按时。
这里我以按日索引为例因为你能准确地预知明天、后天的日期所以全部索引都可以预先创建这就免去了动态创建时的寻找节点、创建分片、在集群中广播变动信息等开销。而又因为所有新的日志都是“今天”的日志所以你只要建立“logs_current”这样的索引别名来指向当前索引就能避免代码因日期而变动。
从数据价值的角度看
日志基本上只会以最近的数据为检索目标,随着时间推移,早期的数据会逐渐失去价值。这点就决定了我们可以很容易地区出分冷数据和热数据,进而对不同数据采用不一样的硬件策略。
比如说为热数据配备SSD磁盘和更好的处理器为冷数据配备HDD磁盘和较弱的处理器甚至可以放到更为廉价的对象存储如阿里云的OSS、腾讯云的COS、AWS的S3中归档。
不过这里我也想给你提个醒儿咱们课程这部分的主题是日志在可观测性方面的作用而还有一些基于日志的其他类型的应用比如从日志记录的事件中去挖掘业务热点、分析用户习惯等等这就属于大数据挖掘的范畴了并不在我们讨论“价值”的范围之内事实上它们更可能采用的技术栈是HBase与Spark的组合而不是Elastic Stack。
从数据使用的角度看
要知道分析日志很依赖全文检索和即席查询这对实时性的要求就是处于实时与离线两者之间的“近实时”也就是并不强求日志产生后立刻能查到但我们也不能接受日志产生之后按小时甚至按天的频率来更新而这些检索能力和近实时性也正好都是Elasticsearch的强项。
Elasticsearch只提供了API层面的查询能力它通常搭配同样出自Elastic.co公司的Kibana一起使用我们可以把Kibana看作是Elastic Stack的GUI部分。
不过尽管Kibana只负责图形界面和展示但它提供的能力远不止让你能在界面上执行Elasticsearch的查询那么简单。
Kibana宣传的核心能力是“探索数据并可视化”也就是把存储在Elasticsearch中的被检索、聚合、统计后的数据定制形成各种图形、表格、指标、统计以此观察系统的运行状态找出日志事件中潜藏的规律和隐患。按Kibana官方的宣传语来说就是“一张图片胜过千万行日志”。
Kibana可视化界面
小结
这节课,我们学习了日志从输出、收集、缓冲、加工、聚合、存储、查询等这些步骤的职责与常见的解决方案。
由于日志是程序中最基础的功能之一,我们每个人一定都做过,所以我只花了一节课的时间去讲解,而我的重点并不在于介绍具体的步骤该如何操作,而在于向你呈现每个步骤需要注意的事项。你可以记住以下几个核心要点:
好的日志要能够毫无遗漏地记录信息、格式统一、内容恰当,而“恰当”的真正含义是指日志中不该出现的内容不要有,而该有的不要少。
分布式系统处理一个请求要跨越多个服务节点,因此当每个节点输出日志到文件后,就必须要把日志文件统一收集起来,集中存储、索引,而这正是日志收集器需要做的工作。此外,日志收集器还要尽力保证日志数据的连续性。
由于日志是非结构化数据,因此我们需要进行加工,把日志行中的非结构化数据转换为结构化数据,以便针对不同的数据项来建立索引,进行条件查询、统计、聚合等操作。
一课一思
这节课里,我把日志中“打印追踪诊断信息”作为一种反模式来进行说明,这点其实是有争议的,很多公司、程序员都提倡在日志中打印尽可能多的调试信息,以便跟踪解决问题。那你是如何看待这点的呢?
欢迎在留言区分享你的见解。如果你觉得有收获,也欢迎你把今天的内容分享给更多的朋友。好,感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,159 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
43 _ 一个完整的分布式追踪系统是什么样子的?
你好,我是周志明。这节课我们来讨论链路追踪的话题。
虽然在2010年之前就已经有了X-Trace、Magpie等跨服务的追踪系统了但现代分布式链路追踪公认的起源是Google在2010年发表的论文《Dapper : a Large-Scale Distributed Systems Tracing Infrastructure》这篇论文介绍了Google从2004年开始使用的分布式追踪系统Dapper的实现原理。
此后所有业界有名的追踪系统无论是国外Twitter的Zipkin、Naver的PinpointNaver是Line的母公司Pinpoint的出现其实早于Dapper论文的发表在Dapper论文中还提到了Pinpoint还是国内阿里的鹰眼、大众点评的CAT、个人开源的SkyWalking后来进入Apache基金会孵化毕业都受到了Dapper论文的直接影响。
那么从广义上讲一个完整的分布式追踪系统应该由数据收集、数据存储和数据展示三个相对独立的子系统构成而从狭义上讲则就只是特指链路追踪数据的收集部分。比如Spring Cloud Sleuth就属于狭义的追踪系统通常会搭配Zipkin作为数据展示搭配Elasticsearch作为数据存储来组合使用。
而前面提到的那些Dapper的徒子徒孙们就大多都属于广义的追踪系统它们通常也被称为“APM系统”Application Performance Management应用性能管理
追踪与跨度
为了有效地进行分布式追踪Dapper提出了“追踪”与“跨度”两个概念。
从客户端发起请求抵达系统的边界开始记录请求流经的每一个服务直到向客户端返回响应为止这整个过程就叫做一次“追踪”Trace为了不产生混淆我后面就直接使用英文Trace来指代了
由于每次Trace都可能会调用数量不定、坐标不定的多个服务那么为了能够记录具体调用了哪些服务以及调用的顺序、开始时点、执行时长等信息每次开始调用服务前系统都要先埋入一个调用记录这个记录就叫做一个“跨度”Span
Span的数据结构应该足够简单以便于能放在日志或者网络协议的报文头里也应该足够完备起码要含有时间戳、起止时间、Trace的ID、当前Span的ID、父Span的ID等能够满足追踪需要的信息。
事实上每一次Trace都是由若干个有顺序、有层级关系的Span所组成一颗“追踪树”Trace Tree如下图所示
Trace和Spans图片来源于Dapper论文
那么这样来看,我们就可以从下面两个角度来观察分布式追踪的特征:
从目标来看链路追踪的目的是为排查故障和分析性能提供数据支持系统在对外提供服务的过程中持续地接受请求并处理响应同时持续地生成Trace按次序整理好Trace中每一个Span所记录的调用关系就能绘制出一幅系统的服务调用拓扑图了。
这样根据拓扑图中Span记录的时间信息和响应结果正常或异常返回我们就可以定位到缓慢或者出错的服务然后将Trace与历史记录进行对比统计就可以从系统整体层面分析服务性能定位性能优化的目标。
从实现来看为每次服务调用记录Trace和Span并以此构成追踪树结构看起来好像也不是很复杂。然而考虑到实际情况追踪系统在功能性和非功能性上都有不小的挑战。
功能上的挑战来源于服务的异构性,各个服务可能会采用不同的程序语言,服务间的交互也可能会采用不同的网络协议,每兼容一种场景,都会增加功能实现方面的工作量。
而非功能性的挑战,具体就来源于以下这四个方面:
低性能损耗:分布式追踪不能对服务本身产生明显的性能负担。追踪的主要目的之一就是为了寻找性能缺陷,越慢的服务就越是需要追踪,所以工作场景都是性能敏感的地方。
对应用透明:追踪系统通常是运维期才事后加入的系统,所以应该尽量以非侵入或者少侵入的方式来实现追踪,对开发人员做到透明化。
随应用扩缩:现代的分布式服务集群都有根据流量压力自动扩缩的能力,这就要求当业务系统扩缩时,追踪系统也能自动跟随,不需要运维人员人工参与。
持续的监控即要求追踪系统必须能够7x24小时工作否则就难以定位到系统偶尔抖动的行为。
所以总而言之分布式追踪的主要需求是如何围绕着一个服务调用过程中的Trace和Span来低损耗、高透明度地收集信息不管是狭义还是广义的链路追踪系统都要包含数据收集的工作这是可以说是追踪系统的核心。那么接下来我们就来了解下三种主流的数据收集方式。
数据收集的三种主流实现方式
目前追踪系统根据数据收集方式的差异可以分为三种主流的实现方式分别是基于日志的追踪Log-Based Tracing基于服务的追踪Service-Based Tracing和基于边车代理的追踪Sidecar-Based Tracing
基于日志的追踪
基于日志的追踪思路是将Trace、Span等信息直接输出到应用日志中然后随着所有节点的日志归集过程汇聚到一起再从全局日志信息中反推出完整的调用链拓扑关系。日志追踪对网络消息完全没有侵入性对应用程序只有很少量的侵入性对性能的影响也非常低。
但这种实现方式的缺点是直接依赖于日志归集过程,日志本身不追求绝对的连续与一致,这就导致了基于日志的追踪,往往不如其他两种追踪实现来的精准。
还有一个问题是由于业务服务的调用与日志的归集并不是同时完成的也通常不由同一个进程完成有可能发生业务调用已经顺利结束了但由于日志归集不及时或者精度丢失导致日志出现延迟或缺失记录进而产生追踪失真的情况。这也正是我在上节课介绍Elastic Stack时提到的观点ELK在日志、追踪和度量方面都可以发挥作用这对中小型应用确实能起到一定的便利作用但对于大型系统来说最好还是由专业的工具来做专业的事。
日志追踪的代表产品是Spring Cloud Sleuth下面是一段由Sleuth在调用时自动生成的日志记录你可以从中观察到TraceID、SpanID、父SpanID等追踪信息。
# 以下为调用端的日志输出:
Created new Feign span [Trace: cbe97e67ce162943, Span: bb1798f7a7c9c142, Parent: cbe97e67ce162943, exportable:false]
2019-06-30 09:43:24.022 [http-nio-9010-exec-8] DEBUG o.s.c.s.i.web.client.feign.TraceFeignClient - The modified request equals GET http://localhost:9001/product/findAll HTTP/1.1
X-B3-ParentSpanId: cbe97e67ce162943
X-B3-Sampled: 0
X-B3-TraceId: cbe97e67ce162943
X-Span-Name: http:/product/findAll
X-B3-SpanId: bb1798f7a7c9c142
# 以下为服务端的日志输出:
[findAll] to a span [Trace: cbe97e67ce162943, Span: bb1798f7a7c9c142, Parent: cbe97e67ce162943, exportable:false]
Adding a class tag with value [ProductController] to a span [Trace: cbe97e67ce162943, Span: bb1798f7a7c9c142, Parent: cbe97e67ce162943, exportable:false]
基于服务的追踪
基于服务的追踪是目前最为常见的追踪实现方式被Zipkin、SkyWalking、Pinpoint等主流追踪系统广泛采用。服务追踪的实现思路是通过某些手段给目标应用注入追踪探针Probe比如针对Java应用一般就是通过Java Agent注入的。
探针在结构上可以看作是一个寄生在目标服务身上的小型微服务系统它一般会有自己专用的服务注册、心跳检测等功能有专门的数据收集协议可以把从目标系统中监控得到的服务调用信息通过另一次独立的HTTP或者RPC请求发送给追踪系统。
因此,基于服务的追踪会比基于日志的追踪消耗更多的资源,也具有更强的侵入性,而换来的收益就是追踪的精确性与稳定性都有所保证,不必再依靠日志归集来传输追踪数据。
这里我放了一张Pinpoint的追踪效果截图从图中可以看到参数、变量等相当详细的方法级调用信息。不知道你还记不记得在上节课“日志分析”里我把“打印追踪诊断信息”列为了反模式并提到如果需要诊断方法参数、返回值、上下文信息或者方法调用耗时这类数据通过追踪系统来实现会是比通过日志系统实现更加恰当的解决方案。
Pinpoint的追踪截图
另外我也必须给你说明清楚像图例中的Pinpoint这种详细程度的追踪对应用系统的性能压力是相当大的一般仅在除错时开启而且Pinpoint本身就是比较重负载的系统运行它必须先维护一套HBase这其实就严重制约了它的适用范围。目前服务追踪的其中一个发展趋势是轻量化国产的SkyWalking正是这方面的佼佼者。
基于边车代理的追踪
基于边车代理的追踪是服务网格的专属方案也是最理想的分布式追踪模型它对应用完全透明无论是日志还是服务本身都不会有任何变化它与程序语言无关无论应用是采用什么编程语言来实现的只要它还是通过网络HTTP或者gRPC来访问服务就可以被追踪到它也有自己独立的数据通道追踪数据通过控制平面进行上报避免了追踪对程序通信或者日志归集的依赖和干扰保证了最佳的精确性。
而如果要说这种追踪实现方式还有什么缺点的话,那就是服务网格现在还不够普及。当然未来随着云原生的发展,相信它会成为追踪系统的主流实现方式之一。
还有一点就是边车代理本身对应用透明的工作原理决定了它只能实现服务调用层面的追踪像前面Pinpoint截图那样的本地方法调用级别的追踪诊断边车代理是做不到的。
现在市场占有率最高的边车代理Envoy就提供了相对完善的追踪功能但没有提供自己的界面端和存储端所以Envoy和Sleuth一样都属于狭义的追踪系统需要配合专门的UI与存储来使用。SkyWalking、Zipkin、Jaeger、LightStep Tracing等系统现在都可以接受来自于Envoy的追踪数据充当它的界面端。
不过,虽然链路追踪在数据的收集这方面,已经有了几种主流的实现方式,但各种追踪产品通常并不互通。接下来我们就具体看看追踪在行业标准与规范方面存在的问题。
追踪规范化
要知道,比起日志与度量,追踪这个领域的产品竞争要相对激烈得多。
一方面在这个领域内目前还没有像日志、度量那样出现具有明显统治力的产品仍处于群雄混战的状态。另一方面现在几乎市面上所有的追踪系统都是以Dapper的论文为原型发展出来的基本都算是同门师兄弟在功能上并没有太本质的差距却又受制于实现细节彼此互斥很难搭配工作。
之所以出现这种局面我觉得只能怪当初Google发表的Dapper只是论文而不是有约束力的规范标准它只提供了思路并没有规定细节。比如该怎样进行埋点、Span上下文具体该有什么数据结构、怎样设计追踪系统与探针或者界面端的API接口等等这些都没有权威的规定。
因此为了推进追踪领域的产品标准化2016年11月CNCF技术委员会接受了OpenTracing作为基金会的第三个项目。OpenTracing是一套与平台无关、与厂商无关、与语言无关的追踪协议规范只要遵循OpenTracing规范任何公司的追踪探针、存储、界面都可以随时切换也可以相互搭配使用。
在操作层面OpenTracing只是制定了一个很薄的标准化层位于应用程序与追踪系统之间这样探针与追踪系统只要都支持OpenTracing协议就算它们不是同一个厂商的产品那也可以互相通讯。此外OpenTracing还规定了微服务之间在发生调用时应该如何传递Span信息OpenTracing Payload
关于这几点,我们具体可以参考下图例中的绿色部分:
符合OpenTracing的软件架构
如此一来在OpenTracing规范公布后几乎所有业界有名的追踪系统比如Zipkin、Jaeger、SkyWalking等都很快宣布支持OpenTracing。
但谁也没想到的是Google自己却在这个时候出来表示反对并提出了与OpenTracing目标类似的OpenCensus规范随后又得到了巨头Microsoft的支持和参与。
OpenCensus不仅涉及到追踪还把指标度量也纳入了进来而在内容上它不仅涉及到规范制定还把数据采集的探针和收集器都一起以SDK目前支持五种语言的形式提供出来了。
这样OpenTracing和OpenCensus就迅速形成了可观测性的两大阵营一边是在这方面深耕多年的众多老牌APM系统厂商另一边是分布式追踪概念的提出者Google以及与Google同样庞大的Microsoft。
所以,对追踪系统的规范化工作,也并没有平息厂商竞争的混乱,反倒是把水搅得更浑了。
不过正当群众们买好西瓜搬好板凳的时候2019年OpenTracing和OpenCensus又忽然宣布握手言和它们共同发布了可观测性的终极解决方案OpenTelemetry并宣布会各自冻结OpenTracing和OpenCensus的发展。
OpenTelemetry的野心很大它不仅包括了追踪规范还包括了日志和度量方面的规范、各种语言的SDK以及采集系统的参考实现。距离一个完整的追踪与度量系统只是差了一个界面端和指标预警这些会与用户直接接触的后端功能OpenTelemetry“大度”地把它们留给具体产品去实现勉强算是没有对一众APM厂商赶尽杀绝留了一条活路。
可以说OpenTelemetry一诞生就带着无比炫目的光环直接进入CNCF的孵化项目它的目标是统一追踪、度量和日志三大领域目前主要关注的是追踪和度量在日志方面官方表示将放到下一阶段再去处理。不过OpenTelemetry毕竟是2019年才出现的新生事物尽管背景渊源深厚前途光明但未来究竟如何发展能否打败现在已有的众多成熟系统目前仍然言之尚早。
小结
这节课我给你介绍了分布式追踪里“追踪”与“跨度”两个概念要知道目前几乎所有的追踪工具都是围绕这两个Dapper提出的概念所设计的因此理解它们的含义对你使用任何一款追踪工具都会有帮助。
而在理论之外,我还讲解了三种追踪数据收集的实现方式,分别是基于日志、基于服务、基于边车代理的追踪,你可以重点关注下这几种方式各自的优势和缺点,以此在工作实践中选择合适的追踪方式。
一课一思
在你所负责的产品中,有引入链路追踪工具吗?如果有,是哪一款?达到你的期望了吗?如果没有,你是如何解决应用运行过程中的除错、性能分析等问题的呢?
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给其他的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,192 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
44 _ 聚合度量能给我们解决什么问题?
你好,我是周志明。这节课我们来探讨“可观测性”这个小章节的最后一个话题:聚合度量。
度量Metrics的目的是揭示系统的总体运行状态。相信你可能在一些电影里见过这样的场景舰船的驾驶舱或者卫星发射中心的控制室处在整个房间最显眼的位置布满整面墙壁的巨型屏幕里显示着一个个指示器、仪表板与统计图表沉稳端坐中央的指挥官看着屏幕上闪烁变化的指标果断决策下达命令……
而如果以上场景被改成指挥官双手在键盘上飞舞,双眼紧盯着日志或者追踪系统,试图判断出系统工作是否正常。这光想像一下,你都能感觉到一股身份与行为不一致的违和气息,由此可见度量与日志、追踪的差别。
简单来说,度量就是用经过聚合统计后的高维度信息,以最简单直观的形式来总结复杂的过程,为监控、预警提供决策支持。
我们大多数人的人生经历可能都会比较平淡没有驾驶航母的经验甚至连一颗卫星或者导弹都没有发射过那就只好打开电脑按CTRL+ALT+DEL呼出任务管理器看看下面这个熟悉的界面它也是一个非常具有代表性的度量系统。
Windows系统的任务管理器界面
在总体上,度量可以分为客户端的指标收集、服务端的存储查询以及终端的监控预警三个相对独立的过程,每个过程在系统中一般也会设置对应的组件来实现。
那么现在呢你不妨先来看一下我在后面举例时会用到的Prometheus组件流程图图中Prometheus Server左边的部分都属于客户端过程而右边的部分就属于终端过程。
虽然说Prometheus在度量领域的统治力暂时还不如日志领域中Elastic Stack的统治地位那么稳固但在云原生时代里它基本已经算得上是事实标准了。所以接下来我就主要以Prometheus为例给你介绍这三部分组件的总体思路、大致内容与理论标准。
指标收集
我们先来了解下客户端指标收集部分的核心思想。这一部分主要是解决两个问题:“如何定义指标”以及“如何将这些指标告诉服务端”。
如何定义指标?
首先我们来聊聊“如何定义指标”这个问题。乍一看你可能会觉得它应该是与目标系统密切相关的,必须根据实际情况才能讨论,但其实并不绝对。
要知道无论目标是何种系统它都具备了一些共性特征虽然在确定目标系统前我们无法决定要收集什么指标但指标的数据类型Metrics Types是可数的所有通用的度量系统都是面向指标的数据类型来设计的现在我就来一一给你解读下
计数度量器Counter这是最好理解也是最常用的指标形式计数器就是对有相同量纲、可加减数值的合计量。比如业务指标像销售额、货物库存量、职工人数等技术指标像服务调用次数、网站访问人数等它们都属于计数器指标。
瞬态度量器Gauge瞬态度量器比计数器更简单它就表示某个指标在某个时点的数值连加减统计都不需要。比如当前Java虚拟机堆内存的使用量这就是一个瞬态度量器再比如网站访问人数是计数器而网站在线人数则是瞬态度量器。
吞吐率度量器Meter顾名思义它是用于统计单位时间的吞吐量即单位时间内某个事件的发生次数。比如在交易系统中常以TPS衡量事务吞吐率即每秒发生了多少笔事务交易再比如港口的货运吞吐率常以“吨/每天”为单位计算10万吨/天的港口通常要比1万吨/天的港口的货运规模更大。
直方图度量器Histogram直方图就是指常见的二维统计图它的两个坐标分别是统计样本和该样本对应的某个属性的度量以长条图的形式记录具体数值。比如经济报告中要衡量某个地区历年的GDP变化情况常会以GDP为纵坐标、时间为横坐标构成直方图来呈现。
采样点分位图度量器Quantile Summary分位图是统计学中通过比较各分位数的分布情况的工具主要用来验证实际值与理论值的差距评估理论值与实际值之间的拟合度。比如我们说“高考成绩一般符合正态分布”这句话的意思就是高考成绩高低分的人数都比较少中等成绩的比较多按不同分数段来统计人数得出的统计结果一般能够与正态分布的曲线较好地拟合。
除了以上常见的度量器之外还有Timer、Set、Fast Compass、Cluster Histogram等其他各种度量器采用不同的度量系统支持度量器类型的范围肯定会有所差别比如Prometheus就支持了上面提到的五种度量器中的Counter、Gauge、Histogram和Summary四种。
如何将这些指标告诉服务端?
然后是针对“如何将这些指标告诉服务端”这个问题它通常有两种解决方案拉取式采集Pull-Based Metrics Collection和推送式采集Push-Based Metrics Collection
所谓Pull是指度量系统主动从目标系统中拉取指标相对地Push就是由目标系统主动向度量系统推送指标。
这两种方式实际上并没有绝对的好坏优劣以前很多老牌的度量系统比如Ganglia、Graphite、StatsD等是基于Push的而以Prometheus、Datadog、Collectd为代表的另一派度量系统则青睐Pull式采集Prometheus官方解释选择Pull的原因。另外你也要知道对于是要选择Push还是Pull不仅是在度量中才有所有涉及到客户端和服务端通讯的场景都会涉及到该谁主动的问题上一节课讲的追踪系统也是如此。
不过一般来说,度量系统只会支持其中一种指标采集方式,这是因为度量系统的网络连接数量,以及对应的线程或者协程数可能非常庞大,如何采集指标将直接影响到整个度量系统的架构设计。
然而Prometheus在基于Pull架构的同时还能够有限度地兼容Push式采集这是为啥呢原因是它有Push Gateway的存在。
如下图所示这是一个位于Prometheus Server外部的相对独立的中介模块它会把外部推送来的指标放到Push Gateway中暂存然后再等候Prometheus Server从Push Gateway中去拉取。
Prometheus组件流程图
Prometheus设计Push Gateway的本意是为了解决Pull的一些固有缺陷比如目标系统位于内网需要通过NAT访问外网而外网的Prometheus是无法主动连接目标系统的这就只能由目标系统主动推送数据又比如某些小型短生命周期服务可能还等不及Prometheus来拉取服务就已经结束运行了因此也只能由服务自己Push来保证度量的及时和准确。
而在由Push和Pull决定完该谁主动以后另一个问题就是指标应该通过怎样的网络访问协议、取数接口、数据结构来获取呢
跟计算机科学中其他类似的问题一样人们一贯的解决方向是“定义规范”应该由行业组织和主流厂商一起协商出专门用于度量的协议目标系统按照协议与度量系统交互。比如说网络管理中的SNMP、Windows硬件的WMI以及第41讲中提到的Java的JMX都属于这种思路的产物。
但是,定义标准这个办法在度量领域中其实没有那么有效,前面列举的这些度量协议,只是在特定的一小块领域里流行过。
要究其原因的话一方面是因为业务系统要使用这些协议并不容易你可以想像一下让订单金额存到SNMP中让Golang的系统把指标放到JMX Bean里即便技术上可行这也不像是正常程序员会干的事而另一方面度量系统又不会甘心局限于某个领域成为某项业务的附属品。
另外我们也要明确一个事实度量面向的是广义上的信息系统它横跨存储日志、文件、数据库、通讯消息、网络、中间件HTTP服务、API服务直到系统本身的业务指标甚至还会包括度量系统本身部署两个独立的Prometheus互相监控是很常见的
所以,这些度量协议其实都没有成为最正确答案的希望。
如此一来既然没有了标准有一些度量系统比如老牌的Zabbix就选择同时支持了SNMP、JMX、IPMI等多种不同的度量协议。而另一些以Prometheus为代表的度量系统就相对强硬它们不支持任何一种协议只允许通过HTTP访问度量端点这一种访问方式。如果目标提供了HTTP的度量端点如Kubernetes、Etcd等本身就带有Prometheus的Client Library就直接访问否则就需要一个专门的Exporter来充当媒介。
这里的Exporter是Prometheus提出的概念它是目标应用的代表它既可以独立运行也可以与应用运行在同一个进程中只要集成Prometheus的Client Library就可以了。
Exporter的作用就是以HTTP协议Prometheus在2.0版本之前支持过Protocol Buffer目前已不再支持返回符合Prometheus格式要求的文本数据给Prometheus服务器。得益于Prometheus的良好社区生态现在已经有大量、各种用途的Exporter让Prometheus的监控范围几乎能涵盖到所有用户关心的目标绝大多数用户都只需要针对自己系统业务方面的度量指标编写Exporter即可。你可以参考下这里给出的表格
另外顺便一提在前面我提到了一堆没有希望成为最终答案的协议比如SNMP、WMI等等。不过现在一种名为OpenMetrics的度量规范正逐渐从Prometheus的数据格式中分离出来有望成为监控数据格式的国际标准最终结果究竟如何要看Prometheus本身的发展情况还有OpenTelemetry与OpenMetrics的关系如何协调。
存储查询
好,那么当指标从目标系统采集过来了之后,就应该存储在度量系统中,以便被后续的分析界面、监控预警所使用。
存储数据对于计算机软件来说其实是司空见惯的操作,但如果用传统关系数据库的思路来解决度量系统的存储,效果可能不会太理想。
我举个例子假设你要建设一个中等规模、有着200个节点的微服务系统每个节点要采集的存储、网络、中间件和业务等各种指标加一起也按200个来计算监控的频率如果按秒为单位的话一天时间内就会产生超过34亿条记录而对于这个结果你可能会感到非常意外
200节点× 200指标× 86400= 3,456,000,000记录
因为在实际情况中大多数这种200节点规模的系统本身一天的生产数据都远到不了34亿条那么建设度量系统肯定不能让度量反倒成了业务系统的负担。可见度量的存储是需要专门研究解决的问题。至于具体要如何解决让我们先来观察一段Prometheus的真实度量数据吧
{
// 时间戳
"timestamp": 1599117392,
// 指标名称
"metric": "total_website_visitors",
// 标签组
"tags": {
"host": "icyfenix.cn",
"job": "prometheus"
},
// 指标值
"value": 10086
}
通过观察,我们可以发现这段度量数据的特征:每一个度量指标由时间戳、名称、值和一组标签构成,除了时间之外,指标不与任何其他因素相关。
当然指标的数据总量固然是不小的但它没有嵌套、没有关联、没有主外键不必关心范式和事务这些就都是可以针对性优化的地方。事实上业界也早就有了专门针对该类型数据的数据库即“时序数据库”Time Series Database
额外知识:时序数据库-
时序数据库是用于存储跟随时间而变化的数据,并且以时间(时间点或者时间区间)来建立索引的数据库。-
时序数据库最早是应用于工业(电力行业、化工行业)应用的各类型实时监测、检查与分析设备所采集、产生的数据,这些工业数据的典型特点是产生频率快(每一个监测点一秒钟内可产生多条数据)、严重依赖于采集时间(每一条数据均要求对应唯一的时间)、测点多信息量大(常规的实时监测系统均可达到成千上万的监测点,监测点每秒钟都在产生数据)。-
时间序列数据是历史烙印,它具有不变性、唯一性、有序性。时序数据库同时具有数据结构简单、数据量大的特点。
我们应该注意到存储数据库在写操作时时序数据通常只是追加很少删改或者根本不允许删改。因此针对数据热点只集中在近期数据、多写少读、几乎不删改、数据只顺序追加等特点时序数据库被允许可以做出很激进的存储、访问和保留策略Retention Policies
以日志结构的合并树Log Structured Merge TreeLSM-Tree代替传统关系型数据库中的B+Tree作为存储结构LSM适合的应用场景就是写多读少且几乎不删改的数据。
设置激进的数据保留策略比如根据过期时间TTL自动删除相关数据以节省存储空间同时提高查询性能。对于普通的数据库来说数据会存储一段时间后被自动删除的这个做法可以说是不可想象的。
对数据进行再采样Resampling以节省空间比如最近几天的数据可能需要精确到秒而查询一个月前的数据只需要精确到天查询一年前的数据只要精确到周就够了这样将数据重新采样汇总就极大地节省了存储空间。
而除此之外时序数据库中甚至还有一种并不罕见却更加极端的形式叫做轮替型数据库Round Robin DatabaseRRD它是以环形缓冲在“服务端缓存”一节介绍过的思路实现只能存储固定数量的最新数据超期或超过容量的数据就会被轮替覆盖因此它也有着固定的数据库容量却能接受无限量的数据输入。
所以Prometheus服务端自己就内置了一个强大的时序数据库实现我说它“强大”并不是客气在DB-Engines中近几年它的排名就在不断提升目前已经跃居时序数据库排行榜的前三。
这个时序数据库提供了一个名为PromQL的数据查询语言能对时序数据进行丰富的查询、聚合以及逻辑运算。当然了某些时序库如排名第一的InfluxDB也会提供类SQL风格的查询但PromQL不是它是一套完全由Prometheus自己定制的数据查询DSL写起来的风格有点像带运算与函数支持的CSS选择器。比如我要查找网站icyfenix.cn的访问人数的话会是如下写法
// 查询命令:
total_website_visitors{host=“icyfenix.cn”}
// 返回结果:
total_website_visitors{host=“icyfenix.cn”,job="prometheus"}=(10086)
这样通过PromQL就可以轻易实现指标之间的运算、聚合、统计等操作在查询界面中也往往需要通过PromQL计算多种指标的统计结果才能满足监控的需要语法方面的细节我就不详细展开了具体你可以参考Prometheus的文档手册。
最后我还想补充说明一下时序数据库对度量系统来说确实是很合适的选择但并不是说绝对只有用时序数据库才能解决度量指标的存储问题Prometheus流行之前最老牌的度量系统Zabbix用的就是传统关系数据库来存储指标。
好,接下来我们继续探讨聚合度量的第三个过程:终端的监控预警。
监控预警
首先要知道,指标度量是手段,而最终目的是要做分析和预警。
界面分析和监控预警是与用户更加贴近的功能模块但对度量系统本身而言它们都属于相对外围的功能。与追踪系统的情况类似广义上的度量系统由面向目标系统进行指标采集的客户端Client与目标系统进程在一起的Agent或者代表目标系统的Exporter等都可归为客户端负责调度、存储和提供查询能力的服务端ServerPrometheus的服务端是带存储的但也有很多度量服务端需要配合独立的存储来使用以及面向最终用户的终端BackendUI界面、监控预警功能等都归为终端组成而狭义上的度量系统就只包括客户端和服务端不包含终端。
那么按照定义Prometheus应该算是处于狭义和广义的度量系统之间尽管它确实内置了一个界面解决方案“Console Template”以模版和JavaScript接口的形式提供了一系列预设的组件菜单、图表等让用户编写一段简单的脚本就可以实现可用的监控功能。不过这种可用程度往往不足以支撑正规的生产部署只能说是为把度量功能嵌入到系统的某个子系统中提供了一定的便利。
因而在生产环境下大多是Prometheus配合Grafana来进行展示的这是Prometheus官方推荐的组合方案。但该组合也并非唯一的选择如果你要搭配Klbana甚至SkyWalking8.x版之后的SkyWalking支持从Prometheus获取度量数据来使用也都是完全可行的。
另外,良好的可视化能力对于提升度量系统的产品力也非常重要,长期趋势分析(比如根据对磁盘增长趋势的观察,判断什么时候需要扩容)、对照分析(比如版本升级后对比新旧版本的性能、资源消耗等方面的差异)、故障分析(不仅从日志、追踪自底向上可以分析故障,高维度的度量指标也可能自顶向下寻找到问题的端倪)等分析工作,既需要度量指标的持续收集、统计,往往还需要对数据进行可视化,这样才能让人更容易地从数据中挖掘规律,毕竟数据最终还是要为人类服务的。
而除了为分析、决策、故障定位等提供支持的用户界面外度量信息的另一种主要的消费途径就是用来做预警。比如你希望当磁盘消耗超过90%时,给你发送一封邮件或者是一条微信消息,通知管理员过来处理,这就是一种预警。
Prometheus提供了专门用于预警的Alert Manager我们将Alert Manager与Prometheus关联后可以设置某个指标在多长时间内、达到何种条件就会触发预警状态在触发预警后Alert Manager就会根据路由中配置的接收器比如邮件接收器、Slack接收器、微信接收器或者更通用的WebHook接收器等来自动通知我们。
小结
今天是“可观测性”章节的最后一节课可观测性作为控制理论中的一个概念从1960年代起就已经存在了虽然它针对信息系统和分布式服务的适用性是最近若干年中新发现的但在某种程度上这也算是过去20年对这些系统的监控方式的演变产物。
那么学完了今天这节课,你需要记住一个要点,即传统监控和可观测性之间的关键区别在于:可观测性是系统或服务内在的固有属性,而不是在系统之外对系统所做出的额外增强,后者是传统监控的处理思路。
除此之外,构建具有可观测性的服务,也是构建健壮服务不可缺少的属性,这是分布式系统架构师的职责。那么作为服务开发者和设计者,我们应该在其建设期间,就要设想控制系统会发出哪些信号、如何接收和存储这些信号,以及如何使用它们,以确保在用户能在受到影响之前了解问题、能使用度量数据来更好地了解系统的健康状况和状态。
一课一思
在你设计系统时,是否考虑过要对外部暴露哪些可观测的属性?你通常会暴露哪些数据?是以什么方式暴露的呢?欢迎在留言区分享你的见解和做法。
如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,118 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
45 _ 模块导学:从微服务到云原生
你好,我是周志明。
上一个模块,我们以“分布式的基石”为主题,了解并学习了微服务中的关键技术问题与解决方案。实际上,解决这些技术问题,原本就是我们架构师和程序员的本职工作。
而从今天开始,我们就进入了一个全新的模块,从微服务走到了云原生。在这个模块里,我会围绕“不可变基础设施”的相关话题,以容器、编排系统和服务网格的发展为主线,给你介绍虚拟化容器与服务网格是如何模糊掉软件与硬件之间的界限,如何在基础设施与通讯层面上帮助微服务隐藏复杂性,以此解决原本只能由程序员通过软件编程来解决的分布式问题。
什么是不可变基础设施?
“不可变基础设施”这个概念由来已久。2012年马丁 · 福勒Martin Fowler设想的“凤凰服务器”与2013年查德 · 福勒Chad Fowler正式提出的“不可变基础设施”都阐明了基础设施不变性给我们带来的好处。
而在云原生基金会定义的“云原生”概念中,“不可变基础设施”提升到了与微服务平级的重要程度。此时,它的内涵已经不再局限于只是方便运维、程序升级和部署的手段,而是升华为了向应用代码隐藏分布式架构复杂度、让分布式架构得以成为一种能够普遍推广的普适架构风格的必要前提。
云原生定义Cloud Native Definition-
Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach.-
These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil.-
云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。-
这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。-
—— Cloud Native Definition, CNCF2018
不过,不可变基础设施是一种抽象的概念,不太容易直接对它分解描述,所以为了能把云原生这个本来就比较抽象的架构思想落到实处,我选择从我们都比较熟悉的,至少是能看得见、摸得着的容器化技术开始讲起。
虚拟化的目标与类型
容器是云计算、微服务等诸多软件业界核心技术的共同基石。容器的首要目标是让软件分发部署的过程,从传统的发布安装包、靠人工部署,转变为直接发布已经部署好的、包含整套运行环境的虚拟化镜像。
在容器技术成熟之前,主流的软件部署过程是由系统管理员编译或下载好二进制安装包,根据软件的部署说明文档,准备好正确的操作系统、第三方库、配置文件、资源权限等各种前置依赖以后,才能将程序正确地运行起来。
这样做当然是非常麻烦的Chad Fowler在提出“不可变基础设施”这个概念的文章《Trash Your Servers and Burn Your Code》里开篇就直接吐槽要把一个不知道打过多少个升级补丁不知道经历了多少任管理员的系统迁移到其他机器上毫无疑问会是一场灾难。
另外我们也知道让软件能够在任何环境、任何物理机器上达到“一次编译到处运行”曾经是Java早年的宣传口号不过这并不是一个简单的目标不设前提的“到处运行”仅靠Java语言和Java虚拟机是不可能达成的。因为一个计算机软件要能够正确运行需要通过以下三方面的兼容性来共同保障这里仅讨论软件兼容性不去涉及“如果没有摄像头就无法运行照相程序”这类问题
ISA兼容目标机器指令集兼容性比如ARM架构的计算机无法直接运行面向x86架构编译的程序。
ABI兼容目标系统或者依赖库的二进制兼容性比如Windows系统环境中无法直接运行Linux的程序又比如DirectX 12的游戏无法运行在DirectX 9之上。
环境兼容:目标环境的兼容性,比如没有正确设置的配置文件、环境变量、注册中心、数据库地址、文件系统的权限等等,当任何一个环境因素出现错误,都会让你的程序无法正常运行。
额外知识ISA与ABI-
指令集架构Instruction Set ArchitectureISA是计算机体系结构中与程序设计有关的部分包含了基本数据类型、指令集、寄存器、寻址模式、存储体系、中断、异常处理以及外部I/O。指令集架构包含一系列的Opcode操作码即通常所说的机器语言以及由特定处理器执行的基本命令。-
应用二进制接口Application Binary InterfaceABI是应用程序与操作系统之间或其他依赖库之间的低级接口。ABI涵盖了各种底层细节如数据类型的宽度大小、对象的布局、接口调用约定等等。ABI不同于应用程序接口Application Programming InterfaceAPIAPI定义的是源代码和库之间的接口因此同样的代码可以在支持这个API的任何系统中编译而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能直接运行。
这里我把使用仿真Emulation以及虚拟化Virtualization技术来解决以上三项兼容性问题的方法都统称为虚拟化技术。那么根据抽象目标与兼容性高低的不同虚拟化技术又分为了五类下面我们就分别来看看
指令集虚拟化ISA Level Virtualization
即通过软件来模拟不同ISA架构的处理器工作过程它会把虚拟机发出的指令转换为符合本机ISA的指令代表为QEMU和Bochs。
指令集虚拟化就是仿真它提供了几乎完全不受局限的兼容性甚至能做到直接在Web浏览器上运行完整操作系统这种令人惊讶的效果。但是由于每条指令都要由软件来转换和模拟它也是性能损失最大的虚拟化技术。
硬件抽象层虚拟化Hardware Abstraction Level Virtualization
即以软件或者直接通过硬件来模拟处理器、芯片组、内存、磁盘控制器、显卡等设备的工作过程。
硬件抽象层虚拟化既可以使用纯软件的二进制翻译来模拟虚拟设备也可以由硬件的Intel VT-d、AMD-Vi这类虚拟化技术将某个物理设备直通Passthrough到虚拟机中使用代表为VMware ESXi和Hyper-V。这里你可以知道的是如果没有预设语境一般人们所说的“虚拟机”就是指这一类虚拟化技术。
操作系统层虚拟化OS Level Virtualization
无论是指令集虚拟化还是硬件抽象层虚拟化都会运行一套完全真实的操作系统来解决ABI兼容性和环境兼容性的问题虽然ISA兼容性是虚拟出来的但ABI兼容性和环境兼容性却是真实存在的。
而操作系统层虚拟化则不会提供真实的操作系统,而是会采用隔离手段,使得不同进程拥有独立的系统资源和资源配额,这样看起来它好像是独享了整个操作系统一般,但其实系统的内核仍然是被不同进程所共享的。
操作系统层虚拟化的另一个名字就是这个模块的主角“容器化”Containerization。所以由此可见容器化仅仅是虚拟化的一个子集它只能提供操作系统内核以上的部分ABI兼容性与完整的环境兼容性。
而这就意味着如果没有其他虚拟化手段的辅助在Windows系统上是不可能运行Linux的Docker镜像的现在可以是因为有其他虚拟机或者WSL2的支持反之亦然。另外这也同样决定了如果Docker宿主机的内核版本是Linux Kernel 5.6那无论上面运行的镜像是Ubuntu、RHEL、Fedora、Mint或者是其他任何发行版的镜像看到的内核一定都是相同的Linux Kernel 5.6。
容器化牺牲了一定的隔离性与兼容性,换来的是比前两种虚拟化更高的启动速度、运行性能和更低的执行负担。
运行库虚拟化Library Level Virtualization
与操作系统虚拟化采用隔离手段来模拟系统不同,运行库虚拟化选择使用软件翻译的方法来模拟系统,它是以一个独立进程来代替操作系统内核,来提供目标软件运行所需的全部能力。
那么这种虚拟化方法获得的ABI兼容性高低就取决于软件能不能足够准确和全面地完成翻译工作它的代表为WINEWine Is Not an Emulator的缩写一款在Linux下运行Windows程序的软件和WSL特指Windows Subsystem for Linux Version 1
语言层虚拟化Programming Language Level Virtualization
即由虚拟机将高级语言生成的中间代码转换为目标机器可以直接执行的指令代表为Java的JVM和.NET的CLR。
不过虽然各大厂商肯定都会提供在不同系统下接口都相同的标准库但本质上这种虚拟化技术并不是直接去解决任何ABI兼容性和环境兼容性的问题而是将不同环境的差异抽象封装成统一的编程接口供程序员使用。
小结
作为整个模块的开篇,我们这节课的学习目的是要明确软件运行的“兼容性”指的是什么,以及要能理解我们经常能听到的“虚拟化”概念指的是什么。只有理清了这些概念、统一了语境,在后续的课程学习中,我们关于容器、编排、云原生等的讨论,才不会产生太多的歧义。
一课一思
这节课介绍的五种层次的虚拟化技术,有哪些是你在实际工作中真正用过的?你是用来达成什么目的呢?
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给其他的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,87 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
46 _ 容器的崛起(上):文件、访问、资源的隔离
你好,我是周志明。接下来的两节课,我会以容器化技术的发展为线索,带你从隔离与封装两个角度,去学习和了解容器技术。
今天我们就先来学习下Linux系统中隔离技术的发展历程以此为下节课理解“以容器封装应用”的思想打好前置基础。
隔离文件chroot
首先要知道,人们使用容器的最初目的,并不是为了部署软件,而是为了隔离计算机中的各类资源,以便降低软件开发、测试阶段可能产生的误操作风险,或者是专门充当蜜罐,吸引黑客的攻击,以便监视黑客的行为。
容器的起点呢可以追溯到1979年Version 7 UNIX系统中提供的chroot命令这个命令是英文单词“Change Root”的缩写它所具备的功能是当某个进程经过chroot操作之后它的根目录就会被锁定在命令参数所指定的位置以后它或者它的子进程就不能再访问和操作该目录之外的其他文件。
1991年世界上第一个监控黑客行动的蜜罐程序就是使用chroot来实现的那个参数指定的根目录当时被作者被戏称为“Chroot监狱”Chroot Jail而黑客突破chroot限制的方法就叫做Jailbreak。后来FreeBSD 4.0系统重新实现了chroot命令把它作为系统中进程沙箱隔离的基础并将其命名为FreeBSD jail。
再后来苹果公司又以FreeBSD为基础研发出了举世闻名的iOS操作系统此时黑客们就把绕过iOS沙箱机制以root权限任意安装程序的方法称为“越狱”Jailbreak当然这些故事都是题外话了。
到了2000年Linux Kernel 2.3.41版内核引入了pivot_root技术来实现文件隔离pivot_root直接切换了根文件系统rootfs有效地避免了chroot命令可能出现的安全性漏洞。咱们课程后面要提到的容器技术比如LXC、Docker等等也都是优先使用pivot_root来实现根文件系统切换的。
不过时至今日chroot命令依然活跃在Unix系统以及几乎所有主流的Linux发行版中同时也以命令行工具chroot(8)或者系统调用chroot(2) 的形式存在着但无论是chroot命令还是pivot_root它们都不能提供完美的隔离性。
其实原本按照Unix的设计哲学一切资源都可以视为文件In UNIXEverything is a File一切处理都可以视为对文件的操作在理论上应该是隔离了文件系统就可以安枕无忧才对。
可是哲学归哲学现实归现实从硬件层面暴露的低层次资源比如磁盘、网络、内存、处理器再到经操作系统层面封装的高层次资源比如UNIX分时UNIX Time-SharingUTS、进程IDProcess IDPID、用户IDUser IDUID、进程间通信Inter-Process CommunicationIPC等等都存在着大量以非文件形式暴露的操作入口。
所以我才会说以chroot为代表的文件隔离仅仅是容器崛起之路的起点而已。
隔离访问namespaces
那么到了2002年Linux Kernel 2.4.19版内核引入了一种全新的隔离机制Linux名称空间Linux Namespaces
名称空间的概念在很多现代的高级程序语言中都存在它主要的作用是避免不同开发者提供的API相互冲突相信作为一名开发人员的你肯定不陌生。
Linux的名称空间是一种由内核直接提供的全局资源封装它是内核针对进程设计的访问隔离机制。进程在一个独立的Linux名称空间中朝系统看去会觉得自己仿佛就是这方天地的主人拥有这台Linux主机上的一切资源不仅文件系统是独立的还有着独立的PID编号比如拥有自己的0号进程即系统初始化的进程、UID/GID编号比如拥有自己独立的root用户、网络比如完全独立的IP地址、网络栈、防火墙等设置等等此时进程的心情简直不能再好了。
事实上Linux的名称空间是受“贝尔实验室九号项目”一个分布式操作系统“九号”项目并非代号操作系统的名字就叫“Plan 9 from Bell Labs”充满了赛博朋克风格的启发而设计的最初的目的依然只是为了隔离文件系统而不是为了什么容器化的实现。这点我们从2002年发布时Linux只提供了Mount名称空间并且其构造参数为“CLONE_NEWNS”即Clone New Namespace的缩写而非“CLONE_NEWMOUNT”就能看出一些端倪。
到了后来要求系统隔离其他访问操作的呼声就愈发强烈从2006年起内核陆续添加了UTS、IPC等名称空间隔离直到目前最新的Linux Kernel 5.6版内核为止Linux名称空间支持了以下八种资源的隔离内核的官网Kernel.org上仍然只列出了前六种从Linux的Man命令能查到全部八种
阅读链接补充:-
Hostname-
Domain names
如今对文件、进程、用户、网络等各类信息的访问都被囊括在Linux的名称空间中即使一些今天仍有没被隔离的访问比如syslog就还没被隔离容器内可以看到容器外其他进程产生的内核syslog在以后也可以跟随内核版本的更新纳入到这套框架之内。
现在,距离完美的隔离性就只差最后一步了:资源的隔离。
隔离资源cgroups
如果要让一台物理计算机中的各个进程看起来像独享整台虚拟计算机的话,不仅要隔离各自进程的访问操作,还必须能独立控制分配给各个进程的资源使用配额。不然的话,一个进程发生了内存溢出或者占满了处理器,其他进程就莫名其妙地被牵连挂起,这样肯定算不上是完美的隔离。
而Linux系统解决以上问题的方案就是控制群组Control Groups目前常用的简写为cgroups它与名称空间一样都是直接由内核提供的功能用于隔离或者说分配并限制某个进程组能够使用的资源配额。这里的资源配额包括了处理器时间、内存大小、磁盘I/O速度等等具体你可以参考下这里给出的表格
cgroups项目最早是由Google的工程师主要是Paul Menage和Rohit Seth在2006年发起的当时取的名字就叫做“进程容器”Process Containers不过“容器”Container这个名词的定义在那时候还没有今天那么清晰不同场景中常有不同的指向。
所以为了避免混乱到2007年这个项目才被重新命名为cgroups在2008年合并到了2.6.24版的内核后正式对外发布这一阶段的cgroups就被称为“第一代cgroups”。
后来在2016年3月发布的Linux Kernel 4.5中搭载了由Facebook工程师主要是Tejun Heo重新编写的“第二代cgroups”其关键改进是支持Unified Hierarchy这个功能可以让管理员更加清晰、精确地控制资源的层级关系。目前这两个版本的cgroups在Linux内核代码中是并存的不过在下节课我会给你介绍的封装应用Docker就暂时仅支持第一代的cgroups。
小结
这节课我给你介绍了容器技术和思想的起源chroot命令这是计算机操作系统中最早的成规模的隔离技术。
此外你现在也了解到了namespaces和cgroups对资源访问与资源配额的隔离它们不仅是容器化技术的基础在现代Linux操作系统中也已经成为了无可或缺的基石。理解了这些基础性的知识是学习和掌握下节课中讲解的容器应用即不同封装对象、封装思想的必要前提。
一课一思
请你思考一下Docker的起源依赖于Linux内核提供的隔离能力那Docker就是Linux专属的吗Windows系统中是否有文件、访问、资源的隔离手段是否存在Windows版本的容器运行时呢
提示你可以搜索关于Window Server Contianer的信息与LXCLinux Container对比一下。
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,198 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
47 _ 容器的崛起(下):系统、应用、集群的封装
你好,我是周志明。在理解了从隔离角度出发的容器化技术的发展之后,这节课我们接着从封装的角度来学习容器应用的发展。
封装系统LXC
当文件系统、访问、资源都可以被隔离后容器就已经具备它降生所需要的全部前置支撑条件了并且Linux的开发者们也已经明确地看到了这一点。
因此为了降低普通用户综合使用namespaces、cgroups这些低级特性的门槛2008年Linux Kernel 2.6.24内核在刚刚开始提供cgroups的同一时间就马上发布了名为Linux容器LinuX ContainersLXC的系统级虚拟化功能。
当然在这之前在Linux上并不是没有系统级虚拟化的解决方案比如传统的OpenVZ和Linux-VServer都能够实现容器隔离并且只会有很低的性能损失按OpenVZ提供的数据只会有1~3%的损失),但它们都是非官方的技术,使用它们最大的阻碍是系统级虚拟化必须要有内核的支持。为此,它们就只能通过非官方内核补丁的方式来修改标准内核,才能获得那些原本在内核中不存在的能力。
如此一来LXC就带着令人瞩目的光环登场它的出现促使“容器”从一个阳春白雪的、只流传于开发人员口中的技术词汇逐渐向整个软件业的公共概念、共同语言发展就如同今天的“服务器”“客户端”和“互联网”一样。
不过相信你现在肯定会好奇为什么如今一提到容器大家首先联想到的是Docker而不是LXC为什么去问10个开发人员至少有9个听过Docker但如果问LXC可能只有1个人听说过
那么我们首先可以知道的是LXC的出现肯定是受到了OpenVZ和Linux-VServer的启发摸着巨人的肩膀过河当然没有什么不对。但可惜的是LXC在设定自己的发展目标时也被前辈们的影响所局限了。
其实LXC眼中的容器的定义与OpenVZ和Linux-VServer并没有什么差别它们都是一种封装系统的轻量级虚拟机而Docker眼中的容器的定义则是一种封装应用的技术手段。这两种封装理念在技术层面并没有什么本质区别但在应用效果上差异可就相当大了。
我举个具体的例子如果你要建设一个LAMPLinux、Apache、MySQL、PHP应用按照LXC的思路你应该先编写或者寻找到LAMP的template可以暂且不准确地类比为LXC版本的Dockerfile吧以此构造出一个安装了LAMP的虚拟系统。
如果按部署虚拟机的角度来看这还算挺方便的作为那个时代距今也就十年的系统管理员所有软件、补丁、配置都是要自己搞定的部署一台新虚拟机要花费一两天时间都很正常而有了LXC的template一下子帮你把LAMP都安装好了还想要啥自行车
但是作为一名现代的系统管理员这里的问题就相当大了如果我想把LAMP改为LNMPLinux、Nginx、MySQL、PHP该怎么办如果我想把LAMP里的MySQL 5调整为MySQL 8该怎么办这些都得通过找到或者自己编写新的template来解决。
或者好吧那这台机的软件、版本都配置对了下一台机我要构建LYME或者MEAN又该怎么办呢以封装系统为出发点如果仍然是按照先装系统再装软件的思路就永远无法做到一两分钟甚至十几秒钟就构造出一个合乎要求的软件运行环境这也就决定了LXC不可能形成今天的容器生态。
所以接下来舞台的聚光灯终于落到了Docker身上。
封装应用Docker
在2013年宣布开源的Docker毫无疑问是容器发展历史上里程碑式的发明然而Docker的成功似乎没有太多技术驱动的成分。至少对于开源早期的Docker而言确实没有什么能构成壁垒的技术。
事实上它的容器化能力直接来源于LXC它的镜像分层组合的文件系统直接来源于AUFS在Docker开源后不久就有人仅用了一百多行的Shell脚本便实现了Docker的核心功能名为Bocker提供了docker bulid/pull/images/ps/run/exec/logs/commit/rm/rmi等功能
那你可能就要问了为何历史选择了Docker而不是LXC或者其他容器技术呢对于这个问题我想引用下转述非直译有所精简DotCloud公司当年创造Docker的公司已于2016年倒闭创始人所罗门 · 海克斯Solomon Hykes在Stackoverflow上的一段问答
为什么要用Docker而不是LXCWhy would I use Docker over plain LXC-
Docker除了包装来自Linux内核的特性之外它的价值还在于-
跨机器的绿色部署Docker定义了一种将应用及其所有的环境依赖都打包到一起的格式仿佛它原本就是绿色软件一样。而LXC并没有提供这样的能力使用LXC部署的新机器很多细节都要依赖人的介入虚拟机的环境基本上肯定会跟你原本部署程序的机器有所差别。-
以应用为中心的封装Docker封装应用而非封装机器的理念贯穿了它的设计、API、界面、文档等多个方面。相比之下LXC将容器视为对系统的封装这局限了容器的发展。-
自动构建Docker提供了开发人员从在容器中构建产品的全部支持开发人员无需关注目标机器的具体配置就可以使用任意的构建工具链在容器中自动构建出最终产品。-
多版本支持Docker支持像Git一样管理容器的连续版本进行检查版本间差异、提交或者回滚等操作。从历史记录中你可以查看到该容器是如何一步一步构建成的并且只增量上传或下载新版本中变更的部分。-
组件重用Docker允许将任何现有容器作为基础镜像来使用以此构建出更加专业的镜像。-
共享Docker拥有公共的镜像仓库成千上万的Docker用户在上面上传了自己的镜像同时也使用他人上传的镜像。-
工具生态Docker开放了一套可自动化和自行扩展的接口在此之上用户可以实现很多工具来扩展其功能比如容器编排、管理界面、持续集成等等。-
—— Solomon HykesStackoverflow2013
这段回答也被收录到了Docker官网的FAQ上从Docker开源到今天从没有改变过。
其实促使Docker一问世就惊艳世间的并不是什么黑科技式的秘密武器而是它符合历史潮流的创意与设计理念还有充分开放的生态运营。由此可见在正确的时候正确的人手上有一个优秀的点子确实有机会引爆一个时代。
这里我还想让你看一张图片它是Docker开源一年后截至2014年12月获得的成绩。
我们可以发现从开源到现在只过了短短数年时间Docker就已经成为了软件开发、测试、分发、部署等各个环节都难以或缺的基础支撑而它自身的架构也发生了相当大的改变Docker被分解为了几个子系统包括Docker Client、Docker Daemon、Docker Registry、Docker Container等等以及Graph、Driver、libcontainer等各司其职的模块。
所以此时我们再说一百多行脚本就能实现Docker的核心功能再说Docker没有太高的技术含量就不太合适了。
2014年Docker开源了自己用Golang开发的libcontainer这是一个越过LXC直接操作namespaces和cgroups的核心模块有了libcontainer以后Docker就能直接与系统内核打交道不必依赖LXC来提供容器化隔离能力了。
到了2015年在Docker的主导和倡议下多家公司联合制定了“开放容器交互标准”Open Container InitiativeOCI这是一个关于容器格式和运行时的规范文件其中包含了运行时标准runtime-spec 、容器镜像标准image-spec和镜像分发标准distribution-spec分发标准还未正式发布
运行时标准定义了应该如何运行一个容器、如何管理容器的状态和生命周期、如何使用操作系统的底层特性namespaces、cgroup、pivot_root等
容器镜像标准规定了容器镜像的格式、配置、元数据的格式,你可以理解为对镜像的静态描述;
镜像分发标准则规定了镜像推送和拉取的网络交互过程。
由此为了符合OCI标准Docker推动自身的架构继续向前演进。
首先它是将libcontainer独立出来封装重构成runC项目并捐献给了Linux基金会管理。runC是OCI Runtime的首个参考实现它提出了“让标准容器无所不在”Make Standard Containers Available Everywhere的口号。
而为了能够兼容所有符合标准的OCI Runtime实现Docker进一步重构了Docker Daemon子系统把其中与运行时交互的部分抽象为了containerd项目。
这是一个负责管理容器执行、分发、监控、网络、构建、日志等功能的核心模块其内部会为每个容器运行时创建一个containerd-shim适配进程默认与runC搭配工作但也可以切换到其他OCI Runtime实现上然而实际并没做到最后containerd仍是紧密绑定于runC
后来到了2016年Docker把containerd捐献给了CNCF管理。
可以说runC与containerd两个项目的捐赠托管既带有Docker对开源信念的追求也带有Docker在众多云计算大厂夹击下自救的无奈这两个项目也将会成为未来Docker消亡和存续的伏笔到这节课的末尾你就能理解这句矛盾的话了
以上我列举的这些Docker推动的开源与标准化工作既是对Docker为开源乃至整个软件业做出贡献的赞赏也是为后面给你介绍容器编排时讲解当前容器引擎的混乱关系做的前置铺垫。
我们当然很清楚的一个事实就是Docker目前无疑在容器领域具有统治地位但其统治的稳固程度不仅没到高枕无忧说是危机四伏都不为过。
我之所以这么说的原因是因为现在已经能隐隐看出足以威胁动摇Docker地位的潜在可能性而引出这个风险的就是Docker虽然赢得了容器战争的胜利但Docker Swarm却输掉了容器编排战争。
实际上从结果回望当初Docker能赢得容器战争是存在了一些偶然性的而能确定的是Docker Swarm输掉编排战争是必然的。为什么这么说呢下面我就来揭晓答案。
封装集群Kubernetes
如果说以Docker为代表的容器引擎是把软件的发布流程从分发二进制安装包转变为了直接分发虚拟化后的整个运行环境让应用得以实现跨机器的绿色部署那以Kubernetes为代表的容器编排框架就是把大型软件系统运行所依赖的集群环境也进行了虚拟化让集群得以实现跨数据中心的绿色部署并能够根据实际情况自动扩缩。
我们从上节课的容器崛起之路讲到现在Docker和Kubernetes这个阶段已经不再是介绍历史了从这里开始发生的变化都是近几年软件业界中的热点事件也是“容器的崛起”这个小章节我们要讨论的主要话题。不过现在我暂时不打算介绍Kubernetes的技术细节在“容器间网络”“容器持久化存储”及“资源调度”这几个章节中我还会进行更详细的解析。
在今天这节课里我们就先从宏观层面去理解Kubernetes的诞生与演变的驱动力这对正确理解未来云原生的发展方向是至关重要的。
从Docker到Kubernetes
众所周知Kubernetes可谓是出身名门它的前身是Google内部已经运行多年的集群管理系统Borg在2014年6月使用Golang完全重写后开源。自它诞生之日起只要能与云计算稍微扯上关系的业界巨头都对Kubernetes争相追捧IBM、RedHat、Microsoft、VMware和华为都是它最早期的代码贡献者。
此时距离云计算从实验室到工业化应用已经有十个年头不过大量应用使用云计算的方式还是停滞在了传统的IDCInternet Data Center时代它们仅仅是用云端的虚拟机代替了传统的物理机而已。
尽管早在2013年Pivotal持有着Spring Framework和Cloud Foundry的公司就提出了“云原生”的概念但是要实现服务化、具备韧性Resilience、弹性Elasticity、可观测性Observability的软件系统依旧十分困难在当时基本只能依靠架构师和程序员高超的个人能力云计算本身还帮不上什么忙。
而在云的时代,不能充分利用云的强大能力,这让云计算厂商无比遗憾,也无比焦虑。
所以可以说直到Kubernetes横空出世大家才终于等到了破局的希望认准了这就是云原生时代的操作系统是让复杂软件在云计算下获得韧性、弹性、可观测性的最佳路径也是为厂商们推动云计算时代加速到来的关键引擎之一。
2015年7月Kubernetes发布了第一个正式版本1.0版更重要的事件是Google宣布与Linux基金会共同筹建云原生基金会Cloud Native Computing FoundationCNCF并且把Kubernetes托管到CNCF成为其第一个项目。随后Kubernetes就以摧枯拉朽之势消灭了容器编排领域的其他竞争对手哪怕Docker Swarm有着Docker在容器引擎方面的先天优势DotCloud后来甚至把Swarm直接内置入Docker之中都不能稍稍阻挡Kubernetes前进的步伐。
但是我们也要清楚Kubernetes的成功与Docker的成功并不一样。
Docker靠的是优秀的理念它是以一个“好点子”引爆了一个时代。我相信就算没有Docker也会有Cocker或者Eocker的出现但由成立仅三年的DotCloud公司三年后又倒闭做成了这样的产品确实有一定的偶然性。
而Kubernetes的成功不仅有Google深厚的技术功底作支撑、有领先时代的设计理念更加关键的是Kubernetes的出现符合所有云计算大厂的切身利益有着业界巨头不遗余力地广泛支持所以它的成功便是一种必然。
Kubernetes与Docker两者的关系十分微妙因此我们把握住两者关系的变化过程是理解Kubernetes架构演变与CRI、OCI规范的良好线索。
Kubernetes是如何一步步与Docker解耦的
在Kubernetes开源的早期它是完全依赖且绑定Docker的并没有过多地考虑日后有使用其他容器引擎的可能性。直到Kubernetes 1.5之前Kubernetes管理容器的方式都是通过内部的DockerManager向Docker Engine以HTTP方式发送指令通过Docker来操作镜像的增删改查的如上图最右边线路的箭头所示图中的kubelet是集群节点中的代理程序负责与管理集群的Master通信其他节点的含义在下面介绍时都会有解释
现在我们可以把这个阶段的Kubernetes与容器引擎的调用关系捋直并结合前面提到的Docker捐献containerd与runC后重构的调用一起来梳理下这个完整的调用链条
Kubernetes Master → kubelet → DockerManager → Docker Engine → containerd → runC
然后到了2016年Kubernetes 1.5版本开始引入“容器运行时接口”Container Runtime InterfaceCRI这是一个定义容器运行时应该如何接入到kubelet的规范标准从此Kubernetes内部的DockerManager就被更为通用的KubeGenericRuntimeManager所替代了实际上在1.6.6之前都仍然可以看到DockerManagerkubelet与KubeGenericRuntimeManager之间通过gRPC协议通信。
不过由于CRI是在Docker之后才发布的规范Docker是肯定不支持CRI的所以Kubernetes又提供了DockerShim服务作为Docker与CRI的适配层由它与Docker Engine以HTTP形式通信从而实现了原来DockerManager的全部功能。
此时Docker对Kubernetes来说就只是一项默认依赖而非之前的不可或缺了现在它们的调用链为
Kubernetes Master → kubelet → KubeGenericRuntimeManager → DockerShim → Docker Engine → containerd → runC
接着再到2017年由Google、RedHat、Intel、SUSE、IBM联合发起的CRI-OContainer Runtime Interface Orchestrator项目发布了首个正式版本。
一方面我们从名字上就可以看出来它肯定是完全遵循CRI规范来实现的另一方面它可以支持所有符合OCI运行时标准的容器引擎默认仍然是与runC搭配工作的如果要换成Clear Containers、Kata Containers等其他OCI运行时也完全没有问题。
不过到这里开源版的Kubernetes虽然完全支持用户去自由选择根据用户宿主机的环境选择是使用CRI-O、cri-containerd还是DockerShim来作为CRI实现但在RedHat自己扩展定制的Kubernetes企业版即OpenShift 4中调用链已经没有了Docker Engine的身影
Kubernetes Master → kubelet → KubeGenericRuntimeManager → CRI-O→ runC
当然因为此时Docker在容器引擎中的市场份额仍然占有绝对优势对于普通用户来说如果没有明确的收益也并没有什么动力要把Docker换成别的引擎。所以CRI-O即使摆出了直接挖掉Docker根基的凶悍姿势实际上也并没有给Docker带来太多即时可见的影响。不过我们能够想像此时Docker心中肯定充斥了难以言喻的危机感。
时间继续来到了2018年由Docker捐献给CNCF的containerd在CNCF的精心孵化下发布了1.1版1.1版与1.0版的最大区别是此时它已经完美地支持了CRI标准这意味着原本用作CRI适配器的cri-containerd从此不再被需要。
此时我们再观察Kubernetes到容器运行时的调用链就会发现调用步骤会比通过DockerShim、Docker Engine与containerd交互的步骤要减少两步这又意味着用户只要愿意抛弃掉Docker情怀的话在容器编排上就可以至少省略一次HTTP调用获得性能上的收益。而且根据Kubernetes官方给出的测试数据这些免费的收益还相当地可观。
如此Kubernetes从1.10版本宣布开始支持containerd 1.1在调用链中就已经能够完全抹去Docker Engine的存在了
Kubernetes Master → kubelet → KubeGenericRuntimeManager → containerd → runC
而到了今天要使用哪一种容器运行时就取决于你安装Kubernetes时宿主机上的容器运行时环境但对于云计算厂商来说比如国内的阿里云ACK、腾讯云TKE等直接提供的Kubernetes容器环境采用的容器运行时普遍都已经是containerd了毕竟运行性能对它们来说就是核心生产力和竞争力。
小结
学完这节课我们可以试着来做一个判断在未来随着Kubernetes的持续发展壮大Docker Engine经历从不可或缺、默认依赖、可选择、直到淘汰会是大概率的事件。从表面上看这件事情是Google、RedHat等云计算大厂联手所为可实际淘汰它的还是技术发展的潮流趋势。这就如同Docker诞生时依赖LXC到最后用libcontainer取代掉LXC一样。
同时我们也该看到事情的另一面现在连LXC都还没有挂掉反倒还发展出了更加专注于跟OpenVZ等系统级虚拟化竞争的LXD就可以相信Docker本身也是很难彻底消亡的已经养成习惯的CLI界面已经形成成熟生态的镜像仓库等都应该会长期存在只是在容器编排领域未来的Docker很可能只会以runC和containerd的形式存续下去毕竟它们最初都源于Docker的血脉。
一课一思
在2021年1月初Kubernetes宣布将会在v1.23版本中把Dockershim从 Kubelet中移除那么你会如何看待容器化日后的发展呢
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,147 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
48 _ 以容器构建系统(上):隔离与协作
你好,我是周志明。从这节课开始,我们讨论的焦点会从容器本身,过渡到容器编排上。
我们知道自从Docker提出“以封装应用为中心”的容器发展理念成功取代了“以封装系统为中心”的LXC以后一个容器封装一个单进程应用已经成为了被广泛认可的最佳实践。
然而当单体时代过去之后分布式系统里对于应用的概念已经不再等同于进程了此时的应用需要多个进程共同协作通过集群的形式对外提供服务那么以虚拟化方法实现这个目标的过程就被称为容器编排Container Orchestration
而到今天Kubernetes已经成为了容器编排的代名词。不过在课程中我并不打算过多介绍Kubernetes具体有哪些功能也不会为你说明它由Pod、Node、Deployment、ReplicaSet等各种类型的资源组成可用的服务、集群管理平面与节点之间是如何工作的、每种资源该如何配置使用等等如果你想了解这方面信息可以去查看Kubernetes官网的文档库或任何一本以Kubernetes为主题的使用手册。
在课程中我真正希望能帮你搞清楚的问题是“为什么Kubernetes会设计成现在这个样子”“为什么以容器构建系统应该这样做
而要寻找这些问题的答案,最好是从它们设计的实现意图出发。所以在接下来的两节课中,我虚构了一系列从简单到复杂的场景,带你来理解并解决这些场景中的问题。
这里我还想说明一点学习这两节课的内容并不要求你对Kubernetes有过多深入的了解但需要你至少使用过Kubernetes和Docker基本了解它的核心功能与命令另外课程中还会涉及到一点儿Linux系统内核资源隔离的基础知识别担心只要你仔细学习了“容器的崛起”这个小章节就已经完全够用了。
构建容器编排系统时都会遇到什么问题?
好,现在我们来设想一下,如果让你来设计一套容器编排系统,协调各种容器来共同来完成一项工作,你可能会遇到什么问题?会如何着手解决呢?
我们先从最简单的场景开始吧:
场景一假设你现在有两个应用其中一个是Nginx另一个是为该Nginx收集日志的Filebeat你希望将它们封装为容器镜像以方便日后分发。
最直接的方案就将Nginx和Filebeat直接编译成同一个容器镜像这是可以做到的而且并不复杂。不过这样做其实会埋下很大的隐患它违背了Docker提倡的单个容器封装单进程应用的最佳实践。
Docker设计的Dockerfile只允许有一个ENTRYPOINT这并不是什么随便添加的人为限制而是因为Docker只能通过监视PID为1的进程即由ENTRYPOINT启动的进程的运行状态来判断容器的工作状态是否正常像是容器退出执行清理、容器崩溃自动重启等操作Docker都必须先判断状态。
那么我们可以设想一下即使我们使用了supervisord之类的进程控制器来解决同时启动Nginx和Filebeat进程的问题如果因为某种原因它们不停发生崩溃、重启那Docker也无法察觉到它只能观察到supervisord的运行状态。所以场景一关于封装为容器镜像的需求会理所当然地演化成场景二。
场景二假设你现在有两个Docker镜像其中一个封装了HTTP服务为便于称呼叫它Nginx容器另一个封装了日志收集服务叫它Filebeat容器。现在你要求Filebeat容器能收集Nginx容器产生的日志信息。
其实场景二的需求依然不难解决只要在Nginx容器和Filebeat容器启动时分别把它们的日志目录和收集目录挂载为宿主机同一个磁盘位置的Volume即可在Docker中这种操作是十分常用的容器间信息交换手段。
不过,容器间信息交换不仅仅是文件系统。
假如此时我又引入了一个新的工具confd它是Linux下的一种配置管理工具作用是根据配置中心Etcd、ZooKeeper、Consul的变化自动更新Nginx的配置。那么这样的话就又会遇到新的问题。
这是因为confd需要向Nginx发送HUP信号才便于通知Nginx配置已经发生了变更而发送HUP信号自然就要求confd与Nginx能够进行IPC通信才行。
当然尽管共享IPC名称空间不如共享Volume常见但Docker同样支持了这个功能也就是通过docker run命令提供了ipc参数用来把多个容器挂载到同一个父容器的IPC名称空间之下以实现容器间共享IPC名称空间的需求。类似地如果要共享UTS名称空间可以使用uts参数要共享网络名称空间的话就使用net参数。
这就是Docker针对场景二这种不跨机器的多容器协作所给出的解决方案了。
实际上自动地为多个容器设置好共享名称空间就是Docker Compose提供的核心能力。
不过这种针对具体应用需求来共享名称空间的方案确实可以工作但并不够优雅也谈不上有什么扩展性。要知道容器的本质是对cgroups和namespaces所提供的隔离能力的一种封装在Docker提倡的单进程封装的理念影响下容器蕴含的隔离性也多了仅针对于单个进程的额外局限。
然而Linux的cgroups和namespaces原本都是针对进程组而不只是单个进程来设计的同一个进程组中的多个进程天然就可以共享相同的访问权限与资源配额。
所以如果现在我们把容器与进程在概念上对应起来那容器编排的第一个扩展点就是要找到容器领域中与“进程组”相对应的概念这是实现容器从隔离到协作的第一步。在Kubernetes的设计里这个对应物叫做Pod。
额外知识Pod名字的由来与含义-
在容器正式出现之前的Borg系统中Pod的概念就已经存在了从Google的发表的《Large-Scale Cluster Management at Google with Borg》里可以看出Kubernetes时代的Pod整合了Borg时代的“Prod”Production Task的缩写与“Non-Prod”的职能。由于Pod一直没有权威的中文翻译我在后面课程中会尽量用英文指代偶尔需要中文的场合就使用Borg中Prod的译法即“生产任务”来指代。
这样有了“容器组”的概念只需要把多个容器放到同一个Pod中场景二的问题就可以解决了。
Pod的含义与职责
事实上扮演容器组的角色满足容器共享名称空间的需求是Pod两大最基本的职责之一同处于一个Pod内的多个容器相互之间会以超亲密的方式协作。请注意“超亲密”在这里的用法不是什么某种带强烈感情色彩的形容词而是代表了一种有具体定义的协作程度。
具体是什么意思呢?
对于普通非亲密的容器来说它们一般以网络交互方式其他的如共享分布式存储来交换信息也算跨网络协作对于亲密协作的容器来说是指它们被调度到同一个集群节点上可以通过共享本地磁盘等方式协作而超亲密的协作是特指多个容器位于同一个Pod这种特殊关系它们将默认共享以下名称空间
UTS名称空间所有容器都有相同的主机名和域名。
网络名称空间所有容器都共享一样的网卡、网络栈、IP地址等等。因此同一个Pod中不同容器占用的端口不能冲突。
IPC名称空间所有容器都可以通过信号量或者POSIX共享内存等方式通信。
时间名称空间:所有容器都共享相同的系统时间。
也就是说同一个Pod的容器只有PID名称空间和文件名称空间默认是隔离的。
PID的隔离让开发者的每个容器都有独立的进程ID编号它们封装的应用进程就是PID为1的进程开发人员可以通过Pod元数据定义中的spec.shareProcessNamespace来改变这点。而一旦要求共享PID名称空间容器封装的应用进程就不再具有PID为1的特征了这就有可能导致部分依赖该特征的应用出现异常。
而在文件名称空间方面容器要求文件名称空间的隔离是很理所应当的需求因为容器需要相互独立的文件系统以避免冲突。但容器间可以共享存储卷这是通过Kubernetes的Volume来实现的。
额外知识Kubernetes中Pod名称空间共享的实现细节-
Pod内部多个容器共享UTS、IPC、网络等名称空间是通过一个名为Infra Container的容器来实现的这个容器是整个Pod中第一个启动的容器只有几百KB大小代码只有很短的几十行见这里Pod中的其他容器都会以Infra Container作为父容器UTS、IPC、网络等名称空间实质上都是来自Infra Container容器。-
如果容器设置为共享PID名称空间的话Infra Container中的进程将作为PID 1进程其他容器的进程将以它的子进程的方式存在此时就会由Infra Container来负责进程管理比如清理僵尸进程、感知状态和传递状态。-
由于Infra Container的代码除了注册SIGINT、SIGTERM、SIGCHLD等信号的处理器外就只是一个以pause()方法为循环体的无限循环永远处于Pause状态所以它也常被称为“Pause Container”。
除此之外Pod的另一个基本职责是实现原子性调度。这里我们可以先明确一点就是如果容器编排不跨越集群节点那是否具有原子性其实都不影响大局。
但是在集群环境中,容器可能会跨机器调度时,这个特性就变得非常重要了。
如果以容器为单位来调度的话不同容器就有可能被分配到不同机器上。而两台机器之间本来就是物理隔离依靠网络连接的所以这时候谈什么名称空间共享、cgroups配额共享都没有意义了由此我们就从场景二又演化出了场景三。
场景三假设你现在有Filebeat、Nginx两个Docker镜像在一个具有多个节点的集群环境下要求每次调度都必须让Filebeat和Nginx容器运行于同一个节点上。
其实,两个关联的协作任务必须一起调度的需求,在容器出现之前很久就有了。
我举个简单的例子。在传统的多线程或多进程并发调度中如果两个线程或进程的工作是强依赖的单独给谁分配处理时间而让另一个被挂起都会导致某一个线程无法工作所以也就有了协同调度Coscheduling的概念它主要用来保证一组紧密联系的任务能够被同时分配资源。
这样来看的话,如果我们在容器编排中,仍然坚持把容器看作是调度的最小粒度,那针对容器运行所需资源的需求声明,就只能设定在容器上。如此一来,集群每个节点的剩余资源越紧张,单个节点无法容纳全部协同容器的概率就越大,协同的容器被分配到不同节点的可能性就越高。
说实话协同调度是很麻烦的实现起来要么很低效比如Apache Mesos的Resource Hoarding调度策略就要等所有需要调度的任务都完备后才会开始分配资源要么就是很复杂比如Google就曾针对Borg的下一代Omega系统发表过论文《Omega: Flexible, Scalable Schedulers for Large Compute Clusters》其中介绍了它是如何通过乐观并发Optimistic Concurrency、冲突回滚的方式做到高效率且高度复杂的协同调度。
而如果我们将运行资源的需求声明定义在Pod上直接以Pod为最小的原子单位来实现调度的话由于多个Pod之间一定不存在超亲密的协同关系只会通过网络非亲密地协作那就根本没有协同的说法自然也不需要考虑复杂的调度了关于Kubernetes的具体调度实现我会在“资源与调度”这个小章节中展开讲解
Pod是隔离与调度的基本单位也是我们接触的第一种Kubernetes资源。Kubernetes把一切都看作是资源不同资源之间依靠层级关系相互组合协作这个思想是贯穿Kubernetes整个系统的两大核心设计理念之一不仅在容器、Pod、主机、集群等计算资源上是这样在工作负载、持久存储、网络策略、身份权限等其他领域中也都有着一致的体现。
另外我想说的是因为Pod是Kubernetes中最重要的资源又是资源模型中一种仅在逻辑上存在、没有物理对应的概念因为对应的“进程组”也只是个逻辑概念也是其他编排系统没有的概念所以我这节课专门给你介绍了下它的设计意图而不是像帮助手册那样直接给出它的作用和特性。
对于Kubernetes中的其他计算资源像Node、Cluster等都有切实的物理对应物很容易就能形成共同的认知我就不一一介绍了这里你只需要了解下它们的设计意图就行
容器Container延续了自Docker以来一个容器封装一个应用进程的理念是镜像管理的最小单位。
生产任务Pod补充了容器化后缺失的与进程组对应的“容器组”的概念Pod中的容器共享UTS、IPC、网络等名称空间是资源调度的最小单位。
节点Node对应于集群中的单台机器这里的机器既可以是生产环境中的物理机也可以是云计算环境中的虚拟节点节点是处理器和内存等资源的资源池是硬件单元的最小单位。
集群Cluster对应于整个集群Kubernetes提倡的理念是面向集群来管理应用。当你要部署应用的时候只需要通过声明式API将你的意图写成一份元数据Manifests把它提交给集群即可而无需关心它具体分配到哪个节点尽管通过标签选择器完全可以控制它分配到哪个节点但一般不需要这样做、如何实现Pod间通信、如何保证韧性与弹性等等所以集群是处理元数据的最小单位。
集群联邦Federation对应于多个集群通过联邦可以统一管理多个Kubernetes集群联邦的一种常见应用是支持跨可用区域多活、跨地域容灾的需求。
小结
学完了这节课,我们要知道,容器之间顺畅地交互通信是协作的核心需求,但容器协作并不只是通过高速网络来互相连接容器而已。如何调度容器,如何分配资源,如何扩缩规模,如何最大限度地接管系统中的非功能特性,让业务系统尽可能地免受分布式复杂性的困扰,都是容器编排框架必须考虑的问题,只有恰当解决了这一系列问题,云原生应用才有可能获得比传统应用更高的生产力。
一课一思
现在,我们能够明确隔离与协作的含义,也就是容器要让它管理的进程相互隔离,使用独立的资源与配额;容器编排系统要让它管理的各个容器相互协作,共同维持一个分布式系统的运作。但除了协作之外,你认为容器编排系统是否还有其他必须考虑的需求目标呢?
欢迎在留言区分享你的见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
49 _ 以容器构建系统(下):韧性与弹性
你好,我是周志明。今天,我们接着上节课“隔离与协作”的话题,继续来讨论容器编排的另一个目标:韧性与弹性。
我曾经看过一部电影叫做《Bubble Boy》主要讲了一个体内没有任何免疫系统的小男孩每天只能生活在无菌的圆形气球里对常人来说不值一提的细菌都会直接威胁到他的性命。小男孩尽管能够降生于世但并不能真正地与世界交流这种生命是极度脆弱的。
真实世界的软件系统,跟电影世界中的小男孩所面临的处境其实差不多。
要知道,让容器能够相互连通、相互协作,仅仅是以容器构建系统的第一步,我们不仅希望得到一个能够运行起来的系统,而且还希望得到一个能够健壮运行的系统、能够抵御意外与风险的系统。
当然在Kubernetes的支持下你确实可以直接创建Pod将应用运行起来但这样的应用就像是电影中只能存活在气球中的小男孩一样脆弱无论是软件缺陷、意外操作或者硬件故障都可能导致在复杂协作的过程中某个容器出现异常进而出现系统性的崩溃。
为了解决这个问题架构师专门设计了服务容错的策略和模式你可以回顾复习第36、37讲。而Kubernetes作为云原生时代的基础设施也尽力帮助我们以最小的代价来实现容错为系统健壮运行提供底层支持。
那么Kubernetes所提供的帮助就是指除资源模型之外的另一个核心设计理念控制器设计模式。它的其中一种重要应用就是这节课我们要探讨的主题实现具有韧性与弹性的系统。
接下来我们就从如何解决场景四的问题开始一起来探讨下为什么Kubernetes要设计这些控制器以及为什么这些控制器会被设计成现在这种样子。
编排系统如何快速调整出错的服务?
我们先来看看场景四的问题:
场景四假设有个由数十个Node、数百个Pod、近千个Container所组成的分布式系统作为管理员你想要避免该系统因为外部流量压力、代码缺陷、软件更新、硬件升级、资源分配等各种原因而出现中断的状况那么你希望编排系统能为你提供何种支持
作为用户,我们当然最希望容器编排系统能自动地把所有意外因素都消灭掉,让每一个服务都永远健康,永不出错。但永不出错的服务是不切实际的,只有凑齐七颗龙珠才可能办得到。
所以我们就只能退而求其次,让编排系统在这些服务出现问题、运行状态不正确的时候,能自动将它们调整成正确的状态。
这种需求听起来其实也挺贪心的但已经具备足够的可行性了。而且我们可以采取的应对办法在工业控制系统里已经有了非常成熟的应用它叫做控制回路Control Loop
关于控制回路的一般工作过程在Kubernetes官方文档中是以“房间里空调自动调节温度”为例来具体介绍的当你设置好了温度就是告诉空调你对温度的“期望状态”Desired State而传感器测量出的房间实际温度是“当前状态”Current State
那么,根据当前状态与期望状态的差距,控制器对空调制冷的开关进行调节控制,就能让其当前状态逐渐接近期望状态。
由此我们把这种控制回路的思想迁移应用到容器编排上自然会为Kubernetes中的资源附加上了期望状态与实际状态两项属性。
不管是已经出现在上节课的资源模型中用于抽象容器运行环境的计算资源还是没有登场的对应于安全、服务、令牌、网络等功能的资源第40讲中曾提及过如果用户想使用这些资源来实现某种需求并不能像平常编程那样去调用某个或某一组方法来达成目的。而是要通过描述清楚这些资源的期望状态由Kubernetes中对应监视这些资源的控制器来驱动资源的实际状态逐渐向期望状态靠拢才能够达成自己的目的。
而这种交互风格就被叫做Kubernetes的声明式API如果你之前有过实际操作Kubernetes的经验那你日常在元数据文件中的spec字段所描述的就是资源的期望状态。
额外知识Kubernates的资源对象与控制器-
目前Kubernetes已内置支持相当多的资源对象并且还可以使用CRDCustom Resource Definition来自定义扩充你可以使用kubectl api-resources来查看它们。下面我根据用途分类给你列举了一些常见的资源-
用于描述如何创建、销毁、更新、扩缩Pod包括AutoscalingHPA、CronJob、DaemonSet、Deployment、Job、Pod、ReplicaSet、StatefulSet
用于配置信息的设置与更新包括ConfigMap、Secret
用于持久性地存储文件或者Pod之间的文件共享包括Volume、LocalVolume、PersistentVolume、PersistentVolumeClaim、StorageClass
用于维护网络通信和服务访问的安全包括SecurityContext、ServiceAccount、Endpoint、NetworkPolicy
用于定义服务与访问包括Ingress、Service、EndpointSlice
用于划分虚拟集群、节点和资源配额包括Namespace、Node、ResourceQuota-
这些资源在控制器管理框架中,一般都会有相应的控制器来管理,这里我也列举了一些常见的控制器,按照它们的启动情况进行了分类,如下:-
必须启用的控制器EndpointController、ReplicationController、PodGCController、ResourceQuotaController、NamespaceController、ServiceAccountController、GarbageCollectorController、DaemonSetController、JobController、DeploymentController、ReplicaSetController、HPAController、DisruptionController、StatefulSetController、CronJobController、CSRSigningController、CSRApprovingController、TTLController
默认启用的可选控制器可通过选项禁止TokenController、NodeController、ServiceController、RouteController、PVBinderController、AttachDetachController
默认禁止的可选控制器可通过选项启用BootstrapSignerController、TokenCleanerController
那么,与资源相对应的,只要是实际状态有可能发生变化的资源对象,就通常都会由对应的控制器进行追踪,每个控制器至少会追踪一种类型的资源。
因此为了管理众多资源控制器Kubernetes设计了统一的控制器管理框架kube-controller-manager来维护这些控制器的正常运作并设计了统一的指标监视器kube-apiserver用于在控制器工作时为它提供追踪资源的度量数据。
Kubernetes控制器模式的工作原理
那么Kubernetes具体是怎么做的呢在回答之前我想先解释下毕竟我们不是在写Kubernetes的操作手册没办法展开和详解每个控制器所以下面我就以两三种资源和控制器为代表来举例说明一下。
OK回到问题上。这里我们只要把场景四进一步具体化转换成下面的场景五就可以得到一个很好的例子了。
比如说我们就以部署控制器Deployment Controller、副本集控制器ReplicaSet Controller和自动扩缩控制器HPA Controller为例来看看Kubernetes控制器模式的工作原理。
场景五:通过服务编排,我们让任何分布式系统自动实现以下三种通用的能力:
Pod出现故障时能够自动恢复不中断服务
Pod更新程序时能够滚动更新不中断服务
Pod遇到压力时能够水平扩展不中断服务。
在这节课的一开始我提到过虽然Pod本身也是资源完全可以直接创建但由Pod直接构成的系统是十分脆弱的就像是那个气球中的小男孩所以在实际生产中并不提倡。
正确的做法是通过副本集ReplicaSet来创建Pod。
ReplicaSet也是一种资源它是属于工作负荷一类的资源代表了一个或多个Pod副本的集合你可以在ReplicaSet资源的元数据中描述你期望Pod副本的数量即spec.replicas的值
当ReplicaSet成功创建之后副本集控制器就会持续跟踪该资源一旦有Pod发生崩溃退出或者状态异常默认是靠进程返回值你还可以在Pod中设置探针以自定义的方式告诉Kubernetes出现何种情况Pod才算状态异常ReplicaSet都会自动创建新的Pod来替代异常的Pod如果因异常情况出现了额外数量的Pod也会被ReplicaSet自动回收掉。
总之就是确保在任何时候集群中这个Pod副本的数量都会向期望状态靠拢。
另外我们还要清楚一点就是ReplicaSet本身就能满足场景五中的第一项能力可以保证Pod出现故障时自动恢复。但是在升级程序版本时ReplicaSet就不得不主动中断旧Pod的运行重新创建新版的Pod了而这会造成服务中断。
因此对于那些不允许中断的业务以前的Kubernetes曾经提供过kubectl rolling-update命令来辅助实现滚动更新。
所谓的滚动更新Rolling Updates是指先停止少量旧副本维持大量旧副本继续提供服务当停止的旧副本更新成功新副本可以提供服务以后再重复以上操作直至所有的副本都更新成功。我们把这个过程放到ReplicaSet上就是先创建新版本的ReplicaSet然后一边让新ReplicaSet逐步创建新版Pod的副本一边让旧的ReplicaSet逐渐减少旧版Pod的副本。
而到了现在之所以kubectl rolling-update命令会被淘汰其实是因为这样的命令式交互完全不符合Kubernetes的设计理念这是台面上的说法我觉得淘汰的根本原因主要是因为它不够好用。如果你希望改变某个资源的某种状态就应该将期望状态告诉Kubernetes而不是去教Kubernetes具体该如何操作。
所以现在新的部署资源Deployment与部署控制器就被设计出来了。具体的实现步骤是这样的我们可以由Deployment来创建ReplicaSet再由ReplicaSet来创建Pod当我们更新了Deployment中的信息以后比如更新了镜像的版本部署控制器就会跟踪到新的期望状态自动地创建新ReplicaSet并逐渐缩减旧的ReplicaSet的副本数直到升级完成后彻底删除掉旧ReplicaSet。这个工作过程如下图所示
好,我们再来看看场景五中的最后一种情况。
你可能会知道在遇到流量压力时管理员完全可以手动修改Deployment中的副本数量或者通过kubectl scale命令指定副本数量促使Kubernetes部署更多的Pod副本来应对压力。然而这种扩容方式不仅需要人工参与而且只靠人类经验来判断需要扩容的副本数量也不容易做到精确与及时。
为此Kubernetes又提供了Autoscaling资源和自动扩缩控制器它们能够自动地根据度量指标如处理器、内存占用率、用户自定义的度量值等来设置Deployment或者ReplicaSet的期望状态实现当度量指标出现变化时系统自动按照“Autoscaling→Deployment→ReplicaSet→Pod”这样的顺序层层变更最终实现根据度量指标自动扩容缩容。
小结
故障恢复、滚动更新、自动扩缩这些特性在云原生时代中常常被概括成服务的弹性Elasticity与韧性ResilienceReplicaSet、Deployment、Autoscaling的用法也是所有Kubernetes教材资料中都会讲到的“基础必修课”。
学完了这两节课我还想再说明一点如果你准备学习Kubernetes或者其他云原生的相关技术我建议你最好不要死记硬背地学习每个资源的元数据文件该如何编写、有哪些指令、有哪些功能更好的方式是站在解决问题的角度去理解为什么Kubernetes要设计这些资源和控制器理解为什么这些资源和控制器会被设计成现在这种样子。
一课一思
如果你觉得已经理解了前面几种资源和控制器的例子,那不妨思考几个问题:
假设我想限制某个Pod持有的最大存储卷数量应该如何设计
假设集群中某个Node发生硬件故障Kubernetes要让调度任务避开这个Node应该如何设计
假设一旦这个Node重新恢复Kubernetes要能尽快利用上面的资源又该如何去设计
其实只要你真正接受了资源与控制器是贯穿整个Kubernetes的两大设计理念即便不去查文档手册也应该能推想出个大概轮廓你在这个基础上再去看手册或者源码的时候想必就能够事半功倍。
好,欢迎给我留言,分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
50 _ 应用为中心的封装Kustomize与Helm
你好,我是周志明。
在理解了前面几节课所讲的容器技术发展的历程之后,不知你会不会有种“套娃式”的迷惑感?
现在你已经知道容器的崛起缘于chroot、namespaces、cgroups等内核提供的隔离能力而系统级的虚拟化技术使得同一台机器上互不干扰地运行多个服务成为了可能
为了降低用户使用内核隔离能力的门槛随后出现了LXC它是namespaces、cgroups特性的上层封装这就让“容器”一词真正走出了实验室开始走入工业界进行实际应用
然后为了实现跨机器的软件绿色部署出现了Docker最初是LXC的上层封装彻底改变了软件打包分发的方式迅速被大量企业广泛采用
而为了满足大型系统对服务集群化的需要又出现了Kubernetes最初是Docker的上层封装从而使得以多个容器共同协作构建出健壮的分布式系统成为了今天云原生时代的技术基础设施。
那这样你的疑惑可能也就出现了Kubernetes会是容器化崛起之路的终点线吗它达到了人们对云原生时代技术基础设施的期望了吗
首先从能力角度来看可以说是的。Kubernetes被誉为云原生时代的操作系统自诞生之日起它就因为出色的管理能力、扩展性和以声明代替命令的交互理念收获了无数喝彩声。
但是从易用角度来看坦白说差距还非常大。云原生基础设施的其中一个重要目标是接管掉业务系统复杂的非功能特性这会让业务研发与运维工作变得足够简单不受分布式的牵绊。然而Kubernetes被诟病得最多的就是复杂它从诞生之日起就因为陡峭的学习曲线而出名。
我举个具体例子吧。如果要用Kubernetes部署一套Spring Cloud版的Fenixs Bookstore你需要分别部署一个到多个的配置中心、注册中心、服务网关、安全认证、用户服务、商品服务、交易服务然后要对每个微服务都配置好相应的Kubernetes工作负载与服务访问为每一个微服务的Deployment、ConfigMap、StatefulSet、HPA、Service、ServiceAccount、Ingress等资源都编写好元数据配置。
这个过程最难的地方不仅在于繁琐,还在于要想写出合适的元数据描述文件,你既需要懂开发(网关中服务调用关系、使用容器的镜像版本、运行依赖的环境变量等等这些参数,只有开发最清楚),又需要懂运维(要部署多少个服务、配置何种扩容缩容策略、数据库的密钥文件地址等等,只有运维最清楚),有时候还需要懂平台(需要什么样的调度策略、如何管理集群资源,通常只有平台组、中间件组或者核心系统组的同学才会关心),一般企业根本找不到合适的角色来为它管理、部署和维护应用。
这个事儿Kubernetes心里其实也挺委屈因为以上提到的复杂性不能说是Kubernetes带来的而是分布式架构本身的原罪。
对于大规模的分布式集群来说无论是最终用户部署应用还是软件公司管理应用都存在着像前面提到的这诸多痛点。这些困难的实质是源于Docker容器镜像封装了单个服务而Kubernetes通过资源封装了服务集群却没有一个载体真正封装整个应用这就使得它会把原本属于应用内部的技术细节给圈禁起来不暴露给最终用户、系统管理员和平台维护者而让使用者去埋单。
那么如此所造成的应用难以管理的矛盾,就在于封装应用的方法没能将开发、运维、平台等各种角色的关注点恰当地分离。
但是,既然在微服务时代,应用的形式已经不再局限于单个进程,那就也该到了重新定义“以应用为中心的封装”这句话的时候了。至于具体怎样的封装才算是正确的,其实到今天也还没有特别权威的结论,不过经过人们的尝试探索,已经能够窥见未来容器应用的一些雏形了。
所以接下来,我会花两节课的时间,给你介绍一下最近几年容器封装的两种主流思路,你可以从中理解容器“以应用为中心的封装”这个理念在不同阶段的内涵变化,这也是对“应用”这个概念的不断扩展升华的过程。
今天这节课呢我们就先来了解下Kustomize和Helm它们是封装“无状态应用”的典型代表。
额外知识:无状态应用与有状态应用的区别-
无状态应用Stateless Application与有状态应用Stateful Application说的是应用程序是否要自己持有其运行所需的数据如果程序每次运行都跟首次运行一样不依赖之前任何操作所遗留下来的痕迹那它就是无状态的反之如果程序推倒重来之后用户能察觉到该应用已经发生变化那它就是有状态的。-
下一讲要介绍的Operator与OAM就是支持有状态应用的封装方式这里你可以先了解一下。
Kustomize
最初由Kubernetes官方给出的“如何封装应用”的解决方案是“用配置文件来配置文件”这不是绕口令你可以把它理解为是一种针对YAML的模版引擎的变体。
Kubernetes官方认为应用就是一组具有相同目标的Kubernetes资源的集合如果逐一管理、部署每项资源元数据太麻烦啰嗦的话那就提供一种便捷的方式把应用中不变的信息与易变的信息分离开以此解决管理问题把应用所有涉及的资源自动生成一个多合一All-in-One的整合包以此解决部署问题。
而完成这项工作的工具就叫做Kustomize它原本只是一个独立的小程序从Kubernetes 1.14起被纳入了kubectl命令之中成为随着Kubernetes提供的内置功能。Kustomize使用Kustomization文件来组织与应用相关的所有资源Kustomization本身也是一个以YAML格式编写的配置文件里面定义了构成应用的全部资源以及资源中需根据情况被覆盖的变量值。
Kustomize的主要价值是根据环境来生成不同的部署配置。只要建立多个Kustomization文件开发人员就能以基于基准进行派生Base and Overlay的方式对不同的模式比如生产模式、调试模式、不同的项目同一个产品对不同客户的客制化定制出不同的资源整合包。
在配置文件里无论是开发关心的信息还是运维关心的信息只要是在元数据中有描述的内容最初都是由开发人员来编写的然后在编译期间由负责CI/CD的产品人员针对项目进行定制。最后在部署期间由运维人员通过kubectl的补丁Patch机制更改其中需要运维去关注的属性比如构造一个补丁来增加Deployment的副本个数构造另外一个补丁来设置Pod的内存限制等等。
k8s
├── base
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ └── service.yaml
└── overlays
└── prod
│ ├── load-loadbalancer-service.yaml
│ └── kustomization.yaml
└── debug
└── kustomization.yaml
从上面这段目录结构中我们可以观察到一个由kustomize管理的应用结构它主要由base和overlays组成。Kustomize使用Base、Overlay和Patch生成最终配置文件的思路与Docker中分层镜像的思路有些相似这样的方式既规避了以“字符替换”对资源元数据文件的入侵也不需要用户学习额外的DSL语法比如Lua
从效果来看使用由Kustomize编译生成的All-in-One整合包来部署应用是相当方便的只要一行命令就能够把应用涉及的所有服务一次安装好在“探索与实践”小章节中会介绍的Kubernetes版本和Istio版本的Fenixs Booktstore都使用了这种方式来发布应用你也不妨实际体验一下。
但是毕竟Kustomize只是一个“小工具”性质的辅助功能对于开发人员来说Kustomize只能简化产品针对不同情况的重复配置它其实并没有真正解决应用管理复杂的问题要做的事、要写的配置最终都没有减少只是不用反复去写罢了而对于运维人员来说应用维护不仅仅只是部署那一下应用的整个生命周期除了安装外还有更新、回滚、卸载、多版本、多实例、依赖项维护等诸多问题都很麻烦。
所以说要想真正解决这些问题还需要更加强大的管理工具比如下面我要介绍的主角Helm。不过Kustomize能够以极小的成本在一定程度上分离了开发和运维的工作不用像Helm那样需要一套独立的体系来管理应用这种轻量便捷本身也是一种可贵的价值。
OK下面我们就来具体讲讲Helm。
Helm与Chart
Helm是由Deis公司开发的一种更具系统性的管理和封装应用的解决方案它参考了各大Linux发行版管理应用的思路应用格式是Chart。
Helm一开始的目标就很明确如果说Kubernetes是云原生操作系统的话那Helm就要成为这个操作系统上面的应用商店与包管理工具。
我相信Linux下的包管理工具和封装格式如Debian系的apt-get命令与dpkg格式、RHEL系的yum命令与rpm格式你肯定不会陌生。有了包管理工具你只要知道应用的名称就可以很方便地从应用仓库中下载、安装、升级、部署、卸载、回滚程序而且包管理工具掌握着应用的依赖信息和版本变更情况具备完整的自管理能力每个应用需要依赖哪些前置的第三方库在安装的时候都会一并处理好。
Helm模拟的就是这种做法它提出了与Linux包管理直接对应的Chart格式和Repository应用仓库另外针对Kubernetes中特有的一个应用经常要部署多个版本的特点也提出了Release的专有概念。
Chart用于封装Kubernetes应用涉及到的所有资源通常是以目录内的文件集合的形式存在的。目录名称就是Chart的名称没有版本信息比如官方仓库中WordPress Chart的目录结构是这样的
WordPress
├── templates
│ ├── NOTES.txt
│ ├── deployment.yaml
│ ├── externaldb-secrets.yaml
│ └── 版面原因省略其他资源文件
│ └── ingress.yaml
└── Chart.yaml
└── requirements.yaml
└── values.yaml
其中有几个固定的配置文件Chart.yaml给出了应用自身的详细信息名称、版本、许可证、自述、说明、图标等等requirements.yaml给出了应用的依赖关系依赖项指向的是另一个应用的坐标名称、版本、Repository地址values.yaml给出了所有可配置项目的预定义值。
可配置项就是指需要部署期间由运维人员调整的那些参数它们以花括号包裹在templates目录下的资源文件中。当部署应用时Helm会先将管理员设置的值覆盖到values.yaml的默认值上然后以字符串替换的形式传递给templates目录的资源模版最后生成要部署到Kubernetes的资源文件。
由于Chart封装了足够丰富的信息所以Helm除了支持命令行操作外也能很容易地根据这些信息自动生成图形化的应用安装、参数设置界面。
我们再来说说Repository仓库。它主要是用于实现Chart的搜索与下载服务Helm社区维护了公开的Stable和Incubator的中央仓库界面如下图所示也支持其他人或组织搭建私有仓库和公共仓库并能够通过Hub服务把不同个人或组织搭建的公共仓库聚合起来形成更大型的分布式应用仓库这也有利于Chart的查找与共享。
Helm Hub商店
所以整体来说Helm提供了应用全生命周期、版本、依赖项的管理能力同时Helm还支持额外的扩展插件能够加入CI/CD或者其他方面的辅助功能。
如此一来它的定位就已经从单纯的工具升级到应用管理平台了强大的功能让Helm收到了不少支持有很多应用主动入驻到官方的仓库中。而从2018年起Helm项目被托管到CNFC成为其中的一个孵化项目。
总而言之Helm通过模仿Linux包管理器的思路去管理Kubernetes应用在一定程度上确实是可行的。不过在Linux与Kubernetes中部署应用还是存在一些差别最重要的一点是在Linux中99%的应用都只会安装一份而Kubernetes里为了保证可用性同一个应用部署多份副本才是常规操作。
所以Helm为了支持对同一个Chart包进行多次部署每次安装应用都会产生一个ReleaseRelease就相当于该Chart的安装实例。对于无状态的服务来说靠着不同的Release就已经足够支持多个服务并行工作了但对于有状态的服务来说服务会与特定资源或者服务产生依赖关系比如要部署数据库通常要依赖特定的存储来保存持久化数据这样事情就变得复杂起来了。
既然Helm无法很好地管理这种有状态的依赖关系那么这一类问题就是Operator要解决的痛点了。这也是我在下一节课要给你重点介绍的工具。
小结
今天我给你介绍了两种比较常用也较为具体的应用封装方式分别是Kubernetes官方推出的Kustomize以及目前在Kubernetes上较为主流的“应用商店”格式Helm与Chart。这样的封装对于无状态应用已经足够了但对于有状态应用来说仍然不能满足需要。
在下节课,我们将继续应用封装这个话题,一起来探讨如何为有状态应用提供支持。
一课一思
你是否尝试过在Kubernetes中部署一些需共享状态的集群应用比如Etcd、Easticsearch等等你是自己编写YAML定义它们所需的各种资源的吗
欢迎在留言区分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,274 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
51 _ 应用为中心的封装Operator与OAM
你好我是周志明。上节课我们了解了无状态应用的两种主流封装方式分别是Kustomize和Helm。那么今天这节课我们继续来学习有状态应用的两种封装方法包括Operator和开放应用模型。
Operator
与Kustomize和Helm不同的是Operator不应当被称作是一种工具或者系统它应该算是一种封装、部署和管理Kubernetes应用的方法尤其是针对最复杂的有状态应用去封装运维能力的解决方案最早是由CoreOS公司于2018年被RedHat收购的华人程序员邓洪超提出的。
简单来说Operator是通过Kubernetes 1.7开始支持的自定义资源Custom Resource DefinitionsCRD此前曾经以TPR即Third Party Resource的形式提供过类似的能力把应用封装为另一种更高层次的资源再把Kubernetes的控制器模式从面向内置资源扩展到了面向所有自定义资源以此来完成对复杂应用的管理。
具体怎么理解呢我们来看一下RedHat官方对Operator设计理念的阐述
Operator设计理念-
Operator是使用自定义资源CR本人注CR即Custom Resource是CRD的实例管理应用及其组件的自定义Kubernetes控制器。高级配置和设置由用户在CR中提供。Kubernetes Operator基于嵌入在Operator逻辑中的最佳实践将高级指令转换为低级操作。Kubernetes Operator监视CR类型并采取特定于应用的操作确保当前状态与该资源的理想状态相符。-
—— 什么是 Kubernetes OperatorRedHat
这段文字是直接由RedHat官方撰写并翻译成中文的准确严谨但比较拗口对于没接触过Operator的人来说并不友好比如你可能就会问什么叫做“高级指令”什么叫做“低级操作”它们之间具体如何转换呢等等。
其实要理解这些问题你必须先弄清楚有状态和无状态应用的含义及影响然后再来理解Operator所做的工作。在上节课我给你补充了一个“额外知识”已经介绍过了二者之间的区别现在我们再来看看
有状态应用Stateful Application与无状态应用Stateless Application说的是应用程序是否要自己持有其运行所需的数据如果程序每次运行都跟首次运行一样不依赖之前任何操作所遗留下来的痕迹那它就是无状态的反之如果程序推倒重来之后用户能察觉到该应用已经发生变化那它就是有状态的。
无状态应用在分布式系统中具有非常巨大的价值我们都知道分布式中的CAP不兼容原理如果无状态那就不必考虑状态一致性没有了C那A和P就可以兼得。换句话说只要资源足够无状态应用天生就是高可用的。但不幸的是现在的分布式系统中多数关键的基础服务都是有状态的比如缓存、数据库、对象存储、消息队列等等只有Web服务器这类服务属于无状态。
站在Kubernetes的角度看是否有状态的本质差异在于有状态应用会对某些外部资源有绑定性的直接依赖比如说Elasticsearch在建立实例时必须依赖特定的存储位置只有重启后仍然指向同一个数据文件的实例才能被认为是相同的实例。另外有状态应用的多个应用实例之间往往有着特定的拓扑关系与顺序关系比如etcd的节点间选主和投票节点们都需要得知彼此的存在明确每一个节点的网络地址和网络拓扑关系。
为了管理好那些与应用实例密切相关的状态信息Kubernetes从1.9版本开始正式发布了StatefulSet及对应的StatefulSetController。与普通ReplicaSet中的Pod相比由StatefulSet管理的Pod具备几项额外特性。
Pod会按顺序创建和按顺序销毁StatefulSet中的各个Pod会按顺序地创建出来而且再创建后面的Pod之前必须要保证前面的Pod已经转入就绪状态。如果要销毁StatefulSet中的Pod就会按照与创建顺序的逆序来执行。
Pod具有稳定的网络名称Kubernetes中的Pod都具有唯一的名称在普通的副本集中这是靠随机字符产生的而在StatefulSet中管理的Pod会以带有顺序的编号作为名称而且能够在重启后依然保持不变。
Pod具有稳定的持久存储StatefulSet中的每个Pod都可以拥有自己独立的PersistentVolumeClaim资源。即使Pod被重新调度到其他节点上它所拥有的持久磁盘也依然会被挂载到该Pod这点会在“容器持久化”这个小章节中进一步介绍。
看到这些特性以后你可能还会说我还是不太理解StatefulSet的设计意图呀。没关系我来举个例子你一看就理解了。
如果把ReplicaSet中的Pod比喻为养殖场中的“肉猪”那StatefulSet就是被当作宠物圈养的“荷兰猪”不同的肉猪在食用功能上并没有什么区别但每只宠物猪都是独一无二的有专属于自己的名字、习性与记忆。事实上早期的StatefulSet就曾经使用过PetSet这个名字。
StatefulSet出现以后Kubernetes就能满足Pod重新创建后仍然保留上一次运行状态的需求了。不过有状态应用的维护并不仅限于此。比如说对于一套Elasticsearch集群来说通过StatefulSet最多只能做到创建集群、删除集群、扩容缩容等最基本的操作并不支持常见的运维操作比如备份和恢复数据、创建和删除索引、调整平衡策略等等。
我再举个实际例子来说明一下Operator是如何解决那些StatefulSet覆盖不到的有状态服务管理需求的。
假设我们要部署一套Elasticsearch集群通常要在StatefulSet中定义相当多的细节比如服务的端口、Elasticsearch的配置、更新策略、内存大小、虚拟机参数、环境变量、数据文件位置等等。
这里我直接把满足这个需求的YAML全部贴出来让你对咱们前面反复提到的Kubernetes的复杂性有更加直观的理解。
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-cluster
spec:
clusterIP: None
selector:
app: es-cluster
ports:
- name: transport
port: 9300
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-loadbalancer
spec:
selector:
app: es-cluster
ports:
- name: http
port: 80
targetPort: 9200
type: LoadBalancer
---
apiVersion: v1
kind: ConfigMap
metadata:
name: es-config
data:
elasticsearch.yml: |
cluster.name: my-elastic-cluster
network.host: "0.0.0.0"
bootstrap.memory_lock: false
discovery.zen.ping.unicast.hosts: elasticsearch-cluster
discovery.zen.minimum_master_nodes: 1
xpack.security.enabled: false
xpack.monitoring.enabled: false
ES_JAVA_OPTS: -Xms512m -Xmx512m
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: esnode
spec:
serviceName: elasticsearch
replicas: 3
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: es-cluster
spec:
securityContext:
fsGroup: 1000
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
command: ["sysctl", "-w", "vm.max_map_count=262144"]
containers:
- name: elasticsearch
resources:
requests:
memory: 1Gi
securityContext:
privileged: true
runAsUser: 1000
capabilities:
add:
- IPC_LOCK
- SYS_RESOURCE
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.1
env:
- name: ES_JAVA_OPTS
valueFrom:
configMapKeyRef:
name: es-config
key: ES_JAVA_OPTS
readinessProbe:
httpGet:
scheme: HTTP
path: /_cluster/health?local=true
port: 9200
initialDelaySeconds: 5
ports:
- containerPort: 9200
name: es-http
- containerPort: 9300
name: es-transport
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
- name: elasticsearch-config
mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
subPath: elasticsearch.yml
volumes:
- name: elasticsearch-config
configMap:
name: es-config
items:
- key: elasticsearch.yml
path: elasticsearch.yml
volumeClaimTemplates:
- metadata:
name: es-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi
可以看到这里面的细节配置非常多。其实之所以会这样根本原因在于Kubernetes完全不知道Elasticsearch是个什么东西。所有Kubernetes不知道的信息、不能启发式推断出来的信息都必须由用户在资源的元数据定义中明确列出必须一步一步手把手地“教会”Kubernetes部署Elasticsearch这种形式就属于咱们刚刚提到的“低级操作”。
如果我们使用Elastic.co官方提供的Operator那就会简单多了。Elasticsearch Operator提供了一种kind: Elasticsearch的自定义资源在它的帮助下只需要十行代码将用户的意图是“部署三个版本为7.9.1的ES集群节点”说清楚就能实现跟前面StatefulSet那一大堆配置相同甚至是更强大的效果如下面代码所示
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: elasticsearch-cluster
spec:
version: 7.9.1
nodeSets:
- name: default
count: 3
config:
node.master: true
node.data: true
node.ingest: true
node.store.allow_mmap: false
有了Elasticsearch Operator的自定义资源就相当于Kubernetes已经学会怎样操作Elasticsearch了。知道了所有它相关的参数含义与默认值就不需要用户再手把手地教了这种就是所谓的“高级指令”。
Operator将简洁的高级指令转化为Kubernetes中具体操作的方法跟Helm或Kustomize的思路并不一样
Helm和Kustomize最终仍然是依靠Kubernetes的内置资源来跟Kubernetes打交道的
Operator则是要求开发者自己实现一个专门针对该自定义资源的控制器在控制器中维护自定义资源的期望状态。
通过程序编码来扩展Kubernetes比只通过内置资源来与Kubernetes打交道要灵活得多。比如在需要更新集群中某个Pod对象的时候由Operator开发者自己编码实现的控制器完全可以在原地对Pod进行重启不需要像Deployment那样必须先删除旧Pod然后再创建新Pod。
使用CRD定义高层次资源、使用配套的控制器来维护期望状态带来的好处不仅仅是“高级指令”的便捷更重要的是可以在遵循Kubernetes的一贯基于资源与控制器的设计原则的同时又不必受制于Kubernetes内置资源的表达能力。只要Operator的开发者愿意编写代码前面提到的那些StatfulSet不能支持的能力如备份恢复数据、创建删除索引、调整平衡策略等操作都完全可以实现出来。
把运维的操作封装在程序代码上从表面上看最大的受益者是运维人员开发人员要为此付出更多劳动。然而Operator并没有被开发者抵制相反因为用代码来描述复杂状态往往反而比配置文件更加容易开发与运维之间的协作成本降低了还受到了开发者的一致好评。
因为Operator的各种优势它变成了近两、三年容器封装应用的一股新潮流现在很多复杂分布式系统都有了官方或者第三方提供的Operator这里收集了一部分。RedHat公司也持续在Operator上面进行了大量投入推出了简化开发人员编写Operator的Operator Framework/SDK。
目前看来应对有状态应用的封装运维Operator也许是最有可行性的方案但这依然不是一项轻松的工作。以etcd的Operator为例etcd本身不算什么特别复杂的应用Operator实现的功能看起来也相当基础主要有创建集群、删除集群、扩容缩容、故障转移、滚动更新、备份恢复等功能但是代码就已经超过一万行了。
现在开发Operator的确还是有着相对较高的门槛通常由专业的平台开发者而非业务开发或者运维人员去完成。但是Operator非常符合技术潮流顺应了软件业界所提倡的DevOps一体化理念等Operator的支持和生态进一步成熟之后开发和运维都能从中受益未来应该能成长为一种应用封装的主流形式。
OAM
我要给你介绍的最后一种应用封装的方案是阿里云和微软在2019年10月上海QCon大会上联合发布的开放应用模型Open Application ModelOAM它不仅是中国云计算企业参与制定甚至是主导发起的国际技术规范也是业界首个云原生应用标准定义与架构模型。
OAM思想的核心是将开发人员、运维人员与平台人员的关注点分离开发人员关注业务逻辑的实现运维人员关注程序的平稳运行平台人员关注基础设施的能力与稳定性。毕竟长期让几个角色厮混在同一个All-in-One资源文件里并不能擦出什么火花反而会将配置工作弄得越来越复杂把“YAML Engineer”弄成容器界的嘲讽梗。
OAM对云原生应用的定义是“由一组相互关联但又离散独立的组件构成这些组件实例化在合适的运行时上由配置来控制行为并共同协作提供统一的功能”。你可能看不懂是啥意思没有关系为了方便跟后面的概念对应我先把这句话拆解一下
OAM定义的应用-
一个Application由一组Components构成每个Component的运行状态由Workload描述每个Component可以施加Traits来获取额外的运维能力同时我们可以使用Application Scopes将Components划分到一或者多个应用边界中便于统一做配置、限制、管理。把Components、Traits和Scopes组合在一起实例化部署形成具体的Application Configuration以便解决应用的多实例部署与升级。
然后我来具体解释一下上面列出来的核心概念来帮助你理解OAM对应用的定义。在这句话里面每一个用英文标注出来的技术名词都是OAM在Kubernetes的基础上扩展而来的概念每一个名词都有相对应的自定义资源。换句话说它们并不是单纯的抽象概念而是可以被实际使用的自定义资源。
我们来看一下这些概念的具体含义。
Components服务组件自SOA时代以来由Component构成应用的思想就屡见不鲜然而OAM的Component不仅仅是特指构成应用“整体”的一个“部分”它还有一个重要的职责那就是抽象出那些应该由开发人员去关注的元素。比如应用的名字、自述、容器镜像、运行所需的参数等等。
Workload工作负荷Workload决定了应用的运行模式每个Component都要设定自己的Workload类型OAM按照“是否可访问、是否可复制、是否长期运行”预定义了六种Workload类型如下表所示。如果有必要使用者还可以通过CRD与Operator去扩展。
Traits运维特征开发活动有大量复用功能的技巧但运维活动却很缺少这样的技巧平时能为运维写个Shell脚本或简单工具就已经算是个高级的运维人员了。Traits就可以用来封装模块化后的运维能力它可以针对运维中的可重复操作预先设定好一些具体的Traits比如日志收集Trait、负载均衡Trait、水平扩缩容Trait等等。这些预定义的Traits定义里会注明它们可以作用于哪种类型的工作负荷还包括能填哪些参数、哪些必填选填项、参数的作用描述是什么等等。
Application Scopes应用边界多个Component共同组成一个Scope你可以根据Component的特性或作用域来划分Scope。比如具有相同网络策略的Component放在同一个Scope中具有相同健康度量策略的Component放到另一个Scope中。同时一个Component也可能属于多个Scope比如一个Component完全可能既需要配置网络策略也需要配置健康度量策略。
Application Configuration应用配置将Component必须、Trait必须、Scope非必须组合到一起进行实例化就形成了一个完整的应用配置。
OAM使用咱们刚刚所说的这些自定义资源对原先All-in-One的复杂配置做了一定层次的解耦
开发人员负责管理Component
运维人员将Component组合并绑定Trait把它变成Application Configuration
平台人员或基础设施提供方负责提供OAM的解释能力将这些自定义资源映射到实际的基础设施。
这样,不同角色分工协作,就整体简化了单个角色关注的内容,让不同角色可以更聚焦、更专业地做好本角色的工作,整个过程如下图所示:
OAM角色关系图图片来自OAM规范GitHub
事实上OAM未来能否成功很大程度上取决于云计算厂商的支持力度因为OAM的自定义资源一般是由云计算基础设施负责解释和驱动的比如阿里云的EDAS就已内置了OAM的支持。
如果你希望能够应用在私有Kubernetes环境中目前OAM的主要参考实现是Rudr已声明废弃和Crossplane。Crossplane是一个仅发起一年多的CNCF沙箱项目主要参与者包括阿里云、微软、Google、RedHat等工程师。Crossplane提供了OAM中全部的自定义资源和控制器安装以后就可以用OAM定义的资源来描述应用了。
小结
今天容器圈的发展是一日千里各种新规范、新技术层出不穷。在这两节课里我特意挑选了非常流行而且有代表性的四种分别是“无状态应用”的典型代表Kustomize和Helm和“有状态应用”的典型代表Operator和OAM。
其他我没有提到的应用封装技术还有CNAB、Armada、Pulumi等等。这些封装技术会有一定的重叠之处但并非都是重复的轮子在实际应用的时候往往会联合其中多个工具一起使用。而至于怎么封装应用才是最佳的实践目前也还没有定论但可以肯定的是以应用为中心的理念已经成为了明确的共识。
一课一思
在“虚拟化容器”这个小章节中,我安排了五节课来介绍虚拟化容器的原理和应用,不知道经过这几节课的学习后,你对容器是否有更新的认识?可以在留言区说说你对容器现状和未来的看法,我们一起交流讨论。
如果你觉得有收获,欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,170 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
52 _ Linux网络虚拟化信息是如何通过网络传输被另一个程序接收到的
你好,我是周志明。从这节课开始,我会用两讲的时间带你学习虚拟化网络方面的知识点。
如果不加任何限定“虚拟化网络”其实是一项内容十分丰富研究历史十分悠久的计算机技术它完全不依附于虚拟化容器而是作为计算机科学中一门独立的分支。像是网络运营商经常提起的“网络功能虚拟化”Network Function VirtualizationNFV还有网络设备商和网络管理软件提供商经常提起的“软件定义网络”Software Defined NetworkingSDN等等这些都属于虚拟化网络的范畴。
不过,对于我们这样普通的软件开发者来说,一般没有什么必要去完全理解和掌握虚拟化网络,因为这需要储备大量开发中不常用到的专业知识,而且还会消耗大量的时间成本。
所以在课程里我们讨论的虚拟化网络是狭义的它特指“如何基于Linux系统的网络虚拟化技术来实现的容器间网络通信”更通俗一点说就是只关注那些为了相互隔离的Linux网络名称空间可以相互通信而设计出来的虚拟化网络设施。
另外我还要说明的是在这个语境中的“虚拟化网络”就是直接为容器服务的说它是依附于容器而存在的也完全可行。所以为了避免混淆我在课程中会尽量回避“虚拟化网络”这个范畴过大的概念而是会以“容器间网络”和“Linux网络虚拟化”为题来展开讲解。
好了下面我们就从Linux下网络通信的协议栈模型以及程序如何干涉在协议栈中流动的信息来开始了解吧。
Linux系统下的网络通信模型
如果抛开虚拟化只谈网络的话那我认为首先应该了解的知识就是Linux系统的网络通信模型即信息是如何从程序中发出通过网络传输再被另一个程序接收到的。
从整体上看Linux系统的通信过程无论是按理论上的OSI七层模型还是以实际上的TCP/IP四层模型来解构都明显地呈现出“逐层调用逐层封装”的特点这种逐层处理的方式与栈结构比如程序执行时的方法栈很类似所以它通常被称为“Linux网络协议栈”简称“网络栈”有时也称“协议栈”。
下图就体现了Linux网络通信过程与OSI或者TCP/IP模型的对应关系也展示了网络栈中的数据流动的路径你可以看一下
在图中传输模型的左侧我特别标示出了网络栈在用户与内核空间的部分也就是说几乎整个网络栈应用层以下都位于系统内核空间之中而Linux系统之所以采用这种设计主要是从数据安全隔离的角度出发来考虑的。
由内核去处理网络报文的收发无疑会有更高的执行开销比如数据在内核态和用户态之间来回拷贝的额外成本所以就会损失一些性能但是这样能够保证应用程序无法窃听到或者去伪造另一个应用程序的通信内容。当然针对特别关注收发性能的应用场景也有直接在用户空间中实现全套协议栈的旁路方案比如开源的Netmap以及Intel的DPDK都能做到零拷贝收发网络数据包。
另外,图中传输模型的箭头展示的是数据流动的方向,它体现了信息从程序中发出以后,到被另一个程序接收到之前经历的几个阶段,下面我来给你一一分析下。
Socket
应用层的程序是通过Socket编程接口来和内核空间的网络协议栈通信的。Linux Socket是从BSD Socket发展而来的现在的Socket已经不局限于作为某个操作系统的专属功能而是成为了各大主流操作系统共同支持的通用网络编程接口是网络应用程序实际上的交互基础。
在这里应用程序通过读写收、发缓冲区Receive/Send Buffer来与Socket进行交互在Unix和Linux系统中出于“一切皆是文件”的设计哲学对Socket的操作被实现为了对文件系统socketfs的读写访问操作通过文件描述符File Descriptor来进行。
TCP/UDP
传输层协议族里最重要的协议无疑就是传输控制协议Transmission Control ProtocolTCP和用户数据报协议User Datagram ProtocolUDP两种它们也是在Linux内核中被直接支持的协议。此外还有流控制传输协议Stream Control Transmission ProtocolSCTP、数据报拥塞控制协议Datagram Congestion Control ProtocolDCCP等等。当然了不同的协议处理流程大致都是一样的只是封装的报文和头、尾部信息会有些不一样。
这里我以TCP协议为例内核发现Socket的发送缓冲区中有新的数据被拷贝进来后会把数据封装为TCP Segment报文常见的网络协议的报文基本上都是由报文头Header和报文体Body也叫荷载“Payload”两部分组成。
接着系统内核将缓冲区中用户要发送出去的数据作为报文体然后把传输层中的必要控制信息比如代表哪个程序发、由哪个程序收的源、目标端口号用于保证可靠通信重发与控制顺序的序列号、用于校验信息是否在传输中出现损失的校验和Check Sum等信息封装入报文头中。
IP
网络层协议最主要的就是网际协议Internet ProtocolIP其他的还会有因特网组管理协议Internet Group Management ProtocolIGMP、大量的路由协议EGP、NHRP、OSPF、IGRP、……等等。
这里我就以IP协议为例它会把来自上一层即前面例子中的TCP报文的数据包作为报文体然后再次加入到自己的报文头中比如指明数据应该发到哪里的路由地址、数据包的长度、协议的版本号等等这样封装成IP数据包后再发往下一层。关于TCP和IP协议报文的内容我曾在“负载均衡”这节课中详细讲解过你可以去回顾复习下。
Device
Device即网络设备它是网络访问层中面向系统一侧的接口。不过这里所说的设备跟物理硬件设备并不是同一个概念Device只是一种向操作系统端开放的接口它的背后既可能代表着真实的物理硬件也可能是某段具有特定功能的程序代码比如即使不存在物理网卡也依然可以存在回环设备Loopback Device
许多网络抓包工具比如tcpdump、Wirshark就是在此处工作的我在前面第38讲介绍微服务流量控制的时候曾提到过的网络流量整形通常也是在这里完成的。
Device主要的作用是抽象出统一的界面让程序代码去选择或影响收发包出入口比如决定数据应该从哪块网卡设备发送出去还有就是准备好网卡驱动工作所需的数据比如来自上一层的IP数据包、下一跳Next Hop的MAC地址这个地址是通过ARP Request得到的等等。
Driver
网卡驱动程序Driver是网络访问层中面向硬件一侧的接口网卡驱动程序会通过DMA把主存中的待发送的数据包复制到驱动内部的缓冲区之中。数据被复制的同时也会把上层提供的IP数据包、下一跳的MAC地址这些信息加上网卡的MAC地址、VLAN Tag等信息一并封装成为以太帧Ethernet Frame并自动计算校验和。而对于需要确认重发的信息如果没有收到接收者的确认ACK响应那重发的处理也是在这里自动完成的。
好了,上面这些阶段就是信息从程序中对外发出时,经过协议栈的过程了,而接收过程则是从相反方向进行的逆操作。
这里你需要记住,程序发送数据做的是层层封包,加入协议头,传给下一层;而接受数据则是层层解包,提取协议体,传给上一层。你可以通过类比来理解数据包的接收过程,我就不再啰嗦一遍数据接收的步骤了。
干预网络通信的Netfilter框架
到这里,我们似乎可以发现,网络协议栈的处理是一套相对固定和封闭的流程,在整套处理过程中,除了在网络设备这层,我们能看到一点点程序以设备的形式介入处理的空间以外,其他过程似乎就没有什么可供程序插手的余地了。
然而事实并非如此从Linux Kernel 2.4版开始内核开放了一套通用的、可供代码干预数据在协议栈中流转的过滤器框架这就是Netfilter框架。
Netfilter框架是Linux防火墙和网络的主要维护者罗斯迪·鲁塞尔Rusty Russell提出并主导设计的它围绕网络层IP协议的周围埋下了五个钩子Hooks每当有数据包流到网络层经过这些钩子时就会自动触发由内核模块注册在这里的回调函数程序代码就能够通过回调来干预Linux的网络通信。
下面我给你介绍一下这五个钩子分别都是什么:
PREROUTING来自设备的数据包进入协议栈后就会立即触发这个钩子。注意如果PREROUTING钩子在进入IP路由之前触发了就意味着只要接收到的数据包无论是否真的发往本机也都会触发这个钩子。它一般是用于目标网络地址转换Destination NATDNAT
INPUT报文经过IP路由后如果确定是发往本机的将会触发这个钩子它一般用于加工发往本地进程的数据包。
FORWARD报文经过IP路由后如果确定不是发往本机的将会触发这个钩子它一般用于处理转发到其他机器的数据包。
OUTPUT从本机程序发出的数据包在经过IP路由前将会触发这个钩子它一般用于加工本地进程的输出数据包。
POSTROUTING从本机网卡出去的数据包无论是本机的程序所发出的还是由本机转发给其他机器的都会触发这个钩子它一般是用于源网络地址转换Source NATSNAT
Netfilter允许在同一个钩子处注册多个回调函数所以数据包在向钩子注册回调函数时必须提供明确的优先级以便触发时能按照优先级从高到低进行激活。而因为回调函数会有很多个看起来就像是挂在同一个钩子上的一串链条所以钩子触发的回调函数集合就被称为“回调链”Chained Callbacks这个名字也导致了后续基于Netfilter设计的Xtables系工具比如下面我要介绍的iptables都使用到了“链”Chain的概念。
那么虽然现在看来Netfilter只是一些简单的事件回调机制而已但这样一套简单的设计却成为了整座Linux网络大厦的核心基石Linux系统提供的许多网络能力比如数据包过滤、封包处理设置标志位、修改TTL等、地址伪装、网络地址转换、透明代理、访问控制、基于协议类型的连接跟踪、带宽限速等等它们都是在Netfilter的基础之上实现的。
而且以Netfilter为基础的应用也有很多其中使用最广泛的毫无疑问要数Xtables系列工具比如iptables、ebtables、arptables、ip6tables等等。如果你用过Linux系统来做过开发的话那我估计至少这里面的iptables工具你会或多或少地使用过它常被称为是Linux系统“自带的防火墙”。
但其实iptables实际能做的事情已经远远超出了防火墙的范畴严谨地讲iptables比较贴切的定位应该是能够代替Netfilter多数常规功能的IP包过滤工具。
要知道iptables的设计意图是因为Netfilter的钩子回调虽然很强大但毕竟要通过程序编码才够能使用并不适合系统管理员用来日常运维而它的价值就是以配置去实现原本用Netfilter编码才能做到的事情。
一般来说iptables会先把用户常用的管理意图总结成具体的行为预先准备好然后就会在满足条件的时候自动激活行为比如以下几种常见的iptables预置的行为
DROP直接将数据包丢弃。
REJECT给客户端返回Connection Refused或Destination Unreachable报文。
QUEUE将数据包放入用户空间的队列供用户空间的程序处理。
RETURN跳出当前链该链里后续的规则不再执行。
ACCEPT同意数据包通过继续执行后续的规则。
JUMP跳转到其他用户自定义的链继续执行。
REDIRECT在本机做端口映射。
MASQUERADE地址伪装自动用修改源或目标的IP地址来做NAT
LOG在/var/log/messages文件中记录日志信息。
……
当然这些行为本来能够被挂载到Netfilter钩子的回调链上但iptables又进行了一层额外抽象它不是把行为与链直接挂钩而是会根据这些底层操作的目的先总结为更高层次的规则。
我举个例子假设你挂载规则的目的是为了实现网络地址转换NAT那就应该对符合某种特征的流量比如来源于某个网段、从某张网卡发送出去、在某个钩子上比如做SNAT通常在POSTROUTING做DNAT通常在PREROUTING进行MASQUERADE行为这样具有相同目的的规则就应该放到一起才便于管理所以也就形成了“规则表”的概念。
iptables内置了五张不可扩展的规则表其中的security表并不常用很多资料只计算了前四张表我们来看看
raw表用于去除数据包上的连接追踪机制Connection Tracking
mangle表用于修改数据包的报文头信息比如服务类型Type Of ServiceToS、生存周期Time to LiveTTL以及为数据包设置Mark标记典型的应用是链路的服务质量管理Quality Of ServiceQoS
nat表用于修改数据包的源或者目的地址等信息典型的应用是网络地址转换Network Address Translation
filter表用于对数据包进行过滤控制到达某条链上的数据包是继续放行、直接丢弃或拒绝ACCEPT、DROP、REJECT典型的应用是防火墙。
security表用于在数据包上应用SELinux这张表并不常用。
这五张规则表是有优先级的raw→mangle→nat→filter→security也就是前面我列举出的顺序。这里你要注意在iptables中新增规则时需要按照规则的意图指定要存入到哪张表中如果没有指定就默认会存入filter表。此外每张表能够使用到的链也有所不同具体表与链的对应关系如下所示
那么你从名字上其实就能看出预置的五条链是直接源自于Netfilter的钩子它们与五张规则表的对应关系是固定的用户不能增加自定义的表或者修改已有表与链的关系但可以增加自定义的链。
新增的自定义链与Netfilter的钩子没有天然的对应关系换句话说就是不会被自动触发只有显式地使用JUMP行为从默认的五条链中跳转过去才能被执行。
可以说iptables不仅仅是Linux系统自带的一个网络工具它在容器间通信中也扮演着相当重要的角色。比如Kubernetes用来管理Sevice的Endpoints的核心组件kube-proxy就依赖iptables来完成ClusterIP到Pod的通信也可以采用IPVSIPVS同样是基于Netfilter的这种通信的本质就是一种NAT访问。
当然对于Linux用户来说前面提到的内容可能都是相当基础的网络常识但如果你平常比较少在Linux系统下工作就可能需要一些用iptables充当防火墙过滤数据、充当作路由器转发数据、充当作网关做NAT转换的实际例子来帮助理解了这些操作在网上也很容易就能找到这里我就不专门去举例说明了。
小结
Linux目前提供的八种名称空间里网络名称空间无疑是隔离内容最多的一种它为名称空间内的所有进程提供了全套的网络设施包括独立的设备界面、路由表、ARP表IP地址表、iptables/ebtables规则、协议栈等等。
虚拟化容器是以Linux名称空间的隔离性为基础来实现的那解决隔离的容器之间、容器与宿主机之间乃至跨物理网络的不同容器间通信问题的责任就很自然地落在了Linux网络虚拟化技术的肩上。这节课里我们暂时放下了容器编排、云原生、微服务等等这些上层概念走进Linux网络的底层世界去学习了一些与设备、协议、通信相关的基础网络知识。
最后我想说的是到目前为止我给你介绍的Linux下网络通信的协议栈模型以及程序如何干涉在协议栈中流动的信息它们与虚拟化都没有产生什么直接联系而是整个Linux网络通信的必要基础。在下节课我们就要开始专注于跟网络虚拟化密切相关的内容了。
一课一思
说实话,今天的内容其实很适合以实现业务功能为主、平常并不直接接触网络设备的普通开发人员,而如果你是做平台基础设施开发或者运维的,那学习这节课可能就会觉得有点太基础或啰嗦了,因为这些都是基本的工作技能。
所以在最后,我想来了解一下,如果你是一名程序员,那你是否经常有机会接触这些网络方面的知识呢?如果有,你都用它们来做什么?欢迎给我留言。
另外,如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,298 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
53 _ Linux网络虚拟化Docker所提供的容器通讯方案有哪些
你好我是周志明。今天我们接着上节课介绍的Linux网络知识继续来学习它们在虚拟化网络方面的应用从而为后续学习容器编排系统、理解各个容器是如何通过虚拟化网络来协同工作打好基础。
虚拟化网络设备
首先我们要知道,虚拟化网络并不需要完全遵照物理网络的样子来设计。不过,由于现在大量现成的代码,原来就是面向于物理存在的网络设备来编码实现的,另外也有出于方便理解和知识继承方面的考虑,因此虚拟化网络与物理网络中的设备还是具有相当高的相似性。
所以接下来,我就会从网络中那些与网卡、交换机、路由器等对应的虚拟设施,以及如何使用这些虚拟设施来组成网络入手,给你介绍容器间网络的通信基础设施。
好了,我们开始吧。
网卡tun/tap、veth
首先是虚拟网卡设备。
目前主流的虚拟网卡方案有tun/tap和veth两种其中tun/tap出现得时间更早它是一组通用的虚拟驱动程序包里面包含了两个设备分别是用于网络数据包处理的虚拟网卡驱动以及用于内核空间与用户空间交互的字符设备Character Devices这里具体指/dev/net/tun驱动。
大概在2000年左右Solaris系统为了实现隧道协议Tunneling Protocol开发了这套驱动从Linux Kernel 2.1版开始tun/tap移植到了Linux内核中当时它是作为源码中的可选模块而在2.4版之后发布的内核都会默认编译tun/tap的驱动。tun和tap是两个相对独立的虚拟网络设备其中tap模拟了以太网设备操作二层数据包以太帧tun则是模拟了网络层设备操作三层数据包IP报文
那么使用tun/tap设备的目的其实是为了把来自协议栈的数据包先交给某个打开了/dev/net/tun字符设备的用户进程处理后再把数据包重新发回到链路中。这里你可以通俗地理解为这块虚拟化网卡驱动一端连接着网络协议栈另一端连接着用户态程序而普通的网卡驱动则是一端连接着网络协议栈另一端连接着物理网卡。
如此一来,只要协议栈中的数据包能被用户态程序截获并加工处理,程序员就有足够的舞台空间去玩出各种花样,比如数据压缩、流量加密、透明代理等功能,都能够在此基础上实现。
这里我就以最典型的VPN应用程序为例程序发送给tun设备的数据包会经过如下所示的顺序流进VPN程序
应用程序通过tun设备对外发送数据包后tun设备如果发现另一端的字符设备已经被VPN程序打开这就是一端连接着网络协议栈另一端连接着用户态程序就会把数据包通过字符设备发送给VPN程序VPN收到数据包会修改后再重新封装成新报文比如数据包原本是发送给A地址的VPN把整个包进行加密然后作为报文体封装到另一个发送给B地址的新数据包当中。
这种把一个数据包套进另一个数据包中的处理方式就被形象地形容为“隧道”Tunneling隧道技术是在物理网络中构筑逻辑网络的经典做法。而其中提到的加密实际上也有标准的协议可以遵循比如IPSec协议。
不过使用tun/tap设备来传输数据需要经过两次协议栈所以会不可避免地产生一定的性能损耗因而如果条件允许容器对容器的直接通信并不会把tun/tap作为首选方案而是一般基于veth来实现的。
但tun/tap并没有像veth那样有要求设备成对出现、数据要原样传输的限制数据包到了用户态程序后我们就有完全掌控的权力要进行哪些修改、要发送到什么地方都可以通过编写代码去实现所以tun/tap方案比起veth方案有更广泛的适用范围。
那么这里我提到的veth就是另一种主流的虚拟网卡方案了在Linux Kernel 2.6版本Linux开始支持网络名空间隔离的同时也提供了专门的虚拟以太网Virtual Ethernet习惯简写为veth让两个隔离的网络名称空间之间可以互相通信。
其实直接把veth比喻成虚拟网卡并不是很准确如果要和物理设备类比它应该相当于由交叉网线连接的一对物理网卡。
额外知识:直连线序、交叉线序-
交叉网线是指一头是T568A标准另外一头是T568B标准的网线。直连网线则是两头采用同一种标准的网线。-
网卡对网卡这样的同类设备,需要使用交叉线序的网线来连接,网卡到交换机、路由器就采用直连线序的网线,不过现在的网卡大多带有线序翻转功能,直连线也可以网卡对网卡地连通了。
veth实际上也不是一个设备而是一对设备因而它也常被称作veth pair。我们要使用veth就必须在两个独立的网络名称空间中进行才有意义因为veth pair是一端连着协议栈另一端彼此相连的在veth设备的其中一端输入数据这些数据就会从设备的另一端原样不动地流出它在工作时的数据流动如下图所示
由于两个容器之间采用veth通信不需要反复多次经过网络协议栈这就让veth比起tap/tun来说具备了更好的性能也让veth pair的实现变得十分简单内核中只用几十行代码实现一个数据复制函数就可以完成veth的主体功能。
不过veth其实也存在局限性。
虽然veth以模拟网卡直连的方式很好地解决了两个容器之间的通信问题然而对多个容器间通信如果仍然单纯只用veth pair的话事情就会变得非常麻烦毕竟让每个容器都为与它通信的其他容器建立一对专用的veth pair根本就不实际真正做起来成本会很高。
因此这时,就迫切需要有一台虚拟化的交换机,来解决多容器之间的通信问题了。
交换机Linux Bridge
既然有了虚拟网卡我们很自然就会联想到让网卡接入到交换机里来实现多个容器间的相互连接。而Linux Bridge就是Linux系统下的虚拟化交换机虽然它是以“网桥”Bridge而不是“交换机”Switch为名但在使用过程中你会发现Linux Bridge看起来像交换机功能使用起来像交换机、程序实现起来也像交换机所以它实际就是一台虚拟交换机。
Linux Bridge是在Linux Kernel 2.2版本开始提供的二层转发工具由brctl命令创建和管理。Linux Bridge创建以后就能够接入任何位于二层的网络设备无论是真实的物理设备比如eth0还是虚拟的设备比如veth或者tap都能与Linux Bridge配合工作。当有二层数据包以太帧从网卡进入Linux Bridge它就会根据数据包的类型和目标MAC地址按照如下规则转发处理
如果数据包是广播帧,转发给所有接入网桥的设备。
如果数据包是单播帧且MAC地址在地址转发表中不存在则洪泛Flooding给所有接入网桥的设备并把响应设备的接口与MAC地址学习MAC Learning到自己的MAC地址转发表中。
如果数据包是单播帧且MAC地址在地址转发表中已存在则直接转发到地址表中指定的设备。
如果数据包是此前转发过的又重新发回到此Bridge说明冗余链路产生了环路。由于以太帧不像IP报文那样有TTL来约束所以一旦出现环路如果没有额外措施来处理的话就会永不停歇地转发下去。那么对于这种数据包就需要交换机实现生成树协议Spanning Tree ProtocolSTP来交换拓扑信息生成唯一拓扑链路以切断环路。
刚刚提到的这些名词比如二层转发、泛洪、STP、MAC学习、地址转发表等等都是物理交换机中已经非常成熟的概念了它们在Linux Bridge中都有对应的实现所以我才说Linux Bridge不仅用起来像交换机实现起来也像交换机。
不过它与普通的物理交换机也还是有一点差别的普通交换机只会单纯地做二层转发Linux Bridge却还支持把发给它自身的数据包接入到主机的三层协议栈中。
对于通过brctl命令显式接入网桥的设备Linux Bridge与物理交换机的转发行为是完全一致的它也不允许给接入的设备设置IP地址因为网桥是根据MAC地址做二层转发的就算设置了三层的IP地址也没有意义。
然而Linux Bridge与普通交换机的区别是除了显式接入的设备外它自己也无可分割地连接着一台有着完整网络协议栈的Linux主机因为Linux Bridge本身肯定是在某台Linux主机上创建的我们可以看作是Linux Bridge有一个与自己名字相同的隐藏端口隐式地连接了创建它的那台Linux主机。
因此Linux Bridge允许给自己设置IP地址这样就比普通交换机多出了一种特殊的转发情况如果数据包的目的MAC地址为网桥本身并且网桥设置了IP地址的话那该数据包就会被认为是收到发往创建网桥那台主机的数据包这个数据包将不会转发到任何设备而是直接交给上层三层协议栈去处理。
这时网桥就取代了物理网卡eth0设备来对接协议栈进行三层协议的处理。
那么设置这条特殊转发规则的好处是什么呢就是只要通过简单的NAT转换就可以实现一个最原始的单IP容器网络。这种组网是最基本的容器间通信形式下面我举个具体例子来帮助你理解。
假设现在有以下几个设备,它们的连接情况如图所示,具体配置是这样的:
网桥br0分配IP地址192.168.31.1。
容器三个网络名称空间容器分别编号为1、2、3均使用veth pair接入网桥且有如下配置
在容器一端的网卡名为veth0在网桥一端网卡名为veth1、veth2、veth3
三个容器中的veth0网卡分配IP地址192.168.1.10、192.168.1.11、192.168.1.12
三个容器中的veth0网卡设置网关为网桥即192.168.31.1
网桥中的veth1、veth2、veth3无IP地址。
物理网卡eth0分配的IP地址14.123.254.86。
外部网络外部网络中有一台服务器地址为122.246.6.183。
这样一来如果名称空间1中的应用程序想访问外网地址为122.246.6.183的服务器由于容器没有自己的公网IP地址程序发出的数据包必须经过处理之后才能最终到达外网服务器。
我们来具体分析下这个处理步骤:
应用程序调用Socket API发送数据此时生成的原始数据包为-
a. 源MACveth0的MAC-
b. 目标MAC网关的MAC即网桥的MAC-
c. 源IPveth0的IP即192.168.31.1-
d. 目标IP外网的IP即122.246.6.183
从veth0发送的数据会在veth1中原样出来网桥将会从veth1中接收到一个目标MAC为自己的数据包并且网桥有配置IP地址这样就触发了Linux Bridge的特殊转发规则。这个数据包也就不会转发给任何设备而是转交给主机的协议栈处理。
注意从这步以后就是三层路由了已经不在网桥的工作范围之内而是由Linux主机依靠Netfilter进行IP转发IP Forward去实现的。
数据包经过主机协议栈Netfilter的钩子被激活预置好的iptables NAT规则会修改数据包的源IP地址把它改为物理网卡eth0的IP地址并在映射表中记录设备端口和两个IP地址之间的对应关系经过SNAT之后的数据包最终会从eth0出去此时报文头中的地址为-
a. 源MACeth0的MAC-
b. 目标MAC下一跳Hop的MAC-
c. 源IPeth0的IP即14.123.254.86-
d. 目标IP外网的IP即122.246.6.183
可见经过主机协议栈后数据包的源和目标IP地址均为公网的IP这个数据包在外部网络中可以根据IP正确路由到目标服务器手上。这样当目标服务器处理完毕对该请求发出响应后返回数据包的目标地址也是公网IP。当返回的数据包经过链路上所有跳点由eth0达到网桥时报文头中的地址为-
a. 源MACeth0的MAC-
b. 目标MAC网桥的MAC-
c. 源IP外网的IP即122.246.6.183-
d. 目标IPeth0的IP即14.123.254.86
可见这同样是一个以网桥MAC地址为目标的数据包同样会触发特殊转发规则然后交给协议栈处理。这时Linux会根据映射表中的转换关系做DNAT转换把目标IP地址从eth0替换回veth0的IP最终veth0收到的响应数据包为-
a. 源MAC网桥的MAC-
b. 目标MACveth0的MAC-
c. 源IP外网的IP即122.246.6.183-
d. 目标IPveth0的IP即192.168.31.1
好了,这就是程序发出的数据包到达外网服务器之前的所有处理步骤。
在这个处理过程中Linux主机独立承担了三层路由的职责一定程度上扮演了路由器的角色。而且由于有Netfilter的存在对网络层的路由转发就不需要像Linux Bridge一样专门提供brctl这样的命令去创建一个虚拟设备了。
通过Netfilter很容易就能在Linux内核完成根据IP地址进行路由的功能。你也可以把Linux Bridge理解为是一个人工创建的虚拟交换机而Linux内核是一个天然的虚拟路由器。
当然除了我介绍的Linux Bridge这一种虚拟交换机的方案还有OVSOpen vSwitch等同样常见而且更强大、更复杂的方案这里我就不讨论了感兴趣的话你可以去参考这个链接。
网络VXLAN
那么,有了虚拟化网络设备后,下一步就是要使用这些设备组成网络了。
我们知道,容器分布在不同的物理主机上,每一台物理主机都有物理网络相互联通,然而这种网络的物理拓扑结构是相对固定的,很难跟上云原生时代下,分布式系统的逻辑拓扑结构变动频率,比如服务的扩缩、断路、限流,等等,都可能要求网络跟随做出相应的变化。
也正因为如此软件定义网络Software Defined NetworkSDN的需求在云计算和分布式时代就变得前所未有地迫切。SDN的核心思路是在物理的网络之上再构造一层虚拟化的网络把控制平面和数据平面分离开来实现流量的灵活控制为核心网络及应用的创新提供良好的平台。
SDN里位于下层的物理网络被称为Underlay它着重解决网络的连通性与可管理性位于上层的逻辑网络被称为Overlay它着重为应用提供与软件需求相符的传输服务和网络拓扑。
事实上SDN已经发展了十几年的时间比云原生、微服务这些概念出现得要早得多。网络设备商基于硬件设备开发出了EVIEthernet Virtualization Interconnect、TRILLTransparent Interconnection of Lots of Links)、SPBShortest Path Bridging等大二层网络技术软件厂商也提出了VXLANVirtual eXtensible LAN、NVGRENetwork Virtualization Using Generic Routing Encapsulation、STTA Stateless Transport Tunneling Protocol for Network Virtualization等一系列基于虚拟交换机实现的Overlay网络。
不过由于跨主机的容器间通信用的大多是Overlay网络所以接下来我会以VXLAN为例给你介绍Overlay网络的原理。
VXLAN你可能没怎么听说过但VLAN相信只要从事计算机专业的人都会有所了解。VLAN的全称是“虚拟局域网”Virtual Local Area Network从名称来看它也算是网络虚拟化技术的早期成果之一了。
由于二层网络本身的工作特性决定了VLAN非常依赖于广播无论是广播帧如ARP请求、DHCP、RIP都会产生广播帧还是泛洪路由它的执行成本会随着接入二层网络设备数量的增长而等比例地增加当设备太多广播又频繁的时候很容易就会形成广播风暴Broadcast Radiation
因此VLAN的首要职责就是划分广播域把连接在同一个物理网络上的设备区分开来。
划分的具体方法是在以太帧的报文头中加入VLAN Tag让所有广播只针对具有相同VLAN Tag的设备生效。这样既缩小了广播域也附带提高了安全性和可管理性因为两个VLAN之间不能直接通信。如果确实有通信的需要就必须通过三层设备来进行比如使用单臂路由Router on a Stick或者三层交换机。
可是VLAN有两个明显的缺陷第一个缺陷在于VLAN Tag的设计。定义VLAN的802.1Q规范是在1998年提出的当时的网络工程师完全不可能预料到在未来云计算会如此地普及因而就只给VLAN Tag预留了32 Bits的存储空间其中还要分出16 Bits存储标签协议识别符Tag Protocol Identifier、3 Bits存储优先权代码点Priority Code Point、1 Bits存储标准格式指示Canonical Format Indicator剩下的12 Bits才能用来存储VLAN IDVirtualization Network IdentifierVNI
所以换句话说VLAN ID最多只能有 \(\\mathrm{2}^{12}\)=4096种取值。当云计算数据中心出现后即使不考虑虚拟化的需求单是需要分配IP的物理设备都有可能数以万计、甚至数以十万计这样的话4096个VLAN肯定是不够用的。
后来IEEE的工程师们又提出802.1AQ规范力图补救这个缺陷大致思路是给以太帧连续打上两个VLAN Tag每个Tag里仍然只有12 Bits的VLAN ID但两个加起来就可以存储 \(\\mathrm{2}^{24}\)=16,777,216个不同的VLAN ID了由于两个VLAN Tag并排放在报文头上802.1AQ规范还有了个QinQ802.1Q in 802.1Q)的昵称别名。
QinQ是2011年推出的规范但是直到现在其实都没有特别普及这是因为除了需要设备支持外它还解决不了VLAN的第二个缺陷跨数据中心传递。
VLAN本身是为二层网络所设计的但是在两个独立数据中心之间信息只能跨三层传递。而由于云计算的灵活性大型分布式系统完全有跨数据中心运作的可能性所以此时如何让VLAN Tag在两个数据中心间传递又成了不得不考虑的麻烦事。
由此为了统一解决以上两个问题IETF定义了VXLAN规范这是三层虚拟化网络Network Virtualization over Layer 3NVO3的标准技术规范之一是一种典型的Overlay网络。
VXLAN采用L2 over L4 MAC in UDP的报文封装模式把原本在二层传输的以太帧放到了四层UDP协议的报文体内同时加入了自己定义的VXLAN Header。在VXLAN Header里直接就有24 Bits的VLAN ID同样可以存储1677万个不同的取值。
如此一来VXLAN就可以让二层网络在三层范围内进行扩展不再受数据中心间传输的限制了。VXLAN的整个报文结构如下图所示
图片来源Orchestrating EVPN VXLAN Services with Cisco NSO
VXLAN对网络基础设施的要求很低不需要专门的硬件提供特别支持只要三层可达的网络就能部署VXLAN。
VXLAN网络的每个边缘入口上布置有一个VTEPVXLAN Tunnel Endpoints设备它既可以是物理设备也可以是虚拟化设备主要负责VXLAN协议报文的封包和解包。互联网号码分配局Internet Assigned Numbers AuthorityIANA也专门分配了4789作为VTEP设备的UDP端口以前Linux VXLAN用的默认端口是8472目前这两个端口在许多场景中仍有并存的情况
从Linux Kernel 3.7版本起Linux系统就开始支持VXLAN。到了3.12版本Linux对VXLAN的支持已经达到了完全完备的程度能够处理单播和组播能够运行于IPv4和IPv6之上一台Linux主机经过简单配置之后就可以把Linux Bridge作为VTEP设备来使用。
VXLAN带来了很高的灵活性、扩展性和可管理性同一套物理网络中可以任意创建多个VXLAN网络每个VXLAN中接入的设备都像是在一个完全独立的二层局域网中一样不会受到外部广播的干扰也很难遭受外部的攻击这就让VXLAN能够良好地匹配分布式系统的弹性需求。
不过VXLAN也带来了额外的复杂度和性能开销具体表现为以下两点
传输效率的下降如果你仔细数过前面VXLAN报文结构中UDP、IP、以太帧报文头的字节数你就会发现经过VXLAN封装后的报文新增加的报文头部分就整整占了50 BytesVXLAN报文头占8 BytesUDP报文头占8 BytesIP报文头占20 Bytes以太帧的MAC头占14 Bytes而原本只需要14 Bytes而已而且现在这14 Bytes的消耗也还在只是被封到了最里面的以太帧中。以太网的MTU是1500 Bytes如果是传输大量数据额外损耗50 Bytes并不算很高的成本但如果传输的数据本来就只有几个Bytes的话那传输消耗在报文头上的成本就很高昂了。
传输性能的下降每个VXLAN报文的封包和解包操作都属于额外的处理过程尤其是用软件来实现的VTEP要知道额外的运算资源消耗有时候会成为不可忽略的性能影响因素。
副本网卡MACVLAN
现在理解了VLAN和VXLAN的原理后我们就有足够的前置知识去了解MACVLAN这最后一种网络设备虚拟化的方式了。
前面我提到两个VLAN之间位于独立的广播域是完全二层隔离的要通信就只能通过三层设备。而最简单的三层通信就是靠单臂路由了。
接下来,我就以这里的示意图中给出的网络拓扑结构为例,来给你介绍下单臂路由是如何工作的。
假设位于VLAN-A中的主机A1希望把数据包发送给VLAN-B中的主机B2由于A、B两个VLAN之间二层链路不通因此引入了单臂路由。单臂路由不属于任何VLAN它与交换机之间的链路允许任何VLAN ID的数据包通过这种接口被称为TRUNK。
这样A1要和B2通信A1就把数据包先发送给路由只需把路由设置为网关即可做到然后路由根据数据包上的IP地址得知B2的位置去掉VLAN-A的VLAN Tag改用VLAN-B的VLAN Tag重新封装数据包后发回给交换机交换机收到后就可以顺利转发给B2了。
这个过程并没什么复杂的地方但不知道你有没有注意到一个问题路由器应该设置怎样的IP地址呢
由于A1、B2各自处于独立的网段上它们又各自要把同一个路由作为网关使用这就要求路由器必须同时具备192.168.1.0/24和192.168.2.0/24的IP地址。当然如果真的就只有VLAN-A、VLAN-B两个VLAN那把路由器上的两个接口分别设置不同的IP地址然后用两条网线分别连接到交换机上也勉强算是一个解决办法。
但要知道VLAN最多可以支持4096个VLAN那如果要接四千多条网线就太离谱了。因此为了解决这个问题802.1Q规范中专门定义了子接口Sub-Interface的概念它的作用是允许在同一张物理网卡上针对不同的VLAN绑定不同的IP地址。
所以MACVLAN就借用了VLAN子接口的思路并且在这个基础上更进一步不仅允许对同一个网卡设置多个IP地址还允许对同一张网卡上设置多个MAC地址这也是MACVLAN名字的由来。
原本MAC地址是网卡接口的“身份证”应该是严格的一对一关系而MACVLAN打破了这层关系。方法就是在物理设备之上、网络栈之下生成多个虚拟的Device每个Device都有一个MAC地址新增Device的操作本质上相当于在系统内核中注册了一个收发特定数据包的回调函数每个回调函数都能对一个MAC地址的数据包进行响应当物理设备收到数据包时会先根据MAC地址进行一次判断确定交给哪个Device来处理如下图所示。
这样,我们以交换机一侧的视角来看,这个端口后面就像是另一台已经连接了多个设备的交换机一样。
用MACVLAN技术虚拟出来的副本网卡在功能上和真实的网卡是完全对等的此时真正的物理网卡实际上也确实承担着类似交换机的职责。
在收到数据包后物理网卡会根据目标MAC地址判断这个包应该转发给哪块副本网卡处理由同一块物理网卡虚拟出来的副本网卡天然处于同一个VLAN之中因此可以直接二层通信不需要将流量转发到外部网络。
那么与Linux Bridge相比这种以网卡模拟交换机的方法在目标上其实没有什么本质上的不同但MACVLAN在内部实现上则要比Linux Bridge轻量得多。
从数据流来看副本网卡的通信只比物理网卡多了一次判断而已就能获得很高的网络通信性能从操作步骤来看由于MAC地址是静态的所以MACVLAN不需要像Linux Bridge那样要考虑MAC地址学习、STP协议等复杂的算法这也进一步突出了MACVLAN的性能优势。
而除了模拟交换机的Bridge模式外MACVLAN还支持虚拟以太网端口聚合模式Virtual Ethernet Port AggregatorVEPA、Private模式、Passthru模式、Source模式等另外几种工作模式有兴趣的话你可以去参考下相关资料我就不再逐一介绍了。
容器间通信
好了,前面我们通过对虚拟化网络基础知识的一番铺垫后,现在,我们就可以尝试使用这些知识去解构容器间的通信原理了,毕竟运用知识去解决问题,才是学习网络虚拟化的根本目的。
在这节课里我们先以Docker为目标谈一谈Docker所提供的容器通信方案。当下节课介绍过CNI下的Kubernetes网络插件生态后你也许会觉得Docker的网络通信相对简单对于某些分布式系统的需求来说甚至是过于简陋了。不过虽然容器间的网络方案多种多样但通信主体都是固定的不外乎没有物理设备的虚拟主体容器、Pod、Service、Endpoints等等、不需要跨网络的本地主机、以及通过网络连接的外部主机三种层次。
所有的容器网络通信问题其实都可以归结为本地主机内部的多个容器之间、本地主机与内部容器之间以及跨越不同主机的多个容器之间的通信问题其中的许多原理都是相通的所以我认为Docker网络的简单在作为检验前面网络知识有没有理解到位时倒不失为一种优势。
好,下面我们就具体来看看吧。
Docker的网络方案在操作层面上是指能够直接通过docker run --network参数指定的网络或者是先被docker network create创建后再被容器使用的网络。安装Docker的过程中会自动在宿主机上创建一个名为docker0的网桥以及三种不同的Docker网络分别是bridge、host和none你可以通过docker network ls命令查看到这三种网络具体如下所示
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2a25170d4064 bridge bridge local
a6867d58bd14 host host local
aeb4f8df39b1 none null local
事实上这三种网络对应着Docker提供的三种开箱即用的网络方案它们分别为
桥接模式,使用--network=bridge指定这种也是未指定网络参数时的默认网络。桥接模式下Docker会为新容器分配独立的网络名称空间创建好veth pair一端接入容器另一端接入到docker0网桥上。Docker会为每个容器自动分配好IP地址默认配置下的地址范围是172.17.0.0/24docker0的地址默认是172.17.0.1并且会设置所有容器的网关均为docker0这样所有接入同一个网桥内的容器可以直接依靠二层网络来通信在此范围之外的容器、主机就必须通过网关来访问具体过程我在前面介绍Linux Bridge时已经举例讲解过了这里不再啰嗦
主机模式,使用--network=host指定。主机模式下Docker不会为新容器创建独立的网络名称空间这样容器一切的网络设施比如网卡、网络栈等都会直接使用宿主机上的容器也就不会拥有自己独立的IP地址。在这个模式下与外界通信也不需要进行NAT转换没有性能损耗但它的缺点也十分明显因为没有隔离就无法避免网络资源的冲突比如端口号就不允许重复。
空置模式,使用--network=none指定。空置模式下Docker会给新容器创建独立的网络名称空间但是不会创建任何虚拟的网络设备此时容器能看到的只有一个回环设备Loopback Device而已。提供这种方式是为了方便用户去做自定义的网络配置比如自己增加网络设备、自己管理IP地址等等。
而除了前面三种开箱即用的网络方案以外Docker还支持由用户自行创建的网络比如说
容器模式,创建容器后使用--network=container:容器名称指定。容器模式下新创建的容器将会加入指定的容器的网络名称空间共享一切的网络资源但其他资源比如文件、PID等默认仍然是隔离的。两个容器间可以直接使用回环地址localhost通信端口号等网络资源不能有冲突。
MACVLAN模式使用docker network create -d macvlan创建。这种网络模式允许为容器指定一个副本网卡容器通过副本网卡的MAC地址来使用宿主机上的物理设备所以在追求通信性能的场合这种网络是最好的选择。这里要注意Docker的MACVLAN只支持Bridge通信模式所以在功能表现上跟桥接模式是类似的。
Overlay模式使用docker network create -d overlay创建。Docker说的Overlay网络实际上就是特指VXLAN这种网络模式主要用于Docker Swarm服务之间进行通信。然而由于Docker Swarm败给了Kubernetes并没有成为主流所以这种网络模式实际上很少被人使用。
小结
这节课我从模拟网卡、交换机这些网络设备开始给你介绍了如何在Linux网络名称空间的支持下模拟出一个物理上实际并不存在但可以像物理网络一样让程序可以进行通讯的虚拟化网路。
虚拟化网络是容器编排必不可少的功能,网络的功能和性能,对应用程序各个服务间通讯都有非常密切的关联,这一点你要重点关注。在实际生产中,容器编排系统就是由一批容器通过网络交互来共同对外提供服务的,其中的开发、除错、效率优化等工作,都离不开这些基础的网络知识。
一课一思
你在使用Docker时有没有关注或者调整过它的容器通讯网络在哪些需求场景下你做出过调整呢
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给其他的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,201 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
54 _ 容器网络与生态与CNM竞争过后的CNI下的网络插件生态
你好我是周志明。前面的两节课我们学习了Linux系统本身的网络虚拟化知识今天这节课我们就来看看这些理论知识实际是如何应用于容器间网络的。
容器网络的第一个业界标准是源于Docker在2015年发布的libnetwork项目。如果你还记得在“容器的崛起”这个小章节中我提到的关于libcontainer的故事那从名字上你就能很容易地推断出libnetwork项目的目的与意义。libnetwork项目是Docker用Golang编写的、专门用来抽象容器间网络通信的一个独立模块。
类似于libcontainer是作为OCI的标准来实现的libnetwork是作为Docker提出的CNM规范Container Network Model的标准实现而设计的。不过跟ibcontainer因为孵化出runC项目到今天都仍然广为人知的结局不一样libnetwork随着Docker Swarm的失败已经基本上失去了实用的价值只具备历史与学术研究方面的价值了。
接下来我就会从CNM规范的出现以及它与CNI的竞争开始说起带你了解容器间网络所解决的问题。
CNM与CNI
首先可以说现在的容器网络的事实标准CNIContainer Networking Interface与CNM在目标上几乎是完全重叠的这就决定了CNI与CNM之间只能是你死我活的竞争关系而这与容器运行时提到的CRI和OCI的关系明显不一样。CRI与OCI的目标并不相同所以两者有足够的空间可以和平共处。
不过尽管CNM规范已是明日黄花但它作为容器网络的先行者对后续的容器网络标准的制定仍然有直接的指导意义。
要知道,提出容器网络标准的目的,就是为了把网络功能从容器运行时引擎、或者容器编排系统中剥离出去,毕竟网络的专业性和针对性极强,如果不把它变成外部可扩展的功能,而都由自己来做的话,不仅费时费力,还不讨好。这个特点从下图所列的一大堆容器网络提供商就可见一斑。
另外网络的专业性与针对性也决定了CNM和CNI都采用了插件式的设计这样需要接入什么样的网络就设计一个对应的网络插件即可。所谓的插件在形式上也就是一个可执行文件再配上相应的Manifests描述。
为了方便插件编写CNM把协议栈、网络接口对应于veth、tap/tun等和网络对应于Bridge、VXLAN、MACVLAN等分别抽象为Sandbox、Endpoint和Network并在接口的API中提供了这些抽象资源的读写操作。
而CNI中尽管也有Sandbox、Network的概念其含义也跟CNM的大致相同不过在Kubernetes资源模型的支持下它就不需要刻意去强调某一种网络资源应该如何描述、如何访问了所以在结构上就显得更加轻便。
那么从程序功能上看CNM和CNI的网络插件提供的能力都可以划分为网络的管理与IP地址的管理两类而插件可以选择只实现其中的某一个也可以全部都实现。下面我们就具体来了解一下。
管理网络创建与删除
顾名思义这项能力解决的是如何创建网络、如何将容器接入到网络以及容器如何退出和删除网络的问题。这个过程实际上是对容器网络的生命周期管理如果你更熟悉Docker命令可以把它类比理解成基本上等同于docker network命令所做的事情。
CNM规范中定义了创建网络、删除网络、容器接入网络、容器退出网络、查询网络信息、创建通信Endpoint、删除通信Endpoint等十个编程接口而CNI中就更加简单了只要实现对网络的增加与删除两项操作即可。你甚至不需要学过Golang语言只从名称上都能轻松看明白以下接口中每个方法的含义是什么。
type CNI interface {
AddNetworkList (net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
DelNetworkList (net *NetworkConfigList, rt *RuntimeConf) error
AddNetwork (net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
DelNetwork (net *NetworkConfig, rt *RuntimeConf) error
}
管理IP地址分配与回收
这项能力解决的是如何为三层网络分配唯一的IP地址的问题。我们知道二层网络的MAC地址天然就具有唯一性不需要刻意考虑如何分配的问题。但是三层网络的IP地址只有通过精心规划才能保证在全局网络中都是唯一的。否则如果两个容器之间可能存在相同地址那它们就最多只能做NAT而不可能做到直接通信。
相比起基于UUID或者数字序列实现的全局唯一ID产生器IP地址的全局分配工作要更加困难一些。
首先是要符合IPv4的网段规则而且得保证不重复这在分布式环境里就只能依赖etcd、ZooKeeper等协调工具来实现Docker自己也提供了类似的libkv来完成这项工作其次是必须考虑到回收的问题否则一旦Pod发生持续重启就有可能耗尽某个网段中的所有地址最后还必须要关注时效性原本IP地址的获取采用标准的DHCP协议Dynamic Host Configuration Protocol就可以了但DHCP有可能产生长达数秒的延迟对于某些生存周期很短的Pod这就已经超出了它的忍受限度所以在容器网络中往往Host-Local的IP分配方式会比DHCP更加实用。
总而言之虽然现在时过境迁舞台的聚光灯已然落到了CNI身上但CNM规范作为容器间网络的首个技术规范依然起到了为继任者指明方向的作用。
CNM到CNI
容器网络标准能够提供一致的网络操作界面不管是什么网络插件都使用一致的API这就提高了网络配置的自动化程度和在不同网络间迁移的体验对最终用户、容器提供商、网络提供商来说都是三方共赢的事情。
从CNM规范发布以后借助Docker在容器领域的强大号召力很快就得到了网络提供商与开源组织的支持不说专门为Docker设计针对容器互联的网络最起码也会让现有的网络方案兼容于CNM规范以便能在容器圈中多分一杯羹比如Cisco的Contiv、OpenStack的Kuryr、Open vSwitch的OVNOpen Virtual Networking以及来自开源项目的Calico和Weave等都是CNM阵营中的成员。
而唯一对CNM持有不同意见的是那些和Docker存在直接竞争关系的产品比如Docker的最大竞争对手来自CoreOS公司的RKT容器引擎。
其实凭良心说并不是其他容器引擎想刻意去抵制CNM而是Docker制定CNM规范时完全是基于Docker本身来设计的并没有考虑CNM用于其他容器引擎的可能性。因而为了平衡CNM规范的影响力也是为了在Docker的垄断背景下寻找一条出路RKT提出了与CNM目标类似的“RKT网络提案”RKT Networking Proposal
事实上一个业界标准成功与否很大程度上取决于它的支持者阵营的规模对于容器网络这种插件式的规范就更是如此了。Docker力推的CNM毫无疑问是当时统一容器网络标准的最有力的竞争者如果没有外力的介入有很大的可能会成为最后的胜利者。
然而影响容器网络发展的外力还是出现了即使我之前没有提过CNI你也应该很容易猜到在容器圈里能够掀翻Docker的“外力”也就只有Kubernetes一家而已。
Kubernetes开源的初期Kubernetes 1.5提出CRI规范之前在容器引擎上是选择彻底绑定于Docker的但是在容器网络的选择上Kubernetes一直都坚持独立于Docker自己来维护网络。
在CNM和CNI提出以前的早期版本里Kubernetes会使用Docker的空置网络模式--network=none来创建Pause容器然后通过内部的kubenet来创建网络设施再让Pod中的其他容器加入到Pause容器的名称空间中共享这些网络设施。
额外知识kubenet-
kubenet是kubelet内置的一个非常简单的网络它是采用网桥来解决Pod间通信。kubenet会自动创建一个名为cbr0的网桥当有新的Pod启动时会由kubenet自动将其接入cbr0网桥中再将控制权交还给kubelet完成后续的Pod创建流程。kubenet采用Host-Local的IP地址管理方式具体来说是根据当前服务器对应的Node资源上的PodCIDR字段所设的网段来分配IP地址。当有新的Pod启动时会由本地节点的IP段中分配一个空闲的IP给Pod使用。
其实在CNM规范还没有提出之前Kubernetes自己来维护网络是必然的结果因为Docker自带的网络基本上只聚焦于如何解决本地通信完全无法满足Kubernetes跨集群节点的容器编排的需要。而当CNM规范提出之后原本Kubernetes应该是除Docker外的最大受益者才对毕竟CNM的价值就是能很方便地引入其他网络插件来替代掉Docker自带的网络。
但Kubernetes却对Docker的CNM规范表现得很是犹豫经过一番评估考量Kubernetes最终决定转为支持当时极不成熟的RKT的网络提案他们与CoreOS合作以RKT网络提案为基础发展出了CNI规范。
Kubernetes Network SIG的Leader、Google的工程师蒂姆·霍金Tim Hockin也曾专门撰写过一篇文章《Why Kubernetes doesnt use libnetwork》来解释为什么Kubernetes要拒绝CNM与libnetwork。
当时容器编排战争还处于三国争霸Kubernetes、Apache Mesos、Docker Swarm的拉锯阶段即使强势如Kubernetes拒绝CNM其实也要冒不小的风险付出很大的代价因为这个决定不可避免地会引发一系列技术和非技术的问题比如网络提供商要为Kubernetes专门编写不同的网络插件、由docker run启动的独立容器将会无法与Kubernetes启动的容器直接相互通信等等。
而促使Kubernetes拒绝CNM的理由也同样有来自于技术和非技术方面的。
首先在技术方面Docker的网络模型做出了许多对Kubernetes无效的假设Docker的网络有本地网络不带任何跨节点协调能力比如Bridge模式就没有全局统一的IP分配和全局网络跨主机的容器通信例如Overlay模式的区别本地网络对Kubernetes来说毫无意义而全局网络又默认依赖libkv来实现全局IP地址管理等跨机器的协调工作。
这里的libkv是指Docker建立的lib*家族中的另一位成员它主要是用来对标etcd、ZooKeeper等分布式K/V存储而这对于已经拥有了etcd的Kubernetes来说就如同鸡肋。
然后在非技术方面Kubernetes决定放弃CNM的原因很大程度上还是由于他们与Docker在发展理念上的冲突Kubernetes当时已经开始推进Docker从必备依赖变为可选引擎的重构工作了而Docker则坚持CNM只能基于Docker来设计。
蒂姆·霍金在他的文章中举了一个例子CNM的网络驱动没有向外部暴露网络所连接容器的具体名称只使用了一个内部分配的ID来代替这就让外部包括网络插件和容器编排系统很难将网络连接的容器与自己管理的容器对应关联起来而当他们向Docker开发人员反馈这个问题时却以“工作符合预期结果”Working as Intended为理由被直接关闭掉了这个问题。
蒂姆·霍金还专门列出了这些问题的详细清单比如libnetwork #139、libnetwork #486、libnetwork #514、libnetwork #865、docker #18864。这种设计被Kubernetes认为是在人为地给非Docker的第三方容器引擎使用CNM设置障碍。而在整个沟通过程中Docker表现得也很强硬明确表示他们对偏离当前路线或委托控制的想法都不太欢迎。
其实刚刚提到的这些“非技术”的问题即使没有Docker的支持Kubernetes自己也不是不能从“技术上”去解决但Docker的理念会让Kubernetes感到忧虑因为Kubernetes在Docker之上扩展了很多功能而Kubernetes却并不想这些功能永远绑定在Docker之上。
CNM与libnetwork是2015年5月1日发布的CNI则是在2015年7月发布两者的正式诞生只相差不到两个月时间可见这显然是竞争的需要而不是什么单纯的巧合。
当然在五年之后的今天这场容器网络的话语权之争已经尘埃落定CNI获得了全面的胜利除了Kubernetes和RKT之外Amazon ECS、RedHat OpenShift、Apache Mesos、Cloud Foundry等容器编排圈子中除了Docker之外其他具有影响力的参与者都已经宣布支持CNI规范而原本已经加入了CNM阵营的Contiv、Calico、Weave网络提供商也纷纷推出了自己的CNI插件。
那么下面,我就带你具体了解下当前的网络插件生态,看看目前业界常用的容器间网络大体上是如何实现的。
网络插件生态
首先要说明的是到今天为止支持CNI的网络插件已经多达数十种我不太可能逐一细说。不过跨主机通信的网络实现方式来去也就Overlay模式、路由模式、Underlay模式这三种所以接下来我就不妨以网络实现模式为主线每种模式给你介绍一个具有代表性的插件以达到对网络插件生态窥斑见豹的效果。
Overlay模式
我们已经学习过Overlay网络知道这是一种虚拟化的上层逻辑网络好处在于它不受底层物理网络结构的约束有更大的自由度更好的易用性坏处是由于额外的包头封装导致信息密度降低额外的隧道封包解包会导致传输性能下降。
而在虚拟化环境(如 OpenStack网络限制往往比较多比如不允许机器之间直接进行二层通信只能通过三层转发。那么在这类被限制网络的环境里基本上就只能选择Overlay网络插件。
常见的Overlay网络插件有FlannelVXLAN模式、CalicoIPIP模式、Weave等等。这里我就以Flannel-VXLAN为例来给你介绍一下。
由CoreOS开发的Flannel可以说是最早的跨节点容器通信解决方案在很多其他网络插件的设计中都能找到Flannel的影子。
早在2014年VXLAN还没有进入Linux内核的时候Flannel就已经开始流行了。当时的Flannel只能采用自定义的UDP封包实现自己私有协议的Overlay网络由于封包、解包的操作只能在用户态中进行而数据包在内核态的协议栈中流转这就导致数据要反复在用户态、内核态之间拷贝因此性能堪忧从此Flannel就给人留下了速度慢的坏印象。
而当VXLAN进入了Linux内核以后这种内核态用户态的转换消耗已经完全消失了Flannel-VXLAN的效率比起Flannel-UDP有了很大提升所以目前已经成为最常用的容器网络插件之一。
路由模式
路由模式其实是属于Underlay模式的一种特例这里我把它单独作为一种网络实现模式来给你介绍一下。
相比起Overlay网络路由模式的主要区别在于它的跨主机通信是直接通过路由转发来实现的因而不需要在不同主机之间进行隧道封包。这种模式的好处是性能相比Overlay网络有明显提升而坏处是路由转发要依赖于底层网络环境的支持并不是你想做就能做到的。
路由网络要求要么所有主机都位于同一个子网之内都是二层连通的要么不同二层子网之间由支持边界网关协议Border Gateway ProtocolBGP的路由相连并且网络插件也同样支持BGP协议去修改路由表。
在上节课我介绍Linux网络基础知识的时候提到过Linux下不需要专门的虚拟路由因为Linux本身就具备路由的功能。而路由模式就是依赖Linux内置在系统之中的路由协议把路由表分发到子网的每一台物理主机的。这样当跨主机访问容器时Linux主机可以根据自己的路由表得知该容器具体位于哪台物理主机之中从而直接将数据包转发过去避免了VXLAN的封包解包而导致的性能降低。
常见的路由网络有FlannelHostGateway模式、CalicoBGP模式等等。这里我就以Flannel-HostGateway为例Flannel通过在各个节点上运行的Flannel AgentFlanneld把容器网络的路由信息设置到主机的路由表上这样一来所有的物理主机都拥有整个容器网络的路由数据容器间的数据包可以被Linux主机直接转发通信效率与裸机直连都相差无几。
不过因为Flannel Agent只能修改它运行主机上的路由表一旦主机之间隔了其他路由设备比如路由器或者三层交换机这个包就会在路由设备上被丢掉而要解决这种问题就必须依靠BGP路由和Calico-BGP这类支持标准BGP协议修改路由表的网络插件共同协作才行。
Underlay模式
这里的Underlay模式特指让容器和宿主机处于同一网络两者拥有相同的地位的网络方案。Underlay网络要求容器的网络接口能够直接与底层网络进行通信因此这个****模式是直接依赖于虚拟化设备与底层网络能力的。常见的Underlay网络插件有MACVLAN、SR-IOVSingle Root I/O Virtualization等。
实际上对于真正的大型数据中心、大型系统来说Underlay模式才是最有发展潜力的网络模式。这种方案能够最大限度地利用硬件的能力往往有着最优秀的性能表现。但也是由于它直接依赖于硬件与底层网络环境必须根据软、硬件情况来进行部署所以很难能做到Overlay网络那样的开箱即用的灵活性。
这里我以SR-IOV为例来给你介绍下。SR-IOV不是某种专门的网络名字而是一种将PCIe设备共享给虚拟机使用的硬件虚拟化标准目前用在网络设备上的应用比较多理论上也可以支持其他的PCIe硬件。通过SR-IOV程序员能够让硬件在虚拟机上实现独立的内存地址、中断和DMA流而不需要虚拟机管理系统的介入。
对于容器系统来说SR-IOV的价值是可以直接在硬件层面虚拟多张网卡并且以硬件直通Passthrough的形式交付给容器使用。但SR-IOV直通部署起来一般都很繁琐现在容器用的SR-IOV方案不少是使用MACVTAP来对SR-IOV网卡进行转接的。
不过虽然MACVTAP提升了SR-IOV的易用性但是这种转接又会带来额外的性能损失并不一定会比其他网络方案有更好的表现。
好了在了解过CNI插件的大致实现原理与分类后相信你的下一个问题就是哪种CNI网络最好如何选择合适的CNI插件
其实选择CNI网络插件主要有两方面的考量因素。
首先就必须是你系统所处的环境是支持的,这点我在前面已经有针对性地介绍过。然后在环境可以支持的前提下,另一个因素就是性能与功能方面是否合乎你的要求。
关于性能方面这里我引用一组测试数据来供你参考。这些数据来自于2020年8月刊登在IETF的论文《Considerations for Benchmarking Network Performance in Containerized Infrastructures》其中测试了不同CNI插件在裸金属服务器之间BMP to BMPBare Metal Pod、虚拟机之间VMP to VMPVirtual Machine Pod以及裸金属服务器与虚拟机之间BMP to VMP的本地网络和跨主机网络的通信表现。
其中,最具代表性的是裸金属服务器之间的跨主机通信,这里我把它的结果列了出来,你可以去看看:
那么从测试结果来看MACVLAN和SR-IOV这样的Underlay网络插件的吞吐量最高、延迟最低所以只从网络性能上看它们肯定是最优秀的。而相对来说Flannel-VXLAN这样的Overlay网络插件它的吞吐量只有MACVLAN和SR-IOV的70%左右,延迟更是高了两至三倍之多。
所以说Overlay为了易用性、灵活性所付出的代价还是不可忽视的但是对于那些不以网络I/O为性能瓶颈的系统来说这样的代价并不是一定不能接受就看你心中对通用性与性能是如何权衡取舍的了。
而在功能方面的问题就比较简单了,这完全取决于你的需求是否能够满足。
对于容器编排系统来说网络并不是孤立的功能模块只提供网络通信就可以的比如Kubernetes的NetworkPolicy资源是用于描述“两个Pod之间是否可以访问”这类ACL策略。但它不属于CNI的范畴所以不是每个CNI插件都会支持NetworkPolicy的声明。
如果你有这方面的需求就应该放弃Flannel去选择Calico、Weave等插件。类似的其他功能上的选择的例子还有很多这里我就不一一列举了。
小结
如何保证信息安全准确快速地出传输、如何更好地连接不同的集群节点、如何连接异构的容器云平台,这些都是我们需要考虑的一系列的网络问题。
当然,容器网络技术也在持续地演进之中。我们要知道,容器间网络是把应用从单机扩展到集群的关键钥匙,但它也把虚拟化容器推入到了更复杂的境地,网络要去适应这种变化,要去适配容器的各种需求,所以才出现了百花齐放的容器网络方案。
一课一思
这节课里出现了许多中不同的容器网络,你认为对这些网络的选择,主要应该是架构师的职责,还是运维工程师的职责呢?
欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,320 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
55 _ 谈谈Kubernetes的存储设计理念
你好我是周志明。从这节课起我会用三讲带你学习容器编排系统存储方面的知识点。今天这节课我们先来探讨下Kubernetes的存储设计理念。
Kubernetes的存储设计考量
在开始之前我想先表明一下我对Kubernetes存储能力的态度。Kubernetes在规划持久化存储能力的时候依然遵循着它的一贯设计哲学用户负责以资源和声明式API来描述自己的意图Kubernetes负责根据用户意图来完成具体的操作。不过我认为就算只是描述清楚用户的存储意图也不是一件容易的事情相比Kubernetes提供的其他能力的资源它内置的存储资源其实格外地复杂甚至可以说是有些繁琐的。
如果你是Kubernetes的拥趸不能认同我对Kubernetes的批评那不妨来看一看下列围绕着“Volume”所衍生出的概念它们仅仅是与Kubernetes存储相关概念的一个子集而已你在看的时候也可以来思考一下这些概念是否全都是必须的、是否还有整合的空间、是否有化繁为简的可能性
概念Volume、PersistentVolume、PersistentVolumeClaim、Provisioner、StorageClass、Volume Snapshot、Volume Snapshot Class、Ephemeral Volumes、FlexVolume Driver、Container Storage Interface、CSI Volume Cloning、Volume Limits、Volume Mode、Access Modes、Storage Capacity……-
操作Mount、Bind、Use、Provision、Claim、Reclaim、Reserve、Expand、Clone、Schedule、Reschedule……
其实啊Kubernetes之所以有如此多关于存储的术语概念最重要的原因是存储技术本来就有很多种类为了尽可能多地兼容各种存储Kubernetes不得不预置了很多In-Tree意思是在Kubernetes的代码树里插件来对接让用户根据自己的业务按需选择。
同时为了兼容那些不在预置范围内的需求场景Kubernetes也支持用户使用FlexVolume或者CSI来定制Out-of-Tree意思是在Kubernetes的代码树之外的插件实现更加丰富多样的存储能力。下表中列出了Kubernetes目前提供的一部分存储与扩展的插件
事实上迫使Kubernetes存储设计得如此复杂的原因除了是要扩大兼容范畴之外还有一个非技术层面的因素就是Kubernetes是一个工业级的、面向生产应用的容器编排系统。
而这就意味着即使Kubernetes发现了某些已存在的功能有更好的实现方式但直到旧版本被淘汰出生产环境以前原本已支持的功能都不允许突然间被移除或者替换掉。否则当生产系统更新版本时已有的功能就会出现异常那就会极大威胁到产品的信誉。
当然在一定程度上我们可以原谅Kubernetes为了实现兼容而导致的繁琐但这样的设计确实会让Kubernetes的学习曲线变得更加陡峭。
Kubernetes提供的官方文档的主要作用是为实际开发提供参考它并不会告诉你Kubernetes中各种概念的演化历程、版本发布新功能的时间线、改动的缘由与背景等信息只会以“平坦”的方式来陈述所有目前可用的功能这可能有利于熟练的管理员快速查询到关键信息却不利于初学者去理解Kubernetes的设计思想。
如此一来因为很难理解那些概念和操作的本意初学者往往就只能死记硬背很难分辨出它们应该如何被“更正确”地使用。而介绍Kubernetes设计理念的职责只能由Kubernetes官方的Blog 这类信息渠道,或者其他非官方资料去完成。
所以接下来我会从Volume的概念开始以操作系统到Docker再到Kubernetes的演进历程为主线带你去梳理前面提到的那些概念与操作以此帮你更好地理解Kubernetes的存储设计。
首先我们来看看Mount和Volume这两个概念。
Mount和Volume
Mount和Volume都是来源于操作系统的常用术语Mount是动词表示将某个外部存储挂载到系统中Volume是名词表示物理存储的逻辑抽象目的是为物理存储提供有弹性的分割方式。
而我们知道容器是源于对操作系统层的虚拟化为了满足容器内生成数据的外部存储需求我们也很自然地会把Mount和Volume的概念延至容器中。因此要想了解容器存储的发展我们不妨就以Docker的Mount操作为起始点。
目前Docker内建支持了三种挂载类型分别是Bind--mount type=bind、Volume--mount type=volume和tmpfs--mount type=tmpfs如下图所示。其中tmpfs主要用于在内存中读写临时数据跟我们这个小章节要讨论的对象“持久化存储”并不相符所以后面我们只着重关注Bind和Volume两种挂载类型就可以了。
图片来自Docker官网文档
我们先来聊聊Bind。
Bind Mount是Docker最早提供的发布时就支持挂载类型作用是把宿主机的某个目录或文件挂载到容器的指定目录或文件比如下面命令中参数-v表达的意思就是把外部的HTML文档挂到Nginx容器的默认网站根目录下
docker run -v /icyfenix/html:/usr/share/nginx/html nginx:latest
请注意,虽然命令中-v参数是--volume的缩写但-v最初只是用来创建Bind Mount而不是创建Volume Mount的。
这种迷惑的行为其实也并不是Docker的本意只是因为Docker刚发布的时候考虑得不够周全随随便便就在参数中占用了“Volume”这个词到后来真的需要扩展Volume的概念来支持Volume Mount的时候前面的-v已经被用户广泛使用了所以也就只能如此将就着继续用。
从Docker 17.06版本开始Bind就在Docker Swarm中借用了--mount参数过来这个参数默认创建的是Volume Mount用户可以通过明确的type子参数来指定另外两种挂载类型。比如说前面给到的命令就可以等价于下面所示的--mount版本
docker run --mount type=bind,source=/icyfenix/html,destination=/usr/share/nginx/html nginx:latest
从Bind Mount到Volume Mount实质上是容器发展过程中对存储抽象能力提升的外在表现。我们根据“Bind”这个名字以及Bind Mount的实际功能其实可以合理地推测Docker最初认为“Volume”就只是一种“外部宿主机的磁盘存储到内部容器存储的映射关系”但后来它眉头一皱发现事情并没有那么简单存储的位置并不局限只在外部宿主机存储的介质并不局限只是物理磁盘存储的管理也并不局限只有映射关系。
我给你举几个例子。
比如Bind Mount只能让容器与本地宿主机之间建立某个目录的映射那么如果想要在不同宿主机上的容器共享同一份存储就必须先把共享存储挂载到每一台宿主机操作系统的某个目录下然后才能逐个挂载到容器内使用这种跨宿主机共享存储的场景如下图所示
图片来自Docker官网文档
这种存储范围超越了宿主机的共享存储,配置过程却要涉及到大量与宿主机环境相关的操作,只能由管理员人工地去完成,不仅繁琐,而且由于每台宿主机环境的差异,还会导致主机很难实现自动化。
再比如即使只考虑单台宿主机的情况基于可管理性的需求Docker也完全有支持Volume Mount的必要。为什么这么说呢
实际上在Bind Mount的设计里Docker只有容器的控制权存放容器生产数据的主机目录是完全独立的与Docker没有任何关系它既不受Docker保护也不受Docker管理。所以这就使得数据很容易被其他进程访问到甚至是被修改和删除。如果用户想对挂载的目录进行备份、迁移等管理运维操作也只能在Docker之外靠管理员人工进行而这些都增加了数据安全与操作意外的风险。
因此Docker希望能有一种抽象的资源来代表在宿主机或网络中存储的区域以便让Docker能管理这些资源这样就很自然地联想到了操作系统里的Volume。
提出Volume最核心的一个目的是为了提升Docker对不同存储介质的支撑能力这同时也是为了减轻Docker本身的工作量。
要知道存储并不是只有挂载在宿主机上的物理存储这一种介质。在云计算时代网络存储逐渐成为了数据中心的主流选择不同的网络存储都有各自的协议和交互接口。而且并不是所有的存储系统都适合先挂载到操作系统然后再挂载到容器的如果Docker想要越过操作系统去支持挂载某种存储系统首先必须要知道该如何访问它然后才能把容器中的读写操作自动转移到该位置。
Docker把解决如何访问存储的功能模块叫做存储驱动Storage Driver。通过docker info命令你能查看到当前Docker所支持的存储驱动。虽然Docker已经内置了市面上主流的OverlayFS驱动比如Overlay、Overlay2、AUFS、BTRFS、ZFS等等但面对云计算的快速迭代只靠Docker自己来支持全部云计算厂商的存储系统是完全不现实的。
为此Docker就提出了与Storage Driver相对应的Volume Driver卷驱动的概念。
我们可以通过docker plugin install命令安装外部的卷驱动并在创建Volume时指定一个与其存储系统相匹配的卷驱动。比如我们希望数据存储在AWS Elastic Block Store上就找一个AWS EBS的驱动如果想存储在Azure File Storage上也是找一个对应的Azure File Storage驱动即可。
而如果在创建Volume时不指定卷驱动那默认就是local类型在Volume中存放的数据就会存储在宿主机的/var/lib/docker/volumes/目录之中。
Static Provisioning
好了了解了Mount和Volume的概念含义之后现在我们把讨论主角转回容器编排系统上。
这里我们会从存储如何分配、持久存储与非持久存储的差异出发来具体学习下Static Provisioning的设计。
首先我们可以明确一件事即Kubernetes同样是把操作系统和Docker的Volume概念延续了下来并对其进行了进一步的细化。
Kubernetes把Volume分为了持久化的PersistentVolume和非持久化的普通Volume两类这里为了不跟我前面定义的Volume这个概念产生混淆后面课程我提到的Kubernetes中非持久化的Volume时都会带着“普通”这个前缀。
普通Volume的设计目标并不是为了持久地保存数据而是为同一个Pod中多个容器提供可共享的存储资源所以普通Volume的生命周期非常明确也就是与挂载它的Pod有着相同的生命周期。
这样就意味着尽管普通Volume不具备持久化的存储能力但至少比Pod中运行的任何容器的存活期都更长Pod中不同的容器能共享相同的普通Volume当容器重新启动时普通Volume中的数据也能够得到保留。
当然一旦整个Pod被销毁普通Volume也就不复存在了数据在逻辑上也会被销毁掉。至于实际中是否会真正删除数据就取决于存储驱动具体是如何实现Unmount、Detach、Delete接口的这个小章节的主题是“持久化存储”所以关于无持久化能力的普通Volume我就不再展开了
如此一来从操作系统里传承下来的Volume概念就在Docker和Kubernetes中继续按照一致的逻辑延伸拓展了只不过Kubernetes为了把它跟普通Volume区别开来专门取了PersistentVolume这个名字。你可以从下图中直观地看出普通Volume、PersistentVolume和Pod之间的关系差异
其实我们从Persistent这个单词的意思就能大致了解PersistentVolume的含义它是指能够将数据进行持久化存储的一种资源对象。
PersistentVolume可以独立于Pod存在生命周期与Pod无关所以也就决定了PersistentVolume不应该依附于任何一个宿主机节点否则必然会对Pod调度产生干扰限制。我们在前面“Docker的三种挂载类型”图例中可以看到“Persistent”一列里都是网络存储这便是很好的印证。
额外知识Local PersistentVolume-
对于部署在云端数据中心的系统,通过网络访问同一个可用区中的远程存储,速度是完全可以接受的。但对于私有部署的系统来说,基于性能考虑,使用本地存储往往会更加常见。-
因此考虑到这样的实际需求从1.10版起Kubernetes开始支持Local PersistentVolume这是一种将一整块本地磁盘作为PersistentVolume供容器使用的专用方案。-
所谓的“专用方案”就是字面意思它并不适用于全部应用Local PersistentVolume只是针对以磁盘I/O为瓶颈的特定场景的解决方案因而它的副作用就很明显由于不能保证这种本地磁盘在每个节点中都一定存在所以Kubernetes在调度时就必须考虑到PersistentVolume分布情况只能把使用了Local PersistentVolume的Pod调度到有这种PersistentVolume的节点上。-
尽管调度器中专门有个Volume Binding Mode模式来支持这项处理但是一旦使用了Local PersistentVolume还是会限制Pod的可调度范围。
那么在把PersistentVolume与Pod分离后就需要专门考虑PersistentVolume该如何被Pod所引用的问题了。
实际上原本在Pod中引用其他资源是常有的事要么是通过资源名称直接引用要么是通过标签选择器Selectors间接引用。但是类似的方法在这里却都不太妥当至于原因你可以先思考一下“Pod该使用何种存储”这件事情应该是系统管理员运维人员说的算还是由用户开发人员说的算
要我看最合理的答案是他们一起说的才算因为只有开发能准确评估Pod运行需要消耗多大的存储空间只有运维能清楚地知道当前系统可以使用的存储设备状况。
所以为了让这二者能够各自提供自己擅长的信息Kubernetes又额外设计出了PersistentVolumeClaim资源。
其实在Kubernetes官方给出的概念定义中也特别强调了PersistentVolume是由管理员运维人员负责维护的用户开发人员通过PersistentVolumeClaim来匹配到合乎需求的PersistentVolume。
PersistentVolume & PersistentVolumeClaim-
A PersistentVolume PV is a piece of storage in the cluster that has been provisioned by an administrator.-
A PersistentVolumeClaim PVC is a request for storage by a user.-
PersistentVolume是由管理员负责提供的集群存储。-
PersistentVolumeClaim是由用户负责提供的存储请求。-
—— Kubernetes Reference DocumentationPersistent Volumes
PersistentVolume是Volume这个抽象概念的具象化表现通俗点儿说即它是已经被管理员分配好的具体的存储。
这里的“具体”是指有明确的存储系统地址有明确的容量、访问模式、存储位置等信息而PersistentVolumeClaim是Pod对其所需存储能力的声明通俗地说就是“如果要满足这个Pod正常运行需要满足怎样的条件”比如要消耗多大的存储空间、要支持怎样的访问方式。
所以,实际上管理员和用户并不是谁引用谁的固定关系,而是根据实际情况动态匹配的。
下面我们就来看看这两者配合工作的具体过程:
管理员准备好要使用的存储系统它应该是某种网络文件系统NFS或者云储存系统一般来说应该具备跨主机共享的能力。
管理员会根据存储系统的实际情况手工预先分配好若干个PersistentVolume并定义好每个PersistentVolume可以提供的具体能力。如下面例子所示
apiVersion: v1
kind: PersistentVolume
metadata:
name: nginx-html
spec:
capacity:
storage: 5Gi # 最大容量为5GB
accessModes:
- ReadWriteOnce # 访问模式为RXO
persistentVolumeReclaimPolicy: Retain # 回收策略是Retain
nfs: # 存储驱动是NFS
path: /html
server: 172.17.0.2
这里我们来简单分析下以上YAML中定义的存储能力
存储的最大容量是5GB。
存储的访问模式是“只能被一个节点读写挂载”ReadWriteOnceRWO另外两种可选的访问模式是“可以被多个节点以只读方式挂载”ReadOnlyManyROX和“可以被多个节点读写挂载”ReadWriteManyRWX
存储的回收策略是Retain即在Pod被销毁时并不会删除数据。另外两种可选的回收策略分别是Recycle 即在Pod被销毁时由Kubernetes自动执行rm -rf /volume/*这样的命令来自动删除资料以及Delete它让Kubernetes自动调用AWS EBS、GCE PersistentDisk、OpenStack Cinder这些云存储的删除指令。
存储驱动是NFS其他常见的存储驱动还有AWS EBS、GCE PD、iSCSI、RBDCeph Block Device、GlusterFS、HostPath等等。
用户根据业务系统的实际情况创建PersistentVolumeClaim声明Pod运行所需的存储能力。如下面例子所示
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: nginx-html-claim
spec:
accessModes:
- ReadWriteOnce # 支持RXO访问模式
resources:
requests:
storage: 5Gi # 最小容量5GB
可以看到在以上YAML中声明了要求容量不得小于5GB必须支持RWO的访问模式。
Kubernetes在创建Pod的过程中会根据系统中PersistentVolume与PersistentVolumeClaim的供需关系对两者进行撮合如果系统中存在满足PersistentVolumeClaim声明中要求能力的PersistentVolume就表示撮合成功它们将会被绑定。而如果撮合不成功Pod就不会被继续创建直到系统中出现新的、或让出空闲的PersistentVolume资源。
以上几步都顺利完成的话意味着Pod的存储需求得到满足进而继续Pod的创建过程。
以上的整个运作过程如下图所示:
图片来自《Kubernetes in Action》
Kubernetes对PersistentVolumeClaim与PersistentVolume撮合的结果是产生一对一的绑定关系“一对一”的意思是PersistentVolume一旦绑定在某个PersistentVolumeClaim上直到释放以前都会被这个PersistentVolumeClaim所独占不能再与其他PersistentVolumeClaim进行绑定。
这意味着即使PersistentVolumeClaim申请的存储空间比PersistentVolume能够提供的要少依然要求整个存储空间都为该PersistentVolumeClaim所用这有可能会造成资源的浪费。
比如某个PersistentVolumeClaim要求3GB的存储容量当前Kubernetes手上只剩下一个5GB的PersistentVolume了此时Kubernetes只好将这个PersistentVolume与申请资源的PersistentVolumeClaim进行绑定平白浪费了2GB空间。
假设后续有另一个PersistentVolumeClaim申请2GB的存储空间那它也只能等待管理员分配新的PersistentVolume或者有其他PersistentVolume被回收之后才被能成功分配。
Dynamic Provisioning
对于中小规模的Kubernetes集群PersistentVolume已经能够满足有状态应用的存储需求。PersistentVolume依靠人工介入来分配空间的设计虽然简单直观却算不上是先进一旦应用规模增大PersistentVolume很难被自动化的问题就会凸显出来。
这是由于Pod创建过程中需要去挂载某个Volume时都要求该Volume必须是真实存在的否则Pod启动可能依赖的数据如一些配置、数据、外部资源等都将无从读取。Kubernetes虽然有能力随着流量压力和硬件资源状况自动扩缩Pod的数量但是当Kubernetes自动扩展出一个新的Pod后并没有办法让Pod去自动挂载一个还未被分配资源的PersistentVolume。
想解决这个问题要么允许多个不同的Pod都共用相同的PersistentVolumeClaim这种方案确实只靠PersistentVolume就能解决却损失了隔离性难以通用要么就要求每个Pod用到的PersistentVolume都是已经被预先建立并分配好的这种方案靠管理员提前手工分配好大量的存储也可以实现却损失了自动化能力。
无论哪种情况都难以符合Kubernetes工业级编排系统的产品定位对于大型集群面对成百上千来自成千上万的Pod靠管理员手工分配存储肯定是无法完成的。在2017年Kubernetes发布1.6版本后终于提供了今天被称为Dynamic Provisioning的动态存储解决方案让系统管理员摆脱了人工分配的PersistentVolume的窘境并把此前的分配方式称为Static Provisioning。
那Dynamic Provisioning方案是如何解放系统管理员的呢我们先来看概念Dynamic Provisioning方案是指在用户声明存储能力的需求时不是期望通过Kubernetes撮合来获得一个管理员人工预置的PersistentVolume而是由特定的资源分配器Provisioner自动地在存储资源池或者云存储系统中分配符合用户存储需要的PersistentVolume然后挂载到Pod中使用完成这项工作的资源被命名为StorageClass它的具体工作过程如下
管理员根据储系统的实际情况先准备好对应的Provisioner。Kubernetes官方已经提供了一系列预置的In-Tree Provisioner放置在kubernetes.io的API组之下。其中部分Provisioner已经有了官方的CSI驱动如vSphere的Kubernetes自带驱动为kubernetes.io/vsphere-volumeVMware的官方驱动为csi.vsphere.vmware.com。
管理员不再是手工去分配PersistentVolume而是根据存储去配置StorageClass。Pod是可以动态扩缩的而存储则是相对固定的哪怕使用的是具有扩展能力的云存储也会将它们视为存储容量、IOPS等参数可变的固定存储来看待比如你可以将来自不同云存储提供商、不同性能、支持不同访问模式的存储配置为各种类型的StorageClass这也是它名字中“Class”类型的由来如下面这个例子
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs #AWS EBS的Provisioner
parameters:
type: gp2
reclaimPolicy: Retain
用户依然通过PersistentVolumeClaim来声明所需的存储但是应在声明中明确指出该由哪个StorageClass来代替Kubernetes处理该PersistentVolumeClaim的请求如下面这个例子
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: standard-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard #明确指出该由哪个StorageClass来处理该PersistentVolumeClaim的请求
resource:
requests:
storage: 5Gi
如果PersistentVolumeClaim中要求的StorageClass及它用到的Provisioner均是可用的话那这个StorageClass就会接管掉原本由Kubernetes撮合的PersistentVolume和PersistentVolumeClaim的操作按照PersistentVolumeClaim中声明的存储需求自动产生出满足该需求的PersistentVolume描述信息并发送给Provisioner处理。
Provisioner接收到StorageClass发来的创建PersistentVolume请求后会操作其背后存储系统去分配空间如果分配成功就生成并返回符合要求的PersistentVolume给Pod使用。
前面这几步都顺利完成的话就意味着Pod的存储需求得到了满足会继续Pod的创建过程整个过程如下图所示。
图片来自《Kubernetes in Action》
好了通过刚刚的讲述相信你可以看出Dynamic Provisioning与Static Provisioning并不是各有用途的互补设计而是对同一个问题先后出现的两种解决方案。你完全可以只用Dynamic Provisioning来实现所有的Static Provisioning能够实现的存储需求包括那些不需要动态分配的场景甚至之前例子里使用HostPath在本地静态分配存储都可以指定no-provisioner作为Provisioner的StorageClass以Local Persistent Volume来代替比如下面这个例子
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
所以说相较于Static Provisioning使用Dynamic Provisioning来分配存储无疑是更合理的设计不仅省去了管理员的人工操作的中间层也不再需要将PersistentVolume这样的概念暴露给最终用户因为Dynamic Provisioning里的PersistentVolume只是处理过程的中间产物用户不再需要接触和理解它只需要知道由PersistentVolumeClaim去描述存储需求由StorageClass去满足存储需求即可。只描述意图而不关心中间具体的处理过程是声明式编程的精髓也是流程自动化的必要基础。
除此之外由Dynamic Provisioning来分配存储还能获得更高的可管理性。如前面提到的回收策略当希望PersistentVolume跟随Pod一同被销毁时以前经常会配置回收策略为Recycle来回收空间即让系统自动执行rm -rf /volume/*命令。
但是这种方式往往过于粗暴要是遇到更精细的管理需求如“删除到回收站”或者“敏感信息粉碎式彻底删除”这样的功能实现起来就很麻烦。而Dynamic Provisioning中由于有Provisioner的存在如何创建、如何回收都是由Provisioner的代码所管理的这就带来了更高的灵活性。所以现在Kubernetes官方已经明确建议废弃掉Recycle策略如果有这类需求就改由Dynamic Provisioning去实现了。
另外相较于Dynamic ProvisioningStatic Provisioning的主要使用场景就局限于管理员能够手工管理存储的小型集群它符合很多小型系统尤其是私有化部署系统的现状但并不符合当今运维自动化所提倡的思路。Static Provisioning的存在某种意义上也可以视为是对历史的一种兼容在可见的将来Kubernetes肯定还是会把Static Provisioning作为用户分配存储的一种主要方案来供用户选用。
小结
容器是镜像的运行时实例,为了保证镜像能够重复地产生出具备一致性的运行时实例,必须要求镜像本身是持久而稳定的,这就决定了在容器中发生的一切数据变动操作,都不能真正写入到镜像当中,否则必然会破坏镜像稳定不变的性质。
为此容器中的数据修改操作大多是基于写入时复制Copy-on-Write策略来实现的容器会利用叠加式文件系统OverlayFS的特性在用户意图对镜像进行修改时自动将变更的内容写入到独立区域再与原有数据叠加到一起使其外观上看起来像是“覆盖”了原有内容。这种改动通常都是临时的一旦容器终止运行这些存储于独立区域中的变动信息也将被一并移除不复存在。所以可见如果不去进行额外的处理容器默认是不具备持久化存储能力的。
而另一方面容器作为信息系统的运行载体必定会产生出有价值的、应该被持久保存的信息比如扮演数据库角色的容器大概没有什么系统能够接受数据库像缓存服务一样重启之后会丢失全部数据多个容器之间也经常需要通过共享存储来实现某些交互操作比如我在第48讲中曾经举过的例子Nginx容器产生日志、Filebeat容器收集日志两者就需要共享同一块日志存储区域才能协同工作。
而正因为镜像的稳定性与生产数据持久性存在矛盾,所以我们才需要去重点了解这个问题:如何实现容器的持久化存储。
一课一思
不知你是否察觉这节课里还埋藏了一条暗线的逻辑以Kubernetes的存储为样例讨论当新的更好的解决方案出来之后系统对既有旧方案和旧功能的兼容。这是很多场景中都会遇到的问题系统设计必须考虑现实情况必须有所妥协很难单纯去追求理论上的最优解。越大规模的应用通常都带着更大的现实牵绊。如果你也有这样的经历不妨留言与我分享一下。
如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,166 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
56 _ Kubernetes存储扩展架构一个真实的存储系统如何接入或移除新存储设备
你好,我是周志明。
我们知道容器存储具有很强的多样性如何对接后端实际的存储系统并且完全发挥出它所有的性能与功能并不是Kubernetes团队所擅长的工作这件事情只有存储提供商才能做到最好。所以我们其实可以理解容器编排系统为什么会有很强烈的意愿想把存储功能独立到外部去实现。
在上节课我已经反复提到过多次In-Tree、Out-of-Tree插件那么今天这节课我就会以存储插件的接口与实现为中心带你去解析Kubernetes的容器存储生态。
Kubernetes存储架构
在正式开始讲解Kubernetes的In-Tree、Out-of-Tree存储插件前我们有必要先去了解一点Kubernetes存储架构的知识。了解一个真实的存储系统是如何接入到新创建的Pod中成为可以读写访问的Volume以及当Pod被销毁时Volume如何被回收回归到存储系统之中的。
那么对于刚刚所说的这几点Kubernetes其实是参考了传统操作系统接入或移除新存储设备的做法把接入或移除外部存储这件事情分解为了以下三个操作
决定应准备Provision何种存储Provision可类比为给操作系统扩容而购买了新的存储设备。这步确定了接入存储的来源、容量、性能以及其他技术参数它的逆操作是移除Delete存储。
将准备好的存储附加Attach到系统中Attach可类比为将存储设备接入操作系统此时尽管设备还不能使用但你已经可以用操作系统的fdisk -l命令查看到设备。这步确定了存储的设备名称、驱动方式等面向系统侧的信息它的逆操作是分离Detach存储设备。
将附加好的存储挂载Mount到系统中Mount可类比为将设备挂载到系统的指定位置也就是操作系统中mount命令的作用。这步确定了存储的访问目录、文件系统格式等面向应用侧的信息它的逆操作是卸载Unmount存储设备。
实际上前面步骤中提到的Provision、Delete、Attach、Detach、Mount、Unmount六种操作并不是直接由Kubernetes来实现而是在存储插件中完成的。它们会分别被Kubernetes通过两个控制器及一个管理器来进行调用这些控制器、管理器的作用如下
PV控制器PersistentVolume Controller
“以容器构建系统”这个小章节中我介绍过Kubernetes里所有的控制器都遵循着相同的工作模式即让实际状态尽可能接近期望状态。PV控制器的期望状态有两个分别是“所有未绑定的PersistentVolume都能处于可用状态”以及“所有处于等待状态的PersistentVolumeClaim都能配对到与之绑定的PersistentVolume”。
它内部也有两个相对独立的核心逻辑ClaimWorker和VolumeWorker来分别跟踪这两种期望状态。可以简单地理解为PV控制器实现了PersistentVolume和PersistentVolumeClaim的生命周期管理职能。在这个过程中它会根据需要调用存储驱动插件的Provision/Delete操作。
AD控制器Attach/Detach Controller
AD控制器的期望状态是“所有被调度到准备新创建Pod的节点都附加好了要使用的存储当Pod被销毁后原本运行Pod的节点都分离了不再被使用的存储”。如果实际状态不符合该期望会根据需要调用存储驱动插件的Attach/Detach操作。
Volume管理器Volume Manager
Volume管理器实际上是kubelet众多管理器的其中一个它主要作用是支持本节点中Volume执行Attach/Detach/Mount/Unmount操作。你可能注意到这里不仅有Mount/Unmount操作也出现了Attach/Detach操作。
这是历史原因导致的因为最初版本的Kubernetes中并没有AD控制器Attach/Detach的职责也在kubelet中完成。而现在kubelet默认情况下已经不再会执行Attach/Detach了但有少量旧程序已经依赖了由kubelet来实现Attach/Detach的内部逻辑所以kubelet不得不设计一个--enable-controller-attach-detach参数如果将其设置为false的话就会重新回到旧的兼容模式上由kubelet代替AD控制器来完成Attach/Detach。
这样一来后端的真实存储经过Provision、Attach、Mount操作之后就形成了可以在容器中挂载的Volume。当存储的生命周期完结经过Unmount、Detach、Delete操作之后Volume便能够被存储系统回收。而对于某些存储来说其中有一些操作可能是无效的比如NFS实际使用并不需要Attach此时存储插件只需将Attach实现为空操作即可。
FlexVolume与CSI
Kubernetes目前同时支持FlexVolume与CSIContainer Storage Interface两套独立的存储扩展机制。FlexVolume是Kubernetes早期版本1.2版开始提供1.8版达到GA状态就开始支持的扩展机制它是只针对Kubernetes的私有的存储扩展目前已经处于冻结状态可以正常使用但不再发展新功能了。
CSI则是从Kubernetes 1.9开始加入1.13版本达到GA状态的扩展机制如同之前我介绍过的CRI和CNI那样CSI是公开的技术规范。任何容器运行时、容器编排引擎只要愿意支持都可以使用CSI规范去扩展自己的存储能力这是目前Kubernetes重点发展的扩展机制。
由于是专门为Kubernetes量身订造的所以FlexVolume的实现逻辑与上节课我介绍的Kubernetes存储架构高度一致。FlexVolume驱动其实就是一个实现了Attach、Detach、Mount、Unmount操作的可执行文件甚至可以仅仅是个Shell脚本而已。该可执行文件应该存放在集群每个节点的/usr/libexec/kubernetes/kubelet-plugins/volume/exec目录里其工作过程也就是当AD控制器和Volume管理器需要进行Attach、Detach、Mount、Unmount操作时自动调用它的对应方法接口如下图所示。
FlexVolume Driver工作过程
如果仅仅考虑支持最基本的Static Provisioning那实现一个FlexVolume Driver确实是非常简单的。然而也是由于FlexVolume过于简单了导致它应用起来会有诸多不便之处比如说
FlexVolume并不是全功能的驱动FlexVolume不包含Provision和Delete操作也就无法直接用于Dynamic Provisioning想要实现这个功能除非你愿意再单独编写一个External Provisioner。
FlexVolume部署维护都相对繁琐FlexVolume是独立于Kubernetes的可执行文件当集群节点增加时需要由管理员在新节点上部署FlexVolume Driver。为了避免耗费过多人力有经验的系统管理员通常会专门编写一个DaemonSet来代替人工来完成这项任务。
FlexVolume实现复杂交互也相对繁琐FlexVolume的每一次操作都是对插件可执行文件的一次独立调用这种插件实现方式在各种操作需要相互通讯时会很别扭。比如你希望在执行Mount操作的时候生成一些额外的状态信息并在后面执行Unmount操作时去使用这些信息时却只能把信息记录在某个约定好的临时文件中这样的做法对于一个面向生产的容器编排系统来说实在是过于简陋了。
相比起FlexVolume的种种不足CSI可算是一个十分完善的存储扩展规范。这里“十分完善”可不是客套话根据GitHub的自动代码行统计FlexVolume的规范文档仅有155行而CSI则长达2704行。
那么从总体上看CSI规范可以分为需要容器系统去实现的组件以及需要存储提供商去实现的组件两大部分。前者包括了存储整体架构、Volume的生命周期模型、驱动注册、Volume创建、挂载、扩容、快照、度量等内容这些Kubernetes都已经完整地实现了大体上包括以下几个组件
Driver Register负责注册第三方插件CSI 0.3版本之后已经处于Deprecated状态将会被Node Driver Register所取代。
External Provisioner调用第三方插件的接口来完成数据卷的创建与删除功能。
External Attacher调用第三方插件的接口来完成数据卷的挂载和操作。
External Resizer调用第三方插件的接口来完成数据卷的扩容操作。
External Snapshotter调用第三方插件的接口来完成快照的创建和删除。
External Health Monitor调用第三方插件的接口来提供度量监控数据。
但是需要存储提供商去实现的组件才是CSI的主体部分也就是我在前面多次提到的“第三方插件”。这部分着重定义了外部存储挂载到容器过程中所涉及操作的抽象接口和具体的通讯方式主要包括以下三个gRPC接口
CSI Identity接口用于描述插件的基本信息比如插件版本号、插件所支持的CSI规范版本、插件是否支持存储卷创建、删除功能、是否支持存储卷挂载功能等等。此外Identity接口还用于检查插件的健康状态开发者可以通过Probe接口对外提供存储的健康度量信息。
CSI Controller接口用于从存储系统的角度对存储资源进行管理比如准备和移除存储Provision、Delete操作、附加与分离存储Attach、Detach操作、对存储进行快照等等。存储插件并不一定要实现这个接口的所有方法对于存储本身就不支持的功能可以在CSI Identity接口中声明为不提供。
CSI Node接口用于从集群节点的角度对存储资源进行操作比如存储卷的分区和格式化、将存储卷挂载到指定目录上或者将存储卷从指定目录上卸载等等。
CSI组件架构
与FlexVolume以单独的可执行程序的存在形式不同CSI插件本身是由一组标准的Kubernetes资源所构成CSI Controller接口是一个以StatefulSet方式部署的gRPC服务CSI Node接口则是基于DaemonSet方式部署的gRPC服务。
这意味着虽然CSI实现起来要比FlexVolume复杂得多但是却很容易安装——如同安装CNI插件及其它应用那样直接载入Manifest文件即可也不会遇到FlexVolume那样需要人工运维或者自己编写DaemonSet来维护集群节点变更的问题。
此外通过gRPC协议传递参数比通过命令行参数传递参数更加严谨灵活和可靠最起码不会出现多个接口之间协作只能写临时文件这样的尴尬状况。
从In-Tree到Out-of-Tree
Kubernetes原本曾内置了相当多的In-Tree的存储驱动甚至还早于Docker宣布支持卷驱动功能这种策略使得Kubernetes能够在云存储提供商发布官方驱动之前就将其纳入到支持范围中同时也减轻了管理员维护的工作量为它在诞生初期快速占领市场做出了一定的贡献。
但是这种策略也让Kubernetes丧失了随时添加或修改存储驱动的灵活性只能在更新大版本时才能加入或者修改驱动导致云存储提供商被迫要与Kubernetes的发版节奏保持一致。此外这个策略还涉及到第三方存储代码混杂在Kubernetes二进制文件中可能引起的可靠性及安全性问题。
因此当Kubernetes成为市场主流以后——准确的时间点是从1.14版本开始Kubernetes启动了In-Tree存储驱动的CSI外置迁移工作按照计划在1.21到1.22版本大约在2021年中期Kubernetes中主要的存储驱动如AWS EBS、GCE PD、vSphere等都会迁移至符合CSI规范的Out-of-Tree实现不再提供In-Tree的支持。
这种做法在设计上无疑是正确的但是这又会导致Kubernetes面临此前提过的该如何兼容旧功能的策略问题我举个例子下面YAML定义了一个Pod
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod-example
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: html-pages-volume
mountPath: /usr/share/nginx/html
- name: config-volume
mountPath: /etc/nginx
volumes:
- name: html-pages-volume
hostPath: # 来自本地的存储
path: /srv/nginx/html
type: Directory
- name: config-volume
awsElasticBlockStore: # 来自AWS ESB的存储
volumeID: vol-0b39e0b08745caef4
fsType: ext4
可以发现其中用到了类型为hostPath的Volume这相当于Docker中驱动类型为local的Volume不需要专门的驱动而类型为awsElasticBlockStore的Volume从名字上就能看出是指存储驱动为AWS EBS的Volume当CSI迁移完成awsElasticBlockStore从In-Tree卷驱动中移除掉之后它就应该按照CSI的写法改写成如下形式
- name: config-volume
csi:
driver: ebs.csi.aws.com
volumeAttributes:
- volumeID: vol-0b39e0b08745caef4
- fsType: ext4
这样的要求有悖于“升级版本不应影响还在大范围使用的已有功能”这条原则所以Kubernetes 1.17中又提出了称为CSIMigration的解决方案让Out-of-Tree的驱动能够自动伪装成In-Tree的接口来提供服务。
这里我想说明的是我之所以专门来给你介绍Volume的CSI迁移倒不是由于它算是多么重要的特性而是这种兼容性设计本身就是Kubernetes设计理念的一个缩影在Kubernetes的代码与功能中随处可见。好的设计需要权衡多个方面的利益很多时候都得顾及现实的影响要求设计向现实妥协而不能仅仅考虑理论最优的方案。
小结
这节课我们学习了Kubernetes的存储扩展架构知道了一个真实的存储系统是如何接入到新创建的Pod中成为可以读写访问的Volume以及当Pod被销毁时Volume如何被回收回归到存储系统之中的。
此外我们还要明确的是目前的Kubernetes系统中存在两种存储扩展接口分别是FlexVolume与CSI我们要知道这两种插件的相似与差异之处以及这两种接口的大致的结构。
一课一思
你使用过哪些类型的存储?你了解过块存储、文件存储、对象存储等不同的存储系统之间的差异吗?可以在留言区说说你的看法,下节课我们就会来学习这部分的知识。
如果你觉得有收获,欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
57 _ Kubernetes存储生态系统几种有代表性的CSI存储插件的实现
你好,我是周志明。
随着Kubernetes的CSI规范成为容器业界统一的存储接入标准现在几乎所有的云计算厂商都支持自家的容器通过CSI规范去接入外部存储能够应用于CSI与FlexVolume的存储插件更是多达数十上百款下图就展示了部分容器存储提供商可以说容器存储已经算是形成了初步的生态环境。
不过在咱们的课程里我并不会去展开讨论各种CSI存储插件的细节我会采取跟CNI网络插件类似的讲述方式以不同的存储类型为线索介绍其中有代表性的实现。
部分容器存储提供商
实际上,目前出现过的存储系统和设备,我们都可以划分到块存储、文件存储和对象存储这三种存储类型之中,其划分的根本依据并不是各种存储是如何储存数据的,因为那完全是存储系统私有的事情。
我认为更合理的划分依据是,各种存储会提供什么形式的接口来供外部访问数据,而不同的外部访问接口会如何反过来影响存储的内部结构、性能与功能表现。虽然块存储、文件存储和对象存储可以彼此协同工作,但它们各自都有自己明确的擅长领域与优缺点。所以,只有理解它们的工作原理,因地制宜地选择最适合的存储,才能让系统达到最佳的工作状态。
那么接下来,我就按照它们出现的时间顺序来给你一一介绍下。
块存储
块存储是数据存储最古老的形式它把数据都储存在一个或多个固定长度的块Block想要读写访问数据就必须使用与存储相匹配的协议SCSI、SATA、SAS、FCP、FCoE、iSCSI……
这里你可以类比一下前面第52讲提到的网络通讯中网络栈的数据流动过程你可以把存储设备中由块构成的信息流与网络设备中由数据包构成的信息流进行对比。事实上像iSCSI这种协议真的就是建设在TCP/IP网络之上让上层以SCSI作为应用层协议对外提供服务的。
我们熟悉的硬盘就是最经典的块存储设备以机械硬盘为例一个块就是一个扇区大小通常在512 Bytes至4096 Bytes之间。老式机械硬盘用柱面-磁头-扇区号Cylinder-Head-SectorCHS组成的编号进行寻址现代机械硬盘只用一个逻辑块编号Logical Block AddressingLBA进行寻址。
为了便于管理硬盘通常会以多个块这些块甚至可以来自不同的物理设备比如磁盘阵列的情况来组成一个逻辑分区Partition将分区进行高级格式化之后就形成了卷Volume这就与第55讲中提到“Volume是源于操作系统的概念”衔接了起来。
块存储由于贴近底层硬件,没有文件、目录、访问权限等的牵绊,所以性能通常都是最优秀的(吞吐量高,延迟低)。
另外尽管人类作为信息系统的最终用户并不会直接面对块来操作数据多数应用程序也是基于文件而不是块来读写数据的但是操作系统内核中许多地方就是直接通过块设备Block Device接口来访问硬盘一些追求I/O性能的软件比如高性能的数据库也会支持直接读写块设备以提升磁盘I/O。
而且因为块存储的特点是具有排它性一旦块设备被某个客户端挂载后其他客户端就无法再访问上面的数据了。因此Kubernetes中挂载的块存储大多的访问模式都要求必须是RWOReadWriteOnce的。
文件存储
好,下面我们接着来说说文件存储。
文件存储是最贴近人类用户的数据存储形式,数据存储在长度不固定的文件之中,用户可以针对文件进行新增、写入、追加、移动、复制、删除、重命名等各种操作,通常文件存储还会提供有文件查找、目录管理、权限控制等额外的高级功能。
文件存储的访问不像块存储那样有五花八门的协议其POSIX接口Portable Operating System InterfacePOSIX已经成为了事实标准被各种商用的存储系统和操作系统共同支持。具体POSIX的文件操作接口我就不去举例罗列了你可以类比Linux下的各种文件管理命令来自行想象一下。
绝大多数传统的文件存储都是基于块存储之上去实现的“文件”这个概念的出现是因为“块”对人类用户来说实在是过于难以使用、难以管理了。我们可以近似地认为文件是由块所组成的更高级存储单位对于固定不会发生变动的文件直接让每个文件连续占用若干个块在文件头尾加入标志区分即可就比如像磁带、CD-ROM、DVD-ROM就采用了由连续块来构成文件的存储方案。
不过,对于可能发生变动的场景,我们就必须考虑如何跨多个不连续的块来构成为文件。这种需求从数据结构的角度看,只需要在每个块中记录好下一个块的地址,形成链表结构就能满足。但是链表的缺点是只能依次顺序访问,这样访问文件中的任何内容都要从头读取多个块,这显然过于低效了。
事实上真正被广泛运用的解决方案是把形成链表的指针整合起来统一存放这就是文件分配表File Allocation TableFAT。既然已经有了专门组织块结构来构成文件的分配表那在表中再加入其他控制信息就能很方便地扩展出更多的高级功能。
比如除了文件占用的块地址信息外,在表中再加上文件的逻辑位置就形成了目录,加上文件的访问标志就形成了权限,我们还可以再加上文件的名称、创建时间、所有者、修改者等一系列的元数据信息,来构成其他应用形式。
人们把定义文件分配表应该如何实现、储存哪些信息、提供什么功能的标准称为文件系统File SystemFAT32、NTFS、exFAT、ext2/3/4、XFS、BTRFS等都是很常用的文件系统。而前面介绍存储插件接口时我提到的对分区进行高级格式化操作实际上就是在初始化一套空白的文件系统供后续用户与应用程序访问。
文件存储相对于块存储来说是更高层次的存储类型,加入目录、权限等元素后形成的树状结构以及路径访问的方式,方便了人们对它的理解、记忆和访问;文件系统能够提供进程正在打开或正在读写某个文件的信息,这也有利于文件的共享处理。
但在另一方面计算机需要把路径进行分解然后逐级向下查找最后才能查找到需要的文件。而要从文件分配表中确定具体数据存储的位置就要判断文件的访问权限并要记录每次修改文件的用户与时间这些额外操作对于性能产生的负面影响也是无可避免的。因此如果一个系统选择不采用文件存储的话那磁盘I/O性能一般就是最主要的原因。
对象存储
对象存储是相对较新的数据存储形式,它是一种随着云数据中心的兴起而发展起来的存储,是以非结构化数据为目标的存储方案。
这里的“对象”可以理解为一个元数据及与其配对的一个逻辑数据块的组合,元数据提供了对象所包含的上下文信息,比如数据的类型、大小、权限、创建人、创建时间,等等,数据块则存储了对象的具体内容。你也可以简单地理解为数据和元数据这两样东西共同构成了一个对象。
每个对象都有属于自己的全局唯一标识这个标识会直接开放给最终用户使用作为访问该对象的主要凭据通常会是以UUID的形式呈现。对象存储的访问接口就是根据该唯一标识对逻辑数据块进行的读写删除操作的通常接口都会十分简单甚至连修改操作权限都不会提供。
对象存储基本上只会在分布式存储系统之上去实现由于对象存储天生就有明确的“元数据”概念不必依靠文件系统来提供数据的描述信息因此完全可以将一大批对象的元数据集中存放在某一台服务器上再辅以多台OSDObject Storage Device服务器来存储对象的数据块部分。
当外部要访问对象时多台OSD能够同时对外发送数据因此对象存储不仅易于共享、拥有庞大的容量还能提供非常高的吞吐量。不过由于需要先经过元数据查询确定OSD存放对象的确切位置这个过程可能涉及多次网络传输所以在延迟方面就会表现得相对较差。
由于对象的元数据仅描述对象本身的信息与其他对象都没有关联换而言之每个对象都是相互独立的自然也就不存在目录的概念可见对象存储天然就是扁平化的与软件系统中很常见的K/V访问相类似。
不过许多对象存储会提供Bucket的概念用户可以在逻辑上把它看作是“单层的目录”来使用。由于对象存储天生的分布式特性以及极其低廉的扩展成本使它很适合于CDN一类的应用拿来存放图片、音视频等媒体内容以及网页、脚本等静态资源。
选择合适的存储
那么,在理解了三种存储类型的基本原理后,接下来又到了治疗选择困难症的环节。主流的云计算厂商,比如国内的阿里云、腾讯云、华为云,都有自己专门的块存储、文件存储和对象存储服务,关于选择服务提供商的问题,我就不作建议了,你可以根据价格、合作关系、技术和品牌知名度等因素自行去处理。
而关于应该选择三种存储类型中哪一种的问题,这里我就以世界云计算市场占有率第一的亚马逊为例,给你简要对比介绍下它的不同存储类型产品的差异。
亚马逊的块存储服务是Amazon Elastic Block StoreAWS EBS你购买EBS之后在EC2亚马逊的云计算主机里看见的是一块原始的、未格式化的块设备。这点就决定了EBS并不能做为一个独立存储而存在它总是和EC2同时被创建的EC2的操作系统也只能安装在EBS之上。
EBS的大小理论上取决于建立的分区方案也就是块大小乘以块数量。MBR分区的块数量是232块大小通常是512 Bytes总容量为2 TBGPT分区的块数量是264块大小通常是4096 Bytes总容量64 ZB。当然这是理论值64 ZB已经超过了世界上所有信息的总和不会有操作系统支持这种离谱的容量AWS也设置了上限是16 TB在此范围内的实际值就只取决于你的预算额度EBS的性能取决于你选择的存储介质类型SSD、HDD还有优化类型通用性、预置型、吞吐量优化、冷存储优化等这也会直接影响存储的费用成本。
EBS适合作为系统引导卷适合追求磁盘I/O的大型工作负载以及追求低时延的应用比如Oracle等可以直接访问块设备的大型数据库。但EBS只允许被单个节点挂载难以共享这点在单机时代虽然是天经地义的但在云计算和分布式时代就成为了很要命的缺陷。除了少数特殊的工作负载外如前面说的Oracle数据库我并不建议将它作为容器编排系统的主要外置存储来使用。
亚马逊的文件存储服务是Amazon Elastic File SystemAWS EFS你购买EFS之后只要在EFS控制台上创建好文件系统并且管理好网络信息如IP地址、子网就可以直接使用无需依附于任何EC2云主机。
EFS的本质是完全托管在云端的网络文件系统Network File SystemNFS你可以在任何兼容POSIX的操作系统中直接挂载它而不会在/dev中看到新设备的存在。按照前面开头我提到的Kubernetes存储架构中的操作来说就是你只需要考虑Mount无需考虑Attach了。
这样得益于NFS的天然特性EFS的扩缩可以是完全自动、实时的创建新文件时无需预置存储删除已有文件时也不必手动缩容以节省费用。在高性能网络的支持下EFS的性能已经能够达到相当高的水平尽管由于网络访问的限制性能最高的EFS依然比不过最高水平的EBS但仍然能充分满足绝大多数应用运行的需要。
还有最重要的一点优势是由于脱离了块设备的束缚EFS能够轻易地被成百上千个EC2实例共享。考虑到EFS的性能、动态弹性、可共享这些因素我给出的明确建议是它可以作为大部分容器工作负载的首选存储。
亚马逊的对象存储服务是Amazon Simple Storage ServiceAWS S3S3通常是以REST Endpoint的形式对外部提供文件访问服务的这种方式下你应该直接使用程序代码来访问S3而不是靠操作系统或者容器编排系统去挂载它。
如果你真的希望这样做也可以通过存储网关如AWS Storage Gateway将S3的存储能力转换为NFS、SMB、iSCSI等访问协议。经过转换后操作系统或者容器就能够将其作为Volume来挂载了。
S3也许是AWS最出名、使用面最广的存储服务这个结果并不是由于它的性能优异事实上S3的性能比起EBS和EFS来说是相对最差的但它的优势在于它名字中“Simple”所标榜的简单。
我们挂载外部存储的目的十有八九就是为了给程序提供存储服务而使用S3就不必写一行代码就能直接通过HTTP Endpoint进行读写访问而且完全不需要考虑容量、维护和数据丢失的风险这就是简单的价值。
除此之外S3的另一大优势就是它的价格相对于EBS和EFS来说往往要低一至两个数量级因此程序的备份还原、数据归档、灾难恢复、静态页面的托管、多媒体分发等功能就非常适合使用S3来完成。
小结
这节课我们了解学习了块存储、文件存储和对象存储这三种存储类型的基本原理,而关于应该选择这三种存储类型中哪一种的问题,我以亚马逊为例,给你简要对比了下它的不同存储类型产品的差异。
最后我还想补充一点,你可以来看看下面的图例,这是截取自亚马逊销售材料中三种存储的对比。说实话,从目前的存储技术发展来看,其实不会有哪一种存储方案能够包打天下。你要知道,不同业务系统的场景需求不同,对存储的诉求就会不同,那么选择自然也会不同。
图片来自AWS的销售材料
一课一思
计算机进入云计算时代已经有十年了,你是否在生产系统中使用过云存储?如果有,你用过哪些?如果没有,你认为障碍是什么呢?
欢迎在留言区分享你的答案。如果觉得有收获,也欢迎你把今天的内容分享给其他的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,292 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
58 _ Kubernetes的资源模型与调度器设计
你好,我是周志明。
调度是容器编排系统最核心的功能之一“编排”这个词本来也包含了“调度”的含义。调度是指为新创建出来的Pod寻找到一个最恰当的宿主机节点来运行它而这个过程成功与否、结果恰当与否关键就取决于容器编排系统是怎么管理和分配集群节点的资源的。
那么这样一来,我们就可以认为,调度必须要以容器编排系统的资源管控为前提。
因此这节课我们就从Kubernetes的资源模型谈起来学习下Kubernetes是如何为一个新创建出来的Pod寻找到一个最恰当的宿主机节点来运行的。
资源模型
在开始之前,我们先来理清一个概念:资源是什么。
在Kubernetes中资源是非常常用的术语从广义上来讲Kubernetes系统中所有你能接触的方方面面都被抽象成了资源比如表示工作负荷的资源Pod、ReplicaSet、Service、……表示存储的资源Volume、PersistentVolume、Secret、……表示策略的资源SecurityContext、ResourceQuota、LimitRange、……表示身份的资源ServiceAccount、Role、ClusterRole、……等等。
事实上“一切皆为资源”的设计也是Kubernetes能够顺利施行声明式API的必要前提。Kubernetes以资源为载体建立了一套同时囊括了抽象元素如策略、依赖、权限和物理元素如软件、硬件、网络的领域特定语言。它通过不同层级间资源的使用关系来描述上至整个集群甚至是集群联邦下至某一块内存区域或者一小部分的处理器核心的状态这些对资源状态的描述的集合就共同构成了一幅信息系统工作运行的全景图。
在第48讲“以容器构建系统”里我第一次提到了Kubernetes的资源模型把它跟控制器模式一并列为了Kubernetes中最重要的两个设计思想。当然在这节课中我们还会再次讨论资源模型但是这里所说的主要是狭义上的物理资源即特指排除了广义的那些逻辑上的抽象资源只包括能够与真实物理底层硬件对应起来的资源比如处理器资源、内存资源、磁盘存储资源等等。
另外需要说明的是因为咱们今天讨论的话题是调度而作为调度最基本单位的Pod只会与这些和物理硬件直接相关的资源产生供需关系所以后面我提到的资源如果没有额外说明的话就都是特指狭义上的物理资源。
OK现在我们说回到Kubernetes的资源模型上来。
首先从编排系统的角度来看Node是资源的提供者Pod是资源的使用者而调度是将两者进行恰当的撮合。
那么Kubernetes具体是如何撮合它们俩的呢别着急我们先从Node开始来了解。
Node通常能够提供三方面的资源计算资源如处理器、图形处理器、内存、存储资源如磁盘容量、不同类型的介质和网络资源如带宽、网络地址。其中与调度关系最密切的是处理器和内存虽然它们都属于计算资源但两者在调度时又有一些微妙的差别
处理器这样的资源被叫做是可压缩资源Compressible Resources特点是当可压缩资源不足时Pod只会处于“饥饿状态”运行变慢但不会被系统杀死也就是容器会被直接终止或者是被要求限时退出。
而像内存这样的资源则被叫做是不可压缩资源Incompressible Resources特点是当不可压缩资源不足或者超过了容器自己声明的最大限度时Pod就会因为内存溢出Out-Of-MemoryOOM而被系统直接杀掉。
Kubernetes给处理器资源设定的默认计量单位是“逻辑处理器的个数”。至于具体“一个逻辑处理器”应该如何理解就要取决于节点的宿主机是如何解释的它通常会是我们在/proc/cpuinfo中看到的处理器数量。比如它有可能会是多路处理器系统上的一个处理器、多核处理器中的一个核心、云计算主机上的一个虚拟化处理器Virtual CPUvCPU或者是处理器核心里的一条超线程Hyper-Threading
总之Kubernetes只负责保证Pod能够使用到“一个处理器”的计算能力而对不同硬件环境构成的Kubernetes集群乃至同一个集群中不同硬件的宿主机节点来说“一个处理器”所代表的真实算力完全有可能是不一样的。
另外在具体设置方面Kubernetes沿用了云计算中处理器限额设置的一贯做法。如果不明确标注单位比如直接写0.5默认单位就是Core即0.5个处理器当然也可以明确使用Millcores为单位比如写成500 m同样也代表0.5个处理器因为Kubernetes规定了1 Core = 1000 Millcores。
而对于内存来说它早已经有了广泛使用的计量单位即Bytes如果设置中不明确标注单位就会默认以Bytes计数。
为了实际设置的方便Kubernetes还支持以Ei、Pi、Ti、Gi、Mi、Ki以及E、P、T、G、M、K为单位这两者略微有一点儿差别。这里我就以Mi和M为例它们分别是Mebibytes与Megabytes的缩写前者表示1024×1024 Bytes后者表示1000×1000 Bytes。
服务质量与优先级
那么到这里我们要知道设定资源计量单位的目的是为了管理员能够限制某个Pod对资源的过度占用避免影响到其他Pod的正常运行。
Pod是由一个到多个容器组成的资源最终是交由Pod的各个容器去使用所以资源的需求是设定在容器上的具体的配置是Pod的spec.containers[].resource.limits/requests.cpu/memory字段。但是对资源需求的配额则不是针对容器而是针对Pod整体Pod的资源配额不需要手动设置因为Pod的资源配额就是Pod包含的每个容器资源需求的累加值。
实际上为容器设定最大的资源配额的做法从cgroups诞生后就已经屡见不鲜了但不知你有没有注意到Kubernetes给出的配置中有limits和requests两个设置项
这两者的区别其实很简单request是给调度器用的Kubernetes选择哪个节点运行Pod只会根据requests的值来进行决策而limits才是给cgroups用的Kubernetes在向cgroups的传递资源配额时会按照limits的值来进行设置。
Kubernetes会采用这样的设计完全是基于“心理学”的原因因为Google根据Borg和Omega系统长期运行的实践经验总结出了一条经验法则用户提交工作负载时设置的资源配额并不是容器调度一定必须严格遵守的值因为根据实际经验大多数的工作负载运行过程中真正使用到的资源其实都远小于它所请求的资源配额。
额外知识Purchase Quota-
Even though we encourage users to purchase no more quota than they need, many users overbuy because it insulates them against future shortages when their applications user base grows.-
即使我们已经努力建议用户不要过度申请资源配额,但仍难免有大量用户过度消费,他们总希望避免因用户增长而产生资源不足的现象。-
—— Large-Scale Cluster Management at Google with BorgGoogle
当然,“多多益善”的想法完全符合人类的心理,大家提交的资源需求通常都是按照可能面临的最大压力去估计的,甚至考虑到了未来用户增长所导致的新需求。为了避免服务因资源不足而中断,都会往大了去申请,这点我们可以理解。
但是,如果直接按照申请的资源去分配限额,必然会导致服务器出现两方面的影响:一方面,在大多数时间里服务器都会有大量的硬件资源闲置;而另一方面,这些闲置资源又已经分配出去,有了明确的所有者,不能再被其他人利用,难以真正发挥价值。
不过我们也能想到Kubernetes不太可能因为把一个资源配额的设置拆分成了limits和requests两个设置项 就能完全解决这个矛盾。所以为此Kubernetes还进行了许多额外的处理。
比如现在我们知道一旦选择不按照最保守、最安全的方式去分配资源就意味着容器编排系统必须要为有可能出现的极端情况买单。而如果允许节点给Pod分配的资源总和超过了Kubernetes自己最大的可提供资源的话假如某个时刻这些Pod的总消耗真的超标了就会不可避免地导致节点无法继续遵守调度时对Pod许下的资源承诺。
那么此时Kubernetes就迫不得已要杀掉一部分Pod以腾出资源来保证其余Pod能正常运行这个操作就是我后面要给你介绍的驱逐机制Eviction
而要想进行驱逐首先Kubernetes就必须拿出当资源不足时该先牺牲哪些Pod、该保留哪些Pod的明确准则所以由此就形成了Kubernetes的服务质量等级Quality of Service LevelQoS Level和优先级Priority的概念。
我们先来了解下Kubernetes的服务质量等级的概念。
服务质量等级
质量等级是Pod的一个隐含属性也是Kubernetes优先保障重要的服务放弃一些没那么重要的服务的衡量准绳。
那到这里不知道你有没有想到这样一个细节如果不去设置limits和requests会怎样
答案是不设置处理器和内存的资源就意味着没有上限该Pod可以使用节点上所有可用的计算资源。不过你先别高兴得太早这类Pod能以最灵活的方式去使用资源但也正是这类Pod在扮演着最不稳定的风险来源的角色。
在论文《Large-Scale Cluster Management at Google with Borg》中Google明确地提出了针对这类Pod的一种近乎带着惩罚性质的处理建议当节点硬件资源不足时优先杀掉这类Pod。说得文雅一点的话就是给予这类Pod最低的服务质量等级。
Kubernetes目前提供的服务质量等级一共分为三级由高到低分别为Guaranteed、Burstable和BestEffort
如果Pod中所有的容器都设置了limits和requests且两者的值相等那此Pod的服务质量等级就是最高的Guaranteed
如果Pod中有部分容器的requests值小于limits值或者只设置了requests而未设置limits那此Pod的服务质量等级就是第二级Burstable
如果是前面说的那种情况limits和requests两个都没设置那就是最低的BestEffort了。
一般来说我们会建议把数据库应用等有状态的应用或者是一些重要的、要保证不能中断的业务的服务质量等级定为Guaranteed。这样除非是Pod使用超过了它们的limits所描述的不可压缩资源或者节点的内存压力大到Kubernetes已经杀光所有等级更低的Pod了否则它们都不会被系统自动杀死。
而相对地我们也应该把一些临时的、不那么重要的任务设置为BestEffort这样有利于它们调度时能在更大的节点范围中寻找宿主机也利于它们在宿主机中利用更多的资源快速地完成任务然后退出尽量缩减影响范围当然遇到系统资源紧张时它们也更容易被系统杀掉。
小说《动物庄园》:-
All animals are equal, but some animals are more equal than others.-
所有动物生来平等,但有些动物比其他动物更加平等。-
—— Animal Farm: A Fairy StoryGeorge Orwell, 1945
优先级
除了服务质量等级以外Kubernetes还允许系统管理员自行决定Pod的优先级这是通过类型为PriorityClass的资源来实现的。优先级决定了Pod之间并不是平等的关系而且这种不平等还不是谁会多占用一点儿的资源的问题而是会直接影响Pod调度与生存的关键。
优先级会影响调度这很容易理解这就是说当多个Pod同时被调度的话高优先级的Pod会优先被调度。而Pod越晚被调度就越大概率地会因节点资源已被占用而不能成功。
但优先级影响更大的一方面是指Kubernetes的抢占机制Preemption正常在没有设置优先级的情况下如果Pod调度失败就会暂时处于Pending状态被搁置起来直到集群中有新节点加入或者旧Pod退出。
但是如果有一个被设置了明确优先级的Pod调度失败无法创建的话Kubernetes就会在系统中寻找出一批牺牲者Victims把它们杀掉以便给更高优先级的Pod让出资源。
而这个寻找的原则就是在优先级低于待调度Pod的所有已调度的Pod里按照优先级从低到高排序从最低的杀起直至腾出的资源可以满足待调度Pod的成功调度为止或者已经找不到更低优先级的Pod为止。
驱逐机制
说实话前面我动不动就提要杀掉某个Pod听起来实在是不够优雅其实在Kubernetes中更专业的称呼是“驱逐”Eviction即资源回收这也是我在前面提过要给你介绍的概念。
Pod的驱逐机制是通过kubelet来执行的kubelet是部署在每个节点的集群管理程序因为它本身就运行在节点中所以最容易感知到节点的资源实时耗用情况。kubelet一旦发现某种不可压缩资源将要耗尽就会主动终止节点上服务质量等级比较低的Pod以保证其他更重要的Pod的安全。而被驱逐的Pod中所有的容器都会被终止Pod的状态会被更改为Failed。
现在我们已经了解了内存这种最重要的不可压缩资源那么在默认配置下前面我所说的“资源即将耗尽”的“即将”其具体阈值是可用内存小于100 Mi。
而除了可用内存memory.available其他不可压缩资源还包括有宿主机的可用磁盘空间nodefs.available、文件系统可用inode数量nodefs.inodesFree以及可用的容器运行时镜像存储空间imagefs.available。后面三个的阈值都是按照实际容量的百分比来计算的具体的默认值如下
memory.available < 100Mi
nodefs.available < 10%
nodefs.inodesFree < 5%
imagefs.available < 15%
管理员可以在kubelet启动时通过命令行参数来修改这些默认值比如说如果是在可用内存只剩余100 Mi时才启动驱逐那对大多数生产系统来说都过于危险了所以我建议在生产环境中可以考虑当内存剩余10%时就开始驱逐具体的调整命令如下所示
$ kubelet --eviction-hard=memory.available<10%
如果你是一名JavaC#Golang等习惯了自动内存管理机制的程序员我还要提醒你一下Kubernetes的驱逐不能完全等同于编程语言中的垃圾收集器
这里主要体现在两个方面
一方面我们要知道垃圾收集是安全的内存回收行为而驱逐Pod是一种毁坏性的清理行为它有可能会导致服务产生中断因而必须更加谨慎比如说要同时兼顾到硬件资源可能只是短时间内间歇性地超过了阈值的场景以及资源正在被快速消耗很快就会危及高服务质量的Pod甚至是整个节点稳定的场景
如此一来驱逐机制中就有了软驱逐Soft Eviction硬驱逐Hard Eviction以及优雅退出期Grace Period的概念
软驱逐通常会配置一个比较低的警戒线比如可用内存仅剩20%当触及此线时系统就会进入一段观察期如果只是暂时的资源抖动在观察期内能够恢复到正常水平的话那就不会真正启动驱逐操作否则资源持续超过警戒线一段时间就会触发Pod的优雅退出Grace Shutdown系统会通知Pod进行必要的清理工作比如将缓存的数据落盘然后自行结束在优雅退出期结束后系统会强制杀掉还没有自行了断的Pod
硬驱逐通常会配置一个比较高的终止线比如可用内存仅剩10%一旦触及此线系统就会立即强制杀掉Pod不理会优雅退出
软驱逐是为了减少资源抖动对服务的影响硬驱逐是为了保障核心系统的稳定它们并不矛盾一般会同时使用如以下例子中所示
$ kubelet --eviction-hard=memory.available<10% \
--eviction-soft=memory.available<20% \
--eviction-soft-grace-period=memory.available=1m30s \
--eviction-max-pod-grace-period=600
另一方面Kubernetes的驱逐跟垃圾收集器的不同之处还在于垃圾收集可以应收尽收而驱逐显然不行系统不能无缘无故地把整个节点中所有可驱逐的Pod都清空掉但是系统通常也不能只清理到刚刚低于警戒线就停止必须要考虑到驱逐之后的新Pod调度与旧Pod运行的新增消耗
比如kubelet驱逐了若干个Pod让资源使用率勉强低于阈值那么很可能在极短的时间内资源使用率又会因为某个Pod稍微占用了些许资源而重新超过阈值再产生新一次驱逐如此往复
为此Kubernetes提供了--eviction-minimum-reclaim参数用于设置一旦驱逐发生之后至少要清理出来多少资源才会终止
不过问题到这里还是没有全部解决要知道Kubernetes中很少会单独创建Pod通常都是由ReplicaSetDeployment等更高层资源来管理的而这就意味着当Pod被驱逐之后它不会从此彻底消失Kubernetes会自动生成一个新的Pod来取代并经过调度选择一个节点继续运行
这样也就是说如果没有进行额外的处理那很大概率这个新生成的Pod就会被调度到当前这个节点上重新创建因为上一次调度就选择了这个节点而且这个节点刚刚驱逐完一批Pod得到了空闲资源那它显然应该符合此Pod的调度需求
所以为了避免被驱逐的Pod出现阴魂不散的问题Kubernetes还提供了另一个参数--eviction-pressure-transition-period来约束调度器在驱逐发生之后多长时间内不能往该节点调度Pod
另外关于驱逐机制你还应该意识到既然这些措施被设计为以参数的形式开启那就说明了它们一定不是放之四海皆准的通用准则
举个例子假设当前Pod是由DaemonSet控制的一旦该Pod被驱逐你又强行不允许节点在一段时间内接受调度那显然就有违DaemonSet的语义了
不过到目前Kubernetes其实并没有办法区分Pod是由DaemonSet还是别的高层次资源创建的所以刚刚的这种假设情况确实有可能发生而比较合理的解决方案是让DaemonSet创建Guaranteed而不是BestEffort的Pod
总而言之在Kubernetes还没有成熟到变为傻瓜式容器编排系统之前因地制宜地合理配置和运维是都非常必要的
最后我还想说明的是关于服务质量优先级驱逐机制这些概念都是在Pod层面上限制资源是仅针对单个Pod的低层次约束而在现实中我们还经常会遇到面向更高层次去控制资源的需求比如想限制由多个Pod构成的微服务系统耗用的总资源或者是由多名成员组成的团队耗用的总资源
我举个具体例子现在你想要在拥有32 GiB内存和16个处理器的集群里允许A团队使用20 GiB内存和10个处理器的资源再允许B团队使用10 GiB内存和4个处理器的资源再预留2 GiB内存和2个处理器供将来分配那么要满足这种资源限制的需求Kubernetes的解决方案是应该先为它们建立一个专用的名称空间然后再在名称空间里建立ResourceQuota对象来描述如何进行整体的资源约束
但是这样ResourceQuota与调度就没有直接关系了它针对的对象也不是Pod所以这里我所说的资源可以是广义上的资源系统不仅能够设置处理器内存等物理资源的限额还可以设置诸如Pod最大数量ReplicaSet最大数量Service最大数量全部PersistentVolumeClaim的总存储容量等各种抽象资源的限额
甚至当Kubernetes预置的资源模型不能满足约束需要的时候还能够根据实际情况去拓展比如要控制GPU的使用数量完全可以通过Kubernetes的设备插件Device Plugin机制拓展出诸如nvidia.com/gpu: 4这样的配置来
默认调度器
了解了Kubernetes的资源模型和服务质量优先级驱逐机制这些概念以后我们再回过头来探讨下前面开头我提出的问题Kubernetes是如何撮合Pod与Node的这其实也是最困难的一个问题
现在我们知道调度是为新创建出来的Pod寻找到一个最恰当的宿主机节点去运行它而在这句话里就包含有运行恰当两个调度中的关键过程它们具体是指
运行从集群的所有节点中找出一批剩余资源可以满足该Pod运行的节点为此Kubernetes调度器设计了一组名为Predicate的筛选算法
恰当从符合运行要求的节点中找出一个最适合的节点完成调度为此Kubernetes调度器设计了一组名为Priority的评价算法
这两个算法的具体内容稍后我会详细给你解释这里我要先说明白一点在几个十几个节点的集群里进行调度调度器怎么实现都不会太困难但是对于数千个乃至更多节点的大规模集群要实现高效的调度就绝不简单
请你想象一下现在有一个由数千节点组成的集群每次Pod的创建都必须依据各节点的实时资源状态来确定调度的目标节点然而我们知道各节点的资源是随着程序运行无时无刻都在变动的资源状况只有它本身才清楚
这样如果每次调度都要发生数千次的远程访问来获取这些信息的话那压力与耗时都很难降下来所以结果不仅会让调度器成为集群管理的性能瓶颈还会出现因耗时过长某些节点上资源状况已发生变化调度器的资源信息过时而导致调度结果不准确等问题
额外知识Scheduler-
Clusters and their workloads keep growing, and since the schedulers workload is roughly proportional to the cluster size, the scheduler is at risk of becoming a scalability bottleneck.-
由于调度器的工作负载与集群规模大致成正比随着集群和它们的工作负载不断增长调度器很有可能会成为扩展性瓶颈所在-
Omega: Flexible, Scalable Schedulers for Large Compute ClustersGoogle
因此针对前面所说的问题Google在论文Omega: Flexible, Scalable Schedulers for Large Compute Clusters里总结了自身的经验并参考了当时Apache Mesos和Hadoop on DemandHOD的实现提出了一种共享状态Shared State的双循环调度机制
这种调度机制后来不仅应用在Google的Omega系统Borg的下一代集群管理系统也同样被Kubernetes继承了下来它整体的工作流程如下图所示
状态共享的双循环
状态共享的双循环第一个控制循环可被称为Informer Loop它是一系列Informer的集合这些Informer会持续监视etcd中与调度相关资源主要是Pod和Node的变化情况一旦PodNode等资源出现变动就会触发对应Informer的Handler
Informer Loop的职责是根据etcd中的资源变化去更新调度队列Priority Queue和调度缓存Scheduler Cache中的信息
比如当有新Pod生成就将其入队Enqueue到调度队列中如有必要还会根据优先级触发上节课我提到的插队和抢占操作再比如当有新的节点加入集群或者已有的节点资源信息发生变动Informer也会把这些信息更新同步到调度缓存之中
另一个控制循环可被称为Scheduler Loop它的核心逻辑是不停地把调度队列中的Pod出队Pop然后使用Predicate算法进行节点选择
Predicate本质上是一组节点过滤器Filter它会根据预设的过滤策略来筛选节点Kubernetes中默认有三种过滤策略分别是
通用过滤策略最基础的调度过滤策略用来检查节点是否能满足Pod声明中需要的资源比如处理器内存资源是否满足主机端口与声明的NodePort是否存在冲突Pod的选择器或者nodeAffinity指定的节点是否与目标相匹配等等
卷过滤策略与存储相关的过滤策略用来检查节点挂载的Volume是否存在冲突比如将一个块设备挂载到两个节点上或者Volume的可用区域是否与目标节点冲突等等Kubernetes存储设计中提到的Local PersistentVolume的调度检查就是在这里处理的
节点过滤策略与宿主机相关的过滤策略最典型的是Kubernetes的污点与容忍度机制Taints and Tolerations比如默认情况下Kubernetes会设置Master节点不允许被调度这就是通过在Master中施加污点来避免的前面我提到的控制节点处于驱逐状态或者在驱逐后一段时间不允许调度也是在这个策略里实现的
此外Predicate算法所使用的一切数据都来自于调度缓存它绝对不会去远程访问节点本身这里你要知道只有Informer Loop与etcd的监视操作才会涉及到远程调用而Scheduler Loop中除了最后的异步绑定要发起一次远程的etcd写入外其余全部都是进程内访问这一点正是调度器执行效率的重要保证
所谓的调度缓存就是两个控制循环的共享状态Shared State这样的设计避免了调度器每次调度时主动去轮询所有集群节点保证了调度器的执行效率
但是它也存在一定的局限也就是调度缓存并不能完全避免因节点信息同步不及时而导致调度过程中实际资源发生变化的情况比如节点的某个端口在获取调度信息后发生实际调度前被意外占用了
为此当调度结果出来以后在kubelet真正创建Pod以前还必须执行一次Admit操作在该节点上重新做一遍Predicate来进行二次确认经过Predicate算法筛选出来符合要求的节点集会交给Priorities算法来打分0~10分排序以便挑选出最恰当的一个
这里的恰当其实是带有主观色彩的词语Kubernetes也提供了不同的打分规则来满足不同的主观需求比如最常用的LeastRequestedPriority规则它的计算公式是
score = (cpu((capacity-sum(requested))×10/capacity) + memory((capacity-sum(requested))×10/capacity))/2
从公式上我们能很容易地看出这就是在选择处理器和内存空闲资源最多的节点因为这些资源剩余越多得分就越高经常与它一起工作的是BalancedResourceAllocation规则它的公式是
score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)×10
在这个公式中三种Fraction的含义是Pod请求的资源除以节点上的可用资源variance函数的作用是计算各种资源之间的差距差距越大函数值越大由此可知BalancedResourceAllocation规则的意图是希望调度完成后所有节点里各种资源分配尽量均衡避免节点上出现诸如处理器资源被大量分配而内存大量剩余的尴尬状况
Kubernetes内置的其他的评分规则还有ImageLocalityPriorityNodeAffinityPriorityTaintTolerationPriority等等有兴趣的话你可以去阅读Kubernetes的源码这里我就不再逐一解释了
这样经过Predicate的筛选Priorities的评分之后调度器已经选出了调度的最终目标节点最后一步就是通知目标节点的kubelet可以去创建Pod了我们要知道调度器并不会直接与kubelet通讯来创建Pod它只需要把待调度的Pod的nodeName字段更新为目标节点的名字即可kubelet本身会监视该值的变化来接手后续工作
不过从调度器在etcd中更新nodeName到kubelet从etcd中检测到变化再执行Admit操作二次确认调度可行性最后到Pod开始实际创建这个过程可能会持续一段不短的时间如果一直等待这些工作都完成了才宣告调度最终完成那势必也会显著影响调度器的效率
所以实际上Kubernetes调度器采用了乐观绑定Optimistic Binding的策略来解决这个问题它会同步地更新调度缓存中Pod的nodeName字段并异步地更新etcd中Pod的nodeName字段这个操作被称为绑定Binding如果最终调度成功了那etcd与调度缓存中的信息最终必定会保持一致否则如果调度失败了那就会由Informer来根据Pod的变动将调度成功却没有创建成功的Pod清空nodeName字段重新同步回调度缓存中以便促使另外一次调度的开始
最后你可能会注意到这个部分的小标题我用的是默认调度器这其实是在强调以上行为仅是Kubernetes默认的行为对调度过程的大部分行为你都可以通过Scheduler Framework暴露的接口来进行扩展和自定义如下图所示
Scheduler Framework的可扩展性
可以看到图中绿色的部分就是Scheduler Framework暴露的扩展点由于Scheduler Framework属于Kubernetes内部的扩展机制通过Golang的Plugin机制来实现的需静态编译它的通用性跟我在前面课程中提到的其他扩展机制比如CRICNICSI那些无法相提并论属于比较高级的Kubernetes管理技能了这里我就简单地提一下你稍作了解就行
小结
调度可以分解为几个相对独立的子问题来研究比如说如何衡量工作任务的算力需求如何区分工作任务的优先级保障较重要的任务有较高的服务质量如何在资源紧张时自动驱逐相对不重要的任务等等解决这一系列子问题的组件就称为容器编排系统的调度器
这节课我带你学习了Kubernetes是如何为一个新创建出来的Pod寻找到一个最恰当的宿主机节点来运行的由于Kubernetes基于超卖所设计的资源调度机制在更合理充分利用物理服务器资源的同时也让资源调度成为了一项具有风险和挑战性的工作所以你只有正确理解了这节课介绍的服务质量优先级驱逐机制等概念在生产实践中才能在资源利用率最大化与服务稳定性之间取得良好平衡
一课一思
调度是容器编排系统的核心功能之一但调度却不仅仅存在于容器编排之中除了Kubernetes等编排系统外你还遇到过哪些需要进行资源调度的场景呢
欢迎在留言区分享你的答案如果你觉得有收获也欢迎把今天的内容分享给更多的朋友感谢你的阅读我们下一讲再见

View File

@@ -0,0 +1,120 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
59 _ 透明通讯的涅槃(上):通讯的成本
你好,我是周志明。接下来这三节课,我们来学习目前最新的服务通讯方案:服务网格。
Kubernetes为它管理的工作负载提供了工业级的韧性与弹性也为每个处于运行状态的Pod维护其相互连通的虚拟化网络。不过程序之间的通信不同于简单地在网络上拷贝数据一个可连通的网络环境仅仅是程序间能够可靠通信的必要但非充分的条件。
作为一名经历过SOA、微服务、云原生洗礼的的分布式程序员你必定已经深谙路由、容错、限流、加密、认证、授权、跟踪、度量等问题在分布式系统中的必要性。
在“远程服务调用”这个小章节里,我曾以“通信的成本”为主题,给你讲解了三十多年的计算机科学家们,对“远程服务调用是否可能实现为透明通信”的一场声势浩大的争论。而今天,服务网格的诞生在某种意义上,就可以说就是当年透明通信的重生,服务网格试图以容器、虚拟化网络、边车代理等技术所构筑的新一代通信基础设施为武器,重新对已经盖棺定论三十多年的程序间远程通信中,非透明的原则发起冲击。
今天,这场关于通信的变革仍然在酝酿发展当中。最后到底会是成功的逆袭,还是会成为另一场失败,我不敢妄言定论,但是作为程序通信发展历史的一名见证者,我会丝毫不吝啬对服务网格投去最高的期许与最深的祝愿。
通信的成本
程序间通信作为分布式架构的核心内容,我在第一个模块“演进中的架构”中,就已经从宏观角度讲述过它的演进过程。而在这节课里,我会从更微观、更聚焦的角度,分析不同时期应用程序该如何看待与实现通信方面的非功能性需求,以及它们是如何做到可靠通信的。
我会通过以下五个阶段的变化,帮助你理解分布式服务的通信是如何逐步演化成我们要探讨的主角“服务网格”的。
第一阶段:将通信的非功能性需求视作业务需求的一部分,由程序员来保障通信的可靠性。
这一阶段是软件企业刚刚开始尝试分布式时,选择的早期技术策略。这类系统原本所具有的通信能力不是作为系统功能的一部分被设计出来的,而是遇到问题后修补累积所形成的。
在刚开始时系统往往只具备最基本的网络API比如集成OKHTTP、gRPC这些库来访问远程服务如果远程访问接收到异常就编写对应的重试或降级逻辑去应对处理。而在系统进入生产环境以后遇到并解决的一个个通信问题就逐渐在业务系统中留下了越来越多关于通信的代码逻辑。
这些通信的逻辑由业务系统的开发人员直接编写,与业务逻辑直接共处在一个进程空间之中,如下图所示(注:这里以及后面的一系列图片中,我会以“断路器”和“服务发现”这两个常见的功能来泛指所有的分布式通信所需的能力,但你要知道实际上并不局限于这两个功能)。
控制逻辑和业务逻辑耦合
这一阶段的主要矛盾是绝大多数擅长业务逻辑的开发人员,其实都并不擅长处理通信方面的问题。要写出正确、高效、健壮的分布式通信代码,是一项极具专业性的工作。所以大多数的普通软件企业都很难在这个阶段支撑起一个靠谱的分布式系统来。
另一方面,把专业的通信功能强加于普通开发人员,这无疑为他们带来了更多工作量。尤其是这些“额外的工作”与原有的业务逻辑耦合在一起,让系统越来越复杂,也越来越容易出错。
第二阶段:将代码中的通信功能抽离重构成公共组件库,通信的可靠性由专业的平台程序员来保障。
开发人员解耦一贯依赖的有效办法是抽取分离代码与封装重构组件。实际上微服务的普及也离不开一系列封装了分布式通信能力的公共组件库其代表性产品有Twitter的Finagle、Spring Cloud中的许多组件等。
这些公共的通信组件由熟悉分布式的专业开发人员编写和维护不仅效率更高、质量更好还都提供了经过良好设计的API接口让业务代码既可以使用它们的能力又无需把处理通信的逻辑散布于业务代码当中。
抽取公共的分布式通信组件
分布式通信组件让普通程序员开发出靠谱的微服务系统成为可能,这是无可争议的成绩。但普通程序员使用它们的成本依然很高,不仅要学习分布式的知识,还要学习这些公共组件的功能的使用规范,最麻烦的是,对于同一种问题往往还需学习多种不同的组件才能解决。
造成这些问题的主要原因是因为通信组件是一段由特定编程语言开发出来的程序是与语言绑定的一个由Python编写的组件再优秀对Java系统来说也没有太多的实用价值。目前基于公共组件库开发微服务仍然是应用最为广泛的解决方案但肯定不是一种完美的解决方案这是微服务基础设施完全成熟之前必然会出现的应用形态同时也一定是微服务进化过程中必然会被替代的过渡形态。
第三阶段:将负责通信的公共组件库分离到进程之外,程序间通过网络代理来交互,通信的可靠性由专门的网络代理提供商来保障。
为了能够让分布式通信组件与具体的编程语言脱钩也为了避免程序员还要去专门学习这些组件的编程模型与API接口这一阶段进化出了能专门负责可靠通信的网络代理。这些网络代理不再与业务逻辑部署于同一个进程空间但仍然与业务系统处于同一个容器或者虚拟机当中它们可以通过回环设备甚至是UDSUnix Domain Socket进行交互可以说具备相当高的网络性能。
也就是说只要让网络代理接管掉程序七层或四层流量就能够在代理上完成断路、容错等几乎所有的分布式通信功能前面提到过的Netflix Prana就属于这类产品的典型代表。
通过网络代理获得可靠的通信能力
在通过网络代理来提升通信质量的思路提出以后,其本身的使用范围其实并不算特别广泛,但它的方向是正确的。这种思路后来演化出了两种改进形态:
第一种形态,将网络代理从进程身边拉远,让它与进程分别处于不同的机器上,这样就可以同时给多个进程提供可靠通信的代理服务。这种形态逐渐演变成了今天我们常见的微服务网关。
第二种形态,如果将网络代理往进程方向推近,不仅能让它与进程处于同一个共享网络名称空间的容器组之中,还可以让它透明并强制地接管通讯,这便形成了下一阶段所说的边车代理。
第四阶段:将网络代理以边车的形式注入到应用容器,自动劫持应用的网络流量,让通信的可靠性由专门的通信基础设施来保障。
与前一阶段的独立代理相比,以边车模式运作的网络代理拥有两个无可比拟的优势:
它对流量的劫持是强制性的通常是靠直接写容器的iptables转发表来实现。
此前,独立的网络代理只有程序首先去访问它,它才能被动地为程序提供可靠的通信服务,只要程序依然有选择不访问它的可能性,代理就永远只能充当服务者而不能成为管理者。上阶段的图中,保留的两个容器网络设备直接连接的箭头,就代表了这种可能性,而这一阶段的图例中,服务与网络名称空间的虚线箭头代表了被劫持后,应用程序以为存在,但实际并不存在的流量。
边车代理对应用是透明的无需对已部署的应用程序代码进行任何改动不需要引入任何的库这点并不是绝对的有部分边车代理也会要求有轻量级的SDK也不需要程序专门去访问某个特定的网络位置。
这意味着它对所有现存程序都具备开箱即用的适应性无需修改旧程序就能直接享受到边车代理的服务这样使得它的适用面就变得十分广泛。目前边车代理的代表性产品有Linkerd、Envoy、MOSN等。
边车代理模式
如果说边车代理还有什么不足之处的话那大概就是来自于运维人员的不满了。边车代理能够透明且具有强制力地解决可靠通信的问题但它本身也需要有足够的信息才能完成这项工作比如获取可用服务的列表、得到每个服务名称对应的IP地址等等。
而这些信息不会从天上掉下来自动到边车里去,是需要由管理员主动去告知代理,或者代理主动从约定的好的位置获取的。可见,管理代理本身也会产生额外的通信需求。如果没有额外的支持,这些管理方面的通信都得由运维人员去埋单,由此而生的不满便可想而知。为了管理与协调边车代理,程序间通信进化到了最后一个阶段:服务网格。
第五阶段:将边车代理统一管控起来实现安全、可控、可观测的通信,将数据平面与控制平面分离开来,实现通用、透明的通信,这项工作就由专门的服务网格框架来保障。
从总体架构看,服务网格包括两大块内容,分别是由一系列与微服务共同部署的边车代理,以及用于控制这些代理的管理器所构成。代理与代理之间需要通信,用以转发程序间通信的数据包;代理与管理器之间也需要通信,用以传递路由管理、服务发现、数据遥测等控制信息。
服务网格使用数据平面Data Plane通信和控制平面Control Plane通信来形容这两类流量下图中的实线就表示数据平面通信虚线表示控制平面通信。
服务网格的控制平面通信与数据平面通信
实际上,数据平面与控制平面并不是什么新鲜概念,它最初就是用在计算机网络之中的术语,通常是指网络层次的划分。在软件定义网络中,也把解耦数据平面与控制平面作为其最主要的特征之一。服务网格把计算机网络的经典概念引入到了程序通信之中,既可以说是对程序通信的一种变革创新,也可以说是对网络通信的一种发展传承。
小结
分离数据平面与控制平面的实质是将“程序”与“网络”进行解耦,把网络可能出现的问题(比如中断后重试、降级),与可能需要的功能(比如实现追踪度量)的处理过程从程序中拿出来,放到由控制平面指导的数据平面通信中去处理,这样来制造出一种“这些问题在程序间通信中根本不存在”的假象,仿佛网络和远程服务都是完美可靠的。
而这种完美的假象,就让应用之间可以非常简单地交互,而不必过多地考虑异常情况;而且也能够在不同的程序框架、不同的云服务提供商环境之间平稳地迁移。与此同时,还能让管理者能够不依赖程序支持就得到遥测所需的全部信息,能够根据角色、权限进行统一的访问控制,这些都是服务网格的价值所在。
一课一思
远程通讯在性能上与本地访问有好几个数量级的差距,目前完全看不到有“透明”的可能性。不过,在功能上,在可预见的将来,是否有可能在实现透明的远程服务,业界仍然没有统一的共识,这个问题你的看法是什么?欢迎在留言区分享你的见解。
如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,276 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
60 _ 透明通讯的涅槃(下):控制平面与数据平面
你好,我是周志明。这节课,我会延续服务网格将“程序”与“网络”解耦的思路,通过介绍几个数据平面通信与控制平面通信中的核心问题的解决方案,帮助你更好地理解这两个概念。
在开始之前我想先说明一点就是我们知道在工业界数据平面领域已经有了Linkerd、Nginx、Envoy等产品在控制平面领域也有Istio、Open Service Mesh、Consul等产品。不过今天我主要讲解的是目前市场占有率最高的Istio与Envoy因为我的目的是要让你理解两种平面通信的技术原理而非介绍Istio和Envoy的功能与用法这节课中涉及到的原理在各种服务网格产品中一般都是通用的并不局限于哪一种具体实现。
好,接下来我们就从数据平面通信开始,来了解一下它的工作内容。
数据平面
首先数据平面由一系列边车代理所构成它的核心职责是转发应用的入站Inbound和出站Outbound数据包因此数据平面也有个别名叫转发平面Forwarding Plane
同时,为了在不可靠的物理网络中保证程序间通信最大的可靠性,数据平面必须根据控制平面下发策略的指导,在应用无感知的情况下自动完成服务路由、健康检查、负载均衡、认证鉴权、产生监控数据等一系列工作。
那么,为了顺利完成以上所说的工作目标,数据平面至少需要妥善解决三个关键问题:
代理注入:边车代理是如何注入到应用程序中的?
流量劫持:边车代理是如何劫持应用程序的通信流量的?
可靠通信:边车代理是如何保证应用程序的通信可靠性的?
好,下面我们就具体来看看吧。
代理注入
从职责上说,注入边车代理是控制平面的工作,但从叙述逻辑上,将其放在数据平面中介绍更合适。因为把边车代理注入到应用的过程并不一定全都是透明的,所以现在的服务网格产品产生了以下三种将边车代理接入到应用程序中的方式。
基座模式Chassis这种方式接入的边车代理对程序就是不透明的它至少会包括一个轻量级的SDK让通信由SDK中的接口去处理。基座模式的好处是在程序代码的帮助下有可能达到更好的性能功能也相对更容易实现。但坏处是对代码有侵入性对编程语言有依赖性。这种模式的典型产品是由华为开源后捐献给Apache基金会的ServiceComb Mesher。基座模式的接入方式目前并不属于主流方式我也就不展开介绍了。
注入模式Injector根据注入方式不同又可以分为
手动注入模式这种接入方式对使用者来说不透明但对程序来说是透明的。由于边车代理的定义就是一个与应用共享网络名称空间的辅助容器这天然就契合了Pod的设定。因此在Kubernetes中要进行手动注入是十分简单的——就只是为Pod增加一个额外容器而已即使没有工具帮助自己修改Pod的Manifest也能轻易办到。如果你以前未曾尝试过不妨找一个Pod的配置文件用istioctl kube-inject -f YOUR_POD.YAML命令来查看一下手动注入会对原有的Pod产生什么变化。
自动注入模式这种接入方式对使用者和程序都是透明的也是Istio推荐的代理注入方式。在Kubernetes中服务网格一般是依靠“动态准入控制”Dynamic Admission Control中的Mutating Webhook控制器来实现自动注入的。
额外知识-
istio-proxy是Istio对Envoy代理的包装容器其中包含用Golang编写的pilot-agent和用C++编写的envoy两个进程。pilot-agent进程负责Envoy的生命周期管理比如启动、重启、优雅退出等并维护Envoy所需的配置信息比如初始化配置、随时根据控制平面的指令热更新Envoy的配置等。
这里我以Istio自动注入边车代理istio-proxy容器的过程为例给你介绍一下自动注入的具体的流程。只要你对Istio有基本的了解你应该就能都知道对任何设置了istio-injection=enabled标签的名称空间Istio都会自动为其中新创建的Pod注入一个名为istio-proxy的容器。之所以能做到自动这一点是因为Istio预先在Kubernetes中注册了一个类型为MutatingWebhookConfiguration的资源它的主要内容如下所示
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: istio-sidecar-injector
.....
webhooks:
- clientConfig:
service:
name: istio-sidecar-injector
namespace: istio-system
path: /inject
name: sidecar-injector.istio.io
namespaceSelector:
matchLabels:
istio-injection: enabled
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
以上配置其实就告诉了Kubernetes对于符合标签istio-injection: enabled的名称空间在Pod资源进行CREATE操作时应该先自动触发一次Webhook调用调用的位置是istio-system名称空间中的服务istio-sidecar-injector调用具体的URL路径是/inject。
在这次调用中Kubernetes会把拟新建Pod的元数据定义作为参数发送给此HTTP Endpoint然后从服务返回结果中得到注入了边车代理的新Pod定义以此自动完成注入。
流量劫持
边车代理做流量劫持最典型的方式是基于iptables进行的数据转发我曾在“Linux网络虚拟化”这个小章节中介绍过Netfilter与iptables的工作原理。这里我仍然以Istio为例它在注入边车代理后除了生成封装Envoy的istio-proxy容器外还会生成一个initContainer这个initContainer的作用就是自动修改容器的iptables具体内容如下所示
initContainers:
image: docker.io/istio/proxyv2:1.5.1
name: istio-init
- command:
- istio-iptables -p "15001" -z "15006"-u "1337" -m REDIRECT -i '*' -x "" -b '*' -d 15090,15020
以上命令行中的istio-iptables是Istio提供的用于配置iptables的Shell脚本这行命令的意思是让边车代理拦截所有的进出Pod的流量包括拦截除15090、15020端口这两个分别是Mixer和Ingress Gateway的端口关于Istio占用的固定端口你可以参考官方文档所列的信息外的所有入站流量全部转发至15006端口Envoy入站端口经Envoy处理后再从15001端口Envoy出站端口发送出去。
这个命令会在iptables中的PREROUTING和OUTPUT链中挂载相应的转发规则使用iptables -t nat -L -v命令你可以查看到如下所示配置信息
Chain PREROUTING
pkts bytes target prot opt in out source destination
2701 162K ISTIO_INBOUND tcp -- any any anywhere anywhere
Chain OUTPUT
pkts bytes target prot opt in out source destination
15 900 ISTIO_OUTPUT tcp -- any any anywhere anywhere
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:ssh
2 120 RETURN tcp -- any any anywhere anywhere tcp dpt:15090
2699 162K RETURN tcp -- any any anywhere anywhere tcp dpt:15020
0 0 ISTIO_IN_REDIRECT tcp -- any any anywhere anywhere
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15006
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- any lo 127.0.0.6 anywhere
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner UID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner UID match 1337
15 900 RETURN all -- any any anywhere anywhere owner UID match 1337
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner GID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner GID match 1337
0 0 RETURN all -- any any anywhere anywhere owner GID match 1337
0 0 RETURN all -- any any anywhere localhost
0 0 ISTIO_REDIRECT all -- any any anywhere anywhere
Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 1
实际上用iptables进行流量劫持是最经典、最通用的手段。不过iptables重定向流量必须通过回环设备Loopback交换数据流量不得不多穿越一次协议栈如下图所示。
其实这种方案在网络I/O不构成主要瓶颈的系统中并没有什么不妥但在网络敏感的大并发场景下会因转发而损失一定的性能。因而目前如何实现更优化的数据平面流量劫持仍然是服务网格发展的前沿研究课题之一。
其中一种可行的优化方案是使用eBPFExtended Berkeley Packet Filter技术在Socket层面直接完成数据转发而不需要再往下经过更底层的TCP/IP协议栈的处理从而减少它数据在通信链路的路径长度。
另一种可以考虑的方案是让服务网格与CNI插件配合来实现流量劫持比如Istio就有提供自己实现的CNI插件。只要安装了这个CNI插件整个虚拟化网络都由Istio自己来控制那自然就无需再依赖iptables也不必存在initContainers配置和istio-init容器了。
这种方案有很高的上限与自由度不过要实现一个功能全面、管理灵活、性能优秀、表现稳定的CNI网络插件决非易事连Kubernetes自己都迫不及待想从网络插件中脱坑其麻烦程度可想而知因此目前这种方案使用并不广泛。
流量劫持技术的发展与服务网格的落地效果密切相关有一些服务网格通过基座模式中的SDK也能达到很好的转发性能但考虑到应用程序通用性和环境迁移等问题无侵入式的低时延、低管理成本的流量劫持方案仍然是研究的主流方向。
可靠通信
注入边车代理、劫持应用流量,最终的目的都是为了代理能够接管应用程序的通信,然而,在代理接管了应用的通信之后,它会做什么呢?这个问题的答案是:不确定。
代理的行为需要根据控制平面提供的策略来决定传统的代理程序比如HAProxy、Nginx是使用静态配置文件来描述转发策略的而这种静态配置很难跟得上应用需求的变化与服务扩缩时网络拓扑结构的变动。
因此针对这个问题Envoy在这方面进行了创新它将代理的转发的行为规则抽象成Listener、Router、Cluster三种资源。以此为基础它又定义了应该如何发现和访问这些资源的一系列API现在这些资源和API被统称为“xDS协议族”。自此以后数据平面就有了如何描述各种配置和策略的事实标准控制平面也有了与控制平面交互的标准接口目前xDS v3.0协议族已经包含有以下具体协议:
这里我就不逐一介绍这些协议了但我要给你说明清楚它们一致的运作原理。其中的关键是解释清楚这些协议的共同基础即Listener、Router、Cluster三种资源的具体含义。
Listener
Listener可以简单理解为Envoy的一个监听端口用于接收来自下游应用程序Downstream的数据。Envoy能够同时支持多个Listener且不同的Listener之间的策略配置是相互隔离的。
自动发现Listener的服务被称为LDSListener Discovery Service它是所有其他xDS协议的基础如果没有LDS也没有在Envoy启动时静态配置Listener的话其他所有xDS服务也就失去了意义因为没有监听端口的Envoy不能为任何应用提供服务。
Cluster
Cluster是Envoy能够连接到的一组逻辑上提供相同服务的上游Upstream主机。Cluster包含该服务的连接池、超时时间、Endpoints地址、端口、类型等信息。具体到Kubernetes环境下可以认为Cluster与Service是对等的概念但是Cluster实际上还承担了服务发现的职责。
自动发现Cluster的服务被称为CDSCluster Discovery Service通常情况下控制平面会将它从外部环境中获取的所有可访问服务全量推送给Envoy。与CDS紧密相关的另一种服务是EDSEndpoint Discovery Service。当Cluster的类型被标识为需要EDS时则说明该Cluster的所有Endpoints地址应该由xDS服务下发而不是依靠DNS服务去解析。
Router
Listener负责接收来自下游的数据Cluster负责将数据转发送给上游的服务而Router则决定Listener在接收到下游的数据之后具体应该将数据交给哪一个Cluster处理。由此定义可知Router实际上是承担了服务网关的职责。
自动发现Router的服务被称为RDSRouter Discovery ServiceRouter中最核心的信息是目标Cluster及其匹配规则即实现网关的路由职能。此外根据Envoy中的插件配置情况也可能包含重试、分流、限流等动作实现网关的过滤器职能。
Envoy的另外一个设计重点是它的Filter机制Filter通俗地讲就是Envoy的插件通过Filter机制Envoy就可以提供强大的可扩展能力。插件不仅是无关重要的外围功能很多Envoy的核心功能都是用Filter来实现的比如对HTTP流量的治理、Tracing机制、多协议支持等等。
另外利用Filter机制Envoy理论上还可以实现任意协议的支持以及协议之间的转换也可以在实现对请求流量进行全方位的修改和定制的同时还保持较高的可维护性。
控制平面
如果说数据平面是行驶中的车辆,那控制平面就是车辆上的导航系统;如果说数据平面是城市的交通道路,那控制平面就是路口的指示牌与交通信号灯。控制平面的特点是不直接参与程序间通信,只会与数据平面中的代理通信。在程序不可见的背后,默默地完成下发配置和策略,指导数据平面工作。
由于服务网格暂时没有大规模引入计算机网络中管理平面Management Plane等其他概念所以控制平面通常也会附带地实现诸如网络行为的可视化、配置传输等一系列管理职能其实还是有专门的管理平面工具的比如Meshery、ServiceMeshHub。这里我仍然以Istio为例具体介绍一下控制平面的主要功能。
Istio在1.5版本之前Istio自身也是采用微服务架构开发的它把控制平面的职责分解为Mixer、Pilot、Galley、Citadel四个模块去实现其中Mixer负责鉴权策略与遥测Pilot负责对接Envoy的数据平面遵循xDS协议进行策略分发Galley负责配置管理为服务网格提供外部配置感知能力Citadel负责安全加密提供服务和用户层面的认证和鉴权、管理凭据和RBAC等安全相关能力。
不过经过两、三年的实践应用很多用户都在反馈Istio的微服务架构有过度设计的嫌疑。lstio在定义项目目标时曾非常理想化地提出控制平面的各个组件都应可以独立部署然而在实际的应用场景里却并不是这样独立的组件反而带来了部署复杂、职责划分不清晰等问题。
图片来自Istio官方文档
因此从1.5版本起Istio重新回归单体架构把Pilot、Galley、Citadel的功能全部集成到新的Istiod之中。当然这也并不是说完全推翻之前的设计只是将原有的多进程形态优化成单进程的形态让之前各个独立组件变成了Istiod的内部逻辑上的子模块而已。
单体化之后出现的新进程Istiod就承担所有的控制平面职责具体包括以下几种。
1. 数据平面交互:这是部分是满足服务网格正常工作所需的必要工作。
具体包括以下几个方面:
边车注入在Kubernetes中注册Mutating Webhook控制器实现代理容器的自动注入并生成Envoy的启动配置信息。
策略分发接手了原来Pilot的核心工作为所有的Envoy代理提供符合xDS协议的策略分发的服务。
配置分发接手了原来Galley的核心工作负责监听来自多种支持配置源的数据比如kube-apiserver本地配置文件或者定义为网格配置协议Mesh Configuration ProtocolMCP的配置信息。原来Galley需要处理的API校验和配置转发功能也包含在内。
2. 流量控制:这通常是用户使用服务网格的最主要目的。
具体包括以下几个方面:
请求路由通过VirtualService、DestinationRule 等Kubernetes CRD资源实现了灵活的服务版本切分与规则路由。比如根据服务的迭代版本号如v1.0版、v2.0版、根据部署环境如Development版、Production版作为路由规则来控制流量实现诸如金丝雀发布这类应用需求。
流量治理包括熔断、超时、重试等功能比如通过修改Envoy的最大连接数实现对请求的流量控制通过修改负载均衡策略在轮询、随机、最少访问等方式间进行切换通过设置异常探测策略将满足异常条件的实例从负载均衡池中摘除以保证服务的稳定性等等。
调试能力:包括故障注入和流量镜像等功能,比如在系统中人为设置一些故障,来测试系统的容错稳定性和系统恢复的能力。又比如通过复制一份请求流量,把它发送到镜像服务,从而满足 A/B验证的需要。
3. 通信安全:包括通信中的加密、凭证、认证、授权等功能。
具体包括以下几个方面:
生成CA证书接手了原来Galley的核心工作负责生成通信加密所需私钥和CA证书。
SDS服务代理最初Istio是通过Kubernetes的Secret卷的方式将证书分发到Pod中的从Istio 1.1之后改为通过SDS服务代理来解决。这种方式保证了私钥证书不会在网络中传输仅存在于SDS代理和Envoy的内存中证书刷新轮换也不需要重启Envoy。
认证:提供基于节点的服务认证和基于请求的用户认证,这项功能我曾在服务安全的“认证”中详细介绍过。
授权:提供不同级别的访问控制,这项功能我也曾在服务安全的“授权”中详细介绍过。
4. 可观测性:包括日志、追踪、度量三大块能力。
具体包括以下几个方面:
日志收集程序日志的收集并不属于服务网格的处理范畴通常会使用ELK Stack去完成这里是指远程服务的访问日志的收集对等的类比目标应该是以前Nginx、Tomcat的访问日志。
链路追踪为请求途经的所有服务生成分布式追踪数据并自动上报运维人员可以通过Zipkin等追踪系统从数据中重建服务调用链开发人员可以借此了解网格内服务的依赖和调用流程。
指标度量:基于四类不同的监控标识(响应延迟、流量大小、错误数量、饱和度)生成一系列观测不同服务的监控指标,用于记录和展示网格中服务状态。
小结
容器编排系统管理的最细粒度只能到达容器层次,在此粒度之下的技术细节,仍然只能依赖程序员自己来管理,编排系统很难提供有效的支持。
2016年原Twitter基础设施工程师威廉·摩根William Morgan和奥利弗·古尔德Oliver Gould在GitHub上发布了第一代的服务网格产品Linkerd并在很短的时间内围绕着Linkered组建了Buoyant公司。而后担任CEO的威廉·摩根在发表的文章《Whats A Service Mesh? And Why Do I Need One?》中首次正式地定义了“服务网格”Service Mesh一词。
此后,服务网格作为一种新兴通信理念开始迅速传播,越来越频繁地出现在各个公司以及技术社区的视野中。之所以服务网格能够获得企业与社区的重视,就是因为它很好地弥补了容器编排系统对分布式应用细粒度管控能力高不足的缺憾。
说实话,服务网格并不是什么神秘难以理解的黑科技,它只是一种处理程序间通信的基础设施,典型的存在形式是部署在应用旁边,一对一为应用提供服务的边车代理,以及管理这些边车代理的控制程序。
“边车”Sidecar本来就是一种常见的容器设计模式用来形容外挂在容器身上的辅助程序。早在容器盛行以前边车代理就就已经有了成功的应用案例。
比如2014年开始的Netflix Prana项目由于Netfilix OSS套件是用Java语言开发的为了让非JVM语言的微服务比如以Python、Node.js编写的程序也同样能接入Netfilix OSS生态享受到Eureka、Ribbon、Hystrix等框架的支持Netflix建立了Prana项目它的作用是为每个服务都提供一个专门的HTTP Endpoint以此让非JVM语言的程序能通过访问该Endpoint来获取系统中所有服务的实例、相关路由节点、系统配置参数等在Netfilix组件中管理的信息。
Netflix Prana的代理需要由应用程序主动去访问才能发挥作用但在容器的刻意支持下服务网格不需要应用程序的任何配合就能强制性地对应用通信进行管理。
它使用了类似网络攻击里中间人流量劫持的手段,完全透明(既无需程序主动访问,也不会被程序感知到)地接管容器与外界的通信,把管理的粒度从容器级别细化到了每个单独的远程服务级别,这就让基础设施干涉应用程序、介入程序行为的能力大为增强。
如此一来,云原生希望用基础设施接管应用程序非功能性需求的目标,就能更进一步。从容器粒度延伸到远程访问,分布式系统继容器和容器编排之后,又发掘到了另一块更广袤的舞台空间。
一课一思
服务网格中数据平面、控制平面的概念是从计算机网络中的SDN软件定义网络借用过来的在此之前你是否有接触过SDN方面的知识呢它与今天的服务网格有哪些联系与差异
欢迎在留言区分享你的答案和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。

View File

@@ -0,0 +1,202 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
61 _ 服务网格与生态:聊聊服务网格的两项标准规范
你好,我是周志明。这节课,我们来了解服务网格的主要规范与主流产品。
服务网格目前仍然处于技术浪潮的早期不过现在业界早已普遍认可它的价值基本上所有希望能影响云原生发展方向的企业都已经参与了进来。从最早2016年的Linkerd 和 Envoy到2017年Google、IBM和Lyft共同发布的Istio再到后来CNCF把Buoyant的 Conduit 改名为 Linkerd2再度参与Istio竞争。
而到了2018年后服务网格的话语权争夺战已经全面升级到由云计算巨头直接主导比如Google把Istio搬上Google Cloud Platform推出了Istio的公有云托管版本Google Cloud Service Mesh亚马逊推出了用于AWS的App Mesh微软推出了Azure完全托管版本的Service Fabric Mesh发布了自家的控制平面 Open Service Mesh国内的阿里巴巴也推出了基于Istio的修改版 SOFAMesh并开源了自己研发的 MOSN 代理。可以说,云计算的所有玩家都正在布局服务网格生态。
不过,市场繁荣的同时也带来了碎片化的问题。要知道,一个技术领域能够形成被业界普遍承认的规范标准,是这个领域从分头研究、各自开拓的萌芽状态,走向工业化生产应用的成熟状态的重要标志,标准的诞生可以说是每一项技术普及之路中都必须经历的“成人礼”。
在前面的课程中,我们接触过容器运行时领域的 CRI规范、容器网络领域的 CNI规范、容器存储领域的 CSI规范尽管服务网格诞生至今只有数年时间但作为微服务、云原生的前沿热点它也正在酝酿自己的标准规范也就是这节课我们要讨论的主角服务网格接口Service Mesh InterfaceSMI与通用数据平面APIUniversal Data Plane APIUDPA。现在我们先来看下这两者之间的关系
SMI规范与UDPA规范
实际上,服务网格是数据平面产品与控制平面产品的集合,所以在规范制订方面,很自然地也分成了两类:
SMI规范提供了外部环境实际上就是Kubernetes与控制平面交互的标准使得Kubernetes及在其之上的应用能够无缝地切换各种服务网格产品
UDPA规范则提供了控制平面与数据平面交互的标准使得服务网格产品能够灵活地搭配不同的边车代理针对不同场景的需求发挥各款边车代理的功能或者性能优势。
可以发现这两个规范并没有重叠它们的关系与我在容器运行时中介绍到的CRI和OCI规范之间的关系很相似。下面我们就从这两个规范的起源和支持者的背景入手了解一下它们要解决的问题及目前的发展状况。
服务网格接口
在2019年5月的KubeCon大会上微软联合Linkerd、HashiCorp、Solo、Kinvolk和Weaveworks等一批云原生服务商共同宣布了Service Mesh Interface规范希望能在各家的服务网格产品之上建立一个抽象的API层然后通过这个抽象来解耦和屏蔽底层服务网格实现让上层的应用、工具、生态系统可以建立在同一个业界标准之上从而实现应用程序在不同服务网格产品之间的无缝移植与互通。
如果你更熟悉Istio的话那你可以把SMI的作用理解为是给服务网格提供了一套Istio中VirtualService、DestinationRule、Gateway等私有概念对等的行业标准版本只要使用SMI中定义的标准资源应用程序就可以在不同的控制平面上灵活迁移唯一的要求是这些控制平面都支持了SMI规范。
SMI与Kubernetes是彻底绑定的规范的落地执行完全依靠在Kubernetes中部署SMI定义的CRD来实现这一点在SMI的目标中被形容为“Kubernetes Native”也就说明了微软等云服务厂商已经认定容器编排领域不会有Kubernetes之外的候选项了这也是微软选择在KubeCon大会上公布SMI规范的原因。
但是在另外一端 SMI并不与包括行业第一的Istio或者是微软自家的Open Service Mesh在内的任何控制平面所绑定这点在SMI的目标中被形容为“Provider Agnostic”说明微软务实地看到了服务网格领域目前还处于群雄混战的现状。Provider Agnostic对消费者有利但对目前处于行业领先地位的Istio肯定是不利的所以我们完全可以理解为什么SMI没有得到Istio及其背后的Google、IBM与Lyft的支持。
然而在过去两年里Istio无论是发展策略上、还是设计上过度设计的风评都不算很好业界一直在期待Google和Istio能做出改进这种期待在持续两年的失望之后已经有很多用户在考虑Istio以外的选择了。
所以SMI一经发布就吸引了除Istio之外几乎所有的服务网格玩家的目光大家全部参与了进来这恐怕并不只是因为微软号召力巨大的缘故。而且为了对抗Istio的抵制SMI自己还提供了一个 Istio的适配器以便使用Istio的程序能平滑地迁移到SMI之上所以遗留代码并不能为Istio构建出特别坚固的壁垒。
到了2020年4月SMI被托管到CNCF成为其中的一个Sandbox项目Sandbox是最低级别的项目CNCF只提供有限度的背书如果能够经过孵化、毕业阶段的话SMI就有望成为公认的行业标准这也是开源技术社区里民主管理的一点好处。
SMI规范的参与者
好了到这里我们就了解了SMI的背景与价值现在我们再来学习一下SMI的主要内容。目前v0.5版本的SMI规范包括四方面的API构成下面我们就分别来看一下。
流量规范Traffic Specs
目标是定义流量的表示方式比如TCP流量、HTTP/1流量、HTTP/2流量、gRPC流量、WebSocket流量等应该如何在配置中抽象和使用。目前SMI只提供了TCP和HTTP流量的直接支持而且都比较简陋比如HTTP流量的路由中甚至连以Header作为判断条件都不支持。
当然我们可以暂时自我安慰地解释为SMI在流量协议的扩展方面是完全开放的没有功能也有可能自己扩充哪怕不支持的或私有协议的流量也有可能使用SMI来管理。而我们知道流量表示是做路由和访问控制的必要基础因为它必须要根据流量中的特征为条件才能进行转发和控制而流量规范中已经自带了路由能力访问控制就被放到独立的规范中去实现了。
流量拆分Traffic Split
目标是定义不同版本服务之间的流量比例提供流量治理的能力比如限流、降级、容错等等以满足灰度发布、A/B测试等场景。
SMI的流量拆分是直接基于Kubernetes的Service资源来设置的这样做的好处是使用者不需要去学习理解新的概念而坏处是要拆分流量就必须定义出具有层次结构的Service即Service后面不是Pod而是其他Service。而Istio中则是设计了VirtualService这样的新概念来解决相同的问题它是通过Subset来拆分流量。至于两者孰优孰劣这就见仁见智了。
流量度量Traffic Metrics
目标是为资源提供通用集成点度量工具可以通过访问这些集成点来抓取指标。这部分完全遵循了Kubernetes的Metrics API进行扩充。
流量访问控制Traffic Access Control
目标是根据客户端的身份配置对特定的流量访问特定的服务提供简单的访问控制。SMI绑定了Kubernetes的ServiceAccount来做服务身份访问控制这里说的“简单”不是指它使用简单而是说它只支持ServiceAccount一种身份机制在正式使用中这恐怕是不足以应付所有场景的日后应该还需要继续扩充。
以上这四种API目前暂时都是Alpha版本也就是意味着它们还不够成熟随时可能发生变动。从目前的版本来看至少跟Istio的私有API相比SMI还没有看到明显的优势不过考虑到SMI还处于项目早期阶段不够强大也情有可原希望未来SMI可以成长为一个足够坚实可用的技术规范这也有助于避免数据平面出现一家独大的情况有利于竞争与发展。
通用数据面API
现在我们接着来了解一下通用数据面API的规范内容。同样是2019年5月CNCF创立了一个名为“通用数据平面API工作组”Universal Data Plane API Working GroupUDPA-WG的组织其工作目标是制定类似于软件定义网络中OpenFlow协议的数据平面交互标准。可以说工作组的名字被敲定的那一刻就已经决定了它所产出的标准名字必定是叫“通用数据平面API”Universal Data Plane APIUDPA
其实如果不纠结于是否足够标准、是否是由足够权威的组织来制定的话上节课我介绍数据平面时提到的Envoy xDS协议族就已经完全满足了控制平面与数据平面交互的需要。
事实上Envoy正是UDPA-WG工作组的主要成员在2019年11月的EnvoyCon大会上Envoy的核心开发者、UDPA的负责人之一来自Google公司的哈维 · 图奇Harvey Tuch做了一场以“The Universal Dataplane APIEnvoys Next Generation APIs”为题的演讲他详细而清晰地说明了xDS与UDAP之间的关系UDAP的研发就是基于xDS的经验为基础的在未来xDS将逐渐向UDPA靠拢最终将基于UDPA来实现。
UDPA规范与xDS协议融合时间表
上图是我在哈维 · 图奇的演讲PPT中截取的UDPA与xDS的融合时间表在演讲中哈维 · 图奇还提到了xDS协议的演进节奏会定为每年推出一个大版本、每个版本从发布到淘汰起要经历Alpha、Stable、Deprecated、Removed四个阶段、每个阶段持续一年时间简单地说就是每个大版本xDS在被淘汰前会有三年的固定生命周期。
基于UDPA的xDS v4 API原本计划会在2020年发布进入Alpha阶段不过我写下这段文字的时间是2020年的10月中旬已经可以肯定地说前面所列的这些计划一定会破产因为从目前公开的资料看来UDPA仍然处于早期设计阶段距离完备都还有一段很长的路程所以基于UDPA的xDS v4在2020年是铁定出不来了。
另外在规范内容方面由于UDPA连Alpha状态都还没能达到目前公开的资料还很少。从GitHub和Google文档上能找到的部分设计原型文件来看UDAP的主要内容会分为传输协议UDPA-TPTransPort和数据模型UDPA-DMData Model两部分这两个部分是独立设计的也就是说以后完全有可能会出现不同的数据模型共用同一套传输协议的可能性。
服务网格生态
OK到这里我们就基本理清了服务网格的主要规范。其实从2016年“Service Mesh”一词诞生至今不过短短四年时间服务网格就已经从研究理论变成了在工业界中广泛采用的技术用户的态度也从观望走向落地生产。
那么到目前,服务网格市场已经形成了初步的生态格局,尽管还没有决出最终的胜利者,但我们已经能基本看清这个领域里几个有望染指圣杯的玩家。下面,我就按照数据平面和控制平面,给你分别介绍一下目前服务网格产品的主要竞争者。
首先我们来看看在数据平面的主流产品主要有5种
Linkerd
2016年1月发布的Linkerd是服务网格的鼻祖使用Scala语言开发的Linkerd-proxy也就成为了业界第一款正式的边车代理。一年后的2017年1月Linkerd成功进入CNCF成为云原生基金会的孵化项目但此时的Linkerd其实已经显露出了明显的颓势由于Linkerd-proxy运行需要Java虚拟机的支持启动时间、预热、内存消耗等方面相比起晚它半年发布的挑战者Envoy均处于全面劣势因而Linkerd很快就被Istio和Envoy的组合所击败结束了它短暂的统治期。
Envoy
2016年9月开源的Envoy是目前边车代理产品中市场占有率最高的一款已经在很多个企业的生产环境里经受过大量检验。Envoy最初由Lyft公司开发后来Lyft与Google和IBM三方达成合作协议Envoy就成了Istio的默认数据平面。Envoy使用C++语言实现比起Linkerd在资源消耗方面有了明显的改善。
此外由于采用了公开的xDS协议进行控制Envoy并不只为Istio所私有这个特性也让Envoy被很多其他的管理平面选用为它夺得市场占有率桂冠做出了重要贡献。2017年9月Envoy加入CNCF成为CNCF继Linkerd之后的第二个数据平面项目。
nginMesh
2017年9月在NGINX Conf 2017大会上Nginx官方公布了基于著名服务器产品Nginx实现的边车代理nginMesh。nginMesh使用C语言开发有部分模块用了Golang和Rust是Nginx从网络通信踏入程序通信的一次重要尝试。
而我们知道Nginx在网络通信和流量转发方面拥有其他厂商难以匹敌的成熟经验因此本该成为数据平面的有力竞争者才对。然而结果却是Nginix在这方面投入资源有限方向摇摆让nginMesh的发展一直都不温不火到了2020年nginMesh终于宣告失败项目转入“非活跃”No Longer Under Active状态。
Conduit/Linkerd 2
2017年12月在KubeCon大会上Buoyant公司发布了Conduit的0.1版本这是Linkerd-proxy被Envoy击败后Buoyant公司使用Rust语言重新开发的第二代的服务网格产品最初是以Conduit命名在Conduit加入CNCF后不久Buoyant公司宣布它与原有的Linkerd项目合并被重新命名为Linkerd 2这样就只算一个项目了
使用Rust重写后Linkerd2-proxy的性能与资源消耗方面都已经不输Envoy了但它的定位通常是作为Linkerd 2的专有数据平面所以成功与否在很大程度上还是要取决于Linkerd 2的发展如何。
MOSN
2018年6月来自蚂蚁金服的MOSN宣布开源MOSN是SOFAStack中的一部分使用Golang语言实现在阿里巴巴及蚂蚁金服中经受住了大规模的应用考验。由于MOSN是技术阿里生态的一部分对于使用了Dubbo框架或者SOFABolt这样的RPC协议的微服务应用MOSN往往能够提供些额外的便捷性。2019年12月MOSN也加入了CNCF Landscape。
OK前面我介绍的是知名度和使用率最高的一部分数据平面我在选择时其实也考虑了不同程序语言实现的代表性其他的没提及的数据平面还有HAProxy Connect、Traefik、ServiceComb Mesher等等我就不再逐一介绍了。
然后,除了数据平面,服务网格中另外一条争夺激烈的战线是控制平面产品,主要包括了以下几种:
Linkerd 2
这是Buoyant公司的服务网格产品可以发现无论是数据平面还是控制平面他们都采用了“Linkerd”和“Linkerd 2”的名字。
现在Linkerd 2的身份已经从领跑者变成了Istio的挑战者。不过虽然代理的性能已经赶上了Envoy但功能上Linkerd 2还是不能跟Istio相媲美在mTLS、多集群支持、支持流量拆分条件的丰富程度等方面Istio都比Linkerd 2要更有优势毕竟两者背后的研发资源并不对等一方是创业公司Buoyant而另一方是Google、IBM等巨头。
然而相比起Linkerd 2Istio的缺点很大程度上也是由于其功能丰富带来的每个用户真的都需要支持非Kubernetes环境、支持多集群单控制平面、支持切换不同的数据平面等这类特性吗其实我认为在满足需要的前提下更小的功能集合往往意味着更高的性能与易用性。
Istio
这是Google、IBM和Lyft公司联手打造的产品它是以自己的Envoy为默认数据平面。Istio是目前功能最强大的服务网格如果你苦恼于这方面产品的选型直接挑选Istio的话不一定是最合适的但起码能保证应该是不会有明显缺陷的选择同时Istio也是市场占有率第一的控制平面不少公司发布的服务网格产品都是在它的基础上派生增强而来的比如蚂蚁金服的SOFAMesh、Google Cloud Service Mesh等。
不过服务网格毕竟比容器运行时、容器编排要年轻Istio在服务网格领域尽管占有不小的优势但统治力还远远不能与容器运行时领域的Docker和容器编排领域的Kubernetes相媲美。
Consul Connect
Consul Connect是来自HashiCorp公司的服务网格Consul Connect的目标是把现有由Consul管理的集群平滑升级为服务网格的解决方案。
就像Connect这个名字所预示的“链接”含义一样Consul Connect十分强调它整合集成的角色定位它不跟具体的网络和运行平台绑定可以切换多种数据平面默认为Envoy支持多种运行平台比如Kubernetest、Nomad或者标准的虚拟机环境。
OSM
Open Service MeshOSM是微软公司在2020年8月开源的服务网格它同样是以Envoy为数据平面。OSM项目的其中一个主要目标是作为SMI规范的参考实现。同时为了跟强大却复杂的Istio进行差异化竞争OSM明确以“轻量简单”为卖点通过减少边缘功能和对外暴露的API数量降低服务网格的学习使用成本。
现在服务网格正处于群雄争霸的战国时期世界三大云计算厂商中亚马逊的AWS App Mesh走的是专有闭源的发展路线剩下就只有微软与Google具有相似的体量能够对等地掰手腕了。
但是它们又选择了截然不同的竞争策略OSM开源后微软马上把它捐献给了CNCF成为开源社区的一部分与此相对尽管CNCF与Istio都有着Google的背景关系但Google却不惜违反与IBM、Lyft之间的协议拒绝将Istio托管至CNCF而是自建新组织转移了Istio的商标所有权。这种做法不出意外地遭到了开源界的抗议让观众产生了一种微软与Google身份错位的感觉在云计算的激烈竞争中似乎已经再也分不清楚谁是恶龙、谁是屠龙少年了。
以上就是目前一些主流的控制平面的产品了其他我没有提到的控制平面还有很多比如Traefik Mesh、Kuma等等我就不再展开介绍了。如果你有兴趣的话可以参考下我这里给出的链接。
小结
服务网格也许是未来的发展方向,但想要真正发展成熟并能大规模落地,还有很长的一段路要走。
一方面,相当多的程序员已经习惯了通过代码与组件库去进行微服务治理,并且已经积累了很多的经验,也能把产品做得足够成熟稳定,所以对服务网格的需求并不迫切;另一方面,目前服务网格产品的成熟度还有待提高,冒险迁移过于激进,也容易面临兼容性的问题。所以,也许我们要等到服务网格开始远离市场宣传的喧嚣,才会走向真正的落地。
一课一思
这节课已经是这门架构课程的最后一节了,希望这门课程能够对你有所启发,如果你学习完之后有什么感悟的话,希望你能留言与我分享。
不过接下来,我还会针对不同架构、技术方案(如单体架构、微服务、服务网格、无服务架构,等等),建立若干配套的代码工程,它们是整个课程中我所讲解的知识的实践示例。这些代码工程的内容就不需要录制音频了,你可以把它作为实际项目新创建时,可参考引用的基础代码。
好了,感谢你的阅读,如果你觉得有收获,把今天的内容分享给更多的朋友。

View File

@@ -0,0 +1,171 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
62 _ Fenix's Bookstore的前端工程
你好,我是周志明,现在我们就到了课程的最后一个模块。在上节课的最后,我给你简要介绍过了这个模块的设置目的,也就是建立若干配套的代码工程,作为针对不同架构、技术方案(如单体架构、微服务、服务网格、无服务架构,等等)的演示程序。它们是整个课程中我所讲解的知识的实践示例。这些代码工程的内容不需要录制音频,你可以把它作为实际项目新创建时,可参考引用的基础代码。
这节课的内容是由这些工程的README.md文件同步而来的不过因为没有经过持续集成工具自动处理所以可能会有偶尔更新不一致的情况我建议你可以到这些项目的GitHub页面上去查看最新情况。
文档工程:
软件架构探索;
Vuepress支持的文档工程。
前端工程:
Mock.js支持的纯前端演示
Vue.js 2实现前端工程。
后端工程:
Spring Boot实现单体架构
Spring Cloud实现微服务架构
Kubernetes为基础设施的微服务架构
Istio为基础设施的服务网格架构
AWS Lambda为基础的无服务架构。
在课程最开始的“导读”一节课中我已经说明了“The Fenix Project”的意义。Fenixs Bookstore的主要目的是展示不同的后端技术架构相对来说前端并不是它的重点。不过前端的页面比起后端的各种服务来是要直观得多的它能让使用者更容易理解我们将要做的是一件什么事情。
假设你是一名驾驶初学者,合理的学习路径肯定应该是把汽车发动,然后慢慢行驶起来,而不是马上从“引擎动力原理”“变速箱构造”入手,去设法深刻地了解一台汽车。所以,下面我们就先来运行下程序,看看最终的效果是什么样子吧。
运行程序
我们通过以下几种途径,就可以马上浏览最终的效果:
从互联网已部署由Travis-CI提供支持的网站由GitHub Pages提供主机直接在浏览器访问 http://bookstore.icyfenix.cn/。
通过Docker容器方式运行
$ docker run -d -p 80:80 --name bookstore icyfenix/bookstore:frontend
然后在浏览器访问http://localhost。
通过Git上的源码以开发模式运行
# 克隆获取源码
$ git clone https://github.com/fenixsoft/fenix-bookstore-frontend.git
# 进入工程根目录
$ cd fenix-bookstore-frontend
# 安装工程依赖
$ npm install
# 以开发模式运行地址为localhost:8080
$ npm run dev
然后在浏览器访问http://localhost:8080。
也许你已经注意到了以上这些运行方式都没有涉及到任何的服务端、数据库的部署。因为在现代的软件工程里基于MVVM的工程结构可以让前、后端的开发完全分离只要互相约定好服务的位置及模型即可。
Fenixs Bookstore以开发模式运行的时候会自动使用Mock.js拦截住所有的远程服务请求并通过事先准备好的数据来完成对这些请求的响应。
同时,你也应当注意到,在以纯前端方式运行的时候,所有对数据的修改请求实际都是无效的。比如用户注册,无论你输入何种用户名、密码,由于请求的响应是静态预置的,所以最终都会以同一个预设的用户登录。也正是因为这样,我并没有提供“默认用户”“默认密码”一类的信息供用户使用,你可以随意输入即可登录。
不过,那些只维护在前端的状态依然是可以变动的,典型的比如对购物车、收藏夹的增删改。让后端服务保持无状态,而把状态维持在前端中的设计,对服务的伸缩性和系统的鲁棒性都有着很大的益处,多数情况下都是值得倡导的良好设计。而其伴随而来的状态数据导致请求头变大、链路安全性等问题,都会在后面的服务端部分专门讨论和解决。
构建产品
要知道当你把程序用于正式部署时一般不应该部署开发阶段的程序而是要进行产品化Production与精简化Minification。你可以通过以下命令由node.js驱动webpack来自动完成
# 编译前端代码
$ npm run build
或者使用report参数同时输出依赖分析报告
# 编译前端代码并生成报告
$ npm run build --report
编译结果会存放在/dist目录中你应该把它拷贝到Web服务器的根目录下使用。而对于Fenixs Bookstore的各个服务端来说则通常是拷贝到网关工程中静态资源目录下。
与后端联调
同样,出于前后端分离的目的,理论上后端通常只应当依据约定的服务协议(接口定位、访问传输方式、参数及模型结构、服务水平协议等)提供服务,并以此为依据,进行不依赖前端的独立测试,最终集成时使用的是编译后的前端产品。
不过在开发期就进行的前后端联合在现今许多的企业之中仍然是主流形式由一个人“全栈式”地开发某个功能时更是如此。所以当你要在开发模式中进行联调时需要修改项目根目录下的main.js文件使其不导入Mock.js也就是如下代码所示的条件语句判断为假
/**
* 默认在开发模式中启用mock.js代替服务端请求
* 如需要同时调试服务端,请修改此处判断条件
*/
// eslint-disable-next-line no-constant-condition
if (process.env.MOCK) {
require('./api/mock')
}
当然也有其他一些相反的情况比如需要在生产包中仍然继续使用Mock.js提供服务时比如Docker镜像icyfenix/bookstore:frontend就是如此同样也应该修改这个条件使其结果为真在开发模式依然导入了Mock.js即可。
工程结构
Fenixs Bookstore的工程结构完全符合vue.js工程的典型习惯事实上它在建立时就是通过vue-cli初始化的。这项工程的结构与其中各个目录的作用主要如下所示
+---build webpack编译配置该目录的内容一般不做改动
+---config webpack编译配置用户需改动的内容提取至此
+---dist 编译输出结果存放的位置
+---markdown 与项目无关用于支持markdown的资源如图片
+---src
| +---api 本地与远程的API接口
| | +---local 本地服务如localStorage、加密等
| | +---mock 远程API接口的Mock
| | | \---json Mock返回的数据
| | \---remote 远程服务
| +---assets 资源文件会被webpack哈希和压缩
| +---components vue.js的组件目录按照使用页面的结构放置
| | +---home
| | | +---cart
| | | +---detail
| | | \---main
| | \---login
| +---pages vue.js的视图目录存放页面级组件
| | \---home
| +---plugins vue.js的插件如全局异常处理器
| +---router vue-router路由配置
| \---store vuex状态配置
| \---modules vuex状态按名空间分隔存放
\---static 静态资源,编译时原样打包,不会做哈希和压缩
组件
Fenixs Bookstore的前端部分是基于以下开源组件和免费资源构建的
Vue.js渐进式JavaScript框架。
Element一套为开发者、设计师和产品经理准备的基于Vue 2.0的桌面端组件库。
AxiosPromise based HTTP client for the browser and node.js。
Mock.js生成随机数据拦截Ajax请求。
DesignEvo一款由PearlMountain有限公司设计研发的Logo设计软件。
协议
课程的工程代码部分采用Apache 2.0协议进行许可。在遵循许可的前提下,你可以自由地对代码进行修改、再发布,也可以将代码用作商业用途。但要求你:
署名:在原有代码和衍生代码中,保留原作者署名及代码来源信息;
保留许可证在原有代码和衍生代码中保留Apache 2.0协议文件。

View File

@@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
63 _ 基于Spring Boot的单体架构
你好,我是周志明。
单体架构是Fenixs Bookstore服务端的起始版本它与后面的基于微服务Spring Cloud、Kubernetes、服务网格Istio、无服务Serverless架构风格实现的其他版本在业务功能上的表现是完全一致的。
所以,如果你不是针对性地带着解决某个具体问题、了解某项具体工具或技术的目的而来,而是有比较充裕的时间,希望了解软件架构的全貌与发展的话,我就推荐你从这个工程入手,来探索现代软件架构。因为单体架构的结构相对来说比较直观和易于理解,这对后面要接触的其他架构风格,也可以起到良好的铺垫作用。
运行程序
好,同样地,我们可以根据以下几种途径来运行程序,看看它的最终效果是怎么样的。
通过Docker容器的方式运行
$ docker run -d -p 8080:8080 --name bookstore icyfenix/bookstore:monolithic
然后在浏览器访问http://localhost:8080系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
这里默认会使用HSQLDB的内存模式作为数据库并在系统启动时自动初始化好了Schema完全开箱即用。但这同时也意味着当程序运行结束时所有的数据都不会被保留。而如果你希望使用HSQLDB的文件模式或者其他非嵌入式的独立的数据库支持的话也是很简单的。
这里我以常用的MySQL/MariaDB为例程序中也已经内置了MySQL的表结构初始化脚本你可以使用环境变量PROFILES来激活Spring Boot中针对MySQL所提供的配置命令如下所示
$ docker run -d -p 8080:8080 --name bookstore icyfenix/bookstore:monolithic -e PROFILES=mysql
此时你需要通过Docker link、Docker Compose或者直接在主机的Host文件中提供一个名为mysql_lan的DNS映射使程序能顺利链接到数据库。关于数据库的更多配置你可以参考源码中的application-mysql.yml。
通过Git上的源码以Maven运行
# 克隆获取源码
$ git clone https://github.com/fenixsoft/monolithic_arch_springboot.git
# 进入工程根目录
$ cd monolithic_arch_springboot
# 编译打包
# 采用Maven Wrapper此方式只需要机器安装有JDK 8或以上版本即可无需包括Maven在内的其他任何依赖
# 如在Windows下应使用mvnw.cmd package代替以下命令
$ ./mvnw package
# 运行程序地址为localhost:8080
$ java -jar target/bookstore-1.0.0-Monolithic-SNAPSHOT.j
然后在浏览器访问http://localhost:8080系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
通过Git上的源码在IDE环境中运行
以IntelliJ IDEA为例Git克隆本项目后在File->Open菜单选择本项目所在的目录或者pom.xml文件以Maven方式导入工程。
IDEA会自动识别出这是一个Spring Boot工程并定位启动入口为BookstoreApplication等到IDEA内置的Maven自动下载完所有的依赖包后运行该类即可启动。
如果你使用其他的IDE没有对Spring Boot的直接支持也可以自行定位到BookstoreApplication这是一个带有main()方法的Java类运行即可。
你可以通过IDEA的Maven面板中Lifecycle里面的package来对项目进行打包、发布。
在IDE环境中修改配置如数据库等会更加简单具体你可以参考工程里application.yml和application-mysql.yml中的内容。
技术组件
Fenixs Bookstore单体架构的后端会尽可能地采用标准的技术组件进行构建而不依赖于具体的实现包括以下几种
JSR 370Java API for RESTful Web Services 2.1JAX-RS 2.1-
在RESTFul服务方面采用的实现为Jersey 2你也可以替换为Apache CXF、RESTeasy、WebSphere、WebLogic等。
JSR 330Dependency Injection for Java 1.0-
在依赖注入方面采用的实现为Spring Boot 2.0中内置的Spring Framework 5。虽然在大多数场合中都尽可能地使用了JSR 330的标准注解但因为Spring在对@Named@Inject等注解的支持表现上跟它本身提供的注解存在差异所以仍然会有少量地方使用了Spring的私有注解。如果你要替换成其他的CDI实现比如HK2就需要进行比较大的改动了。
JSR 338Java Persistence 2.2-
在持久化方面采用的实现为Spring Data JPA。你可以替换为Batoo JPA、EclipseLink、OpenJPA等实现只需把使用CrudRepository所省略的代码手动补全回来即可无需做其他改动。
JSR 380Bean Validation 2.0-
在数据验证方面采用的实现为Hibernate Validator 6你也可以替换为Apache BVal等其他验证框架。
JSR 315Java Servlet 3.0-
在Web访问方面采用的实现为Spring Boot 2.0中默认的Tomcat 9 Embed你也可以替换为Jetty、Undertow等其他Web服务器。
不过,也有一些组件仍然依赖了非标准化的技术实现,包括以下两种:
JSR 375Java EE Security API specification 1.0-
在认证/授权方面在2017年才发布的JSR 375中仍然没有直接包含OAuth2和JWT的直接支持。这里因为后续实现微服务架构时作对比的需要在单体架构中我选择了Spring Security 5作为认证服务Spring Security OAuth 2.3作为授权服务Spring Security JWT作为JWT令牌支持并没有采用标准的JSR 375实现比如Soteria。
JSR 353/367Java API for JSON Processing/Binding-
在JSON序列化/反序列化方面由于Spring Security OAuth的限制使用JSON-B作为反序列化器时的结果与Jackson等有差异我采用了Spring Security OAuth默认的Jackson并没有采用标准的JSR 353/367实现比如Apache Johnzon、Eclipse Yasson等。
工程结构
Fenixs Bookstore单体架构的后端参考并未完全遵循了DDD的分层模式和设计原则整体分为以下四层。
1. Resource
对应DDD中的User Interface层负责向用户显示信息或者解释用户发出的命令。
请注意这里指的“用户”不一定是使用用户界面的人而可以是位于另一个进程或计算机的服务。由于这个工程采用了MVVM前后端分离的模式因此这里所指的用户实际上是前端的服务消费者所以这里我就以RESTful中的核心概念“资源”Resource来命名了。
2. Application
对应DDD中的Application层负责定义软件本身对外暴露的能力即软件本身可以完成哪些任务并负责对内协调领域对象来解决问题。
根据DDD的原则应用层要尽量简单不包含任何业务规则或者知识而只为下一层中的领域对象协调任务分配工作使它们互相协作这一点在代码上表现为Application层中一般不会存在任何的条件判断语句。
实际上在许多项目中Application层都会被选为包裹事务代码进入此层事务开始退出此层事务提交或者回滚的载体。
3.Domain
对应DDD中的Domain层负责实现业务逻辑即表达业务概念处理业务状态信息以及业务规则这些行为此层是整个项目的重点。
4. Infrastructure
对应DDD中的Infrastructure层向其他层提供通用的技术能力比如持久化能力、远程服务通讯、工具集等等。
协议
课程的工程代码部分采用Apache 2.0协议进行许可。在遵循许可的前提下,你可以自由地对代码进行修改、再发布,也可以将代码用作商业用途。但要求你:
署名:在原有代码和衍生代码中,保留原作者署名及代码来源信息;
保留许可证在原有代码和衍生代码中保留Apache 2.0协议文件。

View File

@@ -0,0 +1,173 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
64 _ 基于Spring Cloud的微服务架构
你好,我是周志明。
直到现在由不同编程语言、不同技术框架所开发的微服务系统中基于Spring Cloud的解决方案仍然是最主流的选择。这个结果既是Java在服务端应用所积累的深厚根基的体现也是Spring在Java生态系统中统治地位的体现。
而且从Spring Boot到Spring Cloud的过渡让现存数量非常庞大的、基于Spring和Spring Boot的单体系统可以平滑地迁移到微服务架构中让这些系统的大部分代码都能够无需修改或少量修改即可保留重用。
在微服务时代的早期Spring Cloud就集成了Netflix OSS以及Spring Cloud Netflix进入维护期后对应的替代组件这种成体系的微服务套件基本上也能算“半透明地”满足了在微服务环境中必然会面临的服务发现、远程调用、负载均衡、集中配置等非功能性的需求。
不过我个人是一直不太倾向于Spring Cloud Netflix这种以应用代码去解决基础设施功能问题的“解题思路”。因为以自顶向下的视角来看这既是虚拟化的微服务基础设施完全成熟之前必然会出现的应用形态也是微服务进化过程中必然会被替代的过渡形态。
不过我的看法如何并不重要基于Spring Cloud Netflix的微服务在当前就是主流甚至直到未来不算短的一段时间内仍然都会是主流。而且从应用的视角来看能自底向上地观察基础设施在微服务中面临的需求和挑战能用我们最熟悉的Java代码来解释分析问题也有利于深入理解微服务的整体思想。所以把它作为我们了解的第一种微服务架构的实现我认为是十分适合的。
那么下面我们就先来具体了解下在这种微服务架构下Fenixs Bookstore的需求场景是什么。
需求场景
小书店Fenixs Bookstore生意日益兴隆客人、货物、营收都在持续增长业务越来越复杂对信息系统的并发与可用方面的要求也越来越高。当然了由于业务属性和质量属性要求的提升信息系统需要更多的硬件资源去支撑这是合情合理的。但是如果我们把需求场景列得更具体些就会发现“合理”下面还有很多的无可奈何之处。
比如说,制约软件质量与业务能力提升的最大因素是人,而不是硬件。要知道,大多数企业即使再有钱也很难招到大量的、靠谱的开发者。此时,无论是引入外包团队,还是让少量技术专家带着大量普通水平的开发者去共同完成一个大型系统,就成为了必然的选择。
在单体架构下,没有什么能有效阻断错误传播的手段,系统中“整体”与“部分”的关系没有物理的划分,只能靠研发与项目管理措施来尽可能地保障系统质量,少量的技术专家也很难阻止大量螺丝钉式的程序员,或者是不熟悉原有技术架构的外包人员,在某个不起眼的地方犯错并产生全局性的影响,所以并不容易做出整体可靠的大型系统。
再比如说技术异构的需求从可选渐渐成为了必须。Fenixs Bookstore的单体版本是以目前应用范围最广的Java编程语言来开发的但我们依然可能遇到很多想做可Java却不擅长的事情。比如想去做人工智能进行深度学习训练发现大量的库和开源代码都离不开Python想要引入分布式协调工具时发现近几年ZooKeeper已经有被后起之秀Golang的etcd蚕食替代的趋势想要做集中式缓存发现无可争议的首选是ANSI C编写的Redis等等。
很多时候,为异构能力进行的分布式部署,并不是你想或者不想的问题,而是没有选择、无可避免的问题。
微服务的需求场景还有很多,这里我就不多列举了。总之,系统发展到一定程度,我们总能找到充分的理由去拆分与重构它。
在我设定的演示案例中准备把单体的Fenixs Bookstore 拆分为“用户”“商品”“交易”三个能够独立运行的子系统它们将在一系列非功能性技术模块认证、授权等和基础设施配置中心、服务发现等的支撑下互相协作以统一的API网关对外提供与原来单体系统功能一致的服务其应用视图如下图所示
运行程序
我们可以通过以下几种途径来运行程序,浏览最终的效果。
通过Docker容器方式运行
微服务涉及到多个容器的协作通过link单独运行容器已经被Docker官方声明为不提倡的方式。所以在工程中我提供了专门的配置以便你使用docker-compose来运行
# 下载docker-compose配置文件
$ curl -O https://raw.githubusercontent.com/fenixsoft/microservice_arch_springcloud/master/docker-compose.yml
# 启动服务
$ docker-compose up
然后在浏览器访问http://localhost:8080系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
通过Git上的源码以Maven编译、运行
由于我已经在配置文件中设置好了各个微服务的默认的地址和端口号以便于本地调试所以如果你要在同一台机运行这些服务并且每个微服务都只启动一个实例的话那不加任何配置、参数就可以正常以Maven编译、以Jar包形式运行。
另外,由于各个微服务需要从配置中心里获取具体的参数信息,因此唯一的要求只是“配置中心”的微服务必须作为第一个启动的服务进程,其他就没有别的前置要求了。具体的操作过程如下所示:
# 克隆获取源码
$ git clone https://github.com/fenixsoft/microservice_arch_springcloud.git
# 进入工程根目录
$ cd microservice_arch_springcloud
# 编译打包
# 采用Maven Wrapper此方式只需要机器安装有JDK 8或以上版本即可无需包括Maven在内的其他任何依赖
# 克隆后你可能需要使用chmod给mvnw赋予执行权限如在Windows下应使用mvnw.cmd package代替以下命令
$ ./mvnw package
# 工程将编译出七个SpringBoot Jar
# 启动服务需要运行以下七个微服务组件
# 配置中心微服务localhost:8888
$ java -jar ./bookstore-microservices-platform-configuration/target/bookstore-microservice-platform-configuration-1.0.0-SNAPSHOT.jar
# 服务发现微服务localhost:8761
$ java -jar ./bookstore-microservices-platform-registry/target/bookstore-microservices-platform-registry-1.0.0-SNAPSHOT.jar
# 服务网关微服务localhost:8080
$ java -jar ./bookstore-microservices-platform-gateway/target/bookstore-microservices-platform-gateway-1.0.0-SNAPSHOT.jar
# 安全认证微服务localhost:8301
$ java -jar ./bookstore-microservices-domain-security/target/bookstore-microservices-domain-security-1.0.0-SNAPSHOT.jar
# 用户信息微服务localhost:8401
$ java -jar ./bookstore-microservices-domain-account/target/bookstore-microservices-domain-account-1.0.0-SNAPSHOT.jar
# 商品仓库微服务localhost:8501
$ java -jar ./bookstore-microservices-domain-warehouse/target/bookstore-microservices-domain-warehouse-1.0.0-SNAPSHOT.jar
# 商品交易微服务localhost:8601
$ java -jar ./bookstore-microservices-domain-payment/target/bookstore-microservices-domain-payment-1.0.0-SNAPSHOT.jar
由于在命令行启动多个服务、通过容器实现各服务隔离、扩展等都比较繁琐我提供了一个docker-compose.dev.yml文件便于你在开发期调试使用
# 使用Maven编译出JAR包后可使用以下命令直接在本地构建镜像运行
$ docker-compose -f docker-compose.dev.yml up
以上两种本地运行的方式你可以任选其一服务全部启动后在浏览器访问http://localhost:8080系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
通过Git上的源码在IDE环境中运行
以IntelliJ IDEA为例Git克隆本项目后在File -> Open菜单选择本项目所在的目录或者pom.xml文件以Maven方式导入工程。
待Maven自动安装依赖后即可在IDE或者Maven面板中编译全部子模块的程序。
本工程下面的八个模块其中除bookstore-microservices-library-infrastructure外其余均是Spring Boot工程将这七个工程的Application类加入到IDEA的Run Dashboard面板中。
在Run Dashboard中先启动“bookstore-microservices-platform-configuration”微服务然后可以一次性启动其余六个子模块的微服务。
配置与横向扩展:-
工程中还预留了一些环境变量,以便于配置和扩展,比如想要在非容器的单机环境中,模拟热点模块的服务扩容,就需要调整每个服务的端口号。预留的这类环境变量包括:
# 修改配置中心的主机和端口默认为localhost:8888
CONFIG_HOST
CONFIG_PORT
# 修改服务发现的主机和端口默认为localhost:8761
REGISTRY_HOST
REGISTRY_PORT
# 修改认证中心的主机和端口默认为localhost:8301
AUTH_HOST
AUTH_PORT
# 修改当前微服务的端口号
# 比如,你打算在一台机器上扩容四个支付微服务以应对促销活动的流量高峰
# 可将它们的端口设置为8601默认、8602、8603、8604等
# 真实环境中,它们可能是在不同的物理机、容器环境下,这时扩容可无需调整端口
PORT
# SpringBoot所采用Profile配置文件默认为default
# 比如服务默认使用HSQLDB的内存模式作为数据库如需调整为MySQL可将此环境变量调整为mysql
# 因为我默认预置了名为applicaiton-mysql.yml的配置以及HSQLDB和MySQL的数据库脚本
# 如果你需要支持其他数据库、修改程序中其他的配置信息,可以在代码中自行加入另外的初始化脚本
PROFILES
# Java虚拟机运行参数默认为空
JAVA_OPTS
技术组件
Fenixs Bookstore采用基于Spring Cloud微服务架构微服务部分主要采用了Netflix OSS组件进行支持它们包括
配置中心默认采用Spring Cloud Config也可使用Spring Cloud Consul、Spring Cloud Alibaba Nacos代替。
服务发现默认采用Netflix Eureka也可使用Spring Cloud Consul、Spring Cloud ZooKeeper、etcd等代替。
服务网关默认采用Netflix Zuul也可使用Spring Cloud Gateway代替。
服务治理默认采用Netflix Hystrix也可使用Sentinel、Resilience4j代替。
进程内负载均衡默认采用Netfilix Ribbon也可使用Spring Cloud Loadbalancer代替。
声明式HTTP客户端默认采用Spring Cloud OpenFeign。声明式的HTTP客户端其实没有找替代品的必要性如果需要你可以考虑Retrofit或者使用RestTemplete乃至于更底层的OkHTTP、HTTPClient以命令式编程来访问多写一些代码而已。
尽管Netflix套件的使用人数很多但考虑到Spring Cloud Netflix已经进入维护模式所以这里我都列出了上述组件的代替品。这些组件几乎都是声明式的这确保了它们的替代成本相当低廉只需要更换注解修改配置无需改动代码。你在阅读源码时也会发现三个“platform”开头的服务基本上没有任何实际代码的存在。
其他与微服务无关的技术组件REST服务、安全、数据访问等等我已经在Fenixs Bookstore单体架构中介绍过了这里就不再重复。
协议
课程的工程代码部分采用Apache 2.0协议进行许可。在遵循许可的前提下,你可以自由地对代码进行修改、再发布,也可以将代码用作商业用途。但要求你:
署名:在原有代码和衍生代码中,保留原作者署名及代码来源信息;
保留许可证在原有代码和衍生代码中保留Apache 2.0协议文件。

View File

@@ -0,0 +1,157 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
65 _ 基于Kubernetes的微服务架构
你好,我是周志明。
我在第5讲中曾经把2017年描述为是“后微服务时代”的开端这是容器生态发展历史中具有里程碑意义的一年。
在这一年长期作为Docker竞争对手的RKT容器一派的领导者CoreOS宣布放弃自己的容器管理系统Fleet未来将会把所有容器管理的功能转移到Kubernetes之上去实现。
在这一年容器管理领域的独角兽Rancher Labs宣布放弃其内置了数年的容器管理系统Cattle提出了“All-in-Kubernetes”战略从2.0版本开始把1.x版本能够支持多种容器管理工具的Rancher“反向升级”为只支持Kubernetes一种容器管理系统。
在这一年Kubernetes的主要竞争者Apache Mesos在9月正式宣布了“Kubernetes on Mesos”集成计划由竞争关系转为对Kubernetes提供支持使其能够与Mesos的其他一级框架如HDFS、Spark 和Chronos等等进行集群资源动态共享、分配与隔离。
在这一年Kubernetes的最大竞争者Docker Swarm的母公司Docker终于在10月被迫宣布Docker要同时支持Swarm与Kubernetes两套容器管理系统事实上承认了Kubernetes的统治地位。
至此这场已经持续了三、四年时间以Docker Swarm、Apache Mesos与Kubernetes为主要竞争者的“容器战争”就终于有了明确的结果Kubernetes登基加冕是容器发展中一个时代的终章也将是软件架构发展下一个纪元的开端。
需求场景
当引入了基于Spring Cloud的微服务架构后小书店Fenixs Bookstore初步解决了扩容缩容、独立部署、运维和管理等问题满足了产品经理不断提出的日益复杂的业务需求。
可是对于团队的开发人员、设计人员、架构人员来说并没有感觉到工作变得轻松微服务中的各种新技术名词比如配置中心、服务发现、网关、熔断、负载均衡等就够一名新手学习好长一段时间从产品角度来看各种Spring Cloud的技术套件比如Config、Eureka、Zuul、Hystrix、Ribbon、Feign等也占据了产品的大部分编译后的代码容量。
而之所以在微服务架构里,我们选择在应用层面,而不是基础设施层面去解决这些分布式问题,完全是因为由硬件构成的基础设施,跟不上由软件构成的应用服务灵活性的无奈之举。
不过当Kubernetes统一了容器编排管理系统之后这些纯技术性的底层问题就开始有了被广泛认可和采纳的基础设施层面的解决方案。为此Fenixs Bookstore也迎来了它在“后微服务时代”中的下一次架构演进这次升级的目标主要有两点。
目标一:尽可能缩减非业务功能代码的比例。
在Fenixs Bookstore中用户服务Account、商品服务Warehouse、交易服务Payment三个工程是真正承载业务逻辑的认证授权服务Security可以认为是同时涉及到了技术与业务而配置中心Configuration、网关Gateway和服务注册中心Registry则是纯技术性。我们希望尽量消除这些纯技术的工程以及那些依附在其他业务工程上的纯技术组件。
目标二:尽可能在不影响原有代码的前提下完成迁移。
得益于Spring Framework 4中的Conditional Bean等声明式特性的出现近年来新发布的Java技术组件中声明式编程Declarative Programming已经逐步取代了命令式编程Imperative Programming成为主流的选择。
在声明式编程的支持下,我们可以从目的而不是过程的角度,去描述编码意图,让代码几乎不会与具体的技术实现产生耦合。而如果要更换一种技术实现,我们也只需要调整配置中的声明即可。
那么从升级结果来看如果仅以Java代码的角度来衡量本工程与此前基于Spring Cloud的实现没有任何差异两者的每一行Java代码都是一模一样的。
而实际上真正的区别在于Kubernetes的实现版本中直接删除了配置中心、服务注册中心的工程在其他工程的pom.xml中也删除了如Eureka、Ribbon、Config等组件的依赖。取而代之的是新增了若干以YAML配置文件为载体的Skaffold和Kubernetes的资源描述这些资源描述文件将会动态构建出DNS服务器、服务负载均衡器等一系列虚拟化的基础设施去代替原有的应用层面的技术组件。升级改造之后的应用架构如下图所示
运行程序
在已经部署Kubernetes集群的前提下你可以通过以下几种途径运行程序浏览最终的效果
直接在Kubernetes集群环境上运行
工程在编译时就已经通过Kustomize产生出集成式的资源描述文件你可以通过该文件直接在Kubernetes集群中运行程序
# 资源描述文件
$ kubectl apply -f https://raw.githubusercontent.com/fenixsoft/microservice_arch_kubernetes/master/bookstore.yml
注意在命令执行的过程中一共需要下载几百MB的镜像尤其是当Docker中没有各层基础镜像缓存时请你根据自己的网速保持一定的耐心。等未来GraalVM对Spring Cloud的支持更成熟一些后你也可以考虑采用GraalVM来改善这一点。
当所有的Pod都处于正常工作状态后你可以在浏览器访问http://localhost:30080系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
通过Skaffold在命令行或IDE中以调试方式运行
一般开发基于Kubernetes的微服务应用是在本地针对单个服务编码、调试完成后通过CI/CD流水线部署到Kubernetes中进行集成的。如果只是针对集成测试这并没有什么问题但同样的做法应用在开发阶段就相当不方便了我们不希望每做一处修改都要经过一次CI/CD流程这会非常耗时而且难以调试。
Skaffold是Google在2018年开源的一款加速应用在本地或远程Kubernetes集群中构建、推送、部署和调试的自动化命令行工具。对于Java应用来说它可以帮助我们做到监视代码变动自动打包出镜像将镜像打上动态标签并更新部署到Kubernetes集群为Java程序注入开放JDWP调试的参数并根据Kubernetes的服务端口自动在本地生成端口转发。
以上都是根据skaffold.yml中的配置来进行的开发时Skaffold会通过dev指令来执行这些配置具体的操作过程如下所示
# 克隆获取源码
$ git clone https://github.com/fenixsoft/microservice_arch_kubernetes.git && cd microservice_arch_kubernetes
# 编译打包
$ ./mvnw package
# 启动Skaffold
# 此时将会自动打包Docker镜像并部署到Kubernetes中
$ skaffold dev
服务全部启动后你可以在浏览器访问http://localhost:30080系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
另外由于面向的是开发环境基于效率原因我并没有像传统CI工程那样直接使用Maven的Docker镜像来打包Java源码而这就决定了在构建Dockerfile时我们要监视的变动目标将是Jar文件而不是Java源码。Skaffold的执行是由Jar包的编译结果来驱动的它只在进行Maven编译、输出了新的Jar包后才会更新镜像。
这样做的原因一方面是考虑到在Maven镜像中打包不方便利用本地的仓库缓存尤其在国内网络中速度实在难以忍受另一方面是我其实并不希望每保存一次源码时都自动构建和更新一次镜像毕竟比起传统的HotSwap或者Spring Devtool Reload来说更新镜像重启Pod是一个更加重负载的操作。未来CNCF的Buildpack成熟之后应该可以绕过笨重的Dockerfile对打包和容器热更新做更加精细化的控制。
另外如果你有IDE调试的需求我推荐你采用Google Cloud CodeCloud Code同时提供了VS Code和IntelliJ Idea的插件来配合Skaffold使用毕竟这是同一个公司出品的产品搭配起来能获得几乎与本地开发单体应用一致的编码和调试体验。
技术组件
Fenixs Bookstore采用基于Kubernetes的微服务架构并采用Spring Cloud Kubernetes做了适配其中主要的技术组件包括以下几种。
环境感知
Spring Cloud Kubernetes本身引入了Fabric8的Kubernetes Client作为容器环境感知不过引用的版本很旧比如Spring Cloud Kubernetes 1.1.2中采用的是Fabric8 Kubernetes Client 4.4.1Fabric8提供的兼容性列表中这个版本只支持到Kubernetes 1.14虽然实测在1.16上也能用但是在1.18上就无法识别到最新的Api-Server。
因此Maven引入依赖时你需要手工处理排除旧版本引入新版本本工程采用的是4.10.1)。
配置中心
采用Kubernetes的ConfigMap来管理通过Spring Cloud Kubernetes Config自动将ConfigMap的内容注入到Spring配置文件中并实现动态更新。
服务发现
采用Kubernetes的Service来管理通过Spring Cloud Kubernetes Discovery自动将HTTP访问中的服务转换为FQDN。
负载均衡
采用Kubernetes Service本身的负载均衡能力实现就是DNS负载均衡就可以不再需要Ribbon这样的客户端负载均衡了。Spring Cloud Kubernetes从1.1.2开始也已经移除了对Ribbon的适配支持暂时没有对其代替品Spring Cloud LoadBalancer提供适配。
服务网关
网关部分仍然保留了Zuul没有采用Ingress来代替。这里我主要有两点考虑一是Ingress Controller不算是Kubernetes的自带组件它可以有不同的选择如KONG、Nginx、Haproxy等同时也需要独立安装因此作为演示工程出于环境复杂度最小化的考虑我没有使用Ingress二是Fenixs Bookstore的前端工程是存放在网关中的移除了Zuul之后也仍然要维持一个前端工程的存在不能进一步缩减工程数量也就削弱了移除Zuul的动力。
服务熔断
这里仍然采用HystrixKubernetes本身无法做到精细化的服务治理包括熔断、流控、监视等等我们将在基于Istio的服务网格架构中解决这个问题。
认证授权
这里仍然采用Spring Security OAuth 2.0Kubernetes的RBAC授权可以解决服务层面的访问控制问题但Security是跨越了业务和技术的边界的认证授权模块本身仍然承担着对前端用户的认证、授权职责这部分是与业务相关的。
协议
课程的工程代码部分采用Apache 2.0协议进行许可。在遵循许可的前提下,你可以自由地对代码进行修改、再发布,也可以将代码用作商业用途。但要求你:
署名:在原有代码和衍生代码中,保留原作者署名及代码来源信息;
保留许可证在原有代码和衍生代码中保留Apache 2.0协议文件。

View File

@@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
66 _ 基于Istio的服务网格架构
你好,我是周志明。
当软件架构演进到基于Kubernetes实现的微服务时已经能够相当充分地享受到虚拟化技术发展的红利比如应用能够灵活地扩容缩容、不再畏惧单个服务的崩溃消亡、立足应用系统更高层来管理和编排各服务之间的版本、交互。
可是单纯的Kubernetes仍然不能解决我们面临的所有分布式技术问题。
在上一讲针对基于Kubernetes架构中的“技术组件”的介绍里我已经说过光靠着Kubernetes本身的虚拟化基础设施很难做到精细化的服务治理比如熔断、流控、观测等等而即使是那些它可以提供支持的分布式能力比如通过DNS服务来实现的服务发现与负载均衡也只能说是初步解决了分布式中如何调用服务的问题而已只靠DNS其实很难满足根据不同的配置规则、协议层次、均衡算法等去调节负载均衡的执行过程这类高级的配置需求。
Kubernetes提供的虚拟化基础设施是我们尝试从应用中剥离分布式技术代码踏出的第一步但只从微服务的灵活与可控这一点来说基于Kubernetes实现的版本其实比上一个Spring Cloud版本里用代码实现的效果功能强大、灵活程度有所倒退这也是当时我们没有放弃Hystrix、Spring Security OAuth 2.0等组件的原因。
所以说Kubernetes给予了我们强大的虚拟化基础设施这是一把好用的锤子但我们却不必把所有问题都看作钉子不必只局限于纯粹基础设施的解决方案。
现在基于Kubernetes之上构筑的服务网格Service Mesh是目前最先进的架构风格也就是通过中间人流量劫持的方式以介乎于应用和基础设施之间的边车代理Sidecar来做到既让用户代码可以专注业务需求不必关注分布式的技术又能实现几乎不亚于此前Spring Cloud时代的那种通过代码来解决分布式问题的可配置、安全和可观测性。
而这个目标现在已经成为了最热门的服务网格框架Istio的SloganConnect, Secure, Control, And Observe Services。
需求场景
得益于Kubernetes的强力支持小书店Fenixs Bookstore已经能够依赖虚拟化基础设施进行扩容缩容把用户请求分散到数量动态变化的Pod中处理可以应对相当规模的用户量了。
不过随着Kubernetes集群中的Pod数量规模越来越庞大到一定程度之后运维的同学就会无奈地表示已经不能够依靠人力来跟进微服务中出现的各种问题了一个请求在哪个服务上调用失败啦是A有调用B吗还是C调用D时出错了为什么这个请求、页面忽然卡住了怎么调度到这个Node上的服务比其他Node慢那么多这个Pod有Bug消耗了大量的TCP链接数……
而另外一方面随着Fenixs Bookstore程序规模与用户规模的壮大开发团队的人员数量也变得越来越多。尽管根据不同微服务进行拆分可以把每个服务的团队成员都控制在“2 Pizza Teams”的范围以内但一个很现实的问题是高端技术人员的数量总是有限的人多了就不可能保证每个人都是精英如何让普通的、初级的程序员依然能够做出靠谱的代码成为这一阶段技术管理者要重点思考的难题。
这时候,团队内部就出现了一种声音:微服务太复杂了,已经学不过来了,让我们回归单体吧……
所以在这样的故事背景下Fenixs Bookstore就迎来了它的下一次技术架构的演进这次的进化的目标主要有两点
目标一:实现在大规模虚拟服务下可管理、可观测的系统。
必须找到某种方法,针对应用系统整体层面,而不是针对单一微服务来连接、调度、配置和观测服务的执行情况。
此时,可视化整个系统的服务调用关系,动态配置调节服务节点的断路、重试和均衡参数,针对请求统一收集服务间的处理日志等功能,就不再是系统锦上添花的外围功能了,而是关系到系统能否正常运行、运维的必要支撑点。
目标二在代码层面裁剪技术栈深度回归单体架构中基于Spring Boot的开发模式而不是Spring Cloud或者Spring Cloud Kubernetes的技术架构。
我们并不是要去开历史的倒车,相反,我们是很贪心地希望开发重新变得简单的同时,又不能放弃现在微服务带来的一切好处。
在这个版本的Fenixs Bookstore里所有与Spring Cloud相关的技术组件比如上个版本遗留的Zuul网关、Hystrix断路器还有上个版本新引入的用于感知适配Kubernetes环境的Spring Cloud Kubernetes都将会被拆除掉。如果只观察单个微服务的技术堆栈它跟最初的单体架构几乎没有任何不同甚至还更加简单了连从单体架构开始一直保护着服务调用安全的Spring Security都移除掉了。
由于Fenixs Bookstore借用了Spring Security OAuth 2.0的密码模式做为登录服务的端点所以在Jar包层面Spring Security还是存在的但其用于安全保护的Servlet和Filter已经被关闭掉。
那么从升级目标上,我们可以明确地得到一种导向,也就是我们必须控制住服务数量膨胀后传递到运维团队的压力,只有让“每个运维人员能支持服务的数量”这个比例指标有指数级的提高,才能确保微服务下运维团队的健康运作。
而对于开发团队我们可以只要求一小部分核心的成员对微服务、Kubernetes、Istio等技术有深刻理解即可其余大部分的开发人员仍然可以基于最传统、普通的Spirng Boot技术栈来开发功能。升级改造之后的应用架构如下图所示
运行程序
在已经部署Kubernetes与Istio的前提下我们可以通过以下几种途径运行程序来浏览最终的效果
在Kubernetes无Sidecar状态下运行
在业务逻辑的开发过程中或者其他不需要双向TLS、不需要认证授权支持、不需要可观测性支持等非功能性能力增强的环境里可以不启动Envoy但还是要安装Istio的因为用到了Istio Ingress Gateway工程在编译时已经通过Kustomize产生出集成式的资源描述文件
# Kubernetes without Envoy资源描述文件
$ kubectl apply -f https://raw.githubusercontent.com/fenixsoft/servicemesh_arch_istio/master/bookstore-dev.yml
请注意资源文件中对Istio Ingress Gateway的设置是针对Istio默认安装编写的即以istio-ingressgateway作为标签以LoadBalancer形式对外开放80端口对内监听8080端口。在部署时可能需要根据实际情况进行调整你可以观察以下命令的输出结果来确认这一点
$ kubectl get svc istio-ingressgateway -nistio-system -o yaml
然后在浏览器访问http://localhost系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
在Istio服务网格环境上运行
工程在编译时已经通过Kustomize产生出集成式的资源描述文件你可以通过该文件直接在Kubernetes with Envoy集群中运行程序
# Kubernetes with Envoy 资源描述文件
$ kubectl apply -f https://raw.githubusercontent.com/fenixsoft/servicemesh_arch_istio/master/bookstore.yml
当所有的Pod都处于正常工作状态后这个过程一共需要下载几百MB的镜像尤其是Docker中没有各层基础镜像缓存时请根据自己的网速保持一定的耐心。未来GraalVM对Spring Cloud的支持更成熟一些后可以考虑采用GraalVM来改善这一点在浏览器访问http://localhost系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
通过Skaffold在命令行或IDE中以调试方式运行
这个运行方式与上一讲调试Kubernetes服务是完全一致的。它是在本地针对单个服务编码、调试完成后通过CI/CD流水线部署到Kubernetes中进行集成的。不过如果只是针对集成测试这并没有什么问题但同样的做法应用在开发阶段就非常不方便了我们不希望每做一处修改都要经过一次CI/CD流程这会非常耗时而且难以调试。
Skaffold是Google在2018年开源的一款加速应用在本地或远程Kubernetes集群中构建、推送、部署和调试的自动化命令行工具。对于Java应用来说它可以帮助我们做到监视代码变动自动打包出镜像将镜像打上动态标签并更新部署到Kubernetes集群为Java程序注入开放JDWP调试的参数并根据Kubernetes的服务端口自动在本地生成端口转发。
以上都是根据skaffold.yml中的配置来进行的开发时skaffold通过dev指令来执行这些配置具体的操作过程如下所示
# 克隆获取源码
$ git clone https://github.com/fenixsoft/servicemesh_arch_istio.git && cd servicemesh_arch_istio
# 编译打包
$ ./mvnw package
# 启动Skaffold
# 此时将会自动打包Docker镜像并部署到Kubernetes中
$ skaffold dev
服务全部启动后你可以在浏览器访问http://localhost系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。注意这里开放和监听的端口同样取决于Istio Ingress Gateway你可能需要根据系统环境来进行调整。
调整代理自动注入:
项目提供的资源文件中默认是允许边车代理自动注入到Pod中的而这会导致服务需要有额外的容器初始化过程。开发期间我们可能需要关闭自动注入以提升容器频繁改动、重新部署时的效率。如果需要关闭代理自动注入请自行调整bookstore-kubernetes-manifests目录下的bookstore-namespaces.yaml资源文件根据需要将istio-injection修改为enable或者disable。
如果关闭了边车代理就意味着你的服务丧失了访问控制以前是基于Spring Security实现的在Istio版本中这些代码已经被移除、断路器、服务网格可视化等一系列依靠Envoy代理所提供能力。但这些能力是纯技术的与业务无关并不影响业务功能正常使用所以在本地开发、调试期间关闭代理是可以考虑的。
技术组件
Fenixs Bookstore采用基于Istio的服务网格架构其中主要的技术组件包括
配置中心通过Kubernetes的ConfigMap来管理。
服务发现通过Kubernetes的Service来管理由于已经不再引入Spring Cloud Feign了所以在OpenFeign中我们直接使用短服务名进行访问。
负载均衡未注入边车代理时依赖KubeDNS实现基础的负载均衡一旦有了Envoy的支持就可以配置丰富的代理规则和策略。
服务网关依靠Istio Ingress Gateway来实现这里已经移除了Kubernetes版本中保留的Zuul网关。
服务容错依靠Envoy来实现这里已经移除了Kubernetes版本中保留的Hystrix。
认证授权依靠Istio的安全机制来实现这里实质上已经不再依赖Spring Security进行ACL控制但Spring Security OAuth 2.0仍然以第三方JWT授权中心的角色存在为系统提供终端用户认证为服务网格提供令牌生成、公钥JWKS等支持。
协议
课程的工程代码部分采用Apache 2.0协议进行许可。在遵循许可的前提下,你可以自由地对代码进行修改、再发布,也可以将代码用作商业用途。但要求你:
署名:在原有代码和衍生代码中,保留原作者署名及代码来源信息;
保留许可证在原有代码和衍生代码中保留Apache 2.0协议文件。

View File

@@ -0,0 +1,90 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
67 _ 基于云计算的无服务架构
你好,我是周志明。
首先我们要知道无服务架构Serverless跟微服务架构本身没有继承替代的关系它们并不是同一种层次的架构无服务的云函数可以作为微服务的一种实现方式甚至可能是未来很主流的实现方式。在课程中我们的话题主要还是聚焦在如何解决分布式架构下的种种问题所以相对来说无服务架构并不是重点不过为了保证架构演进的完整性我仍然建立了无服务架构的简单演示工程。
另外还要明确一点由于无服务架构在原理上就决定了它对程序的启动性能十分敏感这天生就不利于Java程序尤其不利于Spring这类启动时组装的CDI框架。因此基于Java的程序除非使用GraalVM做提前编译、将Spring的大部分Bean提前初始化或者迁移至Quarkus这种以原生程序为目标的框架上否则是很难实际用于生产的。
运行程序
Serverless架构的Fenixs Bookstore是基于亚马逊AWS Lambda平台运行的这是最早商用也是目前全球规模最大的Serverless运行平台。不过从2018年开始中国的主流云服务厂商比如阿里云、腾讯云也都推出了各自的Serverless云计算环境如果你需要在这些平台上运行Fenixs Bookstore你要根据平台提供的Java SDK对StreamLambdaHandler的代码做少许调整。
现在假设你已经完成了AWS注册、配置AWS CLI环境以及IAM账号的前提下就可以通过以下几种途径来运行程序浏览最终的效果
通过AWS SAMServerless Application Model Local在本地运行
AWS CLI中附有SAM CLI但是版本比较旧你可以通过如下地址安装最新版本的SAM CLI。另外SAM需要Docker运行环境支持你可参考此处部署。
首先编译应用出二进制包执行以下标准Maven打包命令即可
$ mvn clean package
根据pom.xml中assembly-zip的设置打包将不会生成SpringBoot Fat JAR而是产生适用于AWS Lambda的ZIP包。打包后确认已经在target目录生成了ZIP文件并且文件名称与代码中提供的sam.yaml配置的一致然后在工程根目录下运行如下命令启动本地SAM测试
$ sam local start-api --template sam.yaml
在浏览器访问http://localhost:3000系统预置了一个用户user:icyfenixpw:123456你也可以注册新用户来测试。
通过AWS Serverless CLI将本地ZIP包上传至云端运行
确认已经配置了AWS凭证后工程中已经提供了serverless.yml配置文件确认文件中ZIP的路径与实际Maven生成的一致然后在命令行执行
$ sls deploy
此时Serverless CLI会自动将ZIP文件上传至AWS S3然后生成对应的Layers和API Gateway运行结果如下所示
$ sls deploy
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service bookstore-serverless-awslambda-1.0-SNAPSHOT-lambda-package.zip file to S3 (53.58 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: spring-boot-serverless
stage: dev
region: us-east-1
stack: spring-boot-serverless-dev
resources: 10
api keys:
None
endpoints:
GET - https://cc1oj8hirl.execute-api.us-east-1.amazonaws.com/dev/
functions:
springBootServerless: spring-boot-serverless-dev-springBootServerless
layers:
None
Serverless: Removing old service artifacts from S3...
访问输出结果中的地址比如上面显示的https://cc1oj8hirl.execute-api.us-east-1.amazonaws.com/dev/)即可浏览结果。
这里要注意由于Serverless对响应速度的要求本来就较高所以我不建议再采用HSQLDB数据库来运行程序了毕竟每次冷启动都重置一次数据库本身也并不合理。代码中有提供MySQL的Schema我建议采用AWS RDB MySQL/MariaDB作为数据库来运行。
协议
课程的工程代码部分采用Apache 2.0协议进行许可。在遵循许可的前提下,你可以自由地对代码进行修改、再发布,也可以将代码用作商业用途。但要求你:
署名:在原有代码和衍生代码中,保留原作者署名及代码来源信息;
保留许可证在原有代码和衍生代码中保留Apache 2.0协议文件。

View File

@@ -0,0 +1,165 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
春节特别放送_ 有的放矢,事半功倍
你好,我是周老师的课程编辑王惠。今天是正月初一,首先在这里祝你春节快乐、牛年吉祥,在新的一年,学业进步、工作顺利~另外疫情当前,你也要注意保护好身体。
不知你还记不记得在开篇词里,我们曾发起过一个活动:
在课程更新的过程中,分享出你的学习心得、实践感悟等等,或者也可以分享出你自己在架构设计中的实践经历、遇到的坑以及避坑的经验。在期中和期末时,课程编辑会甄选出优秀的留言分享内容,专门做一个展示模块,最后还会送出这门课程的纸质版图书。
那么到了这里,我们就已经跟随老师走完一半的学习之旅了,相信这两个半月的时间里,你学到了很多,究竟掌握得怎么样呢?
所以在春节假期这段时间,正好我们可以把那些硬核艰深的架构知识放一放,轻松一下,一起来复盘复盘我们学过的知识点,做到有的放矢地学习。
然后,我们再通过同学们的优秀留言,来理解那些自己可能还不够熟悉的课程内容,或者体验一下自己没有经历过的实践过程、没有亲身踩过的坑,希望以此能帮助你更高效地学习课程。
好,我们开始吧。
“演进中的架构”模块内容复盘
这个模块里我们一起了解了微服务发展历程中出现的大量技术名词、概念以及了解了这些技术的时代背景和探索过程同时也在此过程中更深入地理解了Unix设计哲学的思想。
原始分布式时代。这是计算机科学对分布式和服务化的第一次探索DCE、CORBA等都是早期的分布式基础架构原始分布式架构设计的主要目的就是为了追求简单、符合 Unix 哲学的分布式系统,这也是软件开发者对分布式系统最初的美好愿景。
单体系统时代。单体作为迄今为止使用人数最多的一种软件架构风格具有易于分层、易于开发、易于部署测试、进程内的高效交互等优势。它也存在一些关键性的问题比如存在隔离与自治能力上的欠缺、不兼容“Phoenix”的特性等。但这并不意味着单体最终会被微服务所取代未来它仍然会长期存在。
SOA时代。虽然SOA架构具有完善的理论和工具可以解决分布式系统中几乎所有主要的技术问题曾经也被视为更大规模的软件发展的方向但它最终还是没能成为一种普适的软件架构。为什么呢实际上这正是由于SOA架构过于严谨精密的流程与理论使得它脱离了人民群众从而走上了被架构者抛弃的不归路。
微服务时代。早期的微服务架构作为SOA的一种轻量化的补救方案是在SOA发展的同时被催生出来的产物。但发展到现在可以说微服务已然成为了一种独立的架构风格。在该架构模式下我们需要解决什么问题就引入什么工具团队熟悉什么技术就使用什么框架对开发者来说十分友善。不过我们也同样需要警惕因为在微服务中对于那些分布式服务的问题不再有统一的解决方案因此可以说微服务所带来的自由是一把双刃剑。
后微服务时代。现在人们常说的“云原生”时代,就是课程中所讲的后微服务时代,因为它跟前面的微服务时代中追求的目标相比,并没有什么本质的改变,都是通过一系列小型服务去构建大型系统。可以说,容器化技术、虚拟化技术的发展和兴起,对软件架构、软件开发产生了很大改变,软件和硬件的界限开始变得模糊,业务与技术能够完全分离,远程与本地完全透明,如同老师所说,也许这就是分布式架构最好的时代。
无服务时代。无服务是近几年出现的新概念,它最大的卖点就是简单,只涉及了后端设施和函数两块内容,其设计目标是为了让开发者能够更纯粹地关注业务。不过我们要注意,与单体架构、微服务架构不同,无服务架构天生的一些特点,比如冷启动、无状态、运行时间有限制等等,决定了它不是一种具有普适性的架构模式,我们也不要误会它比微服务更先进。
模块留言精选
第1讲
来自@Jxin
我认为,可以从两个方面来看待“简单”,分别是业务和技术。
先说业务。现代软件系统的业务复杂性越来越高,而分离关注点,无疑是应对日益增长的业务复杂性的有效手段。但如果依旧是一个大型单体系统(所有业务单元都在一个容器下),那么跨业务单元的知识诉求便很难避免了,并且在开发迭代以及版本发布中,彼此还会相互影响。而微服务的出现,就为其提供了设定物理边界的技术基础,这就使得多个特性团队对业务知识的诉求可以收敛在自身领域内,降低了单个特性团队所需了解的业务知识。
再来说下技术。这里我认为主要体现在技术隔离上。就如同RPC可以让你像调用本地方法一样调用远程方法微服务技术组件的出现大多是为了让开发人员可以基于意图去使用各种协调分布式系统的工具而不用深入具体工具的实现细节去研究怎么解决的分布式难题。
另外就像SpringBoot提到的生产就绪微服务的生态已经不局限于开发的阶段。在部署和运行阶段都有健全组件的支持。它可以让开发人员基于意图就可以简便地实现金丝雀发布基于意图就能拿到所有系统运行期的数据。而所有的这些便利都算是技术隔离带来的好处。
来自@J.Spring
目前我们团队在做从传统HTTP直接调用、向SOA服务化架构的改造这个过程让我对SpringCloud这种面向HTTP的服务以及Dubbo-RPC服务产生了疑问。
因为单论简单SpringCloud看起来更简单但它缺乏完善且强大的服务治理能力。而Dubbo框架看似沉重却拥有很强大的服务治理功能。
所以我认为,简单的东西可能后期会变得复杂。而一开始的复杂,可能后期会变得简单。
第2讲
来自@STOREFEE
如果可以很明显地预估到项目的开发规模不会很大,但是对性能要求很高,局部范围需要经常迭代,而且需要多点部署的场景,那就非常适合单体架构。
不过我观察到凡是和互联网沾边的流行的软件项目基本其规模都在不断膨胀趋向于包罗万象。因为现在很多用户会觉得软件越来越多去切换不同的东西太麻烦了有的还得申请账号、填写资料等比较繁琐。最明显的例子就是石墨和飞书前几年感觉石墨文档很贴近Word挺不错的。另外现在飞书、企业微信等工具都整合了企业聊天、会议、文档、存储、绘图等一系列的东西。
所以说像这种一站式服务,绝对会采用非单体架构。
来自@小高
单体架构并不是一无是处的。在公司的初始阶段,为了让业务快速上线,就必须得采用单体架构。然后随着业务的增长,架构才得以演进。
还是那句话,架构不是一成不变的,而是持续演进的。或许,微服务也不是终点。
第3讲
来自@Wacky小恺
在目前的信息技术行业中如果按照严谨的SOA架构去设计系统那么不仅为开发人员带来了负担也加重了用户的学习成本使得在快速迭代中需求会被架构所限制。
我认为软件的设计应当为简洁的、无门槛使用的比如国民产品微信不需要过多的学习成本即可使用。而SOA的风格是自上向下的工业标准自然不符合时代的潮流“不接地气”因而就会被时代所抛弃。
来自@Frank
我之前也使用过SOAP协议来开发服务那时候我们公司自己搞了一个ESB但是好景不长。一开始是所有服务调用均走ESB不过后来由于某些原因直接绕过了ESB当时我其实并不理解为什么要这么做。后面随着不断学习才慢慢明白之前搞得ESB服务之间的协议等等的 “太重”了,实施维护成本很高,不适合自身业务的发展。
第4讲
来自@陈珙
我做.Net实施微服务的时候当时业界还没有特别成熟的选型与方案所以自己在组件、方案之间选型对比、整合花了不少的功夫。
老师说架构师是做平衡与取舍,而开发工程师是实施。我也这么认为。微服务的分而治之、化繁为简的思想是减少了业务开发复杂度,但同时引入了很多组件支撑服务,因此加大了技术复杂度。
我自己是有几条设计原则的,如下:
技术服务于架构,架构服务于业务;
康威定律;
架构的实施是需要对应开发模式支撑的。
那总结起来就是业务规模与团队规模决定了架构的规模一个增删查改的系统并不需要用微服务架构使用了前后端分离那么团队里多数是有前端工程师由微服务架构拆分引起的量变导致质变结合DevOps能更好地支持运作。
来自@Mr.Chen
其实用不用微服务架构,主要取决于业务,撇开业务谈架构都是在耍流氓!
我们公司面向企业私有化的项目就没有用微服务,主要是用户的并发量小,考虑到部署和运维的简单,直接上单体架构。
第5讲
来自@zhanyd
软件架构的发展方向,是慢慢地把与业务无关的技术问题,从软件的层面剥离出来,在硬件的基础设施之内就被悄悄解决掉,让开发人员只专注于业务,真正“围绕业务能力构建”团队与产品。
把复杂的问题交给计算机硬件解决,使得开发人员只需要关注业务,让开发越来越简单,同时能够调用的计算机资源也会越来越强大。
这也符合奥卡姆剃刀原则:“如无必要,勿增实体”。如果问题能让计算机自动解决,就不要麻烦人类。
来自@Jxin
分布式架构发展到服务网格后,真的是到达“最好的时代”了吗?我的回答是:没有最好,只有更好。
云原生下SLS的FaaS和服务网格的纯应用包这两个各自的需求差异还是挺大的。前者算是技术架构上对效率和成本的创新后者算是业务架构上对技术分离的追求。这是两个发展分支但是也不知道会不会产生新的问题。
不过,业务知识的易传递性、代码的开发、软件发布的效率、高可用和高性能的诉求,等等,这些在可见的未来,应该还会是需要持续解决的问题。
第6讲
来自@大D
2011年我刚毕业进公司开始使用Mule、ESB做集成当时我也是初次接触WebService这一套东西SOAP、WSDL等等用了一年也没搞明白都是干啥用的感觉就是俩字“复杂”。再后来公司的产品采用OSGI的方式自己通过订制Eclipse插件的方式开发了一套IDE每次打包要勾选一堆的依赖解决依赖冲突、查找依赖苦不堪言。这些东西本身就有很多技术壁垒和学习成本。
再后来Maven流行开始各种分模块后面公司用MQ实现了一套总线现在看来它就类似于老师讲的事件驱动架构这个架构还是要自己解决很多负载、补偿、事务等问题不过总体来说比之前有进步。
然后直到微服务的出现,感觉轻松了很多,框架层面的东西已经有了很多的解决方案,选择一个合适的就行,其他的专注于业务开发即可。
现在的年轻人确实赶上了一个好时代,不用理解那么多的复杂实现,可以更多地磨练自身编码能力。但我觉得经历过的都是财富,不然也不会对老师的课程产生强烈的共鸣。
来自@walkingonair
我正在腾讯云上摸索无服务的架构模式,完全赞同老师的说法。选择无服务的初始原因是由于微信小程序生态的强大,在腾讯云上进行产品的开发,能大大降低人力成本、运维成本,提高产品的开发速度,帮助创业小公司度过艰难的初期。
同时,无服务的架构模式,也能在业务量快速上升时,只需要简单增加成本投入,即可快速提高整个架构的业务承载能力,满足未来更大的业务增长。
但这种架构模式的不足也是十分明显的:
我虽然是一个全栈开发工程师但是平常使用最多的还是Java语言而Java的运行离不开Java虚拟机那么在这种架构下使用Java开发的云函数性能上能得到保证吗这个问题我保持怀疑态度这也导致我在语言方面选择的是Node而且它也更适合小程序开发者。
虽然无服务与编程语言无关,但是工程师的开发能力与语言有关,代码的规范、设计、管理方面与语言有关。就像老师所说的,做到普适性还有很长的路要走。
由于无服务架构对非业务层(云函数)的封装,一些特殊需求变得难以实现。例如云数据库的封装和限制,使基于云函数的开发、批量数据变得难以处理,函数运行的超时时间限制和数据库对大批量获取的限制等等,都是瓶颈。
无服务架构虽然屏蔽了除业务开发外的实现但是也对开发人员提出了更高的要求。云函数的实现需要满足无状态、幂等的要求否则或许会出现“匪夷所思”的Bug。
当前云开发的各项功能还不完善,开发人员权限的管理、各种资源的授权分配、云函数和云数据库等产品的管理在大型企业的模式下难以适用。
总而言之,云开发有着诱人的优点,但也有一些致命的不足。从架构演化的角度来说,无服务架构未来值得期待,这也是我选择无服务架构的最大原因。

View File

@@ -0,0 +1,276 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
春节特别放送_ 积累沉淀,知行合一
你好,我是编辑王惠,今天初四啦,同学们过年好啊~
今天呢,我们继续来复盘课程的第二个模块“架构师的视角”中的核心知识点,以及再次来感受、学习下在该模块中各位优秀同学的所学所得、所思所想。
“架构师的视角”模块内容复盘
在这个模块里,我们系统性地了解了在做架构设计时,架构师都应该思考哪些问题、可以选择哪些主流的解决方案和行业标准做法,以及这些主流方案都有什么优缺点、会给架构设计带来什么影响,等等,以此对架构设计这种抽象的工作有了更具体、更具象的认知。
服务风格设计
远程服务调用: RPC以模拟进程间方法调用为起点表示数据、传递数据和表示方法是RPC必须解决的三大基本问题。解决这些问题可以有很多方案这也是 RPC 协议/框架出现群雄混战局面的一个原因而另一个原因是简单的框架很难能达到功能强大的要求。一个RPC框架要想取得成功就要选择一个发展方向因此我们也就有了朝着面向对象发展、朝着性能发展和朝着简化发展这三条线。
RESTful服务 面向过程和面向对象两种编程思想虽然出现的时间有先后但在人类使用计算机语言来处理数据的工作中无论用哪种思维来抽象问题都是合乎逻辑的。而面向资源编程这种思想是把问题空间中的数据对象作为抽象的主体把解决问题时从输入数据到输出结果的处理过程看作是一个数据资源的状态不断发生变换而导致的结果。这符合目前网络主流的交互方式所以REST常常被看作是为基于网络的分布式系统量身定做的交互方式。
事务处理
本地事务: 本地事务是指仅操作特定单一事务资源的、不需要“全局事务管理器”进行协调的事务。ARIES理论提出了Write-Ahead Logging式的日志写入方法通过分析、重做、回滚三个阶段实现了STEAL、NO-FORCE从而实现了既高效又严谨的日志记录与故障恢复。此外在实现隔离性这方面我们要知道不同隔离级别以及幻读、脏读等问题都只是表面现象它们是各种锁在不同加锁时间上组合应用所产生的结果锁才是根本的原因。
全局事务: 全局事务可以理解为是一种适用于单个服务使用多个数据源场景的事务解决方案其中的两段式提交和三段式提交模式还会在一些多数据源的场景中用到它们追求ACID的强一致性这个目标不仅给它带来了很高的复杂度而且吞吐量和使用效果上也不够好。
共享事务: 共享事务是指多个服务共用同一个数据源,虽然目前共享事务确实已经很少见,不过通过了解事务演进的过程,也更便于我们理解其他三种事务类型。
分布式事务: 现在系统设计的主流已经变成了不追求ACID而是强调BASE的弱一致性事务也就是分布式事务它是指多个服务同时访问多个数据源的事务处理机制。我们要知道分布式系统中不存在放之四海皆准的万能事务解决方案针对具体场景选择合适的解决方案达到一致性与可用性之间的最佳平衡是我们作为一名设计者必须具备的技能。
透明多级分流系统
客户端缓存: 客户端缓存具体包括“状态缓存”、“强制缓存”和“协商缓存”三类,利用好客户端的缓存能够节省大量网络流量,这是为后端系统分流,以实现更高并发的第一步。
域名解析: 域名解析对于大多数信息系统尤其是基于互联网的系统来说是必不可少的组件它的主要作用就是把便于人类理解的域名地址转换为便于计算机处理的IP地址。
传输链路: 这也是一种与客户端关系较为密切的传输优化机制。这里我们要明确一点即HTTP并不是只将内容顺利传输到客户端就算完成任务了如何做到高效、无状态也是很重要的目标。另外在HTTP/2之前要想在应用一侧优化传输就必须要同时在其他方面付出相应的成本而HTTP/2 中的多路复用、头压缩等改进项,就从根本上给出了传输优化的解决方案。
内容分发网络: 内容分发网络CDN是一种已经存在了很长时间也被人们广泛应用的分流系统其工作过程主要涉及到路由解析、内容分发、负载均衡和它所能支持的应用内容四个方面。CDN能为互联网系统提供性能上的加速也能帮助增强许多功能比如说安全防御、资源修改、功能注入等。而且这一切又实现得极为透明可以完全不需要开发者来配合。
负载均衡: 负载均衡的两大职责就是“选择谁来处理用户请求”和“将用户请求转发过去”。如今一般实际用于生产的系统几乎都离不开集群部署,而在其中用于承担调度后方的多台机器,以统一的接口对外提供服务的技术组件,就是负载均衡器了。理解其工作原理,对于我们做系统的流量和容量规划工作是很有必要的。
服务端缓存: 服务端缓存也是一种通用的技术组件,它主要用于减少多个客户端相同的资源请求,缓解或降低服务器的负载压力,因此可以作为一种分流手段。
安全架构
认证: 认证解决的是“你是谁?”的问题,即如何正确分辨出操作用户的真实身份。在课程中我们了解了三种主流的认证方式,分别为通讯信道上的认证、通讯协议上的认证、通讯内容上的认证。
授权: 授权解决的是“你能干什么”的问题即如何控制一个用户该看到哪些数据、能操作哪些功能。我们可以使用OAuth 2.0来解决涉及到多方系统调用时可靠授权的问题而针对如何确保授权的结果可控的问题可以通过基于角色的访问控制RBAC来解决。
凭证: 凭证解决的是“你要如何证明”的问题即如何保证它与用户之间的承诺是准确、完整且不可抵赖的。对此我们也了解了Cookie-Session机制和无状态的JWT两种凭证实现方案它们分别适用于不同的场景因此我们在做架构设计时要做好权衡。
保密: 即解决如何保证敏感数据无法被内外部人员所窃取、滥用的问题。这里我们要知道,保密是有成本的,追求越高的安全等级,我们就要付出越多的工作量与算力消耗。
传输: 即解决如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充的问题。传输环节是最复杂、最有效,但又是最早就有了标准解决方案的,不管是哈希摘要、对称加密和非对称加密这三种安全架构中常见的保密操作,还是通过数字证书达成共同信任、通过传输安全层隐藏繁琐的安全过程。
验证: 验证解决的是“你做的对不对?”的问题,即如何确保提交的信息不会对系统稳定性、数据一致性、正确性产生风险。虽然貌似数据验证并不属于安全的范畴,但其实它与程序如何编码是密切相关的。这里我们需要明确一点,就是缺失的校验会影响数据质量,而过度的校验也不会让系统更加健壮,反而在某种意义上会制造垃圾代码,甚至还会有副作用。
模块留言精选
第7讲
来自@zhanyd
让计算机能够跟调用本地方法一样去调用远程方法应该是RPC的终极目标。但是目前的技术水平无法实现这一终极目标所以就有了其他更可行的折中方案。
事物是慢慢演化发展的,目标可以远大,但是做事还是要根据实际情况,实事求是。
第8讲
来自@Mr.Chen
RPC只是服务进程之间简化调用的一种方式它可以让开发者聚焦于业务本身。而对于服务间通信的各种细节交给框架处理这个维度来说如果撇开这一层面分布式系统的服务调用可以采用任何一种通信方式比如HTTP、Socket等。
第9讲
来自@tt
对于REST我的第一印象就是服务端无状态有利于水平扩展但更多的是停留于具体的技术层面。
过程如果有意义的话,一定会产生一个结果,这个结果就是资源的状态发生了转移(幂等前提下的重试不算),但是过程的细节更多,所以抽象程度无法做到向面向资源看齐。在一个过程中,我们可以对有关联关系的不同层次与结构的资源同时进行处理,但是面向资源却不容易做到。
我能想到的例子是转账操作同时操作两个资源即账户而且要保证事务的ACID。在转账的过程中要处理很多异常情况尤其是涉及到多方交易的时候所以写这样的交易就非常复杂容易出错。
如果用面向资源的角度去考察,可以看成是对三个资源的操作:转出账户、转入账户以及事务。这里把事务列为单独的资源,是为了呼应上面提到的一个资源状态变化引起的关联资源的变化。
如果转账操作利用TCCTry-Confirm-Cancel的方法我觉得就是一种更偏向于面向资源的做法每次只改变一个资源的状态。如果某个关联资源的状态改变失败就对它发起一个逆操作比如冲正。这样可以做到很高的并发在做到保序性的前提下做差错处理也很简单。相当于把一个复杂操作分解成了多个简单操作这样开发起来也很快很容易复用。有点类似CISC和RISC指令集的关系。
第10讲
来自@陈珙
谢谢老师的分享我也谈谈自己实践REST和RPC后的感想主要在第一、第二的争议点感触非常深。
争议一面向资源的编程思想只适合做CRUD只有面向过程、面向对象编程才能处理真正复杂的业务逻辑。-
争议二REST与HTTP完全绑定不适用于要求高性能传输的场景中。
之前我们用REST到了第2成熟度关于第一点争议包括我现在的想法也是认为面向资源更加适合做数据读写接口的场景例如某NoSQL的应用服务API封装或者提供某内部使用API的微服务更加适合。
原因主要有两个。首先如果是作为提供给前端使用处理起复杂业务的时候不好抽象其次原本一个接口可以处理的复杂逻辑但是因为REST原因导致接口粒度要细到N个假如由前端人员对接那么就会增大他们的业务组合难度我更加倾向大部分的业务逻辑由后端解决前端尽量关注数据展示与动画交互数据离后端人员最近
那么当粒度细化了以后就会引申出第二个争议所说的性能问题。这里也有两方面原因首先是因为要做接口的编排组合其次也是因为REST被HTTP绑得死死的那么开发人员就不得不去关注那些细节了。
举个例子HTTP Status Code的参数除了是body外可能还会是header也有可能是URI参数对于实际开发的便捷度来说并不够友好。
另外课程中老师还提到了GraphQL。该技术的确能缓解Query的部分问题但是我认为它同时也存在不可避免的问题就是如何让使用端可以很好地了解并对接数据源及其属性
对于这种存在争议性的东西,我的建议是尽可能地少引入团队。毕竟争议性越大,就意味着大家对它的理解越少,无论是引入推广还是实施的具体效果,都会存在很长周期的磨合与统一。
不过它也不是一文不值的。我们团队做的是解决方案解决的是针对性的问题场景对于一些比较清晰、比较接近数据的场景如NoSQL的API或者简单的、方便抽象的、相对需求稳定的业务场景如某内部微服务的API我认为是可以尝试使用的。
第11讲
来自@zhanyd
FORCE策略要求事务提交后变动的数据马上写入磁盘没有日志保护但是这样不能保证事务的原子性。比如用户的账号扣了钱、写入了数据库这时候系统崩溃了商品库存的变更信息和商家账号的变更信息都还没来得及写入数据库这样数据就不一致了。
因此为了实现原子性保证能够恢复崩溃绝大多数的数据库都采用NO-FORCE策略。而为了实现NO-FORCE策略就需要引入Redo Log重做日志来实现即使修改数据时系统崩溃了重启后根据Redo Log就可以选择恢复现场继续修改数据或者直接回滚整个事务。
换句话说就是,我先把我要改的东西记录在日志里,再根据日志统一写到磁盘中。万一我在写入磁盘的过程中晕倒了,等我醒来的时候,我照着日志重新做一遍,也能成功。
Commit Logging方式实行了NO-FORCE策略照理说这样已经实现了事务的功能已经很牛了但是当一个事务中的数据量特别大的时候等全部变更写入Redo Log然后再统一写入磁盘这样性能就不是很好就会很慢老板就会不开心。
那能不能在事务提交之前,偷偷地先写一点数据到磁盘呢(偷跑)?
答案是可以的这就是STEAL策略。但是问题来了你偷摸地写了数据万一事务要回滚或者系统崩溃了这些提前写入的数据就变成了脏数据必须想办法把它恢复才行。
这就需要引入Undo Log回滚日志在偷摸写入数据之前必须先在Undo Log中记录都写入了什么数据、改了什么地方到时候事务回滚了就按照Undo Log日志一条条恢复到原来的样子就像没有改过一样。
这种能够偷摸先写数据的方式就叫做Write-Ahead Logging。性能提高了同时也更复杂了。不过虽然它复杂了点但是效果很好啊MySQL、SQLite、PostgreSQL、SQL Server等数据库都实现了WAL机制呢。
第12讲
来自@Wacky小恺
在软件开发的发展历程中“提供简洁的API”始终贯穿至今因此这一讲中提到的透明事务在我看来对普通开发人员的使用层面来说是完全有必要的。
但是作为开发人员一定要有精益求精的品质也许我们在日常使用中已经习惯了使用简洁的API来实现强大的功能。但如果遇到棘手的问题或者需要自己思考解决方案的场景那么“内功”就能显露出它的威力。
第13讲
来自@zhanyd
学校组织知识竞赛,学生们(参与者)以一组为单位参加比赛,由一个监考老师(协调者)负责监考。考试分为考卷和答题卡,学生必须先在十分钟内把答案写在考卷上(记录日志),然后在三分钟内把答案涂到答题卡上(提交事务)。
两段式提交
准备阶段: 老师宣布“开始填写考卷时间十分钟”。十分钟内写好考卷的学生就回答Prepared。十分钟一到动作慢还没写好的学生就回答Non-Prepared。如果有学生回答Non-Prepared该小组被淘汰。
提交阶段: 如果所有的学生都回答了Prepared老师就会在笔记本上记下“开始填答题卡”Commit然后对所有的学生说“开始填答题卡”发送 Commit 指令)。学生听到指令后,就开始根据考卷去涂答题卡。
如果学生在涂答题卡的时候,过于紧张把答题卡涂错了,还可以根据考卷重新涂;如果所有的学生在规定时间内都填好了答题卡,老师宣布该小组考试通过。
三段式提交
CanCommit阶段 老师先给学生看一下考卷,问问学生能不能在十分钟内做完。如果有学生说没信心做完,该小组直接淘汰。
PreCommit阶段 如果学生都说能做完,老师就宣布:“开始填写考卷,时间十分钟”,和两段式提交的准备阶段一样。
DoCommit阶段 和两段式提交的提交阶段一样。
第14讲
来自@Goku
XA事务成立的前提是所有的服务都可用在分布式环境下使用的代价是如果有一个服务不可用那么整个系统就不可用了。另外XA事务可能会对业务有侵入而依靠可靠消息队列和重试机制则不需要侵入业务。
第15讲
来自@tt
我觉得可靠事件队列最适用的场景就是在内部系统中做高可靠。
TCC的范围扩大了一些适合于新设计的系统SAGA的适用性最广因为对服务提供的接口没有要求可以有落地人工处理做保证。我们现在涉及三方互联的老系统都可以看作是SAGA的一种形式。
第16讲
来自@zhanyd
一个不起眼的DNS竟然暗藏了这么多精妙的设计计算机技术发展的每个阶段成果都是人类智慧的结晶。
关于奥卡姆剃刀原则我也想做点补充。奥卡姆剃刀原则又被称为“简约之原则”它是由14世纪圣方济各会修道士奥卡姆英格兰的一个地方的威廉William of Occam提出来的他说过这样一段话
切勿浪费较多东西,去做“用较少的东西,同样可以做好的事情”。
更有名的一句话是:如无必要,勿增实体。
在历史上各个时代,最高深的物理学理论,从形式上讲都不复杂,从牛顿力学,到爱因斯坦的相对论,到今天物理学的标准模型。例如,质能方程 E=mc^2 ,欧拉恒等式 e^(iπ) + 1 = 0都以极简的方式描述了极其复杂的规律。
关于计算机系统,在能满足需求的前提下,最简单的系统就是最好的系统。很多人为了显示自己的技术水平,明明是很简单的需求,却上了一堆高大上的技术,为了技术而技术,忘了技术的本质是为业务服务的,这显然违背了奥卡姆剃刀原则。
第17讲
来自@追忆似水年华
我在做前端开发的时候遇到了微信会自动缓存页面静态资源的问题必须要手动刷新页面才行有时候还得刷新好几遍才可以有些极端情况则是短时间内连续刷新依然显示旧页面这个问题在公司内一些同事的手机上均出现过。学习了周老师的这节课对缓存有了基本的了解明天就用Charles抓包看看微信内对网页的客户端缓存策略是什么。
第18讲
来自@Jxin
这里想补充一下QUIC相对于TCP的两点内容
自定义重传机制: TCP是通过采样往返时间RTT不断调整的但这个采样存在不准的问题。第一次发送包A超时未返回第二次重发包A这时收到了包A的响应但TCP并不能识别当前包A的响应是第一次发送还是第二次重发返回的这时不管怎么减都可能出现计时偏长或过偏短的问题。而QUIC为每次发送包都打了版本号包括重发所以可以很好地识别返回的包是哪次发送包的进而计算也就相对准确。
自定义流量控制: TCP的流量控制是通过滑动窗口协议是在连接上控制的窗口。QUIC也玩滑动窗口但是力度是可以细分到具体的Stream。
其实应用层的协议多种多样比如直播的RTMP、物联网终端的MQTT等但感觉都是两害取其轻的专项优化、对症下药的方案。只有QUIC直面了TCP的问题通过应用层的编码实现系统地提供更好的“TCP连接”。
第19讲
来自@zhanyd
在网上看到华为云CDN主要的应用场景希望借此可以帮助我们更好地理解内容
网站加速CDN网络能够对加速域名下的静态内容提供良好的加速服务。支持自定义缓存规则用户可以根据数据需求设置缓存过期时间缓存格式包括但不限于zip、exe、wmv、gif、png、bmp、wma、rar、jpeg、jpg等。
文件下载加速适用于使用http/https文件下载业务的网站、下载工具、游戏客户端、App商店等。
点播加速适用于提供音视频点播服务的客户。通过分布在各个区域的CDN节点将音视频内容扩展到距离用户较近的地方随时随地为用户提供高品质的访问体验。
全站加速适用于各行业动静态内容混合含较多动态资源请求如asp、jsp、php等格式的文件的网站。
第20讲
来自@zhanyd
为什么负载均衡不能只在某一个网络层次中完成,而是要进行多级混合的负载均衡?
因为每一个网络层的功能是不一样的这样就决定了每一层都有自己独有的数据在不同的网络层做负载均衡能达到不同的效果。比如要修改MAC地址在数据链路层修改最方便要修改IP地址最好在网络层修改。
关于网络分层我这里也打个比方。小帅在网上下单买东西卖家需要寄快递把要寄的商品物理层打包到包装盒里数据链路层然后把包装盒放到快递盒子里网络层在快递单上写上寄件地址和收件地址Headers
然后快递员打电话给小帅拿快递传输层这里出现了TCP三次握手连接
快递员:“喂,这里有你的快递,麻烦到门口拿一下”。
小帅:“好的,我这就过来”。
快递员:“那我在门口等你”。
小帅拿到快递后,在网上点击确认收货按钮,确认收货(返回 http Status Code 200应用层
第22讲
来自@zhanyd
“能满足需求的前提下最简单的系统就是最好的系统”。这句话的隐藏前提是我们的选择空间要足够大。不管是CDN、负载均衡、客户端缓存、服务端缓存还是分布式缓存都给我们提供了大量的选择余地可以根据自己系统的实际情况灵活地选择最适合的方案。
这句话的另一种理解是:没有最好的方案,只有最合适的方案。
第25讲
来自@zhanyd
角色是为了解耦用户和权限之间的多对多关系。比如有100个用户他们的权限都是一样的如果给每个用户都设一遍权限这就太麻烦了而且还很容易出错。这时候设置一个角色把对应的权限配置到角色上然后这100个用户加到这个角色中就行了。
角色还有一个好处,如果角色的权限变了,所有角色中用户的权限也会同时变更,不用一个个用户去设置了。
许可是为了解耦操作与资源之间的多对多关系,比如有新增用户、编辑用户、删除用户的三种操作,通常这些都是一起的,要么都能操作,要么都不能操作。这时候就可以把这三种操作打包成一个用户维护许可,用许可和角色关联更简洁。
关于Spring Security中的Role和Authority我是这么理解的Role就是普通的角色拥有一组许可这个角色下的所有用户的权限都是一样的。
但是如果一个角色中的一些用户有个性化的需求,比如销售助理角色,本来没有查看客户的权限,但是某个销售助理比较特殊,需要查看客户的信息,这时如果是单角色的系统,就需要新增一个“销售助理可查看客户角色”,这样很容易导致角色数量爆炸。
而有了Authority就可以满足这种个性化需求只要把查看客户的权限加到Authority中赋予用户就行了。
第26讲
来自@zhanyd
Cookie-Session就相当于是坐飞机托运了行李只要带着登机牌就行了。但是一旦托运了行李行李就和飞机绑定了你就不能随意换航班了。
JWT就相当于是坐飞机拎着行李到处跑每次过安检还要打开行李箱检查而且箱子太小也带不了多少东西。但它的优点是可以随意换航班行李都在自己身边。

View File

@@ -0,0 +1,115 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
用户故事 _ 詹应达:持续成长,不惧未来
你好,我是编辑王惠。今天是大年初六,春节假期也快要结束啦,春节玩得还开心吗?在放松的同时也别忘了要继续学习哦。
今天这一讲,我们邀请了一位优秀的同学来做分享,如果你看过之前更新的两篇春节特别放送,那应该会很熟悉他的名字@zhanyd
其实在梳理课程留言的时候,我就注意到了詹应达同学一直在跟随周老师的脚步,学习和实践软件架构的相关知识点。留言的内容十分有见地、提出的问题也能看出是经过了他深入的思考。所以我邀请他来和我们分享一下他的学习心得与体会。
OK下面我们就一起来看看吧
你好我是詹应达zhanyd一名工作十多年的程序员目前在温州做制造业信息化相关的工作很高兴能和你分享我学习这门课程的心得。
为什么要学这门课?
首先我想和你聊聊我为什么想要学习这门课。
作为一个三线城市的程序员CRUD BOY想在工作中不断学习、突破瓶颈有质的飞跃说实话我觉得真的很难。
就拿我自己来说吧我们公司搞开发的就那么几个人大家的水平都差不多都是“面向百度编程”而且业务上只有简单的CRUD和复杂的CRUD高并发分布式不存在的。要想提升技术能力的话就只能靠自己的悟性了。
可是高并发和分布式系统在程序员的能力进阶之路上,都是绕不开的高墙。所以就算是工作中没需求,我也想要学,不然像我这种“高龄”程序员,再不往上提升的话,迟早会被市场淘汰。
然而,分布式系统是出了名的难搞,我想学,但是无从下手,在网上东看篇文章,西学点概念,都是很零碎的知识,不成体系,又没深度。再这样下去,我迟早会知难而退,对分布式系统敬而远之。
不过幸运的是,后来我在极客时间找到了周老师的课程,看了课程的目录,真是如获至宝。现在也跟着课程更新学完了一半的内容,收获颇多,老师对不同架构风格的阐释,让我可以从全局性的视角来理解分布式架构的发展历史、来龙去脉,了解各种架构技术的时代背景和探索过程,从而让我能去深入理解架构设计的本质。
而且除此之外,课程里的很多内容都有既高屋建瓴、又深入浅出的特点,就算是对架构知识的积累不够多的人,都可以做到不紧不慢地跟上老师的脚步。所以,我也想来谈谈这门课程最吸引我的三个原因。
自成体系
分布式架构内容太多、太复杂,让人望而生畏,无从下手?不要怕,这门课程的知识内容真的非常全面,它并没有像现在市面上的一些介绍分布式的书籍资料,只是着眼于一个很小的分支,而是涵盖了分布式系统的方方面面,且自成体系,既有广度,又有深度。
在课程中,周老师已经给我们画好了学习地图,按照这个地图去探索分布式架构,就一定不会迷路,而且这个地图还能让我们对分布式架构的演化历程、如何解决分布式系统难题的高手思路,以及不同的技术方案都有什么优缺点、如果对各种技术做好取舍等,都有具体化、具象化的感知,我们能够清楚地知道自己所掌握的知识程度,也能以此查漏补缺,弥补认知上的不足。
授之以渔
周老师不仅在课程中表达了自己的观点,用自己的话把架构技术知识讲解得很透彻,而且在课程里针对很多理论概念都给出了超链接,把参考资料以及思考的过程展示给我们看。
其中包括很多专业的论文,老师会找到原始的出处,带我们去看第一手资料,从技术的诞生开始,把技术的来龙去脉讲得清清楚楚,有理有据。
我平时在网上看惯了二手、三手的资料,很多内容都是互相搬运,甚至可能是错的。而老师提供的原始论文以及高质量的文章,最能接近技术的本质,信息量极大,不仅教会了我们知识,还教会了我们学习的方法,既授之以鱼,又授之以渔。
独到见解
在学习课程的过程中,我常常惊叹于老师的知识面之广、对技术的理解之深。这么多的知识点,老师都能讲得驾轻就熟,把复杂的知识概念解释得通俗易懂。
有些内容我以前找了好多资料还是搞不懂其中的概念而老师一两节课就讲明白了对新人非常友好。举个简单的例子像是CAP定理、TCC、SAGA等概念我一直都处于懵懵懂懂的状态而且时间一长就会觉得这都是些高深难懂的理论大厂专用不是我等能学会的严重打击了我的自信心。结果老师在讲“分布式事务”这两节课的时候就从原理上解释得清清楚楚我一看就懂了。
还有,老师很多时候并不会囿于“术”的层面去讲解知识点,还会从“道”的层面帮我们拓展技术视野。比如我们很熟悉的奥卡姆剃刀原则:如无必要,勿增实体。这不仅体现在架构设计上,对于我们的生活来说,也是一样的道理,只有适合自己的才是最好的。可以说,周老师的课程真正做到了“道”与“术”的平衡,为我们揭开了软件架构设计的神秘面纱。
总而言之,在这门课程中,我找到了学习分布式系统的绝好资料和学习路径,手拿老师的课程地图,和同学们一起脚踏实地地学习,做到心中有数,不会迷路、不孤单。
我对于学习方法的思考
我猜,一直跟随学习课程的同学,应该对我比较眼熟,因为几乎每节课学完后,我都会来留言,不管是说说对这节课的思考也好,还是只简单地刷一下存在感也好,我都一直在坚持做这件事。
其实我这么做是有原因的。
不知道你有没有类似的感受,以前在学习其他专栏课程的时候,明明学得很认真、很仔细,每节课的知识点我都觉得理解了、学明白了,但总是过几天就会忘掉,再久一点儿就只剩一个很模糊的印象了。
后来我发现这里存在一个问题,就是从学校到职场,我们一直都在学习各种新知识、新技术,但貌似具体的学习方法却从没有系统地学习过。
而直到我接触了学习金字塔,才终于知道这是怎么回事了。
原来,人的学习分为了被动学习和主动学习两个层次。
在单纯地听讲时人们只能记住5%的内容阅读只能记住10%的内容,这些都是在低效地、被动地学习,所以怪不得我听过看过的内容忘得这么快。而主动地、高效地学习,是要去思考、总结和归纳,并且要找人讨论、实践,然后以教为学。
所以也就是说,我们一定要有输出,用输出来倒逼输入。这正如同周老师在开篇词里所说的:把自己“认为掌握了的”知识给叙述出来,能够写得条理清晰,讲得理直气壮;能够让别人听得明白,释去心中疑惑;能够把自己的观点交给别人审视,乃至质疑。在这个过程之中,就会挖掘出很多潜藏在“已知”背后的“未知”。
那么具体我们要如何学会主动学习呢?这里我想啰嗦一下非常著名的费曼学习法。简单来说分为四步:
选择一个你要了解的概念或知识点;
试着把它讲给10岁的小孩子听
如果卡壳了,或者说不明白,就重新去查资料学习;
确认自己理解清楚了,再用最简洁的语言或者比喻重新讲一遍。
因此按照费曼学习法我在开始学习课程之前就立了个Flag每篇文章下面都要写留言而且最好能用通俗的语言或比喻表达出来。
不得不说,写留言真的很有效果,一些看似已经明白的概念,如果用自己的话再说一次,就会发现其中的很多细节我其实根本没搞懂,我要不停地查资料,把不懂的地方搞明白,然后再试着联系生活场景来打比方,用自己的话讲出来。有时候一条一两百字的留言,我甚至要写好几个小时。
第26讲中关于我对Cookie-Session和JWT两种凭证实现方案的理解-
Cookie-Session相当于坐飞机托运了行李只要带着登机牌就行了但是一旦托运了行李行李就和飞机绑定了你就不能随意换航班了JWT相当于坐飞机拎着行李到处跑每次过安检还要打开行李箱检查而且箱子太小也带不了多少东西但优点是可以随意换航班行李都在自己身边。
而且留言还有一个好处,就是我可以通过其他同学的提问,以及与同学们的交流讨论,再次巩固学到的知识点,让不同的思想碰撞,从而更接近学习的本质;更重要的也是能通过周老师的回复和指点,可以进一步拓展认知,收获应用、实践的经验。
周老师这样的大神,我在平时是绝无可能碰到的,能和老师近距离地交流,是非常难得的机会,要好好珍惜。
写在最后
说起来我也算是周老师的老读者了《深入理解Java虚拟机》读完之后收获非常大对Java虚拟机有了全面的认识老师总能抓住事物的本质把问题说得明明白白这门课程也保持了老师一贯的高品质。
周老师在课程里提到过技术人的成长捷径,就是“做技术不仅要去看、去读、去想、去用,更要去写、去说”,这让我印象非常深刻。
所以在最后,我还想说的是,我们一定要保持成长型的思维模式,也就是相信自己只要努力就可以做得更好。我一直认为,成功主要来源于尽自己最大的努力做事情,来源于主动学习和自我提高,不管我们的起点有多低,受到过多少挫折,只要我们有成长型思维,努力奋斗、不怕失败,保持终身学习、不断成长,就能在如今技术日新月异的时代,不被淘汰、不惧未来。
我都可以做到,相信你一定能行!
好了,我的分享就到这里啦,不知道你是怎么学习这门课程的?有没有什么独特的学习方法和心路历程呢?欢迎你在留言区分享,我们一起交流,相互鼓励,共同进步!

View File

@@ -0,0 +1,116 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 _ 程序员之路
你好,我是周志明。
到这里我们的软件架构之旅就要到终点站了首先感谢你与我一起学完了这门70多讲、30多万字的课程。
这门课讲的是软件架构,不过这并不意味着你学完这门课程就要做架构师。我想,在座的同学在现在、将来或者至少过去曾经是一名程序员,所以在结束语中,我想来跟你聊一点儿与技术相关,但又不局限于具体技术的话题。
程序员的发展观
程序员通俗地说就是写程序代码的人,但在不少人的认知里,今天去写代码,却是为了日后可以不必再写代码。
从职业经理人的视角来看,不管是架构师、资深专家,还是研发部门管理者,这些程序员的“进阶职业”似乎都已经脱离了字面意义上的“写代码的人”,衡量他们工作目标的依据主要是治下的程序员是否有更高的工作效率、更好的投入产出。那么如此一来,不少程序员想成为“不必再写代码”的人,倒是也可以理解。
不过从技术人员的视角来看程序员这个群体天生就带有一种工匠式的图腾崇拜精神大家都奉行达者为师并不迷信管理自己的人但尊重能够指导自己的人爱讲逻辑、爱讲道理讲不通至少还能“Talk is cheap, show me the code”。而如此一来要脱离技术去管理好一群程序员可是相当困难的。
其实,我之所以说这些,是希望以后无论你的职业目标是永远做一名程序员,还是架构师,或者是成为一名研发管理者,都不要轻易地离开技术领域的一线。
离开技术、放弃编码的决定,很可能会像你高考之后放下的数学、生物、地理等知识那样,一旦放手,以后就很难有机会再重新捡起来。
久而久之,你对代码、技术、产品状态与团队研发状态的理解,就会渐渐和团队成员产生偏差错位,从而丧失在细节上给予指导的能力,丧失在专业问题上提出接地气解决方案的能力,只能在短期内无法验证对错的大战略方向上提意见,在会议、流程及团队管理措施上下功夫,在职业经理人式的宣讲与汇报上寻找存在感。
如果是这样,那么你就从团队的导师变成了管理者,最后你跟团队的关系,就会从携手并肩奋斗的伙伴,完全演变成了只能靠公司制度与管理职位的权力来维系的雇佣关系。
当然我也相信,假如能够轻松地做好技术,也没有人愿意随便放弃。我听过的离开技术一线最常见的原因,就是“年纪大了,时间不够用了”或者要“聚焦精力去做管理了”。对这种现象,我的看法是:确实很难轻松地做好技术,但是在做好技术工作的前提下,却有可能比较轻松地做好架构和管理工作。
我自己也是一名架构师和管理者,在作自我介绍的场合,用的头衔却从来都是“兼职一些管理工作的程序员”,这是一种人设标签。如果你问我,为什么管理几十人、几百人的团队的同时,还能抽出时间去编码、去写作、去关注具体的细节与技术的潮流发展,我会理所当然地回答,“因为我是一名程序员”啊。
这句话的第一层意思是,我是程序员,去编码是天经地义的。另一层意思是,我是程序员,与一群最讲道理、最直来直往、最不需要琢磨小心思的程序员协同工作,管理才不需要耗费太多的精力,所以“兼职管理”才是可行的。
程序员的价值观
聊完编程与程序员的发展观,我们再来探讨两个关于程序员价值观方面的问题:
在工作中所需要的知识技能,自己并不感兴趣,该怎么办?
在工作中接触不到的知识技能,有没有必要专门去了解、学习,乃至刻意锻炼?
我们知道,工作的职责能跟自己感兴趣的方向一致、能跟自己知识体系的缺失形成互补,这样的机会是可遇不可求的。今天的软件业已经高度成熟了,分工日益细致,对于大多数人来说,聚焦在少数几个点上拧螺丝是常态,能够在广袤的舞台上造火箭才是特例。
所以,前面两个问题不一定是每位同学都认真思考过,但我相信它应该是每位程序员都实际遇到过的。比如,有位同学就在课程开篇词中提了一个问题,不知在你的职业生涯中的某个时刻,是不是也有过相似的感受:
周老师,想了解一下你之前是怎样从业务往架构转型的?我是工作两年的小白,一直都很想学习架构方面的课程,但是由于工作全是业务逻辑,而且是极其复杂繁琐的业务,每天都是对着协议研究业务实现,感觉自己都困在业务里面无法自拔。
人生苦短,光阴易逝,把有限的时间和精力投入到对自己最有价值的方向上显得尤为关键,大多数人都能接受“选择永远比努力更重要”的观点,但进一步问“什么才是好的选择”时,就只有少数人能对自己学习的知识技能、从事的工作方向做出定量的价值判断。
所以,这里我就以这位同学的问题为例,拿出自己的判断模型,供你参考:
价值 = (技能收益 + 知识收益) × 提升空间 / 投入成本
技能收益
刚刚的问题里提到的“每天都是对着协议研究业务实现”,就属于典型的技能,它往往代表着直接收益。
我认为,一项工作中每天都要用到的技能,不管你是否感兴趣,都值得花一些时间精力去掌握,因为它至少是对你短期的利益起到了明确的支撑作用;反之,永远都不会派上用场的屠龙术,再高大上也是水月镜花。
所以,正视技能收益的意义就在于可以避免自己变得过度浮躁,而不是用“兴趣不合”“发展不符”之类的借口去过度挑剔。
我也提倡兴趣驱动,提倡快乐工作,但不设前提条件的兴趣驱动就未免太过“凡尔赛”了。首先在社会中务实地生存,不涉及是否快乐,先把本分工作做对做好,再追求兴趣选择和机遇发展,这才是对多数人的最大的公平。
知识收益
问题中提到的“架构方面的课程”有不少都属于知识。知识的收益往往是间接的它最终会体现在缩减了模型中的“投入成本”因素即降低认知负荷Cognitive Load上。世界上鲜有“烟囱式”的专业人才专才的知识体系基本还是“金字塔式”的这些人在领域里能够显著超过他人高度的前提条件往往就是他们拥有超过他人的知识广度。
而具体到软件开发中,像计算机体系结构、编译原理、操作系统等原理性的知识,对于不写编译器、不开发操作系统的程序员来说,在实践中是几乎找不到直接的应用场景的。可是毫无疑问,这些知识就是程序员知识体系的基石,是许多实用技能和常见工具溯源的归宿。
我们花费一定的成本去学习这类知识,目的是要把自己的知识点筑成体系,把大量的、不同的、零散的知识点,通过内化、存储、整理、归档、输出等方式组合起来,以点成线、以线成面,最终形成系统的、有序的、清晰的脉络结构,这就是知识体系。
程序员是需要终身学习的群体,当有新的信息输入时,如果能在知识体系中快速找到它应该安放的位置,定位它的问题与解题空间,找到它与其他知识点的关联关系,那你接受新信息的认知负荷就降低了。通俗地讲,你就有了比别人更高的学习效率,更敏锐的技术触觉。
提升空间
如果一项工作对你来说是个全新的领域,甚至能称为是一项挑战,那风险的背后往往也蕴藏着更高的收益。但我把提升空间归入到价值判断的因素之中,更重要的目的是规避舒适区的陷阱。
人性会在持续的颓废时发出示警,却也容易被无效的努力所欺骗。我们去做一些已经完全得心应手的事情时,自然不会耗费什么精力,也不会觉得痛苦困难,如果把它当作打游戏看电影般的娱乐消遣,放松自己是合适的,但我们不应该再指望从中追求什么价值。
而没有价值,是因为提升空间已经下降到零了,可我们要注意,其中的投入成本根本不可能为零,因为成本中不仅包括精力,还包括时间。花时间重复去做已经完全熟练的事情,相当于计算分子为零的算式,结果自然是没有价值的。
投入成本
在这门架构课程中,我经常讲的一个词是“权衡”,经常说的一句话是“凡事不能只讲收益不谈成本”。在我的价值模型里,收益大小也是必须在确定的成本下,才有衡量比较的意义。这里的成本,既包括你花费的时间、金钱与机会,也包括你投入的知识、精神与毅力。
强调投入成本,是希望你不要去钻牛角尖。如果一项知识或技能,你学习起来非常吃力,花费大力气弄懂之后,过一段时间却又迅速地忘掉了,这很可能是因为你既没有实际应用它的场景,也没有在知识体系中建立好掌握它的稳固的前置基础。这种就属于你目前还拿不动的东西,不如趁早放手,先做好减法,才能做好加法;你也不必觉得可惜,如果它对你来说是必要的,就一定还会再次出现,想躲也躲不掉。
好了,这就是我的价值判断模型,每个人都应该有属于自己的价值观,你可以参考,但不必非得跟谁的一致。我也并不是提倡凡事都要把价值判断当成公式一样去计算,而是希望你能养成一种类似的思维习惯。
将思考具象化
前面我谈论的是发展观、价值观这种大方向的话题,最后,我想以一个具体可操作的小话题来结束这篇结束语:程序员应该如何构筑自己知识体系?顺便我也跟你解释一下,为何这门课程会是一门公开课。
我践行的知识整理方法是“将思考具象化”。因为我们知道,思考这件事是外界不可知的,其过程如何、其结果如何只有自己心里才清楚。如果不把自己思考的内容输出给他人,很容易就会被自己所欺骗,误以为自己已经理解得足够完备了。
在开篇词中,我提到过做这门课程的目的:做技术不仅要去看、去读、去想、去用,更要去写、去说。把自己“认为掌握了”的知识给叙述出来,能够说得条理清晰,讲得理直气壮;能够让别人听得明白,释去心中疑惑;能够把自己的观点交给别人的审视,乃至质疑。在这个过程中,就会挖掘出很多潜藏在“已知”背后的“未知”。
这个目的也是它成为免费公开课的原因:课程本身就是我对自己知识体系整理的成果,是我思考的具象化表现,在这件事情中,我自己是最大的受益者,而其后所做的极客时间课程,以及出版的纸质书籍,都可以算是额外的收获。这样看来,经济上的回报也就不那么重要了。
实际上,在这门架构课里,我不仅在探讨架构的知识与技术,也很希望能够把自己如何思考、整理、构筑知识体系的方法展示出来。之前的用户故事中,詹同学把它总结为“用输出来倒逼输入”,我看了心中觉得颇感知音。在此,一并感谢每位同学的支持与同行。
最后我想说的是,课程结束并非终点,我们还可以在留言区互动交流,也祝你享受成长,学有所成。另外,我准备了一份毕业问卷,希望你能花两三分钟填写一下,我也非常期待听到你对这门课的反馈。
就到这里,我们再会!

View File

@@ -0,0 +1,23 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结课测试 _ 一套习题,测出你的掌握程度
你好,我是周志明。
咱们课程到这里已经正式更新完了,感谢你一直以来的认真学习和支持。在临近告别前,我给你准备了一个结课小测试,来帮你检验自己的学习效果。
这套测试题一共有5道单选题和5道多选题满分100分核心考点都出自课程里讲到的知识点希望可以帮助你进行一场自测。
除此之外,我也很想知道你对这门课的建议,这里我给你准备了一份问卷。欢迎你在问卷里聊一聊你的想法,也许有机会获得礼物或者是课程阅码哦。
好了,话不多说,点击下面的按钮开始测试吧!