在 clouddriver 中缓存云厂商的各项数据,是由这些 agent 所来完成。本文主要聚焦于何时执行、如何存储、以及存储后如何使用这三点上。
ProviderUtils.rescheduleAgents()
上面的 agentScheduler 的实现类为 com.netflix.spinnaker.cats.redis.cluster.ClusteredAgentScheduler
,具体操作就是将待添加的 100 个 agents 全部放到其中的 map 中,并以 accountName+region+className 作为 key。
到此时,已经把新增的 agents 全部安排好了。
首先,ClusteredAgentScheduler 实现类 Runnable
并且,在其实现的 run 方法中,会将所有的 agents 封装成一个 Runnable,然后丢到 agentExecutionPool 中执行。
@Override
public void run() {
try {
runAgents();
}//....
}
private void runAgents() {
Map<String, NextAttempt> thisRun = acquire();
activeAgents.putAll(thisRun);
for (final Map.Entry<String, NextAttempt> toRun : thisRun.entrySet()) {
final AgentExecutionAction exec = agents.get(toRun.getKey());
agentExecutionPool.submit(new AgentJob(toRun.getValue(), exec, this));
}
}
首先 ClusteredAgentScheduler 作为一个 Runnable,被加入到一个 ScheduledExecutor 中,并且它的执行周期为 30s。
Executors.newSingleThreadScheduledExecutor(
new NamedThreadFactory(ClusteredAgentScheduler.class.getSimpleName())),
// ...
Integer lockInterval = agentLockAcquisitionIntervalSeconds == null ? 1 : agentLockAcquisitionIntervalSeconds;
lockPollingScheduler.scheduleAtFixedRate(this, 0, lockInterval, TimeUnit.SECONDS);
agentLockAcquisitionIntervalSeconds 最终来源为 yaml 文件中的配置数值
即:
上面的描述,并不是特别直观,执行到 agent 拉取逻辑的调用链路不是特别明显,它的调用链路如下:
agentScheduler.schedule(agent, agent.getAgentExecution(catsModule.providerRegistry), catsModule.executionInstrumentation)
// ...
// 其中 agentExecution 最终来自 agent,也就是抽象类 CachingAgent 中的内部类。
AgentExecutionAction agentExecutionAction =
new AgentExecutionAction(agent, agentExecution, executionInstrumentation);
final AgentExecutionAction exec = agents.get(toRun.getKey());
agentExecutionPool.submit(new AgentJob(toRun.getValue(), exec, this));
AgentJob
的 run 方法中,执行 AgentExecutionAction
的 execute()
方法private final AgentExecutionAction action;
@Override
public void run() {
Status status = Status.FAILURE;
try {
status = action.execute();
} finally {
scheduler.agentCompleted(
action.getAgent().getAgentType(), lockReleaseTime.getNextTime(status));
}
}
AgentExecutionAction
中执行 agentExecution 的 executeAgent(agent) 方法Status execute() {
try {
executionInstrumentation.executionStarted(agent);
long startTime = System.nanoTime();
agentExecution.executeAgent(agent);
executionInstrumentation.executionCompleted(
agent, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
return Status.SUCCESS;
} catch (Throwable cause) {
executionInstrumentation.executionFailed(agent, cause);
return Status.FAILURE;
}
}
@Override
public void executeAgent(Agent agent) {
AgentIntrospection introspection = new DefaultAgentIntrospection(agent);
CacheResult result = executeAgentWithoutStore(agent); // 此处进入 agent 拉取逻辑
introspection.finish(result);
CacheIntrospectionStore.getStore().recordAgent(introspection);
storeAgentResult(agent, result);
}
public CacheResult executeAgentWithoutStore(Agent agent) {
CachingAgent cachingAgent = (CachingAgent) agent;
ProviderCache cache = providerRegistry.getProviderCache(cachingAgent.getProviderName());
return cachingAgent.loadData(cache); // 调用 agent 的拉取逻辑
}
获取数据的入口是函数 loadData()
,只负责从云账号拉取信息,并将信息按照一定的形式组装后返回,交给后续的逻辑处理模板进行处理。此处获取数据及组织数据的逻辑,根据 agent 类型的不同,有很大的出入,但是他们的共同点在于,组装所需返回的数据的形式。
以 TencentImageCachingAgent
为例,它有一个函数,实现了某个接口,如下:
这里有两个问题:
getProvidedDataTypes()
何时调用?用于什么场景?AgentDataType
?有什么用处?String typeName
和 Authority authority
,在此处 typeName 就是 NAMESPACE 中所对应的云主机所拥有的信息类型,而 authority 有两个值,一个是 AUTHORITATIVE 表示此数据是资源数据,一个是 INFORMATIVE 表示此数据为关联数据(起关联作用)。最后,loadData()
函数返回的数据,就是以上述 AgentDataType
所包含的值作为 key 的 map。
在这一部分的代码中,逻辑非常多且略带抽象,因为这一部分代码,是所有 agent 拉完数据后,进行存储的统一模板代码,有部分代码处理个别 agent 数据的情况出现。但是化繁为简,它的目的就是做存储,并且此时的存储方式,有两种,资源对应 AUTHORITATIVE;关联数据对应 INFORMATIVE。抛开 agent 的差异,专注到存储的逻辑中,它的逻辑其实也很明了。
存储数据的位置在:CacheingAgent.java 的内部类 CacheExecution
的 executeAgent()
的最后一行,如下:
从 storeAgentResult(agent, result)
出发。准备工作其实主要是确定 agent 拉回的数据,哪些是需要存储的,以及用什么样的方式进行存储。
进入 SqlProviderCache.kt
的 putCacheResult 方法。它的代码很长,但是主要根据 agent 数据类型的不同,最终调用两个方法,即:
private fun cacheDataType(type: String, agent: String, items: Collection<CacheData>, authoritative: Boolean, cleanup: Boolean)
private fun cacheDataType(type: String, agent: String, items: Collection<CacheData>, authoritative: Boolean) {
cacheDataType(type, agent, items, authoritative, cleanup = true)
}
本质上是调用同一个方法,也就是第一个方法,只是入参会有所不同。所以 putCacheResult 的主要逻辑可归纳如下:
上面便是对此函数的抽象概括,接下来进入实际的存储操作中。按照上面的分析,实际 db 操作包括两种,即增、删。
删除相对比较简单。
override fun evictDeletedItems(type: String, ids: Collection<String>) {
// ...
backingStore.evictAll(type, ids)
// ...
}
override fun evictAll(type: String, ids: Collection<String>) {
// ...
var deletedCount = 0
var opCount = 0
try {
ids.chunked(dynamicConfigService.getConfig(Int::class.java, "sql.cache.read-batch-size", 500)) { chunk ->
withRetry(RetryCategory.WRITE) {
jooq.deleteFrom(table(resourceTableName(type)))
.where("id in (${chunk.joinToString(",") { "'$it'" }})")
.execute()
}
deletedCount += chunk.size
opCount += 1
}
}
//...
}
增加的逻辑在 cacheDataType 方法中,处理逻辑比较多,比较绕。其中包括了覆盖更新、追加更新两种。
从 SqlProviderCache.kt
的 cacheDataType() 进入 SqlCache.kt
的 mergeAll() 方法,便开始了新增逻辑。
val storeResult = if (authoritative) {
storeAuthoritative(type, agent, items, cleanup)
} else {
storeInformative(type, items, cleanup)
}
AUTHORITATIVE 和 INFORMATIVE 的区别:
它们通过 type 来获取表名的函数的实现分别为:
private fun resourceTableName(type: String): String =
checkTableName("cats_v${schemaVersion}_", sanitizeType(type), "")
private fun relTableName(type: String): String =
checkTableName("cats_v${schemaVersion}_", sanitizeType(type), "_rel")
另外它们两,还有一个特性上的区别,如下(来自:AgentDataType.java):
If an agent is an Authoritative source of data, then it’s resulting data set will be considered the current complete set for that data source. If an agent is an Informative source of data, its results will contribute to the data set for that type, but is never considered the complete set of data, so will not result in deletions when elements are no longer present.
AUTHORITATIVE 的更新资源的逻辑如下:
// 从表中取出已经存在的信息,包括 body_hash 和 id
val existingHashIds = getHashIds(type, agent)
// 找出已存在信息的 id
val existingIds = existingHashIds
.asSequence()
.map { it.id }
.toSet()
// 插入新的信息
// 代码过长,省略
// 根据 id 删除已存在的信息
if (!cleanup) {
return result
}
val toDelete = existingIds
.asSequence()
.filter { !currentIds.contains(it) }
.toSet()
evictAll(type, toDelete)
可以看出来,cleanup 为 true 的时候,才会删除之前的信息,即 cleanup 为 true 才覆盖更新。
此类型的 db 相关代码逻辑较绕,即函数 storeInformative(type, items, cleanup) 中,引入了很多变量,但通过命名揣测相应含义并不太可行,需要先行知晓一些概念。用到此类型的腾讯云 agent 如下:
外加一个 TencentLoadBalancerInstanceStateCachingAgent,它所实现的接口 HealthProvidingCachingAgent 引用了 INFORMATIVE,即:
此处仅以 TencentLoadBalancerInstanceStateCachingAgent 为例,此 Agent 返回一个 AUTHORITATIVE 类型的 health,还有一个 INFORMATIVE 类型的 instances。health 不看,只看 instances,且直接到 storeInformative(type, items, cleanup)。
与 AUTHORITATIVE 相比,cleanup 仍然是决定是否删除先前数据的一个开关,但是可以看出: