Quantcast
Channel: Charsyam's Blog
Viewing all 122 articles
Browse latest View live

[입 개발] Scala의 거시기 : _(underscore) 의 용법 정리

$
0
0

Scala 를 사용하면 만나게 되는 여러 문법 중에, 처음 접하는 이들의 머리를 깨는 것이 있으니, 그게 바로 Scala에서 거시기로 통하는 _(underscore) 입니다. 이게 뭐지 하고 고민하는 중에 팀분이(멀린 사랑해요.) 아래 자료를 알려주셨습니다. 그리고 이 블로그는 아래의 문서를 풀이하는 것입니다. 사실 아래의 문서만 봐도 거의 모든 것이 이해되지만, 제 부족한 머리를 위해서 정리해둡니다. 참고로 dreaded 는 “무서운” 이런뜻입니다.

첫 슬라이드는 다음과 같습니다. 뭔가 어려워보이죠? 각종 _ 의 용법은 모두 들어가 있습니다.
아주 간단하게 설명하면 아래의 offset 에 대입되는 커링 함수 sum2(count) 에서 count는 매번 실행시의 count 값이 바인딩됩니다.(악 벌써 여기부터 어려워!!!)

class Underscores {
	import collection.{ Map => _ , _ }

	var count : Int = _

	def sum = (_:Int) + (_:Int)
	def sum2(a:Int)(b:Int) = a+b
	def offset = sum2(count) _

	def sizeOf(l:Traversable[_]) : Unit = l match {
		case it:Iterable[Int] => count = (0/:it)(_ + _)
		case s:Seq[_] => s.foreach( _ => count = count + 1)
		case _ => println(offset(l.size))
	}
}

두번째 슬라이드는 각각 어떤 용법으로 이루어졌나를 보여줍니다. 총 6가지 용법을 색상까지 이쁘게 보여주네요.

세번째 슬라이드 부터 각각의 용법에 대해서 알려줍니다.

1번은 “모두”를 의미합니다. 아래의 예에서 첫번째 Map => _ 는 일단 무시하시고(5번이니깐요.)
그 뒤의 _ 가 자바에서의 import * 와 같은 용법입니다.(왜 *가 아닌지는…)

	import collection.{ Map => _ , _ }

실제 예는 다음과 같습니다.

import java.util._
val date = new Date()

import scala.util.control.Breaks._
breakable {
	for (i <- 0 to 10) { if (i == 5) break }
}

웬지 breakable은 Exception을 잡아서 나가는 것 같은 느낌이 진하게 납니다.

2번은 디폴트 값 지정입니다. 숫자면 0 문자면 null로 지정됩니다.

	var count : Int = _

다만 이렇게 지정하는 건 생성자에서만 되고 함수안에서는 되지 않습니다.

class Foo {
	var i:Int = _ // i = 0
	var s:String = _ // s = null

	def f {
	// var i:String = _//error: local variables must be initialized
	}
}

3번째는 unused variables 입니다. 아래와 같이 _로 받은걸 쓰지 않게 되는 겁니다.

		case _ => println(offset(l.size))

예를 보면 다음과 같습니다. 아래의 두 예는 같은 예입니다. x를 파라매터로 받지만… 쓰지 않는 겁니다.

(1 to 5) foreach { (x:Int) => println("one more")}
(1 to 5) foreach { _ => println("one more")}

다음 예제들도 동일합니다.

def inPatternMatching1(s:String) {
	s match {
		case "foo" => println("foo !")
		case x => println("not foo")
	}
}

def inPatternMatching2(s:String) {
	s match {
		case "foo" => println("foo !")
		case _ => println("not foo")
	}
}

4번째는 이름 없는 파라매터입니다. 아주 명확하게 들어갈 변수 대신에 지정되게 됩니다.(3번하고는 반대입죠.)
아래의 예는 1…10 까지가 x로 바인딩되는데, 이걸 _로 대체하는 경우입니다. 명시적으로 사용하기 위해서이죠.

(1 to 10) map { x => x + 1}
(1 to 10) map { _ + 1}

(1 to 10).foldLeft(0) { (x,y) => x+y }
(1 to 10).foldLeft(0) { _+_ }

이제 partial function 에서 보면 더더욱 눈이 휘둥굴해 집니다.

def f(i:Int) : String = i.toString

def g = (x:Int) => f(x)
def g2 = f _
def f2 = (_:String).toString

def u(i:Int)(d:Double) : String = i.toString + d.toString

def v = u _

def w1 = u(4) _

def w2 = u(_:Int)2.0)

5번은 아까 Map 관련 헤더들을 import 하지 말라는 뜻입니다.
6번은 c++의 Template 처럼 특정 타입을 지칭하는 것입니다. 위에서 넘어온 타입을 그대로 사용하겠다라고 하면 이해가 쉬울까요.

다시 한번 말씀드리지만, 슬라이드가 잘 되어있으니, 실제로 보시면 꽤 도움이 되실껍니다. 뭐, 저도 공부하는 중이라…



[입 개발] Redis internal : Redis Dictionary Type 에 관해서

$
0
0

Redis 는 기본적으로 Hash 형태로 데이터를 관리하지만, 내부적으로 관리가 필요한 정보들 역시, 내부적으로는 Dictionary 라고 부르는 Hash 형태로 관리하고 있습니다. 그런데 이 Dictionary 에 대한 핸들링이, 관리되는 데이터의 종류에 따라서 다르게 처리되어야 할 때가 있습니다. 아마 오늘 글을 그냥 Redis를 쓰시는 분 입장에서는 별 내용이 없고, Redis 소스를 건드리시는 분들에게는 아주 살짝 도움이 될듯합니다.

보통 대부분의 언어에서는 함수 오버라이딩등을 이용한 폴리모피즘을 이용해서 이런 방식의 요구사항을 좀 수월하게 처리하게 되어 있습니다. 그러나 C 에서는… 기본적으로 이런 방식이 제공되지 않지만, 함수 포인터를 이용해서 이런 방식을 구현할 수 있습니다. 가장 유명한 예가, 리눅스 커널의 VFS 같은 부분을 보면 됩니다.

cluster.c 의 clusterInit 코드를 보면 다음과 같이 dictCreate 함수를 이용해서 dictionary를 생성하는 것을 볼 수 있습니다.

    server.cluster->nodes = dictCreate(&clusterNodesDictType,NULL);
    server.cluster->nodes_black_list =
        dictCreate(&clusterNodesBlackListDictType,NULL);

dictCreate 함수를 살펴보면 다음과 같이 dictType, privDataPtr 두개의 파라매터를 가지고, 결과로 dict 를 넘겨줍니다.

dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);
    return d;
}

먼저 dict 구조체부터 살펴보도록 하겠습니다.

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

dict 구조체는 dictType 과 dictht 두개를 가집니다. 여기서 dictht는 dict의 테이블 확장시에 사용하기 위한 것입니다. 그럼 이 dict 안에 있는 dictType은 뭘까요? dictType은 다음과 같이 정의되어 있습니다.

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

dictType 을 보시면 hashFuction, keyDup, valDup, keyCompare, keyDestructor, valDestructor 같은 값들이 정의되어 있습니다. 즉, 함수포인터를 가지고 있기 때문에, 여기서 가리키는 함수의 동작이 다르면, 같은 형태로 보이더라도 서로 다르게 동작하게 할 수 있습니다.

그리고 현재 Redis 에는 다음과 같이 8개의 dictType이 정의되어 있습니다.

  • dictType setDictType;
  • dictType clusterNodesDictType;
  • dictType clusterNodesBlackListDictType;
  • dictType dbDictType;
  • dictType shaScriptObjectDictType;
  • dictType hashDictType;
  • dictType replScriptCacheDictType;

dictType 구조체 안에서 특히 keyCompare 를 통해서 해당 키를 찾아내게 됩니다.
예를 들어서, 내부적으로 실제 키들을 저장하는 곳에서는 dbDictType 을 사용합니다. dbDictType을 보면 다음과 같습니다.

/* Db->dict, keys are sds strings, vals are Redis objects. */
dictType dbDictType = {
    dictSdsHash,                /* hash function */
    NULL,                       /* key dup */
    NULL,                       /* val dup */
    dictSdsKeyCompare,          /* key compare */
    dictSdsDestructor,          /* key destructor */
    dictRedisObjectDestructor   /* val destructor */
};

dbDictType에서는 Key의 비교를 위해서는 dictSdsKeyCompare 를 사용하고, dictSdsKeyCompare는 다음과 같이 구현되어 있습니다.

int dictSdsKeyCompare(void *privdata, const void *key1,
        const void *key2)
{
    int l1,l2;
    DICT_NOTUSED(privdata);

    l1 = sdslen((sds)key1);
    l2 = sdslen((sds)key2);
    if (l1 != l2) return 0;
    return memcmp(key1, key2, l1) == 0;
}

그리고 Redis Command 를 저장하는 곳에서 사용하는 commandTableDictType 에서는 대소문자 구분이 필요없을 때는 dictSdsKeyCaseCompare 를 사용합니다.

int dictSdsKeyCaseCompare(void *privdata, const void *key1,
        const void *key2)
{
    DICT_NOTUSED(privdata);

    return strcasecmp(key1, key2) == 0;
}

위와 같은 형태에서 dictFind를 살펴보면 내부적으로 dictType 함수포인터를 이용하게 됩니다.

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    unsigned int h, idx, table;

    if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

#define dictCompareKeys(d, key1, key2) \
    (((d)->type->keyCompare) ? \
        (d)->type->keyCompare((d)->privdata, key1, key2) : \
        (key1) == (key2))

위의 dictCompareKeys가 내부적으로 dictType의 keyCompare를 이용하는 것을 볼 수 있습니다. 그런데 여기서 조심해야 하는 것들이 있습니다. 실제 dictFind 에서 key를 넘겨줄 때, dictSdsKeyCompare 는 key가 무조건 sds type이어야 하지만, dictSdsKeyCaseCompare 에서는 실제 값만 비교하므로, 단순 string 이어도 가능합니다. 말 그대로 가능만 합니다. 그런데, 뭔가 잘못 쓰기 시작하면… Redis가 뻥뻥 죽어나가는 걸 보실 수 있을껍니다.

그래서 cluster.c 소스를 보면, 무조건 key가 sds 형태여야 하기 때문에 발생하는 사소한 오버헤드도 존재합니다. 뭐, 그러나 자주 발생하지는 않으니… 그냥 패스를 ㅎㅎㅎ


[입개발] Redis Command 구조체에 대해서…

$
0
0

오늘 올라온 Redis 버그 중에 클러스터에서 geo command가 redirect를 먹지 않는다 라는 버그가 있었습니다. 그런데 antirez가 아주 쉽게 버그를 수정하면서 패치가 되었다고 답변을 답니다.(이 버그는 cluster 모드일때만 발생합니다.) 그리고 해당 글은 꼭 끝까지 보셔야 합니다!!!

https://github.com/antirez/redis/issues/2671

그리고 그 패치라는 것도 다음과 같습니다.
https://github.com/antirez/redis/commit/5c4fcaf3fe448c5575a9911edbcd421c6dbebb54

     {"geoadd",geoaddCommand,-5,"wm",0,NULL,1,1,1,0,0},
     {"georadius",georadiusCommand,-6,"r",0,NULL,1,1,1,0,0},
     {"georadiusbymember",georadiusByMemberCommand,-5,"r",0,NULL,1,1,1,0,0},
-    {"geohash",geohashCommand,-2,"r",0,NULL,0,0,0,0,0},
-    {"geopos",geoposCommand,-2,"r",0,NULL,0,0,0,0,0},
-    {"geodist",geodistCommand,-4,"r",0,NULL,0,0,0,0,0},
+    {"geohash",geohashCommand,-2,"r",0,NULL,1,1,1,0,0},
+    {"geopos",geoposCommand,-2,"r",0,NULL,1,1,1,0,0},
+    {"geodist",geodistCommand,-4,"r",0,NULL,1,1,1,0,0},
     {"pfselftest",pfselftestCommand,1,"r",0,NULL,0,0,0,0,0},
     {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0},
     {"pfcount",pfcountCommand,-2,"r",0,NULL,1,1,1,0,0},

0, 0, 0 이 1, 1, 1 로만 바뀐거죠. 그럼 도대체 저 값들은 뭘 의미하는 걸까요? 그걸 위해서 이제 Redis Command 구조체를 알아보도록 하겠습니다.

먼저 Redis Command 구조체는 다음과 같습니다.

struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    long long microseconds, calls;
};

먼저 위의 샘플 커맨드와 하나를 비교해보면

     {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0},

name 과 proc는 각각 명확합니다. name은 사용자로 부터 입력받는 명령어이고, proc는 해당 명령어에 연결된 함수포인터입니다. 즉 name으로 사용자가 입력하면 proc가 실행되는 것이죠.

이제 arity 부터가 영어를 알면 쉽게 알 수 있습니다. 그렇습니다. 영어를 모르면 어렵지만…(전 방금 사전을…)
다음과 같은 뜻이 있습니다.

(logic, mathematics, computer science) The number of arguments or operands a function or operation takes. For a relation, the number of domains in the corresponding Cartesian product.

즉 파라메터의 개수입니다. 다음 코드를 보시죠. arity가 양수일 때는 파라매터 개수랑 동일해야 하고(고정적), 음수이면, 절대값으로 이값보다는 많거나 같아야 하는거죠(동적). 즉 -2 면 파라매터가 2개 이상이어야 하는거고, 2면 딱 2개여야 하는겁니다.

    } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
               (c->argc < -c->cmd->arity)) {
        flagTransaction(c);
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
            c->cmd->name);
        return REDIS_OK;
    }

sflags는 명령어의 속성을 문자열로 나타냅니다. 각 속성은 populateCommandTable 함수에 잘 나타나 있습니다.

            case 'w': c->flags |= REDIS_CMD_WRITE; break;
            case 'r': c->flags |= REDIS_CMD_READONLY; break;
            case 'm': c->flags |= REDIS_CMD_DENYOOM; break;
            case 'a': c->flags |= REDIS_CMD_ADMIN; break;
            case 'p': c->flags |= REDIS_CMD_PUBSUB; break;
            case 's': c->flags |= REDIS_CMD_NOSCRIPT; break;
            case 'R': c->flags |= REDIS_CMD_RANDOM; break;
            case 'S': c->flags |= REDIS_CMD_SORT_FOR_SCRIPT; break;
            case 'l': c->flags |= REDIS_CMD_LOADING; break;
            case 't': c->flags |= REDIS_CMD_STALE; break;
            case 'M': c->flags |= REDIS_CMD_SKIP_MONITOR; break;
            case 'k': c->flags |= REDIS_CMD_ASKING; break;
            case 'F': c->flags |= REDIS_CMD_FAST; break;

flags는 값이 모두 0입니다. 이 값은 로딩시에 sflags 로 부터 계산해서 만들어집니다. 위의 populateCommandTable 함수를 보시면 sflags 속성에 따라서 c->flags를 만드는 코드가 바로 보입니다. 왜 이렇게 구현했을까요? 아마 숫자값으로 설정되어 있는것 보다, 문자열이 보기 쉬워서가 아닐까 합니다.

getkeys_proc 는 파라매터만 가지고는 key를 찾기가 힘들때, 보조적으로 사용하는 함수의 함수포인터입니다. 위의 예에서는 전부 NULL 이지만, 다음과 같은 커맨들들이 있습니다.

    {"zunionstore",zunionstoreCommand,-4,"wm",0,zunionInterGetKeys,0,0,0,0,0},
    {"zinterstore",zinterstoreCommand,-4,"wm",0,zunionInterGetKeys,0,0,0,0,0},

zunionInterGetKeys 의 형태를 살펴보면 다음과 같습니다.

/* Helper function to extract keys from following commands:
 * ZUNIONSTORE <destkey> <num-keys> <key> <key> ... <key> <options>
 * ZINTERSTORE <destkey> <num-keys> <key> <key> ... <key> <options> */
int *zunionInterGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) {
    int i, num, *keys;
    REDIS_NOTUSED(cmd);

    num = atoi(argv[2]->ptr);
    /* Sanity check. Don't return any key if the command is going to
     * reply with syntax error. */
    if (num > (argc-3)) {
        *numkeys = 0;
        return NULL;
    }

    /* Keys in z{union,inter}store come from two places:
     * argv[1] = storage key,
     * argv[3...n] = keys to intersect */
    keys = zmalloc(sizeof(int)*(num+1));

    /* Add all key positions for argv[3...n] to keys[] */
    for (i = 0; i < num; i++) keys[i] = 3+i;

    /* Finally add the argv[1] key position (the storage key target). */
    keys[num] = 1;
    *numkeys = num+1;  /* Total keys = {union,inter} keys + storage key */
    return keys;
}

그리고 getkeys_proc 를 쓰는 곳은 db.c 에서 getKeysFromCommand 에서 다음과 같이 사용하고 있습니다.

int *getKeysFromCommand(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) {
    if (cmd->getkeys_proc) {
        return cmd->getkeys_proc(cmd,argv,argc,numkeys);
    } else {
        return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
    }
}

그런데 재미있는 것은 getkeys_proc 는 실제로 코드는 예전부터 존재했으나, 거의 쓰이지 않던 파라매터라는 것입니다. 실제로 현재 버전에서도 commandCommand 와 cluster 정보를 얻기위해서만 쓰고 있습니다.

firstkey, lastkey, keystep 은 각각, 첫번째 파라매터가 key 된다. 마지막 파라매터가 key 가 된다. keystep은 1이면 [key, key, key] 형태고, 2이면 [key, val, key, val, key, val] 형태가 되는 것을 의미합니다. 그래서 이 값이, 0, 0, 0 이면 geohash가 다음과 같은 결과를 냅니다.

127.0.0.1:6379> GEOHASH Sicily Palermo Catania
(empty list or set)

위의 값이 1로 셋팅이 되면… 다음과 같이 redirect 결과가 나옵니다.

127.0.0.1:6381> GEOHASH Sicily Palermo Catania
(error) MOVED 10713 127.0.0.1:6380

물론 둘 다 해당 서버인 6380에서 실행하면 정상적인 결과가 나옵니다.

127.0.0.1:6380> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"

그런데 왜 이게 이런 차이가 날까요? 아까 살짝 언급한게 “getkeys_proc 는 commandCommand 와 cluster 정보를 얻기위해서만 쓰고 있습니다.” 라고 말했습니다. 저 firstkey, lastkey, keystep 도 상단의 getKeysUsingCommandTable 즉 getKeysFromCommand 에서만 사용되고 있습니다.

