バイセル Tech Blog

バイセル Tech Blogは株式会社BuySellTechnologiesのエンジニア達が知見・発見を共有する技術ブログです。

AWS LambdaとRDS Proxyとコネクションプールの話

はじめに

テクノロジー戦略本部で在庫管理システムを担当している長兵衛です。

いきなりですが、最近間違ったことを発信してしまったらどうしようと尻込みしてあまりアウトプットができていませんでした。

失敗を恐れて何もしないより、どんどん失敗して学んだ方がいいと自分に言い聞かせて書きはじめています。

そのため半分自己満足なのですが、内容は有益だと思って書いているのでご容赦ください!

では本題に入ります。

概要

以前、新卒がサーバレスアーキテクチャに挑戦してみたという記事を書きました。

今回はこのRuby on Jets環境での「AWS Lambda-Amazon RDS Proxy間のコネクション接続/切断」、「Amazon RDS Proxy-Amazon Aurora PostgreSQL間のコネクション使いまわし」に着目していきたいと思います。

環境

  • Ruby on Jets 2.3.18
    • ruby 2.5.7
    • ActiveRecord 6.0.3
  • Amazon Aurora PostgreSQL 11.6
  • Amazon RDS Proxy

Lambda-RDS Proxy間で発生するピン留めについて

ピン留めの原因

公式ドキュメントに書いていますが、SET ステートメントやプリペアドステートメント等の原因により起こってしまう現象で、RDS Proxy-DB間のコネクションをclientが専有してしまいます。

これによって、RDS Proxyの最大のメリットであるコネクションプーリング機能が使われにくくなってしまいます。

詳しくは図で説明したいと思います。

発生する問題

例としてLambda実行後にAというコネクションがピン留め状態で残っている場合、

コネクションBはAが使用したRDS Proxy-Aurora間のコネクションを使い回すことができません。

f:id:bst-tech:20210423174534p:plain

今回やりたいこととしては、Lambda実行後にRDS Proxyとのコネクションを切断することで、コネクションBが「Aが使っていたコネクション」を使い回せるようにしたいと思います。

f:id:bst-tech:20210423174554p:plain

ピン留めを解決できなかった

RDS Proxyログを見ると、Ruby on JetsとのコネクションのSET client_encordingが原因となってピン留めが起きていました。

f:id:bst-tech:20210423174702p:plain

はじめは公式ドキュメントを参考にSETステートメントをRDS Proxyの初期化クエリに移そうとしてみましたが、SETステートメントを指定しない方法が見つからなかったため、下記のような対策を取りました。

Lambda function実行後にコネクションを切断してみる

Ruby on JetsでもActiveRecordが使用されていて、Railsと同様にコネクションプーリング機能が搭載されています。しかし、コネクションプーリングはRDS Proxyが担うため、function実行後に切断したいというのが今回の目的となっています。

ActiveRecordの挙動を確認

Ruby on Railsと同様にRuby on Jetsでもinitializerでestablish_connectionが実行されます

これによってconnetion_poolが作成された状態になり、SQL発行時にActiveRecord::Base.connectionにより、connection_poolに紐付いたconnectionを作成します。

以下の方法で確認してみました。

$ bundle exec jets c

# この時点でestablish_connectionが実行されている
# コネクションプールが作成されていることが確認できる
irb(main):023:0> ActiveRecord::Base.connection_pool
=> #<ActiveRecord::ConnectionAdapters::ConnectionPool:0x00005558909eb720
@mon_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x00005558909eb5b8>,
@query_cache_enabled=#<Concurrent...

# しかし、connectionはまだ貼られていない
irb(main):023:0> ActiveRecord::Base.connected?     
=> false

# connection_poolに紐付いたconnectionを確立
irb(main):027:0> ActiveRecord::Base.connection                        
=> #<ActiveRecord::ConnectionAdapters::PostgreSQLAdapter:0x0000555893f02f80
@transaction_manager=#<ActiveRecord::ConnectionAdapters::TransactionManager:0x0000555893f29b58
@stack=[], @connection=#<ActiveRecord::ConnectionAdapters::PostgreSQLAdapter:0x0000555893f02f80 ...>,
@has_unmaterialized_transacti...

