Nginx deploy! (+uWSGI +flask)

Nginx deploy! (+uWSGI +flask)

uWSGI

uWSGI
Flask(uWSGI)를 nginx에 연결하기

uWSGI를 먼저 설치하고 실행한다.

$ pip install uwsgi

나는 ini파일을 만들어 실행했다. python path 문제 때문에 그래야만 했다. virtualenv를 안 써서 그런지 자꾸 python path를 못 찾아서, sys.path를 치면 나오는 path 들을 죄다 갖다 박아줬다. 다른 실행 옵션들은 위 링크를 참조하자.

[uwsgi]
chdir=/home/khanrc/tworoom/flask
chmod-socket=666
callable=app
module=app
socket=/tmp/uwsgi.sock
pythonpath=/usr/lib/python2.7
pythonpath=/usr/lib/python2.7/plat-linux2
pythonpath=/usr/lib/python2.7/lib-tk
pythonpath=/usr/lib/python2.7/lib-old
pythonpath=/usr/lib/python2.7/lib-dynload
pythonpath=/usr/local/lib/python2.7/dist-packages
pythonpath=/usr/lib/python2.7/dist-packages
pythonpath=/usr/lib/python2.7/dist-packages/PIL
pythonpath=/usr/lib/python2.7/dist-packages/gtk-2.0
pythonpath=/usr/lib/pymodules/python2.7

간단하게 적었지만 이 과정에서 엄청 고생했다. 이제, 실행해보자.

$ uwsgi uwsgi.ini &

당연한 얘기지만 &를 빼면 foreground로 실행된다.

Nginx

설치부터 하자.

$ apt-get install nginx-full

설치하고 나면 설정파일을 수정해서 uwsgi와 연결해 주자.

server {
    listen 80;
    server_name ip_address;

    location / {
        try_files $uri @app;
    }

    location @app {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/uwsgi.sock;
    }
}

이제 nginx와 uwsgi, flask가 전부 연결되었다.

$ /etc/init.d/nginx restart

Done!

Problem

어? 다 된 줄 알았는데 request에 header가 없다. -_-;;
뭔가 하고 한참 삽질했는데, 보니까 우리 헤더 이름이 user_token인데 이게 규약에 어긋나는 것 같다. 자세한 건 여기를 참조하자: List of HTTP header fields

flask를 그냥 python app.py로 실행하면 자체 서버로 실행이 되는데, 이러면 이 자체 서버가 헤더를 바꿔주는 것 같다. user_token으로 보내도 User-Token으로 들어온다. 그런데 nginx는 그렇지 않다. user_token으로 보내면 그냥 버려버린다 -_-;;

그래서 user_token을 User-Token으로 변경하는 것으로 마무리했다.

'Server(Back-end)' 카테고리의 다른 글

Nginx deploy! (+uWSGI +flask)  (0) 2014.11.15
logstash (2) - with logrotate  (0) 2014.11.07
cron, crontab의 개념  (0) 2014.11.06
logstash with python  (0) 2014.11.05
docker global hack day에 다녀와서  (0) 2014.11.02
Docker & Vagrant  (0) 2014.11.02

logstash (2) - with logrotate

logstash (2) - with logrotate

khanrc: logstash with python에서 파이썬 logging모듈로부터 logstash를 거쳐 mongodb에 로깅하는것까지 성공했다. 허나 mongodbinsert속도가 그리 빠르지 않다는 멘토님의 조언에 따라, 몽고디비가 아닌 그냥 file에 쓰기로 했다.

우리 프로젝트는 로그를 기록하면 data analyzer가 로그를 읽어서 분석하는 구조로 되어 있다. 데이터 분석기는 일정 주기마다 로그를 읽어서 분석할것이다. 이 때, 이전에 읽은 로그는 놔두고 새로 들어온 로그만 읽어야 한다. 이를 어떻게 처리할 것인가?

logrotate

리눅스 패키지인 logrotate라는 것이 있다. 일정 주기별로 로그를 관리해준다. logrotate에서는 일정 주기별로 로그를 백업하고, 파일을 rotate하고, 이를 압축하며 메일로 전송하는 등 다양한 관리를 할 수 있다. 이를 이용하여 logstash의 로그를 관리해 보자.

먼저 /etc/logrotate.d/에서 config파일을 작성하자. /etc/logrotate.conf는 default설정 파일이다.

$ cd /etc/logrotate.d/
$ vim newsup

/var/log/newsup/user.log
{
    rotate 1008
    daily
    missingok
    nocompress
    create
}

자세한 옵션은 따로 구글링하자. 한가지 참고할 점은, rotate옵션은 파일의 개수라는 점이다. logrotate.conf 파일에서 rotate 4 에 주석으로 4주간 로그를 보관한다고 되어 있는데, 이는 log file rotate 주기가 weekly이기 때문에 4개를 보관하면 자동으로 4주간 보관이 될 뿐이다. 즉, file rotate 주기에 따라 보관 기간이 달라진다.

그럼, 위 예시에선 daily로 되어 있는데 1008일 동안 보관하는 것인가? 그건 아니다. logrotate에서 제공하는 rotate 주기 옵션은 daily, weekly, monthly 이 3가지 뿐이라 10분마다 파일을 로테이트해야 하는 우리 프로젝트와는 맞지 않는다. 해서 강제로 10분으로 설정해 주었다. 즉, 1008 이라는 수치는 (60/10)*24*7 = 1008 일 뿐이다. 즉 1주일 간 보관하는 것이다.

file rotate 주기 설정

khanrc: cron, crontab
위에서 언급한, logrotate에서 제공하는 주기가 아닌 커스텀 설정을 하고 싶으면 어떻게 해야 할까? cron에 직접 등록해 주어야 한다. 구글에 검색해 보면 여러 얘기가 나오는데, 처음엔 /etc/cron.d/ 에 등록을 하라길래 그렇게 했더니 뭐가 안 되서 결국 crontab에 등록했다.

$ sudo -s
$ crontab -e

...
*/10  *  *  *  *   root    /usr/sbin/logrotate -f /etc/logrotate.d/newsup

이렇게 crontab -e로 등록하면 /var/spool/cron/crontabs/root 에 등록이 된다. crontab -l로 확인할 수 있다. 위의 cron 명령어는 10분마다 newsup config 파일에 맞게 logrotate를 돌려주는 명령이다.

problem

문제가 있다. logstash에서 파일에 로깅을 할 때 한번 파일을 열고 그 파일에 계속 쓴다. 생각해보면 당연히 그런 형식이겠지. C로 따지면 fopen을 하고 나면 그 파일 포인터로 계속 write를 한다는 소리다. 굳이 close를 해서 로그가 들어올 때마다 파일을 새로 열 필요가 없다. 그러다 보니 문제가 발생한다. logrotate에서 로그 파일을 바꿔줘도 새 파일에 로깅하는 것이 아니라 기존 파일에 계속 로깅하는 것이다.

즉, 예를 들어 logstash가 user.log에 로깅하고 있었다면, logrotate가 이 user.log를 user.log.1로 바꾸고 새로 로깅하라고 user.log를 만들어 준다. 근데 새로 만든 user.log에 로깅하는 것이 아니라 기존의 user.log.1에 로깅하는 것이다. 왜? 파일 포인터는 user.log.1을 가리키고 있으니까. 새로 파일을 열어줘야 하는 것이다.

solution

이를 해결하기 위해서는 logrotate가 파일을 rotate한 후에 logstash를 재시작해 줘야 한다. 가장 이상적인 구조는, logstashInputsrabbitmq를 연결한다. 그래서 logrotate가 log file rotate를 하기 전에 logstash를 멈추고, file rotate가 끝난 후에 logstash를 다시 실행시킨다. 이렇게 하면 logstash가 잠시 멈추지만 앞에서 rabbitmq가 로그를 받고 있으므로 로그가 사라지지도 않고 모두가 행복하게 된다.

산 넘어 산

이제 다 좋은데, logstashservice가 아니라는 문제가 남았다. logrotate에서 file rotate앞뒤로 logstash를 재시작해 주려면 service logstash stop, start, restart 따위의 명령어를 써야 할 텐데 그게 없는 것이다. 내가 직접 하는거면 ps aux | grep logstash 해서 kill pid로 죽이면 그만이지만 그게 아니니 문제가 된다.

