《Java高并发核心编程》卷1:内容介绍

第1~4章从操作系统的底层原理开始,浅显易懂地揭秘高并发IO的底层原理,并介绍如何让单体Java应用支持百万级的高并发;从传统的阻塞式OIO开始,细致地解析Reactor高性能模式,介绍高性能网络开发的基础知识。这些非常底层的原理知识和基础知识非常重要,是开发过程中解决Java实际问题必不可少的。

第5~8章重点讲解Netty。目前Netty是高性能通信框架皇冠上当之无愧的明珠,是支撑其他众多著名的高并发、分布式、大数据框架底层的框架。这几章从Reactor模式入手,以“四两拨千斤”的方式为大家介绍Netty原理。同时,还将介绍如何通过Netty来解决网络编程中的重点难题,如Protobuf序列化问题、半包问题等。

第9~12章从TCP、HTTP入手,介绍客户端与服务端、服务端与服务端之间的高性能HTTP通信和WebSocket通信。这几章深入浅出地介绍TCP、HTTP、WebSocket三大常用的协议,以及如何基于Netty实现HTTP、WebSocket高性能通信。

第13章对ZooKeeper进行详细的介绍。除了全面地介绍Curator API之外,还从实战的角度出发介绍如何使用ZooKeeper设计分布式ID生成器,并对重要的SnowFlake算法进行详细的介绍。另外,还结合小故事以图文并茂的方式浅显易懂地介绍分布式锁的基本原理。

第14章从实战开发层面对Redis进行介绍,详细介绍Redis的5种数据类型、客户端操作指令、Jedis Java API。另外,还通过spring-data-redis来完成数据分布式缓存的实战案例,详尽地介绍Spring的缓存注解以及涉及的SpEL表达式语言。

第15章通过CrazyIM项目为大家介绍一个亿级流量的高并发IM系统模型,这个高并发架构的系统模型不仅仅限于IM系统,通过简单的调整和适配就可以应用于当前主流的Java后台系统。

《Java高并发核心编程》卷1:第9章Http原理与Web服务器实战

高性能Web应用架构

十万级并发的Web应用架构

QPS在十万每秒的Web应用,其架构大致如图所示。
alt text
十万级QPS的Web应用架构主要包括客户端层、接入层、服务层,重点是接入层和服务层。
首先看服务层,在Spring Cloud微服务技术流程之前,服务层主要是通过Tomcat集群部署的向外提供服务的独立Java应用;在微服务技术成为主流之后,服务层主要是微服务Provider实例,并通过内部网关(如Zuul)向外提供统一的访问服务。
其次看接入层,接入层可以理解为客户端层与服务层之间的一个反向代理层,利用高性能的Nginx来做反向代理:
(1)Nginx将客户端请求分发给上游的多个Web服务;Nginx向外暴露一个外网IP,Nginx和内部Web服务(如Tomcat、Zuul)之间使用内网访问。
(2)Nginx需要保障负载均衡,并且通过Lua脚本可以具备动态伸缩、动态增加Web服务节点的能力。
(3)Nginx需要保障系统的高可用(High Availability),任何一台Web服务节点挂了,Nginx都可以将流量迁移到其他Web服务节点上。
Nginx的原理与Netty很像,也是应用了Reactor模式。Nginx执行过程中主要包括一个Master进程和n(n ≥1)个Worker进程,所有的进程都是单线程(只有一个主线程)的。Nginx使用了多路复用和事件通知。其中,Master进程用于接收来自外界的信号,并给Worker进程发送信号,同时监控Worker进程的工作状态。Worker进程则是外部请求真正的处理者,每个Worker请求相互独立且平等的竞争来自客户端的请求。
因为Nginx应用了Reactor模式,所以在处理高并发请求时内存消耗非常小。在30000并发连接下,开启的10个Nginx进程才消耗150(15×10=150)MB内存。
与Nginx类似、同样比较有名的Web服务器为Apache HTTP Server(纯Java实现)。该服务器在处理并发连接时会为每个连接建立一个单独的进程或线程,并且在网络输入/输出操作时阻塞。该阻塞式的IO将导致内存和CPU被大量消耗,因为新起一个单独的进程或线程需要准备新的运行时环境,包括堆内存和栈内存的分配,以及新的执行上下文,这些操作也会导致多余的CPU开销。最终会由于过多的上下文切换而导致服务器性能变差。因此,接入层的反向代理服务器原则上需要使用高性能的Nginx而不是Apache HTTP Server。
尽管单体的Nginx比较稳定,在长时间运行的情况下,还是存在有可能崩溃的情况。如何保障接入层的Nginx高可用呢?可以使用Nginx + KeepAlived组合模式,具体如下:
(1)使用两台(或以上)Nginx组成一个集群,分别部署上KeepAlived,设置成相同的虚IP供下游访问,从而保证Nginx的高可用。
(2)当一台Nginx挂了,KeepAlived能够探测到,并会将流量自动迁移到另一台Nginx上,整个过程对下游调用方透明。
如果流量不断增长,两台Nginx的集群模式不够,就可以使用LVS + KeepAlived组合模式实现Nginx的可扩展,并且在架构上进行升级,具体请看千万级流量的Web应用架构。

