Mildly Internet
April 4, 2016 | code

Supercharge Resque and Sidekiq With Go

Process background job queues faster by offloading jobs to Go. Take advantage of Go’s speed and low memory usage to replace slow or IO bound Ruby Workers.

Certain types of jobs can be very computation heavy - calculating the product of large matrices, solving puzzles (Sudoku), etc. Or be very simple but occur very often. For example on every page load you might create a job to post the user data and url to a third party analytics service (this might happen a billion+ times a month).

If you are using Resque it will fork off processes similar in memory size to your running application for each worker. Some large or old Rails apps have a tendency to bloat well past 500MB which makes having many workers expensive. By moving these types of jobs to Go, I free up my Ruby workers to do more meaningful work related to my application.

Setup Sidekiq

To keep things simple for this example I will run the Sidekiq UI from Rack instead of Rails. To get the Sidekiq UI booted create a config.ru file with following contents:

require "sidekiq"
require "sidekiq/web"

Sidekiq.configure_client do |config|
  config.redis = { db: 1 }
end

Sidekiq.configure_server do |config|
  config.redis = { db: 1 }
end

run Sidekiq::Web

To start the UI on port 4001 (can be any port) run:

rackup config.ru -p 4001

Jobs need to be pushed onto a queue that the Go Workers will consume. The normal Sidekiq API for pushing jobs is MyRubyWorker.perform_async(args). The worker lives in the Go code so that class does not exist within the scope of our Ruby program. However, Sidekiq does provide an API for manually pushing jobs onto a queue. Run this in an irb session:

Sidekiq::Client.push(
  "queue" => "go_queue",
  "class" => "GoWorker",
  "args"  => ["I am Running"]
)

Wrap that in a 1000.times do block to quickly enqueue a large batch of jobs.

Go Worker

To create our Go Workers we’ll use the excellent Go Workers library. In the main function the workers connect to Redis and are configured to pull from a particular queue. The function name for the worker must match the "class" argument we supplied earlier.

package main

import (
    "fmt"
    "github.com/jrallison/go-workers"
)

func GoWorker(message *workers.Msg) {
    fmt.Println(message)
}

func main() {
    workers.Configure(map[string]string{
        // location of redis instance
        "server": "localhost:6379",
        // instance of the database
        "database": "1",
        // number of connections to keep open with redis
        "pool": "10",
        // unique process id for this instance of workers (for proper recovery of inprogress jobs on crash)
        "process": "1",
    })

    // pull messages from "go_queue" with concurrency of 10
    workers.Process("go_queue", GoWorker, 10)

    // Blocks until process is told to exit via unix signal
    workers.Run()
}

Once that program starts running it will process any jobs we push onto the go_queue. They will be processed extremely quickly while using minimal system resources. Concurrency can easily be bumped up to several thousand which will greatly out perform Sidekiq or Resque.

In Part 2 we’ll take a look at some more advanced examples.