英文全称Dynamic Naming and Configuration Service,Na为naming/nameServer即注册中心,co为configuration即注册中心,service是指该注册/配置中心都是以服务为核心(相当于Spring Cloud的Eureka+Config)。
服务发现(服务注册中心)
配置管理(服务配置中心)
配置管理:动态管理发布配置,无需重启服务,更好保证服务的可用。(其实这里的无需重启服务是针对某些情况下的,只是为了说明nacos配置发生变更后,服务端和客户端会进行通信,然后获取到变更的信息;但是有些配置信息是在应用服务初始化启动时加载进来的,在服务运行时不再获取配置信息的,如果配置信息发生变更,则需要重启服务。比如:数据库连接池大小初始化时加载。)
1.发布配置——打开nacos控制台,并点击菜单配置管理->配置列表,新增配置。
(yml与properties一样,只是yml更简洁)
2.添加依赖——nacos-config
3.创建配置文件
在实际开发中,通常有多套不同的环境(默认只有public),那么这个时候可以根据指定的环境来创建不同的namespce,例如,开发、测试和生产(还有预生产)三个不同的环境,那么使用一套 nacos 集群可以分别建以下三个不同的 namespace。以此来实现多环境的隔离。
在项目模块中,修改bootstrap.properties添加如下配置:
#指定命名空间
spring.cloud.nacos.config.namespace=命名空间ID
(1)配置文件加载顺序
(2)配置多个开发环境
修改项目bootstrap.properties配置文件,添加一行配置
spring.profiles.active=xxx
服务注册:Nacos注册中心分为server与client,server采用Java编写,为client提供注册发现服务与配置服务。而client可以用多语言实现,client与微服务嵌套在一起,Nacos提供sdk和openApi。(理解zk)
补充:服务注册的策略的是每5秒向nacos server发送一次心跳,心跳带上了服务名,服务ip,服务端口等信息。同时 nacos server也会向client 主动发起健康检查,支持tcp/http检查。如果15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例。
nacos支持两种服务发现方式:
Istio 就是我们上述提到的 service mesh 的一种实现。
Consistency 一致性,在分布式系统中的所有数据备份,在同一时刻是否同样的值;
Availability 可用性,只要收到用户的请求,服务器就必须给出回应;
Partition tolerance 分区容错性。(zk:CP,Eureka:AP,nacos:CP+AP)
Raft协议:通俗的就是“民主投票任期责任制”C。
Distro协议:通俗的就是“服务联产承包责任制”A。
一般来说,如果需要在服务级别编辑或者存储配置信息,那么CP是必须的;如果对数据的一致性要求很高,那么就需要CP模式。 (zk:CP)
一般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。( Eureka:AP)
Distro协议服务端节点发现使用寻址机制来实现服务端节点的管理。之所以使用临时服务的模式,是因为临时数据通常和服务器保持一个session会话, 该会话只要存在,数据就不会丢失 。
①cmdb顾名思义,配置管理数据库。用于管理nacos的各种配置资源(提前配置好的,不需要我们使用者的初始化创建)。它是支撑自动化交付平台(DevOps的持续交付)的核心基础模块;
②nacos-example提供了使用nacos的示例代码,从这里也能看出来,我们使用nacos时真正关心的只有服务器配置、配置中心管理以及注册中心管理(示例代码静态配置写死);
③Istio基于Service Mesh的理念,承担着服务发现、服务通信、负载均衡、限流熔断、监控等等功能。Nacos直接采用Istio,可以让nacos不用关心这些底层逻辑,专注于nacos本身的业务的开发和Istio的服务功能访问即可。
在客户端调用服务订阅接口时,会将客户端的UPD信息(IP和端口)上送到注册中心,注册中心以PushClient对象来进行封装和存储。当注册中心有实例变化时,会发布一个ServiceChangeEvent事件,注册中心监听到这个事件之后,会遍历存储的PushClient,基于UDP协议对客户端进行通知。客户端接收到UDP通知,即可更新本地缓存的实例列表。
程序通过创建一个NamingService ,接着注册了一个服务实例,最后是调用了getAllInstances方法获取某个服务的实例列表。
@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
String clusterString = StringUtils.join(clusters, ",");
//是否订阅,默认是订阅的,也就是subscribe =true
if (subscribe) {
serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
if (null == serviceInfo) {
serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
}
} else {
//不进行订阅
serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
}
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
注:UDP端口是0 ,因为这里是不订阅的,另外,这个UDP是给订阅的接收通知用的。
最后调用reqApi 选择server 发送请求了,请求url是 /nacos/…/instance/list ,请求方法是get。
@Override
public ServiceInfo queryInstancesOfService(String serviceName, String groupName, String clusters, int udpPort,
boolean healthyOnly) throws NacosException {
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, NamingUtils.getGroupedName(serviceName, groupName));
params.put(CLUSTERS_PARAM, clusters);
params.put(UDP_PORT_PARAM, String.valueOf(udpPort));//UDP=0,即为不订阅
params.put(CLIENT_IP_PARAM, NetUtils.localIP());
params.put(HEALTHY_ONLY_PARAM, String.valueOf(healthyOnly));
//生成url
String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
if (StringUtils.isNotEmpty(result)) {
return JacksonUtils.toObj(result, ServiceInfo.class);
}
return new ServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), clusters);
}
InstanceController-list方法
获取对应的service对象,并判断UDP以及客户端等信息是否适合推送。紧接着根据cluster获取这个服务下面对应的实例集合,并进行筛选操作。
Service service = serviceManager.getService(namespaceId, serviceName);//获取服务
long cacheMillis = switchDomain.getDefaultCacheMillis();//默认cache时间
// now try to enable the push
try {//判断这个客户端是否可用,(客户端语言、配置、版本等信息)
if (subscriber.getPort() > 0 && pushService.canEnablePush(subscriber.getAgent())) {
subscriberServiceV1.addClient(namespaceId, serviceName, cluster, subscriber.getAgent(),
new InetSocketAddress(clientIP, subscriber.getPort()), pushDataSource, StringUtils.EMPTY,
StringUtils.EMPTY);//添加客户端
cacheMillis = switchDomain.getPushCacheMillis(serviceName);
}
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP,
subscriber.getPort(), e);
cacheMillis = switchDomain.getDefaultCacheMillis();
}//异常信息
if (service == null) {
if (Loggers.SRV_LOG.isDebugEnabled()) {
Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
}
result.setCacheMillis(cacheMillis);
return result;
}//判断是否有服务
checkIfDisabled(service);//检查服务是否可用
List<com.alibaba.nacos.naming.core.Instance> srvedIps = service
.srvIPs(Arrays.asList(StringUtils.split(cluster, StringUtils.COMMA)));//从服务中找到实例
// filter ips using selector:使用过滤器过滤
if (service.getSelector() != null && StringUtils.isNotBlank(clientIP)) {
srvedIps = selectorManager.select(service.getSelector(), clientIP, srvedIps);
}
用来遍历区分健康(true)的实例与不健康(false)的实例,接着就是判断是否检查,默认是false的,获取服务保护的阈值,默认是0 ; 如果健康的服务实例数量占比小于这个阈值的话,就会将不健康的实例也放到健康的里面,这就是nacos的服务保护机制。问题:可以联想下Eureka的服务保护机制?
//遍历所有实例,区分开健康实例 不健康实例
for (com.alibaba.nacos.naming.core.Instance ip : srvedIps) {
if (!ip.isEnabled()) {
continue;
}// remove disabled instance:
ipMap.get(ip.isHealthy()).add(ip);
total += 1;
}
//保护边界阈值
double threshold = service.getProtectThreshold();
List<Instance> hosts;
if ((float) ipMap.get(Boolean.TRUE).size() / total <= threshold) {
//如果存活的不健康实例阈值小于既定阈值的话,则进行保护加到健康实例中;
Loggers.SRV_LOG.warn("protect threshold reached, return all ips, service: {}", result.getName());
result.setReachProtectionThreshold(true);
hosts = Stream.of(Boolean.TRUE, Boolean.FALSE).map(ipMap::get).flatMap(Collection::stream)
.map(InstanceUtil::deepCopy)
// set all to `healthy` state to protect
.peek(instance -> instance.setHealthy(true)).collect(Collectors.toCollection(LinkedList::new));
} else {
result.setReachProtectionThreshold(false);
hosts = new LinkedList<>(ipMap.get(Boolean.TRUE));
if (!healthOnly) {
hosts.addAll(ipMap.get(Boolean.FALSE));
}
}
ServiceInfoHolder:先是根据clusters与serviceName生成一个订阅key,接着就是调用getServiceInfo0 方法获取本地的一个缓存,然后去serviceInfoMap 这个map中获取,它你可以理解成一个本地的缓存。紧接着就是调用serverProxy 的queryInstancesOfService。
//群组服务名称
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
String key = ServiceInfo.getKey(groupedServiceName, clusters);//根据name 和名称生成key
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
return serviceInfoMap.get(key);//返回key
ServiceInfoUpdateService-scheduleUpdateIfAbsent:如果有了的话直接返回,很显然第一次请求肯定是没有的,然后通过调用了addTask方法添加一个task,然后返回一个future,并缓存到map中去。
public void scheduleUpdateIfAbsent(String serviceName, String groupName, String clusters) {
String serviceKey = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
if (futureMap.get(serviceKey) != null) {//查看是否已经存在
return;
}
synchronized (futureMap) {//同步 根据futureMap加锁进行双重校验
if (futureMap.get(serviceKey) != null) {
return;
}
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, groupName, clusters));//添加任务
futureMap.put(serviceKey, future);//将任务添加到map中
}
}
不难看出,就是将task扔到一个任务调度线程池,然并且延迟1s调度。
这里先是更新一下任务里面维护的这个lastRefTime时间值,接着就是判断如果唤醒监听列表中没有订阅这个服务并且 futureMap(任务集合)里面没有这个的话,就说明被任务被停了, 接着就是计算下延迟时间,然后放到调度线程池中执行,普通情况延迟10s,失败的话就多延迟会,但是不会超过60s。
try {//先检查是否已订阅这个服务 如果map中也没有的话 终止服务
if (!changeNotifier.isSubscribed(groupName, serviceName, clusters) && !futureMap.containsKey(serviceKey)) {
NAMING_LOGGER
.info("update task is stopped, service:{}, clusters:{}", groupedServiceName, clusters);
return;
}
ServiceInfo serviceObj = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
if (serviceObj == null) {//null的话立即更新服务
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
lastRefTime = serviceObj.getLastRefTime();
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
}
lastRefTime = serviceObj.getLastRefTime();
if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
incFailCount();
return;
}//如果hosts为空的话,增加失败次数
// 延迟时间值由服务端决定 为10s
delayTime = serviceObj.getCacheMillis() * DEFAULT_UPDATE_CACHE_TIME_MULTIPLE;
resetFailCount();
} catch (Throwable e) {
incFailCount();
NAMING_LOGGER.warn("[NA] failed to update serviceName: {}", groupedServiceName, e);
} finally {
executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
}
调用了pushService 组件的addClient 方法,这个pushService 组件主要就是用来进行推送的, 比如我们订阅了某个服务,然后这个服务下面的实例信息发生了变化,pushService组件就会通知所有的订阅客户端,将新的数据给客户端推过去。通过PushClient生成一个serviceKey ,然后去clientMap中获取,最后就是将PushClient 转成字符串当作key,去clients这个map中获取。
public void addClient(PushClient client) {
// client is stored by key 'serviceName' because notify event is driven by serviceName change
String serviceKey = UtilsAndCommons.assembleFullServiceName(client.getNamespaceId(), client.getServiceName());
ConcurrentMap<String, PushClient> clients = clientMap.get(serviceKey);
if (clients == null) {
clientMap.putIfAbsent(serviceKey, new ConcurrentHashMap<>(1024));
clients = clientMap.get(serviceKey);
}
PushClient oldClient = clients.get(client.toString());
if (oldClient != null) {
oldClient.refresh();
} else {
PushClient res = clients.putIfAbsent(client.toString(), client);
if (res != null) {
Loggers.PUSH.warn("client: {} already associated with key {}", res.getAddrStr(), res);
}
Loggers.PUSH.debug("client: {} added for serviceName: {}", client.getAddrStr(), client.getServiceName());
}
}
ConfigLongPoll_CITCase的init方法进行初始化创建configService实例,并加载properties配置信息。
// use local config first(优先使用本地配置)
String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}",
worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
cr.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor
.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
cr.setEncryptedDataKey(encryptedDataKey);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
//获取远程配置
ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
cr.setContent(response.getContent());
cr.setEncryptedDataKey(response.getEncryptedDataKey());
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
通过dataId, group, fileExtension加载配置文件信息,并通过RPC方式远程加载配置参数。
初始请求配置完成后,会通过 WorkClient 进行长轮询查询配置。进行初始化使用了两个线程池:
ScheduledExecutorService executorService = Executors
.newScheduledThreadPool(ThreadUtils.getSuitableThreadCount(1), r -> {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker");
t.setDaemon(true);
return t;
});
agent.setExecutor(executorService);
agent.start();
if (securityProxy.isEnabled()) {
securityProxy.login(serverListManager.getServerUrls());
this.executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
securityProxy.login(serverListManager.getServerUrls());
}
}, 0, this.securityInfoRefreshIntervalMills, TimeUnit.MILLISECONDS);
}
在这个方法里面主要是分配任务,给每个task分配一个taskId,后面会去检查本地配置和远程配置,最终调用的是executeConfigListen方法。
//check local listeners consistent.
if (cache.isSyncWithServer()) {
cache.checkListenerMd5();
if (!needAllSync) {
continue;
}
}
//get listen config
if (!cache.isUseLocalConfigInfo()) {
List<CacheData> cacheDatas = listenCachesMap.get(String.valueOf(cache.getTaskId()));
if (cacheDatas == null) {
cacheDatas = new LinkedList<CacheData>();
listenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);
}
cacheDatas.add(cache);
}
@Override
public void startInternal() throws NacosException {
executor.schedule(new Runnable() {
@Override
public void run() {
while (!executor.isShutdown() && !executor.isTerminated()) {
try {
listenExecutebell.poll(5L, TimeUnit.SECONDS);
if (executor.isShutdown() || executor.isTerminated()) {
continue;
}
executeConfigListen();
} catch (Exception e) {
LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
}
}
}
}, 0L, TimeUnit.MILLISECONDS);
}
当服务端收到请求后,会持住当前请求,如果有变化就返回,如果没有变化就等待超时之前返回无变化。
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
String tag = req.getHeader("Vipserver-Tag");
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
// Do nothing but set fix polling timeout.
} else {
long start = System.currentTimeMillis();
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
String ip = RequestUtil.getRemoteIp(req);
// Must be called by http thread, or send response.
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout() is incorrect, Control by oneself
asyncContext.setTimeout(0L);
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
大量的无效日志打印,这些日志的打印会迅速占用完用户的磁盘空间,同时也让有效日志难以查找。
1、access 日志大量打印,access日志不能自动清理和滚动并且不能控制日期以及文件大小限制。这个日志是 Spring Boot 提供的 Tomcat 访问日志打印。
——server.tomcat.accesslog.enabled=false(生产环境磁盘允许的话,不建议删除)
2、服务端业务日志大量打印
#调整naming模块的naming-raft.log的级别为error:
curl -X PUT '$nacos_server:8848/nacos/v1/ns/operator/log?logName=naming-raft&logLevel=error'
#调整config模块的config-dump.log的级别为warn:
curl -X PUT '$nacos_server:8848/nacos/v1/cs/ops/log?logName=config-dump&logLevel=warn‘
3、客户端日志大量打印(心跳日志、轮询日志)
(如果允许的话,可以进行二次开发,对于轮询以及心跳不设置日志信息输出,或者采用超时+时间片的方式进行控制输出)