[입 개발] kafka 0.8 설치 방법

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

Kafka는 LinkedIn에서 만든 분산 메시지 큐로, 디스크를 이용함에도 꽤 빠른 처리속도를 보장하는(더 복잡한 부분은 논문을 보셔야 쿨럭…) 소프트웨어입니다. 주로, 로그를 수집하기 전에, 이벤트 처리를 위해서, 데이터 버스로써, 또는 일종의 버퍼로 HBase나, 다른 툴들의 앞에서 이벤트 전달을 여과해주는 역할을 주로 합니다.

0.8로 진행하면서 약간의 사용방법이 바뀌어서 설치 방법을 정리합니다.

1. 준비
여기서는 KT UCloud에 8Core, 16GB 메모리 장비를 세대 생성했습니다.
server1, server2, server3

2. Zookeeper 설치
Zookeeper 의 설치는 기본이기 때문에 넘어가도록 하겠습니다. Zookeeper 설치 방법은 뭘 봐도 동일합니다.

3. kafka 다운로드
$> git clone https://github.com/apache/kafka.git

4. kafka 빌드
$> ./sbt update
$> ./sbt package
$> ./sbt assembly-package-dependency

5. config/consumer.properties 수정

zookeeper.connect=server1:2181,server2:2181,server3:2181

6. config/producer.properties 수정

metadata.broker.list=1:server1:9092,2:172.27.174.20:9092,3:172.27.92.32:9092

7. config/server.properties 수정

#서버에 맞춰서 unique 한 값을 준다.
broker.id=0

log.dir=/home/charsyam/kafka/log

8. kafka 서버 실행

bin/kafka-server-start.sh config/server.properties

9. topic 생성
topic을 안 만들어주면 제대로 동작을 하지 않는다.

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 2 --topic test

10. 샘플 실행
* consumer

bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic test --from-beginning

* producer

bin/kafka-console-producer.sh --broker-list server1:9092,server2:9092,server3:9092 --topic test

이제 Kafka가 정상적으로 동작하는 것을 볼 수 있다. 이제는 이를 이용해서 어떤 작업을 할 수 있는지 알아보자.

[입 개발] 한 서버에 하나의 Redis를 띄우시나요? 아니면 여러 대를 띄우시나요?

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

여러분은 하나의 서버에 Redis 인스턴스를 하나만 실행하시나요? 아니면 CPU Core 수나 메모리 양에 따라서 여러 개의 Redis 인스턴스를 실행시키시나요?

아마도 일반적으로는 하나의 서버에 하나의 Redis 인스턴스를 실행하는 경우가 많을 것 같지만, 아마도 다 각자의 기준이 있거나 또는, 그냥 실행시키지 않을까 싶기도 합니다.

오늘은 바로 이 이야기에 대해서 해볼려고 합니다.

일단 먼저, 어떤 방식을 선택해야 한다라는 정답은 없습니다. 다 잘쓰면 되지요.(퍽퍽퍽, 이 따위 소릴 할려고 소중한 내 시간을 뺐느냐? 라는 주먹들이 보이시는군요.) 그런데, 일단 끝까지 헛소리를 들으시고 그 뒤에 절(퍽퍽퍽… 들을 가치도 없어보인다는 말씀들이 덜덜덜)

일단 두 가지 경우의 장단점을 간단히 따져보면,

하나의 서버에 Redis 인스턴스를 하나만 실행하는 경우는 관리가 싶습니다. 그리고 행여나 다른 이슈로 인해서 해당 Redis 인스턴스가 영향을 받을 경우도 덜합니다.

하나의 서버에 여러개의 Redis 인스턴스를 실행하는 것은, 일단, Redis가 싱글스레드이기 때문에, 그래도 성능이 더 잘 나올 수 있습니다. 그리고, 여러 대의 서버를 써야 한다면, 하나의 서버 여러개를 관리하나, 여러 대의 서버에서 여러 개의 인스턴스를 사용하나 비슷할 가능성이 높습니다.

그럼, 일단 너는 어떤걸 권장하냐? 라고 물어보신다면, 적절히 잘 관리하면 어떤 방법이든 상관없지만, 개인적으로는 하나의 서버에 여러 대의 Redis 인스턴스를 사용하는 것을 추천합니다.

자 일단 예를 들어보겠습니다. 다음과 같은 서버 사양이 있다고 합니다.(KT UCloud 에서 해당 서버 목록을 가져왔습니다.)

*2vCore, 4G memory
*4vCore, 8G memory
*8vCore, 16G memory

위와 같은 사양에서 만약 한 서버에 하나의 Redis 인스턴스를 실행한다면, 어느정도 메모리를 사용할 경우, 메모리를 많이 사용한다고 할 수 있을가요?

그냥 제 맘대로 생각했을때, 3G, 6G, 13~14G 정도 사용하면 거의 맥시멈으로 사용한다라고 생각하지 않을까요? 이게 그냥 일반적인 생각일 것입니다. 그런데 만약 우리가 마스터/슬레이브 형태로 Redis 를 사용한다면, 여기서 문제가 하나 생길 수 있습니다. 슬레이브 노드가 마스터에 연결될 때, 마스터가 죽을 수도(도를 강조) 있다는 것입니다.

“왜” 라는 질문이 나오는 것이 정상일 것입니다. 그리고 죽을 수 있는 이유는 RDB 때문입니다. 여기서 “아!!!” 하시는 분과 “엥???” 하시는 분이 계실껍니다. 그럼 RDB 꺼두면 되는거 아니예요? 라고 물어보시는 분도 생길껍니다. 그런데… RDB 설정을 켜두든, 꺼두든… 이 문제는 발생할 수 있습니다.

이유를 살펴보면, Redis의 마스터/슬레이브 연결시에는 RDB 설정 여부에 상관없이 무조건 RDB를 생성하게 됩니다. 그리고 이 RDB 파일을 seed로 전송하고 그 뒤의 차이를 버퍼에 저장한 후에 이를 보내서 sync를 맞추게 되는데, 이 때, write가 많은 서버라면 메모리를 많이 사용해서 서버가 죽을 수도 있습니다. 처음 서비스 시작시에 슬레이브를 설정하면, 당연히 메모리 사용량이 얼마 안되니, 큰 문제가 없지만, 서비스로 인해서 메모리가 거의 풀로 찬 상황에서 슬레이브가 붙는다면, 문제의 소지가 있는 것이죠.

이 때문에, Core 수나 메모리에 따라서 적절히 Redis 인스턴스를 여러 개 띄워주는게 유리합니다. 즉, N개의 인스턴스를 실행한다면 (Memory-(운영체제필요메모리))/(N+1) 정도의 규칙으로 적절히 나누면(물론 Core 수도 중요합니다.) 하나의 인스턴스가 순간적으로 메모리를 많이 사용하더라도, 안정적으로 넘어갈 수 있습니다.(물론 이 때도 관리를 잘 해야합니다. 관리를 잘못하면… 한대든 여러대든 똑같은 이슈가…)

즉, 위와 같은 문제를 해결하기 위해서 하나의 서버에서 하나의 인스턴스만 실행하면, 16G 메모리라면, 최악을 대비해서 7G 정도만 사용해야 하지만… 여러 개의 인스턴스를 실행한다면 4G*3 개 정도의 인스턴스를 운영할 수 있습니다. 어차피 여러 대 관리해야 하면, 관리 이슈도 비슷하게 들테니까요.

여담이지만, Redis 나 이런 서버종류를 단순히 8G 서버 4대를 32G 서버 한대로 변경할 수 있는 것은 아닙니다. 컨넥션 수의 관리 때문에, 무조건 일정 대수 이상은 있어야 할 경우도 충분히 있습니다. 즉, 어느 것이 답이라는게 아니라, 어떤 것을 쓰든, 내부 구조를 잘 알아야 한다는게 오늘의 이야기입니다.

[입 개발] libcloud 로 KT Olleh UCloud Biz 이용하기

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

libcloud는 apache에 있는 multi cloud를 단일 인터페이스로 사용할 수 있도록 하는 Open Source Python Project 입니다.  libcloud를 보면 내부에 여러가지가 있지만, 가장 큰 것 두 가지가 compute 와 storage 입니다. compute 는 IaaS, 즉 아마존 aws의 ec2와 같이 가상 서버를 할당받고 종료하기 위해서 사용하는 프로젝트이고 storage 는 S3 나 OpenStack의 Swift와 같은 object store를 다루기 위한 API입니다.

그런데 갑자기 왜 KT Olleh UCloud Biz를 얘기하는 가 하면 몇일전에 libcloud에 KT UCloud 의 Storage를 사용할 수 있는 패치가 올라왔기 때문입니다. compute 에 대한 부분은 작년 쯤에 패치가 되었는데, storage 쪽이 늦게된 이유는, 게을러서라고 생각합니다.(죄송합니다. 제가 게을러서 T.T), 사실 올 2월에 패치를 한번 냈다가, 까였었습니다. OpenStack Driver 자체를 Refactoring하려고 시도했더니, 수정사항이 너무 많아져서 커미터가 반대를 T.T, 이번에는 그래서 아주 간단한 패치로…KT Ucloud Storage 가 OpenStack Swift 기반이라 Rackspace 에서 cloudfile 드라이버를 구현하면서, 이미 거의 다 만들어나서 숟가락만 살짝 얹은…

그래서 이번에는 libcloud 가 제대로 동작하는지 확인해보고 실제로 KT UCloud를 어떻게 사용하게 되는지 간단한 예제들을 통해서 살펴보도록 하겠습니다.

1. 설치

git clone https://github.com/apache/libcloud.git
python setup.py build
sudo python setup.py install

2. Compute Driver 사용

from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver
from config import Config

ktucloud = get_driver(Provider.KTUCLOUD)

driver = ktucloud(Config.APIKEY, Config.SECRETKEY,
                  path='/server/v1/client/api', host='api.ucloudbiz.olleh.com')

nodes = driver.list_nodes()
sizes = driver.list_sizes()
images = driver.list_images()

print sizes[0]
print images[0]
driver.create_node("libcloud", sizes[0], images[0])

위의 예제를 실행하면, 해당 size와 image 에 맞는 인스턴스가 생성되는 것을 볼 수 있습니다.

libcloud

두 번째는 kt ucloud storage 드라이버의 사용방법입니다. KT UCloud Storage의 경우, compute driver 와는 또 키가 다릅니다. ucloud storage->API Key 관리에서 id와 secret key를 확인할 수 있습니다.

from pprint import pprint

from libcloud.storage.types import Provider
from libcloud.storage.providers import get_driver
from config import Config

CloudFiles = get_driver(Provider.KTUCLOUD)

driver = CloudFiles(Config.STORAGE_API_ID, Config.STORAGE_API_SECRET)

containers = driver.list_containers()
container_objects = driver.list_container_objects(containers[0])

pprint(containers)
pprint(container_objects)

사실 기존에는 Compute Driver 만 지원되고 Storage Driver 가 지원되지 않아서 libcloud에서 사용하는건 반쪽이었지만, 지금은, 그래도 libcloud를
사용해서 Multi Cloud를 이용할 수 있을꺼라고 생각합니다.

[뻘 생각] 왜 우리는 클라우드로 가야하는가?

해당 블로그는 KT olleh ucloud biz 의 지원을 받고 있습니다. 이 글에서의 클라우드는 개발자 입장에서의 클라우드 서버(IaaS, PaaS, SaaS, BaaS) 를 의미합니다. 최근에 직접적이지는 않지만, 클라우드를 왜 써야 하는지에 대한 질문을 종종 받습니다. 또한 클라우드가 싸다고 하는데 더 비싼거 같아요?, 성능도 떨어지는 것 같다라는 얘기도 자주 듣습니다. 그러면서 도대체 왜 클라우드를 써야 하는지에 대해 궁금해 합니다. 먼저 첫번째 대답은, 말하신게 전부 맞습니다라는 겁니다.

실제로 16G 메모리 정도를 사용하는 서버를 비교해보면 다음과 같습니다. Aws, KT UCloud, cloudv.kr, 통큰아이를 비교해봅니다.(무약정 월간 기준, 코어의 종류는 조금 다를 수 있습니다. ECC의 차이도 있겠지만 이건 일단 무시하겠습니다. 특히 통큰아이는 32GB 메모리 기준입니다. 환율은 2013/11/03 기준으로 1,061원입니다.)

|                 | 업체             | 가격         |사양

