[입 개발] Redis Montoring 바보 되기…

오늘은 레디스 모니터링 중에 바보된 사연을 소개합니다.

resque 나 sidekiq 같은 Redis를 Queue 로 사용하면서 겪은 삽질입니다.(개인적으로 둘 중에 하나를 추천하라면, sidekiq을 강력하게 추천합니다. 일단 sentinel과 연동된 failover 기능도 있고, resque가 lpop을 쓰는데 비해 sidekiq은 blpop을 써서 네트웍이나 redis 의 resource를 훨씬 적게 사용합니다.)

보통 위와 같이 redis를 queue로 사용할 때 redis network 의 input size 가 output size 보다 크거나 비슷해야 합니다. 왜냐하면 queue 이기 때문에 input == output 입니다. request 는 rpush queue_name data 인데, response 는 lpop queue_name + data 이니, 그런데… output 이 훨씬 많은 경우가 발생했습니다.

특정 기능이 동작하는지 보기 위해서 redis monitor 명령을 사용했습니다.

이유가 뭘까 고민을 하면서 다음과 같은 가정을 몇가지 해봤습니다.
1. 에러가 나서 리턴 스트링이 더 많다?
예를 들어 rdb 장애로 쓰기 금지 상태이면 ping 만 보내도 “MISCONF 어쩌고” 하는 에러 메시지가 전달됩니다.

그런데 이건 아니더군요.

그런데 의외로 간단하고 멍청한 이슈였습니다.

위의 기능을 확인하기 위한 redis monitor를 걸었더니… 그것도 외부에서 redis monitor | grep “1234” 식으로 했더니… 해당 서버에서 모니터링 메시지가 급증했던 것입니다. 실제로 모든 데이터가 넘어오니…

또한 해당 서버로 들어가서 redis monitor만 해도 실제로 ssh를 통해서 모니터링 메시지를 모두 보기 때문에, network outbound 가 늘어납니다. redis monitor | grep “1234” 이렇게 해당 장비 내부에서 하면 거의 늘어나지 않겠죠?

바보 같은 삽질기이지만… 웬지 다음에 또 실수할까 봐서 올립니다.

[입 개발] hadoop fs -tail [-f] URI 의 구현에 대해서

가끔씩 보면 tail -f 는 어떻게 동작할지에 대해서 궁금할때가 생깁니다. 얘가 무슨 수로… 뒤에를 계속 읽어올까?
OS에서 제공하는 notify 관련 함수를 이용할까? 등의 별별 생각을 해보지만… 사실 백가지 생각이 불여일견입니다.

hadoop cmd 는 각각의 command 클래스를 이용하는 CommandPattern 형태로 되어있습니다. 그러나 여기서 관심있는 것은 오직 tail 뿐… 그런데, 너무나 간단하게 Tail 소스만 까면… 끝납니다.

1. -f 옵션이 붙으면, 무한루프를 돈다.(파일 사이즈를 계산해서 offset 보다 적으면 종료)
2. 한번 looping 후에 followDelay 만큼 sleep 한다.
3. 기본적으로 현재 파일 사이즈의 끝 – 1024 만큼의 offset 부터 읽는다.
4. 한번에 읽어들이는 내용은 conf 에 정의되어 있다.(여기서 사이즈가 1024보다 적으면… 음… 맨 뒤가 아닐수도 있는데, 이부분은 뭔가 최저값이 셋팅되지 않을까?)

class Tail extends FsCommand {
  public static void registerCommands(CommandFactory factory) {
    factory.addClass(Tail.class, "-tail");
  }
..
  public static final String NAME = "tail";
  public static final String USAGE = "[-f] <file>";
  public static final String DESCRIPTION =
    "Show the last 1KB of the file.\n" +
    "\t\tThe -f option shows appended data as the file grows.\n";

  private long startingOffset = -1024;
  private boolean follow = false;
  private long followDelay = 5000; // milliseconds

  @Override
  protected void processOptions(LinkedList<String> args) throws IOException {
    CommandFormat cf = new CommandFormat(1, 1, "f");
    cf.parse(args);
    follow = cf.getOpt("f");
  }
  // TODO: HADOOP-7234 will add glob support; for now, be backwards compat
  @Override
  protected List<PathData> expandArgument(String arg) throws IOException {
    List<PathData> items = new LinkedList<PathData>();
    items.add(new PathData(arg, getConf()));
    return items;
  }

  @Override
  protected void processPath(PathData item) throws IOException {
    if (item.stat.isDirectory()) {
      throw new PathIsDirectoryException(item.toString());
    }

    long offset = dumpFromOffset(item, startingOffset);
    while (follow) {
      try {
        Thread.sleep(followDelay);
      } catch (InterruptedException e) {
        break;
      }
      offset = dumpFromOffset(item, offset);
    }
  }

  private long dumpFromOffset(PathData item, long offset) throws IOException {
    long fileSize = item.refreshStatus().getLen();
    if (offset > fileSize) return fileSize;
    // treat a negative offset as relative to end of the file, floor of 0
    if (offset < 0) {
      offset = Math.max(fileSize + offset, 0);
    }
....
    FSDataInputStream in = item.fs.open(item.path);
    try {
      in.seek(offset);
      // use conf so the system configured io block size is used
      IOUtils.copyBytes(in, System.out, getConf(), false);
      offset = in.getPos();
    } finally {
      in.close();
    }
    return offset;
  }
}

[입 개발] mecab 설치 with 은전한닢 (mac)

1. Download Files
– 은전한닢 mecab & dic
* https://bitbucket.org/eunjeon/mecab-ko
* https://bitbucket.org/eunjeon/mecab-ko-dic/downloads/mecab-ko-dic-1.6.1-20140515.tar.gz
– python binding
* https://bitbucket.org/eunjeon/mecab-python-0.996

2. Build
– mecab-ko
* ./configure
* make
* make install

3. mecab-ko-dic
* ./autogen.sh
* ./configure
* make
* make install

4. python binding
* python setup.py build
* python setup.py install

5. Test

import MeCab
m = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ko-dic')
ret = m.parse("아버지가 방에 들어가신다.")

아버지	NNG,*,F,아버지,*,*,*,*,*
가	JKS,*,F,가,*,*,*,*,*
방	NNG,*,T,방,*,*,*,*,*
에	JKB,*,F,에,*,*,*,*,*
들어가	VV,*,F,들어가,*,*,*,*,*
신다	EP+EF,*,F,신다,Inflect,EP,EF,시/EP+ᆫ다/EF,*
.	SF,*,*,*,*,*,*,*,*
EOS

[입 개발] RabbitMQ 설치하기 for CentOS 6.5

1. OS: Centos 6.5
2. 설치
– erlang(EPEL)
-> wget -O /etc/yum.repos.d/epel-erlang.repo http://repos.fedorapeople.org/repos/peter/erlang/epel-erlang.repo
-> yum install erlang

– rabbitmq
-> http://www.rabbitmq.com/install-rpm.html
-> http://www.rabbitmq.com/releases/rabbitmq-server/v3.3.4/rabbitmq-server-3.3.4-1.noarch.rpm
-> sudo rpm -Uvh rabbitmq-server-3.3.4-1.noarch.rpm

3. 설정
– /etc/hosts 호스트를 등록한다.
– /etc/rabbitmq/rabbitmq.config

[
   {mnesia, [{dump_log_write_threshold, 1000}]},
   {rabbit, [
       {tcp_listeners, [5672]},
        {log_levels, [{connection, info}]}
   ]},
   {rabbitmq_management, [
       {listener,[{port, 55672}]},
       {redirect_old_port, false}
   ]}
 ].

– .erlang.cookie 설정(/var/lib/rabbitmq/.erlang.cookie)
-> 동일하게 맞춘다.

