[발 번역] Memcached를 걷어내자. 그건 너무 느리다.

*해당 블로그는KT ucloud 의 지원을 받고 있습니다.

해당 글은 http://blog.serverdensity.com/2012/05/11/removing-memcached-because-its-too-slow/ 글을 발번역한 것입니다. 오역에 주의하시길 바랍니다. DB에 로그등을 저장할 경우, DB의 저장 속도가 로그 생성 속도를 따라가지 못하는 케이스가 꽤 있고, 그 해결책으로 제시되는 방법 중에 하나가, 로그를 메모리 캐시 솔루션등에 일시적으로 모으고 그걸 배치 등으로 한번에 저장하는 방법을 많이 이용합니다. 그런데, 해당 기사는 속도 때문에 해당 아키텍처를 사용하다가 MongoDB 1.8에서  2.0으로 전환하면서 해당 솔루션을 제거했습니다. 왜 그런지에 대해서 알아두시면 나름 좋은 방법이 될 듯합니다.

개인적으로는 Server Density에서 사용한 이전 방법이 부하가 더 커질 경우에 사용할 수 있는 좋은 방법이라는 생각이 듭니다. 그리고, 필자의 의견에 동의를 못하는 것이, 필자가 말하는 것처럼, Moxi에서 24ms 이 나오는 것도 이상합니다. 보통 moxi를 사용하더라도 1ms 이하로 나오는 것이 정상이기 때문입니다. 댓글을 보면 Moxi나 Client Library 문제가 아닌가라는 의심이 나옵니다. 개인적으로는 Moxi보다는 클라이언트 라이브러리 이슈나 네트워크 위치나 설정에 따른 문제가 아닐까 의심됩니다. 저도 실제로 네트웍 이슈로 해당 속도가 느려지는 것을 경험해본적이 있습니다. 그리고 memcached 메모리가 swapping 되기 시작하면 속도가 꽤 느려질 수 있습니다. 그러나 필자가 말하는 컴포넌트를 하나 제거할 수 있다라는 장점은 상당히 큰 매력이네요. 여러가지 관점에서 고민해보시면 좋을 듯 합니다. @lqez 님도 비슷한 방법으로 재미를 보신걸로 알고 있습니다.

Memcached를 제거하자. 그건 너무 느리다.

Server Density 코드베이스를 변경하면서, Memcached 를 시스템에서 제거한 이야기를 짧게하려고 합니다. 여기에는 두가지 목적이 있습니다.

  1. UI 캐싱: 계정 데이터를 초기 로딩할 경우( server list, alert list, users lists 등 ) MongoDB 에서 바로 읽어서 해당 데이터가 변경될 때 까지 캐시해두고, 변경되면, 캐시를  제거했습니다.
  2. 병목 MongoDB  1.8에서의 글로벌 락의 성능 영향으로 인해서  모니터링 postback 을 MongoDB에 바로 저장할 수 없었습니다.  그래서 Memcached 에 먼저 데이터를 넣고, 수 많은 웹 클라이언트와는 대조적으로 몇 개의 데몬을 통해서 저장해야 했습니다.


Performance map

이 방법은 MongoDB 2.0이 나오기 전까지는 꽤 잘 동작했습니다. 꽤 똑똑한 Lock 양보 정책으로 Global Lock의 영향이 크게 감소되었고, 2.2에서는 database 수준의 락킹으로 더 좋아질 것입니다. 차후에는 동시성 관련 부분도 더 좋아질 것입니다.

이미 코드베이스에서는 Memcached를 이용하는 것에 영향을 받는 부분은 이미 제거해뒀고, 마침내 Memcached를 완전히 제거할 수 있다는 것을 성능 지표에서 보여주고 있습니다. MongoDB로의 직접 접근이 훨씬 빨라졌기 때문입니다. 확실히,  우리의 평균 DB 쿼리 응답 속도는 0.43 ms 이고 Memcached 로 부터는 24.2ms 가 나옵니다.

Database throughput

Response time

모든 종류의 데이터가 중요한 데이터 저장소로써, 다수의 MongoDB 클러스터가 있고( 시간관련 데이터들은 따로 분리되어 있지만 )  2개의 샤드가 있고 각각의 샤드는 4대의 데이터 노드로 구성되었습니다. 각각의 샤드는 미국내의 워싱턴 DC와 San Jose 에 있는 각각의 데이터 센터에 있고, 각 서버는 8GB 메모리에,  Intel Xeon-SandyBridge Quad Core 3.4Ghz CPUs, 100GB SSDs 장비에 Ubuntu 10.04 LTS를 이용해서 MongoDB를 구동중입니다.  각각은 2Gbps 네트웍으로 연결이 되어 있습니다. ( 역자 주:  8GB 메모리였다면, 메모리를 좀 더 올리는 것도 방법이 아니었을 까 합니다. SSD가 속도가 확실히 빠르긴 하지만, 일단 메모리가 많으면, SSD 보다 더 빠릅니다. )

Nodes

Memcached를 제거함으로써, 시스템이 더더욱 간단해지고, 코어 소프트웨어 스택은 단지 Apache, PHP, python, MongoDB, Ubuntu 로만 구성될 수 있을 것입니다.  이로 인해 Memcached를 독립된 클러스터에서 동작시킬 필요가 없어졌습니다.  Moxi proxy의 failover 처리를 위한 모니터링을 추가할 필요도 없어졌습니다., Ubuntu LTS를 통한 공식 버전을 사용하길 원할 경우에, 또 다른 불편인, PHP와 Python 에 memcached library를 추가할 필요도 없어졌습니다. 무엇보다 24ms 의 응답시간을 제거할 수 있었습니다.

[발 번역] 잘가~~ Global Lock, MongoDB 2.0 vs 2.2

해당 글은 http://blog.serverdensity.com/2012/05/23/goodbye-global-lock-mongodb-2-0-vs-2-2/ 발 번역한 것입니다. 예전에 MongoDB의 Global Lock에 대해서 올라온 글(http://blog.pythonisito.com/2011/12/mongodbs-write-lock.html) 을 보면 MongoDB의 Global Lock이 성능에 어떤 영향을 주는 지 알 수 있습니다. 특히 메모리 사이즈 이상의 데이터가 올라갔을 때, 느려지는 원인 중에 이 Global Lock의 영향도 있었습니다.  그리고 2.2에서 다시 개선이 있다고 합니다. 제목만 보면 아시겠죠? 오역에 주의하시길 바랍니다.

아마도 MongoDB 에서 가장 악명 높고 자주 거론되는 문제가 Global Lock 이슈일껍니다.  Write 동작이 일어날때 전체 서버가 락이 된다는 것을 의미합니다. 엄청 안좋게 들리지만, 실제로,  성능에 끼치는 영향은 별로 없습니다. MongoDB 2.0 버전부터 이런 종류의 작업 동안 적절히 락을 포기하도록 개선이 있었기 때문입니다.(MongoDB 2.0 includes significant improvements)

