Links
Tags
apache
armenia
books
bsd
c
c++
chips
cinema
concurrency
cooking
database
dragonfly
erlang
filesystem
freebsd
fun
hardware
java
javascript
json
languages
linux
lyric
mac_osx
mail
math
misc
music
personal
poems
presentation
programming
python
references
ruby
rubyjs
scm
software
spiking_neural_net
study
sysadm
sysarch
technology
testing
travel
virtualization
web
wee
windows
Damien Katz, the creator of CouchDB, writes about what sucks about Erlang.
The last day I hacked on another piece of Erlang code. I gave it the name http_hub. It’s not that easy to explain what it actually does. Clients can connect to it, as it behaves for them like a HTTP server. The client’s request is forwarded to a backend server. But no answer is sent back to the client. Instead http_hub waits on another port (e.g. 14000) for another HTTP request from the backend server (usually a single worker node in a cluster will answer), which includes the response that should be sent back to the client. Once this arrives, the client is answered with the included response. If no request on port 14000 arrives within a specified time, an error response is sent back to the client.
The code is here.
While hacking Erlang I discovered some pitfalls or non-orthogonalities.
Process ID and registered atom non-orthogonality
In Erlang you can globally register a Process Id (PID) to an atom as shown below. Then you can use the atom to send a message to the process instead of the PID itself. It’s like a global variable for now. But as you can see in the last line, the atom is not accepted when being assigned to a variable. That’s ugly!
Pid = spawn(fun() -> ... end), % spawn a process
Pid ! {msg, "test"}, % send a message
register(test, Pid), % register process
test ! {msg, "test"}, % send message
P = test,
P ! {msg, "test"} % doesn't work!
Put function
Because of the non-orthogonality described above, I had to write code like shown below to allow both atoms and PIDs as arguments.
put(Pid, N) when is_atom(Pid) -> put(whereis(Pid), N);
put(Pid, N) -> Pid ! {put, N}, N.
When I compile this file I get the following warning:
./credit.erl:8: Warning: call to put/2 will call erlang:put/2; not put/2 in this module (add an explicit module name to the call to avoid this warning)
This means that I have to explicitly specify the module name in the call to put. But there is a difference (for example in performance) when explicitly specifying the module name. Maybe there is another solution I don’t know about. Probably one should not use put as function name at all.
I’m currently reading the great Programming Erlang book. At the end of chapter 8, the author invites the reader to implement a ring benchmark in Erlang. Then implement the same in another language, compare the results and finally blog about it. That’s what I am doing here ;-). I’ve choosen Ruby because it’s the language I am mostly familiar with (and I am too lazy to implement it using C and MPI).
The Ruby version uses Fibers (Co-Routines) and as such needs Ruby 1.9. It has half the number of lines as the Erlang version and is IMHO easier to understand, thanks to Arrays and loops. The Erlang program uses processes and is around 4 times as fast.
The sourcecode for both languages you can find here.
Okay, I did a simple performance benchmark on my laptop.
My 31 lines Erlang webserver shown below using the undocumented option {packet, http} as described here is very fast!
-module(http).
-export([start/0]).
start() ->
{ok, LSock} =
gen_tcp:listen(8081, [binary, {packet, http}, {reuseaddr, true}, {active, false}, {backlog, 30}]),
accept(LSock).
accept(LSock) ->
{ok, Sock} = gen_tcp:accept(LSock),
Pid = spawn(?MODULE, request(Sock)),
accept(LSock).
request(Sock) ->
{ok, {http_request, Method, Path, Version}} = gen_tcp:recv(Sock, 0),
headers(Sock).
headers(Sock) ->
case gen_tcp:recv(Sock, 0) of
{ok, {http_header, _, _, _, _}} -> headers(Sock);
{error, {http_error, _}} -> headers(Sock);
{ok, http_eoh} -> body(Sock)
end.
body(Sock) ->
gen_tcp:send(Sock, [<<"HTTP/1.1 200 OK\r\n">>,
["Connection", ": ", "Keep-Alive", "\r\n",
"Content-Length: 7\r\n",
"Content-Type: text/plain\r\n"],
<<"\r\n">>, <<"1234567">>]),
request(Sock).
Save this code in a file http.erl. Then start up Erlang "erl" and type "c(http)." followed by "http:start().". After that, you can run apache-bench on it:
ab -n 50000 -c 100 -k http://127.0.0.1:8081/test
The result on my very old laptop is 3600 requests/sec.
Now the same with Mongrel:
require 'rubygems'
require 'mongrel'
class TestHandler < Mongrel::HttpHandler
def process(req, resp)
resp.start(200) do |head, out|
head["Content-Length"] = "7"
out.write("1234567")
end
end
end
Mongrel::Configurator.new :host => '127.0.0.1' do
listener :port => 8081 do
uri "/test", :handler => TestHandler.new
run
join
end
end
The same benchmark with Mongrel gives only around 500 requests/sec. Of course Mongrel does a little more than the simple Erlang web server shown above. And I think Mongrel doesn't employ connection keep alive. But if you see that lighttpd serves only at 1050 requests/sec, then it's impressive what the Erlang web server can handle. Okay, lighttpd has to load the file from disk (7 bytes), that's much less efficient than serving directly from memory.