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


Archive»


 

'소개'에 해당되는 글 41

  1. 2018.08.11 쿠버네티스 #15 - 모니터링 (3/3) 구글 스택드라이버를 이용한 쿠버네티스 모니터링
  2. 2018.07.16 쿠버네티스 #13 - 모니터링 (1/2)
  3. 2018.06.11 쿠버네티스 #5 - 디스크 (볼륨/Volume) (1)
  4. 2018.05.30 쿠버네티스 #3- 개념이해 (2/2) 컨트롤러
  5. 2018.05.21 쿠버네티스 #2 - 개념 이해 (1/2)
  6. 2018.04.15 Circuit breaker 패턴을 이용한 장애에 강한 MSA 서비스 구현하기 #2 - Spring에서 Circuit breaker 구현
  7. 2017.07.15 데이타 워크플로우 관리를 위한 Apache Airflow #1 - 소개
  8. 2017.06.15 연예인 얼굴 인식 모델을 만들어보자 - #2. CNN 모델을 만들고 학습시켜 보자 (11)
  9. 2017.01.31 텐서 보드를 이용하여 학습 과정을 시각화 해보자
  10. 2016.11.30 머신러닝의 과학습 / 오버피팅의 개념 (1)
  11. 2016.10.04 수학포기자를 위한 딥러닝-#2 머신러닝 개념 이해 (6)
  12. 2016.09.22 노트7의 소셜 반응을 분석해 보았다. - #3 제플린 노트북을 이용한 상세 데이타 분석
  13. 2016.09.01 파이어베이스 애널러틱스를 이용한 모바일 데이타 분석- #3 빅쿼리에 연동하여 모든 데이타를 분석하기
  14. 2016.08.29 파이어베이스 애널러틱스를 이용한 모바일 데이타 분석 #1-Hello Firebase (3)
  15. 2016.07.13 구글 클라우드의 대용량 분산 큐 서비스인 Pub/Sub 소개 #1
  16. 2016.06.18 빅쿼리-#3 데이타 구조와 접근(공유) (3)
  17. 2016.06.16 구글 빅데이타 플랫폼 빅쿼리 아키텍쳐 소개
  18. 2016.06.15 구글 빅데이타 플랫폼 빅쿼리(BIGQUERY)에 소개
  19. 2016.06.13 분산 로그 & 데이타 수집기 Fluentd (2)
  20. 2016.06.01 구글 클라우드 플랫폼 제품 소개
 


쿠버네티스 #15

모니터링 3/3 구글 스택드라이버를 이용한 모니터링

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



구글 클라우드 쿠버네티스 스택드라이버 모니터링

쿠버네티스 모니터링 시스템을 구축하는 다른 방법으로는 클라우드 서비스를 사용하는 방법이 있다. 그중에서 구글 클라우드에서 제공하는 스택 드라이버 쿠버네티스 모니터링에 대해서 소개하고자한다.

https://cloud.google.com/monitoring/kubernetes-engine/


현재는 베타 상태로, 구글 클라우드 쿠버네티스 서비스 (GKE)에서만 지원이 되며, 쿠버네티스 버전 1.10.2 와 1.11.0 (또는 그 상위버전)에서만 지원이 되고, 모니터링 뿐 아니라, 쿠버네티스 서비스에 대한 로깅을 스택드라이버 로깅 서비스를 이용해서 함께 제공한다.


스택드라이버 쿠버네티스 모니터링을 설정하는 방법은 간단하다. 쿠버네티스 클러스터를 설정할때, 아래 그림과 같이 Additional features 항목에서 “Try the new Stackdriver beta monitoring and Logging experience” 항목을 체크하면 된다.



클러스터를 생성한 후에, 구글 클라우드 콘솔에서 Monitoring 메뉴를 선택한 후에



스택드라이버 메뉴에서 Resources 메뉴에서 아래 그림과 같이 Kubernetes 메뉴를 선택하면 쿠버네티스 모니터링 내용을 볼 수 있다.



모니터링 구조

스택드라이버 쿠버네티스 모니터링의 가장 큰 장점 중의 하나는 단순한 단일 뷰를 통해서 대부분의 리소스 모니터링 과 이벤트에 대한 모니터링이 가능하다는 것이다.

아래 그림이 스택드라이버 모니터링 화면인데, “2”라고 표시된 부분이 시간에 따른 이벤트이다. 장애등이 발생하였을 경우 아래 그림과 같이 붉은 색으로 표현되고, 3 부분을 보면, 여러가지 뷰 (계층 구조)로 각 자원들을 모니터링할 수 있다. 장애가 난 부분이 붉은 색으로 표시되는 것을 확인할 수 있다.



<출처 : https://cloud.google.com/monitoring/kubernetes-engine/observing >


Timeline에 Incident가 붉은 색으로 표시된 경우 상세 정보를 볼 수 있는데, Timeline에서 붉은 색으로 표시된 부분을 누르면 아래 그림과 같이 디테일 이벤트 카드가 나온다. 이 카드를 통해서 메모리,CPU 등 이벤트에 대한 상세 내용을 확인할 수 있다.



<출처 : https://cloud.google.com/monitoring/kubernetes-engine/observing >


반대로 정상적인 경우에는 아래 그림과 같이 이벤트 부분에 아무것도 나타나지 않고, 모든 자원이 녹색 동그라미로 표시되어 있는 것을 확인할 수 있다.


개념 구조

쿠버네티스 모니터링중에 어려운 점중의 하나는 어떤 계층 구조로 자원을 모니터링 하는가 인데, 이런점을 해결하기 위해서 구글 스택드라이버 쿠버네티스 모니터링은 3가지 계층 구조에 따른 모니터링을 지원한다. 모니터링 화면을 보면 아래와 같이 Infrastructure, Workloads, Services 와 같이 세가지 탭이 나오는 것을 볼 수 있다.



어떤 관점에서 클러스터링을 모니터링할것인가인데,

  • Infrastructure : 하드웨어 자원 즉, node를 기준으로 하는 뷰로,  Cluster > Node > Pod > Container 의 계층 구조로 모니터링을 제공한다.

  • Workloads : 워크로드, 즉 Deployment를 중심으로 하는 뷰로 Cluster > Namespace > Workload (Deployment) > Pod > Container 순서의 계층 구조로 모니터링을 제공한다.

  • Services : 애플리케이션 즉 Service 를 중심으로 하는 뷰로 Cluster > Namespace > Service > Pod > Container 계층 순서로 뷰를 제공한다.

Alert 에 대한 상세 정보

각 계층 뷰에서 리소스가 문제가 있을 경우에는 앞의 동그라미가 붉은색으로 표시가 되는데,  해당 버튼을 누르게 되면, Alert 에 대한 상세 정보 카드가 떠서, 아래 그림과 같이 이벤트에 대한 상세 정보를 확인할 수 있다.


<출처 : https://cloud.google.com/monitoring/kubernetes-engine/observing >

결론

지금까지 간단하게 쿠버네티스에 대한 모니터링과 로깅에 대해서 알아보았다. 프로메테우스나 그라파나와 같은 최신 기술을 써서 멋진 대쉬 보드를 만드는 것도 중요하지만 모니터링과 로깅은 시스템을 안정적으로 운영하고 장애전에 그 전조를 파악해서 대응하고, 장애 발생시에는 해결과 향후 예방을 위한 분석 및 개선 활동이 일어나야 한다. 이를 위해서 모니터링과 로깅은 어디까지나 도구일 뿐이고, 어떤 지표를 모니터링 할것인지 (SLI : Service Level Indicator), 지표의 어느값까지를 시스템 운영의 목표로 삼을 것인지 (SLO : Service Level Object)를 정하는 프렉틱스 관점이 더 중요하다.  이를 구글에서는 SRE (Site Reliability Engineering)이라고 하는데, 이에 대한 자세한 내용은 https://landing.google.com/sre/book.html 를 참고하기 바란다.

이런 프렉틱스를 구축하는데 목적을 두고, 모니터링을 위한 툴링등은 직접 구축하는 것보다는 클라우드에서 제공하는 스택 드라이버와 같은 솔루션이나 데이타독(Datadog)와 같은 전문화된 모니터링 툴로 구축을 해서 시간을 줄이고, 프렉틱스 자체에 시간과 인력을 더 투자하는 것을 권장한다.




쿠버네티스 #13

모니터링 1/2


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


시스템을 운영하는데 있어서 운영 관점에 있어서 가장 중요한 기능중의 하나는 시스템에 대한 모니터링이다. 시스템 자원의 사용량이나 에러등에 대한 모니터링을 통해서, 시스템을 안정적으로 운영하고 문제 발생시 원인 파악과 대응을 할 수 있다.

이번 글에서는 쿠버네티스 모니터링 시스템에 대한 개념과, 아키텍쳐 그리고 구축 방법에 대해서 소개하고자 한다.

쿠버네티스 모니터링 컨셉

쿠버네티스에 대한 모니터링을 보면 많은 툴과 지표들이 있어서 혼돈하기 쉬운데, 먼저 모니터링 컨셉에 대한 이해를 할 필요가 있다.

쿠버네티스 기반의 시스템을 모니터링하기 위해서는 크게 아래와 같이 4가지 계층을 모니터링해야 한다.



1. 호스트 (노드)

먼저 쿠버네티스 컨테이너를 실행하는 하드웨어 호스트 즉 노드에 대한 지표 모니터링이 필요하다. 노드의 CPU,메모리, 디스크, 네트워크 사용량과, 노드 OS와 커널에 대한 모니터링이 이에 해당한다.

2. 컨테이너

다음은 노드에서 기동되는 각각의 컨테이너에 대한 정보이다. 컨테이너의 CPU,메모리, 디스크, 네트워크 사용량등을 모니터링 한다.

3. 애플리케이션

컨테이너안에서 구동되는 개별 애플리케이션의 지표를 모니터링 한다. 예를 들어, 컨테이너에서 기동되는 node.js 기반의 애플리케이션의 응답시간, HTTP 에러 빈도등을 모니터링한다.

4. 쿠버네티스

마지막으로, 컨테이너를 컨트롤 하는 쿠버네티스 자체에 대한 모니터링을한다. 쿠버네티스의 자원인 서비스나 POD, 계정 정보등이 이에 해당한다.

쿠버네티스 기반의 시스템 모니터링에 대해서 혼돈이 오는 부분중의 하나가 모니터링이라는 개념이 포괄적이기 때문이다. 우리가 여기서 다루는 모니터링은 자원에 대한 지표 대한 모니터링이다. 포괄적인 의미의 모니터링은 로그와, 에러 모니터링등 다양한 내용을 포괄한다.  

쿠버네티스 로깅

지표 모니터링과 함께 중요한 모니터링 기능중 하나는 로그 수집 및 로그 모니터링이다.

로그 수집 및 로그 모니터링 방법은 여러가지 방법이 있지만, 오픈소스 로그 수집 및 모니터링 조합인 EFK (Elastic search + FluentD + Kibina) 스택을 이용하는 경우가 대표적이다.

Fluentd 에이전트를 이용하여, 각종 로그를 수집하여, Elastic search에 저장하고, 저장된 지표를 Kibana 대쉬 보들르 이용하여 시작화 해서 나타내는 방법이 있다.

이에 대한 자세한 설명을 생략한다.

쿠버네티스 모니터링 시스템 구축

그러면 이러한 모니터링 시스템을 어떻게 구축할 것인가?

쿠버네티스 모니터링은 버전업 과정에서 많은 변화를 겪고 있다. 기존 모니터링 시스템의 아키텍쳐는 cAdvisor,Heapster를 이용하는 구조였으나, 이 아키텍쳐는 곧 deprecated 될 예정이고, Prometheus등 다양한 모니터링 아키텍쳐가 후보로 고려 되고 있다.

아래 그래프를 보면 재미있는 통계 결과가 있는데, cAdvisor,Heapster,Promethus 를 이용하는 방법도 있지만, 클라우드의 경우에는 클라우드 벤더에서 제공하는 쿠버네티스 모니터링 솔루션을 그대로 사용하거나 (18%) 또는 데이타독이나 뉴렐릭 (Datadog, newRelic)과 같이 전문화된 모니터링 클라우드을 사용하는 비율 (26%) 도 꽤 높다.



<그림. 쿠버네티스 모니터링 솔루션 분포 >

출처 :  https://thenewstack.io/5-tools-monitoring-kubernetes-scale-production/


개인적인 의견으로는 직접 모니터링 솔루션을 구축해서 사용하는 것보다는 비용은 약간 들지만 클라우드 벤더에서 제공되는 모니터링 도구나 또는 데이타독과 같은 전문 모니터링 솔루션을 이용하는 것을 추천한다.


직접 모니터링 솔루션을 구축할 경우 구축과 운영에 드는 노력도 꽤 크고, 또한 어떠한 지표를 모니터링해야할지 등에 대한 추가적인 노하우가 필요하다. 또한 cAdvisor,Heapster,Promethues 조합은 호스트와 컨테이너 그리고 쿠버네티스에 대한 모니터링은 제공하지만 애플리케이션 지표에 대한 모니터링과 로깅 기능은 제공하지 않기 때문에 별도의 구축이 필요하다. 이런 노력을 들이는 것 보다는 모든 기능이 한번에 제공되고 운영을 대행해주는 데이타독이나 클라우드에서 제공해주는 모니터링 솔루션을 사용하는 것을 추천한다.

Heapster 기반 모니터링 아키텍처

이러한 모니터링 요건을 지원하기 위해서, 쿠버네티스는 자체적인 모니터링 컴포넌트를 가지고 있는데, 그 구조는 다음과 같다.



<그림. 쿠버네티스 모니터링 시스템 아키텍쳐>

출처 Source : https://www.datadoghq.com/blog/how-to-collect-and-graph-kubernetes-metrics/


cAdvisor

cAdvisor는 모니터링 에이전트로, 각 노드마다 설치되서 노드에 대한 정보와 컨테이너 (Pod)에 대한 지표를 수집하여, Kubelet으로 전달한다.

Heapster

cAdvisor에 의해 수집된 지표는 Heapster 라는 중앙 집중화된 지표 수집 시스템에 모이게 되고, Heapster는 수집된 지표를 스토리지 백앤드에 저장한다.

Storage backend

Heapster가 지표를 저장하는 데이타베이스를 스토리지 백앤드라고 하는데, Heapster는 확장성을 위해서 다양한 스토리지 백앤드를 플러그인 구조를 선택하여 연결할 수 있다.

현재 제공되는 대표적인 스토리지 백앤드는 구글 클라우드의 모니터링 시스템인 스택드라이버 (stackdriver), 오픈 소스 시계열 데이타베이스인 인플럭스 디비 (InfluxDB) 등을 지원한다.

그래프 대쉬 보드

이렇게 저장된 모니터링 지표는 그래프와 같은 형태로 시각화 될필요가 있는데, 스토리지 백앤드를 지원하는 다양한 시각화 도구를 사용할 수 있다. 구글의 모니터링 시스템인 스택드라이버의 경우에는 자체적인 대쉬보드 및 그래프 인터페이스가 있고, 인플럭스 디비나 프로메테우스의 경우에는 오픈소스 시각화 도구인 그라파나(Grafana)를 사용할 수 있다.


<그림. 그라파나와 프로메테우스를 연결하여, 지표 모니터링을 시각화 한 예제>


그러나 이 아키텍쳐는 deprecation 계획이 시작되서 1.13 버전 부터는 완전히 제거될 예정이다.

https://github.com/kubernetes/heapster/blob/master/docs/deprecation.md


쿠버네티스 대시보드

