[입 개발] Redis 의 slowlog는 어떻게 측정되는가?

Redis 를 쓰면서 많이 참고하게 되는 명령들 중에 slowlog 가 있습니다. 그런데 이 slowlog가 정확하게 뭘 보여주는지에 대해서는 잘 정리가 되어있지 않아서…(너만 몰라… 컥… 퍽퍽퍽) 공부를 해봤습니다.

slowlog 는 뭘까요? 사실 DBMS등에도 보면 해당 쿼리가 얼마나 오래 실행되었는지 보여주는 기능들이 있습니다. 수행 시간(duration)이 얼마이상이면 로그를 남겨주기도 합니다. 어플리케이션에서도 보통 마지막에 수행시간이 얼마 이상이면 따로 로그를 남기지요. nginx 도 호출이 들어가고 나서 응답이 나올 때 까지의 시간을 재서 보여주는 기능이 있습니다.

showlog는 수행 시간이 느린 요청(쿼리)를 보여주는 기능입니다. 그럼 그 수행 시간을 어떻게 정의하는 가에 따라서 천차만별이라고 할 수 있습니다.

일반적으로 생각하는 것은 쿼리를 Redis 가 받아들인 시간 부터 결과가 나오는 시간이라고 생각할 것입니다. 이게 사실 거의 맞긴 합니다. 먼저 Redis에서 시간을 측정하는 코드를 보시죠.

void call(client *c, int flags) {
    long long dirty, start, duration;
    
    ......
    start = ustime();
    c->cmd->proc(c);
    duration = ustime()-start;
    ......
    if (flags & CMD_CALL_SLOWLOG && c->cmd->proc != execCommand) {
        char *latency_event = (c->cmd->flags & CMD_FAST) ?
                              "fast-command" : "command";
        latencyAddSampleIfNeeded(latency_event,duration/1000);
        slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
    }    ......
}

위의 코드를 보면 Redis 에서의 수행 시간을 재는 범위는 명확합니다. 패킷이 완성되어서, 실제로 딱 수행된 시간입니다.

수행된 시간만 들어간다는 의미는 무엇일까요? 즉 패킷이 완성되기 까지 대기하는 시간은 포함되지 않는다는 것입니다. 여기서 Redis slowlog의 맹점이 하나 존재합니다. 그것은 Redis는 Single Threaded 라는 겁니다.

스크린샷 2016-08-09 오전 1.41.44

Redis Event Loop는 처음부터 연결된 이벤트가 발생된 클라이언트의 루프를 돌면서 데이터를 읽고 패킷이 완성되면 그 때 실행하게 됩니다. 그런데 클라이언트들이 많고, 처리해야 하는 명령들이 많다면, 뒤에 있는 녀석은 실행이 늦게될 수 있습니다. 그런데 그 명령이 수행되는 시간 자체는 짧은 경우에 slowlog에는 남지 않습니다. 즉 실제 응답은 늦고 처리도 늦게 되었지만, 명령이 수행되는 시간 자체는 짧으므로, slowlog에 남지 않는 경우가 됩니다.

그러므로 slowlog에 잡히지는 않지만, 서버의 응답이 느려지는 경우는 Redis 서버가 너무 많은 쿼리를 처리하고 있는 건 아닌지 확인하셔야 합니다. 그리고 쿼리 수 가 너무 많다면, 서버를 분리하여, 쿼리 처리량을 줄이는 것이 해결책입니다.

[입 개발] Jedis 2.1.0 을 가지고 삽질한 이야기…

최근에… “왜 안되지!!!” -> “헉… 왜 됬지!!!” -> “왜 안되지!!!” -&gt “마음의 평안”을 가진 일이 있습니다. 그게 바로 Jedis 2.1.0을 쓰면서 발생한 일입니다. -_-;;; 저는 레디스 꼬꼬마기 때문에… 여러 개의 Request 가 갈 수도 있으니…pipeline을 이용해야지라고 결정을 했습니다. 참고로 현재 Jedis 버전은 2.8.2 와 2.9.0 입니다.(즉 2.1.0 은 아주아주 예전 버전입니다.)

