電書部技術班、電子書籍配信サーバーに挑む

第7回電書フリマにおける配信サーバーの改善

今回は、文学フリマ版の電書サーバーの補足説明を行った後に、文学フリマにおける電書販売のフィードバックを受けて、電書フリマではどのように電書サーバーを改善したのかを解説します。

文学フリマ版電書サーバーの補足

URLハンドラ

iOSには「ダウンロードしたデータを指定したアプリで起動する」という機能がなかった(バージョン3.2から追加)ため、URLハンドラがよく使われます。例えば、あるURLのPDFをGoodReader(iPad/iPhone用のPDFリーダー)で読み込むためには、リンクを以下のようにします。

GoodReaderのURLハンドラ
ghttp://foo.jp/bar.pdf

httpではなくghttpというプロトコルを使うことで、ダウンロードしたデータがGoodReaderに引き渡されます。EPUBリーダーのStanzaならstanza://です。文学フリマ版電書サーバーが購入者に送信するメールには、通常のhttpの他、ePubならstanza、pdfならghttpのリンクを載せています。

運営コスト

コストをざっと計算すると、フリマの参加費が4,500円。herokuの使用料が約9ドル、s3の使用料は5セントでした。ちなみにherokuで課金されたのは、フリマ当日にダイノ(同時に起動するWebのインスタンス数)を1→2と増やしたのと、メール送信のプラグインSendgridを当日だけproバージョンにした部分のみです。電書部で作成した「電子書籍の出てくる本のレビュー集」である「未来のテキスト」は約160冊(単価100円)販売できたため、サーバー経費は十分にまかなえた計算になります。

電書フリマ構想

この成功を受けて構想されたのが、電書のみを販売する「電書フリマ」です。電書フリマは、ブース一つだった文学フリマとは異なり、渋谷のコラボカフェを借りて電書部が主催するイベントです。実際には、2010年7月17日に開催されました。

フィードバックとして、技術班には以下の要望が寄せられました。

  • タイムアウトの解消/PDF分割の解消
  • 複数の場所でフリマを開催して、売上計算は個別にしたい
  • iPhone対応
  • タグのサポート

電書の数も大幅に増えることになったため、PDF担当はtk_zombieに加えていなちょうinachou⁠⁠、EPUB担当は小嶋に加えて早坂tamisukeと2名の応援を仰ぐことになりました。

タイムアウトの解消/PDF分割の解消

第3回で解説したとおり、文学フリマ版の電書サーバーは以下のような処理の流れでした。

  1. 購入ボタンを押す
  2. 電書マスターをAmazon S3(Amazonのストレージサービス)からダウンロード
  3. 電書にメアドを埋め込む
  4. 再びS3にアップロード
  5. 購入完了メール(ダウンロードURL付き)を送信

1~5の処理がherokuの制限である30秒を超えるとタイムアウトエラーになります。特に電書マスターのPDFが大きすぎると処理が間に合わないため、サイズの大きなPDFは分割して対応していました。

電書フリマ版の電書サーバーでは、この問題を解決するために処理フローを以下のように変更しました。

  1. 購入ボタンを押す
  2. 電書生成処理をバックグラウンドにセット
  3. ありがとうメールを送信(ここで販売は完了)
  4. バックグラウンドで電書生成(マスターDL、メアド埋め込み、S3アップロード)
  5. 完了メール(ダウンロードURL付き)を送信

Delayed_job

電書生成をバックグラウンドで行われることで、販売処理は即座に終了するためタイムアウトはなくなり、かつ電書マスターPDFを分割する必要もなくなります。

rubyにはいくつかバックグラウンド処理用のライブラリがあり、言語自身にもスレッド機能がありますが、herokuではDelayed_job(DJ)を使います。DJで使えるバックグラウンドプロセスの数は、ワーカ(worker)で指定します。デフォルトでは0で、一つ増やす毎に0.05ドル/時間(36ドル/月)課金されます。

DJの特徴は、特定のメソッドだけを非同期動作にしたり、あるクラス全体を非同期動作にしたり、柔軟な設定ができる点です。以下は実際に使っているコードの一部です。handle_asynchronously宣言したメソッドを、通常のrubyのメソッドのように呼び出せば非同期に動作します。

Delayed_jobを使って、非同期なメソッドを定義する
class DenshoMaker
  def make
    begin
      send_registration_email
      make_ebooks
      send_completion_email
    rescue
      send_error_email($!.to_s)
    end
  end
  # 非同期宣言
  handle_asynchronously :make
end

非同期処理をスタート/ストップさせるにはrakeを使います。herokuコマンドを使って、サーバ側のrakeを起動します。

rakeによる非同期処理
heroku rake jobs:work   # 非同期処理開始
heroku rake jobs:clear  # キューに残っている非同期タスクをクリア

複数の場所で開催

電書フリマは、メイン開催は渋谷のコラボカフェそのほかにも「僕たちだけがおもしろい」公開収録現場電書フリマ@京都で開催することになりました。また、渋谷でも周辺のカフェで販売する企画も出ました。そのため、だれが、どの電書を、何冊販売したの集計をとる必要があります。文学フリマ版で認証を導入していたので、これを拡張することにしました。

文学フリマ版の認証
    def authorize(login, password)
      login == "samurai" && password == "sushi"
    end
