methaneのブログ

このブログに乗せているサンプルコードはすべてNYSLです。

PyMySQL 0.6 がリリースされました

PyMySQL のコントリビュート始めました という記事を書いたころ、ちょうど PyMySQL のメンテナが活動できなくて開発が停滞していたので新しいメンテナを募集していました。

僕はその募集に気づいていなかったので立候補もしていなかったのですが、幸運なことにアクティブなメンテナが立候補してくれ、無事引き継ぎもされました。 新しいメンテナにお願いして、リポジトリを PyMySQL Organization で管理した上で、僕もコミッタにさせてもらいました。

というわけで新しいリポジトリのURLはこちらになります。 https://github.com/PyMySQL/PyMySQL/

履歴を見てもらえると判るのですが、凄まじいペースで Issue を消化して、今日0.6がリリースされました。

大きい変更点としては次のとおりになります

Python 3.3 以降にコード変換なしで対応

以前は Python 3 に対応したバージョンは PyMySQL3 という名前で PyPI に登録されていましたが、 今回からは Python 2 でも 3 でも PyMySQL という名前のパッケージを使ってください。

Python 3.2 以前は公式にはサポートしていませんが、テストコードの中で u'文字列' リテラル (Python 3.3 から Python 2 と共通のコードを書きやすくするために導入された) を使っているだけで、本体は多分 Python 3.2 でも 動くのではないかと思います。

特に Python 3 でバイト列を % 方式のフォーマットで扱えずに PyMySQL 0.5 ではサポートできていなかったのですが、 0.6 では hexstring を使い x'DEADBEEF' という形にすることででなんとか対応することができました。

チューニング

MySQL のプロトコルはパケットベースなのですが、例えばSELECTクエリを実行した場合、カラム1つずつ、行1つずつに対して パケットが割り当てられています。 参考

そのため、1クエリを実行するにも沢山のパケットを受信しないといけないのですが、実際にはパケットはまとめて送られてくるので、 TCPからの受信も1パケットずつではなくてバッファリングしながら行ったほうが速いです。

ですが、下手に Python 上でバッファリングしようとすると余計に遅くなります。 特に Python 2 の socket.makefile() が返す socket._fileobject オブジェクトは、 Pure Python なのに 古い Python に対応した遅い実装のままで、めちゃくちゃ遅いです。 低レベルの _socket.makefile() は、内部で fdopen して本物の file object (C言語FILE* を ラップしたオブジェクト。 Python 3 では廃止された) を作っているので、 fd を浪費するとか timeout 設定が効かなくなるとか 弊害も大きいです。

幸運なことに、Python 2.7 では io モジュールが C 言語で書き直され、 Python 3.2 と高い互換性を持っているので、 Python 3.2 用の socket.makefile() をバックポートすることで高速化に成功しました。 Python 2.6 では io モジュールも Pure Python なので、諦めてバッファリングしていません。

その他、ボトルネックになるパケット解析周りのコードをリファクタリングして高速化しました。 ちなみに、ソケット周りの高速化する方法を考えるときは、 gevent monkeypatch ができるという大きな魅力を損なわない 方法を前提に指定ます。

CPython ではまだまだ libmysqlclient ラッパーである MySQL-python より遅いですが、 普通の Web アプリではボトルネックにならない性能が出てると思います。 PyPy では MySQL-python よりも速いです。

server_status を使った正確な Autocommit / エスケープ

MySQL のプロトコルを調べていてわかったのですが、 OK パケットに含まれる server_status の ビットフィールドを読めば、AUTOCOMMITがどうなっているかとか、 NO_BACKSLASH_ESCAPES になっているか どうかなどが取れます。

これらは、サーバー側の my.cnf などで設定したり、文字列の SET コマンドをクエリとして投げたりするとドライバ側からは 設定が判らなくて不必要なパケットを送ったり間違ったエスケープをしてしまうケースが有ったのですが、 ドライバ内部で状態を管理するのではなくてこのビットフィールドを参照することで正確な動作が可能になりました。

その他 MySQL-python との互換性向上や大量のバグ修正

Django のテストセットを MySQL-python を PyMySQL に置き換えて全てパスしたり、16MBを超えるパケットへの 対応など、大量の Issue を消化しました。

あまりにも修正が多岐に渡るので、実績という意味での信頼性はまたこれから積み上げて行けないと思いますが、 複数のコミッタがIssue/PRを適切に処理する流れはこの3週間でできたと思います。