다른 방법으로는 쿠버네티스를 모니터링 하고 관리할 수 있는 쉬운 방법이 하나 있는데, 쿠버네티스 대시보드를 사용하는 방법이다. 쿠버네티스는 기본적으로 kubectl이라는 커맨드 라인 인터페이스 (이하 CLI : Command Line Interface)를 사용하지만, 추가적으로 웹 기반의 관리 콘솔을 제공한다. 이를 쿠버네티스 대시보드라고 한다. (https://github.com/kubernetes/dashboard)

대시 보드 설치

쿠버네티스 대시 보드 설치 방법은 간단하다. 아래와 같이 대시보드 설정 yaml 파일을 이용하면 간단하게 대시 보드를 쿠버네티스 클러스터에 설치할 수 있다.


% kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml


일반적인 경우에는 위의 스크립트로 설치가 가능하지만, 구글 클라우드 쿠버네티스 엔진의의 경우에는 설치 중에 권한 관련 에러가 나올 수 있는데, 구글 클라우드 쿠버네티스 엔진의 경우에는 보안을 이유로 일반적인 쿠버네티스보다 권한 설정 레벨이 높게 설정되어 있기 때문이다. 구글 클라우드 쿠버네티스 엔진에서 대시보드를 설치하고자할때에는 위의 스크립트를 실행하기 전에 먼저 아래 명령어를 이용해서, 현재 사용자 계정에 대해서 cluster-admin 롤을 부여해줘야 한다.  


%kubectl create clusterrolebinding cluster-admin-binding \
--clusterrole cluster-admin --user $(gcloud config get-value account)

대시 보드 접속

대시보드 설치가 끝났으면, 대시보드를 접속해보자

대시보드는 외부 서비스로 제공되지 않고, 내부 IP로만 접속이 가능한데, 클러스터 외부에서 접근하려면 kubectl proxy를 이용하면, 간단하게 접근이 가능하다.

kubectl proxy는 로컬 머신 (예를 들어 노트북)과 쿠버네티스 클러스터간의 통신을 프록싱해줘서, 로컬 머신에서 쿠버네티스 클러스터내의 HTTP 서비스를 접근할 수 있도록 해준다.

사용 방법은 로컬 머신에서 간단하게

%kubectl proxy

명령을 실행해주면 localhost:8001 포트를 통해서 쿠버네티스 클러스터로 트래픽을 프록시 해준다.

위와 같이 proxy를 실행한후에,  아래 URL로 접근을 하면, 대시보드 콘솔에 접근할 수 있다.

http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/


URL에 접근하면 아래와 같이 로그인 창이 나타난다.



사용자 계정 및 토큰등에 대해서는 보안 부분에서 별도로 다루기로 하겠다.

대쉬보드를 사용하기 위해서는 사용자 인증이 필요한데, 간단하게 인증을 위한 토큰을 사용하는 방법을 이용하도록 하겠다.

토큰은 쿠버네티스 API 인증 메커니즘중의 하나로, 여기서는 admin-user라는 계정을 하나 만든후에, 그 계정에, 클러스터 관리자롤을 부여한 후에, 그 사용자의 토큰을 사용하는 방법을 사용하겠다.


먼저 아래 스크립트를 이용해서 admin-user 라는 사용자를 생성한다.

admin-user.yaml 파일

apiVersion: v1

kind: ServiceAccount

metadata:

 name: admin-user

 namespace: kube-system


다음 아래 스크립트를 이용해서 cluster-admin 롤을 앞에서 생성한 admin-user에 부여한다.

admin-rolebinding.yaml 파일

apiVersion: rbac.authorization.k8s.io/v1beta1

kind: ClusterRoleBinding

metadata:

 name: admin-user

roleRef:

 apiGroup: rbac.authorization.k8s.io

 kind: ClusterRole

 name: cluster-admin

subjects:

- kind: ServiceAccount

 name: admin-user

 namespace: kube-system


다음 아래 명령어를 이용하면 admin-user의 토큰 값을 알 수 있다.

% kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')


명령을 실행하면 아래와 같이 토큰이 출력된다.


이 토큰 값을 앞의 로그인 창에 입력하면, 대시보드에 로그인할 수 있다.

대시 보드에 로그인하면 아래와 같이 노드나, Pod, 서비스등 쿠버네티스의 자원의 대부분의 정보에 대한 모니터링이 가능하다.




또한 kubectl CLI 명령을 사용하지 않고도 손쉽게 Deployment 등 각종 자원을 생성할 수 있다.


로그 부분에 들어가면 아래와 같이 로그 정보를 볼 수 있다



재미있는 기능중 하나는 아래 그림과 같이 특정 Pod의 컨테이너를 선택하면, 웹콘솔상에서 해당 컨테이너로 SSH 로그인이 가능하다.



여기서 다룬 쿠버네티스 대시보드 설정 및 로그인 부분은 프록시 사용, 로그인을 토큰을 사용하는 등, 운영환경에는 적절하지 않은 방법이다. 개발환경이나 테스트 용도로만 사용하도록 하고, 운영 환경에서는 사용자 계정 시스템 생성과 적절한 권한 배정을 한 후에, 적절한 보안 인증 시스템을 마련한 후에 적용하도록 하자.



쿠버네티스 #4

Volume (디스크)

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


이번 글에서는 쿠버네티스의 디스크 서비스인 볼륨에 대해서 알아보도록 하겠다.

쿠버네티스에서 볼륨이란 Pod에 종속되는 디스크이다. (컨테이너 단위가 아님). Pod 단위이기 때문에, 그 Pod에 속해 있는 여러개의 컨테이너가 공유해서 사용될 수 있다.

볼륨 종류

쿠버네티스의 볼륨은 여러가지 종류가 있는데,  로컬 디스크 뿐 아니라, NFS, iSCSI, Fiber Channel과 같은 일반적인 외장 디스크 인터페이스는 물론, GlusterFS나, Ceph와 같은 오픈 소스 파일 시스템, AWS EBS, GCP Persistent 디스크와 같은 퍼블릭 클라우드에서 제공되는 디스크, VsphereVolume과 같이 프라이비트 클라우드 솔루션에서 제공하는 디스크 볼륨까지 다양한 볼륨을 지원한다.

자세한 볼륨 리스트는 https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes 를 참고하기 바란다.


이 볼륨 타입을 구별해보면 크게 임시 디스크, 로컬 디스크 그리고 네트워크 디스크 등으로 분류할 수 있다.


Temp

Local

Network

emptyDir

hostPath

GlusterFS

gitRepo

NFS

iSCSI

gcePersistentDisk

AWS EBS

azureDisk

Fiber Channel

Secret

VshereVolume


그럼 각각에 대해서 알아보도록 하자

emptyDir

emptyDir은 Pod가 생성될때 생성되고, Pod가 삭제 될때 같이 삭제되는 임시 볼륨이다.

단 Pod 내의 컨테이너 크래쉬되어 삭제되거나 재시작 되더라도 emptyDir의 생명주기는 컨테이너 단위가 아니라, Pod 단위이기 때문에, emptyDir은 삭제 되지 않고 계속해서 사용이 가능하다.

생성 당시에는 디스크에 아무 내용이 없기 때문에, emptyDir  이라고 한다.

emptyDir의 물리적으로 노드에서 할당해주는 디스크에 저장이 되는데, (각 환경에 따라 다르다. 노드의 로컬 디스크가 될 수 도 있고, 네트워크 디스크등이 될 수 도 있다.) emptyDir.medium 필드에 “Memory”라고 지정해주면, emptyDir의 내용은 물리 디스크 대신 메모리에 저장이 된다.


다음은 하나의 Pod에 nginx와 redis 컨테이너를 기동 시키고, emptyDir 볼륨을 생성하여 이를 공유하는 설정이다.


apiVersion: v1

kind: Pod

metadata:

 name: shared-volumes

spec:

 containers:

 - name: redis

   image: redis

   volumeMounts:

   - name: shared-storage

     mountPath: /data/shared

 - name: nginx

   image: nginx

   volumeMounts:

   - name: shared-storage

     mountPath: /data/shared

 volumes:

 - name : shared-storage

   emptyDir: {}


shared-storage라는 이름으로 emptyDir 기반의 볼륨을 만든 후에, nginx와 redis 컨테이너의 /data/shared 디렉토리에 마운트를 하였다.


Pod를 기동 시킨후에, redis 컨테이너의 /data/shared 디렉토리에 들어가 보면 당연히 아무 파일도 없는 것을 확인할 수 있다.

이 상태에서 아래와 같이 file.txt 파일을 생성하였다.



다음 nginx 컨테이너로 들어가서 /data/shared 디렉토리를 살펴보면 file.txt 파일이 있는 것을 확인할 수 있다.



이 파일은 redis 컨테이너에서 생성이 되어 있지만, 같은 Pod 내이기 때문에, nginx 컨테이너에서도 접근이 가능하게 된다.

hostPath

다음은 hostPath 라는 볼륨 타입인데, hostPath는 노드의 로컬 디스크의 경로를 Pod에서 마운트해서 사용한다. 같은 hostPath에 있는 볼륨은 여러 Pod 사이에서 공유되어 사용된다.

또한  Pod가 삭제 되더라도 hostPath에 있는 파일들은 삭제되지 않고 다른 Pod가 같은 hostPath를 마운트하게 되면, 남아 있는 파일을 액세스할 수 있다.


주의할점 중의 하나는 Pod가 재시작되서 다른 노드에서 기동될 경우, 그 노드의 hostPath를 사용하기 때문에, 이전에 다른 노드에서 사용한 hostPath의 파일 내용은 액세스가 불가능하다.


hostPath는 노드의 파일 시스템을 접근하는데 유용한데, 예를 들어 노드의 로그 파일을 읽어서 수집하는 로그 에이전트를 Pod로 배포하였을 경우, 이 Pod에서 노드의 파일 시스템을 접근해야 한다. 이러한 경우에 유용하게 사용할 수 있다.


아래는 노드의 /tmp 디렉토리를 hostPath를 이용하여 /data/shared 디렉토리에 마운트 하여 사용하는 예제이다.


apiVersion: v1

kind: Pod

metadata:

 name: hostpath

spec:

 containers:

 - name: redis

   image: redis

   volumeMounts:

   - name: terrypath

     mountPath: /data/shared

 volumes:

 - name : terrypath

   hostPath:

     path: /tmp

     type: Directory



이 Pod를 배포해서 Pod를 Id를 얻어보았다.


Pod Id를 통해서 VM을 아래와 같이 확인하였다.


VM에 SSH로 접속해서 /tmp/에 hello.txt 파일을 생성하였다.




다음, Pod의 컨테이너에서 마운트된 /data/shared 디렉토리를 확인해보면 아래와 같이 노드의 /tmp 디렉토리의 내용이 그대로 보이는 것을 볼 수 있다.


gitRepo

볼륨 타입중에 gitRepo라는 유용한 볼륨 타입이 하나 있어서 소개한다.

이 볼륨은 생성시에 지정된 git 리파지토리의 특정 리비전의 내용을 clone을 이용해서 내려 받은후에 디스크 볼륨을 생성하는 방식이다. 물리적으로는 emptyDir이 생성되고, git 레파지토리 내용을 clone으로 다운 받는다.




HTML과 같은 정적 파일이나 Ruby on rails, PHP, node.js 와 같은 스크립트 언어 기반의 코드들은 gitRepo 볼륨을 이용하여 손쉽게 배포할 수 있다.


apiVersion: v1

kind: Pod

metadata:

name: gitrepo-volume-pod

spec:

containers:

- image: nginx:alpine

  name: web-server

  volumeMounts:

  - name: html

    mountPath: /usr/share/nginx/html

    readOnly: true

  ports:

  - containerPort: 80

    protocol: TCP

volumes:

- name: html

  gitRepo:

       repository: https://github.com/luksa/kubia-website-example.git

       revision: master

       directory: .


이 설정은 https://github.com/luksa/kubia-website-example.git 의 master 리비전을 클론으로 다운받아서 /usr/share/nginx/html에 마운트 시키는 설정이다.


PersistentVolume and PersistentVolumeClaim

일반적으로 디스크 볼륨을 설정하려면 물리적 디스크를 생성해야 하고, 이러한 물리적 디스크에 대한 설정을 자세하게 이해할 필요가 있다.

쿠버네티스는 인프라에 대한 복잡성을 추상화를 통해서 간단하게 하고, 개발자들이 손쉽게 필요한 인프라 (컨테이너,디스크, 네트워크)를 설정할 수 있도록 하는 개념을 가지고 있다

그래서 인프라에 종속적인 부분은 시스템 관리자가 설정하도록 하고, 개발자는 이에 대한 이해 없이 간단하게 사용할 수 있도록 디스크 볼륨 부분에 PersistentVolumeClaim (이하 PVC)와 PersistentVolume (이하 PV)라는 개념을 도입하였다.


시스템 관리자가 실제 물리 디스크를 생성한 후에, 이 디스크를 PersistentVolume이라는 이름으로 쿠버네티스에 등록한다.

개발자는 Pod를 생성할때, 볼륨을 정의하고, 이 볼륨 정의 부분에 물리적 디스크에 대한 특성을 정의하는 것이 아니라 PVC를 지정하여, 관리자가 생성한 PV와 연결한다.


그림으로 정리해보면 다음과 같다.


시스템 관리자가 생성한 물리 디스크를 쿠버네티스 클러스터에 표현한것이 PV이고, Pod의 볼륨과 이 PV를 연결하는 관계가 PVC가 된다.


이때 주의할점은 볼륨은 생성된후에, 직접 삭제하지 않으면 삭제되지 않는다. PV의 생명 주기는 쿠버네티스 클러스터에 의해서 관리되면 Pod의 생성 또는 삭제에 상관없이 별도로 관리 된다. (Pod와 상관없이 직접 생성하고 삭제해야 한다.)

PersistentVolume

PV는 물리 디스크를 쿠버네티스에 정의한 예제로, NFS 파일 시스템 5G를 pv0003이라는 이름으로 정의하였다.




PV를 설정하는데 여러가지 설정 옵션이 있는데, 간략하게 그 내용을 살펴보면 다음과 같다.

  • Capacity
    볼륨의 용량을 정의한다. 현재는 storage 항목을 통해서 용량만을 지정하는데 향후에는 필요한 IOPS나 Throughput등을 지원할 예정이다.

  • VolumeMode
    VolumeMode는 Filesystem (default)또는 raw를 설정할 수 있는데, 볼륨이 일반 파일 시스템인데, raw 볼륨인지를 정의한다.

  • Reclaim Policy
    PV는 연결된 PVC가 삭제된 후 다시 다른 PVC에 의해서 재 사용이 가능한데, 재 사용시에 디스크의 내용을 지울지 유지할지에 대한 정책을 Reclaim Policy를 이용하여 설정이 가능하다.

    • Retain : 삭제하지 않고 PV의 내용을 유지한다.

    • Recycle : 재 사용이 가능하며, 재 사용시에는 데이타의 내용을 자동으로 rm -rf 로 삭제한 후 재사용이 된다.

    • Delete : 볼륨의 사용이 끝나면, 해당 볼륨은 삭제 된다. AWS EBS, GCE PD,Azure Disk등이 이에 해당한다.

Reclaim Policy은 모든 디스크에 적용이 가능한것이 아니라, 디스크의 특성에 따라서 적용이 가능한 Policy가 있고, 적용이 불가능한 Policy 가 있다.

  • AccessMode
    AccessMode는 PV에 대한 동시에 Pod에서 접근할 수 있는 정책을 정의한다.

    • ReadWriteOnce (RWO)
      해당 PV는 하나의 Pod에만 마운트되고 하나의 Pod에서만 읽고 쓰기가 가능하다.

    • ReadOnlyMany(ROX)
      여러개의 Pod에 마운트가 가능하며, 여러개의 Pod에서 동시에 읽기가 가능하다. 쓰기는 불가능하다.

    • ReadWriteMany(RWX)
      여러개의 Pod에 마운트가 가능하고, 동시에 여러개의 Pod에서 읽기와 쓰기가 가능하다.

위와 같이 여러개의 모드가 있지만, 모든 디스크에 사용이 가능한것은 아니고 디스크의 특성에 따라서 선택적으로 지원된다.


PV의 라이프싸이클

PV는 생성이 되면, Available 상태가 된다. 이 상태에서 PVC에 바인딩이 되면 Bound 상태로 바뀌고 사용이 되며, 바인딩된 PVC가 삭제 되면, PV가 삭제되는 것이 아니라  Released 상태가 된다. (Available이 아니면 사용은 불가능하고 보관 상태가 된다.)

PV 생성 (Provisioning)

PV의 생성은 앞에서 봤던것 처럼 yaml 파일등을 이용하여, 수동으로 생성을 할 수 도 있지만, 설정에 따라서 필요시마다 자동으로 생성할 수 있게 할 수 있다. 이를 Dynamic Provisioning (동적 생성)이라고 하는데, 이에 대해서는 PVC를 설명하면서 같이 설명하도록 하겠다.

PersistentVolumeClaim

PVC는 Pod의 볼륨과 PVC를 연결(바인딩/Bind)하는 관계 선언이다.

아래 예제를 보자 아래 예제는 PVC의 예제이다.



(출처 : https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims)


  • accessMode, VolumeMode는 PV와 동일하다.

  • resources는 PV와 같이, 필요한 볼륨의 사이즈를 정의한다.

  • selector를 통해서 볼륨을 선택할 수 있는데, label selector 방식으로 이미 생성되어 있는 PV 중에, label이 매칭되는 볼륨을 찾아서 연결하게 된다.


PV/PVC 예제

그러면 예제를 통해서 PV를 생성하고, 이 PV를 PVC에 연결한후에, PVC를 Pod에 할당하여 사용하는 방법을 살펴보도록 하자. 예제는 구글 클라우드 환경을 사용하였다.

1.물리 디스크 생성

먼저 구글 클라우드 콘솔에서 Compute Engine 부분에서 아래와 같이 Disks 부분에서 물리 디스크를 생성한다.


디스크를 pv-demo-disk라는 이름으로 생성하였다.

이때 주의할점은 디스크의 region과 zone이 쿠베네티스 클러스터가 배포된 region과 zone에 동일해야 한다.


2.생성된 디스크로 PV를 선언

생성된 디스크를 이용하여 PV를 생성한다. 아래는 PV를 생성하기 위한 yaml 파일이다.


existing-pd.yaml

apiVersion: v1

kind: PersistentVolume

metadata:

 name: pv-demo

spec:

 storageClassName:

 capacity:

   storage: 20G

 accessModes:

   - ReadWriteOnce

 gcePersistentDisk:

   pdName: pv-demo-disk

   fsType: ext4


PV의이름은 pv-demo이고, gcePersistentDisk에서 앞에서 생성한 pv-demo-disk 를 사용하도록 정의하였다.

파일을 실행하면, 아래와 같이 pv-demo로 PV가 생성된것을 확인할 수 있다.

3. 다음 PVC를 생성한다.

아래는 앞에서 생성한 pv-demo PV를 사용하는 PVC를 생성하는 yaml 파일이다. 하나의 Pod에서만 액세스가 가능하도록 accessMode를 ReadWriteOnce로 설정하였다.


existing-pvc.yaml

apiVersion: v1

kind : PersistentVolumeClaim

metadata:

 name: pv-claim-demo

spec:

 storageClassName: ""

 volumeName: pv-demo

 accessModes:

   - ReadWriteOnce

 resources:

   requests:

     storage: 20G


4. Pod를 생성하여, PVC를 바인딩

그러면 앞에서 생성한 PV와 PVC를 Pod에 생성해서 연결하자


existing-pod-redis.yaml

apiVersion: v1

kind: Pod

metadata:

 name: redis

spec:

 containers:

 - name: redis

   image: redis

   volumeMounts:

   - name: terrypath

     mountPath: /data

 volumes:

 - name : terrypath

   persistentVolumeClaim:

     claimName: pv-claim-demo


앞에서 생성한 PVC pv-claim-demo를 Volume에 연결한후, 이 볼륨을 /data 디렉토리에 마운트 하였다.

Pod를 생성한후에, 생성된 Pod에 df -k 로 디스크 연결 상태를 확인해 보면 다음과 같다.



/dev/sdb 가 20G로 생성되어 /data 디렉토리에 마운트 된것을 확인할 수 있다.

Dynamic Provisioning

앞에서 본것과 같이 PV를 수동으로 생성한후 PVC에 바인딩 한 후에, Pod에서 사용할 수 있지만, 쿠버네티스 1.6에서 부터 Dynamic Provisioning (동적 생성) 기능을 지원한다. 이 동적 생성 기능은 시스템 관리자가 별도로 디스크를 생성하고 PV를 생성할 필요 없이 PVC만 정의하면 이에 맞는 물리 디스크 생성 및 PV 생성을 자동화해주는 기능이다.




PVC를 정의하면, PVC의 내용에 따라서 쿠버네티스 클러스터가 물리 Disk를 생성하고, 이에 연결된 PV를 생성한다.

실 환경에서는 성능에 따라 다양한 디스크(nVME, SSD, HDD, NFS 등)를 사용할 수 있다. 그래서 디스크를 생성할때, 필요한 디스크의 타입을 정의할 수 있는데, 이를 storageClass 라고 하고, PVC에서 storage class를 지정하면, 이에 맞는 디스크를 생성하도록 한다.

Storage class를 지정하지 않으면, 디폴트로 설정된 storage class 값을 사용하게 된다.


동적 생성 방법은 어렵지 않다. PVC에 필요한 디스크 용량을 지정해놓으면, 자동으로 이에 해당하는 물리 디스크 및 PV가 생성이 된다. 아래는 동적으로 PV를 생성하는 PVC 예제이다.


dynamic-pvc.yaml

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

 name: mydisk

spec:

 accessModes:

   - ReadWriteOnce

 resources:

   requests:

     storage: 30Gi


다음 Pod를 생성한다.

apiVersion: v1

kind: Pod

metadata:

 name: redis

spec:

 containers:

 - name: redis

   image: redis

   volumeMounts:

   - name: terrypath

     mountPath: /data/shared

 volumes:

 - name : terrypath

   persistentVolumeClaim:

     claimName: mydisk


Pod를 생성한후에, kubectl get pvc 명령어를 이용하여, 생성된 PVC와 PV를 확인할 수 있다.

PVC는 위에서 정의한것과 같이 mydisk라는 이름으로 생성되었고, Volume (PV)는 pvc-4a…. 식으로 새롭게 생성되었다.

Storage class

스토리지 클래스를 살펴보자,

아래는  AWS EBS 디스크에 대한 스토리지 클래스를 지정한 예로, slow 라는 이름으로 스토리지 클래스를 지정하였다. EBS 타입은 io1을 사용하고, GB당 IOPS는 10을 할당하도록 하였고, 존은 us-east-1d와 us-east-1c에 디스크를 생성하도록 하였다.



아래는 구글 클라우드의 Persistent Disk (pd)의 예로, slow라는 이름으로 스토리지 클래스를 지정하고, pd-standard (HDD)타입으로 디스크를 생성하되 us-central1-a와 us-central1-b 존에 디스크를 생성하도록 하였다.



이렇게 정의한 스토리지 클래스는  PVC 정의시에, storageClassName에 적으면 PVC에 연결이 되고, 스토리지 클래스에 정해진 스펙에 따라서 물리 디스크와 PV를 생성하게 된다.

쿠버네티스 #3

개념이해 (2/2) : 고급 컨트롤러


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



고급 컨트롤러

RC,RS,Deployment는 웹서버와 같은 일반적인 워크로드에 대해 Pod를 관리하기 위한 컨트롤러이다. 실제 운영환경에서는 웹서버와 같은 일반적인 워크로드 이외에,  데이타베이스,배치 작업, 데몬 서버와 같이 다양한 형태의 워크로드 모델이 존재하는데 이를 지원하기 위해서 쿠버네티스는 다양한 컨트롤러를 제공함으로써, Pod의 운영을 다양한 시나리오에 맞게 지원하고 있다.

DaemonSet

DaemonSet (이하 DS) 은 Pod가 각각의 노드에서 하나씩만 돌게 하는 형태로 Pod를 관리하는 컨트롤러이다. 아래 그림을 보자


RC나 RS에 의해서 관리되는 Pod 는 여러 노드의 상황에 따라서 일반적으로 비균등적으로 배포가 되지만,  DS에 의해 관리되는 Pod는 모든 노드에 균등하게 하나씩만 배포 된다.

이런 형태의 워크로드는 서버의 모니터링이나 로그 수집 용도로 많이 사용되는데, DS의 다른 특징중 하나는, 특정 Node들에만 Pod가 하나씩만 배포 되도록 설정이 가능하다.

앞에서 언급한 로그나 모니터링 시나리오에서 특정 장비에 대한 모니터링을 하고자 할 때 이런 시나리오가 유효하다. 예를 들어 특정 장비(노드)에만 Nvme SSD를 사용하거나 GPU를 사용할 경우에는 그 장비가 설치된 노드만을 모니터링하면 된다.



DS는 특정 노드에만 Pod를 배포할 수 있도록 , Pod의 “node selector”를 이용해서 라벨을 이용하여 특정 노드만을 선택할 수 있게 지원한다.

Job

워크로드 모델중에서 배치나 한번 실행되고 끝나는 형태의 작업이 있을 수 있다.

예를 들어 원타임으로 파일 변환 작업을 하거나, 또는 주기적으로 ETL 배치 작업을 하는 경우에는 웹서버 처럼 계속 Pod가 떠 있을 필요없이 작업을 할때만 Pod 를 띄우면 된다.

이러한 형태의 워크로드 모델을 지원하는 컨트롤러를 Job이라고 한다.


Job에 의해서 관리되는 Pod는 Job이 종료되면, Pod 를 같이 종료한다.

Job을 정의할때는 보통 아래와 같이 컨테이너 스펙 부분에 image 뿐만 아니라, 컨테이너에서 Job을 수행하기 위한 커맨드(command) 를 같이 입력한다.


apiVersion: batch/v1
kind: Job
metadata:
 name: pi
spec:
 template:
   spec:
     containers:
     - name: pi
       image: perl
       command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
     restartPolicy: Never
 backoffLimit: 4



Job 컨트롤러에 의해서 실행된 Pod 는 이 command의 실행 결과에 따라서 Job이 실패한지 성공한지를 판단한다. (프로세스의 exit 코드로 판단한다.)  Job이 종료되었는데, 결과가 실패라면,이 Job을 재 실행할지 또는 그냥 끝낼지를 설정에 따라서 결정한다.


Job이 끝나기 전에 만약에 비정상적으로 종료된다면 어떻게 될것인가?

아래 그림을 보자 쿠버네티스 클러스터에서 특정 노드가 장애가 났다고 가정하자, RC/RS에 의해서 관리되고 있는 Pod 는 자동으로 다른 노드에서 다시 자동으로 생성되서 시작될것이고, 컨트롤러에 의해 관리되고 있지 않은 Pod 는 다시 다른 노드에서 기동되지 않고 사라질것이다.

그렇다면 Job 에 의해서 관리되는 Pod는 어떻게 될것인가?



두가지 방법으로 설정할 수 있는데, 장애시 다시 시작하게 하거나 또는 장애시 다시 시작하지 않게 할 수 있다.

다시 시작의 개념은 작업의 상태가 보장되는것이 아니라, 다시 처음부터 작업이 재 시작되는 것이기 때문에 resume이 아닌 restart의 개념임을 잘 알아야하고, 다시 시작 처음부터 작업을 시작하더라도 데이타가 겹치거나 문제가 없는 형태라야 한다.


배치 작업의 경우 작업을 한번만 실행할 수 도 있지만, 같은 작업을 연속해서 여러번 수행하는 경우가 있다. (데이타가 클 경우 범위를 나눠서 작업하는 경우) 이런 경우를 위해서 Job 컨트롤러는 같은 Pod를 순차적으로, 여러번 실행할 수 있도록 설정이 가능하다. Job 설정에서 completion에 횟수를 주면, 같은 작업을 completion 횟수만큼 순차적으로 반복한다.


만약에 여러 작업을 처리해야 하지만 순차성이 필요없고 병렬로 처리를 하고 싶다면, Job설정에서 parallelism 에 동시 실행할 수 있는 Pod의 수를 주면, 지정된 수 만큼 Pod를 실행하여 completion 횟수를 병렬로 처리한다. 아래 그림은 completion이 5, parallelism이 2일때, 하나의 노드에서 모든 Pod가 실행된다고 가정했을때, 실행 순서를 보여주는 그림이다.



Cron jobs

Job 컨트롤러에 의해서 실행되는 배치성 작업들에 대해서 고려할 점중 하나는 이런 배치성 작업을 메뉴얼로 실행하는 것이 아니라, 주기적으로 자동화해서 실행할 필요가 있는데, 이렇게 주기적으로 정해진 스케쥴에 따라 Job 컨트롤러에 의해 작업을 실행해주는 컨트롤러로 cron jobs 컨트롤러가 있다.

cron jobs 컨트롤러는 Unix cron 명령어처럼, 시간에 따른 실행조건을 정의해놓을 수 있고, 이에 따라 Job 컨트롤러를 실행하여, 정의된 Pod를 실행할 수 있게 한다.


아래는 cron jobs 컨트롤러의 예제인데, job 컨트롤러와 설정이 다르지 않다.


apiVersion: batch/v1beta1
kind: CronJob
metadata:
 name: hello
spec:
 schedule: "*/1 * * * *"
 jobTemplate:
   spec:
     template:
       spec:
         containers:
         - name: hello
           image: busybox
           args:
           - /bin/sh
           - -c
           - date; echo Hello from the Kubernetes cluster
         restartPolicy: OnFailure



다른 점은 CronJob 스펙 설정 부분에 “schedule”이라는 항목이 있고 반복 조건을 unix cron과 같이 설정하면 된다.

StatefulSet

마지막으로, 1.9에 정식으로 릴리즈된 StatefulSet이 있다.

RS/RC나 다른 컨트롤러로는 데이타베이스와 같이 상태를 가지는 애플리케이션을 관리하기가 어렵다.

그래서 이렇게 데이타 베이스등과 같이 상태를 가지고 있는 Pod를 지원하기 위해서 StatefulSet 이라는 것이 새로 소개되었는데, 이를 이해하기 위해서는 쿠버네티스의 디스크 볼륨에 대한 이해가 필요하기 때문에 다음에 볼륨과 함께 다시 설명하도록 한다.


2회에 걸쳐서 쿠버네티스의 컴포넌트 개념들에 대해서 살펴보았고, 다음글에서는 쿠버네티스의 아키텍쳐에 대해서 간략하게 살펴보도록 하겠다.

쿠버네티스 #2

개념 이해 (1/2)


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


쿠버네티스를 공부하면서 가장 헷갈리는 부분이 용어와 컨셉이다. 이 컨셉만 잘 이해하면 쿠버네티스를 쉽게 이해하고 사용할 수 있지만, 적어도 내 기준에서는 문서들의 용어나 개념 설명이 다소 어려웠다.

쿠버네티스의 개념은 크게 오브젝트 두개의 개념에서 출발한다. 각각을 살펴보도록 하자

마스터와 노드

쿠버네티스를 이해하기 위해서는 먼저 클러스터의 구조를 이해할 필요가 있는데, 구조는 매우 간단하다. 클러스터 전체를 관리하는 컨트롤러로써 마스터가 존재하고, 컨테이너가 배포되는 머신 (가상머신이나 물리적인 서버머신)인 노드가 존재한다.


오브젝트

쿠버네티스를 이해하기 위해서 가장 중요한 부분이 오브젝트이다. 가장 기본적인 구성단위가 되는 기본 오브젝트(Basic object)와, 이 기본 오브젝트(Basic object) 를 생성하고 관리하는 추가적인 기능을 가진 컨트롤러(Controller) 로 이루어진다. 그리고 이러한 오브젝트의 스펙(설정)이외에 추가정보인 메타 정보들로 구성이 된다고 보면 된다.

오브젝트 스펙 (Object Spec)

오브젝트들은 모두 오브젝트의 특성 (설정정보)을 기술한 오브젝트 스펙 (Object Spec)으로 정의가 되고, 커맨드 라인을 통해서 오브젝트 생성시 인자로 전달하여 정의를 하거나 또는 yaml이나 json 파일로 스펙을 정의할 수 있다.

기본 오브젝트 (Basic Object)

쿠버네티스에 의해서 배포 및 관리되는 가장 기본적인 오브젝트는 컨테이너화되어 배포되는 애플리케이션의 워크로드를 기술하는 오브젝트로 Pod,Service,Volume,Namespace 4가지가 있다.


간단하게 설명 하자면 Pod는 컨테이너화된 애플리케이션, Volume은 디스크, Service는 로드밸런서 그리고 Namespace는 패키지명 정도로 생각하면 된다. 그러면 각각을 자세하게 살펴보도록 하자.

Pod

Pod 는 쿠버네티스에서 가장 기본적인 배포 단위로, 컨테이너를 포함하는 단위이다.

쿠버네티스의 특징중의 하나는 컨테이너를 개별적으로 하나씩 배포하는 것이 아니라 Pod 라는 단위로 배포하는데, Pod는 하나 이상의 컨테이너를 포함한다.


아래는 간단한 Pod를 정의한 오브젝트 스펙이다. 하나하나 살펴보면


apiVersion: v1

kind: Pod

metadata:

 name: nginx

spec:

 containers:

 - name: nginx

   image: nginx:1.7.9

   ports:

   - containerPort: 8090


  • apiVersion은 이 스크립트를 실행하기 위한 쿠버네티스 API 버전이다 보통 v1을 사용한다.

  • kind 에는 리소스의 종류를 정의하는데, Pod를 정의하려고 하기 때문에, Pod라고 넣는다.

  • metadata에는 이 리소스의 각종 메타 데이타를 넣는데, 라벨(뒤에서 설명할)이나 리소스의 이름등 각종 메타데이타를 넣는다

  • spec 부분에 리소스에 대한 상세한 스펙을 정의한다.

    • Pod는 컨테이너를 가지고 있기 때문에, container 를 정의한다. 이름은 nginx로 하고 도커 이미지 nginx:1.7.9 를 사용하고, 컨테이너 포트 8090을 오픈한다.


Pod 안에 한개 이상의 컨테이너를 가지고 있을 수 있다고 했는데 왜 개별적으로 하나씩 컨테이너를 배포하지 않고 여러개의 컨테이너를 Pod 단위로 묶어서 배포하는 것인가?


Pod는 다음과 같이 매우 재미있는 특징을 갖는다.


  • Pod 내의 컨테이너는 IP와 Port를 공유한다.
    두 개의 컨테이너가 하나의 Pod를 통해서 배포되었을때, localhost를 통해서 통신이 가능하다.
    예를 들어 컨테이너 A가 8080, 컨테이너 B가 7001로 배포가 되었을 때, B에서 A를 호출할때는 localhost:8080 으로 호출하면 되고, 반대로 A에서 B를 호출할때에넌 localhost:7001로 호출하면 된다.

  • Pod 내에 배포된 컨테이너간에는 디스크 볼륨을 공유할 수 있다.
    근래 애플리케이션들은 실행할때 애플리케이션만 올라가는것이 아니라 Reverse proxy, 로그 수집기등 다양한 주변 솔루션이 같이 배포 되는 경우가 많고, 특히 로그 수집기의 경우에는 애플리케이션 로그 파일을 읽어서 수집한다. 애플리케이션 (Tomcat, node.js)와 로그 수집기를 다른 컨테이너로 배포할 경우, 일반적인 경우에는 컨테이너에 의해서 파일 시스템이 분리되기 때문에, 로그 수집기가 애플리케이션이 배포된 컨테이너의 로그파일을 읽는 것이 불가능 하지만, 쿠버네티스의 경우 하나의 Pod 내에서는 컨테이너들끼리 볼륨을 공유할 수 있기 때문에 다른 컨테이너의 파일을 읽어올 수 있다.


위와 같이 애플리케이션과 애플리케이션에서 사용하는 주변 프로그램을 같이 배포하는 패턴을 마이크로 서비스 아키텍쳐에서 사이드카 패턴(Side car pattern)이라고 하는데, 이 외에도 Ambassador, Adapter Container 등 다양한 패턴이 있는데, 이는 나중에 다른 글에서 상세하게 설명하도록 한다.

Volume

Pod가 기동할때 디폴트로, 컨테이너마다 로컬 디스크를 생성해서 기동되는데, 이 로컬 디스크의 경우에는 영구적이지 못하다. 즉 컨테이너가 리스타트 되거나 새로 배포될때 마다 로컬 디스크는 Pod 설정에 따라서 새롭게 정의되서 배포되기 때문에, 디스크에 기록된 내용이 유실된다.

데이타 베이스와 같이 영구적으로 파일을 저장해야 하는 경우에는 컨테이너 리스타트에 상관 없이 파일을 영속적으로 저장애햐 하는데, 이러한 형태의 스토리지를 볼륨이라고 한다.

볼륨은 컨테이너의 외장 디스크로 생각하면 된다. Pod가 기동할때 컨테이너에 마운트해서 사용한다.


앞에서 언급한것과 같이 쿠버네티스의 볼륨은 Pod내의 컨테이너간의 공유가 가능하다.


웹 서버를 배포하는 Pod가 있을때, 웹서비스를 서비스하는 Web server 컨테이너, 그리고 컨텐츠의 내용 (/htdocs)를 업데이트하고 관리하는 Content mgmt 컨테이너, 그리고 로그 메세지를 관리하는 Logger라는 컨테이너이가 있다고 하자

  • WebServer 컨테이너는 htdocs 디렉토리의 컨테이너를 서비스하고, /logs 디렉토리에 웹 억세스 기록을 기록한다.

  • Content 컨테이너는 htdocs 디렉토리의 컨텐트를 업데이트하고 관리한다.

  • Logger 컨테이너는 logs 디렉토리의 로그를 수집한다.

이 경우 htdocs 컨텐츠 디렉토리는 WebServer와 Content 컨테이너가 공유해야 하고 logs 디렉토리는 Webserver 와 Logger 컨테이너가 공유해야 한다. 이러한 시나리오에서 볼륨을 사용할 수 있다.


아래와 같이 htdocs와 logs 볼륨을 각각 생성한 후에, htdocs는 WebServer와, Contents management 컨테이너에 마운트 해서 공유하고, logs볼륨은 Logger와 WebServer 컨테이너에서 공유하도록 하면된다.  



쿠버네티스는 다양한 외장 디스크를 추상화된 형태로 제공한다. iSCSI나 NFS와 같은 온프렘 기반의 일반적인 외장 스토리지 이외에도, 클라우드의 외장 스토리지인 AWS EBS, Google PD,에서 부터  github, glusterfs와 같은 다양한 오픈소스 기반의 외장 스토리지나 스토리지 서비스를 지원하여, 스토리지 아키텍처 설계에 다양한 옵션을 제공한다.

Service

Pod와 볼륨을 이용하여, 컨테이너들을 정의한 후에, Pod 를 서비스로 제공할때, 일반적인 분산환경에서는 하나의 Pod로 서비스 하는 경우는 드물고, 여러개의 Pod를 서비스하면서, 이를 로드밸런서를 이용해서 하나의 IP와 포트로 묶어서 서비스를 제공한다.


Pod의 경우에는 동적으로 생성이 되고, 장애가 생기면 자동으로 리스타트 되면서 그 IP가 바뀌기 때문에, 로드밸런서에서 Pod의 목록을 지정할 때는 IP주소를 이용하는 것은 어렵다. 또한 오토 스케일링으로 인하여 Pod 가 동적으로 추가 또는 삭제되기 때문에, 이렇게 추가/삭제된 Pod 목록을 로드밸런서가 유연하게 선택해 줘야 한다.

그래서 사용하는 것이 라벨(label)과 라벨 셀렉터(label selector) 라는 개념이다.


서비스를 정의할때, 어떤 Pod를 서비스로 묶을 것인지를 정의하는데, 이를 라벨 셀렉터라고 한다. 각 Pod를 생성할때 메타데이타 정보 부분에 라벨을 정의할 수 있다. 서비스는 라벨 셀렉터에서 특정 라벨을 가지고 있는 Pod만 선택하여 서비스에 묶게 된다.

아래 그림은 서비스가 라벨이 “myapp”인 서비스만 골라내서 서비스에 넣고, 그 Pod간에만 로드밸런싱을 통하여 외부로 서비스를 제공하는 형태이다.



이를 스펙으로 정의해보면 대략 다음과 같다.


kind: Service
apiVersion: v1
metadata:
 name: my-service
spec:
 selector:
   app: myapp
 ports:
 - protocol: TCP
   port: 80
   targetPort: 9376


  • 리소스 종류가 Service 이기 때문에, kind는 Service로 지정하고,

  • 스크립트를 실행할 api 버전은 v1으로 apiVersion에 정의했다.

  • 메타데이타에 서비스의 이름을 my-service로 지정하고

  • spec 부분에 서비스에 대한 스펙을 정의한다.

    • selector에서 라벨이 app:myapp인 Pod 만을 선택해서 서비스에서 서비스를 제공하게 하고

    • 포트는 TCP를 이용하되, 서비스는 80 포트로 서비스를 하되, 서비스의 80 포트의 요청을 컨테이너의 9376 포트로 연결해서 서비스를 제공한다.


Name space

네임스페이스는 한 쿠버네티스 클러스터내의 논리적인 분리단위라고 보면 된다.

Pod,Service 등은 네임 스페이스 별로 생성이나 관리가 될 수 있고, 사용자의 권한 역시 이 네임 스페이스 별로 나눠서 부여할 수 있다.

즉 하나의 클러스터 내에, 개발/운영/테스트 환경이 있을때, 클러스터를 개발/운영/테스트 3개의 네임 스페이스로 나눠서 운영할 수 있다. 네임스페이스로 할 수 있는 것은

  • 사용자별로 네임스페이스별 접근 권한을 다르게 운영할 수 있다.

  • 네임스페이스별로 리소스의 쿼타 (할당량)을 지정할 수 있다. 개발계에는 CPU 100, 운영계에는 CPU 400과 GPU 100개 식으로, 사용 가능한 리소스의 수를 지정할 수 있다.

  • 네임 스페이스별로 리소스를 나눠서 관리할 수 있다. (Pod, Service 등)


주의할점은 네임 스페이스는 논리적인 분리 단위이지 물리적이나 기타 장치를 통해서 환경을 분리(Isolation)한것이 아니다. 다른 네임 스페이스간의 pod 라도 통신은 가능하다.

물론 네트워크 정책을 이용하여, 네임 스페이스간의 통신을 막을 수 있지만 높은 수준의 분리 정책을 원하는 경우에는 쿠버네티스 클러스터 자체를 분리하는 것을 권장한다.


참고 자료 네임 스페이스에 대한 베스트 프랙틱스 : https://cloudplatform.googleblog.com/2018/04/Kubernetes-best-practices-Organizing-with-Namespaces.html

https://kubernetes.io/blog/2016/08/kubernetes-namespaces-use-cases-insights/

라벨

앞에서 잠깐 언급했던 것 중의 하나가 label 인데, 라벨은 쿠버네티스의 리소스를 선택하는데 사용이 된다. 각 리소스는 라벨을 가질 수 있고, 라벨 검색 조건에 따라서 특정 라벨을 가지고 있는 리소스만을 선택할 수 있다.

이렇게 라벨을 선택하여 특정 리소스만 배포하거나 업데이트할 수 있고 또는 라벨로 선택된 리소스만 Service에 연결하거나 특정 라벨로 선택된 리소스에만 네트워크 접근 권한을 부여하는 등의 행위를 할 수 있다.

라벨은 metadata 섹션에 키/값 쌍으로 정의가 가능하며, 하나의 리소스에는 하나의 라벨이 아니라 여러 라벨을 동시에 적용할 수 있다.


"metadata": {
 "labels": {
   "key1" : "value1",
   "key2" : "value2"
 }
}


셀렉터를 사용하는 방법은 오브젝트 스펙에서 selector 라고 정의하고 라벨 조건을 적어 놓으면 된다.

쿠버네티스에서는 두 가지 셀렉터를 제공하는데, 기본적으로 Equaility based selector와, Set based selector 가 있다.

Equality based selector는 같냐, 다르냐와 같은 조건을 이용하여, 리소스를 선택하는 방법으로

  • environment = dev

  • tier != frontend

식으로, 등가 조건에 따라서 리소스를 선택한다.

이보다 향상된 셀렉터는 set based selector로, 집합의 개념을 사용한다.

  • environment in (production,qa) 는 environment가 production 또는 qa 인 경우이고,

  • tier notin (frontend,backend)는 environment가 frontend도 아니고 backend도 아닌 리소스를 선택하는 방법이다.

다음 예제는 my-service 라는 이름의 서비스를 정의한것으로 셀렉터에서 app: myapp 정의해서 Pod의 라벨 app이 myapp 것만 골라서 이 서비스에 바인딩해서 9376 포트로 서비스 하는 예제이다.


kind: Service
apiVersion: v1
metadata:
 name: my-service
spec:
 selector:
   app: myapp
 ports:
 - protocol: TCP
   port: 80
   targetPort: 9376



컨트롤러

앞에서 소개한 4개의 기본 오브젝트로, 애플리케이션을 설정하고 배포하는 것이 가능한데 이를 조금 더 편리하게 관리하기 위해서 쿠버네티스는 컨트롤러라는 개념을 사용한다.

컨트롤러는 기본 오브젝트들을 생성하고 이를 관리하는 역할을 해준다. 컨트롤러는 Replication Controller (aka RC), Replication Set, DaemonSet, Job, StatefulSet, Deployment 들이 있다. 각자의 개념에 대해서 살펴보도록 하자.

Replication Controller

Replication Controller는  Pod를 관리해주는 역할을 하는데, 지정된 숫자로 Pod를 기동 시키고, 관리하는 역할을 한다.

Replication Controller (이하 RC)는 크게 3가지 파트로 구성되는데, Replica의 수, Pod Selector, Pod Template 3가지로 구성된다.

  • Selector : 먼저 Pod selector는 라벨을 기반으로 하여,  RC가 관리한 Pod를 가지고 오는데 사용한다.

  • Replica 수 :  RC에 의해서 관리되는 Pod의 수인데, 그 숫자만큼 Pod 의 수를 유지하도록 한다.예를 들어 replica 수가 3이면, 3개의 Pod만 띄우도록 하고, 이보다 Pod가 모자르면 새로운 Pod를 띄우고, 이보다 숫자가 많으면 남는 Pod를 삭제한다.

  • Pod를 추가로 기동할 때 그러면 어떻게 Pod를 만들지 Pod에 대한 정보 (도커 이미지, 포트,라벨등)에 대한 정보가 필요한데, 이는 Pod template이라는 부분에 정의 한다.




주의할점은 이미 돌고 있는 Pod가 있는 상태에서 RC 리소스를 생성하면 그 Pod의 라벨이 RC의 라벨과 일치하면 새롭게 생성된 RC의 컨트롤을 받는다. 만약 해당 Pod들이 RC에서 정의한 replica 수 보다 많으면, replica 수에 맞게 추가분의 pod를 삭제하고, 모자르면 template에 정의된 Pod 정보에 따라서 새로운 Pod를 생성하는데, 기존에 생성되어 있는 Pod가 template에 정의된 스펙과 다를지라도 그 Pod를 삭제하지 않는다. 예를 들어 기존에 아파치 웹서버로 기동중인 Pod가 있고, RC의 template은 nginx로 Pod를 실행하게 되어 있다하더라도 기존에 돌고 있는 아파치 웹서버 기반의 Pod를 삭제하지 않는다.


아래 예를 보자.


이 예제는 ngnix라는 이름의 RC를 정의한 것으로, label이 “app:ngnix”인 Pod들을 관리하고 3개의 Pod가 항상 운영되도록 설정한다.

Pod는 app:ngix 라는 라벨을 가지면서 이름이 ngnix이고 nginx 이미지를 사용해서 생성하고 컨테이너의 포트는 80 번 포트를 이용해서 서비스를 제공한다.

ReplicaSet

ReplicaSet은 Replication Controller 의 새버전으로 생각하면 된다.

큰 차이는 없고 Replication Controller 는 Equality 기반 Selector를 이용하는데 반해, Replica Set은 Set 기반의 Selector를 이용한다.

Deployment

Deployment (이하 디플로이먼트) Replication controller와 Replica Set의 좀더 상위 추상화 개념이다. 실제 운영에서는 ReplicaSet 이나 Replication Controller를 바로 사용하는 것보다, 좀 더 추상화된 Deployment를 사용하게 된다.

쿠버네티스 배포에 대한 이해

쿠버네티스의 Deployment 리소스를 이해하기 위해서는 쿠버네티스에서 Deployment 없이 어떻게 배포를 하는지에 대해서 이해를 하면 Deployment 를 이해할 수 있다.


다음과 같은 Pod와 RC가 있다고 하자


애플리케이션이 업데이트되서 새로운 버전으로 컨테이너를 굽고 이 컨테이너를 배포하는 시나리오에 대해서 알아보자. 여러가지 배포 전략이 있겠지만, 많이 사용하는 블루/그린 배포와 롤링 업데이트 방식 두가지 방법에 대해서 설명한다.

블루/그린 배포

블루/그린 배포 방식은 블루(예전)버전으로 서비스 하고 있던 시스템을 그린(새로운)버전을 배포한 후, 트래픽을 블루에서 그린으로 한번에 돌리는 방식이다.

여러가지 방법이 있지만 가장 손쉬운 방법으로는 새로운 RC을 만들어서 새로운 템플릿으로 Pod를 생성한 후에, Pod 생성이 끝나면, 서비스를 새로운 Pod로 옮기는 방식이다.


후에, 배포가 완료되고 문제가 없으면 예전 버전의 RC 와 Pod를 지워준다.

롤링 업그레이드

롤링 업그레이드 방식은 Pod를 하나씩 업그레이드 해가는 방식이다.

이렇게 배포를 하려면 먼저 새로운 RC를 만든후에, 기존 RC에서 replica 수를 하나 줄이고, 새로운 RC에는 replica 수를 하나만 준다.


라벨을 같은 이름으로 해주면 서비스는 자연히 새로운 RC에 의해 생성된 Pod를 서비스에 포함 시킨다.

다음으로 기존 RC의 replica를 하나 더 줄이고, 새로운 RC의  replica를 하나 더 늘린다.


그러면 기존 버전의 Pod가 하나더 서비스에서 빠지게 되고 새로운 버전의 Pod가 서비스에 추가된다.

마찬가지 작업을 반복하게 되면, 아래 그림과 같이 예전 버전의 Pod가 모두 빠지고 새 버전의 Pod만 서비스 되게 된다.


만약에 배포가 잘못되었을 경우에는 기존 RC의 replica 수를 원래대로 올리고, 새버전의 replicat 수를 0으로 만들어서 예전 버전의 Pod로 롤백이 가능하다.

이 과정은 kubectl rolling-update라는 명령으로 RC 단위로 컨트롤이 가능하지만, 그래도 여전히 작업이 필요하고, 배포 과정을 모니터링 해야 한다. 그리고 가장 문제는 kubectl rolling-update 명령은 클라이언트에서 실행 하는 명령으로, 명령어 실행중에 클라이언트의 연결이 끊어 지면 배포작업이 비정상적으로 끊어질 수 있는 문제가 있다.

그리고 마지막으로, 롤백과정 역시 수동 컨트롤이 필요할 수 있다.

그래서 이러한 과정을 자동화하고 추상화한 개념을 Deployment라고 보면 된다.

Deployment는 Pod 배포를 위해서 RC를 생성하고 관리하는 역할을 하며, 특히 롤백을 위한 기존 버전의 RC 관리등 여러가지 기능을 포괄적으로 포함하고 있다.



Deployment 에 대해서는 뒤에 다른 글에서 조금 더 자세하게 설명하도록 한다.


이글에서는 쿠버네티스를 이루는 기본적인 오브젝트와 이를 생성 제어하기 위한 기본적인 컨트롤러에 대해서 알아보았다.

다음 글에서는 조금 더 발전된 형태의 컨트롤러에 대해서 알아보기로 한다.




Circuit breaker 패턴을 이용한 장애에 강한 MSA 서비스 구현하기 #2

Spring을 이용한 Circuit breaker 구현


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


앞의 글에서는 넷플릭스 Hystrix를 이용하여 Circuit break를 구현해보았다.

실제 개발에서 Hystix로 개발도 가능하지만, 보통 자바의 경우에는 Spring framework을 많이 사용하기 때문에 이번 글에서는 Spring framework을 이용한 Circuit breaker를 구현하는 방법을 알아보도록 한다.


다행이도 근래에 Spring은 넷플릭스의 MSA 패턴들을 구현화한 오픈 소스들을 Spring 오픈 소스 프레임웍안으로 활발하게 합치는 작업을 진행하고 있어서 어렵지 않게 구현이 가능하다.


구현하고자 하는 시나리오는 앞의 글에서 예제로 사용한 User service에서 Item Service를 호출하는 구조를 구현하고, User service에 circuit breaker를 붙여보도록 하겠다.

User service 코드 전체는 https://github.com/bwcho75/msa_pattern_sample/tree/master/user-spring-hystrix 에 그리고 Item Service 코드 전체는 https://github.com/bwcho75/msa_pattern_sample/tree/master/item-spring-hystrix 에 있다


Spring Circuit breaker 구현

User service pom.xml 정의

Hystrix circuit breaker를 사용하기 위해서는 pom.xml에 다음과 같이 hystrix 관련 라이브러리에 대한 의존성을 정의해줘야 한다.

<dependency>

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

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

<version>1.4.4.RELEASE</version>

</dependency>

<dependency>

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

<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>

<version>1.4.4.RELEASE</version>

</dependency>

<dependency>

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

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

<version>1.5.11.RELEASE</version>

</dependency>


spring-cloud-starter-hystrix 는 Hystrix circuit breaker를 이용한 의존성이고 hystrix-dashboard와 actuator 는 hystix dash 보드를 띄우기 위한 의존성이다.



User service 구현

UserApplication

Circuit breaker를 이용하기 위해서는 User Service의 메인 함수인 UserApplication 에 Annotation으로 선언을 해준다.



package com.terry.circuitbreak.User;




import org.springframework.boot.SpringApplication;


import org.springframework.boot.autoconfigure.SpringBootApplication;


import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;


import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;




@SpringBootApplication


@EnableCircuitBreaker


@EnableHystrixDashboard


public class UserApplication {





public static void main(String[] args) {


SpringApplication.run(UserApplication.class, args);


}


}


위의 코드와 같이 @EnableCircuitBreaker Annotation을 추가해주면 Circuit breaker를 사용할 수 있고, 그리고 추가적으로 Hystrix 대쉬 보드를 사용할것이기 때문에, @EnableHystrixDashboard Annotation을 추가한다.

Item Service를 호출

그러면 UserSerivce에서 ItemService를 호출하는 부분을 구현해보도록 하자. Hystrix와 마찬가지로 Spring Hystrix에서도 타 서비스 호출은 Command로 구현한다.  아래는 Item Service에서 Item 목록을 가지고 오는 GetItemCommand 코드이다.

GetItemCommand

Hystrix Command와 거의 유사하지만 Command를  상속 받아서 사용하지 않고, Circuit breaker를 적용한 메서드에 간단하게  @HystrixCommand Annotation만을 추가하면 된다.


아래 코드를 자세하게 보자. 주의할점은 Item Service 호출을 RestTemplate API를 통해서하는데, RestTemplate 객체인 resetTemplate는 Autowrire로 생성한다.



@Service


public class GetItemCommand {



@Autowired


RestTemplate restTemplate;



  @Bean


  public RestTemplate restTemplate() {


      return new RestTemplate();


  }





// GetItem command


@HystrixCommand(fallbackMethod = "getFallback")


public List<User> getItem(String name)  {


List<User> usersList = new ArrayList<User>();



List<Item> itemList = (List<Item>)restTemplate.exchange("http://localhost:8082/users/"+name+"/items"


,HttpMethod.GET,null


,new ParameterizedTypeReference<List<Item>>() {}).getBody();


usersList.add(new User(name,"myemail@mygoogle.com",itemList));



return usersList;


}



// fall back method


// it returns default result


@SuppressWarnings("unused")


public List<User> getFallback(String name){


List<User> usersList = new ArrayList<User>();


usersList.add(new User(name,"myemail@mygoogle.com"));



return usersList;


}


}


Item Service를 호출하는 코드는 getItem(String name) 메서드이다. 여기에 Circuit breaker를 적용하기 때문에, 메서드 앞에  @HystrixCommand(fallbackMethod = "getFallback") Annotation을 정의하였다. 그리고 Item Service 장애시 호출한 fallback 메서드는 getFallback 메서드로 지정하였다.

getItem안에서는 ItemService를 RestTemplate을 이용하여 호출하고 그 결과를 List<User> 타입으로 반환한다.


앞서 정의한 Fallback은 getFallback() 메서드로 Circuit breaker를 적용한 원래 함수와 입력 (String name)과 출력 (List<User>) 인자가 동일하다.

Circuit breaker 테스트


User service와 Item Service를 기동한 상태에서 user service를 호출하면 아래와 같이 itemList에 Item Service가 리턴한 내용이 같이 반환 되는 것을 확인할 수 있다.


terrycho-macbookpro:~ terrycho$ curl localhost:8081/users/terry

[  

  {  

     "name":"terry",

     "email":"myemail@mygoogle.com",

     "itemList":[  

        {

           "name":"computer",

           "quantity":1

        },

        {

           "name":"mouse",

           "quantity":2

        }

     ]

  }

]


Item Service를 내려놓고 테스트를 해보면 지연 응답 없이 User service로 부터 응답이 리턴되고, 앞서 정의한 fallback 메서드에 의해서 itemList에 아무 값이 없인할 수 있다.


terrycho-macbookpro:~ terrycho$ curl localhost:8081/users/terry

[  

  {  

     "name":"terry",

     "email":"myemail@mygoogle.com",

     "itemList":[]

  }

]


Hystrix Dashboard

User service에서 Hystrix Dash board를 사용하도록 설정하였기 때문에, User Service의 호출 상태를 실시간으로 확인할 수 있다.


User serivce 서버의 URL인 localhost:8081에서 localhost:8081/hystrix.stream을 호출 해보면

아래와 같이 Circuit Breaker가 적용된 메서드의 상태 현황 정보가 계속해서 업데이트 되면서 출력하는 것을 확인할 수 있다.




그러면 대쉬보드에 접속해보자 대쉬 보드 URL은 http://{user service}/hystrix 이다. User service url이 localhost:8081이기 때문에 http://localhost:8081/hystrix로 접속해보자


대쉬 보드에서는 모니터링 할 서비스의 스트림 URL을 넣어줘야 하는데 위에서 설명한 http://localhost:8081/hystrix.stream 을 입력한다.


URL을 입력하고 모니터링을 하면 아래와 같이 Circuit breaker가 등록된 서비스들이 모니터링 된다.

아래 그림은 부하가 없을때 상태이다.


실제로 부하를 주게 되면 아래와 같이 그래프가 커져가면서 정상적인 호출이 늘어가는 것을 확인할 수 있고, 응답 시간들도 모니터링이 가능하다.


아래는 Circuit breaker를 통해서 호출되는 Item service를 죽였을때인데, 그래프가 붉은색으로 표시되면서 붉은색 숫자가 증가하는 것을 볼 수 있고 Item service가 장애이기 때문에, Circuit 의 상태가 Close에서 Open을 변경된것을 확인할 수 있다.



운영 적용에 앞서서 고려할점

앞에서 예제로 사용한 Dashboard는 어디까지나 테스트 수준에서 사용할만한 수준이지 실제 운영환경에 적용할때는 여러가지 고려가 필요하다. 특히 /hystrix , /hystrix.stream이 외부에서 접근이 가능하기 때문에,, 이에 대해서 이 두 URL이 외부로 접근하는 것을 막아야 하며, circuit의 상태에 대한 정보를 하나의 서비스만 아니라 여러 서비스에서 대용량 서비스에 적용할시에는 중앙 집중화된 대쉬보드가 필요하고 또한 많은 로그를 동시에 수집해야 하기 때문에, 대용량 백앤드가 필요하다. 이를 지원하기 위해서 넷플릭스에서는 터빈 (Turbine)이라는 이름으로, 중앙 집중화된 Hystrix 대쉬 보드 툴을 지원하고 있다. (https://github.com/Netflix/turbine/wiki)


이번 글에서는 Spring 프레임웍을 이용하여 Circuit breaker 패턴을 Hystrix 프레임웍을 이용하여 적용하는 방법을 알아보았다.


Spring을 사용하면 편리는 하지만 자바 스택만을 지원한다는 한계점을 가지고 있다. Circuit breaker를 이처럼 소프트웨어로 지원할 수 도 있지만, 소프트웨어가 아닌 인프라 설정을 이용해서 적용이 가능한데, envoryproxy 를 이용하면 코드 변경 없이 모든 플랫폼에 적용이 가능하다. 다음 글에서는 envoy proxy를 이용하여, circuit breaker를 사용하는 방법에 대해서 알아보도록 한다.

Apache airflow


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

배경

빅데이타 분석이나, 머신러닝 코드를 만들다 보면 필요한것중에 하나가 여러개의 태스크를 연결해서 수행해야 할 경우가 있다. 데이타 베이스의 ETL 작업과 비슷한 흐름이라고 보면 된다.


예를 들어 머신러닝의 학습 과정을 보면 데이타 전처리,학습,배포,예측과 같은 단계를 가지게 된다.


  • rawdata를 읽어서 preprocessing 단계를 거쳐서 학습에 적절한 training data로 변경하고,

  • 변경된 training data를 가지고 머신러닝 모델을 학습한후, 학습된 모델을 저장한다.

  • 학습된 모델을 가지고 예측을 해서 결과를 저장한다.


이렇게 머신러닝은 여러개의 단계를 거쳐서 수행이 되는데, 각 단계가 끝나면 다음 단계를 수행해야 한다. 단순하게 CRON+쉘로 순차적으로 수행하는 것등이 가능하지만, 에러가 났을때 재처리를 하거나 , 수행 결과에 따라 분기를 하는 등 조금 더 구조화된 도구가 필요하다.

데이타 워크 플로우 관리 도구

이런 요구 사항 때문에 여러가지 툴이 개발되었는데, 대표적인 도구로는 하둡 에코시스템에 우지(oozie ) 등이 있다.



<그림. Oozie eclipse 클라이언트 >


하둡의 여러 에코 시스템 솔루션들을 유기적으로 조합하기 위해서 개발된 도구로, 하둡 에코 시스템에 있는 여러가지 다양한 솔루션과 연동하기 위한 아답터를 가지고 있다.

이외에도 rundeck, luigi와 같은 유사한 솔루션들이 있다.

오늘 소개하고자 하고자하는 데이타 워크 플로우 관리도구는 아파치 오픈소스 airflow 이다. 원래 airbnb에서 개발된 도구로 현재 아파치 오픈소스에서 인큐베이터 단계에 있는 소프트웨어이다.


airflow를 소개하는 이유는 첫번째 파이썬 기반으로 태스크 코드를 작성할 수 있기 때문에, 데이타 분석이나 머신러닝을 개발하는 엔지니어들에게 익숙한 언어이고, 한대에서 동작하는게 아니라 여러 머신에 분산하여 수행 될 수 있는 장점을 가지고 있다.



<그림. Apache airflow 의 작업 그래프 구조 화면 >

airflow 시작하기

그러면 간단하게 airflow에 대한 개념과 사용법에 대해서 알아보자

airflow 설치

airflow는 실행되는 작업의 상태등을 저장하기 위해서 데이타 베이스 (MySQL이나 Postgres)등이 필요하며, 분산 환경을 위해서 여러대에 설치할 수 있다. 또한 로컬 환경에 sqlite와 함께 간단하게 설치할 수 있다. 여기서는 간단하게 개인 맥북환경에 로컬로 설치 및 실행하는 시나리오로 설명한다.


설치 방법은 매우 간단하다. 파이썬 2.7 환경에서 아래와 같이 간단학 “pip install airflow”만 실행해주면 된다.

%pip install airflow



airflow가 설치되었으면 데이타 베이스 설정을 해줘야 하는데, 이 튜토리얼에서는 개발 및 테스트를 위해서 sqlite를 사용한다. sqlite를 초기화 하기 위해서 다음과 같이 “airflow initdb” 명령을 실행한다.

% airflow initdb


자아 이제 설치가 끝나고 airflow를 사용할 준비가 되었다. 이제 airflow 웹콘솔을 기동해보자

“airflow webserver -p 8080” 을 실행하고 웹에 http://localhost:8080에 접근하면 airflow 콘솔을 볼 수 있다.

airflow 코드

airflow에서 워크플로우를 저장하기 위해서 몇가지 추상화된 개념을 사용한다.

Airflow DAG의 구조

DAG (Directed Acyclic Graph)

DAG는 하나의 워크 플로우라고 보면 된다. 위의 예제처럼, 머신러닝 이라는 DAG를 정의한다면, Preprocessing,Training,Prediction 워크플로우가 하나의 DAG가 된다.

Operator and Task

Operator는 DAG안에서 정의되는 작업 함수(함수임을 주의하자) 이야기 하는데, Pre processing, Training, Prediction 함수가 Operatorator 이다.

이 Operator 함수가 DAG 상에서 실제로 워크플로우 상에 정의되서 호출 되면 이것이 Task 이다.

객체지향 언어에서 Operator가 class 라면, Task는 object 라고 보면 된다.


이해가 잘안될 수 있는데, 코드를 보자


from airflow import DAG

from airflow.operators.bash_operator import BashOperator

from airflow.operators.dummy_operator import DummyOperator

from airflow.operators.python_operator import PythonOperator

from datetime import datetime,timedelta


dag = DAG('hello-airflow',description='Hello airflow DAG',

         schedule_interval = '*/5 0 * * *',

         start_date=datetime(2017,07,01),catchup=False)


def print_hello():

   return 'Hello Airflow'


python_task = PythonOperator(

                   task_id='python_operator',

                   python_callable = print_hello,

                   dag = dag)


bash_task = BashOperator(

       task_id='print_date',

       bash_command='date',

       dag=dag)


bash_task.set_downstream(python_task)


DAG 정의 부분을 보자. DAG 객체는 DAG에 대한 전체 컨택스를 저장 및 유지 관리한다.

DAG('hello-airflow',description='Hello airflow DAG', 에서 DAG를 이름을 ‘hello-airflow’로 정의하고 description에 설명을 적는다.

schedule_interval = '*/5 * * * *', 다음으로 이 DAG가 실행되는 주기를 정해야 하는데, cron 명령과 같은 노테이션으로 정의한다. 위 설정은 매 5분마다 실행되도록 하는 설정이다.

마지막으로, start_date=datetime(2017,07,01), ,DAG를 언제부터 시작할것인지 지정한다. DAG는 반드시 전역 변수로 지정한다. DAG안에서 다른 DAG를 부르는 sub DAG의 경우에는 지역 변수로 지정이 가능하다.


다음 task에 사용할 operator를 정의하는데, 파이썬 코드를 실행할 오퍼레이터인 PythonOperator와 쉘 커맨드를 실행할 BashOperator를 가지고 각각 파이썬 태스크 python_task와, 쉘 태스크 bash_task를 정의한다.


python_task = PythonOperator(

                   task_id='python_operator',

                   python_callable = print_hello,

                   dag = dag)


파이썬 태스크의 id는 “python_operator”라고 지정하였고, 실행시 print_hello를 호출하도록 하였다.

그리고 이 태스크는 DAG인 dag에 지정한다.


다음 쉘 태스크의 내용은 다음과 같다.

bash_task = BashOperator(

       task_id='print_date',

       bash_command='date',

       dag=dag)


print_data라는 이름으로 태스크를 정의하고, 쉘 명령어 date를 실행하도록 하였다.

등록

코드 작성이 끝나면 코드를 배포해보자. Dag 파일을 airflow에 등록해야 하는데, dag 파일을 저장하는 장소는 dags_folder 라는 변수로 $AIRFLOW_HOME/airflow.cfg 파일안에 정의 되어 있다. 디폴트 장소는 $AIRFLOW_HOME/dags/ 폴더이다. 위에서 작성한 코드를 해당 디렉토리에 복사하자

다음 dag이 제대로 등록되었는지를 확인한다. 커멘드 창에서 “airflow list_dags”라는 명령을 수행하면 현재 등록되어 있는 DAG 목록을 볼 수 있다. 아래 그림과 같이 hello-airflow dag가 등록된것을 확인할 수 있다.




hello-airflow dag안에 어떤 태스크들이 정의되어 있는지를 확인하려면 ‘airflow list_tasks hello-airflow’ 명령을 이용하면 hello-airflow 안에 등록된 태스크 목록을 출력해준다.


테스트

테스트를 하려면 태스크 단위로 테스트가 가능하다. airflow test {DAG ID} {태스크 ID} {실행날짜} 식으로 하면 된다.

, 예를 들어 print_date 태스크를 2017-07-01을 기준으로 실행하고자하면 airflow test hello-airflow print_date 2017-07-01

Hello-airflow DAG안에 print_date라는 태스크를 실행한다.



실행

DAG 코드 개발 등록과 테스트가 완료되었으면 이제 airflow scheduler 를 띄워준다. (일종의 데몬이다.) 스케쥴러는 DAG 코드에 정의된 스케쥴에 따라서 테스크를 실행해준다.

스케쥴러 실행은 간단하게 airflow scheduler 명령을 실행하면 된다.



스케쥴러가 실행되면, 각 DAG의 스케쥴에 따라서 자동으로 태스크들을 수행한다.


로그 모니터링

스케쥴러에 의해서 실행되는 DAG와 태스크들의 결과와 로그는 어떻게 모니터링 할까? airflow에 의해서 수행되는 태스크들은 $AIRFLOW_HOME/logs 디렉토리에 저장된다.

logs 디렉토리 아래에 각각 DAG 이름으로 저장이 되며, DAG 이름으로 된 디레토리안에는 태스크명으로 된 서브 디렉토리가 있고, 이 서브 디렉토리 아래에 시간대별 로그가 있다.

즉 hello-airflow DAG의 print_date 태스크에 대한 로그는 $AIRFLOW_HOME/logs/hello-airflow/print_date/{날짜및시간} 파일 명으로 저장된다.

웹 콘솔을 이용한 모니터링

airflow의 강력한 기능중의 하나는 웹 기반의 모니터링 콘솔을 제공한다. 뒤에서는 주요 웹 콘솔의 주요 기능에 대해서 알아보도록 한다.

Graph View

Graph View는 DAG의 구조를 그래프 형태로 보여주는 뷰이다.


복잡한 워크플로우의 경우 그 구조를 파악하는데 유용한다. 위의 그림은 앞서 만든 hello-airflow 에 대한 태스크간 그래프로 print_date를 호출한 후에, python_operator 태스크를 호출하는 것을 볼 수 있다.

Tree View


트리뷰를 보면, DAG의 구조를 트리 형태로 보여주고, DAG의 태스크가 각각 성공했는지 실패 했는지를 우측 그래프 처럼 표현해준다. 각 태스크를 로그를 보려면 각 태스크 실행 결과 그래프를 누르면 아래와 같이 세부 메뉴가 나온다.



여기서 View Log를 누르면 각 Task 별로 실행 당시의 로그를 볼 수 있다. 아래는 Python_Operator 태스크를 실행한 로그이다.



아래서 두번째 줄을 보면 Hello Airflow 라는 문자열을 리턴한것을 확인할 수 있다.


Task Duration

Task duration은 DAG에서 수행된 각 태스크의 수행 시간을 그래프 형태로 나타내준다.



어떤 태스크가 시간이 많이 걸리는지 그리고 수행시간이 매번 수행할때 마다 올바른지 (큰 변화가 없고 일정한지. 이건 매우 유용할듯) 등을 체크할 수 있다.

Task Tries


Task Tries 에서는 각 수행별로 각각의 태스크를 수행한 횟수를 그래프로 보여준다. 즉 재시도 (RETRY)횟수를 모니터링할 수 있다.


Gantt


Gantt 차트는 각 수행에 대해서 태스크들의 수행 순서에 따라서 소모된 시간과 함께 간트 차트로 표시해준다.

앞의 차트에서 이미 얻을 수 있는 뷰이지만, 각 태스크의 수행 순서와 태스크당 시간을 한꺼번에 보여주기 때문에 병목 구간 파악이 쉽다.


<그림 airflow gantt chart 그래프 예제 (출처 : https://www.agari.com/airflow-agari/) >


이미 링크드인의 Azkaban이나, 스포티파이의 Luigi, 하둡의 Oozie 등 여러가지 워크 플로우 관리 시스템이 있지만, 아직 인큐베이터 단계인 airflow를 주목하는 이유는 분산 환경 지원이 가능하고, 태스크에 대한 스크립트를 파이썬을 사용할 수 있기 때문에, 각종 빅데이타 분석 시스템이나 라이브러리 그리고 머신러닝 시스템과 연동이 쉽고, 파이썬 언어만 알면 쉽게  정교한 플로우 개발이 가능하기 때문에, ( XML등의 설정을 하지 않고도) 활용 가능성이 높다.

연예인 얼굴 인식 모델을 만들어보자

#2 CNN 모델을 만들고 학습 시켜보기

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

선행 학습 자료

이 글은 딥러닝 컨볼루셔널 네트워크 (이하 CNN)을 이용하여 사람의 얼굴을 인식하는 모델을 만드는 튜토리얼이다. 이 글을 이해하기 위해서는 머신러닝과 컨볼루셔널 네트워크등에 대한 사전 지식이 필요한데, 사전 지식이 부족한 사람은 아래 글을 먼저 읽어보기를 추천한다.

 

머신러닝의 개요 http://bcho.tistory.com/1140

머신러닝의 기본 원리는 http://bcho.tistory.com/1139

이산 분류의 원리에 대해서는 http://bcho.tistory.com/1142

인공 신경망에 대한 개념은 http://bcho.tistory.com/1147

컨볼루셔널 네트워크에 대한 개념 http://bcho.tistory.com/1149

학습용 데이타 전처리 http://bcho.tistory.com/1176

학습용 데이타 전처리를 스케일링 하기 http://bcho.tistory.com/1177

손글씨를 CNN을 이용하여 인식하는 모델 만들기 http://bcho.tistory.com/1156

손글씨 인식 CNN 모델을 이용하여 숫자 인식 하기 http://bcho.tistory.com/1157

환경

본 예제는 텐서플로우 1.1과 파이썬 2.7 그리고 Jupyter 노트북 환경 및 구글 클라우드를 사용하여 개발되었다.

준비된 데이타

학습에 사용한 데이타는 96x96 사이즈의 얼굴 이미지로, 총 5명의 사진(안젤리나 졸리, 니콜키드만, 제시카 알바, 빅토리아 베컴,설현)을 이용하였으며, 인당 학습 데이타 40장 테스트 데이타 10장으로 총 250장의 얼굴 이미지를 사용하였다.

사전 데이타를 준비할때, 정면 얼굴을 사용하였으며, 얼굴 각도 변화 폭이 최대한 적은 이미지를 사용하였다. (참고 : https://www.slideshare.net/Byungwook/ss-76098082 ) 만약에 이 모델로 학습이 제대로 되지 않는다면 학습에 사용된 데이타가 적절하지 않은것이기 때문에 데이타를 정재해서 학습하기를 권장한다.

데이타 수집 및 정재 과정에 대한 내용은 http://bcho.tistory.com/1177 를 참고하기 바란다.

 

컨볼루셔널 네트워크 모델

얼굴 인식을 위해서, 머신러닝 모델 중 이미지 인식에 탁월한 성능을 보이는 CNN 모델을 사용하였다. 테스트용 모델이기 때문에 모델은 복잡하지 않게 설계하였다.

 

학습과 예측에 사용되는 이미지는 96x96픽셀의 RGB 컬러 이미지를 사용하였다.

아래 그림과 같은 모델을 사용했는데, 총 4개의 Convolutional 계층과, 2개의 Fully connected 계층, 하나의 Dropout 계층을 사용하였다.


Convolutional 계층의 크기는 각각 16,32,64,128개를 사용하였고, 사용된 Convolutional 필터의 사이즈는 3x3 이다.

Fully connected 계층은 각각 512, 1024를 사용하였고 Dropout 계층에서는 Keep_prob값을 0.7로 둬서 30%의 뉴론이 drop out 되도록 하여 학습을 진행하였다.

 

학습 결과 5개의 카테고리에 대해서 총 200장의 이미지로 맥북 프로 i7 CPU 기준 7000 스텝정도의 학습을 진행한 결과 테스트 정확도 기준 90% 정도의 정확도를 얻을 수 있었다.

코드 설명

텐서플로우로 구현된 코드를 살펴보자

파일에서 데이타 읽기

먼저 학습 데이타를 읽어오는 부분이다.

학습과 테스트에서 읽어드리는 데이타의 포맷은 다음과 같다

 

/Users/terrycho/training_data_class5_40/validate/s1.jpg,Sulhyun,3

이미지 파일 경로, 사람 이름 , 숫자 라벨

 

파일에서 데이타를 읽어서 처리 하는 함수는 read_data_batch(), read_data(), get_input_queue()  세가지 함수가 사용된다.

  • get_input_queue() 함수는 CSV 파일을 한줄씩 읽어서, 파일 경로 및 숫자 라벨 두가지를 리턴할 수 있는 큐를 만들어서 리턴한다.

  • read_data() 함수는 get_input_queue()에서 리턴한 큐로 부터 데이타를 하나씩 읽어서 리턴한다.

  • read_batch_data()함수는 read_data() 함수를 이용하여, 데이타를 읽어서 일정 단위(배치)로 묶어서 리턴을 하고, 그 과정에서 이미지 데이타를 뻥튀기 하는 작업을 한다.

즉 호출 구조는 다음과 같다.

 

read_batch_data():

 → Queue = get_input_queue()

 → image,label = read_data(Queue)

 → image_data = 이미지 데이타 뻥튀기

Return image_data,label

 

실제 코드를 보자

get_input_queue

get_input_queue() 함수는 CSV 파일을 읽어서 image와 labels을 리턴하는 input queue를 만들어서 리턴하는 함수이다.

 

def get_input_queue(csv_file_name,num_epochs = None):

   train_images = []

   train_labels = []

   for line in open(csv_file_name,'r'):

       cols = re.split(',|\n',line)

       train_images.append(cols[0])

       # 3rd column is label and needs to be converted to int type

       train_labels.append(int(cols[2]) )

                           

   input_queue = tf.train.slice_input_producer([train_images,train_labels],

                                              num_epochs = num_epochs,shuffle = True)

   

   return input_queue

 

CSV 파일을 순차적으로 읽은 후에, train_images와 train_labels라는 배열에 넣은 다음 tf.train.slice_input_producer를 이용하여 큐를 만들어냈다. 이때 중요한 점은 shuffle=True라는 옵션을 준것인데, 만약에 이 옵션을 주지 않으면, 학습 데이타를 큐에서 읽을때 CSV에서 읽은 순차적으로 데이타를 리턴한다. 즉 현재 데이타 포맷은 Jessica Alba가 40개, Jolie 가 40개, Nicole Kidman이 40개 .. 식으로 순서대로 들어가 있기 때문에, Jessica Alba를 40개 리턴한 후 Jolie를 40개 리턴하는 식이 된다.  이럴 경우 Convolutional 네트워크가 Jessica Alba에 치우쳐지기 때문에 제대로 학습이 되지 않는다. Shuffle은 필수이다.

read_data()

input_queue에서 데이타를 읽는 부분인데 특이한 점은 input_queue에서 읽어드린 이미지 파일명의 파일을 읽어서 데이타 객체로 저장해야 한다. 텐서플로우에서는 tf.image.decode_jpeg, tf.image.decode_png 등을 이용하여 이러한 기능을 제공한다.

def read_data(input_queue):

   image_file = input_queue[0]

   label = input_queue[1]

   

   image =  tf.image.decode_jpeg(tf.read_file(image_file),channels=FLAGS.image_color)

   

   return image,label,image_file

read_data_batch()

마지막으로 read_data_batch() 함수 부분이다.get_input_queue에서 읽은 큐를 가지고 read_data함수에 넣어서 이미지 데이타와 라벨을 읽어서 리턴하는 값을 받아서 일정 단위로 (배치) 묶어서 리턴하는 함수이다. 중요한 부분이 데이타를 뻥튀기 하는 부분이 있다.

이 모델에서 학습 데이타가 클래스당 40개 밖에 되지 않기 때문에 학습데이타가 부족하다. 그래서 여기서 사용한 방법은 read_data에서 리턴된 이미지 데이타에 대해서 tf.image.random_xx 함수를 이용하여 좌우를 바꾸거나, brightness,contrast,hue,saturation 함수를 이용하여 매번 색을 바꿔서 리턴하도록 하였다.

 

def read_data_batch(csv_file_name,batch_size=FLAGS.batch_size):

   input_queue = get_input_queue(csv_file_name)

   image,label,file_name= read_data(input_queue)

   image = tf.reshape(image,[FLAGS.image_size,FLAGS.image_size,FLAGS.image_color])

   

   # random image

   image = tf.image.random_flip_left_right(image)

   image = tf.image.random_brightness(image,max_delta=0.5)

   image = tf.image.random_contrast(image,lower=0.2,upper=2.0)

   image = tf.image.random_hue(image,max_delta=0.08)

   image = tf.image.random_saturation(image,lower=0.2,upper=2.0)

   

   batch_image,batch_label,batch_file = tf.train.batch([image,label,file_name],batch_size=batch_size)

   #,enqueue_many=True)

   batch_file = tf.reshape(batch_file,[batch_size,1])

 

   batch_label_on_hot=tf.one_hot(tf.to_int64(batch_label),

       FLAGS.num_classes, on_value=1.0, off_value=0.0)

   return batch_image,batch_label_on_hot,batch_file

 

그리고 마지막 부분에 label을 tf.one_hot을 이용해서 변환한것을 볼 수 있는데, 입력된 label은 0,1,2,3,4 과 같은 단일 정수이다. 그런데, CNN에서 나오는 결과는 정수가 아니라 클래스가 5개인 (분류하는 사람이 5명이기 때문에) 행렬이다. 즉 Jessica Alba일 가능성이 90%이고, Jolie일 가능성이 10%이면 결과는 [0.9,0.1,0,0,0] 식으로 리턴이 되기 때문에, 입력된 라벨 0은 [1,0,0,0,0], 라벨 1은 [0,1,0,0,0] 라벨 2는 [0,0,1,0,0] 식으로 변환되어야 한다. tf.one_hot 이라는 함수가 이 기능을 수행해준다.

 

모델 코드

모델은 앞서 설명했듯이 4개의 Convolutional 계층과, 2개의 Fully connected 계층 그리고 Dropout 계층을 사용한다. 각각의 계층별로는 코드가 다르지 않고 인지만 다르니 하나씩 만 설명하도록 한다.

 

Convolutional 계층

아래 코드는 두번째 Convolutional 계층의 코드이다.

  • FLAGS.conv2_layer_size 는 이 Convolutional 계층의 뉴런의 수로 32개를 사용한다.

  • FLAGS.conv2_filter_size 는 필터 사이즈를 지정하는데, 3x3 을 사용한다.

  • FLAGS.stride2 = 1 는 필터의 이동 속도로 한칸씩 이동하도록 정의했다.

 

# convolutional network layer 2

def conv2(input_data):

   FLAGS.conv2_filter_size = 3

   FLAGS.conv2_layer_size = 32

   FLAGS.stride2 = 1

   

   with tf.name_scope('conv_2'):

       W_conv2 = tf.Variable(tf.truncated_normal(

                       [FLAGS.conv2_filter_size,FLAGS.conv2_filter_size,FLAGS.conv1_layer_size,FLAGS.conv2_layer_size],

                                             stddev=0.1))

       b2 = tf.Variable(tf.truncated_normal(

                       [FLAGS.conv2_layer_size],stddev=0.1))

       h_conv2 = tf.nn.conv2d(input_data,W_conv2,strides=[1,1,1,1],padding='SAME')

       h_conv2_relu = tf.nn.relu(tf.add(h_conv2,b2))

       h_conv2_maxpool = tf.nn.max_pool(h_conv2_relu

                                       ,ksize=[1,2,2,1]

                                       ,strides=[1,2,2,1],padding='SAME')

       

       

   return h_conv2_maxpool

 

다음 Weight 값 W_conv2 와 Bias 값 b2를 지정한후에, 간단하게 tf.nn.conv2d 함수를 이용하면 2차원의 Convolutional 네트워크를 정의해준다. 다음 결과가 나오면 이 결과를 액티베이션 함수인 relu 함수에 넣은 후에, 마지막으로 max pooling 을 이용하여 결과를 뽑아낸다.

 

각 값의 의미에 대해서는 http://bcho.tistory.com/1149 의 컨볼루셔널 네트워크 개념 글을 참고하기 바란다.

같은 방법으로 총 4개의 Convolutional 계층을 중첩한다.

 

Fully Connected 계층

앞서 정의한 4개의 Convolutional 계층을 통과하면 다음 두개의 Fully Connected 계층을 통과하게 되는데 모양은 다음과 같다.

  • FLAGS.fc1_layer_size = 512 를 통하여 Fully connected 계층의 뉴런 수를 512개로 지정하였다.

 

# fully connected layer 1

def fc1(input_data):

   input_layer_size = 6*6*FLAGS.conv4_layer_size

   FLAGS.fc1_layer_size = 512

   

   with tf.name_scope('fc_1'):

       # 앞에서 입력받은 다차원 텐서를 fcc에 넣기 위해서 1차원으로 피는 작업

       input_data_reshape = tf.reshape(input_data, [-1, input_layer_size])

       W_fc1 = tf.Variable(tf.truncated_normal([input_layer_size,FLAGS.fc1_layer_size],stddev=0.1))

       b_fc1 = tf.Variable(tf.truncated_normal(

                       [FLAGS.fc1_layer_size],stddev=0.1))

       h_fc1 = tf.add(tf.matmul(input_data_reshape,W_fc1) , b_fc1) # h_fc1 = input_data*W_fc1 + b_fc1

       h_fc1_relu = tf.nn.relu(h_fc1)

   

   return h_fc1_relu

 

Fully connected 계층은 단순하게 relu(W*x + b) 함수이기 때문에 이 함수를 위와 같이 그대로 적용하였다.

마지막 계층

Fully connected 계층을 거쳐 나온 데이타는 Dropout 계층을 거친후에, 5개의 카테고리에 대한 확률로 결과를 내기 위해서 final_out 계층을 거치게 되는데, 이 과정에서 softmax 함수를 사용해야 하나, 학습 과정에서는 별도로 softmax 함수를 사용하지 않는다. softmax는 나온 결과의 합이 1.0이 되도록 값을 변환해주는 것인데, 학습 과정에서는 5개의 결과 값이 어떤 값이 나오던 가장 큰 값에 해당하는 것이 예측된 값이기 때문에, 그 값과 입력된 라벨을 비교하면 되기 때문이다.

즉 예를 들어 Jessica Alba일 확률이 100%면 실제 예측에서는 [1,0,0,0,0] 식으로 결과가 나와야 되지만, 학습 중는 Jessica Alaba 로 예측이 되었다고만 알면 되기 때문에 결과가 [1292,-0.221,-0.221,-0.221] 식으로 나오더라도 최대값만 찾으면 되기 때문에 별도로 softmax 함수를 적용할 필요가 없다. Softmax 함수는 연산 비용이 큰 함수이기 때문에 일반적으로 학습 단계에서는 적용하지 않는다.

 

마지막 계층의 코드는 다음과 같다.

# final layer

def final_out(input_data):

 

   with tf.name_scope('final_out'):

       W_fo = tf.Variable(tf.truncated_normal([FLAGS.fc2_layer_size,FLAGS.num_classes],stddev=0.1))

       b_fo = tf.Variable(tf.truncated_normal(

                       [FLAGS.num_classes],stddev=0.1))

       h_fo = tf.add(tf.matmul(input_data,W_fo) , b_fo) # h_fc1 = input_data*W_fc1 + b_fc1

       

   # 최종 레이어에 softmax 함수는 적용하지 않았다.

       

   return h_fo

전체 네트워크 모델 정의

이제 각 CNN의 각 계층을 함수로 정의 하였으면 각 계층을 묶어 보도록 하자. 묶는 법은 간단하다 앞 계층에서 나온 계층을 순서대로 배열하고 앞에서 나온 결과를 뒤의 계층에 넣는 식으로 묶으면 된다.

 

# build cnn_graph

def build_model(images,keep_prob):

   # define CNN network graph

   # output shape will be (*,48,48,16)

   r_cnn1 = conv1(images) # convolutional layer 1

   print ("shape after cnn1 ",r_cnn1.get_shape())

   

   # output shape will be (*,24,24,32)

   r_cnn2 = conv2(r_cnn1) # convolutional layer 2

   print ("shape after cnn2 :",r_cnn2.get_shape() )

   

   # output shape will be (*,12,12,64)

   r_cnn3 = conv3(r_cnn2) # convolutional layer 3

   print ("shape after cnn3 :",r_cnn3.get_shape() )

 

   # output shape will be (*,6,6,128)

   r_cnn4 = conv4(r_cnn3) # convolutional layer 4

   print ("shape after cnn4 :",r_cnn4.get_shape() )

   

   # fully connected layer 1

   r_fc1 = fc1(r_cnn4)

   print ("shape after fc1 :",r_fc1.get_shape() )

 

   # fully connected layer2

   r_fc2 = fc2(r_fc1)

   print ("shape after fc2 :",r_fc2.get_shape() )

   

   ## drop out

   # 참고 http://stackoverflow.com/questions/34597316/why-input-is-scaled-in-tf-nn-dropout-in-tensorflow

   # 트레이닝시에는 keep_prob < 1.0 , Test 시에는 1.0으로 한다.

   r_dropout = tf.nn.dropout(r_fc2,keep_prob)

   print ("shape after dropout :",r_dropout.get_shape() )

   

   # final layer

   r_out = final_out(r_dropout)

   print ("shape after final layer :",r_out.get_shape() )

 

   return r_out

 

이 build_model 함수는 image 를 입력 값으로 받아서 어떤 카테고리에 속할지를 리턴하는 컨볼루셔널 네트워크이다.  중간에 Dropout 계층이 추가되어 있는데, tf.nn.dropout함수를 이용하면 간단하게 dropout 계층을 구현할 수 있다. r_fc2는 Dropout 계층 앞의 Fully Connected 계층에서 나온 값이고,  두번째 인자로 남긴 keep_prob는 Dropout 비율이다.

 

   r_dropout = tf.nn.dropout(r_fc2,keep_prob)

   print ("shape after dropout :",r_dropout.get_shape() )

 

모델 학습

데이타를 읽는 부분과 학습용 모델 정의가 끝났으면 실제로 학습을 시켜보자

 

def main(argv=None):

   

   # define placeholders for image data & label for traning dataset

   

   images = tf.placeholder(tf.float32,[None,FLAGS.image_size,FLAGS.image_size,FLAGS.image_color])

   labels = tf.placeholder(tf.int32,[None,FLAGS.num_classes])

   image_batch,label_batch,file_batch = read_data_batch(TRAINING_FILE)

 

먼저 학습용 모델에 넣기 위한 image 데이타를 읽어드릴 placeholder를 images로 정의하고, 다음으로 모델에 의해 계산된 결과와 비교하기 위해서 학습데이타에서 읽어드린 label 데이타를 저장하기 위한 placeholder를 labels로 정의한다. 다음 image_batch,label_batch,fle_batch 변수에 배치로 학습용 데이타를 읽어드린다. 그리고 dropout 계층에서 dropout 비율을 지정할 keep_prob를 place holder로 정의한다.

각 변수가 지정되었으면, build_model 함수를 호출하여, images 값과 keep_prob 값을 넘겨서 Convolutional 네트워크에 값을 넣도록 그래프를 정의하고 그 결과 값을 prediction으로 정의한다.

 

   keep_prob = tf.placeholder(tf.float32) # dropout ratio

   prediction = build_model(images,keep_prob)

   # define loss function

   loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction,labels=labels))

   tf.summary.scalar('loss',loss)

 

   #define optimizer

   optimizer = tf.train.AdamOptimizer(FLAGS.learning_rate)

   train = optimizer.minimize(loss)

 

중간 중간에 학습 과정을 시각화 하기 위해서 tf.summary.scalar 함수를 이용하여 loss 값을 저장하였다.

 

그래프 생성이 완료 되었으면, 학습에서 계산할 비용 함수를 정의한다. 비용함수는 sofrmax cross entopy 함수를 이용하여, 모델에 의해서 예측된 값 prediction 과, 학습 파일에서 읽어드린 label 값을 비교하여 loss 값에 저장한다.

그리고 이 비용 최적화 함수를 위해서 옵티마이져를 AdamOptimizer를 정의하여, loss 값을 최적화 하도록 하였다.

 

학습용 모델 정의와, 비용 함수, 옵티마이저 정의가 끝났으면 학습 중간 중간 학습된 모델을 테스트하기 위한 Validation 관련 항목등을 정의한다.

 

   # for validation

   #with tf.name_scope("prediction"):

   validate_image_batch,validate_label_batch,validate_file_batch = read_data_batch(VALIDATION_FILE)

   label_max = tf.argmax(labels,1)

   pre_max = tf.argmax(prediction,1)

   correct_pred = tf.equal(tf.argmax(prediction,1),tf.argmax(labels,1))

   accuracy = tf.reduce_mean(tf.cast(correct_pred,tf.float32))

           

   tf.summary.scalar('accuracy',accuracy)

      

   startTime = datetime.now()

 

학습용 데이타가 아니라 검증용 데이타를 VALIDATION_FILE에서 읽어서 데이타를 validate_image_batch,validate_label_batch,validate_file_batch에 저장한다. 다음, 정확도 체크를 위해서 학습에서 예측된 라벨값과, 학습 데이타용 라벨값을 비교하여 같은지 틀린지를 비교하고, 이를 가지고 평균을 내서 정확도 (accuracy)로 사용한다.

 

학습용 모델과, 테스트용 데이타 등이 준비되었으면 이제 학습을 시작한다.

학습을 시직하기 전에, 학습된 모델을 저장하기 위해서 tf.train.Saver()를 지정한다. 그리고, 그래프로 loss와 accuracy등을 저장하기 위해서 Summary write를 저장한다.

다음 tf.global_variable_initializer()를 수행하여 변수를 초기화 하고, queue에서 데이타를 읽기 위해서 tf.train.Corrdinator를 선언하고 tf.start_queue_runners를 지정하여, queue 러너를 실행한다.

 

   #build the summary tensor based on the tF collection of Summaries

   summary = tf.summary.merge_all()

   

   with tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)) as sess:

       saver = tf.train.Saver() # create saver to store training model into file

       summary_writer = tf.summary.FileWriter(FLAGS.log_dir,sess.graph)

       

       init_op = tf.global_variables_initializer() # use this for tensorflow 0.12rc0

       coord = tf.train.Coordinator()

       threads = tf.train.start_queue_runners(sess=sess, coord=coord)

       sess.run(init_op)

 

변수 초기화와 세션이 준비되었기 때문에 이제 학습을 시작해보자. for 루프를 이용하여 총 10,000 스텝의 학습을 하도록 하였다.

 

       for i in range(10000):

           images_,labels_ = sess.run([image_batch,label_batch])

 

다음 image_batch와 label_batch에서 값을 읽어서 앞에서 정의한 모델에 넣고 train 그래프 (AdamOptimizer를 정의한)를 실행한다.

 

           sess.run(train,feed_dict={images:images_,labels:labels_,keep_prob:0.7})

 

이때 앞에서 읽은 images_와, labels_ 데이타를 피딩하고 keep_prob 값을 0.7로 하여 30% 정도의 값을 Dropout 시킨다.

 

다음 10 스텝 마다 학습 상태를 체크하도록 하였다.

           

           if i % 10 == 0:

               now = datetime.now()-startTime

               print('## time:',now,' steps:',i)         

               

               # print out training status

               rt = sess.run([label_max,pre_max,loss,accuracy],feed_dict={images:images_

                                                         , labels:labels_

                                                         , keep_prob:1.0})

               print ('Prediction loss:',rt[2],' accuracy:',rt[3])

위와 같이 loss 값과 accuracy 값을 받아서 출력하여 현재 모델의 비용 함수 값과 정확도를 측정하고

 

               # validation steps

               validate_images_,validate_labels_ = sess.run([validate_image_batch,validate_label_batch])

               rv = sess.run([label_max,pre_max,loss,accuracy],feed_dict={images:validate_images_

                                                         , labels:validate_labels_

                                                         , keep_prob:1.0})

               print ('Validation loss:',rv[2],' accuracy:',rv[3])

학습용 데이타가 아니라 위와 같이 테스트용 데이타를 피딩하여, 테스트용 데이타로 정확도를 검증한다. 이때 keep_prob를 1.0으로 해서 Dropout 없이 100% 네트워크를 활용한다.

 

               if(rv[3] > 0.9):

                   Break

 

만약에 테스트 정확도가 90% 이상이면 학습을 멈춘다. 그리고 아래와 같이 Summary

 

               # validation accuracy

               summary_str = sess.run(summary,feed_dict={images:validate_images_

                                                         , labels:validate_labels_

                                                         , keep_prob:1.0})

 

               summary_writer.add_summary(summary_str,i)

               summary_writer.flush()

 

마지막으로 다음과 같이 학습이 다된 모델을 saver.save를 이용하여 저장하고, 사용된 리소스들을 정리한다.

       saver.save(sess, 'face_recog') # save session

       coord.request_stop()

       coord.join(threads)

       print('finish')

   

main()

 

이렇게 학습을 끝내면 본인의 경우 약 7000 스텝에서 테스트 정확도 91%로 끝난것을 확인할 수 있다.

 

아래는 텐서보드를 이용하여 학습 과정을 시각화한 내용이다.

 


 

코드는 공개가 가능하지만 학습에 사용한 데이타는 저작권 문제로 공유가 불가능하다. 약 200장의 사진만 제대로 수집을 하면 되기 때문에 각자 수집을 해서 학습을 도전해보는 것을 권장한다. (더 많은 인물에 대한 시도를 해보는것도 좋겠다.)

정리 하며

혹시나 이 튜토리얼을 따라하면서 학습 데이타를 공개할 수 있는 분들이 있다면 다른 분들에게도 많은 도움이 될것이라고 생각한다. 가능하면 데이타가 공개되었으면 좋겠다.

전체 코드는 https://github.com/bwcho75/facerecognition/blob/master/1.%2BFace%2BRecognition%2BTraining.ipynb 에 있다.

그리고 직접 사진을 수집해보면, 데이타 수집 및 가공이 얼마나 어려운지 알 수 있기 때문에 직접 한번 시도해보는 것도 권장한다. 아래는 크롬브라우져 플러그인으로 구글 검색에서 나온 이미지를 싹 긁을 수 있는 플러그인이다. Bulk Download Images (ZIG)

https://www.youtube.com/watch?v=k5ioaelzEBM

 



이 플러그인을 이용하면 손쉽게 특정 인물의 데이타를 수집할 수 있다.

다음 글에서는 학습이 끝난 데이타를 이용해서 실제로 예측을 해보는 부분에 대해서 소개하도록 하겠다.

 

 

 

텐서보드를 이용하여 학습 과정을 시각화 해보자


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


텐서플로우로 머신러닝 모델을 만들어서 학습해보면, 각 인자에 어떤 값들이 학습이 진행되면서 어떻게 변화하는지 모니터링 하기가 어렵다. 앞의 예제들에서는 보통 콘솔에 텍스트로 loss 값이나, accuracy 값을 찍어서, 학습 상황을 봤는데, 텐서보다는 학습에 사용되는 각종 지표들이 어떻게 변화하는지 손쉽게 시각화를 해준다.


예를 들어 보면 다음 그림은 학습을 할때 마다 loss 값이 어떻게 변하는지를 보여주는 그래프이다.

가로축은 학습 횟수를 세로축은 모델의 loss 값을 나타낸다.





잘 보면 두개의 그래프가 그려져 있는 것을 볼 수 있는데, 1st 그래프는 첫번째 학습, 2nd 는 두번째 학습에서  추출한 loss 값이다.

Visualize Learning

그러면 어떻게 학습 과정을 시각화할 수 있는지를 알아보자

학습 과정을 시각화 하려면 학습중에 시각화 하려는 데이타를 tf.summary 모듈을 이용해서 중간중간에 파일로 기록해놨다가, 학습이 끝난 후에 이 파일을 텐서 보드를 통해서 읽어서 시각화 한다. 이를 위해서 다음과 같이 크게 4가지 메서드가 주로 사용이 된다.

  • tf.summary.merge_all
    Summary를 사용하기 위해서 초기화 한다.

  • tf.summary.scalar(name,value)
    Summary에 추가할 텐서를 정의 한다. name에는 이름, vallue에는 텐서를 정의한다. Scalar 형 텐서로 (즉 다차원 행렬이 아닌, 단일 값을 가지는 텐서형만 사용이 가능하다.) 주로 accuracy나 loss와 같은 스칼라형 텐서에 사용한다.

  • tf.summary.histogram(name,value)
    값(value) 에 대한 분포도를 보고자 할때 사용한다. .scalar와는 다르게 다차원 텐서를 사용할 수 있다. 입력 데이타에 대한 분포도나, Weight, Bias값의 변화를 모니터링할 수 있다.

  • tf.train.SummaryWriter
    파일에 summary 데이타를 쓸때 사용한다.


예제는 https://www.tensorflow.org/tutorials/mnist/tf/ 를 참고하면 된다.


mnist.py에서 아래와 같이 loss 값을 모니터링 하기 위해서 tf.summary.scalar를 이용하여 ‘loss’라는 이름으로 loss 텐서를 모니터링하기 위해서 추가하였다.


다음 fully_connected_feed.py에서

Summary를 초기화 하고, 세션이 시작된 후에, summary_writer를 아래와 같이 초기화 하였다.


이때, 파일 경로 (FLAGS.log_dir)을 설정하고, 텐서 플로우의 세션 그래프(sess.graph)를 인자로 넘긴다.




다음 트레이닝 과정에서, 100번마다, summary 값을 문자열로 변환하여, summary_writer를 이용하여 파일에 저장하였다.


트레이닝이 끝나면 위에서 지정된 디렉토리에 아래와 같이 summary 데이타 파일이 생성 된다.



이를 시각화 하려면 콘솔에서 tensorboard --logdir=”Summary 파일 디렉토리 경로" 를 지정해주면 6060 포트로 텐서보드 웹 사이트가 준비된다.



웹 브라우져를 열어서 localhost:6060에 접속해보면 다음과 같은 그림이 나온다.


Loss 값이 트레이닝이 수행됨에 따라 작아 지는 것을 볼 수 있다. (총 2000번 트레이닝을 하였다.)

세로축은 loss 값, 가로축은 학습 스텝이 된다.


만약에 여러번 학습을 하면서 모델을 튜닝했다면, 각 학습 별로 loss 값이나 accuracy 값이 어떻게 변하는지 그래프를 중첩하여 비교하고 싶을 수 있는데, 이 경우에는


% tensorboard --logdir=이름1:로그경로2,이름2:로그경로2,....


이런식으로 “이름:로그경로"를 ,로 구분하여 여러개를 써주면 그래프를 중첩하여 볼 수 있다.

아래는 1st, 2nd 두개의 이름으로 두개의 summary 로그를 중첩하여 시각화하여 각 학습 별로 loss 값이 어떻게 변화 하는지를 보여주는 그래프 이다.



Histogram

히스토 그램은 다차원 텐서에 대한 분포를 볼 수 있는 방법인데,

https://github.com/llSourcell/Tensorboard_demo 에 히스토그램을 텐서보드로 모니터링할 수 있는 좋은 샘플이 있다. 이 코드는 세개의 히든레이어를 갖는 뉴럴네트워크인데, (사실 좀 코드는 이상하다. Bias 값도 더하지 않았고, 일반 레이어 없이 dropout 레이어만 엮었다. 모델 자체가 맞는지 틀리는지는 따지지 말고 어떻게 Histogram을 모니터링 하는지를 살펴보자)


모델 그래프는 다음과 같다.




다음, 각 레이어에서 사용된 weight 값인 w_h,w_h2,w_o를 모니터링 하기 위해서 이 텐서들을 tf.historgram_summary를 이용하여 summary에 저장 한다.



이렇게 저장된 데이타를 텐서 보드로 시각화 해보면


Distribution 탭에서는 다음과 같은 값을 볼 수 있다.



w_h_summ 값의 분포인데, 세로 축은 w의 값, 가로축은 학습 횟수 이다.

학습이 시작되는 초기에는 w값이 0을 중심으로 좌우 대칭으로 모여 있는 것을 볼 수 있다. 잘 보면, 선이 있는 것을 볼 수 있는데, 색이 진할 수 록, 값이 많이 모여 있는 것이고 흐릴 수 록 값이 적게 있는 것이다.


다른 뷰로는 Histogram View를 보면, 다음과 같은 그래프를 볼 수 있는데,



세로축이 학습 횟수, 가로축이 Weight의 값이다.

그래프가 여러개가 중첩 되어 있는 것을 볼 수 있는데, 각각의 그래프는 각 학습시에 나온 Weight의 값으로, 위의 그래프에서 보면 중앙에 값이 집중되어 있다가, 아래 그래프를 보면 값이 점차적으로 옆으로 퍼지는 것을 볼 수 있다.


사실 개인적인 의견이지만 Weight 값의 분포를 보는 것이 무슨 의미를 가지는지는 잘 모르겠다. CNN에서 필터링 된 피쳐의 분포나, 또는 원본 데이타의 분포에는 의미가 있을듯하다.


머신러닝의 과학습 / 오버피팅의 개념


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


머신 러닝을 공부하다보면 자주 나오는 용어 중에 하나가 오버피팅 (Overfitting)이다.

과학습이라고도 하는데, 그렇다면 오버 피팅은 무엇일까?


머신 러닝을 보면 결과적으로 입력 받은 데이타를 놓고, 데이타를 분류 (Classification) 하거나 또는 데이타에 인접한 그래프를 그리는 (Regression) , “선을 그리는 작업이다.”

그러면 선을 얼마나 잘 그리느냐가 머신 러닝 모델의 정확도와 연관이 되는데, 다음과 같이 붉은 선의 샘플 데이타를 받아서, 파란선을 만들어내는 모델을 만들었다면 잘 만들어진 모델이다. (기대하는)


언더 피팅


만약에 학습 데이타가 모자라거나 학습이 제대로 되지 않아서, 트레이닝 데이타에 가깝게 가지 못한 경우에는 다음과 같이 그래프가 트레이닝 데이타에서 많이 떨어진것을 볼 수 있는데, 이를 언더 피팅 (under fitting)이라고 한다.



오버 피팅

오버 피팅은 반대의 경우로, 다음 그림과 같이 트레이닝 데이타에 그래프가 너무 정확히 맞아 들어갈때 발생한다.


샘플 데이타에 너무 정확하게 학습이 되었기 때문에, 샘플데이타를 가지고 판단을 하면 100%에 가까운 정확도를 보이지만 다른 데이타를 넣게 되면, 정확도가 급격하게 떨어지는 문제이ㅏㄷ.

오버피팅의 해결

이런 오버피팅 문제를 해결하는 방법으로는 여러가지가 있는데 대표적인 방법으로는

  • 충분히 많은 학습 데이타를 넣거나

  • 피쳐의 수를 줄이거나

  • Regularization (정규화)를 이용하는 방법이 있다.



그림 출처 : 출처 : https://kousikk.wordpress.com/2014/11/20/problem-of-overfitting-in-machine-learning/




수포자를 위한 딥러닝


#2 - 선형회귀분석을 통한 머신러닝의 기본 개념 이해


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


Linear Regression을 통한 머신 러닝의 개념 이해

거리에 따른 택시 요금 문제

머신러닝이란 무엇일까? 개념 이해를 돕기 위해서 선형 회귀 (Linear Regression)이라는 머신러닝 모델을 보자. 먼저 선형 회귀 (Linear regression)이 무엇인지 부터 이해를 해야 하는데, 쉽게 설명하자면 결과값 (output value)이 있고 그 결과값을 결정할 것이라고 추정되는 입력값 (input value)과 결과 값의 연관관계를 찾는 것이고 이를 선형 관계를 통해 찾는 방법이 선형 회귀 (Linear regression)이다.

예를 들어서 설명해보자, 택시 요금을 예로 들어보자,택시 요금은 물론 막히냐 마냐에 따라 편차가 있지만, 대부분 거리에 비례해서 요금이 부과된다. 그래서 결과값 (요금)과 입력값 (거리)의 관계를 찾아야 한다.


거리별 요금을 그래프로 나타내보면 대략 다음과 같은 분포를 띄게 된다

원본 데이타의 거리를 x_data 그리고, 그 거리에서 측정된 택시 요금을 y_origin 이라고 하자.



가설 (Hypothesis) 정의

거리와 요금이 서로 비례하기 때문에, 거리(x_data)와 요금(y_data)간의 상관 관계는 다음과 같이 일차 방정식과 형태의 그래프를 그리게 된다고 가정하자. W (Weight)는 그래프의 각도, b는 bias를 뜻한다

y_data = Wx_data + b

이 일차 방정식 형태로 대충 1차원 그래프를 그려보자 같은 형태로 아래와 같이 그래프를 그려봤다.


그래프를 그려보니 그래프의 각이 안맞는것 같다. 그래프의 각도와 높이를 보정해보자


그래프를 보정했지만 또 안 맞는 것 같다. 그렇다면 최적의 그래프의 각도 W와, 높이 B는 어떻게 찾아야 하는 것일까?

코스트(비용) 함수

우리가 구하고자 하는 그래프는 실제 값에서 그래프의 값까지 차이가 가장 작은 값을 구하고자 하는 것이다. 아래 그림을 보자, 아래와 같이 y_data=Wx_data +b와 같은 그래프를 그렸다고 하자.


원래 값에서 우리가 예측한 값의 차이는 

(원래값과 계산된 값의 차이) = 측정값 - 그래프의 값

인데, 차이를 d라고 하자. 그리고 그래프에 의해서 계산된 값은 y_data라고 하면 택시 거리 x_data 에서 원래 측정된 값을 y_orgin라고 해서 수식으로 나타내면,

d = y_data - y_origin

이 된다. 이때 측정값은 여러개가 있기 때문에 n이라고 하면  n번째 측정한 택시비와 산식에 의해서 예측된 값의 차이는 dn이 된다.


dn = y_data_n - y_origin_n


즉 우리가 구하고자 하는 값은 dn의 합이 최소가 되는 W와 b의 값을 구하고자 하는 것이다.

다르게 설명하면 실제 측정한값과, 예측한 값의 차이가 최소가 되는 W와 b를 구하고자 하는 것이다.

dn은 위의 그래프에서 처럼 그래프 위에도 있을 수 있지만 (이경우 dn은 양수), 그래프 아래에도 있을 수 있기 때문에, (이경우 dn은 음수). 합을 구하면, 예측 선에서의 실측값 까지의 거리의 합이 되지 않기 때문에, dn에 대한 절대값을 사용한다고 하자.

그리고 n이 측정에 따라 여러개가 될 수 있기 때문에, 평균을 사용하자.


( ABS(d1)+ABS(d2)+ABS(d3)+.....+ABS(dn)) ) / n