4. rabbitmq 설정(rabbit1, rabbit2 서버 두대인 경우)
– rabbit1> sudo service rabbitmq-server start
– rabbit2> sudo service rabbitmq-server start
– rabbit2> sudo rabbitctl stop_app
– rabbit2> sudo rabbitmqctl join_cluster –ram rabbit@indigo117
– rabbit2> sudo rabbitmqctl change_cluster_node_type disc
– rabbit2> sudo rabbitmqctl start_app
– rabbit1> sudo rabbitmqctl cluster_status
– rabbit2> sudo rabbitmqctl cluster_status
– rabbit1> sudo rabbitmqctl set_policy ha-all “^\.” ‘{“ha-mode”:”all”}’

5. web plugin(all node)
– rabbitmq-plugins enable rabbitmq_management
– sudo service rabbitmq-server restart

6. 유저 설정
– sudo rabbitmqctl delete_user guest
– sudo rabbitmqctl add_user test 1234
– sudo rabbitmqctl set_user_tags test administrator

7. http://servername:55672/

8. example
– pip install pika

#!/usr/bin/env python
import pika

#Very Important!!!
credentials = pika.PlainCredentials("id", "password")
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost', credentials=credentials))

channel = connection.channel()
channel.queue_declare(queue='hello')

channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello World!')
print " [x] Sent 'Hello World!'"
connection.close()

[입 개발] org.apache.commons.codec.binary.Base64 decoding 동작에 관해서…

원래 개인적으로 삽질을 잘 하긴 하지만, 다시 최근에 큰 삽을 한번 팠습니다. 일단, 제가 자바를 잘 모르고, 초보다 보니, 남들 다 아는 지식을 모르고 넘어가는 경우가 많은데, 웬지 딴분들은 다 아셔서 이런일이 안생길것 같지만, 그냥 정리 차원에서 적어둡니다.

먼저 발생한 사건은 다음과 같습니다. 제 아이디인 charsyam 을 base64로 인코딩하면 다음과 같은 값이 나옵니다.

“Y2hhcnN5YW0=”

그리고 이걸 다시 decode 하면 “charsyam” 이라는 값이 나올겁니다. 그런데… 이제 보통 c/c++의 구현을 보면…
하다가 이상한 문자가 있으면 오류가 발생합니다. 즉 위의 “Y2hhc###nN5YW0=” 이런 글자가 있으면 일반적인 기대값은 오류라고 생각합니다.(아니라면 T.T)

그런데… apache coomons의 Base64구현은 조금 재미납니다.

“Y2hhc###nN5YW0=”
“Y2hhc#n#N#5YW0=”
“Y2hhc$$$nN5YW0=”
“Y2hhc$#n4N5YW0=”
“###Y2hhcnN5YW0=”
“###Y2hhcnN5YW0#=”
“Y2hhcnN5YW0=”

이런 것들이 모두 동일하게 “charsyam”으로 제대로 디코딩이 됩니다. 이것은 해당 코드가 DECODE_TABLE이라는 것을 가지고 여기에서 사용하지 않는 문자들은 전부 버린 뒤에 사용하기 때문에 일어나는 동작입니다. 다만 저 같은 사람은… 일단 제 코드를 의심하기 때문에, 삽질이 흑흑흑

먼저 다음과 같이 DECODE_TABLE 이 정의되어 있습니다.

    private static final byte[] DECODE_TABLE = {
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54,
            55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
            5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
            24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
            35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
    };

