Rick's lab

c-137


  • Home

  • Archives

DNS 配置有误导致监控误报的问题排查

Posted on 2018-08-09 | In Tech

背景

  • XXL-Job 是一套开源的任务调度框架,目前服务端部署了一套 XXL-Job 服务,用于监控线上服务质量
  • 由于 Ucloud 出现过几次内网域名服务器故障,我们在 XXL-Job 中增加了定时任务监控第三方域名连通状态,并报警到钉钉群

问题描述

  • 添加三方域名报警之后,钉钉群中经常收到报警,但是实际观察时,发现服务并无问题,报警为误报

排查过程

  • 查看报警对应的执行脚本,核心代码如下

      ping -c 5 target.com
      if (($? == 0))
      then
          //正常
      else
          //报警
          exit 1;
      fi
    
  • 查看报警对应的脚本执行日志,发现如下报错

      ping: unknown host
    
  • 初步判断是域名解析有问题导致报警,使用 dig 和 nslookup 查看域名解析情况,结果正常返回。由于报警也是偶发,觉得可能是域名解析偶发失败导致 ping 命令报错

      [work@monitor01 ~]$ nslookup target.com
      Server:       10.9.255.1
      Address:  10.9.255.1#53
    
      Non-authoritative answer:
      Name: target.com
      Address: xx.xx.xx.xx
    
  • 在 XXL-Job 部署的机器上尝试复现问题,脚本如下。执行脚本,直到有报警时停止,查看日志,没有失败的情况

      while true
      do
        echo "`date`============"
        ping -c1 target.com
        echo "result: $?"
      done
    
  • 在 XXL-Job 机器上观察 XXL-Job 进程,使用 ps -elf 命令追踪父进程。发现进程是运行在 Docker 容器中的

      [work@monitor01 ~]$ ps -elf | grep 31525
      0 S root     31525  1689  0  80      0 - 86935 futex_ Jun29 ?           00:16:10    /usr/bin/docker-containerd-shim    current    6b0f5c6aa2f52318867466a32774071    f7bbf540779ef92bb16ccf00494c6c5    /var/run/docker/libcontainerd/6    0f5c6aa2f52318867466a327740711f    bbf540779ef92bb16ccf00494c6c5e    /usr/libexec/docker/docker-runc-current
    
  • 容器和宿主机的网络环境是隔离的,所以 XXL-Job 报警时,宿主机上的脚本没有复现问题。于是在容器中继续执行脚本,尝试复现问题

  • 脚本执行几分钟后,在日志中观察到有报错的情况。这里注意到,报错的地方似乎花费的时间比较久

      Mon Jul  2 08:30:35 UTC     2018============
      ping: unknown host
      result: 1
      Mon Jul  2 08:30:37 UTC     2018============
    
  • 对脚本进行调整,记录调用时间

      time ping -c1 target.com
    
  • 重新执行脚本,发现报错时,ping 命令的调用花费了一秒左右的时间。感觉可能是 DNS 解析超时了。

      Mon Jul  2 09:43:12 UTC     2018============
      ping: unknown host
    
      real    0m1.049s
      user    0m0.002s
      sys     0m0.002s
      result: 1
    
  • 查看容器 DNS 解析配置

      root@f9c78250c2a0:~# cat /etc/resolv.conf
      search xxx.com
      nameserver 127.0.0.11
      options timeout:1 attempts:1 rotate single-request-reopen ndots:0
    
  • 通过 man resolv.conf 查阅 resolv.conf 参数的含义。发现容器内的 DNS 解析超时时间配置成了一秒,并且只尝试一次。DNS 使用 UDP 协议传输数据。UDP 协议设计上就是不可靠的,所以会存在丢包的情况,而且 DNS 解析需要去公网域名服务器请求数据,延迟也有不确定性。当 DNS 解析失败时,ping 命令就会报错,进而触发报警。

      timeout:n
        Sets the amount of time the resolver will wait for a response from a remote name server before retrying the query via a different name server.  Measured in seconds, the  default is RES_TIMEOUT (currently 5, see <resolv.h>).  The value for this option is silently capped to 30.
    
      attempts:n
        Sets  the  number  of  times  the resolver will send a query to its name servers before giving up and returning an error to the calling application.  The default is RES_DFLRETRY (currently 2, see <resolv.h>).  The value for this option is silently capped to 5.
    
  • 问题到这里基本定位完成,开始尝试解决问题。查阅 Docker 官方文档中关于网络的配置,发现可以通过 dns_opt 参数配置容器的 DNS 解析配置

      --dns-opt A key-value pair representing a DNS option and its value. See your operating system’s documentation for resolv.conf for valid options.
    
  • 修改容器配置,将 DNS 解析超时设置为 2s,重试一次。重启服务,观察半个小时,不再出现误报,问题解决

      dns_opt:
        - timeout:2
        - attempts:2
        - single-request
        - rotate
    

