ISUCON6予選にチーム「試運転」で参加しました

isucon.net

私と@menphim, @yurahunaで予選2日目に参加しました。

事前にやったこと

使用する言語の選択では,メンバー全員が研究で多少は使っていることから,Pythonを選択することに決めました。過去のISUCONでの本選出場者数から考えるとPythonはやや分が悪いっぽいのですが,普段Web系のプログラミングをしていない勢のチームだったので使い慣れた言語の使う方が大切だと思ったからです。

練習はISUCON5の予選でやりました。「MySQLってどうやってログインするの?」とか「バックアップってどうやるの?」というところから始めないといけなくて時間がかかりましたが,数日かかって21000点くらいに到達して「私なんだかいける気がしてきた…!」(ここに青葉ちゃんの画像を貼る)となりました。

ISUCON5はMySQLのクエリ改善が重要だったのですが,それ以前のISUCONではキャッシュやパラメータ設定が重要だったりしたこともあったようなので,Redisのチュートリアルをやったり,NginxやMySQLやGunicornの秘伝のタレを作ったりしておきました。

予選本番

問題のアプリケーションの内容

はてなキーワードのようなアプリでした。

d.hatena.ne.jp

キーワードに関する記事を登録,編集,削除(実はベンチマークには削除リクエストは含まれていないらしい?)ができて,登録されているキーワードが記事の中に出現したときには <a href="...">...</a> で囲む必要があります。

同一箇所に複数のキーワードがマッチした場合には(出現位置,キーワードの長さ DESC)という順番で優先されます。例を以下に挙げます。

また,キーワード記事には「はてなスター」のような機能があり,お星様をつけることができます。

序盤

初手でNginx, MySQL, Gunicornの設定を秘伝のタレに書き換えました。

ただ,この時点で動作確認をすると500が帰ってきて大変でした。

まずNginxとGunicornの間をUnix domain socketで接続するように変更したら,エラーが出てしまいました。ログを確認するとなぜか urllib の文字が。今回のアプリケーションはキーワード記事を管理するisudaというアプリケーションと,はてなスターを管理するisutarというアプリケーションに分かれており,相互にHTTPで情報をやり取りするマイクロサービスのような構成になっていました。isudaとisutarの接続ができなくなってしまったことでエラーになっていたようなので,とりあえずUnix domain socketでの接続を無効にすることにしました。

他にもGunicornのworker classをMeinheldに変更したら謎のエラーが起こったり,/etc/mysql/my.cnfに貼ったシンボリックリンク先の設定ファイルが読み込まれなかったりして,まともに動くようになった頃には13:00頃になっていました…

設定が終わったら何かアプリに加える前にプロファイルを取ると心に決めていたので,kataribeで実行時間の多いパスを探すことにしました。合計実行時間は /, /login, /keyword が上位でした。

他には,mysqldumpslowでスロークエリを探してみると,スロークエリらしいクエリは何も見つかりませんでした。ベンチマーク中にtopコマンドを見てもわかるように,今回はSQL勝負ではないようです。

中盤

load_starsとhtmlifyの2つのメソッドが遅いことが分かったので,この2つを中心に改善することにしました。

load_stars の中身はisudaからisutarにHTTPリクエストを投げて「はてなスター」の情報を取ってくる処理でした。HTTPリクエストがボトルネックっぽいのでisudaとisutarのマイクロサービスをぶち壊してモノリシックな構成にすることにしました。isutarの実装がとても軽かったので,isudaにisutarを取り込みました。

続いてhtmlifyの中身は記事中に出現したキーワードにリンクを付ける処理でした。正規表現よりはAho-Corasickのほうマシな気がしたので,ライブラリを探してきて検証をしていました。

終盤

ブラウザ上での動作確認では正しく動いているっぽいのに,ベンチマークにかけるとFAILする現象が発生していてデバッグが大変だったのですが,その原因が分かりました。いつの間にか,ブラウザで閲覧しているIPアドレスが,以前ISUCON5の練習で使っていたインスタンスIPアドレスになっていました。しかもそのIPアドレスでは,別のどこかのISUCONチームのインスタンスが立ち上がっていたので間違いに全く気付きませんでしたw