확실히, MongoDB 1.8 에서는 데이터를 저장하기 전에 Memcached를 이용해서 insert를 조절했지만, 2.0으로 업그레이드 한 후에는 직접 MongoDB로 저장하고 이런 부분을 제거했습니다. 이 부분의 향상은 some benchmarks at the end of last year comparing v1.8 to 2.0 에서 잘 설명되어 있습니다.(역자 주: 제가 예전에 올린 발번역 글 중에도 해당 글이 있습니다. https://charsyam.wordpress.com/2012/01/04/%EB%B0%9C-%EB%B2%88%EC%97%AD-mongodb-write-lock/ )

FaultingWrites

그럼에도 불구하고, 2.2 버전의 중요 이슈는 Global Lock의 제거와 데이터베이스 Lock을 collection 수준의 Lock으로 변경하고 더 미래에는 더욱 더 세분화된 동시성  제어 방식으로 변경하는 것입니다.(역자 주: 일반적인 DBMS는 Global Lock(서버락), 데이터베이스 Lock, Table(collection) Lock, Page Lock, Record Lock 수준으로 볼 수 있습니다. 후자일 수록 Lock의 범위가 적습니다.)

2.2 버전에서의 개선 부분은 2 가지 입니다.

  • global reader/writer lock 의 제거 – 첫 시도로 database level lock
  • PageFaultException 구조 – page fault 시에 Lock 양보.

첫째로, database level Lcok으 구현된건 맞지만, 제대로 사용하기 위해서는 어플리케이션에서 몇가지 아키텍처 변경이 필요합니다. 예를 들어, 하나의 큰 collection을 사용하면,  하나의 커다란 database를 사용한느 것이기 때문에, 거의 차이가 없습니다. 하지만,  두번째, 동시성 향상의 경우(Lock 양보 정책) 하나의 collection 에서도 업그레이드만 하면 바로 이득을 볼 수 있을 듯 합니다.

10gen 의 CEO이자 MongoDB의 최초 개발자인 Dwight Merriman는 MongoSF에서 MongoDB의 변화와 구조에 대해서 좋은 얘기들을 많이 했습니다. 다음 .비디오를 보시길 추천합니다.

10gen 에서는 실제 사용과는 관련이 없다는 이유로, 공식 벤치마크는 제공하지 않습니다. 개인적으로 업그레이드 후에 영향받는 쿼리등을 확인하기 위해서는 benchRun 같은 도구를 이용해야 합니다.  버전에 따른 이런 차이점을 확인하는데는 벤치마크가 유용합니다.

Rick Copeland 는 꽤 멋진 벤치마크를 했고 이것은 1.8과 2.0 사이의 향상ㅇ 대해서 보여주고 있습니다. 그래서 1.8, 2.0과 대비해서 2.1를 테스트 하기로 했습니다. 2.1은 2.2 버전을 위한 개발 버전입니다.

Comparing v1.8, v2.0 and v2.1

Rick이 사용했던 Amazon EC2(m1.large)에 같은 AMI를 이용해서 완전히 동일한 코드를 사용했습니다.  같은 데이터베이스를 설치하고 Page Fault Read/Write에 대해서 벤치마크를 수행했습니다. Non Page Fault 상황은 하지 않았는데, 2.2의 주요 변경이 Page Fault 와 Locks 을 어떻게 다루는가였고, Non Page Fault는 큰 차이가 없었습니다. 그건 항상 빠르기 때문입니다.

그리고 MongoDB 가 큰 데이터를 가지고 있다고 가정했습니다. 보통, 모든 데이터와 인덱스가 메모리에 있는 상황이 아니라 워킹셋만 메모리에 존재할 것이기 때문입니다.(역자 주: 모든 데이터가 메모리에 있으면, Non Page Fault 상황이라고 봐도 크게 무방할 듯 합니다.)

MongoDB Faulting Ops/s

위의 그래프에서 확실하게 Rick의 결과를 재현했습니다. 그리고 MongoDB 2.1의 테스트 결과를 보면, 그 차이가 매우 확연합니다.  read 중에 write에 실패하는 수에 상관없이 성능이 확 떨어지는 부분이 없습니다. 이것은 global lock이 확실하게 사라졌기 때문입니다. 아래의 그래프를 봅시다.

MongoDB Faulting Lock

Rick의 도구도 이용했고, 추가로 mongod에서 무슨 일이 일어났는지 확인하기 위해서 mongostat을 이용해서 통계를 모았습니다.  확실히 global lock 에 사용된 시간은 0% 입니다.

Conclusions

많은 데이터베이스를 사용할 때 큰 성능을 주기 위해서 MongoDB 2.2에서 global lock은 사라졌습니다. 그러나 실제로 업그레이드 할만한 영향을 미치는 것을 PageFaultException 개선을 통한 Lock 양보 작업입니다.  이것은 MongoDB가 write 시에 동기화 작업이 일어나기 전에 page fault 를 발견하고 해당 페이지를 건드리기 때문입니다.

첫번째 그래프는 테스트 동안에 급격한 저하없이 일관적인 성능을 유지한다는 것을 보여줍니다. Rick의 코드 하나의 데이터베이스에서 쿼리를 했고, 벤치마크는 두번째 변경인 PageFaultException의 향상으로 부터 실제 향상이 일어난 것을 보여줍니다. 대부분 업그레이드를 할 이유가 여기에 있을 것입니다.  해당 버전에서 여러 데이터베이스를 통한 벤치마크에서는 얼마나 성능 향상을 보여줄지 흥미롭습니다.

MongoDB 소스로 살펴본 Replica Set 방식에서의 FailOver – 1

MongoDB는 10gen에서 만든 가장 인기있는 NoSQL 중의 하나입니다. 간단하게 그 인기를 소개하자면 NoSQL계의 MySQL이라고 할까요?

그리고 대부분의 이런 종류는 FailOver를 위한 기능을 제공합니다. MongoDB는 Master-Slave 방식과 Replica Set이라는 두 가지 방식을 제공하는데, 대부분 Replica Set을 이용합니다.

그럼 이 Replica Set에서 실제 장애가 났을 때 어떻게 FailOver 되는지가 궁금하지 않으신가요?

그렇죠. 거기 머리 크신 분… 너무너무 궁금하다구요. 안 알려주면 직접 찾아오시겠다구요? 찾아오실 때는 양손을 무겁게 먹을것으로(퍽… 돌만 가득…) 이번에는 실제로 MongoDB에서 어떻게 Replica Set에서 FailOver를 하게 되는지 살짝 살펴보고 소스에서는 그 부분이 어디인지, 살펴보도록 하겠습니다.

먼저 FailOver 가 되기 전에, 이와 밀접한 관계가 있는 Replication을 살펴보도록 하겠습니다.

repl.cpp 파일을 보면 startReplication 함수에서 replication이 시작하게 됩니다.

void startReplication() {
//replSet이 설정되어 있을 때, master나 slave가 설정되면 오류를 표시하고 종료합니다.
//즉 우리는 여기서, replSet 과 master/slave 는 MongoDB에서 공존할 수 없다 라는 것을 알 수 있습니다.</pre>
if( !cmdLine._replSet.empty() ) {
 if( replSettings.slave || replSettings.master ) {
 log() << "***" << endl;
 log() << "ERROR: can't use --slave or --master replication options with --replSet" << endl;
 log() << "***" << endl;
 }
//newRepl()을 호출하고, startReplSets 라는 Thread를 동작 시킵니다.
//newRepl은 단순히 replSettings.master 를 true로 셋팅하고 _logOp를 _logOpUninitialized로 설정합니다.
newRepl();

replSet = true;
 ReplSetCmdline *replSetCmdline = new ReplSetCmdline(cmdLine._replSet);
 boost::thread t( boost::bind( &startReplSets, replSetCmdline) );

return;
 }

oldRepl();

/* this was just to see if anything locks for longer than it should -- we need to be careful
 not to be locked when trying to connect() or query() the other side.
 */
 //boost::thread tempt(tempThread);

if( !replSettings.slave && !replSettings.master )
 return;

{
 dblock lk;
 replLocalAuth();
 }

if ( replSettings.slave ) {
 assert( replSettings.slave == SimpleSlave );
 log(1) << "slave=true" << endl;
 boost::thread repl_thread(replSlaveThread);
 }

if ( replSettings.master ) {
 log(1) << "master=true" << endl;
 replSettings.master = true;
 createOplog();
 boost::thread t(replMasterThread);
 }

while( replSettings.fastsync ) // don't allow writes until we've set up from log
 sleepmillis( 50 );
 }

이제 rs.cpp 의 startReplSets 함수를 따라가 봅니다.

    void startReplSets(ReplSetCmdline *replSetCmdline) {
        Client::initThread("rsStart");
        try {
            assert( theReplSet == 0 );
            if( replSetCmdline == 0 ) {
                assert(!replSet);
                return;
            }
            replLocalAuth();
            //replication에 사용되는 중요한 전역변수 theReplSet을 생성합니다.
            //그리고 theReplSet 의 go 를 호출합니다.
            (theReplSet = new ReplSet(*replSetCmdline))->go();
        }
        catch(std::exception& e) {
            log() << "replSet caught exception in startReplSets thread: " << e.what() << rsLog;
            if( theReplSet )
                theReplSet->fatal();
        }
        cc().shutdown();
    }

_go 함수 안에서 loadLastOpTimeWritten 을 호출해서 가장 최신의 Operation Time 을 가져옵니다.

    /* call after constructing to start - returns fairly quickly after launching its threads */
    void ReplSetImpl::_go() {
        try {
            loadLastOpTimeWritten();
        }
        catch(std::exception& e) {
            log() << "replSet error fatal couldn't query the local " << rsoplog << " collection.  Terminating mongod after 30 seconds." << rsLog;
            log() << e.what() << rsLog;
            sleepsecs(30);
            dbexit( EXIT_REPLICATION_ERROR );
            return;
        }

        changeState(MemberState::RS_STARTUP2);
       //실제 FailOver를 위한 heartbeat 스레드 생성 및 replication 스레드 생성
        startThreads();
        newReplUp(); // oplog.cpp
    }

이제 startThread로 넘어가 보도록 하겠습니다.

    /** called during repl set startup.  caller expects it to return fairly quickly.
        note ReplSet object is only created once we get a config - so this won't run
        until the initiation.
    */      
    void ReplSetImpl::startThreads() {
        task::fork(mgr);
    //여기가 실제 Fail을 확인하기 위한 heartbeat를 하는 msgCheckNewState를 호출합니다.)
    //결국 heartbeat 의 결과는 Manager::msgCheckNewState 가 처리하게 됩니다. 이게 핵심
        mgr->send( boost::bind(&Manager::msgCheckNewState, theReplSet->mgr) );
        
    //여기서 replication이 시작됩니다.
        boost::thread t(startSyncThread);
    
        task::fork(ghost);
    
        // member heartbeats are started in ReplSetImpl::initFromConfig
    }

이제 갑자기 코드가 어려워집니다. msgCheckNewState는 health thread 에 체크한 결과로 Primary를 뽑아야 할 때, 호출됩니다.

    /** called as the health threads get new results */
    void Manager::msgCheckNewState() {
        {
            theReplSet->assertValid();
            rs->assertValid();

            RSBase::lock lk(rs);

            if( busyWithElectSelf ) return;

            checkElectableSet();
            checkAuth();

            const Member *p = rs->box.getPrimary();
            if( p && p != rs->_self ) {
                if( !p->hbinfo().up() ||
                        !p->hbinfo().hbstate.primary() ) {
                    p = 0;
                    rs->box.setOtherPrimary(0);
                }
            }

            const Member *p2;
            {
                bool two;
                p2 = findOtherPrimary(two);
                if( two ) {
                    /* two other nodes think they are primary (asynchronously polled) -- wait for things to settle down. */
                    log() << "replSet info two primaries (transiently)" << rsLog;
                    return;
                }
            }

            if( p2 ) {
                /* someone else thinks they are primary. */
                if( p == p2 ) {
                    // we thought the same; all set.
                    return;
                }
                if( p == 0 ) {
                    noteARemoteIsPrimary(p2);
                    return;
                }
                // todo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                if( p != rs->_self ) {
                    // switch primary from oldremotep->newremotep2
                    noteARemoteIsPrimary(p2);
                    return;
                }
                /* we thought we were primary, yet now someone else thinks they are. */
                if( !rs->elect.aMajoritySeemsToBeUp() ) {
                    /* we can't see a majority.  so the other node is probably the right choice. */
                    noteARemoteIsPrimary(p2);
                    return;
                }
                /* ignore for now, keep thinking we are master.
                   this could just be timing (we poll every couple seconds) or could indicate
                   a problem?  if it happens consistently for a duration of time we should
                   alert the sysadmin.
                */
                return;
            }

            /* didn't find anyone who wants to be primary */
            if( p ) {
                /* we are already primary */

                if( p != rs->_self ) {
                    rs->sethbmsg("error p != rs->self in checkNewState");
                    log() << "replSet " << p->fullName() << rsLog;
                    log() << "replSet " << rs->_self->fullName() << rsLog;
                    return;
                }

                if( rs->elect.shouldRelinquish() ) {
                    log() << "can't see a majority of the set, relinquishing primary" << rsLog;
                    rs->relinquish();
                }

                return;
            }

            if( !rs->iAmPotentiallyHot() ) { // if not we never try to be primary
                OCCASIONALLY log() << "replSet I don't see a primary and I can't elect myself" << endl;
                return;
            }

            /* no one seems to be primary.  shall we try to elect ourself? */
            if( !rs->elect.aMajoritySeemsToBeUp() ) {
                static time_t last;
                static int n;
                int ll = 0;
                if( ++n > 5 ) ll++;
                if( last + 60 > time(0 ) ) ll++;
                log(ll) << "replSet can't see a majority, will not try to elect self" << rsLog;
                last = time(0);
                return;
            }

            if( !rs->iAmElectable() ) {
                return;
            }

            busyWithElectSelf = true; // don't try to do further elections & such while we are already working on one.
        }
        try {
            rs->elect.electSelf();
        }
        catch(RetryAfterSleepException&) {
            /* we want to process new inbounds before trying this again.  so we just put a checkNewstate in the queue for eval later. */
            requeue();
        }
        catch(...) {
            log() << "replSet error unexpected assertion in rs manager" << rsLog;
        }
        busyWithElectSelf = false;
    }

