2017年02月16日

最近の Python-dev (2017-02)

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

バックナンバー:

新しいコア開発者

主にドキュメントの改善について継続的に活動されていることが評価されて、 Mariatta さんがコア開発者に加わりました。

僕は大きなパッチで目立ってコア開発者になってしまったので、このBlogの読者に間違った印象を与えてしまわないように強調しておきたいのですが、コア開発者に重要なのは他のコア開発者と協調してルールを守って貢献することです。

小さい簡単な修正や他人の pull request のレビューなどでも、継続的にコア開発者とやりとりする機会があれば、結構簡単にコア開発者になれるはずです。 後述する Github 移行によって敷居が下がったはずなので、この記事を読んでくださっている方もぜひ狙ってみてください。

Github 移行

2/11 (アメリカ時間なので日本では2/12)に、Python のリポジトリが https://github.com/python/cpython に移行しました。

以前は mercurial を利用していたのでリポジトリの変換が必要になるのですが、以前 python/cpython にあったミラーとは別物でコミットのハッシュが変わったので、ミラーを利用していた人は注意が必要です。 ミラーリポジトリは python/cpython-mirror に移動しましたが、今後削除されるかもしれません。

今までコミットログは "Issue #12345: " で始まっていたのですが、この #12345 は将来 Github にこの番号のプルリクエストが作られてもリンクされないらしいです。 Github えらいですね。 今後のコミットでは bugs.python.org の issue を参照するときは "bpo-12345" のように書くことになります。 Github の pull request を参照するときも #1234 ではなく GH-1234 が推奨されます。(これでもちゃんと Github で自動リンクされます)

と、ここまでは良いんですが、 issue トラッカーを Github 外に置いてる他のプロジェクトの経験者がGreasemonkey使って bpo-12345 をリンク化するようにしたら便利だよー、という話題が発展して、リポジトリ変換時に過去のコミットログの (Issue|issue) #12345bpo-12345 に変換しようという話がマイグレーションの決行数日前に盛り上がってしまいました。

僕は直前にそんな変更入れるのはありえないと思ってコミットログ書き換えには -1 してたのですが、実際にコミットログを書き換えて変換したリポジトリを見て問題が無いかチェックしはじめたりして当日までどうなるか分からない状況でした。 最終的には、コミットログはたくさんのコミッターのもので、ほんの2,3日の議論だけで、議論に参加できなかったコミッターを無視して勝手に書き換えて良い物ではないという結論になって、当初の予定通りコミットログ書き換えはナシになりました。

まだまだ問題点も多く、マイグレーション後に開発される予定になっている bot がまだできてないので作業的には今までより楽になった気はしないのですが、開発に参加する敷居は確実に下がっていて pull request をして Contributer Agreement にサインしてくれる人がたくさんいるので、これから新しいコア開発者が増えることに期待しています。

siphash-1-3

最近のruby-core (2017年1月) を読んで siphash-1-3 について知り、 Python にも 提案してみました

一旦今の siphash-2-4 をそのまま書き換えるパッチを投げたのですが、 configure 時に選択できるようにしてくれた人がいたので、今はその人が pull request を作ってくれるのを待っています。

パフォーマンスへの影響ですが、 Python の文字列オブジェクトは immutable でハッシュ値は一度計算されるとオブジェクトの中にキャッシュされるので、ハッシュ関数が数割速くなった程度ではパフォーマンスには大きい影響はありません。 JSON をデコードするときなど、新しい文字列オブジェクトを dict のキーに追加する処理が集中するときにはちょっと差が出るのですが、標準ライブラリのJSONはそこまでカリカリにチューンされてない (文字列のエスケープ処理など部分的にCを使ってるけど全体は Python で書かれてる) のでそれでも2~3%程度しか差が出ませんでした。

とはいえ、今後も 「最近のruby-core」と「最近のPython-dev」でお互いにいい影響を与えあっていけたらなぁと思います。

re performance

Go の開発者の一人でもある Russ Cox さんの、 PCRE などの正規表現エンジンが (a?){n}a{n} に "a"*n (n=3 なら a?a?a?a{3} と aaa) をマッチさせるのに O(2^n) の計算量がかかってしまうけど、バックトラックじゃなくてNFA使えば O(n^2) にできるよねって記事があります。 https://swtch.com/~rsc/regexp/regexp1.html

これは新しい記事ではないんですが、 Python の標準ライブラリの re がずっとこの遅い方式のままだよねということがMLで話題になりました。

Python の re を置き換えることを目的に開発されていた regex ではこの例の正規表現が遅くならないけど、置き換えどうすんの?やっぱり Python 本体と別にバージョンアップできる現状維持でいいや。 requests みたいにPythonの re のドキュメントからオススメサードパーティーライブラリとして regex へのリンクを書いておけば良いんじゃない? regex はいくつかチェック入れてるからこの記事の例で遅くならないけれども、 NFA じゃなくてバックトラックなのは変わらないよ、NFA使いたいなら re2 を使おう。という感じの議論がされました。

ちなみにGoの正規表現は遅いと昨年のISUCONで話題になりましたが、C実装のre2よりは遅いもののちゃんとNFAになっていて、 Python などの言語よりは redos に強くなっています。

Python で web 開発している人など、外部入力に対して正規表現を使う場面があるなら、 Facebook製の re2 binding である fb-re2 か、その fork で re との互換性を重視してる re2 を使ってみてはいかがでしょう?

Investigating Python memory footprint of one real Web application

Instagram の開発者による、 Dismissing Python Garbage Collection at Instagram という記事が、 Python の「たとえばGCを止める」案件だと一部で話題になりました。

この記事はパフォーマンスというよりは prefork の copy on write 効果でメモリ節約したいのに循環参照GCのせいでメモリが共有されなくなるからという理由で GC を止めていますし、そもそも循環参照GCの手前にある参照カウントGCまで止めるという話ではないので、Ruby on Rails の「たとえばGCを止める」 とは全く別の話です。

Perl や昔の php と同じく、参照カウントGCだけでもワリと普通に動きますし、 循環参照GCは適切にチューニングするのも難しくありません。

それはさておき、弊社ではCPUバウンドのプロセス数はコア数の2倍程度に絞る事が多いのでプロダクション環境でメモリ不足に成ることはほとんどなくて、 AWS の c4 インスタンスでもメモリが全然余ってたりするのですが、世の中には別の思想や条件で設計・構成されてるアプリもあるわけで、メモリ使用量は少ないに越したことはありません。

そこで弊社の開発中のとある案件のコードを拝借して、起動後のメモリ使用量の内訳を解析してみたのがこのMLのスレッドになります。 (解析方法については別の機会に紹介します。) 内容を幾つか紹介しておきます。

弊社のコードがPython の type hinting を多用していて -> List[User] みたいなのが大量にあるんだけど、この List などが Python の ABC (実際に継承していないクラス間にサブクラス関係を定義できるようにする仕組み) を継承して実装されていて、それが SQLAlchemy の ABC の使い方と相性が悪く、サブクラス関係の判定を高速化するためのネガティブキャッシュを大量に生成してしまっていました。 これは typing モジュールが List[X]List でキャッシュを共有するという最適化を導入したので、3月リリース予定の Python 3.6.1 で改善されます。

その他、 type hinting によるメモリ使用量のオーバーヘッドは、大きくもないけど、状況によっては無視もできなさそうだから、 docstring を読み込まない -O2 オプションみたいにランタイムに読み込まない最適化オプションがあった方が良いかもしれない。

型ごとのメモリ使用量は、 str > dict, tuple > その他。 str は特に SQLAlchemy の docstring が大きい。 -O2 を使えば dict, tuple > str > その他になり、 dict と tuple がそれぞれ 10% ずつメモリを使っている。 -O2 は docstring だけでなく assert も消えてしまうので、最適化を個別に制御するオプションが欲しい。

その他、Python 3.6 で compact になった dict をもう1段小さくしたり、 tuple を減らすような実装上のアイデアも出したので、今年末に feature freeze になる Python 3.7 までにできるだけ試していきたい。

続・FASTCALL

先月の記事で tp_call の FASTCALL 版の tp_fastcall を追加する話をしたのですが、このスロットと呼ばれる関数ポインタの追加は、ABI互換性 (限定されたAPIではABIまで後方互換性が確保されていて、古い Python 向けにビルドされた拡張モジュールが新しい Python からも使える) などの関係もあり結構大変です。

そこで、どうせ追加するなら同時に tp_new, tp_init というスロットも FASTCALL 対応したバージョンの tp_fastnew, tp_fastinit を追加してしまえと、 Victor さんがすごく頑張りました。頑張ったんですが、、、マイクロベンチは速くなるものの実際のライブラリを使ったマクロベンチでは大きな速度差が観測できず、一方で型システムのコア部分に手を入れるパッチなので僕がレビューしても「これで想定しているケースで動くのは分かる、でもサードパーティーの行儀悪い拡張とかとの相性で起こる副作用は予想できない」という感じなので、ペンディングになりました。

それ以外の部分では FASTCALL の適用範囲は地道に増えています。例えば、 Python で __getitem__ などの特殊メソッドを実装してスロットを埋めたとき、スロットからその Python 関数を呼び出すのに FASTCALL を使うようになったのと、 Python 3.6 でメソッド呼び出しを高速化したテクニックを組み合わせることで、マイクロベンチで最大で30%の高速化ができました。


@methane


songofacandy at 10:46
この記事のURLComments(0)TrackBack(0)Python 
2017年02月13日

Consulのおもしろそうな仕組みについて調べてみました

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

はじめに

KLabさんの協力会社として一緒にお仕事をさせて頂いておりますクラスターコンピューティングと申します。今回はSerfの同様にHashiCorpより提供されているConsulを試してみました。

コンテナによるクラスタなど随時サービスが追加削除されるような環境ではそのアドレスはDHCPなどにより動的に決定されます。コンテナ名などでそのサービスにアクセスできれば便利ですが、動的なアドレスとコンテナ名の関係をどのように解決するかという点が問題になります。クラスタ内でロードバランサやリバースプロキシ等を利用している場合、サービスの追加に応じてその設定ファイルも動的に更新しなくてはなりません。また、構成が随時変化するなかで、現在どのサービスがどのホストで実行されているかということを把握する必要もでてきます。

Consulを利用することによりこれらの問題を解決することができます。今回はそのためのConsulの機能について簡単にしらべてみました。

Consulについて

本家ホームページ

ConsulはSerfと比較してより高度な機能を持つクラスタの管理ツールです。 Serfはクラスタのメンバシップの管理に特化したものになっており、それ以外のオーケストレーションに関わる機能は自前で用意する必要がありましたが、Consulではいくつかの機能が初めから備わっています。

Consulの機能

Consulの機能は様々なものがありますが一例をあげますと

  • サービスの検索
  • 各Consulクライアントが登録したサービスをDNSやHTTPインターフェースを利用してクラスタ全体から検索できます。
  • 障害検知/ヘルスチェック
  • 各ノードの障害検知だけではなく、それ上で実行されているサービスのヘルスチェックも可能です。
  • Key Value Storage
  • KVSを利用してのクラスタ間でのデータの共有が可能です。
  • 複数データセンタへの対応
  • データセンタを跨ぐインターネットを介したクラスタの構成でも効率よく障害検知や情報の伝播がおこなるようになっています。

Consulの構成

consul_cluster

Consulのクラスタ構成でもSerfと同様にクラスタを構成する全てのノード上でConsul Agentが実行されている必要があります。Consulのベースの部分にはそのままSerfを利用しています。例えば、メンバシップの管理や、各ノードそのものの障害検知の仕組みにはSerfと同様です。 ただ、Serfでは中心となるサーバが存在しない全てのノードが等価な構成でしたが、Consulではデータ等を管理するServerとそれに付随するClientからなる構成になっています。 そのためデータを管理するServerが障害点になっており、それが失われるとクラスタが機能しなくなってしまいますが、Severを複数台で冗長化することにより対処しています。複数台のServerでデータを管理する場合その一貫性が問題になりますが。それにはいわゆる分散合意アルゴリズムを利用して対応しています。

Consul Server

Consul Serverは冗長化のため基本的に複数台存在しています。冗長性を保ちつつ、Server間のデータの一貫性を確保するために仕組みとして、分散合意アルゴリズムであるRaftプロトコルが利用されています。RaftではまずLeaderをいわゆる多数決で選出します。Server間で投票を行い過半数の支持を受けたノードがLeaderになる仕組みです。1台のLeaderが選出された残りのServerはFollowerとなります。一貫性確保のためデータのコミットはこのLeaderが行います。データは随時Server間でレプリケーションされています。そのためClientからのクエリにはServerであれば、LeaderでもFollowerでも応答することが可能になっており、負荷を分散しています。必要があればFollowerがあらためてLeaderに問い合わせをしてClientに正しい返答するようになっています。Leaderのサーバが障害などで不在になると新たなLeaderが自動で選出され業務を引き継ぎます。

Leaderに選出されるために元々のサーバの数の過半数の支持が必要です。そのため障害等で稼働しているServerの数が減り定足数に足りなくなるとLeaderが選出できなくなりクラスタが機能しなくなります。例えばConsul Serverを5台で冗長化している場合、Leaderになるためには少なくとも3台から支持される必要があり、稼働しているServerが2台以下になってしまうとまだServerのConsul Agentが存在していても、クラスタは機能しなくなります。

