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


Archive»


 
 

쿠버네티스 패키지 매니저 Helm

#2-4 Helm Chart Hook


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

Hook은 차트 설치나, 삭제와 같이 차트의 라이프 사이클 중에, 차크 개발자가 동작을 추가해줄 수 있도록 해주는 기능이다. mySQL을 차트로 설치한 후에, mySQL에 테이블을 생성하고 데이타를 로딩하거나, 차트로 Pod를 설치하기전에 Configmap이나 Secret 의 값을 세팅해놓는 것과 같은 작업을 예를 들 수 있다. 

등을 들 수 있다.


Hook 으로 실행되는 리소스는 따로 있는 것이 아니라, 기존의 쿠버네티스 리소스 (Job, Cron Job 등)에 metadata.annotations.”helm.sh/hook”  으로 태그를 달아주면, 이 리소스들은 Hook으로 정의되어 차트 인스톨 전후에 정해진 시점에 실행된다. 

아래는 차트 인스톨 전에  작업을 처리하도록 Hook을 정의한 예제이다. 


apiVersion: ...

kind: ....

metadata:

  annotations:

    "helm.sh/hook": "pre-install"

# ...


예를 들어 아래 그림과 같이 차트에, Deployment,Service,Job 등의 리소스를 차트에 정의해서 배포하고자 한다. 그중에, 인스톨 전과 후에 특정 작업을 Job을 이용해서 처리하고자 한다면, 그 Job들에 annotation으로, Helm Hook임을 명시해줘야 한다. 


<그림. Helm 차트내의 리소스와 Hook으로 정의된 리소스들>


이 차트를 실행하게 되면 다음과 같은 순서로 실행이 된다. 먼저 차트에 정의되어 있는 리소스 중에서 Hook 으로 정의된 리소스중, pre-install 로 정의된 리소스가 먼저 실행이 되고, 그 다음에 다른 리소스가 인스톨이 된다. 인스톨이 완료되고 나면 그후에 post-install 로 정의된 Hook이 실행되는 형태이다. 



<그림. Helm 차트에서 리소스들의 배포 순서>


Hook 종류

Hook 은 인스톨 전과 후 뿐만 아니라, 릴리즈를 삭제하거나 업그레이드 할때 등 다양한 시점에 Hook을 삽입할 수 있도록 정의되어 있다.  자세한 Hook 의 종류는 https://github.com/helm/helm/blob/master/docs/charts_hooks.md 문서를 참고하기 바란다. 정의된 Hook 중 몇가지를 보면 다음과 같다. 


  • pre-install: 리소스를 설치하기전에 실행된다. (정확히 이야기 하면 리소스를 설치하기 위한 템플릿이 렌더링 된 후에, 렌더링된 템플릿으로 리소스를 설치하기 전에 실행된다. )

  • post-install: Helm 차트에 의해서 리소스들이 모두 설치 된 후에 실행된다. 

  • pre-delete: 릴리즈의 리소스를 삭제할때, 삭제 전에 실행된다.

  • post-delete:릴리즈의 리소스를 삭제한 후에, 실행된다.  

  • pre-upgrade: 릴리즈를 업그레이드 하기 전,  ( 템플릿이 렌더링 된 후에) 리소스가 생성되기 바로 전에 실행된다.

  • post-upgrade: 릴리즈 업그레이드가 끝난 후에, 실행된다. 

  • pre-rollback: 릴리즈를 기존 버전으로 롤백 할때 실행된다. (템플릿이 렌더링 된 후에 )

  • post-rollback: 릴리즈에 대한 롤백이 완료된 후에 실행된다. 

  • crd-install:  CRD 리소스를 인스톨하는데, 다른 Hook이나 기타 모든 다른 태스크 보다 우선적으로 실행된다. 이 Hook은 다른 리소스에서 이 CRD를  참조하여사용할때 주로 사용된다. 

  • test-success: “helm test” 명령을 실행할때 수행되는데, 그 결과가 성공 (return code == 0)일때만 실행된다. 

  • test-failure: “helm test” 명령을 실행할때 수행되는데, 그 결과가 실패 (return code != 0)일때만 실행된다. 

Hook weight & policy

Hook에는 실행 시점을 정의하는 기능 뿐만 아니라, 여러개의 Hook이 정의되었을때 실행 순서를 정하거나 또는 Hook 실행이 끝났을때 Hook 리소스를 지울지 나둘지등의 정책을 결정하는 기능이 있다. 

Hook weight

Hook weight는 여러개의 Hook 이 있을때, Hook 들의 실행 순서를 정의하기 위해서 사용한다. 음/양수 모두를 사용할 수 있으며, 작은 수를 가지고 있는 Hook 부터 우선 실행된다.

Hook weight는 아래 그림과 같이 annotations 부분에 “helm.sh/hook-weight” 항목으로 지정할 수 있다. 

 annotations:

    "helm.sh/hook-weight": "5"

Hook delete policy

Hook 이 실행된 후에, Hook 리소스들을 지울지 말지를 설정할 수 있는데, Hook이 실행된 후에, Hook 리소스는 일반적으로는 남아 있다. 만약에 Hook 이 실행된 후에, Hook을 삭제 하고 싶으면 annotation에 “helm.sh/hook-delete-policy”에 아래와 같이 삭제 정책을 정하면 된다. 


annotations:

    "helm.sh/hook-delete-policy": hook-succeeded 

삭제 정책으로 사용할 수 있는 정책은 다음과 같다. 

  • "hook-succeeded" : Hook 이 성공적으로 실행이 되고 나면, 삭제하도록 한다. 

  • "hook-failed" : Hook 실행이 실패하였을 경우 삭제 한다. 

  • "before-hook-creation" : Hook 을 실행하기 전에, 기존의 Hook을 삭제하고 실행하도록 한다. 


