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

[입 개발] 개발자 문화

$
0
0

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

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

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

 



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

$
0
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


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

$
0
0

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 서버가 너무 많은 쿼리를 처리하고 있는 건 아닌지 확인하셔야 합니다. 그리고 쿼리 수 가 너무 많다면, 서버를 분리하여, 쿼리 처리량을 줄이는 것이 해결책입니다.


[입 개발] Consistent Hashing 에 대한 기초

$
0
0

최근에, consitent hashing 에 대해서 다시 한번 공부를 해야할 기회가 생겨서 다시 한번 훝어보게 되었습니다. 그러면서 느낀게… 내가 잘못 이해하고 그렇게 구라를 치고 있었구나라는 점을 느끼게 되었죠.(이것도 개구라일지도…)

Consistent Hashing 에 의 가장 큰 특징 중에 하나는 HashRing에 k 개의 노드가 있는 상황에서, 노드가 사라지거나 추가될 때 1/k 정도의 key에 대한 것만 유실이 되고 나머지 key는 변동 없이 그 위치에 존재한다는 것입니다. 그리고 같은 값으로 노드들이 만들어지면 그 순서도 항상 동일합니다.(전 바로 이 부분이 신기했었습니다. 그런데… 그런데…)

일단 Consistent Hashing 이라는 것은 David Kager 라는 사람에 의해서 처음 소개가 되었습니다. https://www.akamai.com/es/es/multimedia/documents/technical-publication/consistent-hashing-and-random-trees-distributed-caching-protocols-for-relieving-hot-spots-on-the-world-wide-web-technical-publication.pdf

일단 논문은 어려우니 패스하기로 하고…

1] Consistent Hashing의 핵심은 hash 함수

일단 Consistent Hashsing 의 가장 큰 핵심은 hash 함수입니다. 뭐, 여러가지 어려운 얘기들로 시작하면 더욱 더 어려워지므로, 가장 간단하게 말하자면, hash 함수의 특징은 f = hash(key) 의 결과가 항상 같은 key에 대해서는 같은 hash 결과 값이 나온다는 것입니다. 이 얘기는 우리가 host1, host2, host3 와 같은 주소를 해시하면 hash 함수를 바꾸지 않는 이상은 항상 같은 hash 값이 나오게 됩니다. 그러면 그 hash 값으로 정렬을 하게 되면? 항상 같은 순서가 나오겠죠.

그리고 이 hash 값으로 Hash Ring을 만들면…?

이해를 쉽게 하기 위해서, 어떤 hash의 결과가 0 부터 1 까지 float 형태로 나온다고 가정하겠습니다. hash(“host1”) = 0.25, hash(“host2”) = 0.5, hash(“host3”) = 0.75 가 나온다고 가정하고, 특정 key에 대한 hash 결과는 그것보다 hash값이 크면서 가장 가까운 host에 저장이 된다고 하겠습니다. 즉 hash(“key1”) = 0.3 이면 key1이라는 key가 위치할 서버는 0.5 값을 가지는 host2가 되게 됩니다. 0.75 보다 크면 Ring 이므로 다시 첫번째 host1에 저장이 되게 됩니다.

이제 우리는 hash 함수와 서버의 목록만 알면, 바로 특정 key를 어디에 저장할 것인지 결정할 수 있게 되었습니다. 그리고 Consistent Hashing은 위에서 말했듯이… 서버가 추가되거나 없어져도, 1/k 개의 key만 사라지는 특성이 있습니다. 이것은 또 어떻게 보장이 되는 것일까요?

hash(“host4”) = 0.6 인 서버가 하나 추가되었다고 가정하겠습니다. 이 서버가 들어오면 순서는
host1, host2, host4, host3 이 됩니다. 즉 host4 와 host3 사이의 값, 즉 hash 함수의 결과가 0.6 ~ 0.75 인 녀석들만 저장해야 할 서버가 바뀌지, 다른 녀석들은 원래의 위치에 그대로 저장되므로 찾을 수 있게 됩니다.

다시 정리하자면, A, B, C 세 대의 서버가 hash Ring을 구성합니다.

4

여기에 1이라는 key가 들어오면 hash(“1”) 해서 그 결과값을 보니 B가 규칙에 맞아서 B에 저장되게 됩니다.

5

이제 두번 째 2가 들어올 경우 hash(“2”) 한 값이 C에 속해야 하므로 C에 저장되게 됩니다.

6

마지막으로 key 3,4는 hash(“3”), hash(“4”) 의 값이 A 서버에 속하므로 A 에 들어가고 key 5는 C에 가까워서 C에 들어가게 됩니다.

7

그런데 위의 예제나 그림을 보면, A, B, C의 공간이 서로 균일하지가 않습니다. 또, B가 죽는다고 가정하면 B의 부하는 전부 C로 넘어가게 됩니다. 뭔가 불공평한 일이 벌어지는 것이죠. 그래서 이것을 해결하기 위해서 가상의 친구들을 더 만들어냅니다.

가상의 친구들을 한 서버당 2개씩 더 만든다고 하면 hash(‘A’), hash(‘A+1’), hash(‘A+2’) 로 Hash Ring 에 추가합니다. B는 hash(‘B’), hash(‘B+1’), hash(‘B+2’) 등으로 추가하면 됩니다. 즉 총 3개의 서버를 9개로 보이게 하는거죠. 아래와 같이 A+1은 실제로는 A지만, hash ring에서 가상적으로 다른 녀석으로 보이게 됩니다. hash ring 자체도 더 촘촘해지고, 어떤 서버가 한대 장애가 나더라도, 그 부하가, 적절하게 나머지 두 서버로 나눠지게 됩니다. 실제 서비스에서는 서버당 수십개의 가상 노드를 만들어서 처리하게 됩니다.(2~3개도 너무 적습니다.) 이것을 보통 vnode 라고 부르게 됩니다.

ch2

핵심 결론은, 서버 이름으로 hash 값을 만들어서 정렬한 것을 하나의 Ring 처럼 생각해서 key를 hash 값에 따라 저장한다입니다. 다음은 아주 간단하게 만든 Consistent Hashing 코드입니다. rebuild가 핵심입니다.

그런데!!!, 하나 더… 위에서 우리가 놓친 중요한 개념이 있습니다. hash ring이 우리가 의도한 것과 다르게 구성되는 경우가 언제가 될까요? 바로… Hash Ring을 구성하는 서버의 이름이 바뀌게 되는 경우입니다.

Consistent hashing을 많이 쓰는 libmemcached 의 경우 보통 서버 주소가 들어가게 됩니다. “1.1.1.1:11211”, “1.1.1.2:11211”, “1.1.1.3:11211” 그런데 이런 이름의 경우에 만약 1.1.1.2 서버가 문제가 있어서 새 장비를 받아야 하는데 그 장비가 1.1.1.4 의 ip를 가진다면? Hash Ring이 꼬여 버릴 수 있습니다. 이런 문제를 해결하려면 위의 직접적인 이름 대신에 alias 한 다른 이름으로 Consistent Hashing 을 구성해야 합니다. 즉 redis001, redis002, redis003, redis004 이런 이름으로 Hash Ring을 구성하고, 서버가 바뀌더라도 이 이름을 사용하고 Consistent hashing의 결과로 가져올 값만 다르게 가져오면 되는 것이죠.

바로 이해가 안되시더라도 곰곰히 생각해보시면 무릎을 딱 치시게 될껍니다.


[입 개발] 아는 사람은 알지만 모르는 사람은 모르는 memcached expire 이슈…

$
0
0

Memcached는 아주 유명한 오픈소스 인메모리 캐시 솔루션입니다. 많은 사람들이 사용하기에 이미 그 사용법이 굉장히 많이 알려져있습니다. 그런데, 자주 사용하던 사람들이 아니면 잘 모르는 이상한 동작이 memcached 에는 하나 있습니다. 그것이 바로 expire 입니다.

보통 expire를 Memcached 에 셋팅할때는 second 단위로, expire 되어야 할 상대 시간을 넣습니다. 즉 대부분 예상은 아래와 같이 생각하게 됩니다.

expected expire time = current time + set expire time

그리고 이것은 아주 잘 동작합니다. 우리가 30일 이상 expire time 을 설정하기 전까지는…

넵… 주변에서 expire time 을 30일 이상, 즉 60 * 60 * 24 * 30 이상으로 설정하기 전까지는 잘 쓰다가, 왜 30일 이상으로 설정하면 데이터가 바로 사라져서 정상 동작하지 않는다라고 말씀하시는 분들이 속출합니다. 물론 이게 30일인지 아는것도 시간이 걸린다는… 미안해… 에단…

사실 이 문제는 memcached를 오래 다뤄부신 분들은 대부분 한번씩 겪어보는 장애(?) 또는 현상입니다. 왜냐하면 신기하게도 memcached 는 30일이 넘어가는 값에 대해서는 해당 값이 절대 시간이라고 처리를 해버립니다. 왜냐고 묻지 말아주세요. 제가 만든건 아니라서…

먼저 expire 를 처리하게 되는 items.c 안의 do_item_get 을 살펴보도록 하겠습니다. item이 expire 가 되는 경우는 주로 item 에 접근하게 될 때, 시간을 체크합니다.

item *do_item_get(const char *key, const size_t nkey, const uint32_t hv, conn *c) {
    ......

    if (it != NULL) {
        was_found = 1;
        if (item_is_flushed(it)) {
            do_item_unlink(it, hv);
            do_item_remove(it);
            it = NULL;
            pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.get_flushed++;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            if (settings.verbose > 2) {
                fprintf(stderr, " -nuked by flush");
            }
            was_found = 2;
        } else if (it->exptime != 0 && it->exptime <= current_time) { do_item_unlink(it, hv); do_item_remove(it); it = NULL; pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.get_expired++;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            if (settings.verbose > 2) {
                fprintf(stderr, " -nuked by expire");
            }
            was_found = 3;
        } else {
            it->it_flags |= ITEM_FETCHED|ITEM_ACTIVE;
            DEBUG_REFCNT(it, '+');
        }
    }
    ......
}

코드를 보면 it->exptime 이 0이 아니고 it->exptime REALTIME_MAXDELTA 라는 조건이 있습니다. 그리고 그 값은 위에 606024*30 으로 정의되어 있습니다. 즉 30일이죠.

#define REALTIME_MAXDELTA 60*60*24*30

static rel_time_t realtime(const time_t exptime) {
    if (exptime == 0) return 0;

    if (exptime > REALTIME_MAXDELTA) {
        if (exptime <= process_started)
            return (rel_time_t)1;
        return (rel_time_t)(exptime - process_started);
    } else {
        return (rel_time_t)(exptime + current_time);
    }
}

위의 코드를 보면 exptime 이 0일때는 값을 0으로 리턴합니다. exptime이 0일 때는 expire time이 설정되지 않은 아이템입니다. 이제 그 다음 코드를 보시면 exptime > REALTIME_MAXDELTA 라는 조건이 있습니다. 그리고 그 값은 위에 606024*30 으로 정의되어 있습니다. 즉 30일이죠.

그래서 설정한 exptime이 30일이 넘어가면, extpime <= process_started 조건에 만족하면 그냥 1로 셋팅합니다. 즉 1초 뒤에 다 지워지는 겁니다. process_started 는 프로세스가 처음에 시작한 시간입니다. 그리고 REALTIME_MAXDELTA 보다 적으면, current_time 에 exptime을 저장합니다. 즉 상대시간으로 인식이 되는 거죠.

그리고 마지막으로 current_time은 clock_handler 함수에서 설정되어집니다. 아래와 같이 current_time 에서는 이미 process_started 가 빠져 있습니다.

static void clock_handler(const int fd, const short which, void *arg) {
……
struct timeval tv;
gettimeofday(&tv, NULL);
current_time = (rel_time_t) (tv.tv_sec – process_started);
……
}

그럼 30일 이상의 값을 셋팅하고 싶을 때는 어떻게 해야 할까요? 넵 바로 그날짜에 맞는 시간값을 넣어주시면 됩니다. 예를 들어 지금부터 한달 뒤인 2016/11/22 에 맞는 unixtimestamp 로 설정하시면 됩니다. 즉 오늘 날짜를 구해서 거기에 원하는 날짜 만큼 더한 unixtimestamp로 expire time을 설정하시면 제대로 된 expire를 설정할 수 있습니다.

자 이제, memcached의 expire time을 설정할때는 항상 주의하셔야 합니다. 반대로 Redis 는 그냥 -_- 정한 값이 expire time 입니다. Redis 에서는 이런 이슈는 없습니다.


[입 개발] base62와 진법 연산

$
0
0

혹시 shorten url 서비스 같은 것을 어떻게 구현할 것인가에 대해서 고민해 본적이 있으신가요?
이런 서비스를 제공할려고 보면, 겹치지 않는 유니크한 값을 만들어야 합니다. 이건[입 개발] Global Unique Object ID 생성 방법에 대한 정리 를 참고하시면 됩니다.

그런데 이런 값을 그냥 스트링 형태로 표현하면, 123456789 은 binary 로는 4byte 이지만, 문자로 표현하면 9byte가 사용됩니다. 그렇다고 바이너로 표현하면 눈에 보이지 않는 형태이므로, 뭔가 전달하기가 어렵습니다.

123456789 이 각각 1,2,3,4,5,6,7,8 이 한바이트이므로, 이걸 뭔가 줄이는 방법이 없을까요?
이진수로 11111111은 16진수로 표현하면 FF 입니다. 그냥 스트링으로만 보면 8byte 가 2바이트로 줄었습니다. 그러나 123456789를 16진수 스트링으로 표현하면 0x075BCD15 가 됩니다.(Big Endian 입니다.)

그래도 9bytes 가 8bytes로 한바이트가 줄었습니다. 뭔가 더 줄이는 방법이 없을까요? 16진법으로 좀 줄었으니… 진법을 좀 올리면 어떨까요? 대략 62진법 정도로? 그럼 이걸 어떻게 표현해야 할까요?(base62를 설명하기 위한 이 대놓고 설정이라니..)

자 먼저 간단한 예를 들어봅시다. 12233 이라는 값을 62진법으로 표현하기 위해서는 어떻게 해야할까요?

1) 몫은 197, 나머지는 19
      197
   |-----
 62|12233
    12214
    -----
       19

2) 몫은 3, 나머지는 11
        3
   |-----
 62|  197
      186
    -----
       11

3) 몫은 0, 나머지는 3
        0
   |-----
 62|    3
        0
    -----
        3

4) 계산하면 3 * 62^2 + 11 * 62^1 + 19 * 62^0 = 12233 이 됩니다.

즉 12233 은 62진법으로 [3, 11, 19] 로 표현이 됩니다.
그럼 이 값을 이제 각 자리르 62진법으로 표시하기 위한 symbol로 변환해주면 62진법처럼 보일껍니다.

CODEC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

운좋게도 a-z,A-Z,0-9까지를 합치면 62글자가 됩니다. 각각 0부터 61까지 표현한다고 하면…
[3, 11, 19]는 CODEC[3], CODEC[11], CODEC[19]가 됩니다. 그러면 결과는 간단히 dlt 가 됩니다.

그러면 위의 공식대로 간단하게 코드를 작성해 볼까요?
3, 11, 19 는 실제로 나머지(mod) 라고 보시면 됩니다.

def to_base62(v):
    ret = []
    while True:
        idx = v % 62
        v /= 62
        ret.insert(0,CODEC[idx])
        if v == 0:
            break

    return ''.join(ret)

그럼 다시 디코딩은 어떻게 할 수 있을까요? 반대로 하면 됩니다. 문자를 위의 CODEC에서의 위치에서 찾아서, 그 값 곱하기 62^자리수 승을 해주면 됩니다.

CODEC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
CODECMAP = {}

c = 0
for i in CODEC:
    CODECMAP[i] = c
    c += 1

def from_base62(v):
    ret = 0
    i = 0
    for s in reversed(v):
        ret += (pow(62, i) * CODECMAP[s])
        i += 1

    return ret

이제 전체코드를 볼가요?

import sys

CODEC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
CODECMAP = {}

c = 0
for i in CODEC:
    CODECMAP[i] = c
    c += 1

def to_base62(v):
    ret = []
    while True:
        idx = v % 62
        v /= 62
        ret.insert(0,CODEC[idx])
        if v == 0:
            break

    return ''.join(ret)


def from_base62(v):
    ret = 0
    i = 0
    for s in reversed(v):
        ret += (pow(62, i) * CODECMAP[s])
        i += 1

    return ret

r = to_base62(int(sys.argv[1]))
v = from_base62(r)
print(r)
print(v)

우리가 이렇게 수를 특정 진법으로 표현할 수 있다는 것이 핵심입니다. 62진법이 중요한건 아니라는 거죠. 예를 들어, 대소문자를 구별할 수 없는 시스템이라면 이걸 줄여서 알파벳 26글자 + NUMERIC 10글자를 하면 36진법으로도 표현이 가능합니다. 숫자를 못쓰면 26진법도 가능한거죠. 자 이제 자기만의 진법 표현으로 숫자등을 한번 줄여보시기 바랍니다.


[입 개발] base64 가 있는데 base62 같은걸 왜 써야 하나요?

$
0
0

몇일 전에 [입 개발] base62와 진법 연산 라는 글을 적었습니다. 이런 내용을 얘기하면 꼭 빠지지 않고 좋은 질문이 하나 꼭 나옵니다.(나오기를 바랍니다.)

“왜 base64가 있는데 base62 같은걸 써야하죠?” 넵 그렇습니다. 다행히도, 이 내용을 제 주변에 설명했을 때도, 들었던 질문이고, 해당 글을 적었을 때도 받은 질문입니다.(좋은 질문해주신 질문자들에게 감사드립니다.)

먼저 간단하게 설명을 시작하기 전에, 10진수는 영어로 decimal 또는 base 10, 8진수는 octet digits 또는 base 8, 그럼 16진수는 넵 hexdecmial 또는 base 16 이 됩니다. 그럼 당연히 base62는 62진법, base64는 64진법이겠죠.

이 얘기를 하면, 위의 질문이 더 좋아집니다. 64진법으로 표현하는게, 62진법으로 표현하는 것 보다, 진법이 크니, 변화된 정보량이 더 작아지지 않는가? 라는 생각을 하게 하니깐요.

그런데 정답부터 말하자면, base64의 정보량이 더 줄어드는것이 맞습니다.(엥 작성자 양반 도대체 무슨 소리를 하는것이오…) 그런데, 이것을 항상 쓸 수 있는가? 라고 물어보면… 그렇지는 않기 때문입니다.(작성자 양반 이것은 또 무슨소리오!!!)

일단 binary로 표현하는 것은 일종의 256진법 표기입니다. base62, 64에 비해서 한 바이트에 많은 정보가 함축되지요. 그런데, 이제 다음과 같은 질문이 추가로 나와야 할듯 합니다. base64는 왜 나왔을까요? 아무데나 다 쓸 수 있나요? 아래는 base64에서 사용하기 위한 인코딩 표입니다. base62와 비교하면, 사실 +,/,= 해서 3개를 더 쓰고 있습니다.(마지막에 =는 사실 padding 입니다. 값이 없다라는 것을 알려주기 위해서이죠.)

base64

