Thursday, September 6, 2012

FormEncode在SAE上的一点小改动


虽然FormEncode现在貌似用的人不多了,不过它的validate在数据校验、转换上还是很犀利的。如果用的好的话,还是可以大大减轻冗杂的代码量的。不过因为其中部分代码涉及到了一些IO操作,而SAE显然在这方面做了很多改动。因此要在SAE上使用它,就需要对它做点小小改动。主要变更代码如下:
api.py文件中有一个get_localedir函数,在系统载入时就会运行。该函数调用了resource_filename,而这个方法会去查找pwd模块(该模块SAE取消掉了)。
def get_localedir():
    """
    Retrieve the location of locales.

    If we're built as an egg, we need to find the resource within the egg.
    Otherwise, we need to look for the locales on the filesystem or in the
    system message catalog.
    """
    locale_dir = ''
    # Check the egg first
    if resource_filename is not None:
        return os.path.join(os.path.dirname(__file__), 'i18n')
        #try:
        #    locale_dir = resource_filename(__name__, "/i18n")
        #except NotImplementedError:
        #   # resource_filename doesn't work with non-egg zip files
        #    pass
    if not hasattr(os, 'access'):
        # This happens on Google App Engine
        return os.path.join(os.path.dirname(__file__), 'i18n')
    if os.access(locale_dir, os.R_OK | os.X_OK):
        # If the resource is present in the egg, use it
        return locale_dir

    # Otherwise, search the filesystem
    locale_dir = os.path.join(os.path.dirname(__file__), 'i18n')
    if not os.access(locale_dir, os.R_OK | os.X_OK):
        # Fallback on the system catalog
        locale_dir = os.path.normpath('/usr/share/locale')

    return locale_dir

大家注意一下就可以发现,这段代码里面还有个Google App Engine的代码实现。不过现在还没想到有什么好的方法来判断是否运行于SAE之上,因此,最简单的方式就是直接在行数开始处return os.path.join(os.path.dirname(__file__), 'i18n'),先当作SAE上的权宜之计,以后想到了合适的判断方式再来变更吧。

Monday, July 23, 2012

Pyramid 中SQLAlchemy 对象的JSON序列化


pyramid github中master 版本增加了custom objects的JSON支持,而在之前(1.3及以前)的版本中SQLAlchemy model对象的序列化需要编写JSONEncoder的子类,然后在dumps的时候指定。因此,想在程序中直接使用json这个renderer输出view结果是一件麻烦的事。

为了在pyramid程序中增加JSON支持,只需要在model类里面增加__json__方法即可。如

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
def sqlalchemy_json(self, request):
    obj_dict = self.__dict__
    return dict((key, obj_dict[key]) for key in obj_dict if not key.startswith("_"))
Base.__json__ = sqlalchemy_json

之后继承自Base的model类即可直接json renderer中输出了。(本例中暂没测试relation)如

@view_config(route_name='home', renderer='json')
def home(request):
    one = DBSession.query(MyModel).filter(MyModel.name=='one').first()
    return {'one':one, 'project':'MyProject'}

为了支持更多第三方类的序列化,pyramid还提供了adapter的功能,如在SQLAlchemy中常用datetime数据类型,这个数据类型在序列化时也会报错,则需要增加一个adapter,如:

def datetime_adapter(obj, request):
    return obj.strftime('%Y-%m-%d %H:%M:%S')

custom_json_renderer_factory.add_adapter(datetime.datetime, datetime_adapter)

并调用config.add_renderer将 custom_json_renderer_factory注册即可。

随便附上单独提取出来的代码,直接放入项目即可在1.3版本的pyramid上使用。


import json
import datetime

from zope.interface import providedBy, Interface
from zope.interface.registry import Components

class IJSONAdapter(Interface):
    """
    Marker interface for objects that can convert an arbitrary object
    into a JSON-serializable primitive.
    """
_marker = object()

class JSON(object):
    """ Renderer that returns a JSON-encoded string.

    Configure a custom JSON renderer using the
    :meth:`~pyramid.config.Configurator.add_renderer` API at application
    startup time:

    .. code-block:: python

       from pyramid.config import Configurator

       config = Configurator()
       config.add_renderer('myjson', JSON(indent=4))

    Once this renderer is registered as above, you can use
    ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or
    :meth:`~pyramid.config.Configurator.add_view``:

    .. code-block:: python

       from pyramid.view import view_config

       @view_config(renderer='myjson')
       def myview(request):
           return {'greeting':'Hello world'}

    Custom objects can be serialized using the renderer by either
    implementing the ``__json__`` magic method, or by registering
    adapters with the renderer.  See
    :ref:`json_serializing_custom_objects` for more information.

    The default serializer uses ``json.JSONEncoder``. A different
    serializer can be specified via the ``serializer`` argument.
    Custom serializers should accept the object, a callback
    ``default``, and any extra ``kw`` keyword argments passed during
    renderer construction.

    .. note::

       This feature is new in Pyramid 1.4. Prior to 1.4 there was
       no public API for supplying options to the underlying
       serializer without defining a custom renderer.
    """

    def __init__(self, serializer=json.dumps, adapters=(), **kw):
        """ Any keyword arguments will be passed to the ``serializer``
        function."""
        self.serializer = serializer
        self.kw = kw
        self.components = Components()
        for type, adapter in adapters:
            self.add_adapter(type, adapter)

    def add_adapter(self, type_or_iface, adapter):
        """ When an object of the type (or interface) ``type_or_iface`` fails
        to automatically encode using the serializer, the renderer will use
        the adapter ``adapter`` to convert it into a JSON-serializable
        object.  The adapter must accept two arguments: the object and the
        currently active request.

        .. code-block:: python

           class Foo(object):
               x = 5

           def foo_adapter(obj, request):
               return obj.x

           renderer = JSON(indent=4)
           renderer.add_adapter(Foo, foo_adapter)

        When you've done this, the JSON renderer will be able to serialize
        instances of the ``Foo`` class when they're encountered in your view
        results."""

        self.components.registerAdapter(adapter, (type_or_iface,),
            IJSONAdapter)

    def __call__(self, info):
        """ Returns a plain JSON-encoded string with content-type
        ``application/json``. The content-type may be overridden by
        setting ``request.response.content_type``."""
        def _render(value, system):
            request = system.get('request')
            if request is not None:
                response = request.response
                ct = response.content_type
                if ct == response.default_content_type:
                    response.content_type = 'application/json'
            default = self._make_default(request)
            return self.serializer(value, default=default, **self.kw)

        return _render

    def _make_default(self, request):
        def default(obj):
            if hasattr(obj, '__json__'):
                return obj.__json__(request)
            obj_iface = providedBy(obj)
            adapters = self.components.adapters
            result = adapters.lookup((obj_iface,), IJSONAdapter,
                default=_marker)
            if result is _marker:
                raise TypeError('%r is not JSON serializable' % (obj,))
            return result(obj, request)
        return default

custom_json_renderer_factory = JSON()

def datetime_adapter(obj, request):
    return obj.strftime('%Y-%m-%d %H:%M:%S')

def date_adapter(obj, request):
    return obj.strftime('%Y-%m-%d')

custom_json_renderer_factory.add_adapter(datetime.datetime, datetime_adapter)
custom_json_renderer_factory.add_adapter(datetime.date, date_adapter)

Thursday, July 5, 2012

python setuptools test command 的一点小问题


为了测试一点小程序,用pcreate -t starter sampleutils 命令生成了一个项目框架。然后在sampleutils package中建了一个tests package,然后在这个package中放置了一个test_basic.py,程序如下:

import unittest

from pyramid import testing

class View2Tests(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()

    def tearDown(self):
        testing.tearDown()

    def test_my_view(self):
        pass

很简单,里面仅有一个testcase。现在就可以用python setup.py test来有运行单元测试了。

运行结果如下



很奇怪吧,竟然运行了两次同一个testcase。

再换nosetests运行看看:


这里明确显示是一个testcase。究竟哪里发生了问题呢?

再来看一下项目结构:


很快,我们就可以发现,pcreate生成的tests.py文件我们并没有删掉,而且这个module的名字跟tests package的名字重复了。这就是造成这次问题的原因。

但一般的,就算多了这个文件,也应该仅仅是忽略它啊,就像nosetests运行结果一样,不会去tests.py中查找任何test case。

为什么在python setup.py test 命令中会出现载入两次test_basic.py中的test case呢?

我们再来看python setup.py test这个命令的运行机制,该命令是setuptools中的一个命令之一,它通过读取setup.py文件中的配置信息,在指定目录中查找所有的testcase,然后运行。

找到setuptools的源码,打开command中的test.py程序,这个程序定义了setuptools中test command运行流程。



上面这段程序就是test case的查找过程。

从 if file.endswith('.py') and file!='__init__.py' 这一段就可以看出,它将tests.py 跟tests 混为一谈了,并且没有检查该模块是否已经载入,从而导致了在扫描tests的时候,载入了一次sampleutils.tests包,在扫描tests.py的时候,再次载入了sampleutils.tests这个包。这样也就很好的解释了在运行python setup.py test 命令的时候运行了两次相同的test case.

虽然是一个错误造成的问题,但也可以看出 setuptools中的代码还是值得推敲的。


Thursday, June 21, 2012

Mac OS Launchpad 上的重复图标


刚刚更新了一下App Store,发现Launchpad上出现了两个重复的图标iMovie,点击启动之后均打开同一个程序,版本什么的都一样,删也删不掉,好讨厌。在一番google之后,终于看到有人提出简单的修改方法,记录如下:

1. 打开终端
2. cd ~/Library/Application\ Support/Dock/
3. 这里面有一个db文件
4. 做个备份
5. 用sqlite3打开它
6. 查找其中的apps表,select * from apps where title='iMovie';
7. 发现果然有重复的,删掉id大的条目
8. killall Dock
9. 正常了

貌似还有个叫launchpad-control的工具可以完成Launchpad的管理,通过http://chaosspace.de/launchpad-control/下载(墙外)。

环境:Mac OS X 10.7.4

Saturday, May 5, 2012

Pyramid 与 Beaker


Beaker为Python程序提供了良好的缓存和session支持。在Pyramid中,也有一个叫pyramid_beaker的小程序提供了Beaker接入支持,只需要在Pyramid应用中include pyramid_beaker即可在程序中使用Beaker。

pyramid_beaker主要提供了一个session factory,可以读取ini配置文件中的配置信息提供session factory服务。同时,pyramid_beaker也能够将配置的cache信息读取到beaker.cache.cache_regions中,以便在程序中使用@cache_region标注。不过这个插件对cache的支持不是很给力,个人感觉应该直接将给request对象植入一个CacheManager或Cache比较好,这样在应用中就可以不必关注是否使用Beaker了。

一、启用Beaker

将development.ini中的pyramid.includes中增加一项pyramid_beaker即可在应用中启用Beaker支持

pyramid.includes =
    pyramid_debugtoolbar
    pyramid_tm
    pyramid_beaker


三、beaker session

1. session的配置

在development.ini中加入如下配置即可在程序中使用Beaker Session。

session.type = file
session.data_dir = %(here)s/data/sessions/data
session.lock_dir = %(here)s/data/sessions/lock
session.key = mykey
session.secret = mysecret
session.cookie_on_exception = true

其中session.type支持cookie, file, dbm, memory, ext:memcached, ext:database, ext:google这几种类型。(注意:cookie有大小限制)
如果使用了ext:memcached, ext:database这两种类型,还会增加一个session.url的配置,如:

beaker.session.key = sso
beaker.session.secret = somesecret
beaker.session.type = ext:memcached
beaker.session.url = 10.72.249.39:11211;10.90.133.122:11211;10.242.117.122:11211
beaker.session.timeout = 10800


2. session的使用

Pyramid通过session factory封装了session的使用,因此配置了pyramid_beaker之后,引用request.session就是在使用Beaker提供的session支持。


四、beaker cache

1. cache的配置

beaker.cache.regions = default_term, second, short_term, long_term
beaker.cache.type = memory
beaker.cache.second.expire = 1
beaker.cache.short_term.expire = 60
beaker.cache.default_term.expire = 300
beaker.cache.long_term.expire = 3600

其中session.type支持file, dbm, memory, ext:memcached, ext:database, ext:google这几种类型。

cache的配置引入了一个region的概念,可以支持多种类、不同层次的cache支持。如:

beaker.cache.data_dir = %(here)s/data/cache/data
beaker.cache.lock_dir = %(here)s/data/cache/lock
beaker.cache.regions = short_term, long_term
beaker.cache.short_term.type = ext:memcached
beaker.cache.short_term.url' = 127.0.0.1.11211
beaker.cache.short_term.expire = 3600
beaker.cache.long_term.type = file
beaker.cache.long_term.expire = 86400


2. cache的使用

在ini文件配置好之后,在程序中,就可以使用@cache_region这个标注来使用cache功能了。如:

from beaker.cache import cache_region

@cache_region('long_term')
def get_photos():
    pass

注意,不要直接在视图上加这个标注,最好将数据存取的地方抽取成函数,如果一定要在视图上做,view_config本身可以提供简单的自带cache功能。

    def view_callable(request):

        @cache_region('long_term')
        def func_to_cache():
            ...
            return something
        return func_to_cache()


3. cache的另类使用

标注虽然很简单,不过有的时候可能会懒得将数据存取独立成函数,这时候,也可以直接使用Beaker的功能直接往cache中put、get数据,如:

import time
from beaker.cache import cache_regions, CacheManager

cm = CacheManager(cache_regions=cache_regions)
cache = cm.get_cache_region("mypyramid", "short_term")

try:
    atime = cache.get("atime")
except Exception, exp:
    atime = str(time.time())
    cache.put("atime", atime, expiretime=60)


注意,这里的put可以单独指定一个expiretime,这会直接覆盖region中定义的过期时间。

Sunday, April 29, 2012

使用egg包方式在SAE上部署Pyramid应用


用源码在SinaAppEngine 上部署Pyramid应用经常会遇到文件过多的问题,因此尝试了一下egg包部署(只是喜欢egg格式而已~~,本文未涉及其他打包部署方式),主要步骤如下:

1. 打包

因为有的时候需要变更一些第三方包中的内容以适应平台的需要,我们需要将应用涉及到的第三方包重新打包(如果没有变更,不需要重新打包)。这个时候我们可以使用pip zip命令来完成这一工作。

不过不知何故,直接使用类似pip zip --no-pyc pyramid-1.3-py2.6.egg打出来的包里面包含了pyramid-1.3-py2.6.egg这个目录名,直接使用这些包会导致import失败。我们直接打开pip包中的zip.py文件,找到其中的zip_package方法,将其中涉及到的module_name的两行做修改,去掉往zip包写目录的功能即可。

对Pyramid涉及到的第三方包逐个运行pip zip命令即可完成打包工作。打包时注意带上--no-pyc参数,将pyc排除在外。

对于一些其中包含c模块的包,其中的包名可能会涉及到一些本地环境,如OS等。比如zope.interface-3.8.0-py2.6-macosx-10.7-intel.egg,可以直接将其名字改为zope.interface-3.8.0-py2.6.egg。

另外,目前SAE使用python 2.6版本,因此最好使用对应的python环境来打包。


2. 配置virtualenv.bundle目录

将所有涉及到的egg包拷贝到virtualenv.bundle中之后,在该目录建立一个easy-install.pth文件,里面包含如下内容:

import sys; sys.__plen = len(sys.path)
./Chameleon-2.8.4-py2.6.egg
./Mako-0.7.0-py2.6.egg
./MarkupSafe-0.15-py2.6.egg
./ordereddict-1.1-py2.6.egg
./PasteDeploy-1.5.0-py2.6.egg
./pyramid-1.3-py2.6.egg
./repoze.lru-0.5-py2.6.egg
./translationstring-1.1-py2.6.egg
./venusian-1.0a6-py2.6.egg
./WebOb-1.2b3-py2.6.egg
./zope.deprecation-3.5.1-py2.6.egg
./zope.interface-3.8.0-py2.6.egg
import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new)