쿠버네티스 패키지 매니저 HELM

#2-2. Chart 버전과 릴리즈

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

업그레이드와 롤백

Chart로 의해서 설치된 리소스들을 쿠버네티스에서 릴리즈라고 한다.

예를 들어 같은 차트로 MySQL을 쿠버네티스 클러스터 내에 여러번 설치 할 수 있다.  각각 설치된 MySQL들을 릴리즈라고 하고, 릴리즈에 설정이 변경된 경우에, 차트를 수정해서 변경을 반영할 수 있다. 변경이 반영될때 마다 새로운 버전이 생성된다.


처음 helm install로 설치를 할때 --name 옵션으로 저장한 설치 이름이 릴리즈 명이 되고, 이 릴리즈를 업데이트 하고 싶으면 helm upgrade {Helm 릴리즈명} {차트 디렉토리} 를 실행하면, 해당 릴리즈를 업데이트하고, 새로운 버전을 생성한다.

앞의 예제의 values.yaml 에서 replicaCount: 3 를 변경한 후, upgrade를 해보자

%helm upgrade helloworld ./helloworld/

명령을 실행하면 다음과 같이 기존 배포가 업데이트 된다.


Release "helloworld" has been upgraded. Happy Helming!

LAST DEPLOYED: Sun Jun  9 20:58:41 2019

NAMESPACE: default

STATUS: DEPLOYED


RESOURCES:

==> v1/Service

NAME            AGE

helloworld-svc  2d


==> v1beta2/Deployment

helloworld-deployment  2d


==> v1/Pod(related)


NAME                                    READY STATUS RESTARTS AGE

helloworld-deployment-696fc568f9-2dkpm  0/1 ContainerCreating 0 0s

helloworld-deployment-696fc568f9-fqqg8  1/1 Running 0 2d

helloworld-deployment-696fc568f9-r8qct  1/1 Running 0 2d


그리고 위와 같이 Deployment pod가 3개로 늘어난것을 확인할 수 있다.

업그레이드 한후 버전을 확인하려면  helm history {릴리즈 이름} 을 사용하면 되는데,

% helm history helloworld

를 실행하면 아래와 같이 2번 버전이 새로 생긴것을 확인할 수 있다.


REVISION UPDATED                  STATUS     CHART            DESCRIPTION     

1        Fri Jun  7 23:24:34 2019 SUPERSEDED helloworld-0.1.0 Install complete

2        Sun Jun  9 20:58:41 2019 DEPLOYED   helloworld-0.1.0 Upgrade complete


만약 예전 버전으로 돌리고 싶으면 rollback 명령을 사용하면 되는데, helm rollback {릴리즈 이름} {릴리즈 버전} 으로 실행하면 된다. helm rollback helloworld 1 는 helloworld 릴리즈를 1 버전으로 롤백 하는 명령어 이다.  명령어를 실행해보면 다음과 같이 롤백이 완료되는 것을 확인할 수 있고,

% helm rollback helloworld 1

Rollback was a success! Happy Helming!


helm history로 해당 릴리즈의 버전을 확인해보면, 3번 버전에서 1번으로 롤백을 한것을 확인할 수 있다.

%helm history helloworld

REVISION UPDATED                  STATUS     CHART            DESCRIPTION     

1        Fri Jun  7 18:24:34 2019 SUPERSEDED helloworld-0.1.0 Install complete

2        Sun Jun  9 20:58:41 2019 SUPERSEDED helloworld-0.1.0 Upgrade complete

3        Sun Jun  9 21:43:48 2019 DEPLOYED   helloworld-0.1.0 Rollback to 1


릴리즈

앞에서도 설명했듯이 차트 하나로 같은 클러스터에 같은 애플리케이션을 여러개를 설치할 수 있다. MySQL이나 Redis 메모리 서버들은 애플리케이션이 아니기 때문에  같은 이미지로 여러개의 릴리즈를 설치할 수 있다.

그런데 앞의 스크립트로 helloworld 차트를 한번 더 설치하면 에러가 날것이다. 이유는 차트에 정의된 Service와 Deployment 리소스의 이름이 동일하기 때문이다.

templates/helloworld.yaml 파일에서 Deployment 이름 정의 부분을 보면, 이름을 {{.Value.name}}을 사용하도록 하였다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Values.name }}-deployment

:


그래서 values.yaml에서 name을 변경하지 않는 이상, Deployment는 같은 이름을 가지게 된다.

이 문제를 해결하기 위해서 리소스들의 이름을 릴리즈 이름을 사용하도록 하면 된다. 릴리즈 이름은 {{ .Release.Name}} 을 사용하면, 릴리즈 이름을 리소스 이름으로 사용할 수 있다. helloworld.yaml을 아래와 같이 수정하면 된다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Release.Name }}

spec:


그런데, 이렇게 설치가 된 리소스들이 어떤 차트에 의해서 설치된 것인지 구별이 잘 안될 수 있기 때문에, 좀더 좋은 방법은 리소스 이름을 “차트 이름-릴리즈 이름" 형태로 하는 것이 좋다. 차트 이름은 {{ .Chart.Name }} 을 사용하면 된다. 아래는 “차트 이름-릴리즈 이름" 형태로 정의한 예제이다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Chart.Name }}-{{ .Release.Name }}


helm template 명령을 이용해서 테스트를 해보자. 같은 예제를  helloworld2 디렉토리에 새롭게 만들었다. (그래서 아래 명령을 보면 ./helloworld2 디렉토리를 차트 디렉토리로 실행하였다.) 그리고 릴리즈 이름을 --name을 이용해서 myrelease로 지정하였다.


% helm template --name myrelease ./helloworld2

---

# Source: helloworld2/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: helloworld2-myrelease

spec:

 replicas: 3

