Apache?SkyWalking?修復(fù)TTL?timer?失效bug詳解
正文
近期,Apache SkyWalking 修復(fù)了一個(gè)隱藏了近4年的Bug - TTL timer 可能失效問(wèn)題,這個(gè) bug 在 SkyWalking <=9.2.0 版本中存在。 關(guān)于這個(gè) bug 的詳細(xì)信息可以看郵件列表 lists.apache.org/thread/ztp4… 具體如下

首先說(shuō)下這個(gè) Bug 導(dǎo)致的現(xiàn)象:
- 過(guò)期的索引不能被刪除,所有的OAP節(jié)點(diǎn)都出現(xiàn)類似日志
The selected first getAddress is xxx.xxx.xx.xx:port. The remove stage is skipped. - 對(duì)于以
no-init模式啟動(dòng)的 OAP 節(jié)點(diǎn),重啟的時(shí)候會(huì)一直打印類似日志table:xxx does not exist. OAP is running in 'no-init' mode, waiting... retry 3s later.
如果 SkyWalking OAP 出現(xiàn)上面的兩個(gè)問(wèn)題,很可能就是這個(gè) Bug 導(dǎo)致的。
下面我們先了解一下 SkyWalking OAP 集群方面的設(shè)計(jì)
SkyWalking OAP 角色
SkyWalking OAP 可選的角色有 Mixed、Receiver、Aggregator
- Mixed 角色主要負(fù)責(zé)接收數(shù)據(jù)、L1聚合和L2聚合;
- Receiver 角色負(fù)責(zé)接收數(shù)據(jù)和L1聚合;
- Aggregator 角色負(fù)責(zé)L2聚合。
默認(rèn)角色是 Mixed,可以通過(guò)修改 application.yml 進(jìn)行配置
core:
selector: ${SW_CORE:default}
default:
# Mixed: Receive agent data, Level 1 aggregate, Level 2 aggregate
# Receiver: Receive agent data, Level 1 aggregate
# Aggregator: Level 2 aggregate
role: ${SW_CORE_ROLE:Mixed} # Mixed/Receiver/Aggregator
restHost: ${SW_CORE_REST_HOST:0.0.0.0}
restPort: ${SW_CORE_REST_PORT:12800}
# 省略部分配置...
L1聚合:為了減少內(nèi)存及網(wǎng)絡(luò)負(fù)載,對(duì)于接收到的 metrics 數(shù)據(jù)進(jìn)行當(dāng)前 OAP 節(jié)點(diǎn)內(nèi)的聚合,具體實(shí)現(xiàn)參考 MetricsAggregateWorker#onWork() 方法的實(shí)現(xiàn);
L2聚合:又稱分布式聚合,OAP 節(jié)點(diǎn)將L1聚合后的數(shù)據(jù),根據(jù)一定的路由規(guī)則,發(fā)送給集群中的其他OAP節(jié)點(diǎn),進(jìn)行二次聚合,并入庫(kù)。具體實(shí)現(xiàn)見(jiàn) MetricsPersistentWorker 類。
SkyWalking OAP 集群
OAP 支持集群部署,目前支持的注冊(cè)中心有
- zookeeper
- kubernetes
- consul
- etcd
- nacos
默認(rèn)是 standalone,可以通過(guò)修改 application.yml 進(jìn)行配置
cluster:
selector: ${SW_CLUSTER:standalone}
standalone:
# Please check your ZooKeeper is 3.5+, However, it is also compatible with ZooKeeper 3.4.x. Replace the ZooKeeper 3.5+
# library the oap-libs folder with your ZooKeeper 3.4.x library.
zookeeper:
namespace: ${SW_NAMESPACE:""}
hostPort: ${SW_CLUSTER_ZK_HOST_PORT:localhost:2181}
# Retry Policy
baseSleepTimeMs: ${SW_CLUSTER_ZK_SLEEP_TIME:1000} # initial amount of time to wait between retries
maxRetries: ${SW_CLUSTER_ZK_MAX_RETRIES:3} # max number of times to retry
# Enable ACL
enableACL: ${SW_ZK_ENABLE_ACL:false} # disable ACL in default
schema: ${SW_ZK_SCHEMA:digest} # only support digest schema
expression: ${SW_ZK_EXPRESSION:skywalking:skywalking}
internalComHost: ${SW_CLUSTER_INTERNAL_COM_HOST:""}
internalComPort: ${SW_CLUSTER_INTERNAL_COM_PORT:-1}
kubernetes:
namespace: ${SW_CLUSTER_K8S_NAMESPACE:default}
# 省略部分配置...
OAP 啟動(dòng)的時(shí)候,如果當(dāng)前角色是 Mixed 或 Aggregator,則會(huì)將自己注冊(cè)到集群注冊(cè)中心,standalone 模式下也有一個(gè)內(nèi)存級(jí)集群管理器,參見(jiàn) StandaloneManager 類的實(shí)現(xiàn) 。
Data TTL timer 配置
application.yml 中的配置
core:
selector: ${SW_CORE:default}
default:
# Mixed: Receive agent data, Level 1 aggregate, Level 2 aggregate
# Receiver: Receive agent data, Level 1 aggregate
# Aggregator: Level 2 aggregate
role: ${SW_CORE_ROLE:Mixed} # Mixed/Receiver/Aggregator
restHost: ${SW_CORE_REST_HOST:0.0.0.0}
restPort: ${SW_CORE_REST_PORT:12800}
# 省略部分配置...
# Set a timeout on metrics data. After the timeout has expired, the metrics data will automatically be deleted.
enableDataKeeperExecutor: ${SW_CORE_ENABLE_DATA_KEEPER_EXECUTOR:true} # Turn it off then automatically metrics data delete will be close.
dataKeeperExecutePeriod: ${SW_CORE_DATA_KEEPER_EXECUTE_PERIOD:5} # How often the data keeper executor runs periodically, unit is minute
recordDataTTL: ${SW_CORE_RECORD_DATA_TTL:3} # Unit is day
metricsDataTTL: ${SW_CORE_METRICS_DATA_TTL:7} # Unit is day
# 省略部分配置...
- enableDataKeeperExecutor 自動(dòng)刪除過(guò)去數(shù)據(jù)的執(zhí)行器開(kāi)關(guān),默認(rèn)是開(kāi)啟的;
- dataKeeperExecutePeriod 執(zhí)行周期,默認(rèn)5分鐘;
- recordDataTTL record 數(shù)據(jù)的 TTL(Time To Live),單位:天;
- metricsDataTTL metrics 數(shù)據(jù)的 TTL,單位:天。
DataTTLKeeperTimer 定時(shí)任務(wù)
DataTTLKeeperTimer 負(fù)責(zé)刪除過(guò)期的數(shù)據(jù),SkyWalking OAP 在啟動(dòng)的時(shí)候會(huì)根據(jù) enableDataKeeperExecutor 配置決定是否開(kāi)啟 DataTTLKeeperTimer,也就是是否執(zhí)行 DataTTLKeeperTimer#start() 方法。 DataTTLKeeperTimer#start() 方法的執(zhí)行邏輯主要是通過(guò) JDK 內(nèi)置的 Executors.newSingleThreadScheduledExecutor() 創(chuàng)建一個(gè)單線程的定時(shí)任務(wù),執(zhí)行 DataTTLKeeperTimer#delete() 方法刪除過(guò)期的數(shù)據(jù), 執(zhí)行周期是dataKeeperExecutePeriod 配置值,默認(rèn)5分鐘執(zhí)行一次。