千万级高并发的Web应用架构

QPS在百万级甚至千万级的Web应用架构大致如图所示。
alt text
QPS在百万级甚至千万级的Web应用架构主要包括客户端层、负载均衡层、接入层、服务层,重点是客户端层和负载均衡层。
在客户端层,需要在DNS服务器上使用负载均衡的机制。DNS负载均衡的技术很简单,属于运维层面的技术,具体来说就是在DNS服务器中配置多个A记录,如表所示
alt text
通过在DNS服务器中配置多个A记录的方式可以在一个域名下面添加多个IP,由DNS域名服务器进行多个IP之间的负载均衡,甚至DNS服务器可以按照就近原则为用户返回最近的服务器IP地址。
DNS负载均衡虽然简单高效,但是也有不少缺点,具体如下:
(1)通常无法动态调整主机地址权重(也有支持权重配置的DNS服务器),如果多台主机性能差异较大,则不能很好地均衡负载。
(2)DNS服务器通常会缓存查询响应,以便更迅速地向用户提供查询服务。在某台主机宕机的情况下,即使第一时间移除服务器IP也无济于事。
由于DNS负载均衡无法满足高可用性要求,因此通常仅仅被用于客户端层的简单复杂均衡。为了应对百万级、千万级高并发流量,需要在客户端与接入层之间引入一个专门的负载均衡层,该层通过LVS + KeepAlived组合模式达到高可用和负载均衡的目的。负载均衡层中的LVS(Linux Virtual Server,Linux虚拟服务器)是一个虚拟的服务器集群系统,该项目在1998年5月由章文嵩博士成立,是国内最早出现的自由软件项目之一。
QPS在千万级的Web应用的高可用负载均衡层使用LVS + KeepAlived组合模式实现,具体的方案如下:
(1)使用两台(或以上)LVS组成一个集群,分别部署上KeepAlived,设置成相同的虚IP(VIP)供下游访问。KeepAlived对LVS负载调度器实现健康监控、热备切换,具体来说,对服务器池中的各个节点进行健康检查,自动移除失效节点,恢复后再重新加入,从而保证LVS高可用。
(2)在LVS系统上,可以配置多个接入层Nginx服务器集群,由LVS完成高速的请求分发和接入层的负载均衡。
LVS常常使用直接路由方式(DR)进行负载均衡,数据在分发过程中不修改IP地址,只修改MAC地址,由于实际处理请求的真实物理IP地址和数据请求目的IP地址一致,因此响应数据包可以不需要通过LVS负载均衡服务器进行地址转换,而是直接返回给用户浏览器,避免LVS负载均衡服务器网卡带宽成为瓶颈。此种方式又称作三角传输模式,具体如图所示。
alt text
使用三角传输模式的链路层负载均衡是目前大型网站使用最广泛的一种负载均衡手段。目前,LVS是Linux平台上最好的三角传输模式软件负载均衡开源产品。当然,除了软件产品之外,还可以使用性能更好的专用硬件产品(如F5),但是其动辄几十万的昂贵价格并不是所有Web服务提供商所能承受的。
LVS目前已经是Linux标准内核的一部分,从Linux 2.4内核以后,无须专门给内核打任何补丁,可以直接使用LVS提供的各种功能。

LVS和Nginx都具备负载均衡的能力,它们的区别是Nginx主要用于四层、七层的负载均衡,大家平时使用Nginx进行的Web Server负载均衡就属于七层负载均衡;LVS主要用于二层、四层的负载均衡,但是出于性能的原因,LVS更多用于二层(数据链路层)负载均衡。

