1. 程式人生 > 程式設計 >面試官:Netty這些我必問

面試官:Netty這些我必問

Netty

最流行的 NIO 框架,由 JBOSS 提供的,整合了FTP,SMTP,HTTP協議

  1. API 簡單
  2. 成熟穩定
  3. 社群活躍·
  4. 經過大規模驗證(網際網路、大資料、網路遊戲、電信通訊)
    Elasticsearch、Hadoop 子專案 avro專案、阿里開源框架 Dubbo、使用 Netty

BIO

優點:模型簡單,編碼簡單缺點:效能瓶頸,請求數和執行緒數 N:N 關係高併發情況下,CPU 切換執行緒上下文損耗大案例:Tomcat 7之前,都是 BIO,7 之後是 NIO改進:偽 NIO,使用執行緒池去處理邏輯

IO 模式

同步阻塞:丟衣服->等洗衣機洗完->再去晾衣服同步非阻塞:丟衣服->去做其他事情,定時去看衣服是否洗完->洗完後自己去晾衣服非同步非阻塞:丟衣服-> 去做其他事情不管了,衣服洗好會自動晾好,並且通知你晾好了

五種 I/O 模型

五種 I/O 模型:阻塞 IO、非阻塞 IO、多路複用 IO、訊號驅動 IO、非同步 IO,前 4 種是同步 IO,在核心資料 copy 到使用者空間時是阻塞的

阻塞 IO

非阻塞 IO

IO 多路複用

核心:可以同時處理多個 connection,呼叫系統 select 和 recvfrom函式每一個socket 設定為 non-blocking 阻塞是被 select 這個函式 block 而不是 socket阻塞缺點:連線數不高的情況下,效能不一定比 多執行緒+ 阻塞 IO 好(多呼叫一個select 函式)

訊號驅動

非同步 IO

採用 Future-Listener機制

IO 操作分為 2 步:

  1. 發起 IO 請求,等待資料準備
  2. 實際的 IO 操作,將資料從核心拷貝到程式中
    阻塞 IO、非阻塞 IO 區別在於發起 IO 請求是否被阻塞
    同步 IO、非同步 IO 在於實際的 IO 讀寫是否阻塞請求程式
    阻塞非阻塞是執行緒的狀態
    同步和非同步是訊息的通知機制
    同步需要主動讀寫資料,非同步不需要主動讀寫資料
    同步 IO 和非同步 IO 是針對使用者應用程式和核心的互動

IO 多路複用

I/O 是指網路 I /O ,多路指多個 TCP 連線,複用指一個或幾個執行緒。簡單來說:就是使用一個或者幾個執行緒處理多個 TCP 連線,最大優勢是減少系統開銷,不必建立過多的執行緒程式,也不必維護這些執行緒程式

select

檔案描述符 writefds、readdfs、exceptfds30w個連線會阻塞住,等資料可讀、可寫、出異常、或者超時返回select 函式正常返回後,通過遍歷 fdset整個陣列才能發現哪些控制程式碼發生了事件,來找到就緒的描述符fd,然後進行對應的 IO操作,幾乎在所有的平臺上支援,跨平臺支援性好缺點:

  1. select採用輪詢的方式掃描檔案描述符,全部掃描,隨著檔案描述符 FD 數量增多而效能下降。
  2. 每次呼叫 slect (),需要把 fd集合從使用者態拷貝到核心態,並進行遍歷(訊息傳遞都是核心到使用者空間)
  3. 最大缺陷就是單個程式開啟的FD 有限制,預設是 1024

poll

基本流程和 select差不多,處理多個描述符也是輪詢,根據描述符的狀態進行處理,一樣需要把fd集合從使用者態拷貝到核心態,並進行遍歷。區別是poll 沒有最大檔案描述符限制(使用連結串列方式儲存fd)

epoll

