[구글스터디잼] Kubernetes in the Google Cloud #3

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

 * Setting up a Private Kubernetes Cluster
외부 IP를 가지고 않고 내부 네트워크 ip 대역만 가지고 동작하게 되는 쿠버네티스 클러스터를 설정하는 방법에 대해서 소개하는 세션입니다.

gcloud beta container clusters create private-cluster \
    --private-cluster \
    --master-ipv4-cidr 172.16.0.16/28 \
    --enable-ip-alias \
    --create-subnetwork ""

그리고 해당 클러스터 master에 접근할 수 있는 네트웍 대역을 지정할 수 있습니다. 여기서 MY_EXTERNAL_RANGE 는 CIDR 형태로 172.16.0.16/28 같은 형태로 표현해야 합니다.

gcloud container clusters update private-cluster \
    --enable-master-authorized-networks \
    --master-authorized-networks [MY_EXTERNAL_RANGE]

 

  • Helm Package Manager
    Helm 은 쿠버네티스 위에서 어플리케이션을 관리하는 매니저 툴입니다. 클라이언트/서버 구조로 구성되고 클라이언트는 Helm, 서버는 tiller 라고 부릅니다.

    Helm 의 설치는 다음과정을 통해서 이루어집니다.

    curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
    chmod 700 get_helm.sh
    ./get_helm.sh
    kubectl -n kube-system create sa tiller
    

    그리고 tiller role을 설정해줍니다.

    kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller
    

    tiller를 설치합니다.

    helm init --service-account tiller
    

    이제 거기에 올라가서 동작할 chart를 설치해봅니다.

    helm repo update
    helm install stable/mysql
    

  • NGINX Ingress Controller on Google Kubernetes Engine
    Ingress는 해당 노드로 들어오는 트래픽, Egress는 해당 노드에서 외부로 나가는 트래픽을 의미합니다. 여기서는 앞에서 배운 Helm을 이용해서, nginx 라는 좋은 웹서버를 이용해서 Ingress 트래픽을 제어하는 예를 보여주게 됩니다. Ingress 트래픽을 제외한다는 것은 특정 요청을 다른곳으로 보낸다거나, 바꿀수 있습니다.

ingress

먼저 kubenetes 클러스터를 생성합니다.

gcloud container clusters create nginx-tutorial --num-nodes 2

그리고 helm 을 인스톨합니다.

curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
chmod 700 get_helm.sh
./get_helm.sh

Role based access control(RBAC) 설정을 합니다.

kubectl create serviceaccount --namespace kube-system tiller
kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'  

helm init --service-account tiller --upgrade

이제 hello-app 을 실행합니다.

 kubectl run hello-app --image=gcr.io/google-samples/hello-app:1.0 --port=8080
 kubectl expose deployment hello-app

이제 ingress 용 nginx를 실행합니다.

helm install --name nginx-ingress stable/nginx-ingress --set rbac.create=true

스크린샷 2019-01-27 오후 3.42.38

이제 위와 같이 nginx-ingress를 설정합니다.

kubectl apply -f ingress-resource.yaml

Reference

  1. https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/
  2. https://arisu1000.tistory.com/27835
  3. https://sktelecom-oslab.github.io/Virtualization-Software-Lab/Helm/
  4. https://kubernetes.io/blog/2016/10/helm-charts-making-it-simple-to-package-and-deploy-apps-on-kubernetes/
  5. https://bcho.tistory.com/1272
Advertisements

[구글스터디잼] Kubernetes in the Google Cloud #2

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

3장 부터 4장은 Kubernetes 에서 Pod, Service, Scaling과 deployment 를 보여주고, 5,6장은 젠킨스와 slackbot 예제를 보여주고 있습니다.

실제로 이 예제를 따라가다 보면, 영어를 착실하게 보지 않는다면, Pod는 무엇이고 Service는 무엇인가에 대해서 막 혼란이 오게 됩니다.

Pod는 하나 또는 여러개의 컨테이너와 볼륨으로 구성된 오브젝트입니다. namespace 를 공유하고 Pod당 하나의 ip가 할당됩니다.(여기서 할당되는 ip는 public 일수도 있고, 그냥 private 일 수도 있습니다. 그러나, 보통 private 이 할당되겠죠?)

pods_1

그리고 Service는 특정 Pod들에 대한 endpoint 가 됩니다. 해당 Service의 endpoint 에 접속하면 아래의 Pod들에게 자동으로 데이터가 한번씩 전달되게 됩니다.(Load Balancer 역할)

service_1