|클라우드|aws               |313,207    |2 core 17.1GB

|클라우드|KT UCloud  |261,000    |4 core 16 GB

|호스팅     |cloudv         |169,000     |8 core 16 GB

|호스팅     |통큰아이     |125,000     |4 core 32 GB

이렇게 보면, 도리어 웬지 가장 수치상 좋아보이는 통큰아이가 가장 싸보이고, aws나 KT UCloud는 2배이상 비쌉니다.  그리고 가상화를 기본적으로 사용하는 클라우드에서는 또한 실제 물리 장비에 비해서 I/O도 많이 떨어집니다. 즉, 비싼데, 더 안좋은 성능을 보여주는 겁니다.

그렇다면 클라우드를 왜 써야 하는가에 대한 생각이 들 것입니다. 먼저 결론부터 말하자면, 박민우님이 “이미 클라우드는 차와 같다. 차를 사용하는 것은 이미 부인할 수 없는 흐름이고, 차가 없어진다고 다시 마차를 사용하거나 할 수 없다.”  라고 말씀하신 것 처럼 이미 클라우드를 사용하는 것 자체는 거부할 수 없는 흐름으로 넘어가고 있습니다. 그러면 당연히… 도대체 왜? 라는 질문이 들것입니다.

또한 심심찮게 들려오는 클라우드의 장애 소식은 더더욱 클라우드를 쓰기를 불안하게 만들어줍니다. 그럼에도 불구하고, 우리는 클라우드를 사용해야 하고, 점점 더 사용하게 될 것입니다.  다음과 같은 상황들을 가정해 보도록 하겠습니다.(아주 극단적인 상황으로)

1. 소셜 쇼핑 처럼, 특정 시간대의 트래픽 패턴이 극단적으로 다를 때, 만약 오전 1시와 오후 1시의 트래픽이 100배가 차이가 난다면, 안정적인 서비스를 위해서 서버가 몇대가 필요할까요? 그리고 그 피크치가 딱 1시간 정도라면?(아니면 반반이라고 한다면?) 안정적인 서비스를 위해서는 장비가 맥시엄을 대비한 경우보다 더 필요합니다.(장애가 발생할 수도 있으니..) 하지만, 클라우드 서비스 이용한다면, 그 시점에만 맥시엄으로 늘려놓고, 그 외에는 다시 장비 수를 줄여둘 수 있습니다.

2. 차주 서비스를 위해서 100대의 서비스가 필요한데, 주문하니 한달이 걸린다고 합니다.  그럼 어떻게 해야할까요? 물론 이런 질문에 대해서는 큰 업체들은 “예측 가능”이 가장 중요하므로, 서비스의 규모를 파악해서 미리 주문을 한두달 전에 넣어두면 해결되지 않는냐? 라고 반문할 수 있습니다. 그러나, 누구도 완벽하게 트래픽을 계산할 수 없고, 반대로 꾸준히 지속된다라고 말하기는 어렵습니다. 최근의 모바일 게임은 그 변화가 더 커져서 동접이 높아서 100대 1000대를 사서 서비스를 했는데, 일년안에 동접이 확 떨어지면, 남은 장비들을 어떻게 해야할까요?

실제로 경험한 일 중에는, 장비 2대를 추가해야 하는데, IDC 상면에 한대 밖에 추가할 공간이 없어서, 전체 장비의 위치를 다 옮겨야 해서, 옮겨야 할 장비만큼을 새로 할당 받아서 다른 상면에 재 구축하고 원래 장비들을 반납한 적도 있었습니다. 이런 경우는 장비가 갑자기 전에 사용하던 만큼 필요하게 됩니다.

결국 여기서의 핵심은 “예측 가능” 과 “기민성”의 두 가지 키워드로 요약이 되게 됩니다.

즉 , 클라우드 서비스는 항상 정확할 수 없는 “예측 가능”과 빠르게 서비스에 필요할 때 언제든지 투입하거나 제거할 수 있는 “기민성”을 만족시켜주기 때문에, 점점 더 많이 사용하게 될 것이라고 생각합니다.

클라우드 서비스를 설계하게 되면(분산 서비스를 설계하게 되면) 결국 서버를 서버라기 보다는 하나의 컴포넌트로 취급하게 되고, 같은 여러개의 컴포넌트가 컴포넌트 풀에 존재하고, 이것이 사용불가능하게 되면, 다른 컴포넌트를 사용하게 되는 방식으로 구성이 되게 됩니다. 그래서, 클라우드 서비스 를 이용할때도, 기존에 서버에 장애가 있으면, 이를 점검해서 다시 투입하는 방식이 아니라, 새로운 장비를 할당받아서 서비스에 투입하고 기존 인스턴스를 지워버리면 됩니다. 이미지를 기반으로 쉽게 Scale out이 가능해지는 것이죠.

장애 측면에서 보더라도, 클라우드 서비스의 장애는 IDC의 장애와 비슷하지 않을가라고 생각합니다. 결국 DR(Disaster Recovery) 전략이 필요하고, 비용에 따라서, 적절하게 구축여부를 판단해야 될껍니다.

결국 클라우드 서비스를 사용하냐, 마냐 가 아니라, 어떻게 하면 잘 사용할 것인가가 정확한 질문이라고 생각합니다. 물론 클라우드 서비스가 항상 답은 아니고, I/O가 높은 서비스는 가상화 클라우드에서는 좋은 성능을 발휘하기도 힘듭니다.(아마존의 EMR도 가상 서비스 위에서 도는 것이 아니라, 물리 Hadoop 풀을 충분히 갖춰두고 여기서 서비스 하는 것으로 알고 있습니다.)

그리고 PaaS, SaaS, BaaS 등으로 발전해 나가면서, 클라우드 서비스의 조합만으로도 쉽게 서비스 구성이 가능해져서, Time To Market 을 지키는 게 더 쉬워질 것입니다. (다만 여기서, 여러 클라우드 서비스를 이용하면, 반대로 각각의 클라우드 서비스에 대한 장애가 취약해진다는 단점이 있긴 합니다만… 이런 여부에 대한 대응책을 미리 생각해 두는 것이 필요합니다.)

[입 개발] 오픈 소스로 문서의 텍스트 추출 서비스 만들기 #2

해당 포스트는 KT olleh ucloud biz의 지원을 받고 작성되었습니다.

