java-concurrent-JMM

final

  • 构造函数中对象的final字段赋值,与随后将该对象引用赋值,两者不能重排序
  • 读取某个对象,与读取对象final的字段,两者不能重排序

Paxos那些事儿

为何prepare请求要过半返回才算成功?

  • 可终止性:一旦有过半节点的为同一个状态,则集群的状态确定,以后无法改变;
  • 获取Accept请求锁:集群任何时刻,Accept只能接受一个版本号,即最近成功请求prepare的版本号;

为何要使用最新的版本号?

  • Paxos的约定:分歧状态下选取最新的提案广播
  • Acceptor的目的就是获取集群最新状态
  • Paxos选举是一个过程,从集群的多种状态最终衍变到唯一状态,后者认同前者

Paxos选举的原子性?

  • Prepare、Accept通过序号锁定一次选举:
    • 串行执行:P1————>o|P2————>o|A1- - - - ->o|A2————>o

Paxos选举的生命周期

  • 如果出现大于自己之前收到的序号,说明选举仍然在继续
  • 历史终究是历史,历史不能改变,能改变的只有未来

Paxos选举过程的中关键节点

  • 非空未过半
  • 非空过半
  • 相同状态未过半
  • 相同状态过半————结束

Paxos、ZAB、Raft三者关系

  • 个人认为Paxos与ZAB、Raft没有什么关系,因为ZAB、Raft是有Leader的角色,提案广播是基于顺序的,过程中某一提案是否最终提交可以依赖于下一次广播;而Paxos理论上任何节点都可以发起提案,而每一次选举是独立的。
  • 如果硬要说有什么相似之处,那么ZAB选Leader的过程与Paxos是非常相近的,区别就在于约定不同,Paxos的前提是选取最新提案而不关心提案的值,而ZAB选择提案是根据提案的值来确定,因此Paxos需要一个版本号,而ZAB不需要,有人会说ZAB也有版本号的东东,其实在Paxos的世界里那个不是提案的版本号,是提案的值。
  • Raft选Learder的做法似乎与Paxos和ZAB都不同,选Leader不关心版本号,也不关心提案的值,每个Raft节点都会发起选举请求,为了避免冲突,每个节点sleep一个随机值,谁先获取过半数响应谁就是Leader;这么选Leader后log的index不一样怎么办?很简单,选完Leader后由Leader进行同步,这有点意思。不过Raft在多节点集群以及较差网络环境下进行Leader选举似乎会比较困难了。

ZAB、Raft协议的可终止性实现

  • Paxos基于读写串行操作,终止性保证容易理解。对于ZAB和Raft,虽然Leader选举算法不同,但是关于可终止性实现上有相似之处。每个节点刚自认为被选为Leader之后,会给它投票的过半集合发同步请求,如果同样收到了过半集合的Ack,,则Leader取得了其他节点的认可真正成为了Leader,选举结束了。

那些事儿

  • 相同状态未过半时的集群状态
    • 实际的状态
    • “我眼中的状态”
  • 最大提交原则:冲突状态时某次提案获得Accept响应的节点越多,本次提案的值越有可能成为过半数节点的值,即集群最终值。

分布式一致性协议-Paxos

Paxos简述

paxos协议可能难以理解,但是并不复杂,简单来说主要基于以下几个原则:

  • 少数服从多数
  • 抢占式访问
  • 后者认同前者

基本工作流程

关键词解释

  • 角色:Porposer -> 提案发起者,Acceptor -> 提案批准者
  • 序号:每个提案都有唯一序号,序号按时间顺序生成,Acceptor只会接受最新序号的也就是时间最新的提案请求

流程

  • prepare阶段:Proposer使用序号s向过半数的Accepor发送prepare请求,Acceptor收到请求后检查序号是否符合要求,一旦符合要求Acceptor则会响应请求表示通过。如果之前没有通过提案,响应中不包含任何提案信息,如果之前通过某些提案,则相应中包含最大序号的提案。

  • accept阶段:如果Proposer收到了半数以上Acceptor的响应则发出accept请求,请求内容包含之前prepare请求的序号,以及本次提案的值,如果prepare阶段半数以上Acceptor响应的提案全部为空,则本次accept请求的值为Proposer要提交提案的值,如果prepare阶段过半数响应中有非空提案,则本次accept请求的值为提案集合中序号最大提案的值。Acceptor收到请求后仍然只需要检查序号是否符合要求,只要序号符号要求就会通过提案响应请求。

  • commit阶段:如果Proposer收到了来自半数以上Acceptor的accept请求响应,则发出commit请求给每个Acceptor,通知所有Acceptor提交之前通过的提案。