なんとかAho-Corasick化できたのでベンチマークを実行したのですが,スコアほとんど上がらず…

結果

11,000点くらいでした。キャッシュ戦術重要ですね。

学んだこと / 反省したこと

MySQLのmy.cnfのシンボリックリンク問題

my.cnfの実体をGitのリポジトリ下において/etc/mysql/my.cnfにシンボリックリンクを置くことにしていたのですが,なぜか設定ファイルが読み込まれない現象に遭遇しました。これはAppArmorが関係しているようです。

serverfault.com

実は「ISUCON AppArmor」でググると過去の犠牲者がたくさん見つけられます。 ISUCONなら apt-get purge apparmor も考慮するべきですね。

Meinheld

Web Framework Benchmarksや過去のISUCON参加者でGunicornのworker classにMeinheldを使ってスコアを改善したという事例があったので,練習の時に試してみると実際1000点くらい上がったので今回も使ってみました。

FrameworkBenchmarks/gunicorn_conf.py at 497d7bb95d18f00350686c08bed9e117730e1a97 · TechEmpower/FrameworkBenchmarks · GitHub

orangain.hatenablog.com

理由はよくわからないのですが,URLに日本語が入っている場合に落ちてしまいました。以下のコードで落ちてるっぽいので後で調べます。

werkzeug/routing.py at 9ab649fdc225037162a9d29be08648249c4588ab · pallets/werkzeug · GitHub

werkzeug/_compat.py at 9ab649fdc225037162a9d29be08648249c4588ab · pallets/werkzeug · GitHub

開発用の環境の整備

手元でソースコードを編集して git push したあとにサーバー側で git pull することでデプロイしていたのですが,問題が発生した時には git reset --hard HEAD して git push -f するダメなワークフローだったので,今度はもう少しまともなワークロフローを考えたいです。

ログの取り方

