运行环境 ################## 基本环境 ============== 新浪云 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 秒 。 .. note:: 当应用并发超过限制,系统会返回 508 错误,并显示 Connections out of quota。当应用内存占用超过限制,系统会返回 509 错误,并显示 Memory usage out of quota。 环境变量 ============= 您可以通过打印 PHP 的全局变量 `$_SERVER` 来获取跟新浪云相关的环境变量信息,每个环境变量的信息如下: =============== =================================== 变量名 说明 =============== =================================== HTTP_APPNAME 标志该请求属于哪个应用 HTTP_APPVERSION 标志该请求对应该应用的哪个版本 HTTP_ACCESSKEY 该应用访问各种服务资源的帐号 HTTP_SECRETKEY 该应用访问各种服务资源的密码 HTTP_APPCOOKIE 一些和 app 管理相关信息 =============== =================================== .. warning:: 不要直接打印出 `$_SERVER` 变量,这样可能会造成应用的 AccessKey 和 SecretKey 的泄露。为了应用的安全考虑,请保护好自己的 AccessKey 和 SecretKey。 常用字体文件路径: .. php:const:: SAE_Font_Sun 宋体字体文件路径 .. php:const:: SAE_Font_Kai 楷体字体文件路径 .. php:const:: SAE_Font_Hei 文泉驿正黑字体文件路径 .. php:const:: SAE_Font_MicroHei 文泉驿微米黑字体文件路径 全局函数 ============= .. php:function:: is_https() 判断客户端是以 http 还是以 https 的方式连接。 :returns: 如果是 https 连接返回 true,否则返回 false。 本地 IO ============ 考虑到安全和分布式问题,PHP 运行环境对本地的 IO 做了限制: + 应用可以以只读权限访问应用目录以及 PHP 语言的系统库目录。 + 可写 TmpFS + 为最大程度降低应用移植的难度,PHP 为 Storage、Memcached 提供了 wrapper 封装,用户可以像读写文件一样来读写 Memcached 和 Storage。 其中 TmpFS 的路径可以通过 ``SAE_TMP_PATH`` 这个全局变量获取,该路径具有写权限,用户可以往这个目录下写文件。 .. warning:: + 临时文件的生存周期等同于 PHP 请求,也就是当该 PHP 请求完成执行时,所有写入 TmpFS 的临时文件都会被销毁 + TmpFS 是本地临时文件,不是共享存储,而新浪云是全分布式环境,所以不同请求之间无法通过 TmpFS 共享操作文件 + TmpFS 操作的文件限于 SAE_TMP_PATH 目录内,这个目录对每个应用是不一样的 + TmpFS 的文件为纯内存存储 Wrappers ============= .. highlight:: php PHP 自 4.3 版本以来,引入了 stream 流的概念,简单说,就是可以用通用的 IO 读写函数来操作各种资源,比如:tcp、udp、http、ftp 等等,这样做的好处是统一了接口的封装。这就像在 Unix 中将各种设备都抽象成文件,你可以使用 read/write 来操作各种设备,这样统一了操作接口,易于理解和使用。Wrappers 就是用来告诉 stream 流该如何处理(读写)特定的资源。 Wrappers 使用非常简单,比如下面就是一个最常见一个使用 Wrapper 的语句: :: 这里就是使用 `http://` Wrapper 实现抓取远程内容并赋值给一个变量的操作。 由于新浪云的 PHP 运行环境并不提供持久性本地 IO 能力,所以 PHP 运行环境提供了提供了 Memcached,Storage,KVDB 的 Wrappers 来方便开发者迁移原有程序。 如果你的原有程序中,使用了本地文件型缓存,那么你可以方便地使用 `saemc://` 替换本地文件缓存的前缀。 如果你的原有程序中,有文件存储的需求,你原来可能是通过 NFS 或者就是单机提供的存储服务,那么你可以方便地使用 `saestor://` 或 `saekv://` 来替换原来的存储前缀,注意存储的用途是用于文件落地的永久存储,任何缓存、中间临时交换数据的需求都是不适合使用 Storage 和 KVDB 存储的。 :: .. important:: 使用 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 客户端。 ============ ======================================================================================== .. note:: 其中,mysql 系列函数已经在 PHP 5.5 及以上版本中被废弃,建议使用 PHP 5.6 环境的用户使用 mysqli 和 pdo_mysql 进行数据库连接。 .. _Yar: http://www.laruence.com/2012/09/15/2779.html .. _Imagick: http://php.net/manual/zh/book.imagick.php .. _PHP-Redis: https://github.com/phpredis/phpredis .. _php-log-system: 日志系统 ============== 新浪云提供了实时的日志查询功能,方便开发者在线调试分析。 PHP 运行环境中输出的日志类型有: Apache 的访问日志,PHP 的错误日志,可以在日志中心面板中查看。 你可以使用 PHP 的 `error_log `_ 、 `trigger_error `_ 来写日志,更多使用方法请参见 PHP 官方文档: `错误处理和日志记录 `_ 。 .. note:: 目前 PHP 运行环境单个请求最多输出 1000 条日志,超过限制的日志输出会被丢弃。 .. _htaccess: .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 .. warning:: .htaccess 配置文件不能和 config.yaml 配置文件里应用配置(handle 段)一起使用,如果两个一起使用,会导致配置错乱。 .. _php-app-config: 应用配置 ================ .. highlight:: yaml 应用可以通过应用版本目录下的 `config.yaml` 来对 Apache 服务器做一些配置(类似于 Apache 的 htaccess 文件)。 通过配置,开发者可以很方便的实现以下功能: + 目录默认页面 + 自定义错误页面 + 压缩 + 页面重定向 + 页面过期 + 设置响应头的 content-type + 设置页面访问权限 .. note:: 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 .. note:: 通常情况,我们根据响应头 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")。 .. 上面最后一句话的 PHP 脚本写的内容原文档中缺失,我是根据上下文猜测的,复查者请确认一下并删掉这个注释。 开发者可以通过检查是不是输出了响应头: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}" .. warning:: 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 是黑名单。 .. _CIDR: http://zh.wikipedia.org/wiki/%E6%97%A0%E7%B1%BB%E5%88%AB%E5%9F%9F%E9%97%B4%E8%B7%AF%E7%94%B1 例子::: # 禁止 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 存储 .. code-block:: php `_ 访问外网的 HTTP 资源。所有 curl 访问的日志可以在日志中心查看。 - `fsockopen `_ 通过 TCP Socket 访问外网资源。 范例一,使用 curl 库抓取 http://www.sinaapp.com 。 .. code-block:: php 范例三,使用 Socket 与外部网站进行 SSL 连接。 .. code-block:: php .. include:: ../services/fetchurl-ip.rst