这个文章介绍方案过时, 直接参考自定义镜像方案
函数计算目前已经支持了自定义镜像, 感兴趣的同学直接使用镜像体验更流畅,使用 custom-container, 和传统的 php 使用方法一致, 通过 S 工具可以一键部署
前言
这篇文章适合所有的PHP开发新手、老鸟以及想准备学习开发 PHP 的程序猿。众所周知,PHP 是 Web 编程最流行的编程语言,如果有人告诉你,有 Serverless 的 PHP WEB 开发新模式,你是不是会感到好奇和兴奋?在介绍 Serverless Web 开发新模式之前,我们先了解下将 PHP Web Serverless 化的好处:
- 无需采购和管理服务器等基础设施
- 弹性伸缩,动态扩容
- 免运维, 极大降低人力成本
- 按需付费,财务成本低
本文以部署 WordPress 工程在函数计算环境中为例,向您讲解如何使用阿里云函数计算快速构建或移植基于 PHP 框架开发的 Web ,通过本文,您将会了解以下内容:
案例概览
在本教程中,我们讲解如何利用函数计算一步一步来构建 Web 的 Server 端,该案例是把一个 WordPress 部署到函数计算,本文旨在展示函数计算做 Web Backend 能力,具体表现为以下几点:
- 完善的 PHP 系统迁移到 FC 的成本不高
- FC 打通了专有网络 VPC 功能,用户的函数可以配置访问专有网络的云资源,比如本案例中
MYSQL
,NAS
传统服务器架构 VS Serverless架构
正常来说,用户开发 Server 端服务,常常面临开发效率,运维成本高,机器资源弹性伸缩等痛点,而使用 Serverless 架构可以很好的解决上述问题。下面是传统架构和 Serverless 架构的对比:
阿里云函数计算是一个事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传。函数计算会为您准备好计算资源,以弹性、可靠的方式运行您的代码,并提供日志查询,性能监控,报警等功能。借助于函数计算,您可以快速构建任何类型的应用和服务,无需管理和运维。
Serverless 架构详解
从上面的示例图中,整体架构十分简单明了, 用 FC 替代了 Web 服务器,但是换来的是免运维,弹性扩容,按需付费等一系列优点
函数计算运行 PHP 框架原理
传统服务器 PHP 运行原理
- 原理示意图
- A simple nginx conf
从上面原理示意图我们可以看出,Web 服务器根据conf 中 location将 PHP 脚本交给 php-fpm 去解析,然后将解析后的结果返回给 client 端
FC 驱动 PHP 工程原理
- 函数计算的执行环境相当于传统 web 服务的 Apache/Nginx
- 用户函数相当于实现 Apache/Nginx 的 conf 中 location
- 用户将 Web 网站部署在 NAS,然后挂载 NAS 到函数的执行环境, 比如下面代码中
/mnt/www
目录 对于 WordPress 入口函数代码就是这么简单, 建议您先了解下 PHP Runtime
- PHP 入口函数
<?php use RingCentral\Psr7\Response; function startsWith($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); if (startsWith($uri, "/2016-08-15/proxy/share/wp-func/wp-admin/")) { // wordpress admin entrypoint $request = $request->withAttribute("requestURI", $uri); } } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $format = '%s.%s.fc.aliyuncs.com'; $host = sprintf($format, $context['accountId'], $context['region']); // maybe user define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['SERVER_NAME' => $host, 'SERVER_PORT' => '80', 'HTTP_HOST' => $host], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 15000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $filename = rawurldecode($filename); $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } }
其中函数计算为用户提供了一个
$GLOBALS['fcPhpCgiProxy']
对象用来和 php-fpm 进行交互,对
PHP 工程中的 php 文件进行解析,该对象提供了两个重要的接口:requestPhpCgi
requestPhpCgi($request, $docRoot, $phpFile = "index.php", $fastCgiParams = [], $options = [])
$request
: 跟php http invoke
入口的参数一致$docRoot
: Web 工程的根目录$phpFile
: 用于拼接 cgi 参数中的 SCRIPT_FILENAME 的默认参数$fastCgiParams
: 函数计算内部尽量根据$request给您构造default cgi params
, 但是如果您不是想要的,可以使用$fastCgiParams
覆盖一些参数 (reference: [cgi]$options
: array类型,可选参数, debug_show_cgi_params 设为 true ,会打印每次请求 php 解析时候的 cgi 参数, 默认为 false ;readWriteTimeout 设置解析的时间, 默认为 5 秒
案例开发配置步骤
本文编写的时候由于工具链不够完善, 后续章节的操作都是基于控制台,如果您喜欢命令行, 使用 Fun 工具可以大大提高配置部署效率。
准备工作
由于函数运行时的 IP 是不固定的,您需要设置 RDS 允许所有 IP 访问。但是这样会有风险,不建议这样做。在本教程中,我们将创建一个 RDS MYSQL 数据库,并将它置于一个专有网络 VPC 环境内,函数计算支持 VPC 功能,用户可以通过授权的方式安全地访问 VPC 中的资源(同时包含本示例中的 NAS )。
1. 创建 RDS MYSQL 数据库, 配置 VPC , 具体参考通过 VPC 访问 RDS 实例
2. 创建 NAS 挂接点,配置 VPC (注意:这里跟 RDS 采用相同的 VPC), 具体参考函数计算nas使用示例
3. 可选操作,在准备函数的 region 创建日志,用于函数的调试, 具体参考函数计算配置日志服务
创建函数
1. 创建 Service (假设是 share
), 配置准备 vpc config
, nas config
和日志服务,比如案例体验的Service配置如下图:
2. 然后将 WordPress 工程移到上述配置的 NAS 中, www 表示 WordPress 的工程的根目录
|-- index.py
|-- www
index.py代码:
# -*- coding: utf-8 -*-
import logging
import os
file = "/mnt/www/2016-08-15/proxy/share/wp-func"
def mkdir(path):
folder = os.path.exists(path)
if not folder:
os.makedirs(path)
def lsDir():
os.system("ls -ll /mnt/www/2016-08-15/proxy/share/wp-func/")
def handler(event, context):
mkdir(file)
os.system("cp -r /code/www/* /mnt/www/2016-08-15/proxy/share/wp-func/")
print(lsDir())
return 'ok'
基于上述代码创一个函数 move-wp-nas
, 执行函数,将 WordPress 工程包移动到 NAS 的/mnt/www/2016-08-15/proxy/share/wp-func
目录。
- Q1: 为什么创建
/2016-08-15/proxy/share/wp-func
这么奇怪的目录?
A:因为http trigger, 函数访问的格式为下面的url: http://${account_id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(seevice_name}/{function_name}/
,为了保证从一个页面跳转到另外一个页面的时候,能自动带上/2016-08-15/proxy/$(seevice_name}/{function_name}/
,我们需要建立这样目录和设置 cgi 相关参数达到 PHP 框架内部自动跳转正确的目的。
- Q2: 可不可以不用
/2016-08-15/proxy/share/wp-func
这么奇怪的目录?
A:可以,FC Web 设置自定义域名
3. 创建入口函数 wp-func
(对应上面步骤中的 /mnt/www/2016-08-15/proxy/share/wp-func
), 给函数设置 http trigger ,类型为 anonymous
, 类型都选上。
<?php
use RingCentral\Psr7\Response;
function startsWith($haystack, $needle) {
$length = strlen($needle);
return (substr($haystack, 0, $length) === $needle);
}
function handler($request, $context): Response{
$uri = $request->getAttribute("requestURI");
$uriArr = explode("?", $uri);
// default php / or /wp-admin/
if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) {
$uriArr[0] .= "index.php";
$uri = implode($uriArr);
if (startsWith($uri, "/2016-08-15/proxy/share/wp-func/wp-admin/")) {
// wordpress admin entrypoint
$request = $request->withAttribute("requestURI", $uri);
}
}
$proxy = $GLOBALS['fcPhpCgiProxy'];
$root_dir = '/mnt/www';
//php script
if (preg_match('#\.php.*#', $uri)) {
$format = '%s.%s.fc.aliyuncs.com';
$host = sprintf($format, $context['accountId'], $context['region']); // maybe user define domain
$resp = $proxy->requestPhpCgi($request, $root_dir, "index.php",
['SERVER_NAME' => $host, 'SERVER_PORT' => '80', 'HTTP_HOST' => $host],
['debug_show_cgi_params' => false, 'readWriteTimeout' => 15000]
);
return $resp;
} else {
// static files, js, css, jpg ...
$filename = $root_dir . explode("?", $uri)[0];
$filename = rawurldecode($filename);
$handle = fopen($filename, "r");
$contents = fread($handle, filesize($filename));
fclose($handle);
$headers = [
'Content-Type' => $proxy->getMimeType($filename),
'Cache-Control' => "max-age=8640000",
'Accept-Ranges' => 'bytes',
];
return new Response(200, $headers, $contents);
}
}
4. 直接通过 url 访问首页,第一次访问会提示您安装 WordPress, 安装过程中配置之前准备好的数据库、管理员等相关信息, 安装成功后,就可以成功访问首页,登录后台管理 WordPress 网站了。
http://${account_id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(seevice_name}/{function_name}/
for example:
http://1986114430573743.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/share/wp-func/
FC Web 设置自定义域名
- 下载 WordPress, 然后将 WordPress 工程移到上述配置的 NAS 中, www 表示 WordPress 的工程的根目录
|-- index.py
|-- www
index.py代码:
# -*- coding: utf-8 -*-
import logging
import os
file = "/mnt/www/"
def mkdir(path):
folder = os.path.exists(path)
if not folder:
os.makedirs(path)
def lsDir():
os.system("ls -ll /mnt/www/")
def handler(event, context):
mkdir(file)
os.system("cp -r /code/www/* /mnt/www/")
print(lsDir())
return 'ok'
基于上述代码创一个函数 move-wp-nas
, 执行函数,将 WordPress 工程包移动到 NAS 的/mnt/www/
目录。
同时入口函数做下小改动,改成如下代码:
<?php use RingCentral\Psr7\Response; function endsWith($haystack, $needle) { $length = strlen($needle); return $length === 0 || (substr($haystack, -$length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $host = "fcdemo.mofangdegisn.cn"; // your define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['HTTP_HOST' => $host, 'SERVER_NAME' => $host, 'SERVER_PORT' => '80'], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 60000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $filename = rawurldecode($filename); $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } }
- 给函数入口配置自定义域名(操作过程请参考:绑定自定义域名示例), 具体配置假设如下:
注意: 绑定自定义域名之后,不用使用控制台来进行调试,就只能使用浏览器来触发函数,日志服务来进行调试。
总结
函数计算有如下优势:
- 无需采购和管理服务器等基础设施
- 专注业务逻辑的开发
- 提供日志查询、性能监控、报警等功能快速排查故障
- 以事件驱动的方式触发应用响应用户请求
- 毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力
- 按需付费。只需为实际使用的计算资源付费,适合有明显波峰波谷的用户访问场景
除了上面所列的优势,FC 可以做为 Web Backend,只需要编写一个函数实现传统 Web 服务器中的 conf 中的逻辑,就可以将一个完整的 Web 工程迁移到 FC ,从而从传统的 Web 网站运维,监控等繁琐的事务中解放出来。
最后欢迎大家通过扫码加入我们用户群中,搭建过程中有问题或者有其他问题可以在群里提出来。
函数计算官网客户群(11721331)。