Tuesday, September 25, 2007

Great Concurrency -> Better Webapps

I've been asked a few times: Is concurrent programming really useful for web developers? Web servers already handle client requests in different threads/processes, so application code doesn't need to use concurrency explicitly, right?

My opinion is that althought it's possible to write many database-driven web apps without using concurrency, concurrency can still be useful.

Vimagi has some real-time features in that rely heavily on concurrency, but it also helps in a more common scenario: speeding up page loads.

In Vimagi, every time someone views a painting or a user profile, a SQL query is executed to increment the view counter for that page. This is more-or-less what the controller code does:


show(A, Id) ->
painting:increment([num_views], {id,'=',Id}),
Painting = find_id(Id),
{data, Painting}.


This caused a problem. When traffic increased, the 'increment' function, which executes an UPDATE statement in the database, started performing poorly, thereby noticeably slowing down page loads. Occasionally, it even timed out after 5 seconds, showing an error screen to the user.

I increased the timeout to 20 seconds. This helped avoid the error screen, but the response time was still too long.

Erlang made this almost too easy to fix. All I had to do was change


painting:increment([num_views], {id,'=',Id})


to


spawn(
fun()->
painting:increment([num_views], {id,'=',Id})
end)


It's darn simple, and it works. Instead of executing the UPDATE statement in the process that's responsible for rendering the page, I spawn a new process and execute the transaction there. Spawning processes in Erlang is dirt-cheap, so I don't have to worry about performance or memory issues. The user gets a quick response, the view counter is incremented, and the problem goes away.

20 comments:

CV said...

Don't you then end up with an ever-growing queue of updates hitting your SQL server?

Yariv said...

That may happen at some point depending on the level of traffic. If it does, I'll have to let a backgroud process accumulate the page view counts and periodically dumb them into the database.

zimbatm said...

Do you really need to update the database on every request ? You could have a counting process that synchronized to the database each X times.

Jeff said...

You can limit overhead by creating a separate table for the counting. A simple table, just painting_id (primary key), count, would not tax the db heavily, even with many updates over many rows.

iWantToKeepAnon said...

Glad to see erlyweb apps being made. :-))

One other benefit of spawning is that each process gets its own memory heap. Erlang won't GC until a certain amount of that memory is used. So by spawning the update you "get in/get out" so fast that no GC happens. Since spaw is cheap and GC isn't, you probably made the process leaner as well as faster.

In the "inline update" mode you update the DB and then create the content. When creating the content you may have pushed passed the "grace period" and forced Erlang to start GC-ing.

Bryan said...

Hey - I use the same design for Facebook updates. If a user does something that should update her profile immediately (like choosing a new favorite), I spawn a new process to make that REST-API call, instead of making her wait.

Sounds like one of the first patterns we'll see in the inevitable "ErlyWeb in a Nutshell", eh? ;)

wingedsubmariner said...

I can imagine some much more intense uses of this, such as a blog that for every new post, indexes its tags and content for a search function, and computes the new posts related posts, and for every other post computes whether the new post should be a related post, and generates and caches a new tag cloud, and does trackback pinging. And...

The hard part is understanding why (how?) we've gone without concurrency all these years.

wingedsubmariner said...

vimagi looks cute and fun, but http://vimagi.com/gallery/gallery/popular?page=2 says "An internal error occurred." :'(

Nathan Youngman said...

Hi Yariv,

Yes , I have often wondered how concurrency could be used in the middle layer (between the web server and database).

That's one example... when the response doesn't rely on part of the request. Another example could be preparing and sending an email.

The other I can think of is several separate queries/tasks (say for a report) that don't rely on each other. All could run at once, and once all returned, it could move onto the next step of rendering the page.

Of course that doesn't always work, sometimes processing would have to be sequentially.

Nathan.

Nathan Youngman said...

Yariv,

I haven't actually learned Erlang yet, just have been reading *about* it. I would be interested in your thoughts on implementing the above sort of things?

One could also spawn a task to load up the various templates (views). Could you then "block" until the model data was ready... do a quick variable substitution and return the response?

When a query finishes, it can message the main process, right? But how does it ensure that all tasks it sent off are done and ready?

- nathan.

Robin said...

Good point: Significant tasks that are not in the critical path should be executed concurrently, which is trivial in Erlang.

I am sure many ErlyWeb developers are facing a design decision: SQL or Mnesia.

If Great Concurrency -> Better Webapps, then why does Vimagi opt for SQL, when Mnesia is there at your fingertips?

Yariv said...

@Nathan: I think you're on the right track. If your webpage has independent components that can be rendered in parallel, why not use concurrent processes to speed things up? It's certainly doable.

@Robin: Mnesia is great for datasets that fit in RAM, but it's not so great for high-volume data that should be stored on disc. The good thing about Erlang/ErlyWeb is that you can have the best of both worlds. Vimagi, for example, uses MySQL for persistent storage and Mnesia for storing session data.

ajrw said...

Out of curiosity, how much traffic are we talking about here?

Your spam prevention input box keeps sending me back to the comment box when I try to type in it, I had to copy and paste the captcha.

Yariv said...

The traffic numbers aren't very high. I don't know exactly why MySQL started timing out, but it may be because the VPS has little RAM and/or the MySQL server isn't well tuned. There was definitely a gradual degradation of response time from MySQL when the site went live, though.

I know, this captcha plugin sucks. I tried finding a better one but couldn't find one that worked well.

Nathan Youngman said...

@Yariv: But is it worth it to write a web application in a concurrent way? With a lot of traffic, it might be better to dedicate those 80,000 lightweight processes to individual requests, no?

I guess it depends on how much can be gained in response time. Perhaps on big reports with a lot of data fetching. If the queries can't run any faster, it could be fairly trivial to run them asynchronously.

I'm curious about a few things... like how Yaws compares to light-weight web servers like Lighttpd or Nginx.

Also, has anyone considered working with Flex/Flash/AMF3? LiveCycle Data Services has the idea of users concurrently editing data-grids and such, but it's pricey and from what I've heard, not the most stable yet. Erlang seems like a good fit for a server that handles the large number of connections... and could provide a full-stack from web server down to data store. Flash seems a little more well-suited for a lot of client-side concurrency, and open connections (Comet). Though perhaps that isn't desirable, since concurrency in Flash isn't as nice to work with as Erlang (mind you, neither is JavaScript).

- nathan.

bmert said...

@Bryan, how do you handle sessions while updating facebook user profiles. Do you need a permanent session for that?

Yariv said...

@Nathan, I've read of benchmarks where people ran 1-2 million Erlang processes on a single box. The 80,000 number came from Joe Armstrong's benchmark of Yaws, which showed Yaws can support ~80,000 mostly idle processes with relatively stable total throughput. However, that doesn't mean you can't run more than 80,000 processes on a server.

jherber said...

this is a bad approach.

it is terribly inefficient. why not launch a persistent process that does nothing listen for update request messages and write to the db. messaging in erlang is asynchronous, so the code doesn't block.

*bonus* the other thing you are doing wrong is updating the db on every hit. you should update the database after say every N hits, where N is a small number who's precision you are comfortable with losing if you have a catastrophic failure (10? 100?).

please keep updating your blog. erlang is looks very promising so there is building interest in your thoughts, prolbems, and solutions.

Yariv said...

I know it's not an optimally efficient solution, but it's good enough, at least for a site that doesn't get thousands of requests/sec. Having a background process that listens for updates and executes queries only saves you 320 bytes or so of RAM (the size of an Erlang process) per request (Erlang processes are dirt-cheap). If the background process also batched updates, the efficiency would be greater because it would result in fewer queries. Instead of updating every N hits, I would make it update every N seconds. This way I would throttle the number of update queries while guaranteeing a time ceiling for persisting even low numbers of updates. However, this is an optimization that I don't think Vimagi requires right now.

Actual window manager 5.2. said...

Actual window manager 5.2....

Actual window manager 5.2....