PubSub Functionのfan-outの限界

最近、数百から数千単位の処理を、順次処理するのではなく並列に処理することでリソースを有効に活用して処理時間を短縮する方法を考えて実装を行った。スレッドや goroutines のようなプログラミング言語内の仕組みで作ったのではなくて、すべてフルマネージドのクラウドで行ったのだが、その際に謎の挙動に苦しんだのでそのメモを残しておく。

基本的な仕組み

以下を利用した。

  • Cloud Functions
  • Cloud PubSub
  • Cloud Firestore

PubSub Functions で fan-out を行った、という表現になるらしい。

もう少し細かいことを言うと並列に function を呼び出すために、

  1. 順次処理のループを回す function の中で処理そのものを行わずに次の function を起動する message を publish する
  2. 1 から起動された function が個々の処理を別々に実行する

という構造を n段階(徐々に 2 相当の function が多くなる)に渡って作るというもの。

気をつけていたこと

  • 終了条件が分かるようにしつつ多重処理を避ける

Cloud PubSub は at least once で message を配送するらしいので、並列に実行しようとしたら一部の処理が2回走ってしまうことがあり得るなと考えていた。そのため

  1. Firestore に処理対象のリストを作っておく
  2. 処理を始める前にリストから取得する
  3. 処理が終わったらリストから削除する

という実装にした。処理対象リストが空になったらおしまい、次の処理へ進む、という格好だ。

※ 今回は個々の処理全部に対して lock 用のデータを書き込むことはしなかった。さすがにそんなに瞬時に多重になってしまうことはないだろうと踏んでいたのと、実装を端折って結果を見れるようにするのを急いだため。結果、多重配信はなかった。ただし、function の起動と処理が限界を越えると多重配信が起きているかのような挙動になった。

当初ミスっていたこと

PubSub は速い。めちゃくちゃ速い。local の emulator は全然速度が出なくて並列度も2並列くらいしか動かないんだけど、GCP 上の PubSub は何十倍も何百倍も速い。この結果何がミスとして見つかったかというと、

Firestore の読み書きがちゃんと transaction になっていなかった

アホかと思われるかもしれないけど、実装を急いだのと local ではがっちりテストコードでガードしながら実装をしていたけど PubSub emulator が遅すぎて transaction を使っていなくても問題なく動いてしまっていた。その処理が軒並み壊れた。今回のコードは Node.js で書いていたので async/await メンドクセーと思いながら頑張っていたんだけど、そもそも Firestore に transaction を使わせていなかったのでどう気をつけて await していても無駄だった。副作用(?)として write を batch で処理するように書き換えたら個別に write するより逆に速くなったので嬉しかった。

この部分は 外部APIだらけのコードをできるだけTDDっぽく作った話 - あーありがち(2020-03-01) に書いたようにちゃんとレイヤーを分離していたので、対応コストは全体の中では微々たるものだった。原因さえ分かれば。

PubSubは速すぎてBackground Functionsの限界を突破する

割り当て  |  Cloud Functions のドキュメント  |  Google Cloud

Cloud Functions は実はものすごい数の request に耐えられるようになっている。少なくとも HTTP function は。ただし background function はいろいろ細かい制限があって、簡単に言うと

同時呼び出しは最大1000までだが、個々の関数の処理時間、中で処理するイベントの数が増えるとそのレートは下がる

ということになっていて、じゃあ具体的にどの程度までイケるのかは動かしてみないと分からない。

※ なお、PubSub の publish 側は恐らく同時 3000 まではいけます。(シングルリージョンで)

割り当てと上限  |  Cloud Pub/Sub ドキュメント  |  Google Cloud

限界を突破すると何が起きるのか

function を起動できなくなる問題よりは function が利用するリソースをちゃんとつかめなくなる、という現象がいろいろ観測された。1

  • Could not load the default credentials Error
    • Cloud Functions や GAE 上のアプリケーションはその実行中のサービスアカウントの権限情報を Application Default Credentials と呼ばれる方法で取得するが、これがちゃんと機能しなくていきなり死ぬ
  • 大量の function を起動している場合、コールドスタートせずに以前に取得した権限情報のまま上の Error が起きずに進むケースが多いが、結局リソースは確保できないので、例えば Firestore から何か取得した結果を確認するようなコードは全部失敗する
    • データが存在しないのではなくデータを取得する処理が動くためのリソースが確保できていないのだが、そういうエラーとしては捕捉するのは難しい
  • Process exited with code 16
    • function がすでに header を送ってるよというエラーっぽいが、エラーは起きたがちゃんと終了を返せずにさらにエラーが立て続けに送られている?
  • Error: 6 ALREADY_EXISTS: Document already exists
    • これは Firestore のエラーだが、限界を突破した結果 transaction を正しく維持できずに一部二重にデータを保存しようとしてエラーが起きているようだ
  • Error: 14 UNAVAILABLE: The datastore operation timed out
    • これも Firestore で、これはだいぶ明らかな異常だというのが分かりやすい

今回自分が触った中ではだいたい上のようなエラーになった。

上のようなエラーが fan-out した function それぞれで起きるのでものすごい勢いでエラーが増える。何かが根本的におかしいのは分かるのだが、いずれにせよ原因の特定に繋がるようなメッセージはほぼ得られず、限界を越えるとめちゃくちゃ異常な状態になるので、限界を知っておくことと限界への対処方法を知っておくのは超大事だよ という至極当たり前の感想を抱いてこのメモを書いています。今思うと笑えてくるが、当時は終わらない問題にだいぶ世界が濁って見えていた。

限界を突破しないようにするために何が必要か

PubSub で message を publish する処理に対して一度に publish する数をある程度以下に収めるようにする細工が必要になる。

割り当て  |  Cloud Functions のドキュメント  |  Google Cloud

具体的には 1 処理に 10s 掛かる function は単純に同時に 100 までしか起動できなくなる。さらに処理するイベントが多いとどんどん減っていく。ということは fan-out のために publish する側は subscriber function のコストを知ってないといけない。おおう、そんな。decoupled とはなんだったのか。

ということで今回やったのは

  • 待ち行列を設定する処理で Array を chunk に分割(ここで呼び出す function のコスト計算が必要になる)
  • chunk の中のアイテムについてどんどん publish
  • 少なくとも 1000ms 以内の処理にいろいろな制限があるので 1000ms sleep する
  • これを chunk がなくなるまでくり返す

という方法。

そして当然ながら、この fan-out を実行する function は他の function よりも実行時間が伸びやすいので timeout には気をつける必要がある。

なんという職人芸。結局インフラの事情に精通した職人は必要だなぁ、おい。ということが分かりましたとさ。

別解

全部 log run process で処理する

たぶんこれが最も安定する中で最も実行しやすい。ただし GAE Standard Env. などフルマネージドに寄せないと functions の代わりにするにはインスタンスやコンテナイメージの管理が必要になるし、料金も割高になりやすい。

料金については GAE Standard Env. の Basic Scale が実行時間は 24h まで耐えられ、かつ 0 instances まで scale down できるので、処理時間が安定しない function の移行先として最もよさそう。

long run process で PubSub を pull で受ける

上と微妙に違うのは PubSub の部分の実装をそのまま活かす点。ただし PubSub function では意識する必要のない pull の処理は追加で書かないといけない。インフラ管理が高コストになりやすいのは上と同じだし、logrun process の中に閉じてしまえば必要なインフラを一つ減らせるので、処理の数が少ないうちは上の方が簡単でよさそう。

投げっぱなしでよい処理が何種類も増えるようだと PubSub を使っておくメリットが出てくると思う。

long run process にせずに function で pull で受ける

これも可能は可能だけど、topic や subscription の管理がアプリケーションコードの中に現れて面倒になるうえに結局同時に扱えるリソースの上限は変らないので、メリットはほとんどない気がする。

PubSubではなくCloud Tasksで実行時刻を調整 & HTTP Function化

Cloud Functions には cold start 問題があり、deploy 直後はさらにパフォーマンスが落ちることがある。こうなると fan-out の数のチューニングが一時的に意味をなさなくなってしまう可能性がある。

この場合、PubSub ではなく Cloud Tasks を利用して HTTP Function に変換しつつ細かく実行時刻を設定することで、 fan-out する Function の実行時間を伸ばすことなく fan-out された Function が Background Function の限界を回避しつついい具合にタイミングを調整して実行することができる。

ただし Cloud Tasks には emulator もなく、サーバ側は最悪 OpenAPI の情報を利用して mock サーバを立てることはできるだろうけど、GUI も CLI も管理ツールが対応していないので、そこも作り込む? いやーだいぶヘビーに使い倒さないとオーバーキル感がすごい。Tasks は GCP のものを使いつつ、Function 自体は HTTP Function なので HTTP tunnel を掘って作っていくのが現実的かな。

安定性第一ならこれがよいと思うけど、少なくとも PubSub だけに閉じるよりはどうしても開発コストは大きくなりそう。

cf.

  1. もしかしたら起動されないこと自体は何も問題として現れないのかもしれない。 

View FrameworkインスタンスをControllerに預けて管理してもらう現実的で雑なClean Architecture

[2019-04-11 追記]最初 Use Case に預けると書いたけど、Interactor の仕様が constructor のないものだったので Controller に変更しました。

まとめ

Web FrontendでClean Architectureを試す - Qiita を読んで違和感を覚えたので、もうちょっとざっくりでいいし React に詳細突っ込みすぎでは?という気持ちを整理しようと考えてたらたどりついた現時点での考え方。ざっくり言うと

  • MVVM でやってることの範囲をそのまま広げれば Controller まで適用可能なのでは?
  • 枯れたライブラリにもっと任せるとよいのでは?

と思った話。

もっと雑に言うとタイトルの通りで View Framework を Controller に DI しちゃえばだいたいイケんじゃね説。

※ もちろん PubSub や EventBus に置き換えてもよい。少なくとも View Framework を直接 DI してしまうと Framework への変更の通知が直接手続きになってしまいやすい。そうなると密結合となりやすく、View Framework の詳細を知らないといけなくなる。ここは注意が必要。Usecase の実行後の処理も View Framework 側にイベントで通知する形を強制するには PubSub の方が安全ではある。1

Clean Architectureってなんだっけ

The Clean Architecture - Clean Coder Blog

Clean Architecture はざっくり言うと具体的なテクノロジー、詳細を外側に置き、原則的に外から内へ依存させようという考え方。処理の流れと依存関係が逆転するところがある。テキストで無理やり有名な図を描くとこんな感じ。

+----------------------------+
| Devices                Web |
|  +----------------------+  |
|  |     Controllers      |  |
|  |   +--------------+   |  |
|  |   |  Use Cases   |   |  |
|  |   | +----------+ |   |  |
|  |   | | Entities | |   |  |
|  |   | +----------+ |   |  |
|  |   +--------------+   |  |
|  | Gateways  Presenters |  |
|  +----------------------+  |
| DB                      UI |
+----------------------------+

ではWebフロントエンドってなんだっけ

まず前提として昨今の View Framework は Presenter から Web / UI の層を担っている。というか Web フロントエンドって言ってるんだから当たり前で、そもそも中心ではなく比較的周辺の領域を扱っている。

この View / UI Framework という周辺事項から内側の Controller へは普通に依存を持つことができる。

しかし View / UI 以外の情報も扱う必要がある場合、例えば Storage や Network を扱う場合は、以下の問題にぶつかる。

  • まず Network や Storage などは View / UI コンポーネントの関心外
  • かつ Controller / Use Cases から周辺に依存しない方がよい

じゃあUseCaseを意識しつつViewFrameworkとどう付き合うのがよいのか

上の問題に対する解として例のサークルで言うところの Gateways と Use Cases の境界を曖昧にすることで、以下のように書ける気がする。例は雑に Vue.js + ES2015+ で書いてある。

☆ App.vue

<script>
import Controller from 'controller'

export default {
  created() {
    this.controller = new Controller(this)
  },
  methods: {
    handler1() {
      this.$emit('event1', val1)
    },
    handler2() {
      this.$emit('event2', val2)
    }
  }
}
</script>

「Controller に対して ViewModel 自身を DI する」ことで中心を Controller 側にずらす意味合いを表しているつもり。

考え方としては Backbone.js, Angular 1 以降に定着し、ViewModel コンポーネントも実際にやっている、「依存を片方向に限定し、反対方向はすべてイベントで伝える」ということを頼りにしつつ DI で依存関係を逆転させるというもの。

Controller のインスタンスを this の中に保存しているのは単に消えてしまわないようにするためだけで、実際には View / UI Framework から Controller の何かを操作するといったことは行わない。

☆ controller.js

import Repository   from 'repository'
import RemoteFacade from 'remote_facade'
import Interactor   from 'interactor'

class Controller {
  constructor(vm) {
    this.vm = vm
    this.initListeners()
  }

  initListeners() {
    this.vm.$on(event1, usecase1)
    this.vm.$on(event2, usecase2)
    this.vm.$on(event3, usecase3)
  }

  async usecase1(val1) {
    return await Repository.foo(val1)
  }

  async usecase2(val2) {
    return await RemoteFacade.bar(val2)
  }

  async usecase3(val3) {
    return await Interactor.run(val3)
  }
}

Controller 側は ViewModel インスタンスの特定のイベントを処理できるようにしつつ、関連する Repository や RemoteFacade, Interactor などもここで初期化している。

大事なのは Repository や RemoteFacade, Usecase Interactor は外側の技術的な詳細そのものではないということ。あくまで Gateway 的なものですよという言い訳と言ってもよいかもしれない。少なくともここを見ると「何と何を使って何をしようとしているのかは分かる」くらいの粒度に収めたいというのが前提にある。

各 use case はメソッド一つで処理できるかもしれないし、中身が複雑になったら class に分けてもよい。Operator とか Command とか Service とかそんなやつ。Clean Architecture 的には Interactor.

今回改めて考えてみたこと

上で意識したのは

  • View / UI Framework のコンポーネントは2それだけで複雑であり、その初期化に関して必要以上に情報を持ち込まない方がよい
  • View / UI Framework は「entry point ではあるが全体から見ると周辺事項」であり、「全体を見る人は別にいた方」が扱いやすそう
    • 全体には当然 View / UI 以外も入る
  • Repository や RemoteFacade の詳細は気にしたくないので書かない。その向こうは依存の方向が内→外になる可能性もあるが、気にしない
    • テスタビリティの確保なら stub out とか方法はある
    • 必要ならこれらを Factory にして具象 → 抽象の依存の方向を実現することはできる
  • Repository と Factory の関係は Usecase Interactor にも適用できるかも
    • 直接インスタンスを作るのではなく Factory 経由で全部 constructor DI にするとか

こうしておけば例えば WebRTC などが増えた際には素直に WebRTC を中心に追加できる。3

いずれにせよ、いかに Clean かということよりも、

  1. View / UI Framework の中だけで頑張りすぎないこと
  2. いかに独立してテストしやすくするかということ
  3. そのうえで関心外の情報が一気に全部見えないようにすること
  4. 中心的に見なければいけないのは Controller / Use Cases であり、そこを見ると関連をたどりやすいこと

を考えている。

本来の Clean Architecture はフレームワーク非依存な考え方だが、現実的にはなんらかのフレームワークが考え方の起点となっていることは多い。

実際に Web フロントエンドはなんらかの View / UI Framework を採用することはほぼ前提と言ってよく、これらのフレームワークでは DOM や Event といった最も外側に位置する最も詳細なテクノロジーから一段階距離を置く手法が取られている。そしてこれと同じことは Storage や Remote Facade 系のライブラリにも言える。

つまりそれぞれの領域において概ね成熟したライブラリに任せていることは同じであり、必要以上に Clean な依存の方向にこだわりすぎてコード量が増えて特定のところで一気に依存解決する複雑さが見えてしまうくらいなら、まずは思い切ってざっくり View(UI) / Storage / Network くらいに分けてそれらをコントロールする人を中心に置く方が全体の見通しは楽になるし、個々のライブラリの機能を生かすことでテスタビリティは十分確保できるので、こんなもんでいいんじゃないの?ということを考えている。

逆に言うとこの考え方では cookie も localStorage も Fetch API もブラウザネイティブのものは極力直接使わないことを前提にしている。

で、「これはフレームワークから距離を置いた方がよい」と考えた場合には、グッと Clean Architecture に寄せてコード量を増やしてゴリゴリ書けばよい。

TDD も Clean Architecture も考え方を参考にしつつ、現実には増えるコード量とコスト感との兼ね合いの部分は必ず出てくる。その際に

  • どこが中心か
  • どこは任せてよいか
    • 外側の詳細に直接タッチしていないか
  • 特定のフレームワークの中だけで頑張ろうとしすぎていないか

くらいだけを気にしておけば、「そこそこClean」な状態は維持できるんじゃないかなーという感じのことを最近は考えている。

参考

  1. Vueの場合、空っぽのVueインスタンスをEventBus ( EventHub ) として使う方法も公式に紹介されてたりする。https://vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced 

  2. 以前よりシンプルになっているとは言え 

  3. もちろんその役割を表す一段抽象化してものだが 

リベンジシリーズ第2弾 J.B.ハロルドの事件簿

TSUTAYA で見つけて衝動買い。

当時残り1人か2人くらい登場人物に会うことができず解けなかった本格推理アドベンチャーですよ! 捜査資料を自分で紙のノートのうえに作って行く過程がまた楽しくて非常に面白かった、そして悔しかった思い出のあるゲームだ。

風呂の友が増えた。(レイトン教授は残ったナゾのレベルが高くなりすぎてコンプリートできないまま挫折している。)

……。

あれ? 解けなかったのってマンハッタン・レクイエムじゃね? あれ? 全然違うゲーム買った?

リアルタイム性と記録性、閲覧性、検索性の間

ITmedia Biz.ID:“社内IRC”を駆使するエンジニアの仕事術とは——モバイルファクトリー・松野徳大さん

こ、こんな若かったのか…。というのは置いといて。

irc + bot + plagger っつーのは確かにいいかもしんない。IP messenger のフォルダ添付もかなり便利で、(ファイルサーバを経由しちゃうとどこ行ったか訳分からなくなったりするような)こまいデータをリアルタイムにやりとりするにはものすごく重宝するんだけど、こういう応用はできないんだよな。

実は内部向け irc は一度試そうと思ったことがあって、というのは前から言ってるように Classic Mac も混ざっているネットワーク上ではまともなメッセンジャーソリューションてないわけ1。じゃあ irc かなぁと思って実験してたんだけど、そのときは「(確か試した Classic 用のクライアントが)DCC send でファイルを送ろうとすると死ぬほど遅い」ということで断念したような気がする2。文字だけなら IP messenger でもカバーできてるしってことで3。ただこういう全体のシステムと同期させるってのは考えてなかったなぁ。

確かに以前にやりとりした情報を閲覧するとか検索するってことになると、Web ベースの方がなんとなくしっくりきたりする4。でも入力の段階では Web ベースってのはちょっとだるい。専用のインターフェイスがあったりするのも意外と面倒。URL を特定するのが面倒だったりね5。irc と連携させちゃうことで両方を一気に解決するってのはかなり面白い。irc bot って漠然とは興味持ってたんだけど、こうなると俄然面白そうだ。

ただこの irc ベースって、人数に制限があるような気がするんだよな…。100人、200人の体制で irc ってのはかなり無理があるような。channel 分けできる分、IP Messenger で行くよりは現実的だけど、例えば一時的に別な部署と情報を共有したいとかって要求になるとちょっと難しくなってくる。複数の channel に手動で入る操作って結構面倒くさいんだよな。起動して自動で入るようになってるだけなら簡単でいいんだけど。あ、それも bot 任せにすればいいのか。自分で手で channel に入らなくても bot が全体の情報を扱うシステムに転送して、そこから例えば複数の channel に bot が broadcast なんてこともできるのか。おぉすげぇ。妄想だけならどんどんシステムが膨らんでいくわ。

  1. Classic Mac を捨てろという正論はとりあえず置いておく。 

  2. DCC なんだからサーバの問題じゃないよねぇ? 

  3. ログは各自の判断で残すこともできるし、検索とかはお任せってことで。ま、たぶんそんな機能があるってことを知ってる人はほとんどいないだろうけど。 

  4. これは最近 RSS リーダーべったりになっちゃったからかもしんないし、データの取り込みさえできればインターフェイスはみんな同じにできそうっていうのがもう見えてるからだな。 

  5. Widget とか bookmarklet にすればいいのかもしんないけど。 

自分の行為の正当性はどこからくるのか

404 Blog Not Found:取材拒否の理由

別に今さら大上段から NHK やマスコミ批判をしたいなんてことはこれっぽっちも思っちゃいないんだけど、自分の正しさを信じて疑わない人は案外いるよなぁって思った。

もちろん、常に自分の正しさを疑うことでそれが自信のなさに繋がり、臆病でビクビクした挙動不審な人間になる1とか、何かの宗教などにすがって思考停止してしまうという状態になるのも困るんだけども、まるっきり疑いがないというのもやっぱり困るんだな。

例えばそれが好意の押し売りレベルの日常的な程度なら愛想笑いしてりゃいいんだけど、マスコミや免許稼業2の中には何らかの特権が世間やお上によって与えられていると勘違いする人も出てくる。まぁ個人的には出てきて当然かなとも思う。

自分の場合を振り返ってみると、なんていうか、振り子のように自信満々モードとマジヘコミモードの間を行ったりきたりしてるような、そんな感覚だ。単振動っていうほど単純ではなく、その周期も振れ幅も不定。振り子運動へ加わるエネルギーは外からのものもあれば中からのものもある。3

例え話のままいくと、上に出てくる「困った人」っていうのは、外からのエネルギーだけでどっちかにばーんと行ったまま帰ってこない人のことなのかなと思う。たまにいる電波くんは中からのエネルギーだけでどっちかに行って帰ってこない人かな。

何が言いたいかっていうと、振り子でどっちかに寄ったままっちゅーのは重力に逆らった不自然な状態なので注意しましょうってことですよ。特にどっちかに振れやすい環境にいる人はね。まぁ、自然な状態を維持するのもそれはそれで大変なんだけどさ。

  1. 自分にもそんな時期はあったし、これからもどうなるかは正直分かんない。 

  2. 先生先生言われる職業とかね 

  3. 本当は振り子じゃなくてバネやゴムの縦波の方がイメージとしては近いんだけど、例えとして分かりにくいのでやめた。 

FreeSBIE

FreeBSD の Live CD その名も FreeSBIE なのであるが、、、重い。回線が重過ぎて全然ダメです。

うえーん。

肉がもたれた

今ごろ思い出したが、そういえば閏肉の日に焼肉を食べていた。(閏肉を 忘れたわけでも食べたメニューを忘れたわけでもないが、リンクするまで に2日掛かった自分の頭。。。)安くてよいと評判だったので行ってみた チェーン店。

もたれた。

金沢の焼肉はたかちゃんに限るとの思いを新たにした。でも、たいがい 混んでて入れないんだよなぁ。大人数なら予約していけるんだけど、4人以下での予約不可は確認済みなので、タイミングをはかる、ねばる、以外に方法がなかったりする。

VikiWiki と blosxom

VikiWiki は本家でもエラー出まくりなので、やっぱりどうも期待できないんだなということにして放置することにした。とりあえず WikiFarm は PukiWiki でどうにか工夫するということで。(Unix なら Symlink が使えて楽なんだが。)まぁ mod_ruby の実験にはなったのでよしとしよう。

blosxom は PHP 版の完成度が高くないが Perl 版のぐちゃぐちゃしたコードがいやで、とりあえず Ruby 版を探し始める。PHP 版の足元にも及ばないくらいに完成度の低いものが見つかる。うーん。pyblosxom ってのもあるが、Python はあんまり見たくないし、xrea で使えない(はず)なのでこれもなんだかなぁ。本家 blosxom がコマンドラインじゃなくて CGI で static HTML を出力できればそれでいいのかなぁ。BASIC 認証だけ掛けておいて。えーん、Perl もいじりたくないなぁ。

About

例によって個人のなんちゃらです