Ruby Twitter gemで必要な数だけフォロー、フォロワーのidを取得する方法

Twitterでフォローやフォロワーのidを取得する時の制約

Twitterのフォロー、フォロワーidの一覧取得にはAPIリクエスト 1回で最大5000ユーザー、さらに各API 15分につきリクエストが最大15回までという制限があるため、75000以上のフォロー・フォロワーの全てを取得しようとすると最低でも15分以上かかるという問題が存在します。

後日・あるいは暫く経ってから結果を通知するシステムであれば特に問題はありませんが、できるだけすぐに表示をさせたい場合は予め最大取得数を制限したいですね。(75000ユーザー取得すると数〜数十秒かかりますので、必要性が薄ければ尚更もっと制限したいところ)
では、どのようにすれば必要な数だけ取得できるか次で紹介します。

Ruby Twitter gemで必要な分だけフォロワーを取得する方法

取得する時に、以下のように”friend_ids.take(10)”といった形でtake(取得数)を付けるだけです。
例:

client = Twitter::REST::Client.new do |config|
  config.consumer_key        = "YOUR_CONSUMER_KEY"
  config.consumer_secret     = "YOUR_CONSUMER_SECRET"
  config.access_token        = "YOUR_ACCESS_TOKEN"
  config.access_token_secret = "YOUR_ACCESS_SECRET"
end

#フォローしてる人のidを100人取得
a = client.friend_ids("調べたい人のscreen_nameまたはid").take(100)
#フォロワーのidを100人取得
b = client.follower_ids("調べたい人のscreen_nameまたはid").take(100)

なお、これはuser_timelineなど他のAPIでも応用が効くようです。

Twitterがサーバーダウンした時に出すTwitter APIのレスポンスを列挙してみる。[Ruby Twitter gem]

先日(2016/1/19)に大規模なTwitterのサーバー障害に見舞われました。その時に自分が運営していたサイトのログを確認してどのようなエラーがTwitter側から返ってきたか確認したいと思います。

なお、Ruby Twitter gemを使った上での結果です。

具体的にどのようなエラーが発生したか

Twitter::Error::ServiceUnavailable (Over capacity):
Twitter::Error::InternalServerError (Internal error):
Twitter::Error::RequestTimeout (Net::ReadTimeout):

までは分かるのですが、

