Azure Cloud App 개발 - 개인 연구 프로젝트 실습 및 검증 사례 (e-Charging System)

Updated:


Azure Cloud App 개발 - 개인 연구 Project 실습 및 검증 사례

Azure Cloud App 개발 Intensive Course 에서 제가 직접 수행했던 개인 연구 프로젝트로서 전기자동차 충전 서비스의 간략한 핵심 시나리오를 기반으로한 바운디드 컨텍스트 다이어그램과 기능적 요구사항을 검증해본 실습 사례를 공유하고, Saga (Event/Chreography 패턴 적용), CQRS, 동기식 호출(Req/Resp), Deployment, Circuit Breaker와 Fallback 처리, Autoscale Out (HPA), Zero-Downtime deploy(Readiness Probe), ConfigMap, Polyglot Persistence, Self-healing(Liveness Probe) 등 12가지 MSA/Cloud 기술요소 중심으로 구현 및 테스트 실습 데이타로 검증을 확인하는 주요내용 및 사례들을 살펴보겠습니다.

전기자동차 충전관리 시스템 (e-charging)

image


서비스 시나리오


기능적 요구사항

  • 전기차 충전소 관리자는 전기충전기를 등록한다.
  • 고객은 전기차 충전소를 필요한 시간대에 예약한다.
  • 고객은 전기 충전 예약을 취소 할 수 있다.
  • 전기차 충전 예약이 된 같은 시간대에는 충전 예약을 할 수 없다.
  • 예약된 고객이 충전소에 방문하여 충전을 시작하면, 해당 충전기는 충전중 상태로 된다.
  • 고객이 충전을 완료 하면, 충전 완료 상태가 된다.
  • 고객은 예약 및 충전 상태 정보를 확인 할 수 있다.



비기능적 요구사항

  • 트랜잭션
    • 전기차 충전 예약이 된 같은 시간대에는 충전 예약을 할 수 없다. (Sync 호출)
  • 장애격리
    • 전기 충전 기능이 수행되지 않더라도 충전 예약은 365일 24시간 받을 수 있어야 한다. (Async (event-driven), Eventual Consistency)
    • 예약시스템이 과중 되면 사용자를 잠시동안 받지 않고 예약을 잠시후에 하도록 유도한다. (Circuit breaker, fallback)
  • 성능
    • 고객은 MyPage에서 본인 예약 및 충전 상태를 확인 할 수 있어야 한다. (CQRS - 조회전용 서비스)


분석/설계

  1. 이벤트를 식별하여 타임라인으로 배치, 부적격 이벤트 제거함
  2. Event를 발생시키는 Command와 발생시키는주체(Actor)를 식별함
  3. 연관있는도메인 이벤트들을 Aggregate로 묶음
  4. 바운디드 컨텍스트로 묶음
  5. 폴리시 부착/이동 및 바운디드 컨텍스트에 매핑함


Event Storming 결과


image

MSA-EZ 도구를 활용하여 이벤트 스토밍 및 설계/코드 Download

image


헥사고날 아키텍처 다이어그램 도출

image


구현

분석/설계 단계에서 도출된 헥사고날 아키텍처에 따라,구현한 각 서비스를 로컬에서 실행하는 방법은 아래와 같습니다. (서비스별 각각의 포트넘버는 8081 ~ 8084, 8088)

cd echarger
mvn spring-boot:run

cd reservation
mvn spring-boot:run 

cd echarging
mvn spring-boot:run 

cd mypage
mvn spring-boot:run

cd gateway
mvn spring-boot:run 


기능적 요구사항 검증

1) 전기차 충전소 관리자는 전기충전기를 등록한다.

image

2) 고객은 전기차 충전소를 필요한 시간대에 예약한다.

image

3) 고객은 전기 충전 예약을 취소 할 수 있다.

image

4) 전기차 충전 예약이 된 같은 시간대에는 충전 예약을 할 수 없다. ( 해당시간대에 예약되어 있지 않으면 예약 가능)

image

