Nacos 能帮助我们发现、配置和管理微服务,提供了一组简单易用的特性集,帮助我们快速实现动态服务发现、服务配置、服务元数据及流量管理。
默认情况下,Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
大概的原理就是Nacos 服务端保存了配置信息,客户端连接到服务端之后,根据 dataID,group可以获取到具体的配置信息,当服务端的配置发生变更时,客户端会收到通知,拿到变更后的最新配置信息。
那么问题来了,这里是服务端推给客户端,还是客户端主动去服务端拉?
既然是配置中心,当然是要从配置中心最重要的一个接口ConfigService
入手,下面看一下它有哪些方法。
public interface ConfigService {
//拉取配置信息
String getConfig(String dataId, String group, long timeoutMs) throws NacosException;
//获取配置信息并注册监听器
String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener) throws NacosException;
//新增一个监听器,当服务端发生数据修改时,客户端会触发监听器回调,推荐使用异步线程
void addListener(String dataId, String group, Listener listener) throws NacosException;
//推送配置信息
boolean publishConfig(String dataId, String group, String content) throws NacosException;
//移除配置信息
boolean removeConfig(String dataId, String group) throws NacosException;
//移除监听器
void removeListener(String dataId, String group, Listener listener);
//获取服务端状态
String getServerStatus();
接着看一下是如何创建这个ConfigService
实例的。找到ConfigFactory
工厂类,可以看出这里是通过反射调用了实现类NacosConfigService
的构造方法来创建ConfigService
实例。
public class ConfigFactory {
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
//...
}
接着看一下这个实现类NacosConfigService
,这里我们只关注与数据同步有关的部分,先看构造方法,具体实现方法等实例化完再看。
public class NacosConfigService implements ConfigService {
//超时时间,看下面的单位应该是30秒
private static final long POST_TIMEOUT = 3000L;
/**
* http agent http代理
*/
private HttpAgent agent;
/**
* longpolling 长轮询
*/
private ClientWorker worker;
//过滤器调用链
private ConfigFilterChainManager configFilterChainManager = new ConfigFilterChainManager();
//构造函数,入参是配置信息
public NacosConfigService(Properties properties) throws NacosException {
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
encode = Constants.ENCODE;
} else {
encode = encodeTmp.trim();
}
initNamespace(properties);
//实例化MetricsHttpAgent对象
//装饰者模式,两个类都实现了HttpAgent接口,MetricsHttpAgent实际也是调用了ServerHttpAgent内部的方法
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
//这个start方法等会再看
agent.start();
//实例化ClientWorker对象
worker = new ClientWorker(agent, configFilterChainManager, properties);
}
下面重点看一下MetricsHttpAgent
、ServerHttpAgent
、ClientWorker
这几个类的实现。
这个类其实只是在ServerHttpAgent
的基础上做了一层修饰,看起来应该是加了监控之类的东西,重点的实现还是在ServerHttpAgent
,所以这个类不多讲,直接看ServerHttpAgent
。
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
这个类重点看start
、httpGet
、httpPost
、httpDelete
这几个方法。
调用ServerListManager
里的start
方法
public synchronized void start() throws NacosException {
serverListMgr.start();
}
启动了一个间隔30秒的定时任务,接着看一下线程池里面做了什么。
public synchronized void start() throws NacosException {
if (isStarted || isFixed) {
return;
}
GetServerListTask getServersTask = new GetServerListTask(addressServerUrl);
//省略
TimerService.scheduleWithFixedDelay(getServersTask, 0L, 30L, TimeUnit.SECONDS);
isStarted = true;
}
其实很简单,就是去拿Nacos服务的ip地址列表,如果跟之前的不一样就更新,一样就直接返回。
class GetServerListTask implements Runnable {
@Override
public void run() {
/**
* get serverlist from nameserver
*/
try {
updateIfChanged(getApacheServerList(url, name));
} catch (Exception e) {
LOGGER.error("[" + name + "][update-serverlist] failed to update serverlist from address server!",
e);
}
}
}
可以理解为其实就是一个get请求。
可以理解为其实就是一个post请求。
可以理解为其实就是一个delete请求。
这个类是重点,先看一下构造方法,创建了两个线程池,有一个是间隔10毫秒执行一次。
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
//启动线程池,间隔10毫秒执行一次,相当于是一直执行了
executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
接着看一下线程池里面的方法checkConfigInfo
,取出一批的方法,交给executorService
线程池去执行。
public void checkConfigInfo() {
// 分任务
int listenerSize = cacheMap.get().size();
// 向上取整为批数
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
这里最重点的就是看这个LongPollingRunnable
内部类,先说明一下,考虑到服务端故障的问题,客户端会将每次拉取到的最新数据保存在本地的 snapshot 文件中,以后会优先从文件中获取配置信息的值。
这里面有两个check
,一个是检查本地缓存,从注释check failover config
来看,本地缓存应该是用来做故障转移的,假如服务端挂了,还可以获取本地文件的配置。另一个就是检查服务端最新的配置信息,将最新的配置信息更新到缓存。
// check failover config
for (CacheData cacheData : cacheMap.get().values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
看一下检查本地缓存的checkLocalConfig
方法。
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
String md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn("[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
return;
}
if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
cacheData.setUseLocalConfigInfo(false);
LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
dataId, group, tenant);
return;
}
if (cacheData.isUseLocalConfigInfo() && path.exists()
&& cacheData.getLocalConfigInfoVersion() != path.lastModified()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
String md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn("[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
}
接着往下,从上面设置的cacheData.setUseLocalConfigInfo
可以看出,如果缓存有数据,需要启动Md5值的监听器,具体没细看,应该是比较md5值有没有变化。
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
从服务端拿到配置信息,更新到缓存中,然后看一下是怎么从服务端拿数据的。
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
先看checkUpdateDataIds
方法,大概就是首次加载的时候会先初始化缓存。
/**
* 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
*/
List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws IOException {
StringBuilder sb = new StringBuilder();
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isUseLocalConfigInfo()) {
sb.append(cacheData.dataId).append(WORD_SEPARATOR);
sb.append(cacheData.group).append(WORD_SEPARATOR);
if (StringUtils.isBlank(cacheData.tenant)) {
sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
} else {
sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
}
if (cacheData.isInitializing()) {
// cacheData 首次出现在cacheMap中&首次check更新
inInitializingCacheList
.add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
}
}
}
boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}
然后看checkUpdateConfigStr
方法,发送一个长轮询的请求到服务端,监听服务端的变化。
/**
* 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
*/
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
List<String> params = new ArrayList<String>(2);
params.add(Constants.PROBE_MODIFY_REQUEST);
params.add(probeUpdateString);
List<String> headers = new ArrayList<String>(2);
headers.add("Long-Pulling-Timeout");
headers.add("" + timeout);
// told server do not hang me up if new initializing cacheData added in
if (isInitializingCacheList) {
headers.add("Long-Pulling-Timeout-No-Hangup");
headers.add("true");
}
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
}
try {
// In order to prevent the server from handling the delay of the client's long task,
// increase the client's read timeout to avoid this problem.
long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
agent.getEncode(), readTimeoutMs);
if (HttpURLConnection.HTTP_OK == result.code) {
setHealthServer(true);
return parseUpdateDataIdResponse(result.content);
} else {
setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
}
} catch (IOException e) {
setHealthServer(false);
LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
throw e;
}
return Collections.emptyList();
}
如果服务端的配置发生变化,则去服务端拉取数据。
String[] ct = getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(ct[0]);
if (null != ct[1]) {
cache.setType(ct[1]);
}
通过前面的agent.httpGet
发起http请求拿到配置信息。
public static final String CONFIG_CONTROLLER_PATH = BASE_PATH + "/configs";
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
//省略
}
成功拉取到配置信息之后,保存到本地的snapshot文件。
case HttpURLConnection.HTTP_OK:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.content);
走到这里,实例化NacosConfigService
的流程就走完了,接着看它实现的几个方法。
优先从本地缓存中获取配置,如果为空,则从服务端拉取配置,具体拉取配置的方法是getServerConfig
,上面服务端检查那里讲过了。
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
// 优先使用本地配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
//若本地配置为空,则从服务端拉取配置信息
try {
String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
} catch (NacosException ioe) {
if (NacosException.NO_RIGHT == ioe.getErrCode()) {
throw ioe;
}
LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
agent.getName(), dataId, group, tenant, ioe.toString());
}
}
剩下的几个方法不是本次讲解的重点,了解一下大概的用法就行了。
拉取配置并增加监听器
把配置通过httpPost发送到服务端,更新配置。
把配置通过httpDelete发送到服务端,删除配置。
增加监听器
移除监听器
看完整个流程,可以回答前面提的那个问题,客户端是以拉的形式更新配置的,基于http长轮询实现的。
客户端通过一个定时任务来检查自己监听的配置项的数据,一旦服务端的数据发生变化时,客户端将会获取到最新的数据,并将最新的数据保存在一个 CacheData 对象中,然后会重新计算 CacheData 的 md5 属性的值,此时就会对该 CacheData 所绑定的 Listener 触发 receiveConfigInfo 回调。