2019年7月

我这个 Java 应用的堆内存过几天会爆掉吗?

今天遇到一个有意思案例和一个有趣的问题.

某应用发布了新版本到集成测试环境(Staging), 他们发现有监控提示他们可能有内存泄漏, 于是这些开发人员做了很多 heap dump, 自己查了好几天, 都没查出所以然. 最后必须上线的期限快到了, 还没查出来, 索性先在生产上线一台去跑跑看, 监控了几个小时, 啥问题都没发生. 不过最后他们还是不放心, 转而寻求 SRE 帮助他们去查查.

拿到这个案例之后, SRE 线上看了生产上这台机器, 发现已经正常运行了8个小时, 内存, CPU, 线程等一切正常, 查看 GC 日志, 也没发现任何毛病. 于是问开发人员, 你们在集成测试环境到底看到了啥, 为啥怀疑内存泄漏. 于是他们果然给了一个显示 "可能有内存泄漏" 字样的截图. 于是 SRE 有去看了下这台 QA 环境的机器, 终于发现了问题.

原来他们对于不同的环境设置的内存 heap 大小是不一样的. 对于 production 环境, 他们设置的是4.8G 总 heap 大小, 对于其它环境, 一概设置为2.2G 的 heap 大小. 查看 production 环境的 heap 使用情况, 可以明显看到老年代 heap 大小在每次 CMS 并发 GC 之后, 大概占用1.4G, 还有很多空闲可用. 而对于总 heap 大小只有2.2G 的 QA 机器而言, 它的老年大总大小只给了1.4G, 所以等应用起来之后, 和生产环境一样, 一旦有业务请求进来, 最基本都要1.4G heap 被占用. 所以它就不停的做老年代 GC, 大多数情况下做 CMS GC 的速度跟不上新申请内存的速度, 一旦有个请求进来, 要使用的内存稍多一点, 年轻代立马被占满, 开始向老年代转移对象, 老年代就开始触发 Full GC. 于是监控就推测: 这个应用有内存泄漏.

所以这个应用根本么有内存泄漏, 只是因为他稳定运行(stable)之后, 就需要这么多堆内存. 只是因为在 QA 环境设置的太小, 导致监控系统误认为它有内存泄漏. 所以增大 QA 环境的堆内存参数, 是一个正解.

当 SRE 把这个解释告诉开发人员之后, 他们抛出了一个让我印象深刻的问题: "虽然现在生产上没有问题, 是不是因为它设置的堆内存比较大, 如果这样, 是不是运行3天之后, 就会导致像 QA 一样的堆内存不够的问题?".

如果真的 N 天之后, 堆内存被用光, 那确实是代码有问题, 如果代码没问题, 即便是 N 年之后, 堆内存也还是够的. 那么如何验证这个问题呢? 我们从堆内存的的 GC log view来看, 基本能判断这个问题. 下面是一个 Java 应用运行几天的 GC log view 图, 从图里可见, 每次老年代 CMS GC 之后, 老年代都回到同一个水平, 这就是一个没有内存泄漏的形状, 也是一个非常正常的锯齿形状. 如果有内存泄漏, 每次老年代 GC 之后, 它的使用大小都会增加. 久而久之, 老年代即使 GC 之后, 也没有了可用空间. 当然, 这里只是讨论 heap 的内存泄漏, 不考虑永久代, 原生内存等泄漏.
gcDayByDay.png

这里就涉及到一个 stablized heap size 问题. 所谓 stablized heap, 是指程序稳定运行一段时间,已经达到一个平稳的状态, 所有会被遇到的请求, 基本都执行过一定数量, 这时候如果对他做 Full GC, GC 之后留下的堆空间, 我们称之为 stablized heap size. 这个大小在很多时候, 都是设置堆大小的依据. 当一个请求进来之后, 它中间产生的对象实例, 不管是否移动到老年代, 在一定时间之后, 都会被回收. 这些不被回收, 一直在 heap 里面的实例对象, 是为了稳定运行这个程序所必须的对象.

所以, 要推测一个应用是不是过几天堆内存就会被爆掉, 只要查看一段时间的这个 heap GC log 基本就能判断.

某次诊断网络问题的过程及思考

今天花了三个多小时去诊断一个线上的问题, 最终的解决方法只需要1分钟. 回头整理一下整个过程, 发现其中还是有很多机会.