그래서 처음엔 logstash를 service에 등록하려고 삽질을 했다. https://github.com/bloonix/logstash-pkgs 요런 걸 보면 그런 짓을 해주는 걸 볼 수 있다. 나도 처음엔 그쪽으로 생각했는데 실패해서 방법을 바꾸기로 했다. 저 소스도 그렇고 다른 소스도 그렇고 공홈에서 하란대로 설치하는거랑 안 맞는 듯. 추후 여유가 있으면 apt-get으로 설치해보고 테스트 해봐야겠다.

python: os.popen

서비스를 등록하는 삽질을 해보는 것도 충분히 의미있는 삽질(?) 이겠지만, 지금은 시간이 없어 멘토님의 조언대로 파이썬을 활용하기로 했다. 파이썬에는 os.popen이라는 명령어가 있는데, 커맨드라인에 명령어를 치듯이 그대로 할 수 있다. logstash-controller.py를 만들었다.

import os
import sys

def start():
    os.popen("sudo /home/khanrc/tworoom/logstash/bin/logstash -f /home/khanrc/tworoom/logstash/file.conf &", "r")

def stop():
    p = os.popen("ps aux | grep bin/logstash", "r")
    s = p.read()
    d = s.split("\n")
    pid = int(d[0].split()[1])
    os.popen("kill " + `pid`)

...

if sys.argv[1] == "start":
    start()
elif sys.argv[1] == "stop":
    stop()
elif sys.argv[1] == "restart":
    stop()
    start()

대충 이렇게 되어 있다. 훌륭하게 작동한다!
이제 이걸 활용해서, 위에서 언급했었던 file rotate 앞뒤로 stop / start를 해 주어야 한다. logrotate config 파일에 아래 스크립트를 추가해주자.

postrotate
    python /home/khanrc/tworoom/logstash/logstash-controller.py start
endscript
prerotate
    python /home/khanrc/tworoom/logstash/logstash-controller.py stop
endscript

자, 이제 잘 수 있다!

'Server(Back-end)' 카테고리의 다른 글

Nginx deploy! (+uWSGI +flask)  (0) 2014.11.15
logstash (2) - with logrotate  (0) 2014.11.07
cron, crontab의 개념  (0) 2014.11.06
logstash with python  (0) 2014.11.05
docker global hack day에 다녀와서  (0) 2014.11.02
Docker & Vagrant  (0) 2014.11.02

cron, crontab의 개념

cron, crontab, anacron

크론의 사용법 자체는 구글링 하면 널려 있는데, 이 설정을 어디에 저장해야 되냐라던지 개념적으로 애매한 부분이 많아서 정리했다.

what’s different?

cron은 타이머 데몬이고, crontab은 작업 설정 파일을 말한다. crontab명령어는 이 작업 설정 파일에 접근한다.

cron

NAME

crond - 스케쥴된 커맨드를 실행하는 데몬.

DESCRIPTION

크론은 /etc/rc.d/init.d/etc/init.d로 실행할 수 있다. systemd를 사용가능하다면, systemctl start crond.service로 실행가능하다. ‘&’ 파라메터는 필요없다.

크론은 /var/spool/cron/에서 크론탭 파일을 찾는다. /etc/passwd에 등록된 계정 이름으로 계정별 크론탭 파일이 설정되어 있다. 또한 크론은 /etc/crontab을 찾고 /etc/cron.d/ 디렉토리의 파일을 찾는다 - 이 둘은 다른 포맷으로 되어 있다(crontab(5) 참고). 크론은 각 크론탭을 검사하여 ‘current minutes’에 실행되어야 하는지 확인한다.

크론이 크론탭 파일이 수정되었는지 어떻게 알까? 여기에는 두 가지 방법이 있다. 첫째는 크론탭 파일의 modtime(modified time)을 확인하는 것이다. 두번째는 inotify support를 사용한다. inotify는 모든 크론탭 파일을 체크하고, 수정이 발생하면 알려준다.

modtime 옵션을 사용하면 크론은 모든 크론탭 파일을 매 분마다 검사하며 변경이 감지되면 리로드한다. 크론을 재시작할 필요는 없다.

크론은 아래 파일과 디렉토리들을 검사한다:

  • /etc/crontab
    system crontab. 요새는 안 쓰고 /etc/anacrontab 이라는 config파일을 사용한다고 하는데 현재 우리 서버인 우분투 14.04에는 그런거 없다.
  • /etc/cron.d/
    각 유저별 시스템 크론잡을 저장한다.
  • /var/spool/cron
    crontab커맨드에 의해 저장된 유저 크론테이블을 저장한다.

crontab

crontab 은 리눅스 메뉴얼이 두 개다. 유저 커맨드인 crontab(1), 파일 포맷인 crontab(5).

crontabcrontable의 줄임말이다(그런 거 같다). 크론탭 커맨드는 크론이 사용하는 크론테이블들을 관리한다. 각 유저는 자신만의 crontab 파일을 갖고 이것은 각각 /var/spool/ 에 저장된다.

cron.allow, cron.deny 파일을 통해 크론을 유저마다 allow/disallow 할 수 있다. cron.allow 파일이 존재한다면 크론을 사용할 수 있는 유저는 여기에 등록되어야 하고, cron.deny 파일이 존재한다면 그 반대다. 둘 다 없다면 오직 super user, 즉 root 만이 크론을 사용할 수 있다.

anacron

cron과 유사하지만 시스템이 켜져있지 않아도 작동한다. default로 설치되어 있는 건 아닌 것 같고 따로 설치해 주어야 한다. rpm -q anacron로 설치되어 있는지 확인할 수 있다.

'Server(Back-end)' 카테고리의 다른 글

Nginx deploy! (+uWSGI +flask)  (0) 2014.11.15
logstash (2) - with logrotate  (0) 2014.11.07
cron, crontab의 개념  (0) 2014.11.06
logstash with python  (0) 2014.11.05
docker global hack day에 다녀와서  (0) 2014.11.02
Docker & Vagrant  (0) 2014.11.02

logstash with python

logstash

데이터 분석을 하려면 데이터가 있어야 한다. 프로젝트마다 다르겠지만, 개인화 프로젝트에서는 그 데이터를 당연히 제공하는 서비스에서 수집한다. 소마 프로젝트 또한 마찬가지로 데이터 수집 과정이 있는데, 데이터 수집은 자연스럽게 로그 수집으로 이어진다. 그러면서 접하게 된 것이 바로 logstash이다.

log aggregator

여러 노드(인스턴스)로부터 로그 데이터를 모아주는 프레임워크를 log aggregator라 부른다. 클라우드 환경이 대두되고, 빅데이터가 떠오르면서 자연스럽게 필요하게 된 모듈이라고 할 수 있다.

log aggregator도 종류가 다양한데, facebook에서 사용해서 유명해진 scribe, cloudera에서 제작하여 현재 apache의 top level project인 flume, ruby와 c로 짜여진 fluentd, 사용이 간편하다고 하는 logstash등이 있다. 본 프로젝트에서 처음엔 flume을 고려했으나, 사용의 편의성을 위해 logstash를 사용하기로 했다.

getting start

공식 문서를 참조하자:

intro

Logstash는 로그를 받고, 처리하고, 출력한다. Elasticsearch를 백엔드 데이터 스토리지로 사용하고, kibana를 프론트엔드 리포팅 툴로 활용하면서, logstash는 그 동력으로서 로그를 수집하고 분석한다. 간단한 조작을 통해 강력한 기능들을 활용할 수 있다. 당장 시작하자!

prerequisite: java

logstash는 자바로 짜여졌다. 그래서 돌리려면 자바가 필요하다. java -version을 통해 확인해보자.

$ java -version
java version "1.7.0_65"
OpenJDK Runtime Environment (IcedTea 2.5.1) (7u65-2.5.1-4ubuntu1~0.12.04.2)
OpenJDK 64-Bit Server VM (build 24.65-b04, mixed mode)

Up and Running!

Logstash in two commands

일단 logstash를 받자.

$ curl -O https://download.elasticsearch.org/logstash/logstash/logstash-1.4.2.tar.gz