Bug 產(chǎn)生的原因
DataTTLKeeperTimer#start() 方法會(huì)在所有 OAP 節(jié)點(diǎn)啟動(dòng)一個(gè)定時(shí)任務(wù),那如果所有節(jié)點(diǎn)都去執(zhí)行數(shù)據(jù)刪除操作可能會(huì)有問(wèn)題,那么如何保證只有一個(gè)節(jié)點(diǎn)執(zhí)行呢?
如果讓我們?cè)O(shè)計(jì)的話,可能會(huì)引入一個(gè)分布式任務(wù)調(diào)度框架或者實(shí)現(xiàn)分布式鎖,這樣的話 SkyWalking 就要強(qiáng)依賴某個(gè)中間件了,SkyWalking 可能是考慮到了這些也沒(méi)有選擇這么實(shí)現(xiàn)。
那我們看下 SkyWalking 是如何解決這個(gè)問(wèn)題的呢,我們前面提到 OAP 在啟動(dòng)的時(shí)候,如果當(dāng)前角色是 Mixed 或 Aggregator,則會(huì)將自己注冊(cè)到集群注冊(cè)中心,SkyWalking OAP 調(diào)用 clusterNodesQuery#queryRemoteNodes() 方法,從注冊(cè)中心獲取這些節(jié)點(diǎn)的注冊(cè)信息(host:port)集合, 然后判斷集合中的第一個(gè)節(jié)點(diǎn)是否就是當(dāng)前節(jié)點(diǎn),如果是那么當(dāng)前節(jié)點(diǎn)執(zhí)行過(guò)期數(shù)據(jù)刪除操作,如下圖所示

