天天看點

瓜子二手車在 Dubbo 版本更新、多機房方案方面的思考和實踐

前言

  随着瓜子業務的不斷發展,系統規模在逐漸擴大,目前在瓜子的私有雲上已經運作着數百個 Dubbo 應用,上千個 Dubbo 執行個體。瓜子各部門業務迅速發展,版本沒有來得及統一,各個部門都有自己的用法。随着第二機房的建設,Dubbo 版本統一的需求變得越發迫切。幾個月前,公司發生了一次與 Dubbo 相關的生産事故,成為了公司 基于社群 Dubbo 2.7.3 版本更新的誘因。

  接下來,我會從這次線上事故開始,講講我們這段時間所做的 Dubbo 版本更新的曆程以及我們規劃的 Dubbo 後續多機房的方案。

一、Ephermal節點未及時删除導緻provider不能恢複注冊的問題修複

事故背景

  在生産環境,瓜子内部各業務線共用一套zookeeper叢集作為dubbo的注冊中心。2019年9月份,機房的一台交換機發生故障,導緻zookeeper叢集出現了幾分鐘的網絡波動。在zookeeper叢集恢複後,正常情況下dubbo的provider應該會很快重新注冊到zookeeper上,但有一小部分的provider很長一段時間沒有重新注冊到zookeeper上,直到手動重新開機應用後才恢複注冊。

排查過程

  首先,我們統計了出現這種現象的dubbo服務的版本分布情況,發現在大多數的dubbo版本中都存在這種問題,且發生問題的服務比例相對較低,在github中我們也未找到相關問題的issues。是以,推斷這是一個尚未修複的且在網絡波動情況的場景下偶現的問題。

  接着,我們便将出現問題的應用日志、zookeeper日志與dubbo代碼邏輯進行互相印證。在應用日志中,應用重連zookeeper成功後provider立刻進行了重新注冊,之後便沒有任何日志列印。而在zookeeper日志中,注冊節點被删除後,并沒有重新建立注冊節點。對應到dubbo的代碼中,隻有在

FailbackRegistry.register(url)

doRegister(url)

執行成功或線程被挂起的情況下,才能與日志中的情況相吻合。

public void register(URL url) {
        super.register(url);
        failedRegistered.remove(url);
        failedUnregistered.remove(url);
        try {
            // Sending a registration request to the server side
            doRegister(url);
        } catch (Exception e) {
            Throwable t = e;

            // If the startup detection is opened, the Exception is thrown directly.
            boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                    && url.getParameter(Constants.CHECK_KEY, true)
                    && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
            boolean skipFailback = t instanceof SkipFailbackWrapperException;
            if (check || skipFailback) {
                if (skipFailback) {
                    t = t.getCause();
                }
                throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
            } else {
                logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
            }

            // Record a failed registration request to a failed list, retry regularly
            failedRegistered.add(url);
        }
    }           

  在繼續排查問題前,我們先普及下這些概念:dubbo預設使用curator作為zookeeper的用戶端,curator與zookeeper是通過session維持連接配接的。當curator重連zookeeper時,若session未過期,則繼續使用原session進行連接配接;若session已過期,則建立新session重新連接配接。而ephemeral節點與session是綁定的關系,在session過期後,會删除此session下的ephemeral節點。

  繼續對

doRegister(url)

的代碼進行進一步排查,我們發現在

CuratorZookeeperClient.createEphemeral(path)

方法中有這麼一段邏輯:在

createEphemeral(path)

捕獲了

NodeExistsException

,建立ephemeral節點時,若此節點已存在,則認為ephemeral節點建立成功。這段邏輯初看起來并沒有什麼問題,且在以下兩種常見的場景下表現正常:

  1. Session未過期,建立Ephemeral節點時原節點仍存在,不需要重新建立
  2. Session已過期,建立Ephemeral節點時原節點已被zookeeper删除,建立成功
