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 
2016年10月24日

ISUCON6 で優勝しました

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

@methane です。タイトルの通り、 ISUCON でとうとう優勝してきました。

チームメンバーは、(予選と同じく) @kizkoh (インフラ担当), @mecha_g3 (アプリ担当) でした。

私は予選のときはガッツリとアプリを書いていたのですが、本戦では netstat -tn (←老害), top, dstat -ai, sudo perf top などをみつつ指示をだしたり、方針を決めたり、完全に未経験だった node.js & react.js 対策をしたりが主な仕事で、あとは序盤のインフラのタスクが大量にあるときに MySQL を docker から外して基本的なチューニングを入れたり Go を100行程度書いただけです。

結果的には優勝できましたが、メンバーの2人がよく準備し本番でも実力を発揮してくれたのに対して 僕の戦略ミスで中盤から全くスコアを上げられなかったので、最後までヒヤヒヤしていました。

ということで、 nginx や Go でやったことはメンバーの二人が別に記事を書いてくれると思うので、 私の目線で考えたこと、自分でやったことや方針を決めて指示したこと、その考察を書いていきたいと思います。

お題と初期構成について

お題は、リアルタイムに他人の書いた線が他のユーザーに見えるようなお絵かき掲示板でした。

構成が特徴的で、

  • フロントにいるのが通常の Web サーバーではなくて、 react.js を使ったアプリを含む node.js サーバー (以下react)
  • そこからリバースプロキシされる形でアプリが存在し、それには各言語の実装がある (以下app)
  • app のバックエンドとしては MySQL がいる
  • react, app, MySQL がすべて docker になっていて、 docker-compose で動いている

というものでした。

ベンチマーカーは react の Web アプリを想定しているようで、 bundle.js というファイル(react を 全く知りませんが、たぶんサーバーサイドとクライアントサイドで共通なファイル)が1バイトでも変化したら FAIL していたので、かなり厳密にチェックしていたのだと思います。

Docker は @kizkoh が判るということでしたが、 node.js & react が全く分からないのがとにかくつらい。

「react が判らないと勝てない問題なら多言語用意する意味ないし、きっと簡単にここはボトルネックじゃなくなって、 1本線を引いたらそれを何百人に転送して何百点もバーンと入るような問題だ。それならGoは得意だし、 ネットワークで協力プレイするサーバーをGoで開発してる僕らも得意だ。実力出せば勝てる」と メンバーと自分に言い聞かせてスタートしました。

序盤戦 (~14:00 くらい)

初回ベンチを終え、 @kizkoh が nginx の準備、 @mecha_g3 がアプリ開発&チューニングの準備を しているあいだに、フロントの react の server.jsx というファイルと Go アプリのルーティング部分を見て、 フロントに追加する nginx のリバースプロキシ設定 (/api/ は直接 app にリバースプロキシして、それ以外は react にリバースプロキスするなど) をまとめたり、 @mecha_g3 に room のオンメモリ化を始めるように指示したりしました。

このときはまだ react がボトルネックでなくなるという楽観的な前提だったので、まずは @mecha_g3 に線を書くところとそれを SSEで配信するところでオンメモリ化を進めてもらいつつ、 react を docker から引きずり出したり、共有していた開発環境に MySQL をセットアップしてインフラ再構築中でも app の動作確認ができる状況を用意してから、腹をくくって react の server.jsx というファイルを読み始めました。 (server.jsxのソースコード)

そうすると、下の方で renderString() しているところは、ここからHTMLはブラウザで見たソースがどうやって生成されるのか 全くわからない上に、HTMLの方にはたぶんクライアントとサーバーの整合性を確認するためのものと思われるチェックサムがあって、 何をしたら fail するのか分からないできるだけ触りたくない部分に見えた一方、上の方にはいかにもここをチューニングしてくれ という雰囲気で /img/:id というパスの処理をしているコードが目に止まりました。ここもどうやってXMLができるのか全くわからない ものの、レスポンスヘッダを設定しているところから生の svg を生成していることが一目瞭然でした。

/img/:id: の部分は renderString() ではなくて renderToStaticMarkup() という関数を使っていたので、この関数のリファレンスを 探して見てみます。

https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup

Similar to renderToString, except this doesn't create extra DOM attributes such as data-reactid, that React uses internally. This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save lots of bytes.

要するに react の魔術がかかってないプレーンなデータを作るための関数のようです。これは出題者からのヒントだろう。XML生成とか いかにも重そうだし、ここをキャッシュするとか 304 not modified を返せば一気に react は問題にならなくなるんだろう、という希望的予測を立てました。

なお、競技終了後にちょっとディレクトリツリーに目を通してみたら、すぐに components/ というディレクトリ配下にコンポーネント(テンプレート+コード)があって xml を生成している Canvas というコンポーネントも判りました。このディレクトリを見ていればその後の戦略ももっと変わったかもしれません。 ちょっと検索すれば、同じディレクトリにある Room というコンポーネントで同じ Canvas コンポーネントを利用してるのが判るので、 /img/:id だけ 高速化しても react 全体が軽くはならないだろう、 /room/:id も対策が必要だという前提で進められていたはずです。

緊張状態では知らない技術は10倍怖いし、ベンチマーカーが何をチェックしてるかわからないのも10倍怖くて、計100倍の怖さだもんね。仕方ないね。

中盤戦 (~16:30 くらい)

react から ssl を外してフロントを nginx にするとか docker 外しとか一通り終わったので、 @kizkoh には「やりたいことが残ってるならやってていいけど、 無いならムリに何かしようと頑張るより終盤に向けて休憩取っておいて」という指示を出し、 app が返す json とそれを react が変換してブラウザに返す svg を見比べながら、 その svg を Go の内部のデータ構造から直接生成するような fmt.Printf の塊を作ります。 (ソースコード)

@mecha_g3 のオンメモリ化が終わってからつなぎ込み、svgはあっさりベンチをパス。ただGoに持っていってもsvg生成は結構重い(想定内)ので、 @mecha_g3 にキャッシュを依頼。 その時ちょっと面白がって、 stroke が書かれたときのキャッシュの更新でひと工夫。 svg の更新は stroke の追記オンリーなので、全部再生成するのではなくて、単に追加された stroke を最後の </svg> タグの手前に挿入するだけにしてもらいました。スコアにどれくらい影響があったのかはわかりませんが、これが簡単にできたのはGoらしい部分でした。

とはいえ、この工夫が強かったのかと言うと、試してみないとわからないものの、svgを静的ファイルに吐き出してnginxで返すことで参照性能上げたほうが、更新性能を上げるよりスコア上がった可能性が大きいと思います。

さらに、可能性としては、react側の /room/:id を弄ることができれば、 react から静的ファイルを読み込んで埋め込むのは簡単だったはずです。 静的ファイルを使う場合分散構成がネックになりますが、同じ部屋へのアクセスが同じサーバーに分散されるような nginx の設定は @kizkoh ならすぐにやってくれたはずです。

終盤戦

序盤に考えていたことは実現できたものの、そのときに妄想していたレベルの性能は全然出ていません。

インフラ側でも問題が色々でていましたし、なにより react のCPU使用率が支配的で app の性能を全く活かせていません。

今から react を調べてハマらずにチューニングするのはバクチ過ぎて、暫定ダントツトップの状態から挑戦するのはナシです。 ここまで1台で動かしてきたのを、MySQL を isu2 に (これは僕の指示でしたが、データセットが初期状態にもどって軽くなる以外は無意味でした…)、 react を isu3-5 で動かす分散構成に移行します。

他にも too many open files とか TIME_WAIT とかが問題になっていたので、 nofiles や tcp_tw_reuse などを設定するように指示しつつ、 react が HTTP keep-alive を頑なに拒否していたのを直していきます。

まず、 nginx -> react のリバースプロキシ部分で全く keep-alive されないのは、 express.js 部分でレスポンスヘッダーを追加するAPIを調べて res.append('Connection', 'keep-alive') を追加したら直りました。 一方 react -> app の方が問題で、ライブラリの依存関係を潜っていった結果、 bitinn/node-fetch の中の次のようなコードを削除するというちょっと強引な方法で解消しました。

// https://github.com/bitinn/node-fetch/blob/master/index.js#L79-L81
		if (!headers.has('connection') && !options.agent) {
			headers.set('connection', 'close');
		}

こういった修正を、再起動試験の合間を縫ってやっていたのですが、終盤はタイムリミットが近づくにつれ何もしなくてもどんどんスコアが下がる状況に陥っていて、 ベストスコアの 85k 点は最後の修正が入ってない状態でした。

考察

終盤にスコアが伸び悩んだ(むしろ落ちた)のが帯域がネックだと予想している点について補足しておきます。 今回使ったインスタンスは2コアの D2v2 で、ベンチマーカーがアクセスするインスタンスは1台のみ、次のグラフは終盤のベンチにおけるその1台の帯域 (bytes/sec) です。

帯域グラフ

2コアのインスタンスで100MB/secを超えてるのはすごいですね。グラフがガタガタなのは、多分ベンチマーカーが波状攻撃をしかけてきたからだと思います。

次は、各パスへのアクセス数です。集計しているのはベンチマーカーからのアクセスのみで、 react -> app へのアクセスが含まれていません。(これも重要な情報なので、 直接アクセスさせていたのは僕の判断ミスでした)

Request by count
11106 GET /img/*
178 GET /
112 GET /bundle.js
112 GET /css/rc-color-picker.css
112 GET /css/sanitize.css
49 GET /rooms/*
46 POST /api/strokes/rooms/*
41 GET /api/rooms
20 GET /api/stream/rooms/*
3 POST /api/rooms
2 GET //admin/config.php
1 GET /api/rooms/1088

次は、同じくベンチマーカーに返していたレスポンスの、各パスごとの合計/平均バイト数です。

Request by out bytes
2511686449 14110598 GET /
272792736 6653481 GET /api/rooms
266967906 24038 GET /img/*
36283072 323956 GET /bundle.js
9725553 198480 GET /rooms/*
764377 38218 GET /api/stream/rooms/*
340144 3037 GET /css/sanitize.css
303520 2710 GET /css/rc-color-picker.css
184982 4021 POST /api/strokes/rooms/*
75910 75910 GET /api/rooms/1088
1017 339 POST /api/rooms
490 245 GET //admin/config.php

一番帯域を使っている / ですが、これは react から外せていないパスで、この大きいレスポンスが react (別サーバー) -> nginx -> ベンチマーカーに流れているので、 nginx を置いてるマシンの in と out の帯域が同じくらいになっています。

2番めの /api/rooms ですが、これは直接クライアントから呼ばれるのが41回、(多分)/rooms/* を処理している react からアクセスされるのが他に 49 回あるので、 実際に使っている帯域はこの倍以上あったはずで、ここが帯域に引きずられてタイムアウトしやすくなったのがスコアが安定しなかった理由だと思います。

まだ見れてないですが、 / に帯域を減らせるような仕込みがあったのかもしれません。また、それを攻略できなかったとしても、 nginx で全体に ratelimit をかけて 帯域を食うパスを遅くする代わりにそれ以外のパスでタイムアウトになる数を減らすことで、もっとクライアントの並列度を稼いでスコアを取れたかもしれません。

せっかくこれだけの情報を取れていながら、終盤はテンパっていて全然考察できていませんでした。再起動試験中に冷静に最後のもうひと稼ぎを考えないといけませんね。

感想

例年は自分が一番アプリを書ける、かつインフラのチューニングも一番判るという状態で、新卒メンバー2人にタスクを振るという戦い方をしていました。

今年は、 @mecha_g3 を自分と同じくらいアプリが書けると信頼して大まかな方針だけ共有するだけで済んだし、 @kizkoh も僕が全然キャッチアップできていない 近年のミドルウェアを予習してきてくれておまかせできた上に、アプリチューニングが入る前からインフラだけでスコアを伸ばしてくれたおかげで、 手を動かさずに目と頭を使う戦い方ができました。

まだまだ僕の力不足で出題側の想定解法にはたどり着けませんでしたが、3人で強いチームがISUCONで強いチームだということを強く実感でき、 スポコンマンガの登場人物になったかのような快感・達成感を得ることができました。 このチームであと10回くらい勝って賞金で新車のWRXを買いたいです。

終わりになりますが、運営・協賛の各位、特に毎年増え続ける参加者をあたたかみのある運用で運営してくださっている櫛井さんと、 毎年上がり続ける出題レベルのプレッシャーに見事に応える良問と安定ベンチマークシステムを作ってくださった出題チームに感謝いたします。


songofacandy at 19:09
この記事のURLComments(0)TrackBack(0)ISUCON 

KLab 勉強会 #7 -KLabインフラの今- を開催 & 資料公開しました!

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

こんにちは、インフラエンジニアの大野です。

10 月 18 日、 KLab 本社にて KLab 勉強会 #7 -KLabインフラの今- を開催しました。

DSAS開発者の部屋:KLab 勉強会 #7 開催のお知らせ
KLab 勉強会 #7 -KLabインフラの今- - connpass

4 年ぶりの開催となったにもかかわらず、予想を上回る多数の方にご来場いただきました。
ご来場いただいたみなさま、誠にありがとうございました。

当日の発表内容は現在取り組み中のコンテナ技術を取り入れた次世代 DSAS の構想、 DSAS で培った運用とクラウドサービスを活用した独自要件のインフラ運用、そして KLab の OSS や研究開発といったブログでは記事を執筆していない内容のものばかりで、懇親会も含め大変好評でした。

当日の発表資料をまとめていますのでぜひご覧ください。

発表資料

DSAS Next (by 勝見)

KLAWS on GCP (by 千葉)

インフラ部門で働くCプログラマの話 (by 山本)

発表内容の構想中や取り組み中の技術についてはまとまり次第、随時ブログ記事とすることを予定しています。

KLab 勉強会は 4 年ぶりの開催となりましたが、今後は頻度を上げて様々なテーマに展開し勉強会を開催していく予定です。 次回の勉強会の日程が決まりましたら、こちらにてアナウンスしたいと思います。

今後もよろしくお願いします!


klab_gijutsu2 at 08:30
この記事のURLComments(0)TrackBack(0)
2016年10月05日

KLab 勉強会 #7 開催のお知らせ

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

新卒入社 3 年目の大野です。

第7回 KLab 勉強会のお知らせです。

前回が 2012 年の開催で 4 年も空いてしまっていますが、第 7 回ということで開催します。

今回はインフラにフォーカスを当てた内容のセッションを 3 本ご用意しています。

数年前より KLab ではオンプレ以外にもクラウドを活用したサービスの運用を行っています。 オンプレとクラウド両方の観点からそれぞれの特徴を生かしたサービス運用とそれを支える技術について直近の課題を交え KLab の今のインフラについてご紹介します。

また、ささやかですが懇親会もご用意しております。みなさまのご参加をお待ちしております!

開催要項

日時
2016/10/18 (火) 19:20-21:50 (19:00 受付開始/懇親会有)
場所
KLab株式会社 ミーティングルームKLM
東京都港区六本木 6-10-1 六本木ヒルズ森タワー22F
(アクセス方法)
参加費
無料
人数
50名

参加方法

connpass の KLab勉強会 #7 から参加登録してください。


klab_gijutsu2 at 13:55
この記事のURLComments(0)TrackBack(0)

コンテナによるLVS-Web構成

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

はじめに

KLabさんの協力会社として一緒にお仕事をさせて頂いておりますクラスターコンピューティングと申します。今回はコンテナを利用したLVS-Web構成の構築を試してみました。

従来の物理サーバを仮想マシンやコンテナで置き換え、1つの物理システム上に複数のシステムを構築ししたり、必要に応じて動的にシステムを構築するなどのことは最近とはいわずよくおこなわれています。コンテナはそのフットワークの軽さからこれらの用途にとくに有効です。

コンテナはNamespaceやcgroupなどの技術を利用して独立したリソースをコンテナ毎に確保することにより仮想化環境を提供する一方、カーネル自体はホストのカーネルをホストの上のすべてのコンテナで共用しています。今回は、ipvsやiptablesなどカーネル空間の機能がコンテナ内でも利用できるのかの確認とともに、それらの機能を利用したコンテナによるシステム構成について試してみました。

そもそもコンテナ上てipvsやiptablesが使えるの?

結論から言えばコンテナ上でも通常のLinux Box上と同じ感覚で利用できます。コンテナ毎、正確言えばネットワークネームスペース毎に独立したIPVSのバランステーブルやiptablesのルーティングテーブルが用意されるので同じホスト上で複数のコンテナを起動した場合でも問題ありません。ただ、多少注意が必要な点があります。

必要なモジュールはホスト上で事前に読み込んで置く必要がある

ipvsやiptablesの利用に必要なカーネルモジュールはコンテナ上利用する前にホスト側で読み込まれている必要があります。root権限でコンテナを実行していたとしてもコンテナ上で各コマンドを実行したときに自動で読み込んではくれません。

ipvsの利用にはコンテナ上でもroot権限が必要。iptablesは非特権コンテナでも利用できる

root権限でコンテナを実行している場合、ipvsもiptablesも利用に問題はありません。非特権コンテナ、つまり一般ユーザ権限でコンテナを実行している場合、iptablesは利用できますが、ipvsについては無理なようです。ただし、非特権コンテナでiptablesを利用する場合でも先ほどの注意点にあるように、事前にホスト側でモジュールを読み込んでおく必要があり、これには当然root権限が必要になります

iptablesのLOGターゲットに注意

細かいことですが、コンテナ上でLOGターゲットを利用してIptablesのログを記録しようとしても上手くいきません。こはiptablesというよりも一般的なカーネル空間のログの問題のようです。今のところカーネル空間のログをそれぞれのコンテナに関連するログごとに分けて記録するような仕組みがありません。その関係上、コンテナ上てiptablesを利用してLOGターゲットでカーネル空間にログを出力することは今のところ意図的にできないようになっているようです。ログを記録するには、代わりにユーザ空間にログを出力するULOGやNFLOGターゲットを利用する必要があります。

LVS-Web構成

コンテナを利用して複数のLVS-Web構成を1つの物理システム上に構成してみます。LVSコンテナは通常の特権コンテナ、Webのコンテナには非特権コンテナを利用します。ちょっとした工夫としてVLANを利用してLVS-Webホスト間のネットワークをコンテナのグループごとに独立させます。

非特権コンテナの利用

Webコンテナに非特権コンテナを利用することによって、Web管理ユーザにホスト上でroot権限をあたえることなく、Webコンテナ内でのroot権限を与えることができます。Web管理ユーザはコンテナ内ではアプリケーションのインストールや各種設定の変更が可能になります。

VLANの利用

各コンテナは仮想インターフェースおよびブリッジを介して物理インターフェースに接続されていますが間にVLANを挟むことによってコンテナのネットワークをVLAN毎に独立させます。ネットワークを分離することによって安全性や利便性が高くなります。例えば、案件ごとにVLANを割り当てることによって、各案件のコンテナ間での通信が不可能になります。またVLAN内でのアドレス空間の設定は自由にすることができ他の案件と同じアドレスを利用することもできます。例えば、コンテナの役割でアドレスを固定してそのコンテナをそれぞれの案件にデプロイして利用することいったことも可能です。

NAT構成

lvs_r
図1

ちょっと細かいですが図1のように、2台のLVSサーバ、2台のWebサーバからなる物理システム上に2系統のLVS2台-Web2台のNAT構成を構築します。LVSコンテナは冗長化を実行し、それぞれLVSの上流側(外側)、および下流側(Webサーバ側)のゲートウェイアドレスをVRRPで管理します。それぞれ系統のLVS-Webコンテナ間のネットワークはVLANにより分離されており、各コンテナのアドレスは同じものを利用しています。

各ホストに作成したVLANインターフェースにはブリッジが接続され、そこにそれぞれの系統のコンテナが接続されています。ここでは1つのホスト上に1つの系統のコンテナは1つしかありませんが、ブリッジにコンテナを複数接続するとによって同一ホスト上で同じ系統のコンテナを複数利用することは可能です。

点線で囲まれた枠内がコンテナで分離されている範囲です。ブリッジやVLANインターフェースはその外側にありますので、非特権コンテナによりコンテナ内の特権があたえられていたとしてもWebコンテナからはVLANの外側にアクセスすることはできません。

LVSの上流側ではLVSコンテナは同じブリッジに接続されています。LVSホストのネットワークもここに接続されています。

下流側のLVS-Webホスト間のネットワーク(図1のuntagged)もコンテナのネットワークからは分離されていますが、ホストのroot権限があればブリッジ(図1でのlxcbr0など)を介してコンテナのネットワークにアクセスすることは可能です。

ここではとくに記述していませんが、LVS-Webホスト間の物理ネットワーク機器(スイッチなど)は各VLANパケットを通すようにしておきます。

実際にコンテナで構成してみる。

WebホストではUbuntu、LVSホストではDebianを利用しています。コンテナの実装としてはLXCを利用します。それぞれのホストにLXCをパッケージを利用してインストールしています。UbuntuであればLXCをインストール時点で非特権コンテナを実行する準備が整ってるはずです。各アドレスは図のとおりに設定します。非特権コンテナを実行するユーザはVLAN10側がuser10、VLAN20側がuser20とします。

構成の手順としては、まずホスト側で必要なブリッジやVLANインターフェースを用意します。そのあとコンテナを起動してブリッジに接続します。全てのホストでコンテナを起動しコンテナのクラスタが構成されたら、あとは通常のLinux boxの場合と同様に各コンテナ内でのルーティングの設定や、アプリケーションの起動などを実行するだけです。

各種ブリッジやVLANインターフェースの準備、コンテナの起動などすべては手動で行いますが、基本的にLXCとLinuxの基本コマンドしか利用しません。コンテナのクラスタの構成は比較的簡単に可能です。

コンテナクラスタの構成

Webホスト側の準備

まずVLAN10系統のセットアップを行います。コンテナ接続用のブリッジおよびVLANインターフェースを作成し、VLANインターフェースとブリッジを接続します。
root@web1 # brctl addbr lxcbr10
root@web1 # ip link add link eth0 name eth0.10 type vlan id 10
root@web1 # brctl addif lxcbr10 eth0.10
root@web1 # ip link set lxcbr10 up
root@web1 # ip link set eth0.10 up


コンテナを管理するユーザ(user10)を作成します。
root@web1 # groupadd -g 1010 user10
root@web1 # useradd -u 1010 -g 1010 -m user10

ユーザ(user10)のコンテナのブリッジ(lxcbr10)への接続を許可します。
root@web1 # echo "user10 veth lxcbr10 10" > /etc/lxc/lxc-usernet

Webコンテナ(非特権コンテナ)の準備

以下の作業は管理ユーザ(user10)で可能です。Ubuntuで非特権コンテナを実行する場合、LXCの設定ファイルおよびコンテナのディレクトリがユーザのホーム以下になっているようです。
  • デフォルトの設定ファイル:HOME/.config/lxc/default.conf
  • 各コンテナのファイル:HOME/.local/share/lxc/以下
システムのデフォルトの設定ファイルをユーザの設定ファイル場所(HOME/.config/lxc/)にコピーし、コンテナ管理ユーザ(user10)のsubuid/subgidを設定ファイルに記述します。ユーザのsubuid/subgidは/etc/subuid,/etc/subgidに記述されてます。先ほど作成したブリッジを設定ファイルに記述します。一般ユーザの実行する非特権コンテナは/etc/lxc/lxc-usernetで許可されているブリッジにのみ接続できます。コンテナのアドレスは今回はLXC側で設定するようにしました。
user10@web1 $ mkdir -p .config/lxc
user10@web1 $ cp /etc/lxc/default.conf .config/lxc
user10@web1 $ vi .config/lxc/default.conf

lxc.network.type = veth
lxc.network.link = lxcbr10
lxc.network.flags = up
lxc.network.ipv4 =192.168.10.50/24

lxc.id_map = u 0 493216 65536
lxc.id_map = g 0 493216 65536

非特権コンテナとして実行するコンテナを作成します。コンテナの中身はDebian/Jessieで作成しました。

user10@web1 $ lxc-create -t download  -n web10a 

コンテナを起動します。非特権コンテナ内ではrootとしての作業が可能です。ここではデフォルトゲートウェイの設定だけをおきます。アドレスはVRRPで冗長化されているVIPのアドレスを設定します。
アプリケーションなどの設定はあとでまとめておこないます。
user10@web1 $ lxc-start -n web10a
user10@web1 $ lxc-attach -n web10a

root@web10a:/# ip route del defalut via 192.168.10.10 dev eth0


同様にしてもう一つのWebホスト(web2)上でもセットアップおこないます。また同様にしてweb1,web2上でVLAN20系統のためセットアップを行います。基本的にブリッジ(lxcbr20)、VLANインターフェース(eth0.20)、管理ユーザ(user20)などの名前を読み替えるだけです。ただ、図にもありますようにVLAN20系統のLVS-Webコンテナ間のアドレスはVLAN10系統と同じアドレスを設定しています。

LVSホストの準備

LVSコンテナの上流側のブリッジlxcbr0-inは物理インターフェースeth0にバインドします。そのためLVSホストのアドレス(10.10.31.211)はlxcbr0-inのほうに設定する必要があります。
lvs1# brctl addbr lxcbr0-in
lvs1# brctl addif lxcbr0-in eth0
lvs1# ip addr add 10.10.31.211/24 dev lxcbr0-in
lvs1# ip link set lxcbr0-in up

 
LVSコンテナの下流側のブリッジおよびVLANインターフェースを作成します。
lvs1# brctl addbr lxcbr10
lvs1# ip link add link eth1 name eth1.10 type vlan id 10
lvs1# brctl addif lxcbr10 eth1.10
lvs1# ip link set lxcbr10 up
lvs1# ip link set eth1.10 up

LVSコンテナの準備

LVSのコンテナはroot権限で実行するので非特権コンテナのような事前準備は必要ありません。コンテナを作成します。
root@lvs1 $ lxc-create -t download  -n lvs10a

Webコンテナ同様 、今回はIPアドレスはLXCの設定ファイルで直接設定しました。
# Network configuration
lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = lxcbr10
lxc.network.ipv4 =192.168.10.11/24
lxc.network.name = eth1

lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = lxcbr0-in
lxc.network.ipv4 =10.10.31.11/24
lxc.network.name = eth0

コンテナを起動します。ここでもゲートウェイアドレスだけを設定して残りの中身の設定はあとでまとめておこないます。ここで10.10.31.210は上流側のゲードウェイアドレスです。

root@lvs1:/# lxc-start -n lvs10a
root@lvs1:/# lxc-attach -n lvs10a

root@lvs10a:/# ip route add default via 10.10.31.210 dev eth0


同様にしてもう一つのLVSホスト(lvs2)の方もセットアップを実行します。また同様にしてVLAN20系統のセットアップも実行します。Webホストの場合もそうでしたが、新しい系統のVLANを追加する場合、名前を読み替えての同じことの繰り返しです。

それぞれの系統でLVS-Web構成を構築する

これで各VLAN上でコンテナが接続されたました。あとは基本的にコンテナ内で通常のLinux boxと同様なセットアップを行うだけです。

Webコンテナ内でのセットアップ

apacheなどのインストールなどを行います。さらっとapt-getしていますが、ここでは上流側できちんと設定がなされ ていてWebコンテナから外側にアクセスできるようになっていると思ってください。
user10@web1 $ lxc-attach -n web10a

root@web10a:/# apt-get install apache2
root@web10a:/# echo "HELLO,This is web10a" > /var/www/html/index.htm


同様にしてもう一つのWebコンテナ(web10a)、VLAN20系統のコンテナ(web20a/web20b)上でもセットアップをおこないます。

LVSコンテナ内でのセットアップ

ipvsadm,keepalived等必要なものをインストールします。

root@lvs1:/# lxc-attach -n lvs10a

root@lvs10a:/# apt-get install ipvsadm,keepalived


また、Webサービスへののアクセスアドレス(10.10.31.110)はeth0のaliasで設定しておきます。
root@lvs10a# ip addr add 10.10.31.110 label eth0.110 dev eth0

keepalivedの設定をおこないます。はじめにお話しましたが、コンテナ上でkeepalived(ipvs)を利用する前にホスト上でipvsのモジュールを読み込んでおく必要があります。その際ip_vsモジュールさえ読み込んでおけばその他の必要なモジュール(ip_vs_lcなど)は必要に応じて読み込まれるようです。

root@lvs1:# modprobe ip_vs

keepalivedの設定は普通の物理サーバ構成の場合と特に変わりません。またLVS-Webサーバ間のアドレスを同じにしているので、リアルサーバの設定部分はVLAN10系統VLAN20系統で変わりません。
 
keepalived.conf
! Configuration File for keepalived
vrrp_instance VI_VLAN { state BACKUP interface eth1 garp_master_delay 5 virtual_router_id 1 priority 101 # LVS2側のコンテナには以下の設定 # priority 100 nopreempt advert_int 1 authentication { auth_type PASS auth_pass password } virtual_ipaddress { 10.10.31.10/24 dev eth0
# VLAN20系統の場合は以下の設定 # 10.10.31.20/24 dev eth0 192.168.10.10/24 dev eth1 } }

virtual_server_group VLAN {
        10.10.31.110 80

# VLAN20系統の場合は以下の設定
# 10.10.31.120 80
}

virtual_server group VLAN {
    delay_loop 6
    lvs_sched lc
    lvs_method NAT
    protocol TCP

    real_server 192.168.10.50 80 {
        weight 1
        inhibit_on_failure
        HTTP_GET {
            url {
              path /index.html
              status_code 200
            }
            connect_timeout 3
        }
    }

    real_server 192.168.10.51 80 {
        weight 1
        inhibit_on_failure
        HTTP_GET {
            url {
              path /index.html
              status_code 200
            }
            connect_timeout 3
        }
    }
}


同様にしてもう一つのLVSコンテナ(lvs10b)、またVLAN20系統のコンテナ(lvs20a/lvs20b)のセットアップもおこないます。今回はすべて手動で行っていますが、コンテナ内の設定はいわゆる構成管理ツールを利用することによりもっとスマートに行うことが可能になると思います。

Webコンテナの側の準備も完了してから、両方のLVSコンテナでkeepalivedを起動します。

root@lvs10a:/# /etc/init.d/keepalived start
root@lvs10a:/# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.10.31.110:80 lc
  -> 192.168.10.50:80             Masq    1      0          0        
  -> 192.168.10.51:80             Masq    1      0          0        


結果確認

適当な10.10.31.0/24上のクライアントから各VLANのサービスアドレスにアクセスします。上流側からサービスにアクセスする場合、このサービスアドレスへの経路をVRRPで冗長化する上流側のVIPアドレスに設定しておきます。
client# ip route add 10.10.31.110 via 10.10.31.10
client# ip route add 10.10.31.120 via 10.10.31.20

アクセスしてみました。
client# curl http://10.10.31.110
HELLO,This is web10a
client# curl http://10.10.31.110
HELLO,This is web10b
client# curl http://10.10.31.110
HELLO,This is web10a
client# curl http://10.10.31.110
HELLO,This is web10b

client# curl http://10.10.31.120
HELLO,This is web20a
client# curl http://10.10.31.120
HELLO,This is web20b
client# curl http://10.10.31.120
HELLO,This is web20a
client# curl http://10.10.31.120
HELLO,This is web20b

それぞれの系統でちゃんとロードバランスされていることがわかります。

LVSをコンテナ化しない場合

LVSをコンテナ化すると案件ごとに専用のLVSを設置することができます。ただ運用していく中でLVSのマスタとスレーブのコンテナの存在する物理サーバが案件ごとに異なるといった状態になることも考えられます。LVSをコンテナ化しない場合の構成として次のような構成を考えてみました。

lvs_r2
図2

LVS-Webホスト間のネットワークそのままVLANを利用した独立したものになっています。あるVLANのWebコンテナからは別のVLANのコンテナにアクセスはできません。ただ1つのLVSサーバでロードバランスする関係上、それぞれのVLAN内で同じネットワークアドレスは利用できません。この場合はVLANそれぞれに異なるネットワークセグメントを割り当てるのが妥当でしょう。ここではVLAN10に192.168.10.0/24のVLAN20に192.168.20.0/24のセグメントを割り当てています。

LVSをコンテナ化した場合と異なりLVSサーバにトラブルが発生した場合、すべての案件に影響が及んでしまいますが、これは冗長化により対処は可能です。1つのkeepalivedですべての案件のロードバランスも可能ですが、その場合は設定変更時などにトラブルが発生した場合の影響が甚大ですので、案件ごとに別々のkeepalivedを起動する必要があると思われます。

DR構成の場合

もともとはDR構成のケースを考えていました。コンテナ上でiptablesが利用可能かどうかを確認したのもそのためです(今回のNAT構成ではiptablesによるパケットの制御は必要ありません)ここでは外部にでるために上流側のネットワークのVLAN1のネットワークに接続しなくてはならないケースを考えます。このVLAN1のネットワークは上位の管理者の管轄で設定などを変更することは不可能であるとします。この条件でつぎのような構成を考えてみました。今回LVSの冗長化は省略してあります。

lvs_dr2
図3

外部からのパケットはVLAN1からLVSホストに到達し、そこで各VLANのサービスアドレス(VLAN10は10.10.1.110、VLAN20は10.10.1.120)に応じてそれぞれのVLANのLVSコンテナにルーティングされます。そしてLVSコンテナでロードバランスされたあとターゲットのWebコンテナに送られます。戻りのパケットはWebコンテナに用意されたeth1のインターフェースから直接外部に送られます。eth1のインターフェースはWebホスト上に用意されたVLAN1のインターフェースに接続されているブリッジに繋げられています。

この構成ではWebコンテナから直接パケットを返送するために、VLAN1への経路としてeth1がWebコンテナ上に設けられています。このインターフェースからはパケットが出て行くだけなので必ずしもアドレスを設定する必要はありませんが、接続されているブリッジを介して他のVLANのWebコンテナとつながってしまっています。LVS-Webホスト間のVLANネットワークの独立性を保つためにはWebホスト側でこのブリッジを介しての通信を禁止する必要があります(図3では一方通行で表しています)

まとめ

コンテナを利用して複数のLVS-Web構成を1つの物理システム上に構成することを試みてみました。その際にVLANを利用して LVS-Web間のネットワークを独立したものにしています。Webコンテナに非特権コンテナを利用することによってWeb管理者にWebホストや外部ネットワークへの権限を与えないままコンテナ内のみの権限を与えることができます。今回の構成にはLXCとLinuxのカーネルの機能および基本的なコマンドのみを利用しており、比較的簡単にコンテナによるクラスタ構成を作成することができます。またVLANによる構成はコンテナ内からはそれを全く意識することなくコンテナのグループに独立したネットワークを与えることができます。

tech_ccmp at 13:50
この記事のURLComments(0)TrackBack(0)
2016年09月26日

Serfの障害検知とメンバシップ管理について

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

はじめに

KLabさんの協力会社として一緒にお仕事をさせて頂いておりますクラスターコンピューティングと申します。今回Serfという面白そうなツールがあるので試してみました。

システムの高可用性化を目的にクラスタを構成することはよく行われていると思います。可用性を維持するために大切なことはクラスタに属するサーバの状態を常に把握しておくことです。そしてサーバの状態に変化が生じた場合ーたとえばサーバが不意に停止した場合ーそれに応じて適切な動作ができるようにする必要があります。システムのサービスはこの土台となるサーバ管理の仕組みの上に構築されます。

Serfはこのクラスタの土台となるサーバの管理の部分をサポートしてくれるツールです。Serfはゴシッププロトコルを利用したP2P型のクラスタを構成します。これによりシンプルな作りでかつ信頼性やスケーラビリティに優れ、そしてネットワーク的な効率も良い管理システムを提供しています。

Serfの基本機能


本家ホームページ

 SerfはHashcorpで開発されているクラスタの管理ツールです。
イントロダクションのページにも書かれていますが、Serfの基本機能は以下の3つになります。
  •  メンバシップ管理
  •  メンバの障害検知
  •  メンバへのイベント伝達
 今回は上の2つ、メンバシップ管理と障害検知について、そのゴシッププロコトル的な手法とあわせて紹介したいと思います。

serf_cluster2
図1
Serfクラスタにはいわゆる中心サーバは存在せず、各エージェントが相互に通信することによってクラスタの状態を把握している。
Serf はいわゆるP2P型のクラスタを構成します。クラスタを構成する全てのサーバ上でSerfエージェントが実行されています。クラスタを管理する中央サーバ的なものは存在せす、それぞれの Serfエージェントに役割や機能は同じです。Serfでは各エージェントが相互に通信することによって最新のクラスタの状態を把握することによりクラスタ構成しています。すべてのエージェントが保持しているクラスタについての情報は常に最新で同一のものになっています。

Serfでのメンバシップ管理

Serfエージェントが実行されクラスタに所属しているサーバをメンバと呼びます。Serfにおけるメンバシップ管理は次の完全性に基いています。
  • メンバシップ管理における完全性:各メンバが他の全てのメンバの状態を常に把握している。

Serfではこの完全性を満たすため次のようなアプローチをとっています。

  • クラスタの状態に変化が発生したとき、少なくとも1つの正常なノードがそれを検知し、全体にそれを伝達(ブロードキャスト)する。
  • 有限時間内それを検知し、それを有限時間内に全体に伝たえる。

例えば、自身の状態に変化が生じたときは自分でそれを全体に伝えるアクションを実行する必要があります。また、新たなメンバがクラスタに参加する際には、新規メンバはすでに参加しているいずれか1つのメンバを知っていればクラスタに参加できます。複数のメンバをもつクラスタのいずれか1つのメンバが別のクラスタに参加した場合、残りのメンバもそのクラスタに参加し全体でに1つのクラスタになります。


Serfのメンバシップ管理でやり取りされる情報は、メンバのホスト名、IPアドレス、障害ステータス(死活)、タグなどの情報です。


完全性を満たすためには確実に状態の変化を検知する仕組みおよびクラスタ全体にすみやかに情報を伝達する仕組みが必要になります。Serfではゴシッププロトコル的な手法を利用することによって、ネットワークの負荷を抑えつつこれらの仕組みを構築しています。

メンバの障害ステータス

 メンバのステータスはserf membersというコマンドで取得することができます。どのメンバ上で実行しても同じ結果が得られます。

server1 # serf members
server1  192.168.0.101:7946  alive
server2  192.168.0.102:7946  alive 

server3  192.168.0.103:7946  alive


メンバの障害ステータスは正常(alive)、正常終了(left)、障害(failed)の3つの状態で表されます。

aliveは正常に他のメンバからそのメンバにアクセスできる状態です。

leftはSerfエージェントがserf leaveコマンドやCtrl-Cなどによって正常に終了したことを示しています。
Serfエージェントは正常に終了する際に自身の終了のクラスタ全体に通知します。
受け取った各エージェントはそのメンバのステータスをleftに変更して障害検知の対象から外します。

failed はそのメンバにアクセスができず障害と判断されていることを示しています。この場合はそのメンバに対する死活確認は継続し、復旧が確認された場合は自動的 にクラスタのメンバの復帰します。また、failedの状態で決められた期間(デフォルトでは24時間)が経過すると障害検知対象から外されるようにもなっています。

メンバのタグ

 Serfではそのメンバ同士でタグを利用した簡単な情報の共有も可能です。タグとは foo=barで表されるようないわゆるキーバリューのことです。serf tagsコマンドを利用してそれぞれのメンバ上で設定します。各メンバに設定されたタグはserf membersコマンドで確認できます。


タグのセット
server1 # serf tags -set foo=bar
Successfully updated agent tags

タグの確認
server2 # serf members
server1  192.168.0.101:7946  alive  foo=bar
server2  192.168.0.102:7946  alive  
server3  192.168.0.102:7946  alive  

タグの消去
server1# serg tags —delete foo

タグもステータスと同様に全てのメンバで常に最新のタグ情報が共有されます。

Serfの障害検知とメッセージ伝達

メンバシップ管理での完全性を満たすためには、障害検知やメッセージのブロードキャストのためかかるネットワークの負荷が問題になります。そのための工夫としてSerfではゴシッププロトコル的な障害検知手法およびメッセージ伝達方法を利用しています。

Serfの障害検知

swim_failure_detection
図2
SWIMでの障害検知のイメージ
 複数の経路からメンバの死活を確認することにより確実性を高めている。

Serfのゴシッププロトコルについてのページ

SerfではSWIMという手法を利用して障害検知をしています。SWIMとは相互監視によるP2P型の障害検知方法で、次のような特徴があります。
  •  あるメンバの障害を残りの全ての正常なメンバが確実に検知できる。
  •  メンバの障害を信頼性の低いネットワークであっても誤検知しない。
  •  メンバの障害を速やかに有限の時間内で検知できる。
  •  メンバの障害検知のためのネットワーク負荷をクラスタの規模に応じて常に一定量にできる。
障害検知の仕組みは次のようになっています(図2参照)
  1. 各メンバはお互いに常に短い周期で他のメンバの障害監視を行っている。ここではあるメンバ(D)について考える。
  2. メンバ(D)は自身の持っているメンバのリストからランダムに対象メンバ(T)を選択し障害を確認する。
  3. 対象メンバ(T)にアクセス(PING)を実行する。応答(ACK)がなかった場合すぐに障害と判断せずに、他のK個メンバ(S1...SK)に対象メンバ(T)への代替アクセスを要請(PING-REQ)する(図2ではK=3)。要請をするメンバも自身の持っているメンバのリストのなかからランダムに選択する。
  4. PING-REGを受け取ったメンバは代わりに対象メンバ(T)にアクセス(PING)して、その応答(ACK)があれば(D)に応答(ACK)を返す。
  5. (D)はタイムアウトまでにPINGあるいはPING-REQのACKが得られれば対象メンバ(B)は正常(Alive)と判断する。タイムアウトまでにACKが得られなかった場合、障害(Failed)と判断する。 障害と判断された場合、判断したメンバが全体にその旨を通知する。通知を受け取った各々のメンバは自身のもっている(T)のステータス情報を更新する。
障害検知の対象とするメンバの選択は基本的にはランダムです。ただ、SWIMではある全てのメンバが一定の時間間隔で確実に検知対象になるように、対象メンバが一周するまで同じメンバは選択しないように制限をかけて選択しているようです。

メッセージ伝達

piggyback

図3
各メンバ間では常時障害検知のためのパケットがランダムに行き来している。
 障害検知のためのパケットにメッセージを載せることによりランダムな経路で全体にメッセージが伝達される。

SWIM では障害の検知と検知した障害の全体への通知を分けて考えています。初期のSWIMでは全体への通知はマルチキャストを利用していたようですが、確実性とネットワーク負荷の点で問題があったのでやめにしたようです。改良されたSWIMでは障害通知のパケットを障害検知のためのパケット(PING/ACK /PING-REQ)に載せて(Piggyback)伝達するようしています。

障害検知のパケットは常時それぞれのメンバからランダムな相手に対して送信されています。このパケットに便乗して通知のパケットを送信します。送られた先でさらに別の障害検知のパケットに便乗して再送信します。 このようにして、メンバからメンバにランダムに送信されていき、やがて全体に浸透します。これがいわゆるゴシッププロトコル的な方法になっています。

Serf ではこの便乗する仕組みを障害検知の結果の通知以外にも、イベントなどのメッセージの伝達する場合にも利用しています。ただ、障害検知のパケットの送信間隔がやや長く(1秒程度)これだけではメッセージの伝達速度的に問題があるので、メッセージを伝達するだけの専用のパケットを同様の方法でより短い間隔 (200ミリ秒)で送信しているようです。
  •  障害検知のパケットがUDPなのでメッセージの伝達もUDPで実行される。
  •  メッセージにはカウンタがついており規定の回数再送信されるそれ以上送信されなくなる。
  •  仕様上メンバは同じメッセージを複数回受け取る可能性がある。メッセージのバッファは管理されているので同じメッセージであることは認識できる。

ゴシッププロトコルとメッセージ到達性

gossip2

図4
  • Fanout数2、送信回数3のゴシッププロトコルのイメージ
  • 0から3の順番でうわさ話(Gossip)のようにメッセージが伝達されていく。
  • 伝達先の選択はランダムであるため、すでに受け取ったメッセージを再度受け取ったり、同じタイミングで2箇所からメッセージを受け取ることが発生する

ゴシッププロコトルというのはうわさ話(Gossip)のようにランダムな経路でメッセージを全体に伝達する方法です。ちょうど病気の感染が広がっていく様子とも一致することから感染症プロトコルなどとも呼ばれます。
 
ゴシッププロトコルの伝達性は1回の送信でランダムに選択する送信先の数(Fanout数)およびサーバからサーバへメッセージを(再)送信する回数で決まります。送信先の選択がランダムなのですべてのメンバにメッセージが伝わるかどうかは確率的にしか保証されません。ランダムに選択する送信先の数を増やしたり、再送信の回数を増やせばメッセージの伝達速度や全体に浸透する確率が高くなりますが、その分ネットワークの負荷が高くなります。

ゴシッププロコトルによるメッセージの伝達性や情報の収束性について厳密に評価するのは難しい問題のようです。ゴシッププロトコルによるメッセージの伝達を表すモデルはいくつかあるようですが、Serfでは簡単な感染症モデル(SIモデル)から導出される式を利用しているようです。Fanout数とクラスタの規模をパラメータとして充分な確率で全体に浸透するのに必要な再送信数を計算して決めています。

またSerfでは独自の実装としてゴシッププロトコルによるメッセージの伝達とは別に一定の間隔(デフォルトでは30秒)でランダムなメンバとのTCP通信よる保持しているクラスタの情報(メンバのステータスやタグ)の同期を行っています。

Serfの使い所

Serf は通常のHAクラスタからメンバシップの管理およびサーバの障害検知の部分だけを取り出したようなツールになっています。P2P型の構成であることにより導入も簡単です。Serf自身ではなにかサービス等を提供できるわけでありませんが、システムにこれらの機能を簡単に追加することが可能です。
  •  HAクラスタのような高度に連携したシステムでなくても、各サーバにserfエージェントを起動してサーバの死活確認に利用する。
  • タグを利用して各サーバの役割や実行しているカーネルやアプリケーションのバージョン情報を全体で共有する。
Serfのメンバの追加や削除、タグの追加や変更は簡単に動的に可能です。
  • 仮想マシンやコンテナで構成されるクラスタのような、メンバが動的に変化するシステムでのメンバシップの管理。
  • デプロイツールによってアプリケーション構成が動的に変化するシステムでの情報の共有。
などにも適していると考えられます。

まとめ

Serf はクラスタの構成をサポートするためのツールです。Serfを利用することによりサーバのメンバシップの管理や障害検知が可能になります。Serfはゴ シッププロトコル的手法を障害検知およびメッセージの伝達に利用しており、それによってP2P形式のクラスタ構成ながら、ネットワーク負荷を抑え情報のす ばやい浸透が可能になっています。

参考文献

SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol
SerfのWebページからも引用されているSWIMに関する論文です。Failure Detectionの解析的な部分は基本結果のみ引用されています。

On Scalable and Efficient Distributed Failure Detectors
SWIMのFailure Detectionに解析的な議論についての論文です。

The Mathematics of Infectious Diseases
SWIMおよびSerfで利用している感染症モデルについての解説が最初にあります。ただ、ここでは感染からの回復(R)も含めたSIRモデルになっています。




tech_ccmp at 17:00
この記事のURLComments(0)TrackBack(0)
2016年09月20日

ISUCON6予選をトップ通過しました

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

@methane です。「この技術部には問題がある!」というチーム名で @kizkoh (インフラ担当), @mecha_g3 (アプリ担当) とともに ISUCON 6 に参戦し、予選をトップスコアで通過しました。 恒例のふりかえり記事を書きます。

ふりかえり

残念ながらスコアは記録してないのですが、時系列順にやったことをまとめます。 アプリのコードは methane/isu6q-app で公開しているので、興味がある方はコードを確認してください。

strings.Replacer を使う

使用言語は最初から Go と決めていたのですが、Goの初期実装は遅すぎてタイムアウトで最初からスコア無しでした。 top でアプリのCPUが支配的なのはすぐ判りましたし、コードを読めばなにが遅いのかも一発で判りました。そんなに長くないので関数全体を張ります。

func htmlify(w http.ResponseWriter, r *http.Request, content string) string {
	if content == "" {
		return ""
	}
	rows, err := db.Query(`
		SELECT * FROM entry ORDER BY CHARACTER_LENGTH(keyword) DESC
	`)
	panicIf(err)
	entries := make([]*Entry, 0, 500)
	for rows.Next() {
		e := Entry{}
		err := rows.Scan(&e.ID, &e.AuthorID, &e.Keyword, &e.Description, &e.UpdatedAt, &e.CreatedAt)
		panicIf(err)
		entries = append(entries, &e)
	}
	rows.Close()

	keywords := make([]string, 0, 500)
	for _, entry := range entries {
		keywords = append(keywords, regexp.QuoteMeta(entry.Keyword))
	}
	re := regexp.MustCompile("("+strings.Join(keywords, "|")+")")
	kw2sha := make(map[string]string)
	content = re.ReplaceAllStringFunc(content, func(kw string) string {
		kw2sha[kw] = "isuda_" + fmt.Sprintf("%x", sha1.Sum([]byte(kw)))
		return kw2sha[kw]
	})
	content = html.EscapeString(content)
	for kw, hash := range kw2sha {
		u, err := r.URL.Parse(baseUrl.String()+"/keyword/" + pathURIEscape(kw))
		panicIf(err)
		link := fmt.Sprintf("<a href=\"%s\">%s</a>", u, html.EscapeString(kw))
		content = strings.Replace(content, hash, link, -1)
	}
	return strings.Replace(content, "\n", "<br />\n", -1)
}

今回の問題ははてなキーワード+はてなスターのようなサービスで、この関数はキーワードの紹介記事に対して他のキーワードへのリンクを生成しつつHTMLエスケープするものです。キーワードは文字数順でソートしているので、最長一致でリンクになります。

キーワードからリンクへの変換が一発でされていないのは、先に変換するとHTMLエスケープで <a> タグまでエスケープされてしまうし、逆に先にHTMLエスケープをするとキーワードを見つけられなくなるからです。(一度これに気づかず一気に変換するようにしてハマりました)

正規表現のビルドと置換のどちらがどれくらいの割合で重いのかまではまだプロファイルを始めてなかったのでわかりませんが、初手からGoらしい最適化を始めてみます。

まずGoの正規表現は遅いので strings.Replacer を使ってキーワードからリンクへの変換をします。 Replacer を構築するのにも時間がかかるので、 htmlify() という関数で毎回構築するのではなく、起動時と、キーワードの追加削除時に再構築をするようにします。(実際にはすでに存在するキーワードの記事に対する修正のケースを見逃していて無駄に再構築していました。これに気づいていたらもっとスコア上がったはず…)

var (
	mKwControl sync.Mutex
	kwdList    KeywordArray

	mKwReplacer                  sync.RWMutex
	kwReplacer1st, kwReplacer2nd *strings.Replacer
)

func updateReplacer() {
	reps1 := make([]string, 0, len(kwdList)*2)
	reps2 := make([]string, 0, len(kwdList)*2)
	for i, k := range kwdList {
		if k.holder == "" {
			k.holder = fmt.Sprintf("isuda_%x", sha1.Sum([]byte(k.Key)))
			kwdList[i].holder = k.holder
		}

		reps1 = append(reps1, k.Key)
		reps1 = append(reps1, k.holder)

		reps2 = append(reps2, k.holder)
		reps2 = append(reps2, k.Link)
	}
	r1 := strings.NewReplacer(reps1...)
	r2 := strings.NewReplacer(reps2...)
	mKwReplacer.Lock()
	kwReplacer1st = r1
	kwReplacer2nd = r2
	mKwReplacer.Unlock()
}

func AddKeyword(key, link string) {
	k := Keyword{Key: key, Link: link}

	mKwControl.Lock()
	kwdList = append(kwdList, k)
	sort.Sort(kwdList)

	updateReplacer()
	mKwControl.Unlock()
}

func ReplaceKeyword(c string) string {
	mKwReplacer.RLock()
	r1 := kwReplacer1st
	r2 := kwReplacer2nd
	mKwReplacer.RUnlock()
	x := r1.Replace(c)
	x = html.EscapeString(x)
	return r2.Replace(x)
}

AddKeyword はキーワードをポストしたときに、 ReplaceKeywordhtmlify から呼ばれます。これで NewReplacer を呼び出す回数を大幅に削減することができました。

この変更を投入するタイミングで、 MySQL に接続するときに '127.0.0.1' からの接続が許可されていないというエラーがでるようになって(何もしてないのに壊れた追記: 秘伝のタレでskip-name-resolveが入ったのが原因の模様) Unix Domain Socket を使うようにしました。 @kizkoh による nginx や MySQL の設定 (静的ファイルを nginx で返す等) や、初期実装にあったバグを潰したりして、14:00すぎに12万点を出しました。

isutar を isuda にマージ

これは @mecha_g3 に任せた部分です。 isutar と isuda がお互いにJSON APIを使ってやり取りをしている部分があって、2つのアプリとMySQLが1台のマシンに乗っている以上完全に無駄なので全部1つにまとめました。スコアを記録してないのですが順当に性能アップしたはずです。

これを投入してベンチをかけるときにハマったのが、DBコネクションのデッドロックです。もともと2コアの1台のマシンなので、DBへの接続は4本も要らないだろうと思いつつ、念のために8本にしていました。ところが、次のようなコードが原因で8本あっても足りなくなってしまいました。

	rows, err := db.Query(fmt.Sprintf(
		"SELECT * FROM entry ORDER BY updated_at DESC LIMIT %d OFFSET %d",
		perPage, perPage*(page-1),
	))
	if err != nil && err != sql.ErrNoRows {
		panicIf(err)
	}
	entries := make([]*Entry, 0, 10)
	for rows.Next() {
		e := Entry{}
		err := rows.Scan(&e.ID, &e.AuthorID, &e.Keyword, &e.Description, &e.UpdatedAt, &e.CreatedAt)
		panicIf(err)
		e.Html = htmlify(w, r, e.Description)
		e.Stars = loadStars(e.Keyword)
		entries = append(entries, &e)
	}
	rows.Close()

このコードはトップページのハンドラーの一部です。 for rows.Next() ループが終了して rows.Close() が呼ばれるまでDBの接続を握るのですが、このループ中の loadStars(e.Keyword) の部分が中でさらにSQLを呼び出しています。 この状態で、 top ページに8個並列でアクセスが来てループの外側のクエリを実行すると、8本の接続を使い切った状態になり、どの goroutine も内側の loadStars() でDB接続を無限に待ってしまうことになります。

落ち着いて考えればすごく当たり前のことなのですが、最初は rows.Close() を呼んでない場所がどこかにあるんじゃないかと探し回ったり、この形のネストがまずいことに気づいたときも「でも1goroutineあたりのDB接続数が2倍になるだけだから、その程度の余裕は見込んで8本にしたんだけどなあ。」と考えてしまったりしてしまいました。

結果、この問題の対策もちゃんとネストを潰すのではなく、単にコネクションプールの数を倍の16に増やしただけです。ベンチマーカーがどんどん並列度上げてくるタイプだったらこれでも死んでました。

教訓として、 rows.Next() ループ内でネストしてクエリを実行するのは単に必要な接続数が倍になる以上の凶悪さを持っているので、 rows.Close() までの処理は単純にクエリ結果のフェッチだけにしましょう。

Replacer の構築を zero time cache 化

プロファイルを取ってみたところまだ Replacer の構築が重いので、個人的に zero time cache と呼んでいるイディオムを使って安全にキャッシュをしてみます。次のコードを見てください。

var (
	mKwControl sync.Mutex
	kwdList    KeywordArray

	mUpdateReplacer sync.Mutex
	repLastUpdated  time.Time
)

func AddKeyword(key, link string) {
	k := Keyword{Key: key, Link: link}
	mKwControl.Lock()
	kwdList = append(kwdList, k)
	mKwControl.Unlock()
	updateReplacer()
}

func updateReplacer() {
	now := time.Now()
	mUpdateReplacer.Lock()
	defer mUpdateReplacer.Unlock()

	if repLastUpdated.After(now) {
		return
	}
	repLastUpdated = time.Now()

	reps1 := make([]string, 0, len(kwdList)*2)
	reps2 := make([]string, 0, len(kwdList)*2)

	mKwControl.Lock()
	kws := kwdList[:]
	mKwControl.Unlock()

	sort.Sort(kws)
  // ... 以降 Replacer の構築処理
}

mUpdateReplacerrepLastUpdated が追加した変数で、 updateReplacer()repLastUpdated = time.Now() までがイディオムになっています。

例えば、キーワードA, B, C がほぼ同時に追加されたとします。

  1. まずAが最初にロックを取得して、 repLastUpdated = time.Now() を実行して Replacer の構築を始めます。
  2. 続いてキーワードBとCがともに mUpdateReplacer.Lock() で停止します。ロック直前に取得した now は (1) で更新した repLastUpdated よりも未来の時間になります。
  3. キーワードAの Replacer 更新が終了し、 mUpdateReplacer が開放されます。キーワードBを処理していた goroutine がそのロックを取得します。
  4. ロック取得前に取得した nowrepLastUpdated よりも新しいので、 if repLastUpdated.After(now) が真になりません。なので Replacer を再構築します。キーワードAを更新したときの kwdList にキーワードBは入っていなかった(厳密にはタイミング依存で入っていた可能性もある)ので、これは必要な処理です。
  5. さらにキーワードBの Replacer 更新が終わり、キーワードCが mUpdateReplacer のロックを取得します。今度は if repLastUpdated.After(now) が真になるので、 Replacer の再構築はスキップされます。 (4) で Replacer を再構築したときには確実にキーワードCも kwdList に入っていたので、スキップしても安全です。

この説明を一度読んだだけでは理解できないかもしれませんが、ぜひじっくりとコードと見比べて理解してみてください。 このイディオムはISUCONだけではなく、実際にGoで高性能なサーバーを書くときに大変便利です。

また、例えば MySQL が複数のトランザクションを一度の fsync でコミットするグループコミットなど、「同時に実行される重い処理をバッチ化する」というのは並列処理のデザインパターンと呼んでも良いくらい汎用的だと思うのですが、どなたかこのパターンの名前をご存知でしたら @methane 宛の Tweet などで教えてください。

仕上げ

まだ top ページのハンドラーが重かったので、再び zero time cache パターンを使ってさらにトップページの内容の取得を効率化します。

ただしこのときは効率より実装の手早さを優先して、 zero time cache パターンをトップページのハンドラ内に直接実装してしまいました。着実には性能向上しましたが、これもキーワードの更新側に処理を持っていけばもっと劇的な性能向上が見込めたはずです。

最後に、アプリの環境変数で GOGC=400 を設定し、プロファイルやモニタリングやnginxのアクセスログを切り、ベストスコアを出すまで enqueue を数回実行して終了しました。

考察

POST /keyword のアクセスがキーワードの追加だけではなく更新にも使われていることを見逃していて、一番重い Replacer の再構築を必要以上に実行してしまっていたのが悔やまれます。

また、上に書いたようにトップページの内容の更新も、 GET / ではなく POST /keyword で行えばもっと劇的なスコアアップができたでしょう。

さらに、非ログインユーザーのトップページを静的ファイルに書き出してしまって nginx に直接返させるのも、ISUCONで頻出の攻略法で事前に判っていたはずで、実装する時間もあったはずなのにやるタイミングを逸してしまいました。

これらを全部できていれば、ベンチマーカーの実装次第ですが、倍の50万点の可能性もあったかもしれません。

感想

今までは毎年予選は一人で戦って勝ち抜いてきたのですが、去年の予選のボリュームが本戦と同じくらいあってキツかったので、今年は予選からチームで戦うことにしました。

「直したはずのバグがこっちのブランチでは直ってなかった」「プロファイルが違う場所のバイナリ/ソースを参照してたかもしれなくてくてアテにならない」系のトラブルはあったものの、自分がアプリを書くときに心配事を忘れて集中できたり、逆に実装を任せてる間に自分は休憩したり落ち着いて見直しをしたりできて、精神的な負荷が大幅に減り、余裕を持って優雅に戦えたと思います。

一方で、Azureに慣れていなかったことや、14時過ぎに取った初めての0点以外のスコアがその時点の断トツトップで、そこからもほぼずっと2位のダブルスコアをキープしていたこともあって、保守的になりすぎて全力を出し切れたとは言い難いです。

本戦では全力を出せるように、もう少しチームワーク練習や環境構築の練習を重ねて行きます。

それでは、決勝進出者のみなさん、決勝でお会いしましょう。 運営・出題者のみなさんはお疲れ様でした。決勝も引き続きお願いいたします。


songofacandy at 16:25
この記事のURLComments(3)TrackBack(0)ISUCON | golang
2016年08月30日

既成の BLE デバイスを自作プログラムから利用する試み

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

前回の記事:
  • BLE デバイス間の通信内容をパケットレベルで読み解いてみる
    題材には低価格で出回っているありふれた Anti-Lost 系 BLE デバイス A を選びました。この小さなデバイスには LED・ブザーと押しボタンスイッチが 実装されており、対向スマホアプリとの間で双方向のやりとりが可能なつくりになっています。


    デバイス A の UI
前回は BLE パケットスニファを使って既成の BLE デバイス「A」と対向アプリ間の通信内容を記録しその精査を試みました。そこで得られた情報をもとに、今回は実験の一環としてデバイス A と連携する Android アプリの試作を行います。パケットログから窺える BLE セントラル側の所作を自作プログラムで再現することは論理的に可能でしょう。

デバイス A とアプリの連携に必要な処理

前回の記事から、デバイス A とアプリの連携に関する要素をピックアップしてみます。

  • アプリからデバイス A の LED・ブザーをコントロール

    • 「Scene 6: アプリからデバイス A の LED・ブザーを操作」
      Frame 616

      対向アプリ上の所定のボタンを押下すると、Frame 345, Frame 238 の示す、 "「Immediate Alert」サービス配下の「Alert Level」キャラクタリスティック" の Value Handle 0x0025 に値「2(High Alert)」が書き込まれる

      デバイス A の LED・ブザーはファームウェアレベルでこの Alert Level キャラクタリスティックに紐づけられている模様。High Alert 値が書き込まれるとそれに反応して短時間 LED とブザーが ON になる

    ー> デバイス A の提供する Immediate Alert サービス配下の Alert Level キャラクタリスティックにアプリから値 2 を書き込めばデバイス A の LED・ブザーが ON になり、値 0 を書き込めば OFF になる

  • デバイス A のボタン押下をアプリへ通知

    • 「Scene 5: デバイス A ボタン押下時のアプリへの通知を設定」
      Frame 491

      Frame 453、Frame 439、Frame 453 の示す通り、ハンドル 0x0036 は、 "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)配下の Client Characteristic Configuration Descriptor (CCCD)" である

      当該ユーザ定義キャラクタリスティック(UUID = 0xFFE1)のプロパティには Notify が設定されており、クライアントである対向アプリから CCCD 0x0036 に Notification bit (0x0001) を書き込んでおくことで、このキャラクタリスティックの値が更新された時に Value Handle である 0x0035 経由でアプリ側へ通知(Notification)が行われるようになる

    • 「Scene 7: デバイス A のボタンを押すとアプリへ通知」
      Frame 711

      デバイス A の物理ボタンはファームウェアレベルで Frame 439 の示す "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)" に紐づけられている模様。このボタンを押すと当該キャラクタリスティックの値がデバイス内部で更新され Frame 491 での仕込みに基づきアプリ側へ通知が行われる

    ー> 初期処理として、デバイス A の提供するユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)の持つ Client Characteristic Configuration Descriptor (詳細:1, 2) にアプリから値 0x0001を書き込んでおく。それ以降にデバイス A のボタンが押下されると当該キャラクタリスティック経由でデバイス A からアプリへ通知が行われる
処理そのものには特に難しそうな要素もなく、アプリからデバイス A への接続後に所定のキャラクタリスティック・デスクリプタの操作を適切に行うことがポイントとなりそうです。

サービス・キャラクタリスティック・デスクリプタの UUID について

BLE ネイティブの世界では所定のキャラクタリスティックやデスクリプタの I/O には各エントリに紐づけられたハンドルが使用されますが、抽象化された Android API での処理対象はオブジェクトです。所定のキャラクタリスティックやデスクリプタのオブジェクトの取得にはそれぞれの UUID が必要であるため、プログラムの記述に際しては、パケットログ上に記録された所定のハンドルがどの UUID のエントリのものであるかを正確に把握する必要があります。
また、サービス - キャラクタリスティック - デスクリプタは階層関係にあるため、所定のエントリのオブジェクトを取得する手続きは最上位にあるサービスのオブジェクトが常に起点となります。

以下に、パケットログから所定のサービス以下の各エントリの UUID を見つける方法と、各 UUID からそれぞれのエントリのオブジェクトを取得する Android コードの例を示します。

パケットログから所定のサービス以下の各エントリの UUID を拾う

  • 前回記事中のパケットログ Frame 229 では 0x0001 - 0xffff のハンドル空間を対象に Read By Group Type Request で GATT Primary Service Declaration を照会、そのレスポンスが Frame 231 です

           (クリックで大きく表示)

    ここではレスポンスに含まれる 3件のレコードのうち 2件めに注目してみます

    Opcode: Read By Group Type Response (0x11)
             :
    
    Attribute Data, Handle: 0x000c, Group End Handle: 0x000f
      Handle: 0x000c
      Group End Handle: 0x000f
      Value: 0118
             :
    
    • Primary Service Declaration の照会に対する Read By Group Type Response の「Value」には当該サービスの UUID が格納される
    • この例では 16ビット UUID = 0x1801 であり、これは、BLE 既定の Generic Attribute サービスを示す
    • 当該サービスはハンドルグループ 0x000c - 0x000f を占有する
  • Frame 287 では上記のハンドルグループ 0x000c - 0x000f を対象に Read By Type Request で GATT Characteristic Declaration を照会、そのレスポンスが Frame 289 です

           (クリックで大きく表示)

    レスポンスに注目します

    Opcode: Read By Type Response (0x09)
             :
    
    Attribute Data, Handle: 0x000d
      Handle: 0x000d
      Value: 200e00052a
    
    • Characteristic Declaration の照会に対する Read By Type Response の「Value」には当該キャラクタリスティックのプロパティ・Value Handle・UUID が格納される(詳細:1a, 2a, 1b

      (※表は BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] page 532 より)
    • ここでは「value: 200e00052a」につき、プロパティ = 0x20 (Indicate), Characteristic Value Handle = 0x000e, 16ビット UUID = 0x2A52
    • 16ビット UUID = 0x2A52 は、BLE 既定の Service Changed キャラクタリスティックを示す
  • Frame 302 では残りのハンドル 0x000f についての情報を GATT へ Find Information Request で照会、そのレスポンスが Frame 305 です

           (クリックで大きく表示)

    レスポンスに注目します

    Opcode: Find Information Response (0x05)
    UUID Format: 16-bit UUIDs (0x01)
    Handle: 0x000f
    UUID: Client Characteristic Configuration (0x2902)
    
    • ハンドル 0x000f は Client Characteristic Configuration Descriptor (CCCD) であり、この CCCD は直前の Indicate プロパティを持つ Service Changed キャラクタリスティックに属する
      (※CCCD の 16ビット UUID は 0x2902 固定

以上のことから、デバイス A 上の Generic Attribute サービスの構成は以下の内容であることがわかります。

Generic Attribute サービス(UUID = 0x1801)
  |  Handle Group = 0x000c - 0x000f
 |
  +-- Service Changed キャラクタリスティック(UUID = 0x2A52)
        |   Handle = 0x000d, Value Handle = 0x000e
        |
        +-- Client Characteristic Configuration Descriptor (UUID = 0x2902)
              Handle = 0x000f

所定の UUID のエントリのオブジェクトを取得する

ここまでに登場した UUID はすべて 16ビット値でしたが、16 ビット UUID は Bluetooth 用にアサインされている本来の 128ビット UUID の固定部分(BASE_UUID)を省略した表現です。

  • BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part B] page 227
    2.5 SEARCHING FOR SERVICES
           :
    2.5.1 UUID
           :
    To reduce the burden of storing and transferring 128-bit UUID values, 
    a range of UUID values has been pre-allocated for assignment to 
    often-used, registered purposes. The first UUID in this pre-allocated
    range is known as the Bluetooth Base UUID and has the value 
    00000000-0000-1000-8000-00805F9B34FB,
           :
    
つまり、Generic Attribute サービスの 16ビット UUID「0x1801」は、128ビット UUID「00001801-0000-1000-8000-00805F9B34FB」です。

Android API を用いて前出の Generic Attribute サービスと Service Changed キャラクタリスティック、およびその配下の Client Characteristic Configuration Descriptor のオブジェクトを取得するコードのイメージを示します。

private BluetoothGatt mBtGatt;

private BluetoothGattCharacteristic mChServiceChanged;
private BluetoothGattDescriptor mCCCD;

// Generic Attribute サービス の UUID
private UUID mUuidSvcGenericAttribute = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb");
// Service Changed キャラクタリスティックの UUID 
private UUID mUuidChServiceChanged    = UUID.fromString("00002a52-0000-1000-8000-00805f9b34fb");
// Client Characteristic Configuration Descriptor の UUID (固定値)
private UUID mUuidCCCD                = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
                 :

private BluetoothGattCallback mGattCallback = new bleGattCallback();
                 :

// デバイス A へ接続後、GATT の提供する各サービス以下の一覧を取得
mBtGatt.discoverServices();(mCtx, false, mGattCallback);
                 :

// GATT イベントハンドラ
private class bleGattCallback extends BluetoothGattCallback {
  @Override
  // GATT サービス一覧取得完了
  public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);

    // デバイス A の Generic Attribute サービスの
    // Service Changed キャラクタリスティックオブジェクトを取得
    BluetoothGattService svc = gatt.getService(mUuidSvcGenericAttribute);
    mChServiceChanged = svc.getCharacteristic(mUuidChServiceChanged);

    // Service Changed キャラクタリスティックの
    // Client Characteristic Configulation Descriptor を取得
    mCCCD = mChServiceChanged.getDescriptor(mUuidCCCD);
                 :
  }

ちなみに、アプリ開発の初期に BluetoothGattCallback の onServicesDiscovered() に次の要領のコードを挿入して 各 GATT サービス配下の全エントリの UUID を階層的に出力し保存しておくと何かと便利です。

public void onServicesDiscovered(BluetoothGatt gatt, int status) {
  super.onServicesDiscovered(gatt, status);

  List<BluetoothGattService> serviceList = gatt.getServices();

  Log.d(TAG, "onServicesDiscovered: serviceList.size=" + serviceList.size());

  for (BluetoothGattService s : serviceList) {
    Log.d(TAG, "onServicesDiscovered: svc uuid=" + s.getUuid().toString());
    List<BluetoothGattCharacteristic> chlist = s.getCharacteristics();
    Log.d(TAG, "onServicesDiscovered: chrlist.size=" + chlist.size());

    for (BluetoothGattCharacteristic c : chlist) {
      UUID uuid = c.getUuid();
      Log.d(TAG, "onServicesDiscovered:  chr uuid=" + uuid.toString());
      List<BluetoothGattDescriptor> dlist = c.getDescriptors();

      Log.d(TAG, "onServicesDiscovered:  desclist.size=" + dlist.size());
      for (BluetoothGattDescriptor d : dlist) {
        Log.d(TAG, "onServicesDiscovered:   desc uuid=" + d.getUuid());
      }
    }
  }

デバイス A への接続後に上のコードを実行した際のログです。(※見やすさのためにサービスごとに改行を挿入)

当然ながら、この内容は前回採取したパケットログ内の各エントリの情報と符合しています。

作成したアプリ

以上の内容にもとづいてアプリを作成しました。ごくシンプルなものですが期待通りに動いています。

ソースコード一式

動画:動作の様子

作成した Android アプリとデバイス A の連携の様子を収めた動画です。デバイス A は UI 部分のみを露出しています。
(34秒 アラーム音あり 音量注意)
    

メモ:実装手順など

private BluetoothAdapter mBtAdapter;
private BluetoothLeScanner mBtScanner;
private BluetoothDevice mBtDevice;
private BluetoothGatt mBtGatt;

1. BluetoothAdapter 〜 BluetoothLeScanner を取得

  • BluetoothAdapter - developer.android.com
    Represents the local device Bluetooth adapter. The BluetoothAdapter lets you
    perform fundamental Bluetooth tasks, such as initiate device discovery, query
    a list of bonded (paired) devices, instantiate a BluetoothDevice using a known
    MAC address, and create a BluetoothServerSocket to listen for connection
    requests from other devices, and start a scan for Bluetooth LE devices.
                      :
    
    static BluetoothAdapter	getDefaultAdapter()
    
    Get a handle to the default local Bluetooth adapter. 
    
  • BluetoothAdapter - getBluetoothLeScanner - developer.android.com
    BluetoothLeScanner getBluetoothLeScanner ()
    
    Returns a BluetoothLeScanner object for Bluetooth LE scan operations. 
    
    • BluetoothLeScanner - developer.android.com
      BluetoothLeScanner
      
      This class provides methods to perform scan related operations for Bluetooth
      LE devices. An application can scan for a particular type of Bluetooth LE
      devices using ScanFilter. It can also request different types of callbacks
      for delivering the result. 
      
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
mBtScanner = mBtAdapter.getBluetoothLeScanner();

2. アドバタイジングパケットのスキャン 〜 対象とする BluetoothDevice を取得

private ScanCallback mScanCallback = new bleScanCallback();
                 :

mBtScanner.startScan(mScanCallback);
                 :

private class bleScanCallback extends ScanCallback {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        if (.......) {
          mBtDevice = result.getDevice();
        }
    }
    @Override
    public void onScanFailed(int errorCode) {
        super.onScanFailed(errorCode);
        Log.e(TAG, "onScanFailed: err=" + errorCode);
    }
}

3. デバイスへの接続

  • BluetoothDevice - connectGatt - developer.android.com
    BluetoothGatt connectGatt (Context context, 
                    boolean autoConnect, 
                    BluetoothGattCallback callback)
    
    Connect to GATT Server hosted by this device. Caller acts as GATT client. 
    The callback is used to deliver results to Caller, such as connection status
    as well as any further GATT client operations. The method returns a
    BluetoothGatt instance. You can use BluetoothGatt to conduct GATT client
    operations.
                      :
    callback 	BluetoothGattCallback: GATT callback handler that will
    receive asynchronous callbacks.
                      :
    
  • BluetoothGattCallback - developer.android.com
    •  onConnectionStateChange - developer.android.com
      void onConnectionStateChange (BluetoothGatt gatt, 
                      int status, 
                      int newState)
      
      Callback indicating when GATT client has connected/disconnected to/from
      a remote GATT server.
                        :
      newState 	int: Returns the new connection state. Can be one of
      STATE_DISCONNECTED or STATE_CONNECTED
                        :
      
private BluetoothGattCallback mGattCallback = new bleGattCallback();
                 :

mBtGatt = mBtDevice.connectGatt(mCtx, false, mGattCallback);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                      int newState) {
    super.onConnectionStateChange(gatt, status, newState);
    if (newState == BluetoothProfile.STATE_CONNECTED) {
      // 接続確立 - デバイスの GATT サービス一覧の取得へ
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
      // 切断完了の処理
    }
  }
                 :

4. GATT サービス - キャラクタリスティック - デスクリプタを探索 〜 必要なオブジェクトを取得

  • BluetoothGatt - discoverServices - developer.android.com
    boolean discoverServices ()
    
    Discovers services offered by a remote device as well as their
    characteristics and descriptors.
    
    This is an asynchronous operation. Once service discovery is completed, 
    the onServicesDiscovered(BluetoothGatt, int) callback is triggered. 
    If the discovery was successful, the remote services can be retrieved
    using the getServices() function. 
                      :
    
  • BluetoothGattCallback - onServicesDiscovered - developer.android.com
    void onServicesDiscovered (BluetoothGatt gatt, 
                    int status)
    
    Callback invoked when the list of remote services, characteristics
    and descriptors for the remote device have been updated, ie new services
    have been discovered.
                      :
    
  • BluetoothGatt - getService - developer.android.com
    getService
    
    BluetoothGattService getService (UUID uuid)
    
    Returns a BluetoothGattService, if the requested UUID is supported by
    the remote device.
    
    This function requires that service discovery has been completed for
    the given device.
    
    If multiple instances of the same service (as identified by UUID) exist,
    the first instance of the service is returned.
    
    Requires BLUETOOTH permission.
    
    Parameters
    uuid 	UUID: UUID of the requested service
    Returns
    BluetoothGattService 	BluetoothGattService if supported, or null if the
                            requested service is not offered by the remote device. 
    
    • BluetoothGattService - getCharacteristic - developer.android.com
      getCharacteristic
      
      BluetoothGattCharacteristic getCharacteristic (UUID uuid)
      
      Returns a characteristic with a given UUID out of the list of
      characteristics offered by this service.
      
      This is a convenience function to allow access to a given characteristic
      without enumerating over the list returned by getCharacteristics()
      manually.
      
      If a remote service offers multiple characteristics with the same UUID,
      the first instance of a characteristic with the given UUID is returned.
      
      Parameters
      uuid 	UUID
      Returns
      BluetoothGattCharacteristic  GATT characteristic object or null if no
                                   characteristic with the given UUID was found. 
      
      
      • BluetoothGattCharacteristic - getDescriptor - developer.android.com
        getDescriptor
        
        BluetoothGattDescriptor getDescriptor (UUID uuid)
        
        Returns a descriptor with a given UUID out of the list of descriptors
        for this characteristic.
        
        Parameters
        uuid  UUID
        Returns
        BluetoothGattDescriptor  GATT descriptor object or null if no
                                 descriptor with the given UUID was found. 
        
private BluetoothGattCharacteristic mChAlertLevel = null;
private BluetoothGattCharacteristic mChUser1 = null;
private BluetoothGattDescriptor mDescUser1 = null;

// デバイス A の提供するサービス・キャラクタリスティック群の UUID より
private UUID mUuidSvcImAlert   = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb");
private UUID mUuidChAlertLevel = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb");
private UUID mUuidSvcUser1     = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb");
private UUID mUuidChUser1      = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb");
// UUID for Client Characteristic Configuration Descriptor
// - BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] page 537
private UUID mUuidCCCD         = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
                 :

private BluetoothGattCallback mGattCallback = new bleGattCallback();
                 :

mBtGatt.discoverServices();(mCtx, false, mGattCallback);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  // GATT サービス一覧取得完了
  public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);

    // デバイス A の Immediate Alert サービスの
    // Alert Level キャラクタリスティックオブジェクトを取得
    BluetoothGattService svc = gatt.getService(mUuidSvcImAlert);
    mChAlertLevel = svc.getCharacteristic(mUuidChAlertLevel);

    // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの
    // Client ​Characteristic Configulation Descriptor を取得
    svc = gatt.getService(mUuidSvcUser1);
    mChUser1 = svc.getCharacteristic(mUuidChUser1);
    mDescUser1 = mChUser1.getDescriptor(mUuidCCCD);
                 :
  }

5. 所定のキャラクタリスティックからの通知を有効に

// デバイス A への Alert 指示用
private byte[] mCmdAlertOff = new byte[] {(byte)0x00}; // OFF (No Alert)
private byte[] mCmdAlertOn  = new byte[] {(byte)0x02}; // ON (High Alert)
                 :

  // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの
  // Client ​Characteristic Configulation Descriptor を取得
  svc = gatt.getService(mUuidSvcUser1);
  mChUser1 = svc.getCharacteristic(mUuidChUser1);
  mDescUser1 = mChUser1.getDescriptor(mUuidCCCD);

  // 同キャラクタリスティックの値変更時の通知を有功にして
  // 同 CCCD へ ENABLE_NOTIFICATION_VALUE を書き込んで通知へ待機
  mBtGatt.setCharacteristicNotification(mChUser1, true);
  byte[] val = new byte[] {(byte)0x01, (byte)0x00};
  mDescUser1.setValue(val);
  mBtGatt.writeDescriptor(mDescUser1);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  public void onCharacteristicChanged (BluetoothGatt gatt,
                                       BluetoothGattCharacteristic ch) {
    Log.d(TAG, "onCharacteristicChanged");
    // デバイス A のユーザ定義キャラクタリスティック 1 からの通知を受信
    if (ch == mChUser1) {
        Toast.makeText(mCtx, "* P U S H E D *", Toast.LENGTH_SHORT).show();
    }
  }
  @Override
  public void onDescriptorWrite (BluetoothGatt gatt,
                                BluetoothGattDescriptor desc,
                                int status) { // writeDescriptor() 結果
    super.onDescriptorWrite(gatt, desc, status);
    Log.d(TAG, "onDescriptorWrite: sts=" + status);
    if (desc == mDescUser1) {
      // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの
      // Client ​Characteristic Configulation Descriptor への書き込みが完了
    }
  }
                 :

6. 所定のキャラクタリスティックへの書き込み

// デバイス A への Alert 指示用
private byte[] mCmdAlertOff = new byte[] {(byte)0x00}; // OFF (No Alert)
private byte[] mCmdAlertOn  = new byte[] {(byte)0x02}; // ON (High Alert)
                 :

  mChAlertLevel.setValue(mCmdAlertOn);
  mBtGatt.writeCharacteristic(mChAlertLevel);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  public void onCharacteristicWrite(BluetoothGatt gatt,
                                    BluetoothGattCharacteristic ch,
                                    int status) { // writeCharacteristic 結果
    super.onCharacteristicWrite(gatt, ch, status);
    if (ch == mChAlertLevel) { 
      Log.d(TAG, "mChAlertLevel: onCharacteristicWrite: sts=" + status);
    }
  }
                 :

(tanabe)
klab_gijutsu2 at 14:28
この記事のURLComments(0)TrackBack(0)Bluetooth | IoT
2016年08月24日

BLE デバイス間の通信内容をパケットレベルで読み解いてみる

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

前回の記事:
パケットスニファを使って実際に BLE 機器間の応酬を追ってみました。備忘をかねて以下に情報を控えます。題材には低価格で出回っているありふれた Anti-Lost 系 BLE デバイス A を選びました。この小さなデバイスには LED・ブザーと押しボタンスイッチが実装されており、対向スマホアプリとの間で双方向のやりとりが可能なつくりになっています。


デバイス A の UI

操作内容

デバイス A と Android 端末を使って以下の操作を行いスニファで BLE パケットのログを採取しました。

  1. デバイス A の電源を入れる
  2. Android 端末上の対向アプリからデバイスを探索しデバイス A との接続を確立
  3. アプリからデバイス A の LED+ブザーを ON にする
  4. デバイス A のボタンを押しアプリへ通知を送る
  5. アプリ上でデバイス A との接続を切断

応酬の全体像

スニファの Wireshark ログより

上の操作時に収集したパケットログの全体図です。   (クリックで大きく表示)

    

要約

パケットログの内容の要約を以下に示します。   (クリックで大きく表示)

  1. デバイス A の電源を入れる
    • デバイス A がアドバタイジングを開始 (ADV_IND)
  2. スマホ上の対向アプリからデバイスを探索しデバイス A との接続を確立
    • デバイスのスキャン (SCAN_REQ / SCAN_RSP) を経て接続要求 (CONNECT_REQ) 〜接続が確立するとデバイス A はアドバタイジングを停止
    • 機能情報を交換 (LL_FEATURE_REQ / LL_FEATURE_RSP)
    • デバイス A の提供する GATT プライマリサービス一覧を取得
    • 各サービス配下のエントリ(Included Service, キャラクタリスティック)一覧を取得
    • 各キャラクタリスティック配下のデスクリプタを取得
    • デバイス A のボタンが押された時に通知されるよう所定の Client Characteristic Configuration descriptor へアプリから Notification Bit 0x0001 を書き込む
  3. アプリからデバイス A の LED+ブザーを ON にする
    • アプリ上の所定のボタンが押されたタイミングでアプリはデバイス A の Immediate Alert サービス配下の Alert Level キャラクタリスティックへ値 2 (High Alert)を書き込む。これにより一定時間 A の LED とブザーが ON になる
  4. デバイス A のボタンを押しアプリへ通知を送る
    • デバイス A のボタンが押されたタイミングでデバイス A は自機のユーザ定義サービス 1 (UUID=0xFFE0) 配下のユーザ定義キャラクタリスティック (UUID=0xFFE1) へ値 1 を書き込む。当該キャラクタリスティック配下の Client Characteristic Configuration descriptor にはアプリ側から予め Notification Bit がセットされているためアプリへ通知される
  5. アプリ上でデバイス A との接続を切断
    • アプリからデバイス A へ LL_TERMINATE_IND を送る

応酬の詳細

全体の流れを見渡したところで次に個々のパケットデータを読み進めていきます。

  • 以下の記事には Wireshark のスクリーンショットをログとして添えています  (それぞれクリックで大きく表示)
  • ログには BLE パケットデータ本体に加え nRF Sniffer が解析の便宜のためにログへ出力する「Nordic BLE sniffer meta」ヘッダが含まれています
    • nRF Sniffer User Guide v1.1 (PDF) - www.nordicsemi.com Page 8 より
      4   Using Wireshark
      
      All BLE packets detected by the Sniffer are passed to Wireshark and are
      wrapped in a header which contains useful meta-information not present
      in the BLE packet itself. Wireshark dissects the packets and separates
      the actual packet from the meta-information.
                   :
      
  • 記事には下記資料からの抜粋へのリンクを参照用に適宜挿入しています
  • 随所に「https://developer.bluetooth.org/gatt/」下のサービス・キャラクタリスティックの記事へのリンクを張っていますが、2016年7月頃までは参照可能だった各記事が現在はリンク切れになっており(再構成中?)、さらに現時点では代替ページが見当たらないためリンク先はやむなく http://web.archive.org/ 上のキャッシュとしています。ただし web.archive.org 上にも保存履歴のない記事については一階層上のページへのリンクを張っています
    例:「Link Loss」サービスの単独ページのキャッシュは web.archive.org にも見当たらないため、上位のサービス一覧ページのキャッシュをターゲットに

局面

Scene 1: デバイス A のアドバタイジングと SCAN_REQ / SCAN_RSP

Frame 124 - 126 はデバイス A 発のアドバタイジングパケット

  • 全 40 チャネルのうちアドバタイジングパケット用の 37, 38, 39 の 3 チャネルが順次使用されていることが見てとれる
  • 「0x8e89bed6」の Access Address はアドバタイジングチャネルパケットで使用される固定値
  • デバイス A のアドバタイジングのタイプは Connectable Undirected(詳細: 1, 2, 3, 4) であることを示す一般的な「ADV_IND」である
  • 「Advertising Data: 020106020a000702031802180418」に含まれる不完全 16ビットサービスクラス UUID は次の内容
    • 16 bit uuid: 0x1803 --> 既定の「Link Loss」サービス
    • 16 bit uuid: 0x1802 --> 既定の「Immediate Alert」サービス
    • 16 bit uuid: 0x1804 --> 既定の「Tx Power」サービス
Frame 124

    

Frame 125

    

Frame 126

    

Frame 127

接続に際し対向アプリがデバイス A に対して SCAN_REQ を発行

    

Frame 128

デバイス A が対向アプリからの SCAN_REQ に対し SCAN_RSP を返信。ここでは ScanRspData としてデバイスのローカルネームが渡されている

    

Scene 2: 接続の確立と情報交換

Frame 217

対向アプリがデバイス A に対して CONNECT_REQ(詳細:1a, 2a, 1b, 2b)を発行して接続を試みる

  • 「Connection Request」中の「Connection Access Address」には、接続確立後のデータ通信において Access Address として双方が一意に使用する任意のアドレスが含まれる。ここでは「0xa6a188c8」
  • 「Connection Request」中の「Channel map」には接続確立後のデータ通信で使用するチャネル番号のリストが提示される。ここでは 0 - 36 の全データチャネルが指定されている
  • その他のパラメータについては上のリンクの資料を参照のこと
  • CONNECT_REQ に対する直接のレスポンスは発生しない。接続が確立するとデータ通信アイドル時にスレーブ - マスタ間で空パケット(Empty PDU)の応酬が始まるためこれが接続成否の判定に用いられる

    

Frame 218, 224 は接続確立後の最初の応酬。接続が確立すると対向アプリはマスタ、デバイス A はスレーブの位置づけとなる。ここでは リンク層制御 PDU の LL_FEATURE_REQ および LL_FEATURE_RSP によりマスタ - スレーブ間で Feature Exchange を行っている。ここでは双方とも「Supported feature: LE Encryption (0)」のみを提示(ただしここでは以降の通信において暗号化は行われていない)
Frame 218

    

Frame 224

    

Frame 222 ではアプリ側が「Device Name」を要求し Frame 226 でデバイス A 側がそれに応えている。
「Device Name」は、既定の Generic Access(GAP)サービス配下の既定のキャラクタリスティック(UUID = 0x2A00)であり、GATT サーバは必ず GAP サービスを含んでいる(詳細:1, 2
Frame 222

  Opcode: Read By Type Request (0x08)
  Starting Handle: 0x0001
  Ending Handle: 0xffff
  UUID: Device Name (0x2a00)

    

Frame 226

  Opcode: Read By Type Response (0x09)
  Length: 6
  Attribute Data, Handle: 0x0003
      Handle: 0x0003
      Value: ********

    

(GAP サービスおよび Device Name キャラクタリスティックは後続の Frame 231, Frame 266 であらためて表に現れる)

Scene 3: デバイス A の GATT サービス群の取得

(Scene 3, Scene 4 共通の基礎知識)
BLUETOOTH SPECIFICATION Version 4.2(PDF) [Vol 3, Part G] より
    2.6 GATT PROFILE HIERARCHY
       2.6.1 Overview, 2.6.2 Service, 2.6.3 Included Services, 2.6.4 Characteristic
  3 SERVICE INTEROPERABILITY REQUIREMENTS
     3.1 SERVICE DEFINITION, 3.2 INCLUDE DEFINITION, 3.3 CHARACTERISTIC DEFINITION
       3.3.1 Characteristic Declaration
         3.3.1.1 Characteristic Properties, 3.3.1.2 Characteristic Value Attribute Handle, 3.3.1.3 Characteristic UUID
       3.3.2 Characteristic Value Declaration

初期処理として、マスタはスレーブの提供するサービス一覧を取得する (詳細:1, 2, 3

  • アプリ側が、デバイス A の GATT サーバの提供する公開サービス(プライマリサービス)の問合せを開始。 以降、GATT サーバ上の所定のサービスへアクセスするためのハンドルのアドレスと当該サービスの種類を識別する UUID を順次取得する
  • ハンドルのアドレス空間は 0x0000 - 0xFFFF であり GATT サーバ側の応答に応じて照会範囲を絞っていく


      (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

Frame 229

まずハンドル 0x0001 - 0xffff 全範囲についてプライマリサービスを照会

    Opcode: Read By Group Type Request (0x10)
    Starting Handle: 0x0001
    Ending Handle: 0xffff
    UUID: GATT Primary Service Declaration (0x2800)

    

Frame 231

以下のみっつのサービスの情報が得られた

  • ハンドルグループ 0x0001 - 0x000b:既定の「Generic Access」サービス(UUID = 0x1800)が使用
  • ハンドルグループ 0x000c - 0x000f:既定の「Generic Attribute」サービス(UUID = 0x1801)が使用
  • ハンドルグループ 0x0010 - 0x0022:既定の「Device Information」サービス(UUID = 0x180A)が使用

    Opcode: Read By Group Type Response (0x11)
    Length: 6
    Attribute Data, Handle: 0x0001, Group End Handle: 0x000b
        Handle: 0x0001
        Group End Handle: 0x000b
        Value: 0018
    Attribute Data, Handle: 0x000c, Group End Handle: 0x000f
        Handle: 0x000c
        Group End Handle: 0x000f
        Value: 0118
    Attribute Data, Handle: 0x0010, Group End Handle: 0x0022
        Handle: 0x0010
        Group End Handle: 0x0022
        Value: 0a18

    

Frame 235

続けてハンドル 0x0023 - 0xffff 範囲のプライマリサービスを照会

    

Frame 238

以下のみっつのサービスの情報が得られた

  • ハンドルグループ 0x0023 - 0x0025:既定の「Immediate Alert」サービス(UUID = 0x1802)が使用
  • ハンドルグループ 0x0026 - 0x002a:既定の「Tx Power」サービス(UUID = 0x1804)が使用
  • ハンドルグループ 0x002b - 0x002d:既定の「Link Loss」サービス(UUID = 0x1803)が使用

    

Frame 242

続けてハンドル 0x002e - 0xffff 範囲のプライマリサービスを照会

    

Frame 247

以下のみっつのサービスの情報が得られた

  • ハンドルグループ 0x002e - 0x0032:既定の「Battery Service」サービス(UUID = 0x180F)が使用
  • ハンドルグループ 0x0033 - 0x0037:ユーザ定義のサービス 1(UUID = 0xFFE0)が使用
  • ハンドルグループ 0x0038 - 0x003a:ユーザ定義のサービス 2(UUID = 0xFFF0)が使用

    

Frame 250

続けてハンドル 0x003b - 0xffff 範囲のプライマリサービスを照会

    

Frame 253

「Attribute Not Found (0x0a)」のエラーが返る。これ以上プライマリサービスが存在しないことを意味する。サービスの照会はここまで

    Opcode: Error Response (0x01)
    Request Opcode in Error: Read By Group Type Request (0x10)
    Handle in Error: 0x003b
    Error Code: Attribute Not Found (0x0a)

    

Scene 4: デバイス A 各サービス配下のキャラクタリスティック - デスクリプタの取得

(Scene 3, Scene 4 共通の基礎知識)
BLUETOOTH SPECIFICATION Version 4.2(PDF) [Vol 3, Part G] より
    2.6 GATT PROFILE HIERARCHY
       2.6.1 Overview, 2.6.2 Service, 2.6.3 Included Services, 2.6.4 Characteristic
  3 SERVICE INTEROPERABILITY REQUIREMENTS
     3.1 SERVICE DEFINITION, 3.2 INCLUDE DEFINITION, 3.3 CHARACTERISTIC DEFINITION
       3.3.1 Characteristic Declaration
         3.3.1.1 Characteristic Properties, 3.3.1.2 Characteristic Value Attribute Handle, 3.3.1.3 Characteristic UUID
       3.3.2 Characteristic Value Declaration

次に、収集ずみの各プライマリサービスのハンドルグループごとに以下を行う (詳細:1, 2, 3, 4, 5, 6

  • 所定のサービスに含まれる Included Service の照会


      (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

  • 所定のサービスに含まれるキャラクタリスティックの照会


      (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

    • 所定のキャラクタリスティックに含まれるデスクリプタの照会


        (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

Frame 255

前掲の Frame 231 の示すハンドルグループ 0x0001 - 0x000b の「Generic Access」サービス内の Included Service を照会

    Opcode: Read By Type Request (0x08)
    Starting Handle: 0x0001
    Ending Handle: 0x000b
    UUID: GATT Include Declaration (0x2802)

    

Frame 259

当該サービス内に Included Service は存在しない(注:図のログではスニファがパケットデータを取りこぼしている)

    

Frame 262

ハンドルグループ 0x0001 - 0x000b の「Generic Access」サービス内のキャラクタリスティックを照会

    Opcode: Read By Type Request (0x08)
    Starting Handle: 0x0001
    Ending Handle: 0x000b
    UUID: GATT Characteristic Declaration (0x2803)

    

Frame 266

以下のみっつのキャラクタリスティックの情報が得られた

(※キャラクタリスティック の Value フィールドの構成は前掲の「BLUETOOTH SPECIFICATION Version 4.2] - [Vol 3, Part G] p.532 「3.3.1 Characteristic Declaration」 に、プロパティ値の意味は同じく p.533 「3.3.1.1 Characteristic Properties」 に説明あり)

  • ハンドル:0x0002
    「value: 080300002a」より、プロパティ = 0x08 (Write), Characteristic Value Handle = 0x0003
    UUID = 0x2A00 = 既定の「Device Name
  • ハンドル:0x0004
    「value: 020500012a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0005
    UUID = 0x2A01 = 既定の「Appearance
  • ハンドル:0x0006
    「value: 020700042a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0007
    UUID = 0x2A04 = 既定の「Peripheral Preferred Connection Parameters

    Opcode: Read By Type Response (0x09)
    Length: 7
    Attribute Data, Handle: 0x0002
      Handle: 0x0002
      Value: 080300002a
    Attribute Data, Handle: 0x0004
      Handle: 0x0004
      Value: 020500012a
    Attribute Data, Handle: 0x0006
      Handle: 0x0006
      Value: 020700042a

    

Frame 270

続けてハンドル 0x0007 - 0x000b 範囲のキャラクタリスティックを照会

    

Frame 275

以下のふたつのキャラクタリスティックの情報が得られた

  • ハンドル:0x0008
    「value: 020900022a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0009
    UUID = 0x2A02 = 既定の「Peripheral Privacy Flag
  • ハンドル:0x000a
    「value: 020b00032a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x000b
    UUID = 0x2A03 = 既定の「Reconnection Address

    

Frame 279

前掲の Frame 231 の示すハンドルグループ 0x000c - 0x000f の「Generic Attribute」サービス内の Included Service を照会

    

Frame 284

当該サービス内に Included Service は存在しない

    

Frame 287

ハンドルグループ 0x000c - 0x000f の「Generic Attribute」サービス内のキャラクタリスティックを照会

    

Frame 289

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x000d
    「value: 200e00052a」より、プロパティ = 0x20 (Indicate), Characteristic Value Handle = 0x000e
    UUID = 0x2A52 = 既定の「Service Changed

    

Frame 293

続けてハンドル 0x000e - 0x000f 範囲のキャラクタリスティックを照会

    

Frame 298

当該サービス内にはこれ以上キャラクタリスティックは存在しない

    

Frame 302

Frame 289 の示す通り 0x0000e は Service Changed キャラクタリスティックの Value Handle であり、残る0x000f に関する情報を GATT に問い合わせてみる (詳細:1, 2

    Opcode: Find Information Request (0x04)
    Starting Handle: 0x000f
    Ending Handle: 0x000f

    

Frame 305

0x000f は Service Changed キャラクタリスティックの Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    Opcode: Find Information Response (0x05)
    UUID Format: 16-bit UUIDs (0x01)
    Handle: 0x000f
    UUID: Client Characteristic Configuration (0x2902)

    

Frame 308

前掲の Frame 231 の示すハンドルグループ 0x0010 - 0x0022 の「Device Information」サービス内の Included Service を照会

    

Frame 312

当該サービス内に Included Service は存在しない

    

Frame 315

ハンドルグループ 0x0010 - 0x0022 の「Device Information」サービス内のキャラクタリスティックを照会

    

Frame 319

以下のみっつのキャラクタリスティックの情報が得られた

  • ハンドル:0x00011
    「value: 021200292a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0012
    UUID = 0x2A29 = 既定の「Manufacturer Name String
  • ハンドル:0x00013
    「value: 021400242a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0014
    UUID = 0x2A24 = 既定の「Model Number String
  • ハンドル:0x00015
    「value: 021600252a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0014
    UUID = 0x2A25 = 既定の「Serial Number String

    

Frame 323

続けてハンドル 0x0016 - 0x0022 範囲のキャラクタリスティックを照会

    

Frame 326

以下のみっつのキャラクタリスティックの情報が得られた

  • ハンドル:0x00017
    「value: 021800262a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0018
    UUID = 0x2A26 = 既定の「Firmware Revision String
  • ハンドル:0x00019
    「value: 021a00272a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x001a
    UUID = 0x2A27 = 既定の「Hardware Revision String
  • ハンドル:0x0001b
    「value: 021c00282a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x001c
    UUID = 0x2A28 = 既定の「Software Revision String

    

Frame 330

続けてハンドル 0x001c - 0x0022 範囲のキャラクタリスティックを照会

    

Frame 333

以下のみっつのキャラクタリスティックの情報が得られた

  • ハンドル:0x0001d
    「value: 021e00232a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x001e
    UUID = 0x2A23 = 既定の「System ID
  • ハンドル:0x0001f
    「value: 0220002a2a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0020
    UUID = 0x2A2A = 既定の「IEEE 11073-20601 Regulatory Certification Data List
  • ハンドル:0x00021
    「value: 022200502a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0022
    UUID = 0x2A50 = 既定の「PnP ID

    

Frame 337

前掲の Frame 238 の示すハンドルグループ 0x0023 - 0x0025 の「Immediate Alert」サービス内の Included Service を照会
(注:このリクエストに対する正しいレスポンスは「Attribute Not Found (0x0a)」だが、スニファ取りこぼしのためログが欠落している)

    

Frame 341

ハンドルグループ 0x0023 - 0x0025 の「Immediate Alert」サービス内のキャラクタリスティックを照会

    

Frame 345

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0024
    「value: 042500062a」より、
    プロパティ = 0x04 (Write without Response),
    Characteristic Value Handle = 0x0025
    UUID = 0x2A06 = 既定の「Alert Level

    

Frame 349

前掲の Frame 238 の示すハンドルグループ 0x0026 - 0x002a の「Tx Power」サービス内の Included Service を照会
(注:このリクエストに対する正しいレスポンスは「Attribute Not Found (0x0a)」だが、スニファ取りこぼしのためログが欠落している)

    

Frame 355

ハンドルグループ 0x0026 - 0x002a の「Tx Power」サービス内のキャラクタリスティックを照会

    

Frame 359

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0027
    「value: 122800072a」より、プロパティ = 0x12 (Read | Notify), Characteristic Value Handle = 0x0028
    UUID = 0x2A07 = 既定の「Tx Power Level

    

Frame 362

続けてハンドル 0x0028 - 0x002a 範囲のキャラクタリスティックを照会

    

Frame 366

当該サービス内にはこれ以上キャラクタリスティックは存在しない

    

Frame 369

Frame 359 の示す通り 0x00028 は Tx Power Level キャラクタリスティックの Value Handle であり後続の 0x0029 - 0x002a に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 373

0x0029 は Tx Power Level キャラクタリスティックの Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    

Frame 376

残る0x002a に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 380

0x002a は Tx Power Level キャラクタリスティックの Characteristic Presentation Format Descriptor (UUID = 0x2904) (詳細:1, 2, 3, 4) である旨の情報が得られた

    Opcode: Find Information Response (0x05)
    UUID Format: 16-bit UUIDs (0x01)
    Handle: 0x002a
    UUID: Characteristic Presentation Format (0x2904)

    

Frame 384

前掲の Frame 238 の示すハンドルグループ 0x002b - 0x002d の「Link Loss」サービス内の Included Service を照会
(注:このリクエストに対する正しいレスポンスは「Attribute Not Found (0x0a)」だが、スニファ取りこぼしのためログが欠落している)

    

Frame 390

ハンドルグループ 0x002b - 0x002d の「Link Loss」サービス内のキャラクタリスティックを照会

    

Frame 392

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x002c
    「value: 0a2d00062a」より、プロパティ = 0x0a (Read | Write), Characteristic Value Handle = 0x002d
    UUID = 0x2A06 = 既定の「Alert Level

    

Frame 395

前掲の Frame 247 の示すハンドルグループ 0x002e - 0x0032 の「Battery Service」サービス内の Included Service を照会

    

Frame 397

当該サービス内に Included Service は存在しない

    

Frame 401

ハンドルグループ 0x002e - 0x0032 の「Battery Service」サービス内のキャラクタリスティックを照会

    

Frame 404

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x002f
    「value: 123000192a」より、プロパティ = 0x12 (Read | Notify), Characteristic Value Handle = 0x0030
    UUID = 0x2A19 = 既定の「Battery Level

    

Frame 407

続けてハンドル 0x0030 - 0x0032 範囲のキャラクタリスティックを照会

    

Frame 411

当該サービス内にこれ以上キャラクタリスティックは存在しない

    

Frame 415

Frame 404 の示す通り 0x00030 は Battery Level キャラクタリスティックの Value Handle であり後続の 0x0031 - 0x0032 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 420

0x0031 は Battery Level キャラクタリスティックの Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    

Frame 423

残る0x0032 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 427

0x0032 は Battery Level キャラクタリスティックの Characteristic Presentation Format Descriptor (UUID = 0x2904) (詳細:1, 2, 3, 4) である旨の情報が得られた

    

Frame 431

前掲の Frame 247 の示すハンドルグループ 0x0033 - 0x0037 のユーザ定義サービス 1(UUID = 0xFFE0)内の Included Service を照会

    

Frame 434

当該サービス内に Included Service は存在しない

    

Frame 437

ハンドルグループ 0x0033 - 0x0037 のユーザ定義サービス 1 内のキャラクタリスティックを照会

    

Frame 439

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0034
    「value: 103500e1ff」より、
    プロパティ = 0x10 (Notify),
    Characteristic Value Handle = 0x0035
    UUID = 0xFFE1 : ユーザ定義のキャラクタリスティック

    

Frame 442

続けてハンドル 0x0035 - 0x0037 範囲のキャラクタリスティックを照会

    

Frame 446

当該サービス内にこれ以上キャラクタリスティックは存在しない

    

Frame 450

Frame 439 の示す通り 0x00035 はユーザ定義キャラクタリスティック(UUID = 0xFFE1)の Value Handle であり後続の 0x0036 - 0x0037 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 453

0x0036 はユーザ定義キャラクタリスティック(UUID = 0xFFE1)の Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    

Frame 456

残る0x0037 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 458

0x0037 はユーザ定義キャラクタリスティック(UUID = 0xFFE1)の Characteristic User Description Descriptor (UUID = 0x2901)である旨の情報が得られた

    

Frame 462

前掲の Frame 247 の示すハンドルグループ 0x0038- 0x003a のユーザ定義サービス 2(UUID = 0xFFF0)内の Included Service を照会

    

Frame 465

当該サービス内に Included Service は存在しない

    

Frame 468

ハンドルグループ 0x0038 - 0x003a のユーザ定義サービス 2 内のキャラクタリスティックを照会

    

Frame 470

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0039
    「value: 043a00f1ff」より、プロパティ = 0x04 (Write without Response), Characteristic Value Handle = 0x003a
    UUID = 0xFFF1 : ユーザ定義のキャラクタリスティック

    

Scene 5: デバイス A ボタン押下時のアプリへの通知を設定

Frame 491

Frame 453Frame 439Frame 453 の示す通り、ハンドル 0x0036 は、 "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)配下の Client Characteristic Configuration Descriptor (CCCD)" である

当該ユーザ定義キャラクタリスティック(UUID = 0xFFE1)のプロパティには Notify が設定されており、クライアントである対向アプリから CCCD 0x0036 に Notification bit (0x0001) を書き込んでおくことで、このキャラクタリスティックの値が更新された時に Value Handle である 0x0035 経由でアプリ側へ通知(Notification)が行われるようになる
(詳細: 1a, 2a, 1b, 2b

    Opcode: Write Request (0x12)
    Handle: 0x0026
    Value: 0100

    

Frame 495

    Opcode: Write Response (0x13)

    

Scene 6: アプリからデバイス A の LED・ブザーを操作

Frame 616

対向アプリ上の所定のボタンを押下すると、Frame 345, Frame 238 の示す、 "「Immediate Alert」サービス配下の「Alert Level」キャラクタリスティック" の Value Handle 0x0025 に値「2(High Alert)」が書き込まれる

デバイス A の LED・ブザーはファームウェアレベルでこの Alert Level キャラクタリスティックに紐づけられている模様。High Alert 値が書き込まれるとそれに反応して短時間 LED とブザーが ON になる

なお、Frame 345 の示すように Alert Level キャラクタリスティックのプロパティには「Write without Response」(0x04) が設定されているため、アプリから値を書き込んでもデバイス A からのレスポンスは発生しない

    Opcode: Write Command (0x52)
    Handle: 0x0025
    Value: 02

    

Scene 7: デバイス A のボタンを押すとアプリへ通知

Frame 711

デバイス A の物理ボタンはファームウェアレベルで Frame 439 の示す "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)" に紐づけられている模様。このボタンを押すと当該キャラクタリスティックの値がデバイス内部で更新され Frame 491 での仕込みに基づきアプリ側へ通知が行われる

    Opcode: Handle Value Notification (0x1b)
    Handle: 0x0035
    Value: 01

    

Scene 8: アプリ側からデバイス A との接続を切断

対向アプリ上の所定のボタンを押下するとリンク層制御 PDU の LL_TERMINATE_IND (0x02) がデバイス A 側に送出され両者間の接続が終了する(詳細:1, 2

Frame 779

    


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