자, 이제 다시 한번 질문드립니다. 위의 base64 문자표에 있는 값으로 구성을 하면, 웹 url 형태의 query string으로 넘겼을 때 제대로 처리가 될까요? 자자 열심히 머리를 굴려봅시다.(이 질문을 하는 이유는… 심지어 이 글을 쓰는 이유는 여기서 뭔가 제대로 처리가 되지 않기 때문이겠죠?

https://charsyam.wordpress.com/abc?q===query=abcd+/=

위의 query string ㅔ서 q== 이 key 이고 query=abcd+/= 가 value라고 하면 뭔가 이상합니다. 하지만, key와 value 가 모두 base64로 인코딩이 된다면, 가능한 일입니다. 그럼 이제 패드는 안쓴다고 해봅시다. 그래도 다음과 같은 형태는 가능합니다.

https://charsyam.wordpress.com/abc?q+/+=query+/

그래서 url safe base64 라는 형태를 찾아보면 위의 표에서 ‘+’, ‘/’ 를 각각 ‘-‘,’_’ 등으로 바꾸고, ‘=’도 ‘.’ 이나 다른 문자로 바꾸는 경우가 있습니다. 즉 base64의 테이블표를 변경해야 하는 이슈가 생기는 것입니다.

실제로 base64는 이메일에서 안전하게 메일을  보내기 위한 인코딩 방법으로 출발했습니다. (rfc1341 를 참고하세요.)예전에는 ascii 만이 세상의 표준이므로 대부분의 서비스들이 7bit 까지만 인식하고 8bit로 된 데이터는 뭔가 처리하는데 에러가 있는 시대였습니다. 그래서 우리의 EUC-KR로 표현되던 한글이나 2byte 언어 국가 CJK 같은 경우는 메일로 첨부파일도 못보내고, 더 심한건, 그냥 한글로는 메일을 못보낸다는 것입니다.(EUC-KR 등에서는 한글 표현을 위해서 2byte를 사용하는데, 확장 표시를 위해서 두 byte의 첫 bit를 1로 셋팅했습니다.) 그래서 여기서 좀 안전한 방법을 찾자가 quoted-printed 와 base64 가 나오게 되었습니다.(quoted-printed는 url-encode 와 유사하게 128보다 큰 문자는 %AB 이런식으로 3글자로 표시하는 방식입니다.)

위와 같은 이유로 base64가 나오게 된 것입니다. 경우에 따라서 테이블을 변경해서 쓰거나 해야 안전해 지는 것이죠. 그런데… base64에서 사용하는 문자들을 또 특정한 시스템에서 쓸 수 없다면 어떻게 될까요? 8bit 표현을 6bit 로 줄인것 처럼, 64로 표시할 수 있는 데이터를 다시 더 줄여야 할 필요가 생길 수 있습니다. base62 또한 그런 상황에서 필요가 되어서 만들어 진 것이죠. 예를 들어 web에서 사용하는데 벌써 ‘-‘, ‘_’ 는 예약 문자등으로 쓰여서 쓸 수 없거나 하는…

그래서 base62, base36, base26, base10 등 얼마든지 만들어 나갈 수 있습니다. encoding, decoding 이라는 것은, 이러한 상황에서의 문제를 풀기 위해서고, 이런 문제는 얼마든지 다시 발생할 수 있으니까요. 설명이 되었으면 좋겠습니다.


[입 개발] Google Cloud Engine 에 Redis 설치하기

$
0
0
  • 해당 글은 Google Cloud Engine 로 부터 테스트 지원을 받아서 작성되었습니다.

Redis는 In-Memory Cache/Store 입니다. 또는 In-Memory Key-Value NoSQL 로 불리기도 합니다. 사실 어떻게 불리는가는 특별히 중요하지 않습니다. 굉장히 여러 분야에서, 다양하게 사용되고 있다는게 중요합니다.

그런데 Amazon AWS, Microsoft Azure 의 경우에는 Redis 는 아예 PaaS 형태로 존재하고 있습니다. AWS의 Elastic Cache 라든지, Azure 의 Redis Cache 가 있습니다. (아마도 GCE에서도 뭔가 곧 나오지 않을까 예상합니다. 여기서는 서로 서로 점점 유사해지고 있으니…)

이렇게 PaaS로 제공하는 것은 각각 장점이 있습니다. 편한 대신에, 세밀한 컨트롤이 안된다든가, 반대로 불편한 대신에 좀더 컨트롤이 명확하든가… Elastic Cache의 경우에는 아예 몇가지 쓸 수 없는 명령이 있어서 spring data redis에서 뭔가 이슈가 있기도 합니다.(지금은 수정되었나 모르겠네요.)

일단은 아주 간단하게 최신버전의 Redis를 GCE 에서 설치해보고 간단하게 테스트를 돌려보는 것 까지 확인해 보도록 하겠습니다. 환경은 Ubuntu 16.04 LTS 이지만 어디서든 거의 비슷한 형태로 하시면 가능합니다.

먼저 간단하게 살피고 넘어가면 ssh로 접근하기 위한 키를 생성합니다. 제 환경은 맥이지만, 비슷하니 키 생성은 쉽게 될껍니다. 이 때 주의할 것은 메일 주소를 실제로 사용하는 google 계정으로 하는게 좋은거 같습니다.(뭔가 제 잘못이겠지만, 그냥 public key를 등록했더니 제대로 안되는…)

ssh-keygen -t rsa -C "&lt;구글 계정&gt;"

그리고 이 ssh 키를 전역으로 사용하기 위해서, SSH 키를 미리 등록해둬야 합니다. 아래 그림과 같이 메타데이터 -> SSH 키 -> 수정 을 선택합니다.

gce-ssh-image

그리고 로컬에 생성한 (따로 이름을 지정안했으면 ~/.ssh/id_rsa.pub) public key 파일을 열어서 추가해줍니다. 그럼 자동으로 사용자 이름이 만들어지면서 추가됩니다. 그러면 이제 저장을 누르고 VM 인스턴스 -> 인스턴스 만들기 로 이동합니다.

여기서 해당 서버를 외부에서 바로 접속하고 싶다면, 꼭 네트워크 설정에서 외부를 임시든 고정 IP를 설정해 줘야 합니다. 셋팅안하면 그냥 없음으로 해서 내부 ip만 만들어지는…(물론 이것도 제가 잘못아는 거일 수도 있습니다.)

gce-vm-create

이제 인스턴스가 만들어지면 ssh -i ~/.ssh/id_rsa @ 로 접속하면 됩니다. 여기서 username 은 위에서 만들어진 사용자 이름을 사용하시면 됩니다.

GCE의 IO는 vCPU 수에 비례한다고 합니다. 네트워크도 vCPU에 비례해서 밴드위스가 추가된다고 하네요 이 내용은 benchmark 돌릴때 상당히 중요할듯 합니다.

자 이제 VM 을 생성했으니 여기서 끝내겠습니다. 다들 수고하셨습니다.(퍽퍽퍽)
앗, 그러고보니 이 글의 목표는 Redis 를 까는 거였습니다. -_-;;;
VM 인스턴스를 선택하면 자신의 서버 주소를 알 수 있습니다.
먼저 기본적인 툴들을 설치해야 합니다.

sudo apt update
sudo apt install build-essential libtool tcl

이제 Redis 최신 버전을 받아봅시다.

wget http://download.redis.io/releases/redis-3.2.6.tar.gz

압축을 풀고 빌드를 해봅니다.

tar zxvf redis-3.2.6.tar.gz
cd redis-3.2.6
make

Redis 는 생각보다 빌드가 굉장히 쉽습니다. 필요한 컴파일러만 설치되면 그냥 make 만 하시면됩니다. 이제 테스트를 해보겠습니다. test를 위해서는 tcl 이 필요하고 그래서 위에서 tcl 를 설치해두었습니다.

make test

이제 정상적이면 다음과 같은 로그를 보실 수 있습니다. 정상적으로 완료되었으면 문제는 없습니다. 가끔씩 에러가 날수도 있는데, 타이밍 이슈등이므로, 메모리 검사를 해보고 큰 문제가 없다면 무시하셔도 됩니다.

Testing integration/replication-4
[ok]: BRPOPLPUSH with wrong destination type
[ok]: BRPOPLPUSH maintains order of elements after failure
[ok]: BRPOPLPUSH with multiple blocked clients
[ok]: Linked BRPOPLPUSH
[ok]: Circular BRPOPLPUSH
[ok]: Self-referential BRPOPLPUSH
[ok]: BRPOPLPUSH inside a transaction
[ok]: PUSH resulting from BRPOPLPUSH affect WATCH

그런데 redis 를 실행해놓고 외부에서 접속을 시도해보면 당연히 접속이 안될껍니다. 왜 그럴까요?(일단 bind 는 0.0.0.0 으로 설정해 둔 다음에도요.) 이것은 해당 GCE의 네트웍 방화벽 설정에 6379 port 가 안열려 있어서 그렇습니다. 이걸 풀어주시면 제대로 설정이 될겁니다.

일단은 GCE(Google Cloud Engine) 에서 Redis 를 수동으로 설치하는 방법에 대해서 알아보았습니다. 다음번에는 실제로 이걸로 셋팅을 하고 서비스를 위한 테스트를 어떻게 할지 살펴보도록 하겠습니다.



2016년 회고와 2017년 계획

$
0
0

이제 2016년이 정말로 얼마남지 않았다. 이제 곧 2017년…(흑흑흑 나이먹기 싫어요.)
과연 나는 2016년 한해 무엇을 했을까? 먼저 2016년 계획을 찾아보았다.

그런데 검색결과 없다. -_-(그렇다 나의 2016년 계획 따위는 없었던 것이다!!! – 망했어!!!)

그럼 나는 무엇으로 2016년을 회고할 것인가!!!

  1. 인생의 슬픔… 둘째 유산…10월 24일… 뭔가 제대로 확인되지도 못하고 사라진 율율구리 two는 뭔가 슬픔이라는 감정을 느끼기도 전에 무언가 일이 벌어졌던거 같다. 태동을 듣지 못한게, 개인적으로 다행이다 싶기도한… 나보다도 마님이 더 충격적이지 않을까 했는데, 또 그렇게 아무런 기억없이 사라지는… 그냥 뭔가 미안한 마음뿐이다.
  2. RedisConf 2016
    올 한해 일단 가장 큰 기억은 역시 5월 10~11 일에 있었던 redisconf 2016에 참여한 것이다. 미친척 하고 proposal을 던지고, 그게 된지도 모르고 떨어졌다라고 자괴감에 빠져있다가 발표 10일전에 해당 메일을 찾게되서… 부랴부랴 준비했던… 이때 저에게 도움주신 많은 분들에게 감사를… 샌프란에서 몇일 동안 긴장해서 잠도 못자고, 내 발표 끝나고 나서는 한동안 정신을 못차린… 그러나 그 결과는 이제 평생 놀림감으로 남을 유투브 동영상이라니… 그래도 나름 열심히 하긴 했던…
  3. 안식휴가 9월에 한달
    카카오에는 만3년을 다니면 한달을 쉴 수 있는 안식휴가 제도가 있다. 마님과 율율구리와 함께, 한달을 지내는데, 제주도에서 일주일, 부산에서 일주일 정도 지내면서, 뭔가 직장을 다니면서, 안다니는거 같은 느낌을 받았다. 물론 이 시기에도, 버그도 내고, 배포도 하고, 장애도 내는 신기를… 그래도 한달이라는 쉬는 기간은 웬지…

그럼 이제 2017년에는 무엇을 할것인가? 일단 뭔가 2016년에 벌린 일들을 수습하고 새롭게 진행해야 하는데…

  1. 건강
    매년 건강검진때 마다 의사선생님들께 욕 한바가지 먹게 되는게 건강 상태다. 2017년에는 몸무게도 10kg 정도 빼고, 운동을 열심히 해서, 최소한 2016년 올해보다는 좋은 건강상태를 만드는게 1순위
  2. 영어 공부
    매년 얘기하면서도 매년 못하는 영어 공부는 올해는 강제로라도 해보자.
  3. 머신러닝 학습
    올 해는 머신러닝에 대해서도 살짝 공부는 해봐야 겠다.

[책 리뷰] 파이썬 머신 러닝

$
0
0

해당 리뷰는 지앤선에서 도서를 제공해주셔서 진행하였습니다.

머신러닝이라는 것은 용어가 예전의 클라우드, 빅데이터를 처럼 버즈워드로 시작했다가 어느 순간부터는 대부분의 사람이 알아야 하는 필수가 되어 버렸다. 알파고를 넘어서 “딥러닝”, “강화학습”, 어느 순간 GAN이라는 게 나와서, 스스로 대결해서 스스로 학습해버리는…. 스카이넷이 얼마 남지 않은…

사실 말하기 부끄럽지만, 나름 꽤 많은 머신 러닝 책을 표지만 보고 지나간 사람으로… 내가 공부하기엔 너무 어려운게 아닌가 라는 생각을 여전히 가지고 있습니다. 나름 쉽게 설명하는 머신러닝 강의도 찾아다니고 책도 보는데, 왜 이렇게 어려울까 하는데… 제가 수학이 약했던…

그 중에서 원래 원서가 굉장히 소문이 좋았던 Packt사의 Python Machine Learning 이라는 책이 한국어로 번역이 나와서, 리뷰를 할 수 있는 좋은 기회를 얻었습니다.

 

일단 책의 내용이 굉장히 탄탄합니다. 그러면서 좀 쉽게(여기서의 쉽게는 나름 쉽게고 실제로 아예 여기 관련 내용을 모른다면 꽤 이해하기 어려운… 그래서 제가 잘 이해못하는…) 설명을 하고 있습니다. 실제로 여러 부분을 다루고 있구요. Python 으로 진행하면서, 머신러닝이라는 분야에 대해서 여러가지로 잘 설명하고 있습니다. (전 원래 베이지안이 전부인줄만 알았는데…)

기존의 선형회귀, 로지스틱 회귀, SVM, 에이다 부스팅, K-Means, 딥러닝의 CNN 과 RNN 이야기도 나옵니다.(전 이걸 설명할 능력이 없는…)

사실 이 책의 난이도가, 아무런 배경지식이 없으면 읽기 어렵다고 말씀드릴 수 있습니다. 저도 상당부분은 사실 여전히 이해를 못하고(상당부분 == 거의 다) 그냥 이런게 있구나로만… 머신러닝 책들은 왜 보기만 해도 어느 순간… 눈을 감고 있게 되는지…(기본적으로 수학을 공부하셔야 잘 이해가 될것 같습니다.)

책을 보다보면, 쉬운 부분도 있고, 많이 어려운 부분들도 있는데, 이게 사람마다 체감하는게 완전히 다를 수 있을듯 합니다. 중간에 역전파(백프로파게이션) 을 이해하는데도 한참 걸린… 흑흑흑

그러나 머신러닝은 정말 개발자가 최소한의 지식은 꼭 있어야 할 분야로 보입니다. 다만 이 책은 완전히 초급에서 보기는 그렇고, 최소한 용어가 이해된 중급 수준에서 보면 상당한 도움이 될듯합니다. 한국판 서적중에서 가장 자세한 편입니다.

 


[용어 정리] 입개발자를 위한 Accuracy, Recall, Precision

$
0
0

최근에 공부하게 된 내용을 아주 가볍게 정리하고자 합니다. 머신러닝은 못하고 러닝머신도 못하고 있지만(저질 체력이라…) 맨날 공부하자 말만 하고 모르고 있다가… 아는 게 없어서 맨날 구라만 치는 중입니다. 그러던 중, 위의 내용들을 가볍게 설명할 일이 생겼는데… 역시 저의 구라로 시작한 일은 비극적으로 구라가 들통나버리는… 흑흑흑

그래서 좀 더 큰데서 구라를 다시 한번 치기 위해서 용어를 정리합니다. 흑흑흑 그래요 저 이런것도 모릅니다.

table

(해당 그림은 wikipedia 에서 가져왔습니다.)

다들 아시겠지만, 저는 잘 모르니… 먼저 간단하게 정리합니다. 일단 다음 4개의 용어를 먼저 기억해야 합니다.

True Positivie(TP) True 인데, True라고 맞춘 경우(잘한 경우)
False Positive(FP) False 인데, True라고 한 경우(틀렸어요.)
True Negative(TN) False 인데, False라고 맞춘 경우(잘한 경우)
False Negative(FN) True 인데 False 라고 한 경우(틀렸어요.)

TP, TN은 잘 한 경우, FP, FN은 잘못한 경우입니다. 그런데 FP, FN 중에 뭐가 낫냐고 하면, 그건 Case By Case 입니다. 예를 들어 암인데, 암이 아니라고 진단하거나, 암이 아닌데 암이 라고 진단하는 케이스는 어떤 경우가 더 나쁠까요?

그럼 이제 Accuracy, Recall, Precision 에 대해서 알아보도록 하겠습니다. 먼저 Accuracy 는 굉장히 간단합니다. 명확하게 정확도입니다. 정확도라고 생각하면, 즉 전체 중에서 정답을 얼마나 맞춰는가죠.  위의 표를 보시면 total population 이라고 되어 있는데, 그냥 위의 TP+FP+TN+FN, 즉 다 더한겁니다. 즉 전체 합 분의 잘 찾은 경우 즉 TP+TN이 되는 것이죠. 그래서 Accurancy 는 TP+TN/TP+TN+FP+FN 이 됩니다. 간단하죠? 간단하게 말하면, 전체 케이스 중에 정확하게 맞춘 비율입니다.

먼저 precision 은 검출한 것의 정확도라고 할 수 있습니다. 그냥 정확도라고 하면 위의 Accuracy 와 혼동이 오게 되는데, 위의 공식을 보면 TP/Prediction Positive 라고 되어있습니다. Prediction Positive 와 condition positive 가 표에 나오는데 Prediction Positive는 분류를 True 라고 말하는 케이스, Condition Positive 는 실제로 True 인 케이스입니다. 즉 Prediction Positive 는 위의 표에서 TP + FP 가 되구요, Condition Positive 는 TP+FN 이 됩니다. 위의 그림대로입니다. 다시 precision으로 돌아와서 간단하게 TP/TP+FP 입니다. 즉 True라고 분류했으면, 진짜 True 일 확률입니다.

그럼 이제 Recall 은 무엇인가? 검출율이라고 설명하면 쉬운데, 위의 공식을 보면 TP/condition positive 입니다. 즉 TP/TP+FN, 즉 진짜 True 중에 내가 얼마나 TRUE를 제대로 맞췄는가 라고 말할 것인가에 대한 값입니다.

이제 아래의 그림대로 한번 계산을 해보도록 하면…

table2

Accuracy TP+TN/TP+TN+FP+FN 30420/33376 = 0.911
Precision TP/TP+FP 26455/27812 = 0.951
Recall TP/TP+FN 26455/28054 = 0.943

이제 어디가서 좀 아는척 좀 하면 되겠습니다.


[용어 정리] 입 개발자를 위한 TF-IDF

$
0
0

뭔가 아는척을 위해서 알아두면 좋은 단어중에 지난번에 언급했던 Accuracy, Recall, Precision 같은 것들이 있는데, 이것 말고도 알아두면 입 개발자로 아는 척 하기 좋은 단어가 있습니다. 바로 TF-IDF 인데요. 보통, 검색이나 다른쪽을 하시는 분들은 다들 잘 알고 있는 단어이기도 합니다.(개인적으로는 해당 강의 https://www.coursera.org/learn/ml-foundations 를 들으시길 추천합니다.)

그럼 일단 단어를 정리하면 TF-IDF 는 TF와 IDF의 합성어입니다.

TF Term Frequency, 문서에서 해당 단어가 얼마나 나왔는지를 나타내는 단어, 예를 들어,  이 문서에 “입개발”이 10번 나오면 입개발의 TF는 10이라고 할 수 있습니다. 다만 이런 값의 정의는 바꿀 수도 있습니다. 여러번 나와도 1이라고 정의할 수도 있고, 엄청 많은 값을 좀 줄이기 위해서 log 값을 씌우기도 합니다.
DF Document Frequency 입니다. TF는 한 문서에서 나타난 빈도라면, DF는 전체 문서들에서, 몇 개의 문서에 나타나는지에 대한 값입니다.  즉 이에 대한 수식은 대략 (해당 단어가 나타난 문서 수/ 전체 문서 수) 라고 보시면 됩니다.
IDF Inverse Docuemnt Frequency 입니다. DF의 역수를 취했다고 보시면 됩니다. 즉 (전체 문서 수/해당 단어가 나타난 문서 수) 입니다. 그런데 해당 단어가 있는 문서가 없을 수도 있으니 보통 분모에 1을 더해줘서(0 되지 말라고), 해서 (전체 문서 수/1 + 해당 단어가 나타난 문서 수)로 많이 표시합니다.

이제 여기서 중요한 것은 왜 IDF를 사용하는가 입니다. 검색이든, 문서의 유사도 검색을 할 때도 많이 사용하는데, 이런 것들을 할때 중요한 것은 해당 문서의 특징을 뽑아내는 거라고 할 수 있습니다.(지금부터 구라가 작열합니다!!)

먼저 문서의 유사도를 비교한다면 어떻게 할 수 있을까요? “머신러닝” 이렇게 외치시면, 일단 “러닝머신”을 한두시간 타 보시고요. 어려운 방법을 빼고 생각해보면… 단어가 얼마나 일치하는가 보면 될것 같습니다.

  1. 단어들을 모두 분리해서, 각 단어의 개수를 센다.
  2. 해당 단어들의 개수랑 얼마나 일치하는 지 살펴본다.
    1. 그런데 요 부분도 이해하기 어려울 수…

tfidf1

위의 그림을 보면 각 단어의 출현 빈도를 저장하고, 이 값들을 비교해서 다른 문서와 얼마나 유사한지 비교하게 됩니다.

tfidf2

위의 그림도 단순한 유사도를 구하는 예입니다. 여러 가지 방법이 있을 수 있습니다.(여기서는 문서에 많은 단어가 있으면, 그 유사도 값이 너무나 커버리는 이슈도 있어서, 이 값을 normalize 를 시켜야 하는데 이런건 일단 넘어가도록 하겠습니다. )

그런데 문서를 하나 본다면 일단 설명을 쉽게 하기 위해서 영어를 예로 들면, the, a, an, and, or, but 등등의 관사나 조사 같은 것들이 많이 들어있게 됩니다. 그런데 단순히 문서를 단어로만 나눠서 갯수로 비교를 한다면? 위의 기법을 써버리면 엄청 the 가 많아도 다들 비슷한 문서로 생각해 버리게 될겁니다. 그럼 어떤 방법이 있을까요? 간단하게 생각하기에…

  1. 저렇게 쓸모 없는 단어를 다 빼고 비교한다.
    1. 그럼에도 중요한 단어와 중요하지 않은 단어를 구분하지 못하는 문제가…
  2. 그냥 저렇게 중요하지 않을 단어들은 가중치를 낮게 주고 중요한 단어들은 가중치를 올려주자.

위의 두 방법중에, 1번은 꽤 명확한데, 2번은 그럼 중요한 단어를 어떻게 정할 것인가 하는 이슈가 생깁니다. 그런데 지금 이게 무슨 용어를 설명하는 걸까요? 네, 그렇습니다. TF-IDF!!!

즉, TF-IDF가 중요한 단어와 중요하지 않은 단어를 구분할 수 있는 방법인 것입니다. 여기서 일단 TF-IDF의 가정은, 특정 단어가, 해당 문서에서는 자주 출현하지만, 다른 문서에서는 많이 안나오면 중요한 단어일 것이다 라는 것입니다. 왜냐하면 다른 문서들에도 자주 나오는 거면, 아까 말한 the, a, an, of, and, or, but 같은 관사나 접속사등이 많을 것이기 때문입니다.

이제 뭔가 연관이 보이시나요? IDF의 (전체 문서 수/해당 단어가 나타난 문서 수)가 어떤 의미일까요? 즉 해당 단어가 적은 문서에 나타날 수록 IDF 가 커지게 됩니다. DF는 반대로, 해당 단어가 여러 문서에 나타날 수록 값이 커지는거구요.

이제 IDF를 구함으로써, 우리는 문서들 중에서는 적게 나타나는 단어를 찾을 수 있게 되었습니다. 그리고 해당 문서에서 중요한 단어는 TF로 구할 수 있기 때문에, 우리의 핵심 가정 – “해당 문서에서는 자주 나타나고, 전체 문서에는 적게 나타나는 단어”를 구하는 방법이 TF와 IDF를 곱하는 TF-IDF 가 되는 것입니다.

그런데 실제로 아까 제가 말한 공식으로 바로 쓰지는 않습니다. 왜냐하면 해당 값들이 천차 만별로 커지기 때문에, 로그를 씌운다든지, 제곱근을 구한다든지 그렇게 됩니다. 위키디피아에 꽤 설명이 잘 되어 있습니다. https://ko.wikipedia.org/wiki/TF-IDF


[혀로그래머 charsyam은 구라쟁이 #1] 샤딩은 쉬워요 샤딩하세요.

$
0
0

안녕하세요. 혀로그래머!!! charsyam 입니다.  민방위 훈련을 받다가, 나처럼 실력 없는 개발자는 스스로 갈궈야 한다라는 생각이 들어서, 스스로를 비판하고 까기 위한 글을 쓰기로 했습니다.

원래 제가 발표하는 주 대상이 주로 개발 경험이 많이 없는 학생, 주니어들을 대상으로 하다보니, 많은 구라를 포함하고 있습니다.(제가 발표때 마다 호구(?) 조사를 빙자해서 학력/경력을 물어보는 이유입니다.) 그 중에서 가장 대표적인 구라가 바로… 샤딩 쉬워요. 샤딩하세요 입니다.

사실 서비스가 성장해 나가다가 가장 문제가 되는 부분이 데이터의 폭발입니다. 서비스가 성장하는 데 API 서버가 문제라면 소위 Stateless 형태라면, 쉽게 확장이 가능합니다.(이런 얘기 할 때, 절대로, Stateless 하지 못한 경우는 언급하면 안됩니다. 구라가 깨집니다.) 그런데 데이터의 폭발로 인해서 DB서버가 늘어나야 한다고 하면, 뭔가 버거워 보입니다. 그래서 항상 서비스 시작 전에 이런 부분에 대해서 미리 고민하고 시작해라라고 말을 합니다.(딱, 여기까지만 얘기합니다.)

그러면서 화려하게 DB 샤딩 방법에는 Range, Module, Indexed 같은 방법이 있고, 각각의 특성이 있다고 하면서 섞어써도 된다는 말들을 화려하게 해줍니다. 이러면서 구라가 완성이 되는 거죠.

그런데…, 처음 서비스를 구축할 때 부터, 이런 부분을 고려하면 정말 좋은걸까요? 당연히 시간이 조금 더 걸립니다. 하지만, 뒤에는 좋긴 합니다. 처음부터 고려를 하고 시작한 서비스니깐요. 그리고 여기까지만 얘기하고 이제 조용히 종료를 해야죠.(구라는 언제나 진실과 함께 해야만  잘 먹혀듭니다.)

그런데, 이런 고민을 서비스를 시작할때만 할까요? 기존에 서비스를 쓰고 있던 업체들에게도 샤딩하세요 라고 얘기를 합니다.(실제로는 서비스하시는 분들은 제가 피해다닙니다. 구라가 걸릴까봐… 손목가지 날라갑니다.) 그런데 그게 그렇게 쉽게 가능할까요?

자, DB 한대, API 서버 한대로 운영중인 회사가 있다고 하겠습니다. 서비스가 성장하는게 보입니다. 이제 슬슬 DB 서버에 데이터가 꽉 찬거 처럼 보입니다. 그런데 지나가던 구라쟁이(?)가 샤딩하세요, 어렵지 않습니다. 이런 얘기를 합니다. 그걸 믿고, 이제 사장님은 개발자에게 샤딩을 하라고 시키시죠. 그거 쉽데, 이런 것들이 있고, 블라 블라 블라 하십니다.

아, 쉽겠구나 하면서, DB 로직을 살펴봅니다. 그런데… 어디서 join 문들이 보이기 시작하네요. 또 프로시저 같은것도 쓰고 있습니다. 슬슬 열불이 나기 시작합니다. 어떤 XX가 쉽다고 얘기했을까요?

잘 생각해보면 DB 서버 안에 테이블 여러개라면 join이 쉽게 가능합니다. 느리게 돌 수도 있지만, DB가 처리하는 것 만큼 효과적으로 짜기도 어렵습니다. 그런데… 샤딩을 하고 나니… 테이블이… 서로 다른 서버에 데이터를 나눠 가지고 있습니다. join 해야할 테이블도 분리되어 있습니다.

거기다가, 트랜잭션도 쓰고 있군요. 이제 부터 지옥이 펼쳐집니다. 사실 기술 세미나에서 이런 얘기를 쉽게 하지 않습니다. 사실 쉬운 방법도 없구요. 처음부터 시작하면서 이런걸 고민해도, 사실 쉽지 않을 수 있습니다. 다른 테이블 간의 트랜잭션도 없애야 하고, join도 다 로직단에서 처리를 해줘야 합니다. 처음 부터 그렇게 만들지 않았다면… 절대로 쉬운 일이 아닙니다. 이렇게 되면, 실제로 코드에 해당 실패에 대한 대응들이 다 들어가야 합니다. 일종의 보상 처리라고도 합니다만… 잘 짜도 실수의 여지는 항상 있습니다.(돈 많이 주고 오라클 쓰시는 방법도 있다고 합니다. 전 안써봐서 모릅니다.)

그럼 문제가 여기에만 있을까요? 아닙니다. 샤딩을 하면, 이제 장비가 추가되어야 할 때마다, 노력이 추가로 들어갑니다. 물론 어느정도 자동화가 가능합니다만, 꼭 모니터링이 필요한 일입니다. (이런 글을 참고하세요. http://gywn.net/2012/05/how_to_shard_big_data_in_tumblr/)

밑단의 데이터에서 뭔가 변경이 일어나는 것은 절대로 쉽지 않습니다. 다만 성장하다보면, 절대로 피해갈 수 없는 길이기도 합니다. 그리고 누가 샤딩이 쉽다라고 하면, 아 저 아재 구라쟁이구나 이렇게 보셔도 됩니다.(초천재일수도 있긴 합니다만…)

다음 편은, 캐시 멤캐시나 레디스 쓰세요. 쉬워요에 대한 구라를 파헤치도록 하겠습니다. 모두들 고운하루되세요.


[혀로그래머 charsyam은 구라쟁이 #2] 캐시 멤캐시나 레디스 쓰세요. 쉬워요

$
0
0

안녕하세요. 혀로그래머이자 구라쟁이 charsyam 입니다. 오늘은 지난 1편 [혀로그래머 charsyam은 구라쟁이 #1] 샤딩은 쉬워요 샤딩하세요. 의 뒤를 이은 2편 “캐시 멤캐시나 레디스 쓰세요. 쉬워요” 편입니다.

일단 캐시를 왜 쓰는걸까요? 캐시라는 의미는 원래는 연산이 오래 걸리는 작업이나, 레이턴시가 긴곳에서 뭔가 가져와야 할때, 그 시간을 위해서 보다 빠른 저장 장치나, 미리 결과를 저장해 놓고 전달하는 것을 말합니다.

즉, DB나 다른 API 서버에서 결과를 받아와야 하는 것이 느리면, 메모리나, 로컬에 미리 저장을 해놓고 그 결과를 던져주는 것으로 시간을 줄이는게 캐시의 역할입니다.

그런데 로컬에만 들고 있으면, 어떤 서버는 해당 값을 들고 있을 수도 있고, 또 어떤 서버는 없을 수도 있고, write가 발생하면 해당 캐시를 지워줘야 하는데…, 그럼 전체 서버에 해당 값을 지우라고 보내야 하니깐… 귀찮아서, 어느 서버에 데이터를 담아두고, 모두가 거기를 참조하게 됩니다.

이때, 보통 멤캐시나 레디스를 쓰게 됩니다. 남들이 많이 쓰니깐, 추천도 많이 받으니깐요. 현재는 보통 멤캐시보다 레디스를 더 많이 사용하게 됩니다. 이 이유를 설명하자면 복잡한데, 간단하게 설명하면, 레디스가 더 많은 기능을 제공해 줍니다. 그래서 사용하기에 더 편합니다.(레디스 말고 캐시로만 쓰실꺼면 Memcache 변형인 Arcus 같은 것도 좋습니다.)

사실, 이런 캐시가 도입되면 사실 여러가지로 편해집니다. 속도도 빨라지고, 알고리즘이 기본적으로 제공되는 것들을 이용하면, 구현도 좀 더 쉬워지고요. 실제로 거의 대부분(99.99999%) 의 웹서비스 업체에서는 멤캐시나, 레디스를 이용하고 있습니다. 아래를 보면 DB 앞에 멤캐시를 추가하고 나서 DB의 query 가 변경되는 실제 예입니다. select query 수가 1000 까지 오르던 것이 거의 100 근처까지 떨어진 것을 알 수 있습니다.

스크린샷 2017-05-28 오전 12.49.08.png

 

레디스나 멤캐시를 쓰다보면, 처음에는 모든 것이 좋습니다. 그래서 이런 글도 있습니다.

레디스와 함께한 시간 모두 눈부셨다.

날이 좋아서

날이 좋지 않아서

날이 적당해서

모든 날이 좋았다.

(드라마 레디스)

그런데 트래픽이 몰아치고,  메모리가 한계에 이르기 시작하면 새로운 세상이 펼쳐집니다. 보통 말하지 않는 것들이 모두 이 메모리의 이슈와 연관되어 있습니다.

먼저 첫번째 문제로, 캐시가 죽으면 어떻게 해야 할까요? 캐시니깐 죽어도 됩니다라고 말하면, 구라입니다. 트래픽이 적을 때는 큰 문제는 아닙니다. 자, 캐시가 무엇때문에 쓴다고 했었나요? 연산이 오래걸리거나, 시간이 긴 작업의 속도를 빠르게 제공하기 위해서 입니다. 그럼 다음과 같은 상황을 가정해봅시다.

  1. 현재 트래픽이 굉장히 높습니다.
  2. 캐시가 죽습니다.

그러면, 이제 기존의 트래픽이 어디로 가게될까요? 빙고, DB로 가게 됩니다.(사실입니까?) 그럼, 캐시가 전혀 없을 때, 이 DB가 버틸 수 있다면, 사실 큰 문제가 없습니다.(아깐 앞에선 구라라며, 여기서도 구라를…) 그런데 보통 캐시가 대부분의 처리를 담당하고 있었다면, 캐시 서버가 죽는 순간 모든 트래픽은 DB로 전달되고, DB가 못버티면, 서비스 장애가 발생하게 됩니다. 그러면 캐시 서버는 몇대를 두는 것이 정답일까요? 정답은…

 

정답은… 죽어도 디비에 큰 영향이 없을 수 만큼 두어야 합니다.(즉 케바케!!!, 죽어 퍽퍽퍽, 이런걸 답이라고…) 사실, 결국 이런건 적절히 서비스를 운영해보면서 바꿀 수 밖에 없습니다. 약간의 힌트가 있다면, 캐시 없이 버틸 수 있는 QPS가 어느정도인지 확인해보고, 캐시가 커버해주는 처리량등을 잘 확인해야 합니다.

그렇다면, 캐시 서버들이 죽지 않도록 만들면 되지 않을까요? 라는 질문을 할 수 있습니다. 그런데, 그러기 위해서 필요한 것들이 캐시 서버의 메모리 관리입니다. 그런데 이 메모리 관리가 쉽지가 않습니다. 특히 멤캐쉬의 경우는 좀 문제가 적은데, 레디스의 메모리 관리는 많은 노력이 필요합니다.

일단 레디스는 메모리 파편화가 발생하기 쉽습니다. 그로 인해서 항상 레디스의 RSS 실제 물리 메모리 사용량을 모니터링 하다가, 어느 수준의 증가가 보이면, 다른 장비로 이전을 시켜줘야 합니다. 그럼 이 과정은 쉽냐?, 크게 어렵지는 않지만, 또한 실패할 가능성도 있습니다. 그리고, 이를 위해서는 결국 서버 아키텍처가 이런 변화를 쉽게 대응할 수 있도록 해줘야 합니다. 다른 장비로 이전을 한다면, 기존 서버의 도메인이나 ip를 새로운 장비가 가지도록 하거나, 쉽게 시그널을 보내면, 기존 장비 대신에 새로운 서버로 연결되도록 미리 만들어 두어야 합니다.(하지만 실제로 이러기도 쉽지는 않습니다.)

또한 죽지 않더라도, 인메모리 캐시들은 스왑메모리 영역을 사용하게 되면, 그때부터 성능이 많이 떨어지게 되는데, 멤캐시나 레디스를 쓰다보면, 특정 상황에 스왑메모리를 사용하게 되는 경우가 발생합니다. 이렇게 되면 해당 캐시서비를 리스타트 하지 않는 이상 해당 메모리는 계속 스왑을 쓸 수도 있기 때문에, 특정 상황에서 성능이 저하될 수 있습니다.

 

정리하자면, 레디스나 멤캐시를 사용하는 것은, 쉽고 좋습니다. 하지만, 이를 많이 사용하게 되었을 때 부터의 관리 이슈는, 상당히 심각합니다. 이를 해결하지 못하면, 도리어 레디스나 멤캐시를 사용하는 것이 심각한 문제의 원인이 됩니다.

 

재밌는 것은 많이 쓰는 곳은 외국의 큰 회사들도 같은 문제를 가지고 있습니다. 1대, 2대가 아니라, 몇십대, 아니면 몇백대, 아니면 몇천대를 쓰는 곳에서는 이런 이슈들을 어떻게 다루어야 할까요? 거기에 대한 많은 고민이 필요합니다.


[혀로그래머 charsyam은 구라쟁이 Q&A] 레디스 관련 Q&A

$
0
0

안녕하세요. 혀로그래머 구라쟁이 charsyam 입니다. 오늘은 제가 자주 서식하는 페북 커뮤니티에 질문을 누군가 올려주셔서 거기에 대한 답변을 간단하게 달아놓은 것을… 질문이 워낙 좋으셔서… 정리해 봤습니다.

먼저 질문은 다음과 같습니다.

  1. 인프라 구조에서 Scale-Out 구조를 가진 경우 각 데이터를 어떤 Node에 저장되고 있는지를 판별하고 있어야 하며 데이터 유실을 대비하여 데이터 블럭을 보통 분리하여 저장합니다. 레디스의 경우 한 노드가 죽었을때 휘발성인 캐쉬임을 대비하여 어떤 방식을 구현하는지요?
  2. 본문에 사용량이 많아지면 메모리 파편화가 일어난다고 하였는데(보통 디스크나 메모리의 경우 영속성이 있어야 성능이 잘나오는걸로 알고 있습니다.) 해당 파편화를 줄이는 알고리즘이나 파편화가 일어난 경우 해당 데이터를 재배치를 하는건가요?
  3. 서버 아키텍처는 캐쉬의 경우 여러 종류의 캐쉬를 두어 각 캐쉬별 역할을 구분하게 되어지는데 레디스도 그런 방식을 차용하고 있는건가요?
  4. 이슈를 대비하여 서버 대수만 늘려야한다면 아키 설계가 어려울듯 한데 아키 설계는 보통 어떻게?
  5. 레디스 서버 한대 다운시 처리는 어케 하는지요

여기에 대한 답변을 다음과 같이 정리했습니다.

  1. 인프라 구조에서 Scale-Out 구조를 가진 경우 각 데이터를 어떤 Node에 저장되고 있는지를 판별하고 있어야 하며 데이터 유실을 대비하여 데이터 블럭을 보통 분리하여 저장합니다. 레디스의 경우 한 노드가 죽었을때 휘발성인 캐쉬임을 대비하여 어떤 방식을 구현하는지요?
    1. 글에서 언급한 것 처럼 그냥 버리는 케이스가 있습니다. 각 노드들에 데이터들이 날아가도 실제 DB에서 처리할 수 있는 정도라면… 예를 들어 한대 죽었을 때 10% 정도 부하가 올라가는데, 이 정도는 원래 처리할 수 있다면, 무시해도 되겠죠.
    2. 캐시도 중요할 경우 Master/Slave 로 레디스 같은 경우 설정해 둘 수 있습니다. 멤캐시는 이게 안되서, 따로 리플리케이션을 구현하셔야 합니다.(Mysql Binlog를 이용하든지, 서버 로직에서 두 군데를 쓰든지…)
  2. 본문에 사용량이 많아지면 메모리 파편화가 일어난다고 하였는데(보통 디스크나 메모리의 경우 영속성이 있어야 성능이 잘나오는걸로 알고 있습니다.) 해당 파편화를 줄이는 알고리즘이나 파편화가 일어난 경우 해당 데이터를 재배치를 하는건가요?
    1. 레디스의 메모리 파편화는 다른 장비로 이전하는 수 밖에 없습니다. 보통 메모리 파편화가, 잦은 메모리 할당과 해제로 인해서 발생하므로, 장비 이전을 하면, 삽입만 대량으로 발생하니, 단편화 이슈가 조금 덜합니다. 보통 이런 경우 메모리를 2배로 늘린 장비로 이전합니다. 이전 과정은 간단하지만, 모니터링이 필요합니다.
    2. 말씀하신것 처럼 해당 데이터를 재배치 하는 것은 현재 레디스 상황에서는 쉽지는 않습니다. 재배치를 해봐도, 메모리 상황이 바로 좋아지는 것이 아니라, jemalloc에서 내부적으로 관리하는 매커니즘과 섞여서, 좀 외부에서 알기 어렵습니다.
  3. 서버 아키텍처는 캐쉬의 경우 여러 종류의 캐쉬를 두어 각 캐쉬별 역할을 구분하게 되어지는데 레디스도 그런 방식을 차용하고 있는건가요?
    1. 레디스가 내부적으로 그렇게 나누는 것은 아니고, 캐시를 사용하는 비지니스 로직에서 보통은 종류를 나눠서 사용하는 것이 제 경험상 메모리 사용량이나 파편화면에서 유리했습니다.(보통 그렇게 많이 쓰구요.)
    2. 통합 캐시(그냥 다 때려박는 형태)의 경우는 아이템별 메모리 사이즈의 차이가 커서 파편화를 더 가속화 시키는 측면이 있습니다.
  4. 이슈를 대비하여 서버 대수만 늘려야한다면 아키 설계가 어려울듯 한데 아키 설계는 보통 어떻게?
    1. 클라우드냐, 자체 IDC냐에 따라서 고려해야 할 것들이 좀 바뀝니다. 일단, 공통적으로 서비스의 Configuration이 Dynamic 할 수 있어야 합니다. 뭘 쓰는지는 크게 중요하지 않지만, 서비스를 내리고 올리는 형태가 아니라, 특정 보드에 설정을 바꾸면, 그게 전체 서버에 자동으로 반영되서, 서비스를 중단하지 않을 수 있어야 합니다. 서버의 추가나 제거도 마찬가지 입니다.
    2. 그런 아키텍처가 구성이 되면, 이제 IDC냐 클라우드냐에 따라서 고민할 것이 네트웍 밴드위스와 상면의 이슈가 있습니다. 개발자 입장에서는 네트웍 스위치의 밴드위스를 고려하지 않는 경우가 있는데, 이럴 경우, 큰 문제를 일으킬 수 있습니다. 상면 위치도 마찬가지입니다. 미리 잘 고민 안하면, 장비가 추가가 안되서, 해당 캐시만 다른 IDC에 넣어야 하는 경우도…(레이턴시가…)
  5. 레디스 서버 한대 다운시 처리는 어케 하는지요
    1. 레디스 서버 한대 다운시는… 여러 가지 방법이 있습니다. 자동 failover를 원하시면 sentinel 을 쓰든지 자체로 간단한 agent 를 만들어서 하는 방법이 있습니다. 이걸 vip, dynamic dns랑 잘 활요하면 클라이언트 입장에서는 크게 신경을 쓰지 않게 auto failover 를 제공할 수도 있습니다.


[입 개발] I don’t know DNS Caching

$
0
0

흐음, 입 개발 전문가 CharSyam  입니다. 나름 입 개발을 오래해보긴 하고, DNS 프로토콜도 직접 구현해보고, Dynamic DNS를 Zookeeper 기반으로 만들어보기도 해서 잘 안다고(이렇게 적고 실제로는 일도 모른다고 읽으시면 됩니다.) 생각했는데… 제 상식을 깨는 일이 발생했습니다.(다른 분들의 상식이 아니라 제 상식이니 무시하시면 됩니다.)

아래와 같은 코드가 존재합니다. 여기서 http://www.naver.com 은 몇번 호출이 될까요?

import requests
requests.get('http://www.naver.com')
requests.get('http://www.naver.com')

 한번도 안한다. 한번만 한다. 두번 한다. 세번 한다. 네번 한다. 정답은 설정과 OS에 따라 다르다입니다.(퍽퍽퍽, 죽어!!!) 디폴트 설정이라는 가정하에서는 Windows와 Linux 에서의 설정이 또 다릅니다. 이게 무슨 소리냐 하면 Windows 는 OS 레벨에서의 DNS가 캐싱이 되므로 아마, 위의 코드는 한번도 안할 수도 있습니다.(이전에 했다면…), 처음 실행된다는 가정하에서는 그럼 한번만 되겠죠. 그런데 여기서는 이제 Linux 쪽, 특히 Debian 계열로 한정을 짓는다면, 위의 코드는 4번의 DNS 호출을 하게 됩니다.(왜 두번이 아니라… 그것은 http://www.naver.comhttps://www.naver.com 으로 redirection 되기 때문에~~~), 그런데 엉? 4번이라고? 처음이라도 한번만 되어야 되는게 아니야?

먼저 tcpdump 를 설치합니다.

sudo apt-get install tcpdump

그리고 udp 53번을 모니터링 합니다.

sudo tcpdump -i eth0 udp port 53

그리고 위의 코드를 실행시키면… 다음과 같은 결과가 나옵니다.

01:32:31.303838 IP 192.168.0.2.60630 > acns.uplus.co.kr.domain: 30077+ A? www.naver.com. (31)
01:32:31.303846 IP 192.168.0.2.60630 > pcns.bora.net.domain: 30077+ A? www.naver.com. (31)
01:32:31.303856 IP 192.168.0.2.60630 > pcns.bora.net.domain: 45919+ AAAA? www.naver.com. (31)
01:32:31.306473 IP pcns.bora.net.domain > 192.168.0.2.60630: 30077 3/3/3 CNAME www.naver.com.nheos.com., A 210.89.160.88, A 210.89.164.90 (199)
01:32:31.306983 IP acns.uplus.co.kr.domain > 192.168.0.2.60630: 30077 3/3/3 CNAME www.naver.com.nheos.com., A 125.209.222.142, A 210.89.164.90 (199)
01:32:31.311150 IP pcns.bora.net.domain > 192.168.0.2.60630: 45919 1/1/0 CNAME www.naver.com.nheos.com. (116)
01:33:08.638991 IP 192.168.0.2.60630 > acns.uplus.co.kr.domain: 31222+ A? www.naver.com. (31)
01:33:08.638999 IP 192.168.0.2.60630 > pcns.bora.net.domain: 31222+ A? www.naver.com. (31)
01:33:08.639010 IP 192.168.0.2.60630 > pcns.bora.net.domain: 64566+ AAAA? www.naver.com. (31)
01:33:08.642771 IP pcns.bora.net.domain > 192.168.0.2.60630: 64566 1/1/0 CNAME www.naver.com.nheos.com. (116)
01:33:08.642781 IP pcns.bora.net.domain > 192.168.0.2.60630: 31222 3/3/3 CNAME www.naver.com.nheos.com., A 125.209.222.141, A 210.89.160.88 (199)
01:33:08.643297 IP acns.uplus.co.kr.domain > 192.168.0.2.60630: 31222 3/3/3 CNAME www.naver.com.nheos.com., A 125.209.222.141, A 210.89.160.88 (199)
......

우리는 OS 레벨의 DNS Caching을 기대하지만, 이 얘기는 기본적으로 OS 레벨에서의 DNS 캐시가 꺼져있다라는 얘기가 됩니다.(대부분의 linux에서 꺼져있다라는…) DNS Level Failover를 적용할려고 하다가, Python 에서는 DNS Caching 이 어떻게 이루어지는지를 보려고 하다보니…, 우연히 https://stackoverflow.com/questions/11020027/dns-caching-in-linux 를 찾게 되었는데…(역시 갓 SO, 참고로 JVM 에서는 DNS Caching 이 영구적이라, 특정 옵션을 주지 않으면, 처음 받게 된 ip를 계속 사용하게 되므로, 기본 옵션으로는 DNS Level Failover를 쓸 수 없습니다.)

여기서 “꺼져있다”, “꺼져있다”, “꺼져있다”를 보고 충격을 받았습니다. 그래서 위와 같이 실험을 했더니… 사실이었습니다. 자세한 내용은 위의 SO 글을 읽으시면… 잘 알게 되는데, glibc의 getaddrinfo 자체에서 발생하는 이슈라고 해서, 다음과 같이 코드를 실행해 봤습니다.

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    struct addrinfo hints;
    struct addrinfo *result;

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = 0;
    hints.ai_protocol = 0;

    for (int i = 0; i < atoi(argv[1]); i++) {
        int s = getaddrinfo("www.naver.com", NULL, &hints, &result);
        if (s != 0) {
            fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
            exit(-1);
        }
        printf("%d\n", i);
        sleep(10);
    }
    return 0;
}

실행해보면, 매번 쿼리가 날라가는 것을 볼 수 있습니다. 즉, 이 이야기는, DNS Caching을 app 수준에서 따로 제어하지 않는다면, DNS 쿼리를 매번 호출하게 된다라는 얘기가 됩니다. 보통 DNS 쿼리를 굉장히 자주 날리는 것은 성능상 문제를 일으킬 수 있습니다. 실제로 이걸 잘 하는게 쉽지는 않을듯 하네요.

glibc 코드를 살짝 까보면, getaddrinfo 는 gaih_inet 이라는 함수를 호출해서 결과를 가져옵니다. gaih_inet 는 USE_NSCD가 켜져있으면 NSCD에서 캐시된 결과를 찾는 것으로 보이고, 그게 아니라면, 일단 hosts 파일에 있는지 체크합니다. 이래서 hosts에 등록하면 DNS 쿼리 없이 항상 제일 먼저 가져오게 됩니다. 그 뒤에 옵션이나 상황에 따라, gethostbyname2_r, gethostbyname3_r, gethostbyname4_r 을 콜하게 됩니다. 이 함수들은 실제로 resolv/nss_dns/dns-host.c 에 있는 _nss_dns_gethostbynameX_r 함수들과 매핑이 되고 여기서 DNS Query를 날리고 가져오게 됩니다.

즉, 이 얘기는 OS단에서 해주는게 없을 가능성이 높고, 지금 DNS 쿼리는 여전히 발생하고 있을지 모른다는 얘기다 됩니다.(일단 저는 이렇게 이해했는데… 잘 아시는 분 답변좀 T.T), 이걸 해결하기 위해서는 nscd 를 설치해야만, OS 레벨의 캐싱이 적용이 되게 됩니다. 아니면, app 레벨에서 직접 해줘야…

일단 자바의 경우는 jvm 레벨에서 DNS 캐싱이 적용되어 있습니다. 그래서 DNS Based Failover를 하려면 좀 더 자세히 알아야 합니다. jdk 소스를 보면 InetAddress.java, InetAddressCachePolicy.java 이 있습니다. InetAddress.java 에서 InetAddressCachePolicy 클래스를 사용하는데, 여기의 기본 옵션이 FOREVER 입니다. 그리고 InetAddress 에서 getCachedAddresses 함수를 호출하면서, 매번 위의 Policy를 확인하고, 위의 값들을 조절하면 networkaddress.cache.ttl 는 내부적으로 사용할 TTL 의 값(가져와서 ttl 확인하고 expire 시키네요.) 두 definition networkaddress.cache.ttl 이나 sun.net.inetaddr.ttl 을 먼저 체크해서 하나라도 값이 있으면, 해당 값으로 설정이 되고 없으면 SecurityManager를 체크하는데 이때 SecurityManager도 없으면 기본 TTL이 30초로 설정되게 됩니다.

[입 개발] DNS Caching in JVM

$
0
0

다음과 같은 오류가 발견되어서 정정합니다. 현재는 JVM에서의 DNS Caching 이 30초입니다.
자바6 이후로는 계속 그렇게 설정되어 있는듯 합니다. 흑흑흑 나는 왜 지금까지 그 옵션을 열심히 사용했을까요?
그러나 흐름 자체는 도움이 될듯하여 내용은 수정해서 남겨둡니다. 알려주신 역촋 정상혁님께 감사를

JVM 에서 (혹은 Java) 에서는 DNS Caching 이 디폴트로 FOREVER 입니다. 이 말은 한번 쿼리한 DNS 주소는 다시는 쿼리하지 않는다는 것입니다. 이러면 당연히 DNS 쿼리 시간이 들지 않으니, 속도면에서는 유리하지만, DNS 변화를 주는 것으로 뭔가 처리할려고 하면, 결국 계속 실패하게 됩니다.  이전 ip를 계속 써버리니… 그럼 이 동작을 어떻게 확인하는가? JDK 소스를 까보시면 간단하게 아실 수 있습니다.

./src/share/classes/java/net/InetAddress.java
./src/share/classes/sun/net/InetAddressCachePolicy.java

InetAddress class 는 getAllByName0 함수가 불려질 때 먼저 cache 되어있는지를 getCachedAddresses 함수를 통해서 확인합니다. 아래 코드르 보면 cache 에 없을때만 실제 DNS 질의를 하게 됩니다.

    private static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)
        throws UnknownHostException  {

        /* If it gets here it is presumed to be a hostname */
        /* Cache.get can return: null, unknownAddress, or InetAddress[] */

        /* make sure the connection to the host is allowed, before we
         * give out a hostname
         */
        if (check) {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkConnect(host, -1);
            }
        }

        InetAddress[] addresses = getCachedAddresses(host);

        /* If no entry in cache, then do the host lookup */
        if (addresses == null) {
            addresses = getAddressesFromNameService(host, reqAddr);
        }

        if (addresses == unknown_array)
            throw new UnknownHostException(host);

        return addresses.clone();
}

그리고 getCachedAddresses 함수는 InetAddressCachePolicy 가 FOREVER일 때는 해당 값을 expire 시키지 않습니다.

private int getPolicy() {
    if (type == Type.Positive) {
        return InetAddressCachePolicy.get();
    } else {
        return InetAddressCachePolicy.getNegative();
   }
}

public CacheEntry get(String host) {
    int policy = getPolicy();
    if (policy == InetAddressCachePolicy.NEVER) {
        return null;
    }
    CacheEntry entry = cache.get(host);
 
    // check if entry has expired
    if (entry != null && policy != InetAddressCachePolicy.FOREVER) {
        if (entry.expiration >= 0 &&
            entry.expiration < System.currentTimeMillis()) {
            cache.remove(host);
            entry = null;
        }
    }
 
    return entry;
}

따로 설정을 하지 않으면 InetAddressCachePolicy 는 디폴트로 FOREVER 입니다.

// Controls the cache policy for successful lookups only
private static final String cachePolicyProp = "networkaddress.cache.ttl";
private static final String cachePolicyPropFallback =
    "sun.net.inetaddr.ttl";
 
// Controls the cache policy for negative lookups only
private static final String negativeCachePolicyProp =
    "networkaddress.cache.negative.ttl";
private static final String negativeCachePolicyPropFallback =
    "sun.net.inetaddr.negative.ttl";
 
public static final int FOREVER = -1;
public static final int NEVER = 0;
 
private static int cachePolicy = FOREVER;
 
public static synchronized int get() {
    return cachePolicy;
}

그리고 해당 값은 Security Property 와 System Property에 의해서 제어가 가능합니다.

스크린샷 2017-12-27 오후 5.28.32

단위는 초고, 초가 자동으로 밀리세컨으로 변경됩니다.

long expiration;
if (policy == InetAddressCachePolicy.FOREVER) {
    expiration = -1;
} else {
    expiration = System.currentTimeMillis() + (policy * 1000);
}

우선순위는 networkaddress.cache.ttl 가 있으면 이걸 먼저 사용하고, 없으면 그 다음에 sun.net.inetaddr.negative.ttl 을 사용합니다. 즉 Security Property 설정이 우선입니다.
아래 한글 주석을 확인하면 아무 옵션이 없을 때, SecurityManager 가 설정되어 있지 않으면 30초로 설정되게 됩니다.

Integer tmp = java.security.AccessController.doPrivileged(
  new PrivilegedAction<Integer>() {
    public Integer run() {
        try {
            String tmpString = Security.getProperty(cachePolicyProp);
            if (tmpString != null) {
                return Integer.valueOf(tmpString);
            }
        } catch (NumberFormatException ignored) {
            // Ignore
        }
 
        try {
            String tmpString = System.getProperty(cachePolicyPropFallback);
            if (tmpString != null) {
                return Integer.decode(tmpString);
            }
        } catch (NumberFormatException ignored) {
            // Ignore
        }
        return null;
    }
  });
if (tmp != null) {
    cachePolicy = tmp.intValue();
    if (cachePolicy < 0) {
        cachePolicy = FOREVER;
    }
    propertySet = true;
} else {
    /* SecurityManager 가 설정되어 있지 않으면, 여기서 cachePolicy 가 DEFAULT_POSITIVE 가
     * 30초로 설정됩니다.
     */
    /* No properties defined for positive caching. If there is no
     * security manager then use the default positive cache value.
     */
    if (System.getSecurityManager() == null) {
        cachePolicy = DEFAULT_POSITIVE;
    }
}

즉 위와 같이 해당 값을 설정하면 그 이후에는 캐시가 날라가서 다시 실제 쿼리를 날리게 됩니다. 보통 로컬에 dnsmasq 나 unbound 같은 로컬 DNS 캐시 서버를 둬서, 거기서 캐싱을 하면 실제적으로 외부로 날라가는 것보다는 훨씬 DNS 쿼리 비용을 줄일 수 있습니다.

참고문헌:
https://www.lesstif.com/pages/viewpage.action?pageId=17105897

[입 개발] IPv4 TCP Socket, Listen 에서 Accept 까지…

$
0
0

갑자기 초괴수 지인분이 TCP Socket 에서 Listen 하고 Accept 할 때 어떤 일이 벌어지는지에 대해서 궁금해 하시는 질문을 올리셨습니다. 사실 Accept 자체는 별로 하는게 없다라는 건 알고 있었는데, 실제로 그 사이에 어떤 일이 벌어지는지에 대해서는 저도 잘 모르고 있어서, 그냥 한번 살펴봤습니다. 먼저, 이걸 보기 전에 TCP의 Connection이 맺어지는 3-way handshake는 굉장히 유명하고 중요하니, 이미지를 도용해옵시다.

3whs

일단 위의 그림을 보면 client 가 connect 를 하기 전에 server 는 listen을 해둬야 합니다. 그럼 이 listen을 하는 동안 어떤 작업이 일어나게 될까요?(linux 4.12.2 기준입니다.)

먼저 봐야할 소스는 net/ipv4/af_inet.c 의 inet_stream_ops 설정입니다. 실제 c코드 등의 함수가 커널레벨에서는 이 함수들과 매핑이 된다고 보시면 됩니다. 여기서 listen 은 inet_listen, accept 은 inet_accept 이 설정되어 있는 것을 볼 수 있습니다.

const struct proto_ops inet_stream_ops = {
    .family        = PF_INET,
    .owner         = THIS_MODULE,
    .release       = inet_release,
    .bind          = inet_bind,
    .connect       = inet_stream_connect,
    .socketpair    = sock_no_socketpair,
    .accept        = inet_accept,
    .getname       = inet_getname,
    .poll          = tcp_poll,
    .ioctl         = inet_ioctl,
    .listen        = inet_listen,
    .shutdown      = inet_shutdown,
    .setsockopt    = sock_common_setsockopt,
    .getsockopt    = sock_common_getsockopt,
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    .mmap          = sock_no_mmap,
    .sendpage      = inet_sendpage,
    .splice_read       = tcp_splice_read,
    .read_sock     = tcp_read_sock,
    .peek_len      = tcp_peek_len,
#ifdef CONFIG_COMPAT
    .compat_setsockopt = compat_sock_common_setsockopt,
    .compat_getsockopt = compat_sock_common_getsockopt,
    .compat_ioctl      = inet_compat_ioctl,
#endif
};

그럼 이제 inet_listen 함수를 찾아봅니다. 코드를 보면 TCP_FASTOPEN 에 대한 처리도 있는데, 이 부분은 일단은 생략합니다. inet_listen 함수에서는 해당 socket 이 TCP_LISTEN 상태가 아니면 inet_csk_listen_start 함수를 호출하고 listen의 파라매터로 넘어오는 backlog 를 설정합니다.

/*
 *  Move a socket into listening state.
 */
int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err;

    lock_sock(sk);

    err = -EINVAL;
    if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
        goto out;

    old_state = sk->sk_state;
    if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
        goto out;

    /* Really, if the socket is already in listen state
     * we can only allow the backlog to be adjusted.
     */
    if (old_state != TCP_LISTEN) {
        /* Enable TFO w/o requiring TCP_FASTOPEN socket option.
         * Note that only TCP sockets (SOCK_STREAM) will reach here.
         * Also fastopen backlog may already been set via the option
         * because the socket was in TCP_LISTEN state previously but
         * was shutdown() rather than close().
         */
        if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
            (sysctl_tcp_fastopen & TFO_SERVER_ENABLE) &&
            !inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
            fastopen_queue_tune(sk, backlog);
            tcp_fastopen_init_key_once(true);
        }

        err = inet_csk_listen_start(sk, backlog);
        if (err)
            goto out;
    }
    sk->sk_max_ack_backlog = backlog;
    err = 0;

out:
    release_sock(sk);
    return err;
}

그럼 다시 inet_csk_listen_start 함수를 살펴봅니다 net/ipv4/inet_connection_sock.c 안에 있습니다. inet_csk_listen_start 함수에서 처음에 신경써서 볼 부분은 reqsk_queue_alloc 함수를 호출하는 부분입니다. 변수명이 뭔가 와 닫는가요? icsk_accept_queue 라는 이름으로 할당하고 있습니다. 네, 이것이 바로 TCP에서 실제 connect 하는 client 에 대한 연결 요청이 저장되는 queue 입니다. accept 에서는 여기에 있으면 바로 가져가고, 없으면 대기하게 되는거죠. 여기서 해당 포트를 확보하는데 문제가 발생하면 TCP_CLOSE 상태로 가게됩니다.

int inet_csk_listen_start(struct sock *sk, int backlog)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct inet_sock *inet = inet_sk(sk);
    int err = -EADDRINUSE;

    reqsk_queue_alloc(&icsk->icsk_accept_queue);

    sk->sk_max_ack_backlog = backlog;
    sk->sk_ack_backlog = 0;
    inet_csk_delack_init(sk);

    /* There is race window here: we announce ourselves listening,
     * but this transition is still not validated by get_port().
     * It is OK, because this socket enters to hash table only
     * after validation is complete.
     */
    sk_state_store(sk, TCP_LISTEN);
    if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
        inet->inet_sport = htons(inet->inet_num);

        sk_dst_reset(sk);
        err = sk->sk_prot->hash(sk);

        if (likely(!err))
            return 0;
    }

    sk->sk_state = TCP_CLOSE;
    return err;
}

