Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎

Ruby製のアプリケーションサーバーは、通常nginxのようなWebサーバーと組み合わせて運用されます。ユーザーがRailsアプリケーションのページをリクエストすると、nginxがそのリクエストを受け取り、アプリケーションサーバーへ処理を委譲します。しかし、この仕組みは具体的にどのように実現されているのでしょうか?nginxはどのようにしてUnicornと会話しているのでしょうか?

最も効率的な方法のひとつが「UNIXドメインソケット(UNIXソケット)」を使うことです。本記事では、まずソケットの基本から解説し、最終的には自作のシンプルなアプリケーションサーバーを作成して、それをnginxでプロキシするところまで進めます。

ソケットを使うと、プログラム同士がまるでファイルを読み書きするかのように通信できます。下図の例では、Unicornがソケットを作成して接続を監視し、nginxがそのソケットに接続してUnicornとやり取りします。

UNIXソケットとは何か?

UNIXソケットは、あるプログラムが別のプログラムと、ファイル操作に似た感覚で対話できるようにする仕組みです。いわゆるIPC(Inter-Process Communication:プロセス間通信)の一種です。

ソケット経由でアクセス可能にするには、プログラムはまずソケットを作成し、ファイルと同じようにディスク上に「保存」します。その後、ソケットを監視して着信接続を待ち受けます。接続が確立されると、標準的なIOメソッドを使ってデータの読み書きを行います。

Rubyには、UNIXソケットを扱うために必要な機能がいくつかのクラスとして用意されています。

  • UNIXServer:ソケットを作成してディスクに保存し、新しい接続を監視できます。

  • UNIXSocket:既存のソケットを開いてIO操作を行います。

注意: ソケットには他にも種類があります。代表的なのはTCPソケットですが、本記事ではUNIXソケットのみ扱います。両者の見分け方は簡単です。UNIXソケットにはファイル名が存在します。

最もシンプルなソケット

ここで、小さな2つのプログラムを見てみましょう。

ひとつ目は「サーバー」側です。UnixServerクラスのインスタンスを作成し、server.acceptで接続を待ちます。接続が届くと、あいさつのやり取りを行います。

なお、acceptメソッドとreadlineメソッドは、どちらも期待するものを受け取るまでプログラムの実行をブロック(停止)することに注目してください。

require "socket"

server = UNIXServer.new('/tmp/simple.sock')

puts "==== Waiting for connection"
socket = server.accept

puts "==== Got Request:"
puts socket.readline

puts "==== Sending Response"
socket.write("I read you loud and clear, good buddy!")

socket.close

これでサーバーができました。次に必要なのはクライアントです。

以下の例では、サーバーが作成したソケットを開き、通常のIOメソッドを使ってあいさつの送受信を行います。

require "socket"

socket = UNIXSocket.new('/tmp/simple.sock')

puts "==== Sending"
socket.write("Hello server, can you hear me?\n")

puts "==== Getting Response"
puts socket.readline 

socket.close

動作を確認するには、まずサーバーを実行し、続いてクライアントを実行します。結果は以下のようになります。

Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎 シンプルなUNIXソケットによるクライアント・サーバー通信の例。左がクライアント、右がサーバーです。

nginxとの連携

UNIXソケットの「サーバー」を作る方法がわかったので、nginxとの連携は簡単に実現できます。

信じられないかもしれませんが、実際に試してみましょう。ここでは前述のコードを少し改変し、ソケットから受信したすべてのデータを出力するようにします。

require "socket"

# ソケットを作成し、ファイルシステムに保存する
server = UNIXServer.new('/tmp/socktest.sock')

# 接続(nginxからのアクセス)を待ち受ける
socket = server.accept

# ソケットからすべてのデータを読み込む
while line = socket.readline
  puts line.inspect
end

socket.close

この状態で、nginxに/tmp/socktest.sockへのリクエスト転送を設定すると、nginxがどんなデータを送っているのかを確認できます。(設定方法については後ほど詳しく説明します)

Webリクエストを送ると、nginxは以下のようなデータを私たちの小さなサーバーに送信してきました。

Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎

面白いですね。これは通常のHTTPリクエストに、いくつかの追加ヘッダーが付いただけのものです。これで本格的なアプリケーションサーバーを作る準備が整いました。ただ、その前にnginxの設定について触れておきましょう。

nginxのインストールと設定

開発マシンにまだnginxがインストールされていない場合は、先にインストールしておきましょう。macOSならHomebrewで簡単に導入できます。

brew install nginx

次に、localhost:2048宛てのリクエストを、/tmp/socktest.sockという名前のソケット経由でアップストリームサーバーへ転送するようnginxを設定します。ソケット名に特別な意味はなく、アプリケーションサーバー側で使うソケット名と一致していれば問題ありません。

