目录
学习目标:通过本篇的学习需要解客户端底层是如果同服务端通讯的。
简介:
NacosNamingService 底层的服务注册、下线服务及服务查询等功能都是通过NamingProxy以api请求的形式与Nacos Server 完成交互。NamingProxy 类核心功能是完成不同请求接口的参数拼装以及请求处理流程。
属性说明
public class NamingProxy {
//默认的nacos Server 服务端口
private static final int DEFAULT_SERVER_PORT = 8848;
private int serverPort = DEFAULT_SERVER_PORT;
//可以表示环境信息
private String namespaceId;
//远程拉取serverList的地址
private String endpoint;
//其中某个 nacos Server的地址
private String nacosDomain;
//配置文件中配置的serverList
private List<String> serverList;
//从远程服务为拉取的nacos server 列表
private List<String> serversFromEndpoint = new ArrayList<String>();
//最近一次拉取serverList时间
private long lastSrvRefTime = 0L;
//隔多久拉取一次远程服务列表
private long vipSrvRefInterMillis = TimeUnit.SECONDS.toMillis(30);
private Properties properties;
....
}
初始化方法
public class NamingProxy {
....
//namespaceId:命名空间
//endpoint: 拉取远程服务列表的ip:port
//serverList: 本地配置的serverList
public NamingProxy(String namespaceId, String endpoint, String serverList) {
this.namespaceId = namespaceId;
this.endpoint = endpoint;
if (StringUtils.isNotEmpty(serverList)) {
this.serverList = Arrays.asList(serverList.split(","));
if (this.serverList.size() == 1) {
this.nacosDomain = serverList;
}
}
//如果设置了endpoint属性, 定时从远程拉取serverList
initRefreshSrvIfNeed();
}
//每隔30s从远程更新ServerList
private void initRefreshSrvIfNeed() {
if (StringUtils.isEmpty(endpoint)) {
return;
}
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.naming.serverlist.updater");
t.setDaemon(true);
return t;
}
});
//vipSrvRefInterMillis:30s 执行一次
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
refreshSrvIfNeed();
}
}, 0, vipSrvRefInterMillis, TimeUnit.MILLISECONDS);
refreshSrvIfNeed();
}
private void refreshSrvIfNeed() {
try {
//如果本地已经设置了serverList 不从远程拉取serverList
if (!CollectionUtils.isEmpty(serverList)) {
NAMING_LOGGER.debug("server list provided by user: " + serverList);
return;
}
//间隔时间未到
if (System.currentTimeMillis() - lastSrvRefTime < vipSrvRefInterMillis) {
return;
}
//从endpoint属性提供的地址拉取serverList
List<String> list = getServerListFromEndpoint();
if (CollectionUtils.isEmpty(list)) {
throw new Exception("Can not acquire Nacos list");
}
if (!CollectionUtils.isEqualCollection(list, serversFromEndpoint)) {
NAMING_LOGGER.info("[SERVER-LIST] server list is updated: " + list);
}
//存储结果
serversFromEndpoint = list;
//存储最近一次拉取serverList 时间
lastSrvRefTime = System.currentTimeMillis();
} catch (Throwable e) {
NAMING_LOGGER.warn("failed to update server list", e);
}
}
....
}
serverList: 提供了2个种方式获取
1、属性配置 2、提供查询的端点地址
一般小集群或者服务的ip不怎么改变的情况可以直接本地化配置,
如果集群数量比较大或者ip会变化就只能用第二种方式
从远程服务拉取服务列表是通过一个定时任务没隔30s更新一次
服务注册
public class NamingProxy {
...
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
...
final Map<String, String> params = new HashMap<String, String>(9);
//4个通用参数
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, serviceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put("ip", instance.getIp());
params.put("port", String.valueOf(instance.getPort()));
params.put("weight", String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put("healthy", String.valueOf(instance.isHealthy()));
params.put("ephemeral", String.valueOf(instance.isEphemeral()));
params.put("metadata", JSON.toJSONString(instance.getMetadata()));
reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);
}
....
}
ephemeral: 代表是否是临时节点[可以随时上下线的节点] 一般我们的服务注册的都是临时节点 相对的还有一种持久化的节点 这种一般我们得不多,后面讲到服务端代码时再作说明
注册服务的参数包括: 命名空间 服务名 分组名 集群名 ip port 权重 是否可用 是否健康 是否临时节点 扩展数据
Instance 对象在初始化时候 下面的属性默认值情况如下:
weight = 1.0;
healthy = true;
enabled = true;
ephemeral = true;cluster=DEFAULT
//底层API调用方法
public String reqAPI(String api, Map<String, String> params, List<String> servers, String method) {
params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(nacosDomain)) {
throw new IllegalArgumentException("no server available");
}
Exception exception = new Exception();
if (servers != null && !servers.isEmpty()) {
//实际上如果用默认的构造方法底层也是会使用当前时间跟一个cas变量计算的结果
//这里直接用System.currentTimeMillis 性能上会有些优势
Random random = new Random(System.currentTimeMillis());
//随机从服务器中获取一个服务地址
int index = random.nextInt(servers.size());
for (int i = 0; i < servers.size(); i++) {
String server = servers.get(index);
try {
return callServer(api, params, server, method);
} catch (NacosException e) {
exception = e;
NAMING_LOGGER.error("request {} failed.", server, e);
} catch (Exception e) {
exception = e;
NAMING_LOGGER.error("request {} failed.", server, e);
}
//如果失败会逐个服务器做轮询
index = (index + 1) % servers.size();
}
throw new IllegalStateException("failed to req API:" + api + " after all servers(" + servers + ") tried: "
+ exception.getMessage());
}
//如果服务地址列表为空, 就使用nacosDomain地址请求,重试次数3次
for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; i++) {
try {
return callServer(api, params, nacosDomain);
} catch (Exception e) {
exception = e;
NAMING_LOGGER.error("[NA] req api:" + api + " failed, server(" + nacosDomain, e);
}
}
throw new IllegalStateException("failed to req API:/api/" + api + " after all servers(" + servers + ") tried: "
+ exception.getMessage());
}
这里有2点要注意
1、服务会自动fail over 轮询下一个服务器 , 这个有点类似于网关或者代理服务如果服务出现网络异常或者服务不通 会循环重试最多 N 次 <= 服务的次数
2、如果server list 为空 使用兜底的 serverDomain 重试3次请求
public String callServer(String api, Map<String, String> params, String curServer, String method)
throws NacosException {
long start = System.currentTimeMillis();
long end = 0;
// 加上签名数据 阿里云环境下的配置
//私有化部署的话 这快逻辑可以忽略
checkSignature(params);
List<String> headers = builderHeaders();
//如果以http://或者https://开头 直接拼接api路径
//如果么有端口信息则拼上默认端口8848或属性【nacos.naming.exposed.port】指定得端口信息
//否则拼接上http://或者https://
String url;
if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
url = curServer + api;
} else {
if (!curServer.contains(UtilAndComs.SERVER_ADDR_IP_SPLITER)) {
curServer = curServer + UtilAndComs.SERVER_ADDR_IP_SPLITER + serverPort;
}
url = HttpClient.getPrefix() + curServer + api;
}
//底层通过HttpUrlConnection发起请求
HttpClient.HttpResult result = HttpClient.request(url, headers, params, UtilAndComs.ENCODING, method);
end = System.currentTimeMillis();
//基于Prometheus指标收集工具收集数据
MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code))
.observe(end - start);
if (HttpURLConnection.HTTP_OK == result.code) {
return result.content;
}
if (HttpURLConnection.HTTP_NOT_MODIFIED == result.code) {
return StringUtils.EMPTY;
}
throw new NacosException(NacosException.SERVER_ERROR, "failed to req API:"
+ curServer + api + ". code:"
+ result.code + " msg: " + result.content);
}
// 对数据(serviceName+时间戳)使用sk签名 一是身份验证 二是防篡改 个人赶紧这个场景身份验证的含义
// ak 和 sk 阿里云上面的一个云账号名 和 密钥
private void checkSignature(Map<String, String> params) {
String ak = getAccessKey();
String sk = getSecretKey();
params.put("app", AppNameUtils.getAppName());
if (StringUtils.isEmpty(ak) && StringUtils.isEmpty(sk)) {
return;
}
try {
String signData = getSignData(params.get("serviceName"));
String signature = SignUtil.sign(signData, sk);
params.put("signature", signature);
params.put("data", signData);
params.put("ak", ak);
} catch (Exception e) {
e.printStackTrace();
}
}
callServer 方法本质是通过底层HttpClient
其他的服务下线、服务实例查询等接口的通用上面的嗲用逻辑,区别在于请求路径和入参和metho
其他几个需要单独说明的接口如下:
public void updateService(Service service, AbstractSelector selector) throws
NacosException
{
NAMING_LOGGER.info("[UPDATE-SERVICE] {} updating service : {}",
namespaceId, service);
final Map<String, String> params = new HashMap<String, String>(6);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, service.getName());
params.put(CommonParams.GROUP_NAME, service.getGroupName());
params.put("protectThreshold", String.valueOf(service.getProtectThreshold()));
params.put("metadata", JSON.toJSONString(service.getMetadata()));
params.put("selector", JSON.toJSONString(selector));
reqAPI(UtilAndComs.NACOS_URL_SERVICE, params, HttpMethod.PUT);
}
public ListView<String> getServiceList(int pageNo, int pageSize, String groupName,
AbstractSelector selector) throws NacosException {...}
public class ListView<T> {
//当前页的数据
private List<T> data;
//总的数据
private int count;
....
}
updateService 方法根据服务更新protectThreshold、metadata、selector信息。
protectThreshold: 0-1 服务保护阈值
selector:服务实例过滤器
AbstractSelector selector:是服务和实例的筛选条件 后面解析到服务端代码的时候详细介绍。
getServiceList:是一个分页查询 返回服务总数量和当页的数据列表
服务订阅
@Override
public ServiceInfo subscribe(String serviceName, String groupName, String clusters)
throws NacosException {
return queryInstancesOfService(serviceName, groupName, clusters,
pushReceiver.getUdpPort(), false);
}
@Override
public void unsubscribe(String serviceName, String groupName, String clusters)
throws NacosException {
}
@Override
public ServiceInfo queryInstancesOfService(String serviceName, String groupName,
String clusters, int udpPort,
boolean healthyOnly) throws NacosException {
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, NamingUtils.getGroupedName(serviceName,
groupName));
params.put(CLUSTERS_PARAM, clusters);
//这2个参数,作用服务端后面主动推送udp消息到客户端
params.put(UDP_PORT_PARAM, String.valueOf(udpPort));
params.put(CLIENT_IP_PARAM, NetUtils.localIP());
params.put(HEALTHY_ONLY_PARAM, String.valueOf(healthyOnly));
String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params,
HttpMethod.GET);
if (StringUtils.isNotEmpty(result)) {
return JacksonUtils.toObj(result, ServiceInfo.class);
}
return new ServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
clusters);
}
subscribe方法:pushReceiver.getUdpPort() 这个参数是为了后面方便服务端通过udp推送服务变更消息给客户端
unsubscribe方法: 方法体为空说明不提供取消订阅的功能
queryInstancesOfService:返回的【ServiceInfo】对象包装了服务名、组名、集群及集群下的实例列表的信息, 如果没有查询到就创建一个空的【ServiceInfo】对象
心跳发送
public long sendBeat(BeatInfo beatInfo) {
try {
...
Map<String, String> params = new HashMap<String, String>(4);
params.put("beat", JSON.toJSONString(beatInfo));
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat",
params, HttpMethod.PUT);
JSONObject jsonObject = JSON.parseObject(result);
if (jsonObject != null) {
return jsonObject.getLong("clientBeatInterval");
}
}...
return 0L;
}
public boolean serverHealthy() {
try {
String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/operator/metrics", new
HashMap<String, String>(2));
JSONObject json = JSON.parseObject(result);
String serverStatus = json.getString("status");
return "UP".equals(serverStatus);
} catch (Exception e) {
return false;
}
}
public class BeatInfo {
private int port;
private String ip;
private double weight;
private String serviceName;
private String cluster;
private Map<String, String> metadata;
private volatile boolean scheduled;
private volatile long period;
private volatile boolean stopped;
...
}
sendBeat: 发送心跳服务接收一个BeatInfo 参数,包含了Instance实例的基本属性在此不赘述,其他几个新的属性说明如下:
stopped:心跳信息移除标志(移除后不会被执行)
period: 心跳任务多久一行执行
serverHealthy(): 检查Nacos Server的状态
总结
通过本节的学习我们知道了Nacos客户通过java 的HttpURLConnection向服务端发送请求。
NamingProxy 正如其名是服务端功能的代理。