小白也能懂的 Nacos 服務模型介紹

做者:島風數據庫

前言


按照目前市場上的主流使用場景,Nacos 被分紅了兩塊功能:服務註冊發現(Naming)和配置中心(Config)。在以前的文章中我介紹了 Nacos 配置中心的實現原理,今天這篇文章所介紹的內容則是與 Nacos 服務註冊發現功能相關,來聊一聊 Nacos 的服務模型。緩存


說到服務模型,其實須要區分視角,一是用戶視角,一個內核視角。即 Nacos 用戶視角看到的服務模型和 Nacos 開發者設計的內核模型多是徹底不同的,而今天的文章,是站在用戶視角觀察的,旨在探討 Nacos 服務發現的最佳實踐。數據結構

 

服務模型介紹


通常我在聊註冊中心時,都會以 Zookeeper 爲引子,這也是不少人最熟悉的註冊中心。但若是你真的寫過或看過使用 Zookeeper 做爲註冊中心的適配代碼,會發現並非那麼容易,再加上註冊中心涉及到的一致性原理,這就致使不少人對註冊中心的第一印象是:這個東西好難!但歸根究竟是由於 Zookeeper 根本不是專門爲註冊中心而設計的,其提供的 API 以及內核設計,並無預留出「服務模型」的概念,這就使得開發者須要自行設計一個模型,去填補 Zookeeper 和服務發現之間的鴻溝。架構


微服務架構逐漸深刻人心後,Nacos、Consul、Eureka 等註冊中心組件進入大衆的視線。能夠發現,這些」真正「的註冊中心都有各自的「服務模型」,在使用上也更加的方便。app


爲何要有「服務模型」?理論上,一個基礎組件能夠被塑形成任意的模樣,若是你願意,一個數據庫也能夠被設計成註冊中心,這並非」誇張「的修辭手法,在阿里還真有人這麼幹過。那麼代價是什麼呢?必定會在業務發展到必定體量後遇到瓶頸,必定會遇到某些極端 case 致使其沒法正常工做,必定會致使其擴展性低下。正如剛學習數據結構時,同窗們常見的一個疑問同樣:爲何棧只能先進後出。不是全部開發都是中間件專家,因此 Nacos 設計了本身的「服務模型」,這雖然限制了使用者的」想象力「,但保障了使用者在正確地使用 Nacos。負載均衡


花了必定的篇幅介紹 Nacos 爲何須要設計「服務模型」,再來看看實際的 Nacos 模型是個啥,其實沒那麼玄乎,一張圖就能表達清楚:框架

 


與 Consul、Eureka 設計有別,Nacos 服務發現使用的領域模型是命名空間-分組-服務-集羣-實例這樣的多層結構。服務 Service 和實例 Instance 是核心模型,命名空間 Namespace 、分組 Group、集羣 Cluster 則是在不一樣粒度實現了服務的隔離。運維


爲了更好的理解兩個核心模型:Service 和 Instance,咱們以 Dubbo 和 SpringCloud 這兩個已經適配了 Nacos 註冊中心的微服務框架爲例,介紹下兩者是如何映射對應模型的。ide

  • Dubbo。將接口三元組(接口名+分組名+版本號)映射爲 Service,將實例 IP 和端口號定義爲 Instance。一個典型的註冊在 Nacos 中的 Dubbo 服務:providers:com.alibaba.mse.EchoService:1.0.0:DUBBO
  • Spring Cloud。將應用名映射爲 Service,將實例 IP 和端口號定義爲 Instance。一個典型的註冊在 Nacos 中的 Spring Cloud 服務:helloApp


下面咱們將會更加詳細地闡釋 Nacos 提供的 API 和服務模型之間的關係。微服務

 

環境準備


須要部署一個 Nacos Server 用於測試,我這裏選擇直接在 https://mse.console.aliyun.com/ 購買一個 MSE 託管的 Nacos,讀者們能夠選擇購買 MSE Nacos 或者自行搭建一個 Nacos Server。


MSE Nacos 提供的可視化控制檯,也能夠幫助咱們更好的理解 Nacos 的服務模型。下文的一些截圖,均來自 MSE Nacos 的商業化控制檯。

 

快速開始


先來實現一個最簡單的服務註冊與發現 demo。Nacos 支持從客戶端註冊服務實例和訂閱服務,具體代碼以下:

Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, "mse-xxxx-p.nacos-ans.mse.aliyuncs.com:8848");

String serviceName = "nacos.test.service.1";
String instanceIp = InetAddress.getLocalHost().getHostAddress();
int instancePort = 8080;

namingService.registerInstance(serviceName, instanceIp, instancePort);

System.out.println(namingService.getAllInstances(serviceName));

上述代碼定義了一個 service:nacos.test.service.1;定義了一個 instance,以本機 host 爲 IP 和 8080 爲端口號,觀察實際的註冊狀況:

 

 


而且控制檯也打印出了服務的詳情。至此一個最簡單的 Nacos 服務發現 demo 就已經完成了。對一些細節稍做解釋:

  • 屬性 PropertyKeyConst.SERVER_ADDR 表示的是 Nacos 服務端的地址。
  • 建立一個 NamingService 實例,客戶端將爲該實例建立單獨的資源空間,包括緩存、線程池以及配置等。Nacos 客戶端沒有對該實例作單例的限制,請當心維護這個實例,以防新建多於預期的實例。
  • 註冊服務 registerInstance 使用了最簡單的重載方法,只須要傳入服務名、IP、端口就能夠。


上述的例子中,並無出現 Namespace、Group、Cluster 等前文說起的服務模型,我會在下面一節詳細介紹,這個例子主要是爲了演示 Nacos 支持的一些缺省配置,其中 Service 和 Instance 是必不可少的,這也驗證了前文提到的服務和實例是 Nacos 的一等公民。
經過截圖咱們能夠發現缺省配置的默認值:

  • Namespace:默認值是 public 或者空字符串,均可以表明默認命名空間。
  • Group:默認值是 DEFAULT_GROUP。
  • Cluster:默認值是 DEFAULT。

 

構建自定義實例


爲了展示出 Nacos 服務模型的全貌,還須要介紹下實例相關的 API。例如咱們但願註冊的實例中,有一些可以被分配更多的流量;或者可以傳入一些實例的信息存儲到 Nacos 服務端,例如 IP 所屬的應用或者所在的機房,這樣在客戶端能夠根據服務下掛載的實例元信息,來自定義負載均衡模式。Nacos 也提供了另外的註冊實例接口,使得用戶在註冊實例時能夠指定實例的屬性:

/**
 * register a instance to service with specified instance properties.
 *
 * @param serviceName name of service
 * @param groupName   group of service
 * @param instance    instance to register
 * @throws NacosException nacos exception
 */
void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException;

這個方法在註冊實例時,能夠傳入一個 Instance 實例,它的屬性以下:

public class Instance {
    
    /**
     * unique id of this instance.
     */
    private String instanceId;
    
    /**
     * instance ip.
     */
    private String ip;
    
    /**
     * instance port.
     */
    private int port;
    
    /**
     * instance weight.
     */
    private double weight = 1.0D;
    
    /**
     * instance health status.
     */
    private boolean healthy = true;
    
    /**
     * If instance is enabled to accept request.
     */
    private boolean enabled = true;
    
    /**
     * If instance is ephemeral.
     *
     * @since 1.0.0
     */
    private boolean ephemeral = true;
    
    /**
     * cluster information of instance.
     */
    private String clusterName;
    
    /**
     * Service information of instance.
     */
    private String serviceName;
    
    /**
     * user extended attributes.
     */
    private Map<String, String> metadata = new HashMap<String, String>();
}


有一些字段能夠望文生義,有一些則須要花些功夫專門去了解 Nacos 的設計,我這裏挑選幾個我認爲重要的屬性重點介紹下:

  • healthy 實例健康狀態。標識該實例是否健康,通常心跳健康檢查會自動更新該字段。
  • enable 是否啓用。它跟 healthy 區別在於,healthy 通常是由內核健康檢查更新,而 enable 更可能是業務語義偏多,能夠徹底根據業務場景操控。例如在 Dubbo 中,通常使用該字段標識某個實例 IP 的上下線狀態。
  • ephemeral 臨時實例仍是持久化實例。很是關鍵的一個字段,須要對 Nacos 有較爲深刻的瞭解纔可以理解該字段的含義。區別在於,心跳檢測失敗必定時間以後,實例是自動下線仍是標記爲不健康。通常在註冊中心場景下,會使用臨時實例。這樣心跳檢測失敗以後,可讓消費者及時收到下線通知;而在 DNS 模式下,使用持久化實例較多。在《一文詳解 Nacos 高可用特性》中我也介紹過,該字段還會影響到 Nacos 的一致性協議。
  • metadata 元數據。一個 map 結構,能夠存儲實例的自定義擴展信息,例如機房信息,路由標籤,應用信息,權重信息等。


這些信息在由服務提供者上報以後,由服務消費者獲取,從而完成信息的傳遞。如下是一個完整的實例註冊演示代碼:

Properties properties = new Properties();
// 指定 Nacos Server 地址
properties.setProperty(PropertyKeyConst.SERVER_ADDR, "mse-xxxx-p.nacos-ans.mse.aliyuncs.com:8848");
// 指定命名空間
properties.setProperty(PropertyKeyConst.NAMESPACE, "9125571e-bf50-4260-9be5-18a3b2e3605b");

NamingService namingService = NacosFactory.createNamingService(properties);
String serviceName = "nacos.test.service.1";
String group = "DEFAULT_GROUP";
String clusterName = "cn-hangzhou";
String instanceIp = InetAddress.getLocalHost().getHostAddress();
int instancePort = 8080;
Instance instance = new Instance();
// 指定集羣名
instance.setClusterName(clusterName);
instance.setIp(instanceIp);
instance.setPort(instancePort);
// 指定實例的元數據
Map<String, String> metadata = new HashMap<>();
metadata.put("app", "nacos-demo");
metadata.put("site", "cn-hangzhou");
metadata.put("protocol", "1.3.3");
instance.setMetadata(metadata);
// 指定服務名、分組和實例
namingService.registerInstance(serviceName, group, instance);

System.out.println(namingService.getAllInstances(serviceName));

構建自定義服務


除了實例以外,服務也能夠自定義配置,Nacos 的服務隨着實例的註冊而存在,並隨着全部實例的註銷而消亡。不過目前 Nacos 對於自定義服務的支持不是很友好,除使用 OpenApi 能夠修改服務的屬性外,就只能使用註冊實例時傳入的服務屬性來進行自定義配置。因此在實際的 Dubbo 和 SpringCloud 中,自定義服務通常較少使用,而自定義實例信息則相對經常使用。


Nacos 的服務與 Consul、Eureka 的模型都不一樣,Consul 與 Eureka的服務等同於 Nacos 的實例,每一個實例有一個服務名屬性,服務自己並非一個單獨的模型。Nacos 的設計在我看來更爲合理,其認爲服務自己也是具備數據存儲需求的,例如做用於服務下全部實例的配置、權限控制等。實例的屬性應當繼承自服務的屬性,實例級別能夠覆蓋服務級別。如下是服務的數據結構:

/**
     * Service name
     */
    private String name;

    /**
     * Protect threshold
     */
    private float protectThreshold = 0.0F;

    /**
     * Application name of this service
     */
    private String app;

    /**
     * Service group which is meant to classify services into different sets.
     */
    private String group;

    /**
     * Health check mode.
     */
    private String healthCheckMode;

    private Map<String, String> metadata = new HashMap<String, String>();


在實際使用過程當中,能夠像快速開始章節中介紹的那樣,僅僅使用 ServiceName 標記一個服務。

 

服務隔離:Namespace & Group & Cluster


出於篇幅考慮,這三個概念放到一塊兒介紹。


襄王有意,神女無意。Nacos 提出了這幾種隔離策略,目前看來只有 Namespace 在實際應用中使用較多,而 Group 和 Cluster 並無被當回事。


Cluster 集羣隔離在阿里巴巴內部使用的很是廣泛。一個典型的場景是這個服務下的實例,須要配置多種健康檢查方式,有一些實例使用 TCP 的健康檢查方式,另一些使用 HTTP 的健康檢查方式。另外一個場景是,服務下掛載的機器分屬不一樣的環境,但願可以在某些狀況下將某個環境的流量所有切走,這樣能夠經過集羣隔離,來作到一次性切流。在 Nacos 2.0 中,也在有意的弱化集羣的概念,畢竟開源仍是要面向用戶的,有些東西適合阿里,但不必定適合開源,等再日後演進,集羣這個概念又有可能從新回到你們的視線中了,history will repeat itself。


Group 分組隔離的概念能夠參考 Dubbo 的服務隔離策略,其也有一個分組。支持分組的擴展,用意固然是好的,實際使用上,也的確有一些公司會習慣使用分組來進行隔離。須要注意的一點是:Dubbo 註冊三元組(接口名+分組+版本)時,其中 Dubbo 的分組是包含在 Nacos 的服務名中的,並非映射成了 Nacos 的分組,通常 Nacos 註冊的服務是默認註冊到 DEFAULT_GROUP 分組的。


Namespace 命名空間隔離,我認爲是 Nacos 一個比較好的設計。在實際場景中使用也比較廣泛,通常用於多個環境的隔離,例如 daily,dev,test,uat,prod 等環境的隔離。特別是當環境很是多時,使用命名空間作邏輯隔離是一個比較節約成本的策略。但強烈建議你們僅僅在非線上環境使用 Namespace 進行隔離,例如多套測試環境能夠共享一套 Nacos,而線上環境單獨搭建另外一套 Nacos 集羣,以避免線下測試流量干擾到線上環境。

 

服務發現:推拉模型


上面介紹完了 Nacos 服務發現的 5 大領域模型,最後一節,介紹下如何獲取服務模型。


Nacos 的服務發現,有主動拉取和推送兩種模式,這與通常的服務發現架構相同。如下是拉模型的相關接口:

/**
 * Get all instances of a service
 *
 * @param serviceName name of service
 * @return A list of instance
 * @throws NacosException
 */
List<Instance> getAllInstances(String serviceName) throws NacosException;

/**
 * Get qualified instances of service
 *
 * @param serviceName name of service
 * @param healthy     a flag to indicate returning healthy or unhealthy instances
 * @return A qualified list of instance
 * @throws NacosException
 */
List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException;

/**
 * Select one healthy instance of service using predefined load balance strategy
 *
 * @param serviceName name of service
 * @return qualified instance
 * @throws NacosException
 */
Instance selectOneHealthyInstance(String serviceName) throws NacosException;


Nacos 提供了三個同步拉取服務的方法,一個是查詢全部註冊的實例,一個是隻查詢健康且上線的實例,還有一個是獲取一個健康且上線的實例。通常狀況下,訂閱端並不關心不健康的實例或者權重設爲 0 的實例,可是也不排除一些場景下,有一些運維或者管理的場景須要拿到全部的實例。細心的讀者會注意到上述 Nacos 實例中有一個 weight 字段,即是做用在此處的selectOneHealthyInstance接口上,按照權重返回一個健康的實例。我的認爲這個功能相對雞肋,通常的 RPC 框架都有自身配套的負載均衡策略,不多會由註冊中心 cover,事實上 Dubbo 和 Spring Cloud 都沒有用到 Nacos 的這個接口。


除了主動查詢實例列表,Nacos還提供訂閱模式來感知服務下實例列表的變化,包括服務配置或者實例配置的變化。可使用下面的接口來進行訂閱或者取消訂閱:

/**
 * Subscribe service to receive events of instances alteration
 *
 * @param serviceName name of service
 * @param listener    event listener
 * @throws NacosException
 */
void subscribe(String serviceName, EventListener listener) throws NacosException;
/**
 * Unsubscribe event listener of service
 *
 * @param serviceName name of service
 * @param listener    event listener
 * @throws NacosException
 */
void unsubscribe(String serviceName, EventListener listener) throws NacosException;


在實際的服務發現中,訂閱接口尤其重要。消費者啓動時,通常會同步獲取一次服務信息用於初始化,緊接着訂閱服務,這樣當服務發生上下線時,就能夠感知變化了,從而實現服務發現。

總結


Nacos 爲了更好的實現服務發現,提供一套成熟的服務模型,其中重點須要關注的是 Namespace、Service 和 Instance,得益於這一套服務模型的抽象,以及對推拉模型的支持,Nacos 能夠快速被微服務框架集成。


理解了 Nacos 的服務模型,也有利於咱們瞭解 Nacos 背後的工做原理,從而確保咱們正確地使用 Nacos。但 Nacos 提供的這些模型也不必定全部都須要用上,例如集羣、分組、權重等概念,被實踐證實是相對雞肋的設計,在使用時,也須要根據自身業務特色去評估特性用量,不要盲目地爲了使用技術而去用。

 

點擊閱讀原文

相關文章
相關標籤/搜索