深入理解基于 eBPF 的 C/C++ 内存泄漏分析

对于 C/C++ 程序员来说,内存泄露问题是一个老生常谈的问题。排查内存泄露的方法有很多,比如使用 valgrind、gdb、asan、tsan 等工具,但是这些工具都有各自的局限性,比如 valgrind 会使程序运行速度变慢,gdb 需要了解代码并且手动打断点,asan 和 tsan 需要重新编译程序。对于比较复杂,并且在运行中的服务来说,这些方法都不是很方便。

好在有了 eBPF,我们可以使用它来分析内存泄露问题,不需要重新编译程序,对程序运行速度的影响也很小。eBPF 的强大有目共睹,不过 eBPF 也不是银弹,用来分析内存泄露也还是有很多问题需要解决,本文接下来就来探讨一下基于 eBPF 检测会遇到的常见问题。

内存泄露模拟

在 C/C++ 中,内存泄露是指程序在运行过程中,由于某些原因导致未能释放已经不再使用的内存,从而造成系统内存的浪费。内存泄露问题一旦发生,会导致程序运行速度减慢,甚至进程 OOM 被杀掉。内存泄露问题的发生,往往是由于在编写程序时,没有及时释放内存;或者是由于程序设计的缺陷,导致程序在运行过程中,无法释放已经不再使用的内存。

下面是一个简单的内存泄露模拟程序,程序会在循环中分配内存,但是没有释放,从而导致内存泄露。main 程序如下,发生泄露的函数调用链路是 main->caller->slowMemoryLeak:

#include <iostream>

namespace LeakLib {
    void slowMemoryLeak();
}

int caller() {
    std::cout << "In caller" << std::endl;
    LeakLib::slowMemoryLeak();
    return 0;
}
int main() {
    std::cout << "Starting slow memory leak program..." << std::endl;
    caller();
    return 0;
}

其中内存泄露的代码在 slowMemoryLeak 函数中,具体如下:

namespace LeakLib {
    void slowMemoryLeak() {
        int arrSize = 10;
        while (true) {
            int* p = new int[arrSize];
            for (int i = 0; i < arrSize; ++i) {
                p[i] = i; // Assign values to occupy physical memory
            }
            sleep(1); // wait for 1 second
        }
    }
}

注意这里编译的时候,带了帧指针选项(由 -fno-omit-frame-pointer 选项控制),这是因为 eBPF 工具需要用到帧指针来进行调用栈回溯。如果这里忽略掉帧指针的话(-fomit-frame-pointer),基于 eBPF 的工具就拿不到内存泄露的堆栈信息。完整编译命令如下(-g 可以不用加,不过这里也先加上,方便用 gdb 查看一些信息):

$ g++ main.cpp leak_lib.cpp -o main -fno-omit-frame-pointer -g

memleak 分析

接下来基于 eBPF 来进行内存分析泄露,BCC 自带了一个 memleak 内存分析工具,可以用来分析内存泄露的调用堆栈。拿前面的示例泄露代码来说,编译后执行程序,然后执行内存泄露检测 memleak -p $(pgrep main) --combined-only。

目前版本的 memleak 工具有 bug,在带 --combined-only 打印的时候,会报错。修改比较简单,我已经提了 PR #4769,已经被合并进 master。仔细看脚本的输出,发现这里调用堆栈其实不太完整,丢失了 slowMemoryLeak 这个函数调用。

[11:19:44] Top 10 stacks with outstanding allocations:
	480 bytes in 12 allocations from stack
		operator new(unsigned long)+0x1c [libstdc++.so.6.0.30]
		caller()+0x31 [main]
		main+0x31 [main]
		__libc_start_call_main+0x7a [libc.so.6]

调用链不完整

这里为啥会丢失中间的函数调用呢?我们知道eBPF 相关的工具,是通过 frame pointer 指针来进行调用堆栈回溯的,具体原理可以参考朋友的文章 消失的调用栈帧-基于fp的栈回溯原理解析。如果遇到调用链不完整,基本都是因为帧指针丢失,下面来验证下。

首先用 objdump -d -S main > main_with_source.asm 来生成带源码的汇编指令,找到 slowMemoryLeak 函数的汇编代码,如下图所示:

从这段汇编代码中,可以看到 new int[] 对应的是一次 _Znam@plt 的调用。这是 C++ 的 operator new[] 的名字修饰(name mangling)后的形式,如下:

$ c++filt _Znam
operator new[](unsigned long)

我们知道在 C++ 中,new 操作用来动态分配内存,通常会最终调用底层的内存分配函数如 malloc。这里 _Znam@plt 是通过 PLT(Procedure Linkage Table) 进行的,它是一个动态解析的符号,通常是 libstdc++(或其他 C++ 标准库的实现)中实现的 operator new[]。_Znam@plt 对应的汇编代码如下:

0000000000001030 <_Znam@plt>:
    1030:       ff 25 ca 2f 00 00       jmp    *0x2fca(%rip)        # 4000 <_Znam@GLIBCXX_3.4>
    1036:       68 00 00 00 00          push   $0x0
    103b:       e9 e0 ff ff ff          jmp    1020 <_init+0x20>

这里并没有像 slowMemoryLeak 调用一样去做 push %rbp 的操作,所以会丢失堆栈信息。这里为什么会没有保留帧指针呢?前面编译的时候带了 -fno-omit-frame-pointer 能保证我们自己的代码带上帧指针,但是对于 libstdc++ 这些依赖到的标准库,我们是无法控制的。当前系统的 C++ 标准库在编译的时候,并没有带上帧指针,可能是因为这样可以减少函数调用的开销(减少执行的指令)。是否在编译的时候默认带上 -fno-omit-frame-pointer 还是比较有争议,文章最后专门放一节:默认开启帧指针来讨论。

相关视频推荐

5种内存泄漏检测方式,让你重新理解内存管理

4种内存泄漏的解决方案,每一种背后都有哪些隐藏技术

面对内存再不发怵,手把手带你实现内存池(自行准备linux环境)

免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

tcmalloc 泄露分析

如果想拿到完整的内存泄露函数调用链路,可以带上帧指针重新编译 libstdc++,不过标准库重新编译比较麻烦。其实日常用的比较多的是 tcmalloc,内存分配管理更加高效些。这里为了验证上面的代码在 tcmalloc 下的表现,我用 -fno-omit-frame-pointer 帧指针编译了 tcmalloc 库。如下:

git clone https://github.com/gperftools/gperftools.git
cd gperftools
./autogen.sh
./configure CXXFLAGS="-fno-omit-frame-pointer" --prefix=/path/to/install/dir
make
make install

接着运行上面的二进制,重新用 memleak 来检查内存泄露,注意这里用 -O 把 libtcmalloc.so 动态库的路径也传递给了 memleak。参数值存在 obj 中,在 attach_uprobe 中用到,指定了要附加 uprobes 或 uretprobes 的二进制对象,可以是要跟踪的函数的库路径或可执行文件。详细文档可以参考 bcc: 4. attach_uprobe。比如下面的调用方法:

# 在 libc 的 getaddrinfo 函数入口打桩,当进入函数时,会调用自定义的 do_entry 函数
b.attach_uprobe(name="c", sym="getaddrinfo", fn_name="do_entry")

注意在前面的示例中,没有指定 -O,默认就是 “c”,也就是用 libc 分配内存。在用 tcmalloc 动态库的时候,这里 attach_uprobe 和 attach_uretprobe 必须要指定库路径:

bpf.attach_uprobe(name=obj, sym=sym, fn_name=fn_prefix + "_enter", pid=pid)
bpf.attach_uretprobe(name=obj, sym=sym, fn_name=fn_prefix + "_exit", pid=pid)

不过工具的输出有点出乎语料,这里竟然没有输出任何泄露的堆栈了:

$ memleak -p $(pgrep main) --combined-only -O /usr/local/lib/libtcmalloc.so
Attaching to pid 1409827, Ctrl+C to quit.
[19:55:45] Top 10 stacks with outstanding allocations:

[19:55:50] Top 10 stacks with outstanding allocations:

明明 new 分配的内存没有释放,为什么 eBPF 的工具检测不到呢?

深入工具实现

