2018年02月08日

Re: Configuring sql.DB for Better Performance

はてなブックマークに登録

Configuring sql.DB for Better Performance という記事を知りました。 コネクションプールの大きさを制御する3つの設定を丁寧に解説されたとても良い記事です。

しかし、この記事で推奨されている設定については同意することができません。私が推奨する設定とその理由を解説していきたいと思います。

Limit ConnMaxLifetime instead of MaxIdleConns

Allowing just 1 idle connection to be retained and reused makes a massive difference to this particular benchmark — it cuts the average runtime by about 8 times and reduces memory usage by about 20 times. Going on to increase the size of the idle connection pool makes the performance even better, although the improvements are less pronounced.

この、 "to this particular benchmark" というのが問題です。このベンチマークでは、8並列で常にDBにクエリを投げ続けています。1つのクエリが終了するとすぐに次のクエリを投げるので、 DB.SetMaxIdleConns(1) で大きな効果が現れました。

このベンチマークの動作は、例えばDBに大量のデータを挿入するバッチ処理などに当てはまりますが、Web アプリケーションなどには当てはまりません。

1秒間に1000回クエリを実行するアプリケーションを想定した簡単なシミュレータを書いてみました。クエリは一様分布でランダムなタイミングで実行され、各クエリと新規接続には10msかかるとします。 (このシミュレータのgist)

MaxOpenConns(20) の時、 MaxIdleConns(4) と MaxIdleConns(10) の動作を比べてみましょう。オレンジの線は総接続数、青い線は使用中の接続数、緑の線は接続が利用可能になるのを待っている時間の最大値をミリ秒で表しています。

maxidle-4-vs-10

1000回のクエリを実行するのに、 MaxIdleConns(4) だと 285 回接続していますが、 MaxIdleConns(10) だとそれを 69 回まで減らすことができています。一方で、負荷が止まった後もずっと維持し続ける接続も増えてしまっています。

今度は SetMaxIdleConns(100); SetConnMaxLifetime(time.MilliSecond * 300) のシミュレーション結果を見てください。

maxlifetime-300

20x4=80 回の接続をしています。 MaxIdleConns(10) のときの 69 回よりも多いですが、これは動作をわかりやすくするために lifetime を短く設定しているためです。シミュレーション時間を100秒に伸ばしたら、MaxIdleConns(10) の場合では接続回数はおよそ 690 回になり、 SetConnMaxLifetime(time.Second * 30) の場合の接続回数は 80 回になるでしょう。

このグラフで、再接続が特定のタイミングに集中し、そのタイミングでレイテンシが伸びてしまっているのが気になるかもしれません。これはシミュレーションが完全に一様分布になっていて、最初に全ての接続がほぼ同時に作られてしまっているからです。時間によって負荷が変動するアプリケーションでは、接続が作られるタイミングがもっと分散するので、このスパイクは発生しにくいはずです。次のグラフは、200msかけて段階的に負荷が増えた後に、上のグラフと同じ1000msの負荷がかかったときのものです。

maxlifetime-300-2

SetConnMaxLifetime を使う他の理由

DB.SetConnMaxLifetime() を提案し実装したのは私です。このAPIはアイドルな接続を減らす SetMaxIdleConns() よりも良い方法ですが、それだけではありません。

"Configuring sql.DB for Better Performance" で紹介されたとおり、 MySQL では wait_timeout という設定で接続がサーバーから切られる恐れがあります。また、OSやルーターが長時間利用されていないTCP接続を切断することもあります。どのケースでも、 go-sql-driver/mysql はクエリを送信した後、レスポンスを受信しようとして初めてTCPが切断されたことを知ります。切断を検知するのに何十秒もかかるかもしれませんし、送信したクエリが実行されたかどうかを知ることもできないので安全なリトライもできません。

こういった危険をなるべく避けるためには、長時間使われていなかった接続を再利用せずに切断し、新しい接続を使うべきです。 SetConnMaxLifetime() は接続の寿命を設定するAPIですが、寿命を10秒に設定しておけば、10秒使われていなかった接続を再利用することもありません。

接続の寿命を設定することで、他にも幾つかの問題に対処することができます。

  • DBサーバーがロードバランスされているとき、サーバーの増減をしやすくする
  • DBサーバーのフェイルオーバーをしやすくする
  • MySQL でオンラインで設定変更したとき、古い設定で動作するコネクションが残り続けないようにする

接続のアイドル時間を制限するAPIを別に追加しなかったのは、現実的な環境における性能への影響と、 sql.DB の実装の複雑さを天秤にかけた結果です。

推奨する sql.DB の設定

  • SetMaxOpenConns() は必ず設定する。負荷が高くなってDBの応答が遅くなったとき、新規接続してさらにクエリを投げないようにするため。できれば負荷試験をして最大のスループットを発揮する最低限のコネクション数を設定するのが良いが、負荷試験をできない場合も max_connection やコア数からある程度妥当な値を判断するべき。
  • SetMaxIdleConns() は SetMaxOpenConns() 以上に設定する。アイドルな接続の解放は SetConnMaxLifetime に任せる。
  • SetConnMaxLifetime() は最大接続数 × 1秒 程度に設定する。多くの環境で1秒に1回接続する程度の負荷は問題にならない。1時間以上に設定したい場合はインフラ/ネットワークエンジニアによく相談すること。

@methane
songofacandy at 19:35
この記事のURLComments(0)golang 
2018年01月30日

最近のPython-dev(2018-01)

はてなブックマークに登録

バックナンバー:

Python 3.7b1 が29日の予定です。問題なければ日本時間の今日中に出るはずですが、buildbotやTravisが不安定なので少し遅れるかもしれません。これで feature freeze なので新機能追加やパフォーマンス向上などは基本的に終わりです。

beta 前の駆け込みで、先月号で紹介したAcceptされたPEPを実装する大型コミットがたくさんマージされました。(間に合わなかったものは無いはず。たぶん。)

今回は私が関係していたところを中心に紹介していきます。


dict の順序の言語仕様化

Python 3.6 から dict が挿入順序を保存するようになりましたが、この挙動は実装詳細であり、まだ言語仕様ではありませんでした。そのため、他の Python 実装で同じ挙動になることは保証されません。具体的には PyPy は同じ挙動(というかCPythonがPyPy実装を真似した)ですが、 MicroPython は違います。

この順序に依存しないように、内部では維持している順序をイテレート時にランダム化するような改造をするか、便利だしいっその事言語仕様にしてしまうかという話題があって、結果として 3.7 からは言語仕様になることになりました。

これに関連して、僕が collections.OrderedDict からリンクリストを削ってメモリ使用量を半減する実装を試していたのですが、挿入順を維持するためだけなら dict を使えるのでこの実装はディスコンになりました。

hasattr(), getattr() の高速化

https://github.com/python/cpython/pull/5173

hasattr(), getattr() は探索する属性が見つからない場合、内部で一度 AttributeError を発生してからそれを握りつぶして、 hasattr なら False, getattr なら第三引数に渡されたデフォルト値を返すという挙動をしていました。

この例外を発生させる+潰すの組み合わせが若干のオーバーヘッドになっているのでなんとかしたいのですが、すべてのケースで回避することはできません。 Python で __getattr__ を定義しているときにも AttributeError を投げるのが決まり(プロトコル)ですし、Cレベルでこれに相当する tp_getattro というスロットも同じプロトコルにもとづいています。

しかし、属性アクセスについて何らかのカスタマイズが不要な場合、 tp_getattro にはデフォルトで PyObject_GenericGetAttr() という関数ポインタが設定されています。 tp_getattro を呼び出す前に、それが PyObject_GenericGetAttr であれば、属性が見つからなかったことを例外を使わずに返す特殊化された関数を呼ぶようにしました。

