分佈式(一) 搞定服務註冊與發現

背景

最近在作分佈式相關的工做,因爲人手不夠只能我一我的來懟;看着這段時間的加班表想一想就是夠慘的。java

不過其中也有遇到的很多有意思的事情從此再拿來分享,今天重點來討論服務的註冊與發現node

分佈式帶來的問題

個人業務比較簡單,只是須要知道如今有哪些服務實例可供使用就能夠了(並非作遠程調用,只須要拿到信息便可)。git

要實現這一功能最簡單的方式能夠在應用中配置全部的服務節點,這樣每次在使用時只須要經過某種算法從配置列表中選擇一個就能夠了。github

但這樣會有一個很是嚴重的問題:算法

因爲應用須要根據應用負載狀況來靈活的調整服務節點的數量,這樣個人配置就不能寫死。apache

否則就會出現要麼新增的節點沒有訪問或者是已經 down 掉的節點卻有請求,這樣確定是不行的。api

每每要解決這類分佈式問題都須要一個公共的區域來保存這些信息,好比是否能夠利用 Redis?緩存

每一個節點啓動以後都向 Redis 註冊信息,關閉時也刪除數據。服務器

其實就是存放節點的 ip + port,而後在須要知道服務節點信息時候只須要去 Redis 中獲取便可。網絡

以下圖所示:

但這樣會致使每次使用時都須要頻繁的去查詢 Redis,爲了不這個問題咱們能夠在每次查詢以後在本地緩存一份最新的數據。這樣優先從本地獲取確實能夠提升效率。

但一樣又會出現新的問題,若是服務提供者的節點新增或者刪除消費者這邊根本就不知道狀況。

要解決這個問題最早想到的應該就是利用定時任務按期去更新服務列表。

以上的方案確定不完美,而且不優雅。主要有如下幾點:

  • 基於定時任務會致使不少無效的更新。
  • 定時任務存在週期性,無法作到實時,這樣就可能存在請求異常。
  • 若是服務被強行 kill,無法及時清除 Redis,這樣這個看似可用的服務將永遠不可用!

因此咱們須要一個更加靠譜的解決方案,這樣的場景其實和 Dubbo 很是相似。

用過的同窗確定對這張圖不陌生。

引用自 Dubbo 官網

其中有一塊很是核心的內容(紅框出)就是服務的註冊與發現。

一般來講消費者是須要知道服務提供者的網絡地址(ip + port)才能發起遠程調用,這塊內容和我上面的需求其實很是相似。

而 Dubbo 則是利用 Zookeeper 來解決問題。

Zookeeper 能作什麼

在具體討論怎麼實現以前先看看 Zookeeper 的幾個重要特性。

Zookeeper 實現了一個相似於文件系統的樹狀結構:

這些節點被稱爲 znode(名字叫什麼不重要),其中每一個節點均可以存放必定的數據。