沒有描述符限制,使用者態拷貝到核心態只需要一次使用事件通知,通過epoll_ctl註冊fd,一旦該fd 就緒,核心就採用callback機制啟用對應的fd優點:

  1. 沒有fd限制,所支援的 FD 上限是作業系統的最大檔案控制程式碼數(65535),1G 記憶體大概支援 10W 控制程式碼,支援百萬連線的話,16G 記憶體就可以搞定
  2. 效率高,使用回撥通知而不是輪詢方式,不會隨著 FD 數目增加效率下降
  3. 通過 callback 機制通知,核心和使用者空間 mmap 同一塊記憶體實現
    缺點:
    程式設計模型比 select / poll 複雜
    linux核心核心函式
  4. epoll_create() 系統啟動時,會向linux核心申請一個檔案系統,b+樹,返回epoll 物件,也是一個fd
  5. epoll_ctl() 操作epoll物件,在這個物件裡面修改新增刪除對應的連結fd,繫結一個callback函式
  6. epoll_wait() 判斷並完成對應的 IO 操作
    例子:100W 個連線,1W 個活躍,在 select,poll,epoll中怎麼樣表現
    select :不修改巨集定義,需要 1000 個程式才能支援 100W 連線
    poll:100W連線,遍歷都響應不過來,還有空間的拷貝消耗大量的資源
    epoll: 不用遍歷fd,不用核心空間和使用者空間資料的拷貝
    如果 100W 個連線中,95W 活躍,則 poll 和 epoll差不多

Java的i/o

  1. jdk1.4之前是採用同步阻塞模型(BIO)
    大型服務一般採用 C/C++,因為可以直接作業系統提供的非同步 IO(AIO)
  2. jdk1.4之後推出NIO,支援非阻塞 IO,jdk1.7 升級推出 NIO2.0,提供了AIO 功能,支援檔案和網路套接字的非同步 IO

Netty 執行緒模型和 Reactor 模式

Reactor模式(反應器設計模式),是一種基於事件驅動的設計模式,在事件驅動的應用中,將一個或者多個客戶的請求進行分離和排程。在事件驅動的應用中,同步地,有序地處理接受多個服務請求。屬於同步非阻塞 IO優點:

  1. 響應快,不會因為單個同步而阻塞,雖然 reactor本身是同步的
  2. 程式設計相對簡單,最大程度避免複雜的多執行緒以及同步問題,避免了多執行緒、程式切換開銷
  3. 可擴充套件性,可以方便的通過 reactor例項個數充分利用 CPU 資源
    缺點:
  4. 相對複雜,不易於除錯
  5. reactor模式需要系統底層的支援。比如java中的selector支援,作業系統select系統呼叫支援

Reactor 單執行緒模型

  1. 作為 NIO 伺服器,接受客戶端 TCP 連線,作為 NIO 客戶端,向服務端發起 TCP 連線
  2. 服務端讀請求資料並響應,客戶端寫請求並讀取響應
    場景:
    對應小業務則適合,編碼簡單,對於高負載,高併發不合適。一個 NIO 執行緒處理太多請求,負載很高,並且響應變慢,導致大量請求超時,萬一執行緒掛了,則不可用

Reactor 多執行緒模型

一個 Acceptor執行緒,一組 NIO 執行緒,一般是使用自帶執行緒池,包含一個任務佇列和多個可用執行緒場景:可滿足大多數場景,當Acceptor需要做負責操作的時候,比如認證等耗時操作 ,在高併發情況下也會有效能問題

Reactor 主從執行緒模型

Acceptor不在是一個執行緒,而是一組 NIO 執行緒,IO 執行緒也是一組 NIO 執行緒,這樣就是 2 個執行緒池去處理接入和處理 IO場景:滿足目前大部分場景,也是 Netty推薦使用的執行緒模型BossGroup 處理連線的WorkGroup 處理業務的

Netty 使用 NIO 而不是 AIO