지난주에도 살짝 언급하기는 했지만, 지난 번의 아키텍처는 사실 분산이 어려운 구조입니다. 하나의 서버안에 웹서버와 Celery가 함께 있어야만 데이터에 접근이 가능합니다. 즉 하나의 서버에서 남는 자원을 활용하는 형태가 되겠죠. 다음과 같은 형태입니다.

text_extraction_2_1

그러나 이러라고 만들어진 Celery가 아닌데, 과연 그렇다면 어떤 방법이 있을까요?

그런데 이런 고민이 사실 확장 가능한 시스템을 고민할 때 항상 하게 되는 고민입니다. 특히 현재의 일반적인 클라우드 서비스의 구조가 뒷단의 데이터 스토리지 영역에서 데이터를 동기화거나 저장해두고, 앞단에서는 공통적으로 이 데이터를 사용하자라는 개념으로 접근하게 됩니다. 즉, 다음과 같은 구조가 됩니다.

text_extraction_2_2

그런 연고로, 확장성을 위해서는 아래의 Data Storage Layer도 분산 시스템이 되어야 합니다. 그럼 다시 문제를 한정시켜서, 문자 추출 서비스를 할 때 여러 서버가 동시에 접근할 수 있는 파일 서비스가 필요합니다.

이럴 때 쉽게 사용할 수 있는것이 Object Store 이고 여기에 가장 유명한 것이 아마존의 S3입니다. 수 많은 기업들이 아마존 S3를 이용해서 서비스를 만드는 경우도 많습니다. 대표적인 케이스가 Dropbox 입니다. 물론, 여기서는 싱크를 잘 맞추는 게 더 어렵겠지만, 파일의 관리를 S3를 통해서 간소화 시켰습니다. 더 핵심에 집중하게 된거죠.

여기서도 마찬가지입니다. 텍스트 추출과 확장성이 목표라면, 이런 서비스를 안 쓸 이유가 없겠죠. KT ucloud 에서 제공하는 ucloud storage 는 기본적으로 openstack의 swift 서비스를 사용중입니다.

사용을 위해서는 다음과 같이 해당 페이지에서 API Key를 받아야 사용이 가능합니다.

text_extraction_2_3

그렇다면 이제 어떻게 아키텍처가 변경이 되게 될까요? 웹 서버는 swift나 S3 같은 곳에 등록된 파일을 저장하게 됩니다. 그리고 그쪽 파일 uri를 celery에 전달하게 되면, celery는 자신의 local이 아닌 분산 오브젝트 스토어에서 해당 파일을 읽고 이를 읽어서 처리하게 되는 것입니다.

다음과 같이 중간의 Data Storage가 하나라면 SPOF가 되지만, 분산 파일 시스템이라면, 장애에 안정적이고 확장성도 높아지는 아키텍처가 구성이 되는 것입니다.

text_extraction_2_4

물론, 장단점이 다 존재합니다. 추가적인 레이어를 사용하기 위해서 각각 네트웍을 통해서 파일을 쓰고 읽는 과정이 필요합니다. 이제 여기서 고민할 것은 각각의 비용이 얼마나 쌀지 생각해서 비용이 더 싼 곳으로 결정하는 지혜(?)가 필요합니다.(이건 뭐, 알아서 ㅎㅎㅎ)

여기에서 추가로 더 확장을 한다고 하면, 각각의 클라우드 서비스가 제공하는 API를 이용해서 자동으로 장비를 부하에 따라서 확장을 하는 것도 가능합니다.

ucloud storage 나 S3 말고도, 직접 ceph 나 grusterfs를 설치해서 직접 관리하는 방법도 있습니다.

여러가지 말들을 주절주절 적었지만, 결론은, 자신의 요구사항에 맞는 형태에서 가장 효과적인 아키텍처를 고민해야 한다는 것입니다. 특히 클라우드의 특성을 잘 알아야 안정적인 서비스를 구축할 수 있습니다. 특히 클라우드로 넘어가면서 속도보다는 안정성/확장성을 더 높이는 방향으로 설계를 하는 것이 클라우드 환경에서는 유리합니다.

ps. python으로 swift 관련 작업을 한다면, 다음 문서를 참고해서 swift-tool을 사용해보는 것도 좋습니다.(http://developer.ucloudbiz.olleh.com/blog/swift/ucloud-storage—Swift-Tool/)

[입 개발] 오픈 소스로 문서의 텍스트 추출 서비스 만들기 #1

해당 포스트는 KT Olleh Ucloud Biz의 지원을 받고 작성되었습니다.

갑자기 Apache Tika를 보다가 문서에서의 텍스트 추출 서비스를 간단히 만들 수 있겠다라는 생각이 들어서 시도를 해보게 되었습니다.  이에 대한 개발을 위해 사용된 기술들은 Python, Python Flask, Apache Tika(java), Celery(Python), Redis(c) 등입니다. 파일을 등록하면 여기서 텍스트만 추출해서 보여주는 것입니다.  간단한 UI는 다음과 같습니다.

text_extraction_1_1

예전 같았다면 문서에 대한 텍스트 변환 코드를 다 작성했어야 했지만(실제로 5~8년 전에는 문서에서 텍스트 추출을 직접 구현했던 경험이… 그때는 한글 포맷도 없던 시절에 T.T) 이제는 실제로 이런 것들을 해주는 오픈소스들이 대부분 존재합니다. 여기서 사용하는 Apache Tika의 경우도 그런 라이브러리 중에 하나이고, 꽤 많은 문서를 지원해주고 있습니다.

참고로 모든 소스는 https://github.com/charsyam/celery_with_tika/ 에서 받으실 수 있습니다.

 

Apache Tika는 http://tika.apache.org 에서 받을 수 있고, 라이브러리 형태로도 사용할 수 있고, tika-app 을 이용하면 그냥 커맨드 쉘 형태로도 사용이 가능합니다.
Python Flask 는 Python에서 사용할 수 있는 web framework 입니다.(django가 더 유명하지만, 전 웬지 반창고파라 ㅋㅋㅋ, 이유는 없습니다. 그냥 Flask를 먼저 배워서 ㅋㅋ)
Python Celery 는 Python 기반의 분산 작업 처리 솔루션입니다. 사실 여기서 핵심은 Tika(문서 추출), Celery(분산 작업) 입니다.

왜냐하면, 궁극적인 이 프로젝트의 목적은 text extraction as a service 형태인데, 최대한 간단하게 작업 자체를 확장하거나, 줄일 수 잇는게 목적입니다. 그런데 이런 작업을 잘 만들려면
많은 노력이 들지만, Celery를 통해서 누워서 떡먹기식으로 해결이 가능합니다. 텍스트 추출은 tika가 처리를 해주고요.

그래서 시스템의 간단한 아키텍처는 다음과 같습니다.
text_extraction_1_2

구조를 보면 매우 단순합니다. 하지만, 조금만 생각해보면, 아주 확장에 자유로운 구조가 될 수 있습니다.
text_extraction_1_3

여기서 다시 웹서버만 늘리면 다음 구조가 되게 됩니다.
text_extraction_1_4

굉장히 확장이 쉬워보입니다.(물론, 여기에는 함정이 있습니다. 마지막 아키텍처 처럼 될려면, 그 후방이 분산 파일 시스템처럼 구성이 되어야 합니다. 즉, 웹서버에서는 파일을 분산 파일 시스템에 저장하고, Celery에서 이를 읽어서 처리하는 방법이 되어야 합니다. 그러나 그림은 더 이상은 생략)

최초에 이를 만들때 생각한 것은 최대한 쉽게 만들 수 있도록 하자였습니다. 그래서 결론적으로 tika를 이용하는 방법은 os.system 이나 subprocess를 이용해서 그냥 커맨드를 실행시키고 이 결과를 이용해서 보여주는 방식입니다.(중간에 pyjnius 등의 이야기도 하겠지만…)

그리고 이를 구현하는데 든 코드는 몇줄 되지 않습니다. 일단 웹 페이지를 보여주는 python flask 관련 코드는 다음과 같습니다.

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        file = request.files['file']
        if file:
            workid = id_generator();
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], workid))
            result = textextractor.delay(workid)
            return render_template('result.html',task_id=result.task_id)
        else:
            return render_template('index.html')
    else:
        return render_template('index.html')

