提升内存管理效率,携程酒店查询服务轻量化探索和实践

2024-02-09
32 阅读

作者简介

NekoMatryoshka,携程酒店资深后端开发工程师,主要工作是缓存类组件的开发维护,并对业务应用的排障和优化有所关注。

一、背景和目标

在容器化部署成为主流的现在,降低集群中单个容器的资源需求的意义已经不只限于更少的硬件成本,同时也意味着整个集群更加轻量化,这通常会带来一系列其他优势:例如更短的恢复时间,更精确的资源控制和调度,和更快速的伸缩和部署等。但在另一方面,一味的追求压缩容器配置必然会严重影响应用在稳定性、响应耗时和吞吐量等方面的表现,所以轻量化的措施需要在多个性能维度上进行仔细的权衡取舍,以达到一个总体更优的结果。

作为携程计算量最大的接口之一,酒店查询服务一直承担着沉重的硬件成本压力,仅仅详情页集群就包含了千余台服务器实例和数十TB的Redis资源,因此对应用进行全面的轻量化有着很高的必要性和预期收益。在内存方向上,我们的主要目标是将单个容器的内存从32GB压缩到16GB,并在以下两个基本方向上进行了探索:

减少内存增长速度:压缩本地缓存,减少浮动内存的产生,并对线程,类库,参数和代码逻辑进行针对性的优化和调整。提升内存管理效率:加强JVM等服务依赖的基础组件其本身的性能。

由于第一个方向需要根据应用的具体代码实现来分析和排查,普适性相对较差,所以本文将主要分享查询服务在轻量化中对于内存管理方向上的探索过程和实践经验。

二、堆内内存管理

我们的应用原本运行在JDK8的CMS收集器之上,但是在JDK11以后,CMS已经被完全淘汰。于是,要提高堆内的内存管理效率,我们首先尝试的便是对GC进行升级和调优。因此我们对G1、ZGC和ShenandoahGC等更现代的收集器进行了性能上的测试和对比,来尝试找出最合适的技术选型。

2.1 垃圾收集器的选型

首先,在JDK17上,ZGC第一次以生产环境可用的状态登陆了LTS版本,所以我们这次选型起初的目标也是尝试将应用迁移到ZGC之上。相对于大家熟悉的G1,ZGC最主要的优势在于其通过着色指针和读屏障两个特性,使得用户线程几乎可以全程与标记-复制算法并行,基本解决了YGC的STW问题。简单来说,ZGC在标记过程中会向64位指针的高位4bit中记录三色标记、重分配标记和可达性标记;当应用线程访问对象时,读屏障机制会依据指针状态和复制表信息去更新对象的地址和状态。

这样,即使GC线程正在后台转移、复制或清理对象,也可以保证前台线程能始终访问到正确的地址,这使得ZGC几乎可以做到无停顿回收。除此之外,ZGC还向用户承诺了可扩展性:由于ZGC的停顿时间基本只和初始扫描中GC Roots的数量相关,堆的大小和活跃对象的数量并不会导致停顿时间的增长。

其次,ShenandoahGC与ZGC同为新一代的零停顿收集器,总体来看,其内存布局非常类似于G1,而并发设计则与ZGC如出一辙,所以我们也将其作为一个可能的备选方案。

ShenandoahGC与ZGC的主要区别在于其使用的是Brook指针而非染色指针:即在对象头中额

外记录一个指向复制后正确地址的指针。但是由于额外信息记录在对象头中,Brook指针的读屏障无法在第一次访问后直接更新正确地址来自我恢复。另一方面,ShenandoahGC的区块布局和回收阶段则与G1非常相似,甚至部分代码都是直接复用的。其不同主要在于ShenandoahGC利用了一个被称为连接矩阵的二维数组来取代G1中开销巨大的记忆集,来解决跨区引用问题:例如区块N引用了区块M,则在数组的`[N][M]`坐标打上标记。

最后,作为现在最主流的收集器,同时也是CMS的取代者,G1理所应当的也被我们作为最成熟和稳妥的一个选择。G1本身的内存布局使得其对可控的停顿耗时和吞吐量的平衡上有较好的兼顾,在理论上使它更适合查询接口这种会短时间内突然生成大量临时对象的计算密集型应用。

