サーバレスなバッチを管理するためのKudaを作成しました

概要

こちらの記事でも書いたのですが、小さなバッチを運用するにはCloud Runのようなコンテナベースのサーバレス環境はとても向いています。

バッチサーバとしてインスタンスを常駐しておく必要が無いのでコスト面でも有利ですし、横にスケールさせやすいと言うのもあります。

ただ、バッチとして考えた場合にはジョブ管理ツールというかワークフローエンジンが無い。1ジョブで完結するものは良いですが、典型的なジョブは以下のように後続処理を持っています。

f:id:pascal256:20190708234925p:plain
Job Flow

後続処理が並列で動いて集約の処理がそれを待つとか典型的な実装ですよね?

ただ、これをマイクロサービスでやるのはちょっと面倒。おそらくMSA的に正しく実装しようとすると全てをイベントドリブンで記述して「JOBaの後にJOBbが動く」と言ったことを連鎖的に書くのだと思います。

これは大規模な処理で並列度を高めるには良いと思うのですが、既存のマイグレーションや小規模な運用では少し面倒です。

なので、シンプルにそれを実現するためのツールとして「Kuda」を作りました。

github.com

Kudaを使うことで各マイクロサービスは後続のことを気にする事無く記述でき、ジョブのワークフローを外出しできます。

使い方

現状、アルファ版でとりあえず作ったレベルでバギー&GCPのみ対応ですが、一応動かす事は可能です。

まず、下記のようなjobs.yamlというYAMLファイルでワークフローを記述します。

name: exec-batch
tasks:
  - name: step-load
    url: https://xxx-app1.run.app
  - name: step-parallel1
    dependencies: [step-load]
    url: https://xxx-app2.run.app
  - name: step-parallel2
    dependencies: [step-load]
    url: https://xxx-app3.run.app
  - name: step-join
    dependencies: [step-parallel1, step-parallel2]
    url: https://xxx-app4.run.app

tasks配下に対象となるジョブの一覧を書いていきます。 nameはそのまま名前で、urlがジョブとして実行したいマイクロサービスのURLとなります。dependenciesには依存(先行ジョブ)を書きます。

同じ依存を書けばそれらは並列で実行されますし、カンマ区切りで複数個書けば全てのジョブの完了を待ってから処理に入ります。

このファイルをGCS上に置いてkuda自体もCloud Runなどにデプロイすれば準備完了です。

ジョブのステータスを確認

JOBの実行状態を以下のようにURLを叩く事で確認できます。「READY -> RUNNING -> DONE」という状態遷移をします。サンプルは未稼働状態なので全部READY。

$ curl http://localhost:8080/jobq 
[(step-load, READY), (step-parallel1, READY), (step-parallel2, READY), (step-join, READY)]

実行

以下のようにexecで実行を開始します。

$ curl http://localhost:8080/exec 
done

このURLは非同期なのでジョブの完了を待つ事なくすぐにレスポンスを返します。 ジョブの進捗を見る場合は先ほどのjobqを実行するか、kuda側の標準出力を見る事で確認できます。 標準出力の結果は下記の通り。

07:09:29 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-1) START:JOB step-btc-collector
07:09:35 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-1) END:JOB step-btc-collector
07:09:35 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-3) START:JOB step-btc-predictor-dtree
07:09:35 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-5) START:JOB step-btc-predictor-knn
07:09:39 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-5) END:JOB step-btc-predictor-knn
07:09:39 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-3) END:JOB step-btc-predictor-dtree
07:09:39 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-1) START:JOB step-btc-scoring
07:09:45 INFO  [cn.or.pa.ku.WorkflowManager]] (ForkJoinPool.commonPool-worker-1) END:JOB step-btc-scoring
07:09:45 INFO  [cn.or.pa.ku.rs.WorkflowResource]] (executor-thread-3) END:WORKFLOW
07:09:45 INFO  [cn.or.pa.ku.rs.WorkflowResource]] (executor-thread-3) CLEAR:JOBQ

仕組み

基本的な挙動としては

  1. execが実行されるとjobs.yamlを解析してDAGを作成
  2. DAGを元に実行状態を表すキューを作ってファイルに保存
  3. キューを読み込んで依存ジョブが全てDONEになってるREADY のジョブがあれば全て実行
  4. 実行時にジョブのステータスをRUNNNINGに変更してファイルに保存
  5. 実行時に実行完了時に自分自身(kuda)を呼ぶようにするために非同期処理で対象ジョブをキック
  6. 全てのジョブがDONEになるまで3-5を繰り返し

となります。

ポイントとしては一つ一つのリクエストは非同期処理で終わらせてるのでjobs.yamlに書いたジョブ全てが終わるまでkudaのリクエストが継続する訳ではない、ということです。

なので、小粒なジョブをたくさん書いてトータル実行時間が伸びたから親のリクエストであるKudaがタイムアウト、とかにはなら無いはず。まだ、ちゃんとテストして無いから肝心の部分がバグってるかもですがw

Kuda自体もCloud Runで動くサーバレス環境で状態はGCSなど外部に保存する想定なので並列度が上がれば勝手にスケールするはず。ただ、並列度が上がれば誤って同時実行しそうな気がするので排他ロックをDB使うなりで実現が必要な気はしてます。

今後の想定

とりあえず今後の想定としては以下の通り

  • ジョブの実行状態とかが見えるWeb UIの作成
  • Cloud Scheduler とかと連携してスケジューラーもWeb UIに統合
  • quarkusで作ってあるのでnative-buildして高速化 # 現状実行時エラーになるのでjavaで稼働
  • 排他処理周りの検討
  • テストとリファクタリングをいっぱい

まとめ

hwrapと合わせて使うことで、Cloud Runなどを利用して簡単にサバーレスバッチを作るためのツールを作りました。

巨大なバッチならSparkとか使えばスケールアウト構成に出来るのですが、そうじゃない場合はk8sとかを使って上手くできないかなー、と思ってたのでようやく開発着手、というところですね。

Dockerというかk8sをベースにバッチ管理だとargoがありますが、あれよりも機能を減らしてでも手軽に運用出来るものに出来たら良いな、と思っています。HTTP対応してれば良いのでk8s/Cloud Runに本来的には縛られないし。

ちなみに名前の由来は「パイプライン」からの連想で「管」と、なんとなくリクエストを連鎖的に発生させて動かすイメージから「管狐」を連想したのが理由です。

それでは、Happy Hacking!

「二段階認証…?」と言わないためのMFA入門 --- あるいはIDシステム地獄への案内

はじめに

