作者:島風
前言
按照目前市場上的主流使用場景,Nacos 被分成了兩塊功能:服務注冊發現(Naming)和配置中心(Config)。在之前的文章中我介紹了 Nacos 配置中心的實現原理,今天這篇文章所介紹的內容則是與 Nacos 服務注冊發現功能相關,來聊一聊 Nacos 的服務模型。
說到服務模型,其實需要區分視角,一是用戶視角,一個內核視角。即 Nacos 用戶視角看到的服務模型和 Nacos 開發者設計的內核模型可能是完全不一樣的,而今天的文章,是站在用戶視角觀察的,旨在探討 Nacos 服務發現的最佳實踐。
服務模型介紹
一般我在聊注冊中心時,都會以 Zookeeper 為引子,這也是很多人最熟悉的注冊中心。但如果你真的寫過或看過使用 Zookeeper 作為注冊中心的適配代碼,會發現並不是那么容易,再加上注冊中心涉及到的一致性原理,這就導致很多人對注冊中心的第一印象是:這個東西好難! 但歸根到底是因為 Zookeeper 根本不是專門為注冊中心而設計的,其提供的 API 以及內核設計,並沒有預留出「服務模型」的概念,這就使得開發者需要自行設計一個模型,去填補 Zookeeper 和服務發現之間的鴻溝。
微服務架構逐漸深入人心后,Nacos、Consul、Eureka 等注冊中心組件進入大眾的視線。可以發現,這些”真正“的注冊中心都有各自的「服務模型」,在使用上也更加的方便。
為什么要有「服務模型」?理論上,一個基礎組件可以被塑造成任意的模樣,如果你願意,一個數據庫也可以被設計成注冊中心,這並不是”誇張“的修辭手法,在阿里還真有人這么干過。那么代價是什么呢?一定會在業務發展到一定體量后遇到瓶頸,一定會遇到某些極端 case 導致其無法正常工作,一定會導致其擴展性低下。正如剛學習數據結構時,同學們常見的一個疑問一樣:為什么棧只能先進后出。不是所有開發都是中間件專家,所以 Nacos 設計了自己的「服務模型」,這雖然限制了使用者的”想象力“,但保障了使用者在正確地使用 Nacos。
花了一定的篇幅介紹 Nacos 為什么需要設計「服務模型」,再來看看實際的 Nacos 模型是個啥,其實沒那么玄乎,一張圖就能表達清楚:
與 Consul、Eureka 設計有別,Nacos 服務發現使用的領域模型是命名空間-分組-服務-集群-實例這樣的多層結構。服務 Service 和實例 Instance 是核心模型,命名空間 Namespace 、分組 Group、集群 Cluster 則是在不同粒度實現了服務的隔離。
為了更好的理解兩個核心模型:Service 和 Instance,我們以 Dubbo 和 SpringCloud 這兩個已經適配了 Nacos 注冊中心的微服務框架為例,介紹下二者是如何映射對應模型的。
- 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 =