简介
MySQL 本地驱动程序与 MySQL 客户端库的内存管理方式不同。这些库在内存分配和释放方式、从 MySQL 读取结果时如何分块分配内存、存在哪些调试和开发选项以及如何将从 MySQL 读取的结果链接到 PHP 用户变量方面存在差异。
以下注释旨在向有兴趣了解 MySQL 本地驱动程序 C 代码级内容的用户介绍和总结。
使用的内存管理函数
所有内存分配和释放都使用 PHP 内存管理函数完成。因此,mysqlnd 的内存消耗可以使用 PHP API 调用(例如 memory_get_usage())进行跟踪。由于内存是使用 PHP 内存管理进行分配和释放的,因此更改可能不会立即在操作系统级别显示。PHP 内存管理充当代理,可能会延迟向系统释放内存。因此,很难比较 MySQL 本地驱动程序和 MySQL 客户端库的内存使用情况。MySQL 客户端库直接使用操作系统内存管理调用,因此其影响可以在操作系统级别立即观察到。
PHP 强制执行的任何内存限制也影响 MySQL 本地驱动程序。这可能会在获取超出 PHP 提供的剩余内存大小的较大结果集时导致内存不足错误。由于 MySQL 客户端库不使用 PHP 内存管理函数,因此它不遵守任何设置的 PHP 内存限制。如果使用 MySQL 客户端库,根据部署模型,PHP 进程的内存占用可能会超过 PHP 内存限制。但 PHP 脚本也可能能够处理更大的结果集,因为分配用于保存结果集的部分内存不受 PHP 引擎控制。
PHP 内存管理函数通过一个轻量级包装器由 MySQL 本地驱动程序调用。包装器除了其他优点外,还使调试更容易。
结果集的处理
各种 MySQL 服务器和各种客户端 API 区分 缓冲和非缓冲 结果集。非缓冲结果集在客户端遍历结果时按行从 MySQL 传输到客户端。缓冲结果在传递到客户端之前,会由客户端库全部获取。
MySQL 本地驱动程序使用 PHP 流进行与 MySQL 服务器的网络通信。MySQL 发送的结果从 PHP 流网络缓冲区获取到 mysqlnd 的结果缓冲区。结果缓冲区由 zval 组成。在第二步中,结果将提供给 PHP 脚本。从结果缓冲区到 PHP 变量的最终传输会影响内存消耗,并且在使用缓冲结果集时最为明显。
默认情况下,MySQL 本地驱动程序尝试避免在内存中两次保存缓冲结果。结果只保存在内部结果缓冲区及其 zval 中。当结果由 PHP 脚本获取到 PHP 变量中时,变量将引用内部结果缓冲区。数据库查询结果不会被复制,并且只保存一次。如果用户修改保存数据库结果的变量的内容,则必须执行写时复制以避免更改引用的内部结果缓冲区。缓冲区的内容不能被修改,因为用户可能决定第二次读取结果集。写时复制机制使用额外的引用管理列表和标准 zval 引用计数器实现。如果用户将结果集读入 PHP 变量并在 unset 变量之前释放结果集,也必须执行写时复制。
一般来说,这种模式对于只读取一次结果集并且不修改保存结果的变量的脚本非常有效。其主要缺点是由于额外的引用管理导致的内存开销,这主要是因为保存结果的用户变量在 mysqlnd 引用管理停止引用它们之前无法完全释放。当结果集被释放或执行写时复制时,MySQL 本地驱动程序会删除对用户变量的引用。观察者会看到总内存消耗会增加,直到结果集被释放。使用 统计信息 检查脚本是否显式释放结果集或驱动程序是否隐式释放结果集,从而导致内存使用时间超过必要时间。统计信息还有助于查看发生了多少次写时复制操作。
使用等效于或等同于 while ($row = $res->fetch_assoc()) { ... }
的代码片段的 PHP 脚本读取缓冲结果集中的许多小行,可以通过请求副本而不是引用来优化内存消耗。虽然请求副本意味着在内存中保存两次结果,但它允许 PHP 在释放结果集本身之前,在遍历结果集时释放 $row
中的副本。在负载较高的服务器上,优化峰值内存使用情况可能有助于提高整体系统性能,尽管对于单个脚本来说,由于额外的分配和内存复制操作,副本方法可能更慢。
监控和调试
有几种方法可以跟踪 MySQL 本地驱动程序的内存使用情况。如果目标是获得快速的高级概述或验证 PHP 脚本的内存效率,请检查库收集的 统计信息。例如,统计信息允许您捕获生成的结果比 PHP 脚本处理的结果更多的 SQL 语句。
可以配置 调试 跟踪日志以记录内存管理调用。这有助于查看何时分配或释放内存。但是,请求的内存块的大小可能不会列出。
一些最新的 MySQL 本地驱动程序版本支持模拟随机内存不足情况。此功能仅供库的 C 开发人员或 mysqlnd 插件 作者使用。请在源代码中搜索相应的 PHP 配置设置和更多详细信息。此功能被认为是私有的,并且可能在任何时候更改,恕不另行通知。