예제를 살펴봐도, 하나의 Pod가 여러개의 container를 포함하지만, 하나의 Pod가 한 종류의 여러 Container를 가지는 걸로 보이지는 않고, 하나의 서버 단위가 하나의 Pod라고 봐도 될듯합니다. 예를 들어, 하나의 Pod에 API Server, Volume 가 들어가고, 이제 Service가 그 Pod들을 로드밸런싱 합니다. 서비스에 더 처리용량이 필요하면, Pod를 계속 추가하는 거죠. 이제 kubernetes의 scaling 이라고 보시면 됩니다.

아래 그림을 보면 kind 가 Pod로 나옵니다.
스크린샷 2019-01-20 오후 8.00.53

그리고 이 Pod 는 다음과 같이 실행이 가능합니다.

kubectl create -f pods/monolith.yaml

이제 이 앞단에 들어갈 서비스를 생성해봅시다.

스크린샷 2019-01-20 오후 8.00.40

생성하는 방법은 동일합니다.

kubectl create -f service/monolith.yaml

또, 논리적으로 구분하는 Namespace 라는 개념과, Label 이라는 개념을 이용해서 관리가 가능합니다. 해당 Pod는 frontend 다, backend다 이런식으로 태그를 달아주는 것이죠.

그리고 이런 오브젝트들을 관리하기 위해 컨트롤로러라는 개념이 있다고 합니다. 여기에 replicaset, deployment 라는 컨트롤러가 있고 이를 이용해서 쉽게 scale 변경이 가능합니다.

kubectl scale deployment hello --replicas=3
kubectl scale deployment hello --replicas=5

그리고 Rolling Update 도 지원합니다(한번에 정해진 단위로만 업데이트를 해서 전체를 업데이트 합니다.)
다음과 같이 edit deployment 를 이용하여 container 이미지를 수정하게 되면 자동으로 Rolling update가 시작합니다.

kubectl edit deployment hello

Rolling update를 멈추거나 멈춘 업데이트를 재개하고 싶다면 다음 명령을 이용합니다.

kubectl rollout pause deployment/hello
kubectl rollout resume deployment/hello

여기서 중요한 것은, 업데이트 이후에 이전 버전으로 돌리고 싶다면… rollout undo를 이용해서 이전 버전으로 쉽게 돌릴 수 있습니다. 이것도 Rolling update로 진행됩니다.

kubectl rollout undo deployment/hello

Reference
1. https://zzsza.github.io/development/2018/04/17/docker-kubernetes/
2. https://kubernetes.io/ko/docs/concepts/overview/working-with-objects/kubernetes-objects/
3. http://bcho.tistory.com/1256
4. https://blog.2dal.com/2017/03/07/kubernetes/

[구글스터디잼] Kubernetes in the Google Cloud #1

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

총 10개의 챕터가 있고 일단은 가장 처음 두 개인, Introduction to Docker, Hello Node Kubernetes 를 공부했는데…

1] Introduction to docker
-> 실제로 Docker 책을 보면 쉽게 보게되는 명령들을 소개해줍니다. 즉 docker 를 실행시키거나, 현재 수행중인 container 에 접속한다거나, 현재의 수정본을 docker hub 등의 외부 registry에 등록하는 방법(사실 여기서는 gcloud 를 이용해서 gcp내의 registry에 등록하더군요.)

/node-app:0.2

사실 이미 docker 를 사용해 보신 경험이 있다면, 특별한 차이를 느끼지 못하실꺼 같습니다. 다른 차이는 실습을 하게 되는 환경에서 이미 docker/kubenetes 등이 다 설치되어 있어서… 아주 편하게 실습을 할 수 있다는 것 정도… 일단 제목이 Kubernetes in the Google Cloud 인거 처럼 kubenetes 내용은 2장 부터 시작됩니다.

2] Hello Node Kubernetes

Kubernetes 는 Container Orchestration Tool 이라고 볼 수 있습니다.(즉 컨테이너를 관리해주는?) 여기서는 간단히 kubenetes 위에서 node application을 배포하고 Rolling update 하는 것을 보여주게 됩니다.

2-1] Cluster 의 생성
다음과 같은 과정을 통해서 클러스터를 생성하게 됩니다. 클러스터가 생성된다고 이미지가 돌고 있거나 그러지는 않고, 클러스터만 실행된다고 생각하면 됩니다.

#project 이름 설정
gcloud config set project PROJECT_ID

#hello-world 라는 이름으로 cluster 생성, 노드는 2개, 머신 타입은 n1-standard-1, 생성되는 존은 us-centrall-a 입니다.
gcloud container clusters create hello-world \
                --num-nodes 2 \
                --machine-type n1-standard-1 \
                --zone us-central1-a


