初探分布式系统
A distributed system is one in which components located at networked computers communicate and coordinate their actions only by passing messages.
重点在于:分布在网络计算机上,以及仅仅通过消息传递来通信并协调行动。
为什么我们尝试使用分布式系统?
- 升级单机处理能力的性价比越来越低。
- 单机处理能力存在瓶颈。
- 处于稳定性和可用性的考虑。
摩尔定律:当价格不变时,每隔18个月,继承电路上可容纳的晶体管数量就会增加一倍,性能也将提升一倍。
对于上面问题有对应的三个解答:
- 在同一时间点,通过更换硬件做垂直扩展的方式来提升性能会越来越不划算。
- 某个固定时间点,单颗处理器有自己的性能瓶颈,也就是说即使你花费更多的钱去买计算力也买不到。
- 分布式系统将带来稳定性、可用性的提升。
通过阿姆达尔定律可以看到,程序中串行部分对于增加 CPU 核心来提升处理速度存在限制,它是这样描述的:
其中,P 指的是程序中可并行部分的程序在单核上执行时间的占比,N 表示处理器的个数(总核心数)。S(M) 是指程序在 N 个处理器(总核心数)相对在单个处理器(单核)中的速度提升。
1.1 多线程交互模式
1.1.1 互不通信的多线程模式
多个线程独立完成自己的任务:
1.1.2 基于共享容器协同的多线程模式
通过共享的数据进行处理,比如生产者和消费者模式,我们可以使用队列用于生产和消费,多个线程会并发访问这个队列:
此时,我们需要保护和控制以保证访问的正确性,对于线程不安全的容器或对象,一般可以通过加锁或者通过 Copy On Write 的方式来控制并发访问。
值得注意的是,我们在使用一些线程安全的容器,如『ConcurrentHashMap』的时候,它是线程安全的,但我们的一些操作可能是线程不安全的,比如,一个整形自增,取是一个操作,增是一个操作,它不是一个原子操作,所以做 i++
的时候,结果不一定正确,我们可以采用原子类等方式完成这个功能。
1.1.3 通过事件协同的多线程模式
例如 A B 两个线程,B 线程需要等到某个状态或事件发生才能继续自己的工作,而这个状态改变或者事件产生与 A 相关,这个场景下,就需要线程间的协调。
下面的过程中,线程 B 将会等待线程 A 的执行,原子性获得需要的锁或者注意调整多个锁的获取顺序,可以有效避免死锁:
1.2 多进程模式
多进程和多线程有比较多相似的地方,而造成不同最大的原因是因为:线程属于进程,多个线程共享了进程的内存空间;多个进程之间的内存空间是独立的,因此多个进程间通过内存共享、交换数据的方式与多个线程间的方式就有所不同。
多进程相对于单进程的特点是:
- 资源控制更加简单。
- 多进程中单个进程出现问题,不会造成整体的不可用。
当然,使用多进程会比多线程复杂,多进程也可以共享数据,但是代价比多进程大,会涉及序列化和反序列化。
1.3 网络 IO 实现
主要介绍 BIO NIO AIO 三种方式。
1.3.1 BIO 方式
一个 Socket 套接字需要一个线程来进行处理,发生「建立连接」、「读数据」、「写数据」的时候,都可能发生阻塞。
1.3.2 NIO 方式
基于事件驱动的思想,采用『Reactor』模式:
『Reactor』会管理所有的「handler」,并把出现的事件交给相应的『Handler』去处理,它的通信过程如下:
在 NIO 的方式下不是所有单个线程去应对单个「socket」套接字,而是统一通过『Reactor』对所有客户端的「socket」进行处理,最后派发到不同的线程中,这解决了 BIO 中需要开启足够多线程的问题。
1.3.3 AIO 方式
异步 IO ,采用的是 Proactor 模式,它和 NIO 的差别在于,AIO 在进行读、写的时候,需要调用相应的 read、write 方法,并且需要传入『CompletionHandler』动作完成处理器;完成以后会调用『CompletionHandler』,而 NIO 的通知是发生在动作之前,是在可写、可读的时候『Selector』发现这些事件后调用「Handler」处理。
所以 NIO 和 AIO 最大的区别是:NIO 在有通知时可以进行相关的操作,例如读或者写;而 AIO 在有通知的时候操作已经完成。
1.4 控制器的变化
分布式系统是由多个节点通过网络连接在一起并通过消息的传递进行协调的系统。
单机系统中,控制器指的是 CPU 中的控制器;而分布式系统中,我们的控制器是不再是 CPU 中那些具体的电子元件,而是分布式系统中的控制方式。
1.4.1 透明代理
跳过硬件负载均衡,我们看看通过 LVS 的方式来进行请求的负载均衡:
这是透明代理,从发起请求到处理请求,都是透明的。发起方以为是中间的代理提供了服务;处理方以为是中间的代理发起了请求。这个方式有三个不足:
- 增加了网络的开销,指的是流量,如果使用 LVS 的 TUN 或者 DR 模式,那么处理请求服务器上的返回结果会直接到请求服务的机器,不会再通过中间的代理,只有请求的数据包在过程中多一次代理的转发,数据包小的时候不明显,随着数据包的增大,流量增加会比较明显。
- 延时,这是结构上存在的理论层面的问题,实际影响很小。
- 强依赖于代理,代理是必经之路,如果代理出现问题,那么所有请求都会收到影响,我们需要考虑对代理服务器的热备份,不过切换时还是会有影响。
1.4.2 名称服务
同样是完成请求发起到请求处理的请求处理的请求派发工作,与「透明代理」不同的是,在请求发起和请求处理的两个集群中没有代理服务器这样的设备,而是采用「直连」的方式,现在多了一个「名称服务」的角色,它的作用是:
- 收集服务提供者的地址信息。
- 提供这些信息给消费者。
发起请求的机器,需要根据「名称服务」得到的地址进行负载均衡操作,原来在透明代理上做的工作被拆分到了「名称服务」和发起请求的机器上了。
它的优势在于:这个「名称服务」不是必经之路,如果出现问题,我们有不少办法能保证请求的正常。同时它减少了中间路径额外的带宽消耗。
劣势在于:代码的升级较为复杂。
1.4.3 规则服务器
它和「名称服务」的方式很像,只是这里采用的是「规则服务器」的方式,请求发起和请求处理的机器也是直接连接的,这是靠服务器给的规则实现的,在请求发起的机器上,对规则进行处理,从而进行「请求处理服务器」选择的代码逻辑。
它和「名称服务」的不同在于,它本身不和请求处理的机器进行交互,只负责将规则提供给请求发起的机器。
它的优缺点也和「名称服务」的方式类似。
1.4.4 Master + Worker
存在一个 Master 点来管理任务,它将任务分发给 Worker 进行执行,这个方式更多的是任务的分配和管理。
1.5 运算器的变化
在单机系统中运算器是具体的电子元件,而在分布式中,运算器是多个节点组成的。单机的计算能力有上限,而分布式系统中的运算器是运用多个节点的计算能力来协同完成整体的计算任务。
1.5.1 DNS 协调
在用户在解析 DNS 的时候,就会被给予一个服务器的地址,这样的方式看起来我们在控制器部分的「名称服务」或者「规则服务器的方式」,没有中间代理设备,用户直接知道提供服务的服务器的地址。
1.5.2 负载均衡
我们在其间加上一个负载均衡设备,可以是纯硬件或者 LVS 软件,DNS 返回的永远是负载均衡的地址,而用户的访问都会通过负载均衡到达后面的网站服务器。
构成运算器的多个节点在控制器的配合下对外提供服务,构成了分布式系统的运算器。
1.6 存储器的变化
在单机系统中,我们一般把存储器分为内存和外村,内存的数据在机器出现状况的情况下会出现丢失,而外存是用于长久保存数据的,当然,也不是绝对可靠。
1.6.1 代理服务器
加上代理服务器,这个服务器作为控制器转发来自应用服务器的请求,而转发请求使用的策略与具体业务非常密切,可以用 key 作为划分。
1.6.2 名称服务
采用「名称服务」的方案,「名称服务」用于管理在线的 KV 存储服务器,并且把地址传到应用服务器这边,应用服务器会和 KV 存储服务器直接联系。
1.6.3 规则服务器
我们省去「名称服务」,规则服务器的规则不仅写明了如何对数据做 Sharding ,还包含了具体的目标 KV 存储器的地址,我们根据既定规则让应用服务器去具体的 KV 存储器。
1.6.4 Master 控制
Master 根据请求返回目标 KV 存储服务器地址,然后由应用服务器直接去访问对应的 KV 存储服务器,它和「名称服务」的区别在于,它是根据请求到 Master 返回 KV 存储服务器的地址,而不是全部地址,所以具体的 KV 存储服务器选择工作在 Master 上完成了,而不是在应用服务器上。和「规则服务器」相比,它也不需要告诉应用服务器规则,Master 已经将事情完成了。
1.7 分布式难点
1.7.1 缺乏全局时钟
分布式系统中,每个节点都有自己的时钟,在通过相互发送消息进行协调时,如果仍然依赖时序,那么会很难进行处理,很多时候我们使用时钟,它是用来区分两个动作的顺序,而不是一个准确的时间,我们可以把这个工作给一个单独的集群,用这个集群来区分多个动作的顺序。
1.7.2 面对故障的独立性
整个分布式系统也是有可能出现宕机的,但实践中,更多是某个或某些节点有问题,我们要保证其他节点的独立性,必须找到应对和解决故障独立性的方法。
1.7.3 处理单点故障
在分布式系统中,如果某个角色或者功能只有某个单机在支持,那么这个节点就是一个单点,它发生的故障就是单点故障,我们在分布式系统中应该尽量避免,关键在于把这个功能从单机变成集群实现,如果不能,一般有另外两个选择:
- 给单点做好备份,能够在出现问题时进行恢复,并且尽量做到自动恢复,降低恢复需要的时间。
- 降低单点故障的范围,比如 KV 存储的时候增加 KV 服务器的数量,拆分数据,减少故障影响。
1.7.4 事务的挑战
数据库中我们有 ACID ,在单机中实现事务是比较简单的,在分布式中如何实现事务也是重要的一部分。