다시말하면, 결국 cluster 에서 뭔가 정보를 얻을때, firstkey, lastkey, keystep을 쓴다는 것입니다. 그냥 커맨드가 독립적으로 실행될 때는 안쓴다는 T.T

코드를 찾아보면 cluster.c 의 getNodeByQuery 에서 위의 getKeysFromCommand 를 쓰고 있습니다. 어떤용도로 쓰고 있을까요?

geohash 명령을 cluster 모드에서 받았다고 할때… 실제로 getKeysFromCommand 는 getKeysUsingCommandTable를 호출하게 되는데 cmd->firstkey 는 0이므로 NULL이 리턴됩니다. numkey도 0이 셋팅됩니다.

int *getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, int *numkey>
    int j, i = 0, last, *keys;
    REDIS_NOTUSED(argv);

    if (cmd->firstkey == 0) {
        *numkeys = 0;
        return NULL;
    }
    last = cmd->lastkey;
    if (last < 0) last = argc+last;
    keys = zmalloc(sizeof(int)*((last - cmd->firstkey)+1));
    for (j = cmd->firstkey; j <= last; j += cmd->keystep) {
        redisAssert(j < argc);
        keys[i++] = j;
    }
    *numkeys = i;
    return keys;
}

그래서 getNodeByQuery에서는 다음과 같은 코드가 실행됩니다.

clusterNode *n = NULL
keyindex = getKeysFromCommand(mcmd,margv,margc,&numkeys);

//......

if (n == NULL) return myself;

즉 numkey가 0이라 노드를 찾는 작업을 하지 않고 n == NULL 이라서 자기자신에 속한다고 리턴해버립니다. 그래서 자기가 붙은 노드에서 검색하고 결과가 없어서 빈 값이 넘어가게 되는겁니다.

실제로 processCommand 소스를 보시면… 아래에 getNodeByQuery 를 호출하고, myself 가 아닐때에는 hashslot을 가진 서버로 clusterRedirectClient를 호출해서 보내게 되는데… 아까 위의 결과에서 myself가 리턴되기 때문에… 해당 서버에서 실행되게 되어서 결과가 없는 것입니다.

    if (server.cluster_enabled &&
        !(c->flags & REDIS_MASTER) &&
        !(c->flags & REDIS_LUA_CLIENT &&
          server.lua_caller->flags & REDIS_MASTER) &&
        !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0))
    {
        int hashslot;

        if (server.cluster->state != REDIS_CLUSTER_OK) {
            flagTransaction(c);
            clusterRedirectClient(c,NULL,0,REDIS_CLUSTER_REDIR_DOWN_STATE);
            return REDIS_OK;
        } else {
            int error_code;
            clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,&hashslot,&error_cod>
            if (n == NULL || n != server.cluster->myself) {
                flagTransaction(c);
                clusterRedirectClient(c,n,hashslot,error_code);
                return REDIS_OK;
            }
        }
    }

딱, 이렇습니다라고 말해야 하지만… 사실 여기는 훼이크가 있습니다. 위의 processCommand의 코드를 자세히 살펴보면… 아래와 같은 코드가 있습니다.

    !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0)

네 그렇습니다. getkeys_proc 가 없고, firstkey가 0이 아니어야 아래의 코드를 타는 것입니다. 미리 조건을 걸러낸것입니다. 그래서 실제로… 위의 코드를 타지 않고… 아예 바로 해당 서버에서 실행됩니다. 즉 myself를 리턴한것과 동일한 효과가 나는 것이지요. firstkey가 0이면 파라매터가 없는 명령으로 인식되어서 그냥 해당 서버에서 실행이 되는 것입니다.

살짝 속으셨다고 분노하시겠지만, 결국은 해당 코드도 그렇게 동작하게 됩니다. :) 호호호호… 그러면 즐거운 하루되시길…


[입 개발] Redis AOF Rewrite 설정에 관하여…

$
0
0

오래간만에 Redis 관련 블로깅을 하네요.(요새는 실력 부족이라… 적을 내용이…) 먼저 실제 내용 자체는 지인의 질문에 대한 답변을 해드리다가, 정리할 필요가 있어서 이렇게 적습니다.(그렇습니다. 나중에 제가 까먹을까봐지요. 그 때 소스 다시 보기는 그러니…)

Redis에서 Persist 를 제공하는 방식은 RDB/AOF 두 가지가 있습니다. 뭐, 당연히 이쯤 되면 다들 기본적으로 이게 뭔지는 아실테니… 자세한 내용은 넘어가고요. 원래 AOF는 Redis 의 Comment Handler가 처리하는 명령들 중에 write관련 커맨드들만, 디스크에 Redis Protocol 그대로 저장하는 방식입니다.(현재 이에 대해 압축을 해볼까 하는 얘기들도 오고가고 있습니다만… 일단 이건 논외로…)

그런데 로그중에 다음과 같은 에러를 보셨다고 합니다.

"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis."

사실 Redis는 스레드를 몇 개 사용하긴 합니다. 그 중에 하나가 fsync를 스레드에서 실행하는 거죠. appendfsync 가 everysec 일 때만, 백그라운드로 돕니다. fsync 자체에서 대기하게 되면, redis가 아무작업도 못하게 되니…

appendfsync everysec

하여튼 fsync가 큐에 들어갔는데, 아직 실행이 안끝났다. 디스크가 바쁘거나, 뭔가 이슈가 있을꺼라라는 뜻이죠. 실제로 디스크가 좀 이상한 느낌이라고…

삼천포로 빠졌는데, 다시 AOF rewrite 얘기로 넘어가면, 실제로 모든 write 성 작업이 저장되기 때문에 AOF 파일이 한없이 커질 수가 있습니다. 그래서 특정시점에, 해당 파일 사이즈가 얼마 이상이면, rdb 생성 처럼 fork 후에 현재 메모리 정보를 전부 AOF 형식으로 다시 쓰게 됩니다.(그럼 사이즈가 줄어들 가능성이 있겠죠?, 변경이나 삭제가 한번도 없었다면, 줄어들지 않을수도…)

그럼 당연한 의문이 생깁니다. 어느 정도 되어야 rewrite가 일어날까? 관련 옵션이 바로 아래 두가지 입니다.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

일단 rewrite가 rdb 처럼 fork를 이용하므로 사용하지 않고 싶다면 위의 auto-aof-rewrite-percentage 값을 0으로 설정하면 됩니다. auto-aof-rewrite-min-size 는 일단 최소 이것보다는 사이즈가 커야 rewrite 하라는 뜻입니다.

내부적으로 코드를 살펴보면 다음과 같습니다.

         /* Trigger an AOF rewrite if needed */
         if (server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             server.aof_rewrite_perc &&
             server.aof_current_size > server.aof_rewrite_min_size)
         {
            long long base = server.aof_rewrite_base_size ?
                            server.aof_rewrite_base_size : 1;
            long long growth = (server.aof_current_size*100/base) - 100;
            if (growth >= server.aof_rewrite_perc) {
                serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                rewriteAppendOnlyFileBackground();
            }
         }

내부적으로 aof_current_size 와 aof_rewrite_base_size 를 사용하는데… AOF를 처음 만들면… 마지막으로 만들어졌던 AOF 파일의 크기가 aof_rewrite_base_size 가 되고 현재 AOF 파일의 크기가 aof_current_size 가 됩니다. 즉 auto-aof-rewrite-percentage 이 100이면, 이전에 만들어진 AOF가 1G 였다면, 2G가 되면(즉 100% 커지면) 하라는 뜻이 됩니다. 그리고 위의 조건 문에 의해서 0이면 실제로 AOF rewrite는 동작하지 않습니다.


[입 개발] RabbitMQ를 제대로 설치하는 방법

$
0
0

개발을 하다보면, Queue를 써야 하는 경우가 종종 있습니다. 뭔가를 비동기 적으로 처리하기 위해서, 또는 다이렉트로 들어가는 부담을 줄이기 위해서…

그런데 그러다보면 항상 고민되는 것이 바로 HA 입니다. 뭐, Queue의 모든 데이터가 날아가도 된다. 이러면 사실 큰 문제가 아니겠지만… 일반적으로 Queue의 데이터를 그렇게 날려도 되는 경우는 많지 않습니다.

일반적으로 많이 사용하는 Queue 는 여기서 소개할 RabbitMQ, ActiveMQ, Redis 기반의 Resque나 SideKiq(Resque 보다는 Sidekiq이 ㅎㅎㅎ) 아니면, 로그나 이런 대규모 부하에서는 거의 디팩토인 kafka 가 있습니다.

일단 각각 장단점이 있기 때문에, 여기서는 꽤 안정성을 보장해 주는 RabbitMQ의 HA를 구성하는 설치에 대해서만 설명하도록 하겠습니다.(다만 RabbitMQ의 경우는 운영하다 Consumer 가 느리면… 뻗어버리는 단점이…)

사실 설치 자체는 RabbitMQ 사용법을 그대로 따르면 됩니다. 별로 어려운 것도 없습니다.

CentOS면 yum으로, Ubuntu 면 apt로 쉽게 설치가 가능합니다. Rabbitmq 공식 페이지를 참조하면 됩니다.

그리고 보통은 클러스터를 설정합니다. 이 설정 방법도 쉽습니다. erlang 쿠키를 맞춰주고, join_cluster 만 해주면 됩니다. RabbitMQ HA Guide를 참고하시면 쉽습니다.

그러면 무엇을 해야하는가? 바로 이 Cluster가 그냥 쓰면 장애의 원인이 되기 때문입니다. 기본적으로, RabbitMQ에서 Cluster 모드가 되면, 채널 정보가 보통 Disk Node 에 저장되게 되는데, 이 서버가 내려가면… 해당 채널들은 모두 사용이 불가능해집니다.(장애시, HA를 하려고 설정한건데… 도리어, 장애가 나는거죠. 물론 그냥 죽어도 장애긴 합니다.)

그럼 제대로 RabbitMQ에서 HA를 구성하는 방법은 무엇일까요?

바로 mirrored queue를 운영하는 것입니다. RabbitMQ는 mirroed queue 라고 해서, 해당 Queue의 내용이 클러스터된 서버내에 중복되어 저장되는 기능을 의미합니다. 다만, 디폴트 설정으로는 설정이 된 시점의 데이터 부터, 복사가 되고 그 이전의 데이터는 복사되지 않습니다. (물론 이것도 옵션에 의해서 모두 가져가도록 할 수 있습니다.)

Mirrored Queue가 구성하는 방법 역시 RabbitMQ HA Guide에 잘 나와있습니다.

rabbitmqctl set_policy ha-all "^ha\." '{"ha-mode":"all"}

이러면… ha. 으로 시작하는 queue 들은 mirrored 형태로 구성하겠다는 것입니다. 실제 web ui에서 보면 Queue 이름에 두대로 클러스터가 되었다면 ha.test +1, 세 대라면 ha.test +2 식으로 구성되어야만 제대로 설정이 된 것입니다.

이제 어떤 서버로 연결할 것인가의 이슈가 생깁니다. 앞에 Load Balancer 를 붙여서(haproxy든 L4든) 장애난 서버를 제외하고 연결하게 하면… 제대로 된 서비스가 가능해집니다.


[입 개발] memcached 의 binary protocol 과 text protocol의 동작은 조금씩 다르다.

$
0
0

보통 가장 유명한 캐시 솔루션을 뽑으라면… Memcached 나 Redis 가 될 가능성이 높습니다.(당연히 다른 좋은 솔루션들도 많지만…) 가장 많이 쓰이는 건 이 두가지가 아닐듯 합니다.

Redis의 경우는 text protocol 만 지원하지만, memcached의 경우는 text와 binary protocol 두 가지를 모두 지원합니다. 실제로 binary protocol을 사용하면 당연히 속도도 더 빠릅니다.

그런데… 당연히 text/binary protocol 두 가지를 지원하다고 하면 두 동작이 모두 동일할꺼라고 생각하기 쉽습니다. 그러나… memcached 에서 text와 binary protocol은 미묘하게 동작이 다른 경우가 있습니다. 그리고 그 대표적인 예가 incr 입니다.

먼저 text protocol에서 “incr” 커맨드의 동작을 살펴보면 다음과 같습니다. 실제 text protocol은 process_command 라는 함수에서 동작하게 됩니다. 소스가 엄청 길어서 중간에 “incr”를 찾아보면 process_arithmetic_command 함수를 호출한다는 것만 알면됩니다.

static void process_command(conn *c, char *command) {

    token_t tokens[MAX_TOKENS];
    size_t ntokens;
    int comm;

    assert(c != NULL);

    MEMCACHED_PROCESS_COMMAND_START(c->sfd, c->rcurr, c->rbytes);

    if (settings.verbose > 1)
        fprintf(stderr, "<%d %s\n", c->sfd, command);

    /*
     * for commands set/add/replace, we build an item and read the data
     * directly into it, then continue in nread_complete().
     */

    c->msgcurr = 0;
    c->msgused = 0;
    c->iovused = 0;
    if (add_msghdr(c) != 0) {
        out_of_memory(c, "SERVER_ERROR out of memory preparing response");
        return;
    }

    ntokens = tokenize_command(command, tokens, MAX_TOKENS);
    if (ntokens >= 3 &&
        ((strcmp(tokens[COMMAND_TOKEN].value, "get") == 0) ||
         (strcmp(tokens[COMMAND_TOKEN].value, "bget") == 0))) {

        process_get_command(c, tokens, ntokens, false);
    } else if ((ntokens == 6 || ntokens == 7) &&
               ((strcmp(tokens[COMMAND_TOKEN].value, "add") == 0 && (comm = NREAD_ADD)) ||
                (strcmp(tokens[COMMAND_TOKEN].value, "set") == 0 && (comm = NREAD_SET)) ||
                (strcmp(tokens[COMMAND_TOKEN].value, "replace") == 0 && (comm = NREAD_REPLACE)) ||
                (strcmp(tokens[COMMAND_TOKEN].value, "prepend") == 0 && (comm = NREAD_PREPEND)) ||
                (strcmp(tokens[COMMAND_TOKEN].value, "append") == 0 && (comm = NREAD_APPEND)) )) {

        process_update_command(c, tokens, ntokens, comm, false);

    } else if ((ntokens == 7 || ntokens == 8) && (strcmp(tokens[COMMAND_TOKEN].value, "cas") == 0 && (comm = NREAD_CAS))) {

        process_update_command(c, tokens, ntokens, comm, true);

    } else if ((ntokens == 4 || ntokens == 5) && (strcmp(tokens[COMMAND_TOKEN].value, "incr") == 0)) {

        process_arithmetic_command(c, tokens, ntokens, 1);

    } else if (ntokens >= 3 && (strcmp(tokens[COMMAND_TOKEN].value, "gets") == 0)) {

        process_get_command(c, tokens, ntokens, true);

    } else if ((ntokens == 4 || ntokens == 5) && (strcmp(tokens[COMMAND_TOKEN].value, "decr") == 0)) {

        process_arithmetic_command(c, tokens, ntokens, 0);

    } else if (ntokens >= 3 && ntokens <= 5 && (strcmp(tokens[COMMAND_TOKEN].value, "delete") == 0)) {

        process_delete_command(c, tokens, ntokens);

    } else if ((ntokens == 4 || ntokens == 5) && (strcmp(tokens[COMMAND_TOKEN].value, "touch") == 0)) {

        process_touch_command(c, tokens, ntokens);

    } else if (ntokens >= 2 && (strcmp(tokens[COMMAND_TOKEN].value, "stats") == 0)) {

        process_stat(c, tokens, ntokens);
    } else if (ntokens >= 2 && ntokens <= 4 && (strcmp(tokens[COMMAND_TOKEN].value, "flush_all") == 0)) {
        time_t exptime = 0;
        rel_time_t new_oldest = 0;

        set_noreply_maybe(c, tokens, ntokens);

        pthread_mutex_lock(&c->thread->stats.mutex);
        c->thread->stats.flush_cmds++;
        pthread_mutex_unlock(&c->thread->stats.mutex);

        if (!settings.flush_enabled) {
            // flush_all is not allowed but we log it on stats
            out_string(c, "CLIENT_ERROR flush_all not allowed");
            return;
        }

        if (ntokens != (c->noreply ? 3 : 2)) {
            exptime = strtol(tokens[1].value, NULL, 10);
            if(errno == ERANGE) {
                out_string(c, "CLIENT_ERROR bad command line format");
                return;
            }
        }

        /*
          If exptime is zero realtime() would return zero too, and
          realtime(exptime) - 1 would overflow to the max unsigned
          value.  So we process exptime == 0 the same way we do when
          no delay is given at all.
        */
        if (exptime > 0) {
            new_oldest = realtime(exptime);
        } else { /* exptime == 0 */
            new_oldest = current_time;
        }
        if (settings.use_cas) {
            settings.oldest_live = new_oldest - 1;
            if (settings.oldest_live <= current_time)
                settings.oldest_cas = get_cas_id();
        } else {
            settings.oldest_live = new_oldest;
        }
        out_string(c, "OK");
        return;

    } else if (ntokens == 2 && (strcmp(tokens[COMMAND_TOKEN].value, "version") == 0)) {

        out_string(c, "VERSION " VERSION);

    } else if (ntokens == 2 && (strcmp(tokens[COMMAND_TOKEN].value, "quit") == 0)) {

        conn_set_state(c, conn_closing);

    } else if (ntokens == 2 && (strcmp(tokens[COMMAND_TOKEN].value, "shutdown") == 0)) {

        if (settings.shutdown_command) {
            conn_set_state(c, conn_closing);
            raise(SIGINT);
        } else {
            out_string(c, "ERROR: shutdown not enabled");
        }
    } else if (ntokens > 1 && strcmp(tokens[COMMAND_TOKEN].value, "slabs") == 0) {
        if (ntokens == 5 && strcmp(tokens[COMMAND_TOKEN + 1].value, "reassign") == 0) {
            int src, dst, rv;

            if (settings.slab_reassign == false) {
                out_string(c, "CLIENT_ERROR slab reassignment disabled");
                return;
            }

            src = strtol(tokens[2].value, NULL, 10);
            dst = strtol(tokens[3].value, NULL, 10);

            if (errno == ERANGE) {
                out_string(c, "CLIENT_ERROR bad command line format");
                return;
            }

            rv = slabs_reassign(src, dst);
            switch (rv) {
            case REASSIGN_OK:
                out_string(c, "OK");
                break;
            case REASSIGN_RUNNING:
                out_string(c, "BUSY currently processing reassign request");
                break;
            case REASSIGN_BADCLASS:
                out_string(c, "BADCLASS invalid src or dst class id");
                break;
            case REASSIGN_NOSPARE:
                out_string(c, "NOSPARE source class has no spare pages");
                break;
            case REASSIGN_SRC_DST_SAME:
                out_string(c, "SAME src and dst class are identical");
                break;
            }
            return;
        } else if (ntokens == 4 &&
            (strcmp(tokens[COMMAND_TOKEN + 1].value, "automove") == 0)) {
            process_slabs_automove_command(c, tokens, ntokens);
        } else {
            out_string(c, "ERROR");
        }
    } else if (ntokens > 1 && strcmp(tokens[COMMAND_TOKEN].value, "lru_crawler") == 0) {
        if (ntokens == 4 && strcmp(tokens[COMMAND_TOKEN + 1].value, "crawl") == 0) {
            int rv;
            if (settings.lru_crawler == false) {
                out_string(c, "CLIENT_ERROR lru crawler disabled");
                return;
            }

            rv = lru_crawler_crawl(tokens[2].value);
            switch(rv) {
            case CRAWLER_OK:
                out_string(c, "OK");
                break;
            case CRAWLER_RUNNING:
                out_string(c, "BUSY currently processing crawler request");
                break;
            case CRAWLER_BADCLASS:
                out_string(c, "BADCLASS invalid class id");
                break;
            case CRAWLER_NOTSTARTED:
                out_string(c, "NOTSTARTED no items to crawl");
                break;
            }
            return;
        } else if (ntokens == 4 && strcmp(tokens[COMMAND_TOKEN + 1].value, "tocrawl") == 0) {
            uint32_t tocrawl;
             if (!safe_strtoul(tokens[2].value, &tocrawl)) {
                out_string(c, "CLIENT_ERROR bad command line format");
                return;
            }
            settings.lru_crawler_tocrawl = tocrawl;
            out_string(c, "OK");
            return;
        } else if (ntokens == 4 && strcmp(tokens[COMMAND_TOKEN + 1].value, "sleep") == 0) {
            uint32_t tosleep;
            if (!safe_strtoul(tokens[2].value, &tosleep)) {
                out_string(c, "CLIENT_ERROR bad command line format");
                return;
            }
            if (tosleep > 1000000) {
                out_string(c, "CLIENT_ERROR sleep must be one second or less");
                return;
            }
            settings.lru_crawler_sleep = tosleep;
            out_string(c, "OK");
            return;
        } else if (ntokens == 3) {
            if ((strcmp(tokens[COMMAND_TOKEN + 1].value, "enable") == 0)) {
                if (start_item_crawler_thread() == 0) {
                    out_string(c, "OK");
                } else {
                    out_string(c, "ERROR failed to start lru crawler thread");
                }
            } else if ((strcmp(tokens[COMMAND_TOKEN + 1].value, "disable") == 0)) {
                if (stop_item_crawler_thread() == 0) {
                    out_string(c, "OK");
                } else {
                    out_string(c, "ERROR failed to stop lru crawler thread");
                }
            } else {
                out_string(c, "ERROR");
            }
            return;
        } else {
            out_string(c, "ERROR");
        }
    } else if ((ntokens == 3 || ntokens == 4) && (strcmp(tokens[COMMAND_TOKEN].value, "verbosity") == 0)) {
        process_verbosity_command(c, tokens, ntokens);
    } else {
        out_string(c, "ERROR");
    }
    return;
}

