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