그런데… 이상한 일은 Redis에 데이터를 집어넣는 아주 간단한 작업인데… 데이터가 늦게 들어가는 것이었습니다. 즉 해당 함수를 호출하면, 이상하게 함수를 호출 한 뒤, 몇 초 후에 해당 명령어가 Redis Monitor를 통해서 실행되는 것을 볼 수 있었습니다.

이 때 부터는 “왜 안되지!!!” 모드였습니다.

현재 버전까지는 Jedis 가 Future를 이용해서 비동기 실행을 지원하는 구조가 아니기 때문에, 아주 옛날 버전인 2.1.0 에서는 당연히 비동기가 안 될것은 자명한 일이었습니다.

그런데… 결국 제가 pipeline 에서 sync 함수를 사용해야 하는데 exec 를 사용했기 때문에 발생한 이슈라는 것을 깨닫게 되었습니다.(역시 사람은 낮잠을 자야…) 그런데… 이게 문제다라는 것을 깨달은 순간부터…

그럼 Jedis의 2.1.0 에서의 sync 와 exec 함수를 간단하게 살펴보도록 하겠습니다. 먼저 기본적으로 Jedis는 응답을 바로 주지만, pipeline 모드에서는 Response 구조체에 값을 넣어주게 됩니다.

    public void sync() {
        List<Object> unformatted = client.getAll();
        for (Object o : unformatted) {
            generateResponse(o);
        }
    }

    public Response<List<Object>> exec() {
        client.exec();
        Response<List<Object>> response = super.getResponse(currentMulti);
        currentMulti = null;
        return response;
    }

위의 코드 처럼 sync는 그냥 던진 리퀘스트에 대한 응답을 모두 생성하게 됩니다. getAll 함수는 inputStream 으로 들어온 응답들을 파싱해서 돌려주게 됩니다.

    public List<Object> getAll(int except) {
        List<Object> all = new ArrayList<Object>();
        flush();
        while (pipelinedCommands > except) {
        	try{
                all.add(Protocol.read(inputStream));
        	}catch(JedisDataException e){
        		all.add(e);
        	}
            pipelinedCommands--;
        }
        return all;
    }

그리고 pipeline은 아래의 Queable 을 상속 받기 때문에, 위에서 getAll로 받은 결과들을 각 Response에 채워줍니다.

package redis.clients.jedis;

import java.util.LinkedList;
import java.util.Queue;

public class Queable {
    private Queue<Response<?>> pipelinedResponses = new LinkedList<Response<?>>();

    protected void clean() {
        pipelinedResponses.clear();
    }

    protected Response<?> generateResponse(Object data) {
        Response<?> response = pipelinedResponses.poll();
        if (response != null) {
            response.set(data);
        }
        return response;
    }

    protected <T> Response<T> getResponse(Builder<T> builder) {
        Response<T> lr = new Response<T>(builder);
        pipelinedResponses.add(lr);
        return lr;
    }
}

그렇기 때문에 sync를 했다면, 제대로 실행되었겠지만, 제가 호출한 exec()는 multi()의 쌍을 위한 것이므로 완전히 잘못되었다고 볼 수 있습니다.

이 때 부터, 이제 “왜 됬지!!!!!!” 모드로 변환하게 됩니다. 이 때도 물론 전 좀 잘못된 지식을 가지고 있었습니다. sync를 할 때 명령이 실행될 것이다라고 생각한거죠. 그러니 sync 도 안했는데… 왜!!! exec는 exec 커맨드만 보냅니다. -_-(아마 여기서는 에러 응답이…)(자꾸 pipeline 곽 multi/exec 를 헷갈리고 있습니다.)

그런다가 다시 jedis를 소스를 따라가보니… 또 다시 충격이… set을 실행하면 다음과 같이 client.set을 실행합니다.

    public Response<String> set(String key, String value) {
        client.set(key, value);
        return getResponse(BuilderFactory.STRING);
    }

다시 client의 set을 따라가 봅시다. 헉 sendCommand 가 있습니다. 실제로 보낸다는 얘기죠.

    public void set(final byte[] key, final byte[] value) {
	sendCommand(Command.SET, key, value);
    }

