ActiveRecordのestablish_connectionを読む

ActiveRecord における DB との接続確立方法をきちんと理解できてなかったので、pry-byebug を使いながらコードを読み解いてみる。

Railsのversion は 4.2.0 という前提で。

establish_connection

blog.livedoor.jp

sonots さん解説の通り、Rails を起動すると establish_connection メソッドが呼び出される。

# lib/active_record/connection_handling.rb

    def establish_connection(spec = nil)
      spec     ||= DEFAULT_ENV.call.to_sym
      resolver =   ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
      spec     =   resolver.spec(spec)

      unless respond_to?(spec.adapter_method)
        raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
      end

      remove_connection
      connection_handler.establish_connection self, spec
    end

binding.pry でデバッグ

適当な箇所に binding.pry を挿入して rails server するとコードを追いかけやすくてよい。

self は何?

[12] pry(ActiveRecord::Base)> self
=> ActiveRecord::Base

なぜ self が ActiveRecord::Base になるかというと、ConnectionHandling モジュールを extendしてるから。 https://github.com/rails/rails/blob/v4.2.0/activerecord/lib/active_record/base.rb#L276

spec とは?

[11] pry(ActiveRecord::Base)> p spec
:development

resolver.spec(spec) の結果は?

[1] pry(ActiveRecord::Base):1> resolver.spec(spec)
=> #<ActiveRecord::ConnectionAdapters::ConnectionSpecification:0x007fa52311ae20
 @adapter_method="mysql2_connection",
 @config={:adapter=>"mysql2", :encoding=>"utf8mb4", :database=>"development", :pool=>5, :username=>"root", :password=>nil, :socket=>"/tmp/mysql.sock"}>

connection_handler とは?

def self.connection_handler
  ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
end

def self.connection_handler=(handler)
  ActiveRecord::RuntimeRegistry.connection_handler = handler
end

self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new

https://github.com/rails/rails/blob/v4.2.0/activerecord/lib/active_record/core.rb#L100-L102

により、ActiveRecord::ConnectionAdapters::ConnectionHandler のインスタンスということになる。

そして...

# lib/active_record/connection_handling.rb

    def establish_connection(spec = nil)
      spec     ||= DEFAULT_ENV.call.to_sym
      resolver =   ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
      spec     =   resolver.spec(spec)

      unless respond_to?(spec.adapter_method)
        raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
      end

      remove_connection
      connection_handler.establish_connection self, spec
    end

の最後の一行は

# lib/active_record/connection_adapters/abstract/connection_pool.rb
def establish_connection(owner, spec)
  @class_to_pool.clear
  raise RuntimeError, "Anonymous class is not allowed." unless owner.name
  owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
end

lib/active_record/connection_adapters/abstract/connection_pool.rb に行き着く。

実は owner_to_pool に instance を代入しているだけで接続していない。

いつ接続するのか?

[Ruby] 例えば、ActiveRecord の connection_pool を止める - sonots:blog

同じく sonots さんの解説の通り、クエリが投げられる時に connection pool から取得 or 新規接続という感じになっている。

#connection

#checkout

#acquire_connection

#checkout_new_connection

#new_connection

という流れで、

def new_connection
  Base.send(spec.adapter_method, spec.config)
end

がコネクションを貼ってる箇所になり、spec.adapter_method を send で呼び出すことになる。

[10] pry(ActiveRecord::Base)> p spec.adapter_method
=> "mysql2_connection"

まとめ

establish_connectionでは接続確立せず、実際はクエリを投げるタイミングである。

また、spec.adapter_methodには 例えば "mysql2_connection" などが格納されており、これを動的に実行することでconfig/database.yml に書かれた DB に接続する。

ブロードキャストなICMP Echo Requestを無視する

OSはUbuntu 14.04。

結論から書くと、

echo "net.ipv4.icmp_echo_ignore_broadcasts=1" > /etc/sysctl.d/60-icmp-echo.conf

service procps start

でOK(のはず)。

設定前

$ ping 192.168.33.255
PING 192.168.33.255 (192.168.33.255): 56 data bytes
64 bytes from 192.168.33.1: icmp_seq=0 ttl=64 time=0.081 ms
64 bytes from 192.168.33.20: icmp_seq=0 ttl=64 time=0.395 ms
64 bytes from 192.168.33.1: icmp_seq=1 ttl=64 time=0.125 ms
64 bytes from 192.168.33.20: icmp_seq=1 ttl=64 time=0.665 ms

設定後