즉 우리가 구하고자 하는 W와 b는 위의 함수의 값이 최소가 되는 값을 구하면 된다.

이렇게 측정된 값에서 연산된 값간의 차이를 연산하는 함수를 비용 함수 또는 영어로 코스트 함수 (Cost function이라고 한다.


사람이 일일이 계산할 수 없이니 컴퓨터를 이용해서 W=0.1,0.2,0.3,.... b=0.1,0.2,0.3,..... 식으로 넣어보고 이 코스트 함수가 가장 최소화되는 W와 b의 값을 찾을 수 있다.

옵티마이져 (Optimizer)

코스트 함수의 최소값을 찾는 알고리즘을 옵티마이져(Optimizer)라고 하는데, 상황에 따라 여러 종류의 옵티마이져를 사용할 수 있다. 여기서는 경사 하강법 (Gradient Descent) 라는 옵티마이져에 대해서 소개하도록 하겠다.


경사 하강법

그러면 W와 b를 구할때 W와 b를 어떤식으로 증가 또는 감소 시켜서 코스트 함수의 최소값을 가장 효율적으로 찾아낼 수 있을까? 위에서 언급한것 처럼 W를 0.0에서 부터 ). 0.1씩 증가시켜나가고 b도 같이 0.0에서 부터 1씩 증가 시켜 나갈까? 무한한 컴퓨팅 자원을 이용하면 되기는 하겠지만, 이렇게 무식하게 계산하지는 않는다.