其实这个问题昨天就被观察到了, 当时发现有些服务自动熔断了(Service client auto markdown), 一路追查到底, 发现最下层一个访问 Couchbase 的应用在访问 Couchbase 的时候 timeout 了. 大概几百次访问里, 会有那么1, 2次可能会 timeout. 那么这样问题已经确诊到最下层应用到 Couchbase 这些组件了.

从最下层应用到 Couchbase, 只涉及三段: 1) 最下层应用本身; 2) 网络; 3) Couchbase. 首先, 这个最下层应用本身除了 error 增加之外, 并没有发现可疑的地方, 但是也不能完全排除; 其次, 应用和 Couchbase 之间大概有8跳 (hop), 网络 team 查看他们的监控数据, 中间这些网络设备并无异常. 最后, DBA 从 Couchbase 的监控指标数据去看, 也没有发现异常. 尽管除了应用的 error 数量有异常, 查看了其它所有指标, 都看上去还是很平静.

进一步分析这个错误的数据, 可能会指明一些方向. 首先, 这个应用在三个数据中心分别有30多台机器, 只有一个数据中心的这个 timeout error 非常多, 其它数据中心也有, 但是相比之下, 几乎可以忽略. 那么可以断定, 这个问题只是局限于这个数据中心. 这个数据中心的应用代码和其它数据中心的是一样的, 所以不大可能是代码的问题. 这个数据中心的机器最近也没做过什么改动, 那么不大可能是这些应用有问题. 退一步讲, 如果这些机器有问题, 那么很有可能这个数据中心的其它应用机器也应该有问题, 或者说这个应用访问其它外部服务, 也很有可能有 timeout 的问题, 可是这个情况不存在. 对于网络问题, 可以做同样的假设, 如果是网络问题, 这个网络上走的其它服务, 也一样会遇到网络问题暴露出来, 可是并没有发生一样的网络问题. 那么排除下来, 最有可能的是 Couchbase 数据库那边有问题, 可是 Couchbase 的监控数据却显示, Couchbase 操作的延时( latency) 并没有任何波动.

既然是 timeout 问题, 就可以用 tcpdump 去确认到底是不是网络问题. 网络抓包最完全的是关键的每跳都抓, 今天在有限的权限的情况下, 只能在应用这边抓一下. 从应用这边看, 它连着同数据中心的 Couchbase 集群里的8台机器, 端口都是11211, 于是对涉及这端口的都抓一下. 从抓取的 tcp 包来看, 发现有些 tcp 包有乱序现象, 尽管这些乱序的 tcp 包在非常短的时间内都收到了, 可是 Couchbase 客户端却没有对这些乱序的包进行正确的组装, 生成 response 数据, 所以尽管 tcp 包都收到了, 因为没组装成功, 并返回给调用者, 导致调用者认为最终没有收到 response, 而 timeout. 进一步查看这种乱序的 tcp 包, 发现只局限于 Couchbase 那边的一台机器. 于是用 mtr -s 1040 去测试一把, 发现大概每80次, 就会有一次突然超过1s的延时, 正常情况下因为在一个数据中心延时都小于10ms. 另外一个值得注意的地方是, 从应用到 Couchbase 的 mtr 数据结果里, 最后这1跳可能发生大于1s 的延时, 前面所有跳都么有. 从 Couchbase 到应用这边, 如果哪次延迟高, 所有的跳都显示很高. 从这些数据可以看出, 基本是 Couchbase 这边的机器有问题.

使用 mtr 又继续验证了到其它 Couchbase 数据库的路径, 没有发现有这么长的延时问题. 所以最终怀疑是这个数据库所在机器的操作系统层面出了问题, 又或者因为这个机器是虚拟机, 这个虚拟机所在的宿主机出了问题. DBA 先把这台数据库移除集群, 然后 reboot 这个机器, 起来之后从新加入集群, 问题消失了. 尽管没有深入挖掘出为什么这个机器除了问题, 也因为它已经被重启, 没有了犯罪现场, 很难查清. 只能期望如果下次还有类似问题, 可以先保留现场, 查清一直在修复.

