正文

近期,Apache SkyWalking 修复了一个隐藏了近4年的Bug - TTL timer 可能失效问题,这个 bug 在 SkyWalking <=9.2.0 版本中存在。 关于这个 bug 的详细信息可以看邮件列表 lists.apache.org/thread/ztp4… 具体如下

首先说下这个 Bug 导致的现象:

  • 过期的索引不能被删除,所有的OAP节点都出现类似日志 The selected first getAddress is xxx.xxx.xx.xx:port. The remove stage is skipped.
  • 对于以 no-init 模式启动的 OAP 节点,重启的时候会一直打印类似日志 table:xxx does not exist. OAP is running in 'no-init' mode, waiting... retry 3s later.

如果 SkyWalking OAP 出现上面的两个问题,很可能就是这个 Bug 导致的。

下面我们先了解一下 SkyWalking OAP 集群方面的设计

SkyWalking OAP 角色

SkyWalking OAP 可选的角色有 Mixed、Receiver、Aggregator

  • Mixed 角色主要负责接收数据、L1聚合和L2聚合;
  • Receiver 角色负责接收数据和L1聚合;
  • Aggregator 角色负责L2聚合。

默认角色是 Mixed,可以通过修改 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}
# 省略部分配置...  

L1聚合:为了减少内存及网络负载,对于接收到的 metrics 数据进行当前 OAP 节点内的聚合,具体实现参考 MetricsAggregateWorker#onWork() 方法的实现;

L2聚合:又称分布式聚合,OAP 节点将L1聚合后的数据,根据一定的路由规则,发送给集群中的其他OAP节点,进行二次聚合,并入库。具体实现见 MetricsPersistentWorker 类。

SkyWalking OAP 集群

OAP 支持集群部署,目前支持的注册中心有

  • zookeeper
  • kubernetes
  • consul
  • etcd
  • nacos

默认是 standalone,可以通过修改 application.yml 进行配置

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 启动的时候,如果当前角色是 Mixed 或 Aggregator,则会将自己注册到集群注册中心,standalone 模式下也有一个内存级集群管理器,参见 StandaloneManager 类的实现 。

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 自动删除过去数据的执行器开关,默认是开启的;
  • dataKeeperExecutePeriod 执行周期,默认5分钟;
  • recordDataTTL record 数据的 TTL(Time To Live),单位:天;
  • metricsDataTTL metrics 数据的 TTL,单位:天。

DataTTLKeeperTimer 定时任务

DataTTLKeeperTimer 负责删除过期的数据,SkyWalking OAP 在启动的时候会根据 enableDataKeeperExecutor 配置决定是否开启 DataTTLKeeperTimer,也就是是否执行 DataTTLKeeperTimer#start() 方法。 DataTTLKeeperTimer#start() 方法的执行逻辑主要是通过 JDK 内置的 Executors.newSingleThreadScheduledExecutor() 创建一个单线程的定时任务,执行 DataTTLKeeperTimer#delete() 方法删除过期的数据, 执行周期是dataKeeperExecutePeriod 配置值,默认5分钟执行一次。

Bug 产生的原因

DataTTLKeeperTimer#start() 方法会在所有 OAP 节点启动一个定时任务,那如果所有节点都去执行数据删除操作可能会有问题,那么如何保证只有一个节点执行呢?

如果让我们设计的话,可能会引入一个分布式任务调度框架或者实现分布式锁,这样的话 SkyWalking 就要强依赖某个中间件了,SkyWalking 可能是考虑到了这些也没有选择这么实现。

那我们看下 SkyWalking 是如何解决这个问题的呢,我们前面提到 OAP 在启动的时候,如果当前角色是 Mixed 或 Aggregator,则会将自己注册到集群注册中心,SkyWalking OAP 调用 clusterNodesQuery#queryRemoteNodes() 方法,从注册中心获取这些节点的注册信息(host:port)集合, 然后判断集合中的第一个节点是否就是当前节点,如果是那么当前节点执行过期数据删除操作,如下图所示

节点A和节点集合中的第一个元素相等,则节点A负责执行过期数据删除操作。

这就要求 queryRemoteNodes 返回的节点集合是有序的,为什么这么说呢, 试想一下,如果每个 OAP 节点调用 queryRemoteNodes 方法返回的注册信息顺序不一致的话,就可能出现所有节点都不和集合中的第一个节点相等,这种情况下就没有 OAP 节点能执行过期数据删除操作了,而 queryRemoteNodes 方法恰恰无法保证返回的注册信息顺序一致。

解决 Bug

