Skip to content

Instantly share code, notes, and snippets.

@palkan
Created December 7, 2016 16:41
Show Gist options
  • Save palkan/4f849adabef70aafd44964988525401d to your computer and use it in GitHub Desktop.
Save palkan/4f849adabef70aafd44964988525401d to your computer and use it in GitHub Desktop.
AnyCable post
title category date description draft authors colors
<b>AnyCable:</b>the Action Cable Power-Up
Back-end
2016-12-17
This article tells the story of the AnyCable project which aims to increase Action Cable performance and functionality.
true
name about email facebook twitter links
Vladimir Dementyev
Back-end Developer at Evil Martians
palkan@evilmartians.com
@palkan_tula
title category date background_css
#d9e021
#29abe2
#f15a24
background: #2D0048
[Action Cable](http://edgeguides.rubyonrails.org/action_cable_overview.html) is one of the most attractive new features in Rails 5. WebSockets out of the box – sounds pretty cool, doesn't it? Or maybe the proper question is "Is it really so good?"

Action Cable shows a plenty of good parts:

  • Stupidly easy configuration
  • Application logic access through channels (definitely the killer feature)
  • Javascript client that just works.

You got everything you need to build yet another Chat on Rails in minutes!

And what about cons?

In my opinion, there is the only one problem (despite bugs, of course) – Ruby itself, the language that I'm fond of, but I'm not considering it as a technology for writing scalable concurrent applications. At least, not yet (yeah, everyone heard about Ruby 3x3 and Matz's plans).

Ruby itself cannot be considered as a technology for writing scalable concurrent applications. At least, not yet.

Benchmarks: Action Cable vs. Erlang vs. Golang

What exactly is wrong with Action Cable? Performance.

Let's consider several metrics: memory usage, CPU pressure and broadcasting time.

We built our benchmarks on top of the famous WebSocket Shootout by HashRocket.

We compare Action Cable with servers written in Erlang and Golang. Hardware we used to run the benchmarks: AWS EC2 4.xlarge (16 vCPU, 30Gib memory).

Let's look at the results!

First, take a look at the RAM usage:

<%= image_tag 'posts/actioncable-power-up/memory.png', alt: 'Action Cable memory usage', class: 'post-media__object' %> **Benchmark:** handle 20 thousand idle clients to the server (no subscriptions, no transmissions).

Results are pretty self-explaining. Action Cable requires much more memory. Because it's Ruby. And, of course, Rails too.

For the next two measurements we used WebSocket Shootout broadcasting benchmark which can be described as follows: 1000 clients connect, 40 of them sends a message to a server, which is then re-transmitted to all the clients. The sender-client measures the time it takes the message to make a round-trip (RTT).

See how CPU feels itself during the benchmark:

<%= image_tag 'posts/actioncable-power-up/cpu.gif', alt: 'Action Cable CPU usage', class: 'post-media__object' %>

And finally, the broadcasting performance:

<%= image_tag 'posts/actioncable-power-up/rtt.png', alt: 'Action Cable RTT', class: 'post-media__object' %>

About one second for only one thousand clients and more than ten seconds for 10 thousand! It doesn't look like real-time, does it?

So, that's why I'm not going to use Action Cable in production.

Can we find a way out? Can we use the good parts of Action Cable with the power of Erlang/Golang/Whatever-you-like altogether?

My answer is "Yes, we can." And here comes AnyCable.

I'm not going to use Action Cable in production.

The Idea

When @dhh announced Action Cable on April 2015, my first thought was "Wow! Cool! Omakase rulz!" Then the second thought: "Oh, no, Ruby was not meant for that." And finally, the third thought: "I guess, I know how to fix this!"

I had an experience in writing WebSocket-based applications in Erlang, and I knew that Erlang is one of the best players in this game.

So I wrote down a simple architecture sketch:

<%= image_tag 'posts/actioncable-power-up/sketch.png', alt: 'AnyCable sketch', class: 'post-media__object' %>

Looks pretty obvious except from one thing: the communication between Erlang server and Rails app (Action Cable channels).

The straightforward solution – HTTP ❤️ REST! But we want to improve performance, did you forget? Not an option.

"Aren't we able to develop our own RPC protocol with raw TCP transport and binary encoding, maybe, Protocol Buffers?" – I asked myself. "Sure!" was the answer. And I've started thinking that way...

Hopefully, I found a better option – gRPC. The puzzle had been solved, and AnyCable was born.

Introducing AnyCable

Take a look at the figure below:

<%= image_tag 'posts/actioncable-power-up/anycable_arch.png', alt: 'AnyCable architecture', class: 'post-media__object' %> AnyCable architecture.

AnyCable WebSocket server is responsible for handling clients, or sockets. That includes:

  • low-level connections (sockets) management

  • subscriptions management

  • broadcasting messages to clients.

It doesn't know anything about your business-logic. It's a kind of logic-less proxy server (just like NGINX for HTTP traffic).

WebSocket server should include gRPC client built from AnyCable rpc.proto, which describes the following RPC service:

service RPC {
  rpc Connect (ConnectionRequest) returns (ConnectionResponse) {}
  rpc Subscribe (CommandMessage) returns (CommandResponse) {}
  rpc Unsubscribe (CommandMessage) returns (CommandResponse) {}
  rpc Perform (CommandMessage) returns (CommandResponse) {}
  rpc Disconnect (DisconnectRequest) returns (DisconnectResponse) {}
}

RPC server is a connector between Rails application and WebSocket server. Actually, it's just an instance of your application with a gRPC endpoint, which implements rpc.proto.

This server is a part of the anycable gem. This gem also makes it possible to use all Action Cable infrastructure (channels, streams) without any tweak (see below for more information).

We use Redis pub/sub to send messages from the application to the WS server, which then broadcasts the messages to clients.

We're discussing other options for this communication, 'cause we don't want to require any specific dependencies (even such popular technology as Redis). Feel free to join us!

How AnyCable works?

After the brief look at the AnyCable architecture let's go deeper and see what's going on from both a Rails application and a client points of view.

Consider a simple example – yes, yet another Action Cable chat application.

We want to implement the following features:

  • Only authenticated users can join rooms

  • User can join the specific chat room (using ID)

  • User can send a message to everyone in the room.

Every feature requires some communication between client and server. Let's discuss these communications one by one.

Authentication

How does an authentication happen in Action Cable? The same way we usually do in our controllers – through the cookies.

Every time a client connects to Action Cable the application instantiate an instance of ApplicationCable::Connection class and invokes #connected method on it. This instance is added to the list of all connections and thus stays in memory 'till client disconnects.

The connection allows you to access the underlying request object and, of course, it's headers (including cookies):

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connected
      self.current_user = User.find_by(id: cookies[:user_id])
      reject_unauthorized_connection unless current_user
    end
  end
end

But what about AnyCable? Connection itself occurs outside of the Rails application, in our WebSocket server. How can we authenticate it (or more precisely, how to invoke our #connected method)?

This is how AnyCable WebSocket server handles a client connection:

– accepts connection

  • invokes Connect RPC method and pass the request headers (some of them, e.g. 'Cookies') to it

  • receives a response from RPC server describing connection identifiers (JSON encoded) and status (diconnected or not?)

  • disconnects the client if necessary

  • transmit messages to the client if the response object contains any.

And that's what happens within Connect method realization in our RPC server:

  • create instance of slightly patched ApplicationCable::Connection class

  • invoke #handle_open on this connection

  • reply with the connection information (identifiers) and status.

Note that AnyCable doesn't store any objects in memory (e.g. connections), they are all disposable.

Subscriptions

Ok, now we're connected to the server. But to make this connection useful we have to subscribe to a channel. To do that the client has to send a message of a specific form – {"command": "subscribe", "identifier": "..."}.

Consider our chat example channel's code:

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:id]}"
  end
end

The client sends the message (JS code):

App.cable.subscriptions.create({channel: 'ChatChannel', id: 1}, ...)

AnyCable server transforms this message into a Subscribe RPC call and then:

  • adds the client (socket) to the "chat_1" broadcasting group

  • sends a subscription confirmation to the client.

What's the broadcasting group? Implementations may vary and depend on a language or platform you choose to build your WebSocket server. But to one degree or another it quacks like a hash that maps streams to connections.

Performing Actions

We're almost done. The only question remains: how to send message to the clients?

Let's add one more method to our channel:

class ChatChannel < ApplicationCable::Channel
  ...

  def speak(data)
    ActionCable.server.broadcast(
      "chat_#{params[:id]}",
      text: data['text'], user_id: current_user.id
    )
  end
end

To perform an action the client should send another command – {"command": "message", "identifier": "..."}. So we can handle it the same way we do with the "subsribe" command.

What's interesting here is the #broadcast call. Why? Because it's the weakest part of Action Cable (see broadcasting benchmark above).

In a simplified form broadcasting can be implemented like this:

subscribers_map[stream_id].each do |channel|
  channel.transmit message
end

What's wrong with it? First, Rails itself. The second suspect is Ruby.

AnyCable moves this functionality out of Ruby and Rails and leverages the power of other technologies.

I think, it's time to repeat our benchmarks with the new kid on the block – AnyCable.

Benchmarks: AnyCable

AnyCable shows almost the same resources usage as raw Erlang or Golang servers.

Memory usage is more than acceptable:

<%= image_tag 'posts/actioncable-power-up/memory.png', alt: 'AnyCable memory usage', class: 'post-media__object' %> **Benchmark:** handle 20 thousand idle clients to the server (no subscriptions, no transmissions).

And CPU doesn't feel any pressure too:

<%= image_tag 'posts/actioncable-power-up/any_cpu.gif', alt: 'AnyCable CPU usage', class: 'post-media__object' %>

Broadcasting time is perfect:

<%= image_tag 'posts/actioncable-power-up/rtt.png', alt: 'Action Cable RTT', class: 'post-media__object' %>

Compatibility

AnyCable aims to as much compatible with Action Cable as it possible. First of all, to allow you to use standard Action Cable in development and test environments.

But some features are rather hard to implement without a loss of the perfmorance gain. See compatibility table for more information.

What's next?

We have learned the only one aspect of AnyCable – the performance improvement over Action Cable.

This approach – extracting low-level functionality from business-logic (or seperation of concerns) – can help us to solve many problems, for example:

  • support transport fallbacks (e.g. long-polling) or custom transports

  • build shareable WebSocket servers (i.e. one WebSocket server for several different applications)

  • add enhanced analytics and monitoring to your WebSocket server.

And, of course, you can easily extend your cable with custom features, which are not supported (and unlikely to be) by Action Cable.


AnyCable is a fast-growing open-source project, which includes several libraries in different languages. We are currently on alpha stage and have a plan to become beta in a couple of months.

We are open for questions (use our [Gitter]) and contributions (check out AnyCable projects on GitHub).

Let's make Action Cable not suck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment