《大型网站系统与Java中间件实践》读书笔记(未完成)

分布式概览

一. 分布式系统

1.1 什么是分布式系统?

分布式系统是一种组件分布在网络计算机上组件之间仅仅通过消息传递来通信并协调行动的系统。

对于用户来说,他面对的只是一台服务器,但实际上这台服务器是由众多服务器组成的“一台超级计算机”。所以我们可以联想到我们日常使用的各种网站,它们背后都是一整个大型分布式系统在提供服务。

1.2 分布式系统的意义?

为什么要从单一应用架构或垂直应用架构升级到分布式系统?

  • 升级单机处理能力的性价比越来越低
  • 单机处理能力有瓶颈
  • 分布式系统带来稳定性和可用性
  • 单一应用不利于扩展和升级维护
  • 单一应用拆分后,应用间的交互越来越复杂

1.3 基础知识补齐

1.3.1 组成计算机的5要素

冯 · 诺依曼计算机的5个组成部分:

  • 运算器
  • 控制器
  • 存储器
    • 内存
    • 外存:断电后仍能保存数据。
  • 输入设备
  • 输出设备

既然分布式系统看起来像一个超级计算机,是不是也有类似的结构呢?

1.3.2 线程与进程的执行模式

我们学习编程都是从单线程模式开始的,面对的都是程序的顺序、分支和循环执行。多线程(指单进程内的多线程)要处理线程间通信,要对线程并发做控制,做好线程间的协调工作

在多核CPU的时代,程序的并发并行很重要,决定着是否能够有效利用多核带来的性能提升。

多线程的几种交互模式:

  • 互不通信的多线程模式:最简单的多线程模式,线程间无共享数据,也不需要做动作协调,就是多个独立的线程各自完成自己的任务。
  • 基于共享容器的多线程模式:多线程具有共享数据,如经典的生产者消费者问题。我们需要保证数据访问的正确性,对于存储数据的容器有线程安全或不安全之分,线程不安全的容器一般可以通过加锁(数据读写比例很高,采用读写锁而非互斥锁)或写时复制CopyOnWrite的方式来控制。
  • 通过事件协同的多线程模式:除了并发访问的控制,线程间还会有协调的需求。比如A、B两个线程,B线程需要等待某个状态或事件发生后才能继续工作,而这个改变和A线程有关,这时就需要完成线程间的协调(等待通知机制),这种情况下需要注意避免死锁。

多进程模式:线程属于进程,所以一个进程内的多个线程共享了进程的内存空间,而进程之间的内存空间是独立的。所以多个进程间通过内存共享、交换数据的方式与多线程有所不同。

  • 单机多进程
  • 多机多进程:单机OS所支持的功能需要另外实现,单个机器故障处理的好就不影响集群。

1.3.3 网络通信

OSI与TCP/IP网络模型

OSI七层模型:

  • 应用层
  • 表示层 :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。
  • 会话层 :建立及管理会话。
  • 运输层
  • 网路层
  • 数据链路层
  • 物理层

五层协议:

  • 应用层 :为特定应用程序提供数据传输服务,例如 HTTPDNS 等协议。数据单位为报文。
  • 传输层 :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:
    • 传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;
    • 用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。
    • TCP 主要提供完整性服务,UDP 主要提供及时性服务。
  • 网络层 :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。
  • 数据链路层 :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。
  • 物理层 :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。

五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。

TCP/IP:

  • 它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
  • TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。

网络IO的实现方式:使用Socket套接字进行网络通信开发时,会使用哪些实现方式。

  • BIO:Blocking IO,阻塞式实现。一个套接字需要使用一个线程来处理,建立连接、读数据、写数据都可能会阻塞。

  • NIO:Nonblocking IO,非阻塞式实现,基于事件驱动思想,采用 Reactor 模式。相比于BIO,NIO不用为每个套接字分配一个线程,而是一个线程处理多个套接字的相关工作。

  • AIO:AsynchronousIO,异步IO,采用 Proactor 模式

    • 与NIO的差异是,AIO在进行读写操作时,只需调用相应的 read/write 方法,并且传入 CompletionHandler(动作完成的处理器);在动作完成后,会调用CompletionHandler。
    • NIO的通知发生在动作之前,是在可写、可读的时候,Selector发现这些事件后调用 Handler 处理。
    • NIO在有通知时可以进行相关操作,AIO在有通知时表示相关操作已完成。