これで、 getattr をカスタマイズしていない多くの型で getattr() や hasattr() が高速になりました。 例えば Django のテンプレートで hasattr(s, '__html__') としているところがあり、かなりの回数で False を返すので、少し高速化できたことを確認しています。

私は hasattr(), getattr() だけを高速化していたのですが、さらに Serhiy さんが _PyObject_Lookup という名前の新プライベートAPIとして整備して、 Python 内部で PyObject_GetAttr() を呼んでから AttributeError を消していたすべての呼び出し元を新APIを使うように書き換えてくれました。

定数畳み込み

例えば 1 + 2 というコードがあったとき、1 と 2 という定数をスタックに積んでから BINARY_ADD するというバイトコードが一旦作られます。

その後、 peephole と呼ばれているバイトコードレベルの最適化で、連続する LOAD_CONST のあとに BINARY_ADD が来たときは、1 + 2 を計算してしまって結果の 3 を定数テーブルに登録し、1つの LOAD_CONST に置き換えるという最適化をしていました。これを定数畳み込みといいます。

しかし従来の方法には欠点がありました。畳み込み前にテーブルに入っていた定数が不要になるかもしれないのに、そのまま残ってしまっていたのです。次のサンプルコードでは、定数3しか利用していないのに、1と2がテーブルに残っている例です。 (なお、co_consts の最初にある None は docstring なので消せません)

>>> import dis
>>> def x():
...     return 1+2
...
>>> x.__code__.co_consts
(None, 1, 2, 3)
>>> dis.dis(x)
  2           0 LOAD_CONST               3 (3)
              2 RETURN_VALUE

定数畳み込みのあとに定数テーブルを再構築するという最適化が提案されたこともありますが、定数テーブルを2回作り直すよりも定数畳み込みをバイトコード生成前のASTの段階でやってしまう方が筋が良いだろうということで却下されていました。

そしてASTレベルの定数畳み込みは、Eugene Toderさんが実装されていたのですが、それも何年も放置されていました。 それを見つけて今のバージョンで動くように手直して、 Python 3.7 に入りました。 Python 3.7 では上の例はこうなります。

>>> x.__code__.co_consts
(None, 3)
>>> dis.dis(x)
  2           0 LOAD_CONST               1 (3)
              2 RETURN_VALUE

TextIOWrapper の encoding を変更可能に

「Python 3.7 でテキストファイルのエンコーディングを初期化後に変更可能になります」 という記事で紹介したのでそちらを参照してください。

lru_cache の省メモリ化

functools.lru_cache は dict と双方向リンクリストを使って LRU を実装しています。

dict と双方向リンクリストという組み合わせは OrderedDict と同じです。同じ実装が2種類あるのは無駄だなぁと思って一度 OrderedDict を使う形で書き直してみたのですが、パフォーマンスが落ちてしまいました。 OrderedDict は dict を継承しているしがらみが強くて、要素を削除する時などに内部で探索をなんどかやり直していることがあり、 dict + 双方向リンクリストとしての実装は lru_cache の内部実装の方が圧倒的に素直だったのです。

ということでこの試みは失敗したのですが、その際に lru_cache の双方向リストの各エントリがGC可能オブジェクトになっていることを発見しました。これにより、一要素ごとに3ワード (64bit マシンでは 24byte) のオーバーヘッドがかかり、GCの処理時間も長くなります。

Python の巡回参照GCを動作させるには、各エントリを個別に走査してもらわなくても lru_cache 本体が走査されるときにリンクリストを舐めて行けばすむということを思いつき、各エントリのGCヘッダを削除することができました。これでキャッシュされている要素数 * 24バイトの削減と、lru_cacheをヘビーに使っているアプリでのGC速度の向上が見込めます。

ABCのC実装

https://github.com/python/cpython/pull/5273

Python の起動時間を遅くする3大要因が ABC と Enum と正規表現です。(独断と偏見)

起動時にほぼ確実に import されるモジュールに _collections_abc があるのですが、これを見ると、たくさんの ABC が定義され、たくさんの型がそこに register されているのがわかります。

以前に紹介した importtime オプションで確認してみます。

$ local/py37/bin/python3 -Ximporttime -c 42
import time: self [us] | cumulative | imported package
...
import time:        74 |         74 |       _stat
import time:       202 |        275 |     stat
import time:       166 |        166 |       genericpath
import time:       244 |        409 |     posixpath
import time:      1696 |       1696 |     _collections_abc
import time:       725 |       3105 |   os
import time:       218 |        218 |   _sitebuiltins
import time:       160 |        160 |     _locale
import time:       155 |        315 |   _bootlocale
import time:       500 |        500 |   sitecustomize
import time:       220 |        220 |   usercustomize
import time:       692 |       5047 | site

ABCの定義とregisterの両方を根本的に高速化するために、Ivan Levkivskyi さん (つづりが難しい) がC実装を作ってくれたのですが、色々問題があって協力して改善中です。

量としては多くないんだけどメタクラスと weakref を扱うので正しく実装するのが面倒なんですよね。。。

なんとか 3.7b1 までにマージしても問題なく動きそうなところまでは持っていったのですが、 3.7 のリリースマネージャーが b1 後にしても良いよと例外を認めてくれたので、一旦落ち着いて実装を磨き直す予定です。

これがマージされると、 Python の起動時間 (python -c 42) はこんな感じになります。

バージョン起動時間(ms)
3.616.4
3.7b114.4
3.7b1 + C-ABC13.9

また、 python -c 'import asyncio' の時間も、 Python 3.6, 3.7b1, C-ABC で 88.2ms -> 58.7ms -> 57.6ms と早くなって来ています。

(追記) str, bytes, bytearray に isascii() メソッドを追加

Python の str の isdigit() などのメソッドは Unicode 対応しているために非ASCII文字に対しても True を返してしまいます。また int() も Unicode 対応しています。

>>> s = "123"
>>> print(ascii(s))
'\uff11\uff12\uff13'
>>> s.isdigit()
True
>>> int(s)
123

しかし、ASCII文字だけを受け付けたいというケースもあるでしょう。その時に .encode('ascii') が例外を投げないかチェックするのは手間です。

そこで str.isascii() メソッドの追加を提案しました。これを使えば s.isascii() and s.isdigit() で ASCII の整数だけが使われているかをチェックできます。

いろいろな意見はあったものの基本的に反対意見はなくて、Guidoがbytesとbytearrayにも同じメソッドを追加しておいてというメッセージで決まりました。

実装の話として、 str は内部で ASCII フラグを持っている (このフラグで UTF-8 へのエンコードがスキップできる) ので O(1) ですが、 bytes と bytearray はバイト列を舐めて最上位ビットが立っていないかチェックしているので文字列長nに対して O(n) になります。

(追記) PEP 567 -- Context Variables

引数を使わずに関数呼び出しを超えて状態を持ち回りたい事があります。

例えばWebアプリケーションだとしたら、ログを書く時に現在のリクエストのIDとかユーザーIDを残したいけれども、ログを書く全ての関数の引数にそれらを追加したくないなどです。

Python の標準ライブラリで言えば、 decimal モジュールが演算精度などをコンテキストとして管理しています。

こういった状態は、マルチスレッドではスレッドごとにコンテキストが異なるので、従来はスレッドローカル変数に格納していました。

しかし、 asyncio などの非同期フレームワークでは、複数のコンテキストが1つのスレッドで動くので、スレッドローカルでのコンテキスト管理がうまく行きません。そこでスレッドと独立したコンテキスト管理のためのライブラリが PEP 567 で追加されました。

