Apollo源码解析——客户端通知配置变化(客户端长轮询请求接口)


一、概述

在之前讲解配置发布的时候,NotificationControllerV2 得到配置发布的 AppId+Cluster+Namespace 后,会通知对应的客户端 ,实现在handleMessage方法中具体的实现流程是:

  1. 客户端发起长轮询请求
  2. NotificationControllerV2不会立即返回结果,而是通过Spring Deferredresult把请求挂起。
  3. 如果60s没有客户端关心的配置发布,那么就会返回403给客户端
  4. 如果有客户端关心的配置,NotificationControllerV2会调用DeferredresultsetResult方法,传入有配置变化的namespace信息,同时请求会立即返回,然后客户端从返回的结果中拿到配置变化的namespace,会立即请求Config Service获取namespace的最新配置

二.代码流程

1. NotificationControllerV2#pollNotification方法

这里代码很长,但是逻辑很简单,首先就是把各种映射关系保存,然后Watch住,等待配置发生变化后的通知,如果有新的通知,设置结果结束长轮询,没新的通知,注册到DeferredResult中,等待有配置变更或超时。

   @GetMapping
    public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> pollNotification(
            @RequestParam(value = "appId") String appId,
            @RequestParam(value = "cluster") String cluster,
            @RequestParam(value = "notifications") String notificationsAsString,
            @RequestParam(value = "dataCenter", required = false) String dataCenter,
            @RequestParam(value = "ip", required = false) String clientIp) {
        //解析 notificationsAsString 参数,创建 ApolloConfigNotification 数组
        List<ApolloConfigNotification> notifications = null;

        try {
            notifications =
                    gson.fromJson(notificationsAsString, notificationsTypeReference);
        } catch (Throwable ex) {
            Tracer.logError(ex);
        }

        if (CollectionUtils.isEmpty(notifications)) {
            throw new BadRequestException("Invalid format of notifications: " + notificationsAsString);
        }

        // 过滤并创建 ApolloConfigNotification Map
        Map<String, ApolloConfigNotification> filteredNotifications = filterNotifications(appId, notifications);

        if (CollectionUtils.isEmpty(filteredNotifications)) {
            throw new BadRequestException("Invalid format of notifications: " + notificationsAsString);
        }

        // 创建 DeferredResultWrapper 对象
        DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper(bizConfig.longPollingTimeoutInMilli());
        // Namespace 集合
        Set<String> namespaces = Sets.newHashSetWithExpectedSize(filteredNotifications.size());
        // 客户端的通知 ma 。 key 为 Namespace 名, value 为通知编号
        Map<String, Long> clientSideNotifications = Maps.newHashMapWithExpectedSize(filteredNotifications.size());

        // 循环 ApolloConfigNotification Map。初始化上述变量
        for (Map.Entry<String, ApolloConfigNotification> notificationEntry : filteredNotifications.entrySet()) {
            String normalizedNamespace = notificationEntry.getKey();
            ApolloConfigNotification notification = notificationEntry.getValue();
            // 添加到 `namespace` 中
            namespaces.add(normalizedNamespace);
            // 添加到  `clientSideNotifications` 中
            clientSideNotifications.put(normalizedNamespace, notification.getNotificationId());
            // 记录名字被归一化的 Namespace 。 因为,最终返回给客户端,使用原始的 Namespace 名字,否则客户端无法识别
            if (!Objects.equals(notification.getNamespaceName(), normalizedNamespace)) {
                deferredResultWrapper.recordNamespaceNameNormalizedResult(notification.getNamespaceName(), normalizedNamespace);
            }
        }

        // 组装 Watch Key Multimap
        Multimap<String, String> watchedKeysMap =
                watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter);

        // 生成 Watch Key 集合
        Set<String> watchedKeys = Sets.newHashSet(watchedKeysMap.values());

        /**
         * 1、set deferredResult before the check, for avoid more waiting
         * If the check before setting deferredResult,it may receive a notification the next time
         * when method handleMessage is executed between check and set deferredResult.
         */
        //注册超时事件
        deferredResultWrapper
                .onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys"));

        //注册结束事件
        deferredResultWrapper.onCompletion(() -> {
            //unregister all keys
            // 移除 Watch Key + DeferredResultWrapper 出 `deferredResults`
            //
            for (String key : watchedKeys) {
                deferredResults.remove(key, deferredResultWrapper);
            }
            logWatchedKeys(watchedKeys, "Apollo.LongPoll.CompletedKeys");
        });

        //register all keys
        //注册 Watch Key + DeferredResultWrapper 到 `deferredResults` 中,等待配置发生变化后通知
        for (String key : watchedKeys) {
            this.deferredResults.put(key, deferredResultWrapper);
        }

        logWatchedKeys(watchedKeys, "Apollo.LongPoll.RegisteredKeys");
        logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
                watchedKeys, appId, cluster, namespaces, dataCenter);

        /**
         * 2、check new release
         */
        //获得 Watch Key 集合中,每个 Watch Key 对应的 ReleaseMessage 记录
        List<ReleaseMessage> latestReleaseMessages =
                releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys);

        /**
         * Manually close the entity manager.
         * Since for async request, Spring won't do so until the request is finished,
         * which is unacceptable since we are doing long polling - means the db connection would be hold
         * for a very long time
         */
        //手动关闭实体管理器。由于对于异步请求,Spring 在请求完成之前不会这样做,这是不可接受的,因为我们正在进行长时间轮询 - 意味着数据库连接将保持很长时间
        entityManagerUtil.closeEntityManager();

        //获取配置更新的结果
        List<ApolloConfigNotification> newNotifications =
                getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap,
                        latestReleaseMessages);

        //若有新的通知,直接设置 DeferredResult 的结果,从而结束常轮训
        if (!CollectionUtils.isEmpty(newNotifications)) {
            deferredResultWrapper.setResult(newNotifications);
        }

        return deferredResultWrapper.getResult();
    }
  1. NotificationControllerV2#handleMessage方法

