is Neet

ネトゲしながら暮したい

Rails 3.1で画像ストレージにAmazon S3を使う

僕が趣味で開発&運営しているラクガキサービス「Leeno」の画像ストレージを、s3へ移行しました。


まずLeenoの構成は以下。

  • VPS:さくらのVPS 1.5Gプラン(今はもうないけど)
  • OS:Fedora
  • リバースプロキシ:Nginx
  • appサーバ:unicorn
  • 言語:Ruby( on Rails 3.1 )
  • DB:MongoDB

といった具合。



s3へ移行前のLeeno

Leenoは画像をメインのコンテンツとして扱うサービスです。
今回S3に移行するまでは、MongoDBのGridFSという巨大ファイルを扱うためのストレージを使っていました。
まずUploadの流れですが、これはとてもシンプルで、

  • html5canvas要素からbase64エンコードされた文字列を取得
  • jsでサーバに送信して、受け取ってデコードしてふごふごする
  • ふごふごしたデータをGridFSに突っ込む


で、表示のさいはRailsのactionで

def image
  send_data( @history.to_image, :disposition => "inline", :type => "image/png" )
end


といった具合で、画像データを返すactionを用意しておいてView側で

image_tag( history_image_path(@history.to_params_key), options )


みたいに使っていました。
しかしまぁこのままでは、画像データにアクセスするたびにRailsのaction通るわ、GridFSから画像読んでくるわで効率悪すぎワロタ状態なので、Railsのcache機能を使いcontrollerで、

caches_page :image

という宣言をしておきます。
これで一度誰かが閲覧した画像は、Rails.root/public/以下にキャッシュが残っていくので、次回移行のアクセスの場合はRailsのactionを通る前にpublic以下を見てファイルが存在すればそのままそれを返却してくれるようにしておきます。


一応この構成でも満足な速度は出ていたのですが、大きな問題点がありました。

  • まったく冗長化されていいない。GridFSのファイルが何らかの障害で壊れたらおしまい。
  • public以下にキャッシュしているとは言え、多い場合は1ページに100枚以上の画像を読み込むので、その分リクエストはサーバに飛んでくる
  • Railsのpublic以下にキャッシュするということは、いずれwebサーバが複数台にまたがった時に破綻するキャッシュ方法だということ

とくに一番目が心配でした。



Amazon S3を使うと何が嬉しいか

  • まず冗長化ハンパない。

 データの保存と同時にS3作成したリージョン内で物理的に違うサーバ複数台でレプリケーションしてくれる。
 その冗長化に喪失があればすぐに検知して修復してくれる。
 つまり東京リージョンで作成した場合、東京が一瞬で消滅しない限りLeenoのデータは安心。堅牢性ヤバい。


  • リクエストがleeno.jpサーバに飛んでこない。

 全部S3に直リンしてるからウチのサーバが悲鳴あげる事ない。


  • スケールアウトしやすい

 いずれwebサーバが複数台にまたがったとしてもデータはS3上にあるので無問題。


と、良いことばっかりな気がするので移行しました。




S3にバケットを作成する

AWSへの登録とかは端折ります。
S3ではまずBucketを作成し、そのBucketにオブジェクトを保存していく流れになります。
まず、AWS consoleに入りS3タブを選択しCreate Bucketを押します。


f:id:soplana:20120524003652p:plain


そうするとポップアップが開くので、適当にBucket名を入力し、RegionはTokyoを選択しCreateします。

f:id:soplana:20120524004216p:plain


以上でBucketの作成が完了しました。お早い。



Railsから保存する

まずS3を扱うためのGem入れましょうね。
お手元のGemfileを開き、

gem 'aws-s3', :require => 'aws/s3'

を追加して、bundle installしてください。


次に、S3へアクセスする為のキーを取得します。
AWS consoleに入り、右上のアカウントメニューから、Security Credentialsを選択ます。

f:id:soplana:20120524004845p:plain


そしたらアクセスキーとシークレットアクセスキーが取得出来るのでメモっとておきます。

f:id:soplana:20120524004914p:plain



以降、Leeno仕様で話を進めます。
LeenoはCanvasというcollection(RDBでいうテーブル)があり、Canvas collectionはhas_manyでHistory collectionを持ちます。
誰かが一枚目にラクガキを書いた時にCanvasが作成され、そこにはCanvasの作成者やサイズなど基本的なデータを持ち、Historyが具体的な画像データなどを持っています。


なので今回、画像を保存するといった処理もHistory Classが管理する仕事なので、History Classにincludeしたらそれっぽく使えるmoduleを簡単に作成しました。こんな感じ。

module LeenoS3
  BUCKET = "leeno"
  
  # moduleをincludeした時にAWSの設定を実行する
  def self.included(model)
    # メモっておいたアクセスキーとシークレットアクセスキーを設定
    AWS::S3::Base.establish_connection!(
      :access_key_id     => 'xxx', 
      :secret_access_key => 'xxx'
    )
    # gemが東京リージョンに対応してないらいので設定
    AWS::S3::DEFAULT_HOST.replace "s3-ap-northeast-1.amazonaws.com"
  end

  # accessの範囲をpublicに設定してあげないと外部から見れない
  def upload file_name, file 
    AWS::S3::S3Object.store(file_name, file, LeenoS3::BUCKET, :access => :public_read)
  end

end


あとは、Base64エンコードされた画像データとファイル名をふごふごしてuploadメソッドに渡すと保存完了です。
ちなみにファイル名は"hoge/sample.png"とファイルパスで渡すと、S3上にもそのディレクトリが出来上がります。


View側で呼び出す時は、History Classにto_image_urlみたいなメソッドを定義して、S3へのURLを取得できるようにしておき、

image_tag( @history.to_image_url )

といった具合に、リファクタリングしました。


問題なのはLeenoにはRedrawという、他人が描いた絵にラクガキ出来る機能があります。
処理的には、

  • Aさんがhoge.pngというラクガキ画像を作成。
  • hoge.pngがleeno.jp(GridFS)に保存される。
  • Bさんがhoge.pngを元に新しいラクガキ画像を作成しようとする。
  • GridFSにアクセスして、hoge.pngを取得してHTML5canvas要素にdrawImageメソッドを使い描画する。
  • Bさんがラクガキしてサブミットするとhoge2.pngがGridFSに保存される。


となっていたワケですが、canvas要素の仕様で外部ドメインの画像は直接扱えません。
今まではleeno.jp(GridFS)から取得していた画像だったので、問題なくRedraw出来ていたのですが、S3へ移行すると外部ドメインになるので、Redrawが出来ないといった問題が浮上しました。
なのでRedraw用の画像を取得するactionを少し改修して、

def redraw_image
  send_data( @history.to_image, :disposition => "inline", :type => "image/png" )
end

となっていた所を、

def redraw_image
  send_data( open(@history.to_image_url,"rb").read, :disposition => "inline", :type => "image/png" )
end

というふうに、一度open-uriでS3の画像を開き、それをsend_dataで返す事で、leeno.jpの画像のように見せかける事にしました。


だいたい以上で、移行は終了しました。

現在Leenoは約7000枚程画像があるので、それらをS3にアップロードするスクリプト書いて終了です。




Nginxの一時ファイル保存ディレクトリの所有者変更(5/24追記)

ところが本番リリースしたあと、画像データを半分くらいまでしか読み込まないという問題が発生しました。
Nginxのログを確認した所なにやら見慣れぬエラーが。

2012/05/23 02:25:52 [crit] 21236#0: *66652 open() "/var/lib/nginx/tmp/proxy/9/63/0000000639" failed (13: Permission denied)

どうやらopen_uriを使って取得したデータをsend_dataしてる部分で、一時的にファイルを保存してるみたいなのですがその保存先のディレクトリに権限がなくてエラーになってるようです。
なのでNginxを起動しているUserに所有者を変更してあげれば、この問題は解決しました。
だいたいPermission deniedなのに何で画像の半分くらいは読み込めたのかは謎なのですが。
原因究明してくれた@T_Hash先生に感謝。




S3凄い! でもお高いんでしょう?

気になりますよね。値段。
取り敢えず現段階での話になるのですが、大体一日で、$0.19でした。
ただ、S3の従量課金はPOSTの方が高めに設定されていて、この日は7000枚の画像をS3にアップロードした事もあり、かなり高めになっていると思います。
$0.19 = ¥16だとすると、仮に¥16/日だとしても¥16 * 30 = ¥480となります。
7000枚アップロードしてこれなので、かなり良心的な値段設定なのではないでしょうか。
アクセス数やPOST数が10倍とかになると¥5000/月なのでそこそこかかってしまいますが。

                    • 6/1追記-------------

S3に乗り換えて10日ほど経ちましたが、現在$0.23でした。
一日$0.01以下で済んでいるので、アホみたいに安いです。ビビった。安すぎる。


まとめ

移行の手間もそんなにかからなかったし、値段も良心的だし、今のところ移行してよかったなと感じております。
ここからさらにCloudFrontとかを立ててキャッシュを効かせたりすると、さらに効果的だったりするようですが、それはまだ対応していません。
cssやjs等のassetsファイルだけCloudFrontに置いてアクセスの高速化を測る、などといった使い方も出来るのでLeenoもそのうち導入しようかなーとは思っています。


おしまい。