分布式系统
概述
定义
分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此间仅仅通过消息传递进行通信和协调的系统。
- 很多台计算机组成一个整体,一个整体一致对外并且处理同一请求,并且在这些计算机上部署了我们的组件,其分布在网络计算机上。
- 内部的每台计算机仅仅通过消息传递来通信并协调行动(rest/rpc)。
- 客户端到服务端的一次请求到响应结束会历经多台计算机。
意义
- 升级单机处理能力的性价比越来越低。CPU、内存、磁盘和网络。
- 单机处理能力存在瓶颈。
- 出于稳定性和可用性的考虑。
基础知识
从单个计算机角度看一下计算机的组成要素。组成计算机的五要素:
- 外存。计算机断电时外存的数据不会丢失。
- 内存。计算机断电时,内存中存储的数据会丢失。
- CPU。
- 输出设备。
- 输入设备。
阿姆达尔定律
程序中的串行部分对于增加CPU核心来提升处理速度存在限制
$$
S(N)=1/(1-P+P/N)
$$
P指程序中可并行部分的程序在单核上执行时间的占比。N表示处理器的个数,S(N)指程序在N个处理器相对在单个处理器中的速度提升比。
多线程模式
基于共享容器协同的多线程模式
常见的有生产者-消费者模型,这个队列会共享一个容器或数据对象,多个线程会并发地访问这个队列。
通过事件协同的多线程模式
线程间存在协调的需求,例如A、B两个线程,B线程需要等到某个状态后事件发生后才能继续自己的工作,而这个状态改变或者事件产生和A线程相关。这个场景下需要完成线程间协调。
多进程模式
分布式就类似于将单机多线程变为了单机的多进程。
- 多进程的内存空间是独立的,因此通过内存共享、交换数据的方式与多个线程间的方式就有所不同。
- 进程间的通信、协调以及通过一些事件通知或等待一些互斥锁的释放方面也会与多线程不一样。
网络通信基础知识
组件分布在网络上的多个节点中,通过消息的传递来通信并进行动作的协调,因此网络通信在分布式系统中非常重要。
网络IO
BIO、NIO、AIO。
如何从单机扩展到分布式
即使是分布式系统,也是绕不开输入、输出、运算、存储、控制的。
输入设备的变化
分布式系统由通过网络连接的多个节点组成,那么输入设备可以分为两类:
- 互相连接的多个节点,在接收其他节点传来的信息时,该节点可以看做是输入设备。
- 传统意义上的人机交互的输入设备。
输出设备的变化
- 系统中的节点在向其他节点传递信息时,该节点可以看做是输出设备。
- 传统意义上的人机交互的输出设备。
控制器的变化
分布式系统是由多个节点通过网络连接在一起并通过消息的传递进行协调的系统,控制器主要的作用是协调或控制节点之间的动作和行为。
负载均衡
基于硬件负载均衡的请求调用。所有的请求都要经过这个负载均衡设备来完成请求转发的控制,这就是一种控制的方式:
基于软件负载均衡系统。代价更低,并且可控性较强,可以相对自由地按照自己的需求去增加负载均衡的策略。
该方式一般称为透明代理,在集群当中,这种方式对于发起请求的一方和处理请求的一方来说,都是透明的,发起请求的一方会以为是中间的代理提供了服务,而处理请求的一方会以为是中间的代理请求的服务。因此发起请求的一方不用关心由多少台机器提供服务,只需要知道中间透明代理即可,但是有两个不足:
- 增加网络的开销,存在流量大与延迟的问题。
- 如果采用LVS的TUN或DR模式,那么处理的返回结果会之间返回请求服务的机器,适合于请求数据包小,处理数据包大的场景。如果请求的数据包很大,则影响较为明显。
- 延迟问题实际很小,只是存在。
- 透明代理处于请求的必经路径上,如果代理出现问题,所有请求都会受到影响。
采用名称服务的直连方式的请求调用。在请求发起方和处理方中间没有代理服务器,而是直接连接。名称服务的作用是收集提供请求处理的服务器的地址信息,提供这些地址信息给请求发送方。即名称服务起到一个地址交换的作用,在发起请求的机器上需要根据从名称服务得到的地址进行负载均衡的工作,即将原来透明代理的工作拆分到名称服务和发起请求的机器。
- 名称服务不在请求的必经路径上,即如果名称服务出现问题,依然有不少办法可以保证请求处理的正常。
- 直连方式减少了中间路径与可能的额外带宽消耗。
- 但是代码的升级较为复杂。
采用规则服务器控制路由的请求直连调用。依然是直连的方式,但是请求发起的一方选择请求处理的机器需要依赖规则服务器给的规则,发起的机器会有对规则进行处理从而选择机器的逻辑,此时规则服务器本身不会和请求处理的机器交互,只负责将规则提供给请求发起的机器。
Master+Worker的方式。使用Master节点管理任务,Master将任务分配给不同的Worker处理,其主要是任务的分配与管理,而不是请求发起与处理。
运算器的变化
概念
分布式系统中的运算器是运用多个节点的计算能力来协同完成整体的计算任务。
当依靠多台机器处理用户请求时,首先的问题就是用户应该去访问哪个服务器,有两种解决方式:
- DNS服务进行调度和控制。用户解析DNS时,就会被给予一个服务器的地址,类似于名称服务或者规则服务器的方式。
- 负载均衡。DNS返回的永远是负载均衡的地址,而用户的访问都是通过负载均衡到达后面的网站服务器。
即构成运算器的多个节点在控制器的配合下对外提供服务,构成了分布式系统中的运算器。
日志处理
当使用多台服务器收集日志并行处理,此时可以采用Master-Worker的方式进行处理:
存储器的变化
在分布式系统中,我们需要把承担存储功能的多个节点组织在一起,使其看起来是一个存储器,我们也需要通过控制器来配合完成工作。
如果要将一台KV存储服务器扩展为两台:
- 使用代理服务器,根据请求的Key进行划分。
- 使用规则服务,写明如何对数据做Sharding,并包含对具体目标KV存储服务器的地址。
- 使用Master-Worker,具体的KV选择在Master上就完成。
如果KV服务器出故障了或新增加了,应用服务器如何感知是一种较复杂的问题,尤其是对需要做持久数据服务的节点。
分布式系统的挑战
缺乏全局时钟
单机程序中,程序以单机的时钟为准,控制时序比较容易,而在分布式系统中每个节点都有自己的时钟,在通过相互发送消息进行协调时,如果仍然依赖时序,就会相对难处理。
很多时候我们使用时钟,它可以区分两个动作的顺序,而不是一定要知道准确的事件,我们可以将这个工作交给一个单独的集群来完成,通过这个集群来区分多个动作的顺序。
面对故障独立性
有可能出现某个节点或某些节点存在问题,而其他节点以及网络设备都没有问题。即一部分故障,而其他服务依然可用,即故障独立性。
处理单点故障
如果某个角色或功能只有单台单机在支撑,那么这个节点称为单点,发生的故障称为单点故障,即SPoF。一般将其变为集群实现,或者还要另外两种选择:
- 给这个单点做好备份,能够在出现问题时进行恢复,并尽量做到自动恢复,降低恢复需要用的事件。
- 降低单点故障的影响范围。
- 例如将整个交易信息存放在一个数据库,则通过Sharding进行拆分数据库,此时只有当两台数据库同时故障才会影响全部范围。
事务的挑战(数据一致性)
两阶段提交、最终一致性、BASE、CAP、Paxos等
分布式系统的瓶颈
服务框架的设计与实现
应用从集中式走向分布式所遇到的问题
要将单层Web应用的结构改为多层的、有服务层的结构时,大多人都是直接为当前要用的服务做一个RPC的功能,为服务使用者提供相关的客户端。而实际上当提供服务的集群多于一个时,通用的服务框架就非常重要了。
转化为RPC后,最关心的问题时提高易用性以及降低性能损失两方面。
服务调用端的设计与实现
要调用远程服务的过程:
- 获得可用服务地址列表。
- 确定要调用服务的目标机器。
- 可进行负载均衡。例如规则服务器、直接连接等。
- 建立连接。
- 请求序列化,并发送请求。
- 接收并解析结果。
服务调用者与服务提供者间通信方式的选择
远程通信遇到的问题
当为两台机器时,写死IP与端口就可以解决问题了。
但是在实际应用中,提供某种服务的机器一定是多台的,是一个集群,并且调用服务的机器也是集群,则此时如何解决调用者与服务提供者之间的通信问题。
采用透明代理与调用者、服务提供者直连的解决方案
我们可以通过LVS,即硬件负载均衡或者LVS的中间代理来解决该问题。
而在服务框架的设计中,采用的是基于注册中心的解决方案。
出于效率因素,并不是在每次调用远程服务前都通过这个服务注册中心来查找可用地址,而是将地址缓存在调用者本地,当有变化时主动从服务注册中心发起通知,告诉调用者可用的服务提供者列表的变化。
获得地址后,如何为当次的调用进行选择就是路由要解决的问题了。具体到负载均衡的实现上:
- 随机、轮询。对服务提供者能力对等的情况下比较合适。
- 权重。权重以动态形式,基于响应时间来计算。
引入基于接口、方法、参数的路由
一般而言会以接口作为服务的粒度,即一个服务就是指一个接口的远程实现。
一般情况下,集群会提供多个服务,每个服务有多个方法,因此除了基础的负载均衡策略,还有更加细粒度地控制服务路由的需求。
如果接口其中的某个方法A执行很慢,其明显慢于其他方法,但是调用频率与其他方法差不多。则由于线程池当中的线程数目一定,在高负载情况下,可能出现所有线程都被方法A占用的情况。
- 增加资源保证系统的能力是超出需要的。
- 隔离资源,从而使得快慢不同、重要级别不同的方法之间互不影响。
- 从客户端的角度讲,控制同一个集群中不同服务的路由并进行请求的隔离是一种可行方案。
- 即在调用者方,将调用接口A的请求路由到集群D1,将调用接口B的请求路由到集群D2。
- 采用将路由规则集中管理的方式进行管理,在具体调用者端的服务框架上先获取规则后进行路由的处理的负载均衡算法。
多机房场景
机房之间的距离和分工决定了我们应该采用什么样的架构与策略。考虑较近的同城机房。
- 服务注册中心做一些工作,通过它来甄别不同机房的调用者集群,给它们不同服务提供者的地址。
- 通过路由完成,服务注册查找中心给不同机房的调用者相同的服务提供者列表,我们在服务框架内部进行地址过滤。
服务调用端的流控处理
这里的流控是指加载到调用者的控制功能,是为了控制到服务提供者的请求的流量。
- 以固定的值表示每s可请求的次数,超过则拒绝请求。
- 设定0-1开关。
那么基于数目维度来进行控制呢?
- 根据服务端自身的接口、方法做控制,根据不同的接口、方法设置不同的阈值,保证服务端的不同接口、方法间的负载不互相影响。
- 根据来源做控制,对相同的接口、方法,根据不同来源设置不同的限制,一版用在较为基础的服务上。
序列化与反序列化处理
网络通信的实现选择
支持多种异步服务调用方式
使用NIO能够完成连接复用以及对调用者的同步调用的支持。
- Oneway。只管请求而不关心结果的方式。
- Callback。请求方发送请求后会继续执行自己的操作,等对方有响应时进行一个回调,其执行不是在原请求线程中,而是在新的IO线程中。
- Future。一种能够主动控制超时、获取结果的方式,并且它的执行仍然在原请求线程中。
- 可靠异步。一般通过消息中间件来完成。
使用Future方式对远程服务调用的优化
当我们需要在一个请求中调用多个远程服务,例如:
那么由于同步调用,我们可能要消耗很久的时间,为寻求改变:
即当请求发出去,我们统一等待服务A、B、C的执行结果,然后再进行本地的数据处理。这种方式以Future的方式进行并行优化,且底层使用NIO方式,并行没有产生额外的开销。