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,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 “附身”大厂架构师,身临其境设计高并发系统
你好我是李智慧目前担任同程旅行交通首席架构师。我曾在阿里巴巴和英特尔担任架构师主要从事高并发系统架构和大数据相关产品的开发。我参与过alibaba.com和Apache Spark的架构设计与开发也曾作为CTO领导团队经历了日订单从零到一百万的高并发技术挑战。
说来我也是你的老朋友了因为我在极客时间已经开过两门专栏课程了分别是《从0开始学大数据》和《后端技术面试38讲》。
在前两个专栏中,我时不时就能看到同学们对“高并发架构”的学习需求。确实,“高并发”是系统架构设计的核心关键词,也是很多大厂的关注焦点。面试大厂的时候,你要是对高并发架构说不出个一二三,恐怕面试也凶多吉少了。
其实,我们都知道高并发的重要性了,甚至是看过了不少高并发系统设计的技术资料,但还是会有困惑:学了形形色色的高并发技术以后,为什么我还是对设计一个完整的高并发系统没有概念?
在我看来,这种困惑并不是源于对知识的理解不足或掌握不够,而是源于缺乏架构现场的感受:不能把自己设身处地地放在一个需要真实构建系统的环境中。没有那种面对真实场景的压力、挑战,也没有完成任务后的喜悦与轻松,学到的各种技术就无法基于这种真实的现场感而融汇成自己的观点,最后还是一些零碎的知识。
正所谓:听过很多道理,依然过不好这一生。说到底,还是因为缺乏实践。
当然了,大厂能为我们提供高并发系统架构设计的实践机会。但若你没有高并发架构设计经验,大厂的大门又怎肯为你打开呢?这就形成了一个死循环。
难道我们就只能一直陷在这个困局里出不去吗?事实上,我们自己也在刷抖音、玩微博、用搜索引擎,我们自己就是这些高并发应用的用户。我们只需要从用户角色转换到设计角色,把自己想象成这些大厂的架构师,设身处地去思考:如果让我来设计这个系统,我将如何开展我的工作?
通过这个专栏,你可以“附身”大厂架构师,借助我的经验和知识,身临其境地,站在大厂架构师的视角,理解高并发系统的设计思路。
我如何帮你获得高并发系统设计的“现场感”?
为了让你更有身临其境的感受,我为你找到了三条途径。
足够真实的高并发系统设计场景
如果现阶段你还不能进入大厂真正去实践,那么为自己创造一个相对真实的设计场景,进行不断的模拟练习,会比单纯学习理论的效果更好。
这个专栏的所有案例都是基于真实场景的,甚至有些案例本身就是由真实设计文档改编的。只有这些还不够,每节课设计的系统都有自己的名字。一位先哲说过,当一个东西有了自己的名字,就拥有了生命。
希望你在阅读专栏的过程中,能把自己带入到真实的系统设计场景中,把文章当成真实的设计文档,把自己想象成设计文档的作者(也就是我)的同事,你正在评审我做的设计。
你可以一边阅读一边思考:这个设计哪些地方考虑不周,哪些关键点有缺漏。然后你可以把自己的思考写在评论区,当做你的评审意见。最重要的是,通过这种方式,你拥有了和我一样的关于每一个软件设计案例的现场感:你不是一个阅读专栏的读者,而是置身于互联网大厂的资深架构师,你在评审同事的设计,也在考虑公司的未来。
如果你在阅读这些案例设计文档的时候,能够对设计整体有个评价,对具体细节能给出若干改进意见,那么你和大厂架构师的距离也就不远了。
贴合工作场景的设计文档形式
你可能会觉得,设计文档和自己关系不大,一是平时不怎么写,也不愿意写文档,觉得写文档价值不大;二是自己不擅长写文档,觉得写也写不好,甚至不太知道设计文档该怎么写。
但工作了这么多年,我发现,写东西可以帮助人更好地思考。技术人员如果不写设计文档,就会缺少对技术更深刻的思考,对技术方案的优点和缺点就缺乏系统的认识,也就不知道如何找到更好的技术和更合理的方案。很显然,这会阻滞技术人员自身的职业发展。
不仅如此,如果没有系统设计文档,缺乏技术的深度思考,那么开发出来的软件就缺乏创新,显得平庸,公司的技术产品在市场上就缺少竞争力。所以长远看来,不写系统设计文档,看起来忙忙碌碌,但这样对公司、对自己的发展都不利。
可以粗暴一点地说:没有设计文档就没有设计,没有设计就没有技术的进步。
所以,这个专栏我将以软件设计文档的形式去写一系列软件的系统架构设计,这些设计文档的风格是相对统一的。我希望你可以在这些“重复”的设计文档组织方式、软件建模与架构方式中,学习到一般的软件设计方法和软件设计文档的写作方法。
求同存异的典型系统架构案例
我精挑细选了10余个系统架构案例这些案例大多是目前大家比较关注的高并发、高性能、高可用系统。比如网盘、搜索引擎、短视频应用、打车软件、交友软件、微博等。它们是高并发架构设计的优秀“课代表”它们的技术可以解决现有的80%以上的高并发共性问题。所以在阅读这些文档的过程中,你可以进一步学习、借鉴这些典型的分布式互联网系统架构,了解现在高并发技术的热点。
为了避免每篇文档中都出现大量重复、雷同的设计,我在内容方面也进行了取舍,精简了一些常规的、技术含量较低的内容,而尽量多地介绍那些有独特设计思想的技术点。尽可能做到在遵循设计文档规范的同时,又突出每个系统自己的设计重点。
此外,专栏中还有一部分设计是针对这些大型应用系统的重要组成部分的,比如限流器、防火墙、加解密服务、大数据平台等。
但我需要强调一点,专栏会针对这些知名的大厂应用重新进行设计,而不是分析现有这些应用是如何设计的。一方面,重新设计完全可以按自己的意愿来,不管是设计方案还是需求估算,都是一件很爽的事;另一方面,因为现有应用中的某些关键设计并没有公开,我们要想讨论清楚这些高并发应用的架构设计,没有现成的资料,就需要自己进行分析并设计。这有点像流行的穿越剧:我如果能穿越回去,我将如何设计这些大厂的系统?只要我愿意,我也可以成为大神。
所以,在设计文档中,很多案例都有需求估算部分,来分析我们重新设计的系统需要承载的并发压力有多大、系统资源需要多少,这些估算大多数都略高于现有大厂系统的指标。希望你在阅读这些估算内容的时候,能够更具体地体会到架构师的“现场感受”:我评审、设计的这个系统将服务全球数十亿用户;这个系统每年需要的服务器和网络带宽需要几十亿资金;这个系统宕机十几分钟,公司就会损失数千万人民币。
此时此刻,你可能还是会有很多疑惑:
这些高并发系统架构中,有哪些常见的架构模式?
各种常见的分布式技术,是如何应用到系统架构中的?
不同的应用场景,又有怎样独特的技术挑战,以及是如何应对的?
了解这些,对自己的架构设计能力提升有哪些帮助?
……
不必着急,通过这个专栏,你的问题都会被一一解答。而且,我相信,这一系列的架构设计实战文档会给你更多不一样的启迪。
这门课有哪些内容?
这个专栏共计22篇内容其中包括17篇设计案例实战文档还有5篇是关于软件设计方法高并发、高性能、高可用系统架构的一些基础知识方便你对设计案例中涉及到的技术进行一些回顾。
我们常说高并发、高性能、高可用,事实上,这三者并不是平行的关系。通常情况下,高并发是根源和核心。正是因为高并发,大量的用户同时请求我们的系统,导致系统资源快速消耗,服务器无法及时处理用户请求,响应变慢,系统出现性能问题。更进一步,性能继续恶化,导致服务器资源耗尽,就会出现系统崩溃,可用性也出现问题。
根据高并发系统的特点我把这个专栏划分成了5个实战模块。
实战模块一:高并发系统的海量数据处理架构案例
我们将主要讨论高并发处理海量数据的场景,包括海量的数据如何存储、如何传输、如何进行并发控制。
在这个模块中你可以看到一些看似相同的需求其实可以有完全不同的解决方案比如海量的短视频和海量的网盘存储还有一些看似非常不同的场景其实可以用同一个技术搞定比如短URL和短视频。
实战模块二:高并发系统的高性能架构案例
我们将主要讨论在高并发场景下,如何保证系统的响应性能。
在这个模块中,你会看到,在海量的网页中快速搜索到一些网页,和在海量的人群中快速寻找一些人,其技术挑战是如何的不同,其解决方案又分别是如何的巧妙。
实战模块三:高并发系统的高可用架构案例
高并发导致系统的崩溃,最经典的案例莫过于明星半夜宣布离婚导致的微博宕机。为什么明星离婚会导致微博崩溃?拥有数千万关注的明星,微博消息是如何推送给粉丝的?微博如何处理这种热点新闻的海量消息转发所引起的系统压力?
实战模块四:安全系统架构案例
系统安全也是高并发系统的一个重要挑战。恶意的用户请求如何处理?敏感的数据如何加密解密?这里的几个案例都来自真实的应用。如果你需要,你可以将这几个设计直接落地,开发、应用到你的工作中。
实战模块五:网约车架构案例专题
在这个模块里我们将深入讨论如何设计一个数亿用户、千万日订单的高并发打车软件。面对业务迭代如何利用DDD对系统微服务进行重构设计。还有如何利用大数据技术实现大数据杀熟然而并不可以
最后,最重要的,就是希望你能把自己想象成大厂架构师,设身处地地思考,每一个案例都要产生自己的意见和看法,并表达出来。
期望你能在这个专栏学习结束后,自己挑选几个大厂的应用案例,按照专栏文章的设计模板,自己完成这些应用的架构设计。做到这一点,你就可以说对高并发架构登堂入室了,对自己的架构能力也建立起信心了。
祝你学习顺利,成为一名实战能力强、能够主导公司技术核心的架构师。

View File

@ -0,0 +1,161 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 软件建模与文档:架构师怎样绘制系统架构蓝图?
你好,我是李智慧。
我在[开篇词]中说过,我们课程中的一系列软件架构设计是用设计文档的形式呈现的。所以,在拆解一个个案例之前,我们先来了解一些关于软件设计文档的基础知识,这样你在学习后面的具体案例时,就能更加清楚地理解文档是基于什么方式来组织的了。
首先,请你设想这样一个场景:如果公司安排你做架构师,要你在项目开发前期进行软件架构设计,你该如何开展你的工作?如何输出你的工作成果?如何确定你的设计是否满足用户需求?你是否有把握最后交付的软件是满足要求的?是否有把握让团队每个工程师清楚自己的职责范围并有效地完成开发工作……
这些问题其实都是软件开发管理与技术架构的核心诉求,而架构师的核心工作就是做好软件设计,解决这些诉求。这些问题搞定了,软件的开发过程和结果也就都得到了保证。那怎么实现这些诉求呢?我们主要的手段就是软件建模,以及将这些软件模型组织成一篇有价值的软件设计文档。
软件建模
所谓软件建模,就是为要开发的软件建造模型。
模型是对客观存在的抽象,例如著名的物理学公式\(E=mc^{2}\),就是质量能量转换的物理规律的数学模型。除了物理学公式以外,还有一些东西也是模型,比如地图是对地理空间的建模;机械装置、电子电路、建筑设计的各种图纸是对物理实体的建模。而软件,也可以通过各种图进行建模。
软件系统庞大复杂,通过软件建模,我们可以抽象软件系统的主要特征和组成部分,梳理这些关键组成部分的关系。在软件开发过程中依照模型的约束开发,系统整体的格局和关系就会可控。相关人员从始至终都能清晰了解软件的蓝图和当前的进展,不同的开发工程师会清晰自己开发的模块和其他同事工作内容的关系与依赖,并按照这些模型开发代码。
那么我们是根据什么进行软件建模的呢?要解答这个疑问,你需要先知道,在软件开发中,有两个客观存在。
一个是我们要解决的领域问题。比如我们要开发一个电子商务网站,那么客观的领域问题就是如何做生意,卖家如何管理商品、管理订单、服务用户,买家如何挑选商品,如何下订单,如何支付等等。对这些客观领域问题的抽象就是各种功能及其关系、各种模型对象及其关系、各种业务处理流程。
另一个客观存在就是最终开发出来的软件系统。软件系统要解决的问题包括软件由哪些主要类组成,这些类如何组织构成一个个的组件,这些类和组件之间的依赖关系如何,运行期如何调用,需要部署多少台服务器,服务器之间如何通信等。
而对这两个客观存在进行抽象化处理的手段,就是我们的软件模型。
一方面我们要对领域问题和要设计的软件系统进行分析、设计、抽象,另一方面,我们根据抽象出来的模型进行开发,最终实现出一个软件系统,这就是软件开发的主要过程。而对领域问题和软件系统进行分析、设计和抽象的这个过程,就是软件建模设计。
软件设计方法
因此,软件设计其实就是软件建模的过程。我们通过软件建模工具,将软件模型画出来,实现软件设计。
在实践中通常用来进行软件建模画图的工具是UML统一建模语言。UML包含的软件模型有10种其中常用的有7种类图、序列图、组件图、部署图、用例图、状态图和活动图。
下面我们简单了解下这7种常用UML图的使用场景和基本样例。在专栏后面的设计文档中你会多次见到它们看多了你就懂了也就自然会画了。当然如果你想更详细地学习UML知识我也非常鼓励并且推荐你阅读马丁富勒的《UML精粹》一书。
类图
类图是最常见的UML图形用来描述类的特性和类之间的静态关系。
一个类包含三个部分类的名字、类的属性列表和类的方法列表。类之间有6种静态关系关联、依赖、组合、聚合、继承、泛化。把相关的一组类及其关系用一张图画出来就是类图。
比如你在后面的课程中会遇到下面这幅图,它就是类图。你可以把我上面说的类图包含元素和图片一一对照,感受类图的用法。
时序图
类图之外,另一种常用的图是时序图,类图描述类之间的静态关系,时序图则用来描述参与者之间的动态调用关系。
从图中可以看出,每个参与者有一条垂直向下的生命线。而参与者之间的消息从上到下表示其调用的前后顺序关系,这正是“时序图”这个词的由来。每个生命线都有若干个激活条,也就是那些细长的矩形条,只要这个条出现,就表示参与者是激活状态的。
时序图通常用于表示参与者之间的交互,这个参与者可以是类对象,也可以是更大粒度的参与者,比如组件、服务器、子系统等。总之,只要是描述不同参与者之间交互的,都可以使用时序图。
组件图
组件是比类粒度更大的设计元素一个组件中通常包含很多个类。组件图有的时候和包图的用途比较接近组件图通常用来描述物理上的组件比如一个JAR、一个DLL等等。在实践中我们进行模块设计的时候用得更多的就是组件图。
组件图描述组件之间的静态关系,主要是依赖关系,如果你想要描述组件之间的动态调用关系,可以使用组件时序图,以组件作为参与者,描述组件之间的消息调用关系。
部署图
部署图描述软件系统的最终部署情况,比如需要部署多少服务器,关键组件都部署在哪些服务器上。
部署图是软件系统最终物理呈现的蓝图,根据部署图,所有相关者,诸如客户、老板、工程师都能清晰地了解到最终运行的系统在物理上是什么样子,和现有的系统服务器的关系,和第三方服务器的关系。根据部署图,还可以估算服务器和第三方软件的采购成本。
因此部署图是整个软件设计模型中,比较宏观的一种图,是在设计早期就需要画的一种模型图。根据部署图,各方可以讨论对这个方案是否认可。只有对部署图达成共识,才能继续后面的细节设计。
用例图
用例图通过反映用户和软件系统的交互,描述系统的功能需求。
图中小人形象的元素,被称为角色,角色可以是人,也可以是其他的系统。系统的功能可能会很复杂,所以一张用例图可能只包含其中一小部分功能,这些功能被一个矩形框框起来,这个矩形框被称为用例的边界。框里的椭圆表示一个一个的功能,功能之间可以调用依赖,也可以进行功能扩展。
状态图
状态图用来展示单个对象生命周期的状态变迁。
业务系统中,很多重要的领域对象都有比较复杂的状态变迁,比如账号,有创建状态、激活状态、冻结状态、欠费状态等等各种状态。此外,用户、订单、商品、红包这些常见的领域模型都有多种状态。
这些状态的变迁描述可以在用例图中用文字描述,随着角色的各种操作而改变,但是用这种方式描述,状态散乱在各处,不要说开发的时候容易搞错,就是产品经理自己在设计的时候,也容易搞错对象的状态变迁。
UML的状态图可以很好地解决这一问题一张状态图描述一个对象生命周期的各种状态及其变迁的关系。如图所示门的状态有开Opened、关Closed和锁Locked三种状态与变迁关系用一张状态图就可以搞定。
活动图
活动图主要用来描述过程逻辑和业务流程。UML中没有流程图很多时候人们用活动图代替流程图。
活动图和早期流程图的图形元素也很接近,实心圆代表流程开始,空心圆代表流程结束,圆角矩形表示活动,菱形表示分支判断。
此外,活动图引入了一个重要的概念——泳道。活动图可以根据活动的范围,将活动根据领域、系统和角色等划分到不同的泳道中,使流程边界更加清晰。
我们上面介绍了UML建模常用的7种模型那么这7种模型分别应用在软件设计的什么阶段用来表达什么样的设计意图呢
软件设计文档
软件设计文档就是架构师的主要工作成果,它需要阐释这节课开头提到的各种诉求,描绘软件的完整蓝图,而软件设计文档的主要组成部分就是软件模型。
软件设计过程可以拆分成需求分析、概要设计和详细设计三个阶段。
在需求分析阶段,主要是通过用例图来描述系统的功能与使用场景;对于关键的业务流程,可以通过活动图描述;如果在需求阶段就提出要和现有的某些子系统整合,那么可以通过时序图描述新系统和原来的子系统的调用关系;可以通过简化的类图进行领域模型抽象,并描述核心领域对象之间的关系;如果某些对象内部会有复杂的状态变化,比如用户、订单这些,可以用状态图进行描述。
在概要设计阶段,通过部署图描述系统最终的物理蓝图;通过组件图以及组件时序图设计软件主要模块及其关系;还可以通过组件活动图描述组件间的流程逻辑。
在详细设计阶段,主要输出的就是类图和类的时序图,指导最终的代码开发,如果某个类方法内部有比较复杂的逻辑,那么可以将这个方法的逻辑用活动图进行描述。
我们在每个设计阶段使用几种UML模型对领域或者系统进行建模然后将这些模型配上必要的文字说明写入到文档中就可以构成一篇软件设计文档了。
我们专栏中的十几讲软件设计案例,都是按照这样的方式组织的,你可以在学习的过程中,一方面了解各种系统软件是如何设计的,一方面也可以借鉴设计文档是如何写作的。
同时也要说明一下,设计文档的写法并没有一定之规,最重要的是这个文档能否向阅读者传递出架构师完整的设计意图。而不同的阅读者关注点是不同的,老板、客户、运维、测试、开发这些角色都是设计文档的阅读者,他们想要看到的东西显然是不一样的。
客户和测试人员可能更关注功能性需求和实现逻辑,老板和运维人员可能更关注非功能需求和整体架构,而开发人员可能更关注整体架构与关键技术细节。
我们专栏的案例基本上是以开发人员作为阅读视角进行编写的,你在阅读这些案例时,会明显感觉到我的表达方式和其他专栏文章不太一样,措辞会更“坚硬”一点,文字和读者的距离也有点“疏离”,而这正是设计文档自身的特质。
架构、系统,文档、相关人员之间的关系可以参考下面这张图。
每个软件系统都需要有一个架构,每个架构都包含若干架构元素。架构元素就是前面提到的服务器、组件、类、消息、用例、状态等等。这些元素之间的关系是什么?如何把它们组织在一起?我们可以用部署图、组件图、时序图等各种模型图来描述。
架构最终需要一个文档来承载,把这些模型图放进这个文档,再配以适当的文字说明,就是一篇架构设计文档。而设计文档是给人阅读的,这些人就是系统的相关方。不同的相关方关注点不同,也需要由不同的模型图来进行表达,所以架构师应该针对不同的相关方,使用不同的模型图输出不同的架构文档。
上面这张图是关于架构的架构图,也就是说,是关于软件模型的模型。我们下一篇会讲高并发系统的方法论,而方法论是关于方法的方法。
小结
软件设计就是在软件开发之前,对要解决的业务问题和对要实现的软件系统进行思考,并将这个思考的结果通过软件模型表达出来的过程。
人类作为万物之灵,最大的特点就是,在行动之前就已经在头脑中将行动的过程和行动的结果构建成了一个蓝图,然后将这个蓝图付诸实践。我们的祖先将第一块石头打磨成石器的时候,就已经拥有了这种能力。软件系统的开发是一个复杂的智力活动,参与其中的我们更需要拥有构建蓝图并付诸实践的能力。
目前有个很火的词叫“元宇宙”,“元”通俗地讲,就是一切开始的地方,是关于如何用自己描述自己,是抽象之上的抽象。这种“元”能力对架构师而言,非常重要。架构师只有掌握各种技术背后的技术,了解各种问题背后的问题,才能超越当下的种种羁绊,设计出面向未来的架构。
思考题
假设时光倒流你回到2003年成为淘宝的架构师你将如何设计淘宝的系统架构请使用在线绘图工具www.draw.io 或者你熟悉的其他UML建模工具绘制满足淘宝早期应用场景的用例图以及部署图。如果你有什么灵感或者遇到什么问题欢迎在评论区留言我们一起探讨。
也欢迎把这节课分享给更多对高并发架构设计感兴趣的朋友,我们共同进步。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 高并发架构设计方法:面对高并发,怎么对症下药?
你好,我是李智慧。
我们知道,“高并发”是现在系统架构设计的核心关键词。一个架构师如果设计、开发的系统不支持高并发,那简直不好意思跟同行讨论。但事实上,在架构设计领域,高并发的历史非常短暂,这一架构特性是随着互联网,特别是移动互联网的发展才逐渐变得重要起来的。
现在有很多大型互联网应用系统,其用户是面向全球的普通大众,用户体量动辄十几亿。这些用户即使只有万分之一同时访问系统,也会产生十几万的并发访问量。
因此高并发是现在的大型互联网系统必须面对的挑战当同时访问系统的用户不断增加时要消耗的系统计算资源也不断增加。所以系统需要更多的CPU和内存去处理用户的计算请求需要更多的网络带宽去传输用户的数据也需要更多的硬盘空间去存储用户的数据。而当消耗的资源超过了服务器资源极限的时候服务器就会崩溃整个系统将无法正常使用。
今天我将基于高并发系统的技术挑战,来为你介绍典型的分布式解决方案。这节课的内容,会被应用到后面的大部分实战案例中。所以我希望通过这节课,带你做个简单的预习,同时你也能对自己学过的高并发技术做个简单回顾。
我要先说明一点,今天的高并发系统架构方法比较多,但它们是殊途同归的,都要遵循一个相同的高并发应对思路。所以我们今天的首要目标就是明确这个思路到底是什么,也就是要搞清楚高并发系统架构的方法论。
高并发系统架构的方法论
高并发的技术挑战,核心就是为了满足用户的高并发访问,系统需要提供更多的计算资源。那么如何提供这些计算资源,也就是说,如何使系统的计算资源随着并发的增加而增加?
对此人们提出各种技术解决方案这些解决方案大致可以分成两类一类是传统大型软件系统的技术方案被称作垂直伸缩方案。所谓的垂直伸缩就是提升单台服务器的处理能力比如用更快频率的CPU、更多核的CPU、更大的内存、更快的网卡、更多的磁盘组成一台服务器从普通服务器升级到小型机从小型机提升到中型机从中型机提升到大型机从而使单台服务器的处理能力得到提升。通过这种手段提升系统的处理能力。
当业务增长,用户增多,服务器计算能力无法满足要求的时候,就会用更强大的计算机。计算机越强大,处理能力越强大,当然价格也越昂贵,技术越复杂,运维越困难。
由于垂直伸缩固有的这些问题,人们又提出另一类解决方案,被称作水平伸缩方案。所谓的水平伸缩,指的是不去提升单机的处理能力,不使用更昂贵更快更厉害的硬件,而是使用更多的服务器,将这些服务器构成一个分布式集群,通过这个集群,对外统一提供服务,以此来提高系统整体的处理能力。
水平伸缩除了可以解决垂直伸缩的各种问题,还有一个天然的好处,那就是随着系统并发的增加,可以一台服务器一台服务器地添加资源,也就是说,具有更好的弹性。而这种弹性是大多数互联网应用场景所必须的。因为我们很难正确估计一个互联网应用系统究竟会有多少用户来访问,以及这些用户会在什么时候来访问。而水平伸缩的弹性可以保证不管有多少用户,不管用户什么时候来访问,只要随时添加服务器就可以了。
因此现在的大型互联网系统多采取水平伸缩方案,来应对用户的高并发访问。
高并发系统架构的方法
我们知道了分布式集群优势明显,但是将一堆服务器放在一起,用网线连起来,并不能天然地使它们构成一个系统。要想让很多台服务器构成一个整体,就需要在架构上进行设计,使用各种技术,让这些服务器成为整体系统的一个部分,将这些服务器有效地组织起来,统一提升系统的处理能力。
这些相关的技术就是高并发系统架构的主要技术方法,其核心是各种分布式技术。
分布式应用
应用服务器是处理用户请求的主要服务器工程师开发的代码就部署在这些服务器上。在系统运行期间每个用户请求都需要分配一个线程去处理而每个线程又需要占用一定的CPU和内存资源。所以当高并发的用户请求到达的时候应用服务器需要创建大量线程消耗大量计算机资源当这些资源不足的时候系统就会崩溃。
解决这个问题的主要手段就是使用负载均衡服务器,将多台应用服务器构成一个分布式集群,用户请求首先到达负载均衡服务器,然后由负载均衡服务器将请求分发到不同的应用服务器上。当高并发的用户请求到达时,请求将被分摊到不同的服务器上。这样一来,每台服务器创建的线程都不会太多,占用的资源也在合理范围内,系统就会保持正常运行。
通过负载均衡服务器构建分布式应用集群如下图。
分布式缓存
系统在运行期需要获取很多数据,而这些数据主要存储在数据库中,如果每次获取数据都要到数据库访问,会给数据库造成极大的负载压力。同时数据库的数据存储在硬盘中,每次查询数据都要进行多次硬盘访问,性能也比较差。
目前常用的解决办法就是使用缓存。我们可以将数据缓存起来,每次访问数据的时候先从缓存中读取,如果缓存中没有需要的数据,才去数据库中查找。这样可以极大降低数据库的负载压力,也有效提高了获取数据的速度。同样,缓存可以通过将多台服务器够构成一个分布式集群,提升数据处理能力,如下图。
首先应用程序调用分布式缓存的客户端SDKSDK会根据应用程序传入的key进行路由选择从分布式缓存集群中选择一台缓存服务器进行访问。如果分布式缓存中不存在要访问的数据应用程序就直接访问数据库从数据库中获取数据然后将该数据写入到缓存中。这样下次再需要访问该数据的时候就可以直接从缓存中得到了。
分布式消息队列
分布式消息队列是解决突发的高并发写操作问题和实现更简单的集群伸缩的一种常用技术方案。消息队列架构主要包含三个角色:消息生产者、消息队列、消息消费者,如下图。
比如我们要写数据库,可以直接由应用程序写入数据库,但是如果有突发的高并发写入请求,就会导致数据库瞬间负载压力过大,响应超时甚至数据库崩溃。
但是如果我们使用消息队列,应用程序(消息生产者)就可以将写数据库的操作,写入到消息队列中,然后由消息消费者服务器从消息队列中消费消息,根据取出来的消息将数据写入到数据库中。当有突发的高并发写入的时候,只要控制消息消费者的消费速度,就可以保证数据库的负载压力不会太大。
同时,由于消息生产者和消息消费者没有调用耦合,当我们需要增强系统的处理能力,只需要增加消息生产者或者消息消费者服务器就可以了,不需要改动任何代码,实现伸缩更加简单。
分布式关系数据库
关系数据库本身并不支持伸缩性,但是关系数据库又是存储数据最传统的手段。为了解决关系数据库存储海量数据以及提供高并发读写的问题,人们提出了将数据进行分片,再将不同分片写入到不同数据库服务器的方法。
通过这种方法,我们可以将多台服务器构建成一个分布式的关系数据库集群,从而实现数据库的伸缩性,如下图。
分布式微服务
我们前面提到的分布式应用,是在一个应用程序内部完成大部分的业务逻辑处理,然后将这个应用程序部署到一个分布式服务器集群中对外提供服务,这种架构方案被称作单体架构。与此相对应的是分布式微服务架构,这是一种目前更广为使用的架构方案,如下图。
微服务的核心思想是将单体架构中庞大的业务逻辑拆分成一些更小、更低耦合的服务,然后通过服务间的调用完成业务的处理。
具体处理过程是:用户请求通过负载均衡服务器分发给一个微服务网关集群,在网关内开发一个简单的微服务客户端,客户端调用一个或多个微服务完成业务处理,并将处理结果构造成最后的响应结果返回给用户。
微服务架构的实现需要依赖一个微服务框架这个框架包括一个微服务注册中心和一个RPC远程调用框架。微服务客户端通过注册中心得到要调用的微服务具体的地址列表然后通过一个软负载均衡算法选择其中一个服务器地址再通过PRC进行远程调用。
此外除了以上这些分布式技术高并发系统中常用的还有大数据、分布式文件、区块链、搜索引擎、NoSQL、CDN、反向代理等技术也都是一些非常经典的分布式技术。如果你对这些技术感兴趣想要更详细地了解它们那么你可以阅读我在极客时间的另两个专栏分别是《从0开始学大数据》和《后端技术面试38讲》。
系统并发指标
我们这个专栏大部分案例都是关于高并发系统的,那么和并发相关的指标有哪些?并发量又该如何估算?首先,我们来看和并发相关的指标,主要有以下这些。
目标用户数
目标用户数是所有可能访问我们系统的潜在用户的总和比如微信的目标用户是所有中国人那么微信的目标用户数就是13亿。目标用户数可以反映潜在的市场规模。
系统用户数
并不是所有的目标用户都会来访问我们的系统,只有那些真正访问过我们系统的用户才被称作系统用户。越是成功的系统,系统用户数和目标用户数越接近。
活跃用户数
同样地,访问过我们系统的用户可能只是偶尔过来访问一下,甚至只访问一次就永不再来。所以我们还需要关注用户的活跃度,也就是经常来访问的用户规模有多大。如果以一个月为单位,那么一个月内只要来访问过一次,就会被统计为活跃用户,这个数目被称为月活用户数。同样地,一天内访问过的总用户数被称为日活用户数。
在线用户数
当活跃用户登录我们的系统的时候,就成为在线用户了。在线用户数就是正在使用我们系统的用户总数。
并发用户数
但在线用户也并不总是在点击App请求我们的系统服务他可能搜索得到一个页面然后就在自己的手机端浏览。只有发起请求在服务器正在处理这个请求的用户才是并发用户。事实上高并发架构主要关注的就是用户发起请求服务器处理请求时需要消耗的计算资源。所以并发用户数是架构设计时主要关注的指标。
在我们后续的案例分析中,我都是根据市场规模估计一个目标用户数,然后再根据产品特点、竞品数据等,逐步估算其他的用户数指标。
有了上面这些用户数指标,我们就可以进一步估算架构设计需要考虑的其他一些技术指标,比如每天需要新增的文件存储空间,存储总系统用户需要的数据库规模,总网络带宽,每秒处理的请求数等等。
技术指标估算能力是架构师的一个重要能力,有了这个能力,你才有信心用技术解决未来的问题,也会因此对未来充满信心。这个估算过程,我们会在后面的案例课中不断重复,你也可以根据你的判断,分析这些估算是否合理,还有哪些没有考虑到的、影响架构设计的指标。
小结
高并发架构的主要挑战就是大量用户请求需要使用大量的计算资源。至于如何增加计算资源,互联网应用走出了一条水平伸缩的发展道路,也就是通过构建分布式集群架构,不断向集群中添加服务器,以此来增加集群的计算资源。
那如何增加服务器呢?对此,又诞生了各种各样的分布式技术方案。我们掌握了这些分布式技术,就算是掌握了高并发系统架构设计的核心。具体这些技术如何应用在高并发系统的架构实践中,我们在后面的案例中会不断进行展示。
思考题
我们在前面提到过分布式缓存客户端SDK会根据应用程序传入的key从分布式缓存集群中选择一台服务器进行访问那么这个客户端SDK如何选择服务器呢它怎么知道自己要访问的key在哪台服务器上你可以尝试说说自己知道几种方法算法它们各有什么优缺点。
欢迎在评论区分享你的思考,也欢迎把这节课分享给更多对高并发架构设计感兴趣的朋友,我们共同进步。

View File