在猜测原因之前,先仔细看下 memleak 工具的代码,完整梳理下工具的实现原理。首先能明确的一点是,工具最后的输出部分,是每个调用栈以及其泄露的内存量。为了拿到这个结果,用 eBPF 分别在内存分配和释放的时候打桩,记录下当前调用栈的内存分配/释放量,然后进行统计。核心的逻辑如下:

  1. gen_alloc_enter: 在各种分配内存的地方,比如 malloc, cmalloc, realloc 等函数入口(malloc_enter)打桩(attach_uprobe),获取当前调用堆栈 id 和分配的内存大小,记录在名为 sizes 的字典中;

  2. gen_alloc_exit2: 在分配内存的函数退出位置(malloc_exit)打桩(attach_uretprobe),拿到此次分配的内存起始地址,同时从 sizes 字段拿到分配内存大小,记录 (address, stack_info) 在 allocs 字典中; 同时用 update_statistics_add 更新最后的结果字典 combined_allocs,存储栈信息和分配的内存大小,次数信息;

  3. gen_free_enter: 在释放内存的函数入口处打桩(gen_free_enter),从前面 allocs 字典中根据要释放的内存起始地址,拿到对应的栈信息,然后用 update_statistics_del 更新结果字典 combined_allocs,也就是在统计中,减去当前堆栈的内存分配总量和次数。

GDB 堆栈跟踪

接着回到前面的问题,tcmalloc 通过 new 分配的内存,为啥统计不到呢?很大可能是因为 tcmalloc 底层分配和释放内存的函数并不是 malloc/free,也不在 memleak 工具的 probe 打桩的函数内。那么怎么知道前面示例代码中,分配内存的调用链路呢?比较简单的方法就是用 GDB 调试来跟踪,注意编译 tcmalloc 库的时候,带上 debug 信息,如下:

$ ./configure CXXFLAGS="-g -fno-omit-frame-pointer" CFLAGS="-g -fno-omit-frame-pointer"

编译好后,可以用 objdump 查看 ELF 文件的头信息和各个段的列表,验证动态库中是否有 debug 信息,如下:

$ objdump -h /usr/local/lib/libtcmalloc_debug.so.4 | grep debug
/usr/local/lib/libtcmalloc_debug.so.4:     file format elf64-x86-64
 29 .debug_aranges 000082c0  0000000000000000  0000000000000000  000b8c67  2**0
 30 .debug_info   00157418  0000000000000000  0000000000000000  000c0f27  2**0
 31 .debug_abbrev 00018a9b  0000000000000000  0000000000000000  0021833f  2**0
 32 .debug_line   00028924  0000000000000000  0000000000000000  00230dda  2**0
 33 .debug_str    0009695d  0000000000000000  0000000000000000  002596fe  2**0
 34 .debug_ranges 00008b30  0000000000000000  0000000000000000  002f005b  2**0

接着重新用 debug 版本的动态库编译二进制,用 gdb 跟踪进 new 操作符的内部,得到结果如下图。可以看到确实没有调用 malloc 函数。

其实 tcmalloc 的内存分配策略还是很复杂的,里面有各种预先分配好的内存链表,申请不同大小的内存空间时,有不少的策略来选择合适的内存地址。

正常内存泄露分析

前面不管是 glibc 还是 tcmalloc,用 new 来分配内存的时候,memleak 拿到的分析结果都不是很完美。这是因为用 eBPF 分析内存泄露,必须满足两个前提:

  1. 编译二进制的时候带上帧指针(frame pointer),如果有依赖到标准库或者第三方库,也都必须带上帧指针;

  2. 实际分配内存的函数,必须在工具的 probe 打桩的函数内,比如 malloc, cmalloc, realloc 等函数;

那么下面就来看下满足这两个条件后,内存泄露的分析结果。修改上面的 leak_lib.cpp 中内存分配的代码:

// int* p = new int[arrSize];
int* p = (int*)malloc(arrSize * sizeof(int));

然后重新编译运行程序,这时候 memleak 就能拿到完整的调用栈信息了,如下:

$ g++ main.cpp leak_lib.cpp -o main -fno-omit-frame-pointer -g
# run main binary here

$ memleak -p $(pgrep main) --combined-only
Attaching to pid 2025595, Ctrl+C to quit.
[10:21:09] Top 10 stacks with outstanding allocations:
	200 bytes in 5 allocations from stack
		LeakLib::slowMemoryLeak()+0x20 [main]
		caller()+0x31 [main]
		main+0x31 [main]
		__libc_start_call_main+0x7a [libc.so.6]