작년에 쓴 글인데, 일단은 살짝 정리개념으로 포스팅 합니다. -_- 뒤에를 더 분석해야 제대로된 건데 말이죠. 일단은 1로 하고 2는 곧 다시 정리하도록 하겠습니다.

[책 리뷰] MongoDB 활용 가이드

국내에 MongoDB 관련 번역서가 딱 3권이 있는데, 한권이 “MongoDB 완벽가이드” 이고, 나머지 두 권이 바로 이 “MongoDB 활용 가이드이다.” 뭔가 이상하지 않은가? 왜 한권의 책을 두 권이라고 말을 할까?

 

지금부터  “MongoDB 활용 가이드”의 엄청난 비밀을 하나 소개하려고 한다.  해당 서적은 실제로는 책 두 권을 번역해서 한 권으로 합쳐놓은 것이다. 놀랍지 않은가? 원래 해당 서적은 오렐리에서 권당 50페이지 정도의 책으로 나왔었다. 책 표지는 “Scaling Mongo” 라는 MongoDB의 샤딩부분만을 다룬 책이고, 나머지는 절반은 “50 Tips & Tricks for MongoDB Developers” 라는 책이다.  서문을 유심히 읽어보신 분이라면 이 사실을 알아채셨을테지만, 의외로 모르시는 분들이 많은 듯 하다. 이 책은 MongoDB 완벽가이드를 쓴 크리스티나 초도로우가 모두 지은 책이다.

 

재미난 것은 “완벽 가이드”는 윤진석님이, 이 책은 이진석님이 번역을 하셨다는 것인데 ^^

 

기본적으로 이 책은  PART2 -> PART1 순으로 보는 것이 좋지 않을까 싶다.  PART2는 정말 MongoDB 활용에 관한 부분이고, PART1은 샤딩에 관련된 부분이기 때문이다.  보통 MongoDB는 SchemaLess 와 샤딩때문에 각광을 받지만, 개인적으로는 적당한 데이터 규모에서 빠른속도와 사용하기 편리함이 주된 사용 이유이지 않을까 싶다.( 개인적으로는 MongoDB가 RDBMS의  mysql 비슷한 존재라고 주장하고 싶다는 ^^)

 

두 권을 번역해서 한 권으로 만든 책이지만,  책 페이지 수도 100페이지 정도밖에 되지 않아서 가볍게 읽을 수 있다는게 장점이다.(물론 그 안의 내용을 이해하는 것은 다른 얘기지만)

 

물론, 대다수의 NoSQL 관련 책들이 그렇긴 하지만, 실제 규모에서 써먹을 수 있는 특별한 팁들을 제공하지는 않는다. 하지만, 최소한의 지켜야 할 것들에 대해서 미리 인지시켜준다고 해야할까? MongoDB에 대해서 잘 모른다면, 꼭 읽어보기를 추천한다.

 

