V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
shipinyun2016
V2EX  ›  推广

网易视频云: HBase GC 的前生今世 - 身世篇

  •  
  •   shipinyun2016 · 2016-07-13 17:25:46 +08:00 · 1609 次点击
    这是一个创建于 3089 天前的主题,其中的信息可能已经有所发展或是发生改变。

    网易视频云是网易倾力打造的一款基于云计算的分布式多媒体处理集群和专业音视频技术,提供稳定流畅、低时延、高并发的视频直播、录制、存储、转码及点播等音视频的 PaaS 服务,在线教育、远程医疗、娱乐秀场、在线金融等各行业及企业用户只需经过简单的开发即可打造在线音视频平台。现在,网易视频云的技术专家给大家分享一则技术文: HBase GC 的前生今世 - 身世篇。

    使用 LRUBlockCache 缓存机制会因为 CMS GC 策略导致内存碎片过多,从而可能引发臭名昭著的 Full GC ,触发可怕的’ stop-the-world ’暂停,严重影响上层业务;而 Bucket Cache 缓存机制因为在初始化的时候就申请了一片固定大小的内存作为缓存,缓存淘汰不再由 JVM 管理,数据 Block 的缓存操作只是对这篇空间的访问和覆盖,因而大大减少了内存碎片的出现,降低了 Full GC 发生的频率。那 CMS GC 策略如何导致内存碎片过多?内存碎片过多如何触发 Full GC ? HBase 在演进的道路上又如何不断优化 CMS GC ?接下来这个系列《 HBase GC 的前生今生》将会为你一一揭开谜底,这个系列一共两篇文章,本篇文章-’身世篇’将会带你全面了解 HBase 的 GC 机制,后面一篇-’演进篇’将会给你道出 HBase 在发展的道路上如何不断对 Full GC 进行优化。

    Java GC 概述

    整个 HBase 是构建在 JVM 虚拟机上的,因此了解 HBase 的内存管理机制以及不同缓存机制对 GC 的影响,就必须对 Java GC 有一个全面的了解。至于深入地理解 Java GC 的工作原理,不在本文的讨论范围之内;当然,如果已经对 Java GC 比较熟悉,也可以跳过此节。

    Java GC 建立在这样一个假设基础上的:大多数内存对象要么生存周期比较短,很快就会没人引用,比如处理 RPC 请求的 buffer 可能只会生存几微秒;要么生存周期比较长,比如 Block Cache 中的热点 Block ,可能就会生存几分钟,甚至更长时间。基于这样的事实, JVM 将整个堆内存分为两个部分:新生代( young generation )和老生代( tenured generation ),除此之外, JVM 还有一个非堆内存区- Perm 区,主要存放 class 信息以及其他 meta 元信息,内存结构如下图所示:

    1

    其中 Young 区又分为 Eden 区和两个 Survivor 区: S0 和 S1 。一个内存对象在创建之后,首先会为其在新生代申请一块内存空间,如果这个对象在新生代存活了很长时间,会将其迁移到老生代。 在大多数对延迟敏感的业务场景下(比如 HBase ),建议使用如下 JVM 参数,-XX:+UseParNewGC 和 XX:+UseConcMarkSweepGC ,其中前者表示对新生代执行并行的垃圾回收机制,而后者表示对老生代执行并行标记-清除垃圾回收机制。可见, JVM 允许针对不同内存区执行不同的 GC 策略。

    新生代 GC 策略 – Parallel New Collector

    根据上文所述,对象初始化之后会被放入 Young 区,更具体的话应该是 Eden 区,当 Eden 区满了之后,会进行一次 GC 。 GC 算法会检查所有对象的引用情况,如果某个对象还有被引用,表示该对象存活。检查完成之后,会将这些存活的对象移到 S0 区,并且回收整个 Eden 区空间,称为一次 Minor GC ;接着新对象进来,又会放入 Eden 区,满了之后会检查 S0 和 Eden 区存活的对象,将所有存活的对象移到 S1 区,再回收整个 S0 和 Eden 区空间;很容易理解, S0 和 S1 两个区总会有一个区是预留给下次存放存活对象用的。

    整个过程可以使用如下图示:

    2

    这种算法称为复制算法,对于这种算法,有两点需要关注:

    1. 算法会执行’ stop-the-world ’暂停,但时间非常短。因为 Young 区通常会设置的比较小(一般不建议不超过 512M ),而且 JVM 会启动大量线程并发执行,一次 Minor GC 一般都会在几毫秒内完成

    2. 不会产生碎片,每次 GC 之后都会将存活的对象放入连续的空间( S0 或 S1 )

    内存中所有对象都会维护一个计数器,每次 Minor GC 移动一个对象之后,都会为这个对象的计数器加一。当计数器增加到一定阈值之后,算法就会认为该对象生命周期很长,会将其移入老生代。该阈值可以通过 JVM 参数 XX:MaxTenuringThreshold 指定。

    老生代 GC 策略 – Concurrent Mark-Sweep

    每次执行 Minor GC 之后,都会有部分生命周期较长的对象被移入老生代,一段时间之后,老生代空间也会被占满。此时就需要针对老生代空间执行 GC 操作,此处我们介绍 Concurrent Mark-Sweep ( CMS )算法。 CMS 算法整个流程分为 6 个阶段,其中部分阶段会执行 ‘ stop-the-world ’ 暂停,部分阶段会和应用线程一起并发执行:

    1. initial-mark :这个阶段虚拟机会暂停所有正在执行的任务。这一过程虚拟机会标记所有 ‘根对象’,所谓‘根对象’,一般是指一个运行线程直接引用到的对象。虽然会暂停整个 JVM ,但因为’根对象’相对较少,这个过程通常很快。

    2. concurrent mark :垃圾回收器会从‘根节点’开始,将所有引用到的对象都打上标记。这个阶段应用程序的线程和标记线程并发执行,因此用户并不会感到停顿。

    3. concurrent precleaning :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行 mark 阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。

    4. remark :在阶段 3 的基础上对查找到的对象进行重新标记,这一阶段会暂停整个 JVM ,但是因为阶段 3 已经欲检查出了所有新进入的对象,因此这个过程也会很快。

    5. concurrent sweep :上述 3 阶段完成了引用对象的标记,此阶段会将所有没有标记的对象作为垃圾回收掉。这个阶段应用程序的线程和标记线程并发执行。

    6. concurrent reset :重置 CMS 收集器的数据结构,等待下一次垃圾回收。

    相应的,对于 CMS 算法,也需要关注两点:

    1. ‘ stop - the - world ’暂停时间也很短暂,耗时较长的标记和清理都是并发执行的。

    2. CMS 算法在标记清理之后并没有重新压缩分配存活对象,因此整个老生代会产生很多的内存碎片。

    CMS Failure Mode

    上文提到在正常的情况下 CMS 整个流程的暂停时间都是很短的,一般也就在 10ms ~ 100ms 左右。然而这与线上的情况并不相符,线上集群在读写压力很大的情况下,经常会出现长时间的卡顿,有些卡顿甚至长达几分钟,导致很严重的读写阻塞,甚至会造成 Region Server 和 Zookeeper 之间 Session 超时,使得 Region Server 异常离线。实际上, CMS 并不是很完美,它会在两种场景下产生严重的 Full GC ,接下来分别进行介绍。

    Concurrent Failure

    这种场景其实比较简单,假如现在系统正在执行 CMS 回收老生代空间,在回收的过程中新生代来了一批对象进来,不巧的是,老生代已经没有空间再容纳这些对象了。这种场景下, CMS 回收器会停止继续工作,系统进入 ’ stop-the-world ’ 模式,并且回收算法会退化为单线程复制算法,重新分配整个堆内存的存活对象到 S0 中,释放所有其他空间。很显然,整个过程会非常’漫长’。但是这种问题也很容易解决,只需要让 CMS 回收器更早一点回收就可以避免。 JVM 提供了参数-XX:CMSInitiatingOccupancyFraction=N 来设置 CMS 回收的时机,其中 N 表示当前老生代已使用内存占新生代总内存的比例,该值默认为 68 ,可以将该值修改的更小使得回收更早进行。

    Promotion Failure

    假设此时设置 XX:CMSInitiatingOccupancyFraction = 60 ,但是在已使用内存还没有达到总内存 60%的时候,已经没有空间容纳从新生代迁移的对象了。 oh , my god !怎么会这样?罪魁祸首就是内存碎片,上文中提到 CMS 算法会产生大量碎片,当碎片容量积累到一定大小之后就会造成上面的场景。这种场景下, CMS 回收器一样会停止工作,进入漫长的 ’ stop-the-world ’ 模式。 JVM 也提供了参数 -XX: UseCMSCompactAtFullCollection 来减少碎片的产生,这个参数表示会在每次 CMS 回收垃圾之后执行一次碎片整理,很显然,这个参数会对性能有比较大的影响,对 HBase 这种对延迟敏感的业务来说并不是一个完美解决方案。

    HBase 内存碎片统计实验

    在实际线上环境中,很少出现 Concurrent Failure 模式的 Full GC ,大多数 Full GC 场景都是 Promotion Failure 。我们线上集群也会每隔半个月左右就会因为 Promotion Failure 触发一次 Full GC 。为了更好地理解 CMS 策略下内存碎片是如何触发 Promotion Failure ,接下来我们做一个简单的实验: JVM 提供了参数 -XX:PrintFLSStatistics=1 来打印每次 GC 前后内存碎片的统计信息,统计信息主要包括 3 个维度: Free Space 、 Max Chunk Size 和 Num Chunks ,其中 Free Space 表示老生代当前空闲的总内存容量, Max Chunk Size 表示老生代中最大的内存碎片所占的内存容量大小, Num Chunks 表示老生代中总的内存碎片数。我们在测试环境集群(共 4 台 Region Server )将这个参数设置为 1 ,然后使用一个客户端 YCSB 执行 Read-And-Write 操作,分别统计日志中 Free Space 和 Max Chunk Size 两个指标随时间的变化情况。

    测试结果如下图所示,其中第一张图表示 Total Free Space 随时间的变化曲线图,第二张图表示 Max Chunk Size 随时间变化曲线图。其中横坐标表示时间,纵坐标表示相应内存大小。

    3

    4

    根据第一张曲线图可知,老生代总的空闲内存容量维持在 300M~400M 之间,当内存容量到达 300M 左右时就会进行一次 GC , GC 后内存容量就会又回到 400M 左右。而第二张曲线图会更加形象地说明内存碎片导致的 Promotion Failure ,刚开始随着数据不断写入, Max Chunk Size 会不断变小,之后很长一段时间基本维持在 30M 左右。在横坐标为 1093 那点,人为地将写入的单条数据大小由 500Byte 变为 5M 大小,此后 Max Chunk Size 会再次减小,当减小到一定程度之后曲线会忽然升高到 350M 左右,经过日志确认,此时 JVM 发生了 Promotion Failure 模式的 Full GC ,持续时间约 4.91s 。此后一段时间 Full GC 还在持续发生。

    经过上述分析,可以知道: CMS GC 会不断产生内存碎片,当碎片小到一定程度之后就会基本维持不变,如果此时业务写入一些单条数据量很大的 KeyValue ,就有可能触发 Promotion Failure 模式 Full GC 。

    总结

    本文首先介绍了两种常见的 Java GC 策略,再接着介绍了 CMS 策略可能引起两种模式的 Full GC ,最后通过一个小实验说明了 CMS GC 确实产生了内存碎片,而且会导致长时间的 Full GC 发生。

    更多技术分享,请关注网易视频云官方网站( http://vcloud.163.com/)

    或者网易视频云官方微信( vcloud163 )进行交流与咨询

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3367 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 10:41 · PVG 18:41 · LAX 02:41 · JFK 05:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.