spark streaming、kafka 內存調優、分區調優

  1. spark Streaming 限速
    spark.streaming.kafka.maxRatePerPartition

而在Direct Approach,則是通過參數 spark.streaming.kafka.maxRatePerPartition 來配置的。這裏需要注意的是,這裏是對每個Partition進行限速。所以你需要事先知道Kafka有多少個分區,纔好評估系統的實際吞吐量,從而設置該值。

業務需要做事務,保證 Exactly Once 語義
這裏業務場景被區分爲兩個:
冪等操作
業務代碼需要自身添加事物操作

所謂冪等操作就是重複執行不會產生問題,如果是這種場景下,你不需要額外做任何工作。但如果你的應用場景是不允許數據被重複執行的,那隻能通過業務自身的邏輯代碼來解決了。

dstream.foreachRDD { (rdd, time) =>
rdd.foreachPartition { partitionIterator =>
val partitionId = TaskContext.get.partitionId()
val uniqueId = generateUniqueId(time.milliseconds, partitionId)
// use this uniqueId to transactionally commit the data in partitionIterator
}
}
拿ConsumerGroupid+topic+patition作爲key hbase數據庫自動去重
這代碼啥含義呢? 就是說針對每個partition的數據,產生一個uniqueId,只有這個partion的所有數據被完全消費,則算成功,否則算失敗,要回滾。下次重複執行這個uniqueId 時,如果已經被執行成功過的,則skip掉。
這樣,就能保證數據 Exactly Once 語義啦。

  1. 論SparkStreaming的數據可靠性和一致性

Driver HA
由於流計算系統是長期運行、且不斷有數據流入,因此其Spark守護進程(Driver)的可靠性至關重要,它決定了Streaming程序能否一直正確地運行下去。
Driver實現HA的解決方案就是將元數據持久化,以便重啓後的狀態恢復。如圖一所示,Driver持久化的元數據包括:
Block元數據(圖1中的綠色箭頭):Receiver從網絡上接收到的數據,組裝成Block後產生的Block元數據;
Checkpoint數據(圖1中的橙色箭頭):包括配置項、DStream操作、未完成的Batch狀態、和生成的RDD數據等;

堆內內存(On-heap Memory)
默認情況下,Spark 僅僅使用了堆內內存。Executor 端的堆內內存區域大致可以分爲以下四大塊:
Execution 內存:主要用於存放 Shuffle、Join、Sort、Aggregation 等計算過程中的臨時數據
Storage 內存:主要用於存儲 spark 的 cache 數據,例如RDD的緩存、unroll數據;
用戶內存(User Memory):主要用於存儲 RDD 轉換操作所需要的數據,例如 RDD 依賴等信息。
預留內存(Reserved Memory):系統預留內存,會用來存儲Spark內部對象。

  1. 堆外內存(Off-heap Memory)

Spark 1.6 開始引入了Off-heap memory(詳見SPARK-11389)。這種模式不在 JVM 內申請內存,而是調用 Java 的 unsafe 相關 API 進行諸如 C 語言裏面的 malloc() 直接向操作系統申請內存,由於這種方式不進過 JVM 內存管理,所以可以避免頻繁的 GC,這種內存申請的缺點是必須自己編寫內存申請和釋放的邏輯。
默認情況下,堆外內存是關閉的,我們可以通過 spark.memory.offHeap.enabled 參數啓用,並且通過 spark.memory.offHeap.size 設置堆外內存大小,單位爲字節。如果堆外內存被啓用,那麼 Executor 內將同時存在堆內和堆外內存,兩者的使用互補影響,這個時候 Executor 中的 Execution 內存是堆內的 Execution 內存和堆外的 Execution 內存之和,同理,Storage 內存也一樣。相比堆內內存,堆外內存只區分 Execution 內存和 Storage 內存,其內存分佈如下圖所示:

  1. Execution 內存和 Storage 內存動態調整
    細心的同學肯定看到上面兩張圖中的 Execution 內存和 Storage 內存之間存在一條虛線,這是爲什麼呢?
    用過 Spark 的同學應該知道,在 Spark 1.5 之前,Execution 內存和 Storage 內存分配是靜態的,換句話說就是如果 Execution 內存不足,即使 Storage 內存有很大空閒程序也是無法利用到的;反之亦然。這就導致我們很難進行內存的調優工作,我們必須非常清楚地瞭解 Execution 和 Storage 兩塊區域的內存分佈。而目前 Execution 內存和 Storage 內存可以互相共享的。也就是說,如果 Execution 內存不足,而 Storage 內存有空閒,那麼 Execution 可以從 Storage 中申請空間;反之亦然。所以上圖中的虛線代表 Execution 內存和 Storage 內存是可以隨着運作動態調整的,這樣可以有效地利用內存資源。Execution 內存和 Storage 內存之間的動態調整可以概括如下:

具體的實現邏輯如下:
程序提交的時候我們都會設定基本的 Execution 內存和 Storage 內存區域(通過 spark.memory.storageFraction 參數設置);
在程序運行時,如果雙方的空間都不足時,則存儲到硬盤;將內存中的塊存儲到磁盤的策略是按照 LRU 規則進行的。若己方空間不足而對方空餘時,可借用對方的空間;(存儲空間不足是指不足以放下一個完整的 Block)
Execution 內存的空間被對方佔用後,可讓對方將佔用的部分轉存到硬盤,然後"歸還"借用的空間
Storage 內存的空間被對方佔用後,目前的實現是無法讓對方"歸還",因爲需要考慮 Shuffle 過程中的很多因素,實現起來較爲複雜;而且 Shuffle 過程產生的文件在後面一定會被使用到,而 Cache 在內存的數據不一定在後面使用。
注意,上面說的借用對方的內存需要借用方和被借用方的內存類型都一樣,都是堆內內存或者都是堆外內存,不存在堆內內存不夠去借用堆外內存的空間。

  1. Task 之間內存分佈
    爲了更好地使用使用內存,Executor 內運行的 Task 之間共享着 Execution 內存。具體的,Spark 內部維護了一個 HashMap 用於記錄每個 Task 佔用的內存。當 Task 需要在 Execution 內存區域申請 numBytes 內存,其先判斷 HashMap 裏面是否維護着這個 Task 的內存使用情況,如果沒有,則將這個 Task 內存使用置爲0,並且以 TaskId 爲 key,內存使用爲 value 加入到 HashMap 裏面。之後爲這個 Task 申請 numBytes 內存,如果 Execution 內存區域正好有大於 numBytes 的空閒內存,則在 HashMap 裏面將當前 Task 使用的內存加上 numBytes,然後返回;如果當前 Execution 內存區域無法申請到每個 Task 最小可申請的內存,則當前 Task 被阻塞,直到有其他任務釋放了足夠的執行內存,該任務纔可以被喚醒。每個 Task 可以使用 Execution 內存大小範圍爲 1/2N ~ 1/N,其中 N 爲當前 Executor 內正在運行的 Task 個數。一個 Task 能夠運行必須申請到最小內存爲 (1/2N * Execution 內存);當 N = 1 的時候,Task 可以使用全部的 Execution 內存。
    比如如果 Execution 內存大小爲 10GB,當前 Executor 內正在運行的 Task 個數爲5,則該 Task 可以申請的內存範圍爲 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB的範圍。

  2. 分配內存的實例

一個示例
爲了更好的理解上面堆內內存和堆外內存的使用情況,這裏給出一個簡單的例子。
只用了堆內內存
現在我們提交的 Spark 作業關於內存的配置如下:
–executor-memory 18g
由於沒有設置 spark.memory.fraction 和 spark.memory.storageFraction 參數,我們可以看到 Spark UI 關於 Storage Memory 的顯示如下:

如果想及時瞭解Spark、Hadoop或者Hbase相關的文章,歡迎關注微信公共帳號:iteblog_hadoop
上圖很清楚地看到 Storage Memory 的可用內存是 10.1GB,這個數是咋來的呢?根據前面的規則,我們可以得出以下的計算:
systemMemory = spark.executor.memory
reservedMemory = 300MB
usableMemory = systemMemory - reservedMemory
StorageMemory= usableMemory * spark.memory.fraction * spark.memory.storageFraction
如果我們把數據代進去,得出以下的結果:
systemMemory = 18Gb = 19327352832 字節
reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800

usableMemory = systemMemory - reservedMemory = 19327352832 - 314572800 = 19012780032