if __name__ == '__main__':
    app.run(host="0.0.0.0", debug=True)

Celery 를 사용하기 때문에(그것도 비동기로) celery 큐에 집어넣는 작업만 하면 끝납니다.
이 때, KT Ucloud를 사용하신다면 Flask가 기본적으로 5000번 포트를 이용하는데(지정가능)
Port Forwarding에서 다음과 같이 5000번 포트를 추가하셔야 정상적으로 서비스가 가능합니다.

text_extraction_1_5

그리고 실제 Celery 를 이용한 부분은 다음과 같습니다.

@celery.task
def textextractor(workid):
    filename = "%s/%s"%(Config.UPLOAD_FOLDER, workid)
    builds = "%s/%s"%(Config.BUILD_FOLDER, workid)
    status = 0

    command = "/usr/bin/java -jar %s -t %s"%(Config.TIKA_CLASSPATH,filename)

    args = shlex.split(command)
    try:
        contents = subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
    except:
        contents = "tika error"
        status = -1

    os.remove(filename)
    return (contents, status)

실제로 tika를 실행시켜주고 그 결과를 가져오는 것이기 때문에, 아주 간단합니다. 다만, 눈에 보이는 버그도 숨어있습니다.(귀찮아서 아직 처리를 못한…)

즉, tika만 익숙하면 구현하는데는 거의 1시간도 채 걸리지 않습니다. 그렇다면, 그렇게 쉽게 걸릴일을 왜 이렇게 오래걸렸냐라고 물어보면, 딴 삽질들에 대해서 얘기를 해야합니다. 비동기 작업(Celery가 해주지만, 결과를 polling 하는 방법), 좀 더 깔끔하게 하기 위한 방법, 뒤에 hwp를 추가하기 위한 간단한 삽질등은 다음번에 올리도록 하겠습니다. 다만, 위에 코드만으로도 아주 간단히 -_- 문서에서의 text 추출을 만들 수 있다는게, 오픈소스의 대단함이지 않을가 싶습니다.

[입 개발] Redis Sentinel을 이용하면서 겪게 되는 문제 하나와 해결책

해당 포스트는 NIPA의 클라우드 지원센터의 도움으로 작성되었습니다.

Redis Sentinel을 이용하다보면, 흔한 상황은 아니지만, 문제가 생기는 상황이 있습니다. 어떤 상황이냐 하면, “모든 Redis 노드가 장애가 나고, 다시 한 대가 살아났을 때, 최초 Master가 아니면, Master 로 전환되지 않는 문제” 입니다.

즉 다음과 같이 Redis Master/Slave 각각 한대와 Sentinel 이 한대 있다고 가정하겠습니다.(여기서 Sentinel 과 Slave의 개수는 상관없습니다.)

sentinel1

그런데 여기서 Master가 장애나면 당연히 Slave가 Master로 승격이 되게 됩니다. 당연히 기대한 동작입니다.

sentinel2

sentinel3

자 이제 사고로 마지막 노드까지 장애가 나면 전혀 서비스 할 수 있는 서버가 없어지게 됩니다.

sentinel4

여기서 두 가지 가정이 있습니다.

  • 최초의 master 가 살아난다.
  • 마지막의 master 가 다시 살아난다.

첫번째 경우에는 전혀 문제가 없습니다. 즉 정상적으로 master가 인식이 됩니다.

sentinel5

그런데 두번째 케이스 원래 최후의 master나 다른 slave가 살아나네 되면… sentinel은 해당
노드 자체를 인식하지 못하게 되고, 계속 해당 노드는 Slave로 남아있습니다. 당연히 master가 되기를 바라고 있는데 말이죠.

sentinel6

그렇다면 왜 그런걸까요? 어떤 문제가 있는건가요?

사실, 실제로 Redis 두 대가 모두 죽어있을 때도 Sentinel 은 해당 두 노드에 대한 연결을 모두 가지고 있고 이를 지속적으로 체크하게 됩니다. 해당 작업은 sentinelHandleDictOfRedisInstances 에서 이루어집니다.

