中间件与分布式系统


1. 分布式系统基本知识

1.1 单机系统的局限性

a) 升级单机处理能力的性价比越来越低;

b) 单机处理能力存在瓶颈;

c) 出于稳定性和可用性的考虑。

1.2 多线程协同模式

a) 基于共享容器或共享对象的协同

对于线程不安全的容器或对象,一般通过加锁或通过Copy on Write的方式来控制并发。如果读写比例很高,加锁会考虑读写锁。而对于线程安全的容器或对象,可以直接在多线程环境下使用。

b) 基于事件的多线程协同

通常涉及到多个线程需要等到某个状态或某个事件发生后才能继续执行时,就需要基于事件的协同控制。实现的方法可以通过concurrent包中的CountDownLatch、,CyclicBarrier和Semaphore等来实现。

1.3 分布式系统常见的难点

a) 全局时钟的统一

分布式系统中,每个节点都有自己的时钟,在对时钟敏感的应用中容易出现不同节点时钟不一致导致的问题。

b) 故障独立性与单点故障

分布式系统中,整个系统某一部分有问题而其他部分正常的情形下出现的情况,我们称之为故障独立性。特别是某个功能只有一个节点在处理的时候,需要考虑单点故障的影响。在分布式系统中应尽量避免出现单点,对于出现的单点服务,一是考虑给单点做备份,二是降低单点故障的影响。

c) 分布式事物的挑战

 

2 大型系统的架构演进

2.1 应用服务器扩展

应用服务器由单台扩展到集群后,需要解决的问题包括负载均衡、session管理等。用户在使用网站的服务中,通常需要与web服务器进行多次交互,但HTTP协议本身是无状态的,因此需要基于HTTP协议的会话状态保持机制。

session控制的方法主要有:负载均衡将同一session的请求发给同一节点;不同节点的session进行复制和同步;session集中存储;基于cookie的session存储等。

目前常用的方案是session集中单独存储和基于cookie的session保存。集中存储则依赖于session服务器或集群的稳定性和可用性。session存放在cookie中则面临着cookie长度限制、请求数据变大和安全性的风险,需要根据具体场景来做出选择。

2.2 数据层的扩展

数据层的扩展包括缓存使用、DB读写分离、分库分表等操作。另外对于NoSQL的使用也是减小DB压力的方式,例如针对搜索业务建立单独的搜索引擎。

缓存使用除了服务端数据缓存外,还包括页面缓存的使用,例如将页面缓存和页面渲染放在一起来处理。

数据库的扩展包括垂直拆分、水平拆分等。垂直拆分将不同业务的数据从一个库拆到多个数据库中,需要考虑跨库事务的处理,一种方式是使用分布式事务管理,或者是去掉事务。水平拆分则是将数据分布在不同的表中,但是需要解决访问SQL路由的问题,有些自增主键需要做出调整。

2.3 服务化的处理

当应用系统拆分成多个子模块后,不可避免涉及到子模块之间的相互调用,那么进一步就发展出服务化的概念,通过远程服务调用框架来实现相互访问。涉及到的变化包括:业务功能之间的访问不仅是单机内部的方法调用,还包括远程服务调用;共享代码不再分布在不同应用中,其实现被放在各个服务中心;数据库的连接也发生转变,由服务中心进行数据库的交互。

3 Java并发编程常用接口、类

3.1 线程池

使用线程池主要是复用线程,减小线程创建和销毁的代价。常见的线程池包括有ThreadPoolExecutor、ScheduleThreadPool、SingleThreadExecutor、CachedThreadPool、FixedThreadPool等。需要注意的是CachedThreadPool返回的线程池没有线程上限,所以尽量使用有固定线程上限的线程池。

3.2 常见并发工具类

a) synchronized关键字

synchronized生声明的方法或代码块是互斥的,只能有一个线程进行访问,其他线程均处于阻塞状态。

b) ReentrantLock

ReentrantLock是可重入锁,是一种递归无阻塞的同步机制,用法类似于synchronized,不过需要显式进行unlock,如果代码出现异常没有unlock,容易出现问题。当ReenTrantLock执行tryLock失败时,直接返回flase。

c) volatile关键字

volatile关键字主要是实现变量的可见性。但是并不能够控制并发,也就是说多个线程同时对volatile变量操作,不能保证线程的同步性。

d) Atomic包

Atomic包中提供了基本数据类型的原子操作,内部实现是通过CAS(循环比较)的方式实现的,属于非阻塞同步机制。

e)CountDownLatch

CountDownLatch主要提供的机制是当多个线程都达到预期状态或完成预期工作时触发事件,其他线程等待这个事件来触发自己后续的工作,而且CountDownLatch可以用来唤醒多个等待的线程。当CountDownLatch的count值为1时,就退化为一个单一事件通知了,即由一个线程来通知其他线程。

f) CyclicBrrrier

CyclicBrrrier从字面上理解是指循环屏障,即让多个线程都在屏障前等待,当所有线程都达到这个屏障时,再一起执行后面的步骤。

