分布式配置中心: Spring Cloud Config:
spring cloud config是一個基於http協議的遠程配置實現方式。通過統一的配置管理服務器進行配置管理,客戶端通過https協議主動的拉取服務的的配置信息,完成配置獲取。
spring cloud config的使用方式非常簡單,spring cloud config server默認的實現方式是git管理配置,官方文檔介紹已經詳細說明有幾種使用方式。下面看一下git的spring cloud config server實現方式。
boot版本:springBoot : 2.0.1.RELEASE
一、構建配置中心:
1.依賴管理(.pom)
<dependencyManagement> <dependencies> <dependency> <!-- SpringCloud 所有子項目 版本集中管理. 統一所有SpringCloud依賴項目的版本依賴--> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </dependencies>
2.啟動類注解(@EnableConfigServer)
@SpringBootApplication @EnableConfigServer public class App { private final static Logger log = LoggerFactory.getLogger(App.class); public static void main(String[] args) { SpringApplication.run(App.class, args); log.info("服務啟動成功"); } }
3.application.properties 文件配置
server.port = 8888 spring.application.name=config# 選擇的同步工具 spring.profiles.active=git # git地址 spring.cloud.config.server.git.uri=https://github.com/wuzhenzhao/spring-cloud-config-repo.git # 搜索目錄 spring.cloud.config.server.git.search-paths=properties # git倉庫default-label默認值是master spring.cloud.config.server.git.default-label=master
如果倉庫是私有的還需要配置如下:
spring.cloud.config.server.git.username=
spring.cloud.config.server.git.password=
github 上有如下3個環境的配置文件,內容分別是foo=hello-dev / foo=hello-pro / foo=hello-test
就這樣完成了簡單的配置,啟動程序,如果想獲取開發配置,訪問http://localhost:8888/config/dev可以讀取到config-dev.properties的配置內容。請求配置的參數通過路徑參數設置。
完成了這些准備工作之后,我們就可以通過瀏覽器、 POSTMAN或CURL等工具直接來訪問我們的配置內容了。訪問配置信息的URL與配置文件的映射關系如下所示:
- /{ application } / {profile} [/{label}]
- /{ application }-{profile}. yml
- /{ label }/{ application }-{profile}.yml
- /{ application }-{profile}.properties
- /{ label }/{ application }-{profile}.properties
上面的 url 會映射 {application}-{profile} .properties 對應的配置文件,其中 {label} 對應Git上不同的分支,默認為 master 。我們可以嘗試構造不同的 url 來訪問不同的配置內容, 例如:http://localhost:8888/{applicationName}/{profile}/{label} , label分支,不傳的話默認master。並獲得如下返回信息:
同時, 我們可以看到 config-server 的控制台中還輸出了下面的內容,配置服務器在從 Git 中獲取配置信息后, 會存儲 一 份在 config-server 的文件系統中, 實質上config-server 是通過 git clone 命令將配置內容復制了一 份在本地存儲, 然后讀取這些內容並返回給微服務應用進行加載。config-server 通過 Git 在本地倉庫暫存,可以有效防止當 Git 倉庫出現故障而引起無法加載配置信息的情況。
二、構建客戶端:
1.依賴管理(.pom)
<dependencyManagement> <dependencies> <dependency> <!-- SpringCloud 所有子項目 版本集中管理. 統一所有SpringCloud依賴項目的版本依賴--> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> </dependencies>
2.bootstrap.properties 配置
server.port = 8889 spring.application.name=config spring.cloud.config.label=master spring.cloud.config.profile=test spring.cloud.config.uri=http://localhost:8888/
3.測試類
@RestController public class TestRestController { @Value("${foo}") String foo; @RequestMapping(value = "/hello") public String hello(){ return foo; } }
4.啟動類注解 @EnableConfigServer
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ConfigServerConfiguration.class}) public @interface EnableConfigServer { }
項目啟動后訪問測試類的接口,可以根據配置文件中的spring.cloud.config.profile所配置的環境獲取到不同的值。
三、服務端源碼詳解:
注解 EnableConfigServer 可以開啟應用服務對配置中心的支持。當開啟之后,配置服務器就會在啟動時進行自動配置。通過該注解尋找到如下配置。
@Configuration public class ConfigServerConfiguration { public ConfigServerConfiguration() { } @Bean public ConfigServerConfiguration.Marker enableConfigServerMarker() { return new ConfigServerConfiguration.Marker(); } class Marker { Marker() { } } }
可以看到好像沒有繼續前進的入口了,僅僅是注入了這個Marker類嘛?通過尋找我們可以發現該類唯一被引用的地方,就是如下類。
@Configuration @ConditionalOnBean({Marker.class}) @EnableConfigurationProperties({ConfigServerProperties.class}) @Import({EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class}) public class ConfigServerAutoConfiguration { public ConfigServerAutoConfiguration() { } }
@ConditionalOnBean(Marker.class)表示當裝配了ConfigServerConfiguration.Marker的實例時才會執行ConfigServerAutoConfiguration的處理。
這里又另外引入了5個配置類。
EnvironmentRepositoryConfiguration: 環境變量存儲相關的配置類
CompositeConfiguration:組合方式的環境倉庫配置類
ResourceRepositoryConfiguration:資源倉庫相關的配置類
ConfigServerEncryptionConfiguration:加密斷點相關的配置類
ConfigServerMvcConfiguration:對外暴露的MVC端點控制器的配置類,
對於服務端來說,其基本職責就是能夠將具體存儲中的配置信息先拿到,然后提供出 API 供客戶端來調用。下面從ConfigServerAutoConfiguration 中 import的這些配置類來具體看下實現。
重點是 EnvironmentRepositoryConfiguration 類。
@Configuration @EnableConfigurationProperties({ SvnKitEnvironmentProperties.class, JdbcEnvironmentProperties.class, NativeEnvironmentProperties.class, VaultEnvironmentProperties.class}) @Import({CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class, VaultRepositoryConfiguration.class, SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class, DefaultRepositoryConfiguration.class}) public class EnvironmentRepositoryConfiguration { ....... }
這里的@Import又引入了7種配置類,會發現其實剛好對應config server的幾種實現方式git的實現方式使用的配置類就是GitRepositoryConfiguration。以GitRepositoryConfiguration的為例分析。
@Configuration @Profile({"git"}) class GitRepositoryConfiguration extends DefaultRepositoryConfiguration { GitRepositoryConfiguration() { } }
GitRepositoryConfiguration 集成了 DefaultRepositoryConfiguration,這也說明了 Spring Cloud Config 默認使用的是Git。不同的配置類實現都會被標注一個@Profile,可以通過這個來激活相應的配置類;具體做法是在配置服務端的 application.properties(application.yml) 中來指定:
spring.profile.active=git
DefaultRepositoryConfiguration 的 ConditionalOnMissingBean 可以知道,如果上下文中沒有 EnvironmentRepository,那么就使用 DefaultRepositoryConfiguration。最后DefaultRepositoryConfiguration是封裝了一個 MultipleJGitEnvironmentRepository 這個bean。
@Configuration @ConditionalOnMissingBean( value = {EnvironmentRepository.class}, search = SearchStrategy.CURRENT ) class DefaultRepositoryConfiguration { @Autowired private ConfigurableEnvironment environment; @Autowired private ConfigServerProperties server; @Autowired( required = false ) private TransportConfigCallback customTransportConfigCallback; DefaultRepositoryConfiguration() { } @Bean public MultipleJGitEnvironmentRepository defaultEnvironmentRepository( MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory, MultipleJGitEnvironmentProperties environmentProperties) throws Exception { return gitEnvironmentRepositoryFactory.build(environmentProperties); } }
我們可以先來看一下 MultipleJGitEnvironmentRepository 類的類圖:
這里我們可以發現 MultipleJGitEnvironmentRepository 實現了 InitializingBean 接口,那么在配置加載完以后一定要調用 afterPropertiesSet 方法,我們來看一下具體都做了什么:
public synchronized void afterPropertiesSet() throws Exception { Assert.state(this.getUri() != null, "You need to configure a uri for the git repository."); this.initialize(); if (this.cloneOnStart) { this.initClonedRepository(); } }
從源碼中我們看到,首先會將配置從git上clone到本地,然后再進行其他的操作。接着就本地的git倉庫中獲取指定的數據了。
MultipleJGitEnvironmentRepository 的頂層接口是 EnvironmentRepository ,當然其他的實現也都是實現了這個接口的。另外一個需要關注的是 SearchPathLocator。EnvironmentRepository:定義了獲取指定應用服務環境信息的方法,返回一個Enviroment
SearchPathLocator 中有一個內部類 Locations ,Locdations中定義了應用服務配置存儲信息。
public interface EnvironmentRepository { Environment findOne(String application, String profile, String label); }
AbstractScmEnvironmentRepository 實現了 AbstractScmAccessor 和 EnvironmentRepository ,主要就是EnvironmentRepository 中 findOne 的實現:
public synchronized Environment findOne(String application, String profile, String label) { //新建了一個本地倉庫作為代理倉庫來使用 NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(this.getEnvironment(), new NativeEnvironmentProperties()); //獲取本地倉庫中指定應用的位置 Locations locations = this.getLocations(application, profile, label); delegate.setSearchLocations(locations.getLocations()); //根據這個路徑搜索應用服務的配置信息 Environment result = delegate.findOne(application, profile, ""); result.setVersion(locations.getVersion()); result.setLabel(label); return this.cleaner.clean(result, this.getWorkingDirectory().toURI().toString(), this.getUri()); }
getLocations 是一個模板方法,Config Server中提供了三種實現:
以 git 方式為例 最后會調到 JGitEnvironmentRepository#getLocations 方法:
public synchronized Locations getLocations(String application, String profile, String label) { if (label == null) { label = this.defaultLabel; } // 獲取最新的版本號 String version = this.refresh(label); // 根據最新的版本號返回 Locations 定位到資源的搜索路徑 return new Locations(application, profile, label, version, this.getSearchLocations(this.getWorkingDirectory(), application, profile, label)); }
refresh 方法做的作用就是刷新本地倉庫的配置狀態,這樣就能保證每次都能拉取到最新的配置信息。下面來分析這個方法。
public String refresh(String label) { Git git = null; String var20; try { // 創建一個git客戶端 git = this.createGitClient(); // 是否需要執行 git pull if (this.shouldPull(git)) { FetchResult fetchStatus = this.fetch(git, label); if (this.deleteUntrackedBranches && fetchStatus != null) { this.deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git); } // 獲取后checkout,這樣我們就可以獲得任何新的分支、tag等。 this.checkout(git, label); this.tryMerge(git, label); } else { // 沒有什么要更新,所以只是checkout和merge。 // 合並是因為遠程分支以前可能已經更新過 this.checkout(git, label); this.tryMerge(git, label); } // 返回當前的版本 var20 = git.getRepository().findRef("HEAD").getObjectId().getName(); } catch (RefNotFoundException var15) { throw new NoSuchLabelException("No such label: " + label, var15); } catch (NoRemoteRepositoryException var16) { throw new NoSuchRepositoryException("No such repository: " + this.getUri(), var16); } catch (GitAPIException var17) { throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + this.getUri(), var17); } catch (Exception var18) { throw new IllegalStateException("Cannot load environment", var18); } finally { try { if (git != null) { git.close(); } } catch (Exception var14) { this.logger.warn("Could not close git repository", var14); } } return var20; }
這個里面基本就是通過git客戶端的一些操作。先是檢查遠程倉庫的狀態,然后判斷本地倉庫是否要執行刷新操作。如果有狀態更新,比如新的提交時,Git客戶端就會執行fetch,然后再進行merge,更新到本地倉庫。最終是裝配一個MultipleJGitEnvironmentRepository的bean,實際每種配置類的實現的最終都是裝配一個EnvironmentRepository
的子類,可以認為,有一個地方最終會引用到EnvironmentRepository的bean,在ConfigServerAutoConfiguration類中曾經導入了ConfigServerMvcConfiguration 類,而這個類正是向外暴露出端口供客戶端訪問的配置,在里面組裝了兩個的 Controller:
@Bean public EnvironmentController environmentController(EnvironmentRepository envRepository, ConfigServerProperties server) { EnvironmentController controller = new EnvironmentController(this.encrypted(envRepository, server), this.objectMapper); controller.setStripDocumentFromYaml(server.isStripDocumentFromYaml()); controller.setAcceptEmpty(server.isAcceptEmpty()); return controller; } @Bean @ConditionalOnBean({ResourceRepository.class}) public ResourceController resourceController(ResourceRepository repository, EnvironmentRepository envRepository, ConfigServerProperties server) { ResourceController controller = new ResourceController(repository, this.encrypted(envRepository, server)); return controller; }
而這兩個應該是客戶端獲取服務端配置的入口,以 EnvironmentController 為例查看代碼如下。
@RequestMapping({"/{name}/{profiles}/{label:.*}"}) public Environment labelled(@PathVariable String name, @PathVariable String profiles, @PathVariable String label) { if (name != null && name.contains("(_)")) { name = name.replace("(_)", "/"); } if (label != null && label.contains("(_)")) { label = label.replace("(_)", "/"); } Environment environment = this.repository.findOne(name, profiles, label); if (this.acceptEmpty || environment != null && !environment.getPropertySources().isEmpty()) { return environment; } else { throw new EnvironmentNotFoundException("Profile Not found"); } }
注意這里的EnvironmentController#repository屬性就是GitRepositoryConfiguration實例化的MultipleJGitEnvironmentRepository,如果是別的實現方式就是別的EnvironmentRepository。可以看出”/{name}/{profiles}/{label:.*}”路徑參數正好與我們的請求方式相對應,因此Config Server是通過建立一個RestController來接收讀取配置請求的,然后使用EnvironmentRepository來進行配置查詢,最后返回一個這個對象的JSON。
public class Environment { private String name; private String[] profiles; private String label; private List<PropertySource> propertySources; private String version; private String state; ......... }
通過訪問 http://localhost:8888/config/pro/master 會得到信息如下,而這個信息正是 Environment 類
{ "name":"config", "profiles":["pro"], "label":"master", "version":"e6a0ce237a9f9e05608e5c276a9365f0fdd67ed6", "state":null, "propertySources":[{ "name":"https://github.com/wuzhenzhao/spring-cloud-config-repo.git/properties/config-pro.properties", "source":{"foo":"hello-pro"} }] }
SVN倉庫配置:
Config Server除了支持Git倉庫之外, 也能 使用SYN倉庫, 只需要做如下配置。 在 pom.xml中引入SYN的依賴配置,讓ConfigServer擁有讀取SYN內容的能力:
<dependency> <groupId>org.tmatesoft.svnkit</groupId> <artifactId>svnkit</artifactId> <version>1.8.10</version> </dependency>
在application.properties中使用SVN的配置屬性來指定SVN服務器的位置, 以及訪問的賬戶名與密碼:
spring.cloud.config.server.svn.uri=svn://localhost:443/wuzz/config-repo spring.cloud.config.server.svn.username = username spring.cloud.config.server.svn.password = password
通過上面的配置修改,ConfigServer就可以使用SVN作為倉庫來存儲配置文件了, 而對於客戶端來說, 這個過程是透明的, 所以不需要做任何變動。
本地倉庫:
在使用了Git或SVN倉庫之后, 文件都會在ConfigServer的本地文件系統中存儲一 份,這點上面也體現了。這 些文 件默認會被存 儲於以 config-repo 為前綴的臨 時 目錄中, 比如名為/tmp/config-repo-<隨機數>的目錄。 由於其隨機性以及臨時目錄的特性, 可能會有 一些不可預知的后果, 為了避免將來可能會出現的問題, 最好的辦法就是指定一 個固定的位置來存儲這些重要信息。我們只需要通過spring.cloud.config.server.git.basedir或 spring.cloud.config.server.svn.basedir來配置一 個我們准備好的目錄即可。
屬性覆蓋:
Config Server 還有 一 個“ 屬性覆蓋 ”的特性, 它可以讓開發人員為所有的應用提供配置屬性,只需要通過 spring.cloud.config.server.overrides 屬性來設置鍵值對的參數,這些參數會以 Map 的方式加載到客戶端的配置中。 比如:
spring.cloud.config.server.overrides.name = wuzz spring.cloud.config.server.overrides.from = hangzhou
通過該屬性配置的參數,不會被 Spring Cloud 的客戶端修改,並且 Spring Cloud 客戶端從 Config Server 中獲取配置信息時,都會取得這些配置信息。 利用該特性可以方便地為Spring Cloud 應用配置 一 些共同屬性或是默認屬性。 當然,這些屬性並非強制的,我們可以通過改變客戶端中更高優先級的配置方式(比如,配置環境變量或是系統屬性),來選擇是否使用 Config Server 提供的默認值。
安全保護:
由於配置中心存儲的內容比較敏感,做 一 定的安全處理是必需的。 為配置中心實現安全保護的方式有很多,比如物理網絡限制、 0Auth2 授權等。 不過,由於我們的微服務應用和配置中心都構建於 Spring Boot 基礎上,所以與 Spring Security 結合使用會更加方便。我們只需要在配置中心的 pom.xml 中加入 spring-boot-starter-security 依賴,不需要做任何其他改動就能實現對配置中心訪問的安全保護。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
大多數情況下,我們並不會使用隨機生成密碼的機制。 我們可以在配置文件中指定用戶和密碼, 比如:
security.user.name=user security.user.password=37cc5635-559b-4e6f-b633-7e932b813f73
由於我們已經為 config-server 設置了安全保護,如果這時候連接到配置中心的客戶端中沒有設置對應的安全信息,在獲取配置信息時會返回401錯誤。 所以,需要通過配置的方式在客戶端中加入安全信息來通過校驗, 比如:
spring.cloud.config.username=user spring.cloud.config.password=37cc5635-559b-4e6f-b633-7e932b813f73
此外,spring cloud 還提供了加解密。這里就不說明了。因為配置比較繁瑣,而且實際環境中應用的比較少。
高可用配置:
當要將配置中心部署到生產環境中時, 與服務注冊中心 一 樣, 我們也希望它是 一 個高可用的應用。 Spring Cloud Config 實現服務端的高可用非常簡單, 主要有以下兩種方式。
傳統模式: 不需要為這些服務端做任何額外的配置, 只需要遵守 一 個配置規則, 將所有的 Config Server 都指向同 一 個 Git 倉庫, 這樣所有的配置內容就通過統 一 的共享文件系統來維護。而客戶端在指定 Config Server 位置時,只需要配置 Config Server上層的負載均衡設備地址即可, 就如下圖所示的結構。
服務模式:除了上面這種傳統的實現模式之外, 我們也可以將 Config Server 作為 一個普通的微服務應用,納入 Eureka 的服務治理體系中。 這樣我們的微服務應用就可以通過配置中心的服務名來獲取配置信息, 這種方式比起傳統的實現模式來說更加有利於維護, 因為對於服務端的負載均衡配置和客戶端的配置中心指定都通過服務治理機制一 並解決了, 既實現了高可用, 也實現了自維護。
主要步驟是在服務端添加 Eureka依賴,啟用@EnableDiscoveryClient 注解。在 application.properties 中配置參數eureka.client.serviceUrl.defaultZone以指定服務注冊中心的位置。然后進行配置客戶端,也是需要添加Eureka依賴,開啟@EnableDiscoveryClient注解,然后添加以下配置信息
spring.application.name=wuzz-config-client server.port=7002 #指定服務注冊中心, 用於服務的注冊與發現 eureka.client.serviceUrl.defaultZone = http: / /localhost: 1111/eureka/ #開啟通過服務來訪間 Config Server 的功能 spring.cloud.config.discovery.enabled=true #指定 Config Server 注冊的服務名 spring.cloud.config.discovery.serviceid=config-server spring.cloud.config.profile=dev
失敗快速響應與重試:
Spring Cloud Config的客戶端會預先加載很多其他信息,然后再開始連接ConfigServer進行屬性的注入。 當我們構建的應用較為復雜的時候, 可能在連接ConfigServer之前花費較長的啟動時間, 而在 一 些特殊場景下, 我們又希望可以快速知道當前應用是否能順利地從ConfigSe rve r獲取到配置信息, 這對在初期構建調試環境時, 可以減少很多等待啟動的時間。 要實現客戶端優先判斷ConfigSe rve r獲取是否正常, 並快速響應失敗內容, 只需在bootstrap.properties中配置參數spring.cloud.config.failFast= true即可。
但是, 若只是因為網絡波動等其他間歇性原因導致的問題, 直接啟動失敗似乎代價有些高。 所以, Config 客戶端還提供了自動重試的功能, 在開啟重試功能前, 先確保已經配置了 spring.cloud.config.failFast=true, 再進行下面的操作。先添加以下依賴
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
不需要再做其他任何配置, 啟動客戶端應用, 在控制台中可以看到如下內容。 客戶端在連接 Config Server 失敗之后, 會繼續嘗試, 直到第 6 次失敗后, 才返回錯誤信息。 通過這樣的重試機制, 可以避免 一 些間歇性問題引起的失敗導致客戶端應用無法啟動的情況。
若對默認的最大重試次數和重試間隔等設置不滿意,還可以通過下面的參數進行調整。
- spring.cloud.config.retry.multiplier: 初始重試間隔時間(單位為毫秒),默認為 1000 毫秒。
- spring.cloud.config.retry.initail-interval: 下 一 間隔的乘數,默認為 1.1, 所以當最初間隔是 1000 毫秒時, 下 一 次失敗后的間隔為 1100 毫秒。
- spring.cloud.config.retry.max-interval: 最大間隔時間,默認為 2000毫秒。
- spring.cloud.config.retry.max-attempts: 最大重試次數,默認為 6 次。
動態刷新配置:
有時候, 我們需要對配置 內容做 一 些實時更新, 那么Spring Cloud Config是否可以實現呢?
在config-client的pom.xml中新增spring-boot-starter-actuator監控模塊。 其中包含了/refresh 端點的 實現,該端點將用於實現客戶端應用配置信息的重新獲取與刷新。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
在修改了Git上面的配置信息之后。通過POST請求發送到http://localhost:8888/refresh .再去獲取配置信息就是修改之后的,若無法訪問改端口請加上配置:
#開啟這個端點,便於等等我們查看配置
management.endpoints.web.exposure.include = refresh