たにしきんぐダム

プログラミングやったりアニメやゲーム見たり京都に住んだりしてます

JavaScript とクロスブラウザでの IME event handling (2017年)

この記事は CAMPHOR Advent Calendar 2017 22日目の記事です。
昨日は @ryota-ka による Type-level TypeScript - ryota-ka's blog でした。

CompositionEvent が多くの主要ブラウザでサポートされた2017年冬なら、JavaScriptで日本語入力に対応したちょっとした入力補完機能の実装はシュッとできると思ったのですが、ブラウザによって細かい実装が異なっていてちまちまとしたworkaroundが必要になったのでメモ

検証環境

KeyboardEvent.keyCode

IME によるテキスト編集中は keyodown イベントによって発火する KeyboardEventkeyCode229 になります。 これを使えば IMEによるテキスト編集中の keydown イベントにトリガする処理を制御することができる。

editorElem.addEventListener('keydown', function(event) {
  if (event.keyCode !== 229) {
    // do something ...
  }
});

https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode

If an Input Method Editor is processing key input and the event is keydown, return 229.

Gecko

GeckoではIMEによるテキスト編集中はkeydown/keyupイベントが発火しないみたいですね

https://bugzilla.mozilla.org/show_bug.cgi?id=354358

deprecated

KeyboardEvent.keyCode は deprecated なようです。W3Cでも keyCode 属性が記載されていたのは Legacy Key Attributes の欄ですね、keyCode はプラットフォームや実装依存の値なので、排除していきたい流れなのかも。

KeyboardEvent.keyCode - Web APIs | MDN

KeyboardEvent.isComposing

KeyboardEvent.isComposing - Web APIs | MDN

  • IMEのような text composition system によるテキスト編集中は true となるreadonlyな属性
    • 後述する compositionstartcompositionend の間で発火したイベントなら true
  • Browser compatibility
    • MDN の Browser compatibility では(デスクトップの) IE, Safari で未対応と書かれていますが、検証環境の Safari 11.0.2 では KeyboardEvent.isComposing の値を取得することができました。
    • 検証環境の Microsoft Edge 41、IE11 では、KeyboardEvent.isComposingundefined になってしまうようでした。

将来的にはこちらを使うと良いのだろうけど、IE, Edge で未対応なのでまだ使えそうにはないですね...

InputEvent.isComposing

InputEvent.isComposing - Web APIs | MDN

  • text composition system によるテキスト編集中は true となる readonly な属性
    • 後述する compositionstartcompositionend の間で発火したイベントなら true
  • Browser compatibility
    • MDN の Browser compatibility では(デスクトップの) IE, Safari, Chrome, Opera で未対応と書かれていますが、検証環境のOpera49、Google Chrome では InputEvent.isComposing を取得できた。
    • Microsoft Edge 41、IE11 では、KeyboardEvent.isComposingundefined になってしまうようでした。

こちらも将来的にIMEによるテキスト編集中の InputEvent かどうかの判断にはこの値を使うと良いのだろうけど、IE, Edge で未対応だとまだ使えそうにないですね。

CompositionEvent

CompositionEvent - Web APIs | MDN

  • compositionstart: IMEによるテキスト編集開始時に発火
  • compositionupdate: IME で編集中のテキストが変更された時に発火
  • compostionend: IMEによるテキスト編集終了時に発火

  • MDN の Browser compatibility によれば Opera は未サポート、Safari? と書かれていますが、検証環境のSafari, Operaではイベントは発火するようでした。
    • ただし data 属性は未実装だったりする様子
  • InputEventIMEによるテキスト編集中のものかどうかを調べるには InputEvent.isComposing を使う代わりに、CompostionEvent を監視して状態を管理すると良さそう
var isComposing = false;

editorElem.addEventListener('input', function(event) {
  if (!isComposing) {
    // do something ...
  }
});
editorElem.addEventListener('compostionstart', function(event) {
  isComposing = true;
});
editorElem.addEventListener('compostionstart', function(event) {
  isComposing = false;
});

楽ちん

CompositionEvent/InputEvent の発火順序

詳しくは

IMEによるテキスト編集開始/編集中

event note
compositionstart 編集開始時
beforeinput
compositionupdate
ここでDOMが更新される
input

テキスト編集終了時

event note
compositionend
beforeinput
ここでDOMが更新される
input

このようなイベント発火順序となるのでIMEによるテキスト編集中の InputEvent は必ず compositionstartcompositionend の間に発火する、はず。

確認してみた

実験用スクリプトを書いて検証環境のブラウザで確認してみた

  • IE11, Edge41: テキスト編集終了時の input イベントが発火してないように見える?
  • Google Chrome, Safari, Opera: テキスト編集終了時の input イベントが compositionend の前に発火してる?

うーむ僕のスクリプトの書き方が悪いだけなのかな?詳しい人教えてください

CompositionEvent/KeyboardEvent の発火順序

詳しくは https://www.w3.org/TR/uievents/#events-composition-key-events

IMEによるテキスト編集開始/編集中

event note
keydown
compositionstart 編集開始時
compositionupdate
keyup

テキスト編集終了時

event note
keydown
compositionend
input
keyup

確認してみた

