Hyperf 1.x Proxy 缓存失效问题

本文仅针对Hyperf 1.x版本, 实际上2.x版本也遭遇到同样的现象, 但是和1.x版本是不一样的问题

在对SCRM项目后端应用进行迁移时, 收到集群的一个告警

image-20200925191614707

OOM的全称是Out Of Memory, 翻译成中文就是内存用完了, 和后面的killed连起来就是这个容器组的内存用完了所以k8s把它杀掉了, 容器组的内存限制配置如下

可以看到上面的配置运行容器组使用1G的内存, 这对正常的PHP应用来说是足够的, 况且这个容器组刚启动还没有任何流量的情况下就被OOM killed了, 这显然是不正常的

在传统的Nginx + PHP-FPM架构中, 一个请求对应一个进程, 请求结束后进程占用的内存会被释放, 但Swoole是常驻型的, 一个进程处理所有请求, 内存只会在GC时或者进程结束时被释放

由于容器是刚启动就被OOM killed, 也就是说不是由于接口导致内存泄露的, 那就是启动阶段的某个环节出了问题, 启动时的这两行日志引起了我的注意

翻译过来就是发现了可用的缓存, 跳过app/vendor的扫描, 这里的扫描指的是Hyperf的注解扫描, 注解在业务代码中大量使用, 极大的提高了开发效率, 但是注解并不是PHP原生支持的功能, 框架读取我们写的注解实际上用的是很原始的方式, 即是对所有代码进行语法分析, 语法分析的本质是字符串操作, 也就是把所有代码加载到内存中, 然后转换成AST, 再读取其中的注解, 如果不理解这个过程也没关系, 只要记住一点扫描的动作需要把所有代码当做字符串加载到内存中

Hyperf提供了预生成缓存的功能, 正如上面两行输出, 我们可以先生成一个缓存然后应用启动的时候就不需要扫描了, 这个预生成的动作放在镜像构建里, 这会增加镜像构建时的开销, 但减轻了容器运行时的开销

在Dockerfile中使用了composer dump-autoload -o命令进行预生成缓存, 执行完后会生成两个缓存文件

上面两个以.cache为后缀的就是缓存文件, 照理说有缓存了Hyperf启动时就会读取, 然后跳过上面说的注解扫描动作, 但奇怪的事情发生了, 在k8s里的容器启动时输出的日志是这样的

从日志中看到框架并没有读取app缓存, 而是重新进行了一次扫描, 基本可以确定这个扫描动作就是导致OOM的原因, 那为什么框架会不读取app缓存呢, 下面进行debug

如果需要逐个环节去追Hyperf的启动流程, 这会很复杂, 所以使用一种比较简单粗暴的方法快速定位相关代码, 搜索关键字

我们直接在vendor/hyperf目录下搜索Scanning这个字符串, 这能让我们快速定位到相关代码

image-20200925193716874

追进去找到一个loadMetadata方法, 大概长这样

从这个方法代码看起来只能是第八行的hasAvailableCache调用返回了false导致代码走到15行后面的生成注解逻辑, 继续追hasAvailableCache方法

可以看到有四个地方会返回false, 在每个可能返回false的地方打上标记, 发现是走到了第13行的false返回了, 这段代码的逻辑是读取缓存文件(runtime/container/annotations.app.cache)的第一行, 然后与$pathsHash变量进行比较, 所以问题就是缓存文件的第一行和$pathsHash的值不一致导致的, 回到loadMetadata方法查看$pathsHash的生成规则

可以看到$pathsHash是由$paths数组使用逗号拼成字符串后的md5值, 那$paths是什么呢, 篇幅所限这里就不展开追踪, $paths的值是从配置annotations.scan.paths字段获取的, 先来看一下这个配置

这个配置指示了注解扫描器要扫描哪些目录下的文件, 从上面的代码来看就是扫描app/目录下除了Model下的所有文件, 打印这个数组可以看到

这看起来没什么问题, 因为这些目录是固定的, 那为什么把这些目录拼起来之后md5的值会改变呢? 这问题的答案很简单, PHP的遍历是不保证顺序的, 就比如上面的输出是镜像构建时生成缓存的时候输出的, 但是当容器启动时, 输出就变成了

虽然里面的内容没变但是顺序变了, 拼接成字符串之后自然就不一样了! 解决方法也很简单, 只要确保$paths数组内的目录顺序始终保持一致就ok了

仅加了一行代码, 在返回$paths数组前使用asort函数对数组的值进行一次排序, 这样就确保了每次返回的$paths数组内容都是完全一致的了

修改前

修改后

内存使用量下降了一倍, Nice!

最后更新于

这有帮助吗?