Raftによる仕組みはConsul Server間のみで構成されています。Raftは構成するノードの数が増加すると全体にかかる負荷が大きなものになってしまいますが、それをServer間のみに限ることによって負荷を抑えつつクラスタ全体でのデータの一貫性を保てる構成になっています。

Consul Client

Clientとして実行されているConsul AgentはServerのAgentと通信して必要なクラスタ情報を取得あるいは登録します。Clientのノード上でクラスタの情報を取得する場合、ユーザは基本的にはローカルホストのAgentに問い合わせます。その後、ローカルホストのはAgentがConsul Serverと通信して情報を取得し、その結果をユーサに返します。ClientはServerのノードと通信できなくなってしまうと(他のClientとは通信できたとしても)基本的にクラスタの一員として動作できなくなります。また、Clientとして実行されているAgentがServerが不在でクラスタが機能しなくなった際に自動でServerに昇格するような仕組みはありません。

Consul AgentへのアクセスはユーザからローカルホストへのAgentへのアクセス、あるいはAgentからAgentへのアクセスが基本ですが、外部からのAgentへのアクセスを許可することも可能です。そのようにするとConsul Agentが実行されていないホスト上からクラスタの情報を取得したりすることが可能です。

サービスの登録

SerfでもTagという形でサーバの役割などを登録することはできましたが、ConsulではServiceとしてもっと具体的に登録することが可能です。Service名、Tag、アドレス、ポート番号、そしてヘルスチェック方法などを設定できます。これら登録した情報はDNSやHTTPインターフェス経由のクエリを通して参照することができます。

ヘルスチェック

SerfでのヘルスチェックはAgentそのものの死活のみでした。Consulでも同様な仕組みで各Consul Agentの死活を監視しています。それに加えて、Consulではアプリケーションのヘルスチェックをおこなうことが可能です。ヘルスチェックはTCPチェックはHTTPチェックなどの汎用のものおよび任意のスクリプトによるチェックが利用できます。

ヘルスチェックをサービスと関連つけることによりサービスのヘルスチェックとして利用できます。 この場合はヘルスチェックに失敗するとサービスに障害が発生したと判断されます。特にサービスを指定しない場合ノード全体と関連つけられます。この場合はヘルスチェックに失敗するとノード全体に障害が発生したと判断されます。 ヘルスチェックはDNSやHTTP経由でのクエリと関連していて、障害が発生した判断されているサービスやノードはクエリの結果から除外されるようになります。

サービスの登録は下記のようにJSONで設定を記述してConsul agentに読み込ませます。


{
  "service": {
    "name": "web",
    "tags": ["web1"],
    "port": 80,
    "enableTagOverride": false,
    "checks": [
      {
        "id": "api",
        "name": "HTTP API on port 80",
        "http": "http://localhost:80/index.html",
        "interval": "10s",
        "timeout": "1s"
      }
    ]
  }
}

DNSクエリについて

Consulの特徴の1つとしてAgentがDNS機能を提供している点が挙げられます。DNSインターフェースを利用してConsulのノード名でのアドレスの取得や提供しているサービスを検索することが可能です。これによって一般のアプリケーションからもConsulのノード名でのノードへのアクセスが可能になります。ただ、ConsulがDNSを提供しているポートは通常とは異なる番号(デフォルトでは8600番)のため、一般のアプリケーションがそれを参照するためにはdnsmasqなどを利用してシステムがConsulのDNSにも問い合わせするように環境を整えて置く必要があります。

デフォルトではnode.consulというドメイン名でアクセスできます。例えばserver1というノード名のホストであれば、server1.node.consulというホスト名でローカルの8600番ポートにアクセスすると応答が得られます。


# dig @127.0.0.1 -p 8600 server1.node.consul

; <<>> DiG 9.9.5-9+deb8u7-Debian <<>> @127.0.0.1 -p 8600 server1.node.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53745
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;server1.node.consul.           IN      A

;; ANSWER SECTION:
server1.node.consul.    0       IN      A       10.20.0.11

;; Query time: 7 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Mon Oct 03 05:42:15 UTC 2016
;; MSG SIZE  rcvd: 53

また、アドレスのほかに登録したサービスの検索もできます。ただし、UDPのアクセスの場合、返答されるレコード数(サイズ)に制限があるので注意が必要です。

サービスは<サービス名>.service.consulで検索可能です。


# dig @127.0.0.1 -p 8600 web.service.consul

; <<>> DiG 9.9.5-9+deb8u7-Debian <<>> @127.0.0.1 -p 8600 web.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42433
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;web.service.consul.            IN      A

;; ANSWER SECTION:
web.service.consul.     0       IN      A       10.20.0.51
web.service.consul.     0       IN      A       10.20.0.52

;; Query time: 9 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Mon Oct 03 05:42:47 UTC 2016
;; MSG SIZE  rcvd: 68

ほかにもサービスの登録時に設定したポート番号などをSRVレコードとして検索することもできます。

暗号化

consul_ssl

ConsulではAgent間の通信をSSLで暗号化することができます。SSLで通信するためにはCAのルート証明書の秘密鍵で署名された自身のサーバ証明書とCAのルート証明書がそれぞれのノードに必要です。SSLでの認証は相手のサーバ証明書がCAで署名されいるかをルート証明書で検証しています。したがって自己署名の証明書を利用するなどしてサーバ証明書への署名を管理することにより、許可されたノードのみがConsulクラスタに接続できるようにすることができます。

暗号化の設定は通信する双方のAgentで一貫した設定がされている必要があります。またSSLによる暗号化はConsulとしての通信の部分に適用されます。Gossipプロトコルなどは別なので、設定にミスがあった場合、一見(Serfによる)クラスタには正しく参加しているのに、Consulの機能は利用できない状態になってしまうので注意が必要です。

暗号化および認証は基本出ていく通信(outgoing)において相手の証明書の署名の検証および暗号化をおこないます。入ってくる通信(incoming)においても相手の認証および暗号化の強制を行う、相手がConsul Serverの場合、証明書のCommon nameを確認するなどの設定も可能です。

verify_outgoing:true/false

  • AgentからAgentへの通信(outgoingの通信)にTLS通信を利用する。
  • 通信時に相手から提供されるサーバ証明書を自身の持つルート証明書で検証する 。
  • 通信先のConsul AgentがTLS通信に対応してない場合には通信できない状態になる

verify_server_hostname:true/false

  • 相手がConsul serverの通信では通常のoutgoingの通信の手順に加えて、証明書のホストネーム(common name)も確認する。
  • Consulサーバのサーバ証明書のホスト名(common name)がserver..<ドメイン名>になっていない場合通信を拒否する。"server”は実際のそのサーバのホスト名ではなく固定の名前。(例:server.dc1.consul)
  • consulのclientとしてしか許可されていないホストがserverにならないにするための設定

verify_incoming:true/false

  • 他のConsul Agentから(incomingの通信)のTLS通信において相手先を認証する。
  • 通常のTLS通信の手続きに加えて、受信側から送信側の証明書を要求してそれを自身の持つルート証明書で認証する。
  • verify_incomingがtrueに設定されている場合、他のAgentからのTLS以外の通信は許可しない
  • verify_incomingが設定されていない場合、TLS通信でも通常の通信でもどちらでも許可する

Key Value Storage

Consulの大きな特徴の1つはKVS(Key Value Storage)を備えていることです。KVSのデータにはクラスタ内どのノード上からでもアクセスが可能です。また、外部のサーバからも許可があればアクセス可能です。

データはConsul Serverとして起動されたAgentが管理しています。Consul Serverを複数実行することによってデータは冗長化され、サーバ間でのRaftによる分散合意アルゴリズムによって一貫性が保証されます。ノードの名前やアドレス、登録したサービスなどの情報もこのKVS上のデータとして共有されています。

データはまずLeaderのConsul ServerのWAL(Write Ahead Log)に書き込まれたあとコミットされた時点で永続化されます。具体的にはLeaderのConsul Agentの作業領域に指定されているディスク上に書き込まれます。Leaderで永続化されたあと、各FollowerのConsul Serverにレプリケーションされます。Consulの設定によって永続化を無効にすることも可能です。ディスクに書き込む手間がなくなる分早くなりますが、データはメモリ上のみの保存になるのであくまで検証用です。

デフォルトではKVS上のデータには誰でもフルアクセス(読み書き)が可能です。Consulをroot権限で実行していても一般ユーザからKVS上のデータにフルアクセスが可能になっています。KVSへのアクセスを制限するにはACL(Access Control List)という仕組みを利用します。これはトークン単位で可能なアクセスを設定するものでKVS上の値だけではなく、サービスの検索やイベントへのアクセス権限も設定することが可能です。

KVSの操作はHTTP APIまたはConsul Cli(consul kvコマンド)で可能です。HTTPでアクセスする場合、ローカルホストの8500番がデフォルトのアクセスポートになっています。KVSにはすべてのメンバ上でアクセス可能で、Clientノード上の場合ローカルホストのAgentが適当なServerにアクセスしてユーザに返答します。HTTPでの応答はJSON形式でキーの値はBase64でエンコードされてます。


# curl -X PUT -d 'Hello World' http://localhost:8500/v1/kv/test
true

# curl -X GET http://localhost:8500/v1/kv/test | jq "."
[
  {
    "LockIndex": 0,
    "Key": "test",
    "Flags": 0,
    "Value": "SGVsbG8gV29ybGQ=",
    "CreateIndex": 359871,
    "ModifyIndex": 359871
  }
]

# curl -X GET http://localhost:8500/v1/kv/test | jq ".[].Value" -r | base64 -d
Hello World

イベントハンドラ

Serfではイベントをトリガとしてノード上でスクリプト実行などの処理を自動で行わせることが可能でしたが、Consulではイベントだけではなく、KVS上のデータの変化やServiceの登録、ヘルスチェックの結果などもトリガとして利用できるようになっています。

ConsulではKVS上のデータの変化を監視にBlocking Queryと呼ばれる仕組みが利用できます。これはKVS上の値への問い合わせに対してその値に変化があるかタイムアウトするまで応答をBlockし、変化があれ(あるいはタイムアウトすれば)応答を返す仕組みです。これによって従来のPollingなどと比較してKVSなどの変化に対して即座に処理を実行することが可能になっています。

consul-template

KVSの値を元にテンプレートから各種設定ファイルを作成し、変化に応じてそれを更新して反映させるなどの処理を実行したい場合、consul-templateと呼ばれるツールを利用することができます。ConsulのKVSやServiceなどの状態を監視して変化があった場合にはテンプレートから設定ファイルを再生成したあと、デーモン等を再起動して設定を反映させることを自動で可能になります。

/etc/hostsファイルのテンプレートを作成します。以下の例ではConsulのServiceの登録情報からホストの一覧を作成します。consul-templateはテンプレートエンジンとしてgo templateを利用しておりループ構造なども利用できます。

host.ctmpl


{{range services}}# {{.Name}}{{range service .Name "any"}}
{{.Address}}    {{.Node}}{{end}}

{{end}}

consul-templateはBlocking Queryを利用してKVS上の値を監視し、変化(追加)があった場合即座にテンプレートを適用してファイルを更新します。必要に応じて適用後のデーモンの再起動なども可能です。


# consul-template -consul localhost:8500 -template "template/hosts.ctmpl:/etc/hosts" 
> /etc/hosts

# web
10.20.0.51    web1
10.20.0.52    web2
10.20.0.53    web3

まとめ

今回はコンテナによるクラスタ構成において利用できそうなConsulの機能について調べてみました。ConsulはSerfをベースにしたメンバーシップ管理機能に加えて、メンバのアドレスやサービスの検索、Key Value Storageを利用したメンバ間データの共有などの機能を提供します。また、イベントハンドラや外部ツールと組わせることによって構成の変化に応じて動的に設定ファイルを再生成しリロードさせるといったオーケストレーション的な動作も可能です。


tech_ccmp at 15:36
この記事のURLComments(0)TrackBack(0)
2017年01月23日

最近の Python-dev (2017-01)

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

@methane です。 compact dict が Python 3.6 が9月(ベータになる直前)にマージされ、それのおかげで推薦をもらい 10月ごろから Python の Core Developer になりました

「PythonのフルタイムコミッタとしてKLabに雇われている」という訳ではないのですが、 もともと自己裁量で業務時間の大半をOSSへの貢献やコードを読むことに費やし、特にこの3ヶ月位は Python ばかり触っていたので、実質的には近い状態です。

そちらでの活動をあまり日本で共有する機会がないので、 Money Forward の卜部さんが書かれている 最近の ruby-core という記事をリスペクトして、 最近の Python の開発状況を紹介する記事を書いてみたいと思います。

Python 3.6 リリース

12/23 に Python 3.6 がリリースされました。紹介しきれないほどたくさんの重要な改善がされていますが、 多分 (Python を普段使わない人にも) 一番わかり易いのは f-string でしょう。

f-string は文字列の中に式を書くことができる機能です。多くのLLが持っている機能なのですが、乱用すると簡単にメンテしにくいコードが書けてしまうので、 従来 "{foo.name} = {foo.value}".format(foo=foo) と書いていたのを f"{foo.name} = {foo.value}" とするように、 純粋に .format(name=name) の置き換えとして使い始めることをおすすめします。