[10:21:14] Top 10 stacks with outstanding allocations:
	400 bytes in 10 allocations from stack
		LeakLib::slowMemoryLeak()+0x20 [main]
		caller()+0x31 [main]
		main+0x31 [main]
		__libc_start_call_main+0x7a [libc.so.6]

如果分配内存的时候用 tcmalloc,也是可以拿到完整的泄露堆栈。

内存火焰图可视化

在我之前的 复杂 C++ 项目堆栈保留以及 ebpf 性能分析 这篇文章中,用 BCC 工具做 cpu profile 的时候,可以用 FlameGraph 把输出结果转成 CPU 火焰图,很清楚就能找到 cpu 的热点代码。对于内存泄露,我们同样也可以生成内存火焰图。

内存火焰图的生成步骤也类似 cpu 的,先用采集工具比如 BCC 脚本采集数据,然后将采集到的数据转换为 FlameGraph 可以理解的格式,之后就可以使用 FlameGraph 脚本将转换后的数据生成一个 SVG 图像。每个函数调用都对应图像中的一块,块的宽度表示该函数在采样中出现的频率,从而可以识别资源使用的热点。FlameGraph 识别的每行数据的格式通常如下:

[堆栈跟踪] [采样值]
main;foo;bar 58

这里的“堆栈跟踪”是指函数调用栈的一个快照,通常是一个由分号分隔的函数名列表,表示从调用栈底部(通常是 main 函数或者线程的起点)到顶部(当前执行的函数)的路径。而“采样值”可能是在该调用栈上花费的 CPU 时间、内存使用量或者是其他的资源指标。对于内存泄露分析,采样值可以是内存泄露量,或者内存泄露次数。

可惜的是,现在的 memleak 还不支持生成可以转换火焰图的数据格式。不过这里改起来并不难,PR 4766 有实现一个简单的版本,下面就用这个 PR 里的代码为例,来生成内存泄露火焰图。

可以看到这里生成的采集文件很简单,如上面所说的格式:

__libc_start_call_main+0x7a [libc.so.6];main+0x31 [main];caller()+0x31 [main];LeakLib::slowMemoryLeak()+0x20 [main] 480

最后用 FlameGraph 脚本来生成火焰图,如下:

默认开启帧指针

文章最后再来解决下前面留下的一个比较有争议的话题,是否在编译的时候默认开启帧指针。我们知道 eBPF 工具依赖帧指针才能进行调用栈回溯,其实栈回溯的方法有不少,比如:

  • DWARF: 调试信息中增加堆栈信息,不需要帧指针也能进行回溯,但缺点是性能比较差,因为需要将堆栈信息复制到用户空间来进行回溯;

  • ORC: 内核中为了展开堆栈创建的一种格式,其目的与 DWARF 相同,只是简单得多,不能在用户空间使用;

  • CTF Frame: 一种新的格式,比 eh_frame 更紧凑,展开堆栈速度更快,并且更容易实现。 仍在开发中,不知道什么时候能用上。

所以如果想用比较低的开销,拿到完整的堆栈信息,帧指针是目前最好的方法。既然帧指针这么好,为什么有些地方不默认开启呢?在 Linux 的 Fedora 发行版社区中,是否默认打开该选项引起了激烈的讨论,最终达成一致,在 Fedora Linux 38 中,所有的库都会默认开启 -fno-omit-frame-pointer 编译,详细过程可以看 Fedora wiki: Changes/fno-omit-frame-pointer。

上面 Wiki 中对打开帧指针带来的影响有一个性能基准测试,从结果来看:

  • 带帧指针使用 GCC 编译的内核,速度会慢 2.4%;

  • 使用帧指针构建 openssl/botan/zstd 等库,没有受到显着影响;

  • 对于 CPython 的基准测试性能影响在 1-10%;

  • Redis 的基准测试基本没性能影响;

当然,不止是 Fedora 社区倾向默认开启,著名性能优化专家 Brendan Gregg 在一次分享中,建议在 gcc 中直接将 -fno-omit-frame-pointer 设为默认编译选项:

• Once upon a tme, x86 had fewer registers, and the frame pointer register was reused for general purpose to improve performance. This breaks system stack walking. • gcc provides -fno-omit-frame-pointer to fix this – Please make this the default in gcc!

此外,在一篇关于 DWARF 展开的论文 提到有 Google 的开发者在分享中提到过,google 的核心代码编译的时候都带上了帧指针。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/407553.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Spring Boot的安康旅游网站的设计与实现,计算机毕业设计(带源码+论文)

源码获取地址&#xff1a; 码呢-一个专注于技术分享的博客平台一个专注于技术分享的博客平台,大家以共同学习,乐于分享,拥抱开源的价值观进行学习交流http://www.xmbiao.cn/resource-details/1760645517548793858

uni-app 人脸识别 App端

文章目录 背景介绍开发前准备基础版获取视频流人脸识别版本这时候就可以开心的调试了背景介绍 本文介绍如何制作人脸打卡等类似功能的实现。 使用nvue+live-pusher来实现。在App端这是成本较低的可以控制样式的方案了 实现了两个版本 基础版本:视频流 => 抓拍照片 => 传…

Unity与Android交互通信系列(5)

在前述文章中&#xff0c;已经使用了AndroidJavaProxy代理接口&#xff0c;本节我们将详细的介绍AndroidJavaProxy代理的用法。正如其名&#xff0c;AndroidJavaProxy是一个代理&#xff0c;它在Android端代码与Unity端代码交互中起一个桥接作用。其一般用法为在Java代码中定义…

《Docker 简易速速上手小册》第2章 容器和镜像(2024 最新版)

文章目录 2.1 理解 Docker 容器2.1.1 重点基础知识2.1.2 重点案例&#xff1a;使用 Docker 运行 Python 应用2.1.3 拓展案例 1&#xff1a;Docker 中的 Flask 应用2.1.4 拓展案例 2&#xff1a;Docker 容器中的数据分析 2.2 创建与管理 Docker 镜像2.2.1 重点基础知识2.2.2 重点…

电子器件系列63:肖特基二极管NSQ03A04\SS34C

以下是肖特基二极管_SS34C_规格书_SLKOR(萨科微),立创编号C880740 以下是肖特基二极管NSQ03A04的规格书&#xff1a; 稍微比较下参数&#xff0c;发现两者参数接近&#xff0c;ss34的几个参数还要略微好一些&#xff0c;可以用ss34来作替换。 在电源电路中的应用&#xff1a; …

java——IO流基础

目录 IO流IO流的四大分类&#xff1a;IO流的体系&#xff1a;FileinputStream&#xff08;文件字节输入流&#xff09;FileOutputStream(文件字节输出流&#xff09;文件复制资源释放FileReader&#xff08;文件字符输入流&#xff09;FileWriter(文件字符输出流&#xff09;缓…

[rospack] Error: package ‘moveit_setup_assistant‘ not found解决方法

执行&#xff1a;rosrun moveit_setup_assistant moveit_setup_assistant 显示报错&#xff1a;[rospack] Error: package ‘moveit_setup_assistant’ not found 这是由于没有安装moveit的包&#xff0c;所以找不到。 解决方法就是安装moveit包&#xff1a; sudo apt-get in…

【XR806开发板试用】踩坑deepin20,回归ubuntu,开发环境

首先&#xff0c;感谢给予XR806的试用机会。由于之前使用过deepin进行过开发&#xff0c;使用了很长的一段时间&#xff0c;印象还是不错的&#xff0c;所以&#xff0c;这次就想用deepin进行xr806的开发&#xff0c;但过于折腾。 sudo 安装 1. sudo apt-get install build-e…

128 Linux 系统编程6 ,C++程序在linux 上的调试,GDB调试

今天来整理 GDB 调试。 在windows 上我们使用vs2017开发&#xff0c;可以手动的加断点&#xff0c;debug。 那么在linux上怎么加断点&#xff0c;debug呢&#xff1f;这就是今天要整理的GDB调试工具了。 那么有些同学可能会想到&#xff1a;我们在windows上开发&#xff0c;…

Threejs 实现3D影像地图,Json地图,地图下钻

