서버를 만드실때는 포트를 32768 이전으로 설정하세요.

최근에 아주 재미난(?) 일을 격었습니다. 서버를 시작시키는데, 아무리해도 서버가 뜨지 않는 것이었죠.
코드를 봐서는 아무런 문제가 없는데… 에러 로그는

“binding error: port is already used” 비슷한 오류가 발생하는 것이었습니다.

netstat 으로 LISTEN 포트만 찾아봐서는… 제가 사용하는 포트를 찾을 수도 없었죠.

그런데 netstat으로 포트를 더 찾아보니… 이상한 결과를 볼 수 있었습니다.

tcp        0      0 192.168.1.4:48121          192.168.1.4:6379         ESTABLISHED 15598/redis-server

여기서 제 서버에서 사용하고자 하던 포트를 48121 이라고 가정합니다. -_-(잉, 뭔가 가정이 이상하다구요. 그렇죠, 저도 뭔가 많이 이상합니다. 그러나 그것이 현실로 일어났습니다.)

그리고 다시 한번 제목을 보시죠.. 일단 포트에 대해서 잘 모르는 지식을 잠시 정리하자면 다음과 같습니다.

  • socket 이 바인딩하게 되는 포트는 udp/tcp 는 별개다. 즉 udp:53, tcp:53번 모두 바인딩 가능합니다. 실제로 DNS가 일반적으로 이렇게 하고 있죠.
  • socket 이 바인딩하게 되는 주소는 ip + port 다 즉 1.2.3.4 를 가지고 있는 서버는 127.0.0.1:48121 과 1.2.3.4:48121 를 따로 바인딩할 수 있다.

즉 다음과 같은 예제를 보면 에러가 발생해야 합니다.

# -*- coding: utf-8 -*-.

from flask import Flask, request, redirect, url_for, jsonify

app = Flask(__name__,static_folder='static', static_url_path='')

@app.route('/health_check.html')
def health():
    return "OK"

if __name__ == '__main__':
    app.run(host="127.0.0.1", port=20000)

두 개를 실행시키면 다음과 같이 오류가 발생합니다.

 * Running on http://127.0.0.1:20000/
Traceback (most recent call last):
  File "2.py", line 23, in <module>
    app.run(host="127.0.0.1", port=20000)
  File "/Users/charsyam/anaconda/lib/python2.7/site-packages/flask/app.py", line 772, in run
    run_simple(host, port, self, **options)
  File "/Users/charsyam/anaconda/lib/python2.7/site-packages/werkzeug/serving.py", line 710, in run_simple
    inner()
  File "/Users/charsyam/anaconda/lib/python2.7/site-packages/werkzeug/serving.py", line 692, in inner
    passthrough_errors, ssl_context).serve_forever()
  File "/Users/charsyam/anaconda/lib/python2.7/site-packages/werkzeug/serving.py", line 486, in make_server
    passthrough_errors, ssl_context)
  File "/Users/charsyam/anaconda/lib/python2.7/site-packages/werkzeug/serving.py", line 410, in __init__
    HTTPServer.__init__(self, (host, int(port)), handler)
  File "/Users/charsyam/anaconda/lib/python2.7/SocketServer.py", line 419, in __init__
    self.server_bind()
  File "/Users/charsyam/anaconda/lib/python2.7/BaseHTTPServer.py", line 108, in server_bind
    SocketServer.TCPServer.server_bind(self)
  File "/Users/charsyam/anaconda/lib/python2.7/SocketServer.py", line 430, in server_bind
    self.socket.bind(self.server_address)
  File "/Users/charsyam/anaconda/lib/python2.7/socket.py", line 224, in meth
    return getattr(self._sock,name)(*args)
socket.error: [Errno 48] Address already in use

이제 강제로 아이피를 설정해보겠습니다.

127.0.0.1, 192.168.1.4, 0.0.0.0 으로 세 개의 ip로 실행을 시켰습니다. 모두 20000 번으로 실행했습니다.

charsyam ~/kakao_home_affiliate (master*) $ python 1.py
 * Running on http://127.0.0.1:20000/

charsyam ~/kakao_home_affiliate (master*) $ python 2.py
 * Running on http://0.0.0.0:20000/

charsyam ~/kakao_home_affiliate (master*) $ python 3.py
 * Running on http://192.168.1.4:20000/

charsyam ~ $ telnet 0 20000 <-- 이건 실제로 local loopback으로 전달되므로 127.0.0.1 과 동일합니다.
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
GET /health_check.html HTTP/1.1

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 2
Server: Werkzeug/0.9.4 Python/2.7.5
Date: Mon, 14 Apr 2014 14:48:43 GMT

OK

charsyam ~ $ telnet 127.0.0.1 20000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /health_check.html HTTP/1.1

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 2
Server: Werkzeug/0.9.4 Python/2.7.5
Date: Mon, 14 Apr 2014 14:48:53 GMT

OK

charsyam ~ $ telnet 192.168.1.4 20000
Trying 192.168.1.4...
Connected to 192.168.1.4.
Escape character is '^]'.
GET /health_check.html HTTP/1.1

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 2
Server: Werkzeug/0.9.4 Python/2.7.5
Date: Mon, 14 Apr 2014 14:49:08 GMT

OK

모두 동작하는 걸 볼 수 있습니다.

charsyam ~/kakao_home_affiliate (master*) $ python 1.py
 * Running on http://127.0.0.1:20000/
127.0.0.1 - - [14/Apr/2014 23:48:43] "GET /health_check.html HTTP/1.1" 200 -
127.0.0.1 - - [14/Apr/2014 23:48:53] "GET /health_check.html HTTP/1.1" 200 -

