[JVM][EB] Elastic BeanstalkのBlue-Green Deploymentの際に注意すべきDNSキャッシュ

はじめに

こんにちはこむろです。

Blue-Green Deploymentしてますか?ぼくは 毎月 しています( ー`дー´)キリッ

今回はElastic BeanstalkでBlue-Green Deploymentを実施した際に、CNAMEスワップして環境のELBを切り替えたにも関わらず旧環境のELBへのアクセスが止まらないという現象に遭遇したのでそちらの調査・まとめです。

EBのBlue-Green Deploymentのおさらい

まずはEBにおけるBlue-Green Deploymentをおさらいです。

EBでは同じアプリケーション内に複数環境を持つことが出来ます。アプリケーションとしてはただ一つのアドレスを持っており、各環境のCNAMEを切り替えることで、環境の切り替えが可能です。

まずは、Blue環境とGreen環境を作成します *1

Elasticbeanstalk BG Deployment_green

新しく立ち上げた環境で導通試験や機能の確認をした後にCNAMEスワップを実行。

Elasticbeanstalk BG Deployment_Blue

これでアプリケーションのアドレスが指す先がGreen環境からBlue環境へと変更されました。以降はDNSでアプリケーションのアドレスを解決した際には、Blue環境のELBアドレスが返却されるはずです。Client側は、特に変更することなく新たな環境への接続へ変更されています。

CNAMEスワップしても旧環境へのアクセスが止まらない

CNAMEスワップ後は、旧環境へのリクエストがないことを確認し縮退すればOKです。

しかし、CNAMEスワップ後、DNSの解決するアドレスも新しいBlue環境のELBアドレスが変更されているにも関わらず旧環境へのリクエストが止まりません。2時間ほど経過観察をしましたが、いずれも停止する気配はありません。どうやらどこかのアプリケーションががっつりと、旧環境のELBアドレスをキャッシュしているようです。

Play Frameworkで実装されたAPIサーバーアプリケーション

Cloudwatch Logsで集計したログからある一つのアプリケーションにたどり着きました。Play Frameworkで実装されたAPIサーバーです。サーバーを構成しているバージョンは以下の通りです。ちょっと古めですね。

  • scalaVersion := "2.11.6"
  • sbt.version=0.13.8
  • addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.6")
  • Netty 3.10.4.Final
  • jdk-8u131-linux-x64

APIサーバーの捌いているRequest数については、およそ9,000/min といったところです。EBのコンソールから新環境と旧環境のリクエスト数をモニタリングしていましたが、全くリクエスト数が減少しません。はて?何故こいつだけDNSが更新され、ELBのIPが変更されたにも関わらずリクエスト先を変更してくれないのでしょうか。

犯人はJVMのDNSキャッシュ

JVMにはDNSキャッシュの機能があります。これにより過去名前解決したIPアドレスをキャッシュしており、こちらのキャッシュが参照されることにより旧環境へのELBへRequestを出し続けていたようです。AWSではELBは都度不定期なタイミングでIPが切り替わる可能性があります。そのため、DNSキャッシュの設定を60秒以内にすることが推奨されているようです。

調べる対象は以下。

  1. JVMのデフォルト設定の設定ファイル(java.security)
  2. コード内部でTTL設定を書き換えているかを確認
  3. 設定値が存在しない場合のデフォルト動作について、Javaの中身のコードを確認してみる

JVMのデフォルト設定の設定ファイル(java.security)

OracleJDKの場合、 /usr/java/latest/jre/lib/security/java.security が対象の設定ファイルです。

#
# The Java-level namelookup cache policy for successful lookups:
#
# any negative value: caching forever
# any positive value: the number of seconds to cache an address for
# zero: do not cache
#
# default value is forever (FOREVER). For security reasons, this
# caching is made forever when a security manager is set. When a security
# manager is not set, the default behavior in this implementation
# is to cache for 30 seconds.
#
# NOTE: setting this to anything other than the default value can have
#       serious security implications. Do not set it unless
#       you are sure you are not exposed to DNS spoofing attack.
#
#networkaddress.cache.ttl=-1

# The Java-level namelookup cache policy for failed lookups:
#
# any negative value: cache forever
# any positive value: the number of seconds to cache negative lookup results
# zero: do not cache
#
# In some Microsoft Windows networking environments that employ
# the WINS name service in addition to DNS, name service lookups
# that fail may take a noticeably long time to return (approx. 5 seconds).
# For this reason the default caching policy is to maintain these
# results for 10 seconds.
#
#
networkaddress.cache.negative.ttl=10

networkaddress.cache.ttlはコメントアウトされ、 networkaddress.cache.negative.ttlのみ有効値が設定されています。いずれの設定値もNegativeの値を設定するとCacheを永久に更新しない設定になります。

ネットワークのプロパティ を確認するとそれぞれの定義が記載されています。

項目 説明
networkaddress.cache.ttl 指定する値は、成功した検索結果をキャッシュする秒数を示す整数です。
networkaddress.cache.negative.ttl 指定する値は、失敗した検索結果をキャッシュする秒数を示す整数です。

DNSの失敗した検索結果をキャッシュする 10秒 という設定値のみ適用されている状態です。

いずれもデフォルト値が記載されていませんが、コメントに記載されています。

default value is forever (FOREVER). For security reasons, this

caching is made forever when a security manager is set. When a security

manager is not set, the default behavior in this implementation

is to cache for 30 seconds.

Positive TTLのデフォルト値は FOREVER となっています。しかし、Security Managerが設定されていない場合はデフォルトの値は30秒となるようです。Play Frameworkのアプリケーションではこの状態の設定で動作していました。

コード内部でTTL設定を書き換えているかを確認

Play FrameworkアプリケーションではコードでTTLが上書き設定されていませんでしたが、別のSpring Bootで実装されたアプリケーションではTTLの値がコード上で設定されていました。以下が設定のコードです。

public class DnsCacheTtl {

    static {
        setup();
    }

    public static void setup() {
        Security.setProperty("networkaddress.cache.ttl", "15");
        Security.setProperty("networkaddress.cache.negative.ttl", "5");
    }
}

この設定をJVMのネットワーク設定が走る前に実行しておきます。

@SpringBootApplication
@EnableAspectJAutoProxy
@EnableWebMvc
public class Application {
    static {
        DnsCacheTtl.setup();
    }
    ....
}

これにより、CacheのPositive TTLとNegative TTLにそれぞれ正の値が設定されています。

設定値が存在しない場合のデフォルト動作について、Javaの中身のコードを確認してみる

調べてるうちにかなり筋道からずれたので一番最後に後述しました。

稼働しているアプリケーションの対応

恒久対応としてはDNSのCacheのTTLを適切に設定する必要があります。今回はすでに稼働しているアプリケーションをどのように対応するかを検討します。ひとまずDNSのCacheを吹き飛ばすには、JVMのプロセスごと再起動すれば良さそうです。3つほど考えました。

jvmのプロセスの再起動

起動しているインスタンスが少ないならば、直接EC2インスタンスの中に入ってしまいプロセスの再起動が一番早そうです。アプリケーションは一時的に停止しますので、アプリケーションの監視などが走っている場合は、当然アラートが発生します。問題ないように事前にネゴシエーションしておくべきでしょう。プロセスが立ち上がって正常に処理が開始されているかはなかなかわかりづらいですが、Cloudwatch LogsへインスタンスごとにGroupを切っている場合などは、そちらのログが流れていることを確認するなどすれば良さそうです。

インスタンスごと全て入れ替え

およそAPIサーバーはAutoscaling Groupに属して動作しているものが大半だと思います。そこで、Scale Out, Scale Inによって新しいインスタンスに全て入れ替えてしまいましょう。インスタンスごと入れ替えるのですから、当然アプリケーションは全て再起動します。こちらであれば一度Scale Outして正常に動作していることを確認してからScale Inで旧インスタンスを縮退するため、確実にアプリケーションの再起動ができそうです。

Elasticbeanstalk BG Deployment_Blue_EC2s

[凶悪] 問答無用で全断

取得したIPアドレスへのリクエストが不達となれば、そのCacheはもはや使い物になりません。そうなれば強制的に再度名前解決を実行するはずです。(でなければ重大な不具合)

ということで、一度サーバーへのリクエストが失敗することを許容できるならば、旧環境を問答無用でズバンと削除してしまいましょう。こうすればClient側は一度接続エラーになりますが、DNSを再度引き直し新しいELBのIPへと切り替わってくれます。

Elasticbeanstalk BG Deployment_Blue_EC2_NG

まとめ

CNAMEスワップで環境を切り替えた後、全くリクエストが減らずなかなか縮退できない状況が発生したため調査しました。今回は特にリクエスト数が非常に多いアプリケーションが1つだけ残ってしまったため、切り替え後の環境のリクエスト数と切り替え前の環境のリクエスト数がほぼ同数になっており、なかなか状況が読めませんでした。JVMのDNSキャッシュというインフラともアプリケーション側とも言えない境界での設定の影響でした。

結局、今回もまずは切り替え前の環境のELB及びインスタンスのログを確認し、どのクライアントがアクセスしているか、どのエンドポイントへのアクセスが実行されているのかを把握しました。いずれも何はともあれログを確認するのが解析の第一歩です。ログの存在しないアプリケーションの場合はこうはいきません *2。適切なログを出力しましょう。

JVMのDNS Cacheについては、なかなかアプリケーションの実装者は気づかない機能です。

安定した運用、DevOpsを実践するにはインフラ、アプリケーションの知識だけではなく、実行環境などのミドルウェアの知識もまだまだ修行が足りませんでした。精進します。

おまけ

以下は完全に興味の範疇で調査したものです。少々冗長な上結論まで行き着いていないので、面倒な場合は読み飛ばして問題ない内容です。

OpenJDKのコードで確認してみました。

OpenJDK/jdk8/jdk8/jdk - view src/share/classes/sun/net/InetAddressCachePolicy.java @ 9107:687fd7c7986d

116行目を確認すると以下のようなコメントが記載されています。

/* No properties defined for positive caching. If there is no
 * security manager then use the default positive cache value.
 */
if (System.getSecurityManager() == null) {
    cachePolicy = DEFAULT_POSITIVE;
}

PositiveTLL Cacheのプロパティが存在せず、Security Managerも存在しない場合は、デフォルトのPositive Cacheの値が適用される

と読めます。 DEFAULT_POSITIVE の値はこのクラスの上部に定義されており30秒です。

/* default value for positive lookups */
public static final int DEFAULT_POSITIVE = 30;

まず通常の権限でPropertyを読み、設定値の取得を試みます。その後、パラメータがなければ特権ブロックで同じProperty値の取得を試みているようです。

tmp = java.security.AccessController.doPrivileged
    (new sun.security.action.GetIntegerAction(cachePolicyPropFallback));
if (tmp != null) {
    cachePolicy = tmp.intValue();
    if (cachePolicy < 0) {
        cachePolicy = FOREVER;
    }
    propertySet = true;

java.security.AccessController.doPrivilegedこちらOracleのドキュメントを参照しました。特権ブロックでライブラリにアクセスするためのAPIとのことです。

GetIntegerAction ではコンストラクタ引数に文字列が入力できます。以下のProperty名が入力されています。

private static final String cachePolicyPropFallback =
        "sun.net.inetaddr.ttl";

GetIntegerActionは PrivilegedAction インタフェースが実装されているため以下でメソッドがOverrideされていました。

public Integer run() {
    Integer value = Integer.getInteger(theProp);
    if ((value == null) && defaultSet)
        return new Integer(defaultVal);
    return value;
}

うーむ、しかしなぜPositive TTLのデフォルト値がFOREVERなのかは最後まで分からず仕舞いでした。Security Managerによって裏で特権ブロックでアクセスした際に設定値が取得できる何かが適用されているような気がします。

(Thank you support by @wreulicke)

参照

脚注

  1. なぜGreen環境からBlue環境へかって?すでにGreen環境へ一度変更を実施しているためです。
  2. そんなものあってたまるか