Python共享运行环境

运行环境介绍

新浪云 Python 应用运行于沙箱环境之中,前端负载均衡会根据负载在后端的多个节点中选择一个来处理 HTTP 请求。新浪云 Python 应用支持标准 WSGI 应用。

请求处理

新浪云的负载均衡会使用请求的域名来决定处理的应用。比如 http://your_app_name.applinzi.com 的请求会被路由给名为 your_app_name 的应用来处理。

每个应用可以同时运行多个版本,版本以数字为标示,默认版本为 1。如果需要访问非默认版本,需要在域名的前面加上版本号作为子域名: http://version.your_app_name.applinzi.com

默认 URL 符合以下规则的请求会作为静态文件处理:

  • /static/*
  • /favicon.ico

其他所有请求,都被路由到 /index.wsgi:application,即应用根目录 index.wsgi 文件,名为 applicationcallable,不可修改。

application 使用下列方式创建:

sae.create_wsgi_app(app)

将标准 wsgi 应用封装为适宜在新浪云上运行的应用:

import sae

def app(environ, start_response):
    # Your app
    ...

application = sae.create_wsgi_app(app)

所有请求的最大执行时间为 300s ,超过该时间的请求会被系统强制结束,可能会导致返回 502。

对于每个应用的请求,系统会为其维护一个队列,如果出现请求在一定的时间里得不到处理,服务器会自动给该应用增加 instance。如果某一个 instance 在一段时间里没有任何请求,会被系统回收。

基本环境

Python 运行环境使用的是 Python 2.7.9。

  • 仅支持运行纯 Python 的应用,不能动态加载 C 扩展,即.so,.dll 等格式的模块不能使用
  • 进程,线程操作受限
  • 本地文件系统只读。应用可以读取本应用目录,Python 标准库下的内容,如需读写临时文件建议使用 StringIO 或者 cStringIO 来替代。

说明

可以使用独享的python环境规避以上的限制。

Python 默认的模块搜索路径为:当前目录 > 系统目录。添加模块搜索目录的方法为:

import sys
sys.path.insert(0, your_custom_module_path)

请注意

Python 当前目录下的子目录只有包含__init__.py 才会被 Python 认为是一个 package,才可以直接 import。

用户可以上传和使用 .pyc 文件,注意 .pyc 文件必须是 python2.7.9 生成的,否则无效。

Python 运行环境设置了一些自定义的环境变量,这些环境变量可以通过 os.environ 这个 dict 获取。

  • APP_NAME:应用名
  • APP_VERSION: 当前应用使用的版本号
  • SERVER_SOFTWARE: 当前 server 的版本(目前为 direwolf/0.1)。 可以使用这个环境变量来区分本地开发环境还是在线环境,本地开发环境未设置这个值。

创建应用

  1. 登录云应用管理系统,点击“+创建应用”;
  2. 开发语言选择Python,运行环境选择“共享环境”,选择一个代码的管理方式,即可快速创建一个Python的共享环境。
如何创建一个共享环境的Python应用

Hello, world!

以一个简单的 hello world 应用介绍一下一个 Python 应用在新浪云上的创建和部署过程。

准备代码

以应用名为helloworld例,下面部分代码中需要修改这个应用名为你创建的应用名。 目录下准备两个文件:

在目录下创建应用配置文件 config.yaml ,内容如下:

name: helloworld
version: 1
  • name表示应用的名称,请换为你创建时指定的应用名;
  • version 表示应用的版本,请换位你创建的版本。

创建应用的代码入口文件 index.wsgi ,内容如下:

import sae

def app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello, world!']

application = sae.create_wsgi_app(app)

新浪云上的 Python 应用的入口为 index.wsgi:application ,也就是 index.wsgi 这个文件中名为 applicationcallable object。在 helloworld 应用中,该 application 为一个 wsgi callable object

上传代码

访问网站

通过浏览器访问应用可以看到输出的hello world:

浏览器访问应用

使用Web开发框架

说明

以下所有的示例代码的完整版本都可以在我们的 Github repo中获得。

Django

目前新浪云 Python 运行环境中预置了多个版本的 Django,默认的版本为 1.2.7,在本示例中我们使用 1.4 版本。

创建一个 Django project:mysite。

jaime@westeros:~/pythondemo$ django-admin.py startproject mysite
jaime@westeros:~/pythondemo$ ls mysite
manage.py  mysite/

创建应用配置文件 config.yaml ,在其中添加如下内容指定使用的Django版本:

libraries:
- name: "django"
  version: "1.4"

创建文件 index.wsgi,内容如下:

import sae
from mysite import wsgi

application = sae.create_wsgi_app(wsgi.application)

最终的目录结构如下:

jaime@westeros:~/pythondemo$ ls
config.yaml index.wsgi manage.py mysite/
jaime@westeros:~/pythondemo/1$ ls mysite
__init__.py settings.py  urls.py  views.py

部署代码,访问 http://<your-application-name>.applinzi.com ,就可看到 Django 的欢迎页面。

示例代码下载:

以下说明一些在使用Django的过程中会碰到的问题。

处理用户上传文件

在 setttings.py 中添加以下配置:

# 修改上传时文件在内存中可以存放的最大 size 为 10m
FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760

# 新浪云的本地文件系统是只读的,修改 django 的 file storage backend 为 Storage
DEFAULT_FILE_STORAGE = 'sae.ext.django.storage.backend.Storage'
# 使用 media 这个 bucket
STORAGE_BUCKET_NAME = 'media'
# ref: https://docs.djangoproject.com/en/dev/topics/files/

说明

需要开启Storage服务,并创建一个名称为media的bucket,Storage的操作请参考Storage服务说明

发送邮件

settings.py 中添加以下配置,即可使用新浪云的mail服务来处理 django 的邮件发送:

ADMINS = (
    ('administrator', 'administrator@gmail.com'),
)

# ref: https://docs.djangoproject.com/en/dev/ref/settings/#email
EMAIL_BACKEND = 'sae.ext.django.mail.backend.EmailBackend'
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'sender@gmail.com'
EMAIL_HOST_PASSWORD = 'password'
EMAIL_USE_TLS = True
SERVER_EMAIL = DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

说明

需要开启Mail服务,Mail操作请参考邮件服务说明

数据库的主从读写

参见 Django 官方文档 Multiple databases

如何 syncdb 到线上数据库

在本地开发环境中,如下配置数据库,即可执行 python manage.py syncdb 直接 syncdb 到线上数据库。

# 线上数据库的配置
MYSQL_HOST = 'w.rdc.sae.sina.com.cn'
MYSQL_PORT = '3307'
MYSQL_USER = 'ACCESSKEY'
MYSQL_PASS = 'SECRETKEY'
MYSQL_DB   = 'app_APP_NAME'

from sae._restful_mysql import monkey
monkey.patch()

DATABASES = {
    'default': {
        'ENGINE':   'django.db.backends.mysql',
        'NAME':     MYSQL_DB,
        'USER':     MYSQL_USER,
        'PASSWORD': MYSQL_PASS,
        'HOST':     MYSQL_HOST,
        'PORT':     MYSQL_PORT,
    }
}

如何 serve admin app 的静态文件

方法一:

修改 settings.py 中的 STATIC_ROOT 为应用目录下 static 子目录的绝对路径。

运行 python manage.py collectstatic 将静态文件收集到应用的 static 子目录下。

修改 config.yaml ,添加对 static 文件夹下的静态文件的 handlers

handlers:
- url: /static
  static_dir: path/to/mysite/static

方法二:

在开发调试(settings.py 中 DEBUG=True)过程中,可以将 staticfiles_urlpatterns 加到你的 URLConf,让 Django 来处理 admin app 的静态文件:

# urls.py
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    #...

    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
)

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()

由于新浪云默认 static 为静态文件目录,需要修改 config.yaml,添加任意一条规则覆盖默认行为:

handlers:
- url: /foo
  static_dir: foo

可以参考Django的文档:

Flask

以下示例演示如何使用Flask框架,在代码目录下放置index.wsgi和myapp.py文件。

index.wsgi

import sae
from myapp import app

application = sae.create_wsgi_app(app)

myapp.py

import MySQLdb
from flask import Flask, g, request

app = Flask(__name__)
app.debug = True

from sae.const import (MYSQL_HOST, MYSQL_HOST_S,
    MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_DB
)

@app.before_request
def before_request():
    g.db = MySQLdb.connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASS,
                           MYSQL_DB, port=int(MYSQL_PORT))

@app.teardown_request
def teardown_request(exception):
    if hasattr(g, 'db'): g.db.close()

@app.route('/')
def hello():
    return "Hello, world! - Flask"

@app.route('/demo', methods=['GET', 'POST'])
def greeting():
    html = ''

    if request.method == 'POST':
        c = g.db.cursor()
        c.execute("insert into demo(text) values(%s)", (request.form['text']))

    html += """
    <form action="" method="post">
        <div><textarea cols="40" name="text"></textarea></div>
        <div><input type="submit" /></div>
    </form>
    """
    c = g.db.cursor()
    c.execute('select * from demo')
    msgs = list(c.fetchall())
    msgs.reverse()
    for row in msgs:
        html +=  '<p>' + row[-1] + '</p>'

    return html

Bottle

以下示例演示如何使用Bottle框架,在代码目录下放置index.wsgi文件。

from bottle import Bottle, run

import sae

app = Bottle()

@app.route('/')
def hello():
    return "Hello, world! - Bottle"

application = sae.create_wsgi_app(app)

web.py

以下示例演示如何使用web.py框架,在代码目录下放置index.wsgi文件。

import os

import sae
import web

urls = (
    '/', 'Hello'
)

app_root = os.path.dirname(__file__)
templates_root = os.path.join(app_root, 'templates')
render = web.template.render(templates_root)

class Hello:
    def GET(self):
        return render.hello()

app = web.application(urls, globals()).wsgifunc()

application = sae.create_wsgi_app(app)

Tornado

以下示例演示如何使用Tornado框架,在代码目录下放置index.wsgi文件。

import tornado.wsgi
import sae

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world! - Tornado")

app = tornado.wsgi.WSGIApplication([
    (r"/", MainHandler),
])

application = sae.create_wsgi_app(app)

外网访问

直接使用 urllib, urllib2、httplib 或者 socket 模块访问网络资源即可。

出口IP

请参考出口和入口IP

日志系统

调试日志

打印到 stdout 和 stderr 的内容会记录到应用的日志中心中,所以直接使用 print 语句或者 logging 模块来记录应用的日志即可。

日志内容在 『应用管理中心 / 日志及监控 / 日志中心/ HTTP / 调试日志』 中查看:

python调试日志

静态文件日志

静态文件的日志单独记录,从『应用管理中心 / 日志及监控 / 日志中心/ HTTP / Alert日志』分类可以查询:

python静态文件访问日志

请注意

logging 默认设置的 level 是 WARNING,也就是 level >= WARNING 的消息才会被输出。

应用缓存

新浪云 Python 运行环境会对应用导入的模块(包括 index.wsgi)进行缓存,从而缩短请求响应时间,对于缓存了的应用,请求处理只是取出 index.wsgi 中 application 这个 callable 并调用。

应用配置

应用程序的配置文件为应用目录下的 config.yaml 文件。

使用预装模块

如何使用

Python 环境中已经预装了很多的第三方模块,可以直接使用:

  • 对于存在默认版本的第三方模块,如果没有在 config.yaml 中配置,将使用对应模块的默认版本
  • 对于没有默认版本的模块,在应用的 config.yaml 文件中添加 libraries 段,指定你需要用的预装模块以及其对应的版本。 name 为模块的名称, version 为需要使用的版本,这两个字段为必填字段。

例如:

libraries:
- name: django
  version: "1.4"
- name: numpy
  version: "1.6.1"

当前预装的第三方模块列表

名称 支持的版本列表 默认版本
django 1.2.7, 1.4, 1.5, 1.8.3 1.2.7
flask 0.7.2 0.7.2
flask-sqlalchemy 0.15 0.15
werkzeug 0.7.1 0.7.1
jinja2 2.6 2.6
tornado 2.1.1, 2.4.1, 3.1.1 2.1.1
bottle 0.9.6 0.9.6
sqlalchemy 0.7.10, 0.9.7 0.7.10
webpy 0.36 0.36
PIL 1.1.7 1.1.7
MySQLdb 1.2.3 1.2.3
numpy 1.6.1
lxml 2.3.4
PyYAML 3.10 3.10
misaka 1.0.2
matplotlib 1.1.1
PyCrypto 2.6
py-bcrypt 0.2
greenlet 0.4.0 0.4.0
gevent 1.0rc2 1.0rc2
markupsafe 0.15
bitarray 0.8.0

请注意

默认版本为“无”的模块必须在config.yaml中手工指定才可以使用。

添加第三方依赖包

除了使用新浪云上已经预装的模块之外,您还可以通过以下方式给自己的应用添加第三方依赖包。

如何添加

  1. 首先,在应用的根目录下创建一个第三方依赖包目录 vendor
mkdir vendor
  1. 调用 pip 命令安装依赖包,使用其 -t 选项指定第三方包的安装目录。
$ pip install -t vendor PACKAGE ...
  1. 将 vendor 目录和应用的代码一起提交,即可在应用代码里使用安装的第三方依赖包了。

关于PIP版本

需要 pip 6.0.0 或者更高版本

如果依赖包安装的目录名不为 vendor ,你需要在 index.wsgi 文件的最开始,添加以下代码,将目录加入到 sys.path 中。

import sae
sae.add_vendor_dir('路径')
# 注意:以上代码得放在 index.wsgi 的最前面,所有其它代码之前。

函数的原型为:

sae.add_vendor_dir(dir)
  • site目录或者virtualenv目录加入到sys.path
  • 参数dir:site目录或者virtualenv目录的相对路径(相对于应用的根目录)

TIP

  • 部分第三方库已经包含在默认搜索路径中,可以不在 config.yaml 中指定直接使用;
  • 仅为兼容性考虑保留,不推荐使用,请在 config.yaml 明确配置。

静态文件处理

在应用目录下配置config.yaml如下的文件内容:

handlers:
- url: /robots.txt
  static_path: robots.txt
- url: /favicon.ico
  static_path: favicon.ico
- url: /static/
  static_path: static

上述的配置表示:

  • http://xxxx.applinzi.com/robots.txt在根目录下找robots.txt文件
  • http://xxxx.applinzi.com/favicon.ico在根目录下找favicon.ico文件
  • http://xxxx.applinzi.com/static/xxxx.txt去根目录下的static目录下找xxxx.txt文件

url 为 URL 的前缀,static_path 为静态文件所在的目录(相对于应用目录)。

当请求的url为目录时,服务器会首先尝试static_path 下的 index.html,当 index.html文件存在时,返回index.html的内容,否则返回404页面。

TIP

  • 如果 config.yaml 中没有设置静态文件相关的 handlers,系统会默认将 /static 为前缀 的 URL 转发到应用目录下的 static 目录;
  • 仅为兼容性考虑保留,不推荐使用,请在 config.yaml 明确配置。

gzip 压缩

可以从config.yaml文件中配置压缩选项,例如:

handlers:
- url: /static/
  gzip: on
- url: /a-big-file.txt
  gzip: on

表示所有请求/static/下的文件或者/a-big-file.txt都开启gzip压缩。

url 为 URL 的前缀。

请注意

注意,当客户端支持 gzip(HTTP 请求的 Accept-Encoding 中包含 gzip)时,服务端才会开启压缩。

本地开发环境

本地开发环境仅为应用开发便利之用,对新浪云 Python 环境的模拟并不完全。

安装

直接使用 pip 或者 easy_install 安装 sae-python-dev 包即可。

或者可以选择从 github 下载源码安装。

$ git clone https://github.com/sinacloud/sae-python-dev-guide.git
$ cd sae-python-dev-guide/dev_server
$ python setup.py install

基本使用

进入应用的本地开发目录,也就是 index.wsgi 和 config.yaml 所在的目录。运行如下的命令启动测试 server:

$ dev_server.py
MySQL config not found: app.py
Start development server on http://localhost:8080/

访问 http://localhost:8080 端口就可以访问你的应用了。

使用 MySQL 服务

首先配置好 MySQL 本地开发 server。然后使用 –mysql 参数运行 dev_server.py。

$ dev_server.py --mysql=user:password@host:port

现在你可以在应用代码中像在新浪云线上环境一样使用 MySQL 服务了。 dev_server.py 默认使用名为 app_应用名 的数据库

使用 Storage 服务

使用 –storage-path 参数运行 dev_server.py。

$ dev_server.py --storage-path=/path/to/local/storage/data

本地的 Storage 服务使用以下的目录结构来模拟线上的 Storage。

storage-path/
      domain1/
            key1
            key2
      domain2/
      domain3/

–storage-path 配置的路径下每个子文件夹会映射为 Storage 中的一个 domain, 而每个子文件夹下的文件映射为 domain 下的一个 key,其内容为对应 key 的数据。

说明

为方便调试,dev_server 自带的 sae.storage 在某个 domain 不存在的情况下会自动创建该 domain。 线上环境中的 domain 需要在新浪云后台面板中手动创建。

使用 pylibmc

dev_server 自带了一个 dummy pylibmc,所以无须安装 pylibmc 就可以直接使用 Memcached 服务了。 该模块将所有的数据存贮在内存中,dev_server.py 进程结束时,所有的数据都会丢失。

使用 KVDB

KVDB 默认数据存在内存中,dev_server.py 进程结束时,数据会全部丢失,如果需要保存数据, 请使用如下命令行启动 dev_server.py。

$ dev_server.py --kvdb-file=/path/to/kvdb/local/file

可用插件

新浪云 Python Shell

新浪云 Python Shell 是一个 wsgi 中间件,提供了一个在线的 interactive shell,便于在线调试 app,查看系统信息等。(由 shellpy 修改而来)。

原型:

class sae.ext.shell.ShellMiddleware(app, password=None)
  • app:你的应用 callable
  • password: 可选,登录 shell 时需要输入的口令,用于保护 shell 不被非法访问。

使用步骤

  1. 开启Memcached服务;
  2. 修改 index.wsgi,启用 shell 插件,示例如下:
import sae
from sae.ext.shell import ShellMiddleware

def app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello, world!']

application = sae.create_wsgi_app(ShellMiddleware(app))

访问地址 https://<your-app-name>.applinzi.com/_sae/shell ,根据提示输入你设置的口令即可。

说明

以上示例代码没有设置口令,直接访问即可,上线后请务必删除,以防网站的安全问题。

示例

Python在线shell

常见问题

如何调试

复杂程序建议您本地调试成功后,再上传运行。

新浪云 Python 版本为 2.7.3,本地调试时注意不要使用高于此版本的Python。

如果你使用内置的第三方库,本地调试时最好使用同样的版本。

对于50X页面,如果异常被web框架捕获,则需要打开web框架的调试开关,查看详细的异常信息。如果异常被Python Server捕获,Python Server会直接在浏览器中打印出异常。

说明

在header已经发出的情况下,异常在浏览器中可能显示不出来,请查看日志。

没有我要使用的包怎么办

MySQL gone away问题

共享型MySQL连接超时时间为30s,不是默认的8小时,所以你需要在代码中检查是否超时,是否需要重连。

对于使用sqlalchemy的用户,需要在请求处理结束时调用 db.session.close() ,关闭当前session,将mysql连接还给连接池,并且将连接池的连接recyle时间设的小一点(推荐为60s)。

MySQL InterfaceError: (-1, ‘error totally whack(xxx)’)

这个错误表示mysql返回的错误码是新浪云自定义的错误码,其中totally whack后面括号中的数字是具体的错误码。

你可以在共享MySQL文档中查询到相关错误码具体代表的信息。

如何区分本地开发环境和线上环境

可以通过判断环境变量中是否有SERVER_SOFTWARE判断是否在云端环境,如果没有说明在本地开发环境。

if 'SERVER_SOFTWARE' in os.environ:
    # SAE
else:
    # Local

如何使用virtualenv管理依赖关系

当你的应用依赖很多第三方包时,可以使用virtualenv来管理并导出这些依赖包,流程如下:

首先,创建一个全新的Python虚拟环境目录ENV,启动虚拟环境。

$ virtualenv --no-site-packages ENV
$ source ENV/bin/activate
(ENV)$

可以看到命令行提示符的前面多了一个(ENV)的前缀,现在我们已经在一个全新的虚拟环境中了。

使用pip安装应用所依赖的包并导出依赖关系到requirements.txt。

(ENV)$ pip install Flask Flask-Cache Flask-SQLAlchemy
(ENV)$ pip freeze > requirements.txt

编辑requirements.txt文件,删除一些新浪云内置的模块,eg. flask, jinja2, wtforms。

使用dev_server/bundle_local.py工具,将所有requirements.txt中列出的包导出到本地目录virtualenv.bundle目录中。如果文件比较多的话,推荐压缩后再上传。

(ENV)$ bundle_local.py -r requirements.txt
(ENV)$ cd virtualenv.bundle/
(ENV)$ zip -r ../virtualenv.bundle.zip .

virutalenv.bundle目录或者virtualenv.bundle.zip拷贝到应用的目录下。

修改index.wsgi文件,在导入其它模块之前,将virtualenv.bundle目录或者virtualenv.bundle.zip添加到module的搜索路径中,示例代码如下:

import os
import sys

app_root = os.path.dirname(__file__)

# 两者取其一
sys.path.insert(0, os.path.join(app_root, 'virtualenv.bundle'))
sys.path.insert(0, os.path.join(app_root, 'virtualenv.bundle.zip'))

到此,所有的依赖包已经导出并加入到应用的目录里了。

更多virtualenv的使用可以参考其官方文档

请注意

  • 请删除requirements.txt中的wsgiref==0.1.2这个依赖关系,否则可能导致 bundle_local.py导出依赖包失败。
  • 有些包是not-zip-safe的,可能不工作,有待验证。 含有c扩展的package 不能工作。

Matplotlib使用常见问题

Matplotlib使用了numpy,所以需要在config.yaml文件里将numpymatplotlib添加到libraries里(对应的版本请参看:使用预装模块)。否则会导致matplotlib import失败。

从config.yaml中配置:

libraries:
- name: numpy
  version: "1.6.1"
- name: matplotlib
  version: "1.1.1"

新浪云环境不支持matplotlib的interative模式,所以无法使用 pyplot.show() 直接来显示图像,只能使用 pyplot.savefig() 将图像保存到一个输出流中(比如一个cStringIO.StringIO的实例中)。

如果想要在matplotlib中显示中文,可以使用以下任一方法。

方法一

import os.path
from matplotlib.font_manager import FontProperties
zh_font = FontProperties(fname=os.path.abspath('wqy-microhei.ttf'))
import matplotlib.pyplot as plt
plt.title(u'中文', fontproperties=zh_font)

方法二

import os
# 设置自定义字体文件所在目录路径,多条路径之间使用分号(:)隔开
os.environ['TTFPATH'] = os.getcwd()
import matplotlib
# 设置默认字体名
matplotlib.rcParams['font.family'] = 'WenQuanYi Micro Hei'
import matplotlib.pyplot as plt
plt.title(u'中文')

其中方法一适用于ttf和ttc字体,方法二适用于只适用于ttf字体

如果有 matplotlibrc 配置文件,请将该文件与index.wsgi放在同一个目录下(默认的当前路径)。

设置基于主机的访问控制

Python运行环境目前无法通过config.yaml来配置基于主机的访问控制,用户如果需要这个设置,可以通过wsgi middleware来完成,示例代码如下:

def filter_middleware(app):
    def _(environ, start_response):
        remote_addr = environ.get('REMOTE_ADDR')

        # 判断remote_addr是否在允许访问范围内
        # ...

        if ok:
            return app(environ, start_response)

        start_response('401 Unauthorized', [])
        return ["<b>401 Unauthorized</b>",]
    return _

# 给application加上访问控制的中间件
application = filter_middleware(application)