Unyablog.

のにれんのブログ

ISUCON7 の予選に出た (95352点)

今年も isucon に参加した。 id:tyageid:non_117 と一緒に参加した。

isucon.net

max 113846、最終的なスコアは 95252 だった。言語は ruby 。今年は社会人枠で出たので本戦には10万点ぐらい足りなかった。

まあ学生の中だけで考えると上の方に位置していたので良いんでは? 前回は fail だったし大躍進である。

レポジトリはこちら。 Systemd の設定ファイルなどコミットしていないのも一部あるけど。

github.com

準備

もともと何度か出場していたので今回は特に練習は行わなかった。デプロイ用のスクリプト、負荷試験用スクリプトなどの軽い環境整備はチームの人がやってくれていた。

当日

自分は11時過ぎに起きた。一時間遅れていなかったら遅刻するところだった。

ついたあとは3人で軽く作戦会議。初動の分担を決めたりしていた。

開始

蓋を開けてみたらまさかの複数台構成だった。しかもちゃんと DB と app が分かれていて。

あとは時系列で。

  • 13:00 mackerel やデプロイスクリプトssh 鍵などを整備。
  • 13:30 実装を眺める。nginx のログを仕込む。
  • 14:00 goaccess で計測結果の確認。 今度はスローログを見始める。
  • 14:30 nginx で /js, favicon.ico, /css, /fonts を cache で返すようにする。
  • 15:00 icons のキャッシュが上手く行かなくて調べ始める。 また、スローログから index を貼ったりする。

この時、なかなか 304 が返らなくて調べていた。最終的に tcpdump して正常時とヘッダーを見比べ、一回目のレスポンスに Last-Modified ヘッダがないことが原因とわかった。

icons は一回置かれたら変更されないことがわかっていたので、 nginx 側で add-header Last-Modified "Sat, 04 Jun 2011 08:51:44 GMT"; と固定してしまった。これで、 304 が二回目のリクエストから適用されるようになり点数がだいぶ上がった。

次は一回目の icons 取得、その他一般の api がネックになるようになったので、アプリケーションのコード自体を変更する必要が出てきた。

  • 16:30 icons 以外の遅い部分を改善し始める。 sleep はとりあえず消した。

なぜか2台目の app がうまく動かない。よく見たら python が動き続けていた!! ちゃんと disable する。

  • 17:00 proxy_cache を使って nginx で img をキャッシュすることで一回目のアイコン取得の高速化を試みる。

うまくキャッシュできたが、結局 /register で新しく登録されたアイコンを取ってくるのは遅いままなので、DB をやめてファイルに置くことを検討する。

  • 18:30 リクエストはすべて一台の nginx で受けるようにして、app を分配することにする。

また、 mysql に画像を保存するのをやめてすべて LB のあるサーバーに置くようにする( /profile もこのサーバーに飛ばす )。これで nginx から静的配信ができるようになった。この時 3~5 万ぐらい?

  • 19:00 app をよく見ると N+1 がたくさんあったので改善する。サーバー側では FD 増やしたり mysql のキャッシュ増やしたりしていた。

この頃から DB のコネクションが残り続けることが問題になり始める。結局 statement をすべて close するようにした。

  • 20:00 puma のスレッドを増やしたらスコアが増えていくことに気づく。結局二台とも50スレッドぐらい動かした。11万点ぐらい取れたので満足し始める。

この辺りでやっと CPU が全部 100% 回るようになった。

  • 20:45 諸々サービス切ったりして reboot 、最終スコアを出す。ベンチマークガチャを引いて終了。

自分は主に nginx・キャッシュ、その他サーバー周りを見ていた。