5) 예약된 고객이 충전소에 방문하여 충전을 시작하면, 해당 충전기는 충전중 상태로 된다. (충전중 상태 : CHARGING_STARTED)

image

6) 고객이 충전을 완료 하면, 충전 완료 상태가 된다. (충전완료 상태 : CHARGING_ENDED)

image

7) 고객은 예약 및 충전 상태 정보를 확인 할 수 있다.

image


Saga (Event/Chreography 패턴 적용)

Choreography 방식은 Event 방식으로 비동기로 작동하는 방식입니다. 각각의 서비스는 이벤트를 발행하고, 트리거링 하여 개별적으로 동작을 하는 방식입니다. 각 서비스마다 자신의 트랜잭션을 관리하며 현재 상태를 변경한 후 이벤트를 발생시키고, 그 이벤트를 다른 서비스에 전달하는 방식으로 구현합니다. (Publish/Subscribe)

(테스트 실습 Data)
충전예약(Reservation, Pub), 충전(echarging, Sub)
http POST http://52.231.156.9:8080/reservations chargerId=1 rsrvTimeAm=Y userId=1
http POST http://52.231.156.9:8080/reservations chargerId=1 rsrvTimePm=Y userId=2
http GET http://52.231.156.9:8080/echargings
http GET http://52.231.156.9:8080/mypages
http PATCH http://52.231.156.9:8080/echargings/1 amount=10000 (충전시작)
http GET http://52.231.156.9:8080/echargings
http PATCH http://52.231.156.9:8080/echargings/1 amount=9000 (충전종료)
http GET http://52.231.156.9:8080/mypages

[Publish]

image

[Subscribe]

image


CQRS (Command and Query Responsibility Segregation)

CQRS는 네이밍에서 알 수 있듯이 명령과 쿼리의 역할을 구분 한다는 것입니다. 즉 커맨드 (Create - Insert, Update, Delete : 데이터를 변경) 와 쿼리 (Select - Read : 데이터를 조회)의 책임을 분리한다는 것입니다. 비지니스 로직은 대부분 데이터 변경(CUD) 에서 처리되고, 조회(Read) 은 단순 데이터 조회가 대부분이 되는것을 볼 수 있었습니다. 이것을 하나의 Domain Model 에서 처리하게 되니, 필요치 않은 외부 속성들과의 연계등의 복잡도가 증가하는 문제를 해결하기 위하여 명령과 조회를 분리하는 방법을 고안하였고 이렇게 나온 방법이 CQRS 입니다.
타 마이크로서비스의 데이터 원본에 접근없이(Composite 서비스나 조인SQL 등 없이)도 내 서비스의 충전 예약 신청 내역 조회가 가능하게 구현하였습니다. 본 프로젝트에서 View 역할은 mypage 서비스가 수행합니다.

[충전 예약 신청 후 mypage 조회]

image

[충전 종료후 mypage 조회]

image


Correlation

각 이벤트 건(메시지)이 어떤 Policy를 처리할 때 어떤건에 연결된 처리건인지를 구별하기 위한 Correlation-key를 제대로 연결하였는지를 검증합니다.

image


동기식 호출(Req/Resp)

전기차 충전예약(reservation) -> 충전소(echarger) 예약 가능 Check 간의 호출은 동기식 일관성을 유지하는 트랜잭션으로 처리했으며. 호출 프로토콜은 RestController를 FeignClient를 이용하여 호출하였습니다.

(테스트 실습 Data)
충전예약(Reservation) --> 충전소(echarger) 예약가능 체크합니다.
http POST http://52.231.156.9:8080/reservations chargerId=3 rsrvTimeAm=Y userId=1
http GET http://52.231.156.9:8080/echargers
http POST http://52.231.156.9:8080/reservations chargerId=2 rsrvTimeAm=Y userId=1

(Reservation) EchargerService.java

package echarging.external;

@FeignClient(name="echarger", url="${api.url.echarger}", fallback = EchargerServiceFallback.class)
public interface EchargerService {