Twitter::Error (784: unexpected token at '<!DOCTYPE html>

としまいにはJSONでなくHTMLを返されてしまったようで…鯖落ちした時のエラーのレスポンスの返り方はもう予測不可能ですねw

で、結局どう例外処理すればいいの?

結論としましては、

  1. まずはTwitter関連で発生したエラーのデフォルトの処理を記述する
    (内容はTwitterがサーバーダウンした場合を想定)
  2. TooManyRequests(API制限で利用不可)
    Unauthorized(鍵垢、トークン無効などの理由で読み取り不可)
    Forbidden(140字以上のツイートなどの理由で拒否)
    などユーザー側の操作の理由で発生したエラーの捕捉

をしたほうがいいと思います。

例えばRailsで書くとするとこんな感じでしょうか

class ApplicationController < ActionController::Base
  #rescue_from節は下のほうほど優先順位が高いため、Twitter::Errorの例外は一番上に書きそのサブクラスの例外を下に記述すること。
  rescue_from Twitter::Error, with: :twitter_server_error
  rescue_from Twitter::Error::TooManyRequests, with: :twitter_too_many_requests
  rescue_from Twitter::Error::Unauthorized, with: :twitter_unauthorized
  rescue_from Twitter::Error::Forbidden, with: :twitter_forbidden

  def twitter_server_error
   #Twitterが鯖落ちした時の処理を記述
  end

  #以下同様にしてエラー時の処理を行う各メソッドを記述
end

Twitter API連携のアプリ作ったら予想外にアクセスが来たのでサーバーの設定をいじってみた。

Twitterアカウントと連携して犯罪係数測定という数年前の某アニメネタのn番煎じなのを作って( 犯罪係数測定 )、まあこれなら程々にしかアクセス伸びないだろうと思ってたらアクセスが殺到してサーバーがまともに動かなくなってしまったので、改善して安定化させるまでに取った手順を記録します。色々と試行錯誤してみましたがその中で効果が大きかったものをピックアップしていきます。
本番環境で動かしていますので実際のデータに基づいた検証ができているわけではなくあくまで体感でですが、TwitterをはじめとしてAPI連携を行うWebサービスを作りたい人たちの参考になれば(もしくはご指摘があれば)幸いです。

サーバーの構成について

旧サーバー(今でもDB用に使用中)

  • さくらのVPS メモリ1G HDDプラン
  • Ubuntu14.04
  • Apache2
  • Passenger
  • Ruby on rails4
  • MySQL

新サーバー

  • さくらのVPS メモリ2G SSDプラン
  • CentOS6
  • Nginx
  • Unicorn
  • Ruby on Rails4

ネックはメモリ

1GB HDD→2GB SSDのVPSに

これまでさくらのVPS(1GBメモリ、HDDプラン)を利用していたのですが、やたらとサーバーが止まっては(cronで設定したコードで)自動再起動を繰り返すよう不安定さになってしまい、鯖落ち通知メールを見るたび僕の犯罪係数が上がってしまいそう(笑)になってしまいました。
SQLの発行回数減らしたり無駄なAPIリクエスト(今後別のTwitterアプリ作成を見越して冗長になってた)を無くしたり…とやってみましたが、焼け石に水で大した効果は上がらず。
ネックはメモリの制約からPassengerのWorkerを3〜(無理して)4くらいしか設定できないところにあるかなと感じ、このまま落ち着かないまま続くのも嫌だし思いっきり2GB SSDのプランでVPSを増設しました。

またHDDを全然使い切れてなかったのと最悪スワップアウトしてもSSDならIO早いしある程度は使えるんじゃ…との目論見で容量半減を受け入れてSSDを選択しましたw
結果的にはスワップアウトよりは設定変更時の再起動の早さやコマンド操作がサクサクと進むほうの恩恵が大きかったですが。

メモリが制約になる理由としましては、APIを使う関係上APIを投げた相手のサーバーからのリクエストを待たなければならず、どうしてもリクエストを処理する時間が長くなってしまいます。
特に今回のサービスの場合はAPIを数〜十数個くらい(なるべく同時並行にしてはいるものの)投げてから処理していますので、APIリクエスト中に当該リクエストのデータ等諸々を保持するためのメモリの占有時間ががどうしても大きくなってしまう→スループットを伸ばせないからだと分析しました。

ところでなんでクラウド使わないかですって?AWSとさくらのクラウドについてざっと調べた感じ、(最初の割引を除けば)最低維持費がメモリ2GBくらいのVPSよりも高くつきそうだからですw
しかもそれでスペックは1GBのVPS以下だったりして趣味で使う範囲では割高感が否めないですね

まずはWWWサーバー機能のみを新しいVPSに移管して、順位や友達との比較機能、(今後は過去との比較機能も実装しようとしているので)データベースに記録を保管しているのですが、移行がちょっと面倒なデータベースはとりあえず後回しにしようと思い、実行しました。
その結果、移管後の元のVPSはCPU・IO・ネットワーク無事どれも余裕が生まれました。
結構サーバーでネックになりやすいのはDBだとはよく言われますけど、使い方によっては逆にそうではないケースもあるようで今後も気をつけて見極めたいです。

UnicornのWorkerを多め、kill早めに

これまでApache+Passengerを使ってきたので、他のツールを試してみたいとNginx+Unicornにしてみました。(後で1workerで並列リクエスト処理可能なPassengerのほうが良かったかもしれないことに気付くのですが…w)
またしばらく使っているとメモリリークがあるのかメモリ使用量がだんだんと増えてきたので、とりあえずUnicorn Worker Killerを使ってみました。Worker数とkillするタイミングで色々と試してみたところ、Worker数が7で256〜512リクエストでkillする設定にしました。
またメモリ使用量基準でkillする場合、APIの同時リクエスト等でマルチスレッドのコードを書くとkillされた処理のを待ってるプロセスがタイムアウト→まとめてkillになり、パフォーマンスが激落ちするので注意が必要でした。

Disk IOが問題にならない程度にスワップを使う

スワップアウトは極力発生させてはいけないとはよく言いますが、メモリが不足している状況下ではそんな悠長なことは言ってられないもの。とはいえスワップを使い過ぎるとディスクのIO速度がネックになってWorker単体だけでなく全体で見てもスループットが落ちました。またCPU使用率を見ても10%ないしはそれ以上をスワップ関係に割かれることになり…DISK IOと合わせてもし過負荷認定されて制限食らうと怖いなと思うところ。
そこでVPSのコントロールパネルなどを見てDisk IOが極端に多くなっていないか確認しました。Disk IO の確認に関してはこの記事なども参考になりました。
その結果、自分の環境ではWorker数を9まで増やしてたところ7に減らしたほうが捌けるリクエスト数も体感で変わらずか増えて、レスポンスの早さも良くなってで改善されました。(これでもまだスワップは500MB程度ありますが、avgqu-szが5〜30→0.1になって%utilも30〜90%→一桁%に収まりました)

Nginxでリクエスト制限を設定

アクセスが集中している時に無尽蔵に受け入れると通常1秒以内に済むレスポンスで10秒くらい待たされたりしてよろしくないので潔く503を出すことにしました。

keep-aliveを見なおしてTCPコネクションの同時接続数を減らす

keep-aliveは1回のTCP接続で複数のHTTPリクエストを処理する機能です。結構次のリクエストまで待つべく60〜120秒まで持たせようとしている記事が多いですが、connection数が溜まることによるメモリの増加を防ぎたいので、今回はページ読み込みに付随する画像やCSS、JSを一度の接続で読み込むためだけに使いたいと考え5秒としました。また後述するlimit_connを負荷に即して設定しやすくなる副効果もあります。

Limit_req、Limit_connを設定

APIを大量リクエストするため時間のかかる解析処理と逆に軽量な画像やCSS/JSなどの静的ファイル処理、それ以外の処理(普通のページ表示)の主に3つに分けました。
リクエスト数の1秒あたりのrateで制限するとレスポンスまでの時間がかかる処理が偶々集中した時に中々繋がらなくなるので、connection数で制限をかけたほうが負荷に即していい感じでした。(ただしkeep-aliveをオフにするか数秒の場合に限ります。)
特にTwitterアカウントの解析処理ではTwitterサーバーの調子によっての変動や、各ユーザーのフォローid等を取得する場合は解析中のユーザーのフォロー数に応じてレスポンスまでの時間の変動が大きいためconnection数での制限をメインにしてrateは補助的に使用したほうがいいと考えました。最終的にはこんな感じにしました。(9割型connection数で引っかかります。)ちなみに静的ファイル以外にもTwitterログイン時の前処理や後処理、検索エンジンのクローラなどのbotからのアクセス…などなど、アクセス解析のPVには現れないリクエストはたくさんあります。ですので実際のPV数は170くらいでもrateは最低300くらいは必要だったりするので注意して考えて下さい。

静的ファイル 解析 その他
connection 制限なし 25 40
rate 2000req/m 40req/m 500req/m

静的ファイルをNginx完結で渡せるよう設定

1回のページのロードで大抵の場合は最低でもjs、cssファイルを1つずつ、また画像をページによっては数ファイルか(それ以上)読み込ませることになるはずです。静的ファイルは負荷は軽いとはいえページを返すより何倍も回数は多いもの。また静的ファイルのパスは{Railsのルート}/public/以下と一緒であるためRailsアプリケーションで処理させる必要はありません。Unicornも通さずnginxで直接返すよう設定しましょう。

設定結果

/etc/nginx/nginx.conf

(前略・だいたいデフォルト通り)
http {
(前略・だいたいデフォルト通り)

keepalive_timeout 5;
keepalive_requests 60;
reset_timedout_connection off;

client_header_timeout 6;
client_body_timeout 6;

proxy_connect_timeout 6;

limit_req_zone $binary_remote_addr zone=staticperip:13m rate=300r/m;
limit_req_zone $server_name zone=staticperserver:13m rate=2000r/m;
limit_req_zone $binary_remote_addr zone=perip:10m rate=40r/m;
limit_req_zone $server_name zone=perserver:10m rate=500r/m;
limit_req_zone $binary_remote_addr zone=analyzeperip:5m rate=30r/m;
limit_req_zone $server_name zone=analyzeperserver:5m rate=40r/m;
limit_conn_zone $server_name zone=connection:20m;

#gzip  on;

include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/conf.d/default.conf

server {
(前略)

#rssやapple-touch-iconはよく参照されるので無ければRailsを通さずに404を返す
location ~ /(\.?rss|apple-touch-icon(.*).png)$ {
return 404;
}

#解析は一番重い処理であるためlimitを別に設定
location ~ /analyze$ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://unicorn;
limit_req zone=analyzeperserver burst=6 nodelay;
limit_req zone=analyzeperip burst=1 nodelay;
limit_conn connection 25;
}

#静的ファイル用設定
location ~ ^/(images|assets|uploads)/(.*) {
alias /home/nk16/www/twi_analytics/public/$1/$2;
limit_req zone=staticperip burst=20 nodelay;
limit_req zone=staticperserver burst=100 nodelay;
}

#その他の設定
location @unicorn {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://unicorn;
limit_req zone=perserver burst=30 nodelay;
limit_req zone=perip burst=5 nodelay;
limit_conn connection 40;
}
}

で、結局どれくらいまでのアクセスに耐えられたのか

結果としては1分間あたりで
〜75PV 余裕
100PV前後 時々表示が遅くなりはじめる
100PV〜 アクセス制限で503が出始める
150PV 限界。でも1秒でレスポンスが返ることもあるし、遅くとも(解析以外は)5秒以内にはほぼ返る。

という感じになりました。多少の503を許容するのであれば1日でおおよそ5〜10万PVに対応できました。
サーバー移転前と比べると約3〜4倍にまで改善できました(移転前の設定がポンコツすぎただけかも…)
追々DBも移行して同居させればSQLのレスポンスが早くなる分1Worker当たりのスループットが上がるかもしれないけども、同時にネックのメモリを圧迫する考えると…どっちに転んでしまうかw

…ただこれだけ設定しても2年半前の自分は素のPHPで書いた仕組みとしては類似のアプリ、Twitter試験を作った時のほうが時間あたりのPV数はもっと捌けてましたw
Google Analyticsによると一時間で最大約8500PV(1分あたりで140PV以上)にまで達してたようで…メモリ1GのVPSを使ってた上に当時の自分は無知だったのでフレームワークも使わずApacheで適当に公開に必要な設定だけ書いてただけだったのですが…皮肉なもので。