これを使えば、各スレッドが「現在のコンテキスト」を持っているものの、スレッド内でコンテキストの切り替えが明示的にできるので、 asyncio だとイベントループが関数呼出しする毎にそのコンテキストを復元してやることができます。

MLでの議論は長すぎて私は見てなかったのですが、そろそろ Accept されそうだというところで依頼されて実装の方のレビューをしていました。

内部実装としては HAMT と呼ばれるアルゴリズムを利用していて、名前は聞いたことがあったもの詳細は知らなかったので、それを理解しながらC実装をレビューするのは楽しかったです。

songofacandy at 16:01
この記事のURLComments(0)Python 
2018年01月23日

Pythonアプリの起動を高速化する

はてなブックマークに登録

pipenv 9.0.2 のリリースでCLIの大幅な高速化をしたというアナウンスを見かけました。

興味を持ってすぐに試してみたのですが、あまり速く感じられませんでした。そこで Python 3.7 の新機能を使って速度を調査することにしました。

この記事ではその新機能と実際の使い方を紹介します。

起動時間 ≒ import時間

pipenv -h のようなコマンドの実行時間は、実際にヘルプメッセージを表示するための時間よりもずっと長くなります。

アプリケーションが起動するときには、設定ファイルの読み込みなど一定の処理が必要になるものです。

しかし、Pythonアプリケーションの場合、モジュールのインポートが起動時間の殆どをしめる事が多いです。

たとえば私の環境では pipenv --version は 800ms かかったのですが、そのうち 700ms は import pipenv に使われていました。

$ time local/py37/bin/python3 -c 'import pipenv'

real    0m0.696s
user    0m0.648s
sys     0m0.048s

$ time local/py37/bin/pipenv --version
pipenv, version 9.0.3

real    0m0.812s
user    0m0.761s
sys     0m0.052s

モジュールのimportにかかった時間を表示する

Python 3.7 ではモジュールのimportにかかった時間を表示する機能が追加されました。

この機能は -X importtime オプションか、 PYTHONPROFILEIMPORTTIME=1 と環境変数に設定することで有効になります。

たとえば pipenv を調査したい時は次の2つの方法があります。

$ python3.7 -X importtime -c 'import pipenv' 2> pipenv-imports
または
$ PYTHONPROFILEIMPORTTIME=1 pipenv --version 2>pipenv-imports

前者は import pipenv だけを調査し、後者は pipenv コマンド実行中の全ての import を調査するという違いがあります。

後者の出力結果の例が こちら です。

import時間を解析する

この出力結果の最後の部分を見てみます。

import time: self [us] | cumulative | imported package
...
import time:      3246 |     578972 |   pipenv.cli
import time:       507 |     579479 | pipenv

最後の行の右側の 579479 は、 import pipenv にトータルで 579479us かかったことを意味しています。

pipenv を import する間に、たくさんの他のモジュールが import されます。上の例で言えば、 pipenv は pipenv.cli を import しています。(サブ import は2スペースでインデントされます)

最後の行の左側の 507 は、 pipenv モジュール単体の import (モジュールのグローバルの実行を含む) にたった 507us しか掛かっていないことを意味しています。579 479 - 507 = 578 972us がサブimportに掛かっています。

遅い部分を見つける

先程の出力から、時間を使っている部分木を探しましょう。幾つかの行を抜粋します。

import time: self [us] | cumulative | imported package
...
import time:     86500 |     179327 | pkg_resources
...
import time:       385 |     236655 |             IPython
import time:        22 |     236677 |           IPython.core
import time:        26 |     236703 |         IPython.core.magic
import time:       978 |     237680 |       dotenv.ipython
import time:       199 |     239032 |     dotenv
...
...
import time:      3246 |     578972 |   pipenv.cli
import time:       507 |     579479 | pipenv

pkg_resources

最初に見つけたのが pkg_resources です。注目してほしいのは、このモジュールがインデントされていないことです。つまり、 pipenv モジュールから間接的に import されているわけではないのです。

実際、 pkg_resourcespipenv スクリプト から import されていました。

$ cat local/py37/bin/pipenv
#!/home/inada-n/local/py37/bin/python3.7
# EASY-INSTALL-ENTRY-SCRIPT: 'pipenv==9.0.3','console_scripts','pipenv'
__requires__ = 'pipenv==9.0.3'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
        load_entry_point('pipenv==9.0.3', 'console_scripts', 'pipenv')()
    )

pkg_resources の import が遅いのは既知の問題で、後方互換性を壊さずに修正するのが難しい問題です。

しかし、 pkg_resources の import は回避することができます。

$ local/py37/bin/pip install wheel
$ local/py37/bin/pip install -U --force-reinstall pipenv
$ time local/py37/bin/pipenv --version
pipenv, version 9.0.3

real    0m0.704s
user    0m0.653s
sys     0m0.052s

これで、 pipenv --version コマンドの実行時間はほとんど import pipenv の時間と等しくなりました。

wheel がインストールされている時、 pip はソースパッケージ (.tar.gz など) をダウンロードしてきても、一旦 wheel (.whl) をビルドして、 wheel からインストールします。 そして wheel からインストールする時とソースパッケージからインストールする時でスクリプトを生成する処理が異なり、 wheel からインストールした場合には pkg_resources が使われないのです。

$ cat local/py37/bin/pipenv
#!/home/inada-n/local/py37/bin/python3.7

# -*- coding: utf-8 -*-
import re
import sys

from pipenv import cli

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(cli())

つまり、 wheel をインストールしておけば、ソースパッケージしか配布していないツールをインストールする時にかかる余計な時間(私の環境ではおおよそ100ms程度)を回避することができます。

IPython

次に見つけた遅い部分はここです。

import time:       385 |     236655 |             IPython
import time:        22 |     236677 |           IPython.core
import time:        26 |     236703 |         IPython.core.magic
import time:       978 |     237680 |       dotenv.ipython
import time:       199 |     239032 |     dotenv

pipenvdotenv を import して、 dotenvdotenv.ipython を、 dotenv.ipythonIPython を import しています。

これが、私の環境 (IPython がインストールされていた) で pipenv の起動が遅く感じた最大の理由のようです。

しかしなぜ IPython が import されるのでしょう? dotenv のソースを読んでみた所、どうやら IPython extension のためようです。

もちろん、 pipenv やその他の多くの dotenv のユースケースでは IPython extension は使いません。なので dotenv がオンデマンドで IPython を import するような プルリクエスト を送っておきました。

また、 pipenv は dotenv にパッチを当てた独自コピーを埋め込んで利用しているので、そこから dotenv.ipython を取り除く プルリクエスト も送っておきました。

まとめ

pipenv --version の実行時間を 800ms から 500ms に削減することができました。

$ time local/py37/bin/pipenv --version
pipenv, version 9.0.3

real    0m0.503s
user    0m0.463s
sys     0m0.040s

このように、Python 3.7 の import 時間プロファイルは、CLIアプリケーションの起動時間を高速化するためにとても便利です。


@methane

songofacandy at 16:29
この記事のURLComments(0)Python 
2017年12月27日

安価な NFC タグで秘密情報を安全に携行する試み

はてなブックマークに登録

はじめに

最近 NFC まわりの調査と実験を行っています。切り口が多く奥の深い技術ですが、NFC タグ製品について調べている過程で現在もっとも広く出回っている NXP Semiconductors 製の「NTAG21x」シリーズ (ISO/IEC 14443 Type A: MIFARE Ultralight に実装されている興味深いアクセス制御機構を知りました。

NFC タグの一般的な「手軽さ」とは裏腹に、扱いを誤るとタグがあっさり使用不能になるリスクと背中合わせであるためか今のところこの機構に言及した記事やアプリケーションはあまり見かけません。しかし、仕様を理解した上で適切に取り回せばこの安価なタグ製品の使途を大きく拡げることができるでしょう。

  • 媒体が小さく軽薄で嵩張らない
  • 非接触型であるため露出の必要がなく忍ばせやすい
  • 緊急時には簡単に破壊・破棄できる

手元ではこういった物理的な特長にも注目し、NTAG21x を秘密情報の格納に利用するアイディアを形にしてみました。データ圧縮と複数タグへの分割格納にも対応しています。今回はこの NTAG21x のアクセス制御機構に関する情報の整理を行い、試作した Android アプリケーションの内容を紹介します。

ちなみに手元では実験用に NTAG213 を計 110 枚、NTAG216 を計 25 枚調達しました。どちらもまず Amazon で少量を購入し様子見を経て後顧の憂いなく使い潰せるようにより安く入手できる Aliexpress で買い足しを行いました。参考に購入元と購入時の価格を掲載します。(個人的には現時点で Aliexpress を利用する際には万一の事故に備えての措置をとっています)

NTAG213(ユーザメモリ容量 144 バイト)

NTAG216(ユーザメモリ容量 888 バイト)

なお、本記事においては NTAG メモリ内容の参照に NXP 純正の次のアプリケーションを使用しています。

1. NTAG21x のアクセス制御機構とネイティブコマンド

データシート

アクセス制御機構の概要と簡単なデモ

  • NTAG21x メモリ上の任意のページ(1 ページ = 4 バイト)以降へのアクセス要求に対し当該タグをパスワードによって保護できる
  • パスワードによる保護は Write 要求のみならず Read 要求に対しても設定可能
  • パスワード認証連続試行回数の上限値を設定することが可能であり、この回数を超過すると当該タグの認証機能は永久に封鎖されパスワード正否にかかわらず認証通過不能となる

    デモ 1: 試作アプリから NTAG213 にデータの書き込みを行い全ページの読み書きをパスワードで保護する (動画 1分39秒)

    デモ 2: 上記デモ 1 でのパスワード設定時に併せて設定した認証試行上限回数を超過する操作を行った結果タグが自爆する様子 (動画 1分2秒)

アクセス制御機構の詳細

  • データシート中の記事
    • "1. General description"
    • "8.5 Memory organization"
    • "8.6 NFC counter function"
    • "8.8 Password verification protection"
  • 要点のまとめ
    • NTAG213 の EEPROM メモリの総ページ数は 45(0h〜2Ch)。NTAG215 は 135 ページ, NTAG216 は 231 ページであり、いずれも末尾の 4 ページが Configuration Pages である。下図の NTAG213 のメモリレイアウトを参照のこと。なお 1 ページ = 4 バイト
    • Configuration 第 1 ページの第 4 バイト「AUTH0」にはパスワード保護対象とする開始ページのアドレスを指定する。このフィールドに実在ページの範囲を超えるアドレスが格納された状態ではパスワード保護は無効。出荷時のデフォルトは 0xFF
    • Configuration 第 2 ページの第 1 バイト「ACCESS」の PROT ビットが 0 ならパスワード保護の対象は Write 要求のみ。同ビットが 1 なら Read, Write 要求の両方が対象となる
    • パスワード(PWD)は 4バイト固定。Configuration 第 3 ページへ格納。出荷時の PWD のデフォルトは 0xFFFFFFFF
      • PWD は可読文字列でなくても許容されるためバリエーションは 4 バイトで 256^4 であり用途によっては意図的に非 ASCII データを適用する選択もあり得るだろう
      • 「ACCESS」の AUTHLIM にはブルートフォース攻撃対策用にパスワード認証要求(PWD_AUTH:後出)試行回数上限を設定可能。認証連続失敗回数がこの値を超えると以降の PWD_AUTH はパスワードの正否にかかわらず永久に無効となる。試行可能回数の範囲で PWD_AUTH が成功すると失敗回数を保持する内部カウンタはリセットされる。AUTHLIM の初期値は当該機能の無効を示す 000b
    • Configuration 第 4 ページの先頭 16 ビットの Password Acknowledge(PACK)はパスワード認証通過時の肯定応答値。デフォルトは 0x0000
    • パスワード保護が有効なメモリ領域の読み書きはパスワード認証を通過し PACK を得た後に可能となる
    • PWD および PACK の読み出しは不可能。両フィールドへの READ 要求に対し NTAG は常に 0x00 を返す
    • NTAG21x のパスワード保護機構は不正なメモリアクセスの防止に特化したものでありデータ暗号化等はアプリケーション側の要件
    • Configuration Pages を永続的にロックする CFGLCK 機構にも要注意
  • フィールドの整理
    (※データシートから引用した図・表を説明のために再構成しています)


  • (参考)NTAG213 初期状態のメモリダンプ:ページ 29h 以降が Configuration Pages
    ※ NXP 提供の Android アプリ TagInfo での表示
     

関連する NTAG コマンド

上記アクセス制御機構を利用する上で最低限必要な NTAG21x ネイティブコマンドおよび関連情報を示す。

  • データシート中の記事
    • "9. Command overview"
  • 要点のまとめ
    • NTAG コマンドに対する最短のレスポンスは 4ビットの ACK or NACK である
      • Ah: Acknowledge (ACK)
      • 0h: NAK for invalid argument (i.e. invalid page address)
      • 1h: NAK for parity or CRC error
      • 4h: NAK for invalid authentication counter overflow
      • 5h: NAK for EEPROM write error
    • コマンドに付与する CRC 値は ISO/IEC 14443 の規約に準拠のこと (実装例

    • GET_VERSION コマンド
      NTAG 製品のバージョン,ストレージサイズ等を得る。Configuration Pages のアドレス取得に必要
      • リクエスト
        • コマンド 1 バイト(60h) + CRC 2 バイト
      • レスポンス
        • OK: データ 8 バイト+ CRC 2 バイト
        • NG: NAK 各値
      NTAG 21x GET_VERSION レスポンスデータの内容
      
      Byte no. Description       NTAG213 NTAG215 NTAG216  Interpretation
            0  fixed Header          00h     00h     00h
            1  vendor ID             04h     04h     04h  NXP Semiconductors
            2  product type          04h     04h     04h  NTAG
            3  product subtype       02h     02h     02h  50 pF
            4  major product version 01h     01h     01h  1
            5  minor product version 00h     00h     00h  V0
            6  storage size          0Fh     11h     13h  see following information
            7  protocol type         03h     03h     03h  ISO/IEC 14443-3 compliant
      

      NTAG 製品の識別方法

      storage size の上位 7ビットを符号なし整数値 n として解釈する。使用可能なユーザーメモリの合計サイズは、最下位ビットが 0 の場合には 2^n であり最下位ビットが 1 の場合には 2^n 〜 2^(n+1) である。これを識別に利用する。

      NTAG213 での GET_VERSION レスポンス実例

      HEX: 00 04 04 02 01 00 0F 03

      --> 0x0F = BIN: 00001111 --> BIN: 0000111 = 0x07 --> 2^7 = 128, 2^(7+1) = 256

      ※ 128〜256 バイトのユーザーメモリ空間を持つ製品は NTAG213(144 バイト)

      NTAG216 での GET_VERSION レスポンス実例

      HEX: 00 04 04 02 01 00 13 03

      --> 0x13 = BIN: 00010011 --> BIN: 0001001 = 0x09 --> 2^9 = 512, 2^(9+1) = 1024

      ※ 512〜1024 バイトのユーザーメモリ空間を持つ製品は NTAG216(888 バイト)

    • READ コマンド
      所定のページアドレスから始まる 4 ページ分(16 バイト)のデータを得る
      • リクエスト
        • コマンド 1 バイト(30h)+ 開始アドレス 1 バイト + CRC 2 バイト
      • レスポンス
        • OK: データ 16 バイト+ CRC 2 バイト
        • NG: NAK 各値
    • WRITE コマンド
      所定のページアドレスへ 1 ページ分(4 バイト)のデータを書き込む
      • リクエスト
        • コマンド 1バイト(A2h)+ 対象アドレス 1 バイト + データ 4 バイト + CRC 2 バイト
      • レスポンス
        • OK: ACK
        • NG: NAK 各値
    • PWD_AUTH コマンド
      パスワードで保護された領域へのアクセス要求に先行してパスワード認証を要求する。ここで肯定応答である PACK が得られれば所定のアクセスが可能となる(PACK の値はデフォルト 0x0000 。変更可)
      • リクエスト
        • コマンド 1 バイト(1Bh)+ パスワード 4 バイト + CRC 2 バイト
      • レスポンス
        • OK: PACK 2 バイト
        • NG: NAK 各値

2. Android API による NTAG21x のハンドリングについて

手順等

  • NTAG21x は ISO/IEC 14443 Type A であるため Android でアクセス制御機構を利用する際には標準の NfcA クラスを使用する。なお MIFARE Ultralight に特化した MifareUltralight クラスも存在するがここでは前掲のネイティブコマンド操作が処理の中心であるためあえて同クラスを利用する必然性はない
  • 所定のタグの近接を検知したらまず Tag クラスの getTechList メソッドにより当該タグの TagTechnology 文字列リストを取得。リストに "android.nfc.tech.NfcA" が含まれていれば NfcA クラスの get メソッドで NfcA オブジェクトを取得し transceive メソッドにより適宜 NTAG コマンドを呼び出すことで製品の識別と必要な処理を行う。ターゲットが NTAG21x と識別されない場合は後続処理をキャンセル
  • NfcA - API level 10
    public final class NfcA

    Provides access to NFC-A (ISO 14443-3A) properties and I/O operations on a Tag.

    Acquire a NfcA object using get(Tag).

    The primary NFC-A I/O operation is transceive(byte[]). Applications must implement their own protocol stack on top of transceive(byte[]).
    • get - API level 10
      NfcA get (Tag tag)

      Get an instance of NfcA for the given tag.

      Returns null if NfcA was not enumerated in getTechList(). This indicates the tag does not support NFC-A.

      Parameters
      • tag Tag: an NFC-A compatible tag
      Returns
      • NfcA NFC-A object
    • transceive - API level 10
      byte[] transceive (byte[] data)

      Get an instance of NfcA for the given tag.

      Send raw NFC-A commands to the tag and receive the response.

      Applications must not append the EoD (CRC) to the payload, it will be automatically calculated.


      Applications must only send commands that are complete bytes, for example a SENS_REQ is not possible (these are used to manage tag polling and initialization).

      Use getMaxTransceiveLength() to retrieve the maximum number of bytes that can be sent with transceive(byte[]).

      This is an I/O operation and will block until complete. It must not be called from the main application thread. A blocked call will be canceled with IOException if close() is called from another thread.

      Parameters
      • data byte: bytes to send
      Returns
      • byte[] bytes received in response
      Throws
      • IOException if there is an I/O failure, or this operation is cance

コードを書いて試す

  1. パスワードを設定する(write 要求からの保護)
    • NTAG21x 種別を確認
    • パスワードとして "0000" を設定
    • ページ 04h 以降を保護対象とする
              :
      
          if (isNfcA) {
            int ntagMaxPage = -1, ntagConfPage0;
            NfcA nfca = NfcA.get(tag);
            try {
              nfca.connect();
            } catch (IOException e) {
              Log.d(TAG, "NfcA.connect() err: " + e.toString());
              return;
            }
            // NTAG 種別取得
            try {
              byte[] res = nfca.transceive(new byte[]{
                (byte) 0x60, // GET_VERSION
              });
              Log.d(TAG, "GET_VERSION reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 8) {
                if (res[0] == 0x00 && res[1] == 0x04 &&
                    res[2] == 0x04 && res[3] == 0x02) {
                  byte ntagStrorageSize = res[6];
                  if (ntagStrorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStrorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStrorageSize == 0x13) {
                    ntagMaxPage = 231; // 0xE7 = NTAG216
                  }
                }
              }
            } catch (IOException e) {
              Log.d(TAG, "GET_VERSION err:" + e.toString());
            }
            if (ntagMaxPage == -1) {
              Log.d(TAG, "is not NTAG21x");
              if (nfca.isConnected()) {
                try {
                  nfca.close();
                } catch (IOException e) {
                  Log.d(TAG, "NfcA.close() err: " + e.toString());
                }
              }
              return;
            }
            // Congiguration Pages 開始位置
            ntagConfPage0 = ntagMaxPage - 4;
      
            try {
              // パスワードを設定
              int page = ntagConfPage0 + 2; // config page 2
              byte[] res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      '0', '0', '0', '0'
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
      
              // 保護開始ページを設定
              page = ntagConfPage0; // config page 0
              res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      (byte) 0x04,
                      (byte) 0x00,
                      (byte) 0x00,
                      (byte) 0x04 // 保護開始ページ
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
            } catch (IOException e) {
              Log.d(TAG, "WRITE err:" + e.toString());
            }
      
            if (nfca.isConnected()) {
              try {
                nfca.close();
              } catch (IOException e) {
                e.printStackTrace();
              }
            }
          }
              :
      
    • 上のコードにより NTAG213 のページ 04h 以降への書き込みにパスワード認証通過が必須となった状況
      AUTH0 に保護開始ページ指定
      PROT ビットはデフォルトの 0 のまま
         
  2. パスワードを設定する(read / write 要求からの保護)

    NTAG21x では write 要求のみならず read 要求に対してもパスワードによる保護が可能。ただしパスワードは両者共通

    • NTAG21x 種別を確認
    • パスワードとして "0000" を設定
    • ページ 04h 以降を保護対象とする
    • PROT ビットを立て read 要求からの保護も有効に
              :
      
          if (isNfcA) {
            int ntagMaxPage = -1, ntagConfPage0;
            NfcA nfca = NfcA.get(tag);
            try {
              nfca.connect();
            } catch (IOException e) {
              Log.d(TAG, "NfcA.connect() err: " + e.toString());
              return;
            }
            // NTAG 種別取得
            try {
              byte[] res = nfca.transceive(new byte[]{
                (byte) 0x60, // GET_VERSION
              });
              Log.d(TAG, "GET_VERSION reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 8) {
                if (res[0] == 0x00 && res[1] == 0x04 &&
                    res[2] == 0x04 && res[3] == 0x02) {
                  byte ntagStrorageSize = res[6];
                  if (ntagStrorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStrorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStrorageSize == 0x13) {
                    ntagMaxPage = 231; // 0xE7 = NTAG216
                  }
                }
              }
            } catch (IOException e) {
              Log.d(TAG, "GET_VERSION err:" + e.toString());
            }
            if (ntagMaxPage == -1) {
              Log.d(TAG, "is not NTAG21x");
              if (nfca.isConnected()) {
                try {
                  nfca.close();
                } catch (IOException e) {
                  Log.d(TAG, "NfcA.close() err: " + e.toString());
                }
              }
              return;
            }
            // Congiguration Pages 開始位置
            ntagConfPage0 = ntagMaxPage - 4;
      
            try {
              // パスワードを設定
              int page = ntagConfPage0 + 2; // config page 2
              byte[] res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      '0', '0', '0', '0'
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
      
              // 保護開始ページを設定
              page = ntagConfPage0; // config page 0
              res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      (byte) 0x04,
                      (byte) 0x00,
                      (byte) 0x00,
                      (byte) 0x04 // 保護開始ページ
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
      
              // read 要求からの保護も有効に
              page = ntagConfPage0 + 1; // config page 1
              res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      (byte) 0x80, // read,write protect
                      (byte) 0x05,
                      (byte) 0x00,
                      (byte) 0x00
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
            } catch (IOException e) {
              Log.d(TAG, "WRITE err:" + e.toString());
            }
      
            if (nfca.isConnected()) {
              try {
                nfca.close();
              } catch (IOException e) {
                e.printStackTrace();
              }
            }
          }
              :
      
    • 上のコードにより NTAG213 のページ 04h 以降の読み書きにパスワード認証通過が必須となった状況
         
  3. パスワードを解除する
    • NTAG21x 種別を確認
    • PWD_AUTH コマンドでパスワード "0000" を提示し認証要求
    • AUTH0 に有効範囲外のページ番号 FFh を書き込むことでパスワード保護を無効化する
              :
      
          if (isNfcA) {
            int ntagMaxPage = -1, ntagConfPage0;
            NfcA nfca = NfcA.get(tag);
            try {
              nfca.connect();
            } catch (IOException e) {
              Log.d(TAG, "NfcA.connect() err: " + e.toString());
              return;
            }
            // NTAG 種別取得
            try {
              byte[] res = nfca.transceive(new byte[]{
                (byte) 0x60, // GET_VERSION
              });
              Log.d(TAG, "GET_VERSION reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 8) {
                if (res[0] == 0x00 && res[1] == 0x04 &&
                    res[2] == 0x04 && res[3] == 0x02) {
                  byte ntagStrorageSize = res[6];
                  if (ntagStrorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStrorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStrorageSize == 0x13) {
                    ntagMaxPage = 231; // 0xE7 = NTAG216
                  }
                }
              }
            } catch (IOException e) {
              Log.d(TAG, "GET_VERSION err:" + e.toString());
            }
            if (ntagMaxPage == -1) {
              Log.d(TAG, "is not NTAG21x");
              if (nfca.isConnected()) {
                try {
                  nfca.close();
                } catch (IOException e) {
                  Log.d(TAG, "NfcA.close() err: " + e.toString());
                }
              }
              return;
            }
            // Congiguration Pages 開始位置
            ntagConfPage0 = ntagMaxPage - 4;
      
            // パスワード認証
            // 通過すれば close するまで権限が持続
            boolean authOk = false;
            try {
              byte[] res = nfca.transceive(
                  new byte[]{
                      (byte) 0x1b, // PWD_AUTH
                      '0', '0', '0', '0'
                  }
              );
              Log.d(TAG, "PWD_AUTH reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 2) {
                authOk = true;
              }
            } catch (IOException e) {
              Log.d(TAG, "PWD_AUTH err:" + e.toString());
            }
      
            if (authOk) {
              // パスワード保護を解除
              try {
                int page = ntagConfPage0; // config page 0
                byte[] res = nfca.transceive(
                    new byte[]{
                        (byte) 0xA2, // WRITE
                        (byte) page,
                        (byte) 0x04,
                        (byte) 0x00,
                        (byte) 0x00,
                        (byte) 0xFF // 有効範囲超のページ指定で保護は無効に
                    }
                );
                Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
              } catch (IOException e) {
                Log.d(TAG, "PWD WRITE err:" + e.toString());
              }
            }
      
            if (nfca.isConnected()) {
              try {
                nfca.close();
              } catch (IOException e) {
                Log.d(TAG, "NfcA.close() err: " + e.toString());
              }
            }
          }
              :
      
    • 上のコードにより前掲 2.のコードによるパスワード保護をこのコードで解除した状況
      ここでは PWD 自体には手をつけておらず PROT ビットも 1 のままだが、AUTH0 = FFh につきユーザ領域を自由に読み書き可能な状態に戻っている
         

3. 試作した Android アプリについて

NTAG で秘密情報を扱うという考え方

ここまで見てきたように NTAG21x はパスワード設定により Write 要求のみならず Read 要求を弾くことも可能です。これは興味深い機能ですが、両者のパスワードが共通であるためたとえば次のような取り回しはできません。

  • 部外者: タグデータへのアクセス不可
  • 利用者: Read パスワードによりタグデータの読み出しが可能
  • 管理者: 読み出しに加え Write パスワードでタグデータの書き換えが可能

このように複数のロールでタグを共用したい場合ではなく、所定のタグを独占的・排他的に扱いたい場合にこそこの機能は有用でしょう。その典型的な使用例として秘密情報の格納を想起しました。以下の発想によるものです。

  • デバイスのストレージも公共のネットワークも使用しないため情報漏洩のリスクが極めて小さい
  • 薄くて小さく軟弱な媒体であるためマスターデータの保存には不向きだが、逆にデータのコピーを携行する目的には好適であり、その状況下では簡単に媒体を破壊・破棄できることが利点にもなる
  • NFC の特性上表面を覆っても読み書きができるため手近なものに忍ばせることも容易
  • パスワードで対 Read 保護を設定しておけば他者の手に渡っても一般的なリーダーやアプリではデータを読み出せない。仮に NTAG パスワード認証を試みられたとしても事前に上限回数を設定しておけば試行の過程で当該タグは永久に封鎖される
  • 記録容量の少なさはデータ圧縮と複数タグへの分割格納で補うことが可能。嵩張らないため複数枚に渡っても携行性への影響は微小

なお、こういった使途においては NFC の世界で「データ交換」を行う上での便宜としての NDEF 形式を使用する必然性はありません。ユーザメモリ全体を任意の形式で効率よく取り回せばよいでしょう。

試作中の情報収集時に、秘密情報を紙媒体で管理する「パスワードノート」という製品があることを知りました。

ストレージやネットワークを使用しない点が共通しており興味を持ちました。たしかに「手書き」には他の方法では決して及ばない様々な柔軟性があります。これがヒントとなって NTAG を手帳へ貼付して使うアイディアに至りました。ふたつの文化の利点を活用できる良い組み合わせだと思います。

実装内容

試作の仕様として以下の内容を想定しました。

  1. NTAG213, NTAG216 を対象とする
  2. NDEF は使用せず最小限のデータヘッダに続けてユーザメモリ一杯までデータを格納する
  3. テキスト形式のデータであることを前提とする。容量節約のためマルチバイト文字のエンコーディングには Unicode ではなく Shift-JIS を使用する
  4. データは gzip 形式で圧縮する
  5. データが一枚のタグに収まらない場合は複数のタグへ分割格納する。ただし、データを分割して格納したタグはそれぞれ単独での読み出しも可能とする(全タグの順次読み出しをデータ復元の前提とせず、全タグが揃わなくても部分データを柔軟に取得できるようにする)
  6. Read/Write 要求に対しメモリの全ページをパスワード保護対象とする
  7. NTAG の 4 バイトのパスワード領域へ 4 バイトの ASCII キャラクタコードを格納するのではなく指定されたパスワード文字列の CRC32 値を格納する (PWD のバリエーションは 256^4 となる)
  8. 上記の措置のため他のツールではここで設定したパスワード保護を解除不能につき解除機能を独自に実装する必要がある
  9. 総当たり攻撃への対処としてパスワード認証の試行回数に上限値(AUTHLIM)を設定することにより試行回数超過の場合には NTAG の自爆機構(PWD_AUTH 要求に対する無条件拒否)を発動させる
  10. 上記にも関連し、ここではマスターデータの保存を目的とするのではなく、コピーデータの便宜的な保持を目的とすることを前提とする

NTAG ユーザメモリの使いかた

 ========= ユーザメモリ領域全体の構成 =========

 1. page 04h - 05h に固有の管理情報を格納する
 2. page 06h 以降に gzip データを格納する

 ========= 1. 管理情報について =========

 [page 04h]
    0    1    2    3
  +----+----+----+----+
  |'t' |'t' | -- | -- |
  +----+----+----+----+

  第 1, 2 バイト
    識別子 "tt"

  第 3, 4 バイト
    予備
    ※当初、データを分割格納したタグセットの識別用領域と
      することを想定したが費用対効果に乏しいと判断し中止

 [page 05h]
    0    1    2    3
  +----+----+----+----+
  | NN | -- | NN | NN |
  +----+----+----+----+
  
  第 1 バイト
   最上位ビット:後続タグの有無  0=後続なし 1=後続あり
   下位 4 ビット:タグ連番(0h - Fh)

  第 2 バイト
   予備

  第 3, 4 バイト
   ページ 06h 以降に格納ずみの gzip データサイズ
   short ビッグエンディアン

 ========= 2. gzip データについて =========

  - テキストデータの gzip 圧縮はオンメモリで行う

  - タグへ書き込む際には gzip データの半固定ヘッダ
    10バイト(*)を除去し、読み出しの際には当該ヘッダを
    補填した上で unpack する
    (*) http://www.onicos.com/staff/iz/formats/gzip.html

  - データを複数のタグへ分割格納する場合は上記の管理情報で
    連番管理を行うが、事後に各タグから単独で部分データを
    読み出すことも可能とするために、タグへ書き込むのは
    「元データ全体を圧縮した gzip データの一部分」ではなく
    「元データを適切な位置で分割して圧縮した gzip データ」とする

ソースコード

※本記事の冒頭でも触れたように NTAG21x のアクセス制御機構の扱いには十分な注意が必要です。 試作のソースコード公開はあくまでも技術情報の紹介を目的とするものでありプログラム実行時の動作は保証しません。これを使用して何らかの損害が発生したとしても当方は一切の責任を負いません。

動作の様子

以下のデモ動画ではテストデータとして次の 4,978 バイトの英単語リストを使用。このデータを圧縮して 3 枚の NTAG216 に分割格納しています。

START, ability, abroad,
accept, access, accident,
according, account, action,
activity, actually, add,
addition, additional, address,
adult, advance, advanced,
advantage, advice, advise,
age, agency, agent,
agree, ahead, air,
airline, allow, amount,
angry, announce, announcement,
anxious, appear, appearance,
application, apply, approach,
area, arrange, arrangement,
arrival, arrive, article,
attack, attend, attention,
available, average, avoid,
aware, balance, balanced,
bar, base, basic,
bear, beat, beauty,
benefit, bill, bit,
block, blood, board,
borrow, boss, branch,
break, broad, broadcast,
brush, budget, burn,
business, busy, cab,
cable, call, cancel,
capital, care, careful,
case, cash, catalog,
catch, cause, century,
certain, certainly, chance,
change, charge, chart,
cheap, check, chemical,
choose, citizen, claim,
clear, clerk, close,
clothes, collect, collection,
comfortable, common, communication,
company, compare, complain,
complete, concern, condition,
contact, contain, content,
continue, contract, control,
convenient, conversation, copy,
corner, correct, cost,
count, couple, course,
court, cover, crash,
create, credit, cross,
crowd, crowded, customer,
daily, damage, data,
date, deal, decide,
decision, defence, degree,
delay, deliver, demand,
department, depend, deposit,
describe, design, destroy,
detail, develop, dial,
diet, difference, difficulty,
direct, direction, directly,
director, discount, discuss,
disease, disk, display,
distance, district, document,
double, doubt, downtown,
dress, drive, drop,
drug, due, earn,
earthquake, economy, edge,
education, effect, effective,
effort, electricity, employee,
empty, encourage, energy,
enjoy, enter, entrance,
equipment, event, examination,
example, excellent, except,
exciting, excuse, exercise,
exit, expect, expense,
expensive, experience, expert,
explain, express, extra,
face, fail, fair,
fall, fan, fare,
fashion, fault, favor,
favorite, feature, fee,
fence, figure, file,
fill, film, final,
fine, fire, firm,
fit, fix, flag,
flat, flood, floor,
flow, follow, following,
force, foreign, form,
former, forward, found,
foundation, free, freeze,
front, fund, furniture,
further, future, gain,
garage, gas, gather,
general, global, goal,
government, grade, graduagte,
grand, ground, guard,
guess, guide, handle,
hang, happen, hardly,
head, heart, heat,
highly, hire, hold,
host, human, hurt,
illness, image, immediately,
improve, include, income,
increase, individual, industry,
inform, information, insect,
instead, instruction, insurance,
intend, interest, international,
interview, introduce, invitation,
invite, issue, item,
job, join, judge,
knowledge, lack, land,
language, law, lawyer,
lay, lead, lend,
length, level, license,
lie, lift, likely,
limit, limitation, line,
list, load, loan,
local, location, lock,
lose, loss, luck,
lucky, luggage, mail,
main, major, majority,
manage, management, manager,
mark, market, master,
match, material, matter,
meal, means, measure,
media, medicine, mention,
message, method, mind,
minute, miss, mix,
model, modern, moment,
monthly, movemet, nation,
national, nearly, necessary,
notice, offer, office,
officer, official, operate,
operation, operator, opinion,
order, organization, own,
pack, package, pain,
part, party, pass,
passenger, patient, pay,
payment, per, performance,
performance, period, permit,
personal, pick, plain,
plan, plant, plate,
pleasure, plenty, point,
pole, policy, popular,
position, possible, post,
powerful, practice, prefer,
prepare, present, press,
prevent, price, print,
private, probably, problem,
process, produce, product,
production, professional, profit,
program, progress, project,
promise, property, protect,
protection, proud, provide,
public, publish, purpose,
puzzle, quality, quarter,
race, raise, range,
rapid, rate, reach,
ready, realize, reason,
receive, recent, recently,
recommend, record, reduce,
refer, regard, regarding,
regular, relationship, remember,
remove, rent, repair,
report, request, require,
research, reserve, responsible,
rest, result, return,
rise, room, route,
row, run, rush,
safe, safety, sail,
salary, sale, save,
schedule, seat, section,
security, sense, separate,
separately, serious, serve,
service, shake, shape,
share, ship, shock,
short, show, sign,
signal, single, situation,
skill, skin, slip,
social, solve, sort,
sound, spare, spend,
spot, spread, staff,
stage, stair, stand,
standard, state, statement,
step, stock, store,
storm, stretch, strike,
study, subject, submit,
succeed, success, suffer,
suggest, suit, supply,
support, suppose, sure,
surprise, survey, swing,
system, taste, tax,
temperature, terible, term,
tie, tight, tip,
tired, tool, total,
touch, tour, trade,
traffic, training, trip,
trouble, turn, type,
usual, valuable, value,
view, vote, want,
war, warn, way,
wear, weather, win,
wind, wire, wise,
wonder, worth, worthy,
xenophobia, yard, yarn,
yawn, yearn, yell,
yellow, yesterday, yet,
yield, yolk, young,
youth, zeal, zenith,
zigzag, zinc, zoom,
END

動画:5分1秒

※パスワード認証試行上限回数を超過した状況の動画はこちら


(tanabe)
klab_gijutsu2 at 17:00
この記事のURLComments(0)NFC | Android
2017年12月25日

外部向け HTTP 通信の再送検知ツールを作った話

はてなブックマークに登録

このエントリは KLab Advent Calendar 2017 の最終日の記事です。

こんにちは。インフラ担当 高橋です。

このエントリでは、つい先日、少し変わった仕組みの外部向け HTTP 通信の 3way ハンドシェイクの再送検知ツールを作りましたのでご紹介します。

きっかけ

このツールを作ったのは、DSAS の外部で発生するネットワーク障害を検知できるようにするためです。

例えばこのような障害がありました。

DSAS は複数のデータセンタで稼働しているのですが、ある日、特定データセンタの Web サーバから外部 API への接続が重くなる事象が発生しました。

原因は外部ネットワークからの DDoS 攻撃により、データセンタのネットワークの一部区間が飽和状態になったことによるものでした。

外部ネットワーク障害 その1

またある日は、特定のデータセンタの Web サーバから外部の特定 API に接続ができなくなりました。

原因は外部ネットワークの途中経路の ISP で伝送装置が故障したことで、経路変動が発生し、通信が不安定になったことによるものでした。

外部ネットワーク障害 その2

このように DSAS 外部で発生するネットワーク障害の原因は様々であり、対応も状況によって都度変化します。DSAS 内のネットワーク、データセンタの回線、その先の途中経路、外部 API のサーバなど、切り分けしなければならない箇所は多くあります。

特に途中経路の障害というのは、データセンタの回線や外部 API には問題が発生しておらず、障害箇所の切り分けにも時間がかかります。

このような障害はサービスへの影響に直結するため、インフラ担当はできるだけ早く障害に気づき、対応を開始しなければなりません。

そのため、インフラ担当が DSAS 外部で発生するネットワーク障害を検知できる仕組みを導入することにしました。

どのようにして検知するか

障害を検知するには、障害が発生しそうな箇所を想定し、そこを監視する必要があります。

DSAS 内部とは異なり、DSAS 外部で発生するネットワーク障害の原因は千差万別で、且つ監視できる箇所は限られてきます。

何を検知するか

まずは、先ほど例に挙げた障害を振り返ってみることにします。

このような障害の場合、DSAS のサーバやスイッチは正常なためアラートは発生しません。案件担当からの「外部 API の応答が遅い、もしくはタイムアウトしてエラーになる」といった問い合わせがきっかけで障害に気づくことになります。

つまり、外部 API への HTTP 接続の応答が遅くなったり、タイムアウトが発生していることをきっかけに障害を検知しているということになります。

それでは、外部 API への HTTP 接続の応答時間が長ければネットワーク障害が発生しているかというと、外部 API の処理に時間がかかっている可能性もあり、そうとは言い切れません。

このような障害が発生すると、インフラ担当は モニタリングシステム (Ganglia) のグラフを確認します。

グラフには HTTP の外向きの通信についての項目があり、該当時刻に Web サーバで SYN パケットの再送が大量に記録されていました。

HTTP 通信の SYN パケットの再送が起きているということは、外部 API に SYN パケットが届かなかったり、外部 API からの SYN/ACK パケットが Web サーバに届かなかったりしている可能性が高いです。

これを監視すれば DSAS 外部のネットワーク障害を検知できそうです。

何を使って検知するか

直接、監視サーバから外部への HTTP 監視を行うには、ヘルスチェックのような監視用 URL が必要になりますが、インフラ担当では用意することができませんので、案件担当と連携して監視対象 URL のリストを作成し、管理しなければなりません。

DSAS では複数案件のサービスが稼働しており、その全ての案件のリストを管理するのは大変ですので、それは避けたいという思いがありました。

それに加えて今回は、HTTP 接続の応答時間やステータスコードではなく、SYN パケットの再送を監視する必要があります。

「監視対象 URL を管理せずに、外部向け HTTP 通信の SYN パケットの再送を検知したい」

この要望を叶えることのできるツールが、意外と身近にあることに気づきました。

モニタリングシステムのグラフ作成に使用している tcpeek です。

tcpeek とは

tcpeek は KLab が作成した 3way ハンドシェイク時に発生するエラーを監視・集計するネットワークモニタで、エラー検出、再送検出、フィルタ、データ出力といった機能を備えています。

GitHub - pandax381/tcpeek: TCP 3way-handshake monitor

こちらのエントリに詳細が書いてあります。

ログからは見えてこない高負荷サイトのボトルネック

再送検出

指定したインタフェースを監視し、タイムアウト時間内に SYN の再送が発生すると SYN Segment Duplicate (dupsyn)、接続先から SYN/ACK が再送されてくると S/A Segment Duplicate (dupsynack) がカウントされます。

タイムアウト時にも再送は発生していますが、tcpeek の再送検出は 3way ハンドシェイク成功時に再送が発生していればカウントされるようになっています。

想定される原因としては、途中経路の帯域の輻輳やパケ落ちなどが挙げられます。

エラー検出

指定したインタフェースを監視し、RST パケットを受け取ると Connection Rejected (reject)、ICMP Destination Unreachable パケットを受け取ると ICMP Unreachable (unreach)、接続がタイムアウトすると Connection Timed Out (timeout) がカウントされます。

タイムアウトについては、アプリケーションによりタイムアウトまでの時間は異なりますので、tcpeek 起動時に指定するタイムアウト時間 (デフォルト 60秒) を超えたセッションは Connection Timed Out としてカウントされるようになっています。

想定される原因としては、接続先の TCP リセット、途中経路のルーティングテーブルの消失、接続先の無応答などが挙げられます。

tcpeek を使った SYN パケット再送検知の仕組み

tcpeek を使えば外部向け HTTP 通信に絞った 3way ハンドシェイクの再送発生 (dupsyn / dupsynack) や接続失敗 (reject / unreach / timeout) の計測値を取得することができますので、そのカウントが増えたらインフラ担当に通知するツールを作成することにしました。

====== TCPEEK SUMMARY ======
----------------------------
 http-out
----------------------------
 success
     total           17457
     dupsyn              3
     dupsynack           0
 failure
     total             113
     reject             39
     unreach            45
     timeout            29
============================

tcpeek は Web サーバで動作しますので計測値は Web サーバで取得することになります。

Web サーバから直接インフラ担当に通知する仕組みだと、多数の Web サーバで HTTP の再送が発生すると通知の数が大量になってしまいます。

そのため、Web サーバの計測値をキャッシュに格納して集約し、検知ツールでキャッシュにある統計情報をチェックする仕組みにしました。

監視の仕組み

計測値のカウントが増えており、その数がしきい値を超えていると slack とメールでインフラ担当に通知します。こちらは slack への通知例です。

slack通知

おわりに

今回、このツールを作る際に一番悩んだのが、通知内容としきい値の設定方法でした。

Web サーバ単位でカウントを通知すると、台数が多いと通知が縦に長くなってしまい、特に slack では扱いづらくなるため、案件単位で通知をまとめるようにしました。

また、Web サーバ毎にしきい値を設定すると、特定の接続先の障害で再送などが発生した場合、何台の Web サーバで再送が発生しているのか状況を把握しづらくなるため、案件の Web サーバ全台の合計カウント数をしきい値としています。

この 2点はまだまだ改善の余地があると思っており、これからもブラッシュアップしていきたいです。

もちろん tcpeek は外部向け HTTP 通信だけではなく、様々な通信のフィルタを作成し集計できますので、他の通信に対してもこの検知の仕組みは適用できます。

takahashi_yo at 12:48
この記事のURLComments(0)network | 開発
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
Syndicate this site