apollo之ConfigService服務
apollo configService用於提供給client獲取配置資訊,以及配置更新後實時通知client的服務;configservice僅為client提供服務,且每個環境對應相應的configsevice叢集。
下面通過原始碼來分析configservice功能的具體實現
包路徑
com.ctrip.framework.apollo.configservice
對外API
ConfigController
/**
* 配置獲取控制層,供client根據名稱空間獲取Config資料資訊
* @author Jason Song(song_s@ctrip.com)
*/
@RestController
@RequestMapping("/configs")
public class ConfigController {
/**
* 配置操作服務
*/
@Autowired
private ConfigService configService;
@Autowired
private AppNamespaceServiceWithCache appNamespaceService;
/**
* 名稱空間工具類
*/
@Autowired
private NamespaceUtil namespaceUtil;
@Autowired
private InstanceConfigAuditUtil instanceConfigAuditUtil;
/**
* json解析器
*/
@Autowired
private Gson gson;
private static final Type configurationTypeReference = new TypeToken<Map<String, String>>() {
}.getType();
/**
* 查詢配置資訊
* @param appId 應用ID
* @param clusterName 叢集名稱
* @param namespace 名稱空間
* @param dataCenter 資料中心
* @param clientSideReleaseKey
* @param clientIp 客戶端IP
* @param messagesAsString
* @param request
* @param response
* @return
* @throws IOException
*/
@RequestMapping(value = "/{appId}/{clusterName}/{namespace:.+}", method = RequestMethod.GET)
public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,
@PathVariable String namespace,
@RequestParam(value = "dataCenter", required = false) String dataCenter,
@RequestParam(value = "releaseKey", defaultValue = "-1") String clientSideReleaseKey,
@RequestParam(value = "ip", required = false) String clientIp,
@RequestParam(value = "messages", required = false) String messagesAsString,
HttpServletRequest request, HttpServletResponse response) throws IOException {
//構建名稱空間
String originalNamespace = namespace;
//strip out .properties suffix
namespace = namespaceUtil.filterNamespaceName(namespace);
//fix the character case issue, such as FX.apollo <-> fx.apollo
namespace = namespaceUtil.normalizeNamespace(appId, namespace);
if (Strings.isNullOrEmpty(clientIp)) {
clientIp = tryToGetClientIp(request);
}
//轉換通知訊息
ApolloNotificationMessages clientMessages = transformMessages(messagesAsString);
//已釋出配置集合
List<Release> releases = Lists.newLinkedList();
String appClusterNameLoaded = clusterName;
if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
//載入給定當前引數下的所有已釋出的配置資訊
Release currentAppRelease = configService.loadConfig(appId, clientIp, appId, clusterName, namespace,
dataCenter, clientMessages);
if (currentAppRelease != null) {
//新增發布資訊
releases.add(currentAppRelease);
//we have cluster search process, so the cluster name might be overridden
appClusterNameLoaded = currentAppRelease.getClusterName();
}
}
//if namespace does not belong to this appId, should check if there is a public configuration
if (!namespaceBelongsToAppId(appId, namespace)) {
//查詢公共的釋出資訊
Release publicRelease = this.findPublicConfig(appId, clientIp, clusterName, namespace,
dataCenter, clientMessages);
if (!Objects.isNull(publicRelease)) {
//新增公共的釋出資訊
releases.add(publicRelease);
}
}
if (releases.isEmpty()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
String.format(
"Could not load configurations with appId: %s, clusterName: %s, namespace: %s",
appId, clusterName, originalNamespace));
Tracer.logEvent("Apollo.Config.NotFound",
assembleKey(appId, clusterName, originalNamespace, dataCenter));
return null;
}
auditReleases(appId, clusterName, dataCenter, clientIp, releases);
//合併釋出KEY 用於校驗配置是否有變更操作
String mergedReleaseKey = releases.stream().map(Release::getReleaseKey)
.collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR));
//校驗釋出KEY與客戶端已存在的釋出KEY是否一致
if (mergedReleaseKey.equals(clientSideReleaseKey)) {
//客戶端釋出EKY與查詢到的KEY一致,標識配置未做變更過操作,客戶端的配置為最新配置,返回304
// Client side configuration is the same with server side, return 304
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
Tracer.logEvent("Apollo.Config.NotModified",
assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));
return null;
}
//構建返回例項資訊
ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace,
mergedReleaseKey);
//新增發布配置資訊
apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));
Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded,
originalNamespace, dataCenter));
return apolloConfig;
}
private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
//Every app has an 'application' namespace
if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
return true;
}
//if no appId is present, then no other namespace belongs to it
if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
return false;
}
AppNamespace appNamespace = appNamespaceService.findByAppIdAndNamespace(appId, namespaceName);
return appNamespace != null;
}
/**
* 查詢所有的公共的釋出資訊記錄
* @param clientAppId the application which uses public config
* @param namespace the namespace
* @param dataCenter the datacenter
*/
private Release findPublicConfig(String clientAppId, String clientIp, String clusterName,
String namespace, String dataCenter, ApolloNotificationMessages clientMessages) {
AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);
//check whether the namespace's appId equals to current one
if (Objects.isNull(appNamespace) || Objects.equals(clientAppId, appNamespace.getAppId())) {
return null;
}
String publicConfigAppId = appNamespace.getAppId();
return configService.loadConfig(clientAppId, clientIp, publicConfigAppId, clusterName, namespace, dataCenter,
clientMessages);
}
/**
* 合併釋出的配置資訊
* Merge configurations of releases.
* Release in lower index override those in higher index
*/
Map<String, String> mergeReleaseConfigurations(List<Release> releases) {
//構建配置MAP key-V
Map<String, String> result = Maps.newHashMap();
//遍歷所有釋出的配置資訊
for (Release release : Lists.reverse(releases)) {
//組裝釋出的配置到map集合中
result.putAll(gson.fromJson(release.getConfigurations(), configurationTypeReference));
}
return result;
}
}
ConfigController
- /configs/{appId}/{clusterName}/{namespace:.+} :查詢給定引數下所有已釋出的配置集合,返回ApolloConfig
- 已釋出的配置,包含public和給定引數下配置兩部分
- 此API同時記錄當前應用例項資訊(Instance)到DB中,通過InstanceConfigAuditUtil類
- 此API用於獲取client端中Config的原始資料
ConfigFileController
程式碼略...
ConfigFileController
- /configfiles/{appId}/{clusterName}/{namespace:.+} : 查詢給定引數下所有釋出的配置集合,組裝成給定檔案格式的字串形式返回,(JSON或properties)格式
- 此API存在快取功能,快取儲存在記憶體中
- 已釋出的配置集合通過ConfigController獲取
NotificationControllerV2
程式碼略...
NotificationControllerV2
- /notifications/v2: 當釋出訊息有更新時通知client配置已變更
- 介面使用Http Long Polling方式實現,用於配置中心配置變更後動態通知客戶端
長連線實際上我們是通過Http Long Polling實現的,具體而言:
- 客戶端發起一個Http請求到服務端
- 服務端會保持住這個連線60秒
- 如果在60秒內有客戶端關心的配置變化,被保持住的客戶端請求會立即返回,並告知客戶端有配置變化的namespace資訊,客戶端會據此拉取對應namespace的最新配置
- 如果在60秒內沒有客戶端關心的配置變化,那麼會返回Http狀態碼304給客戶端
- 客戶端在收到服務端請求後會立即重新發起連線,回到第一步
配置查詢服務
ConfigService
/**
* 配置載入服務介面,用於載入釋出的配置資訊
* @author Jason Song(song_s@ctrip.com)
*/
public interface ConfigService extends ReleaseMessageListener {
/**
* 載入釋出配置資訊
* Load config
*
* @param clientAppId the client's app id 客戶端應用ID
* @param clientIp the client ip 客戶端IP
* @param configAppId the requested config's app id 配置應用ID
* @param configClusterName the requested config's cluster name 配置的叢集名稱
* @param configNamespace the requested config's namespace name 配置的名稱空間
* @param dataCenter the client data center 客戶端的資料中心
* @param clientMessages the messages received in client side 通知訊息
* @return the Release
*/
Release loadConfig(String clientAppId, String clientIp, String configAppId, String
configClusterName, String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages);
}
ConfigService介面
- 用於載入給定引數下所有已釋出的配置資訊
AbstractConfigService
/**
* 抽象的配置載入服務,用於載入釋出的配置資訊
* @author Jason Song(song_s@ctrip.com)
*/
public abstract class AbstractConfigService implements ConfigService {
@Autowired
private GrayReleaseRulesHolder grayReleaseRulesHolder;
/**
* 載入釋出配置的記錄
* @param clientAppId the client's app id 客戶端應用ID
* @param clientIp the client ip 客戶端IP
* @param configAppId the requested config's app id 配置應用ID
* @param configClusterName the requested config's cluster name 配置的叢集名稱
* @param configNamespace the requested config's namespace name 配置的名稱空間
* @param dataCenter the client data center 客戶端的資料中心
* @param clientMessages the messages received in client side 通知訊息
* @return
*/
@Override
public Release loadConfig(String clientAppId, String clientIp, String configAppId, String configClusterName,
String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages) {
//判斷叢集名稱是否為預設
// load from specified cluster fist
if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, configClusterName)) {
//根據叢集名稱查配置釋出記錄
Release clusterRelease = findRelease(clientAppId, clientIp, configAppId, configClusterName, configNamespace,
clientMessages);
if (!Objects.isNull(clusterRelease)) {
return clusterRelease;
}
}
// try to load via data center
if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, configClusterName)) {
//根據資料中心查詢配置釋出記錄
Release dataCenterRelease = findRelease(clientAppId, clientIp, configAppId, dataCenter, configNamespace,
clientMessages);
if (!Objects.isNull(dataCenterRelease)) {
return dataCenterRelease;
}
}
//載入預設的配置釋出記錄
// fallback to default release
return findRelease(clientAppId, clientIp, configAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, configNamespace,
clientMessages);
}
/**
* 查詢釋出記錄
* Find release
*/
private Release findRelease(String clientAppId, String clientIp, String configAppId, String configClusterName,
String configNamespace, ApolloNotificationMessages clientMessages) {
Long grayReleaseId = grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(clientAppId, clientIp, configAppId,
configClusterName, configNamespace);
Release release = null;
if (grayReleaseId != null) {
//查詢釋出記錄
release = findActiveOne(grayReleaseId, clientMessages);
}
if (release == null) {
//查詢最後的釋出記錄
release = findLatestActiveRelease(configAppId, configClusterName, configNamespace, clientMessages);
}
return release;
}
/**
* 根據ID查詢有效釋出記錄
* Find active release by id
*/
protected abstract Release findActiveOne(long id, ApolloNotificationMessages clientMessages);
/**
* 根據應用ID,叢集名稱、名稱空間查詢釋出記錄
* Find active release by app id, cluster name and namespace name
*/
protected abstract Release findLatestActiveRelease(String configAppId, String configClusterName,
String configNamespaceName, ApolloNotificationMessages clientMessages);
}
AbstractConfigService
- 查詢釋出配置的抽象實現,重新抽象了方法findLatestActiveRelease與findActiveOne供子類實現
DefaultConfigService
/**
* 預設的配置查詢服務,無快取功能
* config service with no cache
*
* @author Jason Song(song_s@ctrip.com)
*/
public class DefaultConfigService extends AbstractConfigService {
/**
* 釋出記錄操作服務,通過操作DB資源獲取釋出記錄
*/
@Autowired
private ReleaseService releaseService;
@Override
protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) {
//呼叫釋出記錄操作服務查詢配置釋出記錄
return releaseService.findActiveOne(id);
}
@Override
protected Release findLatestActiveRelease(String configAppId, String configClusterName, String configNamespace,
ApolloNotificationMessages clientMessages) {
//呼叫釋出記錄操作服務查詢配置釋出記錄
return releaseService.findLatestActiveRelease(configAppId, configClusterName,
configNamespace);
}
@Override
public void handleMessage(ReleaseMessage message, String channel) {
// since there is no cache, so do nothing
//無本地快取,每次獲取都是資料庫最近的釋出記錄,所以此處釋出記錄變更監聽處理函式不做任何操作
}
}
DefaultConfigService
- 無快取功能的實現,通過DB操作資源來查詢庫中的釋出配置資料
- DB操作資源:ReleaseService
ConfigServiceWithCache
/**
* 配置查詢服務,使用guava做本地快取,帶有本地快取功能的實現
* config service with guava cache
*
* @author Jason Song(song_s@ctrip.com)
*/
public class ConfigServiceWithCache extends AbstractConfigService {
private static final Logger logger = LoggerFactory.getLogger(ConfigServiceWithCache.class);
/**
* 預設的快取失效時長 1h
*/
private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES = 60;//1 hour
private static final String TRACER_EVENT_CACHE_INVALIDATE = "ConfigCache.Invalidate";
private static final String TRACER_EVENT_CACHE_LOAD = "ConfigCache.LoadFromDB";
private static final String TRACER_EVENT_CACHE_LOAD_ID = "ConfigCache.LoadFromDBById";
private static final String TRACER_EVENT_CACHE_GET = "ConfigCache.Get";
private static final String TRACER_EVENT_CACHE_GET_ID = "ConfigCache.GetById";
private static final Splitter STRING_SPLITTER =
Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings();
/**
* 釋出記錄操作服務
*/
@Autowired
private ReleaseService releaseService;
/**
* 釋出訊息操作服務
*/
@Autowired
private ReleaseMessageService releaseMessageService;
/**
* 構建一個釋出訊息ID與配置釋出記錄對應的關係快取
*/
private LoadingCache<String, ConfigCacheEntry> configCache;
private LoadingCache<Long, Optional<Release>> configIdCache;
/**
* 空的配置釋出實體
*/
private ConfigCacheEntry nullConfigCacheEntry;
public ConfigServiceWithCache() {
nullConfigCacheEntry = new ConfigCacheEntry(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, null);
}
/**
* 初始化方法,在例項建立後呼叫
*/
@PostConstruct
void initialize() {
//初始化本地快取
configCache = CacheBuilder.newBuilder()
.expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES)
.build(new CacheLoader<String, ConfigCacheEntry>() {
@Override
public ConfigCacheEntry load(String key) throws Exception {
//appId+clusterName+namespaceName
//根據KEY切分名稱空間資訊集合
List<String> namespaceInfo = STRING_SPLITTER.splitToList(key);
if (namespaceInfo.size() != 3) {
Tracer.logError(
new IllegalArgumentException(String.format("Invalid cache load key %s", key)));
return nullConfigCacheEntry;
}
Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD, key);
try {
//查詢最後配置釋出訊息
ReleaseMessage latestReleaseMessage = releaseMessageService.findLatestReleaseMessageForMessages(Lists
.newArrayList(key));
//查詢最後的配置釋出記錄
Release latestRelease = releaseService.findLatestActiveRelease(namespaceInfo.get(0), namespaceInfo.get(1),
namespaceInfo.get(2));
transaction.setStatus(Transaction.SUCCESS);
//構建通知ID,當最後配置釋出訊息為null 通知ID=-1,標識無通知資訊
long notificationId = latestReleaseMessage == null ? ConfigConsts.NOTIFICATION_ID_PLACEHOLDER : latestReleaseMessage
.getId();
//
if (notificationId == ConfigConsts.NOTIFICATION_ID_PLACEHOLDER && latestRelease == null) {
return nullConfigCacheEntry;
}
//構建快取例項, 通知ID-最後的配置釋出記錄
return new ConfigCacheEntry(notificationId, latestRelease);
} catch (Throwable ex) {
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
}
});
configIdCache = CacheBuilder.newBuilder()
.expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES)
.build(new CacheLoader<Long, Optional<Release>>() {
@Override
public Optional<Release> load(Long key) throws Exception {
Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD_ID, String.valueOf(key));
try {
//查詢配置釋出訊息
Release release = releaseService.findActiveOne(key);
transaction.setStatus(Transaction.SUCCESS);
return Optional.ofNullable(release);
} catch (Throwable ex) {
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
}
});
}
@Override
protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) {
Tracer.logEvent(TRACER_EVENT_CACHE_GET_ID, String.valueOf(id));
return configIdCache.getUnchecked(id).orElse(null);
}
@Override
protected Release findLatestActiveRelease(String appId, String clusterName, String namespaceName,
ApolloNotificationMessages clientMessages) {
//構建快取KEY
String key = ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName);
Tracer.logEvent(TRACER_EVENT_CACHE_GET, key);
//快取獲取
ConfigCacheEntry cacheEntry = configCache.getUnchecked(key);
//校驗快取是否已經失效,失效更新快取
//cache is out-dated
if (clientMessages != null && clientMessages.has(key) &&
clientMessages.get(key) > cacheEntry.getNotificationId()) {
//invalidate the cache and try to load from db again
invalidate(key);
cacheEntry = configCache.getUnchecked(key);
}
return cacheEntry.getRelease();
}
/**
* 校驗快取中的KEY
* @param key
*/
private void invalidate(String key) {
configCache.invalidate(key);
Tracer.logEvent(TRACER_EVENT_CACHE_INVALIDATE, key);
}
/**
* 釋出訊息監聽回撥函式,用於處理新記錄的釋出回撥
* @param message
* @param channel
*/
@Override
public void handleMessage(ReleaseMessage message, String channel) {
logger.info("message received - channel: {}, message: {}", channel, message);
if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(message.getMessage())) {
return;
}
try {
//校驗快取
invalidate(message.getMessage());
//在快取中獲取當前KEY的值,用於更新快取資訊
//warm up the cache
configCache.getUnchecked(message.getMessage());
} catch (Throwable ex) {
//ignore
}
}
/**
* 釋出訊息ID與釋出記錄對應關係實體
*/
private static class ConfigCacheEntry {
private final long notificationId;
private final Release release;
public ConfigCacheEntry(long notificationId, Release release) {
this.notificationId = notificationId;
this.release = release;
}
public long getNotificationId() {
return notificationId;
}
public Release getRelease() {
return release;
}
}
}
ConfigServiceWithCache
帶有本地快取功能的查詢服務實現,與DefaultConfigService比多了資料本地快取功能。
ConfigService對應操作DB庫
ApolloConfigDB
相關文章
- Apollo GraphQL 服務端實踐服務端
- 使用Apollo Server搭建GraphQL的服務端和客戶端Server服務端客戶端
- 服務與服務之間的呼叫
- linux之nfs服務LinuxNFS
- linux之lsync服務Linux
- SpringCloud之服務呼叫SpringGCCloud
- 微服務之Eureka服務發現微服務
- Redis服務之Redis ClusterRedis
- SpringCloud之服務註冊SpringGCCloud
- 患者管理之服務包
- 架構之:微服務和單體服務之爭架構微服務
- 《吃透微服務》 - 服務容錯之Sentinel微服務
- 微服務架構之「 服務註冊 」微服務架構
- 解析百度Apollo之Routing模組
- 【Apollo】(2)--- Apollo架構設計架構
- Linux之郵件mail服務LinuxAI
- 服務治理之重試篇
- Redis服務之常用配置(三)Redis
- Redis服務之常用配置(二)Redis
- Redis服務之常用配置(一)Redis
- WEB服務-Nginx之十-keepalivedWebNginx
- Dubbo原始碼之服務引用原始碼
- 服務與資料之爭
- eureka服務之間呼叫(3)
- 攜程開源分散式配置系統Apollo服務端是如何實時更新配置的?分散式服務端
- 微服務Consul系列之服務部署、搭建、使用微服務
- 微服務之服務註冊和服務發現篇微服務
- NetCore3.1 TCP服務之BeetleXNetCoreTCP
- ITIL4之IT服務戰略
- Dapr初體驗之服務呼叫
- 服務註冊中心之Eureka使用
- 服務網格service mesh 之 Linkerd
- SpringCloud 2020.0.4 系列之服務降級SpringGCCloud
- go-zero之支付服務一Go
- go-zero之支付服務二Go
- go-zero之App支付服務GoAPP
- HCNA Routing&Switching之DHCP服務
- 架構設計之“服務限流”架構