1.使用threejs实现3D影像地图效果&#xff0c;整体效果看起来还可以&#xff0c;底层抽象了基类&#xff0c;实现了通用&#xff0c;对任意省份&#xff0c;城市都可以只替换数据&#xff0c;即可轻松实现效果。 效果如下&#xff1a; 链接https://www.bilibili.com/video/BV1…

nginx 模块 常见内置变量 location

一、nginx 模块 ngx_http_core_module 核心模块 ngx_http_access_module 访问控制模块 deny allow ngx_http_auth_basic_module 身份验证 小红小名&#xff08;虚拟用户&#xff09; ftp也有虚拟用户 ngx_http_gzip_module 压缩模块 ngx_http_gzip_static_modul…

LeetCode二叉树中的第 K 大层和

题目描述 给你一棵二叉树的根节点 root 和一个正整数 k 。 树中的 层和 是指 同一层 上节点值的总和。 返回树中第 k 大的层和&#xff08;不一定不同&#xff09;。如果树少于 k 层&#xff0c;则返回 -1 。 注意&#xff0c;如果两个节点与根节点的距离相同&#xff0c;则…

互联网广告投放与IP地理位置定位

随着互联网的发展和普及&#xff0c;互联网广告投放成为各行业推广营销的重要方式之一。而结合IP地理位置定位技术&#xff0c;可以实现精准定向&#xff0c;提高广告投放的效果和精准度。IP数据云将探讨互联网广告投放与IP地理位置定位的关系&#xff0c;分析其优势和应用场景…

Android LinearLayout 如何让子元素靠下居中对齐 center bottom

Android LinearLayout 如何让子元素靠下居中对齐 center bottom 首先你需要知道两个知识点&#xff1a; android:layout_gravity 指定的是当前元素在父元素中的位置android:gravity 指定的是当前元素子元素的排布位置 比如&#xff1a; 有这么一个布局&#xff0c;我需要让…

【Java程序设计】【C00279】基于Springboot的智慧外贸平台(有论文)

基于Springboot的智慧外贸平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的智慧外贸平台 本系统分为系统功能模块、管理员功能模块、买家功能模块以及商家功能模块。 系统功能模块&#xff1a;在平台首页可以…

snowflake雪花算法

snowflake雪花算法 雪花算法是由Twitter开源的由64位整数组成分布式ID&#xff0c;性能较高&#xff0c;并且在单机上递增。 为什么叫分布式ID生成器&#xff1f; 因为10bit-工作机器id。 雪花算法&#xff08;Snowflake Algorithm&#xff09;之所以适用于分布式系统&#…

opengl 学习纹理

一.纹理是什么&#xff1f; 纹理是一个2D图片&#xff08;甚至也有1D和3D的纹理&#xff09;&#xff0c;它可以用来添加物体的细节&#xff1b;类似于图像一样&#xff0c;纹理也可以被用来储存大量的数据&#xff0c;这些数据可以发送到着色器上。 采样是指用纹理坐标来获取纹…

代码随想录算法训练营第50天|123.买卖股票的最佳时机III、188.买卖股票的最佳时机IV

文章目录 123.买卖股票的最佳时机III思路代码 188.买卖股票的最佳时机IV思路代码 123.买卖股票的最佳时机III 题目链接&#xff1a;123.买卖股票的最佳时机III 文章讲解&#xff1a;代码随想录|123.买卖股票的最佳时机III 视频讲解&#xff1a;123.买卖股票的最佳时机III 思路 …

7分钟0基础彻底理解常用数据压缩原理---哈夫曼编码

前言 如果你之前没有做过数据压缩&#xff0c;或者想要了解数据压缩的原理&#xff0c;那么这编文章将会帮到你。这编文章将会带你彻底了解哈夫曼编码原理&#xff0c;这种编码方式常用作的图片无损压缩&#xff0c;和ZIP的等压缩存储。 思考&#xff0c;计算机的存储与解析获…

消息中间件篇之RabbitMQ-延时队列

一、延时队列 延迟队列&#xff1a;进入队列的消息会被延迟消费的队列。 场景&#xff1a;超时订单、限时优惠、定时发布。 延迟队列死信交换机TTL&#xff08;生存时间&#xff09;。 二、死信交换机 当一个队列中的消息满足下列情况之一时&#xff0c;可以成为死信&#xf…