# connection確認
irb(main):032:0> ActiveRecord::Base.connection_pool.active_connection?
=> #<ActiveRecord::ConnectionAdapters::PostgreSQLAdapter:0x0000555893f02f80 @transaction_manager
=#<ActiveRecord::ConnectionAdapters::TransactionManager:0x0000555893f29b58 @stack=[], 
@connection=#<ActiveRecord::ConnectionAdapters::PostgreSQLAdapter:0x0000555893f02f80 ...>,
...
irb(main):033:0> ActiveRecord::Base.connected?                        
=> true

ここで、connection_poolの状態を確認します。

# connectionが1つ使われているのがわかります
irb(main):042:0> ActiveRecord::Base.connection_pool.stat
=> {:size=>1, :connections=>1, :busy=>1, :dead=>0, :idle=>0, :waiting=>0, :checkout_timeout=>5}

connectionを切断してみる

connectionを切断するためにremove_connectionを実行してみます

irb(main):034:0> ActiveRecord::Base.remove_connection
=> {:adapter=>"postgresql", :encoding=>"utf8", ... }

すると、connection_pool毎削除してしまうみたいです。

# establish_connectionしてくださいというエラーが出る
irb(main):036:0> ActiveRecord::Base.connection_pool
...
ActiveRecord::ConnectionNotEstablished (ActiveRecord::ConnectionNotEstablished)
# 接続は消えた
irb(main):037:0> ActiveRecord::Base.connected?
=> nil

ここで問題になるのが、connection_poolを残したままにしないと、

コンテナが再利用されて以下のコードをActiveRecordが実行した際にconnection_poolが無いと怒られてしまうことです。

irb(main):044:0> ActiveRecord::Base.connection
...
ActiveRecord::ConnectionNotEstablished (No connection pool with 'primary' found.)

なんとかconnection_poolを残したままコネクションを切断する方法がないか模索してみました。

disconnectというメソッドが用意されていたので実行してみます。

# コネクションプールからコネクションを解放する
irb(main):046:0> ActiveRecord::Base.connection_pool.disconnect
=> []

すると、connection_poolが残りました!connectionは切断されています。

irb(main):047:0> ActiveRecord::Base.connection_pool           
=> #<ActiveRecord::ConnectionAdapters::ConnectionPool:0x000055588f7a0de0 
@mon_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x000055588f7a0d18>,
@query_cache_enabled=#<Concurrent::Map:0x000055588f7a0c78 entries=0
...
 
# connectionが切断されています
irb(main):049:0> ActiveRecord::Base.connection_pool.stat
=> {:size=>1, :connections=>0, :busy=>0, :dead=>0, :idle=>0, :waiting=>0, :checkout_timeout=>5}

irb(main):048:0> ActiveRecord::Base.connected?     
=> false

after_actionで実装

特定のリクエストの最後にコネクションを切断したいので、今回はafter_actionで実装してみました!

class ApplicationController < Jets::Controller::Base
  after_action :disconnect

    ...

    private

    def disconnect
    ActiveRecord::Base.connection_pool.disconnect
  end
end

Amazon CloudWatch Logsで検証

修正前

ログ

f:id:bst-tech:20210423174753p:plain

RDS Proxy-Aurora間コネクション数

f:id:bst-tech:20210423174815p:plain

リクエスト毎に確立されていっているのがわかります

修正後

ログ

f:id:bst-tech:20210423174928p:plain

コネクションを使い回すことができているのが確認できました!

RDS Proxy-Aurora間コネクション数

f:id:bst-tech:20210423175013p:plain

コネクションを使い回して接続の確立を抑えることができています。

まとめ

ピン留めが解決できない場合は、Lambda実行後にRDS Proxyへのコネクションを切断する方法が有効だと思います。

コンテナが再利用された場合のオーバーヘッドがかかってしまいますが、それ以外はデメリットがあまりないように感じます。

最後に、BuySell Technologiesではエンジニアを募集しております!

気になる方は是非お問い合わせください!