kseg
I feel like I’m doing something silly. I’m just trying to compare a dummy http server to cowboy and even though my “implementation” is absolutely incomplete, I’m getting considerably worse performance. I figure I’m missing something fundamental.
I know this isn’t a valid http server. I know I can’t just recv once and get an entire payload (though, in this case it’s fine because the payloads are very small)
Here’s what I have:
def run() do
tcp_opts = [:binary, packet: :raw, active: false, nodelay: true, send_timeout_close: true, reuseaddr: true, backlog: 1024]
{:ok, socket} = :gen_tcp.listen(8420, tcp_opts)
accept_loop(socket)
end
defp accept_loop(listen_socket) do
{:ok, client_socket} = :gen_tcp.accept(listen_socket) do
pid = spawn fn -> read_loop(client_socket) end
:gen_tcp.controlling_process(client_socket, pid)
accept_loop(listen_socket)
end
defp read_loop(socket) do
{:ok, data} = :gen_tcp.recv(socket, 0)
conn = %Plug.Conn{
port: 8420,
scheme: :http,
method: "GET",
owner: self(),
path_info: [],
query_string: "",
req_headers: %{},
request_path: "",
host: "127.0.0.1",
remote_ip: {127, 0, 0, 1},
adapter: {__MODULE__, socket},
}
Router.call(conn, [])
read_loop(socket)
end
def send_resp(socket, _status, _headers, _body) do
res = [
"HTTP/1.1 404 Not Found\r\n",
"Content-Length: 0\r\n\r\n"
]
:ok = :gen_tcp.send(socket, res)
{:ok, nil, socket}
end
Router is just a Plug router (what you’d pass as :plug to the Plug.Cowboy.
I’m using autocannon. It’s only starting 10 connections and then doing keepalive (so I doubt the problem is in the accept loop). Cowboy does ~30K/second. This code does a bit less than half. This seems incredible to me given that Cowboy actually has to parse the request and worry about TCP fragmentation.
Anyone know what’s up?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #iex
- #elixirconf-us
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 5- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
OvermindDL1
Well first note, you don’t want to use
:gen_tcp.recvfor the fastest speed, you probably want to useactive: :onceor whatever it was with async messaging back into the app (or if you don’t have to worry about reading messages fast enough then just active true). Arecvis synchronous and involves even more messages, so that would be one of a few causes of slowdowns there, but the first to fix.The next step to optimize is that when you receive a message with active: once then you probably want to go to a full active loop until no message in X milliseconds, at which point drop back to active: once.
Though if this is a completely arbitrary ‘fast-as-possible’ test then just staying fully active receiving would be fastest.
OvermindDL1
Have a couple more minutes right now, looking more.
Could start up a couple of listen sockets on that same port in different processes for faster accepting, in ‘some’ cases it helps.
Also I see you are using
:gen_tcp.sendhere as well, this is also synchronous for a response from a successful send from the OS and can slow you down a good bit if you are using keepalive to handle many requests on a single socket, it can be easiest just to slave it out to another process and then immediately receive for the next packet. However as a single connection on HTTP1 is serialized anyway, it’s not a big deal, justinetsetopt to do active: :once again then callsendin that order and you’ll have a packet waiting in your mailbox about the time it returns.There are lots of other things that can be done, but at the very least I’d use
accept: :onceoraccept: truedepending. For this specific case (although someone could massively slow down the server in that case by sending tons of packets) active: true would probably be fastest, but active: :once is both quite fast (much more so thenrecv/2and safe.OvermindDL1
This is super low level stuff so ignore this post unless you really really want every microsecond, but you can bypass most of the
:gen_tcp.sendcost by sending asynchronously so the socket port directly via low level port commands, you can even get a notification back that way allowing it to fail (and it would tell you) so you know when to back off if you are sending too fast.xlphs
I typically create or set socket with
active: falsethen send message to genserver itself periodically to receive using:inet.setopts(socket, active: 1). And definitely create a new process for each client with:gen_tcp.controlling_process/2. In the rare case the protocol dictates more message is expected after parsing then use:gen_tcp.recv/2.kseg
Thanks. I’ll try that. I initially had a more complete parser with
active: once..but I started to dumb it down to try to figure out what was going on.active: oncewasn’t any faster thanrecvbut setting it before the send is a good idea!