综上所述,我们以原本在轻量化前的生产配置(16C32G+JDK1.8+CMS)作为基准,选取了以下几个组合作为测试方案:

其他各相关参数都为默认配置。

2.2 G1调优实践

在横向比较不同收集器的性能之前,我们首先需要按应用的需求对每个收集器做一些简单的适配和调整,以发挥这些收集器的全部性能。由于G1是为开箱即用而准备的默认收集器,使用起来相对简单,基本上只需要简单设置下堆大小和线程数等参数即可。然而在实际使用中,我们仍然遇到了一些小问题,需要对关键某些参数进行控制。

采用了低地址优先的分配策略,进一步降低了内存碎片率。(使用红黑树记录了地址排序,总是从低地址开始分配,使高地址的内存更整块)

(2)锁的颗粒度:jemalloc在大部分场景下几乎是无锁的。

每个线程都拥有动态伸缩的缓存tcache,在小内存操作时是无锁的。

大部分的线程都会被绑定到专属的arena上,使其操作无锁化(类似于JVM的偏向锁)。即使多个 线程共享一个arena,也会在arena内部细化为局部锁,而不是直接使用全局锁。

(3)内存回收:除了类似于ptmalloc的回收机制外,jemalloc还有两种机制。

当发现某个chunk全部都是脏页后,会直接释放整个chunk。

当脏页数量超过某个阈值的时候,进行主动的purge操作。

(4)额外开销:仅仅占用约2%的额外内存,用于存储一些meta信息。

(5)工具链:jemalloc有完善的内存分析工具,可以更好的定位溢出和泄露问题。

3.5 迁移和收益

对于简单的性能测试,手动安装jemalloc非常容易,甚至不需要重新编译代码,直接在一台正常运行的机器上安装好jemalloc后,修改tomcat的sh文件中将LD_PRELOAD变量指定为对应的so文件覆盖glibc动态库并重启tomcat即可。而后续容器部署也只需要在dockerfile中自定义数行代码模拟上述操作,然后构建并上传自定义镜像便能完成。

目前查询服务已经在jemalloc上生产运行了数个月,至今还没有观察到再次出现堆外溢出的问题;同时RSS的波动非常稳定,即使遇到流量高峰也不会出现内存尖刺,可以保持良好的响应时间和稳定。

从实际的情况来看,jemalloc与ptmalloc相比主要有以下收益:

从运维方面来看,集群为了方便调度,一般会限制几个预设的容器配置以供选择。在资源相对紧张的情况下,jemalloc可以使得应用整体的部署更加灵活,而使用默认的ptmalloc则会被迫将容器配置向上升级,否则就需要额外对特殊配置进行审批和调度,这样不但会造成不必要的资源浪费,同时在流量尖峰时也难以对集群进行调度和扩容。在成本方面,从测试结果出发,仅仅使用jemalloc本身就能比ptmalloc在每台机器上节省1-1.5G的堆外内存,虽然在单机上可能不够显著,但是推广到整个云的范围时收益应该是非常可观的。性能上,jemalloc的内存回收和多线程机制更加高效和智能化,对低配置机器更加友好,能大大加强内存资源紧张的机器上服务的鲁班性,同时对IO、GC、类加载等多线程native操作有较大的优化。从迁移角度看,迁移到jemalloc几乎是无成本的操作,仅仅需要简单的镜像自定义和一定的灰度测试,就可以完成优化。

故综合来看,jemalloc的收益相比于成本大得多的,有一定的分享和推广的意义。

四、结语

本文相对完整的记述了酒店查询服务在轻量化中的一次优化过程,希望其中的经验和过程能对读者有所帮助。然而,对于应用的优化过程是一个从猜想到验证的循环。在有了可能的猜测和方向之后,比起反复的调研,更重要的则是不断向着落地验证去推进。虽然这些经验有一些普适性,但是由于应用之间各有不同,仍然需要读者根据实际情况亲手试验后,才能最终确定是否有借鉴意义。

作者:NekoMatryoshka

来源:微信公众号:携程技术

出处:

分享至:
小草

小草

专注人工智能、前沿科技领域报道,致力于为读者带来最新、最深度的科技资讯。

评论 (0)

当前用户头像