blog.petitviolet.net

Ruby on Rails on YugabyteDB

2020-07-13

RubyDB

YugabyteDB is “The Leading High-Performance Distributed SQL Database”.

https://www.yugabyte.com/

It has been developed as an open source and called Distributed SQL.
YugabyteDB is a Distributed SQL product, like Google Cloud Spanner which is a well-known global distributed databased developed by Google, but it has PostgreSQL compatibile interface, not like Spanner, we can connect to YugabyteDB via PostgreSQL driver. In addition to PostgreSQL interface, YugabyteDB also provides Redis interface called Yedis. Ruby on Rails applications usually depend on Redis as its cache layer and backgound workers storage, such as Sidekiq.

I assumed using YugabyteDB as a backend storage layer of Ruby on Rails application perhaps is a great way to manage storages into one place.
This post describes how to get started and how to use YugabyteDB as backend of Ruby on Rails applications.
Example source codes are available at petitviolet/yuga-rails in GitHub.

Getting Started with YugabyteDB

Official document describes how to launch YugabyteDB instances using docker-compose.

https://docs.yugabyte.com/latest/deploy/docker/docker-compose/

Copy and paste it to local docker-compose.yaml, then launch docker containers by docker-compose up -d as written in the document.

$ docker-compose up -d
$ docker-compose ps
    Name                   Command               State                                                                Ports
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
yb-master-n1    /home/yugabyte/bin/yb-mast ...   Up      10100/tcp, 11000/tcp, 12000/tcp, 5433/tcp, 6379/tcp, 0.0.0.0:7000->7000/tcp, 7100/tcp, 7200/tcp, 9000/tcp, 9042/tcp, 9100/tcp
yb-tserver-n1   /home/yugabyte/bin/yb-tser ...   Up      10100/tcp, 11000/tcp, 12000/tcp, 0.0.0.0:5433->5433/tcp, 0.0.0.0:6380->6379/tcp, 7000/tcp, 7100/tcp, 7200/tcp,

Initial Configurations

As a preparation for the following sections, update credentials using ysqlsh in yb-tserver-n1 container.

$ docker exec -it yb-tserver-n1 /home/yugabyte/bin/ysqlsh -h yb-tserver-n1
ysqlsh (11.2-YB-2.0.5.7-b0)
Type "help" for help.
yugabyte=# alter role yugabyte with password 'password';
ALTER ROLE

Then, we can connect to the YugabyteDB instance just as connecting to PostgreSQL.

$ psql -Uyugabyte -h127.0.0.1 -p5433 -W
Password:
psql (12.3, server 11.2-YB-2.0.5.7-b0)
Type "help" for help.

In order to use YugabyteDB as backend database of a Rails application, create a database for the app in advance.

yugabyte=# create database yuga_rails_development;
CREATE DATABASE

yugabyte=# \l
                                      List of databases
          Name          |  Owner   | Encoding | Collate |    Ctype    |   Access privileges
------------------------+----------+----------+---------+-------------+-----------------------
 postgres               | postgres | UTF8     | C       | en_US.UTF-8 |
 system_platform        | postgres | UTF8     | C       | en_US.UTF-8 |
 template0              | postgres | UTF8     | C       | en_US.UTF-8 | =c/postgres          +
                        |          |          |         |             | postgres=CTc/postgres
 template1              | postgres | UTF8     | C       | en_US.UTF-8 | =c/postgres          +
                        |          |          |         |             | postgres=CTc/postgres
 yuga_rails_development | yugabyte | UTF8     | C       | en_US.UTF-8 |
 yugabyte               | postgres | UTF8     | C       | en_US.UTF-8 |
(6 rows)

yugabyte=# \c yuga_rails_development
Password for user yugabyte:
psql (12.3, server 11.2-YB-2.0.5.7-b0)
You are now connected to database "yuga_rails_development" as user "yugabyte".
yuga_rails_development=# \d
Did not find any relations.

Now, it’s ready to develop an application depending on the YugabyteDB schema. Setting user and password to environmental value will reduce our future typing efforts :)

$ export PGUSER=yugabyte
$ export PGPASSWORD=password

Initialize Ruby on Rails application

Create a new Rails app by using rails new command.

$ ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]

$ rails -v
Rails 6.0.3.2

$ rails new yuga-rails --database=postgresql --api
...

After creating a new app, edit config/database.yml to configure connection settings to YugabyteDB.

