前言
HiveServer2 屬于 Hive 組件的一個(gè)服務(wù),主要提供 Hive 訪問接口,例如可通過 JDBC 的方式提交 Hive 作業(yè),HiveServer2 基于 Java 開發(fā),整個(gè)服務(wù)運(yùn)行過程中,內(nèi)存的管理回收均由 JVM 進(jìn)行控制。在 JVM 語言中的內(nèi)存泄漏與 C/C++ 語言的內(nèi)存泄漏會有些差異,JVM 的內(nèi)存泄漏更多的是業(yè)務(wù)代碼邏輯錯誤引起大量對象引用被持有,導(dǎo)致多次 GC 均無法被回收,或者部分對象占用內(nèi)存過大,直接超過 JVM 分配的內(nèi)存上限,導(dǎo)致 JVM 內(nèi)存耗盡,引起 JVM 的 OOM。這種情況下該 JVM 服務(wù)會停止響應(yīng)并且退出,但是并不會引起操作系統(tǒng)的崩潰。
背景
近期收到反饋,一套開啟高可用的 EMR 集群中的 HiveServer2 一段時(shí)間后便會停止服務(wù),此集群的 HiveServer2 一共有3個(gè)節(jié)點(diǎn),狀態(tài)信息注冊至 Zookeeper 中,提供 HA 的能力,一段時(shí)間后幾乎3個(gè)節(jié)點(diǎn)都會停止服務(wù),通過對 HiveServer2 的日志查看發(fā)現(xiàn)是大量的 FULL GC后出現(xiàn) OOM:
了解到該集群是一套從線下私有化部署的集群遷移而來,遷移前的集群中 HiveServer2 的 heapsize 為 2G,于是為了對齊業(yè)務(wù)參數(shù)將 heapsize 調(diào)整至 2G,間隔一天后,再次收到反饋,OOM 的問題依舊存在,查看日志,問題依舊是 HiveServer2 發(fā)生了 OOM,由于參數(shù)已經(jīng)對齊之前的配置,那么問題可能不單純是內(nèi)存不足,可能會有其他問題。于是首先將 HiveServer2 的 heapsize 調(diào)整為 4G,確?梢栽谝欢〞r(shí)間內(nèi)穩(wěn)定運(yùn)行,留下定位時(shí)間。
定位
定位方向?yàn)閮蓚(gè)方向:一個(gè)是分析 dump file,查看在內(nèi)存不足的時(shí)候,內(nèi)存消耗在哪些地方;第二個(gè)方向是針對日志進(jìn)行細(xì)粒度分析,確保整個(gè)流程執(zhí)行順序是合理的。
通過對 JVM 的 dump 文件進(jìn)行分析,定位到在發(fā)生 HiveServer2 的 OOM 的時(shí)候,queryIdOperation 這個(gè) ConcurrentHashMap 占據(jù)了大量的內(nèi)存,而此時(shí) HiveServer2 的負(fù)載非常低。
再基于具體的 QueryId 進(jìn)行跟蹤日志,HiveServer2 對作業(yè)處理的邏輯為在建立 Connection 的時(shí)候會調(diào)用一次 OpenSession,拿到一個(gè)HiveConnection 對象,此后便通過 HiveConnection 對象調(diào)用 ExecuteStatement 執(zhí)行 SQL,后臺每接收到一個(gè) SQL 作業(yè)便生成一個(gè) Operation 對象用來對 SQL 作業(yè)實(shí)現(xiàn)隔離。
每一個(gè) Operation 有自己獨(dú)立的 QueryId,每條 SQL 作業(yè)會經(jīng)歷編譯,執(zhí)行,關(guān)閉環(huán)節(jié),注意此關(guān)閉指的是關(guān)閉當(dāng)前執(zhí)行的 SQL 作業(yè),而不是關(guān)閉整個(gè) HiveServer2 的連接,基于此思路追蹤日志,發(fā)現(xiàn)部分 QueryId 沒有執(zhí)行 Close operation 方法。
有了這個(gè)思路后,再對 Hive 的源碼進(jìn)行查閱,發(fā)現(xiàn) Close operation 方法被調(diào)用的前提是在一個(gè)名稱為 queryIdOperation 的 Map 對象中可以找出 QueryId,如果沒有從 queryIdOperation 找到合法的 QueryId,則不會觸發(fā) Close 方法。
再結(jié)合前面的堆棧圖,其中 queryIdOperation 占據(jù)了大量的內(nèi)存,于是基本可以確定定位出問題的原因,為當(dāng) SQL 執(zhí)行結(jié)束后,有一個(gè) queryIdOperation 的 Map 對象,沒有成功的移除內(nèi)部的內(nèi)容,導(dǎo)致該 Map 越來越大,最后導(dǎo)致 HiveServer2 內(nèi)存耗盡,出現(xiàn) OOM,有了這個(gè)大概的思路,就需要仔細(xì)分析為什么會出現(xiàn)這個(gè)問題,從而找到具體的解決方案。
分析
在解決這個(gè)問題之前,先對 HiveServer2 本身做一個(gè)分析,HiveServer2 不同于一般的數(shù)據(jù)庫服務(wù),HiveServer2 是由一系列的 RPC 接口組成,具體的接口定義在 org.apache.hive.service.rpc.thrift 包下的 TCLIService.Iface 中,部分接口如下:
更多關(guān)于接口和服務(wù)器的知識可查看:干貨 | 在字節(jié)跳動,一個(gè)更好的企業(yè)級SparkSQL Server這么做
每一個(gè) RPC 接口之間相互獨(dú)立,一個(gè)作業(yè)從連接到執(zhí)行 SQL 再到作業(yè)結(jié)束,會調(diào)用一系列的 RPC 接口組合完成這個(gè)動作,中間通過 OperationHandle 中的 THandleIdentifier 作為唯一 session id,由客戶端每次執(zhí)行的時(shí)候進(jìn)行傳遞,THandleIdentifier 在 OpenSession 的時(shí)候被創(chuàng)建。
HiveServer2 基于此對整個(gè)作業(yè)的執(zhí)行進(jìn)行管理。具體的調(diào)用順序,以及調(diào)用何種接口,對于使用者是透明的,常用的客戶端例如 Hive JDBC Driver 或者 PyHive 等已經(jīng)封裝了對應(yīng)的調(diào)用順序,使用者只需要關(guān)心正常的打開連接,執(zhí)行 SQL,關(guān)閉連接即可,與標(biāo)準(zhǔn)的數(shù)據(jù)庫操作邏輯保持一致。
一個(gè)簡單的調(diào)用邏輯如上圖所示,當(dāng)一個(gè) Connection 執(zhí)行多條 SQL 后,每一條 SQL 都是一個(gè) Operation 進(jìn)行記錄,并且各自擁有各自的 Query Id,HiveServer 基于此 Query Id 做一些狀態(tài)的管理,當(dāng)連接結(jié)束后,調(diào)用 CloseOperation 清理所有內(nèi)容。
每一條 SQL 執(zhí)行結(jié)束后,都會調(diào)用 CloseOperation 進(jìn)行相關(guān)的狀態(tài)清除,如果清除失敗,當(dāng) connection 被 close 的時(shí)候,也會循環(huán)調(diào)用 CloseOperation 去清理狀態(tài),確保狀態(tài)的一致性。這里需要注意的是,既然 HiveServer2 是一系列的獨(dú)立 RPC 接口,那么必然會出現(xiàn)萬一用戶不調(diào)用某些接口怎么辦,例如不調(diào)用 CloseSession,HiveServer2 為了解決這個(gè)問題內(nèi)置了一個(gè)超時(shí)機(jī)制,當(dāng) Connection 達(dá)到超時(shí)的閾值后,會執(zhí)行 close 動作,清除 Session 和 Operation 的狀態(tài),具體的實(shí)現(xiàn)在 SessionManager 中的 startTimeoutChecker 方法中:
有了這些知識,再來分析前面出現(xiàn) OOM 的問題,出現(xiàn) OOM 是一個(gè)名叫 queryIdOperation 的 ConcurrentHashMap 對象占據(jù)了大量的內(nèi)存,對這個(gè)對象分析會發(fā)現(xiàn)這個(gè)對象位于:
一個(gè) Hive Connection 被打開后,可以執(zhí)行多條 SQL,每一條 SQL 都是一個(gè)獨(dú)立的 Operation,此 Map 維護(hù)一個(gè) queryId 和 Operation 的關(guān)系。
當(dāng)一個(gè)新的 SQL 作業(yè)到達(dá)的時(shí)候,QueryState 對象的 build 方法會構(gòu)建出一個(gè) queryState,在這里生成此 SQL 的唯一標(biāo)記,也就是 QueryId:
并且將該 QueryId 添加至 Connection 對象持有的 Hive Session,同時(shí)調(diào)用 OperationManager 的 addOperation 方法將此對象添加至 Map 中:
當(dāng)作業(yè)執(zhí)行結(jié)束后,通過 OperationManager.closeOperation 調(diào)用 removeOperation 移除該 Map 中的映射:
而 Query Id 是通過頂層的 Connection 中的 HiveSession 中去獲。
即使這里 removeOperation 失敗了,在 CloseSession,或者 HiveServer2 觸發(fā)超時(shí)動作后,都會再次回收該 Map 對象中的內(nèi)容。
有了這個(gè)思路,于是再去對日志進(jìn)行深度分析,發(fā)現(xiàn):
很多 SQL 作業(yè)在執(zhí)行后,并沒有調(diào)用 removeOperation 的行為,可以看到也就自然沒有觸發(fā)移除 queryIdOperation 的內(nèi)容,那么內(nèi)存被耗盡自然就可以理解,同時(shí)在 SQL 執(zhí)行后會緊接著產(chǎn)生一個(gè)非法 Operation 的堆棧:
思路理到這里,需要想的問題是:為什么沒有觸發(fā) removeOperation 的行為,或者說 removeOperation 沒有執(zhí)行成功,基于前面的理解來看,removeOperation 會有3種觸發(fā)時(shí)機(jī),分別是:
SQL 作業(yè)執(zhí)行結(jié)束調(diào)用 CloseOperatipn。
Connection 斷開調(diào)用 CloseSession。
HiveServer2 自身的狀態(tài)判斷 Connection 超時(shí)發(fā)起 Close。
所以沒有被調(diào)用的可能性不大,那么只剩下調(diào)用了,但是沒有執(zhí)行成功,沒有執(zhí)行成功也有2種情況:
執(zhí)行了,但是失敗了。
執(zhí)行成功了,但是沒有移除。
失敗可能性不大,因?yàn)槭×耍敲匆欢〞粝露褩P畔,于是只剩下?zhí)行了但是沒有移除,出現(xiàn)這樣的情況基本就是只能是:
里面查詢出的 QueryId 并不是當(dāng)前作業(yè)的 QueryId,這個(gè) ID 發(fā)生了篡改,那么什么樣的情況下會發(fā)生篡改?再來理一理 HiveServer 的狀態(tài)邏輯:
一個(gè) Connection 執(zhí)行 SQL 的時(shí)候,會先產(chǎn)生一個(gè) Operation,并且生成一個(gè) Query Id,將這個(gè) Query Id 設(shè)置成全局 HiveSession的內(nèi)容:
同時(shí)把這些信息存儲到這兩個(gè) Map 中:
在 close 的時(shí)候再從 HiveSession 中去查詢出來,由于 HiveServer2 是一系列的獨(dú)立 RPC 請求,因此不能保證整個(gè)流程的原子性,那么想一種情況,假設(shè) N 個(gè)并行線程,同時(shí)持有一個(gè) Hive Connection,且同時(shí)開始發(fā)送 SQL 會怎樣?
可以看到如果兩個(gè)子線程同時(shí)使用同一個(gè) Connection 執(zhí)行 SQL,于是會出現(xiàn)一個(gè)線程把另一個(gè)線程的 Query Id 進(jìn)行覆蓋,導(dǎo)致其中一個(gè)線程丟失自己的 Query Id,導(dǎo)致無法成功的從 Map 中移除對象,具體的執(zhí)行思路為:
t1: 線程 A 將 conf 中的 queryId 設(shè)成 A;
t2: 線程 B 將 conf 中的 queryId 設(shè)成 B;
t3: 線程 A 從 conf 中拿到 queryId 為 B,并 close B;
t4: 線程 B 從 conf 中拿到 queryId 為 B,并 close B,出現(xiàn)異常。
于是一直遺留了 queryId A,因?yàn)閮蓚(gè)線程同時(shí)變成了相同的 Query Id,當(dāng)其中一個(gè)線程執(zhí)行了 remove 動作后,另一個(gè)線程要基于當(dāng)前 Query Id 再去查詢內(nèi)容的時(shí)候,便會出現(xiàn)緊接著的第二個(gè)錯誤,也就是非法的 Session Id。
由于本次出現(xiàn)問題的使用場景是 Airflow 進(jìn)行調(diào)用,Airflow 具有工作流的能力可同時(shí)在一個(gè) Dag 中并發(fā)開啟 N 個(gè)并行節(jié)點(diǎn),而這些并行節(jié)點(diǎn)在同一個(gè) Dag 下,因此共享同一個(gè) Connection,于是觸發(fā)了這個(gè)問題。
但是我們要知道,多個(gè)線程使用同一個(gè) Connection 是非常常見的現(xiàn)場,特別是在數(shù)據(jù)庫的連接池的概念中,那么為什么沒有出問題呢?這里也就涉及到 HiveServer2 本身的架構(gòu)問題,HiveServer2 本身不是一個(gè)數(shù)據(jù)庫,僅僅提供了兼容 JDBC 接口的協(xié)議和 Driver 而已,因此相比傳統(tǒng)的數(shù)據(jù)庫的連接池,它并不能保證串行,也就是不具有排它效果,當(dāng)然這只是次要問題,主要還是 HiveServer2 實(shí)現(xiàn)的缺陷。
對于此問題的復(fù)現(xiàn),只需要創(chuàng)建一個(gè) HiveConnection,同時(shí)并行開啟多個(gè)線程同時(shí)使用該 Connection 對象執(zhí)行 SQL,便可復(fù)現(xiàn)這個(gè)問題。執(zhí)行過程中觀察 HiveServer2 內(nèi)存變化,可以發(fā)現(xiàn) HiveServer2 的內(nèi)存上升后,并沒有發(fā)生下降,隨著使用時(shí)間的增加,最后直至 OOM。
解決
既然找到了問題,那么解決方案就清楚了,那便是將 Query Id 這個(gè)值設(shè)置成 Operation 級別,而不是 HiveSession 級別,此問題影響 Hive3.x 版本,2.x 暫時(shí)沒有這個(gè)特性,因此不受影響。再對照官方已知的 issue,此問題是已知 issue,目前 Hive 已經(jīng)將此問題修復(fù),且合入了4.0的版本,具體可查看:https://issues.apache.org/jira/browse/HIVE-22275
但是由于該 issue 是針對 4.0.0 的代碼修復(fù)的,對于 3.x 系列并沒有 patch,直接 cherry-pick 將會有大量的代碼不兼容,因此需要自行參考進(jìn)行修復(fù),修復(fù)的思路為給 Operation 新增:
將 Query Id 從 HiveSession 級別移除,存入 Operation 級別,同時(shí)更新 Query Id 的獲取和設(shè)置:
對 Hive 進(jìn)行重新打包,在現(xiàn)有集群上對 hive-service-x.x.x.jar 進(jìn)行替換,即可修復(fù)此問題。
結(jié)尾
雖然有些問題在官方 issue 上已經(jīng)有發(fā)布,但是實(shí)際業(yè)務(wù)過程中我們依舊需要仔細(xì)定位,確保當(dāng)前的問題,與已知問題是一致的,盡可能少的留下隱患,同時(shí)也有助于更加掌握引擎本身的原理和實(shí)現(xiàn)邏輯。只有對問題有清晰的認(rèn)知,且對解決方案的邏輯有足夠的了解,才能保證整個(gè)集群在生產(chǎn)環(huán)境下的穩(wěn)定。
文章內(nèi)容僅供閱讀,不構(gòu)成投資建議,請謹(jǐn)慎對待。投資者據(jù)此操作,風(fēng)險(xiǎn)自擔(dān)。
京東11.11采銷直播探廠為消費(fèi)者揭開答案。近日,京東3C數(shù)碼采銷走進(jìn)武漢攀升工廠、合肥聯(lián)想工廠和科大訊飛展廳,通過直播帶貨廠商爆款產(chǎn)品,并為消費(fèi)者帶來超值低價(jià)與福利。
奧維云網(wǎng)(AVC)推總數(shù)據(jù)顯示,2024年1-9月明火炊具線上零售額94.2億元,同比增加3.1%,其中抖音渠道表現(xiàn)優(yōu)異,同比有14%的漲幅,傳統(tǒng)電商略有下滑,同比降低2.3%。
“以前都要去窗口辦,一套流程下來都要半個(gè)月了,現(xiàn)在方便多了!”打開“重慶公積金”微信小程序,按照提示流程提交相關(guān)材料,僅幾秒鐘,重慶市民曾某的賬戶就打進(jìn)了21600元。
華碩ProArt創(chuàng)藝27 Pro PA279CRV顯示器,憑借其優(yōu)秀的性能配置和精準(zhǔn)的色彩呈現(xiàn)能力,為您的創(chuàng)作工作帶來實(shí)質(zhì)性的幫助,雙十一期間低至2799元,性價(jià)比很高,簡直是創(chuàng)作者們的首選。
9月14日,2024全球工業(yè)互聯(lián)網(wǎng)大會——工業(yè)互聯(lián)網(wǎng)標(biāo)識解析專題論壇在沈陽成功舉辦。