ようへいの日々精進XP

よかろうもん

続・日本で 128 番目くらいに Ruby で AWS Lambda を試したメモ 〜俺のライブラリを Lambda で動かしたいけど...〜

追記

追記 (3) aws-sdk 以外の gem だけをロードする場合ってどうなのか

検証コード

以下のように furikake で利用する aws-sdk を除いて, 依存している gem を require してみます.

require 'benchmark'

def run(event:, context:)
  Benchmark.bm(10) do |r|
    r.report 'require gems' do
      require 'thor'
      require 'markdown-tables'
      require 'backlog_kit'
    end
  end
end

run(event: {}, context: {})

結果

# On macOS (Ruby 2.5.1)
$ bundle exec ruby lambda.rb
                 user     system      total        real
require gems  0.091279   0.049211   0.140490 (  0.387624)

$ bundle exec ruby lambda.rb
                 user     system      total        real
require gems  0.091667   0.041068   0.132735 (  0.133426)

$ bundle exec ruby lambda.rb
                 user     system      total        real
require gems  0.087537   0.043253   0.130790 (  0.132725)

# On Docker Container (lambci/lambda:build-ruby2.5)
bash-4.2# ruby lambda.rb
                 user     system      total        real
require gems  0.060000   0.310000   0.370000 (  1.752973)

bash-4.2# ruby lambda.rb
                 user     system      total        real
require gems  0.060000   0.190000   0.250000 (  1.729625)

bash-4.2# ruby lambda.rb
                 user     system      total        real
require gems  0.140000   0.120000   0.260000 (  1.668639)

# On Lambda
                 user     system      total        real
require gems  0.000016   0.000006   0.000022 (  0.000020)

                 user     system      total        real
require gems  0.000021   0.000008   0.000029 (  0.000026)

                 user     system      total        real
require gems  0.000030   0.000011   0.000041 (  0.000037)

Lambda での処理時間が圧倒的に高速で優秀だと思います. 全く aws-sdk に依存しない gem を利用する場合にはロードに要する時間はとても短いことを確認しました.

見解

これらの結果から, 以下のようなことが言えそうです.

  • サードパーティ製の gem において, aws-sdk に依存している gem を Lambda で利用する場合, 関数の初回起動時だけ gem のロードに時間が掛かる可能性がる (furikake はまさにそうで, furikake 自身が aws-sdk に依存している)

追記 (2) Ruby ランタイムに組み込まれている gem の一覧

以下, Ruby ランタイムに事前に組み込まれている gem の一覧です. aws-sdk は予め組み込まれていました (そりゃそうだ...).