public void createEphemeral(String path) {
        try {
            client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
        } catch (NodeExistsException e) {
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }           

  但是實際上還有一種極端場景,zookeeper的Session過期與删除Ephemeral節點不是原子性的,也就是說用戶端在得到Session過期的消息時,Session對應的Ephemeral節點可能還未被zookeeper删除。此時dubbo去建立Ephemeral節點,發現原節點仍存在,故不重新建立。待Ephemeral節點被zookeeper删除後,便會出現dubbo認為重新注冊成功,但實際未成功的情況,也就是我們在生産環境遇到的問題。

  此時,問題的根源已被定位。定位問題之後,經我們與 Dubbo 社群交流,發現考拉的同學也遇到過同樣的問題,更确定了這個原因。

問題的複現與修複

  定位到問題之後,我們便開始嘗試本地複現。由于zookeeper的Session過期但Ephemeral節點未被删除的場景直接模拟比較困難,我們通過修改zookeeper源碼,在Session過期與删除Ephemeral節點的邏輯中增加了一段休眠時間,間接模拟出這種極端場景,并在本地複現了此問題。

  在排查問題的過程中,我們發現kafka的舊版本在使用zookeeper時也遇到過類似的問題,并參考kafka關于此問題的修複方案,确定了dubbo的修複方案。在建立Ephemeral節點捕獲到

NodeExistsException

時進行判斷,若Ephemeral節點的SessionId與目前用戶端的SessionId不同,則删除并重建Ephemeral節點。在内部修複并驗證通過後,我們向社群送出了issues及pr。

  kafka類似問題issues:

https://issues.apache.org/jira/browse/KAFKA-1387

  dubbo注冊恢複問題issues:

https://github.com/apache/dubbo/issues/5125

二、瓜子的dubbo更新曆程

  上文中的問題修複方案已經确定,但我們顯然不可能在每一個dubbo版本上都進行修複。在咨詢了社群dubbo的推薦版本後,我們決定在dubbo2.7.3版本的基礎上,開發内部版本修複來這個問題。并借這個機會,開始推動公司dubbo版本的統一更新工作。

為什麼要統一dubbo版本

  1. 統一dubbo版本後,我們可以在此版本上内部緊急修複一些dubbo問題(如上文的dubbo注冊故障恢複失效問題)。
  2. 瓜子目前正在進行第二機房的建設,部分dubbo服務也在逐漸往第二機房遷移。統一dubbo版本,也是為dubbo的多機房做鋪墊。
  3. 有利于我們後續對dubbo服務的統一管控。
  4. dubbo社群目前的發展方向與我們公司現階段對dubbo的一些訴求相吻合,如支援gRPC、雲原生等。

為什麼選擇dubbo2.7.3

  1. 我們了解到,在我們之前攜程已經與dubbo社群合作進行了深度合作,攜程内部已全量更新為 2.7.3 的社群版本,并在協助社群修複了 2.7.3 版本的一些相容性問題。感謝攜程的同學幫我們踩坑~
  2. dubbo2.7.3版本在當時雖然是最新的版本,但已經釋出了2個月的時間,從社群issues回報來看,dubbo2.7.3相對dubbo2.7之前的幾個版本,在相容性方面要好很多。
  3. 我們也咨詢了dubbo社群的同學,推薦更新版本為2.7.3。

内部版本定位

  基于社群dubbo2.7.3版本開發的dubbo内部版本屬于過渡性質的版本,目的是為了修複線上provider不能恢複注冊的問題,以及一些社群dubbo2.7.3的相容性問題。瓜子的dubbo最終還是要跟随社群的版本,而不是開發自已的内部功能。是以我們在dubbo内部版本中修複的所有問題均與社群保持了同步,以保證後續可以相容更新到社群dubbo的更高版本。

相容性驗證與更新過程

  我們在向dubbo社群的同學咨詢了版本更新方面的相關經驗後,于9月下旬開始了dubbo版本的更新工作。

  1. 初步相容性驗證

    首先,我們梳理了一些需要驗證的相容性case,針對公司内部使用較多的dubbo版本,與dubbo2.7.3一一進行了相容性驗證。經驗證,除dubboX外,dubbo2.7.3與其他dubbo版本均相容。dubboX由于對dubbo協定進行了更改,與dubbo2.7.3不相容。

  2. 生産環境相容性驗證

    在初步驗證相容性通過後,我們與業務線合作,挑選了一些重要程度較低的項目,在生産環境對dubbo2.7.3與其他版本的相容性進行了進一步驗證。并在内部版本修複了一些相容性問題。

  3. 推動公司dubbo版本更新

    在10月初,完成了dubbo相容性驗證後,我們開始在各個業務線推動dubbo的更新工作。截止到12月初,已經有30%的dubbo服務的完成了版本更新。按照排期,預計于2020年3月底前完成公司dubbo版本的統一更新。

相容性問題彙總

  在推動更新dubbo2.7.3版本的過程整體上比較順利,當然也遇到了一些相容性問題:

  • 建立zookeeper節點時提示沒有權限

    dubbo配置檔案中已經配置了zookeeper的使用者名密碼,但在建立zookeeper節點時卻抛出

    KeeperErrorCode = NoAuth

    的異常,這種情況分别對應兩個相容性問題:
    • issues: https://github.com/apache/dubbo/issues/5076 dubbo在未配置配置中心時,預設使用注冊中心作為配置中心。通過注冊中心的配置資訊初始化配置中心配置時,由于遺漏了使用者名密碼,導緻此問題。
    • https://github.com/apache/dubbo/issues/4991 dubbo在建立與zookeeper的連接配接時會根據zookeeper的address複用之前已建立的連接配接。當多個注冊中心使用同一個address,但權限不同時,就會出現

      NoAuth

      的問題。
    參考社群的pr,我們在内部版本進行了修複。
  • curator版本相容性問題
    • dubbo2.7.3與低版本的curator不相容,是以我們預設将curator版本更新至4.2.0
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>4.2.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.2.0</version>
    </dependency>           
    • 分布式排程架構elastic-job-lite強依賴低版本的curator,與dubbo2.7.3使用的curator版本不相容,這給dubbo版本更新工作帶來了一定阻塞。考慮到elastic-job-lite已經很久沒有人進行維護,目前一些業務線計劃将elastic-job-lite替換為其他的排程架構。
  • openFeign與dubbo相容性問題 https://github.com/apache/dubbo/issues/3990

dubbo的ServiceBean監聽spring的ContextRefreshedEvent,進行服務暴露。openFeign提前觸發了ContextRefreshedEvent,此時ServiceBean還未完成初始化,于是就導緻了應用啟動異常。

參考社群的pr,我們在内部版本修複了此問題。

  • RpcException相容性問題

    dubbo低版本consumer不能識别dubbo2.7版本provider抛出的

    org.apache.dubbo.rpc.RpcException

    。是以,在consumer全部更新到2.7之前,不建議将provider的

    com.alibaba.dubbo.rpc.RpcException

    改為

    org.apache.dubbo.rpc.RpcException

  • qos端口占用

    dubbo2.7.3預設開啟qos功能,導緻一些混部在實體機的dubbo服務更新時出現qos端口占用問題。關閉qos功能後恢複。

  • 自定義擴充相容性問題

    業務線對于dubbo的自定義擴充比較少,是以在自定義擴充的相容性方面暫時還沒有遇到比較難處理的問題,基本上都是變更package導緻的問題,由業務線自行修複。

  • skywalking agent相容性問題

    我們項目中一般使用skywalking進行鍊路追蹤,由于skywalking agent6.0的plugin不支援dubbo2.7,是以統一更新skywalking agent到6.1。

三、dubbo多機房方案

  瓜子目前正在進行第二機房的建設工作,dubbo多機房是第二機房建設中比較重要的一個話題。在dubbo版本統一的前提下,我們就能夠更順利的開展dubbo多機房相關的調研與開發工作。

初步方案

  我們咨詢了dubbo社群的建議,并結合瓜子雲平台的現狀,初步确定了dubbo多機房的方案。

  1. 在每個機房内,部署一套獨立的zookeeper叢集。叢集間資訊不同步。這樣就沒有了zookeeper叢集跨機房延遲與資料不同步的問題。
  2. dubbo服務注冊時,僅注冊到本機房的zookeeper叢集;訂閱時,同時訂閱兩個機房的zookeeper叢集。
  3. 實作同機房優先調用的路由邏輯。以減少跨機房調用導緻的不必要網絡延遲。

同機房優先調用

  dubbo同機房優先調用的實作比較簡單,相關邏輯如下:

  1. 瓜子雲平台預設将機房的标志資訊注入容器的環境變量中。
  2. provider暴露服務時,讀取環境變量中的機房标志資訊,追加到待暴露服務的url中。
  3. consumer調用provider時,讀取環境變量中的機房标志資訊,根據路由政策優先調用具有相同标志資訊的provider。

  針對以上邏輯,我們簡單實作了dubbo通過環境變量進行路由的功能,并向社群送出了pr。

  dubbo通過環境變量路由pr:

https://github.com/apache/dubbo/pull/5348

本文作者:李錦濤,任職于瓜子二手車基礎架構部門,負責瓜子微服務架構相關工作。目前主要負責公司内 Dubbo 版本更新與推廣、 Skywalking 推廣工作。