またもや雑に実験用スクリプトを使って検証環境で調べてみる

  • Firefox: 最初の方でも書いたけどIME編集中はkeydown/keyupイベントが発火しない https://bugzilla.mozilla.org/show_bug.cgi?id=354358
  • Safari: テキスト編集終了時keydowncompositionend の後に発火してる(このときの keyCode は 229)
  • Edge: テキスト編集終了時に keydownkeyup も発火しない

現状だとブラウザ実装によってイベント発火順序が結構違うみたいですね...あまり使うことはない気がするけど、ブラウザ間のイベント発火順序の違いを吸収するためのworkaroundは Handling IME events in JavaScript – Not Rocket Science に詳しく書かれていて良かった。

参考

まとめ

現状だとブラウザ互換性を考えると結構複雑ですね、2017年になっても日本語入力はやっぱり難しい。基本的には暫く KeyboardEvent.keyCode が 229 かどうかを調べていくくらいで良さそうで、複雑なことやろうとする場合はできるだけライブラリ(日本語入力に対応している良い感じの軽量な補完エディタとかあったら教えてください)に頼っちゃうのが楽そう。

好きなセリフってどれですか?

この記事は SHIROBAKO Advent Calendar 2017 の 19 日目の記事です。 今日は社会人になってからSHIROBAKOを視聴し直してみて、心に残ったセリフを書き留めたいと思います。

adventar.org

SHIROBAKO には心に刺さる名言がいくつもありますよね、SHIROBAKO は何度か視聴しているけど何度効いても胸を突き刺さります。 ただ、今年学生から社会人になってからSHIROBAKOを視聴してみて、これまではとは違うセリフが心に強く残るようになりました。立場が変わって視界が変わると作品の見方も変わるものですね。

あたしらの仕事だって厳しいじゃない。厳しさの種類が違うだけだよ

アニメーターの仕事が上手くて情熱のある人じゃないと生き残れないのかも、ということに対して宮森が「厳しいですね...」といったことに対する矢野さんのセリフ

©「SHIROBAKO」製作委員会 / SHIROBAKO 第6話「イデポン宮森 発動篇」より引用

なんかこのセリフ好きです。隣の芝生は青いってよく言うけど、それの反対版?

他所ばっかりよく見える(けど実際は他所だって辛いことがたくさんある)のと同じように、他所ばっかり大変そうに見えても自分たちの仕事だって十分大変だったりする。だから自分たちだって気は抜けないし、そんな大変な中頑張ってる自分たちは偉いなって思えて良いですね。

そうそう、自分のことだけ一生懸命にやるのが一番

落合さんが他者に移って絶賛炎上スケジュール中のプロジェクトに携わること、プルテンの過酷だったスケジュールの話を山田さんと宮森で話していたときの山田さんのセリフ。

周辺の会話もすごく好き。

山田さん『よそのスケジュールディスってたら絶っ対ブーメラン返ってくるもんなんだよ、これ業界あるあるな』
宮森『嫌です!明日は我が身っ』
山田さん『そうそう、自分のことだけ一生懸命にやるのが一番』

©「SHIROBAKO」製作委員会 / SHIROBAKO 第7話「ネコでリテイク」より引用

本当に明日は我が身っ...!今日も頑張る...!

予定通りってことは、もの凄くもの凄く珍しいことなんだよ

宮森『んとねー、今日は仕事が予定通り順調に進んでー』
ねいちゃん『はぁ?予定通りって当たり前でしょ?』
宮森『わかってる。わかってるけどねえちゃん、あのね、予定通りってことはもの凄くもの凄く珍しいことなんだよ』

©「SHIROBAKO」製作委員会 / SHIROBAKO 第7話「ネコでリテイク」より引用

珍しいことだというか、仕事が予定通りに順調に進むことってすごく難しいことで、それをうまくいくように支える制作進行/マネージャーも、実際に作業に取り掛かるプレイヤーも凄いよね...みんな偉い...

宮森さんがアンデスチャッキーを観て笑顔になってくれたのなら、こんな嬉しいことはないよ

宮森にアンデスチャッキーがすごく好きで、子供の頃もすごく影響を受けて、今も大好きだということを伝えられたときの杉江さんのセリフ

©「SHIROBAKO」製作委員会 / SHIROBAKO 第12話「えくそだす・クリスマス」より引用

このときの宮森のアンデスチャッキーが好きだということを伝えるときのセリフもすごく好きなんだけれど、自分の作った作品が誰かに(良い意味での)多大な影響を及ぼしていたということを、面と向かって言われるのはすごく嬉しいだろうな〜と思った。少し涙ぐんでしまった。 素直にこうして自分の気持を伝えられる宮森はすごい

やらなければいけないのなら、やるしかないな

第三飛行少女隊の一話の作業中に、急遽PVを作成するために先行して別のカットを挙げてほしい旨を佐藤さんに伝えられた際の渥美さんのセリフ

©「SHIROBAKO」製作委員会 / SHIROBAKO 第17話「私どこにいるんでしょうか...」より引用

頼まれた側(渥美さん)としては絶対に嬉しくなくて、頼んだ側(佐藤さん)としても心苦しいことで、文句言いたくもなるような状況なのに、佐藤さんに文句も言わずこう言って引き受けてくれる渥美さん、すごく格好良くて好感度*1がバク上がりしたシーン。かっこよすぎ。

