OTP 관리자에서 종속성 주입 및 검색


우리들의 임무
우리는 서로를 이해하기 위해 상사의 형제자매가 필요하다.가장 좋은 것은 우리의 주관 트리가'폐쇄적'이기를 희망한다. 서로 다른 옵션으로 여러 개의 트리 실례를 시작할 수 있다.
예: 네트워크 데이터를 처리하는 데 사용되는 파이프, 이 파이프는 서로 통신하는 몇 개의 프로세스로 구성되어 있으며, 이러한 프로세스는 데이터를 소모하고 변환한다.우리는 특수한 방식으로 서로 다른 데이터 원본을 위해 서로 다른 파이프를 가동하기를 희망한다. 이렇게 하면 서로 간섭하지 않고 밀봉해서는 안 된다.
이 점을 해낼 수 있는 몇 가지 방법이 있다.

우리의 선택

하드 코딩 프로세스 이름 등록
가장 간단하면서도 가장 흔히 볼 수 있는 방법은 전역 이름을 사용하여 프로세스를 간단하게 등록하는 것이다.
defmodule PipelineSupervisor do
  use Supervisor

  @impl Supervisor
  def init([]) do
    children = [
      {SomeConsumer, name: ConsumerGlobalName}
      {SomeWorker, %{consumer: ConsumerGlobalName}},
    ]

    Supervisor.init(children, strategy: :rest_for_one)
  end
end

defmodule SomeWorker do
  use GenServer

  @impl GenServer
  def init(%{consumer: consumer} = options) do
    {:ok, options}
  end

  @impl GenServer
  def handle_call({:process_data_chunk, data_chunk}, _from, state) do
    {:reply, :ok, do_process_data_chunk(data_chunk, state)}
  end

  defp do_process_data_chunk(data_chunk, state) do
    case parse_data_somehow(data_chunk, state) do
      {:ok, parsed_message, new_state} ->
        Process.send(state.consumer, {:message_received, parsed_message})
        new_state

      {:more_data_necessary, new_state} ->
        new_state
    end
  end
end

defmodule SomeConsumer do
  use GenServer

  def start_link(%{name: name}) do
    GenServer.start_link(__MODULE__, [], name: name)
  end

  @impl GenServer
  def init([]) do
    {:ok, :undefined_state}
  end

  @impl GenServer
  def handle_info({:message_received, parsed_message}, state) do
    # ...
    {:noreply, state}
  end
