前言
京東到家訂單中心系統業務中,無論是外部商家的訂單生產,或是內部上下游系統的依賴,訂單查詢的調用量都非常大,造成了訂單數據讀多寫少的情況。
我們把訂單數據存儲在MySQL中,但顯然只通過DB來支撐大量的查詢是不可取的。同時對于一些復雜的查詢,MySQL支持得不夠友好,所以訂單中心系統使用了Elasticsearch來承載訂單查詢的主要壓力。
Elasticsearch作為一款功能強大的分布式搜索引擎,支持近實時的存儲、搜索數據,在京東到家訂單系統中發揮著巨大作用,目前訂單中心ES集群存儲數據量達到10億個文檔,日均查詢量達到5億。
隨著京東到家近幾年業務的快速發展,訂單中心ES架設方案也不斷演進,發展至今ES集群架設是一套實時互備方案,很好地保障了ES集群讀寫的穩定性,下面就給大家介紹一下這個歷程以及過程中遇到的一些坑。
一、網絡編程基礎回顧
1. Socket
Socket
本身有“插座”的意思,不是Java中特有的概念,而是一個語言無關的標準,任何可以實現網絡編程的編程語言都有Socket
。在Linux
環境下,用于表示進程間網絡通信的特殊文件類型,其本質為內核借助緩沖區形成的偽文件。既然是文件,那么理所當然的,我們可以使用文件描述符引用套接字。
與管道類似的,Linux
系統將其封裝成文件的目的是為了統一接口,使得讀寫套接字和讀寫文件的操作一致。區別是管道主要應用于本地進程間通信,而套接字多應用于網絡進程間數據的傳遞。
可以這么理解:Socket
就是網絡上的兩個應用程序通過一個雙向通信連接實現數據交換的編程接口API。
Socket
通信的基本流程具體步驟如下所示:
(1)服務端通過Listen
開啟監聽,等待客戶端接入。
(2)客戶端的套接字通過Connect
連接服務器端的套接字,服務端通過Accept
接收客戶端連接。在connect-accept
過程中,操作系統將會進行三次握手。
(3)客戶端和服務端通過write
和read
發送和接收數據,操作系統將會完成TCP
數據的確認、重發等步驟。
(4)通過close
關閉連接,操作系統會進行四次揮手。
針對Java編程語言,java.net
包是網絡編程的基礎類庫。其中ServerSocket
和Socket
是網絡編程的基礎類型。
SeverSocket
是服務端應用類型。Socket
是建立連接的類型。當連接建立成功后,服務器和客戶端都會有一個Socket
對象示例,可以通過這個Socket
對象示例,完成會話的所有操作。對于一個完整的網絡連接來說,Socket
是平等的,沒有服務器客戶端分級情況。
2. IO模型介紹
對于一次IO操作,數據會先拷貝到內核空間中,然后再從內核空間拷貝到用戶空間中,所以一次read
操作,會經歷兩個階段:
(1)等待數據準備
(2)數據從內核空間拷貝到用戶空間
基于以上兩個階段就產生了五種不同的IO模式。
- 阻塞IO:從進程發起IO操作,一直等待上述兩個階段完成,此時兩階段一起阻塞。
- 非阻塞IO:進程一直詢問IO準備好了沒有,準備好了再發起讀取操作,這時才把數據從內核空間拷貝到用戶空間。第一階段不阻塞但要輪詢,第二階段阻塞。
- 多路復用IO:多個連接使用同一個select去詢問IO準備好了沒有,如果有準備好了的,就返回有數據準備好了,然后對應的連接再發起讀取操作,把數據從內核空間拷貝到用戶空間。兩階段分開阻塞。
- 信號驅動IO:進程發起讀取操作會立即返回,當數據準備好了會以通知的形式告訴進程,進程再發起讀取操作,把數據從內核空間拷貝到用戶空間。第一階段不阻塞,第二階段阻塞。
- 異步IO:進程發起讀取操作會立即返回,等到數據準備好且已經拷貝到用戶空間了再通知進程拿數據。兩個階段都不阻塞。
這五種IO模式不難發現存在這兩對關系:同步和異步、阻塞和非阻塞。那么稍微解釋一下:
同步和異步
- 同步: 同步就是發起一個調用后,被調用者未處理完請求之前,調用不返回。
- 異步: 異步就是發起一個調用后,立刻得到被調用者的回應表示已接收到請求,但是被調用者并沒有返回結果,此時我們可以處理其他的請求,被調用者通常依靠事件,回調等機制來通知調用者其返回結果。
同步和異步的區別最大在于異步的話調用者不需要等待處理結果,被調用者會通過回調等機制來通知調用者其返回結果。
阻塞和非阻塞
- 阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,無法從事其他任務,只有當條件就緒才能繼續。
- 非阻塞: 非阻塞就是發起一個請求,調用者不用一直等著結果返回,可以先去干其他事情。
阻塞和非阻塞是針對進程在訪問數據的時候,根據IO操作的就緒狀態來采取的不同方式,說白了是一種讀取或者寫入操作方法的實現方式,阻塞方式下讀取或者寫入函數將一直等待,而非阻塞方式下,讀取或者寫入方法會立即返回一個狀態值。
如果組合后的同步阻塞(blocking-IO
)簡稱BIO
、同步非阻塞(non-blocking-IO
)簡稱NIO
和異步非阻塞(asynchronous-non-blocking-IO
)簡稱AIO
又代表什么意思呢?
- BIO (同步阻塞I/O模式): 數據的讀取寫入必須阻塞在一個線程內等待其完成。這里使用那個經典的燒開水例子,這里假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,才去處理下一個水壺。但是實際上線程在等待水壺燒開的時間段什么都沒有做。
- NIO(同步非阻塞): 同時支持阻塞與非阻塞模式,但這里我們以其同步非阻塞I/O模式來說明,那么什么叫做同步非阻塞?如果還拿燒開水來說,NIO的做法是叫一個線程不斷的輪詢每個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操作。
- AIO(異步非阻塞I/O模型): 異步非阻塞與同步非阻塞的區別在哪里?異步非阻塞無需一個線程去輪詢所有IO操作的狀態改變,在相應的狀態改變后,系統會通知對應的線程來處理。對應到燒開水中就是,為每個水壺上面裝了一個開關,水燒開之后,水壺會自動通知我水燒開了。
java
中的 BIO
、NIO
和AIO
理解為是 Java 語言
在操作系統層面對這三種 IO
模型的封裝。程序員在使用這些 封裝API 的時候,不需要關心操作系統層面的知識,也不需要根據不同操作系統編寫不同的代碼,只需要使用Java
的API就可以了。由此,為了使讀者對這三種模型有個比較具體和遞推式的了解,并且和本文主題NIO
有個清晰的對比,下面繼續延伸。
Java BIO
BIO
編程方式通常是是Java的上古產品,自JDK 1.0-JDK1.4就有的東西。編程實現過程為:首先在服務端啟動一個ServerSocket
來監聽網絡請求,客戶端啟動Socket
發起網絡請求,默認情況下SeverSocket
會建立一個線程來處理此請求,如果服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕。服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理。大致結構如下:
如果要讓 BIO
通信模型能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是 socket.accept()
、socket.read()
、 socket.write()
涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的 一請求一應答通信模型 。我們可以設想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷,不過可以通過線程池機制改善,線程池還可以讓線程的創建和回收成本相對較低。使用線程池機制改善后的 BIO
模型圖如下:
BIO
方式適用于連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中,是JDK1.4以前的唯一選擇,但程序直觀簡單易懂。Java BIO
編程示例網上很多,這里就不進行coding舉例了,畢竟后面NIO
才是重點。
Java NIO
NIO
(New IO或者No-Blocking IO),從JDK1.4 開始引入的非阻塞IO
,是一種非阻塞
+ 同步
的通信模式。這里的No Blocking IO
用于區分上面的BIO
。
NIO
本身想解決 BIO
的并發問題,通過Reactor模式
的事件驅動機制來達到Non Blocking
的。當 socket
有流可讀或可寫入 socket
時,操作系統會相應的通知應用程序進行處理,應用再將流讀取到緩沖區或寫入操作系統。
也就是說,這個時候,已經不是一個連接就 要對應一個處理線程了,而是有效的請求,對應一個線程,當連接沒有數據時,是沒有工作線程來處理的。
當一個連接創建后,不需要對應一個線程,這個連接會被注冊到 多路復用器
上面,所以所有的連接只需要一個線程就可以搞定,當這個線程中的多路復用器
進行輪詢的時候,發現連接上有請求的話,才開啟一個線程進行處理,也就是一個請求一個線程模式。
NIO
提供了與傳統BIO模型中的Socket
和ServerSocket
相對應的SocketChannel
和ServerSocketChannel
兩種不同的套接字通道實現,如下圖結構所示。這里涉及的Reactor
設計模式、多路復用Selector
、Buffer
等暫時不用管,后面會講到。
NIO 方式適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局 限于應用中,編程復雜,JDK1.4 開始支持。同時,NIO
和普通IO的區別主要可以從存儲數據的載體、是否阻塞等來區分:
Java AIO
與 NIO
不同,當進行讀寫操作時,只須直接調用 API 的 read
或 write
方法即可。這兩種方法均為異步的,對于讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入 read
方 法的緩沖區,并通知應用程序;對于寫操作而言,當操作系統將 write
方法傳遞的流寫入完畢時,操作系統主動通知應用程序。即可以理解為,read/write
方法都是異步的,完成后會主動調用回調函數。在 JDK7
中,提供了異步文件通道和異步套接字通道的實現,這部分內容被稱作 NIO
.
AIO
方式使用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用 OS
參與并發操作,編程比較復雜,JDK7
開始支持。
目前來說 AIO
的應用還不是很廣泛,Netty
之前也嘗試使用過 AIO
,不過又放棄了。
二、NIO核心組件介紹
1. Channel
在NIO
中,基本所有的IO操作都是從Channel
開始的,Channel
通過Buffer(緩沖區)
進行讀寫操作。
read()
表示讀取通道中數據到緩沖區,write()
表示把緩沖區數據寫入到通道。
Channel
有好多實現類,這里有三個最常用:
SocketChannel
:一個客戶端發起TCP連接的ChannelServerSocketChannel
:一個服務端監聽新連接的TCP Channel,對于每一個新的Client連接,都會建立一個對應的SocketChannelFileChannel
:從文件中讀寫數據
其中SocketChannel
和ServerSocketChannel
是網絡編程中最常用的,一會在最后的示例代碼中會有講解到具體用法。
2. Buffer
概念
Buffer
也被成為內存緩沖區,本質上就是內存中的一塊,我們可以將數據寫入這塊內存,之后從這塊內存中讀取數據。也可以將這塊內存封裝成NIO Buffer
對象,并提供一組常用的方法,方便我們對該塊內存進行讀寫操作。
Buffer
在java.nio
中被定義為抽象類:
我們可以將Buffer
理解為一個數組的封裝,我們最常用的ByteBuffer
對應的數據結構就是byte[]
屬性
Buffer
中有4個非常重要的屬性:capacity、limit、position、mark
最后
現在其實從大廠招聘需求可見,在招聘要求上有高并發經驗優先,包括很多朋友之前都是做傳統行業或者外包項目,一直在小公司,技術搞的比較簡單,沒有怎么搞過分布式系統,但是現在互聯網公司一般都是做分布式系統。
所以說,如果你想進大廠,想脫離傳統行業,這些技術知識都是你必備的,下面自己手打了一份Java并發體系思維導圖,希望對你有所幫助。
資料獲取方式:戳這里免費下載
怎么搞過分布式系統,但是現在互聯網公司一般都是做分布式系統。
所以說,如果你想進大廠,想脫離傳統行業,這些技術知識都是你必備的,下面自己手打了一份Java并發體系思維導圖,希望對你有所幫助。
資料獲取方式:戳這里免費下載