私が主に貢献したのは compact dict と asyncio の高速化です。余談ですが、 Python 3.6 の2日後にリリースされた Ruby 2.4.0 にも compact dict とほぼ同一の新しいハッシュの実装が導入されました。偶然ですね。

(主に) C locale での UTF-8 サポート改善

Linux では Python はターミナル、標準入出力、ファイルパスなどのエンコーディングを locale を見て決定します。

ですが、 POSIX で C locale (POSIX locale とも言う) は ASCII を使うと決められているために、非 ASCII 文字を使うと UnicodeEncodeError を発生させてしまいます。標準入出力に関しては PYTHONIOENCODING という環境変数で制御できるのですが、 コマンドライン引数とファイルパスのエンコーディングは設定できませんでした。

C locale はデフォルトのロケールなので、 crontab に指定しなかったとき、 ssh が LANG=ja_JP.UTF-8 を送った接続先の サーバーが ja_JP.UTF-8 ロケールを持っていなかったときなどに意図せず使われます。翻訳されたエラーメッセージなんて (英語で報告しにくいから)見たくないとか、コマンドの挙動が変わるのが嫌だからという理由で意図的に C locale を使うエンジニアも多いでしょう。コンテナや組み込みなどの小さなLinux環境を作るときは容量削減のために C 以外のロケールが そもそも存在しないこともあります。

そういうさまざまな理由で C locale 上で Python を使うと、 UnicodeEncodeError が発生する事があり、特に Python 開発者ではない Python 製のツールを使ってるだけの人を困らせることになります。

Python は広いユーザー層を抱えているので locale の使われ方も様々なのですが、それでも本当に ASCII だけを使いたくて C locale を使っているユーザーはほとんどいないだろうということで、 C locale でデフォルトで UTF-8 を使うための提案がされています。

(まだ提案段階なので、 Python の開発ブランチをチェックアウトしても使えません)

PEP 540: Add a new UTF-8 mode

Python に UTF-8 mode を追加しようという提案です。 UTF-8 mode では locale が指定するエンコーディングを無視して、 ファイルパスと標準入出力が UTF-8 になります。

このモードは厳密に言えば disabled, enabled, strict の3つの状態があります。 enabled と strict の違いは、 UTF-8 でない バイト列を透過的に扱うための surrogate escape を使うか、 UTF-8 でない バイト列はエラーにするかです。

C locale ではデフォルトでこのモードが enabled になり、UTF-8でないファイルパスや標準入出力への読み書きが可能になります。 今時はめったに使わないかも知れないですが、外部のファイルシステムをマウントしたときとかに嬉しいはずです。

C 以外の locale では locale 指定のエンコーディングが strict で使われるので、 UTF-8 以外のデータはエラーを発生させます。 こちらは UTF-8 以外は除去したいときに、誤って文字化けしたデータを作るのを早めに気づいて止めたいときに便利です。

このモードは PYTHONUTF8 という環境変数や -X utf-8 というオプションで制御できるので、 locale を無視したい人は .bashrc などに export PYTHONUTF8=1 と書くか、 /etc/environmentPYTHONUTF8=1 と書いておくと良いでしょう。

PEP 538: Coercing the legacy C locale to C.UTF-8

こちらは、環境変数が指定しているのが C locale だったときに、 locale を C.UTF-8 に(システムが対応していれば)変更してしまおうという提案です。

これにより、 Python 本体だけでなく、別のライブラリも ASCII や latin1 ではなく UTF-8 で動作するようになることが期待されます。

例えば Python の REPL でも使われている readline が該当するので、 Android 上の Python の REPL で 何の設定をしなくても快適に UTF-8 を扱えるようになったという報告があります。

新しい呼び出し規約の拡大 (METH_FASTCALL)

従来、Cの世界から見た Python の関数呼び出しとは、順序引数をタプルで、キーワード引数を dict で渡すものでした。

Python 3.6 でタプルの代わりにただの配列の先頭ポインタ+順序引数の数を渡す呼び出す規約が登場し、呼び出し側が スタックに積んだ引数をタプルに詰めなくてもそのまま渡せるようになりました。

現在、C言語で作った「Pythonから呼べる関数」をこの新しい呼び出し規約に対応させる作業が進んでいて、重要な部分はあらかた対応が終わりました。

また、かなり内部の話になるのですが、 PyMethodDef (名前, 呼び出し規約を示すフラグ, 関数ポインタでなる構造体) で作成する関数やメソッド以外に、 オブジェクト自体が呼び出し可能 (Python に詳しい人なら、 operator.itemgetter() が返すオブジェクトといえば伝わるかも知れません) な型があり、そちらは型のメタデータとなる構造体に関数ポインタ tp_call が含まれています。

PyMethodDef の方は従来からフラグを使って複数の呼び出し規約に対応していたので、C言語から呼ぶときは専用のAPIを使っていました。 一方tp_call の方は今までタプルと dict を受け取る標準の呼び出し規約しかサポートしていなかったので、外部のライブラリがAPIを経由せずに関数ポインタを直接呼び出ししてしまっている可能性があります。

そこで互換性維持のために tp_fastcall という関数ポインタを新たに追加し、 tp_fastcall を用意している型には自動的に tp_call に変換するための関数を埋めるようにするパッチがレビュー中です。

細かいようですが、 Python の世界から見たら1つの呼び出しの引数が、C言語の世界では複数の関数の間で引き渡される事があるので、内部に新方式と従来方式が混ざっていると余計な変換が何度も発生する可能性があります。

Python 3.7 では内部での引数の渡し方が新方式に統一されて新しい呼び出し規約の実力が発揮できると思います。

Github へのマイグレーション

去年の1月にアナウンスされてから着々と準備が進んできていた、 Mercurial から Git、 hg.python.org から Github への移行ですが、着々と準備が進んでいるようです。

1/17 に Python 3.4.6 と 3.5.3 がリリースされ、 migration window と宣言されていた期間が始まったので、そろそろ実際に移行するスケジュールがアナウンスされると思います。1月中に移行できると良いなー。


songofacandy at 16:57
この記事のURLComments(0)TrackBack(0)Python 
2017年01月20日

さよなら Parse

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

はじめに

世界中の利用者の指先と視線を凝固させた突然の発表からまもなく 1年、Parse.com の全サービスが いよいよ 2017年1月28日(土)に終了します。拡張性が高く高機能でありながら使い勝手の良い優れたサービスだったので終息が惜しまれます。

      https://parse.com/

手元では 2年ほど前に IoT の実験として試作した以下のしくみ「Anpi」で Parse.com を採り入れました。

  • mbed と Parse で作る高齢者世帯安否確認システム - 当ブログ
    一般に BaaS の主な目的はアプリケーションの対向サーバ側機能を代替・補完することにあり、サーバの管理運用やサーバ側コード開発に踏み込むコストを抑制しアプリ本体の開発に注力可能となることが利用者にとってのメリットですが、Parse にはサーバ上でユーザコードを実行することのできる「Cloud Code」というしくみが用意されています。(中略)今回作った装置は Parse サーバ上に設置した自作コードをそのまま呼び出して必要な処理を行っています。このように目的に応じて柔軟に利用できることが Parse の魅力のひとつと言ってよいでしょう。
この「Anpi」はシンプルながら実用性が高く現在もプライベートで大いに役立っています。そのためすでに Parse.com からの乗り換えを完了していますが、移行の過程で複数のプラットフォームを対象として再実装を横断的に試してみました。今後必要となった際にスムースに想起できる道具立ての選択肢は多いほうが好ましく、今回のように具体的な要件があればその実装を通じて未体験のサービスの特徴や個性を見定めやすいと考えたためです。この記事はそういった試みの記録で、日本国内ではまだ知名度の低いものにも触れています。興味のある方はご覧下さい。

※文中の記述はいずれも 2016年11月から12月の時点の状況に基づくものであり現在の事情とは異なる可能性があります。あらかじめご了承下さい。

手元の要件と移行先選定基準

「Anpi」において Parse.com サイドで行っていた処理は以下の内容です。

  • 屋内に設置ずみの装置が人感センサ反応時に最短 30 分間隔で送信してくる情報をデータストアへ保存
  • 定刻に直近 60 レコード分をレポートとして所定のアドレスへメール送信
  • 装置につないだボタンが長押しされたら所定のアドレスへメールで通知(緊急メール)
  • メール配信には Parse.com が 公式 API で連携している Mailgun サービスを利用

要件に特に煩雑なものはなくこれらを吸収可能なサービスはいくつも存在するでしょう。その上で、今回は次の三点を移行先選定の基準とすることにしました。

  • 運用に手がかからないこと
  • 柔軟性があること
  • 費用がかからないこと
絵に描いたようなユーザエゴではありますが、当時 Parse.com を選んだ理由はこれらのすべてを満たしていたためでもあります。予備調査を経て次のサービス・プラットフォームを検討の対象としました。

  1. IFTTT + Google Drive + Google Apps Script
  2. Kii Cloud
  3. back4app(+ AWS Lambda)
  4. Backand

なお、要件のうち「緊急メール」の発信については以前「今、ワンボタンの IoT デバイスが面白い」でも利用した SendGrid サービスの API を装置側のコードから直接叩くことにしました。メールの内容は決め打ちなので各サービスに仲介させるよりもそのほうが合理的と考えたためです。

以下、上のよっつを順番にピックアップしてみます。

移行先候補 1: IFTTT + Google Drive + Google Apps Script

もっともシンプルに要件を満たす道具立てとしてまず IFTTT と Google サービスの組み合わせを考えました。IFTTT 経由で Google Drive 上の Spreadsheet をデータストアとして利用し Google Apps Script でサーバサイドの処理を実行する内容です。

仮移行を通じての * 個人的な * 印象

GOOD !

  • シンプルかつ柔軟
  • Google, IFTTT ともに今後課金の発生する可能性がきわめて低い
  • Google, IFTTT ともに今後サービスが終息する可能性がきわめて低い
  • コードを含めすべてをブラウザ上で操作できるため運用上の自由度が高い
! GOOD ?

  • 今回の要件には十分だが規模の大きいデータを扱うには不向き
  • あくまでも SaaS とハブサービスの組み合わせであるため当然ながらプッシュ通知など一般的な IoT プラットフォームの提供する機能は代替できない

移行先候補 2: Kii Cloud

Kii CloudKii 株式会社様の提供する日本発の BaaS です。

仮移行を通じての * 個人的な * 印象

GOOD !

  • 多機能かつ無料枠が広い
  • Parse.com と同様に BaaS でありながらサーバ機能拡張が可能
  • サーバのリージョンを自由に選択可能であり特に中国リージョンの存在は大きい
  • キーバリュー形式のデータは使用容量にカウントされないため他のサービスとの併用にも好適か
  • アクセス制御機能が充実
  • 日本語のリソースが充実している

! GOOD ?

  • ドキュメントの情報量は豊かだが通読性がもうひとつ?リンクの張り方にも改善の余地がありそうな印象(このあたりが改善されればより利用しやすくなるかも)
  • 開発者ポータルは UI・機能ともに今後の進化が期待される
  • 開発用のコマンドラインツールが Node.js ベースなのは利点と欠点が半々くらい? バッチジョブのスケジュール変更といった操作はブラウザ上でできると嬉しい・・

移行先候補 3: back4app(+ AWS Lambda)

back4appBACK4APP SERVICOS DIGITAIS LTDA(本社 米カリフォルニア)の提供する BaaS です。

仮移行を通じての * 個人的な * 印象

GOOD !

  • 無料枠が広い
  • 最初から Parse.com の代替用として起ち上げられた純度の高い Parse Alternative であるため Parse.com との親和性が高く移行が相対的に容易
  • UI, ドキュメントがわかりやすい

! GOOD ?

  • 記事中のジョブ設定の問題など現時点では荒削りな側面も見られる
  • Parse.com を継承するサービスとしては良好だが、逆にそのことが独立した BaaS としての新鮮味や個性の発露を削いでいる印象も?

移行先候補 4: Backand

BackandModuBiz Ltd(本社イスラエル Tel Aviv)の提供する BaaS です。

仮移行を通じての * 個人的な * 印象

GOOD !

  • 多機能かつ拡張性に優れている上に非常に使いやすい
  • テスト・デバッグ機能も充実しており UI もわかり易い
  • ファイルホスティングまわりの操作以外はすべてブラウザ上で完結できるため機動性が高い
  • ユーザビリティは今回の中で一番。あとは可用性とスケーラビリティ次第か

! GOOD ?

  • 無料枠の狭さが残念。また、Prototype Plan $0 -> Hobby Plan $19/月 -> Work Plan $49/月 ... といった大きめの料金差に比して待遇差が小さめ
  • 運営側の内部的な指標である「Cache Memory」「Compute Units」のように利用者が主体的にコントロールすることの難しい指標がプランの基準に含まれているため利用目処を立てにくい(むしろ、利用するならはじめから UNLIMITED な料金プランを選ぶべき)
  • 日本のみならず国外でもまだあまりメジャーではなく情報がとても少ない。優れたサービスであるにもかかわらず世間への浸透圧が低い一因は上の料金体系にもあるのではないか?

おわりに

手元要件の Parse.com からの移行にあたり以上よっつの環境を試してみました。最終的にこの中のひとつを「Anpi」の乗り換え先に決定して現在に至ります。当初はどれを選んだのかを書くつもりでしたが無粋にも思いやめておくことにしました。いずれも質の高いサービスでそれぞれに十分なメリットがあります。

ほんの10年ほど前には影もなかったものが日進月歩で進化していく状況にリアルタイムで向き合っていると10年後の世界への想像が膨らみます。この時代の傍らを去っていく Parse.com をユーザのひとりとして敬意と感謝の念をもって見送りたいと思います。


(tanabe)
klab_gijutsu2 at 14:26
この記事のURLComments(0)TrackBack(0)cloud | IoT
2016年12月25日

CTOの独り言

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

このエントリーは、KLab Advent Calendar 2016 の12/25 の記事です。
最終日の担当はCTOの安井です。

今年のアドベントカレンダーもテーマが多岐に渡っていてとても興味深いです。
ゲームに関係ありそうなものから関係なさそうなものまで盛りだくさんですね。

KLabでは業務と直接関係のない開発活動も積極的に推奨しています。 これには、自分が得意とする分野、興味のある技術に対して全力で取り組んで欲しいという願いがあります。好きなことに全力で取り組んで結果を残すことができるエンジニアは、様々な場面において高いアウトプットを出してくれることが多いものです。

社内ではよく「市場価値の高いエンジニアを目指そう」という言い方をしています。 エンジニアが安心して仕事を続けるためには「自分の実力に自信を持てること」がとても重要なことだと考えています。いくら上司から高い評価を得ていても、いくら業績に貢献していても、社内のモノサシだけで評価されている限り、自分の実力が世の中で通用するのか、今の仕事を続けていてスキルアップできるのかなど、様々な不安にかられてしまうことがあるものです。

そのような不安を少しでも払拭できるように、社外のコミュニティに関わる事を推奨しています。勉強会などで登壇させてもらうのも良いですし、既存のOSSプロダクトにパッチを送ってみたり、自分でプロダクトを立ち上げるのも良いですね。今はブログやQiitaやgithubなどで気軽に技術情報を共有できたりボタン一発で作者にパッチを送ることができる時代ですので、既に多くの方は既に実践されているでしょうし、これから始める方にとってもハードルはかなり低くなっていると思います。


ただ、ゲーム開発を通して得られたノウハウや知見の中には、外部に公開できないものも少なからずあります。そのため、社内に閉じたコミュニティや勉強会も用意しており、誰でも自由に発表することができるようになっています。

KLabの社内勉強会は社内への情報共有も目的のひとつですが、社外へ公開する前のレビューとしての役割があったり、自分の活動を理解してもらうためのプレゼンの練習の舞台でもあったり、誰がどんな活動をしているのかを知ることができる場としても活用されています。

なにかしらの方法で自分の実力が世の中に評価される機会があれば、それは自信に繋がりますし、自分が何をしたいのか、どんな方向に進みたいのかの道筋が見えるきっかけになるかもしれません。その結果、現在の仕事が自分のためになっているのかを客観的かつ冷静に判断できるようにもなるでしょう。

エンジニアの皆さんには「自分はどこの会社でもやっていける」と思えるくらいの自信とプライドを持って欲しいです。そして「自分の力を会社に貸してやるか」くらいの気持ちで働いて欲しいと思っています。会社は「給料分だけ働いてくれればいい」とは決して思っていませんし、「給料分は働かなきゃいけない」と思いながら働いて欲しくもありません。自分の腕を磨き、価値を高め、実力に見合った報酬を受け取ることが、良い仕事をするための条件であり、年齢を重ねても技術者であり続けるために必要なことだと私は考えます。


と、ここまでなんだかんだで好き放題言ってみましたが、実際の現場で理想ばかり掲げているわけにはいかないものです。みんながみんな好きなことだけやってたら仕事は進みませんので・・・

会社が成長するには事業を継続して拡大させていく必要があるわけで、現在のKLabの主力事業はゲームです。ゲームの開発と運営を円滑に進めるには、多くの方々に様々な役割を担っていただかなければなりません。

プロジェクトにおいてエンジニアの役割やタスクを決定するのはマネージャです。
マネージャの役割はプロジェクトを円滑に進めることです。

プロジェクトを円滑に進めるには「できる事をできる人にお願いする」のが最も効率が良いものです。同じ事を同じ人にお願いする事で、スケジュールの遅延や予算超過などのリスクを低減させることができますし、品質の向上も期待できます。それになにより「できないかも?」という不安を抱えなくてよくなることが大きな魅力となります。

ただその反面、同じことの繰り返しは本人のモチベーションを下げてしまったり、ノウハウが一部のメンバに偏ってしまうなどの問題があるので、マネージャは「これまでの経験を活かした仕事」と「新しい仕事にチャレンジする機会」をバランス良く振り分ける事に頭を悩ませます。

社員の成長なくして会社は成長できないことはマネージャは十分に理解しています。 エンジニアに新しいことにチャレンジして欲しい場面では、マネージャはこれまでの成果や面談内容などをベースにして検討しますが、本人がどんな分野に興味があり、業務以外でどのような活動をしているのかを知る事で検討の幅は大きく広がります。先に書いたように、業務に縛られず、自由に自身の活動を社内外に公開することを推奨しているのは、このような狙いもあるからです。


もし「自分の時間なんて全然取れない!仕事してるだけで精一杯だ!」と感じている人がいたら是非アラートを上げて欲しいです。エンジニアのキャパシティを把握するのもマネージャの大事な仕事です。そのためには、多少の無理を承知の上でタスクを割り当てることもあります。自分のキャパを超える仕事が与えられた時には遠慮なく相談しましょう。そして自分のキャパを正しく理解してもらいましょう。できない事を「できない!」というのは恥でもなんでもありません。こっそりと自分の時間を潰してストレスを溜めるくらいなら、仕事の量を調整してもらえるように交渉することもエンジニアの大事な役割です。


つらつらと取り留めのない内容になってしまいましたが、普段社内で喋っていることを文章にしてみました。何度も聞かされてる人にとっては耳タコかもしれませんが、普段交流できていない方々も結構いますので(ごめんなさい)、KLabのCTOはこんなこと考えてるんだよぉってのを少しでも知って頂ければありがたいかなと思います。


klab_gijutsu2 at 00:00
この記事のURLComments(0)TrackBack(0)
2016年12月22日

新しい Amazon Dash Button に「マイク」が残されている理由

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

昨年以下の記事でピックアップした「Amazon Dash Button」が 2016年12月5日に日本国内でもリリースされ話題を集めています。

Amazon.co.jp での販売開始からほどなく Button の多くが品切れとなり 12月22日現在も入荷待ちの状態が続いています。 このデバイスを実質無料で配布する巨大多国籍企業 Amazon のパワーは凄まじいですね。

新旧 Dash Button の違い

現在提供されている Dash Button は 2015年に米国でデビューしたオリジナルとは異なるものです。上のブログ記事でも触れた Matthew Petroff 氏のサイトには新旧 Button それぞれについて詳細かつ網羅的な興味ぶかい記事が掲載されています。

The Amazon Dash Button it an Internet connected button that allows ordering a single product from Amazon.
           :
Others have already posted about disassembling it, so I’ll focus mostly on the electronics, since the aforementioned blog posts are missing high-resolution images of thecircuit board and don’t quite get some details correct.
Amazon updated the Dash Button’s hardware to revision two earlier this year, so I decided it was time for a new teardown (here’s last year’s teardown). The new product number is JK29LP; the old product number is JK76PL. While the form factor and case remained much the same, the internals changed substantially. The major highlights are a switch from Broadcom to Atmel chips, a switch from an Energizer lithium battery to a Duracell alkaline battery, and the addition of Bluetooth Low Energy.
           :

Overall, the new Dash Button appears to be a revision designed to reduce production cost, centered around a reduction in energy usage, which allows for use of a considerably cheaper, alkaline battery.
同氏の記事を参考にしながらまず新旧 Button の主な違いを整理してみます。
(※余談ながら、同じ記事に注目した数日前の GIGAZINE さんの記事に先ほど気がつきましたが、特にぶつかるものでもないため草稿の内容のまま書くことにします)

電源がリチウム一次乾電池からアルカリ乾電池に

旧 Dash Button には単四型リチウム一次乾電池が内蔵されていました。

http://dsas.blog.klab.org/archives/52233150.html

ちなみに Amazon Dash Button は 米エナジャイザー社Ultimate Lithium 乾電池 単4形 (二次電池ではない)1本を内蔵している。アルカリ電池に比べ「最大 9倍長持ち」を惹句とする 現時点でおそらく最強の乾電池。

データシートによると Ultimate Lithium AAA は「Max Discharge: 1.5 Amps Continuous, 2.0Amps Pulse (2 sec on / 8 sec off)」と高容量

現在の Dash Button ではこの高価な電池に代えて米デュラセル社の単四型アルカリ乾電池が使用されています。
旧:Energizer Ultimate Lithium AAA Battery
新:Duracell Ultra AAA Alkaline Battery
https://mpetroff.net/
(2016-12-14 時点での両者の小売価格の例) 電池交換は新 Button においても不可であるため上記の変更が使用寿命に及ぼす影響が気になりますが、Petroff 氏による新旧両 Button の消費電流実測結果(下グラフ:目盛幅の違いに注意)によればスリープ状態では新 Button が 2.0μA以下、旧 Button が 2.3μA以下と前者のほうが良好であり、また、ボタン押下後のアクティブ状態においては新 Button のほうがおおむね 10%前後レベルが高いものの処理を完了し再度スリープするまでの所要時間は旧 Button の半分以下と、電源仕様の変更に伴い電力消費を抑制する作りに変更されている様子が窺えます。
https://mpetroff.net/

マイクロコントローラ / Wi-Fi チップの変更, BLE チップ追加, Flash メモリ容量増加

新 Button では、以前の ST マイクロエレクトロニクス社製マイクロコントローラ STM32F205 が Atmel 社(2016年4月より Microchip Technology 傘下)製 ATSAMG55J19A-MU に、Broadcom 社製の Wi-Fi モジュール BCM943362WCD4 WICED が Atmel 社製 ATWINC1500B に変更されています。一方で 新 Button の Flash メモリは倍容量の 32Mビットに増強されており、また、旧 Button には存在しなかった BLE チップがセットアップ用に追加されています。これら一連の構成要素の変更は性能強化とコストダウンの両立を目的とした判断の結果と考えられます。


U5) マイクロコントローラ:ST STM32F205
U9) Wi-Fi モジュール:
Broadcom BCM943362WCD4 WICED
U6) フラッシュメモリ: Micron M25P16 (16Mbit)
マイクあり


U1) マイクロコントローラ:Atmel ATSAMG55J19A-MU
U19) Wi-Fi チップ:Atmel ATWINC1500B
U22) BLE チップ:Cypress CYBL10563-68FNXI
U15) フラッシュメモリ: Micron N25Q032 (32Mbit)
マイクあり
https://mpetroff.net/

セットアップ方法の変更

新旧 Button はセットアップの方法が異なります。Android または iOS 端末と公式ショッピングアプリを利用する点は共通ですが、新版が同アプリと Button との応酬に BLE 通信を使う内容であるのに対し、旧版では Android 環境では Wi-Fi 通信、iOS 環境では超音波通信を利用する仕様でした。ちなみに、旧版では Fire Phone もセットアップに利用することが可能でした。

旧 Dash Button のセットアップ

web.archive.org に旧 Button のセットアップ手順説明ページのキャッシュが残っています。 この内容から、ボタン長押しにより移行するセットアップモードにおいて旧 Button がダミー Wi-Fi アクセスポイント 兼 音声情報のリスナーとして振る舞っていたことがわかります。

※下の図は上記キャッシュの iOS 端末向けの説明箇所からの抜粋
※動画は旧版のセットアップの様子(Youtube 2015ー08-06 投稿, iPhone)3分20秒あたりから


http://www.amazon.com/
https://youtu.be/NSrdo5oNzsI?t=203
Android 端末を使ったセットアップに Dash Button のダミー Wi-Fi アクセスポイントへの一時的な切り替えが利用されている一方で iOS 端末でのセットアップに音声信号が採用されたのは JailBreak しない限り接続先の Wi-Fi アクセスポイントをプログラムから変更不可であることに起因するもののようです。

新 Dash Button のセットアップ

BLE チップが搭載された新 Button ではセットアップ時の公式アプリとの応酬に BLE 通信が利用されています。

この変更によって Android / iOS プラットフォームでの手順が統一されシンプルでスマートになった反面、旧版では特に言及されていなかった対象 OS バージョンが「iOS 8.3 or higher, Android 4.1 or higher」に限定される形となりました。また、BLE 非対応の Fire Phone(2015年下期販売終了)も手順説明から消えています。

現在の Dash Button セットアップ手順説明ページ

画面遷移

1
2
3
4
5
6
7
8
9

新 Dash Button にマイクが残されている理由

旧 Button が iOS 端末でのセットアップ時に音声信号を利用するためにマイクロフォンを内蔵していたことは理解できます。では、なぜ BLE 通信を利用する新 Button にもマイクが残されているのでしょう?使わない部品であれば製造コスト削減のためにも撤去するほうが合理的なはずです。まさか Amazon が何か良からぬことを企んでいるのでしょうか?

前掲の手順説明ページを読んでいるうちにふと以下の記述が気になりました。

Note: Some Dash Buttons and phones do not support Bluetooth connections. If your phone does not connect to your button, select Skip Bluetooth Setup. Then, follow the instructions in the app. Your phone then uses other connection options.
注: スマートフォンとDash Buttonが接続されない場合は、 Bluetoothをスキップを選択します。次に、アプリの画面に 表示される手順に従います。

公式アプリが Button と BLE 接続を確立できない場合はどのような所作となるのでしょう?興味を感じ試してみることにしました。

BLE 接続不可の状況を再現するもっとも簡単な方法は「Button からの BLE アドバタイジングを発生させない」ことです。つまり、アプリに求められたタイミングでわざと Button のセットアップモードを起動せずそのまま放置しておけばよいでしょう。

結論として、アプリは BLE 接続をしばらく試行した後に自動的に旧 Button でのセットアップシーケンスへ移行することがわかりました。 BLE 接続を諦めると、Android 版アプリは Button のダミー AP へ Wi-Fi 接続の切り替えを試み、iOS 版アプリは端末のスピーカーへ Button を接近させることを利用者へ促します。そのタイミングで新 Button をセットアップモードで起動すると旧スタイルでのセットアップ処理が滞りなく行われます。

つまり、新 Button のセットアップモードは旧 Button でのそれと同一の I/F を備えています。この実装は、BLE の利便性を取り入れつつも間口の広い旧版での機構を温存することによりセットアップ段階でのトラブルを可能な限り吸収することに加え、旧 Button - 新アプリ間の互換性をシンプルに保つことを目的とするものと考えられます。

Android, iOS 端末それぞれでこの操作を行った様子の動画を以下に示します。

  • 新 Button を旧スタイルでセットアップ - Android 版(2分9秒 環境音あり)
  • 新 Button を旧スタイルでセットアップ - iOS 版(1分33秒 環境音あり)
なお、アプリは「端末の Bluetooth 機能が OFF の場合」に以下のメッセージを表示します。ここで Android 版では「拒否」、iOS 版では「Bluetooth をスキップ」を選択することで同様に旧スタイルのセットアップシーケンスへ移行することを確認しました。
Andorid 版
iOS 版

図のように iOS 版に関しては本項の冒頭に引用した英語版の説明記事とほぼ整合しますが、日本語版記事は翻訳が十分ではなく(意図的なもの?)、また、Android ユーザが記事中のメッセージを目にする機会はありません。

米国内にのみ存在する旧 Button は電池寿命で徐々に消えていく過程にあります。また、BLE 非対応の端末も次第に世間から姿を消していくことでしょう。上に掲げた現 Dash Button の二段構えのセットアップ I/F はあるいは過渡的なものかもしれません。


(tanabe)
klab_gijutsu2 at 14:41
この記事のURLComments(0)TrackBack(0)IoT 
2016年12月19日

USBポートに挿すだけでインターネット接続を乗っ取るガジェットを作ってみた

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

このエントリーは、KLab Advent Calendar 2016 の20日目の記事です。

煽り気味のタイトルですみません。少し前に話題になった「PoisonTap」に興味を惹かれ、どうやって動作しているのかを調べて、実際に手元で組み上げてみましたというお話です。(今回は半田ごては使いません)

先日、5V電源が欲しくて同僚にUSBポートを借りようとしたら全力で拒否されました。USBも気軽に挿してもらえない世の中じゃ... @pandax381 です。

*** 本記事の内容は技術的な検証を行うことが目的であり、迷惑行為や犯罪行為を推奨するものではありません ***

PoisonTap とは?

PoisonTap とは、1ヶ月ほど前に Samy Kamkar 氏が公開して話題になったクラッキングデバイスです。

Raspberry Pi Zero をベースにしたデバイスから伸びた USB ケーブルをコンピュータに接続しただけで、対象のコンピュータからクッキーを盗み出したりバックドアを仕込んでしまうという代物で、Hacker News などで取り上げられて話題になりました。

This $5 Device Can Hack your Password-Protected Computers in Just One Minute - The Hacker News

同氏が公開している動画の中では、アンロックされた状態のコンピュータに接続していますが「パスワードロックされた状態」であっても「1分」でクラック可能と謳っているところを見ると、なかなか凶悪なデバイスであることがわかります。

なお、PoisonTap に関する詳細な情報は作者のWebサイトで公開されており、ソースコードも GitHub で公開されています。

なにをやっているのか?

PoisonTap が何をしているのか、ものすごく端折って書き出すと以下の3点にまとめられます。

  • USB Ethernet アダプタとして振る舞う(物理レイヤ)
  • ネットワークトラフィックを吸い込む(ネットワークレイヤ)
  • 本来の通信相手になりすまして悪さをする(アプリケーションレイヤ)

これらの要素をもう少し掘り下げて解説します。

USB Ethernet アダプタとして振る舞う

PoisonTap をコンピュータに接続すると USB Ethernet アダプタとして識別されます。これは、Linux が備えている USB Gadget Driver を利用して実現しているようです。USB Gadget Driver は、USB のデバイスコントローラを使って様々な機能を提供するドライバで、イーサネットやシリアル通信、HID、Webカメラ、ストレージなどとして振る舞うことができます。

USB のコントローラには、ホストコントローラとデバイスコントローラの二種類があり、Gadget Driver を利用するためにはデバイスコントローラが必要になります。通常の PC にはホストコントローラしか搭載されていませんが、スマホや組み込みボードなどには、ホストにもデバイスにもなれる「USB OTG(On-The-Go)」に対応した MicroUSB のポートが搭載されていることが多く、PoisonTap も Raspberry Pi Zero の OTG 対応 MicroUSB ポートを利用しています。

USB Gadget Driver のイーサネット機能は、本来は Raspberry Pi Zero のようにネットワークインタフェースを持たないデバイスが USB を経由して別のホストと通信するために使われるものですが、PoisonTap はこの仕組みを巧妙に利用しています。

ネットワークトラフィックを吸い込む

PoisonTap は、接続したコンピュータに対して通信相手が PoisonTap がなりすました USB Ethernet アダプタの先に存在していると思い込ませることで、ネットワークトラフィックを吸い込みます。これを実現するために、PoisonTap は以下の動作をします。

  • PoisonTap 内部で稼働している DCHP サーバがアドレスを払い出す
  • DHCP でアドレスを払い出す際に、ルータ&DNS サーバとして自身のアドレスを広告する
  • PoisonTap 内部で稼働しているダミーの DNS サーバが何を聞かれても PoisonTap のアドレスを返す

PoisonTap の内部では DHCP サーバと DNS サーバが動作しています。ホスト側がネットワークの自動設定を行うために DHCPのリクエストを投げるため、これに対して内部で動作している DHCP サーバがレスポンスを返してネットワークのパラメータを流し込みます。この際に、DHCP のレスポンスに含まれるルータおよび DNS サーバのアドレスに PoisonTap 自身のアドレスを設定することで、トラフィックを吸い込もうとしています。

ここで、ある程度ネットワークに関する知識のある方は「こんなんで本当にトラフィックを吸い込めるのか?」と思われることでしょう。僕もすごく疑問だったのですが、結論から言うと「ある条件下では確かにトラフィックを吸い込める」ことが分かりました。

「ある条件下」とは、Mac OSX で(ネットワーク環境設定でインタフェースの優先順位を弄らずに)Wi-Fi のみで接続しているケースです。これまであまり意識したことはなかったのですが、OSX では Wi-Fi のインタフェースよりも有線インタフェースの方が優先されるようで、たとえ Wi-Fi でインターネットアクセス可能な環境で接続していたとしても、後から有線インタフェースが接続されると、有線インタフェース側の設定が適用されるのがデフォルトの動作です。そのため、動画にもあるように Wi-Fi だけで運用している OSX に PoisonTap を接続すると、かなり高い確率でプライマリのネットワークデバイスとして扱われ(DNSサーバやデフォルトゲートウェイの設定が適用されて)トラフィックが PoisonTap 側に吸い込まれてしまいます。

このような OSX の動作は、手動でネットワークインタフェースの優先度を設定することで回避できます。また、Windows ではどのような挙動をするのか試していないのでわかりませんが、もしかしたら OSX とは違う挙動をするかもしれません。それを見込んでか、PoisonTap はトラフィックを吸い込むために後述するような手の込んだことをやっています。

加えて、PoisonTap 内部で動作している DHCP サーバが配布しているアドレスがなかなかに邪悪で、 0.0.0.0/1 のネットワークから払い出されたものです。0.0.0.0/1 のアドレス空間は 0.0.0.0 - 127.255.255.255 であり、これは IPv4 アドレス空間の半分を占めています。ルーティングの際にデフォルトゲートウェイが選択されるのは、ルーティングテーブルにそれを内包するネットワークが存在しなかった場合なので、ネットワークアドレスを 1bit だけでも一致させてしまえばそのパケットのルーティング先を PoisonTap に向けることができるのです。つまり、ネットワークインタフェースの優先順位を手動で設定していたとしても、正規のルーティング処理で PoisonTap 側にパケットが吸い込まれるようにしているのです。

また、この方法で吸い込んだ DNS のリクエストにも応答できるように、パケットキャプチャをしながら、DNSパケットに無差別に応答する dnsspoof という DNS サーバを使っています。さらに、名前解決済みのホストに対する通信パケットを吸い込んだ場合を想定して、iptables で REDIRECT の設定もしています。

本来の通信相手になりすまして悪さをする

アプリケーションレイヤは守備範囲外のため今回の調査の対象外です。

実際に作ってみる

まずはじめに残念なお知らせですが、Raspberry Pi シリーズの場合、Zero 以外では USB Gadget の機能を使えません。何故かというと、B シリーズの USB ポートは USB コントローラに直結されているのではなく、USB HUB を内蔵した Ethernet コントローラを経由して引き出されており、USB コントローラが強制的にホストモードで動作するようになっているためです。(A シリーズはさわったことがないのでわかりませんが、B シリーズと同様にデバイスモードで使えないという情報を目にしました)

デバイスの選定

そんなわけで、手元にある Raspberry Pi 2 が使えないため、代わりのデバイスを調達しなければなりません。Raspberry Pi Zero は $5 と安価なものの、送料が $20 くらい掛かってしまうことと、一人1台の購入制限があるため他の候補を探してみました。

IMG_5291

OTG 対応の MicroUSB ポートを備えた手頃なボードを探していて見つけたのが「Orange Pi One」です。名前もさることながら、チップレイアウトがなかなかにロックですね。(ちなみに最新モデルのチップレイアウトは更に前衛的です)

完全に Raspberry Pi を意識しているわけですが、 衝撃のお値段 $9.99 に対して、Cortex-A7 1.2GHz Quad-Core / 512MB RAM / 100MB Ethernet と、まぁまぁなスペックになっています。GPIO ピンの配列も Raspberry Pi と互換性があると謳っています。ラインナップも豊富で、さらにコンパクトで安価なものから PC 代わりに使えそうなハイスペックなものまでありますが、Wi-Fi や Bluetooth などの無線コントローラを搭載していると技適の問題が出てくるので注意が必要です。

この Orange Pi が搭載してる SoC は「Allwinner」という中国の半導体メーカーのもので、有名どころだと「C.H.I.P.」や「ニンテンドークラシックミニ」などに搭載さています。

セットアップ

Orange Pi のサポートサイトで公式のイメージが配布されていますが、最終更新から一年以上経過していてあまりメンテナンスされていないように見受けられます。(っと思っていましたが、久しぶりに見たら数日前に更新されていました!)

ざっと調べたところコミュニティベースで開発されている armbian というディストリビューションを使うのが良さそうということがわかりました。Orange Pi シリーズや Banana Pi シリーズなどがサポートされています。イメージ配布だけではなく、ビルドのためのツールチェーンも公開されているので、カーネルのリビルドなどにも困らなそうです。

https://www.armbian.com/

「Download > Orange Pi One > Jessie server」と進んで、OS イメージをダウンロードします。

ダウンロードした OS イメージは 7zip で圧縮されているため、OSX の場合には展開するためのツールをインストールする必要があります。

$ sudo brew install p7zip

OS イメージを展開して dd で SDカードに書き込みます。(* dd コマンドの of= に指定するデバイスは環境に合わせて変更してください)

$ mkdir image
$ 7z -o./image x ~/Downloads/Armbian_5.20_Orangepione_Debian_jessie_3.4.112.7z
$ sudo dd if=./image/Armbian_5.20_Orangepione_Debian_jessie_3.4.112.img of=/dev/rdisk2  bs=1m

OS イメージにはブートローダ(U-Boot)など必要なものがすべて含まれているので、書き込み後は Orange Pi に差し込んで電源を入れればブート出来ます。

U-Boot SPL 2016.09-armbian (Sep 15 2016 - 07:19:14)
DRAM: 512 MiB
Trying to boot from MMC1


U-Boot 2016.09-armbian (Sep 15 2016 - 07:19:14 +0200) Allwinner Technology

CPU:   Allwinner H3 (SUN8I 1680)
Model: Xunlong Orange Pi One
DRAM:  512 MiB
MMC:   SUNXI SD/MMC: 0
*** Warning - bad CRC, using default environment

In:    serial
Out:   serial
Err:   serial
Net:   phy interface0
eth0: ethernet@1c30000
Hit any key to stop autoboot:  0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot/boot.scr
2886 bytes read in 158 ms (17.6 KiB/s)
## Executing script at 43100000
gpio: pin PL10 (gpio 298) value is 1
   Warning: value of pin is still 0
gpio: pin PG11 (gpio 203) value is 1
0 bytes read in 114 ms (0 Bytes/s)
** File not found /boot/.next **
** Unrecognized filesystem type **
** File not found .next **
35908 bytes read in 422 ms (83 KiB/s)
3114523 bytes read in 322 ms (9.2 MiB/s)
5025168 bytes read in 469 ms (10.2 MiB/s)
## Loading init Ramdisk from Legacy Image at 43300000 ...
   Image Name:   uInitrd
   Image Type:   ARM Linux RAMDisk Image (gzip compressed)
   Data Size:    3114459 Bytes = 3 MiB
   Load Address: 00000000
   Entry Point:  00000000
   Verifying Checksum ... OK
Using machid 0x1029 from environment

Starting kernel ...

...

Debian GNU/Linux 8 orangepione ttyS0

orangepione login:

root の初期パスワードは「1234」に設定されています。また、初回ログイン時に root のパスワード変更と一般ユーザの作成を行うスクリプトが走るようになっています。最後にネットワークの疎通確認も兼ねて apt のパッケージリストを更新してセットアップ完了です。

# apt-get update

USB Gadget の設定

PoisonTap と同じように USB Ethernet アダプタとして振る舞うために USB Gadget の設定を行います。

PoisonTap は pi_startup.sh というスクリプトの中で USB Gadget の設定を行っていますが、今回これは参考に出来ません。何故かというと、PoisonTap のやり方は configfs + libcomposite を使ったモダンな手法のため、armbian の安定版の kernel 3.4 ではレガシーな方法で設定しなければならないためです。

まず、USB Gadget の Ethernet Driver である g_ether.ko をロードします。

# modprobe g_ether idVendor=0x1d6b idProduct=0x0103 use_eem=0

g_ether.ko がロードされると、Orange Pi 側に usb0 というネットワークデバイスが追加されます。

# ip addr show usb0
4: usb0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether a6:e4:fd:ba:02:d6 brd ff:ff:ff:ff:ff:ff

とりあえず手動でアドレスを設定して起動させます。

# ip addr add 1.0.0.1/1 dev usb0
# ip link set usb0 up
# ip addr show usb0 
4: usb0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
    link/ether a6:e4:fd:ba:02:d6 brd ff:ff:ff:ff:ff:ff
    inet 1.0.0.1/1 scope global usb0

続いて Orange Pi の OTG ポートをデバイスモードに設定します。

# echo -n 2 > /sys/bus/platform/devices/sunxi_usb_udc/otg_role

この状態で Orange Pi の OTG ポートと MacBook を USB ケーブルで接続してみると「RNDIS/Ethernet Gadget」というデバイスが自動で追加されます。まだ DHCP サーバを起動していないので、アドレスを手動で設定してあげます。

14

手動でアドレスを設定してから Orange Pi 宛に ping を投げるとちゃんと応答が返ってきます。

$ ping -c 3 1.0.0.1
PING 1.0.0.1 (1.0.0.1): 56 data bytes
64 bytes from 1.0.0.1: icmp_seq=0 ttl=64 time=0.431 ms
64 bytes from 1.0.0.1: icmp_seq=1 ttl=64 time=0.403 ms
64 bytes from 1.0.0.1: icmp_seq=2 ttl=64 time=0.452 ms

--- 1.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.403/0.429/0.452/0.020 ms

ひとまず、これで Orange Pi を USB Ethernet アダプタとして認識させることができました。

DHCP サーバの設定

この辺は手順だけ。まず、DHCP サーバをインストールします。

# apt-get install isc-dhcp-server

続いて、/etc/default/isc-dhcp-server を編集します。

INTERFACES="usb0"

/etc/dhcp/dhcpd.conf に最低限の設定を記述します。

ddns-update-style none;

default-lease-time 600;
max-lease-time 7200;

log-facility local7;

subnet 0.0.0.0 netmask 128.0.0.0 {
  range 1.0.0.2 1.0.0.254;
  option routers 1.0.0.1;
  option domain-name "example.org";
  option domain-name-servers 1.0.0.1;
}

DHCPサーバを起動させます。

# systemctl start isc-dhcp-server.service

DNS サーバの設定

dnsspoof が含まれている dnsniff パッケージをインストールします。

# apt-get install dsniff

ルーティングの設定をしてから dnsspoof を起動します。

# sysctl -w net.ipv4.ip_forward=1
# ip route add 0.0.0.0/0 dev usb0
# dnsspoof -i usb0 port 53 >/dev/null &

試しに MacBook 側から名前解決をしてみると、どのドメイン名に対しても Orange Pi のアドレスが返ってくることが確認できます。

$ dig www.klab.com

; <<>> DiG 9.8.3-P1 <<>> www.klab.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25391
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;www.klab.com.			IN	A

;; ANSWER SECTION:
www.klab.com.		60	IN	A	1.0.0.1

;; Query time: 738 msec
;; SERVER: 1.0.0.1#53(1.0.0.1)
;; WHEN: Tue Dec 20 16:26:56 2016
;; MSG SIZE  rcvd: 46

HTTP サーバの設定

動作確認用に HTTP サーバを立ててダミーのコンテンツを返すように設定します。

# apt-get install lighttpd

/etc/lighttpd/lighttpd.conf を編集して lighttpd の稼働ポートを 8080 に変更します。

server.port                 = 8080

設定を反映させるために lighttpd を再起動させます。

# systemctl restart lighttpd.service

どのアドレス宛に来たリクエストでも処理できるように iptables で REDIRECT の設定をしておきます。

# iptables -t nat -A PREROUTING -i usb0 -p tcp --dport 80 -j REDIRECT --to-port 8080

本当はどの URI にアクセスされてもコンテンツを返すように設定すべきですが、ここでは index のコンテンツだけ返すようにします。

# cat << EOF > /var/www/html/index.html
<html>
<head>
<title>Welcome To The Bad Network< /title>
</head>
<body style="background-color:#000000; color:#ff0000; text-align: center;">
<br/><br/><br/>
<h1>Your traffic is mine!</h1>
</body>
</html>
EOF

さて、この状態で MacBook からブラウザで適当なサイトの閲覧してみるとどうなるでしょうか...

37

見事に Orange Pi 上で動いている HTTP サーバのコンテンツが返りました。この PoisonTap もどきが接続されている限り、どのサイトにアクセスしてもこのコンテンツが返ります。(*HTTPS を除く)

おわりに

本当は Orange Pi 用の U-Boot や Kernel をビルドするところから書こうと思ったのですが、時間が全く足りないので、そのうち別の記事として書きます。(あと、USB Gadget の部分をさらっと書いたものの、本当は g_ether のモジュールをロードする際のパラメータ選定にものすごく苦労したのでその辺りも)

この記事の内容は PoisonTap のネットワーク周りの処理を再現しただけですが、これをベースに何か面白いものが作れないかと考えています。(絶賛アイディア募集中)

良い子のみんなは得体の知れない USB デバイスをつないじゃダメだよ!ラズパイオレパイおじさんとの約束だよ!


pandax381 at 19:53
この記事のURLComments(0)TrackBack(0)
2016年12月15日

Unbound のリトライ処理を追跡してみました

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

この記事は KLab Advent Calendar 2016 の 15 日目の記事です。

こんにちは。大野です。

KLab では最近、ローカルの DNS キャッシュサーバとして Unbound を使うようになりました。

今までは dnscache を利用していたのですが、キャッシュ削除のためにプロセスを再起動しなければならなかったり、特定のレコードのみを削除することができないといった課題や一部の問合せをキャッシュサーバ内部で解決したいといった要求があり Unbound を導入するに至りました。

Unbound を運用していく中でつまずいた点もありましたので、今回この記事で紹介します。

ことの発端

とある案件で AWS を利用した構成の運用を任されていたのですが、アプリケーションのエラーログには以下のような記録が残っており、何らかの理由で RDS で運用している DB サーバの名前解決に失敗し接続できない問題が発生していました。

...
  File "/opt/klab/home/hoge/contents/python/lib/python/site-packages/pymysql/connections.py", line 688, in __init__
    self.connect ()
  File "/opt/klab/home/hoge/contents/python/lib/python/site-packages/pymysql/connections.py", line 937, in connect
    raise exc
sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on 'db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com' ([Errno -2] Name or service not known)")

ここで名前解決のフローに注目してみます。

運用していたシステムでは名前解決にローカルの Unbound を利用していました。 ホストが他のホストに接続する際にはすべてローカルの Unbound を経由して名前解決がなされます。

アプリケーションが動作しているサーバでも同様に名前解決を行い、複数の DB サーバに対して接続を行っています。 DB サーバは RDS を利用して運用しているため、 Unbound での名前解決の問合せ先は 10.0.0.2 を指定していました。 アプリケーションのログからは何らかの理由で Unbound からクライアントに対して SERVFAIL が返されていて、名前解決を諦めていることが分かります。 RDS のホストのレコードの TTL は 5 秒になっていてレコードのキャッシュが切れるたびに 10.0.0.2 に問い合わせるのですが、どうやらその問合せの際に問題が発生していて、 Unbound が SERVFAIL を返すようです。

ということで以降では本題である Unbound を掘り下げていきます。

詳細の追跡

まずは Unbound のログを追っていきます。

名前解決に失敗する以上、名前解決のフローをログに出力するように設定します。

"Unbound documentation" https://www.unbound.net/documentation/unbound.conf.html

    verbosity: 2

Unbound では unbound.conf の verbosity の項目を 2 に設定し、 debug レベル以上のログが出力されるようにすると、以下のように SERVFAIL を返すまでの名前解決のフローが表示されるようになります。

Dec 14 01:20:56 unbound[8826:0] debug: iterator[module 0] operate: extstate:module_state_initial event:module_event_new
Dec 14 01:20:56 unbound[8826:0] info: resolving db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:56 unbound[8826:0] info: processQueryTargets: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:56 unbound[8826:0] info: sending query: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:56 unbound[8826:0] debug: sending to target: <.> 10.0.0.2#53
Dec 14 01:20:56 unbound[8826:0] debug: cache memory msg=17590 rrset=18512 infra=1009 val=0
Dec 14 01:20:56 unbound[8826:0] debug: iterator[module 0] operate: extstate:module_wait_reply event:module_event_noreply
Dec 14 01:20:56 unbound[8826:0] info: iterator operate: query db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:56 unbound[8826:0] info: processQueryTargets: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:56 unbound[8826:0] info: sending query: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:56 unbound[8826:0] debug: sending to target: <.> 10.0.0.2#53
Dec 14 01:20:56 unbound[8826:0] debug: cache memory msg=17590 rrset=18512 infra=1009 val=0
Dec 14 01:20:57 unbound[8826:0] debug: iterator[module 0] operate: extstate:module_wait_reply event:module_event_noreply
Dec 14 01:20:57 unbound[8826:0] info: iterator operate: query db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:57 unbound[8826:0] info: processQueryTargets: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:57 unbound[8826:0] info: sending query: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:20:57 unbound[8826:0] debug: sending to target: <.> 10.0.0.2#53
Dec 14 01:20:57 unbound[8826:0] debug: cache memory msg=17590 rrset=18512 infra=1009 val=0
Dec 14 01:21:00 unbound[8826:0] debug: iterator[module 0] operate: extstate:module_wait_reply event:module_event_noreply
Dec 14 01:21:00 unbound[8826:0] info: iterator operate: query db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:00 unbound[8826:0] info: processQueryTargets: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:00 unbound[8826:0] info: sending query: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:00 unbound[8826:0] debug: sending to target: <.> 10.0.0.2#53
Dec 14 01:21:00 unbound[8826:0] debug: cache memory msg=17590 rrset=18512 infra=1009 val=0
Dec 14 01:21:05 unbound[8826:0] debug: iterator[module 0] operate: extstate:module_wait_reply event:module_event_noreply
Dec 14 01:21:05 unbound[8826:0] info: iterator operate: query db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:05 unbound[8826:0] info: processQueryTargets: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:05 unbound[8826:0] info: sending query: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:05 unbound[8826:0] debug: sending to target: <.> 10.0.0.2#53
Dec 14 01:21:05 unbound[8826:0] debug: cache memory msg=17590 rrset=18512 infra=1009 val=0
Dec 14 01:21:14 unbound[8826:0] debug: iterator[module 0] operate: extstate:module_wait_reply event:module_event_noreply
Dec 14 01:21:14 unbound[8826:0] info: iterator operate: query db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:14 unbound[8826:0] info: processQueryTargets: db001.xxxxxxxxxxxx.xxxxxxxx.rds.amazonaws.com. A IN
Dec 14 01:21:14 unbound[8826:0] debug: configured forward servers failed -- returning SERVFAIL
Dec 14 01:21:14 unbound[8826:0] debug: return error response SERVFAIL
Dec 14 01:21:14 unbound[8826:0] debug: cache memory msg=17590 rrset=18512 infra=1009 val=0

このとき tcpdump でパケットをダンプしてみると、この Unbound は 10 回、 10.0.0.2 に問い合わせているように見えるのですが、 Unbound Timeout Info によると 5 回タイムアウトすると SERVFAIL を返すそうです。

Queries that failed to attain probe status, or if the server is blocked due to timeouts, get a reply with the SERVFAIL error. Also, if the available IP addresses for a domain have been probed for 5 times by a query it is also replied with SERVFAIL. New queries must come in to continue the probing.

Unbound は 1 回の問合せで応答が得られなかった場合、再送処理としてもう一度パケットを送信していて、この 2 回の問合せを 1 回のトライとして、 5 回のトライで応答が得られなかった場合に SERVFAIL を返すようです。 これ以上ログの出力レベルを上げることで各問合せの詳細をさらに出力することもできるのですが、大変に冗長になってしまいログが膨大なサイズになり流れてしまう可能性があります。 そのため、細かい単位での問合せの詳細を追うには tcpdump を使いパケットをダンプしたほうがよさそうです。

というわけで、ここに肝心のエラーが発生したときの tcpdump 結果があるので見ていきます。

0000-00-00 01:23:53.723674 IP (tos 0x0, ttl 64, id 6363, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.58995: [udp sum ok] 28426 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.56 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:23:59.008910 IP (tos 0x0, ttl 64, id 65362, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.61649 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0xf0ae!] 45680+% [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:23:59.011944 IP (tos 0x0, ttl 64, id 6364, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.61649: [udp sum ok] 45680 q: A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.231 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:23:59.019073 IP (tos 0x0, ttl 64, id 65363, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.41853 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0xcc92!] 8929+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:23:59.069337 IP (tos 0x0, ttl 64, id 65365, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.56077 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x900f!] 12244+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
0000-00-00 01:23:59.119624 IP (tos 0x0, ttl 64, id 65367, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.9116 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0xd633!] 39201+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:23:59.177145 IP (tos 0x0, ttl 64, id 65369, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.53906 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x9062!] 12028+% [1au] A? db021.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:23:59.180150 IP (tos 0x0, ttl 64, id 6365, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.53906: [udp sum ok] 12028 q: A? db021.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db021.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.47 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:23:59.219905 IP (tos 0x0, ttl 64, id 65380, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.64595 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x4a5a!] 21571+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
0000-00-00 01:23:59.270182 IP (tos 0x0, ttl 64, id 65386, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.61783 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x0725!] 39540+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:23:59.370381 IP (tos 0x0, ttl 64, id 65391, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.7110 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x2a07!] 21796+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
0000-00-00 01:23:59.470732 IP (tos 0x0, ttl 64, id 65412, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.48543 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x2824!] 44333+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:23:59.671118 IP (tos 0x0, ttl 64, id 65441, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.16698 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x501a!] 2461+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
0000-00-00 01:23:59.871559 IP (tos 0x0, ttl 64, id 65490, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.41675 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x1644!] 55777+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
0000-00-00 01:24:00.271760 IP (tos 0x0, ttl 64, id 53, offset 0, flags [none], proto UDP (17), length 103)
    10.0.2.241.52486 > 10.0.0.2.53: [bad udp cksum 0x1757 -> 0x9c38!] 12722+% [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
0000-00-00 01:24:01.021605 IP (tos 0x0, ttl 64, id 6366, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.56077: [udp sum ok] 12244 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.56 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:24:01.021665 IP (tos 0x0, ttl 64, id 6367, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.9116: [udp sum ok] 39201 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.56 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:24:01.021679 IP (tos 0x0, ttl 64, id 6368, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.61783: [udp sum ok] 39540 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.56 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:24:01.021703 IP (tos 0x0, ttl 64, id 6369, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.64595: [udp sum ok] 21571 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.56 ar: . OPT UDPsize=4096 OK (91)
0000-00-00 01:24:01.021723 IP (tos 0x0, ttl 64, id 6370, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.2.241.7110: [udp sum ok] 21796 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. [5s] A 10.0.7.56 ar: . OPT UDPsize=4096 OK (91)

4 行目から db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. のレコードに対する問合せが始まっています。 以降の行ではすぐに応答結果が得られず、問い合わせの再送処理が始まっていることが分かります。

そして 3 回目の問合せ (Unbound 的には 2 回目の問合せの 1 回目の問合せ) の後に他のレコード db021.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. への問い合わせがあります。 しかし、直後にこの問合せの結果はすぐに得ることができています。 その後も db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. の応答は返されることがなく、 Unbound からパケットが 10 回に渡って送信され、 Unbound が Timeout してから応答が返ってきています。 どうやら、問合せ先の 10.0.0.2 が特定のレコードの問合せに対して、応答が遅れることが原因としてあるようです。

Unbound の最初の問合せから最後の問合せの間隔を見ていきましょう。

1 回目の問合せ (01:23:59.019073)
2 回目の問合せ (01:23:59.069337) + 50 ms
3 回目の問合せ (01:23:59.119624) + 50 ms
4 回目の問合せ (01:23:59.219905) + 100 ms
5 回目の問合せ (01:23:59.270182) + 50 ms
6 回目の問合せ (01:23:59.370381) + 100 ms
7 回目の問合せ (01:23:59.470732) + 100 ms
8 回目の問合せ (01:23:59.671118) + 200 ms
9 回目の問合せ (01:23:59.871559) + 200 ms
10 回目の問合せ (01:24:00.271760) + 400 ms

一定の間隔を空けて、リトライ処理や再送処理が行われているように見えますが、他のレコードに対する問合せが走行したタイミング (3 回目と 4 回目の問合せ間) で問合せの間隔がリセットされているように見えます。

リトライ処理中に他のレコードに対する問合せがない Unbound の処理について再送間隔を見てみましょう (tcpdump の結果は省略します) 。

1 回目の問合せ (09:48:53.980913)
2 回目の問合せ (09:48:54.031078) + 50ms
3 回目の問合せ (09:48:54.081281) + 50ms
4 回目の問合せ (09:48:54.181447) + 100ms
5 回目の問合せ (09:48:54.281630) + 100ms
6 回目の問合せ (09:48:54.481851) + 200ms
7 回目の問合せ (09:48:54.682216) + 200ms
8 回目の問合せ (09:48:55.082701) + 400ms
9 回目の問合せ (09:48:55.483326) + 400ms
10 回目の問合せ (09:48:56.284297) + 800ms

初めは 50ms だった再送間隔がリトライ処理を重ねるにつれ、倍になっているのが分かります。

どうやら、問合せの走行中に他のレコードに対する問合せを同じ DNS サーバに対して行い、応答を受け取ることができると、再送間隔がリセットされている可能性がありそうです。

本当にそういった状況で今回の現象が発生するのか、再現環境を構築して見ていきます。

再現 & 検証

再現環境を用意するためには、特定のレコードに対する問合せが応答を返さずタイムアウトする環境を用意する必要がありそうです。 要件としては特定のクエリの応答を受け取らなければよいだけなので、今回は Netfilter の string モジュールを使い、応答結果のパケットを DROP することで検証を行いました。

以下に検証の手順を残しておきます。

# SRTT を計算する
for i in $(seq 10 200); do dig @127.0.0.1 +tries=1 +retries=1 db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com; dig @127.0.0.1 +tries=1 +retries=1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com; sleep 5s; done

iptables -A INPUT -i eth0 -p udp -s 10.0.0.2/32 -m string --string 'db001' --algo bm --sport 53 -j DROP
tcpdump -tttt -vv -n host 10.0.0.2
time dig +time=20 +tries=1 +retries=1 db001.xxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com& sleep 0.25s; time dig +time=20 +tries=1 +retries=1 db011.xxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com

同様に tcpdump でパケットキャプチャを行ったのですが、結果は以下のようになりました (db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. の応答は省略しています) 。

2016-12-14 05:51:10.578120 IP (tos 0x0, ttl 64, id 13830, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.18425 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0xd18f!] 46972+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
2016-12-14 05:51:10.703403 IP (tos 0x0, ttl 64, id 13858, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.20472 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0x2b20!] 24045+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
2016-12-14 05:51:10.828773 IP (tos 0x0, ttl 64, id 13882, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.57086 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0xfdc3!] 62530+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
2016-12-14 05:51:11.078512 IP (tos 0x0, ttl 64, id 13911, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.48558 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0x0215!] 4162+ [1au] A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
2016-12-14 05:51:11.078934 IP (tos 0x0, ttl 64, id 13912, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.29401 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0xaeaa!] 46977+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
2016-12-14 05:51:11.155562 IP (tos 0x0, ttl 64, id 63125, offset 0, flags [none], proto UDP (17), length 119)
    10.0.0.2.53 > 10.0.65.4.48558: [udp sum ok] 4162 q: A? db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. 1/0/1 db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. A 10.0.3.242 ar: . OPT UDPsize=4096 OK (91)
2016-12-14 05:51:11.329385 IP (tos 0x0, ttl 64, id 13922, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.39718 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0xea21!] 19389+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
2016-12-14 05:51:11.484734 IP (tos 0x0, ttl 64, id 13943, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.58656 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0x18ea!] 56058+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
2016-12-14 05:51:11.640151 IP (tos 0x0, ttl 64, id 13963, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.16822 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0x4f48!] 16391+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
2016-12-14 05:51:11.950629 IP (tos 0x0, ttl 64, id 14002, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.9942 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0x446a!] 28101+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)
2016-12-14 05:51:12.261191 IP (tos 0x0, ttl 64, id 14064, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.50418 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0x510a!] 47880+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1480 OK (75)
2016-12-14 05:51:12.882042 IP (tos 0x0, ttl 64, id 14182, offset 0, flags [none], proto UDP (17), length 103)
    10.0.65.4.42999 > 10.0.0.2.53: [bad udp cksum 0x556a -> 0xb35c!] 32177+ [1au] A? db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com. ar: . OPT UDPsize=1472 OK (75)

再送の間隔についても計算すると以下のようになりました。

1 回目の問合せ (05:51:10.578120)
2 回目の問合せ (05:51:10.703403) + 125ms
3 回目の問合せ (05:51:10.828773) + 125ms
4 回目の問合せ (05:51:11.078934) + 250ms
5 回目の問合せ (05:51:11.329385) + 250ms
6 回目の問合せ (05:51:11.484734) + 125ms
7 回目の問合せ (05:51:11.640151) + 125ms
8 回目の問合せ (05:51:11.950629) + 250ms
9 回目の問合せ (05:51:12.261191) + 250ms
10 回目の問合せ (05:51:12.882042) + 500ms

今回ブログ記事を執筆するにあたり、最新版の Unbound-1.5.10 で再度検証してみたのですが、無事に仮説を実証することができました。 問合せのリトライ処理の走行中に他の問合せが走行して応答が得られると送信間隔がリセットされる確証が得られました。

まとめ & 考察

今回の問題について対応を考える前に、ここまでを簡単にまとめます。

  • DNS サーバの応答が遅れて、Unbound がタイムアウトして SERVFAIL を返す
  • Unbound がタイムアウトして SERVFAIL 返すのは DNS サーバへの 5 回の問合せ ( 10 回のパケットの送信) の間に応答が得られない場合
  • Unbound は間隔を指数的に空けて再度問合せを行うが、同一の DNS サーバに対する他の問合せが成功すると再送間隔がリセットされる
  • レコードが異なっても問合せ先の DNS サーバが同じ以上、 Unbound が再送処理中の問合せ以外の問合せが成功すれば、リトライ間隔をリセットするのは理にかなった実装です。 しかし、このままだと同じ DNS サーバに対する他のレコードの問合せが増加するほど、この現象が発生しやすくなるため、場合によってはサーバが応答を返すのに Unbound が待つ時間は 500 ms 程度になってしまう可能性があります。

    問合せから応答の SERVFAIL が返される時間があまりにも短いと、権威サーバから SERVFAIL が返って来て名前解決に失敗しているのか、 Unbound がタイムアウトしているのか判断がつかず、問題の切り分けの際にも困ります。

    よって、 Unbound のタイムアウトを安定して延ばすため、再送間隔が他の問合せの影響を受けないような対応を考えたいと思います。

    対応策の検討

    対応としては今回、 forward-zone を分けることで対応できることを確認しました。

    #   Forward-Zone section
    forward-zone:
      name: "db001.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com"
      forward-addr: 10.0.0.2
    
    forward-zone:
      name: "db011.xxxxxxxxxxxx.xxxxxxxxx.rds.amazonaws.com"
      forward-addr: 10.0.0.2
    

    内部の RTT や再送間隔を管理している構造体が独立して分かれるため、それぞれのレコードに対する問合せが影響することはありません。 しかし、あくまで暫定的な対応となるためおすすめはできません。 そのため、ソースコードのパッチ作成も考えたのですが、他のキャッシュサーバがどのようにリトライ処理を行っているのか気になるところなので、そちらを調査してからソースコードのパッチを作成することを検討しています。

    以上、長くなりましたが Unbound のリトライ処理時の挙動について調査したお話でした。

    最後になりますが、この問題を調査するにあたり、 @pandax381 さんに SRTT の概念や Netfilter の string モジュールについてアドバイスいただきました。ありがとうございます!

    明日の KLab Advent Calendar 2016jukey17 さんです。


    klab_gijutsu2 at 18:00
    この記事のURLComments(0)TrackBack(0)
    2016年11月21日

    Keepalived 1.3.0 リリース!ヘルスチェック強化パッチが本家にマージされました

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

    @pandax381です。本日、Keepalived の新バージョン 1.3.0 がリリースされましたが、みなさんバージョンアップはお済みですか?

    2016-11-20 | Release 1.3.0
    New MAJOR release with stabilization fixes. Support to DBus. Conf extensions. Parser error log. Security extensions to run scripts more secure. Refer to ChangeLog for more infos.

    (開発 ML へのアナウンスでは 2.0.0 が近々リリースされることも予告されています)

    this quick email to announce new major keepalived release. We are planing with Quentin to push a new release soon as 2.0.0 release. This one fix and extend previous parts. It also came with a Security fix for those making extensive use of scripts.

    今回リリースされた 1.3.0 での大きな変化は DBus 対応とスクリプト実行にセキュリティ機構が導入された点ですが、僕が投げたパッチもひっそりとマージされているのでアリバイ作り*1のために紹介します。

    *1: インフラ部門で働くCプログラマの話 参照

    ヘルスチェック強化パッチ

    Keepalived にはリアルサーバを監視するヘルスチェック機能が備わっていますが、標準で対応しているプロトコルが「TCP」「HTTP(S)」「SMTP」のみと限定的です。HTTP と SMTP 以外は TCP の疎通確認以上のことをやりたければ、自前でヘルスチェック用のスクリプトを用意しなければなりません。

    自前のヘルスチェックスクリプトを実行する MISC_CHECK は微妙な問題を抱えていたため、KLab では Keepalived のヘルスチェックを強化するパッチを開発・公開していました。(微妙な問題については後述します)

    このパッチは、ヘルスチェックの対応プロトコルに「FTP」「DNS」「SSL」を追加するものです。記事の中では「まだDSASの本番環境には適用していません」となっていますが、その後まもなく本番環境にも導入され、現在も絶賛稼働中です。

    独自パッチの宿命とも言えるのが、本家のバージョンアップへの追従です。公開しているパッチは 10 年も前に書いたものなので、そのままでは現在のバージョンに適用できませんが、社内では必要に迫られるたびにパッチを更新し続けています。それでも、新バージョンがリリースされて更新しようとした際にパッチ適用がネックになってしまうのはちょっとツライです。こうした独自パッチの宿命から解放されるには、独自パッチを卒業して本家にマージしてもらう他ありません。

    結論から先に書くと、タイトルにもある通り独自パッチを卒業してめでたく本家にマージされ 1.3.0 から標準機能になりました。

    これがヘルスチェックを強化するパッチのプルリクエストです。オリジナルのパッチは、FTP・DNS・SSL に対応させるものでしたが、現在 DSAS で使っているのは DNS だけということもあり、標準のヘルスチェック機能に DNS ヘルスチェック(DNS_CHECK)を追加する内容になっています。

    DNS_CHECK の書式は以下の通りです。

        # one entry for each realserver
        real_server <IPADDR> <PORT>
        {
               # DNS healthchecker
               DNS_CHECK
               {
                   # ======== generic connection options
                   # Optional IP address to connect to.
                   # The default is the realserver IP
                   connect_ip <IP ADDRESS>
                   # Optional port to connect to
                   # The default is the realserver port
                   connect_port <PORT>
                   # Optional interface to use to
                   # originate the connection
                   bindto <IP ADDRESS>
                   # Optional source port to
                   # originate the connection from
                   bind_port <PORT>
                   # Optional connection timeout in seconds.
                   # The default is 5 seconds
                   connect_timeout <INTEGER>
                   # Optional fwmark to mark all outgoing
                   # checker packets with
                   fwmark <INTEGER>
    
                   # Number of times to retry a failed check
                   # The default is 3 times.
                   retry <INTEGER>
                   # DNS query type
                   #   A | NS | CNAME | SOA | MX | TXT | AAAA
                   # The default is SOA
                   type <STRING>
                   # Domain name to use for the DNS query
                   # The default is . (dot)
                   name <STRING>
               }
        }
    

    いろいろオプションがありますが、最低限「type」と「name」を適切に設定してあげれば動きます。

        realserver 192.0.2.100 53 {
               DNS_CHECK {
                   type A
                   name www.klab.com
               }
        }
    

    ヘルスチェックの成功可否は、ANSWER SECTION に1件以上の回答がある応答を得られるかどうかで判断しています。したがって、レスポンスパケットを受け取っても ANSWER SECTION が空の場合にはそのパケットは無視します。実用性があるかどうかは別として、やろうと思えば DNS のレコードの登録状況によってヘルスチェックの結果を制御することも出来ます。

    ついでにバグも退治しました

    先に「自前のヘルスチェックスクリプトを実行する MISC_CHECK は微妙な問題を抱えていた」と書きましたが、これまでのバージョンの keepalived にはMISC_CHECK から実行されるヘルスチェック用のスクリプトが刺さるとプロセスが増殖してしまうというバグがありました。

    例えば、以下のような設定で MISC_CHECK を実行すると簡単に再現できます。

    MISC_CHECK {
        misc_path "/bin/sleep 3600"
        misc_timeout 10 
    }
    

    MISC_CHECK は、実行したスクリプトが misc_timeout を経過しても終了しない場合にはシグナルを送信して強制的に終了させる作りになっています。しかし、このシグナル送信の処理に問題があり、本来は misc_timeout が経過した後に強制終了させられるはずのプロセスが残り続け、順次新しいプロセスが生成されて結果的にプロセスが大量に増殖してしいます。

    UID   PID  PPID  PGID   SID COMMAND
      0 41010     1 41010 41010 /sbin/keepalived
      0 41013 41010 41010 41010  \_ /sbin/keepalived
      0 41361 41013 41010 41010  |   \_ /sbin/keepalived
      0 41362 41361 41010 41010  |   |   \_ sh -c /bin/sleep 3600
      0 41363 41362 41010 41010  |   |       \_ /bin/sleep 3600
      0 41364 41013 41010 41010  |   \_ /sbin/keepalived
      0 41365 41364 41010 41010  |   |   \_ sh -c /bin/sleep 3600
      0 41366 41365 41010 41010  |   |       \_ /bin/sleep 3600
      0 41367 41013 41010 41010  |   \_ /sbin/keepalived
      0 41368 41367 41010 41010  |       \_ sh -c /bin/sleep 3600
      0 41369 41368 41010 41010  |           \_ /bin/sleep 3600
      0 41014 41010 41010 41010  \_ /sbin/keepalived
      0 41019     1 41010 41010 sh -c /bin/sleep 3600
      0 41020 41019 41010 41010  \_ /bin/sleep 3600
      0 41025     1 41010 41010 sh -c /bin/sleep 3600
      0 41026 41025 41010 41010  \_ /bin/sleep 3600
      0 41031     1 41010 41010 sh -c /bin/sleep 3600
      0 41032 41031 41010 41010  \_ /bin/sleep 3600
    

    MISC_CHECK はヘルスチェック用のスクリプトを実行するために fork(2) してから system(3) を実行しているため、シグナルを送信するプロセスから見て終了させたいプロセスは「ひ孫」の関係にあります。もともとのコードでは、子プロセスに対してのみシグナルを送信していた*2ため、子プロセスのみが終了して孫プロセスとひ孫プロセスが残り続けてしまうというバグでした。

    上記はこのバグを修正するためのプルリクエストです。MISC_CHECK が fork(2) した段階で、setpgid(2) を実行してプロセスグループを分離するようにしました。signal(2) はプロセスグループを指定することで、そのプロセスグループに属する全てのプロセスに対してシグナルを送ることができるので、子プロセスからひ孫プロセスまで全てのプロセスにシグナルを送信するように修正しました。

    この修正もマージされていますので、1.3.0 からは安心して MISC_CHECK を利用できるようになりました。

    *2: 実際には更にバグがあり、肝心の子プロセスも意図せずにシグナル(SIGTERM)を無視してしまう状態になっていて、最終手段として実装されていた強制終了のシグナル(SIGKILL)を受けて殺されているという微妙な状態でしたが、これも修正しました

    苦労話など

    既存のパッチを本家に取り込んでもらった風に書きましたが、実際にはスクラッチで書き直したので既存のコードはほとんど残っていません。

    これには、keepalived が備えているヘルスチェックのフレームワークが TCP を前提としていて UDP のことを考慮しておらず、オリジナルのパッチではソケット周りの処理を全て自前で書いていたという理由があります。そのため、標準のヘルスチェックのコードと比べるとお作法的にお行儀が悪そうな状態で、そのままプルリクを投げたら嫌がられるかもという状態でした。とは言え、フレームワーク側が UDP を考慮していないため他にうまくやる方法もなく、あれこれ悩んだ末に keepalived 本体に手をつけてフレームワークの作りを修正することにしました。(ヘルスチェックの各機能はモジュールのような位置付けで機能追加であれば気軽にできるのですが、本体側の修正となると心理的なハードルがだいぶ上がります)

    このコミットで、TCP しか考慮していなかったフレームワークを UDP にも対応させました。フレームワークを利用している既存のコードを変更しないように、インライン関数でラッパーを作成して互換性を維持するようにしています。この修正によって UDP ベースのヘルスチェッカーが作りやすくなっているので、もしかしたら対応プロトコルが増えるかもしれません。

    あと、一番苦労したのはコード書くよりもプルリクエストのメッセージを書くことでした。なにより僕が英語が絶望的に出来ないというのもありますが、「それ MISC_CHECK でやって」と言われてしまうと終了してしまうので、標準のヘルスチェッカーに組み込んでもらいたいという熱い思いを伝えるのに注力しました。精度が上がる前の Google 翻訳に泣かされつつも、結果的に好意的に受け入れてもらえて本当に良かったです。

    恐らく、これが自分のコードが取り込まれた中で一番有名で大きなプロダクトだと思うので、素直に嬉しいです。需要があるかどうかわかりませんが、DNSヘルスチェック機能を是非使ってみてください!


    pandax381 at 20:29
    この記事のURLComments(0)TrackBack(0)
    2016年11月07日

    MaBeee をダミー電池で利用する

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

    MaBeee のこと

    MaBeee が気に入っています。
    MaBeee は単四電池を装着することで無線制御可能な単三電池として振る舞うユニークな BLE(Bluetooth Low Energy)デバイスです。昨年(2015年)末に Makuake に登場して注目を集め今年8月に一般販売が始まりました。給電のオンオフや出力電圧レベルをスマホやタブレットから簡単にコントロールできるためホビー系の機器などで活用している方も多いことでしょう。。


    http://mabeee.mobi/

    手軽で実用性のある MaBeee ですが、パワーの弱い単四電池での稼働が前提であることを残念に思うことがありました。もちろん、そもそも MaBeee はこのようなスタイルを実現するためにこそ緻密にデザインされた製品なのであくまでも一利用者のわがままではありますが、一方で、より容量の大きいバッテリーや AC 電源で腰を据えて使用できればこの製品の用途がさらに拡がると考えました。そこで役に立ったのがダミー電池です。

    ダミー電池?

    ダミー電池は乾電池と同じサイズ・形状をしており、内部でプラス極とマイナス極が直結しているタイプが基本です。当然ながら充電器への装着は厳禁。電圧調整等の目的に利用され、その方面ではメジャーなアイテムであることを知りました。自作する向きもあるようです。

    http://store.shopping.yahoo.co.jp/ http://eleshop.jp/ http://www.amazon.co.jp/

    工作向けのダミー電池と MaBeee

    上のように両極をショートさせ全体をシールドしているもの以外に、自分で配線したり回路を組み込んだりすることのできるもタイプもあります。これを MaBeee への給電用に使えば良さそうです。ただ、このタイプは種類が少なく一般のメーカーやパーツ屋さんの扱っているものの中に単四型はまず見かけません。


    http://www.marutsu.co.jp/

    単四サイズのものを自作する選択もありますが、ただでさえ精密でデリケートな MaBeee を何かの拍子に傷めるリスクを考えるとあまり気が進まず、ネットを探して以下の製品に行き着ました。工作機械で加工を施したプラスティック製品や機械本体を製造・販売している「工房モコ」さんによる貴重なダミー電池のラインナップです。

    「工房モコ」こんな物いかが? - www.geocities.jp/moco7jp


    ●ハイパーダミー電池(高機能にしていただくタイプ)

    ハイパーダミー電池はユーザーが内部に回路を仕込んで高機能にしていただくタイプです。
    抵抗を入れたり、ダイオードを入れたり、回路をいれたり、機器を外部電源で使いたい時、外部電源との橋渡しに、・・・ どんな工夫ができるかな。
    発生した損害は自己責任でお願いします。

    電池の種類は、次の6種類を用意しています。
     単一 単二 単三 単四 単五 CR123A

    内部のH形のブリッジ部は、不要ならニッパーでカットすれば取り除けます。
    4ケ所のくびれた所をカットして下さい。御希望によりカットして出荷します。

    さっそく単四型のものを購入しました。シンプルな作りながら扱いやすいように工夫されており仕上げも丁寧です。

    単四型の現物 給電用のコードを取り付ける MaBeee へ装着し電池ボックスへ

    ともに「バーチャルな乾電池」である MaBeee とダミー電池の取り合わせ。何やら連想が膨らみます。


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