Elixir

Elixirで遠隔PCに侵入#5風「SV経由で10万プロセス起動した際の負荷」

前回 からの続きで、スーパーバイザ(以下SVと省略)経由の大量GenServer起動における負荷を確認します :headphones:

なお、検証に使ったマシンのスペックは、CPUがIntel Core i5-2540M 2.60GHz、メモリ8GByteと、3Dゲームがギリギリ遊べるかどうかのロースペックな旧型PCを使っています

SVからの複数GenServer起動

SVから、任意のプロセス数のGenServerを起動できるよう、以下のような変更を入れます(起動にかかる時間も測定できるようにしておきます)

lib/pass_supervisor.ex
import Supervisor.Spec

defmodule PassMultipleSupervisor do
    def start_link( processes ) do
        start = Timex.now()
        servers = 1..processes |> Enum.map( &( worker( PassGenServer, [], id: &1 ) ) )
        Supervisor.start_link( servers, strategy: :one_for_one )
        IO.puts( "Spent Milliseconds=#{Timex.diff( Timex.now(), start, :milliseconds )}" )
    end

    def cat( parameter, name \\ "0" ) do
        GenServer.call( :global.whereis_name( name ), { :cat, parameter } )
    end

    def pwd( name \\ "0" ) do
        GenServer.call( :global.whereis_name( name ), :pwd )
    end
end

GenServer側もPIDが確認できるよう変更します

lib/pass_genserver.ex
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( :pwd, _from, _state ) do
        { :ok, result } = File.cwd()
        { :reply, result, "" }
    end

    def handle_call( { :cat, path }, _from, _state ) do
        { :ok, result } = File.read( path )
        { :reply, result, "" }
    end
end

次に、プロセス起動の確認ですが、その前に、Observerで総プロセス数を確認しておきます

iex> :observer.start

プロセス起動前は、107プロセス起動済みであることが確認できます(iexを起動してしばらくは、109プロセスになっていますが、15秒程度すると107に落ち着きます)

image.png

今回、頻繁にプロセス数の変化を見るため、[View]-[Refresh Interval]にて、10秒となっている設定を、1秒に変更しておきます

最初は、プロセス数5個程度で、お試しに正常起動できることの確認をします

iex> PassSupervisor.start_link( 5 )
--- PassGenServer.start_link() PID=#PID<0.212.0> ---
--- PassGenServer.start_link() PID=#PID<0.213.0> ---
--- PassGenServer.start_link() PID=#PID<0.214.0> ---
--- PassGenServer.start_link() PID=#PID<0.215.0> ---
--- PassGenServer.start_link() PID=#PID<0.216.0> ---
Spent Milliseconds=89
{:ok, #PID<0.211.0>}

5個のGenServerのPIDと、1個のスーパーバイザのPIDが発行されました

Observerのプロセス数も、6個増えていることが確認できます

image.png

さて、ここから多数のプロセスを起動してみますが、その前に、GenServer起動時のPID表示で重くなってしまうのを避けるために、コメントアウトしておきます

lib/pass_genserver.ex
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
 …

まず100+1個追加を試してみます

iex> recompile()
iex> PassSupervisor.start_link( 100 )
Spent Milliseconds=13
{:ok, #PID<0.219.0>}

13ミリ秒と、一瞬ですね :dancer_tone1:

image.png

再度、100+1個、追加してみても、起動速度が劣化しないことを確認します

iex> PassSupervisor.start_link( 100 )
Spent Milliseconds=12
{:ok, #PID<0.335.0>}

全く変わりませんでした :kissing_smiling_eyes:

image.png

100個程度のサーバプロセスであれば、一瞬で起動できる、ということが分かりました :pick:

SV経由での大量のGenServer起動時の負荷

では、一気に10,000個を追加してみるとしましょう :laughing:

iex> PassSupervisor.start_link( 10000 )
Spent Milliseconds=1744
{:ok, #PID<0.439.0>}

ナント、1.7秒程度です :scream:

Observerで確認しても、10,000個ちゃんと追加されていることが確認できます

image.png

Observerの「Processes」タブからPIDで見ても、101+101+10001個のGenServerが存在することが確認できます

image.png

更に数回、10,000個ずつを上乗せしても、劣化しないか確認してみます :thinking:

iex> PassSupervisor.start_link( 10000 )
Spent Milliseconds=1772
{:ok, #PID<0.30406.0>}
iex> PassSupervisor.start_link( 10000 )
Spent Milliseconds=1629
{:ok, #PID<0.8555.1>}

 …6回ほど繰り返す…

iex> PassSupervisor.start_link( 10000 )
Spent Milliseconds=1831
{:ok, #PID<0.17225.3>}

ほぼ劣化していないことが確認できました、凄まじいです :tada:

そして気付けば、10万個を超えるGenServerが起動済み、ということになります

image.png

この状態でも、マシンの通常利用も特に遅くなることも無く、iex自体もレスポンスが鈍くなるといったこともありません

また、最初の方や、途中で起動したGenServerを呼び出すと遅くなる、といった現象も出ないし、GenServer自体の反応もクイックなままです

iex> GenServer.call( pid( 0, 212, 0 ), :pwd )
"/code/pass"
iex> GenServer.call( pid( 0, 10435, 0 ), :pwd )
"/code/pass"

今回の検証に使ったマシンは、ロースペックな旧型PCにも関わらず、大量のプロセス起動の負荷をほとんど感じなかったため、もっとハイスペックなマシンであれば、より迅速かつ大量にプロセスをハンドリングできる可能性は高いです :blush:

参考:大量プロセス起動時のPIDの推移

ちなみに、発行されたPID(途中、SVのPIDも含んでいる)は、以下のレンジとなっていました(途中、飛び番がある)

2桁目の数字が32,767を超えると、3桁目がインクリメントされるルールのようです

  • <0.212.0>~<0.32767.0>
  •  <0.0.1>~<0.32767.1>
  •  <0.0.2>~<0.32767.2>
  •  <0.0.3>~<0.27225.3>

予習:プロセスダウン時のSVによる再起動後のPID

さて、プロセスダウン時にSVで再起動がかかったプロセスのPIDがどうなるかを確認しておきましょう

iex> Process.exit( pid( 0, 212, 0 ), :kill )
true

Observerの「Processes」で確認すると、<0, 212, 0>が消え、新たに<0, 10419, 13>というプロセスが起動されていることを確認しました

このため、一斉にプロセスをダウンさせると、元のPIDから、別のレンジでPIDが発行され直すことが想定されます

これを踏まえた上で、次は、SVでの大量プロセスの再起動における負荷を確認します