3. 修改index.wsgi文件

用site.addsitedir将这个目录包含到系统环境中。

import sae

import os
import site

app_root = os.path.dirname(__file__)
site.addsitedir(os.path.join(app_root, 'virtualenv.bundle'))

from pyramid.config import Configurator

settings = {"pyramid.reload_templates":"false",
            "pyramid.debug_authorization":"false",
            "pyramid.debug_notfound":"false",
            "pyramid.debug_routematch":"false",
            "pyramid.default_locale_name":"en"
            }

config = Configurator(settings=settings)
config.add_static_view('static', 'mypyramid:static', cache_max_age=3600)
config.add_route('home', '/')
config.scan("mypyramid")

app = config.make_wsgi_app()

application = sae.create_wsgi_app(app)


4. 上传

使用svn ci -m "my first pyramid in egg package"


5. 浏览

使用SAE提供的链接访问刚才部署的应用吧。


6. example下载与使用


本次使用的例子已经上传到vdisk,下载

下载该例子包含了pyramid开发所需的文件支持,可以直接在本地使用pyramid环境完成日常开发。开发完毕再将涉及的egg包打包到vieturlenv.bundle目录,静态文件拷贝到static目录上传即可。



本例子只适合当前的SAE环境,今后SAE环境的变化可能会导致本例不能正常运行。