다시 process_arithmetic_command를 살펴보면 다음과 같습니다.

static void process_arithmetic_command(conn *c, token_t *tokens, const size_t ntokens, const bool incr) {
    char temp[INCR_MAX_STORAGE_LEN];
    uint64_t delta;
    char *key;
    size_t nkey;

    assert(c != NULL);

    set_noreply_maybe(c, tokens, ntokens);

    if (tokens[KEY_TOKEN].length > KEY_MAX_LENGTH) {
        out_string(c, "CLIENT_ERROR bad command line format");
        return;
    }

    key = tokens[KEY_TOKEN].value;
    nkey = tokens[KEY_TOKEN].length;

    if (!safe_strtoull(tokens[2].value, &delta)) {
        out_string(c, "CLIENT_ERROR invalid numeric delta argument");
        return;
    }

    switch(add_delta(c, key, nkey, incr, delta, temp, NULL)) {
    case OK:
        out_string(c, temp);
        break;
    case NON_NUMERIC:
        out_string(c, "CLIENT_ERROR cannot increment or decrement non-numeric value");
        break;
    case EOM:
        out_of_memory(c, "SERVER_ERROR out of memory");
        break;
    case DELTA_ITEM_NOT_FOUND:
        pthread_mutex_lock(&c->thread->stats.mutex);
        if (incr) {
            c->thread->stats.incr_misses++;
        } else {
            c->thread->stats.decr_misses++;
        }
        pthread_mutex_unlock(&c->thread->stats.mutex);

        out_string(c, "NOT_FOUND");
        break;
    case DELTA_ITEM_CAS_MISMATCH:
        break; /* Should never get here */
    }
}

case DELTA_ITEM_NOT_FOUND 를 보면 아이템이 없으면 incr_misses를 증가시키고, NOT_FOUND를 리턴합니다. 즉 아이템이 없으면 단순히 없다고 리턴하는 것입니다.

그런데 incr의 binary protocol에서의 동작을 살펴보면 조금 미묘하게 다릅니다.
일단 binary protocol 에서 incr이 수행될때 최종 함수는 complete_incr_bin 이라는 함수입니다.

static void complete_incr_bin(conn *c) {
    item *it;
    char *key;
    size_t nkey;
    /* Weird magic in add_delta forces me to pad here */
    char tmpbuf[INCR_MAX_STORAGE_LEN];
    uint64_t cas = 0;

    protocol_binary_response_incr* rsp = (protocol_binary_response_incr*)c->wbuf;
    protocol_binary_request_incr* req = binary_get_request(c);

    assert(c != NULL);
    assert(c->wsize >= sizeof(*rsp));

    /* fix byteorder in the request */
    req->message.body.delta = ntohll(req->message.body.delta);
    req->message.body.initial = ntohll(req->message.body.initial);
    req->message.body.expiration = ntohl(req->message.body.expiration);
    key = binary_get_key(c);
    nkey = c->binary_header.request.keylen;

    if (settings.verbose > 1) {
        int i;
        fprintf(stderr, "incr ");

        for (i = 0; i < nkey; i++) {
            fprintf(stderr, "%c", key[i]);
        }
        fprintf(stderr, " %lld, %llu, %d\n",
                (long long)req->message.body.delta,
                (long long)req->message.body.initial,
                req->message.body.expiration);
    }

    if (c->binary_header.request.cas != 0) {
        cas = c->binary_header.request.cas;
    }
    switch(add_delta(c, key, nkey, c->cmd == PROTOCOL_BINARY_CMD_INCREMENT,
                     req->message.body.delta, tmpbuf,
                     &cas)) {
    case OK:
        rsp->message.body.value = htonll(strtoull(tmpbuf, NULL, 10));
        if (cas) {
            c->cas = cas;
        }
        write_bin_response(c, &rsp->message.body, 0, 0,
                           sizeof(rsp->message.body.value));
        break;
    case NON_NUMERIC:
        write_bin_error(c, PROTOCOL_BINARY_RESPONSE_DELTA_BADVAL, NULL, 0);
        break;
    case EOM:
        out_of_memory(c, "SERVER_ERROR Out of memory incrementing value");
        break;
    case DELTA_ITEM_NOT_FOUND:
        if (req->message.body.expiration != 0xffffffff) {
            /* Save some room for the response */
            rsp->message.body.value = htonll(req->message.body.initial);

            snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu",
                (unsigned long long)req->message.body.initial);
            int res = strlen(tmpbuf);
            it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),
                            res + 2);

            if (it != NULL) {
                memcpy(ITEM_data(it), tmpbuf, res);
                memcpy(ITEM_data(it) + res, "\r\n", 2);

                if (store_item(it, NREAD_ADD, c)) {
                    c->cas = ITEM_get_cas(it);
                    write_bin_response(c, &rsp->message.body, 0, 0, sizeof(rsp->message.body.value));
                } else {
                    write_bin_error(c, PROTOCOL_BINARY_RESPONSE_NOT_STORED,
                                    NULL, 0);
                }
                item_remove(it);         /* release our reference */
            } else {
                out_of_memory(c,
                        "SERVER_ERROR Out of memory allocating new item");
            }
        } else {
            pthread_mutex_lock(&c->thread->stats.mutex);
            if (c->cmd == PROTOCOL_BINARY_CMD_INCREMENT) {
                c->thread->stats.incr_misses++;
            } else {
                c->thread->stats.decr_misses++;
            }
            pthread_mutex_unlock(&c->thread->stats.mutex);

            write_bin_error(c, PROTOCOL_BINARY_RESPONSE_KEY_ENOENT, NULL, 0);
        }
        break;
    case DELTA_ITEM_CAS_MISMATCH:
        write_bin_error(c, PROTOCOL_BINARY_RESPONSE_KEY_EEXISTS, NULL, 0);
        break;
    }
}

아까 전과 비슷한 코드들이 있습니다. 다시 case DELTA_ITEM_NOT_FOUND 를 찾아보면 뭔가 다른 부분을 볼 수 있을 것입니다. else 문 부분은 text_protocol과 동일한데, if 문이 뭔가 추가되었네요.

        if (req->message.body.expiration != 0xffffffff) {
            /* Save some room for the response */
            rsp->message.body.value = htonll(req->message.body.initial);

            snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu",
                (unsigned long long)req->message.body.initial);
            int res = strlen(tmpbuf);
            it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),
                            res + 2);

            if (it != NULL) {
                memcpy(ITEM_data(it), tmpbuf, res);
                memcpy(ITEM_data(it) + res, "\r\n", 2);

                if (store_item(it, NREAD_ADD, c)) {
                    c->cas = ITEM_get_cas(it);
                    write_bin_response(c, &rsp->message.body, 0, 0, sizeof(rsp->message.body.value));
                } else {
                    write_bin_error(c, PROTOCOL_BINARY_RESPONSE_NOT_STORED,
                                    NULL, 0);
                }
                item_remove(it);         /* release our reference */
            } else {
                out_of_memory(c,
                        "SERVER_ERROR Out of memory allocating new item");
            }
        }

코드를 보면 expiration 이 -1이 아니면 값을 셋팅하는 부분이 있습니다. 즉 binary protocol에서는 해당 키가 없더라도… 해당 시점에 생성이 가능합니다. text protocol 에서는 불가능 한데 말이죠. 또한, 최초에 아이템이 없을때만 생성이되고, 있을 경우에는 expiration을 변경하지도 않습니다. 코드를 보니… 궁금증들이 해결이 되는 것 같습니다.


[입 개발] Kafka 에서 auto.offset.reset 의 사용법

$
0
0

최근에 서버로그를 컨슈밍해서 뭔가 작업할 일이 생겼습니다. 데이터를 매일 보면서 실제 얼마나 이벤트가 있었는지 볼려고 하니, 실제 로그의 수와 저장된 이벤트의 수가 엄청 다른 것입니다.

확인을 해보니, 로그가… 몇일 전꺼부터 계속 돌고 있었습니다. -_-;;; 즉, 몇일 치가 하루데이터라고 생각하고 계속 저장되고 있었으니, 생각하는 값보다 훨씬 많이 오버된 T.T

저만 그런 문제가 있는가 해서 뒤져보니, auto.offset.reset 이라는 환경설정 값을 찾을 수 있었습니다. auto.offset.reset 은 다음과 같이 설명이 되어 있습니다.

What to do when there is no initial offset in Zookeeper or if an offset is out of range:
* smallest : automatically reset the offset to the smallest offset
* largest : automatically reset the offset to the largest offset
* anything else: throw exception to the consumer. If this is set to largest, the consumer may lose some messages when the number of partitions, for the topics it subscribes to, changes on the broker. To prevent data loss during partition addition, set auto.offset.reset to smallest

일단 설정이 안되었을 때의 기본 값은 largest 입니다.(아주 옛날 소스는 autooffset.reset 이라는 설정에, latest 라는 설정으로 되어있었지만… 현재는 바뀌어 있습니다.)

smallest 는 가지고 있는 오프셋 값 중에 가장 작은 값을 사용합니다. largest는 반대로 가장 최신 offset 을 사용하게 됩니다.

handleOffsetOutOfRange 에서는 autoOffsetReset 값을 통해서 startTimestamp를 설정하고 실제 newOffset 을 earliestOrLatestOffset 을 통해서 가져오게 됩니다.

<pre>def handleOffsetOutOfRange(topicAndPartition: TopicAndPartition): Long = {
  val startTimestamp = config.autoOffsetReset match {
    case OffsetRequest.SmallestTimeString =&gt; OffsetRequest.EarliestTime
    case OffsetRequest.LargestTimeString =&gt; OffsetRequest.LatestTime
    case _ =&gt; OffsetRequest.LatestTime
  }
  val newOffset = simpleConsumer.earliestOrLatestOffset(topicAndPartition, startTimestamp, Request.OrdinaryConsumerId)
  val pti = partitionMap(topicAndPartition)
  pti.resetFetchOffset(newOffset)
  pti.resetConsumeOffset(newOffset)
  newOffset
}</pre>

그럼 이 옵션을 언제 적용하게 되느냐? 즉, 바꿔말하면 handleOffsetOutOfRange이 언제 호출되는가를 알아야 합니다. AbstractFetcherThread.scala를 보게 되면 Errors.NONE 일 때는 해당 결과 offset 으로 현재 offset을 업데이트 합니다. partitionMap에 PartitionFetchState 로 그 offset을 저장합니다.

<pre>private def processFetchRequest(fetchRequest: REQ) {
  val partitionsWithError = new mutable.HashSet[TopicAndPartition]
  var responseData: Map[TopicAndPartition, PD] = Map.empty

  try {
    trace("Issuing to broker %d of fetch request %s".format(sourceBroker.id, fetchRequest))
    responseData = fetch(fetchRequest)
  } catch {
    case t: Throwable =&gt;
      if (isRunning.get) {
        warn(s"Error in fetch $fetchRequest", t)
        inLock(partitionMapLock) {
          partitionsWithError ++= partitionMap.keys
          // there is an error occurred while fetching partitions, sleep a while
          partitionMapCond.await(fetchBackOffMs, TimeUnit.MILLISECONDS)
        }
      }
  }
  fetcherStats.requestRate.mark()

  if (responseData.nonEmpty) {
    // process fetched data
    inLock(partitionMapLock) {

      responseData.foreach { case (topicAndPartition, partitionData) =&gt;
        val TopicAndPartition(topic, partitionId) = topicAndPartition
        partitionMap.get(topicAndPartition).foreach(currentPartitionFetchState =&gt;
          // we append to the log if the current offset is defined and it is the same as the offset requested during fetch
          if (fetchRequest.offset(topicAndPartition) == currentPartitionFetchState.offset) {
            Errors.forCode(partitionData.errorCode) match {
              case Errors.NONE =&gt;
                try {
                  val messages = partitionData.toByteBufferMessageSet
                  val validBytes = messages.validBytes
                  val newOffset = messages.shallowIterator.toSeq.lastOption match {
                    case Some(m: MessageAndOffset) =&gt; m.nextOffset
                    case None =&gt; currentPartitionFetchState.offset
                  }
                  partitionMap.put(topicAndPartition, new PartitionFetchState(newOffset))
                  fetcherLagStats.getFetcherLagStats(topic, partitionId).lag = Math.max(0L, partitionData.highWatermark - newOffset)
                  fetcherStats.byteRate.mark(validBytes)
                  // Once we hand off the partition data to the subclass, we can't mess with it any more in this thread
                  processPartitionData(topicAndPartition, currentPartitionFetchState.offset, partitionData)
                } catch {
                  case ime: CorruptRecordException =&gt;
                    // we log the error and continue. This ensures two things
                    // 1. If there is a corrupt message in a topic partition, it does not bring the fetcher thread down and cause other topic partition to also lag
                    // 2. If the message is corrupt due to a transient state in the log (truncation, partial writes can cause this), we simply continue and
                    // should get fixed in the subsequent fetches
                    logger.error("Found invalid messages during fetch for partition [" + topic + "," + partitionId + "] offset " + currentPartitionFetchState.offset  + " error " + ime.getMessage)
                  case e: Throwable =&gt;
                    throw new KafkaException("error processing data for partition [%s,%d] offset %d"
                      .format(topic, partitionId, currentPartitionFetchState.offset), e)
                }
              case Errors.OFFSET_OUT_OF_RANGE =&gt;
                try {
                  val newOffset = handleOffsetOutOfRange(topicAndPartition)
                  partitionMap.put(topicAndPartition, new PartitionFetchState(newOffset))
                  error("Current offset %d for partition [%s,%d] out of range; reset offset to %d"
                    .format(currentPartitionFetchState.offset, topic, partitionId, newOffset))
                } catch {
                  case e: Throwable =&gt;
                    error("Error getting offset for partition [%s,%d] to broker %d".format(topic, partitionId, sourceBroker.id), e)
                    partitionsWithError += topicAndPartition
                }
              case _ =&gt;
                if (isRunning.get) {
                  error("Error for partition [%s,%d] to broker %d:%s".format(topic, partitionId, sourceBroker.id,
                    partitionData.exception.get))
                  partitionsWithError += topicAndPartition
                }
            }
          })
      }
    }
  }

  if (partitionsWithError.nonEmpty) {
    debug("handling partitions with error for %s".format(partitionsWithError))
    handlePartitionsWithErrors(partitionsWithError)
  }
}</pre>

두 번째로는 Errors.OFFSET_OUT_OF_RANGE 에러가 발생했을 때입니다. 이 에러가 발생하면, 아까 본 handleOffsetOutOfRange 함수를 통해서 다시 시작 newOffset을 설정하게 됩니다. 그럼 이 에러는 언제 발생하게 될까요?

Kafka는 consumer 가 자신이 처리한 위치 까지의 offset을 가지고 있는 구조입니다.(보통 주키퍼에 /consumers/{groupid} 형태로 기록이 되지요.) 그러니, 주키퍼의 해당 consumers 목록에 없다면 해당 설정대로 값을 읽게 됩니다. 두 번째로 이미 데이터가 접근할 수 없는 offset 즉, 이미 지워진 상태면… 해당 값을 읽어올 수 없다면, auto.commit.reset의 설정이 동작하게 됩니다.