charsyam ~/kakao_home_affiliate (master*) $ python 2.py
 * Running on http://0.0.0.0:20000/

charsyam ~/kakao_home_affiliate (master*) $ python 3.py
 * Running on http://192.168.1.4:20000/
192.168.1.4 - - [14/Apr/2014 23:49:08] "GET /health_check.html HTTP/1.1" 200 -

실제로 0.0.0.0 이 INADDR_ANY이지만 127.0.0.1 이나 192.168.1.4로 명시하면, 각각의 주소가 명시되므로 거기로 전달되게 됩니다.
(이러면… port 를 탈취하는 것도 가능할것 같네요. 실제로 테스트해보면 192.168.1.4를 kill 하면 그 다음부터는 0.0.0.0 으로 전달이 되네요.)

자자… 그러면… 이제 무슨 일이 있었던 걸까요?

정리하자면… 실제 서버가 Redis 서버에 연결되면서 하필이면 192.168.1.4:48121를 만들어서 실제 192.168.1.4:6379 에 붙어버린겁니다. 그러면서 서버는 48121포트를 할당하려고 하니 이미 bind 에러가 발생한것이죠.(흑흑흑, 정말 재수가 없었던겁니다.)

그럼… 이를 해결하는 방법이 있을까요? 뭔가 할당되는 정책이 있을까요? 이것을… 사실 local port 라고 합니다. 외부로 나가기 위해서 만들어지는 port 이죠.

이 값은 그럼 어떻게 설정될까요? 기본적으로 shell 에서 sysctl -A 를 해보면 거기서 다음과 같은 두 개의 값을 발견할 수 있습니다.

net.ipv4.ip_local_port_range = 32768	61000
net.ipv4.ip_local_reserved_ports =

ip_local_port_range 를 보면 기본적으로 저 값 안에서 32768에서 61000번 사이에서 할당이 하게 됩니다. 즉, 외부에서 접속하는 소켓의 포트가 위의 범위에서 할당되므로, 이 값을 피하는게 좋습니다. 위의 32768, 61000이 기본으로 설정되는 범위입니다. 즉 다음과 같은 port는 서버 설정시에 피하시는게 좋습니다.

그럼 실제로 커널 코드를 까보면 다음과 같습니다. 코드는 지루할테니 설렁설렁 보겠습니다. linux kernel 3.14를 기본으로 합니다.
먼저 해당 범위 값을 가져오는 함수는 inet_get_local_port_range 함수입니다.

void inet_get_local_port_range(struct net *net, int *low, int *high)
{
  unsigned int seq;

  do {
    seq = read_seqbegin(&net->ipv4.sysctl_local_ports.lock);

    *low = net->ipv4.sysctl_local_ports.range[0];
    *high = net->ipv4.sysctl_local_ports.range[1];
  } while (read_seqretry(&net->ipv4.sysctl_local_ports.lock, seq));
}

그리고 이 함수를 inet_csk_get_port 에서 호출합니다. 이 코드에서 실제로 local_port 가 결정납니다.

    do {
      if (inet_is_reserved_local_port(rover))
        goto next_nolock;
      head = &hashinfo->bhash[inet_bhashfn(net, rover,
          hashinfo->bhash_size)];
      spin_lock(&head->lock);
      inet_bind_bucket_for_each(tb, &head->chain)
        if (net_eq(ib_net(tb), net) && tb->port == rover) {
          if (((tb->fastreuse > 0 &&
                sk->sk_reuse &&
                sk->sk_state != TCP_LISTEN) ||
               (tb->fastreuseport > 0 &&
                sk->sk_reuseport &&
                uid_eq(tb->fastuid, uid))) &&
              (tb->num_owners < smallest_size || smallest_size == -1)) {
            smallest_size = tb->num_owners;
            smallest_rover = rover;
            if (atomic_read(&hashinfo->bsockets) > (high - low) + 1 &&
                !inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
              snum = smallest_rover;
              goto tb_found;
            }
          }
         if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
            snum = rover;
            goto tb_found;
          }
          goto next;
        }
      break;
    next:
      spin_unlock(&head->lock);
    next_nolock:
      if (++rover > high)
        rover = low;
    } while (--remaining > 0);

뭔가 코드는 복잡하지만 inet_is_reserved_local_port 를 이용해서 예약된 local_port 는 피하는 것을 알 수 있습니다.
(udp는 또 udp_lib_get_port 에서 결정이 됩니다.)

물론 여기서 좀 더 깊게는 더 커널을 살펴봐야 겠지만… 대략 위의 형태로 동작한다는 것을 알 수 있습니다. 일단 이번 글은 여기서 끝!!!

[입 개발] Redis 가 멀티스레드라구요?

제가 항상 강조하는 것중에 Redis는 멀티스레드가 아니고 싱글 스레드이기 때문에 항상 사용에 주의해야 한다고 말을 드렸는데… 뭔가 깽기는게 있어서 ps -eLf 를 해보겟습니다.

charsyam@charsyam-vm-main:~/redis$ ps -eLf | grep "redis"
charsyam 31860  2920 31860 10    3 22:58 pts/0    00:00:05 src/redis-server *:6379
charsyam 31860  2920 31861  0    3 22:58 pts/0    00:00:00 src/redis-server *:6379
charsyam 31860  2920 31862  0    3 22:58 pts/0    00:00:00 src/redis-server *:6379

헉… 무려 스레드가 3개가 떠 있습니다. 자 바로 주먹에 돌을 쥐시면서, 이 구라쟁이야 하시는 분들이 보이시는 군요. (퍽퍽퍽!!!)

자… 저는 분명히 맨날 싱글 스레드 싱글 스레드라고 외쳤는데… Redis는 무려 멀티 스레드 어플리케이션이었던 것입니다!!!

