OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

BEAR.Sunday を GAE flex 環境で使う際の tips

このようなことを偉そうにもつぶやいてしまったので、この記事を書く運びとなりました。

改めましてこんばんわ @kalibora です。

弊社では BEAR.Sunday を Google App Engine の Flexible Environment(以下 GAE)上で動かしているのですが、 実はどーにもこーにもずっと解決出来ない問題がありました。

それはGAEのオートスケールでインスタンスが増える際に、下記のような Ray.DI 起因だと思われるエラーがたまに出る。 というものです。

Dummy\Exception(Argument 1 passed to Dummy\Foo::__construct() must be an instance of Dummy\Bar, integer given, called in /app/var/tmp/prod-hal-api-app/Dummy_Foo-.php on line 5)

一部ファイル名やパスを書き換えていますが、 Ray.DI に生成を任せているクラスの引数に、期待通りのクラスではなく integer が与えられた。という内容のエラーです。

なぜこの様な事が起きるのか詳細は理解できていないのですが、

(本番環境では同時に多数のアクセスが来て、最初のアクセスでオンデマンドにコンパイルしている最中に、また次のリクエストが来て・・・という具合でエラーになるのでしょうか?)

プロダクション | BEAR.Sundayデプロイ の項目には下記のような記述があります。

セットアップを行う際にvendor/bin/bear.compileスクリプトを使ってプロジェクトをウオームアップすることができます。

コンパイルスクリプトはDI/AOP用の動的に作成されるファイルやアノテーションなどの静的なキャッシュファイルを全て事前に作成します。

全てのクラスでインジェクションを行うのでランタイムでDIのエラーが出ることもありません。

また.envには一般にAPIキーやパスワードなどのクレデンシャル情報が含まれますが、内容は全てPHPファイルに取り込まれるのでコンパイル後に消去可能です。コンパイルはdeployをより高速で安全にします。

ということで、本番環境ではコンパイルすべきだということが分かりました。

では GAE 環境へのデプロイではどのタイミングでコンパイルすべきでしょうか?

GAE のデプロイ

そもそも GAE のデプロイの流れを理解していないといけないので少し内部の挙動を追いました。

gcloud app deploy コマンドを打つと、内部では下記のような処理が走るようです。

  1. app.yaml の skip_files なども加味しつつ ソースファイルを src.tgz に固めて Google Cloud Storage にアップロード
  2. Google Cloud Build でビルド(docker イメージを生成)
  3. デプロイ(生成された docker イメージを使って新バージョンを立ち上げ)
  4. プロモート(新バージョンにトラフィックを切り替え)

2.3. のタイミングでコンパイルできれば良さそうですが、 3. の際にカスタム処理を入れる方法は私の方では見つけられませんでした。

2. のタイミングでは composer install 処理が走るので、 composer の post-install-cmd でコンパイル処理を追加すれば、 生成される docker イメージに vendor ファイルと共にコンパイルされたファイル(tmp/{context}/...)も追加されるので都合が良さそうです。

ですが、ここでいくつか自分たちが遭遇した罠(というか勝手に自分でハマっただけですが)がありました。

罠1. Cloud Build 時に includes している環境変数は使われない

GAE の app.yaml では includes を使って env_variables を別のファイルに書くことも可能で、共通の環境変数はそこで定義していたのですが、 これは Cloud Build 時に使われませんでした。

これに関しては下記のソースコードを見ると分かります。

https://github.com/GoogleCloudPlatform/php-docker/blob/master/builder/gen-dockerfile/src/Builder/GenFilesCommand.php

ということで、 includes は使わないことにしました。

罠2. Doctrine2 が勝手にコンパイル時にDB接続しに行く罠

さて、これで解決かと思いきや Doctrine2 ユーザーへの罠もありました。

DI のコンパイル時に本番環境のデータベース(Cloud SQL)に接続しに行こうとして、 Cloud Build 環境では接続出来ずにエラーになる。

というものです。

実際にデプロイされたGAE環境ではもちろん本番データベースに接続できるものの、Cloud Build環境では接続できないようなので(普通接続する必要はないので問題ない)どうしたものかと悩みましたが、

結局この原因は Automatic platform version detection という接続先のデータベースを自動で判定する機能が原因だったので、 この機能をやめ、明示的に接続先のプラットフォームを指定するようにしました。

罠3. src の タイムスタンプがコンパイル時と docker イメージ作成時で異なる?