저는 처음에 이것이 kafka 내에 설정값이 있고 해당 값보다 크면, 항상 새로 읽을 줄 알았습니다. T.T

그럼, 어떻게 항상 최신 데이터부터만 읽을 것인가? 가장 쉬운 방법은 해당 groupid에 속하는 오프셋을 필요할 때마다 지워주는 것입니다. 이러면 항상 최신의 데이터 부터 읽어오는 것을 알 수 있습니다. 두 번째는 굉장히 빨리 데이터를 지우도록 설정하는 것인데, 이러면 의도하지 않은 동작(그 주기 이전부터 읽어오게 되므로) 원하는 형태는 아닙니다.


[책 리뷰] 초보자를 위한 안드로이드 스튜디오

$
0
0

[해당 리뷰는 한빛미디어에서 진행하는 한빛리더스모임에 의해서 제공받은 책으로 진행했습니다.]

 

먼저 이 책은, “따라하기” 식으로 구성되어 있습니다. 처음부터, 끝까지 따라가면 안드로이드 스튜디오에 대한 기본적인 지식을 익히면서, 간단한(정말 간단하지는 T.T) 앱들을 구현하고, 실제 구글스토어에 올려볼 수 있습니다.

따라하기 식이다 보니, 안드로이드 프로그래밍 자체에 대해서 깊은 지식을 알려주지는 않지만 반대로,  쉽게 간단한 프로그램을 만들 수 있습니다.(좀 더 깊은 지식이 필요하다면 안드로이드 프로그래밍 전문서를 읽는 것을 추천합니다.)

그런데 뭔가 이상한걸 느끼지 않으셨나요? 이 책의 제목은 “초보자를 위한 안드로이드 스튜디오” 입니다. 제목만 보면 안드로이드 스튜디오만 설명할 것 같지만… 실제로 책의 앞부분에는 안드로이드 스튜디오를 이용한 방법이 많이 설명되고 있고, 책 전체적으로 따라하기가 안드로이드 스튜디오를 통해서 이루어지고 있습니다. 그렇지만, 안드로이드 개발에 대해서도 다루고 있는 책입니다.

책의 원서를 찾아보면, 실제로 제목이 안드로이드 스튜디오를 이용한 안드로이드 개발의 교과서 라는 이름입니다.(읽다보니, 웬지 개발서적인데… 이름이 이상해서 결국 인터넷 검색을 통해서 원서를 찾아봤습니다. 역시… 원서이름은… 둘다 포함하는)

원서는 여기서(http://www.amazon.co.jp/Android-Studio%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8BAndroid%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-%EF%BD%9EAndroid-Studio-%E6%95%99%E7%A7%91%E6%9B%B8%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA/dp/483995643X/ref=sr_1_7?ie=UTF8&qid=1456153918&sr=8-7&keywords=Android+Studio)

그렇다고, 완전 초보자용 도서도 아닙니다. 내용을 제대로 이해하기 위해서는
안드로이드에 대한 기본적인 지식이 필요합니다. 실제 예제도 채팅 클라이언트와 간단한 벽돌깨기 같은 게임을 만드는 예제가 있습니다.

실제로 예제를 설명하면서 필요한 지식도 어느정도 잘 설명하고 있습니다.(일본 서적들이 대부분 이런 부분에서 강점을 보여주더군요.) 채팅앱의 경우에, 사실 네트웍 동작이나, 실제 내부 저장같은 부분은 다 빠져있고, 실제 화면에 보이는 부분들만 있는게 좀 아쉽긴 합니다. 반대로 음성인식 API 사용이 있으니 이건 좋네요.

마지막으로 해당 앱을 구글스토어에 어떻게 올리는지에 대해서도 설명하고 있습니다. 코드 사이닝을 하고, 각 해상도에 맞는 아이콘을 등록하고, 이미지를 올리는… 이런게 귀찮지만, 꼭 알아둬야만 하는 내용들인데… 그런 것도 꼼꼼히 알려주고 있습니다.

 



[입 개발] Redis Cluster 에서의 Pub/Sub은 어떻게 동작할까?

$
0
0

저도 잘 모르고 있다가, 오늘 질문을 받고 급하게 찾아보고 아는 척을 한게 있습니다. 저야 뭐, 레디스 클러스터를 잘 안쓰고 있어서… 예전에 살짝 살펴보고 관심이 많이 떨어져 있기도 한…(비겁한 변명입니다.)

오늘의 질문은 “Redis Cluster 에서 Pub/Sub이 동작하는가?” 였습니다. 아마 다들 아시고 있으실 것 같지만… 일단 답 부터 하면 “동작합니다. 아무 노드에서나 subscribe 하고 아무 노드에서나 publish 하면 다 받을 수 있습니다.” 가 답입니다.

그럼 어떻게 구현이 되어 있을가요?

먼저 subscribe 동작 부터 살펴보면 subscribe 동작은 기존의 redis 와 동일합니다.

void subscribeCommand(client *c) {
   int j;

   for (j = 1; j &lt; c-&gt;argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
        c->flags |= CLIENT_PUBSUB;
   }
}

int pubsubSubscribeChannel(client *c, robj *channel) {
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    /* Add the channel to the client->channels hash table */
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        retval = 1;
        incrRefCount(channel);
        /* Add the client to the channel->list of clients hash table */
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }
        listAddNodeTail(clients,c);
    }
    /* Notify the client */
    addReply(c,shared.mbulkhdr[3]);
    addReply(c,shared.subscribebulk);
    addReplyBulk(c,channel);
    addReplyLongLong(c,clientSubscriptionsCount(c));
    return retval;
}

즉 클라이언트는 클러스터내의 아무(마스터중에서) Redis 서버에 접속해서 subscribe 명령을 날립니다.
그러면 해당 Redis 서버에는 server.pubsub_channels 라는 dict 안에 해당 channel 이 생성되게 됩니다.

이제 중요한 부분은 Publish 부분입니다. 그러나 간단합니다. 사실 Redis Cluster Spec 이나 github issue #1927 이런 걸 보면 Cluster Bus니 하면서 굉장히 복잡하게 보이지만… 그냥 publish는 모든 마스터에 이 채널에 이런 메시지가 왔다라고 전달하게 됩니다. 그러면 해당 서버는 다시 해당 채널에 등록된 클라이언트들에게 메시지를 보내는 아주 간단한 구조입니다.

void publishCommand(client *c) {
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        forceCommandPropagation(c,PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}

void clusterPropagatePublish(robj *channel, robj *message) {
    clusterSendPublish(NULL, channel, message);
}

void clusterSendPublish(clusterLink *link, robj *channel, robj *message) {
    unsigned char buf[sizeof(clusterMsg)], *payload;
    clusterMsg *hdr = (clusterMsg*) buf;
    uint32_t totlen;
    uint32_t channel_len, message_len;

    channel = getDecodedObject(channel);
    message = getDecodedObject(message);
    channel_len = sdslen(channel->ptr);
    message_len = sdslen(message->ptr);

    clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_PUBLISH);
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    totlen += sizeof(clusterMsgDataPublish) - 8 + channel_len + message_len;

    hdr->data.publish.msg.channel_len = htonl(channel_len);
    hdr->data.publish.msg.message_len = htonl(message_len);
    hdr->totlen = htonl(totlen);

    /* Try to use the local buffer if possible */
    if (totlen < sizeof(buf)) {
        payload = buf;
    } else {
        payload = zmalloc(totlen);
        memcpy(payload,hdr,sizeof(*hdr));
        hdr = (clusterMsg*) payload;
    }
    memcpy(hdr->data.publish.msg.bulk_data,channel->ptr,sdslen(channel->ptr));
    memcpy(hdr->data.publish.msg.bulk_data+sdslen(channel->ptr),
        message->ptr,sdslen(message->ptr));

    if (link)
        clusterSendMessage(link,payload,totlen);
    else
        clusterBroadcastMessage(payload,totlen);

    decrRefCount(channel);
    decrRefCount(message);
    if (payload != buf) zfree(payload);
}

void clusterBroadcastMessage(void *buf, size_t len) {
    dictIterator *di;
    dictEntry *de;

    di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL) {
        clusterNode *node = dictGetVal(de);

        if (!node->link) continue;
        if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_HANDSHAKE))
            continue;
        clusterSendMessage(node->link,buf,len);
    }
    dictReleaseIterator(di);
}

아래로 내려가면 최종적으로 clusterSendMessage를 모든 노드에 보내는 것을 알 수 있습니다. 맨 마지막 코드에서 보면 자기자신은 제외하는데, 이미 젤 위의 publishCommand 에서 자기 자신에 존재하는 채널을 찾아서 이미 보내고 있습니다.

그래서 결국 아무 Redis 서버에나 subscribe를 하고 아무 Redis 서버에서 publish를 하면 pub/sub 이 동작합니다. 그런데!!! 그런데!!! 그런데!!! 과연 이런 구조가 아무런 문제가 없을까요? 하나 더 집어드리자면… 간단하게 생각해서 A노드에만 subscribe 된 B라는 채널이 있고 마스터가 10대라면… 의미없는 9대의 서버에도 현재는 모두 메시지를 전달해야 합니다. 밴드위스 낭비와, 노드가 많아질때마다 성능상 문제가 있을 수도 있습니다. 여기에 대해서 걱정하는 글들도 있긴합니다만, 현재까지는… 좋은 방법이 구현되어 있지는 않습니다. 일단 해당 관련 내용은 다음 글들을 참고해보시기 바랍니다.

issue 2672
issue 122


[책 리뷰] 클라우드 시스템을 관리하는 기술

$
0
0

해당 리뷰는 “한빛미디어”에서 제공하는 도서를 이용하여 리뷰를 진행하였습니다.

딱 책을 받았을 때의 첫 느낌은… “어렵겠다” 였습니다. 일단 목차만 봐도 실제로 굉장히 많은 내용을 담고 있는 것으로 보이고, 저자가 구글에서 “Site resilience Engineer” 로 일했기 때문에, 구글 내부의 특별한 기술들을 가지고 있지 않을까 생각했습니다.(외국은 업무에 따라서 부르는 명칭이 많이 다른데, 위의 직종은 시스템이 장애가 나더라도 잘 운영될 수 있도록 하거나, 서비스의 확장에 무리없도록 해주는 서버 엔지니어를 말합니다. – DevOps 쪽 역할이라고 할수 있습니다.)

일단 내용 자체는 첫 느낌 그대로 방대하고 어렵습니다입니다. 저 역시 서버 엔지니어로 경력이 조금 있긴 하지만, 한번에 제대로 이해하기는 어려운 내용들이었습니다.(실제 내용이 Software Engineer 보다는 System Engineer 쪽에 좀 더 취중해 있는 내용이라, 어떻게 보면 모르는게 당연합니다.)

Global DNS 시스템을 구성하는 방법이라든지…(Kakao의 Anycast 와 유사합니다. http://tech.kakao.com/2014/05/29/anycast/) 또는 소프트웨어 로드 밸런서가 좋을지 하드웨어 로드 밸런서가 좋을지 등등 굉장히 여러부분을 다루고 있습니다.

이쪽 계통에서 일을 하고 있는 사람이나, 관심이 있는 사람이나 충분히 읽어보면 꽤 도움이 될만한 서적입니다.(초급자보다는 어느정도 지식이 있는 사람들에게 좀 더 좋을듯 합니다.) 다만, 워낙 많은 내용을 다룰려고 하다보니, 아주 상세한 내용보다는, 이런것들을 이런데 쓴다 정도의 개념만 설명하고 넘어가는 경우가 많습니다.

읽다보면 “사례 연구” 라고 해서 회색으로 표시된 부분들이 있는데, 자세히 읽어보고 넘어가시는게 좋습니다. 실제로 대규모 서비스를 운영하는 곳에서, 어떤 문제들이 있는지에 대해서 아주 가볍게 말하고 넘어가는데… 꼭 알아두어야 하는 부분들이지만, 반대로… 왜 그런가에 대한… 이유가 충분히 나오지는 않는… 경우도, 또한 구글의 엔지니어였기 때문에, 구글 수준에서는 쉽게 해결할 수 있겠지만, 일반적인 경우에는 해결하기 어려운 문제들도 꽤 있습니다.

결론적으로 내용이 방대하고 어려운부분(기반지식이 없다면…) 이 이 책의 장점이자, 단점이지 않을까 싶습니다. 그러나, 실제로 이정도 규모의 서비스를 경험해보기가 쉽지 않기 때문에, 그리고 이정도로 방대하게 풀어낸 책은 아직 없는듯 합니다. 꼭 이해하지 못하더라도 한번 읽어보고, 이해할려고 노력해보는 것이 필요한 책입니다.


[책 리뷰] 비주얼 컴플렉시티

$
0
0

해당 리뷰는 “한빛미디어”에서 제공하는 도서를 이용하여 리뷰를 진행하였습니다.

 

사실 책을 받기 전에 제목과 샘플만 보고 느낀점은 “꼭 읽어봐야 할 것 같다” 뭔가 도움이 되겠구나 라는 느낌이었습니다.

그러나 책을 받고 실제로 읽어보면서.. 제 머리속에서 뭔가 큰 소리가 들렸습니다.
“큰일났다… 엄청 어렵구나…”

이 책의 제목처럼 비주얼 컴플렉시티는 “복잡한 정보를 효과적으로 표현하는 놀라운 시각화 기법” 이라는 부제를 가지고 있는데요. 그러나… 실제로 관련 지식이 없는 사람에게는 일종의 시각화에 대한 철학책 처럼 보일 수 있습니다.(네, 저한테 이렇게 보인다는 거죠.)

여러가지 주제와 이 내용을 어떻게 표현할지에 대한 내용들도 있고, 실제로 이런 정보를 이렇게 표현했다라는 부분도 있습니다. 예를 들어… 중간에 테러리스트를 표시하는 부분은 여러가지 이미지로(네트웍을 표현하는 이미지들) 해당 내용을 표시합니다. 물론 실제 나타내는 의미나 주제도 다 다르죠.

visual_1

네 이런 정보가 가득합니다. 솔직하게 제가 이쪽 분야에 대해서 전혀 알지를 못하기 때문에… 책을 그림 책 보듯이 읽을 수 밖에 없었습니다. 그냥 이렇게도 표현할 수 있다라는 느낌으로… 다만… 그림책으로 보더라도 상당히 코퀄리티의 느낌을 받을 수 있습니다. 이렇게 그래픽으로 대부분 표현해야 하는 내용들이다 보니… 전부 컬러라는…

다만 순수 기술쪽만 보다가 이런 종류의 책을 보니… 이해하지는 못해도… 뭔가 멋져 보인다는 것만… (이번 리뷰는 망했어요. T.T)

저에게 좌절만 안겨준 책이지만… 반대로 이런 식으로 데이터를 표현한다는 것은… 좀 색다른 경험이긴 합니다.


[책 리뷰] 누구나 쉽게 배우는 스칼라

$
0
0

해당 리뷰는 “한빛미디어”에서 제공하는 도서를 이용하여 리뷰를 진행하였습니다.
스칼라라는 언어는 저에게는 약간 항상 배우고는 싶지만… 뭔가 어려운 언어입니다. 실제로
몇번의 스칼라 스터디를 했었고…

Programming in Scala 한국어판 이라든지…

브루스 테이트의 세븐 랭귀지 라든지…(원서였지만…)

이렇게 몇년에 걸쳐서 스칼라 스터디를 하지만… 심지어, 아주 조금씩 쓰고는 있지만… 남의 코드를 copy & paste 할 정도였지… 제대로 공부했다라고 하기는 어려운 상황입니다.

Implicit 라든지… 여러 가지 개념들부터… 뭔가 저에게는 상당히 어려운… 심지어… 지금도 마틴 오덕스키 아저씨의 scala 강의를 coursera 에서 틀어놓고 있는 중입니다.

일단 이 책을 완독한 느낌을 짧게 정리하면 다음과 같습니다.

1. 정말 쉽다…
2. 중요한 개념 일부는 잘 설명되어 있다.(케이스 클래스, 패턴 매칭, trait 이라든지…)
3. 초급자용 책이라… 잘 쓰길 워한면 이걸로는 부족해보인다가 첫 느낌이네요.

일단 필요한 개념들은 잘 정리되어 있습니다. 중요한 개념들에 대한 설명도… 다만… 실제적으로… 스칼라로 뭔가 복잡한걸 해보고 싶다고 한다면, 내용이 조금 부족합니다.

그러나 기초적인 개념을 잡고, Programming in Scala 가 어렵다면…(대부분의 사람한테는 어려울듯한…) 이 책을 먼저 읽고, 기초를 잡고 Programming in Scala를 보는 것이 좋을 듯 합니다.

이 책과 함께 아직 저도 보지는 못했지만… 프로그래밍 스칼라 번역서가 나왔는데, 이 책도 읽어보시면 좋을듯 합니다. 저도 곧 읽어볼 계획입니다.

다만 스칼라라는 언어가 단순히 책만 읽는다고 쉽게 이해가 되는 언어는 아니라서, 계속 계속
짜보셔야만 어느정도 익숙해 질듯 합니다. 저도 열심히 보는 중입니다.


[입 개발] memcached 소스 분석 #1

$
0
0

slab

MAX_NUMBER_OF_SLAB_CLASSES = 63 + 1로 정의됨

static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

slabclass_t 구조체는 다음과 같다. slabclass 에서 list_size 는
slab_list 의 capacity 이다. 그리고 slabs는 현재 몇개의 slab이
할당되었는지는 나타낸다.(현재의 length)

typedef struct {
unsigned int size; /* sizes of items */
unsigned int perslab; /* how many items per slab */

void *slots; /* list of item ptrs */
unsigned int sl_curr; /* total free items in list */

unsigned int slabs; /* how many slabs were allocated for this class */

void **slab_list; /* array of slab pointers */
unsigned int list_size; /* size of prev array */

size_t requested; /* The number of requested bytes */
} slabclass_t;

memcache 는 시작시에 factor 값에 의해서 사이즈 Range 별로 slabclass id를 가지도록
설정한다. slabs_preallocate 을 이용해서 미리 메모리를 할당해둘 수 도 있다.
이걸 이용하지 않으면 chunk 가 필요할 때 chunk를 할당한다. 기본 chunk size는 1MB이다.

#define CHUNK_ALIGN_BYTES 8

CHUNK_ALIGN_BYTES = 8

void slabs_init(const size_t limit, const double factor, const bool prealloc) {
int i = POWER_SMALLEST - 1;
//default chunk_size = 48
//sizeof(item) = 48
//factor = 1.25
//item_size_max = 1024 * 1024
//POWER_SMALLEST = 1

unsigned int size = sizeof(item) + settings.chunk_size;

mem_limit = limit;

if (prealloc) {
/* Allocate everything in a big chunk with malloc */
mem_base = malloc(mem_limit);
if (mem_base != NULL) {
mem_current = mem_base;
mem_avail = mem_limit;
} else {
fprintf(stderr, "Warning: Failed to allocate requested memory in"
" one large chunk.\nWill allocate in smaller chunks\n");
}
}

memset(slabclass, 0, sizeof(slabclass));

while (++i < MAX_NUMBER_OF_SLAB_CLASSES-1 && size <= settings.item_size_max / factor) {
/* Make sure items are always n-byte aligned */
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);

slabclass[i].size = size;
slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
size *= factor;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
i, slabclass[i].size, slabclass[i].perslab);
}
}

