在閒魚,咱們如何用Dart作高效後端開發?

背景

像阿里其餘技術團隊以及業界的作法同樣,閒魚的大多數後端應用都是所有使用java來實現的。java易用、豐富的庫、結構容易設計的特性決定了它是進行業務開發的最好語言之一。後端應用中數據的存儲、訪問、轉換、輸出雖然都屬於後端的範疇,可是其中變動的頻率是不一樣的。一般領域對象肯定以後,它的變化是不多的,可是客戶端展現的變化不少,致使接口層(或者叫粘連前臺和後臺的膠水層)的變化很是快。大多數web應用採用統一的技術棧來實現後端,膠水層跟領域層使用統一技術,這樣的作法仍然有能夠優化的地方:javascript

  • 在預發環境中驗證調試比較困難:一方面,每次提交代碼、構建、部署、驗證的總時間相對較長;另外一方面,多人共用一個部署環境,相互干擾(代碼衝突和部署衝突),增長了成本。後端開發人員都渴望有一個獨立、高效的開發環境,就像開發一個前端頁面那樣
  • 前臺(java、object-c,javascript)和後臺(java)的技術不一樣,致使前臺同窗很難開發後端程序,閒魚技術團隊爲了追求更高的開發效率,但願可以跨越服務端開發與客戶端、前端的界限,讓前臺開發人員也可以寫後端代碼
  • 膠水層一般依賴不少後端服務,計算比較簡單,是IO密集型的任務。咱們理想中的編程框架是可以像寫同步代碼同樣簡單,可是享受異步的好處。目前的方案還沒法徹底作到這一點。

爲何選擇dart

閒魚技術團隊選擇使用dart做爲膠水層的實現語言。前端

  • dart是一種靜態類型語言,在編譯器就能徹底肯定變量的類型。它是支持泛型的面嚮對象語言,任何變量都是對象,不存在java中的原始類型。跟javascript相似,它是一種單線程語言,對異步的支持很是好(async/await)。dart的語法與主流開發語言(java,python,c/c++,javascript)很相似, 在主流的語言語法基礎上,dart增長了不少語法結構,getter/setter、方法級聯、函數式編程、閉包,這些語法讓容許開發人員更加容易地寫出簡潔的代碼;全面易用的類庫也是dart可以做爲flutter開發語言的重要緣由。
  • flutter證實了dart在客戶端開發上的成功,閒魚不只走在flutter開發的前列,也正在嘗試使用dart開發後端應用;語法跟javascript,java相近,有人形容這門語言是傻瓜式的簡單(stupid-simple to learn),不管是java後端開發人員,仍是客戶端開發同窗,亦或是前端開發同窗,都可以快速上手寫出生產級的代碼。全部技術同窗都可以開發後端接口在閒魚是能夠作到的。
  • dart對異步化的良好支持對業務開發是強大助力。後端應用膠水層代碼大多數IO密集型的任務,使用異步化技術能夠把多個IO請求的總RT,從全部請求RT之和,下降爲全部請求中最高RT。dart對異步有良好的支持,開發同窗使用dart能夠以近乎同步的代碼風格取得異步的性能。咱們以閒魚寶貝詳情頁的代碼舉例,對比不一樣的編碼方式。
// java同步代碼
ItemDetailDO queryItemDetail(Long itemId) {
    ItemDetailDO data = new ItemDetailDO();
    data.setBrowseCount(IdleItemBrowseService.count(itemId));// 多少人看過
    data.setFavorCount(IdleItemFavorService.count(itemId));// 多少人點贊
    return data;
}

// dart異步代碼
ItemDetailDO queryItemDetail(int itemId) async {
    var data = new ItemDetailDO();
    await Future.wait([
        IdleItemBrowseService.count(itemId).then((count) => data.browseCount = count)
                      .catchError((exp, stackTrace) => logError('$exp $stackTrace')),
          IdleItemFavorService.count(itemId).then((count) => data.favorCount = count)
                      .catchError((exp, stackTrace) => logError('$exp $stackTrace'))
    ]);
    return data;
}

// rxjava異步代碼
ItemDetailDO queryItemDetail(Long itemId) {
      ItemDetailDO data = new ItemDetailDO();
    Flowable<Long> browseCountFlow = Flowable.fromCallable(
      () => IdleItemBrowseService.count(itemId)
    ).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
    Flowable<Long> favorCountFlow = Flowable.fromCallable(
      () => IdleItemFavorService.count(itemId)
    ).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
    Flowable.zip(browseCountFlow, favorCountFlow, (browseCount, favorCount) -> {
        data.setBrowseCount(browseCount);
        data.setFavorCount(favorCount);
    }).blockingFirst();
}