@ -0,0 +1,221 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 短 URL 生成器设计:百亿短 URL 怎样做到无冲突?
你好,我是李智慧。
从这节课开始,我们将结合具体的案例,来看看怎么设计高并发系统,你也可以学习具体的软件设计文档写法了。这个模块,我们先来看看,当高并发遇到海量数据处理时的架构。
在社交媒体上人们经常需要分享一些URL但是有些URL可能会很长比如https://time.geekbang.org/hybrid/pvip?utm_source=geektime-pc-discover-banner&utm_term=geektime-pc-discover-banner
这样长的URL显然体验并不友好。我们期望分享的是一些更短、更易于阅读的短URL比如像 http://1.cn/ScW4dt 这样的。当用户点击这个短URL的时候可以重定向访问到原始的链接地址。为此我们将设计开发一个短URL生成器产品名称是“Fuxi伏羲”。
我们预计Fuxi需要管理的短URL规模在百亿级别并发吞吐量达到数万级别。这个量级的数据对应的存储方案是什么样的用传统的关系数据库存储还是有其他更简单的办法此外如何提升系统的并发处理能力呢这些是我们今天要重点考虑的问题。
需求分析
短URL生成器也称作短链接生成器就是将一个比较长的URL生成一个比较短的URL当浏览器通过短URL生成器访问这个短URL的时候重定向访问到原始的长URL目标服务器访问时序图如下。
对于需要展示短URL的应用程序由该应用调用短URL生成器生成短URL并将该短URL展示给用户用户在浏览器中点击该短URL的时候请求发送到短URL生成器短URL生成器以HTTP服务器的方式对外提供服务短URL域名指向短URL生成器短URL生成器返回HTTP重定向响应将用户请求重定向到最初的原始长URL浏览器访问长URL服务器完成请求服务。
短URL生成器的用例图
用户client程序可以使用短URL生成器Fuxi为每个长URL生成唯一的短URL并存储起来。
用户可以访问这个短URLFuxi将请求重定向到原始长URL。
生成的短URL可以是Fuxi自动生成的也可以是用户自定义的。用户可以指定一个长URL对应的短URL内容只要这个短URL还没有被使用。
管理员可以通过web后台检索、查看Fuxi的使用情况。
短URL有有效期2年后台定时任务会清理超过有效期的URL以节省存储资源同时回收短URL地址链接资源。
性能指标估算
Fuxi的存储容量和并发量估算如下。
预计每月新生成短URL 5亿条短URL有效期2年那么总URL数量120亿。
\(\\small 5亿\\times12月\\times2年=120亿\)
存储空间-
每条短URL数据库记录大约1KB那么需要总存储空间12TB不含数据冗余备份
\(\\small 120亿\\times1KB=12TB\)
吞吐量-
每条短URL平均读取次数100次那么平均访问吞吐量每秒访问次数2万。
\(\\small5亿\\times100\\div30\\times24\\times60\\times60\\approx20000\)
一般系统高峰期访问量是平均访问量的2倍因此系统架构需要支持的吞吐能力应为4万。
网络带宽-
短URL的重定向响应包含长URL地址内容长URL地址大约500BHTTP响应头其他内容大约500B所以每个响应1KB高峰期需要的响应网络带宽320Mb。
\(\\small 4万每秒次请求\\times1KB=40MB\\times8bit=320Mb\)
Fuxi的短URL长度估算如下。
短URL采用Base64编码如果短URL长度是7个字符的话大约可以编码4万亿个短URL。
\(\\small 64^{7}\\approx4万亿\)
如果短URL长度是6个字符的话大约可以编码680亿个短URL。
\(\\small 64^{6}\\approx680亿\)
按我们前面评估总URL数120亿6个字符的编码就可以满足需求。因此Fuxi的短URL编码长度6个字符形如http://l.cn/ScW4dt 。
非功能需求
系统需要保持高可用,不因为服务器、数据库宕机而引起服务失效。
系统需要保持高性能服务端80%请求响应时间应小于5ms99%请求响应时间小于20ms平均响应时间小于10ms。
短URL应该是不可猜测的即不能猜测某个短URL是否存在也不能猜测短URL可能对应的长URL地址内容。
概要设计
短URL生成器的设计核心就是短URL的生成即长URL通过某种函数计算得到一个6个字符的短URL。短URL有几种不同的生成算法。
单项散列函数生成短URL
通常的设计方案是将长URL利用MD5或者SHA256等单项散列算法进行Hash计算得到128bit或者256bit的Hash值。然后对该Hash值进行Base64编码得到22个或者43个Base64字符再截取前面的6个字符就得到短URL了如图。
但是这样得到的短URL可能会发生Hash冲突即不同的长URL计算得到的短URL是相同的MD5或者SHA256计算得到的Hash值几乎不会冲突但是Base64编码后再截断的6个字符有可能会冲突。所以在生成的时候需要先校验该短URL是否已经映射为其他的长URL如果是那么需要重新计算换单向散列算法或者换Base64编码截断位置。重新计算得到的短URL依然可能冲突需要再重新计算。
但是这样的冲突处理需要多次到存储中查找URL无法保证Fuxi的性能要求。
自增长短URL
一种免冲突的算法是用自增长自然数来实现即维持一个自增长的二进制自然数然后将该自然数进行Base64编码即可得到一系列的短URL。这样生成的的短URL必然唯一而且还可以生成小于6个字符的短URL比如自然数0的Base64编码是字符“A”就可以用http://1.cn/A作为短URL。
但是这种算法将导致短URL是可猜测的如果某个应用在某个时间段内生成了一批短URL那么这批短URL就会集中在一个自然数区间内。只要知道了其中一个短URL就可以通过自增以及自减的方式请求访问其他URL。Fuxi的需求是不允许短URL可预测。
预生成短URL
因此Fuxi采用预生成短URL的方案。即预先生成一批没有冲突的短URL字符串当外部请求输入长URL需要生成短URL的时候直接从预先生成好的短URL字符串池中获取一个即可。
预生成短URL的算法可以采用随机数来实现6个字符每个字符都用随机数产生用0~63的随机数产生一个Base64编码字符。为了避免随机数产生的短URL冲突需要在预生成的时候检查该URL是否已经存在用布隆过滤器检查。因为预生成短URL是离线的所以这时不会有性能方面的问题。事实上Fuxi在上线之前就已经生成全部需要的144亿条短URL并存储在文件系统中预估需要短URL120亿Fuxi预生成的时候进行了20%的冗余即144亿。
Fuxi的整体部署模型
Fuxi的业务逻辑比较简单相对比较有挑战的就是高并发的读请求如何处理、预生成的短URL如何存储以及访问。高并发访问主要通过负载均衡与分布式缓存解决而海量数据存储则通过HDFS以及HBase来完成。具体架构图如下。
系统调用可以分成两种情况一种是用户请求生成短URL的过程另一种是用户访问短URL通过Fuxi跳转到长URL的过程。
对于用户请求生成短URL的过程在短URL系统Fuxi上线前已经通过随机数算法预生成144亿条短URL并将其存储在HDFS文件系统中。系统上线运行后应用程序请求生成短URL的时候即输入长URL请求返回短URL请求通过负载均衡服务器被发送到短URL服务器集群短URL服务器再通过负载均衡服务器调用短URL预加载服务器集群。
短URL预加载服务器此前已经从短URL预生成文件服务器HDFS中加载了一批短URL存放在自己的内存中这时只需要从内存中返回一个短URL即可同时将短URL与长URL的映射关系存储在HBase数据库中时序图如下。
对于用户通过客户端请求访问短URL的过程即输入短URL请求返回长URL请求通过负载均衡服务器发送到短URL服务器集群短URL服务器首先到缓存服务器中查找是否有该短URL如果有立即返回对应的长URL短URL生成服务器构造重定向响应返回给客户端应用。
如果缓存没有用户请求访问的短URL短URL服务器将访问HBase短URL数据库服务器集群。如果数据库中存在该短URL短URL服务器会将该短URL写入缓存服务器集群并构造重定向响应返回给客户端应用。如果HBase中没有该短URL短URL服务器将构造404响应返回给客户端应用时序图如下。
过期短URL清理服务器会每个月启动一次将已经超过有效期2年的URL数据删除并将这些短URL追加写入到短URL预生成文件中。
为了保证系统高可用Fuxi的应用服务器、文件服务器、数据库服务器都采用集群部署方案单个服务器故障不会影响Fuxi短URL的可用性。
对于Fuxi的高性能要求80%以上的访问请求将被设计为通过缓存返回。Redis的缓存响应时间1ms左右服务器端请求响应时间小于3ms满足80%请求小于5ms的性能目标。对于缓存没有命中的数据通过HBase获取HBase平均响应时间10ms也可以满足设计目标中的性能指标。
对于Redis缓存内存空间估算业界一般认为超过80%请求集中在最近6天生成的短URL上Fuxi主要缓存最近六天生成的短URL即可。根据需求容量估计最近6天生成的短URL数量约1亿条因此需要Redis缓存服务器内存空间\(\\small 1亿\\times1KB=100GB\)。
详细设计
详细设计关注重定向响应码、短URL预生成文件及加载、用户自定义短URL等几个关键设计点。
重定向响应码
满足短URL重定向要求的HTTP重定向响应码有301和302两种其中301表示永久重定向即浏览器一旦访问过该短URL就将重定向的原始长URL缓存在本地此后不再请求短URL生成器直接根据缓存在浏览器HTTP客户端的长URL路径进行访问。
302表示临时重定向每次访问短URL都需要访问短URL生成器。
一般说来使用301状态码可以降低Fuxi服务器的负载压力但无法统计短URL的使用情况而Fuxi的架构设计完全可以承受这些负载压力因此Fuxi使用302状态码构造重定向响应。
短URL预生成文件及预加载
Fuxi的短URL是在系统上线前全部预生成的并存储在HDFS文件中。共144亿个短URL每个短URL 6个字符文件大小\(\\small 144亿\\times6B=86.4GB\)。
文件格式就是直接将144亿个短URL的ASC码无分割地存储在文件中如下是存储了3个短URL的文件示例
Wdj4FbOxTw9CHtvPM1
所以如果短URL预加载服务器第一次启动的时候加载1万个短URL那么就从文件头读取60K数据并标记当前文件偏移量60K。下次再加载1万个短URL的时候再从文件60K偏移位置继续读取60K数据即可。
因此Fuxi除了需要一个在HDFS记录预生成短URL的文件外还需要一个记录偏移量的文件记录偏移量的文件也存储在HDFS中。同时由于预加载短URL服务器集群部署多台服务器会出现多台服务器同时加载相同短URL的情况所以还需要利用偏移量文件对多个服务器进行互斥操作即利用文件系统写操作锁的互斥性实现多服务器访问互斥。
应用程序的文件访问流程应该是:写打开偏移量文件 -> 读偏移量 -> 读打开短URL文件 -> 从偏移量开始读取60K数据 -> 关闭短URL文件 -> 修改偏移量文件 -> 关闭偏移量文件。
由于写打开偏移量文件是一个互斥操作所以第一个预加载短URL服务器写打开偏移量文件以后其他预加载短URL服务器无法再写打开该文件也就无法完成读60K短URL数据及修改偏移量的操作这样就能保证这两个操作是并发安全的。
加载到预加载短URL服务器的1万个短URL会以链表的方式存储每使用一个短URL链表头指针就向后移动一位并设置前一个链表元素的next对象为null。这样用过的短URL对象可以被垃圾回收。
当剩余链表长度不足2000的时候触发一个异步线程从文件中加载1万个新的短URL并链接到链表的尾部。
与之对应的URL链表类图如下。
URLNodeURL链表元素类成员变量uRL即短URL字符串next指向下一个链表元素。
LinkedURLURL链表主类成员变量head指向链表头指针元素uRLAmount表示当前链表剩余元素个数。acquireURL()方法从链表头指针指向的元素中取出短URL字符串并执行urlAmount 操作。当urlAmount < 2000的时候调用私有方法loadURL()该方法调用一个线程从文件中加载1万个短URL并构造成链表添加到当前链表的尾部并重置uRLAmount
用户自定义短URL
Fuxi允许用户自己定义短URL即在生成短URL的时候由用户指定短URL的内容为了避免预生成的短URL和用户指定的短URL冲突Fuxi限制用户自定义短URL的字符个数不允许用户使用6个字符的自定义短URL且URL长度不得超过20个字符
但是用户自定义短URL依然可能和其他用户自定义短URL冲突所以Fuxi生成自定义短URL的时候需要到数据库中检查冲突是否指定的URL已经被使用如果发生冲突要求用户重新指定
URL Base64编码
标准Base64编码表如下
其中“+”“/”在URL中会被编码为“%2B以及“%2F”,“%”在写入数据库的时候又和SQL编码规则冲突需要进行再编码因此直接使用标准Base64编码进行短URL编码并不合适URL保留字符编码表如下
所以我们需要针对URL场景对Base64编码进行改造使用URL保留字符表以外的字符对Base64编码表中的6263进行编码“+”改为-”,“/”改为_”,Fuxi最终采用的URL Base64编码表如下
小结
我们开头提到Fuxi是一个高并发2万QPS)、海量存储144亿条数据)、还需要10ms的高性能平均响应时间的系统但是我们后面看到Fuxi的架构并不复杂
这一方面是源于Fuxi的业务逻辑非常简单只需要完成短URL与长URL的映射关系生成与获取就可以了另一方面则是源于开源技术体系的成熟比如一个HDFS集群可支持百万TB规模的数据存储而我们需要的存储空间只有区区不到100GB都有点大材小用了事实上Fuxi选择HDFS更多的考量是利用HDFS的高可用HDFS的自动备份策略为我们提供了高可用的数据存储解决方案
同理高并发也是如此2万QPS看起来不小但实际上由于业务逻辑简单单个数据都很小加上大部分请求数据可以通过Redis缓存获取所以实际响应时间是非常短的10ms的平均响应时间使得Fuxi真正承受的并发压力只有200对于这样简单的业务逻辑以及200这样的并发压力我们使用配置高一点的服务器的话只需要一台短URL服务器其实就可以满足了所以我们在短URL服务器之前使用负载均衡服务器这也是更多地为高可用服务
思考题
用户每次请求生成短URL的时候Fuxi都会返回一个新生成的短URL也就意味着如果用户重复提交同一个长URL请求生成短URL每次都会返回一个新的短URL你认为这将导致什么问题对此你有什么解决方案
另外小结里提到2万QPS10ms平均响应时间这种情况下真正的并发量只有200这个200是如何得到的
欢迎在评论区分享你的思考或者提出对这个设计文档的评审意见我们共同进步

View File