코스트 함수를 최적화 시킬 수 있는 여러가지 방법이 있지만, Linear regression의 경우에는 경사 하강법 (그레이언트 디센트 : Gradient descent)라는 방식을 사용한다. 경사하강법에 대해서는 자세하게 알필요는 없고 ”대략 이런 개념을 사용하는 구나” 하는 정도만 알면 된다.


경사 하강법을 사용하기 위해서는 위의 코스트 함수를,측정값과 예측값의 절대값의 평균이 아니라 평균 제곱 오차라는 함수를 사용한다.

이 함수는 형식으로 정의되는데, 평균 제곱 오차 함수 (Mean square error function)이라고 한다.


Cost =  Sum( (y_data_n - y_origin_n) ^ 2) / n


풀어서 설명하면, n 번째의 원래데이타(y_origin_n)와 예측 데이타(y_data_n)의 차이를 제곱(^2)해서, 이 값을 n으로 나눈 평균 값이다.

즉 이 Cost가 최소가 되는 W와 b값을 구하면 된다.

편의상 W하나만을 가지고 설명해보자. 위의 그래프를 W와 b에 대한 상관 그래프로 그려보면 다음과 같은 함수 형태가 된다.


이 그래프에서 W에 대한 적정값에 대한 예측을 시작하는 점을 위의 그림에서 파란 점이라고 하면, 경사 하강법은 현재 W의 위치에 대해서, 경사가 아래로 되어 있는 부분으로 점을 움직이는 방법이다. 어느 방향으로 W를 움직이면 Cost 값이 작아지는지는 현재 W위치에서 비용 함수를 미분하면 된다. (고등학교 수학이 기억이 나지 않을 수 있겠지만 미분의 개념은 그래프에서 그 점에 대한 기울기를 구하는 것이다. )


