methaneのブログ

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

MySQL Connector/C の現状

MySQL 8 が GA になってたときは MySQL がどういうつもりなのかいまいち分からなかったのですが、 8.0.13 リリースを機に改めて調べてみたらこんな感じでした。

  • 従来の MySQL Connector/C は単体提供されない。サーバーをインストールしたらついてくる。
  • MySQL Connector/C++ は、client/server protocol の方のAPIC++ しか対応していない。 X Dev API の方は C 言語でも利用可能な形式になっている。

MySQL :: Download Connector/C (libmysqlclient)

ところで、Windows 版クライアントを考えたときに面白いのが MariaDB Connector/C です。 MySQL Connecotr/C が Windows 版でも OpenSSL 同梱になった等、 static link でシングルバイナリを作る使い方がほぼ不可能になったのに対して、 MariaDB Connector/C は OpenSSL を使わずに WindowsAPI を使って sha256_password や SSL 接続に対応していて、開発版では caching_sha2_password にも対応しました。

MySQL Connector/C がどうなってるのか公式のアナウンスがなかったのと、 MySQL Connector/Python が site-packages 直下に OpenSSL とか MySQL client の DLL とかをブッ込んでくる強硬手段を取ってきたので同じことをするとコンフリクトするので、 mysqlclient-pythonWindows版の wheel の提供を中断していました。

MariaDB Connector/C であれば static link で DLL ブッコミ不要の従来どおりの提供ができそうなので、 caching_sha2_password 対応版が出たらそっちに切り替えるつもりでいます。

Python の repr(float) を高速化する

Python の repr(float) は、 float(repr(float)) が保証される最短の表現を返します。 実装には netlib の dtoa を使っています。

この dtoa より速い実装が世の中にはあって、 V8 なんかで使われているようです。

github.com

float の repr の実装をこの速いやつに入れ替えるパッケージが frepr です。 frepr.install()float.__repr__ を入れ替えてしまうので、 jsonエンコードなども速くすることができるらしい。

Python で float のデータを大量に扱う人には嬉しそう。

ISUCON 8 予選の Go 初期実装に見る初心者コード

会社のBlogにも書いたのですが、ISUCON 8 予選で負けてきました。

さて、 ISUCON の初期実装の定番として、初心者が書いたようなSQLやコードになっている点が挙げられます。

今回の Go の初期実装もその定番にもれず、初心者がやりがちな、Goの良さを殺してしまうコードがありました。

今回負けた反省点の一つとして、アプリの書き換えを二人でやっていたのでコンフリクトを恐れてそのリファクタリングを怠ったというのもあります。 Goで本戦に参加されるチームの方にはぜひこれを克服してもらいたいと思います。

アンチパターン: 長い無名関数

