配置中心:
新建配置:
Data-id:
配置中心用来区分和精准定位配置文件的属性
在Nacos-Server中新建配置,其中Data ID它的定义规则是:${prefix}-${spring.profiles.active}.${file-extension}
- prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置。
- spring.profile.active 即为当前环境对应的 profile,可以通过配置项 spring.profile.active 来配置。
- file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
注意:当 spring.profile.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 {prefix}.{file-extension}
这里我创建Data Id 为pxsemic-demo.properties的配置文件,其中Group为指定应用pxsemic-demo,配置文件的格式也相应的选择properties,如图所示
Nacos的默认Group为DEFAULT_GROUP,可以在新建时根据需要进行切换
个人开发使用nacos,建议直接变更group为自定义参数进行指定加载即可
环境切换方案:
一、Data ID方案
命名规则为:${prefix}-${spring.profiles.active}.${file-extension}
通过其中的spring.profile.active属性即可进行多环境下配置文件的读取
新建配置
1、启动Nacos-Server后,创建配置文件Data ID为:pxsemic-demo.properties, 其配置如下:
server.port: 10001
这里是dev环境下默认加载
2、继续创建配置文件Data ID为:pxsemic-demo-test.properties, 其配置如下:
server.port: 8899
这里是test环境
多环境测试
通过Idea启动demo项目,并指定spring.profiles.active,通过不同的环境进行启动
二、Group方案
可以通过指定group进行配置文件的指定切换
只通过Group来进行多环境的区分的方式不推荐使用,因为涉及到了多环境自然就会改变spring.profile.active,而profile一旦生效,配置文件就会依据DataID的规则进行查找。所以Group的方式仅作参考。
Group的合理用法应该是配合namespace进行服务列表和配置列表的隔离和管理,如开发环境多开发人员各自持有一份配置文件。
三、namespace方案
Namespace命名空间进行环境隔离也是官方推荐的一种方式。Namespace的常用场景之一是不同环境的配置的区分隔离,例如:开发测试环境和生产环境的资源(如配置、服务)隔离等。
创建命名空间
创建命名空间dev和prod,不同的命名空间会生成相应的UUID,如下图
可以通过指定spring.cloud.nacos.config.namespace来进行namespace的切换
共享配置(shared-configs)和扩展配置(extension-config)
日常开发中,多个模块可能会有很多共用的配置,比如数据库连接信息,Redis 连接信息,MQ 连接信息,监控配置等等。那么此时,我们就希望可以加载多个配置,多个项目共享同一个配置之类等功能,Nacos Config 也确实支持。
- Nacos在配置路径spring.cloud.nacos.config.extension-config下,允许我们指定⼀个或多个额外配置。
- Nacos在配置路径spring.cloud.nacos.config.shared-configs下,允许我们指定⼀个或多个共享配置。
上述两类配置都⽀持三个属性:data-id、group(默认为字符串DEFAULT_GROUP)、refresh(默认为true)。
1、配置实例
spring.cloud.nacos.config.file-extension=properties spring.cloud.nacos.config.extension-configs[0].data-id=api-conf.properties spring.cloud.nacos.config.extension-configs[0].group=pxsemic-demo spring.cloud.nacos.config.extension-configs[0].refresh=true spring.cloud.nacos.config.extension-configs[1].data-id=comm-plugins.properties spring.cloud.nacos.config.extension-configs[1].group=pxsemic-demo spring.cloud.nacos.config.extension-configs[1].refresh=true spring.profiles.active=test #spring cloud中可以复用的配置 e.g.数据库连接 #spring.cloud.nacos.config.shared-configs[0].data-id= ##共享配置一般不指定group也可以根据应用区分 ##spring.cloud.nacos.config.shared-configs[0].group= #spring.cloud.nacos.config.shared-configs[0].file-extension=properties
参数解析:
- data-id : Data Id
- group:自定义 Data Id 所在的组,不明确配置的话,默认是 DEFAULT_GROUP。
- refresh: 控制该 Data Id 在配置变更时,是否支持应用中可动态刷新, 感知到最新的配置值。默认是不支持的。
注意:这里的Data ID后面是加.properties后缀的,且不需要指定file-extension。
三、共享配置和扩展配置的区
实际上,Nacos中并未对extension-configs和shared-configs的差别进⾏详细阐述。我们从他们的结构,看不出本质差别;除了优先级不同以外,也没有其他差别。那么,Nacos项⽬组为什么要引⼊两个类似的配置呢?我们可以从当初该功能的需求(issue)上找到其原始⽬的。
3.1 Nacos对配置的默认理念
- namespace区分环境:开发环境、测试环境、预发布环境、⽣产环境。
- group区分不同应⽤:同⼀个环境内,不同应⽤的配置,通过group来区分。
3.2 主配置是应⽤专有的配置
因此,主配置应当在dataId上要区分,同时最好还要有group的区分,因为group区分应⽤(虽然dataId上区分了,不⽤设置group也能按应⽤单独加载)。
3.3 要在各应⽤之间共享⼀个配置,请使⽤上⾯的 shared-configs
因此按该理念,shared-configs指定的配置,本来应该是不指定group的,也就是应当归⼊DEFAULT_GROUP这个公共分组。
3.4 如果要在特定范围内(⽐如某个应⽤上)覆盖某个共享dataId上的特定属性,请使⽤ extension-config
⽐如,其他应⽤的数据库url,都是⼀个固定的url,使⽤shared-configs.dataId = mysql的共享配置。但其中有⼀个应⽤demo是特例,需要为该应⽤配置扩展属性来覆盖。
3.5 关于优先级
1、上述两类配置都是数组,对同种配置,数组元素对应的下标越⼤,优先级越⾼。也就是排在后⾯的相同配置,将覆盖排在前⾯的同名配置。
- 同为扩展配置,存在如下优先级关系:extension-configs[3] > extension-configs[2] > extension-configs[1] > extension-configs[0]。
- 同为共享配置,存在如下优先级关系:shared-configs[3] > shared-configs[2] > shared-configs[1] > shared-configs[0]。
2、不同种类配置之间,优先级按顺序如下:主配置 > 扩展配置(extension-configs) > 共享配置(shared-configs)
注册中心:
服务注册中心,是一个给服务提供者注册服务、给服务消费者获取服务信息的地方,一般还提供服务列表查询、心跳检测等功能。注册中心为保证可用性一般集群部署。
注册中心组件我们可选的组件有Eureka、ZK、Nacos,ZK支持CP,Eureka支持AP,Nacos可支持AP也可支持CP。下面分别阐述下。
一、Eureka
Eureka分服务端与客户端,服务端为注册中心,而客户端完成向服务端注册与服务发现。服务端的主要工作有:
- 提供服务注册。提供一个统一存储服务的地方,客户端(服务提供者)将服务注册后服务端提供管理
- 提供注册表。为客户端(服务消费者)提供服务列表的查询,也就是服务发现功能。客户端获取服务列表后一般会在本地进行缓存,以减少与注册中心交互
- 服务剔除。如果客户端在一定时间内未上报心跳,服务端会剔除此服务实例
- 自我保护机制。一段时间内如果客户端可用率低于一定比例则会进入自我保护阶段,防止Eureka客户端本身是可以正常访问的,但是由于网路通信故障等原因,造成Eureka服务端失去于客户端的连接,从而形成的不可用
客户端的功能这里只是简单罗列一下:服务注册、自动刷新缓存获取最新服务列表、服务续约上报心跳、远程调用、服务下线。
注册与发现的工作流程:
- server启动后,client启动后将服务注册到server
- client默认每30s向server发起心跳
- server若 90s没收到client的心跳请求,则统计15分钟内是否有超过85%的比例,如果有进入自我保护状态;如果没有则剔除该client
- client定时调用server接口获取服务列表更新本地缓存
- client远程调用时,先从本地缓存找,如果找到则直接发起调用,如果没有则先向server进行查询后再发起调用
- client关闭时会发http请求到server,server接受请求后将该实例剔除
Eureka是CAP里的AP
从CAP理论看,Eureka是一个AP系统,其优先保证可用性(A)和分区容错性,不保证强一致性,但能做到最终一致性。
- 只要集群中任意一个实例不出现问题,Eureka服务就是可用的;即Eureka Client 在向某个 Eureka Server 注册时,如果发现连接失败,则会自动切换至其它节点;
- Eureka集群中没有主从的概念,各个节点都是平等的,节点间采用Replicate异步的方式来同步数据;
由于Eureka并不强调一致性而侧重可用性,在设计上为提升性能采用了多级缓存的方案。这种设计和mysql的读写分离及JDK里的CopyOnWriteArrayList有点类似,目的是为了使操作不阻塞读操作。
Eureka数据存储机制
Eureka没有采用数据库这类存储介质,它的数据层分数据存储层和缓存层。数据存储层记录注册到 Eureka Server 上的服务信息,缓存层是经过包装后的数据,可以直接在 Eureka Client 调用时返回。
存储层
我们先来看看数据存储层的数据结构,它底层是一个双层HashMap:
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
- 第一层的 ConcurrentHashMap 的 key=spring.application.name 也就是客户端实例注册的应用名;value 为嵌套的 ConcurrentHashMap。
- 第二层嵌套的 ConcurrentHashMap 的 key=instanceId 也就是服务的唯一实例 ID,value 为 Lease 对象,Lease 对象存储着这个实例的所有注册信息,包括 ip 、端口、属性等。
缓存层
接下来我们再来看看缓存层。
Eureka Server 为了提供响应效率,提供了两层的缓存结构,将 Eureka Client 所需要的注册信息,直接存储在缓存结构中。
- 第一层缓存:readOnlyCacheMap,本质上是 ConcurrentHashMap,依赖定时从 readWriteCacheMap 同步数据,默认时间为 30 秒。
readOnlyCacheMap : 是一个 CurrentHashMap 只读缓存,这个主要是为了供客户端获取注册信息时使用,其缓存更新,依赖于定时器的更新,通过和 readWriteCacheMap 的值做对比,如果数据不一致,则以 readWriteCacheMap 的数据为准。
- 第二层缓存:readWriteCacheMap,本质上是 Guava 缓存。
readWriteCacheMap:readWriteCacheMap 的数据主要同步于存储层。当获取缓存时判断缓存中是否没有数据,如果不存在此数据,则通过 CacheLoader 的 load 方法去加载,加载成功之后将数据放入缓存,同时返回数据。
readWriteCacheMap 缓存过期时间,默认为 180 秒,当服务下线、过期、注册、状态变更,都会来清除此缓存中的数据。
存储类型 | 数据结构 | 概述 |
readOnlyCacheMap 一级缓存 | ConcurrentHashMap | 周期更新,默认每30s从二级缓存readWriteCacheMap中同步数据更新;Eureka Client默认从这里获取服务注册信息,可配为直接从readWriteCacheMap获取 |
readWriteCacheMap 二级缓存 | Guava Cache | 服务有变动时实时更新,缓存时间180秒,如果不存在此数据,则通过 CacheLoader 的 load 方法去registry层加载 |
registry 存储层 | 双层HashMap | 服务有变动时实时更新,又名注册表,UI界面从这里获取服务注册信息 |
客户端查询流程
客户端查询服务端数据的流程是怎样的呢,这里暂时不考虑客户端自身的缓存。
Eureka Client 获取全量或者增量的数据时,会先从一级缓存中获取;如果一级缓存中不存在,再从二级缓存中获取;如果二级缓存也不存在,这时候先将存储层的数据同步到缓存中,再从缓存中获取。
通过 Eureka Server 的二层缓存机制,可以非常有效地提升 Eureka Server 的响应时间,通过数据存储层和缓存层的数据切割,根据使用场景来提供不同的数据支持。
客户端缓存
客户端缓存只是简单提一下:
Eureka Client 也同样存在着缓存机制,Eureka Client 启动时会全量拉取服务列表,启动后每隔 30 秒从 Eureka Server 量获取服务列表信息,并保持在本地缓存中。
二、ZK
ZK作为注册中心原理主发依赖于其自身的文件,ZK 的文件结构类似于 Linux 系统的树状结构,注册服务时,即在 ZK 中创建一个唯一的 znode 节点来保存服务的 IP、端口、服务名等信息;发现服务时,遍历树状结构文件找到具体的 znode 节点或者服务相关信息进行远程调用。
注册与发现的工作流程
- ZK 已经启动,服务提供者启动时把服务注册到 ZK 注册中心;
- ZK 注册中心和服务提供者之间建立一个 Socket 长连接,ZK 注册中心定时向每个服务提供者发数据包,如果服务提供者没响应,则剔除该服务提供者实例,把更新后的服务列表发送给所有服务消费者(即通知);
- 服务消费者启动时到 ZK 注册中心获取一份服务列表缓存到本地供以后使用;
- 服务消费者远程调用服务时,先从本地缓存找,如果找到则直接发起服务调用,如果没有则到 ZK 注册中心获取服务列表缓存到本地后再发起服务调用;
- 当其中一个服务提供者宕机或正常关闭时,ZK 注册中心会把该节点剔除,并通知所有服务消费者更新本地缓存
- 当这个服务提供者正常启动后,ZK 注册中心也能感知到,并通知所有服务消费者更新本地缓存。
ZK与Eureka的区别
- 根据 CAP 定律,ZooKeeper 支持 CP,Eureka 支持 AP。因为 ZK 集群中如果有节点宕机则需要选举 leader,选举过程需要 30 至 120 秒,选举过程时集群不可用,牺牲时间来保证数据一致性,因此支持 CP;而 Eureka 每个节点的数据都一致,没有主从节点之分,不需选举,如果其中一个节点宕机则马上切换到另外一个健康的节点上,保证可用性,因此支持 AP。
- 微服务架构当中,可用性比一致性更重要些,Eureka 比 ZooKeeper 更合适,而 ZooKeeper 更适合做分布式协调服务,比如:hadoop 集群。
这里简单提一下京东自研的RPC框架JSF,其注册中心为NameServer,与Eureka各节点一样,它们之间也是平等的、各节点存着全量数据,数据的同步采用的是消息总线的方式,显而易见,NameServer属于AP原则,通过消息总线来保证最终一致性。
三、Nacos
Nacos既能作为注册中心也可以作为配置中心,下面是作为注册中心的流程:
- Nacos启动后,服务提供者启动时将服务注册到Nacos
- 服务提供者定时发送http请求,上报心跳
- Nacos长时间没收到服务提供者的心跳,则剔除该实例
- 服务消费者发现服务支持两种方式,一种是主动请求注册中心获取服务列表(图中左下角不推荐),一种是订阅注册中心的服务并提交一个 Listener,如果注册中心的服务有变更,由 Listener 来通知服务消费者更新本地服务列表(图中右下角)
Nacos里的CP与AP
先看一下Nacos的架构图:
我们可以看到Nacos是集成了Raft与Distro这两种一致性协议的,我们先从Distro入手,看下Nacos的设计机制:
- 平等机制:nacos每个节点是平等的,都可以处理写请求,同时会将数据同步到其它节点。
- 路由转发机制:客户端发送的写请求,如果属于自己则处理,否则路由转发给其它节点
- 本地计机制:每个节点独立处理读请求,及时从本地发出响应,因为每个节点都存有全量数据
- 异步复制机制:节点间通过1s的延迟任务,将数据同步给其它节点
- 健康检查机制:每个节点只存部分数据,定期检查客户端状态保持数据一致性
- 新节点同步机制:服务实例注册到nacos后,通过UDP的方式推送到所有服务实例,让其它服务实例感知到服务列表的变化
Nacos哪些地方用到了AP与CP呢?
- 对于临时服务实例,采用AP来保证注册中心的可用性,Distro协议
- 对于持久化服务实例,采用CP保证各个节点的强一致性,JRaft协议(Nacos对Raft的改造)
- 对于配置中心,无Database 作为存储的情况下,Nacos 节点之间的内存数据为了保持一致,采用 CP
- 对于配置中心,有 Database 作为存储的情况下,Nacos 通过持久化后通知其他节点到数据库拉取数据来保证数据一致性,另外采用读写分离架构来保证高可用,这里应该是AP
Nacos、ZK、Eureka区别
Eureka不能支撑大量服务实例,因为它的每个节点之间会产生大量心跳检查导致并发性能降低;ZK如果出现频繁上下线通知也会导致性能下降;Nacos可以支持大量服务实例而又不丢失性能,服务数量可达到10万级别。
实战:
一、开始:
使用nacos注册中心需要添加依赖如下:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
正常添加上方依赖,nacos控制中心前端才能刷新注册客户端数据
二、客户端:
在SpringBoot启动类上开启@EnableDiscoveryClient注解启用注册到注册中心
配置文件:
spring.cloud.nacos.discovery.server-addr=192.168.25.136:8848 spring.cloud.nacos.discovery.namespace=83b829ce-6ebd-46d9-bcdc-6f9d95f364a3 #group不指定默认为 DEFAULT_GROUP spring.cloud.nacos.discovery.group=pxsemic-demo #根据需求设定实例是否为持久化实例,默认为true spring.cloud.nacos.discovery.ephemeral=false
nacos设置是否持久化实例发生变更且变更应用曾注册至nacos,需要将 \data\protocol\raft目录下注册元数据清除后重启nacos来进行应用的正常启动
根据此配置可以决定nacos使用CP/AP模式
服务注册源码:
Nacos心跳检测(客户端):
心跳检测(服务端):
/** * Create a beat for instance. * * @param request http request * @return detail information of instance * @throws Exception any error during handle */ @CanDistro @PutMapping("/beat") @Secured(action = ActionTypes.WRITE) public ObjectNode beat(HttpServletRequest request) throws Exception { ObjectNode result = JacksonUtils.createEmptyJsonNode(); result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval()); String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY); RsInfo clientBeat = null; if (StringUtils.isNotBlank(beat)) { clientBeat = JacksonUtils.toObj(beat, RsInfo.class); } String clusterName = WebUtils .optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME); String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY); int port = Integer.parseInt(WebUtils.optional(request, "port", "0")); if (clientBeat != null) { if (StringUtils.isNotBlank(clientBeat.getCluster())) { clusterName = clientBeat.getCluster(); } else { // fix #2533 clientBeat.setCluster(clusterName); } ip = clientBeat.getIp(); port = clientBeat.getPort(); } String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}, namespaceId: {}", clientBeat, serviceName, namespaceId); BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder(); builder.setRequest(request); int resultCode = getInstanceOperator() .handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat, builder); result.put(CommonParams.CODE, resultCode); result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip, port, clusterName)); result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled()); return result;
@Override public int handleBeat(String namespaceId, String serviceName, String ip, int port, String cluster, RsInfo clientBeat, BeatInfoInstanceBuilder builder) throws NacosException { // 获取服务 Service{namespace='public', group='DEFAULT_GROUP', // name='service-consumer', ephemeral=true, revision=0} Service service = getService(namespaceId, serviceName, true); // 获取客户端ID信息 192.168.60.1:1020#true String clientId = IpPortBasedClient.getClientId(ip + InternetAddressUtil.IP_PORT_SPLITER + port, true); // 根据客户端ID信息从 clientManager 中获取注册的实例 IpPortBasedClient client = (IpPortBasedClient) clientManager.getClient(clientId); // 如果 client 为空或者发布者 publishers 信息集合里面没有该客户端实例那么就从新注册一个 if (null == client || !client.getAllPublishedService().contains(service)) { // 如果心跳实体信息为空返回 20404,请求未找到 if (null == clientBeat) { return NamingResponseCode.RESOURCE_NOT_FOUND; } // 构建实例信息 Instance instance = new Instance(); // 设置端口信息 instance.setPort(clientBeat.getPort()); // 设置IP信息 instance.setIp(clientBeat.getIp()); //设置权重 instance.setWeight(clientBeat.getWeight()); // 设置元数据 instance.setMetadata(clientBeat.getMetadata()); // 设置集群名称 instance.setClusterName(clientBeat.getCluster()); // 设置服务名称 instance.setServiceName(serviceName); // 设置实例ID信息 instance.setInstanceId(instance.getInstanceId()); // 设置虚拟节点 instance.setEphemeral(clientBeat.isEphemeral()); // 发起注册 registerInstance(namespaceId, serviceName, instance); // 注册完后再一次获取 client 从 clientManager 中, // 因为注册的时候就是把这个客户端放入到 clientManager 中去的 client = (IpPortBasedClient) clientManager.getClient(clientId); } // 注册时候已经将当前的 service 放入到了 ServiceManager.getInstance()的 // ConcurrentHashMap singletonRepository 里面 // 所以在这里面获取了一次所以会有的 if (!ServiceManager.getInstance().containSingleton(service)) { throw new NacosException(NacosException.SERVER_ERROR, "service not found: " + serviceName + "@" + namespaceId); } // 如果心跳信息为空那么就构建一个心跳信息 if (null == clientBeat) { clientBeat = new RsInfo(); clientBeat.setIp(ip); clientBeat.setPort(port); clientBeat.setCluster(cluster); clientBeat.setServiceName(serviceName); } // 创建心跳处理器任务 ClientBeatProcessorV2 beatProcessor = new ClientBeatProcessorV2(namespaceId, clientBeat, client); HealthCheckReactor.scheduleNow(beatProcessor); client.setLastUpdatedTime(); return NamingResponseCode.OK; }
Nacos与eureka区别(事件处理):
@Override public void run() { if (Loggers.EVT_LOG.isDebugEnabled()) { Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString()); } // 获取ID String ip = rsInfo.getIp(); // 端口号 int port = rsInfo.getPort(); // 服务名称 String serviceName = NamingUtils.getServiceName(rsInfo.getServiceName()); // 组名称 String groupName = NamingUtils.getGroupName(rsInfo.getServiceName()); // 获取服务 Service service = Service.newService(namespace, groupName, serviceName, rsInfo.isEphemeral()); // 获取健康检测实例发布信息里面多了几个属性, // 比如 lastHeartBeatTime 和 healthCheckStatus HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client.getInstancePublishInfo(service); System.out.println("健康检测 ====== start =====+" + ip + "_" + port + "+ HealthCheckInstancePublishInfo instance" + instance.toString() + "date: " + new Date() + "====end===="); if (instance.getIp().equals(ip) && instance.getPort() == port) { if (Loggers.EVT_LOG.isDebugEnabled()) { Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString()); } // 更新一下实例的心跳时间 instance.setLastHeartBeatTime(System.currentTimeMillis()); System.out.println("健康检测 ====== start =====+" + ip + "_" + port + "+ HealthCheckInstancePublishInfo instance" + instance.toString() + "date: " + new Date() + "====end===="); // 如果实例是健康的那么就直接执行完任务, // 如果不是健康的那么就设置成健康的,之后发布服务改变事件和客户端改变事件 if (!instance.isHealthy()) { instance.setHealthy(true); Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, "region: {}, msg: client beat ok", rsInfo.getServiceName(), ip, port, rsInfo.getCluster(), UtilsAndCommons.LOCALHOST_SITE); NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service)); NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client)); } }