さて、7 Payの社長が2段階認証知らなかった問題が良い感じに炎上していますね。 個人的には常に知っておく必要が絶対あるかというと専門分野の問題があるから議論の余地あるとして、セキュリティの問題で会見するなら部下にちゃんとレクさせろよ、とは思う。

7 PayとMFAの話は下記の動画でも話したんですが、7 Pay云々は忘れて「そもそもMFAってなに?」ってところをもう少し整理してまとめていきたいと思います。

www.youtube.com

認証と認可

まずはおきまりの「認証(Authentication)」と「認可(Authorization)」の違いです。

日本語で書いても英語で書いてもややこしいのですが、「認証」とは「Identify」すなわち誰であるかの確認です。対して「認可」は「Access Control」すなわち誰に何をさせる事を許可するか、という事です。

基本的にはMFAは「認証」の技術ですがアクセスコントロールとも実際には絡みます。

二段階認証? 二要素認証? 多要素認証? 2FA? MFA?

二段階認証(2-Step Verification)と呼ばれることが多いですが、2FAあるいは多要素認証(Multi-factor authentication/MFA)と呼ぶこともありますよね?

そもそもこの違いが分かりますか? 基本的には「結果として」同じことを指しますが、1つ以上の認証要素(つまり1種類でも良い)を2回重ねるのが2段階認証で必ず2つ以上の要素を使うのが多要素認証---すなわちMFAです。

定義的に二要素認証は必ず二段階認証ですがその逆は真では無いので要注意です。

では、「認証要素」とは何でしょう? セキュリティ情報の定義を調べるのに便利なNISTの「電子的認証に関するガイドライン(EN, JP)」には以下のように定義されています。

3 つの認証の基本要素

  • Something you(they) know/本人だけが知っていること
  • Something you(they) have/本人だけが持っているもの
  • Something you(they) are/本人だけが持っている特徴

Something you knowは「知識要素」とも言います。本人だけが知ってることつまりパスワードや秘密の質問が典型的な知識要素です。

生年月日や電話番号、親の氏名などもここに含まれはしますがこれらは友人知人はもちろんSNSを介して多くの人が知っているほぼ公知の情報です。

そのため、単独で認証に使うのは弱いでしょう。今回、7 Payで問題になってるのはこれを単独で使ってるからですね。

Something you haveは「所有要素」とも言います。分かりやすいのは昔銀行とかで配ってたセキュリティカード(乱数表カード)ですね。

他にもスマートフォンを用いたSMS認証やメールアドレスによる認証もこれに入ります。二段階認証と聞いて多くの人がイメージするのは今は多分これかな?

他にも端末認証やGoogleのTitanみたいなセキュリティキーやRFID/磁気カードによるスマートカードスマホや専用デバイスを使ったOTPもここに含まれます。あと、自宅とか。

Something you areは「生体要素」とも言います。いわゆる生体認証ですね。スマホ指紋認証に対応したのを皮切りに今では顔認証とかも結構一般的ですよね。他にも静脈認証とか虹彩認証もあります。

これを組み合わせて認証を行うのがMFAです。つまり、パスワードの後に第二パスワードや秘密の質問を聞くようなパターンは「二段階認証」ではあっても、「二要素認証」ではありません。

なんで、多要素が必要なの?

さて、なぜ多要素認証が必要でしょうか? それは単純に複数の要素を同時に揃えるのは難しいからです。

7 Payを例にとってみましょう。7 Payではパスワードやメールアドレスの変更に必要な認証として「ID(現在のメールアドレス)」「電話番号」「生年月日」というとても弱い知識要素を使って本人認証をしています。

なので、容易に攻撃されるわけですがパスワードなどが使えないかつ重要度の高い操作を行うときの鉄板は「変更用URLを登録済みのメールアドレスまたは電話番号のSMSに送信する」ですよね? これは基本情報処理試験にも載っているごく初歩的なテクニックです。

MFAという言葉が無い時代から使われていますが、これは「メールアドレスという所有要素」を足しているMFAなんですね。このように本人確認に複数要素を使うのは一般的で金融機関だと「家に郵送する」という手段によって「自宅」という所有要素を確認するケースも多いです。

同じ要素による二段階認証がなぜセキュリティ的に微妙かというと「同じ手段で抜かれてしまう可能性が高い」からです。例えば、現代は正しいIDとパスワードの組み合わせは普通に流出しているので攻撃者は把握していますが、同じ知識要素である秘密の質問や第二パスワードも同じ手段で抜かれる可能性が高いです。

そのため、違う系統の要素を足すことで効果的にセキュリティを高めようというのが基本的な発想です。

認証要素の組み合わせに関しては、こうしす!さんの「イマドキの小学生は「多要素認証」を正しく使いこなすらしい 」が分かりやすかったです。

www.atmarkit.co.jp

MFAとリスクベース認証とCAAC

MFAそのものでは無いのですが、MFAと合わせて使われる技術としてMFAとCAACがあります。 ついでなのでこれも簡単に説明します。

リスクベース認証

リスクベース認証とは、怪しい振る舞いをしたユーザ認証に対して追加の認証用を加える仕組みです。

例えば、5分前までに日本でログインしたのに次のアクセスがいきなりアメリカだとおかしいですよね? このように強い認証基盤には不正検知(Fraud detection)が必須です。 不正検知は時間やGeo Locationを利用した仕組みが基本だとは思いますが最近はAIなども活用されてかなり高度な分野です。 加えて、現代のセキュリティの常識として「攻撃者は正しいIDとパスワードを知っている」という前提があります。なので、単にブルートフォースを単純に弾けば良いという物でもなくなってきておりいかに「怪しいログインであるか?」を特定するのが鍵となってきています。

でも、怪しいけど正しい動作のこともありますよね? 5分でワープは出来なくても次の日にアメリカに行くこともあるでしょう。普段と違ってカフェの公衆WiFiから繋ぐかもしれません。いつもとは違うPCを使うことだって当然ありますよね?

なので、不正検知により「リスクのある認証」と判定した物は追加の認証を行うのがリスクベース認証です。この時に別な要素、例えばSMS認証とかOTPを使う事が一般的だと思うのでMFAとも大きく関連する技術要素です。

Context-Aware Access Control(CAAC)

アクセスコントロールというと昔からロールベースアクセス制御 (RBAC)とかあるわけですが、ロールだけではなくコンテキストをより適切に見ていこうという認証方式です。

コンテキストとはアクセス時にダイナミックに決まる情報なのですが、まあザックリ書くと

  • 誰が
  • どのデバイス
  • どこから
  • どのシステムの
  • どの操作をするか

といった複数の要素を加味してアクセス権を付与する方式です。