power_largest = i;
slabclass[power_largest].size = settings.item_size_max;
slabclass[power_largest].perslab = 1;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
i, slabclass[i].size, slabclass[i].perslab);
}

/* for the test suite: faking of how much we've already malloc'd */
{
char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
if (t_initial_malloc) {
mem_malloced = (size_t)atol(t_initial_malloc);
}

}

if (prealloc) {
slabs_preallocate(power_largest);
}
}

slab size 초기화는 다음과 같이 이루어진다. 이 의미는 size가 96까지를 다루는 slabclass
는 그 안에 10922개의 아이템을 저장할 수 있다는 의미이다.

size: 96 perslab: 10922
size: 120 perslab: 8738
size: 152 perslab: 6898
size: 192 perslab: 5461
size: 240 perslab: 4369
size: 304 perslab: 3449
size: 384 perslab: 2730
size: 480 perslab: 2184
size: 600 perslab: 1747
size: 752 perslab: 1394
size: 944 perslab: 1110
size: 1184 perslab: 885
size: 1480 perslab: 708
size: 1856 perslab: 564
size: 2320 perslab: 451
size: 2904 perslab: 361
size: 3632 perslab: 288
size: 4544 perslab: 230
size: 5680 perslab: 184
size: 7104 perslab: 147
size: 8880 perslab: 118
size: 11104 perslab: 94
size: 13880 perslab: 75
size: 17352 perslab: 60
size: 21696 perslab: 48
size: 27120 perslab: 38
size: 33904 perslab: 30
size: 42384 perslab: 24
size: 52984 perslab: 19
size: 66232 perslab: 15
size: 82792 perslab: 12
size: 103496 perslab: 10
size: 129376 perslab: 8
size: 161720 perslab: 6
size: 202152 perslab: 5
size: 252696 perslab: 4
size: 315872 perslab: 3
size: 394840 perslab: 2
size: 493552 perslab: 2
size: 616944 perslab: 1
size: 771184 perslab: 1

이렇게 만들어진 slabclass 는 slabs_clsid 를 통해서 접근 할 수 있다. 인덱스를
하나씩 증가하면 적절한 크기의 slabclass를 선택한다.

unsigned int slabs_clsid(const size_t size) {
int res = POWER_SMALLEST;

if (size == 0)
return 0;
while (size > slabclass[res].size)
if (res++ == power_largest) /* won't fit in the biggest slab */
return 0;
return res;
}

slabclass는 item 과도 관계가 있다. 실제 slab_alloc 은 item_alloc 이 호출될때
호출되게 된다.

typedef unsigned int rel_time_t;

typedef struct _stritem {
/* Protected by LRU locks */
struct _stritem *next;
struct _stritem *prev;
/* Rest are protected by an item lock */
struct _stritem *h_next; /* hash chain next */
rel_time_t time; /* least recent access */
rel_time_t exptime; /* expire time */
int nbytes; /* size of data */
unsigned short refcount;
uint8_t nsuffix; /* length of flags-and-length string */
uint8_t it_flags; /* ITEM_* above */
uint8_t slabs_clsid;/* which slab class we're in */
uint8_t nkey; /* key length, w/terminating null and padding */
/* this odd type prevents type-punning issues when we do
* the little shuffle to save space when not using CAS. */
union {
uint64_t cas;
char end;
} data[];
/* if it_flags & ITEM_CAS we have 8 bytes CAS */
/* then null-terminated key */
/* then " flags length\r\n" (no terminating null) */
/* then data with terminating \r\n (no terminating null; it's binary!) */
} item;

최초 item 할당시에 당연히 slab 도 없기 때문에, do_slabs_newslab()를 호출하게 된다.
SLAB_GLOBAL_PAGE_POOL 는 Reassignment를 위한 것이므로 최초에는 slabclass만 존재하고
내부에 할당된 slab은 없음. get_page_from_global_pool() 에서도 데이터가 없으므로 실제
메모리 할당은 memory_allocate 에 의해서 이루어진다.

static int do_slabs_newslab(const unsigned int id) {
slabclass_t *p = &slabclass[id];
slabclass_t *g = &slabclass[SLAB_GLOBAL_PAGE_POOL];
int len = settings.slab_reassign ? settings.item_size_max
: p->size * p->perslab;
char *ptr;

if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0
&& g->slabs == 0)) {
mem_limit_reached = true;
MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}

if ((grow_slab_list(id) == 0) ||
(((ptr = get_page_from_global_pool()) == NULL) &&
((ptr = memory_allocate((size_t)len)) == 0))) {

MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}

memset(ptr, 0, (size_t)len);
split_slab_page_into_freelist(ptr, id);

p->slab_list[p->slabs++] = ptr;
MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

return 1;
}

할당된 메모리 ptr은 split_slab_page_into_freelist 에 의해서 초기화 된다.

static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
slabclass_t *p = &slabclass[id];
int x;
for (x = 0; x < p->perslab; x++) {
do_slabs_free(ptr, 0, id);
ptr += p->size;
}
}

p->size는 slabclass item의 크기이므로 그 값만큼 증가하면서 do_slabs_free 를 호출해서
item을 저장할 수 있는 형태로 정보를 저장한다. ptr 이 p->size 만큼 계속 증가하는데 주목하자.

static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
slabclass_t *p;
item *it;

assert(id >= POWER_SMALLEST && id <= power_largest);
if (id < POWER_SMALLEST || id > power_largest)
return;

MEMCACHED_SLABS_FREE(size, id, ptr);
p = &slabclass[id];

it = (item *)ptr;
it->it_flags = ITEM_SLABBED;
it->slabs_clsid = 0;
it->prev = 0;
it->next = p->slots;
if (it->next) it->next->prev = it;
p->slots = it;

p->sl_curr++;
p->requested -= size;
return;
}

p는 slabclass, it는 해당 ptr의 메모리 영역이다. p->slots는 기존에 할당된 item list를
가리키는 포인터이다. 즉 it->next 로 현재의 it->next 에 기존의 item list를 저장하고,
기존의 item list의 prev는 새롭게 추가될 it가 된다. 그리고 다시 p->slots은 it를 가리키게
되므로, linked list로 slabclass에 할당되는 모든 item들이 double linked list 형식으로
저장되게 된다.(실제로 메모리 할당은 item_max_size 형태로 할당되지만… 논리적으로 이어진다.)

 


[입 개발] memcached 소스분석 #2

$
0
0

item

해당 부분은 실제로 hashtable을 구축해서, item을 어떻게 저장하고 찾을 것인지에 대한 부분이다.

#define POWER_LARGEST 256 /* actual cap is 255 */

#define LARGEST_ID POWER_LARGEST

static item *heads[LARGEST_ID];
static item *tails[LARGEST_ID];

POWER_LARGEST는 slabclass의 최대 개수이다. 즉 item *heads, *tails는 slabclass 마다 존재한다고
생각하면 간단하다.

일단 프로토콜 파싱 부분을 보자. 여기서는 일단 text 프로토콜만 살펴본다.(실제로는 거의 유사하므로…)
protocol 파싱 부분은 memcached.c 에 존재한다. get은 데이터를 찾아오는 명령이다. 실제로 item_get
을 호출해서 item이 존재하는지 체크한다.

/* ntokens is overwritten here... shrug.. */
static inline void process_get_command(conn *c, token_t *tokens, size_t ntokens, bool return_cas) {
char *key;
size_t nkey;
int i = 0;
item *it;
token_t *key_token = &tokens[KEY_TOKEN];
char *suffix;
assert(c != NULL);

do {
while(key_token->length != 0) {

key = key_token->value;
nkey = key_token->length;

if(nkey > KEY_MAX_LENGTH) {
out_string(c, "CLIENT_ERROR bad command line format");
while (i-- > 0) {
item_remove(*(c->ilist + i));
}
return;
}

it = item_get(key, nkey);
if (settings.detail_enabled) {
stats_prefix_record_get(key, nkey, NULL != it);
}
if (it) {
if (i >= c->isize) {
item **new_list = realloc(c->ilist, sizeof(item *) * c->isize * 2);
if (new_list) {
c->isize *= 2;
c->ilist = new_list;
} else {
STATS_LOCK();
stats.malloc_fails++;
STATS_UNLOCK();
item_remove(it);
break;
}
}

/*
* Construct the response. Each hit adds three elements to the
* outgoing data list:
* "VALUE "
* key
* " " + flags + " " + data length + "\r\n" + data (with \r\n)
*/

if (return_cas)
{
MEMCACHED_COMMAND_GET(c->sfd, ITEM_key(it), it->nkey,
it->nbytes, ITEM_get_cas(it));
/* Goofy mid-flight realloc. */
if (i >= c->suffixsize) {
char **new_suffix_list = realloc(c->suffixlist,
sizeof(char *) * c->suffixsize * 2);
if (new_suffix_list) {
c->suffixsize *= 2;
c->suffixlist = new_suffix_list;
} else {
STATS_LOCK();
stats.malloc_fails++;
STATS_UNLOCK();
item_remove(it);
break;
}
}

suffix = cache_alloc(c->thread->suffix_cache);
if (suffix == NULL) {
STATS_LOCK();
stats.malloc_fails++;
STATS_UNLOCK();
out_of_memory(c, "SERVER_ERROR out of memory making CAS suffix");
item_remove(it);
while (i-- > 0) {
item_remove(*(c->ilist + i));
}
return;
}
*(c->suffixlist + i) = suffix;
int suffix_len = snprintf(suffix, SUFFIX_SIZE,
" %llu\r\n",
(unsigned long long)ITEM_get_cas(it));
if (add_iov(c, "VALUE ", 6) != 0 ||
add_iov(c, ITEM_key(it), it->nkey) != 0 ||
add_iov(c, ITEM_suffix(it), it->nsuffix - 2) != 0 ||
add_iov(c, suffix, suffix_len) != 0 ||
add_iov(c, ITEM_data(it), it->nbytes) != 0)
{
item_remove(it);
break;
}
}
else
{
MEMCACHED_COMMAND_GET(c->sfd, ITEM_key(it), it->nkey,
it->nbytes, ITEM_get_cas(it));
if (add_iov(c, "VALUE ", 6) != 0 ||
add_iov(c, ITEM_key(it), it->nkey) != 0 ||
add_iov(c, ITEM_suffix(it), it->nsuffix + it->nbytes) != 0)
{
item_remove(it);
break;
}
}
if (settings.verbose > 1) {
int ii;
fprintf(stderr, ">%d sending key ", c->sfd);
for (ii = 0; ii < it->nkey; ++ii) {
fprintf(stderr, "%c", key[ii]);
}
fprintf(stderr, "\n");
}

/* item_get() has incremented it->refcount for us */
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.slab_stats[ITEM_clsid(it)].get_hits++;
c->thread->stats.get_cmds++;
pthread_mutex_unlock(&c->thread->stats.mutex);
item_update(it);
*(c->ilist + i) = it;
i++;

} else {
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.get_misses++;
c->thread->stats.get_cmds++;
pthread_mutex_unlock(&c->thread->stats.mutex);
MEMCACHED_COMMAND_GET(c->sfd, key, nkey, -1, 0);
}

key_token++;
}

/*
* If the command string hasn't been fully processed, get the next set
* of tokens.
*/
if(key_token->value != NULL) {
ntokens = tokenize_command(key_token->value, tokens, MAX_TOKENS);
key_token = tokens;
}

} while(key_token->value != NULL);

c->icurr = c->ilist;
c->ileft = i;
if (return_cas) {
c->suffixcurr = c->suffixlist;
c->suffixleft = i;
}

if (settings.verbose > 1)
fprintf(stderr, ">%d END\n", c->sfd);

/*
If the loop was terminated because of out-of-memory, it is not
reliable to add END\r\n to the buffer, because it might not end
in \r\n. So we send SERVER_ERROR instead.
*/
if (key_token->value != NULL || add_iov(c, "END\r\n", 5) != 0
|| (IS_UDP(c->transport) && build_udp_headers(c) != 0)) {
out_of_memory(c, "SERVER_ERROR out of memory writing get response");
}
else {
conn_set_state(c, conn_mwrite);
c->msgcurr = 0;
}
}

item_get 은 다음과 같이 구현되어 있다. key 를 hash해서 hv 값을 만든다.
이때 hash 함수는 jenkins hash 와 murmur3가 있는데 기본으로 jenkins hash가 적용된다.
(설정가능)

/*
* Returns an item if it hasn't been marked as expired,
* lazy-expiring as needed.
*/
item *item_get(const char *key, const size_t nkey) {
item *it;
uint32_t hv;
hv = hash(key, nkey);
item_lock(hv);
it = do_item_get(key, nkey, hv);
item_unlock(hv);
return it;
}

먼저 item_lock 코드를 살펴보자.

#define hashsize(n) ((unsigned long int)1<<(n))
#define hashmask(n) (hashsize(n)-1)

void item_lock(uint32_t hv) {
mutex_lock(&item_locks[hv & hashmask(item_lock_hashpower)]);
}

먼저 해당 item 이 들어있는 영역의 Lock을 건다. 여기서 이 item_locks는
memcached_thread_init() 함수의 thread 개수에 따라서 바뀌게 된다. thread
개수가 3개 이하면 1024개의 item_locks 가 생기고 5개 이상이면 8192개의
item_locks 가 생기게 된다.

이 의미는 해당 영역만 lock 이 걸리므로 다른 hash 결과와 연관된 item의 경우는
다른 thread에서 여전히 접근이 가능하다는 것이다.

/*
* Initializes the thread subsystem, creating various worker threads.
*
* nthreads Number of worker event handler threads to spawn
* main_base Event base for main thread
*/
void memcached_thread_init(int nthreads, struct event_base *main_base) {
int i;
int power;

for (i = 0; i < POWER_LARGEST; i++) {
pthread_mutex_init(&lru_locks[i], NULL);
}
pthread_mutex_init(&worker_hang_lock, NULL);

pthread_mutex_init(&init_lock, NULL);
pthread_cond_init(&init_cond, NULL);

pthread_mutex_init(&cqi_freelist_lock, NULL);
cqi_freelist = NULL;

/* Want a wide lock table, but don't waste memory */
if (nthreads < 3) {
power = 10;
} else if (nthreads < 4) {
power = 11;
} else if (nthreads < 5) {
power = 12;
} else {
/* 8192 buckets, and central locks don't scale much past 5 threads */
power = 13;
}

if (power >= hashpower) {
fprintf(stderr, "Hash table power size (%d) cannot be equal to or less than item lock table (%d)\n", hashpower, power);
fprintf(stderr, "Item lock table grows with `-t N` (worker threadcount)\n");
fprintf(stderr, "Hash table grows with `-o hashpower=N` \n");
exit(1);
}

item_lock_count = hashsize(power);
item_lock_hashpower = power;

item_locks = calloc(item_lock_count, sizeof(pthread_mutex_t));
if (! item_locks) {
perror("Can't allocate item locks");
exit(1);
}
for (i = 0; i < item_lock_count; i++) {
pthread_mutex_init(&item_locks[i], NULL);
}

threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));
if (! threads) {
perror("Can't allocate thread descriptors");
exit(1);
}

dispatcher_thread.base = main_base;
dispatcher_thread.thread_id = pthread_self();

for (i = 0; i < nthreads; i++) {
int fds[2];
if (pipe(fds)) {
perror("Can't create notify pipe");
exit(1);
}

threads[i].notify_receive_fd = fds[0];
threads[i].notify_send_fd = fds[1];

setup_thread(&threads[i]);
/* Reserve three fds for the libevent base, and two for the pipe */
stats.reserved_fds += 5;
}

/* Create threads after we've done all the libevent setup. */
for (i = 0; i < nthreads; i++) {
create_worker(worker_libevent, &threads[i]);
}

/* Wait for all the threads to set themselves up before returning. */
pthread_mutex_lock(&init_lock);
wait_for_thread_registration(nthreads);
pthread_mutex_unlock(&init_lock);
}

이제 다시 do_item_get() 함수를 살펴보자.

/** wrapper around assoc_find which does the lazy expiration logic */
item *do_item_get(const char *key, const size_t nkey, const uint32_t hv) {
item *it = assoc_find(key, nkey, hv);
if (it != NULL) {
refcount_incr(&it->refcount);
/* Optimization for slab reassignment. prevents popular items from
* jamming in busy wait. Can only do this here to satisfy lock order
* of item_lock, slabs_lock. */
/* This was made unsafe by removal of the cache_lock:
* slab_rebalance_signal and slab_rebal.* are modified in a separate
* thread under slabs_lock. If slab_rebalance_signal = 1, slab_start =
* NULL (0), but slab_end is still equal to some value, this would end
* up unlinking every item fetched.
* This is either an acceptable loss, or if slab_rebalance_signal is
* true, slab_start/slab_end should be put behind the slabs_lock.
* Which would cause a huge potential slowdown.
* Could also use a specific lock for slab_rebal.* and
* slab_rebalance_signal (shorter lock?)
*/
/*if (slab_rebalance_signal &&
((void *)it >= slab_rebal.slab_start && (void *)it < slab_rebal.slab_end)) {
do_item_unlink(it, hv);
do_item_remove(it);
it = NULL;
}*/
}
int was_found = 0;

if (settings.verbose > 2) {
int ii;
if (it == NULL) {
fprintf(stderr, "> NOT FOUND ");
} else {
fprintf(stderr, "> FOUND KEY ");
was_found++;
}
for (ii = 0; ii < nkey; ++ii) {
fprintf(stderr, "%c", key[ii]);
}
}

if (it != NULL) {
if (item_is_flushed(it)) {
do_item_unlink(it, hv);
do_item_remove(it);
it = NULL;
if (was_found) {
fprintf(stderr, " -nuked by flush");
}
} else if (it->exptime != 0 && it->exptime <= current_time) {
do_item_unlink(it, hv);
do_item_remove(it);
it = NULL;
if (was_found) {
fprintf(stderr, " -nuked by expire");
}
} else {
it->it_flags |= ITEM_FETCHED|ITEM_ACTIVE;
DEBUG_REFCNT(it, '+');
}
}

if (settings.verbose > 2)
fprintf(stderr, "\n");

return it;
}