節(jié)點(diǎn)A和節(jié)點(diǎn)集合中的第一個(gè)元素相等,則節(jié)點(diǎn)A負(fù)責(zé)執(zhí)行過(guò)期數(shù)據(jù)刪除操作。
這就要求 queryRemoteNodes 返回的節(jié)點(diǎn)集合是有序的,為什么這么說(shuō)呢, 試想一下,如果每個(gè) OAP 節(jié)點(diǎn)調(diào)用 queryRemoteNodes 方法返回的注冊(cè)信息順序不一致的話,就可能出現(xiàn)所有節(jié)點(diǎn)都不和集合中的第一個(gè)節(jié)點(diǎn)相等,這種情況下就沒(méi)有 OAP 節(jié)點(diǎn)能執(zhí)行過(guò)期數(shù)據(jù)刪除操作了,而 queryRemoteNodes 方法恰恰無(wú)法保證返回的注冊(cè)信息順序一致。
解決 Bug
我們既然知道了 bug 產(chǎn)生的原因,解決起來(lái)就比較簡(jiǎn)單了,只需要對(duì)獲取到的節(jié)點(diǎn)集合調(diào)用 Collections.sort() 方法對(duì) RemoteInstance(實(shí)現(xiàn)了java.lang.Comparable 接口)做排序,保證所有OAP節(jié)點(diǎn)做比較時(shí)都是一致的順序,代碼如下