一句话总结

容器和宿主机 DNS 解析配置不同,导致容器内 ping 命令调用偶发失败,引发报警误报。通过 dns_opt 参数修改容器 DNS 超时时间和重试次数后,问题解决。

小试 Docker swarm mode

Posted on 2016-12-14 | In Tech

最近在公司尝试将测试环境 docker 化,由于组件比较多,考虑引入容器编排方案。首先尝试使用 docker-compose 来管理各个组件实例。可以跑通基本逻辑,但是目前没有服务发现框架,只能使用 docker 内置网络中的 DNS 发现其它服务,这限定了测试环境只能跑在单机环境上,时间久了各种 OOM。最近看到 docker 新版本附带的 swarm mode,尝试搭建了一套多机 docker 环境,感觉很满足当前的需求,并且对代码基本零侵入。简单的记录了一下搭建过程以及一些注意事项。

Swarm mode 简介

背景

  • docker swarm 是 docker 官方编排项目之一,提供将多个 docker engine 实例变成一台虚拟 docker engine 实例的能力,便于 docker 容器集群管理
  • docker 1.12 版本推出了 swarm mode 功能,将 docker swarm 深度集成到 docker engine 中,提供了整套更为简洁的 api 对 swarm 集群进行管理。相对于原生的 docker swarm,swarm mode 部署更为简单,无需外置的服务发现框架,并且提供了容器路由服务

概念

  • Node: node 是 swarm 集群中一个 docker engine 实例
    • Manager node: 对整体集群进行管理和编排,并维护整个集群
    • Worker node: 接收 manager node 分配的 tasks,并运行,默认情况下,manager node 本身也作为一个 worker node 运行 task,可以通过改变配置让 manager node 变成一个 manager-only 的纯管理节点
  • Task: swarm 集群任务的基本单元,通常是一个 docker container 的实例
  • Service: 由一组同类型 task 聚合而成,是 swarm 集群中主要操作对象
  • Load balancing: swarm 集群有一套内置的负载均衡策略提供外部访问到目标容器的路由,在创建 Service 时可以发布端口到外部网络上,访问 swarm 集群任意节点该端口的请求都会被路由到该 Service

Swarm mode quick start

初始化 swarm 集群

  1. 初始化机器,安装 docker-engine (版本需要高于 1.12)
  2. 选择一台机器作为 swarm manager 节点,在 console 中运行 docker swarm init 创建 swarm 集群,命令会返回加入集群的命令

     Swarm initialized: current node (c8dzd8xek2c9csoxj27ycl1nh) is now a manager.
    
     To add a worker to this swarm, run the following command:
    
         docker swarm join \
         --token SWMTKN-1-07bc8loetz13sy5eqv2t1uycqfr9dqabsd7x08gr22w7gj79fc-eiz5ljflasv65rnoq36aj2mfc \
         192.168.65.2:2377
    
     To add a manager to this swarm, run 'docker swarm join-token manager' and follow the     instructions.
    
  3. 在另一台机器上,运行提示中的命令加入集群,如果没有保存之前的提示,可以通过 docker swarm join-token worker 查询

     docker swarm join \
         --token SWMTKN-1-07bc8loetz13sy5eqv2t1uycqfr9dqabsd7x08gr22w7gj79fc-eiz5ljflasv65rnoq36aj2mfc \
         192.168.65.2:2377
    
  4. 在 manager 节点上运行 docker info 可以看到 swarm 集群的信息

     Swarm: active
      NodeID: c8dzd8xek2c9csoxj27ycl1nh
      Is Manager: true
      ClusterID: cj75fozp5effsiyuuq2dixv95
      Managers: 1
      Nodes: 2
      Orchestration:
       Task History Retention Limit: 5
      Raft:
       Snapshot Interval: 10000
       Heartbeat Tick: 1
       Election Tick: 3
      Dispatcher:
       Heartbeat Period: 5 seconds
      CA Configuration:
       Expiry Duration: 3 months
      Node Address: 192.168.65.2
    
  5. 在节点上运行 docker swarm leave 可以让当前节点离开集群,如果是管理节点,需要增加 --force 参数

