运行环境

基本环境

新浪云 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 的相关功能。

禁用函数和类

出于平台安全性考虑,我们禁用了以下函数和类,禁用的标准主要有四点:

  1. 出于对安全性的考虑
  2. 出于对资源管理的考虑
  3. 不常用的 API
  4. 我们提供更好替代方案的 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
  • mail
  • 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

文泉驿微米黑字体文件路径

全局函数

is_https()

判断客户端是以 http 还是以 https 的方式连接。

返回:如果是 https 连接返回 true,否则返回 false。

本地 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_logtrigger_error 来写日志,更多使用方法请参见 PHP 官方文档: 错误处理和日志记录

注解

目前 PHP 运行环境单个请求最多输出 1000 条日志,超过限制的日志输出会被丢弃。

.htaccess 配置文件

新浪云 PHP 运行环境支持 Apache 原生的 htaccess 配置文件格式,你可以直接使用应用根目录下的 .htaccess 文件来配置服务器。目前支持的指令包括:

  • SetEnv
  • 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}"

警告

  1. 如果有形如 %{REQUEST_URI} ~ “^(.*)$”类的请求,一定要加上是否是目录或者文件,防止无穷的 rewrite。
  2. 在 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.480 端口。

<?
$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 列表:

  • 220.181.136.56
  • 220.181.136.57
  • 220.181.129.102
  • 220.181.129.119
  • 220.181.129.89
  • 220.181.129.126
  • 220.181.129.121
  • 220.181.129.92
  • 220.181.129.99
  • 220.181.84.185
  • 220.181.136.120
  • 220.181.136.141
  • 123.125.23.211
  • 123.125.23.212
  • 123.125.23.213
  • 123.125.23.214
  • 61.172.201.27
  • 61.172.201.28
  • 183.60.187.57
  • 183.60.187.58
  • 221.179.190.238
  • 221.179.190.240

如果你需要给访问的外部接口添加 IP 访问授权,建议添加以下七个 CIDR 规则:

  • 220.181.129.0/24
  • 220.181.136.0/24
  • 220.181.84.0/24
  • 123.125.23.0/24
  • 61.172.201.0/24
  • 183.60.187.0/24
  • 221.179.190.0/24

由于出口IP会变动,建议您使用 域名解析方式 获取出口IP地址:

  • dig iplist.sinacloud.com A