官方阐述

官方阐述

RPC框架-Dubbo

Dubbo简介

dubbo是阿里巴巴开发的RPC框架,虽然阿里集团内部使用dubbo并不多,但是近年来dubbo被很多其他互联网厂商广泛使用,这也许就是所谓墙内开花墙外香吧。虽然它并不完美,(如果完美可能就不会有HSF了吧),但是它有足够的理由让大家认可。

  • 足够健壮,有多年生产环境实践经验;
  • 足够开放,它所依赖的都是开源且流行的组件;
  • 足够灵活,基于插件式架构,可以根据使用场景灵活配置协议和运输层组合。

关于Dubbo的默认协议

Dubbo的默认协议即Dubbo协议,它使用单TCP链接的Hessian序列化协议,Hessian虽然性能不及ProtocolBuffer、Thrift,但其不需要中间解释文件,相对灵活,对于一般公司来说已经足够。至于单链接也许会存在争议,有人可能会说单链接存在带宽问题,其实如果不是某个请求发送大文件,业务之间不会相互影响,而且性能略好,但是如果网络环境较差再加上业务量陡增的话就悲催了。

架构简析

对于RPC框架的服务端来说,主要功能无非有两个,即:数据传输 + 协议。
广义上Dubbo和Netty同属于RPC框架的范畴,两者对比来看:

  • 从架构角度来说,微内核的Dubbo为Netty、Mina、Jetty等组件以及Hessian、Thrift等协议提供了平台,即Netty等提供数据传输功能,Hessian等提供协议功能,这样可以使各种方式灵活搭配以满足不同场景需要。
  • 从功能上来说,Dubbo是Netty的扩展。Dubbo自身不具备数据传输功能,需要借助Netty等组件实现,两者关于协议功能的实现都是通过自己的责任链完成的,当Dubbo使用Netty作为运输层组件时,Netty的责任链基本没有做任何事情,将接收到的二进制数据全部提交到上层Dubbo的责任链负责协议解码,除此之外,Dubbo的责任链还有负载均衡、错误重试、降级、限流等功能。

疑问

  • 对于一个RPC框架来说真的需要这样的插件式架构吗,对于大部分项目来说,只要选中了一个插件组合基本上就不会再频繁变动,而Dubbo提供了如此多的选择,如此多的重复功能的插件,对于用户来说大部分都是没有用的,而架构上又要满足不同插件的不同特性做适配,导致框架整体上确实太重了。个人认为RPC框架选择了一种协议或者网络框架之后应该在此之上做精做细,而不是把能做RPC框架的所有插件都集成进来。

Epoll模型

epoll提供的接口

epoll提供了3个系统调用

  • epoll_create;创建一个epoll
  • epoll_ctl:将文件句柄添加到epol中,并注册相关事件到文件句柄
  • epoll_wait:返回活跃文件句柄

实现机制

主要数据结构

  • 句柄树:为方便查找,epoll使用二叉树存储所有句柄;
  • 句柄数组:epoll在用户空间创建句柄数组,用来存放当前活跃句柄。

流程

  • 当句柄状态发生变化时会主动将句柄添加到用户空间的句柄数组中;
  • 当调用epoll_wait时,epoll返回句柄数组的内存地址,用户通过内存地址就可以获取当前活跃的句柄信息。

ET与LT模式

ET:边缘触发、LT:水平触发
最初epoll只提供了ET模式,因为ET模式对于用户来说难以操作,才妥协推出了性能稍低LT模式,那就看一下两者的区别;

区别

ET模式下,只有某个描述符的状态变为可用后(不可读变为可读/不可写变为可写),调用epoll_wait才会返回该描述符。LT模式下,只要当某个描述符处于某种状态时(可读/可写),每次调用epoll_wait都会返回该描述符;
当用户使用ET模式时,需要在两次epoll_wait调用之间将活跃的描述符一次性将数据读完,或者一次性将数据发完/填满,或者需要在用户代码中保存句柄状态。

性能对比

从性能因素考虑,ET模式效率会高一些,因为活跃的描述符会存放在数组当中,LT模式下每当用户IO操作完成之后,要检查描述符的状态来确定是否删除数组中的描述符;而ET模式只要用户epoll_wait之后,扫过数组就会删除描述符;

Mina-IO线程模型