什么是二层、四层、七层负载均衡?
(1)二层(OSI模型的数据链路层)负载均衡:主要根据报文中的链路层内容(如MAC地址等)在多个上游服务器之间选择一个RS(Real Server,真实服务器),然后进行报文的处理和转发,从而实现负载均衡。
(2)四层(OSI模型的传输层)负载均衡:主要通过修改报文中的目标IP地址和端口在多个上游TCP/UDP服务器之间选择一个RS,然后进行报文转发,从而实现负载均衡。
(3)七层(OSI模型的应用层)负载均衡:主要根据报文中的应用层内容(如HTTP协议URI、Cookie信息、虚拟主机Host名称等)在多个上游应用层服务器(如HTTP Web服务器)之间选择一个RS,然后进行报文转发,从而实现负载均衡。

上面所指的二层、四层、七层属于OSI模型的层次概念,不属于TCP/IP协议的层次概念,具体请参考后面章节有关TCP/IP协议的具体知识。

Nginx不具备二层的负载均衡能力LVS不具备七层(应用层)的负载均衡能力,如果需要完成七层负载均衡的工作(如URL解析等),则使用LVS无法完成。
LVS的转发分为NAT模式(属于四层负载均衡)和DR模式(属于二层负载均衡),具体的介绍如下:
1)LVS的NAT模式(属于四层负载均衡)
NAT(Network Address Translation)是一种外网和内网地址映射的技术,是一种网络地址转换技术。在NAT模式下,网络数据报的进出都要经过LVS的处理。LVS需要作为RS(真实服务器)的网关。
NAT包括目标地址转换(DNAT)和源地址转换(SNAT)。当包到达LVS时,LVS需要做目标地址转换(DNAT):将目标IP改为RS的IP,RS在接收到数据包以后,仿佛是客户端直接发给它的一样;RS处理完返回响应时,源IP是RS的IP,目标IP是客户端的IP,这时LVS需要做源地址转换(SNAT),将包的源地址改为VIP(对外的IP),这样这个包对客户端看起来就仿佛是LVS直接返回给它的。
(2)LVS的DR模式(属于二层负载均衡)
DR模式也叫直接路由、三角传输模式。DR模式下需要LVS和RS集群绑定在同一个VIP上,与NAT的不同点在于:请求由LVS接收,处理后由RS直接返回给用户,响应返回的时候不经过LVS,所以也被形象地称为三角传输模式。
一个请求过来时,LVS只需要将网络帧的MAC地址修改为某一台RS的MAC,该包就会被转发到相应的RS处理,注意此时的源IP和目标IP都没变,RS收到LVS转发来的包时,链路层发现MAC是自己的,到上面的网络层,发现IP也是自己的,于是这个包被合法地接收,RS感知不到前面有LVS的存在。当RS返回响应时,只要直接向源IP(客户端的IP)返回即可,不再经过LVS转发。这里有一个系统运维的要点:RS的Loopback口需要和LVS设备上存在相同的VIP地址,这样响应才能直接返回到客户端。
在DR负载均衡模式下,数据在分发过程中不修改IP地址,只修改MAC地址,由于实际处理请求的真实物理IP地址和数据请求目的IP地址一致,因此不需要通过负载均衡服务器进行地址转换,其最大的优势为:可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈,因此DR模式具有较好的性能,是目前大型网站使用最广泛的一种负载均衡手段。
术业有专攻,LVS、KeepAlived的具体配置和运维更多的属于运维人员的工作,对于开发人员来说只要清楚其工作原理即可。
总之,如何抵抗十万级甚至千万级QPS访问洪峰,涉及大量的开发知识、运维知识,对于开发人员来说,并不一定需要掌握太多的操作系统层面(如LVS)的运维知识,主要原因是企业一般都会有专业的运维人员去解决系统的运行问题,对千万级QPS系统中所涉及的高并发方面的开发知识则是必须掌握的。
在十万级甚至千万级QPS的Web应用架构过程中,如何提高平台内部的接入层Nginx到服务层Tomcat(或者其他Java容器)之间的HTTP通信能力涉及高并发HTTP通信以及TCP、HTTP等基础的知识。接下来,本书从HTTP应用层协议开始为大家解读这些作为Java核心工程师、架构师所必备的基础知识。

详解HTTP应用层协议

Http的演进

基于Netty实现简单的Web服务器

基于Netty的HTTP请求的处理流程

Netty内置的HTTP报文解码流程

基于Netty的HTTP响应编码流程

使用Postman发送多种类型的请求体

《Java高并发核心编程》卷1:第10章高并发Http通信的核心原理

需要进行HTTP连接复用的高并发场景

详解传输层TCP

TCP连接状态的原理与实验

TCP/IP连接的11种状态