1.4 如何把应用从单机扩展到分布式

分布式这种“超级计算机”的五个组成部分的变化:

  • 输入设备:
    • 互相连接的多个节点,某个节点接受其他节点信息时,可以看作输入设备。
    • 传统人机交互的输入设备。
  • 输出设备:
    • 互相连接的多个节点,某个节点向其他节点传递信息时,可以看作输出设备。
    • 传统人机交互的输出设备,如屏幕。
  • 控制器:
    • 单机的控制器就是CPU中的控制器。
    • 分布式中控制器负责协调或控制节点之间的动作和行为。如远程服务调用的场景,有几种实现方式:
      • 硬件负载均衡:所有请求都要通过机器转发。
      • LVS透明代理:增加了网络的开销(流量和延迟),因为多了一层转发;代理崩溃会影响所有请求。
      • 名称服务:没有代理地址,请求的发起者和处理者直接连接,只不过名称服务模块搜集处理服务器的地址信息,发起者先从本机名称服务获取地址。代码升级比较复杂。
      • 规则服务器:也是直接连接,区别是名称服务和处理者机器交互记录其地址,规则服务器则不和处理者交互,只提供规则给请求的发起者。
      • Master+Worker:Master节点管理任务,分配给不同的Worker进行处理。
  • 运算器:分布式系统运用多个节点的计算能力来协同完成整体的计算任务。
    • 例如我们需要日志处理服务器从应用服务器集群收集日志并处理,随着应用服务器的增多,单台日志处理服务器达到瓶颈,所以需要增加日志服务器来提升处理能力。
    • 可以使用Master+Worker来控制日志服务器集群,当然也可以采用规则服务器等。
  • 存储器:
    • 单机中存储器分为内存和外存。
    • 分布式中要把承担存储功能的多个节点组织在一起。
    • 实现方式:(如KV存储服务器)
      • 代理服务器,根据请求的Key划分(Sharding)进行转发
      • 名称服务,根据不同场景有两种实现:
        • 配合规则服务器,完成固定的Sharding策略
        • 在消息中间件的应用场景,同等看待KV存储服务器,可以灵活的增加或减少服务器。
      • 规则服务器,除了对数据进行Sharding,还包括具体KV存储服务器的地址。
      • Master根据请求返回目标KV存储服务器地址,相比名称服务返回所有地址,Master根据请求返回对应地址;相比规则服务器把规则传给具体应用再由应用服务器解析并完成规则下的路由选择,Master自身完成了这件事情,只把结果传给应用服务器,应用服务器只须拿着地址去访问即可。

1.5 分布式系统的难点

  • 缺乏全局时钟:单机有机器时钟为标准,容易控制时序。分布式系统中同步本身就有时间差,很难保持所有机器的时间一致。不过我们使用时钟来区分动作间的顺序不需要准确的时间,一般会由一个单独的集群来区分动作间的顺序。
  • 面对故障独立性:分布式系统中,某个部分有问题而其他部分正常是经常发生的。
  • 处理单点故障:单点即在分布式系统中的某个功能只由单机在支持,其发生的故障即单点故障,SPoE(Single Point of Failure)。所以能扩展为集群的就尽量扩展,只能单点实现的:
    • 做好这个单点的备份,能够在出问题时及时恢复,最好能自动恢复、快速恢复。
    • 降低单点故障的影响范围,尽量减少故障发生时的损失。
  • 事务:两阶段提交(2PC)、最终一致、BASE、CAP、Paxos等

1.6 集中式和分布式都有哪些特点?

集中式系统中数据以及所有业务单元都集中在中心节点,部署结构简单,无需考虑多个节点的部署。