    @RequestMapping(method= RequestMethod.GET, path="/echargers/chkAndRsrvTime")  
    public boolean chkAndRsrvTime(@RequestParam Long chargerId);

}

충전 예약을 받은 직후 충전소 예약가능 확인을 요청하도록 처리합니다. (Reservation) EchargerController.java

package echarging;

@RestController
public class EchargerController {

    @Autowired
    EchargerRepository echargerRepository;

    @RequestMapping(value = "/echargers/chkAndRsrvTime",
        method = RequestMethod.GET,
        produces = "application/json;charset=UTF-8")

    public boolean chkAndRsrvTime(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("##### /echargers/chkAndRsrvTime  called #####");

        boolean status = false;

        Long echargerId = Long.valueOf(request.getParameter("chargerId"));

        Optional<Echarger> echarger = echargerRepository.findById(echargerId);
        if(echarger.isPresent()) {
            Echarger echargerValue = echarger.get();

            //Hystrix Timeout 점검
            if(echargerValue.getChargerId() == 2) {
                System.out.println("### Hystrix 테스트를 위한 강제 sleep 5초 ###");
                Thread.sleep(5000);
            }
            //예약 가능한지 체크
            if(echargerValue.getRsrvTimeAm() == null || echargerValue.getRsrvTimePm() == null) {
                status = true;

                //예약 가능하면 예약할 시간대 선택/저장
                if(echargerValue.getRsrvTimeAm() == null){
                        echargerValue.setRsrvTimeAm("Y");
                }else if(echargerValue.getRsrvTimePm() == null){
                        echargerValue.setRsrvTimePm("Y");
                }    

                echargerRepository.save(echargerValue);
            }
        }

        return status;
    }
}

동기식 호출에서는 호출 시간에 따른 타임 커플링이 발생하며, 충전소 관리 시스템이 장애가 나면 충전 예약도 못 받는다는 것을 확인:

충전소 관리(echarger) 서비스를 잠시 내려놓음 (ctrl+c)

충전예약처리

http POST localhost:8088/reservations chargerId=1 rsrvTimeAm=Y userId=1   #Fail
http POST localhost:8088/reservations chargerId=2 rsrvTimePm=Y userId=1   #Fail

충전소 관리 서비스 재기동

cd echarger
mvn spring-boot:run

충전예약처리

http POST localhost:8088/reservations chargerId=1 rsrvTimeAm=Y userId=1   # Success
http POST localhost:8088/reservations chargerId=2 rsrvTimePm=Y userId=1   # Success

운영단계에서는 Circuit Breaker를 이용하여 충전소 관리 시스템에 장애가 발생하여도 충전예약 접수는 가능하도록 하였습니다.


Gateway

API Gateway를 통하여 마이크로 서비스들의 진입점을 통일할 수 있습니다. 다음과 같이 Gateway를 적용하여 모든 마이크로서비스들은 http://localhost:8088/{context}로 접근할 수 있습니다.

server:
  port: 8088

---

