fasttext 의 Tag Prediction 따라하기
Facebook에서 공개한 fasttext 를 가지고 놀던중 text classification에 대한 예제는 존재하지만, tag prediction에 대한 예제가 없어서 직접 작성해 보기로 했습니다.
tag prediction에 대한 내용은 다음 논문에서 볼 수 있습니다.
Bag of Tricks for Efficient Text Classification
[2] A. Joulin, E. Grave, P. Bojanowski, T. Mikolov, Bag of Tricks for Efficient Text Classification
@article{joulin2016bag,
title={Bag of Tricks for Efficient Text Classification},
author={Joulin, Armand and Grave, Edouard and Bojanowski, Piotr and Mikolov, Tomas},
journal={arXiv preprint arXiv:1607.01759},
year={2016}
}
먼저 tag prediction을 하기 위해서 Training 데이터를 가져와야하는데, Training 데이터는 Yahoo에서 공개한 YFCC100M이라는 1억개 짜리 Flickr 데이터입니다. 먼저 Yahoo에 로그인한 다음에 본인의 AWS 정보를 적어주면 s3cmd 명령어를 통해서 S3에 있는 데이터를 다운 받을 수 있습니다. 이 과정은 생략하겠습니다. 압축된 데이터 총 용량은 14GB입니다.
다운로드가 완료되면 다음과 같은 파일 목록을 확인할 수 있습니다.
forcemax@forcemax-envy14:~/development/tagpred$ ls -ahl ../yfcc100m/
합계 17G
drwxrwxr-x 2 forcemax forcemax 4.0K 10월 7 17:57 .
drwxrwxr-x 19 forcemax forcemax 4.0K 10월 4 17:16 ..
-rw-rw-r-- 1 forcemax forcemax 6.9K 9월 30 03:35 WebscopeReadMe.txt
-rw-rw-r-- 1 forcemax forcemax 2.6G 9월 30 03:40 yfcc100m_autotags-v1.bz2
-rw-rw-r-- 1 forcemax forcemax 1.1G 9월 30 03:35 yfcc100m_dataset-0.bz2
-rw-rw-r-- 1 forcemax forcemax 1.1G 9월 30 03:35 yfcc100m_dataset-1.bz2
-rw-rw-r-- 1 forcemax forcemax 1.1G 9월 30 03:35 yfcc100m_dataset-2.bz2
-rw-rw-r-- 1 forcemax forcemax 1.1G 9월 30 03:35 yfcc100m_dataset-3.bz2
-rw-rw-r-- 1 forcemax forcemax 1.1G 9월 30 03:35 yfcc100m_dataset-4.bz2
-rw-rw-r-- 1 forcemax forcemax 1.1G 9월 30 03:35 yfcc100m_dataset-5.bz2
-rw-rw-r-- 1 forcemax forcemax 1.4G 9월 30 03:35 yfcc100m_dataset-6.bz2
-rw-rw-r-- 1 forcemax forcemax 1.4G 9월 30 03:37 yfcc100m_dataset-7.bz2
-rw-rw-r-- 1 forcemax forcemax 1.4G 9월 30 03:37 yfcc100m_dataset-8.bz2
-rw-rw-r-- 1 forcemax forcemax 1.4G 9월 30 03:37 yfcc100m_dataset-9.bz2
-rw-rw-r-- 1 forcemax forcemax 2.0G 9월 30 03:46 yfcc100m_hash.bz2
파일 중에서 우리는 yfcc100m_dataset-0에서 yfcc100m_dataset-9까지(이하 dataset 파일) 사용할 것입니다. 먼저 bzip2 명령으로 압축을 풀어 둡니다. (오래걸립니다)
forcemax@forcemax-envy14:~/development/tagpred$ bzip2 -d ../yfcc100m/*dataset*.bz2
논문 내용에 100번 이상 나오지 않는 단어는 제거한다고 되어 있으니 이를 위한 작업을 합니다.
먼저 dataset 파일에서 word count를 구해야 하는데, 압축이 해제된 dataset 파일이 총 45GB 입니다. 데이터량이 너무 많기때문에 시스템 성능을 최대한 사용하기 위해서 python의 multiprocessing 라이브러리를 사용합니다.
참고로 dataset 파일은 tab(\t)로 구분되어 있으며 다음과 같은 필드로 구성되어 있습니다. 이 중에서 우리는 Title, Description, User Tag를 사용할 것입니다.
* Photo/video identifier
* User NSID
* User nickname
* Date taken
* Date uploaded
* Capture device
* Title
* Description
* User tags (comma-separated)
* Machine tags (comma-separated)
* Longitude
* Latitude
* Accuracy
* Photo/video page URL
* Photo/video download URL
* License name
* License URL
* Photo/video server identifier
* Photo/video farm identifier
* Photo/video secret
* Photo/video secret original
* Extension of the original photo
* Photos/video marker (0 = photo, 1 = video)
다음 코드를 보면 Title, Description, User Tag를 파일에서 추출한 다음 내용에 있는 Percent-encoding 을 제거하고 User Tag는 Comma로 구분, Title과 Description은 공백으로 구분하여 word count를 계산하도록 하였습니다.
def wordcount_worker(path):
print('wordcount worker started : %s' % path)
wordcount = collections.Counter()
count = 0
words = []
with open(path) as f:
for line in f:
count += 1
sline = line.split('\t')
# user tag
words += [k.strip() for k in clean_str(urllib.parse.unquote(sline[8])).replace('+', '_').split(',') if k.strip() != '']
# title & description
words += [k.strip() for k in clean_str(urllib.parse.unquote_plus(sline[6] + ' ' + sline[7])).split() if k.strip() != '']
if count % 100000 == 0:
try:
words[:] = (v for v in words if v != '')
except ValueError:
pass
wordcount.update(words)
words[:] = []
if count % 1000000 == 0:
print('%s : line %d passed' % (path, count))
print('wordcount worker finished : %s' % path)
return wordcount
위 코드를 multiprocessing 라이브러리를 사용하여 구동시키는데, dataset 파일당 하나의 thread가 동작하도록 하였습니다. 다만 dataset 파일 하나의 크기가 크다보니 메모리 사용량이 상당히 커서 12GB RAM이 장착된 제 PC에서는 동시에 2개만 돌리도록 하였습니다.
wordcount = collections.Counter()
with Pool(processes = 2) as pool:
jobs = pool.imap_unordered(wordcount_worker, files)
for res in jobs:
wordcount.update(res)
전체 dataset 파일에 대한 word count를 구한 후에 100번 이상 발생한 단어만 뽑습니다.
keepwords = set()
for k in wordcount.keys():
if wordcount[k] >= 100:
keepwords.add(k)
이제 전체 dataset 파일에서 keepwords에 포함된 단어만 남기고 나머지 단어는 제거한 후에 별도 파일로 저장합니다.
def clean_data(tags, titles, descriptions):
string = ""
for t, ti, desc in zip(tags, titles, descriptions):
t_tags = clean_str(urllib.parse.unquote(t)).replace('+', '_').split(',')
t_tags = [k.strip() for k in t_tags if k.strip() in keepwords]
t_tags = ['__label__'+k for k in t_tags]
t_titles = clean_str(urllib.parse.unquote_plus(ti))
t_titles = [k.strip() for k in t_titles.split() if k.strip() in keepwords]
t_descriptions = clean_str(urllib.parse.unquote_plus(desc))
t_descriptions = [k.strip() for k in t_descriptions.split() if k.strip() in keepwords]
if len(t_titles) < 1 and len(t_descriptions) < 1:
continue
if len(t_tags) < 1:
continue
if len(t_tags) == 1 and t_tags[0] == '__label__':
continue
string += "%s %s %s\n" % (' '.join(t_tags), ' '.join(t_titles), ' '.join(t_descriptions))
return string
def clean_worker(path):
print("clean worker started : %s" % path)
tags, titles, descriptions = ([] for i in range(3))
count = total_count = 0
with open(path + '_cleaned', 'w') as w:
with open(path) as f:
for line in f:
count += 1
total_count += 1
sline = line.split('\t')
titles.append(sline[6])
descriptions.append(sline[7])
tags.append(sline[8])
if count == CLEANED_TRAIN_FILE_WRITE_INTERVAL:
w.write("%s" % clean_data(tags, titles, descriptions))
print("%s line processed : %d" % (path, total_count))
tags[:], titles[:], descriptions[:] = ([] for i in range(3))
count = 0
if len(tags) > 0:
w.write("%s" % clean_data(tags, titles, descriptions))
print("clean worker finished : %s" % path)
위 코드 역시 multiprocessing 라이브러리를 사용하여 구동시키는데, 전체 word count를 구하는 작업보다는 메모리 사용량이 적으므로 최대한 많은 thread를 사용하도록 합니다.
with Pool(processes=6) as pool:
jobs = pool.imap_unordered(clean_worker, files)
for res in jobs:
pass
위 내용에 대한 전체 코드는 다음 주소에서 확인할 수 있습니다.
https://gist.github.com/forcemax/a6b5885fea859b43763f7712e82d546b
Intel i7-6700hq, 12GB RAM, SSD를 사용한 시스템에서 실행시키면 약 2시간 가량 소요됩니다.
데이터가 준비 되었으니 이제 fasttext를 이용하여 training을 합니다.
먼저 train을 위한 데이터가 아직 10개의 dataset 파일로 분리되어 있으니 하나의 파일로 합쳐줍니다.
forcemax@forcemax-envy14:~/development/tagpred$ ls -alh ../yfcc100m/*_cleaned
-rw-rw-r-- 1 forcemax forcemax 799M 10월 13 16:05 ../yfcc100m/yfcc100m_dataset-0_cleaned
-rw-rw-r-- 1 forcemax forcemax 799M 10월 13 16:05 ../yfcc100m/yfcc100m_dataset-1_cleaned
-rw-rw-r-- 1 forcemax forcemax 799M 10월 13 16:03 ../yfcc100m/yfcc100m_dataset-2_cleaned
-rw-rw-r-- 1 forcemax forcemax 798M 10월 13 16:02 ../yfcc100m/yfcc100m_dataset-3_cleaned
-rw-rw-r-- 1 forcemax forcemax 798M 10월 13 15:48 ../yfcc100m/yfcc100m_dataset-4_cleaned
-rw-rw-r-- 1 forcemax forcemax 799M 10월 13 15:48 ../yfcc100m/yfcc100m_dataset-5_cleaned
-rw-rw-r-- 1 forcemax forcemax 1.6G 10월 13 15:51 ../yfcc100m/yfcc100m_dataset-6_cleaned
-rw-rw-r-- 1 forcemax forcemax 1.6G 10월 13 15:51 ../yfcc100m/yfcc100m_dataset-7_cleaned
-rw-rw-r-- 1 forcemax forcemax 1.6G 10월 13 15:52 ../yfcc100m/yfcc100m_dataset-8_cleaned
-rw-rw-r-- 1 forcemax forcemax 1.6G 10월 13 15:52 ../yfcc100m/yfcc100m_dataset-9_cleaned
forcemax@forcemax-envy14:~/development/tagpred$ cat ../yfcc100m/*_cleaned > train.txt
논문 내용을 보면 Train set, Validation set, Test set으로 데이터를 구분하는데, 구분하는 기준이 없어서 일단 전체 데이터를 Train Set이라고 생각하고 Training 합니다.
논문에 나온 내용인 hidden unit 200, bigram, epoch 5로 training합니다. 제 PC에서는 loss가 negative sampling 외에는 training이 안됩니다. RAM이 더 큰 장비에서는 hierarchical softmax를 사용할 수도 있을 것이라 생각됩니다. 그러나, softmax는 테스트 안해보시는게 좋을겁니다. label이 몇십만개 단위이기 때문에 training에 너무 많은 시간이 소요됩니다. (fasttext는 컴파일 해두셨겠죠?)
forcemax@forcemax-envy14:~/development/tagpred$ time ../fastText/fasttext supervised -input train.txt -output yfcc100m -minCount 1 -dim 200 -lr 0.05 -wordNgrams 2 -bucket 10000000 -epoch 5 -loss ns -thread 6
논문 내용에는 vocabulary size가 297,141이고 tag가 312,116인데 Train set, Validation set, Test set으로 구분을 안해줘서 더 많은 값이 나왔네요. 제 시스템에서 28분이 넘게 소요되는데, 논문에는 13분 정도 소요됐다고 나옵니다. 논문에서 사용한 시스템은 20 thread를 사용했으니...
최종 loss가 1.112073인데 epoch을 늘려주면 좀 더 줄어듭니다. 논문에 나온 파라미터를 그대로 사용했더니 결과가 저렇게 되네요. 이 부분은 직접 확인해 보시기 바랍니다.
논문에서 테스트한 값을 직접 넣어서 predict 해보겠습니다.
forcemax@forcemax-envy14:~/development/tagpred$ echo "christmas" | ../fastText/fasttext predict yfcc100m.bin -
__label__christmas
여기까지 입니다. 작성된 코드의 성능을 개선하거나 잘못된 부분이 있으면 알려주세요. ^^
*** 2016년 10월 18일에 fasttext github에 preprocessed YFCC100M 데이터가 올라왔습니다.
해당 데이터는 https://research.facebook.com/research/fasttext/ 페이지에서 다운 받을 수 있습니다. 압축된 상태로 7.5GB 이고, 압축을 해제하면 18GB 입니다.
Train Set, Validation Set, Test Set으로 구분되어 있으므로 바로 fasttext를 이용해서 training 할 수 있습니다.
또한, 10월 18일자로 fasttext의 supervised 명령에 minCountLabel 옵션이 추가되었습니다. 논문에 나온 내용인 100번 이상 나오지 않는 단어를 제거하고 training 하려면 minCountLabel 옵션과 minCount 옵션을 모두 100으로 지정하면 됩니다.
Twitter sample public status rolling top count project for Apache storm starter
Reference
1. Storm-starter : https://github.com/apache/storm/tree/master/examples/storm-starter
2. Storm : https://github.com/apache/storm
3. Implementing Real-Time Trending Topics With a Distributed Rolling Count Algorithm in Storm : http://www.michael-noll.com/blog/2013/01/18/implementing-real-time-trending-topics-in-storm/
소개
회사 업무로 Apache Storm(이하 Storm)과 관련된 프로젝트를 완료하고 휴식중에, Storm 기초를 설명할 수 있을만한 예제를 만들어보기 위해서 Storm Starter를 참조하여 간단한 프로젝트를 만들었다.
이 프로젝트는 Twitter Sample Public Status API(https://dev.twitter.com/streaming/reference/get/statuses/sample)를 사용하여 Twitter realtime stream data의 일부를 Input으로 하고, HashTag 정보를 추출한 후 일정 시간 간격(emit frequency)으로 일정 시간 동안(window length)의 일정 갯수(TOP_N)의 Top HashTag를 생성하여 출력하는 프로젝트이다.
Storm-starter project에서 많은 소스코드를 가져 왔으며 Twitter Library는 Twitter4J를 사용한다.
Project source : https://github.com/forcemax/storm_twitter_hashtag
실행하기
0. Prerequisites
Java 1.7 이상, Storm 0.9.5, Maven, Git
(Twitter API를 사용하기 위한 consumerKey, consumerSecret, accessToken, accessTokenSecret을 변경하지 않으면 실행이 안된다.)
1. 소스 가져오기
$ git clone https://github.com/forcemax/storm_twitter_hashtag
2. 소스 빌드하기
$ mvn clean package
3. Storm Cluster에 Topology submit
$ storm jar StormTwitterHashtag-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.embian.forcemax.twitter.StormTwitterHashtagTopologyRunner server
4. Storm UI에서 확인하기
그림. topology 목록
그림. topology 상세 정보
설명
- Rolling Count Algorithm은 Reference 3번 사이트를 참조
- Topology는 다음과 같이 구성되어 있다. Twitter API를 사용하기 위해서는 Twitter에 App 등록을 해야하며, App 등록을 하면 consumerKey, consumerSecret, accessToken, accessTokenSecret 값을 얻을 수 있다. 다음 코드에 해당 값을 넣어서 사용한다.
그림. Topology 구성도
- TwitterSpout은 Twitter4J Library를 사용하며, LinkedBlockingQueue를 사용해서 새로운 Public Status가 있을때 Queue에 저장한다. nextTuple() 호출시에 Queue에서 꺼내서 ExtractHashTagBolt에 넘긴다.
- ExtractHashTagBolt는 받은 Public Status에서 HashTag만 뽑아내서, RollingCountBolt로 넘긴다. HashTag의 갯수 만큼 emit이 발생한다.
- RollingCountBolt는 생성할 때 인자로 받은 window length와 emit frequency 값을 바탕으로, emit frequency마다 window length에 속하는 데이터에서 word별 count를 계산해서 IntermediateRankingsBolt로 넘긴다. 이때, emit frequency마다 emit을 하기 위해서 TickTuple을 사용하는데, TickTuple은 Storm 0.8에 새로 들어간 기능이며 Component(Spout, Bolt)내에서 일정 주기 마다 Tuple을 발생시키는 기능이다.
- IntermediateRankingsBolt와 TotalRankingsBolt는 생성할때 인자로 topN, emit frequency를 받으며, 입력된 word별 count를 바탕으로 상위 topN개의 word와 count를 뽑아내고 emit frequency 마다 emit한다. IntermediateRankingsBolt는 parallelism hint를 크게 주어 map-reduce 구조에서 map의 역할을 하고, TotalRankingsBolt는 parallelism hint를 1로 주고 reduce의 역할을 한다. emit frequency마다 emit을 하기 위해서 RollingCountBolt와 마찬가지로 TickTuple을 사용한다.
- 마지막으로 PrinterBolt는 TotalRankingsBolt에서 emit한 Tuple을 출력하기 위해서 사용하며 특별한 기능은 없다.
이 프로젝트는 Storm의 Spout(nextTuple과 open), Bolt(execute, prepare)와 Topology wiring에 대한 이해만 있다면 코드를 보는데 아무 무리가 없을 정도로 간단한 예제이다. 그러나 외부 서비스(Twitter)과의 연계를 통한 Spout 구성, Atomic하게 역할을 분리한 Bolt, parallelism hint를 조절하여 성능을 향상시키는 방법을 확인해 보기에 알맞은 예제이다.
Twitter Sample Public Status API는 10분에 20000 statuses 정도의 데이터밖에 제공하지 않으므로, 한대의 Storm에서 처리하기에 충분하다.
초보자가 알아보기 쉽게 코드가 구성되어 있으니, Storm을 이용하여 realtime CEP 엔진을 공부하려는 분들에게 많은 도움이 되었으면 한다.
Apache Storm Cluster Fault Tolerance
Reference : https://storm.apache.org/documentation/Fault-tolerance.html
Apache Storm(이하 Storm) Cluster를 Production 단계에서 사용하기 위해 고려하다보면 Nimbus의 SPoF(Single Point of Failure) 여부에 대해서 고민하지 않을 수 없다. 자세하게 Storm Cluster의 Fault Tolerance에 대해서 살펴보면 다음과 같다.
1. Worker가 Down되면?
Worker는 Supervisor에 의해서 관리되며 Worker가 Down될 경우 Supervisor가 Worker를 재기동(restart)한다. 그러나 Worker가 기동되는 단계에서 지속적으로 문제가 발생해서 기동에 실패하면, Nimbus에 의해서 다른 Supervisor에게 넘긴다.
2. Node가 Down되면?
여기서 Node는 장비(Machine)을 칭하며, Supervisor가 구동중인 장비로 규정한다. Node가 Down될 경우 Nimbus가 이를 감지하고 해당 Supervisor에서 동작된던 task를 다른 Supervisor로 넘긴다. 이 경우 다른 Supervisor에서 Worker가 새로 기동될 수도 있고, 현재 기동되어 있는 Worker에 task를 할당하기도 한다.
3. Nimbus 또는 Supervisor가 Down되면?
Nimbus와 Supervisor는 Storm에서 제공하는 문서에 의하면 Fail-Fast & Stateless 디자인이다. monit이나 daemontools와 같은 툴을 사용해서 process가 종료되면 자동으로 재기동 되도록 구성하는 것이 좋다. 좋은 정도가 아니라 꼭 이렇게 하라고 한다. 상태 정보가 Zookeeper 또는 디스크에 저장되어 있으므로 재기동시에 이를 읽어들여 기존 상태와 같이 구성한다.
4. Nimbus는 SPoF인가?
Nimbus node가 Down되더라도 다른 Node에서 동작중이던 Worker는 영향이 없다. 만약 다른 Node에서 동작중이던 Worker가 Down되더라도 해당 Node에서 동작중인 Supervisor에 의해서 재기동된다. 그러나 Worker가 재기동될 때 지속적으로 문제가 발생해서 기동에 실패한다면, 해당 Worker는 Down상태로 남는다.
그러므로 "Nimbus는 SPoF인가?"라는 물음에 대답은 "Nimbus는 일종의 SPoF로 볼 수 있다."이다. 그러나, 실제로 Nimbus node가 Down되어 있는 중에 비극적인 일(Worker가 막 Down된다던지...)만 발생하지 않는다면 큰 문제는 아니다. 향후 Nimbus의 HA를 지원할 계획은 갖고 있다.
결과적으로,
Nimbus Node(Nimbus process가 아니라)가 Down 되더라도 다른 Node(Supervisor Node)들은 정상적으로 Topology를 실행하고 있는 상태이다. 그러나, 정상적인 상태로 복구하기 위해서는 Nimbus Node를 복구해야한다.
여기서 Nimbus Node를 복구하는 방법에 두가지 경우가 있을 수 있다.
1. Nimbus Node가 상태 정보 데이터(in Disk)를 가지고 복구되는 경우 : Nimbus가 상태 정보를 디스크에서 읽어 들일 수 있으므로, 기존 상태 정보를 가지고 기동된다.
2. Nimbus Node에 상태 정보 데이터(in Disk)를 가지고 복구 되지 않는 경우 : 기존 정보가 없으므로 Storm Cluster가 새로 기동되는 상태이다. 이 경우 기존 동작중인 Supervisor가 새로운 Nimbus에 연결되면서 기존 Worker를 제거한다. 그러므로, Topology를 다시 Submit해야 한다.
PS. Storm 1.0.0 부터는 HA Nimbus가 지원됩니다~~
Dockerizing Embian eFolder Server Application
최근 이슈가 되고 있는 Docker를 제대로 한번 사용해보고 경험해보고 싶어서 가장 많이 만져봤던 Embian eFolder(이하 eFolder) Server를 Dockerize 하기로 하였다. eFolder Server를 초보가 설치하는데 하루가 넘게 걸리는 것을 보면 Dockerize할 대상으로 적합하다고 생각하였다.
Docker에 대한 기본적인 설명은 생략한다. (nacyot님의 도커(Docker) 튜토리얼 : 깐 김에 배포까지)
작업환경 : Ubuntu 14.04 LTS
1. Docker 설치
Docker 설치는 docker 사이트에 자세하게 나와있으니 생략.
Docker Documentation Installation Guide
2. Dockerfile 작성
Dockerfile : Docker image를 build할때 어떠한 내용으로 bulid할 것인지 정의해 놓은 파일.
https://github.com/forcemax/efolder-docker/blob/master/Dockerfile
# # eFolder Dockerfile # # Pull base image. FROM ubuntu:14.04 MAINTAINER Jae-cheol Kim <forcemax@gmail.com> # Install apache, mysql, mod-perl, php5 ENV DEBIAN_FRONTEND noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends \ apache2 \ apache2-data \ && apt-get install -y --no-install-recommends \ libapache2-mod-perl2 \ libncurses5-dev \ libdbi-perl \ libtext-iconv-perl \ libtimedate-perl \ libdate-calc-perl \ libdbd-mysql-perl \ libnet-dns-perl \ libmime-lite-perl \ libossp-uuid-perl \ libemail-address-perl \ libmailtools-perl \ libsoap-lite-perl \ libsphinx-search-perl \ && apt-get install -y --no-install-recommends \ libapache2-mod-php5 \ php5-mysql \ && apt-get install -y --no-install-recommends \ git \ && apt-get install -y --no-install-recommends \ mysql-server \ && apt-get install -y --no-install-recommends \ sphinxsearch \ && apt-get install -y --no-install-recommends \ supervisor \ && rm -r /var/lib/apt/lists/* RUN mkdir -p /var/log/supervisor RUN git clone https://github.com/forcemax/efolder /app RUN rm -rf /app/.git COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY 000-default.conf /etc/apache2/sites-available/000-default.conf COPY 00.CONFIG /app/etc/00.CONFIG COPY FILE_crawl.pl /app/etc/FILE_crawl.pl RUN chmod a+x /app/etc/FILE_crawl.pl COPY makeSphinxIndex.sh /app/etc/makeSphinxIndex.sh RUN chmod a+x /app/etc/makeSphinxIndex.sh COPY sphinx.conf /app/etc/sphinx.conf COPY searchd.sh /app/etc/searchd.sh COPY ddns.sh /app/etc/ddns.sh RUN chmod a+x /app/etc/ddns.sh COPY init_db.sh /app/doc/db/init_db.sh RUN chmod a+x /app/doc/db/init_db.sh COPY setup.php /app/www/eFolderAdmin/Config/setup.php COPY EmbianSoapHandler.pm /app/src/EmbianSoap/EmbianSoapHandler.pm COPY CONFIG.pm /app/src/FTPService/lib/perl/eFolder/CONFIG.pm RUN echo "" >> /etc/crontab RUN echo "36 4 * * * root ( cd /app/etc ; bash makeSphinxIndex.sh all 2> /dev/null > /dev/null )" >> /etc/crontab RUN echo "* * * * * root ( cd /app/etc ; bash makeSphinxIndex.sh delta 2> /dev/null > /dev/null )" >> /etc/crontab RUN ln -fs /usr/share/zoneinfo/Asia/Seoul /etc/localtime VOLUME ["/eFolder", "/var/lib/mysql"] EXPOSE 80 CMD ["/usr/bin/supervisord"]
FROM 에서 어떤 base image를 사용할 것인지
MAINTAINER 에서 누가 작성한 것인지
CMD 에서 image를 실행할 때 어떤 명령을 실행할 것인지
ENV, RUN, COPY 를 조합하여 image생성시에 어떠한 내용이 담길 것인지 지정한다.
eFolder Server는 apache2 + mod_perl + mod_php 를 사용하여 WEB, mysql을 사용하여 DB, 그리고 sphinxsearch를 사용하여 fulltext search를 조합하여 운영된다. 일반적으로 Docker는 하나의 process만 실행시킬 것을 권장하고 있으나 여러 daemon을 실행하기 위해서 supervisord를 사용하였다.
https://github.com/forcemax/efolder-docker/blob/master/supervisord.conf
[supervisord]
nodaemon=true
[program:mysqld]
command=/bin/bash -c "/usr/bin/mysqld_safe"
[program:cron]
command=/bin/bash -c "/usr/sbin/cron -f"
[program:searchd]
command=/bin/bash /app/etc/searchd.sh
startsecs=0
[program:ddns]
command=/bin/bash /app/etc/ddns.sh
startsecs=0
[program:apache2]
command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND"
3. DB 및 파일 데이터 저장소
eFolder는 웹하드 서비스를 위해서 만들어졌으니 당연히 파일 데이터가 저장된다. 또한 fulltext search 및 사용자 관리를 위해서 DB를 사용하니 DB 데이터 역시 저장된다.
Docker에서 container는 stateless이다. 그렇다면 "서버가 reboot되면 DB 및 파일 데이터는 사라지는데?" 또는 "image를 수정해서 다시 실행하면 어떡하지?"
이런 Persistent Data에 대한 필요성 때문에 Docker에서는 Data Volume Container라는 concept을 내놓았다. Data Volume Container는 실행 상태의 container가 아니며, 다른 container에서 Data Volume Container의 volume을 mount해서 사용하는 것이라 생각하면 된다.
- Data Volume Container 생성
sudo docker run -i -t --name efolder_mysql_data -v /var/lib/mysql -v /eFolder busybox /bin/sh
- eFolder Container에서 Data Volume Container 사용
sudo docker run -d -p 80:80 -e HOSTIPADDR=$HOSTIPADDR --volumes-from efolder_mysql_data forcemax/efolder:latest
4. Github 및 Docker Hub에 등록
eFolder 및 eFolder Dockerize 관련 내용은 모두 Github에 등록하였으며, 아무나 사용할 수 있도록 public으로 공개되어 있다.
Github eFolder Docker Repository
Docker Hub 에도 Repository를 생성하였으며, image를 automated build하도록 설정하였다. 참고로 eFolder는 Docker Hub에서 build하는데 약 15분이 필요하다.
다음 명령으로 image를 build하지 않고 바로 다운로드 받아서 사용할 수 있다.
sudo docker pull forcemax/efolder:latest
모든 작업을 마치고..
기존 서버들이 Ubuntu 12.04 LTS로 운영중이라 Ubuntu 14.04 LTS 에서 eFolder를 테스트하지 못했는데, Ubuntu 14.04 LTS에서 HTTP::Message Perl Module 버전이 올라가면서 기존 코드가 에러를 내서 코드 수정까지 진행해야 했다.
eFolder Server 설치를 편하게 할 수 있도록 하는 것이 우선이라 한 image에 모든 daemon을 운영하도록 하였으나, Scale-out을 고려하여 daemon별로 나누는 것도 고려해 볼만 하다.
JDK 7u40 VisualVM 사용을 위한 jstatd 설정
서버에 JVM에서 도는 프로그램의 메모리 사용량을 모니터링 할 일이 생겨서 관련 내용을 검색하던 중 VisualVM을 찾았고 이를 활용하기 위해서 jstatd를 서버에 구동해야 한다는 것을 확인하고 설정했다.
여러 자료를 확인해본 결과 다들 아래와 같이만 하면 된다고 했다.
~/jstatd.all.policy
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};
~/jstatd_run.sh
#!/bin/bash
$JAVA_HOME/bin/rmiregistry 2020 &
$JAVA_HOME/bin/jstatd -J-Djava.security.policy=jstatd.all.policy -p 2020 &
그런데 정보가 안나온다? 왜?
stackoverflow에 관련 자료를 검색해본다.
버그인거 같단다.
다음과 같이 바꿔주었더니 정상동작헀다.
~/jstatd_run.sh
#!/bin/bash
$JAVA_HOME/bin/rmiregistry 2020 &
$JAVA_HOME/bin/jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=192.168.0.42 -p 2020 &
192.168.0.42는 저 서버의 IP 주소이다. 저게 없어서 동작안했다는게 말이되나?
Social Curation Service
Social Curation Service (Crowd curating)
관련 블로그 : http://trendinsight.biz/archives/37990
대표적 사이트 Pinterest
pinterest 관련하여 자료를 찾던중 보았던 글 중에서 가장 좋았던 블로그
핀터레스트 관련 국내 기사 : http://www.hani.co.kr/arti/economy/economy_general/525367.html
Pinterest의 copy site로 유명한 Pinspire
http://www.pinspire.com/ (local site : http://www.pinspire.co.kr/ )
국내 비지니스 관련 기사 : http://sports.chosun.com/news/news.htm?id=201203210100152040012710&ServiceDate=20120321
대규모 웹하드 솔루션으로서의 eFolder 활용
eFolder 솔루션은 대규모 웹하드 서비스를 위한 솔루션이기도 하지만, 중소규모에서도 사용할 수 있는 가벼운 솔루션입니다. 이러한 eFolder 솔루션이 외부에 알려지지 않은 상황을 안타깝게 생각하여 2010년 부터 eFolder 솔루션의 공개를 주장해 왔으며, 2011년 3월 드디어 오픈소스로 배포하게 되었습니다.
eFolder 솔루션은 지난 10년간 FolderPlus, 소리바다, DayFolder등의 서비스에서 사용되었으며, FolderPlus에서는 지난 8년간 서비스에 사용되었습니다.
이 경험을 바탕으로 '대규모 웹하드 솔루션'에서 eFolder의 활용 방법을 기록으로 남기기 위해 블로깅을 하기로 결정하였습니다.
먼저, FolderPlus가 어떻게 운영되었으며, 어느 정도 규모의 서비스 였는지를 알아보겠습니다. FolderPlus는 2003년 (주)아이서브에서 런칭한 웹하드 서비스 입니다. 2010년 서비스가 중단되었지만, 웹하드 업계 5위의 서비스였습니다.
- 총 사용자 : 600 만명
- 총 저장공간 : 1.5 PB ( 1.5 PB = 1,500 TB = 1,500,000 GB )
- 총 네트워크 대역폭 : 60 Gb/s
- 총 사용서버 : 약 500 ea
- 웹 서버 : 약 200 ea
- 기타 서버 : 약 30 ea
- Uniform namespace 지원 (한 공간을 동시에 Read/Write 가능)
- Quota 및 NSS 지원
- 자체 개발한 인증 시스템
- NSS 모듈 제작 & MySQL을 사용하여 데이터 저장
- 웹하드 서비스 특성상 트래픽이 한 곳에 몰리는 현상이 발생
- 고성능 SSD 스토리지를 사용하여 고속 I/O를 처리하는 시스템 구성
- 요청 빈도가 높은 파일을 선택하는 알고리즘 구축
- Apache에 mod_perl2를 사용하여 작성
- I/O 처리에 특화된 CGI를 작성하여 웹하드 솔루션에 특화
- Server Side 데이터 처리를 바탕으로 높은 데이터 안정성 제공
http://www.openafs.org/
Carnegie Mellon 대학에서 개발된 AFS(Andrew File System) 에서 부터 시작되었으며, 분산 컴퓨팅 환경을 위한 FileSystem입니다.
기본적으로 CS(Client-Server) 구조로 구성되며, File Server 를 관장하는 DB Server가 존재합니다. 이 구조를 FolderPlus에서 사용한 방식으로 예를 들어 설명하겠습니다.
(아쉽게도, 이미지 캡춰를 하면서 마우스 포인터가 들어갔네요;;)
위 이미지는 FolderPlus의 CS(Client-Server) 구조를 매우 간략화한 구조도 입니다. 각 구성요소에 대해서 하나씩 설명하면 다음과 같습니다. (Client는 생략)
위의 구조가 보기에는 약간 복잡해 보이는데, 이는 OpenAFS가 가진 문제점 때문입니다. OpenAFS Client는 kernel level의 driver이기 때문에 문제가 발생하면 OS가 Kernel panic을 일으키며 정지하게 됩니다. 이는 전체 서비스에 영향을 줄 수 있기때문에, 하나의 서버에 장애가 발생하여도 서비스에 영향을 주지 않도록 하기 위해서 약간 복잡하기는 하지만 위와 같은 구성을 하게 되었습니다.
OpenAFS가 위와 같은 문제점을 가지고 있긴 하지만, 이는 장점에 비하면 아주 간단한 문제입니다. 위에도 설명했지만 Uniform namespace를 사용한다는 것, 그리고 Quota 및 NSS(Name Service Switch)를 지원한다는 점입니다. 대용량 웹하드 서비스를 구성하기 위해서 가장 필요한 요소입니다.
2. Quota 및 NSS 지원 : Quota는 간단하게 최대 디스크 사용량을 제한하는 기능입니다. POSIX 표준에도 있는 기능이니, 이 기능이 지원되면 사용자별 디스크 사용량 관리가 쉬워집니다. NSS는 사용자 인증, 즉 사용자별 Home Directory, ID, Password등의 인증 및 사용자 정보를 확인하고 받아올 수 있는 기능입니다. 이 역시 대부분의 UNIX 시스템에서 사용되고 있기 때문에, NSS가 지원되면 적은 노력으로 여러 시스템에서 운영할 수 서비스를 구축할 수 있습니다.
위 이미지에서는 간략화되어 있지만, 실제 FolderPlus에서는 다음의 규모로 운영되었습니다.
OpenAFS에 대해서 더 알고 싶으신 분은 다음 링크를 참고하세요.
http://www.ibm.com/developerworks/kr/library/os-openafs/index.html
2. UAS (자체 개발한 인증 시스템)
http://www.embian.com/Embian_eAccount.phtml
UAS는 (주)엠비안에서 자체 제작한 인증 시스템입니다. 사용자 인증을 가장 빠르게 처리하기 위해서 개발되었으며, FolderPlus에서는 600만명의 사용자를 대상으로 사용되었던 경험이 있습니다. 위 링크는 UAS에 대한 설명은 아니지만, UAS의 전신인 Embian eAccount에 대한 설명이 있는 링크이니, 참고하시면 좋을 것이라 생각됩니다.
UAS의 가장 큰 장점은 NSS를 사용한다는 것입니다. 위 OpenAFS에서도 설명되었지만 NSS를 지원하면 적은 노력으로 여러 시스템에서 운영할 수 있습니다. 즉, 플랫폼 독립적인 서비스를 구축할 수 있다는 것입니다.
더불어 과거 (주)엠비안 기술연구소 소장으로 재직하였던, 전성진님의 기술이 녹아들어가서 상당히 빠른 응답을 동작속도가 보장됩니다.
== 작성중입니다. ==
== 문의사항 알려주시면 해당 부분에 대해서도 글에 포함하도록 하겠습니다. ==
OpenLDAP에 계정 추가하기
먼저 ldif 파일을 다음과 같이 만든다.
uid: ldapaddtest
cn: ldapaddtest
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword: e2NyeXB0fSQxJG4wOXM0N3FFJE9JUlI4R1pJNjduZ2Q3UXJ5VThPZDE=
loginShell: /bin/bash
uidNumber: 10067
gidNumber: 100
homeDirectory: /home/ldapaddtest
다음 명령으로 위의 파일에 있는 계정을 추가한다.
위 명령을 실행시키면 비밀번호를 물어본다. /etc/ldap.secret 파일에 있는 비밀번호를 입력한다.
- 이제 비밀번호를 변경할 차례
luma를 설치하고 실행한다.
crypt 패스워드를 얻기 위해서는 다음과 같이 입력한다.
luma에서 위에 추가한 계정으로 이동한 후에 패스워드 필드를 수정을 선택하고 'crypt' 방식으로 패스워드를 변경해준다.
이렇게하면 계정 추가 끝
** 위에 ldapadd 명령에 -x가 붙어있다. -x는 sasl을 사용하지 않게 해준다.
위에 계정이 제대로 추가되었는지 확인하려면
MySQL Proxy
https://launchpad.net/mysql-proxy
http://forge.mysql.com/wiki/MySQL_Proxy
MySQL에 fail-over와 load balancing(round-robin)을 하기 위한 가장 쉬운 도구.
MySQL에서 특정 Thread가 "Sending data" 상태에서 오랜시간 머무른다면..
이 때문에 Table에 Lock이 걸려 다른 Query들이 다 대기상태가 된다.
MySQL의 Query Caching 시스템에 문제가 있을 수 있으니 query cache를 off해 보란다.
http://bugs.mysql.com/bug.php?id=45544
뭐.. 나아지기만 바란다 ㅠㅠ
PS. 그러나 제일 중요한건 Slow Query가 발생하는 원인이 되는 Query를 수정하는 것이지 않을까. ㅎㅎ