[kuber-study] Chapter10. StatefulSets

jeonghyun yu·2022년 2월 7일
0

kuber-study

목록 보기
5/7
post-thumbnail

ReplicaSet을 사용한 stateful pod

  • ReplicaSet을 사용한 각 pod 복제본은 별도의 PersistentVolumeClaim을 사용할 수 없다.

ReplicaSet과 별도의 스토리지

  1. 수동으로 POD 생성

    : pod를 관리하는 replicaSet이 없어서 노드 장애가 발생하면 수동으로 관리해줘야한다.

  2. replicaSet당 POD 한 개

    : 각각의 pod가 replicaSet과 PVC를 가질 수 있지만, 여러 개의 replicaSet을 생성하는 것은 번거롭다.

  3. 동일한 볼륨에서 여러 개의 디렉토리 사용

    : pod 간의 디렉토리 조정이 필요, 공유되는 볼륨에 병목현상

각 pod에 대한 안정적인 IP

  • 각 POD에 대한 전용 service

    : 여러 개의 service를 생성하는 것 또한 번거롭다.



StatefulSets

StatefulSets과 ReplicaSets 비교

  • 애완동물 vs 가축

    • stateless pod = 가축
      인스턴스가 죽어도 새로운 pod로 대체되면 차이를 느끼지 못한다.
      ReplicaSet / ReplicationController는 가축처럼 pod를 관리

    • stateful pod = 애완동물
      동일한 상태의 인스턴스로 대체하지 않으면 안된다.
      StatefulSet은 안정적인 정체성을 갖는 애완동물처럼 pod를 관리

안정적인 네트워크 ID

  • StatefulSet에 의해 만들어지는 pod들은 제로기반 순서형 인덱스를 가진다.

  • GOVERNING HEADLESS SERVICE

    : 클러스터 IP가 할당되지 않고, StatefulSet에 있는 모든 pod에게 실제 네트워크 identity 제공. 로드밸런싱/프록시를 하지 않는다. pod에 자체 dns를 제공하여 각 파드에 접근할 수 있게 한다.

  • 동일한 호스트네임을 가진 pod로 교체

STATEFULSET 크기 조정

  • scale out : 다음 인덱스를 사용하여 새로운 pod 생성
  • scale in : 가장 높은 인덱스의 pod가 제거

  • 축소와 증가는 한 번에 한 pod 씩 점진적으로 수행

안정적인 전용 스토리지

  • PVC과 PV은 1:1 매핑되기 때문에 다른 PV을 사용하려면 다른 PVC을 참조해야한다.

볼륨 클레임 템플릿을 가진 POD 템플릿

  • StatefulSet은 pod 템플릿과 함께 PVC 템플릿을 생성

  • PERSISTENTVOLUMECLAIMS의 생성 및 삭제

    • 생성 : StatefulSet의 복제본 수를 늘리면 둘 이상의 API 객체 생성(pod와 하나 이상의 PVC)
    • 삭제 : 복제본 수를 줄이면 pod만 삭제되고 PVC는 유지. 수동 삭제
  • 동일한 새 인스턴스에 PERSISTENTVOLUMECLAIM을 다시 부착


  • 쿠버네티스가 pod의 상태를 잘못 확인하여 동일한 스토리지에 바인딩 되거나, 동일한 id를 가진 두 개의 인스턴스가 생성된다면?

최대 한개의 인스턴스

  • pod가 교체될 때 이전 pod가 정지되었음을 확인


StatefulSet 사용법

  • 앱 및 컨테이너 이미지 생성
    • app.js
...
const dataFile = "/var/data/kubia.txt";
...
var handler = function(request, response) {
  if (request.method == 'POST') {  // POST 요청
    var file = fs.createWriteStream(dataFile); 
    file.on('open', function (fd) { 
      request.pipe(file); 
      console.log("New data has been received and stored."); 
      response.writeHead(200); 
      response.end("Data stored on pod " + os.hostname() + "\n"); 
    });
  } else {  // GET 요청과 기타 요청들
    var data = fileExists(dataFile) 
      ? fs.readFileSync(dataFile, 'utf8') 
      : "No data posted yet"; 
    response.writeHead(200); 
    response.write("You've hit " + os.hostname() + "\n"); 
    response.end("Data stored on this pod: " + data + "\n"); 
  }
};

var www = http.createServer(handler);
www.listen(8080);
  • Dockerfile
FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]

StatefulSet을 사용한 앱 배포

  1. PersistentVolumes (동적 프로비저닝 미지원 시)
  2. Governing Service
  3. StatefulSet

PersistentVolume 생성

  • disk 생성

$ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-a
pv-b, pv-c도 생성

  • persistent-volumes-gcepd.yaml
kind: List            # 볼륨 3개를 작성
apiVersion: v1
items:
- apiVersion: v1
  kind: PersistentVolume 
  metadata:
    name: pv-a            # 볼륨이름
  spec:
    capacity:             # 용량
      storage: 1Mi 
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle  # 반환 정책
    gcePersistentDisk:    # gce 디스크 사용
      pdName: pv-a 
      fsType: nfs4 
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-b
 ...

GOVERNING SERVICE 생성

  • service-headless.yaml
apiVersion: v1
kind: Service
metadata:
  name: kubia 
spec:
  clusterIP: None  # headless service
  selector: 
    app: kubia 
  ports:
  - name: http
    port: 80

StatefulSet 생성

  • statefulset.yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia
  replicas: 2
  template:
    metadata:
      labels: 
        app: kubia 
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data 
          mountPath: /var/data  # 해당 경로에 pvc 볼륨 마운트
  volumeClaimTemplates:         # PVC 정의
  - metadata: 
      name: data 
    spec: 
      resources: 
        requests: 
          storage: 1Mi 
      accessModes: 
      - ReadWriteOnce 
  • stateful pod 확인
$ kubectl get po kubia-0 -o yaml
apiVersion: v1
kind: Pod
metadata:
  ...
spec:
  containers:
  - image: luksa/kubia-pet
    ...
    volumeMounts:
    - mountPath: /var/data  # pvc 볼륨 마운트 해준 경로
      name: data 
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-r2m41
      readOnly: true
  ...
  volumes:
  - name: data                # StatefulSet에서 생성한 볼륨
    persistentVolumeClaim:
      claimName: data-kubia-0 
  - name: default-token-r2m41
    secret:
      secretName: default-token-r2m41

pod와 통신

  • 현재 서비스는 headless라 개별 pod에 직접 연결해야한다.
    • pod 안에서(exec) curl 실행
    • port forwarding

API 서버를 pod에 대한 프록시로 사용

  • pod의 URL

<apiServerHost>:<port>/api/v1/namespaces/default/pods/kubia-0/proxy/<path>

  • 프록시를 사용해 pod에 request
$ kubectl proxy
Starting to serve on 127.0.0.1:8001

# GET
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/

# POST
$ curl -X POST -d "Hey there! This greeting was submitted to kubia-0."\
➥ localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/

STATEFUL POD 삭제

$ kubectl delete po kubia-0

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
Data stored on this pod: Hey there! This greeting was submitted to kubia-0.

일반 서비스와 API 서버를 사용한 stateful pod 연결

  • service.yaml
apiVersion: v1
kind: Service
metadata:
  name: kubia-public
spec:
  selector:
    app: kubia
  ports:
  - port: 80
    targetPort: 8080
  • 서비스 URL

/api/v1/namespaces/<namespace>/services/<service name>/proxy/<path>

❗ 서비스를 통한 요청은 매번 랜덤한 클러스터 노드로 도달한다.



SRV 레코드

: 특정 서비스를 제공하는 서버를 식별. 서버의 hostname과 port를 가리키는 데 사용

# SRV DNS lookup
$ kubectl run -it srvlookup --image=tutum/dnsutils --rm 
➥ --restart=Never -- dig SRV kubia.default.svc.cluster.local
  • --restart=Never : 일회성
  • -it : 쉘에 연결
  • --rm : 종료 시 삭제
  • dig SRV kubia.default.svc.cluster.local 명령 실행
# 명령어 출력 결과
...
;; ANSWER SECTION:
kubia.default.svc.cluster.local. 30 IN SRV    10 33 0 kubia-0.kubia.default.svc.cluster.local.
kubia.default.svc.cluster.local. 30 IN SRV    10 33 0 kubia-1.kubia.default.svc.cluster.local.

;; ADDITIONAL SECTION:
kubia-0.kubia.default.svc.cluster.local. 30 IN A 172.17.0.4
kubia-1.kubia.default.svc.cluster.local. 30 IN A 172.17.0.6
...
  • Answer : headless 서비스의 pod들을 가리키는 SRV 레코드
  • Additional : 자체 A 레코드

DNS를 통한 peer 검색

  • 일반 서비스를 통해 게시한 데이터는 랜덤한 클러스터 노드에 저장된다. 모든 클러스터 노드를 확인하려면 모든 peer를 찾아야 하는데, 이 때 SRV 레코드 사용

  • app.js

