結果整合性を意識した柔軟な非同期処理の作り方
エンジニアLT 2019/4/12
岡本 浩治
みんな非同期処理大好き
Railsプロジェクトでは ActiveJob を使った非同期処理を実装しがち
みんなリトライ戦略までちゃんと考えていますか?
今日は比較的リトライしやすい非同期処理の実装方法を見ていきましょう
Ruby on Rails とはあまり関係のないアーキテクチャそのものの話です
前提の知識のおさらい
整合性について
MapReduceパターン
結果整合性
みなさん "結果整合性 / Eventual Consistency" ってご存知ですか?
調べ直したら結構古い(2009年くらい)概念なので若い人々は学校で習っているのかも。
結果整合性
ACID特性と同様にデータベースにおける一貫性モデルであるBASE特性における最も重要な考え方の一つである。例えば、インターネットのDNS、NTPプロトコル、GPSなどのように、即座にデータが反映されることを前提としていないが、問題なく成立している事例は数多く存在する。
ACID特性、CAP定理、BASEについておさらい
ACID特性
信頼性のあるトランザクションシステムの持つべき性質
CAP定理
分散システムは 一貫性、可用性、分断耐性の保証のうち、同時に2つの保証を満たすことはできるが、同時に全てを満たすことはできないというもの。
BASE
分散データベースシステムにおけるトランザクションを表現したもの
結果整合とは
状態 A を 状態 A' に変更する操作をした場合に、一貫性は担保されないものの、そのうち A' という状態に収束することを保証するアーキテクチャ
もちろんちゃんと壊れるし、壊れたらなんとかするしかない
でも結構堅牢なのが知られていて、次に示すような成功例がある
"整合性" を担保しているシステムの例
TCP
DNS
Google Cloud Datastore / Amazon DynamoDB
ブロックチェーン
"結果整合性" を担保している
TCP
DNS /icons/pass.icon
MySQL の 非同期レプリケーション /icons/pass.icon
Google Cloud Datastore / Amazon DynamoDB /icons/pass.icon
ブロックチェーン /icons/pass.icon
TCPの整合性
TCP では送信されたパケットを受信する際に一貫性が保たれない場合、送信元が受信を確認できていないパケットの再送処理を行う
https://tech.nikkeibp.co.jp/it/article/COLUMN/20070703/276588/zu03_02.jpg
DNSの整合性
DNS サーバーには必ずしも最新の値が反映されず、値はインターネット上の多数のディレクトリでキャッシュされ、複製されます。
https://cloud.google.com/datastore/docs/articles/images/balancing-strong-and-eventual-consistency-with-google-cloud-datastore/eventual-consistency.png
MySQL
非同期レプリケーションとは、マスターでトランザクション(更新処理)が完了したとしても、スレーブにはその時点で何も影響を与えていないということです。
MySQL
変更が完了した後、マスターはバイナリログにデータの変更内容、もしくは更新クエリーそのものを書き込みます(中略)。 マスター上のdumpスレッドはバイナリログの内容を読み取り、スレーブのIOスレッドに送信します。そして、スレーブのIOスレッドはその内容を「処理を貯めるキュー」(リレーログ)に書き込みます。
https://www.percona.com/blog/wp-content/uploads/2017/01/replicationnew.png
MySQLの非同期レプリケーションの整合性
GTID
グローバルトランザクション識別子 (GTID) は、発生元のサーバー (マスター) で作成され、そこでコミットされた各トランザクションに関連付けられる一意識別子です。
GTID = source_id:transaction_id
MapReduceパターン
黄色い気持ち悪像の事ではなくてプログラミングパターンの方
並列実行可能な処理に分解して振り分ける(Reduce ステップ)と分離した時間のかかる処理(Reduce ステップ)
直列と並列
code:seq.rb
Benchmark.bm(3) do |r|
r.report "seq" do
(1..10).map { |i| sleep 1 }
end
r.report "thread" do
(1..10).map { |i| Thread.new { sleep 1 } }.map { |thread| thread.join }
end
end
直列と並列
直列だと10秒かかるけど並列なら1秒で終わる
sequence 0.090000 0.060000 0.150000 ( 10.023350)
thread 0.000000 0.000000 0.000000 ( 1.004528)
本題
ここで5分だと良いペース
ギョームシステムにおける結果整合の例:
計算の順序を問う処理
TCP や MySQL と同じように、一貫性のある更新をサブシステムが受信し、一貫性を保った状態でサブシステムの情報を更新するもの(A)
計算の順序を問わない処理
順不同なデータの更新をサブシステムが受信し、順不同であっても問題が発生しないようにサブシステムの情報を更新するもの(B)
AとBそれぞれ実装方法を見ていきましょう
100円、300円、300円の入金を非同期でおこなう、たとえば銀行振込のようなシステムの場合
A.順序を問う処理とは?
例えばサブシステムにリソースを作り出したり、消したりする必要がある処理
特定のリソースに対して、1.作成→2.更新→3.削除という3つの処理がある場合、順序が変わると更新が破綻する
A.順序を問う処理の実装
TCPや、MySQL の非同期レプリケーションに学ぶと良い
処理の発生した際、サブシステムで処理を受け付けた際に一意な通番を発行して順序を保証する
MySQL の GTID
payload の例
code:payload1.json
{ seq: 100, from: { shop: 234, account: 1234567 }, to: { shop: 234, account: 7654321 } delta: 100 }
{ seq: 101, from: { shop: 234, account: 1234567 }, to: { shop: 234, account: 7654321 } delta: 300 }
{ seq: 102, from: { shop: 234, account: 1234567 }, to: { shop: 234, account: 7654321 } delta: 300 }
A.順序を問う処理の柔軟性をあげる
例えば100番でエラーが発生した際は100番をエラーとし101番の処理には移行しない
101番でシステムの更新処理が止まっている場合であっても102番以降の処理を受け付けるようにする
エラーで止まっても処理を受け付け続け、訂正したら続きから再開できるようにする
101番が処理済みの時に103番を受信しちゃった時(102が欠けてるとき)は103番をキューに返す
原子性の単位
https://gyazo.com/c9c6a34a936a12b328920009cafd3d6c
原子性の単位(MySQL)
https://www.percona.com/blog/wp-content/uploads/2017/01/replicationnew.png
ちょっとまって!キューイングシステムと整合性
お使いのキューイングシステムのその "キュー"、 本当に FIFO か確認しましょう
早いキューは FIFO を保証してない場合もあります(AWS SQS Standard とかね)
ただし、ときとして (ほぼ無制限のスループットが可能な高分散アーキテクチャであるために) メッセージの複数のコピーが順不同で配信されることがあります。
B.順序を問わない処理とは?
特定の銀行口座に対して、①100円入金→②300円入金→③300円入金という3件の入出金が発生する場合、どのように前後が順序しても結果が+700円になれば良い
B.順序を問わない処理の実装方法
入金処理に失敗した場合のリトライのことだけ考えて何しろ実装して良い
並列度をあげることを考えて良い
それぞれ1秒かかる処理で直列で3秒かかる処理でも1秒で終わらすことができる
payload の例
code:payload1.json
{ uuid: "2c203222-3f9c-4fb5-b669-d9f486886a52", from: { shop: 234, account: 1234567 }, to: { shop: 234, account: 7654321 } delta: 100 }
{ uuid: "45689e7c-eb30-4ec2-955e-9cde2ffb5565", from: { shop: 234, account: 1234567 }, to: { shop: 234, account: 7654321 } delta: 300 }
{ uuid: "93426f1f-59f6-493d-aee5-c8f93c9d1914", from: { shop: 234, account: 1234567 }, to: { shop: 234, account: 7654321 } delta: 300 }
B.順序を問わない処理の柔軟性をあげる
一貫性を捨てる、つまりそれぞれの処理を完全に分離した処理として独立させる
②の入金が失敗した場合は①と③の処理だけ正常終了させ、②の処理だけ別途リトライさせる
②がエラーになっている時も他の処理は受け付けられるようにする
処理がすんだID(ここではuuid)を保存しておき、リトライされた際には処理ごと破棄する
アーキテクチャ
https://gyazo.com/c9c6a34a936a12b328920009cafd3d6c
B ++
https://gyazo.com/c1e3d64478ac43821fb6ffeda8ba386c