日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

如果某个字段值相同则触发器新增_Thrift IDL新增字段导致版本不一致引发的惨案...

發(fā)布時(shí)間:2025/3/19 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如果某个字段值相同则触发器新增_Thrift IDL新增字段导致版本不一致引发的惨案... 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

公司某業(yè)務(wù)出現(xiàn)嚴(yán)重的線上故障,復(fù)盤發(fā)現(xiàn)原因竟是某接口的 Thrift IDL 變更,未及時(shí)同步所有上游,導(dǎo)致上游某服務(wù) OOM 引發(fā) Crash。看似操作不規(guī)范是本次故障的根因,但進(jìn)一步思考:該接口非主流程接口,即使 IDL 版本不統(tǒng)一,帶來“最壞”的后果不應(yīng)該是“僅僅報(bào)錯(cuò)”嗎,為什么會(huì)產(chǎn)生 OOM 導(dǎo)致整個(gè)服務(wù) Crash?

實(shí)際上這樣的例子并不少見,很多公司內(nèi)部 RPC 使用 Thrift 協(xié)議,IDL 變更及版本不一致在所難免,極端情況下,安全團(tuán)隊(duì)掃描 thrift 端口也會(huì)出現(xiàn)類似故障。為了避免更多團(tuán)隊(duì)采坑,有必要研究 Thrift 何種情況下會(huì)觸發(fā),以及為什么會(huì)觸發(fā) OOM。

一. 從 IDL 的字段變更說起

化繁為簡,該故障的發(fā)生可以這樣描述:某 Thrift 服務(wù)的返回值是一個(gè)數(shù)組,數(shù)組中每個(gè)元素本來包含 5 個(gè)字段,某次調(diào)整后,在中間位置新增 1 個(gè)字段,其余保持不變。服務(wù)上線后,某上游調(diào)用方未收到變更通知,仍使用舊版本的 SDK。參照下圖所示:

看到這里,有經(jīng)驗(yàn)的同學(xué)已經(jīng)瑟瑟發(fā)抖。在 Thrift 協(xié)議跨語言、高性能的背后,做了很多取舍,比如接收方在反序列化時(shí),僅做了方法名等少量的校驗(yàn),通過序號(hào)、字段類型認(rèn)為該返回值屬于哪個(gè)字段,而并未校驗(yàn)字段名,因此一旦上下游 IDL 版本不一致,極易產(chǎn)生字段錯(cuò)位的情況。以上述 IDL 為例,id、image 字段正確解析,而 bg_image 被錯(cuò)誤解析為 link、link 被錯(cuò)誤解析為 title:

字段增加時(shí),考慮到字段位置及新舊版本不匹配的各種場景,我們枚舉可能的后果:

  • 新增字段放到中間位置
  • A. 新Server、舊Client:容易字段錯(cuò)位,引發(fā)業(yè)務(wù)錯(cuò)誤,不推薦
  • B. 舊Server、新Client:容易字段錯(cuò)位,引發(fā)業(yè)務(wù)錯(cuò)誤,不推薦
  • 新增字段放到最后,并且是required
  • A. 新Server、舊Client:舊Client感知不到新增字段,會(huì)忽略
  • B. 舊Server、新Client:新Client獲取不到新增字段,會(huì)報(bào)錯(cuò)
  • 新增字段放到最后,并且是optional
  • A. 新Server、舊Client:舊Client感知不到新增字段,會(huì)忽略
  • B. 舊Server、新Client:新Client獲取不到新增Optional字段,會(huì)忽略
  • 再進(jìn)一步,如果是刪除字段呢?從刪除字段的位置、是否 required,大家可自行思考,當(dāng)然結(jié)果類似,輕則被忽略,重則字段錯(cuò)位,當(dāng)然更嚴(yán)重的會(huì)導(dǎo)致服務(wù) OOM。

    小結(jié):Thrift IDL 不要變更已有字段的序列號(hào),上下游版本不一致極易發(fā)生錯(cuò)位現(xiàn)象。如需新增字段,應(yīng)放到最后并設(shè)置為 optional。

    二. OOM 問題復(fù)現(xiàn)

    回到 IDL 變更上來,為什么會(huì)引發(fā)上游服務(wù)的 OOM 呢?我們用一段很短的 IDL 就可以重現(xiàn)。

    namespace java com.didiglobal.thrift.sample
    struct Items{
    1:required i64 id;
    2:required list<Item> items;
    }
    struct Item {
    1:required string name;
    2:required string image; // 新增字段
    3:required list<string> contents;
    }
    service Sample {
    Items getItems(1:i64 id);
    }

    如果你也想嘗試,可以下載項(xiàng)目代碼,在本地搭建環(huán)境。

  • Github下載項(xiàng)目代碼:https://github.com/aqingsao/thrift-oom
  • 使用你喜歡的IDE導(dǎo)入工程,該項(xiàng)目基于thrift 0.11.0版本,依賴JDK1.8+以及Maven
  • 運(yùn)行包c(diǎn)om.didiglobal.thrift.sample1.sampleold中OldClientNewServerTest.java類的這個(gè)測試用例:oldclient_should_oom_at_concurrency_10
  • 該測試用例會(huì)啟動(dòng)一個(gè)簡單的Thrift服務(wù),客戶端使用10個(gè)并發(fā),很快觸發(fā)OOM(如果遇到問題,可聯(lián)系作者)。
  • 使用不同的 Thrift 版本,0.9.3 到最新的 0.13.0-snapshot 均可以重現(xiàn)。使用 jmap 命令,可以看到應(yīng)用創(chuàng)建了大量的字節(jié)數(shù)組。

    小結(jié):只需要 10 個(gè)并發(fā)就可以重現(xiàn) OOM,該問題廣泛存在于 Thrift 版本 0.9.3 到最新的 0.13.0-snapshot。

    三. 為什么會(huì) OOM?

    本次故障的根因是新增 String 字段,并且客戶端進(jìn)程創(chuàng)建了大量的字節(jié)數(shù)組,首先懷疑字段錯(cuò)位后 Thrift 未正確處理,但查看了下源碼,thrift 對(duì)字段類型不匹配、多余字段均作了 skip 處理:

    查看 skip 方法的實(shí)現(xiàn),一度懷疑在類型為 String 時(shí)調(diào)用了錯(cuò)誤的方法,但考慮 String 使用字節(jié)流傳輸,下圖中直接調(diào)用 readBinary()方法也無不妥,測試后發(fā)現(xiàn)也不是該問題:

    思考了幾個(gè)其他方向,并做了多次嘗試,均發(fā)現(xiàn)思路不對(duì),而且始終無法解釋的是,單個(gè)請(qǐng)求數(shù)據(jù)量只有上百字節(jié),為什么只需要 10 個(gè)并發(fā)、幾十個(gè)請(qǐng)求就會(huì) OOM?這些請(qǐng)求的數(shù)據(jù)量累加起來也不過幾十 K,一度陷入僵局。

    下班的路上反復(fù)思考,是不是和 Thrift 拋出的異常有關(guān),恰遇某個(gè)路口超長時(shí)間的紅燈,每每感嘆該路口浪費(fèi)多少青春年華,此時(shí)卻從容拿出電腦,對(duì) Thrift 拋出的異常做了臨時(shí)處理,運(yùn)行測試,果然不再 OOM 了!

    思路一下子清晰起來:雖然 Thrift 做了多余字段的 skip 處理,但由于拋出的異常,這些 skip 操作并未執(zhí)行到,甚至,List 中第一個(gè)元素校驗(yàn)拋出異常后,后面所有字段都未繼續(xù)消費(fèi)!

    按該思路重新閱讀相關(guān)代碼,果然找到了可疑點(diǎn):Thrift 接受服務(wù)端響應(yīng)時(shí),會(huì)首先解析 TMessage 對(duì)象,前 4 個(gè)字節(jié)(I32)代表了某個(gè)字符串的長度,后面 readStringBody()方法會(huì)分配該長度的字節(jié)數(shù)組(byte[] buf = new byte[size]),該字符串實(shí)際上是 thrift 的方法名,而 debug 發(fā)現(xiàn),長度值是 184549632,大約 176M,這合理解釋了為什么 10 個(gè)并發(fā)就會(huì)觸發(fā) OOM。

    小結(jié):Thrift 接收到請(qǐng)求后首先讀取 TMessage 結(jié)構(gòu),IDL 版本不一致的極端情況下,會(huì)分配 176M 的內(nèi)存空間,導(dǎo)致 10 個(gè)并發(fā)就占用上 G 內(nèi)存,觸發(fā) OOM。

    四. 為什么會(huì)分配 176 兆的內(nèi)存空間?

    解決這個(gè)問題,有助于了解哪些場景下會(huì)觸發(fā) OOM,從而更好地避免。

    參考 Thrift TServiceClient 的 receiveBase()方法,詳細(xì)介紹了返回值的解析過程,參考下圖可以更好地理解,基本上是一個(gè)從前到后類似堆棧的反序列化:

    前面已經(jīng)介紹過,如果返回值中多了參數(shù)、或者參數(shù)類型不對(duì),Thrift 可以通過 skip()操作對(duì)該字段進(jìn)行忽略。但 thrift 對(duì) struct 結(jié)構(gòu)體有個(gè)額外的操作,就是解析完成后的調(diào)用 validate()方法,如果結(jié)構(gòu)不合法,會(huì)拋出異常:

    正常情況下,這里拋出異常,請(qǐng)求失敗,應(yīng)該關(guān)閉該連接,或者想重用連接的話,要先把底層 Socket 的輸入流做清零處理。但 Thrift 未對(duì)輸入流做任何處理,直接重用了該 CLient 實(shí)例及其底層的 Socket 連接,導(dǎo)致下一次解析響應(yīng)數(shù)據(jù)時(shí),讀取的是上一次請(qǐng)求失敗未處理完的數(shù)據(jù)。由于連接及底層 Socket 被重用,下一次發(fā)出請(qǐng)求,很快收到響應(yīng),Thrift 慣例開始讀取消息頭:readMessageBegin→readI32(),由于存在未清空的臟數(shù)據(jù),根據(jù)上圖的堆棧分析,readI32()時(shí)讀取的這 4 個(gè)字節(jié),分別是:

    byte type = TType.STRING; // 字節(jié) 0:Item 第一個(gè)字段(name)的類型,String,值為 11

    short id = 1; // 字節(jié) 1 和 2:Item 第一個(gè)字段(name)的位置,值為 1

    int size = 6; // 字節(jié) 3:Item 第一個(gè)字段(name)的 size 的首字節(jié),值為 6

    byte(1 個(gè)字節(jié))+short(2 個(gè)字節(jié))+int(第 1 個(gè)字節(jié)),按 big endian 編碼,其值恰好等于 184549632:

    小結(jié):184549632 的出現(xiàn)有其必然性,不恰當(dāng)?shù)?IDL 變更,Thrift 客戶端拋出異常后未做輸入流清理,下一個(gè)請(qǐng)求會(huì)把之前的殘留數(shù)據(jù),錯(cuò)誤解析成字符串大小,很快導(dǎo)致 OOM。

    五. 沒遇到 OOM,我是不是很幸運(yùn)?

    當(dāng)然依 IDL 不同、異常拋出的位置不同,讀取消息頭時(shí) readI32()讀取的數(shù)字不一定恰好是 184549632。項(xiàng)目中提供了一個(gè)叫做 sample2 的 IDL,舊 Client 訪問新 server 時(shí),該值的大小是 8388864,大約等于 8M,因此 10 個(gè)并發(fā)甚至 100 個(gè)并發(fā)也不一定會(huì)觸發(fā) OOM。

    所以當(dāng)有不恰當(dāng)?shù)?IDL 變更,你沒遇到 OOM,只是幸運(yùn)而已。

    但進(jìn)一步分析,這種情況雖不會(huì)觸發(fā) OOM,但客戶端一直等待服務(wù)端返回 8388864 個(gè)字節(jié)的數(shù)據(jù),久等而不得,于是該連接被阻塞了,一直到超時(shí)。

    小結(jié):不恰當(dāng)?shù)?IDL 變更,不會(huì)全部導(dǎo)致 OOM,也有可能導(dǎo)致大量連接被阻塞,直到超時(shí)錯(cuò)誤。

    六. 如何修復(fù) OOM?

    參考 Thrift themissing guide 文檔,IDL 變更要有規(guī)范,并在團(tuán)隊(duì)內(nèi)反復(fù)宣導(dǎo)。

    諷刺的是,“通過規(guī)范、最佳實(shí)踐來避免 Crash 類問題”,本身就不是一種最佳實(shí)踐,至少使用 Http 協(xié)議不用遇到這種問題。

    這可以認(rèn)為是 Thrift 自身的一個(gè)缺陷,作為當(dāng)前廣泛使用的 RPC 協(xié)議,需要優(yōu)雅地處理用戶必須遇到的 IDL 變更的問題。

    思路一:讀取任何 struct 類型的字段時(shí),catch 住異常,保障 thrift 把輸入流的所有數(shù)據(jù)讀完

    Thrift 在反序列化階段,遇到任何 struct 類型的字段,讀取結(jié)束后都會(huì)調(diào)用 validate()方法,因此在 struct 類型字段的讀取時(shí),可以添加 try-catch 校驗(yàn)異常,保證 thrift 讀完所有的數(shù)據(jù)。

    該思路經(jīng)測試驗(yàn)證可以,但需要修改的地方較多,不具備可操作性。

    思路二:無論是否拋出異常,從協(xié)議層面保證清空輸入流的殘留數(shù)據(jù)

    該思路具備較好的收斂性,只需要修改 Thrift 源碼的 2 個(gè)文件:TServiceClient 修改如下圖

    1,TSocket 類的修改如下圖 2:

    經(jīng)壓測驗(yàn)證,在 Macbook Air 上,使用 100 個(gè)并發(fā)進(jìn)行驗(yàn)證,持續(xù) 15 分鐘,累計(jì)發(fā)送~160 萬次請(qǐng)求,單次請(qǐng)求響應(yīng)數(shù)據(jù)量~0.5K,除了正常報(bào)錯(cuò),客戶端、服務(wù)端內(nèi)存無任何異常。

    小結(jié):Thrift IDL 變更不可避免,上下游版本不一致也不可避免,協(xié)議其實(shí)可以做到優(yōu)雅地去處理,而不是 OOM 了事。

    七. 有沒有臨時(shí)方案?

    思路一:使用嚴(yán)格讀模式?

    Thrift 眾多的配置項(xiàng)中,有嚴(yán)格讀(strictRead)、嚴(yán)格寫(strictWrite)兩個(gè)選項(xiàng),由于嚴(yán)格讀默認(rèn)值為 False,改為 true 是否可以呢?如果 OK 的話,只需要修改配置而無需修改源碼,這將會(huì)是最輕量級(jí)的方案。查看 Thrift 源碼,如果命中嚴(yán)格讀會(huì)提前報(bào)錯(cuò),避免下方 readStringBody()方法分配太大的內(nèi)存空間:

    嚴(yán)格讀(strictRead)的修改方式如下:

    // 很多情況下大家如此使用 TProtocol 創(chuàng)建連接,此時(shí) strictWrite 值為 true,而、strictRead 值為 false;

    TProtocol protocol = new TBinaryProtocol(transport);

    // 嚴(yán)格讀:直接創(chuàng)建 TBinaryProtocol,傳入?yún)?shù)

    TProtocol protocol = new TBinaryProtocol(transport, true, true);

    // 嚴(yán)格讀:使用 Protocol 工廠模式,傳入?yún)?shù)

    TBinaryProtocol.Factory protFactory = new TBinaryProtocol.Factory(true, true);

    經(jīng)測試驗(yàn)證,使用嚴(yán)格讀之后,客戶端還會(huì)報(bào)錯(cuò),但 OOM 問題消失了!“貌似”這也是我們想要的結(jié)果,壓測效果如何呢?

    在 MacbookAir 上,使用 100 個(gè)并發(fā)進(jìn)行驗(yàn)證,5 分鐘之后,發(fā)送大約 56 萬請(qǐng)求,客戶端出現(xiàn)大量的 SocketException(Broken pipe),甚至客戶端開始宕死。

    究其原因,strictRead 雖然避免了創(chuàng)建大量字節(jié)數(shù)組,但拋異常時(shí) thrift 也未對(duì)輸入流做任何清理,會(huì)產(chǎn)生誤讀取殘余數(shù)據(jù),甚至引起連接阻塞。

    所以使用嚴(yán)格讀,并不能解決這一問題,同樣 thrift 提供了另外一個(gè)配置項(xiàng)“讀取字符串或字節(jié)的最大長度(stringLengthLimit_)”,也不能解決該問題。

    思路二:使用短連接?

    每個(gè)請(qǐng)求創(chuàng)建一個(gè)短連接,用完就關(guān)閉?這樣雖可以避免請(qǐng)求之間的數(shù)據(jù)污染,但毫無疑問也會(huì)帶來較大的性能損失,通常不建議。

    思路三:使用 TFramedTransport?

    TFramedTransport 對(duì)整幀數(shù)據(jù)使用了緩存,一次性把底層 Socket 中 Inputstream 中的數(shù)據(jù)完全讀到了 readBuffer,理論上不會(huì)出現(xiàn) OOM 了。

    驗(yàn)證了一把,很可惜還是會(huì) OOM,原因是拋異常之后,TFramedTransport 中的 readBuffer 中的數(shù)據(jù)未做清理,下次請(qǐng)求會(huì)錯(cuò)誤讀取 readBuffer 中的殘余數(shù)據(jù)。

    如果你想驗(yàn)證,可以運(yùn)行 OldClientNewServerTest 中的測試用例:oldclient_should_oom_if_use_TFramedTransport_at_concurrency_10

    小結(jié):使用嚴(yán)格讀、限制字符串長度等配置方式,使用 TFramedTransport 都不能完美解決問題,要么 OOM,要么連接被阻塞。而短連接對(duì)性能影響較大,在 Thrift 中也很少使用。

    八. 總結(jié)

    Thrift IDL 不恰當(dāng)?shù)刈兏?#xff0c;當(dāng)上下游 IDL 版本不一致時(shí),極易引發(fā)字段錯(cuò)位、連接阻塞、甚至 OOM。Java、Go 等語言實(shí)現(xiàn)中均存在該問題,并廣泛存在于 Thrift 0.9 到最新的 0.13.0-snapshot 等各個(gè)版本。

    該問題觸發(fā)的根本原因是:客戶端接收數(shù)據(jù)后,對(duì) struct 類型做 validate()時(shí)拋出異常,并未對(duì)底層的 socket 輸入流做清零處理,后續(xù)請(qǐng)求誤把殘留數(shù)據(jù)讀作字節(jié)長度,極易引發(fā) OOM,或者導(dǎo)致 Socket 連接阻塞超時(shí)。

    Thrift IDL 變更不可避免,上下游版本不一致也不可避免,從協(xié)議層面提供優(yōu)雅支持是完全可能的。已向 Thrift 提交 Issue,以及部分語言實(shí)現(xiàn)的 Pull Request,但爭吵幾輪之后,Thrift 維護(hù)人員拒絕合并,認(rèn)為“遵守最佳實(shí)踐就夠了”,他們敷衍的態(tài)度太讓人 piss off 了。

    本文轉(zhuǎn)載自技術(shù)鎖話公眾號(hào)。

    原文鏈接:https://mp.weixin.qq.com/s/aqHoM7-hKQOFHRzWipfa8g

    與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖

    總結(jié)

    以上是生活随笔為你收集整理的如果某个字段值相同则触发器新增_Thrift IDL新增字段导致版本不一致引发的惨案...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。