Elixir:ロギングプロセス-実践ガイド


Elixir (およびもちろんもちろんErlang )のプロセスは一意のプロセス識別子 -pidを使用して識別されます。
これらを使用してプロセスとやり取りします。 メッセージはpidよう送信され、仮想マシン自体がこれらのメッセージを正しいプロセスに配信します。
ただし、場合によっては、 pidに対する過度の信頼が重大な問題につながる可能性があります。
たとえば、死んだプロセスのpid保存したり、 pid作成を抽象化するSupervisorを使用したりできます。したがって、どのpidを持っているかさえわかりません( per :また、 Supervisorでは、別のpidプロセスを再起動できます。これについては一切知りません)。
簡単なアプリケーションを作成して、直面する可能性のある問題と、これらの問題をどのように解決するかを見てみましょう。


レジストリなしで始める


最初の例では、簡単なチャットを作成します。 mixプロジェクトを作成することから始めましょう。


 $ mix new chat 

この記事のすべての例で使用する、絶対に標準的なGenServer作成しましょう。


 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer # API def start_link do GenServer.start_link(__MODULE__, []) end def add_message(pid, message) do GenServer.cast(pid, {:add_message, message}) end def get_messages(pid) do GenServer.call(pid, :get_messages) end # SERVER def init(messages) do {:ok, messages} end def handle_cast({:add_message, new_message}, messages) do {:noreply, [new_message | messages]} end def handle_call(:get_messages, _from, messages) do {:reply, messages, messages} end end 

そのようなコードがなじみのない、または理解できないと思われる場合は、OTPに関する優れたパラグラフが記載されているElixirの作業開始をお読みください。

mix環境でiexセッションをiexし、サーバーでの作業を試みましょう。


 $ iex -S mix iex> {:ok, pid} = Chat.Server.start_link {:ok, #PID<0.107.0>} iex> Chat.Server.add_message(pid, "foo") :ok iex> Chat.Server.add_message(pid, "bar") :ok iex> Chat.Server.get_messages(pid) ["bar", "foo"] 

このステップのコードはこのコミットにあります。

この段階では、すべてが素晴らしく、素晴らしいだけです。 pidを取得し、送信する各メッセージ( add_message/2およびget_messages/1 )に対してこのpidを渡します。すべてが予想get_messages/1機能し、退屈さえします。
しかし、 Supervisorを追加しようとすると楽しみが始まります...


とてもいい:私はSupervisorです!


そのため、何らかの理由でChat.ServerプロセスChat.Server死に絶えています。 私たちは空のコールドiexセッションにiexており、新しいプロセスを開始し、そのpidを取得し、この新しいpidメッセージを書き込む以外に選択肢はありません。 Supervisor作成しましょう-このような些細なことを心配する必要はありません!


 # ./lib/chat/supervisor.ex defmodule Chat.Supervisor do use Supervisor def start_link do Supervisor.start_link(__MODULE__, []) end def init(_) do children = [ worker(Chat.Server, []) ] supervise(children, strategy: :one_for_one) end end 

さて、 Supervisor作成Supervisor非常に簡単です。 しかし、サーバーの動作モデルが変わらない場合、問題が発生します。 結局のところ、 Chat.Serverプロセスを自分で開始するのではなく、 Supervisorがこれを行ってくれます。 したがって、 pidプロセスにはアクセスできません。


これはバグではなく、 SupervisorなどのOTPパターンの機能です。 子プロセスのpidアクセスすることはできません。予期せず(ただし、もちろん必要な場合のみ)プロセスを再起動できますが、実際にはプロセスをpidし、新しいpid新しいpidを作成します。


プロセス名を登録する


Chat.ServerプロセスChat.Serverアクセスするには、プロセスを指す方法を考え出す必要があります。もう一方はpidはありません。 Supervisorを介してプロセスが再起動された場合でも(つまり、 pidが変更された場合でも)ポインターが保存されるように、ポインターが必要です。
そして、そのようなポインターはと呼ばれます!


まず、 Chat.Server変更しChat.Server


 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer def start_link do # We now start the GenServer with a `name` option. GenServer.start_link(__MODULE__, [], name: :chat_room) end # And our function doesn't need to receive the pid anymore, # as we can reference the process with its unique name. def add_message(message) do GenServer.cast(:chat_room, {:add_message, message}) end def get_messages do GenServer.call(:chat_room, :get_messages) end # ... end 

変更-このコミットで

これですべてが同じように機能するはずですが、より良いだけです-このpidどこにでも渡さないでください:


 $ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.94.0>} iex> Chat.Server.add_message("foo") :ok iex> Chat.Server.add_message("bar") :ok iex> Chat.Server.get_messages ["bar", "foo"] 

