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


Archive»


 
 


쿠버네티스 #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를 생성하게 된다.

얼굴 인식 모델을 만들어보자 #4 클라우드를 이용하여 학습 시키기

(머신러닝 학습 및 예측 시스템의 운영환경화)


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

앞에서 모델을 만들고 학습도 다했다. 이제, 이 모델을 실제 운영 환경에서 운영할 수 있는 스케일로 포팅을 하고자 한다.


로컬 환경 대비 실제 운영 환경으로 확장할때 고려해야 하는 사항은


  • 대규모 학습 데이타를 저장할 수 있는 공간

  • 대규모 학습 데이타를 전처리하기 위한 병렬 처리 환경
    이 내용은 이미  http://bcho.tistory.com/1177에서 다루었다.

  • 대규모 학습 데이타를 빠르게 학습 시킬 수 있는 컴퓨팅 파워

  • 학습된 데이타를 이용한 대규모 예측 서비스를 할 수 있는 기능


위의 요건을 만족하면서 텐서플로우로 환경을 올리는 방법은 여러가지가 있지만, 클라우드를 선택하기로 한다.

이유는

  • 첫번째 모델 개발에 집중하고, 텐서플로우의 설치 및 운영 등에 신경쓰지 않도록 한다. 단순한 텐서플로우 설치뿐만 아니라 여러 장비를 동시에 이용하여 분산 학습을 하려면, 클러스터 구성 및 유지가 부담이 된다.

  • 클라우드 컴퓨팅 파워를 이용하여, 대규모 데이타에 대한 전처리를 수행하고 개개별 학습 속도를 높이는 것은 물론이고, 모델을 튜닝하여 동시에 여러 모델을 학습 시킬 수 있다.

  • 대용량 학습 데이타를 저장하기 위한 스토리지 인프라에 대한 구성 및 운영 비용을 절감한다.


즉 설정이나 운영은 클라우드에 맏겨 놓고, 클라우드의 무한한 자원과 컴퓨팅 파워를 이용하여 빠르게 모델을 학습하기 위함이다.

구글 클라우드


아무래도 일하는 성격상 구글 클라우드를 먼저 볼 수 밖에 없는데, 구글 클라우드에서는 텐서플로우의 매니지드 서비스인 CloudML을 제공한다.


CloudML은 별도의 설치나 환경 설정 없이 텐서플로우로 만든 모델을 학습 시키거나 학습된 결과로 예측을 하는 것이 가능하다. 주요 특징을 보면 다음과 같다.


  • 학습시에, 별도의 설정 없이 텐서플로우 클러스터 크기 조절이 가능하다. 싱글 머신에서 부터 GPU 머신 그리고 여러대의 클러스터 머신 사용이 가능하다

  • 하이퍼 패러미터 튜닝이 가능하다. DNN의 네트워크의 폭과 깊이도 하이퍼 패러미터로 지정할 수 있으며, CloudML은 이런 하이퍼패러미터의 최적값을 자동으로 찾아준다.

  • 예측 서비스에서는 Tensorflow Serv를 별도의 빌드할 필요 없이 미리 환경 설정이 다되어 있으며 (bazel 빌드의 끔직함을 겪어보신 분들은 이해하실듯) gRPC가 아닌 간단한 JSON 호출로 예측 (PREDICTION) 요청을 할 수 있다

  • 분당 과금이다. 이게 강력한 기능인데, 구글 클라우드는 기본적으로 분당 과금으로 CPU를 사용하던, GPU를 사용하던 정확히 사용한 만큼만 과금하기 때문에, 필요할때 필요한 만큼만 사용하면 된다. 일부 클라우드의 경우에는 시간당 과금을 사용하기 때문에, 8대의 GPU머신에서 1시간 5분을 학습하더라도 8대에 대해서 2시간 요금을 내야하기 때문에 상대적으로 비용 부담이 높다.

  • 가장 큰 메리트는 TPU (Tensorflow Processing Unit)을 지원한다는 것인데, 딥러닝 전용 GPU라고 생각하면 된다. 일반적인 CPU또는 GPU대비 15~30배 정도 빠른 성능을 제공한다.


    현재는 Close Alpha로 특정 사용자에게만 시범 서비스를 제공하고 있지만 곧 CloudML을 통해서 일반 사용자에게도 서비스가 제공될 예정이다.

CloudML을 이용하여 학습하기

코드 수정

CloudML에서 학습을 시키려면 약간의 코드를 수정해야 한다. 수정해야 하는 이유는 학습 데이타를 같이 올릴 수 없기 때문인데, 여기에는 두 가지 방법이 있다.


  • 학습 데이타를 GCS (Google Cloud Storage)에 올려놓은 후, 학습이 시작되기 전에 로컬 디렉토리로 복사해 오거나

  • 또는 학습 데이타를 바로 GCS로 부터 읽어오도록 할 수 있다.


첫번째 방법은 gsutil 이라는 GCS 명령어를 이용하여 학습 시작전에 GCS에서 학습 데이타를 카피해오면 되고,

두번째 방법은 학습 데이타의 파일명을 GCS 로 지정하면 된다.

예를 들어 텐서 플로우 코드에서 이미지 파일을 아래와 같이 로컬 경로에서 읽어왔다면

   image =  tf.image.decode_jpeg(tf.read_file(“/local/trainingdata/”+image_file),channels=FLAGS.image_color)


