社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
前面通过源码分析过了注册中心的实现,本文继续思考如何选择【微服务基础设施组件:配置中心】
对于注册中心,要对于项目特性,并结合注册中心的功能和实现特性来决定匹配度,那么对于配置中心,我们最应该了解的则是其配置信息的组织模式(决定系统的扩展性、可维护性等)、数据的存储方式和通信方式(决定性能、响应速度)、以及组件之间的契合度(笔者认为,如果两个组件之间是完全解耦的又不需要较多的额外开发就能配合工作的话,那么就认为两者的契合度很高)。
git上有很多开源的优秀的配置中心的实现,本文仅针对Apollo、Nacos以及Spring Cloud Config来介绍和分析,本质上spring cloud config应与前面两者划分开。
Apollo:呼声很高,功能很强大,较成熟
Nacos:合并了注册中心的功能,开发中
Spring Cloud Config:知名度高,使用者广泛
由于配置中心的底层实现与注册中心的实现有很多相似之处,笔者将会减少源码分析的部分,重点分析配置中心可能面临的难题以及一些常用的解决方案。
Apollo的README.md中有描述这样几个不错的特性(附上界面图):
可以看到,基本涵盖了大部分生产用的功能,是一个比较成熟的中间件,下面就继续了解它的API接入文档。
大致整理了一下,下面通过源码重点了解以下几个部分:
从官网推荐的文章上找到的一张Apollo架构图,好让我们初步的认识它
这张架构图可以简单回答问题1、4、6:
Config Service提供配置信息的管理,它的高可用依赖于Eureka集群和Meta Service,Meta Service起中转请求的作用,接收发现Config Service和Admin Service服务的请求,并向Eureka请求服务列表并返回给客户端。
运维相关的控制由Admin Service承担,它依赖于Config DB和Portal DB。
Config Service则依赖于Config DB,可以猜测到配置信息也是以表记录的形式存储在数据库中。
下面就用代码说话,完全理解Apollo的运行机制。
->apollo-configservice模块
有metaservice和configservice两个部分,在metaservice中发现configservice和adminservice服务发现的方式是http协议通信的方式。
@RestController
@RequestMapping("/services")
public class ServiceController {
private final DiscoveryService discoveryService;
private static Function<InstanceInfo, ServiceDTO> instanceInfoToServiceDTOFunc = instance -> {
ServiceDTO service = new ServiceDTO();
service.setAppName(instance.getAppName());
service.setInstanceId(instance.getInstanceId());
service.setHomepageUrl(instance.getHomePageUrl());
return service;
};
public ServiceController(final DiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
@RequestMapping("/meta")
public List<ServiceDTO> getMetaService() {
List<InstanceInfo> instances = discoveryService.getMetaServiceInstances();
List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
return result;
}
@RequestMapping("/config")
public List<ServiceDTO> getConfigService(
@RequestParam(value = "appId", defaultValue = "") String appId,
@RequestParam(value = "ip", required = false) String clientIp) {
List<InstanceInfo> instances = discoveryService.getConfigServiceInstances();
List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
return result;
}
@RequestMapping("/admin")
public List<ServiceDTO> getAdminService() {
List<InstanceInfo> instances = discoveryService.getAdminServiceInstances();
List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
return result;
}
}
并且metaservice依赖了eureka的client,向eureka获取服务列表。
@Service
public class DiscoveryService {
private final EurekaClient eurekaClient;
public DiscoveryService(final EurekaClient eurekaClient) {
this.eurekaClient = eurekaClient;
}
public List<InstanceInfo> getConfigServiceInstances() {
Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_CONFIGSERVICE);
if (application == null) {
Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_CONFIGSERVICE);
}
return application != null ? application.getInstances() : Collections.emptyList();
}
public List<InstanceInfo> getMetaServiceInstances() {
Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_METASERVICE);
if (application == null) {
Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_METASERVICE);
}
return application != null ? application.getInstances() : Collections.emptyList();
}
public List<InstanceInfo> getAdminServiceInstances() {
Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_ADMINSERVICE);
if (application == null) {
Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_ADMINSERVICE);
}
return application != null ? application.getInstances() : Collections.emptyList();
}
}
跟我们平时写的业务代码没有什么分别…
也就是说,Config Service和Admin Service的高可用是依赖于eureka来解决的,由于这两个服务的数据存储依赖于数据库,所以无需进行数据同步,只需要保证连接同一数据源即可,对于客户端而言,无论请求到哪一台service,最终均会根据同一数据源的数据来进行处理返回,从而解决单点故障的问题。
那么继续看,是否完全依赖数据库?
从configservice包可以看到,其对外提供服务依旧是通过http接口,服务层有一个核心服务类com.ctrip.framework.apollo.configservice.service.config.ConfigService
它有两种实现类,com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCache
和com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService
从controller出发
->com.ctrip.framework.apollo.configservice.service.config.ConfigService#loadConfig
->com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService#loadConfig
->com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService#findRelease
->com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService#findActiveOne
或com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService#findLatestActiveRelease
最终定位到com.ctrip.framework.apollo.biz.service.ReleaseService
内部持有一个Repository类com.ctrip.framework.apollo.biz.repository.ReleaseRepository
(ps. 这里依赖了spring data模块的repository接口)
最终发现,其通过对数据库操作来获取配置信息,验证猜想。
值得一提的是ConfigServiceWithCache
,它是有缓存的实现类,它的缓存机制实现的非常简单,通过在JVM中持有map并定期轮询数据库来更新,它通过参数config-service.cache.enabled
决定是否开启。
到这里,验证了问题1、6的猜测,并且可以总结出问题1、6的答案:
继续了解权限部分是如何控制的,首先我们知道apollo是依赖数据库来进行数据存储的,那么权限部分很可能就是通过表设计来控制,首先找到提供权限控制的入口:
com.ctrip.framework.apollo.portal.spi.configuration.AuthConfiguration
com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService
可以发现,它依赖了spring security框架,在其上实现权限控制的效果,权限控制关系由这几个表表示:
Users Authorities Permission Role RolePermission
这里就不介绍了。
以上均是apollo服务端的实现,接着进入客户端的实现部分。
com.ctrip.framework.apollo.internals.DefaultConfigManager
这个类用于与服务端通信来获取配置信息
个人觉得这部分的实现设计的很好,美中不足在于没有抽象到接口
例如这里有Repository
、FactoryManager
/Factory
、Manager
、Config
/ConfigFile
四个概念
最外层是Service
,service对外提供服务
service内部,由Manager
管理配置,这一层级存在一级缓存(JVM层)
Manager
内部存在FactoryManager
用于管理不同namespace的Factory
,这样就很好的隔离了不同namespace
每个Factory
则仅负责该namespace下的Config
的创建
每个Config
内部抽象了自动刷新的功能,监听了配置变化
而与远程通信的部分由Repository
承担,Repository
有本地实现和远程实现,这里使用了装饰者模式
Config
内部持有一个LocalFileConfigRepository
LocalFileConfigRepository
内部持有RemoteConfigRepository
这样又很好的解耦了远程与本地的配置信息管理
那么与服务端的通信都封装在RemoteConfigRepository
感兴趣的可以直接看这段逻辑com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync
->com.ctrip.framework.apollo.internals.RemoteConfigRepository#loadApolloConfig
->HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);
最终定位到【老套路】,发起http请求,获取响应,解析…
那么监听又是如何实现的?
当本地配置仓库同步信息成功,感应到配置信息变更后会调用
->com.ctrip.framework.apollo.internals.LocalFileConfigRepository#onRepositoryChange
->com.ctrip.framework.apollo.internals.AbstractConfigRepository#fireRepositoryChange
触发listeners集合的onRepositoryChange()
方法
而listeners集合是在Config
被构造出来的时候进行初始化的过程中将自身作为监听器add进去,这样就实现了一个【Config
内部抽象了自动刷新的功能,监听了配置变化】的Config
对象
触发同步的时机有这样几种情况:
这样就基本保证了客户端尽快感知到配置变更情况。(官方称实时性在1s以内)
而在这段代码的阅读中,对于问题5也得到了回答:
以sync过程为例:
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
...
try {
...
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
由一个Transaction
对象来存储监控信息。
通过反射构造Transaction对象的Factory对象的Method对象,这里听起来有点绕,不理解的话最好是追踪一下源码。
最终由Method对象来构造Transaction
对象。
到这里,只剩下最后一个client如何进行文件缓存的问题:
前面说到本地配置信息的类是com.ctrip.framework.apollo.internals.LocalFileConfigRepository
直接进入这个类,一切谜底就揭晓了…
-> om.ctrip.framework.apollo.internals.LocalFileConfigRepository#updateFileProperties
-> com.ctrip.framework.apollo.internals.LocalFileConfigRepository#persistLocalCacheFile
->java.util.Properties#store(java.io.OutputStream, java.lang.String)
最终将Properties类信息写入文件,读文件反之
->com.ctrip.framework.apollo.internals.LocalFileConfigRepository#loadFromLocalCacheFile
->java.util.Properties#load(java.io.InputStream)
写的时机:
读的时机:
总结以上六个问题:
答:
com.ctrip.framework.apollo.tracer.Tracer
类来记录信息,具体实现可自行跟踪源码ps.吐槽一句,apollo的代码还是非常适合新手看的,典型的web MVC的风格,大量依赖Spring框架,底层的抽象部分做的还是欠缺了一些,可能最开始设计的时候没有考虑过多的情况,但从结果上看还是非常成功的。
关于Nacos的config server的实现,相对于naming server简单的多。
目前Nacos已发布第一个RC版本:1.0.0-RC1
那么源码部分的分析以该版本为准。
要知道,作为开发者,需要构建一个稳定可靠的微服务环境,对于配置中心这样的公用基础设施,最为重要的则是服务的可用性。
基于以下几个核心问题的考量来分析:
首先从官网找入口,Config Service的sdk入口:
https://nacos.io/zh-cn/docs/sdk.html
定位到client模块的com.alibaba.nacos.api.config.ConfigService
这个接口
继续进入它的实现类com.alibaba.nacos.client.config.NacosConfigService
以getConfig为例追踪
->com.alibaba.nacos.client.config.NacosConfigService#getConfig
->com.alibaba.nacos.client.config.NacosConfigService#getConfigInner
->(如果在本地缓存没有找到)com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig
->result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
->最终调用HttpSimpleClient.httpGet()
,可以发现其底层封装了HttpURLConnection
来进行http请求
那么可以追踪到请求的url为 /v1/cs/configs
通过全局搜索,定位到com.alibaba.nacos.config.server.controller.ConfigController
定位到方法->com.alibaba.nacos.config.server.controller.ConfigController#getConfig
->com.alibaba.nacos.config.server.controller.ConfigServletInner#doGetConfig
->(如果内存缓存中没有找到)
if (STANDALONE_MODE && !PropertyUtil.isStandaloneUseMysql()) {
configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
} else {
file = DiskUtil.targetBetaFile(dataId, group, tenant);
}
这段代码告诉我们,当使用单机模式且不使用mysql作为数据源时,使用persistService获取,而persisteService则是以debyDB为数据源来实现的,即依赖debyDB数据库来进行数据存储;当处于集群模式或使用mysql作为数据源时,从文件中获取配置信息,并通过transferTo的API将文件中的内容写入响应outputStream。
(这里使用了零拷贝提高性能)
到这里,还是疑惑的,没有看到有关于可用性的代码部分,如果有多个节点,文件之间是如何保持同步的呢?还是采用冗余备份+低一致性的方式呢?
那么,就要知道这个文件是如何产生的。
笔者这里直接贴出来入口:com.alibaba.nacos.config.server.service.dump.DumpProcessor#process
定位的过程就不做介绍了,大概想法是笔者认为既然没有找到节点间数据同步的部分,那么很可能是依赖数据库的可用性,文件只是作为缓存而存在。
ConfigInfo4Beta cf = dumpService.persistService.findConfigInfo4Beta(dataId, group, tenant);
boolean result;
if (null != cf) {
result = ConfigService.dumpBeta(dataId, group, tenant, cf.getContent(), lastModified, cf.getBetaIps());
if (result) {
ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified,
cf.getContent().length());
}
} else {
result = ConfigService.removeBeta(dataId, group, tenant);
if (result) {
ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0);
}
}
最终猜想验证,Nacos的高可用依赖于数据库,如果是单机模式且不使用mysql的情况,则无需进行文件缓存,直接从数据源获取;而如果是集群模式或使用mysql的情况,则启动dump线程,定期将数据源中的信息dump到文件(作为缓存,仅提供读),值得注意的是,为了保证数据,不能仅仅通过定期dump来保持文件和数据源之间的数据同步,而是写入数据库的同时通过异步通知来令数据写入文件缓存。
ps.那么就会存在一个小问题,如果server cpu压力大或IO压力大的情况下,可能存在写入配置后不能第一时间感知到配置的变更。
继续思考问题2,这个问题其实官网已经给出了答案,前文的https://nacos.io/zh-cn/docs/sdk.html。
Nacos提供了client模块,由用户来构建ConfigService对象,并使用其上API来接入。
这样的方式与Apollo一致,比起直接提供restful api而言更加方便,无需用户封装http client请求,对用户更加友好。
另外,在Spring Cloud Alibaba工程也提供了Spring Cloud Alibaba Config的接入方式,用户可以通过直接引入jar包,无需配置、直接接入。
指的注意的是,Nacos提供了两种部署方式:
一、Naming Server和Config Server合并部署
二、Naming Server和Config Server分离部署
于是,Spring Cloud Alibaba依然提供了Spring Cloud Alibaba Config Server的支持,这样就大大简化了Spring Cloud工程的接入。
到这里问题3得到了部分的回答,为什么说是部分呢?虽然Config Server对外提供的大部分功能是使用了http协议,但其实现了配置监听的功能,需要了解其监听的方式是如何实现的,这样才能完整的回答问题3。
首先读完前文,我们知道了Apollo的监听实现方式采用了定时轮询+(缓存未命中时)主动同步拉取的方式。
继续追踪Nacos监听配置的过程:
->com.alibaba.nacos.client.config.NacosConfigService#addListener
->com.alibaba.nacos.client.config.impl.ClientWorker#addTenantListeners
->com.alibaba.nacos.client.config.impl.CacheData#addListener
->listeners.addIfAbsent(wrap)
这里实现的过程比较复杂,就不贴源码了。
最终将listener包装后(先不论包装类起了作用)添加到CacheData持有的listeners中。
(每个CacheData对应了由dataId+group组装成的key,即某一数据中心下的某个组的配置的数据均存储在一个CacheData中。)
并开启多个LongPollingRunnable
(一个task对应处理多个CacheData)任务,不断轮询,从Config Server拉取信息并写入其对应的CacheData,一旦检测到CacheData的内容发生了变化,则回调一次(用户自定义逻辑的)监听器,并且Nacos将长轮询任务使用的线程池和通知任务使用的线程池分离,异步通知。
ps.这里使用了MD5来区分内容是否发生变化,主要考量是MD5的加密速度较快,而无需使用equals的方式在每次变化时都要解析一次,这样的加速了比较内容是否发生变化的过程。
可以看到,与Apollo的方式有些不同,Nacos采用长轮询的方式监听配置的变化。
可以看到两者的效果异同之处:
ps.所以本质上监听的方式也是使用了http协议,周期性请求。
而对于问题4:
相对Apollo,Nacos没有配置继承的概念,从实现上看,后获取的配置会覆盖先获取的配置。
Nacos的接入方式、部署方式相对容易的多。
从代码上看,Nacos在很多地方运用了异步,看起来能提高系统的吞吐量,但还需要经历考验。
所以,似乎也没有特别亮眼的特性,或许AP/CP/MIX模式是一个比较容易引起注意的功能,但目前看来似乎没有太大的优势,这么做仅仅是为了旧版本兼容。
不过,Nacos官网提供了多注册中心的同步组件https://nacos.io/zh-cn/docs/nacos-sync.html,用于迁徙还是非常方便的。
由于Spring Cloud Config是基于Spring Boot来开发的,源码分析的部分将会减少,读者可以根据需要阅读Spring及Spring Boot源码。
使用Spring Cloud Config的方式很简单,在一个Spring Boot应用上使用@EnableConfigServer注解,即激活启动Spring Cloud Config Server。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {
}
可以看到其导入了ConfigServerConfiguration.class
@Configuration
public class ConfigServerConfiguration {
class Marker {}
@Bean
public Marker enableConfigServerMarker() {
return new Marker();
}
}
并通过内部类Marker开启ConfigServerAutoConfiguration自动配置
@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {
}
到这里可以看到,最终进行了一些配置,而主要初始化的内容包括:
EnvironmentRepositoryConfiguration
:配置中心核心类的配置和注入,包含了各种具体实现,如Git、fileSystem、Db、ConsulCompositeConfiguration
:如果存在多个配置中心的实现,则进行该配置,将各个实现类综合到一个类中来管理ResourceRepositoryConfiguration
:资源仓库的配置,用于加载并读取本地文件ConfigServerEncryptionConfiguration
:加密配置,将publicKey等加密信息通过http接口的形式提供给其他客户端(保证仅有集群内的节点能对信息进行解读)ConfigServerMvcConfiguration
:MVC配置,将EnvironmentController(提供配置获取等接口)、ResourceController(提供资源获取等接口)注入到Spring容器大致了解了其配置的内容后,进入核心的配置类EnvironmentRepositoryConfiguration
了解一下其实现:
以其默认实现git为例,从代码逻辑上看,默认配置下生效并注入的代码有以下
@Bean
@ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true)
public ConfigServerHealthIndicator configServerHealthIndicator(
EnvironmentRepository repository) {
return new ConfigServerHealthIndicator(repository);
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public MultipleJGitEnvironmentProperties multipleJGitEnvironmentProperties() {
return new MultipleJGitEnvironmentProperties();
}
@Configuration
@ConditionalOnMissingBean(EnvironmentWatch.class)
protected static class DefaultEnvironmentWatch {
@Bean
public EnvironmentWatch environmentWatch() {
return new EnvironmentWatch.Default();
}
}
@Configuration
@ConditionalOnClass(TransportConfigCallback.class)
static class JGitFactoryConfig {
@Bean
public MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory(
ConfigurableEnvironment environment, ConfigServerProperties server,
Optional<ConfigurableHttpConnectionFactory> jgitHttpConnectionFactory,
Optional<TransportConfigCallback> customTransportConfigCallback) {
return new MultipleJGitEnvironmentRepositoryFactory(environment, server, jgitHttpConnectionFactory,
customTransportConfigCallback);
}
}
@Configuration
@ConditionalOnClass({ HttpClient.class, TransportConfigCallback.class })
static class JGitHttpClientConfig {
@Bean
public ConfigurableHttpConnectionFactory httpClientConnectionFactory() {
return new HttpClientConfigurableHttpConnectionFactory();
}
}
@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Autowired
private
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/xc1158840657/article/details/90712723
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!