我们既然知道了 bug 产生的原因,解决起来就比较简单了,只需要对获取到的节点集合调用 Collections.sort() 方法对 RemoteInstance(实现了java.lang.Comparable 接口)做排序,保证所有OAP节点做比较时都是一致的顺序,代码如下

相关代码如下:

/**
 * 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;
        // 创建定时任务
        Executors.newSingleThreadScheduledExecutor()
                 .scheduleAtFixedRate(
                     new RunnableWithExceptionProtection(
                         this::delete, // 删除过期的数据
                         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 {
            // 查询服务节点
            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;
            }
            // 返回的第一个节点是自己,则执行删除操作
            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 接口的具体实现
            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);
        }
    }
}

更多技术细节大家可以参考下面的链接

相关链接

  • lists.apache.org/thread/ztp4…
  • github.com/apache/skyw…
  • github.com/apache/skyw…

以上就是Apache SkyWalking 修复TTL timer 失效bug详解的详细内容,更多关于Apache SkyWalking 修复bug的资料请关注Devmax其它相关文章!

Apache SkyWalking 修复TTL timer 失效bug详解的更多相关文章

  1. IOs Cordova长按显示文本选择放大镜即使禁用文本选择,如何删除?

    是否有任何可能导致此问题的插件?任何帮助深表感谢.Cordova插件:>com.mbppower.camerapreview>cordova-plugin-statusbar>cordova-plugin-whitelist>离子插件键盘>org.apache.cordova.camera>org.apache.cordova.console>org.apache.cordova.device>org.apache.cordova.dialogs>org.apache.cordova.file>org.a

  2. android – org.apache.cordova.api不存在. PhoneGap 3.0

    我正在尝试将VideoPlayer插件(https://github.com/macdonst/VideoPlayer)添加到我的phonegapAndroid应用程序中.在编译时遇到问题:第25行:解决方法将您的导入更改为:

  3. 如何将android客户端连接到我的笔记本电脑内的Apache服务器(php)的localhost?

    我的笔记本电脑中的localhost-127.0.0.1或android10.0.0.1中的localhost?>那么,如果我想从android访问localhost来调用PHP来运行?哪个ip地址/url我需要放在Android应用程序?我需要在httpconfig中为XAMPP修改任何内容吗?解决方法使用ipconfig在笔记本电脑中找到您的IP地址.在手机中使用该地址而不是127.0.0.1.

  4. android – 在android工作室中的proguard错误

    我想在我的应用程序中使用proguard,我启用它但是当我想生成apk文件时,它给了我这个错误:我正在使用最新版本的sdk23,这是我的gradle文件:怎么了?我在这段代码中做错了什么?谢谢解决方法只需在proguard上添加:

  5. 无法修复Android Proguard返回错误代码1错误

    当我尝试在我的Android应用程序中使用proguard时只需添加到我的project.properties文件,APK导出失败并显示消息Proguard返回错误代码1这是我的project.properties文件这是错误堆栈:解决方法将这些行添加到proguard配置文件(proguard-android.txt)见ProguardTroubleshooting请注意,如果您使用您的配置文件

  6. Phonegap 2.4 Android Proguard配置

    有人有主意吗???

  7. android – 如何在sharedPreferences中分析ANR

    在sharedPreferences中遇到ANR,不知道如何定位问题.以下是trace的三个部分,其他大多数线程都是“WAIT”或“TIMED_WAIT”.由于countdownlatch.await(),“主”线程被阻止.第二个线程“pool-1-thread-1”等待fsync.最后一个是试图读一些东西.我认为第二个线程已经阻塞了主线程,因为如果这个无法完成,它将不会调用countdownla

  8. Android无法访问org.apache.http.client.HttpClient

    我正在使用androidstudio创建一个向服务器发出GET请求的应用程序.我的代码是这样的:问题是AndroidStudio标记了这一行有错误:说“无法访问org.apache.http.client.HttpClient”这是我的gradle文件:解决方法在AndroidSDK23中不推荐使用HttpClient,因为它推断,您可以在HttpURLConnection中迁移代码https:/

  9. Android L – 没有对等证书

    我开发了一个小应用程序,使用带有自签名证书的SSL连接到我的服务器.为了使它工作,我使用BouncyCastleProvider将我的证书加载到自定义密钥库中,并在我的自定义SSLSocketFactory中导入证书.Everythink在android2.3(最小sdk)到4.4.4之间运行良好.但在androidL(预览版)中,我的应用失败了:TueAug1214:34:40BRT2014:j

  10. Android Http服务器和破碎的管道

    标题可能有问题吗?

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部