四、Eureka概念的理解
4.1、服务的注册
当项目启动时(eureka 的客户端),就会向 eureka-server 发送自己的元数据(原始数据) (运行的 ip,端口 port,健康的状态监控
等,因为使用的是 http/ResuFul 请求风格), eureka-server 会在自己内部保留这些元数据(内存中)。(有一个服务列表)(restful 风
格,以 http 动词的请求方式,完成对 url 资源的操作)
4.2、服务的续约
项目启动成功了,除了向 eureka-server 注册自己成功,还会定时的向 eureka-server 汇 报自己,心跳,表示自己还活着。(修改一个时间)
4.3、服务的下线
当项目关闭时,会给 eureka-server 报告,说明自己要下机了。
4.4、服务的剔除(被动下线,主动剔除)
当项目超过了指定时间没有向 eureka-server 汇报自己,那么 eureka-server 就会认为此节点死掉了,会把它剔除掉,也不会放流量和请
求到此节点了。
五、Eureka原理分析
5.1、Eureka 运作原理的特点
Eureka-server 对外提供的是 restful 风格的服务 :以 http 动词的形式对 url 资源进行操作 get post put delete。
http 服务 + 特定的请求方式 + 特定的 url 地址:只要利用这些 restful 我们就能对项目实现注册和发现。
只不过,eureka 已经帮我们使用 java 语言写了 client,让我们的项目只要依赖 client 就能实现注册和发现!
只要你会发起 Http 请求,那你就有可能自己实现服务的注册和发现。不管你是什么语言!
5.2、服务注册的源码分析【重点】
5.2.1、Eureka-client发起注册请求
5.2.1.1、源码位置
5.2.1.2、如何发送信息来进行注册?
真正去执行注册的是在一个抽象类中:
总结: 当 eureka 启动的时候,会向我们指定的 serviceUrl 发送请求,把自己节点的数据以 post 请求的方式,数据以 json 形式发送过去。 当返回的状态码为 204 的时候,表示注册成功。
5.2.2、Eureka-server实现注册+保存
1、接受客户端的请求
com.netflix.eureka.resources.ApplicationResource
2、实际服务端的源码位置
3、处理请求(注册自己,向其他节点注册)
上面第二段super.register() public void register(InstanceInfo info, boolean isReplication) { int leaseDuration = 90; if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) { leaseDuration = info.getLeaseInfo().getDurationInSecs(); } //真正注册自己 super.register(info, leaseDuration, isReplication); this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication); }
4、真正的注册自己
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { this.read.lock(); try { //通过服务名称得到注册的实例 Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName()); EurekaMonitors.REGISTER.increment(isReplication); //因为之前没有实例,肯定为 null if (gMap == null) { //新建一个集合来存放实例 ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap(); gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap); if (gMap == null) { gMap = gNewMap; } } //gMap 就是该服务的实例 Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId()); if (existingLease != null && existingLease.getHolder() != null) { Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp(); Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp(); logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp); if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) { logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp); logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant"); registrant = (InstanceInfo)existingLease.getHolder(); } } else { synchronized(this.lock) { if (this.expectedNumberOfClientsSendingRenews > 0) { ++this.expectedNumberOfClientsSendingRenews; this.updateRenewsPerMinThreshold(); } } logger.debug("No previous lease information found; it is new registration"); } //新建一个服务的实例节点 Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration); if (existingLease != null) { lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp()); } //放到注册 map 的列表里 ((Map)gMap).put(registrant.getId(), lease); this.recentRegisteredQueue.add(new Pair(System.currentTimeMillis(), registrant.getAppName() + "(" + registrant.getId() + ")")); if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) { logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the overrides", registrant.getOverriddenStatus(), registrant.getId()); if (!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) { logger.info("Not found overridden id {} and hence adding it", registrant.getId()); this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus()); } } InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId()); if (overriddenStatusFromMap != null) { logger.info("Storing overridden status {} from map", overriddenStatusFromMap); registrant.setOverriddenStatus(overriddenStatusFromMap); } InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication); registrant.setStatusWithoutDirty(overriddenInstanceStatus); if (InstanceStatus.UP.equals(registrant.getStatus())) { lease.serviceUp(); } //设置心跳时间等参数 registrant.setActionType(ActionType.ADDED); this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease)); registrant.setLastUpdatedTimestamp(); this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress()); logger.info("Registered instance {}/{} with status {} (replication={})", new Object[]{registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication}); } finally { this.read.unlock(); } }
5.2.3、服务注册总结
重要的类:
DiscoveryClient 里面的 register()方法完后注册的总体构造
AbstractJerseyEurekaHttpClient 里面的 register()方法具体发送注册请求(post)
InstanceRegistry 里面 register()方法接受客户端的注册请求
PeerAwareInstanceRegistryImpl 里面调用父类的 register()方法实现注册
AbstractInstanceRegistry 里面的 register()方法完成具体的注册保留数据到 map 集合保存服务实例数据的集合:
保存服务实例数据的集合:
第一个 key 是应用名称(全大写) spring.application.name Value 中的 key 是应用的实例 id eureka.instance.instance-id Value 中的 value 是 具体的服务节点信息 private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
5.3、服务剔除的源码分析(被动下线)
AbstractInstanceRegistry 的evict()方法中筛选剔除的节点。
在 internalCancel 方法里面真正实现剔除:其实就是对map执行remove操作。
在服务剔除中涉及到哪些重要的点
怎么删除一个集合里面过期的数据?
Redis 怎么清除过期的 key LRU(热点 key)
1 定时(k-thread)
2 惰性 (在再次访问该 key 时有作用)
3 定期 (使用一个线程来完成清除任务)
操作:定期(实时性差) + 惰性
何时进行剔除操作?
通过一个定时任务:
5.4、服务下线的源码分析
eureka-client发起下线请求
①通过unregister()方法来进行发起下线请求
②真正的发起请求下线:AbstractJerseyEurekaHttpClient
eureka-server处理下线请求
①接受下线请求
②真正的下线服务
5.5、服务发现的源码分析
5.5.1、服务发现流程分析
从 discoveryClient.getInstances(serviceId);方法进去,找到eureka 的实现
从 getInstancesByVipAddress 方法进去看到真正的服务发现
在 getInstancesByVirtualHostName 方法里面做真正的服务发现
5.5.2、在 eureka-client 客户端也有 map 集合存放服务列表?
我们发现,当我们还没有做服务发现之前,集合里面已经有值了,说明项目启动的时候就去server 端拉取服务列表并且缓存了起来
到底何时从 server 拉取服务放进去的呢?
在 eureka 的 DiscoverClient 类的一个构造方法里面,有一个任务调度线程池:
查看 initScheduledTasks()这个方法
在 CacheRefreshThread()中
fetchRegistry()方法中判断决定是全量拉取还是增量拉取
getAndStoreFullRegistry()全量拉取:
getAndUpdateDelta()增量拉取:
5.5.3、服务发现总结
重要的类:
DiscoveryClient 类里面的构造方法执行线程初始化调用
CacheRefreshThread 类里面的 run 方法执行服务列表的拉取(方便后期做服务发现)
fetchRegistry()方法去判断全量拉取还是增量拉取
全量拉取发生在:当服务列表为 null 的情况 当项目刚启动就全量拉取
增量拉取发生:当列表不为 null ,只拉取 eureka-server 的修改的数据(注册新的服务, 上线服务)
eureka 客户端会把服务列表缓存到本地 为了提高性能 ,但是有脏读问题,当你启动一个新的应用的时候 不会被老的应用快速发现
六、服务发现
6.1、什么是服务发现?
根据服务名称发现服务的实例过程
客户端会在本地缓存服务端的列表
拉取列表是有间隔周期的 (导致服务上线 客户端不能第一时间感知到 (可以容忍))
其实每次做服务发现 都是从本地的列表来进行的
6.2、简单实现服务发现功能
我们在eureka-client-a中编写对应的服务发现控制器:
package com.changlu.eurekaclienta.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @Description: 我的服务注册发现 * @Author: changlu * @Date: 10:04 PM */ @RestController public class MyDiscoveryController { @Autowired private DiscoveryClient discoveryClient;//这个接口是springcloud提供的,然后eureka提供了对应的实现类 @GetMapping("/find") public String find(String serviceId) { //调用服务发现 List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); instances.forEach(System.out::println); return instances.toString(); } }
启动一个server服务以及两个客户端:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dgfv64lB-1657597370320)(C:\Users\93997\AppData\Roaming\Typora\typora-user-images\image-20220703221052050.png)]
访问下:http://localhost:8761/,查看下当前的服务
接着我们来进行测试服务发现:http://localhost:8082/find?serviceId=eureka-client-a