장기 실행 스냅샷 쿼리 수행
스냅샷 쿼리를 사용하면 최근 특정 단일 시점에 표시된 데이터를 그대로 읽을 수 있습니다.
MongoDB 5.0 부터는 읽기 고려 (read concern) "snapshot"
를 사용하여 세컨더리 노드의 데이터를 쿼리 할 수 있습니다. 이 기능 은 애플리케이션 읽기의 다용성과 회복 탄력성 을 향상시킵니다. 데이터의 정적 사본을 생성하여 별도의 시스템으로 옮기고, 이러한 장기 실행 쿼리가 운영 워크로드 를 방해하지 않도록 수동으로 격리할 필요가 없습니다. 대신 일관적인 데이터 상태 를 읽으면서 라이브 트랜잭션 데이터베이스 에 대해 장기 실행 쿼리를 수행할 수 있습니다.
세컨더리 노드에서 읽기 고려 (read concern) "snapshot"
를 사용해도 애플리케이션의 쓰기 (write) 워크로드 에는 영향 을 미치지 않습니다. 애플리케이션 읽기에만 장기 실행 쿼리가 세컨더리 쿼리로 격리됨으로써 이점을 누릴 수 있습니다.
다음을 수행하려는 경우 스냅샷 쿼리를 사용합니다.
여러 개의 관련 쿼리를 수행하고 각 쿼리가 똑같은 시점의 데이터를 읽도록 합니다.
과거 어느 점 의 데이터의 일관적인 상태 에서 읽어야 합니다.
로컬 및 스냅샷 읽기 고려 비교
MongoDB가 기본 "local"
읽기 고려를 사용하여 장기 실행 쿼리를 수행하는 경우, 쿼리 결과에 쿼리와 동시에 발생하는 쓰기 데이터가 포함될 수 있습니다. 결과적으로 쿼리가 예기치 않거나 일관되지 않은 결과를 반환할 수 있습니다.
이 시나리오를 방지하려면 세션 을 생성하고 읽기 고려 (read concern) "snapshot"
를 지정합니다. 읽기 고려 (read concern) "snapshot"
를 사용하면 MongoDB 는 스냅샷 점 격리 사용하여 쿼리 를 실행 쿼리 .
예시
이 페이지의 예는 스냅샷 쿼리를 사용하여 다음을 수행하는 방법을 보여줍니다.
동일한 시점에서 관련 쿼리 실행
읽기 고려 "snapshot"
을 사용하면 세션 내에서 여러 관련 쿼리를 실행하고 각 쿼리가 동일한 점의 데이터를 읽도록 할 수 있습니다.
동물 보호소에는 각 유형의 애완 동물에 대한 컬렉션이 포함된 pets
데이터베이스 가 있습니다. pets
데이터베이스 에는 다음과 같은 컬렉션이 있습니다.
cats
dogs
각 컬렉션의 각 문서에는 반려동물 입양 가능 여부를 알려주는 adoptable
필드가 포함되어 있습니다. 예를 들어, cats
컬렉션의 문서는 다음처럼 구성되어 있습니다.
{ "name": "Whiskers", "color": "white", "age": 10, "adoptable": true }
쿼리를 실행해 컬렉션 전체에서 입양 가능한 총 반려동물 수를 확인하려고 합니다. 데이터에 관한 일관된 뷰를 제공하려면 각 컬렉션에서 반환되는 데이터가 특정 단일 시점의 데이터인지 확인하도록 하세요.
이 목표를 달성하려면 세션 내에서 읽기 고려 (read concern) "snapshot"
를 사용합니다.
mongoc_client_session_t *cs = NULL; mongoc_collection_t *cats_collection = NULL; mongoc_collection_t *dogs_collection = NULL; int64_t adoptable_pets_count = 0; bson_error_t error; mongoc_session_opt_t *session_opts; cats_collection = mongoc_client_get_collection (client, "pets", "cats"); dogs_collection = mongoc_client_get_collection (client, "pets", "dogs"); /* Seed 'pets.cats' and 'pets.dogs' with example data */ if (!pet_setup (cats_collection, dogs_collection)) { goto cleanup; } /* start a snapshot session */ session_opts = mongoc_session_opts_new (); mongoc_session_opts_set_snapshot (session_opts, true); cs = mongoc_client_start_session (client, session_opts, &error); mongoc_session_opts_destroy (session_opts); if (!cs) { MONGOC_ERROR ("Could not start session: %s", error.message); goto cleanup; } /* * Perform the following aggregation pipeline, and accumulate the count in * `adoptable_pets_count`. * * adoptablePetsCount = db.cats.aggregate( * [ { "$match": { "adoptable": true } }, * { "$count": "adoptableCatsCount" } ], session=s * ).next()["adoptableCatsCount"] * * adoptablePetsCount += db.dogs.aggregate( * [ { "$match": { "adoptable": True} }, * { "$count": "adoptableDogsCount" } ], session=s * ).next()["adoptableDogsCount"] * * Remember in order to apply the client session to * this operation, you must append the client session to the options passed * to `mongoc_collection_aggregate`, i.e., * * mongoc_client_session_append (cs, &opts, &error); * cursor = mongoc_collection_aggregate ( * collection, MONGOC_QUERY_NONE, pipeline, &opts, NULL); */ accumulate_adoptable_count (cs, cats_collection, &adoptable_pets_count); accumulate_adoptable_count (cs, dogs_collection, &adoptable_pets_count); printf ("there are %" PRId64 " adoptable pets\n", adoptable_pets_count);
using namespace mongocxx; using bsoncxx::builder::basic::kvp; using bsoncxx::builder::basic::make_document; auto db = client["pets"]; int64_t adoptable_pets_count = 0; auto opts = mongocxx::options::client_session{}; opts.snapshot(true); auto session = client.start_session(opts); { pipeline p; p.match(make_document(kvp("adoptable", true))).count("adoptableCatsCount"); auto cursor = db["cats"].aggregate(session, p); for (auto doc : cursor) { adoptable_pets_count += doc.find("adoptableCatsCount")->get_int32(); } } { pipeline p; p.match(make_document(kvp("adoptable", true))).count("adoptableDogsCount"); auto cursor = db["dogs"].aggregate(session, p); for (auto doc : cursor) { adoptable_pets_count += doc.find("adoptableDogsCount")->get_int32(); } }
ctx := context.TODO() sess, err := client.StartSession(options.Session().SetSnapshot(true)) if err != nil { return err } defer sess.EndSession(ctx) var adoptablePetsCount int32 err = mongo.WithSession(ctx, sess, func(ctx context.Context) error { // Count the adoptable cats const adoptableCatsOutput = "adoptableCatsCount" cursor, err := db.Collection("cats").Aggregate(ctx, mongo.Pipeline{ bson.D{{"$match", bson.D{{"adoptable", true}}}}, bson.D{{"$count", adoptableCatsOutput}}, }) if err != nil { return err } if !cursor.Next(ctx) { return fmt.Errorf("expected aggregate to return a document, but got none") } resp := cursor.Current.Lookup(adoptableCatsOutput) adoptableCatsCount, ok := resp.Int32OK() if !ok { return fmt.Errorf("failed to find int32 field %q in document %v", adoptableCatsOutput, cursor.Current) } adoptablePetsCount += adoptableCatsCount // Count the adoptable dogs const adoptableDogsOutput = "adoptableDogsCount" cursor, err = db.Collection("dogs").Aggregate(ctx, mongo.Pipeline{ bson.D{{"$match", bson.D{{"adoptable", true}}}}, bson.D{{"$count", adoptableDogsOutput}}, }) if err != nil { return err } if !cursor.Next(ctx) { return fmt.Errorf("expected aggregate to return a document, but got none") } resp = cursor.Current.Lookup(adoptableDogsOutput) adoptableDogsCount, ok := resp.Int32OK() if !ok { return fmt.Errorf("failed to find int32 field %q in document %v", adoptableDogsOutput, cursor.Current) } adoptablePetsCount += adoptableDogsCount return nil }) if err != nil { return err }
db = client.pets async with await client.start_session(snapshot=True) as s: adoptablePetsCount = 0 docs = await db.cats.aggregate( [{"$match": {"adoptable": True}}, {"$count": "adoptableCatsCount"}], session=s ).to_list(None) adoptablePetsCount = docs[0]["adoptableCatsCount"] docs = await db.dogs.aggregate( [{"$match": {"adoptable": True}}, {"$count": "adoptableDogsCount"}], session=s ).to_list(None) adoptablePetsCount += docs[0]["adoptableDogsCount"] print(adoptablePetsCount)
$catsCollection = $client->selectCollection('pets', 'cats'); $dogsCollection = $client->selectCollection('pets', 'dogs'); $session = $client->startSession(['snapshot' => true]); $adoptablePetsCount = $catsCollection->aggregate( [ ['$match' => ['adoptable' => true]], ['$count' => 'adoptableCatsCount'], ], ['session' => $session], )->toArray()[0]->adoptableCatsCount; $adoptablePetsCount += $dogsCollection->aggregate( [ ['$match' => ['adoptable' => true]], ['$count' => 'adoptableDogsCount'], ], ['session' => $session], )->toArray()[0]->adoptableDogsCount; var_dump($adoptablePetsCount);
db = client.pets with client.start_session(snapshot=True) as s: adoptablePetsCount = db.cats.aggregate( [{"$match": {"adoptable": True}}, {"$count": "adoptableCatsCount"}], session=s ).next()["adoptableCatsCount"] adoptablePetsCount += db.dogs.aggregate( [{"$match": {"adoptable": True}}, {"$count": "adoptableDogsCount"}], session=s ).next()["adoptableDogsCount"] print(adoptablePetsCount)
client = Mongo::Client.new(uri_string, database: "pets") client.start_session(snapshot: true) do |session| adoptable_pets_count = client['cats'].aggregate([ { "$match": { "adoptable": true } }, { "$count": "adoptable_cats_count" } ], session: session).first["adoptable_cats_count"] adoptable_pets_count += client['dogs'].aggregate([ { "$match": { "adoptable": true } }, { "$count": "adoptable_dogs_count" } ], session: session).first["adoptable_dogs_count"] puts adoptable_pets_count end
앞의 일련의 명령:
MongoClient()
) 사용해 MongoDB 배포서버로의 연결을 설정합니다.pets
데이터베이스로 전환하세요.세션을 설정합니다. 명령은
snapshot=True
를 지정하기 때문에 세션은 읽기 고려"snapshot"
를 사용합니다.pets
데이터베이스의 각 컬렉션에 대해 다음 작업을 수행합니다.adoptablePetsCount
변수를 출력합니다.
세션 내의 모든 쿼리는 동일한 점 에 나타난 데이터를 읽습니다. 결과적으로 최종 개수에는 데이터의 일관적인 스냅샷 이 반영됩니다.
참고
세션이 WiredTiger 기록 보존 기간( 기본값300 초)보다 오래 지속되면 쿼리 는 SnapshotTooOld
오류를 발생시킵니다. 스냅샷 보존을 구성하고 장기 실행 쿼리를 활성화 하는 방법을 학습 보려면 스냅샷 보존 구성을 참조하세요.
과거 특정 시점의 일관된 데이터 상태로부터 읽기
읽기 고려 "snapshot"
을 사용하면 쿼리에서 최근 특정 점에 나타난 데이터를 그대로 읽을 수 있습니다.
온라인 신발 매장에는 매장에서 판매되는 각 품목에 대한 데이터가 포함된 sales
컬렉션이 있습니다. 예를 들어 sales
컬렉션의 문서는 다음과 같습니다.
{ "shoeType": "boot", "price": 30, "saleDate": ISODate("2022-02-02T06:01:17.171Z") }
매일 자정에 해당 날짜에 신발이 몇 켤레가 판매되었는지 확인하는 쿼리 가 실행됩니다. 일일 판매 쿼리 는 다음과 같습니다.
mongoc_client_session_t *cs = NULL; mongoc_collection_t *sales_collection = NULL; bson_error_t error; mongoc_session_opt_t *session_opts; bson_t *pipeline = NULL; bson_t opts = BSON_INITIALIZER; mongoc_cursor_t *cursor = NULL; const bson_t *doc = NULL; bool ok = true; bson_iter_t iter; int64_t total_sales = 0; sales_collection = mongoc_client_get_collection (client, "retail", "sales"); /* seed 'retail.sales' with example data */ if (!retail_setup (sales_collection)) { goto cleanup; } /* start a snapshot session */ session_opts = mongoc_session_opts_new (); mongoc_session_opts_set_snapshot (session_opts, true); cs = mongoc_client_start_session (client, session_opts, &error); mongoc_session_opts_destroy (session_opts); if (!cs) { MONGOC_ERROR ("Could not start session: %s", error.message); goto cleanup; } if (!mongoc_client_session_append (cs, &opts, &error)) { MONGOC_ERROR ("could not apply session options: %s", error.message); goto cleanup; } pipeline = BCON_NEW ("pipeline", "[", "{", "$match", "{", "$expr", "{", "$gt", "[", "$saleDate", "{", "$dateSubtract", "{", "startDate", "$$NOW", "unit", BCON_UTF8 ("day"), "amount", BCON_INT64 (1), "}", "}", "]", "}", "}", "}", "{", "$count", BCON_UTF8 ("totalDailySales"), "}", "]"); cursor = mongoc_collection_aggregate (sales_collection, MONGOC_QUERY_NONE, pipeline, &opts, NULL); bson_destroy (&opts); ok = mongoc_cursor_next (cursor, &doc); if (mongoc_cursor_error (cursor, &error)) { MONGOC_ERROR ("could not get totalDailySales: %s", error.message); goto cleanup; } if (!ok) { MONGOC_ERROR ("%s", "cursor has no results"); goto cleanup; } ok = bson_iter_init_find (&iter, doc, "totalDailySales"); if (ok) { total_sales = bson_iter_as_int64 (&iter); } else { MONGOC_ERROR ("%s", "missing key: 'totalDailySales'"); goto cleanup; }
ctx := context.TODO() sess, err := client.StartSession(options.Session().SetSnapshot(true)) if err != nil { return err } defer sess.EndSession(ctx) var totalDailySales int32 err = mongo.WithSession(ctx, sess, func(ctx context.Context) error { // Count the total daily sales const totalDailySalesOutput = "totalDailySales" cursor, err := db.Collection("sales").Aggregate(ctx, mongo.Pipeline{ bson.D{{"$match", bson.D{{"$expr", bson.D{{"$gt", bson.A{"$saleDate", bson.D{{"$dateSubtract", bson.D{ {"startDate", "$$NOW"}, {"unit", "day"}, {"amount", 1}, }, }}, }, }}, }}, }}, bson.D{{"$count", totalDailySalesOutput}}, }) if err != nil { return err } if !cursor.Next(ctx) { return fmt.Errorf("expected aggregate to return a document, but got none") } resp := cursor.Current.Lookup(totalDailySalesOutput) var ok bool totalDailySales, ok = resp.Int32OK() if !ok { return fmt.Errorf("failed to find int32 field %q in document %v", totalDailySalesOutput, cursor.Current) } return nil }) if err != nil { return err }
db = client.retail async with await client.start_session(snapshot=True) as s: docs = await db.sales.aggregate( [ { "$match": { "$expr": { "$gt": [ "$saleDate", { "$dateSubtract": { "startDate": "$$NOW", "unit": "day", "amount": 1, } }, ] } } }, {"$count": "totalDailySales"}, ], session=s, ).to_list(None) total = docs[0]["totalDailySales"] print(total)
$salesCollection = $client->selectCollection('retail', 'sales'); $session = $client->startSession(['snapshot' => true]); $totalDailySales = $salesCollection->aggregate( [ [ '$match' => [ '$expr' => [ '$gt' => ['$saleDate', [ '$dateSubtract' => [ 'startDate' => '$$NOW', 'unit' => 'day', 'amount' => 1, ], ], ], ], ], ], ['$count' => 'totalDailySales'], ], ['session' => $session], )->toArray()[0]->totalDailySales;
db = client.retail with client.start_session(snapshot=True) as s: db.sales.aggregate( [ { "$match": { "$expr": { "$gt": [ "$saleDate", { "$dateSubtract": { "startDate": "$$NOW", "unit": "day", "amount": 1, } }, ] } } }, {"$count": "totalDailySales"}, ], session=s, ).next()["totalDailySales"]
이전 쿼리:
$match
를$expr
와 함께 사용하여saleDate
필드 에 필터하다 를 지정합니다.를 사용하면 단계에서
$expr
집계 표현식 (예:NOW
)을 사용할$match
수 있습니다.
$gt
연산자와$dateSubtract
표현식을 사용하여 쿼리가 실행되기 하루 전보다saleDate
가 큰 문서를 반환합니다.$count
를 사용하여 일치하는 문서 수를 반환합니다. 개수는totalDailySales
변수에 저장됩니다.쿼리 가 단일 점 부터 읽도록 읽기 고려 (read concern)
"snapshot"
를 지정합니다.
sales
컬렉션은 크기가 매우 커서 이 쿼리를 실행하는 데 몇 분 정도 걸릴 수 있습니다. 매장은 온라인이기 때문에 하루 중 언제든지 판매가 가능합니다.
예를 들어, 다음과 같은 경우를 고려할 수 있습니다.
쿼리 가 12:00 오전에 실행되기 시작합니다.
고객이 12:02 오전에 신발 세 켤레를 삽니다.
쿼리 실행이 12:04 오전에 완료됩니다.
쿼리에서 읽기 고려 "snapshot"
를 사용하지 않는 경우, 보고서를 생성한 날짜에 수행하지 않더라도 쿼리가 시작되는 시점과 종료되는 시점 사이에 발생한 매출이 쿼리 수에 포함될 수 있으며 이로 인해 일부 매출이 두 번 계산되는 등 부정확한 보고서가 생성될 수 있습니다.
읽기 고려 (read concern) "snapshot"
를 지정하면 쿼리 는 쿼리 실행이 시작되기 직전의 점 에 데이터베이스 에 있던 데이터만 반환합니다.
참고
쿼리 가 WiredTiger 기록 보존 기간( 기본값300 초)보다 오래 걸리는 경우 SnapshotTooOld
오류와 함께 쿼리 오류가 발생합니다. 스냅샷 보존을 구성하고 장기 실행 쿼리를 활성화 하는 방법을 학습 보려면 스냅샷 보존 구성을 참조하세요.
스냅샷 보존 구성
기본적으로 WiredTiger storage engine 은 300 초 동안 기록을 유지합니다. 세션의 첫 번째 작업 시간부터 마지막 작업까지 총 300 초 동안 snapshot=true
로 세션을 사용할 수 있습니다. 세션을 더 오랜 기간 동안 사용하면 SnapshotTooOld
오류와 함께 세션이 실패합니다. 마찬가지로 읽기 고려 "snapshot"
를 사용하여 데이터를 쿼리하고 쿼리가 300 초 이상 지속되면 쿼리가 실패합니다.
쿼리 또는 세션이 300 초 이상 실행되는 경우 스냅샷 보존 기간을 늘리는 것이 좋습니다. 보존 기간을 늘리려면 minSnapshotHistoryWindowInSeconds
매개변수를 수정합니다.
예를 예시, 이 명령은 minSnapshotHistoryWindowInSeconds
의 값을 600 초로 설정합니다.
db.adminCommand( { setParameter: 1, minSnapshotHistoryWindowInSeconds: 600 } )
중요
MongoDB Atlas 클러스터의 minSnapshotHistoryWindowInSeconds
을(를) 수정하려면 Atlas 지원팀에 문의하세요.
디스크 공간 및 기록
minSnapshotHistoryWindowInSeconds
값을 늘리면 서버가 지정된 기간 내에 이전에 수정된 값의 기록을 유지해야 하므로 디스크 사용량이 늘어납니다. 사용되는 디스크 공간은 워크로드에 따라 다르며, 워크로드가 클수록 더 많은 디스크 공간이 필요합니다.