만약 curl이 없다면 깔아라.

$ sudo apt-get install curl

그리고 나서 압축을 풀고 실행해보자.

$ tar zxvf logstash-1.4.2.tar.gz
$ cd logstash-1.4.2

$ bin/logstash -e 'input { stdin { } } output { stdout {} }'

그리고 아무거나 쳐 보면, 그대로 로깅이 된다.

hello world
2014-10-31T15:02:10.201+0000 c826a788-0110-4c31-8ac0-db1dab0fda32 hello world

위 명령어를 되새겨보자. stdin으로 입력받고, stdout으로 출력했다. -e커맨드는 config를 cli에서 곧바로 입력할 수 있게 해준다.

이제, 좀 더 fancy한 걸 해보자.

$ bin/logstash -e 'input { stdin { } } output { stdout { codec => rubydebug } }'

hi
{
       "message" => "hi",
      "@version" => "1",
    "@timestamp" => "2014-10-31T15:12:51.619Z",
          "host" => "c826a788-0110-4c31-8ac0-db1dab0fda32"
}

보다시피, codec을 추가한 것만으로 출력 포맷을 바꿨다. 이렇게 간단한 설정을 통해 입력과 출력 및 필터링을 할 수 있다.

Moving On

logstash의 config를 네가지로 분류하자면 Inputs, Outputs, Codecs 그리고 Filters다. 아래에 대표적인 케이스를 소개한다:

  • Inputs: 어디서 로그를 입력받을 것인가?
    • file
    • syslog
    • redis
    • lumberjack
  • Filters: InputsOutputs사이에서의 중간 프로세싱 과정
    • grok: 임의의 log 텍스트를 구조화한다
    • mutate: rename, replace, remove, modify fields …
    • drop: 어떤 이벤트를 통째로 버린다
    • clone: 카피한다 - 필드에 수정을 가해서 카피할수도 있다.
    • geoip: GEOgraphical location of IP. 위치정보를 추가한다. kibana에서 display할 수 있다.
  • Outputs: 어디에 출력할 것인가?
    • elasticsearch
    • file
    • graphite: 데이터를 저장하고 시각적으로 보여주는 오픈소스 툴
    • statsd
  • Codecs: 어떻게 출력할 것인가?
    • json
    • multiline

More fun

Persistent Configuration file

config를 -e로 줄 수 있지만, 당연히 file로 설정할 수도 있다. logstash-simple.conf라는 파일을 만들고 로그스태시 디렉토리에 저장하자.

input { stdin { } }
output {
  stdout { codec => rubydebug }
}

그리고 로그스태시를 실행하자:

$ bin/logstash -f logstash-simple.conf

-e로 설정을 준 것과 동일하게 잘 작동하는 것을 볼 수 있다. 즉, -e는 커맨드라인에서 설정을 읽고 -f는 파일에서 설정을 읽는다.

Filters

필터를 적용해 보자. grok 필터는, 위에서도 언급했지만, 대표적인 로그들을 자동으로 구조화해준다.

input { stdin { } }

filter {
  grok {
    match => { "message" => "%{COMBINEDAPACHELOG}" }
  }
  date {
    match => [ "timestamp" , "dd/MMM/yyyy:HH:mm:ss Z" ]
  }
}

output {
  stdout { codec => rubydebug }
}

이렇게 config파일을 수정하고 로그스태시를 실행해서 아래 로그를 입력하자:

$ bin/logstash -f logstash-filter.conf

127.0.0.1 - - [11/Dec/2013:00:01:45 -0800] "GET /xampp/status.php HTTP/1.1" 200 3891 "http://cadenza/xampp/navi.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:25.0) Gecko/20100101 Firefox/25.0"

그럼 이런 결과를 얻을 수 있다:

{
        "message" => "127.0.0.1 - - [11/Dec/2013:00:01:45 -0800] \"GET /xampp/status.php HTTP/1.1\" 200 3891 \"http://cadenza/xampp/navi.php\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:25.0) Gecko/20100101 Firefox/25.0\"",
     "@timestamp" => "2013-12-11T08:01:45.000Z",
       "@version" => "1",
           "host" => "cadenza",
       "clientip" => "127.0.0.1",
          "ident" => "-",
           "auth" => "-",
      "timestamp" => "11/Dec/2013:00:01:45 -0800",
           "verb" => "GET",
        "request" => "/xampp/status.php",
    "httpversion" => "1.1",
       "response" => "200",
          "bytes" => "3891",
       "referrer" => "\"http://cadenza/xampp/navi.php\"",
          "agent" => "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:25.0) Gecko/20100101 Firefox/25.0\""
}

위 예시에서 grok필터 뿐만 아니라 date필터도 사용되었다. 위에서 볼 수 있다시피, 로그의 timestamp를 캐치한다.

with Elasticsearch?

logstashElasticsearch와 강력하게 연동되지만, 공식 문서에서도 관련하여 여러가지를 소개하지만 우리 프로젝트에서 엘라스틱서치를 사용하지 않기 때문에 그 내용은 따로 다루지 않았다. 필요하면 찾아보도록 하자.

Apply

http://logstash.net/docs/1.4.2/
위 도큐먼트에서 어떤식으로 각 plugin을 연동시켜야 할지 찾아볼 수 있다.

milestone

각 플러그인을 보면 milestone level이라는 것이 있다. 위 링크를 참조하자. 높을수록 좋은 것이고 2 이상이면 안정적인 것 같다.

Outputs: mongodb

http://logstash.net/docs/1.4.2/outputs/mongodb
출력을 어떻게 해야 할 지는 조금 더 고민해 봐야 할 문제지만 일단 mongodb에 출력하기로 한다. 도큐먼트를 참조하자. 먼저 contrib plugin을 설치해야 한다.

$ bin/plugin install contrib

그리고 config파일을 작성하자

$ vim logstash-mongo.conf

input { stdin { } }

output {
    mongodb {
        collection => "logstash"
        database => "test"
        uri => "mongodb://localhost"
    }
}

-t로 config파일이 제대로 작성되었는지 확인할 수 있다.

$ bin/logstash -t -f logstash-mongo.conf
Using milestone 2 output plugin 'mongodb'. This plugin should be stable, but if you see strange behavior, please let us know! For more information on plugin milestones, see http://logstash.net/docs/1.4.2/plugin-milestones {:level=>:warn}
Configuration OK

이제 -t를 빼고 실행시켜보면 mongodb와 정상적으로 연동된다.

Inputs: python-logstash

pythonlogging모듈과 logstash를 연결해 주는 python-logstash라는 라이브러리가 있다.

$ sudo pip install python-logstash

먼저 테스트를 해 보자. example에서 udp, 5959번 포트로 로그를 보내므로 그에 맞게 config파일을 설정해주자.

input {
    udp {
        port => 5959
    }
}

output {
  stdout { codec => rubydebug }
}

example을 좀 들여다 보면,

import logging
import logstash

test_logger = logging.getLogger('python-logstash-logger')
test_logger.setLevel(logging.INFO)
test_logger.addHandler(logstash.LogstashHandler(host, 5959, version=1))

test_logger.info('python-logstash: test logstash info message.')

...

logging모듈을 동일하게 사용하되 handler만 logstashHandler로 설정해 주면 된다는 것을 알 수 있다. python-logstash는 Inputsudptcp 두 가지를 지원한다.

그럼 이제 로그스태시를 실행시키고 example.py를 돌리면

$ bin/logstash -f logstash-simple.conf
Using milestone 2 input plugin 'udp'. This plugin should be stable, but if you see strange behavior, please let us know! For more information on plugin milestones, see http://logstash.net/docs/1.4.2/plugin-milestones {:level=>:warn}
{
       "message" => "{\"host\": \"c826a788-0110-4c31-8ac0-db1dab0fda32\", \"logger\": \"python-logstash-logger\", \"type\": \"logstash\", \"tags\": [], \"path\": \"test.py\", \"@timestamp\": \"2014-11-04T08:44:50.616251Z\", \"@version\": 1, \"message\": \"python-logstash: test logstash error message.\", \"levelname\": \"ERROR\"}",
      "@version" => "1",
    "@timestamp" => "2014-11-04T08:44:50.618Z",
          "host" => "127.0.0.1"
}

