一、什么是这次要排查的“内存泄漏”
先打个生活化的比方,把计算机的内存比作一个公司的办公区,每个工位就是一块内存地址,员工(进程/资源)会占用工位工作,当员工离职后,对应的工位如果没人清理,就会一直被占着,不会留给后来的人用。日积月累,整个办公区的工位都被占满,新员工来了没位置,公司就得临时扩容,要是扩容资源也不够,整个公司就会停摆——对应到技术里的服务崩溃、OOM(内存溢出)。
这次我们要排查的,是Envoy代理服务里的一类特殊内存泄漏,具体和它的“Cluster(集群)”模块有关,还有每次更新配置时的资源释放环节,很多做微服务网关的开发同学可能都遇过:网关本来能用好几个月,突然某天开始内存飙升,过几天就挂了,排查半天找不到原因,大概率就是这个问题。
1.1 场景还原
假设你负责的API网关用Envoy做代理,后端有几个微服务组成的集群,比如用户服务集群、订单服务集群。为了适配业务变化,你经常要更新Envoy的配置:比如给用户服务集群加个节点,或者把订单服务的名字改了,这时候就要触发“配置热更新”——不用重启Envoy进程,直接把新的配置推过去,让Envoy自动切换到新的配置。
但你不知道的是,每次热更新时,旧的Cluster实例和它的统计数据(比如请求次数、成功率这些)没被释放,就像旧员工离职没清工位,时间一长,这些没人管的“垃圾工位”占满了内存,网关慢慢就扛不住了。
1.2 排查前的准备
排查这类问题就像查案,得先找线索,不用太复杂的工具,新手也能上手:比如用Envoy自带的Admin接口,或者通用的内存分析工具(比如针对C++的valgrind,或者模拟简化的C++代码用来演示)。这次我们用最容易懂的C++代码来模拟这个泄漏过程,不用复杂的生产环境,先搞懂原理再上手实际环境。
二、具体排查与修复演示
我们用两段完整的C++代码,一段是有泄漏的,一段是修复后的,全程有注释,方便大家对照看问题出在哪。
2.1 泄漏代码演示
// 技术栈:C++ 11(单一技术栈,全程用这个)
#include <iostream>
#include <map>
#include <string>
// 模拟Cluster的统计类,用来存这个集群的关键数据(比如请求数、成功率)
class ClusterStats {
public:
int request_count; // 累计请求次数
double success_rate; // 请求成功率
// 构造函数,初始化数据
ClusterStats() : request_count(0), success_rate(1.0) {}
};
// 模拟Envoy的Cluster管理器,负责创建、更新、销毁集群
class ClusterManager {
private:
// 用原始指针存所有Cluster的Stats,这是泄漏的根源:原始指针需要手动释放,没人管就会丢
std::map<std::string, ClusterStats*> all_clusters;
public:
// 模拟配置热更新的方法:传入集群名字,更新这个集群的配置
void update_cluster(const std::string& cluster_name) {
// 检查这个集群已经存在
if (all_clusters.find(cluster_name) != all_clusters.end()) {
// 这里本来应该先释放旧集群的Stats!但原代码没做,直接新建
std::cout << "热更新集群【" << cluster_name << "】,旧实例未释放!" << std::endl;
}
// 新建一个ClusterStats,赋值给对应集群的键
all_clusters[cluster_name] = new ClusterStats();
}
// 析构函数:程序退出时,应该释放所有未管理的内存
~ClusterManager() {
std::cout << "程序退出,开始清理所有内存..." << std::endl;
for (auto& item : all_clusters) {
delete item.second; // 释放每个集群的Stats
}
}
};
int main() {
ClusterManager manager;
// 模拟3次配置热更新,都是同一个叫"user-service"的集群
for (int i = 0; i < 3; ++i) {
manager.update_cluster("user-service");
std::cout << "第" << (i+1) << "次热更新,当前管理的集群数:" << (i+1) << std::endl;
}
// 模拟:中途的两次热更新,旧的Stats已经没被任何指针指向了,内存泄漏了
// 只有程序退出时才会清理所有,但如果服务一直运行,泄漏会越来越多
return 0;
}
运行这段代码的话,你会看到每次热更新都提示“旧实例未释放”,程序退出时才清理,但如果服务一直跑,没机会退出,泄漏的内存会越来越大——这就是实际Envoy里会发生的情况。
2.2 修复后的代码
找到问题后很简单,核心就是“旧资源要在更新时释放”,用C++的智能指针来管理内存,就不用手动delete了,避免忘释放的问题:
// 技术栈:C++ 11
#include <iostream>
#include <map>
#include <string>
#include <memory> // 引入智能指针头文件,用来自动管理内存
class ClusterStats {
public:
int request_count;
double success_rate;
ClusterStats() : request_count(0), success_rate(1.0) {}
};
class ClusterManager {
private:
// 用智能指针std::unique_ptr代替原始指针,离开作用域自动释放内存
std::map<std::string, std::unique_ptr<ClusterStats>> all_clusters;
public:
void update_cluster(const std::string& cluster_name) {
// 先删除旧的集群实例,智能指针会自动释放对应的内存,不会泄漏
all_clusters.erase(cluster_name);
// 新建集群实例,交给智能指针管理
all_clusters.emplace(cluster_name, std::make_unique<ClusterStats>());
std::cout << "热更新集群【" << cluster_name << "】,旧实例已释放!" << std::endl;
}
};
int main() {
ClusterManager manager;
for (int i = 0; i < 3; ++i) {
manager.update_cluster("user-service");
std::cout << "第" << (i+1) << "次热更新,当前管理的集群数:" << (i+1) << std::endl;
}
// 程序退出时,所有智能指针自动释放,没有内存泄漏
return 0;
}
运行修复后的代码,你会看到每次热更新后旧实例都被释放了,全程不会有泄漏,不管热更新多少次,内存占用都稳定。
三、应用场景分析
3.1 适用场景
这个问题主要出在“需要频繁热更新配置的代理/网关服务”,比如用Envoy做微服务网关、API网关的场景:业务经常变,配置(后端服务地址、集群节点、权重等)更新频繁,每次都要触发热更新,要是没处理旧Cluster的释放,就会慢慢泄漏内存。另外,任何用动态配置、经常更新实例的服务,都要注意这个坑。
3.2 不适用场景
如果你的服务配置很少更新,比如一年才改一两次,那内存泄漏的速度会非常慢,可能几个月都不会有问题,这种场景下可以不用太紧张,但也要定期监控内存,避免积累到一定程度才发现。
四、技术优缺点
先说说原来的原始指针方案的优缺点: 优点:原始指针灵活,性能稍微高一点,适合对内存性能要求极高的场景; 缺点:必须手动管理内存,忘释放、释放早都会出问题,容易引发内存泄漏,尤其是在复杂的配置更新逻辑里,很容易漏。
再说说修复后的智能指针方案的优缺点: 优点:自动管理内存,不用手动释放,从根源避免了漏释放的问题,代码更安全,调试的时候也不容易出现野指针的问题; 缺点:智能指针的性能比原始指针稍差,不过对大部分网关服务来说,这个性能损耗完全可以忽略,换得内存安全非常值。
五、注意事项
- 配置热更新时,一定要先清理旧的Cluster实例,再创建新的,不能直接替换;
- 用Envoy的Admin接口定期查看内存消耗,比如访问
http://你的Envoy地址:端口/memory,看Cluster相关的内存占比,要是发现Cluster的内存一直在涨,大概率就是这个泄漏问题; - 别混用智能指针和原始指针,很容易出现指针悬空、内存泄漏的问题;
- 尽量用Envoy官方推荐的配置更新方式,不要自己写额外的资源管理逻辑,官方已经处理了大部分边界情况。
六、总结
这次排查的核心其实就是“资源释放的时机”:每次换新集群的时候,一定要把旧的清干净,不能让旧的资源占着内存。用智能指针这样的自动内存管理工具,能帮你少踩很多这类坑。对于用Envoy做网关的同学来说,这个问题非常常见,只要记住“热更新前先释放旧集群”,就能避免网关内存泄漏的问题,保障服务稳定。
评论
围绕“内存泄漏排查实录:Envoy中Cluster统计累积与配置热更新资源释放”参与讨论