StorageMemory= usableMemory * spark.memory.fraction * spark.memory.storageFraction
= 19012780032 * 0.6 * 0.5 = 5703834009.6 = 5.312109375GB
不對啊,和上面的 10.1GB 對不上啊。爲什麼呢?這是因爲 Spark UI 上面顯示的 Storage Memory 可用內存其實等於 Execution 內存和 Storage 內存之和,也就是 usableMemory * spark.memory.fraction:
StorageMemory= usableMemory * spark.memory.fraction
= 19012780032 * 0.6 = 11407668019.2 = 10.62421GB
還是不對,這是因爲我們雖然設置了 --executor-memory 18g,但是 Spark 的 Executor 端通過 Runtime.getRuntime.maxMemory 拿到的內存其實沒這麼大,只有 17179869184 字節,所以 systemMemory = 17179869184,然後計算的數據如下:
systemMemory = 17179869184 字節
reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800

usableMemory = systemMemory - reservedMemory = 17179869184 - 314572800 = 16865296384

StorageMemory= usableMemory * spark.memory.fraction
= 16865296384 * 0.6 = 9.42421875 GB
我們通過將上面的 16865296384 * 0.6 字節除於 1024 * 1024 * 1024 轉換成 9.42421875 GB,和 UI 上顯示的還是對不上,這是因爲 Spark UI 是通過除於 1000 * 1000 * 1000 將字節轉換成 GB,如下:
systemMemory = 17179869184 字節
reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800

usableMemory = systemMemory - reservedMemory = 17179869184 - 314572800 = 16865296384

StorageMemory= usableMemory * spark.memory.fraction
= 16865296384 * 0.6 字節 = 16865296384 * 0.6 / (1000 * 1000 * 1000) = 10.1GB
現在終於對上了。
具體將字節轉換成 GB 的計算邏輯如下(core 模塊下面的 /core/src/main/resources/org/apache/spark/ui/static/utils.js):
function formatBytes(bytes, type) {
if (type !== ‘display’) return bytes;
if (bytes == 0) return ‘0.0 B’;
var k = 1000;
var dm = 1;
var sizes = [‘B’, ‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’, ‘EB’, ‘ZB’, ‘YB’];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ’ ’ + sizes[i];
}
我們設置了 --executor-memory 18g,但是 Spark 的 Executor 端通過 Runtime.getRuntime.maxMemory 拿到的內存其實沒這麼大,只有 17179869184 字節,這個數據是怎麼計算的?
Runtime.getRuntime.maxMemory 是程序能夠使用的最大內存,其值會比實際配置的執行器內存的值小。這是因爲內存分配池的堆部分分爲 Eden,Survivor 和 Tenured 三部分空間,而這裏面一共包含了兩個 Survivor 區域,而這兩個 Survivor 區域在任何時候我們只能用到其中一個,所以我們可以使用下面的公式進行描述:

ExecutorMemory = Eden + 2 * Survivor + Tenured
Runtime.getRuntime.maxMemory = Eden + Survivor + Tenured
上面的 17179869184 字節可能因爲你的 GC 配置不一樣得到的數據不一樣,但是上面的計算公式是一樣的。
用了堆內和堆外內存
現在如果我們啓用了堆外內存,情況咋樣呢?我們的內存相關配置如下:
spark.executor.memory 18g
spark.memory.offHeap.enabled true
spark.memory.offHeap.size 10737418240

從上面可以看出,堆外內存爲 10GB,現在 Spark UI 上面顯示的 Storage Memory 可用內存爲 20.9GB,如下:

如果想及時瞭解Spark、Hadoop或者Hbase相關的文章,歡迎關注微信公共帳號:iteblog_hadoop
其實 Spark UI 上面顯示的 Storage Memory 可用內存等於堆內內存和堆外內存之和,計算公式如下:
堆內
systemMemory = 17179869184 字節
reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800

usableMemory = systemMemory - reservedMemory = 17179869184 - 314572800 = 16865296384

totalOnHeapStorageMemory = usableMemory * spark.memory.fraction
= 16865296384 * 0.6 = 10119177830

堆外
totalOffHeapStorageMemory = spark.memory.offHeap.size = 10737418240

StorageMemory = totalOnHeapStorageMemory + totalOffHeapStorageMemory
= (10119177830 + 10737418240) 字節
= (20856596070 / (1000 * 1000 * 1000)) GB
= 20.9 GB

本博文是參考別的博主自己總結,如有問題請留言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章