反省とか

  • キャッシュ周りの解決に2~3時間かかっており、ここを N+1 の残りとかに回せればもう少し点取れてたかもしれない。さっさとファイルに保存するようにしておけばよかった。
  • unix domain socket にしようとして失敗したのだけど、できていればファイル投稿周りは早くなっていたと思う。
  • もうちょっと早くなってくると、雑な nginx LB 一台だと帯域が〜〜ってなってきそう。
  • 結局今年は redis 一切使わなかった。 varnish とか使うと icons 周りでもうちょっと捌けたかもしれない?

  • log をみてちゃんとキャッシュができているか(304 が返っているか)丁寧に確認したのは良かった。ブラウザと挙動が微妙に違ったので。

  • mackerel-agent を入れていたのも良かった。はじめにサーバー情報を取得できるし、どこで詰まってるかもわかりやすい。

感想

バランスが取れていて良い問題だったと思います。いろいろ改善できる場所があり、楽しくて良かった。

来年も絶対に参加したい。社会人枠でも本戦に出れるぐらいにならないとな〜。

運営の方々、今年もありがとうございました。本戦の問題も楽しみです。

systemd-nspawn 235 のコンテナで mlock を行う

systemd-nspwan で Elasticsearch の運用をしている。親のホストは ArchLinux なので新しい systemd が入ってくる。

ある日、 pacman -Syu して systemd を 234 から 235 に上げたら Elasticsearch がエラーで落ちるようになった。

[2017-10-19T07:10:13,955][WARN ][o.e.b.JNANatives         ] Unable to lock JVM Memory: error=1, reason=Operation not permitted
[2017-10-19T07:10:13,957][WARN ][o.e.b.JNANatives         ] This can result in part of the JVM being swapped out.
...
ERROR: [1] bootstrap checks failed
[1]: memory locking requested for elasticsearch process but memory is not locked

ソースコードを読むと、mlockall できてなくて落ちていた。

調べた結果、systemd 235 では systemd-nspawn の SystemCallFilter がホワイトリストになり、 mlockall ができなくなった のが原因だった。

解決法

コンテナに CAP_IPC_LOCK を与えるか、@memlock グループを許可する。

例えば、 foo というコンテナであれば、 /etc/systemd/nspawn/foo.nspawn

[Exec]
SystemCallFilter=@memlock # システムコールのみを許可する場合
Capability=CAP_IPC_LOCK # CAP_IPC_LOCK ごと許可する場合

を追加すれば良い。CAP_IPC_LOCK を与えた場合、 @memlock は自動で許可される。

詳細

systemd には SystemCallFilter というのがあって、呼び出せる system call を制限できる。

https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=

systemd-nspawn では、デフォルトでこの制限がかかっており、もともとはブラックリストで制限されていた。

systemd 235 からこのリストのデフォルトがホワイトリスト形式になり、CAP_IPC_LOCK がない場合は @memlock が許可されないようになった。

    * systemd-nspawn gained support for a new --system-call-filter= command
      line option for adding and removing entries in the default system
      call filter it applies. Moreover systemd-nspawn has been changed to
      implement a system call whitelist instead of a blacklist.

https://github.com/systemd/systemd/blob/c1719d8bc924ed59448616bd748671c5c7a66d93/NEWS

当該PRは以下*1

https://github.com/systemd/systemd/pull/6818/files#diff-524fbe88b6a5bf2879b598646b22ed25R70

Elasticsearch では bootstrap.memory_lock: true とした場合に mlockall を内部で実行するのだが、これが権限不足で失敗していた。

これを解決するには、capability を足すか @memlock を直接許可すれば良い。sytemd.nspawn 内でどちらも設定できる(「解決法」を参照)。

CAP_IPC_LOCK を付加すると、自動的に @memlock が付加される*2

Note that the applied system call filter is also altered implicitly if additional capabilities are passed using the --capabilities=.

https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html#--system-call-filter=

@memlock のみを許可した場合、コンテナに CAP_IPC_LOCK は与えられないので、 RLIMIT_MEMLOCK の範囲内で mlock できるようになる。

In Linux 2.6.8 and earlier, a process must be privileged (CAP_IPC_LOCK) in order to lock memory and the RLIMIT_MEMLOCK soft resource limit defines a limit on how much memory the process may lock.
Since Linux 2.6.9, no limits are placed on the amount of memory that a privileged process can lock and the RLIMIT_MEMLOCK soft resource limit instead defines a limit on how much memory an unprivileged process may lock.