Friday, April 27, 2012

在SAE上部署Pyramid应用(Python应用)


SAE是国内做的比较出色的PaaS平台,而且很早就推出了Python支持。不过因为没有内置Pyramid框架的支持(一大堆杂七杂八的包,支持也不易啊~~),要将一个Pyramid应用部署到还是要花点时间的。

一、创建应用

1. 按照SAE手册,在“我的首页”创建新应用:pyramidkoans。


3. 创建目录1


二、加入Pyramid应用

1. 新建一个Pyramid应用:mypyramid

2. 将mypyramid/mypyramid目录拷贝到1目录

3. 将mypyramid/mypyramid/static 目录拷贝到1目录(SAE处理了/static开始的请求,不过不能删除原来目录下的static目录及内容,否则pyramid在生成static_url的时候会出错。)

4. 编辑index.wsgi文件为

import sae

import os
import sys

app_root = os.path.dirname(__file__)
sys.path.insert(0, os.path.join(app_root, 'virtualenv.bundle'))

from pyramid.config import Configurator

settings = {"pyramid.reload_templates":"false",
            "pyramid.debug_authorization":"false",
            "pyramid.debug_notfound":"false",
            "pyramid.debug_routematch":"false",
            "pyramid.default_locale_name":"en"
            }

config = Configurator(settings=settings)
config.add_static_view('static', 'mypyramid:static', cache_max_age=3600)
config.add_route('home', '/')
config.scan("mypyramid")

app = config.make_wsgi_app()

application = sae.create_wsgi_app(app)


三、整理Pyramid支持包

1. 打包

SAE提供的bundle_local.py在处理

zope.deprecation-3.5.1-py2.7.egg
zope.interface-3.8.0-py2.7-macosx-10.7-intel.egg

这两个包的时候会出错(zope目录已经建立。。。,没判断。)。

因此自己一个一个拷贝到virtualenv.bundle吧。

2. 修改

因SAE不支持载入.so文件,因此需要对

zope.interface-3.8.0-py2.7-macosx-10.7-intel.egg
MarkupSafe-0.15-py2.7-macosx-10.7-intel.egg

这两个包进行修改,修改内容为:
删除 zope.interface-3.8.0-py2.7-macosx-10.7-intel.egg包中的 _zope_interface_coptimizations.c  _zope_interface_coptimizations.py
删除 MarkupSafe-0.15-py2.7-macosx-10.7-intel.egg包中的 _speedups.c _speedups.py

3. 下载ordereddict-1.1包,解开将其中的ordereddict.py放入virtualenv.bundle根目录。http://pypi.python.org/pypi/ordereddict

4. 最终的项目目录结构如下:



virtualenv.bundle中目录结构如下:




5. 因考虑到可能需要修改一些包中的文件,这次测试没有用egg、zip包(SAE提到了可以将全部第三方代码打成一个zip包,没提每个第三方能否分别用一个egg包的方式),全部解开上传的。


四、上传到SAE

在pyramidkoans运行svn add 1,然后运行svn ci -m "my first pyramid app"即可。

然后就可以通过SAE提供的URL进行访问了。





Thursday, April 26, 2012

Amazon EC2 AutoScaling配置备忘录


Amazon AWS EC2 提供了良好的AutoScaling能力,可以根据预先定义的几个系统性能参数的阈值提供自动地服务能力伸缩,并且同时提供了良好的使用文档、开发文档,基本看完其开发文档即可正常使用其提供的功能。本文仅记录一次AutoScaling过程的一些操作步骤备忘以及一些需要今后注意地地方。

一、简单实验

1. 下载AutoScaling工具包



2. 配置本地环境

如果电脑上没有安装Java,就安装一个,然后处理后续操作。

export JAVA_HOME=/Library/Java/Home

export AWS_AUTO_SCALING_HOME=/Users/eryxlee/amazon/tools/AutoScaling-1.0.49.1

export PATH=$PATH:$AWS_AUTO_SCALING_HOME/bin