この辺、実装が先行してる気がしていて学術的な定義があるんだか無いんだが分からん感じです。論文だとこの辺りだと思いますが、普通にGoogleのCloud IAPやAzureADの「条件付きアクセス」ですね。

この仕組みの良いところは、人が同じでもアクセスしてる場所や対象機能が異なれば権限を変えれる事です。なので、例えば同じ人でも会社からアクセスする時と自宅からアクセスする時で権限を変更できるんですね。 しかも、通常はリスクベースの認証と組み合わせて自宅などから作業するときは認証を追加することも可能です。

ロケーションとかを加味せずに例えば同じアカウントでも明細を見るときはパスワードだけで良いけど決済機能を使うときはOTPを経由する、みたいなのも一般的な実装になってきたと思いますがこれがRBACの範疇なのかCAACに含まれるのか勉強不足で判断が出来ないです。。。詳しい人の解説を求む。

IDシステムを自作するのは止めよう!

読んで分かる通りMFAというか認証システムは奥が深いです。とりあえず関連することを図にしてみましたが特に認証がAIとか必要だしやること多い。アクセスコントロールも実装は状態遷移複雑でめんどそう。 f:id:pascal256:20190706151859p:plain

MFA周りは実はライブラリも充実してるしどうとでもなる気はしますが、真の闇は不正検知とリスクベース認証です。 この辺りを真っ当に運用するためには相当の技術と監視体制が必要です。国内外を問わず昔からtoCのサービスを出してる所は内部にそれなりのコストをかけて仕組みを作っています。戦いの歴史なので。

でも、スタートアップはもちろん大手企業ですらいきなりそのレベルの仕組みを作るのは困難だと思います。何しろ「認証システムなんてビジネス的にはどうでも良い」事がほとんどでしょうから。。。

そんなところに予算を積むほうがおかしいです。なので、特別な理由が無い限りは自作はやめましょう。

エンタープライズシステムならAzureADなどのIDaaSに乗っかるのが一番です。何しろMSは「世界で2番目にアタックされてる組織」なので投資規模が計り知れないです。とりあえずSAML2対応しとけばなんとかなる。

コンシューマーはベストプラクティスがまだなくて、CIAM(Customer Identity Access Management)という言葉もで始めてるようですが代名詞的な製品はまだ無い気がします。 とりあえず、Google/Twitter/Github/FacebookあたりのSNS認証を使って自前では認証を作らないのが基本でしょうか? 独自ID作りたいのは分かるけど「認証の不正検知を作り込む予算と時間を得るまで」我慢しましょう。

まとめ

さて、MFA周りについてざっと書きましたが如何でしたでしょうか。今回はFIDOがどうとか規格/実装側には触れず概念的なことを中心に書きました。 とりあえず大事なこととしては「MFAの要素として如何ありそれを複数個組み合わせるのが重要!」ですね。

  • Something you(they) know/本人だけが知っていること
  • Something you(they) have/本人だけが持っているもの
  • Something you(they) are/本人だけが持っている特徴

あと、不用意に自前で認証システム作るのは脆弱性の元だからやめましょう!

それではHappy Hacking!

参考

どんなコマンド/スクリプトもサーバレス化するためのhwrapを書いた

ちょっとした処理を実行する時にはサーバレス(FaaS/CaaS)環境は便利ですよね。 呼び出しベースでコストが最適化されるのでバッチサーバ的なものを常駐させておく必要もありません!

ただ、AWS Lambdaとかで書くと特定の言語/FWで実装する必要があるので過去に書いたちょっとしたスクリプトとか、 シェルで良いのにレベルのことをサーバレス化するのには不便でした。

そんな時に便利なのがCaaS(Container as a Service)です。HerokuやAWS Fargate、最近だとk8sベースのKNativeとそれをGCPでマネージドしてるCloud Runあたりが有名です。

こいつらはDockerにさえ対応してれば良いので言語やFWに縛られる必要はなくOSのパッケージコマンドも使い放題です。実際、Cloud Runのデモでは、LibreOfficeを使ってPDFを生成していました。

ただ、基本的にはHTTPをトリガーにしてるので一々HTTPサーバ作るの面倒だな。。 と思ってラッパーになるコマンドとしてhwrapを作りました。

github.com

使い方は簡単です。ポートを指定して任意のコマンドを引数に取るだけ。

$ hwrap -p 8080 "date"   
port:8080, commad:date, args:[]

ポートは-pまたは--portで指定します。特に指定しない場合はデフォルトの8080を使用します。 この状態で別のターミナル等からアクセスしてみます。

$ curl http://localhost:8080 
success

successが表示されましたね。この状態で元のターミナルにはdateコマンドの実行結果が表示されます。

$ hwrap -p 8080 "date"   
port:8080, commad:date, args:[]
Sun Jun 30 11:17:50 PDT 2019

引数付きの処理も対応できます。その場合は""で括る必要があるので

$ hwrap -p 8080 "ls -l /" 
port:8080, commad:ls, args:[-l /]

$ hwrap -p 8080 "python script.py" 
port:8080, commad:python, args:[script.py]

となります。実行結果は標準出力なので、例えばStackDriverのようなログ管理機能と相性が良いと思います。 今回は日付関数の結果を出してますが、通常はバッチスクリプトの進捗とか実行結果を出すと思いますし。

まとめ

セッション管理機構を自作してはいけない。理由:自作することで脆弱性が混入していた例が多々あるためリスクヘッジ

http://raichel.hatenablog.com/entry/2015/02/14/010110

CGIみたいに実行結果を戻り値として欲しいというよりは、小さなバッチをサーバレスな環境で実行したいと思って作ったコードでした。 何気にほぼ初Go言語になるので開発方法も学んで行かないと。

本当は適切なマイクロサービスに切り出してストリーム処理ないしはイベントドリブン処理にするべきなんでしょうが、シンプルなバッチもやっぱ作るのが楽というメリットはあるんですよねぇ。バグ対応も簡単だし。 なので、この辺を自分用にもうちょっと整理していきたいかな。エラー処理とかもちゃんと500返すようにしたり。

それではHappy Hacking!

なぜRDBからCSV + COBOLに変更する事でコスト削減と高速化を同時に実現出来たかの考察

TLを見てると以下の記事が少し話題になってました。 tech.nikkeibp.co.jp tech.nikkeibp.co.jp

対象の記事は有料会員じゃないと見れないのだけど事例としては以下みたい。

リソース - ユーザー事例 - COBOL製品 ユーザー事例 : マイクロフォーカス

さて、この記事の驚きポイントは「1億レコードくらいのDB処理をRDBからCOBOL + CSVに変更してUnixサーバからWindowsサーバに変える事で性能を維持しつつコストを1/5くらいにした」という事でしょう。 「せっかく7割もあったSQLを全部COBOLに変えるとか時代に逆行しすぎ!」とか「RDBをファイルに変えるなんてとんでもない」とか思う人も多いんじゃないでしょうか。とりあえず私は思いましたw

しかし実際に高速化はありえるのか、その理由を分析していきましょう。

そもそも既存はどんなロジック?

中の人じゃ無いので分かりません! 分かりませんが、推測はできます。

1年分が約1億レコードに迫るデータ量で、それを1度に数年分扱う大規模システムである 毎月、会員保険会社から契約情報、保険金支払情報などのデータを収集、機構内でデータを処理した上で料率計算の基礎データ作成を行います

月次処理として過去情報から利率を求めるってことは本質的にオフラインバッチです。つまり、トランザクションの考慮は不要です。

もともとこれはメインフレームで稼働していました。〜中略〜 2003年にUNIX環境へ移植 COBOLシステムは日本IBM製のメインフレームで稼働しており、保守コストの高さも問題になっていた。 その内訳はSQLが7割、COBOLが2割、C言語が1割。SQLのユーザー定義関数を利用し....

直接の移植はUnixからですが、ベースのビジネスロジックメインフレームで作成されていた事が分かります。 SQLが7割もあるのでハッキリとは断言できませんが、メインフレームで作っているならベースのビジネスロジックはIMSのような階層型データベースやVSAMといったファイルベースの可能性があります。 その場合、RDBを使っていても単にキーでデータを取得する単なる箱になっている可能性は高いです。

というのもIMS/VSAMはSQLのような条件付きデータ取得ができません。そのため典型的なメインフレームでのDB操作は「全てのレコードを読み込んでプログラムで柔軟に条件判定を行う」という作りをしています。

疑似言語 + SQLで書くと以下のような処理は

sql = "SELECT SUM(amount) FROM hoge WHERE flag == 3"
result = db.exec(sql)
puts result

メインフレーム的なアーキテクチャだと以下のように書く必要があります。

def isValid(record)
    record.flag == 3
end

sum = 0
db.get('hoge').records.each{|record|
    if isValid(record)
        sum += record.amount
    end
}

全部データをPG側で舐めているのでフルスキャンと同等、いえネットワークコスト等も加味するとそれ以上の負荷がかかる事は容易に想像できるでしょう。 公開されてる情報からは想像の世界を出ませんが、メインフレーム時代から使ってるならこのような構造の可能性は高く、今回はその方向で考えます。

RDBなんだからWhere句使ったら?

さて、既存のビジネスロジックが前述した通り総ナメ処理だと仮定し、性能が足りないという状況。皆さんならどう改善しますか? ほとんどの人は「ちゃんとRDBに仕事をさせろ、WHERE句使え。最悪インデックスが無くてもそっちのが爆速」と考えるでしょう。 王道ですね。しかし、単純にそうはいかないケースが多々あります。

例ではisValidはシンプルにレコードの中の特定カラムの値を比較するだけでした。 しかし、実際は遥かに複雑な条件判定ビジネスロジックが幾重にも使われてる可能性が高いです。別のテーブルの値と相関比較は当たり前に出てきます。そもそもそもそもDBの数字型や文字列型では無く、COBOLのデータ型のバイナリがそのままRAWとして突っ込まれている可能性すらあります。古いコードはデータ容量節約のためにboolean的なフラグとか使わずにビットをシフト演算で判定するとかあるあるですし。 こうなってくると単純にWHERE句に入れるのは難しい。

根本的なビジネスロジックを考え直すか、JOINやCASE文を駆使してSQLに無理やり判定処理を移植するしか無いです。そして、後者はフルスキャンになりがちですし性能が伸び悩むこともあるんじゃ無いかと。 ユーザ定義関数を色々使われてるし、7割もSQLがあるということは割と後者寄りの対応が元々はされていたのかもしれないです。

前者の根本対応が出来ればベストですがそれは基本設計からやり直しなので、移植や更改レベルのコストだと割に合わない可能性は高そうです。

なぜファイルにすると速くなるのか?

最初からSQLを前提としたビジネスロジックでは無く、RDBを使ってるとしても単なるキー検索の箱か無理やりSQLで複雑なロジックを組んでる可能性が高いのでRDBの特徴である集計処理を活用できてない可能性を述べました。

では、なぜファイルにすると速くなるのでしょうか? まずファイルといってもJSONXMLなどのような柔軟な構造化データでは無く、おそらく固定長データ処理を行なっています。そのためシリアライズが極めてシンプルなので高速化が期待できます。 固定長というのは「1レコードは10バイトあって「1バイト目はID」「2から5バイト目は項目名」「残りの5バイトは数量」みたいな暗黙のルールでデータを記述していき、読むときにルールに従って前から順に解釈していく」という方式です。今でもHDFSの内部データとか高速性を要求される部分には普通に使われている技術です。

余った項目は空白とか0で埋めます。CSV自体は可変長のデータフォーマットですが、固定長的にこれを処理してるんじゃ無いかと思われます。 というのも固定長データとCOBOLのレコード構文は非常に相性がいいためです。

固定長でファイルを処理している場合はRDBに比べてレコード読み込みは非常に高速です。 まず、SQLや同時アクセス/トランザクション周りのコストがありません。またネットワーク通信では無いのでI/Oも高速です。

加えて、COBOLで固定長を扱うなら「シリアライズ」が不要です。ほとんどのCOBOLではレコードのメモリの中身をそのままファイルに吐き出すのを基本とします。 これはある種のバイナリフォーマットですが*1シリアライズのコスト無くデータを読むことができます。

加えてCOBOLコンパイル言語なのでわりかし速度は速い方です。 複雑な条件判定自体もSQLで無理やり実装するよりは単純に高速化される可能性は高いです。

これらのポイントにより例え全データを舐める処理になったとしても、最適な利用をされて無いSQLより大幅に高速化される可能性は十分にあります。

並列化と分散処理による高速化の可能性

今回使ってるかが分かりませんが、データを例えば種別とか年月で分割して処理できるなら並列処理/分散処理による高速化も期待できます。 トラディショナルなRDBは同時リクエストを受けるのは得意でも一つの処理を並列処理するのはさほど得意では無い気がします(パラレルクエリとかもあるけれど)。加えて根本的にスケールアウトさせずらいアーキテクチャというのもあります。

そのため、そういった処理が可能なデータ特性/ビジネスロジックであった場合は単純にマルチスレッド、ジョブの並列化、あるいはHadoop/Sparkの活用*2することで高速化を狙うこともできます。 Unixサーバ(AIX?) Windowsサーバになったのでクラウド化や社内でもvmwareを使ったスケールアウトは容易になるでしょう。実際、テスト環境は作りやすくなったとあるし。

COBOL使う必要あったの? Javaとかじゃダメだったの?

これは出てる情報だけからはなんともですね。一応記事中には「バッチ処理がメインのためJavaでは速度が出なかった。」とありますが個人的な認識としてJavaはそこまで遅い言語では無い。 考えられるのはプロジェクトの経緯的にJavaの時点ではRDB撤廃は決まってなかったのでRDB + Javaが比較の対象であるとか、COBOLや他のレガシーとの互換性を保つために検討したFWが重いとかそういう方向じゃ無いかという気がします。

JavaやあるいはRubyなんかを使ってリライトしても「CSVじゃ無くてKryoやMassagePackのような高速なシリアライザを使う」「個々のリライトコードではCOBOLらしさとか互換性は捨てる」という事をすれば結構速度出たのでは? という気がしなくも無い。

たぶん、単純な速度以上に他言語への移植コスト/移植後のメンテコストを加味した結果かな、という気もしています。COBOLの開発に慣れてるなら人も活かせるし。 COBOLにコミットした開発体制を維持するならCOBOLに無理がない範囲で集約するのは一つのジャッジで、5年程度の延命としては十分かな。 10年後、20年後は技術者もさらに減るだろうし分からないけどCOBOL「私を殺すと言ってた言語は、みんな死んだよ」 | おごちゃんの雑文という話もあるので、大丈夫な気もする。

まとめ

さて、色々仮説を重ねてはいますが「RDBをやめてCSV + COBOLにしたら高速化した理由」を考察してみました。あってるかどうかは知らないですが、大きく外してもないんじゃないかと。 「ファイルやCOBOLにするとか時代の逆行!」と思いがちですが、新しいものが常に最適解とは限らないので多くの技術を学んで自分たちの状況に最適なものを選んでいきたいですね。

どうでも良いけどCOBOLの移植先としてはRuby(特にJRuby)が、Decimalも簡単に扱えるし比較COBOL的なシンプルさと柔軟な抽象化を両立できると思うので結構良いと思ってる。JavaよりCOBOLっぽく書きやすいと思う。フレームワークは作る前提だが。 だから、金融系SEはもっとRubyに可能性を感じるべき!

それではHappy Hacking!

*1:COBOLの場合は文字列や数値をテキストとして内部データも保持してるので結果として一部をのぞいてテキストになります。ASCIIかはさておき

*2:COBOLHadoopで動かす話はそれなりに事例がある。

ついにDockerに対応したWSL2を私見で解説してみた

多くの記事も出ていますがMicrosoft Build 2019にて、Windows Subsystem for Linuxの次期版でフル互換のLinuxが動作するとの発表がありました。 これの何が嬉しいってDokcerがWSLで普通に動かせるようになる事です! www.publickey1.jp

セッションを聞いて来たのですが中々面白かったので、自分の理解できた範囲でWSL1から何が変わったのか、どう実現しているのか? を解説していきたいと思います。 私の理解が間違ってるかもしれないので、違っただ誰か指摘ください。

そもそもWindows Subsystem for Linuxって何?

WSLに関して知らない人もいると思いますが、Windows上でLinuxを動かすための仕組みです。 主な用途としては開発ツールチェインを始めとしたLinuxの資産を活かすためなのですが、類似のツールは結構たくさんあります。

などなど。かつてはcoLinuxとかもありました。 Cygwin/MinGW/Git-Bashはツールチェインの移植です。手軽な反面Linuxそのものでは無いので細かく挙動が違いますし、aptなんかも当然使えません。 一方で、Docker for Windows/Docker Toolbox on WindowsWindowsVMを入れて本物のLinuxを起動した上でLinuxを動かすという方法になります。 Linuxに閉じてサーバ開発とかをする分には十分ですが、Windowsから完全に隔離されるのでWindowsの操作を拡張する という用途では使えません。 その辺を両方とも解決してくれるのがWindows Subsystem for Linux すなわちWSLです。

ターミナルとしてLinux環境であるUbuntuSUSEが動くのはもちろんのことWindows環境とファイルは連携されているし、Windows側のPGをWSL上から起動することすらできます。 Windows 10から搭載された機能なので開発者以外には興味ももたれなかったと思いますが、ここ最近で一番アツい機能の一つです。

WSL1はどう動いてる?

WSL1は発表資料を引用すると下記のような構成になっています。 f:id:pascal256:20190509160244p:plain

私の理解だとWSL1はある種のUser Mode Linuxです。 LinuxカーネルへのシステムコールWindows NTカーネル上に作成しバイパスする事で動作しています。

いわゆるx86を全てエミュレートする仮想マシンモデルよりは、こう言ったシミュレータの方が原理的には高速ですし、Windows側との親和性も高く出来ます。 それでいて本物のLinuxディストリビューションが動くのでaptとかyumとか使えて便利ですね。

ただ、以下のような問題もありました。

なので、どうしても微妙に使いづらい感があったのは正直なところです。 特に今はDocker全盛時代でそれが動かないのは個人的には困りました。そこだけDocker Toolboxに接続するとかまあ色んな裏技を試していましたけど、そうすると細かい問題がチラチラと。

この問題はおそらくWindows上でLinuxのシステムコールを実装してることに起因しています。 セッション中でも「メモリモデルとかも違うし単純に1:1に移植できるわけじゃ無いからシステムコールのフル実装は辛い」という感じのことを言っていました。たぶん。 やはり、同じユーザ空間でのシステムコールの実装と言ってもLinux同士で単純にバイパスできるケースがほとんどのUMLやgVisorとは、何もかもが違うNTカーネルLinuxシステムコールを移植するのは相当大変なのでしょう。そもそもマイクロカーネルモノリシックカーネルというレベルで違うし。。。 この辺りが起因してWSL1のいまいち感は出ていました。

WSL2はどう動いている?

というわけで本題のWSL2です。野心的な試みのWSL1に対してWSL2は一見時代に逆行するシンプルなVMアプローチです。 f:id:pascal256:20190510115022p:plain

ただ、VMと言ってもAWSのFireclakcerのように1秒未満の超高速で起動するマイクロVMアプローチになります。 LinuxカーネルもNTカーネルHyper-V上で起動してユーザプロセス同士で通信はするって感じですね。

本物のLinuxカーネルVM上で動いているのでDocker含めて当然全ての機能が動きます。Hyper-Vベースなので技術的にこなれてるのもポイント。

さらにWSL2の環境毎にVMを立ち上げるのではなくLinuxカーネルは一つだけ立ち上げて、それぞれのWSL環境はコンテナで仕切っている様です。 f:id:pascal256:20190510115225p:plain

この辺りはHyper-Vを使ったDockerの実行方法であLinux Containers on Windows (LCOW)の技術を応用してる感じですかね。これによってVMによるオーバヘッドを極小化しているため、demoを見ている限りでは起動など劣化はほぼ見られませんでした。

f:id:pascal256:20190510114820p:plain

なお、Linuxカーネル自体もマイクロソフト謹製なので用途最適化されてる様です。たぶんVM前提でデバイスドライバ減らすとかそういうの。 ちなみにWindows UpdateLinuxカーネルがバージョンアップされる様になったとのコメントには会場も爆笑でしたw

パフォーマンス

ファイル周りの遅さが改善されてパフォーマンスは大きく向上している様です。 f:id:pascal256:20190510114336p:plain f:id:pascal256:20190510114450p:plain

理論的にはUML方式のが速い気がするのですが、実装の最適化度合いやファイルシステム周りの違いかな。 NTカーネル経由せずに直接ハイパーバイザを叩くならAPI的にもシンプルな可能性がありますし。 また、VMだと使用メモリ等が気になるところですがデモ中リソースモニタで監視しててもほとんど増えず、カーネルを仮想で動かしてる部分に関してはかなり小さい様です。

CPUヘビーな処理だとどうなるのかはちょっと気になるところですが、最近のVMでCPUが極端に劣化するイメージも無いので通常用途は大丈夫な気がします。

Windows側との連携

これだけだと「LCOWでDokcer起動すれば良くねって?」って話になるのですが、Windowsとの連携こそがWSLの真骨頂。ちゃんとWSL2も以下の特徴は引き継いでる様です。

  • Windows上のファイルがWSLから見れる
  • WSL上のファイルがWindowsから見れる
  • Windowsの実行ファイルをWSLのターミナルから実行できる

WSLからのアクセス f:id:pascal256:20190510120351p:plain

WSLへのアクセス f:id:pascal256:20190510120535p:plain

どうやって実現してるのかというと、まさかの9P(Plan 9 Filesystem Protocol)です。令和の時代にまさからPlan9の単語を聞くとは思わなかったですが分散OSなので、こういうのには向いてるのかもしれないです。 一応「SMBとかNFSとかなんで使わないの?」って質問に対して「こっちのが単純だから」と答えて気がします。ちょっと自信なし。

あと、あまり実装の詳細は分からなかったですが従来通りWindows側の実行ファイルをキックしたりも出来ていたので、この辺りの開発者フレンドリーなUXを作って来るのはさすがMSって感じがしました。

このおかげでWindows環境のツールチェインの一部としてLinuxの機能をフルに利用する事が出来ます。

まとめ

WSL2によりFuseやDockerがWindows10で簡単に動かせる様になりそうです。 これによってCI環境でWindowsコンテナとLinuxコンテナを混在させたかった課題とかが解決しそうで個人的にはドンピシャなソリューション。

それをおいてもWIndowsLinuxの開発はかなりしやすくなりますし、Windowsの補助ツールとしても使える。 もしかしたら将来的にはWIndowsを直接サポートせずにWSL前提なOSSとかも出てくるんじゃないかと思います。リリースが待ち遠しい!

そうなって来ると最近はMacもいまいちだし、Surface欲しくなるなぁ、とそんな気持ちになりますね。Linuxは敵だと言っていたMicrosoftも今は昔。

それではHappy Hacking!

参考

Serverless時代のJavaEEコンテナ - Quarkus

はじめに

Quarkusをご存知ですか? Redhat社が出した爆速のJavaEEコンテナです。

Publickeyの記事でも紹介されていますがGraalVMのnative-imageでコンパイルされるため、JAX-RSCDIJPAや100ms以下の起動速度と省メモリを誇るスーパーソニックなが最大の特徴です。

WildflyやThorntailがあるのになんでまた作り始めたの?」となりますが、サーバレスへの対応は既存のマイクロサービスへの対応だけでは不十分だからです。 ここで言うサーバレスはAWS LambdaのようなFaaSに限ったことではなく、実際のコンピュートではなく使用リソースなどを指定してオンデマンドでプロセスを立ち上げたり課金がされるような仕組みのことをさします。

JavaEEコンテナの世代と移り変わり

JavaEEコンテナは現在大きな変革を求められています。J2EEからJavaEE5にシフトした時以来の大きな変化だと思います。 そもそもJavaEEWebアプリケーションフレームワークというより分散システムとして始まります。 当時はそれ向けエコシステムがあったわけではないでしょうし、基本的に全てJavaEE内でリソース管理をすることを想定しています。 なのでアプリケーション実行エンジンに加えて「負荷分散」「クラスタリング」「QoS/リソース管理」「モニタリング」「デプロイメント/バージョニング」「Isolation」までをカバーするのが他言語のフレームワークと少し違うところです。 ベンダーの独自拡張もありますがそうはいっても上記の基本的な要素はJavaEEの仕様としても入っており標準化されているのが良いですね。これを仮に第1世代と呼びます。

しかし、時代は代わりマイクロサービスが台等してくるとエコシステムも進化して、JavaEEがもともと持っていたような管理機能はk8sやIstioによって実現されるようになってきました。 こうなると、他のマイクロサービスと異なりJavaEEだけ独自の管理機能を持ってるのは不便です。そのため他システムとのインテグレーションのために再定義された仕様がMicroProfileであり、MicroProfileだけをターゲットにすることでスリム化を計ったのがPayara MicroやThorntailです。

以下の画像に載ってるように以下のようコンテナ機能およびその管理機能がk8sに移行されミドルウェアの領域が小さくなります。 f:id:pascal256:20190415045549p:plain 引用 - Kubernetesを軸に再定義されつつある、新しい「クラウド対応」の意味とは

これを仮に第2世代と呼ぶとします。JavaEEではありませんがSpringBootも第2世代ですね。

さて、ここからがQuarkusの話ですが第2世代ではまだ不十分です。 それはコンテナ化とマイクロサービス化が進んだ結果としてサーバレスが注目され始めたからです。 実のところサーバレスなアプリケーション実行環境というのは昔からあります。代表的なのはHerokuとGoogle AppEngine(GAE)です。 これらのサービスはリクエストに応じて処理をスケールさせることができ、使ってないときはインスタンスが起動してないのでコストを払う必要がありません。 この構成はコンテナと非常に相性が良い(というか上記サービスは元々独自コンテナでそれを実現していた)ので、リソースやコスト(含む運用コスト)の効率化のために再度注目されてると思います。 そこで重要になってくるのは要求があってから起動してリクエストを返すまでの時間、つまりスピンアップタイムです。なぜならサーバレスではインスタンスが一つも起動してないことは普通にあり得るので。 これはスケールアウトはあっても基本的には常駐させることが前提の従来的なマイクロサービスの管理とは要件が異なります。 これを第3世代---は少し言い過ぎなので2.5世代と呼ぶします。@kisからコメントがあったMicronautJavaEEでは無いですが2.5世代ですね。これもGraalVM使ってネイティブコンパイルしています。

Table: JavaEEコンテナの世代の比較

Item 第1世代 第2世代 第2.5世代
Product Wildfly, GlassFish Payara-Micro, Thorntail Quarkus, Helidon
サポート機能 Full JavaEE Micro Profile Micro Profile
スピンアップ 分オーダー 秒オーダー ミリ秒オーダー
Challenge 分散システムの管理 他のMicro Serviceとの統合 スピンアップタイムの高速化

ミリ秒オーダーの起動を実現するには従来のコンポーネントを徹底的に見直す必要があります。 また、基本的にはリクエスト毎にコンテナが立ち上がれば良いのでスレッド管理周りも強力なものは不要ですし、コンテナでデプロイするのでクラスローダ周りもシンプルにできます。 また、GraalVMのnative-imageによるバイナリへのコンパイルが大きな鍵になります。その際にSpringBoot等も含めて既存のモジュールではビルドが困難なので利用可能なものだけを組み合わせるためフルスクラッチで作り直してるのだと思います。 OracleHelidonもnative-imageはまだ実現していませんがコンセプトは同様です。

Quarkus

さて前置きが長くなりましたがQuarkusについてです。 MiroProfileによりJAX-RS, CDI, JPA, Config, OpenAPI, Tracingなどが使えます。それに加えてVert.xやKafkaとの連携も組み込まれています。 それでいて爆速起動/省メモリなのが特徴です。

f:id:pascal256:20190415053510p:plain 引用 - Quarkus公式

また、ビルド形式をJARとnative-imageの2種類用意してあります。

まずはmvnコマンドでプロジェクトを作成します。

mvn io.quarkus:quarkus-maven-plugin:create        
....
[INFO] --- quarkus-maven-plugin:0.13.1:create (default-cli) @ standalone-pom ---
Set the project groupId [org.acme.quarkus.sample]: 
Set the project artifactId [my-quarkus-project]: 
Set the project version [1.0-SNAPSHOT]: 
Do you want to create a REST resource? (y/n) [no]: y
Set the resource classname [org.acme.quarkus.sample.HelloResource]: 
Set the resource path  [/hello]: 
Creating a new project in /private/tmp/my-quarkus-project
Configuration file created in src/main/resources/META-INF/application.properties

...

[INFO] BUILD SUCCESS

続いて実行してみます。以下のコマンドでホットデプロイが効く開発モードで起動が可能です。

$ mvn compile quarkus:dev
...
2019-04-14 13:31:50,697 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-04-14 13:31:51,988 INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 1291ms
2019-04-14 13:31:52,564 INFO  [io.quarkus] (main) Quarkus 0.13.1 started in 1.966s. Listening on: http://[::]:8080
2019-04-14 13:31:52,566 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

curlで実行してみます。

$ curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/plain;charset=UTF-8
Content-Length: 5
Date: Sun, 14 Apr 2019 20:33:55 GMT

hello% 

次にネイティブイメージにコンパイルして実行します。 ネイティブイメージへのビルドは依存関係を全部コンパイルし直してるのでちょっと時間がかかります。

$ export GRAALVM_HOME=/Applications/Graalvm.app/Contents/Home/
$ mvn package -Pnative

実行してみましょう。

まずはJVMから。

% java -jar my-quarkus-project-1.0-SNAPSHOT-runner.jar   
...
INFO: Quarkus 0.13.1 started in 1.356s. Listening on: http://[::]:8080
Apr 14, 2019 2:07:52 PM io.quarkus.runtime.Timing printStartupTime
INFO: Installed features: [cdi, resteasy]

1.3秒で起動しました。まあ悪くない数字です。第2世代と比べても速いですし、第1世代では質レベルで違う速度。 続いて使用メモリをチェックしてみましょう。

% ps aux 18124
USER     PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
koduki 18124   0.0  0.2  8014528  18684 s002  S+    2:07PM   0:03.41 /usr/bin/java -jar my-quarkus-project-1.0-SNAPSHOT

8MB程度使っていますね。いつも1GBはおろか32GBとか平気でJavaに指定してる身からすれば劇的に小さいです。

では続いてネイティブバージョン。

$ ./my-quarkus-project-1.0-SNAPSHOT-runner     
2019-04-14 13:59:01,168 INFO  [io.quarkus] (main) Quarkus 0.13.1 started in 0.011s. Listening on: http://[::]:8080
2019-04-14 13:59:01,303 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

0.011秒という劇的な速度で起動してることが分かります。10msとか並みのレスポンス速度よりも速いですね。。。 続いて使用メモリ。

$ ps aux 18084          
USER     PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
koduki 18084   0.0  0.1  4353676   4448 s002  S+    1:59PM   0:00.04 ./my-quarkus-project-1.0-SNAPSHOT-runne
/Users/koduki% 

こちらも4MB程度でJVM版よりもさらに小さい使用量となります。

比べると以下の通り。

Item JVM Native
起動速度(sec) 1.356 0.011
使用メモリ(MB) 7.6 4.2

まとめ

Quarkusが爆速であることと、なぜその速度がServerless時代には求められるかを書きました。 このレベルの速度で起動するならプロセス常駐させる必要はなく、結果としてJavaの鬼門の一つであるGCも実質解決ができます。 つまり、QuarkusはJavaの難点と言われていたスピンアップタイムとGCを改善したクラウドネイティブなJavaEE環境と言えるかと思います。 OracleのHelidonも同じ路線のはずなので、いい感じに刺激しあえると良いですね。

なお、JPAを使ったもうちょっと本格的なアプリに関しては下記で記事を書いてますのでこちらも参考ください。

追加: ブコメで「これCGIじゃね?」と言うのがありましたが私も同感です。特にFaaSやKNativeは今風の良い感じなCGI2.0的なものかと。 実のところCGIはスレッドではなくプロセスなので取り回しは悪くなかったと思うのですがパフォーマンスの問題がありました。 で、現在のインフラだとリソースを安全にシェアできるので横に必要に応じてたくさん並べれば多少効率落ちても良くね? と言うことで回帰してると思っています。