https://linux.die.net/man/2/mlockall

*1:そもそも @memlock が作られたのはこの pr である

*2:capsh --print で持っている capability が見える

ldap: "additional info: objectClass: value #0 invalid per syntax"

slapd のセットアップで、ドキュメントに基づいて

# cat ~/admin.ldif
dn: dc=example,dc=com
objectClass: organization
objectClass: dcObject
dc: example
o: example

dn: cn=admin,dc=example,dc=com
objectClass: organizationalRole
cn: admin

ldapadd しようとしたら

# ldapadd -Y EXTERNAL -H ldapi:// -f ~/admin.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
adding new entry "dc=example,dc=com"
ldap_add: Invalid syntax (21)
        additional info: objectClass: value #0 invalid per syntax

というエラーが。

どうやらこれは objectClass が定義されていない時に出るエラーらしいが、 organizationdcObject も core.schema にあるし普通に定義されている。

おかしいな〜と思ってググってると同じような症状の人が。

https://www.openldap.org/lists/openldap-software/200110/msg00255.html

なるほど最後のスペース…と思って自分のものを見なおしてみたら含まれていた!!!!

どうやら openldap のドキュメントからそのままコピーしたらスペースが含まれてしまうらしい。気をつけましょう…。

↑の人はこれで2日潰れたらしい。2日は潰しませんでしたが2時間は潰しました…。

OCaml 入門途中の感想

最近京大の五十嵐先生が公開しているテキストを使って OCaml に入門していた。

このテキストは object まで行かないので入門したとは言えそうにないが、一区切り付いたので初心を記録する意味で雑に感想を書いておく。

主に Haskell を考えながら書いています、が Haskell もそんなに知らないです。

  • 型推論強くて良い
  • ref は便利ですね
    • 実用的!
  • type 、簡単に型推論してくれるのは良い
    • 変数名被ったらダメなのはつらい
  • IO 周りが楽なので良い
  • 対話環境は ghci の方が便利
  • セミコロン難しい
    • セミコロンの数で意味が変わる
    • リストよくカンマと間違える
    • 全体的に他の言語と違う作法の使い方をしているので引っかかりやすい
      • 考えられた結果なのか対抗心なのかは知らない
  • 括弧が面倒
    • Haskell における $ がほしい
    • 最近?入ったらしいけどコミュニティでは使われているのかな
    • OCaml でこんなことを言っているし LISP だとどうなることやら
  • 全体的な結合順に違和感がある
    • とりあえず括弧つければ直るが
    • 慣れの問題ではありそう
  • 遅延評価じゃないのは分かりやすい
    • ただ無限リストなどを作るのは大変になる

Haskell のほうが記号が多くて便利だし、楽しさはあると思う 。ラムダ式などはシュッと書きたい。

でもシンプルな構文により初見で分かりやすいのも長所である。どっちもどっちだ(まだ途中なので本当にシンプルなのか分からないけど)。

構文はあまり好きではない(嫌いでもない)けど、諸概念は良いし納得できるものも多いので難しいところだなあという感じ。

とりあえず Haskell の入門を再開したくなった。

予防線張りすぎでは?

まだ途中だからね!

その他

最近とにかく強い静的型付け言語を使いたい気持ちが高まっていて、python のような軽い文法で無いかなあと探していたら nim があった。また使いたい。

他の現代的な言語として Kotlin や Swift は最高だと思っているけど、エディタでサクッと書くには向いてない気がするんですよね。。

pipenv を使ってみる

Python における bundler といえば (超ざっくり言えば) virtualenv になるのですが*1、イマイチ使いにくいなと思っていたら pipenv というものが登場していた。ちょっと使ってみたのでメモ。

何するやつ

github.com

  • Pipfile(.lock)を元に良い感じに virtualenv 作成 + pip install してくれるやつ
    • Pipfile が使える便利 virtualenv wrapper という感じ
  • requests と同じ作者なので流行りそう