编辑认证文件credential.txt,里面包含如下内容:(具体keyed,key到AWS Security Credentials中寻找。
AWSAccessKeyId=XXXXXXXXXXXXXXXXX
AWSSecretKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

编辑完之后,变更权限
chmod 600 credential.txt

export AWS_CREDENTIAL_FILE=/Users/eryxlee/amazon/tools/AutoScaling-1.0.49.1/credential.txt


3. 简单命令操作

配置完环境变量之后,运行as-cmd命令即可得到一个AutoScaling的命令列表。

运行as-describe-auto-scaling-groups得到No AutoScalingGroups found,如果你之前没有建过的话


4. 建立简单的一个AutoScaling配置,并最终销毁它

4.1. 建立节点启动配置

as-create-launch-config MyTestLC --image-id ami-7530ee1c --instance-type t1.micro

这里:
MyTestLC是配置名
image-id是AMI的id
instance-type是节点类型

运行完之后可以运行as-describe-launch-configs查阅已经创建的配置。

4.2. 建立AutoScaling组

as-create-auto-scaling-group MyTestGroup --launch-configuration MyTestLC --availability-zones us-east-1a --min-size 1 --max-size 1

这里:
MyTestGroup是组名
launch-configuration是上一步创建的配置名
availability-zones指定节点创建所在的可用区域
min-size指最小节点数量
max-size指最大节点数量

运行完之后可以运行验证命令:

as-describe-auto-scaling-groups --headers

可以的大如下类似信息:
AUTO-SCALING-GROUP  GROUP-NAME   LAUNCH-CONFIG  AVAILABILITY-ZONES  MIN-SIZE  MAX-SIZE  DESIRED-CAPACITY
AUTO-SCALING-GROUP  MyTestGroup  MyTestLC       us-east-1a          1         1         1              
INSTANCE  INSTANCE-ID  AVAILABILITY-ZONE  STATE      STATUS   LAUNCH-CONFIG
INSTANCE  i-cf3291a8   us-east-1a         InService  Healthy  MyTestLC

4.3. 查验结果

现在登录AWS console,可以看到已经有一个节点在启动了。不过我们也可看到,这个节点没有配置security group,也没有配置keypair,基本上属于不可用状态。(除非已经在AMI中配置好了所有自启动并且计算完将结果传输到其他服务器上。)

4.4. 销毁节点

在清理AutoScaling组之前,推荐先销毁所有的节点,运行一下命令,指定最大最小数量均为0:

as-update-auto-scaling-group MyTestGroup --min-size 0 --max-size 0

整个节点关闭过程需要几分钟,耐心等待。

4.5. 销毁AutoScaling组

as-delete-auto-scaling-group MyTestGroup

4.6. 销毁启动配置

as-delete-launch-config MyTestLC

这样一个AutoScaling的轮回就基本结束了。不过也可以看到,除非AMI配置很好,这个AutoScaling组做不了啥工作。


二、建立一个真实可用的AutoScaling组

1. 准备工作

要做好一个运行良好的AutoScaling组,需要实现做好规划,准备在节点上面完成什么功能,结果如何输出,是否需要人工登录,是否需要安全配置等等。

下面我们就看一个简单的WEB应用服务器的简单扩容配置方式,假定我们需要提供一个Pyramid应用,它提供80端口访问,使用独立的MySQL数据库,并且管理人员偶尔需要登录上去进行简单管理。

1.1. AMI制作

先从已有AMI启动一个节点,假定这个节点已经设置好OS配置,可以正常运行。下面开始应用的部署:

* 在该节点上配置好python环境
* 安装好Pyramid
* 部署应用
* 配置应用Log集中输出到特定服务器
* 配置supervisor服务

因为AutoScaling的自身特性,不推荐在其上配置MySQL之类的数据库服务,也不建议在上面存储任何数据,如果要存储文件,建议直接存储到S3。这样该节点随时打开关闭都不会影响应用数据完整性。

也可以配置puppet等自动安装服务,不过如果应用系统稳定,建议直接做出AMI,免去启动之后安装配置动作,直接启动即可用。

如果节点启动之后需要一个比较长的暖场时间,则需要小心配置之后涉及的冷却时间和ELB存活自动检测以免出现多启动节点或ELB认为节点已正常但实际还未提供服务的情况。

1.2. keypair生成

如果今后希望可以登录AutoScaling的节点,则需要在创建的时候提供keypair,建议在开始配置前准备好一个可用的keypair

1.3. security group配置

如果需要配置对外访问安全,还需要配置好security group。我们这里因只需要提供80端口(Pyramid 6543端口)、22端口服务,则配置22端口对所有IP可访问。6543端口对ELB可访问(amazon-elb/amazon-elb-sg),ELB对外提供80端口,外网无法直接访问节点6543端口。

1.4. ELB配置

配置ELB HTTP 80端口转向6543端口


2. 环境配置

2.1. AutoScaling的配置

同第一节内容

2.2. CloudWatch配置

下载CloudWatch工具包,http://aws.amazon.com/developertools/2534

export AWS_CLOUDWATCH_HOME=/Users/eryxlee/amazon/tools/CloudWatch-1.0.12.1

export PATH=$PATH:$AWS_CLOUDWATCH_HOME/bin


3. 创建启动配置

as-create-launch-config MyTestLC --image-id ami-7530ee1c --instance-type t1.micro --key testenv-keypair --group test-autoscale

这里多了两个参数:

key指key pair的名字
group指security group的名字


4. 创建AutoScaling组

as-create-auto-scaling-group MyAutoScalingTestGroup --launch-configuration MyTestLC --availability-zones us-east-1a us-east-1b --min-size 1 --max-size 10 --load-balancers autoscale

这里可用区域指定了2个,之后创建的节点可以分布在这两个区域中。
load-balancers指定了ELB名字

同第一节,创建完之后,即可看到一个节点已经启动,现在就可以测试直接通过ELB的DNS名字可以直接访问Pyramid应用。如果有任何问题,检查之前的配置。

注意:新建的ELB很有可能会出现节点加入了之后Health Check过不了没法提供服务的情况(已经遇到几次了。),这时候只要手工将节点从ELB中移除,然后再加入,就会正常。正式使用前,最好几个配置到Auto Scaling中的可用区域中的节点都试试看,直到正常自动加入为止。


5. 配置ScaleUp策略

as-put-scaling-policy MyScaleUpTestPolicy --auto-scaling-group MyAutoScalingTestGroup --adjustment=1 --type ChangeInCapacity --cooldown 90

这个命令指定了一直ScaleUp的策略,策略内容为每次增加一个节点,之后90秒为冷却时间。冷却时间是为了确保刚才启动的节点已经真正提供服务之后再进行系统负载评估。在冷却时间内,其他Scale操作命令都无效,安静的等待刚才的服务节点提供服务能力。良好的冷却时间选择能够有效的防止系统规模暴涨暴跌。

该命令执行之后,会返回类似arn:aws:autoscaling:us-east-1:538202822254:XXXXXXXXXXX的一长串ARN名字,记得保留这个名字后面有用。


定义了ScaleUp策略之后,怎么触发这个策略呢?那就需要用到刚才配置的CloudWatch工具了。下面的命令定义了一个CPU负载过高的告警,一旦CPU一段时间内达到定义的负载阈值,就会触发这个告警:

mon-put-metric-alarm MyHighCPUAlarm --comparison-operator GreaterThanThreshold --evaluation-periods 1 --metric-name CPUUtilization --namespace "AWS/EC2" --period 60 --statistic Average --threshold 60 - -alarm-actions POLICY-ARN_from_previous_step --dimensions "AutoScalingGroupName=MyAutoScalingTestGroup"

这是一个很复杂的命令。它的主要目的就是评估现有系统性能,一旦发现超出告警设定,就使用AWS SNS发出消息。其参数的含义是:

comparison-operator:比较操作符,主要有GreaterThanOrEqualToThreshold、GreaterThanThreshold、LessThanOrEqualToThreshold、LessThanThreshold 这几种。
evaluation-periods:评估时间周期
metric-name:度量对象,这里选择CPU
namespace:命名空间,如AWS/EC2、AWS/ELB
period:告警时间周期
statistic:统计方式,可以是Minimum、Maximum、Sum、Average、SampleCount
threshold:阈值,CPU达到60%
alarm-actions:这里是SNS话题名,往这个话题发送消息
Dimensions:是一个key/value对,用于标识这个度量对象的一些特定属性。

注意:
a. 为了演示需要(尽快看到效果),在上面几个命令中,各参数的值都被人为缩小了,实际环境所用的值大多都会大于上面的设定。具体的配置值需要根据实际场景反复实验推敲。
b. 注意各配置参数之间的关系,以及它们之间的配合。
c. AWS还提供了定时ScaleUp等很多策略配置,具体查看AWS文档。

6. 定义ScaleDown策略

定义了ScaleUp策略之后,最好还要定义ScaleDown策略,这样就会在负载低的时候减少节点,从而减少资源占有,降低成本。

as-put-scaling-policy MyScaleDownTestPolicy --auto-scaling-group MyAutoScalingTestGroup --adjustment=-1 --type ChangeInCapacity --cooldown 90

mon-put-metric-alarm MyLowCPUAlarm --comparison-operator LessThanThreshold --evaluation-periods 1 --metric-name CPUUtilization --namespace "AWS/EC2" --period 60 --statistic Average --threshold 15 --alarm-actions POLICY-ARN_from_previous_step --dimensions "AutoScalingGroupName=MyAutoScalingTestGroup"

命令含义基本跟ScaleUp类似。


7. 加压吧!

完成上面的配置之后,马上可以再开个节点安装个siege进行压力测试了,也应该可以在Console中看到instance的增加减少,不过需要点耐心。

通过压力测试也可以验证各参数之间的配合是否协调,是否能满足设计需要。


8. 清理现场

如果不要这个AutoScaling配置了,还需要完成下面动作清理现场。
mon-delete-alarms MyLowCPUAlarm
mon-delete-alarms MyHighCPUAlarm
as-update-auto-scaling-group MyAutoScalingTestGroup --min-size 0 --max-size 0
as-delete-auto-scaling-group MyAutoScalingTestGroup
as-delete-launch-config MyTestLC


9. 计费事项

实现了AutoScaling是件很爽的事情,除了收到账单的时候。AWS的整体费用真正用起来可不算低哦。在整个AutoScaling配置中,涉及到收费的地方有:

a. Amazon SNS,每月10w免费额度,之后每10w收6美分
b. Instance,按节点量、大小计算
c. ELB,按时长+流量计算
d. CloudWatch,按节点+告警计算
e. 流量费用
f. EBS、EBS IO费用,如果用了的话



Monday, April 23, 2012

Pyramid 1.3 使用说明文档


这是一个Pyramid 1.3使用文档的集合,其主要内容编译自官方文档,部分片段、案例按照自己的理解和测试做了改写,去掉了其中繁琐重复以及不常用的内容、并将一些个人感觉比较凌乱的章节进行了整理。



Pyramid 1.3基础


因Pyramid1.3相比以前的版本从使用命令方面变更比较多,因此补个简单使用的章节。

一、虚拟环境

在安装Pyramid之前,推荐首先利用virtualenv建立一个专用的虚拟环境。

virtualenv --no-site-packages pyramid-1.3

这里使用了--no-site-packages参数,避免了跟系统安装包的冲突,可以保证一个比较干净的环境。

使用source bin/activate激活这个虚拟环境,以便后续操作。


二、安装Pyramid

安装Pyramid 1.3版本。

easy_install pyramid==1.3

系统会自动下载安装所依赖的包。


三、建立工程

使用命令:

pcreate -s alchemy simpleCRUD

即可建立一个项目框架,这里可以选择 alchemy、starter、zodb三种类型的项目框架(scaffold)。


四、进入开发模式

项目建立好之后,在开始开发前,还需要在该项目目录下运行如下命令:

python setup.py develop

该命令会在我们刚才建立的虚拟环境中的lib/python/site-packages目录下建立一个指向本目录的指针 -- 1). simpleCRUD.egg-link文件(该文件内容包含了本项目的绝对路径),2). 在easy_install.pth中加入本项目的绝对路径。如果setup.py中包含了未安装得python包,也会在此时安装这些包。