在java中咱們也普遍使用RxJava這種強大的響應式擴展實施異步操做:RxJava做爲java的響應式編程擴展,功能很是強大全面,它使用流的概念封裝全部的異步操做。須要注意的是這裏的兩個服務調用都被放到一個IO線程池中運行, 這個線程池是無界的,容易消耗線程這種系統稀缺的資源。這意味着當流量很是大的時候,系統的線程池很容易被打滿,須要設置合理的背壓策略。
從上面的代碼中能夠看到「數據獲取」,「數據組裝」的邏輯很是清晰,不像同步代碼分散在各處;相比於同步操做,dart的異步操做容許咱們同時等待多個IO事件,下降總的響應時間。dart的異步代碼擁有同步代碼的簡潔容易理解的優勢,又具備異步編程的性能優點。
dart異步的原理也是容易理解的。做爲單線程語言,dart依靠事件循環運行代碼。dart從main函數開始執行,咱們在main函數裏面建立Future,至關於在一個dart內部維護的事件隊列(event queue)中添加計劃任務(添加的任務並不會當即執行)。main中的代碼執行完以後,dart事件循環開始從事件隊列中依次獲取任務執行。async/await是dart的語法糖,它容許開發人員可以以書寫同步代碼的方式來實施異步編程(在C#、javascript中也有相似實現)。被async修飾的方法返回一個Future,調用這樣的方法,至關於建立一個Future。await一個Future,至關於把await以後的代碼打包放在Future.then()的代碼塊裏,這樣就保證以後的代碼在Future以後執行。因爲任務存儲於事件隊列,dart在流量大的時候,內存消耗較大,也須要咱們前期合理評估需求和分配系統資源。java

dart後端開發實戰

爲了提升開發效率,咱們利用dart的特性構建了一套高效的隔離開發環境。在業務開發實踐中,咱們總結出基本的開發架構和代碼模式。在這些技術基礎上,開發了閒魚寶貝詳情頁的主幹業務。下面逐一介紹。python

高效的隔離開發環境

咱們以往的開發場景是:提交代碼 -> 代碼衝突(多人共用一個部署環境) -> 構建/部署 -> 經過接口驗證 -> 提交fix -> 構建/部署 -> 驗證 的迭代。在這個過程當中,開發人員有可能須要親自解決代碼衝突,或者依賴別人解決代碼衝突,須要等待構建/部署的時間(少則5分鐘,多則十幾分鍾)。並且這個過程可能須要迭代屢次,時間成本很高,若是由於其餘開發人員的代碼分支的問題致使部署失敗,那麼等待驗證的時間成倍增長。這樣的開發效率顯然不是特別理想。c++

在閒魚的dart應用中,這種問題會獲得緩解。每一個開發人員使用本身獨立的開發環境,開發環境使用每一個人的工號惟一識別。在不須要提交代碼的狀況下,開發人員把代碼部署到遠程預發環境中,並在本地調用預發服務,查看服務的輸出,作到本地驗證調試的效果,極大地提升了開發效率。由於只會有開發本身單一分支的代碼部署,不會牽扯到代碼衝突。整個過程,部署、服務調用過程十分快速,能夠在10秒內完成。驗證和調試的效率很是高。web

每一個開發人員的獨立開發環境對應預發機器上的一個isolate。dart的isolate至關於一個線程,可是不會和其餘isolate共享內存,isolate之間的通訊經過發送、接收消息完成。閒魚技術團隊使用每一個開發人員的代碼建立一個isolate,使用工號做爲標識,代碼能夠全量替換掉運行中的isolate,也可使用熱部署增量替換掉isolate中更改的功能。整個過程很是快。在早期使用dart原生的編譯器,發現速度較慢(10多秒)後,咱們對dart編譯器作了裁剪和優化,把編譯時間從10多秒下降到幾百毫秒(簡單來講就是,把dart原生的編譯器的附加功能,從新封裝,而後經過JIT/AOT生成新的編譯工具)。通過咱們對dart開發環境的加強,如今開發dart膠水層接口,只須要點擊開發工具上的一個按鈕,就能夠把修改的代碼,在幾秒內部署到遠程的預發環境,並調用當前的開發接口,在本地查看輸出。得到和在預發環境上驗證同樣的效果,可是體驗就像在開發一個徹底不依賴外部的本地應用程序。編程

業務開發架構

業務開發中最重要的部分是分離出變化和不變的部分,變化的部分用最靈活、快捷的方式實現(變的最多的地方固然用最快的方式處理),不變的部分使用穩定、高效的方式實現。咱們已經把dart建設成爲一種可以高效開發,而且適合客戶端、前端、後端技術人員共同使用的技術。這種技術最適合應用於發生快速變化的接口層,也就是客戶端和後端交互的地方,業務需求的變化致使這裏的數據結構快速變化,也稱之爲膠水層。對於相對穩定的數據服務,咱們使用java實現爲領域服務。

上圖是服務之間的交互圖,實現方式以下圖所示:

膠水層dart應用以HTTP協議方式做爲MTOP接口提供給客戶端調用,往下使用HSF從Java應用中獲取數據。
一般先定義並開發好領域服務,而後再與客戶端對接開發出接口,領域服務提供的接口,包含了獲取基礎數據的全部方法,開發好以後,不多發生變化;膠水層獲取領域服務提供的數據,對數據進行加工、裁剪、組裝,輸出爲客戶端可以解析的視圖數據,客戶端解析、渲染、展現爲頁面。膠水層的代碼大體能夠分爲:獲取數據,而後數據處理和組裝。抽象出代碼模式以下所示:後端

// 數據處理和組裝
void putTiger(Zoo zoo, Tiger tiger) => zoo.tiger =  tiger;
void putDophin(Zoo zoo, Dophin dophin) => zoo.dophin =  dophin;
void putRatel(Zoo zoo, Ratel ratel) => zoo.ratel =  ratel;

// 發起多個異步請求,全部請求完成後返回全部數據
Future<T> catchError<T>(Future<T> future) {
  return future.catchError((e, st) => LogUtils.error('$e $st'));
}
Future<List<T>> waitFutures<T>(List<Future<T>> futures) {
  Future<List<T>> future = Future.wait(futures.map(catchError));
  return catchError(future);
}

// 服務接口
Future<Zoo> process(Parameter param) async {
  var zoo = new Zoo();
  // 數據獲取
  await waitFutures(
    Service1.invoke(param).then((animal) -> putTiger(zoo, animal)),
    Service2.invoke(param).then((animal) -> putLion(zoo, dophin)),
    Service3.invoke(param).then((animal) -> putRatel(zoo, animal))
  );
  return finalData;
}

爲了使用java的領域服務,咱們首先解決了dart和java之間數據交互問題,主要是經過序列化對java類文件和dart類文件進行合理的轉換,保證dart可以透明、簡潔地使用java的數據結構,調用java的遠程服務;在調用鏈路上設置全局惟一的上下文id,跨越dart和java調用棧,支持全鏈路排查;對全部的服務的成功率,rt和額外業務參數有詳細的日誌,能夠配置以日誌爲數據源的監控告警等等(後續的文章將詳細介紹咱們對這些問題的詳細解決方案,請持續關注哦)。緩存

服務化詳情頁主幹開發

閒魚寶貝詳情頁是咱們使用dart開發的一個重要項目。最先的閒魚寶貝詳情頁把各個業務的代碼邏輯耦合在一塊兒,致使維護和變動困難,穩定性也難以保證。咱們設計的swak框架(更多細節請查看文章swak框架),可以分離垂直業務的共性和差別性,把閒魚寶貝詳情頁的實現分割成主幹實現和垂直業務實現兩塊。咱們使用本身開發的dart後端開發框架,對swak框架作了最小實現。項目完成了詳情頁主幹的完整功能和基礎優化:數據結構

  • 垂直業務路由:咱們使用dart中的zone存儲每一個閒魚商品的業務標識,代碼生成的靜態代理類依據業務標識調用相應的服務,在主幹數據裏填充各個業務的獨有數據。zone是dart異步代碼的執行環境,可以緩存一些可重用數據(業務代碼裏除非非此不可,儘可能不要多用)
  • 做爲遠程服務的提供方:在hsfcpp對hessian協議的實現基礎上作開發,dart也能成爲遠程服務的提供方
  • 服務調用的優化: 對java遠程服務的代理作了優化,隔離業務層面對框架層的感知,作到透明調用
  • 解決緩存調用的差別性:咱們依賴緩存的c++接口訪問緩存,可是仍然須要處理java/c++緩存讀寫不兼容問題完成dart和java對同一緩存的同時讀寫
    項目流程圖可見下圖:

    ![dart-detail-flow.png](http://gw.alicdn.com/mt/TB1Pyv6V9zqK1RjSZFjXXblCFXa-558-561.png)

實際效果

目前該項目已經上線超過6周,qps最高可達400,成功率在99.5%以上。整個調用鏈路的RT與一樣功能的java應用持平。因爲前期的精心設計,領域服務不多改動,大部分變動發生在dart膠水層。從上線後經歷的若干次變動來看,dart膠水層從修改代碼結束到提供給客戶端使用總耗時不超過2分鐘,而相同功能的java應用須要10分鐘以上。

總結

dart是一門簡潔、容易上手、對異步支持良好的編程語言,在flutter的開發中大放異彩。在咱們的努力下,dart用於後端開發的支持逐漸完善,前臺開發同窗和後端開發人員快速高效地開發膠水層接口。咱們在不少生產項目中使用了dart用於後端開發,性能、穩定性良好,開發效率大大提升。將來咱們會着力於進一步改善dart開發體驗、與java項目的兼容性、提高dart遠程服務的性能,挖掘dart在後端開發中更大的潛力。


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索