在 linux系統上,AIO 的底層實現仍然使用 epoll,與 NIO 相同,因此在效能上沒有明顯的優勢Netty 整體架構是 reactor 模型,採用 epoll機制,IO 多路複用,同步非阻塞模型Netty是基於 Java NIO 類庫實現的非同步通訊框架特點: 非同步非阻塞,基於事件驅動,效能高,高可靠性,高可定製性。

Echo服務

回顯服務,用於除錯和檢測的服務

原始碼剖析

EventLoop和EventLoopGroup

高效能 RPC框架 3 個要素:IO 模型、資料協議(http,brotobuf/thrift)、執行緒模型EventLoop 好比一個執行緒,一個 EventLoop可以服務多個Channel,一個Channel只有一個EventLoop,可以建立多個EventLoop來優化資源的利用,也就是EventLoopGroup一個Cahnnel 一個連線,EventLoopGroup 負責 EventLoop
NIO(單執行緒處理多個Channels) BIO(一個執行緒處理一個Channels)事件: accept,connect,read,writeEventLoopGroup 預設建立執行緒數是 CPU 核數 * 2

Bootstrap

  1. group:設定執行緒中模型,Reactor執行緒模型對比EventLoopGroup
    1) 單執行緒
EventLoopGroup g = new NioEventLoopGroup(1);
ServerBootstrap strap = new ServerBootstrap();
strap.group(g)複製程式碼

2) 多執行緒3) 主從執行緒

channel

NioServerSocketChannelOioServerSocketChannelEpollServerSocketChannelKQueueServerSocketChannel

childHandler

用於對每個通道里面的資料處理

childOption

作用於被 accept之後的連線

option

作用於每個新建立的 channel,設定 TCP 連線中的一些引數

  • ChannelOption.SO_BACKLOG
    存放已完成三次握手的請求的等待佇列的最大長度
    Linux 伺服器 TCP 連線底層知識:
    syn queue: 半連線佇列,洪水攻擊(偽造 IP 海量傳送第一個握手包),tcpmaxsyn_backlog (修改半連線 vi /etc/sysctl.conf)
    accept queue:全連線佇列 net.core.somaxconn 當前機器最大連線數
    系統預設的somaxconn引數要足夠大,如果 backlog 比 somaxconn大,則會優先用後者
  • ChannelOption.TCP_NODELAY
    預設是 false,要求高實時性,有資料時馬上傳送,就將該值改為 true 關閉 Nagle 演演算法 (Nagle演演算法會積累一定大小後再傳送,為了減少傳送次數)Nagle演演算法只允許一個未被 ACK 的包存在於網路
    (tcpsynackretries = 0 加快回收半連線,如果收不到第三個握手包 ACK,不進行重試,預設值是 5,每次等待 30S,半連線會 hold住大約 180s,tcpsynretries 預設值是 5,客戶端沒收到 SYN+ACK 包,客戶端也會重試 5 次傳送 SYN 包)

childOption

作用於被 accept之後的連結

childHandler

用於對每個通道里面的資料處理

Channel

  • Channel
    客戶端和服務端建立的一個連線通道
  • ChannelHandler
    負責Channel的邏輯處理
  • ChannelPipeline
    負責管理 ChannelHandler的有序容器

一個Channel包含一個ChannelPipeline,所有 ChannelHandler都會順序加入到ChannelPipeline中。Channel當狀態出現變化,對觸發對應的事件

狀態:

  • channelRegistered
    channel註冊到一個EventLoop,和Selector繫結
  • channelUnRegistered
    channel已建立,但是未註冊到一個EventLoop裡面,也就是沒有和Selector繫結
  • channelActive
    變為活躍狀態,連線到了遠端主機,可以接受和傳送資料
  • channelInActive
    channel處於非活躍狀態,沒有連線到遠端主機

ChannelHandler和ChannelPipeline