五、运行单元测试

生成的项目框架默认有一个单元测试的例子,在项目配置好之后,即可运行单元测试命令:

python setup.py test -q

如果安装了nose、coverage,还可以得到单元测试覆盖率:

nosetests --cover-package=simplecrud--cover-erase --with-coverage


六、安装数据库

如果是使用了sqlalchemy的项目,还需要额外的一步:建立数据库。

initialize_simpleCRUD_db development.ini

运行该命令将会执行simplecrud/script目录下的initializedb.py文件,在该文件中,可以建立数据库、插入初始数据。


七、启动应用

做完上面所有步骤之后,即可使用:

pserve development.ini --reload

启动服务器,开始提供服务了。

用Pyramid建立一个简单的CRUD程序


熟悉Pyramid之后,可以很方便的建立一个简单的CRUD程序。下面我们展示一下一个最简单的书籍管理应用的构建过程。

一、建立项目框架

pcreate -s alchemy simpleCRUD

cd simpleCRUD

python setup.py develop

二、建立model

model.py 加入代码:

class Book(Base):
    __tablename__ = 'book'
    id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
    name = Column(Unicode(64), unique=True, nullable=False)
    author = Column(Unicode(32), nullable=True)
    desc = Column(Unicode, nullable=True)
    ISBN = Column(Unicode(20), nullable=True)
    price = Column(Float, nullable=True)

    def __init__(self, name, author, desc, ISBN, price):
        self.name = name
        self.author = author
        self.desc = desc
        self.ISBN = ISBN
        self.price = price