以下の設定を/tmp/nginx.confとして保存し、nginx -c /tmp/nginx.confコマンドで起動すれば読み込まれます。

# nginxをデーモンではなく通常のコンソールプログラムとして起動する
daemon off;

# エラーログをstdoutに出力する
error_log /dev/stdout info;

events {} # お決まりの記述

http {

  # アクセスログをstdoutに出力する
  access_log /dev/stdout;

  # 私たちのソケット上に存在する外部サーバー@appをnginxに登録する
  upstream app {
    server unix:/tmp/socktest.sock fail_timeout=0;
  }

  server {

    # localhost:2048で接続を受け付ける
    listen 2048;
    server_name localhost;

    # アプリケーションのルートディレクトリ
    root /tmp;

    # ディスク上にパスが存在しない場合、リクエストを@appへ転送する
    try_files $uri/index.html $uri @app;

    # @appに転送されるリクエストに対する設定
    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass https://app;
    }

  }
}

この設定により、nginxはデーモンとしてではなく通常のターミナルアプリのように動作し、すべてのログが標準出力(stdout)に書き出されます。nginxを起動すると、以下のような表示になります。

Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎 非デーモンモードで動作中のnginx。

自作アプリケーションサーバー

nginxと自分のプログラムを接続する方法がわかったところで、シンプルなアプリケーションサーバーの構築はごく簡単な作業になります。nginxがリクエストをソケットへ転送するとき、それは標準的なHTTPリクエストです。少し実験してみると、ソケットが有効なHTTPレスポンスを返せば、それがブラウザに表示されることがわかりました。

以下のアプリケーションは、任意のリクエストに対してタイムスタンプを表示します。

require "socket"

# Connectionはソケットを作成し、新しい接続を受け付ける
class Connection

  attr_accessor :path

  def initialize(path:)
    @path = path
    File.unlink(path) if File.exists?(path)
  end

  def server
    @server ||= UNIXServer.new(@path)
  end

  def on_request
    socket = server.accept
    yield(socket)
    socket.close
  end
end


# AppServerは受信リクエストをログに出力し、ビューを返す
class AppServer

  attr_reader :connection
  attr_reader :view

  def initialize(connection:, view:)
    @connection = connection
    @view = view
  end

  def run
    while true
      connection.on_request do |socket|
        while (line = socket.readline) != "\r\n"
          puts line 
        end
        socket.write(view.render)
      end
    end
  end

end

# TimeViewは単純にHTTPレスポンスを提供する
class TimeView
  def render
%[HTTP/1.1 200 OK

The current timestamp is: #{ Time.now.to_i }

]
  end
end


AppServer.new(connection: Connection.new(path: '/tmp/socktest.sock'), view: TimeView.new).run

nginxとこのスクリプトを同時に起動して、ブラウザでlocalhost:2048にアクセスしてみましょう。リクエストは私たちのアプリに送られ、レスポンスはブラウザに描画されます。素晴らしい!

Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎 シンプルなアプリケーションサーバーがHTTPリクエストをSTDOUTに記録しています。

そして、これが私たちの労働の結晶です。ご覧ください!なんと、タイムスタンプが表示されました!

Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎 サーバーが返すタイムスタンプがブラウザに表示されます。


  1. RubyのStructとOpenStructの使い方を徹底解説!値オブジェクトを簡単に作る方法

    Rubyには、関連する属性をまとめて保持する「値オブジェクト」を手軽に作れるStructという組み込みクラスが用意されています。本記事では、Structの基本的な使い方から注意点、そしてOpenStructとの違いまで詳しく解説します。 そもそもStructとは何か? StructはRubyに組み込まれたクラスで、新しいクラスを動的に生成し、値オブジェクト(Value Object)を作成するために使われます。値オブジェクトとは、関連性のある複数の属性をひとまとめにして扱うためのオブジェクトです。 具体例を挙げてみましょう。 2つの座標(xとy)を持つPointというデータを表したい場合、この

  2. Rubyネットワークプログラミング入門!ソケットの基本とTCPサーバーの作り方

    Rubyでオリジナルのネットワーククライアントやサーバーを作りたいと思ったことはありませんか?あるいは、その仕組みを理解したいだけかもしれません。 その場合、必ず「ソケット」と向き合うことになります。 この記事では、Rubyネットワークプログラミングの基礎を学び、Rubyを使って他のサーバーやクライアントと通信を始めるための方法をご紹介します。 ソケットとは何か? ソケットとは、通信チャネルのエンドポイント(終端)のことです。クライアントとサーバーの両方が、このソケットを使って通信を行います。 その仕組みは非常にシンプルです。 接続が確立されると、ソケットにデータを書き込むことでデータが相手側