그런데 후자의 경우, Redis 노드가 다시 살아나면서!!!, 이전의 설정을 읽어드립니다. 즉, 자신이 slave라고 인식을 하게 되고, Sentinel 이 INFO 명령을 줬을 때, 자신이 slave 라고 전달하게 됩니다. 이 때, Sentinel은 slave가 연결되었을 때, master를 인식하고자 하고, 현재의 연결 정보를 날리고, Master 에 대한 연결만 가지게 됩니다. 정상적인 상황에서는 Master에 질의해서 Slave들의 정보를 가져올 수 있기 때문입니다.(sentinelRefreshInstanceInfo)

    /* ---------------------------- Acting half -----------------------------
     * Some things will not happen if sentinel.tilt is true, but some will
     * still be processed. */

    /* When what we believe is our master, turned into a slave, the wiser
     * thing we can do is to follow the events and redirect to the new
     * master, always. */
    if ((ri->flags & SRI_MASTER) && role == SRI_SLAVE && ri->slave_master_host)
    {
        sentinelEvent(REDIS_WARNING,"+redirect-to-master",ri,
            "%s %s %d %s %d",
            ri->name, ri->addr->ip, ri->addr->port,
            ri->slave_master_host, ri->slave_master_port);
        sentinelResetMasterAndChangeAddress(ri,ri->slave_master_host,
                                               ri->slave_master_port);
        return; /* Don't process anything after this event. */
    }

즉 Sentinel은 위의 작업 때문에 최초의 master에 대한 정보만 가지고, 계속 이전 아직 살아나지 않은 노드만 체크하게 되는겁니다. 그래서 정상적인 master가 살아날때는 문제가 없이 인식이 되는거죠.

사실 서버의 관리상 한대가 죽더라도 이에 대한 관리를 즉각 해야하기 때문에, 이런 일이 발생하는 일이 많지는 않지만, 서버가 급격히 죽을 경우는 문제가 될수도 있습니다.

그럼 이에 대한 해결책이 있는가? 라는 질문을 받게 된다면… 크게 두 가지 방법이 있습니다.
전자는 완벽한 해결책은 아니고, 장애가 난 마지막 master 노드가 살아났을 때에 대한 해결책입니다. redis에는 config rewrite 라는 새로운 명령이 들어가 있고, master 변환이 생길때마다 해당 명령을 던져주면 master로 동작하게 됩니다.

config rewrite

그러나 이것은 모든 경우에 대한 해결책은 아닙니다. 그렇다면, 어떻게 해결을 해야 하는가? 라는 질문이 생깁니다. 후자는 sentinel을 수정하는 방법입니다. 이건 다음 번에…(실제로 해당 수정을 통해서 위의 경우에 sentinel이 master로 인식이 되도록 하게 했습니다.) 그럼 60초 후에(?) 뵙겠습니다.

[입 개발: Redis 장애] twilio 의 Redis 장애의 원인과 해결책(?)

몇일전에 twilio에 빌링 관련해서 장애가 발생했다고 합니다. 주 내용은 http://www.twilio.com/blog/2013/07/billing-incident-post-mortem.html 에서 보실 수 있습니다. 그리고 이 글이 해커 뉴스(https://news.ycombinator.com/item?id=6093954) 에 올라 왔고, 이에 대해 다시 @antirez가 답변을 블로그에 올리기도 했습니다.(http://antirez.com/news/60)

그럼 왜 이런 현상이 일어났을까요?

먼저 twilio의 발표에는 네트웍 단절로 인해서 slave 노드들이 모두 연결이 실패하고 이에 따라서 master 노드에 여러대의 slave가 동시에 sync 과정을 거치면서 master의 부하가 높아져서, 실제 처리해야 할 리퀘스트들을 처리하지 못하게 되어서 장애가 생긴걸로 설명하고 있습니다.(밑에 덧글도 읽어보면 싸우기 시작하는군요. 재미있습니다.)

먼저 현재 2.6.x 대의 Redis는 master와의 연결이 끊어지면 무조건 sync를 다시 하게 됩니다. sync 과정은 master가 rdb를 백그라운드로 생성하게 되고(무조건!!!) 이를 client에 전달하고, 그 사이에 쌓인 버퍼를 전달해서 sync를 맞추게 됩니다. 이 때, 여러 개의 client가 동시에 sync를 요청해도 백그라운드로 rdb는 하나만 생성되고, 현재 쌓인 버퍼만 복사해서 전달하게 됩니다.
syncCommand에 자세히 설명되어 있습니다. 코드를 보면 child_pid != -1이면 현재 child_pid 생성중이면, 현재 sync를 기다리는 자식 노드가 있으면 sync를 위한 추가 명령 버퍼만 복사하고, 리스트에 추가합니다. slave가 아무것도 없으면, 지금 rdb 저장이 마무리 되었다고 생각하고, 다음 rdb 생성이 일어나길 기다리게 됩니다. 반대로 child_pid == -1이면 현재 아무런 sync 요청이 없으므로 rdb를 백그라운드로 생성합니다.

void syncCommand(redisClient *c) {
    /* ignore SYNC if already slave or in monitor mode */
    if (c->flags & REDIS_SLAVE) return;

    /* Refuse SYNC requests if we are a slave but the link with our master
     * is not ok... */
    if (server.masterhost && server.repl_state != REDIS_REPL_CONNECTED) {
        addReplyError(c,"Can't SYNC while not connected with my master");
        return;
    }

    /* SYNC can't be issued when the server has pending data to send to
     * the client about already issued commands. We need a fresh reply
     * buffer registering the differences between the BGSAVE and the current
     * dataset, so that we can copy to other slaves if needed. */
    if (listLength(c->reply) != 0) {
        addReplyError(c,"SYNC is invalid with pending input");
        return;
    }
    redisLog(REDIS_NOTICE,"Slave ask for synchronization");
    /* Here we need to check if there is a background saving operation
     * in progress, or if it is required to start one */
    if (server.rdb_child_pid != -1) {
        /* Ok a background save is in progress. Let's check if it is a good
         * one for replication, i.e. if there is another slave that is
         * registering differences since the server forked to save */
        redisClient *slave;
        listNode *ln;
        listIter li;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            slave = ln->value;
            if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break;
        }
        if (ln) {
            /* Perfect, the server is already registering differences for
             * another slave. Set the right state, and copy the buffer. */
            copyClientOutputBuffer(c,slave);
            c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
            redisLog(REDIS_NOTICE,"Waiting for end of BGSAVE for SYNC");
        } else {
            /* No way, we need to wait for the next BGSAVE in order to
             * register differences */
            c->replstate = REDIS_REPL_WAIT_BGSAVE_START;
            redisLog(REDIS_NOTICE,"Waiting for next BGSAVE for SYNC");
        }
    } else {
        /* Ok we don't have a BGSAVE in progress, let's start one */
        redisLog(REDIS_NOTICE,"Starting BGSAVE for SYNC");
        if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) {
            redisLog(REDIS_NOTICE,"Replication failed, can't BGSAVE");
            addReplyError(c,"Unable to perform background save");
            return;
        }
        c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
    }

    if (server.repl_disable_tcp_nodelay)
        anetDisableTcpNoDelay(NULL, c->fd); /* Non critical if it fails. */
    c->repldbfd = -1;
    c->flags |= REDIS_SLAVE;
    c->slaveseldb = 0;
    listAddNodeTail(server.slaves,c);
    return;
}