ChannelHandler生命週期:handlerAdded:當ChannelHandler新增到ChannelPipeline呼叫handlerRemoved:當ChannelHandler從ChannelPipeline移除時呼叫exceptionCaught:執行丟擲異常時呼叫ChannelHandler有 2 個子介面:ChannelInboundHandler(入站): 處理輸入資料和Channel狀態型別改變,介面卡 ChannelInboundHandlerAdapter(介面卡設計模式),常用 SimpleChannelInboundHandlerChannelOutboundHandler(出站):處理輸出資料,介面卡 Channel

ChannelPipeline:好比廠裡的流水線一樣,可以在上面新增多個ChannelHandler,也可以看成是一串 ChannelHandler 例項,攔截穿過Channel的輸入輸出 event,ChannelPileline實現了攔截器的一種高階形式,使得使用者可以對事件的處理以及ChannelHandler之間互動獲得完全的控制權

ChannelHandlerContext

  1. channelHandlerContext 是連線 ChannelHandler 和 ChannelPipeline 的橋樑
    ChannelHandlerContext 部分方法是和 Channel以及ChannelPipleline重合,好比呼叫 write方法
    Channel,ChannelPipeline,ChannelHandlerContext都可以呼叫寫方法,前 2 者會在整個管道流裡傳播,而 ChannelHandlerContext只會在後續的 Handler裡傳播
  2. AbstractChannelHandlerContext
    雙向連結串列結構,next/prev 後繼、前驅節點
  3. DefaultChannelHandlerContext 是實現類,但是大部分都是父類完成,整個只是簡單的實現一些方法,主要就是判斷 Handler的型別
    fire呼叫下一個 handler,不fire就不呼叫

Handler執行順序

InboundHandler順序執行,OutboundHandler逆序執行

channel.pipeline().addLast(new OutboundHandler1());
channel.pipeline().addLast(new OutboundHandler2());
channel.pipeline().addLast(new InboundHandler1());
channel.pipeline().addLast(new InboundHandler2());複製程式碼

InboundHandler1 InboundHandler2 OutboundHandler2 OutboundHandler1InboundHandler1之間通過 fireChannelRead()方法呼叫InboundHandler通過ctx.write(msg),傳遞到OutboundHandlerctx.write(msg)傳遞訊息,Inbound需要放在結尾,在 outbound之後,不然outboundHandler不會執行,使用 channel.write(msg),或者 pipline.write(msg),就不用考慮(傳播機制)客戶端: 發起請求再接受請求,先 outbound再inbound服務端:先接受請求再傳送請求,先inbound再outbound

ChannelFuture

netty中所有 I/0 操作都是非同步的,意味著任何 I/0 呼叫都會立即返回,而ChannelFuture會提供有關的資訊 I/0 操作的結果或狀態未完成:當 I/0 操作開始時,將建立一個新的物件,新的最初是未完成的,它既沒有成功,也沒有被取消,因為 I/0 操作尚未完成。已完成:當 I/0 操作完成,不管是成功、失敗還是取消,Future都是標記為已完成的,失敗的時候也有具體的資訊,例如原因失敗,但請注意,即使失敗和取消屬於完成狀態。注意:不要在 IO 執行緒內呼叫Future物件的sync和await方法,不能在 channelhandler中呼叫 sync 和 await

ChannelPromise

繼承 ChannelFuture,進一步擴充套件用於設定 IO 操作的結果

編解碼

java序列化/反序列化,url編解碼,base64編解碼java自帶序列化的缺點:

  1. 無法跨語言
  2. 序列化後的碼流太大,資料包太大
  3. 序列化和反序列化效能比較差

業界其他編解碼框架:PB,Thrift,Marshalling,Kyro

Netty裡面的編解碼:

  • 解碼器:主要負責處理入站 InboundHandler
  • 編碼器: 主要負責處理出站 OutBoundHandler
    Netty預設編解碼器,也支援自定義編解碼器
    Encoder(編碼器),Decoder(解碼器),Codec(編解碼器)

Netty解碼器 Decoder