이제 해당 socket 이 TCP_LISTEN 상태가 되었습니다. 그런데 젤 앞에 TCP 3-way handshake는 client 가 connect 함수를 호출하면서 SYN 패킷을 보내면서 부터 시작되게 됩니다. 이 부분은 어디서 처리하게 될까요? tcp_rcv_state_process 라는 함수가 net/ipv4/tcp_input.c 에 있습니다. 그런데 이 함수는 어디서 호출되는 것일까요? 다음과 같이 tcp_protocol 정의를 보면 실제 데이터를 처리하는 tcp_v4_rcv 라는 함수가 있습니다.

static struct net_protocol tcp_protocol = {
    .early_demux    =   tcp_v4_early_demux,
    .early_demux_handler =  tcp_v4_early_demux,
    .handler    =   tcp_v4_rcv,
    .err_handler    =   tcp_v4_err,
    .no_policy  =   1,
    .netns_ok   =   1,
    .icmp_strict_tag_validation = 1,
};

해당 socket 이 TCP_LISTEN 상태이면 다시 tcp_v4_do_rcv 라는 함수를 호출하게 되고 다시 tcp_child_process 함수를 호출하거나 하지 않더라도 최종적으로 tcp_rcv_state_process 함수를 호출하게 됩니다.(tcp_child_process가 호출되지 않아도 그 밑에 tcp_rcv_state_process가 호출됩니다.) TCP_LISTEN 인 경우를 보면, 앞에 TCP 3-way handshake를 한번 더 기억해야 합니다.

