Apollo(3) Portal、Config、Admin Service包

  • 这几部分以业务为主,主要关心数据流向和长轮询的实现,没贴太多代码


一、数据库表:
  1. portal DB表:
  • App、AppNamespace
  • 包括普通用户、三方用户、权限、操作记录、收藏等
  • 基础配置,包括生效环境、meta地址、部门、接口超时时间等portal的基础配置
  • 总体来说portalDB主要围绕用户和界面展示相关的
  • protal DB里的App、AppNamespace相关数据都是未生效的,数据放入portal DB后,用spring的发布监听机制,异步把数据同步到各个env的admin DB
  1. config db表:
  • App、Cluster
  • AppNamespace:属于App,1对多,代表用户创建过的配置,不可删除,只用于记录,比如用户创建过叫application和db的AppNamespace,继续往下对比和Namespace的区别
  • Namespace:属于Cluster,1对多,代表正在使用的配置文件,可以删除,在创建新Cluster时下面默认会创建出application、db,创建的依据是上面的AppNamespace表的记录,可以在后台手动试试,但用户想删除某个app cluster下的配置,删的就是Namespace表了
  • commit:提交历史,在执行所有修改操作时增加一条记录,记录修改item操作的增量数据
  • item:所有具体配置项,未发布过的也都记录在这
  • release message:下面写
  • server config:一些服务相关配置,和portal类似
二、一些细节:
  1. cluster不创建就会用default表示,界面上不会显示,但并不是没有
  2. 每创建一个app默认会创建一个叫application的namespace,对应spring项目的application.properties
  3. openApi可以通过在portal界面注册三方用户,用HTTP + Token的方式直接调用portal相关接口,不用登陆portal手动点击配置发布等等操作
  4. namespace公有、私有、关联
  • 私有namespace:app私有的namespace,只能本服务获取到
  • 公有namespace:其他app可以关联app的公有namespace,app私有namespace如果有相同字段,以私有为准
  • 关联namespace:app关联其他公有namespace后会,会在本项目页面显示关联的共有namespace叫“关联namespace”
  1. 关联公有namespace实际是创建了一条新的namespace给相应app
  2. apollo通过appId + clusterName + namespaceName定位一个具体namespace,类似于maven的groupId + artifactId,源码里也叫watchKey
三、修改、发布流程:
  1. 配置修改时,向config db item表记录配置项,commit表增加一条修改记录,到这里修改部分就结束了,主要看发布
  2. 配置发布概述,portal向admin service请求是同步,admin service到config service是异步,为了不引入额外组件,admin service写入数据库,config service轮询数据库推送消息的方式代替了消息队列,通过最新数据的ID作为版本号确认是否有配置更新,所以ID必须自增,就是releaseMessage表
  3. 发布时item和commit表没变化,release、releaseHistory和releaseMessage会增加一条记录
  • release表:记录namespace的定位、namespace配置发布的记录
  • releaseHistory表:记录操作日志,上次releaseId、本次releaseId、定位、操作内容(发布、回滚、灰度相关操作)
  • releaseMessage表:模拟MQ推送,根据namespace定位、ID拉取数据
  1. 发送消息时,releaseMessage的ID会存到一个BlockingQueue里,config service会每5s取一次BlockingQueue的数据取完为止,因为只记录定位,同一namespace多次发布releaseMessage表里会有重复数据,需要定时清理,只保留同一namespace最新一条数据
  2. config service每秒扫描是否有更新,更新时取出遍历所有listener推送Client,这个监听器里触发更新后的一些处理,包括清缓存、通知Client等等,最后更新缓存最大ID记录
    // 初始化ReleaseMessageScanner,加入各种MessageListener
    @Bean
    public ReleaseMessageScanner releaseMessageScanner() {
      ReleaseMessageScanner releaseMessageScanner = new ReleaseMessageScanner();
      //0\. handle release message cache
      releaseMessageScanner.addMessageListener(releaseMessageServiceWithCache);
      //1\. handle gray release rule
      releaseMessageScanner.addMessageListener(grayReleaseRulesHolder);
      //2\. handle server cache
      releaseMessageScanner.addMessageListener(configService);
      releaseMessageScanner.addMessageListener(configFileController);
      //3\. notify clients
      releaseMessageScanner.addMessageListener(notificationControllerV2);
      releaseMessageScanner.addMessageListener(notificationController);
      return releaseMessageScanner;
    }
// 启动定时任务
  @Override
  public void afterPropertiesSet() throws Exception {
    databaseScanInterval = bizConfig.releaseMessageScanIntervalInMilli();
    maxIdScanned = loadLargestMessageId();
    executorService.scheduleWithFixedDelay(() -> {
      Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageScanner", "scanMessage");
      try {
        scanMissingMessages();
        scanMessages();
        transaction.setStatus(Transaction.SUCCESS);
      } catch (Throwable ex) {
        transaction.setStatus(ex);
        logger.error("Scan and send message failed", ex);
      } finally {
        transaction.complete();
      }
    }, databaseScanInterval, databaseScanInterval, TimeUnit.MILLISECONDS);

  }