...

이렇게 로그를 잘 받아온다.

in flask

실제로 flask에 적용해보자.

input {
    udp {
        port => 5959
    }
}

output {
    mongodb {
        collection => "logstash"
        database => "test"
        uri => "mongodb://localhost"
    }
}

Inputsudp로, Outputsmongodb로 설정했다.
그리고 example처럼 코드를 적어주자.

import logstash
import logging

...

# 로거는 이 프로세스가 죽을때까지 누적된다.
# 따라서 아래와 같이 처음 로거를 할당한건지를 체크해서 한 번만 설정해 줘야 한다.
# 그렇지 않으면, level은 상관 없지만 handler는 계속 누적되어 핸들러가 계속 늘어나서 한번만 로깅해도 여러개가 찍히게 된다.
logger = logging.getLogger('logstash-logger')
if len(logger.handlers) == 0: 
    logger.setLevel(logging.INFO)
    formatter = logging.Formatter("%(message)s")
    lh = logstash.LogstashHandler('localhost', 5959, version=1)
    lh.setFormatter(formatter)
    logger.addHandler(lh)

logger.info(json.dumps(js))

example에서는 한번 logger를 할당해서 사용한 후 프로세스가 종료되므로 상관 없지만, flask에서는 프로세스가 계속 살아있다. 이 때문에, 주석에서 설명한 것처럼 handler가 여러개 세팅되지 않도록 조심해야 한다.

또한, 원래 logging handler의 default format이 “%(message)s” 인데, logstashHandler는 포맷이 다르게 설정되어 있다. 그래서 formatter를 설정해주지 않으면 쓸데없는 포멧들이 같이 나온다. 이를 수정해 주었다.

'Server(Back-end)' 카테고리의 다른 글

logstash (2) - with logrotate  (0) 2014.11.07
cron, crontab의 개념  (0) 2014.11.06
logstash with python  (0) 2014.11.05
docker global hack day에 다녀와서  (0) 2014.11.02
Docker & Vagrant  (0) 2014.11.02
REST API 서버 제작기 (4) - db연동 및 decorator, contextlib  (0) 2014.10.12

docker global hack day에 다녀와서

docker global hack day

평소 docker에 관심이 있어 docker hack day에 참가했다. 해커톤과 세션이 동시에 진행되는데 나는 초보이므로 세션만 들었다. 원래는 여기에 참가하고 난 후에, 도커에 대한 내용을 정리해서 하나의 포스트로 올리려고 했는데 정리하려면 한 세월일 거 같아서 따로 올린다. 도커를 실제로 사용하지도 않는 상황에서 이정도 선이 적당한 것 같다.

전반적으로 유익한 세미나였는데 이 포스트의 제일 밑에 적혀 있는 부분인, 즉 도커에 대한 잘못된 생각을 바로잡을 수 있었던 게 제일 좋았다. 나는 도커를 결국 가상화의 일종이라고 생각하고 있었다. 헌데 생각보다 더 가상화와는 거리가 있다1. 도커는 말 그대로 process isolation으로, 마치 자바의 JVM위에서 프로세스들이 독립적으로 돌아가는 것에 더 유사하다. 어떤 프로세스가 독립적으로 돌아갈 수 있도록 환경을 제공하는 것, 그리고 그 프로세스들이 다시 연결될 수 있도록 연결고리를 제공하는 것이 바로 docker의 역할이다.

도커란 무엇인가?

첫번째 세션.
http://nacyot.com
참고. 발표자 블로그.

도커의 핵심 모듈은 거의 go로 짜여졌다. 처음엔 python이었는데 go 로 바꿨다는 듯.

container라는 것이 핫하다. 클라우드의 새 표준.
결국 프로세스 격리의 개념이라는데…

  • chroot: 초기 단계?
  • LinuX Container: LXC. chroot의 진화단계.
  • dockerheroku
    둘다 lxc기반으로 시작함. dockerlxc에서 LibContainer로 옮김. 본질적으론 똑같지만 lxc에서 지원하는걸 자체 구현을 해서 더 많은 걸 지원함. 확장!

docker conatiner == heroku dyno

현재 컨테이너형 가상화 기술의 표준으로 자리잡음

what’s different?

도커를 사용한다는 건 서버를 가볍에 올리고 가볍게 폐기한다는 것.
무언가를 설치하고 준비하는 과정을 미리 도커에 해 놓고, 가볍게 올리고 가볍게 내릴 수 있다.
immutable infrastructure.

-> PaaS와 같은 편리함 + IaaS와 같은 자유도

  • Packer : 이미지를 만들어주는 툴. 도커 이미지 뿐만 아니라 구글 앱엔진, AWS등 다 만들 수 있음.
    도커로 넘어가는게 불안정하지 않느냐? 이런 툴을 사용할 수 있다. (도커가 아니라 기존 인프라스트럭쳐에 적용할 수 -> 좀더 안정되게 컨테이너형 가상화 기술을 활용할 수 있다)

Getting Started with Docker

두번째 세션. 위 슬라이드를 참고하자.
목표: 내 PC에 도커로 리눅스를 돌려보자!

1. Installing Docker

맥이나 윈도우는 리눅스 기반이 아니니까 바로 돌릴 수 없다.
-> Boot2Docker: VirtualBox위에 docker를 띄우고 연결해 주는 역할.
Installing Docker on Mac OS X 이걸 그대로 따라하면 된다.

자세히는 모르겠지만, $(boot2docker shellinit)이 명령어가 스크립트를 돌려서 도커를 연결해 주는 역할이라고 한다. 저걸 안 치면 docker info에서 에러남. boot2docker앱을 실행하면 지가 터미널을 열어서 자동으로 명령어를 쳐 주기 때문에 잘 된다.

2. Build our first container

$ docker run -i -t ubuntu /bin/bash

Unable to find image 'ubuntu' locally
ubuntu:latest: The image you are pulling has been verified

511136ea3c5a: Pull complete
d497ad3926c8: Downloading [=>                                                 ] 4.861 MB/201.6 MB 29m7s
ccb62158e970: Download complete
e791be0477f2: Download complete
3680052c0f5c: Download complete
22093c35d77b: Downloading [====================================>              ] 6.185 MB/8.395 MB 16s
5506de2b643b: Download complete

우분투 이미지를 로컬에서 찾고, 없으면 받아온다. 이게 성공하면 우분투 서버가 뜬다.

실습하는데 우분투를 받는데 너무 오래 걸려서 busybox라는 걸로 하기로 했다. 비지박스는 최소한의 리눅스로, 파일서버 정도로 활용한다고 한다. 진짜 짱 작다. 아무리 최소한의 리눅스라지만 이렇게 작을 수 있나… 하긴 뭐 리누스 토발즈가 리눅스를 처음 짰을 땐 만 줄에 불과했다고 하니…

$ docker run -i -t busybox /bin/sh
Unable to find image 'busybox' locally
busybox:latest: The image you are pulling has been verified

df7546f9f060: Pull complete
e433a6c5b276: Pull complete
e72ac664f4f0: Pull complete
511136ea3c5a: Already exists
Status: Downloaded newer image for busybox:latest

3. Working with our first container

슬라이드에서는 디테일하게 나눠 놓았는데, 너무 디테일해서 여기선 따로 나누지 않았다.

우분투가 잘 돌아가고 있는건지 확인해 보자.

호스트 네임 확인
$ hostname

/etc/hosts 파일 확인
$ cat /etc/hosts

네트워크 설정 확인
$ ip a

실행중인 프로세스 확인
$ ps -aux

패키지 설치
$ apt-get update
$ apt-get install

는 비지박스를 쓰기로 함.

name옵션을 줘 보자.

$ docker run --name my_busybox -i -t busybox /bin/sh

그리고 나면

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
98086a32497f        busybox:latest      "/bin/sh"           38 seconds ago      Exited (0) 9 seconds ago                        my_busybox
d5fece0d7cf9        busybox:latest      "/bin/sh"           3 minutes ago       Exited (0) 55 seconds ago                       hopeful_mccarthy