:


metadata:

 name: helloworld-deployment

spec:

 replicas: 10

 minReadySeconds: 5

삭제

그리고 설치된 차트는 간단하게 helm delete 명령으로 삭제가 가능하다.

%helm delete helloworld

release "helloworld" deleted



쿠버네티스 패키지 매니저 HELM

#2-1 .Chart
조대협 http://bcho.tistory.com

Helm Chart

차트는 helm의 패키지 포맷으로, 하나의 애플리케이션을 설치하기 위한 파일들로 구성되어 있다. 예를 들어 tomcat을 설치하기 위한 쿠버네티스의 pod,service,deployment를 위한 YAML 파일등을 포함한다.

템플릿과 밸류

Helm 은 기본적으로 템플릿의 개념을 사용한다. 템플릿 파일을 만들어놓은 후에, 밸류 값을 채워 넣어서 쿠버네티스 리소스를 정의한 YAML 파일을 생성한다. 예제를 살펴보자

Helm 은 기본적으로 템플릿의 개념을 사용한다. 템플릿 파일을 만들어놓은 후에, 밸류 값을 채워 넣어서 쿠버네티스 리소스를 정의한 YAML 파일을 생성한다. 예제를 살펴보자.


먼저 templates/helloworld.yaml 파일을 정의한다.

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Values.name }}-deployment

spec:

 replicas: {{ .Values.replicaCount }}

 minReadySeconds: 5

 selector:

   matchLabels:

     app: {{ .Values.name }}

 template:

   metadata:

     name: {{ .Values.name }}-pod

     labels:

       app: {{ .Values.name }}

   spec:

     containers:

     - name: {{ .Values.name }}

       image: gcr.io/terrycho-sandbox/helloworlds:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

---

apiVersion: v1

kind: Service

metadata:

 name: {{ .Values.name }}-svc

spec:

 selector:

   app: {{ .Values.name }}

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer


그리고 ./values.yaml 파일을 아래와 같이 정의한다.


name: "helloworld"

replicaCount: 3


템플릿은 값을 채울 수 있는 말 그대로 템플릿이고, Value의 값을 이용해서 값을 채운다.

이를 개념적으로 표현해보면 다음과 같은 형태가 된다.



좌측은 템플릿 파일이고, 템플릿에서 밸류값은 별도의 파일에 정의한다.

정의한 키/밸류 형식으로 name:”helloworld”로 정의하였고, replicaCount: 2로 정의하였다

그리고 이 밸류값을 불러들이기 위해서는 {{.Value.키이름}} 식으로 템플릿내에 정의한다.

위의 예제에서 보면 name등에 {{.Value.name}} 으로 정의하였고, replicas 수는 {{ .Value.replicaCount }} 로 정의한다.

이렇게 정의된 템플릿에 밸류 내용을 정의하면 오른쪽 처럼 YAML 파일을 생성할 수 있다.


외부에서 Value를 받는 방법

Value값을 values,yaml에 지정해놨지만, 설치에 따라서 각 값을 변경하고 싶은 경우가 있다. 예를 들어 replicaCount를 3이 아니라 10으로 변경하고 싶을 경우에는 values.yaml 파일을 일일이 에디트 해야 한다. 나중에, 차트를 차트 리파지토리에 등록하기 위해서는 압축된 파일 형태를 사용하는데, 이 경우에는 그러면 차트 압축 파일을 다운로드 받은 후에 압축을 풀고나서, 내용을 수정하고 설치에 사용해야 하는 불편함이 있다.

이렇게 일일이 수정하지 않고 CLI에서 변경하고 싶은 인자만 간단하게 지정할 수 있는 방법이 없을까?

helm install 이나 upgrade시에  --set 옵션을 사용하면 된다. 예를 들어 values.yaml에 정의된 replicaCount를 10으로 변경하고자 하면 다음과 같이 하면 된다. %helm template --name myrelease --set replicaCount=10 ./helloworld


아래는 template 명령을 이용해서 테스트한 결과이다.   replicas가 10으로 변경된것을 볼 수 있다.

---

# Source: helloworld/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment


만약 설정값이 많아서 --set 을 이용해서 parameter로 넘기기가 어렵다면, 필요한 변수만 파일로 만들어서 넘길 수 있다.

예를 들어 myvalue.yaml 에 아래와 같이 name만 “fromValuefile” 로 정의를 한후에,


name: "fromValuefile"


helm install에서 -f 옵션으로 value 파일을 지정할 수 있다.

%helm install -f myvalues.yaml --name newrelease --dry-run --debug ./helloworld


결과를 보면 다음과 같다.

# Source: helloworld/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: fromValuefile-deployment

spec:

 replicas: 3

:

myvalues.yaml에 지정한 name에 의해서 Deployment name이  fromValuefile-deployment로 변경되고, replica 수는 원래 values.yaml에 지정한대로 3을 사용한 것을 확인할 수 있다.


디렉토리 구조

개념을 이해 하였으면, 파일을 어디에 저장하는지 디렉토리 구조를 살펴보자, helloworld라는 차트를 만들 것인데, helloworld라는 디렉토리를 만든다.

그리고 그 아래 템플릿들은 templates 라는 디렉토리에 yaml 로 정의한다. 여기에 채울 value들은 helloworld/values.yaml 파일내에 저장한다.



그리고 helloworlds/Chart.yaml 이라는 파일을 생성해야 하는데, 이 파일에는 이 헬름 차트에 대한 버전이나 작성자, 차트 이름과 같은 메타 정보를 정의한다.


다음은 Chart.yaml 의 내용이다.

apiVersion: v1

appVersion: "1.0"

description: A Helm chart for Kubernetes

name: helloworld

version: 0.1.0

테스트(검증)

