Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

小謝同學發表於2023-04-17

Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下:

一、報錯資訊:

使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖:

格式化後的報錯資訊:

{
	"timestamp": "2023-04-17 22:46:36",
	"status": 500,
	"error": "Internal Server Error",
	"message": "Java heap space",
	"trace": "java.lang.OutOfMemoryError: Java heap 
    ......
}

二、報錯原因:

原始碼分析:

 public final class PlatformDependent {
     // 直接記憶體大小,可透過 “-Dio.netty.maxDirectMemory”引數設定。
     private static final long DIRECT_MEMORY_LIMIT;
     
     // 預設的最大直接記憶體大小 ,方法略。大概意思還是:先獲取“Eclipse OpenJ9”的“sun.misc.VM”引數,如果沒有則獲取JVM的“-XX:MaxDirectMemorySize”作為預設值。
     private static final long MAX_DIRECT_MEMORY = maxDirectMemory0();
     
     static {
        // 其他賦值操作,略
         
      // 給 直接記憶體大小賦值,如果有設定 "-Dio.netty.maxDirectMemory" 引數,則使用使用者設定的,如果沒有則使用預設的
      logger.debug("-Dio.netty.maxDirectMemory: {} bytes", maxDirectMemory);
      DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;
                
     }

    private static void incrementMemoryCounter(int capacity) {
        if (DIRECT_MEMORY_COUNTER != null) {
            long newUsedMemory = DIRECT_MEMORY_COUNTER.addAndGet(capacity);
            
            //關鍵判斷:如果 netty內部使用的記憶體大小 大於 “直接記憶體大小”的話,就丟擲 "OutOfDirectMemoryError"異常。
            if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
                DIRECT_MEMORY_COUNTER.addAndGet(-capacity);
                throw new OutOfDirectMemoryError("failed to allocate " + capacity
                                                 + " byte(s) of direct memory (used: " + (newUsedMemory - capacity)
                                                 + ", max: " + DIRECT_MEMORY_LIMIT + ')');
            }
        }
    }
 }    

總結原因:

1)、Springboot 2.x 以後預設使用 Lettuce作為操作 redis 的客戶端。它是使用 netty 進行網路通訊的。

2)、從spring-boot-starter-data-redis(2.2.3.RELEASE) 依賴可以看出內建使用的確實是 Lettuce 客戶端,分析原始碼得知,lettuce 使用的 netty 框架,引用的netty包netty-common-4.1.43.Final.jar裡面有一個PlatformDependent.java類 ,底層有個-Dio.netty.maxDirectMemory 引數,會自己校驗堆外記憶體是否大於當前服務可使用的記憶體,如果大於則丟擲 OutOfDirectMemoryError(堆外記憶體溢位)。顯然,這是屬於 Netty(netty-common-4.1.43.Final.jar)的bug導致堆外記憶體溢位的。

三、解決方法:

不能使用-Dio.netty.maxDirectMemory 只調大堆外記憶體,這隻能延遲bug出現的時機,不能完全解決該問題。想解決有如下兩個方案:

1、升級 Lettuce客戶端,期待新版本會解決該問題。

2、排除 Lettuce客戶端,切換使用沒有該問題的 Jedis 客戶端。

Netty 框架效能更好,吞吐量更大,但是Springboot預設使用的Lettuce 客戶端對Netty的支援不夠好;

Jedis客戶端雖然沒有Netty更快,但勝在穩定,沒有上述bug。

因此下面使用第二種解決方案:

切換使用Jedis 客戶端:

在data-redis中排除lettuce,在引入jedis

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <!--排除 lettuce -->
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--引入 jedis -->
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

四、檢驗結果:

壓力測試:

測試結果:

解決 Redis 報“OutOfDirectMemoryError(堆外記憶體溢位) 成功”

相關文章