./config/database.yml
default: &default
    adapter: postgresql

development:
    <<: *default
    database: yuga_rails_development
    username: <%= ENV.fetch("PGUSER") %>
    password: <%= ENV.fetch("PGPASSWORD") %>
    host: 127.0.0.1
    port: 5433

No need to add additional database drivers, configurations, etc. to use YugabyteDB. It’s ready to use YugabyteDB as a database of the Rails app!

Manipulate YugabyteDB data through Rails app

Defining Model using rails g command.

$ bundle exec rails g model user email:string name:string gender:integer
...
$ vim ./db/migrate/20200621050634_create_users.rb # edit
./db/migrate/20200621050634_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
def change
    create_table :users do |t|
    t.string :email, null: false, index: { unique: true }
    t.string :name, null: false
    t.integer :gender, null: false

    t.timestamps
    end
end
end

Then, apply database migration.

$ bundle exec rails db:migrate
== 20200621050634 CreateUsers: migrating ======================================
-- create_table(:users)
-> 3.6016s
== 20200621050634 CreateUsers: migrated (3.6016s) =============================

Launch Rails console to play with the app.

$ bundle exec rails c
Running via Spring preloader in process 20732
Loading development environment (Rails 6.0.3.2)
irb(main):001:0> User.all
  User Load (11.4ms)  SELECT "users".* FROM "users" LIMIT $1  [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>

irb(main):002:0> 30.times.each { |i| User.create!(email: "user-#{i}@example.com", name: "user-#{i}", gender: i % 3) }
...
=> 30

irb(main):003:0> User.count
   (23.2ms)  SELECT COUNT(*) FROM "users"
=> 30

irb(main):004:0> User.first
  User Load (13.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">

It’s working expectedly, though it seems slow than expected.

Using Yedis as a Cache of Rails

To enable Redis API of YugabyteDB which called Yedis API, run this command that described in the document.

$ docker exec -it yb-master-n1 /home/yugabyte/bin/yb-admin --master_addresses yb-master-n1:7100 setup_redis_table

I0623 13:42:41.866884    45 mem_tracker.cc:250] MemTracker: hard memory limit is 1.651742 GB
I0623 13:42:41.867004    45 mem_tracker.cc:252] MemTracker: soft memory limit is 1.403981 GB
I0623 13:42:41.945003    45 table_creator.cc:261] Created table system_redis.redis of type REDIS_TABLE_TYPE
I0623 13:42:41.945096    45 yb-admin_client.cc:465] Table 'system_redis.redis' created.

$ redis-cli -p 6379 # can connect to Yedis from redis-cli
127.0.0.1:6379> set key value
OK
127.0.0.1:6379> get key
"value"
127.0.0.1:6379> del key
(integer) 1
127.0.0.1:6379> get key
(nil)

Now we can use YugabyteDB as Redis. Update Rails configuration to enable to use Redis as its cache store.

  • Install necessary gems to use Redis.

    Gemfile
    +gem 'redis', '~> 4.0'
    +gem 'redis-rails'
  • Update config file to specify :redis_store as cache_store in Rails

    ./config/environments/development.rb
    +  config.cache_store = :redis_store, 'redis://localhost:6379/0/yuga_rails_cache', { expires_in: 30.seconds }
    +  config.active_record.cache_versioning = false

After these procedures, it’s able to use Rails.cache like:

$ bundle exec rails c
Running via Spring preloader in process 68202
Loading development environment (Rails 6.0.3.2)
irb(main):001:0> Rails.cache.class
=> ActiveSupport::Cache::RedisStore
irb(main):002:0> Rails.cache.fetch("User.first") { User.first }
  User Load (32.9ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
irb(main):003:0> Rails.cache.fetch("User.first") { User.first }
=> #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
irb(main):004:0> Rails.cache.read("User.first")
=> #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
irb(main):005:0> sleep 30
=> 30
irb(main):006:0> Rails.cache.read("User.first")
=> nil

Yay!

Summary

YugabyteDB offers both PostgreSQL and Redis interfaces which can be used as backend database layers of Ruby on Rails applications. Thus, it frees us from the hell of “polyglot” databases. In addition to these, it also offers Cassandra API called Ycql so that it’s easy to use key-value store if needed. However, the performance of YugabyteDB looks slower than using PostgreSQL, so it seems mandatory to evaluate its performance before using it in production environments.