全部を一度には無理でもね、小さなことからコツコツやればいつかは終わるよ

トラブルにトラブルが重なり、それに加えて安藤さんと佐藤さんとタローからもヘルプを求められて、処理が落ち着かずにパニクってしまったみゃーもりに対する(?)ロロのセリフ

©「SHIROBAKO」製作委員会 / SHIROBAKO 第18話「俺をはめやがったな!」より引用

僕は同時実行能力があまり高くなくて、同時にいろんな仕事が舞い込むと、あたふたしてしまうことが多いのですが、ロロが言ってくれているように当然 当然全部の仕事を一度にやる必要はない(できない)し、目の前にある今できる仕事をひとつひとつこなしてTODOリストを小さくすれば意外となんとかなることが多い。

このセリフ頭の中でよく唱えてる気がします。

これは僕には描けないな、安原さんにしか描けない画だ。良いね

絵麻が描いたアリアと猫が触れ合うカットを観た杉江さんの感想

©「SHIROBAKO」製作委員会 / SHIROBAKO 第22話「ノアは下着です。」より引用

このカットは絵麻が自分からやりたいと言って受けた仕事で、杉江さんは絵麻にとって憧れのアニメーターで、そんな憧れである杉江さんが少なくともこの分野では敵わないし、この仕事は君にしかできないっていうことをいって言ってもらえたの、すごく嬉しかっただろうな。

こんな褒め方ができる杉江さんはやっぱり格好良いし、このあと目を潤ませて「ありがとうございます!」っていう絵麻が本当に素敵。そしてそれを見て一人で会議に出席する決意をする久乃木さんも良い。本当に良いシーン...

まとめ

SHIROBAKO 毎年一度は視聴しているのですが、今年も泣いてしまった...

ところで最終話で、第三飛行少女隊に関わったスタッフの集合写真を撮るのシーンがあるのですが、この集合写真のほとんど全員の顔を見て、「ああこの人はあの仕事をしていた人だ」って思い返すことができる。一人ひとりがどんな仕事をしていたかを思い返すと、こんなにたくさんの人が関わって1クールのアニメが出来ているんだな、実際だともっとたくさんの人が関わることになるんだろうな、みんなで力を合わせてひとつのものを作るのって凄く素敵なことだなと感じます。

©「SHIROBAKO」製作委員会 / SHIROBAKO第24話「遠すぎた納品」より引用

SHIROBAKO なんど見ても良かった。来年も観ます。そして来年もまた違うことを感じて、また泣いてしまうんだろうなと思う。

*1:高感度と誤字っていました、ご指摘ありがとうございます

Python機械学習プログラミング を読んだ

機械学習に入門しようと思い、重い腰をあげて実践的な入門書として評判が良さそうだったPython機械学習プログラミング」を読みました。

この本を読んでやっと機械学習分野の全体像がうっすら見えてきて、やっと入り口に立てたなと思います。読んで本当に良かった。 機械学習初心者の人が買うか迷っていたらオススメしたい。

この記事を書いた人のレベル

  • 機械学習に関する知識は情報系の学部レベルの機械学習の授業を受けた程度
  • Pythonの文法とか使い方はだいたい知ってる
  • 情報系の学部初等レベルの数学はある程度分かる(けど結構忘れてる)

読書の進め方

読書会にした

一人で読み進めると絶対途中で飽きるだろうなと思ったので、興味のありそうな友人を2人巻き込んで、読書会形式で進めることにしました。

  • 1~2週間ごとに1章ずつみんな読んでくる
  • みんなで集まって(今回は全員住んでる場所がバラバラだったのでSkypeで)読んだ章で分かりにくかったことなどを話し合う
  • 特に発表資料とかは作ってない、ゆるゆる読書会
  • 勉強会としての体はあまり成してなかったけど、ペースメーカーとして大いに役に立った

読書プラン

翻訳の福島さんがこの本の読み進め方ガイドを書いてくれているのですが、基本的には特訓コース(全部読むコース)でやりつつ、読書会参加メンバーの興味や知識を鑑みて8章と9章(機械学習の応用)と13章(Theano/Kerasの使い方)は興味があったら読んでくるという感じで進めました。 thinkit.co.jp

Theanoは開発中止になっちゃったけど、Python Machine Learning, Second Edition ではTheanoの代わりにTensorFlowを使ったチュートリアルになったり、RNNやCNNについて取り上げた章が追加されているようですね。

どのくらい読み込んだか

  • Pythonでのプログラミング
    • Python自体の文法には慣れていたのですが、NumPyやPandasやscikit-learnを使った機械学習プログラミングの経験はあまり無かったので、最初のうちは写経したり、同様の手法を別のデータに適用したりして遊んだ。
    • 後半からは慣れてきたので写経せずにコード読みつつ、気になる部分だけJupyter Notebookで実験してみるぐらいになってた
  • 機械学習理論

Pros/Cons

良かったところ

  • 手を動かしながら学べるのが良かった
  • 機械学習の実践を見据えた構成になっていて、実際に機械学習プログラムを書くときに、どういうことをすれば良いかが分かった
    • この本をよむ前は
      • 理論的な入門書を読んでも、理論はわかったけど結局どういうことをすれば良いのか分からない
      • Pythonだとどうすれば良いのか分からず、愚直に実装して疲れ果てたり
      • 機械学習入門記事を読んで適当にscikit-learnで機械学習してみたはいいけどモデルの評価とかどうすれば良いのかよくわからない
    • ということが多かったのだけれど、この本を読んですっきりと整理できたのが本当に良かった

