[입 개발] Cache에서의 Eviction, Expiration, Passivation

Cache를 공부하다 보면 다음과 같은 개념들을 만날 수 있습니다. Eviction 과 Expiration. 이게 비슷한 행동을 하고, 비슷한 내용으로 보이는 케이스가 있지만, 사실상 완전히 다른 개념입니다. 넵, 저도 맨날 오해하고 있지만… 그래서 여기에 대해서 간단하게 적어보려고 합니다.

캐시 서버는 속도를 위해서 대부분 메모리를 사용합니다. 디스크를 써도 마찬가지겠지만, 메모리를 쓰면 더더욱 용량이 모자라기 때문에, 이 공간을 어떻게 써야 하는지에 대해서 문제가 발생하게 됩니다. 하지만, 어떻게 아끼든 결국 스토리지의 한계는 오기 마련이고, 이제, 데이터 공간을 확보하기 위해서 무엇을 해야할까요?

넵, 정답은, 안쓰는 데이터를 지우는 겁니다. 뭐, 이걸 지우는 전략은 여러가지가 있을 수 있지만, 일반적으로는 LRU를 많이 사용합니다. 다른 전략들 역시 검색해보면 다 나옵니다. 이렇게, 공간이 필요할 때, 어떤 데이터를 지우는 것을 Eviction 이라고 합니다.

그럼 Expiration은 뭘까요? 넵, 말 그대로 유통기한이 있는겁니다. 특정 값에 Expiration을 걸어두면, 어떻게든 나중에 해당 시간이 지나면, 해당 값이 없어지는 겁니다. 사실 그래서 Eviction 을 Expiration을 이용해서 구현하기도 합니다. 즉, 이 데이터가 하루 지나면 필요없는 데이터다 싶으면 Expiration을 1 Day로 설정하는 겁니다.

그런데, 이 두개의 개념에는 어마어마한 차이가 있습니다. 그것은 Eviction 은 사용자가 명시적으로 의도하지 않은 동작인 것이고, Expiration은 사용자가 이 녀석을 지우겠다라고 명시적으로 의도한 것이라는 것입니다. 뭐, 둘다 지워지는 건 마찬가지다라고 하더라도, 이 두 개념의 차이에 대해서 아는 것은 매우 중요합니다.(정말?) 실제로 캐시 내부에서도 이 두 개의 삭제처리는 보통 다른 형태로 구현이 됩니다.

즉 Expiration은 실제 읽을 때 시간이 지났으면 삭제가 가능하지만, Eviction은 메모리가 모자라서 수행되므로, 항상 작업 전에 수행이되어야 합니다.

마지막으로 Passivation 은 뭐냐, 저도 잘 모릅니다만 ㅋㅋㅋ Infinispan에서 지원한다고 합니다. 메모리에서 삭제되었을때, 이게 디스크에 읽어들이는 것보다, 연산시간이 많이 걸리거나 할때, 실제로 메모리에서 바로 지우지 않고 Eviction 되는 값을 디스크등의 다른 스토리지에 저장하는 것입니다. 뭐, 이런 개념들이 있으니, 한번씩 곰곰히 집고 넘어가면 좋을듯 합니다.

마세라티 문제에 대한 말도 안되는 개인적인 생각

마세라티 문제라는 것이 있다. 일단 여기 관련된 내용은 http://blog.kivol.net/post/51143318331 를 보면 이해할 수 있을 것이다. 사실 여기에 대해서 반박(?) 하기 위해서 쓰는 것은 아니고, 위의 블로그의 내용이 지극히 옳은 내용이다. 나도 거의 동감한다.

일반적인 서비스가 초반에는 당연히 서버 한대에(그것도 사양이 낮은) 웹서버든, 디비서버든 다 한방에 몰아넣고, 일단은 서비스 내용에 집중해야 하는것이 맞다. 여기서 Failover든 확장가능한 구조로 만든다든지 하는 것은 어떻게 보면 과도한 오버엔지니어링이고, 스타트업의 귀중한 시간을 날리는 문제일 수 있고, 또한, 그 규모가 되버리면, 돈이 벌린다는 뜻이니, 과감히 돈으로 좋은 엔지니어를 사서 돈으로 커버하면 된다.

그런데, 최근에 카카오톡/라인 이나 페이스북 게임(이게 과연 최근일까?) 의 성장과 함께 개인적으로는 완전히 새로운 문제가 생겨버렸다. 그것은 마세라티를 너무 빨리 타야 하는 경우가 생기는 것이다.