インストール

$ pip3 install pipenv

pipenv 環境作成

--three と指定すると Python3 だけの環境が作られる。--two も同様。

$ pipenv --three

Creating a Pipfile for this project...
Creating a virtualenv for this project...
⠋Already using interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/nonylene/.local/share/virtualenvs/pipenv-test-Iy4gNUM5/bin/python3
Also creating executable in /home/nonylene/.local/share/virtualenvs/pipenv-test-Iy4gNUM5/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location: /home/nonylene/.local/share/virtualenvs/pipenv-test-Iy4gNUM5

home 以下に virtualenv を作成されたくない場合は環境変数 PIPENV_VENV_IN_PROJECT を 1 にすれば .venv/ に作成される。この辺 --path で指定したいですね。

Pipfile

PipfileGemfile と同じ要領で使えるもの。toml で記述する。

[[source]]
verify_ssl = true
url = "https://pypi.python.org/simple"

pip 2.0 に向けて Pipfile の仮実装があり、これを使っているらしい。

Pipfile を主に実装した人は pipenv 作った人と同じ人だった。実装は落ち着いているけど pip に取り入れられるのはいつになるのか…。

github.com

パッケージ追加

pipenv install [package] でインストールするとパッケージが virtualenv にインストールされ、 Pipfile も置き換えてくれる。

$ pipenv install requests

Installing requests...
Collecting requests
  Downloading requests-2.13.0-py2.py3-none-any.whl (584kB)
Installing collected packages: requests
Successfully installed requests-2.13.0

Adding requests to Pipfile's [packages]...
P.S. You have excellent taste! ✨ 🍰 ✨

最後の行は作者のライブラリを入れた時に表示される。良い話。

$ cat Pipfile

[[source]]
verify_ssl = true
url = "https://pypi.python.org/simple"

[packages]
requests = "*"

lock

Pipfile.lock の生成。

$ pipenv lock

Locking [dev-packages] dependencies...
⠹Locking [packages] dependencies...
⠹Updated Pipfile.lock!

Pipfile.lock が生成される。 json らしい。人間が触るものではない。

$ cat Pipfile.lock

