Thrift client并發異常之out of sequence
1. 環境異常現象及初步分析
項目中對接外部的thrift服務,使用該服務提供的thrift client進行訪問;我們剛開始進行功能驗證時一切運行正常,但后來請求量稍微高一點后,就出現了進程卡死的現象,查看后臺日志,開始以為是哪個接口有bug導致服務崩潰,后來發現接口是隨機性的報錯,并且在異常拋出后,client無法再響應后繼請求。
跟蹤日志查看,每次在出現org.apache.thrift.TApplicationException: xxx? failed: out of sequence response異常后,client便無法進行下一次請求,而out of sequence明顯不是我們服務中自己定義的異常,跟進去的代碼看下,該異常來自于thrift源碼TServiceClient下面這段:
//參見:TServiceClient //API方法調用時,發送請求數據流 protected?void?sendBase(String?methodName,?TBase?args)?throws?TException?{ ????oprot_.writeMessageBegin(new?TMessage(methodName,?TMessageType.CALL,?++seqid_));//首先寫入"方法名稱"和"seqid_" ????args.write(oprot_);//序列化參數 ????oprot_.writeMessageEnd(); ????oprot_.getTransport().flush(); } protected?void?receiveBase(TBase?result,?String?methodName)?throws?TException?{ ????TMessage?msg?=?iprot_.readMessageBegin();//如果執行有異常 ????if?(msg.type?==?TMessageType.EXCEPTION)?{ ????TApplicationException?x?=?TApplicationException.read(iprot_); ????iprot_.readMessageEnd(); ????throw?x; ????} ????//檢測seqid是否一致 ????if?(msg.seqid?!=?seqid_)?{ ????throw?new?TApplicationException(TApplicationException.BAD_SEQUENCE_ID,?methodName?+?"?failed:?out?of?sequence?response"); ????} ????result.read(iprot_); ????//反序列化 ????iprot_.readMessageEnd(); }
從上面TApplicationException.BAD_SEQUENCE_ID這塊就是我們的異常來源,那么seqid是什么,而我們的msg的seqid又為何不一致了?
2. 根因分析
客戶端和服務端之間的通訊的媒介是Message,那么我們就簡單看下Message的結構是怎樣的。通常在thrift自動生成的代碼中都會有send_methodname_xxx,receive_methodname_xxx這些方法,而在send方法中的第一句就是寫入一個Message Begin。不論不同的協議會如何處理傳入參數,我們知道了這個Message Begin是一個org.apache.thrift.protocol.TMessage類型的。那么TMessage又包括了那些東西
public?TMessage(String?n,?byte?t,?int?s)?{? ????name?=?n;?type?=?t;?seqid?=?s;? }? public?final?String?name;? public?final?byte?type;? public?final?int?seqid;
由此可見,一個Message應該包括的內容有3個:name,type,seqid。
name:根據上面send方法中的設置來看,name被設置為功能名稱,如果使用TMessage的無參數構造器的話,name會被設為空字符串””。
type:雖然這是一個byte類型的,但是實際上是從TMessageType中選擇的,具體含義見源碼,不過如果使用TMessage的無參數構造器的話,type會被置為TType.STOP。
public final class TMessageType { ??public static final byte CALL??= 1; ??public static final byte REPLY = 2; ??public static final byte EXCEPTION = 3; ??public static final byte ONEWAY = 4; }
seqid:這個就是用來標記客戶端的
因此,只要我們同一個client的seqid能夠對應上就不會出錯,那么應該就是我們的client并發請求時響應的seqid亂掉了,導致異常。
3. 鎖定原因并解決
我們client的msg的順序異常,難道client底層不能并發進行請求,沒有個連接池來管理seqid,為了驗證猜想,繼續查看源碼可以看到thrift確實沒有連接池,client 中生成的 seqid 只是用來和服務端返回的 rseqid 進行匹配。
func?(p?*TStandardClient)?Recv(iprot?TProtocol,?seqId?int32,?method?string,?result?TStruct)?error?{ ????rMethod,?rTypeId,?rSeqId,?err?:=?iprot.ReadMessageBegin() ????if?err?!=?nil?{ ????????return?err ????} ????? ????if?method?!=?rMethod?{ ????????return?NewTApplicationException(WRONG_METHOD_NAME,?fmt.Sprintf("%s:?wrong?method?name",?method)) ????}?else?if?seqId?!=?rSeqId?{ ????????return?NewTApplicationException(BAD_SEQUENCE_ID,?fmt.Sprintf("%s:?out?of?order?sequence?response",?method)) ????}?else?if?rTypeId?==?EXCEPTION?{ ????????var?exception?tApplicationException ????????if?err?:=?exception.Read(iprot);?err?!=?nil?{ ????????????return?err ????????} ????? ?????if?err?:=?iprot.ReadMessageEnd();?err?!=?nil?{ ?????????return?err ?????} ?????return?&exception ?????}?else?if?rTypeId?!=?REPLY?{ ?????????return?NewTApplicationException(INVALID_MESSAGE_TYPE_EXCEPTION,?fmt.Sprintf("%s:?invalid?message?type",?method)) ?????} ????? ?????if?err?:=?result.Read(iprot);?err?!=?nil?{ ?????????return?err ?????} ????? ?????return?iprot.ReadMessageEnd() }
thrift 的每個 client 對象中包裹了一個 transport:
... ????useTransport,?err?:=?transportFactory.GetTransport(transport) ????client?:=?NewEchoClientFactory(useTransport,?protocolFactory) ????if?err?:=?transport.Open();?err?!=?nil?{ ????????fmt.Fprintln(os.Stderr,?"Error?opening?socket?to?127.0.0.1:9898",?"?",?err) ????????os.Exit(1) ????} ????defer?transport.Close() ????? ????req?:=?&EchoReq{Msg:?"You?are?welcome."} ????res,?err?:=?client.Echo(context.TODO(),?req) ????... ????? ????type?EchoClient?struct?{ ????????c?thrift.TClient ????} ????? ????func?NewEchoClientFactory(t?thrift.TTransport,?f?thrift.TProtocolFactory)?*EchoClient?{ ????????return?&EchoClient{ ????????????c:?thrift.NewTStandardClient(f.GetProtocol(t),?f.GetProtocol(t)), ????????} ????}
這個包裹的 transport 就是一條單獨的 tcp 連接,沒有連接池,并發請求msg的返回先后就無法保證。
既然原因是thrift的單個client不能處理并發請求,并且我們對client的使用場景下不需要client單實例,那就自然而然的想到添加一個對象池管理我們的client,這樣就能很好的解決我們的client使用問題,最終選擇使用了GenericObjectPool來管理我們的對象池。不過每個配置場景也要好好學習下,后面在性能測試時還出現了配置原因導致了性能損耗問題,這是后話。
相關參考文檔: https://issues.cloudera.org/browse/KITE-328
EI企業智能 智能數據 數據湖治理中心 DGC
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。