요렇게 확인할 수 있다. 아래 hopeful_mccarthy는 도커가 랜덤하게 만들어낸 이름. 이름을 안주면 이렇게 자기가 랜덤하게 만든다.

docker ps는 현재 실행중인 도커 프로세스를, docker ps -a는 모든 도커 프로세스를 보여준다. 즉 종료된 도커 프로세스도 보여줌.

$ docker start my_busybox

start: 네이밍을 하고 나면 이렇게 이름으로 실행시킬 수 있다.

$ docker run --name my_daemon3 -d -i -t busybox /bin/sh -c "while true; do echo hello world; sleep 1; done"

-d옵션을 주면 daemon으로 띄울 수 있다.

$ docker logs my_daemon3
hello world
hello world
hello world
...

$ docker logs -ft my_daemon3
2014-11-01T04:02:32.084417935Z hello world
2014-11-01T04:02:33.093224868Z hello world
2014-11-01T04:02:34.094749057Z hello world
...

logs: 로그를 볼 수 있다. -f옵션을 주면 실시간으로 계속 볼 수 있고, -t옵션을 주면 타임로그도 찍어준다.

$ docker top my_daemon3
PID                 USER                COMMAND
1537                root                /bin/sh -c while true; do echo hello world; sleep 1; done
1726                root                sleep 1

top: 현재 돌아가는 프로세스를 볼 수 있다.

$ docker stop my_daemon3

stop: 컨테이너를 죽인다.

$ docker inspect my_busybox

inspect: 컨테이너의 디테일한 정보를 볼 수 있다.

$ docker inspect -f '{{ .State.Running }}' my_busybox
true

$ docker inspect -f '{{.Name}} {{ .State.ExitCode }}' my_busybox my_daemon
/my_busybox 0
/my_daemon -1

-f: inspect에 정보가 너무 많으니 그중에서도 일부만 볼 수 있다.

$ docker rm my_daemon3
my_daemon3

rm: 죽은 도커가 ps -a에 나오는데, rm으로 아예 없애버릴 수 있다. 아예 지워버리는 것으로 당연히 start name을 통해 다시 실행시키지도 못한다.

Docker 이미지, 컨테이너, Dockerfile

설명이 좀 복잡했는데, 결국 핵심은 이미지 위에 컨테이너가 돌아간다는 것.

Image

읽기 전용 layer. 상태가 없음.
커널 위에 이미지가 있다.
Parent Image : 패런트 이미지 위에 새 이미지를 추가하는 방식… 즉 이미지를 층층이 쌓는다
Base Image : ubuntu, centos, busybox같은 제일 기본 이미지
Image ID : hash code.

Container

이미지 위에 컨테이너가 추가되는 것.
그래서 그 컨테이너 위에서 write를 할 수 있는 것이다.

Container State: running, exited.
컨테이너는 시작/정지/재시작 가능. 재부팅의 개념. 메모리는 초기화되고 디스크는 그대로
Container ID.

how to make image?

  1. docker commit (container -> image. 컨테이너의 상태를 이미지로 빼는 것.)
  2. docker build (dockerfile)

commit으로 container에서 image만들기

ubuntu에 파이썬 설치

$ docker run -i -t ubuntu /bin/bash
ubuntu# apt-get update
ubuntu# apt-get install python
$ docker commit container_id

#는 우분투 쉘이다.

$ docker commit baa2f6034491
e25382af03530b91d8c188482cafb7da78730712662040d279ede369c75c737f

$ docker run -it e25382af03530b91d8c188482cafb7da78730712662040d279ede369c75c737f /bin/bash

ubuntu# python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>

내 우분투 컨테이너 아이디를 입력하고 커밋했다. 그렇게 생성한 이미지를 바탕으로 도커 컨테이너를 띄워 파이썬을 실행시켜보면 잘 작동하는 것을 볼 수 있다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>              <none>              e25382af0353        33 minutes ago      243.1 MB
ubuntu              latest              5506de2b643b        8 days ago          199.3 MB
busybox             latest              e72ac664f4f0        4 weeks ago         2.433 MB

docker images를 통해 만든 도커 이미지들을 볼 수 있다. REPOSITORYTAG가 none으로 나오는 걸 보면 이미지를 만들 때 입력할 수 있는 듯.

dockerfile

dockerfile이 있으면 docker build .으로 도커 이미지를 만들 수 있다. (마지막 .은 file path)

dockerfile에서 쓰는 문법이 따로 있는 듯. FROM, ENV, RUN, VOLUME 등등…

  • FROM: ubuntu:14.04
  • ENV: 환경변수 설정
  • RUN: apt-get update …
  • VOLUME: 컨테이너 안의 폴더와 밖의 폴더를 연결하는 듯. 연결해 두면 컨테이너 안에서 뭔가 파일을 만들면
  • ADD: 호스트의 파일을 컨테이너의 파일로 복제
  • CMD: 도커가 run될때 시작 명령어. CMD는 한번만 먹는다. 왜냐면: 도커는 process의 컨테이너이기 때문에 한 프로세스만 돌리도록 디자인되었다. 다만 굳이 원한다면 shell file을 만들어서 그걸 돌린다던가 supervisor d라는 명령어를 써서 할 수 있다.
    자세히는 모르겠지만 이렇게 되면 컨테이너에서 어떤 프로세스가 돌아갈 때 커맨드 쉘로 접속할 수가 없는 거 같다. 그래서 도커 1.3버전에서 exec이라는 명령어가 새로 나왔다고 한다 - 프로세스를 돌리고 있는 컨테이너에 접속할 수 있게 해준다.

docker는 결국 rest api로 동작하는데, 이 동작 config가 inspect에 나오는 거다. dockerfile에 설정을 하면 이게 rest api config로 들어간다.

Docker hub, Docker private registry를 이용한 이미지 관리

도커로 배포하려면?
이미지를 어딘가에 올리고 받아야 한다.
-> registry

docker hub

공식 레지스트리.
http://hub.docker.com

docker run을 할때, 일단 로컬을 살펴보고 없으면 자동으로 도커허브에서 pull받는다.
테스트 해 보자.

# Dockerfile
FROM busybox
CMD echo 'Hello, Docker!'

# 위의 Dockerfile을 빌드
$ docker build -t khanrc/test .

#이렇게 빌드하고 나면 images로 확인할 수 있다
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
khanrc/test         latest              522d4994d5b8        26 minutes ago      2.433 MB

# 도커 실행 확인
$ docker run --rm khanrc/test
Hello, Docker!

# 이미지 삭제. rmi로 이미지를 지우면 images에서도 없어진 걸 확인할 수 있음.
$ docker rmi khanrc/test
$ docker run --rm khanrc/test
Unable to find image 'khanrc/test' locally
Pulling repository khanrc/test
2014/11/01 15:12:48 Error: image khanrc/test not found

# 이미지 빌드 및 푸쉬
$ docker build -t khanrc/test .
$ docker push khanrc/test
$ docker rmi khanrc/test
$ docker run --rm khanrc/test
Unable to find image 'khanrc/test' locally
Pulling repository khanrc/test
...
Status: Downloaded newer image for khanrc/test:latest
Hello, Docker!

이렇게 push를 해서 docker hub에 등록하면 로컬에 없을시 자동으로 도커허브에서 받아온다.

호스팅

  • tutum.co
  • quay.io

도커허브 같은 호스팅 서비스.

사설 레지스트리

https://github.com/docker/docker-registry
직접 도커 레지스트리를 구축하는 걸 말한다.

레지스트리가 지금은 문제가 많은데, 앞으로 python에서 go로 변경할 예정.

도커로 Ghost 블로그 설치하기

마지막 세션

# 마리아db부터 설치. 포트설정을 해 줘야 한다
# 설치하고 띄움.
docker run -d --name mariadb -p 3306:3306 meoooh/mariadb10.0

# ghost 설치 및 띄움. volume설정을 해서 이어주자. 중요한 파일들은 바깥에 보관을 해야 컨테이너가 날라가도 데이터가 보존된다.
# DB_PASS는 위 마리아디비 설치시에 자동으로 랜덤 설정되어 나오는데 logs로 확인 후 넣어주면 된다
# DB_HOST는 mariadb를 0.0.0.0으로 띄웠기 때문에 docker의 아이피 주소인 172.17.42.1로 접속하면 된다
docker run -d --name ghost -v $(pwd):/ghost-external -p 80:2368 -e GHOST_DB=ghost -e DB_ID=admin -e DB_PASS=... -e DB_HOST=172.17.42.1 meoooh/ghost:0.5.3