あまり良くなかったところ

  • 数式が平易に解説されていて(逆に)少しわかりにくかった。
    • 大学初等レベルの数学が分かっていなくても雰囲気が分かるように平易に書いてくれている(どうしても難しくなるところは潔く省略されてた)のだけれど、個人的には煙に巻かれたような気持ちになった
    • とはいえ他の本を参照すれば全然OK

まとめ

機械学習初心者にとっては本当に良い本でした。機械学習やってみたいけど何すればいいのかよくわからない/理論よりの機械学習入門書読んだけど何すれば良いのか分からんという人には絶対オススメしたい。

今後はこの本の範囲外の機械学習(理論)について勉強しつつ、kaggleや適当なデータセットを使って手を動かしていきたいと思う。

ISUCON7予選敗退した

ISUCON7 開催&日程決定! #isucon : ISUCON公式Blog

(開催から2週間も経ってしまった...!)(ポエミーな感じです) ISUCON7予選に id:astjid:aerealid:tanishiking24 との3人で「秒速5000兆クエリ」というチームを組んで参加しました。最終スコアは 92037 で本戦進出ならずでした...。

反省はいろいろあるけど思いついたことはだいたい試したのでまた来年頑張ろうという気持ちです、100万円欲しかった!2人ともめちゃくちゃ頼もしくて助かりまくりでした

めちゃくちゃ楽しかったし、あの参加者の数で予選から3台構成すごすぎる、運営のみなさまありがとうございました!

予選まで

pixiv社さんの社内ISUCONを利用させていただいて練習した。それまではPerlで出る?Goで出る?と言語も決まっていなかったのだけれど、このISUCONにはPerl実装が用意されていなさそうだったのでGoで練習し、その流れでそのまま予選もGoでという雰囲気が出てきたのでありがたかった。

コンテストをがっつり解くというよりは以下のようなことをして本番でスムーズに導入できるようにという感じ(良かった)

  • 3人で作業するときにどういう感じで作業しようかを認識合わせしたり
  • ISUCONのためのツールのインストール/使ってみる素振りをしたり

予選

開始が遅れるとのことだったので、ちょっと良い肉を食べてくることにした。肉はめちゃくちゃ美味しかったのだが、つい最近まで学生で安い肉しか消化してこなかった胃がびっくりしたのか、ISUCONへの緊張のためか、予選開始の5分くらいまで腹痛に苦しめられることになってしまった、なんでや。

hafuu.com

おすすめです。


3人の中でISUCON経験が一番多い id:astj が司令塔となってくれた。僕以外の2人が開発環境/デプロイまわりの整備やMackerelプラグインのインストールなどをテキパキとこなしてくれて、その間に僕はログ解析ツールのインストールやアプリのコードを読んでおいて後で共有するなどした

なんか画像配信部分がボトルネックになってそうだけど、結構ヘビーな変更になりそうだし、軽くやれそうな高速化からやっていきましょうということでインデックスを貼ったり、未読数の全件カウントをredisに保存したり、ちょっとしたN+1を1クエリでできるようにした。

この時点で3~4万点くらいには上がっていたけれど、トップ勢はぶっちぎりで高いスコアを出していたのでやっぱり画像配信なんとかせないかんねとなって、画像配信まわりに手をつけ始める。


変更のない画像へのリクエストに対して304を返す作業は他の2人が進めてもらっていたので、僕はその間に複数台サーバーで画像をうまいこと静的に返せるようにいくつか実験をして失敗したりしていた

  • 画像へのリクエストのファイル名に、どちらのサーバーでアップロードされた画像かわかるようにprefixをつける。prefixをみてnginxでサーバーをふりわけする作戦
    • default.png とか最初からサーバーに上がっている画像への取り扱いがうまくいかずテンパってしまい結局revertする...
    • 今になって考えればそんなに難しいことじゃないはずだし、上位のチームも似たようなことをやっていたようなのでここちゃんと丁寧にやれば良かった...

その間に他の2人が変更のない画像に対して Last-modified をつけて画像の読み込み頻度を減らす作戦を実装していたのだけれど、手元のChromeIf-Modified-since をつけてリクエストしてくれるのにベンチマーカーは If-Modified-Since つけてくれなくてなんでだ〜ってなっていた。 結局2人が Cache-Control: max-age=xxx をつけたら304返せるようになって、max-ageがきれたときにrevalidateするような実装だったんだな〜ってなったり、最初からサーバーに上がってる画像だけでも静的に配信するようにするなどして9万点がでた。

そのあとはmy.cnfのパラメータを変えたりしてエンキューしまくっていたのだけれど点数が大きく上がったり下がったりしていたので、パラメータの変更が良かったのか悪かったのかよく分からなかった...そんなこんなしてたら制限時間が迫ってきたのでこれまでの最高スコアに近い点数が出た時点で終了!!!ということに。