分布式系统:

  • 分布性:机器在空间上随意分布。
  • 对等性:机器间没有主从之分,每个节点都是对等的。分布式系统常用副本这一概念来冗余数据和服务,比如在不同节点持久化同一份数据,避免数据完全丢失;或多个节点提供相同服务等。
  • 并发性:分布式系统中多个节点很可能会并发的操作一些共享资源。
  • 缺乏全局时钟:分布式系统中各个进程分布在任意空间,通过消息相互通信,所以相比单机有本机时间,分布式系统天然具有时间差。
  • 故障总是会发生:设计阶段考虑到的异常情况总是会在实际运行中发生。

第二节 大型网站架构演变历程

2.1 早期-用Java技术和单机来构建网站

熟悉的技术:LAMP、MVC框架、JSP、Servlet、JDBC、Spring、Struts、Hibernate、HTML、CSS、JavaScript、Python等。

举一个交易网站的例子,核心模块有用户、商品和交易

2.2 单机负载告警,数据库与应用分离

随着访问量增加,单台服务器负载持续增高,我们首先把数据库与应用分开到两条服务器。这种变更对系统影响很小,只须修改数据库地址即可。

2.3 应用服务器负载告警,走向集群

应用服务器压力不断增加,可以根据监测来进行针对性的优化,我们选用将应用服务器由单机转为集群的优化方式。

(1)单机转为集群的两个问题

变更为集群会带来两个要解决的问题:

  • 两个应用服务器的选择问题,我们可以通过DNS或负载均衡设备来解决。
  • Session问题。

引入负载均衡设备后:

我们知道HTTP协议是无状态的,为了支持有状态的会话需求,我们需要Session机制,在会话开始时分配一个SessionId,通过Cookie告知浏览器,以后会话中的每次请求都会携带此SessionId来告知Web服务器此次属于哪个会话。Web服务器需要独立的存储会话信息,当Cookie禁用时会拼接到URL中。

所以当服务器变为两台时,Session就出现了问题,保存在单机上的会话,和随机访问任一服务器的会话请求产生了矛盾。

(2)Session问题的解决方案

  1. Session Sticky:通过负载均衡器保证同一个会话的请求都在同一个Web服务器上处理。

    最简单的方案,但也存在问题:

    • 一台服务器宕机或重启,其会话数据会丢失,对应的用户需要重新登录;
    • 因为会话标识是应用层的信息,负载均衡器将会话请求保存在同一个Web服务器,需要进行应用层(第7层)的解析,开销会比第4层的交换大;
    • 负载均衡器也变成了一个有状态的节点,相比无状态节点,内存消耗更大,容灾方面更麻烦。
  2. Session Replication:每台Web服务器都保存相同的会话信息。需要在Web服务器间进行数据同步,保证Session数据一致。

    同样也有其问题:

    • 网络开销越来越大:同步Session数据导致了网络带宽的开销,只要Session数据有变化,就需要同步到所有机器,机器数越多,带来的网络开销越大;
    • 内存占用越来越大:每台服务器都要保存所有的Session数据,当整个集群的用户量很多时,保存数据所需的空间会很大。
  3. Session数据集中存储:集中的存放所有的Session数据,所有Web服务器向同样地址获取Session。

    此方案的问题:

    • 读写Session数据引入了网络操作,相比本机数据读取存在时延和不稳定行(内网通信好一些);
    • 集中存储Session的机器有问题,会影响整个应用(扩展集群)。
  4. Cookie Based:把Session存放在Cookie中,Web服务器从Cookie中生成对应Session数据。

    此方案的不足:

    • Cookie有长度限制
    • 安全性:Session数据本身是服务端数据,此方案是由客户端存放通过外部网络传输,所以要对写入Cookie的Session数据进行加密(并不绝对安全)。
    • 带宽消耗:指数据中心整体外部带宽的消耗(每次请求都增加了数据)。
    • 性能影响:每次HTTP请求和响应都带有Session数据,对Web服务器来说,响应的结果输出越少,支持的并发请求就越多。