다음 뉴스를 참고하면(http://economyplus.chosun.com/special/special_view_past.php?boardName=C11&t_num=6965&img_ho=) 다음과 같은 기록이 나온다.

  • 애니팡: 39일 천만 다운로드
  • 캔디팡: 20일 천만 다운로드
  • 드래곤플라이트: 30~40일 사이에 천만 다운로드

자, 일반적인 웹서비스가 출시 한두달만에 사용자가 천만명을 넘을 수 가 있을까? (뭐, 이게 플랫폼의 힘이긴하다.) 위의 게임들이 사실 카카오에 별로 게임이 없던 시절에 출시된 초기 게임이기도 하지만, 카카오톡이나 라인의 게임 출시는 이럴 가능성이 충분히 있다는 것이다. 특히, 현재처럼 게임이 출시가 많은 시기에, “확장성”을 고민하지 않고 출시해서 최소 2주정도를 확장하게 고친다고 서비스가 제대로 안된다면, 과연 게임이 성공할 수 있을까?

즉, 특정 분야에서는 시작과 동시에 “마세라티 문제”를 고민해야 할지도 모른다는 것이다. 그런데 이런 확장에 대해서 얼마의 지식이나 경험을 가지고 있을지도 의문이기는 하다. 물론, 이런 게임의 대부분이 서버와의 통신이 많은 게임이 아니기 때문에, 기존의 MMORPG나 MO에서 사용하는 형태의 기술력이 필요하지는 않아보이지만, 그러나, 시스템의 구조가 확장가능한가, 아닌가에 따라서 많은 영향을 받을 수 있다라고 생각한다. 드래곤플라이트가 하루에 10억 매출일 때가 있었는데, 이런 시기에 2주 정도 서비스가 안된다면, 쿨럭…

분명히 개인적으로 “마세라티 문제”는 일반적인 스타트업에서는 고민하지 않고 서비스의 내용에 일단 집중하는게 맞다고 생각한다. 하지만, 이런 특수한 경우에는 처음부터 장비를 갖출 필요는 없지만, 최소한, 확장 가능한 구조로 잡고 시작하는 것은 이제 당연한 일이지 않을까 싶다. 이런 카카오 게임의 특성상 클라우드 환경이 각광받는 이유도 사실 이런 몇일 만에 수백만명 이상이 늘었다가, 다시 수백만명이 줄어들 수 도 있다. “확장 가능한 구조” 라는 건, 증가도 쉽지만, 규모를 줄이는 것도 쉽다는 의미이기 때문에, 도리어 “마세라티 문제” 가 더 이상 “마세라티 문제” 가 아닌 당연히 해결해야 하는 이슈인 부분도 있다는 얘기다.(역시 뻘글!!!, 주제가 뭐지?)

초보개발자의 DevOps에 대한 생각

먼저 나는 DevOps에 대해서는 잘 모른다. 이 Role이 어떤 역할을 수행해야 하는지, 또는 왜 각광받게 되는지 이런 부분에 대한 이해가 떨어질 수 있기 때문에, 그냥 초보 개발자의 그냥 잡소리로 생각해주면 좋을듯 하다.

사실 나는 DevOps라는게 기존의 패키지 소프트웨어 개발자보다는 서비스 개발자를 특성 때문에, 어떻게 각광받고 있는게 아닐까 싶다. 그러나 어떻게 생각하면, DevOps라는 것은, 어떤 낭비가 줄어든다는 느낌(?) 때문이지 아닐까 싶기도 하다.

먼저 Software Engineer 에 대해서 생각해보자. 프론트 엔드 개발자이든 백엔드 개발자이든 상관없이, 먼저 주된 업무는 서비스를 개발하는 일이다. 배포나 이런 작업을 일단 제외로 하고, 서비스에서 장애가 발생했다. 갑자기 웹페이지에 파일 업로드가 되지 않는다. 당연히 해당 기능의 개발자가 먼저 소스코드를 확인할 것이다. 그러다가 소스코드에서 문제가 없다면, 이것저것 테스트해볼 것이다. 아, 이 장비에서만 문제가 발생하고, 다른 장비에서는 문제가 없구나라고 한다면, 해당 장비의 셋팅이나 서버의 설정등을 살펴보고 디스크에 남아있는 용량도 확인한 다음, 이것도 동일하면, 장비가 이상해요라고 하고 System Engineer에게 확인을 요청할 것이다. 그러면 System Engineer가 이에 대해서 장비를 확인하고 디스크에 문제가 있다든지, 아니면 inode가 다차서 실제로는 디스크 용량은 남지만 파일시스템에 더 쓸 수 없어서 문제가 발생한 것을 알고 이에 대해서 파일시스템을 재 포맷해야 한다거나, 쓸데 없는 파일을 지우면 된다고 해답을 주게 될 것이다.

그럼, 이제 이런 지식에 대해서 잘 알고 있는 개발자라면, 소스코드에 이상이 없고 해당 장비에만 문제가 생긴다는 걸 확인하면, 아마도 디스크 용량과, inode 개수를 확인해보고, 쓸모없는 파일을 지운다든지의 행동을 통해서, 해당 장비를 바로 복구하고 서비스에 투입할 수 있을것이다. 즉, 전체 문제를 해결하는 시간이 줄어든다.

이제 반대로 생각해보자. 어떤 Software 개발자가 서비스에 버그를 만들었지만, 이를 찾지 못해서, System Engineer에게 책임을 떠 넘긴다고 하자. tcpdump로 패킷을 잡고, dummy 서버를 만들어서 직접 동작을 확인할 수 있다면, 훨씬 빨리 개발자의 잘못이라는 것을 알 수 있다. 쓸데 없는 시간낭비를 줄일 수 있다.

Software Engineer 든 System Engineer든 사람이라면 보통 안정을 추구한다. 잘 돌아가는 코드가 있다면 이쪽 수정은 하지 않고 살짝 돌려서 만들고 싶은 욕망이 생기고, 안정적인 서비스에 뭔가 새로운 시도를 하기란 쉽지가 않다. 하지만, 항상 마일스톤은 정해져 있고, 서비스는 진화해야 하기 때문에, 변화는 피할 수 없고, 결국 이 변화에 빨리 대응할 수 있는 개발자가 당연히 요구되어지는 것이다. 결국 Software Engineer는 System의 영역을 System Engineer는 Software의 영역을 서로 침범하고, 서로의 영역을 넓혀 갈 수 밖에 없다고 생각한다.

그러나, 그것이 완전히 같아지기는 힘들꺼라고 보인다. 점점 더 도메인이 넓어지면서, 배워야 할 것이 늘어나고 혼자서는 모든 것을 알기가 힘들어지고 있다. 물론, 자신의 도메인에 대한 깊이있는 지식도 중요하지만, 다른 영역의 지식을 알아야만, 좀 더 훌륭한 Sotware Engineer 그리고 System Engineer가 될 수 있지 않을까? 결국 DevOps 는 Developer + Operator 의 합성어처럼 둘 다 다하라는 의미보다는 다른 도메인도 이해해서 생산성을 올릴 수 있는 Engineer가 필요하다라는 것의 다른 표현이 아닐까 싶다.

짧은 개발 경력동안에도, 발생하는 문제는 항상 복합적이었다. 어떤것은 리눅스 OS 커널의 정책때문이었고, 어떤 것은 네트웍 장비에 대한 이해 부족이었다.(물론, 대부분은 내가 실수한 내 로직 버그였지만) 그리고, 이 문제를 해결하기 위해서 Software, System 구분 없이 지식이 필요했다.

결국, DevOps 라는 단어는 서로 상대방의 영역에 대해서 조금 더 이해하고, 더 훌륭한 엔지니어가 되자라는 의미가 아닐까 싶다. 아, 뻘글이 산으로 가는구나… 원래는 DevOps가 되면 무조건 생산성이 좋아지고, 인력이 줄어들어도 된다라는 그런 내용에 대한 개인적인 반박이었는데, 이미 주제도, 내용도 안드로메다구나 T.T

[입 개발] Redis 데이터 모델링시에 주의할점

Redis를 사용하는 큰 이유중에 하나가 여러가지 자료구조를 제공해준다는 점입니다. set, list, sorted set, hash 등 그래서, 실제로 유저별 데이터를 prefix:id 또는 id:data_prefix 형태로 데이터를 set/get을 이용하거나 hset/hget/hgetall 등을 이용해서 데이터를 가져옵니다. 사실 이것은 적합한 형태는 서비스의 모델마다 틀리고, 어떤 목적으로 이용할 것인가에 따라서 틀리므로, 서비스에 맞게 설계하고, 사용해보면 됩니다.

예를 들어, 트위터와 비슷한 서비스를 만들때, 팔로워 정보를 list 나 set, sorted set에 넣어둘 수도 있고, hash를 이용할수도 있습니다. 즉 뭘해도 크게 상관이 없다는 것입니다. 바로 찾을 필요가 있다면 hash에 넣을것이고, 전체 순회만 한다면 list를 써도 되는거죠. 그런데, 여기서 놓치기 쉬운 문제가 하나 숨어있습니다.

자 list 나 set 등의 collection 에 친구목록을 넣는다고 합니다. 어떤 유저는 친구가 너무 많아서 백만명이고, 어떤 유저는 몇백명 또는 몇명일 수 있습니다. 그런데 이제 이 사용자가 탈퇴를 해야해서 데이터를 지우려고 합니다. 어떻게 해야할까요?

 del [key]

물론, 위의 형태로 간단하게 삭제가 가능합니다. 그러면 어떤 문제가 발생할까요? 레디스의 철학은 짧은 시간에 지울 수 있는 일만 하자입니다. 그래서 단순히 백만개 정도의 데이터를 지운다고 하면 1초 정도의 시간이 걸립니다. 자 그럼 백만개의 아이템이 들어있는 set을 지우는데는 얼마의 시간이 걸릴까요? 넵 당연히 1초 정도가 걸릴겁니다. 그러면, 이 시간동안 레디스는 다른 작업을 처리할 수 가 없습니다.

그래서 이런 삭제 문제가 사실 레디스에서 처리하기 어려운 문제입니다. 그렇다면 이런 문제는 어떻게 해결해야 할까요? 일단 다음과 같은 룰을 지켜야 합니다. 데이터 콜레션에는 데이터를 일정이상 넘기지 않는다. 많아도 몇천개 정도로 한정합니다. 그리고 이 몇천개의 데이터를 가지는 key 목록에 대해서 다시 한번 목록을 유지합니다.

즉 다음과 같은 형태가 됩니다.
목록(set1, set2, set3)
|
|
set1 —— set2 ——– set3

이걸 특정시간에 다 지우지 않고 한번에 한 set들만 지우는 것입니다. 이래야만 실제로, 하나의 거대한 작업을 쪼개서 처리할 수 있게 됩니다. 이런 테크닉은 사실, 싱글 스레드에서 대량의 데이터를 다뤄야 하는 경우에는, 다른 곳에서도 많이 응용되고 있습니다. 긴 작업은 여러 개의 작은 작업으로 나눠서 단계별로 처리하게 하는것이죠.

[입 개발] Redis 에서 Key의 Expire는 어떻게 처리되는가?

Redis를 사용하다보면 Key 의 Expire가 어떻게 처리되는지 궁금할 때가 있습니다. 결론부터 말하자면, Redis에서는 Key가 두 가지 방법으로 Expire가 될 수 가 있습니다.

  1. memcached 처럼 key에 대한 접근이 발생할 때
  2. activeExpireCycle 에 의한 삭제
  3. command 처리 전에 memory가 부족할 때 메모리 정책에 따라서 삭제

memcached의 경우 expire 된 key의 경우 그 시점에 지워지지 않고 실제로 해당 key에 접근이 되는 시점에 없다라고 돌려주게 됩니다. Redis도 기본적으로 get등의 operation이 발생할 때 key의 expire가 되었는지를 체크하게됩니다. 이 때 getExpire라는 함수를 이용해서 expire time을 가져오게 됩니다.

long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;

    /* No expire? return ASAP */
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;

    /* The entry was found in the expire dict, this means it should also
     * be present in the main dict (safety check). */
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    return dictGetSignedIntegerVal(de);
}