-v: VOLUME
-e: ENV. 환경변수

아래에 적은 것처럼, 도커는 process isolation이다. 즉 고스트 블로그를 띄우려면 위와 같이 mariadbghost(node.js)를 따로 띄워야 한다.

CoreOS를 이용한 도커 컨테이너 배포

추가 세션!

slide; CoreOS : 설치부터 컨테이너 배포까지
youtube; CoreOS : 설치부터 컨테이너 배포까지
Docker 전용 경량 리눅스 - CoreOS
도커로 모든 어플리케이션을 컨테이너로 만든다면, 서버는 도커를 돌리기 위해 존재할 뿐이다
-> 그래서 나온 것이 도커 구동에 최적화된 가볍고 최소화된 OS인 CoreOS
대표적인 특징으로 package manager가 없다(apt/yum). ChromeOS를 기반으로 몇가지 기능을 추가하고 서버로 사용할 수 있도록 커스터마이징한 OS

특징

  • Minimal OS
  • Painless Updating
    CoreOS는 두개로 분리되어 있어서 OS업데이트를 하면 기존 OS는 A는 그대로 두고 B를 업데이트한다. 그리고 업데이트가 완료되면 A로 돌아가던 걸 B로 돌림.
  • Docker Containers
  • Clustered by Default

핵심구성

  • etcd
    key-value store. 여러 인스턴스에서 읽을 수 있는 shared instance개념인 듯. 클러스터를 구성한다면 꼭 필요하다.
    • 사용하기 편리함
    • SSL 보안
    • Watch: 키 감시
    • key TTL: Time To Live. 키를 저장할 때 시간제한을 둘 수 있음
    • Lock: 멀티노드에서 접근하는 거니까 뭐 당연히…
  • systemd
    better init system (than init.d)
    원래 프로그램마다 logfile path라던가, pid 등등 다 다른데 관리를 다 해줌.
    우분투도 곧 이걸로 넘어올 예정.
  • fleet
    etcd를 백엔드로 해서 systemd에 커스텀 config를 추가하여 service와 machine을 추상화하고 high availibility를 구현함.
    etcd, systemd를 이용해서 CoreOS에서 만든 서비스로, 서버 클러스터에서 어플리케이션을 실행시키고 종료하는 관리 도구다. 즉 systemd가 인스턴스 종속적인 서비스라면 fleet는 클러스터 범위에서 돌아간다. 당연한 얘기지만 어느 서버에서 돌릴지 등등 디테일한 설정이 가능.
  • +@ cloud-init

study

혼자 하는 스터디

docker image는 process isolation이다

즉 한 이미지에 한 프로세스만 돌아간다. (그렇게 하도록 의도되었다)
그럼 내가 웹 프로젝트를 배포하고 싶다면 어떻게 해야 하는가? 각 프로세스를 이미지로 따서 배포한다.

소마 프로젝트를 보면 flask, redis, mongodb, mariadb, logstash, crawler(cron), rabbitmq가 돌아간다. 이 각각을 이미지로 만들어 배포하는 것이다.


  1. 물론 이것도 가상화의 일종이지만 일반적으로 가상화라고 하면 하드웨어(또는 OS) 가상화를 의미하기 때문에… 굳이 따지자면 프로세스 환경 가상화라고 할 수 있겠다.

Docker & Vagrant

Docker & Vagrant

기존의 프로젝트를 새로운 서버에 올린다고 생각해보자. 서버를 파고, add user라던가 port forwarding등 기본 세팅을 한 후, 프로젝트 세팅을 해야 한다. 소마 프로젝트를 예로 들자면, python을 설치하고, python에서 사용하는 라이브러리들인 flask, nltk, newspaper 등등 수많은 라이브러리들을 설치한다. 뿐만 아니라 mariadb, mongodb, redis등 데이터베이스들도 설치하고, 파이썬에서 이 데이터베이스에 커넥트하기 위해 사용하는 라이브러리들도 또 설치해야 한다. 또한 메시지 큐로 사용하는 rabbitmq도 설치하고, 위 과정과 마찬가지로 또 파이썬용 라이브러리를 설치해야 한다. 여기까지만 해도 충분히 많은데 이게 끝이 아니다. 이걸 다 설치했으면 소스를 가져와야 하는데, 일단 git을 먼저 설치하고, git clone을 통해 프로젝트를 가져와야 한다. 뿐만 아니라 프로젝트를 실제로 디플로이 하기 위해서 웹서버인 apachenginx를 설치하고, 파이썬 어플리케이션 컨테이너인 wsgi 모듈도 설치해야 한다.

이정도 하고 나면 이제야 이 서버를 돌릴 수 있다. 당연한 얘기지만 이 과정에서 실수가 나오고 문제가 발생하기 마련이며, 미처 적지 못한 과정도 있을 것이다. 즉, 한마디로 엄청 머리아프다. 지금까지 얘기한 이 모든 과정을 바로 배포, 즉 deploy라 한다.

마치 git이 프로젝트 소스 전체를 컨트롤하듯, deploy의 과정 전체를 컨트롤 할 수 있는 도구가 바로 docker다.

what is docker?

조대협의 블로그: Docker 소개
khanrc: 가상화 이야기

VM

VMWare나, Parallel desktop과 같이 우리가 평소에 사용하는 Virtual Machine은 이러한 형태로 동작한다. 먼저 Host OS위에 Hypervisor가 깔린다. 하이퍼바이저는 단일 시스템을 분할 및 관리한다. 이 위에 Virtual Machine이 올라가는데, 버추얼 머신은 하드웨어를 통째로 가상화한다. 이 가상 하드웨어 위에 새로운 Guest OS가 올라가는 것이 바로 Virtual Machine의 구조다.

Container

docker는 이와 달리, 하드웨어를 가상화하지도 않으며 다른 OS를 통째로 올리지도 않는다. 도커는 리눅스 컨테이너로서 OS간의 차이를 저장한다. 즉 Host OS가 우분투고 컨테이너에 CentOS를 올렸다면, 이 두 OS의 diff만을 따로 패키징하는 것이다. 이것이 바로 Guest OSIsolation이다. 이 덕분에 기존의 가상화와는 달리 따로 하드웨어 가상화를 거치지 않아 매우 가벼우며, 컨테이너 내에서의, 즉 Guest OS의 퍼포먼스가 Host OS와 별반 차이가 나지 않는다는 점이 특장점이다.

Vagrant

조대협의 블로그: Vagrant를 이용한 개발환경 관리(간단한 VM관리)
Slideshare: Vagrant로 서버와 동일한 개발환경 꾸미기
vargant간소화된 VM 관리 서비스다. 내가 사용하는 맥에, 서버와 동일한 환경을 구축하기 위해 우분투를 올린다고 생각해보자. VMWare 따위의 Hypervisor를 이미 설치해 두었다고 해도, 먼저 가상 하드웨어 머신을 생성해야 한다. 그리고 우분투 이미지를 가져와 그 위에 우분투를 설치해야 한다. 우분투가 설치되고 나면 기본 설정을 해주어야 한다. 이러한 노가다성 작업을 손쉽게 해결하기 위한 프로젝트가 바로 vargant다. 크게 보면 docker와 아이디어 면에서 비슷한 점이 많다.

// VM의 이미지와 기본 설정을 가져온다
$ vagrant box add precise32 http://files.vagrantup.com/precise32.box
$ vagrant init precise32

// VM을 설치하고 구동시킨다
$ vagrant up

// ssh를 통해 VM에 로그인한다. 둘 중 아무거나 써도 됨.
$ vagrant ssh
$ ssh localhost:2222
  • box
    기본 OS 이미지 + 기본 설정(CPU, 메모리, 네트워크 등)
  • vagrant file
    vagrant init를 하면 생성된다. 어떤 box를 사용하는지, box에서 설정된 기본 설정들이 어떠한지 등이 명시되어 있다.
  • vagrant up
    위에서 정의한 설정대로 VM을 생성한다.