在之前我们梳理过,当配置变更的时候会发送消息,通知对应的监听者,然后就会调用到NotificationControllerV2HandleMessage方法,这里逻辑也很简单,从deferredResults中获取所有监听这个消息的客户端,然后遍历数组,异步通知对应的客户端。这里有个比较设计好的点就是,当通知的客户端比较多,使用线程池异步通知,为了避免惊群效应,会每推送N个客户端,sleep一段时间,假设一个公共Namespace10w台机器,那么当下发配置变更消息的时候,就导致这10w台机器都来请求配置,惊群效应,对Config Service的压力也比较大。

    public void handleMessage(ReleaseMessage message, String channel) {
        logger.info("message received - channel: {}, message: {}", channel, message);

        String content = message.getMessage();
        Tracer.logEvent("Apollo.LongPoll.Messages", content);
        // 仅处理 APOLLO_RELEASE_TOPIC
        if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
            return;
        }

        // 获得对应的 Namespace 名字
        String changedNamespace = retrieveNamespaceFromReleaseMessage.apply(content);

        if (Strings.isNullOrEmpty(changedNamespace)) {
            logger.error("message format invalid - {}", content);
            return;
        }

        if (!deferredResults.containsKey(content)) {
            return;
        }

        //create a new list to avoid ConcurrentModificationException
        // 创建 DeferredResultWrapper 数组,避免并发问题
        List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get(content));

        // 创建 ApolloConfigNotification 对象
        ApolloConfigNotification configNotification = new ApolloConfigNotification(changedNamespace, message.getId());
        configNotification.addMessage(content, message.getId());

        //do async notification if too many clients
        // 若需要通知的客户端过多,使用 ExecutorService 异步通知,避免`惊群效应`
        if (results.size() > bizConfig.releaseMessageNotificationBatch()) {
            largeNotificationBatchExecutorService.submit(() -> {
                logger.debug("Async notify {} clients for key {} with batch {}", results.size(), content,
                        bizConfig.releaseMessageNotificationBatch());
                for (int i = 0; i < results.size(); i++) {
                    // 每 N 个K客户端,Sleep 一段事件
                    if (i > 0 && i % bizConfig.releaseMessageNotificationBatch() == 0) {
                        try {
                            TimeUnit.MILLISECONDS.sleep(bizConfig.releaseMessageNotificationBatchIntervalInMilli());
                        } catch (InterruptedException e) {
                            //ignore
                        }
                    }
                    logger.debug("Async notify {}", results.get(i));
                    // 设置结果
                    results.get(i).setResult(configNotification);
                }
            });
            return;
        }

        logger.debug("Notify {} clients for key {}", results.size(), content);

        for (DeferredResultWrapper result : results) {
            result.setResult(configNotification);
        }
        logger.debug("Notification completed");
    }

