海盜中間件:美團服務體驗平臺對接業務數據的最佳實踐
背景
移動互聯網時代,用戶體驗為王。美團服務體驗平臺希望能夠幫助客戶解決在選、購、用美團產品過程中遇到的各種問題,真正做到“以客戶為中心”,為客戶排憂解難。
但服務體驗平臺內部只維護客戶的客訴數據,為了精準地預判和更好地解決客戶遇到的問題,系統必須依賴業務部門提供的一些業務數據,包括但不限於訂單數據、退款數據、產品數據等等。 本文會著重講一下在整個系統交互過程中遇到的一些問題,然後分享一下在實踐中探索出來的經驗和方法論,希望能夠給大家帶來一些啟發。
問題
對接場景廣而雜
首先,需要接入服務體驗平臺服務(包括直接面向用戶的C端服務、面向客服的工單服務等等)的業務方非常多且雜,而且在不斷拓展。美團有非常多的業務線,比如外賣、酒店、旅遊、打車、交通、到店餐飲、到店綜合、貓眼等等。其中部分業務又延展出多條子業務線,比如大交通部門包含火車票、汽車票、國內機票、國際機票、船票等等。具體到每一條子業務線的每一個業務場景,客戶都有可能會遇到問題。
對於這些場景,服務體驗平臺服務都需要調用對應的業務數據接口,來幫助用戶自助或者客服協助解決這些問題。就美團現有的業務而言,這樣的場景數量會達到萬級。而且業務形態在不斷叠代,還會有更多的場景被挖掘出來,這些都需要持續對接更多的業務數據來進行支撐。
接入場景定制化要求高
其次,接入服務體驗平臺服務的業務方定制化要求很高。因為業務場景的差異化非常大,不同的接入方都希望能夠定制特殊復雜邏輯,需要服務體驗平臺提供的服務解決方案與業務深度耦合。這就需要服務體驗平臺側對接入方業務邏輯和數據接口深入了解,並對這些業務數據進行組裝,針對每個場景進行定制開發。
方案
早期方案
為了解決上述問題,初期在做系統設計時候,考慮業務方多是既有系統,所以服務體驗平臺服務趨向平臺化設計,並引入了適配層。服務體驗平臺內部對所有的業務數據和邏輯進行統一抽象,對內標準化接口,屏蔽掉業務邏輯和接口的差異。所有的定制化邏輯都在適配層中封裝。但這需要客服側RD對所有的場景去編寫適配器代碼,將從一個或者多個業務部門接口中拿到的業務數據,轉成內部實際場景需要的數據。
其系統交互如下圖所示:
缺點
雖然上述系統設計能滿足業務上的要求,但是存在兩個比較明顯的缺點:
編碼工作量繁重
如上圖所示,每個業務場景都需要編寫適配器來滿足需求,如果依賴的外部接口比較少,場景也比較單一,按照上述方案實施還可以接受。但業務接入非常多且雜,給客服側RD帶來了非常繁重的工作量,包括適配器編寫以及後續維護過程中對下遊業務接口的持續跟蹤和監控。客服側RD需要深入了解業務方邏輯
另外,由於客服側RD對於業務模型的不熟悉,解析業務模型然後組裝最終展示給客戶的數據,需要比業務方RD花更多的時間來梳理和實現,並且花費更多的時間來驗證正確性。比如下面是一個真實的組裝業務接口並對業務數據進行處理的案例:
public class TicketAdapterServiceImpl implements OrderAdapterService {
@Resource(name = "tradeQueryClient")
private TradeTicketQueryClient tradeTicketQueryClient;
@Resource
private ColumbusTicketService columbusTicketService;
/**
* 根據訂單ID獲取門票相關的訂單數據、門票數據、退款數據等
**/
@Override
public OrderInfoDTO handle(OrderRequestDTO orderRequestDTO) {
List<ITradeTicketQueryService.TradeDetailField> tradeDetailFieldList = new ArrayList<ITradeTicketQueryService.TradeDetailField>();
tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.ORDER);
tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.TICKET);
tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.REFUND_REQUEST);
try {
//通過接口A得到部分訂單數據、門票數據和退款數據
RichOrderDetail richOrderDetail = tradeTicketQueryClient.getRichOrderDetailById(orderRequestDTO.getOrderId(), tradeDetailFieldList);
if (richOrderDetail == null) {
return null;
}
if (richOrderDetail.getOrderDetail() == null) {
return null;
}
OrderDetail orderDetail = richOrderDetail.getOrderDetail();
RefundDetail refundDetail = richOrderDetail.getRefundDetail();
OrderInfoDTO orderInfoDTO = new OrderInfoDTO();
//解析和處理接口A返回的字段,得到客服側場景真正需要的數據
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));
//根據接口B獲取另外一些訂單數據、門票詳情數據、退款數據
ColumbusTicketDTO columbusTicketDTO = columbusTicketService.getByDealId((int) richOrderDetail.getOrderDetail().getMtDealId());
if (columbusTicketDTO == null) {
return orderInfoDTO;
}
//解析和處理接口B返回的字段,得到客服側場景真正需要的數據
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());
return orderInfoDTO;
} catch (TException e) {
Cat.logError("查詢不到對應的訂單詳情", e);
return null;
}
}
}
探索
將適配層交由業務方實現
為了克服早期方案的兩個缺點,最初,我們希望能夠把場景數據的準備和業務模型的解析工作,都交給對業務比較熟悉的團隊來處理,即將適配層交由業務方來實現。
這樣做的話優勢和劣勢也比較明顯:
優勢
客服這邊關註自己的領域服務就好,做好平臺化,數據提供都交給業務團隊,解放了客服側RD。
劣勢
但對業務方來說帶來了比較大的工作量,業務方既有服務的復用性很低,對客服側每一個需要數據的場景,都要重新封裝新的服務。
更好的解決方案?
這個時候我們思考:是否可以既能讓業務方解析自己的業務數據,又能夠盡量利用既有服務呢?我們考慮把既有服務的組裝過程以及模型的轉換都讓一個服務編排的中間件來實現。但是使用這個中間件有一個前提,就是業務方提供出來的既有服務必須支持泛化調用,避免調用方直接依賴服務方客戶端(文章下一個小節也會補充下對於泛化調用的解釋)。其交互模型如下圖所示:
海盜中間件
簡介
什麽是海盜?
海盜就是一個用來對支持泛化調用(上述所說)的服務進行編排,然後獲取預期結果的一個中間件。使用該中間件調用方可以根據場景來對目標服務進行編排,按需調用。
何為泛化調用?
通常服務提供方提供的服務都會有自己的接口協議,比如一個獲取訂單數據的服務:
package com.dianping.demo;
public interface DemoService{
OrderDTO getById(String orderId);
}
而調用方調用該服務需要引入該接口協議,即依賴該服務提供的JAR包。如果調用方需要集成多方數據,那就需要依賴非常多的API,同時服務方接口升級客戶端也需要隨之進行升級。而泛化調用就可以解決這個問題,通過泛化調用客戶端可以在服務方沒有提供接口協議和不依賴服務方API的情況下對服務進行調用,通過類似GenericService
這樣一個接口來處理所有的服務請求。
如下是一個泛化調用的Demo:
public class DemoInvoke{
public void genericInvoke(){
/** 調用方配置 **/
InvokerConfig<GenericService> invokerConfig = new InvokerConfig("com.dianping.demo.DemoService", com.dianping.pigeon.remoting.common.service.GenericService.class);
invokerConfig.setTimeout(1000);
invokerConfig.setGeneric(GenericType.JSON.getName());
invokerConfig.setCallType("sync");
/** 泛化調用 **/
final GenericService genericService = ServiceFactory.getService(invokerConfig);
List<String> paramTypes = new ArrayList<String>();
paramTypes.add("java.lang.String");
List<String> paramValues = new ArrayList<String>();
paramValues.add("0000000001");
String result = genericService.$invoke("getById", paramTypes, paramValues);
}
}
有了這個泛化調用的前提,我們就可以重點去思考如何對服務進行編排,然後對取得的結果進行處理了。
DSL設計
首先重新梳理一下海盜的設計目標:
對既有服務進行編排調用
對獲取的數據進行處理
而為了實現服務編排,需要定義一個數據結構來描述服務之間的依賴關系、調用順序、調用服務的入參和出參等等。之後對獲取的結果進行處理,也需要在這個數據結構中具體描述對什麽樣的數據進行怎麽樣的處理等等。
所以我們需要定義一套DSL(領域特定語言)來描述整個服務編排的藍圖,其語法如下:
{
//定義好需要調用的接口以及接口之間的依賴關系,一個接口調用即為一個task
"tasks": [
//第一個task
{
"url": "http://helloWorld.test.hello", //url 為pigeon發布的遠程服務地址:
"alias": "d1", //別名,結果取值的時候可以通過別名引用
"taskType": "PigeonGeneric", //task的類別一般可以設置為PigeonGeneric,默認是pigeonAgent方式。
"method": "getByDoubleRequest", //要調用的pigeon接口的方法名
"timeout": 3000, //task的超時時間
"inputs": { //入參情況,多個入參通過key:value的結構書寫,key的類別通過下面的inputsExtra定義。
"helloWorld": {
"name": "csophys", //可以通過#orderId,從上下文中獲取值,可以通過$d1.orderId的形式從其他的task中獲取值
"sex": "boy"
},
"name": "winnie"
},
"inputsExtra": { //入參key的類別定義
"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泛型調用測試", //DSL的描述
"outputs": { //定義好最後輸出的數據模型
"d1name": "$d1.name",
"languages": "$d2.languages",
"language1": "$d2.languages[0]",
"name": "csophys"
}
}
架構設計
有了DSL來描述整個編排藍圖之後,海盜自然要對該DSL進行解析,然後對服務進行具體調用。其整體架構如下所示:
其中涉及到幾個重點概念:
Facade:對外提供統一接口,供客戶端調用。
Parser:對於輸入的DSL進行解析,解析成內部流轉的數據結構,同時得到所有的task,並且構建task調用邏輯樹。
Executor:真實發起調用的模塊,目前支持平臺內部的Pigeon和MTThrift調用方式,同時對HTTP等其他協議有良好的擴展性。
DataProcessor:數據後處理。這邊會把所有接口拿到的數據轉換層客服場景這邊需要的數據,並且通過設計的一些內部函數,可以支持一些如數據半脫敏等功能。
組件插件化:對日誌等功能實現可插拔,調用方可以自定義這些組件,即插即用。
主要Feature
海盜具有如下主要特點:
采用去中心化的設計思路,引擎集成在SDK中。方案通用化,每一個需要業務數據的場景都可以通過海盜直接調用數據提供方。
服務編排支持並行和串行調用,使用方可以根據實際場景自己構造服務調用樹。通過DSL的方式把之前硬編碼組裝的邏輯實現了配置化,然後通過海盜引擎把能並行調用的服務都執行了並行調用,數據使用方不用再自己處理性能優化。
使用JSON DSL 描述整個工作藍圖,簡單易學。
支持JSONPath語法對服務返回的結果進行取值。
支持內置函數和自定義指令(語法參考ftl)對取到的元數據進行處理,得到需要的最終結果。
編排服務樹可視化。
目前集團內部RPC中間件包括Pigeon、MTThrift,已進行了泛化調用支持,可以通過海盜實現Pigeon服務和MTThrift的服務編排。不需要限制業務團隊的服務提供方式,但需要升級中間件版本。這裏特別感謝服務治理團隊的大力支持。
Tutorial
場景:需要根據訂單ID查詢訂單狀態和支付狀態,但目前沒有現成的接口支持該功能,但有兩個既有接口分別是:
接口1:根據訂單ID,獲取到訂單狀態和支付流水號
接口2:根據支付流水號獲取支付狀態
那我們可以對這兩個接口進行編排,編寫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": "組裝上述接口獲取訂單狀態和支付狀態",
"outputs": {
"orderStatus": "$d1.orderStatus",
"payStatus": "$d2.payStatus"
}
}
然後客戶端進行調用:
String DSL = "上述DSL文件";
String params = "{\"orderId\":\"000000001\"}";
Response resp = PirateEngine.invoke(DSL, params);
最後得到的數據即為調用場景真正需要的數據:
{
"orderStatus":1,
"payStatus":2
}
開發流程變化
因為獲取數據的架構產生了變化,開發流程也隨之發生改變。
如圖所示,因為減少了客服側RD不斷去向業務方RD確認返回的數據含義和邏輯,雙方RD各自專註各自熟悉的領域,開發效率和最終結果準確性都有顯著提升。
總結和展望
最後總結一下使用海盜之後的優勢:
去中心化的設計,可用性得到保證。
服務復用性高,領域劃分更加清晰,讓RD專註在自己熟悉的領域,降低研發成本。
因為流程變化後,業務方可以提前驗證提供的數據,高質量交付。
客服側對數據獲取進行統一收口,可以對所有調用服務統一監控並對數據統一處理。
展望
海盜的技術規劃:
豐富內部函數和運算表達式
目前海盜提供了一部分簡單的內部函數用來對取到的值進行簡單處理,同時正在實現支持調用方自定義運算表達式來支持復雜場景的數據處理,這部分需要持續完善。屏蔽遠程調用協議異構性
目前海盜只支持對美團Pigeon和MTThrift服務進行編排,這裏要對協議進行擴展,支持類似HTTP等通用協議,同時支持調用方自定義協議和調用實現。運營工具完善
提供一個比較完整的運營工具,調用方可以自行配置DSL並進行校驗,然後一鍵調用查詢最終結果。同時調用方可以通過該工具進行日誌、報表等相關數據查詢。自動生成單元測試
能夠把經過驗證的DSL生成相應的單元測試用例給到數據提供方,持續保障提供的DSL的可用性和正確性。
作者簡介
王彬,美團資深研發工程師,畢業於南京大學,2017年2月加入美團。目前主要專註於智能客服領域,從事後端工作。
陳勝,海盜項目負責人,智能客服技術負責人,2013年加入大眾點評。在未來智能客服組會持續在平臺化和垂直領域方向深入下去,為消費者、商家、企業提供更加智能的客戶服務體驗。
---------- END ----------
招聘信息
服務體驗平臺可以深入接觸到公司的所有業務,推進業務改善產品。提升客戶的服務體驗。打造一個客戶貼身的智能服務助手。通過技術的手段更快地解決客戶的問題,並且最大程度地節省客服的人力成本。歡迎有意向的同學加入服務體驗平臺,上海、北京都有需求。簡歷請投遞至:sheng.chen#dianping.com
也許你還想看
UAS:大眾點評用戶行為系統
MCI:大眾點評千人移動研發團隊怎樣做持續集成?
2000萬日訂單背後:美團外賣客戶端高可用建設體系
海盜中間件:美團服務體驗平臺對接業務數據的最佳實踐