codestar (1.7.0)
aws-sdk-cognitoidentity (1.5.0)
aws-sdk-cognitoidentityprovider (1.10.0)
aws-sdk-cognitosync (1.5.0)
aws-sdk-comprehend (1.8.0)
aws-sdk-configservice (1.19.0)
aws-sdk-connect (1.8.0)
aws-sdk-core (3.37.0)
aws-sdk-costandusagereportservice (1.5.0)
aws-sdk-costexplorer (1.12.0)
aws-sdk-databasemigrationservice (1.13.0)
aws-sdk-datapipeline (1.5.0)
aws-sdk-dax (1.7.0)
aws-sdk-devicefarm (1.12.0)
aws-sdk-directconnect (1.8.0)
aws-sdk-directoryservice (1.10.0)
aws-sdk-dlm (1.6.0)
aws-sdk-dynamodb (1.16.0)
aws-sdk-dynamodbstreams (1.5.0)
aws-sdk-ec2 (1.56.0)
aws-sdk-ecr (1.8.0)
aws-sdk-ecs (1.22.0)
aws-sdk-efs (1.6.0)
aws-sdk-eks (1.7.0)
aws-sdk-elasticache (1.9.0)
aws-sdk-elasticbeanstalk (1.13.0)
aws-sdk-elasticloadbalancing (1.7.0)
aws-sdk-elasticloadbalancingv2 (1.16.0)
aws-sdk-elasticsearchservice (1.14.0)
aws-sdk-elastictranscoder (1.6.0)
aws-sdk-emr (1.7.0)
aws-sdk-firehose (1.8.0)
aws-sdk-fms (1.6.0)
aws-sdk-gamelift (1.9.0)
aws-sdk-glacier (1.13.0)
aws-sdk-glue (1.20.0)
aws-sdk-greengrass (1.10.0)
aws-sdk-guardduty (1.10.0)
aws-sdk-health (1.7.0)
aws-sdk-iam (1.10.0)
aws-sdk-importexport (1.5.0)
aws-sdk-inspector (1.11.0)
aws-sdk-iot (1.18.0)
aws-sdk-iot1clickdevicesservice (1.5.0)
aws-sdk-iot1clickprojects (1.5.0)
aws-sdk-iotanalytics (1.9.0)
aws-sdk-iotdataplane (1.5.0)
aws-sdk-iotjobsdataplane (1.6.0)
aws-sdk-kinesis (1.8.0)
aws-sdk-kinesisanalytics (1.7.0)
aws-sdk-kinesisvideo (1.6.0)
aws-sdk-kinesisvideoarchivedmedia (1.6.0)
aws-sdk-kinesisvideomedia (1.5.0)
aws-sdk-kms (1.11.0)
aws-sdk-lambda (1.13.0)
aws-sdk-lambdapreview (1.5.0)
aws-sdk-lex (1.8.0)
aws-sdk-lexmodelbuildingservice (1.11.0)
aws-sdk-lightsail (1.10.0)
aws-sdk-machinelearning (1.5.0)
aws-sdk-macie (1.5.0)
aws-sdk-marketplacecommerceanalytics (1.5.0)
aws-sdk-marketplaceentitlementservice (1.5.0)
aws-sdk-marketplacemetering (1.5.0)
aws-sdk-mediaconvert (1.16.0)
aws-sdk-medialive (1.15.0)
aws-sdk-mediapackage (1.9.0)
aws-sdk-mediastore (1.6.0)
aws-sdk-mediastoredata (1.7.0)
aws-sdk-mediatailor (1.6.0)
aws-sdk-migrationhub (1.7.0)
aws-sdk-mobile (1.5.0)
aws-sdk-mq (1.7.0)
aws-sdk-mturk (1.8.0)
aws-sdk-neptune (1.6.0)
aws-sdk-opsworks (1.8.0)
aws-sdk-opsworkscm (1.9.0)
aws-sdk-organizations (1.15.0)
aws-sdk-pi (1.5.0)
aws-sdk-pinpoint (1.12.0)
aws-sdk-pinpointemail (1.0.0)
aws-sdk-polly (1.13.0)
aws-sdk-pricing (1.5.0)
aws-sdk-rds (1.36.0)
aws-sdk-redshift (1.13.0)
aws-sdk-rekognition (1.14.0)
aws-sdk-resourcegroups (1.7.0)
aws-sdk-resourcegroupstaggingapi (1.5.0)
aws-sdk-resources (3.27.0)
aws-sdk-route53 (1.15.0)
aws-sdk-route53domains (1.7.0)
aws-sdk-s3 (1.23.1)
aws-sdk-sagemaker (1.22.0)
aws-sdk-sagemakerruntime (1.6.0)
aws-sdk-secretsmanager (1.19.0)
aws-sdk-serverlessapplicationrepository (1.9.0)
aws-sdk-servicecatalog (1.12.0)
aws-sdk-servicediscovery (1.7.0)
aws-sdk-ses (1.13.0)
aws-sdk-shield (1.8.0)
aws-sdk-signer (1.4.0)
aws-sdk-simpledb (1.5.0)
aws-sdk-sms (1.5.0)
aws-sdk-snowball (1.9.0)
aws-sdk-sns (1.7.0)
aws-sdk-sqs (1.9.0)
aws-sdk-ssm (1.32.0)
aws-sdk-states (1.7.0)
aws-sdk-storagegateway (1.12.0)
aws-sdk-support (1.5.0)
aws-sdk-swf (1.5.0)
aws-sdk-transcribeservice (1.10.0)
aws-sdk-translate (1.6.0)
aws-sdk-waf (1.10.0)
aws-sdk-wafregional (1.11.0)
aws-sdk-workdocs (1.6.0)
aws-sdk-workmail (1.6.0)
aws-sdk-workspaces (1.8.0)
aws-sdk-xray (1.8.0)
aws-sigv2 (1.0.1)
aws-sigv4 (1.0.3)
bigdecimal (default: 1.3.4)
bundler (1.17.1)
cmath (default: 1.0.0)
csv (default: 1.0.0)
date (default: 1.0.0)
dbm (default: 1.0.0)
etc (default: 1.0.0)
fcntl (default: 1.0.0)
fiddle (default: 1.0.0)
fileutils (default: 1.0.2)
gdbm (default: 2.0.0)
io-console (default: 0.4.6)
ipaddr (default: 1.2.0)
jmespath (1.4.0)
json (default: 2.1.0)
openssl (default: 2.1.2)
psych (default: 3.0.2)
rdoc (default: 6.0.1)
scanf (default: 1.0.0)
sdbm (default: 1.0.0)
stringio (default: 0.0.1)
strscan (default: 1.0.0)
webrick (default: 1.4.2)
zlib (default: 1.0.0)