sendCommand 는 실제로 Protocol.java 의 sendCommand를 호출합니다.

    private static void sendCommand(final RedisOutputStream os,
	    final byte[] command, final byte[]... args) {
	try {
	    os.write(ASTERISK_BYTE);
	    os.writeIntCrLf(args.length + 1);
	    os.write(DOLLAR_BYTE);
	    os.writeIntCrLf(command.length);
	    os.write(command);
	    os.writeCrLf();

	    for (final byte[] arg : args) {
		os.write(DOLLAR_BYTE);
		os.writeIntCrLf(arg.length);
		os.write(arg);
		os.writeCrLf();
	    }
	} catch (IOException e) {
	    throw new JedisConnectionException(e);
	}
    }

이것만 보고는 다시 -_- “왜 안되지!!!” 로 되돌아가게 됩니다. 코드만 보면 pipeline도 바로 실행이 되기 때문에, 제가 호출한 시점에 set 명령이 동작했어야 하기 때문입니다. 그리고 유심히 코드를 보다가 RedisOutputStream 을 보고서야 그 의문이 풀렸습니다.

RedisOutputStream 은 java/redis/clients/util/RedisOutputStream.java 에 있습니다. 아래는 RedisOutputStream의 생성자입니다. 일단 8192 bytes를 buf 라는 이름으로 할당합니다.
그리고 write 함수를 보면 buf에 commands를 저장합니다. 그리고 이 데이터를 실제 쓰는 시점은 flushBuffer를 호출할 때입니다.

    public RedisOutputStream(final OutputStream out, final int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

    public void write(final byte b[], final int off, final int len) throws IOException {
        if (len >= buf.length) {
            flushBuffer();
            out.write(b, off, len);
        } else {
            if (len >= buf.length - count) {
                flushBuffer();
            }

            System.arraycopy(b, off, buf, count, len);
            count += len;
        }
    }

    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }

이 얘기는 반대로 flushBuffer 가 호출되지 않을때는 명령이 전달되지 않는다는 것입니다. write 함수를 보면 데이터가 buf size(8192) 보다 커져야 할 때 실제로 명령을 전달하지 않습니다. IO를 줄이기 위해서죠.

즉, 제가 보낸 set 명령은 8192 바이트보다 적어서 flushBuffer 가 호출되지 않아서 전달되지 않다가, 나중에 이 버퍼가 차거나 다른 이유로 그 때 전달된 것입니다. 이제 정신 상태가 “마음의 평화” 상태로 전이되었습니다.

그런데 한가지 의문이 들 수 있습니다. client 의 sendCommmand를 바로 호출한 흐름과 동일하므로 평소에는 어떻게 바로 결과를 얻을 수 있을까요?

그것은 Jedis 가 get관련 함수가 있거나 응답을 바로 읽어야 할 때, flush 함수를 임의로 호출 해 주기 때문입니다. 즉, 바로 결과를 읽어야 할 시점에는 buf 에 있는 데이터를 전부 전달하고 그 결과를 받아오게 됩니다.

정리하자면, 제 set 명령은 buf 사이즈가 차지 않아서 buf에만 들어가고 실행되지 않고 리턴 된 것입니다. 그냥 set 명령을 호출했으면 바로 결과를 보기 위해서 getStatusCodeReply() 함수등이 실행되면서 명령이 전달되었을텐데, pipeline이라 바로 결과를 읽지 않기 때문에 발생한 것입니다.
그래서 다른 명령들이 해당 클라이언트를 사용해서 8192 바이트가 넘거나 해당 명령이 결과를 얻을려고 할 때 실제로 수행이 됩니다. 만약 다른 명령이 응답을 읽으려고 했다면, 버퍼에 들어가 있던 set 명령의 결과를 받아가므로 실제로는 익셉션이나 다른 값을 얻게 되었을겁니다.

결과적으로 Jedis는 multi 가 호출되지 않으면 exec 에서 Exception을 내도록 수정이 되었으므로, 그 뒤에는 이런 문제를 겪을 일이 없습니다. -_-;;; 저 혼자 삽질을 쭈욱쭈욱 했네요. 흑흑흑, 처음에는 딴 이슈인줄 알고 보다가… 엄청나게 삽질한 케이스입니다. T.T

[입 개발] 개발자 문화

최근에 개발자 문화에 대해서 이야기를 했어야 하는데… 잘못 알고 엄한 소리만 하다가 끝난 적이 있습니다. 그래서 이 글은 그 발표에 대한 반성을 하고자 합니다.