反省

  • 落ち着きたい
    • あせっていたのもあって冷静になればできるはずの実装に失敗し続けていた
  • とにかく手を動かし続ければ良かった
    • アイコンの配信がボトルネックになっていたのでN+1や未読数のキャッシュをしても大してスコアが伸びず、まずはアイコンなんとかしないとダメだよな〜となってオタオタしていたが、そのボトルネックを抜けたあとはアプリチューニングの勝負だったので、オタオタしてないで(その時点でスコアが伸びるか分からなくても)ひたすら小さなボトルネックをこつこつ潰していたらもっと良いスコアが出せたかもしれないなと反省しています...
  • 手元で動作するからといって安心しない
    • 手元のChromeに対しては304が返せていたのにベンチマーカには304が返せてないし、Last-modified-Since指定するだけで良いはずだよね??と思考停止していた。色々実験すれば良かったのに、とにかくドキュメントなどを読み返すことしかできなかった、無念(キャッシュ周りもっと勉強しよう)

いろいろ反省はあるけど、あの緊張感と限られた時間内ではやれることはやった気がする。来年はもっと強くなって本戦も出るし100万もゲットやで

余談

「秒速5000兆クエリ」、5000兆円ネタにもあやかっているし、「秒速5センチメートル」とも、「びょうそくごせんち」までprefixが一致していて良い名前だなと思いました。

トランザクション技術とリカバリとInnoDBパラメータを調べた

トランザクションはACID特性を満たすと言われている。 そのうちA(Atomicity)はトランザクション内の操作をAll or Nothingとなるよう保証し、トランザクションが中途半端に実行されて(アプリケーションレベルから見た)データの整合性が失われることを防ぐ特性。またD(Durability)とはシステム運用中に起こる様々な障害からデータを守る(整合性を保つ)特性。

これらの特性を満たすためのDBMSの古典的なテクニックがすごく面白いので、それに関するMySQL(主にInnoDB)のパラメータ・パフォーマンスにどのような影響を及ぼすかを調べた(*'ω'*)

なお紹介している技術は基本的に教科書に書かれていた技術で、実際にInnoDBに実装されているアルゴリズムとは異なることがある(とはいえベースにはなっている)

参考

障害の種類

  • トランザクション障害: トランザクションがabortしたときに発生
  • システム障害: 例えばシステムの電源が落ちたなどの理由で揮発性ストレージ上のデータが消失
  • メディア障害: 永続ストレージの一部分が破損し、永続ストレージ上のデータが消失

これらの障害から回復するためにDBMSには様々な機能が実装されている。 ここで「回復する」とは、障害からのシステム再起動時に以下の状態に復元することを指す。

DBMSの基本構成

DBMSにおけるストレージは以下の図のような構成。

f:id:tanishiking24:20171005182045p:plain:w700

データの読み書きの際は、まずはメモリ上のキャッシュ(innodb buffer pool)にを介して行われ、キャッシュに書かれたデータは非同期的にディスクにflushされる。これによりパフォーマンスが向上するが、メモリ上に存在しディスクにはまだ書き込まれていないページは(システム)障害発生時には消失してしまう。

障害発生後の再起動時にコミットしたデータを回復するためにはどうすれば良いだろうか。 考えられる単純な方法としてはCOMMIT時にトランザクションの操作の結果をディスクに書き込むことであるが、これはトランザクションがCOMMITされるたびにディスクへのランダムIOが発生することになり効率が悪い。

ランダムIOによるパフォーマンスの低下を回避しつつコミットしたトランザクションの操作の内容をディスクに永続化するための機能(?)としてトランザクションログがある。

トランザクションログについて説明する前にキャッシュ(DBバッファ)について軽く触れておく。

データベースバッファ

概要

データベースへの読み書きはデータベースバッファを経由して行われる。バッファのサイズが大きければ大きいほど、多くのデータをバッファに置きIOの頻度を減らしてパフォーマンスを向上させることができる。

またバッファ上での同一ページへの更新は、ディスクへのflush時に一つ一つ実行されるのではなく、更新の最終的な結果にまとめられてディスクにflushされる。これによりディスクへの書き込みを減らすことができる。このようなテクニックをwrite combiningという。

データベースバッファのflushはDBMSが良い感じのアルゴリズムによって管理しており、InnoDBではLRUによるバッファ管理がされている。

関連するMySQLパラメータ

innodb_buffer_pool_size

InnoDBがテーブル・インデックス・データをキャッシュするための領域のサイズ。

innodb_buffer_pool_instances

  • innodb_buffer_pool_size をいくつに分割するか。バッファプールサイズが巨大な場合はインスタンスの数を増やすことでバッファプールへの読み書きの競合が抑えられ同時実行性が向上する場合がある。
  • innodb_buffer_pool_size が1G以上に設定された場合のみ有効。

innodb_lru_scan_depth

  • InnoDBはflush対象のダーティページを見つけるために毎秒LRUリストを探索する。 innodb_lru_scan_depth は各buffer pool instanceのLRUリストをどのくらいのページ数探索するか。
  • 最大 innodb_buffer_pool_instances * innodb_lru_scan_depth だけ一度にflushされる可能性があるので、それを考慮して innodb_io_capacity も大きくする必要がありそう。

