查看原文
其他

分享 | 低延迟高吞吐的CAL Ingress

eBay技术荟 2022-03-15


供稿 | eBay Unified Monitoring Platform 

翻译&编辑 | 顾欣怡

本文2891字,预计阅读时间9分钟

更多干货请关注“eBay技术荟”公众号


导读

CAL Ingress作为eBay的集中式日志系统,每天都要处理大量数据,面临低延迟、高吞吐的需求和挑战。本文将分享我们如何通过架构和性能调优,支持当前生产环境的高数据流量,解决上述问题,旨在为同行带来思考和借鉴。


一. CAL Ingress



CAL(Centralized Application Logging)是eBay的集中式应用日志框架,其主要功能是集中搜集应用服务器的本地日志,并据此生成报告。这些报告洞察eBay系统的重要领域,对eBay业务的决策是无价的。

CAL的主要模块CAL Ingress用于搜集、处理和分发搜集来的数据。CAL Ingress搜集成千上万的应用服务器上的日志,并且处理后分发到不同的目的地。

CAL Ingress搜集和处理以下三种数据:

  1. 原始日志(Raw logs)

  2. VID 指标(metrics)

  3. Heart Beat 指标(metrics)

如图1数据流所示,CAL Ingress通过TCP连接搜集了数据,再分发数据到不同的目的地。原始日志直接发到Filer storage;从日志里提取的VID指标会经由KAFKA发到eBay应用程序状态实时分析系统OLAP2.0;Heart Beat 指标会发送到指标存储TSDB(Time Series Database, 时序列数据库)。


图 1 数据流


二. 需求和挑战



作为一个集中式日志系统,CAL Ingress每天将从eBay的各种应用中搜集处理大约2.5PB的数据,数据流量巨大,面临着巨大的需求和挑战。而这需求与挑战,具体来说,就是低延迟和高吞吐。


1.低延迟

CAL客户通过NIO(NO-blocking I/O)持久的TCP连接发送数据到CAL,并且要求很高的服务质量(QoS)。如果客户不能一次发送所有数据,客户和CAL之间的链连接会被认为异常,连接会被断开,客户和CAL缓存的数据就会丢失。此为“TCP 断连”。一旦发生“TCP 断连”,就会有数据丢失。为了减缓“TCP 断连”和“数据丢失”,CAL Ingress必须极快地收取并确认数据,因此要求低延迟。

目前,CAL客户机器上配置的网络传输缓存为128k, 每个连接的平均流量是200k/秒,因此延迟必须低于0.64秒。对于那些流量大的客户,延迟要更低。

CAL同时搜集来自一百万个连接的数据,延迟的时间可以通过下面的公式进行计算:


2. 高吞吐

CAL Ingress平均需要搜集和处理105TB/小时的日志平均流量是29.87GB/秒,最高可达58GB/秒可见吞吐量极高。


三. 架构和性能调优



面对这样低延迟、高吞吐的挑战,我们致力于调整优化架构和性能。CAL Ingress采用Netty 服务器,通过持久的TCP连接(long-lived TCP connection)接受客户数据。遵循低延迟和高吞吐的需求,我们把数据读取和数据处理分开,置于不同的EventLoopGroup处理。图2展示了CAL Ingress的架构。

图 2 CAL Ingress 架构

CAL Ingress同时服务成百万个连接(eBay的生产环境里大约1百万),并依次从连接中读取数据。延迟可通过下面的公式计算:

Tperiod-读取间隔。  Nconnection– 连接数, Nthread- 线程数。 对于一个连接来说,读取的时间间隔为Tgc- gc 停顿时间(pause time)。)

要降低延迟,可以降低读取间隔和GC停顿时间。为此,我们采用了“不间断数据读取”和“GC 优化”策略。


1. 不间断数据读取

首先,设置“TCP_NODELAY”和“TCP_ QUICKACK”,保证数据读取不会在网络层停止。

接着,打断正常的数据流程:读取-》处理-》读取,分开数据读取和数据处理。

最后,我们发现数据读取从某种程度来说,从不停止。

如图2所示,数据读取和数据处理属于不同的EventLoopGroup。有一个专用的EventLoopGroup从套接口读取数据,这样数据读取就不会被繁重的数据处理影响。

多个专用的线程不停地从网络套接口读取数据,一旦数据从套接口读出,存到直接内存(direct memory)后,会接着读取下一批数据。

目前,数据读取速度已赶上客户数据发送速度,极少数据会阻塞在客户端的套接口缓冲区,“TCP 断连”也极少会发生。

以r1reco pool为例,在之前的版本中,“TCP 断连”的次数很高,一分钟最少2500次,最多21000次(如图3所示)。但是,在目前的CAL Ingress中, 其次数明显减少,一分钟最少4次,最多也只有65次(如图4所示)。

之前版本:最少次数:2500,最多次数:21000

图 3 pool r1reco 的TCP断连次数 (之前版本)


前版本最少次数:4,最多次数:65

图 4 pool r1reco 的TCP 断连次数(目前版本)


2. GC优化

GC有一个“全局停顿(Stop-the-world)”的时段,不管使用何种GC算法,“全局停顿”都会发生。“全局停顿”表示GC执行时,JVM将停止应用的运行。因而,“全局停顿”发生时,CAL Ingress将停止读取数据,但是CAL用户仍然会继续发送数据。如果客户的网络缓冲区满了,CAL客户没法发送下一批数据,“TCP 断连”就会发生。

我们选择G1GC作为GC 收集器。而为了CAL Ingress能够挺过“全局停顿”,我们需要在两个方面调优GC:

  • 降低GC发生频率

  • 减少GC 停顿时间