追記 (1) Ruby ランタイムの gem environment

以下のようになっていました.

RubyGems Environment:
  - RUBYGEMS VERSION: 2.7.6
  - RUBY VERSION: 2.5.3 (2018-10-18 patchlevel 105) [x86_64-linux]
  - INSTALLATION DIRECTORY: /var/runtime
  - USER INSTALLATION DIRECTORY: /.gem/ruby/2.5.0
  - RUBY EXECUTABLE: /var/lang/bin/ruby
  - EXECUTABLE DIRECTORY: /var/runtime/bin
  - SPEC CACHE DIRECTORY: /.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /var/lang/etc
  - RUBYGEMS PLATFORMS:
    - ruby
    - x86_64-linux
  - GEM PATHS:
     - /var/runtime
     - /var/task/vendor/bundle/ruby/2.5.0
     - /opt/ruby/gems/2.5.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => false
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /var/lang/bin
     - /var/lang/bin
     - /usr/local/bin
     - /usr/bin/
     - /bin
     - /opt/bin

これは

qiita.com

俺でもわかるシリーズ Advent Calendar 2018 第 8 日目の記事になる予定です.

qiita.com

初老丸 Advent Calendar 2018 第 12 日目の記事になる予定です.

tl;dr

昨日, 以下のような記事を書きました.

qiita.com

RubyAWS Lambda 上で動くなんてすごいなあと興奮していたのもつかの間, 自分で作ったライブラリが動くかなあと試そうとして躓いた (今も躓いている) ので, メモっておきます.

あれ, 関数の起動が遅いよ...これ

furikake を Lambda で

以下のような issue を立てて, furikake を Lambda 上で動かしてみようとしました.

github.com

以下のようなエントリーポイントになるコードを書いて sam でローカルテストを実行するところまで漕ぎ着けました.

require 'furikake'

def run(event:, context:)
  report = Furikake::Report.new
  report.show
end

以下, 実際の実行例です. 関数のタイムアウト自体は 60 秒に設定しています.

$ echo '{"test":"dayo"}' | sam local invoke ServerlessFurikake
2018-12-01 07:26:47 Reading invoke payload from stdin (you can also pass it from file with --event)
2018-12-01 07:26:47 Found credentials in shared credentials file: ~/.aws/credentials
2018-12-01 07:26:47 Invoking lambda.run (ruby2.5)

Fetching lambci/lambda:ruby2.5 Docker container image......
2018-12-01 07:26:49 Mounting /path/to/serverless-furikake as /var/task:ro inside runtime container
2018-12-01 07:27:50 Function 'ServerlessFurikake' timed out after 60 seconds

あれ...なんでや...タイムアウトしてしまいました.

何が起きているのか

gem のロードが遅くない??

furikake は aws-sdk 等の幾つかの gem に依存しているのですが, これらの gem のロードが遅いんじゃないかな...という仮説を立ててみました. ほんとにそれくらいしか思いつかなかったので. ということで, ローカル環境と sam invoke local で実行する Docker コンテナ環境, Lambda 環境の三環境で gem の読み込み速度の比較をしてみたいと思います.

検証環境

sam は local invoke する際に Docker コンテナを起動して, そのコンテナ上で関数を実行しているので, そのコンテナ上でデバッグしてみたいと思います.

尚, 利用する Docker イメージは以下のイメージです.

github.com

また, Docker を動かしている環境は以下の通りです.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.6
BuildVersion:   17G65

$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

ハードウェア的には以下の通りです.

2018 年でも快適に利用出来る環境だと思います.

で, 一旦, furikake から離れて, シンプルな検証用のコードを書いて検証してみます.

require 'benchmark'

def run(event:, context:)
  Benchmark.bm(10) do |r|
    r.report 'require aws-sdk' do
      require 'aws-sdk'
    end
  end
end

run(event: {}, context: {})

gem は bundle init で Gemfile を生成して以下のように記載して bundle install --path vendor/bundle と実行してインストールしました.

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'aws-sdk'

