About
RSS

Bit Focus


简易配置 gunicorn

引子

    单纯 gevent 跟 nodejs 一样有个问题是如果服务器有大的同步计算 (比如压缩一张图片什么的) 需求时, 服务器会很卡. 这也不能怪它们, 因为本来它们的长处是 IO 异步化, 同步计算卡住是缺陷特性之一.
    然, 或荐基独搅受 gunicorn 以解此困. 只是其首页上例子意味不明, 各种文档文章都说要编写一些离奇复杂的配置文件, 然后跑个语焉不详的 hello world, 并没能明示重点问题.

正文

    嘛, 一番探索之后配了下面一个用例 (Flask)
import time
import flask

app = flask.Flask(__name__)

@app.route('/<int:n>')
def root(n):
    time.sleep(2)
    i = n / 2
    while 1 < i:
        if n % i == 0:
            return 'not prime'
        i -= 1
    return 'prime'

if __name__ == '__main__':
    app.run(port=8000)
    这个例子里面兼顾了长 IO (用睡眠去模拟) 跟大计算 (算请求的数是不是个素数). 把这货在控制台裸着启动起来, 然后用 apache benchmark 来一发 (如果觉得后面请求参数里那个素数不够大, 可以自行算一个大的替换)
ab -n 500 -c 50 localhost:8000/16785407
    当然了, -c 50 这个参数纯是卖萌的, 因为上面这代码自身根本异步不起来. 结果自然是惨不忍睹, 重点两行在测试机上表现如下
Time per request:       131417.472 [ms] (mean)
Time per request:       2628.349 [ms] (mean, across all concurrent requests)
    平均单个请求耗时 2.6 秒以上, 其中 2 秒是睡过去的, 剩下 0.6 秒是计算. 也就是说 IO 时间与计算时间大概的比例是 3:1.

    安装 gunicorn 可以直接通过 pip 安装, 简单容易, 就不废话了. 下面上 gunicorn 平装版, 把上面的文件保存为 test.py, 在控制台中执行
gunicorn -w 4 test:app
    这个是说, 开 4 个进程跑 test 模块下的 app (就是文件里全局定义的 app 变量啦). 现在再开 ab 来一炮 (参数完全相同), 结果是
Time per request:       33150.026 [ms] (mean)
Time per request:       663.001 [ms] (mean, across all concurrent requests)
    从结果上来看差不多就是裸跑的 1/4 了, 因为开了 4 个进程一起搅嘛.

    虽然有 4 个进程睡睡醒醒轮番搞, 但没有异步 IO 的支持, 进程睡着就不干事了. 作为要榨干 worker 进程以及 CPU 使用率的系统管理员来说这可不能忍, 于是继续折腾个 gevent 进去好了, 两者互补, 相得益彰.
    不过用 gunicorn 就不需要在文件最开始打猴子补丁了, gunicorn 有个参数直接让 gevent 嵌入进程
gunicorn -w 4 -k gevent test:app
    再来一发 ab, 结果是
Time per request:       9724.214 [ms] (mean)
Time per request:       194.484 [ms] (mean, across all concurrent requests)
    嘛, 算是还看得过去的数据了.

补充说明

绑定其它端口

Permanent Link: ?p=516 Load full text

Post tags:

 Python
 Flask
 Web Server
 Gevent
 Gunicorn

Flask / MongoDB 搭建简易图片服务器

前期准备

通过 pip 或 easy_install 安装了 pymongo 之后, 就能通过 Python 调教 mongodb 了.
接着安装个 flask 用来当 web 服务器.
当然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同学, 安装最新版要略费些周折, 具体说是

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-10gen

如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己, 那么最好还准备个 Pillow 库

pip install Pillow

或 (更适合 Windows 用户)

easy_install Pillow

正片

Flask 文件上传

    Flask 官网上那个例子居然分了两截让人无从吐槽. 这里先弄个最简单的, 无论什么文件都先弄上来
import flask

app = flask.Flask(__name__)
app.debug = True

@app.route('/upload', methods=['POST'])
def upload():
    f = flask.request.files['uploaded_file']
    print f.read()
    return flask.redirect('/')

@app.route('/')
def index():
    return '''
    <!doctype html>
    <html>
    <body>
    <form action='/upload' method='post' enctype='multipart/form-data'>
         <input type='file' name='uploaded_file'>
         <input type='submit' value='Upload'>
    </form>
    '''

if __name__ == '__main__':
    app.run(port=7777)
  • 注: 在 upload 函数中, 使用 flask.request.files[KEY] 获取上传文件对象, KEY 为页面 form 中 input 的 name 值
    因为是在后台输出内容, 所以测试最好拿纯文本文件来测.

保存到 mongodb

    如果不那么讲究的话, 最快速基本的存储方案里只需要

Permanent Link: ?p=514 Load full text

Post tags:

 Python
 Web Server
 Flask
 Tutorial
 MongoDB

Flask 出坑记

    Flask 是个 Python Web 框架. 网站上文档例子都很详尽, 这里就不废话了, 只是来扯两个使用中需要注意的地方.

获取 POST 请求体

    21 世纪的 Web 交互中服务器跟浏览器互相丢 JSON 已经成了司空见惯的事情. 在 Flask 框架作成的服务器上要搞到 JSON 数据当然是直接访问 POST 请求体了, 如
import flask
import functools

app = flask.Flask(__name__)

@app.route('/wtf', methods=['POST'])
def wtf():
    return 'Received: ' + flask.request.data

def main():
    app.run(port=7777)

if __name__ == '__main__': main()
    按文档的说法, flask.request.data 包含请求数据字符串. 但其实这也是个坑, 默认情况下根本取不到请求数据
curl -d "[1,1,2,3,5,8]" http://localhost:7777/wtf
Received:
    熊孩子你把拿到的字符串给吃了吧? 实际上如果去看看那文档会看到并不如上面所说的那样, 而是
  • Contains the incoming request data as string in case it came with a mimetype Flask does not handle.
    后面这个状语从句真是着急, 那到底什么 mimetype 会使得 Flask does not handle 呢? 根本没说清楚啊. 扫一眼文档后面, 还有个东西可以用: flask.request.json, 但这货一般是 None, 只有当请求 mimetype 被设置为 application/json 的时候才有用, Flask 你真心是跟 mimetype 过不去啊. 也就是说得这样发请求
curl -d "[1,1,2,3,5,8]" localhost:7777/wtf
Received: null
curl -d "[1,1,2,3,5,8]" -H "Content-Type:application/json" localhost:7777/wtf
Received: [1, 1, 2, 3, 5, 8]
# Server codes

import json
import flask
import functools

app = flask.Flask(__name__)

@app.route('/wtf', methods=['POST'])
def wtf():
    return 'Received: ' + json.dumps(flask.request.json)

def main():
    app.run(port=7777)

if __name__ == '__main__': main()
    问题是现在前端攻城狮都被浏览器兼容性折腾得满世界买表, 哪还有心情检查每个请求的 content-type 对不对. 况且这还只对 JSON 有效, 如果是山寨协议又怂了.
    好吧, 如果实在不行, 就挖到 WSGI 里面去好了, 比如这样
def request_data():
    d = flask.request.data
    if not d:
        return ''.join(flask.request.environ['wsgi.input'].readlines())
    return d

Permanent Link: ?p=509 Load full text

Post tags:

 Python
 Flask
 Web Server

NodeJS liteview Postgres Stekinscript 搭建留言板 [完]

上篇

前期准备

安装软件与库

    需要安装
  • Postgres (建议 9 或以上版本)
  • libpq-dev
    npm 安装 Postgres 库
npm install pg

配置数据库与表

    参考这篇文章前半部分配置 Postgres 数据库.
    如果已经为 PG 用户 psuser 授权使用 mydb 这个数据库, 现在进入数据库
psql -Upsuser -dmydb -W
Password for user psuser:
建立数据表用于存放留言
mydb=> CREATE TABLE message (
mydb(>   id SERIAL,
mydb(>   content TEXT,
mydb(>   post_time TIMESTAMP DEFAULT NOW(),
mydb(>   PRIMARY KEY(id)
mydb(> );
NOTICE:  CREATE TABLE will create implicit sequence "message_id_seq" for serial column "message.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "message_pkey" for table "message"
CREATE TABLE

显示留言信息

更新页面模板

    之前的页面模板实在显得太单薄, 下面修改一下准备放入留言数据
<html>
<head><title>Guestbook</title></head>
<body>
#{foreach(#messages)}
<div>
    <p style='font-size: 0.6em'>#{[post_time]}</p>
    <p style='margin-left: 20px'>#{[content]}</p>
</div>
#{end}
    liteview 文档如是说
  • #{foreach(#XXX)} 即在 XXX 对象上作循环
  • #{[YYY]} 即每次循环的循环变量的 YYY 属性
    因此上面的页面模板需要传入留言对象列表, 每个留言对象有属性 post_time, content. 这命名与数据库表是一致的.
    然后改一下 index.stkn, 放假数据进去先看看效果
view: require('liteview').create()

exports.get: (request, response):
    response.writeHead(200, { 'Content-Type':: 'text/html' })
    response.end(view.render('index.html', {messages: [
        {
            post_time: '1970-01-01 00:00:00',
            content: 'Nostalgia',
        },
        {
            post_time: '1970-01-01 00:00:00',
            content: 'Eternal Rite',
        },
        {
            post_time: '1970-01-01 00:00:00',
            content: 'Shinto Shrine',
        },
    ]}))
    刷下页面, 这些东西就出现了.

从数据库加载内容

    巧妇难为无米之炊, 现在首先要做的是向数据库插入一些数据. 还是更刚才建表一样, 进入 psql, 使用下面的语句插入一些水帖

Permanent Link: ?p=500 Load full text

Post tags:

 Postgres
 Stekin
 Tutorial
 Web Server
 NodeJS

NodeJS liteview Postgres Stekinscript 搭建留言板 [零]

前期准备

配置软件与库

    需要安装
  • nodejs 0.8 或以上版本 (之后编译 Postgres 模块需要高版本)
  • npm
  • 下载编译 Stekinscript (非必需) $ git clone git://github.com/neuront/stekinscript.git
    Ubuntu 用户安装 nodejs 0.8 版本请加入下面的源
add-apt-repository ppa:chris-lea/node.js
    创建一个工作目录, 通过 npm 安装要用到的模块
npm install liteview
npm install validator

Stekinscript 快速入门 (非必需)

    请参见 https://github.com/neuront/stekinscript/wiki/Language-Specification.
    对 Stekinscript 没有兴趣的同学可以无视这一步, 在每节之后生成的 Javascript 代码会奉上, 对于 Stekinscript 代码, 可以认为它是更易阅读的 JS.

开工

上手

    创建如下内容的 main.stkn 文件, 先启动一个简单的服务器
http: require('http')

port: 8888

http.createServer((request, response):
    response.writeHead(200, { 'Content-Type':: 'text/html' })
    response.end('<html><body>Hello, World!')
).listen(port, '127.0.0.1')

console.log('Server running at local host port=' + port)
    使用 stekin 编译它 (请用合适的路径替换下面的 stekin, 或者将编译生成的 stekin 可执行程序加入 PATH)
stekin -i require < main.stkn > main.js
    生成的 JS 代码类似 (下面这是经过去除额外括号与重新排版的, 实际上现在 Stekinscript 生成的代码真是惨不忍睹)
(function () {
    const s_http = require("http");
    s_http.createServer(function (s_request, s_response) {
        s_response.writeHead(200, {
            "Content-Type": "text/html"
        });
        s_response.end("<html><body>Hello, World!");
    }).listen(8888, "127.0.0.1");
    console.log("Server running at local host port=8888");
})();
    然后使用 node 来运行它, 就在本地搭建了一枚服务器
node main.js
Server running at local host port=8888
    访问 http://localhost:8888/ 就可以看到服务器响应了.

Makefile

Permanent Link: ?p=499 Load full text

Post tags:

 NodeJS
 Stekin
 Web Server
 Tutorial

风之力 - Tornado 搭建基于 WebSocket 的聊天服务

    这年头 Python web 框架是有点泛滥了. 下面要介绍的是 facebook 的开源框架 tornado. 这东西比较简单, 而且自带 WebSocket 支持, 可以用它做个简单的聊天室.
    读者最好已具备 Javascript 与 WebSocket 的基础知识.

安装

    使用 easy_install 能很方便地爬到 tornado. 或者, 下载源代码, 解包后在源码目录执行
$ python setup.py build
# python setup.py install
即可.

开张

    首先还是来个 hello world.
import tornado.web
import tornado.ioloop

class Index(tornado.web.RequestHandler):
    def get(self):
        self.write('<html><body>Hello, world!')

if __name__ == '__main__':
    app = tornado.web.Application([
        ('/', Index),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()
    保存为 main.py, 然后执行
$ python main.py
    并访问 http://localhost:8000/ 即可看到页面中的 "Hello, world!".

    在分支中定义的 app 在构造时接受的一个列表参数
[
    ('/', Index),
]
用来配置 URL 映射, 比如这里访问根路径则映射至 Index 实例去处理, 在 Index 实例中, 定义的 get 方法将会处理请求.

处理 WebSocket 连接

添加请求处理类

    接下来就进入 WebSocket 环节. 先修改返回的页面, 让这个页面在加载后连接服务器.
class Index(tornado.web.RequestHandler):
    def get(self):
        self.write('''
<html>
<head>
<script>
var ws = new WebSocket('ws://localhost:8000/soc');
ws.onmessage = function(event) {
    document.getElementById('message').innerHTML = event.data;
};
</script>
</head>
<body>
<p id='message'></p>
        ''')
    修改这个类后, 然后在控制台中止服务器 (猛击 Ctrl-C), 并重新启动之.
    现在, 访问 http://localhost:8000/ 会遇到 404 错误, 因为 WebSocket 请求的 URL "ws://localhost:8000/soc" 还没有映射任何处理器, 因此这里需要再添加一个, 用于处理 WebSocket 请求的类.
import tornado.websocket

class SocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        self.write_message('Welcome to WebSocket')
    并为这个类加上 URL 映射

Permanent Link: ?p=489 Load full text

Post tags:

 Python
 Web Server
 Tornado
 Tutorial

GAE 速成简易博客 - 简化 RequestHandler

上节回顾 - 进阶数据库操作

在 index.py 和 single_post.py 中, 请求处理器 IndexSinglePost 的代码重复的部分还是挺多的
class SinglePost(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__),
                            'templates/single_post.html')
        posts = db.GqlQuery('SELECT * FROM Post WHERE pid = :1',
                            int(self.request.get('id')))
        self.response.out.write(template.render(path, {
                'post': posts[0],
            }))

class Index(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__), 'templates/index.html')
        posts = db.GqlQuery('SELECT * FROM Post ORDER BY date DESC')
        self.response.out.write(template.render(path, {
                'posts': posts,
            }))
作为程序员, 应该对这样的重复代码零容忍, 当机立断, 大刀阔斧来改起!

    从上面的对比看来, 很明显, 对于日常情况中的请求, 服务器端重要的的响应参数包括
  • 模板文件的相对路径
  • 交给模板的参数
    那么很好, 来弄个基类, 新建个 base.py 放进去
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
import os

class BaseHandler(webapp.RequestHandler):
    def put_page(self, templ_file, templ_args):
        path = os.path.join(os.path.dirname(__file__), templ_file)
        self.response.out.write(template.render(path, templ_args))
    开始用新的 BaseHandler 搞起吧.
    先来搞 index.py

Permanent Link: ?p=480 Load full text

Post tags:

 Web Server
 Google AppEngine
 Tutorial
 Python

GAE 速成简易博客 - 更多数据库操作

上节回顾 - 表单处理与基本的数据库操作

现在首页能显示文章列表了, 但是
  • 文章的顺序貌似是乱的, 而一般来说, 博客系统会按照发布的时间先后顺序来放置
  • 构造一个页面, 看指定的某一篇文章的内容
那么, 现在就开始修改数据库吧.

为文章加上 ID 和日期

添加属性

修改 model.py
class Post(db.Model):
    pid = db.IntegerProperty()
    title = db.StringProperty(multiline=False)
    content = db.TextProperty()
    date = db.DateTimeProperty(auto_now_add=True)

def put_post(title, content):
    其中 pid 表示 post id, 是一篇文章的唯一标识; date 是文章的发布时间, 它被设置为对象被存入数据库时自动设置为当前时间 (auto_now_add=True).
    在 GAE 存储中, 并没有类似 auto_increment 的设置, 因此 pid 的管理需手动进行. 在数据库中, GAE 也有给每个对象设置一个全局唯一的 id, 可以通过如 post.key().id() 来获取, 但是这样获取的 id 值在发布服务器上没有规律可言, 不具备有序性, 不建议使用.

按 ID 排序查询和自增 ID

刚刚为 Post 添加的两个属性中, date 是会自动添加到数据库中的, 但 pid 并不会, 得手动给加上. 想要实现自增 ID 的功能, 一个简单的思路是, 从数据库中取出 pid 最大的那篇, 在它的基础上 +1 赋值给新文章即可. 那么继续修改 model.py
def next_post_id():
    posts = db.GqlQuery('SELECT * FROM Post ORDER BY pid DESC')
    return 0 if posts.count() == 0 else posts[0].pid + 1

def put_post(title, content):
    post = Post()
    post.pid = next_post_id()
    post.title = title
    post.content = content
    post.put()
这里 GQL 中的 ORDER BY pid DESC 表示按照 pid 排序, 而且是降序排列.

另外, 还得修改 add_post.py 里的 AddPostHandler 不能让它乱来了, 而应该改为调用 put_post
class AddPostHandler(webapp.RequestHandler):
    def post(self):
      # new_post = model.Post()
      # new_post.title = self.request.get('title')
      # new_post.content = self.request.get('content')
      # new_post.put()
        model.put_post(self.request.get('title'), self.request.get('content'))
        self.redirect('/add_post')

Permanent Link: ?p=478 Load full text

Post tags:

 Web Server
 Python
 Tutorial
 Google AppEngine

Page 0 1


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site