推荐 最新
后端小马

微服务架构:Nacos本地缓存 PK 微服务优雅下线

前言在上篇文章《微服务:剖析一下源码,Nacos的健康检查竟如此简单》中讲了当微服务突然挂掉的解放方案:调整健康检查周期和故障请求重试。朋友看了文章,建议再聊聊正常关闭服务时如何让微服务优雅下线。为什么说是优雅下线?我们知道在分布式应用中为了满足CAP原则中的A(可用性),像Nacos、Eureka等注册中心的客户端都会进行实例列表的缓存。当正常关闭应用时,虽然可以主动调用注册中心进行注销,但这些客户端缓存的实例列表还是要等一段时间才会失效。上述情况就有可能导致服务请求到已经被关闭的实例上,虽然通过重试机制可以解决掉这个问题,但这种解决方案会出现重试,在一定程度上会导致用户侧请求变慢。这时就需要进行优雅的下线操作了。下面我们先从通常关闭进程的几种方式聊起。方式一:基于kill命令Spring Cloud本身对关闭服务是有支持的,当通过kill命令关闭进程时会主动调用Shutdown hook来进行当前实例的注销。使用方式:kill Java进程ID这种方式是借助Spring Cloud的Shutdown hook机制(本质是Spring Boot提供,Spring Cloud服务发现功能进行具体注销实现),在关闭服务之前会对Nacos、Eureka等服务进行注销。但这个注销只是告诉了注册中心,客户端的缓存可能需要等几秒(Nacos默认为5秒)之后才能感知到。这种Shutdown hook机制不仅适用于kill命令,还适用于程序正常退出、使用System.exit()、终端使用Ctrl + C等。但不适用于kill -9 这样强制关闭或服务器宕机等场景。这种方案虽然比直接挂掉要等15秒缩短了时间,相对好一些,但本质上并没有解决客户端缓存的问题,不建议使用。方式二:基于/shutdown端点在Spring Boot中,提供了/shutdown端点,基于此也可以实现优雅停机,但本质上与第一种方式相同,都是基于Shutdown hook来实现的。在处理完基于Shutdown hook的逻辑之后,也会进行服务的关闭,但同样面临客户端缓存的问题,因此,也不推荐使用。这种方式首先需要在项目中引入对应的依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>然后在项目中配置开启/shutdown端点:management: endpoint: shutdown: enabled: true endpoints: web: exposure: include: shutdown然后停服时请求对应的端点,这里采用curl命令示例:curl -X http://实例服务地址/actuator/shutdown方式三:基于/pause端点Spring Boot同样提供了/pause端点(Spring Boot Actuator提供),通过/pause端点,可以将/health为UP状态的实例修改为Down状态。基本操作就是在配置文件中进行pause端点的开启:management: endpoint: # 启用pause端点 pause: enabled: true # pause端点在某些版本下依赖restart端点 restart: enabled: true endpoints: web: exposure: include: pause,restart然后发送curl命令,即可进行服务的终止。注意这里需要采用POST请求。关于/pause端点的使用,不同的版本差异很大。笔者在使用Spring Boot 2.4.2.RELEASE版本时发现根本无法生效,查了Spring Boot和Spring Cloud项目的Issues发现,这个问题从2.3.1.RELEASE就存在。目前看应该是在最新版本中Web Server的管理改为SmartLifecycle的原因,而Spring Cloud对此貌似放弃了支持(有待考察),最新的版本调用/pause端点无任何反应。鉴于上述版本变动过大的原因,不建议使用/pause端点进行微服务的下线操作,但使用/pause端点的整个思路还是值得借鉴的。基本思路就是:当调用/pause端点之后,微服务的状态会从UP变为DOWN,而服务本身还是可以正常提供服务。当微服务被标记为DOWN状态之后,会从注册中心摘除,等待一段时间(比如5秒),当Nacos客户端缓存的实例列表更新了,再进行停服处理。这个思路的核心就是:先将微服务的流量切换掉,然后再关闭或重新发布。这就解决了正常发布时客户端缓存实例列表的问题。基于上述思路,其实自己也可以实现相应的功能,比如提供一个Controller,先调用该Controller中的方法将当前实例从Nacos中注销,然后等待5秒,再通过脚本或其他方式将服务关闭掉。方式四:基于/service-registry端点方式三中提到的方案如果Spring Cloud能够直接支持,那就更好了。这不,Spring Cloud提供了/service-registry端点。但从名字就可以知道专门针对服务注册实现的一个端点。在配置文件中开启/service-registry端点:management: endpoints: web: exposure: include: service-registry base-path: /actuator endpoint: serviceregistry: enabled: true访问http://localhost:8081/actuator 端点可以查看到开启了如下端点:{ "_links": { "self": { "href": "http://localhost:8081/actuator", "templated": false }, "serviceregistry": { "href": "http://localhost:8081/actuator/serviceregistry", "templated": false } } }通过curl命令来进行服务状态的修改:curl -X "POST" "http://localhost:8081/actuator/serviceregistry?status=DOWN" -H "Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8"执行上述命令之前,查看Nacos对应实例状态为:可以看到实例详情中的按钮为“下线”也就是说目前处于UP状态。当执行完上述curl命令之后,实例详情中的按钮为“上线”,说明实例已经下线了。上述命令就相当于我们在Nacos管理后台手动的操作了实例的上下线。当然,上述情况是基于Spring Cloud和Nacos的模式实现的,本质上Spring Cloud是定义了一个规范,比如所有的注册中心都需要实现ServiceRegistry接口,同时基于ServiceRegistry这个抽象还定义了通用的Endpoint:@Endpoint(id = "serviceregistry") public class ServiceRegistryEndpoint { private final ServiceRegistry serviceRegistry; private Registration registration; public ServiceRegistryEndpoint(ServiceRegistry<?> serviceRegistry) { this.serviceRegistry = serviceRegistry; } public void setRegistration(Registration registration) { this.registration = registration; } @WriteOperation public ResponseEntity<?> setStatus(String status) { Assert.notNull(status, "status may not by null"); if (this.registration == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found"); } this.serviceRegistry.setStatus(this.registration, status); return ResponseEntity.ok().build(); } @ReadOperation public ResponseEntity getStatus() { if (this.registration == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found"); } return ResponseEntity.ok().body(this.serviceRegistry.getStatus(this.registration)); } }我们上面调用的Endpoint便是通过上面代码实现的。所以不仅Nacos,只要基于Spring Cloud集成的注册中心,本质上都是支持这种方式的服务下线的。小结很多项目都逐步在进行微服务化改造,但一旦因为微服务系统,将面临着更复杂的情况。本篇文章重点基于Nacos在Spring Cloud体系中优雅下线来为大家剖析了一个微服务实战中常见的问题及解决方案。

0
0
0
浏览量2011
后端小马

Spring Cloud集成Nacos服务发现源码解析?翻了三套源码,保质保鲜!

前言前面文章我们介绍了Nacos的功能及设计架构,这篇文章就以Nacos提供的服务注册功能为主线,来讲解Nacos的客户端是如何在Spring Cloud进行集成和实现的。本会配合源码分析、流程图整理、核心API解析等维度来让大家深入浅出、系统的来学习。Spring Boot的自动注册故事要从头Spring Boot的自动注入开始。很多朋友大概都了解过Spring Boot的自动配置功能,而Spring Cloud又是基于Spring Boot框架的。因此,在学习Nacos注册业务之前,我们先来回顾一下Spring Boot的自动配置原理,这也是学习的入口。Spring Boot通过@EnableAutoConfiguration注解,将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。上述过程是通过@Import(AutoConfigurationImportSelector.class)导入的配置功能,AutoConfigurationImportSelector中的方法getCandidateConfigurations,得到待配置的class的类名集合,即所有需要进行自动配置的(xxxAutoConfiguration)类,这些类配置于META-INF/spring.factories文件中。最后,根据这些全限定名类上的注解,如:OnClassCondition、OnBeanCondition、OnWebApplicationCondition条件化的决定要不要自动配置。了解了Spring Boot的基本配置之后,我们来看看Nacos对应的自动配置在哪里。Spring Cloud中的Nacos自动配置查看Spring Cloud的项目依赖,本人引入依赖对应的jar包为spring-cloud-starter-alibaba-nacos-discovery-2021.1.jar;对应的pom依赖为:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>查看jar包中META-INF/spring.factories文件的内容:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\ com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\ com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\ com.alibaba.cloud.nacos.NacosServiceAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration可以看到EnableAutoConfiguration类对应了一系列的Nacos自动配置类。其中NacosServiceRegistryAutoConfiguration是用来封装实例化Nacos注册流程所需组件的,装载了对三个对象NacosServiceRegistry、NacosRegistration、NacosAutoServiceRegistration,这三个对象整体都是为了Nacos服务注册使用的。@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties @ConditionalOnNacosDiscoveryEnabled @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) @AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class, AutoServiceRegistrationAutoConfiguration.class, NacosDiscoveryAutoConfiguration.class }) public class NacosServiceRegistryAutoConfiguration { @Bean public NacosServiceRegistry nacosServiceRegistry( NacosDiscoveryProperties nacosDiscoveryProperties) { return new NacosServiceRegistry(nacosDiscoveryProperties); } @Bean @ConditionalOnBean(AutoServiceRegistrationProperties.class) public NacosRegistration nacosRegistration( ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers, NacosDiscoveryProperties nacosDiscoveryProperties, ApplicationContext context) { return new NacosRegistration(registrationCustomizers.getIfAvailable(), nacosDiscoveryProperties, context); } @Bean @ConditionalOnBean(AutoServiceRegistrationProperties.class) public NacosAutoServiceRegistration nacosAutoServiceRegistration( NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) { return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration); } }其中NacosServiceRegistry封装的就是注册流程,它继承自ServiceRegistry:public class NacosServiceRegistry implements ServiceRegistry<Registration> {...}查看该类源码,可以看到该类中实现了服务注册、注销、关闭、设置状态、获取状态5个功能。我们要追踪的服务注册功能,便是通过它提供的register方法来实现的。至此,我们可以梳理一下Nacos客户端在Spring Cloud中集成并实例化的处理流程。 Spring Cloud的ServiceRegistry接口上面提到NacosServiceRegistry集成自ServiceRegistry,那么ServiceRegistry又是何方神圣呢?ServiceRegistry接口是Spring Cloud的类,来看一下ServiceRegistry接口的定义:public interface ServiceRegistry<R extends Registration> { void register(R registration); void deregister(R registration); void close(); void setStatus(R registration, String status); <T> T getStatus(R registration); }可以看出ServiceRegistry接口中定义了服务注册、注销、关闭、设置状态、获取状态五个接口。如果看其他服务发现框架对Spring Cloud进行集成时,基本上都是实现的这个接口。也就是说,ServiceRegistry是Spring Cloud提供的一个服务发现框架集成的规范。对应的框架安装规范实现对应的功能即可进行集成。 我们可以看到Eureka、Zookeeper、Consul在Spring Cloud中集成也都是实现了该接口,同时,如果你需要自定义服务发现功能,也可以通过实现该接口来达到目的。NacosServiceRegistry服务注册实现暂且不关注其他的辅助类,直接来看NacosServiceRegistry#register方法,它提供了服务注册的核心业务逻辑实现。我们把该类的辅助判断去掉,直接展示最核心的代码如下:@Override public void register(Registration registration) { // 获取NamingService NamingService namingService = namingService(); String serviceId = registration.getServiceId(); String group = nacosDiscoveryProperties.getGroup(); // 构造实例,封装信息来源于配置属性 Instance instance = getNacosInstanceFromRegistration(registration); // 将实例进行注册 namingService.registerInstance(serviceId, group, instance); }上述代码中NamingService已经属于Nacos Client项目提供的API支持了。关于Nacos Client的API流程查看,可直接查看Nacos对应的源码,NamingService#registerInstance方法对应的流程图整理如下: 上述流程图还可以继续细化,这个我们在后续章节中进行专门讲解,这里大家知道大概的调用流程即可。Spring Cloud服务注册链路下面我们来梳理一下Spring Cloud是如何进行服务注册的,其中流程的前三分之二部分几乎所有的服务注册框架都是一样的流程,只有最后一部分进行实例注册时会调用具体的框架来进行实现。直接来看整个调用的链路图: 图中不同的颜色代表这不同的框架,灰色表示业务代码,浅绿色表示SpringBoot框架,深绿色表示Spring框架,浅橙色表示SpringCloud框架,其中这一部分也包含了依赖的Nacos组件部分,最后浅紫色代表着Nacos Client的包。核心流程分以下几步:第一步,SpringBoot在启动main方法时调用到Spring的核心方法refresh;第二步,在Spring中实例化了WebServerStartStopLifecycle对象。重点说一下WebServerStartStopLifecycle对象,它的start方法被调用时会发布一个ServletWebServerInitializedEvent事件类,这个事件类继承自WebServerInitializedEvent。后面用来处理服务注册的类AbstractAutoServiceRegistration同时也是一个监听器,专门用来监听WebServerInitializedEvent事件。第三步,AbstractApplicationContext的finishRefresh中会间接调用DefaultLifecycleProcessor的startBeans方法,进而调用了WebServerStartStopLifecycle的start方法。就像上面说的,触发了ServletWebServerInitializedEvent事件的发布。第四步,AbstractAutoServiceRegistration监听到对应的事件,然后基于Spring Cloud定义的ServiceRegistry接口进行服务注册。上面的描述省略了一些部分细节,但整个流程基本上就是SpringBoot在启动时发布了一个事件,Spring Cloud监听到对应的事件,然后进行服务的注册。小结为了这篇文章,肝了好几天。Spring Cloud源码、Spring Boot源码、Nacos源码都翻了个遍。最终为大家分享了Nacos或者说是Spring Cloud中服务发现的实现机制及流程。之所以写这篇文章,也是想倡导大家更多的走进源码,而不是仅仅在使用。你学到了吗?

0
0
0
浏览量2011
后端小马

微服务:剖析一下源码,Nacos的健康检查竟如此简单

