海盗中间件:美团服务体验平台对接业务数据的最佳实践
背景
移動互聯(lián)網(wǎng)時代,用戶體驗為王。美團服務(wù)體驗平臺希望能夠幫助客戶解決在選、購、用美團產(chǎn)品過程中遇到的各種問題,真正做到“以客戶為中心”,為客戶排憂解難。
但服務(wù)體驗平臺內(nèi)部只維護客戶的客訴數(shù)據(jù),為了精準(zhǔn)地預(yù)判和更好地解決客戶遇到的問題,系統(tǒng)必須依賴業(yè)務(wù)部門提供的一些業(yè)務(wù)數(shù)據(jù),包括但不限于訂單數(shù)據(jù)、退款數(shù)據(jù)、產(chǎn)品數(shù)據(jù)等等。 本文會著重講一下在整個系統(tǒng)交互過程中遇到的一些問題,然后分享一下在實踐中探索出來的經(jīng)驗和方法論,希望能夠給大家?guī)硪恍﹩l(fā)。
問題
對接場景廣而雜
首先,需要接入服務(wù)體驗平臺服務(wù)(包括直接面向用戶的C端服務(wù)、面向客服的工單服務(wù)等等)的業(yè)務(wù)方非常多且雜,而且在不斷拓展。美團有非常多的業(yè)務(wù)線,比如外賣、酒店、旅游、打車、交通、到店餐飲、到店綜合、貓眼等等。其中部分業(yè)務(wù)又延展出多條子業(yè)務(wù)線,比如大交通部門包含火車票、汽車票、國內(nèi)機票、國際機票、船票等等。具體到每一條子業(yè)務(wù)線的每一個業(yè)務(wù)場景,客戶都有可能會遇到問題。
對于這些場景,服務(wù)體驗平臺服務(wù)都需要調(diào)用對應(yīng)的業(yè)務(wù)數(shù)據(jù)接口,來幫助用戶自助或者客服協(xié)助解決這些問題。就美團現(xiàn)有的業(yè)務(wù)而言,這樣的場景數(shù)量會達到萬級。而且業(yè)務(wù)形態(tài)在不斷迭代,還會有更多的場景被挖掘出來,這些都需要持續(xù)對接更多的業(yè)務(wù)數(shù)據(jù)來進行支撐。
接入場景定制化要求高
其次,接入服務(wù)體驗平臺服務(wù)的業(yè)務(wù)方定制化要求很高。因為業(yè)務(wù)場景的差異化非常大,不同的接入方都希望能夠定制特殊復(fù)雜邏輯,需要服務(wù)體驗平臺提供的服務(wù)解決方案與業(yè)務(wù)深度耦合。這就需要服務(wù)體驗平臺側(cè)對接入方業(yè)務(wù)邏輯和數(shù)據(jù)接口深入了解,并對這些業(yè)務(wù)數(shù)據(jù)進行組裝,針對每個場景進行定制開發(fā)。
方案
早期方案
為了解決上述問題,初期在做系統(tǒng)設(shè)計時候,考慮業(yè)務(wù)方多是既有系統(tǒng),所以服務(wù)體驗平臺服務(wù)趨向平臺化設(shè)計,并引入了適配層。服務(wù)體驗平臺內(nèi)部對所有的業(yè)務(wù)數(shù)據(jù)和邏輯進行統(tǒng)一抽象,對內(nèi)標(biāo)準(zhǔn)化接口,屏蔽掉業(yè)務(wù)邏輯和接口的差異。所有的定制化邏輯都在適配層中封裝。但這需要客服側(cè)RD對所有的場景去編寫適配器代碼,將從一個或者多個業(yè)務(wù)部門接口中拿到的業(yè)務(wù)數(shù)據(jù),轉(zhuǎn)成內(nèi)部實際場景需要的數(shù)據(jù)。
其系統(tǒng)交互如下圖所示:
缺點
雖然上述系統(tǒng)設(shè)計能滿足業(yè)務(wù)上的要求,但是存在兩個比較明顯的缺點:
編碼工作量繁重
如上圖所示,每個業(yè)務(wù)場景都需要編寫適配器來滿足需求,如果依賴的外部接口比較少,場景也比較單一,按照上述方案實施還可以接受。但業(yè)務(wù)接入非常多且雜,給客服側(cè)RD帶來了非常繁重的工作量,包括適配器編寫以及后續(xù)維護過程中對下游業(yè)務(wù)接口的持續(xù)跟蹤和監(jiān)控。客服側(cè)RD需要深入了解業(yè)務(wù)方邏輯
另外,由于客服側(cè)RD對于業(yè)務(wù)模型的不熟悉,解析業(yè)務(wù)模型然后組裝最終展示給客戶的數(shù)據(jù),需要比業(yè)務(wù)方RD花更多的時間來梳理和實現(xiàn),并且花費更多的時間來驗證正確性。比如下面是一個真實的組裝業(yè)務(wù)接口并對業(yè)務(wù)數(shù)據(jù)進行處理的案例:
publicclassTicketAdapterServiceImplimplementsOrderAdapterService{
@Resource(name="tradeQueryClient")
privateTradeTicketQueryClienttradeTicketQueryClient;
@Resource
privateColumbusTicketServicecolumbusTicketService;
/**
*根據(jù)訂單ID獲取門票相關(guān)的訂單數(shù)據(jù)、門票數(shù)據(jù)、退款數(shù)據(jù)等
**/
@Override
publicOrderInfoDTOhandle(OrderRequestDTOorderRequestDTO){
List<ITradeTicketQueryService.TradeDetailField>tradeDetailFieldList=newArrayList<ITradeTicketQueryService.TradeDetailField>();
tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.ORDER);
tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.TICKET);
tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.REFUND_REQUEST);
try{
//通過接口A得到部分訂單數(shù)據(jù)、門票數(shù)據(jù)和退款數(shù)據(jù)
RichOrderDetailrichOrderDetail=tradeTicketQueryClient.getRichOrderDetailById(orderRequestDTO.getOrderId(),tradeDetailFieldList);
if(richOrderDetail==null){
returnnull;
}
if(richOrderDetail.getOrderDetail()==null){
returnnull;
}
OrderDetailorderDetail=richOrderDetail.getOrderDetail();
RefundDetailrefundDetail=richOrderDetail.getRefundDetail();
OrderInfoDTOorderInfoDTO=newOrderInfoDTO();
//解析和處理接口A返回的字段,得到客服側(cè)場景真正需要的數(shù)據(jù)
orderInfoDTO.put("dealId",orderDetail.getMtDealId());
orderInfoDTO.put(DomesticTicketField.VOUCHER_CODE.getValue(),getVoucherCode(richOrderDetail));
orderInfoDTO.put(DomesticTicketField.REFUND_CHECK_DUE.getValue(),getRefundCheckDueDate(richOrderDetail));
orderInfoDTO.put(DomesticTicketField.REFUND_RECEIVED_DUE.getValue(),getRefundReceivedDueDate(richOrderDetail));
//根據(jù)接口B獲取另外一些訂單數(shù)據(jù)、門票詳情數(shù)據(jù)、退款數(shù)據(jù)
ColumbusTicketDTOcolumbusTicketDTO=columbusTicketService.getByDealId((int)richOrderDetail.getOrderDetail().getMtDealId());
if(columbusTicketDTO==null){
returnorderInfoDTO;
}
//解析和處理接口B返回的字段,得到客服側(cè)場景真正需要的數(shù)據(jù)
orderInfoDTO.put(DomesticTicketField.REFUND_INFO.getValue(),columbusTicketDTO.getRefundInfo());
orderInfoDTO.put(DomesticTicketField.USE_METHODS.getValue(),columbusTicketDTO.getUseMethods());
orderInfoDTO.put(DomesticTicketField.BOOK_INFO.getValue(),columbusTicketDTO.getBookInfo());
orderInfoDTO.put(DomesticTicketField.INTO_METHOD.getValue(),columbusTicketDTO.getIntoMethod());
returnorderInfoDTO;
}catch(TExceptione){
Cat.logError("查詢不到對應(yīng)的訂單詳情",e);
returnnull;
}
}
}
探索
將適配層交由業(yè)務(wù)方實現(xiàn)
為了克服早期方案的兩個缺點,最初,我們希望能夠把場景數(shù)據(jù)的準(zhǔn)備和業(yè)務(wù)模型的解析工作,都交給對業(yè)務(wù)比較熟悉的團隊來處理,即將適配層交由業(yè)務(wù)方來實現(xiàn)。
這樣做的話優(yōu)勢和劣勢也比較明顯:
優(yōu)勢
客服這邊關(guān)注自己的領(lǐng)域服務(wù)就好,做好平臺化,數(shù)據(jù)提供都交給業(yè)務(wù)團隊,解放了客服側(cè)RD。
劣勢
但對業(yè)務(wù)方來說帶來了比較大的工作量,業(yè)務(wù)方既有服務(wù)的復(fù)用性很低,對客服側(cè)每一個需要數(shù)據(jù)的場景,都要重新封裝新的服務(wù)。
更好的解決方案?
這個時候我們思考:是否可以既能讓業(yè)務(wù)方解析自己的業(yè)務(wù)數(shù)據(jù),又能夠盡量利用既有服務(wù)呢?我們考慮把既有服務(wù)的組裝過程以及模型的轉(zhuǎn)換都讓一個服務(wù)編排的中間件來實現(xiàn)。但是使用這個中間件有一個前提,就是業(yè)務(wù)方提供出來的既有服務(wù)必須支持泛化調(diào)用,避免調(diào)用方直接依賴服務(wù)方客戶端(文章下一個小節(jié)也會補充下對于泛化調(diào)用的解釋)。其交互模型如下圖所示:
海盜中間件
簡介
什么是海盜?
海盜就是一個用來對支持泛化調(diào)用(上述所說)的服務(wù)進行編排,然后獲取預(yù)期結(jié)果的一個中間件。使用該中間件調(diào)用方可以根據(jù)場景來對目標(biāo)服務(wù)進行編排,按需調(diào)用。
何為泛化調(diào)用?
通常服務(wù)提供方提供的服務(wù)都會有自己的接口協(xié)議,比如一個獲取訂單數(shù)據(jù)的服務(wù):
packagecom.dianping.demo;
publicinterfaceDemoService{
OrderDTOgetById(StringorderId);
}
而調(diào)用方調(diào)用該服務(wù)需要引入該接口協(xié)議,即依賴該服務(wù)提供的JAR包。如果調(diào)用方需要集成多方數(shù)據(jù),那就需要依賴非常多的API,同時服務(wù)方接口升級客戶端也需要隨之進行升級。而泛化調(diào)用就可以解決這個問題,通過泛化調(diào)用客戶端可以在服務(wù)方?jīng)]有提供接口協(xié)議和不依賴服務(wù)方API的情況下對服務(wù)進行調(diào)用,通過類似GenericService這樣一個接口來處理所有的服務(wù)請求。
如下是一個泛化調(diào)用的Demo:
publicclassDemoInvoke{
publicvoidgenericInvoke(){
/**調(diào)用方配置**/
InvokerConfig<GenericService>invokerConfig=newInvokerConfig("com.dianping.demo.DemoService",com.dianping.pigeon.remoting.common.service.GenericService.class);
invokerConfig.setTimeout(1000);
invokerConfig.setGeneric(GenericType.JSON.getName());
invokerConfig.setCallType("sync");
/**泛化調(diào)用**/
finalGenericServicegenericService=ServiceFactory.getService(invokerConfig);
List<String>paramTypes=newArrayList<String>();
paramTypes.add("java.lang.String");
List<String>paramValues=newArrayList<String>();
paramValues.add("0000000001");
Stringresult=genericService.$invoke("getById",paramTypes,paramValues);
}
}
有了這個泛化調(diào)用的前提,我們就可以重點去思考如何對服務(wù)進行編排,然后對取得的結(jié)果進行處理了。
DSL設(shè)計
首先重新梳理一下海盜的設(shè)計目標(biāo):
對既有服務(wù)進行編排調(diào)用
對獲取的數(shù)據(jù)進行處理
而為了實現(xiàn)服務(wù)編排,需要定義一個數(shù)據(jù)結(jié)構(gòu)來描述服務(wù)之間的依賴關(guān)系、調(diào)用順序、調(diào)用服務(wù)的入?yún)⒑统鰠⒌鹊取V髮Λ@取的結(jié)果進行處理,也需要在這個數(shù)據(jù)結(jié)構(gòu)中具體描述對什么樣的數(shù)據(jù)進行怎么樣的處理等等。
所以我們需要定義一套DSL(領(lǐng)域特定語言)來描述整個服務(wù)編排的藍圖,其語法如下:
{
//定義好需要調(diào)用的接口以及接口之間的依賴關(guān)系,一個接口調(diào)用即為一個task
"tasks":[
//第一個task
{
"url":"http://helloWorld.test.hello",//url為pigeon發(fā)布的遠程服務(wù)地址:
"alias":"d1",//別名,結(jié)果取值的時候可以通過別名引用
"taskType":"PigeonGeneric",//task的類別一般可以設(shè)置為PigeonGeneric,默認是pigeonAgent方式。
"method":"getByDoubleRequest",//要調(diào)用的pigeon接口的方法名
"timeout":3000,//task的超時時間
"inputs":{//入?yún)⑶闆r,多個入?yún)⑼ㄟ^key:value的結(jié)構(gòu)書寫,key的類別通過下面的inputsExtra定義。
"helloWorld":{
"name":"csophys",//可以通過#orderId,從上下文中獲取值,可以通過$d1.orderId的形式從其他的task中獲取值
"sex":"boy"
},
"name":"winnie"
},
"inputsExtra":{//入?yún)ey的類別定義
"helloWorld":"com.dianping.csc.pirate.remoting.pigeon.pigeon_generic_demo_service.HelloWorld",
"name":"java.lang.String"
}
},
//另一個task
{
"url":"http://helloWorld.test.hello",
"alias":"d2",
"taskType":"PigeonGeneric",
"method":"getByDoubleRequest",
"inputsExtra":{
"helloWorld":"com.dianping.csc.pirate.remoting.pigeon.pigeon_generic_demo_service.HelloWorld",
"name":"java.lang.String"
},
"timeout":3000,
"inputs":{
"helloWorld":{
"name":"csophys",
"sex":"boy"
},
"name":"winnie"
}
}
],
"name":"pigeonGenericUnitDemo",//DSL的名稱定義,暫時沒有特別含義
"description":"pigeon泛型調(diào)用測試",//DSL的描述
"outputs":{//定義好最后輸出的數(shù)據(jù)模型
"d1name":"$d1.name",
"languages":"$d2.languages",
"language1":"$d2.languages[0]",
"name":"csophys"
}
}
架構(gòu)設(shè)計
有了DSL來描述整個編排藍圖之后,海盜自然要對該DSL進行解析,然后對服務(wù)進行具體調(diào)用。其整體架構(gòu)如下所示:
其中涉及到幾個重點概念:
Facade:對外提供統(tǒng)一接口,供客戶端調(diào)用。
Parser:對于輸入的DSL進行解析,解析成內(nèi)部流轉(zhuǎn)的數(shù)據(jù)結(jié)構(gòu),同時得到所有的task,并且構(gòu)建task調(diào)用邏輯樹。
Executor:真實發(fā)起調(diào)用的模塊,目前支持平臺內(nèi)部的Pigeon和MTThrift調(diào)用方式,同時對HTTP等其他協(xié)議有良好的擴展性。
DataProcessor:數(shù)據(jù)后處理。這邊會把所有接口拿到的數(shù)據(jù)轉(zhuǎn)換層客服場景這邊需要的數(shù)據(jù),并且通過設(shè)計的一些內(nèi)部函數(shù),可以支持一些如數(shù)據(jù)半脫敏等功能。
組件插件化:對日志等功能實現(xiàn)可插拔,調(diào)用方可以自定義這些組件,即插即用。
主要Feature
海盜具有如下主要特點:
采用去中心化的設(shè)計思路,引擎集成在SDK中。方案通用化,每一個需要業(yè)務(wù)數(shù)據(jù)的場景都可以通過海盜直接調(diào)用數(shù)據(jù)提供方。
服務(wù)編排支持并行和串行調(diào)用,使用方可以根據(jù)實際場景自己構(gòu)造服務(wù)調(diào)用樹。通過DSL的方式把之前硬編碼組裝的邏輯實現(xiàn)了配置化,然后通過海盜引擎把能并行調(diào)用的服務(wù)都執(zhí)行了并行調(diào)用,數(shù)據(jù)使用方不用再自己處理性能優(yōu)化。
使用JSON DSL 描述整個工作藍圖,簡單易學(xué)。
支持JSONPath語法對服務(wù)返回的結(jié)果進行取值。
支持內(nèi)置函數(shù)和自定義指令(語法參考ftl)對取到的元數(shù)據(jù)進行處理,得到需要的最終結(jié)果。
編排服務(wù)樹可視化。
目前集團內(nèi)部RPC中間件包括Pigeon、MTThrift,已進行了泛化調(diào)用支持,可以通過海盜實現(xiàn)Pigeon服務(wù)和MTThrift的服務(wù)編排。不需要限制業(yè)務(wù)團隊的服務(wù)提供方式,但需要升級中間件版本。這里特別感謝服務(wù)治理團隊的大力支持。
Tutorial
場景:需要根據(jù)訂單ID查詢訂單狀態(tài)和支付狀態(tài),但目前沒有現(xiàn)成的接口支持該功能,但有兩個既有接口分別是:
接口1:根據(jù)訂單ID,獲取到訂單狀態(tài)和支付流水號
接口2:根據(jù)支付流水號獲取支付狀態(tài)
那我們可以對這兩個接口進行編排,編寫DSL如下:
{
"tasks":[
{
"url":"http://test.service",
"alias":"d1",
"taskType":"PigeonGeneric",
"method":"getByOrderId",
"timeout":3000,
"inputs":{
"orderId":"#orderId"
},
"inputsExtra":{
"name":"java.lang.String"
}
},
{
"url":"http://test.service",
"alias":"d2",
"taskType":"PigeonGeneric",
"method":"getPayStatus",
"timeout":3000,
"inputs":{
"paySerialNo":"$d1.paySerialNo"
},
"inputsExtra":{
"time":"java.lang.String"
}
}
],
"name":"test",
"description":"組裝上述接口獲取訂單狀態(tài)和支付狀態(tài)",
"outputs":{
"orderStatus":"$d1.orderStatus",
"payStatus":"$d2.payStatus"
}
}
然后客戶端進行調(diào)用:
StringDSL="上述DSL文件";
Stringparams="{"orderId":"000000001"}";
Responseresp=PirateEngine.invoke(DSL,params);
最后得到的數(shù)據(jù)即為調(diào)用場景真正需要的數(shù)據(jù):
{
"orderStatus":1,
"payStatus":2
}
開發(fā)流程變化
因為獲取數(shù)據(jù)的架構(gòu)產(chǎn)生了變化,開發(fā)流程也隨之發(fā)生改變。
如圖所示,因為減少了客服側(cè)RD不斷去向業(yè)務(wù)方RD確認返回的數(shù)據(jù)含義和邏輯,雙方RD各自專注各自熟悉的領(lǐng)域,開發(fā)效率和最終結(jié)果準(zhǔn)確性都有顯著提升。
總結(jié)和展望
最后總結(jié)一下使用海盜之后的優(yōu)勢:
去中心化的設(shè)計,可用性得到保證。
服務(wù)復(fù)用性高,領(lǐng)域劃分更加清晰,讓RD專注在自己熟悉的領(lǐng)域,降低研發(fā)成本。
因為流程變化后,業(yè)務(wù)方可以提前驗證提供的數(shù)據(jù),高質(zhì)量交付。
客服側(cè)對數(shù)據(jù)獲取進行統(tǒng)一收口,可以對所有調(diào)用服務(wù)統(tǒng)一監(jiān)控并對數(shù)據(jù)統(tǒng)一處理。
展望
海盜的技術(shù)規(guī)劃:
豐富內(nèi)部函數(shù)和運算表達式
目前海盜提供了一部分簡單的內(nèi)部函數(shù)用來對取到的值進行簡單處理,同時正在實現(xiàn)支持調(diào)用方自定義運算表達式來支持復(fù)雜場景的數(shù)據(jù)處理,這部分需要持續(xù)完善。屏蔽遠程調(diào)用協(xié)議異構(gòu)性
目前海盜只支持對美團Pigeon和MTThrift服務(wù)進行編排,這里要對協(xié)議進行擴展,支持類似HTTP等通用協(xié)議,同時支持調(diào)用方自定義協(xié)議和調(diào)用實現(xiàn)。運營工具完善
提供一個比較完整的運營工具,調(diào)用方可以自行配置DSL并進行校驗,然后一鍵調(diào)用查詢最終結(jié)果。同時調(diào)用方可以通過該工具進行日志、報表等相關(guān)數(shù)據(jù)查詢。自動生成單元測試
能夠把經(jīng)過驗證的DSL生成相應(yīng)的單元測試用例給到數(shù)據(jù)提供方,持續(xù)保障提供的DSL的可用性和正確性。
作者簡介
王彬,美團資深研發(fā)工程師,畢業(yè)于南京大學(xué),2017年2月加入美團。目前主要專注于智能客服領(lǐng)域,從事后端工作。
陳勝,海盜項目負責(zé)人,智能客服技術(shù)負責(zé)人,2013年加入大眾點評。在未來智能客服組會持續(xù)在平臺化和垂直領(lǐng)域方向深入下去,為消費者、商家、企業(yè)提供更加智能的客戶服務(wù)體驗。
---------- END ----------
招聘信息
服務(wù)體驗平臺可以深入接觸到公司的所有業(yè)務(wù),推進業(yè)務(wù)改善產(chǎn)品。提升客戶的服務(wù)體驗。打造一個客戶貼身的智能服務(wù)助手。通過技術(shù)的手段更快地解決客戶的問題,并且最大程度地節(jié)省客服的人力成本。歡迎有意向的同學(xué)加入服務(wù)體驗平臺,上海、北京都有需求。簡歷請投遞至:sheng.chen#dianping.com
也許你還想看
UAS:大眾點評用戶行為系統(tǒng)
MCI:大眾點評千人移動研發(fā)團隊怎樣做持續(xù)集成?
2000萬日訂單背后:美團外賣客戶端高可用建設(shè)體系
總結(jié)
以上是生活随笔為你收集整理的海盗中间件:美团服务体验平台对接业务数据的最佳实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Golang进位计数制(进制)
- 下一篇: 数据分析