最主要的是 znode 有四種類型:

  • 永久節點(除非手動刪除,節點永遠存在)
  • 永久有序節點(按照建立順序會爲每一個節點末尾帶上一個序號如:root-1
  • 瞬時節點(建立客戶端與 Zookeeper 保持鏈接時節點存在,斷開時則刪除並會有相應的通知)
  • 瞬時有序節點(在瞬時節點的基礎上加上了順序)

考慮下上文使用 Redis 最大的一個問題是什麼?

其實就是不能實時的更新服務提供者的信息。

那利用 Zookeeper 是怎麼實現的?

主要看第三個特性:瞬時節點

Zookeeper 是一個典型的觀察者模式。

  • 因爲瞬時節點的特色,咱們的消費者能夠訂閱瞬時節點的父節點。
  • 當新增、刪除節點時全部的瞬時節點也會自動更新。
  • 更新時會給訂閱者發起通知告訴最新的節點信息。

這樣咱們就能夠實時獲取服務節點的信息,同時也只須要在第一次獲取列表時緩存到本地;也不須要頻繁和 Zookeeper 產生交互,只用等待通知更新便可。

而且無論應用什麼緣由節點 down 掉後也會在 Zookeeper 中刪除該信息。

效果演示

這樣實現方式就變爲這樣。

爲此我新建了一個應用來進行演示:

https://github.com/crossoverJie/netty-action/tree/master/netty-action-zk

就是一個簡單的 SpringBoot 應用,只是作了幾件事情。

  • 應用啓動時新開一個線程用於向 Zookeeper 註冊服務。
  • 同時監聽一個節點用於更新本地服務列表。
  • 提供一個接口用於返回一個可有的服務節點。

我在本地啓動了兩個應用分別是:127.0.0.1:8083,127.0.0.1:8084。來看看效果圖。

兩個應用啓動完成:


當前 Zookeeper 的可視化樹狀結構:


當想知道全部的服務節點信息時:


想要獲取一個可用的服務節點時:

這裏只是採起了簡單的輪詢。


當 down 掉一個節點時:應用會收到通知更新本地緩存。同時 Zookeeper 中的節點會自動刪除。


再次獲取最新節點時:


當節點恢復時天然也能獲取到最新信息。本地緩存也會及時更新。

編碼實現

實現起來倒也比較簡單,主要就是 ZKClient 的 api 使用。

貼幾段比較核心的吧。

註冊

啓動註冊 Zookeeper。

主要邏輯都在這個線程中。

  • 首先建立父節點。如上圖的 Zookeeper 節點所示;須要先建立 /route 根節點,建立的時候會判斷是否已經存在。
  • 接着須要判斷是否須要將本身註冊到 Zookeeper 中,由於有些節點只是用於服務發現,他自身是不須要承擔業務功能(是我本身項目的需求)。
  • 將當前應用的所在 ip 以及端口註冊上去,同時須要監聽根節點 /route ,這樣才能在其餘服務上下線時候得到通知。

根據本地緩存

監聽到服務變化
public void subscribeEvent(String path) {
        zkClient.subscribeChildChanges(path, new IZkChildListener() {
            @Override
            public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
                logger.info("清除/更新本地緩存 parentPath=【{}】,currentChilds=【{}】", parentPath,currentChilds.toString());

                //更新全部緩存/先刪除 再新增
                serverCache.updateCache(currentChilds) ;
            }
        });


    }

能夠看到這裏是更新了本地緩存,該緩存採用了 Guava 提供的 Cache,感興趣的能夠查看以前的源碼分析

/**
     * 更新全部緩存/先刪除 再新增
     *
     * @param currentChilds
     */
    public void updateCache(List<String> currentChilds) {
        cache.invalidateAll();
        for (String currentChild : currentChilds) {
            String key = currentChild.split("-")[1];
            addCache(key);
        }
    }

客戶端負載

同時在客戶端提供了一個負載算法。

其實就是一個輪詢的實現:

/**
     * 選取服務器
     *
     * @return
     */
    public String selectServer() {
        List<String> all = getAll();
        if (all.size() == 0) {
            throw new RuntimeException("路由列表爲空");
        }
        Long position = index.incrementAndGet() % all.size();
        if (position < 0) {
            position = 0L;
        }

        return all.get(position.intValue());
    }

固然這裏能夠擴展出更多的如權重、隨機、LRU 等算法。

Zookeeper 其餘優點及問題

Zookeeper 天然是一個很棒的分佈式協調工具,利用它的特性還能夠有其餘做用。

  • 數據變動發送通知這一特性能夠實現統一配置中心,不再須要在每一個服務中單獨維護配置。
  • 利用瞬時有序節點還能夠實現分佈式鎖。

在實現註冊、發現這一需求時,Zookeeper 其實並非最優選。

因爲 Zookeeper 在 CAP 理論中選擇了 CP(一致性、分區容錯性),當 Zookeeper 集羣有半數節點不可用時是不能獲取到任何數據的。

對於一致性來講天然沒啥問題,但在註冊、發現的場景下更加推薦 Eureka,已經在 SpringCloud 中獲得驗證。具體就不在本文討論了。

但鑑於個人使用場景來講 Zookeeper 已經可以勝任。

總結

本文全部完整代碼都託管在 GitHub。

https://github.com/crossoverJie/netty-action

一個看似簡單的註冊、發現功能實現了,但分佈式應用遠遠不止這些。

因爲網絡隔離以後帶來的一系列問題還須要咱們用其餘方式一一完善;後續會繼續更新分佈式相關內容,感興趣的朋友不妨持續關注。

你的點贊與轉發是最大的支持。

相關文章
相關標籤/搜索