///
Search

07_로깅 및 모니터링

서비스를 시작하게 되면, 초기에는 20ms 속도로 즉시 응답 하지만, 사람들이 서비스에 항목을 추가하면서 점차 속도가 느려질 수 있다.
우리는 속도가 느려지는 원인을 알아야한다.
구글 애널리틱스 같은 것을 사용해서 최종 사용자가 겪은 로딩 속도를 측정해야한다.
모놀리식 애플리케이션에서는 HTML를 잘못 작성하거나, 일체형 애플리케이션이 느려져서 그렇기에 쉽게 원인을 찾을수 있다.
하지만 마이크로서비스에서는 데이터 저장소가 1,000개가 있을 수 있으며, 더 나아가 클라우드 저장소 큐와 메시지 라우터랑도 수 십개의 서비스가 연결될 수 있다.
또 다른 문제점은 구글 애널리틱스를 사용하면 부하가 걸려 있는 상태에서는 사이트가 느려지고 있다고 쉽게 알려 줄 수 없다.
API만 있고 웹사이트 없는 서비스 경우 구글 애널리틱스를 사용할 수 없다.
스택 트레이스 및 문제를 진단하는 데 도움이 되는 기타 애플리케이션 출력은 세가지 범주로 나눌 수 있다.
1.
측정 지표 : 시계열 데이터(예를 들면 트랜잭션이나 개별 컴포넌트의 실행시간) 항목이다.
2.
텍스트 기반 로그 : 텍스트 기반 레코드는 Nginx 같은 애플리케이션이 출력하는 구식 로그나 사용자가 만든 애플리케이션 소프트웨어의 텍스트 로그이다.
3.
예외 : 예외는 잠재적으로 앞의 두 범주에 속할 수 있다. 그러나 예외는 예외적으로 다루어야 하므로 다른 범주로 분리하고자 한다.

로깅 모범 사례

1.
애플리케이션 로깅을 진행중인, 반복되는 프로세스로 간주한다. 높은 수준에서 로그를 남기고 더 자세한 계측 정보를 추가한다.
2.
분산 시스템에서 문제가 발생하면 전체가 제대로 동작하지 않으므로 프로세스의 모든 부분을 측정한다.
3.
기대 이하의 성능을 나타내는 모든 것을 기록해야 한다. 예상되는 범위 밖에서 동작하는 모든 항목을 기록한다.
4.
하나의 로그 이벤트에서 일어난 일에 대한 전체적인 그림을 얻기 위해 컨텍스트 정보를 가능한 충분히 기록하도록 한다.
5.
시스템의 최종 소비자가 인간이 아닌 기계라고 생각해야 한다. 로그 관리 솔루션이 해석할 수 있는 레코드를 만든다.
6.
하나의 데이터 보다는 데이터의 경향성(트렌드)가 많은 것을 알려준다.
7.
계측이 프로파일링(동적 성능 분석)을 대신할 수 없으며, 반대의 경우도 마찬가지다.
8.
아무것도 모르는 상태로 시스템을 운영하는 것보다는 천천히 운영하는 것이 낫다. 따라서 측정을 할 것인지 말 것인지는 논쟁의 여지가 없으며, 얼마나 하느냐가 문제다.

(번외) 조금 더 심화적 성능 테스트 관련 서적

측정지표

측정 지표는 굉장히 유용한 로깅 형식 (간단한 숫자 데이터로 구성됨)

명명규칙

서비스의 문제를 조사하다 보면 많은 경우에 서비스 자체의 문제가 아닌 다양한 원인으로 인해 발생한 문제가 많다.
1.
호스트 서버의 CPU 고갈
2.
메모리 고갈
3.
네트워크 지연
4.
느린 데이터 저장소 쿼리
5.
상기 요인으로 인한 다운스트림 서비스 지연
점 표기법 서비스 이름 나누는 법
prod.server1.kittenserver.handlers.list.ok prod.server1.kittenserver.mysql.select_kittens.timing
HTML
복사

main.go

package main import ( "fmt" "log" "net" "net/http" "os" "time" "github.com/alexcesaro/statsd" "github.com/bshuster-repo/logrus-logstash-hook" "github.com/building-microservices-with-go/chapter7/server/handlers" "github.com/sirupsen/logrus" ) const port = 8091 func main() { statsd, err := createStatsDClient(os.Getenv("STATSD")) if err != nil { log.Fatal("Unable to create statsD client") } logger, err := createLogger(os.Getenv("LOGSTASH")) if err != nil { log.Fatal("Unable to create logstash client") } setupHandlers(statsd, logger) log.Printf("Server starting on port %v\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) } func setupHandlers(statsd *statsd.Client, logger *logrus.Logger) { validation := handlers.NewValidationHandler( statsd, logger, handlers.NewHelloWorldHandler(statsd, logger), ) bangHandler := handlers.NewPanicHandler( statsd, logger, handlers.NewBangHandler(), ) http.Handle("/helloworld", handlers.NewCorrelationHandler(validation)) http.Handle("/bang", handlers.NewCorrelationHandler(bangHandler)) } func createStatsDClient(address string) (*statsd.Client, error) { return statsd.New(statsd.Address(address)) } func createLogger(address string) (*logrus.Logger, error) { retryCount := 0 l := logrus.New() hostname, _ := os.Hostname() var err error // Retry connection to logstash incase the server has not yet come up for ; retryCount < 10; retryCount++ { conn, err := net.Dial("tcp", address) if err == nil { hook := logrustash.New( conn, logrustash.DefaultFormatter( logrus.Fields{"hostname": hostname}, ), ) l.Hooks.Add(hook) return l, err } log.Println("Unable to connect to logstash, retrying") time.Sleep(1 * time.Second) } log.Fatal("Unable to connect to logstash") return nil, err }
Go
복사
20행 에서 statsD 클라이언트를 초기화 하는 것을 볼 수 있다.
statsd, err := createStatsDClient(os.Getenv("STATSD")) if err != nil { log.Fatal("Unable to create statsD client") } logger, err := createLogger(os.Getenv("LOGSTASH")) if err != nil { log.Fatal("Unable to create logstash client") } setupHandlers(statsd, logger) log.Printf("Server starting on port %v\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) } func setupHandlers(statsd *statsd.Client, logger *logrus.Logger) { validation := handlers.NewValidationHandler( statsd, logger, handlers.NewHelloWorldHandler(statsd, logger), ) bangHandler := handlers.NewPanicHandler( statsd, logger, handlers.NewBangHandler(), ) http.Handle("/helloworld", handlers.NewCorrelationHandler(validation)) http.Handle("/bang", handlers.NewCorrelationHandler(bangHandler)) } func createStatsDClient(address string) (*statsd.Client, error) { return statsd.New(statsd.Address(address)) }
Go
복사

저장소 및 조회

측정 지표 데이터를 저장하고 조회하는 데는 여러 가지 옵션이 있다.
자체적으로 호스팅하는 서비스가 있거나, SaaS(클라우드에서 서비스의 형태로 제공되는 소프트웨어)
SaaS의 경우 Datadog를 살펴 보는것이 유리하고 측정값을 보내는 방식은 두가지가 있다.
첫 번째는 API와 직접 통신하는 것
두 번째는 Datadog 수집기를 클러스터 내부의 컨테이너로 실행하는 것이다.
Graphite, Prometheus, InfluxDB, ElasticSearch와 같은 백엔드 데이터 저장소에 대한 많은 선택지가 있다.

/docker-compose.yml

statsD : Prometheus가 통계를 수집하는데 사용할 수 있는 end 포인트를 나타낸다.
grafana : 데이터를 그래프로 나타내기 위해 사용한다.
prometheus : 데이터를 수집하는 데 사용되는 데이터베이스 서버이다.
위의 3가지를 필수적으로 설치를 해줘야한다.
version: '2' services: kittenserver: build: ./ image: kittenserver:logging command: sh -c '/kittenserver' ports: - 8091:8091 environment: - "STATSD=statsd:9125" - "LOGSTASH=logstash:5000" links: - statsd - logstash prometheus: image: prom/prometheus links: - statsd volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - 9090:9090 statsd: image: prom/statsd-exporter graphana: image: grafana/grafana ports: - 3000:3000 links: - prometheus elasticsearch: image: elasticsearch:2.4.2 ports: - 9200:9200 - 9300:9300 environment: ES_JAVA_OPTS: "-Xms1g -Xmx1g" kibana: image: kibana:4.6.3 ports: - 5601:5601 environment: - ELASTICSEARCH_URL=http://elasticsearch:9200 links: - elasticsearch logstash: image: logstash command: -f /etc/logstash/conf.d/ ports: - 5000:5000 volumes: - ./logstash.conf:/etc/logstash/conf.d/logstash.conf links: - elasticsearch
YAML
복사

Grafana

수집한 측정 항목을 표시하기 위해 사용한다.
make runserver 명령을 사용해 스택을 시작하고 서버가 시작될 때까지 잠시 기다린 다음, 엔드 포인트에 몇개의 curl 요청을 실행해 시스템에 데이터를 채울 수 있다.

로깅

고도로 분산된 컨테이너에서 작업하는 경우 100개 이상의 애플리케이션 인스턴스를 실행할 수 도있다.
docker 기반의 애플리케이션이 동작하며 스케쥴러가 애플리케이션을 여러개의 호스트 사이트로 옮기면서 복잡한 관리가 필요하게 될 수있다.
위 문제를 해결하기 위한 좋은 방법은 처음부터 디스크에 로그를 쓰지 않는 것이다.
ELK 스택, Logmatic, Loggly와 같은 서비스 플랫폼 소프트웨어는 이 문제를 해결한다.
Elasticsearch
Logstash
Kibana(ELK)
는 자세한 로그를 남기는 데 있어 업계의 표준이다.
elasticsearch: image: elasticsearch:2.4.2 ports: - 9200:9200 - 9300:9300 environment: ES_JAVA_OPTS: "-Xms1g -Xmx1g" kibana: image: kibana:4.6.3 ports: - 5601:5601 environment: - ELASTICSEARCH_URL=http://elasticsearch:9200 links: - elasticsearch logstash: image: logstash command: -f /etc/logstash/conf.d/ ports: - 5000:5000 volumes: - ./logstash.conf:/etc/logstash/conf.d/logstash.conf links: - elasticsearch
YAML
복사

/logstash.conf

input { tcp { port => 5000 codec => "json" type => "json" } } ## Add your filters / logstash plugins configuration here output { elasticsearch { hosts => "elasticsearch:9200" } }
YAML
복사
위 입력 설정으로 TCP를 통해 Logstash 서버로 로그를 직접 전송할 수 있게 된다.
이렇게 하면 디스크에 로그를 쓰고 Logstash가 이 파일을 읽어야 한다는 문제가 해결된다.
위험의 감수에 따라 IDP도 사용할 수있다. UDP는 TCP보다 빠르지만 데이터가 수신됐음을 확인할 수 없으며, 일부 로그를 잃어 버릴수 있다는 점을 기억해야한다.

예외

go 에서 가장 중요한 특징중 하나는 에러가 발생했을 때 에러를 호출자 쪽으로 올려 보내서 사용자에게 노출되도록 하는 대신, 항상 에러를 처리하는 것이 표준 패턴이라는 것이다.
go 에서는 예기치 않은 에러를 처리하는데에 두가지의 훌륭한 방법이 있다.
1.
패닉 : 언어에 내장된 panic 함수는 현재 실행 중인 Go 루틴의 정상적인 실행을 중단한다. 모든 지연된 함수가 정상적으로 실행 된 후 프로그램이 종료된다.
2.
복구 : 이 함수는 애플리케이션이 패닉에 빠진 고 루틴의 동작을 제어할 수 있게 한다. 지연된 함수 내에서 호출 되면 복구는 패닉 함수의 실행을 중지하고 패닉 함수 호출시 전달된 에러를 리턴한다.

요약

1.
로깅 및 모니터링은 특정 유스 케이스 및 환경에 맞게 조정할 수 있는 항목이다.
2.
Datadog 와 Logmatic 과 같은 Saas를 사용하는 것은 매우 신속하게 준비하고 실행할 수 있는 좋은 방법이다.
3.
OpsGenie 또는 PagerDuty와 통합해 두면 문제가 발생할때 마다 즉각적인 경고를 받을수 있게 한다.