Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Elixir入門「第6回:Elixirはtry…catchを書かない~障害対応のパラダイムシフト~」

Elixirの「スーパーバイザ」を使って、シンプルかつ強力な耐障害性を実現するコードをサクっと書いてみます

  • Login to see the comments

Elixir入門「第6回:Elixirはtry…catchを書かない~障害対応のパラダイムシフト~」

  1. 1. Elixir入門 第6回 Elixirは try…catchを書かない 例外処理から耐障害性へのパラダイムシフト~ 2017/08/17 ver0.5作成 2017/08/23 ver0.9作成
  2. 2. 1 1. 例外処理から耐障害性へのシフト 2. try…catchからどう変わるのか? 3. スーパーバイザのプロセス監視、復旧 4. Phoenixから学ぶ準正常系 5. 耐障害性のための構成と復旧戦略 6. パターンマッチと耐障害性へのシフト 7. 「耐障害性プログラミング」にようこそ 目次
  3. 3. 2 1.例外処理から耐障害性へのシフト
  4. 4. 3 1.例外処理から耐障害性へのシフト C++やJava、その他、多くのプログラミング言語では、「例外」を 扱うために、try…catchと例外ハンドラのセットで対応します その中には、「プログラムのバグ」のような、人間が想定し切れない ような対象も含める必要があります このように例外処理は、本来のメイン処理で無いハンドラを多数 想定・準備する労力がかかる上、想定外には対応し切れない、 脆弱性を持つアプローチです (ハンドラ自体のバグにも弱い) 一方、Elixirでは、「想定外の発生は仕方無いので、クリーンに 落とし、復旧する」ことに着目した、「耐障害性」を備えています Elixirにおける、耐障害性のデザインや様式を学ぶことで、障害 対応・例外処理に関するパラダイムシフトを体験できます
  5. 5. 4 2.try…catchからどう変わるのか?
  6. 6. 5 ログアラータ 従来のプログラム 2.try…catchからどう変わるのか? try…catchによる例外処理は、「本体処理」をtry~で囲み、 例外発生時は、各catchで捕まえ、例外ハンドラを起動します 一方、Elixirでは、本体処理には手を加えず、「スーパーバイザ」 と呼ばれる監視プログラムを別系で配備します try 本体処理 例外ハンドラ群 catch① 例外ハンドラ① catch② 例外ハンドラ② Elixirプログラム 本体処理 スーパーバイザ (プロセス監視) 復旧戦略 ダウン 監視 ダウンしたら 再起動 アラート 通知 ログ 監視
  7. 7. 6 2.try…catchからどう変わるのか? 監視プログラムからの復旧戦略の代表として「プロセス再起動」が ありますが、プロセス起動が軽量なElixirだから実現可能という、 プログラミング言語の特性が大きく寄与しています (関数型言語のイミュータブルな特性も、この実現に貢献します) ・本体処理と例外処理を1つ のコードにまとめられる ※これは分離できない、という デメリットでもある メリット デメリット Elixir 耐障害性 例外処理 ・メモリリークを作り込みやすい ・不整合を作り込みやすい ・プロセスを再起動する設計が 考慮から漏れる可能性がある ・プロセス再起動で全てが解決 できるとは限らない ※とはいえ、プロセス再起動の 設計は例外処理でも本当 は必要 ・障害対応を本体処理から 分離できる ・メモリリークや不整合を解消 できる構造 ・プロセス再起動を予め設計
  8. 8. 7 従来のプログラム 2.try…catchからどう変わるのか? ちなみに、try…catchによる例外処理をしているプログラムでも、 外部の監視系を導入していれば、Elixirのスーパーバイザと同様 の構築にあたりますが、「例外処理と監視系が障害監視の役割 を重複していた」というケースが実態でしょう (もしくは、プロセスの 再起動が考慮されていなかった、というケースもあり得ます) try 本体処理 例外ハンドラ群 catch 例外ハンドラ (外部の監視系) アラート ダウン 監視 ダウンしたら 再起動 復旧戦略 通知
  9. 9. 8 3.スーパーバイザのプロセス監視、復旧
  10. 10. 9 3.スーパーバイザのプロセス監視、復旧 プロセス監視、再起動の例として、「ファイルを読み込み、内容を 返す」という簡単なサーバプログラムを使って説明します defmodule Pass do # サーバ def cat_server() do receive do { sender_pid, path } -> { :ok, result } = File.read( path ) send( sender_pid, { true, result } ) end cat_server() end # サーバプロセス起動 def start_cat_server() do pid = spawn( Pass, :cat_server, [] ) :global.register_name( :cat, pid ) end # サーバを呼び出すクライアント def cd( path ) do send( :global.whereis_name( :cat ), { self(), path } ) listen() end end lib/pass.ex ※Elixir入門 第2回「PC間で通信するアプリをサクっと書いてみる」の例と同じコードです
  11. 11. 10 3.スーパーバイザのプロセス監視、復旧 まず、「GenServer」という汎用モジュールを使って書き直します (サーバとクライアントの書き分けや起動コードが不要になります) 呼び出し方は以下のように変わりますが、実行結果は同じです defmodule PassGenServer do use GenServer def start_link() do { :ok, pid } = GenServer.start_link( __MODULE__, "" ) IO.puts( "--- PassGenServer.start_link() PID=#{inspect pid} ---" ) { :ok, pid } end def handle_call( { :cat, path }, _from, _state ) do { :ok, result } = File.read( path ) { :reply, result, "" } end end lib/pass_genserver.ex iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" ) {:ok, #PID<0.211.0>} iex> GenServer.call( pid, { :cat, "a.txt" } ) "I'm a.txt"
  12. 12. 11 3.スーパーバイザのプロセス監視、復旧 このサーバにおける「想定外」として、以下のようなパターンがあり ますが、ここでは、存在しないファイルを指定した例を行います  異常系・・・存在しないファイルを指定、読込権限無、等  バグ・・・ファイル名が文字列で無い、長過ぎる、等 例外が発生し、サーバプロセスがダウンします iex> GenServer.call( pid, { :cat, "b.txt" } ) ** (EXIT from #PID<0.206.0>) an exception was raised: ** (MatchError) no match of right hand side value: {:error, :enoent} (node1) lib/pass_genserver.ex:10: PassGenServer.handle_call/3 (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 14:31:55.520 [error] GenServer #PID<0.211.0> terminating ** (MatchError) no match of right hand side value: {:error, :enoent} (node1) lib/pass_genserver.ex:10: PassGenServer.handle_call/3 (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 Last message: {:cat, "b.txt"}
  13. 13. 12 3.スーパーバイザのプロセス監視、復旧 サーバプロセスがダウンした後は、存在するファイルを指定しても、 エラーとなります 再度サーバ起動してやり直すと、今度は正常に返します この流れをプログラム化することで、復旧処理を自動化することが、 「スーパーバイザ」の役割になります iex> GenServer.call( pid, { :cat, "a.txt" } ) warning: variable "pid" does not exist and is being expanded to "pid()", please use parentheses to remove the ambiguity or change the variable name iex:1 ** (CompileError) iex:1: undefined function pid/0 (stdlib) lists.erl:1354: :lists.mapfoldl/3 iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" ) iex> GenServer.call( pid, { :cat, "a.txt" } ) "I'm a.txt"
  14. 14. 13 3.スーパーバイザのプロセス監視、復旧 スーパーバイザのコードは、以下の通りです たった、これだけのコードを追加するだけで、プロセスダウン監視と プロセスダウン後の再起動が実現されることは、驚異的です 例外処理で、同等の処理を書くことは、不可能に限りなく近く、 また自前の監視系を作るのも、骨が折れる作業です Elixirは、この機能が標準装備されており、非常にお手軽です import Supervisor.Spec defmodule PassSupervisor do def start_link() do servers = [ worker( PassGenServer, [ 0, [ name: :server_process ] ] ) ] Supervisor.start_link( servers, strategy: :one_for_one ) end end lib/pass_supervisor.ex
  15. 15. 14 3.スーパーバイザのプロセス監視、復旧 スーパーバイザを起動します GenServerプロセス (PID=<0.190.0>) が起動され、監視 するスーパーバイザプロセス (PID=<0.189.0>) が起動され ていることが確認できます これまで、PIDを指定してGenServerを呼んでいましたが、スー パーバイザ経由でのGenServer起動では、PIDを取得すること ができないため、「:server_process」というプロセス名を付与 しており、プロセス名で呼び出しが可能です iex> PassSupervisor.start_link() --- PassGenServer.start_link() PID=#PID<0.190.0> --- {:ok, #PID<0.189.0>} iex> GenServer.call( :server_process, { :cat, "a.txt" } ) "I'm a.txt"
  16. 16. 15 3.スーパーバイザのプロセス監視、復旧 では、プロセスダウン監視とプロセス再起動を試してみましょう ログ出力順が逆転していますが、例外が発生し、GenServerが ダウンしていますが、新たなGenServerが自動起動しています iex> GenServer.call( :server_process, { :cat, "b.txt" } ) --- PassGenServer.start_link() PID=#PID<0.197.0> --- ** (exit) exited in: GenServer.call(:server_process, {:cat, "b.txt"}, 5000) ** (EXIT) an exception was raised: ** (MatchError) no match of right hand side value: {:error, :enoent} (node1) lib/pass_genserver.ex:16: PassGenServer.handle_call/3 (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 (elixir) lib/gen_server.ex:774: GenServer.call/3 iex(4)> 15:22:39.965 [error] GenServer :server_process terminating ** (MatchError) no match of right hand side value: {:error, :enoent} (node1) lib/pass_genserver.ex:16: PassGenServer.handle_call/3 (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 Last message: {:cat, "b.txt"} State: ""
  17. 17. 16 3.スーパーバイザのプロセス監視、復旧 本当に再起動できているか確認します 問題無く、再起動できていることが確認できました このように、スーパーバイザを使うと、とても手軽に、プロセス監視 と再起動を組み込むことができます ここで気になるのが、プロセスを再起動しても済まないケース… つまり「準正常系」をどう扱うか、だと思います この例として、Phoenixのような、エラー時でもレスポンスする 必要があるケースについて、次章で見ていきます iex> GenServer.call( :server_process, { :cat, "a.txt" } ) "I'm a.txt"
  18. 18. 17 4.Phoenixから学ぶ準正常系
  19. 19. 18 4.Phoenixから学ぶ準正常系 ここまでの障害対応は、「異常系」「バグ」の2パターンのみでした 一方で、PhoenixのようなWebアプリでは、404エラーのような、 レスポンスを返すエラーがあり、これは「準正常系」と呼ばれます
  20. 20. 19 4.Phoenixから学ぶ準正常系 準正常系は、異常系やバグと異なり、発生が想定可能なため、 try…catchでのエラーハンドリングを作り込むこととなります Phoenixで、404エラーのような準正常系のエラーハンドリングが、 どのように実現されているか見てみましょう tryの中の本体処理で、例外が発生した場合、catch中にある、 エラーレンダラーが呼び出され、エラーページが表示されます defmodule Phoenix.Endpoint do … def call(conn, opts) do … try do super(conn, opts) catch kind, reason -> Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, @phoenix_render_errors) end end … deps/phoenix/lib/phoenix/endpoint.ex
  21. 21. 20 4.Phoenixから学ぶ準正常系 ここまでをまとめると、障害対応において大事なことは、「異常系」 「バグ」「準正常系」の3種類を、「明確に区別する」ということです 「異常系」「バグ」の場合は、想定ができないため、try…catch でのエラーハンドリングは書かず、プロセスをダウンさせ、再起動で クリーンナップする方向で対応します 一方、「準正常系」は、想定可能なため、try…catchでエラー ハンドリングを作り込み、暗黙のプロセスダウン/再起動には 任せません 万全と、「try…catchは普通に書くものだ」と自動思考すること で、この区別が不明確となるような事態を、Elixirは避けられる 構造を持っており、自然と耐障害性が実現されます
  22. 22. 21 5.耐障害性のための構成と復旧戦略
  23. 23. 22 5.耐障害性のための構成と復旧戦略 ここまでは、単品かつデータも持たない単純なプロセスでの復旧を 見てきましたが、複数プロセスで、共通のデータを保持する場合 には、以下のような構成が必要となってきます 共通データ 保持プロセス スーパーバイザ 監視 本体処理① プロセス 監視 本体処理② プロセス 監視
  24. 24. 23 5.耐障害性のための構成と復旧戦略 更に、本体処理自体が、複数のプロセスで構成される場合は、 サブのスーパーバイザを作り、そのプロセスグループの中で再起動 を制御できます 共通データ 保持プロセス スーパーバイザ 監視 本体処理① プロセス 監視 本体処理②-1 プロセス 監視 本体処理②用 スーパーバイザ 本体処理②-2 プロセス 監視 監視
  25. 25. 24 5.耐障害性のための構成と復旧戦略 スーパーバイザから、スーパーバイザを起動するコードは、以下の ようになります 通常のサーバを起動するコードと、ほぼ変わりません import Supervisor.Spec defmodule PassSubSupervisor do def start_link() do servers = [ worker( PassGenServer, [ 0, [ name: :server_process ] ] ) ] Supervisor.start_link( servers, strategy: :one_for_one ) end end lib/pass_sub_supervisor.ex import Supervisor.Spec defmodule PassSupervisor do def start_link() do servers = [ supervisor( PassSubSupervisor, [ 0, [ name: :ssv_process ] ] ) ] Supervisor.start_link( servers, strategy: :one_for_one ) end end lib/pass_supervisor.ex
  26. 26. 25 5.耐障害性のための構成と復旧戦略 「復旧戦略」が、何種類か選べます (代表2つを紹介)  one_for_one ・・・ 1プロセス落ちたら1プロセス再起動  one_for_all ・・・ 1プロセス落ちたら配下を全再起動 共通データ 保持プロセス スーパーバイザ 監視 本体処理① プロセス 監視 本体処理②-1 プロセス 監視 本体処理②用 スーパーバイザ 本体処理②-2 プロセス 監視 監視
  27. 27. 26 6.パターンマッチと耐障害性へのシフト
  28. 28. 27 6.パターンマッチと耐障害性へのシフト 「想定外」のうち、バグの例として、「ファイル名が文字列で無い」 「長過ぎる」といった、いわゆる「バリデーションチェック」に相当する ものを紹介しました 他の言語であれば、関数に入った後に、バリデーションチェックを 行いますが、Elixirでは、「パターンマッチ」による、関数呼出前の チェックが行えます この特徴を活用すると、想定外に対するバリデーションチェックの 開発をスキップし、本体処理のコーディングにより専念することが 可能となります 「try…catchの煩雑さ」と「バリデーションチェックの煩雑さ」が、 コードから無くなり、シンプルな本質のみのコードという世界観です
  29. 29. 28 7.「耐障害性プログラミング」にようこそ
  30. 30. 29 7.「耐障害性プログラミング」にようこそ 今回は、try…catchに変わる障害対応として、スーパーバイザ を使ったプロセス監視と再起動についてご説明しました Elixirの耐障害性プログラミングが「そこまで難しく無いかも?」と 思っていただけたら、この入門としては大成功です 本体処理に例外処理を混ぜず、障害対応を分離し、想定外が 発生した際はプロセス再起動する世界は、これまで親しんだプロ グラミングの概念と、大きく異なるコンセプトでは無かったでしょうか このシンプルで強力な役割の分離により、プログラムの設計という ものが根底から改善されます この新たなパラダイムを活かし、仕事でも趣味でも、プログラミング ライフをエンジョイしてください!
  31. 31. 30 ご清聴ありがとうございます

×