TCP建立连接、传输数据和断开连接是一个复杂的过程,为了准确地描述这一过程,可以采用有限状态机来完成。有限状态机包含有限个状态,在某一时刻,连接必然处于某一特定状态,当在一个状态下发生特定事件时,连接会进入一个新的状态。
TCP连接的11种状态如下:
(1)LISTEN:表示服务端的某个ServerSocket监听连接处于监听状态,可以接受客户端的连接。
(2)SYN_SENT:这个状态与SYN_RCVD状态相呼应,当客户端Socket连接的底层开始执行connect()方法发起连接请求时,本地连接会进入SYN_SENT状态,并发送SYN报文,并等待服务端发送三次握手中的SYN+ACK报文。SYN_SENT状态表示客户端连接已发送SYN报文。
(3)SYN_RCVD:表示服务端ServerSocket接收到了来自客户端连接的SYN报文。在正常情况下,这个状态是ServerSocket连接在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat指令很难看到这种状态,除非故意写一个监测程序,将三次TCP握手过程中最后一个ACK报文不予发送。当TCP连接处于此状态时,再收到客户端的ACK报文,它就会进入ESTABLISHED状态。
(4)ESTABLISHED:表示TCP连接已经成功建立。
(5)FIN_WAIT_1:当连接处于ESTABLISHED状态时,想主动关闭连接时,主动断发会调用底层的close()方法,要求主动关闭连接,此时主动断开方进入FIN_WAIT_1状态。当对方回应ACK报文后,主动方进入FIN_WAIT_2状态。当然,在实际的正常情况下,无论对方处于任何种情况,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态有时仍可以用netstat指令看到。
(6)FIN_WAIT_2:主动断开方处于FIN_WAIT_1状态后,如果收到对方的ACK报文,主动方会进入FIN_WAIT_2状态,此状态下的双向通道处于半连接(半开)状态,即被动方还可以传递数据过来,但主动方不可再发送数据出去。需要注意的是,FIN_WAIT_2是没有超时的(不像TIME_WAIT状态),这种状态下如果对方不发送FIN+ACK关闭响应(不配合完成4次挥手过程),那么FIN_WAIT_2状态将一直保持,该连接会一直被占用,资源不会被释放,越来越多处于FIN_WAIT_2状态的半连接堆积会导致操作系统内核崩溃。
(7)TIME_WAIT:该状态表示主动断开方已收到了对方的FIN+ACK关闭响应,并发送出ACK报文。TIME_WAIT状态下的主动方TCP连接会等待2MSL的时间,然后回到CLOSED状态。如果FIN_WAIT_1状态下收到了对方同时带FIN+ACK关闭响应报文,就可以直接进入TIME_WAIT状态,而无须经过FIN_WAIT_2状态。这种情况下,四次挥手变成三次挥手。
(8)CLOSING:这种状态在实际情况中很少见,属于一种比较罕见的例外状态。正常情况下,当一方发送FIN报文后,理论上应该先收到对方的ACK报文再收到对方的FIN+ACK关闭响应报文,或同时收到。CLOSING状态表示一方发送FIN报文后并没有收到对方的ACK报文,却也收到了对方的FIN报文。什么情况下会出现此种情况呢?当双方几乎在同时发出close()双向连接时,就会出现双方同时发送FIN报文的情况,这时就会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
(9)CLOSE_WAIT:表示正在等待关闭。在主动断开方调用close()方法关闭一个连接后,主动方会发送FIN报文给被动方,被动方在收到之后会回应一个ACK报文给主动方,回复完成之后,被动方的TCP连接进入CLOSE_WAIT状态。接下来,被动方需要检查是否还有数据要发送给主动方,如果没有,就意味着被动方也就可以关闭连接了,此时给主动方发送FIN+ACK报文,即关闭自己到对方这个方向的连接。简单地说,当连接处于CLOSE_WAIT状态下,可以继续传输数据,传输完成之后关闭连接。
(10)LAST_ACK:当被动断开方发送完FIN+ACK确认断开之后,就处于LAST_ACK状态,等待主动断开方的最后一个ACK报文。当收到对方的ACK报文后,被动关闭方也就可以进入CLOSED可用状态了。
(11)CLOSED:关闭状态或者初始状态,表示TCP连接是“关闭着的”或“未打开的”状态,或者说连接是可用的。

通过netstat指令查看连接状态

HTTP长连接原理

服务端HTTP长连接技术

客户端HTTP长连接技术原理与实验

《Java高并发核心编程》卷1:第11章WebSocket原理与实战