问题虽然通过重启解决, 仔细思考一下整个流程, 还是有些可以改进的地方:

  1. 从最上层应用一路追查到最底层的应用, 花了一些时间, 现阶段没有全量的 tracing 数据. 如果有全量 tracing 数据, 只需要找出一个最上层的 transaction, 那么就直接追查到导致延迟问题的最下层应用. 当然如果能追查到数据库层就更好了.
  2. 最下层的应用使用 RxJava, 里面的观察者等待1秒钟, 然后 timeout. 当 timeout 的时候, 它 new 了一个 TimeoutException(), 参数没有传入任何有意义的字符. 导致在 log 里面看到的是一个 RxJava 的出错栈, 最后给的 error message 是一个 null, 尽管能看到是 TimeoutException, 可是没有任何有意义的提示消息. 一个有意义的消息至少告诉别人是连接哪个 ip, 连接哪个端口, 花了多久 timeout 的.
  3. 如果有批量自动化的工具去做大量机器的 mtr 命令, 并合并在一起分析, 将加速诊断的过程;
  4. 仔细对问题进行分类, 对细节进行逻辑推理, 合理的猜测加论证, 能加速诊断的过程.
  5. 需要更完备的监控. 对于 service client, DB client, NoSQL client 都要搜集 metric 进行监控.

java.util.concurrent.ReentrantLock 与 synchronized 的对比

两者都可以实现同步, 在有些方面二者还是有很多区别. synchronized 是 java 的一个语法级别的特性, ReentrantLock 是 util 里面的一个辅助类.

相同点:

  1. 二者都可以实现锁;
  2. 二者都可以实现 生产者/消费者模式 wait/notify/notifyAll, await/signal/signalAll

不同点:

  1. synchronized 必须是一对 {} 中间的部分, 2 个 synchronized 的代码块无法交叉, ReentrantLock 的写法更灵活, 可以交叉;
  2. Lock 可以设置是否回应 interrupt;
  3. Lock 可以使用 tryLock() 如果不能获得锁, 立马返回;
  4. Lock 可以使用 tryLock(timeout) 可以设置尝试获得锁的最长等待时间;
  5. 一个 Lock 可以通过 newCondition 设置多个条件队列, 让他们等待不同的事件;
  6. ReentrantLock 可以设置是否是公平锁;
  7. synchronized 在 JDK 6 优化之后, 可以实现锁粗化, 偏向锁, 根据逃逸分析锁消除;

如何生成 java heap dump

Java heap 是某个时间点上 JVM 内存的一个瞬时镜像(snapshot), 通过工具查看内存里面的各种对象以及他们之间的关系, 对于分析内存问题非常有帮助. 常见的 heap dump 都是二进制的 hprof 格式, 获得之后一般通过 jhat, JVisualVM 或者 MAT 分析. 那么第一步, 如何获得 heap dump 呢? 本文将介绍常见的获得 heap dump 的一些方法.

  1. jcmd
    jcmd 是 jdk 自带的一个小工具, 推荐使用. 要通过 jcmd 获得 heap dump, 首先要得到该进程的 ID. 获得进程 ID 之后, 可以通过 jcmd 命令获得 heap dump.

     LM-SHC-16501315:Downloads xiatian$ jcmd
     42596 
     98797 sun.tools.jcmd.JCmd
     LM-SHC-16501315:Downloads xiatian$ jcmd 42596 GC.heap_dump /tmp/heap.hprof
     42596:
     Heap dump file created
  2. jmap
    jmap 是 JDK 从早期开始一直附带的一个小工具, 使用下面的命令来获取 heap dump:
    jmap -dump:[live],format=b,file=
    其中的 live 是可选, 代表是否包含即将被 GC 的对象, 若包含 live, 则不包含即将被 GC 的对象.
    在我的使用经验中, 有时候 jmap 会出现不能 attach 到目标进程的问题.
    例子:

    LM-SHC-16501315:Downloads xiatian$ jmap -dump:live,format=b,file=/tmp/heapdump.hprof 42596
    Dumping heap to /private/tmp/heapdump.hprof ...
    Heap dump file created
  3. JVisualVM
    若在桌面环境, 并且已经安装 JDK, 可以使用 JDK 自带 GUI 工具 JVisualVM.
    heap.png
  4. JConsole
    JConsole 也是 JDK 自带 GUI 工具
    jconsole.png
  5. 可以在 JVM 启动时添加如下参数, 当发生 OOM 时候, 自动产生 heap dump:

    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<file-or-dir-path>
  6. 编程的方式: 可以通过使用 MBean 去操作, 产生 heap dump.

    public static void dumpHeap(String filePath, boolean live) throws IOException {
     MBeanServer server = ManagementFactory.getPlatformMBeanServer();
     HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(
       server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
     mxBean.dumpHeap(filePath, live);
    }

有时候, 你会看到 core dump 和 heap dump, 2个不一样的dump, 那么2个区别是什么呢?
Java 常见的三种 dump 文件: Core Dump, heap dump, thread dump