그런데, 사실 이것 자체가 큰 문제는 아니었는데, 로드가 높다고 생각하고, twilio 운영팀은 Redis 재시작을 결정합니다. 그리고 master를 재시작 했는데, 기본 설정이 잘못되어 있었던 것입니다. 즉 RDB 파일을 읽을 것이라고 생각했는데 AppendOnlyFile을 읽어버린 거죠. 왜냐하면 AOF와 RDB 중에 AOF는 바로 직전 까지의 값을 가지고 있으므로 AOF 활성화 옵션이 있으면 rdb 대신에 AOF를 읽습니다. 아래의 코드를 보면 aof 설정이 켜져 있으면 aof를 읽는 것을 볼 수 있습니다.

/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
    long long start = ustime();
    if (server.aof_state == REDIS_AOF_ON) {
        if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        if (rdbLoad(server.rdb_filename) == REDIS_OK) {
            redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

그런데 여기서 twilio는 aof를 사용하지 않아서 빈 데이터를 로딩하게 되고, 이를 slave가 리플리케이션 받게 되어서 데이터가 날라가게 된 것입니다.

그렇다면, 이런 문제에 대해서 어떻게 대처를 해야할까요?

1] Full resync에 대해서는 2.8 부터는 partial sync라는 기능이 들어가서, 만약 이전 마스터와 동일한 runid를 가지고 있고, 현재 저장된 버퍼 안까지만의 차이라면, 그냥 이 차이값만 받아서 업데이트 하게 됩니다. 아직 2.6.x는 못썻으니…

2] 설정에 대한 검증을 강화해야 합니다. 처음부터 aof를 안쓴느데, aof 관련 설정이 켜져있었다는 것은 뭔가 실수가 있었다는 것입니다. 이런것도 검증하는 서비스가 필요하지 않을가 싶네요.

* 핵심정리 중 하나: slave 가 sync 해야할때의 rdb는 옵션을 끄더라도 무조건 발생합니다. 이것이 메모리를 적당히 나눠서 rdb 관련 이슈를 줄여야 하는 이유중에 하나입니다.

[발 번역] Scale PHP on Ec2 to 30,000 Concurrent Users / Server

해당 문서는 https://coderwall.com/p/__z9ia 를 발 번역한 것입니다. 오역에 주의하시기 바랍니다. 장인의 숨결이 느껴지네요.

RockThePost.com은 아마존 EC2 에서 LAMP 스택을 사용합니다. 동시에 하나의 메일을 백만명의 투자자들에게 보내는 기능을 준비하고 있습니다. 딱, 두명 뿐인 엔지니어링 팀이라, 동시에 몇명의 사람에게 메일을 보낼 수 있는지 빨리 확인해야 했습니다.

서비스는 PHP Zend 프레임워크 2를 사용했고, 웹 서버는 두 대의 m1.medium EC2 장비를 로드 분산을 위해서 ELB에 붙여서 사용했습니다. 그리고 백엔드는 MySQL 을 Master/Slave 방식으로 사용했습니다. 일반적으로 조그만 회사에서 사용하는 것과 거의 유사합니다.

순서 무시하고 그냥 제 생각을 적습니다.

  • PHP의 APC를 사용하자. 왜 이게 디폴트 설정이 아닌지 모르겠제만, APC 설정을 켜두면 성능이 잘 나옵니다.(역자 주: 밑에 코멘트를 보면 APC 사용을 중지하고, Zend의 새로운 OpCache로 옮겼더니, 전체적으로 초당 리퀘스트 처리가 10%~30% 올라갔다고 합니다. PHP 5.5에 들어가 있지만, 5.3, 5.4에서도 사용이 잘 된다고 하네용. PHP-FPM 형태로 사용하면 될듯합니다.)
  • php 요청이 아니면 전부 CDN에 넣어두자. 스태틱 파일 요청 때문에, 서버에 부하를 줄 필요가 없다. 우리는 모든 것을 S3에 올려두고, CloudFront를 사용했다.[부연 설명] : CloudFront 는 최근에 몇몇 이슈가 있었다. CloudFront 이슈가 해결되기 전까지는 모든 스태틱 파일이 S3에서 바로 처리되도록 설정을 변경했다. (역자 주: 무슨 이슈가 있었는지는 잘 모르겠습니다.)
  • PHP code에서 다른 서버로 연결을 만들지 말자. 예를 들어, DBMS나 memcached에 접속하는 것입니다. 무조건 해야 되는 상황이 아니라면, 쓰지 말자. 대부분의 PHP 웹 서비스가 백엔드 세션 관리를 위해서 MySQL을, 캐싱을 위해 멤캐쉬 풀을 이용할꺼라고 생각한다. 다른 서버로의 접속을 코드의 실행 흐름중에 만드는 것은 효과적이지 않다. CPU가 연산 중에, 컨넥션을 만들기 위해서 대기해야 한다. 그 대신에, APC key/value 스토어를 PHP에서 데이터를 저장하기 위해서 사용하고, 전체 페이지 캐싱에는 Varnish를 사용하자.
  • Varnish 를 쓰라는 것을 다시 한번 강조한다. 내 경험상, 대부분의 페이지는 절대로 변경되지 않거나, 거의 어쩌다가 한번 변경된다. Varnish는 웹 서버 캐싱계의 Memcache/ModRewrite 다. 웹 서버에 Varnish를 추가하면 부하 테스트시에 엄청난 성능차를 보여준다.
  • c1.xlarge 인스턴스를 사용하자. m1.medium은 겨우 한개의 CPU로 모든 리퀘스트를 처리해야 한다. 8개의 CPU를 가진 c1.xlarge로 업그레이드 한 뒤에, 실제로 비용이 줄었다는 것을 알게 되었다. 부하 테스트 동안, apt-get install nmon을 해서 nmon을 설치하고, nmon을 실행시켜서 CPU를 모니터링 했는데, 말 그대로, 8개의 코어가 페이지 요청을 마구마구 처리하고 있는 것을 볼 수 있을 것이다.