2-2] Pod 의 생성
생성된 클러스터에서 이제 실제로 container를 실행하게 됩니다.

#hello-node 라는 이름으로 컨테이너 실행
kubectl run hello-node \
    --image=gcr.io/PROJECT_ID/hello-node:v1 \
    --port=8080

#kubectl get deployments 로 현재 deployment 상황을 볼 수 있고
kubectl get deployments

#kubectl get pods 로 현재 수행중인 pod를 볼 수 있습니다.
kubectl get pods

2-3] container 외부에 노출하기
갓 만들어진 컨테이너를 외부에 서비스하기 위해서는 외부에서 접속을 할 수 있어야 합니다. 하지만 처음 생성되었을때는 kubenetes 내부에서만 연결이 되고 외부에서는 접속이 안될 것입니다. 이럴 때 특정 pod는 외부에서 접속할 수 있어야 하는데, 이러면 외부 IP를 가지거나 , 외부에서 해당 pod에 연결할 수 있는 proxy가 실행되어야 할껍니다.(아니면 기존 proxy의 설정이 바뀌거나…)

#외부에 노출하기
kubectl expose deployment hello-node --type="LoadBalancer"

#어떤 서비스들이 있는지 확인
kubectl get services
#결과의 EXTERNAL-IP를 통해서 외부 IP를 확인할 수 있습니다.

2-3] container scaling(개수 변경)
최초에는 container 가 하나만 실행되는데, 이 개수를 바꾸거나 하고 싶을때 어떻게 해야하는지 설명합니다.

#실행되는 컨테이너의 개수를 4개로 바꾸기
kubectl scale deployment hello-node --replicas=4

#kubectl get deployment를 하면 아까와 다른 결과를 볼 수 있습니다.

다만 이 과정과정가 눈깜짝할 사이에 끝나지는 않습니다. 저는 실험결과 몇초에서 몇분 정도 지나야 이제 제대로 적용되는 걸 볼 수 있습니다.

이제 쿠버네티스의 상태는 다음과 비슷합니다.
kube1

실제로 다음에 해보면 좋을것 같은 부분…
scale 은 실제로 서비스를 할 때, 중요한 부분이므로, 자신들의 서비스를 넣고, 롤링 업데이트나 scale을 바꿀때, 실제로 영향이 어떻게 되는지를 확인하시는게 좋을듯 합니다.

[입 개발] EXT4 에서 달라진 부분들 #2 – 데이터 영역의 관리

EXT4가 들어오면서 크게 달라진 부분 그 두 번째는, 데이터 영역을 관리하는 방법입니다. 일단 기존에서 사용하던 방식을 알아보도록 하겠습니다. 파일시스템의 특성을 볼때는, 그 시기에 있던 기술의 한계를 알면 도움이 되는데, 최초에 ext가 나오던 시기는 그렇게 큰 파일이 많지는 않던 시기입니다. 즉 엄청 큰 파일을 처리할 일이 많지는 않던 시기입니다. 그리고 기본적으로 ext2~3 까지는 하나의 파일의 최대 크기는 4 bytes 변수를 사용하기 때문에 최대 4기가 한계입니다.(블럭의 크기와 상관없이 일단 최대한 가졌을 때 표현할 수 있는 한계가 4기가입니다. 파일 크기를 나타내는 변수가 4 bytes 이기 때문이죠.)

여기서 보통 파일의 종류에 따라서 File/Directory 에서 File이면, 실제 데이터 영역에 파일의 내용이 있고, 디렉토리면, 해당 디렉토리에 있는 파일 목록에 대한 정보가 데이터 영역에 관리가 되게 됩니다. 즉, “I am a boy” 라는 내용을 가진 파일이 있다면, 그 파일의 크기는 10이고, block 크기가 4k라면, 한 개의 block만 할당이 되어있을것입니다. 그래서 데이터 영역에 가면 처음 10바이트가 “I am a boy”가 들어있게 됩니다. 이걸 관리하는 정보는 EXT는 inode에 있고, ext2~3에서는 그 크기는 총 60바이트입니다. 그리고 블럭을 가리키는 정보는 하나가 4 bytes 입니다. 즉, 60바이트의 공간이 할당되어 있는데, 한 칸이 4 bytes 면 총 15개의 정보를 저장하는 공간이 들어갈 수 있습니다.(FAT32 를 안다면 Fat Table 을 생각하시면 쉽습니다.)

그림1