좀 더 세부적으로 들어가면 provisioning 등 많은 듯 싶지만, 이쯤에서 정리한다.

'Server(Back-end)' 카테고리의 다른 글

logstash with python  (0) 2014.11.05
docker global hack day에 다녀와서  (0) 2014.11.02
Docker & Vagrant  (0) 2014.11.02
REST API 서버 제작기 (4) - db연동 및 decorator, contextlib  (0) 2014.10.12
REST API 서버 제작기 (3) - API 변화  (0) 2014.10.11
SSH TIMEOUT  (0) 2014.10.08

REST API 서버 제작기 (4) - db연동 및 decorator, contextlib

REST API 서버 제작기 (4) - decorator 및 db연동

최근, 블로그에 글을 컴팩트하게 쓰기로 마음먹으면서 이 시리즈(?)를 왜 쓰는가에 대한 회의가 좀 있지만 일단 시작한 것이니 만큼 끝까지 쓰기로 한다. 시작했으면 끝을 봐야지. 내용이 좀 짧아지는건 어쩔 수 없다.

decorator

khanrc: decorator와 closure
khanrc: decorator (2) - extension

before_filter로 만들어 두었던 것을 decorator로 변경했다. 유저 토큰 검사는 데코레이터와 굉장히 잘 어울린다.

# decorator.
def usertoken_required(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        user_token = request.headers.get('user_token')
        cur.execute("""SELECT * FROM USER WHERE user_token='%s'""" % user_token)
        user = cur.fetchone()

        if user is None:
            return Response(response="user_token is wrong!", 
                            status=401)

        argspec = getargspec(func)
        if argspec[2] is not None: # kwargs를 받는 함수에게만 전달
            kwargs['user_info'] = user
        return func(*args, **kwargs)

    return decorated

mongodb

khanrc: mongoDB
그간 써보고 싶었던 mongodb를 써 보았다. 위 링크를 보면 알겠지만 mongokit이라고 ORM도 있는데, ORM을 그다지 선호하지 않아 pymongo를 사용하였다. flask의 extension인 flask-pymongo가 존재하는데 별로 필요 없어 보인다.

from flask.ext.pymongo import PyMongo
import pymongo

app = Flask(__name__)
app.config['MONGO_DBNAME'] = 'newsup'
# app.config['MONGO_HOST'] = 'localhost' # expect localhost by default.
mongo = PyMongo(app)

def getSeq(name):
    ret = mongo.db.ikeys.find_and_modify(
            query={ '_id' : name },
            update={ '$inc': { 'seq': 1 } },
            new=True
        )

    return ret.get('seq')

@app.route('/users', methods=['POST'])
def add_user():
    user_token = request.headers.get('user_token')
    if user_token is None:
        return Response(response="user_token is None",
                        status=401)

    try:
        ret = mongo.db.users.insert({
            '_id': user_token,
            'user_id': getSeq('user_id')
        })

        if ret == False:
            return "fail!!!!"
    except pymongo.errors.DuplicateKeyError, e:
        return Response(response="{error_code:1, error_msg:'중복된 유저가 있음'}",
                        status=200)
    except:
        print "치명적인 에러 in add_user, insert user_token: " + str(sys.exc_info())
        return Response(response="{error_code:-1}", mimetype='application/json', status=200)


    return Response(response="{error_code:0}", mimetype='application/json', status=201)

getSeq()는 auto_increment를 구현한 것. ikeys 컬렉션에 seq를 저장해 놓고 매 호출마다 증가시킨다.

다만 위처럼 하면 json 인식이 제대로 안 되는데, response를 저렇게 쓰면 안 된다.

return Response(response="""{"error_code":-1}""", mimetype='application/json', status=200)

이렇게 string""로 감싸줘야 한다.

mysql (mariadb)

python-mysql 모듈도 굉장히 다양하게 존재한다. mysql에서 공식적으로 지원하는 mysql.connector, 가장 많이 쓰는 것 같은 mysqldb 등등. 이 모듈들의 성능 비교를 한 포스트가 있다.
Comparing python/mysql drivers

성능이 좋다고 하고, 많이들 사용하는 mysqldb를 사용해 보기로 했다. mysqldb는 mysql의 C API를 직접적으로 사용하는 _mysql모듈의 래퍼다.

install

참고: Is MySQLdb hard to install?

sudo apt-get install build-essential python-dev libmysqlclient-dev
sudo pip install MySQL-python

단, 우리는 mariadb이기 때문에 libmysqlclient-dev가 아니라 libmariadbclient-dev를 설치해야 한다.
http://stackoverflow.com/questions/22949654/mysql-config-not-found-when-installing-mysqldb-python-interface-for-mariadb-10-u 참고.

sudo apt-get install libmariadbclient-dev
sudo pip install MySQL-python

이러면 설치가 된다.

usage

import MySQLdb as mdb
import _mysql_exceptions
import sys

con = mdb.connect(host='host', user='user', passwd='passwd', db='db')
cur = con.cursor()

try:
    p = 'hello11'
    s = """INSERT INTO USER (user_token) VALUES ('%s')""" % (p)
    print s
    cur.execute(s)
    con.commit()
except _mysql_exceptions.IntegrityError, e:
    print "IntegrityError : ", e
    con.rollback()
except:
    print "error!! rollback!!" + str(sys.exc_info())
    con.rollback()

cur.execute("""SELECT * FROM USER""")
print cur.fetchall()

처음에 이렇게 코딩을 했더니, 처음엔 잘 되다가 시간이 지나니 OperationalError: (2006, 'MySQL server has gone away') 이런 에러가 났다. 구글링을 해 보니 connection이 끊겨서 나는 에러라고 하더라. 생각해 보니 저렇게 한번 커넥트를 해주면 되는게 아니라, 매 요청시마다 커넥트를 해 주고 끝나면 닫아줘야 한다. 커넥션이 타임아웃되어 에러가 났던 것.

그래서 매 요청마다 커넥트와 클로즈를 해주자니 너무 코드가 지저분해진다. 그래서 contextlib을 쓰기로 했다.

contextlib

khanrc: contextlib 참고.

위 링크에도 나와있지만

from contextlib import contextmanager

@contextmanager
def db_connect():
    con = mdb.connect(host, user, passwd, db)
    cur = con.cursor()

    try:
        yield (con, cur)
    finally:
        cur.close()
        con.close()

with db_connect() as (con, cur):
    p = """ '%s' """ % user_token
    s = """INSERT INTO USER (user_token) VALUES (%s)""" % (p)
    cur.execute(s)
    con.commit()

요렇게 쓴다.

REST API 서버 제작기 (3) - API 변화

REST API 서버 제작기 (3) - API 변화

여느 API들이 다들 그렇듯, 우리도 API를 정하는 과정에서 많은 변화가 있었다. 내부적인 시스템이 완성된게 아니기 때문에 앞으로도 계속 변화가 있을 수 있다는 사실을 글을 다 쓸 때쯤에 새삼 깨달았으나, 일단 이쯤에서 정리해 둔다.

APIs

0. 유저 토큰

METHOD /API
-H user_token: mytoken

클라이언트는 서버에 리퀘스트를 보낼 때 항상 헤더에 유저 토큰을 넣어서 보낸다. SESSION ID 의 역할을 한다.

1. 메인 카드 리스트

GET /news/category/:category

page를 없앴다. 본 서비스는 뉴스 추천 서비스이기 때문에, 시시각각 새로운 데이터가 업데이트되며, 유저의 리액션에 따라 컨텐츠의 순위가 바뀐다. 즉, 각 페이지별로 고정된 데이터가 아니다. 따라서 페이지별로 불러오는 것은 의미가 없으며 각 카테고리별로 서버에 요청을 하면 클라이언트가 받아보지 못한 새로운 데이터를 돌려준다.

이를 위해 서버는 클라이언트가 어떤 아티클들을 보았는지를 체크한다. 최근의 아티클들에 대해서만 체크하면 되므로 오버로드는 그리 크지 않을 것으로 본다.

2. 뉴스 상세 페이지

GET /news/articles/:article_id

상세 페이지 정보를 불러오는 API는 초기에 생각했던 형태 그대로 간다.

3. 유저 프로파일링

POST /users/log

원래 PUT이었지만 POST로 변경되었다. PUT/POST의 구분은 idempotent1 / not idempotent로 한다. put은 여러번 하더라도 같은 값을 유지해야 하고, post는 여러번 하면 계속 추가된다. 간단한 예를 들면 x=1은 put, x+=1은 post라 할 수 있다.

유저 프로파일링은 한 유저의 프로파일을 계속 업데이트 한다는 관점에서는 put이지만, 유저의 리액션 정보를 데이터베이스에 계속 쌓는 개념이므로 post가 올바르다.

4. 유저 등록

POST /users

신규 유저 등록은 당연히 POST. 앱을 처음 실행하면 이 메소드를 사용하여 유저 토큰을 넘겨 서버에서 신규 유저를 등록할 수 있도록 한다.

State codes

API를 정의할 때 상태 코드도 같이 정의해 주어야 한다. 상태 코드는 크게 통신 상태 코드와 시스템 내부 상태 코드로 나눌 수 있다. 경계가 조금 애매하다는 생각이 드는데, 에러 코드 또한 추후 한번 더 검토하기로 한다.

통신 상태 코드

리퀘스트 형식이 잘못된 경우. 유저 토큰이 잘못되었거나 리퀘스트 바디가 이상하거나.
이 경우엔 http error code를 이용한다.

시스템 에러

리퀘스트는 제대로 들어왔으나, 요청이 이상한 경우.

API Server 개발

내부 시스템이 완성된 건 아니지만 일단 테스트용 api서버를 deploy해 놓기로 하였다.
몇가지 추가된 부분을 정리했다.

유저 토큰 검사

먼저 유저 토큰을 검사하는 부분이 필요하다.

from flask import request

@app.before_request
def before_filter():
    user_token = request.headers.get('user_token')

모든 api에 대해서 유저 토큰을 검사하므로 before_filter로 묶어 놓는다.
유저 토큰은 헤더에 담기로 했고 헤더를 조회하기 위해 request모듈 import가 필요하다.

Response

from flask import Flask, Response

@app.route('/news/articles/<int:article_id>', methods=['GET'])
def article(article_id):
    ret = TEST JSON DATA...

    return Response(response=json.dumps(ret), 
                    mimetype='application/json', 
                    status=200)

테스트데이터를 넣어 두었다.
리스폰스의 http 상태 코드와 mimetype을 조작하기 위해 Response모듈을 임포트했다.
추후에는 db조회를 통해 데이터를 꺼내 유저에게 전송하는 부분이 들어갈 것이다.


  1. 멱등성. 연산을 여러번 하더라도 결과가 달라지지 않는 것.

SSH TIMEOUT

SSH TIMEOUT

서버 개발을 하다보면 ssh 타임아웃 설정은 당연히 하게 된다.

first try

khanrc: Ubuntu에 개발환경 셋팅하기 (2)
여기서 ssh timeout 설정을 했었다.

$ cd /etc/ssh/
$ sudo vim sshd_config

ClientAliveInterval 600
ClientAliveCountMax 3

그런데 왜인지 소마에서 사용하는 ucloud 서버에서는 이게 먹히지 않는다.

second try

http://abipictures.tistory.com/m/post/918
이 블로그의 1 - (2) SSH 접속 타임아웃 값 조정 을 참고하였다.

$ sudo vim /etc/bash.bashrc

...
# readonly export TMOUT=300

제일 아래의 readonly export TMOUT=300 을 주석처리 해주면 된다.

성공!

why?

왜 첫번째 시도는 안 될까? 구글링을 해서 나오는 대부분의 자료는 first try 임에도 불구하고. 우선순위 문제인지, 아니면 우분투 버전 문제인지… 나중에 알아보기로 한다.

WSGI로 보는 웹 서버의 개념

Web server, WSGI, Middleware, Application

예전에 웹 서버와 WAS, WSGI를 공부하면서 블로그에 포스팅을 몇 개 한 적이 있는데, 이제서야 개념정리가 좀 되어 한 글에 정리하기로 했다.

웹 서버

khanrc: 웹서버, WAS, CGI
웹 서버는 정적이다. 리퀘스트가 들어오면 그 리퀘스트를 분석하여 알맞는 리소스를 리턴한다.

이 웹서버를 동적으로 기능하게 하기 위해서 웹서버 위에 flask, django, rails, node.js따위의 프레임워크를 얹는다. 그게 바로 WAS 다.

웹서버의 구조

apache라고 하자.

  1. 80번 포트를 listening
  2. 리퀘스트가 들어오면 리퀘스트를 처리하기 위한 apache process를 fork
  3. 포크된 아파치 프로세스는 리퀘스트에 붙어 처리하고 원래 있던 프로세스는 그대로 listen

이게 과거의 아파치다. 문제는, 프로세스는 무겁다. nginx는 이를 쓰레드로 전환하여 속도를 향상시켰다. 지금은 아파치도 쓰레드 형태로 전환하여 비슷한 퍼포먼스가 나온다고 한다.

프로세스: code, data, heap, stack 모두 갖고 있는 하나의 덩어리. 즉 독자적인 메모리를 갖고 있다.
쓰레드: 라이트웨이트 프로세스. 하나의 프로세스 안에서 code, data, heap을 공유하고 stack만을 별도로 가지고 있다. 즉 메모리를 공유한다.

WAS

웹 서버 위에 서버 어플리케이션을 얹은 것이 바로 WAS.

WSGI

khanrc: WSGI와 CGI의 차이
ko.wiki: WSGI
Web Server Gateway Interface. 파이썬에서 어플리케이션, 즉 파이썬 스크립트가 웹 서버와 통신하기 위한 명세다. 프로토콜 개념으로 이해하면 될 듯.

WSGI는 서버와 앱 양단으로 나뉘어져 있다. WSGI 리퀘스트를 처리하려면 서버에서 환경정보와 콜백함수를 앱에 제공해야 한다. 앱은 그 요청을 처리하고 콜백함수를 통해 서버에 응답한다.

Middleware

이러한 커뮤니케이션을 WSGI 미들웨어가 보충한다. 이 미들웨어는 서버의 관점에서는 앱으로, 앱의 관점에서는 서버로 행동한다. 이 미들웨어는 아래와 같은 기능을 가진다.

  • 환경변수가 바뀌면 타겟 URL에 따라서 리퀘스트의 경로를 지정해준다.
  • 같은 프로세스에서 여러 애플리케이션과 프레임워크가 실행되게 한다.
  • XSLT 스타일시트를 적용하는 것과 같이 전처리를 한다.

미들웨어에는 mod_wsgi, uwsgi, gunicorn, twisted.web, tornado 등등이 있다.

어플리케이션의 관점에서는 이 미들웨어를 통해 앱이 실행되므로 앱을 실행시켜주는 어플리케이션 컨테이너(Application Container)라고도 할 수 있다.

Framework

서버 어플리케이션을 만들기 위해 사용하는 최상단 웹 프레임워크. Flask로 대표되는 Micro 프레임워크와 Django로 대표되는 Full-stack 프레임워크로 나눌 수 있다.

spoqa tech blog: Flask로 만들어 보는 WSGI 어플리케이션

종합

  1. http 리퀘스트가 들어오면
  2. 웹 서버가 그 리퀘스트를 받고
    • 서버사이드 처리가 필요 없으면 리스폰스를 리턴(static한 웹 서버)
  3. 서버사이드 처리가 필요하면 wsgi 미들웨어를 통해 파이썬 어플리케이션으로 리퀘스트 전달
  4. 파이썬 어플리케이션이 리퀘스트를 받아 처리 후 wsgi 미들웨어 - 웹서버를 통해 리스폰스 리턴.

의 구조라고 할 수 있다.

정리하자면, 상식대로 웹 서버 위에 서버 어플리케이션이 올라가는데 이 어플리케이션과 웹 서버간의 커뮤니케이션을 위해 wsgi 미들웨어가 존재하는 것. 이 미들웨어를 어플리케이션을 적재하는 어플리케이션 컨테이너라고도 할 수 있다. 자바는 잘 모르지만, 자바 서블릿 컨테이너도 동일한 개념으로 보인다.