그러면… 이 스레드들을 늘리면… 엄청난 성능 향상이 있을까요?

힌트를 드리자면, 이 스레드들은… 성능과 영향은 있지만… 더 늘린다고 해서 성능 향상이 생기고 기존 명령이 한꺼번에 많이 처리되지는 않는다는 것입니다.

이게 무슨소리냐!!! 라고 하시는 분들이 계실껍니다.

먼저, 목숨을 부지하기 위해서 결론부터 말씀드리자면… 이 두 스레드는 Redis에서 데이터를 처리하는 스레드가 아닙니다.(진짜예요!!! 이번엔 믿어주세요 T.T)

Redis의 스레드를 처리하는 파일은 bio.h 와 bio.c 이고 bio.h 를 보면 다음과 같은 코드를 볼 수 있습니다.

#define REDIS_BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
#define REDIS_BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
#define REDIS_BIO_NUM_OPS       2

REDIS_BIO_NUM_OPS는 몇개의 잡큐(개별 하나당 스레드)를 만들 것인지에 대한 내용이고, REDIS_BIO_CLOSE_FILE 과 REDIS_BIO_AOF_FSYNC를 보면… 아하.. 이것들이 뭔가 데이터 처리를 안할꺼라는 믿음이 생기시지 않습니까?(퍽퍽퍽)

크게 두 가지 함수가 존재합니다. 하나는 작업 큐에 작업을 넣는 bioCreateBackgroundJob 함수
그리고 이걸 실행하는 bioProcessBackgroundJobs 입니다.

void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
    struct bio_job *job = zmalloc(sizeof(*job));

    job->time = time(NULL);
    job->arg1 = arg1;
    job->arg2 = arg2;
    job->arg3 = arg3;
    pthread_mutex_lock(&bio_mutex[type]);
    listAddNodeTail(bio_jobs[type],job);
    bio_pending[type]++;
    pthread_cond_signal(&bio_condvar[type]);
    pthread_mutex_unlock(&bio_mutex[type]);
}

void *bioProcessBackgroundJobs(void *arg) {
    struct bio_job *job;
    unsigned long type = (unsigned long) arg;
    sigset_t sigset;

    /* Make the thread killable at any time, so that bioKillThreads()
     * can work reliably. */
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    pthread_mutex_lock(&bio_mutex[type]);
    /* Block SIGALRM so we are sure that only the main thread will
     * receive the watchdog signal. */
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGALRM);
    if (pthread_sigmask(SIG_BLOCK, &sigset, NULL))
        redisLog(REDIS_WARNING,
            "Warning: can't mask SIGALRM in bio.c thread: %s", strerror(errno));

    while(1) {
        listNode *ln;

        /* The loop always starts with the lock hold. */
        if (listLength(bio_jobs[type]) == 0) {
            pthread_cond_wait(&bio_condvar[type],&bio_mutex[type]);
            continue;
        }
        /* Pop the job from the queue. */
        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        /* It is now possible to unlock the background system as we know have
         * a stand alone job structure to process.*/
        pthread_mutex_unlock(&bio_mutex[type]);

        /* Process the job accordingly to its type. */
        if (type == REDIS_BIO_CLOSE_FILE) {
            close((long)job->arg1);
        } else if (type == REDIS_BIO_AOF_FSYNC) {
            aof_fsync((long)job->arg1);
        } else {
            redisPanic("Wrong job type in bioProcessBackgroundJobs().");
        }
        zfree(job);

        /* Lock again before reiterating the loop, if there are no longer
         * jobs to process we'll block again in pthread_cond_wait(). */
        pthread_mutex_lock(&bio_mutex[type]);
        listDelNode(bio_jobs[type],ln);
        bio_pending[type]--;
    }
}

보면 두 개의 잡큐가 각각 CLOSE 와 AOF_FSYNC 를 처리하고 있습니다. 그리고 이 두 잡큐에 잡을 넣는 것은 모두 aof.c 에 존재합니다.

하나는 aof_background_fsync 함수이고, 나머지 하나는 backgroundRewriteDoneHandler 에서 호출하고 있습니다. 하나는 aof_를 닫을 때 이를 Async 하게 close 하기 위한 것이고, 또 하나는 aof 작업중에 fsync를 통해서 데이터를 동기화 시키는 부분입니다.

이 것들은 disk를 flush 하거나, 파일을 닫기위해서 OS 작업이 되는 것을 해당 스레드에서 하게 되면, 블럭이 되어서 다른 작업이 느려질 수 있으므로, 해당 작업들을 OS 레벨에서 비동기로 처리하기 위한 것입니다.

즉, 이 스레드들은… 더 늘릴 필요도 없고(AOF는 한번에 하나만 생성이 됩니다.) 더 늘린다고 해서 실제로 Redis 자체의 작업을 빠르게 해주는 게 아니라는 것입니다.

즉, 여전히 Redis 는 싱글 스레드라고 보셔야 합니다.

[입 개발] memcached slabclass 에 대해서…

오래간 만에 memcached 소슬 보니, 너무 잘못 이해하고 있거나 한것들이 많아서 처음부터 새로 보면서 이번에는 기록을 좀 남겨둘려고 합니다. 흔히들 memcached가 내부적으로 메모리를 어떻게 관리하는지 잘 아시지만, 코드 레벨로는 잘 모르실 수도 있기 때문에 그냥 정리합니다.

먼저 간단히 용어를 정리하자면…

  • chunk_size : key + value + flag 정보를 저장하기 위한 최소 크기: 기본 48
  • factor : item size 크기를 얼마만큼씩 증가시킬지 결정하는 값: 기본 1.25
  • Chunk_align_bytes : chunk 할당시에 사용하는 align : 8
  • item_size_max: 최대 item의 크기: 기본 1MB

