
MongoDBをサービス無停止で継続的かつ安全にバージョンアップしている話
Posted on
はじめに
こんにちは,プレイドのCore Platformチーム[1](プレイドで社内の全てのプロダクト基盤やリアルタイム解析基盤[2][3]を扱っているチーム)でインターンをしているlaplaといいます.プレイドでは2024年6月より働いており,この度社内で広く用いられているデータベースであるMongoDBのバージョンアップ業務に携わったので,その過程で得られた知見についてまとめようと思います.
データベースのバージョンアップは,データベースを使用しているシステムにとって避け難いタスクであり,それはプレイドも同様です.そこでプレイドでは, 単に公式のアップデートドキュメントを読むだけではなく, MongoDBの内部構造も理解した上で作業することにより,安全なバージョンアップを継続的にかつサービス無停止で行っています.今回はその点について説明しようと思います.
なお,本記事はデータベースのいわゆるHow To記事ではなく, データベースの構造に興味のある方や, 既に他のデータベースを使用していて, 今後MongoDBを使ってみようという方向けに記述を行っているため, 前半にデータベースのバージョンアップに関する一般的な話や,プレイドでの使用状況を述べ,後半にMongoDBの内部構造に触れるという構成を取っています.
自己紹介
現在,筑波大学情報学群情報科学類の4年生で,主にシステムプログラミングやクラウドネイティブな技術に興味がありますが,OSから機械学習までレイヤを問わずにプログラムを書きます.
趣味のプログラミングについて触れると,学部1年の頃に手が滑って,主にC++でx86_64向けのTCP/IPプロトコルスタックが搭載されたGUI操作可能なOSを書いた結果,その後も手が滑り続けて学部3,4年の頃にはRustで64bit RISC-V向けのOSを書いていました.他にもRust言語が好きなので,RustでELFのタイムトラベルデバッガを書いたり,ELFバイナリ難読化器(ELFバイナリの実行時の動作を変えずに,解析からの妨害対策がなされたバイナリを作り出す)を作っていたりしました.最近は内部でRISC-VバイナリをエミュレートするようなOCIコンテナランタイムを開発しています.
比較的高いレイヤも好きで,例えば学部2年の頃には文章から音を生成するCGANを研究していたりしました.
プレイドにおけるMongoDBとその周辺ソフトウェアの利用
プレイドでは,主力プロダクトであるCXプラットフォームKARTEの様々な場面で主にMongoDBを使用しています.また,ODM(Object Document Mapper)としてMongooseを用いていて,Node.js経由でMongoDBを操作しています.プレイドではサーバーサイドがほとんどNode.jsで書かれており,MongoDBは内部的にデータをBSONで保持している点や非同期処理との相性の良さという点でNode.jsとの親和性が高いため,積極的にMongoDBを使用しています.この辺りについては弊社のgamiが書いた「 PLAIDがNode.jsを採用し、5年間で12万行書いてわかったこと」 をご覧ください.
データベースのバージョンアップ
さて, 冒頭にも述べた通り,データベースの定期的なバージョンアップは,多くのソフトウェア企業に付いて回るタスクの1つです.
主にOSSとして公開されているような一般的なRDBをセルフホストしている場合において,バージョンアップなどの作業を行うためには,データの整合性を取るために一旦データベースへの書き込み処理を停止した上でレプリケーションをおこなってから,アプリケーションが参照するデータベースの接続先を変更する手順を踏むのが一般的と思われます.しかし,いわゆるNewSQLに属するMongoDBでは内部で複数同じ状態のデータを持つノードが複数おり、この特性を活用することで,プレイドでは平日の昼間であっても安全にバージョンアップの作業を行うことができています.
なぜMongoDBの場合にはサービスを停止させずに, 安全にバージョンアップできるのかに関しては,本記事後半で紹介する,MongoDBの内部構造を,論文などを元にして理解することにより達成されています.平日昼間など,通常は忌避される時間帯であっても,安全性の根拠を持ってメンテナンスウィンドウなどを設けずにバージョンアップを行うことは,開発速度の向上などの開発者体験の向上に繋がります.また, サービスを停止させる時間が不要なことは,KARTEをご利用いただくユーザー様にとっても大きな恩恵があると考えます.
今回のバージョンアップ完了までの手順
ここでは今回行ったバージョンアップの大まかな手順について説明します.大枠としては以前にMongoDBをバージョン4からバージョン5に上げた際のものと同様です.当時の記録については以前インターンされていた方が書いた「インターンシップで全社のMongoDBをアップデートした話」に詳しく記載されていますので,ぜひご覧ください.
- アプリケーションコードに影響を与えそうな変更の調査
まずはMongooseやMongoDB Serverの現在のバージョンから,バージョンアップする目標のバージョンまでの変更を確認し,主にプロダクトの動作に影響を与えそうなものをピックアップして調査しました - 社内ライブラリのアップデート
プレイド社内には,様々なアプリケーションが共用するライブラリが存在します.その中にはMongoDBのラッパーなども含まれているため,これらの更新を行いました - 各アプリケーションのアップデート
新しくなった社内のライブラリを用いて,各アプリケーションが動作するように修正を行います.この作業は基本的に各アプリケーションチームに依頼し,書き換えを行なってもらいました - データベースのアップデート
最初に検証環境のデータベースのバージョンを上げ,問題がなさそうであれば本番環境のデータベースもバージョンアップを行います.この際に,本記事後半で触れる内部構造の理解により,安心感をもったバージョンアップを行えています.
バージョンアップ作業の今後
現在は,バージョン7へのバージョンアップに必要な調査や実装を行っています.バージョン7へのバージョンアップに伴う,現状の我々のユースケースに大きな影響を与えそうなMongooseでの変更をいくつか紹介すると,次のようなものがあります:
- callbackを用いた処理を書くことができないようになる(社内のアプリケーションコードには,比較的昔に書かれたものではしばしばcallbackが使われている)
- 社内実装で多く使われている関数を使用できなくなる(findOneAndRemoveなど)
このように,今後に控えているアップデートではより困難なアップグレード作業が予想されますが,今回のアップグレードで得られた経験も積極的に用いて,無事に作業を成功させるべく動いています.
—
ここからはよりMongoDBの内部構造を理解したい方向けに記述します.
MongoDBの内部構造を想像する
さて,冒頭でも述べた通り,本記事の後半ではMongoDBの内部構造について見ていきます. ここではあくまでも公開論文などをもとに内部の仕組みなどを想像して書いているため, 間違っている可能性は否定できないことを述べておきます.
MongoDBの内部では,分散合意アルゴリズムとして著名なRaftを独自に改良したアルゴリズムを用いて合意を取っています.この機序を理解するためには次の3つの論文が特に参考になります:
- In Search of an Understandable Consensus Algorithm (Extended Version)(以下論文1): 分散合意アルゴリズムのRaftについて紹介されている論文です
- Fault-Tolerant Replication with Pull-Based Consensus in MongoDB(以下論文2): MongoDBの内部では,純粋なRaftアルゴリズムではなく,後述するPull-Based Consensusと呼ばれるアルゴリズムを用いてノード間での合意を取っています.このアルゴリズムについて解説した論文です
- Tunable consistency in MongoDB: MongoDBでは,書き込みと読み込みの整合性をある程度自由に設定することができます(Tunable Consistency).これによりアプリケーション特性に合わせた整合性を取ることができます.この内容についての説明がなされています
今回はスペースの都合上,論文1で紹介されている純粋なRaftの動作についての説明はある程度既知のものとし,以降は論文2で提案されているpull-based Raftの動作について説明を行うことにします.
Pull-Based Consensus
先に述べた通り,MongoDBの内部では純粋なRaftではなく,動作が改造されたRaftアルゴリズムが用いられていて,これは論文内ではPull-Based Consensusとされています.本記事では,純粋なRaftの改造版であるということを強調するために,Pull-Based Raftと呼ぶことにします.
まず一般的なRaftアルゴリズムは,次の図のように,LeaderノードからFollowerノードに対してログが配信され,Followerノードはそれを個々に有しているという構造を取っています.Leaderノードで何らかの不具合が起きると,クラスターはvotingを行い,新たなLeaderノードを選出します:
[4]
また,次図に示すように,クライアント側からのリクエストに対しては複数のモジュールにログが分散して保存されます:
[5]
分散合意を取るという目的であればこれで十分ですが,スケーリングやバージョンアップの際など,クラスター内のノードが増減する場合においては,データの同期を柔軟に行うのが難しいといった課題があります.
これに対して,MongoDBは独自のPull-Based Raftアルゴリズムを実装しており,これはFollowerノードが自律的にLeaderノードに対してログの配信を要求しにいくような構造になっています.
[6]
これはレプリカのデータ同期を任意の2つのレプリカで行うことができるという特徴があり,様々なネットワークトポロジーを取ることができるようになります.また,これによりネットワークの帯域幅を削減できるという特徴も存在します.
また,バージョンアップの作業を行うにあたっては,クラスター内の古いバージョンのノードが1つ,新しいバージョンのものと入れ替わって,問題が無ければ他の古いバージョンのものも順々に取り替えていくという方式を取っているものと思われます.このことは, MongoDBの公式のドキュメントには記述がありませんが,バージョンアップ時のメトリクスを観察した結果得られたヒューリスティックな観測です.この挙動が可能なのは, Pull-Based Raftによって任意のレプリカ間でのデータ同期を柔軟に行えるためだと考えています.このような挙動は,バージョンアップ時に何か不具合が生じてもサービスを停止させることなくロールバックできるという点で利点があります.
最後に
ここまで見てきたように,論文などのある程度信頼できるソースを元にした,ソフトウェアの内部構造理解は,より安全なシステム設計・運用につながるものであると言えます.
私自身は今回のバージョンアップ作業に際して,こういった理論面から実装までの理解を一気通貫で行うことができたので,貴重な経験を積めたと思います.
本記事ではその一端をお伝えすることができたと思うのですが,プレイドは計算機的に面白い業務が様々な場所に存在する環境であると思います.プレイドで働くことに興味を持たれた方は,ぜひ次のリンクもご覧ください.
“エンドユーザーに露出するプラットフォーム”をつくる。Core Platformチームの仕事はなぜチャレンジングなのか ↩︎
Diego Ongaro and John Ousterhout. 2014. In search of an understandable consensus algorithm. In Proceedings of the 2014 USENIX conference on USENIX Annual Technical Conference (USENIX ATC'14). USENIX Association, USA, 305–320. ↩︎
Fault-Tolerant Replication with Pull-Based Consensus in MongoDB ↩︎