Decoder對應 ChannelInboundHandler,主要就是位元組陣列轉換成訊息物件方法:

  • decode :常用
  • decodeLast: 用於最後的幾個位元組處理,也就是 cahnnel 關閉的時候,產生的最後一個訊息
    解碼器:
  • ByteToMessageDecoder
    用於將位元組轉為訊息,需要檢查緩衝區是否有足夠的位元組
  • ReplayingDecoder
    繼承ByteToMessageDecoder,不需要檢查緩衝區是否有足夠多的資料,速度略慢於 ByteToMessageDecoder
  • MessageToMessageDecoder
    用於將一種訊息解碼到另外一種訊息(例如 POJO 到 POJO)
    常用的解碼器:(主要解決 TCP 底層的粘包和拆包問題)
  • DelimiterBasedFrameDecoder:執行訊息分隔符的解碼器
  • LineBasedFrameDecoder:以換行符為結束標誌的解碼器
  • FixedLengthFrameDecoder:固定長度的解碼器
  • LengthFieldBasedFrameDecoder: message = header + body,基於長度解碼的通用解碼器
  • StringDecoder:文字解碼器,將接收到的訊息轉為字串,一般會與上面的幾種進行組合,然後再後面加業務的 handler

Netty 編碼器 Encoder

Encoder 對應就是 ChannelOutboundHandler ,訊息物件轉換成位元組陣列編碼器:

  • MessageToByteEncoder
    訊息轉為位元組陣列,呼叫 write方法,會先判斷當前編碼器是否支援需要傳送的訊息型別,如果不支援,則透傳
  • MessageToMessageEncoder 從一種訊息編碼為另外一種訊息

Netty 組合編解碼器 Codec

優點:成對出現,編解碼都是在一個類裡完成缺點:耦合,擴充套件性不佳

  • ByteToMessageCodec
  • MessageToMessageCode

TCP 粘包,拆包

TCP 拆包:一個完整的包可能被 TCP 拆分成多個包進行傳送TCP 粘包:把多個小的包封裝成一個大的資料包傳送,client傳送的若干資料包, server接收時粘在一個包傳送方和接收方都可能出現這個原因傳送方的原因:TCP 預設會使用 Nagle演演算法接收方的原因:TCP 接收到資料放置快取中,應用程式從快取中讀取比較慢UDP 無粘包、拆包問題,有邊界協議

TCP 半包讀寫解決方案

傳送方:關閉 Nagle 演演算法接收方:TCP 是無界的資料流,並沒有處理粘包現象的機制,且協議本身無法避免粘包,半包讀寫的發生需要在應用層進行處理應用層解決半包讀寫方法:

  1. 設定定長訊息 (10 個字元)
    abcdefgh11abcdefgh11abcdefgh11
  2. 設定訊息邊界 ($$切割)
    dfdsfdsfdf$$dsfsdfdsf$dsfdsfsdf
  3. 使用帶訊息頭的協議,訊息頭儲存訊息開始標識及訊息的長度資訊
    header + body

Netty 自帶解決 TCP 半包讀寫方案

  • DelimiterBasedFrameDecoder:指定訊息分隔符的解碼器
  • LineBasedFrameDecoder:以換行符為結束標誌的解碼器
  • FixedLengthFrameDecoder:固定長度解碼器
  • LengthFieldBasedFrameDecoder : message = header + body ,基於長度解碼的通用解碼器

實戰半包讀寫

LineBasedFrameDecoder:以換行符為結束標誌的解碼器StringDecoder 解碼器將物件轉成字串

自定義分隔符解決 TCP 讀寫問題

DelimiterBasedFrameDecodermaxLength: 表示一行最大的長度,超過長度依然沒檢測自定義分隔符,丟擲TooLongFrameExceptionfailFast: 如果為true,則超過 maxLength後立即丟擲TooLongFrameException,不進行繼續解碼,如果為 false,則等到完整訊息被解碼後,再丟擲TooLongFrameExceptionstripDelimiter:解碼後的訊息是否去除分隔符delimiters:分隔符,ByteBuf型別

