實現服務治理、服務動態擴容,以及呼叫時能有負載均衡的效果。
如果我們將服務提供方的ip地址設定在服務消費方的組態檔中,當服務提供方範例上線下線,消費方都需要重啟服務,導致二者耦合度過高。註冊中心就是在二者之間加一層,實現解耦合。
健康檢查和服務摘除:主動的檢查服務健康情況,對於宕機的服務將其摘除服務列表
Naming Service
:註冊中心,提供服務註冊,登出,管理Config Service
:設定中心,Nacos 設定中心為服務設定提供了編輯、儲存、分發、變更管理、歷史版本管理等功能,並且支援在範例執行中,更改設定。OpenAPI
:nacos對外暴露的介面,Provider App(服務提供者)就是呼叫這裡的介面,實現將自己註冊到nacos,Consumer App(服務消費者)也是使用這裡的介面拉去設定中心中的服務提供者的資訊。我們使用nacos作為註冊中心,只需要下載nacos提供的jar包並執行啟動nacos服務,然後在服務提供者,消費者中引入spring-cloud-starter-alibaba-nacos-discovery
,並設定spring.cloud.nacos.discovery.server-addr=nacos服務啟動的地址
,即可在nacos視覺化介面看到:
那麼服務是如何註冊到nacos的暱?
當我們服務引入spring-cloud-starter-alibaba-nacos-discovery
,便可以實現自動進行註冊,這是因為在spring.facotries
中自動裝配了NacosServiceRegistryAutoConfiguration
SpringBoot原始碼學習1——SpringBoot自動裝配原始碼解析+Spring如何處理設定類的
點進NacosServiceRegistryAutoConfiguration
原始碼中,發現它注入了一下三個類
ServiceInstance
表示的是服務發現中的一個範例
這個介面定義了類似於getHost
,getIp
獲取註冊範例host,ip等方法,是springcloud定義的規範介面
Registration
一個標記介面,ServiceRegistry<R>
這裡面的R泛型就是Registration
是springcloud定義的規範介面
ServiceRegistry
服務註冊,定義如何向註冊中心進行註冊,和取消註冊
這個介面定義了register服務註冊
,deregister服務取消註冊
等方法,入參是Registration
。它是springcloud定義的規範介面。
spring cloud 定義了諸多規範介面,無論是服務註冊,還是負載均衡,讓其他中介軟體實現
NacosServiceRegistry
nacos服務註冊介面,實現了ServiceRegistry
,定義瞭如何註冊,如何取消註冊,維護服務狀態等。NacosRegistration
是 Registration
的實現類,象徵著一個Nacos註冊中心的服務,也就是我們自己寫的springboot服務
AutoServiceRegistration
一個標記介面,表示當前類是一個自動服務註冊類AbstractAutoServiceRegistration
實現了ApplicationListener
,監聽WebServerInitializedEvent web服務初始化結束事件
,在ApplicationListener#onApplicationEvent
中進行服務註冊NacosAutoServiceRegistration
使用NacosServiceRegistry
將NacosRegistration
的註冊到nacos註冊中心一通分析之後,可以看到NacosAutoServiceRegistration
是最核心的類,它負責監聽事件,呼叫NacosServiceRegistry
,將服務註冊到註冊中心。
AbstractAutoServiceRegistration
監聽事件進行註冊此類是SpringCloud提供的模板類,讓市面上眾多註冊中心中介軟體實現它,快速接入SpringCloud生態。
AbstractAutoServiceRegistration
想響應WebServerInitializedEvent
,那麼WebServerInitializedEvent
是哪兒發出的暱?
在WebServerStartStopLifecycle#start
方法
WebServerStartStopLifecycle
實現了Lifecycle
,在spring容器重新整理結束的時候,會使用LifecycleProcessor
呼叫所以Lifecycle#start
,從而傳送ServletWebServerInitializedEvent(WebServerInitializedEvent子類)
推播事件
Reactive的springboot上下文則是由WebServerStartStopLifecycle推播ReactiveWebServerInitializedEvent事件,原理一樣,如下圖
AbstractAutoServiceRegistration
在響應事件後,會呼叫bind方法,進而呼叫register
進行服務註冊,這裡就會呼叫到NacosAutoServiceRegistration#register
那麼到底如何進行服務註冊?
可以看到直接呼叫NacosServiceRegistry#register(NacosRegistration)
進行服務註冊
可以看到這裡使用NamingService
將Instance
進行註冊
NamingService
,nacos
框架中的類,負責服務註冊和取消註冊Instance
,nacos
框架中的類,定義一個服務,記錄ip,埠等資訊可以看到nacos有自己一套東西,脫離springcloud,也可以使用,這就是鬆耦合
下面我們看下NamingService是如何進行服務註冊的
如果是臨時範例,會使用ScheduledThreadPoolExecutor
,每5秒傳送一次心跳,傳送心跳即請求nacos註冊中心/instance/beat
介面
然後呼叫NamingProxy
進行服務註冊
最終底層通過Http請求的方式,請求nacos服務的/nacos/v1/ns/instance
。
上面一通分析,我們直到了springboot服務是如何啟動的時候,自動進行服務註冊的,如何進行服務註冊的,但是nacos伺服器端是如何響應註冊請求的的暱
從請求中拿範例資訊
主要包含上述這些欄位。
ServiceManager#registerInstance
服務註冊的邏輯主要在addInstance
方法中
首先根據待註冊服務的namespaceId命名中間id
,serviceName服務名稱
,ephemeral是否臨時服務
構建出一個key,由於我們是一個臨時範例,key最終為com.alibaba.nacos.naming.iplist.ephemeral + namespaceId ## + serviceName
然後呼叫ConsistencyService一致性協定服務#put
進行註冊,這裡和Nacos支援AP,CP架構有關,後續我們分析到一致性協定再補充。
這裡會呼叫到DelegateConsistencyServiceImpl(一致性協定門面)
他會根據key中的是臨時範例,還是非臨時範例,選擇協定,最終選擇到DistroConsistencyServiceImpl
,繼續呼叫put
方法
可以看到DistroConsistencyServiceImpl(Distro一致性協定服務)
會同步到nacos叢集中的其他範例,這部分我們後續分析,我們重點看下onPut,看看nacos服務到底如何註冊。
至此服務註冊請求結束了,只是將註冊請求資訊包裝成了任務加入到Notifier
的任務佇列中。
在看怎麼處理阻塞佇列中的任務前,我們看下nacos的登入檔結構
對應ServiceManager中的serviceMap屬性
/**
* key 是名稱空間
* value 是 分組名稱和Service服務的map
*
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
//Service結構如下
//叢集和叢集物件組成map
private Map<String, Cluster> clusterMap = new HashMap<>();
//Cluster 中的屬性記錄所有範例Instance的集合
上面分析到最終服務註冊請求被包裝放到Notifier
的任務佇列中。我們看下任務佇列的任務在哪裡被拿出來消費。
Notifier
實現了Runnable,在DistroConsistencyServiceImpl
中使用@PostConstruct
將它提交到了排程執行緒池中。
也就是說會有一個單執行緒呼叫Notifier#run
後續會呼叫到Service#onChange
,其updateIPs
方法會更新範例的ip地址
// 這裡 instances 裡面就包含了新範例物件
// ephemeral 為 ture,臨時範例
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
// clusterMap 對應叢集的Map
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
// 把叢集名字都放入到ipMap裡面,value是一個空的ArrayList
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
// 遍歷全部的Instance,這個List<Instance> 包含了之前已經註冊過的範例,和新註冊的範例物件
// 這裡的主要作用就是把相同叢集下的 instance 進行分類
for (Instance instance : instances) {
try {
// 判斷使用者端傳過來的是 Instance 中,是否有設定 ClusterName
if (StringUtils.isEmpty(instance.getClusterName())) {
// 如果沒有,就給ClusterName賦值為 DEFAULT
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
// 判斷之前是否存在對應的 ClusterName,如果沒有則需要建立新的 Cluster 物件
if (!clusterMap.containsKey(instance.getClusterName())) {
// 建立新的叢集物件
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
// 放入到叢集 clusterMap 當中
getClusterMap().put(instance.getClusterName(), cluster);
}
// 通過叢集名字,從 ipMap 裡面取
List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
// 只有是新建立叢集名字,這裡才會為空,之前老的叢集名字,在方法一開始裡面都 value 賦值了 new ArrayList物件
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}
// 把對應叢集下的instance,新增進去
clusterIPs.add(instance);
} catch (Exception e) {
}
}
// 分好類之後,針對每一個 ClusterName ,寫入到登入檔中
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
// entryIPs 已經是根據ClusterName分好組的範例列表
List<Instance> entryIPs = entry.getValue();
// 對每一個 Cluster 物件修改登入檔 ->updateIps
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
}
針對每一個叢集分別進行Cluster#updateIps
public void updateIps(List<Instance> ips, boolean ephemeral) {
// 先判斷是否是臨時範例
// ephemeralInstances 臨時範例
// persistentInstances 持久化範例
// 把對應資料先拿出來,放入到 新建立的 toUpdateInstances 集合中
Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
// 先把老的範例列表複製一份 , 先複製一份新的
//寫時複製,先複製一份
HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
for (Instance ip : toUpdateInstances) {
oldIpMap.put(ip.getDatumKey(), ip);
}
//省略了同步到其他nacos服務的程式碼。。。
// 最後把傳入進來的範例列表,重新初始化一個 HaseSet,賦值給toUpdateInstances
toUpdateInstances = new HashSet<>(ips);
// 判斷是否是臨時範例
if (ephemeral) {
// 直接把之前的範例列表替換成新的
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}