그런데 여기서 잘 생각해보면, 그림1 처럼 한칸이 4 bytes라 하나의 블럭을 가리키면… 60바이트로는 총 15개… 한 칸이 하나의 블럭을 가리키므로, block 크기가 4k면 4k * 15 = 60k 밖에 안됩니다.(아까는 4기가가 최고라며!!!) 어떻게 된 것일까요? 사실 EXT2/3의 특징 중에 하나가 direct, indirect block, double indirect block, triple indirect block 으로 구성이 된다는 것입니다.

먼저 시작하기 전에 inode에 들어있는 15 개의 칸은… 12개의 direct와 각각 하나씩의 indirect, double indirect, triple indirect 로 구성이 되어있습니다. 먼저 direct 는 그 칸 하나가 그림2처럼 실제 디스크의 블럭하나를 가리키는 것입니다. direct 로는 최대 60k 밖에 파일이 가질 수가 없으므로, 이 사이즈를 더 크게 사용하기 위한 것들이 위한 indirect, double indirect, triple indirect 방식입니다.

그림2

12개니 각 블럭이 4k면 48k의 공간을 지정할 수 있습니다. 그렇다면 13번째 칸의 indirect 는 뭐냐, indirect 가 가리키는 블럭으로 가면, 아까 12개의 direct block을 가리키는 정보가 있던 것과 같은 형태로 실제 디스크를 가리키는 블럭이 direct block이 나옵니다. 블럭 크기가 4k면 여기에 실제 디스크의 블럭을 가리킬 수 있는 정보가 각각 4 byte니 1024개가 들어가게 됩니다. 즉, indirect block은 실제로 direct 블럭 1024개를 가리키므로 4M의 공간을 지정할 수 있습니다. 그림3을 참고합시다.

그림3

그럼 이제 슬슬 이해가 가기 시작할 것입니다. indirect 블럭은 해당 블럭이 direct block을 어드레싱 하는 주소 정보가 들어가 있고, double indirect 블럭이 가리키는 정보는 indirect 블럭을 가리키는 블럭 1024개를 가리키는 블럭에 대한 정보가 됩니다. 그림4를 참고하면, 실제로 double indirect 블럭의 내용은 indirect block 정보들입니다.

그림4

여기서는 4M 짜리 1024개를 가리키므로 4G 의 디스크 정보를 가리킬 수 있습니다. 이쯤되면 이제 triple indirect 의 성격도 눈치 채실 껍니다.
triple indirect block이 가리키는 정보는 double indirect block 1024개를 가리키는 정보입니다. 그림5를 참고하면 됩니다. 그런데 여기서
의아한 것은 사실 double indirect block 만으로도… 실제 파일의 최대 크기인 4G가 넘어간다는 것입니다.(물론 블럭 크기가 4K 일 경우입니다. 1K로 설정되면 이 마지막 블럭을 사용하게 되겠지만… 블럭 크기가 4k면… 마지막 triple indirect 는 사용할 이유가 없습니다.)

블럭 크기가 4k 일때, triple indirect block은 4G * 1024 = 4TB 의 데이터 영역을 가리킬 수 있습니다. 그림5를 참고합시다.

그림5

그런데 ext4로 가면서 왜 이런 구조를 버리는 것일까요? 간단하게 생각해보면 해당 구조는 낭비가 너무 심합니다. 혹시나 FAT32에서 NTFS로 가면서 내부구조가 어떻게 바뀌었는지 이해한다면, 실제로 EXT4의 extend를 이해하는 것은 굉장히 수월합니다.

먼저 간단하게 생각해봅시다. 1000, 1001, 1002, 1003 4개의 블럭이 있다고 할 때 이걸 가리키는 방법으로 ext2에서의 기존 방식은 하나에 한칸을 가리켜야 하므로 총 4칸 16 바이트가 필요합니다. 그런데 이렇게 표현할 수 있지 않을까요? (시작위치, 블럭 개수), 이런 형태면 다음과 같은 (1000, 4) 형태로, 표현이 가능해집니다. 데이터 공간도 각각 4 바이트를 쓴다고 하더라도 8바이트면 줄어듭니다. 물론 매번 필요한 공간이 Fragmentation 이 발생한다면 (1000, 1), (1001, 1), (1002, 1), (1003, 1) 형태로 공간을 낭비하게 됩니다. 즉 연속된 공간이 많이 필요할 수록, 새로운 표현 방법이 공간을 절약할 수 있습니다. 또한 추가 분석할 필요 없이도 쉽게 뒤에 얼마만큼 읽어야 할지도 알 수 있게 됩니다.

이런 방식을 적용한것이, ntfs의 cluster runs 나, ext4의 extent 라는 구조입니다. 그런데 extent는 (시작위치, 블럭 개수) 의 구조에서 블록 개수에는 2 byte만 할당되어 있습니다. 그래서 block 크기가 4k 기준일 때 무조건 32768 즉 4k * 32768 = 128MB 이상되면 아무리 디스크에 연속적으로 할당된 공간이 있더라도 extend가 추가로 생기게 됩니다. 즉 1GB 파일이면 8개는 extent가 생겨야 합니다.