それではHappy Hacking!

参考

Unlimited Process Works - 無限にスケールするためのアーキテクチャープロトタイプ

「行くぞDocker! プロセスの貯蔵は十分か!?」

はじめに

さて、以前顧客向けシステムに最適なDBは何だろう? と思ってこんな記事を書きました。

koduki.hatenablog.com

で、その中でも顧客向けシステムはユーザ毎にDB作れば良いんじゃね? Dockerでいけるんじゃね? ということ書いたんですがシステムアーキテクチャを考える上でのプロトタイプとして作ってみました。

github.com

システム名は[Heaven’s Feel]II.lost butterfly見たいけど近所に無いから見れないので、出始めのコメントを思いついて勢いで付けた次第です。後悔はしていない。

基本コンセプト

READMEにも書いてますが、基本コンセプトはCGIのようにリクエスト毎にDockerを立ち上げてそのコンテナでsqlliteをユーザ毎に作成してバインドするとう作りになっています。 Dispatcherが同じユーザなら同じDBにアサインする形ですね。

その上で、参照専用のDBとしてGlobal DBをpostgresqlで持っています。 ここにそれぞれのユーザのDBの内容を非同期で連携していきます。その上でマテリアライズドビュー等を使用して集計したり扱い易い構造にします。

https://cacoo.com/diagrams/OFL4JoIEKUjLIooU-30F44.png

sqliteだと分散トランザクションがー、とか大容量データでの性能大丈夫? とか色々感じるかも知れないですが、 一般的に顧客データは顧客が増えるとレコードが増えますがユーザ毎では数MBとか数百MBで収まるケースも多いです。 また、顧客を超えて読み書きすることは(マスタデータ以外)ありません。

不正検知やレポートを作る作業をはじめとした横断的な集計をする作業はGlobal DBを利用することで高速に実行できます。 そのため、sqlite側でDB間の一貫性を保つ必要はないので分散トランザクションなどの処理は不要です。 これらの作業は大抵はタイムスロットがあるので一定の時間より後のデータで整合性が保証されてるならリアルタイムのそれは問題にならないため、遅延連携が可能となります。

ユースケース

このシステムは超簡単な銀行システムなので以下のことができます。

  • 自分のアカウントに入金
  • 自分のアカウントの中身を表示
  • 全員の残高ランキングを取得

まあ、セキュリティ的にヒドい銀行ですがそれはサンプルということでw

テーブル構造

基本的にはまんまシンプルなのですが、ポイントの一つはIDにシーケンスではなくUUIDを使っていることです。 UUIDを使うことでシーケンスの発行で競合することがありません。加えて、sqlite側と連携先のpostgresql側で同じIDが使えるので整合性チェックも容易になります。 連携が容易にしやすいようにsqlite側ではアップデートをせずにInsertのみで利用する使い方を想定しています。

sqlite

CREATE TABLE account (
        id UUID,
        amount BIGINT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
);

PostgreSQL

CREATE TABLE account (
    id UUID,
    name TEXT,
    amount BIGINT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

CREATE MATERIALIZED VIEW v_account_summary AS 
    SELECT name, SUM(amount) FROM account GROUP BY name;

性能特性

レイテンシー

まずDispatcherですがリクエスト毎にDocker起動させてるのでオーバーヘッドが凄いです。あらゆるリクエストが2秒くらいかかります。 いくら極限のレイテンシーは要求されないとは言え流石に遅すぎですね。これをちゃんとすると多分FaaSになるのかな? でちゃんとする方法は例えばFast Containerを導入するとかになるかも知れないです。

hb.matsumoto-r.jp

ただ、致命的な点としてsqliteのために永続化層が必要です。ステートフルなのでプロセスの再利用が難しいことを考えるとデータロードだけでもレイテンシーを稼ぐ必要があります。 そのためのアイデアとしては2つ

  • KVSなど高速なストレージに乗せる
  • パーティションなどで階層化してすぐ使うデータはメモリなど高速なストレージに置いて、残りは遅延で読み込む

sqliteのdbファイルをKVSに置くのが一番良い気がしますね。それで容量の問題が出るならパーティーション化とか階層化するしかないのかな、と。 なんとなくですがGoogle SpanerとかもKVSの上に構築されたRDBっぽいので、似たような発想なのではないかと。もちろん仕組みはもっと高度でしょうが。

スケーリング

Workerは現在は純粋なプロトタイプv1なのでコンテナをローカルに作ってるため1マシンを超えてスケールしないですが、実際はk8sやswarmを使って複数マシンを使うことでスケールさせる事ができると思います。 Dispatcherは特にステートを持たないのでこれはスケールアウトさせることができます。 ASync ETLは本来的にはKafkaなどを手前に置くことになると思いますがこの状態ならスケールするのではないかと。 Global DBはトラディショナルなRDBなのでスケールの問題がありますが、ユーザトランザクションからの書き込みはないのでリードレプリカで対応できます。

負荷試験を実施してないので断定は出来ないのですが、理論上はこの構造ならかなりのところまで金の弾丸でスケールするんじゃないかな、と。

メンテナンス

メンテナンスですがユーザDBのスキーマを変えるにはGlobal DBとAsync ETLにも変更が必要なのでまあ大変ですね。 とは言え、基本的にはユーザDBを徐々に変更していって、最終的にGlobal DBを変えるという手順が使えるので全体的なメンテナンスコストはオーバーヘッド分ありますが、ここのメンテナンスはそこまで大変でもないかと想定します。

アプリとDBがセットのコンテナをリリースして入れ替えていくだけなので、MySQLとかでsハーディングしてる時より小さくアップデートできて楽かも。

まとめ

とりえず無限にスケールするアーキテクチャというのをコンセプトにシステムを組んでみました。 素朴に作ってみた感じでは無限にスケールはなんとかなりそうだけど、せめてレイテンシーが1秒を切らないと使い物にならないだろうという感じなので、ちゃんとしたミドルウェアを使うのが当面は良さそうです。仮に問題が解決してもこの構造で運用するのは多くのケースでオーバーテクノロジーな気もしますし。

ただ、こうして自分でコンセプトモデルを作ることでスケールのためにアプリケーションやミドルウェアが備えるべき要件もクリアになりましたし、ミドルウェア側の振る舞いもある程度想像ができるようになったので目的は達したかな、と。

勉強にはなるので暇を見てもうちょっと改造はしてみたいなー。 それではHappy Hacking!