...
const dns = require('dns');
const dataFile = "/var/data/kubia.txt";
const serviceName = "kubia.default.svc.cluster.local";
const port = 8080;
...
var handler = function(request, response) {
  if (request.method == 'POST') {
    ...
  } else {
    response.writeHead(200);
    if (request.url == '/data') {
      var data = fileExists(dataFile) 
        ? fs.readFileSync(dataFile, 'utf8') 
        : "No data posted yet";
      response.end(data);
    } else {
      response.write("You've hit " + os.hostname() + "\n");
      response.write("Data stored in the cluster:\n");
      dns.resolveSrv(serviceName, function (err, addresses) {  # DNS lookup
        if (err) {
          response.end("Could not look up DNS SRV records: " + err);
          return;
        }
        var numResponses = 0;
        if (addresses.length == 0) {
          response.end("No peers discovered.");
        } else {   # SRV 레코드를 얻었다면 사용해서 pod 지정
          addresses.forEach(function (item) { 
            var requestOptions = {
              host: item.name, 
              port: port, 
              path: '/data'
            };
            httpGet(requestOptions, function (returnedData) { 
              numResponses++;
              response.write("- " + item.name + ": " + returnedData);
              response.write("\n");
              if (numResponses == addresses.length) {
                response.end();
              }
...
  • test
$ curl localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/
You've hit kubia-2
Data stored on each cluster node:
- kubia-0.kubia.default.svc.cluster.local: The weather is sweet
- kubia-1.kubia.default.svc.cluster.local: The sun is shining
- kubia-2.kubia.default.svc.cluster.local: No data posted yet

이제 클라이언트 요청이 클러스터 노드 중 하나에 도달하면 모든 peer에서 데이터를 수집해서 클라이언트로 응답한다.

StatefulSet 업데이트

# StatefulSet 편집기 실행
$ kubectl edit statefulset kubia

spec.replicas와 spec.template.spec.containers.image 수정

# pod 복제본 확인 - 기존 pod는 업데이트x
$ kubectl get po
NAME    READY STATUS            RESTARTS AGE
kubia-0 1/1   Running           0        25m
kubia-1 1/1   Running           0        26m
kubia-2 0/1   ContainerCreating 0        4s

# pod 수동 삭제 후 재생성
$ kubectl delete po kubia-0 kubia-1

➕ 쿠버네티스 1.7부터는 StatefulSet도 롤링 업데이트 지원



StatefulSet의 노드 장애 처리

쿠버네티스는 statefule pod를 대체하기 전에 정말 pod가 실행되지 않고 있는지 확인해야한다. 그러나 노드가 갑자기 고장나면 쿠버네티스는 노드나 pod의 상태를 알 수 없다. 그럼 어떻게 확인해야 할까?

노드의 네트워크 연결이 끊기면?

# 노드의 네트워크 어댑터 종료
$ gcloud compute ssh gke-kubia-default-pool-32a2cac8-m0g1
$ sudo ifconfig eth0 down

# node 상태 확인
$ kubectl get node
NAME                                 STATUS   AGE VERSION
gke-kubia-default-pool-32a2cac8-596v Ready    16m v1.6.2
gke-kubia-default-pool-32a2cac8-m0g1 NotReady 16m v1.6.2

# pod 상태 확인
$ kubectl get po
NAME    READY STATUS  RESTARTS AGE
kubia-0 1/1   Unknown 0        15m
kubia-1 1/1   Running 0        14m
  • 상태를 알 수 없는 포드는 어떻게 될까?

노드가 지정 시간 내에(default 5min) 다시 온라인 상태가 되면 pod는 다시 running 상태가 된다. 시간을 초과한다면 마스터 노드에서 pod 리소스를 자동으로 종료 처리한다. $ kubectl describe po 확인
원래는 마스터 노드를 확인해서 kubelet이 pod를 종료해야하지만 연결이 안되어서 pod는 계속 실행되고있을 것이다.

수동으로 pod 삭제

# 일반적인 방법으로 pod 삭제
$ kubectl delete po kubia-0

$ kubectl get po
NAME    READY STATUS  RESTARTS AGE
kubia-0 1/1   Unknown 0        15m
kubia-1 1/1   Running 0        14m
  • POD가 삭제되지 않는 이유

pod가 삭제되려면 노드의 kubelet이 컨테이너가 종료되었음을 API 서버에게 알려야 하는데 네트워크가 끊겼으니 불가능해졌습니다.

POD 강제 삭제

$ kubectl delete po kubia-0 --force --grace-period 0


  • 노드를 온라인 상태로 변경

$ gcloud compute instances reset <node name>


출처
Kubernetes in Action

Q. headless 서비스를 생성할 때 작성해야하는 속성은 무엇일까요? 그리고 헤드리스 서비스가 무엇인지 간단하게 설명해주세요.

Q. pod가 실행되고 있는 node의 상태가 NotReady로 변하였습니다. 그 후로 5분 이상이 지나고도 node에 연결이 되지 않습니다. 이럴 때 어떤 방식으로 pod를 다시 재가동 시킬 수 있을까요? (pod-eviction-timeout 따로 설정한 적 없음)

0개의 댓글