그런데 dictFind를 보면 db->expires에서 key를 찾습니다. 그렇다는 얘기는 expire되는 key들을 따로 관리한다는 의미가 될것 같습니다. 그럼 어디서 이것을 변경하게 될까요? getExpire대신에 setExpire라는 함수가 있습니다.

void setExpire(redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    kde = dictFind(db->dict,key->ptr);
    redisAssertWithInfo(NULL,key,kde != NULL);
    de = dictReplaceRaw(db->expires,dictGetKey(kde));
    dictSetSignedIntegerVal(de,when);
}

실제로 모든 key는 db->dict에 저장되고 db->expire에서 expire값을 가진 key만 추가로 관리합니다. 그래서 실제로 dbDelete에서는 두 군데서 해당 포인터를 제거합니다.

그런데 이런 작업 이외에도 expire 된 key가 지워집니다. aof rewrite시에 실제로 보고된 적은 없지만, expire된 key가 타이밍에 따라서 지워지지 않고 aof에 저장될 수 있는 버그를 수정하면서, 중간중간 계속 key가 먼저 지워져서 고민했던 적이 있는데 이 부분이 바로 activeExpireCycle 때문입니다.

100ms 마다 actvieExpireCycle이 database들을 돌면서 랜덤하게 expire된 데이터를 삭제합니다. timeout이 있고, 이 시간마다 다음 작업할 DB를 기억하고 있다가 완료될 때 까지 작업을 하게 됩니다. timelimit은 다음과 같이 정해집니다.

