私が歌川です

@utgwkk が書いている

ISUCON10 予選突破した #isucon

😇😇😇 (:innocent::innocent::innocent:) というチームで、 id:nonylene id:wass80 と参加して、24位でめでたく予選突破できました。学生枠を使わずに予選突破できたのは初めてです。チームメンバーのブログも読んでください。

nonylene.hatenablog.jp

wass80.hateblo.jp

記録に残ってる最終スコアは2204点でした。

やったこと

New Relic導入

まずNew Relicを入れて計測できるようにしよう、というのをやりました。echo向けにはechorelicっていうのがいいのか、と調べて導入しました。

github.com

エンドポイントにかかった時間は計測できたけど、分散トレーシングとか、SQLにかかった時間が取れてない!!! なんでなの!! と言いながらechorelicのコードを読みに行きました。どうもcontextが渡ってない予感がする? echoのミドルウェアを書くのか? と思いつつ echo.WrapMiddleware っていうのを使うとなんか書けそう、ということでガッと試しました。

github.com

最終的にこれでSQLの計測もできるようになりました。これを書いてたときがいちばん輝いてた気がする。

後述するように、 /api/.+/search が重たいけどどういうクエリで重くなるのだろうか、と思ってNew Relicのtrace IDとリクエストの対応をログから取れるようにしたりもしていました。結局分析には使わなかったですが……。

github.com

インデックス貼る

明らかに足りないインデックスがあるよね、ということで貼りまくりました。

椅子におすすめの物件ロジックちょっとよくする

椅子を直方体に見たててドアを通せるか書いてあるけど、短い2辺だけ見ればよいよね、ということになって実装しました。Goでのsortのやり方を調べるより先に、3つのint64のうち小さい2つを返す関数を自前で実装してて野蛮な感じがあります。sort.Ints()を使うと楽に書けるよね、というのを聞いて、たしかに、と思いました。

github.com

うまくインデックスを効かせられないので、せめてスキャンする行数は減らしたい、ということで、EXPLAINを見ながらFORCE INDEXするインデックスを決める、というのもやってました。

github.com

自前アクセスログ解析

/api/.+/search が重いのでインデックスを貼りたいけどなんかめっちゃクエリあるね、どうしようかな、と id:wass80 と会話してて、searchはGETリクエストなのでクエリパラメータの種類でどんな検索が多いか傾向がつかめそう、という話をしました。分析スクリプトを即興で用意して回した結果、大半のリクエストはクエリ1個なのでひとまずそこにインデックスを貼るとよさそう?? とか、 features はLIKE検索やばそうだけどそんなにリクエストされてないから後でいいか、という判断ができたのはよかったです。

use strict;
use warnings;
use feature 'say';
use URI;

while (my $line = <STDIN>) {
  my ($req) = $line =~ qr{"GET (.+) HTTP/1\.1"};
  my $uri = URI->new("http://example.com/$req");
  my %query = $uri->query_form;
  my @keys = grep { $_ ne 'page' && $_ ne 'perPage' } sort keys %query;
  say join ',', @keys;
}

perl nukidasu.pl < search.access.log | sort | uniq -c | sort -n のようにして使うとよい情報が得られました。

(snip)
     11 color,priceRangeId
     11 heightRangeId,priceRangeId
     17 features
    441 widthRangeId
    449 color
    491 kind
    521 depthRangeId
   1027 heightRangeId
   1461 priceRangeId

あとになって、alpとかkataribeとかを使うとこういう解析はすぐできるのかなと思いつつも、小さなスクリプトで大きな効果が得られたな、と思います。

やったけど導入しなかったこと

/api/.+/search のSELECTを1つにする

SELECT COUNT(*) したあとに同じ条件でSELECTしてるのを1つにまとめられないか、ということで実装してみましたが、アプリケーションの互換性チェックに落ちたのでマージしませんでした。

github.com

MaxOpenConnsを大きくする

なんか小さいので大きくしたい、という話をしたのですが、大きくしたらFAILするようになったので結局もとに戻しました。

所感

Ruby → Python → Goと利用言語が変化していきました。ついにGoで参加したのですが、コンパイルが通るからひとまずマージできるでしょう、というふうに進められたのはスムーズでよかったです。VSCodeのGo拡張がだいぶ体験がリッチというのもありました。

New Relicを初手で導入して、このエンドポイントが明らかに重いし、分散トレーシングを見るとN+1なので直しましょう、というのがコマンドなしで分析できたのは便利でした。

今回はDBの負荷がどうしても高くなるようなアプリケーションだった、と思っていて、ひととおり自分ができることをこなしたあとは、id:wass80id:nonylene の2人にMySQL複数台構成の実装をやってもらっていて、後半はずっと手があいてました。たとえばもうちょっと実装をちゃんと見て、 priceRangeId とかをなんとかする、とかやったほうがよかったかもしれません。