3. AppNamespaceServiceWithCache

这个类将AppNamespace缓存在内存,提高ConfigService的查询性能,这个适合我们借鉴下,缓存方式如下:

  1. 启动时,全量初始化 AppNamespace 到缓存
  2. 考虑AppnNamespace新增,后台定时任务,定期增量初始化AppNamespace到缓存
  3. 考虑AppNamespace更新与删除,后台定时更新,定时全量重建AppNamespace到缓存

4. AppNamespaceServiceWithCache#afterPropertiesSet方法

首先从db加载元数据到缓存,然后启动两个定时任务,一个执行全量重建,一个进行增量初始化。

public void afterPropertiesSet() throws Exception {
    // 从 ServerConfig 中,读取定时任务的周期配置
    populateDataBaseInterval();
    // 全量初始化 AppNamespace 缓存
    scanNewAppNamespaces(); //block the startup process until load finished
    // 创建定时任务,全量重构 AppNamespace 缓存
    scheduledExecutorService.scheduleAtFixedRate(() -> {
      Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache",
              "rebuildCache");
      try {
        // 全量重建 AppNamespace缓存
        this.updateAndDeleteCache();
        transaction.setStatus(Transaction.SUCCESS);
      } catch (Throwable ex) {
        transaction.setStatus(ex);
        logger.error("Rebuild cache failed", ex);
      } finally {
        transaction.complete();
      }
    }, rebuildInterval, rebuildInterval, rebuildIntervalTimeUnit);
            //创建定时任务,增量初始化 AppNamespace 缓存
    scheduledExecutorService.scheduleWithFixedDelay(this::scanNewAppNamespaces, scanInterval,
        scanInterval, scanIntervalTimeUnit);
  }
  1. updateAndDeleteCache方法

全量重建,更新和删除AppNamespace缓存,从数据库中查询最新的AppNamespace信息,然后处理更新和删除的情况

  private void updateAndDeleteCache() {
    // 从缓存中获得所有的 AppNamespace 编号集合
    List<Long> ids = Lists.newArrayList(appNamespaceIdCache.keySet());
    if (CollectionUtils.isEmpty(ids)) {
      return;
    }
    // 每 500 一批,从数据库中查询最新的 AppNamespace 信息
    List<List<Long>> partitionIds = Lists.partition(ids, 500);
    for (List<Long> toRebuild : partitionIds) {
      Iterable<AppNamespace> appNamespaces = appNamespaceRepository.findAllById(toRebuild);

      if (appNamespaces == null) {
        continue;
      }

      //handle updated
      //处理更新的情况
      Set<Long> foundIds = handleUpdatedAppNamespaces(appNamespaces);

      //handle deleted
      // 处理删除的情况
      handleDeletedAppNamespaces(Sets.difference(Sets.newHashSet(toRebuild), foundIds));
    }
  }
  1. handleUpdatedAppNamespaces方法

处理AppNamespace更新的情况,这个很简单,就是使用最新的覆盖缓存中的

  private Set<Long> handleUpdatedAppNamespaces(Iterable<AppNamespace> appNamespaces) {
    Set<Long> foundIds = Sets.newHashSet();
    for (AppNamespace appNamespace : appNamespaces) {
      foundIds.add(appNamespace.getId());
      // 获得缓存中的 AppNamespace 对象
      AppNamespace thatInCache = appNamespaceIdCache.get(appNamespace.getId());
      // 从 DB 中查询到的 AppNamespace 的更新时间更大,才认为是更新
      if (thatInCache != null && appNamespace.getDataChangeLastModifiedTime().after(thatInCache
          .getDataChangeLastModifiedTime())) {
        // 添加到 appNamespaceIdCache 中
        appNamespaceIdCache.put(appNamespace.getId(), appNamespace);
        String oldKey = assembleAppNamespaceKey(thatInCache);
        String newKey = assembleAppNamespaceKey(appNamespace);
        // 添加到 appNamespaceCache 中
        appNamespaceCache.put(newKey, appNamespace);

        //in case appId or namespaceName changes
        // 当 appId 或 namespaceName发生改变时,将老的移出去,
        if (!newKey.equals(oldKey)) {
          appNamespaceCache.remove(oldKey);
        }

        //添加到 publishAppNamespaceCache 中
        if (appNamespace.isPublic()) { //新的是共用类型
          // 添加到 publicAppNamespaceCache 中
          publicAppNamespaceCache.put(appNamespace.getName(), appNamespace);

          //in case namespaceName changes
          // 当 namespaceName 发生改变的情况,将老的移除publicAppNamespaceCache
          if (!appNamespace.getName().equals(thatInCache.getName()) && thatInCache.isPublic()) {
            publicAppNamespaceCache.remove(thatInCache.getName());
          }
        } else if (thatInCache.isPublic()) {
          //just in case isPublic changes
          publicAppNamespaceCache.remove(thatInCache.getName());
        }
        logger.info("Found AppNamespace changes, old: {}, new: {}", thatInCache, appNamespace);
      }
    }
    return foundIds;
  }
  1. handleDeletedAppNamespaces处理删除的情况