GCS에서 읽어오려면 GCS 경로로 바꿔 주면 된다. GCS 버킷명이 terrycho-training-data라고 하면

   image =  tf.image.decode_jpeg(tf.read_file(“gs://terrycho-training-data/trainingdata/”+image_file),channels=FLAGS.image_color)


첫번째 방법의 경우에는 데이타가 아주 많지 않고, 분산 학습이 아닌경우 매우 속도가 빠르다. 두번째 방법의 경우에는 데이타가 아주아주 많아서 분산 학습이 필요할때 사용한다. 아무래도 로컬 파일 억세스가 GCS 억세스 보다 빠르기 때문이다.


다음은 첫번째 방식으로 학습 데이타를 로컬에 복사해서 학습하는 방식의 코드이다.


https://github.com/bwcho75/facerecognition/blob/master/CloudML%20Version/face_recog_model/model_localfile.py

코드 내용은 앞서 만들 모델 코드와 다를것이 없고 단지 아래 부분과, 파일 경로 부분만 다르다

def gcs_copy(source, dest):

   print('Recursively copying from %s to %s' %

       (source, dest))

   subprocess.check_call(['gsutil', '-q', '-m', 'cp', '-R']

       + [source] + [dest]


gcs_copy 함수는 GCS의 source 경로에서 파일을 dest 경로로 복사해주는 명령이다.


def prepare_data():

   # load training and testing data index file into local

   gcs_copy( 'gs://'+DESTINATION_BUCKET+'/'+TRAINING_FILE,'.')

   gcs_copy( 'gs://'+DESTINATION_BUCKET+'/'+VALIDATION_FILE,'.')

   

   # loading training and testing images to local

   image_url = 'gs://'+DESTINATION_BUCKET+'/images/*'


   if not os.path.exists(FLAGS.local_image_dir):

        os.makedirs(FLAGS.local_image_dir)

   gcs_copy( image_url,FLAGS.local_image_dir)

   

prepare_data()    

main()


그리고 prepare_data를 이용해서, 학습과 테스트용 이미지 목록 파일을 복사하고, 이미지들도 로컬에 복사한다.

로컬에 데이타 복사가 끝나면 main()함수를 호출하여 모델을 정의하고 학습을 시작한다.



디렉토리 구조

코드를 수정하였으면, CloudML을 이용하여 학습을 하려면, 파일들을 패키징 해야 한다. 별 다를것은 없고


[작업 디렉토리]

  • __init__.py

  • {모델 파일명}.py


식으로 디렉토리를 구성하면 된다.

얼굴 학습 모델을 model_localfile.py라는 이름으로 저장하였다


명령어

이제 학습용 모델이 준비되었으면, 이 모델을 CloudML에 집어 넣으면 된다.

명령어가 다소 길기 때문에, 쉘 스크립트로 만들어놓거나 또는 파이썬 노트북에 노트 형식으로 만들어 놓으면 사용이 간편하다. 다음은 파이썬 노트북으로 만들어놓은 내용이다.


import google.auth

import os

import datetime


os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/Users/terrycho/keys/terrycho-ml.json"

job_name = 'preparefacedata'+ datetime.datetime.now().strftime('%y%m%d%H%M%S')


리모트로 구글 클라우드의 CloudML을 호출하기 때문에, GOOGLE_APPLICATION_CREDIENTIALS에 서비스 어카운트 파일을 지정한다.

그리고 CloudML에 학습을 실행하면, 각 학습은 JOB으로 등록되는데, 손쉽게 JOB을 찾아서 모니터링 하거나 중지할 수 있도록, JOB ID를 현재 시간으로 생성한다.



print job_name

# Job name whatever you want

JOB_NAME=job_name

# the directory of folder that include your source and init file

PACKAGE_PATH='/Users/terrycho/anaconda/work/face_recog/face_recog_model'

# format: folder_name.source_file_name

MODULE_NAME='face_recog_model.model_localfile'

# bucket you created

STAGING_BUCKET='gs://terrycho-face-recog-stage'

# I recommand "europe-west1" region because there are not enough GPUs in US region for you.....

REGION='us-east1'

# Default is CPU computation. set BASIC_GPU to use Tesla K80 !

SCALE_TIER='BASIC_GPU'


# Submit job with these settings

!gcloud ml-engine jobs submit training $JOB_NAME \

--package-path=$PACKAGE_PATH \

--module-name=$MODULE_NAME \

--staging-bucket=$STAGING_BUCKET \

--region=$REGION \

--scale-tier=$SCALE_TIER \


다음은 cloudml 명령어를 실행하면 된다. 각 인자를 보면

  • JOB_NAME은 학습 JOB의 이름이다.

  • package-path는 __init__.py와 학습 모델 및 관련 파일들이 있는 디렉토리가 된다.

  • module-name은 package-path안에 있는 학습 실행 파일이다.

  • staging-bucket은 CloudML에서 학습 코드를 올리는 임시 Google Cloud Storage로, Google Cloud Storage 만든 후에, 그 버킷 경로를 지정하면 된다.

  • region은 CloudML을 사용한 리전을 선택한다.

  • 마지막으로 scale-tier는 학습 머신의 사이즈를 지정한다.

스케일 티어

설명

BASIC

싱글 머신. CPU

BASIC_GPU

싱글 머신 + K80 GPU

STANDARD_1

분산 머신

PREMIUM_1

대규모 분산 머신

CUSTOM

사용자가 클러스터 크기를 마음대로 설정


일반적인 모델은 BASIC_GPU를 사용하면 되고, 모델이 분산 학습이 가능하도록 개발되었으면 STANDARD_1 이나 PREMIUM_1을 사용하면 된다.


이렇게 명령을 수행하면 모델코드가 CloudML로 전송되고, 전송된 코드는 CloudML에서 실행된다.

학습 모니터링

학습이 시작되면 JOB을 구글 클라우드 콘솔의 CloudML 메뉴에서 모니터링을 할 수 있다.




다음은 CloudML에서의 JOB 목록이다.  (진짜 없어 보인다…)




실행중인 JOB에서 STOP 버튼을 누르면 실행중인 JOB을 정지시킬 수도 있고, View Logs 버튼을 누르면, 학습 JOB에서 나오는 로그를 볼 수 있다. ( 텐서플로우 코드내에서 print로 찍은 내용들도 모두 여기 나온다.)




여기까지 간단하게나마 CloudML을 이용하여 모델을 학습하는 방법을 알아보았다.

본인의 경우 연예인 인식 모델을 MAC PRO 15” i7 (NO GPU)에서 학습한 경우 7000 스텝가지 약 8시간이 소요되었는데, CloudML의 BASIC_GPU를 사용하였을때는 10,000 스탭에 약 1시간 15분 정도 (GCS를 사용하지 않고 직접 파일을 로컬에 복사해놓고 돌린 경우) 가 소요되었다. (빠르다)


여기서 사용된 전체 코드는 https://github.com/bwcho75/facerecognition/tree/master/CloudML%20Version 에 있다.


  • model_gcs.py 는 학습데이타를 GCS에서 부터 읽으면서 학습하는 버전이고

  • model_localfile.py는 학습데이타를 로컬 디스크에 복사해놓고 학습하는 버전이다.


다음 글에서는 학습된 모델을 배포하여 실제로 예측을 실행할 수 있는 API를 개발해보도록 하겠다.

텐서플로우의 세션,그래프 그리고 함수의 개념


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


그래프와 세션에 대한 개념이 헷갈려서, 좋은 샘플이 하나 만들어져서 공유합니다.

텐서 플로우의 기본 작동 원리는 세션 시작전에 그래프를 정의해놓고, 세션을 시작하면 그 그래프가 실행되는 원리인데, 그래서 이 개념이 일반적인 프로그래밍 개념과 상의하여 헷갈리는 경우가 많다


즉, 세션을 시작해놓고 함수를 호출하는 케이스들이 대표적인데

http://bcho.tistory.com/1170 코드를 재 사용해서 이해해보도록 하자


이 코드를 보면, tt = time * 10 을 세션 시작전에 정의해놨는데, 이 코드를 함수로 바꾸면 아래와 같은 형태가 된다.


변경전 코드

def main():

   

   print 'start session'

   #coornator 위에 코드가 있어야 한다

   #데이타를 집어 넣기 전에 미리 그래프가 만들어져 있어야 함.

   batch_year,batch_flight,batch_time = read_data_batch(TRAINING_FILE)

   year = tf.placeholder(tf.int32,[None,],name='year')

   flight = tf.placeholder(tf.string,[None,],name='flight')

   time = tf.placeholder(tf.int32,[None,],name='time')

   

   tt = time * 10

   summary = tf.summary.merge_all()

   with tf.Session() as sess:

       summary_writer = tf.summary.FileWriter(LOG_DIR,sess.graph)

       try:


           coord = tf.train.Coordinator()

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


           for i in range(5):

               y_,f_,t_ = sess.run([batch_year,batch_flight,batch_time])

               print sess.run(tt,feed_dict={time:t_})

               #summary_str = sess.run(summary,feed_dict=feed_dict)

               #summary_writer.add_summary(summary_str,i)

               summary_writer.flush()         


변경후 코드

def create_graph(times):

   tt = times * 10

   return tt


def main():

   

   print 'start session'

   #coornator 위에 코드가 있어야 한다

   #데이타를 집어 넣기 전에 미리 그래프가 만들어져 있어야 함.

   batch_year,batch_flight,batch_time = read_data_batch(TRAINING_FILE)

   year = tf.placeholder(tf.int32,[None,],name='year')

   flight = tf.placeholder(tf.string,[None,],name='flight')

   time = tf.placeholder(tf.int32,[None,],name='time')

   

   r = create_graph(time)

   

   summary = tf.summary.merge_all()

   with tf.Session() as sess:

       summary_writer = tf.summary.FileWriter(LOG_DIR,sess.graph)

       try:


           coord = tf.train.Coordinator()

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


           for i in range(5):

               y_,f_,t_ = sess.run([batch_year,batch_flight,batch_time])

               print sess.run(r,feed_dict={time:t_})

               #summary_str = sess.run(summary,feed_dict=feed_dict)

               #summary_writer.add_summary(summary_str,i)

               summary_writer.flush()


변경후 코드는 tt = times * 10 을 create_graph라는 함수로 뺐는데, session 시작전에 함수를 호출한다. 언뜻 보면 개념이 헷갈릴 수 있는데, time 이라는 변수는 텐서플로우의 placeholder로 값이 읽혀지는 시점이 queue_runner를 시작해야 값을 읽을 수 있는 준비 상태가 되고, 실제로 값을 큐에서 읽으려면 session을 실행하고 feed_dict를 이용하여 feeding을 해줘야 값이 채워지기 때문에, 일반적인 프로그램상으로는 session을 시작한 후에 함수를 호출해야할것 같이 생각이 되지만, 앞에서도 언급했듯이 텐서플로우에서 프로그래밍의 개념은 그래프를 다 만들어놓은 후 (데이타가 처리되는 흐름을 모두 정의해놓고) 그 다음 session을 실행하여 그래프에 데이타를 채워놓는 개념이기 때문에, session이 정의되기 전에 함수 호출등을 이용해서 그래프를 정의해야 한다.


텐서플로우 #2 - 행렬과 텐서플로우


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


머신러닝은 거의 모든 연산을 행렬을 활용한다. 텐서플로우도 이 행렬을 기반으로 하고, 이 행렬의 차원을 shape 라는 개념으로 표현하는데, 행렬에 대한 기본적이 개념이 없으면 헷갈리기 좋다. 그래서 이 글에서는 간략하게 행렬의 기본 개념과 텐서플로우내에서 표현 방법에 대해서 알아보도록 한다.


행렬의 기본 개념 훝어보기

행과 열

행렬의 가장 기본 개념은 행렬이다. mxn 행렬이 있을때, m은 행, n은 열을 나타내며, 행은 세로의 줄수, 열은 가로줄 수 를 나타낸다. 아래는 3x4 (3행4열) 행렬이다.


곱셈


곱셈은 앞의 행렬에서 행과, 뒤의 행렬의 열을 순차적으로 곱해준다.

아래 그림을 보면 쉽게 이해가 될것이다.



이렇게 앞 행렬의 행과 열을 곱해나가면 결과적으로 아래와 같은 결과가 나온다.


이때 앞의 행렬의 열과, 뒤의 행렬의 행이 같아야 곱할 수 있다.

즉 axb 행렬과 mxn 행렬이 있을때, 이 두 행렬을 곱하려면 b와 m이 같아야 한다.

그리고 이 두 행렬을 곱하면 axn 사이즈의 행렬이 나온다.

행렬의 덧셈과 뺄셈

행렬의 덧셈과 뺄셈은 단순하다. 같은 행과 열에 있는 값을 더하거나 빼주면 되는데, 단지 주의할점은 덧셈과 뺄샘을 하는 두개의 행렬의 차원이 동일해야 한다.


텐서 플로우에서 행렬의 표현

행렬에 대해서 간단하게 되짚어 봤으면, 그러면 텐서 플로우에서는 어떻게 행렬을 표현하는지 알아보자


을 하는 코드를 살펴보자


예제코드

import tensorflow as tf


x = tf.constant([ [1.0,2.0,3.0] ])

w = tf.constant([ [2.0],[2.0],[2.0] ])

y = tf.matmul(x,w)

print x.get_shape()


sess = tf.Session()

init = tf.global_variables_initializer()

sess.run(init)

result = sess.run(y)


print result


실행 결과

(1, 3)
[[ 12.]]



텐서플로우에서 행렬의 곱셈은 일반 * 를 사용하지 않고, 텐서플로우 함수  “tf.matmul” 을 사용한다.

중간에, x.get_shape()를 통해서, 행렬 x의 shape를 출력했는데, shape는 행렬의 차원이라고 생각하면 된다. x는 1행3열인 1x3 행렬이기 때문에, 위의 결과와 같이 (1,3)이 출력된다.


앞의 예제에서는 contant 에 저장된 행렬에 대한 곱셈을 했는데, 당연히 Variable 형에서도 가능하다.


예제 코드

import tensorflow as tf


x = tf.Variable([ [1.,2.,3.] ], dtype=tf.float32)

w = tf.constant([ [2.],[2.],[2.]], dtype=tf.float32)

y = tf.matmul(x,w)


sess = tf.Session()

init = tf.global_variables_initializer()

sess.run(init)

result = sess.run(y)


print result


Constant 및 Variable 뿐 아니라,  PlaceHolder에도 행렬로 저장이 가능하다 다음은 PlaceHolder에 행렬 데이타를 feeding 해주는 예제이다.

입력 데이타 행렬 x는 PlaceHolder 타입으로 3x3 행렬이고, 여기에 곱하는 값 w는 1x3 행렬이다.


예제 코드는 다음과 같다.


예제코드

import tensorflow as tf


input_data = [ [1.,2.,3.],[1.,2.,3.],[2.,3.,4.] ] #3x3 matrix

x = tf.placeholder(dtype=tf.float32,shape=[None,3])

w = tf.Variable([ [2.],[2.],[2.] ], dtype = tf.float32) #3x1 matrix

y = tf.matmul(x,w)


sess = tf.Session()

init = tf.global_variables_initializer()

sess.run(init)

result = sess.run(y,feed_dict={x:input_data})


print result


실행결과

[[ 12.]
[ 12.]
[ 18.]]


이 예제에서 주의 깊게 봐야 할부분은 placeholder x 를 정의하는 부분인데, shape=[None,3] 으로 정의했다 3x3 행렬이기 때문에, shape=[3,3]으로 지정해도 되지만 None 이란, 갯수를 알수 없음을 의미하는 것으로, 텐서플로우 머신러닝 학습에서 학습 데이타가 계속해서 들어오고  학습 때마다 데이타의 양이 다를 수 있기 때문에, 이를 지정하지 않고 None으로 해놓으면 들어오는 숫자 만큼에 맞춰서 저장을 한다.

브로드 캐스팅

텐서플로우 그리고 파이썬으로 행렬 프로그래밍을 하다보면 헷갈리는 개념이 브로드 캐스팅이라는 개념이 있다. 먼저 다음 코드를 보자


예제코드

import tensorflow as tf


input_data = [

    [1,1,1],[2,2,2]

   ]

x = tf.placeholder(dtype=tf.float32,shape=[2,3])

w  =tf.Variable([[2],[2],[2]],dtype=tf.float32)

b  =tf.Variable([4],dtype=tf.float32)

y = tf.matmul(x,w)+b


print x.get_shape()

sess = tf.Session()

init = tf.global_variables_initializer()

sess.run(init)

result = sess.run(y,feed_dict={x:input_data})


print result


실행결과

(2, 3)
[[ 24.]
[ 48.]]


행렬 x는 2x3 행렬이고 w는 3x1 행렬이다. x*w를 하면 2*1 행렬이 나온다.

문제는 +b 인데, b는 1*1 행렬이다. 행렬의 덧셈과 뺄셈은 차원이 맞아야 하는데, 이 경우 더하고자 하는 대상은 2*1, 더하려는 b는 1*1로 행렬의 차원이 다르다. 그런데 어떻게 덧셈이 될까?

이 개념이 브로드 캐스팅이라는 개념인데, 위에서는 1*1인 b행렬을 더하는 대상에 맞게 2*1 행렬로 자동으로 늘려서 (stretch) 계산한다.


브로드 캐스팅은 행렬 연산 (덧셈,뺄셈,곱셈)에서 차원이 맞지 않을때, 행렬을 자동으로 늘려줘서(Stretch) 차원을 맞춰주는 개념으로 늘리는 것은 가능하지만 줄이는 것은 불가능하다.


브로드 캐스팅 개념은 http://scipy.github.io/old-wiki/pages/EricsBroadcastingDoc 에 잘 설명되어 있으니 참고하기 바란다. (아래 그림은 앞의 링크를 참조하였다.)


아래는 4x3 행렬 a와 1x3 행렬 b를 더하는 연산인데, 차원이 맞지 않기 때문에, 행렬 b의 열을 늘려서 1x3 → 4x3 으로 맞춰서 연산한 예이다.


만약에 행렬 b가 아래 그림과 같이 1x4 일 경우에는 열을 4 → 3으로 줄이고, 세로 행을 1→ 4 로 늘려야 하는데, 앞에서 언급한바와 같이, 브로드 캐스팅은 행이나 열을 줄이는 것은 불가능하다.


다음은 양쪽 행렬을 둘다 늘린 케이스 이다.

4x1 행렬 a와 1x3 행렬 b를 더하면 양쪽을 다 수용할 수 있는 큰 차원인 4x3 행렬로 변환하여 덧셈을 수행한다.



텐서플로우 행렬 차원 용어


텐서플로우에서는 행렬을 차원에 따라서 다음과 같이 호칭한다.

행렬이 아닌 숫자나 상수는 Scalar, 1차원 행렬을 Vector, 2차원 행렬을 Matrix, 3차원 행렬을 3-Tensor 또는 cube, 그리고 이 이상의 다차원 행렬을 N-Tensor라고 한다.


그리고 행렬의 차원을 Rank라고 부른다. scalar는 Rank가 0, Vector는 Rank 가 1, Matrix는 Rank가 2가 된다.


monk 모듈을 이용한 mongoDB 연결


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


mongoDB 기반의 개발을 하기 위해서 mongoDB를 설치한다. https://www.mongodb.org/ 에서 OS에 맞는 설치 파일을 다운로드 받아서 설치한다.

설치가 된 디렉토리에 들어가서 설치디렉토리 아래 ‘./data’ 라는 디렉토리를 만든다. 이 디렉토리는 mongoDB의 데이타가 저장될 디렉토리이다.

 

mongoDB를 구동해보자.

% ./bin/mongod --dbpath ./data



Figure 1 mongoDB 구동화면


구동이 끝났으면 mongoDB에 접속할 클라이언트가 필요하다. DB에 접속해서 데이타를 보고 쿼리를 수행할 수 있는 클라이언트가 필요한데, 여러 도구가 있지만 많이 사용되는 도구로는 roboMongo라는 클라이언트가 있다.

https://robomongo.org/download 에서 다운로드 받을 수 있다. OS에 맞는 설치 파일을 다운로드 받아서 설치 후 실행한다.

 

설치 후에, Create Connection에서, 로컬호스트에 설치된 mongoDB를 연결하기 위해서 연결 정보를 기술하고, 연결을 만든다





Figure 2 robomongo에서 localhost에 있는 mongodb 연결 추가

 

주소는 localhost, 포트는 디폴트 포트로 27017를 넣으면 된다.

 

환경이 준비가 되었으면 간단한 테스트를 해보자. 테스트 전에 기본적인 개념을 숙지할 필요가 있는데, mongoDBNoSQL 계열중에서도 도큐먼트DB (Document DB)에 속한다. 기존 RDBMS에서 하나의 행이 데이타를 표현했다면, mogoDB는 하나의 JSON 파일이 하나의 데이타를 표현한다. JSON을 도큐먼트라고 하기 때문에, 도큐먼트 DB라고 한다.

 

제일 상위 개념은 DB의 개념에 대해서 알아보자, DB는 여러개의 테이블(컬렉션)을 저장하는 단위이다.

Robomongo에서 mydb라는 이름으로 DB 를 생성해보자



Figure 3 robomongo에서 새로운  DB를 추가 하는 화면

 

다음으로 생성된 DB안에, 컬렉션을 생성한다. 컬렉션은 RDBMS의 단일 테이블과 같은 개념이다.

Robomongo에서 다음과 같이 ‘users’라는 이름의 컬렉션을 생성한다



Figure 4 robomongo에서 컬렉션(Collection) 생성

 

users 컬렉션에는 userid를 키로 해서, sex(성별), city(도시) 명을 입력할 예정인데, userid가 키이기 때문에, userid를 통한 검색이나 소팅등이 발생한다. 그래서 userid를 인덱스로 지정한다.

인덱스 지정 방법은 createIndex 명령을 이용한다. 다음과 같이 robomongo에서 createIndex 명령을 이용하여 인덱스를 생성한다.



Figure 5 users 컬렉션에서 userid를 인덱스로 지정

 

mongoDB는 디폴트로, 각 컬렉션마다 “_id”라는 필드를 가지고 있다. 이 필드는 컬렉션 안의 데이타에 대한 키 값인데, 12 바이트의 문자열로 이루어져 있고 ObjectId라는 포맷으로 시간-머신이름,프로세스ID,증가값형태로 이루어지는 것이 일반적이다.

_id 필드에 userid를 저장하지 않고 별도로 인덱스를 만들어가면서 까지 userid 필드를 별도로 사용하는 것은 mongoDBNoSQL의 특성상 여러개의 머신에 데이타를 나눠서 저장한다. 그래서 데이타가 여러 머신에 골고루 분산되는 것이 중요한데, 애플리케이션상의 특정 의미를 가지고 있는 필드를 사용하게 되면 데이타가 특정 머신에 쏠리는 현상이 발생할 수 있다.

예를 들어서, 주민번호를 _id로 사용했다면, 데이타가 골고루 분산될것 같지만, 해당 서비스가 10~20대에만 인기있는 서비스라면, 10~20대 데이타를 저장하는 머신에만 데이타가 몰리게 되고, 10세이하나, 20세 이상의 데이타를 저장하는 노드에는 데이타가 적게 저장된다.

이런 이유등으로 mongoDB를 지원하는 node.js 드라이버에서는 _id 값을 사용할때, 앞에서 언급한 ObjectId 포맷을 따르지 않으면 에러를 내도록 설계되어 있다. 우리가 앞으로 살펴볼 mongoosemonk의 경우에도 마찬가지이다.

 

이제 데이타를 집어넣기 위한 테이블(컬렉션) 생성이 완료되었다.

다음 컬렉션 에 대한 CRUD (Create, Read, Update, Delete) 를 알아보자

SQL 문장과 비교하여, mongoDB에서 CRUD 에 대해서 알아보면 다음과 같다.

CRUD

SQL

MongoDB

Create

insert into users ("name","city") values("terry","seoul")

db.users.insert({userid:"terry",city:"seoul"})

Read

select * from users where id="terry"

db.users.find({userid:"terry"})

Update

update users set city="busan" where _id="terry"

db.users.update( {userid:"terry"}, {$set :{ city:"Busan" } } )

Delete

delete from users where _id="terry"

db.users.remove({userid:"terry"})

Figure 6 SQL문장과 mongoDB 쿼리 문장 비교


mongoDB에서 쿼리는 위와 같이 db.{Collection }.{명령어} 형태로 정의된다.

roboMongo에서 insert 쿼리를 수행하여 데이타를 삽입해보자



Figure 7 mongoDB에서 users 컬렉션에 데이타 추가

 

다음으로 삽입한 데이타를 find 명령을 이용해 조회해보자



Figure 8 mongoDB에서 추가된 데이타에 대한 확인

 

mongoDB에 대한 구조나 자세한 사용 방법에 대해서는 여기서는 설명하지 않는다.

http://www.tutorialspoint.com/mongodb/ mongoDB에 대한 전체적인 개념과 주요 쿼리들이 간략하게 설명되어 있으니 이 문서를 참고하거나, 자세한 내용은 https://docs.mongodb.org/manual/ 를 참고하기 바란다.

https://university.mongodb.com/ 에 가면 mongodb.com에서 운영하는 온라인 강의를 들을 수 있다. (무료인 과정도 있으니 필요하면 참고하기 바란다.)

 

mongoDBnode.js에서 호출하는 방법은 여러가지가 있으나 대표적인 두가지를 소개한다.

첫번째 방식은 mongoDB 드라이버를 이용하여 직접 mongoDB 쿼리를 사용하는 방식이고, 두번째 방식은 ODM (Object Document Mapper)를 이용하는 방식이다. ODM 방식은 자바나 다른 프로그래밍 언어의 ORM (Object Relational Mapping)과 유사하게 직접 쿼리를 사용하는 것이 아니라 맵퍼를 이용하여 프로그램상의 객체를 데이타와 맵핑 시키는 방식이다. 뒷부분에서 직접 코드를 보면 이해가 빠를 것이다.

 

Monk를 이용한 연결

첫번째로 mongoDB 네이티브 쿼리를 수행하는 방법에 대해서 소개한다. monk라는 node.jsmongoDB 클라이언트를 이용할 것이다.

monk 모듈을 이용하기 위해서 아래와 같이 package.jsonmonk에 대한 의존성을 추가한다.


{

  "name": "mongoDBexpress",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0",

    "monk":"~1.0.1"

  }

}

 

Figure 9 monk 모듈에 대한 의존성이 추가된 package.json

 

app.js에서 express가 기동할때, monk를 이용해서 mongoDB에 연결하도록 한다.

var monk = require('monk');

var db = monk('mongodb://localhost:27017/mydb');

 

var mongo = require('./routes/mongo.js');

app.use(function(req,res,next){

    req.db = db;

    next();

});

app.use('/', mongo);

Figure 10 monk를 이용하여 app.js에서 mongoDB 연결하기

 

mongoDB에 연결하기 위한 연결 문자열은 'mongodb://localhost:27017/mydb' mongo://{mongoDB 주소}:{mongoDB 포트}/{연결하고자 하는 DB} 으로 이 예제에서는 mongoDB 연결을 간단하게 IP,포트,DB명만 사용했지만, 여러개의 인스턴스가 클러스터링 되어 있을 경우, 여러 mongoDB로 연결을 할 수 있는 설정이나, Connection Pool과 같은 설정, SSL과 같은 보안 설정등 부가적인 설정이 많으니, 반드시 운영환경에 맞는 설정으로 변경하기를 바란다. 설정 방법은 http://mongodb.github.io/node-mongodb-native/2.1/reference/connecting/connection-settings/ 문서를 참고하자.

 

이때 주의깊게 살펴봐야 하는 부분이 app.use를 이용해서 미들웨어를 추가하였는데, req.dbmongodb 연결을 넘기는 것을 볼 수 있다. 미들웨어로 추가가 되었기 때문에 매번 HTTP 요청이 올때 마다 req 객체에는 db라는 변수로 mongodb 연결을 저장해서 넘기게 되는데, 이는 HTTP 요청을 처리하는 것이 router에서 처리하는 것이 일반적이기 때문에, routerdb 연결을 넘기기 위함이다. 아래 데이타를 삽입하는 라우터 코드를 보자

 

router.post('/insert', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

     

      db = req.db;

      db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Inserted");

            

         });

});

