小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
分布式理论
分布式系统是一个硬件或软件分布在不同的网络计算机上,彼此之间通过消息传递进行通信和协调的系统
面临问题
- 通信异常: 网络的不可靠性,存在延时
- 网络分区: 脑裂
- 三态: 成功, 失败和超时
- 节点故障: 宕机, 僵死现象
一致性
-
强一致性
最符合用户直觉,系统写入什么,读出什么
-
弱一致性
约束系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据达到一致, 但会尽可能地保证到某个时间级别后数据达到一致
-
最终一致性
弱一致性的一个特例,系统保证在一定时间内,能够达到一个数据一致的状态。
分布式事务
分布式事务指事务的参与者, 支持事务的服务器, 资源服务器以及事务服务器分别位于分布式系统的不同节点上, 通常一个分布式事务会涉及多个数据源或业务系统的操作。
CAP
Consistency 一致性
Availablility 可用性
Patition tolerance 分区容错性
分布式系统只能满足其中两个条件。
如果一个系统由两个节点配合组成, 之间通过网络通信, 节点A更新同时更新节点B。 如果发生了网络分区, 一致性无法满足, 如果强行满足一致性,必须停止服务,放弃可用性。
CA:所有数据放在一个分布式节点上, 放弃了系统的可扩展性。
CP: 放弃可用性, 如果网络分区,受到影响的服务会等待一段时间,在等待时间内无法提供服务
AP: 当出现网络分区, 必须让节点继续对外服务,失去一致性,这个一致性是强一致性, 保留数据的最终一致性。能够承诺的是数据最终达到一致性,引入了时间窗口的概念,取决于系统的设计。
BASE
Basically Available Soft State Eventually consistent
既然无法做到强一致性, 但每个应用可以根据自身的业务特点, 采用适当的方式来使系统达到最终一致性。
Basically Available 基本可用
分布式系统在出现不可预知的故障,允许损失部分可用性。
Soft State
允许系统中的数据存在中间状态, 并认为此状态不影响系统的整体可用性,系统在多个节点的数据副本之间进行数据同步的过程中存在延迟
Eventually consistent
最终一致性强调系统中所有的数据副本在一定时间的同步后,最终能够达到一致的状态。
最终一致性存在五种主要变种
-
因果一致性(Causal consistency)
节点A更新完数据后通知了节点B, 节点B之后对数据的访问和修改基于节点A更新后的值。 和节点A无因果关系的节点C的数据访问没有这样的限制。
-
读己之所写(Read your writes)
节点A更新完一个数据后,自身总能读到自己更新的最新值
-
会话一致性(Session consistency)
执行更新操作后, 同一个有效会话中始终读取到数据项的最新值
-
单调读一致性(Monotonic read consistency)
如果一个节点从系统中读取出一个数据项的值, 那么对于该节点后续的任何数据访问不应该返回更旧的值
-
单调写一致性(Monotonic write consistency)
一个系统保证来自同一个节点的写操作被顺序的执行。
2PC
两阶段提交, 一致性协议, 用来保证分布式系统数据的一致性。主要有协调者和参与者两个角色。强一致性事务。
阶段一 投票
- 事务询问 协调者向参与者发送事务内容 询问是否执行事务提交操作
- 执行事务 各节点执行事务操作, 将undo和redo信息记入事务日志
- 各参与者向协调者反馈事务询问的响应
阶段二
两种情况
事务提交:
- 发送提交请求
- 事务提交
- 反馈事务提交结果
- 完成事务
中断事务:
- 发送回滚请求
- 事务回滚
- 返回事务回滚结果
- 中断事务
优缺点
优点: 原理简单,实现方便
缺点:
-
同步阻塞
各参与者等待其他参与者响应过程中无法进行其他操作
-
单点问题
协调者的单点问题
-
数据不一致
如果commit的过程中,有部分参与者崩溃或者网络异常, 就会数据不一致
-
过于保守
提交询问阶段, 参与者出现故障, 协调者无法获取所有参与者响应信息, 协调者只能根据超时机制判断是否需要中断事务。过于保守,没有设计完善的容错机制
3PC
CanCommit
-
事务询问
协调者向所有的参与者发送包含事务内容的canCommit请求, 询问是否可以执行事务提交操作,等待响应。
-
各参与者向协调者反馈事务询问的响应
PreCommit
执行事务提交:
- 发送预提交请求
- 事务预提交
- 各参与者向协调者反馈事务执行的结果
中断事务:
- 发送中断请求
- 中断事务
doCommit
执行事务提交:
- 发送提交请求
- 事务提交
- 反馈事务提交结果
- 完成事务
中断事务:
- 发送中断请求
- 事务回滚
- 返回事务回滚结果
- 中断事务
一旦进入阶段三,可能出现两种故障
- 协调者出现问题
- 协调者和参与者之间的网络故障
出现了任一一种情况, 都会导致参与者无法收到doCommit请求或者abort请求,参与者在等待超时后,继续进行事务提交。
优缺点:
优点: 降低了参与者的阻塞范围(第一个阶段不阻塞), 在单点故障后继续达成一致(2PC提交阶段出现问题, 3PC根据协调者的状态进行回滚或者提交)
缺点: 参与者收到preCommit后, 出现网络分区,参与者等待超时后会进行事务提交, 必然出现分布式事务不一致问题。
3PC对比2PC
对协调者和参与者都设置了超时机制(2PC 中,只有协调者拥有超时机制). PreCommit是一个缓冲,保证了最后提交阶段之前各节点状态一致。
Paxos
概念
提案(Proposal): 包含提案编号和提议的值
角色:
- Proposer 提案发起者
- Acceptor 决策者 可以批准提案
- Learner: 最终决策的学习者
一个进程可能既是Proposer,也是Acceptor, 也是Learners。
Proposer: 只要Proposer发起的提案被半数以上Acceptor接受
Acceptor: 只要Acceptor接受提案
Learner: 只要Acceptor告诉Learner
算法描述
阶段一
-
Proposer选择一个提案N, 然后向半数以上的Acceptor发送编号为N的Prepare请求
-
如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的Prepare请求的编号,那么它会将它已经接受的最大的提案作为响应反馈给Processor,同时承诺不再接受任何编号小于N的提案。
阶段二
- 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它会发送针对[N,V]提案的Accept请求给半数以上的Acceptor。
- 如果Acceptor收到一个针对N的提案的Accept请求, 只要该Acceptor没有对编号大于N的Prepare请求作出过响应,它就会接受提案。
Learner 学习被选定的Value
- 第一种, Acceptor选定后直接通知 M(Acceptor) * N(Proposer)
- 第二种, Acceptor选定后通知一个主learner, 主learner通知其他learner。
- 第三种, Acceptor选定后通知一个learner组, 由这个组通知其他learner。
如何保证Paxos的活性
活性: 最终一定要选定value
通过选取主Proposer, 规定只有主Proposer才能提出议案。 只要主Proposer和过半的Acceptor能够正常进行网络通信, 整个Paxos算法就能保持活性。
Raft
raft是为了管理复制日志的一致性算法。
领导人选举
角色:
- 领导者
- 候选者
- 跟随者
影响角色变化的是选举,raft通过心跳机制触发选举, Server启动时,初始状态都是follower, 每个server有定时器, election timeout。 如果server没有超时的情况下收到来自领导者或者候选者的任何消息,定时器重启,如果超时, 开始一次选举。
日志复制
Lader选出后,接收客户端的请求, leader把请求作为日志条目(Log entries) 加入日志中, 并行的向其他服务器发起AppendEntries RPC 复制日志条目, 当日志被复制到大多数服务器上, Leader将这条日志应用到它的状态机并向客户端返回执行结果
分布式系统设计
心跳机制
-
保证可靠性
-
应对异常机制
-
周期检测心跳机制
Server端每间隔t秒向Node集群发起监测请求
-
累计失效检测机制
在周期检测心跳机制基础上,统计一定周期内节点的返回情况, 计算'死亡'概率。进行有限次重试
-
高可用设计
-
主备
Active-Standby模式。
-
互备
MM模式, 每个Master都具有read-write能力。
-
集群
多个节点运行, 主控节点分担服务请求。
容错
保证分布式环境下响应系统的高可用或者健壮性
负载均衡
硬件负载: F5
软件负载: HAProxy Nginx LVS
主要策略:
- 轮询
- 最小连接
- IP地址哈希
- 基于权重
分布式架构网络通信
RPC
RPC: Remote Procedure Call,远程服务调用,Remote Procedure Call Protocol 是一个计算机通信协议, 它允许像调用本地方法一样调用远程服务, 由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
作用:
- 屏蔽组包解包
- 屏蔽数据接收发送
- 提高开发效率
- 业务发展的必然产物
核心组成:
- 远程方法对象代理
- 连接管理
- 序列化/反序列化
- 寻址与负载均衡
RPC调用方式:
- 同步调用
- 异步调用
RMI
Remote Method Invocation, 是java 原生支持的远程调用, 采用JRMP(Java Remote Messaging protocol)作为通讯协议。主要用于不同虚拟机之间的通信。
客户端
- stub: 远程对象在客户端上的代理对象
- Remote Reference Layer: 解析并执行远程引用协议
- Transport: 发送调用, 传递远程方法参数, 接收远程方法执行结果
服务端
- Skeleton: 读取客户端传递过来的方法参数,调用实际的方法
- Remote Reference Layer: 处理远程引用后向骨架发送远程方法调用
- Transport: 监听客户端的入站连接,接收并转发调用到远程引用层。
注册表
以url 形式注册远程对象,并向客户端回复对远程对象地引用。
代码实现
- 服务端
/***
* 必须继承Remote
*/
public interface IHelloService extends Remote {
/**
* 必须抛出RemoteException
* @param name
* @return
* @throws RemoteException
*/
public String sayHello(String name) throws RemoteException;
}
/**
* 必须继承UnicastRemoteObject
*/
public class HelloService extends UnicastRemoteObject implements IHelloService {
protected HelloService() throws RemoteException {
super();
}
@Override
public String sayHello(String name) throws RemoteException {
return "abc";
}
}
public class RmiServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
IHelloService helloService = new HelloService();
LocateRegistry.createRegistry(8888);
Naming.bind("rmi://localhost:8888/rmiserver", helloService);
}
}
- 客户端
public class RMIClient {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
IHelloService helloService = (IHelloService)Naming.lookup("rmi://localhost:8888/rmiserver");
System.out.println(helloService.sayHello("dad"));
}
}
BIO ,NIO, AIO
同步和异步
同步和异步是指应用程序和内核的交互而言的。
同步: 用户进程触发IO操作等待或者轮询的方式查看IO操作是否就绪
异步: 当一个异步进程调用发出之后, 调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态, 通知来通知调用者,或者通过回调函数来处理这个调用。
阻塞和非阻塞
阻塞和非阻塞针对进程访问数据的时候, 根据IO操作的就绪状态来采取不同的方式。简单来说就是读写操作方法的实现方式。阻塞方式下读取和写入将一直等待, 非阻塞方式下,读取和写入方法会立即返回一个状态值。
BIO
同步阻塞IO, 处理并发请求的时候很慢。
serverSocket().accept 和socket.getInputStream().read(bytes)两个阻塞
NIO
同步非阻塞IO
服务器实现模式为一个请求一个通道, 客户端发送的连接请求都会注册到多路复用器上, 多路复用器轮询到连接有IO请求时才会启动一个线程进行处理
- 通道(Channel)
- 缓冲区(Buffer)
- 选择器(Selector)
public class NIOClient {
public static void main(String[] args) {
InetSocketAddress address = new InetSocketAddress("127.0.0.1",8888);
SocketChannel channel = null;
ByteBuffer buffer = ByteBuffer.allocate(1024);
try{
channel = SocketChannel.open();
channel.connect(address);
Scanner sc = new Scanner(System.in);
while (true){
String line = sc.nextLine();
if(line.equals("exit")){
break;
}
buffer.put(line.getBytes(StandardCharsets.UTF_8));
buffer.flip();
channel.write(buffer);
buffer.clear();
int readLen = channel.read(buffer);
if(readLen ==-1){
break;
}
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String(bytes));
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(null != channel){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class NIOServer implements Runnable{
//多路复用器
private Selector selector;
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
public NIOServer(int port){
init(port);
}
public static void main(String[] args) {
new Thread(new NIOServer(8888)).start();
}
private void init(int port) {
try{
this.selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port));
serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void run() {
while (true){
try{
this.selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey key = keys.next();
keys.remove();
if(key.isValid()){
if(key.isAcceptable()){
accept(key);
}
if(key.isReadable()){
read(key);
}
if(key.isWritable()){
write(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key) {
this.readBuffer.clear();
SocketChannel channel = (SocketChannel)key.channel();
Scanner scanner = new Scanner(System.in);
try{
String line = scanner.nextLine();
writeBuffer.put(line.getBytes("UTF-8"));
writeBuffer.flip();
channel.write(writeBuffer);
System.out.println(line);
channel.register(this.selector, SelectionKey.OP_READ);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(SelectionKey key) {
try{
this.readBuffer.clear();
SocketChannel channel = (SocketChannel)key.channel();
int readLen = channel.read(readBuffer);
if(readLen ==-1){
key.channel().close();
key.cancel();
return;
}
this.readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
System.out.println(new String(bytes, "utf-8"));
channel.register(this.selector, SelectionKey.OP_WRITE);
} catch (IOException e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try{
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(this.selector, SelectionKey.OP_READ);
}catch (IOException e){
e.printStackTrace();
}
}
}
AIO
异步非阻塞IO, 需要一个连接注册读写事件和回调的方法。
使用场景在于 连接数目多且连接比较长的架构, 比如相册服务器,需要OS参与
Netty
优点
对传输协议提供统一的api
高度可定制的线程模型: 单线程, 一个或多个线程池
更好的吞吐量, 更低的等待延迟
更少的资源消耗
最小化不必要的内存拷贝
线程模型
BossGroup 线程池负责接收客户端连接, WorkerGroup负责网络读写操作。NIOEventLoop 表示不断循环执行处理的线程,每个NIOEventLoop都有一个selector, 用于绑定socket网络通道。NIOEventLoop采用串形化设计,从消息读取-> 解码-> 处理->编码-> 发送,始终由NIOEventLoop负责。
核心组件
-
ChannelHandler
ChannelInboundHandler ChannelOutboundHandler
-
ChannelPipeline
一个Handler的集合, 负责处理和拦截inbound或者outbound的事件和操作, 相当于贯穿Netty的链。
-
ChannelHandlerContext
上下文对象, 实际处理节点,每个节点中包含一个具体的事件处理器ChannelHandler,绑定了对应的pipeline和Channel信息,方便对ChannelHandler进行调用
-
ChannelFuture
Channel异步I/O操作的结果
-
EventLoop 和EventLoopGroup
每个EventLoop维护着一个Selector实例。主要有两个EventLoopGroup : BossEventLoopGroup和WorkerEventLoopGroup
-
ServerBootStrap 和BootStrap
启动助手
代码
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
ChannelFuture future = new ServerBootstrap().group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
ChannelPipeline pipeline = nioSocketChannel.pipeline();
pipeline.addFirst(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String o) throws Exception {
System.out.println(o);
}
});
}
}).bind(9999).sync();
future.channel().closeFuture();
}
}
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 9999).channel();
while (true){
channel.writeAndFlush("hell");
Thread.sleep(2000);
}
}
}