你好,我是悟空。
唐朝第二位皇帝唐太宗为了扩张领土,到处攻打周边的小国,即使有不服的小国也被唐太宗打服了。这些小国后来就都需要向唐太宗朝贡。
朝贡就是朝拜和进贡。是两国或者说是两个政府之间的一种承认对方尊卑关系的礼节性外交。
唐朝朝贡图,来源百度百科
“贞观之治” 说的就是当时唐朝的鼎盛时期, 周围小国都被打趴下了,国内繁荣发展,以十分惊人的速度成为世界顶级强国。唐人街中的“唐”就是说的唐朝,足以说明唐朝对世界的影响。
小国定期向唐朝进行朝贡这不就是微服务的心跳机制吗?
他们是在告诉唐朝,我还是服你管教的。然后唐朝就会把这些小国的名字、地址、服饰外貌等特征放到一个朝贡国列表中。万一哪天这些小国不服管了,就把他们从列表中移除掉,后期可能还会攻打他们~
下面是一张多国朝贡的示例图:
朝贡示例图
在微服务领域,心跳机制出现得太频繁了,比如 Eureka、Naocs 中的客户端和服务端的服务续约、Redis 的主从复制等等,其实原理都很相似。
本篇会通过 Eureka 中的服务续约功能作为示例来剖析心跳机制。
对于 Eureka,会涉及到两个端,客户端和服务端。客户端就相当于我们的订单服务、商品服务等。而 Eureka 服务端则是指 Eureka 注册中心这个服务。而保持续约就是客户端隔一段时间就向服务端发送一次心跳,告诉 Eureka 服务端自己的状态是存活的。
主要涉及以下知识点:
- ① 谁发送的心跳请求?
- ② 多久发送一次?
- ③ 如何发送的?
- ④ 如何接收心跳请求的?
- ⑤ 接收后做了什么事情?
谁发送的心跳请求
Eureka 采用的是客户端发送心跳请求给 Eureka 服务端。如下图所示:
上图中有三个微服务:订单服务、商品服务、优惠券服务,都已经成功注册到 Eureka 服务端了(注册中心)。
然后每个微服务自己会单独发送心跳请求给注册中心。
多久发送一次
DIscoveryClient 初始化时,会调度一些定时任务。Eureka 初始化了发送心跳请求的线程池 heartbeatExecutor,用来创建发送心跳的线程 HeartbeatThread。原理如图所示:
线程池 heartbeatExecutor 源码如下所示:
线程池
线程池有核心参数:
- maximumPoolSize:最大线程数。线程池允许创建的最大线程数。
- corePoolSize:核心线程数。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程,等到 需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads() 方法,则线程池会提前创建并启动所有基本线程。
- keepAliveTime:线程活动保持时间 ,线程池的工作线程空闲后,保持存活的时间。
- runnableTaskQueue:任务队列,用于保存等待执行的任务的阻塞队列。有四种:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
然后将这个线程池用来执行定时调度任务,源码如下所示,在定时任务开始后,延迟 30s 开始执行发送心跳请求,然后每隔 30秒执行一次发送心跳请求。这里可以看到 new 了一个 HeartbeatThread 线程。更多线程相关知识,请看这篇:多线程核心知识点
定时任务
如何发送心跳请求的?
HeartbeatThread 线程继承自 Runnable 类,实现了 run 方法,这个里面就会执行发送心跳请求的具体逻辑了。
直接进到 renew() 方法里面,核心逻辑就这一行:
eurekaTransport.registrationClient.sendHeartBeat(
instanceInfo.getAppName(),
instanceInfo.getId(),
instanceInfo,
null);
调用 EurekaHttpClient 的 sentHeartBeat 方法,将实例信息发送给注册信息。
拼接的请求 URL 示例如下:
http://localhost:8080/v2/apps/order/i-000000-1
而且这个请求是 PUT 请求。
如何接收心跳请求的?
请求从客户端发出心跳请求后,服务端就要接收这个请求了。
负责接受请求的类为 ApplicationsResource,它相当于 MVC 中的 Controller。
根据请求的 URL 格式和请求方式(PUT),我们可以找到服务端的方法为 InstanceResource.renewLease()。
ApplicationsResource->ApplicationResource->InstanceResource
接收后做了什么事情
里面的核心代码就是 renew 方法,将实例的一个字段给更新了,这个字段叫做 lastupdateTimestamp,也就是最后更新时间。
public void renew() {
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
心跳机制
这个实例其实是从服务端注册表 registry 中拿到的,它是一个 ConcurrentHashmap,实例名当做 key,来获取 value(实例),也就是说实例信息是存在内存中的。
拿到的是一个 Lease 实例,数据结构是这样的:Lease,它有一个 volatile 修饰的字段 lastUpdateTimestamp。通过更新这个字段来记录实例信息确实存活着在,而且刚刚还跟 Eureka 通信了。
这就像古代唐朝的朝贡,唐朝周边的小国是需要定期进贡给唐朝的,目的是告诉唐朝,我现在还是依附唐朝的。
那么有了这个字段更新,Eureka Server 自身还会有个定时任务,去检查服务实例的最后更新时间,如果过期了,则认为该实例状态异常,需要进行服务下线,这个是下一篇要讲的内容。