DECODE_TABLE 에서 -1에 지정되는 문자들은 전부 버리는 거죠. 사실 이게 apache commons base64의 문제(?)는 아닙니다. base64 에서 사용하는 table 이 사용하는 도메인에 따라서 조금씩 다르기 때문에, 이걸 전부 만족하도록 하게 구현이 되어 있는것입니다. 사실 더 많이 고려된 것이기도 하죠. 자세한건 여기(http://en.wikipedia.org/wiki/Base64)에서 확인하시면 됩니다.

그럼 이제 실제 구현을 살펴보면, 제가 말한 대로 DECODE_TABLE[value]의 값이 0 보다 클 경우에만 사용하게 되어있습니다.

   @Override
   void decode(final byte[] in, int inPos, final int inAvail, final Context context) {
       if (context.eof) {
           return;
       }
       if (inAvail < 0) {
           context.eof = true;
       }
       for (int i = 0; i < inAvail; i++) {
           final byte[] buffer = ensureBufferSize(decodeSize, context);
           final byte b = in[inPos++];
           if (b == PAD) {
               // We're done.
               context.eof = true;
               break;
           } else {
               if (b >= 0 && b < DECODE_TABLE.length) {
                   final int result = DECODE_TABLE[b];
                   if (result >= 0) {
                       context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK;
                       context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;
                       if (context.modulus == 0) {
                           buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);
                           buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                           buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
                       }
                   }
               }
           }
       }

       // Two forms of EOF as far as base64 decoder is concerned: actual
       // EOF (-1) and first time '=' character is encountered in stream.
       // This approach makes the '=' padding characters completely optional.
       if (context.eof && context.modulus != 0) {
           final byte[] buffer = ensureBufferSize(decodeSize, context);

           // We have some spare bits remaining
           // Output all whole multiples of 8 bits and ignore the rest
           switch (context.modulus) {
               case 0 : // impossible, as excluded above
               case 1 : // 6 bits - ignore entirely
                   // TODO not currently tested; perhaps it is impossible?
                   break;
               case 2 : // 12 bits = 8 + 4
                   context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
                   buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                   break;
               case 3 : // 18 bits = 8 + 8 + 2
                   context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
                   buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                   buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                   break;
               default:
                   throw new IllegalStateException("Impossible modulus "+context.modulus);
           }
       }
   }

나름 더 정확한 내용을 알 수 있게 되긴 했지만, 제 멍청함으로 인한 삽질한 시간들은 흑흑흑… 역시… 진리는 소스입니다. 흑흑흑

[책 리뷰] 글로벌 소프트웨어를 말하다.

김익환님의 새 책인 “글로벌 소프트웨어를 말하다.”가 나왔다. 이전 작품인 “글로벌 소프트웨어를 꿈꾸다.” 라는 책을 꽤나 재미있게 읽은 편이라, 이번 책도 나름 집중해서 읽어 보게 되었다.

먼저 제목부터 생각해보면, “글로벌 소프트웨어” 라 멋지지 않은가? 제목에서 부터, 두 가지를 의미한다고 본다. 정말 글로벌한 소프트웨어를 만드는 것과 아직 우리나라에서는 “글로벌 소프트웨어”를 만들기 어렵다라는 부분…

실제로 전작인 “글로벌 소프트웨어를 꿈꾸다” 부터 신작인 “글로벌 소프트웨어를 말하다” 역시, 강하게 동의하는 부분도 있지만, 또한 역시 강하게 동의하기 어려운 부분도 많은 책이다. 그러나, 내가 동의하지 않는다고 해서, 틀렸다고 말하기는 어렵고, 다른다고 말해야 하지 않을까?

이전 책에서 가장 맘에 들던 내용은 실리콘 밸리쪽에서는 70점 부터 시작하지만, 우리나라는 2~30점 부터 시작하기 때문에, 거기에 대한 격차가 벌어진다는 것… 내가 여러 회사를 다닌 것은 아니지만, 그래도 소스 관리 툴등은 기본적으로 사용하는 회사에서 다녔고, 회사의 정책에 따라서, QP활동(정적분석툴, 소스 리뷰) 등도 해봤지만, 이게 문화로 자리잡는게 정말 어렵다는 걸 알고 있기 때문에, 정말 공감이 가는 얘기였다. 또한 내가 경험해보지 못한 영역인 SRS에 대한 강조는 나로서는 또한 이해하기 어려운… 그 취지에는 공감이 가지만, 그게 전부라고 말하는 것은 아직 내 경험상 이해하기 어려운 일이었다.

