1.分布式系统的基础知识
网络IO实现方式:
1)BIO
BIO即Blocking IO,采用阻塞的方式实现。也就是一个套接字需要使用一个线程来处理。
不管是磁盘I/O还是网络I/O,数据在写入OutputStream或者从InputStream读取时都有可能会阻塞,一旦有阻塞,线程将会失去CPU的使用权,这在当前的大规模访问量和有性能要求的情况下是不能被接受的。虽然当前的网络I/O有一些解决办法,如一个客户端对应一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其他线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本,但是在一些使用场景下仍然是无法解决的。如当前一些需要大量HTTP长连接的情况,像淘宝现在使用的Web旺旺,服务端需要同时保持几百万的HTTP连接,但并不是每时每刻这些连接都在传输数据,在这种情况下不可能同时创建这么多线程来保持连接。如果要设置线程的优先级高低就更难了。
2)NIO
NIO即NonBlocking IO,基于事件驱动思想,采用的是Reactor模式。相对于BIO,明显的好处是不需要为每个套接字分配一个线程,而可以在一个线程中处理多个Socket套接字相关的工作。
React不是用单个线程去应对单个Socket套接字,而是统一通过Reactor对所有客户端的Socket套接字的事件做处理,然后派发到不同的线程中。这样就解决了BIO中为支撑更多的Socket套接字而需要打开更多线程的问题。
相关文档:https://www.jianshu.com/p/415325c263b2
NIO作出的改进就是“一个请求一个线程”,在连接到服务端的众多socket中,只有需要进行IO操作的才能获取服务端的处理线程进行IO。这样就不会因为线程不够用而限制了socket的接入。客户端的socket连接到服务端时,就会在事件分离器注册一个 IO请求事件 和 IO 事件处理器。在该连接发生IO请求时,IO事件处理器就会启动一个线程来处理这个IO请求,不断尝试获取系统的IO的使用权限,一旦成功(即:可以进行IO),则通知这个socket进行IO数据传输。
3)AIO
AIO就是AsynchronousIO,就是异步IO。AIO和NIO的区别是,AIO采用Proactor模式,AIO在进行读/写操作时,只需要调用相应的read/write方法,并且传入CompletionHandler(动作完成的处理器);动作完成后,会调用CompletionHandler(动作完成的处理器)。NIO的通知是发生在动作之前,是在可读、可写的时候,Selector发现这些事件后调用Handler处理。
分布式应用
1)请求发起者与请求处理者中间有一个硬件复杂均衡设备,所有的请求都要走这个设备来完成转发的控制。
2)第二种与第一种差别不大,差别仅在于中间的硬件负载均衡设备更换成了LVS,这种方式的主要的特点是代价低,而且可控性较强,即你可以相对自由地按照自己的需要去增加负载均衡的策略。
我们称这种方式为透明代理,这种方式对于请求发起者和请求处理者都是透明的。但是问题是一增加了流量的转发和网络的时延,二是一旦透明代理出了问题,所有的请求都不可达了。
下面是基于LVS的负载均衡文档:https://blog.youkuaiyun.com/weixin_40470303/article/details/80541639
3)第三种方式,在请求发起方和请求处理方这两个集群中间没有代理服务器这样的设备存在,而是请求发起方和请求处理方式的直接连接。在请求发起方和请求处理方的直接连接外部,有一个"名称服务"的角色,它的作用主要有两个,一个是收集提供请求处理的服务器的地址信息;另外一个是供这些地址信息给请求发起方。当然,名称服务只是起到了一个地址交换的作用,在发起请求的机器上,需要根据从名称服务得到的地址进行负载均衡的工作。也就是说,原来在透明代理上做的工作被拆分到了名称服务和发起请求的机器上了。
4)第四种方式,增加规则服务器,与名称服务器类似,名称服务器是交互获得这些机器的地址,而规则服务器的方式中,规则服务器本身并不和请求处理的机器进行交互,只负责把规则提供给请求发起的机器。
5)第五种方式 Master+Worker的工作方式,存在一个Master节点来管理任务,由Master把任务分配给不同的Worker去进行处理。这种方式使用的场景和前面的几种不太一样。这里没有像前面介绍的那种请求发起和请求处理,这个方式更多的是任务的分配和管理。
分布式系统的难点
1)缺乏全局时钟
同步本身就存在着时间差,因此我们需要有其他办法来解决这个问题。
2)面对故障独立性
对于分布式系统,经常会出现某些子系统是正常而一些子系统是异常的情况,我们需要找到应对和解决故障独立性的方法。
3)处理单点故障
在整个分布式系统中,如果某个角色或者功能只有某台机器在支撑,那么这个节点称为单点,其发生的故障称为单点故障,也是常说的SPof(Single Point of Failure)。我们需要在分布式系统中尽量避免出现单点,尽量保证我们的功能都是在集群中完成的。当然,这种变化一般比较困难,否则就不太会有单点问题了,如果不能够把单机实现变成集群实现,那么一般还有另外两种选择。
- 给这个单点做好备份,能够在出现问题时及时回复,并且尽量做到自动回复,降低恢复需要用的时间。
*降低单点故障的影响范围。比如数据库的分片存储。
4)事务的挑战
2.大型网站及其架构演进过程
大型网站的访问量和数据量二者缺一不可。此外,除了海量数据和高并发的访问量,本身业务和系统的复杂度也是考察的方面。
2.1 用Java技术和单机来构建的网站
单实例,数据库和服务在同一台机器上。
2.2 单机负载告警,应用与数据库分离
单实例,数据库与应用在不同机器上,但在同一个内网中。
2.3 应用服务器负载告警,如何让应用服务器走向集群
把应用从单机变为集群的优化方式,这里有两个问题需要解决:
-
最终用户对两个应用服务器访问的选择问题,可以通过DNS来解决,也可以通过负载均衡设备来解决,这里我们着重讲负载均衡。(DNS是DNS服务器上同一台主机配置多个IP地址,在应答DNS查询时,DNS查询文件中记录的IP地址解析返回,将客户端引导到不同的机器上,使得不同的客户端访问不同的机器,来达到负载均衡的区别,它与Ngnix这种负载均衡设备的最大区别是它不能区分服务器的差异,也无法反映服务器当前的状态)
-
引入负载均衡设备,我们会遇到一个Session问题。
2.3.1 解决应用服务器变为集群后的Session问题
先来看看什么是Session:Http协议本身是无状态的,需要基于Http协议支持会话状态(Session State)的机制。而这样的机制应用可以使Web服务器从多次单独的Http请求中看到会话,也就是知道哪些请求是来自哪个会话的。具体实现方式为:在会话开始时,分配一个唯一的会话标识(session id),通过Cookie把这个标识告诉浏览器,以后每次请求的时候,浏览器都会带上这个会话标识来告诉Web服务器请求是属于哪个会话的。在Web服务器上,各个会话有独立的存储,保存不同会话的信息。如果遇到禁用Cookie的情况,一般的做法就是把这个会话标识放到URL的参数中。 但是当我们的服务变成多实例之后,就有了一个问题,会话数据是保存在单机上的,如果不做处理,不能保证每台机器上的会话数据的状态是一致的,这就是Session问题。那么有下面几种方式解决Session不一致的问题:
-
Session Sticky: 这个方案非常简单,我们总是企图把相同的用户请求打到同一台机器上,这样其他机器上就无需保存这个用户的信息了,不过也带来了以下几个问题:
1)如果有一台Web服务器宕机或者重启,那么这台机器上的会话数据就会丢失,如果会话数据中有登录信息,那么用户就要重新登录。
2)会话标识是应用层的信息,那么负载均衡要将同一个会话的请求都保存在同一个Web服务器上的话,就需要进行应用层的解析,这比在传输层解析开销要大(通常这个Cookie的保存是在网关也就是TCP层就鉴权了)。
3)负载均衡器变成了一个有状态的节点,它要计算每次请求应该选择哪个实例,和无状态的节点相比,开销更大,容灾方面更加麻烦。 -
Session Replication:这个方案就是把Session存储在Web应用中(通常是内存),各个实例间Session同步,这个方案也有以下明显几个问题:
1)同步Session数据造成了网络带宽的开销。只要Session数据有变化,就需要将数据同步到所有的机器上,机器数越多,同步带来的网络带宽开销就越大。
2)每台Web服务器都保存所有的Session信息,如果整个集群的Session数很多,每台机器用于保存Session数据的内容将会占用很多,且不利于水平扩展集群。 -
Session集中存储:利用Redis这样的分布式中间件统一保存Session信息,然后与多实例分开部署,所有实例要拿Session信息统一发送请求去Redis里拿,这个方案在大部分情况下都比较好,但也有以下几个问题:
1)读写Session数据引入了网络操作,这相对于本地数据读取,问题在于存在时延和不稳定性,不过我们的部署都是在内网,问题应该不大。
2)如果集中存储Session的机器有问题,会严重影响我们的应用。 -
Cookie Based:这个方案是通过Cookie来传递Session数据的,可以理解为我们把会话信息保存在客户端,然后用户每次请求会带上这个Cookied信息,这个方案有明显的问题:
1)Cookie长度的限制,这会限制Session数据的长度。
2)安全性,尽管我们可以对Cookie内容加密,但是就安全来说,物理上不能接触才是最安全的。
3)带宽消耗,这里指的不是内部Web服务器之间的带宽消耗,而是我们数据中心整体外部的带宽的消耗。
4)性能影响,每次Http请求都会带上Cookie内容(而不是CookieId让服务器去处理)。
Session与Cookie的区别:
https://baijiahao.baidu.com/s?id=1619095369231494766&wfr=spider&for=pc
2.4 数据读压力变大,读写分离
2.4.1 采用数据库为读库
我们只在原来的架构上增加了一个读库,这个结构的变化会带来两个问题:
1)数据复制问题
2)应用对于数据源的选择问题
我们希望通过读库来分担主库上读的压力,那么就要首先解决数据怎么复制到读库的问题。数据库系统一般都提供了数据复制的能力,我们可以直接使用数据库系统的机制。但对于数据复制,我们还需要考虑数据复制带来的时延问题以及复制过程中数据的源和目标之间的映射关系以及关系过滤条件的支持问题。 数据复制延迟带来的就是数据短期的不一致。
对此,不同的数据库有不同的支持,下面是Mysql主从复制的学习链接:
https://baijiahao.baidu.com/s?id=1617888740370098866&wfr=spider&for=pc
2.4.2 搜索引擎其实也是一个读库
搜索引擎在大部分搜索场景下可以替换db的模糊及扫描查询,这样一来我们在架构的设计上就可以降级设计,先去搜索集群上搜索,没有搜到再去db里查,同时查到了立即异步组织成索引的形式倒排插入搜索引擎中,这样很大程度上可以解决db的读压力。
2.4.3 加速数据读取的利器-----缓存
2.4.3.1 数据缓存
分为内存缓存和分布式缓存,内存缓存因为要保证实例的无状态现在用的比较少了。分布式缓存以Redis为代表,服务部署在内网,所有实例均可访问。数据缓存一般用以放热数据,即采用数据淘汰策略,淘汰策略又细分为时间淘汰和最少使用淘汰,庆幸的是,这些redis都帮我们做好了。
数据缓存一般用以缓解db的压力,在这一层如果大部分热数据走缓存,一定要解决缓存不一致和缓存雪崩的问题。假设缓存的性能不尽人意或者命中缓存率太低,这样服务会频繁去更新缓存,反正增加了服务器的消耗。同时,当缓存失效时,数据会大量穿透到db上,把db打死,导致服务的不可用。所以在缓存层还要考虑扩容和容灾等因素。
2.4.3.2 页面缓存
数据缓存可以加速应用在响应请求时的数据读取速度,但最终要返回给用户的还是页面,有些动态产生的页面或者页面的一部分特别热,我们就可以对这些内容进行缓存。ESI就是针对这种情况的一种规范。
2.4.4 弥补关系型数据库的不足,引入分布式存储系统
数据库主要是提供关系数据的查询和强事务的支持。除了数据库之外,还有其他用于存储的系统,也就是我们说的分布式存储系统。分布式存储系统有分布式文件系统,自身起到了存储的作用也提供数据的读写支持,分布式存储系统通过集群提供了一个高容量、高并发、数据冗余容灾的支持。很多场景下我们用它来解决小文件和大文件的存储问题。通过Key-Value系统提供高性能的半结构化的支持,通过分布式数据库来提供一个支持大数据、高并发的数据库系统。
目前主流的分布式存储系统有:Hadoop HDFS、OpenStack的对象存储Swift、Facebook用于图片存储的Haystack。
以下是介绍:http://stor.51cto.com/art/201802/566022.htm
2.4.5 读写分离后,数据库又遇到瓶颈
2.4.5.1 专库专用,数据垂直拆分
所谓的垂直拆分就是把原来数据库里不同的业务数据放在不同的数据库中,其带来的问题是:
1)应用需要配置多个数据源,这就增加了所需的配置,不过带来的是每个数据库连接池的隔离。
2)原来单机中跨业务的事务。 一种办法是使用分布式事务,其性能要明显低于之前的单机事务,而另一种办法就是去掉事务或者不去追求强事务支持,则原来在单库中可以使用的表关联查询也需要改变实现了。
对数据进行垂直拆分后,解决了把所有业务数据放在一个数据库中的压力问题。并且也可以根据不同业务的特点进行优化。
2.4.5.2 垂直拆分后的单机遇到瓶颈,数据水平拆分
与数据垂直拆分对应的还有数据水平拆分。数据水平拆分就是把同一张表的数据,拆分到两个数据库中。产生数据水平拆分的原因是某个业务的数据表的数据量或者更新量达到了单个数据库的瓶颈(200W行就会变慢)。
数据水平拆分与读写分离的区别是,读写分离是解决读压力大的原因,对于数据量大或者更新量大的情况并不起作用。
数据垂直拆分和数据水平拆分的区别是,垂直拆分是把不同的表拆到不同的数据库中,而水平拆分而是把同一张表拆到不同的数据库中。
下面来分析一下水平拆分后对业务带来的问题:
1)访问用户信息的系统要解决数据路由的问题(通常在各种操作前有取模路由的步骤),因为现在用户信息被分在了两个数据库中,需要在进行数据库操作时了解需要操作的数据在哪里。
2)此外,主键的处理也会变得不同,原来依赖单个数据库的一些机制需要变化,例如使用Oracle的Sequence或者Mysql上的自增字段,现在不能简单使用了。并且在不同的数据库中也不能直接使用一些数据库的限制来保证主键不重复了。
3)最后,由于同一个业务的数据被拆分到了不同的数据库中,因此一些查询需要从两个数据库中读取数据,如果数据量太大而需要分页,就会比较难处理了。
不过,一旦我们能够完成数据的水平拆分,我们将能够很好地应对数据量及写入量增长的情况。具体如何完成数据水平拆分,在后面分布式数据访问层会进行更加详细的介绍。
2.4.6 数据库问题解决后,应用面对的新挑战
2.4.6.1 拆分应用
前面所讲的读写分离,分布式存储,数据垂直拆分和水平拆分都是为了解决数据方面的问题。下面来看看应用方面的变化。
随着业务的发展,应用的功能会越来越多,应用也会越来越大。我们需要考虑如何不让应用持续变大。这就需要把应用拆分开,从一个应用变为两个甚至多个应用。
第一种方式,根据业务的特性把业务拆开。
第二种方式,按照功能拆分。如鉴权、交易、查询模块分开。
2.4.6.2 走服务化的路
把应用分为三层,处于最上层的是Web系统,用于完成不同的业务功能;处于中间的是服务中心,如用户中心,商品中心,交易中心,下层则是一些公用的数据库及中间件。
这样改变后有几个很重要的变化,首先,业务功能之间的访问不仅是单机内部的方法调用了,还引入了远程的服务调用。其次,共享的代码不再是散落在不同的应用中了,这些实现被放在了各个服务中心。第三,数据库的连接也发生了一些变化,我们把与数据库的交互工作放到了服务中心,让前端的Web应用更加注重与浏览器的交互工作,不必关心业务逻辑的上去。第四,通过服务化,无论是Web应用还是服务中心,都可以是由固定的小团队来维护的系统,这样可以更好地保持稳定性,并能更好地控制系统本身的发展,况且稳定的服务中心日常发布的次数也远小于前端Web应用,因此这个方式也减小了不稳定的风险。
2.4.7 初始消息中间件
我们来看一下消息中间件。Wiki百科上对消息中间件的定义是
“Message-oriented middleware(MOM) is software infrastrucure focused on
sending and receiving messages between distributed systems.”
意思就是面向消息的系统(消息中间件)是分布式系统中完成消息的发送和接受的基础软件。
消息中间件有两个常被提及的好处,即异步与解耦。 多个应用之间并不直接联系直接通过消息中间件打交道,这样就按成了解耦,目的是收发消息的双方彼此不知道对方的存在,也不受对方影响。所以将消息投递给接受者实际上都采用了异步的方式。