服务端如何感知更新
我们来看官网提供的一张图:
1.用户在Portal操作配置发布。
2.Portal调用Admin Service的接口操作发布。
3.Admin Service发布配置后,发送ReleaseMessage给各个Config Service。
4.Config Service收到ReleaseMessage后,通知对应的客户端。
上面的流程就是从Portal到ConfigService主要流程,下面我们来看看具体的细节。要知道细节我们要自己动手去调试一把源码。我们可以照着官网的文档,自己本地把项目run起来。文档写的还是很详细的,只要按照步骤来都能运行的起来。我们随便新建一个项目然后去编辑下key,然后打开浏览器的F12当我们点击提交按钮的时候我们就知道她到底调用了那些接口,有了接口我们就知道了入口剩下的就是打断点进行调试了。
portal 如何获取AdminService
根据这个方法我们是不是就可以定位到portal模块后端代码的controller。找到对应的controller打开看一看基本没有什么业务逻辑。
然后portal紧接着就是去调用adminService了。
根据上图我们就可以的方法我们就可以找到对应的adminService了,portal是如何找到对应的adminService服务的,因为adminService 是可以部署多台机器,这里就要用到服务注册和发现了adminService只有被注册到服务中心,portal才可以通过服务注册中心来获取对应的adminService服务了。Apollo 默认是采用eureka来作为服务注册和发现,它也提供了nacos、consul来作为服务注册和发现,还提供了一种kubernetes不采用第三方来做服务注册和发现,直接把服务的地址配置在数据库。如果地址有多个可以在数据库逗号分隔。
它提供了四种获取服务列表的实现方式,如果我们使用的注册中心是eureka 我们是不是需要通过eureka的api去获取服务列表,如果我们的服务发现使用的是nacos我们是不是要通过nacos的API去获取服务列表。。。所以Apollo提供了一个MetaService 层,封装服务发现的细节,对Portal和Client而言,永远通过一个Http接口获取Admin Service和Config Service的服务信息,而不需要关心背后实际的服务注册和发现组件。就跟我们平时搬砖一样没有啥是通过增加一个中间层解决不了的问题,一个不行那就再加一个。所以MetaService提供了两个接口services/admin 和services/config 来分别获取Admin Service和Config Service的服务信息。那么Portal 是如何来调用services/admin这个接口的呢?
在 apollo-portal 项目里面com.ctrip.framework.apollo.portal.component#AdminServiceAddressLocator 这个类里面。
- 这个类在加载的时候会通过MetaService 提供的services/admin 接口获取adminService的服务地址进行缓存。
@PostConstruct
public void init() {
allEnvs = portalSettings.getAllEnvs();
//init restTemplate
restTemplate = restTemplateFactory.getObject();
refreshServiceAddressService =
Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ServiceLocator", true));
// 创建延迟任务,1s后开始执行获取AdminService服务地址
refreshServiceAddressService.schedule(new RefreshAdminServerAddressTask(), 1, TimeUnit.MILLISECONDS);
}
上面要去MetaService 请求地址,那么MetaService的地址又是什么呢?这个又如何获取?com.ctrip.framework.apollo.portal.environment#DefaultPortalMetaServerProvider 这个类。
portal 这个模块说完了,我们接着回到adminService了。通过portal调用adminService的接口地址我们很快可以找到它的入口 AdminService 的实现也很简单。
@PreAcquireNamespaceLock
@PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items")
public ItemDTO create(@PathVariable("appId") String appId,
@PathVariable("clusterName") String clusterName,
@PathVariable("namespaceName") String namespaceName, @RequestBody ItemDTO dto) {
Item entity = BeanUtils.transform(Item.class, dto);
ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder();
Item managedEntity = itemService.findOne(appId, clusterName, namespaceName, entity.getKey());
if (managedEntity != null) {
throw new BadRequestException("item already exists");
}
entity = itemService.save(entity);
builder.createItem(entity);
dto = BeanUtils.transform(ItemDTO.class, entity);
Commit commit = new Commit();
commit.setAppId(appId);
commit.setClusterName(clusterName);
commit.setNamespaceName(namespaceName);
commit.setChangeSets(builder.build());
commit.setDataChangeCreatedBy(dto.getDataChangeLastModifiedBy());
commit.setDataChangeLastModifiedBy(dto.getDataChangeLastModifiedBy());
commitService.save(commit);
return dto;
}
PreAcquireNamespaceLock 注解
首先方法上有个@PreAcquireNamespaceLock 这个注解,这个根据名字都应该能够去猜一个大概就是去获取NameSpace的分布式锁,现在分布式锁比较常见的方式是采用redis和zookeeper。但是在这里apollo是采用数据库来实现的,具体怎么细节大家可以去看看源码应该都看的懂,无非就是加锁往DB里面插入一条数据,释放锁然后把这个数据进行删除。稍微有点不一样的就是如果获取锁失败,就直接返回失败了,不会在继续自旋或者休眠重新去获取锁。因为获取锁失败说明已经有其他人在你之前修改了配置,只有这个人新增的配置被发布或者删除之后,其他人才能继续新增配置,这样的话就会导致一个NameSpace只能同时被一个人修改。这个限制是默认关闭的需要我们在数据库里面去配置(ApolloConfigDb的ServiceConfig表)
一般我们应用的配置修改应该是比较低频的,多人同时去修改的话情况会比较少,再说有些公司是开发提交配置,测试去发布配置,提交和修改不能是同一个人,这样的话新增配置冲突就更少了,应该没有必要去配置namespace.lock.switch=true一个namespace只能一个人去修改。
接下来的代码就非常简单明了,就是一个简单的参数判断然后执行入库操作了,把数据插入到Item表里面。这是我们新增的配置数据就已经保存了。效果如下:
这时候新增的配置是不起作用的,不会推送给客户端的。只是单纯一个类似于草稿的状态。
发布配置
接下来我们要使上面新增的配置生效,并且推送给客户端。同样的我们点击发布按钮然后就能知道对应的后端方法入口。
我们通过这个接口可以直接找到adminService的方法入口。
public ReleaseDTO publish(@PathVariable("appId") String appId,
@PathVariable("clusterName") String clusterName,
@PathVariable("namespaceName") String namespaceName,
@RequestParam("name") String releaseName,
@RequestParam(name = "comment", required = false) String releaseComment,
@RequestParam("operator") String operator,
@RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish) {
Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
if (namespace == null) {
throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId,
clusterName, namespaceName));
}
Release release = releaseService.publish(namespace, releaseName, releaseComment, operator, isEmergencyPublish);
//send release message
Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
String messageCluster;
if (parentNamespace != null) {
messageCluster = parentNamespace.getClusterName();
} else {
messageCluster = clusterName;
}
messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, messageCluster, namespaceName),
Topics.APOLLO_RELEASE_TOPIC);
return BeanUtils.transform(ReleaseDTO.class, release);
}
- 上述代码就不仔细展开分析了,感兴趣的可以自己断点调试下我们重点看下releaseService.publish 这个方法,里面有一些灰度发布相关的逻辑,不过这个不是本文的重点,这个方法主要是往release表插入数据。
- 接下来就是messageSender.sendMessage这个方法了,这个方法主要是往ReleaseMessage表里面插入一条记录。保存完ReleaseMessage这个表会得到相应的主键ID,然后把这个ID放入到一个队列里面。然后在加载DatabaseMessageSender的时候会默认起一个定时任务去获取上面队列里面放入的消息ID,然后找出比这这些ID小的消息删除掉。发布流程就完了,这里也没有说到服务端是怎么感知有配置修改了的。
Config Service 通知配置变化
apolloConfigService 在服务启动的时候ReleaseMessageScanner 会启动一个定时任务 每隔1s去去查询ReleaseMessage里面有没有最新的消息,如果有就会通知到所有的消息监听器比如NotificationControllerV2、ConfigFileController等,这个消息监听器注册是在ConfigServiceAutoConfiguration里面注册的。NotificationControllerV2 得到配置发布的 AppId+Cluster+Namespace 后,会通知对应的客户端,这样就从portal到configService 到 client 整个消息通知变化就串起来了。
总结
这样服务端配置如何更新的流程就完了。
1.用户在Portal操作配置发布。
2.Portal调用Admin Service的接口操作发布。
3.Admin Service发布配置后,发送ReleaseMessage给各个Config Service。
4.Config Service收到ReleaseMessage后,通知对应的客户端”apollo的源码相对于其他中间件来说还是相对于比较简单的,比较适合于想研究下中间件源码,又不知道如何下手的同学 。