initializedb.py 中main函数变更为:

def main(argv=sys.argv):
    if len(argv) != 2:
        usage(argv)
    config_uri = argv[1]
    setup_logging(config_uri)
    settings = get_appsettings(config_uri)
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    Base.metadata.create_all(engine)
    with transaction.manager:
        book = Book(name='Python', author='chen', desc='desc', ISBN='978-7-121-06874-4', price=69.80)
        DBSession.add(book)


然后运行

initialize_simpleCRUD_db development.ini

建立数据库,并在数据库中插入第一条记录。


三、建立route结构

在__init__.py中的main函数加入代码:

    config.add_route('book_list', '/book/list')
    config.add_route('book_detail', '/book/detail/{id}')
    config.add_route('book_add', '/book/add')
    config.add_route('book_edit', '/book/edit/{id}')
    config.add_route('book_delete', '/book/delete/{id}')

通过上述URL结构即可构造一个简单的CRUD应用。


四、第一个功能list

首先实现list功能,确保list能正确读取数据库数据。因此,构建单元测试:

    def test_book_list(self):
        from .views import book_list
        request = testing.DummyRequest()
        info = book_list(request)
        self.assertEqual(len(info['books']), 1)
        self.assertEqual(info['books'][0].name, 'Python')

为了满足这个单元测试运行的条件,修改setUp函数为:

    def setUp(self):
        self.config = testing.setUp()
        from sqlalchemy import create_engine
        engine = create_engine('sqlite://')
        from .models import (
            Base,
            Book,
            )
        DBSession.configure(bind=engine)
        Base.metadata.create_all(engine)
        with transaction.manager:
            book = Book(name='Python', author='chen', desc='desc', ISBN='978-7-121-06874-4', price=69.80)
            DBSession.add(book)

该setUp函数建立一个专门用于单元测试的内存数据库,然后建立所有model定义的表结构,并插入第一条记录。在完成上面的初始化之后,我们的第一个单元测试的逻辑是正确的了。

现在再来构建对应的view:

@view_config(route_name='home', renderer='simplecrud:templates/book/book_list.pt')
@view_config(route_name='book_list', renderer='simplecrud:templates/book/book_list.pt')
def book_list(request):
    books = DBSession.query(Book).all()
    return dict(books = books)

这里就不分页了,全部一页取出显示。并且首页就是list页。

这时候运行 python setup.py test 即可运行单元测试查看是否满足。

但单元测试满足之后,现在使用pserver development.ini还是无法看到页面内容的,这需要建立对应的渲染模版,我们这里先使用chameleon模版来完成这个工作。



该模版的主要工作就是显示一个table,里面包含所有数据记录。在模版中的tal属性是chameleon定义的特殊属性。

1. tal:repeat="item books" 循环books列表,逐项取出赋值给item

2. repeat.item.number 循环计数器

3. tal:content="repeat.item.number" 替换该标签的内容部分,这里就是内容“1”

4. request.route_path('book_detail', id=item.id) 生成book_detail链接

5. tal:attributes="href string:${request.route_path('book_edit', id=item.id)}" 将标签的href属性替换


五、显示详细信息页面

详细信息页面基本构成跟list页面基本类似,最主要的区别是,当book id不存在时,详细信息页面需要返回HTTPForbidden,我们需要在单元测试中覆盖这种情况。

    def test_book_detail_unauthorized(self):
        from pyramid.httpexceptions import HTTPForbidden
        from .views import book_detail
        request = testing.DummyRequest()
        request.matchdict['id'] = 555
        info = book_detail(request)
        self.assertIsInstance(info, HTTPForbidden)

注意,如果我们想要在视图中使用raise的化,要使用self.assertRaises(HTTPForbidden, view_fn, request)这种方式来捕获。

六、使用simpleform来添加记录页面

添加记录程序一般会涉及到显示Form、存储、出错这样三种情况,因此,构建三个测试用例:

在request中什么都没有的情况下,显示form
    def test_book_add_form(self):
        from pyramid_simpleform.renderers import FormRenderer
        from .views import book_add
        request = testing.DummyRequest()
        info = book_add(request)
        self.assertIsInstance(info['renderer'], FormRenderer)

在request中指定了是POST时,将取到的数据存入数据库,成功后页面转向list页面,最后验证存入正确
    def test_book_add_save(self):
        from pyramid.httpexceptions import HTTPFound
        from .views import book_add
        from .models import Book

        _registerRoutes(self.config)

        request = testing.DummyRequest({'name':'a new book',
                                        'author':'a new author',
                                        'ISBN':'a new ISBN',
                                        'desc':'a new desc',
                                        'price':'6.0',
                                        'submit':'save'})
        request.method = 'POST'
        info = book_add(request)
        self.assertIsInstance(info, HTTPFound)

        added = DBSession.query(Book).filter(Book.id==2).first()

        self.assertEqual(added.name, 'a new book')
        self.assertEqual(added.author, 'a new author')

在存入数据有错误的情况下,返回提交界面,并确保不会变更数据库已有数据
    def test_book_add_conflict(self):
        from pyramid_simpleform.renderers import FormRenderer
        from .views import book_add
        from .models import Book

        _registerRoutes(self.config)

        request = testing.DummyRequest({'name':'Python',
                                        'author':'a new author',
                                        'ISBN':'a new ISBN',
                                        'desc':'a new desc',
                                        'price':'6.0',
                                        'submit':'save'})
        request.method = 'POST'
        info = book_add(request)
        self.assertIsInstance(info['renderer'], FormRenderer)

        added = DBSession.query(Book).filter(Book.id==1).first()

        self.assertEqual(added.name, 'Python')
        self.assertEqual(added.author, 'chen')
        self.assertEqual(added.ISBN, '978-7-121-06874-4')


下面开始构造视图方法。一般情况下,从form读取数据都有有个验证、校验、转换过程,为了程序简单,我们采用simpleform,以及formencode用于数据校验,form构成。

class BookSchema(Schema):

    filter_extra_fields = True  # 过滤掉其他字段
    allow_extra_fields = True  # 允许form中有其他字段

    name = validators.String(min=2, max=64, not_empty=True)  # name最小2字符,最大64
    author = validators.String(max=32)
    desc = validators.String()
    ISBN = validators.String(max=20)
    price = validators.Number(max=1000)    # price为数字,浮点型

@view_config(route_name='book_add', renderer='simplecrud:templates/book/book_add.pt')
def book_add(request):
    form = Form(request, schema=BookSchema)
    if form.validate():      # 如果是正确提交

        book = Book(form.data.get("name"),
                    form.data.get("author"),
                    form.data.get("desc"),
                    form.data.get("ISBN"),
                    form.data.get("price"))

        try:
            DBSession.add(book)
            DBSession.flush()
            transaction.commit()

            return HTTPFound(location=route_path("book_list", request))  # 返回到list页面
        except IntegrityError:  # name重复,设置form出错信息
            transaction.abort()
            form.errors["global_error"] = 'database insert error, maybe book name conflict.'
        except Exception, e:
            transaction.abort()
            form.errors["global_error"] = 'database error.' + str(e)
            log.error("database error!")

    return dict(renderer=FormRenderer(form))  返回form页面

这里需要注意的是,单元测试中,并没有去构造route表,但我们程序的重定向中使用了route_path方法,因此需要在单元测试中手工构造route表:

def _registerRoutes(config):
    config.add_route('book_list', '/book/list')
    config.add_route('book_detail', '/book/detail/{id}')
    config.add_route('book_add', '/book/add')
    config.add_route('book_edit', '/book/edit/{id}')
    config.add_route('book_delete', '/book/delete/{id}')


七、edit,delete

edit、delete基本类似new与detail,值得注意的是,edit时尽可能只访问一次数据库、减轻数据库访问量。

八、完整程序