平常運転

アニソンが好き

過去記事とかは記事一覧で見れます

JVM の DNS キャッシュを制御する

JVM (Java 仮想マシン) には DNS の名前解決の結果をキャッシュする挙動が備わっている。キャッシュするだけならいいのだけれど、このキャッシュでは DNS の TTL を無視してキャッシュするため、名前解決の結果が変わっても JVM からの接続先が切り替わるまでに(TTL から想定される時間以上に)時間がかかる、あるいは全く切り替わらないということがある。この挙動やその制御について調べたので、その話をする。
(以下の話題では Oracle JDK および OpenJDK を対象にして論じるので、それ以外の JVM 実装でどうなってるかは調べていない。適用できる箇所もあればそうでない箇所もありそう)

背景・解説

これらのデフォルト値は名前解決成功時は セキュリティーマネージャーがインストールされている場合のデフォルト値は -1 (ずっと) で、セキュリティーマネージャーがインストールされていない場合は実装固有 、失敗時は デフォルト: 10秒
(いずれも Oracle Java SE 7 のドキュメントより)となっており、成功時の "実装固有" の値は OpenJDK の実装では30秒となっており、 Oracle JDK でも後述の設定ファイルのコメントを見る限り30秒となっている。つまり、セキュリティマネージャが無効になっている環境を前提にすると、この JVM の DNS キャッシュは以下のような挙動をする:

  • 名前解決に成功したとき、その結果をDNS レコードの TTL に関係なく JVM プロセス内で30秒キャッシュする
  • 名前解決に失敗したとき、その結果をDNS 問い合わせの SOA TTL に関係なく JVM プロセス内で10秒キャッシュする

この挙動については以下のページの解説が詳しいのでそちらに譲る:

Java/Socket, InetAddressにおけるDNS名前解決の仕組みと networkaddress.cache.ttl - Glamenv-Septzen.net

上記はセキュリティマネージャが無効な環境を例示したけれど、有効な場合は positive cache はプロセス内で永遠に保持されて expire しない。すなわち一度名前解決に成功すると、その後 DNS レコードに変更があっても全く反映されないことになる。

このキャッシュ機構は DNS ポイズニングへの対策を意図しているようだけど、特に AWS などのドメイン名でアドレスが提供されており DNS TTL の短いネットワークリソースに Java から接続している場合に大きな問題となる。たとえばフェイルオーバーが DNS ベースで行われる Amazon RDS (など)を利用する場合、フェイルオーバーから復旧までのダウンタイムがこのキャッシュ寿命の分伸びるということになる。特に AWS Aurora は DNS TTL が5秒とかになっているので、これを30秒キャッシュするのは直感的にもあまり筋が良いとは思えない。勿論永遠に保持されると惨劇が起きる。

キャッシュを完全に無効にすると getnamebyaddr の発行コストによりパフォーマンスが低下することも想像されるが(パフォーマンス検証はしていない)、その観点を気にする場合でもせいぜい数秒程度キャッシュすれば十分であろうとは予想できる。

キャッシュ機構の制御手法

ここからが本題。この挙動をカスタマイズする方法自体はいくつか存在している。以下の Qiita エントリで手法が解説されており、手法そのものはそのエントリ通りでよいと思うけれど軽く紹介しておき、ぼくの手元で検証や検討した際の所感を記す。

qiita.com

$JAVA_HOME/lib/security/java.security に記述

セキュリティポリシーファイルの中の networkaddress.cache.ttl と networkaddress.cache.negative.ttl を書き換えるという方法。Java8 の Oracle JDK だと実体は /etc/java-8-oracle/security/java.security になりそう。
この設定値を変更すると同じホスト内で動く他の JVM プロセスにも影響してしまうのだけれど、これが問題になるのは同一ホスト内で異なる複数の Java アプリケーションを動かしており、かつそれらの DNS キャッシュ TTL 要件が異なる場合くらいであり、あまりそういうことはないだろうと思っている。

java.security.Security.setProperty() で指定

アプリケーションコード内部で Security.setProperty を発行して値を書き換える物。
当然アプリケーションコードに触れる局面でしか使えないので Elasticsearch などのミドルウェアの制御には使えない。逆に自前で開発しているアプリケーションの場合、コードレイヤで制御できるので一見手を出したくなるが、個人的にはこの手法はハマりやすいので可能な限り避けたいと考えている。
Qiita のエントリにもあるように、この方法を使う場合は InetAddressCachePolicy クラスがどこかでロードされるより先のタイミングで値をセットする必要がある。main メソッドのあるエントリーポイントが露出しているアプリケーションではまだ可能かもしれないが、アプリケーションフレームワークに乗っているアプリケーションの場合はそのような自明なエントリーポイントが存在しない場合があり、フレームワーク内部でのロード順序に依存するのは非常にスリリングだと思う。
また、これは他の方法にも共通するのだけど、特にセキュリティマネージャ無効時の30秒キャッシュはアプリケーションの表面的な挙動だけ見ていても設定漏れに気付きづらいという問題がある。初期設定時に正しく設定できていたとして、例えば上述のようなフレームワーク内部のロード順序が変わって正しく設定できなくなった際にそれを検知するのは非常に難しい。