timelimit = 1000000*REDIS_EXPIRELOOKUPS_TIME_PERC/server.hz/100;

마지막으로 memory정책에 따라서 command 처리전에 메모리 확보를 위해서 key를 지우게 됩니다.
이 작업은 freeMemoryIfNeeded 에서 진행하게 됩니다.

[발 번역] Apache HBase Internals: Locking and Multiversion Concurrency Control

해당 글은 http://blog.cloudera.com/blog/2013/01/apache-hbase-internals-locking-and-multiversion-concurrency-control/ 라는 글을 발 번역한 것입니다. 오역에 주의하시길 바랍니다.

Apache HBase Internals: Locking and Multiversion Concurrency Control

[노트] 해당 글은 Apache HBase 가 어떻게 concurrency control 을 하는지 설명하고 있습니다. 이 글은 HBase Write Path에 대해서 알고 있다고 가정하고, 추가적인 부분은 다음 글을 읽기를 바랍니다.

Introduction

Apache HBase 는 Consistent 하고 이해할 수 있는 데이터 모델을 사용자에게 제공하면서도 높은 성능을 냅니다. 이 글에서, HBase 데이터 모델의 보장성에 대해서 얘기하고, 전통적인 데이터베이스와 어떻게 다른지 설명합니다. 그리고, concurrent 한 쓰기에 대해서 공부함으로 써, concurrency control 의 필요에 대해서 이야기 하고, 간단한 concurrency control 방법을 소개합니다. 마지막으로,  읽기/쓰기 concurrency control에 대해서 공부하고 MVCC라고 불리는 효과적인 방법에 대해서 이야기할 것입니다.

Why Concurrency Control?

HBase의 concurrency control을 이해하기 위해서, 먼저 HBase에서 왜 concurrency control 이 필요한지 이해해야 합니다. 어떤 속성들로인해 concurrency control 이 필요한 데이터에 대해서 HBase에서 보장해 주는지에 대해서 살펴보겠습니다.

이에 대한 대답은 HBase는 Row 단위의 ACID semantics 를 보장합니다. ACID는 다음 특성들의 첫글자를 딴 것입니다.

  • Atomicity: 모든 트랜잭션은 완료되거나 실패되어야 한다.
  • Consistency: 올바른 데이터만 저장되어야 한다.
  • Isolation: 동시에 발생하는 트랜잭션이 서로에게 영향이 없어야 한다.
  • Durability: 트랜잭션이 커밋되면, 해당 결과가 유지되어야 한다.

전통적인 관계형 데이터베이스에 경험이 있다면, 이 단어들이 친숙할 것입니다. 전통적인 관계형 데이터베이스는 일반적으로 데이터베이스의 모든 데이터에 ACID semantics를 제공합니다. 성능상의 이유로, HBase는 오직 ACID semantics를 Row단위로만 제공합니다. 이 단어들이 익숙하지 않더라도, 걱정하지 마십시오. 정확한 정의에 대해서 살펴보는 대신에, 간단한 예들을 보도록 하겠습니다.

Writes and Write-Write Synchronization

HBase 의 {company, role} 에 동시에 두개의 쓰기가 발생하다고 가정합니다.



Image 1.  Two writes to the same row

이전에 올렸던 HBase Write Path 에서, 각 쓰기에 대해서 다음과 같은 작업을 수행한다는 것을 알고 있습니다.

(1) Write-Ahead-Log (WAL) 쓰기

(2) MemStore 업데이트: (row, column) 쌍의 각 data cell 을 memstore 에 쓴다.

