gefr API updates

早上收到jythonet作者的一封邮件,受到启发,我打算扩展一下gefr这个WSGI adapter。原本gefr是写给soldat用来测试的,结果发现现在出现了买椟还珠的效果,既然大家更加关注这个,我决定多花一些时间在上面。不过话说回来gefr确实是Jython Web程序的新思路,过去大家都在想怎样把Jython放进Servlet中,gefr的思路是把Java的服务器实现放到WSGI后面。未来会有gefr-netty, gefr-mina, gefr-servlet等等出现,如果真的存在一个可靠的backend,也许这种思路也不失为一个办法,毕竟python的web框架更吸引人,而java的基础设施相对可靠,各取所长。

根据这个目的,现在gefr 0.2的API更新为:

即在创建Gefr的时候需要告知backend类型,目前还只有soldat一种后端。未来也可能通过自动检测让这个参数成为可选。

gefr 0.2已经在开发中,您可以通过这里关注:
https://bitbucket.org/sunng/gefr/overview

Setting up soldat and gefr

本文介绍soldat服务器和gefr WSGI适配器的环境搭建,以及jip的基本使用。

安装python工具

virtualenv和pip是python开发的关键工具
sudo apt-get install python-virtualenv
sudo apt-get install python-pip

jython需要您手动下载安装。推荐安装到/usr/local/下,并建立软连接到/usr/local/bin/中,下文将假设您是这么做的。

创建虚拟jython环境

virtualenv -p /usr/local/bin/jython gefr-test
cd gefr-test
source bin/activate

安装jip

pip install jip

配置jip

在$HOME下创建文件.jip,内容为:

[repos:oss]
uri=http://oss.sonatype.org/content/repositories/snapshots/
type=remote

[repos:central]
uri=http://repo1.maven.org/maven2/
type=remote

[repos:local]
uri=/home/sun/.m2/repository/
type=local

安装soldat

配置完jip后,可以使用jip来安装soldat
jip install info.sunng.soldat:soldat:1.0-SNAPSHOT

文件将被下载到 javalib 目录中,您可以检查安装的正确性:
$ ls javalib/
log4j-1.2.16.jar slf4j-log4j12-1.6.1.jar
slf4j-api-1.6.1.jar soldat-1.0-SNAPSHOT.jar

安装gefr

pip install gefr==0.1dev2

创建一个简单的Python WSGI程序

创建test.py

from gefr import Gefr

def wsgiapp(environ, start_response):
    status = '200 OK'
    res_body = "<html><head><title>Welcome</title></head><body><h1>It works!</h1></body></html>"
    res_headers = [('Content-Type', 'text/html'),
            ('Content-Length', str(len(res_body)))]
    start_response(status, res_headers)
    return [res_body]

Gefr(wsgiapp, host='0.0.0.0', port=8000).start()

启动服务

使用jip附带的jython-all
jython-all test.py

打开浏览器,访问 http://localhost:8000/

用ab测试服务性能

$ ab -n 10000 -c 100 http://localhost:8000/

This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests

Server Software:        gefr/0.1dev
Server Hostname:        localhost
Server Port:            8000

Document Path:          /
Document Length:        79 bytes