起動オプションに指定

java プロセスの起動オプションに-Dsun.net.inetaddr.ttl=XXX 、あるいは-Dsun.net.inetaddr.negative.ttl=YYYを指定するもの。これはThese properties may not be supported in future releases.とドキュメントされており、(現実的にはまだしばらく使えるだろうけれど)新規の利用が推奨される物ではない。逆に言えば JDK のバージョンアップをしないと決めている環境であれば選択肢に上がるかもしれない。


追記

JVM の DNS キャッシュを制御する - 平常運転

-Djava.security.manager -Djava.security.policy= JVM起動時オプションはdeprecatedじゃないよね? cf, <a href="http://bit.ly/2tjBmlY" target="_blank" rel="noopener nofollow">http://bit.ly/2tjBmlY</a>

2018/03/06 15:49
b.hatena.ne.jp

非推奨になっているのは、 sun.net.inetaddr.ttl および sun.net.inetaddr.negative.ttl パラメータです。 java プロセスに -D オプションを渡すこと自体は問題ありません。確かに誤解を招きかねない文章だったのでここで補足します。


ここで一つ言及しておきたいのだけれど、Security.setProperty() や java.security ファイルでの設定項目である networkaddress.cache.ttl はこの起動オプション経由で渡すことはできない。これはドキュメントにも記載されているのだけど、インターネット上で可能と言及している言説があったので注意しておきたい。ちなみに起動オプションで渡しても特にエラーは出ずサイレントに無視される。まあそれはそう。

これらの 2 つのプロパティーは、セキュリティーポリシーの一部であるため、-D オプションや System.setProperty() API では設定されません。その代わり、これらのプロパティーは JRE のセキュリティーポリシーファイル lib/security/java.security で設定されます。

https://docs.oracle.com/javase/jp/7/api/java/net/doc-files/net-properties.html

手法の比較

上のセクションでも書いたのだけれど、この挙動の制御において難しいのは、たとえ30秒の DNS キャッシュが有効になっていても、定常時の挙動からはそれを確認しづらいというところにある。そのことを踏まえると、将来的な改修時に壊れにくい選択肢を取るのが重要であると思う。
その観点から考えると、 jvm 起動オプションに deprecated なオプションを渡すのは極力避けたい選択肢に見える。これは明らかに将来の JDK バージョンアップ時のリスクとなる。将来の JDK がこのパラメータ設定に対してエラーを返してくれるならまだしも、サイレントに無視するような変更になった場合に制御できなくなっていることに気付かない可能性が高い。
また、アプリケーションコード内の設定については上のセクションで紹介した時点で注意点について書いた。クラスのロード順序に左右される物であるから個人的にはあまり採用したいと思っていないが、セキュリティポリシーファイルを操作しづらい環境であればこちらも選択肢になるのかもしれない。

ということでやや我田引水的な理屈の展開であるけれど、個人的にはセキュリティポリシーファイルを書き換えるのが望ましいように思う。アプリケーションコードではなくてサーバ側の構成管理で担保する話題になるけれど、現代であれば Chef なり ansible なりでサーバをプロビジョニングしているだろうからその手順の中に含めておけば十分再現可能でしょうと思う。あるいはコンテナの中で jvm を動かすのなら Dockerfile できちんと担保できる。

結論

結論というほどでもないけれど、本件に関するぼく個人の見解のまとめは以下の通り:

  • JVM の DNS キャッシュ機構の TTL はクラウド時代の DNS ベースの仕組みとは最早マッチせず、カスタマイズするとよい
  • アプリケーションコード内部でのカスタマイズはハマりやすく、また java 起動パラメータは現代では推奨されない
  • よって、セキュリティポリシーファイルを(構成管理した上で)書き換えるのが安全なように思う

リンク集

DNS キャッシュ以外も含めたパラメータチューニングの話題。目を通しておくとよさそう。
moznion.hatenadiary.com

余談: 挙動の調査

ところで、これらの挙動の調査に関しては、対象の java プロセスの存在するサーバ上に DNS キャッシュサーバ (unbound) を立て、キャッシュサーバへの DNS クエリ数を Mackerel でモニタリングしながら行った。今仕事で触っているプロジェクトでは別件の事情でアプリケーションサーバ内部に元々 DNS キャッシュサーバが同居しているので手間を掛けずにこれらのパラメータ検証ができたけれど、こういう取り組みのない場合は tcpdump したりパケットキャプチャして様子を見ることになるであろう。手慣れてる人はそれでもいいんだろうけれど、個人的には unbound が同居していて助かったという気持ちになっている。