プロセスが再起動しても、同じ方法でアクセスできます。


 iex> Process.whereis(:chat_room) #PID<0.111.0> iex> Process.whereis(:chat_room) |> Process.exit(:kill) true iex> Process.whereis(:chat_room) #PID<0.114.0> iex> Chat.Server.add_message "foo" :ok iex> Chat.Server.get_messages ["foo"] 

さて、私たちの現在のタスクについては、問題は解決されているように見えますが、もっと複雑な(そして実際のタスクにより近い)ことを試してみましょう。


動的プロセス作成


複数のチャットルームをサポートする必要があるとします。 クライアントは名前を持つ新しいルームを作成でき、希望するルームにメッセージを送信できることを期待しています。 その場合、インターフェースは次のようになります。


 iex> Chat.Supervisor.start_room("first room") iex> Chat.Supervisor.start_room("second room") iex> Chat.Server.add_message("first room", "foo") iex> Chat.Server.add_message("second room", "bar") iex> Chat.Server.get_messages("first room") ["foo"] iex> Chat.Server.get_messages("second room") ["bar"] 

上から始めて、これをすべてサポートするようにSupervisorを変更しましょう。


 # ./lib/chat/supervisor.ex defmodule Chat.Supervisor do use Supervisor def start_link do # We are now registering our supervisor process with a name # so we can reference it in the `start_room/1` function Supervisor.start_link(__MODULE__, [], name: :chat_supervisor) end def start_room(name) do # And we use `start_child/2` to start a new Chat.Server process Supervisor.start_child(:chat_supervisor, [name]) end def init(_) do children = [ worker(Chat.Server, []) ] # We also changed the `strategty` to `simple_one_for_one`. # With this strategy, we define just a "template" for a child, # no process is started during the Supervisor initialization, # just when we call `start_child/2` supervise(children, strategy: :simple_one_for_one) end end 

そして、 start_link関数で名前を受け入れるようにしましょう:


 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer # Just accept a `name` parameter here for now def start_link(name) do GenServer.start_link(__MODULE__, [], name: :chat_room) end #... end 

変更-このコミットで