去年の反省を活かして、VSCode Remoteは重いので手元で開発しましょう、という方向にシフトしたのは、結果的に各自が並列で作業できるようになって効率が上がったのでよかったと思います。

今回のダッシュボードは、ときどき高負荷で見られなくなることがあったとはいえ、体験がかなりよかったと思います。ベンチマークを走らせてると、どんどん点数が上がってる様子が見れるのはおもしろかったです。とてもおもしろい問題だったと思います。SUUMOはときどき引っ越しする予定が無でもブラウジングしていて、なぞって検索機能なんかあった気がする!! と言っていました。

今年こそ優勝したいですね。

追記

旅行っていうタグを間違えてつけたのでアイキャッチ画像がおもしろい感じになりました。

関数の再帰深度を制限するデコレータ / おまじない再び

4ヶ月ぐらい前の話題だったけど、こういう感じで、関数の再帰深度を制限するデコレータは書けないのだろうか、とふと思った。@max_depth(10) なら同じ再帰呼び出し内で10回まで呼び出してよくて、10回を超えたらエラー。

@max_depth(10)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

fib(10) # => 55
fib(11) # => Error!!

なんかうまく書けないものかな、と試行錯誤してたら id:nonylene に教えてもらった。クロージャを使うと書ける。nonlocal を使うと modified のひとつ外のスコープの変数を参照できる。f の中でも結局wrapされた modified が呼び出されることになって、再帰呼び出しをするたびにどんどん count が大きくなる。

def max_depth(i):
    def d(f):
        count = 0
        def modified(*args):
            nonlocal count
            # print(args, count)
            count = count + 1
            if count <= i:
                result = f(*args)
                count -= 1
                return result
            else:
                count = 0
                raise Exception("Too many recursion!!!")
        return modified
    return d

@max_depth(10)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

フィボナッチ数とか計算しようとすると fib(10) までは計算できてお得。6行目のコメントを外すと引数と再帰深度の様子が見れます。

計算の様子を見る

(10,) 0
(9,) 1
(8,) 2
(7,) 3
(6,) 4
(5,) 5
(4,) 6
(3,) 7
(2,) 8
(1,) 9
(0,) 9
(1,) 8
(2,) 7
(1,) 8
(0,) 8
(3,) 6
(2,) 7
(1,) 8
(0,) 8
(1,) 7
(4,) 5
(3,) 6
(2,) 7
(1,) 8
(0,) 8
(1,) 7
(2,) 6
(1,) 7
(0,) 7
(5,) 4
(4,) 5
(3,) 6
(2,) 7
(1,) 8
(0,) 8
(1,) 7
(2,) 6
(1,) 7
(0,) 7
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(6,) 3
(5,) 4
(4,) 5
(3,) 6
(2,) 7
(1,) 8
(0,) 8
(1,) 7
(2,) 6
(1,) 7
(0,) 7
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(4,) 4
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(2,) 5
(1,) 6
(0,) 6
(7,) 2
(6,) 3
(5,) 4
(4,) 5
(3,) 6
(2,) 7
(1,) 8
(0,) 8
(1,) 7
(2,) 6
(1,) 7
(0,) 7
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(4,) 4
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(2,) 5
(1,) 6
(0,) 6
(5,) 3
(4,) 4
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(2,) 5
(1,) 6
(0,) 6
(3,) 4
(2,) 5
(1,) 6
(0,) 6
(1,) 5
(8,) 1
(7,) 2
(6,) 3
(5,) 4
(4,) 5
(3,) 6
(2,) 7
(1,) 8
(0,) 8
(1,) 7
(2,) 6
(1,) 7
(0,) 7
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(4,) 4
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(2,) 5
(1,) 6
(0,) 6
(5,) 3
(4,) 4
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(2,) 5
(1,) 6
(0,) 6
(3,) 4
(2,) 5
(1,) 6
(0,) 6
(1,) 5
(6,) 2
(5,) 3
(4,) 4
(3,) 5
(2,) 6
(1,) 7
(0,) 7
(1,) 6
(2,) 5
(1,) 6
(0,) 6
(3,) 4
(2,) 5
(1,) 6
(0,) 6
(1,) 5
(4,) 3
(3,) 4
(2,) 5
(1,) 6
(0,) 6
(1,) 5
(2,) 4
(1,) 5
(0,) 5
55


引数を取るデコレータみたいなのを書くのはちょっと難しくて、デコレータを作って返す関数、デコレータ、wrapされた関数、みたいな構造になるのが難しい。