创建一个 service

  1. 在 manager 节点上运行命令 docker service create --replicas 1 --name redis docker-registry-cn.easemob.com/redis创建 redis 服务, replicas 参数指定该服务中的 task 数量
    • 如果是私有 registry,需要在 manager 节点上执行 docker login,然后创建服务时,增加 --with-registry-auth 参数将认证信息发送到 worker 节点
  2. 在 manager 节点上运行 docker service ls 查看所有服务状态

     ID            NAME   REPLICAS      IMAGE                                     COMMAND
     0nqfp11n2xgb  redis  1/1           docker-registry-cn.easemob.    com/redis
    
  3. 在 manager 节点上运行 docker service ps redis 查看 redis 服务状态

     ID                         NAME     IMAGE                                 NODE  DESIRED STATE  CURRENT STATE         ERROR
     3qpkmxptn8o336h161183a0vz  redis.1  docker-registry-cn.easemob.com/redis  moby  Running        Running 17 hours ago
    
  4. 在 manager 节点上运行 docker service update redis 更新服务。例如可以用 docker service update redis --publish-add 6379:6379 发布 redis 服务到 swarm 集群 6379 端口上(也可以在创建服务时使用 –publish 参数发布),在 swarm 任意节点上运行 redis-cli 都可以连接到 redis 服务

  5. 在 manager 节点上运行 docker service rm redis 可以删除服务

Service 间网络调用

  • 如果 service publish 了端口,可以通过任意 swarm 节点 publish 端口进行调用

      ➜  ~ docker service update --publish-add 6379:6379 redis1
      redis1
      ➜  ~ telnet localhost 6379
      Trying ::1...
      Connected to localhost.
      Escape character is '^]'.
      set test aaa
      +OK
    
  • 创建一个 overlay 的网络,创建服务时,通过 --network 参数连接到 overlay 网络上。通过内建的 DNS 服务,可以通过 service name 解析出 task IP

      ➜  ~ docker network create --driver overlay --subnet 10.0.10.0/24 sandbox
      e7q4c68ybbw7xhrkb0at47k00
      ➜  ~ docker service create --with-registry-auth --replicas 1 --network sandbox --name redis1  docker-registry-cn.easemob.com/redis
      0dfqszxp925id4ma9blolyjmz
      ➜  ~ docker service create --with-registry-auth --replicas 1 --network sandbox --name redis2  docker-registry-cn.easemob.com/redis
      c3khd0snpu13ixx8popuoludx
      ➜  ~ docker ps
      CONTAINER ID        IMAGE                                         COMMAND                  CREATED             STATUS              PORTS               NAMES
      4c92e01d25fd        docker-registry-cn.easemob.com/redis:latest   "/entrypoint.sh redis"   12 seconds ago      Up 9 seconds        6379/tcp            redis2.1.4qbw5c0hawat5rr50hgzpxj7l
      c60b4659e768        docker-registry-cn.easemob.com/redis:latest   "/entrypoint.sh redis"   6 minutes ago       Up 6 minutes        6379/tcp            redis1.1.4pvn93a6i2n5teou41mddmlxr
      ➜  ~ docker exec -it c60b4659e768 /bin/bash
      root@c60b4659e768:/data# ping redis2
      PING redis2 (10.0.10.4): 56 data bytes
      64 bytes from 10.0.10.4: icmp_seq=0 ttl=64 time=0.109 ms
    

一些坑点

  • 截止 docker 最新版本(1.12.3),在 overlay 网络中,以 VIP 方式启动的服务,可以解析 DNS,但是无法 ping IP,某些依赖 ping 检查网络连通的程序会有问题,见 https://github.com/docker/docker/issues/25497。解决方案是启动模式换成 dnsrr (创建服务时增加参数 –endpoint-mode dnsrr),或者使用 tasks.${service_name} 的方式调用
  • 在某些操作系统(例如 Suse)上,service publish 的端口无法连接,原因不明,可能和缺少某些内核模块有关。建议使用 Centos7
  • swarm manager 会在 service 中 task 退出时重新提交 task,某些需要一次性运行的 docker image (例如环境初始化脚本的执行)endpoint 或者 cmd 需要修改成 block 形式,以免退出后被重复提交。Docker 1.13 版本允许 docker run 启动的容器 attach 到 swarm service 使用的 network 上,可以将一次性运行的 image 直接以 docker run 的方式启动
  • docker service update --image myimage:latest 不会拉取新镜像,即使远端有更新的版本,只能通过手动拉取镜像后重启 service 来更新服务。这个 bug 会在 docker 1.13 上被修复

Have fun :)

Storm 在线业务实践-集群空闲 CPU 飙高问题排查

Posted on 2015-07-18 | In Tech

最近将公司的在线业务迁移到 Storm 集群上,上线后遇到低峰期 CPU 耗费严重的情况。在解决问题的过程中深入了解了 Storm 的内部实现原理,并且解决了一个 Storm 0.9-0.10版本一直存在的严重 bug,目前代码已经合并到了 Storm 新版本中,在这篇文章里会介绍这个问题出现的场景、分析思路、解决的方式和一些个人的收获。

背景

首先简单介绍一下 Storm,老司机可以直接跳过这段。 Storm 是 Twitter 开源的一个大数据处理框架,专注于流式数据的处理。Storm通过创建拓扑结构(Topology)来转换数据流。和 Hadoop 的作业(Job)不同,Topology 会持续转换数据,除非被集群关闭。 下图是一个简单的 Storm Topology 结构图。

topology

可以看出Topology是由不同组件(Component)串/并联形成的有向图。数据元组(Tuple)会在Component之间通过数据流的形式进行有向传递。Component有两种

  • Spout:Tuple 来源节点,持续不断的产生Tuple,形成数据流
  • Bolt:Tuple 处理节点,处理收到的 Tuple,如果有需要,也可以生成新的 Tuple 传递到其他 Bolt

目前业界主要在离线或者对实时性要求不高业务中使用 Storm。随着 Storm 版本的更迭,可靠性和实时性在逐渐增强,已经有运行在线业务的能力。因此我们尝试将一些实时性要求在百毫秒级的在线业务迁入Storm 集群。

现象

  1. 某次高峰时,Storm 上的一个业务拓扑频繁出现消息处理延迟。延时达到了 10s 甚至更高。查看高峰时的物理机指标监控,CPU、内存和 IO 都有很大的余量。判断是随着业务增长,服务流量逐渐增加,某个 Bolt 之前设置的并行度不够,导致消息堆积了。
  2. 临时增加该 Bolt 并行度,解决了延迟的问题,但是第二天的低峰期,服务突然报警,CPU 负载过高,达到了 100%。