innodb_flush_neighbors

  • バッファプールからページをflushする際に、同一エクステント(ページグループ)内のダーティページを一緒にflushするかどうか
  • デフォルトではバッファプール内の同一エクステント内の連続するダーティページがフラッシュされるようになる。
  • HDDでは個々のタイミングでページをflushするより、ヘッドが近くにあるときについでにご近所さんもflushした方がヘッドのシークを抑えることができパフォーマンスの向上につながる。
  • 一方SSDを使っている場合はシーク時間は重要な要素ではないため、このオプションを無効にし不要なIOを抑えることでパフォーマンスを向上させることができる。

innodb_flush_method

データ/(後述する)ログファイルをディスクにどのような方法で書き込むか。デフォルトでは fsync() を使うが、 O_DIRECT を指定することでOSのページキャッシュをすっとばして直接書き込むことができる(DBMSが独自にキャッシュを持っているためdirectIOによるパフォーマンスの劣化はそこまではなく、ページキャッシュを抑えてメモリの節約ができる)

トランザクションログ

概要

ログはデータベースに適用された更新を表現するレコードの列を持つ。そのレコードには意味的には以下の要素を持つ。

  • 更新したページのアドレス
  • 更新を実行していたトランザクションID
  • 作用(READしたとかWRITEしたとかCOMMITしたとかABORTしたとか)
  • ページに書き込まれた後の値(REDO情報)
  • ページに書き込まれる前の値(UNDO情報)

トランザクションがCOMMIT時にはログを永続化していることが保証されていれば、ログを用いて障害発生前にCOMMITしたデータを復元することができる。

またログを書き込むディスクと、データページを書き込むディスクを分けておけば、両方のメディアで同時に障害が発生した場合はどうしようもないけど、どちらか片方だけでのメディア障害に対するリスクを抑えることができる。

ページをCOMMIT時にディスクに書き込むのは遅いとか言いつつ、ログもディスクに書き込んどるやんけと思うかもしれないが、ログは操作を時系列順に書き込むだけで良いためシーケンシャルに書き込むことができ、ヘッドのシークによる書き込み性能の低下はランダムIOと比べて少ない。

WAL

キャッシュ上のページがいつflushされるかはDBMSによって管理されている。 DBMSが「このページもうflushするで〜〜〜」となったときに即座にflushして良いかというと良くない。

ページのflushは非同期的に実行されるため、COMMITしていない(ログに永続化されていない)ページが、ディスクに書き込まれてしまうかもしれない。 この状態でシステム障害が起きてしまうと、未COMMITのトランザクションの操作内容がディスクには書き込まれているが、ログにはそのトランザクションに関する情報が何も残っておらず、リカバリ時に未COMMITのデータをUNDOすることもできず、整合性が崩れてしまうかもれない。

これを解決するためには WAL(Write Ahead Logging) というプロトコルにのっとってページ/ログのディスクへの永続化を行う。 WAL は、「ページをflushする前に、ログに書こうね」という決まり。

ログのフォーマット

物理ログ

各ログレコードが操作前/操作後のページを丸っと持っておくフォーマット。

これは単純だしリカバリ時も丸っと置き換えるだけなのでリカバリ操作が冪等だが、ログの肥大化を招く。さらにシステムがレコードレベルロックを行なっている場合は、この方法はうまく動作しない。

例えば同一ページ上にあるレコードAとレコードBを考える。trxAがレコードAを更新し、trxBがレコードBを更新した後にtrxAをabortする(その後trxBがCOMMITする)と、この方法ではページごと元の状態に戻すため、abortしていないはずのtrxBによる変更まで巻き戻ってしまう。

論理ログ

各ログレコードが、レコードに対する論理的な操作を持つフォーマット。

これによりログの肥大化、レコードレベルロックを行なっている場合でもうまくいく。 ただし複数のページにまたがる操作が行われたとき、それらのページが一斉にflushされないとログの効力が失われてしまう。

また、このようなログによるリカバリは冪等な操作ではないため、単純にチェックポイントレコードからリカバリを始めるということができなくなる。

物理論理ログ

各ログレコードが、レコードに対する操作ではなくページに対する操作を持つ。

ページを丸っと持つわけではないのでログの肥大化を抑えられるし、複数ページにまたがる操作が行われた場合はそれらのページに対する操作のログをページの数だけ追加すればよい。


リカバリ操作が冪等でないという問題が残っており、これを解決するためにはログレコードにIDを振るなどがあるが、後で書くチェックポイント処理の項目で説明する。

関連するパラメータ

innodb_log_file_size

  • その名の通りログファイルのサイズを設定する。
  • (InnoDBの)ログは循環的に書き込まれる固定サイズのファイルで、末尾まで書き込まれていっぱいになると、先頭から上書きしていく。先頭から上書きしていくときに、その先頭のログレコードに対するページがダーティページな場合、ログが失われてしまい障害発生時にリカバリができなくなってしまう。
  • そのためログの使用率(現在のLSNとチェックポイントレコードの位置、ログファイルサイズから計算)(LSN,チェックポイントについては後述)が大きくなってくると、バッファプールに余裕があってもダーティページのディスクへのflushをDBMS推し進めてくる。
  • そのため、 innodb_buffer_pool_size を増やしたときは、 innodb_log_file_size も大きくしないと、データをメモリ上に多く保持してパフォーマンスを向上させることが難しくなってしまう。
  • また、一般的にログファイルが大きいとそれだけ多くのダーティページを持つことができるため、システムのリカバリに時間がかかるようになる。

