设计文档: PulseFLow PHP性能监控插件

Posted by LB on Fri, Aug 17, 2018

一. 背景描述

随着公司PHP项目体的不断增大,随着不同工程师的功能迭代,如何有效获取PHP项目的执行性能,对于系统整体模块显得异常重要,PulseFlow是一个公司团队内部自研地性能跟踪扩展,它可以在程序员无感知的情况下有效跟踪每一个函数的执行效率,主要分析CPU时间消耗、内存大小消耗,执行次数这三个指标,下面我们将从 PHP生命期 到 组件设计 到 性能优化这三个方面来进行阐述组件。

二 . 插件和 PHP生命期

PHP生命周期通常包括 MI(模块初始化)、RI(请求初始化)、RS(请求终止)、MS(模块终止) 这四个步骤,这四个部分使我们能够进行功能渗透的生命期子过程。在MI阶段会包含INI配置文件的解析,在RI阶段会针对每一个CGI请求进行请求初始化操作,在RS阶段会针对每一个请求的关闭进行相关功能拦截。并不是每一次执行PHP均要经历这四个阶段,在CLI模式下,会完整经历这四个阶段,在PHP-FPM这种类CGI模式下,为了提高请求性能,并不会经历过全部阶段,它会着重经历RI、RS阶段。

了解了PHP的生命期轮廓后,我们绘制了下图阐述大概执行流程所属的生命期阶段。

三. 插件和ZEND引擎

在上述的PHP生命周期阶段内,我们可以在不同的阶段分块插件的功能,其次,我们还需要和ZEND引擎打交道,因为ZEND引擎是真正的执行者,我们目前需要托管他们的 zend_execute_ex 内核函数,这个内核函数就是C语言的函数指针,这个内核函数顾名思义就是PHP的内核执行函数。

为了阐述方便,我们使用一个执行流程图,来看一下插件该如何拦截ZEND引擎的执行流程。

三. 开始造轮子

3.1 插件流程分析

首先,我们需要构建插件的执行流程,及各个部分的信息传送关系,我们目前把插件分为两部分,一部分是PHP扩展,用于在PHP生命周期内来进行性能拦截,这部分信息通常存储于 系统进程堆区 或者 系统进程静态区域,第二部分是后台数据转发程序,它负责从信息通道里读取PHP扩展写入的信息,并转发给相应的下一级程序,相关流程图如下。

3.2 环境选择(系统组件选择)

这一步我们选择相应的环境,或者称之为系统组件选择,我们在选择相应组件时根据插件各个执行周期来进行选择。

3.2.1 PHP引擎环境 (PHP7+)

首先PHP引擎我们选择7.0以上,因为 PHP7 与 PHP5 的内核数据结构差距甚大,目前针对PHP7,后面会移植代码覆盖PHP5版本。

3.2.2 插件语言 (C)

虽然现在编写PHP扩展可以使用Go语言、zephir语言、但是为了和原PHP内核及Linux操作系统进行最好性能交互,我们选择C语言进行研发。

3.2.3 信息队列(System V 消息队列)

在3.1中,我们提及了一个很重要的组件,并用红色进行了标记,PHP扩展和后台信息转发程序 如何 沟通?哪一条路最快?

为了选择这条信息通路,我们做了大量实验,覆盖面积包括TCPunix domain socketzeromqnanomsg共享内存posix 内核消息队列system V 内核消息队列,目前最快的是共享内存,其次是system V 和 posix 内核队列。

共享内存虽然是最快的,但是我们目前针对的模型是 多写、多读,为了不对PHP-FPM 内存 和 对系统内存能够更好更有力管理,在第一版中我们将采用system V内核队列,但是我们也已经开放了 共享内存版本的分支 和 给予epoll模型的 posix 内核队列 代码分支,这两个代码分值中均写好模型代码,在后面阶段将会一步步融入主线版本。

为什么选择 system V 队列? 首先system V内核队列在 Linux Kernel 2.6 版本均为内置系统库,特别是redhat系列:包括centos,性能更为优秀,作为系统内置库,自然拥有更好的内核支持、更好的编译条件。