이번 신작에서는 “손자 병법”이 손무가 오자서에 의해 등용되기 전에 쓰여졌다는 부분… 즉, 알고 있는 것과, 이를 제대로 이해해서 실천하는 것의 큰 차이가 있다는 부분에 다시 한번 크게 동의했다. 사실 대규모 서비스에 들어가는 기술들도 대부분은 우리가 이미 알고 있는 것들이다. 정말 특별하다고 할 것은 없지만… 이를 제대로 구현하고 디테일하게 챙기는 것이 쉽지가 않다는 것… 개발자가 이에 대해 알아야 하는 것도 많고… 개인적으로 클라우드나 로컬 서버에 따라서 설계가 바뀌는 것은, 그 환경이 다름을 이해하면, 당연히 고려해야할 문제들일 수 있다. 전통적인 환경보다 더 장애가 많을 수 있고, 반대로 디플로이가 빠르고 환경에 따라서 쉽게 확장이 가능한 장점도 있다.

반대로, 약간 인터넷 포털의 입장보다는 솔루션 소프트웨어 개발쪽의 의견이 더 강하다는 느낌이 들었다. 페북/구글, 라인/카카오/네이버는 소프트웨어 업체라기 보다는 인터넷 포털이나 서비스 업체라고 보는게 더 강한데, 이런 곳에서의 릴리즈는 하루에도 열두번씩도 발생할 수 있고, 요구 상황도 상황에 맞춰서 자주 바뀌게 되는데, “일년에 릴리즈를 세번 이상 하는게 나쁜 회사라고 하기는 어렵다.”(물론 릴리즈가 조금 다른 의미이겠지만…)

외국 회사의 면접 이야기…(하루에 6번 정도 면접을 보면… 정말 피가 마르고, 머리는 멍해진다. 그것도 영어로 보니 한국인의 입장에서는 더더욱 힘든…)

전체적으로 앞부분은, 전작에서 하지못한 다른 이야기, 뒷부분은, 약간 다른 이야기들을 하고 있다. 약간, 국내보다 실리콘 밸리는 무조건 잘하고 있다라는 느낌도 적지 않지만… 그래도 “많은 개발자들의 꿈인 해외 취업” 을 하고 오신 선배님의 재미난 이야기로 생각하면, 꽤 도움이 될 내용들이 많다는 것… 그리고 자신의 경험에 비추어 본다면, 꽤 도움이 될것 같다.

내식대로 내용을 살짝 재해석 한다면…
1. 소스 관리 툴은 뭐든지 일단 도입하자(VSS, CVS, SVN, GIT)
2. Jenkins 등의 CI로 빌드는 항상 확인하자(github이라면 travis CI 같은…)
3. Jira형태의 이슈 관리 툴을 사용하자. 꼭 Jira가 아니더라도, 기록이 잘 남고, 검색이 되면 좋다.
4. 소스 코드 리뷰는, 자신의 실력향상을 위해서 필요하다. 남을 지적질 할 생각이 아니라, 어떻게 짰는지 보고 배우는 형태로…
5. test가 품질을 올려주지는 못하지만, 리그레이션 테스트는 꼭 도입하자.
6. 자신이 알게된 새로운 내용은 팀원들하고 항상 공유하자.

개인적으로 좋은 소프트웨어를 만들어낸다는 가정하에, “개발이 재미있어야 한다.”에 한마디를 더 하고 싶다. “스칼라는 모르지만…” 트위터가 자바로 가지않고 스칼라를 선택한 이유가 개발자의 재미를 위해서였다는 것은… 참 멋진 일이라고 생각한다.

[입 개발:Redis] 나의 잘못된 오해, AOF

오늘 제대로 알고 있지 못한 부분에 대해서 한 독자님의 지적을 받고… AOF 관련 코드를 유심히, 그리고 좀 자세히 보게 되었습니다. 그런데… 음… 제가 완전히 잘못 알고 있던 부분이 있었습니다.

일단, 저는 Redis 의 AOF 가 DB의 WAL(Write ahead Log) 의 변종이라고 생각하고 있습니다. 먼저 Write ahead Log 에 대해서 아주 간략하게 설명하자면…(이번에 조사하면서 WAL조차도 잘못 이해하고 있었다는 걸 알았습니다. T.T)

데이터의 변경이 발생하기 전에 이 변경사항에 대한 Log를 남기고, 이를 이용해서 Data의 durability 를 보장하는 방법입니다. 디비등에서는 실제 데이터 영역의 변경을 하기 전에, 이에 대한 변경 사항을 commit시에 Log로 남기고, 이를 이용해서 나중에 실제 데이터 영역을 변경하기 위해서 사용하기도 합니다.

