本系列文章整理自《大型网站系统与Java中间件实践》
分布式系统的定义
A distributed system is one in which components located at networked computers communicate and coordinate their actions only by passing messages.
组件分布在网络计算机上,组件之间仅仅通过消息传递来通信并协调行动。
理解:分布式系统一定是由多个节点组成的系统,一般来说一个节点就是一台计算机,节点之间互连;最后这些节点部署了我们的组件,并且相互之间的操作会有协同。
分布式系统的意义
- 升级单机处理能力的性价比越来越低;
- 单机处理能力存在瓶颈;
- 出于稳定性和可用性考虑。
分布式系统基本知识
1.组成计算机的5要素
输入设备、输出设备、运算器、控制器、存储器(内存和外存)。计算机断电时内存的数据会丢失,外存的数据依然存在。
2.线程与进程的执行模式
明确:这里指的是单进程下的多线程。多线程开发中,我们需要处理线程间的通信,需要对线程并发做控制,需要做好线程间的协调工作。
2.1阿姆达尔定律
P指的是程序中可并行部分的程序在单核上执行时间的占比,N表示处理器的个数(总核心数)。S(N)是指程序在N个处理器(总核心数)相对在单个处理器(单核)中速度的提升比。
这个公式告诉我们,程序中可并行的代码比例决定你增加处理器(总核心数)所能带来的速度提升上限。例如当P=0.5,速度上限是2。当P=0.2,速度上限是1.25。可见,在多核的时代,并发程序的开发多么重要。
2.2互不通信的多线程模式
在多线程程序中,多个线程会在系统中并发执行。如果线程之间不需要处理共享的数据,也不需要动作协调,将会非常简单,就是多个线程独立完成自己线程的工作。如图:
2.3基于共享容器协同的多线程模式
多个线程之间对共享数据进行处理。如经典的生产者和消费者例子,我们有一个队列用于生产和消费,那么这个队列就是多个线程会共享一个容器或者数据对象,多个线程并发地访问这个队列。
对于这种在多线程环境下对统一数据的访问,我们需要有所保护和控制以保证访问的正确性。对于存储数据的容器或者对象,有线程安全和线程不安全之分,对于线程不安全的,一般通过加锁或者通过Copy On Write的方式来控制并发访问。使用加锁的方式时,如果数据读写比例很高,一般采用读写锁而非简单的互斥锁。对于线程安全的容器或对象,可以在多线程环境下直接使用。
注意:有时通过加锁把使用线程不安全容器的代码改为使用线程安全容器的代码时,会遇到一个陷阱。
即在一个使用Map进行计数统计总数的例子中,map中value整型使用线程不安全容器HashMap是这样的:
public class TestClass {
private HashMap<String, Integer> map = new HashMap<String, Integer>();
public void add(String key){
Integer value = map.get(key);
if (value == null) {
map.put(key, 1);
}else{
map.put(key, value + 1);
}
}
}
使用ConcurrentHashMap来替换HashMap,并且仅仅去掉Synchoronized关键字,问题出现了。(Java代码如下)
public class TestClass {
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
public void add(String key) {
Integer value = map.get(key);
if (value == null) {
map.put(key, 1);
} else {
map.put(key, value + 1);
}
}
}
我的理解是对ConcurrentHashMap的get和put方法是线程安全的,但这个计数过程是要求对整个过程(get值判断再put)线程安全的,先get值判断再put得操作不是原子的,所以不线程安全,会造成计数小于实际。
2.4通过事件协同的多线程模式
除了并发控制访问,线程间会存在协调的需求。例如A、B线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态发生或者改变和A线程相关。这个场景下,需要完成线程间的协调。
如上图,右侧线程执行到某个步骤需要等待事件触发,这个事件由左侧线程触发并通知。期间,右侧线程一直阻塞直到事件通知后才继续执行。
同时还需要避免死锁情况,一般能够原子性的获取需要的多个锁,或者注意调整对多个锁的获取顺序,能有效避免死锁。
下面来看一个死锁的例子:加锁有两个锁A和B,两个线程T1和T2,两个线程T1,T2某段代码都需要获取A锁和B锁,伪代码如下
T1代码
……
A.lock();
B.lock();
……
T2代码
……
B.lock();
A.lock();
……
这个时候T1等不到B,T2等不到A。下面这种做法可以避免死锁:
T1代码
……
A.lock();
B.lock();
……
T2代码
……
A.lock();
B.lock();
……
T2线程获取锁的顺序发生变化,现在和T1一样,都获取A,再获取B锁。
还有另一种方式避免死锁:
T1代码
……
getLocks(A,B);
……
T2代码
……
getLocks(A,B);
……
getLocks方法一次性获取两个锁。
2.5 多进程模式
下面关注进程间的关系,多进程和多线程有许多相似之处。线程是属于进程的,一个进程的多个线程共享内存空间,而多个进程之间内存空间是相互独立的,因此多个进程间通过内存共享,交换数据的方式有所不同。进程间的通信,协调以及事件通知或者等待锁释放也与多线程不一样。
区别:多进程相对于单进程多线程的方式来说,资源控制更容易实现。多进程的单个进程错误不会导致系统整体不可用。
分布式系统是多机组成的系统,可以把它看做单机多线程变成了多机多线程。多机系统带来一个好处,即当单个机器出现问题时,如果处理得好,就不会影响整体的集群。
3.网络通信的基础知识
3.1 OSI与TCP/IP模型
ISO的OSI七层网络模型
OSI模型与TCP/IP模型对比
3.2网络IO实现方式
接触比较多的网络模型主要是TCP/IP协议栈,UDP也在一些场景下用到。
当我们使用socket通信时,有三种方式:BIO、NIO和AIO。
BIO方式
BIO即Blokcing IO,采用阻塞的方式实现。即一个Socket通信需要一个线程处理。发生建立连接、读数据、写数据的操作时,都可能会阻塞。这个模式简单。但带来主要问题是当作为Server端,一个线程只处理一个socket,在支持并发连接时,需要更多的线程完成这个工作。
BIO的工作方式:
NIO方式
NIO即NonBlocking IO,基于事件驱动思想,采用Reactor模式。Java实现的服务器系统较多采用。相对于BIO,NIO一个明显的好处是不需要为每个socket套接字分配一个线程,而可以在一个线程处理多个Socket套接字相关的工作。
上图,Rector会管理所有的hanler,并且把出现的事件交给相应的Handle处理。
通信中的应用:在NIO模式下不是用单个线程去应对单个Socket套接字,而是统一通过Reactor的所有客户端的Socket套接字处理,然后派发到不同线程中。这就解决了BIO为支撑更多Socket套接字而需要打开更多线程的问题。
AIO方式
AIO即AsynchronousIO,即异步IO。通过Proactor模式。AIO与NIO不同之处在于,AIO在读写操作时,只需调用相应的read/write方法,并且传入ComletionHandler(动作完成处理器);
在动作完成后,会调用动作完成处理器。
AIO与NIO最大区别:NIO在有通知时可以进行读或写,而AIO在有通知时表示相关操作已完成。
4.如何把应用从单机扩展到分布式
输入、输出、运算、存储、控制这五个方面组成计算机,分布式有何变化?
4.1输入设备的变化
分布式系统由通过网络连接的多个节点组成,一种是互相连接的多个节点,在接收其他节点传来的信息时,该节点可以看做是输入设备;另外一种就是传统意义上的人机交互输入设备。
4.2输出设备变化
一种是指系统中的节点;另一种是用户终端屏幕。
4.3控制器变化
单机系统中指的是CPU中的控制器。
分布式系统中是由多个节点通过网络连接在一起并通过消息的传递进行协调的系统。
控制器的主要作用就是协调和控制节点之间的动作和行为。
如上图是一个远程服务调用的场景。所有请求经过中间负载均衡设备来完成请求转发的控制,这就是一种控制的方式。
使用LVS请求调用,这种方式代价低,可控性强。一般称之为透明代理。
在集群中,这种方式对于请求发起方和处理请求的一方都是透明的。
这种方式有两种不足:一方面增加网络开销(流量),延迟。数据量越大约明显。由于中间的透明代理处于请求的必经路径上,如果代理出现问题,所有请求都会影响。
第三种方式
请求发起和处理方直连,外部有一个名称服务。作用:一个收集提供请求处理的服务器处理地址信息;另外提供地址信息给请求发起方。起到地址交换作用,在发起请求的机器上需要根据从名称服务得到的服务器地址进行负载均衡的工作。原来透明代理上的工作被拆分到名称服务上和请求发起方。
第四种-规则服务器请求直连
这种模式下,请求方和处理方也是直连的。那么请求方如何选择处理方的机器呢?规则服务器的规则。
在请求方机器,会有对规则进行处理的代码逻辑。规则服务器只负责把规则提供给请求方。
最后一种方式Master+Worker
Master管理任务,分配给不同的Worker来做。场景:任务分配和管理。
4.4运算器的变化
多台服务器,由DNS服务器进行调度和控制,在用户解析DNS时,就会给予一个服务器地址。
另一种方案:负载均衡
中间增加负载均衡设备(纯硬件或LVS),DNS永远返回负载均衡的地址,而用户访问是通过负载均衡到达后面的服务器。
总结来说,构成运算器的多个节点在控制器的配合下对外提供服务,构成分布式系统的运算器。
日志处理系统场景:
单日志系统
改进Master方式
4.5存储器变化
分布式系统中需要把承担存储功能的多个节点组织在一起,使之看起来是一个存储器。
单机的Key-Value场景
改进:使用代理。代理服务器作为控制器转发来自应用服务器的请求。一般可以根据Key来划分。
使用名称服务的Key-Value服务
应用服务器与KV服务器直接相连。KV服务器的选择逻辑放在应用服务器上完成。实际实施中通过规则服务器配合;另外则是对等看待,灵活适应KV服务器的增加减少。
规则服务器不仅写明了如何对数据做划分,还包含了具体的KV服务器地址。
不同的是Master会根据请求返回目标的KV服务器地址,然后由应用服务器直接请求KV服务器。KV存储服务器选择工作在MASTER完成。
应用服务器只需根据Master返回的结果去访问相应的KV服务器即可。具体应用广泛。
5.分布式系统难点
5.1缺乏全局时钟
很多时候,我们使用时钟可以区分2个动作的顺序,而不是一定要知道准确时间。这个工作交给单独集群来完成,通过集群区分多个动作的顺序。
5.2面对故障独立性
5.3处理单点故障
整个分布式系统如果某个功能只有某台单机支撑,这个节点称为单点。故障称为单点故障。SPoF(Single Point of Failure).分布式系统中尽量避免单点。
无法避免:
1.给单点备份,尽量做到遇到问题,自动恢复。
2.降低单点故障影响范围。如:
5.4事务的挑战
两阶段提交(2PC)、最终一致、BASE、CAP、Paxos等。至此简要介绍了分布式系统中比较基本的偏实践的知识。