大型网站比较适合 Session StickySession数据集中存储 ,根据具体场景做出选择和权衡。

2.4 数据读压力增大,读写分离

(1)采用数据库作为读库

对于大型网站,大部分业务是读多写少的场景,这种情况我们可以考虑读写分离的方式。

我们可以在架构中增加一个读库,它只负责读取服务。

带来问题:

  • 数据复制问题:数据库一般会提供数据复制功能,可以直接使用。

    • 要考虑数据复制的时延问题;
    • 复制过程中数据的源和目标之间的映射关系及过滤条件的支持问题。

    数据库支持:(数据库对数据复制的支持是相对有限的)

    • MySQL:Master(主库)+ Slave(备库)的结构,5.5版本之前都是异步数据复制,会有延迟,提供完全镜像方式的复制,保证了备库和主库的数据一致性。5.5后加入了 semi-sync ,在数据安全性上更好,但从读写分离的角度看仍有复制延迟的可能。
    • Oracle:Data Guard方案,主要用于容灾、数据库保护以及故障恢复等场景,在实施中又分为物理备库(物理StandBy)和逻辑备库(逻辑StandBy)。
  • 应用对于数据源的选择问题:写操作走主库,事务读也要走主库,还要考虑备库数据相对于主库的延迟。

(2)搜索引擎

当我们需要根据商品标题查询商品信息时,常规做法是通过数据库的 like 功能进行模糊查询。使用搜索引擎的倒排表方式可以大大提升检索速度。

搜索引擎首先要根据被搜索数据构建索引,数据改变索引也要改变。应用要选择什么数据走搜索引擎,什么数据走数据库。构建索引的过程也是一种数据复制的过程,只不过不是简单的复制对应数据。

可以从两个维度对搜索系统构建索引的方式进行划分:

  • 按照全量/增量划分:第一次构建索引用全量,在全量基础上采用增量方式更新索引(一般加入每日的全量作为补充)。
  • 按照实时/非实时划分:更新时间上倾向于实时的方式,非实时主要考虑对数据源头的保护。

(3)缓存

  1. 数据缓存:数据缓存的目的类似于读库,一般用来保存和查询键值对,来加速应用在响应请求时的数据读取速度。

    两种方式:

    • 应用主动更新缓存:应用访问缓存,数据不存在则从数据库读出并放入缓存,当缓存容量不足时清除最近不被访问的数据。
    • 数据库记录变化更新缓存:数据库发生变化,主动把数据放入缓存,这样能够及时更新缓存中的数据,不会造成读取失效。一般用于全数据缓存,要求相关代码要结合业务逻辑。

  2. 页面缓存:缓存一些动态产生的页面或页面的一部分内容,ESI是一种针对这种情况的规范。

    • Web服务器产生的请求响应结果返回给Apache,处理后找到ESI标签,在缓存中获取标签对应内容;若内容不存在,Apache模板通过Web服务器渲染这些内容,再把结果放回缓存,用内容替换掉ESI标签并返回给客户。

    • ESI模块总是要对响应结果进行解析,Web服务器处理时直接完成这项工作是更好的选择。所以改进为如下结构,把渲染和缓存功能放在Web服务器,这样更高效一些。

使用缓存来加速数据读取时要注意:

  • 缓存命中率:太低就导致大量请求仍要回到数据库;
  • 数据的分布和更新策略:
    • 分布上要考虑避免局部热点的机制,缓存服务器的扩容和缩容要尽量平滑(考虑一致性Hash);
    • 更新上会有定时失效、数据变更时失效和数据变更时更新几种策略。

2.5 引入分布式存储系统

常见的分布式存储系统:

  • 分布式文件系统:弱格式,内容格式需要自行组织,解决小文件和大文件的存储问题;
  • 分布式Key-Value系统:提供高性能的半结构化支持;
  • 分布式数据库:提供一个支持大数据、高并发的数据库系统。

分布式存储系统通过集群提供了一个高容量、高并发访问、数据冗余容灾的支持,直接代替原先的单机主库。