여기서 중요한 부분은 Log 가 persistent 할 수도 있고, 아닐 수도 있다는 점입니다. 왜냐하면 매번 disk에 write가 발생하면, 느려질테니깐요. 그래서 보통 Log를 Buffering 하고 이를 한꺼번에 쓰는 형태의 작업을 하게됩니다. 그런데… 여기서 이제부터 일반적으로 고민이 시작되는거죠.

DB의 데이터는 중요하다. 그런데 insert, update, delete등에 의해서 변경이 벌어지는데, 이것이 log buffer에 쌓이고, 실제 디스크에 쓰여지지 않는다면, 데이터의 유실이 발생할 수도 있는 것입니다.

그래서 mysql 의 innodb 의 경우는 innodb_flush_log_at_trx_commit 옵션을 이용해서 Disk에 flush 하는 주기를 조절하거나, 매번하도록 되어있습니다.(여기서의 기준은 하나의 Query Event 가 그 단위가 되는 것입니다.)

그럼 Redis 의 AOF는 무엇이 다르냐?

어떻게 보면, 거의 유사합니다. 하지만 다음과 같은 부분이 다릅니다.
1. AOF buffer 에 데이터를 남기는 시점이, 실제 메모리에 데이터가 변경된 이후이다.
-> 데이터의 메모리 변경 후에, 커맨드를 만들어서 AOF buffer 에 저장한다.
2. 그리고 실제 disk 에 flushing 하는 시점은 매 event loop의 시작 부분인 beforesleep 에서 동작한다.
-> 즉 AOF buffer 들어 있는 내용은, 하나의 event loop가 모두 끝난 다음에 디스크에 쓰여진다.
-> 각각의 버퍼는 각 명령 수행뒤에 propagation 에서 만들어짐.
-> Redis 는 single thread로 동작하기 때문에, 이 사이에 만약 1024개의 커맨드가 처리되었다고 한다면,
그 사이에 장애가 발생하면 해당 데이터를 돌릴 수 있는 방법은 없다.

여기서 잘못된 저의 오해는
1. Mysql처럼 Query 단위로, 데이터의 변경이 발생하기 전에 Logging이 되어야 된다고 생각
-> 그러나 실제로 Redis 에서는 커맨드 실행 후에, AOF buffer 만 만들어서 저장
-> event loop 전의 beforeSleep 에서 flushAppendOnlyFile 을 호출해서 AOF Buffer 를 Disk에 Flush 함.

그러면 옵션의 appendfsync 는 어떻게 동작하는가? 다음과 같습니다.
1. aof buffer 의 디스크에 쓰기는 오직 flushAppendOnlyFile()에서만 저장된다.
2. appendfsync가 no 면, 그냥 beforeSleep때 마다 os의 write를 호출하고, 실제 os와 disk간의 sync 는 os에
맡긴다.
3. EVERYSEC의 현재 fsync 작업이 스레드 큐에 존재하면, write를 하지 않고 return.
없으면 write 후에 fsync 를 타 스레드에 하도록 돌림.
만약 계속 fsync 작업이 남아있는걸로 판단하면, 그냥 write 함.
EVERYSEC 으로 되어있지만, 해당 cron 작업에 따라, 더 느려질 가능성도 존재.
4. ALWAYS의 경우, 매번 beforeSleep에서 디스크에 쓰고, fsync 도 동기로 호출

즉 Redis 의 AOF는 어떤 옵션을 쓰더라도, Write가 많을 경우에는 장애가 발생할 경우, 바로 직전의 명령이 아니라, 한 이벤트 루프 안에서 업데이트된 꽤 많은 데이터가 유실될 가능성도 있다는 걸 알아두고 사용하시면 좋을듯 합니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

OK

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

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

OK

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

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

OK

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    while(1) {
        listNode *ln;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

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

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

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

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

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

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

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

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

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

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

    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
        ((ptr = memory_allocate((size_t)len)) == 0)) {

        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }

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

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

    return 1;
}

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