end
erlang에서 제공하는 은밀한 이름 등록과 달리 기본적으로 DI 용기를 충당하는 프로세스 등록표를 사용할 수 있습니다. 임의의 id를 통해 의존항 (프로세스pid) 을 등록하고 찾을 수 있는 방법입니다.
이러한 작업은 다음과 같습니다.
  • 은 Elixir와 같이 유명하고 신뢰할 수 있는 레지스트리를 사용합니다.등록표
  • 자가 쓰기 레지스트리를 사용하여 상태(프로세스pid 매핑)를 GenServer 상태 또는 ETS
  • 으로 저장
    프로세스가 시작되면 자동으로 등록됩니다.다른 프로세스가 pid를 필요로 할 때, DI 등록표에 필요한 의존 관계를 요청합니다.
    그러나 문제는 여전히 존재한다.이것은 닭과 알의 딜레마입니다. 등록표를 사용하려면 순서대로 pid나 이름을 알아야 합니다.
    이 해결 방안은 간단하고 흔하지만 사용자의 하드코딩 프로세스 이름 때문에 감시 트리를 여러 번 실행하는 것을 막습니다.

    접두어가 있는 프로세스 이름 등록
    감독 트리를 밀봉하기 위해서, 모든 프로세스의 접두사로 공공 키를 사용하고, 이를 감독의 초기 매개 변수로 사용할 수 있습니다.
    defmodule PipelineSupervisor do
      use Supervisor
    
      @impl Supervisor
      def init(%{name_prefix: prefix}) do
        consumer_name = Module.concat([__MODULE__, prefix, Consumer])
    
        children = [
          {SomeConsumer, name: consumer_name}
          {SomeWorker, %{consumer: consumer_name}},
        ]
    
        Supervisor.init(children, strategy: :rest_for_one)
      end
    end
    
    작업을 완료했지만 프로세스 이름 공간을 어지럽히고 어색해 보일 수 있는 API를 만들었습니다. ("왜 내가 접두사를 제공해야 합니까?")

    스텔스 등록표는 관리자를 사용합니다.아동의 기능은 무엇입니까
    만약 우리가 주관자를 더욱 세심하게 주목한다면, 우리는 어떤 주관자 자체가 기본적으로 등록 센터라는 것을 알 수 있다.모든 프로세스의 PID는 물론 모든 프로세스에 고유한 로컬 ID를 사용하여 감독자가 사용할 수 있습니다.
    불행하게도 감독관은 단일한 용도로 구축된 블록이기 때문에 아이들을 찾기 위한 편리한 API가 부족하다.그러나, 우리는 여전히 Supervisor.which_children 함수를 사용하여 모니터를 레지스터로 사용할 수 있다.
    defmodule PipelineSupervisor do
      use Supervisor
    
      @impl Supervisor
      def init([]) do
        supervisor_pid = self()
    
        children = [
          SomeConsumer,
          {SomeWorker, %{parent_supervisor: supervisor_pid}},
        ]
    
        Supervisor.init(children, strategy: :rest_for_one)
      end
    
      def child_pid!(supervisor_pid, child_id) do
        spec = Supervisor.which_children(supervisor_pid)
    
        case List.keyfind(spec, child_id, 0) do
          {_id, pid, _type, _modules} when is_pid(pid) ->
            pid
    
          nil ->
            raise "no started child with id:#{id} in supervisor spec:#{inspect(spec)}"
        end
      end
    end
    
    defmodule SomeWorker do
      use GenServer
    
      @impl GenServer
      def init(%{supervisor_pid: supervisor_pid} = options) do
        {:ok, options}
      end
    
      defp do_process_data_chunk(data_chunk, state) do
        # ...
        consumer_pid = PipelineSupervisor.child_pid!(state.supervisor_pid, SomeConsumer)
        Process.send(consumer_pid, {:message_received, parsed_message})
        # ...
        end
      end
    end
    
    defmodule SomeConsumer do
      use GenServer
    
      def start_link([]) do
        GenServer.start_link(__MODULE__, [])
      end
    
    end
    
    어떤 경우, 모든 메시지에 대한 호출 담당자는 성능 병목을 초래할 수 있다.child_pid! 함수에서 Worker.init을 한 번 호출하고 GenServer 상태에서 Consumer pid를 저장하여 이 문제를 해결할 수 있습니다.
    defmodule SomeWorker do
      use GenServer
    
      @impl GenServer
      def init(%{supervisor_pid: supervisor_pid} = options) do
        consumer_pid = PipelineSupervisor.child_pid!(state.supervisor_pid, SomeConsumer)
        {:ok, %{consumer_pid: consumer_pid}}
      end
    
      defp do_process_data_chunk(data_chunk, state) do
        # ...
        Process.send(state.consumer_pid, {:message_received, parsed_message})
        # ...
        end
      end
    end
    
    그러나 프로세스 PID를 저장할 때, Consumer이 붕괴되면 어떤 일이 일어날지 항상 조심해야 한다.우리의 경우 rest_for_one 주관 전략 때문에 모든 것이 좋아질 것이다.만약 Consumer이 붕괴된다면, Worker에 저장된pid는 효력을 상실할 것입니다.그러나 Google 주관자는rest for one 정책을 설정했습니다. Consumer은 하위 목록에서 Worker보다 높습니다.이에 따라 Consumer이 붕괴되면 Worker도 다시 시작하고 새로운 Consumer의pid를 발견할 수 있다.

    임시 의존항 시작
    또한, 우리는 주관자가 Supervisor.start_child 함수에서 이미 시작된 프로세스의pid를 되돌려준다는 사실을 이용할 수 있다.
    defmodule PipelineSupervisor do
      use Supervisor
    
      @impl Supervisor
      def init([]) do
        supervisor_pid = self()
    
        children = [
          {SomeWorker, %{parent_supervisor: supervisor_pid}},
        ]
    
        Supervisor.init(children, strategy: :rest_for_one)
      end
    
      def start_consumer do
        Supervisor.start_child(SomeConsumer)
      end
    end
    
    defmodule SomeWorker do
      use GenServer
    
      @impl GenServer
      def init(%{supervisor_pid: supervisor_pid} = options) do
        {:ok, options, {:continue, :start_worker}}
      end
    
      def handle_continue(:start_worker, state) do
        {:ok, consumer_pid} = PipelineSupervisor.start_consumer()
        {:noreply, Map.put(state, :consumer_pid, consumer_pid)}
      end
    
      defp do_process_data_chunk(data_chunk, state) do
        # ...
        Process.send(state.consumer_pid, {:message_received, parsed_message})
        # ...
        end
      end
    end
    
    defmodule SomeConsumer do
      use GenServer
    
      def start_link([]) do
        GenServer.start_link(__MODULE__, [])
      end
    
    end
    
    우리는 반드시 :handle_continue 리셋을 사용하여 주관자가 잠기지 않도록 해야 한다.그렇지 않으면, start_child 호출에 회답할 수 없습니다. 왜냐하면 init 함수가 돌아오기를 기다리기 때문입니다.
    이런 DI 방법은 매우 복잡해서 단지 자신의 이익을 위해서만 가치가 없을 수도 있다.그러나 더 복잡한 장면에서 WorkerConsumer에 메시지를 보내야 할 뿐만 아니라 생존 기간(정지 또는 재시작)도 관리해야 하기 때문에 더욱 의미가 있다.

    결론
    내가 보여준 몇 가지 옵션은 복잡해 보일 수도 있고 필요없을 수도 있다.많은 상황에서 이것은 완전히 정확하다.그러나 OTP를 사용하는 것은 GenServer뿐만 아니라 간혹 한두 명의 맞춤형 감독관도 쓴다는 것이 제 주장입니다.OTP는 우리에게 매우 기본적이지만 매우 스마트한 기본 요소 조합을 제공했고 우리는 그것을 사용하여 더욱 높은 수준의 사용 모델을 형성할 수 있다.지역 사회로서 우리의 임무는 이러한 모델을 발견하고 쉽게 사용할 수 있도록 하는 것이다.
    관리자 및 사용 모델에 대한 더 많은 정보를 얻으려면 이 우수한 블로그 글 Using Supervisors to Organize Your Elixir Application을 보십시오

    작성자 정보
    Miroslav Malkin, Erlang/Elixir 전문가, 전화 FunBox입니다.저도 CogitoΣ의 공동 창시자입니다. 우리는 여기서 코드와 과정의 질을 조사하고 제품 성공에 미친 영향을 평가합니다.

    좋은 웹페이지 즐겨찾기