以下のようなディレクトリ, ファイル構成となります.

$ tree . -L 2
.
├── Gemfile
├── Gemfile.lock
├── lambda.rb
├── template.yaml
└── vendor
    └── bundle

2 directories, 4 files

ローカル環境

以下のように, real で 1.5 秒程度掛かっていますが, 2 回目以降は 0.5 秒程度となり, 個人的に違和感はありません.

$ bundle exec ruby lambda.rb
                 user     system      total        real
require aws-sdk  0.327319   0.268804   0.596123 (  1.496711)
$ bundle exec ruby lambda.rb
                 user     system      total        real
require aws-sdk  0.296917   0.187150   0.484067 (  0.489871)
$ bundle exec ruby lambda.rb
                 user     system      total        real
require aws-sdk  0.299350   0.188319   0.487669 (  0.494612)

sam の local invoke で利用する Docker 環境

以下のようにコンテナを起動します.

$ docker run -t -i --rm -v "$PWD":/var/task lambci/lambda:build-ruby2.5 /bin/bash

以下のように 100 秒以上掛かるようになりました.

bash-4.2# ruby lambda.rb && ruby lambda.rb && ruby lambda.rb
                 user     system      total        real
require aws-sdk 27.580000   8.450000  36.030000 (119.208587)
                 user     system      total        real
require aws-sdk 25.780000   9.320000  35.100000 (105.250906)
                 user     system      total        real
require aws-sdk 24.730000   9.860000  34.590000 (102.976592)

あれ.

Lambda 環境 (sam でデプロイ)

sam を使ってデプロイしてマネジメントコンソールで確認します. タイムアウトは最大の 15 分に設定しています.

                 user     system      total        real
require aws-sdk 17.486155   1.504303  18.990458 (232.400251)
                 user     system      total        real
require aws-sdk  0.000095   0.000008   0.000103 (  0.000101)
                 user     system      total        real
require aws-sdk  0.000026   0.000002   0.000028 (  0.000025)
                 user     system      total        real
require aws-sdk  0.000026   0.000002   0.000028 (  0.000025)

初回が鬼のように時間が掛かっています. 2 回目以降は 1 回目が嘘のように速くなっていて, むしろローカル環境で実行するよりも速くなっています. どのような仕組みになっているかは分かりかねますが, gem がキャッシュに載っているんだと思います... きっと.

エントリーポイントになるコードを修正して, デプロイし直したところ, キャッシュを含む環境が破棄されて新しい環境に展開される為, 初回起動と同じくらいの起動時間を要してしまいました.

                 user     system      total        real
require aws-sdk 18.216496   1.705607  19.922103 (243.839962)
Hello.
                 user     system      total        real
require aws-sdk  0.000100   0.000010   0.000110 (  0.000107)
Hello.

なぜか, 初回起動時だけ 2 回 require 'aws-sdk' が実行されているのは...すいません. コードの問題だとおもいます.

Lambda 環境 (手動で Lambda 関数を作成)

マネジメントコンソールにて手動で Lambda 関数を作って, コードを貼り付けてテストしてみます.

                 user     system      total        real
require aws-sdk  0.264964   0.043559   0.308523 (  3.765794)
                 user     system      total        real
require aws-sdk  0.000027   0.000004   0.000031 (  0.000027)
                 user     system      total        real
require aws-sdk  0.000024   0.000004   0.000028 (  0.000026)

初回起動時は 3 秒ほど掛かっていますが, 2 回目以降は一瞬です. Ruby ランタイムには既に aws-sdk が利用可能な状態で組み込まれているようですので, 単純に aws-sdk だけを利用する Lambda 関数を実行する場合には, この程度の起動時間を要すると考えておくと良さそうです.

結果のまとめ

  • Lambda 環境では初めて関数を起動する際, サードパーティ製 gem のロードに時間を要する場合があるので気長に待つ (タイムアウトは長めに設定しておく必要がある)
  • 2 回目以降の実行は爆速, コードを変更してデプロイし直すと環境は破棄されて gem を再ロードしてしまうので...起動は遅くなる
  • sam を使ってローカル実行する場合には, 毎回 gem をロードしてしまうので注意 (そんなもんだと割り切るしかないのかな...)

以上

初回起動時間にさえ注意すれば, 多くの Ruby ライブラリを Lambda 上で動かすことができそうです!

ということで, furikake を動かせそうな気がしてきました.