SYN -> SYN+ACK -> ACK 형태의 순서로 넘어가게 되는데, SYN과 ACK가 server 쪽에서 받게 되는 패킷입니다. ACK가 오면, 이제 ESTABLISH 가 되는 것이므로 여기서는 바로 return 1을 하게 됩니다. 그러면 실제로 SYN+ACK를 보내는 상황은 SYN을 받았을 때 입니다. FIN이 설정되어 있으면, TCP 접속을 종료하는 거니 discard 하게 되고, 정상적이면 conn_request 함수를 호출하게 됩니다.

    case TCP_LISTEN:
        if (th->ack)
            return 1;

        if (th->rst)
            goto discard;

        if (th->syn) {
            if (th->fin)
                goto discard;
            /* It is possible that we process SYN packets from backlog,
             * so we need to make sure to disable BH right there.
             */
            local_bh_disable();
            acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
            local_bh_enable();

            if (!acceptable)
                return 1;
            consume_skb(skb);
            return 0;
        }
        goto discard;

conn_request 함수는 다음 ipv4_specific를 살펴봐야 합니다. tcp_v4_conn_request 함수랑 매핑이 되어 있네요.

const struct inet_connection_sock_af_ops ipv4_specific = {
    .queue_xmit    = ip_queue_xmit,
    .send_check    = tcp_v4_send_check,
    .rebuild_header    = inet_sk_rebuild_header,
    .sk_rx_dst_set     = inet_sk_rx_dst_set,
    .conn_request      = tcp_v4_conn_request,
    .syn_recv_sock     = tcp_v4_syn_recv_sock,
    .net_header_len    = sizeof(struct iphdr),
    .setsockopt    = ip_setsockopt,
    .getsockopt    = ip_getsockopt,
    .addr2sockaddr     = inet_csk_addr2sockaddr,
    .sockaddr_len      = sizeof(struct sockaddr_in),
#ifdef CONFIG_COMPAT
    .compat_setsockopt = compat_ip_setsockopt,
    .compat_getsockopt = compat_ip_getsockopt,
#endif
    .mtu_reduced       = tcp_v4_mtu_reduced,
};