List 1. Simple list of write steps

재해 복구의 목적으로 WAL을 작성하고, 데이터를 메모리에(MemStore)에 저장합니다.

이제, 다음과 같은 순서로 이벤트가 발생한고, concurrency control 이 없다고 가정하자.


Image 2.  One possible order of events for two writes

결과적으로 다음과 같은 결과를 얻게 될 것이다.

Image 3.  Inconsistent result in absence of write-write synchronization

결코 용납할 수 없는 일입니다. ACID 에서 말하는, 쓰기에 대한 Isolation을 제공하지 않았기 때문에, 두 개의 쓰기가 섞여 버렸습니다.

이래서 확실히 concurrency control 이 필요합니다. 간단한 방법은 배타 락을 Row마다 제공해서 같은 Row를 업데이트 해야할 경우, 쓰기가 독립적으로 일어나도록 합니다.(파란색 안에 새로운 단계가 있습니다.)

(0) Row Lock 획득
(1) Write-Ahead-Log (WAL) 작성
(2) MemStore 업데이트: memstore 에 각 셀의 내용을 저장
(3) Row Lock 반환

List 2: List of write-steps with write-write synchronization

Read-Write Synchronization

이제, ACID semantics를 보장하기 위해서 쓰기에 row lock을 추가합니다. 읽기를 위해서도 concurrency control이 필요할까요? 예를 들어, 다음과 같은 순서로 이벤트가 일어난다고 가정합니다.( List 2의 규칙을 따릅니다.)


Image 4.  One possible order of operations for two writes and a read

읽기에 대해서는  Concurrency control 이 없고, 두 개의 쓰기가 발생하는 중에 read를 동시에 요청했다고 하고, 그리고 read 가 “Waitor”가 MemStore에 저장되기 직전에 바로 실행되었다고 가정합니다. 이 읽기는 그림에서 붉은 줄로 표시되어 있습니다. 이 경우에, read에는 row 데이터의 불일치가 발생하게 됩니다.



Image 5.  Inconsistent result in absence of read-write synchronization

그러므로 읽기-쓰기 동기화를 위해서도 concurrency control이 필요합니다. 간단한 방법은 쓰기와 같은 방법으로 읽기 시에도 락을 획득하는 방법입니다. 이 방법은 ACID 위반은 해결할 수 있지만, row lock으로 인해서 읽기와 쓰기에 성능저하가 일어나게 됩니다.

대신, HBase 에서는 row lock을 읽기를 위해서 획득하는 대신에 Multiversion Concurrency Control (MVCC) 라는 방법을 이용합니다. HBase 에서 MVCC는 다음과 같이 동작합니다.

쓰기:

(w1) RowLock 획득 뒤에, 각 쓰기 연사은 즉시 쓰기 번호를 할당 받는다.
(w2) 각 데이터 셀은 쓰기 번호를 저장한다.
(w3) 각 쓰기 연산은 쓰기 번호를 저장하면서 완료한다.

읽기:

(r1) 각 읽기 연산은 읽기 시간을 할당 받고, 이를 read point라고 한다.
(r2) 각 read point는  쓰기 번호 중에 가장 높은 값(완료된 값 중에서) 을 할당 받는다.
(r3)각 읽기 r은 r 의 read point 와 같거나 작은 것 중에서 쓰기 번호가 가장 높은 것을 가지는 (row, column)과 일치하는 데이터 셀에서 (row, column) 의 조합을 가져온다.


List 3. Multiversion Concurrency Control steps

그림 4의 동작을 다시 살펴보자, 여기서 MVCC 를 사용하고 있다.


Image 6.  Write steps with Multiversion Concurrency Control

MVCC 때문에 새로운 단계가 추가되었다. 각 쓰기는 쓰기 번호를 할당받고(w1), 각 데이터 셀은 memstore에 자신의 쓰기 번호와 함께 저장한다.(w2, 예를 들어 “Cloudera [wn=1]“) , 그리고 쓰기 번호를 저장하면서 각각의 쓰기는 완료된다.(w3)

이제 그림 4에서의 읽기에 대해서 생각해 보자. 예를 들어 “Restarant[wn=2]” 이 후, “Waiter[wn=2]” 단계 이전에 읽기가 발생한다면, r1 과 r2의 규칙에 따라서, read point는 1이 할당된다. r3로 인해서 읽기에서는 쓰기 번호가 1인 것의 값을 읽게 되고, 다음과 같은 결과를 얻게 된다.

Image 7.  Consistent answer with Multiversion Concurrency Control

읽기에 대한 락 없이 필요없이 일관성있는 응답을 얻게되었다.

MVCC의 쓰기 과정을 정리하면(붉은색 안의 읽기-쓰기 동기화를 획득하는 새로운 단계를 포함해서)

(0)  Row Lock 획득
(0a) 쓰기 번호 획득
(1) Write-Ahead-Log (WAL) 쓰기
(2) MemStore 업데이트 : memstore 에 각 셀의 데이터 저장
(2a) Write Number 저장
(3) Row Lock 반환

Conclusion

이 글에서는 처음에는 HBase의 row-level ACID의 보장성에 대해서 얘기했고, 동시 쓰기에 대해서 배우므로써 concurrency control의 필요성에 대해서 이야기한 다음, row-level  락 솔루션을 소개했다. 마지막으로 읽기-쓰기 concurrency control 에 대해서 살펴보고 MVCC라고 불리는 효과적인 방법을 알아보았다.

