Sake駆動開発

エンジニアリングやガジェットのことなど

AWS Batch + Aurora をスケールアップしてたら DB への接続で communication link failure が発生するようになった。

こんな記事書いてますが私はインフラエンジニアではなくバックエンド経験1年ちょっとの元スマホアプリエンジニア(swift/kotlin)です。 AWSは現職にて初めて触っております。環境が変われば何でも自分でやるですね。

経緯

弊社では特別な調査向けに AWS Batch + Aurora + Lambda で、調査の規模毎にスケーラブルな環境を構築して使っています。

嬉しいことに、調査数の増加と既存調査タスクのリプレース(この辺は別途記事にしたいが一年くらい書けてない)も進み、段々とシステムが大きくなってきました。 アプリケーション側こそスケーラブルですが、スケールアップして増殖した調査タスクたちがこぞってつなぎに行くのがスケールしないDBです。 案の定、たまたま同時に動いた複数の調査タスクたちがこぞってつなぎに行った結果 Connection が枯渇してしまいました。

DBのスケールアップを行ったら長い戦いのもととなるエラーと邂逅した。

調査タスクが全て同時に動いた時を想定して、倍以上のメモリを持つインスタンスにスケールアップすることにしました。 スケールアップ後は順調に動くかと思いきや、バタバタと調査タスクが死ぬではないですか。

急いでログを確認すると、そこにいたのが DBとの接続ができないという「 Communications link failure 」でした。そして私達はこの原因不明のエラーと長い戦いを演じることになります。

試した対策

Communications link failure と出されてまず思い浮かぶのはそのまま RDS との接続切れです。接続が切れる原因として思いつくのは先ほどもありましたが Connection の枯渇や、アプリケーション側の接続設定などがあります。

RDS 側の負荷が原因ではない

まっさきに疑ったのが、RDSの負荷が高くて接続が切られたことです。 しかし、スケールアップしたばかりのRDSはまったくもって余裕をもっていました。強いて言うならCPUが60〜70%程度まで上がっていたくらいです。 いままで使用率90%に張り付いても正常に動いていたので、これが原因とはにわかには考えられませんでした。 MaxConnectionも90→1000に増えていて、ピーク時の接続は200程度だったので、ここが問題とは思えません。

アプリケーション側 ( Spring Boot ) の設定を疑った

アプリケーション側にもConnectionの設定があります。 Springでは HikariCP を使用していたので、application.yml に設定していた以下の値を確認しました。

spring:
  datasource:
    username: hoge
    password: pass
    hikari:
      connection-test-query: SELECT 1
      minimum-idle: 5 # 最小connection数
      maximum-pool-size: 10 # 最大connection数
  • connection-test-query

Connectionの接続チェックを行い、timeoutしないようにします。

  • maximum-pool-size

コネクションプールの最大値。いくつConnectinonを保持できるか。ここの値が低いと沢山のconnectionをはろうとしたときに落ちてしまいます。

確認の結果、十分な値が設定されていた。

そもそも、この値が十分でないと Too Many Connection Exception みたいのが出るはず。

AWS Batch Job Definition で Ulimits を設定する

この辺から全くわからず、先輩エンジニアの助けを借りました。 Ulimits でファイル操作のためのディスクリプタ上限を管理しているそうです。なぜファイルディスクリプタなのかというと、 Unix システムはディスクファイルだけではなくて TCP のソケットもファイルとして取り扱う そうで、DBへの接続数が増えるとこの上限に引っかかるそうです。

AWS::Batch::JobDefinition Ulimit - AWS CloudFormation

この辺を見ながら設定して上限を上げました。

しかし、変化なし。敗れる。

根本的な原因

問題その1 TCP ソケットが枯渇していた

はい、ここも全くわからず、弊社先輩エンジニアにより発見に至りました。 アプリケーション側で使用しているEC2インスタンス内では DB への接続のためにエフェメラルポートが割り当てられますが、それが使用後 TIME-WAIT となりしばらく残っているそうです。

そしてこれが残っているとどうなるか?残っているポートは再利用されずにどんどん新規にポートが開かれます。このエフェメラルポートの数にも上限があり、結果ポートが枯渇するのです。

これはDBへの高負荷をさばくチューニングとしては結構一般的なことだそうで、、知識不足で不甲斐ない。

問題その2 高速化を進めた結果の高負荷

こちらは、異なる調査タスク間である処理を同時に実行すると必ず落ちることから気づきました。 ファイル読み込みや、データの紐付け処理を非同期で実行しているのですが、それがあまりにも高速なため短時間で大量のデータアクセスを行うことになっていました。もちろんデータアクセスが増えるのを見越してまとめて更新するようになっていたのですが、それを上回ってしまいました。

この短時間での大量アクセスにより、問題1でも書いた エフェメラルポート の枯渇が置きていた。ということです。

負荷テストでは起きなかったのはなぜか?

もちろん我々も負荷テストは行いました。これの不思議なところは、負荷テストでは発生しなかったことです。疑問に思ったのですが、AWS Batch のコンピューティング環境が関係しています。

AWS Batch はJOBが動くために必要なコンピューティングリソース(EC2インスタンス)を都度確保して、そのインスタンス上でDocker Container として動きます。(設定で必要な分を常駐させることもできます) なので確保したEC2 インスタンスの性能が十分であれば、同一インスタンス上でJOB が2つ動くこともあり得るということです。

f:id:Nabesuke_00:20191215224738p:plain

つまり、負荷の高いJOBがたまたま同一インスタンス上で実行されたときに発生するものだった。

たまたま負荷テストでは上記の条件が発生しなかったため発生しなかっただけでした。奥が深い。

Bugfix

アプリケーション側はDBへの接続を最小限にするようにチューニングを行いました。 TCP エフェメラルポート の枯渇に対してですが、カーネルパラメータの変更が必要なため以下の方法を取りました。

起動テンプレートを使って AWS Batch でカーネルパラメータを変更する

普通のEC2インスタンスと違って AWS Batch は調査タスクの負荷によってスケールアップするので、通常の手順では設定できません。 AWS Batch ではコンピューティングリソースの設定で 起動テンプレートを指定できます。 AWS Batch のJOB を実行するために確保したEC2インスタンスを起動するときに、設定した起動テンプレートを参照してくれるというものです。

起動テンプレートのサポート - AWS Batch

そして、同様に AWS Batch でパラメーターをチューニングしている方がいました。

Dockerホストのパフォーマンスを引き出すTCPカーネルパラメータチューニング · tehepero note(・ω<) 2.0

実を言うと弊社ではまだ設定に成功していません(常駐させているコンピューティングリソースに潜って直接設定してしまいました)が、引き続き設定を調整しているところです。 今後設定に成功したら更新します。

解決に1週間近くかかった

原因のようなものを調べては トライ&エラーを繰り返しつづけて気づけば一週間近くが費やされていました。その間エラーの原因についてあれこれと考えたのですが、自分はアプリケーション側には強いんですがインフラは全くダメダメでした。せいぜいスケールアップしたりConnectionを疑って見る程度。 Ulimitsやエフェメラルポートの枯渇については全く思いつきませんでした。 自身の知識不足を補うために勉強するのはもちろんですが、エンジニア同士で知識を共有し合うのがとても大事だなと思いました。古いつながりやチームの先輩エンジニアに助けられました。

一人でできないことも、皆でやれば解決できる。チーム開発の醍醐味を味わえたのは幸いでした。もちろん不具合を出さないようにする、不具合をすぐ直せるようにするというのは大前提です。

この記事は後ほど対応内容だけをまとめて qiita にも載せます。