(1)堆外缓存(Off-heap)


通常,所有的数据都是在内存里处理,所以,数据需要先从直接内存中拷贝到JVM堆内存,然后再处理。

而CAL Ingress采用堆外缓存(Off-heap)机制数据无需从网络缓冲区拷贝到堆内存,生产内部对象,所有的数据处理都是直接在直接内存里。由于数据都存在堆外缓存中,使用的堆内存很少,GC的发生频率降低。


(2)内存优化


GC主要包括三个耗时的处理:

  1. GC 新生代的对象扫描

  2. 数据拷贝

    a. 从一个满的幸存者区(survivor area)拷贝到另一个幸存者区

    b. 把对象从新生代(young area)拷贝/提升到老年代(old area)

  3.  Termination Attempts

降低#1和#2,有效的方法就是降低幸存者区的对象数和堆内存中的对象数。

如图5所示,我们用Java MAT 对堆内存进行分析。

根据memory dump,我们发现大部分的对象生命周期都较短,因此,最好能在新生代就释放它们。因而,我们增大新生代的大小,设置JVM参数增大它。

-XX:G1NewSizePercent=50

根据memory dump,大部分堆对象(62.82%)是SlicedByteBuf。而这些对象大部分是在VID 处理程序的“isVIDRelated”函数中生成的。所以我们需要优化该方法。在解析时,不再生成大量的SlicedByteBuf,我们把内容拷贝到堆内存, 在堆内存中按字节比较,这样就不必生产SlicedByteBuf。堆内存中的对象数从而大减。



图 5 Java MAT – 内存优化

针对 #3, 我们的解决方案是降低使用的线程数。

使用太多的线程将导致CPU资源争用(CPU contention)。如果有太多的进程或者系统活动需要CPU资源,GC工作线程很可能会因为CPU资源争用而被延迟执行。

通过研究,我们使用线程池(thread-pool),而不是每次executor 唤醒时都生产新的线程,从而降低了使用的线程数量


3. 数据处理过程优化

所有从网络套接口中读取的数据将一直存在直接内存中,直到全部处理完才释放数据。如果数据处理速度赶不上数据读取速度,直接内存很可能被用光,导致内存不足(OOM)

Traffic-数据流量,Tprocess-数据处理时间)

图 6 数据处理流程

如图6所示,数据要经过多个处理程序(handlers):解压缩(decompress),解析器(parser),VID处理程序,HeartBeat处理程序和Filer处理程序。

优化每个处理程序可以缩短数据处理时间。我们可以从以下三个方面优化:

  1. 改进耗时方法

  2. 批量压缩

  3. 延迟写(Lazy-write)和可控的内存使用


(1) 改进耗时方法


通过计算使用频率高的方法的时间消耗,我们找到了一些可以改进的方法。

例如,ByteBuf的Search/indexof方法。在ByteBuf里搜索,需要从直接内存里一个一个字节读取,再一个一个字节比较。从直接内存里读取数据很耗时,所以Search/indexof将消耗大量时间。所以,我们重写了Search方法,不再读取一个一个字节比较,而是取一批字节,一次性比较。读取的次数少了,Search执行的时间就少了。最终结果是减少了30%时间(如图7所示)。

图 7 函数执行时间


(2)批量压缩


真实环境中,从网络套接口中一次读取的数据包大小是多变的,大部分情况下,数据包是很小的。之前设计中,每一份数据单独解析,单独压缩。然而,压缩是耗时的操作,随着压缩的次数增加,消耗的时间也就增加了。所以,我们把多个小数据包合成一个大的数据包,等达到预定的数据包大小后再一次性压缩。目前,我们设置数据包压缩的阈值为160k,压缩的次数降到1/10,压缩时间也随之减少。


(3)延迟写和可控的内存使用


文件存储(filer storage)是一个慢速设备,需要很长的时间我们才能把数据写到文件存储设备。为了降低文件存储过慢(filer slowness)的影响,我们采用延迟写(Lazy-Write)。不再把数据直接写入慢速的文件存储设备,而是把数据先写到直接内存的ByteBuf,压缩后由其它的线程flush到文件存储设备。

为了控制直接内存的使用,我们采用了环形缓冲区(Ring-Buffer)。所有待写的数据会存在一个有预定大小的环形队列(Ring Queue)里。当由于文件存储过慢而使用太多的内存时,环形队列满了,就会丢弃后面的数据以免内存用光。


4. 系统优化

除了上面CAL Ingress Java进程的优化,我们还做了一些JVM和系统级别的调优。总的来说,1:使用cloudflare zlib 库降低压缩时间。2: 设置RSS和CPU Numa Pin提高CPU的效率。3: Off-CPU检查以降低CPU上下文切换(context-switch)。


四. 总结

总而言之,经过架构和性能的调整优化,CAL Ingress具备了低延迟和高吞吐的性能,可以很好地支持当前生产环境的高数据流量。例如,当CAL Ingress服务1000个连接的时候,总共的吞吐量能达到220MB/秒,并且没有“TCP 断连”。

图8展示了LnP基准测试的结果。它展现了CAL Ingress服务多个连接时,保证没有“TCP 断连”下的吞吐量和GC 停顿时间。

图 8 吞吐量和平均GC暂停时间

总结来说,CAL Ingress在吞吐量、延迟、连接断开以及数据丢失等方面改进了很多(如图9所示)。

图 9 改进


↓点击阅读原文,eBay大量优质职位虚席以待。

我们的身边,还缺一个你!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存