실제로 item은 다시 assoc_find 함수를 통해서 hashtable에서 item을 가져오게 된다.
item이 있다면, flush 되었는지, expire 가 되었는지 확인해서 이 경우에는 item을 지우고
(정확히는 item 객체를 풀에 반납하고) NULL을 반납하게 된다.

memcached의 경우는 내부적으로 flush_all 이라는 명령이 오면.. 현재 시간을 저장하고, 그것보다
이전에 생성된 데이터의 경우에 (item->time 에 생성시간이 저장됨.) 지워버림. 이것을 flush 라고 함

item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
item *it;
unsigned int oldbucket;

if (expanding &&
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
it = old_hashtable[oldbucket];
} else {
it = primary_hashtable[hv & hashmask(hashpower)];
}

item *ret = NULL;
int depth = 0;
while (it) {
if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {
ret = it;
break;
}
it = it->h_next;
++depth;
}
MEMCACHED_ASSOC_FIND(key, nkey, depth);
return ret;
}

memcached 에서 primary_hashtable 와 old_hashtable 가 있는데 old_hashtable는
hashtable의 expanding 중에만 존재하는 hashtable이다. 정확히는 확장 전까지의
primary_hashtable이 old_hashtable이 된다. 확장 전의 bucket 이면 old_hashtable에서
데이터를 찾고, 이미 확장된 bucket 은 primary_hashtable 에서 찾는다.

memcached 의 hashtable은 일반적으로 해당 hashtable 안에 linked list 가 들어가 있는
구조이다. 즉 hv 값으로 hashtable을 찾고 거기서는 linked list의 선형 탐색을 하게 되는
것이다. java8에서의 hashmap은 안에 tree 형태로 데이터가 들어가서 많은 데이터를 가지고
있을 경우, 더 빠른 탐색 시간을 보장하는데… memcached 와 redis 등은 hashtable extending
으로 이런 이슈를 해결한다.

이제 hashtable에 저장하는 코드를 살펴보자. process_update_command() 에서 시작하게 된다.

static void process_update_command(conn *c, token_t *tokens, const size_t ntokens, int comm, bool handle_cas) {
char *key;
size_t nkey;
unsigned int flags;
int32_t exptime_int = 0;
time_t exptime;
int vlen;
uint64_t req_cas_id=0;
item *it;

assert(c != NULL);

set_noreply_maybe(c, tokens, ntokens);

if (tokens[KEY_TOKEN].length > KEY_MAX_LENGTH) {
out_string(c, "CLIENT_ERROR bad command line format");
return;
}

key = tokens[KEY_TOKEN].value;
nkey = tokens[KEY_TOKEN].length;

if (! (safe_strtoul(tokens[2].value, (uint32_t *)&flags)
&& safe_strtol(tokens[3].value, &exptime_int)
&& safe_strtol(tokens[4].value, (int32_t *)&vlen))) {
out_string(c, "CLIENT_ERROR bad command line format");
return;
}

/* Ubuntu 8.04 breaks when I pass exptime to safe_strtol */
exptime = exptime_int;

/* Negative exptimes can underflow and end up immortal. realtime() will
immediately expire values that are greater than REALTIME_MAXDELTA, but less
than process_started, so lets aim for that. */
if (exptime < 0)
exptime = REALTIME_MAXDELTA + 1;

// does cas value exist?
if (handle_cas) {
if (!safe_strtoull(tokens[5].value, &req_cas_id)) {
out_string(c, "CLIENT_ERROR bad command line format");
return;
}
}

vlen += 2;
if (vlen < 0 || vlen - 2 < 0) {
out_string(c, "CLIENT_ERROR bad command line format");
return;
}

if (settings.detail_enabled) {
stats_prefix_record_set(key, nkey);
}

it = item_alloc(key, nkey, flags, realtime(exptime), vlen);

if (it == 0) {
if (! item_size_ok(nkey, flags, vlen))
out_string(c, "SERVER_ERROR object too large for cache");
else
out_of_memory(c, "SERVER_ERROR out of memory storing object");
/* swallow the data line */
c->write_and_go = conn_swallow;
c->sbytes = vlen;

/* Avoid stale data persisting in cache because we failed alloc.
* Unacceptable for SET. Anywhere else too? */
if (comm == NREAD_SET) {
it = item_get(key, nkey);
if (it) {
item_unlink(it);
item_remove(it);
}
}

return;
}
ITEM_set_cas(it, req_cas_id);

c->item = it;
c->ritem = ITEM_data(it);
c->rlbytes = it->nbytes;
c->cmd = comm;
conn_set_state(c, conn_nread);
}

일단 할당을 위해서 item_alloc() 을 호출하게 됩니다. item_alloc은 do_item_alloc 에서
내부적으로 lock을 사용하므로 따로 lock을 사용하지 않습니다.

item *item_alloc(char *key, size_t nkey, int flags, rel_time_t exptime, int nbytes) {
item *it;
/* do_item_alloc handles its own locks */
it = do_item_alloc(key, nkey, flags, exptime, nbytes, 0);
return it;
}

do_item_alloc 에서는 먼저 item 이 사용해야할 size를 구합니다. slabclass가 size 별로
구분되어 있는 걸 기억한다면, 아 이제 slabclass 에서 item 을 가져오겠다는 걸 예상할 수 있습니다.
item_make_header() 는 item이 차지할 size를 알려주는 함수입니다.

slab_alloc을 호출해서 사용할 item 을 가져옵니다.(이 때 실제로 slab에서 할당됩니다.)
여기서는 실제 item 구조체에 data, flags, key등을 셋팅하고 item 구조체를 넘겨줍니다.

item *do_item_alloc(char *key, const size_t nkey, const int flags,
const rel_time_t exptime, const int nbytes,
const uint32_t cur_hv) {
int i;
uint8_t nsuffix;
item *it = NULL;
char suffix[40];
unsigned int total_chunks;
size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
if (settings.use_cas) {
ntotal += sizeof(uint64_t);
}

unsigned int id = slabs_clsid(ntotal);
if (id == 0)
return 0;

/* If no memory is available, attempt a direct LRU juggle/eviction */
/* This is a race in order to simplify lru_pull_tail; in cases where
* locked items are on the tail, you want them to fall out and cause
* occasional OOM's, rather than internally work around them.
* This also gives one fewer code path for slab alloc/free
*/
for (i = 0; i < 5; i++) {
/* Try to reclaim memory first */
if (!settings.lru_maintainer_thread) {
lru_pull_tail(id, COLD_LRU, 0, false, cur_hv);
}
it = slabs_alloc(ntotal, id, &total_chunks, 0);
if (settings.expirezero_does_not_evict)
total_chunks -= noexp_lru_size(id);
if (it == NULL) {
if (settings.lru_maintainer_thread) {
lru_pull_tail(id, HOT_LRU, total_chunks, false, cur_hv);
lru_pull_tail(id, WARM_LRU, total_chunks, false, cur_hv);
lru_pull_tail(id, COLD_LRU, total_chunks, true, cur_hv);
} else {
lru_pull_tail(id, COLD_LRU, 0, true, cur_hv);
}
} else {
break;
}
}

if (i > 0) {
pthread_mutex_lock(&lru_locks[id]);
itemstats[id].direct_reclaims += i;
pthread_mutex_unlock(&lru_locks[id]);
}

if (it == NULL) {
pthread_mutex_lock(&lru_locks[id]);
itemstats[id].outofmemory++;
pthread_mutex_unlock(&lru_locks[id]);
return NULL;
}

assert(it->slabs_clsid == 0);
//assert(it != heads[id]);

/* Refcount is seeded to 1 by slabs_alloc() */
it->next = it->prev = it->h_next = 0;
/* Items are initially loaded into the HOT_LRU. This is '0' but I want at
* least a note here. Compiler (hopefully?) optimizes this out.
*/
if (settings.lru_maintainer_thread) {
if (exptime == 0 && settings.expirezero_does_not_evict) {
id |= NOEXP_LRU;
} else {
id |= HOT_LRU;
}
} else {
/* There is only COLD in compat-mode */
id |= COLD_LRU;
}
it->slabs_clsid = id;

DEBUG_REFCNT(it, '*');
it->it_flags = settings.use_cas ? ITEM_CAS : 0;
it->nkey = nkey;
it->nbytes = nbytes;
memcpy(ITEM_key(it), key, nkey);
it->exptime = exptime;
memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
it->nsuffix = nsuffix;
return it;
}

그런데 뭔가 이상한 걸 느끼지 못했나요? get 은 실제 hashtable에서 데이터를 가져왔는데…
왜 set 은 실제 데이터를 넣는 부분이 없을까요? 실제 memcached 코드에서는 이 부분이 나눠져
있습니다. 먼저 memcached text 프로토콜에서 set 프로토콜을 한번 살펴봅시다.
먼저 set 명령은 2 line으로 구분되고 첫번째 라인은 다음과 같이 구성됩니다.

SET key [flags] [exptime] length [noreply]

중간에 length 가 보이지요? 그럼 그 다음중에는 무엇이 와야 할까요? 네… 정답입니다.
위에서 length 지정한 크기 만큼의 실제 데이터죠. 그래서 process_update_command의 마지막에서
다음과 같이 conn_set_state 함수를 통해서 현재 connection의 상태를 conn_nread로 만들어둡니다.

c->item = it;
c->ritem = ITEM_data(it);
c->rlbytes = it->nbytes;
c->cmd = comm;
conn_set_state(c, conn_nread);

그러면 실제 event_handler 에서 conn_nread 상태에서는 다음과 같이 호출되게 됩니다.

case conn_nread:
if (c->rlbytes == 0) {
complete_nread(c);
break;
}

/* Check if rbytes < 0, to prevent crash */
if (c->rlbytes < 0) {
if (settings.verbose) {
fprintf(stderr, "Invalid rlbytes to read: len %d\n", c->rlbytes);
}
conn_set_state(c, conn_closing);
break;
}

/* first check if we have leftovers in the conn_read buffer */
if (c->rbytes > 0) {
int tocopy = c->rbytes > c->rlbytes ? c->rlbytes : c->rbytes;
if (c->ritem != c->rcurr) {
memmove(c->ritem, c->rcurr, tocopy);
}
c->ritem += tocopy;
c->rlbytes -= tocopy;
c->rcurr += tocopy;
c->rbytes -= tocopy;
if (c->rlbytes == 0) {
break;
}
}

/* now try reading from the socket */
res = read(c->sfd, c->ritem, c->rlbytes);
if (res > 0) {
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.bytes_read += res;
pthread_mutex_unlock(&c->thread->stats.mutex);
if (c->rcurr == c->ritem) {
c->rcurr += res;
}
c->ritem += res;
c->rlbytes -= res;
break;
}
if (res == 0) { /* end of stream */
conn_set_state(c, conn_closing);
break;
}
if (res == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
if (!update_event(c, EV_READ | EV_PERSIST)) {
if (settings.verbose > 0)
fprintf(stderr, "Couldn't update event\n");
conn_set_state(c, conn_closing);
break;
}
stop = true;
break;
}
/* otherwise we have a real error, on which we close the connection */
if (settings.verbose > 0) {
fprintf(stderr, "Failed to read, and not due to blocking:\n"
"errno: %d %s \n"
"rcurr=%lx ritem=%lx rbuf=%lx rlbytes=%d rsize=%d\n",
errno, strerror(errno),
(long)c->rcurr, (long)c->ritem, (long)c->rbuf,
(int)c->rlbytes, (int)c->rsize);
}
conn_set_state(c, conn_closing);
break;

conn_nread 에서는 c->rlbytes 값이 0이 될때까지 계속 이벤트가 발생할 때 마다
read 함수를 통해서 데이터를 읽어들이게 됩니다. 그리고 c->rlbytes == 0이면
필요한 만큼의 데이터를 읽었다는 의미이므로 complete_nread()를 호출합니다.

static void complete_nread(conn *c) {
assert(c != NULL);
assert(c->protocol == ascii_prot
|| c->protocol == binary_prot);

if (c->protocol == ascii_prot) {
complete_nread_ascii(c);
} else if (c->protocol == binary_prot) {
complete_nread_binary(c);
}
}

이제 complete_nread_ascii 함수에서 store_item()을 호출해서 최종 마무리 작업을 하게 됩니다.

/*
* we get here after reading the value in set/add/replace commands. The command
* has been stored in c->cmd, and the item is ready in c->item.
*/
static void complete_nread_ascii(conn *c) {
assert(c != NULL);

item *it = c->item;
int comm = c->cmd;
enum store_item_type ret;

pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.slab_stats[ITEM_clsid(it)].set_cmds++;
pthread_mutex_unlock(&c->thread->stats.mutex);

if (strncmp(ITEM_data(it) + it->nbytes - 2, "\r\n", 2) != 0) {
out_string(c, "CLIENT_ERROR bad data chunk");
} else {
ret = store_item(it, comm, c);

switch (ret) {
case STORED:
out_string(c, "STORED");
break;
case EXISTS:
out_string(c, "EXISTS");
break;
case NOT_FOUND:
out_string(c, "NOT_FOUND");
break;
case NOT_STORED:
out_string(c, "NOT_STORED");
break;
default:
out_string(c, "SERVER_ERROR Unhandled storage type.");
}
}

item_remove(c->item); /* release the c->item reference */
c->item = 0;
}

store_item 은 다시 item_lock을 걸고 do_store_item 을 호출합니다.

/*
* Stores an item in the cache (high level, obeys set/add/replace semantics)
*/
enum store_item_type store_item(item *item, int comm, conn* c) {
enum store_item_type ret;
uint32_t hv;

hv = hash(ITEM_key(item), item->nkey);
item_lock(hv);
ret = do_store_item(item, comm, c, hv);
item_unlock(hv);
return ret;
}

do_store_item 은 좀 더 복잡한 작업을 합니다. 최초의 함수명을 기억하시나요?
process_update_command 입니다. 즉 없던 item이 추가되는 경우만 아니라, 기존의 item 이
바뀌는 경우도 당연히 발생합니다.

그래서 먼저 do_item_get() 을 호출해서 item을 찾습니다. 일단은 기존에 item 이 없던
새로운 item이 추가되는 경우만 살펴보도록 하겠습니다.

/*
* Stores an item in the cache according to the semantics of one of the set
* commands. In threaded mode, this is protected by the cache lock.
*
* Returns the state of storage.
*/
enum store_item_type do_store_item(item *it, int comm, conn *c, const uint32_t hv) {
char *key = ITEM_key(it);
item *old_it = do_item_get(key, it->nkey, hv);
enum store_item_type stored = NOT_STORED;

item *new_it = NULL;
int flags;

if (old_it != NULL && comm == NREAD_ADD) {
/* add only adds a nonexistent item, but promote to head of LRU */
do_item_update(old_it);
} else if (!old_it && (comm == NREAD_REPLACE
|| comm == NREAD_APPEND || comm == NREAD_PREPEND))
{
/* replace only replaces an existing value; don't store */
} else if (comm == NREAD_CAS) {
/* validate cas operation */
if(old_it == NULL) {
// LRU expired
stored = NOT_FOUND;
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.cas_misses++;
pthread_mutex_unlock(&c->thread->stats.mutex);
}
else if (ITEM_get_cas(it) == ITEM_get_cas(old_it)) {
// cas validates
// it and old_it may belong to different classes.
// I'm updating the stats for the one that's getting pushed out
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.slab_stats[ITEM_clsid(old_it)].cas_hits++;
pthread_mutex_unlock(&c->thread->stats.mutex);

item_replace(old_it, it, hv);
stored = STORED;
} else {
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.slab_stats[ITEM_clsid(old_it)].cas_badval++;
pthread_mutex_unlock(&c->thread->stats.mutex);

if(settings.verbose > 1) {
fprintf(stderr, "CAS: failure: expected %llu, got %llu\n",
(unsigned long long)ITEM_get_cas(old_it),
(unsigned long long)ITEM_get_cas(it));
}
stored = EXISTS;
}
} else {
/*
* Append - combine new and old record into single one. Here it's
* atomic and thread-safe.
*/
if (comm == NREAD_APPEND || comm == NREAD_PREPEND) {
/*
* Validate CAS
*/
if (ITEM_get_cas(it) != 0) {
// CAS much be equal
if (ITEM_get_cas(it) != ITEM_get_cas(old_it)) {
stored = EXISTS;
}
}

if (stored == NOT_STORED) {
/* we have it and old_it here - alloc memory to hold both */
/* flags was already lost - so recover them from ITEM_suffix(it) */

flags = (int) strtol(ITEM_suffix(old_it), (char **) NULL, 10);

new_it = do_item_alloc(key, it->nkey, flags, old_it->exptime, it->nbytes + old_it->nbytes - 2 /* CRLF */, hv);

if (new_it == NULL) {
/* SERVER_ERROR out of memory */
if (old_it != NULL)
do_item_remove(old_it);

return NOT_STORED;
}

/* copy data from it and old_it to new_it */

if (comm == NREAD_APPEND) {
memcpy(ITEM_data(new_it), ITEM_data(old_it), old_it->nbytes);
memcpy(ITEM_data(new_it) + old_it->nbytes - 2 /* CRLF */, ITEM_data(it), it->nbytes);
} else {
/* NREAD_PREPEND */
memcpy(ITEM_data(new_it), ITEM_data(it), it->nbytes);
memcpy(ITEM_data(new_it) + it->nbytes - 2 /* CRLF */, ITEM_data(old_it), old_it->nbytes);
}

it = new_it;
}
}

if (stored == NOT_STORED) {
if (old_it != NULL)
item_replace(old_it, it, hv);
else
do_item_link(it, hv);

c->cas = ITEM_get_cas(it);

stored = STORED;
}
}

if (old_it != NULL)
do_item_remove(old_it); /* release our reference */
if (new_it != NULL)
do_item_remove(new_it);

if (stored == STORED) {
c->cas = ITEM_get_cas(it);
}

return stored;
}

기존 item 이 있다면(update), item_replace 이 호출되고, 새로운 item(set) 이라면
do_item_link 가 호출되게 됩니다.

