Why is ranch server slow?

As a beginner, this is the “big” personal project am currently working on. So, the basic premise is: I am building a TCP server (-which adheres certain “spec”) which accepts a “command” and sends a response to it. My only criteria for this server is how fast does it respond back

After much debating, I chose :ranch as my preferred TCP server. There are loads of blogs, articles praising its performance.

Given this I tried two different versions of the respond handler. There are some interesting articles I studied along with the way, based on which I have the below handlers. Both work fine and does their job. But, here is the problem:

the logic:

CmdHandler.handle_command(raw_command)

takes ~30 μs (99%)

but, the req-response latency is around ~10 mill seconds(99%) and I barely get 9k RPS for a single connection. THIS is not what I expected for all the tall claims from ranch. I am not sure where I am doing wrong, I want to fix this. any help is good.

Environment:

Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.6.4 (compiled with OTP 20)
macOs 10.13.2 Kernel Version 17.3.0

Some good articles that gave a lot of insights on ranch:


TCP Handlers

defmodule Server.GenServerHandler do
  require Logger
  alias Command.Handler, as: CmdHandler

  use GenServer
  @behaviour :ranch_protocol

  def start_link(ref, socket, transport, _opts) do
    pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport])
    {:ok, pid}
  end

  def init(ref, socket, transport) do
    log(socket, "New Connection")

    :ok = :ranch.accept_ack(ref)
    :ok = transport.setopts(socket, [{:active, true}, {:nodelay, true}, {:reuseaddr, true}])
    :gen_server.enter_loop(__MODULE__, [], %{socket: socket, transport: transport})
  end

  def handle_info({:tcp, socket, input}, %{socket: socket, transport: transport} = state) do
       resp =input |> CmdHandler.handle_command()
    transport.send(socket, resp)
    {:noreply, state}
  end

  def handle_info({:tcp_closed, socket}, %{socket: socket, transport: transport} = state) do
    log(socket, "Terminating connection")
    transport.close(socket)
    {:stop, :normal, state}
  end

  defp log(ref, input, resp \\ "") do
    Logger.debug(
      fn ->
        "[#{inspect(ref)}]: #{input} || #{resp}"
      end
    )
  end
end

defmodule Server.SimpleHandler do
  alias Command.Handler, as: CmdHandler

  def start_link(ref, socket, transport, opts) do
    pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
    {:ok, pid}
  end

  def init(ref, socket, transport, _Opts = []) do
    :ok = :ranch.accept_ack(ref)
    :ok = transport.setopts(socket, [{:nodelay, true}, {:reuseaddr, true}])
    loop(socket, transport)
  end

  def loop(socket, transport) do
    case transport.recv(socket, 0, 5000) do
      {:ok, input} ->
        resp = input |> CmdHandler.handle_command()
        transport.send(socket, resp)
        loop(socket, transport)
      {:error, :closed} ->
        :ok = transport.close(socket)
      {:error, :timeout} ->
        :ok = transport.close(socket)
      {:error, _} -> # err_message
        :ok = transport.close(socket)
    end
  end
end
1 Like