이렇게, 경사를 따라서 아래로 내려가다 보면 Cost 함수가 최소화가 되는 W 값을 찾을 수 있다. 이렇게 경사를 따라서 하강 (내려가면서) 최소값을 찾는다고 하여 경사 하강법이라고 한다.  


학습

코스트 함수가 정의 되었으면 실제 데이타 x_data_n과 y_data_n을 넣어서 경사하강법에 의해서 코스트 함수가 최소가 되는 W와 b를 구한다. 이 작업은 W값을 변화시키면서 반복적으로 x_data_n로 계산을 하여, 실제 측정 데이타와 가설에 의해서 예측된 결과값에 대한 차이를 찾아내고 최적의 W와 b값을 찾아낸다.

예측

학습 과정에 의해서 최적의 W와 b를 찾았으면 이제, 이 값들을 이용해서 예측 해보자

학습에 의해서 찾아낸 W가 1600, b가 2000이라고 하면, 앞의 가설에서 정의한 함수는 Wx*b였기 때문에, 예측 함수는 


y = Wx +b

거리에 따른 택시비 = W*(거리) + b

거리에 따른 택시비 = 1600 * (거리) + 2000

이 되고, 이를 학습된 모델 이라고 한다.


이제 예측을 수행해보자, 거리가 10km일 때 택시비는 얼마일까? 공식에 따라

택시비 = 1600 * 10km + 2000

으로, 18000원이 된다.

머신 러닝의 순서

지금까지 택시 거리와 택시비에 대한 문제를 가지고 머신 러닝에 대한 기본 원리를 살펴보았다.

이를 요약해서 머신 러닝이란 것이 어떤 개념을 가지고 있는지 다시 정리해보자.


기본 개념은 데이타를 기반으로해서 어떤 가설 (공식)을 만들어 낸 다음, 그 가설에서 나온 값이 실제 측정값과의 차이(코스트 함수)가 최소한의 값을 가지도록 변수에 대한 값을 컴퓨터를 이용해서 찾은 후, 이 찾아진 값을 가지고 학습된 모델을 정의해서 예측을 수행 하는 것이다.  


학습 단계

즉 모델을 만들기 위해서, 실제 데이타를 수집하고, 이 수집된 데이타에서 어떤 특징(피쳐)를 가지고 예측을 할것인지 피쳐들을 정의한 다음에, 이 피쳐를 기반으로 예측을 한 가설을 정의하고, 이 가설을 기반으로 학습을 시킨다.


예측 단계

학습이 끝나면 모델 (함수)가 주어지고, 예측은 단순하게, 모델에 값을 넣으면, 학습된 모델에 의해서 결과값을 리턴해준다.


지금까지 Linear regression 분석을 통한 머신러닝의 원리에 대해서 간략하게 알아보았다. 다음 다음장에서는 이 모델을 어떻게 프로그래밍 언어를 이용하여 학습을 시키고 운영을 하는지에 대해서 알아보도록 하겠다.



Thanx to 

이글은 딥러닝 전문가 김홍회 박사님(Ayden Kim - https://www.facebook.com/Ayden.Kim )이 검수해주셨습니다. 감사합니다.


노트7의 소셜 반응을 분석해 보았다. 


#3 제플린 노트북을 이용한 상세 분석



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



데이타 스튜디오는 편리하게 사용할 수 있지만, 쿼리 사용등이 불가능하기 때문에, 원본 데이타를 이용한 상세 분석에는 어려움이 있다. 원본 데이타를 이용해서 상세 분석을 하려면 노트북 계열의 애플리케이션이 효과적인데, 빅쿼리를 연동할 수 있는 노트북으로는 이전에 소개한 주피터 노트북 기반의 데이타랩 (datalab)과, 스파크나 다른 빅데이타 솔루션과 함께 많이 사용되는 제플린 노트북(zeppelin.apache.org) 이 있다.


지난 글에서 데이타랩에 대한 연동 방법은 이미 소개하였으니, 이번에는 제플린을 통하여, 빅쿼리의 데이타를 분석해보도록 한다.


제플린 설치

제플린을 설치 하는 방법은 간단하다. Zeppelin.apache.org 에서, 설치 파일을 다운로드 받는다.

빅쿼리 연동 인터프리터는 제플린 버전 0.61 버전 이상에 포함되어 있기 때문에, 0.61 버전 이상을 다운로드 받는다.  이 때 모든 인터프리터가 포함된 버전을 다운 받아야 한다. (아니면 별도로 인터프리터를 설치해야 하는 번거로움이 따른다.)


다운 로드 받은 파일의 압축을 푼다. 다음으로 제플린 설치 디렉토리로 들어가서 다음 명령어를 수행한다.

% ./bin/zeppelin.sh

윈도우의 경우에는 %./bin/zeppelin.cmd 를 실행하면 된다.

자바 애플리케이션이기 때문에 별도의 설치 과정이 필요없고, 제플린 애플리케이션을 실행하기만 하면 된다.

제플린이 기동되었으면 브라우져에서 http://localhost:8080 으로 접속하면 다음과 같이 제플린 콘솔을 볼 수 있다.

노트북 생성

제플린 콘솔에 들어왔으면 초기화면에서 Create new note 라는 메뉴를 이용하여 새로운 노트북을 생성하자. 여기서는 편의상 “BQ 노트북" 이라는 이름으로 노트북을 생성하였다.


분석 쿼리 작성

이제 분석할 내용은 수집된 트윗의 명사들에 대해서, 시간 단위로 그룹핑을 한 다음에, 각 단어에 대해서 발생한 횟수를 카운트해서 보여주는 내용을 구현하려고 한다.

예를 들어서 9월20일에는 “유행" 이라는 단어가 200회 발생하였고, “패션" 이라는 단어가 100회 발생하였다. 라는 식으로 조회를 하려고 한다.


현재 테이블 구조는 다음과 같다.

Date (발생 시간)

Noun (명사)

count (발생 횟수)


SQL 문장을 작성해보자

select date,noun,sum(count) from 테이블명

group by date,noun


이렇게 쿼리를 하면, 시간대 별로, 명사와 그 명사의 발생 횟수를 리턴을 해주는데, 우리는 앞의 데이타 플로우 프로그램에서 30초 단위로 통계를 집계하도록 하였기 때문에, 30초 단위로 결과가 리턴된다. 우리가 원하는 결과는 30초 단위가 아니라 1시간 단위의 결과 이기 때문에, 다음과 같이 쿼리를 수정한다.


select  DATE(date) as ddate,HOUR(date) as dhour,noun,sum(count) from 테이블명

group by ddate,dhour,noun


DATE와 HOUR라는 함수를 사용하였는데, DATE는 타임 스탬프 형태의 컬럼에서 날짜만을 추출해주는 함수 이고, HOUR는 타임 스탬프 형태의 컬럼에서 시간만을 추출해주는 함수 이다.

이렇게 날짜와 시간만을 추출한 다음에, group by 절을 이용하여, 날짜와,시간 그리고 명사로 그룹핑을 하게 되면 우리가 원하는 것과 같이 각 날짜의 시간대별로 명사별 발생횟수 ( sum(count)) 값의 통계를 얻을 수 있다.


제플린에서 빅쿼리 명령을 수행하려면 다음과 같이 %bigquery.sql 이라고 첫줄에 선언을 한 다음에 SQL 문장을 수행하면 된다.




결과는 디폴트로 테이블 형태로 나오는데, 아래 아이콘 중에서 그래프 아이콘을 누르면 그래프 형태로 볼 수 가 있는데, 이 때 X,Y축의 변수를 지정할 수 있다.

아래 그림과 같이 Keys (X축을) ddate,dhour를 선택하고 Values(Y축)을 dhour SUM을 선택하면, 시간별 나타난 단어수를 볼 수 있다.



그런데 이 쿼리를 수행하면, 각 시간별로 발생한 명사 단어의 수가 매우 많기 때문에, 보기가 매우 어렵다.

그렇다면 시간대별로 발생한 명사중에서 각 시간대별로 많이 발생한 명사 5개씩만을 볼 수 없을까? 즉 group by를 전체 데이타 구간이 아니라, 각각 시간대 별로 계산을 해줄 수 는 없을까 하는 필요가 발생한다.

빅쿼리 파티셔닝

데이타를 구간 별로 나눠서 연산할 수 있는 기능으로 빅쿼리에는 파티션이라는 기능이 있다.

예를 들어서 group by를 전체 결과에 대해 그룹핑을 하는 것이 아니라, 앞에서 언급한 요건 처럼 일 단위로 짤라서 그룹핑을 하는 것이 가능하다.




파티션을 이용해서 할 수 있는 것은 파티션별로 합계나, 통계를 내거나, 파티션의 각 로우의 값의 백분율(%)나 또는 소팅한 순서등을 볼 수 있다. 여기서는, 시간으로 파티션을 나누고  파티션내에서 명사의 수가 많은 수 순서대로 소팅을 한후에, RANK라는 함수를 이용하여 그 파티션에서 그 명사가 몇번째로 많이 나타났는지를 출력하도록 해보겠다.


파티션의 사용법은 다음과 같다.

“파티션 함수 OVER (PARTITION BY 파티션을할 키 목록)”

여기서는 일/시간 별로 파티션을 나눈 후에, 그 순위별로 소팅을 할 것이기 때문에, 다음과 같은 식을 쓴다.

RANK() OVER (PARTITION BY ddate,dhour ORDER BY ncount DESC  ) as rank


이를 쿼리에 적용하면 다음과 같다.

   SELECT

       DATE(date) as ddate,HOUR(date) as dhour

       ,noun

       ,sum(count) as ncount

       , RANK() OVER (PARTITION BY ddate,dhour ORDER BY ncount DESC  ) as rank

   FROM [useful-hour-138023:twitter.noun]

   group by noun,ddate,dhour

   order by ddate,dhour,ncount desc


그러면 다음과 같이 일/날짜 파티션별로 많이 발생한 명사 순으로 발생횟수와 순위(rank)를 출력해준다.



그런데 쿼리를 돌려보면 알겠지만, 시간대별로 수집한 명사의 종류가 많기 때문에, 일자별 데이타가 매우 많다. 그래서 파티션별로 많이 등장하는 단어 5개만을 보려고 하면 rank <5 인것만 걸러내면 된다. 이는 중첩 쿼리를 이용해서 수행이 가능하다

다음은 이를 적용한 예이다.


SELECT ddate,dhour

   ,noun

   , rank

from (

   SELECT

       DATE(date) as ddate,HOUR(date) as dhour

       ,noun

       ,sum(count) as ncount

       , RANK() OVER (PARTITION BY ddate,dhour ORDER BY ncount DESC  ) as rank

   FROM [useful-hour-138023:twitter.noun]

   where noun != "note7" and noun != "samsung" and noun !="galaxy"

   group by noun,ddate,dhour

   order by ddate,dhour,ncount desc

   )

where rank < 6


이렇게 하면, 각 시간대별로 자주 등장하는 단어 6개만을 보여준다.


이 쿼리를 이용하여 데이타를 어떻게 분석하는지는 예전글 http://bcho.tistory.com/1136 을 참고하세요.


간단하게나마 트위터 피드에서 특정 키워드를 기반으로 하여, 명사와 형용사를 추출하여 소셜 반응을 분석하는 애플리케이션 개발과 데이타 분석 방법에 대해서 설명하였다.

아이폰7을 분석해보니, 명사 분석도 의미가 있었지만, 아이폰7에 대한 기대를 형용사 분석을 통해서도 많은 인사이트를 얻을 수 있었다. Awesome, excellent와 같은 기대치가 높은 형용사가 많이 검출되었고 bad, fuck 과 같은 부정적인 의미의 형용사는 다소 낮게 검출되었다. (아마 이즈음이 노트7 폭발로 인하여 반사 이익을 얻은게 아닐까 추정되는데.)


이외에도, 이모콘티만 추출하여 분석을 한다거나, 부사등을 통해서 분석을 하는 것도 가능하고, 구글 자연어 처리 API는 글을 통해서 사람의 감정을 분석해주는 기능도 있기 때문에 응용 분야는 훨씬 더 넓다고 볼 수 있다.

파이어베이스 애널러틱스를 이용한 모바일 데이타 분석

#3 빅쿼리에 연동하여 모든 데이타를 분석하기


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


파이어베이스 애널러틱스의 대단한 기능중의 하나가, 모바일에서 올라온 모든 원본 로그를 빅쿼리에 저장하고, 이를 빅쿼리를 통해서 분석할 수 있는 기능이다. 대부분의 매니지드 서비스 형태의 모바일 애널리틱스 서비스는 서비스에서 제공하는 지표만, 서비스에서 제공하는 화면을 통해서만 볼 수 있기 때문에, 상세한 데이타 분석이 불가능하다. 파이어베이스의 경우에는 빅쿼리에 모든 원본 데이타를 저장함으로써 상세 분석을 가능하게 해준다.


아울러, 모바일 서비스 분석에 있어서, 상세 로그 분석을 위해서 로그 수집 및 분석 시스템을 별도로 만드는 경우가 많은데, 이 경우 모바일에 설치될 로그 수집 에이전트에서 부터 로그를 수집하는 API 서버, 이를 저장하기 위한 분산 큐(카프카 Kafka)와 같은 복잡한 백앤드 시스템을 설계 구현해야 하는데, 파이어베이스 애널러틱스의 로깅 기능을 이용하면 별도의 이런 인프라 구현이 없이도 손쉽게 로그를 수집 및 분석할 수 있다. (일종의 무임 승차라고나 할까?)


가격 정책

그렇다면 가장 고민이 되는 것이 가격 정책일 것이다. 파이어베이스 애널러틱스에서 빅쿼리에 데이타를 저장하려면 파이어베이스 플랜중 무료가 아닌 유료 플랜인 Blaze 플랜을 사용해야 한다.

그러나, 다행이도 Blaze 플랜은 “Pay as you go” 모델로 사용한 만큼 비용을 지불하는 모델인데, “Google Cloud Integration”은 별도의 비용이 부과 되지 않는다.



단지 빅쿼리에 대한 비용만 부담이 되는데, 빅쿼리의 경우 데이타 로딩은 무료이고, 저장 요금 역시 GB당 월 0.02$ (약 22원)이며, 90일동안 해당 데이타를 사용하지 않으면 이 요금은 50%로 자동 할인되서 GB당 월 0.01$(약 11원)만 과금된다. 이외에 쿼리당 비용이 과금되는데, 쿼리당 비용은 쿼리에서 스캔한 데이타 용량 만큼만 과금이 된다. TB를 쿼리 했을때 5$가 과금이되는데, 이역시 전체 테이블을 스캔을 하는것이 아니라, 쿼리에서 스캔하는 컬럼에 대해서만 과금이 되고, 전체 테이블이 아니라, 쿼리에서 스캔하는 날짜만 과금이 되기 때문에, 실제 과금 금액은 미미하다고 볼 수 있다. 실제로 실 서비스에서 모 앱의 하루 데이타를 수집한 경우 17만건의 이벤트가 수집되었는데 저장 용량은 전체 350 MB에 불과하다. 전체 컬럼을 스캔한다고 하더라도 (전체 컬럼을 스캔할 일은 없겠지만….) 쿼리 비용은 0.00175$에 불과하다.


파이어베이스 애널러틱스와 빅쿼리를 연동하여 데이타 수집하기

파이어베이스 애널러틱스에서 데이타를 빅쿼리로 수집하기 위해서는 앞에서 언급한바와 같이 먼저 파이어베이스 플랜을 Blaze로 업그레이드 해야 한다. 파이어베이스 콘솔 좌측 하단을 보면 아래와 같이 UPGRADE 버튼이 있다. 이 버튼을 눌러서 Blaze 플랜으로 업그레이드를 하자


다음으로 파이어베이스 애널러틱스 프로젝트를 빅쿼리와 연결을 해줘야 한다.

파이어베이스 콘솔 좌측 상단에서 설정 버튼을 누른 후에, Project settings 메뉴를 선택한다.


프로젝트 세팅 메뉴에 들어가서 상단 메뉴중에 ACCOUNT LINKING이라는 메뉴를 선택한다.


그러면 구글 플레이나 광고 플랫폼등과 연결할 수 있는 메뉴와 함께 아래 그림처럼 빅쿼리로 연결할 수 있는 메뉴와 “LINK TO BIGQUERY”라는 버튼이 화면에 출력된다.


이 버튼을 누르면 작업은 끝났다. 이제부터 파이어베이스의 모든 로그는 빅쿼리에 자동으로 수집되게 된다.

만약에 수집을 중단하고 싶다면 위의 같은 화면에서 LINK TO BIGQUERY라는 버튼이 MANAGE LINKING으로 바뀌어 있는데, 이 버튼을 누르면 아래와 같이 App Details가 나온다.



여기서 스위치 버튼으로 Send data to BigQuery를 끔 상태로 변경해주면 된다.

이제 부터 대략 한시간 내에, 데이타가 빅쿼리에 수집되기 시작할 것이다.  

수집 주기

그러면 파이어베이스 애널러틱스에서는 어떤 주기로 데이타를 수집하고 수집된 데이타는 언제 조회가 가능할까? 이를 이해하기 위해서는 앱 로그 수집에 관여되는 컴포넌트와 흐름을 먼저 이해할 필요가 있다.

로그 수집이 가능한 앱은 크게, 구글 플레이 스토어에서 배포되는 앱, 구글 플레이 스토어를 통하지 않고 배포되는 앱 그리고 iOS 앱 3가지로 나눌 수 있다.

이 앱들이 파이어베이스 서버로 로그를 보내는 방식은 앱마다 약간씩 차이가 있다.


  • 플레이스토어에서 다운 받은 앱 : 각 개별 앱이 이벤트 로그를 수집하여 저장하고 있다가 1시간 주기로, 모든 앱들의 로그를 모아서 파이어베이스 서버로 전송한다.

  • 플레이스토어에서 다운받지 않은 앱 : 플레이스토어에서 다운로드 받은 앱과 달리 다른 앱들과 로그를 모아서 함께 보내지 않고 한시간 단위로 로그를 모아서 개별로 파이어베이스에 전송한다.

  • iOS 앱 : 앱별로 한시간 단위로 로그를 모아서 파이어베이스 서버로 전송한다.


이렇게 앱에서 파이어베이스 서버로 전송된 데이타는 거의 실시간으로 구글 빅쿼리에 저장된다.

그러나 파이어베이스 애널러틱스의 대쉬 보다는 대략 최대 24시간 이후에 업데이트 된다. (24시간 단위로 분석 통계 작업을 하기 때문이다.)


이 전체 흐름을 도식화 해보면 다음과 같다.



수집된 데이타 구조

그러면 빅쿼리에 수집된 테이블은 어떤 구조를 가질까?

테이블 구조를 이해하기 전에 테이블 종류를 먼저 이해할 필요가 있다.

앱에서 수집한 로그는 안드로이드와 iOS 각각 다른 데이타셋에 저장되며, 테이블 명은

  • app_events_YYYYMMDD

가 된다. 2016년 8월30일에 수집한 로그는  app_events_20160830 이 된다.



Intraday 테이블

여기에 intraday 테이블이라는 개념이 존재하는데, 이 테이블은 app_events_intraday_YYYYMMDD 라는 이름으로 저장이 되는데, 이 테이블은 실시간 데이타 수집을 목적으로 하는 테이블로 오늘 데이타가 저장된다. 예를 들어 오늘이 2016년9월1일이라면, app_events테이블은 app_events_20160831 까지만 존재하고, 9월1일자 데이타는 app_events_intraday_20160901 이라는 테이블에 저장된다.

9월1일이 지나면 이 테이블은 다시 app_events_20160901 이라는 이름으로 변환된다.

intraday 테이블의 특성중의 하나는 몇몇 필드들은 값이 채워지지 않고 NULL로 반환된다. 모든 데이타를 수집하고 배치 연산을 통해서 계산이 끝나야 하는 필드들이 그러한데, LTV 값과 같은 필드가 여기에 해당한다.


여기서 주의할점 중의 하나가 intraday 테이블이 하나만 존재할것이라는 가정인데. 결론 부터 이야기 하면 최대 2개가 존재할 수 있다. 9월1일 시점에  app_events_intraday_20160901 테이블이 존재하다가 9월2일이 되면 app_events_intraday_20160902 테이블이 생성된다. app_events_intraday_20160901 를 app_events_20160901 테이블로 변환을 해야 하는데, 단순히 복사를 하는 것이 아니라, 배치 연산등을 수행하기 때문에 연산에 다소 시간이 걸린다. 그래서 연산을 수행하는 동안에는 app_events_intraday_20160901 테이블과 app_events_intraday_20160902이 동시에 존재하고, 9월1일 데이타에 대한 연산이 종료되면 app_events_intraday_20160901 은 app_events_20160901 로 변환 된다.  

테이블 스키마

빅쿼리에 저장된 데이타의 테이블 구조를 이해하기 위해서 빅쿼리의 데이타 저장 특성을 이해할 필요가 있는데, 빅쿼리는 테이블 데이타 구조를 가지면서도 JSON과 같이 컬럼안에 여러 컬럼이 들어가는 RECORD 타입이나, 하나의 컬럼안에 여러개의 데이타를 넣을 수 있는  REPEATED 필드라는 데이타 형을 지원한다.



<그림. 레코드 타입의 예>

레코드 타입은 위의 그림과 같이 Name이라는 하나의 컬럼 내에 Last_name과 First_name이라는 두개의 서브 컬럼을 가질 수 있는 구조이다.

아래는 REPEATED 필드(반복형 필드)의 데이타 예인데, Basket이라는 컬럼에 Books,Galaxy S7, Beer 라는 3개의 로우가 들어가 있다.


<그림. 반복형 필드 예>

이런 구조로 인하여, 빅쿼리는 JSON과 같이 트리 구조로 구조화된 데이타를 저장할 수 있고, 실제로 파이어베이스 애널러틱스에 의해 수집되어 저장되는 데이타도 JSON과 같은 데이타 구조형으로 저장이 된다.

많은 데이타 필드가 있지만, 큰 분류만 살펴보면 다음과 같은 구조를 갖는다.



하나의 레코드는 하나의 앱에서 올라온 로그를 나타낸다. 앱은 앞의 수집 주기에 따라서 한시간에 한번 로그를 올리기 때문에, 하나의 레코드(행/로우)는 매시간 그 앱에서 올라온 로그라고 보면 된다.


가장 상위 요소로 user_dim과, event_dim이라는 요소를 가지고 있다.

user_dim은 사용자나 디바이스에 대한 정보를 주로 저장하고 있고, event_dim은 앱에서 발생한 이벤트들을 리스트 형태로 저장하고 있다.

user_dim에서 주목할만한 것은 userid에 관련된 것인데, userid는 사용자 id 이지만, 파이어베이스가 자동으로 수집해주지는 않는다. 개발자가 앱의 파이어베이스 에이전트 코드에서 다음과 같이 setUserId 메서드를 이용해서 설정해줘야 빅쿼리에서 조회가 가능하다. (앱 서비스의 계정을 세팅해주면 된다.)

mFirebaseAnalytics.setUserId(Long.toString(user.id));

다음 주목할 필드는 user_dim에서 app_info.app_instance_id 라는 필드인데, 이 필드는 각 앱의 고유 ID를 나타낸다. 파이어베이스가 자동으로 부여하는 id로 설치된 앱의 id이다.

예를 들어 내가 갤럭시S7과 노트7를 가지고 같은 앱을 설치했다고 하더라도 각각 다른 디바이스에 설치되었기 때문에 각각의 앱 id는 다르다.


다음은 event_dim인데, event_dim은 이벤트들로 레코드들의 배열(리스트)로 구성이 되고 각각의 이벤트는 이벤트 이름과 그 이벤트에 값을 나타내는 name 과 params라는 필드로 구성이 되어 있다.  params는 레코드 타입으로 여러개의 인자를 가질 수 있고, params내의 인자는 또 각각 key와 value식으로 하여 인자의 이름과 값을 저장한다. values는 string_value,int_value,double_value 3가지 서브 필드를 가지고 있는데, 인자의 타입에 따라서 알맞은 필드에만 값이 채워진다. 예를 들어 인자의 타입이 문자열 “Cho” 이고, 인자의 이름이 “lastname”이면, params.key “lastname”이 되고, params.value.string_value=”Cho”가 되고 나머지 필드인 params.value.int_value와 params.value.float.value는 null이 된다.


   "event_dim": [

     {

       "name": "Screen",

       "params": [

         {

           "key": "firebase_event_origin",

           "value": {

             "string_value": "app",

             "int_value": null,

             "float_value": null,

             "double_value": null

           }

         },

         {

           "key": "Category",

           "value": {

             "string_value": "Main",

             "int_value": null,

             "float_value": null,

             "double_value": null

           }

         },

      ]

    },

     {

       "name": "Purchase",

       "params": [

         {

           "key": "amount",

           "value": {

             "string_value": null,

             "int_value": “5000”,

             "float_value": null,

             "double_value": null

           }

         }

         },

      ]

    },


위의 예제는 빅쿼리에 저장된 하나의 행을 쿼리하여 JSON형태로 리턴 받은 후, 그 중에서 event_dim 필드 내용 일부를 발췌한 것이다.

Screen과 Purchase라는 두개의 이벤트를 받았고,

Screen은 firebase_event_origin=”app”, Category=”main” 이라는 두개의 인자를 받았다.

Purchase는 amount=5000 이라는 정수형 인자 하나를 받았다.


전체 빅쿼리의 스키마는 다음과 같이 되어 있다.




파이어베이스 애널러틱스에서 빅쿼리로 저장된 테이블 스키마에 대한 상세는 https://support.google.com/firebase/answer/7029846?hl=en 를 참고하기 바란다.


구글 빅쿼리에 대한 자료 아래 링크를 참고하기 바란다.


  1. 2016.08.01 빅쿼리를 이용하여 두시간만에 트위터 실시간 데이타를 분석하는 대쉬보드 만들기

  2. 2016.07.31 빅데이타 수집을 위한 데이타 수집 솔루션 Embulk 소개

  3. 2016.06.18 빅쿼리-#3 데이타 구조와 접근(공유) (3)

  4. 2016.06.16 구글 빅데이타 플랫폼 빅쿼리 아키텍쳐 소개

  5. 2016.06.15 구글 빅데이타 플랫폼 빅쿼리(BIGQUERY)에 소개

  6. 빅쿼리로 데이타 로딩 하기 http://whitechoi.tistory.com/25


다음은 데이타랩을 통하여 데이타를 직접 분석해보도록 하겠다.