デコレータは最初はおまじないに見えて、関数を取って関数を返す関数なのだけど、私はPythonを書き始めてからそれが分かるまでに少なくとも3年かかった。Flaskを使ってて @app.route('/') とか書くと思うけど、あれも最初はそういうおまじないだと思ってた。

デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:
def f(...):
    ...
f = staticmethod(f)

@staticmethod
def f(...):
    ...
用語集 — Python 3.8.6rc1 ドキュメント

おまじないの概念については昔いい記事を書いてたのでこっちも読んでください。高度に発展した科学技術が魔術と区別できない、みたいな感じで、原理が分からない間は不思議な力に見えるだろう、科学技術の全てを知らなくても生活はできるのでそれと同じようなものではないか、と受け止めている。それが気味悪いのであればやっていくしかない気もする。

blog.utgw.net

おまじない、というと、原義では不思議な力による云々らしいけれど、プログラミング言語におけるおまじないは、別に不思議な力でも神仏でもなくて、そういう機能がちゃんとあって、みたいな、ずっとおまじないのままにしておくのは何だかつらい感じがある。でもまぁ、正直なところ、おまじないがおまじないのままであっても、ある程度、ライブラリを読み込むおまじないとか、クラスっていう便利なかたまりが定義できるおまじないとか、そういう感じでも、ある程度は何とかなるかもしれないんだよなぁー、みたいな気分になっていて、うーん。

1年後も同じようなことについて話してた。

blog.utgw.net

入門するにあたって本質でないところ,たとえば少しやってくるとハマる可能性があるようなところは,入門の段階でやるのはまだ早いのではないか?? と思った. 我々が使いたいのはその言語とかライブラリであって,現状はこうすればいいという道筋があってやっているし,いちおうの言語の入門もある. 言語の入門では,言語が使えるようになることが第一であって,言語の詳細を知るのは第二なのではなかろうかと思う.

電話ごしに英語で会話してる。向こうが何を言っているのかはなんとなく分かるんだけど、こっちはなかなか英語が出てこなくて It is dificcult... みたいなことばかり言ってる。えーっと、あのー、と言ってたら、向こうに日本語が通じることが分かったので諦めて日本語で話すことにした。

落ちたテストを片っ端からtodoにして書き戻すTest2プラグイン

github.com

表題のものができました。

どういうことかというと、たとえば以下のようなテストを走らせると、

use strict;
use warnings;
use Test2::V0;
use Test2::Plugin::MakeFailedTestTODO;

ok 0;
fail 'failed';
pass 'passed';

is 1,
   0,
   'multiline';

subtest 'foo' => sub {
    ok 0;
};

done_testing;

テストが終わった後には以下のような内容に書き換えられて、同じテストを2回走らせると、落ちたテストが全部todoになっているのでpassします。

use strict;
use warnings;
use Test2::V0;
use Test2::Plugin::MakeFailedTestTODO;

todo 'by Test2::Plugin::MakeFailedTestTODO' => sub { ok 0; };
todo 'by Test2::Plugin::MakeFailedTestTODO' => sub { fail 'failed'; };
pass 'passed';

todo 'by Test2::Plugin::MakeFailedTestTODO' => sub { is 1,
   0,
   'multiline'; };

subtest 'foo' => sub {
    todo 'by Test2::Plugin::MakeFailedTestTODO' => sub { ok 0; };
};

done_testing;

PPIを使って落ちたテストに該当する文をパースして、todoを組み立てて書き戻す、というのをやっています。なので、複数行のテストでも、インデントの感じはよくないけどちゃんとtodoにできています。

便利なグッズができたと思ったのですが、当初これを使って解決しようと思っていた課題に対しては、もうちょっと簡単で良いアプローチがあるというのに気づいたので供養という形になります。今のところCPANizeの予定はありません。

辛子高菜うまい

三条のずんどう屋っていうラーメン屋に行って辛子高菜をいっぱい食べたので今日は満足した。辛子高菜、こっちに来てからなかなか食べる機会がなくて、ありえないほどにおいしいけどその味をしばらく忘れていて、今日ついに思い出した。こんなに無限に食べられるものはなかなかない。

r.gnavi.co.jp

辛子高菜うますぎた

辛くておいしいものが大好きで、辛っしゃいませが閉店したことをずっと悲しんでいる。

kaenkaramen.com

  • ばいきんまんが音楽科の教師をしていた
  • だんだん周囲の人々がハイになっていったので、これはおかしいぞ、と思って隙を見て音楽室を抜け出す
  • スマホアプリのアイコンが全部デフォルトアイコン? になっていて、アプリを入れなおそうと思ってApp Storeを開いたけど、いま自分が使っているのはAndroidということを思い出した