nacos(二) 配置和N皇后位算法

369 阅读6分钟

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个任务DataChangeTaskClientLongPolling, 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);
        }
    }
}