自定義長度半包讀寫器 LengthFieldBasedFrameDecoder

maxFrameLength 資料包最大長度lengthFieldOffset 長度欄位的偏移量,長度欄位開始的地方(跳過指定長度個位元組之後的才是訊息體欄位)lengthFieldLength 長度欄位佔的位元組數,幀資料長度的欄位本身的長度lengthAdjustment一般 Header + Body,新增到長度欄位的補償值,如果為負數,開發人員認為這個Header的長度欄位是整個訊息包的長度,,則Netty應該減去對應的數字initialBytesToStrip 從解碼幀中第一次去除的位元組數,獲取完一個完整的資料包之後,忽略前面的指定位數的長度位元組,應用解碼器拿到的就是不帶長度域的資料包

ByteBuf

位元組容器,

  • JDK 中原生 ByteBuffer
    讀和寫公用一個索引,每次換操作都需要Flip()
    擴容麻煩,而且擴容後容易造成浪費
  • Netty ByteBuf
    讀寫使用不同的索引,所以操作便捷
    自動擴容,便捷

ByteBuf 建立方法與常見的模式

ByteBuf:傳遞位元組資料的容器ByteBuf的建立方法:

  1. ByteBufAllocator
    Netty 4.x之後預設使用池化(PooledByteBufAllocator)提高效能,最大程度減少記憶體碎片
    非池化:UnPooledByteBufAllocator 每次返回一個新的例項
  2. Unpooled:提供靜態方法建立未池化的ByteBuf,可以建立堆記憶體和直接記憶體緩衝區

ByteBuf使用模式:

  1. 堆快取區
    優點:heap buffer 儲存在 jvm的堆空間中,快速的分配和釋放
    缺點:每次使用前會拷貝到直接快取區 (堆外記憶體)
  2. 直接快取區
    Direct buffer
    優點:不用佔用 JVM 的堆記憶體,儲存在堆外記憶體
    缺點:記憶體的分配和釋放,比在堆快取區更復雜
  3. 複合緩衝區
    建立多個不同的 ByteBuf,然後放在一起,但是隻是一個檢視

選擇:大量 IO 資料讀寫,用直接快取區,業務訊息編解碼用堆快取區

Netty 設計模式

Builder 構造器模式:ServerBootstrap責任鏈設計模式:pipeline的事件傳播工廠模式:建立 channel介面卡模式:HandlerAdapter

Netty 單機百萬實戰

  1. 網路 IO模型
  2. Linux檔案描述符
    單程式檔案描述符(控制程式碼數),每個程式都有最大的檔案描述符限制
    全域性檔案控制程式碼數,也有預設值,不同系統版本會不一樣
  3. 如何確定唯一 TCP 連線
    TCP 四元組:源 IP,源埠,目標 IP,目標埠
    服務端埠範圍(1024~65535)

    65545
    優化:
  4. sudo vim /etc/security/limits.conf 修改區域性 fd數目,修改後要重啟,ulimit -n 檢視當前這個使用者每個程式最大 FD 數
root soft nofile 1000000
root hard nofile 1000000
* soft nofile 1000000
* hard nofile 1000000複製程式碼
  1. sudo vim /etc/sysctl.conf 修改全域性 fd 數目
fs.file-max=1000000複製程式碼

sysctl -p 重啟生效引數cat /proc/sys/fs/file-max 檢視全域性fd數目

  1. 重啟生效 reboot

-Xms5g -Xmx5g -XX:NewSize=3g -XX:MaxNewSize=3g

資料鏈路

瀏覽器同域名下資源載入有併發數限制,建議不同資源用不同域名輸入域名-》瀏覽器核心排程-》本地 DNS 解析-》遠端 DNS解析-》IP-》路由多層跳轉-》目的伺服器-》伺服器核心-》應用程式

本文由部落格一文多發平臺 OpenWrite 釋出!