海盜中間件:美團服務體驗平臺對接業務數據的最佳實踐

背景java

移動互聯網時代,用戶體驗爲王。美團服務體驗平臺但願可以幫助客戶解決在選、購、用美團產品過程當中遇到的各類問題,真正作到「以客戶爲中心」,爲客戶排憂解難。後端

但服務體驗平臺內部只維護客戶的客訴數據,爲了精準地預判和更好地解決客戶遇到的問題,系統必須依賴業務部門提供的一些業務數據,包括但不限於訂單數據、退款數據、產品數據等等。 本文會着重講一下在整個系統交互過程當中遇到的一些問題,而後分享一下在實踐中探索出來的經驗和方法論,但願可以給你們帶來一些啓發。性能優化

問題數據結構

對接場景廣而雜架構

首先,須要接入服務體驗平臺服務(包括直接面向用戶的C端服務、面向客服的工單服務等等)的業務方很是多且雜,並且在不斷拓展。美團有很是多的業務線,好比外賣、酒店、旅遊、打車、交通、到店餐飲、到店綜合、貓眼等等。其中部分業務又延展出多條子業務線,好比大交通部門包含火車票、汽車票、國內機票、國際機票、船票等等。具體到每一條子業務線的每個業務場景,客戶都有可能會遇到問題。ide

對於這些場景,服務體驗平臺服務都須要調用對應的業務數據接口,來幫助用戶自助或者客服協助解決這些問題。就美團現有的業務而言,這樣的場景數量會達到萬級。並且業務形態在不斷迭代,還會有更多的場景被挖掘出來,這些都須要持續對接更多的業務數據來進行支撐。函數

接入場景定製化要求高工具

其次,接入服務體驗平臺服務的業務方定製化要求很高。由於業務場景的差別化很是大,不一樣的接入方都但願可以定製特殊複雜邏輯,須要服務體驗平臺提供的服務解決方案與業務深度耦合。這就須要服務體驗平臺側對接入方業務邏輯和數據接口深刻了解,並對這些業務數據進行組裝,針對每一個場景進行定製開發。性能

方案單元測試

早期方案

爲了解決上述問題,初期在作系統設計時候,考慮業務方可能是既有系統,因此服務體驗平臺服務趨向平臺化設計,並引入了適配層。服務體驗平臺內部對全部的業務數據和邏輯進行統一抽象,對內標準化接口,屏蔽掉業務邏輯和接口的差別。全部的定製化邏輯都在適配層中封裝。但這須要客服側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萬日訂單背後:美團外賣客戶端高可用建設體系

 

相關文章
相關標籤/搜索