文章很長,建議收藏起來,慢慢讀! Java 高並發 發燒友社群:瘋狂創客圈 奉上以下珍貴的學習資源:
-
免費贈送 經典圖書:《Java高並發核心編程(卷1)》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《Java高並發核心編程(卷2)》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《SpringCloud Nginx高並發核心編程》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文
入大廠 、做架構、大力提升Java 內功 必備的精彩博文 | 2021 秋招漲薪1W + 必備的精彩博文 |
---|---|
1:Redis 分布式鎖 (圖解-秒懂-史上最全) | 2:Zookeeper 分布式鎖 (圖解-秒懂-史上最全) |
3: Redis與MySQL雙寫一致性如何保證? (面試必備) | 4: 面試必備:秒殺超賣 解決方案 (史上最全) |
5:面試必備之:Reactor模式 | 6: 10分鍾看懂, Java NIO 底層原理 |
7:TCP/IP(圖解+秒懂+史上最全) | 8:Feign原理 (圖解) |
9:DNS圖解(秒懂 + 史上最全 + 高薪必備) | 10:CDN圖解(秒懂 + 史上最全 + 高薪必備) |
11: 分布式事務( 圖解 + 史上最全 + 吐血推薦 ) | 12:seata AT模式實戰(圖解+秒懂+史上最全) |
13:seata 源碼解讀(圖解+秒懂+史上最全) | 14:seata TCC模式實戰(圖解+秒懂+史上最全) |
Java 面試題 30個專題 , 史上最全 , 面試必刷 | 阿里、京東、美團... 隨意挑、橫着走!!! |
---|---|
1: JVM面試題(史上最強、持續更新、吐血推薦) | 2:Java基礎面試題(史上最全、持續更新、吐血推薦 |
3:架構設計面試題 (史上最全、持續更新、吐血推薦) | 4:設計模式面試題 (史上最全、持續更新、吐血推薦) |
17、分布式事務面試題 (史上最全、持續更新、吐血推薦) | 一致性協議 (史上最全) |
29、多線程面試題(史上最全) | 30、HR面經,過五關斬六將后,小心陰溝翻船! |
9.網絡協議面試題(史上最全、持續更新、吐血推薦) | 更多專題, 請參見【 瘋狂創客圈 高並發 總目錄 】 |
SpringCloud 精彩博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
SpringCloud gateway (史上最全) | 更多專題, 請參見【 瘋狂創客圈 高並發 總目錄 】 |
Netty解決Selector空輪詢BUG的策略(圖解+秒懂+史上最全)
Selector 的空輪詢BUG
若Selector的輪詢結果為空,也沒有wakeup或新消息處理,則發生空輪詢,CPU使用率100%。
注意:是CPU 100%,非常嚴重的bug。
這個臭名昭著的epoll bug,是 JDK NIO的BUG,官方聲稱在JDK1.6版本的update18修復了該問題,但是直到JDK1.7、JDK1.8版本該問題仍舊存在,只不過該BUG發生概率降低了一些而已,它並沒有被根本解決。該BUG以及與該BUG相關的問題單可以參見以下鏈接內容:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933
Netty解決空輪詢的4步驟:
Netty的解決辦法總覽:
- 1、對Selector的select操作周期進行統計,每完成一次空的select操作進行一次計數,若在某個周期內連續發生N次空輪詢,則觸發了epoll死循環bug。
- 2、重建Selector,判斷是否是其他線程發起的重建請求,若不是則將原SocketChannel從舊的Selector上去除注冊,重新注冊到新的Selector上,並將原來的Selector關閉。
Netty解決空輪詢的4步驟
Netty解決空輪詢的4步驟,具體如下:
第一部分:定時阻塞select(timeMillins)
- 先定義當前時間currentTimeNanos。
- 接着計算出一個執行最少需要的時間timeoutMillis。
- 定時阻塞select(timeMillins) 。
- 每次對selectCnt做++操作。
第二部分:有效IO事件處理邏輯
第三部分:超時處理邏輯
- 如果查詢超時,則seletCnt重置為1。
第四步: 解決空輪詢 BUG
- 一旦到達SELECTOR_AUTO_REBUILD_THRESHOLD這個閥值,就需要重建selector來解決這個問題。
- 這個閥值默認是512。
- 重建selector,重新注冊channel通道
Netty解決空輪詢的4步驟的核心代碼
long time = System.nanoTime();
//調用select方法,阻塞時間為上面算出的最近一個將要超時的定時任務時間
int selectedKeys = selector.select(timeoutMillis);
//計數器加1
++selectCnt;
if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
//進入這個分支,表示正常場景
//selectedKeys != 0: selectedKeys個數不為0, 有io事件發生
//oldWakenUp:表示進來時,已經有其他地方對selector進行了喚醒操作
//wakenUp.get():也表示selector被喚醒
//hasTasks() || hasScheduledTasks():表示有任務或定時任務要執行
//發生以上幾種情況任一種則直接返回
break;
}
//此處的邏輯就是: 當前時間 - 循環開始時間 >= 定時select的時間timeoutMillis,說明已經執行過一次阻塞select(), 有效的select
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
//進入這個分支,表示超時,屬於正常的場景
//說明發生過一次阻塞式輪詢, 並且超時
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//進入這個分支,表示沒有超時,同時 selectedKeys==0
//屬於異常場景
//表示啟用了select bug修復機制,
//即配置的io.netty.selectorAutoRebuildThreshold
//參數大於3,且上面select方法提前返回次數已經大於
//配置的閾值,則會觸發selector重建
//進行selector重建
//重建完之后,嘗試調用非阻塞版本select一次,並直接返回
selector = this.selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
Netty對Selector.select提前返回的檢測和處理邏輯主要在NioEventLoop.select方法中,完整的代碼如下:
public final class NioEventLoop extends SingleThreadEventLoop {
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
//計數器置0
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
//根據注冊的定時任務,獲取本次select的阻塞時間
long selectDeadLineNanos = currentTimeNanos + this.delayNanos(currentTimeNanos);
while(true) {
//每次循環迭代都重新計算一次select的可阻塞時間
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
//如果可阻塞時間為0,表示已經有定時任務快要超時
//此時如果是第一次循環(selectCnt=0),則調用一次selector.selectNow,然后退出循環返回
//selectorNow方法的調用主要是為了盡可能檢測出准備好的網絡事件進行處理
if (timeoutMillis <= 0L) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
//如果沒有定時任務超時,但是有以前注冊的任務(這里不限定是定時任務),
//且成功設置wakenUp為true,則調用selectNow並返回
if (this.hasTasks() && this.wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
//調用select方法,阻塞時間為上面算出的最近一個將要超時的定時任務時間
int selectedKeys = selector.select(timeoutMillis);
//計數器加1
++selectCnt;
if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
//進入這個分支,表示正常場景
//selectedKeys != 0: selectedKeys個數不為0, 有io事件發生
//oldWakenUp:表示進來時,已經有其他地方對selector進行了喚醒操作
//wakenUp.get():也表示selector被喚醒
//hasTasks() || hasScheduledTasks():表示有任務或定時任務要執行
//發生以上幾種情況任一種則直接返回
break;
}
//如果線程被中斷,計數器置零,直接返回
if (Thread.interrupted()) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely because Thread.currentThread().interrupt() was called. Use NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
}
selectCnt = 1;
break;
}
//這里判斷select返回是否是因為計算的超時時間已過,
//這種情況下也屬於正常返回,計數器置1,進入下次循環
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
//進入這個分支,表示超時,屬於正常的場景
//說明發生過一次阻塞式輪詢, 並且超時
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//進入這個分支,表示沒有超時,同時 selectedKeys==0
//屬於異常場景
//表示啟用了select bug修復機制,
//即配置的io.netty.selectorAutoRebuildThreshold
//參數大於3,且上面select方法提前返回次數已經大於
//配置的閾值,則會觸發selector重建
//進行selector重建
//重建完之后,嘗試調用非阻塞版本select一次,並直接返回
selector = this.selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
//這種是對於關閉select bug修復機制的程序的處理,
//簡單記錄日志,便於排查問題
if (selectCnt > 3 && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector);
}
} catch (CancelledKeyException var13) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, var13);
}
}
}
private Selector selectRebuildSelector(int selectCnt) throws IOException {
logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, this.selector);
//進行selector重建
this.rebuildSelector();
Selector selector = this.selector;
//重建完之后,嘗試調用非阻塞版本select一次,並直接返回
selector.selectNow();
return selector;
}
}
上面調用的this.rebuildSelector()源碼如下:
public final class NioEventLoop extends SingleThreadEventLoop {
public void rebuildSelector() {
//如果不在該線程中,則放到任務隊列中
if (!this.inEventLoop()) {
this.execute(new Runnable() {
public void run() {
NioEventLoop.this.rebuildSelector0();
}
});
} else {
//否則表示在該線程中,直接調用實際重建方法
this.rebuildSelector0();
}
}
private void rebuildSelector0() {
Selector oldSelector = this.selector;
//如果舊的selector為空,則直接返回
if (oldSelector != null) {
NioEventLoop.SelectorTuple newSelectorTuple;
try {
//新建一個新的selector
newSelectorTuple = this.openSelector();
} catch (Exception var9) {
logger.warn("Failed to create a new Selector.", var9);
return;
}
int nChannels = 0;
Iterator var4 = oldSelector.keys().iterator();
//對於注冊在舊selector上的所有key,依次重新在新建的selecor上重新注冊一遍
while(var4.hasNext()) {
SelectionKey key = (SelectionKey)var4.next();
Object a = key.attachment();
try {
if (key.isValid() && key.channel().keyFor(newSelectorTuple.unwrappedSelector) == null) {
int interestOps = key.interestOps();
key.cancel();
SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
if (a instanceof AbstractNioChannel) {
((AbstractNioChannel)a).selectionKey = newKey;
}
++nChannels;
}
} catch (Exception var11) {
logger.warn("Failed to re-register a Channel to the new Selector.", var11);
if (a instanceof AbstractNioChannel) {
AbstractNioChannel ch = (AbstractNioChannel)a;
ch.unsafe().close(ch.unsafe().voidPromise());
} else {
NioTask<SelectableChannel> task = (NioTask)a;
invokeChannelUnregistered(task, key, var11);
}
}
}
//將該NioEventLoop關聯的selector賦值為新建的selector
this.selector = newSelectorTuple.selector;
this.unwrappedSelector = newSelectorTuple.unwrappedSelector;
try {
//關閉舊的selector
oldSelector.close();
} catch (Throwable var10) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to close the old Selector.", var10);
}
}
if (logger.isInfoEnabled()) {
logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
}
}
}
}
Netty空輪詢的閾值配置
Netty在NioEventLoop中考慮了這個問題,並通過在select方法不正常返回(Netty源碼注釋稱其為prematurely,即提前返回)超過一定次數時重新創建新的Selector來修復此bug。
Netty提供了配置參數io.netty.selectorAutoRebuildThreshold供用戶定義select創建新Selector提前返回的次數閾值,超過該次數則會觸發Selector自動重建,默認為512。
但是如果指定的io.netty.selectorAutoRebuildThreshold小於3在Netty中被視為關閉了該功能。
public final class NioEventLoop extends SingleThreadEventLoop {
private static final int SELECTOR_AUTO_REBUILD_THRESHOLD;
static {
//......省略部分代碼
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
if (selectorAutoRebuildThreshold < 3) {
selectorAutoRebuildThreshold = 0;
}
SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.noKeySetOptimization: {}", DISABLE_KEY_SET_OPTIMIZATION);
logger.debug("-Dio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD);
}
}
}
參考文獻:
https://www.jianshu.com/p/b1ba37b6563b
https://blog.csdn.net/zhengchao1991/article/details/106534280