tcp_v4_conn_request 는 tcp_conn_request 라는 함수를 다시 호출합니다. TCP_FASTOPEN 이 아닐 때 보면 inet_csk_reqsk_queue_hash_add 를 호출하는데 이 함수가 실제로 accept_queue에 값을 집어넣는 함수입니다. 라고 생각했는데, 소스를 잘못 본것입니다. 여기서는 TIMEOUT만 설정하게 됩니다. 그리고 SYN+ACK를 보내고 되죠.

    if (fastopen_sk) {
        af_ops->send_synack(fastopen_sk, dst, &fl, req,
                    &foc, TCP_SYNACK_FASTOPEN);
        /* Add the child socket directly into the accept queue */
        inet_csk_reqsk_queue_add(sk, req, fastopen_sk);
        sk->sk_data_ready(sk);
        bh_unlock_sock(fastopen_sk);
        sock_put(fastopen_sk);
    } else {
        tcp_rsk(req)->tfo_listener = false;
        if (!want_cookie)
            inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
        af_ops->send_synack(sk, dst, &fl, req, &foc,
                    !want_cookie ? TCP_SYNACK_NORMAL :
                           TCP_SYNACK_COOKIE);
        if (want_cookie) {
            reqsk_free(req);
            return 0;
        }
    }