Accept事件处理

  • 继承关系:NioSocketAcceptor -> AbstractPollingIoAcceptor -> AbstractIoAcceptor -> AbstractIoService

NioSocketAcceptor

  • selector

  • init():初始化NIO.selector

  • open():将Accept事件注册到selector
  • select():执行NIO.selector.select()

AbstractPollingIoAcceptor

  • Acceptor -> Runnable:run()
  • SimpleIoProcessorPool -> IoProcessor

  • bindInternal():调用startupAcceptor(),执行Accept.run();

  • Accept.run():调用open()、select(),将select()返回的SelectionKey通过accept(IoProcessor)初始化为NioSocketSession,随后初始化session,并将session绑定到processor(最终绑定processor的逻辑在SimpleIoProcessorPool.add(),该方法调用NioProcessor数组中某个NioProcessor的add方法将session与其绑定,第一次调用NioProcessor.add时将启动processor线程)

Read/Write事件处理

  • 继承关系:NioProcessor -> AbstractPollingIoProcessor

SimpleIoProcessorPool

  • NioProcessor[]:Processor数组,默认大小为CPU核心数量+1;

NioProcessor

  • selector

  • init();打开selector;

  • select():调用selector的select();
  • selectedSessions():返回selector.selectedKeys()方法;
  • registerNewSelector():处理当程序掉入nio坑里时,将老selector中所有keys注册到新selector上面;

AbstractPollingIoProcessor

  • Processor -> Runnable

  • add():将新session加入到Queue,随后检查如果第一次被调用的话则启动startupProcessor()方法执行Processor.run();

  • Processor.run():执行select(),判断是否掉到nio坑里,随后处理newSession,初始化newSession并绑定Read/Write事件到selector;然后调用process开始处理活跃的session,再调用flush(),将需要发送的数据交给TCP
  • process():遍历活跃的session集合调用process(session)处理每个session;
  • process(session):判断如果是可读状态的session则调用read(),如果可写状态则将session加入flushingSessions队列;
  • read():将数据读取后调用IoFilterChain.fireMessageReceived方法,此时数据将通过Mina的filters处理数据,最终交给handler处理。
  • flush():遍历flushingSession取出session,调用flushNow(session),如果还有未发送的数据则需要调用scheduleFlush(session),“//Kernel buffer is full.”;
  • flushNow(session):取出session中的writeRequestQueue,发送队列里面的数据;
  • scheduleFlush(session):将session重新加入到flushingSession中;

session

NioSocketSession -> NioSession -> AbstractIoSession

Mina-写异步实现

摘要

异步Io是Mina等网络的框架的重要功能,它大大降低了用户操作网络IO的复杂度,这其中由于TCP的协议特点,通过TCP发送数据的异步实现更是尤为重要,本文就简述一下Mina写异步的代码实现。

发送数据时的代码流程

调用session.write() -> 调用session中的filterChain.fireFilterWrite(WriteRequest) -> 从TailFilter一直到HeadFilter,在HeadFilter中调用processor.write方法 -> AbstractPollingIoProcessor.write:将session列入flushingSession当中,随后的IO线程将发送flushingSession中session的WriteRequestQueue所有WriteRequest。

WriteFuture

因为是异步操作,所以Future是必不可少的,Mina提供了WriteFuture作为WriteRequest发送成功后的通知。WriteFuture被组合在WriteRequest当中,并在调用session.write()时返回。
DefaultWriteFuture -> DefaultIoFuture

WriteFuture阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public IoFuture await() throws InterruptedException {
synchronized (lock) {
while (!ready) {
waiters++;
try {
// Wait for a notify, or if no notify is called,
// assume that we have a deadlock and exit the
// loop to check for a potential deadlock.
lock.wait(DEAD_LOCK_CHECK_INTERVAL);
} finally {
waiters--;
if (!ready) {
checkDeadLock();
}
}
}
}
return this;
}

当IO线程将WriteRequest发送完成时,调用filterChain的fireMessageSent(),唤醒等待的线程。
WriteFuture释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void setValue(Object newValue) {
synchronized (lock) {
// Allow only once.
if (ready) {
return;
}
result = newValue;
ready = true;
if (waiters > 0) {
lock.notifyAll();
}
}
notifyListeners();
}

Mina之责任链模式实现

数据结构

DefaultIoFilterChain

  • EntryImpl head;
  • EntryImpl tail;

Mina的责任链数据结构主要是一个双向链表,链表项为Entry的实现EntryImpl,DefaultIoFilterChain中保存了链表的head和tail。