2.6 读写分离后,数据库又遇到瓶颈

虽然进行了以上的优化,我们的核心功能数据(用户、商品和交易)还都在一个数据库中,随着数据量和访问量的增大,数据库的压力还在不断增加。

此时我们需要进行数据的垂直拆分水平拆分

(1)专库专用,数据垂直拆分

垂直拆分就是把数据库中不同业务数据拆分到不同的数据库,对于交易系统这个例子就是把用户、商品、交易数据分开。

带来的影响:

  • 应用配置多数据源;
  • 单机跨业务事务要转为分布式事务;
  • 表关联要改变实现。

(2)垂直拆分后的单机仍有瓶颈,数据水平拆分

水平拆分就是把同一个表中的数据拆分到多个数据库中。一般是因为某个业务的数据表的数据量太大,达到了单个数据库的瓶颈。

水平拆分与读写分离:读写分离解决的是读压力过大的问题,对于数据量大或更新量的情况并不起作用。

水平拆分与垂直拆分:垂直拆分把不同表放到不同数据库,水平拆分把同一个表拆到不同数据库。

带来的影响:(假设拆分用户表)

  • 解决SQL路由问题:用户信息被切分到了两个库,在进行数据库操作时要知道操作的数据在哪个库。
  • 解决主键问题:原先可能是Oracle的 Sequence 或MySQL的自增字段,现在要保证多个库的主键不重复。
  • 修改查询:同个业务的数据被拆分到不同数据库,所以一些查询要从多个库取数据,如果还要分页就很难处理。

完成水平拆分后,我们能很好的应对数据量及写入量增长的情况。

2.7 单机负载告警,数据库与应用分离

解决完数据库问题后,我们来看下应用的变化。我们之前完成了应用服务器从单机到多机的扩展,但随着业务的发展,应用功能越来越多,为了控制应用的大小,我们需要把应用拆开。

(1)拆分应用

根据业务特性拆分:交易系统中就是拆成分别以交易和商品为主的两个应用,当然两个应用都有涉及用户的地方,我们先粗糙的让系统各自完成用户工作。

通常新拆出的应用之间没有直接的相互调用,但可能会链接相同的数据库。如下,我们拆分了几个业务,它们之间没有直接的调用,都依赖底层的数据库、缓存、文件系统、搜索等。

(2)服务化

如下图,我们把应用分成了三层,最上层是Web系统,用于完成不同的业务功能;中间层是服务中心,各自提供不同的业务服务;最下层则是业务的数据库。(图中省略了缓存等基础系统)

服务化的区别:

  • 方法调用:业务功能内不仅仅是单机的内部方法调用了,还需要远程服务调用(RPC);
  • 共享代码:不再是散落在不同的应用,而是集中在各个服务中心
  • 数据库连接:数据库交互工作在服务中心,Web应用专注于与浏览器交互的工作,不必多关注业务逻辑。服务中心不仅把散落的可共用的业务代码集中起来,还可以使其得到更好的维护。
  • 适合人员分工:每个服务都可以由固定的小团队来维护,更好的保持稳定性。

2.8 消息中间件

消息中间件(Message-oriented middleware,MOM)就是面向消息的系统,是在分布式系统中完成消息的发送和接收的基础软件。

消息中间件有两个为人熟知的优点:异步解耦

2.9 总结


第三节 Java中间件

在网站的演变过程中,无论是服务化、数据库的读写分离和拆分处理还是消息系统,都会用到相关的中间件。

中间件就是为软件应用提供除操作系统提供的服务以外的服务,让软件开发者更方便的处理通信、输入和输出,关注自己应用的部分。

我们这里主要了解三个领域的中间件:

  • 远程过程调用和对象访问中间件
  • 消息中间件
  • 数据访问中间件

第四节 服务框架


第五节 数据访问层


第六节 消息中间件


第七节 软负载中心与集中配置管理


第八节 CDN


第九节 分布式存储


第十节 搜索系统


第十一节 其他


参考:

🔗 《大型网站系统与Java中间件实践》