[발 번역] MongoDB With RAID 10 on Ubuntu 11.04

다음 글은 MongoDB with Raid 10 on Ubuntu 11.04(http://blog.troygrosfield.com/2011/11/08/mongodb-with-raid-10-on-ubuntu-11-04/) 라는 글을 발번역한 것입니다. 오역에 주의하세요.

MongoDB With RAID 10 on Ubuntu 11.04

최근에 MongoDB 서버를 RAID 10에 설정했다. 왜 RAID 10을 적용했고, 어떻게 했는지에 대해서 공유하려고 한다. 모든 작업은 아마존 AWS 위에서 Ubuntu 11.04 를 기반으로 작업했다.

1. RAID 10이란 무엇인가?

2. 왜 RAID 10을 선택했는가?

3. Ubuntu 11.04에서 RAID 10 과 MongoDB 설정하기

4. RAID 모니터링

5. 장애 대응

1] RAID Array에서 디스크가 장애나면 어떻게 해야 하는가?

2] 장애난 디스크를 어떻게 교체하는가?

3] RAID가 깨지면 어떻게 해야하는가?(역자주: 디스크가 하나 깨지는 것과 RAID가 깨지는 것은 다른 현상이다.  이는 뒤부분을 보면 알 수 있다.)

4] 이전의 MongoDB 장비를 어떻게 새로운 RAID MongoDB로 변경하는가?

6. 리소스

1. What is RAID 10

RAID 는 Redundant Array of Inexpensive/independent Disks의 약어이다.

위키디피아에서는 다음과 같이 정의되어 있다.

RAID 1+0: (a.k.a. RAID 10) mirrored sets in a striped set (minimum four drives; even number of drives) provides fault tolerance and improved performance but increases complexity.

The key difference from RAID 0+1 is that RAID 1+0 creates a striped set from a series of mirrored drives. The array can sustain multiple drive losses so long as no mirror loses all its drives.

MongoDB에서의 RAID 10 사용은 Data Replication 뿐만 아니라, EBS 드라이버의 장애시에도 매우 잘 동작한다.

“MongoDB의 Replica Sets만으로 충분하지 않나요?” 라고 물어본다면, 아래의 왜 RAID10을 선택했는지를 보면 된다.

2. Why We Chose RAID 10

mongodb master 장비에서 EBS 드라이브가 장애가 나면,  개발 시스템에서 항상 Single Point of Failure 였다.  해당 장비를 Replica Set에서 제외하기 전까지 항상 시스템이 다운되었다. MongoDB Replica Set은 EBS 드라이브가 고장이 났을 때 가 아니라, 실제 하드웨어 장비(역자주: 여기서는 VM장비 다운)가 문제가 있을 때만 FailOver가 동작했다. EBS 드라이브가 장애가 나면(올해에만 3번 정도 발생했다.) Replicat Set에 있는 나머지 두 장비로 FailOver 되지 않았다.  we would get some nasty PROD error emails.

RAID 10으로 셋팅을 하게 되면, SPOF를 막아주고, RAID 장애시에 올바르게 Replica Set의 다른 장비로 FailOver 되게 해주었다.

RAID 10 은 mongodb 에서 추천하는 RAID 레벨이다.(http://www.mongodb.org/display/DOCS/Production+Notes#ProductionNotes-RAID)

3. Setup RAID 10 with MongoDB on Ubuntu 11.04

MongoDB를 위한 RAID 생성 방법은 아래와 같습니다.(역자주: 꼭 MongoDB만을 위한 건 아니겠죠?)

1] (AWS 에서만) AWS Console 에서 MongoDB 서버로 동작할 EC2 instance를 생성합니다.

2] (AWS에서만) 생성한 EC2 instance에서 사용할 4개의 동일한 크기의 EBS 볼륨을 같은 zone에서 생성합니다.

3] (AWS에서만) 4개의 생성된 EBS 볼륨을 생성한 EC2 장비에 추가합니다. 어디에 추가해야 되는지 물어볼 수 있는데, /dev/sdp4, /dev/sdp5, /dev/sdp6, /dev/sdp7 으로 추가하면 됩니다. 숫자는 크게 중요하지 않습니다. 그냥 다르게 만들면 됩니다.

* ubuntu 11.04 AMI 를 생성하면, /dev/sdp# 대신에 /dev/xsdp# 에 생성될 수도 있다. https://forums.aws.amazon.com/thread.jspa?messageID=205549 를 참고하자.

4] ssh로 EC2 장비에 접속한다.

5] 새로 추가된 드라이브를 확인한다.

$ ls /dev
// The ouput here should show you all of your drives on this
// box. Make note that you don't have md0 (this is where 
// we will create the RAID) and see where you're newly
// attached EBS' are located. You should see something like:
...
xvdp4
xvdp5
xvdp6
xvdp7

6] RAID 관리를 위해서 mdadm 을 이용할 것이다. mdadm은 소프트웨어 RAID를 관리하는 리눅스 유틸리티이다.

$ sudo apt-get install mdadm

7] mdadm이 설치되면, EBS drive 로 RAID를 생성한다.

$ sudo mdadm --create --verbose /dev/md0 --level=10 --chunk=256 --raid-devices=4 /dev/xvdp4 /dev/xvdp5 /dev/xvdp6 /dev/xvdp7

-create 는 RAID를 생성하는 명령

-verbose 는 결과를 출력한다.

/dev/md0 는 어디에 RAID Array를 생성할지 지정한다. md0이 사용중이면 md1을 선택한다.

-level 은 RAID 레벨을 결정한다.

-chunk 는 Chunk 크기를 의미한다.(http://tldp.org/HOWTO/Software-RAID-HOWTO-5.html#ss5.10)

-raid-devices 는 RAID에 포함될 디바이스를 수를 의미한다.

-/dev/xvdp4 /dev/xvdp5 /dev/xvdp6 /dev/xvdp7 는 RAID를 구성할 EBS 드라이브를 의미한다.

8] RAID에 파일 시스템을 생성한다.

$ sudo mke2fs -t ext4 -F /dev/md0

MognoDB에서는 ext4나 xfs를 권장한다.(http://www.mongodb.org/display/DOCS/Production+Notes#ProductionNotes-LinuxFileSystems)

9] MongoDB  EBS RAID로 마운트할 디렉토리를 생성한다.

$ sudo mkdir -p /ebs

10]  fstab 에 해당 설정을 추가한다.

$ sudo vi /etc/fstab
// Add the following line at the bottom of the file
/dev/md0        /ebs     auto    defaults,nobootwait,noatime     0       0

11] RAID를 ebs 폴더로 마운트한다.

$ sudo mount /dev/md0 /ebs

12] RAID가 성공적으로 생성되었고 동작하는지 상태를 확인한다.

$ sudo mdadm --detail /dev/md0
// Sample healthy RAID output:

...[omitted]...

 Active Devices : 4
Working Devices : 4
 Failed Devices : 0

...[omitted]...

 Rebuild Status : 6% complete

...[omitted]...   

Number   Major   Minor   RaidDevice State
   0     202      244        0      active sync   /dev/xvdp4
   1     202      245        1      active sync   /dev/xvdp5
   2     202      246        2      active sync   /dev/xvdp6
   3     202      247        3      active sync   /dev/xvdp7

빌드 상태에 대한 확인은 설정된 볼륨 사이즈에 따라 긴 시간이 소요될 수 있다. 120GB 짜리 EBS 4개를 구성하는데 2.5시간이 걸렸다.(역자주: 꽤 오래걸리네요.)

13] RAID setup이 완료되면,  EC2 장비에 MongoDB 서버를 설치하고 Replica Set 에 추가하자.

이것으로 RAID 설정이 끝났다. 이게 전부다.

* AWS를 사용했지만, RAID가 AWS에서만 구성되는 것은 아니다. 장비에 디스크를 추가하고, 위의 방법대로 RAID를 구성할 수 있다.