spring:
  profiles: default
  cloud:
    gateway:
      routes:
        - id: echarger
          uri: http://localhost:8081
          predicates:
            - Path=/echargers/**
        - id: reservation
          uri: http://localhost:8082
          predicates:
            - Path=/reservations/** 
        - id: echarging
          uri: http://localhost:8083
          predicates:
            - Path=/echargings/** 
        - id: mypage
          uri: http://localhost:8084
          predicates:
            - Path= /mypages/**
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins:
              - "*"
            allowedMethods:
              - "*"
            allowedHeaders:
              - "*"
            allowCredentials: true

---

spring:
  profiles: docker
  cloud:
    gateway:
      routes:
        - id: echarger
          uri: http://echarger:8080
          predicates:
            - Path=/echargers/** 
        - id: reservation
          uri: http://reservation:8080
          predicates:
            - Path=/reservations/** 
        - id: echarging
          uri: http://echarging:8080
          predicates:
            - Path=/echargings/** 
        - id: mypage
          uri: http://mypage:8080
          predicates:
            - Path= /mypages/**
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins:
              - "*"
            allowedMethods:
              - "*"
            allowedHeaders:
              - "*"
            allowCredentials: true

server:
  port: 8080


Deployment


git에서 소스 가져오기

git clone https://github.com/jameshan0317/e-charging.git

Build 하기

cd /echarger
mvn package

cd /reservation
mvn package

cd /echarging
mvn package

cd /mypage
mvn package

cd /gateway
mvn package

Azure 레지스트리에 Docker Image Build/Push, deploy/service 생성 (yml 이용)

namespace 생성

kubectl create ns e-charging

reservation 서비스는 무정지 재배포 테스트를 위해 v1, latest 두 개 버전 build,push

cd reservation
az acr build --registry jameshan055 --image jameshan055.azurecr.io/reservation:v1 .
az acr build --registry jameshan055 --image jameshan055.azurecr.io/reservation:latest .

cd kubernetes
kubectl apply -f deployment.yml -n e-charging
kubectl apply -f service.yaml -n e-charging
cd echarger
az acr build --registry jameshan055 --image jameshan055.azurecr.io/echarger:latest . 

cd kubernetes
kubectl apply -f deployment.yml -n e-charging
kubectl apply -f service.yaml -n e-charging
cd echarging
az acr build --registry jameshan055 --image jameshan055.azurecr.io/echarging:latest .

cd kubernetes
kubectl apply -f deployment.yml -n e-charging
kubectl apply -f service.yaml -n e-charging
cd mypage
az acr build --registry jameshan055 --image jameshan055.azurecr.io/mypage:latest . 

cd kubernetes
kubectl apply -f deployment.yml -n e-charging
kubectl apply -f service.yaml -n e-charging
cd gateway
az acr build --registry jameshan055 --image jameshan055.azurecr.io/gateway:latest . 

cd kubernetes
kubectl apply -f deployment.yml -n e-charging
kubectl apply -f service.yaml -n e-charging

yml 파일 이용한 deploy (e-charging/reservation/kubernetes/deployment.yml 파일)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: reservation
  namespace: e-charging
  labels:
    app: reservation
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reservation
  template:
    metadata:
      labels:
        app: reservation
    spec:
      containers:
        - name: reservation
          image: jameshan055.azurecr.io/reservation:latest
          ports:
            - containerPort: 8080
          env:
            - name: echarger-url
              valueFrom:
                configMapKeyRef:
                  name: echargerurl
                  key: url
          readinessProbe:
            httpGet:
              path: '/actuator/health'
              port: 8080
            initialDelaySeconds: 10
            timeoutSeconds: 2
            periodSeconds: 5
            failureThreshold: 10
          livenessProbe:
            httpGet:
              path: '/actuator/health'
              port: 8080
            initialDelaySeconds: 120
            timeoutSeconds: 2
            periodSeconds: 5
            failureThreshold: 5
          resources:
            requests:
              memory: "64Mi"
              cpu: "250m"
            limits:
              memory: "500Mi"
              cpu: "500m" 

(e-charging/reservation/kubernetes/service.yml 파일)

apiVersion: v1
kind: Service
metadata:
  name: reservation
  namespace: e-charging
  labels:
    app: reservation
spec:
  ports:
    - port: 8080
      targetPort: 8080
  selector:
    app: reservation

deploy 완료

image


Circuit Breaker와 Fallback 처리

Request/Response 통신은 성능저하와 장애 전파를 회피 하기 위한 전략을 새워야 합니다. 서킷브레이커를 통하여 장애 전파를 원천 차단 할 수 있습니다. 서킷브레이커와 retry 를 같이 사용하여 서비스의 resilience (탄력성) 을 높일 수 있습니다.

서킷브레이커 패턴은 회로 차단기에서 차용한 개념으로 평소 (close) 상태에서는 정상적으로 작동하다가 문제가 생기면 회로를 open 하여 더이상 전기가 흐르지 않게 하는 방식입니다. 즉 문제가 되는 기능 자체를 동작하지 않게 하여 리소스를 점유 하지 않도록 하는 방법입니다.

요청/응답 통신중에 특정 서비스가 응답시간이 느려진다거나, 오류가 특정치 이상 발생할때 서킷브래이커를 적용하여 빠르게 차단을 시켜버립니다.

본 프로젝트에서는 Spring FeignClient + Hystrix 를 사용하여 구현합니다.

시나리오는 예약(Reservation)–>충전소(echarger) 확인 시 예약 요청에 대한 충전소 예약가능시간대 확인이 3초를 넘어설 경우 Circuit Breaker 를 통하여 장애를 격리시킵니다.

Hystrix 를 설정: FeignClient 요청처리에서 처리시간이 3초가 넘어서면 CB가 동작하도록 (요청을 빠르게 실패처리, 차단) 설정 추가로, 테스트를 위해 1번만 timeout이 발생해도 CB가 발생하도록 설정합니다.

(테스트 실습 Data)
충전예약(Reservation) --> 충전소(echarger) (처리시간이 3초가 넘어서면 CB Open)
http POST http://52.231.156.9:8080/reservations chargerId=2 rsrvTimeAm=Y userId=1

(application.yml)

image

호출 서비스(예약)에서는 충전소API 호출에서 문제 발생 시 예약건을 Out of available Time 처리하도록 FallBack 구현합니다.

(Reservation) EchargerService.java

package echarging.external;
 ...
@FeignClient(name="echarger", url="${api.url.echarger}", fallback = EchargerServiceFallback.class)
public interface EchargerService {

    @RequestMapping(method= RequestMethod.GET, path="/echargers/chkAndRsrvTime")  
    public boolean chkAndRsrvTime(@RequestParam Long chargerId);

}

(Reservation) EchargerServiceFallBack.java

package echarging.external;
  ...
@Component
public class EchargerServiceFallback implements EchargerService {
    @Override
    public boolean chkAndRsrvTime(@RequestParam Long chargerId) {
        System.out.println("Circuit breaker has been opened. Fallback returned instead.");
        return false;
    }  
}

(Reservation) Reservation.java

image

피호출 서비스(충전소 : echarger )에서 테스트를 위해 chargerId가 2인 예약건에 대해 sleep 처리됩니다.

(Echarger) EchargerController.java

image

서킷 브레이커 동작 확인: chargerId가 1번 인 경우 정상적으로 충전예약 처리 완료합니다.

http POST http://52.231.156.9:8080/reservations chargerId=1 rsrvTimeAm=Y userId=3993

image

chargerId가 2번 인 경우 CB에 의한 timeout 발생 확인합니다. (예약건은 Out of available Time 처리됩니다) image

일정시간 뒤에는 다시 주문이 정상적으로 수행되는 것을 알 수 있습니다. image

운영시스템은 죽지 않고 지속적으로 CB 에 의하여 적절히 회로가 열림과 닫힘이 벌어지면서 Thread 자원 등을 보호하고 있음을 증명합니다.

Autoscale Out (HPA)

충전소 서비스가 몰릴 경우를 대비하여 자동화된 확장 기능을 적용하였습니다.

충전소 서비스에 리소스에 대한 사용량을 정의합니다. (echarger/kubernetes/deployment.yml)

    resources:
            requests:
              memory: "64Mi"
              cpu: "250m"
            limits:
              memory: "500Mi"
              cpu: "500m"

충전소 서비스에 대한 replica를 동적으로 늘려주도록 HPA를 설정한다. 설정은 CPU 사용량이 15%를 넘어서면 replica를 3개까지 늘려줍니다.

kubectl autoscale deploy echarger --min=1 --max=3 --cpu-percent=15 -n e-charging

워크로드를 100명이 2분 동안 걸어줍니다.

kubectl exec -it pod/siege -c siege -n e-charging -- /bin/bash

# siege -c100 -t120S -r10 -v --content-type "application/json" 'http://52.231.156.9:8080/echargers POST {"cgName": "이마트충전소"}'

오토스케일 확인을 위해 모니터링을 걸어둡니다.

watch kubectl get all -n e-charging

잠시 후 echarger에 대해 스케일 아웃이 발생하는 것을 확인할 수 있습니다. image


Zero-Downtime deploy - Readiness Probe

deployment.yml에 정상 적용되어 있는 readinessProbe

image

readiness 설정 제거한 yml 파일로 echarger deploy 다시 생성 후, siege 부하 테스트 실행해둔 뒤 재배포를 진행합니다.

siege 테스트

kubectl exec -it pod/siege -c siege -n e-charging -- /bin/bash

# siege -c100 -t120S -r10 -v --content-type "application/json" 'http://52.231.156.9:8080/echargers POST {"cgName": "이마트충전소999"}'

echarger 새버전으로의 배포 시작 (두 개 버전으로 버전 바꿔가면서 테스트)

cd echarger/kubernetes/
(Readiness 설정을 뺀 파일)
kubectl apply -f deployment_test_readiness.yml -n e-charging
(Readiness 설정 파일) 
kubectl apply -f deployment.yml -n e-charging

새 버전으로 배포되는 중 (구버전, 신버전 공존) image

image

배포기간중 Availability 가 100%가 안 되는 것을 확인. 원인은 쿠버네티스가 성급하게 새로 올려진 서비스를 READY 상태로 인식하여 서비스 유입을 진행한 것이기 때문. 이를 막기 위해 Readiness Probe 를 설정합니다.

image

image

다시 readiness 정상 적용 후(deployment.yml), Availability 100% 확인

image


ConfigMap

시스템별로 변경 가능성이 있는 설정들을 ConfigMap을 사용하여 관리합니다.

e-charging시스템에서는 충전예약 서비스에서 충전소 서비스의 예약가능 check 호출 시 “호출 주소”를 ConfigMap 처리하였습니다.

Java 소스에 “호출 주소”를 변수(api.url.echarger)처리합니다. (/reservation/src/main/java/echarging/external/EchargerService.java)

package echarging.external;
 ...
@FeignClient(name="echarger", url="${api.url.echarger}", fallback = EchargerServiceFallback.class)
public interface EchargerService {

    @RequestMapping(method= RequestMethod.GET, path="/echargers/chkAndRsrvTime")  
    public boolean chkAndRsrvTime(@RequestParam Long chargerId);
}

application.yml 파일에서 api.url.echarger를 ConfigMap과 연결

reservation application.yml (reservation/src/main/resources/application.yml) image

reservation deployment.yml 에 적용 (reservation/kubernetes/deployment.yml) image

ConfigMap 생성 후 조회

kubectl create configmap echargerurl --from-literal=url=http://echarger:8080 -n e-charging
kubectl get configmap echargerurl -o yaml -n e-charging

image

reservation pod 내부로 들어가서 환경변수도 확인

kubectl exec -it pod/reservation-669bf984fc-88qxq -n e-charging -- /bin/sh
# env

image


Polyglot Persistence

mypage 서비스의 DB와 echarger/reservation/echarging 서비스의 DB를 다른 DB를 사용하여 MSA간 서로 다른 종류의 DB간에도 문제 없이 동작하여 다형성을 만족하는지 확인하였습니다. (Polyglot을 만족)

서비스 DB pom.xml
echarger H2 image
reservation H2 image
echarging H2 image
mypage HSQL image


Self-healing (Liveness Probe)

deployment.yml에 정상 적용되어 있는 livenessProbe

image

Self-healing 확인을 위한 Liveness Probe 옵션 변경 (Port 변경) 설정해놓은 deploy로 다시 배포 후, retry 시도 확인 (echarger서비스)

deployment_Test_liveness.yml

image

Liveness 확인 실패에 따른 retry발생 확인

(테스트 실습 Data)
kubectl apply -f deployment_Test_liveness.yml

image

이상으로 12가지 MSA/Cloud App.개발 기술요소에 대한 구현 및 검증 완료되었음 확인하였습니다.