作ってみたきっかけ
2019年5月3日 AM5:00ごろから、Azureの各種サービスのホスト名を解決するDNSサーバーで設定ミスがあって、BLOBストレージや、SQLなど各種サービスのエンドポイントが名前解決できないという問題が発生した。
発生したことは公式のサービスステータス履歴にも上がっているし、いくつかのメディアでも取り上げられている。
https://www.publickey1.jp/blog/19/microsoft_azuredns.html
https://www.itmedia.co.jp/news/articles/1905/09/news084.html
幸いにして日本は10連休の真っ只中かつ、早朝に発生したことから、起きたことのわりにビジネス的なインパクトは小さかったようで、そこまでの大問題として扱われていないようだ。
DNSをサービスディスカバリとして使うことの問題点
分散DBとしてのDNSは、メールやWebなどIPアドレスの変更が極めて稀な安定したユースケースにおいて、長めのTTLを前提として運用されてきた。
このためDNSサーバーの設定変更、ゾーンファイルの編集によって誤ったIPアドレスをAレコードに登録してしまったようなミスがあると、どこかでキャッシュされてしまい丸一日は反映されないようなことも(たまに)発生してしまっていた。
このDNSによるホスト名解決の仕組みが、インターネットで提供されるAPIエンドポイントのホスト名としての利用されるようになると、DNSの仕組みが持つ鷹揚さが逆にシステム全体の安定稼働にとって問題となる。
最近では、解決するIPアドレスを変えることによってHAが実現されているためDNSのTTLには短い時間が設定されるケースが多いが、ネガティブキャッシュの設定はまた別だったりするので、今回のようにクラウドプロバイダや、アプリをホストしている環境の上位DNSがやらかすと、自分のサービス全体が機能停止に至るようなこともあり得るのである。
SREの観点では、こういう事態においてもシステムが稼働し続けなければならず、システムの内部にDNSの名前解決が必要となるアプリケーションでは、名前解決の仕組み自体に何らかの対策が求められる。
方式1: dnsmasqでローカルキャッシュを構築する
ローカルで動かすことができるDNSのキャッシュサーバーとして、dnsmasqというものがある。一度解決したホスト名をローカルにキャッシュしておくことで、今回のようなトラブル時にもローカルで動くアプリケーションは稼働しつづけるシステムを構築することができる。
この方式には問題があり、dnsmasqでTTLを無視した独自のキャッシュを導入すると、HAの切り替わりのようなIPアドレスが変更になるDNSの変更がいつまでたっても反映されなくなってしまう。
我々が使っているような Azure MySQL for Database を運用していると、わりと頻繁に(数ヶ月に一度程度)IPアドレスの変更が起きるのでこれでは解決にならない。
また、なるべくメンテナンスする対象を少なくしたい観点からもこの方式は避けたい。
方式2: getaddrinfo(3)にキャッシュの仕組みを導入する
サーバープロセスのように長時間稼働しつづけるサービスの場合、DNS名の解決に失敗したとき、前回に成功したIPアドレスを使ってそのまま接続してみるという挙動をしてほしい。
そして、glibc/Linuxの環境では、動作時にglibcで提供されるgetaddrinfo(3)を、自分の実装で横取りする仕掛けを作ることができる。
これをつかってDNS解決に失敗しても前回の結果を返すように実装した getaddrinfo(3) が、libsaferesolv.so (https://git.io/fjWaX) である。
Dockerのようなコンテナの中で動かすことを想定しており、どのようなインフラの構成であってもコンテナ内で動くアプリをDNS障害からガードすることができる。
getaddrinfo(3)を置き換えることで対応しているため、あらゆる言語・ライブラリに対応しているのが肝である。
例: libsaferesolv.so を用いたJavaアプリをガードしてみる
glibc/Linux環境において、libsaferesolv.so をビルドしてそのバイナリが、lib/libsaferesolv.so として存在するとき、以下のようにコンテナをビルドするとコンテナ内で起動するJVM環境のホスト名解決はすべてガードされた状態となる。
FROM openjdk:8-jre-slim
EXPOSE 8080
# 省略
ADD lib/libsaferesolv.so /usr/local/lib/libsaferesolv.so
ENV LD_PRELOAD /usr/local/lib/libsaferesolv.so
# 省略
ADD target/app.jar /app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Dnetworkaddress.cache.ttl=0", "-Dnetworkaddress.cache.negative.ttl=0", "-jar", "/app.jar"]
LD_PRELOADは、glibcの仕組みなので、Alpine Linuxのような軽量なイメージで使うことができないのが残念なところ。
今後について
この仕組み。glibcを弄るというアプリ屋からするとかなり心理的障壁が高い方式のため、実は我々の本番環境にはまだ投入できていない。検証してうまくいきそうだという雰囲気を得ただけである。
真のSRE(Site Reliability Engineer)によるクリティカルな批評がもらえて自信が持てれば投入したい。