4. Monitor the RAID

mdadm 은 RAID가 정상적인지를 모니터링 할 수 있는 –monitor 라는 기능을 가지고 있다. 매 10분 마다 RAID가 장애가 났는지를 체크해서 장애가 났다면 메일을 보내주는 cron 작업을 생성하자.

1] RAID 상태를 체크하고 , 장애가 나면 메일을 보내주는 스크립트를 생성한다.

#!/bin/sh

STATUS="$(cat /proc/mdstat | grep '(F)')"

if [ "$STATUS" != "" ]
then
    DETAILED_STATUS="$(sudo mdadm --detail /dev/md0)"
    MESSAGE="Failed device on $STATUS\n\n$DETAILED_STATUS"
    sendEmail -f some_from_email@gmail.com \
              -t someadminemail@gmail.com \
              -u 'Failed device on RAID' \
              -m "$MESSAGE" \
fi

2] 매 10분마다 동작하도록 Crontab에 등록한다.

# Job to monitor the mongo RAID array to make sure no devices went bad.
# If a devices is indeed bad, an email will be sent to the admins.
# This task runs ever 10 minutes.
*/10 * * * * sudo sh /location/to/monitor/script/RaidMonitor

3] cron 을 시작한다.

$ crontab /location/to/crontab/file/jobs.crontab

5. Troubleshooting

1] What to do When a Device in An Array Goes Bad.

디스크가 하나 고장나서 교체할 필요가 있을 때, 어떻게 해야하는지 아래의 순서로 진행하면 된다. RAID array 상태를 체크하라. 여기에 두 가지 방법이 있다.

$ cat /proc/mdstat

해당 명령이 실행되는 동안에, RAID array의 상태를 표시하게 된다.

md0 : active raid10 xvdp7[3] xvdp6[2] xvdp5[1] xvdp4[0]

장애가 있다면, 다음과 같이 표시된다.

md0 : active raid10 xvdp7[3] xvdp6[2] xvdp5[1] xvdp4[0](F)

xvdp4 에 (F) 표시가 있는 것을 볼 수 있을 것이다. 이제 좀더 자세히 확인해보자.

$ sudo mdadm --detail /dev/md0

다음 샘플을 보면, 모든 장비가 정상적일 때는 다음과 같은 결과를 볼 수 있다.

...[omitted]...

 Active Devices : 4
Working Devices : 4
 Failed Devices : 0

...[omitted]...

Number   Major   Minor   RaidDevice State
   0     202      244        0      active sync   /dev/xvdp4
   1     202      245        1      active sync   /dev/xvdp5
   2     202      246        2      active sync   /dev/xvdp6
   3     202      247        3      active sync   /dev/xvdp7

만약 하나의 디스크가 장애가 났다면, 다음과 같은 결과를 볼 수 있다.

...[omitted]...

 Active Devices : 4
Working Devices : 3
 Failed Devices : 1

...[omitted]...

Number   Major   Minor   RaidDevice State
   0       0        0        0      removed
   1     202      245        1      active sync   /dev/xvdp5
   2     202      246        2      active sync   /dev/xvdp6
   3     202      247        3      active sync   /dev/xvdp7

   0     202      244        -      faulty spare   /dev/xvdp4

/dev/xvdp4 의 상태를 보면, 장애가 났고, RAID array에서 제거해야 될 필요가 있다는 것을 알 수 있다.

2] How to Replace the Faulty Device?

1) ssh로 MongoDB 마스터 장비로 접속한 다음 RAID array 에서 장애난 디스크를 제거한다.

$ sudo mdadm /dev/md0 -r /dev/xvdp4

2) AWS Console 에서 MongoDB 마스터 장비에 추가하기 위한 새로운 EBS 볼륨을 생성한다.  새로운 볼륨의 위치는 (/dev/xvdp8) 이라고 하자.

3) 볼륨이 추가되면,  장비에 추가한 후에, RAID Array 에 추가한다.

$ sudo mdadm /dev/md0 -a /dev/xvdp#

* ‘#’을 해당 볼륨의 번호로 변경하자. 추가된 볼륨의 번호는 AWS Console에서 확인할 수 있다.

4) 다음 명령을 실행시키자. RAID에 디스크가 추가되었고 sync가 시작되었는지 볼 수 있다.

$ sudo mdadm --detail /dev/md0

실행 결과는 다음과 같다.

...[omitted]...

 Active Devices : 3
Working Devices : 4
 Failed Devices : 0
  Spare Devices : 1

...[omitted]...

Rebuild Status : 6% complete

...[omitted]...

Number   Major   Minor   RaidDevice State
   0     202      244        0      spare rebuilding /dev/xvdp8
   1     202      245        1      active sync   /dev/xvdp5
   2     202      246        2      active sync   /dev/xvdp6
   3     202      247        3      active sync   /dev/xvdp7

5) Sync 가 완료되면 끝이다.

3] What to do if the RAID fails

매우 불행히도 RAID에 포함된 3개 이상의 EBS가 장애가 나면 발생하게 된다.

1) 다음 명령을 통해서 RAID의 상태를 체크한다.

$ sudo mdadm --details /dev/md0

2) state를 보여주는데 실패하면 다음을 계속 진행한다.

3) ssh로 해당 장비에 접속한다.

4) md 디바이스를 unmount 한다.

$ sudo umount /dev/md0

5) RAID Array를 정지 시킨다.

$ sudo mdadm -S /dev/md0

RAID를 정지시키면, Secondary Replica Set Member가 Primary가 된다.  그러므로, 시스템이 다운중이면, 해당 Array를 정지시키면, 서비스가 복구가 될 것이다.( 역자주: Secondary가 Primary가 되면서 서비스가 재개된다는 의미)

6) 정상적으로 동작하는 디바이스로 다시 RAID를 재생성한다.

$ sudo mdadm --create --verbose /dev/md0 --level=10 --chunk=256 --raid-devices=4 /dev/xvdp# /dev/xvdp# /dev/xvdp# /dev/xvdp#

7) RAID Array를 mount한다.

$ sudo mount /dev/md0 /ebs

8) 더 진행하기 전에 resync이 완료되기를 기다린다. 동작 상태를 체크하자.

$ sudo mdadm --detail /dev/md0

 

4] Replacing an old MongoDB Instance with a new MongoDB RAID Instance

새로운 MongoDB RAID 장비를 기존의 Replica Set에 추가하고 Master로 전환하는지에 대해서 보여준다.

1) 해당 포스터의 앞부분에 있는 방버법에 따라서 RAID MongoDB Instance를 생성하라

2)RAID Sync 가 끝나기를 기다려라. 이것은 꽤 시간이 걸린다.( 몇시간 정도 ) MongoDB Replica Set에 RAID장비를 추가할 수 있지만, RAID sync가 끝날때 까지 기다리는 것이 좋다. 다음 명령으로 sync의 상태를 체크할 수 있다.

$ sudo mdadm --detail /dev/md0

3) RAID sync가 끝나면, mongodb master shell로 가서, 새로운 mongo 장비를 replica Set에 추가한다.

$ rs.add("ec2-##-##-###-###.compute-1.amazonaws.com")

4) mongo RAID 장비가 다른 Replica Set 과 Sync 되기를 기다려라. 새로 추가한 장비의 shell로 가서, 데이터가 제대로 들어갔는지 테스트하라. 다른 Mongo Box 등과 유저수 등이 일치해야 합니다.

5) 모든 데이터가 sync 되었다고 확신하면, 다음과 같은 명령으로 해당 Mongo RAID 장비를 master로 설정합니다.

> config = rs.conf()
{
	"_id" : "foo",
	"version" : 1,
	"members" : [
		{
			"_id" : 0,
			"host" : "A",
		},
		{
			"_id" : 1,
			"host" : "B",
		},
		{
			"_id" : 2,
			"host" : "C",
		},
		{
			"_id" : 3,
			"host" : "D",   config.version++
> // the member number is the 0 based index in the list. Not the _id.  So
> // '3' is the 3rd index or 4th member in the array.
> // the default priority is 1
> config.members[3].priority = 2
> rs.reconfig(config)

http://www.mongodb.org/display/DOCS/Forcing+a+Member+to+be+Primary 를 보라.

해당 장비가 “Primary” 가 될 때 까지, 계속 체크하라.

6) mongo master shell을 이용해서 더 이상 사용하지 않는 mongo 장비를 제거하라. 더 이상 사용되지 않을 것이다. mongo master shell에 서 다음 명령어를 사용해서 제거한다.

> config = rs.config()
> config.version++
> // Find the members you want to keep.
> config.members = [config.members[1], config.members[2], config.members[3]]
> rs.reconfig(config)
> rs.status()

유지하기를 원하는 세 대의 mongodb 장비만을 볼 수 있다.

 

이제 새로운 RAID MongoDB 장비를 Replica Set에 추가하고 이전 장비를 제거했다.

 

6. Helpful Resources

http://tldp.org/HOWTO/Software-RAID-HOWTO.html

http://man-wiki.net/index.php/8:mdadm

[발 번역] MongoDB Write Lock

해당 내용은 http://blog.pythonisito.com/2011/12/mongodbs-write-lock.html?m=1 의 글을 발번역 한 것입니다. 오역에 주의하세요. 실제로 1.8 과 2.0 에 대한 소스의 변화는 다음번에 확인해서 어떤 변화가 있는지 한번 추가로 포스팅 하도록 하겠습니다.

 

MongoDB Write Lock

아는 사람들은 알고, 모르는 사람은 모르겠지만, MongoDB는 Process-Wide write Lock을 가집니다.( 역자 주: 프로세스가 하나의 Write Lock을 가지고 있다는 뜻입니다.  )  이런 Primitive Locking Model 때문에 순수 데이터베이스주의자들에게서 조롱거리가 되기도 합니다. 현재 MongoDB의 로드맵에는 콜렉센 레벨 락이라든지, 데이터베이스 레벨 락이 들어가 있긴하지만, 아직은 구현되지 않았습니다.  MongoDB Version 2.0 에서는 locking-with-yield  라는 기법이 들어갔다고 발표했습니다.  Write Lock 이 미치는 영향과 lock-with-yield 의 성능 향상이 얼마나 되는지 궁금해졌고, MongoDB 1.8 과 MongoDB 2.0의 간단한 벤치마크를 하기로 결정했습니다.

PAGE FAULTS AND WORKING SETS

MongoDB 는 디스크에 대한 쓰기를 위해 운영체제의 Virtual Memory를 사용합니다.( 역자 주: 정확히는 Memory Mapped 방식을 이용합니다.) 이 방식은 데이터 베이스의 모든 내용을 OS의 Virtual Memory에 매핑하고, OS 스스로 해당 페이지가 ‘hot’ 한지, 물리 메모리에 있을 필요가 있는지와, ‘cold’ 한지, disk로 swap-out 될 수 있는지 결정하게 합니다.( 역자 주:  현재의 대다수의 OS는 Virtual Memory를 이용하고, 이와 함께 페이징을 이용합니다. 페이징을 하게 될때, 실제 메모리 크기 때문에 물리 메모리에 올라가 있는 것과,  디스크에 있는 것이 생기게 됩니다. 이 때 메모리로 실제 내용을 올리는 것이 ‘Swap-In’, 디스크로 내리는 것을 ‘Swap-Out’ 이라고 합니다. 이 뿐만 아니라, 실제 메모리에 없는 페이지에 접근하게 되면, Page Fault 가 발생하고,  Page Fault 가 발생했을 때, 해당 Fault Handler 에서  필요한 메모리를 찾아서 실제 물리 메모리와 매핑시키게 됩니다. – 혹시나, 여기서 한번 더 Page Fault 가 발생하면, Second Fault 라고 해서 OS가 오류를 내게됩니다. – 또는 죽습니다.  – 자세한 건 리눅스 커널의 이해등의 OS 책을 보시길 추천드립니다.)  MongoDB 역시 메모리에 있는 데이터베이스가 디스크와 주기적으로 싱크합니다.(MongoDB는 데이터를 유실을 방지하는 저널링 시스템도 가지고 있지만, 이 부분은 해당 글의 범위를 넘어갑니다.)

이것이 무슨 말이냐 하면, 특정 메모리 페이지가  메모리에 실제로 존재하는가 아닌가에 따라서, 서로 다른 동작을 하게 된다는 것입니다. 메모리에 없다면, 이걸 Page Fault 라고 부릅니다.  query나 update는 디스크에서 필요한 데이터를 로딩하는 매우 느린 동작을 기다려야 합니다. 반대로 데이터가 메모리에 있다면(OS는 이걸 ‘hot’  페이지라고 합니다.) , update는  단순히 System RAM에 쓰기만 합니다. 매우 빠릅니다. 이것이 10gen( MongoDB 제작사 ) 에서 가능한 전체 Working Set(빈번하게 접근하게 되는 데이터)을 유지할 수 있는 충분한 메모리를 장착하라고 항상 추천하는 이유입니다. (역자 주: 여기서 메모리의 의미는 실제 물리 메모리를 의미합니다. Page Fault는 가상 메모리가 실제 메모리 공간보다 크므로, 이를 실제 메모리에 매핑하지 못해서 발생하는 현상입니다. – 실제로는 물리 메모리보다 큰 메모리 공간을 사용하기 위한 기법으로 고안이 되었습니다.)

만약 이렇게 할 수 있다면 Global write Lock은 거의 영향을 미치지 못합니다.  쓰기 완료를 위해서 읽기가 대기하는 시간은 겨우 수 Nanoseconds 라서 문제가 되지 않습니다.( 이걸 실제로 측정하지는 못했지만, Global Write Lock의 획득 시간이 실제 Write 작업보다 시간이 많이 더 걸릴까요? )  결국, non-faulting 상황에서 write 수행율이 얼마나되는지 측정했고, 거의 영향이 없다라는 것을 발견했습니다. ( 역자 주: 원문에 But this is all kind of hand-waving at this point. 라는 문장이 있는데, 무슨 뜻인지 모르겠네요. 아시는 분 알려주세요. )

2.0  TO THE RESCUE?

Write를 하는 중에, Lock을 획득하고,  Lock을 잡고 있는 중에, 쓰기를 시도하고, Page Fault 가 발생한다면, Page Fault 는 Non Fault 동작보다 40,000 배 정도 더 느리기 때문에, 문제를 일으킬 수 있다. MongoDB 2.0이나 그 상위 버전에서는 Page Fault 의 가능성을 감지해서 Page Fault 전에 Lock을 해제 함으로써 이 문제를 해결했다. (역자 주: 그래서 write lock with yield 인가 봅니다. 직역하면 Write Lock 과 포기 정도일까요?)  이것이 정말로 측정하길 원했던 것입니다. 이전의 ‘Write Lock’ 에 비해서  ‘write lcok with yield’ 의 효과는 무엇일까?

BENCHMARK SETUP

첫번째로, 가능한 재현 가능한 벤치마크를 만들고 싶고, 내 노트북에 돌리는 것들로 인해서 발생할 요소들에 대해서 걱정하고 싶지 않아서, 아마존 EC2에서 벤치마크를 돌리기로 결정했다. ( 작성한 코드들은 여기(here  – fill_data.py, lib.py combined_benchmark.py) 에 있습니다. 만약 단순히 결과를 보고 싶다면 ami-63ca1e0a off AWS 이미지를 이용하기 바랍니다. ) 임시 디스크를 database store로 사용하는 ubuntu 서버를 m1.large 장비에서 사용하였습니다. 또한 noatime 성능 향상을 목적으로 임시 디스크를 ext3에서 ext4로 재포맷했습니다. MondoDB 는 10gen .deb repositories 로 부터 version 1.8.4(mongodb18-10gen) 와 2.0.2(mongodb-10gen)를 설치했습니다.

데이터베이스는 하나의 Collection을  1억5천만개의 112-byte 다큐먼트로 채웠습니다. 최종 데이터 크기는 m1.large 장비의 메모리가 7.5GB 이므로 이것을 초과하기 위해서 선택했습니다.(역자 주:  대략 150MB * 112 하면 16.8GB 정도 되겠네요.) (Collection의 크기는 인덱스를 포함해서 가상메모리에서 21.67GB 정도 차지합니다.)  저널링은 1.8과 2.0 모두 off 시켰습니다.

Read 와 동시에 발생하는 Write의 성능을 측정하고 싶었고, 이를 위한 로드를 발생하기 위해서, Python 벤치마크 코드는 gevent를 이용해서 두 개의 greelets 를 생성했습니다. 하나는 10,000개의 다큐먼트 Working Set 에서 랜덤하게 읽는 것이고, non-faulting 상태에서의 read 성능을 보기 위한 것입니다. 두 번째 greenlet은 1억 5천만개의 전체 문서 중에서 특정 주기로 랜덤하게 Write 를 하는 것입니다.  write 와 read 측정 결과를 그래프로 생성했습니다.

RESULTS

첫번째로 non-faulting write 와 non-faulting read 의 성능을 보여주길 원했고, read 와 마찬가지로 처음 10,000 개의 Working Set 안에서만 write가 되도록 제한했습니다. 그 결과, write lock 은 거의 성능 저하가 없었습니다. 그리고 이 경우에 1.8과 2.0은 거의 차이가 없었습니다.

다음 테스트에서는 1.8과 2.0의 차이를 뚜렷하게 보여줍니다. 1.8에서는 faulting write 가 나타나면 read 성능이 바로 떨어지는 것을 볼 수 있습니다. 초당 48 write를 하는 동안에 거의 읽기가 발생하지 않는 습니다. 2.0 에서는 같은 Write 레벨에서 최대 성능의 60% 정도를 내고 있습니다.( 초당 48 fauling write 이상 부터는  page fault를 처리하는 것이 디스크의 전체 성능의 대부분을 차지해서 실제 성능을 측정하기 어렵게 만듭니다.)

 

CONCLUSION

MongoDB 의 Global Write Lock이 사라져도 전혀 슬프지 않다. 하지만, 여기서 본 것처럼,  non-faulting reads 와  writes 상황에서는 기대했던 것 보다 성능 차이가 거의 없다.( Working Set의 크기가  RAM의 사이즈에 맞춰져 있다면), write lock이 성능을 떨어트리는 것은 write 수 가 증가하거나, faulting writes 가 발생할 때이다.  write with yield 라는 2.0의  접근방법은 가끔씩 발생하는 fauting writes의 성능에 대한 나쁜 영향을 완화시킨다.

[발 번역]아마존 EBS 위의 MongoDB 백업하기

해당 문서는 http://blog.fiesta.cc/post/13127613694/backing-up-mongodb-instances-on-ebs 의 Backing up MongoDB instances on EBS 라는 글을 발 번역한 글입니다. 오역에 주의하세요.

 

아마존 EBS 위의 MongoDB 백업하기

Replica Set과 Journaling 을 사용함에도 불구하고, 정기적으로 데이터를 백업하는 것은 여전히 좋은 생각입니다.( 역자 주: MongoDB에서는 데이터의 안전성을 위해서 REplica Set과 Journaling을 함께 사용하는 것을 권장합니다. 개발 노드에서도 Journaling을 안 쓸 이유가 없다라고 하더군요.)  MongoDB 유저나, 다른 사람들로 부터,  꽤 복잡한 시스템에 정기적인 백업을 하지 않는다는 얘기를 자주 듣고 놀랍니다. 정기 백업은 Fiesta 의 백업 인프라스트럭처를 빠르게 대충 살펴보는데 많은 도움을 준다고 생각합니다.  누구든지 어떻게 이런 작업들을 다루는지 얘기해 준다면 경청하도록 하겠습니다.

 

Architecture

아마존 EC2 위에서 Single Replica Set으로 동작하고 있습니다. Secondary 장비를 백업을 위해서 사용하고 있습니다. 그리고 EBS위에서 동작하고 있습니다. 실제 백업을 위해서 아마존 Snapshot을 이용하며,  S3에 저장해둡니다. 모든 데이터는 MongoDB와 S3에 나뉘어져 있습니다. MongoDB 데이터를 어떻게 백업할 것인지에 초점을 맞추고 있습니다.

 

Journaling

MongoDB 1.8.x 부터 Journaling 이 들어갔습니다. Journaling 을 켜두면, MongoDB 데이터 디렉토리의 hot snapshot 을 만들 수 있습니다. 우리는 현재 Journaling 을 사용하고 있지는 않고 있습니다. 좀 더 조심스럽게 접근하기 위해서입니다. 현재는 백업노드에서 snapshot을 만들기 위해서 fsync 와 lock을 사용하고 있습니다.  해당 작업을 하는 코드는 다음과 같습니다.

 

The Code

다음 코드는 실제로 백업을 수행하는 코드입니다. lock_and_backup 함수는 매일 밤마다 cron 으로 동작합니다.


import subprocess

from pymongo import Connection
import settings
def do_ebs_backup():
 env = {"EC2_HOME": settings.ec2_home,
 "JAVA_HOME": settings.java_home}
 keyfile = EC2_KEY
 certfile = EC2_CERT
 args = ["-K", keyfile, "-C", certfile]

# We use the 'backup' tag to find the volumes to back up
 out = subprocess.Popen([settings.ec2_tools + "bin/ec2-describe-volumes",
 "-F", "tag:env=backup"] + args,
 stdout=subprocess.PIPE,
 env=env).communicate()[0]
 volumes = set([v for v in out.split() if v.startswith("vol-")])
 if not volumes:
 raise Exception("No volumes to backup?")

# Create a snapshot for each volume we found above
 snaps = []
 for volume in volumes:
 snap = subprocess.Popen([settings.ec2_tools + "bin/ec2-create-snapshot"] + \
 args + [volume],
 stdout=subprocess.PIPE,
 env=env).communicate()[0]
 snaps.append(snap)
 return snaps
def lock_and_backup():
 conn = Connection(slave_okay=True)
 try:
 conn.admin.command("fsync", lock=True)
 do_ebs_backup()
 finally:
 conn.admin["$cmd.sys.unlock"].find_one()
<pre>

 

Snapshot을 위해서 EC2 command-line 툴을 이용하고 있으며, Python의 Subproecess 모듈을 해당 툴과의 연동을 위해서 사용하고 있습니다.  가장 중요한 작업은 lock_and_backup() 함수이며, fsync 커맨드의 실행과, snapshot 전에 쓰기 Lock을 설정합니다.

(역자 주: 코멘트 중에 왜 boto를 안쓰냐는 얘기가 있습니다.  boto는 아마존 Ec2를 위한 오픈소스 Python 툴입니다. 현재 boto 말고도, python 쪽에 libcloud 라는 좋은 툴이 있는데, boto는 정말 아마존 특화된 기능들이 많습니다.)

MongoDB 최신 버전 빌드하기 – 2.1.0 pre 기준

MongoDB 를 git에 있는 최신버전으로 빌드해서 scons로 빌드를 하려고 하면 제대로 빌드가 되지 않습니다.

가장 큰 이유는 MongoDB가 2.x 버전으로 가면서 기존의 SpiderMonkey 버전 대신에 v8을 기본 JavaScript 엔진으로 바꾸려고 하기 때문입니다.

git clone git://github.com/mongodb/mongo.git 으로 소스를 받고 (임의상 repo/charsyam/mongo/ 라고 지정합니다. )

이때 boost 라이브러리는 기본 위치(/usr/local) 에 설치 되어있어야 합니다. boost 설치는 워낙 잘 나와있어서 생략합니다.

그리고 scons 도 설치가 되어 있어야 합니다.(이것도 생략)

 

그리고 또 v8도 설치되어야 합니다.

svn checkout http://v8.googlecode.com/svn/trunk/ v8

이 때 v8의 경로가 mongodb 소스 기준으로 (../v8) 이어야 합니다.

그런다음 mongodb 소스에서 scons all –usev8 이런식으로 하시면 잘 빌드가 됩니다.

(참고 사이트: http://www.howsthe.com/blog/2010/feb/22/mongodb-and-v8/)

자세한 내용은 다음 스레드를 참조하시면 합니다. http://groups.google.com/group/mongodb-dev/browse_thread/thread/faf14db204ea7e5d

다음 jira를 보시면 v8로의 변환이 현재 어떻게 진행되고 있는지 알 수 있습니다. 지금 2.0.2 인데 2.1.1 에서나 끝날꺼 같네요.  https://jira.mongodb.org/browse/SERVER-2407

PyMongo에서의 MongoDB Replica Set에서의 FailOver 과정

지난 온라인 서버 제작자 모임에서 누군가가 질문을 하셔서 간단하게 정리합니다. MongoDB에서는 Master-Slave 와 Replica-Set 이라는 두 가지 방법을 제공하고 대부분 Replica-Set을 사용합니다. 그리고 이 경우 Primary가 죽을 경우, 자동적으로 새로운 Primary 가 선출되게 됩니다. 예를 들어 현재 저의 머신에는 다음과 같이 세 개의 mongodb instance를 실행시켜두었습니다.

 

charsyam 5657 1 0 Dec07 ? 00:03:23 /home/charsyam/mongo/mongod –dbpath /home/charsyam/data/mongodb/db1 –port 10001 –replSet charsyammongo/localhost:10002
charsyam 7016 6583 3 01:23 pts/11 00:00:46 /home/charsyam/mongo/mongod –dbpath /home/charsyam/data/mongodb/db2 –port 10002 –replSet charsyammongo/localhost:10001
charsyam 7399 6583 0 01:29 pts/11 00:00:03 /home/charsyam/mongo/mongod –dbpath /home/charsyam/data/mongodb/db3 –port 10003 –replSet charsyammongo/localhost:10001,localhost:10002

 

pymongo를 이용해서 connection을 만듭니다.

 

>>> from pymongo import Connection
>>> db = Connection(“localhost:10001″, replicaset=”charsyammongo”).test
>>> db.test.insert( {‘x’:1} )

ObjectId(‘4edf948ce50cd21aea000000′)
>>> db.test.find_one()

{u’x’: 1, u’_id’: ObjectId(‘4edcf0e4e50cd2389f000001’)}

>>> db.connection.host
localhost

>>> db.connection.port

10001

 

db.connection.host 와 db.connection.port를 이용해서 지금 어디에 연결되어 있는지를 확인할 수 있습니다.

localhost:10001 에 연결되어 있다고 가정하고 해당 instance를 한번 종료시켜 보겠습니다.

 

kill -9 5657

 

그리고 다시 db.test.find_one()를 하게 되면 다음과 같은 에러가 발생합니다.

 

Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
File “pymongo/collection.py”, line 518, in find_one
for result in self.find(spec_or_id, *args, **kwargs).limit(-1):
File “pymongo/cursor.py”, line 699, in next
if len(self.__data) or self._refresh():
File “pymongo/cursor.py”, line 662, in _refresh
self.__uuid_subtype))
File “pymongo/cursor.py”, line 612, in __send_message
**kwargs)
File “pymongo/connection.py”, line 868, in _send_message_with_response
sock = self.__socket()
File “pymongo/connection.py”, line 680, in __socket
sock, from_pool = self.__pool.get_socket(host, port)
File “pymongo/connection.py”, line 165, in get_socket
self.sock = (pid, self.connect(host, port))
File “pymongo/connection.py”, line 127, in connect
s.connect((host, port))
File “<string>”, line 1, in connect

 