이 블로그는 HBase 0.92에 맞춰져 있고 HBase 0.94에서는 좀 더 다양한 최적화가 이루어졌다. HBASE-5541 는 차후에 다시 다루도록 하겠다.

Gregory Chanan은 클라우데라의 소프트웨어 엔지니어이고 HBase committer이다.

[발 번역] Apache HBase 는 어떻게 스케일링 하는가?

오래간만에 발 번역을 하게 됩니다. 이걸 번역할 시간이 없긴 한데, 공부 겸으로 제가 관심이 가는 분야로 이해도를 높이기 위해서 -_-, 발로 다시 번역을 시작했습니다.  원 글은 http://blog.cloudera.com/blog/2013/04/how-scaling-really-works-in-apache-hbase/ 에서 보실 수 있습니다.

How Scaling Really Works in Apache HBase

얼핏보면, Apache HBase 아키텍처는 master/slave 모델을 따르고, 마스터가 모든 요청을 받지만, 실제 작업은 slave 들에서 실제로 작업이 완료되는 걸로 보입니다. 그러나 이건 옳지 않고, 이 글에서, 실제로 master 와 slave에 의해서 어떻게 작업이 처리되는지 설명하려고 합니다.(역자 주: 여기서 master/slave 모델이라고 하는 것은 HBase Master 와 Region Server 들을 의미합니다. 일반적인 DB 의 Replication을 생각하지 마시고, Master 노드가 작업을 분배하고  실제 Worker들이 작업을 처리하는 모델을 생각하시면 됩니다.)

Regions and Region Servers

HBase 는 HDFS 기반으로 low-latency random reads 와 write 를 다루는 하둡 스토리지 매니저이고 페타데이터를 다룰 수 있습니다. HBase의 재미난 기능 중에 하나는 오토-샤딩입니다. 오토-샤딩은 시스템의 테이블이 너무 커졌을 때 동적으로 재분배 해주는 기능입니다.

Region 은 HBase 에서 수평 확장을 위한 기본 단위입니다. Region 은 함께 저장되는, 연결되어 있고, 정렬되어 있는 데이블 데이터의 부분집합입니다.

처음에는 테이블에는 오직 한개의 Region만 있고, 아래에서 볼 수 있듯이, Region이 row들이 추가되어 너무 커지면, Region 은 적당히 반으로 가르는 중간 키를 이용해서 나눠지게 됩니다.

HBase 에서 Slave는 Region Server  라 불리고, 각 Region Server 는 region들의 집합을 관리하는 역할을 하고 하나의 Region은( 데이터 row들의 범위를 가지는) 오직 하나의 Region Server 에 의해서만 처리됩니다.

HBase 아키텍처는 두 개의 메인 서비스로 구성되는데, 클러스터를 조직화하고, 관리 작업을 수행하는 HMaster와 각 테이블 데이터의 부분을 서비스하는 HRegionServer 로 되어 있습니다.

HMaster, Region Assignment, and Balancing

이전에 언급했듯이, HBase Master는 HBase 클러스터를 조직화하고, 관리하는 작업을 하게 됩니다.

Region Server는 하나 이상의 Region을 서비스하고, 각 Region은 하나의 Region Server에 시작시 할당됩니다. Master는 로드밸런싱의 결과로 Region을 원래의 Region Server 에서 다른쪽으로 이동하도록 결정할 수 있습니다.

Region과 Region Server의 매핑은 META라고 불리는 시스템 테이블에 의해서 유지되고, META 를 읽음으로서, 어떤 키가 어느 Region에 속하고 서비스 받는지 알 수 있습니다. 이것은 Read 와 Write 작업에 master가 전혀 참여하지 않고, 클라이언트가 직접 요청되는 데이터를 처리하는 Region Server와 통신할 수 있다는 의미입니다.

Locating a Row-Key: Which Region Server is Responsible?

row 를 쓰거나 읽기 위해서, 클라이언트는 master와 연결된 필요가 없고, 직접적으로 해당 row를 가진 Region Server에 연결될 수 있다. scan 의 경우에, 다뤄야 할 키들의 집합을 가진 Region Server들에 직접적으로 연결될 수 있다.

Region Server를 찾기 위해서, 클라이언트는 META 테이블로 쿼리한다.

META는 Region 들을 모니터링 하기 위해서 사용되는 시스템 테이블입니다. 서버의 이름과 테이블 이름을 비교하기 위한 Region 식별자, 그리고 시작 row-key 를 가지고 있습니다. start-key 와 다음 Region 의 start-key를 이용해서 클라이언트는 특정 Region에 속해있는 row의 범위를 알 수 있습니다.

클라이언트는 Region의 위치를 위한 캐시를 유지하고, 이것은 같은 Region을 확인해야 할 때, 매번 META table을 확인해야 하는 것을 피하게 해줍니다. Region이 나눠지거나, 다른 Region Server로 이동할 때(할당 정책이나, 밸런싱 때문에), 클라이언트는 예외를 받게 되고, 이로 인해 META 테이블에서 가져온 새로운 정보로 캐시가 갱신되게 됩니다.

 META 역시 다른 테이블과 유사하기 때문에, 클라이언트는 META 서버가 어디에 있는지 찾아야 할 필요가 있고, META 의 위치는 ZooKeeper 노드에 Master에 의해서 저장되게 됩니다.  그리고 클라이언트는 META가 저장된 Region Server의 주소를 바로 가져오게 됩니다.

