Monitoring with cAdvisor + Prometheus + Grafana

UzukiLive 서버가 제공하는 기능들이 점점 많아지고, 접속이 활발해지면서 특정 인스턴스가 많은 양의 자원을 사용할 때 알려주는 기능이 필요했었다.

그래서 2019년 4월에 High Compute 머신으로 옮기면서 모니터링 스택 + 관리 봇을 만들었는데, 오늘은 그 중 ‘모니터링 스택’ 에 대해 사용한 툴과 사용법을 간단히 소개하고자 한다.

사용한 도구

  • cAdvisor
  • Prometheus
  • Grafana
  • Caddy: 물론, Nginx 나 Apache로도 가능하지만 최근에는 간단한 사용법을 가진 Caddy를 선호하긴 한다.

준비 과정 – cAdvisor

cAdvisor(https://github.com/google/cadvisor)는 실행중인 컨테이너에 대한 자원 사용량이나 성능에 대한 분석 기능을 제공하는 도구로, 아래와 같이 올릴 수 있다.

  cadvisor:
  image: google/cadvisor:latest
  container_name: cadvisor
  volumes:
    - /:/rootfs:ro
    - /var/run:/var/run:ro
    - /sys:/sys:ro
    - /var/lib/docker/:/var/lib/docker:ro
    - /dev/disk/:/dev/disk:ro
  restart: always

자체적인 웹 UI도 제공하긴 하지만, Prometheus가 데이터를 가져올 수 있게 하는 용도면 충분하므로 웹 UI는 사용하지 않도록 한다.

준비 과정 – Prometheus

Prometheus(https://github.com/prometheus/prometheus) 는 cAdvisor, node-exporter 등 여러 정보를 수집해주는 도구의 데이터를 가져오고, 이에 대해 분석할 수 있는 쿼리 기능을 제공하는 도구로, 아래와 같이 올릴 수 있다.

prometheus:
  image: prom/prometheus
  container_name: prometheus
  restart: always
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
  command:
    - '--config.file=/etc/prometheus/prometheus.yml'

Prometheus 구동에는 설정 파일이 필요한데, docker-compose가 위치하는 폴더에 prometheus.yml 이라는 파일을 만들고, 다음과 같이 작성한다.

global:
scrape_interval: 15s

external_labels:
  monitor: 'uzukilive-monitor'

scrape_configs:
- job_name: 'cAdvisor'
  scrape_interval: 5s
  static_configs:
  - targets: ['cadvisor:8080']

각각 항목은 다음과 같다.

  • global.scrape_interval: 기본적인 수집 간격
  • global.external_labels.monitor: 모니터에 대한 별칭 설정
  • scrape_config: 스크래핑 할 수집 도구의 정보를 정의한다.
  • scrape_config.scrape_interval: 위 global.scrape_interval 와 다르게 해당 수집 도구의 수집 간격을 설정한다.
  • scape_config.static_configs.targets: 스크래핑할 수집 도구의 주소를 입력한다. 여기에서는 cAdvisor를 설정할 것이므로, 컨테이너 이름:포트 형식으로 입력한다.

마찬가지로, Prometheus는 자체적인 웹 UI를 제공하지만 Grafana로 볼 수 있도록 할 것이므로 웹 UI는 사용하지 않도록 한다. (하지만, 초반에 쿼리 습득을 위해 웹 UI를 잠시 열어놓고 사용하는 것도 나쁘지는 않다고 본다.)

준비 과정 – Grafana

Grafana(https://github.com/grafana/grafana) 는 각종 데이터 소스(Prometheus 등)에 대해 데이터를 대시보드 형태로 시각화하는 도구로, 최근에는 자체적인 Alert 기능을 제공하기 시작했다. Grafana는 아래와 같이 올릴 수 있다.

  grafana:
  image: grafana/grafana
  container_name: grafana
  environment:
    - GF_SECURITY_ADMIN_PASSWORD=<GF_SECURITY_ADMIN_PASSWORD>
  volumes:
    - grafana-storage:/var/lib/grafana
  depends_on:
    - prometheus

준비 과정 – Caddy

Caddy(https://github.com/caddyserver/caddy/) 는 Web server로 Let’s encrypt 자동 연동 기능을 제공하거나, 손쉽게 사용할 수 있는 장점을 가지고 있다.

  caddy:
  image: abiosoft/caddy
  container_name: caddy
  ports:
    - '80:80'
    - '443:443'
  volumes:
    - .Caddyfile:/etc/Caddyfile
    - .caddy:/root/.caddy
  restart: always

Caddy 구동에는 설정 파일이 필요한데, docker-compose가 위치하는 같은 폴더 내에 .Caddyfile 이라는 파일을 만들고 다음과 같이 작성한다.

사용할 도메인 {
  proxy / grafana:3000
}

proxy 명령어는 match 하는 주소에 특정 주소를 연결하는 명령어로, proxy {matrcher token} {url} 로 이루어진다. 여기에서는 / 에 대해 grafana 컨테이너에 접속한다.

Grafana 설정 – 데이터 소스 추가

위 4개의 컨테이너를 모두 설정하고 Caddy에 설정한 도메인으로 접속한 다음, grafana에 설정한 비밀번호와 함께 admin 으로 로그인하면 Grafana의 메인 페이지가 보이게 된다.

‘Add data source’ 를 클릭하여, Prometheus 를 클릭하고 URL에 http://prometheus:9090 을 입력한다.

그 다음, 밑의 ‘Save & Test’ 를 클릭하여 저장한다.

Grafana 설정 – 대시보드 구현

데이터 소스를 추가했다면, 왼쪽의 +를 눌러 대시보드를 추가한다. 대시보드에는 다수의 Panel를 보여지게 할 수 있는데, 이 Panel는 각각 Prometheus의 쿼리를 통해 가져온 데이터를 표시한다.

Panel는 우측 상단 그래프 모양 +를 눌러 추가할 수 있고, 패널에 표시할 데이터는 Add Query를 클릭하여 추가할 수 있다.

Add Query를 클릭하면 상단 그래프와 함께 쿼리를 입력하는 곳이 나오는데, 여기에 후술할 Prometheus 쿼리 (PromQL) 를 입력한다.

쿼리를 입력하여 데이터가 나오는 것을 확인했다면, 좌측의 그래프 모양을 눌러 표시할 UI를 선택할 수 있다.

그래프, 스탯, 게이지 등 다양한 UI를 가지고 있고, 각각에 대해 설정도 가능하다.

마지막으로 설정 아이콘을 눌러 Panel에 대한 기본 설정 (이름, 설명) 등에 대해 추가가 가능하다.

cAdvisor 가 제공하는 컨테이너 측정 항목은 https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md 여기에서 볼 수 있는데, 이 글에서는 아래의 측정 항목을 보여주려고 한다.

  • 실행중인 컨테이너 갯수
  • 총 메모리 사용량
  • 총 CPU 사용량
  • 컨테이너별 CPU 사용량
  • 컨테이너별 메모리 사용량
  • 컨테이너별 네트워크 Rx (수신량)
  • 컨테이너별 네트워크 Tx (발송량)

PromQL 설명

Prometheus 가 제공하는 쿼리를 PromQL(Prometheus Query Language) 라고 하는데, 아래 4가지 타입을 제공한다.

  • 즉석 벡터(Instant vector) – 각 시계열에 대해 단일 표본을 포함하는 집합으로, 모두 동일한 timestamp를 공유한다.

즉석 벡터는 주어진 타임스탬프에서 각 시계열에 대해 단일 표본을 선택할 수 있다. 간단하게는 측정 항목의 이름으로만 지정되는데, 가령 후술할 container_last_seen 에 대한 모든 시계열을 보고 싶다면 container_last_seen 를 쿼리로 입력하면 된다.

위 사진에서도 알 수 있듯이 container_last_seen 에 대한 시계열은 여러 데이터를 가지고 있는데, 이에 대한 필터링을 {} 에 추가하여 설정할 수 있다. 가령, 도커 이미지가 있는 실제 데이터를 필터링하고 싶다면 container_last_seen{image != ""} 를 쿼리로 입력하면 된다.

조건대로, image가 비어있지 않은 항목만 나온 것을 알 수 있다.

  • 범위 벡터(Range vector) – 각 시계열에 대해 시간 경과에 따른 데이터의 범위를 포함하는 시계열 집합

범위 벡터는 주어진 타임스탬프에서 지정한 과거 시간까지에 대해 각 시계열에서 데이터를 추출할 수 있다. 기본적으로 ‘즉석 벡터’와 같으나, [5m] 등의 추가 쿼리가 붙는다.

가령, 후술할 container_network_receive_bytes_total 에 대해 5초동안의 데이터를 보고 싶다면, container_network_receive_bytes_total{image != ""}[5s] 를 쿼리로 입력하면 된다.

  • 스칼라(Scalar) – 단순한 부동 소수점(floating point)
  • 문자열(String) – 단순한 문자열 값, 현재는 사용되지 않음

위 네 가지 타입을 제공하면서 같이 자체적인 쿼리 펑션을 제공한다. sum 이나 count 같은 기본적인 구성부터, 범위 벡터에 대한 초당 시계열을 제공하는 irate까지 다양하나 다 설명하기는 어렵고, 실제로 쿼리를 사용해 시각화해보면서 설명하기로 한다.

문서는 https://prometheus.io/docs/prometheus/latest/querying/functions/ 에서 볼 수 있다.

실행중인 컨테이너 갯수 표시하기

  • 사용할 측정 항목: container_last_seen
  • PromQL: count(container_last_seen{image!=””})

container_last_seen 이 컨테이너가 마지막으로 보여진 timestamp 를 반환하는 것을 이용하여 현재 실행중인 컨테이너에 대해 시계열을 가져오고, 이를 count 펑션을 통해 갯수를 가져온다.

단순 컨테이너 텍스트이므로, 시각화 방법은 ‘Singlestat’를 사용한다.

CPU 전체 사용량

  • 사용할 측정 항목: container_cpu_user_seconds_total
  • PromQL: sum(rate(container_cpu_user_seconds_total{image != “”}[5m]) * 100)

container_cpu_user_seconds_total 는 각 컨테이너에 대한 CPU 사용량을 보여주는데, 해당 쿼리는 현재부터 5분 전까지의 시계열 데이터를 가져와 퍼센트를 구하고, (0.12 * 100 = 12%) 이를 합산(sum) 하여 보여주는 역할이다.

퍼센트 데이터의 합계이므로 시각화 방법은 ‘Gauge’ 로 설정하고 필드는 percent (0-100) 를 선택한다.

메모리 전체 사용량

  • 사용할 측정 항목: container_memory_usage_bytes
  • PromQL: sum(container_memory_usage_bytes{image != “”}) / 1024 / 1024

container_memory_usage_bytes 는 각 컨테이너의 메모리 사용량을 보여주는데, 해당 쿼리는 메모리 사용량을 더하고 MB 단위로 표시한다.

단순한 사용량의 합계이므로 시각화 방법은 ‘Singlestat’를 사용하고, Value의 Unit는 megabytes를 선택한다.

참고로, cAdvisor는 기본적인 하드웨어 사용량을 제공하는데, 이를 활용하여 메모리 사용량 퍼센트도 구할 수 있다.

  • 사용할 측정 항목: machine_memory_bytes, container_memory_usage_bytes
  • PromQL: sum(container_memory_usage_bytes{image!=””}) / sum(machine_memory_bytes) * 100

이미지가 존재하는 container_memory_usage_bytes 시계열 데이터에 대해 합계를 구하고, 이를 하드웨어 전체 메모리로 나눈다.

CPU 전체 사용량과 같이 퍼센트 데이터이므로 시각화 방법은 Guage로 설정한다.

컨테이너별 CPU 사용량

  • 사용할 측정 항목: container_cpu_user_seconds_total
  • PromQL: rate(container_cpu_user_seconds_total{image != “”}[5m]) * 100

container_cpu_user_seconds_total 는 각 컨테이너의 초당 CPU 사용량을 보여주는데, 해당 쿼리는 5분동안의 CPU 사용량에 대한 퍼센트를 표시한다. 자세히 보면 전체 사용량의 쿼리에서 sum이 빠진 것을 제외하고는 같다.

그래프 데이터이므로 시각화 방법은 ‘Graph’를 표시한다. 이 때 Legend가 해당 시계열 데이터의 모든 정보를 표시하는데, 이를 ‘이름’ 만 보여주고 싶다면 쿼리 입력하는 곳의 Legend에 {{name}} 를 입력하면 이름만 보이게 된다.

컨테이너별 메모리 사용량

  • 사용할 측정 항목: container_memory_usage_bytes
  • PromQL: container_memory_usage_bytes{image != “”}

container_memory_usage_bytes 는 각 컨테이너의 메모리 사용량을 보여주는데, 해당 쿼리는 메모리 사용량을 나타낸다.

그래프 데이터이므로 시각화 방법은 ‘Graph’ 를 표시한다.

컨테이너별 네트워크 Rx (수신량)

  • 사용할 측정 항목: container_network_receive_bytes_total
  • PromQL: irate(container_network_receive_bytes_total{image != “”}[5m])

container_network_receive_bytes_total 는 각 컨테이너에 대해 수신된 용량을 보여주는데, 해당 쿼리는 각 컨테이너의 수신된 용량을 5분동안 추출하여 초당 시계열을 제공하는 irate 를 사용하여 그 순간에 대해 bytes/sec를 보여주게 한다.

그래프 데이터이므로 시각화 방법은 ‘Graph’를 표시하고, Unit는 byte/sec를 사용한다.

컨테이너별 네트워크 Tx (발송량)

  • 사용할 측정 항목: container_network_transmit_bytes_total
  • PromQL: irate(container_network_transmit_bytes_total{image != “”}[5m])

container_network_transmit_bytes_total 는 각 컨테이너에 대해 발송된 용량을 보여주는데, 해당 쿼리는 각 컨테이너의 발송한 용량을 5분동안 추출하여 초당 시계열을 제공하는 irate를 사용하여 그 순간에 대해 bytes/sec를 보여주게 한다.

그래프 데이터이므로 시각화 방법은 ‘Graph’를 표시하고, Unit는 byte/sec를 사용한다.

위 쿼리들을 사용하여 적절히 구성한 대시보드는 다음과 같다.

컨테이너 json

마지막으로, Grafana는 대시보드에 대해 JSON Model로 Import/Export할 수 있는 기능을 가지고 있다.

이 글에서 최종적으로 만든 Json는https://gist.github.com/WindSekirun/e557879487aa87cfb14745ecdfbf8682 에서 볼 수 있다.

Deploy Minecraft by Docker

시작

언젠가 현실 친구 채팅방에서 ‘우리 다같이 한 것이 뭐가 있었냐?’ 하면서 이야기가 있었는데, 갑자기 마인크래프트 이야기가 나오게 되어 구축하게 되었다.

…는 마인크래프트 서버 구축이 처음은 아니었는데, 지금으로부터 10개월 전에 EC2라는 모드를 넣고 구축한 적이 있었다. 다만, 오래 못 갈 뿐이었다.

하지만, 이번에는 바닐라 기반으로 하자는 이야기가 나왔고, 약 6일 정도 운영하게 되었다.

6일 정도 운영하다보니 나름대로 문제와 이를 해결하기 위한 여러 방안을 구축해서 사용했고, 그 결과 현재는 다음과 같은 구조를 가지게 되었다.

Nodap-MC 서버 구성도

먼저, 마인크래프트 서버라고 해서 단순히 마인크래프트만 돌린다는 것은 전혀 아니다. 그럴 수도 있지만, 가령 아래와 같은 문제가 발생하게 된다.

  • 서버 관리자가 바빠서, 문제가 생겼을 때 복구할 수 없음
  • 오랜 시간 키고 있으면 반응이 느려지는 일이 있고, 서버 컨테이너를 재시작할 필요가 있음

따라서, minecraft 컨테이너 외에 여러 컨테이너들을 동시에 운영하게 되었고, 각각의 역할은 다음과 같다.

  • minecraft: 마인크래프트 서버. 좀 더 나은 성능을 위해 PaperMC 1.15.2 와 adoptOpenJdk11 버전을 사용.
  • rcon: minecraft 서버의 콘솔 접근에 사용되는데, 이는 op 권한을 아무도 갖고 있지 않기 때문이다.
  • Caddy: 외부에서 80, 443 으로 들어온 접근에 대해 각 컨테이너로 연결시킨다.
    • Nginx를 사용할 수도 있었지만, 이번에는 좀 더 간단하다는 Caddy를 사용했다. 제일 마음에 드는 점은, 각 연결 통로마다 tls [email protected] 라고 적어두면 Let’s Encrypt로 통해 인증서를 받아와서 Signing한다는 것이다.
  • Portainer: 웹 UI로 컨테이너나 이미지들에 대해 관리할 수 있고, 로그를 볼 수 있게 하는 도구이다. 서버 관리자(= 필자 본인)이 바쁘기 때문에, 문제가 생겼을 때 누군가가 빠르게 서버를 재부팅해서 사용할 수 있도록 구축했다.
  • Grafana, Prometheus, cAdvisor: cAdvisor는 각 컨테이너에 대한 metrics를 제공하고, Prometheus는 cAdvisor가 수집한 metrics를 저장하는 역할을 맡고, Grafana는 이를 그래프로 수치화해서 보여주는 역할이다. 여기서, Grafana Alerts를 이용하여 메모리 사용량이 일정량 이상일 때 텔레그램으로 경고 메세지를 보낸다.

구축 정리

구축에 사용된 서버는 Vultr 4vCPU 8GB 사양이다.

기본 과정

Ubuntu 기준이며, https://docs.docker.com/install/linux/docker-ce/ubuntu/ 를 참고한다.

sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
curl -fsSL <a href="https://download.docker.com/linux/ubuntu/gpg">https://download.docker.com/linux/ubuntu/gpg</a> | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository "deb [arch=amd64] <a href="https://download.docker.com/linux/ubuntu">https://download.docker.com/linux/ubuntu</a> bionic stable"
sudo apt update
sudo apt install docker-ce docker-ce-cli <a href="http://containerd.io">containerd.io</a>
sudo curl -L "<a href="https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$">https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$</a>(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

여기까지 9개의 Command를 실행하여 Docker-CE 버전과 docker-compose를 설치할 수 있다.

마인크래프트 서버 (itzg/minecraft-server)

version: '2'

services:
  minecraft:
    image: itzg/minecraft-server:adopt11
    container_name: "minecraft"
    ports:
      - ...
    environment:
      - EULA=TRUE
      - ENABLE_RCON=TRUE
      - RCON_PASSWORD=...
      - MEMORY=7G
      - TYPE=PAPER
      - VERSION=1.15.2
      - SERVER_PORT=...
    restart: always
    
  rcon:
    image: itzg/rcon
    container_name: "rcon"
    links:
      - minecraft
    networks:
      - nodap-mc-network

networks:
  nodap-mc-network:
    external: true

만약 Forge 기반의 모드를 사용하고자 하면 TYPE=FORGE로 통해 Forge 기반 서버로 사용할 필요가 있다. 처음에 Forge 기반으로 사용했었으나 메모리 사용량이나 서버가 급격하게 무거워지는 문제가 있었기에, PaperMC 를 사용하게 되었다.

rcon 에 연결된 networks인 nodap-mc-network 는 후술할 예정이지만 Caddy가 각 컨테이너로 연결할 수 있게 묶어주는 역할을 한다. TCP로 연결되어 따로 네트워크를 묶을 필요가 없는 minecraft는 기재하지 않았다.

Portainer 서버 (portainer/portainer)

Portainer 서버는 상기하였듯이 컨테이너의 관리를 웹 UI로 할 수 있다.

version: ‘2’

services:
  portainer:
    image: portainer/portainer
    restart: always
    container_name: "portainer-app"
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - nodap-mc-network

networks:
  nodap-mc-network:
    external: true

여기에서도 nodap-mc-network로 묶어주었다.

모니터링 Stack

모니터링 스택은 상기하였듯이 컨테이너의 CPU/메모리/네트워크 사용량을 볼 수 있게 한다.

cAdvisor 서버 (google/cadvisor)

cadvisor:
image: google/cadvisor:latest
container_name: cadvisor
volumes:
– /:/rootfs:ro
– /var/run:/var/run:ro
– /sys:/sys:ro
– /var/lib/docker/:/var/lib/docker:ro
– /dev/disk/:/dev/disk:ro
restart: always
networks:
– nodap-mc-network

cAdvisor 등이 외부에 노출될 필요는 없으나 필요할 경우, 8080:8080 포트를 추가하여 웹 UI를 볼 수 있다.

Prometheus 서버 (prom/prometheus)

prometheus:
    image: prom/prometheus
    container_name: prometheus
    restart: always
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    networks:
      - nodap-mc-network

설정에 필요한 prometheus.yml는 다음과 같다.

global:
  scrape_interval: 15s 

  external_labels:
    monitor: 'nodap-mc-monitor'

scrape_configs:
  - job_name: 'cAdvisor'
    scrape_interval: 5s
    static_configs:
    - targets: ['cadvisor:8080']

Grafana 서버 (grafana/grafana)

grafana:
image: grafana/grafana
container_name: grafana
environment:
– GF_SECURITY_ADMIN_PASSWORD=
volumes:
– grafana-storage:/var/lib/grafana
depends_on:
– prometheus
networks:
– nodap-mc-network

그리고, Grafana Alerts를 이용하여 알림 기능을 구현하였는데, 위 Memory Usage 의 빨간색 부분이 기준치이다.

알림 룰은 간단하게 진행하였는데, 매 20분마다 5분간의 평균 메모리 사용량이 7301444403 Bytes (= 6.8GiB) 를 넘으면 Notification을 보내주는 규칙이다.

이 세 서버를 모두 정의하면 다음과 같다.

version: ‘2’

services:
  cadvisor:
    image: google/cadvisor:latest
    container_name: cadvisor
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    restart: always
    networks:
      - nodap-mc-network

  prometheus:
    image: prom/prometheus
    container_name: prometheus
    restart: always
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    networks:
      - nodap-mc-network

  grafana:
    image: grafana/grafana
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=
    volumes:
      - grafana-storage:/var/lib/grafana
    depends_on:
      - prometheus
    networks:
      - nodap-mc-network

volumes:
  grafana-storage:

networks:
  nodap-mc-network:
    external: true

Caddy 서버 (abiosoft/caddy)

Caddy는 Go로 작성된 웹 서버로 간단한 사용법으로 TLS certificate 관리, 리버스 프록시, 정적 파일 호스팅 등의 많은 기능을 제공한다.

version: ‘2’

services:
  caddy:
    image: abiosoft/caddy
    container_name: caddy
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - .Caddyfile:/etc/Caddyfile
      - .caddy:/root/.caddy
    restart: always
    networks:
      - nodap-mc-network

networks:
  nodap-mc-network:
    external: true

그리고, 설정 파일이기도 한 Caddyfile는 다음과 같다.

***1.uzuki.live {
    proxy / portainer:9000
    tls [email protected]
}

***2.uzuki.live {
    proxy / grafana:3000
    tls [email protected]
}

***3.uzuki.live {
    proxy / rcon:4326
    tls [email protected]
}

***[1~3].uzuki.live 가 하나의 사이트로 인식하게 되며, 각 사이트는 컨테이너:포트 로의 proxy 기능과 tls [email protected] 를 가지게 된다.

Flask + Gunicorn with Docker

가끔 유지보수 하고 있던 node.js 기반의 텔레그램 봇에서 파이썬을 도입하게 되었다. 그 이유는, 봇의 특정 기능 중 요약문을 제공하는 기능을 필요로 했었고, Komoran + TextRank 알고리즘 기반으로 구성하는 것이 그나마 빠르게 진행할 수 있었기 때문이었다.

다만, 다른 곳에서 해당 기능을 사용하므로 api로 배포할 필요가 있었고, 전체적인 구조는 다음과 같다.

  • 텔레그램 봇 —> Textrank API —> TextRank Analyzer

이 중, API를 사용하기 위해 Flask-RESTFul 를 사용하고, 배포는 Python WSGI 웹 서버인 Gunicorn 를 사용했다.

이 글에서는 TextRank Analyzer 를 API로 expose 하고, Docker로 배포하는 과정을 정리한다.

API 연결

먼저, TextRank 알고리즘 수행을 위해 외부에서 받아 와야 할 정보들에 대해 정리가 필요하다.

  • 요약할 문장 (필수값)
  • 최대 요약할 문장 갯수 (기본값 3)
  • 최대 요약할 키워드 갯수 (기본값 3)
  • PageRankDamping Factor 값 → 사용자가 클릭을 중단할 확률. 기본값 0.85
  • 최상위에 랭크된 문장과 그 다음 문장 간의 최소 코사인 거리. 기본값 0.3
  • 허용 단어 빈도 (최소 n번 사용된 문장을 선택하나 m번 이상 사용된 문장은 사용하지 않음). 기본값 3, 20
  • 최소 문장 단어 수 (최소 n개 단어를 사용한 문장을 사용하나 m개 이상 단어가 사용되면 사용하지 않음). 기본값 10, 80

이제, 이 값들을 body로 받아 보관해야 하는데, Flask-RESTFul 는 reqparse 라는 기능을 제공한다.

이는 argument parsing 에 많이 쓰이는 argparse 와 비슷한 사용법을 가지면서도 사용자의 post body 를 해석하는 기능을 제공한다.

사용법은 `parser.add_argument(name, type) 이고, required 여부나 default 도 지정할 수 있다.

이를 반영한 API 클래스는 다음과 같을 것이다.

class TextRank(Resource):
    def post(self):
        try:
            parser = reqparse.RequestParser()
            parser.add_argument('body', type=str, required=True) 
            parser.add_argument('sentenceCount', type=int, default=3, required=False) 
            parser.add_argument('keywordCount', type=int, default=5, required=False)  
            parser.add_argument('beta', type=float, default=0.85, required=False) 
            parser.add_argument('diversity', type=float, default=0.3, required=False)  
            parser.add_argument('minWordCount', type=int, default=3, required=False)  
            parser.add_argument('maxWordCount', type=int, default=20, required=False)  
            parser.add_argument('minSentencePenalty', type=int, default=10, required=False) 
            parser.add_argument('maxSentencePenalty', type=int, default=80, required=False) 
            args = parser.parse_args()

            ...

            return {
                'code': '1',
                'message': 'Success',
                'sentence': sentence,
                'keyword': keywords
            }

        except Exception as e:
            return {'code': -1, "message": 'Failed ' + str(e)}

만들어진 TextRank 클래스를 Flask에 연결하게 되면, 지정한 routing 에 따라 API가 호출된다.

app = Flask(__name__)
api = Api(app)
api.add_resource(TextRank, '/summarize')

if __name__ == '__main__':
    app.run(port=8000)

이와 같이 작성하고 Run을 하여 127.0.0.1:8000/summarize 로 API 요청을 보내면 결과가 나오게 된다.

Gunicorn 배포

python3 server.py 로 서버를 실행시킬 수 있지만, 이는 development 기준으로 배포용도에는 맞지 않다.

실제로, 위 명령어로 배포하게 되면 아래와 같은 메세지가 나오게 된다.

* Serving Flask app "server" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on <http://127.0.0.1:8000/> (Press CTRL+C to quit)

요점은 production WSGI server 를 사용하라는 것이다. 여기서 WSGI 는 PEP-3333 에 정의된 파이썬 웹 서버 게이트웨이로, 파이썬 스크립트가 웹 서버와 통신하기 위해 작성된 인터페이스라고 설명할 수 있다.

여기서는 상기하였듯이 Gunicorn 를 사용했고, 사용법은 다음과 같다.

gunicorn server:app

여기서 server:app 는 각각 Flask를 담고 있는 파이썬 스크립트와 Flask 로컬 변수를 의미한다.

명령어를 입력하면 아래와 같은 로그가 나오게 된다.

% gunicorn server:app
[2020-02-15 13:15:14 +0900] [38332] [INFO] Starting gunicorn 20.0.4
[2020-02-15 13:15:14 +0900] [38332] [INFO] Listening at: <http://127.0.0.1:8000> (38332)
[2020-02-15 13:15:14 +0900] [38332] [INFO] Using worker: sync
[2020-02-15 13:15:14 +0900] [38336] [INFO] Booting worker with pid: 38336

기본적으로 127.0.0.1 (loopback) 주소에만 대응하고 있는데, 이를 도커 등으로 활용하려면 0.0.0.0:8000 등으로 port만 지정할 필요가 있다.

이는 -b 0.0.0.0:8000 으로 지정할 수 있고, 최종 명령어는 다음과 같다.

gunicorn -b 0.0.0.0:8000 server:app

이를 실행하면 다음과 같이 나온다.

% gunicorn -b 0.0.0.0:8000 server:app
[2020-02-15 13:16:04 +0900] [38387] [INFO] Starting gunicorn 20.0.4
[2020-02-15 13:16:04 +0900] [38387] [INFO] Listening at: <http://0.0.0.0:8000> (38387)
[2020-02-15 13:16:04 +0900] [38387] [INFO] Using worker: sync
[2020-02-15 13:16:04 +0900] [38391] [INFO] Booting worker with pid: 38391

Docker 배포

위 gunicorn 을 사용해 Dockerfile로 만들면 다음과 같을 것이다.

FROM ubuntu
WORKDIR /usr/src/app
RUN rm -rf /var/lib/apt/list/* && apt-get update && apt-get install python3 python3-pip -y
ADD . .
RUN pip3 install -r requirements.txt
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "server:app"]

간단히, python 과 python3-pip 를 설치하고 모든 파일을 복사하여 requirements.txt 에 적힌 항목을 전부 설치한다. 마지막으로, 포트 8000를 expose 하여 gunicorn를 실행한다.