상황에 따라서 다음과 같은 오류가 나기도 합니다.

>>> db.test.find_one()

Trackback (most recent call last):

pymongo.errors.AutoReconnect: …

 

그리고 다시 시도하면 오퍼레이션 도중에 자동으로 접속을 하게 됩니다. Connect 부분에서 call stack을 찍어보면 다음과 같습니다. 참고문서를 보면 AutoReconnect는 이전 primary에 접속할 수 없을 경우에 발생한다고 합니다.

 

(Pdb) w

<stdin>(1)<module>()

/home/charsyam/repo/charsyam/pymongo/pymongo/collection.py(518)find_one()

-> for result in self.find(spec_or_id, *args, **kwargs).limit(-1):

/home/charsyam/repo/charsyam/pymongo/pymongo/cursor.py(699)next()

-> if len(self.__data) or self._refresh():

/home/charsyam/repo/charsyam/pymongo/pymongo/cursor.py(662)_refresh()

-> self.__uuid_subtype))

/home/charsyam/repo/charsyam/pymongo/pymongo/cursor.py(612)__send_message()

-> **kwargs)

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(871)_send_message_with_response()

-> sock = self.__socket()

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(671)__socket()

-> host, port = self.__find_node()

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(641)__find_node()

-> node = self.__try_node(candidate)

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(580)__try_node()

