Loading... # [GO+PHP, 让全宇宙最好的两种语言合体的神器——RoadRunner](https://blog.p2hp.com/archives/7496) 数据说话 从图中可以看出RoadRunner对比Nginx+FPM,运行效率是有数量级上的提升。 一般PHP服务器 传统CGI协议服务器 客户端访问某个URL地址之后,通过 GET/POST/PUT等方式提交数据,并通过HTTP协议向Web服务器发出请求,服务器端将HTTP请求里描述的信息通过标准输入(stdin)和环境变量(environment variable)传递给新建的CGI进程。处理完成后,进程立即关闭。 Nginx + PHP-FPM模式 现在流行的PHP web程序一般都是运行在Nginx + PHP-FPM模式下的。PHP-FPM就是PHP对FastCGI的实现。 master创建并监听多个worker进程,通过共享内存获取worker的状态,进而通过信号控制worker进程。每一个worker进程就类似一个CGI进程,收到CGI请求后会执行相应的PHP文件,并把请求内容作为PHP进程状态的一部分(_GET, _POST, _SERVER等等)。结束请求后,worker不会立刻结束,而是继续留在worker pool. 这就节省了频繁创建结束子进程的开支。 RoadRunner 为什么 现在很多PHP的企业级框架都要求你加载至少十几个文件,构造多个类并解析一些配置,以便处理简单的用户请求或查询数据库。每个任务完成后,你不得不抛弃这些代码。收到下一个HTTP请求时,PHP-FPM会创建一个新的PHP子进程来处理这个请求,所有的文件都要重新加载一遍,即便文件可以有缓存,所有的代码也要重新运行。 如果我们可以避免对每个请求都重启一次PHP子进程,我们就可以节约很多的资源。 基本原理 RoadRunner可以看作一个升级版的Nginx + PHP-FPM. 它直接把长时运行的PHP进程作为worker, 直接对PHP worker进行监控和维护,每次收到http请求时,就发给php worker来处理。这样,我们就不再需要对每个请求重启一遍PHP了。 一些实现细节 整个项目都是开源的,RoadRunner的代码在这里。 RoaderRunner 通过 Socket/Pipe 上的二进制流完成和PHP子进程之间的通信。为此,他们创建了一个轻量的二进制协议,这个协议的包头长这样。 不同的通信方式,创建worker时就会有所区别。RoadRunner实现了两套factory和relay: 当使用pipe时,work是从pipe_factory.go创建的。PHP方面对应的Class是StreamRelay.php 其中读取包头的部分代码是这样的 $prefixBody = fread($this->in, 17); if ($prefixBody === false) { throw new Exceptions\PrefixException("unable to read prefix from the stream"); } $result = unpack("Cflags/Psize/Jrevs", $prefixBody); if (!is_array($result)) { throw new Exceptions\PrefixException("invalid prefix"); } if ($result['size'] != $result['revs']) { throw new Exceptions\PrefixException("invalid prefix (checksum)"); 当使用socket时,对应的PHP Class是SocketRelay.php, 代码是类似的。只不过PHP读取的部分从stdin/out变成了socket. RoadRunner实现了StaticPool和Worker来对所有的PHP worker进行管理。本质上就是一个进程池。当需要PHP对数据进行处理时,StaticPool的allocateWorker()方法会从进程池的free worker中分配一个进程,把数据发送给这个PHP进程。StaticPool不会在PHP进程返回数据后关闭进程,而是把这个进程放回进程池。如果某个PHP进程意外关闭,staticPool会主动丢弃并在需要的时候创建新的进程。 举个栗子 下面是一个实际应用RoadRunner的例子。我们创建了一个最多拥有4个PHP进程的StaticPool,Golang会把0到9的数字依次传给PHP处理,PHP把数字加1并返回。 Golang部分: //创建RoadRunner server srv := roadrunner.NewServer( &roadrunner.ServerConfig{ Command: "php " + phpScriptName, Relay: "pipes", // 选择用pipe通信 Pool: &roadrunner.Config{ NumWorkers: 4, // worker的数量是4 AllocateTimeout: time.Second, DestroyTimeout: time.Second, }, }) defer srv.Stop() err := srv.Start() if err != nil { panic(err) } for i := 0; i < 10; i++ { // 把i作为payload传给PHP进程 res, err = srv.Exec(&roadrunner.Payload{Body: i}) if err != nil { panic(err) } fmt.Print(res) } PHP部分 <?php use Spiral\Goridge; use Spiral\RoadRunner; // 创建Worker实例,选择用pipe通信 $rr = new RoadRunner\Worker(new Spiral\Goridge\StreamRelay(STDIN, STDOUT)); // 长时运行,阻塞式接收go传来的数据 while ($i = $this->rr->receive($context)) { try { $i = $i + 1; $rr->send($i, (string) $context); } catch (\Throwable $e) { $rr->error((string) $e); } } 为什么要go + php? “有些人仍然坚持认为 PHP 是一种缓慢,笨重的语言,只能用来编写 WordPress 插件。他们甚至可能会说 PHP 有一个限制:一旦你的应用程序变得比较大,你就必须切换到更“成熟”的语言并取代之前的 PHP 代码。” 基本上php脚本也都可以用go来写,那为什么不直接用100%的Go来开发项目呢? 我会认为这种模式还是有一些存在的意义的: 开发效率高。尤其是PHP对html渲染有很好的支持,这又正是Go的痛点。 用工成本的考虑。让大牛用Go写出框架,每个模块再由php分别实现,也许是个好主意。(这也算PHP的优势吗-。-?) PHP作为弱类型的脚本语言,用来实现一些小模块,真的很爽~! 参考文献 https://blog.spiralscout.com/php-was-never-meant-to-die-830de87915ee 最后修改:2023 年 08 月 11 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