Nacos 配置中心
nacos的配置中心,ConfigService为核心类。首先作为一个配置中心,需要解决2个问题
第一是如何动态感知配置文件的变更
第二是如何判断配置文件的变更
从理论上讲,在nacos中,采用长轮询来动态感知变更,判断配置文件的变更,加入文件中的配置较多,那比较和传输都是较大的压力,这里采用md5的方法,把文件压缩成md5,然后与客户端中的md5比较,是否相等,如果不相等,就返回具体的配置。在这里nacos也有属于自己的优化,利用多线程来变更比较配置,类似于CHM中的扩容,多个线程参与扩容。
长轮询
长连接都知道是一直保持连接,长轮询也是一样,在收到轮询的请求时,服务端保持住该轮询的请求一断时间,在时间范围内,如果产生了变化,那么马上返回。nacos 是30秒
客户端
知道大概的逻辑后,看到源码中ConfigServiceAPI,实现类为NacosConfigService,首先来看构造参数
public NacosConfigService(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
this.encode = Constants.ENCODE;
} else {
this.encode = encodeTmp.trim();
}
initNamespace(properties);
// agent 远程同学的代理.
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
// 这里ClientWorker为具体的操作方法,俗称“打工人”
转到ClientWorker中,初始化了2个线程池,一个是长轮询的定时调度线程,一个是用来拆分更新配置,使更新速度更快。可以理解为假如6000个key,在nacos中会有2个线程,各自3000个key去维护更新。
PS:这里我觉得nacos的源码有点小精分,同样是为了到达一个目的,周期性的调度,在服务发现那边,采用了
ScheduledExecutorService#schedule(java.lang.Runnable, long, java.util.concurrent.TimeUnit)表示延迟多久后执行一次任务,同时在任务的最后,在加入一个任务到线程池中。而这里直接使用了ScheduledExecutorService#scheduleWithFixedDelay这个api,最后的目的和效果是一样的,可能会在后面的版本中统一吧
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
init(properties);
// 初始化定时调度的线程池
this.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;
}
});
// 初始化长轮询的线程池 主要是为了拆分任务,
this.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;
}
});
// 设置定时执行任务,检查配置信息
this.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);
}
具体在线程池中的任务
public void checkConfigInfo() {
// Dispatch taskes.
// 任务的拆分 (CHM)
int listenerSize = cacheMap.get().size();
// Round up the longingTaskCount.这里就是按照3000个分配一个任务分配
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// The task list is no order.So it maybe has issues when changing.
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
继续通过LongPollingRunnable 来看
// LongPollingRunnable#run中有如下一个代码,可以看到在这里可以看到保持长轮询的实现是通过request这一方定义一个30秒的超时时间,来控制
...
String[] ct = getServerConfig(dataId, group, tenant, 3000L);
之后的代码就不累述了,比较深有兴趣可以慢慢看,大致思路为上面那样,分批去轮询,通过md5比较。
通过查询/v1/cs//listener 接口比较md5,然后如果有差异最后通过/v1/cs/configs接口获取到服务端配置
服务端
先看下图的流程
可以看到在到达服务端后,封装一个ClientLongPolling,放入队列中,同时我们修改配置文件后,会有一个DataChangeTask任务生成,会把产生变化的配置的那一部分,找出所有的监听的客户端,放入
LongPollingService#retainIps的map中,ClientLongPolling从map中获取返回。
入口函数为ConfigController#listener,调用到LongPollingService#addLongPollingClient添加LongPollingClient到队列中
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
String tag = req.getHeader("Vipserver-Tag");
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout. 这里对应了我们上图中最后0.5s的检查期,这一段是为了保证在网络传输过程中开销的时间
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
// Do nothing but set fix polling timeout.
} else {
// 到这里就是立即返回,不进行长轮询
long start = System.currentTimeMillis();
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
String ip = RequestUtil.getRemoteIp(req);
// Must be called by http thread, or send response.
// Servlet 3.0的异步处理支持特性,可以先释放容器分配给请求的线程与相关资源,减轻系统负担,原先释放了容器所分配线程的请求,其响应将被延后,可以在处理完成(例如长时间运算完成、所需资源已获得)时再对客户端进行响应
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout() is incorrect, Control by oneself
asyncContext.setTimeout(0L);
// 线程池任务,结果在此处返回
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
接下来看2个任务DataChangeTask 和ClientLongPolling,
DataChangeTask#run 方法如下
try {
ConfigCacheService.getContentBetaMd5(groupKey);
// 遍历所有订阅的客户端
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
// 得到订阅这一部分数据的客户端
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// If published tag is not in the beta list, then it skipped.
if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
continue;
}
// If published tag is not in the tag list, then it skipped.
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
// 放入需要通知变更的map中,由另一个任务来从中获取
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // Delete subscribers' relationships.
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
RequestUtil
.getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
"polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
}
ClientLongPolling#run如下
// 把任务丢入另一个线程池延迟执行,时间为timeout,也就是我们得到的29.5S的时间
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
@Override
public void run() {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// Delete subsciber's relations.
allSubs.remove(ClientLongPolling.this);
if (isFixedPolling()) {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
List<String> changedGroups = MD5Util
.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
sendResponse(changedGroups);
} else {
sendResponse(null);
}
} else {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
}
}
}, timeoutTime, TimeUnit.MILLISECONDS);
// 加入订阅客户端
allSubs.add(this);
}
可以看到在nacos中,大量使用了线程池 队列等操作,这些和ZK中的源码类似,ZK中也是同样使用了大量的线程池 队列等一部操作
N皇后
刷题时,突然看到使用位运算解题的一种巧妙的思路,故分享一下
LeetCode 52题
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
首先预备一点位运算的技巧
n&(n-1) 把2进制中的最右侧的1变为0
p = n&-n 把2进制中,除了最右侧的1,都变成0
下面提供解法
class Solution {
private int count = 0;
private int size = 0;
public int totalNQueens(int n) {
size=n;
dfs(0,0,0,0);
return count;
}
public void dfs(int row,int col,int pie,int na){
// count累加的条件,row到达n
if(row >= size){
count++;
return;
}
/*** 这里假设n为4
* 第一步传入的为 0000,0000,0000,0000 ,代表在横竖撇捺4个方向都可以放皇后
*(col | pie | na) 的结果就是0000,代表在这一行中都可以放皇后
*到这里,我们需要转换一下,把1作为可以放皇后的,0 不可以放,故需要转换,首先取反得到1111....1111,这里只需要最后的n位,也就是需要4位
*因此 &((1<< size) -1 ) ,就可以得到最后的4位数,也就是1111,代表有4个格子都可以放皇后,那么我们依次来遍历
*先确定偏离的终止条件,每个位都为0时,代表皇后没有地方可以放,就结束了
*bits & (-bits) 这里使用的是把2进制中的最右侧的1变为0,因此变为了1110,故我们将最后一位作为皇后的位置,然后递归,进入下一层,故row+1,同时在下一层的最后一位格子无法放皇后,故下一个col就是col | pos,也就是0001,此时pie和na,分别在最后一个格子的左侧和右侧,故分别为(pie|pos)<<1,(na | pos ) >> 1
*最后bits = bits & (bits - 1);是把2进制中的最后一位1变成0,也是1111 -> 1110,进入下一次的遍历
*
*
*
**/
int bits = (~(col | pie | na)) & ((1<<size) -1);
while(bits > 0){
int pos = bits & (-bits);
dfs(row+1,col | pos,(pie|pos)<<1,(na | pos ) >> 1);
bits = bits & (bits - 1);
}
}
}