源:http://daiwa.ninja/index.php/2015/07/18/storm-cpu-overload/

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

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

背景

首先简单介绍一下Storm,熟悉的同学可以直接跳过这段。

Storm是Twitter开源的一个大数据处理框架,专注于流式数据的处理。Storm通过创建拓扑结构(Topology)来转换数据流。和Hadoop的作业(Job)不同,Topology会持续转换数据,除非被集群关闭。

下图是一个简单的Storm 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占用最高的线程

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

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

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

    (图片来源Understanding the Internal Message Buffers of Storm

    Storm的工作节点称为Worker(其实就是一个JVM进程)。不同Worker之间通过Netty(旧版Storm使用ZeroMQ)进行通讯。

    每个Worker内部包含一组Executor。Strom会为拓扑中的每个Component都分配一个Executor。在实际的数据处理流程中,数据以消息的形式在Executor之间流转。Executor会循环调用绑定的Component的处理方法来处理收到的消息。

    Executor之间的消息传输使用队列作为消息管道。Storm会给每个Executor分配两个队列和两个处理线程。

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

    当Executor的发送线程发送消息时,会判断目标Executor是否在同一Worker内,如果是,则直接将消息写入目标Executor的接收队列,如果不是,则将消息写入Worker的传输队列,通过网络发送。

    Executor工作/发送线程读取队列的代码如下,这里会循环调用consume-batch-when-available读取队列中的消息,并对消息进行处理。

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

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

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

    (图片来源Disruptor queue Introduction

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

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

    查看DisruptorQueue.consumeBatchWhenAvailable实现如下

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

    继续查看_barrier.waitFor方法

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

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

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

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

    1. if ((availableSequence = cursor.get()) < sequence) {
    2. lock.lock();
    3. try {
    4. ++numWaiters;
    5. while ((availableSequence = cursor.get()) < sequence) {
    6. barrier.checkAlert();
    7. if (!processorNotifyCondition.await(timeout, sourceUnit)) {
    8. break;
    9. }
    10. }
    11. }
    12. finally {
    13. --numWaiters;
    14. lock.unlock();
    15. }
    16. }

    BlockingWaitStrategy内部使用信号量来阻塞Consumer,当await超时后,Consumer线程会被自动唤醒,继续循环查询可用消息。这里的实现有个BUG,在processorNotifyCondition.await超时时应该循环查询,但是代码中实际上跳出了循环,直接返回的当前的cursor,

  4. 而DisruptorQueue.consumeBatchWhenAvailable方法中可以看到,Storm此处设置超时为10ms。推测在没有消息或者消息量较少时,Executor在消费队列时会被阻塞,由于超时时间很短,工作线程会频繁超时,再加上BlockingWaitStrategy的BUG,consumeBatchWhenAvailable会被频繁调用,导致CPU占用飙高。

    尝试将10ms修改成100ms,编译Storm后重新部署集群,使用Storm的demo拓扑,将bolt并发度调到1000,修改spout代码为10s发一条消息。经测试CPU占用大幅减少。

    再将100ms改成1s,测试CPU占用基本降为零。

  5. 但是随着调高超时,测试时并没有发现消息处理有延时。继续查看BlockingWaitStrategy代码,发现Disruptor Queu的Producer在写入消息后会唤醒等待的Consumer。

    1. if (0 != numWaiters)
    2. {
    3. lock.lock();
    4. try
    5. {
    6. processorNotifyCondition.signalAll();
    7. }
    8. finally
    9. {
    10. lock.unlock();
    11. }
    12. }

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

    1. ;; :block strategy requires using a timeout on waitFor (implemented in DisruptorQueue),
    2. as sometimes the consumer stays blocked even when there's an item on the queue.
    3. (defnk disruptor-queue
    4. [^String queue-name buffer-size :claim-strategy :multi-threaded :wait-strategy :block]
    5. (DisruptorQueue. queue-name
    6. ((CLAIM-STRATEGY claim-strategy) buffer-size)
    7. (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中信息),记录每个时间段的数据,方便分析集群和拓扑运行状况。

Posted in Tech
Tagged Storm

STORM在线业务实践-集群空闲CPU飙高问题排查的更多相关文章

  1. STORM在线业务实践-集群空闲CPU飙高问题排查(转)

    最近将公司的在线业务迁移到Storm集群上,上线后遇到低峰期CPU耗费严重的情况.在解决问题的过程中深入了解了storm的内部实现原理,并且解决了一个storm0.9-0.10版本一直存在的严重bug ...

  2. 一次FGC导致CPU飙高的排查过程

    今天测试团队反馈说,服务A的响应很慢,我在想,测试环境也会慢?于是我自己用postman请求了一下接口,真的很慢,竟然要2s左右,正常就50ms左右的. 于是去测试服务器看了一下,发现服务器负载很高, ...

  3. 生产系统CPU飙高问题排查

    现状 生产系统CPU占用过高,并且进行了报警 排查方法 执行top命令,查看是那个进程导致的,可以确定是pid为22168的java应用导致的 执行top -Hp命令,查看这个进程的那个线程导致cpu ...

  4. 记一次JAVA进程导致Kubernetes节点CPU飙高的排查与解决

    一.发现问题 在一次系统上线后,我们发现某几个节点在长时间运行后会出现CPU持续飙升的问题,导致的结果就是Kubernetes集群的这个节点会把所在的Pod进行驱逐(调度):如果调度到同样问题的节点上 ...

  5. HBase最佳实践 - 集群规划

    本文由  网易云发布. 作者:范欣欣 本篇文章仅限本站分享,如需转载,请联系网易获取授权. HBase自身具有极好的扩展性,也因此,构建扩展集群是它的天生强项之一.在实际线上应用中很多业务都运行在一个 ...

  6. IM服务器:我的千万级在线聊天服务器集群

    一.服务器特点 01.傻瓜式部署,一键式启动: 02.单机支持10万以上在线用户聊天(8G内存,如果内存足够大,并发量可超过10万): 03.支持服务器集群,集群间高内聚.低耦合,可动态横向扩展IM服 ...

  7. 在线安装TIDB集群

     在线安装TiDB集群 服务器准备 说明:TiDB8需要能够连接外网,以便下载各类安装包 TiDB4非必须,但最好是有一台,因为后续测试Mysql数据同步或者进行性能比较时,都要用到 TiKV最好是采 ...

  8. storm本地可以运行集群出错遇到的问题

    storm本地运行和集群运行是存在区别的: 本地可以读取本地文件系统及java项目中的文件,但是提交集群后就不能读取了,storm只是将topology提交到了集群,所以只能在main方法中将需要读取 ...

  9. Apache shiro集群实现 (六)分布式集群系统下的高可用session解决方案---Session共享

    Apache shiro集群实现 (一) shiro入门介绍 Apache shiro集群实现 (二) shiro 的INI配置 Apache shiro集群实现 (三)shiro身份认证(Shiro ...

随机推荐

  1. CSS3 background-size:cover/contain

    background-size的cover和contain指定背景图片的自适应方式,只能对整张图片进行缩放. cover是拉伸图片使之充满元素,元素肯定是被铺满的,但是图片有可能显示不全. conta ...

  2. 用SecureCRT连接虚拟机

    1.Root用户进入虚拟机系统 2.打开控制台 3.永久关闭防火墙,打开sshd,这样SecureCRT才能连接 chkconfig iptables off;service sshd start 4 ...

  3. #if和#ifdef区别

    #if  是要去判断, 跟值有关 #ifdef  只要定义了即可, 就会走下面的代码, 不管值是0还是1 所以一般都是用#ifdef DEBUG调试

  4. CodeForces 707D Persistent Bookcase

    $dfs$,优化. $return$操作说明该操作完成之后的状态和经过操作$k$之后的状态是一样的.因此我们可以建树,然后从根节点开始$dfs$一次(回溯的时候复原一下状态)就可以算出所有状态的答案. ...

  5. <hdu - 3999> The order of a Tree 水题 之 二叉搜索的数的先序输出

    这里是杭电hdu上的链接:http://acm.hdu.edu.cn/showproblem.php?pid=3999  Problem Description: As we know,the sha ...

  6. 《CSS网站布局实录》读书笔记

    从Web标准.HTML标记.CSS语法基础介绍到实用技巧,事无巨细.实体书已不印刷,只能下载电子版 书的背景: 国内第一本web标准的CSS布局书,2006年9月第一版,作者李超. 环境背景: 当时主 ...

  7. Topself 方便调试的Window服务框架

    Installing Topshelf nuget Install-Package Topshelf public class TownCrier { readonly Timer _timer; p ...

  8. bison实例

    逆波兰记号计算器[文件名rpcalc.y]%{ #define YYSTYPE double #include <stdio.h> #include <math.h> #inc ...

  9. Oracle 字符集小结(遇到一例子:查询结果列标题为汉字,但是显示为‘?')

    问题处理方式: 查询:select userenv('language') from dual; 对比电脑环境变量NLS_LANG的值与查询结果是否一致,如果不一致,修改电脑环境变量NLS_LANG ...

  10. Multidimensional Array And an Array of Arrays

    One is an array of arrays, and one is a 2d array. The former can be jagged, the latter is uniform. T ...