그런데 extent구조를 보면 헤더가 일단 12 바이트이고, 리프 노드(실제 파일의 정보를 가리키는)냐, 인덱스 노드(리프 노드나 다른 인덱스 노드의 정보를 가지는)냐에 따라 가지는 정보들이 각각 12 bytes입니다. 즉… 60바이트에서 헤더를 빼고나면 일단 inode 안에는 4개의 리프 노드만 들어갈 수 있습니다. 즉 512MB 보다 파일 사이즈가 커지거나 fragmentation 이 많이 나면… 결국 tree 형태로 관리되는 정보가 실제 다른 블럭에 들어가야만 합니다.

그림6

그래서 실제 구조는 그림6처럼 되게 됩니다. 그리고 이 extent도 실제로는 해당 파일의 inode의 flag에 USE_EXTENT 가 설정되어 있어야 사용하게 됩니다. 즉 ext4 내에 있는 파일이 어떤 건 extent 형태로, 어떤건 옛날 방식으로도 주소 지정이 가능하다라는 것입니다. 다음번에는 기존에 왜 한 디렉토리에 파일이 많으면, 파일 열기등이 느려지는지, ext4에서는 어떻게 풀고 있는지 가볍게 살펴보도록 하겠습니다.

[입 개발] EXT4 에서 달라진 부분들 #1 – Flexible Block Group

사실 EXT4는 나온지 굉장히 굉장히 오래된 파일 시스템입니다. EXT의 역사를 보면 간략히 다음과 같습니다.

  • EXT, 1992년 출시
  • EXT2, 1993년 출시
  • EXT3, 2001년 커널 2.4.15에 포함
  • EXT4, 2006년 커널 2.6.19에 포함
  • 2008년 커널 2.6.28에 EXT4 안정 버전 포함

한마디로 굉장히 오래되었습니다. 거의 10년이 지났네요. 그런데 아는 분들은 다 아시지만, 그래도 모르는 사람들은
EXT2 -> EXT3 -> EXT4로 넘어가면서 무엇이 변했는지 잘 모릅니다. 그냥 좋아졌다?

EXT4로 들어오면서 크게 바뀐것들이 좀 있는데, 그 중에서도 Flexible Block Group, Extent를 위한 HTree,
많은 수의 파일 목록에서 쉽게 파일을 찾기 위한 Hash Directories 등이 있습니다.

일단 생각은 해당 EXT4 시리즈에서는 크게 위의 세 가지만 다뤄볼려고 합니다. 자세한 것은 당연히 다루지 않습니다.
왜냐… 잘모르니깐…

Flexible Block Group 을 설명하기 위해서는 먼저 EXT에서 Disk Layout 을 어떻게 구성하는가 부터 알아야
합니다. EXT에서는 Block Group 이라고 해서 특정 사이즈 단위로 디스크를 나눠서 관리 정보를 가지고 있다고
보시면 됩니다.

그림1

즉 그림1 처럼 하나의 디스크를 여러개의 Block Group 으로 나누게 됩니다. 보통 하나의 Block Group 은 128MB
할당 됩니다. 그럼 하나의 Block Group 은 어떻게 구성되는가? 그림2 처럼 각 Block Group 은 전체 정보를 가지는
Super Block 과 GDTs, Reserved GDTs를 가지게 됩니다. 이 정보들은 주로 Block Group 0번의 것을 사용하지만
백업용으로 다른 Block Group 들에도 존재합니다. 이 것 이외에 Block Bitmap, Inode Bitmap, Inode Table,
Data 영역은 각 Block Group 단위로 관리되는 정보들입니다. 이 때 보면 Block Bitmap, Inode Bitmap은 하나의
Block 만 할당 받기 때문에 Block의 크기가 4K 라면 bit 단위로 총 32k 개를 관리할 수 있습니다. 그래서 32k * 4K
해서 하나의 Block Group 의 크기가 128MB로 보통 설정되게 됩니다.

그림2

그런데 이 구조의 단점은 무엇일까요? 작은 크기의 파일은 큰 문제가 없는데, 128MB(실제로는 이것보다 조금 더 작은 크기의)
를 넘어가는 파일부터는 Block Group 하나에 저장되지 않아서, 무조건 Disk 에 물리적으로 Fragmentation 이 발생하게 됩니다.
즉 연속적으로 읽을 수 없고, Random Access 가 발생해야 한다는 것이죠. 그리고 현재에는 사실 128MB 단위는 생각보다
많이 작습니다.