这里就是直接从缓存中移除对应的AppNamespace

 private void handleDeletedAppNamespaces(Set<Long> deletedIds) {
    if (CollectionUtils.isEmpty(deletedIds)) {
      return;
    }
    for (Long deletedId : deletedIds) {
      // 从 appNamespaceIdCache 中移除
      AppNamespace deleted = appNamespaceIdCache.remove(deletedId);
      if (deleted == null) {
        continue;
      }
      //从 appNamespaceCache 中移除
      appNamespaceCache.remove(assembleAppNamespaceKey(deleted));
      if (deleted.isPublic()) {
        //从 publicAppNamespaceCache 中移除
        AppNamespace publicAppNamespace = publicAppNamespaceCache.get(deleted.getName());
        // in case there is some dirty data, e.g. public namespace deleted in some app and now created in another app
        if (publicAppNamespace == deleted) {
          publicAppNamespaceCache.remove(deleted.getName());
        }
      }
      logger.info("Found AppNamespace deleted, {}", deleted);
    }
  }
  1. 增量更新scanNewAppNamespaces方法
  private void scanNewAppNamespaces() {
    Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache",
        "scanNewAppNamespaces");
    try {
      // 加载新的 AppNamespace 们
      this.loadNewAppNamespaces();
      transaction.setStatus(Transaction.SUCCESS);
    } catch (Throwable ex) {
      transaction.setStatus(ex);
      logger.error("Load new app namespaces failed", ex);
    } finally {
      transaction.complete();
    }
  }
  1. loadNewAppNamespaces方法

拉取最新的数据,然后合并到缓存中

  private void loadNewAppNamespaces() {
    boolean hasMore = true;
    while (hasMore && !Thread.currentThread().isInterrupted()) {
      //current batch is 500
      //获取大于 maxIdScanned 的 500 条 Appnamespace 记录,按照 id 升序
      List<AppNamespace> appNamespaces = appNamespaceRepository
          .findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
      if (CollectionUtils.isEmpty(appNamespaces)) {
        break;
      }
      // 合并到 AppNamespace 缓存中
      mergeAppNamespaces(appNamespaces);
      // 获得新的 maxIdScanned , 取最后一条记录
      int scanned = appNamespaces.size();
      maxIdScanned = appNamespaces.get(scanned - 1).getId();
      //若拉取不足 500 条,说明无新消息了
      hasMore = scanned == 500;
      logger.info("Loaded {} new app namespaces with startId {}", scanned, maxIdScanned);
    }
  }
  1. mergeAppNamespaces方法

这里就是使用最新的覆盖缓存中原有的

  private void mergeAppNamespaces(List<AppNamespace> appNamespaces) {
    for (AppNamespace appNamespace : appNamespaces) {
      // 添加到 'appNamespaceCache' 中
      appNamespaceCache.put(assembleAppNamespaceKey(appNamespace), appNamespace);
      // 添加到  'appNamespaceIdCache'
      appNamespaceIdCache.put(appNamespace.getId(), appNamespace);
      // 若是公用类型,则添加到 'publicAppNamespaceCache' 中
      if (appNamespace.isPublic()) {
        publicAppNamespaceCache.put(appNamespace.getName(), appNamespace);
      }
    }
  }

  1. ReleaseMessageServiceWithCache

缓存ReleaseMessage的service实现累,通过将ReleaseMessage缓存在内存中,提高查询性能,缓存方式如下:

  1. 启动时,初始化ReleaseMessage 到缓存
  2. 洗澡能时候,基于 ReleaseMessageListener,通知有新的ReleaseMessage,根据是否有消息间隙,直接使用该消息还是从数据库读取
  1. ReleaseMessageServiceWithCache#afterPropertiesSet方法