int do_item_link(item *it, const uint32_t hv) {
MEMCACHED_ITEM_LINK(ITEM_key(it), it->nkey, it->nbytes);
assert((it->it_flags & (ITEM_LINKED|ITEM_SLABBED)) == 0);
it->it_flags |= ITEM_LINKED;
it->time = current_time;

STATS_LOCK();
stats.curr_bytes += ITEM_ntotal(it);
stats.curr_items += 1;
stats.total_items += 1;
STATS_UNLOCK();

/* Allocate a new CAS ID on link. */
ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0);
assoc_insert(it, hv);
item_link_q(it);
refcount_incr(&it->refcount);

return 1;
}

여기서 최종적으로 assoc_insert 를 호출하면서 hashtable에 데이터가 저장됩니다.
그리고 아까 언급했던 item->time 에 현재 시간이 들어갑니다. assoc_find 와 거의 동일합니다.
hv 값을 통해서 hashtable의 index를 구하고 거기 첫 값을 it->h_next 로 설정하고,
해당 table의 첫번째 값을 새로운 item으로 설정합니다. 즉, hashtable의 head로 새로운
item이 들어가는 거죠. 이때 hash_items 가 (hashsize(hashpower) * 3) / 2 보다 커지면
두배로 확장하게 됩니다.

/* Note: this isn't an assoc_update. The key must not already exist to call this */
int assoc_insert(item *it, const uint32_t hv) {
unsigned int oldbucket;

// assert(assoc_find(ITEM_key(it), it->nkey) == 0); /* shouldn't have duplicately named things defined */

if (expanding &&
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
it->h_next = old_hashtable[oldbucket];
old_hashtable[oldbucket] = it;
} else {
it->h_next = primary_hashtable[hv & hashmask(hashpower)];
primary_hashtable[hv & hashmask(hashpower)] = it;
}

pthread_mutex_lock(&hash_items_counter_lock);
hash_items++;
if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
assoc_start_expand();
}
pthread_mutex_unlock(&hash_items_counter_lock);

MEMCACHED_ASSOC_INSERT(ITEM_key(it), it->nkey, hash_items);
return 1;
}

 


[입개발] Cloud Native 와 Spring Cloud

$
0
0

Cloud Native 와 Spring Cloud

이 글은 Josh Long 의 Spring Cloud 강연을 들은 거의 후기와 평소에 제가 생각하는 것들을… 섞어서
제 마음대로 써볼려고 합니다. 일단 제가 이분야 꼬꼬마이니… 틀리시면 조용히 욕만… T.T 그리고 많은
지적질 감사하겠습니다.

참고: 관련 코드는 https://github.com/joshlong/cloud-native-workshop 에서 보실 수 있습니다.

먼저 고백하자면, 전 Java 도 잘 모르고, Spring은 더더욱 잘 모르고, Spring Cloud는 정말 세미나 두 번 들은게 다입니다.

아직 저도 잘 못읽어본…(솔직히 목차만 본…) Cloud Native Java 를 한번 보시는 것도 꽤 도움이 될듯합니다. 재미있는건 제가 Spring Cloud에 대해서 들었던 두 번이 각각 이 책의 저자인 Kenny 와 Josh 입니다.(영어도 잘하고, 심지어 기술도 좋은… 아, 얘들 미국인이지…)

그렇다면 먼저 Spring Cloud 야… 이번에 Spring 에서 미는 기술이고…(정확히는 컴포넌트의 연합?)
그 전에 Cloud Native는 무엇일까요?

한국어 해석은 매우 분분할 수 있는데… 클라우드에서 태어난, 클라우드향?, 클라우드에 적합한?
일단 이 개념을 전도하는(그 전도 맞습니다.) Pivotal 에서는 다음과 같이 정의하고 있습니다.

“Cloud Native describes the patterns of high performing organizations delivering software faster, consistently and reliably at scale. Continuous delivery, DevOps, and microservices label the why, how and what of the cloud natives.”

간단하게 해석하자면…(저 영어 못해요.) 소프트웨어를 빨리 개발하고 릴리즈하면서도, 확장성도 있고 안전성이 높게 만드는 패턴이라고 합니다.(이런 구라를… 그러나 현실에도 장동건처럼 잘생긴데, 돈도 잘벌고, 성격까지 좋은 사람들도…)

이런 기사도 있습니다. “http://www.informationweek.com/cloud/platform-as-a-service/cloud-native-what-it-means-why-it-matters/d/d-id/1321539&#8221;

실제 세미나도 Spring Boot를 소개하는 것으로 시작합니다. 큰 설정 없이 endpoint에 대한 핸들러만
설정해주고 구현해주는 것만으로 쉽게 비지니스 로직의 구현이 가능합니다.

예를 들어, @Get(“/profiles/me”) 이런 annotation을 이용하고 해당 핸들러를 구현하면… 바로
profiles/me 에 대한 url에 대한 처리가 가능해지는 겁니다. 그런데 Play나 뭐, 다른 쉬운 프레임워크
들을 사용해도 이런식의 개발은 쉽게 가능합니다.

일단 제가 Spring Boot 부터 잘 모르니… 자세한 부분은 넘기겠습니다.(그렇다면 내가 아는 것은 뭐지?)

이제 한번 서비스를 하나 만든다고 생각해보겠습니다. 서비스 포트는 8080번 이고, db 서버는 어디에 접속하고… 이런 정보들을 보통 설정 파일을 만들어서 넣어두고, 구동시 해당 값을 읽어서 서비스가 실행되게 됩니다. (그것도 아니면… 보통… 아예 고정으로 소스코드에 넣어둘수도…)

그런데 이러면… 사실… 각각의 서버들의 설정이 서로 다를 수 있습니다. 어디는 새로운 설정을 쓴다거나… 이럴 때의 좋은 방안은 설정 서버를 두는 것입니다. 그래서 서비스가 최초 구동시에 설정 서버에서 자신의 설정을 읽어서 이걸로 서비스를 시작하는 거죠. 이제 모든 서버들이 같은 설정으로 구동할 수 있습니다.

이런 걸 구축할려면… 사실… 여러가지를 알아야 합니다. 일반적으로 zookeeper 나, consul, etcd 같은 곳에 이런 설정을 저장하거나, DB나 다른 스토리지에 저장하기도 합니다. 그런데 이런걸 config-server 란 녀석이 해주는 겁니다.(세상 참 많이 좋아졌습니다.)

이제 설정은 config server에서 가져와서 실행이 되도록 했습니다. 그런데 실제로 서비스를 운영하다 보면… endpoint를 설정에 의해서 바꿔준다든지… 아니면 살짝 로직을 더 추가한다든지가 필요할 경우가 생기는…(두 개의 API를 묶는다든지..) 이렬 경우 뭔가 라우팅을 바꿔준다거나, 실제로 해당 API들을 통해서 뭔가 다른 작업을 할수 있는 API Gateway 를 만들어 주는 것이, 여러가지 방면에서 편리합니다. 여기서 Zuul 이라는 것을 이용해서 아주 쉽게 추가가 가능합니다.(API Gateway 는 사실 여러가지 녀석들이 있습니다.)

그런데 서비스를 운영하다보면, 당장 DB라든지, 아니면 다른 서드파티 서버들과 통신해야 하는 경우가 생깁니다. 예를 들어, 서비스가 분리되어 있는데, 계정 서비스를 호출해서 해당 사용자가 유효한 사용자인지를 체크한다고 합시다.  이럴 경우 일단 계정 서비스의 주소를 알아야 합니다. 서버 IP가 1.2.3.4 라고 하면, 우리는 1.2.3.4 서버의 주소를 가지고 여기로 리퀘스트를 보내게 됩니다.

그런데 이런 ip가 바뀌어야 한다면? 그렇다면 도메인을 쓰는 건 어떨가요? 도메인을 쓰면, 서버 주소가 바뀌더라도 도메인은 그대로이니… 이제 account.server.com 이라고 가정한다면, 이 도메인 주소를 이용하면서 좀 더 유연성이 증가했습니다. 그런데, 도메인의 경우는 클라이언트가 해당 주소를 바꿀 경우, 클라이언트가 해당 주소를 캐시하고 있을 수도 있습니다. 분산 시스템에서는 이런 문제를 위해서 Service Registery 라는 것을 정의하는데 여기서 Spring Cloud는 Eureka 라는 것을 제공합니다.

즉, 서비스가 뜰때, Eureka 에 자기는 A 서비스라고 등록합니다. 그리고 누군가 A 서비스의 목록을 주세요 라고 요청하면 Eureka 서버는 그 서버의 목록을 전달합니다. 심지어 서버가 죽으면 자동으로 해당 서버를 서비스 목록에서 제거합니다. 이런것을 보통 클러스터 멤버쉽이라고 부릅니다. (사실 꼭 이런게 아니더라도, IDC 라면 L4 라든지, VIP를 이용해 VVRP 를 이용한 ip 의 변경등을 할 수 있지만, cloud 환경에서는 이런것들이 쉽지만은 않습니다.)

거기다가 일반적으로 멤버쉽을 독립적으로 관리하면, 서버 목록 가져와서 거기서 라운드 로빈이든 랜덤하게든 선택한 다음 해당 주소로 접속하는 코드를 쫘야 하는데, feign 과 ribbon을 이용해서 해당 서비스에 좀더 동일한 방법으로 Eureka에 myservice 라는 이름으로 등록이 되어있으면, 호스트 이름이 account1.service.com 일때,
http://myservice/profiles/me&#8221; 라는 형태로 연결을 시도하면 내부적으로
http://account1.service.com/profiles/me&#8221; 로 알아서 도메인을 바꾸어줍니다.

그리고 CircuitBreaker 라는 개념을 손쉽게 적용할 수 있도록 해줍니다. hystrix 라는 것을 이용하는데, 예를 들어, 특정 서비스에서 A,B,C,D 라는 서비스의 결과를 가져와서 보여준다고 합니다. A,B,C,D가 독립적이라서 A,B,C는 잘 동작하는데 D서비스가 서비스 불능 상태에 빠져있다면… (Eureka 등에 등록된 서버가 하나도 없다면?)  로직에 따라서는 해당 서비스 때문에, 전체 서비스 결과가 안나올수도 있고, D 서비스의 결과가 Timeout 이 나야만 그제서야 결과가 보여질 수도 있습니다.(깨진 내용이나 등등등) circuitbreaker 를 이용하게 되면, 몇번 시도해서 해당 서비스가 장애라는 판단이 내부적으로 서게 되면, 미리 등록된 failback을 실행시켜줍니다. 그리고 백그라운드에서 해당 서비스가 살아나는지 체크해서 살아나게 되면, 원래 호출을 하도록 해주는 것이 CircuitBreaker 입니다.

사실 위에 설명한 Zuul, Eureka, Feign, Ribbon, Hystrix 등은 전부 Netflix에서 이미 오래전 부터
공개했던 컴포넌트들입니다. 그런데 Spring Cloud의 대단한 점은 이런것들을 아주 쓰기 쉽게 Integration 시켰다는 점입니다. 원래는 개별적으로 사용하기에 조금 어려운 것들을 간단한 설정이나 annotation 만으로 아주 손쉽게 사용할 수 있게 만들었습니다.

결론적으로 Spring Cloud는 여러가지 분산 시스템 개발에 필요한 개념들을 아주 사용하기 쉽게 만들었습니다. 사실 이런 개념들을 알고 적용하는가가… 대규모 서비스에서 개발할 때 필요하는 노하우이기도합니다.(클라우드 나올때도 이제 뭐먹고 살지 고민했는데, Spring Cloud 보니… 이제 나는 굶어죽겠다는 생각만…)

다만, 아주 쉽게 사용하도록 해두었지만… 사실 이걸 제대로 운영할 수준으로 적용하는 것은 쉬운 일이 아닙니다. 왜왜왜? 라고 물어보신다면, 결국 서비스라는 것은 운영에 대한 이슈가 생기기 때문입니다. 그래서 반대로 이 Cloud Native라는 형태가 cloud service 와 결합하면서, 상당히 큰 강점이 생기기도 합니다.(뭔가 모순적이지만… 이게 사실인…)

다시 정리하자면, Spring Cloud는 Cloud Native라는 개념을 효과적으로 풀어낸 구현체입니다. 굉장히 쉽게 만들어둔… 다만… Spring Cloud에서 사용하는 컴포넌트들의 의미를 잘 파악하면, Cloud Native라는 개념을 다른 언어, 다른 컴포넌트로도 풀어낼 수 있습니다.(그러면 저 같은 입개발자는 설 곳이 없어지는…)

클라우드라는 개념이 나왔을때도, 충격을 받았지만…(SE분들이랑 우린 이제 뭐 먹고 사나 했었는데…) Spring Cloud가 나온걸 보니… 또 한번… 굶어죽을 듯한 두려움과, 세상이 또 뭔가 재밌게 변하겠구나라는 생각이 듭니다.

 

 



[입 개발] spring-cloud-config-server 이야기…

$
0
0

spring cloud 에서 가장 중요한 서버를 뽑으라면, 일단 Eureka 서버를 뽑을 수 있습니다.(아니면, concul 이나, etcd 등등등) 그런데 이 Eureka를 잘 쓰기 위해서 먼저 설정해야 할 서버가 있습니다.(사실 Eureka 는 구조를 설명할 능력이 안되므로…)

twelve factors application 개발이라는 어려운 용어를 쓰는 것에 보면, 실제 코드와 설정을 분리하라는 말이 나옵니다. 한글로 번역된 좋은 페이지가 http://12factor.net/ko/ 를 보시면 됩니다.

이런 역할이 지금 얘기할 config server 입니다. 간단하게 설명하면, 다른 서버들의 설정을 제공하는 서비스입니다. 예를 들어, 어떤 API Server 가 있다면, 설정에 자신의 서비스 이름과 config server 주소만 적어주면 됩니다. 그리고 config server 에 해당 API Server에 사용될 port 정보라든지 여러가지 설정들이 들어가게 됩니다. 보통 시작 Port는 설정 파일이나, 코드에 박혀있는 경우도 흔한데, 이럴 경우 쉽게 설정을 변경하기가 어렵습니다.

즉 config server는 바뀌기 쉬운 설정을 외부에서 받아와서 각각의 서비스를 실행하게 하자가 핵심인 겁니다. 그래서 Eureka 서버라든지, API Gateway 라든지, 모든 서비스가 config server에 접근하여 해당 설정을 가져가게 됩니다.

그래서 spring cloud 를 쓸 때 가장 먼저 고민해야 할 것들이… config server 와 Eureka의 HA나 clustering 입니다. 전에 Eureka를 통해서 클러스터 멤버쉽을 관리해주는게 어떻게 보면 spring cloud의 핵심이라고 얘기했었는데, 당장 config server 와 Eureka 가 죽어버리면… 서비스에 문제가 생기게 됩니다.(정확히는 config server나 Eureka 가 죽었을 경우 기존 설정 값을 이용하게 되지만, 신규 장비가 추가되거나 빠져야 할때 이슈가 발생하게 됩니다.)

spring_cloud_01

실제로 Config Service 의 경우는 Stateless 한 서버라, 여러 개를 띄우고, L4든… 클라이언트에서 여러 대 중에 DNS RR이든 알아서 시도하게 하면… 큰 문제가 없습니다.(즉, 기존에  옛날부터 사용되던 HA 방법이 여기에 그대로 적용될 수 있습니다.)

Spring Cloud Config Server의 동작은 실제 코드의 경우는 이것보다 훨씬 복잡하지만,  단순하게 생각하면 아래와 같은 부분이 다입니다. 여기에 Rest Request Handler만 추가되어 있는… 즉 특정 설정을 읽어서 거기 내용을 읽어온 다음, 내부적으로 관리하는 겁니다.

import java.io.*;
import java.net.*;

public class Test {
public static void main(String [] args) {
try {
URL url = new URL("https://raw.githubusercontent.com/charsyam/springboot-config-server-config/master/reservation-service.properties");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));

String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(inputLine);
}
in.close();
} catch (Exception e) {
}
}
}

Eureka 서버의 경우에는 Eureka Replication을 만들 수 있습니다. 그리고, 여러 대 Eureka 서버의 주소를 적어두면, 알아서, 접속을 시도하게 됩니다. Eureka의 경우는 여러 IDC에 존재할 수도 있으므로, 자신이 속해있는 IDC의 Eureka 서버로 우선 접근하게 하는 그런 옵션도 제공합니다.

이런 분산 시스템을 만들때는… SPOF(Single Point of Failure) 를 없애는게 중요합니다. 서비스 자체는 잘 만들었는데,  이런 부분을 놓치는 경우가 많기 때문에 간단하게 정리했습니다.(이제 한동안 Spring Cloud는 패스를…)

 


[책 리뷰] 알고리즘 문제 풀이 전략

$
0
0

해당 리뷰는 “한빛미디어”에서 제공하는 도서를 이용하여 리뷰를 진행하였습니다.

이 책의 제목은 미사여구를 포함해서 “프로그래머의 취업, 이직을 결정하는 알고리즘 문제 풀이 전략” 입니다.  솔직히 말해서, 국내는 알고리즘 문제를 이용해서 기술 면접이 이루어지는 곳이 엄청 많지는 않습니다. 그리고, 약간 면접관에 의해서 케이스 바이 케이스인 경우도 많구요. 다만, 이제 점점 전화면접이나 온라인 홈워크나 문제 풀이를 이용해서 지원자를 스크리닝 하는 경우는 점점 늘어나고 있습니다. 다만, 외국의 특정 G사, F사 처럼 문제 풀이만 시키는 형태가 되기에는 아직은 시간이 걸릴듯 합니다.

아픈 기억이지만, 저도 실제로 알고리즘 문제만 푸는 면접을 2012년에 본 적이 있습니다.  이렇게 아픈 기억이라고 얘기하는 건, 똑 떨어졌다라는 이야기입니다. 5시간 동안 알고리즘만 풀면… 머리가 안돌아가는…

다시 책 이야기로 돌아와서, 이 책은 크게 두 부분으로 구성되어 있습니다. 기본적인 자료구조/알고리즘 부분…(C코드로 되어있는…) 나머지는 실제로 문제와 그에 대한 해답입니다. 개인적으로 이 책의 최대 장점은 문제 부분보다는, 앞쪽에 잘 정리된 자료구조/알고리즘 부분입니다. 이미지를 통해서 실제로 단계별로 어떻게 이렇게 진행되는지에 대해서 자세히 설명되어 있습니다.(간단한거는 그냥 쉽게 훅 넘기기도 합니다. ㅎㅎㅎ)

