一、介绍
客户端的配置有两种方式来维持,一是客户端主动获取,二是客户端长轮询更新。
1.本地配置文件:本地就已经存在的配置文件
2.本地缓存文件:从服务端获取的保存在了本地(本地生成了文件)
3.cacheData缓存数据:内存中缓存的配置文件数据
1.优先从本地配置中获取
2.本地配置中没有则会去服务端获取
3.服务端异常且异常不是因为鉴权失败,则从本地缓存文件中获取
二、Nacos 获取配置 客户端源码分析
1.先看ConfigExample例子源码
public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "localhost";
String dataId = "test";
String group = "DEFAULT_GROUP";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println("[config content] " + content);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("receive:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
boolean isPublishOk = configService.publishConfig(dataId, group, "content");
System.out.println("[publish result] " + isPublishOk);
Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println("[config content]: " + content);
boolean isRemoveOk = configService.removeConfig(dataId, group);
System.out.println("[delete result]: " + isRemoveOk);
Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println("[config content]: " + content);
Thread.sleep(300000);
}
可以发现Nacos获取配置的实现方法是configService.getConfig(),根据dataId和分组group获取配置信息
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
调用了getConfigInner方法,继续看
// We first try to use local failover content if exists.
// A config content for failover is not created by client program automatically,
// but is maintained by user.
// This is designed for certain scenario like client emergency reboot,
// changing config needed in the same time, while nacos server is down.
String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}",
worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
cr.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor
.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
cr.setEncryptedDataKey(encryptedDataKey);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
翻译下源码的英文注释
(1) 首先尝试使用本地的配置文件内容content
(2) 配置content不是由客户端自动创建的,是用户维护的
2.所以这里的源码,就是先从本地配置获取content
try {
ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
cr.setContent(response.getContent());
cr.setEncryptedDataKey(response.getEncryptedDataKey());
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
继续深入看拉取配置的实现,如果分组group为空,就使用默认分组
public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout, boolean notify)
throws NacosException {
if (StringUtils.isBlank(group)) {
group = Constants.DEFAULT_GROUP;
}
return this.agent.queryConfig(dataId, group, tenant, readTimeout, notify);
}
@Override
public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeouts, boolean notify)
throws NacosException {
RpcClient rpcClient = getOneRunningClient();
if (notify) {
CacheData cacheData = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
if (cacheData != null) {
rpcClient = ensureRpcClient(String.valueOf(cacheData.getTaskId()));
}
}
return queryConfigInner(rpcClient, dataId, group, tenant, readTimeouts, notify);
}
构造客户端请求,调用服务端获取配置,最底层实现是grpc实现调用
@Override
public Response request(Request request, long timeouts) throws NacosException {
Payload grpcRequest = GrpcUtils.convert(request);
ListenableFuture<Payload> requestFuture = grpcFutureServiceStub.request(grpcRequest);
Payload grpcResponse;
try {
if (timeouts <= 0) {
grpcResponse = requestFuture.get();
} else {
grpcResponse = requestFuture.get(timeouts, TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
throw new NacosException(NacosException.SERVER_ERROR, e);
}
return (Response) GrpcUtils.parse(grpcResponse);
}
从requestFuture获取结果grpcResponse
3.如果从服务端还是拉取不到配置,就获取本地快照的content信息
content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}",
worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
}
cr.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor
.getEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant);
cr.setEncryptedDataKey(encryptedDataKey);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
三、Nacos发布配置 客户端源码分析
1.从ConfigExample例子代码可以看出发布配置,调用了publishConfig
boolean isPublishOk = configService.publishConfig(dataId, group, "content");
继续调用如下
@Override
public boolean publishConfig(String dataId, String group, String content) throws NacosException {
return publishConfig(dataId, group, content, ConfigType.getDefaultType().getType());
}
private boolean publishConfigInner(String tenant, String dataId, String group, String tag, String appName,
String betaIps, String content, String type, String casMd5) throws NacosException {
group = blank2defaultGroup(group);
ParamUtils.checkParam(dataId, group, content);
ConfigRequest cr = new ConfigRequest();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
cr.setContent(content);
cr.setType(type);
configFilterChainManager.doFilter(cr, null);
content = cr.getContent();
String encryptedDataKey = cr.getEncryptedDataKey();
return worker
.publishConfig(dataId, group, tenant, appName, tag, betaIps, content, encryptedDataKey, casMd5, type);
}
构造configRequest请求,调用doFilter 执行过滤器逻辑
2.继续调用如下
public boolean publishConfig(String dataId, String group, String tenant, String appName, String tag, String betaIps,
String content, String encryptedDataKey, String casMd5, String type) throws NacosException {
return agent.publishConfig(dataId, group, tenant, appName, tag, betaIps, content, encryptedDataKey, casMd5,
type);
}
构造配置发布请求ConfigPublishRequest ,
@Override
public boolean publishConfig(String dataId, String group, String tenant, String appName, String tag,
String betaIps, String content, String encryptedDataKey, String casMd5, String type)
throws NacosException {
try {
ConfigPublishRequest request = new ConfigPublishRequest(dataId, group, tenant, content);
request.setCasMd5(casMd5);
request.putAdditionalParam(TAG_PARAM, tag);
request.putAdditionalParam(APP_NAME_PARAM, appName);
request.putAdditionalParam(BETAIPS_PARAM, betaIps);
request.putAdditionalParam(TYPE_PARAM, type);
request.putAdditionalParam(ENCRYPTED_DATA_KEY_PARAM, encryptedDataKey == null ? "" : encryptedDataKey);
ConfigPublishResponse response = (ConfigPublishResponse) requestProxy(getOneRunningClient(), request);
if (!response.isSuccess()) {
LOGGER.warn("[{}] [publish-single] fail, dataId={}, group={}, tenant={}, code={}, msg={}",
this.getName(), dataId, group, tenant, response.getErrorCode(), response.getMessage());
return false;
} else {
LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}, config={}", getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
return true;
}
} catch (Exception e) {
LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}",
this.getName(), dataId, group, tenant, "unknown", e.getMessage());
return false;
}
}
3.底层也是调用grpc的请求,代码如下
@Override
public Response request(Request request, long timeouts) throws NacosException {
Payload grpcRequest = GrpcUtils.convert(request);
ListenableFuture<Payload> requestFuture = grpcFutureServiceStub.request(grpcRequest);
Payload grpcResponse;
try {
if (timeouts <= 0) {
grpcResponse = requestFuture.get();
} else {
grpcResponse = requestFuture.get(timeouts, TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
throw new NacosException(NacosException.SERVER_ERROR, e);
}
return (Response) GrpcUtils.parse(grpcResponse);
}
四、Nacos获取配置 服务端源码分析
ConfigController#getConfig
/**
* Get configure board information fail.
*
* @throws ServletException ServletException.
* @throws IOException IOException.
* @throws NacosException NacosException.
*/
@GetMapping
@TpsControl(pointName = "ConfigQuery")
@Secured(action = ActionTypes.READ, signType = SignType.CONFIG)
public void getConfig(HttpServletRequest request, HttpServletResponse response,
@RequestParam("dataId") String dataId, @RequestParam("group") String group,
@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "tag", required = false) String tag)
throws IOException, ServletException, NacosException {
// check tenant
ParamUtils.checkTenant(tenant);
tenant = NamespaceUtil.processNamespaceParameter(tenant);
// check params
ParamUtils.checkParam(dataId, group, "datumId", "content");
ParamUtils.checkParam(tag);
final String clientIp = RequestUtil.getRemoteIp(request);
String isNotify = request.getHeader("notify");
inner.doGetConfig(request, response, dataId, group, tenant, tag, isNotify, clientIp);
}
1.检查参数,然后inner.doGetConfig
/**
* Execute to get config [API V1] or [API V2].
*/
public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group,
String tenant, String tag, String isNotify, String clientIp, boolean isV2) throws IOException {
boolean notify = StringUtils.isNotBlank(isNotify) && Boolean.parseBoolean(isNotify);
String acceptCharset = ENCODE_UTF8;
if (isV2) {
response.setHeader(HttpHeaderConsts.CONTENT_TYPE, MediaType.APPLICATION_JSON);
}
final String groupKey = GroupKey2.getKey(dataId, group, tenant);
String autoTag = request.getHeader(com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG);
String requestIpApp = RequestUtil.getAppName(request);
int lockResult = ConfigCacheService.tryConfigReadLock(groupKey);
CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey);
final String requestIp = RequestUtil.getRemoteIp(request);
if (lockResult > 0 && cacheItem != null) {
try {
long lastModified;
boolean isBeta =
cacheItem.isBeta() && cacheItem.getConfigCacheBeta() != null && cacheItem.getIps4Beta() != null
&& cacheItem.getIps4Beta().contains(clientIp);
final String configType =
(null != cacheItem.getType()) ? cacheItem.getType() : FileTypeEnum.TEXT.getFileType();
response.setHeader(com.alibaba.nacos.api.common.Constants.CONFIG_TYPE, configType);
FileTypeEnum fileTypeEnum = FileTypeEnum.getFileTypeEnumByFileExtensionOrFileType(configType);
String contentTypeHeader = fileTypeEnum.getContentType();
response.setHeader(HttpHeaderConsts.CONTENT_TYPE,
isV2 ? MediaType.APPLICATION_JSON : contentTypeHeader);
String pullEvent;
String content;
String md5;
String encryptedDataKey;
if (isBeta) {
ConfigCache configCacheBeta = cacheItem.getConfigCacheBeta();
pullEvent = ConfigTraceService.PULL_EVENT_BETA;
md5 = configCacheBeta.getMd5(acceptCharset);
lastModified = configCacheBeta.getLastModifiedTs();
encryptedDataKey = configCacheBeta.getEncryptedDataKey();
content = ConfigDiskServiceFactory.getInstance().getBetaContent(dataId, group, tenant);
response.setHeader("isBeta", "true");
} else {
if (StringUtils.isBlank(tag)) {
if (isUseTag(cacheItem, autoTag)) {
ConfigCache configCacheTag = cacheItem.getConfigCacheTags().get(autoTag);
md5 = configCacheTag.getMd5(acceptCharset);
lastModified = configCacheTag.getLastModifiedTs();
encryptedDataKey = configCacheTag.getEncryptedDataKey();
content = ConfigDiskServiceFactory.getInstance()
.getTagContent(dataId, group, tenant, autoTag);
pullEvent = ConfigTraceService.PULL_EVENT_TAG + "-" + autoTag;
response.setHeader(com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG,
URLEncoder.encode(autoTag, StandardCharsets.UTF_8.displayName()));
} else {
pullEvent = ConfigTraceService.PULL_EVENT;
md5 = cacheItem.getConfigCache().getMd5(acceptCharset);
lastModified = cacheItem.getConfigCache().getLastModifiedTs();
encryptedDataKey = cacheItem.getConfigCache().getEncryptedDataKey();
content = ConfigDiskServiceFactory.getInstance().getContent(dataId, group, tenant);
}
} else {
md5 = cacheItem.getTagMd5(tag, acceptCharset);
lastModified = cacheItem.getTagLastModified(tag);
encryptedDataKey = cacheItem.getTagEncryptedDataKey(tag);
content = ConfigDiskServiceFactory.getInstance().getTagContent(dataId, group, tenant, tag);
pullEvent = ConfigTraceService.PULL_EVENT_TAG + "-" + tag;
}
}
if (content == null) {
ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, pullEvent,
ConfigTraceService.PULL_TYPE_NOTFOUND, -1, requestIp, notify, "http");
return get404Result(response, isV2);
}
response.setHeader(Constants.CONTENT_MD5, md5);
// Disable cache.
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setDateHeader("Last-Modified", lastModified);
if (encryptedDataKey != null) {
response.setHeader("Encrypted-Data-Key", encryptedDataKey);
}
PrintWriter out;
Pair<String, String> pair = EncryptionHandler.decryptHandler(dataId, encryptedDataKey, content);
String decryptContent = pair.getSecond();
out = response.getWriter();
if (isV2) {
out.print(JacksonUtils.toJson(Result.success(decryptContent)));
} else {
out.print(decryptContent);
}
out.flush();
out.close();
LogUtil.PULL_CHECK_LOG.warn("{}|{}|{}|{}", groupKey, requestIp, md5, TimeUtils.getCurrentTimeStr());
final long delayed = notify ? -1 : System.currentTimeMillis() - lastModified;
ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, lastModified, pullEvent,
ConfigTraceService.PULL_TYPE_OK, delayed, clientIp, notify, "http");
} finally {
ConfigCacheService.releaseReadLock(groupKey);
}
} else if (lockResult == 0 || cacheItem == null) {
ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, ConfigTraceService.PULL_EVENT,
ConfigTraceService.PULL_TYPE_NOTFOUND, -1, requestIp, notify, "http");
return get404Result(response, isV2);
} else {
PULL_LOG.info("[client-get] clientIp={}, {}, get data during dump", clientIp, groupKey);
return get409Result(response, isV2);
}
return HttpServletResponse.SC_OK + "";
}
2.网上找的流程图,如下
五、客户端长轮询更新
长轮询机制
LongPollingService.addLongPollingClient
这个方法主要做几件事:
1.获取客户端请求的超时时间,减去500ms后赋值给timeout变量。
2.判断isFixedPolling,如果为true,定时任务将会在30s后开始执行,否则在29.5s后开始执行
3.和服务端的数据进行MD5对比,如果发送变化则直接返回
4.如果没有变化则将请求转化为异步请求挂起,然后延迟执行ClientLongPolling线程
在长轮询的延迟执行的时间内,服务端也没闲着,一直在监听配置的变更,一旦有配置变更则发布LocalDataChangeEvent事件,触发事件后则提前响应客户端
总结
配置分三种形态,本地配置文件,本地缓存文件,本地缓存数据
客户端通过主动拉取和长轮询的方式来获取配置以及更新配置
主动拉取的顺序是本地配置文件→服务端→本地缓存文件
客户端长轮询中对比配置不同的方式是对比本地文件与本地缓存数据的MD5
长轮询是在客户端与服务端对比配置不同中发起的,存在不同配置服务端则立刻返回,没有则服务端会保持长连接延迟执行任务(30s左右),这中间服务端一旦有配置变更(LocalDataChangeEvent事件)则会提前响应返回
长轮询在获取到不同的配置后还会遍历这些配置主动拉取一次获取具体配置内容并写入本地缓存文件中
集群模式下,配置怎么共享?集群下服务端就必须用数据库来存储配置文件了