例: https://github.com/isucon/isucon8-qualify/blob/9d7890f5433bdaf2cec75b4cdf1ebd0d9a531281/webapp/go/src/torb/app.go#L404-L492

   e.GET("/api/users/:id", func(c echo.Context) error {
        var user User
        if err := db.QueryRow("SELECT id, nickname FROM users WHERE id = ?", c.Param("id")).Scan(&user.ID, &user.Nickname); err != nil {
            return err
        }

        loginUser, err := getLoginUser(c)
        if err != nil {
            return err
        }
        if user.ID != loginUser.ID {
            return resError(c, "forbidden", 403)
        }
... [以下数十行]

Sinatra 風のコードを移植しようとするとありがちなのですが、関数名が main.func8 とかになってしまって、Goの良さであるスタックトレースやプロファイルの使い勝手を大きく損ねます。Goでこういう無名関数の使い方はやめましょう。 (RubyJavaScript では良いのかという話もありますが、それはまた文化が違うので…)

例えば sort.Slice() にわたす比較関数のようにごく短く、中からDBアクセスなどの処理をしない関数は無名関数にしてもいいです。

また、1つの通常関数の中に無名関数が1つかせいぜい2つあるくらいなら、やはり害は少ないです。

スタックトレースとか flamegraph に main.func6 とか main.func8 とかがバラバラでるのはダメです。

リファクタリング: 通常関数への書き換え

無名関数全体を移動し、適当に名前をつけましょう。

ISUCON であれば事前練習で、なにか機械的命名規則を決めてしまうと命名に時間を取られずに済みます。 例えば上の "/api/users/:id" を扱う関数であれば、 handle_GetApiUsersId とかで良いです。Goの慣習の命名規則(アンダースコア使わない、 API とか ID とかは Api, Id にしない)からは大きくハズレますが、 ISUCON では悩まず機械的に作業できる利点の方が大きいです。

アプリの書き換えを複数人でやる場合、このリファクタリングは書き換え範囲が大きくてコンフリクトが厳しいので、真っ先にえいっとやってしまうと良いと思います。

アンチパターン: for rows.Next() ループ内でのクエリ実行

例: https://github.com/isucon/isucon8-qualify/blob/9d7890f5433bdaf2cec75b4cdf1ebd0d9a531281/webapp/go/src/torb/app.go#L236-L252

   rows, err := db.Query("SELECT * FROM sheets ORDER BY `rank`, num")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    for rows.Next() {
...
        err := db.QueryRow("SELECT * FROM reservations WHERE event_id = ? AND sheet_id = ? AND canceled_at IS NULL GROUP BY event_id, sheet_id HAVING reserved_at = MIN(reserved_at)", event.ID, sheet.ID).Scan(&reservation.ID, &reservation.EventID, &reservation.SheetID, &reservation.UserID, &reservation.ReservedAt, &reservation.CanceledAt)

こんな感じで、 rows.Next() ループの中でクエリを実行したり、クエリの実行を含む関数を呼び出すと、複数のDBのコネクションを利用してしまいます。

そしてもっと悪いことに、他の prefork 型のアプリではプロセス数を適当に絞ることができるのに対して、GoでDBのコネクションプールを絞ろうとすると上のようなコードでデッドロックの原因になります。デッドロックを避けるためにコネクションプールの上限を設定しないと、たとえCPUが2コアしかないようなMySQLサーバーに対して数百コネクションから並列でクエリを投げてしまい、遅いクエリがどれか解らないとか、MySQL側がデッドロックを誤検出するとか、いろんなトラブルの原因になります。

for rows.Next() ループの中ではそのクエリの結果のフェッチだけを行い、その結果の各行に対する処理は改めて別のループに書きましょう。

アンチパターン: 長い関数での defer rows.Close()

https://github.com/isucon/isucon8-qualify/blob/9d7890f5433bdaf2cec75b4cdf1ebd0d9a531281/webapp/go/src/torb/app.go#L404-L492

上のアンチパターンの亜種ですが、せっかく for rows.Next() ループ内からDBにアクセスする処理を排除しても、 defer rows.Close() を使っていると、そのクエリに使われたコネクションは関数が終わるまでコネクションプールに返却されません。

とはいえ、安易に defer rows.Close()for rows.Next() ループの後ろに rows.Close() の形で移動するのもダメです。 for rows.Next() ループ内に return が無いか確認しましょう。

リファクタリング: sqlx の利用

上のような問題の「まっとうな」リファクタリング方法は、1つのクエリを実行して結果をフェッチするまでを個別に関数に切り出すことです。そうすると defer rows.Close() が適切なタイミングで実行されます。

しかし ISUCON だといちいちそういったリファクタリングをしている余裕がないかもしれません。そこで sqlx を覚えておくと良いでしょう。

sqlx は、 database/sql の上位互換になっています。 sql.Opensqlx.Open に書き換えるだけで、そのままのコードが動きます。

そして、 db.QueryRow(...).Scan(&data)db.Get(&data, ...) に、 db.Query(...), for rows.Next() { var row Record; rows.Scan(&row); records = append(records, row) } のようなパターンを db.Select(&records, ...) に書き換える事ができます。

特に後者が強力で、 rows.Next()rows.Close() を排除することができるので、コネクションを無駄に大量消費する問題を楽に解決することができます。

それ以外にも便利機能がいくつかあるので、 database/sql を直接使った初心者コードをリファクタリングするときの強力な武器として練習しておくことをおすすめします。

hub コマンドの BDD がユーザードキュメントとして素晴らしい

OSSメンテナをしていると他人のPRやブランチをチェックアウトして何かを確認したいということは頻繁にあって、いちいち git remote add して fetch してってのが面倒なので Github 製の Github CLI クライアントである hub を愛用している。

でも hub コマンドって、ドキュメントがあまりなくて、 help コマンドの出力も最小限で、頻繁に使う一部の機能以外はほとんど使いこなせずにいた。

しかし、 hub コマンドが Cucumber を使って BDD をしているのを最近知った。「どういう仕組でBDDが動いているのか」は全くわからないけれども、 「hub がどういうコマンドを実行するとどういう動作をするのか」は凄くわかりやすい。

たとえば、 hub pr checkout コマンドの Behavior を見てみると、

Feature: hub pr checkout <PULLREQ-NUMBER>
  Background:
    Given I am in "git://github.com/mojombo/jekyll.git" git repo
    And I am "mojombo" on github.com with OAuth token "OTOKEN"


  Scenario: Checkout a pull request
    Given the GitHub API server:
      """
      get('/repos/mojombo/jekyll/pulls/77') {
        json :number => 77, :head => {
          :ref => "fixes",
          :repo => {
            :owner => { :login => "mislav" },
            :name => "jekyll",
            :private => false
          }
        }, :base => {
          :repo => {
            :name => 'jekyll',
            :html_url => 'https://github.com/mojombo/jekyll',
            :owner => { :login => "mojombo" },
          }
        },
        :maintainer_can_modify => false,
        :html_url => 'https://github.com/mojombo/jekyll/pull/77'
      }
      """
    When I run `hub pr checkout 77`
    Then "git fetch origin refs/pull/77/head:fixes" should be run
    And "git checkout fixes" should be run
    And "fixes" should merge "refs/pull/77/head" from remote "origin"
  1. mojombo/jekyll をチェックアウトしたリポジトリにいるときに
  2. hub pr checkout 77 を実行すると、
  3. hubが git fetch origin refs/pull/77/head:fixes を実行して、 (ここで "fixes" は上の Github API のレスポンスで判断していることがなんとなくわかる)
  4. git checkout fixes を実行してくれる

事がわかる。

例となるシナリオを用意して、どんな git コマンドを実行してくれるのかわかる。しらない git コマンドがあればそれは git のマニュアルで調べればいい。

コマンドごとにいくつかシナリオが用意されているので、 hub help コマンド名 するよりも、この feature ファイルを探して斜め読みするほうがずっと hub コマンドで何ができるのか具体的に理解できる。

Python 目線からの GAE/node.js Standard Environment 発表の解説

Google I/O 2018 で GAE/node.js Standard Environment が発表されました。

www.youtube.com

以下、「Python 3 早く来い!」の視点で注目点をピックアップしていきます。

9:00 頃から、 node.js Standard Environment が in a few weeks で登場すると発表

13:00 "idiomatic", You can use any module from the NPM registry you want. There is no API or language restriction. Go や Python みたいに特別な制限は無いようです。

13:33 GAE Standard のインフラの3つの新しい点を紹介していくよ。まずは "Faster than light" ビルド。 gcloud app deploy コマンドが差分アップロードするようになった。 サーバー側で npm install するんだけど、 package.json と package-lock.json に差分がないと npm install はスキップして前の node_modules をそのまま使うよ。

(発表内容から脱線)
さて、 pipenv のドキュメントの Community Integrations に "Mysterious upcoming Google Cloud product (Cloud Hosting)" があります。きっと GAE/Python 3 Standard は pipenv を使って、 npm と同じ "Faster than light" build をするんでしょうね。
(脱線おわり)

14:45 New runtime environment. スタックを上から見ていくと、 "Your code", "node_modules", "node.js" --- これはカスタマイズされてない, "OS packages" --- たとえば headless Chrome なんかがインストールされてる、 "Ubuntu". ってなってる。 node.js 以下は Google が勝手にアップデートする。

16:00 このスタックはサンドボックスで動いている。先週発表した gvisor だ。


今までの GAE Standard Environment は言語ランタイムとかライブラリにカスタマイズしてサンドボックスを提供していたので、言語の追加やアップデートがなかなかされないという欠点がありました。

node.js Standard から利用されている新ランタイムは、 gvisor で作ったサンドボックスのなかで、カスタマイズ無しの言語ランタイムが動くのが魅力です。利用できるライブラリもずっと増えるでしょうし、新しい言語が追加されたり新しいバージョンが利用可能になるのがずっと早くなることが期待できます。