お久しぶりです、スクレイピングエンジニアのSakaeです。

弊社の自慢であるクローリングを簡単に説明すると、Web上の公開データをスクレイピングで集めてくることと言えます。が、このクローリング技術を応用することで、普段人間がポチポチWebページのボタンを押して行うような単純作業を、クローラーに代わりにポチポチさせて自動化することが可能になります。そして、このホワイトカラー業務の自動化は、巷ではRPA(Robotic Process Automation / ロボティック・プロセス・オートメーション)と呼ばれています。

弊社でRPAをご提供する場合、クライアント様のシステムと弊社のRPA(クローラー)を連携させるインターフェースとして、Web APIを利用することが多いです。クライアント様のシステムからWeb APIへのリクエストという形で、RPAクローラーへの指示出し(ジョブのキック)を行います。

このようにWeb APIをご提供する機会も多いので、API構築のためのプロダクト選びもいろいろ試行錯誤をしています。Ruby on RailsにもAPIモードという機能が提供されていますが、正直そこまで高機能は求めておらず、

  • 要件に合わせて柔軟に簡単にスクラッチでAPIの機能が提供できて
  • クライアントのシステムから発行される、そこそこのリクエスト数にも耐えられ
  • 本格的なWeb層-API層(アプリケーション層)の2層に別れたインフラを構築しなくても、1台のそこそこのスペックのサーバーで、Web-API相乗りの環境を安定稼働できるような省CPU・省メモリ

という必要十分な条件を早く作れることが重要になります。

その要件を満たす構成として、標題にもあるSinatra x Puma x Nginxの組み合わせを試していますが、Rubyと言えばRailsに世の中が寄っているためか、Sinatra x Puma x Nginxの組み合わせの情報がググっても少なかったので、今回はこの構成の環境構築についてご紹介させていただきたいと思います。

Railsとは異なるSinatraの良いところ

Ruby on Rails全盛な世の中ですが、Sinatraにも良いところがたくさんあります。

  • とにかく学習コストが低いところ
  • メモリ使用量が少なく軽量なところ
  • 必要十分な機能が揃っているところ

というように、ちょうどよい使い勝手がSinatraの良いところかなと思います。JavaScript界隈でいう、AngularでもReactでもないVue.jsのような立ち位置かと思います。

使い方は公式ページのメインにもあるとおり、下記のようにたったこれだけの手順、そしてソースは4行でapiサーバーとして機能するのがすごいところですね。。

$ gem install sinatra
$ cat myapp
require 'sinatra'
get '/frank-says' do
  'Put this in your pipe & smoke it!'
end
$ruby myapp.rb

Sinatra x Puma x Nginx構成のAPI環境構築方法

以下本題のSinatraにPumaとNginxを組み合わせる環境構築方法に入りますが、最初にまず、なぜSinatraの話なのでPumaやらNginxが必要なのかについてお話したいと思います。

Sinatra x Pumaで同時リクエスト処理のパフォーマンスをあげる

Sinatraはリクエストを1つづつ順番にしか処理(レスポンス)できません。リクエスト後にDBにQueryを送り、レスポンスに時間がかかるような処理の合間に別のリクエストがきたら処理できないという弱点があります。

その弱点を埋めるのがPumaです。PumaはWebサーバーだ!いや、Pumaはアプリケーションサーバーだ!といろいろな情報がありますが、Pumaの使いどころ・構成によりWebサーバーでもあり、アプリケーションサーバーにもなり得るという言い方が良いのではないかと思います。ユーザーからのリクエストを直接受け取るのがWebサーバーという定義でいけば、今回の構成のPumaはアプリケーションサーバーということができます。

PumaはAPIのリクエストをSinatraの代理で受けとり、そして、内部的にSinatraを複数「スレッド」で立ち上げ、処理をしていない空いているSinatra(スレッド)にリクエストを処理させるというコントロールをしてくれます。(Unicornも似た機能を提供してくれますが、Sinatraを複数「プロセス」で立ち上げるのがPumaとの違いです。)

これにより本来1つづつシーケンシャルにしかリクエストをさばくことができないSinatraが、同時に複数来るAPIへのリクエストをさばくことができるようになり、同時アクセスに対するパフォーマンス向上が期待できます。

味があると定評の手書きイラストで表すとこういう感じでしょうか。

puma-to-sinatra

Sinatra x Puma x Nginxでより柔軟にリクエストをさばく

Sinatraを複数立ち上げてくれるPumaの更に手前(ブラウザ寄り)に、NginxやApacheなどのWebサーバーを置くことで、さらに柔軟にユーザーからのリクエストを処理することができます。そして今回の構成ではNginxをWebサーバー(ユーザーのリクエストを直接受け取るサーバー)として使用します。

現代のWebブラウジングでは、1つのWebページを開くだけで、非常にたくさんのファイルがブラウザにダウンロードされます。例えばHTML、CSS、JavaScript、画像/動画ファイル、フォントファイル、などなど、本当にたくさんです。そのDLされるファイル群の一つとして、API(Sinatra)が返すJSONなどのデータがあります。

RailsやSinatraはビュー(View:Webページの見た目)も担当する場合はHTMLなども動的に生成して処理しますが、Web API的にリクエストに応じてデータを返すだけの場合は、HTMLすら処理する必要はありません。(ごく単純化すると、リクエストを受けてDBのデータを加工してJSONにして返すだけということです。)そうした場合、PumaはそもそもHTMLやCSS、JS、画像などのリクエストを受け取る必要がありません。