EntryImpl

  • IoFilter filter;
  • EntryImpl prevEntry;
  • EntryImpl nextEntry;
  • NextFilter nextFilter;

EntryImpl是IoFilter的包装,还提供了前后节点的链接。需要关注的是NextFilter类,由于责任链的两端都可能触发事件,不同事件在链中的传递方向不同,所以IoFilter的实现需要根据不同的事件来判断下一个需要接收事件的Entry是在哪个方向,而Nextfilter正是将这部分逻辑从filter中分离出来,Nextfilter中包含了双向链表中两个方向所有可能发生的事件类型,这样IoFilter只需要根据不同事件调用NextFilter中不同方法即可。

IoFilter

IoFilter是责任链模型中的每个节点主要代码需要实现的接口,包含messageReceive、messageSent等方法。

方向

  • write: tail -> head、previous
  • receive: head -> tail、next

机制

TCP的那些事

开始

这篇文章主要总结一些个人认为TCP中比较重要又容易引起混淆的理论

延迟ack

因为tcp协议是可靠的协议,接收端需要告诉发送端目前它收到了哪些包以及哪些包需要重传,ack就是用来是用来对tcp数据包的确认。简单来说,接收方每接受一个数据包就应该响应一个ack,但是大多数tcp实现认为这样做没必要,它们的做法是累计确认,而且最多只累积确认两个数据包。也就是说,假设A向B发送了10个数据包,而且数据包都是正常按序到达,B接收数据包过程中只需要间隔响应5个ack就可以了。

至于延迟ack的意义,除了上文提到的可以提高ack的传输效率,还可以进行“捎带”操作。

延迟ack似乎也只是在正常情况下才会使用,那什么情况下接收方不会累计确认而会立即响应ack呢?看一下ack的发送时机:

  • 首先,延迟的ack,当收到第一个数据包并等待第二个数据包传输时,等待超过50ms则立即发送ack
  • 其次,累积确认的ack,第一次收到的数据包未响应ack,第二次收到数据包时则立即发送ack
  • 发现有失序到达的数据包时,立即响应冗余ack
  • 发现有比失序到达的数据包序号更小的数据包时,立即响应ack

拥塞控制

  • CongWin:拥塞窗口大小,已经发送但还未收到ack的数据包数量;
  • 慢启动:当tcp开始传输时CongWin大小为1,每收到一个ack后CongWin++,即每过一个RTT之后CongWin = CongWin*2;
  • 拥塞避免:当速率到达某个值(ssthresh:slow start threshold:一般为64K)时进入拥塞避免,每收到一个ack后CongWin = CongWin+1/CongWin,即每过一个RTT之后CongWin++;
  • 超时丢包:当发生超时丢包时,TCP认为情况太糟糕,反应很强烈,CongWin = 1,ssthresh = CongWin/2,重新进入慢启动过程。
  • 收到3个冗余ack丢包:当发送方收到3个冗余ack时,CongWin = CongWin/2,ssthresh = CongWin,进入拥塞避免;
  • 快速恢复:当发生超时丢包后又收到了3个冗余ack,那么TCP认为网络情况还没有之认为的那么严重,随机进入拥塞避免,即CongWin恢复为丢包时的一半,ssthresh恢复为丢包时的CongWin值。

状态汇总

  • closed
  • listen
  • syn_sent
  • syn_rcvd
  • established
  • close_wait:接收到FIN并响应ack后进入close_wait状态,此状态时如果还有未发送完成的数据则可以继续发送
  • last_ack:close_wait状态中发送fin报文之后进入last_ack状态
  • fin_wait_1:主动发送fin报文之后进入fin_wait_1状态,等待ack返回
  • fin_wait_2:收到ack后等待对方发送fin报文,此时可能还有数据包到达
  • time_wait:收到fin报文并响应ack后进入time_wait状态,time_wait状态将持续MSL × 2的时间,主要出于两个原因:1.尽力确保最后的ack送达,如果ack超时,对方会重发fin,要等待fin以及ack两个报文的线路存在时间,所以需要*2;2.确保本链接关闭时不会影响后面新建立的连接,如果没有time_wait状态而直接关闭并且马上在同样的端口号又重建了一个连接,而最后发送的ack又超时,对方重发fin,这又将新建立的连接关闭了。
  • closing:此状态相对比较特殊,主动发送fin后,没有收到ack却收到了fin(双方同时关闭连接),此时响应ack并进入closing状态,随后当收到ack时进入time_wait状态

TCP状态
TCP状态