Apollo在基礎架構中的實踐經驗
本文來自李偉超同學的投稿,如果你有好的文章也歡迎聯絡我。
微服務配置中心 Apollo 使用指南,以下檔案根據 apollo wiki 整理而來,部分最佳實踐說明和程式碼改造基於筆者的工作經驗整理而來,如有問題歡迎溝通。
配置中心
在拆分為微服務架構前,曾經的單體應用只需要管理一套配置。而拆分為微服務後,每一個系統都有自己的配置,並且都各不相同,而且因為服務治理的需要,有些配置還需要能夠動態改變,如業務引數調整或需要熔斷限流等功能,配置中心就是解決這個問題的。
配置的基本概念
- 配置是獨立於程式的只讀變數
- 同個應用在不同的配置有不同的行為
- 應用不應該改變配置
- 配置伴隨應用的整個生命週期
- 初始化引數和執行引數
- 配置可以有多種載入方式
- 配置需要治理
- 許可權控制(應用級別、編輯釋出隔離等)
- 多環境叢集配置管理
- 框架類元件配置管理
配置中心
- 配置註冊與反註冊
- 配置治理
- 配置變更訂閱
Spring Environment
Environment 是 Spring 容器中對於應用環境兩個關鍵因素(profile & properties)的一個抽象。
- profile
profile 是一個邏輯的分組,當 bean 向容器中註冊的時候,僅當配置啟用時生效。
## 配置檔案使用
spring.profiles.active=xxx
## 硬編碼註解形式使用
@org.springframework.context.annotation.Profile
複製程式碼
- properties
Properties 在幾乎所有應用程式中都扮演著重要的角色,並且可能來自各種各樣的來源:properties 檔案、JVM系統屬性、系統環境變數、JNDI、Servlet Context 引數、ad-hoc Properties 物件、Map 等等。Environment 與 Properties 的關係是為使用者提供一個方便的服務介面,用於配置屬性源並從它們中解析屬性。
- Spring 中的擴充套件點
- spring framework 提供了便捷的方式新增自定義資料來源策略新增到 Spring Enviroment 中,如 @PropertySource。
- spring boot 提供了相關的擴充套件方式,如 EnviromentPostProcessor 相關的。docs.spring.io/spring-boot…
- spring boot 同時也提供在開始之前自定義環境擴充套件。docs.spring.io/spring-boot…
- spring framework 提供了便捷的方式新增自定義資料來源策略新增到 Spring Enviroment 中,如 @PropertySource。
Apollo 簡介
簡介
Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,能夠集中化管理應用不同環境、不同叢集的配置,配置修改後能夠實時推送到應用端,並且具備規範的許可權、流程治理等特性。
Apollo 支援4個維度管理 Key-Value 格式的配置:
- application (應用)
這個很好理解,就是實際使用配置的應用,Apollo 客戶端在執行時需要知道當前應用是誰,從而可以去獲取對應的配置。每個應用都需要有唯一的身份標識,我們認為應用身份是跟著程式碼走的,所以需要在程式碼中配置,具體資訊請參見 Java 客戶端使用指南。
- environment (環境)
配置對應的環境,Apollo 客戶端在執行時需要知道當前應用處於哪個環境,從而可以去獲取應用的配置。我們認為環境和程式碼無關,同一份程式碼部署在不同的環境就應該能夠獲取到不同環境的配置,所以環境預設是通過讀取機器上的配置(server.properties中的env屬性)指定的,不過為了開發方便,我們也支援執行時通過 System Property 等指定,具體資訊請參見Java客戶端使用指南。
- cluster (叢集)
一個應用下不同例項的分組,比如典型的可以按照資料中心分,把上海機房的應用例項分為一個叢集,把北京機房的應用例項分為另一個叢集。對不同的cluster,同一個配置可以有不一樣的值,如 zookeeper 地址。叢集預設是通過讀取機器上的配置(server.properties中的idc屬性)指定的,不過也支援執行時通過 System Property 指定,具體資訊請參見Java客戶端使用指南。
- namespace (名稱空間)
一個應用下不同配置的分組,可以簡單地把 namespace 類比為檔案,不同型別的配置存放在不同的檔案中,如資料庫配置檔案,RPC配置檔案,應用自身的配置檔案等。應用可以直接讀取到公共元件的配置 namespace,如 DAL,RPC 等。應用也可以通過繼承公共元件的配置 namespace 來對公共元件的配置做調整,如DAL的初始資料庫連線數。
同時,Apollo 基於開源模式開發,開源地址:github.com/ctripcorp/a…
基礎模型
如下即是Apollo的基礎模型:
- 使用者在配置中心對配置進行修改併發布
- 配置中心通知Apollo客戶端有配置更新
- Apollo客戶端從配置中心拉取最新的配置、更新本地配置並通知到應用
Apollo 架構說明
Apollo 專案本身就使用了 Spring Boot & Spring Cloud 開發。
服務端
上圖簡要描述了Apollo的總體設計,我們可以從下往上看:
- Config Service 提供配置的讀取、推送等功能,服務物件是Apollo客戶端。
- Admin Service 提供配置的修改、釋出等功能,服務物件是Apollo Portal(管理介面)。
- Config Service 和 Admin Service 都是多例項、無狀態部署,所以需要將自己註冊到 Eureka 中並保持心跳
- 在 Eureka 之上我們架了一層 Meta Server 用於封裝 Eureka 的服務發現介面 Client 通過域名訪問 Meta Server 獲取 Config Service 服務列表(IP+Port),而後直接通過 IP+Port 訪問服務,同時在 Client 側會做 load balance、錯誤重試
- Portal 通過域名訪問 Meta Server 獲取 Admin Service 服務列表(IP+Port),而後直接通過 IP+Port 訪問服務,同時在 Portal 側會做 load balance、錯誤重試
- 為了簡化部署,我們實際上會把 Config Service、Eureka 和 Meta Server 三個邏輯角色部署在同一個 JVM 程式中。
客戶端
- 客戶端和服務端保持了一個長連線,從而能第一時間獲得配置更新的推送。
- 客戶端還會定時從 Apollo 配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,為了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率預設為每5分鐘拉取一次,客戶端也可以通過在執行時指定 System Property: apollo.refreshInterval 來覆蓋,單位為分鐘。
- 客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會儲存在記憶體中
- 客戶端會把從服務端獲取到的配置在本地檔案系統快取一份
- 在遇到服務不可用,或網路不通的時候,依然能從本地恢復配置
- 應用程式從Apollo客戶端獲取最新的配置、訂閱配置更新通知
長連線實現上是使用的非同步+輪詢實現,具體實現的解析請檢視下面兩篇文章
Apollo 高可用部署
在 Apollo 架構說明中我們提到過 client 和 portal 都是在客戶端負載均衡,根據 ip+port 訪問服務,所以 config service 和 admin service 是無狀態的,可以水平擴充套件的,portal service 根據使用 slb 繫結多臺伺服器達到切換,meta server 同理。 | 場景 | 影響 | 降級 | 原因 | |:----|:----|:----|:----| | 某臺config service下線 | 無影響 | | Config service無狀態,客戶端重連其它config service | | 所有config service下線 | 客戶端無法讀取最新配置,Portal無影響 | 客戶端重啟時,可以讀取本地快取配置檔案 | | | 某臺admin service下線 | 無影響 | | Admin service無狀態,Portal重連其它admin service | | 所有admin service下線 | 客戶端無影響,portal無法更新配置 | | | | 某臺portal下線 | 無影響 | | Portal域名通過slb繫結多臺伺服器,重試後指向可用的伺服器 | | 全部portal下線 | 客戶端無影響,portal無法更新配置 | | | | 某個資料中心下線 | 無影響 | | 多資料中心部署,資料完全同步,Meta Server/Portal域名通過slb自動切換到其它存活的資料中心 |
Apollo 使用說明
使用說明
最佳實踐
在 Spring Boot & Spring Cloud 中使用。
- 每個應用都需要有唯一的身份標識,我們認為應用身份是跟著程式碼走的,所以需要在程式碼中配置。關於應用身份標識,應用標識對第三方中介軟體應該是統一的,擴充套件支援 apollo 身份標識和 spring.application.name 一致(具體檢視 fusion-config-apollo 中程式碼),其他中介軟體同理。
- 應用開發過程中如使用程式碼中的配置,應該充分利用 Spring Environment Profile,增加本地邏輯分組 local,非開發階段關閉 local 邏輯分組。同時關閉 apollo 遠端獲取配置,在 VM options 中增加 -Denv=local。
以下程式碼是擴充套件 apollo 應用標識使用 spring.application.name,並增加監控配置,監控一般是基礎架構團隊提供的功能,從基礎框架硬編碼上去,業務側做到完全無感知。
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.foundation.internals.io.BOMInputStream;
import com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.Set;
/**
* ApolloSpringApplicationRunListener
* <p>
* SpringApplicationRunListener
* 介面說明 https://blog.csdn.net/u011179993/article/details/51555690https://blog.csdn.net/u011179993/article/details/51555690
*
* @author Weichao Li ([email protected])
* @since 2019-08-15
*/
@Order(value = ApolloSpringApplicationRunListener.APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER)
@Slf4j
public class ApolloSpringApplicationRunListener implements SpringApplicationRunListener {
public static final int APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER = 1;
private static final String APOLLO_APP_ID_KEY = "app.id";
private static final String SPRINGBOOT_APPLICATION_NAME = "spring.application.name";
private static final String CONFIG_CENTER_INFRA_NAMESPACE = "infra.monitor";
public ApolloSpringApplicationRunListener(SpringApplication application,String[] args) {
}
/**
* 剛執行run方法時
*/
@Override
public void starting() {
}
/**
* 環境建立好時候
*
* @param env 環境資訊
*/
@Override
public void environmentPrepared(ConfigurableEnvironment env) {
Properties props = new Properties();
props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED,true);
props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED,true);
env.getPropertySources().addFirst(new PropertiesPropertySource("apolloConfig",props));
// 初始化appId
this.initAppId(env);
// 初始化基礎架構提供的預設配置,需在專案中關聯公共 namespaces
this.initInfraConfig(env);
}
/**
* 上下文建立好的時候
*
* @param context 上下文
*/
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
}
/**
* 上下文載入配置時候
*
* @param context 上下文
*/
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
}
@Override
public void started(ConfigurableApplicationContext context) {
}
@Override
public void running(ConfigurableApplicationContext context) {
}
@Override
public void failed(ConfigurableApplicationContext context,Throwable exception) {
}
/**
* 初始化 apollo appId
*
* @param env 環境資訊
*/
private void initAppId(ConfigurableEnvironment env) {
String apolloAppId = env.getProperty(APOLLO_APP_ID_KEY);
if (StringUtils.isEmpty(apolloAppId)) {
//此處需要判斷一下 meta-inf 下的檔案中的 app id
apolloAppId = getAppIdByAppPropertiesClasspath();
if (StringUtils.isEmpty(apolloAppId)) {
String applicationName = env.getProperty(SPRINGBOOT_APPLICATION_NAME);
if (!StringUtils.isEmpty(applicationName)) {
System.setProperty(APOLLO_APP_ID_KEY,applicationName);
} else {
throw new IllegalArgumentException(
"config center must config app.id in " + DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
}
} else {
System.setProperty(APOLLO_APP_ID_KEY,apolloAppId);
}
} else {
System.setProperty(APOLLO_APP_ID_KEY,apolloAppId);
}
}
/**
* 初始化基礎架構提供的配置
*
* @param env 環境資訊
*/
private void initInfraConfig(ConfigurableEnvironment env) {
com.ctrip.framework.apollo.Config apolloConfig = ConfigService.getConfig(CONFIG_CENTER_INFRA_NAMESPACE);
Set<String> propertyNames = apolloConfig.getPropertyNames();
if (propertyNames != null && propertyNames.size() > 0) {
Properties properties = new Properties();
for (String propertyName : propertyNames) {
properties.setProperty(propertyName,apolloConfig.getProperty(propertyName,null));
}
EnumerablePropertySource enumerablePropertySource =
new PropertiesPropertySource(CONFIG_CENTER_INFRA_NAMESPACE,properties);
env.getPropertySources().addLast(enumerablePropertySource);
}
}
/**
* 從 apollo 預設配置檔案中取 app.id 的值,調整優先順序在 spring.application.name 之前
*
* @return apollo app id
*/
private String getAppIdByAppPropertiesClasspath() {
try {
InputStream in = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
if (in == null) {
in = DefaultApplicationProvider.class
.getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
}
Properties properties = new Properties();
if (in != null) {
try {
properties.load(new InputStreamReader(new BOMInputStream(in),StandardCharsets.UTF_8));
} finally {
in.close();
}
}
if (properties.containsKey(APOLLO_APP_ID_KEY)) {
String appId = properties.getProperty(APOLLO_APP_ID_KEY);
log.info("App ID is set to {} by app.id property from {}",appId,DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
return appId;
}
} catch (Throwable ignore) {
}
return null;
}
}
複製程式碼
動態重新整理
支援 Apollo 配置自動重新整理型別,支援 @Value @RefreshScope @ConfigurationProperties 以及日誌級別的動態重新整理。具體程式碼檢視下文連結。
- @Value
@Value Apollo 本身就支援了動態重新整理,需要注意的是如果@Value 使用了 SpEL 表示式,動態重新整理會失效。
// 支援動態重新整理
@Value("${simple.xxx}")
private String simpleXxx;
// 不支援動態重新整理
@Value("#{'${simple.xxx}'.split(',')}")
private List<String> simpleXxxs;
複製程式碼
- @RefreshScope
RefreshScope(org.springframework.cloud.context.scope.refresh)是 Spring Cloud 提供的一種特殊的 scope 實現,用來實現配置、例項熱載入。
動態實現過程:
配置變更時,呼叫 refreshScope.refreshAll() 或指定 bean。提取標準引數(System,jndi,Servlet)之外所有引數變數,把原來的Environment裡的引數放到一個新建的 Spring Context 容器下重新載入,完事之後關閉新容器。提取更新過的引數(排除標準引數) ,比較出變更項,釋出環境變更事件,RefreshScope 用新的環境引數重新生成Bean。重新生成的過程很簡單,清除 refreshscope 快取幷銷燬 Bean,下次就會重新從 BeanFactory 獲取一個新的例項(該例項使用新的配置)。
- @ConfigurationProperties
apollo 預設是不支援 ConfigurationProperties 重新整理的,這塊需要配合 EnvironmentChangeEvent 重新整理的。
- 日誌級別
apollo 預設是不支援日誌級別重新整理的,這塊需要配合 EnvironmentChangeEvent 重新整理的。
- EnvironmentChangeEvent(Spring Cloud 提供)
當觀察到 EnvironmentChangeEvent 時,它將有一個已更改的鍵值列表,應用程式將使用以下內容: 1,重新繫結上下文中的任何 @ConfigurationProperties bean,程式碼見org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder。 2,為logging.level.*中的任何屬性設定記錄器級別,程式碼見 org.springframework.cloud.logging.LoggingRebinder。 支援動態重新整理
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
/**
* LoggerConfiguration
*
* @author Weichao Li ([email protected])
* @since 2019/11/14
*/
@Configuration
@Slf4j
public class ApolloRefreshConfiguration implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Autowired
private RefreshScope refreshScope;
@ApolloConfigChangeListener
private void onChange(ConfigChangeEvent changeEvent) {
applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
refreshScope.refreshAll();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
複製程式碼
注意原有配置如果有日誌級別需要初始化。
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.Set;
/**
* logging 初始化
*
* @author Weichao Li ([email protected])
* @since 2019/11/14
*/
@Configuration
@Slf4j
public class LoggingConfiguration {
private static final String LOGGER_TAG = "logging.level.";
private static final String DEFAULT_LOGGING_LEVEL = "info";
@Autowired
private LoggingSystem loggingSystem;
@ApolloConfig
private Config config;
@PostConstruct
public void changeLoggingLevel() {
Set<String> keyNames = config.getPropertyNames();
for (String key : keyNames) {
if (containsIgnoreCase(key,LOGGER_TAG)) {
String strLevel = config.getProperty(key,DEFAULT_LOGGING_LEVEL);
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG,""),level);
}
}
}
private static boolean containsIgnoreCase(String str,String searchStr) {
if (str == null || searchStr == null) {
return false;
}
int len = searchStr.length();
int max = str.length() - len;
for (int i = 0; i <= max; i++) {
if (str.regionMatches(true,i,searchStr,len)) {
return true;
}
}
return false;
}
}
複製程式碼
Apollo 最佳實踐 - 配置治理
許可權控制
由於配置能改變程式的行為,不正確的配置甚至能引起災難,所以對配置的修改必須有比較完善的許可權控制。應用和配置的管理都有完善的許可權管理機制,對配置的管理還分為了編輯和釋出兩個環節,從而減少人為的錯誤。所有的操作都有審計日誌,可以方便地追蹤問題
- everyone 要有自己的賬戶(最主要的前置條件)
- 每一個專案都至少有一個 owner(專案管理員,專案管理員擁有以下許可權)
- 可以管理專案的許可權分配
- 可以建立叢集
- 可以建立 Namespace
- 專案管理員(owner)根據組織結構分配配置許可權
- 編輯許可權允許使用者在 Apollo 介面上建立、修改、刪除配置
- 配置修改後只在 Apollo 介面上變化,不會影響到應用實際使用的配置
- 釋出許可權允許使用者在 Apollo 介面上釋出、回滾配置
- 配置只有在釋出、回滾動作後才會被應用實際使用到
- Apollo在使用者操作釋出、回滾動作後實時通知到應用,並使最新配置生效
- 編輯許可權允許使用者在 Apollo 介面上建立、修改、刪除配置
- 專案管理員管理許可權介面
專案建立完,預設沒有分配配置的編輯和釋出許可權,需要專案管理員進行授權。
- 點選application這個namespace的授權按鈕
- 分配修改許可權
- 分配發布許可權
Namespace
Namespace 許可權分類
apollo 獲取許可權分類分為私有的和公共的。
- private (私有的)
private許可權的Namespace,只能被所屬的應用獲取到。一個應用嘗試獲取其它應用private的Namespace,Apollo會報“404”異常。
- public (公共的)
public許可權的Namespace,能被任何應用獲取。
Namespace 的分類
Namespace 有三種型別,私有型別,公共型別,關聯型別(繼承型別)。
Apollo 私有型別 Namespace 使用說明
私有型別的 Namespace 具有 private 許可權。例如服務預設的“application” Namespace 就是私有型別。
- 使用場景
- 服務自身的配置(如資料庫、業務行為等配置)
- 如何使用私有型別 Namespace
一個應用下不同配置的分組,可以簡單地把namespace類比為檔案,不同型別的配置存放在不同的檔案中,如資料庫配置檔案,業務屬性配置,配置檔案等
Apollo 公共型別 Namespace 使用說明
公共型別的 Namespace 具有 public 許可權。公共型別的 Namespace 相當於遊離於應用之外的配置,且通過 Namespace 的名稱去標識公共 Namespace,所以公共的 Namespace 的名稱必須全域性唯一。
- 使用場景
- 部門級別共享的配置
- 小組級別共享的配置
- 幾個專案之間共享的配置
- 中介軟體客戶端的配置
- 如何使用公共型別 Namespace
- 程式碼侵入型
@EnableApolloConfig({"application","poizon-infra.jaeger"})
複製程式碼
- 配置方式形式
# will inject 'application' namespace in bootstrap phase
apollo.bootstrap.enabled = true
# will inject 'application','poizon-infra.jaeger' namespaces in bootstrap phase
apollo.bootstrap.namespaces = application,poizon-infra.jaeger
複製程式碼
Apollo 關聯型別 Namespace 使用說明
關聯型別又可稱為繼承型別,關聯型別具有 private 許可權。關聯型別的 Namespace 繼承於公共型別的 Namespace,用於覆蓋公共 Namespace 的某些配置。
使用建議
- 基礎框架部分的統一配置,如 DAL 的常用配置
- 基礎架構的公共元件的配置,如監控,熔斷等公共元件配置