そこでWebサーバーであるNginxの出番です。Nginxはユーザーのリクエストを見て、HTML、CSS、JSなどのアセット類はNginxが自分でユーザーにレスポンスします。そして、APIへのリクエストが来たときにだけ、「APIへのリクエストが来たのでPumaさんが処理してね」とPumaに処理をプロキシします。そしてPumaは空いてるSinatraスレッドに処理をさせ、Pumaは出来上がったレスポンスをNginxに返し、最終的にNginxがユーザーにSinatraが生成したレスポンスデータを返します。

というように、最前列にNginxなどのWebサーバーを置くことで、PumaとSinatraを本来の仕事のみに集中させることがSinatra x Puma x Nginxという構成の目的になります。

味があると定評の手書きイラストで再び表すと、たぶんこういう感じでしょうか。

Sinatra x Puma x Nginxの構成方法

肝心の構築方法ですが、ググって頂くとNginx、Puma、Sinatraのそれぞれの導入方法は大量に出てきますので、この記事ではそれらを1つのシステムとして結びつける、最もシンプルな設定に絞ってご説明します。

各設定ファイルの構造

本構成に登場する各種設定ファイル群は以下のような構成になります。

. PJ root
│
├── Gemfile
├── Gemfile.lock
├── config
│   └── puma.rb
├── config.ru
├── log
│   ├── development.log
│   └── production.log
├── tmp
│   └── puma
│       └── state
│       └── pid
└── app.rb

/
└── etc
    └── nginx
        └── conf.d
            └──http.conf

Sinatraアプリケーション設定

Sinatraは/helloに対して返答するだけのシンプルなものです。configureでpumaをリクエストを受け取るサーバーとして利用するように指定しています。(デフォルトだとWEBrickが起動するようです)

おしりにあるrun! if app_file == $0はSinatra公式ページにも説明がありますが、ruby app.rbのように直接ファイル指定で呼び出された場合でもWebサーバー(Puma)を起動するための設定です。

Gemfile
source "https://rubygems.org"

gem "puma"
gem "sinatra"
app.rb
require 'sinatra'

configure {
  set :server, :puma
}

class Pumatra < Sinatra::Base
  get '/hello' do
    return 'hello pumatra!'
  end

  run! if app_file == $0
end

Puma設定

後述でPumaとNginxを接続しますが、Puma単体でのデバッグなどがしやすいのでunixドメインソケットでの接続ではなく、TCPの7890ポート(ポート番号は一例です)へのプロキシでnginxからPumaにリクエストを転送します。

この方法ならnginx経由からのPumaアクセスという閲覧方法と、「http://xxxx.com:7890」にアクセスすることでnginxを経由せずにPuma単体へアクセスする閲覧方法の両方が共存可能です。なにか問題が起きた際にNginxとPumaの障害点の切り分けに役立ちます。

また、最後にあるactivate_control_appを指定しておくと、pumactlコマンド(後述)を使ってPumaの起動・停止・状態確認などが行えます。

スレッド数は4〜8スレッドの間で自動調整するようにしています。

config/puma.rb
root = "#{Dir.getwd}"

bind "tcp://0.0.0.0:7890"
pidfile "#{root}/tmp/puma/pid"
state_path "#{root}/tmp/puma/state"
rackup "#{root}/config.ru"
threads 4, 8
activate_control_app

Rack設定

Sinatra上で定義したPumatraクラスをrun(実行)しています。

config.ru
require './app'
run Pumatra

Nginx設定

Nginxの設定は以下のようになります。

ポイントは先の説明の通り、Pumaへの接続をunixドメインソケットではなく、Pumaの待受ポートである7890にプロキシすることでPumaへの接続を行っているところです。パフォーマンスよりも、運用のしやすさを重視しているつもりです。

Nginx.conf
server {
  listen  80;
  server_name localhost;

  location / {
    try_files $uri/index.html $uri @adcrawl_work;
  }

  location @adcrawl_work {
    proxy_pass localhost:7890;
  }
}

Sinatra x Puma x Nginx構成によるAPIの起動方法

Nginxは以下のように起動してください。

systemctl start nginx.service

続いてPuma(Sinatra)を以下のようにバックグラウンド起動します。

bundle exec puma -d -e production -C config/puma.rb

もしくは、

bundle exec pumactl start

のように起動します。これでサーバーに設定したURLにport80でアクセスすれば、Sinatraで定義した各エンドポイントにリクエストが可能です。

さいごに

以上でSinatra x Puma x Nginx環境によるAPIが利用できるようになります。

弊社はサーバーサイド技術が売りなこともありRailsやSinatraをフロントエンドとして利用することはほとんど無いのですが、RPAの需要もありWebAPIに特化させる形でRailsやSinatraによるAPIとWebサイトコントローラ(クローラー)環境をセットで構築する機会も増えてきています。

例えば、APIに「商品一覧取得」をリクエストしたら、弊社のクローラーがあるWebサイトに指定のアカウントでログインして、Webスクレイピングで商品一覧情報を収集し、APIのレスポンスとして返却するような形です。

既存のWebサービスをシステム改修することなく、そのままAPI化されたいというご要望でしたら、ぜひ弊社AIxRPAサービスにお問い合わせください。