描述

最近一次在实现需求的时候发现 Storm 中的一个 Bolt 出现了 OOM 导致的长时间 GC 问题。最后虽然通过 review 新更新的代码找到了问题,但是深究其中还是有一些别的收获,所以在这里进行记录。

在 review 新更新的代码之后发现,我将 JedisPool 的实例化写到了 execute 中而不是 prepare 中,所以 Storm 每次执行 execute 的时候都会重新实例化 JedisPool 并且也没有显式的进行 close

虽然这个问题只是因为疏忽导致的,但是也让我对两个大问题进行了思考。一个是对于 Storm 中资源冲突的问题应该如何去发现、定位、处理,第二个是 Storm 中 Component 的生命周期。下面会讨论这两个问题。

Storm中的资源冲突

a4-1

要解决 Storm 中的资源冲突,那么需要先了解 Storm 中的资源分配。一个集群由一个 nimbus 节点和多个工作节点组成,每个工作节点由一个 Supervisor 管理着多个 Worker process,每个 Worker process 对应着一个 JVM,在其中有多个 Executor thread。每个 Executor thread 中可能存在多个 Task。而 Task 则是一个 bolt 或者 spout 的实例。

在此基础上,可以把资源竞争从所属结构从小到大划分为:

  • Worker process 中的内存冲突
  • Worker process 的冲突
  • 工作节点上的内存冲突
  • 工作节点上的 CPU 冲突
  • 工作节点上的网络 I/O 冲突
  • 工作节点上的磁盘 I/O 冲突

Worker process 中的内存冲突

首先这类冲突实际上是 JVM 中的内存占用过多,表现为 out-of-memory 或者进入长时间的垃圾回收,并且这类冲突会在 UI 上暴露出来。而解决办法无非是:

  • 减少一个 Worker process 中的 Executor thread 个数
    • 保证 Executor thread 数量不变的情况下加大 Worker process 数量
    • 保证 Worker process 数量不变的情况下减少 Executor thread 数量
  • 提高给 JVM 分配的内存:在 storm.yamlworker.childopts 属性是 JVM 相关的参数,可以通过设定 -Xms-Xmx 来进行修改。

而观察任务的 GC 日志是最直接也是最长用到来解决问题的途径,在 storm.yamlworker.childopts 中可以对 JVM 的 GC 日志进行配置。

1
2
3
4
5
6
7
8
9
worker.childopts: ""-XX +PrintGCTimeStamps  
-XX: +PrintGCDetails
-Xloggc: /opt/storm/worker-%ID%-jvm-gc.log
-XX: +UseGCLogFileRotation
-XX: NumberOfGCLogFiles=5
-XX: GCLogFileSize=1M
-XX: +PrintGCDateStamps
-XX: +PrintGCApplicationStoppedTime
-XX: +PrintGCApplicationConsurrentTime"
  • -XX +PrintGCTimeStamps: 打印垃圾回收的时间戳
  • -XX: +PrintGCDetails: 打印额外的 GC 细节
  • -Xloggc: /opt/storm/worker-%ID%-jvm-gc.log: 为每个工作进程分别创建日志文件
  • -XX: +UseGCLogFileRotation: 对 GC 日志文件使用日志转储
  • -XX: NumberOfGCLogFiles=5: 设置日志的分割个数
  • -XX: GCLogFileSize=1M: 设置日志的分割大小
  • -XX: +PrintGCDateStamps: 打印垃圾回收的日期和时间信息
  • -XX: +PrintGCApplicationStoppedTime: 打印应用程序停止时 GC 启动时间(时间在安全点内)
  • -XX: +PrintGCApplicationConsurrentTime: 打印 GC 执行期间程序启动的时间(时间不在安全点内)

Worker process 的冲突

这是由于需要的 Worker process 数量超过了集群中的数量,可以通过扩展集群,或者增加每个工作节点的 Worker process 数量来解决。也可以对减少集群里一些任务的 Worker process 占用来解决。

storm.yamlsupervisor.slots.ports 配置项可以配置一个工作节点的 Worker process 数量,每一个端口对应一个进程。添加添加、删除端口就能进行控制。

工作节点上的内存冲突

因为工作节点的内存需要支撑 Supervisor process, 操作系统,多个 Worker process 和其他的一些进程。如果工作节点在内存上发生了使用冲突,工作节点将开启进程间的内存调度(swapping),会造成有较高的延时发生。可以通过 sar 命令来进行监控:

1
2
3
4
5
6
7
8
9
10
$ sar -S 1 3
```

表示每隔1秒输出3条内存的活动信息,主要需要关注 `kbswpused` 和 `%swpused` 数据。`kbswpused` 是使用中的交换空间内存(KB) ,`%swpused` 是使用中的交换空间内存百分比。如果这两个值大于0则说明系统中存在内存交换。

### 工作节点上的 CPU 冲突
和上一个情况类似,也是因为对 CPU 的使用超过了节点所能提供的而造成的。也可以使用 `sar` 命令来进行监控:

```shell
$ sar -u 1 3

主要需要关注 %idle,即在系统没有任何磁盘 I/O 请求空闲CPU时间百分比,如果值偏低。再到 %user%nice%system 中去找事应用层面上的问题还是系统层面的问题。

工作节点上的 I/O 冲突

同样可以使用 sar 命令来进行监控:

1
$ sar -u 1 3

只是现在需要关注的值是 %iowait,CPU 时间空闲的百分比,在此期间系统将执行 I/O 请求。如果这个值约为 10.00,那么大概率出现因 I/O 冲突导致的性能问题,如果大于 25.00,一定面临比较严重的 I/O 冲突。

然后要做的就是定位问题是在网络 I/O 还是磁盘 I/O,可以先通过下面的方法检查网络 I/O。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取进程id
$ ps aux | grep MY_TOPOLOGY_NAME
stormuser 12345 .....

// 获取端口
$ netstat -antup | grep "12345/"
tcp6 0 0 xx.xx.xx.xx: 12345 xx.xx.xx.xx:42474 ESTABLISHED 4576/java

// 查看限制
$ cat /proc/12345/limits

// 查看该端口的稳定的连接tcp连接
$ netstat -na | grep "tcp6" | grep "4576" | grep "ESTABLISHED" | wc -l

检查 Max open files 一行, soft limit 和 hard limit 的值,如果已经到达设置的极限值,那么就会发生网络 I/O 冲突。然后可以使用 iotop 来观察是否发生了磁盘 I/O。

主要观察 USER 为 storm 的进程, DISK READDISK WRITEIO> 即每秒该进程读取的字节数,每秒该进程写入的字节数,每秒 I/O 调用百分比。如果其中一个值较高就说明相关的任务出现了磁盘 I/O 冲突。

该章节主要参考《Storm应用实践》

Component 生命周期

这里可以先参考一下 Nathan on 自己的回答: The lifecycle of a bolt or spout

可以看到 prepare 仅仅只会被 worker 在开始的时候执行一次,但是 execute 会在每次有 tuple 进入的时候都被调用。也就是说如果我有一个 Bolt 定义了3个 worker,每个 worker 都有3个 executer。那么总共是有9个 task,也就是说 prepare 会被调用9次,而 execute 就会被无限调用。

所以我们一些连接和静态变量的初始化工作放到 prepare 中去完成会更加的实惠。但是由于每个 worker 对应一个单独的 JVM 进程,一个 JVM 进程中会有多个 executor 线程,所以就会有多个 task 同时执行 prepare 的操作。
那么一些连接的创建是需要线程安全的环境,线程安全的单例写法这里不赘述。

Comments

⬆︎TOP