같은 tcp_conn_request 함수안의 앞부분을 보면 inet_reqsk_alloc 을 호출하는 부분이 있습니다.

int tcp_conn_request(struct request_sock_ops *rsk_ops,
             const struct tcp_request_sock_ops *af_ops,
             struct sock *sk, struct sk_buff *skb)
{
    struct tcp_fastopen_cookie foc = { .len = -1 };
    __u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
    struct tcp_options_received tmp_opt;
    struct tcp_sock *tp = tcp_sk(sk);
    struct net *net = sock_net(sk);
    struct sock *fastopen_sk = NULL;
    struct dst_entry *dst = NULL;
    struct request_sock *req;
    bool want_cookie = false;
    struct flowi fl;

    /* TW buckets are converted to open requests without
     * limitations, they conserve resources and peer is
     * evidently real one.
     */
    if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
         inet_csk_reqsk_queue_is_full(sk)) && !isn) {
        want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
        if (!want_cookie)
            goto drop;
    }

    if (sk_acceptq_is_full(sk)) {
        NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
        goto drop;
    }

    req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
    if (!req)
        goto drop;

    ......

inet_reqsk_alloc 함수를 보면 TCP_NEW_SYN_RECV 로 셋팅하는 부분이 있습니다. 그러면서 새로운 child 소켓을 생성하기 위한 준비를 하는 것으로 보입니다. TCP_NEW_SYN_RECV는 https://patchwork.ozlabs.org/patch/449704/ 를 보시면 왜 추가되었는지 설명이 나옵니다.(저도 잘 몰라요 ㅋㅋㅋ)

struct request_sock *inet_reqsk_alloc(const struct request_sock_ops *ops,
                      struct sock *sk_listener,
                      bool attach_listener)
{
    struct request_sock *req = reqsk_alloc(ops, sk_listener,
                           attach_listener);

    if (req) {
        struct inet_request_sock *ireq = inet_rsk(req);

        kmemcheck_annotate_bitfield(ireq, flags);
        ireq->opt = NULL;
#if IS_ENABLED(CONFIG_IPV6)
        ireq->pktopts = NULL;
#endif
        atomic64_set(&ireq->ir_cookie, 0);
        ireq->ireq_state = TCP_NEW_SYN_RECV;
        write_pnet(&ireq->ireq_net, sock_net(sk_listener));
        ireq->ireq_family = sk_listener->sk_family;
    }

    return req;
}

다시 처음의 tcp_v4_rcv 함수로 돌아갑니다.(net/ipv4/tcp_ipv4.c), 여기서 tcp_check_req 함수가 호출이 됩니다.(net/ipv4/tcp_minisocks.c)

    if (sk->sk_state == TCP_NEW_SYN_RECV) {
        struct request_sock *req = inet_reqsk(sk);
        struct sock *nsk;

        sk = req->rsk_listener;
        if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
            sk_drops_add(sk, skb);
            reqsk_put(req);
            goto discard_it;
        }
        if (unlikely(sk->sk_state != TCP_LISTEN)) {
            inet_csk_reqsk_queue_drop_and_put(sk, req);
            goto lookup;
        }
        /* We own a reference on the listener, increase it again
         * as we might lose it too soon.
         */
        sock_hold(sk);
        refcounted = true;
        nsk = tcp_check_req(sk, skb, req, false);
        if (!nsk) {
            reqsk_put(req);
            goto discard_and_relse;
        }
        if (nsk == sk) {
            reqsk_put(req);
        } else if (tcp_child_process(sk, nsk, skb)) {
            tcp_v4_send_reset(nsk, skb);
            goto discard_and_relse;
        } else {
            sock_put(sk);
            return 0;
        }
    }

tcp_check_req 에서는 뭔가 복잡한 작업을 하고 있습니다.(제가 이걸 보고 바로 이해할 능력은 안됩니다. 하하하하 T.T) 일단 SYN+ACK를 보내고 여기서 ACK를 받아야 정상적으로 연결이 완료되기 때문에, child 소켓이 만들어지고(accept 하면 server 소켓이 아니라 다른 소켓을 받게 되는거 기억나시죠?, tcp_v4_syn_recv_sock 함수에서 만들어집니다.) 마지막에 inet_csk_complete_hashdance 함수를 호출하면서

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req,
               bool fastopen)
{
     ......
     /* OK, ACK is valid, create big socket and
     * feed this segment to it. It will repeat all
     * the tests. THIS SEGMENT MUST MOVE SOCKET TO
     * ESTABLISHED STATE. If it will be dropped after
     * socket is created, wait for troubles.
     */
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
                             req, &own_req);
    if (!child)
        goto listen_overflow;

    sock_rps_save_rxhash(child, skb);
    tcp_synack_rtt_meas(child, req);
    return inet_csk_complete_hashdance(sk, child, req, own_req);

inet_csk_complete_hashdance 함수에서는 실제로 inet_csk_reqsk_queue_add 함수를 호출해서 실제로 accept_queue에 새로 생성된 child socket을 집어넣어줍니다.

struct sock *inet_csk_complete_hashdance(struct sock *sk, struct sock *child,
                     struct request_sock *req, bool own_req)
{
    if (own_req) {
        inet_csk_reqsk_queue_drop(sk, req);
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
        if (inet_csk_reqsk_queue_add(sk, req, child))
            return child;
    }
    /* Too bad, another child took ownership of the request, undo. */
    bh_unlock_sock(child);
    sock_put(child);
    return NULL;
}

지금까지 진행한 부분에서 새로 생성된 소켓을 TCP_ESTABLISHED 로 설정되는 부분이 안보입니다. 이건 어디서 할까요? 실제로 위에 살짝 빠지고 넘어간 tcp_v4_syn_recv_sock 함수를 살펴보면, 새로운 소켓을 만들기 위한 tcp_create_openreq_child 함수를 호출합니다. 여기서 다시 inet_csk_clone_lock 함수를 통해서 parent를 clone 하게 되는데 여기서 TCP_SYN_RECV 로 state가 설정되게 되고, 다시 tcp_rcv_state_process 에서 TCP_ESTABLISHED로 설정이 됩니다.(확실하지는 않습니다.)

이제 accept 이 호출되었을 때를 살펴보도록 하겠습니다. accept 은 처음에 inet_accept 함수를 호출하게 되고, 여기서 내부적으로 inet_csk_accept 을 호출하게 됩니다. 먼저 accept_queue를 확인해서 empty 이면, 하나도 존재하지 않으니, inet_csk_wait_for_connect 를 호출해서 내부적으로 대기하게 됩니다. O_NONBLOCK 이 설정되어 있으면 하나도 없을 때, 익숙한 -EAGAIN이 리턴됩니다.

/*
 * This will accept the next outstanding connection.
 */
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct request_sock *req;
    struct sock *newsk;
    int error;

    lock_sock(sk);

    /* We need to make sure that this socket is listening,
     * and that it has something pending.
     */
    error = -EINVAL;
    if (sk->sk_state != TCP_LISTEN)
        goto out_err;

    /* Find already established connection */
    if (reqsk_queue_empty(queue)) {
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

        /* If this is a non blocking socket don't sleep */
        error = -EAGAIN;
        if (!timeo)
            goto out_err;

        error = inet_csk_wait_for_connect(sk, timeo);
        if (error)
            goto out_err;
    }
    req = reqsk_queue_remove(queue, sk);
    newsk = req->sk;

    if (sk->sk_protocol == IPPROTO_TCP &&
        tcp_rsk(req)->tfo_listener) {
        spin_lock_bh(&queue->fastopenq.lock);
        if (tcp_rsk(req)->tfo_listener) {
            /* We are still waiting for the final ACK from 3WHS
             * so can't free req now. Instead, we set req->sk to
             * NULL to signify that the child socket is taken
             * so reqsk_fastopen_remove() will free the req
             * when 3WHS finishes (or is aborted).
             */
            req->sk = NULL;
            req = NULL;
        }
        spin_unlock_bh(&queue->fastopenq.lock);
    }
out:
    release_sock(sk);
    if (req)
        reqsk_put(req);
    return newsk;
out_err:
    newsk = NULL;
    req = NULL;
    *err = error;
    goto out;
}

reqsk_queue_remove 를 호출하면 실제로 accept_queue 에서 연결을 하나 가져오게 됩니다.(링크드 리스트에서 head를 가져옵니다.)

[입 개발] Python 3.3 부터는 hash 결과가 프로세스 마다 달라요!!!.

$
0
0

안녕하세요. 입개발자 charsyam 입니다. 아는 척, 있는 척 하기 위해서 예전에 만들었던 python 코드의 test 를 돌려봤는데… -_- 이게 웬일입니까… 테스트가 다 깨지는!!! 처음 만들었을 때는 분명히 돌아가는 테스트코드였는데… 이게 웬 일입니까…

일단 기본적으로 python 에는 hash 라는 built-in 함수가 존재합니다. 그런데 사실 이 hash 함수를 쓰는 것보다는, 명시적인 hashlib 함수를 사용하는 것을 권장합니다. 빌드에 따라 이 hash 함수가 바뀔 수도 있어서…

하여튼 편하게 가보겠다고 hash 함수를 사용했다가 버전이 2.7.x 에서 3.6.x를 쓰다가 피본 경험을 공유합니다. 먼저 증세를 살펴보면, 일단 프로세스가 기동된 상태에서는 결과는 항상 동일합니다. 그래서 항상 새로운 프로세스로 커맨드 라인에서 테스트를 진행합니다.

먼저 2.7.14의 결과입니다.

#2.7.14
python -c "print(hash('123'))"
163512108404620371
python -c "print(hash('123'))"
163512108404620371
python -c "print(hash('123'))"
163512108404620371
#3.6.x
python -c "print(hash('123'))"
8180009514858937698
python -c "print(hash('123'))"
-3358196339336188655
python -c "print(hash('123'))"
-5852881486981464238

일단 이것은 https://docs.python.org/3.3/using/cmdline.html 를 보면 Python 3.3.x 부터 기본적으로 다음과 같이 Hash Randomization 이라는 것이 들어갔다고 합니다. 이게 뭔지는 저는 몰라요, 며느리도 몰라요.

Kept for compatibility. On Python 3.3 and greater, hash randomization is turned on by default.

On previous versions of Python, this option turns on hash randomization, so that the __hash__() values of str, bytes and datetime are “salted” with an unpredictable random value. Although they remain constant within an individual Python process, they are not predictable between repeated invocations of Python.

Hash randomization is intended to provide protection against a denial-of-service caused by carefully-chosen inputs that exploit the worst case performance of a dict construction, O(n^2) complexity. See http://www.ocert.org/advisories/ocert-2011-003.html for details.

PYTHONHASHSEED allows you to set a fixed value for the hash seed secret.

보통 Hash의 값을 예상할 수 있으면, 특정 위치에만 데이터를 집어넣는 DDOS 공격이 가능한데, 이것을 막기 위한 것으로 보이고 그래서 보통 이런 것을 회피하기하기 위한 siphash를 3.x 부터 사용하는것으로 보입니다.(여담으로 Redis에서도 이런 DDOS를 막기위해서 siphash로 기존 hash 함수가 변경되었습니다.)

그럼 실제 어떻게 변화가 되었는지 2.7.14 기준으로 살펴보도록 하겠습니다.

2.7.14 분석

먼저 Python/bltinmodule.c 파일을 살펴보면 다음과 같은 builtin_methods 구조체를 발견할 수 있습니다. 이것은 python 에서 build-in(내장) 함수의 이름을 모아 놓는 것입니다.

static PyMethodDef builtin_methods[] = {
    ......
    {"hash",            builtin_hash,       METH_O, hash_doc},
    ......
}

그리고 builtin_methods 의 PyMethodDef 는 Include/methodobject.h 에 다음과 같이 정의되어 있습니다.

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction  ml_meth;   /* The C function that implements it */
    int      ml_flags;  /* Combination of METH_xxx flags, which mostly
                   describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

넵, 그렇습니다. 위에 있는 builtin_hash 라는 함수가 실제 hash 명령을 사용했을 때 실행되는 함수입니다. 이제 builtin_hash 함수를 찾아보도록 하겠습니다.(Python/bltinmodule.c)

static PyObject *
builtin_hash(PyObject *self, PyObject *v)
{
    long x;

    x = PyObject_Hash(v);
    if (x == -1)
        return NULL;

    return PyInt_FromLong(x);
}

결론적으로는 PyObject_Hash 함수를 호출합니다. 구조체 안의 tp_hash 라는 것이 실제 hash 함수를 담고 있습니다.

long
PyObject_Hash(PyObject *v)
{
    PyTypeObject *tp = v->ob_type;
    if (tp->tp_hash != NULL) {
        long r = (*tp->tp_hash)(v);
        return r;
    }
    /* To keep to the general practice that inheriting
     * solely from object in C code should work without
     * an explicit call to PyType_Ready, we implicitly call
     * PyType_Ready here and then check the tp_hash slot again
     */
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            return -1;
        if (tp->tp_hash != NULL)
            return (*tp->tp_hash)(v);
    }
    if (tp->tp_compare == NULL && RICHCOMPARE(tp) == NULL) {
        return _Py_HashPointer(v); /* Use address as hash value */
    }
    /* If there's a cmp but no hash defined, the object can't be hashed */
    return PyObject_HashNotImplemented(v);
}

그리고 string type 의 경우에는 저기서 tp_hash 가 string_hash 를 호출하게 됩니다. 재미난건 여기서도 _Py_HashSecret 이라는 것을 사용하고 있다는 것!!!(제가 참고한 버전이 2.7.14라서 이런 부분이 있을 수도 있지만… 더 자세한건 마음속에 있는 걸로… 귀찮아요!!!)

static long
string_hash(PyStringObject *a)
{
    register Py_ssize_t len;
    register unsigned char *p;
    register long x;

#ifdef Py_DEBUG
    assert(_Py_HashSecret_Initialized);
#endif
    if (a->ob_shash != -1)
        return a->ob_shash;
    len = Py_SIZE(a);
    /*
      We make the hash of the empty string be 0, rather than using
      (prefix ^ suffix), since this slightly obfuscates the hash secret
    */
    if (len == 0) {
        a->ob_shash = 0;
        return 0;
    }
    p = (unsigned char *) a->ob_sval;
    x = _Py_HashSecret.prefix;
    x ^= *p << 7;
    while (--len >= 0)
        x = (1000003*x) ^ *p++;
    x ^= Py_SIZE(a);
    x ^= _Py_HashSecret.suffix;
    if (x == -1)
        x = -2;
    a->ob_shash = x;
    return x;
}