在初始化方法中,首先会初始拉取Release Message到缓存,然后又创建了一个定时任务增量拉取消息到缓存,用以处理初始化阶段消息遗漏问题,至于为什么会遗漏,场景如下:

  1. 20:00:00 程序启动过程中,当前 release message 有 5 条
  2. 20:00:01 loadReleaseMessages(0); 执行完成,获取到 5 条记录
  3. 20:00:02 有一条 release message 新产生,但是因为程序还没启动完,所以不会触发 handle message 操作
  4. 20:00:05 程序启动完成,但是第三步的这条新的 release message 漏了
  5. 20:10:00 假设这时又有一条 release message 产生,这次会触发 handle message ,同时会把第三步的那条 release message 加载到
    所以,定期刷的机制就是为了解决第三步中产生的release message问题。
    当程序启动完,handleMessage生效后,就不需要再定期扫了
  public void afterPropertiesSet() throws Exception {
    //从 ServiceConfig 中,读取任务的周期配置
    populateDataBaseInterval();
    //block the startup process until load finished
    //this should happen before ReleaseMessageScanner due to autowire
    // 初始拉取 ReleaseMessage 到缓存
    loadReleaseMessages(0);

    // 创建定时任务,增量拉取 ReleaseMessage 到缓存,用以处理初始化期间,产生的 ReleaseMessage 遗漏的问题
    executorService.submit(() -> {
      while (doScan.get() && !Thread.currentThread().isInterrupted()) {
        Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageServiceWithCache",
            "scanNewReleaseMessages");
        try {
          // 增量拉取 ReleaseMessage 到缓存
          loadReleaseMessages(maxIdScanned);
          transaction.setStatus(Transaction.SUCCESS);
        } catch (Throwable ex) {
          transaction.setStatus(ex);
          logger.error("Scan new release messages failed", ex);
        } finally {
          transaction.complete();
        }
        try {
          scanIntervalTimeUnit.sleep(scanInterval);
        } catch (InterruptedException e) {
          //ignore
        }
      }
    });
  }
  1. ReleaseMessageServiceWithCache#loadReleaseMessages方法

初始化,全量拉取元数据到缓存中

  private void loadReleaseMessages(long startId) {
    boolean hasMore = true;
    while (hasMore && !Thread.currentThread().isInterrupted()) {
      //current batch is 500
      // 获得大于 maxIdScanned 的 500 条 ReleaseMessage 记录,按照 id 升序
      List<ReleaseMessage> releaseMessages = releaseMessageRepository
          .findFirst500ByIdGreaterThanOrderByIdAsc(startId);
      if (CollectionUtils.isEmpty(releaseMessages)) {
        break;
      }
      // 合并到 ReleaseMessage 缓存
      releaseMessages.forEach(this::mergeReleaseMessage);
      int scanned = releaseMessages.size();
      // 获取新的 maxIdScanned ,取最后一条记录
      startId = releaseMessages.get(scanned - 1).getId();
      // 若拉取不足 500 条,说明无新消息了
      hasMore = scanned == 500;
      logger.info("Loaded {} release messages with startId {}", scanned, startId);
    }
  }
  1. ReleaseMessageServiceWithCache#handleMessage方法

这里首先会关闭增量拉取的定时任务,后续通过 ReleaseMessageScanner 通知即可,然后判断消息和前一条收到的消息之间有无间隙,没有直接合并,有说明,有遗漏,增量拉取。

 public void handleMessage(ReleaseMessage message, String channel) {
    //Could stop once the ReleaseMessageScanner starts to work
    // 关闭增量拉取定时任务的执行
    doScan.set(false);
    logger.info("message received - channel: {}, message: {}", channel, message);

    // 仅处理 APOLLO_RELEASE_TOPIC
    String content = message.getMessage();
    Tracer.logEvent("Apollo.ReleaseMessageService.UpdateCache", String.valueOf(message.getId()));
    if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
      return;
    }

    //计算 gap
    long gap = message.getId() - maxIdScanned;
    // 若无空缺,直接合并
    if (gap == 1) {
      mergeReleaseMessage(message);
      //若有空缺 gap ,增量拉取
    } else if (gap > 1) {
      //gap found!
      loadReleaseMessages(maxIdScanned);
    }
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值