AWS SDK for Rubyのmemoizeを利用してAPIリクエストを効率化する

2014.05.17

こんにちは。望月です。
最近、AWS SDK for Rubyを利用して、運用で使うスクリプトを書いているのですが、どうも処理が遅いのが気になっていました。
参考 :

どこで遅くなっているのかを調べているうちに一つわかったことがあったので、ブログに残しておきます。

AWS SDK for RubyのAPI呼び出し

例えば、上に示したうちの2つ目のスクリプトでは、全てのEC2インスタンスに紐付いている、全てのSecurityGroupに対して処理を実施するために以下のようなコードを書いています。

ec2 = AWS::EC2.new  
ec2.instances.each do |i|
  i.security_groups.each do |sec|
    # ...処理...
  end
end

上のコードを、AWS SDKのデバッグモードで実行していると、以下のような表示が。

D, [2014-05-17T19:56:33.882798 #21903] DEBUG -- : [AWS EC2 200 0.368748 0 retries] describe_instances()

D, [2014-05-17T19:56:34.154989 #21903] DEBUG -- : [AWS EC2 200 0.267034 0 retries] describe_instances(:instance_ids=>["i-xxxxxxx"])

D, [2014-05-17T19:56:34.385477 #21903] DEBUG -- : [AWS EC2 200 0.225819 0 retries] describe_tags(:filters=>[{:name=>"key",:values=>["Name"]},{:name=>"resource-type",:values=>["instance"]},{:name=>"resource-id",:values=>["i-xxxxxxx"]}])

D, [2014-05-17T19:56:34.670819 #21903] DEBUG -- : [AWS EC2 200 0.28416 0 retries] describe_instances(:instance_ids=>["i-yyyyyyy"])

D, [2014-05-17T19:56:34.921838 #21903] DEBUG -- : [AWS EC2 200 0.249804 0 retries] describe_tags(:filters=>[{:name=>"key",:values=>["Name"]},{:name=>"resource-type",:values=>["instance"]},{:name=>"resource-id",:values=>["i-yyyyyy"]}])

<...snip...>

私のイメージでは「DescribeInstancesを一回コールして全てのEC2の情報を取得したら、後はその情報を使ってゴニョゴニョやっているんじゃないかな」と適当にイメージしていたのですが、起動しているインスタンスの数だけDescribeInstancesとDescribeTags(処理の中で利用している)がコールされていました。これだけAPIコールが重なると当然処理は遅くなりますし、APIサーバ側にも優しくないと思います。

AWS.Memoize

どうしたものかとSDKのドキュメントを読んでいたところ、AWS.memoizeというメソッドを見つけました。

AWS SDK for Ruby Documentation

While memoization is enabled, every response that is received from the service is retained in memory. Therefore you should use memoization only for short-lived blocks of code that make relatively small numbers of requests. The cached responses are used in two ways while memoization is enabled: Before making a request, the SDK checks the cache for a response to a request with the same signature (credentials, service endpoint, operation name, and parameters). If such a response is found, it is used instead of making a new request. Before retrieving data for an attribute of a resource (e.g. AWS::EC2::Instance#launch_time), the SDK attempts to find a cached response that contains the requested data. If such a response is found, the cached data is returned instead of making a new request.

memoize(メモ化)を有効にすることで、そのスコープ内ではAPIリクエストに対するレスポンスがキャッシュされます。以下の条件に合致した場合は新規APIリクエストが行われず、キャッシュが有効になります。

  • SDKからAPIリクエストを送る際に、APIリクエストに対するsignatureがキャッシュに残っているリクエストのsignatureと一致した場合
  • Resourceのattributeを取得しようとした際に(例 : AWS::EC2::Instance#launch_time)、当該データが過去のキャッシュに存在する場合

ただし、現時点での最新SDK(1.40.0)では、AWS::Core::Resourceを継承したクラスでのみmemoizeが有効なようです。例 : AWS::EC2::InstanceAWS::EC2::SecurityGroupなど。また、あまり長いスコープで使わずに短いスコープの比較的少ないAPIリクエストに対して利用することが推奨されています。

どうやら、このメソッドを利用することで高速化が望めそうです。ということで早速コードを修正してみます。

確認

それでは、先ほどのコードを以下のように修正します。

ec2 = AWS::EC2.new
AWS.memoize do
  ec2.instances.each do |i|
    i.security_groups.each do |sec|
      # ...処理...
    end
  end
end

変更点は、EC2のAPIを利用する処理を行っている部分をブロックとしてAWS.memoizeに渡すようにしただけです。変更した後のコードを、先ほどと同様にデバッグモードで実行してAPIリクエストを確認してみましょう。

D, [2014-05-17T20:12:31.368901 #22045] DEBUG -- : [AWS EC2 200 0.976428 0 retries] describe_instances()

APIコールがDescribeInstanceの一回になりました。memoizeを利用することで、かなりの効率化ができました。memoizeを導入したおかげで、数十秒かかっていたスクリプトが2〜3秒で完了するようになりました。

まとめ

AWS.memoizeを利用することで、不要なAPIリクエストの数を減らすことができます。これによってプログラムの実行速度の向上とAWSのAPIサーバの負荷を少しでも軽減することができます。かなり便利な機能だと思うのですが、私は昨日まで知りませんでした。。AWS SDK for Rubyを利用される方はぜひ使ってみてください。