运行环境¶
基本环境¶
新浪云 PHP 运行环境目前的 Web 服务器使用的是:
- CentOS-6.x
- Apache-2.2.x
- PHP-5.3.x / PHP-5.6.x
Web 服务器运行在 64 位 Linux 环境下。
Apache 运行在 Prefork 模式下,即每个请求都会对应一个 Apache 进程,请求结束后该进程才能服务于下一个请求。平台通过模块方式扩展了 Apache 和 PHP 的相关功能。
禁用函数和类¶
出于平台安全性考虑,我们禁用了以下函数和类,禁用的标准主要有四点:
- 出于对安全性的考虑
- 出于对资源管理的考虑
- 不常用的 API
- 我们提供更好替代方案的 API
禁用的函数:
- symlink
- link
- exec
- system
- escapeshellcmd
- escapeshellarg
- passthru
- shell_exec
- proc_open
- proc_close
- proc_terminate
- proc_get_status
- proc_nice
- dl
- pclose
- popen
- stream_socket_server
- stream_socket_accept
- stream_socket_pair
- stream_wrapper_restore
- mb_send_mail
- posix_kill
- apache_child_terminate
- apache_lookup_uri
- apache_reset_timeout
- apache_setenv
- virtual
- socket_create
- socket_create_pair
- realpath_cache_get
禁用的类:
- SQLiteDatabase
- SQLiteResult
- SQLiteUnbuffered
- SQLiteException
沙箱¶
代码和数据的隔离:每个应用在运行期间,只能“看”到自己的代码和数据,即 A 应用无法访问 B 应用的代码和数据。注意,这里提到的在 Web 服务器上的数据,往往指一些中间处理过程的临时数据,并非最终落地的数据,比如用户上传照片会临时存储到 TmpFS。
连接数的隔离:我们知道,程序写的不好,很容易导致阻塞,并进一步导致连接数的飙升。单个应用过多占用 Apache 连接数,原因往往是多方面的,应用请求外部资源被阻塞是一个最为常见的因素,另外应用页面过大浏览器下载慢也是常见因素之一。公有云平台同一时刻往往运行着大量的应用,如果某一应用出现连接数异常,最直接的后果是整个平台上的所有应用都将陷入瘫痪。新浪云平台目前有设置“应用最大 HTTP 并发连接数”,目前这个值是 500,如果应用平均单个请求处理时长是 100ms,那么该应用每秒的 HTTP 并发连接将可以到达 5000,每天的请求超过 1 亿没有问题。但如果您的应用平均每个请求处理时长 2 秒,那么该应用每秒的 HTTP 并发连接只能到达 250,每天支撑的请求数将在千万。总体而言,尽量迅速处理完请求对应用是有利的,而且也是平台所鼓励的。
内存隔离:目前新浪云平台上对单个 PHP 脚本的处理,设置了 128MB 的上限 (max_memory,ini_set 不可修改),我们认为这个设置是一个相对很高的值,可以说能够满足绝大部分应用的需求。设想一台服务器 8G 内存,如果每个 PHP 处理都消耗 64M 内存,那么该服务器最多只能同时运行 128 个 PHP 脚本。新浪云引入了”应用最大并发内存数“的概念,目前的设置是 4GB。如果应用程序单个请求的内存消耗平均在 16MB,那么可同时运行 256 个请求,这和上面的并发连接数的设定是基本一致的。
CPU 隔离:这主要是通过新浪云的配额系统来达到 CPU 时间的隔离。每个应用都有 CPU 时间消耗的分钟速度限制,避免了某一应用过多非法获取 CPU 资源导致其它应用响应慢的问题。
目前新浪云平台上允许的“单请求最大存活时长”是 300 秒 。
注解
当应用并发超过限制,系统会返回 508 错误,并显示 Connections out of quota。当应用内存占用超过限制,系统会返回 509 错误,并显示 Memory usage out of quota。
环境变量¶
您可以通过打印 PHP 的全局变量 $_SERVER 来获取跟新浪云相关的环境变量信息,每个环境变量的信息如下:
变量名 | 说明 |
---|---|
HTTP_APPNAME | 标志该请求属于哪个应用 |
HTTP_APPVERSION | 标志该请求对应该应用的哪个版本 |
HTTP_ACCESSKEY | 该应用访问各种服务资源的帐号 |
HTTP_SECRETKEY | 该应用访问各种服务资源的密码 |
HTTP_APPCOOKIE | 一些和 app 管理相关信息 |
警告
不要直接打印出 $_SERVER 变量,这样可能会造成应用的 AccessKey 和 SecretKey 的泄露。为了应用的安全考虑,请保护好自己的 AccessKey 和 SecretKey。
常用字体文件路径:
-
constant
SAE_Font_Sun
¶ 宋体字体文件路径
-
constant
SAE_Font_Kai
¶ 楷体字体文件路径
-
constant
SAE_Font_Hei
¶ 文泉驿正黑字体文件路径
-
constant
SAE_Font_MicroHei
¶ 文泉驿微米黑字体文件路径
本地 IO¶
考虑到安全和分布式问题,PHP 运行环境对本地的 IO 做了限制:
- 应用可以以只读权限访问应用目录以及 PHP 语言的系统库目录。
- 可写 TmpFS
- 为最大程度降低应用移植的难度,PHP 为 Storage、Memcached 提供了 wrapper 封装,用户可以像读写文件一样来读写 Memcached 和 Storage。
其中 TmpFS 的路径可以通过 SAE_TMP_PATH
这个全局变量获取,该路径具有写权限,用户可以往这个目录下写文件。
警告
- 临时文件的生存周期等同于 PHP 请求,也就是当该 PHP 请求完成执行时,所有写入 TmpFS 的临时文件都会被销毁
- TmpFS 是本地临时文件,不是共享存储,而新浪云是全分布式环境,所以不同请求之间无法通过 TmpFS 共享操作文件
- TmpFS 操作的文件限于 SAE_TMP_PATH 目录内,这个目录对每个应用是不一样的
- TmpFS 的文件为纯内存存储
Wrappers¶
PHP 自 4.3 版本以来,引入了 stream 流的概念,简单说,就是可以用通用的 IO 读写函数来操作各种资源,比如:tcp、udp、http、ftp 等等,这样做的好处是统一了接口的封装。这就像在 Unix 中将各种设备都抽象成文件,你可以使用 read/write 来操作各种设备,这样统一了操作接口,易于理解和使用。Wrappers 就是用来告诉 stream 流该如何处理(读写)特定的资源。
Wrappers 使用非常简单,比如下面就是一个最常见一个使用 Wrapper 的语句:
<?php
$c = file_get_contents("http://sae.sina.com.cn");
?>
这里就是使用 http:// Wrapper 实现抓取远程内容并赋值给一个变量的操作。
由于新浪云的 PHP 运行环境并不提供持久性本地 IO 能力,所以 PHP 运行环境提供了提供了 Memcached,Storage,KVDB 的 Wrappers 来方便开发者迁移原有程序。
如果你的原有程序中,使用了本地文件型缓存,那么你可以方便地使用 saemc:// 替换本地文件缓存的前缀。
如果你的原有程序中,有文件存储的需求,你原来可能是通过 NFS 或者就是单机提供的存储服务,那么你可以方便地使用 saestor:// 或 saekv:// 来替换原来的存储前缀,注意存储的用途是用于文件落地的永久存储,任何缓存、中间临时交换数据的需求都是不适合使用 Storage 和 KVDB 存储的。
<?php
# 使用"saekv://"这个 Wrapper 将配置文件 config.php 的内容以"config.php"为 key 保存到 KVDB 中,
# 然后用 include 引用了这个文件
file_put_contents('saekv://config.php','');
include 'saekv://config.php';
?>
重要
使用 Wrappers 请要先初始化相应的服务,上例中 KVDB 服务必须是开启的状态。
核心模块¶
MySQL 相关 | 新浪云提供的 MySQL 驱动是原生的,支持 mysql、mysqli 和 pdo_mysql 三个模块。 |
Session | 新浪云提供了 session cluster,使用标准的 session 相关函数即可。 |
Memcached | 新浪云提供的 memcache 模块,调用 memcache_connect() 时,会忽略传入的地址参数,直接获得连接句柄。Memcache 的 hash 使用的是一致性 hash。 |
GD | 为保证兼容性,我们也提供了原生的 GD 模块,但由于 GD 效率问题,我们并不很鼓励使用。 |
cURL | cURL 目前已经基本做到了基本兼容。注意新浪云的 cURL 是重载了 FetchURL 服务的,所以使用 cURL 本质上会不断消耗带宽资源。 |
XhProf | 为方便开发者调试程序,我们也提供了 XHProf 模块,具体使用见面板的”XHProf”即可。 |
Yar | Yar 是一个轻量级的并行 RPC 框架。 |
Imagick | Imagick 是用 ImageMagic API 来创建和修改图像的 PHP 官方扩展。 |
PHP-Redis | Redis 官方推荐的 PHP Redis 客户端。 |
注解
其中,mysql 系列函数已经在 PHP 5.5 及以上版本中被废弃,建议使用 PHP 5.6 环境的用户使用 mysqli 和 pdo_mysql 进行数据库连接。
日志系统¶
新浪云提供了实时的日志查询功能,方便开发者在线调试分析。
PHP 运行环境中输出的日志类型有: Apache 的访问日志,PHP 的错误日志,可以在日志中心面板中查看。
你可以使用 PHP 的 error_log 、 trigger_error 来写日志,更多使用方法请参见 PHP 官方文档: 错误处理和日志记录 。
注解
目前 PHP 运行环境单个请求最多输出 1000 条日志,超过限制的日志输出会被丢弃。
.htaccess 配置文件¶
新浪云 PHP 运行环境支持 Apache 原生的 htaccess 配置文件格式,你可以直接使用应用根目录下的 .htaccess 文件来配置服务器。目前支持的指令包括:
- SetEnvIf
- SetEnvIfNoCase
- Header
- RequestHeader
- RewriteEngine
- RewriteRule
- RewriteCond
- AddType
- AddEncoding
- DirectoryIndex
- ErrorDocument
- FilterProvider
- FilterChain
- AddDefaultCharset
- Options
- Allow
- Deny
- Order
- Satisfy
- ExpiresActive
- ExpiresByType
- ExpiresDefault
警告
.htaccess 配置文件不能和 config.yaml 配置文件里应用配置(handle 段)一起使用,如果两个一起使用,会导致配置错乱。
应用配置¶
应用可以通过应用版本目录下的 config.yaml 来对 Apache 服务器做一些配置(类似于 Apache 的 htaccess 文件)。
通过配置,开发者可以很方便的实现以下功能:
- 目录默认页面
- 自定义错误页面
- 压缩
- 页面重定向
- 页面过期
- 设置响应头的 content-type
- 设置页面访问权限
注解
PHP 运行环境的 config.yaml 文件不会部署到代码目录中,而只是存在于代码仓库中。
应用配置写在 config.yaml 文件的 handle 下,例如::
name: saetest
version: 1
handle:
- rewrite: if (!-d && !-f) goto "/index.php?%{QUERY_STRING}"
基本语法:
- OPTION: ARG1 ARG2 ...
- OPTION: if (CONDICTIONs) ACTION
其中 OPTION 为配置项,ARG1,ARG2 为参数,CONDITIONs 是一个或者多个 CONDITION,多个 CONDITION 之间使用 &&
隔开。ACTION 是 if 条件满足后执行的动作。
CONDITION 可以是以下任意一种:
- 使用
==
和!=
运算符比较变量和字符串; - 使用
~
(大小写敏感)和~*
(大小写不敏感)运算符匹配变量和正则表达式。正则表达式可以包含匹配组,匹配结果后续可以使用变量 $1..$9 引用(正则匹配使用 PCRE 库,你可以在其主页或者 Wikipedia 找到其语法相关文档); - 使用
>
、>=
、<
、<=
比较变量和数字的大小; - 使用
-f
和!-f
运算符检查文件是否存在; - 使用
-d
和!-d
运算符检查目录是否存在; - 使用
-e
和!-e
运算符检查文件、目录是否存在;
appconfig 支持的变量:
%{REQ:HEADER_NAME}
HTTP 请求头中的字段,如 %{REQ:HTTP_HOST}%{RESP:HEADER_NAME}
HTTP 响应头中的字段,如 %{RESP:CONTENT_ENCODING}%{QUERY_STRING}
查询串,一般是 url 中问号后面的内容%{REQUEST_URI}
请求路径,即用户请求的 url 去掉主机部分和查询串后剩下的部分
目录默认页面¶
当访问 url 没有指定文件时,指定返回的文件。
语法:
- directoryindex: FILE [...]
directoryindex 在 config.yaml 文件中仅有一项
例子:
- directoryindex: aaa.php bbb.html
自定义错误页面¶
语法:
- errordoc: httpcode error_file
httpcode 是诸如 404、302 之类的 http 响应码,error_file 是服务器以 httpcode 响应请求时响应的文件。errordoc 在 config.yaml 中可以配置多项。
例子:
- errordoc: 404 /path/404.html
- errordoc: 403 /path/403.html
压缩¶
语法:
- compress: if (CONDICTIONs) compress
在 compress 中,CONDITIONs 只能有一个 CONDITION。
例子:
- compress: if (%{RESP:Content-Length} >= 10240) compress
- compress: if (%{REQ:Referer} == "gphone") compress
- compress: if (%{REQUEST_URI} ~ "/big/") compress
注解
通常情况,我们根据响应头 Content-length,判断是否需要压缩,例如:if (%{RESP:Content-Length} >= 10240) compress,这个静态页面,如 js,css,html 都是没有问题的。但是对 php 脚本,响应 header 中没有 Content-length 这个头,它使用 Transfer-Encoding: chunked, 这个头表示页面输出用 chunked 编码。此时要实现压缩,可以通过应用配置,同时在 PHP 脚本中输出相应头的方式实现。
例如在应用配置中写 if (%{RESP:Use-Compress} == “1”) compress,在需要压缩的 PHP 脚本中写 header(“Use-Compress: 1”)。
开发者可以通过检查是不是输出了响应头:Content-Encoding: gzip 来判断压缩是否生效。
URL 重写¶
语法:
- rewrite: if (CONDITIONs) goto target_url
在 rewrite 中,CONDITIONs 支持多个 CONDITION。除 HTTP 响应 header(没办法根据响应 header 做重定向)外都可以出现在 rewrite 的 CONDITION 中。
target_url 表示重定向的目标 url,在 target_url 可以用 $N 的形式引用 CONDITION 中以正则匹配到的组。
例子:
# 强制使用 https 访问
- rewrite: if (%{REQ:X-Forwarded-Proto} != "https") goto "https://%{HTTP_HOST}%{REQUEST_URI}"
# 当 url 匹配 urldir/(.*) ,并且 输入 header referer 等于 sina 时,跳转至页面 /usr/$1,$1 表示刚刚匹配的 urldir/(.*) 中的 (.*) 部分。
- rewrite: if (%{REQUEST_URI} ~ "urldir/(.*)" && %{REQ:REFERER} == "sina") goto "/url/$1"
# 当 url 匹配 urldir/(.*),并且请求的是一个目录时,跳转至 /url/$1
- rewrite: if (-d && %{REQUEST_URI} ~ "urldir/(.*)") goto "/url/$1"
# 当 url 匹配 path,并且请求的不是一个文件时,跳转至 /url/query.php
- rewrite: if (!-f && %{REQUEST_URI} ~ "path") goto "/url/query.php"
# 当查询串等于 so,并且 url 以 zhaochou 结尾时,跳转至 /url/$1,$1 表示 query_string 匹配到的部分。
- rewrite: if (%{QUERY_STRING} ~ "^(so)$" && %{REQUEST_URI} ~ "zhaochou$") goto "/url/$1"
# 当查询串不包含 sohu,并且 url 以 zhaochou 结尾时,跳转至 /url/query.php?%{QUERY_STRING},%{QUERY_STRING}表示查询串。
- rewrite: if (%{QUERY_STRING} !~ "sohu" && %{REQUEST_URI} ~ "zhaochou$") goto "/url/query.php?%{QUERY_STRING}"
# 如果 url 既不是文件,也不是目录,跳转至 index.php?%{QUERY_STRING}
- rewrite: if (!-d && !-f) goto "/index.php?%{QUERY_STRING}"
警告
- 如果有形如 %{REQUEST_URI} ~ “^(.*)$”类的请求,一定要加上是否是目录或者文件,防止无穷的 rewrite。
- 在 goto 语句中,虽然某些时候可以不以 / 开头,但是强烈建议以 / 开头。
指定过期时间和头信息¶
语法:
- expire: if (CONDITION) time seconds
- mime: if (CONDITION) type content-type
seconds 是秒数,content-type 是表示文档类型的字符串。
例子:
- expire: if (%{REQ:REFERER} ~ "sina") time 10
# 如果 url 请求文件的扩展名是 pdf2,设置 Content-Type 为 application/pdf
- mime: if (%{REQUEST_URI} ~ "\.pdf2$") type "application/pdf"
- mime: if (%{REQUEST_URI} ~ "\.pdf2$") type "application/pdf"
# 只要请求 header referer 包含字符串 sina,就设置 Content-Type 为 text/plain
- mime: if (%{REQ:REFERER} ~ "sina") type "text/plain"
if 语句支持单个 CONDITION。可以出现在 CONDITION 中的变量参考 Apache Docs ,只支持字符串和正则匹配。
基于主机的访问控制¶
语法:
- hostaccess: if (CONDITION) deny IP
- hostaccess: if (CONDITION) allow IP
if 语句只支持单个 CONDITION。
IP 需要加引号,IP 可以是一个或多个 ip 地址、all(所有 IP 地址)、 CIDR (如 108.192.8.0/24),具体可以参考 Apache 配置,allow 是白名单,deny 是黑名单。
例子::
# 禁止 127.0.0.1 访问 private 目录
- hostaccess: if (%{REQUEST_URI} ~ "/private/") deny "127.0.0.1"
# 只允许 127.0.0.1 访问.conf 结尾的文件
- hostaccess: if (%{REQUEST_URI} ~ "\.conf$") allow "127.0.0.1"
# 禁止 127.0.0.1 的所有访问(这个要慎用)
- hostaccess: deny "127.0.0.1"
# 对 cron 任务保护,防止被外部抓取,我们将 cron 任务放在 cron 目录下 (sae 中 cron 服务执行时,走的是内部网络)
- hostaccess: if (%{REQUEST_URI} ~ "/cron/") allow "10.0.0.0/8" 允许 10 打头的所有 IP
# 对于屏蔽一组 IP 地址,可以写成子网掩码形式,或者将多个 IP 之间加以空格。子网掩码形式如下:
- hostaccess: if (%{REQUEST_URI} ~ "/cron/") deny "108.192.8.0/24" 屏蔽 108.192.8 打头的所有 IP
# 允许 108.134.13.24 和 108.122.122.13 这两个 IP
- hostaccess: allow "108.134.13.24 108.122.122.13"
HTTP 基础认证¶
语法:
- passwdaccess: passwd "USERNAME:PASSWORD..."
- passwdaccess: if (CONDITION) passwd "USERNAME:PASSWORD..."
例子:
# 所有访问都要密码,允许用户 writer 用密码 123zxc 访问
- passwdaccess: passwd "write:123zxc"
# 访问 secret 目录需要密码,允许用户 test 用密码 123qwe 访问,用户 coder 用密码 123asd 访问
- passwdaccess: if (%{REQUEST_URI} ~ "/secret/") passwd "test:123qwe coder:123asd"
# 访问.text 结尾的文件需要密码,允许用户 writer 用密码 123zxc
- passwdaccess: if (%{REQUEST_URI} ~ "\.text$") passwd "writer:123zxc"
# 用户的网站后台程序都放在 admin 目录下,需要对 admin 目录做密码保护
- passwdaccess: if (%{REQUEST_URI} ~ "/admin/") passwd "admin:admin123"
if 语句中只支持单个 CONDITION ,%{REQ:HEADER_NAME}, %{REQUEST_URI}可以出现在 CONDITION 中,只支持字符串和正则匹配。
Session¶
新浪云 PHP 环境默认提供了共享的 Session 存储,用户可以不用任何设置直接使用 PHP 提供的 Session 系列函数进行 Session 相关操作。
若用户需要使用特殊的 Session 存储,如将 Session 存入 MySQL,Memcached,KVDB 等服务中,可以参考: PHP: session_set_save_handler - Manual 实现自定义 Session Handler。 新浪云 提供了 Memcached 存储 Session 的方案,可通过如下代码将 Session 数据存入 Memcached 中。
例子:使用新浪云 Memcached 服务作为 Session 存储
<?php
//PHP 5.3 版本
$handler = new sinacloud\sae\MemcacheSessionHandler();
session_set_save_handler(
array($handler, 'open'),
array($handler, 'close'),
array($handler, 'read'),
array($handler, 'write'),
array($handler, 'destroy'),
array($handler, 'gc')
);
session_start();
...
//PHP 5.6 版本
$handler = new sinacloud\sae\MemcacheSessionHandler();
session_set_save_handler($handler, true);
session_start();
...
外网访问¶
您可以通过以下方式来访问外网:
- php curl 库 访问外网的 HTTP 资源。所有 curl 访问的日志可以在日志中心查看。
- fsockopen 通过 TCP Socket 访问外网资源。
范例一,使用 curl 库抓取 http://www.sinaapp.com 。
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://www.sinanapp.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
范例二,使用 fsockopen 连接 IP 1.2.3.4
的 80
端口。
<?
$fp = fsockopen("tcp://1.2.3.4:80", 13, $errno, $errstr);
if (!$fp) {
echo "ERROR: $errno - $errstr\n";
} else {
fwrite($fp, "\n");
echo fread($fp, 26);
fclose($fp);
}
?>
范例三,使用 Socket 与外部网站进行 SSL 连接。
<?php
$fp = fsockopen("ssl://passport.baidu.com", 443, $errno, $errstr);
if (!$fp) {
echo "ERROR: $errno - $errstr \n";
} else {
$str="GET /?login HTTP/1.1\r\n";
$str.="User-Agent: curl/7.19.6 (i686-pc-linux-gnu) libcurl/7.19.6 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5\r\n";
$str.="Host: passport.baidu.com\r\n";
$str.="Accept: */*\r\n\r\n";
fwrite($fp, $str);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
?>
入口和出口IP¶
注解
外网访问出口 IP 列表:
- 220.181.136.91
- 220.181.136.92
- 220.181.136.93
- 123.125.23.211
- 123.125.23.212
- 123.125.23.213
- 123.125.23.214
- 123.126.57.32
- 123.126.57.35
- 14.116.224.3
- 14.116.224.4
- 14.116.224.7
- 14.116.224.8
- 49.7.40.104
- 49.7.40.107
如果你需要给访问的外部接口添加 IP 访问授权,建议添加以下五个 CIDR 规则:
- 220.181.136.0/24
- 123.125.23.0/24
- 123.126.57.0/24
- 14.116.224.0/24
- 49.7.40.0/24
由于出口IP会变动,建议您使用 域名解析方式 获取出口IP地址:
- dig iplist.sinacloud.com A