보통 알고리즘 문제를 내는 곳도 엄청 어려운 문제를 내지는 않습니다. 그리고 정답을 맞추는 것보다 정답을 맞추어가는 과정을 중요시합니다. 그래서 실제 이런 문제를 풀 때는, 자신이 생각하는 과정과 풀이 과정을 계속 면접관과 맞추어가는 것이 중요합니다.

그래서 어떤 자료구조를 선택하고 알고리즘을 선택할 것인가에 대해서 기본기가 튼튼해야 하는데, 이런 부분이 잘 갖춰져 있다는 겁니다. 그리고 문제를 다 풀어보는 것도 꽤나 도움이 될듯합니다. 실제로 같은 문제가 나오지는 않겠지만…(craking code interview 라는 책이 있는데,  인사이트에서 “코딩 인터뷰 완전분석” 이라고 번역되어 나왔습니다. 외국에서 이 책은 거의 취업쪽 바이블로 통하는…) 문제를 풀어보고 연습해 둔다는 것이 상당히 중요합니다. 실제 외국에서도 이직을 할려면 이런 쪽 문제랑 공부를 대략 6개월 정도 합니다.

사실 제가 약한 부분이던, 전위/중위/후위 순회라든지, 정렬쪽이 상당히 설명이 잘 되어있어서 좋았습니다. 여러 책을 보시겠지만, 앞에서 언급한 책과 함께 보시면 좋을듯 합니다.

 


[입 개발] Memcached 에서 incr/decr 은 음수에 대해서는 사용할 수 없습니다.

$
0
0

오늘 우리 팀의 조실장님이 Memcached 관련해서 에러가 난다고 보고를 해주셨습니다. 실제 분석까지 대충 다 끝낸… 조실장 화이팅!!!

그래서 과연 그런가 싶어서 memcached 소스를 먼저 열었습니다. 일단 증상은 다음과 같습니다. memcached 에 값을 음수로 설정하고  incr/decr 을 하면 에러가 발생한다.  일단 조건들은 다음과 같습니다.

  • client library 는 spymemcached

 

enum delta_result_type do_add_delta(conn *c, const char *key, const size_t nkey,
                                    const bool incr, const int64_t delta,
                                    char *buf, uint64_t *cas,
                                    const uint32_t hv) {
    char *ptr;
    uint64_t value;
    int res;
    item *it;

    it = do_item_get(key, nkey, hv);
    if (!it) {
        return DELTA_ITEM_NOT_FOUND;
    }

    /* Can't delta zero byte values. 2-byte are the "\r\n" */
    if (it->nbytes <= 2) {
        return NON_NUMERIC;
    }

    if (cas != NULL && *cas != 0 && ITEM_get_cas(it) != *cas) {
        do_item_remove(it);
        return DELTA_ITEM_CAS_MISMATCH;
    }

    ptr = ITEM_data(it);

    if (!safe_strtoull(ptr, &value)) {
        do_item_remove(it);
        return NON_NUMERIC;
    }

    if (incr) {
        value += delta;
        MEMCACHED_COMMAND_INCR(c->sfd, ITEM_key(it), it->nkey, value);
    } else {
        if(delta > value) {
            value = 0;
        } else {
            value -= delta;
        }
        MEMCACHED_COMMAND_DECR(c->sfd, ITEM_key(it), it->nkey, value);
    }

    pthread_mutex_lock(&c->thread->stats.mutex);
    if (incr) {
        c->thread->stats.slab_stats[ITEM_clsid(it)].incr_hits++;
    } else {
        c->thread->stats.slab_stats[ITEM_clsid(it)].decr_hits++;
    }
    pthread_mutex_unlock(&c->thread->stats.mutex);

    snprintf(buf, INCR_MAX_STORAGE_LEN, "%llu", (unsigned long long)value);
    res = strlen(buf);
    /* refcount == 2 means we are the only ones holding the item, and it is
     * linked. We hold the item's lock in this function, so refcount cannot
     * increase. */

    if (res + 2 <= it->nbytes && it->refcount == 2) { /* replace in-place */
        /* When changing the value without replacing the item, we
           need to update the CAS on the existing item. */
        ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0);

        memcpy(ITEM_data(it), buf, res);
        memset(ITEM_data(it) + res, ' ', it->nbytes - res - 2);
        do_item_update(it);
    } else if (it->refcount > 1) {
        item *new_it;
        new_it = do_item_alloc(ITEM_key(it), it->nkey, atoi(ITEM_suffix(it) + 1), it->exptime, res + 2, hv);
        if (new_it == 0) {
            do_item_remove(it);
            return EOM;
        }
        memcpy(ITEM_data(new_it), buf, res);
        memcpy(ITEM_data(new_it) + res, "\r\n", 2);
        item_replace(it, new_it, hv);
        // Overwrite the older item's CAS with our new CAS since we're
        // returning the CAS of the old item below.
        ITEM_set_cas(it, (settings.use_cas) ? ITEM_get_cas(new_it) : 0);
        do_item_remove(new_it);       /* release our reference */
    } else {
        /* Should never get here. This means we somehow fetched an unlinked
         * item. TODO: Add a counter? */
        if (settings.verbose) {
            fprintf(stderr, "Tried to do incr/decr on invalid item\n");
        }
        if (it->refcount == 1)
            do_item_remove(it);
        return DELTA_ITEM_NOT_FOUND;
    }

    if (cas) {
        *cas = ITEM_get_cas(it);    /* swap the incoming CAS value */
    }
    do_item_remove(it);         /* release our reference */
    return OK;
}

NON_NUMERIC 에러를 리턴하는 경우는 코드에서 2가지 입니다.

  • 기존 아이템이 2글자 이하일 경우
  • safe_strtoull 결과가 false 일 때

 

이제 다시 safe_strtoull 함수를 살펴보면 다음과 같습니다. 처음에 데이터를 unsigned long long 으로 받으므로 해당 값을 singed long long 으로 바꾸고… 이게 0보다 적으면(즉 overflow 상황이면) 실제로 – 로 시작하는지 확인합니다. 그렇습니다. 이건 데이터가 문자열로 들어가 있다는 소리!!! 하여튼 왜 그런지는 모르겠지만 memcached 에서는 음수로 셋팅한 값을 incr/decr 하면 안됩니다.

bool safe_strtoull(const char *str, uint64_t *out) {
    assert(out != NULL);
    errno = 0;
    *out = 0;
    char *endptr;
    unsigned long long ull = strtoull(str, &endptr, 10);
    if ((errno == ERANGE) || (str == endptr)) {
        return false;
    }

    if (xisspace(*endptr) || (*endptr == '\0' && endptr != str)) {
        if ((long long) ull < 0) {
            /* only check for negative signs in the uncommon case when
             * the unsigned number is so big that it's negative as a
             * signed number. */
            if (strchr(str, '-') != NULL) {
                return false;
            }
        }
        *out = ull;
        return true;
    }
    return false;
}

 

재미난건 ascii 에서는 CLIENT_ERROR 에 메시지로 에러 값이, binary 에서는 PROTOCOL_BINARY_RESPONSE_DELTA_BADVAL 이라는 0x06 값이 전달됩니다.

반면에 Redis는 그런 구분 없이 음수형태도 그대로 저장됩니다.

void incrDecrCommand(client *c, long long incr) {
    long long value, oldvalue;
    robj *o, *new;

    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o != NULL && checkType(c,o,OBJ_STRING)) return;
    if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;

    oldvalue = value;
    if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
        (incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
        addReplyError(c,"increment or decrement would overflow");
        return;
    }
    value += incr;

    if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT &&
        (value < 0 || value >= OBJ_SHARED_INTEGERS) &&
        value >= LONG_MIN && value <= LONG_MAX)
    {
        new = o;
        o->ptr = (void*)((long)value);
    } else {
        new = createStringObjectFromLongLong(value);
        if (o) {
            dbOverwrite(c->db,c->argv[1],new);
        } else {
            dbAdd(c->db,c->argv[1],new);
        }
    }
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
    server.dirty++;
    addReply(c,shared.colon);
    addReply(c,new);
    addReply(c,shared.crlf);
}

[입 개발] Redis 접속이 안되요!!! – Protected Mode

$
0
0

최근에 자주 다음 질문을 받는 케이스가 늘어서 정리합니다.(사실 저도 당한…)
최신 버전 3.2.x 을 설치하고 클라이언트를 접속하고 보면… 접속이 안됩니다.

일단 client 로 접속을 하게 되면 다음과 같은 에러를 볼 수 있습니다.
(항상이 아니라 흔히 볼 수 있습니다.)

“-DENIED Redis is running in protected mode because protected ”
“mode is enabled, no bind address was specified, no ”
“authentication password is requested to clients. In this mode ”
“connections are only accepted from the loopback interface. ”
“If you want to connect from external computers to Redis you ”
“may adopt one of the following solutions: ”
“1) Just disable protected mode sending the command ”
“‘CONFIG SET protected-mode no’ from the loopback interface ”
“by connecting to Redis from the same host the server is ”
“running, however MAKE SURE Redis is not publicly accessible ”
“from internet if you do so. Use CONFIG REWRITE to make this ”
“change permanent. ”
“2) Alternatively you can just disable the protected mode by ”
“editing the Redis configuration file, and setting the protected ”
“mode option to ‘no’, and then restarting the server. ”
“3) If you started the server manually just for testing, restart ”
“it with the ‘–protected-mode no’ option. ”
“4) Setup a bind address or an authentication password. ”
“NOTE: You only need to do one of the above things in order for ”
“the server to start accepting connections from the outside.\r\n”;

이유가 무엇인고 하면 3.2.x 부터 Redis 에 Protected mode 라는 것이 생겼습니다. 이 Protected Mode라는 것은 또 무엇인고 하니, 혹시 예전의 보안 사고를 기억하시나요? Redis 는 굉장히 보안에 취약합니다. 특히 public 으로 열어두면 거의 해킹의 온상이 되는… 방법은 그렇게 공개하지는 않겠습니다.

그래서 추가된 것이 이 protected mode 입니다. protected mode 가 설정되어 있는 상태에서 패스워드가 설정되지 않고, 특정 ip로 bind가 되어있지 않으면, connection 자체가 위의 에러를 내면서 실패하게 됩니다.

그런데 이런 문의가 급증하는 것은 이 protected mode 가 default yes 이고 보통 특정 ip로 bind 시키지 않고 requirepass 를 지정하지 않습니다. 보통은 내부망에서만 쓰라는 얘기가 되는거죠. 그래서 이걸 해결 하기 위해서는 다음 명령을 127.0.0.1 즉 local loopback 주소에서 접속한 다음 날려야 합니다.

config set protected-mode no

 

실제 코드를 보면 다음 부분에서 문제가 되는겁니다. src/networking.c 에 있습니다.

    if (server.protected_mode &&
        server.bindaddr_count == 0 &&
        server.requirepass == NULL &&
        !(flags & CLIENT_UNIX_SOCKET) &&
        ip != NULL)
    {
        if (strcmp(ip,"127.0.0.1") && strcmp(ip,"::1")) {
            char *err =
                "-DENIED Redis is running in protected mode because protected "
                "mode is enabled, no bind address was specified, no "
                "authentication password is requested to clients. In this mode "
                "connections are only accepted from the loopback interface. "
                "If you want to connect from external computers to Redis you "
                "may adopt one of the following solutions: "
                "1) Just disable protected mode sending the command "
                "'CONFIG SET protected-mode no' from the loopback interface "
                "by connecting to Redis from the same host the server is "
                "running, however MAKE SURE Redis is not publicly accessible "
                "from internet if you do so. Use CONFIG REWRITE to make this "
                "change permanent. "
                "2) Alternatively you can just disable the protected mode by "
                "editing the Redis configuration file, and setting the protected "
                "mode option to 'no', and then restarting the server. "
                "3) If you started the server manually just for testing, restart "
                "it with the '--protected-mode no' option. "
                "4) Setup a bind address or an authentication password. "
                "NOTE: You only need to do one of the above things in order for "
                "the server to start accepting connections from the outside.\r\n";
            if (write(c->fd,err,strlen(err)) == -1) {
                /* Nothing to do, Just to avoid the warning... */
            }
            server.stat_rejected_conn++;
            freeClient(c);
            return;
        }
    }

 

점점 보안이 중요해지네요. 제 acl 패치는 언제 받아들여질지 T.T


[입 개발] 왜 Cache를 사용하는가?

$
0
0

가끔씩 Redis 가 뭐예요? Memcached 가 뭐예요? 또는 Cache를 왜 써요? 라는 저도 모르는 근원적인 질문을 받을 때가 있습니다.

일단 그 근원적인 질문에 답하기 위해서는 먼저 Cache 란 무엇인가로 부터 시작해야 될것 같습니다. 일단 Cache는 “많은 시간이나 연산이 필요한 일에 대한 결과를 저장해 두는 것” 이라고 할 수 있습니다. 우리가 1 부터 100까지 결과를 더하는 것은 5050 이라고 아주 쉽게 계산할 수 도 있지만…(가우스 천재 녀석…) 보통은 1 부터 100 까지 종에 적어서 결과를 구하는 아주 어려운 방식을 택하게 됩니다. 마치 아래의 경우를 수를 찾는 것 처럼요.su2

실제로 저는 1부터 100 더하기가 파스칼인줄 알았는데, 검색해보니 가우스였습니다. 그런데 1부터 100까지 결과를 계산할 수도 있고, 저처럼 외워둘 수도 있습니다. 가우스라는 걸 찾기 위해서 검색을 해야 하지만, 머릿속에 기억을 해두면, 검색하는 시간을 줄일 수 있습니다. 즉, 결과를 아주 빨리 찾을 수 있습니다.

cache

많이들 아시겠지만, 위의 그림을 보면 CPU 에서는 Register가 가장 속도가 빠르고, 그 뒤로 L1 , L2, L3, Memory, Disk 순으로 접근 속도는 느려지고, 용량은 커지고, 비용도 싸집니다. 인터넷 서비스를 생각해 보도록 하죠. 특정 서비스에 로그인 한다고 하면, 유저 정보를 디스크에서 가지고 오면 가장 느릴 것이고, Memory 에 있으면 훨씬 빠를 겁니다. 특히 데이터가 Disk 블럭 하나에 있다면, 하나의 블럭만 읽으면 되지만, 여러 군데 나눠져 있다면… 그 만큼 더 느려지게 되겠죠.

결국 Cache는 빠른 속도를 위해서 사용하게 되는겁니다. 그런데 이런 의문이 생깁니다. 위의 그림을 봐도 점점 용량은 줄어가는데, 모든 데이터를 빠르게 저장할 수 없지 않느냐? 라는 질문이 생기는 거죠.

물론 용량도 충분히 늘릴 수 있습니다. 다만 돈이 많이 들 뿐이죠. 아직 같은 용량일 때 HDD 보다는 SSD 가 더 비싸고, SSD 보다는 메모리가 더 비싸죠. 그래서 “Cache is Cash” 라는 명언도 있습니다. 그러면 Cache 가 과연 유용한가? 라는 질문이 생깁니다. 비용이 너무 비싸기 때문이죠. 그런데… 역시 세상은 파레토의 법칙에 의해서 돌아갑니다. 전체의 80%의 요청이나 부하가 상위 20% 유저로 인해서 발생하는… 다시 그 20%의 80%도 그 안의 20%에 의해서 발생합니다. 그래서, 자주 접근하는 정보만 Cache 하더라도 엄청나게 좋은 효과를 볼 수 있습니다. 다만, 대부분이 생성, 수정, 삭제라면, Cache를 좀 다른 방법으로 이용해야 합니다.(write-back을 찾아보세요.)

위의 Login 사례를 다시 한번 살펴 보도록 하겠습니다. 실제 유저가 id, passwd를 입력하면 다음과 같은 sql 문이 실행된다고 하겠습니다.

select * from Users where id='charsyam';

위의 쿼리가 실행되는 데는 다음과 같은 시간이 듭니다.

  1. 쿼리를 파싱
  2. id 가 charsyam 인 데이터를 인덱스에 찾아서 전달, 이를 위해서 Disk 읽기 발생

물론, 데이터베이스도 알아서 캐싱을 하고 있습니다. 다만, 데이터량이 엄청 많으면, 그 디스크에 대한 캐시 확률이 더 떨어지게 되죠.

그런데 Cache를 사용하게 되면, 보통 Key-Value 타입을 사용하게 되면, 쿼리를 파싱하는 시간도 없어지고, 훨씬 접근 속도가 빠른 메모리에서 읽어오게 되기 때문에, 거기서 많은 속도 차이가 나게 됩니다. 다음은 DB 서버의 CPU 사용량입니다. Cache를 적용한 이후부터, 전체적으로 Wait I/O가 많이 떨어지는 것을 볼 수 있습니다.

cache_cpu

그리고 두 번째로 DB로 들어오는 쿼리 수입니다.

cache_query

실제로 Update는 계속 들어와야 하는 작업이므로 변화가 없지만, Select와 Qcache_hits 는 거의 1/10 수준으로 줄어버립니다. DB서버로의 요청이 줄어버려서, 전체 서비스가 좀 더 큰 요청이 들어와도 버틸 수 있게 해줍니다.

그럼 Redis 나 Memcached 같은 Cache 서비스를 사용하는 것은 왜 일까요? 당연히 속도면에서는 각각의 서버의 메모리에 들고 있는 것이 유리합니다. 그런데, 여러 서버에 있는 데이터를 동기화 하는 것은 사실 쉬운 일이 아닙니다. 그리고 데이터량이 많으면, 결국은 한 서버에 둘 수 없어서, 여러 서버로 나뉘어야 합니다.

잘 생각해보면, 서비스의 로직 서버와 DB 서버 역시 별도록 분리되어 있습니다. 각 서버에 DB 서버를 모두 올릴 수도 있는데, 이러면, 각 DB 서버의 동기화가 필요해지겠죠.(물론, 이런식으로 하는 서비스 구조도 있습니다. 주로 읽기만 많은 서비스이며, 그 데이터의 동기화가 덜 중요할 경우…)

그래서 결국 Redis와 Memcached 를 쓰는 것은 위의 여러 가지 장점을 취하기 위해서입니다. 물론 데이터 량이 늘어나면, 이 서버들도 여러 대가 필요해지고, 이를 위해 데이터를 어떻게 찾을 지에 대한 룰도 추가로 필요해지게 됩니다.

이런 류의 좀 더 자세한 내용이 알고 싶으시다면, 제 slideshare를 참고하시면 아주 초급적인 자료들이 있는데, 대표적으로 다음 자료를 추천합니다.

 

 


Viewing all 122 articles
Browse latest View live