前言
CMDB在企業(yè)中,一般用于存放與機(jī)器設(shè)備华坦、應(yīng)用、服務(wù)等相關(guān)的元數(shù)據(jù)不从。當(dāng)企業(yè)的機(jī)器及應(yīng)用達(dá)到一定規(guī)模后就需要這樣一個(gè)系統(tǒng)來(lái)存儲(chǔ)和管理它們的元數(shù)據(jù)惜姐。有一些廣泛使用的屬性,例如機(jī)器的IP椿息、主機(jī)名歹袁、機(jī)房坷衍、應(yīng)用、region等条舔,這些數(shù)據(jù)一般會(huì)在機(jī)器部署時(shí)錄入到CMDB枫耳,運(yùn)維或者監(jiān)控平臺(tái)會(huì)使用這些數(shù)據(jù)進(jìn)行展示或者相關(guān)的運(yùn)維操作。
在服務(wù)進(jìn)行多機(jī)房或者多地域部署時(shí)逞刷,跨地域的服務(wù)訪問(wèn)往往延遲較高嘉涌,一個(gè)城市內(nèi)的機(jī)房間的典型網(wǎng)絡(luò)延遲在1ms左右,而跨城市的網(wǎng)絡(luò)延遲夸浅,例如上海到北京大概為30ms仑最。此時(shí)自然而然的一個(gè)想法就是能不能讓服務(wù)消費(fèi)者和服務(wù)提供者進(jìn)行同地域訪問(wèn)。
在Nacos的服務(wù)發(fā)現(xiàn)組件中帆喇,對(duì)接CMDB警医,然后通過(guò)配置的訪問(wèn)規(guī)則,來(lái)實(shí)現(xiàn)服務(wù)消費(fèi)者到服務(wù)提供者的同地域優(yōu)先坯钦。
這實(shí)際上就是一種負(fù)載均衡策略预皇,在Nacos的規(guī)劃中,豐富的服務(wù)端的可配置負(fù)載均衡策略是我們的重要發(fā)展方向婉刀,這與當(dāng)前已有的注冊(cè)中心產(chǎn)品不太一樣吟温。在設(shè)計(jì)如何在開源的場(chǎng)景中,支持就近訪問(wèn)的時(shí)候突颊,與企業(yè)自帶的CMDB集成是考慮的一個(gè)核心問(wèn)題鲁豪。除此之外,也在考慮將Nacos自身擴(kuò)展為一個(gè)實(shí)現(xiàn)基礎(chǔ)功能的CMDB律秃。無(wú)論如何爬橡,都需要能夠從某個(gè)地方獲取IP的環(huán)境信息,這些信息要么是從企業(yè)的CMDB中查詢而來(lái)棒动,要么是從自己內(nèi)置的存儲(chǔ)中查詢而來(lái)糙申。
CMDB插件機(jī)制
先不考慮如何將CMDB的數(shù)據(jù)應(yīng)用于負(fù)載均衡,我們需要首先在Nacos里將CMDB的數(shù)據(jù)通過(guò)某種方法獲取船惨。在實(shí)際使用中柜裸,基本上每個(gè)公司都會(huì)通過(guò)購(gòu)買或者自研搭建自己的CMDB,那么為了能夠解耦各個(gè)企業(yè)的CMDB具體實(shí)現(xiàn)掷漱,一個(gè)比較好的策略是使用SPI機(jī)制粘室,約定CMDB的抽象調(diào)用接口,由各個(gè)企業(yè)添加自己的CMDB插件卜范,無(wú)需任何代碼上的重新構(gòu)建,即可在運(yùn)行狀態(tài)下對(duì)接上企業(yè)的CMDB鹿榜。
Nacos定義了一個(gè)SPI接口海雪,里面包含了與第三方CMDB約定的一些方法锦爵。用戶依照約定實(shí)現(xiàn)了相應(yīng)的SPI接口后,將實(shí)現(xiàn)打成jar包放置到Nacos安裝目錄下奥裸,重啟Nacos即可讓Nacos與CMDB的數(shù)據(jù)打通险掀。整個(gè)流程并不復(fù)雜,但是理解CMDB SPI接口里方法和相應(yīng)概念的含義不太簡(jiǎn)單湾宙。在這里對(duì)CMDB機(jī)制的相關(guān)概念和接口含義做一個(gè)詳細(xì)說(shuō)明樟氢。
CMDB抽象概念
實(shí)體(Entity)
實(shí)體是作為CMDB里數(shù)據(jù)的承載方,在一般的CMDB中侠鳄,一個(gè)實(shí)體可以指一個(gè)IP埠啃、應(yīng)用或者服務(wù)。而這個(gè)實(shí)體會(huì)有很多屬性伟恶,例如IP的機(jī)房信息碴开,服務(wù)的版本信息等。
實(shí)體類型(Entity Type)
我們并不限定實(shí)體一定是IP博秫、應(yīng)用或者服務(wù)潦牛,這取決于實(shí)際的業(yè)務(wù)場(chǎng)景。Nacos有計(jì)劃在未來(lái)支持不同的實(shí)體類型挡育,不過(guò)就目前來(lái)說(shuō)巴碗,服務(wù)發(fā)現(xiàn)需要的實(shí)體類型是IP。
標(biāo)簽(Label)
Label是我們抽象出的Entity屬性即寒,Label定義為一個(gè)描述Entity屬性的K-V鍵值對(duì)橡淆。Label的key和value的取值范圍一般都是預(yù)先定義好的,當(dāng)需要對(duì)Label進(jìn)行變更蒿叠,如增加新的key或者value時(shí)明垢,需要調(diào)用單獨(dú)的接口并觸發(fā)相應(yīng)的事件。一個(gè)常見(jiàn)的Label的例子是IP的機(jī)房信息市咽,我們認(rèn)為機(jī)房(site)是Label的key痊银,而機(jī)房的集合(site1, site2, site3)是Label的value,這個(gè)Label的定義就是:site: {site1, site2, site3}施绎。
實(shí)體事件(Entity Event)
實(shí)體的標(biāo)簽的變更事件溯革。當(dāng)CMDB的實(shí)體屬性發(fā)生變化,需要有一個(gè)事件機(jī)制來(lái)通知所有訂閱方谷醉。為了保證實(shí)體事件攜帶的變更信息是最新準(zhǔn)確的致稀,這個(gè)事件里只會(huì)包含變更的實(shí)體的標(biāo)識(shí)以及變更事件的類型,不會(huì)包含變更的標(biāo)簽的值俱尼。
CMDB約定接口
在設(shè)計(jì)與CMDB交互接口的時(shí)候抖单,我們參考了內(nèi)部對(duì)CMDB的訪問(wèn)接口,并與若干個(gè)外部客戶進(jìn)行了討論。我們最終確定了以下要求第三方CMDB插件必須實(shí)現(xiàn)的接口:
獲取標(biāo)簽列表
Set<String> getLabelNames();
這個(gè)方法將返回CMDB中需要被Nacos識(shí)別的標(biāo)簽名集合矛绘,CMDB插件可以按需決定返回什么標(biāo)簽個(gè)Nacos耍休。不在這個(gè)集合的標(biāo)簽將會(huì)被Nacos忽略,即使這個(gè)標(biāo)簽出現(xiàn)在實(shí)體的屬性里货矮。我們?cè)试S這個(gè)集合會(huì)在運(yùn)行時(shí)動(dòng)態(tài)變化羊精,Nacos會(huì)定時(shí)去調(diào)用這個(gè)接口刷新標(biāo)簽集合。
獲取實(shí)體類型
Set<String> getEntityTypes();
獲取CMDB里的實(shí)體的類型集合囚玫,不在這個(gè)集合的實(shí)體類型會(huì)被Nacos忽略喧锦。服務(wù)發(fā)現(xiàn)模塊目前需要的實(shí)體類型是ip,如果想要通過(guò)打通CMDB數(shù)據(jù)來(lái)實(shí)現(xiàn)服務(wù)的高級(jí)負(fù)載均衡抓督,請(qǐng)務(wù)必在返回集合里包含“ip”燃少。
獲取標(biāo)簽詳情
Label getLabel(String labelName);
獲取標(biāo)簽的詳細(xì)信息。返回的Label類里包含標(biāo)簽的名字和標(biāo)簽值的集合本昏。如果某個(gè)實(shí)體的這個(gè)標(biāo)簽的值不在標(biāo)簽值集合里供汛,將會(huì)被視為無(wú)效。
查詢實(shí)體的標(biāo)簽值
String getLabelValue(String entityName, String entityType, String labelName);
Map<String, String> getLabelValues(String entityName, String entityType);
這里包含兩個(gè)方法涌穆,一個(gè)是獲取實(shí)體某一個(gè)標(biāo)簽名對(duì)應(yīng)的值怔昨,一個(gè)是獲取實(shí)體所有標(biāo)簽的鍵值對(duì)。參數(shù)里包含實(shí)體的值和實(shí)體的類型宿稀。注意趁舀,這個(gè)方法并不會(huì)在每次在Nacos內(nèi)部觸發(fā)查詢時(shí)去調(diào)用,Nacos內(nèi)部有一個(gè)CMDB數(shù)據(jù)的緩存祝沸,只有當(dāng)這個(gè)緩存失效或者不存在時(shí)矮烹,才會(huì)去訪問(wèn)CMDB插件查詢數(shù)據(jù)。為了讓CMDB插件的實(shí)現(xiàn)盡量簡(jiǎn)單罩锐,我們?cè)贜acos內(nèi)部實(shí)現(xiàn)了相應(yīng)的緩存和刷新邏輯奉狈。
查詢實(shí)體
Map<String, Map<String, Entity>> getAllEntities();
Entity getEntity(String entityName, String entityType);
查詢實(shí)體包含兩個(gè)方法:查詢所有實(shí)體和查詢單個(gè)實(shí)體。查詢單個(gè)實(shí)體目前其實(shí)就是查詢這個(gè)實(shí)體的所有標(biāo)簽涩惑,不過(guò)我們將這個(gè)方法與獲取所有標(biāo)簽的方法區(qū)分開來(lái)仁期,因?yàn)椴樵儐蝹€(gè)實(shí)體方法后面可能會(huì)進(jìn)行擴(kuò)展,比查詢所有標(biāo)簽獲取的信息要更多竭恬。
查詢所有實(shí)體則是一次性將CMDB的所有數(shù)據(jù)拉取過(guò)來(lái)跛蛋,該方法可能會(huì)比較消耗性能,無(wú)論是對(duì)于Nacos還是CMDB痊硕。Nacos內(nèi)部調(diào)用該方法的策略是通過(guò)可配置的定時(shí)任務(wù)周期來(lái)定時(shí)拉取所有數(shù)據(jù)赊级,在實(shí)現(xiàn)該CMDB插件時(shí),也請(qǐng)關(guān)注CMDB服務(wù)本身的性能岔绸,采取合適的策略理逊。
查詢實(shí)體事件
List<EntityEvent> getEntityEvents(long timestamp);
這個(gè)方法意在獲取最近一段時(shí)間內(nèi)實(shí)體的變更消息橡伞,增量的去拉取變更的實(shí)體。因?yàn)镹acos不會(huì)實(shí)時(shí)去訪問(wèn)CMDB插件查詢實(shí)體挡鞍,需要這個(gè)拉取事件的方法來(lái)獲取實(shí)體的更新骑歹。參數(shù)里的timestamp為上一次拉取事件的時(shí)間预烙,CMDB插件可以選擇使用或者忽略這個(gè)參數(shù)墨微。
CMDB插件開發(fā)流程
參考:https://github.com/nacos-group/nacos-examples,這里已經(jīng)給出了一個(gè)示例plugin實(shí)現(xiàn)扁掸。
具體步驟如下:
- 1翘县、新建一個(gè)maven工程,引入依賴nacos-api:
<dependencies>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-api</artifactId>
<version>1.4.2</version>
</dependency>
</dependencies>
- 2谴分、引入打包插件:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
- 3锈麸、定義實(shí)現(xiàn)類,繼承com.alibaba.nacos.api.cmdb.CmdbService牺蹄,并實(shí)現(xiàn)相關(guān)方法忘伞。
package com.yibo.cmdb;
import com.alibaba.nacos.api.cmdb.pojo.*;
import com.alibaba.nacos.api.cmdb.spi.CmdbService;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author: huangyibo
* @Date: 2021/7/31 23:59
* @Description:
*/
public class CmdbServiceImpl implements CmdbService {
private int index = 1;
private Map<String,Map<String, Entity>> entityMap = new ConcurrentHashMap<String, Map<String, Entity>>();
private Map<String,Label> labelMap = new ConcurrentHashMap<String, Label>();
/**
* CMDB_DATA_TABLE:模擬cmdb數(shù)據(jù)庫(kù)數(shù)據(jù)
*/
public CmdbServiceImpl() {
Label label = new Label();
label.setName("cluster");
Set<String> values = new HashSet<String>();
values.add("shenzhen");
values.add("shanghai");
label.setValues(values);
labelMap.put(label.getName(),label);
entityMap.put(PreservedEntityTypes.ip.name(),new HashMap<String, Entity>());
//深圳cluster的ip
Entity entityShen = new Entity();
entityShen.setName("192.168.50.253");
entityShen.setType(PreservedEntityTypes.ip.name());
Map<String,String> lableShen = new HashMap<String, String>();
lableShen.put("cluster","shenzhen");
entityShen.setLabels(lableShen);
entityMap.get(PreservedEntityTypes.ip.name()).put(entityShen.getName(),entityShen);
//上海cluster的ip
Entity entityShang = new Entity();
entityShang.setName("192.168.50.175");
entityShang.setType(PreservedEntityTypes.ip.name());
Map<String,String> lableShang = new HashMap<String, String>();
lableShang.put("cluster","shanghai");
entityShang.setLabels(lableShang);
entityMap.get(PreservedEntityTypes.ip.name()).put(entityShang.getName(),entityShang);
}
public Set<String> getLabelNames() {
Set<String> labelNames = new HashSet<>();
labelMap.forEach((key,value) -> {
labelNames.add(key);
});
return labelNames;
}
public Set<String> getEntityTypes() {
Set<String> types = new HashSet<>();
entityMap.forEach((key,value) -> {
types.add(key);
});
return types;
}
public Label getLabel(String labelName) {
return labelMap.get(labelName);
}
public String getLabelValue(String entityName, String entityType, String labelName) {
return entityMap.get(entityType).get(entityName).getLabels().get(labelName);
}
public Map<String, String> getLabelValues(String entityName, String entityType) {
return entityMap.get(entityType).get(entityName).getLabels();
}
public Map<String, Map<String, Entity>> getAllEntities() {
return entityMap;
}
public List<EntityEvent> getEntityEvents(long timestamp) {
Entity entity = new Entity();
entity.setName("1.1.1.1");
entity.setType(PreservedEntityTypes.ip.name());
Map<String, String> labels = new HashMap<>();
labels.put("cluster1", "x1" + ((index % 3) + 1));
labels.put("cluster2", "x2" + ((index++ % 3) + 1));
entity.setLabels(labels);
entityMap.get(PreservedEntityTypes.ip.name()).put("1.1.1.1", entity);
EntityEvent entityEvent = new EntityEvent();
entityEvent.setEntityName("1.1.1.1");
entityEvent.setEntityType("PreservedEntityTypes.ip.name()");
entityEvent.setType(EntityEventType.ENTITY_ADD_OR_UPDATE);
List<EntityEvent> list = new ArrayList<>();
list.add(entityEvent);
return list;
}
public Entity getEntity(String entityName, String entityType) {
return entityMap.get(entityType).get(entityName);
}
}
- 4、在src/main/resource/目錄下新建目錄:META-INF/services沙兰。
- 且在該目錄下新建
com.alibaba.nacos.api.cmdb.CmdbService
文件 - 將CmdbService接口的實(shí)現(xiàn)類全名寫入該文件
- 且在該目錄下新建
com.yibo.cmdb.CmdbServiceImpl
- 5氓奈、代碼自測(cè)完成后,執(zhí)行命令進(jìn)行打包:
mvn package assembly:single -Dmaven.test.skip=true
- 6鼎天、將target目錄下的包含依賴的jar包上傳到nacos CMDB插件目錄:
{nacos.home}/plugins/cmdb
- 7舀奶、在nacos的application.properties里打開加載插件開關(guān):
nacos.cmdb.loadDataAtStart=true
重啟nacos Server,即可加載到您實(shí)現(xiàn)的nacos-cmdb插件獲取您的CMDB數(shù)據(jù)斋射。
使用Selector實(shí)現(xiàn)同機(jī)房?jī)?yōu)先訪問(wèn)
在拿到CMDB的數(shù)據(jù)之后育勺,就可以運(yùn)用CMDB數(shù)據(jù)的強(qiáng)大威力來(lái)實(shí)現(xiàn)多種靈活的負(fù)載均衡策略了,下面舉例來(lái)說(shuō)明如何使用CMDB數(shù)據(jù)和Selector來(lái)實(shí)現(xiàn)就近訪問(wèn)罗岖。
假設(shè)目前Nacos已經(jīng)通過(guò)CMDB拿到了一些IP的機(jī)房信息涧至,且它們對(duì)應(yīng)的標(biāo)簽信息如下:
192.168.50.253
cluster: shenzhen
192.168.50.175
cluster: shanghai
服務(wù)詳情
然后我們修改服務(wù)的“服務(wù)路由類型”,并配置為基于同cluster優(yōu)先的服務(wù)路由:
編輯服務(wù)路由類型
這里我們將服務(wù)路由類型選擇為標(biāo)簽桑包,然后輸入標(biāo)簽的表達(dá)式:
CONSUMER.label.cluster= PROVIDER.label.cluster
這個(gè)表達(dá)式的格式和我們抽象的Selector機(jī)制有關(guān)南蓬,在這里您需要記住的就是,任何一個(gè)如下格式的表達(dá)式:
CONSUMER.label.labelName = PROVIDER.label.labelName
將能夠?qū)崿F(xiàn)基于同labelName優(yōu)先的負(fù)載均衡策略捡多。
以上蓖康,便是我們?cè)贜acos中通過(guò)打通CMDB,實(shí)現(xiàn)就近訪問(wèn)的實(shí)踐垒手。