排查

  1. 用 Top 看了下 CPU 占用,系统调用占用了 70% 左右。再用 wtool 对 Storm 的工作进程进行分析,找到了 CPU 占用最高的线程

     java.lang.Thread.State: TIMED_WAITING (parking)
     at sun.misc.Unsafe.park(Native Method)
     - parking to wait for  <0x0000000640a248f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
     at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
     at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2163)
     at com.lmax.disruptor.BlockingWaitStrategy.waitFor(BlockingWaitStrategy.java:87)
     at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:54)
     at backtype.storm.utils.DisruptorQueue.consumeBatchWhenAvailable(DisruptorQueue.java:97)
     at backtype.storm.disruptor$consume_batch_when_available.invoke(disruptor.clj:80)
     at backtype.storm.daemon.executor$fn__3441$fn__3453$fn__3500.invoke(executor.clj:748)
     at backtype.storm.util$async_loop$fn__464.invoke(util.clj:463)
     at clojure.lang.AFn.run(AFn.java:24)
     at java.lang.Thread.run(Thread.java:745)
    

    我们可以看到这些线程都在信号量上等待。调用的来源是 disruptor$consume_batch_when_available。

  2. disruptor 是 Storm 内部消息队列的封装。所以先了解了一下 Storm 内部的消息传输机制。

    Storm内部消息传输机制(图片来源Understanding the Internal Message Buffers of Storm)

    Storm 的工作节点称为 Worker(其实就是一个JVM 进程)。不同 Worker 之间通过 Netty(旧版 Storm 使用 ZeroMQ)进行通讯。每个Worker 内部包含一组 Executor。Storm 会为拓扑中的每个 Component 都分配一个 Executor。在实际的数据处理流程中,数据以消息的形式在 Executor 之间流转。Executor 会循环调用绑定的 Component 的处理方法来处理收到的消息。 Executor 之间的消息传输使用队列作为消息管道。Storm 会给每个 Executor 分配两个队列和两个处理线程。

    • 工作线程:读取接收队列,对消息进行处理,如果产生新的消息,会写入发送队列
    • 发送线程:读取发送队列,将消息发送其他Executor

    当Executor的发送线程发送消息时,会判断目标Executor是否在同一Worker内,如果是,则直接将消息写入目标Executor的接收队列,如果不是,则将消息写入Worker的传输队列,通过网络发送。 Executor工作/发送线程读取队列的代码如下,这里会循环调用consume-batch-when-available读取队列中的消息,并对消息进行处理。

     (async-loop
       (fn []
         ...
         (disruptor/consume-batch-when-available receive-queue event-handler)
       ...
     ))
    
  3. 我们再来看一下 consume_batch_when_available 这个函数里做了什么。

     (defn consume-batch-when-available
       [^DisruptorQueue queue handler]
       (.consumeBatchWhenAvailable queue handler))
    

    前面提到 Storm 使用队列作为消息管道。Storm 作为流式大数据处理框架,对消息传输的性能很敏感,因此使用了高效内存队列 Disruptor Queue 作为消息队列。

    storm-simple

    Disruptor Queue 是 LMAX 开源的一个无锁内存队列。内部实现如下。

    Disruptor queue
    (图片来源 Disruptor queue Introduction)

    Disruptor Queue 通过 Sequencer 来管理队列,Sequencer 内部使用 RingBuffer 存储消息。RingBuffer 中消息的位置使用 Sequence 表示。队列的生产消费过程如下

    • Sequencer 使用一个 Cursor 来保存写入位置。
    • 每个 Consumer 都会维护一个消费位置,并注册到 Sequencer。
    • Consumer 通过 SequenceBarrier 和 Sequencer 进行交互。Consumer 每次消费时,SequenceBarrier 会比较消费位置和 Cursor 来判断是否有可用消息:如果没有,会按照设定的策略等待消息;如果有,则读取消息,修改消费位置。
    • Producer 在写入前会查看所有消费者的消费位置,在有可用位置时会写入消息,更新 Cursor。

    查看 DisruptorQueue.consumeBatchWhenAvailable 实现如下

     final long nextSequence = _consumer.get() + 1;
     final long availableSequence = _barrier.waitFor(nextSequence, 10, TimeUnit.MILLISECONDS);
     if (availableSequence >= nextSequence) {
         consumeBatchToCursor(availableSequence, handler);
     }
    

    继续查看 _barrier.waitFor 方法

     public long waitFor(final long sequence, final long timeout, final TimeUnit units) throws AlertException, InterruptedException {
         checkAlert();
         return waitStrategy.waitFor(sequence, cursorSequence, dependentSequences, this, timeout, units);
     }
    

    Disruptor Queue 为消费者提供了若干种消息等待策略

    • BlockingWaitStrategy:阻塞等待,CPU 占用小,但是会切换线程,延迟较高
    • BusySpinWaitStrategy:自旋等待,CPU 占用高,但是无需切换线程,延迟低
    • YieldingWaitStrategy:先自旋等待,然后使用 Thread.yield() 唤醒其他线程,CPU 占用和延迟比较均衡
    • SleepingWaitStrategy:先自旋,然后Thread.yield(),最后调用 LockSupport.parkNanos(1L),CPU 占用和延迟比较均衡

    Storm 的默认等待策略为 BlockingWaitStrategy。BlockingWaitStrategy 的 waitFor 函数实现如下

     if ((availableSequence = cursor.get()) < sequence) {
             lock.lock();
             try {
                 ++numWaiters;
                 while ((availableSequence = cursor.get()) < sequence) {
                     barrier.checkAlert();
    
                     if (!processorNotifyCondition.await(timeout, sourceUnit)) {
                         break;
                     }
                 }
             }
             finally {
                 --numWaiters;
                 lock.unlock();
             }
     }
    

    BlockingWaitStrategy 内部使用信号量来阻塞 Consumer,当 await 超时后,Consumer 线程会被自动唤醒,继续循环查询可用消息。

  4. 而DisruptorQueue.consumeBatchWhenAvailable 方法中可以看到,Storm 此处设置超时为 10ms。推测在没有消息或者消息量较少时,Executor 在消费队列时会被阻塞,由于超时时间很短,工作线程会频繁超时,consumeBatchWhenAvailable 会被频繁调用,导致 CPU 占用飙高。尝试将 10ms 修改成 100ms,编译 Storm 后重新部署集群,使用 Storm 的 demo 拓扑,将 bolt 并发度调到 1000,修改 spout 代码为 10s 发一条消息。经测试 CPU 占用大幅减少。再将 100ms 改成 1s,测试 CPU 占用基本降为零。
  5. 但是随着调高超时,测试时并没有发现消息处理有延时。继续查看 BlockingWaitStrategy 代码,发现 Disruptor Queue 的 Producer 在写入消息后会唤醒等待的 Consumer。

     if (0 != numWaiters)
     {
         lock.lock();
         try
         {
             processorNotifyCondition.signalAll();
         }
         finally
         {
             lock.unlock();
         }
     }
    

    这样,Storm 的 10ms 超时就很奇怪了,没有减少消息延时,反而增加了系统负载。带着这个疑问查看代码的上下文,发现在构造 DisruptorQueue 对象时有这么一句注释

     ;; :block strategy requires using a timeout on waitFor (implemented in DisruptorQueue), as sometimes the consumer stays blocked even when there's an item on the queue.
     (defnk disruptor-queue
         [^String queue-name buffer-size :claim-strategy :multi-threaded :wait-strategy :block]
         (DisruptorQueue. queue-name
                     ((CLAIM-STRATEGY claim-strategy) buffer-size)
                     (mk-wait-strategy wait-strategy)))
    

    Storm 使用的 Disruptor Queue 版本为2.10.1。查看 Disruptor Queue 的change log,发现该版本的 BlockingWaitStrategy 有潜在的并发问题,可能导致某条消息在写入时没有唤醒等待的消费者。

    2.10.2 Released (21-Aug-2012)

    • Bug fix, potential race condition in BlockingWaitStrategy.
    • Bug fix set initial SequenceGroup value to -1 (Issue #27).
    • Deprecate timeout methods that will be removed in version 3.

    因此 Storm 使用了短超时,这样在出现并发问题时,没有被唤醒的消费方也会很快因为超时重新查询可用消息,防止出现消息延时。 这样如果直接修改超时到 1000ms,一旦出现并发问题,最坏情况下消息会延迟 1000ms。在权衡性能和延时之后,我们在 Storm 的配置文件中增加配置项来修改超时参数。这样使用者可以自己选择保证低延时还是低 CPU 占用率。

  6. 就 BlockingWaitStrategy 的潜在并发问题咨询了Disruptor Queue的作者,得知2.10.4版本已经修复了这个并发问题(Race condition in 2.10.1 release )。将 Storm 依赖升级到此版本。但是对 Disruptor Queue 的 2.10.1 做了并发测试,无法复现这个并发问题,因此也无法确定 2.10.4 是否彻底修复。谨慎起见,在升级依赖的同时保留了之前的超时配置项,并将默认超时调整为 1000ms。经测试,在集群空闲时 CPU 占用正常,并且压测也没有出现消息延时。

总结

  1. 关于集群空闲CPU反而飙高的问题,已经向Storm社区提交PR并且已被接受 [STORM-935] Update Disruptor queue version to 2.10.4。在线业务流量通常起伏很大,如果被这个问题困扰,可以考虑应用此 patch。
  2. Storm UI 中可以看到很多有用的信息,但是缺乏记录,最好对其进行二次开发(或者直接读取 ZooKeeper 中信息),记录每个时间段的数据,方便分析集群和拓扑运行状况。

Xingyu Su

3 posts
1 categories
5 tags
© 2019 Xingyu Su
Powered by Hexo
|
Theme — NexT.Muse v5.1.4