@ -0,0 +1,221 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 网页爬虫设计:如何下载千亿级网页?
你好,我是李智慧。
在互联网早期,网络爬虫仅仅应用在搜索引擎中。随着大数据时代的到来,数据存储和计算越来越廉价和高效,越来越多的企业开始利用网络爬虫来获取外部数据。例如:获取政府公开数据以进行统计分析;获取公开资讯以进行舆情和热点追踪;获取竞争对手数据以进行产品和营销优化等等。
网络爬虫有时候也被称为网络机器人或者网络蜘蛛。我们准备开发一个全网爬虫爬取全中文互联网的公开网页以构建搜索引擎和进行数据分析。爬虫名称为“Bajie八戒”。
Bajie的技术挑战包括如何不重复地获取并存储全网海量URL如何保证爬虫可以快速爬取全网网页但又不会给目标网站带来巨大的并发压力接下来我们就来看看Bajie的需求与技术架构。
需求分析
Bajie的功能比较简单这里不再赘述。
性能指标估算
因为互联网网页会不断产生所以全网爬虫Bajie也是一个持续运行的系统。根据设计目标Bajie需要每个月从互联网爬取的网页数为20亿个平均每个页面500KB且网页需存储20年。
Bajie的存储量和TPS系统吞吐量估算如下。
每月新增存储量-
估计平均每个页面500KB那么每个月需要新增存储1PB。
\(\\small 20亿\\times500KB=1PB\)
总存储空间-
网页存储有效期20年那么需要总存储空间240PB。
\(\\small 1PB\\times12个月\\times20年=240PB\)
TPS-
Bajie的TPS应为800。
\(\\small 20亿\\div30\\times24\\times60\\times60\\approx800\)
非功能需求
Bajie需要满足的非功能需求如下。
伸缩性当未来需要增加每月爬取的网页数时Bajie可以灵活部署扩大集群规模增强其爬取网页的速度。也就是说Bajie必须是一个分布式爬虫。
健壮性互联网是一个开放的世界也是一个混乱的世界服务器可能会宕机网站可能失去响应网页HTML可能是错误的链接可能有陷阱……所以Bajie应该能够面对各种异常正常运行。
去重一方面需要对超链接URL去重相同的URL不需要重复下载另一方面还要对内容去重不同URL但是相同内容的页面也不需要重复存储。
扩展性当前只需要爬取HTML页面即可将来可能会扩展到图片、视频、文档等内容页面。-
此外Bajie必须是“礼貌的”。爬虫爬取页面实际上就是对目标服务器的一次访问如果高并发地进行访问可能会对目标服务器造成比较大的负载压力甚至会被目标服务器判定为DoS攻击。因此Bajie要避免对同一个域名进行并发爬取还要根据目标服务器的承载能力增加访问延迟即在两次爬取访问之间增加等待时间。
并且Bajie还需要遵循互联网爬虫协议即目标网站的robots.txt协议不爬取目标网站禁止爬取的内容。比如www.zhihu.com的robots.txt内容片段如下。
User-agent: bingbot
Disallow: /appview/
Disallow: /login
Disallow: /logout
Disallow: /resetpassword
Disallow: /terms
Disallow: /search
Allow: /search-special
Disallow: /notifications
Disallow: /settings
Disallow: /inbox
Disallow: /admin_inbox
Disallow: /*?guide*
Zhihu约定Bing爬虫可以访问和不可以访问的路径都列在robots.txt中其他的Google爬虫等也在robots.txt中列明。-
robots.txt还可以直接禁止某个爬虫比如淘宝就禁止了百度爬虫淘宝的robots.txt如下。
User-agent: Baiduspider
Disallow: /
User-agent: baiduspider
Disallow: /
淘宝禁止百度爬虫访问根目录,也就是禁止百度爬取该网站所有页面。-
robots.txt在域名根目录下如www.taobao.com/robots.txt。Bajie应该首先获取目标网站的robots.txt根据爬虫协议构建要爬取的URL超链接列表。
概要设计
Bajie的设计目标是爬取数千亿的互联网页那么Bajie首先需要得到这千亿级网页的URL该如何获得呢
全世界的互联网页面事实上是一个通过超链接连接的巨大网络其中每个页面都包含一些指向其他页面的URL链接这些有指向的链接将全部网页构成一个有向网络图。如下图所示每个节点是一个网页每条有向的边就是一个超链接。
上图中www.a.com包含两个超链接分别是www.b.com和www.c.com对应图中就是节点www.a.com指向节点www.b.com和节点www.c.com的边。同样地www.b.com节点也会指向www.d.com节点。
如果我们从这个图中的某个节点开始遍历,根据节点中包含的链接再遍历其指向的节点,再从这些新节点遍历其指向的节点,如此下去,理论上可以遍历互联网上的全部网页。而将遍历到的网页下载保存起来,就是爬虫的主要工作。
所以Bajie不需要事先知道数千亿的URL然后再去下载。Bajie只需要知道一小部分URL也就是所谓的种子URL然后从这些种子URL开始遍历就可以得到全世界的URL并下载全世界的网页。
Bajie的处理流程活动图如下。
首先Bajie需要构建种子URL它们就是遍历整个互联网页面有向图的起点。种子URL将影响遍历的范围和效率所以我们通常选择比较知名的网站的主要页面比如首页作为种子URL。
然后URL调度器从种子URL中选择一些URL进行处理。后面将在详细介绍中说明URL调度器的算法原理。
Bajie对选择出来的URL经过域名解析后下载得到HTML页面内容进而解析HTML页面分析该内容是否已经在爬虫系统中存在。因为在互联网世界中大约有三分之一的内容是重复的下载重复的内容就是在浪费计算和存储资源。如果内容已存在就丢弃该重复内容继续从URL调度器获取URL如果不存在就将该HTML页面写入HDFS存储系统。
然后Bajie进一步从已存储的HTML中提取其内部包含的超链接URL分析这些URL是否满足过滤条件即判断URL是否在黑名单中以及URL指向的目标文件类型是否是爬虫要爬取的类型。
如果HTML中的某些URL满足过滤条件那么就丢弃这些URL如果不满足过滤条件那么进一步判断这些URL是否已经存在如果已经存在就丢弃该URL如果不存在就记录到待下载URL集合。URL调度器从待下载URL集合中选择一批URL继续上面的处理过程。
这里需要注意想判断URL是否已经存在就要判断这个URL是否已经在待下载URL集合中。此外还需要判断这个URL是否已经下载得到HTML内容了。只有既不是待下载也没被下载过的URL才会被写入待下载URL集合。
可以看到在爬虫的活动图里是没有结束点的从开始启动就不停地下载互联网的页面永不停息。其中URL调度器是整个爬虫系统的中枢和核心也是整个爬虫的驱动器。爬虫就是靠着URL调度器源源不断地选择URL然后有节奏、可控地下载了整个互联网所以URL调度器也是爬虫的策略中心。
据此Bajie的部署图如下。
Bajie系统中主要有两类服务器一类是URL调度器服务器一类是URL下载处理服务器集群它是一个分布式集群。
URL调度器从种子URL或待下载URL集合中载入URL再根据调度算法选择一批URL发送给URL下载处理服务器集群。这个下载处理服务器集群是由多台服务器组成的根据需要达到的TPS集群规模可以进行动态伸缩以实现需求中的伸缩性要求。
每台URL下载处理服务器先得到分配给自己的一组URL再启动多个线程其中每个线程处理一个URL按照前面的流程调用域名解析组件、HTML下载组件、HTML内容解析组件、内容去重组件、URL提取组件、URL过滤组件、URL去重组件最终将HTML内容写入HDFS并将待下载URL写入待下载URL集合文件。
分布式爬虫
需要注意的是URL下载处理服务器采用分布式集群部署主要是为了提高系统的吞吐能力使系统满足伸缩性需求。而URL调度器则只需要采用一台高性能的服务器单机部署即可。
事实上单机URL调度器也完全能够满足目前800TPS的负载压力以及将来的伸缩要求。因为800TPS对于URL调度器而言其实就是每秒产生800个URL而已计算压力并不大单台服务器完全能够满足。
同时URL调度器也不需要考虑单服务器宕机导致的可用性问题因为爬虫并不是一个实时在线系统如果URL调度器宕机只需要重新启动即可并不需要多机部署高可用集群。
相对应地每个URL在URL下载处理服务器上的计算负载压力要大得多需要分布式集群处理也因此大规模爬虫被称为分布式爬虫Bajie就是一个分布式爬虫。
详细设计
Bajie详细设计关注3个技术关键点URL调度器算法、去重算法、高可用设计。
URL调度器算法
URL调度器需要从待下载URL集合中选取一部分URL进行排序然后分发给URL下载服务器去下载。待下载URL集合中的URL是从下载的HTML页面里提取出来然后进行过滤、去重得到的。一个HTML页面通常包含多个URL每个URL又对应一个页面因此URL集合数量会随着页面不断下载而指数级增加。
待下载URL数量将远远大于系统的下载能力URL调度器就需要决定当前先下载哪些URL。
如果调度器一段时间内选择的都是同一个域名的URL那就意味着我们的爬虫将以800 TPS的高并发访问同一个网站。目标网站可能会把爬虫判定为DoS攻击从而拒绝请求更严重的是高并发的访问压力可能导致目标网站负载过高系统崩溃。这样的爬虫是“不礼貌”的也不是Bajie的设计目标。
前面说过网页之间的链接关系构成一个有向图因此我们可以按照图的遍历算法选择URL。图的遍历算法有深度优先和广度优先两种深度优先就是从一个URL开始访问网页后从里面提取第一个URL然后再访问该URL的页面再提取第一个URL如此不断深入。
深度优先需要维护较为复杂的数据结构,而且太深的下载深度导致下载的页面非常分散,不利于我们构建搜索引擎和数据分析。所以我们没有使用深度优先算法。
那广度优先算法如何呢广度优先就是从一个URL开始访问网页后从中得到N个URL然后顺序访问这个N个URL的页面然后再从这N个页面中提取URL如此不断深入。显然广度优先实现更加简单获取的页面也比较有关联性。
图的广度优先算法通常采用队列来实现。首先URL调度器从队列头出队列dequeue取一个URL交给URL下载服务器下载得到HTML再从HTML中提取得到若干个URL入队列enqueue到队列尾URL调度器再从队列头出队列dequeue取一个URL……如此往复持续不断地访问全部互联网页这就是互联网的广度优先遍历。
事实上由于待下载URL集合存储在文件中URL下载服务器只需要向待下载URL集合文件尾部追加URL记录而URL调度器只需要从文件头顺序读取URL这样就天然实现了先进先出的广度优先算法如下图。
但是广度优先搜索算法可能会导致爬虫一段时间内总是访问同一个网站因为一个HTML页面内的链接常常是指向同一个网站的这样就会使爬虫“不礼貌”。
通常我们针对一个网站一次只下载一个页面所以URL调度器需要将待下载URL根据域名进行分类。此外不同网站的信息质量也有高低之分爬虫应该优先爬取那些高质量的网站。优先级和域名都可以使用不同队列来区分如下图。
首先优先级分类器会根据网页内容质量将域名分类后面专栏会讲PageRank质量排名算法并为不同质量等级的域名设置不同的优先级然后将不同优先级记录在“域名优先级表”中。
接下来按照广度优先算法URL列表会从待下载URL集合文件中装载进来。根据“域名优先级表”中的优先级顺序优先级分类器会将URL写入不同的队列中。
下一步优先级队列选择器会根据优先级使用不同的权重从这些优先级队列中随机获取URL这样使得高优先级的URL有更多机会被选中。而被选中的URL都会交由域名分类器进行分类处理。域名分类器的分类依据就是“域名队列映射表”这个表中记录了不同域名对应的队列。所以域名分类器可以顺利地将不同域名的URL写入不同的域名队列中。
最后域名队列选择器将轮询所有的域名队列从其中获得URL并分配给不同的URL下载服务器进而完成下载处理。
去重算法
爬虫的去重包括两个方面一个是URL相同URL不再重复下载一个是内容相同页面内容不再重复存储。去重一方面是提高爬虫效率避免无效爬取另一方面提高搜索质量避免相同内容在搜索结果中重复出现。URL去重可以使用布隆过滤器以提高效率。
内容去重首先要判断内容是否重复,由于爬虫存储着海量的网页,如果按照字符内容对每一个下载的页面都去和现有的页面比较是否重复,显然是不可能的。
Bajie计算页面内容的MD5值通过判断下载页面的内容MD5值是否已经存在判断内容是否重复。
如果把整个HTML内容都计算MD5那么HTML中的微小改变就会导致MD5不同事实上不同网站即使相同内容的页面也总会改成自己的HTML模板导致HTML内容不同。
所以比较内容重复的时候需要将HTML里面的有效内容提取出来也就是提取出去除HTML标签的文本信息针对有效内容计算MD5。更加激进的做法是从有效内容中抽取一段话比如最长的一句话计算这段话的MD5进而判断重复。
而一个内容MD5是否存在需要在千亿级的数据上查找如果用Hash表处理计算和内存存储压力非常大我们将用布隆过滤器代替Hash表以优化性能。
高可用设计
Bajie的可用性主要关注两个方面一是URL调度器或URL下载处理服务器宕机二是下载超时或内容解析错误。
由于Bajie是一个离线系统暂时停止爬取数据的话不会产生严重的后果所以Bajie并不需要像一般互联网系统那样进行高可用设计。但是当服务器宕机后重启时系统需要能够正确恢复保证既不会丢失数据也不会重复下载。
所以URL调度器和URL下载处理服务器都需要记录运行时状态即存储本服务器已经加载的URL和已经处理完成的URL这样宕机恢复的时候就可以立刻读取到这些状态数据进而使服务器恢复到宕机前的状态。对于URL下载处理服务器Bajie采用Redis记录运行时状态数据。
此外为了防止下载超时或内容解析错误URL下载处理服务器会采用多线程设计。每个线程独立完成一个URL的下载和处理线程也需要捕获各种异常不会使自己因为网络超时或者解析异常而退出。
小结
架构设计是一个权衡的艺术,不存在最好的架构,只存在最合适的架构。架构设计的目的是解决各种业务和技术问题,而解决问题的方法有很多种,每一种方法都需要付出各自的代价,同时又会带来各种新的问题。架构师就需要在这些方法中权衡选择,寻找成本最低的、代价最小的、自己和团队最能驾驭得住的解决方案。
因此,架构师也许不是团队中技术最好的那个人,但一定是对问题和解决方案优缺点理解最透彻的那个人。很多架构师把高可用挂在嘴上。可是,你了解你的系统的高可用的目的是什么吗?你的用户能接受的不可用下限在哪里?你为高可用付出的代价是什么?这些代价换来的回报是否值得?
我们在Bajie的设计中核心就是URL调度器。通常在这样的大规模分布式系统中核心组件是不允许单点的也就是不允许单机部署因为单机宕机就意味着核心功能的故障也就意味着整个系统无法正常运行。
但是如果URL调度器采用分布式集群架构提高可用性多服务器共同进行URL调度就需要解决数据一致性和数据同步问题反而会导致系统整体处理能力下降。而Bajie采用单机部署的的方式虽然宕机时系统无法正常运行但是只要在运维上保证能快速重新启动长期看系统整体处理能力反而更高。
此外对于一个千亿级网页的爬虫系统而言最主要的技术挑战应该是海量文件的存储与计算这也确实是早期搜索引擎公司们的核心技术。但是自从Google公开自己的大数据技术论文而Hadoop开源实现了相关技术后这些问题就变得容易很多了。Bajie的海量文件存储就使用了Hadoop分布式文件系统HDFS我会在后面的《常见海量数据处理技术回顾》这一讲详细讨论它。
思考题
一个设计良好的爬虫需要面对的情况还有很多,你还能想到哪些文中没提及的情况?最好也能和我聊聊对应的设计方案。
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。

View File

@ -0,0 +1,172 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 网盘系统设计:万亿 GB 网盘如何实现秒传与限速?
你好,我是李智慧。
网盘又称云盘是提供文件托管和文件上传、下载服务的网站File hosting service。人们通过网盘保管自己拍摄的照片、视频通过网盘和他人共享文件已经成为了一种习惯。我们准备开发一个自己的网盘应用系统应用名称为“DBox”。
十几年前曾经有个段子,技术人员对老板说:您不能在公司电脑打开您家里电脑的文件,再贵的电脑也不能。事实上,随着网盘技术的成熟,段子中老板的需求已经成为现实:网盘可以自动将家里电脑的文件同步到公司电脑,老板可以在公司的电脑打开家里电脑的文件了。
网盘的主要技术挑战是海量数据的高并发读写访问。**用户上传的海量数据如何存储如何避免部分用户频繁读写文件消耗太多资源而导致其他的用户体验不佳我们看下DBox的技术架构以及如何解决这些问题。
需求分析
DBox的核心功能是提供文件上传和下载服务。基于核心功能DBox需要在服务器端保存这些文件并在下载和上传过程中实现断点续传。也就是说如果上传或下载过程被中断了恢复之后还能从中断的地方重新上传或者下载而不是从头再来。
DBox还需要实现文件共享的需求。使用DBox的不同用户之间可以共享文件一个用户上传的文件共享给其他用户后其他用户也可以下载这个文件。
此外网盘是一个存储和网络密集型的应用用户文件占据大量硬盘资源上传、下载需要占用大量网络带宽并因此产生较高的运营成本。所以用户体验需要向付费用户倾斜DBox需要对上传和下载进行流速控制保证付费用户得到更多的网络资源。DBox用例图如下。
负载指标估算
DBox的设计目标是支持10亿用户注册使用免费用户最大可拥有1TB存储空间。预计日活用户占总用户的20%即2亿用户。每个活跃用户平均每天上传、下载4个文件。
DBox的存储量、吞吐量、带宽负载估算如下。
总存储量-
理论上总存储空间估算为10亿TB即1万亿GB。
\(\\small 10亿\\times1TB=10亿TB\)
但考虑到大多数用户并不会完全用掉这个空间还有很多用户存储的文件其实是和别人重复的电影、电子书、软件安装包等真正需要的存储空间大约是这个估算值的10%即1亿TB。
QPS-
系统需要满足的平均QPS约为10000。
\(\\small 2亿\\times4\\div24\\times60\\times60\\approx1万\)
高峰期QPS约为平均QPS的两倍即2万。
带宽负载-
每次上传下载文件平均大小1MB所以需要网络带宽负载10GB/s即80Gb/s。
\(\\small 1万\\times1MB=10GB/s=80Gb/s\)
同样高峰期带宽负载为160Gb/s。
非功能需求
大数据量存储10亿注册用户1000亿个文件约1亿TB的存储空间。
高并发访问平均1万QPS高峰期2万QPS。
大流量负载平均网络带宽负载80Gb/S高峰期带宽负载160Gb/s。
高可靠存储文件不丢失持久存储可靠性达到99.9999% 即100万个文件最多丢失或损坏1个文件。
高可用服务用户正常上传、下载服务可用性在99.99%以上即一年最多53分钟不可用。
数据安全性:文件需要加密存储,用户本人及共享文件外,其他人不能查看文件内容。
不重复上传:相同文件内容不重复上传,也就是说,如果用户上传的文件内容已经被其他用户上传过了,该用户不需要再上传一次文件内容,进而实现“秒传”功能。从用户视角来看,不到一秒就可以完成一个大文件的上传。
概要设计
网盘设计的关键是元数据与文件内容的分离存储与管理。所谓文件元数据就是文件所有者、文件属性、访问控制这些文件的基础信息事实上传统文件系统也是元数据与文件内容分离管理的比如Linux的文件元数据记录在文件控制块FCB中Windows的文件元数据记录在文件分配表FAB中Hadoop分布式文件系统HDFS的元数据记录在NameNode中。
而DBox是将元信息存储在数据库中文件内容则使用另外专门的存储体系。但是由于DBox是一个互联网应用出于安全和访问管理的目的并不适合由客户端直接访问存储元数据的数据库和存储文件内容的存储集群而是通过API服务器集群和数据块服务器集群分别进行访问管理。整体架构如下图。
对于大文件DBox不会上传、存储一整个的文件而是将这个文件进行切分变成一个个单独的Block再将它们分别上传并存储起来。
这样做的核心原因是DBox采用对象存储作为最终的文件存储方案而对象存储不适合存储大文件需要进行切分。而大文件进行切分还带来其他的好处可以以Block为单位进行上传和下载提高文件传输速度客户端或者网络故障导致文件传输失败也只需要重新传输失败的Block就可以进而实现断点续传功能。
Block服务器就是负责Block上传和管理的。客户端应用程序根据API服务器的返回指令将文件切分成一些Block然后将这些Block分别发送给Block服务器Block服务器再调用对象存储服务器集群将Block存储在对象存储服务器中DBox选择Ceph作为对象存储
用户上传文件的时序图如下。
用户上传文件时客户端应用程序收集文件元数据包括文件名、文件内容MD5、文件大小等等并根据文件大小计算Block的数量DBox设定每个block大小4MB以及每个Block的MD5值。
然后客户端应用程序将全部元数据包括所有Block的MD5值列表发送给API服务器。API服务器收到文件元数据后为每个Block分配全局唯一的BlockIDBlockID为严格递增的64位正整数总可记录数据大小\(\\small 2^{64}\\times4MB=180亿PB\)足以满足DBox的应用场景
下一步API服务器将文件元数据与BlockID记录在数据库中并将BlockID列表和应用程序可以连接的Block服务器列表返回客户端。客户端连接Block服务器请求上传BlockBlock服务器连接API服务器进行权限和文件元数据验证。验证通过后客户端上传Block数据Block服务器再次验证Block数据的MD5值确认数据完整后将BlockID和Block数据保存到对象存储集群Ceph中。
类似的,用户下载文件的时序图如下。
客户端程序访问API服务器请求下载文件。然后API服务器会查找数据库获得文件的元数据信息再将元数据信息中的文件BlockID列表及可以访问的Block服务器列表返回给客户端。
下一步客户端访问Block服务器请求下载Block。Block服务器验证用户权限后从Ceph中读取Block数据返回给客户端客户端再将返回的Block组装为文件。
详细设计
为解决网盘的三个重要问题元数据如何管理网络资源如何向付费用户倾斜如何做到不重复上传DBox详细设计将关注元数据库、上传下载限速、秒传的设计实现。
元数据库设计
元数据库表结构设计如下。
从图中可以看出元数据库表结构中主要包括三个表分别是User用户表、File文件表和Block数据块表表的用途和包含的主要字段如下
User用户表记录用户基本信息用户名、创建时间、用户类型免费、VIP、用户已用空间、电话号码、头像等等。
File文件表记录文件元信息文件名、是否为文件夹、上级文件夹、文件MD5、创建时间、文件大小、文件所属用户、是否为共享文件等。
Block数据块表记录Block数据包括BlockID、Block MD5、对应文件等。
其中User表和File表为一对多的关系File表和Block表也是一对多的关系。
这3种表的记录数都是百亿级以上所以元数据表采用分片的关系数据库存储。
因为查询的主要场景是根据用户ID查找用户信息和文件信息以及根据文件ID查询block信息所以User和File表都采用user_id作为分片键Block表采用file_id作为分片键。
限速
DBox根据用户付费类型决定用户的上传、下载速度。而要控制上传、下载速度可以通过限制并发Block服务器数目以及限制Block服务器内的线程数来实现。
具体过程是客户端程序访问API服务器请求上传、下载文件的时候API服务器可以根据用户类型决定分配的Block服务器数目和Block服务器内的服务线程数以及每个线程的上传、下载速率。
Block服务器会根据API服务器的返回值来控制客户端能够同时上传、下载的Block数量以及传输速率以此对不同用户进行限速。
秒传
秒传是用户快速上传文件的一种功能。
事实上,网盘保存的很多文件,内容其实是重复的,比如电影、电子书等等。一方面,重复上传这些文件会加大网盘的存储负载压力;另一方面,每次都要重新上传重复的内容,会导致用户网络带宽的浪费和用户等待时间过长的问题。
所以在设计中物理上相同的文件DBox只会保存一份。用户每次上传文件时DBox都会先在客户端计算文件的MD5值再根据MD5值判断该文件是否已经存在。对于已经存在的文件只需要建立用户文件和该物理文件的关联即可并不需要用户真正上传该文件这样就可以实现秒传的功能。
但是计算MD5可能会发生Hash冲突也就是不同文件算出来的MD5值是相同的这样会导致DBox误判将本不相同的文件关联到一个物理文件上。不但会使上传者丢失自己的文件还会被黑客利用上传一个和目标文件MD5相同的文件然后就可以下载目标文件了。
所以DBox需要通过更多信息判断文件是否相同只有文件长度、文件开头256KB的MD5值、文件的MD5值三个值都相同才会认为文件相同。当文件长度小于256KB则直接上传文件不启用秒传功能。
为此我们需要将上面的元数据库表结构进行一些改动将原来的File表拆分成物理文件表Physics_File和逻辑文件表Logic_File。其中Logic_File记录用户文件的元数据并和物理文件表Physics_File建立多对1关联关系而Block表关联的则是Physics_File表如下。
Physics_File中字段md5和256kmd5字段分别记录了文件MD5和文件头256KB的MD5数据而size记录了文件长度只有这三个字段都相同才会启用秒传。
小结
我们在需求分析中讨论过DBox需要支持大数据量存储、高并发访问、高可用服务、高可靠存储等非功能需求。事实上对于网盘应用而言元数据API服务其实和一般的高并发互联网系统网关没有太大差别。真正有挑战的是海量文件的高可用存储而这一挑战在DBox中被委托给了分布式对象存储Ceph来完成。而Ceph本身设计就是支持大数据量存储、高并发访问、高可用服务、高可靠存储的。
架构师按照职责可以分成两种一种是应用系统架构师负责设计、开发类似网盘、爬虫这样的应用系统另一种是基础设施架构师负责设计、开发类似Ceph、HDFS这样的基础设施系统。
应用架构师需要掌握的技术栈更加广泛,要能够掌握各种基础设施技术的特性,并能根据业务特点选择最合适的方案;而基础设施架构师需要的技术栈更加深入,需要掌握计算机软硬件更深入的知识,才能开发出一个稳定的基础技术产品。
当然,最好的架构师应该是技术栈既广泛又深入,既能灵活应用各种基础设施来开发应用系统,也能在需要的时候自己动手开发新的基础设施系统。
我们专栏大部分案例都是关于应用的,但是也不乏关于编程框架、限流器、安全防火墙、区块链等基础设施的案例。你也可以在学习的过程中,感受下这两种系统的设计方案和技术关键点的不同。
思考题
网盘元数据存储采用分片的关系数据库方案查询目录和文件都比较简单但是性能也比较差。而且文件表按用户ID分片如果某个用户创建大量文件还会导致分片不均衡你有什么优化的手段和方法吗
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。

View File

@ -0,0 +1,174 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 短视频系统设计:如何支持三千万用户同时在线看视频?
你好,我是李智慧。
短视频short video通常时长在15分钟以内主要是在移动智能终端上进行拍摄、美化编辑或加特效并可以在网络社交平台上进行实时分享的一种新型视频形式。短视频具有时间短、信息承载量高等特点更符合当下网民手机使用行为习惯短视频的用户流量创造了巨大的商机。
我们准备开发一个面向全球用户的短视频应用用户总量预计20亿应用名称QuickTok。
视频文件和其他媒体文件相比会更大一点这就意味着存储短视频文件需要更大的存储空间播放短视频也需要更多的网络带宽。因此QuickTok的主要技术挑战是如何应对高并发用户访问时的网络带宽压力以及如何存储海量的短视频文件。接下来我们就来看看QuickTok的需求与技术架构。
需求分析
QuickTok的核心功能需求非常简单用户上传视频、搜索视频、观看视频。我们将主要分析非功能需求。
QuickTok预计用户总量为20亿日活用户约10亿每个用户平均每天浏览10个短视频由此可以预估短视频日播放量为100亿
\(\\small 10亿\\times10=100亿\)
平均播放QPS为11万/秒:
\(\\small 100亿\\div24\\times60\\times60\\approx11万/秒\)
每秒11万用户点击视频假设用户平均观看5分钟那么同时在观看的视频数就是
\(\\small 11万/秒\\times5\\times60秒=3千万\)
假设每个短视频的平均播放次数200次那么为了支撑这样体量的播放量平均需要每秒上传视频数
\(\\small 11万/秒\\div200=550/秒\)
每个短视频平均大小100MB每秒上传至服务器的文件大小为
\(\\small 100MB\\times550=55GB\)
(视频虽然不是一秒内上传至服务器的,但是这样计算依然没有问题。)
每年新增视频需要的存储空间:
\(\\small 55GB\\times60\\times60\\times24\\times365=1700PB\)
事实上为了保证视频数据的高可用不会因为硬盘损坏导致数据丢失视频文件需要备份存储QuickTok采用双副本的备份存储策略也就是每个视频文件存储三份需要的总存储空间
\(\\small 1700PB\\times3=5200PB\)
而播放视频需要的总带宽:
\(\\small 11万\\times100MB\\times8bit=88Tb\)
因此我们需要设计的短视频应用是一个每秒上传550个视频文件、11万次播放、新增165GB存储以及88Tb总带宽的高并发应用系统。这个系统呢需要是高性能的能迅速响应用户的上传和播放操作也需要是高可用的能面向全球用户提供7 * 24小时稳定的服务。
概要设计
QuickTok的核心部署模型如下图。
用户上传视频时上传请求会通过负载均衡服务器和网关服务器到达视频上传微服务。视频上传微服务需要做两件事一是把上传文件数据流写入视频文件暂存服务器二是把用户名、上传时间、视频时长、视频标题等视频元数据写入分布式MySQL数据库。
视频文件上传完成后,视频上传微服务会生成一个视频上传完成消息,并将其写入到消息队列服务器。视频内容处理器将消费这个上传完成消息,并根据消息内容,从视频文件暂存服务器获取视频文件数据,进行处理。
视频内容处理器是一个由责任链模式构建起来的管道。在这个管道中,视频将会被顺序进行内容合规性审查、内容重复性及质量审查、内容标签生成、视频缩略图生成、统一视频转码处理等操作,如下图。
合规且非重复的视频会经过统一转码最终被写入分布式文件存储和CDN。这样视频上传处理就完成了具体时序图如下。
以上就是对视频上传环节的设计,接下来我们将讨论对视频搜索及播放部分的设计,即核心部署模型图中标红的部分,如下。
视频搜索引擎会根据用户提交的视频标题、上传用户等元数据以及视频内容处理器生成的内容标签构建倒排索引。当用户搜索视频时系统会根据倒排索引来检索符合条件的视频并返回结果列表。结果列表在App端向用户呈现时会将此前视频内容处理器生成的缩略图展现给用户使用户对视频内容有个初步而直观的感受。
当用户点击缩略图时App开始播放视频。App并不需要下载完整个视频文件才开始播放而是以流的方式一边下载视频数据一边播放使用户尽量减少等待获得良好的观看体验。QuickTok使用MPEGDASH流媒体传输协议进行视频流传输因为这个协议具有自适应能力而且支持HTTP可以应对QuickTok的视频播放需求。
详细设计
为解决QuickTok的两个重要问题如何存储海量视频文件如何解决高并发视频播放导致的带宽压力详细设计将关注视频存储系统、性能优化与CDN。
此外,“如何生成更吸引用户的缩略图”是短视频应用用户体验的一个关键问题,详细设计也会关注缩略图生成与推荐的设计实现。
视频存储系统设计
由需求分析可知QuickTok每年新增5200PB的存储。因此“如何存储海量视频文件”就是QuickTok设计的重要挑战之一。对此我们可以尝试与[网盘]相同的存储技术方案将视频文件拆分成若干block使用对象存储服务进行存储。
但QuickTok最终采用了另一种存储方案即使用Hadoop分布式文件系统HDFS进行存储。HDFS适合大文件存储的一次写入多次读取的场景满足视频一次上传多次播放的需求同时它还可以自动进行数据备份缺省配置下每个文件存储三份也满足我们关于数据存储高可用的需求。
HDFS适合存储大文件大文件减少磁盘碎片更有利于存储空间的利用同时HDFS NameNode的访问压力也更小所以我们需要把若干个视频文件合并成一个HDFS文件进行存储并将存储相关的细节记录到HBase中。
举个例子当用户上传一个视频文件系统会自动生成一个视频ID这里假设这个ID是123。视频内容处理器先对视频进行一系列处理再调用视频文件存储服务来进行存储。
存储服务首先通过HDFS创建一个文件比如/data/videos/clust0/p0/000000001然后将视频文件数据顺序写入到HDFS中。写完后存储服务就可以得到这个HDFS文件的全路径名(/data/videos/clust0/p0/000000001)、视频文件在HDFS中的偏移量0、文件大小99,000,000B。
然后视频文件存储服务再将这些信息记录到HBase中主键就是视频IDvalue就是。
假设另一个用户上传的视频ID为456文件大小100,000,000B紧随着上一个视频文件也保存到同一个HDFS文件中。那么HBase中就可以记录主键value。
当其他用户播放视频456时播放微服务根据主键ID在HBase中查找value值得到HDFS文件路径/data/videos/clust0/p0/000000001从该文件99,000,000偏移位置开始读取100,000,000Byte数据就是视频ID 456完整的文件数据了。
性能优化与CDN设计
我们前面分析过QuickTok需要的总带宽是88Tb这是一个非常巨大的数字。如果单纯靠QuickTok自己的数据中心来承担这个带宽压力技术挑战和成本都非常巨大。只有通过CDN将用户的网络通信请求就近返回才能缓解数据中心的带宽压力。
App请求获取视频数据流的时候会优先检查离自己比较近的CDN中是否有视频数据。如果有直接从CDN加载数据如果没有才会从QuickTok数据中心获取视频数据流。
如果用户的大部分请求都可以通过CDN返回那么一方面可以极大加快用户请求的响应速度另一方面又可以较大缓解数据中心的网络和硬盘负载压力进一步提升应用整体的性能。
通常的CDN设计是在CDN中没有用户请求的数据时进行回源即由CDN请求数据中心返回需要的数据然后缓存在CDN本地。
但QuickTok考虑到了短视频的特点大V、网红们发布的短视频会被更快速、更广泛地播放。因此针对粉丝量超过10万的用户系统将采用主动推送CDN的方法以提高CDN的命中率优化用户体验如图
从图中可以看出视频内容处理器进行完视频处理后一方面会将视频存储到前面说过的视频存储系统中另一方面又会调用CDN推送服务。然后CDN推送服务将调用大数据平台获取视频上传者的活跃粉丝数、粉丝分布区域等数据。如果是10万粉丝以上的用户发布了短视频CDN推送服务会根据其粉丝活跃的区域将视频推送到对应区域的CDN服务器上。
短视频的完播率通常不足30%所以QuickTok也不需要将完整视频推送到CDN只需要根据视频发布者的历史播放记录计算其完播率和播放期望进度然后将短视频切分成若干chunk将部分chunk推送到CDN即可。
业界一般共识视频应用CDN处理的带宽大约占总带宽的95%以上也就是说通过合理使用CDNQuickTok数据中心需要处理的带宽压力不到4Tb。
缩略图生成与推荐设计
用户可以通过App主页、搜索结果页、视频推荐页等页面看到视频列表其中每个视频都需要有个缩略图。用户点击缩略图就开始播放视频。
缩略图通常是由视频的某一帧画面缩略而生成的。事实上缩略图的选择会极大地影响用户点击、播放视频的意愿。一个10分钟的视频大约包含3万帧画面选择哪一帧画面才能使用户点击视频的可能性最大以及针对不同的用户分类是否选择不同的缩略图会产生更高的点击率
我们需要通过大数据平台的机器学习引擎来完成缩略图的生成和推荐,如下图。
缩略图的生成和推荐可以分为两个具体过程:
实时在线的缩略图推荐过程a
利用离线机器学习生成优质缩略图的过程b。
a过程中用户通过搜索引擎搜索视频搜索引擎产生搜索结果视频列表后根据视频ID从缩略图存储中获取对应的缩略图。
但是一个视频可能对应很多个缩略图如果想要显示最吸引当前用户的那个搜索引擎就需要调用QuickTok大数据平台的缩略图推荐引擎进行推荐。
推荐引擎可以获取当前用户的偏好特征标签以及视频对应的多个缩略图特征使用XGboost算法训练好的模型将用户特征标签和缩略图特征进行匹配然后返回最有可能被当前用户点击的缩略图ID。搜索引擎再按照ID将对应的缩略图构建到搜索结果页面返回给用户。
用户浏览搜索结果列表点击某些缩略图进行播放。App应用会将用户的浏览与点击数据发送给QuickTok大数据平台这样就进入了利用机器学习来生成优质缩略图的过程b。
机器学习系统获取到了海量用户的浏览和点击数据,同时获取每个缩略图的特征。一方面,机器可以学习到,哪些特征的缩略图更容易获得用户点击,从而生成优质缩略图特征标签库;另一方面,机器还可以学习到每个用户自身更偏好的图像特征标签,供前面提到的推荐引擎使用。
有了机器学习系统的加持,视频内容处理器就可以使用优质特征标签库来处理上传的视频内容,抽取符合优质特征的帧,进而生成缩略图。
以上的a、b两个过程不断循环迭代系统就可以不断优化优质特征标签库不断使缩略图更符合用户喜好。
那最开始没有特征库的时候怎么办呢?视频内容处理器可以使用随机的办法,抽取一些帧作为缩略图,进行冷启动。机器学习再从这些随机抽取的缩略图上开始学习,从而进入循环优化过程。
小结
我们在缩略图生成部分,使用了大数据和机器学习的一些技术,如果你不熟悉,可能会觉得有点困难。但是现在人工智能和机器学习几乎是稍具规模的互联网系统的标配,架构师作为整个系统的设计者、技术负责人,可能对算法的细节无法做出具体的优化,但是对于算法在整个架构中的作用、相关数据的处理和流转必须非常熟悉,才能设计出满足业务需要的架构方案。
所以大数据和机器学习的原理和应用方法应该是架构师技能栈的一部分能够和算法工程师顺畅讨论技术细节是架构师必备的能力。如果你对这部分知识掌握不完整可以阅读专栏《从0开始学大数据》。
思考题
不止是缩略图的选择需要用到推荐算法视频内容本身也需要推荐算法当用户播放完一个视频QuickTok需要给用户自动播放下一个视频以此增强用户粘性。那么下一个视频应该播放什么你是否可以参考文中的缩略图生成与推荐架构图自己画一个视频推荐的架构图如果能说说你的设计思路就更好了。
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。

View File

@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 海量数据处理技术回顾:为什么分布式会遇到 CAP 难题?
你好,我是李智慧。
在这个模块的几个案例中,我们都需要处理海量的数据,需要用到海量的存储介质,其实海量数据本质上就是一种磁盘资源敏感的高并发场景。
我们说过为了应对资源不足的问题我们常采用水平伸缩即分布式的方案。数据存储的分布式问题是所有分布式技术中最具挑战性的因为相对于“无状态”stateless的计算逻辑可执行程序数据存储是“有状态”stateful的。无状态的计算逻辑可以在任何一台服务器执行而结果不会改变但有状态的数据却意味着数据存储和计算资源的绑定每一个数据都需要存储在特定的服务器上如果再增加一台空的服务器它没有数据也就无法提供数据访问无法实现伸缩。
数据存储的“有状态”特性还会带来其他问题:为了保证数据存储的可靠性,数据必须多备份存储,也就是说,同一个数据需要存储在多台服务器上。那么又如何保证多个备份的数据是一致的?
因此,海量数据存储的核心问题包括:如何利用分布式服务器集群实现海量数据的统一存储?如何正确选择服务器写入并读取数据?为了保证数据的高可用性,如何实现数据的多备份存储?数据多备份存储的时候,又如何保证数据的一致性?
为了解决这些问题在这个模块的案例设计中我们使用了多个典型的分布式存储技术方案分布式文件系统HDFS、分布式NoSQL数据库HBase、分布式关系数据库。下面我们就来回顾这几个典型技术方案。你可以再重新审视一下我们案例中的技术选型是否恰当是否有改进的空间。
HDFS
这个模块中我们用HDFS作为短URL、爬虫下载文件、短视频文件的存储方案。
HDFS即Hadoop分布式文件系统其架构如下。
HDFS的关键组件有两个一个是NameNode另一个是DataNode。
NameNode负责整个分布式文件系统的元数据管理也就是文件路径名、访问权限、数据块ID、存储位置等信息。而DataNode负责文件数据的存储和读写操作HDFS将文件数据分割成若干数据块Block每个DataNode存储一部分数据块这样文件就分布存储在了整个HDFS服务器集群中。
HDFS集群会有很多台DataNode服务器一般几百到几千不等每台服务器配有数块硬盘整个集群的存储容量大概在几PB到数百PB。通过这种方式HDFS可以存储海量的文件数据。
HDFS为了保证数据的高可用会将一个数据块复制为多份缺省情况为3份并将多份相同的数据块存储在不同的服务器上甚至不同的机架上。这样当有硬盘损坏或者某个DataNode服务器宕机甚至某个交换机宕机导致其存储的数据块不能访问的时候客户端会查找其备份的数据块进行访问。
HDFS的典型应用场景是大数据计算即使用MapReduce、Spark这样的计算框架来计算存储在HDFS上的数据。但是作为一个经典的分布式文件系统我们也可以把HDFS用于海量文件数据的存储与访问就像我们在这个模块的案例中那样。
分布式关系数据库
我们在[网盘案例]中使用了分片的关系数据来存储元数据信息。这是因为关系数据存在存储结构的限制使用B+树存储表数据),通常一张表的存储上限是几千万条记录。而在网盘的场景中,元数据在百亿以上,所以我们需要将数据分片存储。
分片的关系数据库,也被称为分布式关系数据库。也就是说,将一张表的数据分成若干片,其中每一片都包含了数据表中一部分的行记录,然后将每一片存储在不同的服务器上,这样一张表就存储在多台服务器上了。通过这种方式,每张表的记录数上限可以突破千万,保存百亿甚至更多的记录。
最简单的数据库分片存储可以采用硬编码的方式我们在程序代码中直接指定把一条数据库记录存放在哪个服务器上。比如像下图这样要将用户表分成两片存储在两台服务器上那么我们就可以在程序代码中根据用户ID进行分片计算把ID为偶数如94的用户记录存储到服务器1ID为奇数如33的用户记录存储到服务器2。
但是硬编码方式的缺点比较明显。如果要增加服务器那么就必须修改分片逻辑代码这样程序代码就会因为非业务需求产生不必要的变更其次分片逻辑会耦合在业务逻辑的程序代码中修改分片逻辑或业务逻辑都可能影响另一部分代码从而出现Bug。
我们可以使用分布式关系数据库中间件来解决这个问题在中间件中完成数据的分片逻辑这样对应用程序是透明的。我们常用的分布式关系数据库中间件是MyCAT原理如下图。
MyCAT是针对MySQL数据库设计的应用程序可以像使用MySQL数据库一样连接MYCAT提交SQL命令。MyCAT在收到SQL命令以后查找配置的分片逻辑规则。
比如上图中我根据地区进行数据分片把不同地区的订单存储在不同的数据库服务器上。那么MyCAT就可以解析出SQL中的地区字段prov根据这个字段连接相对应的数据库服务器。例子中SQL的地区字段是“wuhan”而在MyCAT中配置“wuhan”对应的数据库服务器是dn1所以用户提交的这条SQL最终会被发送给DB1@Mysql1数据库进行处理
HBase
分布式关系数据库可以解决海量数据的存储与访问但是关系数据库本身并不是分布式的需要通过中间件或者硬编码的方式进行分片这样对开发和运维并不友好于是人们又设计出了一系列天然就是分布式的数据存储系统。因为这些数据存储系统通常不支持关系数据库的SQL语法所以它们也被称为NoSQL数据库。
HBase就是NoSQL数据库中较为知名的一个产品。我们的短URL数据存储、短视频缩略图存储都使用了HBase作为存储方案。上面网盘元数据存储方案使用了分布式关系数据库事实上使用HBase这样的NoSQL数据库会是更好的方案。HBase架构如下。
HRegion是HBase中负责数据存储的主要进程应用程序对数据的读写操作都是通过和HRetion通信完成的。也就是说应用程序如果想要访问一个数据必须先找到HRegion然后将数据读写操作提交给HRegion而HRegion最终将数据存储到HDFS文件系统中。由于HDFS是分布式、高可用的所以HBase的数据存储天然是分布式、高可用的。
因此HBase的设计重点就是HRegion的分布式。HRegionServer是物理服务器这些服务器构成一个分布式集群每个HRegionServer上可以启动多个HRegion实例。当一个 HRegion中写入的数据太多达到配置的阈值时一个HRegion会分裂成两个HRegion并将HRegion在整个集群中进行迁移以使HRegionServer的负载均衡进而实现HRegion的分布式。
应用程序如果想查找数据记录需要使用数据的key。每个HRegion中存储一段Key值区间[key1, key2)的数据而所有HRegion的信息包括存储的Key值区间、所在HRegionServer地址、访问端口号等都记录在HMaster服务器上。因此应用程序要先访问HMaster服务器得到数据key所在的HRegion信息再访问对应的HRegion获取数据。为了保证HMaster的高可用HBase会启动多个HMaster并通过ZooKeeper选举出一个主服务器。
ZooKeeper
我们在上面提到分布式数据存储为了保证高可用需要对数据进行多备份存储但是多份数据之间可能无法保证数据的一致性这就是著名的CAP原理。
CAP原理认为一个提供数据服务的分布式系统无法同时满足数据一致性Consistency、可用性Availibility、分区耐受性Patition Tolerance这三个条件如下图所示。
其中,一致性的意思是,每次读取数据,要么读取到最近写入的数据,要么返回一个错误,而不是过期数据,这样就能保证数据一致。
可用性的意思是,每次请求都应该得到一个响应,而不是返回一个错误或者失去响应,不过这个响应不需要保证数据是最近写入的。也就是说,系统需要一直都能正常使用,不会引起调用者的异常,但是并不保证响应的数据是最新的。
分区耐受性的意思是,即使因为网络原因,部分服务器节点之间消息丢失或者延迟了,系统依然应该是可以操作的。
当网络分区失效发生时,要么我们取消操作,保证数据一致性,但是系统却不可用;要么我们继续写入数据,但是数据的一致性就得不到保证。
对于一个分布式系统而言网络失效一定会发生也就是说分区耐受性是必须要保证的那么可用性和一致性就只能二选一这就是CAP原理。
由于互联网对高可用的追求大多数分布式存储系统选择可用性而放松对一致性的要求。而ZooKeeper则是一个保证数据一致性的分布式系统它主要通过一个ZAB算法Zookeeper Atomic Broadcast Zookeeper原子广播实现数据一致性算法过程如下。
ZooKeeper集群由多台服务器组成为了保证多台服务器上存储的数据是一致的ZAB需要在这些服务器中选举一个Leader所有的写请求都必须提交给Leader。Leader服务器会向其他服务器Follower发起Propose通知所有服务器“我们要完成一个写操作请求请大家检查自己的数据状态是否有问题。”
如果所有Follower服务器都回复Leader服务器ACK即没有问题那么Leader服务器会向所有Follower发送Commit命令要求所有服务器完成写操作。这样包括Leader服务器在内的所有ZooKeeper集群服务器的数据就都更新并保持一致了。如果有两个客户端程序同时请求修改同一个数据因为必须要经过Leader的审核而Leader只接受其中一个请求数据也会保持一致。
在实际应用中客户端程序可以连接任意一个Follower进行数据读写操作。如果是写操作那么这个请求会被Follower发送给Leader进行如上所述的处理如果是读操作因为所有服务器的数据都是一致的那么这个Follower直接把自己本地的数据返回给客户端就可以了。
因为ZooKeeper具有这样的特性所以很多分布式系统都使用ZooKeeper选择主服务器。为了保证系统高可用像HDFS中的NameNode或者HBase中的HMaste都需要主主热备也就是多台服务器充当主服务器这样任何一台主服务器宕机都不会影响系统的可用性。
但是在运行期只能有一台主服务器提供服务否则系统就不知道该接受哪台服务器的指令即出现所谓的系统脑裂因此系统需要选举主服务器。而ZooKeeper的数据一致性特点可以保证只有一台服务器选举成功。在专栏后面的网约车架构案例中我们也使用了ZooKeeper进行服务器管理。
布隆过滤器
我们在[短URL生成]以及[网络爬虫的案例]中还使用了布隆过滤器检查内容是否重复即检查短URL或者网页内容的MD5是否已经存在。如果用Hash表检查重复千亿级的网页内容MD5就需要一个非常大的Hash表内存资源消耗非常大。而用布隆过滤器使用较小的内存就可以检查海量数据中一个数据是否存在。文件MD5重复性检查的布隆过滤器原理如下。
布隆过滤器首先开辟一块巨大的连续内存空间,比如开辟一个 1600G 比特的连续内存空间,也就是 200GB 大的一个内存空间,并将这个空间所有比特位都设置为 0。然后对每个MD5使用多种Hash算法比如使用 8 种Hash算法分别计算 8 个Hash值并保证每个Hash值是落在这个 1600G 的空间里的,也就是,每个 Hash 值对应 1600G 空间里的一个地址下标。然后根据计算出来的Hash值将对应的地址空间里的比特值设为 1这样一个MD5就可以将8个比特位设置为 1。
如果要检查一个MD5是否存在只需要让MD5重复使用这 8 个哈希算法计算出8个地址下标然后检查它们里面的二进制数是否全是 1如果是 那么表示这个MD5已经存在了。所以在海量MD5中检查一个MD5是否存在布隆过滤器会比哈希表更节约内存空间。
小结
因为数据存储是有状态的,所以海量数据存储的分布式架构要解决的核心问题就是:在一个有很多台服务器的分布式集群中,如何知道数据存储在哪台服务器上?
解决方案有两种一种是有专门的服务器记录数据存储在哪里即有一个元数据服务器。HDFS里的NameNode和HBase里的HMaster都是这样的角色。应用程序想访问数据需要先和元数据服务器通信获取数据存储的位置再去具体的数据存储服务器上访问数据。
另一种解决方案是通过某种算法计算要访问的数据的位置这种算法被称作数据路由算法。分片数据库的硬编码算法就是一种数据路由算法根据分片键计算该记录在哪台服务器上。MyCAT其实也是采用路由算法只不过将硬编码的分片逻辑记录在了配置文件中。
软件开发技术是一个快速发展的领域,各种新技术层出不穷,如果你只是被动地学习这些技术,很快就会迷失在各种技术细节里,疲惫不堪,最终放弃。事实上,每种技术的出现都因为要解决某个核心问题,最终诞生几种解决方案。同时,每种方案又会产生自己的新问题,比如分布式存储的数据的高可用,以及高可用带来的数据一致性,又需要产生相应的解决方案。
但是只要把握住核心问题和解决方案,就可以自己分析、推导各种衍生的问题和方案,思考各种优缺点和改进策略,最终理解、掌握一个新的技术门类。这不是通过辛苦学习,来掌握一个技术,而是从上帝视角,站在和这些技术的创造者一样的维度去思考,最终内化到自己的知识体系中。
思考题
分布式存储中有个非常著名的数据路由算法叫一致性Hash算法这个算法要解决的问题是什么解决的思路是什么
欢迎在评论区分享你的思考,我们共同进步。

View File

@ -0,0 +1,130 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 秒杀系统设计:你的系统可以应对万人抢购盛况吗?
你好,我是李智慧。
秒杀是电子商务应用常见的一种营销手段将少量商品常常只有一件以极低的价格在特定的时间点出售。比如周日晚上8点整开售1部1元钱的手机。 因为商品价格诱人,而且数量有限,所以用户趋之若鹜,在秒杀活动开始前涌入系统, 等到秒杀活动开始的一瞬间,点下购买按钮(在此之前购买按钮为灰色,不可以点击),抢购商品。
秒杀虽然对应用推广有很多好处,但是对系统技术却是极大的挑战:系统是为正常运营设计的,而秒杀活动带来的并发访问用户却是平时的数百倍甚至上千倍。也就是说,秒杀的时候,系统需要承受比平时多得多的负载压力。
为了应对这种比较特殊的营销活动我们启动了一个专门的秒杀项目项目代号是“Apollo”。Apollo的核心挑战是如何应对突然出现的数百倍高并发访问压力并保证用户只有在秒杀开始时才能下单购买秒杀商品接下来我们就看看Apollo的需求与技术架构吧。
需求分析
Apollo的需求主要有两点。
独立开发部署秒杀系统,避免影响现有系统和业务
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短、瞬间并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个系统瘫痪。
而且由于秒杀时的最高并发访问量巨大,整个电商系统需要部署比平常运营多好几倍的服务器,而这些服务器在绝大部分时候都是用不着的,浪费惊人。所以秒杀业务不能使用正常的电商业务流程,也不能和正常的网站交易业务共用服务器,甚至域名也需要使用自己独立的域名。总之,我们需要设计部署专门的秒杀系统,进行专门应对。
防止跳过秒杀页面直接下单
秒杀的游戏规则是:到了秒杀时间才能开始对商品下单购买。在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的 URL如果得到这个 URL不用等到秒杀开始就可以下单了。秒杀系统 Apollo 必须避免这种情况。
概要设计
Apollo要解决的核心问题有
如何设计一个独立于原有电子商务系统的秒杀系统,并独立部署。
这个秒杀系统如何承受比正常情况高数百倍的高并发访问压力。
如何防止跳过秒杀页面获得下单URL。-
我们将讨论这三个问题的解决方案,并设计秒杀系统部署模型。
独立秒杀系统页面设计
秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是精细的商品描述等用户体验细节,因此秒杀系统的页面设计应尽可能简单。秒杀商品页面如图。
商品页面中的购买按钮只有在秒杀活动开始时才变亮,在此之前以及秒杀商品卖出后,该按钮都是灰色的,不可以点击。 秒杀时间到,购买按钮点亮,点击后进入下单页面,如图。
下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;只有第一个提交的订单发送给订单子系统,才能成功创建订单,其余用户提交订单后只能看到秒杀结束页面。
秒杀系统只需要设计购买和下单两个页面就可以了,因为不管有多少用户来参与秒杀,只有第一个提交下单的用户才能秒杀成功,因此提交订单并创单成功的用户只有一个,这个时候就没有什么高并发了。所以订单管理、支付以及其他业务都可以使用原来的系统和功能。
秒杀系统的流量控制
高并发的用户请求会给系统带来巨大的负载压力,严重的可能会导致系统崩溃。虽然我们设计并
部署了独立的秒杀系统,秒杀时的高并发访问压力只会由秒杀系统承担,不会影响到主站的电子商务核心系统,但是秒杀系统的高并发压力依然不容小觑。
此外秒杀系统为了提高用户参与度和可玩性秒杀开始的时候浏览器或App并不会自动点亮购买按钮而是要求用户不停刷新页面使用户保持一个高度活跃的状态。但是这样一来用户在秒杀快要开始的时候拼命刷新页面会给系统带来更大的高并发压力。
我们知道缓存是提高响应速度、降低服务器负载压力的重要手段。所以控制访问流量、降低系统负载压力的第一个设计方案就是使用缓存。Apollo采用多级缓存方案可以更有效地降低服务器的负载压力。
首先浏览器尽可能在本地缓存当前页面页面本身的HTML、JavaScript、CSS、图片等内容全部开启浏览器缓存刷新页面的时候浏览器事实上不会向服务器提交请求这样就避免了服务器的访问负载压力。
其次秒杀系统还使用CDN缓存。CDN即内容分发网络是由网络运营服务商就近为用户提供的一种缓存服务。秒杀相关的HTML、JavaScript、CSS、图片都可以缓存到CDN中秒杀开始前即使有部分用户新打开浏览器也可以通过CDN加载到这些静态资源不会访问服务器又一次避免了服务器的访问负载压力。
同样秒杀系统中提供HTML、JavaScript、CSS、图片的静态资源服务器和提供商品浏览的秒杀商品服务器也要在本地开启缓存功能进一步降低服务器的负载压力。
使用多级缓存的秒杀系统部署图如下。
以上是针对秒杀开始前,缓存可以降低用户频繁刷新给服务器造成的流量压力。但是秒杀开始后,用户购买和下单的并发请求就不能使用缓存了,但我们仍然需要对高并发的请求流量进行控制。因此,秒杀开始后,秒杀系统会使用一个计数器对并发请求进行限流处理,如下图。
因为最终成功秒杀到商品的用户只有一个,所以需要在用户提交订单时,检查是否已经有其他用户提交订单。事实上,为了减轻下单页面服务器的负载压力,可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户则直接进入秒杀结束页面。假设下单服务器集群有 10 台服务器每台服务器只接受最多10个下单请求这样整个系统只需要承受100并发就可以了而秒杀成功的用户也只能出现在这100并发请求中。
事实上,限流是一种非常常用的高并发设计方案,我们会在下个模块专门设计一个通用的限流器。通过缓存和限流这两种设计方案,已经可以应对绝大多数情况下秒杀带来的高并发压力。
秒杀商品页面购买按钮点亮方案设计与下单URL下发
前面说过购买按钮只有在秒杀活动开始时才能点亮在此之前是灰色的。如果该页面是动态生成的当然可以在服务器端构造响应页面输出控制该按钮是灰色还是点亮。但是在前面的设计中为了减轻服务器端负载压力更好地利用CDN、反向代理等性能优化手段该页面被设计成了静态页面缓存在 CDN、秒杀商品服务器甚至用户浏览器上。秒杀开始时用户刷新页面请求根本不会到达应用服务器。
因此,我们需要在秒杀商品静态页面中加入一个特殊的 JavaScript 文件这个JavaScript 文件设置为不被任何地方缓存。秒杀未开始时该JavaScript文件内容为空。当秒杀开始时定时任务会生成新的 JavaScript 文件内容并推送到JavaScript服务器。
新的JavaScript文件包含了秒杀是否开始的标志和下单页面 URL 的随机数参数。当用户刷新页面时新JavaScript文件会被用户浏览器加载根据JavaScript中的参数控制秒杀按钮的点亮。当用户点击按钮时提交表单的URL参数也来自这个JavaScript文件如图。
这个JavaScript文件还有一个优点那就是它本身非常小即使每次浏览器刷新都访问 JavaScript 文件服务器,也不会对服务器集群和网络带宽造成太大压力。
秒杀系统部署模型
综上设计方案Apollo 系统整体部署模型如下。
用户在浏览器打开秒杀商品页面浏览器检查本地是否有缓存该商品信息。如果没有就通过CDN加载如果CDN也没有就访问秒杀商品服务器集群。
用户刷新页面时除了特殊JavaScript文件其他页面和资源文件都可以通过缓存获得秒杀没开始的时候特殊JavaScript文件内容是空的所以即使高并发也没有什么负载和带宽访问压力。秒杀开始时定时任务服务器会推送一个包含点亮按钮指令和下单URL内容的新JavaScript文件用来替代原来的空文件。用户这时候再刷新就会加载该新的JavaScript文件使购买按钮点亮并能进入下单页面。
下单URL中会包含一个随机数这个随机数也会由定时任务推送给下单服务器下单服务器收到用户请求的时候检查请求中包含的随机数是否正确即检查该请求是否是伪造的。
进入下单服务器的请求会被服务器进行限流处理每台服务器超过10个的请求会被重定向到秒杀结束页面。只有前十个请求返回下单页面。用户填写下单页面并提交到下单服务器后需要通过全局计数器进行计数。全局计数器会根据秒杀商品库存数量确定允许创单的请求个数超过这个数目的请求也将重定向到秒杀结束页面。最终只有有限的几个用户能够秒杀成功进入订单处理子系统完成交易。
小结
这个文档是根据某互联网大厂真实案例改编的。当年该厂为了配合品牌升级,搞了一次大规模的营销活动,秒杀是整个营销活动的一部分。运营团队在投放了大量广告并确定了秒杀活动的开始时间后才通知技术部:我们准备在一周后搞一个秒杀活动,预计参加秒杀的人数是正常访问人数的几百倍。
当时参加会议的架构师们面面相觑,时间太短,并发量太高,谁也不敢贸然接手。最后有个架构师站出来接手了这个项目,并最终完成了秒杀活动。此后,这名架构师成了公司的红人,短短几年晋升为集团副总裁,负责一个有十多亿用户、几乎所有中国人都耳熟能详的互联网应用。
我们现在重新把这个设计拿出来复盘,看起来技术含量也不过如此。那么如果把你放到当时的会议现场,你是否有勇气站出来说:“我来。”
对一个架构师而言,精通技术是重要的,而用技术建立起自己的信心,在关键时刻有勇气面对挑战更重要。人生的道路虽然漫长,但是紧要处可能只有几秒。这几秒是秒杀系统高并发访问高峰的那几秒,也是面对挑战迎难而上站出来的那几秒。
思考题
Apollo秒杀系统针对的是大量用户在短时间购买极少数商品的情况通过限流器拦截大量用户请求进而降低系统负载压力的设计思路。那么对于大量用户在短时间购买大量商品的情况比如双十一这种电商大促场景设计方案又非常不同你有什么样的设计思路呢
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。

View File

@ -0,0 +1,171 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 交友系统设计:哪种地理空间邻近算法更快?
你好,我是李智慧。
交友与婚恋是人们最基本的需求之一。随着互联网时代的不断发展,移动社交软件已经成为了人们生活中必不可少的一部分。然而,熟人社交并不能完全满足年轻人的社交与情感需求,于是陌生人交友平台悄然兴起。
我们决定开发一款基于地理位置服务LBS的应用为用户匹配邻近的、互相感兴趣的好友应用名称为“Liao”。
Liao面临的技术挑战包括面对海量的用户如何为其快速找到邻近的人可以选择的地理空间邻近算法有哪些Liao如何在这些算法中选择出最合适的那个
需求分析
Liao的客户端是一个移动App用户打开App后上传、编辑自己的基本信息然后系统推荐算法根据其地理位置和个人信息为其推荐位置邻近的用户。用户在手机上查看对方的照片和资料如果感兴趣希望进一步联系就向右滑动照片如果不感兴趣就向左滑动照片。
如果两个人都向右滑动了对方,就表示他们互相感兴趣。系统就通知他们配对成功,并为他们开启聊天功能,可以更进一步了解对方,决定是否建立更深入的关系。
Liao的功能用例图如下。
用户规模分析
Liao的目标用户是全球范围内的中青年单身男女预估目标用户超过10亿系统按10亿用户进行设计。
概要设计
Liao的系统架构采用典型的微服务架构设计方案用户通过网关服务器访问具体的微服务如下图。
由上图可知首先用户所有请求都通过统一的网关服务器处理。网关服务器负责限流、防攻击、用户身份识别及权限验证、微服务调用及数据聚合封装等而真正的业务逻辑则通过访问微服务来完成。Liao的关键微服务有用户微服务、图片微服务、配对微服务、聊天微服务、推荐微服务、邻近算法微服务等。Liao的网关预计将承担每天百亿次规模的访问压力。
用户微服务管理用户的个人信息、兴趣爱好以及交友偏好等此外也负责用户登录服务只有登录用户才能访问系统。因为需要存储十亿条用户数据所以用户数据库采用分片的MySQL数据库。
图片微服务用于管理用户照片提供用户照片存储及展示的功能。Liao需要存储的图片数大约几百亿张。我们使用Nginx作为图片服务器图片服务器可以线性扩容每写满一台服务器及其Slave服务器就继续写入下一台服务器。服务器IP、图片路径则记录在用户数据库中。同时购买CDN服务缓存热门的用户照片。
配对微服务负责将互相喜欢的用户配对,通知用户,并加入彼此的通讯录中。用户每次右划操作都调用该微服务。系统设置一个用户每天可以喜欢(右划)的人是有上限的,但是,对于活跃用户而言,长期积累下来,喜欢的人的数量还是非常大的,因此配对微服务会将数据发送给一个流式大数据引擎进行计算。
推荐微服务负责向用户展示其可能感兴趣的、邻近的用户。因此,一方面,推荐微服务需要根据用户操作、个人兴趣、交友偏好调用协同过滤等推荐算法进行推荐,另一方面必须保证推荐的用户在当前用户的附近。
详细设计
详细设计主要关注邻近位置算法,也就是,如何根据用户的地理位置寻找距其一定范围内的其他用户。
我们可以通过Liao App获取用户当前经、纬度坐标然后根据经、纬度计算两个用户之间的距离距离计算公式采用半正矢公式
其中 r 代表地球半径,\(\\small \\varphi\)表示纬度,\(\\small \\lambda\)表示经度。
但是当我们有10亿用户的时候如果每次进行当前用户匹配都要和其他所有用户做一次距离计算然后再进行排序那么需要的计算量至少也是千亿级别这样的计算量是我们不能承受的。通常的空间邻近算法有以下4种我们一一进行分析最终选择出最合适的方案。
SQL邻近算法
我们可以将用户经、纬度直接记录到数据库中纬度记录在latitude字段经度记录在longitude字段用户当前的纬度和经度为XY如果我们想要查找和当前用户经、纬度距离D之内的其他用户可以通过如下SQL实现。
select * from users where latitude between X-D and X+D and longitude between Y-D and Y+D;
这样的SQL实现起来比较简单但是如果有十亿用户数据分片在几百台服务器上SQL执行效率就会很低。而且我们用经、纬度距离进行近似计算在高纬度地区这种近似计算的偏差还是非常大的。
同时“between X-D and X+D”以及“between Y-D and Y+D”也会产生大量中间计算数据这两个betwen会先返回经度和纬度各自区间内的所有用户再进行交集and处理如下图。
我们的用户量非常大,而计算邻近好友又是一个非常高频的访问,同时,分片数据库进行集合计算需要在中间代理服务器或应用程序服务器完成计算,因此,这样的交集计算带来计算负载压力是我们的系统完全不能承受的。所以这个方案可以被放弃。
地理网格邻近算法
为了减少上述交集计算使用的中间数据量,我们将整个地球用网格进行划分,如下图。
事实上我们划分的网格远比图中示意的要密集得多赤道附近经、纬度方向每10公里一个网格。
这样每个用户必然会落入到一个网格中我们在用户表中记录用户所在的网格IDgridID然后借助这个字段进行辅助查找将查找范围限制在用户所在的网格gridIDx0及其周围8个网格gridIDx1 ~ gridIDx8可以极大降低中间数据量SQL如下。
select * from users where latitude between X-D and X+D and longitude between Y-D and Y+D and gridID in (gridIDx0,gridIDx1,gridIDx2,gridIDx3,gridIDx4,gridIDx5,gridIDx6,gridIDx7,gridIDx8);
这条SQL要比上面SQL的计算负载压力小得多但是对于高频访问的分片数据库而言用这样的SQL进行邻近好友查询依然是不能承受的同样距离精度也不满足要求。
但是基于这种网格设计思想我们发现我们可以不通过数据库就能实现邻近好友查询我们可以将所有的网格及其包含的用户都记录在内存中。当我们进行邻近查询时只需要在内存中计算用户及其邻近的8个网格内的所有用户的距离即可。
我们可以估算下所有用户经、纬度都加载到内存中需要的内存量:\(\\small 1G\\times3\\times4B=12GB\)用户ID、经度、纬度都采用4个字节编码总用户数1G。这个内存量是完全可以接受的。
实际上通过恰当地选择网格的大小我们不停访问当前用户位置周边的网格就可以由近及远不断得到邻近的其他用户而不需要再通过SQL来得到。那么如何选择网格大小如何根据用户位置得到其所在的网格又如何得到当前用户位置周边的其他网格呢我们看下实践中更常用的动态网格和GeoHash算法。
动态网格算法
事实上,不管如何选择网格大小,可能都不合适。因为在陆家嘴即使很小的网格可能就包含近百万的用户,而在可可西里,非常大的网格也包含不了几个用户。
因此,我们希望能够动态设定网格的大小,如果一个网格内用户太多,就把它分裂成几个小网格,小网格内如果用户还是太多,继续分裂更小的网格,如下图。
这是一个四叉树网格结构开始的时候整个地球只有一个网格当用户增加超过阈值500个用户的时候就分裂出4个子树4个子树对应父节点网格的4个地理子网格。同时将用户根据位置信息重新分配到4个子树中。同样如图中所示如果某个子树中的用户增加超过了阈值该子树继续分裂成4个子树。
因此我们可以将全球用户分配在这样一个4叉树网格结构中所有的用户都必然在这个4叉树的叶子节点中而且每个节点内包含的用户数不超过500个。那么陆家嘴的网格可能就会很小而可可西里的网格就会很大太平洋对应的网格可能有几千公里。
当给定当前用户的经、纬度查询邻近用户的时候首先从根节点开始查找如果根节点就是叶子节点那么直接遍历根节点中的所有用户计算距离即可。如果根节点不是叶子节点那么根据给定的经、纬度判断其在网格中的位置左上、右上、右下、左下4个位置顺序对应4个子树根据网格位置访问对应的子树。如果子树是叶子节点那么在叶子节点中查找如果不是叶子节点继续上面的过程直到叶子节点。
上面的过程只能找到当前用户所在网格的好友如何查找邻近网格的其他用户呢事实上我们只需要将4叉树所有的叶子节点顺序组成一个双向链表每个节点在链表上的若干个前驱和后继节点正好就是其地理位置邻近的节点。
动态网格也叫4叉树网格在空间邻近算法中较为常用也能满足Liao的需求。但是编程实现稍稍有点麻烦而且如果网格大小设计不合适导致树的高度太高每次查找需要遍历的路径太长性能结果也比较差。我们再看下性能和灵活性更好的GeoHash算法。
GeoHash算法
除了动态网格算法GeoHash事实上是另外一种变形了的网格算法同时也是Redis中Geo函数使用的算法。GeoHash是将网格进行编码然后根据编码进行Hash存储的一种算法。
经、纬度数字的不同精度意味着经、纬度的误差范围比如保留经、纬度到小数点后第1位那么误差范围最大可能会达到11公里在赤道附近。也就是说小数点后1位精度的经、纬度其覆盖范围是一个11km * 11km的网格。
那么我们用小数点后1位精度的经、纬度做key网格内的用户集合做value就可以构建一个Hash表的对。通过查找这个KV对及其周围8个网格的KV对计算这些value内所有用户和当前用户的距离就可以找到邻近11公里内的所有用户。
实践中redis的GeoHash并不会直接用经、纬度做key而是采用一种基于Z阶曲线的编码方式将二维的经、纬度转化为一维的二进制数字再进行base32编码具体过程如下。
首先,分别针对经度和纬度,求取当前区间(对于纬度而言,开始的区间就是[-90, 90], 对于经度而言,开始区间就是[-180, 180]的平均值将当前区间分为两个区间。然后用用户的经、纬度和区间平均值进行比较用户经、纬度必然落在两个区间中的一个如果大于平均值那么取1如果小于平均值那么取0。继续求取当前区间的平均值进一步将当前区间分为两个区间。如此不断重复可以在经度和纬度方向上得到两个二进制数。这个二进制数越长其所在的区间越小精度越高。
下图表示经、纬度的二进制编码过程最终得到纬度12位编码经度13位编码。
得到两个二进制数后,再将它们合并成一个二进制数。合并规则是,从第一位开始,奇数位为经度,偶数位为纬度,上面例子合并后的结果为 01101 11111 11000 00100 00010 共25位二进制数。
将25位二进制数划分成5组每组5个二进制数对应的10进制数是0-31采用Base32编码可以得到一个5位字符串Base32编码表如下。
编码计算过程如下。
最后得到一个字符串“ezs42”作为Hash表的key。25位二进制的GeoHash编码其误差范围大概2.4公里,即对应一个\(\\small 2.4km\\times2.4km\)的网格。网格内的用户都作为value放入到Hash表中。
一般说来通过选择GeoHash的编码长度实现不同大小的网格就可以满足我们邻近交友的应用场景了。但是在Redis中需要面对更通用的地理位置计算场景所以Redis中的GeoHash并没有用Hash表存储而是用跳表存储。
Redis使用52位二进制的GeoHash编码误差范围0.6米。Redis将编码后的二进制数按照Z阶曲线的布局进行一维化展开。即将二维的经、纬度上的点用一条Z型曲线连接起来Z阶曲线布局示例如下图。
事实上所谓的Z阶曲线布局本质其实就是基于GeoHash的二进制排序。将这些经过编码的2进制数据用跳表存储。查找用户的时候可以快速找到该用户沿着跳表前后检索得到的就是邻近的用户。
Liao的最终算法选择
Liao的邻近算法最终选择使用Hash表存储的GeoHash算法经度采用13bit编码纬度采用12bit编码即最后的GeoHash编码5个字符每个网格\(\\small 4.9km\\times4.9km\\approx 25km^{2}\),将整个地球分为\(\\small 2^{25}\\approx3300万\)个网格去掉海洋和几乎无人生存的荒漠极地需要存储的Hash键不到500万个采用Hash表存储。Hash表的key是GeoHash编码value是一个List其中包含了所有相同GeoHash编码的用户ID。
查找邻近好友的时候Liao将先计算用户当前位置的GeoHash值5个字符然后从Hash表中读取该Hash值对应的所有用户即在同一个网格内的用户进行匹配将满足匹配条件的对象返回给用户。如果一个网格内匹配的对象数量不足计算周围8个网格的GeoHash值读取这些Hash值对应的用户列表继续匹配。
小结
算法是软件编程中最有技术挑战性,也最能考验一个人编程能力的领域。所以很多企业面试的时候特别喜欢问算法类的问题,即使这些算法和将来的工作内容关系不大,面试官也可以凭借这些问题对候选人的专业能力和智力水平进行评判,而且越是大厂的面试越是如此。
架构和算法通常是一个复杂系统的一体两面架构是关于整体系统是如何组织起来的而算法则是关于核心功能如何处理的。我们专栏大多数案例也都体现了这种一体两面很多案例设计都有一两个核心算法比如短URL生成与预加载算法、缩略图生成与推荐算法、本篇的空间邻近算法以及下一篇要讲的倒排索引与PageRank算法都展现了这一点。
一个合格的架构师除了要掌握系统的整体架构,也要能把握住这些关键的算法,才能在系统的设计和开发中做到心中有数、控制自如。
思考题
本文的设计聚焦在邻近算法上所以忽略了常规的TPS、带宽负载、存储空间等性能指标你能否估计下这些性能指标
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。

View File

@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 搜索引擎设计:信息搜索怎么避免大海捞针?
你好,我是李智慧。
在[04讲]中我们讨论了大型分布式网络爬虫的架构设计但是网络爬虫只是从互联网获取信息海量的互联网信息如何呈现给用户还需要使用搜索引擎完成。因此我们准备开发一个针对全网内容的搜索引擎产品名称为“Bingoo”。
Bingoo的主要技术挑战包括
针对爬虫获取的海量数据,如何高效地进行数据管理;
当用户输入搜索词的时候,如何快速查找包含搜索词的网页内容;
如何对搜索结果的网页内容进行排序,使排在搜索结果列表前面的网页,正好是用户期望看到的内容。
概要设计
一个完整的搜索引擎包括分布式爬虫、索引构造器、网页排名算法、搜索器等组成部分Bingoo的系统架构如下。
分布式爬虫通过存储服务器将爬取的网页存储到分布式文件集群HDFS为了提高存储效率网页将被压缩后存储。存储的时候网页一个文件挨着一个文件地连续存储存储格式如下。
每个网页被分配得到一个8字节长整型docIDdocID之后用2个字节记录网页的URL的长度之后4个字节记录压缩后网页内容数据的长度所有存储的网页的头14个字节都是同样的格式。之后存储URL字符串和压缩后的网页内容数据。读取文件的时候先读14个字节的头信息根据头信息中记录的URL长度和数据长度再读取对应长度的URL和网页内容数据。
搜索引擎能够快速查找的核心就是利用索引根据用户的查询内容查找匹配的索引根据索引列表构建结果页面。索引的构造主要通过索引构造器完成索引构造器读取HDFS中的网页内容解压缩后提取网页中的单词构建一个“docID->单词列表”的正排索引。然后,索引构造器再根据这个正排索引构建一个“单词->docID列表”的倒排索引“docID列表”就是包含了这个单词的所有网页列表。利用这个倒排索引搜索器可以快速获得用户搜索词对应的所有网页。
网页中所有的单词构成了一个词典实际上词典就是一个Hash表key就是单词value就是倒排索引的网页列表。虽然互联网页的内容非常庞大但是使用到的单词其实是非常有限的。根据Google的报告256M内存可以存放1400万个单词这差不多就是英文单词的全部了。
在构建索引的过程中因为要不断修改索引列表还要进行排序所以有很多操作是需要进行加锁同步完成的。对于海量的互联网页的计算这样的索引构建速度太慢了。因此我们设计了64个索引桶根据docID取模将不同网页分配到不同的桶中在每个桶中分别进行索引构建通过并行计算来加快索引处理速度。
索引构造器在读取网页内容、构造索引的时候还会调用URL提取器将网页中包含的URL提取出来构建一个链接关系表。链接关系表的格式是“docID->docID”前一个docID是当前网页的docID后一个docID是当前网页中包含的URL对应的docID。一个网页中会包含很多个URL也就是会构建出很多个这样的链接关系。后面会利用这个链接关系表使用PageRank排名算法对所有网页进行打分排名当索引器得到查找的网页列表时利用PageRank值进行排名最终呈现给用户保证用户最先看到的网页是最接近用户期望的结果页面。
详细设计
一个运行良好的搜索引擎的核心技术就是索引和排名,所以我们将分别说明这两种技术要点。
索引
索引构造器从HDFS读取网页内容后解析每个页面提取网页里的每个单词。如果是英文那么每个单词都用空格分隔比较容易如果是中文需要使用中文分词器才能提取到每个单词比如“高并发架构”使用中文分词器得到的就是“高并发”、“架构”两个词。
首先索引构造器将所有的网页都读取完构建出所有的“docID->单词列表”正排索引。
然后遍历所有的正排索引再按照“单词→docID列表”的方式组织起来就是倒排索引了。
我们这个例子中只有两个单词、7个网页。事实上Bingoo数以千亿的网页就是这样通过倒排索引组织起来的网页数量虽然庞大但是单词数却是比较有限的。所以整个倒排索引的大小相比于网页数量要小得多。Bingoo将每个单词对应的网页列表存储在硬盘中而单词则存储在内存的Hash表也就是词典中词典示例
对于部分热门的单词整个网页列表也可以存储在内存中相当于缓存。在词典中每个单词记录下硬盘或者内存中的网页列表地址这样只要搜索单词就可以快速得到对应的网页地址列表。Bingoo根据列表中的网页编号docID展示对应的网页信息摘要就完成了海量数据的快速检索。
如果用户的搜索词正好是一个单词,比如“高并发”,那么直接查找词典,得到网页列表就完成查找了。但是如果用户输入的是一个句话,那么搜索器就需要将这句话拆分成几个单词,然后分别查找倒排索引。这样的话,得到的就是几个网页列表,还需要对这几个网页列表求交集,才能得到最终的结果列表。
比如,用户输入“高并发架构”进行搜索,那么搜索器就会拆分成两个词:“高并发”、“架构”,得到两个倒排索引:
高并发->2,3,5,7
架构->1,2,4
需要对这两个倒排索引求交集也就是同时包含“高并发”和“架构”的网页才是符合搜索要求的结果最终的交集结果应该是只有一篇网页即docID为2的满足要求。
列表求交集最简单的实现就是双层for循环但是这种算法的时间复杂度是O(n^2)我们的网页列表长度n可能有千万级甚至更高这样的计算效率太低。
一个改进的算法是拉链法我们将网页列表先按照docID的编号进行排序得到的就是这样两个有序链表
同时遍历两个链表如果其中一个链表当前指向的元素小于另一个链表当前指向的元素那么这个链表就继续向前遍历如果两个链表当前指向的元素相同该元素就是交集元素记录在结果列表中依此继续向前遍历直到其中一个链表指向自己的尾部nil。
拉链法的时间复杂度是O(2n),远优于双层循环。但是对于千万级的数据而言,还是太慢。我们还可以采用数据分片的方式进行并行计算,以实现性能优化。
比如我们的docID分布在[0, 1万亿)区间而每个倒排索引链表平均包含1千万个docID。我们把所有的docID按照1千亿进行数据分片就会得到10个区间[0, 1千亿)[1千亿2千亿)……[9千亿1万亿)。每个倒排索引链表大致均匀分布在这10个区间我们就可以依照这10个区间范围将每个要遍历的链表切分为10片每片大约包含1百万个docID。两个链表只在自己对应的分片内求交集即可因此我们可以启动10个线程对10个分片进行并行计算速度可提高10倍。
事实上两个1千万长度的链表求交集最终的结果可能不过几万也就是说大部分的比较都是不相等的。比如下面的例子。
第一个链表遍历到自己的最后一个元素,才和第二个链表的第一个元素相同。那么第一个链表能不能跳过前面那些元素呢?很自然,我们想到可以用跳表来实现,如下图。
跳表实际上是在链表上构建多级索引在索引上遍历可以跳过底层的部分数据我们可以利用这个特性实现链表的跳跃式比较加快计算速度。使用跳表的交集计算时间复杂度大约是O(log(n))。
此外,虽然搜索引擎利用倒排索引已经能很快得到搜索结果了,但搜索引擎应用还会使用缓存对搜索进行加速,将整个搜索词对应的搜索结果直接放入缓存,以减少倒排索引的访问压力,以及不必要的集合计算。
PageRank排名算法
Bingoo使用PageRank算法进行网页结果排名以保证搜索结果更符合用户期待。
PageRank算法会根据网页的链接关系给网页打分。如果一个网页A包含另一个网页B的超链接那么就认为A网页给B网页投了一票。一个网页得到的投票越多说明自己越重要越重要的网页给自己投票自己也越重要。
PageRank算法就是计算每个网页的PageRank值最终的搜索结果也是以网页的PageRank值排序展示给用户。事实证明这种排名方法非常有效PageRank值更高的网页确实更满足用户的搜索期望。
以下面四个网页A、B、C、D举例带箭头的线条表示链接。
B网页包含了A、D两个页面的超链接相当于B网页给A、D每个页面投了一票如果初始的时候所有页面都是1分那么经过这次投票后B给了A和D每个页面1/2分B包含了A、D两个超链接所以每个投票值1/2分自己从C页面得到1/3分C包含了A、B、D三个页面的超链接每个投票值1/3分
而A页面则从B、C、D分别得到1/21/31分。用公式表示就是
\(\\small PRA = \\frac{PRB}{2}+\\frac{PRC}{3}+\\frac{PRD}{1}\)
等号左边是经过一次投票后A页面的PageRank分值等号右边每一项的分子是包含A页面超链接的页面的PageRank分值分母是该页面包含的超链接数目。
这样经过一次计算后每个页面的PageRank分值就会重新分配重复同样的算法过程经过几次计算后根据每个页面PageRank分值进行排序就得到一个页面重要程度的排名表。根据这个排名表将用户搜索出来的网页结果排序排在前面的通常也正是用户期待的结果。
但是这个算法还有个问题如果某个页面只包含指向自己的超链接其他页面不断给它送分而自己一分不出随着计算执行次数越多它的分值也就越高这显然是不合理的。这种情况就像下图所示的A页面只包含指向自己的超链接。
解决方案是设想浏览一个页面的时候有一定概率不是点击超链接而是在地址栏输入一个URL访问其他页面表示在公式上就是
\(\\small PRA = \\alpha(\\frac{PRB}{2}+\\frac{PRC}{3}+\\frac{PRD}{1})+\\frac{1-\\alpha}{4}\)
上面\(\\small 1-\\alpha\)就是跳转到其他任何页面的概率通常取经验值0.15(即\(\\small \\alpha\) 为0.85)因为有一定概率输入的URL是自己的所以加上上面公式最后一项其中分母4表示所有网页的总数。
那么对于N个网页任何一个页面\(\\small P_{i}\)的PageRank计算公式如下
\(\\small PageRankP_{i}=\\alpha \\sum_{P_{j}\\in M(P_{i})}^{}{\\frac{PageRank(P_{j})}{L(P_{j})}} + \\frac{1-\\alpha}{N}\)
公式中,\(\\small P_{j}\\in M(P_{i})\) 表示所有包含有\(\\small P_{i}\)超链接的\(\\small P_{j}\)\(\\small L(P_{j})\)表示\(\\small P_{j}\)页面包含的超链接数N表示所有的网页总和。由于Bingoo要对全世界的网页进行排名所以这里的N是一个万亿级的数字。
计算开始的时候将所有页面的PageRank值设为1带入上面公式计算每个页面都得到一个新的PageRank值。再把这些新的PageRank值带入上面的公式继续得到更新的PageRank值如此迭代计算直到所有页面的PageRank值几乎不再有大的变化才停止。
小结
PageRank算法我们现在看起来平平无奇但是正是这个算法造就了Google近2万亿美元的商业帝国。在Google之前Yahoo已经是互联网最大的搜索引擎公司。按照一般的商业规律如果一个创新公司不能带来十倍的效率或者体验提升就根本没有机会挑战现有的巨头。而Google刚一出现就给Yahoo和旧有的搜索引擎世界带来摧枯拉朽的扫荡用户体验的提升不止十倍这其中的秘诀正是PageRank。
二十几年前,我刚刚接触编程的时候,我们中国也有很多这样的编程英雄,王选、王江民、求伯君、雷军等等,他们几乎凭一己之力就创造出一个行业。正是对这些英雄们的崇拜和敬仰,引领我在编程这条路上一直走下去。软件编程是一个可以创造奇迹的地方,而不只是为了混碗饭吃。梦想不能当饭吃,但是梦想带来的可不止是一碗饭。
思考题
PageRank的计算需要在万亿级的数据上进行多次迭代计算才能完成。数据量和计算量都非常大如何完成这样的计算也就是说具体编程实现是怎样的
欢迎在评论区分享你的思考,我们共同进步。

View File

@ -0,0 +1,178 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 反应式编程框架设计:如何使方法调用无阻塞等待?
你好,我是李智慧。
反应式编程本质上是一种异步编程方案在多线程协程、异步方法调用、异步I/O访问等技术基础之上提供了一整套与异步调用相匹配的编程模型从而实现程序调用非阻塞、即时响应等特性即开发出一个反应式的系统以应对编程领域越来越高的并发处理需求。
反应式系统应该具备如下的4个特质。
即时响应:应用的调用者可以即时得到响应,无需等到整个应用程序执行完毕。也就是说应用调用是非阻塞的。
回弹性:当应用程序部分功能失效的时候,应用系统本身能够进行自我修复,保证正常运行,保证响应,不会出现系统崩溃和宕机的情况。
弹性:系统能够对应用负载压力做出响应,能够自动伸缩以适应应用负载压力,根据压力自动调整自身的处理能力,或者根据自身的处理能力,调整进入系统中的访问请求数量。
消息驱动:功能模块之间、服务之间通过消息进行驱动,以完成服务的流程。
目前主流的反应式编程框架有RxJava、Reactor等它们的主要特点是基于观察者设计模式的异步编程方案编程模型采用函数式编程。
观察者模式和函数式编程有自己的优势但是反应式编程并不是必须用观察者模式和函数式编程。我们准备开发一个纯消息驱动完全异步支持命令式编程的反应式编程框架框架名称为“Flower”。
需求分析
互联网及物联网场景下的应用系统开发,基本上都是高并发系统开发。也就是说,在同一个时刻,会有大量的用户或设备请求到达系统,进行计算处理。但是传统的编程模型都是阻塞式编程,阻塞式编程有什么特点,会产生什么问题呢?我们来看一段代码示例。
void a(){
....
int x = m();
int y = n();
return x + y;
}
在方法a中调用了方法m那么在方法m返回之前就不会调用方法n即方法a被方法m阻塞了。这种编程模型下方法m和方法n不能同时执行系统的运行速度就不会快并发处理能力就不会很高。
还有更严重的情况。服务器通常为每个用户请求创建一个线程而创建的总线程数是有限的每台服务器通常几百个。如果方法m是一个远程调用处理比较慢当方法a调用方法m时执行方法a的线程就会被长期挂起无法释放。如果所有线程都因为方法m而无法释放导致服务器线程耗尽就会使服务器陷入假死状态外部表现就是服务器宕机失去响应系统严重故障。
Flower框架应该满足如下典型Web应用的线程特性。
当并发用户请求到达应用服务器时Web容器线程不需要执行应用程序代码它只是将用户的HTTP请求变为请求对象将请求对象异步交给Flower框架的Service去处理而Web容器线程自身立刻就返回。
如果是传统的阻塞式编程Web容器线程要完成全部的请求处理操作直到返回响应结果才能释放线程所以需要很多Web容器线程。但使用Flower框架只需要极少的容器线程就可以处理较多的并发用户请求而且容器线程不会阻塞。
同样在Flower框架中用户请求交给业务Service对象以后Service之间依然是使用异步消息通讯而非阻塞式的调用。一个Service完成业务逻辑处理计算以后会返回一个处理结果这个结果会以消息的方式异步发送给下一个Service。
概要设计
Flower框架实现异步无阻塞一方面是利用了Java Web容器的异步特性主要是Servlet3.0以后提供的AsyncContext快速释放容器线程另一方面则利用了异步的数据库驱动和异步的网络通信主要是HttpAsyncClient等异步通信组件。而Flower框架内核心应用代码之间的异步无阻塞调用则是利用了Akka 的Actor模型。
Akka Actor的异步消息驱动实现如下。
一个Actor向另一个Actor发起通讯时当前Actor就是一个消息的发送者Sender它需要获得另一个Actor的ActorRef也就是一个引用通过引用进行消息通信。而ActorRef收到消息以后会将这个消息放到目标Actor的Mailbox里面然后就立即返回了。
也就是说一个Actor向另一个Actor发送消息时不需要等待对方真正地处理这个消息只需要将消息发送到目标Actor的Mailbox里面就可以了。Sender不会被阻塞可以继续执行自己的其他操作。而目标Actor检查自己的Mailbox中是否有消息如果有则从Mailbox里面获取消息并进行异步的处理。而所有的Actor会共享线程这些线程不会有任何的阻塞。
但是Actor编程模型无法满足人们日常的编程习惯以及Flower的命令式编程需求所以我们需要将Akka Actor封装到一个Flower的编程框架中并通过Flower提供一个新的编程模型。
Flower基于Akka的Actor进行开发将Service封装到Actor里面并且将Actor收到的消息作为参数传入Service进行调用。
Flower框架的主要元素包括Flower Service服务、Flower 流程和Flower容器。其中Service实现一个细粒度的服务功能Service之间会通过Message关联前一个Service的返回值Message必须是后一个Service的输入参数Message。而Flower容器就负责在Service间传递Massage从而使Service按照业务逻辑编辑成一个Flow流程
在Flower内部消息是一等公民基于Flower开发的应用系统是面向消息的应用系统。消息由Service产生是Service的返回值同时消息也是Service的输入。前一个Service的返回消息是下一个Service的输入消息没有耦合的Service正是通过消息关联起来组成一个Service流程并最终构建出一个拥有完整处理能力的应用系统。流程举例
// -> service1 -> service2 -> service5 -> service4
// ^ | ^ |
// | -> service3 -| |
// |___________________________________|
详细设计
Flower核心类图如下。
Flower框架核心关键类及其职责如下
Service以及HttpService接口是框架的编程核心开发者开发的Service需要实现Service或者HttpService接口。HttpService与Service的不同在于HttpService在接口方法中传递Web参数开发者利用Web接口可以将计算结果直接print到HTTP客户端
ServiceFactory负责用户以及框架内置的service实例管理加载*.services文件
ServiceFlow负责流程管理加载*.flow文件
ServiceActor将Service封装到Actor。
Flower初始化及调用时序图如下。
图中包含两个过程第一个过程是服务流程初始化过程。首先开发者通过ServiceFacade调用已经定义好的服务流程。然后ServiceFacade根据传入的flow名和service名创建第一个ServiceActor。这个ServiceActor将通过ServiceFactory来装载Service实例并通过ServiceFlow获得当前Service在流程中所配置的后续Service可能有多个。依此递归创建后续Service的ServiceActor并记录其对应的ActorRef。
时序图中的第二个过程是消息流处理过程。调用者发送给ServiceFacade的消息会被flow流程中的第一个ServiceActor处理这个ServiceActor会调用对应的Service实例并将Service实例的返回值作为消息发送给流程定义的后续ServiceActor。
使用Flower框架开发应用程序就是开发各种Service开发服务Service类必须实现Flower框架的Service接口或者HTTP接口在process方法内完成服务业务逻辑处理。Service代码示例如下。
public class UserServiceA implements Service<User, User> {
static final Logger logger = LoggerFactory.getLogger(UserServiceA.class);
@Override
public User process(User message, ServiceContext context) throws Throwable {
message.setDesc(message.getDesc() + " --> " + getClass().getSimpleName());
message.setAge(message.getAge() + 1);
logger.info("结束处理消息, message : {}", message);
return message;
}
}
服务注册
开发者开发的服务需要在Flower中注册才可以调用Flower提供两种服务注册方式配置文件方式和编程方式。
编程方式示例如下。
ServiceFactory serviceFactory = flowerFactory.getServiceFactory();
serviceFactory.registerService(UserServiceA.class.getSimpleName(), UserServiceA.class);
serviceFactory.registerService(UserServiceB.class.getSimpleName(), UserServiceB.class);
serviceFactory.registerService(UserServiceC1.class.getSimpleName(), UserServiceC1.class);
配置文件方式支持用配置文件进行注册,服务定义配置文件扩展名: .services放在classpath下Flower框架自动加载注册比如flower_test.services。配置文件内容如下。
UserServiceA = com.ly.train.flower.base.service.user.UserServiceA
UserServiceB = com.ly.train.flower.base.service.user.UserServiceB
UserServiceC1 = com.ly.train.flower.base.service.user.UserServiceC1
流程编排
在Flower中服务之间的依赖关系不能通过传统的服务之间依赖调用实现如开头的方法a调用方法m那样。而需要通过流程编排方式实现服务间依赖。服务编排方式也有两种配置文件方式和编程方式。
下面的例子演示的是以编程方式编排流程。
// UserServiceA -> UserServiceB -> UserServiceC1
final String flowName = "flower_test";
ServiceFlow serviceFlow = serviceFactory.getOrCreateServiceFlow(flowName);
serviceFlow.buildFlow(UserServiceA.class, UserServiceB.class);
serviceFlow.buildFlow(UserServiceB.class, UserServiceC1.class);
serviceFlow.build();
而流程配置文件方式则使用扩展名: .flow放在classpath下Flower框架会自动加载编排流程。 比如flower_test.flow文件名flower_test就是流程的名字流程执行时需要指定流程名。配置文件内容示例如下。
UserServiceA -> UserServiceB
UserServiceB -> UserServiceC1
我们将服务Service代码开发好注册到了Flower框架中并通过流程编排的方式编排了这几个Service的依赖关系后面就可以用流程名称进行调用了。调用代码示例如下其中flowName是流程的名字user是流程中的一个Service名是流程开始的Service。
final FlowRouter flowRouter = flowerFactory.buildFlowRouter(flowName, 16);
flowRouter.asyncCallService(user);
Flower框架源代码及更多资料可参考 https://github.com/zhihuili/flower。
小结
架构师是一个技术权威他应该是团队中最有技术影响力的那个人。所以架构师需要具备卓越的代码能力否则就会沦为PPT架构师。PPT架构师可以一时成为团队的焦点但是无法长远让大家信服。
那么架构师应该写什么样的代码?架构师如果写的代码和其他开发工程师的代码一样,又何以保持自己的技术权威,实现技术领导?简单来说,代码可以分成两种,一种代码是给最终用户使用的,处理用户请求,产生用户需要的结果;另一种是给开发工程师使用的,各种编程语言、数据库、编译器、编程框架、技术工具等等。
编程语言、数据库这些是业界通用的,但是编程框架、技术工具,每个公司都可以依据自身的业务特点,开发自己的框架和工具。而架构师应该是开发框架的那个人,每个开发工程师都使用架构师的开发框架以及约定的编程规范开发代码。架构师通过这种方式落地自己的架构设计,保持自己的技术影响。
也许你的开发中不会用到反应式编程你可能也不需要深入学习Flower框架如何设计、如何使用。但是希望你能通过本文学习到如何设计一个编程框架结合你所在公司的业务场景将来开发一个你自己的编程框架。
思考题
Flower纯消息驱动、异步无阻塞的优良特点适合许多对并发处理要求高需要快速、及时响应的场景你能想到的现实应用场景有哪些呢
欢迎在评论区分享你的思考,我们共同进步。

View File

@ -0,0 +1,134 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 高性能架构的三板斧:分析系统性能问题从哪里入手?
你好,我是李智慧。
我们在讨论高性能架构之前,需要先聊聊什么叫高性能,以及如何量化地测试系统的性能。在[02讲]中,我们讨论了一些和并发相关的指标。事实上,并发数正是系统性能的核心指标之一,因为高并发会引起系统资源短缺,来不及处理用户请求,就会导致系统性能下降。
除了系统并发数,一般说来,和系统性能相关的量化指标还有响应时间和吞吐量。在前面的案例分析中,我们也多次估算过响应时间和吞吐量。我们再重新回顾下这几个指标的定义。
吞吐量、响应时间和并发数三者之间是有关联性的。\(\\small 吞吐量 = 并发数\\div响应时间\)。并发数不变,响应时间足够快,那么单位时间的吞吐量就会相应地提高。
以上这些性能指标,我们可以在系统运行期通过监控系统获取,也可以在系统上线前通过性能测试来获取,以此来了解我们系统的性能特性,以及判断系统能否承受预期的高并发压力。
性能测试
性能测试就是使用性能测试工具,通过多线程模拟用户请求,对系统施加高并发的访问压力,得到以上这些性能指标。事实上,随着请求线程数,即并发数逐渐增加,系统的吞吐量和响应时间会呈现出不同的性能特性。具体说来,整个测试过程又可细分为性能测试、负载测试、压力测试三个阶段。
性能测试是以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统在资源可接受的范围内是否达到了性能预期目标。这个过程中,随着并发数的增加,吞吐量也在增加,但是响应时间变化不大。系统正常情况下的并发访问压力应该都在这个范围内。
负载测试则是对系统不断施加并发请求,增加系统的压力,直到系统的某项或多项指标达到安全临界值。这个过程中,随着并发数的增加,吞吐量只有小幅的增加,达到最大值后,吞吐量还会下降,而响应时间则会不断增加。
压力测试是指在超过安全负载的情况下增加并发请求数对系统继续施加压力直到系统崩溃或不再处理任何请求此时的并发数就是系统的最大压力承受能力。这个过程中吞吐量迅速下降响应时间迅速增加。到了系统崩溃点吞吐量为0响应时间无穷大。
性能压测工具不断增加并发请求线程数持续对系统进行性能测试、负载测试、压力测试得到对应的TPS和响应时间将这些指标画在一个坐标系里就得到系统的性能特性曲线。
上图中横轴是系统并发数左侧黄色纵轴为吞吐量TPS对应图中黄色曲线。可以看到随着并发数增加系统负载压力也不断增加系统吞吐量是先上升后下降右侧蓝色纵轴为响应时间对应图中蓝色曲线随着并发负载压力的不断增加系统响应时间先是缓慢增长到了某个点后响应时间急剧增加。
通过性能测试,如果发现系统的性能特性并不能满足我们的预期,就需要对系统进行性能优化。架构方面核心的优化思路有三个:通过分布式集群扩展系统的服务器,降低单一服务器的负载压力;通过缓存的方式降低系统的读负载压力;通过消息队列降低系统的写负载压力。对应的技术方案分别是:负载均衡、分布式缓存、消息队列,我称之为高性能架构的三板斧。
负载均衡
所谓负载均衡,就是将高并发的用户请求分发到多台应用服务器组成的一个服务器集群上,利用更多的服务器资源处理高并发下的计算压力,提升整体的性能指标。下图是比较常用的应用层负载均衡。
用户的HTTP请求先到达应用层负载均衡服务器负载均衡服务器从应用服务器集群中选择一台服务器的IP地址(10.0.0.3)然后将HTTP请求转发给该服务器。该服务器处理完成后将响应内容返回给负载均衡服务器再由负载均衡服务器返回给用户。
不同用户并发提交访问请求的时候,负载均衡服务器就会将这些请求分发到不同的应用服务器上,每台应用服务器处理的用户请求并发量都不是很高,而这样构成的一个应用服务器集群却可以承受较高的并发访问压力。
但是这种应用层负载均衡有个比较大的问题就是所有请求、响应HTTP通信都需要通过负载均衡服务器而HTTP协议又是一个比较重的应用层协议协议的处理需要消耗比较多的计算资源。也就是说应用层负载均衡服务器将会是整个应用服务器集群的瓶颈。
因此应用层负载均衡通常用在规模比较小的集群上而对于大规模的应用服务器集群我们使用IP层负载均衡或者链路层负载均衡。
IP层是网络通讯协议的网络层所以有时候IP层负载均衡也叫网络层负载均衡。它的主要工作原理是用户的请求到达负载均衡服务器后负载均衡服务器会对网络层数据包的IP地址进行转换将其修改为应用服务器的IP地址然后把数据包重新发送出去请求数据就会到达应用服务器。如下图。
IP负载均衡不需要在HTTP协议层工作可以在操作系统内核直接修改IP数据包的地址所以效率比应用层负载均衡高得多。但不管是请求还是响应的数据包都要通过负载均衡服务器进行IP地址转换而响应的数据通常都会比较大甚至会超过IP负载均衡服务器网卡带宽。因此对于大规模的应用服务器集群IP层负载均衡服务器还是会成为响应的流量瓶颈。
优化的方案就是采用链路层负载均衡。链路层负载均衡服务器并不修改请求数据包的IP地址而是修改数据链路层里的网卡mac地址在数据链路层实现负载均衡。应用服务器返回响应数据的时候因为IP地址没有修改过所以这个响应会直接到达用户的设备而不会再经过负载均衡服务器。如下图。
链路层负载均衡避免响应数据再经过负载均衡服务器,因而可以承受较大的数据传输压力,目前大型互联网应用大多使用链路层负载均衡。
分布式缓存
负载均衡可以降低单服务器的并发负载压力,但是需要更多的服务器,同时也无法降低数据库的负载压力。为了弥补这些缺陷,我们还需要使用缓存优化系统性能。所谓缓存,就是将要多次读取的数据暂存起来,这样应用程序就不必从数据源重复加载数据了,以此降低数据源的计算负载压力,提高数据响应速度。
高并发架构中常见的分布式缓存有三种CDN、反向代理和分布式对象缓存。
CDNContent Delivery Network即内容分发网络。我们上网的时候App或者浏览器想要连接到互联网应用的服务器需要移动、电信这样的网络服务商为我们提供网络服务建立网络连接才可以上网。而这些服务商需要在全国范围内部署骨干网络、交换机机房才能完成网络连接服务。
因为这些交换机机房可能会离用户非常近,所以我们自然想到了,互联网应用能不能在这些交换机机房中部署缓存服务器呢?这样的话,用户就可以近距离获得自己需要的数据,既提高了响应速度,又节约了网络带宽和服务器资源。
答案是当然可以。这个部署在网络服务商机房中的缓存就是CDN因为距离用户非常近又被称作网络连接的第一跳。目前很多互联网应用大约80%以上的网络流量都是通过CDN返回的。
我们有时候需要通过代理上网,这个代理是代理我们的客户端上网设备。而反向代理则是代理服务器,所有的网络请求都需要通过反向代理才能到达应用程序服务器。那么在这里加一个缓存,尽快将数据返回给用户,而不是发送给应用服务器,这就是反向代理缓存。
用户请求到达反向代理缓存服务器,反向代理检查本地是否有需要的数据,如果有就直接返回;如果没有,就请求应用服务器,得到需要的数据后缓存在本地,然后返回给用户。同时,只要将后面的应用服务器部署为一个集群,反向代理服务器在请求后面的应用服务器的时候,进行负载均衡选择,那么这个反向代理缓存服务器也就同时成为了前面讨论的应用层负载均衡服务器。也就是说,一台服务器,既做反向代理服务器,也做负载均衡服务器。
CDN和反向代理缓存对应用程序是透明的通常被当做系统前端的一部分。而应用程序如果要使用缓存就需要分布式对象缓存。分布式对象缓存访问架构如下图。
多台缓存服务器构成一个缓存集群缓存数据存储在每台服务器的内存中。每个程序需要依赖一个缓存客户端SDK通过SDK的API来访问缓存服务器。应用程序先调用API由API调用SDK的路由算法路由算法根据缓存的key值计算这个key应该访问哪台缓存服务器。路由算法计算得到目标服务器的IP地址和端口号后API再调用SDK的通信模块将值以及缓存操作命令发送给具体的某台缓存服务器最终由这台服务器完成缓存操作。
使用缓存架构可以减少不必要的计算,快速响应用户请求。但是缓存只能改善系统的读操作性能,对于写操作,缓存是无能为力的。我们不能把用户提交的数据直接写入缓存中,因为缓存通常被认为是一种不可靠的存储。
消息队列
优化写操作性能的主要手段是使用消息队列,将写操作异步化。典型的应用程序写数据的方式如下图。
应用服务器收到用户写操作请求后,调用数据库操作接口,完成数据写入数据库的操作。但是数据库处理速度比较慢,同时又对并发压力比较敏感。大量操作请求同时提交到数据库,可能会导致数据库负载压力太大而崩溃。
使用消息队列将写操作异步化如下图。
应用服务器收到用户写操作请求后,不是直接调用数据库,而是将写操作请求发送给消息队列服务器,再由消息消费者服务器从消息队列服务器消费消息,完成对数据库的写操作。
这样会带来两个好处。一方面,用户请求发送给消息队列就可以直接返回响应给用户了,而消息队列服务器的处理速度要远远快于数据库,用户端的响应时间可以极大缩短;另一方面,消息队列写数据库的时候,可以根据数据库的负载能力控制写入的速度,即使用户请求并发很高,也不会导致数据库崩溃,消息队列可以使系统运行在一个性能最优的负载压力范围内。
这种在用户请求高并发的时候控制处理速度,在用户请求低谷的时候,继续处理请求的方式叫做“削峰填谷”,如下图。
消息队列将直接调用的高峰访问压力推迟到访问低谷的时候处理,使系统保持在性能最优的状态下运行。
小结
这节课的三种高性能架构是最常用的架构性能优化手段,可以解决大多数系统架构性能问题。但是性能优化是一个系统的工程,不问青红皂白,不管三七二十一,上来就是三板斧,那做架构师也未免太容易了些。
性能优化必须有的放矢,必须要了解系统的关键技术设计,以及当前的系统性能指标,然后才能寻找到最合适的性能优化方式。所以性能优化需要从性能测试开始,具体过程可以总结为以下几步:
进行性能测试,了解系统当前性能指标,发现哪些指标不符合性能需求。
分析系统架构设计与关键技术实现,发现导致性能瓶颈的地方。
进行架构以及代码优化,消除性能瓶颈。
进行性能测试,分析优化是否达到目标。
而且性能优化也并不是只能优化架构和代码。对于一个全球用户访问的系统在全球各地部署多个数据中心就近为用户服务可以极大降低网络传输的延迟提升性能对于一些少量而重要的数据计算使用更好的CPU、更大的内存、更快的硬盘也就是说进行垂直伸缩也可以极大改善性能而对操作系统、虚拟机进行参数优化对使用的第三方软件包进行升级改造有时候也会对性能实现成倍的提升。
思考题
在你的工作实践中,曾经遇到过怎样的性能问题,最后如何解决?欢迎分享出来,我们一起讨论,一起学习。

View File

@ -0,0 +1,174 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 微博系统设计:怎么应对热点事件的突发访问压力?
你好,我是李智慧。
微博microblog是一种允许用户即时更新简短文本比如140个字符并可以公开发布的微型博客形式。今天我们就来开发一个面向全球用户、可以支持10亿级用户体量的微博系统系统名称为“Weitter”。
我们知道微博有一个重要特点就是部分明星大V拥有大量的粉丝。如果明星们发布一条比较有话题性的个人花边新闻比如宣布结婚或者离婚就会引起粉丝们大量的转发和评论进而引起更大规模的用户阅读和传播。
这种突发的单一热点事件导致的高并发访问会给系统带来极大的负载压力,处理不当甚至会导致系统崩溃。而这种崩溃又会成为事件热点的一部分,进而引来更多的围观和传播。
因此Weitter的技术挑战一方面是微博这样类似的信息流系统架构是如何设计的另一方面就是如何解决大V们的热点消息产生的突发高并发访问压力保障系统的可用性。今天我们就来看看这样的系统架构该怎么设计。
需求分析
Weitter的核心功能只有三个发微博关注好友刷微博。
发微博用户可以发表微博内容包含不超过140个字的文本可以包含图片和视频。
关注好友:用户可以关注其他用户。
刷微博用户打开自己的微博主页主页显示用户关注的好友最近发表的微博用户向下滑动页面或者点刷新按钮主页将更新关注好友的最新微博且最新的微博显示在最上方主页一次显示20条微博当用户滑动到主页底部后继续向上滑动会按照时间顺序显示当前页面后续的20条微博。
此外,用户还可以收藏、转发、评论微博。
性能指标估算
系统按10亿用户设计按20%日活估计大约有2亿日活用户DAU其中每个日活用户每天发表一条微博并且平均有500个关注者。
而对于发微博所需的存储空间,我们做如下估算。
文本内容存储空间
遵循惯例每条微博140个字如果以UTF8编码存储汉字计算则每条微博需要\(\\small 140\\times3=420\)个字节的存储空间。除了汉字内容以外每条微博还需要存储微博ID、用户ID、时间戳、经纬度等数据按80个字节计算。那么每天新发表微博文本内容需要的存储空间为100GB。
\(\\small 2亿 \\times (420B +80B) = 100GB/天\)
多媒体文件存储空间
除了140字文本内容微博还可以包含图片和视频按每5条微博包含一张图片每10条微博包含一个视频估算每张图片500KB每个视频2MB每天还需要60TB的多媒体文件存储空间。
\(\\small 2亿\\div5\\times500KB+2亿\\div10\\times2MB=60TB/天\)
对于刷微博的访问并发量,我们做如下估算。
QPS
假设两亿日活用户每天浏览两次微博每次向上滑动或者进入某个人的主页10次每次显示20条微博每天刷新微博次数40亿次即40亿次微博查询接口调用平均QPS大约5万。
\(\\small 40亿\\div24\\times60\\times60=46296/秒\)
高峰期QPS按平均值2倍计算所以系统需要满足10万QPS。
网络带宽
10万QPS刷新请求每次返回微博20条那么每秒需访问200万条微博。按此前估计每5条微博包含一张图片每10条微博包含一个视频需要的网络总带宽为4.8Tb/s。
\(\\small 200万\\div5\\times500KB+200万\\div10\\times2MB\\times8bit=4.8Tb/s\)
概要设计
在需求分析中我们可以看到Weitter的业务逻辑比较简单但是并发量和数据量都比较大所以系统架构的核心就是解决高并发的问题系统整体部署模型如下。
这里包含了“Get请求”和“Post请求”两条链路Get请求主要处理刷微博的操作Post请求主要处理发微博的请求这两种请求处理也有重合的部分我们拆分着来看。
我们先来看看Get请求的部分。
用户通过CDN访问Weitter的数据中心、图片以及视频等极耗带宽的请求绝大部分可以被CDN缓存命中也就是说4.8Tb/s的带宽压力90%以上可以通过CDN消化掉。
没有被CDN命中的请求一部分是图片和视频请求其余主要是用户刷新微博请求、查看用户信息请求等这些请求到达数据中心的反向代理服务器。反向代理服务器检查本地缓存是否有请求需要的内容。如果有就直接返回如果没有对于图片和视频文件会通过分布式文件存储集群获取相关内容并返回。分布式文件存储集群中的图片和视频是用户发表微博的时候上传上来的。
对于用户微博内容等请求如果反向代理服务器没有缓存就会通过负载均衡服务器到达应用服务器处理。应用服务器首先会从Redis缓存服务器中检索当前用户关注的好友发表的最新微博并构建一个结果页面返回。如果Redis中缓存的微博数据量不足构造不出一个结果页面需要的20条微博应用服务器会继续从MySQL分片数据库中查找数据。
以上处理流程主要是针对读http get请求那如果是发表微博这样的写http post请求呢我们再来看一下写请求部分的图。
你会看到客户端不需要通过CDN和反向代理而是直接通过负载均衡服务器到达应用服务器。应用服务器一方面会将发表的微博写入Redis缓存集群一方面写入分片数据库中。
在写入数据库的时候,如果直接写数据库,当有高并发的写请求突然到来,可能会导致数据库过载,进而引发系统崩溃。所以,数据库写操作,包括发表微博、关注好友、评论微博等,都写入到消息队列服务器,由消息队列的消费者程序从消息队列中按照一定的速度消费消息,并写入数据库中,保证数据库的负载压力不会突然增加。
详细设计
用户刷新微博的时候如何能快速得到自己关注的好友的最新微博列表10万QPS的并发量如何应对如何避免数据库负载压力太大以及如何快速响应用户请求详细设计将基于功能需求和概要设计主要讨论这些问题。
微博的发表/订阅问题
Weitter用户关注好友后如何快速得到所有好友的最新发表的微博内容即发表/订阅问题,是微博的核心业务问题。
一种简单的办法就是“推模式”即建一张用户订阅表用户关注的好友发表微博后立即在用户订阅中为该用户插入一条记录记录用户id和好友发表的微博id。这样当用户刷新微博的时候只需要从用户订阅表中按用户id查询所有订阅的微博然后按时间顺序构建一个列表即可。也就是说推模式是在用户发微博的时候推送给所有的关注者如下图用户发表了微博0他的所有关注者的订阅表都插入微博0。
推模式实现起来比较简单,但是推模式意味着,如果一个用户有大量的关注者,那么该用户每发表一条微博,就需要在订阅表中为每个关注者插入一条记录。而对于明星用户而言,可能会有几千万的关注者,明星用户发表一条微博,就会导致上千万次的数据库插入操作,直接导致系统崩溃。
所以对于10亿级用户的微博系统而言我们需要使用“拉模式”解决发表/订阅问题。也就是说,用户刷新微博的时候,根据其关注的好友列表,查询每个好友近期发表的微博,然后将所有微博按照时间顺序排序后构建一个列表。也就是说,拉模式是在用户刷微博的时候拉取他关注的所有好友的最新微博,如下图:
拉模式极大降低了发表微博时写入数据的负载压力,但是却又急剧增加了刷微博时候读数据库的压力。因为对于用户关注的每个好友,都需要进行一次数据库查询。如果一个用户关注了大量好友,查询压力也是非常巨大的。
所以首先需要限制用户关注的好友数在Weitter中普通用户关注上限是2000人VIP用户关注上限是5000人。其次需要尽量减少刷新时查询数据库的次数也就是说微博要尽量通过缓存读取。
但即使如此你会发现每次刷新的查询压力还是太大所以Weitter最终采用“推拉结合”的模式。也就是说如果用户当前在线那么就会使用推模式系统会在缓存中为其创建一个好友最新发表微博列表关注的好友如果有新发表微博就立即将该微博插入列表的头部当该用户刷新微博的时候只需要将这个列表返回即可。
如果用户当前不在线,那么系统就会将该列表删除。当用户登录刷新的时候,用拉模式为其重新构建列表。
那么如何确定一个用户是否在线?一方面可以通过用户操作时间间隔来判断,另一方面也可以通过机器学习,预测用户的上线时间,利用系统空闲时间,提前为其构建最新微博列表。
缓存使用策略
通过前面的分析我们已经看到Weitter是一个典型的高并发读操作的场景。10万QPS刷新请求每个请求需要返回20条微博如果全部到数据库中查询的话数据库的QPS将达到200万即使是使用分片的分布式数据库这种压力也依然是无法承受的。所以我们需要大量使用缓存以改善性能提高吞吐能力。
但是缓存的空间是有限的我们必定不能将所有数据都缓存起来。一般缓存使用的是LRU淘汰算法即当缓存空间不足时将最近最少使用的缓存数据删除空出缓存空间存储新数据。
但是LRU算法并不适合微博的场景因为在拉模式的情况下当用户刷新微博的时候我们需要确保其关注的好友最新发表的微博都能展示出来如果其关注的某个好友较少有其他关注者那么这个好友发表的微博就很可能会被LRU算法淘汰删除出缓存。对于这种情况系统就不得不去数据库中进行查询。
而最关键的是,系统并不能知道哪些好友的数据通过读缓存就可以得到全部最新的微博,而哪些好友需要到数据库中查找。因此不得不全部到数据库中查找,这就失去了使用缓存的意义。
基于此我们在Weitter中使用时间淘汰算法**也就是将最近一定天数内发布的微博全部缓存起来用户刷新微博的时候只需要在缓存中进行查找。如果查找到的微博数满足一次返回的条数20条就直接返回给用户如果缓存中的微博数不足就再到数据库中查找。
最终Weitter决定缓存7天内发表的全部微博需要的缓存空间约700G。缓存的key为用户IDvalue为用户最近7天发表的微博ID列表。而微博ID和微博内容分别作为key和value也缓存起来。
此外对于特别热门的微博内容比如某个明星的离婚微博这种针对单个微博内容的高并发访问由于访问压力都集中一个缓存key上会给单台Redis服务器造成极大的负载压力。因此微博还会启用本地缓存模式即应用服务器在内存中缓存特别热门的微博内容应用构建微博刷新页的时候会优先检查微博ID对应的微博内容是否在本地缓存中。
Weitter最后确定的本地缓存策略是针对拥有100万以上关注者的大V用户缓存其48小时内发表的全部微博。
现在我们来看一下Weitter整体的缓存架构。
数据库分片策略
前面我们分析过Weitter每天新增2亿条微博。也就是说平均每秒钟需要写入2400条微博高峰期每秒写入4600条微博。这样的写入压力对于单机数据库而言是无法承受的。而且每年新增700亿条微博记录这也超出了单机数据库的存储能力。因此Weitter的数据库需要采用分片部署的分布式数据库。分片的规则可以采用用户ID分片或者微博 ID分片。
如果按用户ID的hash值分片那么一个用户发表的全部微博都会保存到一台数据库服务器上。这样做的好处是当系统需要按用户查找其发表的微博的时候只需要访问一台服务器就可以完成。
但是这样做也有缺点对于一个明星大V用户其数据访问会成热点进而导致这台服务器负载压力太大。同样地如果某个用户频繁发表微博也会导致这台服务器数据增长过快。
要是按微博 ID的hash值分片虽然可以避免上述按用户ID分片的热点聚集问题但是当查找一个用户的所有微博时需要访问所有的分片数据库服务器才能得到所需的数据对数据库服务器集群的整体压力太大。
综合考虑用户ID分片带来的热点问题可以通过优化缓存来改善而某个用户频繁发表微博的问题可以通过设置每天发表微博数上限每个用户每天最多发表50条微博来解决。最终Weitter采用按用户ID分片的策略。
小结
微博事实上是信息流应用产品中的一种,这类应用都以滚动的方式呈现内容,而内容则被放置在一个挨一个、外观相似的版块中。微信朋友圈、抖音、知乎、今日头条等,都是这类应用。因此这些应用也都需要面对微博这样的发表/订阅问题:如何为海量高并发用户快速构建页面内容?
在实践中信息流应用也大多采用文中提到的推拉结合模式区别只是朋友圈像微博一样推拉好友发表的内容而今日头条则推拉推荐算法计算出来的结果。同样地这类应用为了加速响应时间也大量使用CDN、反向代理、分布式缓存等缓存方案。所以熟悉了Weitter的架构就相当于掌握了信息流产品的架构。
思考题
面对微博的高并发访问压力,你还能想到哪些方案可以优化系统?
欢迎在评论区分享你的思考,我们共同进步。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 百科应用系统设计:机房被火烧了系统还能访问吗?
你好,我是李智慧。
百科知识应用网站是互联网应用中一个重要的类别。很多人上网是为了获取知识而互联网上的信息良莠并存相对说来百科知识应用网站能为普通人提供较为可信的信息。因此百科知识网站虽然功能单一、设计简单但是依然撑起了互联网的一片天空维基百科是全球访问量TOP10的网站百度百科是百度的核心产品之一。
我们准备开发一个供全球用户使用的百科知识应用系统系统名称为“Wepedia”。
Wepedia的功能比较简单只有编辑词条和搜索查看词条这两个核心功能。但是Wepedia的设计目标是支撑每日10亿次以上的访问压力。因此设计目标主要是简单、高效地支持高并发访问以及面对全球用户时保证\(\\small 7\\times24\)小时高可用。
概要设计
Wepedia的整体架构也就是简化的部署模型如图。
在梳理Wepedia整体逻辑之前先说明下架构图中核心组件的作用。
用户在Web端查看一个百科词条的时候首先通过GeoDNS进行域名解析得到离用户最近的数据中心所属的CDN服务器的IP地址。用户浏览器根据这个IP地址访问CDN服务器如果CDN服务器上缓存有用户访问的词条内容就直接返回给用户如果没有CDN会访问和自己在同一个区域的Wepedia的数据中心服务器。
准确地说CDN访问的是Wepedia数据中心负载均衡服务器LVS的IP地址。请求到达LVS后LVS会将该请求分发到某个Nginx服务器上。Nginx收到请求后也查找自己服务器上是否有对应的词条内容如果没有就将请求发送给第二级LVS负载均衡服务器。
接着第二级LVS将请求分发给某个Apache服务器Apache会调用PHP程序处理该请求。PHP程序访问Redis服务器集群确认是否有该词条的对象。如果有就将该对象封装成HTML响应内容返回给用户如果没有就访问MySQL数据库来查找该词条的数据内容。PHP程序一方面会将MySQL返回的数据构造成对象然后封装成HTML返回用户一方面会将该对象缓存到Redis。
如果用户的HTTP请求是一个图片那么Nginx则会访问LightHttp服务器获取图片内容。
因为Nginx缓存着词条内容那么当词条编辑者修改了词条内容时Nginx缓存的词条内容就会成为脏数据。解决这个问题通常有两种方案一种是设置失效时间到了失效时间缓存内容自动失效Nginx重新从Apache获取最新的内容。但是这种方案并不适合Wepedia的场景因为词条内容不会经常被编辑频繁失效没有意义只是增加了系统负载压力而且在失效时间到期前依然有脏数据的问题。
Wepedia为了解决Nginx缓存失效的问题采用了另一种解决方案失效通知。词条编辑者修改词条后Invalidation notification模块就会通知所有Nginx服务器该词条内容失效进而从缓存中删除它。这样当用户访问的时候就不会得到脏数据了。
多数据中心架构
Wepedia在全球部署多个数据中心可以就近为用户提供服务。因为即使是最快的光纤网络从地球一端访问另一端的数据中心在通信链路上的延迟就需要近150ms。
\(\\small 地球周长4万KM\\div2\\div光速30万KM/s\\times请求响应2次通信\\approx133ms\)
150ms是一个人类能够明显感知的卡顿时间。再加上服务器的处理时间用户的响应等待时间可能会超过1秒钟而页面加载时间超过1秒钟用户就会明显不耐烦。多数据中心架构可以通过GeoDNS为用户选择最近的数据中心服务器减少网络通信延迟提升用户体验。
另一方面多数据中心还具有容灾备份功能如果因为天灾或者人祸导致某个数据中心机房不可用那么用户还可以访问其他数据中心保证Wepedia是可用的。
但是多数据中心需要解决数据一致性的问题如果词条编辑者修改词条内容只记录在距离自己最近的数据中心那么这份数据就会和其他数据中心的不一致。所以Wepedia需要在多个数据中心之间进行数据同步用户不管访问哪个数据中心看到的词条内容都应该是一样的。
Wepedia的多数据中心架构如图。
Wepedia的多数据中心架构为一主多从架构即一个主数据中心多个从数据中心。如果用户请求是Get请求读请求那么请求就会在该数据中心处理。如果请求是Post请求写请求那么请求到达Nginx的时候Nginx会判断自己是否为主数据中心如果是就直接在该数据中心处理请求如果不是Nginx会将该Post请求转发给主数据中心。
通过这种方式主数据中心根据Post请求更新数据库后再通过Canal组件将更新同步给其他所有从数据中心的MySQL从而使所有数据中心的数据保持一致。同样LightHttp中的图片数据也进行同步开发LightHttp插件将收到的图片发送给所有从数据中心。
数据中心之间采用类似ZooKeeper的选主策略进行通信如果主数据中心不可用其他数据中心会重新选举一个主数据中心。而如果某个从数据中心失火了用户请求域名解析到其他数据中心即可。
这种多数据中心架构虽然使词条编辑操作的时间变长但是由于Wepedia的绝大多数请求都是Get请求Get与Post请求比超过10001因此对系统的整体影响并不很大。同时用一种简单、廉价的方式实现多数据中心的数据一致性开发和运维成本都比较低。
详细设计
作为一个百科服务类网站Wepedia 主要面临的挑战是应对来自全球各地的巨量并发的词条查询请求。因此详细设计重点关注Wepedia的性能优化。
前端性能优化
前端是指应用服务器(也就是 PHP 服务器)之前的部分,包括 DNS 服务、 CDN 服务、反向代理服务、静态资源服务等。对 Wepedia 而言80% 以上的用户请求可以通过前端服务返回请求根本不会到达应用服务器这也就使得网站最复杂、最有挑战的PHP应用服务端和存储端压力骤减。
Wepedia 前端架构的核心是反向代理服务器 Nginx 集群,大约需要部署数十台服务器。请求通过 LVS 负载均衡地分发到每台 Nginx 服务器热点词条被缓存在这里大量请求可直接返回响应减轻应用负载压力。而Nginx 缓存 不能命中的请求,会再通过 LVS 发送到 Apache 应用服务器集群。
在反向代理 Nginx 之前,是 CDN 服务,它对于 Wepedia 性能优化功不可没。因为用户查询的词条大部分集中在比重很小的热点词条上,这些词条内容页面缓存在 CDN 服务器上,而 CDN 服务器又部署在离用户浏览器最近的地方,用户请求直接从 CDN 返回,响应速度非常快,这些请求甚至根本不会到达 Wepedia 数据中心的 Nginx 服务器,服务器压力减小,节省的资源可以更快地处理其他未被 CDN 缓存的请求。
Wepedia CDN 缓存的几条准则:
内容页面不包含动态信息,以免页面内容缓存很快失效或者包含过时信息。
每个内容页面有唯一的 REST 风格的 URL以便 CDN 快速查找并避免重复缓存。
在 HTML 响应头写入缓存控制信息,通过应用控制内容是否缓存及缓存有效期等。
服务端性能优化
服务端主要是 PHP 服务器这里是业务逻辑的核心部分运行的模块都比较复杂笨重需要消耗较多的资源Wepedia 需要将最好的服务器部署在这里(和数据库配置一样的服务器),从硬件上改善性能。
除了硬件改善Wepedia 还需要使用其他开源组件对应用层进行优化:
使用 APC这是一个 PHP 字节码缓存模块,可以加速代码执行,减少资源消耗。
使用 Tex 进行文本格式化,特别是将科学公式内容转换成图片格式。
替换 PHP 的字符串查找函数 strtr(),使用更优化的算法重构。
存储端性能优化
包括缓存、存储、数据库等被应用服务器依赖的服务都可以归类为存储端服务。存储端服务通常是一些有状态的服务,即需要进行数据存储。这些服务大多建立在网络通信和磁盘操作基础上,是性能的瓶颈,也是性能优化的关键环节。
存储端优化最主要的手段是使用缓存,将热点数据缓存在分布式缓存系统的内存中,加速应用服务器的数据读操作速度,减轻存储和数据库服务器的负载。
Wepedia 的缓存使用策略如下:
热点特别集中的数据直接缓存到应用服务器的本地内存中,因为要占用应用服务器的内存且每台服务器都需要重复缓存这些数据,因此这些数据量很小,但是读取频率极高。
缓存数据的内容尽量是应用服务器可以直接使用的格式,比如 HTML 格式,以减少应用服务器从缓存中获取数据后解析构造数据的代价。
使用缓存服务器存储 session 对象。
作为存储核心数据资产的 MySQL 数据库,需要做如下优化:
使用较大的服务器内存。在 Wepedia 应用场景中,增加内存比增加其他资源更能改善 MySQL 性能。
使用 RAID5 磁盘阵列以加速磁盘访问。
使用MySQL 主主复制及主从复制,保证数据库写入高可用,并将读负载分散在多台服务器。
小结
高可用架构中的各种策略,基本上都是针对一个数据中心内的系统架构、针对服务器级别的软硬件故障而进行设计的。但如果整个数据中心都不可用,比如数据中心所在城市遭遇了地震,机房遭遇了火灾或者停电,不管我们架构的设计多么的高可用,应用依然是不可用的。
为了解决这个问题,同时也为了提高系统的处理能力、改善用户体验,很多大型互联网应用都采用了异地多活的多机房架构策略,也就是说将数据中心分布在多个不同地点的机房里,这些机房都可以对外提供服务。用户可以连接任何一个机房进行访问,这样每个机房都可以提供完整的系统服务,即使某一个机房不可使用,系统也不会宕机,依然保持可用。
思考题
词条编辑者修改词条的时候,可能会同时修改(新增)词条文本和图片。而数据从主数据中心同步到多个从数据中心的时候,数据库同步可能和图片同步时间不一致,导致用户查看词条的时候,图片无法加载或者图片和文本内容不一致。
如何解决这个问题?
附1阿里巴巴在十几年前也遇到数据和图片同步不一致的问题后来解决这个问题的开发工程师晋升为阿里集团副总裁欢迎有志于成为副总裁的同学思考下这个问题。
附2阿里当年遇到并解决这个问题的系统https://github.com/alibaba/otter
附3阿里当年解决这个问题的工程师访谈https://www.infoq.cn/article/pl-alibaba
欢迎在评论区分享你的思考,我们共同进步。

View File

@ -0,0 +1,230 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 限流器设计:如何避免超预期的高并发压力压垮系统?
你好,我是李智慧。
在互联网高可用架构设计中限流是一种经典的高可用架构模式。因为某些原因大量用户突然访问我们的系统时或者有黑客恶意用DoSDenial of Service拒绝服务方式攻击我们的系统时这种未曾预期的高并发访问对系统产生的负载压力可能会导致系统崩溃。
解决这种问题的一个主要手段就是限流,即拒绝部分访问请求,使访问负载压力降低到一个系统可以承受的程度。这样虽然有部分用户访问失败,但是整个系统依然是可用的,依然能对外提供服务,而不是因为负载压力太大而崩溃,导致所有用户都不能访问。
为此我们准备开发一个限流器产品名称为“Diana”。
需求分析
我们将Diana定位为一个限流器组件即Diana的主要应用场景是部署在微服务网关或者其他HTTP服务器入口以过滤器的方式对请求进行过滤对超过限流规则的请求返回“服务不可用”HTTP响应。
Diana的限流规则可通过配置文件获取并需要支持本地配置和远程配置两种方式远程配置优先于本地配置。限流方式包括
全局限流:针对所有请求进行限流,即保证整个系统处理的请求总数满足限流配置。
账号限流:针对账号进行限流,即对单个账号发送的请求进行限流。
设备限流:针对设备进行限流,即对单个客户端设备发送的请求进行限流。
资源限流针对某个资源即某个URL进行限流即保证访问该资源的请求总数满足限流配置。
并且Diana设计应遵循开闭原则能够支持灵活的限流规则功能扩展即未来在不修改现有代码和兼容现有配置文件的情况下支持新的配置规则。
概要设计
Diana的设计目标是一个限流器组件即Diana并不是一个独立的系统不可以独立部署进行限流而是部署在系统网关或者其他HTTP服务器上作为网关的一个组件进行限流部署模型如下
用户请求通过负载均衡服务器到达网关服务器。网关服务器本质也是一个HTTP服务器限流器是部署在网关中的一个过滤器filter组件和网关中的签名校验过滤器、用户权限过滤器等配置在同一个过滤器责任链Chain of Responsibility上。限流器应该配置在整个过滤器责任链的前端也就是说如果请求超过了限流请求不需要再进入其他过滤器直接被限流器拒绝。
用户请求进入限流器后根据限流策略判断该请求是否已经超过限流如果超过限流器直接返回状态码为503Too Many Requests的响应如果没有超过限流请求继续向下处理经过其他网关过滤器并最终调用微服务完成处理。
限流器的策略可以在本地配置,也可以通过远程的配置中心服务器加载,即远程配置。远程配置优先于本地配置。
限流模式设计
请求是否超过限流,主要就是判断单位时间请求数量是否超过配置的请求限流数量。单位时间请求数量,可以本地记录,也可以远程记录。方便起见,本地记录称作本地限流,远程记录称作远程限流(也叫分布式限流)。
本地限流意味着每个网关服务器需要根据本地记录的单位时间请求数量进行限流。假设限流配置为每秒限流50请求如果该网关服务器本地记录的当前一秒内接受请求数量达到50那么这一秒内的后续请求都返回503响应。如果整个系统部署了100台网关服务器每个网关配置本地限流为每秒50那么整个系统每秒最多可以处理5000个请求。
远程限流意味着所有网关共享同一个限流数量每个网关服务器收到请求后从远程服务器中获取单位时间内已处理请求数如果超过限流就返回503响应。也就是说可能某个网关服务器一段时间内根本就没有请求到达但是远程的已处理请求数已经达到了限流上限那么这台网关服务器也必须拒绝请求。我们使用Redis作为记录单位时间请求数量的远程服务器。
高可用设计
为了保证配置中心服务器和Redis服务器宕机时限流器组件的高可用。限流器应具有自动降级功能即配置中心不可用则使用本地配置Redis服务器不可用则降级为本地限流。
详细设计
常用的限流算法有4种固定窗口Window限流算法滑动窗口Sliding Window限流算法漏桶Leaky Bucket限流算法令牌桶Token Bucket限流算法。我们将详细讨论这四种算法的实现。
此外限流器运行期需要通过配置文件获取对哪些URL路径进行限流本地限流还是分布式限流对用户限流还是对设备限流还是对所有请求限流限流的阈值是多少阈值的时间单位是什么具体使用哪种限流算法。因此我们需要先看下配置文件的设计。
配置文件设计
Diana限流器使用YAML进行配置配置文件举例如下
Url:/
rules:
- actor:device
unit:second
rpu:10
algo:TB
scope:global
- actor:all
unit:second
rpu:50
algo:W
scope:local
配置文件的配置项有7种分别说明如下
Url记录限流的资源地址”/“表示所有请求,配置文件中的路径可以互相包含,比如“/”包含“/sample”限流器要先匹配“/”的限流规则,如果“/”的限流规则还没有触发(即访问”/“的流量,也就是单位时间所有的请求总和没有达到限流规则),则再匹配“/sample”。
每个Url可以配置多个规则rules每个规则包括actorunitrpualgoscope
actor为限流对象可以是账号actor设备device全部all
unit为限流时间单位可以是秒secondminutehourday
rpu为单位时间限流请求数request per unit即上面unit定义的单位时间内允许通过的请求数目如unit为secondrpu为100表示每秒允许通过100个请求每秒超过100个请求就进行限流返回503响应
scope为rpu生效范围可以是本地local也可以是全局globalscope也决定了单位时间请求数量是记录在本地还是远程local记录在本地global记录在远程。
algo限流算法可以是windowsliding windowleaky buckettoken bucket 。
Diana支持配置4种限流算法使用者可以根据自己的需求场景为不同资源地址配置不同的限流算法下面详细描述这四种算法实现。
固定窗口Window限流算法
固定窗口限流算法就是将配置文件中的时间单位unit作为一个时间窗口每个窗口仅允许限制流量内的请求通过如图。
我们将时间轴切分成一个一个的限流窗口,每个限流窗口有一个窗口开始时间和一个窗口结束时间,窗口开始时,计数器清零,每进入一个请求,计数器就记录+1。如果请求数目超过rpu配置的限流请求数就拒绝服务返回503响应。当前限流窗口结束后就进入下个限流窗口计数器再次清零重新开始。处理流程活动图如下。
上图包括“初始化”和“处理流程”两个泳道。初始化的时候,设置“窗口计数器”和“当前窗口结束时间”两个变量。处理请求的时候,判断当前时间是否大于“当前窗口结束时间”,如果大于,那么重置“窗口计数器”和“当前窗口结束时间”两个变量;如果没有,窗口计数器+1并判断计数器是否大于配置的限流请求数rpu根据结果决定是否进行限流。
这里的“窗口计数器”可以本地记录也可以远程记录也就是配置中的local和global。固定窗口算法在配置文件中algo项可配置“window”或者缩写“W”。
固定窗口实现比较容易但是如果使用这种限流算法在一个限流时间单位内通过的请求数可能是rpu的两倍无法达到限流的目的如下图。
假设单位时间请求限流数rpu为100在第一个限流窗口快要到结束时间的时候突然进来100个请求因为这个请求量在限流范围内所以没有触发限流请求全部通过。然后进入第二个限流窗口限流计数器清零。这时又忽然进入100个请求因为已经进入第二个限流窗口所以也没触发限流。在短时间内通过了200个请求这样可能会给系统造成巨大的负载压力。
滑动窗口Sliding Window限流算法
改进固定窗口缺陷的方法是采用滑动窗口限流算法,如下图。
滑动窗口就是将限流窗口内部切分成一些更小的时间片,然后在时间轴上滑动,每次滑动,滑过一个小时间片,就形成一个新的限流窗口,即滑动窗口。然后在这个滑动窗口内执行固定窗口算法即可。
滑动窗口可以避免固定窗口出现的放过两倍请求的问题,因为一个短时间内出现的所有请求必然在一个滑动窗口内,所以一定会被滑动窗口限流。
滑动窗口的算法实现基本和固定窗口一致,只要改动重置“窗口计数器”和“当前窗口结束时间”的逻辑就可以。固定窗口算法重置为窗口结束时间+1 unit 时间,滑动窗口算法重置为窗口结束时间+1个时间片。但是固定窗口算法重置后窗口计数器为0而滑动窗口需要将窗口计数器设置为当前窗口已经经过的时间片的请求总数比如上图里一个滑动窗口被分为5个时间片滑动窗口2的浅蓝色部分就是已经经过了4个时间片。
滑动窗口算法在配置文件中algo项可配置“sliding window”或者缩写“SW”。
漏桶Leaky Bucket限流算法
漏桶限流算法是模拟水流过一个有漏洞的桶进而限流的思路,如图。
水龙头的水先流入漏桶,再通过漏桶底部的孔流出。如果流入的水量太大,底部的孔来不及流出,就会导致水桶太满溢出去。
限流器利用漏桶的这个原理设计漏桶限流算法用户请求先流入到一个特定大小的漏桶中系统以特定的速率从漏桶中获取请求并处理。如果用户请求超过限流就会导致漏桶被请求数据填满请求溢出返回503响应。
所以漏桶算法不仅可以限流当流量超过限制的时候会拒绝处理直接返回503响应还能控制请求的处理速度。
实践中,可以采用队列当做漏桶。如图。
构建一个特定长度的队列queue作为漏桶开始的时候队列为空用户请求到达后从队列尾部写入队列而应用程序从队列头部以特定速率读取请求。当读取速度低于写入速度的时候一段时间后队列会被写满这时候写入队列操作失败。写入失败的请求直接构造503响应返回。
但是使用队列这种方式实际上是把请求处理异步化了写入请求的线程和获取请求的线程不是同一个线程并不适合我们目前同步网关的场景如果使用前面设计过的Flower框架开发的异步网关就可以用这种队列方式
因此Diana实现漏桶限流算法并不使用消息队列而是阻塞等待。根据限流配置文件计算每个请求之间的间隔时间例如限流每秒10个请求那么每两个请求的间隔时间就必须>=100ms。用户请求到达限流器后根据当前最近一个请求处理的时间和阻塞的请求线程数目计算当前请求线程的sleep时间。每个请求线程的sleep时间不同最后就可以实现每隔100ms唤醒一个请求线程去处理从而达到漏桶限流的效果。
计算请求线程sleep时间的伪代码如下
初始化 :
间隔时间 = 100ms;
阻塞线程数 = 0;
最近请求处理时间戳 = 0
long sleep时间(){
//最近没有请求,不阻塞
if((now - 最近请求处理时间戳) >= 间隔时间 and 阻塞线程数 <= 0{
最近请求处理时间戳 = now;
return 0; //不阻塞
}
//排队请求太多,漏桶溢出
if(阻塞线程数 > 最大溢出线程数) {
return MAX_TIME;//MAX_TIME表示阻塞时间无穷大当前请求被限流
}
//请求在排队,阻塞等待
阻塞线程数++;
return 间隔时间 * 阻塞线程数 - (now - 最近请求处理时间戳) ;
}
请求线程sleep时间结束继续执行的时候修改阻塞线程数
最近请求处理时间戳 = now;
阻塞线程数--;
注意,以上代码多线程并发执行,需要进行加锁操作。
使用漏桶限流算法,即使系统资源很空闲,多个请求同时到达时,漏桶也是慢慢地一个接一个地去处理请求,这其实并不符合人们的期望,因为这样就是在浪费计算资源。因此除非有特别的场景需求,否则不推荐使用该算法。
漏桶算法的algo配置项名称为“leaky bucket”或者“LB”。
令牌桶Token Bucket限流算法
令牌桶是另一种桶限流算法模拟一个特定大小的桶然后向桶中以特定的速度放入令牌token请求到达后必须从桶中取出一个令牌才能继续处理。如果桶中已经没有令牌了那么当前请求就被限流返回503响应。如果桶中的令牌放满了令牌桶也会溢出。
上面的算法描述似乎需要有一个专门线程生成令牌,还需要一个数据结构模拟桶。实际上,令牌桶的实现,只需要在请求获取令牌的时候,通过时间计算,就可以算出令牌桶中的总令牌数。伪代码如下:
初始化 :
最近生成令牌时间戳 = 0
总令牌数 = 0
令牌生成时间间隔 = 100ms;
boolean 获取令牌(){
//令牌桶中有令牌,直接取令牌即可
if(总令牌数 >= 1){
总令牌数--
return true;
}
//令牌桶中没有令牌了重算现在令牌桶中的总令牌数可能算出的总令牌数依然为0
总令牌数 = min(令牌数上限值,总令牌数 +
(now - 最近生成令牌时间戳) / 令牌生成时间间隔)
if(总令牌数 >= 1){
总令牌数--
最近生成令牌时间戳 = now//有令牌了,才能重设时间
return true
}
return false
}
令牌桶限流算法综合效果比较好能在最大程度利用系统资源处理请求的基础上实现限流的目标建议通常场景中优先使用该算法Diana的缺省配置算法也是令牌桶。令牌桶算法的algo配置项名称为“token bucket”或“TB”。
小结
限流器是一个典型的技术中间件,使用者是应用系统开发工程师,他们在自己的应用系统中使用限流器,通过配置文件来实现满足自己业务场景的限流需求。这里隐含了一个问题:大家都是开发者,这些应用系统开发工程师为什么要用你开发的中间件?事实上,技术中间件天然会受到更多的挑剔,架构师在设计技术组件的时候要格外考虑易用性和扩展性,开发出来的技术中间件要能经得起同行的审视和挑战。
这篇设计文档中,包含了很多伪代码,这些伪代码是限流算法实现的核心逻辑。架构师一方面需要思考宏观的技术决策,一方面要思考微观的核心代码。这里两方面的能力支撑起架构师的技术影响力,既要能上得厅堂,在老板、客户等外部相关方面前侃侃而谈,保障自己和团队能掌控自己的技术方向;也要能下得厨房,搞定最有难度的代码实现,让团队成员相信跟着你混,没有迈不过去的技术坎。
思考题
滑动窗口算法中,如何管理时间片,以及如何计算滑动过程中的一个窗口内各个时间片的窗口计数器之和?用什么样的数据结构和算法比较合适?
欢迎在评论区分享你的思考,我们共同进步。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 高可用架构的十种武器:怎么度量系统的可用性?
你好,我是李智慧。
互联网应用是面向一般大众的应用系统,他们可能会随时需要使用应用,那么应用就必须要保持随时可用,即所谓的\(\\small 7\\times24\)小时可用。但是互联网应用又可能会遇到硬件故障、软件故障、黑客攻击等等各种不可用的场景。
业界通常用多少个9来说明互联网应用的可用性。比如说淘宝的可用性是4个9就是说淘宝的服务99.99%可用。这句话的意思是淘宝的服务要保证在所有的运行时间里只有0.01%不可用那么一年就只有大概53分钟不可用。这个99.99%就叫做系统的可用性指标,这个值的计算公式是:\(\\small 可用性指标=1-年度不可用时间\\div年度总时间\\times100%\)
可用性指标反映系统的可用程度也可以估算出年度不可用时间。我们熟悉的互联网产品淘宝、百度、微信等的可用性大多是4个9。
不同的应用可用性可能会相差很大,主要差别就是在面对各种故障的时候,高可用设计做得是否足够好,我总结了一些高可用架构的技术方案,并称之为高可用架构的十种武器。
第一种武器:解耦
耦合度过高是软件设计的万恶之源也是造成系统可用性问题的罪魁祸首。一个高度耦合的系统牵一发而动全身任何微小的改动都可能会导致意想不到的bug和系统崩溃。连最基本的功能维护都已经勉为其难更不用奢谈什么高可用了。
历数软件技术进化史就是一部软件开发解耦的历史。从汇编语言到面向过程的语言再到面向对象的语言编程语言的要素本身就越来越低耦合。各种编程框架的出现也几乎只有一个目标使软件变得更加低耦合。Web应用容器使得HTTP协议处理与业务开发解耦开发者不需要关注网络通信和协议处理只需要关注请求和响应对象的逻辑处理即可。MVC框架进一步将视图逻辑与业务逻辑解耦前后端工作进一步分离。
这里,我再介绍两种低耦合的设计原则。
组件的低耦合原则无循环依赖原则即技术组件之间不能循环依赖不能A依赖BB又依赖A稳定依赖原则即被依赖的组件尽量稳定尽量少因为业务变化而变化稳定抽象原则即要想使组件稳定组件就要更加抽象。
面向对象的低耦合原则:开闭原则,即对修改封闭、对扩展开放,对象可以扩展新功能,但是不能修改代码;依赖倒置原则,即高层对象不能依赖低层对象,而是要依赖抽象接口,而抽象接口属于高层;接口隔离原则,不要强迫使用者依赖它们不需要的方法,要用接口对方法进行隔离。
第二种武器:隔离
如果说解耦是逻辑上的分割,那么隔离就是物理上的分割。即将低耦合的组件进行独立部署,将不同组件在物理上隔离开来。每个组件有自己独立的代码仓库;每个组件可以独立发布,互不影响;每个组件有自己独立的容器进行部署,互不干扰。
所以隔离就是分布式技术在业务上的应用最常见的就是我们前面案例中也多次使用的微服务技术方案。微服务将一个复杂的大应用单体架构系统进行拆解拆分成若干更细粒度的微服务这些微服务之间互相依赖实现原来大应用的功能逻辑。然后将这些微服务独立开发和发布独立部署微服务之间通过RPC远程过程调用进行依赖调用就是微服务架构。
隔离使得系统间关系更加清晰,故障可以更加隔离开来,问题的发现与解决也更加快速,系统的可用性也更高。
不过,还要强调一下,隔离必须在低耦合的基础上进行才有意义。如果组件之间的耦合关系千头万绪、混乱不堪,隔离只会让这种混乱更雪上加霜。
第三种武器:异步
异步可以认为是在隔离的基础上进一步解耦,将物理上已经分割的组件之间的依赖关系进一步切断,使故障无法扩散,提高系统可用性。异步在架构上的实现手段主要是使用消息队列。
比如用户注册的场景。新用户提交注册请求后,需要给用户发送邮件,发送短信,保存数据库,还要将注册消息同步给其他产品等等。如果用微服务调用的方式,那么后续操作任何一个故障,都会导致业务处理失败,用户无法完成注册。
使用消息队列的异步架构,新用户注册消息发送给消息队列就立即返回,后续的操作通过消费消息来完成,即使某个操作发生故障也不会影响用户注册成功。如下图。
第四种武器:备份
备份主要解决硬件故障下系统的可用性即一个服务部署在多个服务器上当某个服务器故障的时候请求切换到其他服务器上继续处理保证服务是可用的。所以备份与失效转移failover总是成对出现的共同构成一个高可用解决方案。
最常见的备份就是负载均衡,前面的课程中说过,负载均衡主要解决高性能问题。但是,多台服务器构成一个集群,这些服务器天然就是互相备份的关系,任何一台服务器失效,只需要将分发到这台服务器的请求分发给其他服务器即可,如下图
由于应用服务器上只运行程序不存储数据所以请求切换到任何一台服务器处理结果都是相同的。而对于存储数据的服务器比如数据库互相备份的服务器必须要互相同步数据下图是MySQL主主备份的架构图。
第五种武器:重试
远程服务可能会由于线程阻塞、垃圾回收或者网络抖动,而无法及时返回响应,调用者可以通过重试的方式修复单次调用的故障。
需要注意的是,重试是有风险的。比如一个转账操作,第一次请求转账后没有响应,也许仅仅是响应数据在网络中超时了,如果这个时候进行重试,那么可能会导致重复转账,反而造成重大问题。
所以,可以重试的服务必须是幂等的。所谓幂等,即服务重复调用和调用一次产生的结果是相同的。有些服务天然具有幂等性,比如将用户性别设置为男性,不管设置多少次,结果都一样。
第六种武器:熔断
重试主要解决偶发的因素导致的单次调用失败,但是如果某个服务器一直不稳定,甚至已经宕机,再请求这个服务器或者进行重试都没有意义了。所以为了保证系统整体的高可用,对于不稳定或者宕机的服务器需要进行熔断。
熔断的主要方式是使用断路器阻断对故障服务器的调用,断路器状态图如下。
断路器有三种状态,关闭、打开、半开。断路器正常情况下是关闭状态,每次服务调用后都通知断路器。如果失败了,失败计数器就+1如果超过开关阈值断路器就打开这个时候就不再请求这个服务了。过一段时间达到断路器预设的时间窗口后断路器进入半开状态发送一个请求到该服务如果服务调用成功那么说明服务恢复断路器进入关闭状态即正常状态如果服务调用失败那么说明服务故障还没修复断路器继续进入到打开状态服务不可用。
第七种武器:补偿
前面几种方案都是故障发生时如何处理,而补偿则是故障发生后,如何弥补错误或者避免损失扩大。比如将处理失败的请求放入一个专门的补偿队列,等待失败原因消除后进行补偿,重新处理。
补偿最典型的使用场景是事务补偿。在一个分布式应用中,多个相关事务操作可能分布在不同的服务器上,如果某个服务器处理失败,那么整个事务就是不完整的。按照传统的事务处理思路,需要进行事务回滚,即将已经成功的操作也恢复到事务以前的状态,保证事务的一致性。
传统的事务回滚主要依赖数据库的特性当事务失败的时候数据库执行自己的undo日志就可以将同一个事务的多条数据记录恢复到事务之初的状态。但是分布式服务没有undo日志所以需要开发专门的事务补偿代码当分布式事务失效的时候调用事务补偿服务将事务状态恢复如初。
第八种武器:限流
在高并发场景下,如果系统的访问量超过了系统的承受能力,可以通过限流对系统进行保护。限流是指对进入系统的用户请求进行流量限制,如果访问量超过了系统的最大处理能力,就会丢弃一部分用户请求,保证整个系统可用。这样虽然有一部分用户的请求被丢弃,但大部分用户还是可以访问系统的,总比整个系统崩溃,所有的用户都不可用要好。
我们在[第15篇]专门讨论过限流器的设计,这里不再赘述。
第九种武器:降级
降级是保护系统高可用的另一种手段。有一些系统功能是非核心的,但是也给系统产生了非常大的压力,比如电商系统中有确认收货这个功能,即便用户不确认收货,系统也会超时自动确认。
但实际上确认收货是一个非常重的操作,因为它会对数据库产生很大的压力:它要进行更改订单状态,完成支付确认,并进行评价等一系列操作。如果在系统高并发的时候去完成这些操作,那么会对系统雪上加霜,使系统的处理能力更加恶化。
解决办法就是在系统高并发的时候(例如淘宝双十一),将确认收货、评价这些非核心的功能关闭,也就是对系统进行降级,把宝贵的系统资源留下来,给正在购物的人,让他们去完成交易。
第十种武器:多活
多活,即异地多活,在多个地区建立数据中心,并都可以对用户提供服务,任何地区级的灾难都不会影响系统的可用。异地多活的架构案例我们已经在[第14讲]讨论过了。异地多活最极端的案例,是某应用准备将自己的服务器发射到太空,即使地球毁灭也能保证系统可用。
异地多活的架构需要考虑的重点是,用户请求如何分发到不同的机房去。这个主要可以在域名解析的时候完成,也就是用户进行域名解析的时候,会根据就近原则或者其他一些策略,完成用户请求的分发。另一个至关重要的技术点是,因为是多个机房都可以独立对外提供服务,所以也就意味着每个机房都要有完整的数据记录。用户在任何一个机房完成的数据操作,都必须同步传输给其他的机房,进行数据实时同步。
数据库实时同步最需要关注的就是数据冲突问题。同一条数据同时在两个数据中心被修改了该如何解决某些容易引起数据冲突的服务采用类似MySQL的主主模式也就是说多个机房在某个时刻是有一个主机房的某些请求只能到达主机房才能被处理其他的机房不处理这一类请求以此来避免关键数据的冲突。
小结
除了以上的高可用架构方案,还有一些高可用的运维方案。
通过自动化测试减少系统的Bug。对于一个稳定运行的系统每次变更发布可能只改动极小的一部分如果只测试这一小部分的功能那么潜在的其他可能引起故障的连带变更就会被忽视进而可能引发大问题。但是如果全部都回归测试一遍投入的测试成本又非常高。自动化测试可以实现自动化回归对于那些没有变更的功能自动发现是否有引入的Bug或预期之外的变更。
通过自动化监控尽早发现系统的故障。监控系统是技术团队的眼睛,没有监控的系统犹如盲人在崎岖的山路狂奔。所以,一个成熟的高可用系统中必定包含着完整的监控系统,实时监控各种技术指标和业务指标的变化。如果系统出现故障,超过设定的阈值就会引发监控系统报警,或者启动自动化故障修复服务。
通过预发布验证发现测试环境无法发现的Bug。系统在发布上线之前要经过各种测试但是测试环境和线上运行环境还是会有很多不同。所以需要在线上集群中部署一台专门的预发布服务器这台服务器访问的数据和资源完全是线上的但是不会被用户访问到。开发人员发布代码的时候先发布到这台预发布服务器然后在这台服务器上做预发布验证没有问题才会将代码发布到其他服务器上如果有问题也不会影响到用户访问保证系统的高可用。
此外还可以通过灰度发布降低软件错误带来的影响。在一个大规模的应用集群中每次只发布一小部分服务器观察没有问题再继续发布保证即使程序有Bug产生的影响也控制在较小的范围内。
思考题
你还能想到哪些文中没有提到的高可用方法?
欢迎在评论区补充你的思考,我们共同进步。

View File

@ -0,0 +1,213 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 Web 应用防火墙:怎样拦截恶意用户的非法请求?
你好,我是李智慧。
Web应用防火墙Web Application Firewall WAF通过对HTTP(S)请求进行检测识别并阻断SQL注入、跨站脚本攻击、跨站请求伪造等攻击保护Web服务安全稳定。
Web安全是所有互联网应用必须具备的功能没有安全防护的应用犹如怀揣珠宝的儿童独自行走在盗贼环伺的黑夜里。我们准备开发一个Web应用防火墙该防火墙可作为Web插件部署在Web应用或者微服务网关等HTTP服务的入口拦截恶意请求保护系统安全。我们准备开发的Web应用防火墙名称为“Zhurong祝融”。
需求分析
HTTP请求发送到Web服务器时请求首先到达Zhurong防火墙防火墙判断请求中是否包含恶意攻击信息。如果包含防火墙根据配置策略可选择拒绝请求返回418状态码也可以将请求中的恶意数据进行消毒处理也就是对恶意数据进行替换或者插入某些字符从而使请求数据不再具有攻击性然后再调用应用程序处理。如下图
Zhurong需要处理的攻击和安全漏洞列表
概要设计
Zhurong能够发现恶意攻击请求的主要手段是对HTTP请求内容进行正则表达式匹配将各种攻击类型可能包含的恶意内容构造成正则表达式然后对HTTP请求头和请求体进行匹配。如果匹配成功那么就触发相关的处理逻辑直接拒绝请求或者将请求中的恶意内容进行消毒即进行字符替换使攻击无法生效。
其中恶意内容正则表达式是通过远程配置来获取的。如果发现了新的攻击漏洞远程配置的漏洞攻击正则表达式就会进行更新并在所有运行了Zhurong防火墙的服务器上生效拦截新的攻击。组件图如下
HTTP请求先到达请求过滤器请求过滤器提取HTTP请求头和HTTP请求体中的数据这个过滤器其实就是Java中的Filter。过滤器调用漏洞策略处理器进行处理而漏洞策略处理器需要调用漏洞定义文件加载模块获得漏洞定义规则漏洞定义文件加载模块缓存了各种漏洞定义规则文件如果缓存超时就从远程配置中心重新加载漏洞定义规则。
漏洞定义规则文件是Zhurong的核心该文件定义了攻击的正则表达式过滤器正是通过使用这些正则表达式匹配HTTP请求头和HTTP请求体的方式识别出HTTP请求中是否存在攻击内容。同时漏洞定义规则文件中还定义了发现攻击内容后的处理方式是拒绝请求跳转到出错页面还是采用消毒的方式将攻击内容字符进行替换。
漏洞规则定义文件采用XML格式示例如下
<?xml version="1.0"?>
<recipe
attacktype="Sql"
path="^/protectfolder/.*$"
description="Sql injection attacks"
>
<ruleSet
stage = "request"
condition = "or"
>
<action
name="forward"
arg="error.html"
/>
<rule
operator = "regex"
arg = "paramNames[*]"
value = "select|update|delete|count|*|sum|master|script|'|declare|
          or|execute|alter|statement|executeQuery|count|executeUpdate"
/>
</ruleSet>
<ruleSet
stage = "response"
condition = "or"
>
<action
name ="replace"
arg = " "
/>
<rule
operator = "regex"
arg = " responseBody "
value = "(//.+\n)|(/**.+*/)|(<!--.*-->)"
/>
</ruleSet>
</recipe>
recipe是漏洞定义文件的根标签属性attacktype表示处理的攻击类型有以下几种。
SQL SQL注入攻击
XSS 跨站点脚本攻击
CSC 注释与异常信息泄露
CSRF 跨站点请求伪造
FB 路径遍历与强制浏览-
path表示要处理的请求路径可以为空表示处理所有请求路径。
ruleSet是漏洞处理规则集合一个漏洞文件可以包含多个ruleSet。stage标签表示处理的阶段请求阶段request响应阶段response。condition表示和其他规则的逻辑关系“or”表示“或”关系即该规则处理完成后其他规则不需要再处理“and”表示“与”关系该规则处理完成后其他规则还需要再处理。
action表示发现攻击后的处理动作。“forward”表示表示跳转到出错页面后面的“arg”表示要跳转的路径“replace”表示对攻击内容进行替换即所谓的消毒使其不再具有攻击性后面的“arg”表示要替换的内容。
rule表示漏洞规则触发漏洞规则就会引起action处理动作。operator表示如何匹配内容中的攻击内容“regex”表示正则表达式匹配“urlmatch”表示URL路径匹配。“arg”表示要匹配的目标可以是HTTP请求参数名、请求参数值、请求头、响应体、ULR路径。“value”就是匹配攻击内容的正则表达式。
详细设计
Zhurong可以处理的攻击类型有哪些它们的原理是什么Zhurong对应的处理方法又是什么详细设计将解决这些问题。
XSS跨站点脚本攻击
XSS 攻击即跨站点脚本攻击(Cross Site Script),指黑客通过篡改网页,注入恶意 JavaScript脚本在用户浏览网页时控制用户浏览器进行恶意操作的一种攻击方式。
常见的 XSS 攻击类型有两种,一种是反射型,攻击者诱使用户点击一个嵌入恶意脚本的链接,达到攻击的目的。如图:
攻击者发布的微博中有一个含有恶意脚本的 URL在实际应用中该脚本在攻击者自己的服务器 www.2kt.cn上URL 中包含脚本的链接),用户点击该 URL会自动关注攻击者的新浪微博 ID发布含有恶意脚本 URL 的微博,攻击就被扩散了。
另外一种 XSS 攻击是持久型 XSS 攻击,黑客提交含有恶意脚本的请求,保存在被攻击的 Web 站点的数据库中,用户浏览网页时,恶意脚本被包含在正常页面中,达到攻击的目的。如图:
此种攻击经常使用在论坛、博客等 Web 应用中。
Zhurong采用正则表达式匹配含有XSS攻击内容的请求正则表达式如下
"(?:\b(?:on(?:(?:mo(?:use(?:o(?:ver|ut)|down|move|up)|ve)|key(?:press|down|up)|c(?:hange|lick)|s(?:elec|ubmi)t|(?:un)?load|dragdrop|resize|focus|blur)\b\W*?=|abort\b)|(?:l(?:owsrc\b\W*?\b(?:(?:java|vb)script|shell)|ivescript)|(?:href|url)\b\W*?\b(?:(?:java|vb)script|shell)|background-image|mocha):|type\b\W*?\b(?:text\b(?:\W*?\b(?:j(?:ava)?|ecma)script\b|[vbscript])|application\b\W*?\bx-(?:java|vb)script\b)|s(?:(?:tyle\b\W*=.*\bexpression\b\W*|ettimeout\b\W*?)\(|rc\b\W*?\b(?:(?:java|vb)script|shell|http):)|(?:c(?:opyparentfolder|reatetextrange)|get(?:special|parent)folder)\b|a(?:ctivexobject\b|lert\b\W*?\())|<(?:(?:body\b.*?\b(?:backgroun|onloa)d|input\b.*?\\btype\b\W*?\bimage)\b|![CDATA[|script|meta)|(?:.(?:(?:execscrip|addimpor)t|(?:fromcharcod|cooki)e|innerhtml)|\@import)\b)"
匹配成功后根据漏洞定义文件可以选择forward到错误页面也可以采用replace方式进行消毒replace消毒表如下
在XSS攻击字符前后加上“&nbsp;”字符串,使得攻击脚本无法运行,同时在浏览器显示的时候不会影响显示内容。
SQL注入攻击
SQL 注入攻击的原理如下:
攻击者在 HTTP 请求中注入恶意 SQL 命令(drop table users;),服务器用请求参数构造数据库 SQL 命令时,恶意 SQL 被一起构造,并在数据库中执行。
如果在Web页面中有个输入框要求用户输入姓名普通用户输入一个普通的姓名Frank那么最后提交的HTTP请求如下
http://www.a.com?username=Frank
服务器在处理计算后向数据库提交的SQL查询命令如下
Select id from users where username='Frank';
但是恶意攻击者可能会提交这样的HTTP请求
http://www.a.com?username=Frank';drop table users;--
即输入的uername是
Frank';drop table users;--
这样服务器在处理后最后生成的SQL是这样的
Select id from users where username='Frank';drop table users;--';
事实上这是两条SQL一条select查询SQL一条drop table删除表SQL。数据库在执行完查询后就将users表删除了系统崩溃了。
处理SQL注入攻击的rule正则表达式如下。
(?:\b(?:(?:s(?:elect\b(?:.{1,100}?\b(?:(?:length|count|top)\b.{1,100}?\bfrom|from\b.{1,100}?\bwhere)|.*?\b(?:d(?:ump\b.*\bfrom|ata_type)|(?:to_(?:numbe|cha)|inst)r))|p_(?:(?:addextendedpro|sqlexe)c|(?:oacreat|prepar)e|execute(?:sql)?|makewebtask)|ql_(?:longvarchar|variant))|xp_(?:reg(?:re(?:movemultistring|ad)|delete(?:value|key)|enum(?:value|key)s|addmultistring|write)|e(?:xecresultset|numdsn)|(?:terminat|dirtre)e|availablemedia|loginconfig|cmdshell|filelist|makecab|ntsec)|u(?:nion\b.{1,100}?\bselect|tl_(?:file|http))|group\b.*\bby\b.{1,100}?\bhaving|load\b\W*?\bdata\b.*\binfile|(?:n?varcha|tbcreato)r|autonomous_transaction|open(?:rowset|query)|dbms_java)\b|i(?:n(?:to\b\W*?\b(?:dump|out)file|sert\b\W*?\binto|ner\b\W*?\bjoin)\b|(?:f(?:\b\W*?\(\W*?\bbenchmark|null\b)|snull\b)\W*?\()|(?:having|or|and)\b\s+?(?:\d{1,10}|'[^=]{1,10}')\s*?[=<>]+|(?:print]\b\W*?\@|root)\@|c(?:ast\b\W*?\(|oalesce\b))|(?:;\W*?\b(?:shutdown|drop)|\@\@version)\b|'(?:s(?:qloledb|a)|msdasql|dbo)')
从请求中匹配到SQL注入攻击内容后可以设置跳转错误页面也可以选择消毒replacereplace表如下
CSRF跨站点请求伪造攻击
CSRF(Cross Site Request Forgery跨站点请求伪造),攻击者通过跨站请求,以合法用户的身份进行非法操作,如转账交易、发表评论等,如图:
CSRF 的主要手法是利用跨站请求,在用户不知情的情况下,以用户的身份伪造请求。其核心是利用了浏览器 Cookie 或服务器 Session 策略,盗取用户身份。
Zhurong的防攻击策略是过滤器自动在所有响应页面的表单form中添加一个隐藏字段合法用户在提交请求的时候会将这个隐藏字段发送到服务器防火墙检查隐藏字段值是否正确来确定是否为CSRF攻击。恶意用户的请求是自己伪造的无法构造这个隐藏字段就会被防火墙拦截。
注释与异常信息泄露
为调试程序方便或其他不恰当的原因,有时程序开发人员会在前端页面程序中使用 HTML 注释语法进行程序注释,这些 HTML 注释就会显示在客户端浏览器,给黑客造成攻击便利。
此外,许多 Web 服务器默认是打开异常信息输出的,即服务器端未处理的异常堆栈信息会直接输出到客户端浏览器,这种方式虽然对程序调试和错误报告有好处,但同时也给黑客造成可乘之机。黑客通过故意制造非法输入,使系统运行时出错,获得异常信息,从而寻找系统漏洞进行攻击。
匹配HTML注释的正则表达式如下
&lt;!--(.|&#x000A;|&#x000D;)*--&gt;
如果匹配到HTML注释就用空字符串replace该注释。
对于异常信息泄露Zhurong会检查响应状态码。如果响应状态码为500系列错误则会进一步匹配响应体内容检查是否存在错误堆栈信息。
小结
这篇设计文档也是改编自某全球IT企业的内部设计文档这个产品和该企业的Web服务器捆绑销售已经在全球范围内售卖了十几年。这个产品也是中国分公司成立之初最成功的产品帮助中国分公司奠定了自己在总公司的地位。而这个产品的最初版本则是一个架构师带领一个开发小组花了几个月的时间就开发出来的。
人们常说软件工程师的职业生涯只有十几年,甚至只有几年。事实上,很多商业软件的生命周期都不止十几年,也就是说,在你的职业生涯中,只要开发出一款成功的软件,光是为这个软件修修补补、维护升级,你也能干个十几年,几十年。
但是很遗憾,就我所见,大多数软件工程师在自己的职业生涯中都没有经历过成功。要么就是加入一个已经成功的项目修修补补,要么就是在一个不温不火的项目里耗了几年,最后无疾而终。事实上,经历过成功的人会明白什么样的项目将会走向成功,所以不会守着一个成功的项目养老,而是不断追求新的成功;而没有经历过成功的人则在曲曲折折中走向自己的中年危机。
我们这个专栏挑选的设计,都是基于一些已经成功了的案例。成功的东西有一种成功的味道,正是这种味道带领成功者走向成功。希望你在学习技术的同时,也能嗅到成功的味道。
思考题
还有哪些常见的Web安全漏洞如何进行防护
极客时间也有一个专门讲Web漏洞攻击与防护的专栏[Web 漏洞挖掘实战],有兴趣的同学不妨看一看,再回来一起交流讨论。

View File

@ -0,0 +1,216 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 加解密服务平台:如何让敏感数据存储与传输更安全?
你好,我是李智慧。
在一个应用系统运行过程中,需要记录、传输很多数据,这些数据有的是非常敏感的,比如用户姓名、手机号码、密码、甚至信用卡号等等。这些数据如果直接存储在数据库,记录在日志中,或者在公网上传输的话,一旦发生数据泄露,不但可能会产生重大的经济损失,还可能会使公司陷入重大的公关与法律危机。公司上下辛苦十几年,一夜回到解放前。
所以,敏感信息必须进行加密处理,也就是把敏感数据以密文的形式存储、传输。这样即使被黑客攻击,发生数据泄露,被窃取的数据也是密文,获取数据的人无法得到真实的明文内容,敏感数据依然被保护着。而当应用程序需要访问这些密文的时候,只需要进行数据解密,即可还原得到原始明文数据。加解密处理既保证了数据的安全,又保证了数据的正常访问。
但是,这一切的前提是加密和解密过程的安全。加密、解密过程由加密算法、加密密钥、解密算法、解密密钥组成。下图是一个对称加密、解密过程。对称加密密钥和解密密钥是同一个密钥,调用加密算法可将明文加密为密文,调用解密算法可将密文还原为明文。
所以,如果窃取数据的人知道了解密算法和密钥,即使数据是加密的,也可以轻松对密文进行还原,得到原始的明文数据。而很多时候,解密算法和密钥都以源代码的方式保存在代码仓库里,黑客如果窃取了源代码,或者内部人泄露了源代码,那么所有的秘密就都不是秘密了。
此外,在某些情况下,我们的系统需要和外部系统进行对称加密数据传输,比如和银行加密传输信用卡卡号,这时候涉及到密钥交换,即我方人员和银行人员对接,直接传递密钥。如果因密钥泄露导致重大经济损失,那么持有密钥的人员将无法自证清白,这又会导致没有人愿意保管密钥。
因此我们设计了一个加解密服务系统系统名称为“Venus”统一管理所有的加解密算法和密钥。应用程序只需要依赖加解密服务SDK调用接口进行加解密即可而真正的算法和密钥在系统服务端进行管理保证算法和密钥的安全。
需求分析
一般说来,日常开发中的加解密程序存在如下问题:
密钥(包括非对称加解密证书)保存在源文件或者配置文件中,存储分散而不安全。
密钥没有分片交换机制,不能满足高安全级密钥管理和交换的要求。
密钥缺乏版本管理,不能灵活升级,一旦修改密钥,此前加密的数据就可能无法解密。
加密解密算法程序不统一,同样算法不同实现,内部系统之间密文不能正确解析。
部分加解密算法程序使用了弱加解密算法和弱密钥,存在安全隐患。
为此,我们需要设计开发一个专门的加解密服务及密钥管理系统,以解决以上问题。
Venus是一个加解密服务系统核心功能是加解密服务辅助功能是密钥与算法管理。此外Venus还需要满足以下非功能需求
安全性需求-
必须保证密钥的安全性,保证没有人能够有机会看到完整的密钥。因此一个密钥至少要拆分成两片,分别存储在两个异构的、物理隔离的存储服务器中 。在需要进行密钥交换的场景中,将密钥至少拆分成两个片段,每个管理密钥的人只能看到一个密钥片段,需要双方所有人分别交接才能完成一次密钥交换。
可靠性需求-
加解密服务必须可靠,即保证高可用。无论在加解密服务系统服务器宕机、还是网络中断等各种情况下,数据正常加解密都需要得到保障。
性能需求-
加解密计算的时间延迟主要花费在加解密算法上,也就是说,加载加解密算法程序、获取加解密密钥的时间必须短到可以忽略不计。
根据以上加解密服务系统功能和非功能需求,系统用例图设计如下:
系统主要参与者Actor包括
系统主要用例过程和功能包括:
开发工程师使用密钥管理功能为自己开发的应用申请加解密算法和密钥;
安全工程师使用密钥管理功能审核算法和密钥的强度是否满足数据安全要求;
(经过授权的)密钥管理者使用密钥管理功能可以查看密钥(的一个分片);
应用程序调用加解密功能完成数据的加密、解密;
加密解密功能和密钥管理功能调用密钥服务功能完成密钥的存储和读取;
密钥服务功能访问一个安全、可靠的密钥存储系统读写密钥。
总地说来Venus应满足如下需求
集中、分片密钥存储与管理,多存储备份,保证密钥安全易管理。
密钥申请者、密钥管理者、密钥访问者,多角色多权限管理,保证密钥管理与传递的安全。
通过密钥管理控制台完成密钥申请、密钥管理、密钥访问控制等一系列密钥管理操作,实现便捷的密钥管理。
统一加解密服务API简单接口统一算法为内部系统提供一致的加解密算法实现。
概要设计
针对上述加解密服务及密钥安全管理的需求设计加解密服务系统Venus整体结构如下
应用程序调用Venus提供的加解密SDK服务接口对信息进行加解密该SDK接口提供了常用的加密解密算法并可根据需求任意扩展。SDK加解密服务接口调用Venus密钥服务器的密钥服务以取得加解密密钥并缓存在本地。而密钥服务器中的密钥则来自多个密钥存储服务器一个密钥分片后存储在多个存储服务器中每个服务器都由不同的人负责管理。密钥申请者、密钥管理者、安全审核人员通过密钥管理控制台管理更新密钥每个人各司其事没有人能查看完整的密钥信息。
部署模型
Venus部署模型如图
Venus系统的核心服务器是Key Server服务器提供密钥管理服务。密钥分片存储在文件服务器File Store和数据库DB中。
使用Venus加解密服务的应用程序Application部署在应用程序服务器App Server依赖Venus提供的SDK API进行数据加解密。而Venus SDK 则是访问密钥服务器Key Server来获取加解密算法代码和密钥。
安全起见密钥将被分片存储在文件服务器Key File Store和数据库服务器Key DB中。所以Key Server服务器中部署了密钥管理组件Key Manager用于访问数据库中的应用程序密钥元信息Key Meta Data以此获取密钥分片存储信息。Key Server服务器根据这些信息访问File Store和DB获取密钥分片并把分片拼接为完整密钥最终返回给SDK。
此外密钥管理控制台Key Console提供一个web页面供开发工程师、安全工程师、密钥管理者进行密钥申请、更新、审核、查看等操作。
加解密调用时序图
加解密调用过程如下时序图所示。
应用程序App调用Venus SDK对数据进行加密解密
SDK检查在本地是否有缓存加解密需要的密钥和加解密算法代码如果有缓存就直接使用该算法和密钥进行加解密。
如果本地没有缓存密钥和算法,请求远程服务器返回密钥和算法。
部署在Venus服务器的Key Manager收到请求后访问数据库检查该应用配置的密钥和算法Meta信息。
数据库返回的Mata信息中包括了密钥的分片信息和存储位置Key Manager访问文件服务器和数据库获取密钥分片并将多个分片合并成一个完整密钥返回给客户端SDK。
SDK收到密钥后缓存在本地进程内存中并完成对App加解密调用的处理。
通过该设计我们可以看到Venus对密钥进行分片存储不同存储服务器由不同运维人员管理。就算需要进行密钥交换那么参与交换的人员每个人也只能获得一个密钥分片无法得到完整的密钥这样就保证了密钥的安全性。
密钥缓存在SDK所在的进程也就是应用程序App所在的进程只有第一次调用时会访问远程的Venus服务器其他调用只访问本进程缓存。因此加解密的性能只受加解密的数据大小和算法的影响不受Venus服务的性能影响满足了性能要求。
同时由于密钥在缓存中如果Venus服务器临时宕机或者网络通信中断也不会影响到应用程序的正常使用保证了Venus的可靠性。但是如果Venus服务器长时间宕机那么应用重新启动本地缓存被清空就需要重新请求密钥这时候应用就不可用了。那么Venus如何在这种情况下仍然保证高可用呢
解决方案就是对Venus服务器、数据库和文件服务器做高可用备份。Venus服务器部署2-3台服务器构建一个小型集群SDK通过软负载均衡访问Venus服务器集群若发现某台Venus服务器宕机就进行失效转移。同样数据库和文件服务器也需要做主从备份。
详细设计
Venus详细设计主要关注SDK核心类设计。其他的例如数据库结构设计、服务器密钥管理Console设计等这里不做展开。
密钥领域模型
为了便于SDK缓存、管理密钥信息以及SDK与Venus服务端传输密钥信息我们设计了一个密钥领域模型如下图
一个应用程序使用的所有密钥信息都记录在KeyBox对象中KeyBox对象中有一个keySuitMap成员变量这个map的key是密钥名称value是一个KeySuit对象。
KeySuit类中有一个keyChainMap成员变量这个map类的key是版本号value是一个KeyChain对象。Venus因为安全性需求需要支持多版本的密钥。也就是说对同一类数据的加密密钥过一段时间就会进行版本升级这样即使密钥泄露也只会影响一段时间的数据不会导致所有的数据都被解密。
KeySuit类的另一个成员变量currentVersion记录当前最新的密钥版本号也就是当前用来进行数据加密的密钥版本号。而解密的时候则需要从密文数据中提取出加密密钥版本号或者由应用程序自己记录密钥版本号在解密的时候提供给Venus SDK API根据这个版本号获取对应的解密密钥。
具体每个版本的密钥信息记录在KeyChain中包含了密钥名称name、密钥版本号version、加入本地缓存的时间cache_time、该版本密钥创建的时间versionTime、对应的加解密算法algorithm当然还有最重要的密钥分片列表keyChipList里面按序记录着这个密钥的分片信息。
KeyChip记录每个密钥分片包括分片编号no以及分片密钥内容chip。
核心服务类设计
应用程序通过调用加解密API VenusService完成数据加解密。如下图
Venus SDK的核心类是VenusService应用程序调用该对象的encrypt方法进行加密decrypt方法进行解密。应用程序需要构造VenusData对象将加解密数据传给VenusServiceVenusService加解密完成后创建一个新的VenusData对象将加解密的结果写入该对象并返回。VenusData成员变量在后面详细讲解。
VenusService通过VenusConnector类连接Venus服务器获取密钥KeyBox和算法Algorithm并调用Algorithm的对应方法完成加解密。
以加密为例,具体处理过程时序图如下:
首先应用程序App创建VenusData对象并将待加密数据写入该对象。接着App调用VenusService的encrypt方法进行加密VenusService检查加密需要的密钥和算法是否已经有缓存如果没有就调用VenusConnector请求服务器返回密钥和算法。VenusConnector将根据返回的算法字节码来构造加密算法的实例对象同时根据返回的密钥构造相关密钥对象并写入KeyBox完成更新。
下一步VenusService会根据更新后的KeyBox中的密钥和算法进行加密并将加密结果写入VenusData。最后应用程序App从返回的VenusData中获取加密后的数据即可。
加解密数据接口VenusData设计
VenusData用于表示Venus加解密操作输入和输出的数据也就是说加解密的时候构造VenusData对象调用Service对应的方法加解密完成后返回值还是一个VenusData对象。
VenusData包含的属性如下图
VenusData用作输入时
属性bytes和text只要设置一个即要么处理的是二进制bytes数据要么是Striing数据如果两个都设置了Venus会抛出异常。
属性version可以不设置即null表示Venus操作使用的密钥版本是当前版本。
属性outputWithText表示输出的VenusData是否处理为text类型缺省值是true。
属性dataWithVersion表示加密后的VenusData的bytes和text 中是否包含使用密钥的版本信息这样在解密的时候可以不指定版本缺省值是false。
如果dataWithVersion设置为true即表示加密后密文内包含版本号这种情况下VenusService需要在密文头部增加3个字节的版本号信息其中头两个字节为固定的magic code0x5E、0x23第三个字节为版本号也就是说密钥版本号只占用一个字节最多支持256个版本
VenusData用作输出时Venus会设置属性keyName和输入时的值一样、version、 bytes、 outputWithText、dataWithVersion和输入时的值一样并根据输入的 outputWithText决定是否设置text属性。
测试用例代码demo
public static void testVenusService() throws Exception {
// 准备数据
VenusData data1 = new VenusData();
data1.setKeyName("aeskey1");
data1.setText("PlainText");
// 加密操作
VenusData encrypt = VenusService.encrypt(data1);
System.out.printf("Key Name: %s, Secret Text: %s, Version: %d.\n", encrypt.getKeyName(),
encrypt.getText(), encrypt.getVersion());
// 准备数据
VenusData data2 = new VenusData();
data2.setKeyName("aeskey1");
data2.setBytes(encrypt.getBytes());
data2.setVersion(encrypt.getVersion());
// 解密操作
VenusData decrypt = VenusService.decrypt(data2);
System.out.printf("Key Name: %s, Plain Text: %s, Version: %d.\n", decrypt.getKeyName(),
decrypt.getText(), decrypt.getVersion());
}
小结
随着国家信息安全法规的逐步完善以及用户对个人信息安全意识的增强互联网信息安全也变得越来越重要了。据估计我国每年涉及互联网信息安全的灰色产业达1000亿很多应用在自己不知情的情况下已经被窃取了信息并进行交易了。
Venus是根据某大厂真实设计改编的如果你所在的公司还没有类似安全的加解密服务平台不妨参考Venus的设计开发实现一个这样的系统。
思考题
在你的工作中使用了哪些加密算法,算法以及密钥是否安全?有什么改进的思路吗?

View File

@ -0,0 +1,156 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 许可型区块链重构:无中心的区块链怎么做到可信任?
你好,我是李智慧。
过去几年区块链正变成一个日渐热门的词汇除了广为人知的比特币等数字货币基于区块链的分布式账本和智能合约技术也越来越受到企业的重视越来越多的企业也开始使用区块链技术进行跨企业的业务协作。2018 年 6 月 25 日,香港支付宝和菲律宾钱包 Gcash 利用区块链技术实现了跨境转账,仅 3 秒就实现跨境汇款到账,而以前则需要十几分钟到几天的时间。
一般我们把对所有公众都开放访问的区块链叫做“公有链”,而把若干企业构建的仅供企业间访问的区块链叫做“联盟链”,有时候也称作“许可型区块链”。上面提到的支付宝转账就是使用联盟链技术,目前比较有影响力的联盟链技术是 IBM 发起的 Hyperledger Fabric 项目,若干基于 Hyperledger Fabric 的联盟链应用已经落地。比如邮储银行的资产托管、招商银行的跨境结算都使用了 Hyperledger Fabric 技术。
而在公有链领域,目前看来,生态最完整、开发者社区最活跃、去中心化应用最多的公有链技术莫过于 Ethereum 以太坊。在智能合约和去中心化应用开发支持方面,以太坊的生态堪称业界最完备的典范,也受到了最多区块链开发者的支持。
相比于Fabric使用以太坊开发区块链应用更加简单、易于上手但是以太坊作为一个公有链技术目前还无法应用于企业级的联盟链场景。所以我们准备在以太坊的代码基础上进行若干代码模块的重构与开发。开发一个基于以太坊的企业级分布式账本与智能合约平台即一个许可型区块链。这个许可型区块链产品名称为“Taireum”。
需求分析
所谓区块链block chain就是将不断产生的数据按时间序列分组成一个一个连续的数据区块block然后利用单向散列加密算法求取每个区块的Hash值并且在每个区块中记录前一个区块的Hash值这些区块就通过Hash值串成一个链条被形象地称为区块链。如果你想了解区块链更多的背景知识可以参考我的这篇专栏文章《区块链技术架构区块链到底能做什么》。
以太坊Ethereum是一个去中心化的、开源的、有智能合约功能的公共区块链平台。以太币ETH是以太坊的原生加密货币它是市值第二高的加密货币仅次于比特币。而以太坊则是业界使用最多的区块链技术。
相比于比特币,以太坊最大的技术特点是支持智能合约,它是一种存储在区块链上的程序,由链上的计算机节点分布式运行,是一种去中心化的应用程序,也是区块链企业级应用必需的技术要求。
但是以太坊是一种公有链技术,并不适合用于企业级的场景,原因主要有三个:
在准入机制上,使用以太坊构建的区块链网络允许任何节点接入,也意味着区块数据是完全公开的。而联盟链的应用场景则要求仅联盟成员接入网络,非成员拒绝入网,并且数据也仅供联盟成员访问,对非联盟成员保密。
在共识算法上以太坊使用工作量证明PoW的方式对区块打包进行算力证明除非恶意节点获取了以太坊整个网络 51% 以上的计算能力,否则无法篡改或伪造区块数据,以此保证区块数据安全可靠。但是工作量证明需要花费巨大的计算资源进行算力证明,造成算力的极大浪费,也影响了区块链的交易吞吐能力。而在联盟链场景下,由于各个参与节点是经过联盟认证的,背后有实体组织背书,所以在区块打包的时候不需要进行工作量证明,这样可以大大减少算力浪费,提高交易吞吐能力。
在区块链运维管理上,以太坊作为公有链,节点之间通过 P2P 协议自动组网,无需运维管理。而联盟链需要对联盟成员进行管理,对哪些节点可被授权打包区块也需要进行管理,以保证联盟链的有效运行。
那么要如何做,才能既利用以太坊强大的智能合约与技术生态资源,简单高效地进行企业级区块链应用开发,又能满足联盟链对安全、共识、运维管理方面的要求?
Taireum需要在以太坊的基础上进行如下重构
重构以太坊的 P2P 网络通信模块,使其需要进行安全验证,得到联盟许可才能加入新节点,进入当前联盟链网络。
重构以太坊的共识算法。只有经过联盟成员认证授权的节点才能打包区块,打包节点按序轮流打包,无需算力证明。
开发联盟共识控制台CCCConsortium Consensus Console方便对联盟链进行运维管理联盟链用户只需要在 web console 上就可以安装部署联盟链节点,投票选举新的联盟成员和区块授权打包节点。
概要设计
Taireum 复用了以太坊强大的智能合约模块,并对共识算法和网络通信模块进行了重构改造,重新开发了联盟共识控制台,从而使其适用于企业级联盟链应用场景。使用 Taireum 部署的联盟链如图:
企业 A、企业 B、企业 C 合作建立一个联盟链,数据以区块链的方式存储在三家企业的节点上,实现分布式记账,并根据(基于智能合约的)联盟共识授权某些节点对区块数据进行打包。其他企业未经许可无法连接到该联盟链网络上,也不能查看区块链数据。
Taireum部署模型如下
Taireum中每个联盟企业都是一个Taireum节点都需要完整地部署Taireum+CCC控制台 Client使用我们提供的web3jPlus sdk与Geth进行RPC通信。
Geth是Tairem编译出来的区块链运行程序里面包含重写的Tai共识算法重构后的P2P网络模块以及原始的以太坊代码。
不同节点之间的Geth使用P2P网络进行通信。
详细设计
针对以太坊不适合企业级应用的部分Taireum将进行重构详细设计如下。
Taireum联盟共识控制台
联盟共识控制台是Taireum为联盟链运维管理开发的web组件企业可以非常方便地使用联盟共识控制台来部署联盟链运行节点、管理联盟成员和授权节点打包区块。
每个参与联盟链的企业节点都部署自己独立的联盟共识控制台。出于安全目的,每个企业节点的联盟共识控制台彼此独立,互不感知。他们需要通过调用联盟共识智能合约,对联盟管理事务进行协商,以达成共识。合约主要方法签名代码如下:
contract CCC {
//初始化合约,传入联盟创建者信息
//联盟创建者将成为联盟第一个成员和第一个拥有打包区块权限的节点
function CCC (string _companyname,string _email,string _remark,string _enode) public{
}
//联盟新成员申请
function applyMember(string _companyname,string _email,string _remark,string _enode,address _account) public{
}
//投票成为联盟成员
function VoteMember(uint _fromcompanyid,uint _tocompanyid) public {
}
//投票授权打包区块, 前提必须已经是联盟成员
function VoteMine(uint _fromcompanyid,uint _tocompanyid) public {
}
}
联盟共识智能合约目前的版本主要包括投票选举申请加入联盟的新成员,及投票选举联盟链新的区块打包节点。该智能合约由联盟链创立者在第一次启动联盟共识控制台的时候自动创建,是联盟链成员进行联盟管理和协商共识的最主要方式。
既然联盟成员节点部署的联盟控制台彼此独立、互不通信,那么联盟其他成员如何获得联盟共识智能合约的地址呢?
Taireum的做法是联盟链创立者节点的联盟共识控制台第一次成功部署联盟共识智能合约时就把这个合约的地址发给共识算法模块。共识算法在封装区块头的时候将合约地址写入区块头的miner中。下图是记录有联盟共识智能合约地址的区块头。
其中extraData记录经过椭圆曲线加密的区块打包者地址信息其他节点通过解密得到打包节点地址并验证该地址是否有权限打包节点miner中记录联盟共识智能合约地址nonce记录一个magic code “0xcaffffffffffffff”表示该区块获得了共识合约地址并写入了当前区块普通区块nonce magic code为”0x00ffffffffffffff”
这样,联盟链成员节点加入联盟链,同步区块链数据后,就可以从区块头中读取联盟共识智能合约的地址,然后通过联盟共识控制台调用该合约,参与联盟管理及协商共识。
Taireum联盟新成员许可入网
以太坊作为一个公有链,任何遵循以太坊协议的节点都可以加入以太坊网络,同步区块数据,参与区块打包。同时,以太坊作为开源项目,用户也可以下载源代码,自己部署多个以太坊节点,组成一个自己的区块链网络。但是只要这些节点可以通过公网访问,就无法阻止其他以太坊节点连接到自己的区块链网络上,获取区块数据,甚至打包区块。这在联盟链的应用场景中是绝对不能接受的,联盟链需要保证联盟内数据的隐私和安全。
Taireum重构了以太坊的P2P通信模块只有在许可列表中的节点才允许和当前联盟成员节点建立连接其他的连接请求在通信模块就会被拒绝以此保证联盟链的安全和私密性。
许可列表即Taireum成员列表通过前述的联盟共识智能合约管理。P2P通信模块通过联盟共识控制台调用智能合约获得联盟成员列表检查连接请求是否合法。
Taireum联盟新成员许可入网流程
新成员下载Taireum启动联盟共识控制台然后在联盟共识控制台启动Taireum节点获得节点enode url。
将enode url及公司信息提交给当前联盟链某个成员该成员通过联盟共识智能合约发起新成员入网申请。
联盟其他成员通过智能合约对新成员入网申请进行投票,得票数符合约定后,新成员信息被记入成员列表。
新成员节点通过网络连接当前联盟链成员节点当前成员节点p2p通信模块读取智能合约成员列表信息检查新成员节点enode url是否在成员列表中如果在就同意建立连接新成员节点开始下载区块数据。
Taireum授权打包区块
Taireum根据联盟链的应用特点放弃了以太坊ethash工作量证明算法。在借鉴clique共识算法的基础上Taireum重新开发了Tai共识算法引擎对联盟投票选出的授权打包节点排序轮流进行区块打包。
Tai共识算法引擎执行过程如下
联盟成员通过联盟共识智能合约投票选举授权打包区块的节点(在合约创建的时候,创建者即联盟链创始人默认拥有打包区块的权限)。
Tai共识算法通过联盟共识控制台访问智能合约获得授权打包区块的节点地址列表并排序。
检查父区块头的extraData解密取出父区块的打包者签名查看该签名是否在授权打包节点地址列表里如果不在就返回错误。
根据当前区块的块高block number对授权打包区块的节点地址列表长度取模根据余数决定对当前区块进行打包的节点如果计算出来的打包节点为当前节点就进行区块打包并把区块头难度系数设为2如果非当前节点随机等待一段时间后打包区块并把区块头难度系数设为1。难度系数的目的是尽量使当前节点打包的区块被加入区块链同时又保证当前打包节点失效的情况下其他节点也会完成区块打包的工作。
Taireum源码https://github.com/taireum/go-taireum。
小结
区块链也是一个分布式系统,但是不同于我们前面讨论过的各种传统分布式系统。传统分布式系统的各个分布式服务器节点是只属于某一个组织的,叫做中心化数据存储,数据的准确性和安全性靠的是这个组织的保证,使用者需要信任这个组织,比如我们相信支付宝不会偷偷把我们余额里的钱转走。
而区块链的分布式服务器节点并不只属于某一个组织,区块链并没有中心,而且使用区块链也不需要信任某个组织,因为任何数据篡改都会导致区块链条的中断。
区块链的这种特性可以实现无中心的跨组织交易。传统上,平行的组织之间交易需要通过更上一级的组织作为中心记录交易数据,比如商业银行之间的转账,要靠中央银行的数据中心来完成。如果没有更上一级的组织呢,就很难进行交易了。而使用区块链技术,即使没有中心,这些组织也可以进行交易,同时很多上级组织也变得没有那么必要了。
所以区块链会使我们的社会变得更加自组织,也将会给全社会的生产关系带来更深刻的变革。
思考题
今天我想和你讨论两个问题,你也可以任选其一,回复在评论区。
许可型区块链的应用场景还有哪些?
你是否看好区块链未来的发展?为什么?

View File

@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 网约车系统设计:怎样设计一个日赚 5 亿的网约车系统?
你好,我是李智慧。
网约车的官方定义是:“以互联网技术为依托,构建服务平台,整合供需信息,使用符合条件的车辆和驾驶员,提供非巡游的预约出租汽车服务的经营活动。”通俗地说就是:利用互联网技术平台,将乘客的乘车信息发送给合适的司机,由司机完成接送乘客的服务。网约车包含专车、快车、拼车等多种形式。
中国目前网约车用户规模约5亿我们准备开发一个可支撑目前全部中国用户使用的网约车平台应用名称为“Udi”。
需求分析
Udi是一个网约车平台核心功能是将乘客的叫车订单发送给附近的网约车司机司机接单后到上车点接乘客并送往目的地到达后乘客支付订单。根据平台的分成比例司机提取一部分金额作为收益用例图如下
Udi平台预计注册乘客5亿日活用户5千万平均每个乘客1.2个订单日订单量6千万。平均客单价30元平台每日总营收18亿元。平台和司机按37的比例进行分成那么平台每天可赚5.4亿元。
另外平台预计注册司机5千万日活司机2千万。
概要设计
网约车平台是共享经济的一种目的就是要将乘客和司机撮合起来所以需要开发两个App应用一个是给乘客的用来叫车一个是给司机的用来接单。Udi整体架构如下图
相应的Udi的系统也可以分成两个部分一个部分是面向乘客的。乘客通过手机App注册成为用户然后就可以在手机上选择出发地和目的地进行叫车了。乘客叫车的HTTP请求首先通过一个负载均衡服务器集群到达网关集群再由网关集群调用相关的微服务完成请求处理如下图
网关处理叫车请求的过程是:网关首先调用订单微服务,为用户的叫车请求创建一个订单,订单微服务将订单记录到数据库中,并将订单状态设置为“创建”。然后网关调用叫车微服务,叫车微服务将用户信息、出发地、目的地等数据封装成一个消息,发送到消息队列,等待系统为订单分配司机。
Udi系统的另一部分是面向司机的司机需要不停将自己的位置信息发送给平台同时还需要随时接收来自平台的指令。因此不同于用户通过HTTP发送请求给平台司机App需要通过TCP长连接和平台服务器保持通信如下图
Udi司机App每3秒向平台发送一次当前的位置信息包括当前车辆经纬度车头朝向等。位置信息通过TCP连接到达平台的TCP连接服务器集群TCP连接服务器集群的作用类似网关只不过是以TCP长连接的方式向App端提供接入服务。TCP连接服务器将司机的位置信息更新到地理位置服务。
对于前面已经写入到消息队列的乘客叫车订单信息分单子系统作为消息消费者从消息队列中获取并处理。分单子系统首先将数据库中的订单状态修改为“派单中”然后调用派单引擎进行派单。派单引擎根据用户的上车出发地点以及司机上传的地理位置信息进行匹配选择最合适的司机进行派单。派单消息通过一个专门的消息推送服务进行发送消息推送服务利用TCP长连接服务器将消息发送给匹配到的司机同时分单子系统更新数据库订单状态为“已派单”。
详细设计
关于Udi的详细设计我们将关注网约车平台一些独有的技术特点长连接管理、派单算法、距离计算。此外因为订单状态模型是所有交易类应用都非常重要的一个模型所以我们也会在这里讨论Udi的订单状态模型。
长连接管理
因为司机App需要不断向Udi系统发送当前位置信息以及实时接收Udi推送的派单请求所以司机App需要和Udi系统保持长连接。因此我们选择让司机App和Udi系统直接通过TCP协议进行长连接。
TCP连接和HTTP连接不同。HTTP是无状态的每次HTTP请求都可以通过负载均衡服务器被分发到不同的网关服务器进行处理正如乘客App和服务器的连接那样。也就是说HTTP在发起请求的时候无需知道自己要连接的服务器是哪一台。
而TCP是长连接一旦建立了连接连接通道就需要长期保持不管是司机App发送位置信息给服务器还是服务器推送派单信息给司机App都需要使用这个特定的连接通道。也就是说司机App和服务器的连接是特定的司机App需要知道自己连接的服务器是哪一台而Udi给司机App推送消息的时候也需要知道要通过哪一台服务器才能完成推送。
所以司机端的TCP长连接需要进行专门管理处理司机App和服务器的连接信息具体架构如下图。
处理长连接的核心是TCP管理服务器集群。司机App会在启动时通过负载均衡服务器与TCP管理服务器集群通信请求分配一个TCP长连接服务器。
TCP管理服务器检查ZooKeeper服务器获取当前可以服务的TCP连接服务器列表然后从这些服务器中选择一个返回其IP地址和通信端口给司机App。这样司机App就可以直接和这台TCP连接服务器建立长连接并发送位置信息了。
TCP连接服务器启动的时候会和ZooKeeper集群通信报告自己的状态便于TCP管理服务器为其分配连接。司机App和TCP连接服务器建立长连接后TCP连接服务器需要向Redis集群记录这个长连接关系记录的键值对是<司机ID, 服务器名>
当Udi系统收到用户订单派单引擎选择了合适的司机进行派单时系统就可以通过消息推送服务给该司机发送派单消息。消息推送服务器通过Redis获取该司机App长连接对应的TCP服务器然后消息推送服务器就可以通过该TCP服务器的长连接将派单消息推送给司机App了。
长连接管理的主要时序图如下:
如果TCP服务器宕机那么司机App和它的长连接也就丢失了。司机App需要重新通过HTTP来请求TCP管理服务器为它分配新的TCP服务器。TCP管理服务器收到请求后一方面返回新的TCP服务器的IP地址和通信端口一方面需要从Redis中删除原有的<司机ID, 服务器名>键值对,保证消息推送服务不会使用一个错误的连接线路推送消息。
距离计算
乘客发起一个叫车请求时Udi需要为其寻找合适的司机并进行派单所谓合适的司机最主要的因素就是距离。在[第9讲]的交友系统设计中我们已经讨论过GeoHash算法Udi就是直接使用Redis的GeoHash进行邻近计算。司机的位置信息实时更新到Redis中并直接调用Redis的GeoHash命令georadius计算乘客的邻近司机。
但是Redis使用跳表存储GeoHashUdi日活司机两千万每3秒更新一次位置信息平均每秒就需要对跳表做将近7百万次的更新如此高并发地在一个跳表上更新是系统不能承受的。所以我们需要将司机以及跳表的粒度拆得更小。
Udi以城市作为地理位置的基本单位也就是说每个城市在Redis中建立一个GeoHash的key这样一个城市范围内的司机存储在一个跳表中。对于北京这样的超级城市还可以更进一步以城区作为key进一步降低跳表的大小和单个跳表上的并发量。
派单算法
前面说过派单就是寻找合适的司机而合适的主要因素就是距离所以最简单的派单算法就是直接通过Redis获取距离乘客上车点最近的空闲网约车即可。
但是这种算法效果非常差因为Redis计算的是两个点之间的空间距离但是司机必须沿道路行驶过来在复杂的城市路况下也许几十米的空间距离行驶十几分钟也未可知。
因此我们必须用行驶距离代替空间距离即Udi必须要依赖一个地理系统对司机当前位置和上车点进行路径规划计算司机到达上车点的距离和时间。事实上我们主要关注的是时间也就是说派单算法需要从Redis中获取多个邻近用户上车点的空闲司机然后通过地理系统来计算每个司机到达乘客上车点的时间最后将订单分配给花费时间最少的司机。
如果附近只有一个乘客那么为其分配到达时间最快的司机就可以了。但如果附近有多个乘客那么就需要考虑所有人的等待时间了。比如附近有乘客1和乘客2以及司机X和司机Y。司机X接乘客1的时间是2分钟接乘客2的时间是3分钟司机Y接乘客1的时间是3分钟接乘客2的时间是5分钟。
如果按照单个乘客最短时间选择给乘客1分配司机X那么乘客2只能分配司机Y了乘客总的等待时间就是7分钟。如果给乘客1分配司机Y乘客2分配司机X乘客总等待时间就是6分钟。司机的时间就是平台的金钱显然后者这样的派单更节约所有司机的整体时间也能为公司带来更多营收同时也为整体用户带来更好的体验。
这样,我们就不能一个订单一个订单地分别分配司机,我们需要将一批订单聚合在一起,统一进行派单,如下图:
分单子系统收到用户的叫车订单后,不是直接发送给派单引擎进行派单,而是发给一个订单聚合池,订单聚合池里有一些订单聚合桶。订单写完一个聚合桶,就把这个聚合桶内的全部订单推送给派单引擎,由派单引擎根据整体时间最小化原则进行派单。
这里的“写完一个聚合桶”有两种实现方式一种是间隔一段时间算写完一个桶一种是达到一定数量算写完一个桶。最后Udi选择间隔3秒写一个桶。
这里需要关注的是派单的时候需要依赖地理系统进行路径规划。事实上乘客到达时间和金额预估、行驶过程导航、订单结算与投诉处理都需要依赖地理系统。Udi初期会使用第三方地理系统进行路径规划但是将来必须要建设自己的地理系统。
订单状态模型
对于交易型系统而言,订单是其最核心的数据,主要业务逻辑也是围绕订单展开。在订单的生命周期里,订单状态会多次变化,每次变化都是由于核心的业务状态发生了改变,也因此在前面设计的多个地方都提到订单状态。但是这种散乱的订单状态变化无法统一描述订单的完整生命周期,因此我们设计了订单状态模型,如下图:
用户叫车后,系统即为其创建一个订单,订单进入“创单”状态。然后该订单通过消息队列进入分单子系统,分单子系统调用派单引擎为其派单,订单状态进入“派单中”。派单引擎分配到司机,一方面发送消息给司机,一方面修改订单状态为“已派单”。
如果司机去接到乘客,订单状态就改为“行程中”;如果司机拒绝接单,就需要为乘客重新派单,订单重新进入消息队列,同时订单状态也改回为“派单中”;如果司机到达上车点,但是联系不到乘客,没有接到乘客,那么订单就会标记为“已取消”。如果在派单中,乘客自己选择取消叫车,订单也进入“已取消”状态。“已取消”是订单的一种最终状态,订单无法再转变为其他状态。
司机到达目的地后通过App确认送达订单进入“待支付”状态等待用户支付订单金额。用户支付后完成订单生命周期订单状态为“已完成”。
订单状态模型可以帮助我们总览核心业务流程,在设计阶段,可以通过状态图发现业务流程不完备的地方,在开发阶段,可以帮助开发者确认流程实现是否有遗漏。
小结
在软件设计开发中,会涉及两类知识。一类是和具体业务无关的,比如编程语言、编程框架这些技术和具体业务无关,消息队列、分布式缓存这些技术也和具体业务无关。这一类技术更具有通用性,技术人员不管跳槽到哪家公司,几乎都会用到这些技术。
还有一类技术是和具体业务相关的,比如电商业务、金融业务、包括本文的网约车业务等等,这些业务如何用最合适的技术方案实现。这些和具体业务相关的技术经验主要适用于相关的业务领域。
技术人员在职业生涯的早期,需要更多地去关注和学习通用性的技术。而随着年龄增加,应该在业务相关的技术上获得更多沉淀,成为一个领域的专家,才能使自己在职场上获得更强的竞争力。
下一节我们将讨论如何使用领域驱动设计的技术方法解决业务上的问题,带你体会技术人员如何在业务上获得更多沉淀。
思考题
网约车在进行派单的时候,还需要考虑哪些因素,如何实现?

View File

@ -0,0 +1,101 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 网约车系统重构:如何用 DDD 重构网约车系统设计?
你好,我是李智慧。
软件开发是一个过程这个过程中相关方对软件系统的认知会不断改变。当系统现状和大家的认知有严重冲突的时候不重构系统就难以继续开发下去。此外在持续的需求迭代过程中代码本身会逐渐腐坏变得僵硬、脆弱、难以维护需求开发周期越来越长bug却越来越多系统也必须要进行重构。
我们在前一篇讨论的Udi网约车系统经过了几年的快速发展随着业务越来越复杂功能模块越来越多开发团队越来越庞大整个系统也越来越笨拙、难以维护。以前两三天就能开发完成的新功能现在要几个星期开发人员多了工作效率却下降了。
Udi使用微服务架构开始的时候业务比较简单几个微服务就可以搞定。后面随着功能越来越多微服务也越来越多微服务之间的依赖关系也变得越来越复杂常常要开发一个小功能却需要在好几个微服务中进行修改。后来开发人员为了避免这种复杂性倾向于把所有功能都写在一个微服务里结果整个系统架构又开始退回到单体架构。
基于以上原因我们准备对Udi进行一次重构核心就是要解决微服务设计的混乱梳理、重构出更加清晰的微服务边界和微服务之间的依赖关系。我们准备使用DDD即领域驱动设计的方法进行这次重构。
那么领域驱动设计的核心思想是什么设计的一般方法是什么如何将这些方法应用到Udi的重构过程中这些就是我们今天要解决的主要问题。
DDD的一般方法
领域是一个组织所做的事情以及其包含的一切通俗地说就是组织的业务范围和做事方式也是软件开发的目标范围。比如对于淘宝这样一个以电子商务为主要业务的组织C2C电子商务就是它的领域。领域驱动设计就是从领域出发分析领域内模型及其关系进而设计软件系统的方法。
但是如果我们说要对C2C电子商务这个领域进行建模设计那么这个范围就太大了不知道该如何下手。所以通常的做法是把整个领域拆分成多个子域比如用户、商品、订单、库存、物流、发票等。强相关的多个子域组成一个限界上下文它是对业务领域范围的描述对于系统实现而言限界上下文相当于是一个子系统或者一个模块。限界上下文和子域共同组成组织的领域如下
不同的限界上下文也就是不同的子系统或者模块之间会有各种的交互合作。如何设计这些交互合作呢DDD使用上下文映射图来完成如下
在DDD中领域模型对象也被称为实体。先通过业务分析识别出实体对象然后通过相关的业务逻辑设计实体的属性和方法。而限界上下文和上下文映射图则是微服务设计的关键通常在实践中限界上下文被设计为微服务而上下文映射图就是微服务之间的依赖关系。具体设计过程如下图
首先领域专家和团队一起讨论分析业务领域确认业务期望将业务分解成若干个业务场景。然后针对每个场景画UML活动图活动图中包含泳道通过高内聚原则对功能逻辑不断调整使功能和泳道之间的归属关系变得更加清晰合理。这些泳道最终就是限界上下文泳道内的功能就是将来微服务的功能边界泳道之间的调用流程关系就是将来微服务之间的依赖关系即上下文映射图。
但是,这个状态的泳道还不能直接转化成限界上下文。有些限界上下文可能会很大,有些依赖关系可能会比较强。而一个限界上下文不应该超过一个团队的职责范围,因为根据康威定律:组织架构决定系统架构,两个团队维护一个微服务,必然会将这个微服务搞成事实上的两个微服务。所以,我们还需要根据团队特性、过往的工作职责、技能经验,重新对泳道图进行调整,使其符合团队的职责划分,这时候才得到限界上下文。
在这个限界上下文基础上,考虑技术框架、非功能需求、服务重用性等因素,进一步进行调整,就得到最终的限界上下文设计,形成我们的微服务架构设计。
我们将遵循上述DDD方法过程对Udi微服务进行重新分析设计并进行系统重构。
Udi DDD 重构设计
首先分析我们的业务领域,通过头脑/事件风暴的形式,收集领域内的所有事件/命令,并识别事件/命令的发起方即对应的实体。最后识别出来的实体以及相关活动如下表:
基于核心实体模型,绘制实体关系图,如下:
在实体间关系明确且完整的前提下,我们就可以针对各个业务场景,绘制场景活动图。活动图比较多,这里仅用拼车场景作为示例,如下:
依据各种重要场景的活动图,参考团队职责范围,结合微服务重用性考虑及非功能需求,产生限界上下文如下表:
针对每个限界上下文进一步设计其内部的聚合、聚合根、实体、值对象、功能边界。以订单限界上下文为例:
上述订单实体的属性和功能如下表:
最后,在实现层面,设计对应的微服务架构如下图:
这是一个基于领域模型的分层架构,最下层为聚合根对象,组合实体与值对象,完成核心业务逻辑处理。上面一层为领域服务层,主要调用聚合根对象完成订单子域的业务,根据业务情况,也会在这一层和其他微服务通信,完成更复杂的、超出当前实体职责的业务,所以这一层也是一个聚合层。
再上面一层是应用服务层,将实体的功能封装成各种服务,供各种应用场景调用。而最上面是一个接口层,提供微服务调用接口。
小结
领域驱动设计很多时候雷声大雨点小说起来各种术语满天飞真正开发实践的时候又无从下手。这节的案例来自一个真实落地的DDD重构设计文档你可以参考这个文档按图索骥应用到自己的开发实践中。
既然说到“按图索骥”那我认为也有必要在这一节的最后帮你画一个更有概括性的DDD重构路线图我们把使用DDD进行系统重构的过程分为以下六步
讨论当前系统存在的问题发现问题背后的根源。比如架构与代码混乱需求迭代困难部署麻烦bug率逐渐升高微服务边界不清晰调用依赖关系复杂团队职责混乱。
针对问题分析具体原因。比如:微服务 A 太庞大微服务B和C职责不清团队内业务理解不一致内部代码设计不良硬编码和耦合太多。
重新梳理业务流程明确业务术语进行DDD战略设计具体又可以分为三步。-
a. 进行头脑风暴,分析业务现状和期望,构建领域语言;-
b. 画泳道活动图、结合团队特性设计限界上下文;-
c. 根据架构方案和非功能需求确定微服务设计。
针对当前系统实现和DDD设计不匹配的地方设计微服务重构方案。比如哪些微服务需要重新开发哪些微服务的功能需要从A调整到B哪些微服务需要分拆。
DDD技术验证。针对比较重要、问题比较多的微服务进行重构打样设计聚合根、实体、值对象重构关键代码验证设计是否合理以及团队能否驾驭DDD。
任务分解与持续重构。在尽量不影响业务迭代的前提下,按照重构方案,将重构开发和业务迭代有机融合。
思考题
你认为DDD最大的价值是什么如何才能成功落地DDD

View File

@ -0,0 +1,93 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 大数据平台设计:如何用数据为用户创造价值?
特别说明:本文相关技术仅用于技术展示,具体实践中,数据收集和算法应用需要遵循国家个人信息保护法与信息安全法等有关法律制度。
你好,我是李智慧。
现在,业界普遍认为互联网创新已经进入下半场,依靠技术创新或者商业模式创新取得爆发性发展的机会越来越少。于是大家把目光转向精细化运营,主要手段就是依靠大数据技术,挖掘每个用户独特的商业价值,提供更具个性化的服务,以此来提升服务水平和营收能力,最终获得更强的市场竞争能力。
Udi大数据平台的主要目标是根据用户的不同喜好为其分配不同的车型一方面改善用户体验另一方面也增加平台营收。此外如何为用户推荐最优的上车点和下车点如何分析订单和营收波动如何发现潜在的高风险用户等等也需要依赖大数据平台。
大数据技术不同于我们前面设计的高并发案例,高并发案例虽然也要处理海量用户的请求,但是每个用户请求都是独立的,计算与存储也是每个用户独立进行的。而大数据技术则要将这些海量的用户数据进行关联计算,因此,适用于高并发架构的各种分布式技术并不能解决大数据的问题。
Udi大数据平台设计
根据Udi大数据应用场景的需求需要将手机App端数据、数据库订单和用户数据、操作日志数据、网络爬虫爬取的竞争对手数据统一存储到大数据平台并支持数据分析师、算法工程师提交各种SQL语句、机器学习算法进行大数据计算并将计算结果存储或返回。Udi大数据平台架构如下图
大数据采集与导入
Udi大数据平台整体可分为三个部分第一个部分是大数据采集与导入。这一部分又可以分为4小个部分App端数据采集、系统日志导入、数据库导入、爬虫数据导入。
App端除了业务功能模块还需要包含几个数据埋点上报模块。App启动的时候应用启动上报模块会收集用户手机信息比如手机型号、系统版本、手机上安装的应用列表等数据App运行期间也会通过定时数据上报模块每5秒上报一次数据主要是用户当前地理位置数据用户点击操作的时候一方面会发送请求到Udi后端应用系统一方面也会通过用户操作上报模块将请求数据以及其他一些更详细的参数发送给后端的应用上报服务器。
后端的应用上报服务器收到前端采集的数据后发送给消息队列SparkStreamin从消息队列中消费消息对数据进行清洗、格式化等ETL处理并将数据写入到HDFS存储中。
Udi后端应用系统在处理用户请求的过程中会产生大量日志和数据这些存储在日志系统和MySQL数据库中的数据也需要导入到大数据平台。Flume日志收集系统会将Udi后端分布式集群中的日志收集起来发送给SparkStreaming进行ETL处理最后写入到HDFS中。而MySQL的数据则通过Sqoop数据同步系统直接导入到HDFS中。
除了以上这些Udi系统自己产生的数据为了更好地应对市场竞争Udi还会通过网络爬虫从竞争对手的系统中爬取数据。需要注意的是这里的爬虫不同于[04讲]中的爬虫因为竞争对手不可能将订单预估价等敏感数据公开。因此爬虫需要模拟成普通用户爬取数据这些爬来的数据也会存储在HDFS中供数据分析师和产品经理在优化定价策略时分析使用。
大数据计算
Udi大数据平台的第二个部分是大数据计算。写入到HDFS中的数据一方面供数据分析师进行统计分析一方面供算法工程师进行机器学习。
数据分析师会通过两种方式分析数据。一种是通过交互命令进行即时查询通常是一些较为简单的SQL。分析师提交SQL后在一个准实时、可接受的时间内返回查询结果这个功能是通过Impala完成的。另外一种是定时SQL统计分析通常是一些报表类统计这些SQL一般比较复杂需要关联多张表进行查询耗时较长通过Hive完成每天夜间服务器空闲的时候定时执行。
算法工程师则开发各种Spark程序基于HDFS中的数据进行各种机器学习。
以上这些大数据计算组件Hive、Spark、SparkStreaming、Impala都部署在同一个大数据集群中通过Yarn进行资源管理和调度执行。每台服务器既是HDFS的DataNode数据存储服务器也是Yarn的NodeManager节点管理服务器还是Impala的Impalad执行服务器。通过Yarn的调度执行这些服务器上既可以执行SparkStreaming的ETL任务也可以执行Spark机器学习任务而执行Hive命令的时候这些机器上运行的是MapReduce任务。
数据导出与应用
Udi大数据平台的第三个部分是数据导出与应用。Hive命令执行完成后将结果数据写入到HDFS中这样并不方便数据分析师或者管理人员查看报表数据。因此还需要用Sqoop将HDFS中的数据导出到MySQL中然后通过数据分析查询控制台以图表的方式查看数据。
而机器学习的计算结果则是一些学习模型或者画像数据将这些数据推送给推荐引擎由推荐引擎实时响应Udi系统的推荐请求。
大数据平台一方面是一个独立的系统,数据的存储和计算都在其内部完成。一方面又和应用系统有很多关联,数据需要来自应用系统,而计算的结果也需要给应用系统使用。上面的架构图中,属于大数据平台的组件我用蓝色标出,其他颜色代表非大数据平台组件或者系统。
Udi大数据派单引擎设计
我们在第20讲讨论了Udi派单引擎这个派单引擎并没有考虑乘客和车型的匹配关系。根据Udi的运营策略车辆新旧程度、车辆等级与舒适程度、司机服务水平会影响到订单的价格。派单成功时系统会根据不同车辆情况预估不同的订单价格并发送给乘客但是有些乘客会因为预估价格太高而取消订单而有些乘客则会因为车辆等级太低而取消订单还有些乘客则会在上车后因为车辆太旧而给出差评。
Udi需要利用大数据技术优化派单引擎针对不同类别的乘客匹配尽可能合适的车辆。上面采集了乘客的手机型号及手机内安装应用列表订单数据记录了乘客上下车地点乘客评价以及订单取消原因记录了用户乘车偏好车辆及司机数据记录了车辆级别和司机信息这些数据最终都会同步到大数据平台。
我们将利用这些数据优化Udi派单引擎。根据用户画像、车辆画像、乘车偏好进行同类匹配。
基于乘客分类的匹配
根据乘客的注册信息、App端采集的乘客手机型号、手机内安装应用列表、常用上下车地点等我们可以将乘客分类然后根据同类乘客的乘车偏好预测乘客的偏好并进行匹配。
比如根据数据分类乘客A和乘客C是同类乘客而乘客A偏好车辆类型B和D。乘客C叫车的时候那么派单系统会优先给他派单车辆类型B和D。
基于车辆分类的匹配
事实上我们可以直接根据车辆类型属性对车辆类型进行再分类。比如通过机器学习统计分析车辆类型B和D可以归为一类那么如果乘客C偏好车辆类型B那么我们可以认为车辆类型D也匹配他。
使用推荐引擎对派单系统进行优化为乘客分配更合适的车辆前提是需要对用户和车辆进行分类与画像想要完成这部分工作我们可以在大数据平台的Spark机器学习模块通过聚类分析、分类算法、协同过滤算法以及Hive统计分析模块进行数据处理将分类后的数据推送给派单引擎去使用。
派单引擎在原有的最小化等车时间基础上,对派单进行调整,使车辆和乘客偏好更匹配,改善用户体验,也增加了平台营收。
小结
网约车是一个格外依赖大数据进行用户体验优化的应用。比如用户上车点在一个几千平方米的POI区域内乘客方便等车司机不违章的地点可能只有一两个这一两个点又可能在任何地图上都没有标示。这就意味着司机和乘客需要通过电话沟通很久才知道对方说的上车点在哪里然后要么乘客徒步几百米走过来要么司机绕一大圈去接给司机和乘客都造成很多麻烦平台也会因此流失很多用户。
对于这种问题,电子地图应用的厂商需要派测绘人员现场标注这些点。而对于网约车平台,由于不停在上传司机的位置信息,只需要根据乘客最后的上车点进行聚类分析,就会发现该区域大部分乘客最后都是在某个点上车,这个点就是最佳上车点。也就是说,只需要最初的一批乘客忍受麻烦,他们的行为数据就可以被网约车平台用于机器学习和数据挖掘,并被用于优化用户体验。
网约车平台像这样依赖大数据的地方还有很多。所以,网约车平台需要尽可能获取、存储用户和司机的各种行为与业务数据,并基于这些数据不断进行分析、挖掘,寻找潜在的商业机会和用户体验优化。对于一个数亿用户规模的网约车平台,这些数据的规模是非常庞大的,因此需要一个强大、灵活的大数据平台才能完成数据的存储与计算。
思考题
在你的工作中,是否有涉及到大数据和机器学习,它们带来了哪些价值?

View File

@ -0,0 +1,59 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 一个架构师的一天
你好,我是李智慧。
时间过得真快,聊着聊着,这个专栏就到了尾声。恭喜你完成了这一阶段的学习,向架构师又迈进了一步!
在这里我想问你一个问题学完这个专栏除了架构知识以外你对“架构师”这个角色有了哪些新的认知呢毕竟这个专栏的Slogan就是“附身”大厂架构师我还是非常希望能为你代入架构师视角体验这个角色的高光时刻。
我们这个专栏的案例,大多是一些庞大的系统,每个案例在真实世界中,都对应着一家数百上千亿美元市值的公司,需要数千名工程师开发,需要部署数万台服务器的系统。这样的系统不可能是由一个架构师设计出来的,正如开篇词所言,我们的设计是一种架空现实的设计。学习这种架空现实的设计,可以帮助你站在上帝视角,俯视一个个庞大系统的核心关键技术和整体架构,进而帮你构建起全局化的思维方式,而这正是架构师最重要的竞争力。
现实中,拥有一个庞大系统的公司通常有上百名的架构师,他们的日常工作也不是去设计整个系统,而是在自己负责的模块或者子系统内修修补补。所以在结束语里,相比于前面宏大的设计,我想做点“接地气”的内容,带你“穿越”成一位真正的架构师,感受架构师这个角色典型的一天。
我们以某大厂商家管理后台系统的架构师为例。商家管理后台是电商卖家的后台管理系统卖家上下架商品、投放广告、查看运营数据等操作都在这里完成是面向卖家的核心系统。商家管理后台开发部20多个人分了4个开发小组。
920 到公司,翻看昨天的邮件和聊天记录,看看有没有遗漏的事情。昨天下午数据库连接阻塞,连累应用服务器响应超时,焦头烂额搞了一下午,很多聊天消息都没来的及处理。翻了一下还好,没有错过什么重要的事情。
930 开部门小组长晨会开了10分钟会今天晨会时间控制得不错。
945 作为主讲人主持“商家管理后台架构重构设计”部门评审会这次重构是商家后台最近两年最大的一次重构。已经和几个开发小组长还有核心开发人员已经讨论过几次了这次做部门评审事实上是做重构宣导要让所有开发人员都统一认识所以会上争议不多开得比较顺利。另外这次重构设计画了很多UML模型图评审的时候也趁机给大家再普及了下设计建模的重要性和方法。
1100 参加另一个部门的架构评审会,会上他们部门的架构师和一个开发组长吵起来了。晕,自己内部还没统一意见,就不要拉别的部门的人来看戏了。开会是用来统一意见和思想的,不是用来解决问题的,问题要提前解决。趁他们吵的间隙,上极客时间看看有什么有意思的课程,发现《李智慧 · 高并发架构实战课》挺有意思,订阅了。
1200 午饭。
1330 这次架构重构引入DDD领域驱动设计方法上午的评审主要从DDD战略设计的角度对架构进行重构。DDD战术设计感觉不太好掌控先暂时不在部门推广了。自己先写个DDD代码Demo练练吧用哪个功能做Demo呢红包管理功能吧。上半年公司架构师委员会例会上也有几个部门的架构师提出要搞DDD结果没了动静这个螃蟹看来还得自己吃。
1511 一个开发同学过来讨论技术问题聊了十来分钟越聊越觉得不对劲把产品经理也叫过来讨论确定这个需求是有问题的先暂时不开发产品经理回去重新梳理需求。继续写Demo代码。
1603 收到运维部门的会议邀请明天上午1030复盘昨天的数据库访问故障。麻烦这个故障看起来是数据库失去响应其实是个程序Bug线程阻塞导致数据库连接耗尽。这个故障影响不小责任主要在我们部门看看会上怎么说能不能让运维部门也承担一点故障分毕竟他们数据库管理也没做到位。也不知道明天参加会议的运维是哪个好不好说话继续写Demo代码。
1642 收到监控报警通知商品上架数异常波动低于正常值60%。是商品上架服务出问题了赶紧打开监控系统上架服务系统指标正常打开日志系统异常日志数正常。什么情况啊问问运维。哦原来是监控数据消息队列消费服务出了点问题数据统计有误触发报警虚惊一场。继续写Demo代码。
1708 公司负责培训的同事发来消息问能不能做个性能优化方面的内部讲座面向全公司的开发和测试人员。可以呀什么时候讲下周五。好的。时间有点紧啊不做Demo了先做讲座PPT毕竟公司级的讲座难得的提升技术影响力的机会。不过性能优化这个话题有点大啊从哪方面入手呢对了上午看到的李智慧的专栏里面有一篇性能优化的文章看看他怎么讲。可以就按这个文章的思路准备讲座大纲。离下班还有一个多小时做PPT来不及了先收集下PPT素材案例部分用公司内部的理论部分Google一下不错就这样……
上面就是这位架构师一天的经历,你认为他的工作怎么样呢?大部分人都希望工作轻松一点,你觉得文中这位架构师的工作是否轻松?以及什么样的工作是轻松的呢?
首先,清闲的工作未必是轻松的,工作不忙、无所事事,会让人觉得自己失去价值,进而产生焦虑,最后并不轻松。轻松的工作应该是自己做的事情有意义、有价值,还能在工作中不断获得进步;同时工作又游刃有余,自己可以掌控工作,而不是被工作驱赶着疲于奔命。
架构师最核心的事情是要控制技术局面让事情有节奏地推进不要鸡毛蒜皮做决策的人越是忙忙碌碌团队效率越低。文中这位架构师做到了这一点他基本上掌控了他的工作而不是让工作push他。大部分时间他可以按照自己的节奏安排工作突发的情况他也能比较从容地应对。
他做事情看起来似乎毫不费力。事实上,他要处理的很多事情,都是一些既复杂又困难的事情,是别人搞不定了才到了他这里来的。他既要考虑各种人际关系,又要用技术做出判断,还要对判断结果负责,而且要有威望让别人听他的。此外,他还有时间学习、有时间写代码、有时间做设计、有时间扩展自己的影响力,还能按时下班。
其实,做事情要想看起来毫不费力,必须要在看不到的地方费很大力气,进行很多的积累。你也可以问下自己,自己现在每天都做些什么?多长时间用来学习,有没有帮助别人解决问题,有没有考虑积极做些分享扩展自己的影响力,还是只是低下头盯着自己脚下的这块巴掌大的地方呢?
当然,在工作中能达到这位架构师这样的境界并不容易,也不是成为架构师就可以这样工作。但是只要你想往上走,就需要不断学习,有目的地在工作中提升自己,并且主动找机会展示自己的综合能力,构建自己的影响力。相信最终你一定可以掌控自己的工作,还有人生。
希望这个专栏可以在你进步的路上提供帮助,使你对更宏观的知识和实践建立起感性的认知和清晰的目标,然后不断夯实自己的实践能力、拓宽自己的影响力,最后成为你想成为的自己。
](https://jinshuju.net/f/x0MpWT)