@johtani さんと弊社サービスの Elasticsearch について話しました(実装編1)

前回のエントリで、弊社サービス Re:lation が Elasticsearch をどういう感じで使っているのかを、主にインフラにまつわる部分を中心に書きました。

blog.ingage.jp

今回は実装に関わる部分を書いてみたいと思います。

前回の記事も含めて今回の記事につきましても、誤っている箇所がありましたら是非ご指摘ください。

形態素解析かN-gramか?

リリース当初は、日本語検索なんだから、 kuromoji っしょ、と思い、 kuromoji を使うことにしました。 kuromoji は形態素解析器です。ドキュメントの本文も、検索キーワードも形態素にわけて検索します(厳密に言えば analyzer の設定次第なんでしょうが)。

しかし問題がおこりました。

「大阪大学」というキーワードで検索すると、「大阪」で検索した結果や「大学」で検索した結果も含まれてしまうのです。メールボックスを検索する上であまりうれしくはありません。当然スコアが高い順に表示したら「大阪大学」で検索したものが上にくると思いますが、感覚的にもご理解いただけると思いますが、メールの検索ではスコア順ではなく最近のメールから表示してほしいです。また、自分のメールボックスを検索する場合は完全一致がうれしいだろうと思います。RDBMSで言うところの LIKE 検索がしたいのです。

そこで N-gram です。N-gram そのものの説明は割愛します。 最低2文字以上で検索するのであれば bi-gram (2-gram) でもいいのですが、1文字で検索したいことはあります。「橘さん」からのメールはとりあえず「橘」で検索したくなるでしょう。Tokenizer は以下のように設定しています。

            tokenizer: {
              message_ngram_tokenizer: {
                type:        "ngram",
                min_gram:    1,
                max_gram:    2,
                token_chars: [ "letter", "digit", "punctuation", "symbol" ]
              }
            }

N-gram を使うメリットとしては、

  • LIKE 検索が実現できる
  • 未知語については考えなくていい
  • kuromoji と違って elasticsearch に内包されている

があり、

デメリットとしては、

  • インデックスサイズが大きくなる
  • 類義語検索やあいまい検索ができない

があげられると思います。

実は kuromoji を使った場合も EXTENDED という、未知語は uni-gram (1-gram) に分割するという形態素解析とN-gramのハイブリッドのようなモードがあります。 これは当時試してはみましたが、詳しくは忘れましたが思うように検索できないことがあり断念しました。おそらく上述の「大阪大学」のような未知語ではないワードが分割され検索されてしまったのだと思います。

今回のエントリはここまで。

@johtani さんと弊社サービスの Elasticsearch について話しました

https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt05047fdbe3b9c333/5c11ec1f3312ce2e785d9c30/logo-elastic-elasticsearch-lt.svg

@kizashi1122 こと、永田です。

blog.johtani.info

@johtani さんがツイートするもんだから、あいよっと返事してしまいました。

以下、@johtani さんに話したことも話してないこともツラツラと書いていこうと思います

はじめに

弊社のサービスである「Re:lation」はメールやチャットや電話メモなどを一元的に管理できるサービスです。 「そういえば○○なメール来てたはずだけど・・・」などという時のために当然検索は必要になります。RDBMS の LIKE 検索でもいいわけですが、インデックスが効かないしデータが増えるとパフォーマンスがでないのは見えていたので、検索エンジンは専用のソフトウェアを使うべきとは思っていました。当時は世の中では Solr も現役だったのですが、同じ Lucene をエンジンとするミドルウェアでも Elasticsearch が伸びてきているのもあり、Elasticsearch を選択しました。

Elasticsearch on EC2 での運用からスタート

「Re:lation」はリリース当初(2014年)から検索機能はありました。 当時は、Amazon Elasticsearch Service はなく、自前で EC2 上に Elasticsearch を立ててました。最初はなんと1台構成です。余裕でSPOFです。使えなくなるのは検索機能だけですけど。

リリース当時の Elasticsearch のバージョンは当時の最新で v1.3.4 でした。 その後、v1.3.6, v1.7.0, v1.7.1, v2.3.2 とスキを見てはバージョンアップし、その後はだいぶとサボって2018 年の初めに v5.6.5 に上げました。

その頃にはとっくに Amazon Elasticsearch Service がローンチしており(2015年にリリース)、もう十分運用されていることもあり、去年の11月にようやく移行することができました(エンジンのバージョンは v7.1)

Amazon Elasticsearch Service への移行

これはかなり頭を悩ませました。 スナップショットをとって移行すべきか? バージョンが違うと互換性はどうなるのか?

Bit Journey 社の@michiomochi こと道川さんにも相談しましたが、やはりインデックスを作り直すのがいいだろうということになりました。一からドキュメントのインデクシングをするわけなので、互換性については考慮する必要がないのはメリットです。

つまり旧環境で運用しつつ、新環境(Amazon Elasticsearch Service)にDBからデータをインポートするバッチを流すわけです。当然、運用しながらなので旧環境にしか反映されないデータもあるわけですが、初回のインポートが完了後、データベースと新環境のインデックスの差分をみて、差分があれば吸収するバッチを何度も動かして、同期をとっていきます。ほぼ同期がとれたところで、アプリからの参照先を新環境に向け、その後再度同じバッチを流して完了です。ダウンタイムはありません。

この1からインデックスをつくってデータを流し込み、同期をとっていく作業に3週間くらいかかりました。

インデックス設計

Re:lation はマルチテナント型のウェブアプリケーションです。

blog.ingage.jp

テナントごとに検索できる必要があるわけです。 どうやってインデックスの設計をするかが難しいところです。

最初は、どこかで読んだ「1テナント:1インデックスにすべし」という記事を鵜呑みにして、何も考えずにそうしました。 というかこれで特に問題はありませんでした。なんなら、Github の elasticsearch-rails のリポジトリのイシューに投稿されるこの手の疑問には「1テナント:1インデックスがいいよー」と私が答えてました。

https://github.com/elastic/elasticsearch-rails/issues/321 https://github.com/elastic/elasticsearch-rails/issues/359

ただ、顧客(=テナント)が増えると当然インデックスも増えてくるわけで、インデックス多すぎ問題が出てきます。 当時で300弱くらいでした。実のところ、インデックスが多すぎによる問題は発生していませんでしたが、今後のことを考え設計を見直しました。 Elasticsearch が管理する対象のインデックスが多くなれば負荷も上がっていくことが予想されます。

次に考えられるのは「データは1インデックスにして、ドキュメントの属性としてテナント識別子を持つ」というやり方です。毎回検索条件にテナント識別子を含めればいいよねという考え方です。

ただデータ量が相当あったので、インデックスは5つにして、テナントは tenant_id で割り振っていくことにしました。シャード数は6だったかな。シャード を限定したアクセスが可能なルーティングという機能があるのを知り、使うことにしました。ルーティングを使えば、エイリアスにルーティング( _routing )を含めることができるため、物理インデックスを意識しなくても、検索条件に明示的にテナント情報を含めなくてもアクセスできます。

      actions = [{
        add:
          {
            index: real_index_name,
            alias: alias_name,
            routing: tenant_id,
            filter: { term: { tenant_id: tenant_id } }
          }
      }]
      client.indices.update_aliases(body: { actions: actions })

今は基本的な構成はこの頃から変わってないですが、データ量増加にともない、インデックス数は5ですが1インデックスあたりのシャード数は8としています。(1シャードあたりのデータ量は大きくしたくない) また今はルーティング機能による高速化は実感できなかったため素直にルーティングは外しました。

      actions = [{
        add:
          {
            index: real_index_name,
            alias: alias_name,
            filter: { term: { tenant_id: tenant_id } }
          }
      }]
      client.indices.update_aliases(body: { actions: actions })

このあたり正解がなく手探りなところが辛いです。

課題

Amazon Elasticsearch Service ではインスタンスストレージが使える i3 シリーズを使っています。インスタンスストレージはEBSのようにネットワーク越しでディスクを使うわけではなく、PCIバスなどによりサーバーに直接つながっているので高速です(ただしディスクサイズは自由に決められなかったり、揮発性であるというリスクはあります)これにより大幅な検索パフォーマンスの改善ができました。お金の力すごい。

ただ、とはいえ検索遅いクエリなどはまだあったりします。ここは問題です。Elasticsearch では「一定時間で検索したところまでを返却する」ということはできますが、ウチの用途ではさすがにこれは許されません。

複数のインデックスへの割り振りは単純に tenant_id でおこなっていると上述しましたが、当然、ヘビーに使うテナントがあるインデックスに偏るということはあります。平準化が難しいです。

大谷さんにはもっと細かい実装時のお話もしましたが、それはまた別のエントリにわけたいと思います。

scp コマンドにまつわる不思議な挙動を追う

こんにちは、masm11 です。

scp で大きなファイルを転送すること、ありますよね。

私も時々やるのですが、今回、ログアウトすると scp が通信しなくなる、 という妙な症状に見舞われましたので、ご紹介します。

症状

状況としては、まず、3台のコンピュータがあります。

f:id:masm11:20200209235930p:plain

A から B に ssh でログインし、scp コマンドを使って C にあるファイルを B にコピーします。

A$ ssh B
B$ nohup scp C:bigfile.tar.gz . < /dev/null >& /dev/null &

こんな感じですね。zsh を使っているので >& を使っていますが、 bash だと2行目は、

B$ nohup scp C:bigfile.tar.gz . < /dev/null > /dev/null 2>&1 &

になるでしょう。

そして、B の bigfile.tar.gz が大きくなっていくのを以下のようにして 見ていました。

B$ watch -n 1 ls -l bigfile.tar.gz

こうすると、一秒おきに、画面をクリアして ls -l bigfile.tar.gz を実行してくれます。

さて、この後、ひょんなことから、A の端末が落ちてしまいました。

まぁそんなことはよくあることです。 気を取り直して、再び B に ssh して watch コマンドを実行してみたところ、 ファイルサイズが大きくなっていかなくなっていました。

nohup を付けていたのに、scp コマンドが終了してしまったのか? と思い、 ps auxww | grep scp してみたところ、プロセスは生きていました。 B でも C でも確認しました。

プロセスが残っているのに、データが転送されなくなっています。

コネクションが詰まってしまったのでしょうか? B で scp コマンドを kill で殺したところ、C でも scp プロセスがいなくなりました。 コネクションには問題なかったようです。

以上をもう一度試してみたところ、完全に再現しました。再現性はあるようです。

検証

いくつか気になる点がありますので、列挙してみます。

  • nohup はちゃんと仕事をしているのか?
  • SIGHUP を本当に受け取ったのか?
  • scp は独自の SIGHUP ハンドラを設定しているのか?
  • scp は SIGHUP を受けて何かするのか?
  • scp は止まった状態で何をしていたのか?
  • scp のマニュアルに記載はあるか?
  • この症状に zsh は関係するのか?

以下、これらを一つずつ調べていきます。

nohup はちゃんと仕事をしているのか?

nohup を付けると、以下のように出力されますよね。

luna:/tmp % nohup ls
nohup: 入力を無視し、出力を 'nohup.out' に追記します
luna:/tmp % 

標準入力は /dev/null になり、標準出力、標準エラー出力は nohup.out になります。

しかし、nohup の仕事はこれだけではありません。 SIGHUP という signal を無視するようにする、という重要な仕事があります。

これが行われているのかどうかを確認してみました。

C で以下のコードを書きます。

#include <stdio.h>
#include <signal.h>

static void handler(int signo)
{
}

int main(void)
{
    printf("%p\n", signal(SIGHUP, handler));
    return 0;
}

signal() 関数は、新しい signal ハンドラ を設定すると共に、 旧 signal ハンドラを返してくれますので、その値を出力します。

これを test.c というファイル名で保存し、コンパイル、実行してみます。

すると、以下のように出力されます。

luna:~ % ./test 
(nil)
luna:~ % 

では nohup を付けてみます。

luna:~ % nohup ./test 
nohup: 入力を無視し、出力を 'nohup.out' に追記します
luna:~ % cat nohup.out
0x1
luna:~ % 

0x1 ですね。これが何かと言うと、

luna:~ % grep SIG_IGN /usr/include/bits/signum-generic.h
#define    SIG_IGN  ((__sighandler_t)  1)  /* Ignore signal.  */
luna:~ % 

これです。signal を無視する設定 SIG_IGN の値は 1 なのです。 これが出力されたんですね。

このように、nohup は

  • 標準入力を /dev/null にする
  • 標準出力、標準エラー出力は nohup.out にする
  • SIGHUP を無視するようにする
  • これらを設定した上で、指定されたプログラムを実行する

という仕事をしているのです。

問題ないようですね。

SIGHUP を本当に受け取ったのか?

signal を受け取ったかどうかを確認するには、strace を使うのが手っ取り早いです。

B$ strace -f -o log nohup scp C:bigfile.tar.gz . < /dev/null >& /dev/null &
[1] 732184
B$

こうすると、strace の子孫プロセス全てにわたって (-f)、システムコールの状況が log ファイルに (-o log) 出力されます。また、signal を受け取った時はその signal 番号も出力してくれます。

出力から一部を抜き出すと、以下のようになっていました。

732460 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732179, si_uid=1000} ---
732460 select(7, [3 4], [], NULL, NULL <unfinished ...>
732459 <... poll resumed>)              = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
732459 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732179, si_uid=1000} ---
732459 kill(732460, SIGHUP)             = 0
732460 <... select resumed>)            = ? ERESTARTNOHAND (To be restarted if no handler)
732459 wait4(732460,  <unfinished ...>
732460 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732459, si_uid=1000} ---

確かに SIGHUP を受け取っているようです。 SIG_IGN に設定されていると、

732460 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732179, si_uid=1000} ---

という出力はないはずだからです。

scp コマンドが独自の SIGHUP ハンドラを設定しているのでしょうか?

scp は独自の SIGHUP ハンドラを設定しているのか?

これについては、先程の strace のログをもう一度見てみましょう。ログの上の方に、

732459 rt_sigaction(SIGHUP, {sa_handler=0x556e9127d810, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f4df345cfb0},  <unfinished ...>

こんな行がありました。sa_handler= がいかにもプログラムの中っぽいアドレスになっています。

どうやら、自前のハンドラを設定しているようです。 これでは、nohup を付けても意味はないですね。

scp は SIGHUP を受けて何かするのか?

これについては、多少推測する程度しかできません。

先程の strace のログを再度引用すると、

732460 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732179, si_uid=1000} ---
732460 select(7, [3 4], [], NULL, NULL <unfinished ...>
732459 <... poll resumed>)              = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
732459 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732179, si_uid=1000} ---
732459 kill(732460, SIGHUP)             = 0
732460 <... select resumed>)            = ? ERESTARTNOHAND (To be restarted if no handler)
732459 wait4(732460,  <unfinished ...>
732460 --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=732459, si_uid=1000} ---

おそらく scp は2つのプロセスで成り立っていて、732459 が親プロセス、732460 が子プロセスです。 そして、732459 が SIGHUP を受け取った後、kill(732460, SIGHUP) で 732460 に SIGHUP を送って います。

つまり、親プロセスが SIGHUP を受け取ったら、それを子プロセスにも送る、ということですね。 よくある構造です。

その他にも何かやっているかもしれませんが、これ以上は判りませんでした。

scp は止まった状態で何をしていたのか?

これもほとんど手がかりはないのですが、

B$ ss -n | grep :22 
tcp     ESTAB        0        0                         192.168.86.132:36134                       x.x.x.x:22
tcp     ESTAB        0        0                         192.168.86.132:22                         192.168.86.150:51614
B$

ソケットのバッファに何も溜まっていません。このことから、そもそも送信側 (C) が送ってなさそう、と考えられます。

scp のマニュアルに記載はあるか?

http://man7.org/linux/man-pages/man1/scp.1.html

ここにマニュアルがあります。このページから hupsignal でページ内検索してみましたが、引っかかりません。 記載はないようです。

https://linux.die.net/man/1/ssh

こちらに ssh のマニュアルがあります。scp は ssh を使っているので、もしや、と思いましたが、 こちらにも記載はありませんでした。

この症状に zsh は関係するのか?

man zshall して SIGHUP で検索してみたところ、記載がありました。 引用します。

       When  you  try  to leave the shell while jobs are running or suspended,
       you will be warned that `You have suspended (running) jobs'.   You  may
       use  the  jobs command to see what they are.  If you do this or immedi‐
       ately try to exit again, the shell will not warn you a second time; the
       suspended  jobs will be terminated, and the running jobs will be sent a
       SIGHUP signal, if the HUP option is set.

zsh を終了しようとした時、実行中のジョブには SIGHUP が送られるようです。 ただし、HUP オプションがセットされている場合。だそうです。

解決策 (回避策)

さて、ここまでいくつか検証してみた結果、

  • nohup は仕事をしているが、
  • scp が独自のハンドラを設定している。
  • zsh を抜ける時に SIGHUP が送られ、
  • SIGHUP を受け取った scp は
  • 独自のハンドラで何らかの処理を行い、
  • その結果、何故か転送が止まってしまう

ということがわかりました。

何故転送が止まってしまうかについては、マニュアルにも記載がなく、わかりませんでした。

しかし、ここまでわかれば回避はできます。

B で、zsh に

B$ unsetopt HUP

と設定すれば、zsh 終了時に SIGHUP が送られることはなくなります。

また、間に端末 (tty) を挟まなければ OK のはず、と思ったので、

A$ ssh B nohup scp C:bigfile.tar.gz . < /dev/null >& /dev/null &

これでも回避できました。ssh はコマンドラインに直接コマンドを書けば、 tty なしでコマンドを実行してくれます。

私が回避した時には、zsh が絡んでいるとは思っていなかったので、後者の方法で回避し、 以下のように問題なく最後まで転送できました。

B$ ls -l bigfile.tar.gz 
-rw-r--r-- 1 masm masm 2750186558  29 21:04 bigfile.tar.gz
B$

(たかが 3GB 弱です。ネットワーク遅いと萎えますよね…)

まとめ

結局、回避はできましたが、根本的に通信だけ止まっている原因は不明のまま残ってしまいました。 今後解決できるといいのですが…

ではまた!

ラズパイHomeKitカメラをUSBカメラに変更してみる

f:id:TedWada:20200203130554j:plain

こんにちは。Tedです。 前回、下記のようにラズパイ4を使ってHomeKit対応カメラを作りました。

blog.ingage.jp

作成したシステムを使っていく内に、いくつか気になるところがでてきました。

気になる点

  • カメラの画角にて広角が足りない。
  • Mac, iPhoneなどからHomeアプリで接続する際、タイムアウトで繋がらないことがよく起こる。

そこで...

そこで、下記を行うことにしました。

  • 広角カメラに換える。そのカメラはラインアップ豊富なWin, Mac用USBカメラ(Webカメラ)を使う。

    Win, Mac用のWebカメラであれば比較的安価に広角カメラが手に入ります。今回は以下のWebカメラを使うことにしました。

    BSW200MBK : Webカメラ | バッファロー

  • 問題を切り分けるために、別のHomebridge用カメラプラグインを使ってみる。

    タイムアウトの原因を探るために、別のカメラプラグインでの動作を試すことにします。また、前回利用したhomebridge-camera-rpiプラグインはUSBカメラに対応していない(ビデオソースの選択ができない)ため、USBカメラを使うためにもプラグインの変更の必要がありました。 今回使用するプラグインは homebridge-camera-ui としました。

まずは不要なモジュールの削除から

まずは前回構築したシステムから、今回不要なもの、残しておくと衝突が起きそうなものを削除します。 具体的には下記を行います。

  • カメラモジュールをオフにする。
  • カメラプラグインを削除する。

それぞれ以下のように進めます。

カメラインタフェースをオフにする

pi@raspberrypi:~ $ sudo raspi-config

f:id:TedWada:20191223180809p:plain
「5 Interfacing Options」を選択。

f:id:TedWada:20191223180901p:plain
「P1 Camera」を選択。

f:id:TedWada:20200131212516p:plain
カメラインタフェースを削除するので「No」を選択。

f:id:TedWada:20200131212621p:plain
カメラインタフェースが削除された。「Enter」を押す。

f:id:TedWada:20200131212828p:plain
「Finish」を選択。

f:id:TedWada:20200131212914p:plain
「Yes」を選択してリブートする。

これでシステムからカメラインタフェースは削除されました。 本体に接続しているカメラモジュールのケーブルも取り外してOKです。(その際は電源OFFで行ってください)

カメラプラグインを削除する

いったんHomebridgeを停止

pi@raspberrypi:~ $ ps -aux | grep homebridge  #homebridgeプロセスIDを表示
root       432  0.2  1.8 181880 72148 ?        Sl   Dec18  36:25 homebridge  #この場合プロセスIDは 432

pi@raspberrypi:~ $ sudo kill <killしたいPID(上記の場合は432>  #いったんHomebridgeプロセスを終了

config.jsonから該当設定を削除

Homebridge設定ファイルであるconfig.jsonから削除するプラグインに該当する設定を削除します。 他にHomebridgeプラグインを使っていない場合は、いったんconfig.jsonを削除してもよいかと思います。

編集する場合:

pi@raspberrypi:~ $ vi .homebridge/config.json

削除する場合:

pi@raspberrypi:~ $ rm .homebridge/config.json

プラグインをアンインストール

pi@raspberrypi:~ sudo npm uninstall -g homebridge-camera-rpi

新たにシステムを構築

USBカメラの取り付け

WebカメラをUSB端子に接続したら、そのカメラが使えるか確認を行います。

pi@raspberrypi:~ $ lsusb  #USB接続されているデバイス一覧を表示

...
Bus 001 Device 003: ID 0458:708c KYE Systems Corp. (Mouse Systems) Genius WideCam F100
...

このようにカメラが認識されていればOKです。

次にOSがビデオデバイスとして認識しているのかを確認します。

pi@raspberrypi:~ $ ls /dev/video*

/dev/video0  /dev/video1  /dev/video10  /dev/video11  /dev/video12

上記のように /dev/video から始まるデバイスが表示されればOKです。

homebridge-camera-uiのインストール

pi@raspberrypi:~ $ sudo npm i -g homebridge-camera-ui@latest

設定

pi@raspberrypi:~ $ vi .homebridge/config.json

ファイル内に下記を記述します。

{
  "bridge": {
    "name": "Homebridge",  #名称を設定
    "username": "1A:2B:3C:4D:5E:6F",  #16進数で自由に設定
    "port": 46183,
    "pin": "111-22-333"  #自由に設定
  },

  "description": "HomeCamera",  #名称を設定

  "platforms": [
    {
      "platform" : "CameraUI",
      "videoProcessor": "ffmpeg",
      "cameras": [{
        "name": "Home Camera 2",  #名称を設定(Homeアプリでのデバイス名)
        "active": true,
        "videoConfig": {
          "source": "-re -i /dev/video0",
          "maxStreams": 3,
          "maxWidth": 1920,
          "maxHeight": 1080,
          "maxFPS": 10
        }
      }]
    }
  ]
}

以上でインストール・設定は完了です。 最後に以下のコマンドでシステムを再起動します。

pi@raspberrypi:~ $ sudo reboot

最後に

MacやiPhoneでの利用方法は前回書いたとおりになります。 広角カメラに換えたのでこのように視野角が十分広くなりました。

f:id:TedWada:20200203130748p:plain

以前はこんな感じでした。

f:id:TedWada:20191229173256p:plain
ラズパイカメラモジュールでの画角

しかも、一ヶ月くらい使っていますがタイムアウトは全く起こらなくなりました。

Rails のコネクションプールから接続を取り出す処理を追う

こんにちは、masm11 です。

弊社では PostgreSQL のデータベースを Amazon Aurora に移そうとしていますが、 フェイルオーバー時の処理が気になっています。 ググってみたところ、MySQL の情報はたくさん出てくるのですが、PostgreSQL の情報は 少なく、欲しい情報が出てきません。

今回は、弊社で使用している Rails 5.1 でフェイルオーバー時の動作をコードレベルで追いかけてみます。

unicorn は都度接続なのか?

その前に、確認しておきたいことが一つありました。 unicorn が HTTP リクエストごとに DB に接続/切断しているのかどうかです。

config/database.yml には、以下のように設定してあります。

staging:
  adapter:   postgresql
  database:  ...
  username:  ...
  password:  ...
  host:      ...
  statement_limit:      ...

伏せ字ばかりですが、pool: の設定はしていません。

activerecord-5.1.7/lib/active_record/connection_adapters/abstract/connection_pool.rb に以下のコードがあります。

        # default max pool size to 5
        @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5

省略した場合、デフォルトは 5 ですね。プールしていそうです。

他の確認もしてみました。

$ netstat -anp | grep :5432
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 172.31.30.216:24023     172.31.26.254:5432      ESTABLISHED 13853/unicorn worke 
(以下略)

少し時間が経った後、もう一度確認したところ、:24023 の部分が変わっていませんでした。 都度接続しているなら、ここが変わっていくはずです。

やはりコネクションをプールしているようです。 unicorn はシングルスレッドで、コネクションプールから接続を取得して返却し取得して返却し… を 繰り返しているので、デフォルトの 5 でもプロセスごとに1本しか使っていないのでしょう。

都度接続なら、そのたびに Aurora の cluster endpoint を DNS で引けば常に master を向いており、 フェイルオーバー時も新しい master を向いているので、そのまま何もしなくて良いはずです。 しかし、今コネクションプールを使っているアプリケーションを(なんとかして)都度接続に変更すると、 接続のコストが高すぎるように思います。

やはり、フェイルオーバーで接続が切れた場合にどうなるのか、処理を追う必要がありそうです。

Rails のコネクションプールの処理を追う

接続を取得する際によく使うメソッドは ActiveRecord::Base.connection ですね。ここをスタート地点にします。

activerecord-5.1.7/lib/active_record/base.rb では以下のようになっています。なお、途中のコメントやコードは省略して、必要な部分のみ引用しています。

module ActiveRecord #:nodoc:
  class Base
    extend ConnectionHandling

そして activerecord-5.1.7/lib/active_record/connection_handling.rb

module ActiveRecord
  module ConnectionHandling
    # Returns the connection currently associated with the class. This can
    # also be used to "borrow" the connection to do database work unrelated
    # to any of the specific Active Records.
    def connection
      retrieve_connection
    end

とあります。スタート地点はここのようです。

同じ module の中に以下がありました。

    def retrieve_connection
      connection_handler.retrieve_connection(connection_specification_name)
    end

retrieve_connection メソッドを呼び出しています。同名ですが別のメソッドのようです。 探してみたところ、activerecord-5.1.7/lib/active_record/connection_adapters/abstract/connection_pool.rb に以下のメソッドがありました。

module ActiveRecord
  module ConnectionAdapters
    class ConnectionHandler
      # Locate the connection of the nearest super class. This can be an
      # active or defined connection: if it is the latter, it will be
      # opened and set as the active connection for the class it was defined
      # for (not necessarily the current class).
      def retrieve_connection(spec_name) #:nodoc:
        pool = retrieve_connection_pool(spec_name)
        raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
        conn = pool.connection
        raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn
        conn
      end

クラス名から考えて、おそらくこれでしょう。 該当するコネクションプールを取得して、pool.connection で接続を取得しているようです。 同じファイルの中に以下のメソッドがあります。

module ActiveRecord
  module ConnectionAdapters
    class ConnectionPool
      # Retrieve the connection associated with the current thread, or call
      # #checkout to obtain one if necessary.
      #
      # #connection can be called any number of times; the connection is
      # held in a cache keyed by a thread.
      def connection
        @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
      end

ここでは、スレッドごとに接続をキャッシュしているようです。キャッシュになければ checkout するのですね。 checkout を見ていきましょう。

      def checkout(checkout_timeout = @checkout_timeout)
        checkout_and_verify(acquire_connection(checkout_timeout))
      end

checkout_and_verify。それっぽい名前ですね。接続が生きているか確認してそうです。このメソッドを追います。

        def checkout_and_verify(c)
          c._run_checkout_callbacks do
            c.verify!
          end
          c
        rescue
          remove c
          c.disconnect!
          raise
        end

なんだか複雑です。引数 c には acquire_connection の返り値が渡るので、接続ですね。 c.verify! でチェックしてそうです。

verify!activerecord-5.1.7/lib/active_record/connection_adapters/abstract_adapter.rb にあります。

      def verify!(*ignored)
        if ignored.size > 0
          ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.")
        end
        reconnect! unless active?
      end

active?reconnect! が出てきました。一つずつ見ていきます。

active? は同じファイルの中にもあるのですが、

      def active?
      end

と空っぽです。おそらく他の場所でクラスを継承してオーバーライドしているのでしょう。

activerecord-5.1.7/lib/active_record/connection_adapters/postgresql_adapter.rb にありました。

      def active?
        @lock.synchronize do
          @connection.query "SELECT 1"
        end
        true
      rescue PG::Error
        false
      end

SELECT 1 を実行してみて、問題なければ true、失敗なら false を返しているだけですね。 接続が生きているかの確認はここにありました。

active? は理解したので、引き続き reconnect! を見てみます。

activerecord-5.1.7/lib/active_record/connection_adapters/abstract_adapter.rb

      # Disconnects from the database if already connected, and establishes a
      # new connection with the database. Implementors should call super if they
      # override the default implementation.
      def reconnect!
        clear_cache!
        reset_transaction
      end

とあって、それだけ?? と思いましたが、activerecord-5.1.7/lib/active_record/connection_adapters/postgresql_adapter.rb にもありました。

      # Close then reopen the connection.
      def reconnect!
        @lock.synchronize do
          super
          @connection.reset
          configure_connection
        end
      end

super で先に上げた方の reconnect! を呼び出しているんですね。 そして @connection.reset です。

@connection は同じファイルで以下のように作っています。

        def connect
          @connection = PG.connect(@connection_parameters)
          configure_connection
        rescue ::PG::Error => error
          if error.message.include?("does not exist")
            raise ActiveRecord::NoDatabaseError
          else
            raise
          end
        end

PG.connect で作っています。PG は pg という gem で定義されています。 つまり @connection.resetreset メソッドは pg gem の方にあります。

せっかくここまで来たので、ついでに見ていきます。ここからは C 言語になります。

pg-0.21.0/ext/pg_connection.c で以下のようになっています。

 rb_define_method(rb_cPGconn, "reset", pgconn_reset, 0);

ruby でいう reset というメソッドは、C では pgconn_reset() 関数なのですね。 この関数は以下のようになっています。

/*
 * call-seq:
 *    conn.reset()
 *
 * Resets the backend connection. This method closes the
 * backend connection and tries to re-connect.
 */
static VALUE
pgconn_reset( VALUE self )
{
        pgconn_close_socket_io( self );
        gvl_PQreset( pg_get_pgconn(self) );
        return self;
}

いろいろやってますね。一つずつ見ていきます。

pgconn_close_socket_io() は以下の通りです。

static void
pgconn_close_socket_io( VALUE self )
{
        t_pg_connection *this = pg_get_connection( self );
        VALUE socket_io = this->socket_io;

        if ( RTEST(socket_io) ) {
#if defined(_WIN32) && defined(HAVE_RB_W32_WRAP_IO_HANDLE)
                int ruby_sd = NUM2INT(rb_funcall( socket_io, rb_intern("fileno"), 0 ));
                if( rb_w32_unwrap_io_handle(ruby_sd) ){
                        rb_raise(rb_eConnectionBad, "Could not unwrap win32 socket handle");
                }
#endif
                rb_funcall( socket_io, rb_intern("close"), 0 );
        }

        this->socket_io = Qnil;
}

見づらいので、windows 専用コードを削除してみると、以下のようになります。

static void
pgconn_close_socket_io( VALUE self )
{
        t_pg_connection *this = pg_get_connection( self );
        VALUE socket_io = this->socket_io;

        if ( RTEST(socket_io) ) {
                rb_funcall( socket_io, rb_intern("close"), 0 );
        }

        this->socket_io = Qnil;
}

ruby レベルで socket を close しているようです。

次に、pg_get_pgconn() は以下のようになっています。

PGconn *
pg_get_pgconn( VALUE self )
{
        t_pg_connection *this;
        Data_Get_Struct( self, t_pg_connection, this);

        if ( !this->pgconn )
                rb_raise( rb_eConnectionBad, "connection is closed" );

        return this->pgconn;
}

接続に対応する構造体のポインタを取得しているだけのようです。

最後に gvl_PQreset() です。パッと見、見つかりませんでしたが、gvl_wrappers.c にありました。

FOR_EACH_BLOCKING_FUNCTION( DEFINE_GVL_STUB );

FOR_EACH_BLOCKING_FUNCTION はマクロです。gvl_wrappers.h で以下のようになっています。

#define FOR_EACH_BLOCKING_FUNCTION(function) \
  (略)
        function(PQreset, GVL_TYPE_VOID, void, PGconn *, conn) \
  (略)

DEFINE_GVL_STUB が引数 function に渡されるので、

        DEFINE_GVL_STUB(PQreset, GVL_TYPE_VOID, void, PGconn *, conn)

となります。そして DEFINE_GVL_STUB がまたマクロで、以下のようになっています。

        #define DEFINE_GVL_STUB(name, when_non_void, rettype, lastparamtype, lastparamname) \
                rettype gvl_##name(FOR_EACH_PARAM_OF_##name(DEFINE_PARAM_LIST3) lastparamtype lastparamname){ \
                        return name( FOR_EACH_PARAM_OF_##name(DEFINE_PARAM_LIST1) lastparamname ); \
                }

渡された引数を使って展開すると、以下のようになります。

                void gvl_PQreset(FOR_EACH_PARAM_OF_PQreset(DEFINE_PARAM_LIST3) PGConn *conn){
                        return PQreset( FOR_EACH_PARAM_OF_PQreset(DEFINE_PARAM_LIST1) conn );
                }

FOR_EACH_PARAM_OF_PQreset もマクロで、以下のように定義されています。

#define FOR_EACH_PARAM_OF_PQreset(param)

空です。これも展開すると、以下のようになります。

                void gvl_PQreset(PGConn *conn){
                        return PQreset(conn);
                }

たくさんのコードを列挙しなくて済むようマクロが使われていたので、解読に少し手間取りましたが、 結局 gvl_PQreset()PQreset() を呼んでいるだけですね。

PQreset() のドキュメントはこちらにあります。 同じパラメータを使って同じサーバへ接続するそうです。

まとめ

ActiveRecord::Base.connection は、コネクションプールから接続を取得して返します。 コネクションプールは配列から接続を取り出して、生きていればそれを返し、死んでいたら PQreset() で 復活させてからそれを返すようです。

PQreset() のドキュメントを読むと、同じパラメータを使って同じサーバへ接続するそうでが、 接続時のパラメータとして指定するのはホスト名であって IP アドレスは指定しません。 接続しなおす時に DNS を引き直してくれるのかどうかは判りませんでした。 DNS を引き直さない場合、本当に「同じサーバ」(同じインスタンス) になってしまい、 そこはおそらく read-only です。

ドキュメントの説明が若干曖昧なので、あとは実際にテストしてみるしかないかな、と思いました。

UNIX ドメインソケットの接続対応表を作る

明けましておめでとうございます。masm11 です。

UNIX ドメインソケットってご存知ですか? 知らないけど実は使ってる (設定したことがある) 方も結構いらっしゃるかもしれません。 今回は、UNIX ドメインソケットをちょっとだけ便利にする自作ツールを一つご紹介したいと思います。

UNIX ドメインソケットとは?

まず、インターネットドメインといえば、TCP や UDP が有名ですね。 TCP はストリーム型で、つまり、送った連続データがそのまま受け取れます。また、信頼性があると言われています。UDP はデータグラム型で、つまり、一度に送れる量に制限はあるものの、一度で送ったデータは一度で受け取れます。

インターネットドメインの TCP や UDP に対して、UNIX ドメインには名前はありませんが、こちらにもストリーム型やデータグラム型があります。

では、UNIX ドメインソケットはどこで使われているのでしょうか?

nginx に以下のような設定をしたことはありませんか?

upstream app {
    server unix:/tmp/unicorn.sock;
}

unicorn には以下のように設定していますね。

listen '/tmp/unicorn.sock'

これらは /tmp/unicorn.sock を使って nginx と unicorn を接続する設定です。これが UNIX ドメインソケットです。

ls で見てみると、以下のように先頭が s になっていて、 ソケットであることを示しています。

[ec2-user@ip-172-31-30-9 ~]$ ls -al /tmp/unicorn.sock 
srwxrwxrwx 1 ec2-user ec2-user 0 1016 10:31 /tmp/unicorn.sock
[ec2-user@ip-172-31-30-9 ~]$ 

UNIX ドメインソケットは、ファイルシステム中にソケットを作り、 そのソケットを使って2つのプロセスがつながります。 従って、同じコンピュータ内でしか接続できません。

何と何がつながっている?

では、今、私のコンピュータにはどんな UNIX ドメインソケットがあるのでしょうか?

それを見るには ss というコマンドを使います。以前は netstat というコマンドが使われていましたが、Linux ではここ数年で ss コマンドに置き換わりました。

以下のように実行してみてください。

ss -anp | grep ^u_str

UNIX ドメインのストリーム型ソケットの一覧が表示されます。

例えば以下の行を見てみます (見づらいので空白は適当にまとめています)。

u_str  ESTAB  0 0  @/tmp/ibus/dbus-PSq26h33 204929  * 203227  users:(("ibus-daemon",pid=607643,fd=12))
  • u_str

    UNIX ドメインのストリーム型ソケットです。

  • ESTAB

    接続しています。

  • 0 0

    送受信バッファに溜まっているデータ量を示します。

  • @/tmp/ibus/dbus-PSq26h33 204929

    自プロセスが使っている側のソケットについての情報です。 (先頭が @ で始まっているものは、ファイルシステム中には存在しません)

  • * 203227

    接続相手側のソケットについての情報です。

  • users:(("ibus-daemon",pid=607643,fd=12))

    自プロセスについての情報です。

ibus-daemon が使っているソケットですね。

では接続相手は何者でしょうか? そう思った時は、まず * 203227 の部分を見ます。接続相手側のソケットについての情報、ですね。 これで検索してみると、ありました。

u_str  ESTAB  0 0  * 203227  * 204929  users:(("wf-panel",pid=607640,fd=19))

相手側プロセスは wf-panel であることが判ります。

ツールを作る

さて、インターネットドメインの場合は、相手側プロセスは別のコンピュータにあることが極普通なので、それがどんなプロセスなのかを知ることは困難です。しかし、UNIX ドメインなら、同じコンピュータに存在することが判っているので、相手側プロセスが何者なのか、以上のように調べれば判ります。

しかし、あくまで「調べれば判る」というレベルです。例えば、目的のプロセスが2つあって、それらがつながっているかどうか、どうすれば調べられるでしょうか? @/tmp/ibus/dbus-PSq26h33 を使ってつながっているプロセスはたくさんあることが想定されます。

luna:~ % ss -anp |grep ^u_str |grep '@/tmp/ibus/dbus-PSq26h33'  | wc -l
10
luna:~ % 

10個ですね。10個なら一行ずつ調べていけば、まぁなんとかなります。

でも、面倒ですよね?

そこで、ツールを作ってみました。

#!/usr/bin/env ruby

data = []

`ss -anp`.split("\n").each do |line|
  fields = line.split(/\s+/, 9)
  next if fields[0] != 'u_str'
  next if fields[1] != 'ESTAB'
  l_port = fields[5]
  r_port = fields[7]
  dat = {
    socket: fields[4],
    l_port: fields[5],
    r_port: fields[7],
    prog: fields[8],
  }
  data << dat
end

r_port_info = {}
data.each do |dat|
  raise "r_port duplicated: #{dat}." if r_port_info[dat[:l_port]]
  r_port_info[dat[:l_port]] = dat
end

data.each do |dat|
  l_dat = dat
  r_dat = r_port_info[dat[:r_port]]
  next unless r_dat

  next unless l_dat[:prog]
  next unless r_dat[:prog]

  puts "#{l_dat[:l_port]} #{l_dat[:r_port]} #{l_dat[:prog]} #{r_dat[:prog]}"
end

ここまでに説明した ss コマンドを使った調べ方を使って、愚直に接続元と接続先をペアにして列挙するだけのツールです。 仮に uconn という名前にしましょう。

このツールを使えば、例えば ibus-daemon と emacs がつながっているかは、以下のように簡単に確認できます。

luna:~ % uconn |grep ibus-daemon | grep emacs
199631 203253 users:(("emacs",pid=607815,fd=14)) users:(("ibus-daemon",pid=607643,fd=14))
203253 199631 users:(("ibus-daemon",pid=607643,fd=14)) users:(("emacs",pid=607815,fd=14))
luna:~ % 

ibus-daemon と emacs がちゃんとつながっていることが判りますね。

まとめ

UNIX ドメインソケットのおさらいと、ss コマンドの出力の見方、そしてその出力を整形して見やすく出力するツールを紹介しました。日常的に Linux の管理をしていると、時々このツールを使う場面があります。

ただ、途中で少し説明したとおり、ss コマンドはそこそこ最近生まれたコマンドなので、古めの OS だと存在しないくて、このツールが使えません。弊社のサーバも、ss コマンドが存在しないくらいには古い OS なので、このツールは使えません。残念です。

ではまた!

ラズパイ4でHomeKit対応見守りカメラを作ってみました

こんにちは。Tedです。

AppleのHomeKit対応製品は海外では充実していますが、日本ではまだまだ。そのために選択肢が少なく高価なのがネックです。

そこで今回はHomebridge + ラズパイを使って安価にHomeKitに対応した見守りカメラを作ってみました。

Homebridgeとは

HomebridgeはNodeJSを使ったHomeKitのエミュレーションを行うシステムです。オープンソースとして公開されています。

https://homebridge.io

用意するもの

  • ラズパイ(今回は Raspberry Pi 4B を使いました)
  • ラズパイ対応カメラ
  • 32GB マイクロSDカード
  • ラズパイ電源アダプタ
  • Pi4B用ラズパイケース(カメラを内蔵できるものを選びました)

f:id:TedWada:20191230185630j:plain
購入したラズパイ+ACアダプタセットとカメラ

他には環境を構築するためにMacBook Proおよびインターネット接続環境・有線LANケーブル・マイクロSDカードアダプタを用意しました。

「作ろう会」で作りました

今回は下記の作ろう会にて作成してみました。

ingage.connpass.com

masm11さん(当社シニアエンジニア)と植田雄太さんから助言をいただきながら進めました。

システム構築

ラズパイを使ってHomeKitカメラを作った記事はいくつかWeb上に上がっています。 たとえば下記がそうです。

appleinsider.com

このAppleInsiderの記事では構築済のシステムバイナリが提供されていますが、ラズパイ4では動きません。 そこで今回はバイナリを使わずシステムを構築していきます。

ラズパイを起動できるようにする

まずはラズパイを起動できるようにします。 今回は直付けのモニタ・キーボードは用意せずに構築していきます。

SDカードにRasbianを書き込む

下記のRasbianダウンロードサイトよりRasbianシステムをダウンロードします。 今回は Raspbian Buster with desktop and recommended software を使いました。

www.raspberrypi.org

ダウンロードしたZipアーカイブは解凍しておきます。

SDカードへの書き込みはMacにてターミナルを開いて下記のように進めます。

[~]$ diskutil list  #マイクロSDカードのアイデンティファイヤを調べておく
#ここではマイクロSDカードのアイデンティファイヤは /dev/disk1 と記述します。

[~]$ diskutil unmountDisk /dev/disk1  #いったんアンマウント
[~]$ diskutil eraseDisk MS-DOS BOOT /dev/disk1  #フォーマット
[~]$ diskutil unmountDisk /dev/disk1  #フォーマット後自動的にマウントされるので再度アンマウント

[~]$ sudo dd if=/ファイルパス/ダウンロードしたファイル.img of=/dev/rdisk1 bs=1m  #SDカードにイメージを書き込み

LANからラズパイにアクセスできるようにする

[~]$ diskutil mountDisk /dev/disk1  #書き込んだSDカードをマウント
[~]$ cd /Volumes/boot  #SDカードに移動
[~]$ touch ssh  #sshファイルを作成

ラズパイに周辺機器を取り付ける

ラズパイにカメラを取り付ける

写真のようにラズパイにカメラケーブルを取り付けます。

f:id:TedWada:20191230185939j:plain

向きを間違えないように、またしっかりとセットしてください。(見た目にはちゃんとケーブルがセットされているようでも微妙なズレで認識されなかったりします)

その他周辺を接続

バイナリを書き込んだマイクロSDカード、LANケーブルおよび電源をラズパイ本体に接続します。 ラズパイをケースに入れるのは基本的な動作確認ができてからとしました。

いよいよラズパイを起動

電源を入れると、本体の赤LEDが点灯し緑のアクセスLEDが点滅します。 10秒ほど待てばシステム起動となり、ラズパイにアクセスできるようになります。

ラズパイにSSH経由でアクセス

[~]$ ssh pi@raspberrypi.local
# 初期パスワードは raspberry (当然ながら変更しておく)

無線LANでアクセスできるようにする

上記の通りラズパイにアクセスしたら、無線LANに接続できるようにしておきます。

pi@raspberrypi:~ $ sudo vi /etc/wpa_supplicant/wpa_supplicant.conf

下記を記述します。

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=JP
network={
   ssid="<接続したいSSID>"
   psk="<WEPキー>"
}

ここまでできれば一段落です。 後はラズパイにシステムを構築していきます。

ラズパイシステムを最新の状態にアップデート

pi@raspberrypi:~ $ sudo apt-get update && sudo apt-get upgrade

カメラモジュールを有効化

pi@raspberrypi:~ $ sudo raspi-config

f:id:TedWada:20191223180809p:plain
「5 Interfacing Options」を選択。

f:id:TedWada:20191223180901p:plain
「P1 Camera」を選択。

f:id:TedWada:20191223180939p:plain
「Yes」で保存。

f:id:TedWada:20191223181012p:plain
カメラモジュールが有効化された。

タイムゾーンの設定

前記 raspi-config から適切にタイムゾーンを設定しておきます。

設定場所は [4 Localisation Options] - [I2 Change Timezone] にあります。 日本だと [Asia] - [Tokyo] ですね。

Node.jsをインストール

HomebridgeはNode v4.3.2またはそれ以降のバージョンが必要です。 今回は執筆時の最新版である version 10.x を使いました。

https://linuxize.com/post/how-to-install-node-js-on-raspberry-pi/ を参考にしました。

pi@raspberrypi:~ $ curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -  #最新版をget
pi@raspberrypi:~ $ sudo apt-get install -y nodejs  #インストール
pi@raspberrypi:~ $ node --version  #インストールされたバージョンを確認
v10.15.2   #などと表示されればOK

Homebridgeをインストール

pi@raspberrypi:~ $ sudo npm install -g --unsafe-perm homebridge

Homebridge用カメラプラグインをインストール

pi@raspberrypi:~ $ sudo npm install -g homebridge-camera-rpi@latest

Homebridgeの設定ファイルを編集

pi@raspberrypi:~ $ mkdir .homebridge  #最初にディレクトリを作成
pi@raspberrypi:~ $ vi .homebridge/config.json

ファイル内に下記を記述します。

{
  "bridge": {
    "name": "Homebridge",  #名称を設定
    "username": "1A:2B:3C:4D:5E:6F",  #16進数で自由に設定(重複しないようMACアドレス推奨)
    "port": 46183,
    "pin": "111-22-333"  #自由に設定
  },

  "description": "HomeCamera",  #名称を設定

  "platforms": [
    {
      "platform" : "rpi-camera", 
      "cameras": [{
        "name": "Home Camera",  #名称を設定(Homeアプリでのデバイス名)
        "rotate": 90  #0-180の回転角度
      }]
    }
  ]
}

(※2020-1-11 修正: platforms内のplatformの名称はプラグイン固有なのでその旨修正)

あとは、ラズパイ起動時に必要なモジュールが自動的にスタートするようにしておきます。 これは動作確認が終わってからでもよいでしょう。

自動起動の設定

pi@raspberrypi:~ $ sudo vi /etc/rc.local

ファイル内に下記を追加します。 スクリプト後半、exit 0 の前あたりに書きました。

/usr/bin/homebridge -U /home/pi/.homebridge/ &

(※2020-2-16 修正: homebridgeが格納されているディレクトリを修正)

Homeアプリにカメラを登録

前提としてすでにHomeKitホームハブ(HomePod, Apple TVなど)が稼働していることとします。

HomePod、Apple TV、iPad をホームハブとして設定する - Apple サポート

前記ホームハブと同じネットワークセグメントに接続されているデバイス(iPhoneなど)にてHomeアプリを起動します。

f:id:TedWada:20191229150313j:plain

Homeアプリにて [+] をタップしてデバイスを追加します。

f:id:TedWada:20191229150537j:plain

[アクセサリを追加] をタップします。

f:id:TedWada:20191229150954j:plain

[コードがないか、スキャンできません] をタップします。

f:id:TedWada:20191229151126j:plain

設定したカメラデバイスが表示されるのでタップします。

f:id:TedWada:20191229151252j:plain
カメラ名として設定したデバイスを選択する。

[このまま追加] をタップし、その後表示される画面にてconfig.jsonにて設定したpinを入力します。

f:id:TedWada:20191229151447j:plain

以上にてカメラデバイスが追加されます。

f:id:TedWada:20191229151516j:plain

正しく動作すれば図のようにカメラ画像がHomeアプリから確認できるようになります。

f:id:TedWada:20191229173256p:plain

うまく動かない場合は

カメラが正しく動いているか

pi@raspberrypi:~ $ vcgencmd get_camera  #カメラの状態を取得
supported=1 detected=1  #このように表示されていれば正しく認識されている

上記のように返らない場合はカメラケーブルの接続状態を確認しましょう。

カメラ画像が取得できるか

pi@raspberrypi:~ $ raspistill -o pictureFromCamera.jpg

取得した画像は sftp などで取得・確認します。

Homebridgeは正しく起動しているか

Homeアプリからデバイスが見えない場合、Homebridgeが正しく稼働していない可能性が高いです。

pi@raspberrypi:~ $ ps -aux | grep homebridge  #homebridgeプロセスが正しく起動しているか確認
root       432  0.2  1.8 181880 72148 ?        Sl   Dec18  36:25 homebridge  #このような表示があればプロセスは存在している

プロセスは起動しているが動作がおかしい場合はデバッグモードでエラーが起こっていないか確認しましょう。

pi@raspberrypi:~ $ sudo kill <killしたいPID(上記の場合は432>  #いったんHomebridgeプロセスを終了
pi@raspberrypi:~ $ DEBUG=* homebridge -D  #デバッグモードで起動

Homebridgeの設定ファイルに誤りはないか

config.json ファイルを JSON Lint などのJSONバリデータにて確認しましょう。

突然Homeアプリからデバイスが見えなくなった

システムの設定時など、何度か再起動を繰り返していると突然Homeアプリからデバイスが見えなくなることがあります。

この場合は下記のようにペアリングをリセットすると見えるようになります。

  • HomeアプリからHomebridgeアプリをすべて削除する。
  • persist および accessories フォルダを削除する。
pi@raspberrypi:~ $ sudo rm -rf .homebridge/persist
pi@raspberrypi:~ $ sudo rm -rf .homebridge/accessories
  • config.json 内の username を(インクリするなど)別のものに変える。

上記を行った後、システムを再起動します。

それでもうまくいかない場合

下記リンクに沿って調べてみてください。

Basic Troubleshooting · nfarina/homebridge Wiki · GitHub