Google Analytics는 평균적으로 유저가 얼마 정도의 페이지를 요청하는 지를 보여준다. 이 정보를 이용해서, 위의 내용들을 적용한 뒤, 부하 테스트를 Siege를 이용해서 수행했다. 스태틱 페이지는 한 서버당 동시에 30,000 명의 유저를 처리할 수 있었고, PHP가 생성해야 하는 내부 페이지는 한 서버당 동시에 1000개 이상의 세션을 처리할 수 있을꺼라고 생각한다. 다수의 이메일 발송을 위해서, 차후에, 여러 대의 c1.xlarge 서버들을 추가할 것이고, 실제 서비스 부하에 따라서 개수를 천천히 줄일 것이다.

Finally, we will do more work to make our code itself more scaleable… however, the code is only about 4 months old and this is the first time we’ve ever been challenged to actually make it scale.

마지막으로, 코드를 확장 가능하게 만들게 더 많은 노력을 들이겠지만, 현재 코드가 아직 4개월 밖에 안됬고, 앞으로 계속 확장가능하도록 노력할 것이다.

RockThePost.com 에 대해서

새로운 스타트업 기업을 위해서 기업가가 돈을 모을 수 있도록 돕고 있습니다. 소셜 크라우드 펀딩을 이용하고 싶으면 “HACKERNEWS”라는 쿠폰을 사용하면 50% 할인 받으실 수 있습니다.

[입 개발] 왜 Redis는 마스터/마스터 리플리케이션을 지원하지 않을까?

입으로만 개발하고 손으로는 개발하지 않는 입개발 전문가로써 쿨럭… 지금 무슨 얘기를… 다시 원래의 이야기로 돌아가서… 가끔씩 외국도 그렇고 국내도 그렇고 사람들은 다 똑같은가 봅니다. 왜 레디스는 마스터/마스터 리플리케이션을 지원하지 않는가에 대해서 물어봅니다. 뭐, 클러스터만 되면 어느정도 해결이 될것 같기도 하지만… 레디스의 경우 마스터/슬레이브에 대한 리플리케이션이 존재하는 지라… 이것 조금만 고치면 되지 않겠냐라고 생각하시는 분들도 많은 것 같습니다.(저만 그렇게 생각할지도…)

그런데 마스터/마스터 리플리케이션 자체가 그렇게 쉬운 기술이 아닙니다.(저한테만!!! 어려울지도…) 그리고 현재 레디스의 마스터/슬레이브 리플리케이션 구조가 마스터/마스터를 지원하기 힘든 아키텍처로 구성이 되어 있습니다. 그렇다면 먼저 레디스의 리플리케이션 구조를 살펴보는게 우선일듯 합니다.

간단하게 설명하면, 자신이 요청 받은 커맨드가 update 관련(insert/update/delete) 라면 그냥 그대로 자신의 슬레이브 노드들에게 다시 전달해 버립니다. 뭐, 그게 어째서 라고 말씀하시는 분들은 DB의 Bin 로그 같은 것들도 비슷한 방식이지 않냐라고 항변 하시면 할 말이 없습니다만… 여기서 그게 가능할려면 전제조건이 하나 붙어야 합니다. 즉, 자신에게 다시 해당 패킷이 돌아왔을 때, 이게 발신자가 자신이면 거기서 멈춰야 하는 것이죠. 그런데 레디스에서는 이런 정보를 붙일 부분이 없어서, 그렇게 구현을 해버리면 자신에게 해당 패킷이 오면, 계속 다시 무한 반복해서 돌아오게 될것입니다. 즉, 기존의 리플리케이션 구조가 바뀌지 않는 한은 아직은 어렵다라는 거죠.

실제로 Hbase 같은 경우는 클러스터간 마스터/마스터 리플리케이션을 위해서, 패킷에 누가 최초에 명령을 수행해서 리플리케이션 되는 것인지 정보가 추가로 붙어서 전달되게 됩니다. HBase Replication(http://blog.cloudera.com/blog/2012/07/hbase-replication-overview-2/)을 보면 ClusterID를 설정해서 보낸다고 되어 있습니다. 레디스는 현재 이런 작업이 안되어 있는 겁니다. 사이클이 생겼을 때는 또 어떻게 처리할 것인지 등등등 고민거리가 여기에도 꽤 많이 있습니다.

redis.c 의 call() 함수를 보면 propagate() 를 사용하는데, 여기서 AOF와 Replication을 실제로 호출하게 됩니다.

void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)
{
    if (server.aof_state != REDIS_AOF_OFF && flags & REDIS_PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc);
    if (flags & REDIS_PROPAGATE_REPL)
        replicationFeedSlaves(server.slaves,dbid,argv,argc);
}

그리고 replication.c 의 replicationFeedSlaves()를 살펴봅니다. 사실 여기서 제 뻥을 하나 눈치채셔야 합니다. 그대로 보낸다라고 했지만, 실제로는 여기서 못보낼 상황이라면 계속 버퍼를 모으게 됩니다. 보낼 수 있다면 바로보냅니다. 여기서도 최종적으로 데이터 변경이 되는 경우에만 리플리케이션이 됩니다. 예를 들어서 이미 지워진 데이터에 대해서 계속 del을 해도 해당 명령은 전달이 되지 않습니다. 이를 확인하는 것은 telnet 으로 레디스에 붙은 뒤에 sync 라는 명령을 주고 실제 레디스 서버에 set 이나 del을 해보시면 됩니다.

SELECT
$1
0
*3
$3
set
$1
a
$3
123
*2
$3
del
$1
a
*1
$4
PING
*1
$4
PING