そして、ここに問題があります! 複数のChat.Serverプロセスを使用できますが、それらすべてに:chat_roomという名前を:chat_roomことはできません。 トラブル...


 $ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.107.0>} iex> Chat.Supervisor.start_room "foo" {:ok, #PID<0.109.0>} iex> Chat.Supervisor.start_room "bar" {:error, {:already_started, #PID<0.109.0>}} 

正直なところ、 VM非常に雄弁です。 2番目のプロセスを作成しようとしていますが、同じ名前のプロセスが既に存在しているため、環境から非常に悪意があります。 他の方法を考え出す必要がありますが、どれですか?..


残念ながら、 name引数のタイプは明確に定義されています。 {:chat_room, "room name"}ようなものは使用できません。 ドキュメントを見てみましょう:


サポートされる値:
GenServerこの場合、 GenServer Process.register/2を使用して、指定されたatom名でローカルに登録されます。
{:global, term} -この場合、 GenServer:globalモジュールの関数を使用して、指定されたterm名でグローバルに登録されます。
{:via, module, term} -この場合、 GenServer moduleで定義されたメカニズムと名前「term」を使用して登録されmodule

英語のオリジナル
サポートされている値は次のとおりです。
atom GenServerは、 Process.register/2を使用して、指定された名前でローカルに登録されます。
{:global, term} GenServerは、 :globalモジュールの関数を使用して、指定された用語でグローバルに登録されます。
{:via, module, term} GenServerは指定されたメカニズムと名前で登録されます。

最初のオプションはatom 、すでに使用しています。トリッキーなケースでは収まらないことは確かです。
2番目のオプションは、プロセスをノードクラスターにグローバルに登録するために使用されます。 ローカルETSテーブルを使用します。 さらに、クラスター内のノード内で一定の同期が必要になるため、プログラムの速度が低下します。 したがって、 本当に必要な場合にのみ使用してください。
3番目の最後のオプションは、 :viaをパラメーターとして使用するタプルを使用します。これは、まさに問題を解決するために必要なものです。 これについてはドキュメントに次のように書かれています:


オプション:viaは、 register_name/2unregister_name/1whereis_name/1およびsend/2インターフェースを持つモジュールをパラメーターとして受け入れます。

英語のオリジナル
:viaオプションでは、register_name / 2、unregister_name / 1、whereis_name / 1、send / 2をエクスポートするモジュールが必要です。

何もはっきりしていませんか? 私も! それでは、このメソッドの動作を見てみましょう。


タプルの使用:via


したがって、タプル:viaは、 Elixirプロセスを登録するために別のモジュールを使用することを伝える方法です。 このモジュールは次のことを行う必要があります。



これが機能するためには、上記の関数はOTP定義された特定の形式で応答を送信する必要がありhandle_call/3およびhandle_cast/2特定のルールに従うように。


これをすべて知っているモジュールを定義してみましょう:


 # ./lib/chat/registry.ex defmodule Chat.Registry do use GenServer # API def start_link do # We register our registry (yeah, I know) with a simple name, # just so we can reference it in the other functions. GenServer.start_link(__MODULE__, nil, name: :registry) end def whereis_name(room_name) do GenServer.call(:registry, {:whereis_name, room_name}) end def register_name(room_name, pid) do GenServer.call(:registry, {:register_name, room_name, pid}) end def unregister_name(room_name) do GenServer.cast(:registry, {:unregister_name, room_name}) end def send(room_name, message) do # If we try to send a message to a process # that is not registered, we return a tuple in the format # {:badarg, {process_name, error_message}}. # Otherwise, we just forward the message to the pid of this # room. case whereis_name(room_name) do :undefined -> {:badarg, {room_name, message}} pid -> Kernel.send(pid, message) pid end end # SERVER def init(_) do # We will use a simple Map to store our processes in # the format %{"room name" => pid} {:ok, Map.new} end def handle_call({:whereis_name, room_name}, _from, state) do {:reply, Map.get(state, room_name, :undefined), state} end def handle_call({:register_name, room_name, pid}, _from, state) do # Registering a name is just a matter of putting it in our Map. # Our response tuple include a `:no` or `:yes` indicating if # the process was included or if it was already present. case Map.get(state, room_name) do nil -> {:reply, :yes, Map.put(state, room_name, pid)} _ -> {:reply, :no, state} end end def handle_cast({:unregister_name, room_name}, state) do # And unregistering is as simple as deleting an entry # from our Map {:noreply, Map.delete(state, room_name)} end end 

繰り返しますが、レジストリが内部でどのように機能するかを選択してください。 ここでは、単純なMapを使用して、名前とpidを関連付けます。 このコードは、 GenServerどのようにGenServerするかをよく知っている場合は特に、絶対にシンプルで簡単です。 関数によって返される値のみが不慣れに見える場合があります。


iexセッションでレジストリを試す時がiexました:


 $ iex -S mix iex> {:ok, pid} = Chat.Server.start_link("room1") {:ok, #PID<0.107.0>} iex> Chat.Registry.start_link {:ok, #PID<0.109.0>} iex> Chat.Registry.whereis_name("room1") :undefined iex> Chat.Registry.register_name("room1", pid) :yes iex> Chat.Registry.register_name("room1", pid) :no iex> Chat.Registry.whereis_name("room1") #PID<0.107.0> iex> Chat.Registry.unregister_name("room1") :ok iex> Chat.Registry.whereis_name("room1") :undefined 

5秒-素晴らしいフライト! レジストリは正常に機能します。レジストリを登録および削除します。 チャットで使用してみましょう。


問題は、複数のChat.ServerサーバーがChat.ServerSupervisorで初期化されたことでした。 特定のルームにメッセージを送信するには、 Chat.Server.add_message(“room1”, “my message”)を呼び出したいので、サーバー名を{:chat_room, “room1”}および{:chat_room, “room2”}として登録する必要があります{:chat_room, “room2”} タプルを介して行う方法は次のとおりです。


 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer # API def start_link(name) do # Instead of passing an atom to the `name` option, we send # a tuple. Here we extract this tuple to a private method # called `via_tuple` that can be reused in every function GenServer.start_link(__MODULE__, [], name: via_tuple(name)) end def add_message(room_name, message) do # And the `GenServer` callbacks will accept this tuple the # same way it accepts a pid or an atom. GenServer.cast(via_tuple(room_name), {:add_message, message}) end def get_messages(room_name) do GenServer.call(via_tuple(room_name), :get_messages) end defp via_tuple(room_name) do # And the tuple always follow the same format: # {:via, module_name, term} {:via, Chat.Registry, {:chat_room, room_name}} end # SERVER (no changes required here) # ... end 

変更-このコミットで

ここで何が起こるかです: Chat.Serverにメッセージを送信してルームの名前を渡すChat.Serverに、タプルで渡されたモジュールを使用してpidこのpid見つけます:via (この場合はChat.Registry ) 。
これで問題が解決しますChat.Serverプロセスをいくつでも使用できるようになり(名前の空想が終わるまで)、そのpidを知る必要がなくなりました。 絶対に。


ただし、このソリューションには別の問題があります。 推測?
まさに! レジストリは、クラッシュしたプロセスを認識していないため、 Supervisor使用して再起動する必要があります。 これは、これが発生すると、レジストリは同じ名前のレコードを再作成することを許可せずpidデッドプロセスのpid保存することを意味します。


理論的には、この問題の解決策はそれほど複雑ではありません。 レジストリに、 pidが保存されているすべてのプロセスを監視するよう強制します。 そのような「観察可能な」プロセスが落ちたらすぐに、レジストリから削除します。


 # in lib/chat/registry.ex defmodule Chat.Registry do # ... def handle_call({:register_name, room_name, pid}, _from, state) do case Map.get(state, room_name) do nil -> # When a new process is registered, we start monitoring it. Process.monitor(pid) {:reply, :yes, Map.put(state, room_name, pid)} _ -> {:reply, :no, state} end end def handle_info({:DOWN, _, :process, pid, _}, state) do # When a monitored process dies, we will receive a # `:DOWN` message that we can use to remove the # dead pid from our registry. {:noreply, remove_pid(state, pid)} end def remove_pid(state, pid_to_remove) do # And here we just filter out the dead pid remove = fn {_key, pid} -> pid != pid_to_remove end Enum.filter(state, remove) |> Enum.into(%{}) end end 

変更-このコミットで

すべてが機能することを確認します。


 $ iex -S mix iex> Chat.Registry.start_link {:ok, #PID<0.107.0>} iex> Chat.Supervisor.start_link {:ok, #PID<0.109.0>} iex> Chat.Supervisor.start_room("room1") {:ok, #PID<0.111.0>} iex> Chat.Server.add_message("room1", "message") :ok iex> Chat.Server.get_messages("room1") ["message"] iex> Chat.Registry.whereis_name({:chat_room, "room1"}) |> Process.exit(:kill) true iex> Chat.Server.add_message("room1", "message") :ok iex> Chat.Server.get_messages("room1") ["message"] 

さて、 SupervisorChat.Serverプロセスを再起動する回数はまったくChat.Serverルームにメッセージを送信するとすぐに、正しいpid配信されます。


gproc簡素化する


原則として、チャットは終了しますが、タプルを使用して登録を簡素化するもう1つの機能について説明:viaます。 これはgprocライブラリであるgprocです。
そして、 gproc代わりにChat.Server使用するようgprocに教えてChat.ServerChat.Server完全にChat.Registryます。


依存関係から始めましょう。 これを行うには、 gprocmix.exsに追加しmix.exs


 # ./mix.exs defmodule Chat.Mixfile do # ... def application do [applications: [:logger, :gproc]] end defp deps do [{:gproc, "0.3.1"}] end end 

次に、依存関係を次のようにプルアップします。


 $ mix deps.get 

タプルを使用して登録を変更できます:via - gprocではなくChat.Registry使用します:


 # ./lib/chat/server.ex defmodule Chat.Server do # ... # The only thing we need to change is the `via_tuple/1` function, # to make it use `gproc` instead of `Chat.Registry` defp via_tuple(room_name) do {:via, :gproc, {:n, :l, {:chat_room, room_name}}} end # ... end 

gproc{type, scope, key}の3つの値で構成されるgproc使用します。


この場合、以下を使用します。



可能なgproc設定の詳細については、 こちらをご覧ください


このような変更の後、 iexし、 iexセッションですべてが引き続き機能することを確認します。


 $ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.190.0>} iex> Chat.Supervisor.start_room("room1") {:ok, #PID<0.192.0>} iex> Chat.Supervisor.start_room("room2") {:ok, #PID<0.194.0>} iex> Chat.Server.add_message("room1", "first message") :ok iex> Chat.Server.add_message("room2", "second message") :ok iex> Chat.Server.get_messages("room1") ["first message"] iex> Chat.Server.get_messages("room2") ["second message"] iex> :gproc.where({:n, :l, {:chat_room, "room1"}}) |> Process.exit(:kill) true iex> Chat.Server.add_message("room1", "first message") :ok iex> Chat.Server.get_messages("room1") ["first message"] 

変更-このコミットで

次はどこ?


あなたと私はたくさんの複雑な質問を見つけました。 主な調査結果:



もちろん、これだけではありません。 クラスタ内のすべてのノードでグローバル登録が必要な場合は、他のツールも有効です。 Erlangは、グローバル登録用のグローバルモジュール、プロセスグループ用のgprcがあり、同じgprcが役立ちます。


この記事に興味がある場合は、 Saša Jurić. Elixir in Actionをお読みくださいSaša Jurić. Elixir in Action Saša Jurić. Elixir in Action


そして、ここにチーズカブがあります)



Source: https://habr.com/ru/post/J306500/


All Articles