以下のセクションでは、Atlas 配置のレイテンシを低減するための設定オプションについて説明します。
物理的距離
物理的距離はレイテンシの主な原因です。ユーザーとアプリケーションの間、アプリケーションとデータの間、クラスターノード間の距離は、いずれもシステムのレイテンシやアプリケーションのパフォーマンスに影響を与えます。
読み取りおよび書き込み操作のレイテンシを軽減するには、アプリケーションとデータの両方を地理的にユーザーに近いものに配置することが重要です。データを保持する Atlas ノードは、データベースのデータを保存し、読み取りおよび書き込み操作を取り扱う Atlas クラスター内のサーバーノードです。アプリケーションユーザーのデータアクセスを高速化するには、アプリケーションユーザーの過半数に地理的に近いクラウドプロバイダーのリージョンに、データを含む Atlas ノードを配置します。
アプリケーションのユーザーが米国やヨーロッパなど複数の地域に分散されている場合、各地域で1つ以上のリージョンに配置することをお勧めします。これにより、各ロケーションのユーザーのレイテンシを低減できます。マルチリージョン配置の詳細については、「マルチリージョン配置のパラダイム」を参照してください。
データが地理ごとに分割されており、各地域のユーザーが異なるデータセットにアクセスする場合は、リージョンまたは地域ごとにデータをシャード分割して、各地域のユーザーの読み取りおよび書き込みパフォーマンスを最適化することもできます。このアプローチにより、データローカリティを確保しつつ、大規模なデータセットと高スループットを取り扱うことができます。
レプリケーション構成
レプリケーションは、プライマリ ノードからセカンダリ ノードへのデータのコピーです。以下のレプリケーション設定オプションを調整することで、レプリカセット内の読み取りおよび書き込み操作のレイテンシを最小限に抑えることができます。
書込み保証 (write concern) レベル: 書込み保証 (write concern)を設定する場合、書き込み (write)レイテンシと書き込み (write)耐久性の間にはトレードオフがあります。MongoDB のデフォルトのグローバル書込み保証 (write concern) は
majorityに設定されています。このため、Atlas がクライアントに対して操作の完了と成功させることを確認する前に、各書き込み (write)操作がレプリカセット内の投票権を持つデータを保持するノードの過半数に複製される必要があります。書込み保証 (write concern)を高く設定すると、 書き込み レイテンシが増加しますが、書き込み の耐久性も向上し、レプリカセットのフェイルオーバー中にロールバックが発生するのを防止できます。読み取り保証 (read concern) と読み込み設定 (read preference): 読み取り保証 (read concern) と 読み込み設定 (read preference) を構成する場合、クエリレイテンシ、データ可用性、およびクエリ応答の整合性の間にはトレードオフがあります。MongoDB のデフォルトのグローバル読み取り保証 (read concern)は
localです。これにより、読み取り操作は、データが他のノードにレプリケーションされることを確認するのを待つことなく、ローカルレプリカセット内の 1 つのノードからのみ読み取る必要があります。このノードがプライマリ ノードかセカンダリ ノードかは、読み込み設定 (read preference)によって決まります。Atlas はデフォルトでprimaryに設定します。このデフォルトの読み取り保証 (read concern)と設定の組み合わせは、レプリカセット内の最新のノードからのレイテンシではレイテンシが最低となります。ただし、プライマリ ノードのデータが永続的ではなく、次の実行中にロールバックされる可能性があるリスクもあります:フェイルオーバー以降、使用可能な プライマリノードがない場合は、ユーザーはデータをクエリできません。読み込み設定を
primaryPreferredに変更して、使用可能なプライマリノードがない場合や、地理的に近いセカンダリノードがある場合にセカンダリノードから読み込み操作を実行できるようにできますが、セカンダリノードが最新でない場合、古いデータが返されるリスクがあります。このリスクは書込み保証を増やしてより多くのセカンダリノードを最新の状態に保つことで軽減できますが、その代わりに書込みレイテンシが増加します。重要
レプリケーションラグにより、セカンダリ ノードから古いデータが返される可能性があることに注意してください。
クエリ タイムアウトの制限: 配置にグローバルおよび操作レベルのクエリ タイムアウト制限を設定して、アプリケーションがタイムアウトするまでの応答を待つ時間を短縮できます。これにより、進行中のクエリが長期間にわたって配置のパフォーマンスに悪影響を与えるのを防ぐことができます。
ノード選挙の優先順位:メインデータセンターのノードが代替データセンターのノードよりも先にプライマリに選出される可能性を高めるには、レプリカセット選挙中に、代替データセンターのノードの
members[n].priorityをプライマリデータセンターのノードよりも低く設定できます。例として、クラスターを AWS リージョンus-east-1(Northern Virginia)とus-west-1(Northern California)にわたって配置し、ユーザーの過半数がカリフォルニアにいる場合、AWSus-west-1(Northern California)リージョンのノードを優先すると、プライマリノードが常に地理的にユーザーの過半数に近く、読み取りおよび書き込み (write)操作に最小限のレイテンシで応答できるようになります。ミラーリングされた読み取り: ミラーリングされた読み取りは、セカンダリ ノードのキャッシュを事前ウォーミングすることで、停止後のプライマリ選挙の影響を軽減します。詳細については、「ミラーリングされた読み取り」を参照してください。
お客様のニーズに最適なレプリケーション構成を実装するための詳しいガイダンスについては、MongoDB の Professional Services にお問い合わせください。
ネットワーク構成
次のネットワーク接続オプションを使用することで、セキュリティを強化し、さらにレイテンシを低減することができます。
プライベートエンドポイント: プライベートエンドポイントは、アプリケーションの仮想ネットワークと Atlas クラスター間で直接かつ安全な接続を確立し、ネットワーク コストを削減し、レイテンシ を改善する可能性があります。
VPC ピアリング:アプリケーションがピアリングされたリージョンに接続できるように、レプリカセットに VPC ピアリングを設定します。フェイルオーバーが発生した場合、VPC ピアリングはアプリケーションサーバーが新しいプライマリノードを検出し、それに応じてトラフィックをルーティングする方法を提供します。
データモデリングとクエリの最適化
アプリケーションがデータにアクセスする速度は、レイテンシに影響します。優れたデータモデリングとクエリの最適化により、データアクセス速度を向上させることができます。例としては次のことができます。
ドキュメント サイズを縮小: ネットワークを介して転送されるデータ転送量を減らすために、フィールド名と値の長さを短縮することを検討します。
クエリ パターンの最適化: インデックスを効果的に使用して、リージョン全体で読み取られる必要があるデータ量を最小限に抑えます。
レイテンシのモニタリングとテスト
Atlas は、さまざまなリージョンのレイテンシメトリクスを観察するためにリアルタイム パフォーマンス パネル(リアルタイム パフォーマンス パネル)を提供しています。また、アプリケーションレベルのモニタリングを実装して、アプリケーションとの間のエンドツーエンドのレイテンシを追跡することもできます。最終本番環境に配置する前に、さまざまなマルチリージョンシナリオでパフォーマンス テストを実行し、レイテンシのボトルネックを特定して対処することをお勧めします。
配置のモニタリングの詳細については、Atlas のモニタリングとアラート に関するガイダンス を参照してください。
接続構成
可能な限り、アプリケーションのプログラミング言語に対応した最新のドライバー バージョンでビルドされた接続方法を使用することをお勧めします。Atlas が提供するデフォルトの接続文字列は良い出発点ですが、接続文字列オプションを接続文字列に追加して、特定のアプリケーションと配置アーキテクチャのコンテキストでパフォーマンスを向上させることができます。
エンタープライズレベルのアプリケーション配置の場合、 操作レイテンシ を最小限に抑えながら、ユーザーの需要に合わせて接続プールの設定を調整することは特に重要です。例、minPoolSize と maxPoolSize オプションを使用して、データベースクライアント接続の過半数が開く方法とタイミングを調整できるため、関連するネットワーク オーバーヘッドに伴うレイテンシの急増を防止または計画できます。
これらの設定をどの程度構成できるかは、配置アーキテクチャによって異なります。例えば、アプリケーションの配置がAmazon Web Services Lambda のようなシングルスレッド リソースを活用している場合、アプリケーションは 1つのクライアント接続しか開いて使用できません。接続プールの作成方法や使用場所、接続プール設定の指定箇所についての詳細は、「接続プールの概要」を参照してください。
低レイテンシ アプリケーションの例
次のサンプルアプリケーションでは、データ操作のレイテンシ を削減するために、このページに記載されている重要な推奨事項をまとめています。
Atlas が提供する接続文字列を、再試行可能な書き込み、過半数の書込み保証(write concern)、デフォルトの読み取り保証(read concern)とともに使用します。
optimemaxTimeMS メソッドを使用して 制限を指定します。
maxTimeMSを設定する方法については、特定のドライバーのドキュメント を参照してください。重複キーとタイムアウトのエラーを処理します。
アプリケーションは、クライアントがユーザー レコードを作成または一覧表示できるHTTP APIです。これにより、GET および POST リクエストを受け入れるエンドポイントが公開されます ( http://localhost:3000 )。
方式 | エンドポイント | 説明 |
|---|---|---|
|
|
|
|
| リクエスト本文に |
// File: App.java
import java.util.Map;import java.util.logging.Logger;
import org.bson.Document;import org.json.JSONArray;
import com.mongodb.MongoException;import com.mongodb.client.MongoClient;import com.mongodb.client.MongoClients;import com.mongodb.client.MongoCollection;import com.mongodb.client.MongoDatabase;
import fi.iki.elonen.NanoHTTPD;
public class App extends NanoHTTPD { private static final Logger LOGGER = Logger.getLogger(App.class.getName());
static int port = 3000; static MongoClient client = null;
public App() throws Exception { super(port);
// Replace the uri string with your MongoDB deployment's connection string String uri = "<atlas-connection-string>"; client = MongoClients.create(uri);
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); LOGGER.info("\nStarted the server: http://localhost:" + port + "/ \n"); }
public static void main(String[] args) { try { new App(); } catch (Exception e) { LOGGER.severe("Couldn't start server:\n" + e); } }
@Override public Response serve(IHTTPSession session) { StringBuilder msg = new StringBuilder(); Map<String, String> params = session.getParms();
Method reqMethod = session.getMethod(); String uri = session.getUri();
if (Method.GET == reqMethod) { if (uri.equals("/")) { msg.append("Welcome to my API!"); } else if (uri.equals("/users")) { msg.append(listUsers(client)); } else { msg.append("Unrecognized URI: ").append(uri); } } else if (Method.POST == reqMethod) { try { String name = params.get("name"); if (name == null) { throw new Exception("Unable to process POST request: 'name' parameter required"); } else { insertUser(client, name); msg.append("User successfully added!"); } } catch (Exception e) { msg.append(e); } }
return newFixedLengthResponse(msg.toString()); }
static String listUsers(MongoClient client) { MongoDatabase database = client.getDatabase("test"); MongoCollection<Document> collection = database.getCollection("users");
final JSONArray jsonResults = new JSONArray(); collection.find().forEach((result) -> jsonResults.put(result.toJson()));
return jsonResults.toString(); }
static String insertUser(MongoClient client, String name) throws MongoException { MongoDatabase database = client.getDatabase("test"); MongoCollection<Document> collection = database.getCollection("users");
collection.insertOne(new Document().append("name", name)); return "Successfully inserted user: " + name; }}注意
次のサーバーアプリケーションではExpressが使用されています。これは実行前にプロジェクトに 依存関係 として追加する必要があります。
const express = require('express');const bodyParser = require('body-parser');
// Use the latest client libraries by installing & importing themconst MongoClient = require('mongodb').MongoClient;
const app = express();app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: true }));
const uri = "mongodb+srv://<db_username>:<db_password>@cluster0-111xx.mongodb.net/test?retryWrites=true&w=majority";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true});
// ----- API routes ----- //app.get('/', (req, res) => res.send('Welcome to my API!'));
app.get('/users', (req, res) => { const collection = client.db("test").collection("users");
collection .find({}) .maxTimeMS(5000) .toArray((err, data) => { if (err) { res.send("The request has timed out. Please check your connection and try again."); } return res.json(data); });});
app.post('/users', (req, res) => { const collection = client.db("test").collection("users"); collection.insertOne({ name: req.body.name }) .then(result => { res.send("User successfully added!"); }, err => { res.send("An application error has occurred. Please try again."); })});// ----- End of API routes ----- //
app.listen(3000, () => { console.log(`Listening on port 3000.`); client.connect(err => { if (err) { console.log("Not connected: ", err); process.exit(0); } console.log('Connected.'); });});注意
次の WebアプリケーションはFastAPI を使用します。新しいアプリケーションを作成するには、FastAPIサンプルファイル構造を使用します。
# File: main.py
from fastapi import FastAPI, Body, Request, Response, HTTPException, statusfrom fastapi.encoders import jsonable_encoder
from typing import Listfrom models import User
import pymongofrom pymongo import MongoClientfrom pymongo import errors
# Replace the uri string with your |service| connection stringuri = "<atlas-connection-string>"db = "test"
app = FastAPI()
app.on_event("startup")def startup_db_client(): app.mongodb_client = MongoClient(uri) app.database = app.mongodb_client[db]
app.on_event("shutdown")def shutdown_db_client(): app.mongodb_client.close()
##### API ROUTES #####app.get("/users", response_description="List all users", response_model=List[User])def list_users(request: Request): try: users = list(request.app.database["users"].find().max_time_ms(5000)) return users except pymongo.errors.ExecutionTimeout: raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="The request has timed out. Please check your connection and try again.")
app.post("/users", response_description="Create a new user", status_code=status.HTTP_201_CREATED)def new_user(request: Request, user: User = Body(...)): user = jsonable_encoder(user) try: new_user = request.app.database["users"].insert_one(user) return {"message":"User successfully added!"} except pymongo.errors.DuplicateKeyError: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Could not create user due to existing '_id' value in the collection. Try again with a different '_id' value.")