innodb_log_files_in_group

  • デフォルトでは2。ログファイルの数を指定する。REDOログのサイズの合計は innodb_log_file_size x innodb_log_files_in_group となる。
  • このパラメータを使ってREDOログを大きくしつつも個々のファイルサイズは小さく抑えるとかもできる。

ログバッファ/ログファイルへの書き込み

概要

トランザクション実行中のログレコードはメモリ上のログバッファにたまっていき、トランザクションがCOMMITされる前にログはディスクに書き込まれる。

group commit

COMMITする度にディスクに書き込んでいては、ディスクへの書き込みが多くなってしまう。ディスクへの書き込みはできるだけ少なくしてパフォーマンスを向上させたい。group commit は(短時間内の)いくつかのトランザクションによるCOMMITをまとめてディスクに書き込むテクニック

どれくらいのCOMMITをまとめてログに書き込むかはわからないが、InnoDBでは特に何も設定せずともgroup commitの恩恵を受けられるみたい(innodb_flush_log_at_trx_commit=1 にしててもグループコミットの恩恵は受けられるってことで良いのかな)

https://dev.mysql.com/doc/refman/5.7/en/innodb-performance-group_commit.html

関連するパラメータ

innodb_log_buffer_size

文字通りログバッファのサイズを設定する、ログバッファが大きいと大規模なトランザクションを効率よくさばくことができるようになる。

innodb_flush_log_at_trx_commit

  • 1: トランザクションコミットのたびにログバッファの内容がログファイルに書き込まれ、ログバッファにある(ページへの操作)がディスクにflushされる。(デフォルト)
  • 0: 約1秒ごとに1回、ログバッファの内容がログファイルに書き込まれ、ログバッファにある操作がディスクにflushされる。ディスクへの書き込み頻度が減り、パフォーマンスの向上が見込めますが、COMMITした値がログに書き込まれているとは限らなくなってしまうため、システムクラッシュ時に最大一秒間のトランザクションの操作内容が失われてしまう。
  • 2: トランザクションコミットのたびにログバッファの内容がログファイルに書き込まれるが、テーブルスペースへのflushは約1秒間に1度実行される。 0 と同様にパフォーマンスの向上が見込めるがトランザクションは最大1秒間失われるリスクがある。

チェックポイント処理

概要

チェックポイント処理はログによるシステムのリカバリにかかる時間を短縮するためのテクニック。

ログファイルとバッファプールのサイズを大きくするとそれだけ多くのダーティページを持つことができるが、ダーティページが多いとそれだけシステム障害発生時にログに示された多くの処理を行いシステムを復旧させることになり、システムのリカバリに時間がかかるようになる。

チェックポイント処理は定期的にメモリ内のページをディスクに書き込み(そのとき特別なチェックポイントレコードをログに書き込む)、リカバリ時には最後のチェックポイント処理より後のログのみを読めばよいということにしてリカバリにかかる時間を短くする。

sharp checkpoint (単純なチェックポイント処理)

最も単純なチェックポイント処理の手続きは以下のような感じ

この単純なチェックポイント処理は(ダーティページをディスクにすべて書き出すまで新しいシステムを止めることになるので)大きなパフォーマンスの低下につながる。

fuzzy checkpoint

単純なチェックポイント処理の軽量(非同期)バージョン。以下のような手続き

ファジーチェックポイントは前のファジーチェックポイント処理によるダーティページのディスクへの書き込みが全て終了しないと開始することはできない

ファジーチェックポイントでは、トランザクションの開始を抑制するのはダーティページの一覧を知るためにバッファをスキャンしてチェックポイントレコードを書いている間だけで済む。

単純なチェックポイント処理では、リカバリ時には一番最後のチェックポイントレコード以降のログからリカバリすればよかったが、ファジーチェックポイントではチェックポイントレコードを書いた後に非同期的にダーティページがディスクに書き出されるため、最後のレコード以降のログからリカバリしてしまうと、最後から二番目より後で実行されたトランザクションによる操作が失われてしまうかもしれない。

f:id:tanishiking24:20171005175916p:plain

そこでリカバリ時は最後から二番目のチェックポイントレコード以降のログからリカバリ処理を実行することで漏れなくリカバリすることができる。(ファジーチェックポイントが開始する時、以前のファジーチェックポイントによるフラッシュは終了しているから)


ファジーチェックポイントでは非同期的にいい感じに少しずつページををディスクにflushしていく。InnoDBでは「いい感じに」flushしていくテクニックとして adaptive flushing と呼ばれるテクニックが実装されている。 adaptive flushing ではダーティページの割合や、ログの割合と innodb_io_capacity の値に応じて動的にフラッシュする量を決定するそうだが、正直まだよくわかってない...


InnoDBでは以下の状況になったときに sharp checkpoint が発生する

  • バッファプール内のダーティページの割合が innodb_max_dirty_pages_pct を超えたとき
  • ログファイルの使用率が大きくなってきたとき

https://dev.mysql.com/doc/refman/5.6/ja/innodb-performance-adaptive_flushing.html

Log Sequence Number