$ ping 192.168.33.255
PING 192.168.33.255 (192.168.33.255): 56 data bytes
64 bytes from 192.168.33.1: icmp_seq=0 ttl=64 time=0.081 ms
64 bytes from 192.168.33.1: icmp_seq=1 ttl=64 time=0.134 ms

以下、調べたことをメモしておく。

/etc/init/procps.conf

$ cat /etc/init/procps.conf
# procps - set sysctls from /etc/sysctl.conf
#
# This task sets kernel sysctl variables from /etc/sysctl.conf and
# /etc/sysctl.d

snip...

task
script
    cat /etc/sysctl.d/*.conf /etc/sysctl.conf | sysctl -e -p -
end script

このタスクにより、起動時に/etc/sysctl.d/ と /etc/sysctl.conf が適用される。

デーモン化するわけじゃないことに注意。単に task として sysctl コマンドにパイプしてるつくりなので。

/etc/sysctl.d/

README曰く..

This directory contains settings similar to those found in /etc/sysctl.conf.
In general, files in the 10-*.conf range come from the procps package and
serve as system defaults.  Other packages install their files in the
30-*.conf range, to override system defaults.  End-users can use 60-*.conf
and above, or use /etc/sysctl.conf directly, which overrides anything in
this directory.

なので、今回の設定は /etc/sysctl.d/60-icmp-echo.conf にしておいた。

White Paper : いまさら聞けない、SSL サーバ証明書とルート証明書の関係 - Symantec

分かりやすくまとまってるので、ふと忘れてしまった時に。 https://www.jp.websecurity.symantec.com/welcome/pdf/wp_sslandroot-certificate.pdf

ついでに CSR についてもメモっておく。

CSRとは、お客様が生成し、認証局に提出する署名リクエスト(Certificate Signing Request)です。ジオトラストのSSLサーバ証明書の申請には、SSLサーバ証明書を導入する環境で生成するCSRが必要になります。CSRには、お客様の公開鍵の情報の他、生成時に指定する情報(ディスティングイッシュネーム)が含まれます。 ジオトラストのSSLサーバ証明書は、認証の後、お客様の公開鍵に署名をした上でSSL証明書として発行しています。

https://www.geotrust.co.jp/support/ssl/csr/

Ubuntuにおけるinitと起動方法

sysvinit, upstart, systemd, update-rc.d, insserv の関係性・歴史的背景がよくわからなくていろいろ調べたので、メモしておく。

基礎知識

sysvinit とは?

sysvinitはSystemV initの略で,UNIX SystemV(システムファイブ)と呼ばれるAT&T社謹製の古典的なUNIXが採用した起動メカニズムと同じ動作をするように設計されたソフトウェアです。

出典: http://gihyo.jp/dev/serial/01/sc-literacy/0013

upstart とは?

Upstart is an event-based replacement for the /sbin/init daemon which handles starting of tasks and services during boot, stopping them during shutdown and supervising them while the system is running.

It was originally developed for the Ubuntu distribution, but is intended to be suitable for deployment in all Linux distributions as a replacement for the venerable System-V init.

出典: http://upstart.ubuntu.com/

systemd とは?

systemd is a suite of basic building blocks for a Linux system. It provides a system and service manager that runs as PID 1 and starts the rest of the system.

出典: http://www.freedesktop.org/wiki/Software/systemd/

これらはいずれも PID 1として動作し、OS起動時に諸々のプロセスを立ち上げる役割を担う。

実際にどう使われるのかを理解するのにはinitとプロセス再起動 - slideshare が役に立った。

update-rc.d とは?

update-rc.d updates the System V style init script links /etc/rcrunlevel.d/NNname whose target is the script /etc/init.d/name

出典: man update-rc.d(8)

insserv とは?

insserv is a low level tool used by update-rc.d which enables an installed system init script (`boot script') by reading the comment header of the script

出典: man insserv(8)

initの調べ方

sysvinit, upstart, systemd のうちどれが使われているか調べてみた。OS は Ubuntu 14.04.2。

root@ubuntu-14:~# dpkg -S /sbin/init
upstart: /sbin/init
root@ubuntu-14:~# /sbin/init --version
init (upstart 1.12.1)
Copyright (C) 2006-2014 Canonical Ltd., 2011 Scott James Remnant

This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Ubuntu 6.10 から upstart が採用されたと https://help.ubuntu.com/community/UbuntuBootupHowto に書かれてたけど、確かにその通り。

起動方法

http://heartbeats.jp/hbblog/2013/06/service-start-stop.html によれば service コマンドを使って起動するのがオススメの様子。

man service(8)

service runs a System V init script or upstart job in as predictable an environment as possible, removing most environment variables and with the current working directory set to /.

The SCRIPT parameter specifies a System V init script, located in /etc/init.d/SCRIPT, or the name of an upstart job in /etc/init. The existence of an upstart job of the same name as a script in /etc/init.d will cause the upstart job to take precedence over the init.d script.

実行ユーザに依存しない環境変数で起動でき、sysvinit と upstart のどちらにも対応ということらしい。

# /usr/sbin/service より一部抜粋

if [ -r "/etc/init/${SERVICE}.conf" ] && which initctl >/dev/null \
   && initctl version | grep -q upstart
then
   # Upstart configuration exists for this job and we're running on upstart
   case "${ACTION}" in
      start|stop|status|reload)
         # Action is a valid upstart action
         exec ${ACTION} ${SERVICE} ${OPTIONS}
      ;;
      restart)
         # Map restart to the usual sysvinit behavior.
         stop ${SERVICE} ${OPTIONS} || :
         exec start ${SERVICE} ${OPTIONS}
      ;;
      force-reload)
         # Upstart just uses reload for force-reload
         exec reload ${SERVICE} ${OPTIONS}
      ;;
   esac
fi

# Otherwise, use the traditional sysvinit
if [ -x "${SERVICEDIR}/${SERVICE}" ]; then
   exec env -i LANG="$LANG" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" ${ACTION} ${OPTIONS}
else
   echo "${SERVICE}: unrecognized service" >&2
   exit 1
fi

コードを眺めると、upstart job があればそれを優先するようになってる。

upstart を使ってみる

heartbeats.jp

を参考に、カンタンな設定を書いてみる。

"hello" を /var/log/hello に書き出し、10秒 sleep するというもの。

# /etc/init/hello.conf
description "hello"
author  "kotaroito"

start on runlevel [2345]
stop on runlevel [016]

exec /opt/ruby/bin/ruby -e "puts 'hello'; sleep 10" >> /var/log/hello 2>&1

respawn
respawn limit 10 5
$ sudo service hello start

で起動する。

まとめ

  • Ubuntu 6.10 以降は upstart が標準 となった
  • 設定ファイルは /etc/init/*.conf で runlevel も書ける
  • 手動でプロセスを上げる時は service コマンドを使うとよい

Mac + VirtualBox + Vagrant で MySQLのレプリケーション環境構築

MySQLレプリケーションを手軽に試す環境が欲しいなぁと思い、Vagrant を使って複数のGuest OSを立ち上げてみることにした。

Environments

Host

Guest

マスター・スレーブ 1台ずつで。

Installation

VirtualBoxVagrant

それぞれ、Downloads – Oracle VM VirtualBoxDownload - Vagrant by HashiCorp から ダウンロードしてインストールすれば OK。

Guest OS

Guest OS は 今回は Ubuntu を使ってみることにする。適当な box を拾ってきてもいいが、せっかくなので Veewee で 一から作成する。

Ryuzeeさんの記事を参考にさせてもらいました。 www.ryuzee.com

veeweeをインストールする。

mkdir sandbox && git clone https://github.com/jedi4ever/veewee.git
cd veewee

bundle install

ISOイメージをダウンロードし、boxをbuildする。

bundle exec veewee vbox templates | grep ubuntu
veewee vbox define 'ubuntu-14.04-server-amd64' 'ubuntu-14.04-server-amd64' --workdir=/Users/kotaroito/sandbox/veewee

curl -LO http://releases.ubuntu.com/14.04/ubuntu-14.04.2-server-amd64.iso
mv ubuntu-14.04.2-server-amd64.iso iso/
veewee vbox build 'ubuntu-14.04-server-amd64'

boxをexportし、vagrantに追加する。

veewee vbox export 'ubuntu-14.04-server-amd64'
vagrant box add 'ubuntu-14.04-server-amd64' '/Users/kotaroito/sandbox/veewee/ubuntu-14.04-server-amd64.box'

Configuration

Vagrant

blog.hello-world.jp.net を参考にさせてもらった。

master と slave としてそれぞれ1つずつOSを立ち上げる。 ネットワーク設定は vagrantのネットワークについて - Qiita が分かりやすい。

Vagrant.configure(2) do |config|
  config.vm.box = "ubuntu-14.04-server-amd64"

  config.vm.define :db_master do |node|
    node.vm.network "private_network", ip: "192.168.33.10"
  end

  config.vm.define :db_slave do |node|
    node.vm.network "private_network", ip: "192.168.33.11"
  end

かんたんにsshできるように、.ssh/config に登録しておく。

vagrant ssh-config db_master --host vagrantdbm >> ~/.ssh/config
vagrant ssh-config db_slave  --host vagrantdbs >> ~/.ssh/config

Guest OS(ここでは db_master と db_slave) を立ち上げる。

vagrant up

ssh vagrantdbm して、db_master に接続して、ネットワークをチェックする。

$ ifconfig 
eth1      Link encap:Ethernet  HWaddr 08:00:27:c3:5a:0e
          inet addr:192.168.33.10  Bcast:192.168.33.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fec3:5a0e/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:153 errors:0 dropped:0 overruns:0 frame:0
          TX packets:106 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:21300 (21.3 KB)  TX bytes:19440 (19.4 KB)
          
$ ping 192.168.33.11
PING 192.168.33.11 (192.168.33.11) 56(84) bytes of data.
64 bytes from 192.168.33.11: icmp_seq=1 ttl=64 time=0.600 ms
64 bytes from 192.168.33.11: icmp_seq=2 ttl=64 time=0.485 ms

MySQL

同じく http://blog.hello-world.jp.net/centos/556/ が詳しい。Ubuntu での手順を記しておく。

Master DB

mysql-serverをインストールする。

apt-get update
apt-get install mysql-server 

バイナリログを出力するよう設定する。

[mysqld]
server-id = 1
log_bin
bind-address = 0.0.0.0

レプリケーション権限を持つユーザ(repl)を設定する。

echo "GRANT REPLICATION SLAVE ON *.* TO repl@192.168.33.11 IDENTIFIED BY 'repl';" | mysql -uroot

my.cnfの設定を有効にするため、mysqld を再起動する。

service mysql restart

Slave DB

mysql-serverをインストールする。

apt-get update
apt-get install mysql-server 

バイナリログを出力するよう設定する。

[mysqld]
server-id       = 2

my.cnfの設定を有効にするため、mysqld を再起動する。

service mysql restart

Replication

いよいよレプリケーションを試してみる。

まずは、マスターのポジションを確認する。 (クエリは自分以外には発行していない前提で)

mysql> SHOW MASTER STATUS \G
*************************** 1. row ***************************
            File: mysqld-bin.000003
        Position: 107
    Binlog_Do_DB:
Binlog_Ignore_DB:

次にスレーブでレプリケーションを設定する。

CHANGE MASTER TO
       MASTER_HOST='192.168.33.10',
       MASTER_USER='repl',
       MASTER_PASSWORD='repl',
       MASTER_LOG_FILE='mysqld-bin.000003',
       MASTER_LOG_POS=107;
       
START SLAVE;

レプリケーションの状態を確認する。

mysql> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.33.10
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysqld-bin.000003
          Read_Master_Log_Pos: 107
               Relay_Log_File: mysqld-relay-bin.000004
                Relay_Log_Pos: 254
        Relay_Master_Log_File: mysqld-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:

Slave_IO_Running / Slave_SQL_Running が Yes なら大丈夫。

ハマりポイント

  1. バイナリーログのファイル名 当たり前だが、間違ってると動作しない。

  2. bind-address デフォルトで 127.0.0.1 になってたりするので注意。

  3. iptables 弾かれていないことを確認すべし。

確認

実際にレプリケーションされることを確認すべく、Master DB にデータを流し込む。 http://dev.mysql.com/doc/index-other.html の “world database” を使ってみる。

curl -LO http://downloads.mysql.com/docs/world.sql.gz
gzip -d world.sql.gz
mysql -uroot < world.sql

Slave DB で world データベースが見えれば、成功!

mysql> use world;
Database changed
mysql> show tables;
+-----------------+
| Tables_in_world |
+-----------------+
| City            |
| Country         |
| CountryLanguage |
+-----------------+

railsのルーティングエラーを捕捉する

特に何も設定せずとも routes.rb にマッチしなかったリクエストは rails がエラーにしてくれますが、ログに書き出す等の特別な処理を差し込みたい時にはルーティングエラーを捕捉する必要があります。

少々苦戦したので、忘れないようにメモしておきます。 環境はRails 4.2.1ですが、4系なら同じ振る舞いになるはず。たぶん。

routes.rb

末尾に以下の行を追加すると、マッチしなかった全GETリクエストを raise_not_found! アクションで拾えるようになります。

get  '*unmatched_route', to: 'application#raise_not_found!', format: false

application_controller.rb

application_controller は例えば以下のような感じで。

  def raise_not_found!
    e = ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
    render_404(e)
  end

  private

  def render_404(e)
    logger.warn e
    render file: "public/404.html", status: :not_found, layout: false
  end