CountDownLatch是当多个线程都进行countDown操作后才会触发事件,唤醒await在latch上的线程,执行countDown的线程在执行完countDown操作之后会继续自己线程的工作。

CyclicBarrier是一个栅栏,用于同步所有调用await方法的线程,并且等所有线程都到了await方法时,这些线程才一起返回各自的工作。(使用CyclicBarrier的线程都会阻塞在await方法处)

g) semaphore

semaphore用于管理信号量,简单来说相当于令牌,需要控制并发的代码执行前先获取信号(acquire),执行后归还信号(release)。当没有可用的信号时,acquire操作会被阻塞,等待有线程release信号后再执行acquire。

H) future和futureTask

Future是接口,FutureTask则是其具体实现类。通常我们在调用的方法中返回一个Future对象,然后通过future.get来获取真正的返回值。在启动对远程计算结果获取的过程中,同时自己的线程还会继续执行,直到需要时再获取数据。

3.3 动态代理和反射

静态代理是为每个被代理对象构造对应的代理类。使用代理可以在调用真实方法之前和之后做其他的事情,例如记录日志、记录执行时间等等。

动态代理是动态地生成具体委托类的代理类实现对象。与静态代理不同,动态代理不需要为各委托类逐一实现代理类,通过Proxy.newProxyInstance类来创建代理方法即可为不同的委托类都创建代理类。所有代理的方法调用都会进入invoke方法中,通过invoke方法来执行所需要的逻辑。

Java的反射机制为Java本身带来了动态性。可以实现获取对象的类信息、构建对象、动态执行方法、动态操作属性等。

4. 服务框架

服务调用首先根据要调用的服务名称来获取服务的机器的地址列表,并且从可用的服务地址列表中选择一个要调用的目标机器。而这个服务名称也会直接采用接口的完整类名+版本号来作为key。

一个可调用服务基本的三个属性:interfaceName、version、group。interfaceName用来确定被调用服务的方法名称,这样才能生成对这个接口的代理,以供本地调用。version是指版本号,主要是为了解决接口有变动的情况。通过版本号来隔离新旧方法。group是指分组,可以讲提供远程服务的机器归组,然后调用者选择不同的分组来调用不同的机器,这样可以将不同调用者对于同一服务的调用进行隔离。

4.1 异步服务调用方式

异步服务调用的方式主要有:Oneway、Callback和Future三种方式。

Oneway是一种只管发送而不关心结果的方式,实现方式也比较简单,只需要将待发送的数据放入数据队列,就可以继续处理后续的任务而IO线程会负责从数据队列中取出数据并发送给远端被调用服务。

Callback方式下请求方发送请求后会继续执行自己的操作,等待对方有响应之后在进行回调操作。当IO线程收到服务提供方的人返回后,会通知回调对象来执行回调方法。

Future方式是先将Future放入Future对象队列,然后将发送数据放入数据队列,然后继续处理其他操作。等请求线程处理完其他工作之后,就通过Future对象来获取通信结果并控制超时。IO线程从数据队列中获取数据并通信,得到结果之后会将结果写入Future中。

CallBack和Future的区别在于CallBack是一种被动的方式,其执行不在原请求线程中。Future是一种能够主动控制超时、获取结果的方式,并且它的执行仍然在原请求线程中。

4.2 服务升级

 

5. 数据访问层

5.1 垂直拆分与水平拆分的影响

垂直拆分:

1)单机的ACID被打破,原来在单机中的数据可能分布在多个节点上,需要分布式事物来保证;

2)一些连表操作会变得困难,因为数据可能分布在不同的两个库中,需要通过应用来解决查询的问题;

水平拆分:

1)单机ACID被打破;

2)Join操作的影响;

3)单库自增序列中唯一Id的冲突;

4)跨表查询;

5.2 分布式事物

分布式事务中三个组件:AP(应用程序)、RM(资源管理)、TM(事务管理)。

两阶段提交:用于分布式事物处理中,在提交之前增加了准备阶段,所以称为两阶段提交。只有在准备阶段全部执行成功后,才会继续执行第二阶段的提交。

5.3 CAP理论

6. 消息中间件

消息中间件为系统带来了异步处理的特性,在系统间做到了解耦,对大型分布式系统具有非常重要的意义。

6.1 消息发送一致性问题

消息发送一致性是指产生消息的业务动作与消息发送的一致性,即业务操作成功了,那么由这个操作产生的消息一定要发送出去,否则就是丢消息了。

通常为保证业务操作和发送消息的动作一致的,可通过一下逻辑来实现:

1)业务系统将消息发给中间件,发送时标记消息为未处理;

2)消息中间件收到消息后,把消息存储下来,并返回存储结果给业务系统;

3)也无妨接收到消息中间件的返回结果,并根据返回结果判断是否执行业务逻辑;

4)业务操作完成,发送业务操作结果给中间件,中间件根据业务操作结果判断是否更新存储的消息状态为可发送;

5)对于更新状态为可发送的消息进行调度和投递。

6.2 消息模型的设计

消息模型一般有Queue(点对点)和topic(发布/订阅)两种模型。Queue队列中,消息从发送端发送出去不能最终确认是哪个应用节点消费的,但是能保证只有一台节点消费消息。Topic模型中,订阅同一Topic的所有节点都能接收到该Topic中的消息。

通常来讲,可以将集群预计群间的消息的消费做成Topic模型来处理,而集群内部各个应用实例对消息的消费通过Queue队列来处理。

6.3 消息可靠性保证

消息从发送端到接收端的过程中,可靠性保证分为三个阶段:从发送端到消息中间件、消息中间件对消息存储、消息投递给接收者。

发送端的可靠性保证可以通过中间件返回的结果来判断,只有消息中间件及时、明确返回成功信息,才能确保消息可靠到达中间件。存储的可靠性保证需要存储系统的稳定和持久,可以考虑DB、NoSQL等持久化存储系统。消息投递的可靠性保证需要显示收到接收者确认消息处理完毕的信号才能删除消息。

投递过程的优化:把投递消息后返回结果的处理过程放在另外的线程池中来处理,保证投递线程完成消息的投递后可以继续处理下一个消息的投递。等待返回结果的消息则先放在内存中,不占用线程资源。

6.4 消息重复的产生和应对

消息重复的原因包括有发送端重复投递、消息中间件重复投递等原因。一个解决办法是重试发送消息时使用同样的消息id,而不是每次发送重新生成消息id。接收方处理消息中间件发送过来的消息时要求做到幂等设计,即对于重复投递消息的处理也能产生正常操作结果。

7 软负载中心和集中配置管理

7.1 负载中心基本职责

软负载中心两个最基本的职责:聚合地址信息、服务上下线感知。聚合地址信息主要是形成一个可供服务调用者和消息发送方、接收方直接使用的地址列表。服务上下线感知则是能根据服务的上下线自动更新服务地址数据,并将数据传给所需要的调用方。

软负载中心也包括服务端和客户端两部分。服务端职责在于监控服务是否在线、聚合服务提供方的机器信息、并把地址数据传给适用方。客户端一部分是将服务的具体信息传给服务端,当服务发生变化后及时同时服务端;另一部分是从服务端获取需要的服务地址列表,并进行本地缓存。

服务端存储数据信息:聚合数据、订阅关系、连接数据。

聚合数据:即聚合后的地址信息列表,通常根据dataId、group来组成一个二维结构,并唯一定位到一个数据结构。

订阅关系:客户端从服务端获取需要的服务地址,即为订阅关系。订阅的粒度和数据聚合粒度一致,均是通过dataId和groupId来确定数据,形成一个dataId、group到数据订阅者的分组Id的映射关系。

连接信息:是指客户端节点和负载的服务端已经建立的连接的管理。

消息中间件中,同一集群的不同机器是分享所有消息的,因为消息只要在这个集群的某一台节点上去处理就可以了;而负载中心在进行地址数据分发时需要将数据分发给所有的机器。

7.2 集中配置管理

软负载中心管理非持久数据,例如服务地址列表;配置管理中心则是管理持久数据,例如持久订阅关系、路由分发规则等等。集中配置管理中心最为关心的是稳定性和各种异常状况下的容灾策略、其次才是性能的数据的分发效率。

客户端与配置管理中心的连接是通过HTTP短连接方式通信的。相较于socket长连接的通信方式,HTTP是一种轮询的策略。那么为减小服务端压力,轮询的间隔不能设置太短,这样引申出一种长轮询的机制。连接建立后,如果有数据那么立即返回数据,如果没有数据,长轮询会等待,直至拿到数据或超时。

配置管理中心的容灾设计考虑以下策略:客户端有数据缓存、数据快照、本地配置文件等方式。服务端则除了本地配置文件、缓存数据之外,还考虑数据库的存储与备份。

8 其他要素概念

分布式文件系统:最著名的是谷歌的GFS(google FileSystem),由三部分构成:client,master,chunkServer。HDFS系统也是采用类GFS的实现方式。

client负责从master上获取要操作的文件在chunkServer中的具体地址,然后和chunkServer直连通信,进行数据操作。

master可认为是文件系统大脑,维护文件系统的数据信息,还包括与chunkServer的心跳监控,检测是否在线等等。

chunkSever主要通过chunk(数据块)存储文件,每个chunk固定大小,超过size的文件分为多个chunk存储;而比较小的文件则是多个文件保存在同一个chunk中。

缓存系统:主要有redis、memcache两类,redis有点是天然分布式集群设计,而Memcache本身还是一个单机应用,使用时如果要做成集群化一般还需要采用一致性Hash来实现。

搜索系统:全网搜索一般基于爬虫的方式来实现,而内部搜索则是通过自己维护索引数据来实现,例如ES搜索等。

倒排索引:把原来作为值的内容拆分为索引的key,而将原来用作索引的key变成值。一般如何建立倒排索引的关键字主要取决于如何对索引数据进行分词。

你可能感兴趣的:(中间件与分布式系统)