嵌入式PHP


作为一门后台开发语言PHP有着不同的发展阶段。最开始是大家都比较熟悉的LAMP,接着是PHP-fpm和fastcgi,再往后是swoole,之后在swoole的基础上又新增了协程。


为了便于理解,在介绍嵌入式PHP之前要先讲下SAPI的概念。SAPI即后台应用程序编程接口,是PHP与其他应用程序交互的接口。常见的SAPI有cgi、fpm、cli、Apace2 hander,而嵌入式PHP(embed)也是其中一类。


业务场景

探索嵌入式PHP与C/C++结合的无限种可能_java


我们最初的业务框架是基于TSF2.0,底层为Zend Engine和扩展,扩展最核心的部分是基于swoole。在此之上是TSF PHP层,包含协程调度器、微服务框架、监控管理进程、MVC模式。最上层才是真正的执行逻辑的PHP脚本。


这样一套框架存在几个问题。PHP原生的Generator协程需要配合yield使用,对开发来说不怎么友好,因为yield的使用时机不太好确定,尤其是对于新手。由于协程调度器是用原生PHP实现的,因此相对其他语言在性能上会差些,特别是在高并发场景下。还有就是低版本swoole不够稳定,问题最多的就是在内存泄露这块。大家都知道腾讯内部有很多公用组件,这些组件接口大多是用C++实现的,为此我们需要做的是用扩展对接PHP与其他组件,而问题就在于扩展无法使用上层的PHP协程。


方案:SPP+PHP


为什么选择嵌入式PHP


SNG中有个非常有名的C++后台框架SPP,它是一个高性能的网络框架,起始于2008年,被广泛的应用于SNG的各个业务线。众所周知开发效率一直是PHP的长处,性能方面则是短板。所以我们就在想能不能将SPP和PHP结合起来兼顾高性能和开发效率,嵌入式PHP无疑是很好的结合方案。



SPP主要有5个模块。proxy用来处理新请求,内存队列是proxy和workgroup交互的内存区域,workgroup是和后台逻辑脚本交互的模块,controller作为控制核心来控制proxy和workgroup的运行状态,最后是最重要的协程模块。


如何将SPP和Zend结合


SPP其实是基于协程的框架,协程是一个用户态的多线程概念。在协程切换的时候会涉及内存管理的机制,而Zend没有这种切换内存资源的机制,只有全局变量和多线程资源隔离的方式。这样的话要想将SPP和Zend结合起来,就要对Zend进行改造。


Zend的源码大概有60万行,如果直接改动核心源码,不光实施起来很麻烦,对之后的升级也会造成问题。最好的办法是借助Zend本身的机制对入口进行改造,而不侵入内核。


Zend改造


Zend有多进程和多线程两种方式,在多线程模式下有一个线程安全的机制ZTS。ZTS本质其实是对每个线程的全局资源进行了隔离,与SPP协程的结合就需要用到ZTS,下面是具体步骤。


第一步当然是打开Zend内核ZTS开关,第二步为了满足协程上下文切换,需要将ZTS中的线程私有变量转化为全局数据元素,第三步增加资源入口切换API。做完这三步就完成了Zend和SPP的结合,虽然步骤不多但实际上在做的过程中还是会有很多挑战。


PHP执行流调度器

探索嵌入式PHP与C/C++结合的无限种可能_java_02


解决了结合问题之后,接下来为了将整个流程串起来需要有一个执行流程调度器。上图是整个执行流程,首先SPP通过SAPI进入到Zend中,然后Zend执行PHP脚本,先编译成OpCode,之后如果有网路IO就会用到协程。协程也可以基于SPP提供的API来运作,通过Tsrm的全局资源table可以进行协程切换。


探索嵌入式PHP与C/C++结合的无限种可能_java_03


在有这样一套执行流的情况下,扩展也可以依赖SPP的API实现协程调度。


探索嵌入式PHP与C/C++结合的无限种可能_java_04


上图是SPP结合PHP之后的整体架构。最底层是SPP,往上是PHP执行流调度器,中间是PHP内核和扩展,最上层是PHP脚本。这一整套架构以嵌入式的方式结合了SPP和PHP,让开发者既可以利用SPP提供的高性能网络框架,同时也能应用到上层PHP快速开发的特点。


探索嵌入式PHP与C/C++结合的无限种可能_java_05


这里展示的是压测时候的环境数据,包括机型、压测工具、压测方法以及框架的版本等。


探索嵌入式PHP与C/C++结合的无限种可能_java_06


这是压测后获得的实际数据对比,可以看到SPP-PHP框架相对旧的框架性能上大概有3倍的提升。