ここまでで、Cloud Build 時に生成される docker イメージにコンパイルされたファイルを含めることができました。

これで最初のデプロイ時でもオートスケールでインスタンスが増えた際でも、コンパイル済みのファイルが最初から用意されていることになります。 (最初のリクエスト時にコンパイルで待たされることはありません!エラーも出ない!)

と思いきや、もう1つ遭遇した罠がありました。

プロダクション | BEAR.Sundayコンテキスト の項目には下記のような注意書きがあります。

重要: プロダクションではディプロイ毎に$appを再生成する必要があります。

$app を再生成するには src/ ディレクトリのタイムスタンプを変更します。 BEAR.Sundayはそれを検知して $apptmp/{context}/di のDI/AOPファイルの再生成を行います。

これは逆に言えば

src/ ディレクトリのタイムスタンプが変わると $apptmp/{context}/di のDI/AOPファイルが再生成される。

ということになります。

せっかく作った tmp/{context}/di のファイルを消して再生成したくはないので、 コンパイル時の src/ のタイムスタンプと docker イメージ内に含まれる src/ のタイムスタンプが同じでないといけません。

ですが私が試したところ、どうも1秒くらいズレるようです。 この原因や根本的に合わせる方法は分かりませんでした。

ですので、 Bootstrap クラスを独自のものに差し替え、そのクラスでは src/ のタイムスタンプを直接見るのではなく、 タイムスタンプを保存したファイルがあれば、それを見るようにしました。 (コンパイル時に src/ のタイムスタンプをファイルに出力しておく)

これで GAE デプロイ後も /tmp/{context} ディレクトリが削除されることなく、使い続けることができるようになりました。

まとめ

  1. composer の post-install-cmdvendor/bin/bear.compile を呼ぶようにする
  2. Cloud Build 環境と実際にデプロイして動作する環境の差異に注意する
    • Cloud Build 時は includes した環境変数が使えない
    • データベースに接続できる/できない
  3. デプロイ後にDI関連のファイルが再生成されないようにする
    • 自分は src/ のタイムスタンプを固定したが、単純に tmp/{context}/.do_not_clear ファイルを配置するだけでよかったかもしれない(未検証)

はい。そんな感じで現場からは以上です。

Guzzleにキャッシュやリトライは任せちゃおう

曇天が続きますね。 @kalibora です。

今日は小ネタを書かせていただきます。

PHPer のみなさんは HTTP クライアントは Guzzle を使うことが多いと思います。

昔は curl をそのまま使うことも多かったと思いますが、今はあまりそういう状況も少なくなってきたのではないでしょうか。

さて、そんな Guzzle ですが、 Middleware を使っている方はどれくらいいますでしょうか。

Middleware は Guzzle を機能強化する仕組みなんですが、僕が使って便利だなと思ったものを2つばかり紹介したいと思います。

1. kevinrob/guzzle-cache-middleware

kevinrob/guzzle-cache-middleware - Packagist

RFC 7234 に準拠した HTTP Cache の機能を追加するミドルウェアです。

むしろこれを入れないと、いくらAPIなどのサーバー側で Cache-Control ヘッダを返そうが何しようが Guzzle 側ではキャッシュしません。

これを入れれば、レスポンスヘッダーに max-age があると、その間APIにアクセスしないでキャッシュしたレスポンスを使いますし、ETag などがあって 304 が返ってくれば、それもまたキャッシュしたレスポンスを使いまわしてくれます。

キャッシュストレージとキャッシュ戦略(Cache Strategy)とを自分で選ぶことができるので、だいたいどんなケースにも対応できるんじゃないでしょうか。

選択可能なキャッシュストレージ

  1. Psr16CacheStorage
  2. Psr6CacheStorage
  3. DoctrineCacheStorage
    • Doctrine\Common\Cache\Cache を使う
  4. CompressedDoctrineCacheStorage
    • Doctrine\Common\Cache\Cache を使うが、値の保存に gzcompress で圧縮を行う
  5. LaravelCacheStorage
    • Laravel用
    • Illuminate\Contracts\Cache\Repository を使う
  6. FlysystemStorage
  7. WordPressObjectCacheStorage
    • wordpress用
    • 内部で wp_cache_get などのグローバルなWP関数を使う
  8. VolatileRuntimeStorage
    • arrayを使うので、デバッグや開発時の用途かな