그리고 사이즌 chunk_size * 1.25^(n-1) 형태로 증가하게 됨.

이제 slab.c를 보면 slabclass_t 를 볼 수 있습니다.

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 */

    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    size_t requested; /* The number of requested bytes */
} slabclass_t;

static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

MAX_NUMBER_OF_SLAB_CLASSES 는 201로 정의되어 있습니다. 즉 최대 201개의 slabclass 가 만들어지는데, 실제로는 chunk_size 와 factor 값에 따라서 최대 item_size_max를 넘어가 버리면, slabclass는 거기까지만 사용됩니다.(slab_init 를 보면 쉽게 알 수 있습니다.)

/**
 * Determines the chunk sizes and initializes the slab class descriptors
 * accordingly.
 */
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
    unsigned int size = sizeof(item) + settings.chunk_size;

    ......
    ......

    while (++i < POWER_LARGEST && 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);
    }

    ......
    ......

위의 소스에 나오는 size는 slab별로 할당되는 기본 사이즈의 크기이고 위의 slabclass 구조체에는 item_size_max(기본 1MB) 를 넣어주고, perslab 에는 item_size_max / size로 몇개의 아이템이 들어갈 수 있는지 들어가게됩니다.

그리고 이 slab 안에 array로 item 들이 할당되게 됩니다. 기본적으로 array의 크기는 16으로 설정되고 그 뒤로는 2배씩 증가하게 됩니다. 관련 함수는 grow_slab_list를 보시면 됩니다. 그리고 slab에서 사용하는 chunk가 항상 item_size_max 인것은 아니고, size * perslab으로 될 때도 있습니다.(do_slabs_newslab 에서 확인 가능, memory_allocate 를 이용함)

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    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) ||
        (grow_slab_list(id) == 0) ||
        ((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;
    mem_malloced += len;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}

slabclass 는 여기까지 하고, 다음번에는 실제 item 의 추가 삭제시에 어떻게 되는가에 대해서 정리해보도록 하겠습니다.

[Redis] Scan/SScan/ZScan/HScan 이야기…

Redis를 사용하다보면 특정 Key 목록을 가져오고 싶다는 욕망이 항상 생깁니다. 기본적으로 Single Thread 이기 때문에, 전체 Key를 도는 것은 너무 비용이 비싸서…
특정 Key단위로 나눠서 컬렉션에 넣고, 다시 이걸 이용해서 컬렉션을 만드는 짓(?)을 해야만 했었다는 문제가 있었습니다. 그래서, 결국, Redis에도 추가된 명령이 바로

scan/sscan/zscan/hscan 입니다.
scan은 전체 key 목록에서, sscan은 set 안에서, zscan 은 sorted set 안에서, hscan은 hash 안에서 키를 가져오는 명령입니다.

특징은 DB에서 처럼 cursor 라는 개념이 있고 이로 인해서 기본 10개 또는 지정된 개수만큼 가져오는 것이 가능합니다. 또한 keys 명령처럼 패턴으로도 찾을 수 있습니다.

다음과 같은 명령으로 간단하게 사용할 수 있습니다. scan 과 sscan/zscan/hscan의 차이는 key를 지정하는냐 마느냐의 차이 밖에 없습니다.(해당 키 안에서 동작해야하기 때문이죠.)

SCAN cursor [MATCH pattern] [COUNT count]
SSCAN key cursor [MATCH pattern] [COUNT count]
ZSCAN key cursor [MATCH pattern] [COUNT count]
HSCAN key cursor [MATCH pattern] [COUNT count]

샘플은 다음과 같습니다. 첫번째 결과가 다음 cursor 값인데 이것이 0이 나올때 까지 계속해서 호출하면 됩니다.

redis 127.0.0.1:6379> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"

기본적으로 10개를 가져오게 되어있고, 가져올 개수(Count) 를 지정해줄 수 있습니다. 모든 scan은 scanGenericCommand 라는 공통 함수를 이용해서 처리가 됩니다.

void scanGenericCommand(redisClient *c, robj *o, unsigned long cursor) {
    ......
    ht = NULL;
    if (o == NULL) {
        ht = c->db->dict;
    } else if (o->type == REDIS_SET && o->encoding == REDIS_ENCODING_HT) {
        ht = o->ptr;
    } else if (o->type == REDIS_HASH && o->encoding == REDIS_ENCODING_HT) {
        ht = o->ptr;
        count *= 2; /* We return key / value for this type. */
    } else if (o->type == REDIS_ZSET && o->encoding == REDIS_ENCODING_SKIPLIST) {
        zset *zs = o->ptr;
        ht = zs->dict;
        count *= 2; /* We return key / value for this type. */
    }

    if (ht) {
        void *privdata[2];

        /* We pass two pointers to the callback: the list to which it will
         * add new elements, and the object containing the dictionary so that
         * it is possible to fetch more data in a type-dependent way. */
        privdata[0] = keys;
        privdata[1] = o;
        do {
            cursor = dictScan(ht, cursor, scanCallback, privdata);
        } while (cursor && listLength(keys) < count);
    } else if (o->type == REDIS_SET) {
        int pos = 0;
        int64_t ll;

        while(intsetGet(o->ptr,pos++,&ll))
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));
        cursor = 0;
    } else if (o->type == REDIS_HASH || o->type == REDIS_ZSET) {
        unsigned char *p = ziplistIndex(o->ptr,0);
        unsigned char *vstr;
        unsigned int vlen;
        long long vll;

        while(p) {
            ziplistGet(p,&vstr,&vlen,&vll);
            listAddNodeTail(keys,
                (vstr != NULL) ? createStringObject((char*)vstr,vlen) :
                                 createStringObjectFromLongLong(vll));
            p = ziplistNext(o->ptr,p);
        }
        cursor = 0;
    } else {
        redisPanic("Not handled encoding in SCAN.");
    }

    ......

위의 코드에서 핵심적인 부분은, 찾아야 하는 key들을 keys라는 리스트에 추가하고, 이 리스트를 돌면서 패턴에 맞는 것들을 삭제한 결과를 돌려줍니다. “우와 대박이다.” 라고 생각할 수
있는데, 여기서 주의할 것이 있습니다. 기본적으로 딱 정해진 개수만큼 가져오는것이 아니라는 겁니다. 당연히 Redis는 다양한 자료구조를 지원하고 있고, 속도를 위해서 같은 자료구조라도
구현 방식이 여러가지입니다. 예를 들어, sorted set은 SKIPLIST 형태로 구현된것이 잇고, ziplist 형식도 있습니다. 그래서 또한 동작방식도 조금씩 다릅니다.(그래서 사실 이 글을 쓰고
있는거긴 한데 말입니다.)

다음과 같은 경우를 주의해야합니다.
1] 기본적으로 scan 의 경우 table 의 한 블럭을 가져오는 것이라서, 여기에 개수가 많으면 시간이 많이 걸릴 수도 있다.(다만, 리해싱 테이블이 bitmasking 크기만큼 커지므로, 한 블럭이 극단적으로 커질 가능성은 높지 않습니다.)
2] set/sorted set/hash 의 내부 구조가 hash table 이나 Skiplist 가 아닐 경우(ziplist 로 구현되어 있을 경우), 한 컬렉션의 모든 데이터를 가져오므로, keys와 비슷한 문제가 그대로 발생할 수 있다.

위의 경우만 아니면 keys가 필요할 때 scan 명령을 사용하는 것은 꽤 좋은 선택이 될것 같습니다.

기본적으로 사용법은 다음과 같습니다.(python 기준입니다.)

import redis
r = redis.StrictRedis('localhost', port=2000)

init = 0
while(True):
    ret = r.scan(init)
    print init
    init = ret[0]
    print ret[1]
    if (init == '0'):
        break

[책 리뷰] 실전 클라우드 인프라 구축 기술 – 도움이 안되겠지만, 큰 도움이 되는 책…

사실, 클라우드니, 빅데이터라는 타이틀을 드는 책들을 보면, 공통적으로 드는 생각이 있다. “별로 도움이 안된다.” 라는 것이다. 왜냐하면, 이런 것들은, 책만 보고 겨우 장비 1~2대에서 실습을 해봤자, 아무런 도움이 되지 못하기 때문이다. 그럼에도 불구하고, 이 책을 봐야하는 이유는 말 그대로 “시행착오”를 줄이기 위해서라고 할 수 있다. 사실, 이런 분야는 규모가 있어야만 발생하는 문제들이 많기 때문에, 규모가 적을 때는 아무런, 문제가 안보이는 경우가 “대부분”이기 때문이다.

결국 위의 이유로, 내부의 소스를 본다든지, 기반 지식들을 제대로 쌓아야만, 제대로 구축이 가능하거나, 트러블 슈팅을 하거나, 서비스를 할 수가 있는데, 밑바닥부터 이런것들을 전부 시작할려고 하면 엄청난 시간이 걸리기 때문이다.

사실… 이 책에 있는 각각의 내용들이, 전부 책 한권으로도 부족할 수 밖에 없는 내용들이기도 하고, 책에서 설명하고 있는 OS 버전들과 ceph 라든지 glusterfs 역시 버전을 맞추지 않으면, 사실 실행도 못해보고 끝나버리게 되는 경우가 발생하게 된다.(사실 혼자서 glusterfs를 깔다가 버전이 서로 안맞아서 엄청나게 삽질한 적도 많은…)

물론, 장담을 하자면, 여기에 있는 내용들을 다 숙지한다고 해도, 실제 서비스를 한다면, 엄청나게 많은 삽질을 더 하게 될거라는 것을… 확언하지만… 잘 모른다면, 몇십배로 더 고생할 것이, 확실하다라고 말할 수 있다. 한가지 팁이라면… 중간 중간 나오는 팁들을 유심히 살펴보기를 원하는데, 예를 들어, 13장의 팁을 보면… VDI 서비스를 구축할 때, I/O(네트워크, 스토리지) 에 신경을 써야 한다라는 얘기가 나오는데, 이런 것들이 실제로 겪어야만 알 수 있는 문제를 미리 막아준다고 할 수 있다.(다만, 팁이, 1챕터당 하나가 안된다는 슬픈현실이…)

결국 이책은… “도움이 안되겠지만, 큰 도움이 되는 책이라고 할 수 있다.”(좋은 책이지만, 이 분야가 그런거라… 어찌 할 수가 없는…)

테스트를 못짜는 사람의 비겁한 변명 – Effective Unit Testing

이 책을 읽으면서 느낀 생각이지만, 내가 하고 있는 말이 “테스트를 못짜는 사람의 비겁한 변명”이 아닐까라는 생각이 들었다. 사실 안드로이드 개발을 하면서, “테스트를 만드는 것이 굉장히 힘든 일”이라고 생각하게 되었고(솔직하게는, 원래 테스트에 약한?) 거기에 대해서 계속 어려움을 격지만, 무시하고 넘어가고 있었다.

책을 보면서 내내 들었던 또 다른 생각중에 하나는 내가 하고 있는 실수들이 대부분 예시에 등장한다는 것이다. 중간에 나왔던, “주석으로 변한 테스트”의 경우, 구조의 변경이나, 해당 기능의 변경등으로, 필요없는 코드나, 테스트들을 단순히 주석처리만 해두는 경우가 많다. 그런데 사실 이런 경우는 큰 문제는 안되지만… 문제가 되는 케이스는, 그 테스트가 필요없어서가 아니라, 지금 구조에 맞게 테스트 하는 방법을 몰라서 그냥 “주석” 처리하는 것이 가장 나쁜 예가 아닐까 싶다.

또한 항상 어려움을 겪는, 테스트명을 제대로 작성하지 않는다라든지… 나의 가슴에 송곳처럼 찌르는 내용들이 꽤 많았다.(전부 나의 잘못!!!)

– 특정 조건일때만 검사가 되는 조건부 테스트라든지…
– 절대 실패하지 않는 테스트라든지…
– 아주 단순한 결과만 체크한다든지…
– 과잉 보호 테스트라든지…

그리고 테스트를 작성하면, 클래스의 인터페이스등이 그냥 생각하고 짤 때보다 훨씬 실용적이 된다고 할까?, 테스트를 잘 만들기 위해서는 인터페이스의 전달이라든지, 파라매터의 전달이 훨씬 깔끔해지는 것도 사실이다.

또한, 테스트는 좋은 샘플로의 가치도 있다. 함수의 사용법이나, 이런게 어려울 때, 테스트 코드가 많으면, 이를 어떻게 써야하는지 알 수 있는 경우가 많다.(나도 이렇게 만들어야 하는데…)

다만, 책을 읽는 분들이, “유닛 테스트”가 전부라는 생각은 안하는 것이 좋지 않을까 싶다. “유닛 테스트”를 만들면 모든 버그가 사라지는 것은 아니다. “테스트”라는 것의 개발자의 사고에 의해서 그 범위가 제한 당하기 때문에, 모든 케이스를 생각해서 테스트를 만드는 것은 어렵다. 다만, 어느 정도 제대로 된 테스트 셋이 있으면, “리그레이션 테스트”로써 상당히 많은 도움을 줄 수 있다라고 할 수 있다는…

전에서 팀에 한분이 거의 대부분에 대한 “외부 테스트”를 만들어 주셨고, 이를 통해서, 코드 전체가 바뀜에도 거의 큰 문제 없이(도리어 이걸 만들면서 이전의 케케묵은 버그들을 찾을 수 있었다는) 넘어갈 수도 있었다.

특히, 주변의 얘기를 들어보면, DBMS 같은 경우 그 테스트셋이 상당히 중요한 가치가 있다고 한다.

횡설 수설 했지만, 읽으면서, 새해에는 테스트를 이래이래해서 못짰습니다라는 비겁한 변명은 하지 않아야 겠다라는 생각이든다.(흑흑흑 안드로이드 테스트는 어떻게 헤야할까?)

[입 개발] Redis Pipeline and Transaction(Multi/Exec)

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

Redis Pipeline에 대한 내용을 보고 나면, 다음과 같은 질문이 꼭 나오게 됩니다. Pipeline을 이용하면 성능은 좋아지는데, Redis는 Single Thread 라, Pipeline으로 처리하는 동안에는 다른 클라이언트의 작업을 처리못하게 되는거 아닌가요? 라는 질문입니다.

먼저 결론부터 내리자면, Pipeline 형태로 데이터를 전달하더라도 중간에 명령을 수행할 수 있습니다.(진짜루?)

그럼 다음과 같이 간단하게 테스트를 해보도록 하겠습니다. 테스트에는 KT Olleh UcloudBiz 의 8vCore, 16GB 머신을 이용했습니다.

일단 지난번의 테스트 코드를 다시 사용합니다. 다만 테스트를 위해서 프로세스는 하나만 띄웁니다.

from multiprocessing import Process
import redis
import random
import string

def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for x in range(size))

data = id_generator(512, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()")

def f(idx):
    print 'PR', idx
    r = redis.StrictRedis(host='172.27.217.193', port=6379, db=0)
    pipeline = r.pipeline()
    for i in xrange(idx * 1024 * 1024 * 2, (idx+1)*1024*1024 * 2):
        key = 'PR%s'%(i)
        pipeline.get(key)
        if (i != idx * 0124 * 1024 and i%1024==0):
            pipeline.execute();

if __name__ == '__main__':
    print data
    pids = []
    for i in xrange(1):
        p = Process(target=f, args=(i,))
        pids.append(p)
        p.start()

    for pid in pids:
        pid.join()

이제 이 코드를 실행하고 모니터링을 해보면… 이상한 결과가 보여집니다.

redis-pipeline-with-transaction

분명히 문제 없다고 해놓고, 시간이 뻥뛰고 있습니다. redis-stat이라는 모니터링 툴 역시 내부적으로 info 명령을 주기적으로 전달하는 건데, 저렇게 시간이 확 지난거면… 중간에 명령이 실행이 안되는거지 않느냐!!! 라는 생각이 부글부글 드실껍니다.

여기에 비밀이 하나 있습니다. 제가 사용한 Python Redis Clinet 인 redis-py 가 Redis 의 다른 특성을 함께 사용하고 있어서 그렇습니다. 여기서 설명할 것이 Redis Transaction Multi/Exec 입니다. Transaction 이라는 것은 여러 개의 명령을 하나의 성공/실패의 그룹으로 묶어서 수행하는 것을 말합니다. 예를 들어 다음과 같은 명령들이 있다고 합니다.

set A 123
get A

당연히 기대하는 값은 “123″ 이겠지만, set A 123 명령 다음에 set A 23이라는 명령을 다른 클라이언트에 의해서 실행이 되면 기대했던 결과를 얻을 수가 없습니다. 그래서 redis 2.6 부터는 MULTI/EXEC 라는 Transaction을 지원하기 위한 명령을 지원하고 있습니다. MULTI/EXEC 의 특징은 그 안의 명령을 모두 한꺼번에 실행시켜준다라는 특성이 있습니다. 즉

MULTI
set A 123
get A
EXEC

의 마지막 결과는 항상 123 입니다. 이것의 구현은 어떻게 되는 것일까요? 아주 간단합니다. MULTI가 나오면 Redis 가 해당 Client로 할당된 큐에 EXEC가 나오기 전까지 모든 명령을 담아두고 있다가 EXEC 명령이 들어오면, 그냥 모두 한꺼번에 수행시켜줍니다. 그래서 이 안에 blocking 관련 BLPOP등의 명령은 데이터가 없으면 바로 실패하게 됩니다. 그리고 MUTLI/EXEC 사이에 명령이 많으면, 이걸 수행하기 위해서, 그 동안에는 다른 클라이언트의 명령을 처리하지 못합니다.(Single Thread 라…)

왜 이 설명을 했냐 하면, Python Redis Client 는 기본적으로 pipeline을 실행할 때, MULTI/EXEC를 사용합니다.(이 특성 때문에 redis-py 의 pipeline 기능을 twemproxy와 함께 사용하려고 하면 지원하지 않는 명령이라 제대로 실행이 되지 않습니다.)

그럼 어떻게 해야 하는가? 호출시에 transaction=False 를 넣어주면 됩니다. 다음과 같습니다.

def f(idx):
    print 'PR', idx
    r = redis.StrictRedis(host='172.27.217.193', port=6379, db=0)
    pipeline = r.pipeline()
    for i in xrange(idx * 1024 * 1024 * 2, (idx+1)*1024*1024 * 2):
        key = 'PR%s'%(i)
        pipeline.get(key)
        if (i != idx * 0124 * 1024 and i%1024==0):
            pipeline.execute();

자 이렇게 바꾸고 다시 실행 후 모니터링 해보도록 하겠습니다.

redis-pipeline-without-transaction

자, 이제 2초마다 제대로 모니터링 되는 걸 알 수 있습니다. 즉 제대로 중간에 명령이 끼어들 수 있다는 겁니다.
(제가 거짓말 한게 아닙니다.)

정리하면 다음과 같습니다.
1. Redis Pipeline은 중간에 명령을 허용한다. 즉 이로 인해 뭔가 데이터가 바뀔 수도 있다.(다른 클라이언트가 변경할 수 있으니…)
2. Multi/Exec를 쓰면, 그 사이의 명령이 전부 한꺼번에 수행된다. 이 안의 작업이 길면 다른 클라이언트들의 명령이 늦게 처리된다.
3. 사용시에 라이브러리를 잘 확인하자. 위와 같은 이유로 안될 수 있다.

[회고] 2013년 정리 및 2014년 목표 설정

이제 2013년이 하루도 채 남지 않았습니다. 2013년은 아주 다사다난했던 한해가 아니었나 싶은데, 해외취업이 되었다고 생각하고 있다가, 미대사관에서 비자가 안나와서 한국에 주저않았고(다만, 이게 꼭 나쁘다고만은 생각되지가 않는게, 나갔더라면, 지금보다 더 고생하지 않았을까 싶기도 하다는… 한국이 살기 좋다는…) 결국 의도한대로 1년이 아니라, 1년 6개월을 놀게 되어서 모아둔 돈은 다 까먹었다는…

그래고 간신히 취업도 하고, 가장 중요했던 2세도 가진걸 보면… 나쁘지는 않은 한해가 아니었나라고 생각해본다.

이 글을 쓰면서, 작년에 “[회고] 2012년 정리 및 2013년 목표 설정” 라고 쓴 글을 보니…
작년의 목표는 “영어” 와 “오픈소스” 라는데, 영어는 -_- 그냥 완전 포기 수준으로 바뀌었고(그 뒤로 거의 쓴적이 없는…)
오픈소스는 그래도 작년에 레디스 컨트리뷰션 한것들이나, libcloud, Apache Tajo 에 올해 계속 컨트리뷰션 하게 되면서, 의외로 꽤 많은 시도를 한 것 같다.

그 외에도, 뭔가 여러가지 일들에 발을 담그게 되었는데… 페북과 트위터로 많은 좋은분들을 알게 된게 가장 큰 수확중에 하나가 아닐까 싶다. 아 Redis-doc 커밋권한을 얻은것도 있구나…(소스가 아닌 문서지만…)

이제 2014년의 목표를 설정해야 하는데…
사실 다들 알듯이… 곧 2세가 나오시니, 모든 목표 설정은 이것에 맞춰져야 하지 않을까 싶지만…
개인적인 바램은… 2014년 초에 Apache쪽 커미터가 되는것과, 영어 공부도 좀 하고… 등등등이지 않을가 싶다.

한 해의 키워드를 잡아야하지만, 당장은 닥친 하루 하루를 열심히 살아봐야겠다.
오늘 밤에도 별이 바람에 스치운다.

By charsyam Posted in

[입 개발] Redis Pipeline

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

가끔씩 Redis의 성능을 좀 더 극대화 시키고 싶어하시는 분들이 있으시기 때문에, 오늘은 이에 가장 많이 사용되는 Pipeline에 대해서 설명을 할려고 합니다.

먼저 재미난 사실을 알려드리자면, Redis pipeline이라는 것이 성능 향상을 위해서 유리하다라는 것은 많은 분들이 알고계시지만, 사실, 서버에는 Redis pipeline 라는 기능이 없다는 것입니다.

다음과 같은 명령을 Redis 소스에서 확인해보면, redis-benchmark.c 와 redis-cli.c를 빼고는 아무데도 pipe라는 글자자체가 없다는 것을 알 수 있습니다.

grep pipe src/* -Rn

그렇다면 Pipeline의 정체는 무엇일까요? 먼저 Pipeline을 적용한 테스트와 Pipeline을 적용하지 않은 테스트 결과를
보여드리도록 하겠습니다.

언어는 python 에 KT Olleh UCloudBiz 의 8vcore 16GB 메모리 모델을 사용했습니다.
두 대를 사용해서 한대는 redis-server unstable 버전이 설치되어 있고, 다른 서버에서 접속한 버전입니다.

from multiprocessing import Process
import redis
import random
import string

def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for x in range(size))

data = id_generator(512, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()")

def f(idx):
    print 'PR', idx
    r = redis.StrictRedis(host='172.27.217.193', port=6379, db=0)
    for i in xrange(idx * 1024 * 1024, (idx+1)*1024*1024):
        key = 'PR%s'%(i)
        r.get(key)

if __name__ == '__main__':
    print data
    pids = []
    for i in xrange(80):
        p = Process(target=f, args=(i,))
        pids.append(p)
        p.start()

    for pid in pids:
        pid.join()

redis-not-pipeline위의 그림을 보면 55k 에서 65k 수준의 cmd/s 를 볼 수 있습니다.

이제 Pipeline을 적용한 테스트를 한번 보도록 하겠습니다. 다른 부분은 동일하고 함수 f()만 조금 수정되었습니다.

def f(idx):
    print 'PR', idx
    r = redis.StrictRedis(host='172.27.217.193', port=6379, db=0)
    pipeline = r.pipeline()
    for i in xrange(idx * 1024 * 1024, (idx+1)*1024*1024):
        key = 'PR%s'%(i)
        pipeline.get(key)
        if (i != idx * 0124 * 1024 and i%1024==0):
            pipeline.execute();

redis-pipeline

물론 거의 200k 전후로 cmd/s 가 나오는 것을 볼 수 있습니다. 도대체 pipeline은 무엇이길래, 이런 속도차를 보여주는 것일까요?

사실 pipeline을 이용해서 가지게 되는 이점은 다음과 같습니다.
1. 명령들이 뭉쳐서 전달되기 때문에, 클라이언트 입장에서는 함수 콜이 줄어들고, 서버 입장에서도 I/O가 적게 발생합니다.
2. 보통 하나의 명령을 보내면 결과를 확인하는 시점이 있는데, 이미 서버에서는 다음 명령이 실행되고 있기 때문에 이런 시간이 줄어듭니다.

실제로 memcached 나 redis를 쓸 때, 한 서버와 통신할 때 mget등을 이용해서 명령어 전송 수를 줄이는 것도 일종의 튜닝 방법입니다. 사실 서버입장에서는 명령어가 뭉쳐서 들어와서 시스템 콜이 줄어드는 이점 이외에는 아무런 차이가 없지만… 심할때는 위와 같은 차이가 나는 것입니다.

[책 리뷰] Head First Data Analysis – 데이터 분석의 시계로 딥 다이빙

개인적으로 Head First 시리즈를 상당히 좋아하는데, 읽기 쉬워보이지만, 항상 이해하기는 어려운 책이라고 생각한다.(다만, Head First 가 가장 이해하기 쉬운 책들이라는 ㅎㅎㅎ)

개인적으로 데이터 분석이라는 것에 흥미는 가지고 있지만, 통계학 서적을 볼때마다 생기는 머리속의 인지부조화와 수식에 대한 근원적인 두려움 때문에 아직까지 시작만하고 거의 모른다고 볼 수 있다.

솔직하게 말해서 “Head First Data Analysis”를 단순히 읽었다고 해서는 데이터 분석의 세계로 발을 담을거라고 말할 수 도 없지만, 그래도 나름 가장 효과적으로 접근하는 길이 아닐까 싶기도 하다.

Head First Data Analysis를 보면서 가장 좋았던 부분은 5장을 보면서 “가설 검증을 어떻게 할것인가?” 였다.(나머지 부분들도 정말 좋지만!!!)
이런 책을 찾는 사람들의 대부분의 심리는, 사실 통계학적인 어떤 이론에 대한 이해를 원하기도 하겠지만, 이런 시작 부분에서, 자신의 생각이 논리적 타당성을 찾고 싶어하는게 아닐까 싶다.(그래서 6장의 베이지안 통계라는 이름을 가지고 있지만, 주 내용은 거짓 양성(음성) 이라는게 ㅎㅎㅎ)

여러가지 가설중에, 추가되는 증거들로, 가장 적합한 가설을 선택하는 것…(물론, 책에 있는 사건처럼 단순하게 결정되는 것들은 거의 없겠지만…) 을 어떤식으로 진행해야 할지에 대해서 알게되었다고 할까? 뒤로 가면, 실제로 만들어낸 모델의 오류 수정을 어떻게 할 것인가까지로 발전되는, Head First 시리즈의 가장 특징점이라고 할 수 있는, 그런 부분이, 데이터 분석 쪽에서 시작하려고 하는 사람에게는 큰 도움이 될것 같다.

다만… 데이터 분석이라는 분야 자체가 어려운것이라… 이 책을 읽어도 나처럼 고민하게 되는 분들이 꽤 많은 것 같지만…(그건 이 책의 단점은 아니라고 생각한다.) 그래도 데이터 분석이라는 분야에 대한 두려움을 아주 약간은 줄여주는 효과가 있다.