헬름 차트 작성이 끝났으면 제대로 작동하는지 검증을 해볼 수 있다. 먼저 문법적인 오류가 없는지 확인 하는 명령은 helm lint 명령을 사용하면 된다. 명령어 실행은 차트 디렉토리 위에서 해야 한다. 이 예제에서는 ../helloworld 디렉토리가 된다.

실행하면 다음과 같은 결과를 볼 수 있다.


%helm linit ./helloworld

==> Linting ./helloworld/

[INFO] Chart.yaml: icon is recommended


1 chart(s) linted, no failures


문법적인 오류가 없는지 점검을 해준다. 다음으로 탬플릿에 밸류가 제대로 적용되서 원하는 YAML을 제대로 생성해나가는지 검증해야 하는데, helm template이라는 명령어를 사용하면 된다. helm lint 명령과 마찬가지로 차트가 저장된 디렉토리의 상위 디렉토리 (../helloworld)에서 실행한다.

다음은 helm template 명령을 실행한 결과이다.


%helm template ./helloworld

# Source: helloworld/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: helloworld-deployment

spec:

 replicas: 2

 minReadySeconds: 5

 selector:

   matchLabels:

     app: helloworld

 template:

   metadata:

     name: helloworld-pod

     labels:

       app: helloworld

   spec:

     containers:

     - name: helloworld

       image: gcr.io/terrycho-sandbox/helloworlds:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


내용 처럼 Value.name과  Value.replicaCount 값이 채워져서 Deployment 리소스에 대한 YAML 파일이 생성된것을 확인할 수 있다.

helm template 명령을 helm 클라이언트에서 tiller 서버 접속없이 template에 값이 채워지는지만 테스트를 하는 것이고, tiller 서버에 연결해서 테스트 할 경우에는 helm install --dry-run 옵션을 사용한다.  그리고, 내용을 확인하기 위해서 --debug 옵션을 추가한다.

helm install --name myrelease --dry-run --debug ./helloworld 이렇게 명령어를 사용하면 되는데, 장점은, 실제 서버에 연결해서 같은 릴리즈 버전이 있는지 등의 체크를 해주기 때문에, 실수를 막을 수 있다.

아래는 같은 릴리즈 버전으로 설치하는 것을 --dry-run으로 테스트한 결과이다.


% helm install --name myrelease --dry-run --debug ./helloworld

[debug] Created tunnel using local port: '56274'


[debug] SERVER: "127.0.0.1:56274"


[debug] Original chart version: ""

[debug] CHART PATH: /Users/terrycho/dev/workspace/kube/kubernetes-tutorial/31.helm/helloworld


Error: a release named myrelease already exists.

Run: helm ls --all myrelease; to check the status of the release

Or run: helm del --purge myrelease; to delete it


차트를 같은 이름(릴리즈명/뒤에서 다시 설명함)이 있기 때문에 설치할 수 없다 것을 테스트 단계에서 미리 확인할 수 있다.

실제 설치

그러면 생성한 헬름 차트를 이용해서 쿠버네티스에 리소스를 실제로 설치해보자. 설치 방법은 차트가 있는 디렉토리에서 helm install  명령을 이용해서 인스톨을 하면 된다. 이때 --name이라는 이름으로 설치된 차트 인스턴스의 이름을 설정해줘야 한다. 만약에 이름을 정해주지 않으면 임의의 이름이 자동으로 생성되어 사용된다.


%helm install --name helloworld ./helloworld/


명령을 실행하면 아래와 같이 deployment가 생성되는 것을 확인할 수 있다. 예제에서 설명은 하지 않았지만, 테스트에 사용된 코드에는 Service를 배포하는 부분이 함께 포함되어 있기 때문에 아래 실행결과를 보면 Service까지 같이 생성된것을 확인할 수 있다.


NAME:   helloworld

LAST DEPLOYED: Fri Jun  7 23:01:44 2019

NAMESPACE: default

STATUS: DEPLOYED


RESOURCES:

==> v1beta2/Deployment

NAME                   AGE

helloworld-deployment  1s


==> v1/Pod(related)


NAME                                    READY STATUS RESTARTS AGE

helloworld-deployment-696fc568f9-mc6mz  0/1 ContainerCreating 0 0s

helloworld-deployment-696fc568f9-nsw48  0/1 ContainerCreating 0 0s


==> v1/Service


NAME            AGE

helloworld-svc  1s


설치가 완료되었으면 리소스가 제대로 생성되었는지 확인을 해보기 위해서 kubectl get deploy 명령을 실행한다.


%kubectl get deploy

kubectl get deploy

NAME                    DESIRED CURRENT UP-TO-DATE   AVAILABLE AGE

helloworld-deployment   2 2 2         2 10m


위와 같이 Deployment 리소스가 생성 된것을 확인할 수 있다.

헬름을 통해서 설치된 차트들은 helm list 명령을 이용해서, 설치 상태를 확인할 수 있다.

helm list 명령을 실행해보면 아래와 같이 helloworld 차트가 설치된것을 확인할 수 있다.


%helm list

NAME               REVISION UPDATED                  STATUS   CHART            APP VERSION NAMESPACE

helloworld         1        Fri Jun  7 23:01:44 2019 DEPLOYED helloworld-0.1.0 1.0  




쿠버네티스 패키지 매니저 HELM

Helm의 일반적인 개념

Helm은 리눅스의 apt 툴이나, node.js의 npm과 같은 쿠버네티스용 패키지 매니지먼트 도구 이다. 일반적으로 하나의 소프트웨어를 쿠버네티스에 배포하려면, 간단하게 컨테이너만을 배포해서는 사용하기 어려운 경우가 많다. 외부로 IP를 노출 시키기 위해서 쿠버네티스 서비스를 배포해야 하고,쿠버네티스 pod를 관리할 deployment가 필요하며, 디스크 볼륨과 기타 정책등 부가 적인 부분을 추가로 배포해야 한다.

이미 네트워크나 디스크 설정이 완료된 상태에서 애플리케이션을 업데이트 하는 경우에는 쿠버네티스 deployment나 다른 배포 도구를 이용해도 되지만, 처음부터 모든 것을 설치해야 하는 반복적인 작업이 있는 경우에는 배포 도구로 불가능하다.

그래서 Helm은 애플리케이션 컨테이너 배포는 물론이고, 이에 필요한 쿠버네티스 리소스를 모두 배포해주는 역할을 하는데, 이 배포를 패키지 형태로 한다.

Infrastructure as a code (이하 IaaC)를 구현하는 Terraform + 패키지 매니저인 npm 정도의 개념이 합쳐져 있다고 보면 된다.




<Helm 개념도>

Helm의 구성을 보면 위의 그림과 같다.

먼저 CLI 툴인 클라이언트로 helm이 있다. 이 클라이언트는  Helm 서버 모듈과 통신을 하는데, Helm 서버를 Tiller라고 하고, Tiller는 쿠버네티스 클러스터 내에 설치된다.

helm 을 통해서 인스톨하는 패키지를 Chart라고 한다. Chart는 템플릿으로 설치하고자 하는 쿠버네티스 리소스의 설치 스크립트가 된다. 이 Chart들은 Helm Chart Repository에 저장할 수 있는다. Helm Chart repository는 HTTP server를 지원하는 서버로 Google Cloud Storage나 Git Hub page 또는 웹서버등을 사용할 수 있다.


Helm 설치

Helm 설치는 먼저 helm 클라이언트를 설치해야 한다. https://github.com/helm/helm 에 설치가이드가 나와 있는데, 맥 사용자의 경우 brew로 쉽게 설치가 가능하고, Linux의 경우 scoop등의 패키지 인스톨러를 이용하면 된다.


다음 helm 서버인 Tiller를 설치해야 한다.

Tiller 설치는 간단하게 다음 명령을 클라이언트에서 실행하면 된다. (이때, 이미 클라이언트에 kubectl 명령과 쿠버네티스 연결을 위한 설정이 다 되어있어야 한다.)

% helm init ---history-max 200

(Helm 설치 히스토리의 수를 정의해준다. 최대 히스토리 수를 정하지 않으면 히스토리가 무한으로 증가하게 된다.)


그러면 테스트로 MySQL를 Helm을 이용해서 설치해보자 먼저 Chart 저장되는 repository 를 최신으로 업데이트 한다.

%helm repo update 명령을 실행하면 된다.


다음 mySQL을 설치할것인데,

%helm install stable/mysql

명령을 실행하면 다음과 같이 MySQL이 설치된다. 설치가 끝나면 아래 결과와 같이 mySQL 연결을 테스틑 하는 방법과 ROOT 패스워드를 구하는 방법이 나온다.



위의 그림에서와 같이

%  MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default iron-kitten-mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo)

명령어를 실행하여, MySQL Root 패스워드를 알아낸다.

다음

% kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il

을 실행해서, 우분투 이미지 기반의 컨테이너를 하나 만든후에, bash로 접속을 한다.

접속이 된 상태에서

% apt-get update && apt-get install mysql-client -y

명령을 이용해서 MySQL 클라이언트를 설치하고

% mysql -h iron-kitten-mysql -p

를 실행하고 앞에서 구한 비밀 번호를 넣으면 아래  그림과 같이 MySQL에 접속이 된다.



Helm에 의해서 설치된 차트 목록을 확인하려면 아래와 같이

% helm list

를 실행하면 된다. 아래 그림에서 보면 앞에서 설치한 mySQL이 iron-kitten이라는 이름으로 설치가 되어있는 것을 확인할 수 있다.


   

만약 설치된 helm chart를 삭제하기 위해서는

%helm delete {Chart명} 을 하면된다.

앞에서 설치한 iron-kitten mysql chart를 삭제하려면

%helm delete iron-kitten

을 실행하면 아래 그림과 같이 삭제되는 것을 확인할 수 있다.



피쳐 크로싱 (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는 어떻게 일하는가?

조대협 (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



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


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을 이용해서 로그를 분석할 수 있다.



로깅 시스템 #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

}



로그 시스템 #1 - 자바 로그 프레임웍

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

로그 시스템

로그 시스템은 소프트웨어의 이벤트를 기록 함으로써, 소프트웨어 동작 상태를 파악하고 문제가 발생했을때 이 동작 파악을 통해서 소프트웨어의 문제를 찾아내고 해결하기 위해서 디자인 되었다.

주로 로그 파일이라는 형태로 하나의 파일에 이벤트들을 기록하였다.


그러나 소프트웨어 스택이 OS, 미들웨어, 사용자 애플리케이션 (자바나 파이썬등으로 구현된 애플리케이션)으로 점점 다중화되고 시스템이 대형화 되면서 한대가 아니라 여러대의 서버에 로그를 기록하고 또한 마이크로 서비스 아키텍처로 인하여 서버 컴포넌트가 분산됨에 따라서 로그를 수집해야할 포인트가 많아지게 되었다. 이로 인해서 로그 시스템이 분산 환경을 지원해야 할 필요가 되었고, 단순히 파일로 로그를 기록하는 것만으로는 이러한 여러 시스템과 다중 계층에 대한 모니터링이 불가능하게 되었다.


또한 데이터 분석의 중요성이 대두됨에 따라서, 에러등의 동작 파악성의 로그 뿐만 아니라 사용자의 액티버티를 수집하여 데이터 분석에 사용하기 위해서 데이터 수집 역시 로그 시스템을 통하기 시작하였다.


그래서 몇개의 글에 걸쳐서 좋은 로그 시스템을 개발하기 위한 아키텍처에 대해서 설명하고자 한다.

좋은 로그 시스템이란

먼저 좋은 로그 시스템의 기본 개념을 정의 해보면 다음과 같다.

  • 로그 메시지는 애플리케이션의 동작을 잘 이해할 수 있도록 충분히 구체적이어야 한다.

  • 로그 메시지를 기록하는데 성능 저하가 없어야 한다.

  • 어떤 배포 환경이라도 로그를 수집하고 저장할 수 있도록 충분히 유연해야 한다. (분산 환경 지원, 대용량 데이타 지원등)

자바 로깅 프레임워크

각 프로그래밍 언어마다 고유의 로깅 프레임워크을 지원하지만, 특히 자바의 경우에는 그 프레임웍 수가 많고 발전된 모델이 많아서 자바 프레임워크를 살펴보고 넘어가고자 한다.  

자바는 역사가 오래된 만큼 많은 로깅 프레임웍을 가지고 있다. log4j, logback, log4j2,apache common logging, SLF4J 등 다양한 프레임워크 들이 있는데, 그 개념과 장단점을 알아보도록 하자.

SLF4J

SLF4J는 (Simple Logging Facade for Java)의 약자로 이름이 뜻하는 것과 같이 로깅에 대한 Facade 패턴이다. SLF4J는 자체가 로깅 프레임웍이 아니라, 다양한 로깅 프레임웍을 같은 API를 사용해서 접근할 수 있도록 해주는 추상화 계층이다. 그래서 다른 로그프레임웍과 같이 사용해야 하는데, 보통 Log4J, Logback, Log4J2등이 많이 사용된다. 즉 애플리케이션은 SLF4J API 인터페이스를 통해서 호출하지만, 실제로 호출되는 로깅 프레임웍은 다른 프레임웍이 호출된다는 이야기이다. 이렇게 추상화를 통해서 용도와 목적에 맞게 다른 로깅 프레임워크 으로 쉽게 전환이 가능함은 물론이고, 로깅에 필요한 코드들을 추상화해주기 때문에, 훨씬 쉽고 간단하게 로깅이 가능하다. apache common logging 역시, SLF4J와 같이 다른 로깅 프레임워크 들을 추상화 해주는 기능을 제공한다.



<그림 : SLF4J 가 다른 로깅 프레임웍을 추상화 하는 개념도 >

출처 source


그러나 SLF4J 이전에 개발된 레거시 시스템들의 경우에는 이러한 추상화 계층이 없어서 로그 프레임웍을 변경하고 있기 때문에 로깅 프레임웍을 교체하기가 어렵다. 이런 상황을 해결하기 위해서 SLF4J는 기존 로그 프레임웍에 대한 브릿지를 제공한다. 예를 들어 log4J로 개발된 로깅을 브릿지를 이용해서 SLF4J를 사용하도록 전환할 수 있다. 이런 구조는 레거시 로깅 시스템을 사용해서 개발된 시스템에 대해서, 로그 프레임웍에 대한 코드를 변경하지 않고, 뒷단에 로그 프레임웍을 변경할 수 있게 해주기 때문에, 로깅 프레임웍에 대한 마이그레이션을 쉽게 해준다.



<그림 : SLF4J 브릿지를 이용해서, 기존 로그 시스템을 연동 하는 개념도 >


자바 로깅 프레임워크

자바 로그 프레임웍에는 여러가지 종류가 있지만 그중에서 대표적을 사용되는 로그 프레임웍은 log4j,logback,log4j2 세가지 이다.

Log4J

Log4J는 이 중에서 가장 오래된 로그프레임웍으로 로그 프레임웍에 대한 초반 개념을 설정했다고 볼 수 있다. 현재는 개발이 중지되고, Log4J2로 새로운 버전으로 변경되었다.

Logback

아마 현재 국내에서 가장 널리 많이 사용되고 있는 로그 프레임워크일것이다. Log4J 개발자가 개발한 로그 프레임워크로 주로 Log4J 성능 부분에 대한 개선 작업이 많이 이루어 졌다. SLF4J와 네이티브로 연동이 가능하다.

Log4J2

가장 근래에 나온 프레임워크로 Logback 보다 후에 나오고, 가장 빠른 성능을 제공한다. Logback과 SLF4J사이의 연동 문제를 해결하였으며 비동기 로깅 ( asynchronous logging ) 을 제공하여, 특히 멀티 쓰레드 환경에서 높은 성능을 제공한다.



(source : https://logging.apache.org/log4j/2.x/performance.html )


또한 근래의 로깅 시스템들은 로그를 파일로 기록하기 보다는 ELK(Elastic Search)나 Kafka 등 외부 시스템으로 로그를 전송하여 모으는 형태를 많이 취하기 때문에 이에 대한 연동을 Appender를 통해서 제공한다.


제공되는 Appender는 다음과 같다.

  • Console

  • File, RollingFile, MemoryMappedFile

  • Flume, Kafka, JDBC, JMS, Socket, ZeroMQ

  • SMTP (emails on errors, woo!)

  • … much more


만약에 새로운 시스템을 개발한다면, Logback 보다는 그 다음 세대인 격인 Lob4j2를 사용하는 것을 권장한다.

개발자용 노트북 구입기

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


집에서 개발 공부와, 테스트를 위해서 개인 노트북이 필요했는데, 아무래도 업무와 개인 공부를 분리하는것이 좋을것 같아서, 얼마전 부터 개인 노트북을 사용하기 시작했다.


여러가지 고민을 하다가, 딸에게 주었던 한성 U35S를 돌려받고, 딸에게는 ASUS 라이젠 노트북을 사주었다.



I7에 16G 메모리, 128G SSD로 나쁘지 않은 스펙이었지만, 대략 7년정도 된 노트북이라 우분투를 깔고 사용은 했지만, 일반적인 문서 작업이나 코딩들도 가능했지만, 아무래도 서버를 여러개 올리거나 컴파일을 많이 거니, 팬 소리가 너무 귀에 거슬리기 시작했고, 이참에 노트북을 하나 바꿔야 하겠다하고 개발용 노트북을 찾기 시작했다.


아무래도 개발용으로 사용할 노트북이기 때문에, 성능이 좋아야 하고, 쿠버네티스등 여러 서버를 띄울 것이기 때문에, 메모리가 충분해야 한다 (32G), 디스플레이는  개인적으로 IPS를 선호하고, IO는 nvme 디스크를 정해놓고 찾기 시작했다.


고 스펙으로 검색을 하다보니, 게이밍 노트북이 주로 올라왔는데, 이런 노트북은 I7-8550H (6코어)를 지원하는데다가, GTX-1050 TI나 1060 GPU를 지원하였다. 아무래도 GPU 지원만 잘된다면, 텐서플로우나 케라스같은 머신러닝 작업에 적합할것이라고 생각했다.


카페에서도 가끔 코딩을 하기 때문에 노트북이 너무 게이밍 스러우면 안되고, 그래서 좁혀진것이 레너보 Y530과 한성 TFG155 모델이었다.


예전과 다르게 요즘 랩탑들은 우분투 호환이 안되는 것들이 생각보다 많고 멀티 부트가 안되는 모델들이 많다. 특히 일반 노트북들은 크게 문제가 없지만 GTX GPU가 들어가는 경우에는 듀얼 부트가 문제가 많았다.



<그림. 레노버 Y530>


Y530은 다양한 모델 선택이 가능하고, 디자인도 괜찮은 편이고 냉각팬도 많이 달려있지만, 해외 커뮤니티를 살펴보니 우분투 설치가 쉽지는 않았지만, 여러 설정값을 조정하면 설치가 가능하다는 것을 찾았지만, 듀얼 부팅이 안되는 문제점이 있었다. 그리고 IPS 패널이기는 하지만  3 종류의 IPS 패널을 지원하는데, 60HZ, 250 nits 모델은 화면이 누렇다는 이야기가 많았다. 가격도 좋았지만, 결국 윈도우만 돌리고 있을거 같아서 아쉽지만 패스




<그림. 한성 TFG155>


다음 모델은 TFG155  모델인데, 알루미늄 상판에 디자인되 괜찮고, 스펙도 훌륭하다. 그러나 역시 누런 IPS 문제가 고질적인데, 이건 순전히 뽑기 운에 달려있고, 무엇보다 리눅스 설치에 성공했다는 사용자가 없었다. 특히 국내 내수용 모델이기 때문에 해외에서 몬가 관련된 문서도 없었다.  발열 문제도 꽤 심각한걸로 보이고, U35S를 기존에 사용해본 결과, 내구성에 문제가 있다고 판단하고 패스

(2019년 4/13일 추가. 페이스북에 보니, 한성 장비에 우분투 18.04 LTS 설치하신분 글이 올라왔다. 무지 삽질은 하셨다고 하지만 일단은 가능한것으로 알려졌으니 추가)


6 코어 CPU를 가지고 싶었지만, 개발용 장비가 필요한거지 우분투 드라이버나 커널설정에 시간을 보내는게 목적이 아닌만큼, 6 코어를 포기하고 4 코어 제품을 살펴보기 시작하였다.

ASUS 노트북이 가격도 저렴하고 디자인도 깔끔해서 물망에 오르기는 했지만, nvme를 지원하지 않고, 16G 메모리까지 밖에 확장이 안되는 이유로 패스.


리누즈 토발즈가 DELL XPS 시리즈를 사용한다고 해서 살펴보았는데, 역시나이다.

디자인도 깔끔하고 9750 CPU를 지원하고, 우분투 설치도 보장이 된다.


결국은 가격이 문제 260만원이다.. 아.. 가지고 싶지만. 다른 랩탑보다 100~150만원은 비싼듯.. 이 돈이면 애들 학원을 하나 더 보내지..

그래서 하급 기종을 살펴보니 CPU를 8300까지 내려고 200만원..




아무리 스펙을 내려봐도 답이 안나온다. 그나마 미국에서는 가격이 저렴한 모델도 있고 Developer Edition이라고 따로도 있는데.. 한국에는 수입이 안된다. (DELL 관계자 여러분 Developer Edition좀 수입 해주시면 안되실까요? ) 개발자 전용 모델이라 스펙이 정말 개발용으로 딱이다. 미국 출장가서 사서 핸드캐리할까도 생각했지만, 글로벌 AS 가 안된다고 들어서.. 그냥 포기..


그래서 우분투에서 인증 받은 노트북을 찾아보니, 그나마 인스피리언 7580 시리즈가 눈에 들어왔다.

삼성, LG를 제외한 한성이나 다른 브렌드들은 AS가 안좋다는 말이 많은데, DELL은 AS가 좋기로 소문이 나있었고, 공식 우분투 지원이라니 괜히 모험을 할 필요가 없었다.


<그림. Ubuntu 18.04 LTS 지원 델 노트북 목록 >

https://certification.ubuntu.com/desktop/models/?query=&category=Laptop&level=Any&release=18.04+LTS&vendors=Dell

i7 코어에, 32G 메모리 확장 가능, NVME 탑재, IPS 디스플레이, 알루미늄 바디. 덤으로 MX150 GPU까지. 고성능 딥러닝 학습은 불가능하겠지만, 아쉬운데로 쓸 수 있을듯.

그리고 DELL 은 공식적으로 우분투를 지원하는지라, 윈도우와 듀얼 부트 가이드까지 공식적으로 지원한다. https://www.dell.com/support/article/us/en/04/sln301754/dell-pc%EC%97%90%EC%84%9C-ubuntu-%EB%B0%8F-windows-8-%EB%98%90%EB%8A%94-10%EC%9D%84-%EB%93%80%EC%96%BC-%EB%B6%80%ED%8C%85%EC%9C%BC%EB%A1%9C-%EC%84%A4%EC%B9%98-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95?lang=ko


설치 스트레스가 훨씬 덜할듯. 네이버에 후기를 찾아보니.. 헐.. 이 노트북 후기는 거의 없다. 110~120만원대에서는 약간 포지셔닝이 애매할듯. 보통 유사 스펙이면 이것보다 가격이 살짝 낮은데.. 일반적인 사용자들이 메모리 32G에 NVME를 꼽을일은 거의 없다. 그정도 스펙이면 게이밍인데, 대부분 게이밍 노트북으로 넘어가서 GTX GPU를 쓰겠지... (국내 노트북 시장은 개발자 마켓들을 따로 안보나보다..)


<그림 DELL Inspiron 7580 핑크 버전. 한국은 안판다.>


거기다 행사 기간이라고 무선 마우스까지 덤이다. 이것도 제대로 된거 사려면 3~4만원은 줘야 하는데, 윈도우 라이센스는 집에 정품이 있기에 필요는 없지만 그래도 윈도 프로가 들어있다.

그런데 판매하는 제품은 메모리 16 G 옵션밖에 없고 디스크도 128 SSD + 1TB HDD 또는 512 G 메모리만 지원이 된다. 250G 정도면 딱인데…

메모리 스펙을 찾아보니, 대략 16G 삼성 정품으로 10만원 정도면 구입이 가능한데, 인터넷에서는 8G 추가하는데 10만원(??) 호갱인가??? 그리고, 256 삼성 nvme도 10만원이면 따로 구입이 가능하다.

128SSD + 1TB → 512 모델 업그레이드가 10만원 차이인데… 디스크 모자르면 직접 사서 바꿀것으로 생각하고, 그냥 16GB 메모리 두개만 추가해서 구입.. (남는 8G는 당근 마켓에 싸게 팔아버릴 요량..)


발열로 인하 소음도 걱정되고, 모니터와 같이 병행 사용할것이라서 잘만 스탠드도 2만원주고 업글하고,

코딩할때 화면이 적어서 모니터도 하나 구입하기로 하고, 32인치를 사고 싶었지만.. 책상이 좁고 가격 때문에 27인치 결정. IPS에 무결점 그리고 책상 두께만 많이 차지 않으면 괜찮아서, 무난하게 알파 스캔 모니터 주문. 결정적으로 모니터암을 달 수 없는 구조지만.. 그냥 싸니까..



이렇게 해서,

DELL 7580 I7, 8G 메모리, 128 nvme + 1TB 디스크 + 마우스는 위메프에서 할인 쿠폰이 나와서 다나와 보다 싸게 115만원

삼성 DDR4 16G 메모리 2개 = 22만원

27인치 알파스캔 모니터 = 25만원

잘만 쿨러 24000원


이렇게 해서 대략 165만원 정도. 32 기가, 27인치 IPS 모니터, 알루미늄 바디 노트북 세트로 이정도 가격이면 대단히 선방한듯..

다음에는 더 많이 벌어서. 리누즈 토발즈 처럼 XPS15 풀스펙 모델을 쓸 수 있기를 바라며..

(4/13 추가. 모니터 자체는 크게 문제는 없는데, 노트북은 15인치인데, 모니터가 27인치다 보니 둘이 글자 크기가 너무 난다. 27인치로 하려면 더 고해상도를 쓰거나 아니면 모니터를 24인치로 하는 선택이 더 좋았을듯)


===

업데이트 노트북이 도착해서 인스톨 작업을 진행하였다.

128G SSD에 우분투를 인스톨하고 윈도우 듀얼 붓을 사용하려니 윈도우 파티션이 이것저것 나눠지고 깔리고 복잡해서 용량이 모잘라서 결국은 삼성 EVO 970 PRO를 추가 주문하였다. (+20만원). 돈 아끼려다가.. 그냥 초반에 512 NVME SSD (10만원추가)를 주문하는 것을 추천한다.


다음 인스톨 과정인데, 바이오스에서 RAID를 제외하고 ACHI모드인가로 인식하면 인식은 잘 된다. 다음 윈도우를 다시 인스톨 해야 하는데, 델 사이트에서 제공하는 리커버리 윈도우즈 이미지를 사용하면, 설치가 안된다. (디스크에 대한 드라이버를 찾지 못한다. ) 한참을 씨름하다가 마이크로 소프트의 윈도우즈 정식 이미지를 다운 받아서 사용하니 바로 인식도 되고 한번에 인스톨이 된다. (https://www.microsoft.com/ko-kr/software-download/windows10ISO)


트랙패드, 마우스, 디스플레이 모두 완벽하게 작동한다. 단 돌다가 가끔 커널 패닉에 빠지면서 멈추는데..

이건 NVIDIA GPU와 충돌 때문에 발생한다. 소프트웨어 업데이트 메뉴에 가서 드라이버 업데이트 메뉴를 보면  NVIDIA  드라이버 업데이트가 가능하다.

업데이트 후 문제없이 작동한다.





'사는 이야기' 카테고리의 다른 글

개발자 코딩 노트북 구입기 (DELL Inspiron 7580) 우분투  (6) 2019.03.10
블로그 600만 돌파  (1) 2018.12.10
블로그 500만 돌파  (0) 2018.05.10
지난 1년 회고  (0) 2017.09.20
블로그 400만 돌파  (2) 2017.07.17
2016년 업무 종료....  (3) 2016.12.29