secondary에서 write가 된다?

arky uhm·2025년 5월 28일

MongoDB

목록 보기
14/14

replica-set 중 secondary node에서 user01 컬렉션의 모든 데이터를 user02 컬렉션으로 복제하는 Aggregation Pipeline 테스트를 하는 중에 이상한 점이 발견되서 정리해 봅니다. 해당 작업은 write라서 secondary에서는 실패가 나야 하는데 성공적으로 처리가 되서 원인을 찾아봤습니다.

✅ 현상 : secondary에서 생성한 collection이 primary에서 복제된 채로 조회가 됨

--secondary node
% use myappdb  // database는 테스트용으로 아무데서나 해도 상관없음
% db.user01.aggregate([
{ $match: {} },     // 1단계 : 조건에 맞는 문서 필터링하는데, 이 조건은 모든 문서를 선택하게 됨 (필터링 없음)
{ $out: "user02" }  // 2단계 : 결과를 user02 컬렉션에 저장
])
/*
. { $match: {} }
  . 모든 도큐먼트를 선택합니다.
  . 조건이 없으므로 user01의 모든 데이터를 대상으로 합니다.
  . 사실상 이 단계는 생략해도 동일하게 동작합니다.
. { $out: "user02" }
  . aggregation 결과를 user02라는 새로운 컬렉션에 저장합니다.
  . 만약 user02 컬렉션이 이미 있다면, 기존 데이터를 완전히 덮어씁니다.
  . $out은 쓰기 연산이므로 반드시 Primary 노드에서만 실행할 수 있습니다.  
※ pipeline은 여러 단계(stage)로 구성된 데이터 처리 흐름을 의미하는데, 
  각 단계의 출력은 다음 단계의 입력이 되어 마치 공장에서 제품이 여러 생산 공정을 거치는 것처럼 데이터가 순차적으로 가공되므로 pipeline이라는 단어가 사용됩니다.

*/
% show collections
user01
user02  // user02 collecion 생성 확인 

--primary node
% use myappdb
% show collections
user01
user02  // secondary에서 만든 user02 collecion 확인 

MongoDB의 기본 원칙에 따르면
 . secondary 노드에서는 쓰기 작업($out 포함)이 불가능해야 합니다.
 . $out은 새로운 컬렉션을 생성하는 쓰기 작업이므로 primary에서만 실행되어야 합니다.

✅ 원인 찾기

--secondary에서 일반적인 쓰기 작업 테스트
% db.user06.insertOne({test: "write test"})
MongoServerError[NotWritablePrimary]: not primary  // 쓰기 실패

--secondary에서 다른 aggregation 쓰기 작업 테스트
% db.user01.aggregate([
  { $match: {userId: {$lt: 5}} },
  { $merge: {into: "user_test"} }
]) // 성공
--primary에서 oplog확인 
% use local
% db.oplog.rs.find().sort({ts: -1}).limit(5)  // ts(timestamp, 타임스탬프) 필드를 기준으로 내림차순 정렬해서 최신 5개 작업만 가져옴
[
  {
    op: 'n',
    ns: '',
    o: { msg: 'periodic noop' },
    ts: Timestamp({ t: 1748335747, i: 1 }),
    t: Long('46'),
    v: Long('2'),
    wall: ISODate('2025-05-27T08:49:07.590Z')
  },
  {
    op: 'i',
    ns: 'myappdb.user_test',
    ui: UUID('626c29a2-6e59-40a7-b78e-e14e2024b086'),
    o: {
      _id: ObjectId('68356f9d190c2bd9acd861e0'),
      age: 31,
      created_at: ISODate('2025-05-27T07:54:05.656Z'),
      email: 'user1@example.com',
      name: 'user1',
      userId: 1
    },
    o2: { _id: ObjectId('68356f9d190c2bd9acd861e0') },
    ts: Timestamp({ t: 1748335734, i: 5 }),
    t: Long('46'),
    v: Long('2'),
    wall: ISODate('2025-05-27T08:48:54.122Z')
  },
  {
    op: 'i',
    ns: 'myappdb.user_test',
    ui: UUID('626c29a2-6e59-40a7-b78e-e14e2024b086'),
    o: {
      _id: ObjectId('68356f9d190c2bd9acd861e3'),
      age: 44,
      created_at: ISODate('2025-05-27T07:54:05.656Z'),
      email: 'user4@example.com',
      name: 'user4',
      userId: 4
    },
    o2: { _id: ObjectId('68356f9d190c2bd9acd861e3') },
    ts: Timestamp({ t: 1748335734, i: 4 }),
    t: Long('46'),
    v: Long('2'),
    wall: ISODate('2025-05-27T08:48:54.122Z')
  },
  {
    op: 'i',
    ns: 'myappdb.user_test',
    ui: UUID('626c29a2-6e59-40a7-b78e-e14e2024b086'),
    o: {
      _id: ObjectId('68356f9d190c2bd9acd861e2'),
      age: 20,
      created_at: ISODate('2025-05-27T07:54:05.656Z'),
      email: 'user3@example.com',
      name: 'user3',
      userId: 3
    },
    o2: { _id: ObjectId('68356f9d190c2bd9acd861e2') },
    ts: Timestamp({ t: 1748335734, i: 3 }),
    t: Long('46'),
    v: Long('2'),
    wall: ISODate('2025-05-27T08:48:54.122Z')
  },
  {
    op: 'i',
    ns: 'myappdb.user_test',
    ui: UUID('626c29a2-6e59-40a7-b78e-e14e2024b086'),
    o: {
      _id: ObjectId('68356f9d190c2bd9acd861e1'),
      age: 27,
      created_at: ISODate('2025-05-27T07:54:05.656Z'),
      email: 'user2@example.com',
      name: 'user2',
      userId: 2
    },
    o2: { _id: ObjectId('68356f9d190c2bd9acd861e1') },
    ts: Timestamp({ t: 1748335734, i: 2 }),
    t: Long('46'),
    v: Long('2'),
    wall: ISODate('2025-05-27T08:48:54.122Z')
  }
]
/*
// ns(namespace) "user06"이 포함된 모든 oplog 기록 찾기
. db.oplog.rs.find({ns: /user06/}).sort({ts: -1})
// ns(namespace)필드에 정확히 "myappdb.user06"인 oplog 기록만 찾기
. db.oplog.rs.find({ns: "myappdb.user06"}).sort({ts: -1})

*/

--DB버전 확인
% db.version()
8.0.9

✅ 결과

  1. secondary에서 out/out/merge를 실행해도 실제 쓰기는 primary에서 발생
  2. oplog를 보면 모든 쓰기 작업이 primary에 기록됨
  3. MongoDB 드라이버나 mongosh 레벨에서의 자동 라우팅됨

MongoDB 8.0의 관련 내용
 . aggregation pipeline의 쓰기 작업($out, $merge)은 secondary에서 실행되더라도 내부적으로 primary로 자동 라우팅됩니다
 . 일반적인 CRUD 쓰기 작업(insert, update, delete)은 여전히 secondary에서 차단됩니다

※ 참고
$out Read Operations Run on Secondary Replica Set Members
Starting in MongoDB 5.0, $out can run on replica set secondary nodes if all the nodes in cluster have featureCompatibilityVersion set to 5.0 or higher and the Read Preference is set to secondary.

Read operations of the $out statement occur on the secondary nodes, while the write operations occur only on the primary nodes.

Not all driver versions support targeting of $out operations to replica set secondary nodes. Check your driver documentation to see when your driver added support for $out running on a secondary.



🧾 출처
https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/

0개의 댓글