Webオペレーションエンジニアの id:y_uuki です。
2017年8月7日に、メンテナンスの完了報告及びデータ消失とカスタムダッシュボード、式監視の不具合に関するお詫びにてお知らせしたメンテナンス作業時間中のデータ消失について、本エントリにて技術的な観点から原因の詳細をお伝えいたします。
概要
2017年8月7日(日本時間)に、オンプレミスデータセンターからAWSへ、Mackerelをシステム移行するためのメンテナンスを実施しました。 メンテナンス開始時間である14:30以降のデータ同期に失敗していたPostgreSQLデータベースサーバへの意図しないフェイルオーバーが、メンテナンス作業途中の15:30に発生した結果、14:30から15:30の間に更新されたデータを消失しました。
移行作業後のアプリケーションの動作確認中に、特定時間帯のデータを消失していることを発見し、データの復旧を試みました。 しかし、フェイルオーバー元のデータベースサーバとフェイルオーバー先のデータベースサーバのデータを突き合わせて、無理にデータを整合させようとすると、別の箇所でのデータ不整合を招く可能性があったため、消失したデータの復旧を断念しました。
対応については、フェイルオーバー後に、データベースクラスタを再構築し、フェイルオーバーしたとしてもデータ同期が失敗することはない状態にしました。
データの消失は次の2つの原因から発生しました。1点目は、移行先のデータベースサーバへ接続を切り替えた1時間後に、該当データベースサーバとのネットワーク疎通がとれなくなり、スタンバイサーバへの意図せぬフェイルオーバーが発生したことです。 2点目は、フェイルオーバー先のスタンバイサーバについて、移行作業時のみ必要であったレプリケーションに関する設定を忘れてしまうというヒューマンエラーがあり、移行作業中にスタンバイサーバのレプリケーションが意図せず停止し、データ同期に失敗していたことです。
本来は、想定していなかったデータの消失を発生させたことについて、お客様にご迷惑をおかけして大変申し訳ありません。 移行作業中において、移行元と移行先の環境が混在していたために発生した問題のため、移行が完了した現在では、同様の原因でデータの消失は発生しない状態になっております。
障害の原因と対応
ネットワーク不通について
該当データベースサーバに対して、なぜネットワーク疎通がとれなくなったのか、はっきりとした原因はつかめていません。 原因の可能性として、過負荷によるLinuxカーネルのネットワークスタック、ディスクI/Oスタックの飽和、またはEC2インスタンスのハイパーバイザー側の問題ではないかと考え、調査をしました。
後者の可能性について、EC2インスタンスのハイパーバイザーの状態を弊社側からは調査できないため、AWSのサポートチケットに起票したところ、問題となる記録を確認することはできなかったとの回答をいただきました。 具体的に、サポートチケットでは、該当EC2インスタンスおよび該当インスタンスに紐づくEBSボリュームとENI、ネットワークインフラストラクチャなどAWS基盤側の調査を実施していただきました。
前者の可能性について、該当インスタンスを再起動させ、弊社側でsyslog、dmesg、PostgreSQLのサーバプロセスのログを確認しましたが、今回のネットワーク不通と関係するようなログを発見することはできませんでした。 さらに、MackerelとAmazon CloudWatchで取得していたCPU利用率などのサーバメトリックを確認しました。その結果、CloudWatchにてネットワーク不通後にCPU利用率が100%になっていることとStatusCheckFailedが1になっていることを確認しましたが、それ以上の原因追求は難しい状態でした。
参考のため、該当時間帯のCPU利用率のグラフを下記に示します。Mackerelでは15:30ごろにインスタンス内部との疎通がとれなくなったことがわかりますが、ネットワーク不通直前に負荷傾向に変化はありませんでした。これは、他のメトリックについても同様でした。CloudWatchでは、15:20ごろからCPU利用率が上昇し、100%に張り付いた状態になっていました。
また、参考のためデータベースサーバとして使用したインスタンスタイプとソフトウェアの情報を以下に記載します。
- EC2インスタンスタイプ: c4.8xlarge
- EBSボリュームタイプ: 汎用SSD(gp2) EBS最適化有効
- OSバージョン: Linux 3.16, Debian 8
- PostgreSQLバージョン: 9.3
意図しないレプリケーションの停止について
最初に、移行作業内容と移行中に発生した事象を説明します。その後、なぜ意図しないレプリケーションの停止が発生したのかを説明します。
今回の移行におけるデータベースサーバのクラスタの状態を表した簡易図を以下に示します。
矢印はレプリケーションによるデータの流れを示しています。レプリケーション手法として、PostgreSQL標準のストリーミングレプリケーションを利用しています。 db001はdb000のスレーブ、db002はdb001のスレーブつまりdb000の孫スレーブとなっています。
移行作業内容は、db000への接続を遮断し、その間にdb001をマスターとして昇格させ、接続先をdb001へ切り替えるというものでした。そして、db001をマスター昇格させた際に、db001からdb002へのレプリケーションが停止しました。 その後、前述のネットワーク不通により、レプリケーションが停止したdb002へとフェイルオーバした結果、db001をマスター昇格した後からdb001が不通になるまでのデータが同期されないまま、db002へ更新クエリが実行されました。
意図せずレプリケーションが停止した原因は、db002へのレプリケーション開始時に、PostgreSQLのタイムラインIDの変更に対して追従するためのrecovery_target_timeline
という項目を設定し忘れていたことです。
PostgreSQLでは、マスター昇格した際にタイムラインIDがインクリメントされます。レプリケーションのためにスレーブへ転送するWALにはタイムラインIDが含まれており、PostgreSQL 9.3のデフォルト設定では、マスターのタイムラインIDがインクリメントされても、スレーブが新しいタイムラインIDをもつWALを受け付けずにレプリケーションが停止する仕様になっています。
ただし、recovery.confファイルに、recovery_target_timeline = 'latest'
を設定した状態でレプリケーションしている場合、スレーブは新しいタイムラインIDを受け付け、引き続きレプリケーションを継続します。
recovery_target_timeline = 'latest'
の設定は、今回の移行のように孫スレーブが必要になるケースにて有用です。しかし、Mackerelにおける通常運用状態では、この設定は不要であったため、必要なときのみ自前のレプリカ作成スクリプトのオプションにて設定する運用でした。その結果、必要にもかかわらずオプション設定を忘れてしまうというヒューマンエラーが発生しました。
今回の移行作業を実施するにあたって、本番でのミスを防ぐためにstaging環境にて移行テストを実施していました。移行テストでは、production環境におけるクラスタ構成と同等のクラスタ構成を構築し、production環境と同等の移行手順を実施しました。しかし、移行テストの際には、孫スレーブにて、recovery_target_timeline = 'latest'
を正しく設定していたため、移行テスト時にはレプリケーション停止が発生しませんでした。したがって、移行テストにて、今回の問題を発見することができませんでした。
データの復旧について
まず、db001に残ったテーブルデータから手動でデータの復旧ができないかを検討しました。しかし、その時点ですでに多くのデータが更新されており、データベースのテーブル間の整合性を維持しながら、消失したデータ更新をdb002へ反映するのは非常に困難であると判断し、手動による復旧を断念しました。
次に、PostgreSQLのPITR(Point In Time Recovery)機能により、メンテナンス開始以前のベースバックアップ、db001に残された消失したデータ更新を記録したWAL(Write Ahead Log)、およびバックアップサーバへ転送していたWALアーカイブを用いて、データの復旧を試みました。 しかし、PostgreSQLのPITRの仕組み上、データの復旧は難しいと判断し、復旧を断念しました。
PITRに関する具体的な状況を、下記の図に示します。
メンテナンス作業開始前は、db000はタイムラインIDが15でした。そこからメンテナンス作業を実施し、db001がマスター昇格し、タイムラインIDが16となりました。その後、フェイルオーバー発生により、db002がマスター昇格し、メンテナンス作業開始時のデータ状態で、タイムラインIDが17となりました。 この状態から、メンテナンス作業前のベースバックアップを基に、PITRを開始すると、メンテナンス作業開始前までリカバリしたのちに、タイムライン16またはタイムライン17のうち、どちらかのタイムラインしかリカバリできないようになっています。 したがって、タイムライン16をリカバリしたのちに、タイムライン ID 17をリカバリすることはできません。 このような仕組みになっているのは、例えば、タイムライン16でDELETEされたレコードをタイムライン17でUPDATEしていた場合、UPDATE対象のレコードが存在しないことになるため、タイムライン16 とタイムライン17の間で整合性を維持することができないからだと推測しています。
このように、手動またはPITRによりデータベース上で整合性のある状態に復旧することを試みましたが、仮に復旧できたとしても、お客様の視点からは整合性のある状態とは言えないと考え、その他の復旧手法を検討することを断念しました。例えば、すでにお客様が消失したデータの再作成を実施していただいている場合、消失したデータと再作成したデータを合わせて二重に情報が登録されているようにみえる可能性があります。
フェイルオーバ後のクラスタ再構築について
フェイルオーバ後すぐに、フェイルオーバ先のdb002をマスターとして、新規にスレーブサーバdb003を構築し、db002とdb003で冗長構成をとりました。障害のあったdb001はメンテナンス状態で調査用に残しました。その後、db002でdb001と同様の問題が起きないことを一定時間観察した後に、移行メンテナンスを完了としました。
障害の影響
データ消失に関する障害の影響は下記の通りです。
日本時間における2017年8月7日の14:30から15:30までの間に、PostgreSQLサーバに対して更新されたデータを消失しました。また、その時間内に新規に作成されたメトリック、およびAWSインテグレーションとAzureインテグレーションにより投稿されたメトリック以外のメトリックに紐づく時系列データの消失はありません。
今回の移行全体に関する不具合のリストについては、8/7のアナウンスにまとめております。
再発防止策
今回の問題は、通常運用状態では発生しませんが、PostgreSQLのバージョンアップや異なるリージョンへの移行など、将来的に今回と同じような構成をとる可能性があります。その際の再発防止策として、以下の項目を実施します。
- PostgreSQLのレプリカを作成するスクリプトにて、recovery.confファイル内に
recovery_target_timeline = 'latest'
をデフォルトで設定するように変更する - Serverspecなどのサーバ構成管理テストツールにて、recovery.confファイル内に
recovery_target_timeline = 'latest'
文字列が含まれているかをチェックする項目を増やす
recovery_target_timeline = 'latest'
の設定は、孫スレーブではなく、Mackerelにおける通常運用時にスタンバイサーバに設定していたとしても仕組み上問題はありません。したがって、通常運用時ヒューマンエラーが発生する余地がないように、スレーブ作成時にデフォルトでrecovery_target_timeline = 'latest'
を設定する運用に変更しました。ただし、この記事をご覧の方がこの設定を利用される場合、適用先の構成において問題がないかを確認された上で設定されることをおすすめいたします。
データ消失およびその他の不具合を発生させてしまい、お客様にご迷惑をおかけして大変申し訳ありません。今後はさらなるサービス品質向上に努めてまいります。引き続きMackerelをご愛顧くださりますようよろしくお願い申し上げます。