[입 개발] 왜 Redis 응답이 느린데, slowlog에는 안찍히나요?

Redis를 쓰다보면, 확실히 응답이 늦게 와서, Client 에서는 timeout이 걸리는데, 실제 Redis의 slowlog를 보면 아무런 정보가 없을 때가 많습니다.

오늘은 Redis에서 느린건 확실한거 같은데 왜 slowlog는 남지 않는지 살짝 알아보도록 하겠습니다. 먼저 느리다는 것은 어떤 의미일까요? 느리다는 것 자체는 우리가 기대(또는 예상) 하는 속도보다 더 시간이 걸리는 것으로 얘기할 수 있습니다. 영어로는 latency가 얼마인가? 이런식으로도 표현을…

그런데 이 latency가 얼마냐? 라는 것은 어떻게 측정하는가에 따라서 달라집니다. 어떻게 보면 상대적인 거죠. 위에서 얘기했듯이, 분명히 클라이언트에서는 timeout이 걸릴정도로 느릴 수 있는데, Redis 입장에서는 slowlog가 쌓이지 않는다는 것은 Redis 입장에서는 이게 느리지 않다라는 것입니다.(여기서 살짝 뭔가를 덧붙이면, 사실 Redis가 응답을 늦게 주는 건 맞습니다. 그러나 Redis는 일찍 줬다고 측정하는거죠. 왜?)

먼저, 이 속도를 측정하는 구간에 대해서 이야기를 해보겠습니다. 일반적으로 우리는 다음과 같은 상황으로 측정합니다. 보통 클라이언트 입장에서 latency 라고 하면 Redis를 호출하고, Redis의 응답을 받기까지의 시간을 의미합니다.

latency1

즉 위의 그림에서 1에서 2까지의 시간을 우리는 보통 latency라고 표현합니다. 그래서 이 시간동안의 timeout을 넘어가면 응답이 느리다고 인식하게 되는 것이죠.

그런데 Redis 입장에서 latency, 즉 slowlog를 측정하는 방법은 좀 다릅니다.
latency2

위의 그림에서 빨간색 3으로 표시된 부분만이 Redis에서 slowlog의 대상이 되는 시간입니다. 소스를 보면 다음과 같습니다. c->cmd->proc 함수를 호출하는 바로 앞과 뒤에서 해당 함수의 수행시간을 재고 있습니다.

void call(client *c, int flags) {
    ......
    start = server.ustime;
    c->cmd->proc(c);
    duration = ustime()-start;

    ......

    /* Log the command into the Slow log if needed, and populate the
     * per-command statistics that we show in INFO commandstats. */
    if (flags & CMD_CALL_SLOWLOG && !(c->cmd->flags & CMD_SKIP_SLOWLOG)) {
        char *latency_event = (c->cmd->flags & CMD_FAST) ?
                              "fast-command" : "command";
        latencyAddSampleIfNeeded(latency_event,duration/1000);
        slowlogPushEntryIfNeeded(c,c->argv,c->argc,duration);
    }
   
    ......
}

이 상황에서 다시 문제가 되는 부분들을 확인해 봅시다. 크게 두 가지로 나눠집니다.

  • Case A: 클라이언트 입장에서도 latency가 느리고, Redis에서도 느려서 slowlog가 찍힐 만한 상황
  • Case B: 클라이언트 입장에서는 latency가 느리지만, Redis 입장에서는 느리지 않다고 인식하는 경우

먼저 Case A 부터 살펴봅시다. Case A는 Redis에서 위에서 말한 빨간색 3에 해당하는 케이스입니다. Redis의 PrcoessCommand가 느려지는 경우는, O(N)의 명령을 사용하거나, 해당 메모리가 swaping 대상으로 매번 디스크를 접근해야 될 경우입니다. 후자는 메모리가 부족할 때 발생하는 케이스이고, 전자는 대량의 아이템을 한번에 가져가는 케이스, 예를 들어 KEYS 명령을 쓰거나, 굉장히 많은 item이 든 collection의 데이터를 모두 가져오는, 예를 들어, 10만개가 들었는데 10만개를 전부 가져오면 느려집니다. 대표적으로 다음과 같은 명령들이 있습니다.

  1. hgetll
  2. smembers

아니면 이렇게 든 collection을 지울 때 발생하게 됩니다.

그럼 이제 오늘의 주제였던 Case B를 살펴봅시다. 일단 다시 짚고 넘어가야할 것이, Redis는 싱글스레드입니다. 즉, 하나의 명령을 수행하는 동안 다른 명령을 수행하지 못합니다. 그리고, Redis의 네트웍 모듈 역시 이 싱글스레드에 영향을 받습니다.

Redis에서 하나의 명령을 수행하는 과정은, 네트웍 패킷을 클라이언트 마다 epoll 등의 IO multiplexer를 이용해서 읽어들이고, 이게 완성되었다라고 판단이 들면 그 시점에 바로 수행을 해버립니다. 즉, 클라이언트에서 Request를 보냈다고 하더라도, TCP로 인해서 패킷이 분할되서 갈 수도 있고, 한방에 갈수도 있지만, 하나의 Request가 완성이 되었다고 Redis가 판단해야만, 그 때 실행되게 됩니다. 그렇다면, 내가 네트웍으로 Request를 보냈지만, Redis가 다른 명령들의 network 패킷을 읽어드리고 해당 명령어를 처리하는 중이라면, 우리가 보낸 Request는 아직 실행이 되지 않습니다. 즉, 우리가 Request를 보낸 시간은 계속 지나가지만, Redis가 다른 수 많은 명령을 처리한다면, 그리고 그 기간이 길어진다면 위에서 말한 1,2의 시간은 길지만, Redis 입장에서는 하나의 명령을 짧게 처리하면, 자신은 여전히 빨리 처리한다고 생각하므로, slowlog가 남지 않고, 클라이언트에서는 응답이 느려지는 현상이 발생하게 되는 것입니다.

이럴 경우는 Redis에서, 시간이 느린 명령을 최대한 줄이거나(slowlog에 걸릴만큼은 아니지만 느린 명령들), Redis 서버를 늘려서, Requests 자체를 줄이는 방법이 있습니다. 시간이 느린 명령은 info all 명령으로 cmd states를 보시면, 호출 수와 평균 응답시간이 있으니 이거 기준으로 튜닝하시면 됩니다.

그럼 고운 하루되세요.