first commit
This commit is contained in:
125
专栏/李智慧·高并发架构实战课/00开篇词“附身”大厂架构师,身临其境设计高并发系统.md
Normal file
125
专栏/李智慧·高并发架构实战课/00开篇词“附身”大厂架构师,身临其境设计高并发系统.md
Normal file
@ -0,0 +1,125 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 “附身”大厂架构师,身临其境设计高并发系统
|
||||
你好,我是李智慧,目前担任同程旅行交通首席架构师。我曾在阿里巴巴和英特尔担任架构师,主要从事高并发系统架构和大数据相关产品的开发。我参与过alibaba.com和Apache Spark的架构设计与开发,也曾作为CTO领导团队经历了日订单从零到一百万的高并发技术挑战。
|
||||
|
||||
说来我也是你的老朋友了,因为我在极客时间已经开过两门专栏课程了,分别是《从0开始学大数据》和《后端技术面试38讲》。
|
||||
|
||||
在前两个专栏中,我时不时就能看到同学们对“高并发架构”的学习需求。确实,“高并发”是系统架构设计的核心关键词,也是很多大厂的关注焦点。面试大厂的时候,你要是对高并发架构说不出个一二三,恐怕面试也凶多吉少了。
|
||||
|
||||
其实,我们都知道高并发的重要性了,甚至是看过了不少高并发系统设计的技术资料,但还是会有困惑:学了形形色色的高并发技术以后,为什么我还是对设计一个完整的高并发系统没有概念?
|
||||
|
||||
在我看来,这种困惑并不是源于对知识的理解不足或掌握不够,而是源于缺乏架构现场的感受:不能把自己设身处地地放在一个需要真实构建系统的环境中。没有那种面对真实场景的压力、挑战,也没有完成任务后的喜悦与轻松,学到的各种技术就无法基于这种真实的现场感而融汇成自己的观点,最后还是一些零碎的知识。
|
||||
|
||||
正所谓:听过很多道理,依然过不好这一生。说到底,还是因为缺乏实践。
|
||||
|
||||
当然了,大厂能为我们提供高并发系统架构设计的实践机会。但若你没有高并发架构设计经验,大厂的大门又怎肯为你打开呢?这就形成了一个死循环。
|
||||
|
||||
难道我们就只能一直陷在这个困局里出不去吗?事实上,我们自己也在刷抖音、玩微博、用搜索引擎,我们自己就是这些高并发应用的用户。我们只需要从用户角色转换到设计角色,把自己想象成这些大厂的架构师,设身处地去思考:如果让我来设计这个系统,我将如何开展我的工作?
|
||||
|
||||
通过这个专栏,你可以“附身”大厂架构师,借助我的经验和知识,身临其境地,站在大厂架构师的视角,理解高并发系统的设计思路。
|
||||
|
||||
我如何帮你获得高并发系统设计的“现场感”?
|
||||
|
||||
为了让你更有身临其境的感受,我为你找到了三条途径。
|
||||
|
||||
|
||||
足够真实的高并发系统设计场景
|
||||
|
||||
|
||||
如果现阶段你还不能进入大厂真正去实践,那么为自己创造一个相对真实的设计场景,进行不断的模拟练习,会比单纯学习理论的效果更好。
|
||||
|
||||
这个专栏的所有案例都是基于真实场景的,甚至有些案例本身就是由真实设计文档改编的。只有这些还不够,每节课设计的系统都有自己的名字。一位先哲说过,当一个东西有了自己的名字,就拥有了生命。
|
||||
|
||||
希望你在阅读专栏的过程中,能把自己带入到真实的系统设计场景中,把文章当成真实的设计文档,把自己想象成设计文档的作者(也就是我)的同事,你正在评审我做的设计。
|
||||
|
||||
你可以一边阅读一边思考:这个设计哪些地方考虑不周,哪些关键点有缺漏。然后你可以把自己的思考写在评论区,当做你的评审意见。最重要的是,通过这种方式,你拥有了和我一样的关于每一个软件设计案例的现场感:你不是一个阅读专栏的读者,而是置身于互联网大厂的资深架构师,你在评审同事的设计,也在考虑公司的未来。
|
||||
|
||||
如果你在阅读这些案例设计文档的时候,能够对设计整体有个评价,对具体细节能给出若干改进意见,那么你和大厂架构师的距离也就不远了。
|
||||
|
||||
|
||||
贴合工作场景的设计文档形式
|
||||
|
||||
|
||||
你可能会觉得,设计文档和自己关系不大,一是平时不怎么写,也不愿意写文档,觉得写文档价值不大;二是自己不擅长写文档,觉得写也写不好,甚至不太知道设计文档该怎么写。
|
||||
|
||||
但工作了这么多年,我发现,写东西可以帮助人更好地思考。技术人员如果不写设计文档,就会缺少对技术更深刻的思考,对技术方案的优点和缺点就缺乏系统的认识,也就不知道如何找到更好的技术和更合理的方案。很显然,这会阻滞技术人员自身的职业发展。
|
||||
|
||||
不仅如此,如果没有系统设计文档,缺乏技术的深度思考,那么开发出来的软件就缺乏创新,显得平庸,公司的技术产品在市场上就缺少竞争力。所以长远看来,不写系统设计文档,看起来忙忙碌碌,但这样对公司、对自己的发展都不利。
|
||||
|
||||
可以粗暴一点地说:没有设计文档就没有设计,没有设计就没有技术的进步。
|
||||
|
||||
所以,这个专栏我将以软件设计文档的形式去写一系列软件的系统架构设计,这些设计文档的风格是相对统一的。我希望你可以在这些“重复”的设计文档组织方式、软件建模与架构方式中,学习到一般的软件设计方法和软件设计文档的写作方法。
|
||||
|
||||
|
||||
求同存异的典型系统架构案例
|
||||
|
||||
|
||||
我精挑细选了10余个系统架构案例,这些案例大多是目前大家比较关注的高并发、高性能、高可用系统。比如网盘、搜索引擎、短视频应用、打车软件、交友软件、微博等。它们是高并发架构设计的优秀“课代表”,它们的技术可以解决现有的80%以上的高并发共性问题。所以在阅读这些文档的过程中,你可以进一步学习、借鉴这些典型的分布式互联网系统架构,了解现在高并发技术的热点。
|
||||
|
||||
为了避免每篇文档中都出现大量重复、雷同的设计,我在内容方面也进行了取舍,精简了一些常规的、技术含量较低的内容,而尽量多地介绍那些有独特设计思想的技术点。尽可能做到在遵循设计文档规范的同时,又突出每个系统自己的设计重点。
|
||||
|
||||
此外,专栏中还有一部分设计是针对这些大型应用系统的重要组成部分的,比如限流器、防火墙、加解密服务、大数据平台等。
|
||||
|
||||
但我需要强调一点,专栏会针对这些知名的大厂应用重新进行设计,而不是分析现有这些应用是如何设计的。一方面,重新设计完全可以按自己的意愿来,不管是设计方案还是需求估算,都是一件很爽的事;另一方面,因为现有应用中的某些关键设计并没有公开,我们要想讨论清楚这些高并发应用的架构设计,没有现成的资料,就需要自己进行分析并设计。这有点像流行的穿越剧:我如果能穿越回去,我将如何设计这些大厂的系统?只要我愿意,我也可以成为大神。
|
||||
|
||||
所以,在设计文档中,很多案例都有需求估算部分,来分析我们重新设计的系统需要承载的并发压力有多大、系统资源需要多少,这些估算大多数都略高于现有大厂系统的指标。希望你在阅读这些估算内容的时候,能够更具体地体会到架构师的“现场感受”:我评审、设计的这个系统将服务全球数十亿用户;这个系统每年需要的服务器和网络带宽需要几十亿资金;这个系统宕机十几分钟,公司就会损失数千万人民币。
|
||||
|
||||
此时此刻,你可能还是会有很多疑惑:
|
||||
|
||||
|
||||
这些高并发系统架构中,有哪些常见的架构模式?
|
||||
各种常见的分布式技术,是如何应用到系统架构中的?
|
||||
不同的应用场景,又有怎样独特的技术挑战,以及是如何应对的?
|
||||
了解这些,对自己的架构设计能力提升有哪些帮助?
|
||||
……
|
||||
|
||||
|
||||
不必着急,通过这个专栏,你的问题都会被一一解答。而且,我相信,这一系列的架构设计实战文档会给你更多不一样的启迪。
|
||||
|
||||
这门课有哪些内容?
|
||||
|
||||
这个专栏共计22篇内容,其中包括17篇设计案例实战文档,还有5篇是关于软件设计方法,高并发、高性能、高可用系统架构的一些基础知识,方便你对设计案例中涉及到的技术进行一些回顾。
|
||||
|
||||
我们常说高并发、高性能、高可用,事实上,这三者并不是平行的关系。通常情况下,高并发是根源和核心。正是因为高并发,大量的用户同时请求我们的系统,导致系统资源快速消耗,服务器无法及时处理用户请求,响应变慢,系统出现性能问题。更进一步,性能继续恶化,导致服务器资源耗尽,就会出现系统崩溃,可用性也出现问题。
|
||||
|
||||
根据高并发系统的特点,我把这个专栏划分成了5个实战模块。
|
||||
|
||||
|
||||
|
||||
实战模块一:高并发系统的海量数据处理架构案例
|
||||
|
||||
我们将主要讨论高并发处理海量数据的场景,包括海量的数据如何存储、如何传输、如何进行并发控制。
|
||||
|
||||
在这个模块中,你可以看到,一些看似相同的需求,其实可以有完全不同的解决方案,比如海量的短视频和海量的网盘存储;还有一些看似非常不同的场景,其实可以用同一个技术搞定,比如短URL和短视频。
|
||||
|
||||
实战模块二:高并发系统的高性能架构案例
|
||||
|
||||
我们将主要讨论在高并发场景下,如何保证系统的响应性能。
|
||||
|
||||
在这个模块中,你会看到,在海量的网页中快速搜索到一些网页,和在海量的人群中快速寻找一些人,其技术挑战是如何的不同,其解决方案又分别是如何的巧妙。
|
||||
|
||||
实战模块三:高并发系统的高可用架构案例
|
||||
|
||||
高并发导致系统的崩溃,最经典的案例莫过于明星半夜宣布离婚导致的微博宕机。为什么明星离婚会导致微博崩溃?拥有数千万关注的明星,微博消息是如何推送给粉丝的?微博如何处理这种热点新闻的海量消息转发所引起的系统压力?
|
||||
|
||||
实战模块四:安全系统架构案例
|
||||
|
||||
系统安全也是高并发系统的一个重要挑战。恶意的用户请求如何处理?敏感的数据如何加密解密?这里的几个案例都来自真实的应用。如果你需要,你可以将这几个设计直接落地,开发、应用到你的工作中。
|
||||
|
||||
实战模块五:网约车架构案例专题
|
||||
|
||||
在这个模块里,我们将深入讨论如何设计一个数亿用户、千万日订单的高并发打车软件。面对业务迭代,如何利用DDD对系统微服务进行重构设计。还有如何利用大数据技术,实现大数据杀熟(然而并不可以)。
|
||||
|
||||
最后,最重要的,就是希望你能把自己想象成大厂架构师,设身处地地思考,每一个案例都要产生自己的意见和看法,并表达出来。
|
||||
|
||||
期望你能在这个专栏学习结束后,自己挑选几个大厂的应用案例,按照专栏文章的设计模板,自己完成这些应用的架构设计。做到这一点,你就可以说对高并发架构登堂入室了,对自己的架构能力也建立起信心了。
|
||||
|
||||
祝你学习顺利,成为一名实战能力强、能够主导公司技术核心的架构师。
|
||||
|
||||
|
||||
|
||||
|
161
专栏/李智慧·高并发架构实战课/01软件建模与文档:架构师怎样绘制系统架构蓝图?.md
Normal file
161
专栏/李智慧·高并发架构实战课/01软件建模与文档:架构师怎样绘制系统架构蓝图?.md
Normal 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建模工具绘制满足淘宝早期应用场景的用例图以及部署图。如果你有什么灵感,或者遇到什么问题,欢迎在评论区留言,我们一起探讨。
|
||||
|
||||
也欢迎把这节课分享给更多对高并发架构设计感兴趣的朋友,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
137
专栏/李智慧·高并发架构实战课/02高并发架构设计方法:面对高并发,怎么对症下药?.md
Normal file
137
专栏/李智慧·高并发架构实战课/02高并发架构设计方法:面对高并发,怎么对症下药?.md
Normal file
@ -0,0 +1,137 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 高并发架构设计方法:面对高并发,怎么对症下药?
|
||||
你好,我是李智慧。
|
||||
|
||||
我们知道,“高并发”是现在系统架构设计的核心关键词。一个架构师如果设计、开发的系统不支持高并发,那简直不好意思跟同行讨论。但事实上,在架构设计领域,高并发的历史非常短暂,这一架构特性是随着互联网,特别是移动互联网的发展才逐渐变得重要起来的。
|
||||
|
||||
现在有很多大型互联网应用系统,其用户是面向全球的普通大众,用户体量动辄十几亿。这些用户即使只有万分之一同时访问系统,也会产生十几万的并发访问量。
|
||||
|
||||
因此,高并发是现在的大型互联网系统必须面对的挑战,当同时访问系统的用户不断增加时,要消耗的系统计算资源也不断增加。所以系统需要更多的CPU和内存去处理用户的计算请求,需要更多的网络带宽去传输用户的数据,也需要更多的硬盘空间去存储用户的数据。而当消耗的资源超过了服务器资源极限的时候,服务器就会崩溃,整个系统将无法正常使用。
|
||||
|
||||
今天我将基于高并发系统的技术挑战,来为你介绍典型的分布式解决方案。这节课的内容,会被应用到后面的大部分实战案例中。所以我希望通过这节课,带你做个简单的预习,同时你也能对自己学过的高并发技术做个简单回顾。
|
||||
|
||||
我要先说明一点,今天的高并发系统架构方法比较多,但它们是殊途同归的,都要遵循一个相同的高并发应对思路。所以我们今天的首要目标就是明确这个思路到底是什么,也就是要搞清楚高并发系统架构的方法论。
|
||||
|
||||
高并发系统架构的方法论
|
||||
|
||||
高并发的技术挑战,核心就是为了满足用户的高并发访问,系统需要提供更多的计算资源。那么如何提供这些计算资源,也就是说,如何使系统的计算资源随着并发的增加而增加?
|
||||
|
||||
对此,人们提出各种技术解决方案,这些解决方案大致可以分成两类,一类是传统大型软件系统的技术方案,被称作垂直伸缩方案。所谓的垂直伸缩就是提升单台服务器的处理能力,比如用更快频率的CPU、更多核的CPU、更大的内存、更快的网卡、更多的磁盘组成一台服务器,从普通服务器升级到小型机,从小型机提升到中型机,从中型机提升到大型机,从而使单台服务器的处理能力得到提升。通过这种手段提升系统的处理能力。
|
||||
|
||||
当业务增长,用户增多,服务器计算能力无法满足要求的时候,就会用更强大的计算机。计算机越强大,处理能力越强大,当然价格也越昂贵,技术越复杂,运维越困难。
|
||||
|
||||
由于垂直伸缩固有的这些问题,人们又提出另一类解决方案,被称作水平伸缩方案。所谓的水平伸缩,指的是不去提升单机的处理能力,不使用更昂贵更快更厉害的硬件,而是使用更多的服务器,将这些服务器构成一个分布式集群,通过这个集群,对外统一提供服务,以此来提高系统整体的处理能力。
|
||||
|
||||
水平伸缩除了可以解决垂直伸缩的各种问题,还有一个天然的好处,那就是随着系统并发的增加,可以一台服务器一台服务器地添加资源,也就是说,具有更好的弹性。而这种弹性是大多数互联网应用场景所必须的。因为我们很难正确估计一个互联网应用系统究竟会有多少用户来访问,以及这些用户会在什么时候来访问。而水平伸缩的弹性可以保证不管有多少用户,不管用户什么时候来访问,只要随时添加服务器就可以了。
|
||||
|
||||
因此现在的大型互联网系统多采取水平伸缩方案,来应对用户的高并发访问。
|
||||
|
||||
高并发系统架构的方法
|
||||
|
||||
我们知道了分布式集群优势明显,但是将一堆服务器放在一起,用网线连起来,并不能天然地使它们构成一个系统。要想让很多台服务器构成一个整体,就需要在架构上进行设计,使用各种技术,让这些服务器成为整体系统的一个部分,将这些服务器有效地组织起来,统一提升系统的处理能力。
|
||||
|
||||
这些相关的技术就是高并发系统架构的主要技术方法,其核心是各种分布式技术。
|
||||
|
||||
分布式应用
|
||||
|
||||
应用服务器是处理用户请求的主要服务器,工程师开发的代码就部署在这些服务器上。在系统运行期间,每个用户请求都需要分配一个线程去处理,而每个线程又需要占用一定的CPU和内存资源。所以当高并发的用户请求到达的时候,应用服务器需要创建大量线程,消耗大量计算机资源,当这些资源不足的时候,系统就会崩溃。
|
||||
|
||||
解决这个问题的主要手段就是使用负载均衡服务器,将多台应用服务器构成一个分布式集群,用户请求首先到达负载均衡服务器,然后由负载均衡服务器将请求分发到不同的应用服务器上。当高并发的用户请求到达时,请求将被分摊到不同的服务器上。这样一来,每台服务器创建的线程都不会太多,占用的资源也在合理范围内,系统就会保持正常运行。
|
||||
|
||||
通过负载均衡服务器构建分布式应用集群如下图。
|
||||
|
||||
|
||||
|
||||
分布式缓存
|
||||
|
||||
系统在运行期需要获取很多数据,而这些数据主要存储在数据库中,如果每次获取数据都要到数据库访问,会给数据库造成极大的负载压力。同时数据库的数据存储在硬盘中,每次查询数据都要进行多次硬盘访问,性能也比较差。
|
||||
|
||||
目前常用的解决办法就是使用缓存。我们可以将数据缓存起来,每次访问数据的时候先从缓存中读取,如果缓存中没有需要的数据,才去数据库中查找。这样可以极大降低数据库的负载压力,也有效提高了获取数据的速度。同样,缓存可以通过将多台服务器够构成一个分布式集群,提升数据处理能力,如下图。
|
||||
|
||||
|
||||
|
||||
首先应用程序调用分布式缓存的客户端SDK,SDK会根据应用程序传入的key进行路由选择,从分布式缓存集群中选择一台缓存服务器进行访问。如果分布式缓存中不存在要访问的数据,应用程序就直接访问数据库,从数据库中获取数据,然后将该数据写入到缓存中。这样,下次再需要访问该数据的时候,就可以直接从缓存中得到了。
|
||||
|
||||
分布式消息队列
|
||||
|
||||
分布式消息队列是解决突发的高并发写操作问题和实现更简单的集群伸缩的一种常用技术方案。消息队列架构主要包含三个角色:消息生产者、消息队列、消息消费者,如下图。
|
||||
|
||||
|
||||
|
||||
比如我们要写数据库,可以直接由应用程序写入数据库,但是如果有突发的高并发写入请求,就会导致数据库瞬间负载压力过大,响应超时甚至数据库崩溃。
|
||||
|
||||
但是如果我们使用消息队列,应用程序(消息生产者)就可以将写数据库的操作,写入到消息队列中,然后由消息消费者服务器从消息队列中消费消息,根据取出来的消息将数据写入到数据库中。当有突发的高并发写入的时候,只要控制消息消费者的消费速度,就可以保证数据库的负载压力不会太大。
|
||||
|
||||
同时,由于消息生产者和消息消费者没有调用耦合,当我们需要增强系统的处理能力,只需要增加消息生产者或者消息消费者服务器就可以了,不需要改动任何代码,实现伸缩更加简单。
|
||||
|
||||
分布式关系数据库
|
||||
|
||||
关系数据库本身并不支持伸缩性,但是关系数据库又是存储数据最传统的手段。为了解决关系数据库存储海量数据以及提供高并发读写的问题,人们提出了将数据进行分片,再将不同分片写入到不同数据库服务器的方法。
|
||||
|
||||
通过这种方法,我们可以将多台服务器构建成一个分布式的关系数据库集群,从而实现数据库的伸缩性,如下图。
|
||||
|
||||
|
||||
|
||||
分布式微服务
|
||||
|
||||
我们前面提到的分布式应用,是在一个应用程序内部完成大部分的业务逻辑处理,然后将这个应用程序部署到一个分布式服务器集群中对外提供服务,这种架构方案被称作单体架构。与此相对应的是分布式微服务架构,这是一种目前更广为使用的架构方案,如下图。
|
||||
|
||||
|
||||
|
||||
微服务的核心思想是将单体架构中庞大的业务逻辑拆分成一些更小、更低耦合的服务,然后通过服务间的调用完成业务的处理。
|
||||
|
||||
具体处理过程是:用户请求通过负载均衡服务器分发给一个微服务网关集群,在网关内开发一个简单的微服务客户端,客户端调用一个或多个微服务完成业务处理,并将处理结果构造成最后的响应结果返回给用户。
|
||||
|
||||
微服务架构的实现需要依赖一个微服务框架,这个框架包括一个微服务注册中心和一个RPC远程调用框架。微服务客户端通过注册中心得到要调用的微服务具体的地址列表,然后通过一个软负载均衡算法选择其中一个服务器地址,再通过PRC进行远程调用。
|
||||
|
||||
此外,除了以上这些分布式技术,高并发系统中常用的还有大数据、分布式文件、区块链、搜索引擎、NoSQL、CDN、反向代理等技术,也都是一些非常经典的分布式技术。如果你对这些技术感兴趣,想要更详细地了解它们,那么你可以阅读我在极客时间的另两个专栏,分别是《从0开始学大数据》和《后端技术面试38讲》。
|
||||
|
||||
系统并发指标
|
||||
|
||||
我们这个专栏大部分案例都是关于高并发系统的,那么和并发相关的指标有哪些?并发量又该如何估算?首先,我们来看和并发相关的指标,主要有以下这些。
|
||||
|
||||
目标用户数
|
||||
|
||||
目标用户数是所有可能访问我们系统的潜在用户的总和,比如微信的目标用户是所有中国人,那么微信的目标用户数就是13亿。目标用户数可以反映潜在的市场规模。
|
||||
|
||||
系统用户数
|
||||
|
||||
并不是所有的目标用户都会来访问我们的系统,只有那些真正访问过我们系统的用户才被称作系统用户。越是成功的系统,系统用户数和目标用户数越接近。
|
||||
|
||||
活跃用户数
|
||||
|
||||
同样地,访问过我们系统的用户可能只是偶尔过来访问一下,甚至只访问一次就永不再来。所以我们还需要关注用户的活跃度,也就是经常来访问的用户规模有多大。如果以一个月为单位,那么一个月内只要来访问过一次,就会被统计为活跃用户,这个数目被称为月活用户数。同样地,一天内访问过的总用户数被称为日活用户数。
|
||||
|
||||
在线用户数
|
||||
|
||||
当活跃用户登录我们的系统的时候,就成为在线用户了。在线用户数就是正在使用我们系统的用户总数。
|
||||
|
||||
并发用户数
|
||||
|
||||
但在线用户也并不总是在点击App,请求我们的系统服务,他可能搜索得到一个页面,然后就在自己的手机端浏览。只有发起请求,在服务器正在处理这个请求的用户才是并发用户。事实上,高并发架构主要关注的就是用户发起请求,服务器处理请求时需要消耗的计算资源。所以并发用户数是架构设计时主要关注的指标。
|
||||
|
||||
在我们后续的案例分析中,我都是根据市场规模估计一个目标用户数,然后再根据产品特点、竞品数据等,逐步估算其他的用户数指标。
|
||||
|
||||
有了上面这些用户数指标,我们就可以进一步估算架构设计需要考虑的其他一些技术指标,比如每天需要新增的文件存储空间,存储总系统用户需要的数据库规模,总网络带宽,每秒处理的请求数等等。
|
||||
|
||||
技术指标估算能力是架构师的一个重要能力,有了这个能力,你才有信心用技术解决未来的问题,也会因此对未来充满信心。这个估算过程,我们会在后面的案例课中不断重复,你也可以根据你的判断,分析这些估算是否合理,还有哪些没有考虑到的、影响架构设计的指标。
|
||||
|
||||
小结
|
||||
|
||||
高并发架构的主要挑战就是大量用户请求需要使用大量的计算资源。至于如何增加计算资源,互联网应用走出了一条水平伸缩的发展道路,也就是通过构建分布式集群架构,不断向集群中添加服务器,以此来增加集群的计算资源。
|
||||
|
||||
那如何增加服务器呢?对此,又诞生了各种各样的分布式技术方案。我们掌握了这些分布式技术,就算是掌握了高并发系统架构设计的核心。具体这些技术如何应用在高并发系统的架构实践中,我们在后面的案例中会不断进行展示。
|
||||
|
||||
思考题
|
||||
|
||||
我们在前面提到过,分布式缓存客户端SDK会根据应用程序传入的key,从分布式缓存集群中选择一台服务器进行访问,那么这个客户端SDK如何选择服务器呢?它怎么知道自己要访问的key在哪台服务器上?你可以尝试说说自己知道几种方法(算法),它们各有什么优缺点。
|
||||
|
||||
欢迎在评论区分享你的思考,也欢迎把这节课分享给更多对高并发架构设计感兴趣的朋友,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
221
专栏/李智慧·高并发架构实战课/03短URL生成器设计:百亿短URL怎样做到无冲突?.md
Normal file
221
专栏/李智慧·高并发架构实战课/03短URL生成器设计:百亿短URL怎样做到无冲突?.md
Normal 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,并存储起来。
|
||||
用户可以访问这个短URL,Fuxi将请求重定向到原始长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万。
|
||||
|
||||
|
||||
\(\\small(5亿\\times100)\\div(30\\times24\\times60\\times60)\\approx20000\)
|
||||
|
||||
一般系统高峰期访问量是平均访问量的2倍,因此系统架构需要支持的吞吐能力应为4万。
|
||||
|
||||
|
||||
网络带宽-
|
||||
短URL的重定向响应包含长URL地址内容,长URL地址大约500B,HTTP响应头其他内容大约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%请求响应时间应小于5ms,99%请求响应时间小于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链表类图如下。
|
||||
|
||||
|
||||
|
||||
URLNode:URL链表元素类,成员变量uRL即短URL字符串,next指向下一个链表元素。
|
||||
|
||||
LinkedURL:URL链表主类,成员变量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编码表中的62,63进行编码:将“+”改为“-”,将“/”改为“_”,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万QPS,10ms平均响应时间,这种情况下,真正的并发量只有200,这个200是如何得到的?
|
||||
|
||||
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
221
专栏/李智慧·高并发架构实战课/04网页爬虫设计:如何下载千亿级网页?.md
Normal file
221
专栏/李智慧·高并发架构实战课/04网页爬虫设计:如何下载千亿级网页?.md
Normal 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亿\\div(30\\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,我会在后面的《常见海量数据处理技术回顾》这一讲详细讨论它。
|
||||
|
||||
思考题
|
||||
|
||||
一个设计良好的爬虫需要面对的情况还有很多,你还能想到哪些文中没提及的情况?最好也能和我聊聊对应的设计方案。
|
||||
|
||||
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
172
专栏/李智慧·高并发架构实战课/05网盘系统设计:万亿GB网盘如何实现秒传与限速?.md
Normal file
172
专栏/李智慧·高并发架构实战课/05网盘系统设计:万亿GB网盘如何实现秒传与限速?.md
Normal 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\\div(24\\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分配全局唯一的BlockID(BlockID为严格递增的64位正整数,总可记录数据大小\(\\small 2^{64}\\times4MB=180亿PB\),足以满足DBox的应用场景)。
|
||||
|
||||
下一步,API服务器将文件元数据与BlockID记录在数据库中,并将BlockID列表和应用程序可以连接的Block服务器列表返回客户端。客户端连接Block服务器请求上传Block,Block服务器连接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分片,如果某个用户创建大量文件,还会导致分片不均衡,你有什么优化的手段和方法吗?
|
||||
|
||||
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
174
专栏/李智慧·高并发架构实战课/06短视频系统设计:如何支持三千万用户同时在线看视频?.md
Normal file
174
专栏/李智慧·高并发架构实战课/06短视频系统设计:如何支持三千万用户同时在线看视频?.md
Normal 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亿\\div(24\\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使用MPEG–DASH流媒体传输协议进行视频流传输,因为这个协议具有自适应能力,而且支持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中,主键就是视频ID,value就是。
|
||||
|
||||
假设另一个用户上传的视频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%以上,也就是说,通过合理使用CDN,QuickTok数据中心需要处理的带宽压力不到4Tb。
|
||||
|
||||
缩略图生成与推荐设计
|
||||
|
||||
用户可以通过App主页、搜索结果页、视频推荐页等页面看到视频列表,其中每个视频都需要有个缩略图。用户点击缩略图,就开始播放视频。
|
||||
|
||||
缩略图通常是由视频的某一帧画面缩略而生成的。事实上,缩略图的选择会极大地影响用户点击、播放视频的意愿。一个10分钟的视频大约包含3万帧画面,选择哪一帧画面,才能使用户点击视频的可能性最大?以及,针对不同的用户分类,是否选择不同的缩略图会产生更高的点击率?
|
||||
|
||||
我们需要通过大数据平台的机器学习引擎来完成缩略图的生成和推荐,如下图。
|
||||
|
||||
|
||||
|
||||
缩略图的生成和推荐可以分为两个具体过程:
|
||||
|
||||
|
||||
实时在线的缩略图推荐过程a;
|
||||
利用离线机器学习生成优质缩略图的过程b。
|
||||
|
||||
|
||||
a过程中,用户通过搜索引擎搜索视频,搜索引擎产生搜索结果视频列表后,根据视频ID从缩略图存储中获取对应的缩略图。
|
||||
|
||||
但是,一个视频可能对应很多个缩略图,如果想要显示最吸引当前用户的那个,搜索引擎就需要调用QuickTok大数据平台的缩略图推荐引擎进行推荐。
|
||||
|
||||
推荐引擎可以获取当前用户的偏好特征标签以及视频对应的多个缩略图特征,使用XGboost算法训练好的模型,将用户特征标签和缩略图特征进行匹配,然后返回最有可能被当前用户点击的缩略图ID。搜索引擎再按照ID,将对应的缩略图构建到搜索结果页面,返回给用户。
|
||||
|
||||
用户浏览搜索结果列表,点击某些缩略图进行播放。App应用会将用户的浏览与点击数据发送给QuickTok大数据平台,这样就进入了利用机器学习来生成优质缩略图的过程b。
|
||||
|
||||
机器学习系统获取到了海量用户的浏览和点击数据,同时获取每个缩略图的特征。一方面,机器可以学习到,哪些特征的缩略图更容易获得用户点击,从而生成优质缩略图特征标签库;另一方面,机器还可以学习到每个用户自身更偏好的图像特征标签,供前面提到的推荐引擎使用。
|
||||
|
||||
有了机器学习系统的加持,视频内容处理器就可以使用优质特征标签库来处理上传的视频内容,抽取符合优质特征的帧,进而生成缩略图。
|
||||
|
||||
以上的a、b两个过程不断循环迭代,系统就可以不断优化优质特征标签库,不断使缩略图更符合用户喜好。
|
||||
|
||||
那最开始没有特征库的时候怎么办呢?视频内容处理器可以使用随机的办法,抽取一些帧作为缩略图,进行冷启动。机器学习再从这些随机抽取的缩略图上开始学习,从而进入循环优化过程。
|
||||
|
||||
小结
|
||||
|
||||
我们在缩略图生成部分,使用了大数据和机器学习的一些技术,如果你不熟悉,可能会觉得有点困难。但是现在人工智能和机器学习几乎是稍具规模的互联网系统的标配,架构师作为整个系统的设计者、技术负责人,可能对算法的细节无法做出具体的优化,但是对于算法在整个架构中的作用、相关数据的处理和流转必须非常熟悉,才能设计出满足业务需要的架构方案。
|
||||
|
||||
所以,大数据和机器学习的原理和应用方法应该是架构师技能栈的一部分,能够和算法工程师顺畅讨论技术细节是架构师必备的能力。如果你对这部分知识掌握不完整,可以阅读专栏《从0开始学大数据》。
|
||||
|
||||
思考题
|
||||
|
||||
不止是缩略图的选择需要用到推荐算法,视频内容本身也需要推荐算法:当用户播放完一个视频,QuickTok需要给用户自动播放下一个视频,以此增强用户粘性。那么下一个视频应该播放什么?你是否可以参考文中的缩略图生成与推荐架构图,自己画一个视频推荐的架构图?如果能说说你的设计思路就更好了。
|
||||
|
||||
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
133
专栏/李智慧·高并发架构实战课/07海量数据处理技术回顾:为什么分布式会遇到CAP难题?.md
Normal file
133
专栏/李智慧·高并发架构实战课/07海量数据处理技术回顾:为什么分布式会遇到CAP难题?.md
Normal 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)的用户记录存储到服务器1,ID为奇数(如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算法,这个算法要解决的问题是什么?解决的思路是什么?
|
||||
|
||||
欢迎在评论区分享你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
130
专栏/李智慧·高并发架构实战课/08秒杀系统设计:你的系统可以应对万人抢购盛况吗?.md
Normal file
130
专栏/李智慧·高并发架构实战课/08秒杀系统设计:你的系统可以应对万人抢购盛况吗?.md
Normal 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秒杀系统针对的是大量用户在短时间购买极少数商品的情况,通过限流器拦截大量用户请求,进而降低系统负载压力的设计思路。那么对于大量用户在短时间购买大量商品的情况,比如双十一这种电商大促场景,设计方案又非常不同,你有什么样的设计思路呢?
|
||||
|
||||
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
171
专栏/李智慧·高并发架构实战课/09交友系统设计:哪种地理空间邻近算法更快?.md
Normal file
171
专栏/李智慧·高并发架构实战课/09交友系统设计:哪种地理空间邻近算法更快?.md
Normal 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字段,用户当前的纬度和经度为X,Y,如果我们想要查找和当前用户经、纬度距离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公里一个网格。
|
||||
|
||||
这样每个用户必然会落入到一个网格中,我们在用户表中记录用户所在的网格ID(gridID),然后借助这个字段进行辅助查找,将查找范围限制在用户所在的网格(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、带宽负载、存储空间等性能指标,你能否估计下这些性能指标?
|
||||
|
||||
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
149
专栏/李智慧·高并发架构实战课/10搜索引擎设计:信息搜索怎么避免大海捞针?.md
Normal file
149
专栏/李智慧·高并发架构实战课/10搜索引擎设计:信息搜索怎么避免大海捞针?.md
Normal file
@ -0,0 +1,149 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 搜索引擎设计:信息搜索怎么避免大海捞针?
|
||||
你好,我是李智慧。
|
||||
|
||||
在[04讲]中,我们讨论了大型分布式网络爬虫的架构设计,但是网络爬虫只是从互联网获取信息,海量的互联网信息如何呈现给用户,还需要使用搜索引擎完成。因此,我们准备开发一个针对全网内容的搜索引擎,产品名称为“Bingoo”。
|
||||
|
||||
Bingoo的主要技术挑战包括:
|
||||
|
||||
|
||||
针对爬虫获取的海量数据,如何高效地进行数据管理;
|
||||
当用户输入搜索词的时候,如何快速查找包含搜索词的网页内容;
|
||||
如何对搜索结果的网页内容进行排序,使排在搜索结果列表前面的网页,正好是用户期望看到的内容。
|
||||
|
||||
|
||||
概要设计
|
||||
|
||||
一个完整的搜索引擎包括分布式爬虫、索引构造器、网页排名算法、搜索器等组成部分,Bingoo的系统架构如下。
|
||||
|
||||
|
||||
|
||||
分布式爬虫通过存储服务器将爬取的网页存储到分布式文件集群HDFS,为了提高存储效率,网页将被压缩后存储。存储的时候,网页一个文件挨着一个文件地连续存储,存储格式如下。
|
||||
|
||||
|
||||
|
||||
每个网页被分配得到一个8字节长整型docID,docID之后用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/2,1/3,1分。用公式表示就是
|
||||
|
||||
\(\\small PR(A) = \\frac{PR(B)}{2}+\\frac{PR(C)}{3}+\\frac{PR(D)}{1}\)
|
||||
|
||||
等号左边是经过一次投票后,A页面的PageRank分值;等号右边每一项的分子是包含A页面超链接的页面的PageRank分值,分母是该页面包含的超链接数目。
|
||||
|
||||
这样经过一次计算后,每个页面的PageRank分值就会重新分配,重复同样的算法过程,经过几次计算后,根据每个页面PageRank分值进行排序,就得到一个页面重要程度的排名表。根据这个排名表,将用户搜索出来的网页结果排序,排在前面的通常也正是用户期待的结果。
|
||||
|
||||
但是这个算法还有个问题,如果某个页面只包含指向自己的超链接,其他页面不断给它送分,而自己一分不出,随着计算执行次数越多,它的分值也就越高,这显然是不合理的。这种情况就像下图所示的,A页面只包含指向自己的超链接。
|
||||
|
||||
|
||||
|
||||
解决方案是,设想浏览一个页面的时候,有一定概率不是点击超链接,而是在地址栏输入一个URL访问其他页面,表示在公式上,就是
|
||||
|
||||
\(\\small PR(A) = \\alpha(\\frac{PR(B)}{2}+\\frac{PR(C)}{3}+\\frac{PR(D)}{1})+\\frac{(1-\\alpha)}{4}\)
|
||||
|
||||
上面\(\\small (1-\\alpha)\)就是跳转到其他任何页面的概率,通常取经验值0.15(即\(\\small \\alpha\) 为0.85),因为有一定概率输入的URL是自己的,所以加上上面公式最后一项,其中分母4表示所有网页的总数。
|
||||
|
||||
那么对于N个网页,任何一个页面\(\\small P_{i}\)的PageRank计算公式如下:
|
||||
|
||||
\(\\small PageRank(P_{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的计算,需要在万亿级的数据上进行多次迭代计算才能完成。数据量和计算量都非常大,如何完成这样的计算?也就是说,具体编程实现是怎样的?
|
||||
|
||||
欢迎在评论区分享你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
178
专栏/李智慧·高并发架构实战课/11反应式编程框架设计:如何使方法调用无阻塞等待?.md
Normal file
178
专栏/李智慧·高并发架构实战课/11反应式编程框架设计:如何使方法调用无阻塞等待?.md
Normal 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纯消息驱动、异步无阻塞的优良特点,适合许多对并发处理要求高,需要快速、及时响应的场景,你能想到的现实应用场景有哪些呢?
|
||||
|
||||
欢迎在评论区分享你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
134
专栏/李智慧·高并发架构实战课/12高性能架构的三板斧:分析系统性能问题从哪里入手?.md
Normal file
134
专栏/李智慧·高并发架构实战课/12高性能架构的三板斧:分析系统性能问题从哪里入手?.md
Normal 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、反向代理和分布式对象缓存。
|
||||
|
||||
CDN(Content Delivery Network)即内容分发网络。我们上网的时候,App或者浏览器想要连接到互联网应用的服务器,需要移动、电信这样的网络服务商为我们提供网络服务,建立网络连接才可以上网。而这些服务商需要在全国范围内部署骨干网络、交换机机房,才能完成网络连接服务。
|
||||
|
||||
因为这些交换机机房可能会离用户非常近,所以我们自然想到了,互联网应用能不能在这些交换机机房中部署缓存服务器呢?这样的话,用户就可以近距离获得自己需要的数据,既提高了响应速度,又节约了网络带宽和服务器资源。
|
||||
|
||||
答案是当然可以。这个部署在网络服务商机房中的缓存就是CDN,因为距离用户非常近,又被称作网络连接的第一跳。目前很多互联网应用大约80%以上的网络流量都是通过CDN返回的。
|
||||
|
||||
|
||||
|
||||
我们有时候需要通过代理上网,这个代理是代理我们的客户端上网设备。而反向代理则是代理服务器,所有的网络请求都需要通过反向代理才能到达应用程序服务器。那么在这里加一个缓存,尽快将数据返回给用户,而不是发送给应用服务器,这就是反向代理缓存。
|
||||
|
||||
|
||||
|
||||
用户请求到达反向代理缓存服务器,反向代理检查本地是否有需要的数据,如果有就直接返回;如果没有,就请求应用服务器,得到需要的数据后缓存在本地,然后返回给用户。同时,只要将后面的应用服务器部署为一个集群,反向代理服务器在请求后面的应用服务器的时候,进行负载均衡选择,那么这个反向代理缓存服务器也就同时成为了前面讨论的应用层负载均衡服务器。也就是说,一台服务器,既做反向代理服务器,也做负载均衡服务器。
|
||||
|
||||
CDN和反向代理缓存对应用程序是透明的,通常被当做系统前端的一部分。而应用程序如果要使用缓存,就需要分布式对象缓存。分布式对象缓存访问架构如下图。
|
||||
|
||||
|
||||
|
||||
多台缓存服务器构成一个缓存集群,缓存数据存储在每台服务器的内存中。每个程序需要依赖一个缓存客户端SDK,通过SDK的API来访问缓存服务器。应用程序先调用API,由API调用SDK的路由算法,路由算法根据缓存的key值,计算这个key应该访问哪台缓存服务器。路由算法计算得到目标服务器的IP地址和端口号后,API再调用SDK的通信模块,将值以及缓存操作命令发送给具体的某台缓存服务器,最终由这台服务器完成缓存操作。
|
||||
|
||||
使用缓存架构可以减少不必要的计算,快速响应用户请求。但是缓存只能改善系统的读操作性能,对于写操作,缓存是无能为力的。我们不能把用户提交的数据直接写入缓存中,因为缓存通常被认为是一种不可靠的存储。
|
||||
|
||||
消息队列
|
||||
|
||||
优化写操作性能的主要手段是使用消息队列,将写操作异步化。典型的应用程序写数据的方式如下图。
|
||||
|
||||
|
||||
|
||||
应用服务器收到用户写操作请求后,调用数据库操作接口,完成数据写入数据库的操作。但是数据库处理速度比较慢,同时又对并发压力比较敏感。大量操作请求同时提交到数据库,可能会导致数据库负载压力太大而崩溃。
|
||||
|
||||
使用消息队列将写操作异步化如下图。
|
||||
|
||||
|
||||
|
||||
应用服务器收到用户写操作请求后,不是直接调用数据库,而是将写操作请求发送给消息队列服务器,再由消息消费者服务器从消息队列服务器消费消息,完成对数据库的写操作。
|
||||
|
||||
这样会带来两个好处。一方面,用户请求发送给消息队列就可以直接返回响应给用户了,而消息队列服务器的处理速度要远远快于数据库,用户端的响应时间可以极大缩短;另一方面,消息队列写数据库的时候,可以根据数据库的负载能力控制写入的速度,即使用户请求并发很高,也不会导致数据库崩溃,消息队列可以使系统运行在一个性能最优的负载压力范围内。
|
||||
|
||||
这种在用户请求高并发的时候控制处理速度,在用户请求低谷的时候,继续处理请求的方式叫做“削峰填谷”,如下图。
|
||||
|
||||
|
||||
|
||||
消息队列将直接调用的高峰访问压力推迟到访问低谷的时候处理,使系统保持在性能最优的状态下运行。
|
||||
|
||||
小结
|
||||
|
||||
这节课的三种高性能架构是最常用的架构性能优化手段,可以解决大多数系统架构性能问题。但是性能优化是一个系统的工程,不问青红皂白,不管三七二十一,上来就是三板斧,那做架构师也未免太容易了些。
|
||||
|
||||
性能优化必须有的放矢,必须要了解系统的关键技术设计,以及当前的系统性能指标,然后才能寻找到最合适的性能优化方式。所以性能优化需要从性能测试开始,具体过程可以总结为以下几步:
|
||||
|
||||
|
||||
进行性能测试,了解系统当前性能指标,发现哪些指标不符合性能需求。
|
||||
分析系统架构设计与关键技术实现,发现导致性能瓶颈的地方。
|
||||
进行架构以及代码优化,消除性能瓶颈。
|
||||
进行性能测试,分析优化是否达到目标。
|
||||
|
||||
|
||||
而且性能优化也并不是只能优化架构和代码。对于一个全球用户访问的系统,在全球各地部署多个数据中心,就近为用户服务可以极大降低网络传输的延迟,提升性能;对于一些少量而重要的数据计算,使用更好的CPU、更大的内存、更快的硬盘,也就是说,进行垂直伸缩,也可以极大改善性能;而对操作系统、虚拟机进行参数优化,对使用的第三方软件包进行升级改造,有时候也会对性能实现成倍的提升。
|
||||
|
||||
思考题
|
||||
|
||||
在你的工作实践中,曾经遇到过怎样的性能问题,最后如何解决?欢迎分享出来,我们一起讨论,一起学习。
|
||||
|
||||
|
||||
|
||||
|
174
专栏/李智慧·高并发架构实战课/13微博系统设计:怎么应对热点事件的突发访问压力?.md
Normal file
174
专栏/李智慧·高并发架构实战课/13微博系统设计:怎么应对热点事件的突发访问压力?.md
Normal 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亿\\div(24\\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为用户ID,value为用户最近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的架构,就相当于掌握了信息流产品的架构。
|
||||
|
||||
思考题
|
||||
|
||||
面对微博的高并发访问压力,你还能想到哪些方案可以优化系统?
|
||||
|
||||
欢迎在评论区分享你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
137
专栏/李智慧·高并发架构实战课/14百科应用系统设计:机房被火烧了系统还能访问吗?.md
Normal file
137
专栏/李智慧·高并发架构实战课/14百科应用系统设计:机房被火烧了系统还能访问吗?.md
Normal 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请求比超过1000:1),因此对系统的整体影响并不很大。同时用一种简单、廉价的方式实现多数据中心的数据一致性,开发和运维成本都比较低。
|
||||
|
||||
详细设计
|
||||
|
||||
作为一个百科服务类网站,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
|
||||
|
||||
欢迎在评论区分享你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
230
专栏/李智慧·高并发架构实战课/15限流器设计:如何避免超预期的高并发压力压垮系统?.md
Normal file
230
专栏/李智慧·高并发架构实战课/15限流器设计:如何避免超预期的高并发压力压垮系统?.md
Normal file
@ -0,0 +1,230 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
15 限流器设计:如何避免超预期的高并发压力压垮系统?
|
||||
你好,我是李智慧。
|
||||
|
||||
在互联网高可用架构设计中,限流是一种经典的高可用架构模式。因为某些原因,大量用户突然访问我们的系统时,或者有黑客恶意用DoS(Denial of Service,拒绝服务)方式攻击我们的系统时,这种未曾预期的高并发访问对系统产生的负载压力可能会导致系统崩溃。
|
||||
|
||||
解决这种问题的一个主要手段就是限流,即拒绝部分访问请求,使访问负载压力降低到一个系统可以承受的程度。这样虽然有部分用户访问失败,但是整个系统依然是可用的,依然能对外提供服务,而不是因为负载压力太大而崩溃,导致所有用户都不能访问。
|
||||
|
||||
为此,我们准备开发一个限流器,产品名称为“Diana”。
|
||||
|
||||
需求分析
|
||||
|
||||
我们将Diana定位为一个限流器组件,即Diana的主要应用场景是部署在微服务网关或者其他HTTP服务器入口,以过滤器的方式对请求进行过滤,对超过限流规则的请求返回“服务不可用”HTTP响应。
|
||||
|
||||
Diana的限流规则可通过配置文件获取,并需要支持本地配置和远程配置两种方式,远程配置优先于本地配置。限流方式包括:
|
||||
|
||||
|
||||
全局限流:针对所有请求进行限流,即保证整个系统处理的请求总数满足限流配置。
|
||||
账号限流:针对账号进行限流,即对单个账号发送的请求进行限流。
|
||||
设备限流:针对设备进行限流,即对单个客户端设备发送的请求进行限流。
|
||||
资源限流:针对某个资源(即某个URL)进行限流,即保证访问该资源的请求总数满足限流配置。
|
||||
|
||||
|
||||
并且Diana设计应遵循开闭原则,能够支持灵活的限流规则功能扩展,即未来在不修改现有代码和兼容现有配置文件的情况下,支持新的配置规则。
|
||||
|
||||
概要设计
|
||||
|
||||
Diana的设计目标是一个限流器组件,即Diana并不是一个独立的系统,不可以独立部署进行限流,而是部署在系统网关(或者其他HTTP服务器上),作为网关的一个组件进行限流,部署模型如下:
|
||||
|
||||
|
||||
|
||||
用户请求(通过负载均衡服务器)到达网关服务器。网关服务器本质也是一个HTTP服务器,限流器是部署在网关中的一个过滤器(filter)组件,和网关中的签名校验过滤器、用户权限过滤器等配置在同一个过滤器责任链(Chain of Responsibility)上。限流器应该配置在整个过滤器责任链的前端,也就是说,如果请求超过了限流,请求不需要再进入其他过滤器,直接被限流器拒绝。
|
||||
|
||||
用户请求进入限流器后,根据限流策略,判断该请求是否已经超过限流,如果超过,限流器直接返回状态码为503(Too 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,每个规则包括actor,unit,rpu,algo,scope
|
||||
actor为限流对象,可以是账号(actor),设备(device),全部(all)
|
||||
unit为限流时间单位,可以是秒(second),分(minute),时(hour),天(day)
|
||||
rpu为单位时间限流请求数(request per unit),即上面unit定义的单位时间内允许通过的请求数目,如unit为second,rpu为100,表示每秒允许通过100个请求,每秒超过100个请求就进行限流,返回503响应
|
||||
scope为rpu生效范围,可以是本地(local),也可以是全局(global),scope也决定了单位时间请求数量是记录在本地还是远程,local记录在本地,global记录在远程。
|
||||
algo限流算法,可以是window,sliding window,leaky bucket,token 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”。
|
||||
|
||||
小结
|
||||
|
||||
限流器是一个典型的技术中间件,使用者是应用系统开发工程师,他们在自己的应用系统中使用限流器,通过配置文件来实现满足自己业务场景的限流需求。这里隐含了一个问题:大家都是开发者,这些应用系统开发工程师为什么要用你开发的中间件?事实上,技术中间件天然会受到更多的挑剔,架构师在设计技术组件的时候要格外考虑易用性和扩展性,开发出来的技术中间件要能经得起同行的审视和挑战。
|
||||
|
||||
这篇设计文档中,包含了很多伪代码,这些伪代码是限流算法实现的核心逻辑。架构师一方面需要思考宏观的技术决策,一方面要思考微观的核心代码。这里两方面的能力支撑起架构师的技术影响力,既要能上得厅堂,在老板、客户等外部相关方面前侃侃而谈,保障自己和团队能掌控自己的技术方向;也要能下得厨房,搞定最有难度的代码实现,让团队成员相信跟着你混,没有迈不过去的技术坎。
|
||||
|
||||
思考题
|
||||
|
||||
滑动窗口算法中,如何管理时间片,以及如何计算滑动过程中的一个窗口内各个时间片的窗口计数器之和?用什么样的数据结构和算法比较合适?
|
||||
|
||||
欢迎在评论区分享你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
131
专栏/李智慧·高并发架构实战课/16高可用架构的十种武器:怎么度量系统的可用性?.md
Normal file
131
专栏/李智慧·高并发架构实战课/16高可用架构的十种武器:怎么度量系统的可用性?.md
Normal 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依赖B,B又依赖A;稳定依赖原则,即被依赖的组件尽量稳定,尽量少因为业务变化而变化;稳定抽象原则,即要想使组件稳定,组件就要更加抽象。
|
||||
|
||||
面向对象的低耦合原则:开闭原则,即对修改封闭、对扩展开放,对象可以扩展新功能,但是不能修改代码;依赖倒置原则,即高层对象不能依赖低层对象,而是要依赖抽象接口,而抽象接口属于高层;接口隔离原则,不要强迫使用者依赖它们不需要的方法,要用接口对方法进行隔离。
|
||||
|
||||
第二种武器:隔离
|
||||
|
||||
如果说解耦是逻辑上的分割,那么隔离就是物理上的分割。即将低耦合的组件进行独立部署,将不同组件在物理上隔离开来。每个组件有自己独立的代码仓库;每个组件可以独立发布,互不影响;每个组件有自己独立的容器进行部署,互不干扰。
|
||||
|
||||
所以,隔离就是分布式技术在业务上的应用,最常见的就是我们前面案例中也多次使用的微服务技术方案。微服务将一个复杂的大应用(单体架构系统)进行拆解,拆分成若干更细粒度的微服务,这些微服务之间互相依赖,实现原来大应用的功能逻辑。然后将这些微服务独立开发和发布,独立部署,微服务之间通过RPC(远程过程调用)进行依赖调用,就是微服务架构。
|
||||
|
||||
隔离使得系统间关系更加清晰,故障可以更加隔离开来,问题的发现与解决也更加快速,系统的可用性也更高。
|
||||
|
||||
不过,还要强调一下,隔离必须在低耦合的基础上进行才有意义。如果组件之间的耦合关系千头万绪、混乱不堪,隔离只会让这种混乱更雪上加霜。
|
||||
|
||||
第三种武器:异步
|
||||
|
||||
异步可以认为是在隔离的基础上进一步解耦,将物理上已经分割的组件之间的依赖关系进一步切断,使故障无法扩散,提高系统可用性。异步在架构上的实现手段主要是使用消息队列。
|
||||
|
||||
比如用户注册的场景。新用户提交注册请求后,需要给用户发送邮件,发送短信,保存数据库,还要将注册消息同步给其他产品等等。如果用微服务调用的方式,那么后续操作任何一个故障,都会导致业务处理失败,用户无法完成注册。
|
||||
|
||||
使用消息队列的异步架构,新用户注册消息发送给消息队列就立即返回,后续的操作通过消费消息来完成,即使某个操作发生故障也不会影响用户注册成功。如下图。
|
||||
|
||||
|
||||
|
||||
第四种武器:备份
|
||||
|
||||
备份主要解决硬件故障下系统的可用性,即一个服务部署在多个服务器上,当某个服务器故障的时候,请求切换到其他服务器上继续处理,保证服务是可用的。所以,备份与失效转移(failover)总是成对出现的,共同构成一个高可用解决方案。
|
||||
|
||||
最常见的备份就是负载均衡,前面的课程中说过,负载均衡主要解决高性能问题。但是,多台服务器构成一个集群,这些服务器天然就是互相备份的关系,任何一台服务器失效,只需要将分发到这台服务器的请求分发给其他服务器即可,如下图
|
||||
|
||||
|
||||
|
||||
由于应用服务器上只运行程序,不存储数据,所以请求切换到任何一台服务器,处理结果都是相同的。而对于存储数据的服务器,比如数据库,互相备份的服务器必须要互相同步数据,下图是MySQL主主备份的架构图。
|
||||
|
||||
|
||||
|
||||
第五种武器:重试
|
||||
|
||||
远程服务可能会由于线程阻塞、垃圾回收或者网络抖动,而无法及时返回响应,调用者可以通过重试的方式修复单次调用的故障。
|
||||
|
||||
需要注意的是,重试是有风险的。比如一个转账操作,第一次请求转账后没有响应,也许仅仅是响应数据在网络中超时了,如果这个时候进行重试,那么可能会导致重复转账,反而造成重大问题。
|
||||
|
||||
所以,可以重试的服务必须是幂等的。所谓幂等,即服务重复调用和调用一次产生的结果是相同的。有些服务天然具有幂等性,比如将用户性别设置为男性,不管设置多少次,结果都一样。
|
||||
|
||||
第六种武器:熔断
|
||||
|
||||
重试主要解决偶发的因素导致的单次调用失败,但是如果某个服务器一直不稳定,甚至已经宕机,再请求这个服务器或者进行重试都没有意义了。所以为了保证系统整体的高可用,对于不稳定或者宕机的服务器需要进行熔断。
|
||||
|
||||
熔断的主要方式是使用断路器阻断对故障服务器的调用,断路器状态图如下。
|
||||
|
||||
|
||||
|
||||
断路器有三种状态,关闭、打开、半开。断路器正常情况下是关闭状态,每次服务调用后都通知断路器。如果失败了,失败计数器就+1,如果超过开关阈值,断路器就打开,这个时候就不再请求这个服务了。过一段时间,达到断路器预设的时间窗口后,断路器进入半开状态,发送一个请求到该服务,如果服务调用成功,那么说明服务恢复,断路器进入关闭状态,即正常状态;如果服务调用失败,那么说明服务故障还没修复,断路器继续进入到打开状态,服务不可用。
|
||||
|
||||
第七种武器:补偿
|
||||
|
||||
前面几种方案都是故障发生时如何处理,而补偿则是故障发生后,如何弥补错误或者避免损失扩大。比如将处理失败的请求放入一个专门的补偿队列,等待失败原因消除后进行补偿,重新处理。
|
||||
|
||||
补偿最典型的使用场景是事务补偿。在一个分布式应用中,多个相关事务操作可能分布在不同的服务器上,如果某个服务器处理失败,那么整个事务就是不完整的。按照传统的事务处理思路,需要进行事务回滚,即将已经成功的操作也恢复到事务以前的状态,保证事务的一致性。
|
||||
|
||||
传统的事务回滚主要依赖数据库的特性,当事务失败的时候,数据库执行自己的undo日志,就可以将同一个事务的多条数据记录恢复到事务之初的状态。但是分布式服务没有undo日志,所以需要开发专门的事务补偿代码,当分布式事务失效的时候,调用事务补偿服务,将事务状态恢复如初。
|
||||
|
||||
第八种武器:限流
|
||||
|
||||
在高并发场景下,如果系统的访问量超过了系统的承受能力,可以通过限流对系统进行保护。限流是指对进入系统的用户请求进行流量限制,如果访问量超过了系统的最大处理能力,就会丢弃一部分用户请求,保证整个系统可用。这样虽然有一部分用户的请求被丢弃,但大部分用户还是可以访问系统的,总比整个系统崩溃,所有的用户都不可用要好。
|
||||
|
||||
我们在[第15篇]专门讨论过限流器的设计,这里不再赘述。
|
||||
|
||||
第九种武器:降级
|
||||
|
||||
降级是保护系统高可用的另一种手段。有一些系统功能是非核心的,但是也给系统产生了非常大的压力,比如电商系统中有确认收货这个功能,即便用户不确认收货,系统也会超时自动确认。
|
||||
|
||||
但实际上确认收货是一个非常重的操作,因为它会对数据库产生很大的压力:它要进行更改订单状态,完成支付确认,并进行评价等一系列操作。如果在系统高并发的时候去完成这些操作,那么会对系统雪上加霜,使系统的处理能力更加恶化。
|
||||
|
||||
解决办法就是在系统高并发的时候(例如淘宝双十一),将确认收货、评价这些非核心的功能关闭,也就是对系统进行降级,把宝贵的系统资源留下来,给正在购物的人,让他们去完成交易。
|
||||
|
||||
第十种武器:多活
|
||||
|
||||
多活,即异地多活,在多个地区建立数据中心,并都可以对用户提供服务,任何地区级的灾难都不会影响系统的可用。异地多活的架构案例我们已经在[第14讲]讨论过了。异地多活最极端的案例,是某应用准备将自己的服务器发射到太空,即使地球毁灭也能保证系统可用。
|
||||
|
||||
异地多活的架构需要考虑的重点是,用户请求如何分发到不同的机房去。这个主要可以在域名解析的时候完成,也就是用户进行域名解析的时候,会根据就近原则或者其他一些策略,完成用户请求的分发。另一个至关重要的技术点是,因为是多个机房都可以独立对外提供服务,所以也就意味着每个机房都要有完整的数据记录。用户在任何一个机房完成的数据操作,都必须同步传输给其他的机房,进行数据实时同步。
|
||||
|
||||
数据库实时同步最需要关注的就是数据冲突问题。同一条数据,同时在两个数据中心被修改了,该如何解决?某些容易引起数据冲突的服务采用类似MySQL的主主模式,也就是说多个机房在某个时刻是有一个主机房的,某些请求只能到达主机房才能被处理,其他的机房不处理这一类请求,以此来避免关键数据的冲突。
|
||||
|
||||
小结
|
||||
|
||||
除了以上的高可用架构方案,还有一些高可用的运维方案。
|
||||
|
||||
通过自动化测试减少系统的Bug。对于一个稳定运行的系统,每次变更发布可能只改动极小的一部分,如果只测试这一小部分的功能,那么潜在的其他可能引起故障的连带变更就会被忽视,进而可能引发大问题。但是如果全部都回归测试一遍,投入的测试成本又非常高。自动化测试可以实现自动化回归,对于那些没有变更的功能,自动发现是否有引入的Bug或预期之外的变更。
|
||||
|
||||
通过自动化监控尽早发现系统的故障。监控系统是技术团队的眼睛,没有监控的系统犹如盲人在崎岖的山路狂奔。所以,一个成熟的高可用系统中必定包含着完整的监控系统,实时监控各种技术指标和业务指标的变化。如果系统出现故障,超过设定的阈值就会引发监控系统报警,或者启动自动化故障修复服务。
|
||||
|
||||
通过预发布验证发现测试环境无法发现的Bug。系统在发布上线之前要经过各种测试,但是测试环境和线上运行环境还是会有很多不同。所以需要在线上集群中部署一台专门的预发布服务器,这台服务器访问的数据和资源完全是线上的,但是不会被用户访问到。开发人员发布代码的时候,先发布到这台预发布服务器,然后在这台服务器上做预发布验证,没有问题才会将代码发布到其他服务器上;如果有问题,也不会影响到用户访问,保证系统的高可用。
|
||||
|
||||
此外还可以通过灰度发布降低软件错误带来的影响。在一个大规模的应用集群中,每次只发布一小部分服务器,观察没有问题,再继续发布,保证即使程序有Bug,产生的影响也控制在较小的范围内。
|
||||
|
||||
思考题
|
||||
|
||||
你还能想到哪些文中没有提到的高可用方法?
|
||||
|
||||
欢迎在评论区补充你的思考,我们共同进步。
|
||||
|
||||
|
||||
|
||||
|
213
专栏/李智慧·高并发架构实战课/17Web应用防火墙:怎样拦截恶意用户的非法请求?.md
Normal file
213
专栏/李智慧·高并发架构实战课/17Web应用防火墙:怎样拦截恶意用户的非法请求?.md
Normal 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攻击字符前后加上“ ”字符串,使得攻击脚本无法运行,同时在浏览器显示的时候不会影响显示内容。
|
||||
|
||||
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注入攻击内容后,可以设置跳转错误页面,也可以选择消毒replace,replace表如下:
|
||||
|
||||
|
||||
|
||||
CSRF跨站点请求伪造攻击
|
||||
|
||||
CSRF(Cross Site Request Forgery,跨站点请求伪造),攻击者通过跨站请求,以合法用户的身份进行非法操作,如转账交易、发表评论等,如图:
|
||||
|
||||
|
||||
|
||||
CSRF 的主要手法是利用跨站请求,在用户不知情的情况下,以用户的身份伪造请求。其核心是利用了浏览器 Cookie 或服务器 Session 策略,盗取用户身份。
|
||||
|
||||
Zhurong的防攻击策略,是过滤器自动在所有响应页面的表单form中添加一个隐藏字段,合法用户在提交请求的时候,会将这个隐藏字段发送到服务器,防火墙检查隐藏字段值是否正确,来确定是否为CSRF攻击。恶意用户的请求是自己伪造的,无法构造这个隐藏字段,就会被防火墙拦截。
|
||||
|
||||
注释与异常信息泄露
|
||||
|
||||
为调试程序方便或其他不恰当的原因,有时程序开发人员会在前端页面程序中使用 HTML 注释语法进行程序注释,这些 HTML 注释就会显示在客户端浏览器,给黑客造成攻击便利。
|
||||
|
||||
此外,许多 Web 服务器默认是打开异常信息输出的,即服务器端未处理的异常堆栈信息会直接输出到客户端浏览器,这种方式虽然对程序调试和错误报告有好处,但同时也给黑客造成可乘之机。黑客通过故意制造非法输入,使系统运行时出错,获得异常信息,从而寻找系统漏洞进行攻击。
|
||||
|
||||
匹配HTML注释的正则表达式如下:
|
||||
|
||||
“<!--(.|
|
)*-->”
|
||||
|
||||
|
||||
如果匹配到HTML注释,就用空字符串replace该注释。
|
||||
|
||||
对于异常信息泄露,Zhurong会检查响应状态码。如果响应状态码为500系列错误,则会进一步匹配响应体内容,检查是否存在错误堆栈信息。
|
||||
|
||||
小结
|
||||
|
||||
这篇设计文档也是改编自某全球IT企业的内部设计文档,这个产品和该企业的Web服务器捆绑销售,已经在全球范围内售卖了十几年。这个产品也是中国分公司成立之初最成功的产品,帮助中国分公司奠定了自己在总公司的地位。而这个产品的最初版本,则是一个架构师带领一个开发小组花了几个月的时间就开发出来的。
|
||||
|
||||
人们常说软件工程师的职业生涯只有十几年,甚至只有几年。事实上,很多商业软件的生命周期都不止十几年,也就是说,在你的职业生涯中,只要开发出一款成功的软件,光是为这个软件修修补补、维护升级,你也能干个十几年,几十年。
|
||||
|
||||
但是很遗憾,就我所见,大多数软件工程师在自己的职业生涯中都没有经历过成功。要么就是加入一个已经成功的项目修修补补,要么就是在一个不温不火的项目里耗了几年,最后无疾而终。事实上,经历过成功的人会明白什么样的项目将会走向成功,所以不会守着一个成功的项目养老,而是不断追求新的成功;而没有经历过成功的人则在曲曲折折中走向自己的中年危机。
|
||||
|
||||
我们这个专栏挑选的设计,都是基于一些已经成功了的案例。成功的东西有一种成功的味道,正是这种味道带领成功者走向成功。希望你在学习技术的同时,也能嗅到成功的味道。
|
||||
|
||||
思考题
|
||||
|
||||
还有哪些常见的Web安全漏洞,如何进行防护?
|
||||
|
||||
附:极客时间也有一个专门讲Web漏洞攻击与防护的专栏[Web 漏洞挖掘实战],有兴趣的同学不妨看一看,再回来一起交流讨论。
|
||||
|
||||
|
||||
|
||||
|
216
专栏/李智慧·高并发架构实战课/18加解密服务平台:如何让敏感数据存储与传输更安全?.md
Normal file
216
专栏/李智慧·高并发架构实战课/18加解密服务平台:如何让敏感数据存储与传输更安全?.md
Normal 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对象,将加解密数据传给VenusService,VenusService加解密完成后创建一个新的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 code:0x5E、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的设计,开发实现一个这样的系统。
|
||||
|
||||
思考题
|
||||
|
||||
在你的工作中使用了哪些加密算法,算法以及密钥是否安全?有什么改进的思路吗?
|
||||
|
||||
|
||||
|
||||
|
156
专栏/李智慧·高并发架构实战课/19许可型区块链重构:无中心的区块链怎么做到可信任?.md
Normal file
156
专栏/李智慧·高并发架构实战课/19许可型区块链重构:无中心的区块链怎么做到可信任?.md
Normal 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 网络通信模块,使其需要进行安全验证,得到联盟许可才能加入新节点,进入当前联盟链网络。
|
||||
重构以太坊的共识算法。只有经过联盟成员认证授权的节点才能打包区块,打包节点按序轮流打包,无需算力证明。
|
||||
开发联盟共识控制台CCC(Consortium 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。
|
||||
|
||||
小结
|
||||
|
||||
区块链也是一个分布式系统,但是不同于我们前面讨论过的各种传统分布式系统。传统分布式系统的各个分布式服务器节点是只属于某一个组织的,叫做中心化数据存储,数据的准确性和安全性靠的是这个组织的保证,使用者需要信任这个组织,比如我们相信支付宝不会偷偷把我们余额里的钱转走。
|
||||
|
||||
而区块链的分布式服务器节点并不只属于某一个组织,区块链并没有中心,而且使用区块链也不需要信任某个组织,因为任何数据篡改都会导致区块链条的中断。
|
||||
|
||||
区块链的这种特性可以实现无中心的跨组织交易。传统上,平行的组织之间交易需要通过更上一级的组织作为中心记录交易数据,比如商业银行之间的转账,要靠中央银行的数据中心来完成。如果没有更上一级的组织呢,就很难进行交易了。而使用区块链技术,即使没有中心,这些组织也可以进行交易,同时很多上级组织也变得没有那么必要了。
|
||||
|
||||
所以区块链会使我们的社会变得更加自组织,也将会给全社会的生产关系带来更深刻的变革。
|
||||
|
||||
思考题
|
||||
|
||||
今天我想和你讨论两个问题,你也可以任选其一,回复在评论区。
|
||||
|
||||
|
||||
许可型区块链的应用场景还有哪些?
|
||||
你是否看好区块链未来的发展?为什么?
|
||||
|
||||
|
||||
|
||||
|
||||
|
133
专栏/李智慧·高并发架构实战课/20网约车系统设计:怎样设计一个日赚5亿的网约车系统?.md
Normal file
133
专栏/李智慧·高并发架构实战课/20网约车系统设计:怎样设计一个日赚5亿的网约车系统?.md
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
20 网约车系统设计:怎样设计一个日赚 5 亿的网约车系统?
|
||||
你好,我是李智慧。
|
||||
|
||||
网约车的官方定义是:“以互联网技术为依托,构建服务平台,整合供需信息,使用符合条件的车辆和驾驶员,提供非巡游的预约出租汽车服务的经营活动。”通俗地说就是:利用互联网技术平台,将乘客的乘车信息发送给合适的司机,由司机完成接送乘客的服务。网约车包含专车、快车、拼车等多种形式。
|
||||
|
||||
中国目前网约车用户规模约5亿,我们准备开发一个可支撑目前全部中国用户使用的网约车平台,应用名称为“Udi”。
|
||||
|
||||
需求分析
|
||||
|
||||
Udi是一个网约车平台,核心功能是将乘客的叫车订单发送给附近的网约车司机,司机接单后,到上车点接乘客并送往目的地,到达后,乘客支付订单。根据平台的分成比例,司机提取一部分金额作为收益,用例图如下:
|
||||
|
||||
|
||||
|
||||
Udi平台预计注册乘客5亿,日活用户5千万,平均每个乘客1.2个订单,日订单量6千万。平均客单价30元,平台每日总营收18亿元。平台和司机按3:7的比例进行分成,那么平台每天可赚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使用跳表存储GeoHash,Udi日活司机两千万,每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确认送达,订单进入“待支付”状态,等待用户支付订单金额。用户支付后,完成订单生命周期,订单状态为“已完成”。
|
||||
|
||||
订单状态模型可以帮助我们总览核心业务流程,在设计阶段,可以通过状态图发现业务流程不完备的地方,在开发阶段,可以帮助开发者确认流程实现是否有遗漏。
|
||||
|
||||
小结
|
||||
|
||||
在软件设计开发中,会涉及两类知识。一类是和具体业务无关的,比如编程语言、编程框架这些技术和具体业务无关,消息队列、分布式缓存这些技术也和具体业务无关。这一类技术更具有通用性,技术人员不管跳槽到哪家公司,几乎都会用到这些技术。
|
||||
|
||||
还有一类技术是和具体业务相关的,比如电商业务、金融业务、包括本文的网约车业务等等,这些业务如何用最合适的技术方案实现。这些和具体业务相关的技术经验主要适用于相关的业务领域。
|
||||
|
||||
技术人员在职业生涯的早期,需要更多地去关注和学习通用性的技术。而随着年龄增加,应该在业务相关的技术上获得更多沉淀,成为一个领域的专家,才能使自己在职场上获得更强的竞争力。
|
||||
|
||||
下一节我们将讨论如何使用领域驱动设计的技术方法解决业务上的问题,带你体会技术人员如何在业务上获得更多沉淀。
|
||||
|
||||
思考题
|
||||
|
||||
网约车在进行派单的时候,还需要考虑哪些因素,如何实现?
|
||||
|
||||
|
||||
|
||||
|
101
专栏/李智慧·高并发架构实战课/21网约车系统重构:如何用DDD重构网约车系统设计?.md
Normal file
101
专栏/李智慧·高并发架构实战课/21网约车系统重构:如何用DDD重构网约车系统设计?.md
Normal 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?
|
||||
|
||||
|
||||
|
||||
|
93
专栏/李智慧·高并发架构实战课/22大数据平台设计:如何用数据为用户创造价值?.md
Normal file
93
专栏/李智慧·高并发架构实战课/22大数据平台设计:如何用数据为用户创造价值?.md
Normal 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区域内,乘客方便等车,司机不违章的地点可能只有一两个,这一两个点又可能在任何地图上都没有标示。这就意味着,司机和乘客需要通过电话沟通很久才知道对方说的上车点在哪里,然后要么乘客徒步几百米走过来,要么司机绕一大圈去接,给司机和乘客都造成很多麻烦,平台也会因此流失很多用户。
|
||||
|
||||
对于这种问题,电子地图应用的厂商需要派测绘人员现场标注这些点。而对于网约车平台,由于不停在上传司机的位置信息,只需要根据乘客最后的上车点进行聚类分析,就会发现该区域大部分乘客最后都是在某个点上车,这个点就是最佳上车点。也就是说,只需要最初的一批乘客忍受麻烦,他们的行为数据就可以被网约车平台用于机器学习和数据挖掘,并被用于优化用户体验。
|
||||
|
||||
网约车平台像这样依赖大数据的地方还有很多。所以,网约车平台需要尽可能获取、存储用户和司机的各种行为与业务数据,并基于这些数据不断进行分析、挖掘,寻找潜在的商业机会和用户体验优化。对于一个数亿用户规模的网约车平台,这些数据的规模是非常庞大的,因此需要一个强大、灵活的大数据平台才能完成数据的存储与计算。
|
||||
|
||||
思考题
|
||||
|
||||
在你的工作中,是否有涉及到大数据和机器学习,它们带来了哪些价值?
|
||||
|
||||
|
||||
|
||||
|
59
专栏/李智慧·高并发架构实战课/结束语一个架构师的一天.md
Normal file
59
专栏/李智慧·高并发架构实战课/结束语一个架构师的一天.md
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 一个架构师的一天
|
||||
你好,我是李智慧。
|
||||
|
||||
时间过得真快,聊着聊着,这个专栏就到了尾声。恭喜你完成了这一阶段的学习,向架构师又迈进了一步!
|
||||
|
||||
在这里,我想问你一个问题:学完这个专栏,除了架构知识以外,你对“架构师”这个角色有了哪些新的认知呢?毕竟这个专栏的Slogan就是“附身”大厂架构师,我还是非常希望能为你代入架构师视角,体验这个角色的高光时刻。
|
||||
|
||||
我们这个专栏的案例,大多是一些庞大的系统,每个案例在真实世界中,都对应着一家数百上千亿美元市值的公司,需要数千名工程师开发,需要部署数万台服务器的系统。这样的系统不可能是由一个架构师设计出来的,正如开篇词所言,我们的设计是一种架空现实的设计。学习这种架空现实的设计,可以帮助你站在上帝视角,俯视一个个庞大系统的核心关键技术和整体架构,进而帮你构建起全局化的思维方式,而这正是架构师最重要的竞争力。
|
||||
|
||||
现实中,拥有一个庞大系统的公司通常有上百名的架构师,他们的日常工作也不是去设计整个系统,而是在自己负责的模块或者子系统内修修补补。所以在结束语里,相比于前面宏大的设计,我想做点“接地气”的内容,带你“穿越”成一位真正的架构师,感受架构师这个角色典型的一天。
|
||||
|
||||
我们以某大厂商家管理后台系统的架构师为例。商家管理后台是电商卖家的后台管理系统,卖家上下架商品、投放广告、查看运营数据等操作都在这里完成,是面向卖家的核心系统。商家管理后台开发部20多个人,分了4个开发小组。
|
||||
|
||||
|
||||
9:20 到公司,翻看昨天的邮件和聊天记录,看看有没有遗漏的事情。昨天下午数据库连接阻塞,连累应用服务器响应超时,焦头烂额搞了一下午,很多聊天消息都没来的及处理。翻了一下还好,没有错过什么重要的事情。
|
||||
|
||||
9:30 开部门小组长晨会,开了10分钟会,今天晨会时间控制得不错。
|
||||
|
||||
9:45 作为主讲人主持“商家管理后台架构重构设计”部门评审会,这次重构是商家后台最近两年最大的一次重构。已经和几个开发小组长还有核心开发人员已经讨论过几次了,这次做部门评审,事实上是做重构宣导,要让所有开发人员都统一认识,所以会上争议不多,开得比较顺利。另外,这次重构设计画了很多UML模型图,评审的时候也趁机给大家再普及了下设计建模的重要性和方法。
|
||||
|
||||
11:00 参加另一个部门的架构评审会,会上他们部门的架构师和一个开发组长吵起来了。晕,自己内部还没统一意见,就不要拉别的部门的人来看戏了。开会是用来统一意见和思想的,不是用来解决问题的,问题要提前解决。趁他们吵的间隙,上极客时间看看有什么有意思的课程,发现《李智慧 · 高并发架构实战课》挺有意思,订阅了。
|
||||
|
||||
12:00 午饭。
|
||||
|
||||
13:30 这次架构重构引入DDD(领域驱动设计)方法,上午的评审主要从DDD战略设计的角度对架构进行重构。DDD战术设计感觉不太好掌控,先暂时不在部门推广了。自己先写个DDD代码Demo练练吧,用哪个功能做Demo呢?红包管理功能吧。上半年公司架构师委员会例会上,也有几个部门的架构师提出要搞DDD,结果没了动静,这个螃蟹看来还得自己吃。
|
||||
|
||||
15:11 一个开发同学过来讨论技术问题,聊了十来分钟,越聊越觉得不对劲,把产品经理也叫过来讨论,确定这个需求是有问题的,先暂时不开发,产品经理回去重新梳理需求。继续写Demo代码。
|
||||
|
||||
16:03 收到运维部门的会议邀请,明天上午10:30复盘昨天的数据库访问故障。麻烦,这个故障看起来是数据库失去响应,其实是个程序Bug,线程阻塞导致数据库连接耗尽。这个故障影响不小,责任主要在我们部门,看看会上怎么说,能不能让运维部门也承担一点故障分,毕竟他们数据库管理也没做到位。也不知道明天参加会议的运维是哪个,好不好说话?继续写Demo代码。
|
||||
|
||||
16:42 收到监控报警通知,商品上架数异常波动,低于正常值60%。是商品上架服务出问题了?赶紧打开监控系统,上架服务系统指标正常;打开日志系统,异常日志数正常。什么情况啊?问问运维。哦,原来是监控数据消息队列消费服务出了点问题,数据统计有误,触发报警,虚惊一场。继续写Demo代码。
|
||||
|
||||
17:08 公司负责培训的同事发来消息,问能不能做个性能优化方面的内部讲座,面向全公司的开发和测试人员。可以呀,什么时候讲?下周五。好的。时间有点紧啊,不做Demo了,先做讲座PPT,毕竟公司级的讲座,难得的提升技术影响力的机会。不过,性能优化这个话题有点大啊,从哪方面入手呢?对了,上午看到的李智慧的专栏里面有一篇性能优化的文章,看看他怎么讲。可以,就按这个文章的思路准备讲座大纲。离下班还有一个多小时,做PPT来不及了,先收集下PPT素材,案例部分用公司内部的,理论部分Google一下,不错,就这样……
|
||||
|
||||
|
||||
上面就是这位架构师一天的经历,你认为他的工作怎么样呢?大部分人都希望工作轻松一点,你觉得文中这位架构师的工作是否轻松?以及什么样的工作是轻松的呢?
|
||||
|
||||
首先,清闲的工作未必是轻松的,工作不忙、无所事事,会让人觉得自己失去价值,进而产生焦虑,最后并不轻松。轻松的工作应该是自己做的事情有意义、有价值,还能在工作中不断获得进步;同时工作又游刃有余,自己可以掌控工作,而不是被工作驱赶着疲于奔命。
|
||||
|
||||
架构师最核心的事情是要控制技术局面,让事情有节奏地推进,不要鸡毛蒜皮,做决策的人越是忙忙碌碌,团队效率越低。文中这位架构师做到了这一点,他基本上掌控了他的工作,而不是让工作push他。大部分时间,他可以按照自己的节奏安排工作;突发的情况,他也能比较从容地应对。
|
||||
|
||||
他做事情看起来似乎毫不费力。事实上,他要处理的很多事情,都是一些既复杂又困难的事情,是别人搞不定了才到了他这里来的。他既要考虑各种人际关系,又要用技术做出判断,还要对判断结果负责,而且要有威望让别人听他的。此外,他还有时间学习、有时间写代码、有时间做设计、有时间扩展自己的影响力,还能按时下班。
|
||||
|
||||
其实,做事情要想看起来毫不费力,必须要在看不到的地方费很大力气,进行很多的积累。你也可以问下自己,自己现在每天都做些什么?多长时间用来学习,有没有帮助别人解决问题,有没有考虑积极做些分享扩展自己的影响力,还是只是低下头盯着自己脚下的这块巴掌大的地方呢?
|
||||
|
||||
当然,在工作中能达到这位架构师这样的境界并不容易,也不是成为架构师就可以这样工作。但是只要你想往上走,就需要不断学习,有目的地在工作中提升自己,并且主动找机会展示自己的综合能力,构建自己的影响力。相信最终你一定可以掌控自己的工作,还有人生。
|
||||
|
||||
希望这个专栏可以在你进步的路上提供帮助,使你对更宏观的知识和实践建立起感性的认知和清晰的目标,然后不断夯实自己的实践能力、拓宽自己的影响力,最后成为你想成为的自己。
|
||||
|
||||
](https://jinshuju.net/f/x0MpWT)
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user