그럼 결국 Flexible Block Group 은 뭐냐? 몇개의 Block Group 들을 하나로 묶자는 것이 메인 아이디어 입니다.
그래서 그림3 처럼 각 Block Group 의 Block Bitmap, Inode Bitmap 등이 쭈욱 연결되서 하나처럼 관리되게 됩니다. 여기서는
8개의 Block Group 이 하나처럼 관리되는 것으로 가정했습니다.

그림3

정리하자면, 각 Block Group 에 있던, Block Bitmap, Inode Bitmap, Inode Table 영역들이 쭈욱 연결되는 형태로 들어가게 됩니다.
0번 Block Group 에 다른 Block Group 들의 메타정보(Block Bitmap, Inode Bitmap, Inode Table) 들이 다 넘어오게 되는 것이죠.
그림2와 비교해보시면 그 차이가 명확하게 보이실 겁니다.

주의 할 것은 EXT4에서 Flexible Block Group 은 무조건 사용하는 것이 아니라, 헤더에 FLEX_BG 라는 Flag가 설정되어 있어야만 Flexible Block Group 을 사용 가능합니다. 즉, 이 Flag가 꺼져있다면, 예전 방식대로 처리해야한 합니다.

다음번에는 EXT4에서 extents를 어떻게 관리하는지, 살펴보도록 하겠습니다.

EXT4 에 대해서 Ext4 Disk Layout 이라는 매우 좋은 자료가 있습니다. 자세한 건 이걸 참고하세요.

[입 개발] Redis 에서 zadd 와 zincrby 의 차이

안녕하세요. 입개발 CharSyam입니다. 오래간만에 포스팅을 하게 되네요. 오늘은 아주 간단한 것을 포스팅 할려고 합니다. 가끔씩 Redis의 sorted set 을 사용하는 명령중에 zadd 와 zincrby 가 있습니다. 과연 이 두 개의 명령은 어떤 차이가 있을까요?

결론부터 말하자면, zadd 와 zincrby 는 사실 같은 기능을 사용하는 아주 유사한 명령입니다. 먼저 zadd 는 다음과 같이 사용하니다.

zadd key score member

그러면 sorted set 에 member 가 해당 score를 가지게 됩니다. 여기서 가지게 됩니다를 중요하게 봐주세요.

그리고 zincrby 는 특정 값 increment 만큼 score를 증가시키는 명령입니다.

zincrby key increment member

그렇다면 다음 두 가지 경우로 나눠보도록 하겠습니다. 현재 해당 key에 member가 존재하지 않는 경우와 존재하는 경우입니다. 먼저 존재하지 않을 경우, 두 개의 명령 zadd 와 zincrby 는 모두 해당 member를 생성하게 됩니다. 그리고 zadd 는 해당 score로 설정하고 zincrby 는 0에 해당 increment를 추가한 것 처럼 동작합니다.(즉 둘 다 해당 값으로 설정하게 되는거죠.)

그렇다면 해당 member가 존재할 경우는 어떻게 될까요? zadd는 해당 member의 score를 현재 넘겨준 값으로 변경시켜 버립니다. 즉 기존에 3이 었는데 zadd key 1 member 라면 해당 member 의 score 는 3에서 1로 변경이 됩니다. zincrby 의 경우는 zincrby key 1 member 라면 기존 값이 3이 있었다면, 이제 3에 1을 더하게 되어서 4가 되게 됩니다.

코드를 보면 zadd 와 zincrby 는 Flag 하나만 다르고 동일한 함수를 사용합니다.

void zaddCommand(client *c) {
    zaddGenericCommand(c,ZADD_NONE);
}

void zincrbyCommand(client *c) {
    zaddGenericCommand(c,ZADD_INCR);
}

그리고 zaddGenericCommand를 보면 zsetAdd 를 호출하게 되는데… zadd는 모두 기존 값을 가져와서, score 를 비교합니다. 그래서 zincrby 에서 ZADD_INCR 설정되면 new_score 로 기존값 + increment로 설정하고, zadd 에서는 new_score 를 넘겨준 설정값으로 설정하게 됩니다.

            /* Prepare the score for the increment if needed. */
            if (incr) {
                score += curscore;
                if (isnan(score)) {
                    *flags |= ZADD_NAN;
                    return 0;
                }
                if (newscore) *newscore = score;
            }

            /* Remove and re-insert when score changed. */
            if (score != curscore) {
                zobj->ptr = zzlDelete(zobj->ptr,eptr);
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                *flags |= ZADD_UPDATED;
            }

그리고 score가 변경되면 sorted set 은 기존 member를 지우고 다시 insert 하는 식으로 동작하게 됩니다. 결국 zadd 와 zincrby 는 거의 다른게 없습니다.

[입 개발] spring-security-oauth의 RedisTokenStore의 사용은 서비스에 적합하지 않습니다.

해당 내용은 spring-security-oauth 가 패치되면서, 현재는 성능적인 이슈는 해결된 상황입니다.
https://github.com/spring-projects/spring-security-oauth/commit/60f39ce82f380299cb1894baa02d65606f8f1365 다만 Redis를 TokenStore로 사용하면 여전히 메모리 관리에는 신경을 쓰셔야 합니다.

안녕하세요. 입개발 CharSyam 입니다. 저의 대부분의 얘기는 한귀로 듣고 한귀를 씻으시면 됩니다.(엉?) 일단 제목만 보면, 많은 Spring 유저들에게, 저넘의 입개발, 스프링도 모르면서라는 이야기를 들을듯 합니다.(아, 아이돌 까던 분들이 집단 따돌림을 당할 때의 느낌을 미리 체험할 수 있을듯 합니다. – 강해야 클릭율이 올라가는!!!)

먼저, 내용을 시작하기 전에 저는 Spring 맹, Java 맹으로 무식하다는 걸 미리 공개하고 넘어갑니다. 흑흑흑 (나의 봄님이 이럴 리 없어!!!)

일단 아시는 분이, Redis 와 oauth가 궁합이 안맞느냐라는 이야기를 들으면서 시작하게 됩니다. 아니, Redis와 oauth는 철자부터 다른데 무슨 얘기십니까? 라는 질문을 하다보니, Redis 에 과부하가 발생되서 처리가 잘 안된다는 얘기였습니다. 물론 수 많은 이유로 Redis가 느려질 수 있기 때문에, 자세한 정보를 요청했더니, spring-security-oauth 를 이용하고 계시다는 이야기를 들었습니다. 시간이 지나갈 수록 점점 느려진다는 느낌을 받고, 어떨 때는 Redis가 처리를 못한다고… 일반적인으로 아주 일반적으로 Redis는 아주 짧은 get/set 등의 요청은 초당 8~10만 정도는 가볍게 처리가 가능합니다.(일단 어떤식으로 문제에 접근했는가는 다음 번 주제로 미르고)

결론부터 얘기하자면, 당연히 사용하는 사람의 어느정도 실수가 발생하기 때문이긴 하지만, spring-security-oauth 의 RedisTokenStore 는 서비스에서 장애를 일으키기 쉽습니다.(보통은 라이브러리보다는 우리의 문제를 확인하는 것이 첫번째, 두번째도 우리문제, 세번째는 내 잘못인지 확인이… 정답입니다.)

먼저 Redis 는 Single Threaded 입니다. 즉, 하나의 명령이 많은 시간을 소모하면 그 동안은 아무런 작업을 하지 못합니다. 즉 Redis를 잘 쓰고 적합하게 이용하는 것은, 명령어를 빨리 수행해서 결과를 빨리 줄 수 있는 상황에서 이용하는 것이 적합하다는 것입니다.

사실 spring-security-oauth 자체가 큰 문제라기 보다는, 이 코드를 작성한 분은 사용자가 이런식으로 사용하게 되면 문제가 될 것이라는 고려를 하지 않은 것이 가장 큰 이슈입니다. 내부적으로 RedisTokenStore는 단순히 키 자체를 저장하는 get/set 커맨드와, 현재까지 발급되었던 키 정보를 저장하는 List 자료구조를 쓰고 있습니다. 아래 보면 rPush를 사용하는 approvalKey 와 clientId 입니다.

@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
......
    if (!authentication.isClientOnly()) {
	conn.rPush(approvalKey, serializedAccessToken);
    }
    conn.rPush(clientId, serializedAccessToken);
......
}

그런데 저 approvalKey와 clientId는 OAuth2Authentication -> OAuth2Request -> getClientId() 함수를 통해서 만들어집니다. 그런데 여기서 흔히들 하는 실수가 getClientId를 동일한 값으로 셋팅하는 것입니다. 그렇게 되면, 단순히 key가 있는지 확인할 때 말고 해당 List에 위와 같이 데이터가 들어가는 경우는 엄청나게 많은 아이템을 가진 List 자료구조가 생길 가능성이 높습니다.

그런데 Redis의 List 자료구조는 앞이나 뒤로 넣고, 앞이나 뒤에서 빼는 것은 빠르지만 O(1), 그 안의 데이터를 찾거나 모두 가져오면 결국 선형 탐색이 일어납니다. O(N), 그러면 그 안에 백만개 천만개가 들어있다고 가정하면 엄청난 속도 저하를 가져오게 됩니다. 그리고 그 안에 다른 명령을 하나도 처리할 수 가 없게됩니다.

아래 findTokensByClientIdAndUserName 와 findTokensByClientId 두 개의 함수는 대표적으로 모든 아이템을 가져오도록 하고 있습니다. 안쓰는걸로 하셔야 합니다.

	@Override
	public Collection findTokensByClientIdAndUserName(String clientId, String userName) {
		byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
		List byteList = null;
		RedisConnection conn = getConnection();
		try {
			byteList = conn.lRange(approvalKey, 0, -1);
		} finally {
			conn.close();
		}
		if (byteList == null || byteList.size() == 0) {
			return Collections. emptySet();
		}
		List accessTokens = new ArrayList(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections. unmodifiableCollection(accessTokens);
	}

	@Override
	public Collection findTokensByClientId(String clientId) {
		byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
		List byteList = null;
		RedisConnection conn = getConnection();
		try {
			byteList = conn.lRange(key, 0, -1);
		} finally {
			conn.close();
		}
		if (byteList == null || byteList.size() == 0) {
			return Collections. emptySet();
		}
		List accessTokens = new ArrayList(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections. unmodifiableCollection(accessTokens);
	}

위의 함수들은 안쓴다고 하더라도, 아래와 같이 removeAccessToken 함수는 자주 불리는데 여기서도 lRem을 통한 선형 탐색이 발생합니다.(lRem이 문제…)

	public void removeAccessToken(String tokenValue) {
		byte[] accessKey = serializeKey(ACCESS + tokenValue);
		byte[] authKey = serializeKey(AUTH + tokenValue);
		byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
		RedisConnection conn = getConnection();
		try {
			conn.openPipeline();
			conn.get(accessKey);
			conn.get(authKey);
			conn.del(accessKey);
			conn.del(accessToRefreshKey);
			// Don't remove the refresh token - it's up to the caller to do that
			conn.del(authKey);
			List results = conn.closePipeline();
			byte[] access = (byte[]) results.get(0);
			byte[] auth = (byte[]) results.get(1);

			OAuth2Authentication authentication = deserializeAuthentication(auth);
			if (authentication != null) {
				String key = authenticationKeyGenerator.extractKey(authentication);
				byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
				byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
				byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
				conn.openPipeline();
				conn.del(authToAccessKey);
				conn.lRem(unameKey, 1, access);
				conn.lRem(clientId, 1, access);
				conn.del(serialize(ACCESS + key));
				conn.closePipeline();
			}
		} finally {
			conn.close();
		}
	}

당연히 그러면 이건 사용자 실수가 더 큰거 아니냐 라고 하실 수 있습니다. 그런데 꼭 사용자의 실수가 아니더라도 특정 사용자가 비정상적으로 이런 동작을 하면 비슷한 이슈가 발생할 수 있습니다.

그러면 어떻게 해야하는가? 해당 로직들에서 list 를 사용하는 부분을 좀 더 속도가 빠르거나 하는 것들로 바뀌고 모든 데이터를 가져오는 부분은 사용하지 못하게 하는 것이 방법입니다.

어떤 분들은 이미 이런걸 겪으셔서 내부적으로 해당 모듈을 수정해서 쓰시는 분도 계셨습니다. 그러나 이런부분 때문에 관련 부분을 어느정도는 직접 구현해서 쓰시는 거나, 이런 부분을 수정해서 쓰셔야 할듯 합니다. 결론적으로 spring-security-oauth 의 RedisTokenStore를 쓰는 부분은 서비스 부하가 늘어날 때 장애를 일으킬 확률이 높습니다.

그 외에도 refreshToken의 경우는 보통 expire 기간이 훨씬 긴데, 이것들은 전부 메모리에 저장하는 부분들 또한 이슈가 생길 수 있습니다.(돈 많은면, 메모리 빵빵한 장비 쓰시면, 이 부분은 신경 안쓰셔도…)

보통 Spring이라는 이름을 붙이면 굉장히 안정적인데, 소스를 보고, 문제가 생길만한 부분에 대한 가정을 너무 약하게 하고 넘어간 부분이 문제인듯합니다.(하지만 해당 코드는 벌써 3년 전에 만들어진 코드라는 거…)

쉬시면서 Spring 코드 한번 읽어보시는 것도 좋을듯합니다.(전 읽어본 적 없습니다. ㅋㅋㅋ), 해당 글을 작성하는데 도움을 주신 우아한 형제들의 엯촋 이수홍 선생님께 감사드립니다.