分布式系统常用技术及案例分析(第2版)(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-30 14:12:49

点击下载

作者:柳伟卫

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

分布式系统常用技术及案例分析(第2版)

分布式系统常用技术及案例分析(第2版)试读:

前言

写作背景

我一直想写一本关于分布式系统的书。一方面想把个人工作中涉及的分布式技术做一下总结,另一方面想把个人多年的经验分享给广大的读者朋友。由于我的开发工作大都以Java为主,所以一开始设想的主题是“分布式 Java”,书也以开源方式发布在互联网上(网址为https://github.com/waylau/distributed-java)。

后来,陈晓猛编辑看到了这本开源书,以及我关于分布式系统方面的博文,问我是否有兴趣出版分布式相关题材的图书。当然,书的内容不仅仅是“分布式Java”。

对于出书一事,我犹豫良久。首先,本身工作挺忙,实在无暇顾及其他;其次,虽然我之前写过超过一打的书籍(https://waylau.com/books/),但多是开源电子书,时间、内容方面自然不会有太多约束,几乎是“想写就写,没有时间就不写”,这个跟正式出版还是存在比较大的差异的;最后,这本书涉及面相对较广,需要查阅大量资料,实在是太耗费精力。

但陈晓猛编辑还是鼓励我去做这个事情。思索再三,最终我答应了。当然,最后这本书还是在规定时间内完成了。它几乎耗尽了我写作期间所有的业余和休息时间。“不积跬步,无以至千里;不积小流,无以成江海。”虽然整本书从构思到编写完成的时间不足一年,但书中的大部分知识点,都是我在多年的学习、工作中积累下来的。之所以能够实现快速写作,一方面是做了比较严格的时间管理,另一方面得益于我多年坚持写博客和开源书的习惯。

内容介绍

本书为两部分,即分布式系统基础理论和分布式系统常用技术。第一部分为第1章和第2章,主要介绍分布式系统基础理论知识,总结一些在设计分布式系统时需要考虑的范式、知识点及可能会面临的问题。第二部分为第3章到第8章,主要列举了在分布式系统应用中的一些主流技术,并介绍这些技术的作用和用法。

第1章 介绍分布式系统基础理论知识,总结一些在设计分布式系统时需要考虑的范式、知识点及可能会面临的问题,包括线程、通信、一致性、容错性、CAP理论、安全性和并发等相关内容。

第2章 详细介绍分布式系统的架构体系,包括传统的基于对象的体系结构、SOA。

第3章 介绍常用的分布式消息服务框架,包括Apache ActiveMQ、Apache RabbitMQ、Apache RocketMQ、Apache Kafka等。

第4章 介绍分布式计算理论和应用框架方面的内容,包括MapReduce、Apache Hadoop、Apache Spark、Apache Mesos 等。

第5章 介绍分布式存储理论和应用框架方面的内容,包括Bigtable、Apache HBase、Apache Cassandra、Memcached、Redis、MongoDB等。

第6章 介绍分布式监控方面常用的技术,包括Nagios、Zabbix、Consul、ZooKeeper等。

第7章 介绍常用的分布式版本控制工具,包括Bazaar、Mercurial、Git等。

第8章 介绍RESTful API、微服务及容器相关的技术,着重介绍Jersey、Spring Boot、Docker等技术的应用。

源代码

本书提供源代码下载,下载地址为https://github.com/waylau/distributed-systems-technologiesand-cases-analysis。

勘误和交流

本书如有勘误,会在 https://github.com/waylau/distributed-systems-technologies-and-casesanalysis上发布。由于笔者能力有限,时间仓促,书中难免有错漏,欢迎读者批评指正。读者也可以到博文视点官网的本书页面进行交流(www.broadview.com.cn/00000)。

您也可以直接联系我:

博客:https://waylau.com

邮箱:waylau521@gmail.com

微博:http://weibo.com/waylau521

GitHub:https://github.com/waylau

致谢

首先,感谢电子工业出版社博文视点公司的陈晓猛编辑,是您鼓励我将本书付诸成册,并在我写作过程中审阅了大量稿件,给予我很多指导和帮助。感谢工作在幕后的电子工业出版社评审团队对于本书在校对、排版、审核、封面设计、错误改进方面所给予的帮助,使本书得以顺利出版发行。

其次,感谢在我十几年求学生涯中教育过我的所有老师,是你们将知识和学习方法传递给了我。感谢我曾经工作过的公司和单位,感谢和我一起共事过的同事和战友,你们的优秀一直是我追逐的目标,你们所给予的压力正是我不断改进的动力。

感谢我的父母、妻子Funny和两个女儿。由于撰写本书,我牺牲了很多陪伴家人的时间。感谢你们对于我工作的理解和支持。

最后,特别要感谢这个时代,互联网让所有人可以公平地享受这个时代的成果。感谢那些为计算机、互联网做出贡献的先驱,是你们让我可以站在更高的“肩膀”上!感谢那些为本书提供灵感的佳作,包括《分布式系统原理与范式》、UNIX Network Programming、Enterprise SOA、MapReduce Design Patterns、Hadoop: The Definitive Guide Learning Hbase、Advanced Analytics with Spark、Pro Git、Docker in Action、《淘宝技术这十年》、Hatching Twitter,等等,详细的书单可以参阅本书在线资源中的“参考文献”部分。柳伟卫再版序

时光荏苒,岁月匆匆,距离《分布式系统常用技术及案例分析》第1版出版已经一载有余。热心的读者对于本书也投以了极大的关注,提了很多中肯的建议。对于这些建议,不管褒贬,一并全收,于是才有了第2版的出版。

对于技术型书籍的创作,笔者倾向于采用当今软件开发主流的方式——敏捷。敏捷写作打通了编写、校稿、出版、发行的整个流程,让知识可以在第一时间呈现给读者。读者在阅读本书之后,也可以及时对书中的内容进行反馈,从而帮助作者完善书中内容,最终形成良好的反馈闭环。第2版所更新的内容,希望正是读者所期待的。

第2版修改篇幅较大,修改内容大致包括以下几个方面:(1)删除软件安装等比较简单的内容。(2)每章的开头新增“概述”,让各个章节的技术点可以关联起来。(3)每章增加“实战”案例,让技术点更具可操作性。(4)修改第1版中的措辞、插图。

完整的修改内容,读者可以扫描封面上的二维码,参阅本书的在线文档“本书第1版与第2版的差异对比”。柳伟卫2018年5月22日于深圳第1章 分布式系统基础知识1.1 概述

毫无疑问,计算机改变了人类的工作和生活方式,而计算机系统也正在进行一场变革。无论是手机应用,还是智能终端,都离不开背后那个神秘的巨人——分布式系统。正是那些看不见的分布式系统,每天处理着数以亿计的计算,提供可靠而稳定的服务。

本章就揭开分布式系统的神秘面纱。1.1.1 什么是分布式系统《分布式系统原理与范型》一书中是这样定义分布式系统的:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个系统。”

这里面包含了两个要点:

硬件独立;

软件统一。

什么是硬件独立?所谓硬件独立,是指机器本身是独立的。一个大型的分布式系统,由若干计算机组成系统的基础设施。而软件统一,是指对于用户来说,用户就像跟单个系统打交道。就好比我们每天上网看视频,视频网站对我们来说就是一个系统软件,它们背后是如何运作的、部署了几台服务器、每台服务器是干什么的,这些对用户来说是不可见的。用户不关心背后的这些服务器,用户所关心的是,今天能看什么样的节目、视频运行是否流畅、清晰度如何等。

软件统一的另外一个方面是指,分布式系统的扩展和升级都比较容易。分布式系统中的某些节点发生故障,不会影响整体系统的可用性。用户和应用程序交互时,不会察觉哪些部分正在替换或维修,也不会感知新加入的部分。1.1.2 集中式系统与分布式系统

集中式系统主要部署在HP、IBM、Sun等小型机以上档次的服务器中,把所有的功能都集成到主服务器上(这对服务器的要求很高)。它们的主要特色在于年宕机时间只有几小时,所以又统称为z系列(zero,零)。AS/400主要应用在银行和制造业,还用于Domino,主要的技术包括 TIMI(技术独立机器界面)和单级存储。有了 TIMI,可以实现硬件与软件相互独立。RS/6000比较常见,用于科学计算和事务处理等。这类主机的单机性能一般都很强,带多个终端。终端没有数据处理能力,运算全部在主机上进行。现在的银行系统大部分都是集中式系统。此外,这种系统在大型企业、科研单位、军队和政府等也有应用。

集中式系统主要流行于20世纪。现在还在使用集中式系统的很大一部分原因是沿用原来的软件,而这些软件往往很昂贵。集中式系统的优点是便于维护、操作简单。但这样的系统也有缺陷,不出问题还好,一出问题,就会造成单点故障,所有功能就都不能正常工作了,所谓“一荣俱荣,一损俱损”。由于集中式系统的相关技术只被少数厂商所掌握,个人要对这些系统进行扩展和升级往往比较麻烦,一般的企业级应用很少会用到集中式系统。图1-1是一个典型的集中式系统的示意图。图1-1 集中式系统示意图

分布式系统恰恰相反。分布式系统通过中间件对现有计算机的硬件能力和相应的软件功能进行重新配置和整合。它是一种多处理器的计算机系统,各处理器通过网络构成统一的系统。系统采用分布式计算结构,即把原来系统内中央处理器处理的任务分散交给相应的处理器,实现不同功能的多个处理器,这些处理器的相互协调,共享系统的外设与软件。这样就加快了系统的处理速度,简化了主机的逻辑结构。它甚至不需要很高的配置,一些“退休”的低配置机器也能被重新纳入分布式系统中,这样无疑降低了硬件成本且易于维护。同时,分布式系统往往由多台主机组成,任何一台主机宕机都不影响整个系统的使用,所以分布式系统的可用性比较高。图1-2是一个典型的分布式系统的示意图。图1-2 分布式系统示意图

正由于分布式系统的这些优点,使得分布式系统的应用越来越广泛。可以说,这也代表了未来应用的发展趋势。1.1.3 如何设计分布式系统

设计分布式系统的本质就是“合理地将一个系统拆分成多个子系统并部署到不同的机器上”,所以首先要考虑的问题是如何合理地将系统进行拆分。由于拆分后的各个子系统不可能孤立地存在,必然要通过网络进行连接交互,它们之间如何通信变得尤为重要。当然,在通信过程中要识别“敌我”,防止信息在传递过程中被拦截和篡改(这就涉及安全问题了)。分布式系统如果要适应不断增长的业务需求,就需要考虑扩展性。分布式系统还必须保证可靠性和数据的一致性。

概括起来,在设计分布式系统时,应考虑以下问题:

如何将系统拆分为子系统?

如何规划子系统间的通信?

如何考虑通信过程中的安全?

如何让子系统可以扩展?

如何保证子系统的可靠性?

如何实现数据的一致性?

本书的大部分内容都将针对分布式系统中常见的问题进行探讨。1.1.4 分布式系统所面临的挑战

分布式系统是难以理解、设计、构建和管理的,它们将比单个机器多数倍的变量引入设计中,使得应用程序的问题根源更难被发现。SLA(Service-Level Agreement,服务水平协议)是衡量停机和/或性能下降的标准。大多数现代应用程序都有一个期望的弹性SLA,通常按“9”的数量增加(如每月99.9%或99.99%可用性)。

设计分布式系统的挑战如下。

异构性:由于分布式系统基于不同的网络、操作系统、计算机硬件和编程语言构造,必须有一种通用的网络通信协议来屏蔽异构系统之间的差异。一般由中间件来处理这些差异。

缺乏全球时钟:在程序需要协作时,通过交换消息来协调它们的动作。紧密的协调经常依赖于对程序动作发生时间的共识。但是,实际网络上计算机同步时钟的准确性受到了极大的限制,即没有一个正确时间的全局概念。这是通过网络发送消息作为唯一的通信方式这一事实带来的直接结果。

一致性:数据被分散或复制到不同的机器上,如何保证各台主机之间的数据的一致性是一个难点。

故障的独立性:任何计算机都有可能发生故障,且故障不尽相同。故障出现的时间也是相互独立的。一般分布式系统允许出现部分故障而不影响整个系统的正常使用。

并发:使用分布式系统的目的是更好地共享资源,所以系统中的每个资源都必须被设计成在并发环境中是安全的。

透明性:分布式系统中任何组件的故障,或者主机的升级、迁移,对于用户来说都是不可见的。

开放性:分布式系统由不同的程序员来编写不同的组件,组件最终要集成为一个系统,所以组件所发布的接口必须遵守一定的规范且能够被互相理解。

安全性:加密用于给共享资源提供适当的保护,在网络上传递的所有敏感信息都需要进行加密。拒绝服务攻击仍然是一个有待解决的问题。

可扩展性:系统要设计成随着业务量的增加而相应地扩展,以提供对应的服务。1.2 线程

在早期的计算机操作系统中,拥有资源且独立运行的基本单位是进程。随着计算机技术的发展,进程出现了很多弊端。例如,由于进程是资源拥有者,创建、撤销与切换存在较大的时空开销,需要引入轻量型进程;由于对称多处理机(Symmetric Multi-Processor,SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。

因此,在20世纪80年代,出现了能独立运行的基本单位——线程(Thread)。1.2.1 什么是线程

线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程只拥有少量在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间相互制约,线程在运行中呈现出间断性。

线程有三种基本状态:就绪、阻塞和运行。线程的状态图如图1-3所示。图1-3 线程的状态图

就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机,正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是程序中一个单一的顺序控制流程,是进程内一个相对独立的、可调度的执行单元。在单个程序中同时运行多个线程完成不同的工作,称为多线程。在大多数情况下,多线程能提升程序的性能。1.2.2 进程和线程

进程和线程是并发编程的两个基本的执行单元。在大多数编程语言中,并发编程主要涉及线程。

一个计算机系统通常有许多活动的进程和线程。在给定的时间内,每个处理器中只能有一个线程得到真正的运行。对于单核处理器来说,处理时间是通过时间切片在进程和线程之间进行共享的。

进程有一个独立的执行环境。进程通常有一个完整的、私人的基本运行时资源。每个进程都有自己的内存空间。操作系统的进程表(Process Table)存储了CPU寄存器值、内存映像、打开的文件、统计信息和特权信息等。进程一般定义为执行中的程序,也就是当前操作系统的某个虚拟处理器上运行的一个程序。多个进程并发共享同一个CPU和其他硬件资源,操作系统支持进程之间的隔离。这种并发透明性需要付出相对较高的代价。

进程往往被等同为程序或应用程序。然而,用户看到的一个单独的应用程序可能实际上是一组合作的进程。大多数操作系统都支持进程间通信(Inter Process Communication,IPC),如管道和socket。IPC不仅用于同一个系统的进程之间的通信,也用于不同系统的进程之间的通信。

线程,有时被称为轻量级进程(Lightweight Process,LWP)。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个新的进程需要更少的资源。线程系统一般只维护用来让多个线程共享CPU所必需的最少量信息,特别是线程上下文(Thread Context)中一般只包含CPU上下文及某些其他线程管理信息。通常忽略那些对于多线程管理而言不必要的信息。这样,单个进程中防止数据遭到某些线程不合法的访问的任务就完全落在了应用程序开发人员的肩上。线程不像进程那样彼此隔离,以及受到操作系统的自动保护,所以在多线程程序开发过程中需要开发人员做更多的努力。

线程存在于进程中,每个进程至少有一个线程。线程共享进程的资源,包括内存和打开的文件,尽管这使得工作变得高效,但也存在一个潜在的问题——通信。关于通信的内容,会在后面的章节中讲述。

现在多核处理器或多进程的计算机系统越来越流行,这大大增强了系统的进程和线程的并发执行能力。即便在没有多处理器或多进程的系统中,并发仍然是可能的。关于并发的内容,会在后面章节中讲述。1.2.3 线程和纤程

为了提高并发量,某些编程语言中提供了“纤程”(Fiber)的概念,比如Golang的goroutine、Erlang风格的actor。Java 语言虽然没有定义纤程,但仍有一些第三方库供选择,比如Quasar。纤程可以理解为比线程颗粒度更细的并发单元。

纤程是以用户方式代码来实现的,并不受操作系统内核管理,所以内核并不知道纤程,也就无法对纤程实现调度。纤程是根据用户定义的算法来调度的。因此,就内核而言,纤程采用了非抢占式调度方式,而线程是抢占式调度的。

一个线程可以包含一个或多个纤程。线程每次执行哪一个纤程的代码,是由用户来决定的。

所以,对于开发人员来说,使用纤程可以获得更大的并发量,但同时要面临自己实现调度纤程的复杂度。1.2.4 编程语言中的线程对象

在面向对象语言开发中,每个线程都与Thread类的一个实例相关联。由于Java语言较流行,下面将用Java来实现并使用线程对象,作为并发应用程序的基本原型。

1. 定义和启动一个线程

Java中有两种创建Thread实例的方式。

提供Runnable对象。Runnable接口定义了一个方法run,用来包含线程要执行的代码。HelloRunnable示例如下。

继承 Thread 类。Thread 类本身是 Runnable 的实现,只是它的 run 方法什么都没干。HelloThread示例如下。

请注意,这两个例子调用start来启动线程。

第一种方式使用Runnable对象,在实际应用中更普遍,因为Runnable对象可以继承Thread以外的类。第二种方式在简单的应用程序中更容易使用,但受理的任务类必须是一个Thread类的后代。本书推荐使用第一种方式,将 Runnable 任务从 Thread 对象中分离来执行任务。这样不仅更灵活,而且适用于高级线程管理API。

Thread类还定义了大量的方法用于线程管理。

2. 使用sleep来暂停执行

Thread.sleep可以让当前线程执行暂停一个时间段,这样处理器的时间就可以给其他线程使用。

sleep有两种重载形式:一种是指定睡眠时间为毫秒级,另一种是指定睡眠时间为纳秒级。然而,这些睡眠时间不能保证是精确的,因为它们是由操作系统提供的并受其限制。此外,睡眠周期也可以通过中断来终止,我们将在后面的章节中看到。

SleepMessages示例——使用sleep每隔4秒打印一次消息:

请注意,main声明抛出InterruptedException。如果sleep是激活的,若有另一个线程中断当前线程,则sleep抛出异常。因为该应用程序还没有定义另一个线程来引起中断,所以考虑捕捉InterruptedException。

3. 中断(interrupt)

中断表明一个线程应该停止它正在做和将要做的事。线程通过Thread对象调用interrupt实现中断。为了使中断机制正常工作,被中断的线程必须支持自己的中断。

支持中断

如何实现线程支持自己的中断?这要看它目前正在做什么。如果线程调用方法频繁抛出InterruptedException异常,那么它只要在run方法捕获异常之后返回即可。例如:

很多方法都会抛出InterruptedException,如sleep被设计成在收到中断时立即取消它们当前的操作并返回。

若线程长时间没有调用方法抛出InterruptedException,那么它必须定期调用Thread.interrupted,该方法在收到中断后返回true。

在这个简单的例子中,代码简单地测试该中断,如果已接收中断线程就退出。在更复杂的应用程序中,它可能会更有意义地抛出一个InterruptedException:

中断状态标志

中断机制是使用被称为中断状态的内部标志实现的。调用Thread.interrupt可以设置该标志。当一个线程通过调用静态方法 Thread.interrupted 来检查中断时,中断状态被清除。非静态isInterrupted方法用于线程查询另一个线程的中断状态,而不会改变中断状态标志。

按照惯例,任何方法因抛出一个InterruptedException而退出都会清除中断状态。当然,它可能因为另一个线程调用interrupt而让那个中断状态立即被重新设置。

4.join方法

join方法允许一个线程等待另一个线程完成。假设t是一个正在执行的Thread对象,那么

t.join();

会导致当前线程暂停执行直到t线程终止。join方法允许程序员指定一个等待周期,与sleep一样,等待时间依赖于操作系统的时间,同时不能假设join方法的等待时间是精确的。

像sleep一样,join方法通过InterruptedException退出来响应中断。1.2.5 SimpleThreads示例

SimpleThreads示例有两个线程。第一个线程是每个Java应用程序都有的主线程。主线程创建Runnable对象的MessageLoop,并等待它完成。如果MessageLoop需要很长时间才能完成,主线程就中断它。

该MessageLoop线程打印出一系列消息。如果中断之前就已经打印了所有消息,则MessageLoop线程打印一条消息并退出。

对 Java 的 Thread 类感兴趣的读者可以查看其在线 API:https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html。

上面例子的代码可以在 https://github.com/waylau/distributed-systems-technologies-and-cases-analysis的distributed-systems-java-demos程序的com.waylau.essentialjava.thread包下找到。1.3 通信

进程间的通信是一切分布式系统的核心。如果没有通信机制,分布式系统的各个子系统将是“一盘散沙”,毫无作用。本节介绍常用的通信方式。1.3.1 网络I/O模型的演进

1. 同步和异步

同步和异步描述的是用户线程与内核的交互方式:

同步是指用户线程发起I/O请求后需要等待,或者轮询内核I/O操作完成后才能继续执行;

异步是指用户线程发起I/O请求后仍继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

2. 阻塞和非阻塞

阻塞和非阻塞描述的是用户线程调用内核I/O操作的方式:

阻塞是指I/O操作需要彻底完成后才返回用户空间;

非阻塞是指I/O操作被调用后立即返回给用户一个状态值,无须等到I/O操作彻底完成。

一个I/O操作其实分成了两个步骤:发起I/O请求和实际的I/O操作。

阻塞I/O和非阻塞I/O的区别在于第一步,也就是发起I/O请求是否会被阻塞。如果阻塞直到完成,就是传统的阻塞I/O,如果不阻塞,就是非阻塞I/O。

同步I/O和异步I/O的区别在于第二个步骤是否阻塞,如果实际的I/O读写阻塞请求进程,就是同步I/O。

3.UNIX I/O模型

UNIX下共有5种I/O模型:

阻塞I/O;

非阻塞I/O;

I/O复用(select和poll);

信号驱动I/O(SIGIO);

异步I/O(Posix.1的aio_系列函数)。

注:读者若想深入了解UNIX的网络知识,推荐阅读W.Richard Stevens的UNIX Network Programming,Volume 1,Second Edition: Networking APIs: Sockets and XTI。本节只简单介绍了这5种模型,文中的图例也引用自该书。

阻塞I/O模型

请求无法立即完成则保持阻塞。

阶段1:等待数据就绪。网络I/O的情况就是等待远端数据陆续抵达;磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。

阶段2:数据复制。出于系统安全考虑,用户态的程序没有权限直接读取内核态内存,因此内核负责把内核态内存中的数据复制一份到用户态内存中。

阻塞I/O模型如图1-4所示。

本节中将recvfrom函数视为系统调用。一般recvfrom函数的实现都有一个从应用程序进程中运行到内核中运行的切换,一段时间后再跟一个返回应用进程的切换。

在图1-4中,进程阻塞的整段时间是指从调用recvfrom函数开始到它返回的这段时间,当进程返回成功提示时,应用进程开始处理数据报。图1-4 阻塞I/O模型

非阻塞I/O模型

socket设置为NONBLOCK(非阻塞)就是告诉内核,当所请求的I/O操作无法完成时,不要让进程进入睡眠状态,而是立刻返回一个错误码(EWOULDBLOCK),这样请求就不会阻塞;

I/O操作函数将不断地测试数据是否已经准备好,如果没有准备好,则继续测试,直到数据准备好为止。在整个I/O请求的过程中,虽然用户线程每次发起I/O请求后可以立即返回,但是为了等到数据,仍需轮询、重复请求,而这是对CPU时间的极大浪费。

数据准备好了,从内核复制到用户空间。

非阻塞I/O模型如图1-5所示。图1-5 非阻塞I/O模型

一般很少直接使用这种模型,而是在其他 I/O 模型中使用非阻塞 I/O 这一特性。这种方式对单个I/O请求的意义不大,但给I/O复用铺平了道路。

I/O复用模型

I/O复用会用到select或poll函数,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正的 I/O 系统调用。函数也会使进程阻塞,但和阻塞 I/O 不同的是,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作、多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

I/O复用模型如图1-6所示。图1-6 I/O复用模型

从流程上看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select函数最大的优势是用户可以在一个线程内同时处理多个socket的I/O 请求。用户可以注册多个 socket,然后不断地调用select来读取被激活的socket,达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

I/O复用模型使用Reactor设计模式实现了这一机制。

调用select或poll函数的方法由一个用户态线程负责轮询多个socket,直到阶段1的数据就绪,再通知实际的用户线程执行阶段2的复制操作。通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现阶段1的异步化。

信号驱动I/O(SIGIO)模型

首先,我们允许socket进行信号驱动I/O,并通过调用sigaction来安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好后,进程会收到一个SIGIO信号,可以在信号处理函数中调用recvfrom来读取数据报,并通知主循环数据已准备好被处理,也可以通知主循环,让它来读取数据报。

信号驱动I/O(SIGIO)模型如图1-7所示。图1-7 信号驱动I/O(SIGIO)模型

异步I/O模型

异步I/O是POSIX规范定义的。通常,这些函数会通知内核来启动操作并在整个操作(包括从内核复制数据到我们的缓存中)完成时通知我们。

该模型与信号驱动I/O(SIGIO)模型的不同点在于,信号驱动I/O(SIGIO)模型告诉我们I/O操作何时可以启动,而异步I/O模型告诉我们I/O操作何时完成。

调用aio_read函数,告诉内核传递描述字、缓存区指针、缓存区大小和文件偏移,然后立即返回,我们的进程不阻塞于等待 I/O 操作的完成。当内核将数据复制到缓存区后,才会生成一个信号来通知应用程序。

异步I/O模型如图1-8所示。图1-8 异步I/O模型[1]

异步I/O模型使用Proactor设计模式实现了这一机制。

异步I/O模型告知内核:当整个过程(包括阶段1和阶段2)全部完成时,通知应用程序来读数据。

几种I/O模型的比较

前4种模型的区别是阶段1不相同,阶段2基本相同,都是将数据从内核复制到调用者的缓存区。而异步I/O的两个阶段都不同于前4个模型。几种I/O模型的比较如图1-9所示。图1-9 几种I/O模型的比较

同步I/O操作引起请求进程阻塞,直到I/O操作完成。异步I/O操作不引起请求进程阻塞。上面前4个模型——阻塞I/O模型、非阻塞I/O模型、I/O复用模型和信号驱动I/O模型都是同步I/O模型,而异步I/O模型才是真正的异步I/O。

4. 常见Java I/O模型

在了解了UNIX的I/O模型之后,就能明白其实Java的I/O模型也是类似的。“阻塞I/O”模型

下面的EchoServer是一个简单的阻塞I/O例子,服务器启动后,等待客户端连接。在客户端连接服务器后,服务器就阻塞读写数据流。

EchoServer代码:

改进为“阻塞I/O+多线程”模型

使用多线程来支持多个客户端访问服务器。

主线程MultiThreadEchoServer.java:

处理器类EchoServerHandler.java:

存在问题:每次收到新的连接都要新建一个线程,处理完后销毁线程,代价大。当有大量的短连接出现时,性能比较低。

改进为“阻塞I/O+线程池”模型

针对上面多线程的模型中出现的线程重复创建、销毁带来的开销问题,可以采用线程池来优化。每次收到新连接后从池中取一个空闲线程进行处理,处理完后再放回池中,重用线程避免了频繁地创建和销毁线程带来的开销。

主线程ThreadPoolEchoServer.java:

存在问题:在大量短连接的场景中性能会提升,因为不用每次都创建和销毁线程,而是重用连接池中的线程。但在大量长连接的场景中,因为线程被连接长期占用,不需要频繁地创建和销毁线程,所以没有什么优势。

虽然这种方法适用于小到中等规模的客户端的并发数,但是如果连接数超过100000,那么性能将很不理想。

改进为“非阻塞I/O”模型“阻塞I/O+线程池”模型虽然比“阻塞I/O+多线程”模型在性能方面有所提升,但这两种模型存在一个共同的问题:读和写操作都是同步阻塞的,面对高并发(持续大量连接同时请求)的场景,需要消耗大量的线程来维持连接。CPU在大量的线程之间频繁切换,性能损耗很大。一旦单机的连接数超过1万,甚至达到几万,服务器的性能会急剧下降。

而NIO的Selector却很好地解决了这个问题,用主线程(一个线程或CPU个数的线程)保持所有的连接,管理和读取客户端连接的数据,将读取的数据交给后面的线程池处理,线程池处理完业务逻辑后,将结果交给主线程发送响应给客户端,少量的线程就可以处理大量连接的请求。

Java NIO由以下几个核心部分组成:

Channel;

Buffer;

Selector。

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,比如新连接进来、数据接收等事件。

主线程NonBlokingEchoServer.java:

改进为“异步I/O”模式

Java SE 7之后的版本,引入了对异步I/O(NIO.2)的支持,为构建高性能的网络应用提供了一个利器。

主线程AsyncEchoServer.java:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载