image007_charsyam

그런데, 개발자 문화라는 건 뭘까요? (우걱우걱 먹는건가요?)  그래서 먼저 당연히 구글신을 통해서 몇가지 검색을 해봤습니다.

위의 링크들은 제가 구글에서 “개발자 문화”라는 키워드로 나온 검색결과 첫페이지에서 긁어온 것입니다. 읽어보면, 개발자 문화라는 건, 일종의 개발 프로세스하고도 다아있습니다.

그런데, 개발자 문화라는 건 진짜로 개발프로세스만 말하는 것일까요? 물론, 아니라는 건 아닙니다. 우리가 일반적으로 듣고 싶어하는 것도, git/github 을 이용한다든지, 코드를 짜고 코드리뷰를 하고, 자동화된 테스트와 쉬운 deploy 입니다.

어떻게 보면, 우리가 말하는 개발자 문화는 조엘스포스키의 12단계와도 닫아있습니다.

The Joel Test

  1. Source Control(소스 컨트롤)을 사용하십니까?
  2. 한번에 빌드를 만들어낼 수 있습니까?
  3. daily build(일별 빌드)를 만드십니까?
  4. 버그 데이타베이스를 가지고 있습니까?
  5. 새로운 코드를 작성하기 전에 버그들을 잡습니까?
  6. up-to-date(최신) 스케줄을 가지고 있습니까?
  7. spec(설계서)를 가지고 있습니까?
  8. 프로그래머들이 조용한 작업환경을 가지고 있습니까?
  9. 돈이 허락하는 한도내의 최고의 툴들을 사용하고 있습니까?
  10. 테스터들을 고용하고 있습니까?
  11. 신입사원들은 면접때 코드를 직접 짜는 실기시험을 봅니까?
  12. hallway usability testing(무작위 사용성 테스팅)을 하십니까?

 

사실 위의 단계에서 몇가지를 더 추가할 수 있습니다. 개인적으로는

  1. 자동화된 테스트를 보유하고 있습니까?
    1. 버그가 안날 수는 없지만, 버그가 났던 것은 자동화된 테스트로 커버가 되어야 합니다.
  2. 코드 리뷰를 하고 있습니까?
    1. 사실 코드리뷰는 저희도 잘 안하고 있긴한데, 코드리뷰를 전문적으로 하는 것은 상당한 도움이 됩니다.
  3. 코드가 커밋되면 자동으로 빌드와 테스트가 실행됩니까?
  4. 배포/롤백이 쉽습니까?
    1. 배포/롤백이 쉬워야 하루에 몇번씩 또는 몇십번 씩 배포가 가능합니다.
  5. 장애를 낸 것에 대해서 비판하지 않고, 장애를 빨리 고칠려고 노력합니까?
  6. 서비스의 히스토리등이 잘 정리되어 있습니까?

사실 저도 개발자 문화라고 말할 정도로 뭔가 잘 알지를 못하기 때문에… 이런 것들 정도가 생각이 납니다.  최근에 저희 팀의 동료가 올린 멋진 슬라이드가 있습니다.

사실 가장 중요한 것은 git/github을 쓰거나 git flow/gitlab flow 이런 브랜치 방법론이라든지, 뭔가 좋은 툴을 사용하는게 아니라고 생각합니다. 위의 코드 리뷰 처럼, 뭔가를 시도하고 실패하면, 거기서 다시 발전한 부분을 찾는 것, 그리고 서로의 잘못을 찾는 것이 아니라, 장점을 찾고, 존중, 신뢰하며 발전할 부분을 찾는것 그것이어야 말로 가장 중요한 개발자 문화이지 않을까 싶습니다.

이미 모든게 잘 갖춰져 있는 것도 재미있지만, 즐겁게 실패하면서 함께 더 좋은 문화를 만들어갈려고 노력하는 것도 참 재미있는 일이 아닐까 싶습니다.

 

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

가끔씩 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를 참고하시면 아주 초급적인 자료들이 있는데, 대표적으로 다음 자료를 추천합니다.

 

 

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

최근에 자주 다음 질문을 받는 케이스가 늘어서 정리합니다.(사실 저도 당한…)
최신 버전 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

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

오늘 우리 팀의 조실장님이 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);
}

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

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

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

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

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

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

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

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