1、Zookeeper:
ZAB(zookeeper atomic broadcast)协议是zookeeper用于保证分布式数据最终一致性的。
ZK角色:
leader:负责数据读写
follower:负责数据读,具有选举权
observer:负责数据读,无选举权。
数据读流程:client ->任意zk节点->返回数据
数据写流程:client->任意zk节点->转发数据到leader->发送proposal到所有follower->等待半数以上节点ACK->向follower发送commit指令,同时本机commit->返回client成功
1.1、Zab 协议的特性:
1)Zab 协议需要确保那些已经在 Leader 服务器上提交(Commit)的事务最终被所有的服务器提交。
2)Zab 协议需要确保丢弃那些只在 Leader 上被提出而没有被提交的事务。
选举规则:
zxid>serverId
1.2、数据恢复:
场景1:leader在未发送所有commit请求前宕机,可能部分或所有节点未接收到commit指令。
先选举,选择zxid最大的节点(该节点数据最新),即使该节点存在未commit的proposal,也能保证数据不丢失。
leader 将本机的proposal同步至follower,并对未commit的proposal发送commit指令,保证数据不丢失。
场景2:leader在刚接收到proposal时宕机,此时尚未发送proposal到follower。
先选举,选择zxid最大的节点,此时原来的leader恢复,与新leader进行数据同步,老leader丢弃尚未commit的proposal。
2、HTTP协议
3、网络分层
3.1、OSI七层模型
层级 | 协议 | 描述 |
---|---|---|
应用层 | HTTP FTP | 应用层的功能就是规定了应用程序的数据格式。我们经常用的电子邮件、HTTP协议以及FTP数据的格式,就是在应用层定义的。 |
表示层 | TELNET | |
会话层 | DNS SMTP | |
传输层 | TCP UDP | 经过数据链路层和网络层的支持,我们已经可以正常在两台计算机之间进行通讯了,但是计算机会同时运行着许多程序,比如同时开着QQ与WX,那么怎么区分消息是QQ的还是WX的呢?传输层的功能就是建立端口到端口的通信,使得数据能够正确的传送给不同的应用程序。 |
网络层 | IP ICMP | 以太网通过广播这种很原始的形式,解决了两台计算机之间的通信问题。但很明显,它不是把数据包准确的送达接收方,而是向网络中所有的计算机发送数据包。网络层引入一套新的协议用来区分不同的广播域/子网,于是就有了IP 协议。. |
数据链路层 | 以太网 | 数据链路层的功能就是通过规定一套协议来定义电信号的分组方式,以及规定不同的组代表什么意思,从而双方计算机都能够进行识别,这个协议就是“以太网协议”。 |
物理层 | RS232 | 物理层,顾名思义,用物理手段将电脑连接起来,基本上是用双绞线、光纤、无线电波的方式来实现物理层。 |
3.2、TCP/IP4层模型
应用层
传输层
网络层
网络接口层
3.3、TCP/IP5层模型
应用层
传输层
网络层
数据链路层
物理层
4、TCP协议
4.1、TCP 最主要的特点
- TCP 是面向连接的传输层协议。应用程序在使用 TCP 协议之前,必须先建立 TCP 连接。在传送数据完毕后,必须释放已经建立的 TCP 连接
- 每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一)
- TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达
- TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接受缓存,用来临时存放双向通信的数据
- 面向字节流。TCP 中的“流”指的是流入到进程或从进程流出的字节序列
4.2、报文头
- ACK:只有1 bit的标志位,若为1,表示这个数据段中的确认序号是有效的,即这个数据报是对之前接收到的某个报文的确认(一个TCP报文可以同时作为确认报文和传递数据报文)。
- RST:只有1 bit的标志位,若客户端向服务器的一个端口请求建立TCP连接,但是服务器的那个端口并不允许建立连接(比如没开启此端口),则服务器会回送一个TCP报文,将RST位置为1,告诉客户端不要再向这个端口发起连接;
- SYN:只有1 bit的标志位,若为1,表示这是一条建立连接的TCP报文段;
- FIN:只有1 bit的标志位,若为1,表示这是一条断开连接的TCP报文段;
4.2、三次握手
客户端 -> 服务端 : syn=1 seq = x
服务端 -> 客户端:syn=1 ack =1 ack=x+ 1 seq = y
客户端 -> 服务端:ack=1 ack=y+1 seq=x+1
4.3、四次挥手
客户端 -> 服务端:fin=1 seq=x,客户端向服务端发起断开连接请求,客户端已无数据发送。
服务端 -> 客户端:fin=1 ack=1 ack=x+1 seq=y,服务端接收到请求,向客户端发送确认报文,此时服务端处于CLOSE_WAIT状态。
服务端 -> 客户端:fin=1 ack=1 ack=x+1 seq=u,服务端完成数据发送后,向客户端断开连接。
客户端 -> 服务端:ack=1 ack=u+1 seq=x+1,客户端接收到请求,向服务端发送确认报文,服务端收到ACK后立刻断开连接。此时客户端处于TIME_WAIT状态,此时客户端侧连接尚未关闭,等待2MSL(Maximum Segment LifeTime)后,会立即进入CLOSED关闭状态,到这里TCP连接就断开了。
为什么客户端要等待2MSL?
主要原因是为了保证客户端发送那个的第一个ACK报文能到到服务器,因为这个ACK报文可能丢失,并且2MSL是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,这样新的连接中不会出现旧连接的请求报文。
5、IO模型
5.1、BIO
使用BIO通信模型的服务端,通常通过一个独立的Acceptor线程负责监听客户端的连接,监听到客户端连接请求后为每一个客户端创建一个新的线程链路进行处理,处理完成通过输出流回应客户端,线程消耗,这就是典型一对一答模型。
服务端
int port = 3000;
try(ServerSocket serverSocket = new ServerSocket(port)) {
Socket socket = null;
while (true) {
//主程序阻塞在accept操作上
socket = serverSocket.accept();
new Thread(new BioExampleServerHandle(socket)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
private Socket socket;
public BioExampleServerHandle(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try(BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
String message = reader.readLine();
System.out.println("收到客户端消息:" + message);
writer.println("answer: " + message);
} catch (Exception e) {
e.printStackTrace();
}
}
客户端
String host = "127.0.0.1";
int port = 3000;
try(Socket socket = new Socket(host, port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
Scanner input = new Scanner(System.in);
System.out.println("输入你想说的话:");
String message = input.nextLine();
writer.println(message);
String answer = reader.readLine();
System.out.println(answer);
} catch (Exception e) {
e.printStackTrace();
}
5.2、NIO
NIO 弥补了同步阻塞I/O的不足,它提供了高速、面向块的I/O,我们对一些概念介绍一下:
Buffer: Buffer用于和NIO通道进行交互。数据从通道读入缓冲区,从缓冲区写入到通道中,它的主要作用就是和Channel进行交互。
Channel: Channel是一个通道,可以通过它读取和写入数据,通道是双向的,通道可以用于读、写或者同时读写。
Selector: Selector会不断的轮询注册在它上面的Channe,如果Channel上面有新的连接读写事件的时候就会被轮询出来,一个Selector可以注册多个Channel,只需要一个线程负责Selector轮询,就可以支持成千上万的连接,可以说为高并发服务器的开发提供了很好的支撑。
5.3、AIO
NIO2.0 引入了异步通道的概念,提供了异步文件通道和异步套接字通道的实现,我们可以通过Future类来表示异步操作结果,也可以在执行异步操作的时候传入一个Channels,实现CompletionHandler接口为操作回调。
5.4、对比
对比项 | 同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步I/O(AIO) |
---|---|---|---|---|
是否阻塞 | 是 | 是 | 否 | 否 |
是否同步 | 是 | 是 | 是 | 否(异步) |
友好程度 | 简单 | 简单 | 非常难 | 比较难 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
6、JVM
6.1、JVM组成
- 程序计数器:线程私有,生命周期跟随线程,记录程序当前执行指令的地址。
- 虚拟机栈(线程栈):虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈:线程私有,执行本地方法。
- 堆:线程共享,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。分为young和old区,young分为2块survivor和一块eden
6.2、JVM配置
- -Xss 设置每个线程的栈大小。JDK1.5+ 每个线程栈大小为1M,以-X开头的参数是和实现有关的,第一个s表示stack,第二个s表示size。
- -Xms 设置堆的最小空间大小;通常为操作系统可用内存的1/64大小即可。
- -Xmx 设置堆的最大空间大小;通常为操作系统可用内存的1/4大小。
- -Xmn 设置新生代大小,是对-XX:newSize、-XX:MaxnewSize两个参数的同时配置,这个参数是在JDK1.4版本以后出现的;通常为Xmx的1/3或1/4。新生代 = Eden + 2个Survivor空间。实际可用空间 = Eden + 1个Survivor,即90%。
- -XX:NewSize 设置新生代最小空间大小;
- -XX:MaxNewSize 设置新生代最大空间大小;
- -XX:NewRatio 新生代与老年代的比例,如-XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3。
- -XX:SurvivorRatio 新生代中 Eden 与 Survivor的比值。默认值为 8 。即Eden占新生代空间的8/10,另外两个Survivor各占1/10。
6.3、垃圾回收器
6.3.1、回收算法
引用计数算法
为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。
可达性分析算法
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
标记-清除算法
标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
该算法分为两个阶段,标记和清除。标记阶段标记所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题就是内存碎片严重化,后续可能发生对象不能找到利用空间的问题。
复制算法
按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
标记-整理算法
标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
分代算法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代。
三色标记法
//TODO
6.3.2、垃圾回收器
垃圾回收器名称 | 适用内存区域 | 并发类型 | 回收算法 | 优点 | 缺点 |
---|---|---|---|---|---|
Serial | 新生代 | 单线程 | 复制算法 | 简单高效,对于限定单个CPU的环境来说,Serial垃圾收集器没有线程交互(交换)开销,可以获得最高的单线程手机效率 | 会发生SWT现象 |
ParNew | 新生代 | 多线程 | 复制算法 | Serial多线程版本,并行处理能力变强 | 会发生SWT现象 |
Parallel Scavange | 新生代 | 多线程 | 复制算法 | 关注系统吞吐量 | |
Serial Old | 老年代 | 单线程 | 标记整理算法 | STW | |
Parallel Old | 老年代 | 多线程 | 标记整理 | 关注系统吞吐量 | |
CMS | 老年代 | 多线程 | 标记清理 | 获取最短垃圾回收停顿时间 | 产生大量的内存碎片,对CPU资源敏感 |
G1 | 老年代/新生代 | 多线程 | 标记-整理算法+复制算法 | 低停顿 + 停顿时间可控 |
CMS运行过程
Concurrent Mark Sweep (CMS)垃圾收集器时针对老年代的一个并发线程的垃圾收集器,其目的是获取最短垃圾回收停顿时间,它采用的是多线程的标记—清除,但是它需要更多的内存来完成这个动作;多应用于:
- 与用户交互较多的场景
- 希望系统的停顿时间最短,注重服务的响应速度
- 给用户带来较好的体验
- 常见的WEB、B/S系统的服务器应用上
- 初始标记
只是标记一下GC Roots能直接关联的对象,速度很快但仍然需要暂停所有的工作线程; - 并发标记
进行GC Roots跟踪的过程,从刚才产生的集合中标记存活的对象,并发执行不需要暂停工作线程;
但是并不能保证标记出所有的存活对象; - 重新标记
为了修正并发标记期间因为用户程序继续运行而导致标记变动的那一部分对象的标记记录;需要“Stop The World”且停顿时间比初始标记时间长但远比并发标记的时间短; - 并发清除
回收所有的垃圾对象;
CMS另两个致命缺陷
- CMS采用了Mark-Sweep算法,最后会产生许多内存碎片,当到一定数量时,CMS无法清理这些碎片了,CMS会让Serial Old垃圾处理器来清理这些垃圾碎片,而Serial Old垃圾处理器是单线程操作进行清理垃圾的,效率很低。
所以使用CMS就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行Serial Old GC时,效率过低。
解决方案:使用Mark-Sweep-Compact算法,减少垃圾碎片
调优参数(配套使用):
开启CMS的压缩
-XX:+UseCMSCompactAtFullCollection
//默认为0,指经过多少次CMS FullGC才进行压缩
-XX:CMSFullGCsBeforeCompaction
- 当JVM认为内存不够,再使用CMS进行并发清理内存可能会发生OOM的问题,而不得不进行Serial Old GC,Serial Old是单线程垃圾回收,效率低
解决方案:降低触发CMS GC的阈值,让浮动垃圾不那么容易占满老年代
-XX:CMSInitiatingOccupancyFraction 92%
//可以降低这个值,让老年代占用率达到该值就进行CMS GC