Gunicornでログを出力する方法がわからず with open('/tmp/foo.log) as f: printf(message, file=f)` でデバッグしていたのですが,さすがに効率が悪すぎるのであとでやり方を調べます。

MySQLのオプション

MySQLへの接続で以下のようなコードが書いてありました。

cur.execute("SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'")
cur.execute('SET NAMES utf8mb4')
sql_mode

なんか指定したほうが良いらしいです。

www.songmu.jp

utf8mb4

MySQL文字コードにはハハパパ問題と寿司ビール問題という2つの罠があるようです。

www.slideshare.net

ハハパパ問題

www.slideshare.net

MySQL 5.5.11 unicode_ci で同一視される文字

寿司ビール問題

blog.kamipo.net

akataworks.hatenadiary.jp

まとめ

多くのことを学べたので事実上の勝利

人工知能で「ぬ」と「ね」を区別するための手書き文字データセットを作った

経緯

元ネタ

dic.pixiv.net

dic.nicovideo.jp

配布物

↓ここからダウンロードできます。

https://1drv.ms/u/s!Aj7CASq25UmWhT6bwZR8KnyG9VRb

「ぬ」が47枚,「ね」が50枚しかありませんが,機械学習の勉強用などにご自由にどうぞ。

データセットを作るのに使ったソースコード

# coding: UTF-8

import cv2
import os

# 開くファイルを指定する
FILENAME='20160714210454-0001.tif'
# 出力先のディレクトリ名
OUTDIR='nu'
# 矩形の幅と高さ (rectangleよりもsquareのほうが適切な気がする)
RECT=80
# 選択中の矩形の左上の座標
rectX, rectY = 0, 0

def main():
    # 関数内関数のスコープの謎
    global rectX, rectY
    src = cv2.imread(FILENAME, flags=0)

    def mouseCallback(event, x, y, flags, param):
        # 関数内関数のスコープの謎
        global rectX, rectY
        if event == cv2.EVENT_LBUTTONDOWN:
            # cv2.resizeでfx=0.5, fy=0.5にした分を戻す
            rectX, rectY = 2 * x, 2 * y
            print(repr((rectX, rectY)))

    cv2.namedWindow('clipper')
    cv2.setMouseCallback('clipper', mouseCallback)

    while True:
        disp = src.copy()

        # 選択中の座標を画面に表示する
        cv2.rectangle(disp, (rectX - RECT // 2, rectY - RECT // 2), (rectX + RECT // 2, rectY + RECT // 2), color=0)

        disp = cv2.resize(disp, dsize=(0, 0), fx=0.5, fy=0.5)
        cv2.imshow('clipper', disp)

        # keycode
        # 13   : enter
        # 63232: up
        # 63233: down
        # 63234: left
        # 63235: right
        key = cv2.waitKey(1000 // 30)
        if key == ord('q'):
            break
        elif key == 13:
            # ファイル名を連番にする
            i = 0
            while True:
                fname = os.path.join('ne', '{:0>2}.png'.format(i))
                if not os.path.exists(fname):
                    break
                i += 1
            print('save', fname)
            cv2.imwrite(fname, src[rectY - RECT // 2:rectY + RECT // 2, rectX - RECT // 2:rectX + RECT // 2])
        elif key == 63232:
            rectY -= 1
        elif key == 63233:
            rectY += 1
        elif key == 63234:
            rectX -= 1
        elif key == 63235:
            rectX += 1

if __name__ == '__main__':
    main()

ICFPC 2016にチーム「試運転」で参加しました

ICFP Programming Contest 2016: Official Site: Task Description

ICFPC初参加です。

チームメンバー

チーム名「試運転」でNAISTの学生とNAISTの学生予備軍で参加しました。

やったこと

問題文を訳した

Google ドライブに問題文を貼り付けて並列に翻訳しました。

f:id:kujira16:20160808235557p:plain

自動提出ツールを書いた

Pythonで書きました。subprocess.check_output でソルバーと通信して,解答を requests.post で送信するだけです。

ビジュアライザを書いた

https://arosh.github.io/icfpc2016/web/index.html

入力を可視化するためのビジュアライザを作りました。

TypeScriptとD3.jsで書いています。

以下の入力を貼り付けてみてください。

1
4
0,0
1,0
1/2,1/2
0,1/2
5
0,0 1,0
1,0 1/2,1/2
1/2,1/2 0,1/2
0,1/2 0,0
0,0 1/2,1/2

提出状況確認ページを書いた

https://arosh.github.io/icfpc2016/web/result.html

今のソルバーで解けていない問題を確認したりするためのページを作りました。

TypeScript, D3.js, React, react-routerで書きました。

ソルバーを書いた?

幾何パートが難しくて,折り方を探索するパートまで進めませんでした。進捗ダメです。

結果

ソルバーの進捗ダメです。

反省

ぼく一人はソルバー開発にはほとんど貢献せずに,JavaScriptでビジュアライザを作って遊んでいるだけだった気がする。

npmで検索してみると,以下のようにJavaScriptで多倍長の分数を扱うライブラリもあるようだったので,問題を人力で解くのを補助するツールをJavaScriptで開発していたほうがチームに貢献できていたかも。

実装上の知見

データベース

TinyDBというライブラリを使って1つのJSONファイルにデータをすべて格納しようとしてみました。簡単に使えるのでデータ量が小さいうちは便利ですが,JSONファイルが数MBになってきたあたりでJSONファイルを開くだけで重くなってきてしまって残念です。数MBを超えるくらいの容量になることが予想できるならSQLiteかMongoDBを使うほうが良いでしょう。

Welcome to TinyDB! — TinyDB 3.2.1 documentation

JavaScript

Webサーバを用意するのを忘れていたので,提出状況確認ページはReact, react-routerを使ってSPAにしてみました。react-routerがバージョンによってAPIが変わりすぎなこと以外は良かったです。

https://github.com/reactjs/react-router/tree/master/upgrade-guides

ToDo

  • 非凸の多角形と非凸の多角形の包含関係
  • 非凸の多角形の三角形分割
  • 双対グラフ

他のメンバーの参加記

pakapa104.hatenablog.com