Kong 0.12.3 的一处内存泄漏分析

栏目: Lua · 发布时间: 6年前

内容简介:Kong 0.12.3 是最后一个以 API 形式组织接口的版本,后续的版本中 Kong 新增了 Service 和 Route 的概念,对于插件的应用规则更加复杂,当然也更为灵活。不过就我个人而言,我更喜欢直接以 API 的形式来管理接口,简单粗暴,所以也就用 0.12.3 这个版本多一些。然而这个版本当开启 bot-detection 插件的时候会有比较严重的内存泄漏问题。不过,复现这个 Bug 有两个前提:满足上面的条件之后,我们为 Kong 添加一个 API 并配置一个全局 bot-detectio

Kong 0.12.3 是最后一个以 API 形式组织接口的版本,后续的版本中 Kong 新增了 Service 和 Route 的概念,对于插件的应用规则更加复杂,当然也更为灵活。不过就我个人而言,我更喜欢直接以 API 的形式来管理接口,简单粗暴,所以也就用 0.12.3 这个版本多一些。

然而这个版本当开启 bot-detection 插件的时候会有比较严重的内存泄漏问题。不过,复现这个 Bug 有两个前提:

  1. OpenResty 需要配置多个 worker,对应 Kong 配置:  nginx_worker_processes

  2. db_cache_ttl 必须配置为 0,也就是让 Kong 的缓存永不过期

满足上面的条件之后,我们为 Kong 添加一个 API 并配置一个全局 bot-detection 插件:

接下来通过 ab 压测这个 API:

压测过程中,可以看到 Nginx 的一个 worker 的内存占用在持续上升:

Kong 0.12.3 的一处内存泄漏分析

通过 pmap-d2310 可以进一步确认实际内存占用情况:

Kong 0.12.3 的一处内存泄漏分析

不过现在依然无法确定到底是 OpenResty 的问题,还是 LuaJit 导致的泄漏。这里我们继续使用 lj-gc 来分析 LuaJit GC 的情况:

至此, 可以基本确定是 LuaJit 的问题了 。接下来,继续使用 lj-gc-objs 来分析 GC 的详细数据:

可以看到 cdata 的占用极大,似乎是一个 FFI 的问题 。接着使用 sample-bt-leaks 来生成火焰图,看下到底是哪个调用导致:

Kong 0.12.3 的一处内存泄漏分析

貌似并没啥异常。不过,在 bot-detection 中涉及到 FFI 调用的只有两处:

  1. ngx.re.find 用来匹配 UA

  2. resty.lrucache 用来缓存 UA

然而,这个内存泄漏的问题在 Kong 1.0 中却无法复现出来。通过对比这两个版本的 bot-detection 插件源码,几乎没有什么区别,所以可以暂时断定: 内存泄漏不是由 bot-detection 插件自身导致,同时也可以排除掉 ngx.re.findresty.lrucache 自身的问题

问题视乎走到了僵局。再次回顾 bot-detection 插件源码,看到下面这段:

这一段的主要功能就是初始化 ua 的缓存, ua_caches 是一张 weak table。这么做的好处就是,一旦插件的 blacklistwhitelist 发生变更,那么这个 ua_caches 就会被自动 GC 掉,以降低 worker 的内存开销。

理论上,只要插件配置不做变更,在 TTL 内这个 cache 应该会一直被命中 。实际上是不是这样?我们不妨来埋个点:

Kong 0.12.3 的一处内存泄漏分析

坦率的讲,当看到这个日志时,我的内心是奔溃的。原来发生泄漏的 worker 2310 缓存一直没有命中,所以一直在初始化 lrucache,导致内存泄漏。没有命中的原因就是,Kong 在每次调用插件的时候,传入的 conf 都是一个新对象(通过上图 table 的地址可以看出)。

理论上,这个 conf 应该是由 Kong 的 mlcache 缓存在 worker 的 lrucache 内,也就是在 L1 上,应该始终指向同一内存地址。这里为什么没有生效呢?通过查看 mlcache.lua 源码,问题最终定位在了这里:

当请求第一次进入 worker 时,会去 L1 查找 cache,这时候不会命中,所以接着去 L2(ngx.shared.DICT)查找。查找到之后,反序列化返回并更新 L1 缓存。更新 L1 时,会计算 L1 的 TTL 时长,也就是这句 localremaining_ttl=ttl-(now()-at)

当 Kong 配置 db_cache_ttl=0 时,那么这个时长 remaining_ttl 将会是一个负数,这样就会导致每次更新 L1 后,L1 会立即过期。所以 L1 永远不会命中,接下来返回的始终都是反序列化后的 L2,永远都是一个新的对象。

弄清楚了这个 Bug 之后,修复起来也很简单:

就是先判断 TTL 的时长,如果为 0 的话,就将 L1 设置为永不过期即可。

当然,这并不是一个完美的解决方案。因为 remaining_ttl 也有可能为 0,如果不巧这个 L2 还是一个 SENTINEL 状态,那么这个状态将会被永远缓存在 L1。鉴于篇幅原因这里不再赘述,解决方案可以参考这里:Fix expiration when the remaining TTL is exactly 0


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法竞赛入门经典(第2版)

算法竞赛入门经典(第2版)

刘汝佳 / 清华大学出版社 / 2014-6-1 / CNY 49.80

《算法竞赛入门经典(第2版)》是一本算法竞赛的入门与提高教材,把C/C++语言、算法和解题有机地结合在一起,淡化理论,注重学习方法和实践技巧。全书内容分为12 章,包括程序设计入门、循环结构程序设计、数组和字符串、函数和递归、C++与STL入门、数据结构基础、暴力求解法、高效算法设计、动态规划初步、数学概念与方法、图论模型与算法、高级专题等内容,覆盖了算法竞赛入门和提高所需的主要知识点,并含有大量......一起来看看 《算法竞赛入门经典(第2版)》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具