HBase 는 BigTable 에 기반해서 설계가 되었고, -ROOT-라고 불리는 또 다른 테이블이, META 위치를 가지고 있고, Apache ZooKeeper 가 그 위치를 가리키고 있다. HBase 0.96 에서는 META가 나눠지지 않고, 하나의 Region으로만 구성되기 때문에, 이런 구조가 제거되고, ZooKeeper에만 의존하게 되었다.(역자 주: -ROOT- 테이블이 드랍되고 META 의 위치가 주키퍼에 바로 저장되게 되었다. https://issues.apache.org/jira/browse/HBASE-3171)

Client API: Master and Regions Responsibilities

HBase Java client API는 두 가지 주요 인터페이스가 있다.

  • HBaseAdmin 은 테이블의 생성/삭제/수정시에 “table schema”과 연결되도록 해준다. Region의 할당과 할당해제시, Region을 합치거나, 제거를 요청했을 때, 등등에 클러스터와 연결되게 한다. 이 인터페이스는 Master와 통신한다.
  • HTable 은 클라이언트가 get, put, delete, 그 외의 다른 데이터 명령어를 통해서 특정 테이블의 데이터를 다룰 수 있도록 해준다. 요청된 키를 처리하는 Region Server와 바로 통신하도록 해준다.

이 두 인터페이스는 서로 다른 역활을 한다. HBaseAdmin은 관리자 명령과 Master와 동작하고 HTable 은 Region과 통신하고 데이터를 처리한다.

Conclusion

여기서 살펴봤듯이, Master/Slave 아키텍처를 가지는 것이 모든 작업이 마스터를 통한다는 것은 아닙니다. HBase 클라이언트는, 데이터를 읽고 쓰기 위해서, 실제로,  모든 데이터 작업이 HTable 을 통해서 , 요청한 row key를 다루는 특정 Region Server  에 바로 요청하게 됩니다. Master는 테이블의 생성, 수정, 삭제 연산에만 사용됩니다.(HBaseAdmin)

Master라는 개념이 존재하지만, HBase 클라이언트는 데이터 연산시에는 master에 의존하지 않으므로, master가 다운되더라도 데이터 서비스를 할 수 있다.

Matteo Bertozzi 는 플랫폼팀의 소프트웨어 엔지니어이며 HBase 커미터입니다.

[입 개발] Redis Master/Slave 연결이 계속 끊어집니다.

크, 오래간만에 블로그 포스팅을 다시 하게 되었습니다. 이제 Redis 관련 이슈들은 대부분 한 번씩 다뤄서, 남은건 현재 열심히 개발중인 Cluster 에 대해서 이야기를 하던지, 아니면 좀 더 내부 구조에 대해서 깊게 들어가야 할지 고민중입니다.

오늘의 주제는 “Redis Master/Slave의 연결이 계속 끊어집니다.”, 물론 다양한 이유로 접속이 끊어질 수 있습니다. Master가 죽었을 수도 있고, Slave가 죽었을 수도 있고, 하지만, 다음과 같은 이유가 가장 많을 것이라고 생각합니다.

그리고 점점 메모리 사이즈가 커지는 추세로 봐서 일어날 가능성이 높습니다. 일단 가장 큰 이유는 Redis 에는 클라이언트 종류별로(일반, 슬레이브, pubsub) Soft limit 와 Hard limit라는 것을 설정합니다. 그런 설정을 본적이 없으시다구요? 그럼 소스를 봐서 redis.conf를 열어서 끝부분을 보시길 바랍니다.

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

요런게 있는데 각각 [type] [hard limit] [soft limit] [time] 입니다.

hard limit 은 이 값 이상 사용하게 되면 해당 클라이언트를 종료시킵니다.
soft limit 은 이 값 이상으로 time만큼 지속되면 종료시킵니다.

앞에 지시어가 client-output-buffer-limit 인걸로 봐서, 뭔가 client 에게 전달하려다 못하는 상황일 때에 대한 제한입니다. 그런데 이게, 네트웍 상황이 안좋거나, 또는 너무나 큰 데이터가 전달되어야 될 때, 넘칠 수가 있습니다. 즉 위에 “Slave”의 겨우 최대 256mb, 또는 64mb 로 60초가 지속되면 slave와의 접속을 끊어버립니다. 그런데 이 값에 대해서 변경하기 위해서는 고려해야할 것이 있습니다. 아, 그럼 무조건 증가시키면 좋게네라고 생각하고 hard limit 과 soft limit을 막 증가시키면, slave 가 많을 경우 어떤 일이 벌어질까요? 넵, 생각하신대로 서버의 메모리가 부족해서 종료하게 됩니다. 그래서, 실제로 모, 서버에서는 슬레이브가 32개 정도가 있는데, 8G 머신에 256MB 가까이 먹어서 4G를 넘어버린 어처구니 없는 일도 벌어진적이 있습니다. 그리고 한가지 더, 이 값이 또 너무 작아서 계속 접속이 끊어지면, 끊어진 슬레이브는 계속 마스터로 연결을 요청하고, 데이터 전달로 인해서, 마스터가 해야할 일을 못하고 느려지는 경우가 발생하게 됩니다. CPU 100%를 보실 수도 있습니다.

그럼 이 코드는 실제로 어디서 동작할까요?
networking.c 의 asyncCloseClientOnOutputBufferLimitReached() 함수에서 내부적으로 다시 checkClientOutputBufferLimits() 를 통해서 처리하게 됩니다. 코드는 간단합니다.

void asyncCloseClientOnOutputBufferLimitReached(redisClient *c) {
    redisAssert(c->reply_bytes < ULONG_MAX-(1024*64));
    if (c->reply_bytes == 0 || c->flags & REDIS_CLOSE_ASAP) return;
    if (checkClientOutputBufferLimits(c)) {
        sds client = getClientInfoString(c);

        freeClientAsync(c);
        redisLog(REDIS_WARNING,"Client %s scheduled to be closed ASAP for overcoming of output buffer limits.", client);
        sdsfree(client);
    }
}

int checkClientOutputBufferLimits(redisClient *c) {
    int soft = 0, hard = 0, class;
    unsigned long used_mem = getClientOutputBufferMemoryUsage(c);

    class = getClientLimitClass(c);
    if (server.client_obuf_limits[class].hard_limit_bytes &&
        used_mem >= server.client_obuf_limits[class].hard_limit_bytes)
        hard = 1;
    if (server.client_obuf_limits[class].soft_limit_bytes &&
        used_mem >= server.client_obuf_limits[class].soft_limit_bytes)
        soft = 1;

    /* We need to check if the soft limit is reached continuously for the
     * specified amount of seconds. */
    /* We need to check if the soft limit is reached continuously for the
     * specified amount of seconds. */
    if (soft) {
        if (c->obuf_soft_limit_reached_time == 0) {
            c->obuf_soft_limit_reached_time = server.unixtime;
            soft = 0; /* First time we see the soft limit reached */
        } else {
            time_t elapsed = server.unixtime - c->obuf_soft_limit_reached_time;

            if (elapsed <=
                server.client_obuf_limits[class].soft_limit_seconds) {
                soft = 0; /* The client still did not reached the max number of
                             seconds for the soft limit to be considered
                             reached. */
            }
        }
    } else {
        c->obuf_soft_limit_reached_time = 0;
    }
    return soft || hard;
}

마지막으로 다시 중요한 핵심만 다시 정리하자면, 다음과 같습니다.
1. client output buffer limit 이라는 게 존재해서 이 값에 따라서 컨넥션이 끊어질 수 있다.
2. 메모리가 큰 서버에서 가동 중이면 값을 증가시키는 것이 좋다.
3. 슬레이브가 너무 많은 경우는 반대로 값을 줄이는게 마스터 안전성에 도움이 된다.
4. 너무 접속이 빈번하게 끊어지면, 마스터에 부담이 가게 된다.(Sync 작업이 비쌉니다.)

[입 개발] Redis Tips

[입 개발] Redis Pub/Sub 시스템은 일반적인 Message Queue와 다르다.

Redis Issue 에 재미있는 질문이 올라왔습니다. https://github.com/antirez/redis/issues/998 Redis Pub/Sub 에 maximum 값을 설정하고 싶다는 것입니다. 아마도 이 분은 Redis Pub/Sub을 일종의 Message Queue로 착각하고 있는 것입니다.

뭐, 처음부터 집고 넘어가자면, 당연히 Pub/Sub System과 Message Queue는 다른 것입니다. 먼저, Pub/Sub System은 현재 채널에 가입한 Subscriber 들 모두에게 특정 이벤트를 전달하는 것입니다. 그리고 메시지 큐는 보통 일반적으로 작업을 큐잉하고 있다가 요청하는 곳에 데이터를 전달하기 위해서 보관하는 시스템입니다. 그런데 일반적으로 Pub/Sub을 제공하는 시스템들이, 일종의 메시지 큐처럼 데이터를 보관하는 역할을 한다는 것이, 단순히 Redis를 사용할 경우, 착각하게 됩니다.

redis에서 어떤 client 도 subscribe 하지 않은 경우 publish 하게 되면 다음과 같은 결과를 얻게 됩니다.

redis 127.0.0.1:6379> publish abc 123
(integer) 0

리턴값은 몇개의 subscriber 에게 메시지를 전달했다는 결과를 나타냅니다. 즉, redis를 가지고 단순히 미리 publish 해두고 나중에 다른 subscriber가 subscribe 할 경우에는 앞에 publish한 데이터는 전부 유실되게 됩니다.

해당 publish 코드를 살펴보더라도, 단순히 channel을 찾아서 연결된 클라이언트 정보만 체크하지 따로 저장하는 부분은 없습니다. 즉 딱 기본적인 pub/sub 에만 집중한 형태입니다.

/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    struct dictEntry *de;
    listNode *ln;
    listIter li;

    /* Send to clients listening for that channel */
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
            redisClient *c = ln->value;

            addReply(c,shared.mbulkhdr[3]);
            addReply(c,shared.messagebulk);
            addReplyBulk(c,channel);
            addReplyBulk(c,message);
            receivers++;
        }
    }
    /* Send to clients listening to matching channels */
    if (listLength(server.pubsub_patterns)) {
        listRewind(server.pubsub_patterns,&li);
        channel = getDecodedObject(channel);
        while ((ln = listNext(&li)) != NULL) {
            pubsubPattern *pat = ln->value;

            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {
                addReply(pat->client,shared.mbulkhdr[4]);
                addReply(pat->client,shared.pmessagebulk);
                addReplyBulk(pat->client,pat->pattern);
                addReplyBulk(pat->client,channel);
                addReplyBulk(pat->client,message);
                receivers++;
            }
        }
        decrRefCount(channel);
    }
    return receivers;
}
Follow

Get every new post delivered to your Inbox.

Join 29 other followers