{
    "_meta": {
        "hash": {
            "sha256": "da2810af0c3b5333e0de2fce9bea2a228812e2014e5f5fe3b1c533badc6c24e4"
        },
        "requires": {},
        "sources": [
            {
                "url": "https://pypi.python.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "requests": {
            "hash": "sha256:1a720e8862a41aa22e339373b526f508ef0c8988baf48b84d3fc891a8e237efb",
            "version": "==2.13.0"
        }
    },
    "develop": {}
}%

activate virtualenv

$ pipenv shell を用いることで、virtualenv の activate された shell が起動する。

$ pipenv shell

Launching subshell in virtual environment. Type 'exit' or 'Ctrl+D' to return.

$ python3
Python 3.5.2 (default, Nov 17 2016, 17:05:23)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> requests.get("http://example.com")
<Response [200]>

run

$ pipenv run を使えば bundle exec っぽいことができる。これが便利。

$ pipenv run python3 -c "import requests; print(requests.get('http://example.com').text)"
<! doctype html>
...

ただ spawn していて新しいプロセス立ててるので pipe が上手くいってなく、パイプやリダイレクションができない。*2

追記: マージされたので今後は大丈夫になるでしょう。

github.com

その他

  • pipenv install をすると Pipfile.lock || Pipfile にもとづいてインストール・ virtualenv 構築してくれる。
  • その他アップデートコマンドなどもある。 ( README.md にだいたい書いてある )
  • 個人的にはインストールパスを記憶させてほしいけどまだできなさそう

Remember where the virtual environment was created · Issue #252 · kennethreitz/pipenv · GitHub

まとめ

Pipfile をお先に扱える + virtualenv の便利 wrapper として bundler 感覚で使えるのが良いと思いました。積極的に使っていこうと思います。

*1:ざっくり言い過ぎ

*2: exec してくれ~という issue たてた。

vim-keymaps でキーマップを簡単に切り替える

Vim のキーマップを簡単に切り替えられるプラグイン vim-keymaps を作りました。

github.com

下のように、複数のキーマップを設定することができ、簡単に切り替えることが出来ます。下では <C-k> でキーマップを切り替えています。

f:id:nonylene:20170322005931g:plain

今すぐダウンロード!

使い方

各キーマップを示すディクショナリを入れた配列を g:keymaps に設定し、後はキーマップを変更するためのキーを適当に割り当てる or コマンドを打つと使えます。詳しくは README.md で。

初めは接続されているキーボードでなんとか判別できないかなと考えましたが、 ssh 先でキーボードの配列を読むのは不可能なので手動で変更するようにしました。

経緯

私は普段、コーディングするアプリケーション上では数字と記号を入れ替えて使っています。例えば 1! になり、 !1 になります。

瞬時に括弧などが打てるので非常に便利に使っていたのですが、キーボードによって数字と記号の関係は異なるので、JIS 配列に合わせたマッピングを書いていると US キーボードでは崩壊してしまいます。記号だけではなく数字も入力できなくなるので非常に困る。

しかし、いろいろあって先月から US 配列の mac を使うことになりました。

mac のキーボードのみ使うなら .vimrc を書き直して US にマッピングし直せば良いのですが、普段接続して使っている HHKB はもちろん JIS 配列。本体キーボードで使う時だけ paste モードで使う運用をしていました。

しかし paste モードのままでは tab で(空白ではなく) tab が入力されたり、数字記号以外の各種マッピングが効かないなど困ります。

これはもうキーマップをキー配列ごとに変更するしかない。 せっかくの機会なのでプラグインを自分で作りました。

作成

vimプラグインを書くことは初めてでした。まともに vimscript も書いたこと無かったし。

書くときは下のサイトを参考にしたり、有名なプラグインのコードを見たりしながら書きました。

mattn.kaoriya.net

thinca.hatenablog.com

vim のドキュメントも結構読みました。読みやすい英語だったし、充実していて良かった。

vimscript, はじめは変数のスコープ定義が特殊だったりしてとっつきにくいですが、書けてくるとなかなか楽しいです*1

内部の話

設定ファイル

設定ファイルをどのように記述しようか迷って、結局 vimscript の配列を使うことにしました。ただ、 vimscript で配列や辞書型を複数行に記述しようとすると割りと面倒で、行の最初に \ を書くことで行の継続としなければなりません。あくまで行の継続なので、途中にコメントを書くことは出来ません…。( vim は一行ごとのコメントしかできない ) README を書いてる時にその事実に気づいて修正しました…。

マッピングの数は結構多いので、.vimrc に長大な配列を記述することになります。以下は自分の設定 を半分ぐらい省略したもの です。

let g:keymaps =  [
      \  {
      \    'name': 'JP',
      \    'keymap': {
      \      'noremap!': {
      \        '1': '!',
      \        '3': '#',
      \        '4': '$',
      \        '5': '%',
      \        '6': '&',
      \        '!': '1',
      \        '"': '2',
      \        '#': '3',
      \        '$': '4',
      \        '%': '5',
      \        '&': '6',
      \        "'": '7',
      \        '(': '8',
      \        ')': '9',
      \      },
      \      'imap': {
      \        '2': '<Plug>delimitMate"',
      \        '7': "<Plug>delimitMate'",
      \        '8': '<Plug>delimitMate(',
      \        '9': '<Plug>delimitMate)',
      \      },
      \      'cnoremap': {
      \        '2': '"',
      \        '7': "'",
      \        '8': '(',
      \        '9': ')',
      \      },
      \    },
      \  },
      \  {
      \    'name': 'PASTE',
      \    'paste': 1
      \  },
      \]

dein のように toml や json に分けれればなあと思ったのですが、 vimscript では標準ライブラリでシュッとパースすることはできません*2Python バインドでも使えば良いのですが、標準では入っていないことが多いので諦めました*3

map 方法

この g:keymap からマッピングするのは非常に素朴で、コマンド、左辺、右辺をスペースでつなげて放り込んでいます。なのでオプションを適当に入れても動くし、他のコマンドも打てちゃいます(キーマップ削除する時にエラー出ると思いますが)。

execute(l:cmd . ' ' . pair[0] . ' ' . pair[1])

特に立派な API は無かったのでこうしました。スペース等も自分で書くのと同じようにエスケープすれば動きます。

unmap 方法

デフォルト設定ではキーマップ変更時に以前のマッピングを削除しています。よって、マッピングと対になるアンマッピングコマンドを生成する必要があります。

map - Vim日本語ドキュメント

これも素朴に1文字目と2文字目をパースしています。

function! s:create_unmap(origin)
  let l:first = a:origin[0]
  if l:first is 'n'
    if a:origin[1] is 'o'
      " noremap
      let l:cmd = 'unmap'
    else
      let l:cmd = 'nunmap'
    endif
  elseif l:first is 'm'
    " unmap
    let l:cmd = 'unmap'
  else
    let l:cmd = l:first . 'unmap'
  endif

  if a:origin =~ '!'
    let l:cmd .= '!'
  endif

  return l:cmd
endfunction

paste モード関連

このプラグインは paste モードをサポートしているのですが、paste モードに入ってしまうとマッピングが全て外れるのでキーマップの切り替えができません。

set pastetoggle=<同じキー>

とすることで vim の機能によって paste モードを抜けることができるのですが、vim の機能で抜けているので次のキーマップには移動されず paste 用キーマップのままになってしまいます。

結局、autocmd を使うことによって paste モードの変更を検知して、フック上でキーマップを変更することにしました。

augroup keymaps
  autocmd!
  autocmd OptionSet paste call s:on_change_paste()
augroup END

function! s:on_change_paste()
  " sometimes v:option_new not working
  if g:keymaps_paste_auto_rotate && !&paste
        \ && get(keymaps#get_current_keymap(), 'paste', 0)
    " exit paste mode -> rotate
    call keymaps#rotate_keymap()
  endif
endfunction

lightline との連携

keymaps#get_current_keymap_name で現在のキーマップ名を取れるようにしたので、これを lightline に表示すれば現在のキーマップを簡単に見れます。最初に示した gif もそうしています。

paste モード変更時にもうまくするには lightline も paste を検知できるように autocmd を新しく設定する必要があります。

augroup local
  autocmd!
  autocmd OptionSet paste call lightline#update()
augroup END

その他

  • <SID> をキーマップに使っているとその <SID> は実行時のスクリプトに依存するのでプラグイン用のスコープになってしまう。
    • 同じような理由から、 <expr> をキーマップに使って関数を実行したい場合、呼び出す関数はグローバルでなければならなかったりする。
  • autoload/ に多くを分割したのだけど、最初のキーマップを設定するために起動時に読み込まれるので逆効果になっているかもしれない。
    • まあ重いスクリプトではないので大丈夫だと思うけど。
  • vimscript 、 endforend にできるが endfunctionend に省略できなかったりして面白い。
    • 結局全部フルで書いた。
  • l: をちゃんと書いてたけど実は暗黙的に設定されるっぽい。
  • vimscript の文法、だいぶ知れたのでこれからは .vimrc サクサク書けそう。
  • US キーボード、' が一瞬で打てるのが良いですね。でも数字と記号を入れ替えていると 0 に記号が割り当てられてしまって不便だったりする。

まとめ

これで今まで通り記号が簡単に入力できるようになりました。 「paste モードではないが記号のマップは削除したキーマップ」なども設定できて快適です。

Vim 最高!みんなも今すぐプラグイン書こう!!

*1:perl っぽいですね(?)

*2:雑に eval すると大体動くとは思う

*3:実は go とか使うとよかったりするのかな