たにしきんぐダム

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

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)

Scala関西Summit2017に参加/LTしました #scala_ks

参加しました!今年は4トラックもあってScalaコミュニティの盛り上がりを感じる良いイベントでした。 https://skug.connpass.com/event/62304/

ついでにScala製インタプリタをブラウザで動かす3分クッキングというタイトルでLTもしてきました。


今年のトーク内容は入門内容から、Scala導入事例紹介、Akka、FunctionalProgramming、言語コア、設計などなど多岐に渡っており、いろんな人に門戸を開いたバランスの良いカンファレンスだなと思いました(運営のみなさまありがとうございます)

個人的にはこのあたりのトークが特に興味深かったです。

Scalaインタプリタをブラウザで動かす3分クッキング

インターネットにある資料を参考にScalaでおもちゃレベルのmini-MLインタプリタを昔作っていたのでそれを引っ張り出して、Scala.jsを使ってブラウザで動かして遊んでみたよという話。 似たような取り組みは id:motemen さんが3年前にやってたり、いろんな人がやってると思います。

(あらかじめこちらに作ったものが…という3分クッキングあるあるネタが受けたのはよかった)

Scala.jsを使うのは初めてだったけれど、パーサーコンビネータに(Scala.jsに公式対応してる)fastparseを使っていたのもあってシュッとビルドできたし、Annotationによるexportするjsモジュールの指定や、crossProjectを使ってjs向けにだけビルドしたい部分でJVM向けにだけビルドしたい部分とコアロジックをうまく切り分けて管理・テストできるのも便利だった。

勉強用に作ったものなのでこれからも継続して勉強しながらいろいろ実装していきたい٩( ‘ω’ )و


今年もScala関西Summit楽しかったです!開催してくださった運営・スピーカー・スポンサーのみなさんありがとうございました!

YAPC::Fukuoka 2017 HAKATA に行ってきた

yapcjapan.org

特に発表はしませんでしたが、聴講のみで参加してきました ブログを書くまでがYAPCなので感想エントリを書いておく。

分散ユニークID採番機katsubushiとWebアプリケーションへの応用例

分散ユニークID採番機 katsubushi と Web アプリケーションへの応用例 / katsubushi // Speaker Deck

個人的に一番良かったトークsnowflake方式のbit形式で、1ホスト1プロセス起動するマイクロサービスとして利用される。

bit形式は上位から

  • 最上位bitは0固定、signed 64bit integer 対応のため
  • Timestamp: 41bit Epochからの経過時間(ms)
  • WorkerId: 10bit クラスタ内で一意のID
  • Sequence: 12bit 同一Timestamp内での連番

一番気になったのはWorkerIDをどう一意に取得するかでした、WorkerIdが重複してしまうと同一時刻に採番したIDが重複して破滅する。手でぺたぺた重複しないように採番しても良いが、動的にサーバーが起動する場合はどうするのか。発表内では

  • IPアドレスから採番(第3,4octetから計算)
  • IPが使えない場合は fujiwara/raus により指定範囲内(ここでは0~1023)のIDを取得

という方法が紹介されていた。IPから決める方法は順当だなと思いつつ、WorkerIDが10bitしかないのでIPの設定しくったら一瞬で破滅しそうで不安だなと思いながら発表を聞いていたら fujiwara/raus というソフトウェアが紹介された、これはRedisのPubSubを使って指定範囲内のIDを重複なく取得するという優れもの。その発想はなかった…すごい…

ID採番に関する知見が大量に得られる良いトークでした。

Web application good error messages and bad error messages

Web Application Good Error Message (and Bad Error Message) // Speaker Deck

良いエラーメッセージとはどういうものか、悪いエラーメッセージはどういうものかというトーク

  • 何が起きているか(必須)
  • 簡潔であること(好ましい)
  • 対処手段が提示されていること(最高)
  • 解決手段が提示されていること(最高)

確かに下に行けいくほど(トーク内ではエラーメッセージの次元が高くなると表現していた)良いエラーメッセージだというのは何となく思っていたが、それが整理されて良かった。エラーメッセージが無い、誤ったエラーメッセージが返るのはとにかく最悪なのでやめるゾ。

Web API の未来

YAPC::Fukuoka 2017 HAKATA // Speaker Deck

GraphQLの話、GraphQLは一切触ったことが無いのだが、RESTなAPI設計を心がけるとフロント側がたたくAPIの数が大量に出てきて辛いという話は共感があって、それを解決するのがGraphQLとのこと。 でもGraphQLってそれのためのAPI生やさないといけないんでしょ?って思っていたがフロントとバックエンドの間に GraphQL proxy を挟んで、フロントとproxyはGraphQLで会話するけど、proxyとバックエンドは普通に従来のREST APIでやりとりするみたいなことしても良いんですね。

スキップしていいテスト、スキップしてはいけないテスト 〜速さと信頼を兼ねたテストコードを構築する術〜

スキップしていいテスト、スキップしてはいけないテスト 〜速さと信頼を兼ねたテストコードを構築する術〜 / Need for speed of testing in Perl5 Web Application. // Speaker Deck

テスト書いてますか?という啓蒙的な話ではなくて、肥大化してきて大量のテストをいかに短い時間で終わらせるようにするかという話、一般的な話というよりはケーススタディ。テスト書いていることは当たり前でどうテストを郭嘉が重要な時代

テストがめちゃくちゃ重いと、リリース前のフルテストに大量の時間をくってしまう。

  • テスト用のダミーユーザーをDBにINSERTするのに時間がかかっていた。
    • 関連テーブルが20個以上、フルテストなら1900ユーザー程度を挿入
    • フルテストのはじめにbulk insert、ジェネレートメソッドははじめにbulk insertしたユーザーを1つずつ返していく。
    • フルテストが30分->19分に(すごい)(ユーザーの挿入に11分かかってたって…)
  • 金の弾丸
    • はい
  • 同じテスト何回も走らせても意味ないだろう

テストはもうみんな書いてるでしょうっていう話が良かった。

福岡のIT企業さんが福岡の良いところを紹介していた。福岡は(東京と比べて)家賃やオフィス代が安いし、エンジニア文化の活気がある。東京からUターンする人も福岡でキャッチできる。 僕は福岡出身なので福岡が盛り上がるのは嬉しいなと思ったのと、関西もそんな感じなので是非関西にIT企業増えてくれと思った。LINE福岡さんのオフィスはでかくて良かった、ありがとうございました!

次回は沖縄OIST、沖縄行きたい。

yapcjapan.org