Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[RubyKaigi 2018] AnyCable: One cable to rule them all

[RubyKaigi 2018] AnyCable: One cable to rule them all

Vladimir Dementyev

June 01, 2018
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. palkan_tula palkan THE TALK 10 Cables of Ruby and the

    rest of the world The tale of Action Cable AnyCable, the half-blood prince
  2. palkan_tula palkan IN A NUTSHELL 18 Server Client WebSocket Client

    WebSocket stream C Broadcaster stream B stream A stream B
  3. palkan_tula palkan IN A NUTSHELL 18 Server Client WebSocket Client

    WebSocket stream C Broadcaster stream B stream A stream B channel X channel Y channel Z
  4. palkan_tula palkan CHANNELS 19 class AnswersChannel < ApplicationCable ::Channel def

    subscribed reject_subscription unless current_user.admin? end def follow(params) stream_from "questions/ #{params['id']}" end end
  5. palkan_tula palkan SHOOTOUT 24 Broadcast RTT 0,0s 0,8s 1,6s 2,4s

    3,2s 4,0s 4,8s 5,6s 6,4s 7,2s 8,0s Number of connections 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 Go Erlang Action Cable (8x) Action Cable (2x) https://github.com/anycable/anycable/tree/master/benchmarks
  6. palkan_tula palkan MEMORY 29 20k idle connections MB 0 200

    400 600 800 1 000 1 200 1 400 1 600 Go Erlang Action Cable (8x)
  7. palkan_tula palkan http://equipe.com “With Action Cable, we could easily have

    20+ 1GB dynos running during the weekends, with every growing memory” –Jon Stenqvist, CEO, Equipe
  8. palkan_tula palkan –Bo “Due to code duplication and extra work,

    the memory consumption for hijack based solutions is higher and their performance is slower (more system calls, more context switches, etc’)..” 33 https://bowild.wordpress.com/2018/05/01/rubys-rack-push-decoupling-the-real-time-web-application-from-the-web/ THE hijack PRICE
  9. palkan_tula palkan AnyCable 55 syntax = "proto3"; package anycable; service

    RPC { rpc Connect (ConnectionRequest) returns (ConnectionResponse) {} rpc Command (CommandMessage) returns (CommandResponse) {} rpc Disconnect (DisconnectRequest) returns (DisconnectResponse) {} }
  10. palkan_tula palkan gRPC RPS 58 RPC type \ Concurrency 1

    10 50 100 AnyCable RPC (single connection) 1600 2200 2400 2800 AnyCable RPC (connection pool) 1500 1900 2300 2900 Noop RPC (single connection) 3000 4600 5500 6000 * Only build Action Cable connection object without performing an action https://github.com/anycable/anycable/blob/master/benchmarks/2018-05-27-rpc-bench.md
  11. palkan_tula palkan SHOOTOUT 62 Broadcast RTT 0,0s 0,8s 1,6s 2,4s

    3,2s 4,0s 4,8s 5,6s 6,4s 7,2s 8,0s Number of connections 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 anycable-go erlycable Action Cable (8x) https://github.com/anycable/anycable/tree/master/benchmarks
  12. palkan_tula palkan 63 ActionCable: 20+ 2X (1GB) dynos AnyCable: 4

    X (0.5GB) dynos $1000 $100 http://equipe.com
  13. palkan_tula palkan AnyCable 65 WS RPC connect identifiers (JSON +

    GlobalID) command (subscribe/perform) with identifiers initialize “connection”, authenticate initialize “connection” and channel, lazily restore identifiers, perform command transmissions, streams to subscribe * “connection” is a temp object quacking like ActionCable ::Connection
  14. palkan_tula palkan AnyCable 66 class RPCHandler < Anycable ::RPC ::Service

    def connect(request, _unused_call) # WebSocket mock; rack env is built from request headers socket = build_socket(env: rack_env(request)) # Pluggable connection factory connection = connection_factory.call(socket) connection.handle_open if socket.closed? Anycable ::ConnectionResponse.new(status: Anycable ::Status ::FAILURE) else Anycable ::ConnectionResponse.new( status: Anycable ::Status ::SUCCESS, # push identifiers JSON back to WebSocket server to re-use in # subsequent calls identifiers: connection.identifiers_json, transmissions: socket.transmissions ) end end end
  15. palkan_tula palkan AnyCable 67 class RPCHandler < Anycable ::RPC ::Service

    def command(message, _unused_call) socket = build_socket connection = factory.call( socket, # identifiers JSON from WebSocket server identifiers: message.connection_identifiers ) result = connection.handle_channel_command( message.identifier, message.command, message.data ) Anycable ::CommandResponse.new( status: result ? Anycable ::Status ::SUCCESS : Anycable ::Status ::FAILURE, disconnect: socket.closed?, stop_streams: socket.stop_streams?, streams: socket.streams, transmissions: socket.transmissions ) end
  16. palkan_tula palkan DISCONNECT 73 WebSocket Server App App gRPC App

    App App App Envoy Proxy * https://www.envoyproxy.io
  17. palkan_tula palkan PLUG-N-PLAY 76 gem 'anycable-rails', group: :production rails generate

    anycable config.action_cable.url = ‘ws: //example.com:3334' ./bin/anycable # => Run RPC server brew install anycable/anycable/anycable-go anycable-go # => Run WebSocket server
  18. palkan_tula palkan COMPATIBILITY 77 Feature Status Connection Identifiers + Connection

    Request (cookies, params) + Disconnect Handling + Subscribe to channels + Parameterized subscriptions + Unsubscribe from channels + Subscription Instance Variables - Performing Channel Actions + Streaming + Remote Disconnect wip (planned for 0.6.0) Custom stream callbacks - Broadcasting +
  19. palkan_tula palkan LiteCable 80 Rails-free Action Cable No deps (even

    ActiveSupport) Compatible with AnyCable Compatible with Action Cable clients
  20. palkan_tula palkan module Chat class Channel < LiteCable ::Channel ::Base

    identifier :chat def subscribed stream_from "chat_ #{chat_id}" end end end 81 LiteCable
  21. palkan_tula palkan run Rack ::Builder.new do map '/cable' do use

    LiteCable ::Server ::Middleware, connection_class: Chat ::Connection run proc { |_| [200, {}, ['']] } end end 82 LiteCable
  22. palkan_tula palkan CHANNEL 87 Many channels are as simple as

    that: What about not calling RPC in this case? class AnswersChannel < ApplicationCable ::Channel def follow(params) stream_from "questions/ #{params['id']}" end end
  23. palkan_tula palkan NOTE 96 All the Golang code in this

    slides is simplified for readability I just removed all the error handling stuff)
  24. palkan_tula palkan MRUBY-GO 97 package mrb import ( "io/ioutil" "sync"

    "github.com/mitchellh/go-mruby" ) // Engine represents one running mruby VM type Engine struct { VM *mruby.Mrb } // NewEngine builds new mruby VM and return new engine func NewEngine() *Engine { return &Engine{VM: mruby.NewMrb()} }
  25. palkan_tula palkan MRUBY-GO 98 // LoadString loads, parses and eval

    Ruby code within a vm func (engine *Engine) LoadString(contents string) { ctx := mruby.NewCompileContext(engine.VM) defer ctx.Close() filename, _ := nanoid.Nanoid() ctx.SetFilename(fmt.Sprintf("%s.rb", filename)) parser := mruby.NewParser(engine.VM) defer parser.Close() parser.Parse(contents, ctx) parsed := parser.GenerateCode() engine.VM.Run(parsed, nil) }
  26. palkan_tula palkan MRUBY-GO 99 // LoadFile loads, parses and eval

    Ruby file within a vm func (engine *Engine) LoadFile(path string) error { contents, err := ioutil.ReadFile(path) if err != nil { return err } return engine.LoadString(string(contents)) } // Eval runs arbitrary code within a vm func (engine *Engine) Eval(code string) (*mruby.MrbValue, error) { return engine.VM.LoadString(code) }
  27. palkan_tula palkan MRUBY-GO 100 func TestLoadString(t *testing.T) { engine :=

    NewEngine() engine.LoadString( ` module Example def self.add(a, b) a + b end end `, ) result, err := engine.Eval("Example.add(20, 22)") assert.Nil(t, err) assert.Equal(t, 42, result.Fixnum()) }
  28. palkan_tula palkan MRUBY CHANNELS 102 RPC server responds with cacheable

    methods source Load these methods into mRuby VM Invoke mRuby methods instead of calling RPC
  29. palkan_tula palkan MRUBY CHANNELS 103 RPC server responds with cacheable

    methods source [WIP] Load these methods into mRuby VM Invoke mRuby methods instead of calling RPC
  30. palkan_tula palkan MRUBY CHANNELS 104 RPC server responds with cacheable

    methods source [WIP] Load these methods into mRuby VM Invoke mRuby methods instead of calling RPC
  31. palkan_tula palkan MRUBY CHANNELS 105 // NewMCache builds a new

    cache struct for mruby engine func NewMCache() *MCache { return &MCache{ store: make(map[string]map[string]*MAction), } } func(c *MCache) NewEngine() (engine *mrb.Engine){ baseChannelSource := box.String("mrb/files/channel.rb") engine := mrb.NewEngine() // Build base channel class engine.LoadString(baseChannelSource) }
  32. palkan_tula palkan MRUBY CHANNELS 106 // Put compiles method and

    put it in the cache func (c *MCache) Put(channel string, action string, source string) (err error) { var maction *MAction maction, _ = NewMAction(c, channel, source) if _, ok := c.store[channel]; !ok { c.store[channel] = make(map[string]*MAction) } c.store[channel][action] = maction return }
  33. palkan_tula palkan MRUBY CHANNELS 107 // NewMAction compiles a channel

    class within VM func NewMAction(cache *MCache, ch string, src string) *MAction { var buf strings.Builder channelClass := "CachedChannel_" + ch buf.WriteString( "class " + channelClass + " < AnyCable ::Channel\n", ) buf.WriteString("identify \"" + ch + "\"\n") buf.WriteString(src + ”\nend\n") engine := cache.NewEngine() engine.LoadString(buf.String()) mchannel := engine.VM.Class(channelClass, nil) mchannelValue := mchannel.MrbValue(engine.VM) return &MAction{engine: engine, compiled: mchannelValue} }
  34. palkan_tula palkan MRUBY CHANNELS 108 // Perform executes action within

    mruby VM func (m *MAction) Perform(data string) *node.CommandResult { m.mu.Lock() defer m.mu.Unlock() result, _ := m.compiled.Call("perform", mruby.String(data)) decoded := MCallResult{} mruby.Decode(&decoded, result) res := &node.CommandResult{ Transmissions: decoded.Transmissions, StopAllStreams: decoded.StopAllStreams, Streams: decoded.Streams, } return res }
  35. palkan_tula palkan mRuby vs. RPC 109 mRuby #perform call –

    0.05ms RPC #perform call* – 0.7ms (In theory) we can achieve 20k APS** *local connection **action per second
  36. palkan_tula palkan WHAT HAVE WE DONE 111 Moved low-level stuff

    from Ruby to Go and glued them together with gRPC Moved some logic from Ruby to Go to be executed with mRuby
  37. palkan_tula palkan RUBY OR NOT 112 WebSockets in Ruby are

    possible… …but more efficient with the help of other languages …including mRuby
  38. palkan_tula palkan RUBY OR NOT 113 New Rack API could

    help us… …but won’t save us from ourselves …memory problems are likely to not go anywhere …maybe, compacting GC?