Entries tagged “Python”

Cassandra修行 その2

written by shn, on Mar 21, 2010 7:06:00 PM.

「Cassandraというものがなんだかよく分からない」 〜 NoSQLについて、鳩山由紀夫

週末プロジェクトになりつつあるCassandra修行ですが、しょっぱなから躓いている。 (経験から言えば、躓いたらほっておいて先進めばいいだけなので、躓くのを言い訳に止まっている)

さて、

あるユーザーサービスを考えてみる。ユーザー毎にページをもたせたい(http://hogehoge.com/shn/ みたいに) また認証はパスワードとか面倒なのでOpenIDにしてみよう。

というのをCassandra(とLazyboy)でやるにはどうしたらいいのかしら。

MySQLだと

MySQLでやるならこんな感じのテーブルを用意しているはずだ。

CREATE TABLE Accounts (
  account_id INTEGER NOT NULL PRIMARY KEY AUTO INCREMENT,
  account VARCHAR(64) NOT NULL UNIQUE,
  openid_identifier VARCHAR(255) NOT NULL UNIQUE
);

アカウント情報について、「アカウントから」と「OpenIDから」の2系統からアクセスされるので、その二つをUNIQUE INDEXにした。

Cassandraだと

Cassandraの場合はひとつのColumnFamilyについて一つの順序しか持てない。今回のアプリの場合、http://hogehoge.com/shn/からshnを引っ張ってくるケースが多そうなので、とりあえずアカウント名でソートされたColumnFamilyを用意するのか

<Keyspace Name="UserData">
  <ColumnFamily CompareWith="BytesType" Name="Accounts"/>
</Keyspace>

{ // Accounts
  "shn" : {
      "openid_identifier": "http://glucose.jp/shn"
  }, ...
}

て感じだろうか。さてOpenIDからアカウントを引っ張ってくる場合はどうするのだろう。新しくColumnFamilyを定義するか、

<Keyspace Name="UserData">
  <ColumnFamily CompareWith="BytesType" Name="Accounts"/>
  <ColumnFamily CompareWith="BytesType" Name="Identifiers"/>
</Keyspace>

{ // Identifiers
  "http://glucose.jp/shn" : {
      "account": "shn"
  }, ...
}

Accountsの中に特殊なRow Key(__identifiers__的な)を定義してその中に突っ込むかの2通りが思いつく。

{ // Accounts
  "shn" : {
      "openid_identifier": "http://glucose.jp/shn"
  },

  "__identifiers__" : {
      "http://glucose.jp/shn": "shn"
  }, ...
}

どっちが良いのかな。API的にはどっちもgetを2回呼ぶ事になるのだと思うけど、データはどのノードに配置されるのだろうか。

他のものを見てみるとtwissandraの場合前者の方法だ。こっちのBlogの例題の場合後者に近い(ちょっと用途違うけど)。 Delicious clone in cassandraの例はどうやってデータひっぱってくるのかさっぱりわからない。

DataModel - Cassandraw Wikiによると

The row key is what determines what machine data is stored on. Thus, for each key you can have data from multiple column families associated with it. However, these are logically distinct, which is why the Thrift interface is oriented around accessing one ColumnFamily per key at a time. (TODO given this, is the following JSON more confusing than helpful?)

keyによってどのノードにデータが保持されるか決まるそうなので、後者の方法だと特定のマシンにOpenIDのindex情報が固まる事になる。ここらへん、例えば「インデックス用のCFは全ノードに配置したい!」とか邪な事を考えた場合に、カスタマイズしたReplicationStorategyを書けば良さそうなところがCassandraの強みかもしれん。

Lazyboyさん

PythonのCassandra高級インタフェースであるところのLazyboyさんはViewという仕組みがあるようだ。

Lazyboy's view classes provide a way to build secondary indexes into a column family. For example, you might create a view that has only Lazyboy authors from the Users table:

というわけで、IdentifiersというViewを定義してそこのappendしたら良いかな?とやってみたら

{ // Identifiers
    "shn": "shn"
}

というCFができて泣いた。ViewのKeyを決定する関数が

def _record_key(self, record=None):
    """Return the column name for a given record."""
    return record.key.key if record else str(uuid.uuid1())

となっているため、User(key=shn)を突っ込むとViewのkeyもshnになっちゃう… ソース読んでて思ったのだけど、このViewはタイムライン的な奴を実装するためにあるようなので、僕のやりたい事とはちょっとずれているな。

というわけで

というわけで、玉虫色のエントリになったCassandra修行二回目。 その他参考にしたりしたページ

Cassandra修行

written by shn, on Mar 15, 2010 11:10:00 PM.

日曜大工はPython3kにしようか > Python3kはMySQLドライバ無いのか > じゃあCassandra使ってみるか > thriftもPython3k無いじゃん > じゃあ2.6でCassandraか、という経路によりCassandraにトライしてみることにしました。

修行中の身である故、情報の正確性は無担保。 ためしたCassandraは0.5.1

Cassandraとは

Cassandraとは、もともとFacebookが開発して、現在はApacheでホスティングされている分散データベース。最近、TwitterがMySQLから移行したことで話題になった。

いわゆるNoSQL(この呼び方は好きじゃないけど)のジャンルに属するデータベースで、データモデルの表現力的には DataStore(GAE) > Cassandra > Key-Value Storeぐらいのものなんじゃないのかなと思う。

動かしてみる

DLしてみたらJavaだった。Javaなんて大学のレポートいらいですね。あれもJythonで書いてJavaコードにコンパイルして出した気がする。とりあえずシングルノードで動かしてみようと思って

bin/cassandra -f

してみたら、いくつかライブラリが足りなかった(主にログ関係?)。 slf4j-log4j12-1.5.11.jar, slf4j-api-1.5.11.jar, commons-collections-3.2.1.jar, google-collect-1.0.jar, log4j-1.2.15.jarを足してもっかい起動。拍子抜け 。-fはフォアグラウンドで起動するオプション。

Cassandraのデータモデル

CassandraのデータモデルはプロジェクトWikiの他に、PythonのライブラリであるLazyboyや、その作者のブログに詳しい。

特に三つ目のがわかりやすいので、いいんじゃないでしょうか。

Cassandra用語

CassandraのDataModelはKeyspace > (Super)ColumnFamily > Columnという構成になっている。 RDBMSで言うと、Keyspaceは"データベース"、ColumnFamilyは"テーブル"ってぐらいのポジションではないかと。

Column

Columは最小のデータ単位。name, value, timestampのタプル。

{
  "name": "emailAddress",
  "value": "foo@bar.com",
  "timestamp": 123456789
}

timestampはとりあえずスルーしておいていいらしい。name, valueについてはバイト列として扱う。

ColumnFamily

ColumnFamilyは、keyで識別されるRowの固まり。各Rowの中身はColumnのマップになっている。Rowは常にソート済みで保持されている。 ColumnFamilyの定義 - Row keyの評価法(バイト列として評価するか、utf8文字列として評価するか)や、レプリケーション - はstorage-conf.xmlで定義する。

ColumnFamilyのソート順

インデックスが無い代わりに、ColumnFamilyが常にソートされているっていうのがCassandraのユニークな点ではないだろうか。RSS Reader/SNS/Blogのような、昨今のウェブサービス市場を占める「なんかのタイムラインを表示する」アプリケーションに特化している(まだ実際にさわってないからようわからんけど)

ColumnFamilyのソート順はBytesType, UTF8Type, LexicalUUIDType, TimeUUIDType, AsciiType, LongTypeがある模様。 BytesTypeAsciiTypeの違いは後で調べる。 LongTypeは64bit integerと扱うようだ。64bit以上の値とかを入れたらどうなるんだろう。バイト列としてセットするのだろうか。

LexicalUUIDTypeは128bitのUUIDとして評価するのかな。多分、想像だけどLazyboyなんかのライブラリがオブジェクトを追加するときにUUIDを自動生成したりして、keyを決めるのがめんどいときに便利なんじゃないだろうか。

TimeUUIDTypeはTime UUID(ver1のUUIDのことかしら。ようわからん)として評価する? LongTypeと比べたときの利点は、timestampが被っても大丈夫ってあたりなんだろうか? 上にあげたブログ記事に、ブログ記事のタグごとのタイムラインをTimeUUIDTypeで管理する例が載っている。

SuperColumnFamily

SuperColumnFamilyはColumnFamilyのSuperColumn版。SuperColumnとはValueの代わりにColumnのリストが入っている。

TODO

だんだん書いていて飽きてきた。ただ、書くと再確認になってよいですね。ColumnFamilyのどこがソートされているのかについてアヤフヤだったのがわかった。

日本語の情報あまりないなーと思っていたら、id:dannさんが書いていた。幅広く解説してある。紹介されていた、Twitterのデータ構造をCassandraでやってみる例(Twissandra)は後で見よう。

TODOとしては

  • Twissandra見る
  • コード書いてみる
  • Replication, Partitioning, Consistencyあたりを理解する。

あたりだろうか

wafのgaim daemon

written by shn, on Feb 27, 2010 10:53:00 PM.

ビルドツールをwafに代えたのだが、そこにあるテキトーなautomatic buildコードが、FreeBSDで使えるFAM系?のライブラリのgaimに対応していなかったので書き散らかした。 せっかくなのでアップします。

http://gist.github.com/316701

./waf daemonでイケルと思う。_famはうまく動かなかったのでイロイロやってみたけど諦めた。

eventlet kqueue hub

written by shn, on Jan 29, 2010 2:14:00 PM.

先日のAppleのイベント用にこんなのを作って、動かしたりしてたいました。

中身はテキトーに書いたeventletベースCometサーバをFreeBSDで動かしてるのですが、eventletはkqueueのhubが無いのでちょっと負荷が心配になった。ので、kqueueのhubをでっち上げた。例によってテストは無いけど、一晩動いてたのでこの方向でいいのかしら。

http://gist.github.com/287764

kqueueバージンだったので、誰か優しく手ほどきしてください。

eventletでCometサーバ

written by shn, on Jan 17, 2010 6:05:00 PM.

eventletづいてるので、Cometサーバを書いてみたよ。

tinycomet.py at master from shnjp's tinycomet - GitHub

eventlet.wsgiを使って、200行ぐらいで簡単なのが書けました。楽しいですね。

使い方

python tinycomet.pyで起動して、 /update/<UUID>にPOSTするとデータを作成

% curl -v 'http://127.0.0.1:8090/update/hogehoge' -d mogemoge
...
< HTTP/1.1 201 Created
< X-Tc-Timestamp: 2
< Content-Length: 0
...

受け取るには、/wait/<UUID>

% curl -v 'http://127.0.0.1:8090/wait/hogehoge'
...
< HTTP/1.1 200 OK
< Content-Type: application/octet-stream
< X-Tc-Timestamp: 4
< Content-Length: 7
...
mogemoge

waitにsince=<timestamp>をつけると、その時間になるまで待ちます。 各レスポンスのヘッダにあるX-TC-Timestampが、そのデータの最終更新時刻になります。 timestampは論理時間なので、実世界の時刻とは関係ありません

% curl 'http://127.0.0.1:8090/wait/hogehoge?since=4' &
[1] 97027
# waitに入る
% curl -v 'http://127.0.0.1:8090/update/hogeoge' -d fugafuga
fugafuga[1] + done ....

ほかに、waitにtimeout=[timeout:float]を与えると、timeout秒待って408 Request Timeoutを返します。

また、updateにfinished=1を与えると、次のwaitが終わった後にデータを消します。またcontent_type=[mimetype]でContent-Typeを指定できます。デフォルトはapplication/octet-stream

いろいろ引っかかったところ

event(pyevent)はだめくさい

eventletのdefault hubからthread safeじゃないという理由でpyeventが外されていたのだけど、今回はsingle threadなので試してみた。 しかしうまくimport出来ないので諦め。trunkも、配布バージョン(2007年!!)もだめじゃった。

python2.6からkqueueが使えるそうなので、自分でHubを書くのがよいのかな。eventlet自体もtrunkは結構変わっているので見ないといけなさげ

WaitForMultipleObjectsがやりたい

waitのところは、グローバルなConditionを作って

# wait側
with _update_lock:
    while data.last_update <= since:
        _update_lock.wait(timeout)

# update側
with _update_lock:
    _update_lock.notify_all()

とやっているので、捌くdataの数が増えると破綻すると思う。dataごとにイベントを持たせて、WaitForMultipleObjects()的なことがしたいのだけど、この枠組みではどうやるんだろう。オシエテエライヒト

/wait_multiはmultipart/*を吐くコードを書くのが面倒なので実装していない。

eventletでgearman worker

written by shn, on Jan 16, 2010 8:45:00 PM.

eventletっていうライブラリが僕好みの匂いを発していたので遊んでいる。 gearmanを使って、ブログのURLを与えたらそこのFeedのリストを返すってWorkerを書いていたので、それをeventlet化してみよう。

まず、gearmanのeventlet化、gearmanのライブラリにはpure-pythonのやつを使っていたのでさくっと行けた。

green_gearman.py

from eventlet import patcher
from eventlet.green import socket, select

for mod in ['worker', 'connection']:
    patcher.inject('gearman.%s' % mod, globals(),
        ('socket', socket),
        ('select', select)
    )
del patcher

eventletにはpatcherというざっくりしたモジュールがあって、これを使うと指定したモジュールのglobalsを置き換えてくれる。これでいいんかなっていうぐらい簡単。

試しにecho serverを書いてみる。

from eventlet import coros, api
from green_gearman import GearmanWorker

def echo_func(job):
    print api.getcurrent(), job.arg
    time.sleep(5.)

def main():
    pool = coros.CoroutinePool(max_size=200)
    waiters = []

    for idx in range(10):    
        worker = GearmanWorker(['0.0.0.0:4730'])
        worker.register_function('echo', echo_func)

        waiters.append(pool.execute(worker.work))

    for w in waiters:
        w.wait()
main()

gearmanのprotocolは同時に1ジョブしか処理できないっぽいので、とりあえず10個ぐらい突っ込んでみる。 gearman -h localhost -p 4730 -s -f echo hogehogeを20個ぐらい飛ばしてみた所、

<greenlet.greenlet object at 0x804137f30> hogehoge
<greenlet.greenlet object at 0x804127c90> hogehoge
<greenlet.greenlet object at 0x804137db0> hogehoge
<greenlet.greenlet object at 0x804137e10> hogehoge

ってなってるので、なんかまぁそれっぽくなってる気がする。こんなコードじゃよくわからんな… これで良いのでしょうか。

さて肝心のWorkerもこれにのせたいのだが、urllib2を使っていたので、代わりにfrom eventlet.green import urllib2に代えるだけっぽい(まだやってない…)。

んでつらつらコード見てたんだが、今のところ(eventlet 0.9.2)はlibeventサポートがdisabledされているようで、ちょっとがっかり。

eventlet化する場合、全てのIOをこれにしないとブロックが発生してみんな止まることになっちゃうのだけど、MySQL周りとかをeventlet化するのはめんどくさそうなのでアレでアレしてアレですね。

CSSツールをうpした。 - スプライト作成、ネスト、変数

written by shn, on Nov 14, 2009 4:35:00 PM.

Python Hack-a-thonに来たので、昔作ったツールのメンテをしたりしてました。 んで、githubにうpした。

githubのプロジェクトはこちら: github cssc.git

  • YAMLで段組を定義して、CSSスプライトを生成する make_sprite.py
  • IE6のためにPNGをなるたけ綺麗にGIFにする to_gif.py
  • CSSの記述に、ネストや、変数などを使えるようにするコンバータ cssc.py

で構成されてます。

make_sprite.py

YAMLの定義ファイルからCSSスプライトを生成します。 CSSスプライトっていうのは、GoogleのCSSスプライトのような奴で、複数の画像を一枚にまとめて、リクエスト回数、ファイルサイズを節約します。

CSSスプライトを使用すると、タグの代わりに<div>などの要素にbackgroundを指定することのなります。 CSSスプライトの中で画像の座標を調べるのは面倒なので、そこらへんの生成はcssc.pyを通して自動化しています。

使い方

make_sprite.py -o sprite.png -c sprite.json -b ./images/ sprite.yaml
  • -o sprite.png 出力CSSスプライト名
  • -c sprite.json 座標定義ファイル。あとでcssc.pyで使います。
  • -b ./images/ 画像のベースディレクトリ
  • sprite.yaml 画像の段組を記載したyaml

yamlの記法

段組はyamlで記述します。

冗長に書くと、以下のような感じです。

direction: vertical
images:
  - direction: horizontal
    images:
      - left-arrow.png
      - google.png
      - right-arrow.png
  - direction: horizontal
    images:
      - google-large.png
      - direction: vertical
        images:
          - down.png
          - up.png
          - close.png

基本的に directionとimagesのマップのネストで段組を表現します。directionは horizontal もしくは vertical 、imagesは画像ファイル名もしくは、マップのリストになります。

directionを省略して、直接子イメージのリストを書くことも出来ます。その場合の方向は、親の方向と逆になります。 この省略記法を利用すると、上のyamlは以下のようになります。

direction: vertical
images:
    - [left-arrow.png, google.png, right-arrow.png]
    - [google-large.png, [down.png, up.png, close.png]]

to_gif.py

pngをgifにします。

使い方は to_gif.py in.png out.gif 

cssc.py

CSS手書き派は日々、ネストができないことや、変数が使えないことに憤慨しています。

つまりこういう書き方がしたい!

{% set header_height = '24px' %}
{% set main_color = '#001122' %}

#site-header {
    height: {{ header_height }};

    h1#logo {
        font-size: 24px;
        color: {{ main_color }};
    }

    a {
        color: #aabbcc;

        _:hover {
            border: 1px solid #ffeedd;
        }
    }
}

OK! それなら、そうすればよいわ

./cssc.py -o out.css in.cssc

in.csscにみょうちきりんなcssもどきを書くと、in.cssにちゃんとしたものをはいてくれます。

上のmake_sprite.pyで、座標ファイルを出力した場合は以下のような感じで使います。

./cssc.py --coords main,http://static.com/main.png -o out.css in.css

--coordsオプションに
スプライト名:スプライトのURLを指定します。 座標はスプライト名.jsonから読み込みます。

csscファイルの方は、以下のように。冗長

h1#logo {
    {{ sprite_background('main', 'logo.png') }}
}

sprite_background関数の第1引数にスプライト名(コマンドラインで指定する奴)、第2引数に元画像ファイル名を指定します。

cssc詳細

{% set width = '100px' %} /* 変数などはJinja2そのまんまなので[Jinja2のドキュメント](http://jinja.pocoo.org/2/documentation/)を見るのが良いと思います。 */
// C++スタイルのコメントも使えます
{# jinja2スタイルも使えるぜ... #} 

div {
    /* 普通にcssのスタイル定義を書きます */
    border: 1px solid black;
    margin: 4px 8px;

    width: {{ width }}; // これはjinja2

    span {
        /* ネストすると "div span" になります。 */
        font-weight: bold;
    }

    a {
        text-decoration: none;

        _:hover {
            // 親要素とスペース無しで続け対場合は '_' を使います。
            // この場合、このブロックは "div a:hover" になります。
            text-decoration: underline;
        }
    }
}

あとがき

  • いまどきCSSを手書きしている奴はいるのだろうか
  • でもYUI compressorとか使うならMakeで処理できるようにしておきたいよね。
  • みんなここらへんの地味な所どうしてんだろう。
  • PyParsingはじめてトライしてみたんだけど遅い… たぶん文法定義が悪い

FAPWS3でSet-Cookieが出来ないっていう

written by shn, on Aug 10, 2009 2:58:00 AM.

今日は残ってる仕事をしに会社に行ったはずが、Zineのpluginとか作って遊んでたら一日が終わった。 そんななかで子一時間はまったのが、FAPWS3がSet-Cookieヘッダを食っちゃうって話。

FAPWS3駆動にしたZineでadmin panelにログインできないなー、おかしいなーと奮闘してたらコレが原因だった。

Unable to set cookie with Fapws3-0.2 not Set-Cookie headers send :(

fapws.baseのstart_responseが、Set-Cookieを食っちゃってるのが原因なので、cookie関連のコードを全部消しちまえばちゃんとzineが動くようになる。

なんでstart_response.set_cookie()なんてメソッドが用意されてんのかよくわからないけど、作者の勘違いかな。

以下パッチ:

--- base.py 2009-08-09 19:37:49.587425000 +0900
+++ fapws_base.py   2009-08-09 18:38:08.873789000 +0900
@@ -1,17 +1,13 @@
 # -*- coding: utf-8 -*-

 import datetime
-from Cookie import SimpleCookie, CookieError
 try:
     import cStringIO as StringIO
 except ImportError:
     import StringIO
 import traceback, sys, string

 status_reasons = {
     100: 'Continue',
@@ -90,7 +86,6 @@
         self.status_reasons = "OK"
         self.response_headers = {}
         self.exc_info = None
-        self.cookies = SimpleCookie()
         # NEW -- sent records whether or not the headers have been send to the
         # client
         self.sent= False
@@ -111,36 +106,11 @@
         key=str(key)
         val=str(val)
         self.response_headers[key]=val
-    def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=None):
-        self.cookies[key] = value
-        self.response_headers['Set-Cookie'] = self.cookies
-        if max_age:
-            self.cookies[key]['max-age'] = max_age
-        if expires:
-            if isinstance(expires, str):
-                self.cookies[key]['expires'] = expires
-            elif isinstance(expires, datetime.datetime):
-                expires = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
-            else:
-                raise CookieError, 'expires must be a datetime object or a string'
-            self.cookies[key]['expires'] = expires
-        if path:
-            self.cookies[key]['path'] = path
-        if domain:
-            self.cookies[key]['domain'] = domain
-        if secure:
-            self.cookies[key]['secure'] = secure
-    def delete_cookie(self, key):
-        if self.cookies:
-            self.cookies[key] = ''
-        self.cookies[key]['max-age'] = "0"
+
     def __str__(self):
         res = "HTTP/1.1 %s %s\r\n" % (self.status_code, self.status_reasons)
         for key, val in self.response_headers.items():
-            if key.upper() != "SET-COOKIE":
-                res += '%s: %s\r\n' % (key,val)
-        if self.cookies:
-            res+=str(self.cookies)+"\r\n"
+            res += '%s: %s\r\n' % (key,val)
         res += "\r\n"
         return str(res)

SQLAlchemyメモ

written by shn, on Aug 5, 2009 1:49:00 PM.

毎回ググるので、メモっておく。 このブログがSEOされて、このエントリが上位に来れば、次ググる時楽になる寸法だ!

INSERT IGNORE INTO ...

insert(..., prefixes=['IGNORE'])

SELECT ... FOR UPDATE

select(..., for_update=True)

随時更新したい。

WebShot 2009

written by shn, on May 21, 2009 10:09:00 PM.

昨日の夜中に突然ウェブページのSnapShotを撮るサーバが欲しくなった。SBMのサイトによくある奴。FreeBSD / Linuxでやりたいのよね。

先人の知恵を探すのだが、良い検索語がわからない。「Webnailと呼ばれているらしい」ということでGoogleさんに聞いてみたら「もしかして: Webmail」とか言われて死にたくなった。結局 "Webpage screenshot"とかで良かったよ。

んで探すと良く出てくるのがXvfb + firefoxの組み合わせ。2006年のSimpleAPIの時にみなこぞって作ってたらしく、その頃の話が出てくる。 この方法は、Firefoxを使っているので、いろいろな通知系を殺さないといけないのと、そもそもonLoadのイベントが取れないからsleepを適当にやってからScreenshot撮るってのがぜんぜんだめ。

んでもうちょっと探すと、Xvfb + QtWebKitを使った話がでてきた。これだとonLoadのイベントが取れる。

試してみたら、かなり簡単に撮れて拍子抜け。PortsでXorg + Qt一式のビルドに3、4時間かかったけどな!

元コードをちょっと弄って、scrollbarを消してみた

frame = self.page.mainFrame()
# hide menu bar
frame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
frame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)

縦長のScreenShotを撮るには baseSize = QSize(800, 600) # まず基準となるサイズにする self.page.setViewportSize(baseSize) # contentSizeの方が多きかったら(スクロールバーが出てたら) contentSize = self.page.mainFrame().contentsSize() if contentSize.width() > baseSize.width(): self.page.setViewportSize(contentSize)

あと僕の環境だと、Qtの画像縮小は汚ないのでPILとかを使ったほうが良い。

これの前段にSquidとか挟むべきなんだろうな。まぁなにはともあれ、やりたい事はできた