キャッシュ戦略

  1. PublicCacheStrategy
    • 共有キャッシュ(複数のユーザーで使い回せるもの)をキャッシュする
  2. PrivateCacheStrategy
    • プライベートキャッシュ(特定の一人のユーザーのためのキャッシュ)をキャッシュする
  3. GreedyCacheStrategy
    • 自分で設定したTTLでキャッシュする
  4. NullCacheStrategy
    • 何もキャッシュしない

2. RetryMiddleware または caseyamcl/guzzle_retry_middleware

下記の2つはどちらも通信に失敗した場合にリトライしてくれるミドルウェアです。

サーバーとの通信は不安定なので必ず成功するわけでもないですし、単純にリトライしたら成功するケースも多いかと思います。

その場合、自前でリトライ処理を書いてしまうと、インフラ的なコードがアプリケーション層に漏れ出る形になってソースコードが読みづらくなるので、こういうものに任せてしまったほうが楽ですね。

この2つのミドルウェアの違いは

  • \GuzzleHttp\RetryMiddleware
    • リトライするかどうかの判断は自前(クロージャー)で設定
      • 例えばステータスコードで判断したり、リトライ回数で判断したりすることができる
    • リトライ時のsleep時間は自前でも設定できる
      • デフォルトはリトライ回数に応じてべき乗で増える(1, 2, 4, 8, 16 ...)
  • \GuzzleRetry\GuzzleRetryMiddleware
    • リトライするかどうかの判断は自動(ただし設定である程度変更できる)
      • 下記の条件に当てはまるとリトライしない
        • max_retry_attempts (デフォルトは10)で設定した最大リトライ回数を超えた
        • retry_only_if_retry_after_header の設定で必須にしているのに Retry-After ヘッダーがない
        • retry_on_status (デフォルトは 429, 503)で設定したステータスコードではない
        • 接続がタイムアウトしたが、 retry_on_timeout(デフォルトは false) の設定がされていない
        • タイムアウト以外の接続時の例外
    • リトライ時のsleep時間は下記のように決定される
      • default_retry_multiplier (デフォルトは1.5)* リトライ回数(1.5, 3, 4,5, ...)
      • Retry-After ヘッダーがあればその値を使う

といった感じです。

Retry-After ヘッダーを吐くサーバーと通信するなら後者、そうでないなら前者といった使い分けでしょうか。

外部サービスのAPIクライアントのSDKを使っている場合でも、内部的には Guzzle を使用しているケースも多く、 そういった場合でもミドルウェアを設定すればいいので便利ですね。

(ちなみに私は Zendesk API を使用するケースで \GuzzleRetry\GuzzleRetryMiddleware にお世話になりました)

といった感じで、今日はこの辺でまた。

実家を出たので心置きなくスマートホーム化を試みた話

こんにちは、そして初めまして岩Dです。

声優の小野大輔さんを好きすぎるという訳ではないですが(いや、好きではあります)ひょんな事から岩Dと呼ばれています。
小野大輔さんと言えば、悪魔で執事も良いですが、最近だと調査兵団団長も良いですよね。でも一番好きなのは、水樹奈々さんの DISCOTHEQUE という曲の MV で踊る彼です。

ちなみに、小野大輔さんには弊社コンテンツでもいくつか朗読を担当頂いておりますので、是非ご一聴くださいませ。
https://audiobook.jp/search?q=%E5%B0%8F%E9%87%8E%E5%A4%A7%E8%BC%94

さて、前置きが長くなりましたが、オトバンクでは毎週弊社エンジニア全員が集まる場で社内勉強会をしております。 engineering.otobank.co.jp

そんな社内勉強会から今回紹介するのは、ねっしーさんが発表した『実家を出たので心置きなくスマートホーム化を試みた話』になります。

www.slideshare.net

実家を出てどうしてもやりたかった事

今年の2月に実家を出たねっしーさんですが、実家を出てどうしてもやりたかったことがあったそうで、それが..

f:id:siwadate:20190624192947p:plain

スマートホーム化して、家電の操作を楽にしたり、日々のなんとなく感じているストレスを解消するために様々なモノを導入したそうです。
(※なお、導入に際して、賃貸なので基本的には現状回復が必須で、今後の初期投資と考えて予算は特に際限は設けずに好きにやる!をモットーに進めました)

導入したモノ

そんな中今回導入、紹介してくれたのは、以下の3点になります。

  • Google Home (Google Nest Hub)
  • Nature Remo
  • Sesame Mini

Google Home

ご存知 Google Home は、Google が提供するスマートスピーカーになります。

f:id:siwadate:20190625123228j:plain

音声入力で様々な事が出来る Google Home ですが、ねっしーさんはこの様な用途で使っているそうです。

  • 天気の確認
  • タイマー
  • 時刻確認
  • 音楽、動画再生
  • テレビのオンオフ
  • 予定の確認

使っている感想としては、 90/100 点の評価で、 「(Nest Hub の様な)ディスプレイ付きのスマートスピーカーは本当に良い。もっとスマートスピーカーに依存したら各部屋に1つずつ欲しくなってしまう」と絶賛していました。
ただ、まだまだサードパーティアプリの開発があまり進んでいない様で、そこが残念だと感じている様です。

ちなみに、弊社も Google Home 向けにコンテンツを提供しているので、スマートスピーカー持ってるけど使い道に困っている!という方は是非一度お試しください。偶然なことにねっしーさんがメインで開発したコンテンツになります(すっとぼけ)。
https://pages.audiobook.jp/lp/google-home/
(※Google Home Mini プレゼントキャンペーンは終了しております)

Nature Remo

Nature Remo (以下Remo)は、家電をインターネットに繋げて手軽にスマートホームを実現するスマートリモコンです。

f:id:siwadate:20190625123220j:plain

この Remo は、赤外線リモコンを一括管理したり、スマートスピーカーと連携することで音声入力での家電操作、内蔵している各種センサーにより自動でエアコンを付けて室温管理をしたりなどができるそうです。

こんなに色々とできる Remoですが、使った評価としては 40/100 点とかなり低めな判定でした。
理由としては、Remo から対象機器に対して赤外線の届く範囲でしか操作出来ないので、各部屋ごとにこれを設置しないと有効に活用ができない事と、何より一番使いたいと思っていた照明に非対応だった事が要因だった様です。なので、「テレビつけて」くらいしか今の所うまく使えていないそうです。意外な盲点でしたね。

Sesame Mini

Sesame Mini (以下Sesame)は家のドアのサムターン部分に取り付けるタイプの電子錠です。「オープンセサミ!」のアレですね。

f:id:siwadate:20190625123227j:plainf:id:siwadate:20190624195800p:plain

これは、Bluetooth でスマートフォンと連携する事で使える電子錠になります。 取り付けが簡単で、国内のほぼ全てのサムターンに対応可能なのだそうです(もし取り付けができなかった場合は、サポートに連絡すればアタッチメントを無料で作成・送付してくれるそうです。すごい!)。
また、スマートスピーカーとの連携や、Web API での開発でカスタマイズしたりなどもできる様です。

用途としては、物理鍵の代用だけでなく、特定の人に対してキーシェアをしてパーティなどの時に家の出入りを便利にしたり、住人の出入り閲覧などができるので子供が帰ってきた事を外出先で確認できたり、また最近ではオフィスやレンタルルームでの鍵として使われる事もあるそうです。

そんな Sesame ですが、ねっしーさんの評価はなんと 120/100 点ととても満足している様です!
「便利すぎてもう物理鍵には戻れない」とのこと。鍵をカバンの中から探さなくて良かったり、家を出て駅に向かうまでで施錠ができたり、外出先で施錠確認がでたり、鍵を持たずに外出できたりなどがとても良いポイントなのだそうです。

ただ、スマートフォンを無くしたり家に忘れたり、充電がなくなった場合はどうするのか?という問題については、その時は物理鍵を外に隠しておいたり、電池が切れたら自動で解錠する様な設定をすれば家に入れない問題はクリアできるので大丈夫とのこと。

まとめ

今回、スマートホーム化を進めた事で電源の数が足りなくなる問題や、工事のために免許が必要だったり、同居人がそこまで乗り気じゃない?等障害はあったものの、ねっしーさんはそれらを跳ね除けなんとかスマートホーム化の第一歩を踏み出せた様です。

このプレゼンの中で、「以前実家にいた頃、母にプレゼントで購入した Google Home mini を使ってくれなかった経緯があった事もあり、同居人に(こういう新しい試みを)浸透させるにはまず自分が使って便利さを示せたらうまく使ってくれるのではないか」と言っていた事が印象に残りました。
私も、家電が大好きで新しい家電を同居人に納得してもらって買うために、家電の良さ等のプレゼンをしたりしているのでその辺り共感できました。

そして、そんなねっしーさんの次の野望はルンバを導入する事だそうです。
また環境が整った時にスマートホームがどうなっているかの話を聴きたいですね。

それでは。