Figure 11 /routes/mongo.js 에서 데이타를 삽입하는 코드


req 객체에서 폼 필드를 읽어서 userid,sex,city등을 읽어내고, 앞의 app.js 에서 추가한 미들웨어에서 넘겨준 db 객체를 받아서 db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc) 수행하여 데이타를 insert 하였다.

 

다음은 userid필드가 HTTP 폼에서 넘어오는 userid 일치하는 레코드를 지우는 코드 예제이다. Insert 부분과 크게 다르지 않고 remove 함수를 이용하여 삭제 하였다.


router.post('/delete', function(req, res, next) {

      var userid = req.body.userid;

     

      db = req.db;

      db.get('users').remove({'userid':userid},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Removed");

            

         });

});

Figure 12 /routes/mongo.js 에서 데이타를 삭제하는 코드

 

다음은 데이타를 수정하는 부분이다. Update 함수를 이용하여 데이타를 수정하는데,

db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

와 같이 ‘userid’userid 인 필드의 데이타를 },{'userid':userid,'sex':sex,'city':city} 대치한다.

 

router.post('/update', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

      db = req.db;

      db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

      //db.get('users').update({'userid':userid},{$set:{'sex':'BUSAN'}},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Updated");

            

         });

});

Figure 13 /routes/mongo.js 에서 데이타를 수정하는 코드


전체 레코드를 대치하는게 아니라 특정 필드만 수정하고자 하면, $set: 쿼리를 이용하여, 수정하고자하는 필드만 아래와 같이 수정할 수 있다.

db.collection('users').updateOne({_id:userid},{$set:{'sex':'BUSAN'}},function(err,doc){

 

마지막으로 데이타를 조회하는 부분이다. /list URL은 전체 리스트를 리턴하는 코드이고, /get ?userid= 쿼리 스트링으로 정의되는 사용자 ID에 대한 레코드만을 조회해서 리턴한다.

router.get('/list', function(req, res, next) {

      db = req.db;

      db.get('users').find({},function(err,doc){

           if(err) console.log('err');

           res.send(doc);

      });

});

router.get('/get', function(req, res, next) {

      db = req.db;

      var userid = req.query.userid

      db.get('users').findOne({'userid':userid},function(err,doc){

           if(err) console.log('err');

           res.send(doc);

      });

});

Figure 14 /routes/mongo.js 에서 데이타를 조회하는 코드

 

이제 /routes/mongo.js 의 모든 코드 작업이 완료되었다. 이 코드를 호출하기 위한 HTML 폼을 작성하자.

 

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

 

<h1> Native MongoDB Test Example</h1>

<form method='post' action='/insert' name='mongoform' >

      user <input type='text' size='10' name='userid'>

      <input type='submit' value='delete' onclick='this.form.action="/delete"' >

      <input type='button' value='get' onclick='location.href="/get?userid="+document.mongoform.userid.value' >

      <p>

      city <input type='text' size='10' name='city' >

      sex <input type='radio' name='sex' value='male'>male

      <input type='radio' name='sex' value='female'>female

      <p>

      <input type='submit' value='insert' onclick='this.form.action="/insert"' >

      <input type='submit' value='update' onclick='this.form.action="/update"' >

      <input type='button' value='list'  onclick='location.href="/list"' >

     

</form>

</body>

</html>

Figure 15 /public/monksample.html

 

node.js를 실행하고 http://localhost:3000/monksample.html 을 실행해보자



Figure 16 http://localhost:3000/monksample.html 실행 결과

 

아래 insert 버튼을 누르면, 채워진 필드로 새로운 레코드를 생성하고, update 버튼은 user 필드에 있는 사용자 이름으로된 데이타를 업데이트 한다. list 버튼은 컬렉션에서 전체 데이타를 조회해서 출력하고, delete 버튼은 user 필드에 있는 사용자 이름으로된 레코드를 삭제한다. get 버튼은 user 필드에 있는 사용자 이름으로 데이타를 조회하여 리턴한다.

다음은 list로 전체 데이타를 조회하는 화면이다.

 


Figure 17 /list를 수행하여 mongoDB에 저장된 전체 데이타를 조회하는 화면


이 코드의 전체 소스코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongoDBexpress 에 있으니 필요하면 참고하기 바란다


다음 글에서는  node.js의 mongoDB ODM 프레임웍인 mongoose 이용한 접근 방법에 대해서 알아보기로 한다.


Couchbase Server

#6. Couchbase server 구조

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




이번에는 마지막으로 카우치베이스의 아키텍쳐에 대해서 알아보도록 하자

노드와 클러스터 (Node & Cluster)

노드는 물리적인 서버에서 기동하는 하나의 카우치베이스 인스턴스로, 카우치 베이스는 여러 개의 노드로 이루어진 클러스터로 구성된다


클라이언트 SDK (Client SDK)

프로그래밍 언어별로 카우치베이스에 접근하기 위한 API(SDK)를 제공한다.


vBucket 개념

카우치베이스는 실제데이타와 물리서버간의 맵핑을 vBucket이라는 것을 이용해서 관리한다. 카우치베이스는 키-밸류 스토어이다. 그래서, 각 키가 어디에 저장되어 있는지를 vBucket이라는 단위로 관리 하는데. 키에 대한 해쉬값을 계산한 후에, 각 해쉬값에 따라서 저장되는 vBucket을 맵핑한다음 각 vBucket을 노드에 맵핑한다.

아래는 서버 3대가 있었을 때, vBucket을 맵핑하는 구조에 대한 예제이다.



※ 출처 : http://docs.couchbase.com/couchbase-manual-2.5/cb-admin/#vbuckets

클라이언트 SDK는 이 vBucket와 노드에 대한 맵핑 정보를 클러스터로부터 받아서 관리한다. 즉 키에 대한 물리적인 서버 맵핑 정보를 클라이언트가 SDK를 통해 직접알 수 있기 때문에, 클라이언트가 PROXY등을 거치지 않고 직접 데이터가 저장된 노드로 접근이 가능하다.

CF. mongoDB의 경우 중간에 Proxy를 거쳐서 데이터가 저장된 물리 노드로 접근하게 된다.

만약에 노드가 추가되거나 삭제되었을 때, 물리적으로 데이터가 다른 노드로 다시 분산 배치되고, 새롭게 배치된 데이터에 따라서 vBucket to 노드간의 데이터 맵핑 정보도 업데이트 되는데, 이를 Rebalancing이라고 한다. (Rebalancing에 대한 내용은 뒤에 다시 설명)



http://docs.couchbase.com/couchbase-manual-2.5/images/vbuckets-after.png 


노드의 상세구조

그러면 각 노드는 어떤 형태로 구성이 될까? 아래는 노드의 대략적인 아키텍쳐이다.

카우치베이스의 노드는 아래 그림과 같이 크게 좌측의 Data Manager와 우측의 Cluster Manager로 나뉘어 진다.




Cluster Manager

Cluster Manager는 노드에 대한 상태와 클러스터에 대한 상태, 설정등을 관리하는 부분으로 Erlang/OTP 기반으로 구현되어 있다. 그 상위단에는 Admin Portal을 위한 Web UI가 8091 포트로 제공되고 있고, 같은 포트로 REST API가 함께 제공된다.

카우치베이스는 클라이언트 SDK는 이 8091 포트의 REST API를 통해서, 설정 정보와 앞서 설명한 vBucket 정보를 읽어온다. 여기에는 실제로 데이터에 대한 set/get이나 뷰 쿼리 수행용 포트정보도 포함이 되는데,  아래 Data Manager에서 제공되는 11211 포트나, 8092 포트가 사용된다..

그 외에도 클러스터 노드간의 통신을 위한 4389, 21100 포트등 다수의 포트가 사용되는데, 카우치베이스는 서버-클라이언트, 서버-서버간에 사용하는 포트들이 많기 때문에, 배포 전에 반드시 포트들을 확인하고 방화벽이나 네트워크 설정에 반영해야 한다.

※ http://docs.couchbase.com/couchbase-manual-2.5/cb-admin/#faqs 문서를 보면 배포시 오픈해야 하는 포트들이 설명되어 있다.


Data Manager

Data Manager 부분은 직접 데이터에 접근하는 부분으로 set/get 메서드를 이용하여 데이터를 저장하거나, 뷰에 대한 쿼리를 수행할 때 접근되는 인터페이스이다.

맨 아래단에는 멀티쓰레드 기반의 Persistence 엔진이 있으며, 디스크에 데이터를 저장하거나 읽어드릴때 사용되는 컴포넌트이다. 그 윗단에는 memcached가 있으며, 데이터를 캐슁하는데 사용된다. 또한 이 계층에서 뷰에 대한 쿼리 엔진이 제공된다.

Memcached 위에는 moxi 가 Proxy로 사용된다.


데이터 쓰기와 복제

클라이언트에서 데이터 쓰기가 발생했을 때, 카우치베이스는 어떻게 데이터를 저장할까?

먼저 클라이언트에서 Client SDK를 통해서 쓰기 요청을 하면, Client SDK는 해쉬 알고리즘에 따라데이터의 키 값에 맵핑 되는 vBucket을 찾아내고, 그 vBucket에 맵핑 되는 노드를 찾아서 쓰기 요청을 전달한다.

쓰기 요청은 해당 노드의 Listener로 전달되고, 이 Listener는 들어온 데이터를 로컬의 캐쉬에 쓰고 클러스터의 다른 노드로 복제 요청을 보낸다. 그리고 데이터는 노드의 디스크에 저장된다.




쓰기 과정중에 노드간의 복제가 발생한다.


노드별 메모리 레이아웃

그러면 각 노드별로 메모리 레이아웃은 어떻게 되어 있을까? 카우치베이스의 경우, memcached를 이용하는 만큼 서버의 메모리 공간 계산이 매우 중요하다. 앞서 글들에서도 설명하였지만, 메모리에 대해서 고려할 때, 카우치 베이스는 버킷의 키를 모두 메모리에 로딩해놓고 있다. 최소 메모리 공간은 전체키의 합보다는 최소한 커야 한다.그리고 각 도큐먼트당 60바이트의 메타 정보 저장공간이 필요하다. (키크기 + 60 바이트)*전체레코드수 / 노드수 * 3 (복제본수) 가 노드당 최소 메모리양이다. 최소 메모리란 말 그대로 최소한 돌릴 수 있는 수준을 이야기하는 것인데, 이 경우에는 캐쉬를 전혀 사용하지 못하기 때문에, 이 메모리 용량으로 서버를 운영하면 절대 안된다. (말그대로 아주 최~~소한이다.)


전체 하드웨어 공간에서 OS가 기본적으로 사용하는 용량을 제외 하면, 카우치베이스의 노드가 그 메모리 공간을 활용하는데, 카우치베이스에서는 노드에 할당된 메모리 공간을 버킷별로 다시 할당한다. (버킷을 생성할 때 설정할 수 있음)


리밸런스(Rebalance)

리밸런스 노드가 클러스터에 추가되거나, 장애등의 이유로 삭제되었을 때 데이터를 다시 노드에 분산 배치를 하는 작업이다. 노드간에 데이터 복제가 심하게 일어나기 때문에, 리밸런스는 부하가 적은 시간대에 하도록 권장하고 있다. (관리 콘솔을 보면 리밸런스를 멈추거나 시작할 수 있는 기능이 있다.) 향후에는 리밸런스를 Throttling 하는 기능이 나온다고 하니 기대해볼만하다.

NoSQL의 경우 특정 노드가 장애가 나서 시스템이 장애 나는 케이스보다 보통 노드를 추가/삭제할때 발생하는 이런 리밸런싱에 의해서 부하가 올라가거나 해서 장애가 나오는 케이스가 많기 때문에 특별히 주의를 기울일 필요가 있다.

 

XDCR

XDCR은 데이타 센터간에 카우치베이스 클러스터 데이타 복제를 지원하는 기능이다.

현재 최신 버전은 2.5 버전인데, 2.5 버전에서는 XDCR을 TLS/SSL을 이용해서 복제하기 때문에 자체적으로 보안을 지원한다. 그렇지만 무료 버전인 2.2 (Community Edition)의 경우 TLS/SSL 기반의 복제가 지원되지 않기 때문에, VPN 기반의 네트워크를 터널을 설정하고, VPN 터널을 통해서 XDCR 복제를 하도록 가이드 하고 있다.


Couchbase Gateway & CouchBase Mobile

카우치베이스의 흥미로운 점중의 하나는 모바일 디바이스에 탑재될 수 있는 Couchbase Lite버전을 제공한다는 것이다. iOS,안드로이드 버전을 제공하며, 또는 일반적인 애플리케이션에 사용할 수 있도록 자바버전과 .NET  버전도 제공된다. (모두 Community edition이 제공된다)

이 Couchbase Lite는 Couchbase 서버와 동기화가 가능하다.


다 못한 이야기

개발 관점에서는 GET/SET그리고 뷰 정도로 간편하지만, 운영과 설정에 대해서는 많아도 너무 많다. 대략적인 사용방법 아키텍쳐에 대해서 알아봤는데, 언급하지 못한 부분이 있어서 몇 가지만 언급하고자 한다.

카우치베이스는 다양한 커넥터를 이용하여 다른 솔루션과의 연동을 지원한다.

Elastic Search와 연동을 통하여 데이터에 대한 FTS (Full Text Search : 검색)을 지원할 수 있으며, Hadoop 연동을 통해서 Map & Reduce 기반의 데이터 처리가 가능하다. 또 오픈소스 ETL인 Talend 연동을 통해서 데이터를 다른 시스템으로 연동(복제)가 가능하며, 마지막으로, 데이터 암호화 솔루션인 Gazzang과 연동을 통해서 데이터를 암호화하여 저장할 수 있다.

http://www.couchbase.com/couchbase-server/connectors