たにしきんぐダム

プログラミングやったりゲームしてます

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年になっても日本語入力はやっぱり難しい。

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で優しく指摘してもらえると喜びます。

Pythonでの文字コードの取り扱い

定期的にこの手の記事が上がってる気がするけどあげていく。

unicode型とかstr型とかいう言い方するとわかりにくいけど、データの実体がどういうものかわかってれば理解しやすいよね。

参考

Unicode

Unicode では全ての文字にID(コードポイント)(0 ~ 0x10FFFF)をふっている。コードポイントを表す時は U+{16進数} と書く。

  • ord()により、ある文字に対応するコードポイント(の10進数表記を得ることができる)
  • chr()により、あるコードポイント(の10進数整数)から対応する文字を得ることができる
>>> ord("A")
65
>>> hex(ord("A"))
'0x41'
>>> chr(ord("A"))
'A'

ちなみに各コードポイントは文字に必ずしも割り当てられているわけではない。サロゲートコードポイントなどがその一例だが今回はその説明については省略する。

文字符号化方式

Unicodeにより各文字にはそれぞれコードポイントが割り当てられいる、それではこれをコンピュータによってどのように表現するかであるが、 UTF-8 UTF-16 UTF-32 といった符号化方式が存在する。それぞれ 8bit 16bit 32bit を単位とする符号化方式である。

UTF-16

16bitで表すことのできる U+0000 ~ U+FFFF までは1符号単位(16bit)により表す。

16bitだけで表現できない U+10000 ~ U+10FFFF はサロゲートペアという二つの符号単位の塊(16bit * 2)により表現する。

サロゲートペアを構成するコードポイントはサロゲートコードポイントと呼ばれる、サロゲートコードポイントはさらに以下のように分けることができて

上位サロゲートと下位サロゲートによりサロゲートペアを構成する

UTF-8

8bitを符号単位とする符号化方式 1byteで表現できる U+0000 ~ U+007F は1byte 2byteで表現できる U+0080 ~ U+07FF は2byte というようにコードポイントによって異なる長さのバイトシーケンスによりいい感じに符号化を行う

ASCII

ASCIIは7bitにより表される各整数(0~127)に対して文字を割り当てた符号化方式 ord() により得られる整数(0~127)は Unicode code point でありかつ ascii code でもある。 unicode code point 0~127 の文字に関しては Unicode と ascii は互換性がある。

Python3 における str型 と bytes型

ここまできたらPythonにおけるunicode型とstr型が理解できると思う。

Python3では str型Unicode文字(Unicode code point のシーケンスで表された文字)を実体としており、 bytes型utf-8utf-16やasciiといった符号化方式でエンコードされたバイトシーケンスが実体としている。

>>> type("🍣")
<class 'str'>
>>> hex(ord("🍣"))
'0x1f363'  # Unicode code point
>>> "🍣".encode('utf-8')  # unicode code point を utf-8 でエンコード
b'\xf0\x9f\x8d\xa3'  # バイトシーケンス
>>> type(b'\xf0\x9f\x8d\xa3')
<class 'bytes'>
>>> b'\xf0\x9f\x8d\xa3'.decode('utf-8')  # バイトシーケンスをutf-8でエンコードされたものだと思い込んで、unicode code point にデコード
'🍣'

Python3でのデフォルトエンコーディングUTF-8 バイトシーケンス<--->Unicode code point のエンコード/デコードはデフォルトではutf-8で行われる

>>> import sys
>>> sys.getdefaultencoding()
'utf-8'

ファイルの先頭で以下のようにエンコーディングを指定することもできる

# coding: -*- <encoding name> -*-

UnicodeDecodeError/UnicodeEncodeError

バイトシーケンス => Unicode code point への変換を考えてみる

# ascii互換の文字(code point 0~127 の文字)は utf8 バイトシーケンス を
# ascii バイトシーケンスだと思って decode しても unicode code point が得られる
# 0~127 の文字はutf-8でも 1byte だし (128~255 はasciiではない...)
>>> 'A'.encode('utf-8').decode('ascii')
'A'
# ascii非互換の文字のutf-8バイトシーケンス取得
>>> 'あ'.encode('utf-8')
b'\xe3\x81\x82'
# これをasciiだと思ってDecodeを試みる
# \xe3 などは 0~127 外(非ascii) なのでDecodeに失敗する(UnicodeDecodeError)
>>> 'あ'.encode('utf-8').decode('ascii')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position0: ordinal not in range(128)

次に Unicode code point => ascii バイトシーケンスへの変換を考えてみる

# Unicode code point (str型) をバイト列(ascii encode)に変換する際
# コードポイントが0~127ならOK
# それ以外ならascii encode に失敗 (UnicodeEncodeError)
>>> 'A'.encode('ascii')
b'a'
>>> 'あ'.encode('ascii')
UnicodeEncodeError: 'ascii' codec can't encode character '\u3042' in position 0: ordinal not in range(128)

補足 Python2 の str型 と unicode

Python2におけるstr型はPython3のそれとは違う

  • Python2のstr型 = Python3のbytes型 = バイトシーケンス
  • Python2のunicode型 = Python3のstr型 = Unicode code point シーケンス

混乱するね

Python3では 'a' といった表記の文字列は Unicode code point で表されていたが、 Python2では 'a' はバイトシーケンスで表される。 Python2でユニコードコードポイントを得るには u'a' このように書く

>>> import sys
>>> sys.version_info
sys.version_info(major=2, minor=7, micro=13, releaselevel='final', serial=0)
>>> type('a')
<type 'str'>  # ここで言うstrはバイトシーケンスであることに注意
>>> type(u'a')
<type 'unicode'>  # これは unicode code point

Python2 のデフォルトエンコーディング

Python2 でのデフォルトエンコーディングはascii

>>> import sys
>>> sys.getdefaultencoding()
'ascii'

なので

# python3 ではこれは utf-8 エンコード、デコードだが
>>> sys.version_info
sys.version_info(major=3, minor=6, micro=1, releaselevel='final', serial=0)
>>> 'あ'.encode()
b'\xe3\x81\x82'
>>> b'\xe3\x81\x82'.decode()
'あ'
>>> import sys
>>> sys.version_info
sys.version_info(major=2, minor=7, micro=13, releaselevel='final', serial=0)
# unicode code point を ascii エンコードしようとする、「あ」はasciiの範囲外
>>> u'あ'.encode()
UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal in range(128)