저 _Py_HashSecret 은 다시 Python/random.c의 _PyRandom_Init() 에서 초기화 되게 됩니다. 이때 Py_HashRandomizationFlag 가 설정되어 있지 않아야 합니다. 코드는 간단하니… 알아서… PYTHONHASHSEED 가 설정되면 Py_HashRandomizationFlag 가 1로 셋팅됩니다.

void
_PyRandom_Init(void)
{
    char *env;
    void *secret = &_Py_HashSecret;
    Py_ssize_t secret_size = sizeof(_Py_HashSecret_t);

    if (_Py_HashSecret_Initialized)
        return;
    _Py_HashSecret_Initialized = 1;

    /*
      By default, hash randomization is disabled, and only
      enabled if PYTHONHASHSEED is set to non-empty or if
      "-R" is provided at the command line:
    */
    if (!Py_HashRandomizationFlag) {
        /* Disable the randomized hash: */
        memset(secret, 0, secret_size);
        return;
    }

    /*
      Hash randomization is enabled.  Generate a per-process secret,
      using PYTHONHASHSEED if provided.
    */

    env = Py_GETENV("PYTHONHASHSEED");
    if (env && *env != '\0' && strcmp(env, "random") != 0) {
        char *endptr = env;
        unsigned long seed;
        seed = strtoul(env, &endptr, 10);
        if (*endptr != '\0'
            || seed > 4294967295UL
            || (errno == ERANGE && seed == ULONG_MAX))
        {
            Py_FatalError("PYTHONHASHSEED must be \"random\" or an integer "
                          "in range [0; 4294967295]");
        }
        if (seed == 0) {
            /* disable the randomized hash */
            memset(secret, 0, secret_size);
        }
        else {
            lcg_urandom(seed, (unsigned char*)secret, secret_size);
        }
    }
    else {
#ifdef MS_WINDOWS
        (void)win32_urandom((unsigned char *)secret, secret_size, 0);
#elif __VMS
        vms_urandom((unsigned char *)secret, secret_size, 0);
#elif defined(PY_GETENTROPY)
        (void)py_getentropy(secret, secret_size, 1);
#else
        dev_urandom_noraise(secret, secret_size);
#endif
    }
}

하여튼 PYTHONHASHSEED 를 설정하지 않거나 0으로 설정해주면 Randomize가 적용이 되지 않습니다.

그럼 이제 3.6.x 에서는 어떻게 변했을까요? 전 이걸 github에서 땡긴거라… 정확한 버전은 잘 모르겠네요.(라고 하고 찾아보니 3.7.0a4+ 네요.)

3.7.0a4+ 분석

2.7.14 의 분석과 마찬가지로 함수 정의 부터 따라가 보도록 하겠습니다. 3.7.0 에서는 Python/bltinmodule.c 에 builtin_methods 에 builtin 함수들이 정의되어 있고 다시 Python/clinic/bltinmodule.c.h 에 BUILTIN_HASH_METHODDEF 이 다음과 같이 정의되어 있습니다.

#define BUILTIN_HASH_METHODDEF    \
    {"hash", (PyCFunction)builtin_hash, METH_O, builtin_hash__doc__},

여기서도 실제로 builtin_hash 와 연결되어 있으므로 해당 함수를 확인해 봅니다. builtin_hash 는 2.7.14와 크게 다르지 않습니다.

static PyObject *
builtin_hash(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=237668e9d7688db7 input=58c48be822bf9c54]*/
{
    Py_hash_t x;

    x = PyObject_Hash(obj);
    if (x == -1)
        return NULL;
    return PyLong_FromSsize_t(x);
}

친숙한 PyObject_Hash 가 보이네요. 코드도 거의 비슷하지만 사실은 아주 조금 줄어들었네요.

Py_hash_t
PyObject_Hash(PyObject *v)
{
    PyTypeObject *tp = Py_TYPE(v);
    if (tp->tp_hash != NULL)
        return (*tp->tp_hash)(v);
    /* To keep to the general practice that inheriting
     * solely from object in C code should work without
     * an explicit call to PyType_Ready, we implicitly call
     * PyType_Ready here and then check the tp_hash slot again
     */
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            return -1;
        if (tp->tp_hash != NULL)
            return (*tp->tp_hash)(v);
    }
    /* Otherwise, the object can't be hashed */
    return PyObject_HashNotImplemented(v);
}

Python 2.7 과 3.x의 가장 큰 차이라면 string 이 unicode가 되는 것인데, 여기서 보면, 2.7에서는 string_hash, 3.x에서는 unicode_hash를 호출하게 됩니다.

static Py_hash_t
unicode_hash(PyObject *self)
{
    Py_ssize_t len;
    Py_uhash_t x;  /* Unsigned for defined overflow behavior. */

#ifdef Py_DEBUG
    assert(_Py_HashSecret_Initialized);
#endif
    if (_PyUnicode_HASH(self) != -1)
        return _PyUnicode_HASH(self);
    if (PyUnicode_READY(self) == -1)
        return -1;
    len = PyUnicode_GET_LENGTH(self);
    /*
      We make the hash of the empty string be 0, rather than using
      (prefix ^ suffix), since this slightly obfuscates the hash secret
    */
    if (len == 0) {
        _PyUnicode_HASH(self) = 0;
        return 0;
    }
    x = _Py_HashBytes(PyUnicode_DATA(self),
                      PyUnicode_GET_LENGTH(self) * PyUnicode_KIND(self));
    _PyUnicode_HASH(self) = x;
    return x;
}

그리고 unicode_hash 는 조금 재미난 작업을 합니다. self 에 _PyUnicode_HASH 가 -1이 아니면 이미 자기 자신의 hash 값을 저장하고 있습니다. 그래서 값이 -1이 아니면 바로 전달하고, 그게 아니면 실제로 _Py_HashBytes 를 호출하게 됩니다.(PyUnicode_READY 는 뭔가 아주 복잡한 작업을 하지만… 패스…) 그리고 그 결과를 저장하게 됩니다.

Py_hash_t
_Py_HashBytes(const void *src, Py_ssize_t len)
{
    Py_hash_t x;
    /*
      We make the hash of the empty string be 0, rather than using
      (prefix ^ suffix), since this slightly obfuscates the hash secret
    */
    if (len == 0) {
        return 0;
    }

#ifdef Py_HASH_STATS
    hashstats[(len <= Py_HASH_STATS_MAX) ? len : 0]++;
#endif

#if Py_HASH_CUTOFF > 0
    if (len < Py_HASH_CUTOFF) {
        /* Optimize hashing of very small strings with inline DJBX33A. */
        Py_uhash_t hash;
        const unsigned char *p = src;
        hash = 5381; /* DJBX33A starts with 5381 */

        switch(len) {
            /* ((hash << 5) + hash) + *p == hash * 33 + *p */
            case 7: hash = ((hash << 5) + hash) + *p++; /* fallthrough */
            case 6: hash = ((hash << 5) + hash) + *p++; /* fallthrough */
            case 5: hash = ((hash << 5) + hash) + *p++; /* fallthrough */
            case 4: hash = ((hash << 5) + hash) + *p++; /* fallthrough */
            case 3: hash = ((hash << 5) + hash) + *p++; /* fallthrough */
            case 2: hash = ((hash << 5) + hash) + *p++; /* fallthrough */
            case 1: hash = ((hash << 5) + hash) + *p++; break;
            default:
                Py_UNREACHABLE();
        }
        hash ^= len;
        hash ^= (Py_uhash_t) _Py_HashSecret.djbx33a.suffix;
        x = (Py_hash_t)hash;
    }
    else
#endif /* Py_HASH_CUTOFF */
        x = PyHash_Func.hash(src, len);

    if (x == -1)
        return -2;
    return x;
}

중요한 부분만 보면 PyHash_Func.hash 이 코드가 됩니다. 그럼 PyHash_Func.hash는 어떻게 구성이 되는가?

다음과 같은 코드를 쉽게 찾을 수 있습니다. Py_HASH_ALGORITHM 가 무엇으로 설정되는가에 따라서 fnv 나 siphash24 로 설정이 되게 됩니다.(Python/pyhash.c)

typedef struct {
    Py_hash_t (*const hash)(const void *, Py_ssize_t);
    const char *name;
    const int hash_bits;
    const int seed_bits;
} PyHash_FuncDef;

#if Py_HASH_ALGORITHM == Py_HASH_FNV
static PyHash_FuncDef PyHash_Func = {fnv, "fnv", 8 * SIZEOF_PY_HASH_T,
                                     16 * SIZEOF_PY_HASH_T};
#endif

#if Py_HASH_ALGORITHM == Py_HASH_SIPHASH24
static PyHash_FuncDef PyHash_Func = {pysiphash, "siphash24", 64, 128};
#endif

특별한 옵션을 주지 않으면 일단 Py_HASH_SIPHASH24 로 설정이 되게 됩니다. configure 파일을 보면

  --with-hash-algorithm=[fnv|siphash24]
                          select hash algorithm

로 되어있고, 이게 명시되지 않으면, MEMORY ALIGN 이 필요한 CPU(또는 cross compile을 지정해야해서) 쪽에서는 fnv가, 그렇지 않은 저 같은 맥이나 일반 x86 계열에서는 siphash24 가 설정이 되게 됩니다.

PyHash_Func.hash 가 호출되면 pysiphash 가 실제로 불리게 됩니다. 파라매터를 잘 보면 _Py_HashSecret 에서 사용하는 siphash 관련 값들이 넘어가게 됩니다.

static Py_hash_t
pysiphash(const void *src, Py_ssize_t src_sz) {
    return (Py_hash_t)siphash24(
        _le64toh(_Py_HashSecret.siphash.k0), _le64toh(_Py_HashSecret.siphash.k1),
        src, src_sz);
}

그럼 이제 마지막으로 3.x 에서 이 _Py_HashSecret 를 설정하는지 보면 됩니다. 먼저 _Py_HashSecret_t 는 다음과 같이 구성됩니다.

typedef union {
    /* ensure 24 bytes */
    unsigned char uc[24];
    /* two Py_hash_t for FNV */
    struct {
        Py_hash_t prefix;
        Py_hash_t suffix;
    } fnv;
    /* two uint64 for SipHash24 */
    struct {
        uint64_t k0;
        uint64_t k1;
    } siphash;
    /* a different (!) Py_hash_t for small string optimization */
    struct {
        unsigned char padding[16];
        Py_hash_t suffix;
    } djbx33a;
    struct {
        unsigned char padding[16];
        Py_hash_t hashsalt;
    } expat;
} _Py_HashSecret_t;

실제 _Py_HashSecret 의 설정은 _Py_HashRandomization_Init 에서 이루어집니다. 제 맥에서는 _Py_HashRandomization_Init를 호출하는 call stack 은 다음과 같습니다. 이 이야기는 처음에 시작과 동시에 호출이 된다는 것입니다.

 * frame #0: 0x000000010027de16 python`_Py_HashRandomization_Init(config=0x00007fff5fbff778) at bootstrap_hash.c:569
    frame #1: 0x000000010026364f python`_Py_InitializeCore(core_config=0x00007fff5fbff778) at pylifecycle.c:649
    frame #2: 0x00000001002abd05 python`pymain_main(pymain=0x00007fff5fbff720) at main.c:2647
    frame #3: 0x00000001002abea7 python`_Py_UnixMain(argc=1, argv=0x00007fff5fbff8b8) at main.c:2695
    frame #4: 0x0000000100000e62 python`main(argc=1, argv=0x00007fff5fbff8b8) at python.c:15
    frame #5: 0x00007fffa8f62235 libdyld.dylib`start + 1
    frame #6: 0x00007fffa8f62235 libdyld.dylib`start + 1

_Py_HashRandomization_Init 는 Python/bootstrap_hash.c 에 있습니다. 2.7.14와의 차이는 2.7.14에서는 PYTHONHASHSEED 가 없으면 randomize 작업이 없지만, 3.x 에서는 PYTHONHASHSEED 가 설정되어 있으면 그 값으로 seed를, 없으면 pyurandom 을 호출해서 randomize 가 일어나게 된다는 것입니다.

_PyInitError
_Py_HashRandomization_Init(const _PyCoreConfig *config)
{
    void *secret = &_Py_HashSecret;
    Py_ssize_t secret_size = sizeof(_Py_HashSecret_t);

    if (_Py_HashSecret_Initialized) {
        return _Py_INIT_OK();
    }
    _Py_HashSecret_Initialized = 1;

    if (config->use_hash_seed) {
        if (config->hash_seed == 0) {
            /* disable the randomized hash */
            memset(secret, 0, secret_size);
        }
        else {
            /* use the specified hash seed */
            lcg_urandom(config->hash_seed, secret, secret_size);
        }
    }
    else {
        /* use a random hash seed */
        int res;

        /* _PyRandom_Init() is called very early in the Python initialization
           and so exceptions cannot be used (use raise=0).

           _PyRandom_Init() must not block Python initialization: call
           pyurandom() is non-blocking mode (blocking=0): see the PEP 524. */
        res = pyurandom(secret, secret_size, 0, 0);
        if (res < 0) {
            return _Py_INIT_USER_ERR("failed to get random numbers "
                                     "to initialize Python");
        }
    }
    return _Py_INIT_OK();
}

python 에서 hash randomize를 끄고 싶다면, 양 버전 모두 PYTHONHASHSEED 을 0으로 설정하는 것입니다. 하지만, 권장하는 것은 python 의 built-in hash 함수를 쓰지말고, 명시적으로 알 수 있는 hash 함수를 사용하는 것이 훨씬 좋다는 것입니다.(결론은 마지막 한줄!!!)

[입 생활] aws, github, 2FA 활성화나 수정 방법

$
0
0

이제 점점 더 귀찮아지지만 2 Factor Authentication 이 거의 필수처럼 여겨지고 있습니다. aws도 그렇고, github도, organization 에서 2FA가 안켜져 있으면 관리자가 계정 다 삭제해 버릴수도 있습니다. ㅋㅋㅋ

그런데 핸드폰은 2년마다 고장나고…(제껀 3년 만에…) 이럴 경우 이런 2FA를 바꿔야 하는데 aws나 github이나 다 쉽게 가능합니다만… -_-;;; 제가 워낙 바보라 저장해둡니다.

0] OPT 앱

구글 AUthentication 앱 사용

1] aws

my security credentials -> Multi-factor authentication (MFA) 를 선택해서 기존꺼를 삭제하고 추가하시면 됩니다.

2] Github

Settings -> Security -> Two-factor authentication -> Edit -> Delivery Option -> Reconfigure two-factor authentication -> Set up using an app -> Next 하면 바코드가 뜨니 이걸 OPT 앱으로 저장하면 됩니다.
이때 Next 가 비활성화 되어 있는데, 위의 Download, print, copy 중에 하나를 하시면 됩니다.

Viewing all 122 articles
Browse latest View live