電書フリマ版の認証
  def authorize(login, password)
    users = %w(id1 id2 id3 id4 id5)
    passwords = %w(pw1 pw2 pw3 pw4 pw5)
    users.each_with_index do |u,i|
      return true if (u == login && passwords[i] == password)
    end
    nil
  end

文学フリマ版で電書を販売すると、送信先のメールアドレスを持つCustomerモデルのインスタンスをつくります。これは複数のBookモデル(名前がよくないのですが、購入した電書の情報)のインスタンスを持っています。電書フリマ版は、Bookモデルにsold_byという属性を追加して、ログインしたユーザー名を販売時に保存するようにしました。ユーザー名はcurrent_userメソッドで取得できます。

売上は、sold_byカラムでグルーピングして計算しています。

電書フリマはすべてのアカウントで同じ電書を販売するので、アカウント毎のカスタマイズは不要です。そのため販売する人のアカウントはモデルにしませんでした。

iPhone対応

電書フリマ版サーバーは、販売画面をiPhoneに対応しました。メイン会場周辺のカフェや(実際にはやりませんでしたが)道端でもゲリラ的に販売できるようになります。電書フリマのテーマのひとつである同時多発開催のための機能です。

そのために販売画面の構成を見直し、sinatraのviewの一部だった販売画面を切り離して、htmlとしてどこにでも置けるようにしました。コントローラ側にapiを用意して、htmlはJavaScriptでコントローラ側と通信します。プロトコルはJSONPです。PCとiPhoneは、htmlを2つ用意してエージェントを見て切り替えています。

利用したライブラリはrack-jsonpです。rackミドルウェアなので上位のライブラリに関係なく利用できます。オブジェクトにto_jsonメソッドが追加され、レスポンスで返すだけなのでとても簡単。呼び出す側にcallbackパラメータがついていれば、自動的にJSONからJSONPに切り替わります。以下のコードではコンテントタイプを指定していますが、JSONPのときにはコンテントタイプも切り替わります。

電書一覧をJSONで返すアクション
app do
  use Rack::JSONP
  get '/api/allbooks' do
    content_type :json
    BookMaster.all.map{|b|b.to_hash}.to_json
  end
end

タグ

文学フリマで15冊だった電書は、電書フリマでは大幅に増えることが予想されました。そこで電書をタグで管理すること考えました。販売画面でもタグを表示して、電書を一括して選択できる仕組みです。電書のマスターであるBookMasterモデル(テーブル)とTagモデル(テーブル)を、多対多対応させています。DataMapperでは、以下のようになります。

DataMapperにおける多対多の関連
class Tag
  include DataMapper::Resource
  has n, :book_masters, :through => Resource
  property :id, Serial
  property :name, String
end
class BookMaster
  include DataMapper::Resource
  has n, :books
  has n, :tags, :through => Resource
  property :id, Serial
  property :title, String
  # 以下省略
end

sinatra_more

第3回で述べたように、電書サーバーはWebフレームワークとしてsinatraを使っています。小規模アプリを作るにはrailsより向いていますが、画面数が多くなってくるとソースのあちこちにロジックが散らばって管理できなくなってきます。電書フリマ版でもだいぶ機能が増えたので、sinatara_moreを使うことにしました。sinatara_moreは、sinatraのシンプルさはそのままに、rails風の機能を追加する拡張ライブラリです。sinatara_moreは5つのプラグインから構成されていて、不要な機能はオフにしておくことができます。

MarkupPluginはlink_toなどのrails風のタグヘルパーを多数追加します。DataMapperのインスタンスと連動するformのヘルパーもあります。

RenderPluginはHTMLテンプレートから別のテンプレートを読み込むpartial機能がメインです。

WardenPluginはUserモデルにもとづく認証機能を提供します(sinatra-authorizationを使ったので、これは使っていません⁠⁠。

MailerPluginはメール送信機能です。

RoutingPluginはルート機能を大幅に拡張します。

ただしsinatra_moreは開発はすでに終了しているため、バグの対応などは期待できません(開発者はPadrinoに移行したようです⁠⁠。実際にディレクトリの中にあるテンプレートをレンダリングするとfromヘルパーがうまく展開されないというバグがあります。

電書フリマの結果

事前に簡単な予約を募って、来場の時間帯があまり重ならないようしたのですが、電書フリマは想像を遥かにこえる来場者数になりました。会場のコラボカフェは大混雑で、2台あるパソコンはフル稼動、ときどきiPhoneも使われていたようです。コラボカフェは地下にあるのですが、地上につながる階段まで人があふれ、店の前には入れない電書部員やお客さんが群れをなす(幸い歩道が広かったので、あまり混乱はありませんでしたが)状態でした。

その結果、総計でかなりの数を販売することができ、⁠電子書籍を対面で売る」のは、十分に成立することが分かりました。

電書サーバーのコストは、文書フリマより増えました。バックグラウンド処理のためにワーカを増やしたコストが効いています。ワーカはフリマ当日だけではなく、テストのため開発中も増やしていました。一つのワーカを一ヶ月使うと36ドルになります。これが主なコスト原因です。ワーカはプログラム中から動的に制御できるため、最適化の余地があるかもしれません。

次回最終回は現在開発中の電書サーバーの最新型「電書サーバー秋」と、今後の展開について解説します。

おすすめ記事

記事・ニュース一覧