Categories
程式開發

瓜子二手車在Dubbo版本升級,多機房方案方面的思考和實踐


前言

隨著瓜子業務的不斷發展,系統規模在逐漸擴大,目前在瓜子的私有云上​​已經運行著數百個 Apache 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 版本的過程整體上比較順利,當然也遇到了一些兼容性問題:

1、創建 ZooKeeper 節點時提示沒有權限

Dubbo 配置文件中已經配置了 ZooKeeper 的用戶名密碼,但在創建 ZooKeeper 節點時卻拋出 KeeperErrorCode = NoAuth 的異常,這種情況分別對應兩個兼容性問題:

a. issues:

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

dubbo在未配置配置中心時,默認使用註冊中心作為配置中心。通過註冊中心的配置信息初始化配置中心配置時,由於遺漏了用戶名密碼,導致此問題。

b. issues:

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

Dubbo 在建立與 ZooKeeper 的連接時會根據 ZooKeeper 的 address 復用之前已建立的連接。當多個註冊中心使用同一個 address ,但權限不同時,就會出現 NoAuth 的問題。參考社區的 PR ,我們在內部版本進行了修復。

2、curator 版本兼容性問題

a. Dubbo2.7.3 與低版本的 curator 不兼容,因此我們默認將 curator 版本升級至4.2.0


    org.apache.curator
    curator-framework
    4.2.0


    org.apache.curator
    curator-recipes
    4.2.0

b. 分佈式調度框架 elastic-job-lite 強依賴低版本的 curator ,與 Dubbo2.7.3 使用的 curator 版本不兼容,這給 Dubbo 版本升級工作帶來了一定阻塞。考慮到 elastic-job-lite 已經很久沒有人進行維護,目前一些業務線計劃將 elastic-job-lite 替換為其他的調度框架。

3、 OpenFeign 與 Dubbo 兼容性問題

issues:

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

Dubbo 的 ServiceBean 監聽 Spring 的 ContextRefreshedEvent ,進行服務暴露。 OpenFeign提前觸發了 ContextRefreshedEvent ,此時 ServiceBean 還未完成初始化,於是就導致了應用啟動異常。

參考社區的pr,我們在內部版本修復了此問題。

4、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

5、QoS 端口占用

Dubbo2.7.3 默認開啟 QoS 功能,導致一些混部在物理機的 Dubbo 服務升級時出現 qos 端口占用問題。關閉 QoS 功能後恢復。

6、自定義擴展兼容性問題

業務線對於 Dubbo 的自定義擴展比較少,因此在自定義擴展的兼容性方面暫時還沒有遇到比較難處理的問題,基本上都是變更 package 導致的問題,由業務線自行修復。

7、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 推廣工作。

本文轉載自公眾號阿里巴巴中間件(ID:Aliware_2018)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzU4NzU0MDIzOQ==&mid=2247488596&idx=1&sn=12f562bd0d1c2fe6b8630edd48748606&chksm=fdeb2634ca9caf22ef9296c1682bcf5867875e576e3d64a7ecfcb7dfabfe67a8f8afb5f8fc4f&scene=27#wechat_redirect、