相關(guān)代碼如下:
/**
* TTL = Time To Live
*
* DataTTLKeeperTimer is an internal timer, it drives the {@link IHistoryDeleteDAO} to remove the expired data. TTL
* configurations are provided in {@link CoreModuleConfig}, some storage implementations, such as ES6/ES7, provides an
* override TTL, which could be more suitable for the implementation. No matter which TTL configurations are set, they
* are all driven by this timer.
*/
@Slf4j
public enum DataTTLKeeperTimer {
INSTANCE;
private ModuleManager moduleManager;
private ClusterNodesQuery clusterNodesQuery;
private CoreModuleConfig moduleConfig;
public void start(ModuleManager moduleManager, CoreModuleConfig moduleConfig) {
this.moduleManager = moduleManager;
this.clusterNodesQuery = moduleManager.find(ClusterModule.NAME).provider().getService(ClusterNodesQuery.class);
this.moduleConfig = moduleConfig;
// 創(chuàng)建定時(shí)任務(wù)
Executors.newSingleThreadScheduledExecutor()
.scheduleAtFixedRate(
new RunnableWithExceptionProtection(
this::delete, // 刪除過(guò)期的數(shù)據(jù)
t -> log.error("Remove data in background failure.", t)
), moduleConfig
.getDataKeeperExecutePeriod(), moduleConfig.getDataKeeperExecutePeriod(), TimeUnit.MINUTES);
}
/**
* DataTTLKeeperTimer starts in every OAP node, but the deletion only work when it is as the first node in the OAP
* node list from {@link ClusterNodesQuery}.
*/
private void delete() {
IModelManager modelGetter = moduleManager.find(CoreModule.NAME).provider().getService(IModelManager.class);
List<Model> models = modelGetter.allModels();
try {
// 查詢服務(wù)節(jié)點(diǎn)
List<RemoteInstance> remoteInstances = clusterNodesQuery.queryRemoteNodes();
if (CollectionUtils.isNotEmpty(remoteInstances) && !remoteInstances.get(0).getAddress().isSelf()) {
log.info(
"The selected first getAddress is {}. The remove stage is skipped.",
remoteInstances.get(0).toString()
);
return;
}
// 返回的第一個(gè)節(jié)點(diǎn)是自己,則執(zhí)行刪除操作
log.info("Beginning to remove expired metrics from the storage.");
models.forEach(this::execute);
} finally {
log.info("Beginning to inspect data boundaries.");
this.inspect(models);
}
}
private void execute(Model model) {
try {
if (!model.isTimeSeries()) {
return;
}
if (log.isDebugEnabled()) {
log.debug(
"Is record? {}. RecordDataTTL {}, MetricsDataTTL {}",
model.isRecord(),
moduleConfig.getRecordDataTTL(),
moduleConfig.getMetricsDataTTL());
}
// 獲取 IHistoryDeleteDAO 接口的具體實(shí)現(xiàn)
moduleManager.find(StorageModule.NAME)
.provider()
.getService(IHistoryDeleteDAO.class)
.deleteHistory(model, Metrics.TIME_BUCKET,
model.isRecord() ? moduleConfig.getRecordDataTTL() : moduleConfig.getMetricsDataTTL()
);
} catch (IOException e) {
log.warn("History of {} delete failure", model.getName());
log.error(e.getMessage(), e);
}
}
private void inspect(List<Model> models) {
try {
moduleManager.find(StorageModule.NAME)
.provider()
.getService(IHistoryDeleteDAO.class)
.inspect(models, Metrics.TIME_BUCKET);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
更多技術(shù)細(xì)節(jié)大家可以參考下面的鏈接
相關(guān)鏈接
以上就是Apache SkyWalking 修復(fù)TTL timer 失效bug詳解的詳細(xì)內(nèi)容,更多關(guān)于Apache SkyWalking 修復(fù)bug的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Skywalking-agent調(diào)試說(shuō)明以trace-ignore為例
- SpringCloudGateway使用Skywalking時(shí)日志打印traceId解析
- Apache?SkyWalking?監(jiān)控?MySQL?Server?實(shí)戰(zhàn)解析
- 全鏈路監(jiān)控平臺(tái)Pinpoint?SkyWalking?Zipkin選型對(duì)比
- skywalking源碼解析javaAgent工具ByteBuddy應(yīng)用
- skywalking分布式服務(wù)調(diào)用鏈路追蹤APM應(yīng)用監(jiān)控
- skywalking容器化部署docker鏡像構(gòu)建k8s從測(cè)試到可用
- 解析Arthas協(xié)助排查線上skywalking不可用問(wèn)題
- python抓取skywalking中超過(guò)2s的告警接口
相關(guān)文章
JDK14新特性之switch表達(dá)式的實(shí)現(xiàn)
這篇文章主要介紹了JDK14新特性之switch表達(dá)式的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
Java項(xiàng)目中添加外部jar包的兩種方式(收藏版)
這篇文章主要介紹了java項(xiàng)目中添加外部jar包的兩種方式,第二種方式是將外部jar包引入到本地maven倉(cāng)庫(kù)中,本文給大家講解的非常詳細(xì),需要的朋友可以參考下2023-03-03
Java中數(shù)組的一些常見(jiàn)操作和技巧分析
這篇文章主要給大家介紹了關(guān)于Java中數(shù)組的一些常見(jiàn)操作和技巧分析的相關(guān)資料,數(shù)組(Array)是Java中的一種引用數(shù)據(jù)類型,是多個(gè)相同類型數(shù)據(jù)一定順序排列的集合,并使用一個(gè)名字命名,并通過(guò)編號(hào)的方式對(duì)這些數(shù)據(jù)進(jìn)行統(tǒng)一管理,需要的朋友可以參考下2023-08-08
IDEA使用MyBatisCodeHelperPro來(lái)generator代碼的詳細(xì)教程
這篇文章主要介紹了IDEA使用MyBatisCodeHelperPro來(lái)generator代碼的詳細(xì)教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
Java數(shù)據(jù)類型分類與基本數(shù)據(jù)類型轉(zhuǎn)換
這篇文章主要介紹了Java數(shù)據(jù)類型分類與基本數(shù)據(jù)類型轉(zhuǎn)換,Java的數(shù)據(jù)類型主要分為兩類,基本數(shù)據(jù)類型、引用數(shù)據(jù)類型,下文詳細(xì)介紹,感興趣的朋友可以參考一下2022-07-07