Concurrency Level:      100
Time taken for tests:   2.539 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      1640000 bytes
HTML transferred:       790000 bytes
Requests per second:    3938.11 [#/sec] (mean)
Time per request:       25.393 [ms] (mean)
Time per request:       0.254 [ms] (mean, across all concurrent requests)
Transfer rate:          630.71 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.6      0       8
Processing:     9   25  22.4     21     239
Waiting:        9   25  22.4     21     239
Total:         13   25  22.3     21     239

Percentage of the requests served within a certain time (ms)
  50%     21
  66%     22
  75%     22
  80%     23
  90%     26
  95%     40
  98%     65
  99%    233
 100%    239 (longest request)

Good Luck!

Python3 script to query free TOEFL seats

应同学要求,写了一个爬托福考试空闲考位的脚本。这是第一次真正用Python3写。

通过调用get_seat_status,传入省名(如’Jiangsu’),时间(如’201104′),您可以得到这样的数据结构:

[ [datetime.datetime(2011, 4, 23, 10, 0),  'STN80085B',  '南通市教育装备与勤工俭学管理中心',  '1415',  True],
 [datetime.datetime(2011, 4, 23, 10, 0), 'STN80086A', '扬州大学', '1415', True]]

下面是一个多线程调用的例子:

#! /usr/bin/python3

import threading
import pprint
from toeflgraber import get_seat_status

# 查询江浙沪 2011年3月和4月的考位
locations = ['Jiangsu', 'Shanghai', 'Zhejiang']
months = ['201103', '201104']

def descartes(x, y):
    for i in x:
        for j in y:
            yield (i, j)

lock = threading.RLock()
tasks = len(locations) * len(months)
e = threading.Event()
results = []

def task(location, month):
    global results, tasks
    all_seats = get_seat_status(location, month)
    available_seats = filter(lambda x: x[4], all_seats)
    with lock:
        results.extend(available_seats)
        tasks -= 1
        if tasks == 0:
            e.set()

for t in descartes(locations, months):
    t = threading.Thread(target=task, args=t)
    t.daemon = True
    t.start()

e.wait()
pprint.pprint(results)

Enjoy it.

包管理与路径管理

现在几乎每一种语言都有一些依赖管理工具,或者是中央的包仓库。比如这些:

  • Java: maven, ivy, gradle
  • Ruby: gems
  • Python: easy_install, pip
  • Clojure: leinigen
  • Groovy: maven, grape, gradle
  • Hashkell: cabal
  • PHP: pear
  • Nodejs: npm

这些工具在管理包和路径时都会采用各不相同的策略,有的是通过自身实现,有的是借助语言平台本身的特点。

Java

其中最注明的Maven,它的方式是在POM文件中定义你的依赖,Maven会在本地仓库中维护这些依赖。Maven的本地仓库默认是在$HOME/.m2/repository目录下,是用户独立的,当然要下载一个依赖也不需要root权限。而在通过Maven运行Java项目时,Maven插件会自动管理classpath,你并不需要把这些依赖从本地仓库里拷贝出来而单独维护一个所谓lib目录(这样也不好管理)。这是Maven的方式,目前看来,这也是最经典的一种方式。除了maven本身的特性以外,这也和Java的classpath机制有关。

最近势头很猛的Java构建工具gradle,方式也与Maven类似,它会把下载的依赖存放在$HOME/.gradle/cache目录里,并自动管理classpath。

同样是Java,与maven相对应的是ant+ivy。ivy可以为你管理依赖,但是ivy不会帮你管理classpath。ivy的包管理,是以project为scope的,你需要维护一个lib目录来存放这些下载的包,再通过传统的ant的方式去管理classpath,从而使项目可以进行编译和运行。

在Java世界里,还有一个特别的工具叫做grape,它是专门用于groovy的轻量级的依赖解决方案。grape是以脚本为scope,在需要依赖的脚本中通过@Grab声明依赖,grape工具可以从maven仓库中下载依赖到$HOME/.groovy/grapes中,并把相关的依赖加入groovy的classpath。除此以外,grape还有一个命令行工具帮助你手动下载依赖到本地仓库。grape的内部是基于ivy的,不过它的方式比ant要自动化很多。

在Java世界里还有一个特例,是Clojure的依赖管理工具leiningen。leiningen本身也比较简单,它的方式与ivy相同,会解析project.clj文件中定义的依赖关系,并下载到当前的工程目录下的lib中。lein是鼓励通过uberjar的方式把依赖统统打包的,所以它并没有classpath的管理功能。

总体来说,Java世界的工具和Java是相似的,其最大特点就是System independent,安装包不需要root权限,每次的运行都需要管理classpath。作为开发人员,classpath中有哪些可以访问的类库是可以控制的,这也使Java程序的移植性得到良好的控制和管理。

Python

与Java不同,Python通常作为系统分发的一部分,他的包管理和PATH管理要相对混乱一些。通常我们有两种方式来安装一个Python的软件包:

  • sudo apt-get install python-redis
  • sudo easy_install redis

一种是通过系统的包管理工具(如apt-get)从系统的软件仓库里安装,一种是通过python自己的包管理工具(如easy_install / pip)从Python Cheese Shop中下载安装。这两种安装方式有什么不同呢。以Ubuntu为例,通过apt-get安装的python包通常会被放在 /usr/share/pyshared 或 /usr/lib/python2.6/dist-packages 中,相对应的,由easy_install安装的Python包,则存放在 /usr/local/lib/python2.6/dist-packages 中。Python启动后可以通过查看sys.path来了解当前的path情况。

除了安装到系统目录,easy_install可以通过 –user 选项来把软件包安装到用户目录 $HOME/.local/lib/python2.6/site-packages。不过无论是系统级别还是用户级别,python都很难在启动时管理Path,即任何时候python都可以访问安装在系统中的所有软件包。这导致了混乱的情况,导致编写的python软件难以进行依赖管理和移植(即使没有定义在setup.py中,很多依赖还是可以访问的)。

由此virtualenv营运而生,virtualenv帮助你创建一个独立的python运行环境。激活这个小环境之后,easy_install/pip仅仅安装软件到小环境,python仅能访问环境内部的site-packages,这样整个环境中的依赖关系就非常清楚,也保障了程序的移植性。这样,就将原本系统scope的python包管理级别改进为项目级别。我之前写的jip也是将依赖下载或拷贝到virtualenv的小环境中,并且修改jython的启动脚本修改PYTHONPATH的设置,保证Java依赖对Jython的透明可访问。

Nodejs

nodejs是一个新兴的生态系统,一个包管理工具对其也是必不可少。npm是目前整个社区都比较认可的工具。

不过目前npm并不好用。npm默认会把自己安装到 node安装前缀的目录,比如node安装时你选择了默认前缀/usr/local,那么npm会把自己安装到/usr/local/lib/node里。这个目录是系统级别的,所以需要root权限,而npm本身又不鼓励用户用root权限来安装软件包(安全问题)。所以作者说希望用户把/usr/local/lib/node权限授予用户,或者把node安装到用户目录里。这两种方式其实都不太优雅。

Ruby的gems在这方面最符合unix哲学,即用户知道自己在做什么。如果用户以root权限运行gem install,gem会把软件包安装到系统目录中对所有用户可用,而如果以普通用户权限运行,则安装到用户目录 $HOME/.gem 中仅当前用户可见。

nodejs在加载软件包时,会在require.paths中的几个目录里查找,前两个都是用户目录,所以npm也并非一定要把包安装到系统目录里去。虽然现在可以用过修改.npmrc文件在修改npm的默认行为,不过在这个CoC的时代,显然太繁琐了。

Best Pratice

总结一下,包管理和路径管理的最佳实践应该是:语言平台有CoC的路径机制,包管理器有基于环境变量、用户权限的判断执行合适行为。

soldat & gefr

我的这一套stack正在走向完整。上次贴了一张soldat-http的图,现在基于soldat的wsgi服务器也已经有了一个基本可以运行的实现,名字叫做gefr(我的命名出处参考这里)。现在gefr上已经可以跑基于bottle框架的wsgi程序了,也就是说一些基于python的web应用可能可以通过jython来运行在soldat上。为了搞定jython的环境,这几天我还花了不少时间做了jip帮我从maven仓库里自动下载依赖的jar包。

soldat和gefr的代码都放在我的bitbucket上:

此外,这两个项目也分别发布到了sonatype oss仓库python cheese shop

现在还有几个问题:

  • soldat在读buffer的时候先获得buffer的limit,再去读相应长度的buffer有时会出现BufferUnderflowException。这个可能存在线程安全问题,现在还没发现。
  • gefr启动之后可以通过在jvisualvm里找到这个进程,但是绑定profiler之后很诡异的是gefr就不再处理请求了。
  • 直接用soldat的处理http请求,吞吐量可以上万;但是在上面加上jython的gefr,再加上bottle框架,同样的功能吞吐量就剩下原来的十分之一了。就是因为没法做profile,所以还不知道时间花到哪里去了。

简单地 announce 一下,这样我有更多的动力来继续把这两个小东西做好。