MongoDB의 성능 튜닝 포인트는 RDBMS의 그것과 별반 다르지 않습니다.
크게 5가지 관점에서 성능 튜닝에 대해서 그 원인 및 조치를 생각해 볼 수 있습니다.
- Design Tuning
Data를 저장하는 논리적 구조인 Collection의 적절한 분석과 설계 작업이 수행되지 않은 경우 전체적인 Database 성능을 저하 시킬 수 있습니다. 이러한 Collection의 설계 문제로 인한 성능저하 원인과 문제점을 분석하고 대처하는 방법을 Design Tuning이라 합니다.
- Statement Tuning
개발자가 작성한 Query 문장 자체가 적절하게 작성되지 못한 경우 Database의 성능을 저하 시킵니다. 이런 Query문장을 수정하는 것을 Statement Tuning이라 합니다.
- Architecture Tuning
MongoDB는 Big Data의 빠른 Read/Write를 위해 다양한 Solution (Shading System, Reploca Set...)을 제공합니다.
이러한 System 구축시 관련 설정을 제대로 못한 경우 다양한 문제점들이 발생하는데, 여기에 대한 원인 분석 및 성능 해결 방법을 Architecture Tuning이라 합니다.
- Instance Tuning
MongoDB는 Memory Mapping을 이용하여 빠른 Data 처리가 가능하므로 충분한 Memory가 필요합니다. 이러한 문제점들을 발견하고 대처하는 것을 Instance Tuning 이라 합니다.
- Hardware Tuning
MongoDB는 Server 프로그램이기 떄문에 운영체제의 설정 및 Hardware의 영향을 많이 받습니다. 여기서 원인을 찾아내고 분석하고 대처하는 것을 Hardware Tuning이라 합니다.
2.Design Tuning Guide
- Big Data의 INSERT 가 발생하는 Collection의 경우 최초 Extent Size를 충분히 크게 설계하여 INSERT 작업에서의 추가적인 Extent 할당을 최소화 해주는 것이 유리합니다.
Full Scan이 자주 발생하는 Collectoin은 Extent가 큰 것이 유리하고,
Index Scan이 자주 발생하는 경우에는 Extent가 작은 것이 유리합니다.
- 관계가 밀접한 Collection들인 경우는 Rich Document로 설계하는 것이 유리하고, 밀접하지 않은 경우에는 Linking(DBRef)를 이용하는 것이 유리합니다.
- Compound Index를 설계할 때는 자주 검색되는 Field를 선행 Field로 설정하는것이 유리합니다.
모두 자주 사용될 경우에는 분포도가 좋은 Field를 선행 Field로 해야 합니다. (약 10% 내외가 가장 좋습니다.)
앞의 2조건에서 결정이 힘들다면 Data 량이 적은 Field가 유리하며,
그 다음으로 생각해야 할것은 Equal 조건 검색이 자주 되는 Field가 좋습니다.
- Big Data Collection에 대한 Index를 생성 할 때는 Background Index로 설정하여 DB 자원이 부족하지 않을 때에만 Index를 생성하도록 하는것이 좋습니다.
3. Statement Tuning Guide
- 실행된 Query문은 db.system.profile Collection에 저장됩니다.
Profile Level 설정 방법입니다.
> db.commandHelp('profile') help for: profile enable or disable performance profiling { profile : <n> } 0=off 1=log slow ops 2=log all -1 to get current values http://docs.mongodb.org/manual/reference/command/profile/#dbcmd.profile > db.setProfilingLevel(2) // Profile Level 설정 { "was" : 0, "slowms" : 100, "ok" : 1 } > db.getProfilingLevel() // Profile Level 확인 2 |
Profile 내용을 확인하는 방법입니다.
> db.system.profile.find() //모든 Log에 대하여 Profile 확인 // db.system.profile.find( { millis : $gt : 5 } } ) 식으로 수행 시간이 특정값 이상인 조건으로 활용이 가능 { "op" : "command", "ns" : "test.$cmd", "command" : { "profile" : -1 }, "keyUpdates" : 0, "writeConflicts" : 0, "numYiel d" : 0, "locks" : { "Global" : { "acquireCount" : { "r" : NumberLong(1), "w" : NumberLong(1) } }, "MMAPV1Journal" : { "a cquireCount" : { "w" : NumberLong(1) } }, "Database" : { "acquireCount" : { "W" : NumberLong(1) } } }, "responseLength" : 58, "millis" : 0, "execStats" : { }, "ts" : ISODate("2015-08-16T05:59:29.886Z"), "client" : "127.0.0.1", "allUsers" : [ ], "user" : "" } { "op" : "query", "ns" : "test.emp", "query" : { }, "ntoreturn" : 0, "ntoskip" : 0, "nscanned" : 0, "nscannedObjects" : 0, "keyUpdates" : 0, "writeConflicts" : 0, "numYield" : 0, "locks" : { "Global" : { "acquireCount" : { "r" : NumberLong (2) } }, "MMAPV1Journal" : { "acquireCount" : { "r" : NumberLong(1) } }, "Database" : { "acquireCount" : { "r" : NumberL ong(1) } }, "Collection" : { "acquireCount" : { "R" : NumberLong(1) } } }, "nreturned" : 0, "responseLength" : 20, "mill is" : 0, "execStats" : { "stage" : "EOF", "nReturned" : 0, "executionTimeMillisEstimate" : 0, "works" : 1, "advanced" : 0, "needTime" : 0, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0 }, "ts" : ISODat e("2015-08-16T05:59:57.118Z"), "client" : "127.0.0.1", "allUsers" : [ ], "user" : "" } |
Profile을 삭제 후 새로 생성하는 방법입니다.
> db.setProfilingLevel(0) { "was" : 2, "slowms" : 100, "ok" : 1 } > db.system.profile.drop() true > db.createCollection('system.profile', { capped:true , size : 100000 } ) { "ok" : 1 } > db.system.profile.stats() { "ns" : "test.system.profile", "count" : 0, "size" : 0, "numExtents" : 1, "storageSize" : 102400, "lastExtentSize" : 102400, "paddingFactor" : 1, "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.", "userFlags" : 1, "capped" : true, "max" : NumberLong("9223372036854775807"), "maxSize" : 102400, "nindexes" : 0, "totalIndexSize" : 0, "indexSizes" : { }, "ok" : 1 } > db.setProfilingLevel(2) { "was" : 0, "slowms" : 100, "ok" : 1 } > db.emp.find() { "_id" : ObjectId("55d028cd32fe92a924f5978e"), "id" : 1, "name" : "Luna" } > db.system.profile.find() { "op" : "query", "ns" : "test.emp", "query" : { }, "ntoreturn" : 0, "ntoskip" : 0, "nscanned" : 0, "nscannedObjects" : 1, "keyUpdates" : 0, "writeConflicts" : 0, "numYield" : 0, "locks" : { "Global" : { "acquireCount" : { "r" : NumberLong (2) } }, "MMAPV1Journal" : { "acquireCount" : { "r" : NumberLong(1) } }, "Database" : { "acquireCount" : { "r" : NumberL ong(1) } }, "Collection" : { "acquireCount" : { "R" : NumberLong(1) } } }, "nreturned" : 1, "responseLength" : 69, "mill is" : 0, "execStats" : { "stage" : "COLLSCAN", "filter" : { "$and" : [ ] }, "nReturned" : 1, "executionTimeMillisEstimat e" : 0, "works" : 3, "advanced" : 1, "needTime" : 1, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "direction" : "forward", "docsExamined" : 1 }, "ts" : ISODate("2015-08-16T06:11:33.322Z"), "client" : "127.0.0.1", "allUsers" : [ ], "user" : "" } |
- Hint() Explain() 함수를 통해 실행 계획을 분석합니다.
Hint()를 사용하여 원하는 Index를 이용하여 실행되게 할 수 있으며,
Explain()을 사용하여 실행계획을 확인 할 수 있습니다. (아직은 Oracle에 비교해서는 극히 제한적이긴 합니다.)
이렇게 분석하여 Full Collection Scan 되는 문장들은 Tuning을 하는 것이 좋습니다.
db.emp.dropIndexes() db.emp.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.emp" } ] db.emp.createIndex( { empno : 1 } , { uniruq : true } ) db.emp.find( { empno : 7469 } ).hint( { empno : 1 } ).explain() //empno Field에 대해서 Index를 이용한 실행계획 { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.emp", "indexFilterSet" : false, "parsedQuery" : { "empno" : { "$eq" : 7469 } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "empno" : 1 }, "indexName" : "empno_1", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "empno" : [ "[7469.0, 7469.0]" ] } } }, ... } db.emp.find().hint( { $natural : 1 } ).explain() // Full Collection Scan을 정방향으로 { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.emp", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ ] }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "$and" : [ ] }, "direction" : "forward" }, ... } db.emp.find().hint( { $natural : -1 } ).explain() // Full Collection Scan을 역방향으로 { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.emp", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ ] }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "$and" : [ ] }, "direction" : "backward" }, ... } |
- Key로 설정된 Field를 UPDATE, INSERT, DELETE 할 경우 Index에 대한 Update가 일어나야 하므로 성능이 저하 될 수 있습니다. Index의 Key 선정시 자주 바뀌는 Field에 대해서는 고려를 해 보는것이 좋습니다.
- Big Data에 대한 Update를 위해서는 Extent를 충분히 확보해야 합니다.
- Index의 빠른 생성을 위해서는 Background Index의 생성을 고려해 보는것이 좋습니다.
- Graph Data등의 Binary Type은 조회시 응답 시간이 길게 걸릴 수 밖에 없기 때문에 충분히 고려해야 합니다.
4. Architecture Tuning
- Replica Set 영역과 Business Area 영역은 반드시 분리하세요.
Replica Set은 장애 발생을 대비하여 복제되는 Clone Database이므로 실시간으로 지속적으로 Data 복제가 수행됩니다.
하나의 Server에 다양한 기능 설정을 하면 성능이 저하될 수 밖에 없습니다.
- 하나의 Instance 보다는 Shard Cluster를 적절히 활용하세요.
초당 몇 만 건 이상의 Big Data 처리를 하기위해서는 Shard System을 이용하는 것이 좋습니다.
여러대의 Server를 통한 Load Balancing을 지원해 줍니다.
다음의 상황에서는 Shard Server의 추가를 고려해 보는 것이 좋습니다.
- 하나의 Single Node의 Storage가 부족한 경우
- Active 상태의 WorkingSet이 전체 RAM 공간의 90%를 초과하는 경우
- Single Node에 DB-Lock이 집중되는 Write-Scale을 감당하기 힘든 경우
- Slave Server에서는 Backup과 Reports 활동을 중지하세요.
Slave Server는 거의 실시간으로 Master Server의 Data가 복제되는 작업울 수행합니다.
해당 작업들은 Master를 통해 수행하는 것이 좋습니다.
- 한 대의 H/W에는 하나의 MongoDB Instance만 활성화 하세요.
MongoDB는 Map Memory를 많이 사용하므로 여러 대의 mongod가 수행되면 Memory 부족으로 성능 지연이 발생합니다.
Peak Time 때 Page-Fault가 지속적으로 증가하는 경우 Memory 부족일 수 있는데, 이럴 땐 충분한 System Memory Area를 추가로 할당해 주어야 합니다.
앞서 Shard Cluster 쪽에서 설명했듯이 WorkingSet이 전체 RAM의 90%가 초과하는 경우에는 Memory를 추가로 장착해 주는 것이 좋습니다. Peak Time 때에도 70 ~ 80% 를 유지하는 것이 가장 이상적으로 성능을 보장합니다.
Memory 상태, Page Fault, Lock 관련 정보를 얻는 방법은 다음과 같습니다.
db.serverStatus().mem { "bits" : 64, // 시스템 사양 (32bit, 64bit) "resident" : 72, // Working Set Memory Area Size "virtual" : 472, // Virtual Memory Area Size "supported" : true, "mapped" : 160, // Mapped File Size "mappedWithJournal" : 320 // Journal Memory Area Size } db.serverStatus().extra_info { "note" : "fields vary by platform", "page_faults" : 35832, "usagePageFileMB" : 104, "totalPageFileMB" : 9386, "availPageFileMB" : 4191, "ramMB" : 8106 } db.serverStatus().globalLock { "totalTime" : NumberLong(1464604000), // 모든 Lock Time 들의 합 / totalTime 을 하면 Lock Percentage 계산이 가능 "currentQueue" : { "total" : 0, "readers" : 0, "writers" : 0 }, "activeClients" : { "total" : 8, "readers" : 0, "writers" : 0 } } db.serverStatus().locks { "Global" : { "acquireCount" : { "r" : NumberLong(5768), "w" : NumberLong(28), "W" : NumberLong(6) }, "acquireWaitCount" : { "r" : NumberLong(1) }, "timeAcquiringMicros" : { "r" : NumberLong(1183) } }, "MMAPV1Journal" : { "acquireCount" : { "r" : NumberLong(2865), "w" : NumberLong(91), "R" : NumberLong(15634), "W" : NumberLong(7) } }, "Database" : { "acquireCount" : { "r" : NumberLong(2865), "w" : NumberLong(14), "R" : NumberLong(2), "W" : NumberLong(14) } }, "Collection" : { "acquireCount" : { "R" : NumberLong(2995), "W" : NumberLong(16) } }, "Metadata" : { "acquireCount" : { "W" : NumberLong(5) } } } |
5. Instance Tuning
- Profiler와 Explain()을 활용하여 Statement Tuning을 먼저 수행하세요.
대부분의 성능 지연 문제들은 Query문 수정 과 Index 작업 만으로도 해결됩니다.
Index와 Qury문 자체가 적절하지 않은 경우 아무리 Instance Tuning을 하더라도 개선에는 한계가 있습니다.
- 최적화된 Read / Write 가 되도록 Statement를 작성하세요.
Collection에 Document를 저장하는 방법으로는 save() 와 update() 두 가지 방법이 있습니다.
save()는 Document 단위로 저장을 할때 유리하고, update()는 Field 단위로 저장을 할때 유리합니다.
자주 검색되는 Field에는 적절한 Index를 생성하면 성능에 좋습니다.
불필요한 Field에 대해서는 읽기 작업을 피하는 것이 WorkingSet 영역을 최소화시켜 Instance 전체 성능을 향상 시킵니다.
- Data Locality (인접성)이 높아질 수 있도록 설계하고 저장하세요.
유사한 성격의 Data들은 인접한 Data 영역에 저장하는 것이 좋습니다.
아래 방법으로 해당 Document들이 같은 File에 저장되어 있는지 확인이 가능합니다.
db.emp.find( {}, { _id : 0, $diskloc : 1 , userid : 1 } ).showDiskLoc() { "$diskLoc" : { "file" : 0, "offset" : 1077424 } } { "$diskLoc" : { "file" : 0, "offset" : 1077680 } } { "$diskLoc" : { "file" : 0, "offset" : 1077936 } } { "$diskLoc" : { "file" : 0, "offset" : 1078192 } } { "$diskLoc" : { "file" : 0, "offset" : 1078448 } } { "$diskLoc" : { "file" : 0, "offset" : 1078704 } } { "$diskLoc" : { "file" : 0, "offset" : 1078960 } } { "$diskLoc" : { "file" : 0, "offset" : 1079216 } } { "$diskLoc" : { "file" : 0, "offset" : 1079472 } } { "$diskLoc" : { "file" : 0, "offset" : 1079728 } } { "$diskLoc" : { "file" : 0, "offset" : 1079984 } } { "$diskLoc" : { "file" : 0, "offset" : 1080240 } } { "$diskLoc" : { "file" : 0, "offset" : 1080496 } } { "$diskLoc" : { "file" : 0, "offset" : 1080752 } } |
6. Hardware Tuning
- 되도록 SSD를 사용하여 사용하는 것이 좋습니다. 아직 HDD보다 고가이긴 하지만, 확실한 성능 향상을 보장하는 방법입니다.
- Mapped Cache Area의 크기가 2GB인 경우 적정 Memory Size는 10GB이며 64Bit 운영체제에서 좀 더 빠른 성능을 보장합니다.
- 단일 CPU 환경은 CPU 과부하시 시스템 성능 저하 현상을 피할 수 없습니다. 되도록이며 다중 CPU 환경에 구축하세요.
- Shading System을 구축하여 분산 처리를 하세요.
댓글 없음:
댓글 쓰기