블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


피쳐 크로싱 (Feature crossing)

아키텍쳐 /머신러닝 | 2019.05.21 23:00 | Posted by 조대협


참고 문서 : 구글 머신러닝 크래쉬 코스


피처 엔지니어링 #1  - 피처 크로스


조대협 (http://bcho.tistory.com)


일반적인 선형 모델의 경우에 선을 그어서 문제를 해결할 수 있다. 아래 그림과 같은 데이타 분포의 경우에는 파란선과 붉은선 사이에 선을 그으면 문제가 해결된다.


그러나 아래와 같은 데이타 모델의 경우에는 선을 하나 그어서 해결할 수 가 없다. (선형 모델의 경우에)



세로축을 x1, 가로축을 x2라고 할때, y = w1x1 + w2x2 + w3(x1x2) +b 로 세번째 피쳐를 앞의 두 피쳐를 곱한 값을 이용하게 되면, 문제를 해결할 수 있다. 즉 x1이 양수이고 x2가 양수이면 양수가 되고 ,  x2가 음수이면 x1*x2는 양수가 된다. 즉 파란색 점이 위치한 부분은 모두 양수 값이 나온다.

x1이 양수, x2가 음수 이면 x1*x2는 음수가 된다. x1이 음수이고 x2가 음수이면 x1*x2는 음수가 된다. 즉 주황생점이 위치한 부분은 x1*x2가 음수 값이 된다.


이렇게 두개 이상의 피쳐를 곱해서 피쳐를 만드는 기법을 피쳐 크로싱이라고 한다. (Feature crossing) 그리고, 이렇게 피쳐를 가공해서 생성된 피쳐를 Synthetic feature 라고 한다.


위의 공간 문제의 경우에는 전형적인 XOR 문제인데, 이런 XOR 문제는 MLP (Multi Layered Perceptron)로 풀 수 있는데, 굳이 리니어 모델에서 피쳐 크로싱을 하는 이유는, 요즘에야 컴퓨팅 파워가 좋아졌기 때문에 MLP와 같은 복잡한 네트워크 연산이 가능하지만, 기존의 컴퓨팅 파워로는 연산이 어렵기 때문에, 스케일한 큰 모델에는 리니어 모델이 연산양이 상대적으로 적기 때문에, 큰 모델에는 리니어 모델을 사용했고, 리니어 모델에서 XOR와 같은 문제를 해결하기 위해서 피쳐 크로싱을 사용하였다.


'아키텍쳐  > 머신러닝' 카테고리의 다른 글

피쳐 크로싱 (Feature crossing)  (0) 2019.05.21
Deep learning VM  (2) 2018.12.05

SRE #6-운영에서 반복적인 노가다 Toil

조대협 (http://bcho.tistory.com)

Toil

Toil의 사전적인 뜻은 “노역"이라는 뜻을 가지고 있는데, 비속어를 사용해서 표현하자면 운영 업무에서의 “노가다" 정도로 이해하면 된다.  Toil 에 대한 정의를 잘 이해해야 하는데, Toil은 일종의 반복적인 쓸모없는 작업 정도로 정의할 수 있다.

경비 처리나, 회의, 주간 업무 보고서 작성과 같은 어드민 작업은 Toil에 해당하지 않는다. Toil은 운영상에 발생하는 반복적인 메뉴얼 작업인데, 다음과 같은 몇가지 특징으로 정의할 수 있다.

메뉴얼 작업이고 반복적이어야함

Toil의 가장 큰 특징은 사람이 직접 수행하는 메뉴얼 작업이라는 것이다. 그리고 어쩌다 한번이 아니라 지속적으로 발생하는 반복적인 작업이다.

자동화 가능함

자동화가 가능하다는 것은 자동화가 가능한데, 시간이 없어서(?) 자동화를 못하고 사람이 작업을 하고 있다는 것이다. 즉 사람이 하지 않아도 되는 일을 시간을 낭비하면서 하고 있다는 것인데, 서버 배포를 테라폼등으로 자동화할 수 있는데, 자동화 하지 않고, 수동으로 작업하고 있는 경우 Toil에 해당한다.

밸류를 제공하지 않는 작업

Toil작업은 작업을 하고나도, 서비스나 비지니스가 개선되지 않는 작업이다. 작업 전/후의 상태가 같은 작업인데, 장애 처리와 같은 것이 대표적인 예에 속한다. 장애 처리는 시스템을 이전 상태로 돌리는 것 뿐일뿐 새로운 밸류를 제공하지 않는다.

서비스 성장에 따라서 선형적으로 증가하는 작업

Toil은 보통 서비스가 성장하고 시스템이 커지면 선형적으로 증가한다. 애플리케이션 배포나, 시스템 설정, 장애 처리도 시스템 인스턴스의 수가 늘어날 수 록 증가하게 된다.


정리해보자면, Toil이란 성장에 도움이 되지 않으면서 시간을 잡아먹는 메뉴얼한 작업이고, 서비스의 규모가 커지면 커질 수 록 늘어나는 자동화가 가능한 작업이다. 일종의 기술 부채의 개념과도 연결시켜 생각할 수 있다.

Toil을 왜? 그리고 어떻게 관리해야 하는가?

그러면 Toil을 어떻게 관리해야 할까? Toil은 의미없는 작업이 많지만, Toil을 무조건 없애는 것은 적절하지 않다. 예를 들어서 일년에 한두번 발생하는 작업을 자동화하려고 노력한다면, 오히려 자동화에 들어가는 노력이 더 많아서 투자 대비 효과가 떨어진다. 반대로 Toil이 많으면 의미있는 기능 개발작업이나 자동화 작업을할 수 없기 때문에 일의 가치가 떨어진다.

그래서 구글의 SRE 프랙티스 에서는 Toil을 30~50%로 유지하도록 권장하고 있다.  Toil을 줄이는 방법 중에 대표적인 방법은 자동화를 하는 방법이다. 자동화를 하면 Toil을 줄일 수 있는데, 그러면 남은 시간은 어떻게 활용하는가? 이 시간을 서비스 개발에 투자하거나 또는 다른 서비스를 운영하는데 사용한다.



위의 그림이 가장 잘 설명된 그림인데, 초반에는 Service A에 대해서 대부분의 Toil이 발생하는데, 자동화를 하게 되면, Toil 이 줄어든다.그러면 줄어든 Toil 시간을 다른 서비스를 운영하는데 사용을 하고 결과적으로는 여러 서비스를 동시에 적은 시간으로 운영할 수 있도록 된다.


이 개념을 확장해보면 Devops의 목표와도 부합이 되는데, Devops는 개발과 운영을 합쳐서 진행하는 모델인데, 개발이 직접 운영을 하기 위해서는 플랫폼이 필요하다. 즉 개발자가 직접 하드웨어 설치, 네트워크 구성등 로우 레벨한 작업을 하는것이 아니라, 자동화된 운영 플랫폼이 있으면, 개발팀이 직접 시스템을 배포 운영할 수 있게 된다.


<그림, Devops에서 개발자와 Devops 엔지니어의 역할>


그래서 개발팀이 이러한 플랫폼을 이용해서 Devops를 한다면, Devops엔지니어는 개발팀이 사용할 운영플랫폼을 개발하는데, 운영플랫폼이란 자동화된 플랫폼을 이야기 한다.

Toil을 측정해가면서 각 서비스별로 자동화 정도를 측정하고, 자동화가 될 수 록, 그 서비스에서 빠져가면서 새로운 서비스로 옮겨가는 모델이라고 볼 수 있다.


Toil를 어떻게 측정할것인가?

이제 Toil이 무엇인지, Toil을 줄여서 어떻게 활용하는지에 대해서 알아보았다.

그러면, 이 Toil을 어떻게 측정하는가?

Toil의 종류를 보면, 크게 배포나 장애처리 등으로 볼 수 있는데, 장애처리의 경우, 장애 발생시 장애 티켓을 버그 시스템에 등록한후에, 처리가 완료될때까지의 시간을 측정하면, 장애 처리에 대한 Toil을 측정할 수 있다.

메뉴얼 배포와 같은 경우에는 특별한 시스템 (Task management)을 사용하지 않는 이상 정확하게 측정하기가 어려운데 그래서 이런 경우에는 snippet (주간 업무보고)나 또는 주기적인 설문 조사를 통해서 측정하는 방법이 있다.




SRE #5 - Error budget


조대협 (http://bcho.tistory.com)


SLI와 SLO에 대한 개념을 이해 했으면 다음은 Error budget에 대한 개념을 이해해야 한다.

Error budget은 단순하게 생각하면

Error budget = [100% - availability target]

와 같다. 예를 들어 설명하면, 한달에 SLO가 99.999%를 목표치로 설정했다면, 한달간 SLO는 0.001%의 다운 타임을 허용하게되고, 이 0.001%가 Error budget이된다.


위의 표는 가용성에 따라서, 허용되는 장애 시간을 정리해놓은 표이다.앞의 예제에서 99.999% 가용률을 목표로 봤을 때 허용되는 장애시간은, 0.001%로 다운 타임은 한달에 25.9 초만 허용된다.

그러면 이 시간을 어떻게 활용하는가? 허용되는 다운 타임에 한해서 예고된 다운 예를 들어서 배포나 시스템 업데이트를 수행한다. 만약에 남은 Error budget이 없다면, 새로운 기능에 대한 업데이트를 중지하고, 시스템의 가용성을 높이기 위해서 자동화나 프로세스 개선등의 작업등을 한다.

Error budget의 차감은 앞에서 이야기 한것 처럼 계획된 다운 타임이 아니라, 장애등에 의한 계획 되지 않은 다운 타임에도 차감을 한다.

Error budget을 활용하게 되면 개발팀 입장에서도 책임감을 가질 수 있는데, 예를 들어 코드의 허용된 Error budget 안에서만 배포를 할 수 있게 되기 때문에, 한달에 2번할 배포를 한번 하게 되거나 (횟수를 줄이는 것에 중점을 두지 말고, 그 만큼 신중해진다는 의미에 중점을 두기 바란다. ) Error budget을 차감당하지 않도록 하기 위해서 테스트를 좀 더 꼼꼼하게 할 수 있다.

Error budget 을 다 소모하면, Error budget을 복구하는 여러가지 방법이 있겠지만, 이건 팀에서 (임원포함) 정책적으로 동의해서 결정해야 한다. 앞에 예에서 언급한것과 같이 새로운 기능 배포를 멈추고 안정화 작업에 집중을 하는 방법도 있고, 또는 Error budget이 0이 된 경우 해당 엔지니어나 개발팀에 대해서 강도 높은 코드 리뷰를 다시 받도록 하는 방법등 여러가지 방법이 있다.

예전 김요섭님 강의에 몇가지 사례가 있으니 참고하면 좋다.

Azure Devops


조대협 (http://bcho.tistory.com)


페이스북 타임라인을 보다가. “Azure Devop가 나왔는데. JIRA 따위는 꺼져버려.." 라는 말이 있어서 몬가 궁금해서 살펴봤는데 이거 정말 물건이다. MS가 요즘 많이 변하하고 있다고 들었는데, 정말 잘 만든 제품인듯 Devops라고 해서 Monitoring,Logging쪽 제품으로 생각했는데,


Task management, 부하 테스트, git repo & 빌드, 빌드 파이프라인, 웹테스트 툴 등이 들어있는데, 전반적으로 정말 좋은듯. 예전 IBM의 Jazz 플랫폼과 컨셉은 비슷한데, UI나 기능이나 반응 속도나 압권이고 거기다가 일부 플랜은 무료다!! 나 같은 개인 개발자(집에서는)에게는 딱이 아닐까 싶다.


대충 후욱 살펴봤는데,

Board

이건 JIRA대응의 Task management tool + agile board 정도로 보면되는데, JIRA가 충분히 위협 받을만 하겠다.


아래는 백로그를 등록하는 화면인데, 스크럼 스프린트 맞게 맵핑할 수 있도록 되어 있다. 사용자 스토리에 스토리 포인트 넣는 기능도 있고,  




아래는 스프린트 화면인데, 사용자 스토리 아래에 Sub task를 정의할 수 있도록 되어있고, Subtask 마다 진행 상황을 컨트롤할 수 있도록 되어 있다.


Task 트렌드를 추적할 수 있는 그래프도 제공하고


전체 상황을 볼 수 있는 대쉬 보드도 제공한다. 그리고 사용자 스토리 기반으로 스프린트 기반의 팀 속도를 나타낼 수 있는 기능도 제공한다.

그리고 마지막으로, 사용자 스토리 마다, 소스코드 저장소에서 스토리마다 브렌치로 연결할 수 있는 기능이 있다.


한마디로 군더더기가 없고, 아쉬운 기능이 없다. (나중에 찾으면 있을지도 모르지만). 가격도 괜찮고, 정말 써볼만한듯. 그동안 JIRA가 무료 호스팅 버전의 경우에는 사용자 수나 기타 제약이 있었는데, 미니 프로젝트는 이걸로 써도 충분하겠다.


Test plans

테스트 플랜 도구는 테스트 관리 도구인데, 앞의 Board와 연동이 되서 사용자 스토리에 테스트 플랜을 연결해서 사용할 수 있다. 별거 아닌 기능처럼 보일 수 있는데, TestLink(오픈소스)등이 이런 기능을 제공하고, 탐색적 테스팅이나 메뉴얼 테스팅에는 절대적으로 유용한 도구이다.



Testing extension을 이용하면, 별도의 툴을 이용해서 웹 액티버티를 레코딩해서 웹 테스팅도 가능한걸로 보이는데, 모바일 앱쪽은 자료가 없어서 잘모르겠다. Testing extension은 다운받을려고 보니 30일 무료가 뜨는 걸로 봐서 패스.. (사실 우분투가 메인 OS라서 안도는건지도..)

그리고 부하 테스트 기능이 같이 있다.



Jmeter 테스트도 되고, URL 기반으로 간단한 부하테스트등 왠만큼 복잡하지 않은 마이크로벤치정도는 충분히 가능할것 같다.


Repo

다음툴은 Repos 서비스 인데 git 호환 리파지토리로 보이는데 상당히 깔금하다.

github에서 바로 import가 되길래, 바로 Import도 해봤는데, 여기서 연동해서 빌드도 바로 가능하더라.. Maven 등 빌드 타입만 정해주면 바로 빌드가 되니, 빌딩에 들어가는 시간도 절약할 수 있을듯하다.




아직 다 자세하게 보고 써보지 않아서 대형 프로젝트에는 잘 모르겠지만 일반적인 스타트업 수준이라면 이걸로 대부분의 개발은 크게 문제가 없지 않을까 싶다.

일단 강추하는걸로!!



2019년 샌프란시스코 구글 NEXT 2019에서 발표한 로깅 시스템에 대한 강의 입니다.

구글 스택 드라이버 로깅에 대한 소개와, 삼성전자가 어떻게 스택 드라이버 툴을 이용해서 Devops로 적응했는지에 대한 사례입니다.

영어 컨텐츠 입니다.



SRE #4-예제로 살펴보는 SLI/SLO 정의 방법

조대협 (http://bcho.tistory.com)


앞에서 SRE의 주요 지표인 SLO/SLI의 개념에 대해서 설명하였는데, 그러면 실제 서비스에서는 어떻게 SLO/SLI를 정의하는지에 대해서 알아본다.

SLI는 사용자 스토리당 3~5개 정도가 적당하다. 사용자 스토리는 로그인, 검색, 상품 상세 정보와 같이 하나의 기능을 의미한다고 보면된다.


아래 그림과 같은 간단한 게임 서비스가 있다고 가정하자. 이 서비스는 웹사이트를 가지고 있고, 그리고 앱을 통해서 접근이 가능한데, 내부적으로 API 서비스를 통해서 서비스가 된다. 내부 서비스에는 사용자 랭킹(Leader board), 사용자 프로파일 (User profiles) 등의 서비스가 있다.


이 서비스에서 "사용자 프로필" 에 대한 SLI를 정의해보도록 하자.

SLI 지표 레퍼런스

앞에서 설명한 SLI 지표로 주로 사용되는 지표들을 되집어 보면 다음과 같다.

  • 응답 시간 (Request latency) : 시스템의 응답시간

  • 에러율 (Error rate%)  : 전체 요청에서 실패한 요청의 비율

  • 처리량(Throughput) : 일반적으로 초당 처리량으로 측정하고 TPS (Thoughput per second) 또는 QPS (Query per second)라는 단위를 사용한다.

  • 가용성(availability)  : 시스템의 업타임 비율로, 앞에서 예를 들어 설명하였다.

  • 내구성(Durability-스토리지 시스템만 해당) : 스토리지 시스템에만 해당하는데, 장애에도 데이타가 유실되지 않을 확률이다.

이런 지표들이 워크로드 타입에 따라 어떤 지표들이 사용되는지 정의해놓은 정보를 다시 참고해 보면 다음과 같다.

  • 사용자에게 서비스를 제공하는 서비스 시스템 (웹,모바일등) : 가용성, 응답시간, 처리량

  • 스토리지 시스템(백업,저장 시스템): 가용성, 응답시간, 내구성

  • 빅데이터 분석 시스템 : 처리량, 전체 End-to-End 처리 시간

  • 머신러닝 시스템 : 서빙 응답시간, 학습 시간, 처리량, 가용성, 서빙 정확도


이 서비스는 "사용자에게 서비스를 제공하는 서비스 시스템 패턴" 이기 때문에, 이 중에서 가용성과 응답시간을 SLI로 사용하기로 한다.

가용성 SLI

가용성은 프로파일 페이지가 성공적으로 로드된 것으로 측정한다.

그러면 성공적으로 로드 되었다는 것은 어떻게 측정할 것인가? 그리고, 성공호출 횟수와 실패 횟수는 어떻게 측정할것인가? 에 대한 질문이 생긴다.

이 서비스는 웹기반 서비스이기 때문에, HTTP GET /profile/{users}와 /profile/{users}/avatar 가 성공적으로 호출된 비율을 측정하면 된다. 성공 호출은 어떻게 정의할것인가? HTTP response code 200번만 성공으로 생각할 수 있지만 5xx는 시스템 에러이지만 3xx, 4xx는 애플리케이션에서 처리하는 에러 처리 루틴이라고 봤을때, 3xx,4xx도 성공 응답에 포함시켜야 한다. 그래서 2xx,3xx,4xx의 횟수를 성공 호출로 카운트 한다.


그러면 이 응답을 어디서 수집해야 할것인가? 앞의 아키텍쳐 다이어그램을 보면 API/웹서비스 앞에 로드밸런서가 있는 것을 볼 수 있는데, 개별 서버 (VM)에서 측정하는 것이 아니라 앞단의 로드밸런서에서 측정해도 HTTP 응답 코드를 받을 수 있기 때문에, 로드밸런서의 HTTP 응답 코드를 카운트 하기로 한다.

응답시간 SLI

그러면 같은 방식으로 응답시간에 대한 SLI를 정의해보자

응답 시간은 프로파일 페이지가 얼마나 빨리 로드 되었는지를 측정한다. 그런데 빠르다는 기준은 무엇이고, 언제부터 언제까지를 로딩 시간으로 측정해야 할것인가?

이 서비스는 HTTP GET /profile/{users} 를 호출하기 때문에, 이 서비스가 100ms 를 임의의 기준값으로 하여, 이 값 대비의 응답시간으로 정의한다.

응답 시간 역시 가용성과 마찬가지로 로드밸런서에서 측정하도록 한다.


이렇게 SLI를 정의하였으면, 여기에 측정 기간과 목표값을 정해서 SLO를 정한다.



가용성 SLO는 28일 동안 99.95%의 응답이 성공한것으로 정의한다.

응답시간 SLO는 28일 동안 90%의 응답이 500ms 안에 도착하는 것으로 정의한다. 또는 좀더 발전된 방법으로 99% 퍼센타일의 응답의 90%가 500ms 안에 도착하는 것으로 높게 잡을 수 있지만, 처음 정한 SLO이기때문에, 이정도 수준으로 시작하고 점차 높여가는 모델을 사용한다.

복잡한 서비스의 SLI 의 정의

앞의 예제를 통해서 SLI와 SLO를 정의하는 방법에 대해서 알아보았다. 사용자 스토리 단위로 SLI를 정한다하더라도, 현실에서의 서비스는 훨씬 복잡하고 많은 개수를 갖는다.

SLI가 많아지면, 관련된 사람들이 전체 SLI를 보기 어렵기 때문에 조금 더 단순화되고 직관적인 지표가 필요하다.

예를 들어보다. 구글 플레이 스토어를 예를 들어봤을때, 구글 플레이스토어는 홈 화면, 검색, 카테고리별 앱 리스트 그리고 앱 상세 정보와 같이 크게 4가지 사용자 스토리로 정의할 수 있다.


이 4가지 사용자 스토리를 aggregation (합이나 평균)으로 합쳐서 하나의 지표인 탐색(Browse)라는 지표로 재 정의할 수 있다. 아래는 4개의 SLI를 각각 측정한 값이다.



이 개별 SLI들을 합쳐서 표현하면 다음과 같이 표현할 수 있다. 전체 SLI의 값을 합친 후에, 백분률로 표현하였다.


이 하나의 지표를 사용하면 4개의 기능에 대한 SLI를 대표할 수 있다. 이렇게 개별 SLI의 합이나 평균을 사용하는 경우는 대부분의 경우에는 충분하지만 특정 서비스가 비지니스 임팩트가 더 클 경우 이를 동일하게 취급해서 합해버리면 중요한 서비스가 나도 이 대표값에는 제대로 반영이 안될 수 있기 때문에, 필요한 경우 개별 SLI에 적절한 가중치를 곱해서 값을 계산하는 것도 방법이 된다.



SRE #3-SRE 주요 지표 (SLI/SLO)

조대협 (http://bcho.tistory.com)

이글은 앞글 (https://bcho.tistory.com/1327)과 연결 됩니다.
앞에 까지 SRE가 무엇이고,  SRE가 하는일은 무엇이며, 어떻게 그 일을 수행 하는지에 대해서 알아보았다. SRE 프랙티스 에서는 의사 결정을 데이터에(data based decision) 따라 하기 때문에, 데이타 즉 지표를 정의하는 것이 중요하다. 그러면 SRE 에서는 어떻게 지표를 정의하고 활용하는지에 대해서 알아본다.

SLI (Service Level Indicator)

SLI는 서비스에 대한 수준을 측정하여, 정량적으로 정의한 지표이다.

"REST API의 응답시간 = 300ms"와 같이 절대 값을 사용하기도 하지만, (측정값/이상적인 값)과 같은 상대적인 지표를 사용하는 것도 이해하기가 쉽다. 예를 들어 한달동안 30분 장애가 났다고 하면 장애에 대한 SLI를 30분이 아니라 가용성으로 SLI를 정의해서 (30일-30분)/(30일) * 100 = 99.9305 %와 같은 식으로 표현하는게 좋다.

SLI로 사용할 수 있는 지표는 여러가지가 있지만 일반적으로 다음과 같은 지표들을 많이 사용한다.

  • 응답 시간 (Request latency) : 시스템의 응답시간

  • 에러율 (Error rate%)  : 전체 요청에서 실패한 요청의 비율

  • 처리량(Throughput) : 일반적으로 초당 처리량으로 측정하고 TPS (Thoughput per second) 또는 QPS (Query per second)라는 단위를 사용한다.

  • 가용성(availability)  : 시스템의 업타임 비율로, 앞에서 예를 들어 설명하였다.

  • 내구성(Durability-스토리지 시스템만 해당) : 스토리지 시스템에만 해당하는데, 장애에도 데이타가 유실되지 않을 확률이다.

어떤 지표를 SLI로 사용해야 하는가?

SLI로 정의할 수 있는 지표들이 많은데, 그러면 나의 서비스에서는 어떤 지표를 SLI로 정의해서 사용해야 할까? 특별히 정해진 정답은 없지만 시스템의 특성에 따라서 다음과 같이 정의한다.

  • 사용자에게 서비스를 제공하는 서비스 시스템 (웹,모바일등) : 가용성, 응답시간, 처리량

  • 스토리지 시스템(백업,저장 시스템): 가용성, 응답시간, 내구성

  • 빅데이터 분석 시스템 : 처리량, 전체 End-to-End 처리 시간

  • 머신러닝 시스템 : 서빙 응답시간, 학습 시간, 처리량, 가용성, 서빙 정확도

SLI 수집 및 표현

SLI 지표는 각종 모니터링 도구를 통해서 측정할 수 있고, 또는 로그 데이터에서 지표를 추출해 낼 수 있다. 예를 들어 정상 응답과 비정상 응답의 수를 카운트 하거나 또는 로그 메시지내에 있는 필드에서 값을 추출할 수 있다. 이렇게 추출된 값을 합하거나 평균 값을 내서 SLI를 정할 수 있는데, 평균값 보다는 값의 분포를 퍼센타일에 따른 분포를 사용하는것이 좋다. 아래 예제를 보자. 아래 예제는 웹 시스템의 응답 시간을 표현한 그래프이다.


50%,85%,95%,99% 응답 시간의 분포를 본것이다. 50%를 중간값이 이라고 생각하면 이 값이 일반적인 대표 값이 된다. 위의 그래프 값을 보면 20 ms 정도로 볼 수 있다. 느린 응답 시간, 즉 99%에 해당하는 응답 시간을 보면 5000ms (5초)가 나오는 것을 볼 수 있고 그래프 우측을 보면 응답 시간의 변화의 폭도 훨씬 큰것을 볼 수 있다.

실제 시스템에 문제가 되는 것은 느린값들 즉 99% 와 같은 구간에 속하는 응답시간이 문제가 되는데, 평균 값을 사용하게 되면, 느린 응답 시간이 희석화 되서 시스템의 안정성을 제대로 평가하기가 어렵다. 그래서 SRE에서는 퍼센트타일 기반의 SLI 분포도를 보고 90%,99%와 같이 문제가 되는 구간의 지표를 주요 SLI로 정해서 사용하는 것이 좋다.

SLI 지표의 표준화

SLI로 응답 시간이나 가용성 같은 지표를 사용하기로 정하면, 응답시간과 같은 SLI 지표는 여러 컴포넌트에 걸쳐서 측정해야 한다. 그런데 이러한 지표의 측정 단위가 표준화 되어 있지 않으면 혼선을 야기 하기 때문에, 표준을 정할 필요가 있다.


  • 수집 주기 : 지표를 뽑아내는 수집 주기를 10초 단위로 한다.

  • 수집 범위 : 수집 범위는 서비스 클러스터 단위로 한다.

  • 지표화 주기 : 수집은 10초 단위로 했지만, 지표화는 1분 단위로 해서 시각화 한다.

  • 어떤 호출들을 포함 할것인가? : HTTP GET 응답 시간만을 측정하고, 내부 호출은 측정하지 않는다.

  • 어떻게 데이터를 수집할것인가? : 모니터링 시스템을 통해서 수집한다.


이러한 SLI 표준은 여러 시스템에 적용될 수 있기 때문에 재 사용을 위해서 템플릿으로 만들어서 사용하는 것도 좋다.

SLO (Service Level Objective)

앞서 SLI를 통해서 시스템의 지표를 정의하는 방법을 이야기 하였다. 그러면 각 지표에 대한 목표 값은 어떻게 정의할까? 이를 SLO (Service Level Objective)라고 한다.


SLO = SLI + 목표값(Goal)


예를 들어 REST API의 응답시간을 SLI로 정했다면, SLO는 다음과 같이 정의할 수 있다.

“매주, 99% of REST API호출의 응답 시간은 100ms 이하여야한다.”

여기서 "매주,99% REST API 호출 응답시간"이 SLI가 되고, 응답시간 100ms가 목표값이 되서 위의 전체 문장이 SLO가 된다.

What user cares?

SLO를 정의할때 잘못하면, 서비스 제공자 관점에서 생각해서 SLO를 정의하기 쉬운데, SLO는 사용자 관점에서 서비스에 얼마나 영향을 주는 가의 관점에서 결정해야 한다.

시스템 관점에서 API 호출 응답시간이 80% 퍼센타일 구간에서 1초면 꽤 높은 것으로 느껴질 수 있지만, 모바일 서비스를 가정한다면, 모바일 서비스의 경우 지하철이나 차량 이동등으로 인해서, 모바일 네트워크 통신 속도 자체가 느리기 때문에, 80% 퍼센타일 구간에서  1초의 지연이면 사용자에게 체감되는 속도는 그렇게 느린편은 아니다. 이를 무리하게 맞추기 위해서 API호출 응답시간을 1초 이하로 내리는 행위는 적절하지 않다. (오버엔지니어링의 사례로 그 시간에 자동화나 다른 개발을 하는데 시간을 투자하는 것이 났다.)


또는 모바일 앱의 경우 통신망의 가용성이 99.99% 인데, 시스템의 가용성을 99.999%로 만든다 하더라도 통신망에서 더 많은 에러가 나기 때문에 쓸모 없는 SLO가 된다.


물론 많은 경우 사용자가 어떤 지표에 대해서 얼마 만큼의 기대값을 가지는지 알기 어려운 경우가 많다. 이런 경우에는 사용자의 기대치에 대해서 가설을 세워서 시작하고 측정하기 쉬운 항목부터 측정해가면서 나중에 실제로 유용한 SLO로 발전 시켜가는 접근 방법을 사용하도록 한다.


SLO는 반드시 사용자 관점에서 정의해야한다. SLO 관점에서 시스템에 문제가 없다 하더라도, Customer Support Team으로, 불만(성능/안정성)이 들어온다면, SLO 설정을 잘못했을 가능성에 대해서도 생각해봐야 한다. 이런 지표를 효과적으로 수집하기 위해서 Customer Support 항목에 성능/안정성 등에 대한 항목을 넣고 이를 모니터링 하는 것도 좋은 방법중의 하나이다.

좋은 SLO 란?

그러면 좋은 SLO의 정의란 무엇인가? SLO에서 설정하는 목표는 단순히 기술적인 목표가 아니라 그 비지니스가 추구하고자 하는 가치를 반영한 목표여야 한다. 그러면 좋은 SLO를 만들기 위한 조건을 보자

  • 단순할것
    좋은 SLO를 복잡하지 않고 단순해서 이해하기 쉬워야한다. SLO는 개발/운영 조직뿐만 아니라 영업 조직 및 고위 임원까지 모두 동의하고 사용하는 지표이기 때문에, 어려운 정의는 서로 이해하기가 어렵다.

  • 완벽한 값을 사용하지 말것
    완벽한 시스템을 존재할 수 없고 현실성이 없다. 예를 들어 100% 업타임인 무장애 시스템은 존재하지 않는다. 상식적으로 타당한 선에서 SLO를 정의하자

  • 되도록이면 적은 수의 SLO만 정의할것
    보통 시스템에는 하나의 SLO만을 사용하지 않는다. 여러개의 SLO 값을 지정해서 사용하는데, 많은 수의 SLO는 관리가 어렵고 실제로 제대로 사용되지도 않는다. 적은 수의 SLO를 사용하되, 사용되는 SLO는 비지니스 의사결정의 기준으로 사용될 수 있는 SLO만 사용하도록 한다. 의사결정에 사용되지 않는 SLO는 쓸모가 없는 SLO이다.

  • 지속적이고 점진적으로 SLO값을 발전 시킬것
    SLO의 값은 조직의 능력이나 비지니스의 상황에 따라서 지속적으로 조정해야 한다. SLO를 낮은 값에서 시작해서 점점 높은 수준의 값으로 발전 시키는 방법이 있고, 그 반대 방향도 있다.
    만약에 높은 값의 SLO를 목표값으로 시작했다가 개발/운영 조직이 이 목표를 맞추지 못했으니 SLO를 낮추자는 접근 방법은 그다지 좋지 못하다. SLO를 맞추지 못했을때  SLO를 계속해서 낮춰가는 나쁜 습관을 만들 수 있기 때문에 습관적으로 SLO 목표값을 점점 낮추게 되는 나쁜 결과를 나을 수 있다.
    그래서 SLO의 목표값은 낮은 값에서 시작해서 점차적으로 높은 값으로 바꿔 나가는 것이 바람직하다.

SLO에 대한 기대치 관리

SLO는 의사 결정의 기준이 되는 지표로 사용되기 때문에, SLO를 잘 이해하고 활용하는 것이 중요한데, 그중에서도 SLO에 관련된 이해 당사자들의 SLO에 대한 기대치를 잘 관리하는 것이 중요하다. 엄격하게 지켜야할 SLO이기는 하지만 너무 타이트하게 SLO를 잡으면, 반대로 다른 부작용이 발생할 수 있다.


  • SLO의 최소/최대 범위 지정

일반적으로 SLO는 "SLO<=목표값" 식으로 최대값만 설정하거나 또는 "최소값 <==SLO ⇐ 목표값" 형태로 설정을 한다." REST API 의 응답시간 ⇐ 300ms” 는 SLO로 크게 이상하지 않지만 “100ms <= REST API 응답시간 <==300ms”라고 정하는 것과 같이 최소값을 정하는 이유는 무엇일까?

100ms 이상의 지연을 허용한다는 점을 명시적으로 SLO에 적어놓게 되면, 개발자에게는 성능 향상의 목표가 생긴다. 300ms 이하를 유지 하면 되지만, 경우에 따라서 과하게 성능을 끌어 올리려고 몰두 하는 바람에, 정작 개발해야 하는 기능 개발을 하지 못할 수 있기 때문에, 오버 엔지니어링을 막는 차원에 SLO의 최소/최대 범위를 정하는 것도 좋은 방법이라고 할 수 있다.


  • 여유 값을 둘것

SLO를 정의할때, SLO를 외부에 공유해야 하는 경우 (예를 들어, 클라우드 서비스와 같은 경우) 외부에 공유하는 SLO와 내부에서 사용하는 SLO 둘을 나눠서 관리하고 내부용 SLO에 여유치를 둬서 외부용 SLO를 정의하면, 문제 발생시 어느정도의 여유 폭을 가질 수 있다.

물론 외부용 SLO가 사용자의 기대치에 미치지 못하는 수준이면 안되지만, 사용자가 인정할 수 있는 범위라면 어느정도의 여유 폭을 두는게 좋다.


  • Don’t overachieve

만약에 서비스에서 제공되는 성능이나 가용성이 SLO에 정의된 것보다 높다면 사용자는 당연히 현재의 성능에 익숙해진다. 예를 들어 시스템의 REST API 응답시간을 500ms 로 정해놨는데, 실제 시스템의 응답시간이 300ms 라면, 사용자는 이 300ms 응답시간에 익숙해질 것이고, 오히려 500ms 의 정상적인 응답시간이 나오면 느려진다고 느낄 수 있다.

  • 앞에 언급했던 SLO의 최소/최대 범위를 정하는 것도 이런 이유 중의 하나이다.


지금까지 SLO에 대해서 살펴보았다. SLO는 의사결정과 일의 우선 순위를 정하기 위한 매우 중요한 지표이다. 만약에 SLO가 의사 결정이나 우선 순위 결정에 사용되지 않는다면, 그 SLO와 넓게는 SRE 자체가 잘못된 것이다. SLO 는 SRE에서 그만큼 중요한 지표이기 때문에,  정의할 때 신중해야 하는데, SLO를 지정할때 너무 높은 수준의 SLO를 정하게 되면 SLO 수준에 맞는 성능과 안정성을 위해서 개발 자원을 투자해야 하고 그로 인해서 새로운 기능 개발이 늦어진다. 반대로 너무 낮은 수준의 SLO를 정하게 되면, 서비스 자체의 품질을 떨어 뜨린다.


앞에서도 계속  설명했듯이 SLO는 SRE에서 비즈니스 의사 결정과, 개발의 우선 순위를 결정하는등 아주 큰 의미를 가지는 중요한 지표이기 때문에, 현명하게 사용해야 한다.


SRE는 어떻게 일하는가?

조대협 (http://bcho.tistory.com)


이글은 앞의 글 "SRE/DEOPS의 개념과 SRE는 무엇을 하는가?" (https://bcho.tistory.com/1325) 와 연결된 글입니다.

How SRE does Devops?

그럼 SRE들은 이런한 일들을 어떤 방법으로 수행할까?

앞에서 SRE가 해야 하는 일에 대해서 설명하면서 각각에 대해서 일부를 언급했지만, 다시 SRE가 해야하는 일을 하기 위해서는 어떻게(How) 해야 하는지에 대해서 다시 정리해보자.

SRE는 앞에서 언급한 다섯가지 일을 하기 위해서 아래와 같이 다섯 가지 방법을 사용한다.


Reduce organizational silos

첫번째는 부서간 (개발과 운영)의 사일로(단절) 현상을 없애는 노력을 한다. 여러가지 방법이 있겠지만 앞에서도 언급한 Share ownership 기반으로, 시스템 안정성에 대한 오너쉽(주인인식)을 서로 공유한다. 앞에 자세한 설명을 하였기 때문에 기타 부연 설명은 생략하겠다.

Accept failure as normal

두번째는 장애에 대해서 민감하게 반응하지 않아야 하는데, 이를 위해서는 서로 비난 하지 않는 문화와 장애가 발생후에 이를 회고 하고 향후 대책을 수립하는 Postmortem  회고를 수행한다. 또한 시스템의 가용성에 대한 적절한 관리를 위해서 Error budget 의 개념을 도입하여 사용한다.

Implement gradual changes

변화 관리 특히 배포에 관련해서 큰 변경보다는 작은 변경이 배포하기도 편하고 장애가 났을때도 쉽게 롤백이 가능해서 MTTR을 줄일 수 있기 때문에, 점진적인 변경 방법을 쓴다. 카날리 배포나 롤링 업그레이드들이 이에 해당한다.


Leverage tooling and automation

시스템 운영을 자동화 함으로써 사람이 운영에 관여하면서 발생할 수 있는 오류를 최소화하고, 수동(메뉴얼) 작업을 줄여서 사람은 좀 더 가치가 있는 일에 집중할 수 있도록 한다.

이렇게 하려면 수동작업의 양을 측정하고, 수동작업의 양을 적절한 수준으로 조절해야 하는데 이를 위해서 Toil 이라는 개념을 사용할 수 있다. 이 Toil의 개념은 나중에 다시 자세하게 설명하도록 한다.

Measure everything

그리고 SRE는 의사결정을 데이타에 기반으로하기 때문에, 어떤 값들을 어떻게 메트릭으로 표현할것인지를 정하고, 시스템 지표뿐만 아니라, 수동 작업 시간, 장애 시간등 모든 것을 측정해서 데이타화 한다.

Metric matters

앞에 까지 SRE가 무엇이고,  SRE가 하는일은 무엇이며, 어떻게 그런일을 하는지에 대해서 알아보았다. SRE 프랙티스 에서는 의사 결정을 데이터에 따라 하기 때문에, 지표를 정의하는 것이 중요하다. 그러면 SRE 에서는 어떤 지표를 어떻게 사용하는지에 대해서 알아보자. 다음글 https://bcho.tistory.com/1328



AutoEncoder vs Variant AutoEncoder

빅데이타/머신러닝 | 2019.05.10 22:30 | Posted by 조대협

AutoEncoder vs Variant AutoEncoder


조대협 (http://bcho.tistory.com)


Abnormal

AutoEncoder는 입력값을 기반으로 여기서 특징을 뽑아내고, 뽑아낸 특징으로 다시 원본을 재생하는 네트워크이다. 이미지 합성이나 압축, Abnormal Detection 등 여러 유스케이스에 사용이 될 수 있지만, 특히 추출된 특징 (latent coding)은 데이타의 특징을 이해하는데도 유용하게 사용될 수 있다.

이 글에서는 AutoEncoder와 요금 각광 받는 VAE (Variant Auto Encoder) 의 차이를 알아보고 특히 latent coding의 값이 어떻게 다르게 표현되며, 어떤 의미를 가지는지에 대해서 이해한다.


일반 오토 인코더의 모양은 다음과 같다.



<그림 AutoEncoder의 구조>

출처 : https://excelsior-cjh.tistory.com/187


입력값 x로 부터 추출된 특징을 latent code z 라고 하는데,

이를 조금 더 이해하기 쉽게 표현해보면 다음과 같다.


<그림. AutoEncode의 latent code z 표현 방식 비교>

출처 : https://www.jeremyjordan.me/variational-autoencoders/


위의 그림은 오토 인코더를 이용해서 특징을 추출한 결과로 6개의 특징을 추출하였는데, 추출된 특징 latent code z는 특정 숫자값을 갖는다.

VAE는 이 latent code z의 값을 하나의 숫자로 나타내는 것이 아니라 가우시안 확률 분포에 기반한 확률값 (값의 범위)로 나타낸다.

아래 그림은 오토 인코더와 VAE에서 latent code z를 표현하는 방법을 설명한 것인데



<그림. AutoEncoder와 VAE의 latent code z 표현 방식 비교>

출처 : https://www.jeremyjordan.me/variational-autoencoders/


얼굴의 특징을 추출했다고 했을때 좌측은 오토인코더로 하나의 숫자로 특징을 표현하였고, 우측은 가우시안 확률 분포로 특징을 표현하였다. (확률값으로) 그래서 네트워크의 모양을 보면 다음과 같다.



<그림. VAE 구조>

출처 : https://excelsior-cjh.tistory.com/187


AutoEncoder latent coding 값이 single value z라면, VAE는 latent coding z를 가우시안 분포로 나타내기 위해서 평균과, 분산값으로 나타낸다.


AutoEncoder와 VAE의 latent space를 시각화 해보면 다음과 같은 차이를 발견할 수 있다. 아래 그림은 4 dimension latent space를 PCA(차원감소기법)을 통해서 2차원으로 변환한후 시각화 한 내용이다.


<그림. AE vs VAE의 latent space 비교>

출처 : https://thilospinner.com/towards-an-interpretable-latent-space/


MNIST에 대한 latent space인데, 각 점의 색깔은 0~9 숫자(이미지 라벨)을 표현한다.

좌측의 AE의 latent space는 군집이 넓게 퍼져있고, 중심점을 기반으로 잘 뭉치지 않는데 반해서 VAE는 중심점을 기반으로 좀더 컴팩트하게 잘 뭉쳐지는 것을 볼 수 있다.  그래서 원본 데이타를 재생하는데, AE에 비해 장점이 많고, latent space를 통해서 데이타의 군집을 파악하는데도 군집 강도가 높기 때문에 데이타의 특징을 파악하는데 좀더 유리하다.



참고 :


Site Reliability Engineering(SRE)

#1 SRE/DEVOPS의 개념

조대협 (http://bcho.tistory.com)

배경

Devops는 운영팀과 개발팀을 하나의 팀으로 묶어놓고 전체적인 개발 사이클을 빠르게 하고자 하는 조직 구조이자 문화이다.


이 Devops라는 컨셉이 소개된지는 오래되었지만, Devops의 개념 자체는 명확하지만 이 Devops를 어떻게 실전에 적용할것인 가는 여전히 어려운 문제였다.(예전에 정리한 Devops에 대한 개념들 1 , 2)  예전 직장들에 있을때 Devops의 개념이 소개되었고 좋은 개념이라는 것은 이해하고 있었지만, 여전히 운영팀은 필요하였고, 그 역할이 크게 바뀌지 않았다. 심지어 Devops를 하는 기업들도 보면 기존 개발팀/운영팀이 있는데, 새롭게 Devops팀을 만들거나 또는 운영팀 간판을 Devops팀으로만 바꾸는 웃지 못할 결과들이 있었다.

나중에 위메프에서 CTO를 하셨던 김요섭님의 강의를 들을 수 있는 기회가 있었는데, 그때 구글이나 넷플릭스와 같은 사례에 대해 들을 수 있었지만, 그에 대한 디테일한 프렉틱스는 찾을 수 가 없었다.


여러 고민을 하고 있다가 구글에 입사한 후에, 구글의 Devops에 대해서 알게되었고, 여러 자료를 찾아서 공부하고 나니 어느정도 이해가 되서, 개념을 정리해놓는다.

Devops와 SRE

일반적으로 개발팀은 주어진 시간내에 새로운 기능을 내기 위해서 개발 속도에 무게를 두고, 운영팀의 경우에는 시스템 안정성에 무게를 둔다. 그래서 개발팀이 무리하게 기능을 배포하게 되면 장애로 이어지고, 이러한 장애로 인하여 서로를 욕하는 상황이 만들어져서 팀이 서로 멀어지게된다. 그래서 Devops는 이러한 두팀을 한팀에 묶어 놓고 운영하는 문화이자 일종의 운영 철학이다.

그런데 그러면 운영팀과 개발팀을 묶어놓으면 운영을 하던 사람들은 무엇을 하는가? 요즘은 클라우드가 발전해서 왠만한 부분은 개발자들이 직접 배포하고 운영도 할 수 있지만 시스템이 커지면 여전히 운영의 역할은 필요하다. 그렇다면 Devops 엔지니어라고 이름을 바꾼 Devops 엔지니어들이 하는 일은 무엇인가?


그 해답을 구글의 SRE(Site Reliability Engineering)에서 찾을 수 있었는데, 개발자가 셀프 서비스로 운영을 하려면 그 플랫폼이 자동화되어 있어야 한다. 애플리케이션을  빌드하고 유연하게 배포하고, 이를 모니터링할 수 있는 플랫폼이 필요한데, SRE의 역할은 이러한 플랫폼을 개발하고, 이 플랫폼 위에서 개발자들이 스스로 배포,운영을 하는 것이 목표이다. 물론 완벽한 셀프 서비스는 불가능하다. 여전히 큰 장애 처리나 배포등은 SRE 엔지니어가 관여하지만 많은 부분을 개발팀이 스스로 할 수 있도록 점점 그 비중을 줄여 나간다.


그러면 구글 버전의 Devops인 SRE는 서로 다른것인가? 그 관계는 어떻게 되는가? 이 질문에 대해서는 다음 하나의 문장으로 정리할 수 있다.

“ class SRE implements Devops

Devops가 개발과 운영의 사일로(분단) 현상을 해결하기 위한 방법론이자 하나의 조직문화에 대한 방향성이다. 그렇다면 SRE는 구글이 Devops에 적용하기 위한 구체적인 프렉틱스(실사례)와 가이드로 생각하면 된다. 구글도 다른 기업들과 마찬가지로  회사의 성장과 더블어 2000 년도 즈음에 개발자들이 속도에 무게를 두고 운영팀이 안정성에 무게를 둬서 발생하는 문제에 부딪혔고, 이 문제를 풀고자 하는 시도를 하였는데 이것이 바로 SRE (Site Reliability Engineering)이다. SRE는 크게 3가지 방향으로 이런 문제를 풀려고 했는데,

  • 첫번째는, 가용성에 대한 명확한 정의

  • 두번째는, 가용성 목표 정의

  • 세번째는, 장애 발생에 대한 계획

구글 팀은 이러한 원칙을 개발자/운영자뿐만 아니라 임원들까지 동의를 하였는데, 좀 더 구체적으로 이야기를 하면, 이러한 원칙에 따라 장애에 대한 책임을 모두 공유한다는 컨셉이다. 즉 장애가 나도 특정 사람이나 팀을 지칭해서 비난 하는게 아니라, 공동책임으로 규정하고 다시 장애가 나지 않을 수 있는 방법을 찾는 것이다.

위의 3가지 원칙에 따라서, 가용성을 측정을 위해서 어떤 지표를 사용할지를 명확히 정하고 두번째로는 그 지표에 어느 수준까지 허용을 할것인지를 정해서 그에 따른 의사결정은 하는 구조이다.

SRE는 단순히 구글의 운영팀을 지칭하는 것이 아니라, 문화와 운영 프로세스 팀 구조등 모든 개념을 포함한 포괄적인 개념이다.

What does an SRE Engineer do?

그러면 SRE에서 SRE엔지니어가 하는 일은 무엇일까? 아래 그림과 같이 크게 다섯까지 일을 한다.



<출처. 구글 넥스트 2018 발표 자료>

Metric & Monitoring

첫번째는 모니터링 지표를 정의하고, 이 지표를 모니터링 시스템을 올리는 일이다. 뒤에 설명하겠지만 구글에서는 서비스에 대한 지표를 SLI (Service Level Indictor)라는 것을 정하고, 각 지표에 대한 안정성 목표를 SLO (Service Level Objective)로 정해서 관리한다.

이러한 메트릭은 시스템을 운영하는 사람과 기타 여러 이해 당사자들에게 시스템의 상태를 보여줄 수 있도록 대쉬 보드 형태로 시각화 되어 제공된다.

그리고 마지막으로 할일은 이런 지표들을 분석해서 인사이트를 찾아내는 일이다. 시스템이 안정적인 상황과 또는 장애가 나는 지표는 무엇인지 왜인지? 그리고 이러한 지표를 어떻게 개선할 수 있는지를 고민한다. 기본적으로 SRE에서 가장 중요한점중 하나는 모든것을 데이타화하고, 의사결정을 데이타를 기반으로 한다.

Capacity Planning

두번째는 용량 계획인데, 시스템을 운영하는데 필요한 충분한 하드웨어 리소스(서버, CPU,메모리,디스크,네트워크 등)을 확보하는 작업이다. 비지니스 성장에 의한 일반적인 증설뿐만 아니라 이벤트나 마케팅 행사, 새로운 제품 출시등으로 인한 비정상적인 (스파이크성등) 리소스 요청에 대해서도 유연하게 대응할 수 있어야 한다.

시스템의 자원이란 시스템이 필요한 용량(LOAD), 확보된 리소스 용량 그리고 그 위에서 동작하는 소프트웨어의 최적화, 이 3가지에 대한 함수 관계이다.

즉 필요한 용량에 따라 적절하게 시스템 자원을 확보하는 것뿐만 아니라, 그 위에서 동작하는 소프트웨어 대한 성능 튜닝 역시 중요하다는 이야기다. 소프트웨어의 품질은 필요한 자원을 최소화하여 시스템 용량을 효율적으로 쓰게 해주기도 하지만 한편으로는 안정성을 제공해서 시스템 전체에 대한 안정성에 영향을 준다.

그래서 SRE 엔지니어는 자원 활용의 효율성 측면에서 소프트웨어의 성능을 그리고 안정성 측면에서 소프트웨어의 안정성을 함께 볼 수 있어야 한다.

Change Management

세번째는 한글로 해석하자면 변경 관리라고 해석할 수 있는데, 쉽게 이야기 하면 소프트웨어 배포/업데이트 영역이라고 보면 된다. (물론 설정 변경이나 인프라 구조 변경도 포함이 되지만)

시스템 장애의 원인은 대략 70%가 시스템에 변경을 주는 경우에 발생한다. 그만큼 시스템의 안정성에는 변경 관리가 중요하다는 이야기인데, 이러한 에러의 원인은 대부분 사람이 프로세스에 관여했을때 일어나기 때문에, 되도록이면 사람을 프로세스에서 제외하고 자동화하는 방향으로 개선 작업이 진행된다.

이러한 자동화의 베스트프래틱스는 다음과 같이 3가지 정도가 된다.

  • 점진적인 배포와 변경 (카날리 배포나 롤링 업데이트와 같은 방법)

  • 배포시 장애가 발생하였을 경우 빠르고 정확하게 해당 문제를 찾아낼 수 있도록 할것

  • 마지막으로 문제가 발생하였을때 빠르게 롤백할 수 잇을것

자동화는 전체 릴리스 프로세스 중에 일부분일 뿐이다. 잠재적인 장애를 막기 위해서는 코드 관리, 버전 컨트롤, 테스트 등 전체 릴리즈 프로세스를 제대로 정의 하는 것이 중요하다.

Emergency Response

네번째는 장애 처리이다. 시스템 안정성이란 MTTF(Mean Time to failure:장애가 발생하지 않고 얼마나 오랫동안 시스템이 정상 작동했는가? 일종의 건설현장의 "무사고 연속 몇일"과 같은 개념)와 MTTR(Mean time to recover:장애가 났을때 복구 시간)의 복합 함수와 같은 개념이다.

이 중에서 장애처리에 있어서 중요한 변수는 MTTR인데, 장애 시스템을 가급적 빠르게 정상화해서 MTTR을 줄이는게 목표중의 하나이다.

장애 복구 단계에서 사람이 직접 매뉴얼로 복구를 하게 되면 일반적으로 장애 복구 시간이 더 많이 걸린다. 사람이 컨트롤을 하되 가급적이면 각 단계는 자동화 되는게 좋으며, 사람이 해야 하는 일은 되도록이면 메뉴얼화 되어 있는 것이 좋다. 이것을 “Playbook”이라고 부르는데, 물론 수퍼엔지니어가 있는 경우에 수퍼엔지니어가 기가막히게 시스템 콘솔에 붙어서 장애를 해결할 수 있겠지만 대부분의 엔지니어가 수퍼엔지니어가 아니기 때문에, “Playbook” 기반으로 장애 처리를 할 경우 “Playbook”이 없는 경우에 비해 3배이상 MTTR이 낮다는 게 통계이다.

그리고 "Playbook”이 있다고 하더라도, 엔지니어들 마다 기술 수준이나 숙련도가 다르기 때문에, "Playbook”에 따른 장애 복구 모의 훈련을 지속적으로 해서 프로세스에 익숙해지도록 해야한다.

Culture

마지막으로 문화인데, SRE 엔지니어는 앞에서 설명한 운영에 필요한  작업뿐만 아니라 SRE 문화를 전반적으로 만들고 지켜나가는 작업을 해야 한다. 물론 혼자서는 아니라 전체 조직의 동의와 지원이 필요하고, 특히 경영진으로 부터의 동의와 신뢰가 없다면 절대로 성공할 수 없다.

나중에 설명하겠지만 SRE에는 Error budget 이라는 개념이 있는데, 모든 사람(경영층 포함)해서 이 Error budget에 대해서 동의를 하고 시작한다. Error budget은 특정 시스템이 일정 시간동안 허용되는 장애 시간이다. 예를 들어 일년에 1시간 장애가 허용 된다면 이 시스템의 Error budget는 1시간이고, 장애가 날때 마다 장애시간만큼 그 시간을 Error budget에서 차감한 후에, Error budget이 0이 되면 더 이상 신규 기능을 배포하지 않고 시스템 안정성을 올리는 데 개발의 초점을 맞춘다.

그런데 비지니스 조직에서 신규 기능 출시에 포커스하고 Error budget이 0이 되었는데도 신규 기능 릴리즈를 밀어붙이면 어떻게 될까? 아니면 시스템 운영 조직장이 Error budget이 10시간이나 남았는데도 불구하고 10분 장애가 났는데, 전체 기능 개발을 멈추고 시스템을 장애에 잘 견디게 고도화하라고 하면 어떻게 될까? 이러한 이유로 전체 조직이 SRE 원칙에 동의해야 하고,장애가 났을 때도 서로 욕하지 말고 책임을 나눠 가지는 문화가 필요하다.

이런 문화를 만들기 위해서는 크게 3가지 가이드가 있는데 다음과 같다.

  • 데이타에 기반한 합리적인 의사결정
    모든 의사결정은 데이타 기반으로 되어야 한다. 앞에서도 설명했듯이 이를 지키기 위해서는 임원이나 부서에 상관없이 이 원칙에 동의해야 하고, 이것이 실천되지 않는다면 사실상 SRE를 적용한다는 것은 의미가 없다. 많은 기업들이 모니터링 시스템을 올려서 대쉬 보드를 만드는 것을 봤지만 그건 운영팀만을 위한것이었고, SRE를 하겠다고 표방한 기업이나 팀들 역시 대표가 지시해서. 또는 임원이 지시해서 라는 말 한마디에 모든 의사결정이 무너지는 모습을 봤을 때, 이 원칙을 지키도록 고위 임원 부터 동의하지 않는 다면 SRE 도입 자체가 의미가 없다.

  • 서로 비난하지 않고, 장애 원인을 분석하고 이를 예방하는 포스트포턴 문화
    장애는 여러가지 원인에서 오지만, 그 장애 상황과 사람을 욕해봐야 의미가 없다. 장애는 이미 발생해버린 결과이고, 그 장애의 원인을 잘 분석해서 다음에 그 장애가 발생하지 않도록 하는  것이 중요하다. 보통 장애가 나고나서 회고를 하면 다음에는 프로세스를 개선한다던가. 주의하겠다는 식으로 마무리가 되는 경우가 많은데. 사람이 실수를 하도록 만든 프로세스와 시스템이 잘못된것이다. 사람은 고칠 수 없지만 시스템과 프로세스는 개선할 수 있다. 그리고 모든 개선은 문서화되어야 하고 가능한것들은 앞에서 언급한 Playbook에 반영되어야 한다.

  • 책임을 나눠가지는 문화
    그리고 장애에 대해 책임을 나눠 가지는 문화가 있어야 한다. 예를 들어 장애란 개발팀 입장에서 장애는 코드의 품질이 떨어져씩 때문에 장애가 일어난 것이고, 운영팀입장에서는 운영이 고도화 되지 않아씩 때문이며, 비지니스쪽에서는 무리하게 일정을 잡았기 때문이다.  책임을 나눠 가지는 문화는 누군가를 욕하지 않기 위해서라기 보다는 나의 책임으로 일어난 장애이기 때문에, 장애를 없애기 위한 노력도 나의 역할이 되고 동기가 된다.


지금까지 간단하게 나마 SRE의 개념과 SRE엔지니어가 무슨 일을 하는지에 대해서 설명하였다. 다음은 그러면 SRE 엔지니어들이 어떻게 이런일을 해나갈 수 있는지 How(방법)에 대해서 설명하도록 하겠다. 다음글 https://bcho.tistory.com/1325




Reference


SRE는 구글의 Devops의 프랙티스 로 구글의 서비스에 대한 배경과 철학을 읽을 수 있다.

SRE의 기본 사상중의 하나는 서비스의 안정성이 완벽할 수 없으며, (아니 완벽하지 않게 만들며) 장애를 허용하는 모델이다.

고 가용/고 성능 시스템을 만들기 위해서는 그만큼 많은 개발에 대한 노력이 소요되는데, 이로 인해서 기능 개발에 대한 속도가 느려지기 때문에, 사용자가 납득할만한 수준의 가용성을 제공하되 개발의 속도를 유지하는 철학이다.

배경을 살펴보면 구글은 모바일을 기반으로 한 B2C 서비스를 주력으로 하기 때문에, 서비스가 99.999%의 가용성을 제공하더라도, 스마트폰과 통신망 자체가 그정도의 안정성을 제공하지 않기 때문에, 백앤드 서비스가 높은 가용성을 제공하더라도 사용자가 느끼는 가용성은 그 정도 수준이 되지 않는다. 그렇기 때문에 장애를 허용하면서 적정 수준의 가용성을 가지는 구조로 Devops 운영 모델이 정립되었다고 생각한다. 이러한 내용은 구글 엔지니어들이 저술한 SRE 서적에서도 찾아볼 수 있다.


이러한 사상을 반영하여 실제 운영환경에 적용하기 위한 구체적인 가이드와 프랙틱스가 SRE이다. 예를 들어 운영에 측정 가능한 지표를 SLI (Service Level Indicator)로 정의하고, SLI에 대한 목표를 SLO로 정해서 수치화하고 서비스 운영의 지표로 삼는 모델등 다양한 프랙틱스가 있는데, 이 부분은 차차 살펴봐야 겠다.

Serveless를 위한 오픈소스 KNative #2 Eventing


조대협 (http://bcho.tistory.com)


knative의 다른 모듈로써는 비동기 메세지 처리를 위한 eventing 이라는 모듈이 있다. 카프카나, 구글 클라우드 Pub/Sub, AWS SQS와 같은 큐에서 메시지를 받거나 또는 Cron과 같은 타이머에서 이벤트가 발생하면 이를 받아서 처리할 수 있는 비동기 메커니즘을 제공하는 모듈이라고 보면 된다.


메시지 큐나 cron 과 같이 이벤트를 발생 시키는 자원들은 knative에 event source 라는 Custom Resource로 등록이 되고, 등록된 event source는 이벤트가 발생되면 지정된 knative 서비스로 이벤트 메시지를 HTTP로 전송한다. 이때 이벤트를 받는 knative 서비스는 앞에서 언급한 knative serving의 서비스이다. 이때 이벤트에 대한 스펙은 CNCF Serverless WG 에서 정의한 CloudEvents 스펙에 기반한다.

Hello Eventing

자세하게 Eventing에 대해서 알아보기 전에 간단한 예제를 살펴보자. 예제는 knative.dev의 cronjob  예제이다.  Crontab으로 이벤트를 생성하면, event-display 라는 서비스에서 이 이벤트를 받아서 이벤트의 내용을 간략하게 로그로 출력하는 예제이다.


먼저 이벤트를 읽어드릴 event-display 서비스를 배포하자. 해당 서비스는 HTTP post로 받은 이벤트의 내용을 log로 출력해주는 코드로 이벤트의 포맷은 앞에서 설명한 CloudEvent의 포맷을 따른다.

Go 로 구현된 코드이며, 코드 원본은 여기에 있다.

 해당 컨테이너를 배포하기 위해서 아래와 같이 service.yaml 파일을 만들고, kubectl apply -f service.yaml 을 이용해서 배포하면, crontab 에서 이벤트를 받는 serving 인스턴스가 준비된다.

apiVersion: serving.knative.dev/v1alpha1

kind: Service

metadata:

 name: event-display

spec:

 runLatest:

   configuration:

     revisionTemplate:

       spec:

         container:

           image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/event_display

<그림. Event consumer용 knative 서비스 배포>


다음 Crontab event 소스를 아래와 같이 yaml로 정의한다.


apiVersion: sources.eventing.knative.dev/v1alpha1

kind: CronJobSource

metadata:

 name: test-cronjob-source

spec:

 schedule: "*/2 * * * *"

 data: '{"message": "Hello world!"}'

 sink:

   apiVersion: serving.knative.dev/v1alpha1

   kind: Service

   name: event-display

<그림. Crontab event source 정의>


spec>schedule 부분에 이벤트 주기에 대한 설정을 crontab 포맷을 따라서 하고, data 부분에 cron 이벤트가 발생할때 마다 보낼 데이타를 정의한다.

데이타를 보낼 목적지는 sink 부분에 지정하는데, kind에 타입을 정의하고 (여기서는 knative의 Service로 지정) 그리고 service 의 이름을 name에 정의한다. 앞에서 knative serving 서비스를 event-display로 지정하였기 때문에, 서비스명을 event-display로 정의한다.

yaml 파일 설정이 끝났으면 kubectl apply -f  명령을 이용해서 이벤트 소스를 등록하고, 동작을 하는지 확인해보도록 하자.


%kubectl logs -l serving.knative.dev/service=event-display -c user-container --since=10m


명령을 이용하면 앞에서 배포한 event-display 서비스의 로그를 볼 수 있는데, 결과를 보면 다음과 같다.



Data 부분에서 crontab 이벤트 소스에서 보내온 “message”:”Hello world!” 문자열이 도착한것을 확인할 수 있다.

Eventing detail

이벤트는 앞의 예제에서 본것과 같이 이벤트 소스에서 바로 Knative 서빙에서 받아서 처리하는 가장 기본적인 비동기 이벤트 처리 패턴이다.


Broker & Trigger

이러한 패턴이외에도 좀 더 다양한 패턴 구현이 가능한데, 두번째가 Broker와 Trigger이다. Broker는 이벤트 소스로 부터 메시지를 받아서 저장하는 버킷 역할을 하고, Broker에는 Trigger를 달 수 있는데, Trigger에는 메시지 조건을 넣어서, 특정 메시지 패턴만 서비스로 보낼 수 있다. 위의 패턴에서 필터를 추가한 패턴으로 보면 된다.



이해를 돕기 위해서 예제를 보자. 다음은 knative.dev 공식 사이트에 나와 있는 예제중에, Google Cloud Pub/Sub Source를 Broker로 연동하는 예제이다.


# Replace the following before applying this file:

#   MY_GCP_PROJECT: Replace with the GCP Project's ID.


apiVersion: sources.eventing.knative.dev/v1alpha1

kind: GcpPubSubSource

metadata:

 name: testing-source

spec:

 gcpCredsSecret:  # A secret in the knative-sources namespace

   name: google-cloud-key

   key: key.json

 googleCloudProject: MY_GCP_PROJECT  # Replace this

 topic: testing

 sink:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Broker

   name: default

<그림. github-pubsub-source.yaml>


위의 코드는 GCP Pub/Sub Source를 등록하는 부분인데, sink 부분은 이 소스에서 오는 메시지를 어디로 보낼지를 정하는 부분이다. 위에 보면 Broker로 보내는것을 볼 수 있다. Broker는 Default Broker로 보낸다.


다음은 Broker에서 받은 메시지를 Trigger 조건에 따라서 Knative Serving 서비스로 보내는 설정이다.


apiVersion: serving.knative.dev/v1alpha1

kind: Service

metadata:

 name: event-display

spec:

 template:

   spec:

     containers:

     - # This corresponds to

       # https://github.com/knative/eventing-sources/blob/release-0.5/cmd/event_display/main.go           

       image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/event_display@sha256:bf45b3eb1e7fc4cb63d6a5a6416cf696295484a7662e0cf9ccdf5c080542c21d


---


# The GcpPubSubSource's output goes to the default Broker. This Trigger subscribes to events in the

# default Broker.


apiVersion: eventing.knative.dev/v1alpha1

kind: Trigger

metadata:

 name: gcppubsub-source-sample

spec:

 subscriber:

   ref:

     apiVersion: serving.knative.dev/v1alpha1

     kind: Service

     name: event-display


< 그림. Trigger와 이벤트 메시지를 수신하는 Service를 정의한 부분>


서비스는 event-display라는 서비스를 정의하였고, 그 아래 Trigger 부분을 보면 gcppubsub-source-sample 이라는 이름으로 Trigger를 정의하였다. Broker 명을 정의하지 않으면 이 Trigger는 default broker에 적용된다. 별다른 조건이 없기 때문에, Broker의 모든 메시지를 대상 서비스인 event-display로 전달한다.

Channel & subscription

다음 개념은 Channel과 subscription 이라는 개념인데, Channel을 메시지를 저장 후에, Channel에 저장된 메시지는 메시지를 수신하는 Subscription을 통해서 다른 Channel로 포워딩 되거나 또는 Service로 전달 될 수 있다.



<그림. Channel과 Subscription 개념도>


앞에서 Channel에서는 메시지를 저장한다고 했는데, 그러면 저장할 장소가 필요하다. 저장할 장소는 설정으로 다양한 메시지 저장소를 사용할 수 있는데, 현재 메모리, Apache Kafka 또는 NATS Streaming을 지원한다.


간단한 예제를 살펴보자 예제는 이 문서를 참고하였다

먼저 아래 설정을 보자


apiVersion: sources.eventing.knative.dev/v1alpha1

kind: GcpPubSubSource

metadata:

 name: testing-source

spec:

 gcpCredsSecret:  # A secret in the knative-sources namespace

   name: google-cloud-key

   key: key.json

 googleCloudProject: knative-atamel  # Replace this

 topic: testing

 sink:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Channel

   name: pubsub-test



< 그림. GCPPubSub Event Source 정의한 코드>


위 설정은 GCP Pub/Sub을 Event source로 등록하는 부분이다. 이벤트 소스로 등록 한후에, 이벤트를 sink 부분에서 pubsub-test라는 Channel로 전달하도록 하였다.

다음 아래는 Channel을 정의한 부분인데, pubsub-test 라는 이름으로 Channel을 정의하고 "provisioner” 부분에, 메시지 저장소를 "in-memory-channel” 로 지정해서 메모리에 메시지를 저장하도록 하였다.

apiVersion: eventing.knative.dev/v1alpha1

kind: Channel

metadata:

 name: pubsub-test

spec:

 provisioner:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: ClusterChannelProvisioner

   name: in-memory-channel

< 그림. Channel 정의한 코드>



apiVersion: serving.knative.dev/v1alpha1

kind: Service

metadata:

 name: message-dumper-csharp

spec:

 runLatest:

   configuration:

     revisionTemplate:

       spec:

         container:

           # Replace {username} with your actual DockerHub

           image: docker.io/{username}/message-dumper-csharp:v1

---

apiVersion: eventing.knative.dev/v1alpha1

kind: Subscription

metadata:

 name: gcppubsub-source-sample-csharp

spec:

 channel:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Channel

   name: pubsub-test

 subscriber:

   ref:

     apiVersion: serving.knative.dev/v1alpha1

     kind: Service

     name: message-dumper-csharp

< 그림. Serving과 subscription을 정의 코드>


Channel에 저장된 메시지를 다른 Channel로 보내거나 또는 Service로 보내려면 Subscription을 거쳐야 한다. 위에서 gcppubsub-source-sample-charp이라는 subscription을 정의하였고, 이 subscription이 연결되는 Channel은 spec > channel 부분에 아래와 같이 정의 하였다.


aspec:

 channel:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Channel

   name: pubsub-test

< 그림. 위의 Subscription 정의에서 Channel 정의 부분>


그리고 그 채널에서 받은 메시지를 subscriber > ref 부분에서 아래와 같이 message-dumper-charp이라는 서비스로 포워딩 하도록 하였다.

 subscriber:

   ref:

     apiVersion: serving.knative.dev/v1alpha1

     kind: Service

     name: message-dumper-csharp

< 그림.위의 Subscription 정의에서 Service 정의 부분>


전체적으로 Eventing 모듈을 이해하는데 시간이 많이 걸렸는데, Eventing 모듈은 Serving 모듈에 비해서 예제가 적고, 공식 문서에 아직 설명이 부족하다. 예를 들어서 소스 → 서빙으로 메시지를 보낼때 스케일링할 경우 문제가 없는지. Channel → subscription 으로 메시지를 보낼때 Trigger를 사용할 수 있는지 등 정보가 아직 부족해서 자세한 분석이 어려웠다. Knative는 현재 0.5 버전으로 버전이고, Event Source 들도 아직 개발 단계가 아니라 PoC (Proof Of Concept : 기술적으로 가능한지 테스트를 하는 단계) 단계 이기 때문에 제대로 사용하기에는 시간이 더 걸릴 듯 하다.

Serveless를 위한 오픈소스 KNative

조대협(http://bcho.tistory.com)

배경

근래에 들어서 컨테이너를 사용한 워크로드 관리는 쿠버네티스 de-facto 표준이 되어가고 있는데, 쿠버네티스 자체가 안정되어가고 있지만, 이를 현업에 적용하기 위해서는 아직까지 여러가지 챌린지가 있다.

컨테이너 기반의 쿠버네티스 서비스가 지향하는 바는, 셀프서비스 기반의 데브옵스 모델로 인프라와 이를 자동화하는 플랫폼을 인프라엔지니어가 개발하여 개발팀에 제공하고, 개발팀은 개발과 배포/운영을 스스로 하는 모델이다.

그런데 예를 들어 간단한 무상태(stateless) 웹서비스를 하나 구축한다 하더라도 Deployment,Ingress,Service 등의 쿠버네티스 리소스를 정의해서 배포해야 하고, 여기에 오토 스케일링이나, 리소스 (CPU,메모리)등의 설정을 따로 해줘야 한다. 그런데 이런 설정을 일일이 다 하기에는 일반 개발자들에게 부담이 된다. 또한 A/B 테스팅이나 카날리 배포등은 쿠버네티스 자체로 지원이 되지 않고 스피니커(spinnaker)등의 다른 솔루션을 부가해서 써야 하는데, 이런 모델은 컨테이너 기반의 셀프 서비스와는 거리가 멀어진다.

서버쪽에 복잡한 설정 없이 무상태 웹서비스나 간단한 이벤트 컨슈밍 서비스등을 구축하는 방법으로는 서버리스 서비스들이 있는다. 아마존 클라우드의 람다(Lambda)나, 구글 클라우드의 펑션(Function)등이 이에 해당한다. 그런데 이러한 서버리스 서비스들은 특정 클라우드 플랫폼에 의존성을 가지고 있는데, 이러한 문제를 해결하기 위해서 나온 오픈소스 서버리스 솔루션이 Knative 이다.

Knative

Knative는 구글의 주도하는 오픈소스 기반의 서버리스 솔루션으로 쿠버네티스 위에서 기동이 된다. 그래서 특정 클라우드 종속성이 없을뿐만 아니라 On-Prem에서도 설치가 가능하다. 지원되는 인프라 목록은 여기에 있는데, 레드헷 오픈 시프트, 피보탈, IBM 과 같은 On-Prem 쿠버네티스뿐만 아니라, 구글, Azure, IBM 클라우드등 다양한 클라우드를 지원한다.


Knative는 스테이트리스 웹서비스뿐만 아니라, 큐에서 이벤트를 받아서 처리하는 이벤트 핸들링을 위한 서버리스 모델을 지원하고, 거기에 더불어 컨테이너를 빌딩할 수 있는 빌드 기능을 제공한다. 그러면 각각을 살펴보자

Serving

서빙은 무상태 웹서비스를 구축하기 위한 프레임웍으로 간단하게 웹서비스 컨테이너만 배포하면, 로드밸런서의 배치, 오토 스케일링, 복잡한 배포 (롤링/카날리)등을 지원하고, 서비스 매쉬 솔루션인 istio와 통합을 통해서 다양한 모니터링을 제공한다.

Hello Serving

일단 간단한 예제를 보자. 아래는 미리 빌드된 간단한 웹서비스 컨테이너를 배포하는 YAML 스크립트이다.


apiVersion: serving.knative.dev/v1alpha1 # Current version of Knative

kind: Service

metadata:

 name: helloworld-go # The name of the app

 namespace: default # The namespace the app will use

spec:

 runLatest:

   configuration:

     revisionTemplate:

       spec:

         container:

           image: gcr.io/knative-samples/helloworld-go # The URL to the image of the app

           env:

             - name: TARGET # The environment variable printed out by the sample app

               value: "Go Sample v1"

<그림. service.yaml>


kind에 Service로 정의되었고, 서빙을 하는 컨테이너는 container>image에 container image URL이 정의되어 있다. 이 이미지를 이용해서 서빙을 하게 되며, 환경 변수를 컨테이너로 넘길 필요가 있을 경우에는 env에 name/value 식으로 정의하면 된다.


$kubectl apply -f service.yaml

<그림. 서비스 배포>

이렇게 정의된 서비스 yaml 파일은 다른 쿠버네티스의 yaml 파일과 같게 kubectl apply -f {파일명}을 이용하면 배포할 수 있게 된다.

쿠버네티스와 마찬가지로 yaml 파일을 정의해서 컨테이너를 정의하고 서비스를 정의해서 배포하는데, 그렇다면 쿠버네티스로 배포하는 것과 무슨 차이가 있을 것인가? 위의 설정 파일을 보면, 로드밸런서,Ingress 등의 추가 설정없이 간단하게 서비스 컨테이너 이름만 정의하고, 컨테이너만 정의하면 바로 배포가 된다. 서비스를 하는데 필요한 기타 설정을 추상화 시켜서 개발자가 꼭 필요한 최소한의 설정만으로 서비스를 제공할 수 있도록 해서, 복잡도를 줄여주는 장점이 있다.

그러면 배포된 서비스를 호출해보자


서비스를 호출하기 위해서는 먼저 서비스의 IP를 알아야 하는데, Knative serving 은 서비스 매쉬 솔루션인 istio 또는 apigateway인 Gloo 상에서 작동한다. 이 예제는 istio 위에 knative를 설치한것을 가정으로 설명한다.  istio에 대한 설명은 이링크와 이 링크 를 참고하기 바란다.

istio를 사용한 경우에는 istio의 gateway를 통해서 서비스가 되고,하나의 istio gateway가 몇개의 knative 서비스를 라우팅을 통해서 서비스한다. 이때 는 단일 IP이기 때문에 여러 knative 서비스를 서빙하기 위해서는 knative 서비스를 분류할 수 있어야 하는데 URI를 이용해서 구별을 하거나 또는 hostname 으로 구별을 한다. 이 예제에서는 hostname으로 구별하는 방법을 사용하였다.


그러면 실제로 서비스를 호출해보자. 먼저 istio gateway의 ip를 알아야한다.

Istio gateway ip는 다음 명령어를 이용하면 ip를 조회할 수 있다.


$kubectl get svc istio-ingressgateway --namespace istio-system

<그림. Istio gateway IP 조회>


다음으로 해야할일은 서비스의 domain 명을 알아야 하는데, 여기서 배포한 서비스는 helloworld-go 라는 서비스이다. 이 서비스가 배포되면 서비스에 대한 라우팅 정보가 정의되는데, kubectl get route 명령을 이용하면 라우팅 정보를 조회할 수 있고 그 중에서 domain 명을 조회하면 된다.


$kubectl get route helloworld-go  --output=custom-columns=NAME:.metadata.name,DOMAIN:.status.domain

<그림. Istio gateway IP 조회>


호스트명을 조회하면 아래와 같이 해당 서비스의 호스트명을 알 수 있다.

Domain 명은 {route name}.{kubernetes name space}.도메인명 으로 되어 있고, 도메인명은 디폴트로 example.com을 사용한다. helloworld-go 애플리케이션의 route 명은 helloworld-go이고, 쿠버네티스 네임 스페이스는 default 네임 스페이스를 사용하였기 때문에, helloworld-go.default.example.com 이 전체 서비스 호스트명이 된다.


그러면 조회한 호스트명과 ingress gateway의 IP 주소를 이용해서, curl 명령으로 테스트 호출을 실행해보자.

$curl -H "Host: helloworld-go.default.example.com" http://${IP_ADDRESS}

<그림. Istio gateway IP 조회>

 

IP_ADDRESS는 앞에서 조회한 ingress의  gateway 주소를 이용하면 된다.

실행을하고 나면 다음과 같은 결과를 얻을 수 있다.

Serving detail

간단하게, Serving 을 테스트 해봤다. 그럼 Serving이 어떻게 구성되어 있는지 조금 더 자세하게 살펴보도록 하자. Serving 은 쿠버네티스 CRD (Custom Resource Definition)으로 정의된 4개의 컴포넌트로 구성되어 있다.


  • Configuration
    Configuration은 knative serving으로 배포되는 서비스를 정의한다. 컨테이너의 경로, 환경 변수, 오토스케일링 설정, hearbeat 설정등을 정의한다. 재미있는것은 단순히 컨테이너 경로를 정의할 수 도 있지만, 컨테이너 빌드 설정을 정의할 수 있다. 즉 코드가 변경되었을때 Configuration에 있는 빌드 설정을 통해서 새로운 컨테이너를 빌드해서 자동으로 배포하고 새롭게 배포된 컨테이너를 이용해서 서비스를 할 수 있도록 한다.

  • Revision
    Configuration의 히스토리라고 보면 되는데, Configuration을 생성할때 마다 새로운 revision이 생성된다.(Revision은 현재 Configuration의 스냅샷이다.) 그래서, 이전 revision으로 롤백을 하거나 저장된 각각의 다른 버전으로 트래픽을 분할해서 서빙할 수 있다.

  • Route
    Route는 서비스로 들어오는 트래픽을 Revision으로 라우팅 하는 역할을 한다. 단순하게 최신 버전의 revision으로 라우팅할 수 도 있지만, 카날리 테스트와 같이 여러 revision으로 라우팅 하는 역할은 Route에서 정의된다.

  • Service
    Service는 Configuration과 Route를 추상화하여, 하나의 웹서비스를 대표하는 개념이라고 보면 된다. 쿠버네티스에서 Deployment가 ReplicaSet 등을 추상화 하는 개념으로 생각하면 된다.


Serving 컴포넌트의 내용을 추상화하여 그림으로 표현하면 아래 그림과 같다.


<그림. Knative serving의 개념도>


스택 드라이버 로그로 로그 백앤드 구축하기

조대협 (http://bcho.tistory.com)


앞의 글에서까지 로그를 남기는 방법에 대해서 알아보았다. 이번 글에서는 로컬에 남긴 로그를 중앙으로 수집하여 모니터링할 수 있는 도구에 대해서 알아보고자 한다.

보통 로그 시스템은 오픈소스 기반의 ELK (Elastic search + Logstash + Kibana)를 많이 사용한다. 좋은 시스템이기는 하지만 러닝 커브가 있고, 구축과 운영에 노력이 들어간다.

대안으로는 클라우드 기반의 매니지드 서비스를 사용하는 방안이 있는데, 구글 클라우드의 스택드라이버 로깅이 사용이 편리하기 때문에 스택드라이버를 소개하고자 한다.

구글 클라우드의 스택드라이버는 로깅뿐만 아니라 모니터링, 에러 리포팅등 다양한 기능을 제공하는 운영용 도구 이다. 그 중에서 이 글에서는 스택드라이버 로깅만을 설명한다.


스택드라이버 로깅이 로그를 수집하는 방법은 크게, SDK를 사용하는 방법과, 로그 에이전트를 사용하는 방법 두가지가 있다. SDK를 이용하는 방법의 경우에는 잘 알려진 로깅 프레임웍과 잘 통합이 되는 장점을 가지고 있으나, 애플리케이션이 아닌 데이타 베이스나 웹서버와 같은 솔루션 로그 수집은 SDK를 사용할 수 없으니 불가능하고, 경우에 따라서 로깅 프레임워크가 지원되지 않는 경우가 있기 때문에, 이 글에서는 에이전트를 이용하는 방식에 대해서 설명한다.


SDK를 이용하는 방법은 자바는 SLF4J+Logback을 이용하면 되는데, 이글을 참고하면 된다. node.js 예제는 이글을 참고하기 바란다. 로깅 시스템의 개념에서 부터, 시스템을 확장하는 방법까지 자세하게 설명해놓았기 때문에, 두 글을 모두 읽어보는것을 추천한다.

스택드라이버 로그 에이전트

스택드라이버 로그 에이전트는 오픈소스 fluentd 기반으로 개발되었다. 파일뿐만 아니라 여러 데이타 소스에서 로그를 읽어서 클라우드나 데이타베이스로 데이타가 전송이 가능하다.

설치 방법은 이 문서에 잘 정리되어 있기 때문에, 별도로 설명하지 않는다. 단 주의할점은 스택드라이버 로그 에이전트는 AWS와 구글 클라우드에서만 사용이 가능하다.

스택드라이버 로그 에이전트를 설치하면 syslog등 디폴트로 시스템 로그를 수집한다. 디폴트로 수집하는 로그 리스트와 로그 파일의 경로는 이 문서 를 참고하면 된다.

 

스택 드라이버 로그 에이전트의 설정 정보는 /etc/google-fluentd/config.d 디렉토리에 저장되어 있다. 에이전트의 상태는

$ sudo service google-fluentd status

명령을 이용하면 현재 에이전트가 제대로 작동하는지 확인이 가능하다.

에이전트 테스트

설치후 디폴트로 syslog 로그를 수집하기 때문에, 테스트를 위해서는 syslog에 로그를 남겨보면 된다. logger 라는 리눅스 명령어는 syslog에 로그를 남기는 명령어이다.

$ logger “테스트 메세지"

를 남기면, syslog 파일에 저장이 되고, 이 로그는 자동으로 스택드라이버 에이전트에 의해서 서버로 전송이 된다.  아래는 hello terry 등의 문자열을 테스트로 남긴 예제이다.


구글 스택드라이버 로그 웹 콘솔에서 GCE VM Instance 카테고리를 선택해서 보면 아래와 같이 logger에 의해서 보낸 로그가 전달된것을 확인할 수 있다.



에이전트 설정

이 예제에서는 Spring Boot 애플리케이션에서 로그를 파일로 남긴 후에, 이 파일을 스택드라이버 로그 에이전트를 통해서 수집하는 시나리오를 구현한다. 아래 예제에 사용한 Spring Boot 소스코드는 이 링크에 있다. 스택 드라이버 로그 에이전트에 대한 설정 방법은 이 문서를 참고하면 된다.


새로운 로그 파일을 정의하기 위해서는 스택드라이버 로그 에이전트의 설정 파일을 추가해야 한다.

/etc/google-fluentd/config.d 디렉토리 아래 springboot 파일에 설정 정보를 아래와 같이 기술하고 저장한다.


<source>

   @type tail

   # Format 'none' indicates the log is unstructured (text).

   format json

   # The path of the log file.

   path /home/terrycho/log.out

   # The path of the position file that records where in the log file

   # we have processed already. This is useful when the agent

   # restarts.

   pos_file /var/lib/google-fluentd/pos/springboot-log.pos

   read_from_head true

   # The log tag for this log input.

   tag springboot

</source>


path 부분에 로그 파일의 위치를 지정한다. 여기서는 Spring boot 애플리케이션의 로그를 /home/terrycho/log.out 파일에 남기도록 하였기 때문에, 이 파일 경로를 지정한다. 그리고 pos_file은 로그 파일을 어디까지 읽었는지를 표시하는 파일인데, 위의 경로에 지정한다.

마지막으로 tag는 로그를 구별하기 위해서 주는 값으로 여기서는 springboot 라는 태그를 부여하였다.

이 tag를 이용하여 로그 이름은 "projects/[Project-ID]/logs/[tag]” 이름으로 정의된다. 예를 들어 구글 클라우드 프로젝트 이름이 myproject이고, 태그를 springboot로 지정하였으면, 이 로그의 이름은 “projects/myproject/logs/springboot”로 지정된다.

설정이 끝났으면

%sudo service google-fluentd restart

명령어를 이용하여 스택드라이버 로그 에이전트를 재시작한다. 그리고 curl 명령어를 이용하여 Spring boot 애플리케이션에 트래픽을 줘서 로그를 남기게 되면 아래와 같이 로그가 스택드라이버 콘솔로 전송된것을 확인할 수 있다.

애플리케이션에서 JSON으로 저장한 로그는 스택드라이버 로그 엔트리에서 jsonPayload 아래에 json 형태로 저장된다.


<그림. 로그 예제>


그리고, 이 예제는 Zipkin과 MDC를 통합하여 traceId를 넘기는 형태로 아래 화면은 같은 Trace Id로 들어온 요청만 쿼리한 결과이다. trace Id를 통해서 하나의 리퀘스트로 들어온 모든 로그들을 모아볼 수 있다. 아래 두 로그를 보면 jsonPayload > mdc > traceId가 같다.


< 그림. 동일 트레이스 ID로 추적한 결과 >

스택드라이버 로그는 Export 기능을 이용하여 빅쿼리나 클라우드 스토리지로 export가 가능한데, 아래 화면은 테스트용 VM 인스턴스의 로그만 빅쿼리로 export 하도록 설정하는 화면이다.


<그림. Log Export 지정>


이렇게 빅쿼리로 로그가 Export 되면 아래 그림과 같이 SQL을 이용해서 로그를 분석할 수 있다.



티스토리가 개편후 더 이상한듯

분류없음 | 2019.04.14 22:49 | Posted by 조대협

티스토리가 근래에 오랜만에 개편이 되고, 에디터도 대폭 개선이된데다가 마크다운 에디터까지 지원하는 것은 좋은데, 사용자 경험을 그전 호환성을 유지해야 하는데, 이번에는 좀 문제가 있지 않나 싶다. 티스토리 에디터가 너무 불편해서 글쓰기가 어려워서 그간 여러 방법을 고민하다가 내린 결론이 구글닥스를 이용해서 글을 다 쓴 후에, 복붙으로 붙이는 방식을 사용했는데, 잘되다가  얼마전 부터, 글을 써 붙이니 포맷이 모조리 깨져 버린다. 코드블록도 프로그램언어는 있지만 XML이나 JSON,YAML 은 왜 없는지도 싶고..

 

https://bcho.tistory.com/1319

 

로깅 시스템 #6-Spring Boot에서 Zipkin을 이용한 분산 시스템 로깅

Spring Boot + slf4j + MDC + Zipkin 조대협 (http://bcho.tistory.com) 아래 예제는 MDC를 이용해서 여러 메서드간의 컨텍스트를 연결하는 것을 확장해서, 서로 다른 프로세스와 서버간에 로그를 연결하는 방법이..

bcho.tistory.com

마크다운도 좋고, 새로운 에디터도 깨끗해서 좋기는 한데, 그 전 사용자 경험을 위해서 기능 호환성을 계속 유지해줬으면 하는 생각이 있다. 

요즘 같아서는 Wordpress나 github로 옮기고 싶은 생각만 가득한데... 글이 워낙 많아서 마이그레이션 코드를 짜야 할거 같아서... 이러지도 저러지도 못하고 있는...

Spring Boot + slf4j + MDC + Zipkin

 

조대협 (http://bcho.tistory.com)

 

아래 예제는 MDC를 이용해서 여러 메서드간의 컨텍스트를 연결하는 것을 확장해서, 서로 다른 프로세스와 서버간에 로그를 연결하는 방법이다. 서로 다른 프로세스 또는 서버간에 컨텍스트를 전달하려면 HTTP 헤더등을 통해러 리모트로 컨텍스트를 전달해야 하는데, 이를 가능하게 하는 오픈소스로 Zipkin이 있다. (자세한 설명은 이글을 참고하기 바란다. )

Zipkin은 원래 분산 로그 추적용으로 개발된 오픈소스가 아니라 원래 목적은 분산 시스템에서 각 구간별 레이턴시(지연시간)을 측정해서 구간별 소요 시간을 측정하는 트레이스용도로 개발이 되었지만, 구간별 소요 시간을 측정하기 위해서는 각 개별 서비스를 연결해야 하기 때문에, 트레이스 ID가 필요하게 되었고, 트레이스 ID를 로그에 같이 저장하였기 때문에, 부가적인 효과로 분산 로그 추적에도 사용할 수 있다.

Zipkin을 Spring Boot와 연결하는 방법은 오픈소스인 Spring Sleuth를 이용하면 쉽게 된다.

 

아래 예제는 앞의 글인  Spring Boot에서 MDC를 사용하는 예제에 Zipkin 연동만을 추가한 예제이다.

Spring Boot로 간단한 REST API를 구현한후, 로깅을 하는 예제이다. 로거는 slf4j와 logback을 사용하였고,  MDC를 이용해서 userId와 같은 컨택스트 정보를 넘기도록 하였고, JSON 포맷으로 로그를 출력하였다. 마이크로 서비스와 같은 분산 서비스간에 로그 추적성을 제공하기 위해서 ZipKin 라이브러리를 사용하였다. 스프링에서는 ZipKin 라이브러리 통합을 Spring Sleuth를 통해서 지원하기 때문에, Spring Sleuth와 Zipkin을 연결하여 코드를 작성하였다. 전체 코드는 여기를 참고하면 된다.

 

아래는 Spring Boot에서 Zipkin을 사용하기 위해서 메이븐 pom.xml에 의존성을 추가한 내용이다.

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-sleuth</artifactId>

<version>2.1.1.RELEASE</version>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-zipkin</artifactId>

<version>2.1.1.RELEASE</version>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>com.google.cloud</groupId>

<artifactId>google-cloud-logging-logback</artifactId>

<version>0.84.0-alpha</version>

</dependency>


<!-- slf4j & logback dependency -->

<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-json-classic</artifactId>

<version>0.1.5</version>

</dependency>

<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-jackson</artifactId>

<version>0.1.5</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.9.3</version>

</dependency>

<코드.pom.xml >

 

다음은 logback 로거를 사용하기 위해서 logback에 대한 설정을 한 logback.xml이다. JSON 포맷으로 로깅을 하도록 설정 하였다.

 

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

   <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">

       <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

           <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">

               <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>

               <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>

               <appendLineSeparator>true</appendLineSeparator>


               <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">

                   <!--

                   <prettyPrint>true</prettyPrint>

                    -->`

               </jsonFormatter>

           </layout>

       </encoder>

   </appender>


   <root level="info">

       <appender-ref ref="stdout"/>

   </root>

</configuration>

<코드. /resources/logback.xml >

 

Zipkin을 사용할 경우, 트레이스 정보를 zipkin 서버로 전송해야 하는데, 이를 위해서 zipkin 서버에 대한 정보를 설정해야 한다. 보통 zipkin 에이전트가 로컬에서 돌기 때문에, 포트만 지정하면 된다. 아래와 같이 zipkin 서버에 대한 포트를 8081로 지정하였고, 이 애플레케이션의 이름을 zipkin-server1으로 지정하였다. 이 예제에서는 zipkin을 분산로그 추적용으로만 사용하였기 때문에, 실제로 zipkin 서버 에이전트는 실행하지 않았다.

server.port = 8081

spring.application.name = zipkin-server1

<코드. /resources/application.properties >

 

다음은 Spring Boot의 REST API Controller 코드의 일부이다.

@RestController
@RequestMapping("/orders")
public class OrderController {
Logger log = LoggerFactory.getLogger("com.terry.logging.controller.OrderController");
@RequestMapping(value="/{id}",method=RequestMethod.GET)
public Order getOrder(@PathVariable int id,
      @RequestHeader(value="userid") String userid) {
  MDC.put("userId", userid);
  MDC.put("ordierId",Integer.toString(id));
  Order order = queryOrder(id,userid);
  log.info("Get Order");
  MDC.clear();
  return order;
}

Order queryOrder(int id,String userid) {
  String name = "laptop";
  Order order = new Order(id,name);
  order.setUser(userid);
  order.setPricePerItem(100);
  order.setQuantity(1);
  order.setTotalPrice(100);
  log.info("product name:"+name);
  return order;
}

<코드. /resources/application.properties >

 

Spring Sleuth를 사용하게 되면 자동으로 Zipkin 코드를 의존성 주입 (Dependency Injection)을 이용해서 코드에 삽입해주는데, 이때 몇가지 제약사항이 있다. Spring Boot로 들어오는 트래픽은 Servlet Filter를 통해서 의존성 주입을 하는데, Spring Boot에서 다른 서비스로 나가는 트래픽의 경우에는 Rest Template 이나, Feign Client 와 같은 특정한 방법만을 지원한다. 지원되는 라이브러리의 범위에 대해서는 이 링크를 참고하기 바란다.

 

위의 예제는 HTTP Header에서 들어온 userId를 MDC 컨텍스트에 저장하는 예제이다.

위의 REST 서비스를 호출해보면 다음과 같은 결과가 나온다.

<그림. PostMan을 통해서 REST 요청과 응답을 받은 화면 >

 

그리고, 호출후에 나온 로그는 다음과 같다.

 

{  

  "timestamp":"2019-04-14T05:49:52.573Z",

  "level":"INFO",

  "thread":"http-nio-8081-exec-1",

  "mdc":{  

     "traceId":"270b7b7b5a8d4b5c",

     "spanId":"270b7b7b5a8d4b5c",

     "spanExportable":"false",

     "X-Span-Export":"false",

     "ordierId":"1",

     "X-B3-SpanId":"270b7b7b5a8d4b5c",

     "X-B3-TraceId":"270b7b7b5a8d4b5c",

     "userId":"terry"

  },

  "logger":"com.terry.logging.controller.OrderController",

  "message":"Get Order",

  "context":"default"

}

<코드. /resources/application.properties >

 

위의 결과와 같이 MDC 부분에, Zipkin이 자동으로 traceId를 선언해서 삽입해 준다. MDC에 저장한 userId도 위처럼 한꺼번에 출력되는 것을 확인할 수 있다.

 

Spring Sleuth는 slf4j를 사용하는 경우에만 MDC 컨텍스트에 트레이스 ID를 넣어주기 때문에, 다른 자바 로깅 프레임웍을 slf4j없이 사용하는 경우 자동으로 트레이스 ID를 넣어주지 않기 때문에 이점을 주의하기 바란다.

(참고 : "Adds trace and span ids to the Slf4J MDC, so you can extract all the logs from a given trace or span in a log aggregator.")

스택드라이버 로깅을 테스트 하고자 로컬 환경에서 로그를 올리기 위해서 fluentd를 설치했는데

일단 설치는 서비스 어카운트를 다운 받아서 하면 되긴 하는데, 실행을 하고 로그를 전송하려면 아래와 같은 이유가 난다.

2019-04-02 01:20:36 +0900 [error]: #0 Failed to access metadata service:  error_class=Errno::EHOSTUNREACH error="Failed to open TCP connection to 169.254.169.254:80 (No route to host - connect(2) for \"169.254.169.254\" port 80)"
2019-04-02 01:20:36 +0900 [info]: #0 Unable to determine platform
2019-04-02 01:20:36 +0900 [error]: #0 Failed to set monitored resource labels for gce_instance:  error_class=RuntimeError error="Cannot construct a gce_instance resource without vm_id and zone"

169.254.169.254 로 호출을 하게 되어 있는데, 이게 구글 클라우드와 AWS IP대역에서만 호출할 수 있는듯.

이론적으로는 Fluentd를 사용하기 때문에, 다른 인프라에서도 될줄 알았는데. IP 대역을 막아놨네 그랴..

테스트 하려면 애플리케이션들도 같이 올려야 하는데...

도커로 말아서 AWS에서 올려보고 해야 쓰겄다. ㅜㅜ

 

로깅 시스템 #5 - Spring boot에서 JSON 포맷 로깅과 MDC 사용하기

조대협 (http://bcho.tistory.com)


실제로 백앤드 애플리케이션을 자바로 개발할때는 스프링 부트를 사용하는 경우가 대부분이기 때문에 앞에서 적용한 JSON 로그 포맷과 MDC 로깅을 스프링 부트에 적용해보자

스프링 부트라고 해도, 일반 자바 애플리케이션에 대비해서 로그 설정 부분에 다른점은 없다.

아래와 같이 pom.xml에 logback과 json 의존성을 추가한다.


<!-- slf4j & logback dependency -->

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-classic</artifactId>

<version>1.2.3</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-json-classic</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-jackson</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.9.3</version>

</dependency>


다음 로그 포맷팅을 JSON으로 하기 위해서 아래와 같이 logback.xml 파일을 작성하여 main/resources 디렉토리에 저장한다. 이번 예제에서는 스프링 부트로 기동할 경우 스프링 부트 자체에 대한 로그가 많기 때문에, JSON 으로 엘리먼트 단위로 출력하면 줄바꿈이 많아서, 로그를 보는데 어려움이 있으니 엘리먼트 단위로 줄을 바꾸지 않도록 <prettyPrint> 옵션을 false 로 처리하고, 대신 이벤트마다는 줄을 바꾸는게 좋으니, <appendLineSeperator>를 true로 설정하였다.


<?xml version="1.0" encoding="UTF-8"?>

<configuration>

   <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">

       <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

           <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">

               <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>

               <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>

               <appendLineSeparator>true</appendLineSeparator>


               <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">

                   <!--

                   <prettyPrint>true</prettyPrint>

                    -->`

               </jsonFormatter>

           </layout>

       </encoder>

   </appender>


   <root level="debug">

       <appender-ref ref="stdout"/>

   </root>

</configuration>


다음으로 아래와 같이 간단한 Controller를 작성하였다. /orders/{id} 형태의 REST API로 사용자 이름을 userid라는 키로 HTTP Header를 통해서 받도록 하였다.


package com.terry.logging.controller;

import org.springframework.web.bind.annotation.PathVariable;


import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import com.terry.logging.model.*;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;


@RestController

@RequestMapping("/orders")


public class OrderController {

Logger log = LoggerFactory.getLogger("com.terry.logging.controller.OrderController");

@RequestMapping(value="/{id}",method=RequestMethod.GET)

public Order getOrder(@PathVariable int id,

@RequestHeader(value="userid") String userid) {

MDC.put("userId", userid);

MDC.put("ordierId",Integer.toString(id));

Order order = queryOrder(id,userid);

log.info("Get Order");

MDC.clear();

return order;

}

Order queryOrder(int id,String userid) {

String name = "laptop";

Order order = new Order(id,name);

order.setUser(userid);

order.setPricePerItem(100);

order.setQuantity(1);

order.setTotalPrice(100);


log.info("product name:"+name);

return order;

}

}


userid와 orderid를 MDC에 넣어서 매번 로그때 마다 출력하도록 하였다.

아래 코드는 위에서 사용된 Order Value Class 내용이다.


package com.terry.logging.model;


public class Order {

public Order(int id,String item) {

this.item=item;

this.id = id;

}

public String getItem() {

return item;

}

public void setItem(String item) {

this.item = item;

}

public int getPricePerItem() {

return pricePerItem;

}

public void setPricePerItem(int pricePerItem) {

this.pricePerItem = pricePerItem;

}

public int getQuantity() {

return quantity;

}

public void setQuantity(int quantity) {

this.quantity = quantity;

}

public int getTotalPrice() {

return totalPrice;

}

public void setTotalPrice(int totalPrice) {

this.totalPrice = totalPrice;

}

String item;

int pricePerItem;

int quantity;

int totalPrice;

int id;

String user;

public String getUser() {

return user;

}

public void setUser(String user) {

this.user = user;

}

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

}



코드를 실행한후 REST API 클라이언트 도구 (여기서는 Postman을 사용하였다.)를 호출하면 브라우져에는 다음과 같은 결과가 출력된다.

그리고 로그는 아래와 같이 출력된다.


MDC를 이용한 저장한 컨택스트는 아래와 같이 JSON의 mdc 컨택스에 출력되었고, log.info()로 출력한 로그는 message 엘리먼트에 출력된것을 확인할 수 있다.

{"timestamp":"2019-03-25T15:16:16.394Z","level":"DEBUG","thread":"http-nio-8080-exec-2","logger":"org.springframework.web.servlet.DispatcherServlet","message":"Last-Modified value for [/orders/1] is: -1","context":"default"}

{"timestamp":"2019-03-25T15:16:16.395Z","level":"INFO","thread":"http-nio-8080-exec-2","mdc":{"ordierId":"1","userId":"terry"},"logger":"com.terry.logging.controller.OrderController","message":"product name:laptop","context":"default"}

{"timestamp":"2019-03-25T15:16:16.395Z","level":"INFO","thread":"http-nio-8080-exec-2","mdc":{"ordierId":"1","userId":"terry"},"logger":"com.terry.logging.controller.OrderController","message":"Get Order","context":"default"}


전체 소스코드는 https://github.com/bwcho75/javalogging/tree/master/springbootmdc 에 저장되어 있다.


이렇게 하면, 스프링 부트를 이용한 REST API에서 어떤 요청이 들어오더라도, 각 요청에 대한 ID를 Controller에서 부여해서, MDC를 통하여 전달하여 리턴을 할때 까지 그 값을 유지하여 로그로 출력할 수 있다.


그러나 이 방법은 하나의 스프링 부트 애플리케이션에서만 가능하고, 여러개의 스프링 부트 서비스로 이루어진 마이크로 서비스에서는 서비스간의 호출이 있을 경우 이 서비스간 호출에 대한 로그를 묶을 수 없는 단점이 있다.

예를 들어 서비스 A → 서비스 B로 호출을 하였을 경우에는 서비스 A에서 요청에 부여한 ID와 서비스 B에서 요청에 부여한 ID가 다르기 때문에 이를 묶기가 어렵다. 물론 HTTP 헤더로 ID를 전달하는 등의 방법은 있지만, 그다지 구성이 깔끔 하지 않다. 이렇게 마이크로 서비스에서 서비스간의 ID를 추적할 수 있는 방법으로 분산 환경에서 서비스간의 지연 시간을 측정하는 프레임웍으로 Zipkin이라는 프레임웍이 있다. 다음 글에서는 이 Zipkin을 로그 프레임웍과 연결해서 마이크로 서비스 환경에서 스프링 부트 기반으로 서비스간의 로그 추적을 어떻게할 수 있는지에 대해서 알아보도록 한다.


로깅 시스템 #4 - Correlation id & MDC

조대협(http://bcho.tistory.com)

Correlation id

하나의 프로그램은 여러개의 메서드들로 조합이 된다. 하나의 요청을 처리하기 위해서는 여러개의 메서드들이 순차적으로 실행이 되는데, 멀티 쓰레드 프로그램에서 여러개의 쓰레드 동시에 각각의 요청을 처리할때, 각 메서드에 로그를 남기게 되면, 멀티 쓰레드 프로그램에서는 쓰레드들이 서로 컨택스트를 바꿔가며 실행이 되기 때문에, 로그 메시지가 섞이게 된다

아래 그림을 보자.


요청 A와 B가 호출되어 각각 다른 쓰레드에서 실행이 되었을때, 위의 그림과 같이 로그 메시지가 섞이게 된다. 이런 경우 요청 A에 대한 처리 내용을 확인하기 위해서 요청 A에 대한 로그만을 보고 싶을때 로그가 섞여 버려서 요청 A에 대한 로그만을 분리해내는 것이 어렵다.


이런 문제를 해결하기 위해서는 로그를 기록할때, 요청 마다 고유의 ID를 부여해서 로그를 기록하게 되면, 그 ID를 이용해서 각 요청마다의 로그를 묶어서 볼 수 있다.


위의 그림을 보면 요청 A에 대해서는 “1”이라는 ID를 지정하고, 두번째로 들어온 요청 B에 대해서는 “A”라는 ID를 지정하면, 이 ID로 각 요청에 대한 로그들을 묶어서 조회할 수 있다. 이를 Correlation ID라고 한다.

ThreadLocal

그러면 이 Correlation ID를 어떻게 구현해야 할까?

요청을 받은 메서드에서 Correlation ID를 생성한 후, 다른 메서드를 호출할때 마다 이 ID를 인자로 넘기는 방법이 있지만, 이 방법은 현실성이 떨어진다. ID를 넘기기 위해서 모든 메서드에 공통적으로 ID 필드를 가져야 하기 때문이다.

이런 문제는 자바에서는 ThreadLocal이라는 변수를 통해서 해결할 수 있다.

ThreadLocal을 쓰레드의 로컬 컨텍스트 변수로 Thread 가 존재하는 한 계속해서 남아 있는 변수이다.

무슨 이야기인가 하면, 작업 요청이 들어왔을때, 하나의 쓰레드가 생성이 되고 작업이 끝나면 쓰레드가 없어진다고 하면, 쓰레드가 살아있을 동안에, 계속 유지되는 변수이다. 즉 쓰레드가 생성되어 있는 동안에, 쓰레드 안에서 계속 참고해서 쓸수 있는 쓰레드 범위의 글로벌 변수라고 생각하면 된다.

그래서 요청을 처음 받았을때 Correlation ID를 생성하고, 이를 ThreadLocal에 저장했다가 로그를 쓸때 매번 이 ID를 ThreadLocal에서 꺼내서 같이 출력하면 된다. 이 개념을 그림으로 표현해보면 다음과 같다.


ThreadLocal의 ID 변수에 Correlation ID를 저장해놓고, 각 메서드에서 이 값을 불러서 로그 출력시 함께 출력하는 방법이다.

MDC

그러나 이걸 일일이 구현하기에는 불편할 수 있기 때문에, slf4j,logback,log4j2등의 자바 로그 프레임워크에서는 이런 기능을 MDC(Mapped Diagnostic Context)로 제공한다.

단순하게 CorrelationID 뿐만 아니라 map 형식으로 여러 메타 데이타를 넣을 수 있다. Correlation ID는 여러 요청을  묶을 수 는 있지만 다양한 요청에 대한 컨텍스트는 알 수 없다. 실행되는 요청이 어떤 사용자로 부터 들어온것인지, 또는 상품 주문시 상품 주문 ID를 넣는다던지, 요청에 대한 다양한 컨텍스트 정보를 MDC에 저장하고 로그 출력시 함께 출력하면 더 의미 있는 로그를 출력할 수 있다.


MDC를 사용한 코드를 작성해보자

이 예제에서는 slf4j와 logback을 사용한다. 아래처럼 logback과 json 관련 의존성을 pom.xml 에 정의한다.


<dependencies>

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-classic</artifactId>

<version>1.1.7</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-json-classic</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-jackson</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.9.3</version>

</dependency>

</dependencies>



그리고 로그를 json 포맷으로 출력하기 위해서 아래와 같이 logback.xml 을 작성하여, main/resources/logback.xml 로 저장한다.

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

   <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">

       <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

           <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">

               <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>

               <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>


               <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">

                   <prettyPrint>true</prettyPrint>

               </jsonFormatter>

           </layout>

       </encoder>

   </appender>


   <root level="debug">

       <appender-ref ref="stdout"/>

   </root>

</configuration>



다음 아래와 같이 간단하게 MDC를 테스트 하는 코드를 작성한다.

package com.terry.logging.logbackMDC;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;



public class App

{

   private static Logger log = LoggerFactory.getLogger(App.class);

   public static void main( String[] args )

   {

    log.info("Hello logback");

   

    MDC.put("userid", "terrycho");

    MDC.put("event", "orderProduct");

    MDC.put("transactionId", "a123");

    log.info("mdc test");

   

    MDC.clear();

    log.info("after mdc.clear");

   }

}


MDC를 사용하는 방법은 MDC에 값을 넣을때는 mdc.put(key,value)를 이용하여, 값을 넣고 지울때는  mdc.remove(key)를 이용해서 특정 키를 삭제한다. 전체를 지울때는 mdc.clear()를 사용한다. mdc에 저장된 내용은 logger를 이용해서 로그를 출력할때 마다 mdc 라는 element로 자동으로 함께 출력된다.

위의 예제를 보면 log.info(“hello logback”)으로 로그를 출력하였고 그 다음 mdc에 userid,event,transactionid 라는 키들로 값을 채운 다음에 log.info(“mdc test”)라는 로그를 출력하였다. 그리고 마지막에는 mdc를 모두 지운 후에 “after mdc clear”라는 로그를 출력하는 코드이다. 결과는 다음과 같다.


{

 "timestamp" : "2019-03-23T05:42:19.102Z",

 "level" : "INFO",

 "thread" : "main",

 "logger" : "com.terry.logging.logbackMDC.App",

 "message" : "Hello logback",

 "context" : "default"

}{

 "timestamp" : "2019-03-23T05:42:19.118Z",

 "level" : "INFO",

 "thread" : "main",

 "mdc" : {

   "event" : "orderProduct",

   "userid" : "terrycho",

   "transactionId" : "a123"

 },

 "logger" : "com.terry.logging.logbackMDC.App",

 "message" : "mdc test",

 "context" : "default"

}{

 "timestamp" : "2019-03-23T05:42:19.119Z",

 "level" : "INFO",

 "thread" : "main",

 "logger" : "com.terry.logging.logbackMDC.App",

 "message" : "after mdc.clear",

 "context" : "default"

}


첫번째 로그는 “Hello logback”이라는 메시지가 출력된 후에, 두번째 로그는 mdc 가 세팅되어 있기 때문에, mdc라는 element가 출력되는데, 그 안에 mdc에 저장한 event,userid,transactionid  값이 함께 출력되는 것을 볼 수 있다. 그리고 마지막에는 mdc를 clear 한후에, 로그를 출력하였기 때문에, 메시지만 출력이 되고 mdc 라는 element없이 출력된것을 확인할 수 있다.


slf4j+logback 을 사용할 경우에는 앞의 글에서 설명하였듯이 json으로 로그 출력시 message 문자열 하나면 json element로 출력할 수 있었다. 그래서 custom layout 등 다른 대체 기법을 사용하였는데, mdc를 사용하여 컨텍스트 정보를 넘기게 되면, slf4j와 logback의 제약 사항을 넘어서 json으로 여러 element를 로깅 할 수 있다.





로그 시스템 #3 - JSON 로그에 필드 추가하기

조대협 (http://bcho.tistory.com)

JSON 로그에 필드 추가

앞에 예제에서 로그를 Json 포맷으로 출력하였다. 그런데, 실제로 출력된 로그 메세지는 log.info(“문자열") 로 출력한 문자열 하나만 json log의 message 필드로 출력된것을 확인 할 수 있다.

그렇지만, 단순한 디버깅 용도의 로그가 아니라 데이터를 수집하는 용도등의 로깅의 message라는 하나의 필드만으로는 부족하다. 여러개의 필드를 추가하고자 할때는 어떻게 할까? Json Object를 log.info(jsonObject) 식으로 데이터 객체를 넘기면 좋겠지만 불행하게도 slf4j에서 logging에 남길 수 있는 인자는 String  타입만을 지원하고, 데이터 객체를 (json 객체나, Map 과 같은 데이타형) 넘길 수 가 없다.

slf4j + logback

slf4j + logback 의 경우에는 앞에서 언급한것과 같이 로그에 객체를 넘길 수 없고 문자열만 넘길 수 밖에 없기 때문에, json 로그에 여러개의 필드를 넘겨서 출력할 수 가 없다. 아래는 Map 객체를 만든 후에, Jackson json 라이브러리를 이용하여, Json 문자열로 변경하여 slf4j로 로깅한 코드이다.

package com.terry.logging.jsonlog;


import java.util.Map;

import java.util.TreeMap;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;


public class Slf4j

{

private static Logger log = LoggerFactory.getLogger(Slf4j.class);

   public static void main( String[] args ) throws JsonProcessingException

   {

       Map<String,String> map = new TreeMap();

    map.put("name", "terry");

    map.put("email","terry@mycompany.com");

    String msg = new ObjectMapper().writeValueAsString(map);

    System.out.println("MSG:"+msg);

    log.info(msg);

   }

}

실행을 하면 message 엘리먼트 안에 json 문자열로 출력이 되는 것이 아니라 “ 등을 escape 처리하여 json 문자열이 아닌 형태로 출력이 된다.

{

 "thread" : "main",

 "level" : "INFO",

 "loggerName" : "com.terry.logging.jsonlog.Slf4j",

 "message" : "{\"email\":\"terry@mycompany.com\",\"name\":\"terry\"}",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1553182988,

   "nanoOfSecond" : 747493000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}


그래서 다른 방법을 사용해야 하는데, 로그를 남길때 문자열로 여러 필드를 넘기고 이를 로그로 출력할때 이를 파싱해서 json 형태로 출력하는 방법이 있다.

log.info("event:order,name:terry,address:terrycho@google.com");

와 같이 key:value, key:value, ..  식으로 로그를 남기고, Custom Layout에서 이를 파싱해서 json 으로

{

 “key”:”value”,

 “key”:”value”,

 “key”:”value”

}

형태로 출력하도록 하면 된다. 이렇게 하기 위해서는 log message로 들어온 문자열을 파싱해서 json으로 변환해서 출력할 용도로 Layout을 customization 하는 코드는 다음과 같다.


{package com.terry.logging.logbackCustom;


import ch.qos.logback.classic.spi.ILoggingEvent;

import ch.qos.logback.contrib.json.classic.JsonLayout;


import java.util.Map;

import java.util.StringTokenizer;

import java.util.TreeMap;


import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;


public class CustomLayout extends JsonLayout {

   @Override

   protected void addCustomDataToJsonMap(Map<String, Object> map, ILoggingEvent event) {

       long timestampMillis = event.getTimeStamp();

       map.put("timestampSeconds", timestampMillis / 1000);

       map.put("timestampNanos", (timestampMillis % 1000) * 1000000);

       map.put("severity", String.valueOf(event.getLevel()));

       map.put("original_message", event.getMessage());

       map.remove("message");

       

       StringTokenizer st = new StringTokenizer(event.getMessage(),",");

       Map<String,String> json = new TreeMap();


       while(st.hasMoreTokens()) {

       String elmStr = st.nextToken();

       StringTokenizer elmSt = new StringTokenizer(elmStr,":");

       String key = elmSt.nextToken();

       String value = elmSt.nextToken();

       json.put(key, value);

       }

       

    String msg;

try {

msg = new ObjectMapper().writeValueAsString(json);

} catch (JsonProcessingException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

    map.put("jsonpayload", json);

   

   }

}


먼저 JsonLayout을 상속받아서 CustomLayout 이라는 클래스를 만든다. 그리고 addCustomDataToJsonMap 이라는 메서드를 오버라이딩한다. 이 메서드는 로그로 출력할 메시지와 각종 메타 정보(쓰레드명, 시간등)을 로그로 최종 출력하기 전에, Map객체에 그 내용을 저장하여 넘기는 메서드이다.

이 메서드 안에서 앞에서 로그로 받은 문자열을 파싱해서 json 형태로 만든다. 아래 코드가 파싱을 해서 파싱된 내용을 Map에 key/value 형태로 저장하는 코드이고


       StringTokenizer st = new StringTokenizer(event.getMessage(),",");

       Map<String,String> json = new TreeMap();


       while(st.hasMoreTokens()) {

       String elmStr = st.nextToken();

       StringTokenizer elmSt = new StringTokenizer(elmStr,":");

       String key = elmSt.nextToken();

       String value = elmSt.nextToken();

       json.put(key, value);

       }

다음 코드는 이 Map을 json으로 변환한 후, 이를 다시 String으로 변환하는 코드이다.


msg = new ObjectMapper().writeValueAsString(json);


그 후에 이 json 문자열을 jsonpayload 라는 json element 이름으로 해서, json 내용을 json으로 집어 넣는 부분이다.


map.put("jsonpayload", json);

   

그리고, 이 CustomLayout을 사용하기 위해서 src/main/logback.xml에서 아래와 같이 CustomLayout 클래스의 경로를 지정한다.


<?xml version="1.0" encoding="UTF-8"?>

<configuration>

   <appender name="CONSOLE-JSON" class="ch.qos.logback.core.ConsoleAppender">

       <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

           <layout class="com.terry.logging.logbackCustom.CustomLayout">

               <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">

                   <prettyPrint>true</prettyPrint>

               </jsonFormatter>

               <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSXXX</timestampFormat>

               <appendLineSeparator>true</appendLineSeparator>

           </layout>

       </encoder>

   </appender>


   <root level="info">

       <appender-ref ref="CONSOLE-JSON"/>

   </root>

</configuration>



설정이 끝난 후에, 로그를 출력해보면 다음과 같이 jsonpayload element 부분에 아래와 같이 json 형태로 로그가 출력된다.


{

 "timestamp" : "2019-03-22T17:48:56.434+09:00",

 "level" : "INFO",

 "thread" : "main",

 "logger" : "com.terry.logging.logbackCustom.App",

 "context" : "default",

 "timestampSeconds" : 1553244536,

 "timestampNanos" : 434000000,

 "severity" : "INFO",

 "original_message" : "event:order,name:terry,address:terrycho@google.com",

 "jsonpayload" : {

   "address" : "terrycho@google.com",

   "event" : "order",

   "name" : "terry"

 }

}


log4j2

log4j2의 경우 slf4+logback 조합보다 더 유연한데, log.info 와 같이 로깅 부분에 문자열뿐만 아니라 Object를 직접 넘길 수 있다. ( log4j2의 경우에는 2.11 버전부터 JSON 로깅을 지원 : https://issues.apache.org/jira/browse/log4j2-2190 )

즉 log.info 등에 json 을 직접 넘길 수 있다는 이야기다. 그렇지만 이 기능은 log4j2의 기능이지 slf4j의 인터페이스를 통해서 제공되는 기능이 아니기 때문에, slf4j + log4j2 조합으로는 사용이 불가능하고  log4j2만을 이용해야 한다.


log4j2를 이용해서 json 로그를 남기는 코드는 아래와 같다.


package com.terry.logging.jsonlog;


import java.util.Map;

import java.util.TreeMap;


import org.apache.logging.log4j.message.ObjectMessage;

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;


public class App

{

  private static Logger log = LogManager.getLogger(App.class);


   public static void main( String[] args )

   {

    Map<String,String> map = new TreeMap();

    map.put("name", "terry");

    map.put("email","terry@mycompany.com");

    ObjectMessage msg = new ObjectMessage(map);

    log.info(msg);

   }

}



Map 객체를 만들어서 json 포맷처럼 key/value 식으로 데이타를 넣은 다음에 ObjectMessage 객체 타입으로 컨버트를 한다. 그리고 로깅에서 log.info(msg)로 ObjectMessage 객체를 넘기면 된다.

그리고 아래는 위의 코드를 실행하기 위한 pom.xml 에서 dependency 부분이다.


<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-slf4j18-impl</artifactId>

<version>2.11.2</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-core</artifactId>

<version>2.7.4</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.7.4</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-annotations</artifactId>

<version>2.7.4</version>

</dependency>


실행을 해보면 아래와 같이 json 포맷으로 메세지가 출력된 결과이다. message element를 보면, 위에서 넣은 key/value 필드인 email과, name 항목이 출력된것을 확인할 수 있다.


{

 "thread" : "main",

 "level" : "INFO",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : {

   "email" : "terry@mycompany.com",

   "name" : "terry"

 },

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",

 "instant" : {

   "epochSecond" : 1553245991,

   "nanoOfSecond" : 414157000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}