(物理)論理ログによるリカバリ操作が冪等でないということをトランザクションログの項で書いた。

これを解決するためには、リカバリ時に更新レコードをページに適用してもよいのかどうかを認識する必要がある。

そのために、各(ディスクに書かれた)ログレコードにに単調増加するID(Log Sequence Number)(LSN)を振っておく。各ページはそのページに適用した最新のログのLSNを持っておき、リカバリ時には ページが持つLSN >= ログレコードのLSN ならばそのログレコードによる更新は既にページに反映されており再実行する必要がないということがわかる。

InnoDBではLSNはlog bufferへの更新が行われたバイト数の合計が採用されているようだ。 SHOW ENGINE INNODB STATUS[LOG] セクションで確認できる。


LSNによる冪等性の確保は便利だがUNDO操作が複雑になる。というのもAbortにより更新操作を取り消すと、そのページに対する最新のログレコードが(LSNが)分からなくなってしまうから。

なんとなく取り消し操作以前のページに対する操作のログレコード(のLSN)を当てがってやれば良さそうに感じるけれど、以前のログレコードをページに割り当てるということはそのログの操作がまだ反映されていないということになり、おかしくなってしまう。

解決策としては取り消し操作自体をログレコードに残してやればよい (git revert みたいな) abortによる取り消し操作したときにログの歴史を改変するといろいろ大変(LSNの仕組みが破綻)だから、取り消し操作をしたよ〜っていうのをログに書いておけば、歴史を改変しなくてもリカバリ時に取り消し操作を適用できる。

関連するパラメータ

innodb_io_capacity

  • バックグランドで実行されるIO操作の帯域上限
  • innodb_io_capacity の値は innodb_io_capacity_max で定義された最大値まで、100 以上の任意の数値に設定できる
  • システムが1秒あたりに実行できるIO操作の数に設定するのが理想のよう
  • この値があまりに大きすぎるとファジーチェックポイントのバックグランドでのflushが一瞬で終わってしまいキャッシュを利用する恩恵が薄くなってしまう
  • 大きすぎるとwrite combiningが効かない

innodb_max_dirty_pages_pct

バッファプール全体に対するダーティページの比率の上限。これを越えるとInnoDBは強制的に(sharp?)チェックポイント処理を行う。

innodb_adaptive_flushing

adaptive flushing を用いてページのフラッシュを行う。

innodb_adaptive_flushing_lwm

ログの割合がこの値を越えると、 innodb_adaptive_flushing を設定していなくとも adaptive flushing によるディスク書き込みが行われるようになる。

その他

InnoDBロールバックセグメント

いわゆるUNDOログの格納場所、InnoDBではトランザクションログにはREDO情報のみを持ち、UNDO情報はこのロールバックセグメントに持つようです。 ロールバックセグメントは(デフォルトでは)テーブルスペース上に領域が確保され、UNDOログはページへの更新と同様にバッファプールにキャッシュされる。

f:id:tanishiking24:20171005173701p:plain:w600

ロールバックセグメントは基本的にトランザクションのUNDOのための仕組みだが、同時にMVCCのための仕組みでもある。

各行はUNDOログへのポイントを持ち、各UNDOログはさらに古いUNDOログへのポインタを持っている。他のトランザクションが古いデータを読むときはそのトランザクションの開始時刻からどのUNDOログを読むべきかがわかる。

innodb_purge_threads

InnoDBでは行が削除されたとき、テーブルスペースから即座に消されるわけではなくて行が削除されたことを示すマークがつけられる。これはMVCCによってその行が削除される前のデータ(UNDOログ)を参照するトランザクションが存在するかもしれないためである。

削除マークがつけられた行はその行を参照するトランザクションがなくなった時点で削除可能になり、バックグランドで削除処理が行われる(パージ処理)

innodb_purge_threads はこのパージ処理を行うスレッドの数を指定する。更新処理の多い環境で History list length (undoログの長さ)が多い場合に大きくすると良いみたい。 (history list lenght は SHOW ENGINE INNODB STATUS で確認できる)

ダブルライトバッファ

  • InnoDBではダーティページをテーブルスペースに書き込む際、まずダブルライトバッファ(ディスク上)に書き込んでから実際のデータのページに書き込む。
  • InnoDBリカバリ時にダブルライトバッファに書かれたデータをチェックし、それが完全ならばそのデータを実際のページに書き込み、不完全ならそのデータを破棄する。これによりページの部分書き込みを防ぐ。

innodb_doublewrite

ダブルライトバッファを有効/無効にする。無効にすると当然リカバリ時のDBMSによる原子性が保証されなくなるが、パフォーマンスが少し向上する。

ファイルシステムや(ディスク+)カーネルレベルでのアトミックライトが保証されているのであればダブルライトバッファを利用せずとも部分書き込みを排除することができるため無効にするとお得。

まとめ

教科書的なトランザクション技術からInnoDBの実装/パラメータをお勉強してみた。まとめてたら書きたいことがどんどん出てきて壮大になってしまったのでこのへんで。トランザクション技術楽しい!!٩( 'ω' )و

参考にあげてる資料はどれも最高に詳しくてわかりやすいのでもっと知りたい場合はそちらを見ると良さそう。

ご指摘などあればTwitterで優しく指摘してもらえると喜びます。