WebSocket是一种全双工通信的协议,其通信在TCP连接上进行,所以属于应用层协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket编程中,浏览器和服务器只需要完成一次升级握手就直接可以创建持久性的连接,并进行双向数据传输。
对于WebSocket的Java开发,Java官方发布了JSR-356规范,该规范的全称为Java API for WebSocket。不少Web容器(如Tomcat、Jetty等)都支持JSR-356规范,提供了WebSocket应用开发API。Tomcat从7.0.27开始支持WebSocket,从7.0.47开始支持JSR-356规范。
无论是Tomcat还是Jetty,其性能在高并发场景下的表现并不是非常理想。Netty是一款高性能的NIO网络编程框架,在通信连接数与信息量激增时表现依然出色。所以,编写WebSocket服务端程序时,一般基于Netty框架进行编写

WebSocket协议简介

WebSocket回显演示程序开发

WebSocket协议通信的原理

《Java高并发核心编程》卷1:第12章SSL/TLS核心原理与实战

什么是SSL/TLS

SSL/TLS协议的版本演进

alt text

SSL/TLS协议的分层结构

SSL/TLS协议包括握手协议(Handshake Protocol)、密码变化协议(SSL Change Cipher Spec Protocol)、警告协议(Alert Protocol)、记录协议(Record Protocol)。
(1)握手协议:SSL/TLS协议非常重要的组成部分,用来协商通信过程中使用的加密套件(加密算法、密钥交换算法和MAC算法等)、在服务端和客户端之间安全地交换密钥、实现服务端和客户端的身份验证。
(2)密码变化协议:客户端和服务端通过密码变化协议通知对端,随后的报文都将使用新协商的加密套件和密钥进行保护和传输。
(3)警告协议:用来向对端发送告警信息,消息中包含告警的严重级别和描述。
(4)应用数据协议:负责将SSL/TLS承载的应用数据传达给通信对端。
(5)记录协议:主要负责对上层的数据(SSL/TLS握手协议、SSL/TLS密码变化协议、SSL/TLS警告协议和应用数据协议)进行分块计算、添加MAC值、加密等处理,并把处理后的记录块传输给对端。
SSL/TLS协议的分层结构如图所示。
alt text
SSL/TLS协议主要分为两层(上层的是握手协议、密码变化协议、警告协议和应用数据协议,下层的是记录协议),主要负责使用对称密码对消息进行加密。其中,握手协议(Handshake Protocol)是SSL/TSL通信中最复杂的子协议,也是安全通信所涉及的第一个子协议。

加密算法原理与实战

为了理解SSL/TLS原理,大家需要掌握一些加密算法的基础知识。当然,这不是为了让大家成为密码学专家,所以只需对基础的加密算法有一些了解即可。基础的加密算法主要有哈希(Hash,或称为散列)、对称加密(Symmetric Cryptography)、非对称加密(Asymmetric Cryptography)、数字签名(Digital Signature)。

哈希单向加密算法原理与实战

对称加密算法原理与实战

非对称加密算法原理与实战

数字签名原理与实战

SSL/TLS运行过程

SSL/TLS协议实现通信安全的基本思路是:消息发送之前,发送方A先向接收方B申请公钥,发送方A采用公钥加密法对发出去的通信内容进行加密,接收方B收到密文后,用自己的私钥对通信密文进行解密。
SSL/TLS协议运行的基本流程如下:
(1)客户端向服务端索要并验证公钥。
(2)双方协商生成“对话密钥”。
(3)双方采用“对话密钥”进行加密通信。
前两步又称为“握手阶段”,每一个TLS连接都会以握手开始。“握手阶段”涉及四次通信,并且所有通信都是明文的。在握手过程中,客户端和服务端将进行以下四个主要阶段:
(1)交换各自支持的加密套件和参数,经过协商后,双方就加密
套件和参数达成一致。
(2)验证对方(主要指服务端)的证书,或使用其他方式进行服
务端身份验证。
(3)对将用于保护会话的共享主密钥达成一致。
(4)验证握手消息是否被第三方修改。

SSL/TLS第一阶段握手

SSL/TLS第二阶段握手

SSL/TLS第三阶段握手

SSL/TLS第四阶段握手

详解Keytool工具

使用Java程序管理密钥与证书

OIO通信中的SSL/TLS使用实战

单向认证与双向认证

单向认证和双向认证的具体含义如下:
(1)SSL/TLS单向认证:客户端会认证服务端身份,服务端不对客户端进行认证。
(2)SSL/TLS双向认证:客户端和服务端都会互相认证,即双方之间都要发送数字证书给对端,并且对证书进行安全认证。

Netty通信中的SSL/TLS使用实战

HTTPS协议安全通信实战

<未完待续>

参考文献或转载相关: