最近正在負責將公司內部的服務註冊中心做轉移工作,正準備切入到nacos註冊中心,於是花了些時間去整理學習關於使用nacos的一些筆記,並進行一番文檔的輸出。
使用版本 nacos-1.1.4版本
nacos註冊中心的簡單介紹
nacos其實是一款阿里巴巴開源的註冊中心+配置中心,除此之外nacos還有很多強大的功能。
nacos的文件目錄
在nacos安裝包底下,我們可以大概看到nacos包含了以下幾類文件夾,不同的文件底下存儲了不同的數據信息。
最後在target文件夾底下發現了nacos的jar包,因此我好奇地將其打開來一探究竟。
發現內部的jar將nacos的管理臺源代碼給打包了一份。
通過閱讀源碼發現在工程的內部有個lib目錄文件夾,這裏面似乎有自己希望找到的內容
但是目前還是沒法看到內部的源碼,在工程的外部主要就是結合一些源碼的api做了一套可視化界面的控制檯。
這裏面有份jar命名爲:nacos-server.jar。
在startup.sh腳本里面可以看到,nacos有着對應的腳本細節:
這裏對應了java -jar的命令關鍵行進行控制檯的啓動:
配置中心源碼分析
如何進行本地源碼的debug
選擇Nacos工程,然後設置對應的啓動參數:
-Dnacos.standalone=true -Dnacos.home=F:\nacos-local-config
從github下載一份nacos的源碼之後可以看到內部的基礎結構爲:
整體項目裏面對應的工程有好幾個,這裏我們選擇了Config工程這個模塊進行分析,因爲這裏麪包含了nacos控制檯中的拉去服務詳情,查看配置列表等常用接口,有助於我們對工作中常用功能的深入理解。
參照控制檯的接口路徑很快能定位到controller內容
下邊這段接口是對應了查看配置屬性的內容:
http://127.0.0.1:8848/nacos/v1/cs/configs?search=accurate&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTU5MDE1MzAwM30.
TFlSFPTzKd0-2zmmsFjnghV74JfL_tWwo6BOFYAfdjk&dataId&group&pageNo=1&pageSize=10
配置信息查詢接口對應源代碼:
com.alibaba.nacos.config.server.controller.ConfigController#searchConfig --->
com.alibaba.nacos.config.server.service.repository.PersistService#findConfigInfo4Page
在com.alibaba.nacos.config.server.service.repository.EmbeddedStoragePersistServiceImpl#findConfigInfo4Page
裏面看到有關於sql的查詢,深入內部去查看:
單機版本的nacos使用的是 derby 數據庫進行配置存儲的持久化
具體的初始化步驟位於:
com.alibaba.nacos.config.server.service.datasource.LocalDataSourceServiceImpl#init
初始化的時候會鏈接本地數據庫:
jdbc:derby:F:\nacos-local-config\data\derby-data;create=true
單機版本的nacos重啓之後數據並不會丟失,依舊是從本地的存儲文件中讀取數據信息。
derby數據庫的介紹
官網地址:http://db.apache.org/derby/
一款java語言編寫的內嵌於jvm的數據庫,可以支持sql查詢,以及jdbc協議,關於其持久化,大概推斷是存儲到了指定的目錄文件下邊:
服務列表源碼分析
服務註冊原理跟蹤
根據debug會發現,在com.alibaba.nacos.naming.core.ServiceManager 類裏面包含了相關的服務列表存儲信息:
在源碼裏面會發現存儲這些服務列表的本質就是一個ConcurrentHashMap數據結構:
(採用了ConcurrentHashMap來解決併發衝突問題,1.8之前是採用了分段鎖,但是這種方式的鎖粒度過大,所以後邊改爲了採用cas+synchronized的方式來進行加鎖,通過使用無所插入頭結點,如果插入失敗,說明同一時刻有其他線程進行頭插入,再次循壞插入)
private Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
有點奇怪,這個map的數據是存儲在內存裏面的,那麼服務在重啓的時候應該是有進行初始化操作。並且當我們將provider的服務下架之後nacos依舊會有服務信息,在服務關閉之後的三十秒後nacos就查詢不出任何信息了。
藉此推測會有一個調度去專門維護這些數據信息。(猜測是心跳機制)
註冊服務信息到nacos的接口:
/nacos/v1/ns/instance
com.alibaba.nacos.naming.controllers.InstanceController#register
->
com.alibaba.nacos.naming.core.ServiceManager#registerInstance
那麼,假設我們通過啓動dubbo工程,註冊dubbo服務到nacos服務中心之後會看到哪些情況呢?
發現循環調用某些接口
【DistroFilter request url】/nacos/v1/ns/instance/beat
【DistroFilter request url】/nacos/v1/ns/instance/list
通過日誌過濾發現會循環調用這兩個接口,後來查詢文檔估計是某些調度在維護兩端的數據。
客戶端會重複發送心跳包到nacos這邊,這份心跳包包含的數據還挺多的。關於心跳模塊涉及到的類爲:
com.alibaba.nacos.client.naming.beat.BeatReactor
發送的心跳數據基本格式通過BeatInfo格式進行數據傳輸。
關於循環發送心跳數據包的核心是藉助了jdk內部的
ScheduledExecutorService
這個api來實現的,相關模板代碼:
這樣就能實現每個三秒發送一次心跳的功能。
同理,在nacos的服務端和客戶端之間也存在心跳協調的代碼:
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
@Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
//發送心跳包
JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.getIntValue("clientBeatInterval");
boolean lightBeatEnabled = false;
if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.containsKey(CommonParams.CODE)) {
code = result.getIntValue(CommonParams.CODE);
}
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
//如果服務實例消失或者不存在,則註冊一個服務實例
Instance instance = new Instance();
instance.setPort(beatInfo.getPort());
instance.setIp(beatInfo.getIp());
instance.setWeight(beatInfo.getWeight());
instance.setMetadata(beatInfo.getMetadata());
instance.setClusterName(beatInfo.getCluster());
instance.setServiceName(beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ne) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());
}
//每隔5秒重新發送一次心跳包
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
其實我們深入sendbeat函數可以看到最底層就是請求nacos服務端的心跳接口
public JSONObject sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
}
Map<String, String> params = new HashMap<String, String>(8);
String body = StringUtils.EMPTY;
if (!lightBeatEnabled) {
try {
body = "beat=" + URLEncoder.encode(JSON.toJSONString(beatInfo), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new NacosException(NacosException.SERVER_ERROR, "encode beatInfo error", e);
}
}
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
params.put("ip", beatInfo.getIp());
params.put("port", String.valueOf(beatInfo.getPort()));
String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, body, HttpMethod.PUT);
return JSON.parseObject(result);
}
結合springboot的starter如何做服務發現
首先你可能會有思路推斷,加入了一個starter就能生效,估計是有什麼springboot的自動化配置在生效吧。
springboot也有自己的一套spi機制,將spirng.factories配置文件下的類進行實例化操作。
然後根據這些配置的類進行初始化操作。
這裏面有個 NacosServiceRegistryAutoConfiguration 類
參考源代碼:
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration
com.alibaba.cloud.nacos.registry.NacosAutoServiceRegistration
org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration
這個類裏面繼承類spring的事件,ApplicationListener,當spring容器啓動的時候會去觸發onApplicationEvent函數的。
bind(event)-->start --> register--> com.alibaba.nacos.api.naming.NamingService#registerInstance(java.lang.String, java.lang.String, com.alibaba.nacos.api.naming.pojo.Instance)
其實本質就是在這裏調用類nacos的一個遠程方法,關於nacos的遠程方法看看源碼包就瞭解了,這個不難。
註冊的參數
private Instance getNacosInstanceFromRegistration(Registration registration) {
Instance instance = new Instance();
instance.setIp(registration.getHost());
instance.setPort(registration.getPort());
instance.setWeight(nacosDiscoveryProperties.getWeight());
instance.setClusterName(nacosDiscoveryProperties.getClusterName());
instance.setMetadata(registration.getMetadata());
return instance;
}
整體的註冊源碼其實可以濃縮爲下邊這張圖
nacos的集羣化
基本配置條件:
一般集羣需要至少3個節點。我們先準備3臺機器,我這裏選擇了三臺機器作爲集羣搭建基礎:
192.168.11.200:8748
192.168.11.196:8748
192.168.11.126:8748
首先需要有三臺基本的服務器用於運行多個nacos服務端程序。
然後修改conf配置文件:
[root@localhost conf]# ls
application.properties application.properties.example cluster.conf cluster.conf.example.bak nacos-logback.xml nacos-mysql.sql schema.sql
[root@localhost conf]# cat cluster.conf
#it is ip
#example
192.168.164.131:8848
192.168.164.132:8848
192.168.164.133:8848
最後再配置一下數據庫連接部分:
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://10.11.9.243:3306/linhao_test?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=crm
db.password=USszJ497whda
啓動之後日誌會有明顯說明提示nacos的集羣已經部署成功。
如果需要方便操作可以藉助使用nginx來做頁面的轉發。
upstream nacos_server {
server 192.168.11.200:8748;
server 192.168.11.196:8748;
server 192.168.11.126:8748;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://nacos_server;
index index.html index.htm;
}
}
初始化登錄賬號
登錄賬號可以從源碼裏面翻查,然後根據這裏的加密方式在數據庫裏面設置賬號信息:
package com.alibaba.nacos.console.utils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Password encoder tool
*
* @author nacos
*/
public class PasswordEncoderUtil {
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("nacos"));
}
public static Boolean matches(String raw, String encoded) {
return new BCryptPasswordEncoder().matches(raw, encoded);
}
public static String encode(String raw) {
return new BCryptPasswordEncoder().encode(raw);
}
}
下邊這段是nacos初始化時候給定的賬號密碼:
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
經過檢測,不同賬號登錄nacos看到的基礎配置信息大多都是相似的。
nacos裏面的日誌輸出在nacos-logback.xml 配置了日誌輸出位置和等級,如果需要跟蹤或者調整可以進去進行修改。