3.2.3 后台程序(C)

在对后台程序进行设计时,我们仔细分析了不同语言在进行系统调用的性能差距,我们使用Go语言 进行了 相关测试,发现 同样的系统环境下,Go语言对于系统调用的效率严重滞后于C,于是我们使用C语言进行后台程序设计,旨在达到最高性能的内核消息队列消费能力。

3.3 编码优化

在编码期间,我们参考了facebook 的 xhprof 插件 和 tideways 的 XHProf插件,看完相关源码后,我们着重从三个方面进行编码优化:

  1. 调整数据结构存储位置,避免堆区分配,避免内存泄露。

  2. 设计简洁化数据结构,及 简洁化执行流程。

  3. 使用 BKDRHash 字符串哈希算法,提高字符串查询速度。

  4. 拒绝一切 序列化 和 反序列化,提高程序间沟通性能。

3.3.1 数据存储区域选择 、 简化数据结构 及 拒绝序列化

在编写插件的过程中,我们最初采用 PHP-FPM 源码模型,在堆区构造 双数组 数据结构,能够达到时间复杂度为O(n)的查询速度,O(1)的元素定位速度,但是由于分配在堆区,在后期和内核沟通过程中,需要进行大量的拷贝 和 序列化工作,性能损耗巨大,相关数据结构如下图。

这种结构优于 目前 市场上 的 性能监控插件 数据结构,因为 类 和 函数 的 信息是分离的,而不是简单的通过 链表,这样可以达到类 和 函数 o(n)效率,可以保障 o(1)的 类和 函数转换性能。

但是这种强依赖关系,在 进行 数据发送阶段 产生大量的深度拷贝 和 序列化工作, 序列化工作是致命的。

于是 我们设计了第二版数据结构,数据结构如下图

其次,基于此数据结构,可以无需深度拷贝即可和 Linux内核进行沟通,无需序列化和反序列化即可和 C 后端程序进行沟通, 此思路从protobuf 和 easyjson 借鉴并实现。

基于静态数据区域,避免内存泄露问题。

3.3.2 借力 PHP-FPM 模式

新的数据结构,由于全部位于静态资源区,各个PHP-FPM在 MI 阶段加载并初始化,通常一个PHP-FPM只会加载一次MI阶段,所以此次消耗在后面的RI至MS阶段均是不损耗的。可以把资源加载的负担转移。

3.3.3 使用 BKDRHash 字符串哈希算法

我们在数据结构元中 添加了 类名 和 函数名的哈希字段,通过BKDRHash进行字符串哈希计算,利用哈希计算后的值,可以快速 定位数据元素,并优化程序的执行流程,避免多次hash计算,降低hash计算压力。

3.3.4 拒绝序列化 和 反序列化操作

由于我们借助内核消息队列进行消息沟通,所以php扩展 和 后端程序体 数据沟通也会有巨大成本,我们着力解决发送端问题,我们初始化时把数据分配到静态资源区,借助新的数据结构,可以无需深度拷贝即可和 Linux内核进行沟通,无需序列化和反序列化即可和 C 后端程序进行沟通, 此思路从protobuf 和 easyjson 借鉴并实现。

四 . 测试轮子

下面将进行基准测试,不带有任何业务代码的测试 ( i7 - 8核 , 16GB , Ubuntu16.04 , PHP 7.2.8 ,Nginx 1.14.0 )。

总共进行了三轮测试。 吞吐量损耗范围在(1.48% 至 1.78%)

##4.1 第一轮 测试 吞吐量损失 : (17454-17130)/17454 = 1.85 %

4.1.1 无扩展:

4.1.2 有扩展:

##4.2 第二轮 测试 吞吐量损失:(17274-16966)/17274 = 1.78%

4.2.1 无扩展:

4.2.2 有扩展:

##4.3 去除了不必要的函数,缩小so体积 吞吐量损失 (17427-17168)/17427 = 1.48 %

4.3.1 无扩展:

4.3.2 有扩展: