目录
心跳处理第一站(InstanceOperatorClientImpl)
真正心跳处理(ClientBeatProcessorV2 )
学习目标: 我们可能会想到一个心跳请求发送到服务端,服务端一定会记住该实例的最近一次心跳时间,没错,这个是心跳非常重要的处理逻辑,通过本节的学习你会知道,什么是轻量心跳,心跳本身信息除了实例信息还有哪些信息,心跳复活是怎么回事,心跳过后要发生什么事件。
流程图
接收心跳请求
public class InstanceController {
.....
@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());
//获取beat信息
String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
RsInfo clientBeat = null;
if (StringUtils.isNotBlank(beat)) {
//把心跳信息反序列化成RsInfo
clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
}
//如果没有渠道集群字段设置默认值 DEFAULT
String clusterName = WebUtils
.optional(request, CommonParams.CLUSTER_NAME,
UtilsAndCommons.DEFAULT_CLUSTER_NAME);
//获取心跳实例的ip和端口号
String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
if (clientBeat != null) {
//给clientBeat 设置集群名称
if (StringUtils.isNotBlank(clientBeat.getCluster())) {
clusterName = clientBeat.getCluster();
} else {
// fix #2533
clientBeat.setCluster(clusterName);
}
//从心跳信息中的ip和端口覆盖请求的ip和端口
//一般ip和端口信息是保护再beat信息中 不是放在请求里面
ip = clientBeat.getIp();
port = clientBeat.getPort();
}
//命名空间默认是PUBLIC
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
Constants.DEFAULT_NAMESPACE_ID);
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
//BeatInfoInstanceBuilder类是根据beat信息构建Instance对象 具体代码不贴出来了
BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder();
builder.setRequest(request);
//InstanceOperatorClientImpl处理心跳流程
int resultCode = getInstanceOperator()
.handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat,
builder);
//
result.put(CommonParams.CODE, resultCode);
//1、从前面2章节我们讲的元数据管理器中获取实例元数据中心跳间隔时间
//2、如果第一步没有获取到就从该客户端中获取该实例的心跳间隔时间
result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,
getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip,
port, clusterName));
//是否启用轻量级的心跳 这个属性目前1.1.4的客户端还么有用到
//nacos客户端心跳分为两种,一种是注册时心跳,目的是首次注册是上报,第2种是轻量级心
跳,目的是保持链接
//首次注册时心跳,服务端会返回参数lightBeatEnabled=true,标注下次心跳为轻量级心跳
result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
return result;
}
}
public class RsInfo {
private double load;
private double cpu;
private double rt;
private double qps;
private double mem;
private int port;
private String ip;
private String serviceName;
private String ak;
private String cluster;
private double weight;
private boolean ephemeral = true;
private Map<String, String> metadata;
}
注释基本已经把代码逻辑都讲清楚了。还记得讲客户端心跳得时候说了客户端发送心跳得间隔时间以服务端得返回为准,如果服务端没有返回就用客户端本地设置得。今天这里终于看到源头。
RsInfo 对象得属性基本都包括了beatinfo得属性,初次之外多了些什么呢?
load cpu 都是系统负载
rt qps:代码系统性能参数
mem:系统使用内存
这些参数虽然我们目前还不知道是干啥用的,有没有可能是作为负载均衡得参考呢?还是做监控呢?
BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder();
builder会根据beat信息构建一个instance.接下来我们看InstanceOperatorClientImpl是怎么处理心跳得
关于注册时心跳和轻量级心跳,我们目标客户端使用的是1.1.4版本,后续我们把客户端升级到2.x版本后就会使用到这个功能。通过上面rsInfo的属性看到每次一次心跳请求数据尤其是metaData内容如果比较多而且服务集群超大的情况下对网络开销和系统系统性能都有很大影响。
心跳处理第一站(InstanceOperatorClientImpl)
public class InstanceOperatorClientImpl {
@Override
public int handleBeat(String namespaceId, String serviceName, String ip, int port,
String cluster, RsInfo clientBeat, BeatInfoInstanceBuilder builder) throws
NacosException {
//创建服务对象 生成clientId 前面遇到过多次不赘述
Service service = getService(namespaceId, serviceName, true);
String clientId = IpPortBasedClient.getClientId(ip +
InternetAddressUtil.IP_PORT_SPLITER + port, true);
//从客户端管理器查询客户端
IpPortBasedClient client = (IpPortBasedClient)
clientManager.getClient(clientId);
//如果么有找到客户端或者客户端发布实例信息不存在
if (null == client || !client.getAllPublishedService().contains(service)) {
if (null == clientBeat) {
return NamingResponseCode.RESOURCE_NOT_FOUND;
}
//注册实例 走之前得服务注册流程
Instance instance =
builder.setBeatInfo(clientBeat).setServiceName(serviceName).build();
registerInstance(namespaceId, serviceName, instance);
client = (IpPortBasedClient) clientManager.getClient(clientId);
}
//如果服务管理器找不到该服务 报错
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);
}
//心跳处理器交给HealthCheckReactor 立即执行
ClientBeatProcessorV2 beatProcessor = new ClientBeatProcessorV2(namespaceId,
clientBeat, client);
HealthCheckReactor.scheduleNow(beatProcessor);
//修改客户端的最后更新时间
client.setLastUpdatedTime();
return NamingResponseCode.OK;
}
}
上面的代码中什么情况下 clientBeat 会为空呢?结合请求入口,我们猜想有一种情况,
请求的beat信息为空,客户端把ip和port作为请求参数传递到服务端。
需要注意的时候如果服务端找不到该客户端或者客户端注册的实例,那么beat字段必须要传递。换句话说,beat可以不传前提是实例注册已经完毕了。
另外留个思考提为啥那么客户端的最后更新时间被设置了却不发送ClientChangeEvent呢
真正心跳处理(ClientBeatProcessorV2 )
public class ClientBeatProcessorV2 implements BeatProcessor {
private final String namespace;
private final RsInfo rsInfo;
private final IpPortBasedClient client;
....
@Override
public void run() {
//几个经典字段不罗嗦
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());
HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo)
client.getInstancePublishInfo(service);
if (instance.getIp().equals(ip) && instance.getPort() == port) {
//重新设置实例的最后心跳时间
instance.setLastHeartBeatTime(System.currentTimeMillis());
if (!instance.isHealthy()) {
//心跳促使实例复活
//发送服务变更事件通知所有的订阅端
//发送客户端变更事件[这个事件会触发什么逻辑后面碰到再说]
instance.setHealthy(true);
....
NotifyCenter.publishEvent(new
ServiceEvent.ServiceChangedEvent(service));
NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client));
}
}
}
}
//这里再附上HealthCheckInstancePublishInfo 类的代码
//可以看出lastHeartBeatTime 是InstancePublishInfo 实例所么有的
public class HealthCheckInstancePublishInfo extends InstancePublishInfo {
private static final long serialVersionUID = 5424801693490263492L;
private long lastHeartBeatTime = System.currentTimeMillis();
private HealthCheckStatus healthCheckStatus;
}
这段代码大家主要是注意一个心跳的复活逻辑。实例复活后需要发布服务变更事件通知所有的订阅者更新本地的服务实例,同时发送客户端变更事件。
总结
本届我们了解了心跳请求的处理逻辑。 根据请求信息构建RsInfo对象,如果该客户端不存在或者未注册实例就重新注册实例。否则就修改心跳时间和客户端最近更新时间,并且判断收到心跳之前是不是健康的,否则就复活该实例并发送服务&客户端变更事件。另外提到的轻量级的心跳后续讲到2.X客户端时再详细讲解。