前言前面我们多次提到Nacos的健康检查,比如《微服务之:服务挂的太干脆,Nacos还没反应过来,怎么办?》一文中还对健康检查进行了自定义调优。那么,Nacos的健康检查和心跳机制到底是如何实现的呢?在项目实践中是否又可以参考Nacos的健康检查机制,运用于其他地方呢?这篇文章,就带大家来揭开Nacos健康检查机制的面纱。Nacos的健康检查Nacos中临时实例基于心跳上报方式维持活性,基本的健康检查流程基本如下:Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。原理很简单,关于代码层的实现,下面来就逐步来进行解析。客户端的心跳实例基于心跳上报的形式来维持活性,当然就离不开心跳功能的实现了。这里以客户端心跳实现为基准来进行分析。Spring Cloud提供了一个标准接口ServiceRegistry,Nacos对应的实现类为NacosServiceRegistry。Spring Cloud项目启动时会实例化NacosServiceRegistry,并调用它的register方法来进行实例的注册。@Override public void register(Registration registration) { // ... NamingService namingService = namingService(); String serviceId = registration.getServiceId(); String group = nacosDiscoveryProperties.getGroup(); Instance instance = getNacosInstanceFromRegistration(registration); try { namingService.registerInstance(serviceId, group, instance); log.info("nacos registry, {} {} {}:{} register finished", group, serviceId, instance.getIp(), instance.getPort()); }catch (Exception e) { // ... } }在该方法中有两处需要注意,第一处是构建Instance的getNacosInstanceFromRegistration方法,该方法内会设置Instance的元数据(metadata),通过源元数据可以配置服务器端健康检查的参数。比如,在Spring Cloud中配置的如下参数,都可以通过元数据项在服务注册时传递给Nacos的服务端。spring: application: name: user-service-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848 heart-beat-interval: 5000 heart-beat-timeout: 15000 ip-delete-timeout: 30000其中的heart-beat-interval、heart-beat-timeout、ip-delete-timeout这些健康检查的参数,都是基于元数据上报上去的。register方法的第二处就是调用NamingService#registerInstance来进行实例的注册。NamingService是由Nacos的客户端提供,也就是说Nacos客户端的心跳本身是由Nacos生态提供的。在registerInstance方法中最终会调用到下面的方法:@Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { NamingUtils.checkInstanceIsLegal(instance); String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName); if (instance.isEphemeral()) { BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance); beatReactor.addBeatInfo(groupedServiceName, beatInfo); } serverProxy.registerService(groupedServiceName, groupName, instance); }其中BeatInfo#addBeatInfo便是进行心跳处理的入口。当然,前提条件是当前的实例需要是临时(瞬时)实例。对应的方法实现如下:public void addBeatInfo(String serviceName, BeatInfo beatInfo) { NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo); String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()); BeatInfo existBeat = null; //fix #1733 if ((existBeat = dom2Beat.remove(key)) != null) { existBeat.setStopped(true); } dom2Beat.put(key, beatInfo); executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS); MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size()); }在倒数第二行可以看到,客户端是通过定时任务来处理心跳的,具体的心跳请求由BeatTask完成。定时任务的执行频次,封装在BeatInfo,回退往上看,会发现BeatInfo的Period来源于Instance#getInstanceHeartBeatInterval()。该方法具体实现如下:public long getInstanceHeartBeatInterval() { return this.getMetaDataByKeyWithDefault("preserved.heart.beat.interval", Constants.DEFAULT_HEART_BEAT_INTERVAL); }可以看出定时任务的执行间隔就是配置的metadata中的数据preserved.heart.beat.interval,与上面提到配置heart-beat-interval本质是一回事,默认是5秒。BeatTask类具体实现如下: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 { JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled); long interval = result.get("clientBeatInterval").asLong(); boolean lightBeatEnabled = false; if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) { lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean(); } BeatReactor.this.lightBeatEnabled = lightBeatEnabled; if (interval > 0) { nextTime = interval; } int code = NamingResponseCode.OK; if (result.has(CommonParams.CODE)) { code = result.get(CommonParams.CODE).asInt(); } 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 ex) { NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg()); } executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS); } }在run方法中通过NamingProxy#sendBeat完成了心跳请求的发送,而在run方法的最后,再次开启了一个定时任务,这样周期性的进行心跳请求。NamingProxy#sendBeat方法实现如下:public JsonNode 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); Map<String, String> bodyMap = new HashMap<String, String>(2); if (!lightBeatEnabled) { bodyMap.put("beat", JacksonUtils.toJson(beatInfo)); } 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.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT); return JacksonUtils.toObj(result); }实际上,就是调用了Nacos服务端提供的"/nacos/v1/ns/instance/beat"服务。在客户端的常量类Constants中定义了心跳相关的默认参数:static { DEFAULT_HEART_BEAT_TIMEOUT = TimeUnit.SECONDS.toMillis(15L); DEFAULT_IP_DELETE_TIMEOUT = TimeUnit.SECONDS.toMillis(30L); DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5L); }这样就呼应了最开始说的Nacos健康检查机制的几个时间维度。服务端接收心跳分析客户端的过程中已经可以看出请求的是/nacos/v1/ns/instance/beat这个服务。Nacos服务端是在Naming项目中的InstanceController中实现的。@CanDistro @PutMapping("/beat") @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) public ObjectNode beat(HttpServletRequest request) throws Exception { // ... Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port); if (instance == null) { // ... instance = new Instance(); instance.setPort(clientBeat.getPort()); instance.setIp(clientBeat.getIp()); instance.setWeight(clientBeat.getWeight()); instance.setMetadata(clientBeat.getMetadata()); instance.setClusterName(clusterName); instance.setServiceName(serviceName); instance.setInstanceId(instance.getInstanceId()); instance.setEphemeral(clientBeat.isEphemeral()); serviceManager.registerInstance(namespaceId, serviceName, instance); } Service service = serviceManager.getService(namespaceId, serviceName); // ... service.processClientBeat(clientBeat); // ... return result; }服务端在接收到请求时,主要做了两件事:第一,如果发送心跳的实例不存在,则将其进行注册;第二,调用其Service的processClientBeat方法进行心跳处理。processClientBeat方法实现如下:public void processClientBeat(final RsInfo rsInfo) { ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor(); clientBeatProcessor.setService(this); clientBeatProcessor.setRsInfo(rsInfo); HealthCheckReactor.scheduleNow(clientBeatProcessor); }ClientBeatProcessor同样是一个实现了Runnable的Task,通过HealthCheckReactor定义的scheduleNow方法进行立即执行。scheduleNow方法实现:public static ScheduledFuture<?> scheduleNow(Runnable task) { return GlobalExecutor.scheduleNamingHealth(task, 0, TimeUnit.MILLISECONDS); }再来看看ClientBeatProcessor中对具体任务的实现:@Override public void run() { Service service = this.service; // logging String ip = rsInfo.getIp(); String clusterName = rsInfo.getCluster(); int port = rsInfo.getPort(); Cluster cluster = service.getClusterMap().get(clusterName); List<Instance> instances = cluster.allIPs(true); for (Instance instance : instances) { if (instance.getIp().equals(ip) && instance.getPort() == port) { // logging instance.setLastBeat(System.currentTimeMillis()); if (!instance.isMarked()) { if (!instance.isHealthy()) { instance.setHealthy(true); // logging getPushService().serviceChanged(service); } } } } }在run方法中先检查了发送心跳的实例和IP是否一致,如果一致则更新最后一次心跳时间。同时,如果该实例之前未被标记且处于不健康状态,则将其改为健康状态,并将变动通过PushService提供事件机制进行发布。事件是由Spring的ApplicationContext进行发布,事件为ServiceChangeEvent。通过上述心跳操作,Nacos服务端的实例的健康状态和最后心跳时间已经被刷新。那么,如果没有收到心跳时,服务器端又是如何判断呢?服务端心跳检查客户端发起心跳,服务器端来检查客户端的心跳是否正常,或者说对应的实例中的心跳更新时间是否正常。服务器端心跳的触发是在服务实例注册时触发的,同样在InstanceController中,register注册实现如下:@CanDistro @PostMapping @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) public String register(HttpServletRequest request) throws Exception { // ... final Instance instance = parseInstance(request); serviceManager.registerInstance(namespaceId, serviceName, instance); return "ok"; }ServiceManager#registerInstance实现代码如下:public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException { createEmptyService(namespaceId, serviceName, instance.isEphemeral()); // ... }心跳相关实现在第一次创建空的Service中实现,最终会调到如下方法:public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException { Service service = getService(namespaceId, serviceName); if (service == null) { Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName); service = new Service(); service.setName(serviceName); service.setNamespaceId(namespaceId); service.setGroupName(NamingUtils.getGroupName(serviceName)); // now validate the service. if failed, exception will be thrown service.setLastModifiedMillis(System.currentTimeMillis()); service.recalculateChecksum(); if (cluster != null) { cluster.setService(service); service.getClusterMap().put(cluster.getName(), cluster); } service.validate(); putServiceAndInit(service); if (!local) { addOrReplaceService(service); } } }在putServiceAndInit方法中对Service进行初始化:private void putServiceAndInit(Service service) throws NacosException { putService(service); service = getService(service.getNamespaceId(), service.getName()); service.init(); consistencyService .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service); consistencyService .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service); Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson()); }service.init()方法实现:public void init() { HealthCheckReactor.scheduleCheck(clientBeatCheckTask); for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) { entry.getValue().setService(this); entry.getValue().init(); } }HealthCheckReactor#scheduleCheck方法实现:public static void scheduleCheck(ClientBeatCheckTask task) { futureMap.computeIfAbsent(task.taskKey(), k -> GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS)); }延迟5秒执行,每5秒检查一次。在init方法的第一行便可以看到执行健康检查的Task,具体Task是由ClientBeatCheckTask来实现,对应的run方法核心代码如下:@Override public void run() { // ... List<Instance> instances = service.allIPs(true); // first set health status of instances: for (Instance instance : instances) { if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) { if (!instance.isMarked()) { if (instance.isHealthy()) { instance.setHealthy(false); // logging... getPushService().serviceChanged(service); ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance)); } } } } if (!getGlobalConfig().isExpireInstance()) { return; } // then remove obsolete instances: for (Instance instance : instances) { if (instance.isMarked()) { continue; } if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) { // delete instance deleteIp(instance); } } }在第一个for循环中,先判断当前时间与上次心跳时间的间隔是否大于超时时间。如果实例已经超时,且为被标记,且健康状态为健康,则将健康状态设置为不健康,同时发布状态变化的事件。在第二个for循环中,如果实例已经被标记则跳出循环。如果未标记,同时当前时间与上次心跳时间的间隔大于删除IP时间,则将对应的实例删除。小结通过本文的源码分析,我们从Spring Cloud开始,追踪到Nacos Client中的心跳时间,再追踪到Nacos服务端接收心跳的实现和检查实例是否健康的实现。想必通过整个源码的梳理,你已经对整个Nacos心跳的实现有所了解。

0
0
0
浏览量2011
后端小马

学习Nacos?咱先把服务搞起来,实战教程~

前言前面已经写不少Nacos相关的文章了,比如《Spring Cloud集成Nacos服务发现源码解析?翻了三套源码,保质保鲜!》,而且目前也计划写一个Spring Cloud的技术解析专栏,一个技术框架一个技术框架的为大家拆解分析原理和实现。既然拿Nacos作为一个开始,那么我们这篇文章就来补充一下Nacos Server的部署以及Nacos Client的调用,直观的了解一下Nacos都包含了什么功能。这是使用Nacos的基础,也是后续进行深度剖析的依据。强烈建议一起学习一下。Nacos Server的部署关于Nacos Server的部署,官方手册中已经进行了很详细的说明,对应链接地址(https://nacos.io/zh-cn/docs/deployment.html )。其他方式的部署我们暂且不说,我们重点说明通过源码的形式进行构建和部署,这也是学习的最好方式。Nacos部署的基本环境要求:JDK 1.8+,Maven 3.2.x+,准备好即可。从Github上下载源码:// 下载源码 git clone https://github.com/alibaba/nacos.git // 进入源码目录 cd nacos/ // 执行编译打包操作 mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U // 查看生成的jar包 ls -al distribution/target/ // 进入到打包好的文件目录,后续可执行启动 cd distribution/target/nacos-server-$version/nacos/bin经过上述命令进入到bin目录下,通常有不同环境的启动脚本:shutdown.cmd shutdown.sh startup.cmd startup.sh执行对应环境的脚本即可进行启动,参数standalone代表着单机模式运行:// Linux/Unix/Mac sh startup.sh -m standalone // ubuntu bash startup.sh -m standalone // Windows startup.cmd -m standalone上述操作适用于打包和部署,也适用于本地启动服务,但如果是学源码,则可以直接执行console(nacos-console)中的main方法(Nacos类)即可。执行main方法启动,默认也是集群模式,可通过JVM参数来指定单机启动:-Dnacos.standalone=true如果为了方便,也可以直接在启动类的源码中直接添加该参数:@SpringBootApplication(scanBasePackages = "com.alibaba.nacos") @ServletComponentScan @EnableScheduling public class Nacos { public static void main(String[] args) { // 通过环境变量的形式设置单机启动 System.setProperty(Constants.STANDALONE_MODE_PROPERTY_NAME, "true"); SpringApplication.run(Nacos.class, args); } }经过上述步骤,我们已经可以启动一个Nacos Server了,后面就来看如何使用。Nacos管理后台在启动Nacos Server时,控制台会打印如下日志信息: ,--. ,--.'| ,--,: : | Nacos ,`--.'`| ' : ,---. Running in stand alone mode, All function modules | : : | | ' ,'\ .--.--. Port: 8848 : | \ | : ,--.--. ,---. / / | / / ' Pid: 47395 | : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.1.190:8848/nacos/index.html ' ' ;. ;.--. .-. | / / '' | |: :| : ;_ | | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io ' : | ; .' ," .--.; |' ; :__| : | `----. \ | | '`--' / / ,. |' | '.'|\ \ / / /`--' / ' : | ; : .' \ : : `----' '--'. / ; |.' | , .-./\ \ / `--'---' '---' `--`---' `----'通过上面的日志,可以看出启动的模式为“stand alone mode”,端口为8848,管理后台为:http://192.168.1.190:8848/nacos/index.html 。这里我们直接访问本机服务:http://127.0.0.1:8848/nacos/index.html 。 默认情况下,用户和密码都是nacos。登录成功之后,大家可以随便点点,其中包含配置管理、服务管理、权限管理、命名空间、集群管理几个板块。 可以看到默认的命名空间为public,默认的用户为nacos。此时执行一条curl命令,进行模拟服务注册:curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080'执行之后,在此查看管理后台,会发现服务列表中已经添加了一条记录。 点击这条记录的详情,可以看到更多信息。 由于我们上面是通过一个命令注册的服务,这个服务并不存在,Nacos Server会定时检查服务的健康状态。你会发现,过了一会儿这个服务就不见了。这便是Nacos Server发现服务“挂掉”了,将其移除了。 其他的类似操作,大家可以尝试着通过curl命令或客户端工具进行尝试,同时配合管理后台看对应的数据。服务发现命令:curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=nacos.naming.serviceName'发布配置命令:curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld"获取配置命令:curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test"Spring Cloud集成Nacos最后,我们再以一个简单的实例来讲Nacos集成到Spring Cloud项目当中,看是否能够将服务注册成功。关于Spring Cloud之间服务的调用,我们后面文章再专门讲解。首先,新建一个Spring Boot项目,引入Spring Cloud和Spring Cloud Alibaba的依赖,完整的pom.xml文件如下:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.springcloud</groupId> <artifactId>spring-cloud-alibaba-nacos</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-cloud-alibaba-nacos</name> <description>springcloud alibaba nacos集成</description> <properties> <java.version>1.8</java.version> <spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.0</spring-cloud.version> <cloud-alibaba.version>2021.1</cloud-alibaba.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 其中dependencyManagement中定义了Spring Cloud和Spring Cloud Alibaba的依赖的版本信息。这里需要注意的是Spring Cloud和Spring Boot的版本之间是有限制的。这个可以在https://spring.io/projects/spring-cloud中进行查看。然后,在yml中配置nacos注册中心服务器地址:spring: application: name: nacos-spring-cloud-learn cloud: nacos: discovery: server-addr: 127.0.0.1:8848此时,启动服务,查看Nacos Server的控制台,会发现,像前面直接执行curl命令的效果一样,成功的将服务注册到Nacos中了。小结本文通过给大家讲解如何部署Nacos服务、如何集成到SpringCloud,为后面更进一步学习SpringCloud做好准备。其中也涉及到一些实践经验和坑。

0
0
0
浏览量2015
后端小马

一个实例,轻松演示Spring Cloud集成Nacos实例

前言学习一个技术框架,最快速的手段就是将其集成到项目中,体验一下它的功能。在这个过程中,你还踩到很多坑。而排坑的过程,又是一次能力的提升。前面我们写了一些列Nacos的文章,经过《学习Nacos?咱先把服务搞起来,实战教程》的介绍,我们已经可以把Nacos Server给启动起来了。这篇文章,我们就来学习一下如何将Nacos集成到Spring Cloud项目中,同时实例演示一下,基于Nacos的微服务之间的两种调用形式。集成与版本为了演示这个案例,大家首先要将Nacos Server跑起来。同时会构建两个微服务:服务提供方(Provider)和服务消费方(Consumer)。然后,通过两个服务之间的调用及配合查看Nacos Server中的注册信息来进行验证。我们知道,Nacos隶属于Spring Cloud Alibaba系列中的组件。所以,在进行集成之前,有一件事一定要注意,那就是要确保Spring Cloud、Spring Boot、Spring Cloud Alibaba版本的一致。不然发生一些莫名其妙的异常。关于版本信息可以在https://spring.io/projects/spring-cloud中进行查看。这里采用Spring Boot的版本为2.4.2,Spring Cloud采用2020.0.0、Spring Cloud Alibaba采用2021.1。如你采用其他版本,一定确保对照关系。Nacos服务提供者依赖配置创建项目Spring Boot的项目spring-cloud-alibaba-nacos-provider1,在pom文件中添加定义依赖的版本限制:<properties> <java.version>1.8</java.version> <spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.0</spring-cloud.version> <cloud-alibaba.version>2021.1</cloud-alibaba.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后添加依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>其中actuator为健康检查依赖包,nacos-discovery为服务发现的依赖包。配置文件提供者添加配置(application.yml)server: port: 8081 spring: application: name: user-service-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848其中Nacos Server的地址和端口号默认是127.0.0.1:8848。name用来指定此服务的名称,消费者可通过注册的这个名称来进行请求。业务代码在编写业务代码之前,我们先来看一下提供者的启动类:// 版本不同,低版本需要明确使用@EnableDiscoveryClient注解 //@EnableDiscoveryClient @SpringBootApplication public class NacosProviderApplication { public static void main(String[] args) { SpringApplication.run(NacosProviderApplication.class, args); } }注意上面的注释部分,此版本已经不需要@EnableDiscoveryClient注解了,而较低的版本需要添加对应的注解。下面新建一个UserController服务:@RestController @RequestMapping("/user") public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @GetMapping("/getUserById") public UserDetail getUserById(Integer userId) { logger.info("查询用户信息,userId={}", userId); UserDetail detail = new UserDetail(); if (userId == 1) { detail.setUserId(1); detail.setUsername("Tom"); } else { detail.setUserId(2); detail.setUsername("Other"); } return detail; } }其中用到的实体类UserDetail为:public class UserDetail { private Integer userId; private String username; // 省略getter/setter }然后启动服务,查看Nacos Server,会发现已经成功注册。Nacos服务消费者消费者的创建与提供者基本一致,唯一不同的是调用相关的功能。创建项目创建Spring Boot项目spring-cloud-alibaba-nacos-consumer1,pom中的依赖与提供者基本一致,但还需要在它的基础上增加两个依赖:<!-- consumer需要额外添加负载均衡的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency> <!-- 基于Feign框架进行调用--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>其中loadbalancer是用来做服务调用负载均衡的,如果不添加此依赖,在调用的过程中会出现如下一次:java.net.UnknownHostException: user-provider at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:196) ~[na:1.8.0_271] at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394) ~[na:1.8.0_271] at java.net.Socket.connect(Socket.java:606) ~[na:1.8.0_271] at java.net.Socket.connect(Socket.java:555) ~[na:1.8.0_271]而openfeign是用来实现基于feign框架的微服务调用,也就是让服务之间的调用更加方便。这个框架是可选的,如果你想基于RestTemplate方式进行调用,则不需要此框架的依赖。配置文件消费者添加配置(application.yml):spring: application: name: user-service-consumer cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者) service-url: nacos-user-service: http://user-service-provider同样server-addr指定注册Nacos Server的地址和端口。而配置中定义的service-url中便用到了服务提供者的服务名称user-service-provider。业务代码关于启动类上的注解,与提供者一样,如果根据使用的版本决定是否使用@EnableDiscoveryClient注解。创建UserController:@RestController @RequestMapping("/order") public class UserController { @Resource private UserFeignService userFeignService; @Resource private RestTemplate restTemplate; @Value("${service-url.nacos-user-service}") private String userProvider; @GetMapping("getUserInfo") public UserDetail getUserInfo() { int userId = 1; ResponseEntity<UserDetail> result = restTemplate.getForEntity(userProvider + "/user/getUserById?userId=" + userId, UserDetail.class); return result.getBody(); } @GetMapping("getUserInfo1") public UserDetail getUserInfoByFeign() { return userFeignService.getUserById(2); } }上述代码中展示了两种方式的请求,其中注入的RestTemplate和getUserInfo方法是一组,注入的UserFeignService和getUserInfoByFeign方法是一组。前者是基于RestTemplate方式请求,后者是基于Feign框架的模式进行请求的。先来看基于RestTemplate方式的配置,需要先来实例化一下RestTemplate:@Configuration public class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }注意,这里使用了@LoadBalanced注解,RestTemplateCustomizer会给标有@LoadBalance的RestTemplate添加一个拦截器,拦截器的作用就是对请求的URI进行转换获取到具体应该请求哪个服务实例ServiceInstance。如果缺少这个注解,也会报上面提到的异常。基于Feign的模式对应的UserFeignService如下:@FeignClient(name = "user-service-provider") public interface UserFeignService { /** * 基于Feign的接口调用 * * @param userId 用户ID * @return UserDetail */ @GetMapping(value = "/user/getUserById") UserDetail getUserById(@RequestParam Integer userId); } 其中@FeignClient通过name属性指定调用微服务的名称,下面定义的方法则对应提供者的接口。其中@FeignClient通过name属性指定调用微服务的名称,下面定义的方法则对应提供者的接口。启动服务,查看Nacos Server的注册情况。结果验证此时,本地分别请求两个URL地址:http://localhost:8080/order/getUserInfo http://localhost:8080/order/getUserInfo1访问一下,可以成功的返回结果:// getUserInfo对应结果 { "userId": 1, "username": "Tom" } // getUserInfo1对应结果 { "userId": 2, "username": "Other" }至此,Spring Cloud集成Nacos实例演示完毕,完整的源代码地址:https://github.com/secbr/spring-cloud 。小结经过上述实例,我们成功的将Nacos集成到了Spring Cloud当中。相对来说,整个过程还是比较简单的,在实践时,大家唯一需要注意的就是版本问题。Spring Cloud的不同版本,内容和用法调整较大,多参考官方文档的说明。

0
0
0
浏览量2017
后端小马

微服务之:服务挂的太干脆,Nacos还没反应过来,怎么办?

前言我们知道通过Nacos等注册中心可以实现微服务的治理。但引入了Nacos之后,真的就像理想中那样所有服务都由Nacos来完美的管理了吗?Too young,too simple!今天这篇文章就跟大家聊聊,当服务异常宕机,Nacos还未反应过来时,可能会发生的状况以及现有的解决方案。Nacos的健康检查故事还要从Nacos对服务实例的健康检查说起。Nacos目前支持临时实例使用心跳上报方式维持活性。Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。如果服务突然挂掉在正常业务场景下,如果关闭掉一个服务实例,默认情况下会在关闭之前主动调用注销接口,将Nacos服务端注册的实例清除掉。如果服务实例还没来得注销已经被干掉,比如正常kill一个应用,应用会处理完手头的事情再关闭,但如果使用kill -9来强制杀掉,就会出现无法注销的情况。针对这种意外情况,服务注销接口是无法被正确调用的,此时就需要健康检查来确保该实例被删除。通过上面分析的Nacos健康检查机制,我们会发现服务突然挂掉之后,会有15秒的间隙。在这段时间,Nacos服务端还没感知到服务挂掉,依旧将该服务提供给客户端使用。此时,必然会有一部分请求被分配到异常的实例上。针对这种情况,又该如何处理呢?如何确保服务不影响正常的业务呢?自定义心跳周期针对上面的问题,我们最容易想到的是解决方案就是缩短默认的健康检查时间。原本15秒才能发现服务异常,标记为不健康,那么是否可以将其缩短呢?这样错误影响的范围便可以变小,变得可控。针对此,Nacos 1.1.0之后提供了自定义心跳周期的配置。如果你基于客户端进行操作,在创建实例时,可在实例的metadata数据中进行心跳周期、健康检查过期时间及删除实例时间的配置。相关示例如下:String serviceName = randomDomainName(); Instance instance = new Instance(); instance.setIp("1.1.1.1"); instance.setPort(9999); Map<String, String> metadata = new HashMap<String, String>(); // 设置心跳的周期,单位为毫秒 metadata.put(PreservedMetadataKeys.HEART_BEAT_INTERVAL, "3000"); // 设置心跳超时时间,单位为毫秒;服务端6秒收不到客户端心跳,会将该客户端注册的实例设为不健康: metadata.put(PreservedMetadataKeys.HEART_BEAT_TIMEOUT, "6000"); // 设置实例删除的超时时间,单位为毫秒;即服务端9秒收不到客户端心跳,会将该客户端注册的实例删除: metadata.put(PreservedMetadataKeys.IP_DELETE_TIMEOUT, "9000"); instance.setMetadata(metadata); naming.registerInstance(serviceName, instance);如果是基于Spring Cloud Alibaba的项目,可通过如下方式配置:spring: application: name: user-service-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848 heart-beat-interval: 1000 #心跳间隔。单位为毫秒。 heart-beat-timeout: 3000 #心跳暂停。单位为毫秒。 ip-delete-timeout: 6000 #Ip删除超时。单位为毫秒。在某些Spring Cloud版本中,上述配置可能无法生效。也可以直接配置metadata的数据。配置方式如下:spring: application: name: user-service-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848 metadata: preserved.heart.beat.interval: 1000 #心跳间隔。时间单位:毫秒。 preserved.heart.beat.timeout: 3000 #心跳暂停。时间单位:毫秒。即服务端6秒收不到客户端心跳,会将该客户端注册的实例设为不健康; preserved.ip.delete.timeout: 6000 #Ip删除超时。时间单位:秒。即服务端9秒收不到客户端心跳,会将该客户端注册的实例删除;其中第一种配置,感兴趣的朋友可以看一下NacosServiceRegistryAutoConfiguration中相关组件的实例化。在某些版本中由于NacosRegistration和NacosDiscoveryProperties实例化的顺序问题会导致配置未生效。此时可考虑第二种配置形式。上面的配置项,最终会在NacosServiceRegistry在进行实例注册时通过getNacosInstanceFromRegistration方法进行封装: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.setEnabled(nacosDiscoveryProperties.isInstanceEnabled()); // 设置Metadata instance.setMetadata(registration.getMetadata()); instance.setEphemeral(nacosDiscoveryProperties.isEphemeral()); return instance; }其中setMetadata方法即是。通过Nacos提供的心跳周期配置,再结合自身的业务场景,我们就可以选择最适合的心跳检测机制,尽最大可能避免对业务的影响。这个方案看起来心跳周期越短越好,但这样会对Nacos服务端造成一定的压力。如果服务器允许,还是可以尽量缩短的。Nacos的保护阈值在上述配置中,我们还要结合自身的项目情况考虑一下Nacos保护阈值的配置。在Nacos中针对注册的服务实例有一个保护阈值的配置项。该配置项的值为0-1之间的浮点数。本质上,保护阈值是⼀个⽐例值(当前服务健康实例数/当前服务总实例数)。⼀般流程下,服务消费者要从Nacos获取可⽤实例有健康/不健康状态之分。Nacos在返回实例时,只会返回健康实例。但在⾼并发、⼤流量场景会存在⼀定的问题。比如,服务A有100个实例,98个实例都处于不健康状态,如果Nacos只返回这两个健康实例的话。流量洪峰的到来可能会直接打垮这两个服务,进一步产生雪崩效应。保护阈值存在的意义在于当服务A健康实例数/总实例数 < 保护阈值时,说明健康的实例不多了,保护阈值会被触发(状态true)。Nacos会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,但这样也⽐造成雪崩要好。牺牲了⼀些请求,保证了整个系统的可⽤。在上面的解决方案中,我们提到了可以自定义心跳周期,其中能够看到实例的状态会由健康、不健康和移除。这些参数的定义也要考虑到保护阈值的触发,避免雪崩效应的发生。SpringCloud的请求重试即便上面我们对心跳周期进行了调整,但在某一实例发生故障时,还会有短暂的时间出现Nacos服务没来得及将异常实例剔除的情况。此时,如果消费端请求该实例,依然会出现请求失败。为了构建更为健壮的应用系统,我们希望当请求失败的时候能够有一定策略的重试机制,而不是直接返回失败。这个时候就需要开发人来实现重试机制。在微服务架构中,通常我们会基于Ribbon或Spring Cloud LoadBalancer来进行负载均衡处理。除了像Ribbon、Feign框架自身已经支持的请求重试和请求转移功能。Spring Cloud也提供了标准的loadbalancer相关配置。关于Ribbon框架的使用我们在这里就不多说了,重点来看看Spring Cloud是如何帮我们实现的。异常模拟我们先来模拟一下异常情况,将上面讲到的先将上面的心跳周期调大,以方便测试。然后启动两个provider和一个consumer服务,负载均衡基于Spring Cloud LoadBalancer来处理。此时通过consumer进行请求,你会发现LoadBalancer通过轮训来将请求均匀的分配到两个provider上(打印日志)。此时,通过kill -9命令将其中一个provider关掉。此时,再通过consumer进行请求,会发现成功一次,失败一次,这样交替出现。解决方案我们通过Spring Cloud提供的LoadBalancerProperties配置类中定义的配置项来对重试机制进行配置,详细的配置项目可以对照该类的属性。在consumer的application配置中添加retry相关配置:spring: application: name: user-service-consumer cloud: nacos: discovery: server-addr: 127.0.0.1:8848 loadbalancer: retry: # 开启重试 enabled: true # 同一实例最大尝试次数 max-retries-on-same-service-instance: 1 # 其他实例最大尝试次数 max-retries-on-next-service-instance: 2 # 所有操作开启重试(慎重使用,特别是POST提交,幂等性保障) retry-on-all-operations: true上述配置中默认retry是开启的。max-retries-on-same-service-instance指的是当前实例尝试的次数,包括第一次请求,这里配置为1,也就是第一次请求失败就转移到其他实例了。当然也可以配置大于1的数值,这样还会在当前实例再尝试一下。max-retries-on-next-service-instance配置的转移请求其他实例时最大尝试次数。retry-on-all-operations默认为false,也就是说只支持Get请求的重试。这里设置为true支持所有的重试。既然涉及到重试,就需要保证好业务的幂等性。当进行上述配置之后,再次演示异常模拟,会发现即使服务挂掉,在Nacos中还存在,依旧可以正常进行业务处理。关于Ribbon或其他同类组件也有类似的解决方案,大家可以相应调研一下。解决方案的坑在使用Spring Cloud LoadBalancer时其实有一个坑,你可能会遇到上述配置不生效的情况。这是为什么呢?其实是因为依赖引入的问题,Spring Cloud LoadBalancer的重试机制是基于spring-retry的,如果没有引入对应的依赖,便会导致配置无法生效。而官方文档业务未给出说明。<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>另外,上述实例是基于Spring Cloud 2020.0.0版本,其他版本可能有不同的配置。小结在使用微服务的时候并不是将Spring Cloud的组件集成进去就完事了。这篇文章我们可以看到即便集成了Nacos,还会因为心跳机制来进行一些折中处理,比如调整心跳频次。同时,即便调整了心跳参数,还需要利用其它组件来兼顾请求异常时的重试和防止系统雪崩的发生。关注一下吧,持续更新微服务系列实战内容。

0
0
0
浏览量2016
后端小马

你也对阅读源码感兴趣,说说我是如何阅读Nacos源码的

前言最近写了一些列的Nacos源码相关文章,很多朋友都感兴趣的在问:你最近在阅读什么源码,如何阅读源码?今天这篇文章就以Nacos源码阅读来展开聊聊。在读这篇文章的时候呢,要看你想获得什么了。因为这篇文章亦是在写如何阅读Nacos源码,也是在写如何阅读源码。不要被技术栈所束缚,要提炼属于自己的方法。看你所欲,取你所需。阅读源码的目的不清楚大家为什么要阅读源码,就聊聊个人阅读源码的目的,或许可以拿来借鉴。学习底层原理与实现阅读某一个框架的源码,最重要的目的就是更深入的学习它的底层实现及原理。这里的底层实现和原理相对来说要宏观一些,比如阅读Nacos源码我就是想知道,它是如何实现服务注册、服务发现以及那些服务实例是如何存储的。像文章《微服务的灵魂摆渡者——Nacos,来一篇原理全攻略》便是来源于此类阅读。你也可以像我一样,阅读之后绘制成流程图、架构图、数据结构图,甚至整理成文章等帮助自己学习和理解。学习优秀的代码设计这一项包含的点就太多了,比如架构设计、功能实现理念、优秀代码示范、设计模式、算法等等。凡是能看到的,比较优秀的实践,都可以学习。以Nacos为例,简单的一个实例注销的入口方法,你能看到多少值得学习的内容? 上图是我一眼看过去,代码给我最直观的感受。然后就可以对照自己项目中的代码,思考一下是否能够达到这么高的标准?是否能进行改造?再看一个Nacos Client中的例子,在Client中调用Server的API时,会涉及到重试机制和多个Server选一个进行注册的逻辑。看看Nacos是如何实现的。 暂且不说算法的优劣,看到这里是不是感觉又学到了一种实现?而且你也知道了Nacos Client在调用Server时到底是怎么处理请求重试和异常的。有意思吧。当然,这个层面还有一些更深入的,比如一致性算法等很多解决方案的内容。学习知识点的运用这个层面有一个很好的点大家一定要把握住。那就是你可能看过很多文章在写某个知识点,而且也写了一些简单的实例。但如果你没有实践的机会,或者没在大型项目中运用,看源码中的实现和思考就非常有意思了。比如Nacos中对String.intern方法的使用,就没你想象中的那么简单。而且深入思考一下,还会发现并不是每个场景都适合,只有在字符串变化不大的情况下才适合缓存到常量池中。这里再举一个Nacos中对常见知识点的运用,看看咱们思考的维度是否一样。ServiceManager中有下图这么几个成员变量: 上面的知识点可能你已经背的滚瓜烂熟了,但你见过怎么用么?你见过怎么结合起来使用吗?怎么支持分布式、高并发的场景吗?只要你仔细分析一下,阅读一下源码的实现,你就能得到答案,也算是一次实践。从源码中可学的内容太多了,我这里就不逐一讲解了,后面会逐步形成系列文章的形式把我看到的源码中的技术和思想分享给大家。如何阅读源码有了阅读源码的目标,下一步就是执行了。这里就分享一些我阅读源码的方法,不一定适合你,但可以参考和改进。这里全部以Nacos为例,后续不再做特殊说明。代码的下载开源项目可以直接拉取源代码,Nacos的源代码有两个平台可以获取:GitHub和码云。码云库作为同步,定时更新。这里采用GitHub作为源码来源,说不定啥时候还可以贡献一些代码。可以直接执行git命令拉取开源库代码:git clone git@github.com:alibaba/nacos.git但个人并不建议这样直接拉取代码,可以从nacos的仓库fork到自己的GitHub账号下。这样既可以保持与主干的同步,又可以方便的修改一些内容。项目结构下载完成之后,直接通过IDEA打开项目,待依赖类库引入完毕,可看到如下项目结构: 此时可以大概了解一下目录结构,基本都能见名知意。address:地址服务相关;api:naming和config的api抽取;auth:权限控制相关;cmdb:支持对接第三方CMDB获取CMDB数据等;client:为客户端代码;common:共用工具类;config:Nacos配置中心的实现;consistency:一致性实现;console:Nacos控制台相关实现;console-uri:控制台UI部分实现;core:属性加载,初始化,监听器相关;distribution:发布相关;example:示例;istio:对istio的支持,如k8s等;naming:Nacos的核心功能,动态服务发现;项目的启动与测试要阅读源码跟踪流程,肯定要先把项目启动起来。由于Nacos是基于Spring Boot来构建的,只需执行对应入口类的main方法即可。在console项目中,执行Nacos的main方法即可启动项目。Nacos默认启动的是集群模式,研究代码先启动单机模式即可。这里通过在main方法中添加参数指定单机模式:public class Nacos { public static void main(String[] args) { // 通过环境变量的形式设置单机启动 System.setProperty(Constants.STANDALONE_MODE_PROPERTY_NAME, "true"); // 通过环境变量的形式设置关闭权限校验 System.setProperty("nacos.core.auth.enabled", "false"); SpringApplication.run(Nacos.class, args); } }程序启动之后,在client项目中的单元测试(test)中可以看到NamingTest和ConfigTest类,直接执行单元测试中的方法即可连接刚启动的Server进行代码跟踪调试了。此时,关于不同API的操作,也可参看官方文档的说明进行调用验证了,这就不做演示了。项目流程的梳理完成了代码的下载和启动之后,那么如何来梳理源代码的业务逻辑呢?很多朋友可能会遇到一些困惑,比如看不懂,或看了就忘了,每次都得重新梳理一遍。这个种状况在初期阶段很容易出现,也算是正常情况。我的处理方式是,先将fork的代码打一个分支(branch),比如我会打一个comment的分支。在这个分支上,每看到一行代码有点难度的代码或者需要备注的代码,就会在上面添加注释。关于Nacos本人fork的代码地址https://github.com/secbr/nacos,其中comment分支中在逐步添加注释,对Nacos源码感兴趣的朋友可以看一下。 比如上图是client请求服务器进行注册的部分代码逻辑,我会将梳理过的路径通过注释的形式进行描述。这样每次看到就不用再次梳理了。当对上述逻辑看得多了,也就不会忘记了。就这样逐个逻辑,逐个类的添加注释,当享受梳理流程图或架构图时,只用将注释内容进行提炼和概况即可。再看一下client注册时,server对应的代码注释: 至此,关于阅读源码的核心部分已经完事。剩下的就是逐个业务流程的梳理,逐个技术点的学习。如果遇到无法理解的部分,最好通过debug模式跟踪一下,看看执行到此处时,具体的数据都是什么。小结个人觉得阅读源码是每个程序员必备的技能,也是站在巨人肩膀上提升自己的必要手段。只有看得更多,才知道什么是更好的写法和实现。当然,每个人现阶段的能力有限,有很多技术点或设计思想当前阶段可能无法看到,但不要紧,你也可以拿我来做个垫背的,毕竟我是计划写一个源码解析系列的。

0
0
0
浏览量2014
后端小马

微服务的灵魂摆渡者——Nacos,来一篇原理全攻略

前言Nacos在微服务系统的服务注册和发现领域,势头迅猛是肉眼可见的。在微服务系统中,服务的注册和发现又是一个灵魂的存在。没有注册中心的存在,成百上千服务之间的调用复杂度不可想象。如果你计划或已经在使用Nacos了,但仅停留在使用层面,那这篇文章值得你一读。本文我们先从服务发现机制说起,然后讲解Nacos的基本介绍、实现原理、架构等,真正做到深入浅出的了解Nacos。服务注册与发现说起Nacos,不得不先聊聊微服务架构中的服务发现。关于服务发现其实已经在《要学习微服务的服务发现?先来了解一些科普知识吧》一文中进行了全面的讲解。我们这里再简要梳理一下。在传统应用中,一个服务A访问另外一个服务B,我们只需将服务B的服务地址和端口在服务A的静态配置文件中进行配置即可。但在微服务的架构中,这种情况就有所变化了,如下图所示:上图中,服务实例的IP是动态分配。同时,还面临着服务的增减、故障、升级等变化。这种情况,对于客户端程序来说,就需要使用更精确的服务发现机制。为了解决这个问题,于是像etcd、Consul、Apache Zookeeper、Nacos等服务注册中间件便应运而生。Nacos简介Nacos一般读作/nɑ:kəʊs/,这个名字来源于“Dynamic Naming and Configuration Service”。其中na取自“Naming”的前两个字母,co取自“Configuration”的前两个字母,而s则取自“Service”的首字母。Nacos的功能官方用一句话来进行了说明:“一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。”也就是说Nacos不仅提供了服务注册与发现功能,还提供了配置管理的功能,同时还提供了可视化的管理平台。官方文档中还提到“服务(Service)是Nacos世界的一等公民。”,也就是说在Nacos是围绕着Service转的。如果查看源码,会发现Nacos的核心API中定义了两个接口NamingService和ConfigService。服务注册与发现围绕着NamingService展开,而配置管理则围绕着ConfigService展开。官网给出了Nacos的4个核心特性:服务发现和服务健康监测、动态配置服务、动态DNS服务、服务及其元数据管理。我们主要来讲服务发现功能。Nacos的Server与ClientNacos注册中心分为Server与Client,Nacos提供SDK和openApi,如果没有SDK也可以根据openApi手动写服务注册与发现和配置拉取的逻辑。Server采用Java编写,基于Spring Boot框架,为Client提供注册发现服务与配置服务。Client支持包含了目前已知的Nacos多语言客户端及Spring生态的相关客户端。Client与微服务嵌套在一起。Nacos的DNS实现依赖了CoreDNS,其项目为nacos-coredns-plugin。该插件提供了基于CoreDNS的DNS-F客户端,开发语言为go。Nacos注册中的交互流程作为注册中心的功能来说,Nacos提供的功能与其他主流框架很类似,基本都是围绕服务实例注册、实例健康检查、服务实例获取这三个核心来实现的。 以Java版本的Nacos客户端为例,服务注册基本流程:服务实例启动将自身注册到Nacos注册中心,随后维持与注册中心的心跳;心跳维持策略为每5秒向Nacos Server发送一次心跳,并携带实例信息(服务名、实例IP、端口等);Nacos Server也会向Client主动发起健康检查,支持TCP/Http;15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例;服务消费者通过注册中心获取实例,并发起调用;其中服务发现支持两种场景:第一,服务消费者直接向注册中心发送获取某服务实例的请求,注册中心返回所有可用实例,但一般不推荐此种方式;第二、服务消费者向注册中心订阅某服务,并提交一个监听器,当注册中心中服务发生变化时,监听器会收到通知,消费者更新本地服务实例列表,以保证所有的服务均可用。Nacos数据模型关于数据模型,官网描述道:Nacos数据模型的Key由三元组唯一确定,Namespace默认是空串,公共命名空间(public),分组默认是DEFAULT_GROUP。 上面的图为官方提供的图,我们可以进一步细化拆分来看一下: 如果还无法理解,我们可以直接从代码层面来看看Namespace、Group和Service是如何存储的:/** * Map(namespace, Map(group::serviceName, Service)). */ private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();也就是说Nacos服务注册表结构为:Map<namespace, Map<group::serviceName, Service>>。Nacos基于namespace的设计是为了做多环境以及多租户数据(配置和服务)隔离的。如果用户有多套环境(开发、测试、生产等环境),则可以分别建三个不同的namespace,比如上图中的dev-namespace和prod-namespace。Nacos服务领域模型在上面的数据模式中,我们可以定位到一个服务(Service)了,那么服务的模型又是如何呢?官网提供了下图: 从图中的分级存储模型可以看到,在服务级别,保存了健康检查开关、元数据、路由机制、保护阈值等设置,而集群保存了健康检查模式、元数据、同步机制等数据,实例保存了该实例的ip、端口、权重、健康检查状态、下线状态、元数据、响应时间。此时,我们忽略掉一对多的情况,整个Nacos中数据存储的关系如下图: 可以看出,整个层级的包含关系为Namespace包含多个Group、Group可包含多个Service、Service可包含多个Cluster、Cluster中包含Instance集合。对应的部分源码如下:// ServiceManager类,Map(namespace, Map(group::serviceName, Service)) private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>(); // Service类,Map(cluster,Cluster) private Map<String, Cluster> clusterMap = new HashMap<>(); // Cluster类 private Set<Instance> persistentInstances = new HashSet<>(); private Set<Instance> ephemeralInstances = new HashSet<>(); // Instance类 private String instanceId; private String ip; private int port; private double weight = 1.0D; //...其中,实例又分为临时实例和持久化实例。它们的区别关键是健康检查的方式。临时实例使用客户端上报模式,而持久化实例使用服务端反向探测模式。临时实例需要能够自动摘除不健康实例,而且无需持久化存储实例。持久化实例使用服务端探测的健康检查方式,因为客户端不会上报心跳,自然就不能去自动摘除下线的实例。小结我们从微服务系统中为什么使用服务发现讲起,然后介绍了Nacos、Nacos的实现机制、底层数据模型以及部分源码实现。在使用过程中除了关注服务注册与发现、健康检查之外,对于服务的数据模型中Namespace、Group、Service和Instance也需要重点关注。当理解了这些背后的工作原理,对于上层应用的整合以及配置便可以轻松运用了。

0
0
0
浏览量2018
后端小马

要学习微服务的服务发现?先来了解一些科普知识吧

为什么要使用服务发现功能?当调用REST API 或Thrift API的服务时,我们在构建请求时通常需要知道服务实例的IP和端口。在传统应用中,服务实例的地址信息相对固定,可以从配置文件中读取。而这些地址也只是只会偶尔更新。但在现代应用程序中,往往是基于云的微服务架构,此时获取服务实例的IP和端口便是一个需要解决的难题。如下图所示:上图中,服务实例实例的IP是动态分配。同时,还面临着服务的增减、故障以及升级等变化。这对于客户端程序来说,就需要使用更精确的服务发现机制。目前,服务发现模式主要有两种:客户端发现模式和服务端发现模式。先来看一下客户端发现模式。客户端发现模式使用客户端发现模式时,客户端负责判断服务实例的可用性和请求的负载均衡。服务实例存储在注册表中,也就是说注册表是服务实例的数据库。客户端通过查询服务注册表,获得服务实例列表,然后使用负载均衡算法从中选择一个,然后发起请求。下图为这种模式的架构图:此种模式下,当服务实例启动时,会将自己的地址信息注册到服务注册表,当服务停止时从服务注册表中移除。这期间,通常使用心跳机制来定刷新服务实例的注册。Netflix Eureka就是一个服务注册表组件,它提供了基于REST API的服务实例注册和查询功能。Netflix Ribbon是一个IPC客户端,可配合Eureka实现对服务实例请求的负载均衡。客户端发现模式的优点是相对简单,除服务注册表外,不需要其他部分做改动。同时,由于客户端知道所有的可用实例,可以做出更明智的、基于特定应用场景的负载均衡决策,比如使用一致性哈希算法。这种模式的缺点是将客户端和服务注册表功能耦合在了一起,必须为每种编程语言和框架的客户端实现服务发现逻辑。服务器端发现模式另外一种服务发现模式就是服务器发现模式。下图中展示了该模式的结构:客户端通过负载均衡器向服务发起请求,负载均衡器查询服务注册表,并将请求路由到可用的服务实例。与客户端发现相比,服务实例是通过服务注册表进行注册和注销的。AWS的ELB(Elastic Load Balancer)就是服务器端发现路由器的示例。ELB通常用于负载均衡来自外网的流量,但你也可以使用ELB来负载均衡私有云(VPV)内部的流量。客户端使用DNS名称,通过ELB发送请求(Http或TCP),ELB在已注册的弹性计算云(EC2)实例或EC2容器服务(ECS)的容器之间进行负载均衡。这种实现并没有单独的服务注册表,而是将EC2实例和ECS容器注册到ELB自身上。Http服务器和负载均衡器(比如,Nginx plus和Nginx)也可以用作服务器端发现的负载均衡器。比如,使用Consul模板动态配置Nginx反向代理。Consul可以从存储在Consul服务注册表中的配置数据中定时重新生成任意配置文件。每当文件改变时,可以运行一个任意shell命令。比如,Consul模板生成一个nginx.conf文件,用于配置反向代理,然后执行命令告诉Nginx去重新加载配置。某些部署环境(例如Kubernetes和Marathon)会在集群中的每个主机上运行一个代理。这个代理扮演服务器端发现负载平衡器的角色。客户端向服务发出请求时,会通过代理进行路由,透明地将请求转发到集群中某个服务实例。服务器端发现模式最大的优点是,服务发现的实现细节从客户端抽离出来了,客户端只用发送请求到负载均衡器即可。这样就无需为每种编程语言和框架的客户端实现服务发现逻辑。而且,某些部署环境已经免费提供了该功能。当然,这种模式也有一些缺点,如果部署环境未提供负载均衡器,你还需要搭建和管理一个额外的高可用系统组件。服务注册表服务注册表是服务发现的关键,它是一个包含服务实例地址信息的数据库。服务注册表需要具有高可用性和实时更新性。客户端可以缓存从注册表获得的服务实例地址信息。但这些信息会过时,因此,服务注册表也需要是集群模式,且集群之间还需要通过协议维持一致性。Netflix Eureka是一个服务注册表组件,它提供了基于REST API形式的服务实例注册和查询功能。一个服务实例可以通过POST请求将自己注册到注册表中;可以通过PUT请求,每隔30秒刷新它的注册信息;可以通过Http的DELETE请求或超时机制来删除实例的注册信息;可以使用Http的GET请求来检索注册的服务实例。常见的服务注册表组件有:etcd、Consul、Apache Zookeeper、Nacos等。服务注册的选项服务实例必须通过注册表进行注册或注销,通常有几种不同方式来处理注册和注销。一种是服务实例自己注册,即自我注册模式;另一种是基于其他系统组件来管理服务实例的注册,即第三方注册模式。先来看一下自我注册模式。自我注册模式当使用自我注册模式时,服务实例负责在服务注册表中进行自身的注册和注销。如果需要,服务实例还需要发送心跳请求以避免因超时而被注销。下图展示了这种模式的结构图:Netflix OSS Eureka客户端就是这种模式的示例,Eureka客户端负责处理服务实例所有的注册和注销事项。在Spring Cloud项目中,实现了包括服务发现的各种模式,基于此可以很轻松的实现自动注册服务实例到Eureka。你只需在Java配置类上使用@EnableEurekaClient注解即可。自我注册模式的优点是使用起来非常简单,不需要任何其他系统组件。缺点是服务实例与服务注册表紧密耦合,需要在每种编程语言和框架中实现注册功能。另外一种方式可以让服务和注册表解耦的方式就是第三方注册模式。第三方注册模式当使用第三方注册模式时,服务实例不再负责将自己注册到服务注册表。这一功能由第三方组件作为服务注册商来处理。服务注册商通过轮询或订阅事件来跟踪实例的变化。当发现新的可用服务实例时,会将服务实例注册到服务注册表中。同时,也会注销已经停止的服务实例。下图展示了这种模式的结构:开源项目Registrator便是一个示例,它可以基于Docker容器自动注册和注销服务实例。Registrator支持多种注册表,包括etcd和Consul。NetflixOSS Prana项目是另外一个示例,它主要用于非JVM语言编写的服务,是与服务实例并行的Sidecar应用程序。Prana基于Netflix Eureka注册和注销服务实例。第三方注册模式的优点是服务与服务注册表分离,无需每种编程语言和框架的客户端实现服务注册逻辑,而是在专用服务内以集中方式处理服务实例注册。这种模式缺点是,除非部署环境提供内置服务,否则还需要额外搭建和管理一个高度可用的系统组件。总结在微服务应用程序中,服务实例运行状态会动态更改,实例会动态分配地址。因此,为了使客户端可以正常请求服务,必须使用服务发现机制。而本文正是围绕服务发现中的两种模式(客户端发现和服务器端发现)、服务注册表及其两种途径(自我注册模式和第三方注册模式)、反向代理服务器等知识点进行讲解。只有科普了以上基础知识,我们才能更好的学习和认识微服务中的服务发现功能。

0
0
0
浏览量2010
后端小马

微服务之吐槽一下Nacos日志的疯狂输出

前言目前公司系统采用Spring Cloud架构,其中服务注册和发现组件用的Nacos,最近运维抱怨说,磁盘不够用,日志增长的太快。简单排查一下,罪魁祸首竟然是Nacos。按理说Nacos作为服务注册中心,不会应该会产生太多日志的,本身涉及的服务也不多,但几天就会产生1G以上的日志,的确有点疯狂。这篇文章就聊聊Nacos的日志系统。事件背景经过排查,其中输出最多的日志为{nacos.home}/logs/access_log.yyyy-mm-dd.log格式的日志。日志中包含了微服务系统调用Nacos及集群之间通信的日志,比如心跳(/nacos/v1/ns/instance/beat)、获取服务列表(/nacos/v1/ns/instance/list)、状态检查(/nacos/v1/ns/service/status)等。我们知道Nacos是基于Spring Boot实现的,access_log日志是Spring Boot提内置的Tomcat的访问日志。关于该项日志的配置,没有保留最大天数,也没有日志大小的控制。而且随着Nacos Server与各个服务直接的心跳、获取、注册等会不停的产生访问日志,微服务越多,日志增长越快。这些日志打印会迅速占用完磁盘空间,带来资源浪费和运维成本。解决方案上述的access_log日志输出Nacos是提供了控制开关的,在Nacos的conf目录下application.properties配置文件中,默认有以下配置:#*************** Access Log Related Configurations ***************# ### If turn on the access log: server.tomcat.accesslog.enabled=true ### The access log pattern: server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i %{Request-Source}i ### The directory of access log: server.tomcat.basedir=可以看到,关于访问日志支持关闭、日志输出格式以及日志输出的目录。在测试环境,我们可以直接将enabled的配置项设置为false,直接关闭该日志的输出。server.tomcat.accesslog.enabled=false但在生产环境,这样操作就有一定的风险了。当关闭之后,生产出现问题时需要根据日志进行排查,就会找不到对应的日志。此时,只能通过其他方式进行处理,比如在Linux操作系统下通过编写crontab来完成日志的定时删除。对应的脚本示例如下:#!/bin/bash logFile="/data/nacos/bin/logs/nacos_del_access.log" # 保留14天日志 date=`date -d "$date -14 day" +"%Y-%m-%d"` # 具体位置可调整 delFilePath="/data/nacos/bin/logs/access_log.${date}.log" if [ ! -f "${logFile}" ];then echo 'access log文件打印日志频繁. /etc/cron.daily/nacosDelAccessLogs.sh 会定时删除access日志文件' >>${logFile} fi # 日志文件存在, 则删除 if [ -f "${delFilePath}" ];then rm -rf ${delFilePath} curDate=`date --date='0 days ago' "+%Y-%m-%d %H:%M:%S"` echo '['${curDate}'] 删除文件'${delFilePath} >>${logFile} fi虽然问题解决了,但很明显并不优雅,这也是Nacos Server日志输出的问题之一。日志级别动态调整关于Nacos Server日志的输出级别,在1.1.3版本之前,同样会打印大量的日志,而且没办法动态的进行调整。在此版本之后,日志输出得到了优化,并且支持通过API的形式来进行日志级别的调整,示例如下:# 调整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'客户端日志业务系统集成的客户端在1.1.3版本之后,也进行了优化,避免日志大量打印(主要涉及心跳日志、轮询日志等)。在业务系统的application.yml配置文件中,可通过日志级别设置来进行控制:# 日志级别,可以指定到具体类 logging: level: com.alibaba.nacos: warn也可以通过启动时的JVM参数来进行控制,默认是info级别:-Dcom.alibaba.nacos.naming.log.level=warn -Dcom.alibaba.nacos.config.log.level=warn上述示例分别指定了Naming客户端和Config客户端的日志级别,适用于1.0.0及以上版本。更细的日志配置查看conf目录下的nacos-logback.xml配置,你会发现Nacos相关的日志配置项非常多,如果因项目需要进行更精细化的配置,可在此文件中进行直接配置。以naming-server对应的append配置为例,看一下默认的配置:<appender name="naming-server" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/naming-server.log</file> <append>true</append> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/naming-server.log.%d{yyyy-MM-dd}.%i</fileNamePattern> <maxFileSize>1GB</maxFileSize> <maxHistory>7</maxHistory> <totalSizeCap>7GB</totalSizeCap> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <encoder> <Pattern>%date %level %msg%n%n</Pattern> <charset>UTF-8</charset> </encoder> </appender>这里根据自己的需要,可调整输出的日志格式、日志文件分割、日志保留日期及日志压缩等处理。小结关于Nacos的日志输出就聊这么多,整体而言相关的日志输出有些过于多了,而且在灵活配置方面还有待提升。基于目前的现状我们可以通过自定义或定时任务等配合完成日志输出与管理。

0
0
0
浏览量2009