private boolean scanAndSendMessages() {
    // 一次扫500,多了下次再扫
    List<ReleaseMessage> releaseMessages =
        releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
    if (CollectionUtils.isEmpty(releaseMessages)) {
      return false;
    }
    // 遍历所有注册上来的监听器,通知client
    fireMessageScanned(releaseMessages);
    int messageScanned = releaseMessages.size();
    long newMaxIdScanned = releaseMessages.get(messageScanned - 1).getId();
    // check id gaps, possible reasons are release message not committed yet or already rolled back
    if (newMaxIdScanned - maxIdScanned > messageScanned) {
      recordMissingReleaseMessageIds(releaseMessages, maxIdScanned);
    }
    maxIdScanned = newMaxIdScanned;
    return messageScanned == 500;
  }

/**
   * 遍历所有listener推送message
   * @param messages
   */
  private void fireMessageScanned(Iterable<ReleaseMessage> messages) {
    for (ReleaseMessage message : messages) {
      for (ReleaseMessageListener listener : listeners) {
        try {
          listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC);
        } catch (Throwable ex) {
          Tracer.logError(ex);
          logger.error("Failed to invoke message listener {}", listener.getClass(), ex);
        }
      }
    }
  }
  1. 关联Client章节关于RemoteConfigLongPollService的描述,RemoteConfigLongPollService长轮训/notifications/v2这个接口,触发更新时返回http200,超时返回http304,NotificationControllerV2这个对象触发DeferredResult响应的逻辑就包括在上面提到的监听回调中
  2. NotificationControllerV2里面两个主要方法:
  • 接收Client长轮询的Controller (pollNotification)
  • 上面提到的ReleaseMessageListener的回调方法handleMessage
  1. pollNotification:
  • Client通过请求告知要监听的app、cluster、cluster下当前Client所有namespaces的release message表ID
  • 和样例类似,区别只是源码接口一次请求监听app+cluster下的所有namespace,还有一些额外的处理逻辑,比如等待响应期间数据库连接断掉避免资源浪费,controller中还会检查release message ID是否已经更新,如果已更新就直接响应给Client
  • Client请求超时90s,DeferredResult超时60s构成长轮询,Client接收到响应后会立即再次请求,代码见Client包RemoteConfigLongPollService
四、服务注册、发现:
  1. 回顾:Meta Server = config service + meta service + eureka在同一进程
  2. 启动eureka:
  • 在config service启动类标注了@EnableEurekaServer注解启动eureka
  1. 注册到eureka:
  • biz包的ApolloEurekaClientConfig类重写了获取eureka服务地址的方法,admin和config service都会注册到eureka
  1. 发现:
  • Core包里写过MetaDomainConsts用SPI获取了用户配的Meta Service地址,Client从这里拿到地址,Portal从数据库、配置获取有兴趣可以看看
  • Client和Portal分别有ConfigServiceLocator和AdminServiceAddressLocator会定时从Meta Service发现Config和Admin Service地址,虽然请求发起时也有一些负载策略但Meta Service最好做nginx负载,部署图也展示了
  • meta service一共两个接口,分别获取config和admin服务地址,分别实现了nacos、k8s、consul、eureka(默认)的服务发现,eureka实现比较简单,直接从EurekaClient获取AppName、InstanceId、HomePageUrl返回
四、ServerConfig基础配置:
  • portal db和config db都有ServerConfig表,和配置文件类似,在表里配置有通用性,不需要每个项目都配置一遍
  1. RefreshablePropertySource(抽象):继承MapPropertySource,提供抽象refresh(),能看出它是一个能刷新的kv数据源
  2. PortalDBPropertySource、BizDBPropertySource继承RefreshablePropertySource,refresh()时会从数据库抓取所有配置,并放入PropertySource,分别给portal和admin、config service使用
  3. RefreshableConfig(抽象):上面是两个代表数据源,这个是代表具体数据配置,对应也有两个实现,这里每60s会刷新数据源
@PostConstruct
  public void setup() {
    // 获取数据源,因为要区分portal和admin config,所以在实现类提供
    propertySources = getRefreshablePropertySources();
    if (CollectionUtils.isEmpty(propertySources)) {
      throw new IllegalStateException("Property sources can not be empty.");
    }

    // 刷新数据源并置入environment
    for (RefreshablePropertySource propertySource : propertySources) {
      propertySource.refresh();
      environment.getPropertySources().addLast(propertySource);
    }

    // 定时刷新
    ScheduledExecutorService
        executorService =
        Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ConfigRefresher", true));

    executorService
        .scheduleWithFixedDelay(() -> {
          try {
            propertySources.forEach(RefreshablePropertySource::refresh);
          } catch (Throwable t) {
            logger.error("Refresh configs failed.", t);
            Tracer.logError("Refresh configs failed.", t);
          }
        }, CONFIG_REFRESH_INTERVAL, CONFIG_REFRESH_INTERVAL, TimeUnit.SECONDS);
  }
  1. RefreshableConfig对应的两个实现比较简单,根据key获取各种配置
  2. portal service在页面上还提供了对ServerConfig表配置更新的操作,略
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容