-> response = self.admin.command(“ismaster”)

/home/charsyam/repo/charsyam/pymongo/pymongo/database.py(344)command()

-> _uuid_subtype = uuid_subtype)

/home/charsyam/repo/charsyam/pymongo/pymongo/collection.py(518)find_one()

-> for result in self.find(spec_or_id, *args, **kwargs).limit(-1):

/home/charsyam/repo/charsyam/pymongo/pymongo/cursor.py(699)next()

-> if len(self.__data) or self._refresh():

/home/charsyam/repo/charsyam/pymongo/pymongo/cursor.py(662)_refresh()

-> self.__uuid_subtype))

/home/charsyam/repo/charsyam/pymongo/pymongo/cursor.py(612)__send_message()

-> **kwargs)

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(871)_send_message_with_response()

-> sock = self.__socket()

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(674)__socket()

-> sock, from_pool = self.__pool.get_socket(host, port)

/home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(168)get_socket()

-> self.sock = (pid, self.connect(host, port))

> /home/charsyam/repo/charsyam/pymongo/pymongo/connection.py(138)connect()

-> if self.use_ssl:

 

참고문헌: http://api.mongodb.org/python/current/examples/replica_set.html

 

 

간단한 mongodb 2.0.1 replSet 설정 방법(localhost 일때)

start.sh 보통 3대의 구성으로 많이 이용하는 듯 합니다.

#!/bin/sh

/home/charsyam/mongo/mongod –dbpath /home/charsyam/data/mongodb/db1 –port 10001 –replSet charsyammongo/localhost:10002 &
/home/charsyam/mongo/mongod –dbpath /home/charsyam/data/mongodb/db2 –port 10002 –replSet charsyammongo/localhost:10001 &
/home/charsyam/mongo/mongod –dbpath /home/charsyam/data/mongodb/db3 –port 10003 –replSet charsyammongo/localhost:10001,localhost:
10002 &

 

이렇게 실행만 시키면 다음과 같은 오류가 나기 시작합니다.

Sat Nov 19 00:41:48 [rsStart] trying to contact localhost:10002
Sat Nov 19 00:41:48 [rsStart] replSet can’t get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Nov 19 00:41:49 [rsStart] trying to contact localhost:10001
Sat Nov 19 00:41:49 [rsStart] replSet can’t get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Nov 19 00:41:49 [rsStart] trying to contact localhost:10001

 

이제 mongo localhost:10001 로 접속을 하고 다음과 같이 입력합니다.

config = {_id: ‘charsyammongo’, members: [
{_id: 0, host: ‘localhost:10001’},
{_id: 1, host: ‘localhost:10002’},
{_id: 2, host: ‘localhost:10003’}]}

rs.initiate(config)

 

그러면 자기들 끼리 마스터를 선출하기 시작합니다.

이제 에러로그가 다음과 같이 바뀝니다.

 

Sat Nov 19 00:42:27 [rsHealthPoll] replSet member localhost:10001 is now in state PRIMARY
Sat Nov 19 00:42:27 [rsHealthPoll] replSet member localhost:10003 is now in state RECOVERING
Sat Nov 19 00:42:27 [rsHealthPoll] replSet member localhost:10001 is now in state PRIMARY

 

설정 끝!!!