04. SOLID: 코드의 유지보수성을 판단할 때 사용할 수 있는 맥락 (영향 범위 / 의존성 / 확장성)

  • SRP / 단일책임원칙: 한 클래스에 너무 많은 책임이 할당돼서는 안 되며, 단 하나의 책임만 있어야 한다. 클래스는 하나의 책임만 가지고 있을 때 변경이 쉬워진다. = 클래스를 변경해야 할 이유는 단 하나여야 한다.
    • 복잡하고 과한 책임이 할당된 클래스는 코드를 변경하려고 할 때 문제가 된다. 영향 범위를 알 수 없으니 코드 변경이 어려워진다. -> 과하게 집중된 책임은 피하고, 분할해야 한다.
    • 책임: 액터에 대한 책임; 문맥을 포함하는 개념이며, 개인이나 상황마다 다르게 해석될 여지가 있다.
      • 액터 actor: 메세지를 전달하는 주체로, 하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다.
      • 어떤 모듈이나 클래스가 담당하는 액터가 혼자라면 단일 책임 원칙을 지키고 있는 것이며, 어떤 모듈이나 클래스가 담당하는 액터가 여럿이라면 단일 책임 원칙에 위배되는 것이다.
  • Open-Closed Principle / 개방 폐쇄 원칙: 확장에는 열려있고, 변경에는 닫혀 있어야 한다. -> 코드를 추상화된 역할에 의존하게 만든다. = 클래스의 동작을 수정하지 않고 확장할 수 있어야 한다.
  • Liskov Subsitution Principle / 리스코프 치환 원칙: 기본 클래스의 계약을 파생 클래스가 제대로 치환할 수 있는지 확인한다. = 파생 클래스는 기본 클래스의 모든 동작을 완전히 대체할 수 있어야 한다. -> 기본 클래스에 할당된 의도와 계약이 무엇인지 파악할 수 있어야 하며, 코드 외에 테스트 코드를 사용하여 코드 작성자의 의도를 드러낸다.
  • Interface Segregation Principle / 인터페이스 분리 원칙: 클라이언트가 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다. 즉, 어떤 클래스가 자신에게 필요하지 않은 인터페이스의 메서드를 구현하거나 의존하지 않아야 한다. -> 클라이언트별로 세분화된 인터페이스를 만든다. / 범용성을 갖춘 하나의 인터페이스를 만들기보다 다수의 특화된 인터페이스를 만드는 편이 낫다.
    • 인터페이스(역할)가 통합되면 인터페이스의 역할이 모호해지고, 여러 액터들을 상대해야 한다; 인터페이스 분리가 제대로 지켜지지 않은 코드는 단일 책임 원칙도 위배하고 있을 확률이 높다.
    • 인터페이스를 통합하고 한 곳으로 모으는 것은 응집도가 높아지는 것인가? = 응집도는 유사한 코드를 한 곳에 모은다 에서 끝나는 것이 아님; 위로갈수록 응집도가 높다고 평가함
      • 기능적 응집도: 모듈 내 컴포넌트들이 같은 기능을 수행하도록 설계된 경우 = 역할과 책임 측면에서 바라보는 응집도
      • 순차적 응집도: 모듈 내 컴포넌트들이 특정한 작업을 수행하기 위해 순차적으로 연결된 경우
      • 통신적 응집도: 모듈 내 컴포넌트들이 같은 데이터나 정보를 공유하고, 상호작용할 때 이에 따라 모듈을 구성하는 경우
      • 절차적 응집도: 모듈 내 요소들이 단계별 절차를 따라 동작하도록 설계된 경우
      • 논리적 응집도: 모듈 내 요소들이 같은 목적을 달성하기 위해 논리적으로 연관된 경우 = 유사한 코드라서 한 곳에 모은다
  • Dependency Inversion Principle / 의존성 역전 원칙: 상위 모듈은 하위 모듈에 의존해서는 안 된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야하며, 추상화는 세부 사항에 의존해서는 안 된다. 세부사항이 추상화에 의존해야 한다. = 구체화가 아니라 추상화에 의존해야 한다.
    • 의존성: 어떤 객체가 다른 코드를 사용하는 상태 = 결합(coupling)
    • 의존성 주입: 필요한 의존성을 외부에서 넣어주는 것 = 의존성과 결합도를 낮추기 위한 기법 중 하나 -> new 사용을 자제한다. = 상세한 구현 객체에 의존하는 것을 피하고, 구현 객체가 인스턴스화 되는 시점을 최대한 뒤로 미룬다.
    • 의존성 역전: 의존의 방향이 바뀌는 것 = 원래 의존 당하던 객체가 의존을 하는 객체로 바꾸는 것 -> 추상화를 이용한 간접 의존 형태로 바꾼다.
    • 의존성 전이: 한 컴포넌트가 변경되거나 영향을 받으면 관련된 다른 컴포넌트에도 영향이 갈 수 있다. 이렇게 영향을 받은 컴포넌트는 연쇄적으로 또 다른 관련 컴포넌트에 영향을 준다 -> 의존성 전이의 영향 범위를 축소하기 위해 추상화를 적용한다. = 의존성 역전을 적용한다.

 

05. 순환참조: 두 개 이상의 객체나 컴포넌트가 서로를 참조함으로써 의존 관계에 사이클이 생기는 상황 (ex. JPA의 양방향 매핑)

  • 시스템에 무한 루프가 생길 수 있다.
  • 시스템의 복잡도를 높인다.
  • 메모리 누수를 유발할 수 있다.

-> 필요하지 않은 참조는 제거하여 가능한 한 도메인 모델들에 단일 진입점을 만들어 단방향으로 접근하도록 한다.

-> 만약 관계를 표현해야 한다면 한 쪽이 다른 한 쪽의 식별자를 갖고 있게 하여 간접 참조 형태로 관계를 바꾼다.

-> 양쪽 서비스에 있던 공통 기능을 하나의 컴포넌트로 분리하고, 양쪽 서비스가 공통 컴포넌트에 의존하도록 바꾼다.

-> 이벤트 기반 프로그래밍을 적용한다. = 이벤트와 이벤트 큐에 의존한다.

 

 

2부. 스프링과 객체지향 설계

06. 안티패턴

  • 양방향 레이어드 아키텍쳐(=순환 참조): 하위 레이어드 컴포넌트가 상위 레이어에 존재하는 모델을 이용하는 경우
    • ex. Controller(프레젠테이션 레이어)에서 요청받은 PostCreateRequest를 PostService(비즈니스 레이어)에서 사용하는 경우
    • 해결1) 레이어별 모델 구성: PostCreateCommand와 같은 모델을 추가로 만든다. 각 레이어의 관심사를 명확히 분리할 수 있지만, 작성해야 하는 코드의 양이 늘어난다는 단점이 있다.
    • 해결2) 공통 모듈 구성: 순환참조의 원인이 되는 PostCreateRequest를 별도의 공통 모듈로 분리한다. 레이어별 모델 구성에 비해 코드 중복을 줄일 수 있으나, 레이어 간 결합도가 여전히 존재하므로 공통 모듈의 변경이 여러 레이어에 영향을 줄 수 있다. 
  • 트랜잭션 스크립트: 서비스 컴포넌트에서 발생하는 안티패턴으로, 서비스 컴포넌트의 동작이 사실상 트랜잭션이 걸려있는   거대한 스크립트를 실행하는 것처럼 보이는 경우; 객체지향보다 절차지향에 가까운 사례
    • 비즈니스 로직이 처리되는 주 영역은 도메인 모델에 위치해야 한다.
    • 서비스는 단순히 컨트롤러가 사용하는 것이 아니다. 서비스는 도메인을 실행하는 역할을 해야한다.

 

 

07. 서비스: 도메인(도메인객체/도메인서비스)에 일을 위임하는 공간이어야 한다.

  • 도메인 객체를 불러온다 - 도메인에 일을 위임한다. - 도메인 객체의 변경 사항을 저장한다.
  • @Service: 캡슐화 된 상태 없이, 모델과는 독립된 동작을 제공하는 인터페이스이다.
    • 도메인 객체가 처리하기 애매한 연산 자체를 표현하기 위한 컴포넌트
  • 도메인 서비스 vs 애플리케이션 서비스:
    • 도메인 서비스: 비즈니스 연산 로직을 처리하며, 도메인 객체에 기술할 수 없는 연산 로직을 처리한다.
    • 애플리케이션 서비스: 애플리케이션 연산 로직을 처리하며, 도메인을 저장소에서 불러오고/도메인 서비스를 실행하며/도메인을 실행한다.
  • 서비스는 불변해야 한다. 

 

08. 레이어드 아키텍쳐

컴포넌트에 맞춰 레이어를 분류하는 것은 폴더를 관리하는 것과 다를 바 없다.

  • 레이어
    • Layer 간 의존 방향은 단방향이어야 한다.
    • Layer 간 통신은 인접한 레이어에서만 이루어지도록 한다.
  • 아키텍쳐: 정책과 제약을 정하는 과정
    • 아키텍쳐는 제약사항이 더 중요하다 = 목적에 따라 아키텍쳐가 달라질 수 있다.
  • Account 시스템을 개발한다면
    • Account 엔티티를 어떻게 설계할 것인가? -> 객체지향스럽지 않음(데이터 위주의 사고방식)
      • 기능이 만들어지기 전 DB가 만들어져야 하므로 DB 종속적이다.
    • API를 어떻게 만들것인가? / RequestBody/ResponseEntity의 형태는 어때야하는가?
      • 프로젝트가 프레임워크에 종속된다. 웹소켓/gRPC/메세지 큐 등 다양한 형태가 될 수 있어야 함
    • 비즈니스 레이어: 도메인을 파악하고, 도메인 모델을 구성하고, 도메인 분석/개발부터 시작하는 것이 좋다.
  • 패키지 != 레이어
    • 레이어는 외부 라이브러리에 의존하지 않고(어노테이션X) 순수 자바코드로 작성해야
  • 의존성 역전: 변경 영향이 적어진다

 

09. 모듈

  • 모듈과 모듈 시스템 != 패키지
    • 모듈: 프로그램의 기본 구성요소/라이브러리 + 의존성관리/캡슐화/독립성,은닉성
    • 패키지: 폴더, requres/exports

 

10. 도메인: 사용자들이 겪는 문제 영역이고, 이것을 해결하기 위해 소프트웨어를 개발하는 것

신규 프로젝트를 개발할 때, 어떤 기술을 사용할지부터 정하지 말고 무엇을 만들고싶은지, 도메인을 분석하고 도메인의 요구사항부터 정리해야한다. (앞의 내용 복습 느낌) ->시스템의 설계/패턴/세부 구현은 도메인을 바탕으로 선택해야 한다.

  • 보일러플레이트: 새로운 SW 프로젝트를 시작할 때, 기본적으로 필요한 구성 요소나 의존성, 기능 등을 미리 갖춰놓은 프로젝트로, 이러한 프로젝트를 이용하면 반복적인 설정 작업을 건너 뛸 수 있으므로 빠르게 개발에 집중 할 수 있다
  • 도메인 모델과 영속성 개체
    • 통합하기: 객체/매핑 메서드를 추가로 작성할 필요가 없다. ↔ 클래스 책임이 제대로 눈에 들어오지 않는다. DB위주의 사고를 하게 된다. 도메인 모델이 커질수록 관리가 어렵다.
    • 구분하기: 굳이 ORM을 사용할 필요가 없게 된다. DB 접근 라이브러리에 비종속적

 

11. 알아두면 유용한 스프링 활용법

무지성으로 사용했던 것들을 다시 볼 수 있어서 좋았다.

  • @Autowired: 타입을 기반으로 빈을 찾아 의존성 주입
    • 없으면 NoSuchBeanDefinitionException
    • 어떤 빈을 주입할지 선택할 수 없는 상황이라면 NoUniqueBeanDefinitionException
      • @Qualifier: 주입하려는 빈을 지정할 수 있다.
      • @Primary: 타입이 일치하는 빈이 여러개일때 가장 우선하여 주입하도록 한다.
    • List type으로 주입하는 경우 모든 빈이 List의 요소로 들어간다
  • @Resource: 이름이 일치하는 빈을 먼저 찾고, 그 다음에 타입을 기반으로 빈을 찾아 의존성 주입

NotificationService에서 PushNotificationChannel를 추가하는 예제가 흥미로웠다. Push를 추가하는 요구사항이 발생하더라도 NotificationService 컴포넌트를 수정할 필요가 없어짐(OCP)

  • 스프링의 빈 메서드에서 자가 호출이 일어나는 경우(@Transactional 예제): 자가 호출이 발생하면 호출되는 메서드에 적용된 AOP 애너테이션이 동작하지 않을 수 있다.
    • doSomething2 메서드는 doSomething1 메서드의 자가 호출로 실행되므로, doSomething2에 걸려있는 Transactional이 실행되지 않음
    • AOP가 프록시를 기반으로 동작하기 때문에 발생하는 현상: 메서드에 지정된 AOP 애너테이션이 수행되려면 반드시 프록시 객체를 통해 메서드가 실행돼야 한다.
      • 메서드를 자가호출하는 상황에서는 프록시를 거치지 않고 클래스에 정의된 메서드를 곧바로 호출하게 된다.

 

 

3부. 테스트

12. 자동테스트

수동 테스트는 소모적이며, 시스템이 커질수록 누적된다. -> 그래서 테스트 코드를 작성해서 개발자의 부담을 줄여야 한다

  • Postman으로도 자동테스트를 만들 수 있는지 몰랐음 https://www.postman.com/automated-testing/
  • 12.1~12.2 파트를 읽으면서 시스템을 파악할 땐 물론 기본적인 코드도 봐야하지만, 테스트 코드를 통해 큰 시스템을 빠르게 파악할 수도 있겠구나 하는 생각이 들었다.
 

Automated API Testing | Postman

Automated API testing with Postman. Create test suites, integrate with your CI/CD pipeline, scale faster and streamline development and QA.

www.postman.com

 

13. 테스트 피라미드

  • unit test(80) - integration test(15) -e2e test(5, ex.cypress)
  • 소형테스트 - 중형테스트 - 대형테스트 로 사용되는 리소스의 크기로 분류하니 조금 더 직관적이었다.
    • 13.2에 H2를 활용한 테스트는 비결정적으로 동작할 가능성이 있다고 한다는것은 조금 의외였다. 
    • mock을 사용하는 것이 오히려 귀찮아서 H2나 디비 연결을 자주 사용했었는데, 그런 테스트들은 코드베이스 외의 다른 요소에 의존하기 때문에 테스트가 실패하더라도 디버깅이 어렵고 신뢰할 수 없음. -> 사실 H2가 다른 프로세스를 사용하니까 프로세스 상태와 프로세스 간 통신에도 의존할 것이 너무나도 당연하긴 한데 그 동안 이런 생각을 하지 못하고 테스트를 작성했던 것 같다.

 

14. 테스트 대역

코드 14.2~14.3에 UserServie에 DummyVerificationEmailSender를 주입하는 형태는 처음 보는 형태였음 

(원래 Mock으로 만드는것은 많이 봤던거같은데 새로운 Sender를 재정의 해서 동작 상황을 다양하게 연출하는 것은 좋은 것 같다) 이 파트는 상세한 예시가 도움이 많이 되었다.

  • 이런 implements를 통한 재정의를 테스트 과정에 사용하기 위해서는 추상화가 중요하고 의존성 역전이 잘 되어있어야 겠다

 

15. 테스트 가능성: 테스트하기 쉬운 코드일수록 좋은 설계일 학률이 높다.

  • 테스트를 사용하는 목적은 두 가지가 될 수 있다: 회귀 버그 방지 / 좋은 설계를 얻기 위함
  • 테스트는 입력을 쉽게 변경할 수 있고 + 출력을 쉽게 검증할 수 있을 때 작성하기 쉽다. <-> 숨겨진 입력이 존재하거나 숨겨진 출력이 있을 때 테스트를 검증하기 어려워진다.
  • UserService 사용자 login 시각 갱신 예제
    • login의 인수로 timestamp를 받는 것 까진 생각했는데 책에 나와있는대로 의존성을 외부로 미룬게 아닌가 하는 생각이 듦
    • ClockHolder 인터페이스를 만들고, 의존성 주입/의존성 역전 -> 유연한 변경이 가능하다.
  • 반환값의 경우 String 그 자체를 리턴하기보다는 확장성을 위한다면 DTO를 만들도록 한다.
  • private 메서드는 내부 구현이기 때문에 테스트하지 않아도 된다. -> 만약 테스트 해야할정도로 중요하다는 생각이 든다면 책임을 잘못 할당한 경우이다.
  • 서비스 컴포넌트를 테스트 해야할 때 객체 주입이 과하게 필요한 경우 서비스 컴포넌트의 세분화가 필요한 것일 수도 있다.

 

16. 테스트와 설계

  • SRP: 테스트할 때 불필요한 의존성이 있다면 컴포넌트를 분리하는 것이 좋을 수 있다. (=단일 책임 위반)
  • ISP: SRP와 마찬가지로 인터페이스는 기능 단위로 상세히 분리하는 것이 좋다.
  • OCP, DIP: 유연한 설계를 했음을 확인하기 위해 컴포넌트 간 의존 관계를 전부 파악해 볼 수도 있지만, 실제 실행되는 배포환경과 다른 테스트환경에서 새로운 요구사항에 맞게 원활하게 실행되는지 확인하면 된다.
    • 구현체를 상속하여 의존성 역전 원칙 없이도 테스트를 작성할 수 있으나, 서비스가 추상에 의존하도록 해야 세부 구현에 의존하지 않게 된다.
  • LSP: 테스트는 유지하고 싶은 시스템의 상태를 모두 작성하는 것이다. 즉, 리스코프 치환 원칙에 관한 테스트를 작성함으로써, 파생 클래스 수정에도 이 원칙이 준수되는지 테스트를 통해 능동적으로 감시할 수 있다.
    • 하지만 새로운 파생클래스가 처음부터 기본클래스를 대체하지 못하는 경우는 위반을 감지할 테스트가 없다. -> 단, 테스트를 고민함으로써 리스코프 치환을 유지할 수 없는 설계의 잘못된 점을 발견할수도 있다.

 

17. 테스트와 개발방법론

  • TDD: Red-Green-Refactor
    • Red: 아직 구현되지 않은 기능을 테스트 작성 -> 테스트 실패
    • Green: 테스트를 통과시키기 위한 최소한의 코드 작성
    • Refactor(=Blue): 가독성/유지보수성/성능을 높이기 위해 리팩터링 -> 단, 기능은 유지된 상태에서 코드의 구조만 변경
    • TDD는 요구사항이 명확하지 않고 자주 변경되는 상황에서는 좋지 않다. (요구사항이나 인터페이스가 극단적으로 변경되는 경우)
  • BDD: 사용자 행동(행동 명세)을 요구사항으로 먼저 만들고, 이를 테스트로 표현될 수 있게 만든다. -> TDD+DDD
    • TDD의 경우 무엇을 어떻게 테스트해야하는지 알 수 없다.
    • Give-When-Then 형식

 

들어가며

특정 라이브러리, 최신 프로그래밍 언어, 인프라 기술 등은 '기술'로 분류되고,

이런 기술을 학습하는 것은 알고나면 당연한게 많기 때문에 '개발'을 공부하는 게 좋다: OOP, 테스트, 아키텍쳐 등

책 초반부터 찔리는 내용들이 상당히 많음..ㅋㅋ

 

<OOP>

1. 객체지향의 핵심 가치인 역할, 책임, 협력이 무엇인가?

-> 역할: 역할은 객체가 시스템 내에서 수행하는 기능이나 작업을 의미한다. ex. 은행 시스템에서 '계좌' 객체는 잔액 관리, 입금, 출금 등의 역할을 수행할 수 있다.

-> 책임: 책임은 객체가 수행해야 하는 행위와 상태를 관리하는 의무를 말한다. 또한 객체가 알아야 하는 정보와 수행할 수 있는 행동을 포함한다. ex. '계좌' 객체에서 계좌 잔액을 정확히 유지하고, 거래 내역을 기록하며, 잔액 부족 시 출금을 거부하는 등의 책임을 가질 수 있다.

-> 협력: 협력은 객체들이 상호작용하며 시스템의 기능을 수행하는 방식을 의미한다. ex. 은행 시스템에서 '고객' 객체가 '계좌' 객체에 출금 요청을 보낼 수 있다.

 

2. 진행하는 프로젝트를 설명해보고, 객체지향인지 아닌지 어떻게 판단할 수 있는가?

-> 객체지향적인지 판단하기 위해서는 다음과 같은 특징들을 살펴볼 수 있다: (온라인 쇼핑몰 프로젝트에서)

  • 추상화: 복잡한 시스템을 간단하고 이해하기 쉬운 객체들로 모델링했는지 확인 -> ex. '상품', '고객', '주문', '장바구니' 등의 실제 개념을 추상화하여 클래스로 모델링했는지
  • 캡슐화: 객체의 내부 상태와 구현 세부사항을 외부로부터 숨기고, 필요한 인터페이스만 노출하는지 확인 -> ex. 각 클래스가 자신의 데이터를 캡슐화하고 필요한 메서드만 외부에 노출하는지
  • 상속: 객체 간의 계층 구조를 통해 코드 재사용과 확장성을 제공하는지 확인 -> ex. '일반상품'과 '할인상품' 과 같은 클래스들이 '상품' 클래스를 상속받아 구현되었는지
  • 다형성: 동일한 인터페이스를 통해 다양한 객체들이 자신만의 방식으로 동작할 수 있는지 확인 -> 결제' 인터페이스를 통해 '신용카드결제', '계좌이체', 'PayPal결제' 등 다양한 결제 방식을 다형적으로 처리하는지
  • 객체 간 통신: 객체들이 메시지를 주고받으며 상호작용하는지 확인
  • 단일 책임 원칙: 각 객체가 명확하고 단일한 책임을 가지고 있는지 확인
  • 느슨한 결합: 객체들 간의 의존성이 최소화되어 있는지 확인

 

3. 객체지향이 무엇인가? 클래스를 이용해서 프로그래밍하는 방법 외에(ㅋㅋ) 어떤 설명을 할 수 있는가?

-> 2번

 

4. 객체와 클래스의 차이점은 무엇인가? 붕어빵과 붕어빵 틀의 관계(ㅋㅋ;;)라고 설명하지 말고.

-> 객체는 클래스(설계도)를 바탕으로 실제로 만들어진 인스턴스이다. 그래서 클래스는 메모리에 직접 할당되지 않고 객체를 생성하기 위한 '틀'로써 객체의 속성(데이터)과 행위(메서드)를 정의하며 컴파일 시 사용된다. 객체는 클래스의 추상적 정의를 구체화하여 실제 데이터와 상태를 가지며, 런타임에 생성되고 실제로 메모리에 할당되어 프로그램에서 사용된다.

 

5. 메서드는 왜 메서드라고 부르는가? 함수와는 어떤 차이가 있는가?

-> 함수는 독립적으로 존재하며, 어떤 특정 객체에 속하지 않는다. 반면 메서드는 객체에 종속되어 있어 객체를 통해 호출되며, 객체의 상태에 접근할 수 있다. 또한 오버라이딩과 오버로딩을 통해 다형성을 지원할 수 있다.

 

<테스트>

1. 테스트를 학습한다는 것은 무슨 의미인가? Mockito, JUnit, H2 학습 외에 어떤게 있는가?

-> 책 초반에도 말했듯이, 단순히 테스트 도구나 프레임워크 외를 물어본 것 같다.

a) 성능 테스트:

  • 부하 테스트: 시스템이 예상되는 최대 부하 상황에서도 정상적으로 작동하는지 확인한다. ex. 웹 서버가 동시에 N명의 사용자 요청을 처리할 수 있는지
  • 스트레스 테스트: 시스템의 한계를 넘어서는 극단적인 조건에서 시스템의 동작을 확인한다. ex. 데이터베이스 연결이 끊어졌을 때 애플리케이션이 어떻게 대응하는지
  • JMeter, Gatling: 대량의 가상 사용자를 생성하여 시스템에 부하를 가하고, 응답 시간, 처리량 등을 측정한다.

b) 보안 테스트:

  • OWASP 가이드라인: 웹 애플리케이션의 일반적인 보안 취약점(예: SQL 인젝션, 크로스 사이트 스크립팅)을 확인하고 방어
  • 침투 테스트: 실제 해커의 공격을 시뮬레이션하여 시스템의 보안 취약점을 찾아내는 테스트
  • 퍼지 테스트: 보안 쪽에 포함되는지는 잘 모르겠지만, 요번에 진행하는 오픈소스 프로젝트에서 하고 있더라. 용어만 들어봤었는데,  자동화된 도구를 사용해 프로그램 또는 프로그램 기능에 유효하지 않거나 예상치 못한 입력을 제공하고 나서, 결과를 확인해서 프로그램이 충돌하거나 부적절하게 작동하는지 확인하는 테스트라고 함. 힙 버퍼 오버플로우나 use-after-free 와 같은 c/c++ 및 임베디드 코드에서 메모리 손상을 찾는 데 특히 효과적이라고 한다.

c) 테스트 자동화:

  • CI/CD 파이프라인에 테스트 통합: 코드 변경이 있을 때마다 자동으로 테스트를 실행하여 즉시 피드백을 제공 -> Jenkins, GitLab CI와 같은 도구들로 코드 커밋, 빌드, 테스트, 배포 등의 과정을 자동화할 수 있다.

d) 테스트 데이터 관리:

  • 테스트 데이터 생성 전략: 테스트에 필요한 다양한 시나리오의 데이터를 효과적으로 생성하고 관리하는 방법
  • 데이터베이스 시딩: 테스트 실행 전에 데이터베이스에 미리 정의된 데이터를 삽입하여 일관된 테스트 환경을 제공

e) 테스트 더블 기법:

  • Stub: 미리 정의된 응답을 반환하는 간단한 대체 객체
  • Spy: 실제 객체의 일부 메서드만 오버라이드하여 호출을 기록하거나 수정
  • Mock: 예상되는 호출과 응답을 미리 프로그래밍한 객체로, 실제 호출이 예상과 일치하는지 검증
  • Fake: 실제 구현을 단순화한 가벼운 버전의 객체(예: 인메모리 데이터베이스)

f) 계약 테스트:

  • 서비스 제공자와 소비자 사이의 인터페이스 계약을 정의하고 검증한다.
  • Consumer-Driven Contracts: 소비자가 기대하는 서비스의 동작을 계약으로 정의하고, 제공자가 이를 만족하는지 테스트 -> Spring Cloud Contract와 같은 마이크로서비스 간의 계약을 정의하고 검증하는데 사용되는 도구가 있다.

 

2. 단위 테스트, 통합 테스트, end to end 테스트의 차이점이 무엇인가? 또한 단위 테스트에서 말하는 단위는 무엇이고, 통합 테스트에서 말하는 통합은 무엇인가?

-> 단위 테스트: 개별 코드 단위(주로 메서드 수준)의 정확성을 검증한다. 외부 의존성을 모킹 또는 스터빙 해주어야 한다.

-> 통합 테스트: 여러 단위가 함께 올바르게 작동하는지 검증한다. 특히 여러 컴포넌트 또는 모듈 간의 상호작용을 검증하는 데 사용하기 때문에, 실제 의존성을 사용하며 단위 테스트보다 느리고 복잡하다.

-> End-to-End 테스트: 전체 시스템의 흐름과 기능성을 검증한다. 사용자 관점에서의 전체 애플리케이션을 테스트하며, 실제 환경과 유사한 조건에서 수행하기 때문에 가장 복잡하고 시간이 오래 걸린다.

 

  • 단위 테스트에서의 '단위': 주로 메서드나 함수 수준을 의미하는 것 같고, 이 단위가 독립적으로 테스트 가능해야 하며, 외부 의존성은 모킹하거나 스터빙해야 한다.
  • 통합 테스트에서의 '통합': 여러 컴포넌트나 모듈이 함께 작동하는 것을 의미하고, 실제 의존성을 사용하여 여러 부분이 올바르게 결합되어 작동하는지 확인해야 한다. ex. 데이터베이스와의 상호작용, 외부 서비스와의 통신, 여러 레이어 간의 상호작용

 

3. Mockito, JUnit, H2 없이 테스트를 작성해야할 때, 스프링 프로젝트에 테스트를 어떻게 넣을까?

-> 모르겠다.. 직접 구현해야하지 않을까..? 

 

<아키텍쳐>

1. 아키텍쳐라고 하면 떠오르는 것이 무엇인가? SOLID? SOLID를 알고 있다면 의존성 역전과 의존성 주입이 어떻게 다른지 설명해라.

-> 의존성 역전: 모듈 간의 결합도를 낮추고 유연성을 높이기 위해 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 설계 원칙이다. 

// 의존성 역전 적용 전
class EmailService {
    public void sendEmail() { /* 이메일 전송 로직 */ }
}

class NotificationManager {
    private EmailService emailService = new EmailService();
    public void notify() {
        emailService.sendEmail();
    }
}

// 의존성 역전 적용 후
interface MessageService {
    void sendMessage();
}

class EmailService implements MessageService {
    public void sendMessage() { /* 이메일 전송 로직 */ }
}

class NotificationManager {
    private MessageService messageService;
    public NotificationManager(MessageService service) {
        this.messageService = service;
    }
    public void notify() {
        messageService.sendMessage();
    }
}

-> 의존성 주입: 의존성 역전 원칙을 구현하는 방법 중 하나로, 객체의 의존성을 외부에서 제공하는 테크닉이다. 생성자 주입, 세터 주입, 인터페이스 주입 등의 방식이 있다.

class NotificationManager {
    private MessageService messageService;
    
    public NotificationManager(MessageService service) {
        this.messageService = service;
    }
    
    public void notify() {
        messageService.sendMessage();
    }
}

// 사용
MessageService emailService = new EmailService();
NotificationManager manager = new NotificationManager(emailService);

 

 

2. 의존성이 무엇인가?

-> 의존성은 한 모듈이나 클래스가 다른 모듈이나 클래스를 필요로 하는 관계를 말하며, 만약 A가 B에 의존하게 되면 B의 변경이 A에 영향을 줄 수 있다.

 

3. 인터페이스가 하나 있고, 인터페이스의 구현체는 프로젝트에 하나밖에 없다면, 굳이 엔지니어가 구현체와 인터페이스를 분리한 이유가 무엇일까?

-> 일단은 '무엇을 할 것인가'(인터페이스)와 '어떻게 할 것인가'(구현체)를 분리함으로써 코드의 구조를 개선할 수 있고, 이후 다른 구현체가 필요할 때 쉽게 추가할 수 있으며 리팩토링이 쉬워진다.

-> 그 외에도 인터페이스를 사용하면 목(mock) 객체를 쉽게 만들 수 있어 단위 테스트가 용이해진다는 장점이 있다고 함. 

 

 

 

 

 

 

1부. 객체지향

01. 절차지향과 비교하기

  • 순차지향 프로그래밍(sequential oriented programming)과 절차지향 프로그래밍(procedure=함수 oriented programming): 순차지향 프로그래밍은 어셈블리어로 작성된 코드와 같이 함수의 개념이 존재하지 않아 코드를 위->아래로 순차적으로 읽는다. 만약 목적지 주소를 건너뛰어야 하는 경우가 있으면 jmp나 goto와 같이 프로그램의 실행 위치를 옮기는 방식으로 코드가 동작한다. 반면, 절차지향 프로그래밍은 함수를 만들어 프로그램을 만드는 방식으로, 일반적인 c언어로 작성된 코드가 이에 해당된다.
  • 코드 1.6: 책 예시에 있는 코드 1.4, 1.5 방식으로만 작성해왔던 것 같은데, 수익을 RestaurantChain - Store - Order - Food 으로 수익 계산 로직을 객체가 처리하도록 변경되었다. 단순히 food.getPrice()로 계산하지 않고 food 내부에 수익을 리턴하는 함수 calculateRevenue()를 만들고 price를 리턴하도록 함으로써 비즈니스 로직을 객체가 처리하도록 변경했다. 
    • 객체지향으로 코드를 작성하는 이유가 가독성을 높이기 위해서는 아니다. 객체지향은 가독성보단 객체 자체가 맡고 있는 책임에 더 집중한다. 그렇다고 해서 '책임'이라는 개념이 객체지향만의 특징은 아니다. 
  • 코드 1.10: 수익을 계산하는 Calculable 인터페이스를 추가하고, 기존 클래스들이 인터페이스를 구현하도록 변경했다. 즉, 객체 각각에 할당되어 있던 책임을 인터페이스로 분할하게 됐는데, 이 쪽이 더 자연스러워 보이고 이해하기 쉬웠다. C언어는 이러한 추상적인 인터페이스 개념을 지원하지 못하므로 절차지향 언어라고 한다.
    • 객체지향에서는 엄밀히 말해 '책임을 객체를 추상화한 역할에 할당'하는 것을 중요하게 보고, 책임을 어떻게 나누고 어디에 할당하는지 집중한다.
  • 코드 1.12: Food 외에 새로운 수익 아이템이 추가되는 경우, 기존 Calculable 인터페이스로 분리하지 않았다면 계산 로직 자체를 변경해야 했을 것이다. 하지만 Calculable 인터페이스를 사용하게 되면 리스트로 한번에 묶어 코드 변경 없이 기능을 확장할 수 있다.
  • 코드 1.14: canAfford 메서드 파라미터로 Product를 전달하면 더 깔끔하지 않은가 잠깐 생각했는데, Account 클래스에서 금액 비교를 할 때, Product name 등은 의미가 없고 오히려 Product 객체에 의존하게 된다는 단점이 있는 것 같다.

 

02. 객체의 종류

  • Value Object: 코드 2.2에서 Color은 객체이지만 동시에 값이다. 값은 불변성(final, 순수함수)/동등성(equals, hashCode)/자가검증(생성자) 이라는 특징을 가지고 있는데, 이 세가지 특징을 만족할 때 VO라고 부른다. = 신뢰할 수 있는 객체
    • 코드 2.4: 모든 멤버 변수가 final로 선언되어 있더라도 참조타입이라면 불변성이 보장되지 않을 수 있다. 즉, 불변 객체 내부의 참조 객체가 불변이 아니라면 그 객체는 불변이 아니다.
    • 코드 2.6: 또한, VO의 모든 함수는 매번 동일한 값을 반환하는 순수 함수여야 한다.
    • VO를 만드는 이유에 대해 더 집중하는 게 좋다. VO의 불변성으로 객체를 신뢰할 수 있게 만들 수 있다.(멀티 스레드 환경)
    • VO에서 값과 상태가 같으면 같은 객체로 봐야한다. 실제로는 객체를 비교하는 상황이 많지 않아 equals/hashCode를 오버라이딩하는 것을 생략하기도 한다. 동등성도 결국 VO의 신뢰성을 위한 것이므로, 신뢰할 수 있는 객체라면 생략할 수 있다고 본다. (+ 단 VO는 id와 같은 식별자 필드를 멤버 변수로 가지고 있어서 안된다.)
    • 값객체를 만들 때, 롬복의 @Value 어노테이션을 사용할 수 있다. - equals/hashCode 자동생성, 멤버변수 및 클래스 final 선언
    • java16부터 record 키워드를 사용하여 데이터를 담는 간단한 클래스를 만들 수 있다. - equals/hashCode 자동생성, 멤버변수 및 클래스 final 선언/getter 자동 생성
  • Data Transfer Object: 다른 객체의 메서드를 호출하거나 시스템을 호출할 때, 매개변수를 나열하는 것이 불편하니, 데이터를 구조적으로 만들어 전달하기 위한 객체
    • 데이터를 전달하는 데만 집중하며, 그 밖의 역할이나 책임을 가지지 않는다. 데이터 읽기/쓰기 외의 비즈니스 로직이 들어가면 안된다.
    • 롬복의 @Data 어노테이션을 사용할 수 있다. - getter/setter/toString/EqualsAndHashCode와 같은 어노테이션이 한 번에 지정된다.
  • Data Access Object: 데이터베이스 접근과 관련된 역할을 지닌 객체로, 데이터베이스와의 연결/CRUD 연산 수행/보안 취약성을 고려한 쿼리 작성 과 같은 역할을 담당한다. = 스프링에서 Repository 개념 = 도메인 로직과 데이터베이스 연결 로직을 분리하기 위함
  • Entity: 
    • Domain Entity: Domain Model 중 특화된 비즈니스 로직이나 생애주기를 가지는 등, 특별한 기능을 가지는 모델을 말한다. 식별자와 비즈니스 로직을 갖는다.
    • DB Entity: 데이터베이스에서 정보를 담는 객체를 표현하기 위한 수단을 말한다.
    • JPA Entity: 관계형 데이터베이스에 있는 데이터를 객체로 매핑하는 데 사용되는 클래스를 말한다. = 영속성 객체
      • 데이터베이스와의 연동을 추상화하고, 데이터 접근을 담당함으로써 애플리케이션의 비즈니스 로직과 영속성 로직을 분리하기 위함 
      • @Entity 어노테이션을 지정하여 사용한다. 

 

03. 행동

어떤 클래스를 만들 때(ex. 자동차)

클래스에 필요할 것으로 예상되는 속성(=데이터 위주의 사고) vs 클래스가 어떤 행동을 해야하는지(=행동 위주의 사고)

  • 인터페이스와 행동은 다르다. 인터페이스는 외부에서 어떤 객체에게 행동을 시키고자 할 때, 어떤 행동을 지시하는 방법이라고 할 수 있다. 따라서 별도의 지시자가 없으면 public으로 동작한다.
  • 인터페이스로 객체 간의 통신이 이루어질 때, 코드가 실행되기 전까지는 실제로 어떤 메서드가 호출될지 알 수 없다.

 

 

 

Planning for Dynamic Programming (Model-Based)

  • Planning = Model-Based: MDP에 대한 모든 정보를 다 알때(Model이 있을 때) ex. environment, state, state transition, ...
  • Dynamic Programming: Planning 문제를 풀기 위한 방법론으로 DP가 되기 위한 조건이 2가지 있다.
    • Optimal Substructure: 전체 큰 문제에 대한 optimal solution이 작은 문제들로 쪼갤 수 있어야 한다. = Bellman Eq가 recursive하게 적용 가능
    • Overlapping Subproblems: subproblems들을 구하고 캐싱하여 계산적 이득 = value function(작은 문제들의 정답값)들이 저장되고 솔루션을 구할 때 재사용된다.

 

MDP가 만족되면 DP를 적용하여 "문제를 푼다"

DP는 MDP에 대한 모든 것을 알고 있다고 가정한다.(full-knowledge = Model-Based)

  • Prediction을 풀기: MDP & Policy가 주어졌을 때, MDP가 Policy를 따랐을 때 리턴을 얼마 받는지, 즉 value function이 어떻게 되는지 v_π를 찾는 문제 = Value Function을 학습 = Policy Evaluation
    • Bellman Expectation Equation을 iterative하게 적용한다.
      1. random하게 v1 초기화
      2. 모든 State에 대해 π를 이용하여 v2 구하기 (synchronous backup)
      3. 2 반복
      4. v_π에 수렴
  • Control을 풀기: MDP가 주어졌을 때, optimal policy π*를 찾는 문제 = Optimal Policy를 찾기
    • Policy Iteration: 결국 π는 policy iteration을 하면 optimal policy π*로 수렴한다
      1. Policy π를 평가: v_π(s) = E[R_{t+1} + rR_{t+2} + ... | S_t=s]
      2. v_π의 값이 가장 좋은 쪽으로 움직임: π' = greedy(v_π)
      3. 1-2 반복
        • Modified Policy Iteration: policy evaluation 단계에서 꼭 v_π가 수렴할 필요 없이 early-stopping도 가능하고, batch 단위로 업데이트도 가능함
    • Value Iteration: policy iteration과 달리 explicit한 policy가 없다
      1. v1 초기화
      2. Bellman Optimality backup(synchronous) 적용
      3. 2 반복
    • Principle of Optimality: Optimal Policy는 두가지 요소로 나뉠 수 있다
      • 첫 action A가 optimal하다 = A*
      • 그 action을 해서 어떤 State s'에 도달하면 거기서부터 또 optimal policy를 따라간다
      • 즉, Bellman Optimality Equation에 따르면 v*(s) = max_a(R_s^a + r\sum_{s' from State} P_{ss'}^a v*(s'))
        • 즉 상태 s의 optimal value를 알기 위해서는 다음 상태 s'까지 진행해봐야 상태 s의 value가 최적인지/아닌지 판단이 가능하다
        • S0 -> S1 -> ... ->St(terminate) 인 시퀀스가 있을 때, V(St)를 알아야 비로소 V0(S0)을 알 수 있다.

 

길찾기 문제

예를 들어 길찾기 문제에서 goal 근처의 value는 goal을 이용하여 계산하게 된다.

*하지만 DP에서 모든 상태에 대한 업데이트를 동시에 진행해야하므로, goal의 optimal value가 goal과 가까운 곳부터 퍼져나가면서 다른 위치의 칸들이 optimal vlaue를 계산할 수 있을 때 까지 여러번 반복된다. 이를 value iteration이라고 한다.

 

  • 알고리즘이 State value function에 의존하고 있으면 한 iteration마다 complexity는 O(mn^2)
  • 알고리즘이 State action value function에 의존하고 있으면 한 iteration마다 complexity는 O(m^2n^2)

 

위의 경우 모두 synchronous backup을 말하고 있는데, asynchronous backup을 하는 경우 computation을 줄일 수 있다.

(State 들이 비슷한 확률로 다양하게 뽑힌다면 수렴도 보장된다.)

asynchronous backup을 할 땐 state에 우선순위를 두고 하면 빠르게 수렴될 수 있는데, Bellman error가 클수록 우선순위를 높게 설정한다. asynchronous backup을 하는 경우에는 계산적 이득 뿐만 아니라 실제로 구현 시 v array를 하나만 사용해도 된다.

  • Bellman error = |max_a(R_s + r\sum_{s' from State} P_{ss'}^a v(s)) - v(s)|

 

State Space가 넓고 Agent가 도달하는 State가 한정적인 경우에는, Agent를 움직이게 해 놓고, Agent가 방문한 State를 먼저 업데이트하는 Real-Time Dynamic Programming 기법이 효과적이다.

 

사실 DP는 full-width backup으로, s에서 갈 수 있는 모든 s'를 이용하여 value를 업데이트 하게 되기 때문에 medium-sized problem에는 효과적이지만, 큰 문제에서는 비효율적이다. (state 수가 늘어날수록 계산량이 exponential하게 증가하므로)

따라서 큰 문제에서는 sample backup을 사용한다. 

  • state가 많아져도 고정된 비용(sample 갯수)로 backup (=dimension이 커질 때 발생하는 문제를 sampling을 통해 해결)
  • model-free 상황에서도 가능

 

Model-Free Prediction

 

 

 

https://www.youtube.com/watch?v=2pWv7GOvuf0&list=PLzuuYNsE1EZAXYR4FJ75jcJseBmo4KQ9-

스터디 시작 전 David Silver 교수님의 강의를 듣고 개념 정리를 했습니다.

 

 

 

Introduction to Reinforcement Learning

  • 문제 setting, 용어 등 기본적인 소개

강화학습에서는 오로지 보상에만 초점을 맞춘다. 미리 정해진 정답값을제공하는 기존 ML과 달리, 강화학습은 reward를 보고 가장 최적의 솔루션을 찾아내는 데 중점을 둔다.

즉, 강화학습의 목적은 최대 보상을 얻기 위해 특정 행동을 지시하는 것이 아니라, RL이 받고 있는 보상을 최적화 하는 방법을 알아내는 것이다. 이런 접근 방식은 인간은 결국 sub-optimal한 정답값에 도달하게된다는 생각에서 출발한다. 따라서 복잡한 시나리오에서 사람이 답을 알려주며 학습을 하게 되면 결국 sub-optimal한 정답값을 학습하게 되기 때문에 차선책에 만족하는 경우가 많다. 강화학습은 알고리즘이 보상을 기반으로 자유롭게 탐색하고 최적화 하도록 함으로써 복잡한 시나리오에서 자율적으로 global optimal을 찾아나갈 수 있다.

 

핵심적인 차이점은 강화학습은 순서가 중요한 sequential data를 사용한다는 점이다. 고정된 예시가 아니라 경험을 통해 학습함으로써, 시간이 지남에 따라 행동에 대한 보상이 누적되며 agent가 따르는 policy가 형성된다.

 

RangL: A Reinforcement Learning Competition Platform

 

강화학습의 목표는 누적된 총 Reward를 최대화하는 것이다.
  • Reward: 단일 스칼라 숫자값, 시간 t 동안 agent가 얼마나 잘 수행했는지 나타내는 지표
  • Agent: Reward의 축적된 합을 최대화하는 것이 목적인 것
    • 강화학습을 수행하기 위해 Reward를 단일 숫자 값으로 표현할 수 있어야 하며, reward가 스칼라 값 하나로 치환 불가능할 정도로 복잡한 경우는 강화학습이 적합하지 않을 수 있다.
Agent는 다음 Action을 결정하는 데 State를 기반으로 의사 결정을 내린다.
  • State: 다음에 무슨 행동을 할지 결정하기 위한 모든 정보들
    • Environment State: environment가 observation(행동에 따라 상황이 어떻게 바뀌었는지)과 reward를 계산할 때 쓴 모든 정보(Agent는 알 수 없는 정보) = Markov
    • Agent State: 다음 action을 하기 위해 필요한 모든 정보 (reward와 observation 값/가공값)
    • Markov State: 어떤 tate가 Markov한지/아닌지
      • Markov하다 = 미래는 전체 히스토리가 아닌 현재 상태에만 의존한다. t 시간에 대한 결정을 할 때 바로 이전 (t-1)에만 의존하여 결정하는 것, 즉 (t+1) 미래의 시간은 현재 t와 관련 있으며, 과거 (t-1)와 그 이전의 시간들과는 관련 없다. 문제를 단순화 하여 agent가 이전의 모든 상태를 기억하지 않고도 그 순간의 보상을 극대화하는 데 집중할 수 있다.

 

 

Agent의 구성 요소

 

Agent의 목표는 Value Function을 최대화 하는 것이다. 이를 위해서는 장기적으로 가장 높은 Reward를 가져오는 최선의 Action을 선택하도록 Policy를 최적화해야 한다. Value Function과 Policy는 상호 의존적이며 반복적으로 개선된다.

  • Policy: State를 Action에 매핑하는 Agent의 행동
    • Deterministic Policy: State가 주어지면 결정적으로 action이 리턴
    • Stochastic Policy: State가 주어지면 가능한 여러 action들이 일어날 확률이 리턴

  • Value Function: 현재 Policy에 따라 State가 얼마나 좋은지 나타내는 지표, 종료될 때까지 받을 수 있는 미래 리워드의 합산
    • v_π(s): State=s에서 시작했을 때, 정의된 policy에 따라 받게되는 총 기댓값 
    • r: discount factor = 미래의 리워드는 불확실하니까 등의 이유로

  • Model: Model을 활용하여 Environment가 어떻게 될지 예측할 수 있다
    • State s에서 Action a를 했을 때
      • State=s에서 Action=a를 취한 후 즉각적인 reward는 무엇을 받을지 예측
      • State=s에서 Action=a를 취함으로써 발생할 다음 State가 무엇이 될지 예측
    • 이러한 모델링 기법은 RL에서 사용될 수도(model-based) / 사용되지 않을수도(model-gree) 있다.

 

 

RL의 분류 (value-based/policy-based/actor critic) vs (model-free/model-based)

 

강화학습 방법은 두 가지로 분류할 수 있다.

  • value-based vs policy-based vs actor critic
    • value-based: policy(x), value function(o) -> 각 state에서 value를 학습하여, 이로부터 도출된 정책을 따르는 데 의존
      • value function만 있더라도 agent 역할을 할 수 있는 경우
    • policy-based: policy(o), value function(x) -> value function을 하용하지 않고 policy를 직접 최적화하는 데 중점
    • actor critic: policy(o), value function(o) -> policy와 value function을 동시에 학습
  • model-free vs model-based
    • model-free: policy and/or value function(o), model(x)
      • 모델을 내부적으로 만들지 않고
    • model-based: policy and/or value function(o), model(o)
      • agent가 내부적으로 environment 모델을 추측하여 만들어서, model에 근거한 action이 이루어짐

 

RL 문제의 종류

 

강화학습에서 Agent는 크게 두 가지 방법으로 Policy를 개선할 수 있다.

  • Learning: Agent가 environment를 알지 못할 때, environment와 상호작용하면서 policy를 개선해나가는 것
  • Planning: Agent가 environment(reward & state transition)를 알고 있을 때, 실제로 environment에서 동작하지 않고도 내부 계산만으로 다양한 시나리오를 시뮬레이션하고 평가함으로써 policy를 개선해나간다
    • ex. 알파고의 몬테카를로 Search

Agent의 주요 목표는 크게 두가지로 나눌 수 있다.

  • Prediction: 주어진 Policy 하에서, 미래를 평가하는 것 = value function 을 잘 학습시키는 것
  • Control: Policy를 최적화하는 것 = best policy를 찾는 것

 

  • 그 외 용어
  • Exploration: environment에 대한 새로운 정보를 발견하기 위해 이전에 해보지 않은 행동을 시도하는 것
  • Exploitation: 이미 가지고 있는 정보들을 활용하여 reward를 최대화하는 것
    • RL 학습이 잘 이루어지려면 Exploration과 Exploitation 두 개념에 대한 균형이 중요하다

 

 

Markov Decision Process(MDP)

  • 강화학습이 적용되는 문제 도메인은 모두 MDP(=완전히 관찰 가능한 환경)이다
  • 강화학습은 MDP으로 모델링할 수 있는 환경에서 최적의 순차적 의사결정을 하는 것이다.
  • MDP: RL에서의 environment를 표현 & environment가 모두 관측 가능한 상황을 말한다.
    • 모든 강화학습 문제는 MDP 형태로 만들 수 있다.

  • Markov Process: n개의 State가 있고, 과거 경로에 관계없이 확률에 따라 State를 옮겨다니는 프로세스
    • State Transition Matrix: action없이 state를 확률에 따라 옮겨다님(Environment를 설명하기 위한 개념)
      • State S -> S'로 옮겨갈 확률 P_{ss'} = P[S_{t+1}=s' | S_t=s]일때, 모든 state의 확률을 나타내는 matrix

  • Markov Reward Process: n개의 State가 있고, 과거 경로에 관계없이 확률에 따라 State를 옮겨다니는 프로세스로, State s일때의 reward는 R_s=E[R_{t+1} | S_t=s]이고 r(discount factor)는 0~1사이 값이다.
    • Return: 강화학습은 return을 maximize하는 것이다.
      • G_t = R_{t+1} + rR_{t+2} + ... = 미래의 리워드 합
    • Value Function: Return의 기대값 = State s에 왔을 때 G_t의 기대값
      • v(s) = E[G_t | S_t=s] = State s에서 시작한 여러 episode들의 평균
    • Bellman Equation: value function은 두가지 부분으로 나뉜다.
      • v(s) = E[G_t | S_t=s] = E[R_{t+1} + r(R_{t+2} + rR_{t+3} + ...) | S_t = s]
        • R_{t+1}: immediate reward
        • r(R_{t+2} + rR_{t+3} + ...): discounted value of successor state = r v(s_{t+1})
      • v(s) = R_s + r\sum_{s' from S} P_{ss'}v(s')
        • R_s: s일때의 reward
        • P_{ss'}: s->s'로 옮겨갈 확률
        • v(s'): s'일때 value function
      • 이를 행렬로 표현하면 V=(I-rP)^{-1}R이다. 즉, 주어져있는 값이므로 역행렬을 통해 한번에 계산이 가능하다.
        • computational complexity는 O(n^3)이므로 비싼 연산임
        • n이 작을 때는 한번에 계산 가능하지만, n이 클 때는 iterative(DP/Monte-Carlo Evaluation/Temporal Difference Learning)한 방법을 사용해야 한다.
  • Markov Decision Process: n개의 State와 Action이 있고, 과거 경로에 관계없이 확률에 따라 State를 옮겨다니는 프로세스로, Action a를 하고 State s일때의 reward는 R_s^a=E[R_{t+1} | S_t=s, A_t=a]이고 r(discount factor)는 0~1사이 값이다.
    • State s에서 Action a를 하면 확률적으로 다른 State들에 도달하게 된다.
      • P_{ss'}^a = P[S_{t+1}=s' | S_t=s, A_t=a]: State s에서 Action a를 했을 때 State s'로 갈 확률
    • Policy: State s일때 Action a를 할 확률 = agent의 행동을 완전히 결정해줌
      • π(a|s) = P[A_t=a | S_t=s]
    • Value Function: 만약 agent의 policy가 고정되면 Markov process와 동일하다고 볼 수 있으며, reward도 마찬가지로 MRP로 볼 수 있음
      • State Value Function: State s에서 Policy π를 따랐을 때, episode들의 평균
        • v_π(s) = E_π[G_t | S_t=s]
      • Action Value Function: State s에서 Action a를 했을 때, 그 이후에는 Policy π를 따랐을 때의 기댓값
        • q_π(s,a) = E_π[G_t | S_t=s, A_t=a]
      • Bellaman Expectation Equation (= Prediction 문제): Bellman Eq.와 마찬가지로
        • v_π(s) = E_π[G_t | S_t=s] = E_π[R_{t+1} + rv_π(S_{t+1})| S_t=s] 
        • q_π(s,a) = E_π[R_{t+1} + rq_π(S_{t+1}, A_{t+1}) | S_t=s, A_t=a]
        • 위 두 식을 결합하면 v<->q로 표현할 수 있다.
          • v_π(s) = \sum_{a from Action} π(a|s) q_π(s,a)
          • q_π(s,a) = R_s^a + r\sum_{s' from State} P_{ss'}^a v_π(s')
          • v_π(s) = \sum_{a from Action} π(a|s) {R_s^a + r\sum_{s' from State} P_{ss'}^a v_π(s')}
        • MRP에서의 Bellman Eq.에 의해 행렬식으로 바꾸면 V_π = (I-rP_π)^{-1}R_π 로 구할 수 있다.
          • 마찬가지로 n이 커지면 구하기 어려우므로, q는 optimal value function을 이용하여 구한다.
    • Optimal Value Function
      • Optimal State Value Function: State s에서 어떤 policy를 따르든 간에, 그 중 가장 큰 value
      • Optimal Action Value Function: q*(s,a) = max_π(q_π(s,a))
      • Optimal Policy: 두 policy 중 더 나은 policy를 비교하기 위해서는 모든 State s에 대해 v_π(s) >= v_π'(s)가 성립하면 Policy π 가 Policy π' 보다 낫다고 할 수 있다.
        • Optimal Policy π*를 따르면 Optimal State Value Function v*(s)와 Optimal Action Value Function q*(s,a)를 구할 수 있다. 그리고 Optimal Policy는 q*(s,a)를 아는 순간 찾을 수 있다.
      • Bellman Optimality Equation (= Control 문제)
        • v*(s) = max_a(q*(s,a)): q*값들이 각 action마다 있을텐데, 그 중 max인 a를 선택하면 v*와 같다
        • q*(s,a) = R_s^a + r\sum_{s' from State} P_{ss'}^a v*(s')
        • 따라서 v*(s) = max_a(R_s^a + r\sum_{s' from State} P_{ss'}^a v*(s')): 이 식은 max 때문에 linear equation이 성립하지 않아 역행렬을 구하여 풀 수 없는 문제가 된다. 즉, 다양한 iterative solution method로 풀이를 해야한다.(value equation/policy equation/q-learning/sarsa)

 

우연한 기회에 ML 소프트웨어 엔지니어 7년차분과 커피챗을 할 수 있는 기회가 생겼다.

현재 대학원에 재학중인 나는 시스템 수준의 트레이스를 분석하거나,

이를 통해 머신러닝 학습 프로세스를 최적화 하는 방법들에 관한 페이퍼들을 써왔다.

 

대학원 과정에서 혼자 연구 주제를 구체화하고 연구를 진행할 때면,

이게 진짜 학교 밖에서도 요구되는 주제인지

내가 하고 있는 일들이 기업이 원하는 역량들인지 고민됐다.

 

모든 것이 순조롭게 진행된다고 가정할 때, 졸업이 1년정도 남은 상황인데

나는 취업을 위해 구체적으로 어떤 역량을 키워야하는지에 우선 순위에 대한 불확실성에 대해 계속 고민중이었다.

이런 고민들에 보다 명확한 답을 찾기 위해서 커피챗을 신청하게 됐다.

 

 

커리어패스를 명확하게 하는 것이 좋다

1. 학계에 남거나, 2. 기업의 연구원이 되거나, 3. 기업에서 소프트웨어 엔지니어가 되는 등

컴공 대학원생의 진로는 다양할 수 있고, 나는 이들 중 회사에서 엔지니어 일을 하고 싶었다.

 

대학원에서 가르치는 역량은 논문 라이팅이 주가 되고 있는 상황이기 때문에,

기업의 엔지니어는 대학원 졸업생에게 어떤 역량을 기대할 것 같은지 다시 여쭤봤다.

 

 

회사가 나에게 요구하는 역량, 내가 준비해야할 것

  • 엔지니어링 팀에 지원하더라도 인터뷰에서는 박사 과정에서 주로 했던 분야에 관해 가장 많이 물어볼 것이다
  • 박사 과정 학생이 해야할 일에 집중할 것

대학원 이전에도 인턴십 경험이 조금 있는데, 여름학기에 인턴을 다시 지원하는 것도 괜찮을 것이며,

학부 과정에서 배웠던 CS 기초 내용에 대한 지식의 지속적인 관련성을 강조하시면서 기본기에 충실한 학습을 하라고 하셨다.

라이브 코딩이 이루어지면서 이에 관한 소프트스킬도 연습하면 좋다고 하셨다.

 

추가적으로 박사 과정의 경우 종종 시스템 디자인 설계에 대한 부분까지도 기대하기도 하는데,

이는 실제 경험이 없으면 사실 어려운 일이다. 이런 역량을 쌓기 위해서는 잘 쓰여진 책을 살펴보라는 조언을 들었다.

 

나는 작년 오픈 소스 프로젝트에 코드를 기여하지 못한 것에 대해 아쉬움이 들어 재지원을 고민하고 있었다.

멘토님께서는 그런 기여가 유익하긴 하지만, 박사 과정 학생으로서 해야 할 일에 대한 우선순위를 잘 세우라고 조언을 주셨다.

오픈 소스 작업을 학업과 연계하는 것은 좋다고 하셨다.

 

 

엔지니어링 분야의 업계 동향과 발전 사항

  • 개발자 컨퍼런스나 기술 블로그
  • Hidden Technical Debt in Machine Learning Systems .. etc

평소 최적화를 위해, 그리고 ML 모델 서비스의 최신 동향과 발전을 파악하기 위한 접근 방식에 대해 여쭤봤다.

 

학술 논문은 사실 실험실 환경에서 제안된 솔루션인 경우가 많아 실용적인 엔지니어링 인사이트를 얻는 것이 어려우며,

대신 모니터링 모범 사례 등과 같은 빅 테크에서 정리된 페이퍼는 도움이 될 수 있다고 하셨다.

 

주로 현업 개발자 컨퍼런스나 기술 블로그가 유용한 리소스이며,

특정 주제에 대해 깊게 파고들 때는 책/논문/컨퍼런스 가리지 않고 참고한다고 하셨다.

 

 

 

새해를 맞이하면서 계획을 세우고, 만다라트도 만들고(ㅋㅋ) 여러가지를 준비하느라 바빴다.

해야할 일이 너무 많은것 같아서 부담스럽기도 했다.

멘토님께서는 참고만 하라고 하셨지만, 할 일에 대한 우선순위를 확신을 가지고 정하는데 너무너무 도움이 되는 시간이었다!!

 

흥미롭게도, 과거에는 기업의 ML 시스템에 시스템 수준 최적화가 일반적인 초점이 아니었는데

이제 사용자 디바이스에서 직접 모델을 제공하면서 제한된 환경에서 모델 서빙에 최적화가 많이 이루어지고 있다고 하셨다.

귀중한 인사이트와 여러 조언을 들으며 시간을 내주신 멘토님께 감사했다~ :>

 

💡 요약
- abstract: 컨테이너화된 환경을 위해 설계된 IO 제어 솔루션에 관한 연구
- introduction: 블록 스토리지를 위한 기존의 IO 제어 메커니즘은 스토리지 디바이스의 하드웨어 이질성과 데이터센터에 배포된 워크로드의 특수성을 고려하면서 최신 SSD에 알맞게 낮은 오버헤드로 실행되지 못한다는 문제가 있다.
- related works:
  - IO 제어 및 공정성 분야의 다양한 관련 연구
  - 프로덕션 스토리지 디바이스 전반의 성능 변동성, SSD 성능 예측을 위한 모델링 접근법, 가상 머신 모니터를 위한 IO 솔루션에 대한 연구
  - IO 스택의 여러 계층에 걸친 정보를 고려해야 할 필요성
  - 컨테이너를 위한 리소스 관리 솔루션과 아키텍처 확장
- method: 디바이스별 모델을 사용하여 각 IO 요청의 디바이스 점유율을 추정하는 방식으로 작동한다. => 오프라인 프로파일링, 디바이스 모델링, 새로운 작업 절약형 예산 기부 알고리즘을 활용하여 데이터센터의 다양한 워크로드와 이기종 스토리지 디바이스에 대해 확장 가능하고 작업을 절약하며 오버헤드가 낮은 IO 제어 기능을 제공함
- experiment: Meta의 모든 컨테이너에 IOCost를 배포하여 평가한 결과, IOCost는 최소한의 오버헤드로 비례적이고 작업을 절약하며 메모리 관리를 인식하는 IO 제어 성능이 뛰어났다.
- conclusion & discussion: IOCost는 ZooKeeper 배포, AWS Elastic Block Store/Google Cloud Persistent Disk 등의 원격 스토리지에서도 사용 가능하다.

 

 

Introduction

최근 주요 클라우드 업체들에서 컨테이너 기반 솔루션을 제공하고 있으며,

  • https://aws.amazon.com/ko/containers/
  • https://cloud.google.com/containers?hl=ko
  • https://azure.microsoft.com/en-us/products/container-instances/

프라이빗 데이터센터에서도 컨테이너로 운영되는 추세이다. (Facebook도 전체 데이터센터가 컨테이너로만 운영되고 있다고 함)

 

기존에는 컴퓨팅, 메모리, 네트워크에 대한 리소스 격리에 대해 많은 연구가 이루어져왔는데, 블록 스토리지에 대한 기존 IO 제어 메커니즘도 개선이 필요하다.

  • IO 제어는 데이터센터의 하드웨어 이질성을 고려해야 한다. 디바이스 종류 뿐만 아니라 한 유형 내에서도 지연 시간 및 처리량 측면에서 크게 다른 성능 특성을 가지기도 함.
    • 특히 짧은 순간에 성능을 과도하게 발휘한 후 급격히 느려져 스택 환경에 악영향을 미칠 수 있는 SSD 특성을 고려해야
  • IO 제어는 다양한 애플리케이션의 이질성을 고려해야 한다.
    • 지연 시간에 민감 vs 처리량 vs 순차적 또는 무작위 액세스를 버스트 또는 연속적으로 수행

=> 하드웨어 + 애플리케이션 이질성으로 인해 지연 시간과 처리량 간의 균형점을 파악하는 것이 어렵다

  • IO 격리는 페이지 회수 및 스왑과 같은 메모리 관리 작업과 잘 상호 작용 할 수 있도록 고려되어야 한다.

 

 

Background

1) cgroup: 컨테이너별 리소스 할당을 구성하기 위한 기술

컨테이너 런타임은 리소스 제어 및 격리를 위해 cgroup을 사용한다.

  • cgroup: 컨테이너가 프로세스를 계층적으로 구성하고 계층을 따라 시스템 리소스를 제어 및 구성 가능한 방식으로 배포하기 위한 기본 메커니즘
    • 개별 cgroup이 계층 구조를 형성하고, 프로세스는 하나의 cgroup에 속한다.
    • cgroup 컨트롤러는 주로 가중치를 활용하여 CPU, 메모리 및 IO와 같은 특정 시스템 리소스를 트리를 따라 배포한다.
      • 모든 형제 cgroup의 가중치를 합산하고 각각에 합계에 대한 가중치의 비율을 부여하여 리소스를 배포

2) Linux 블록 레이어와 기존 IO 제어 솔루션

Linux Device Driver - Block Device Layer

애플리케이션과 파일시스템은 블록 계층을 사용하여 블록 디바이스에 접근한다.

그림과 같이 userspace에서 시스템 호출을 통해 커널과 상호 작용하며, 파일시스템에 대한 읽기 및 쓰기 작업은 파일시스템 IO(FS IO)로 블록 레이어로 전달된다.

블록 레이어는 bio 데이터 구조를 사용하여 아래와 같은 다양한 정보를 전달한다.

  • 요청 유형: read/write
  • 크기
  • 대상 장치
  • 장치의 섹터 오프셋
  • 요청한 cgroup
  • 데이터를 복사하거나 복사할 메모리 등의 정보

Linux 커널에는 활성화할 수 있는 여러 가지 IO 스케줄러가 있다.

  • cgroup 제어가 없는 IO 스케줄링: 컨테이너에 특정 IO 리소스를 보장하기보다는 일반적인 성능 속성을 유지하는 데 우선순위를 둔다. 전체 시스템 성능을 유지하기 위해 일반적으로 느린 비동기 read가 일반적으로 빠르고 즉각적인 응답이 필요한 동기 read보다 우선권을 갖지 못하도록 한다.
    • 스케줄러 없는 경우
    • mq-deadline
    • kyber 
  • blk-throttle: read/write IOPS 또는 초당 바이트의 형태로 IO 제한을 설정할 수 있으나, 유휴 상태이거나 사용하지 않는 리소스를 필요로 하는 컨테이너 또는 워크로드에 할당할 수 없다는 단점이 있다.
  • BFQ: IO 리소스를 비례적으로 제어할 수 있으나, 메모리 관리와의 상호 작용을 간과하여 잠재적으로 격리 문제를 일으킬 수 있다. 요청당 오버헤드가 높고 지연 시간 편차가 크며, 컨테이너당 read/write 섹터를 기반으로 하는 라운드 로빈 스케줄링은 내부 작업이 복잡한 최신 디바이스에는 효과적이지 않다는 단점이 있다.
  • IOLatency: 개별 cgroup에 대한 IO 레이턴시 목표를 설정할 수 있으며, 다른 cgroup의 IO가 레이턴시 목표를 초과하면 이전 cgroup이 호출된다. IOLatency는 주로 엄격한 우선순위를 가진 컨테이너 간에 스케줄링은 적합하지만, 동일한 우선순위를 가진 컨테이너에 대해 비례 제어가 부족하여 워크로드 간의 공평성을 보장하기 어렵다는 한계가 있다.

3) 이기종 블록 장치와 워크로드가 있는 최신 데이터센터의 컨텍스트

Meta의 데이터센터에는 다양한 SSD가 있고, SSD간에 하드웨어 디바이스적 성능 특성이 매우 이질적이다.

  • H: 낮은 레이턴시에서 높은 IOPS를 달성
  • G: 낮은 IOPS와 상대적으로 낮은 레이턴시를 제공
  • A: 높은 레이턴시와 함께 중간 정도의 IOPS를 제공

Meta의 애플리케이션을 분석해보면 IO 워크로드가 매우 다양함을 알 수 있다.

  • Web A /B: 가장 일반적인 Meta 워크로드로, 읽기 및 쓰기가 무작위 및 순차 작업 측면에서 거의 동일하게 혼합되어 있다.
  • serverless: 할당용량을 초과하는 경우가 많으며, 읽기 및 쓰기 양이 거의 동일하게 혼합되어 있다.
  • Cache A/B: 인메모리 캐시를 위한 백업 저장소로 고속 블록 장치를 사용하는 인메모리 캐싱 서비스로, 두 캐시 모두 많은 양의 순차적 IO로 이루어져있다. (주로 스토리지 관련 작업을 수행)

 

 

Related Work

  • 기존 연구는 대부분 하이퍼바이저를 개선하기 위함 + VM 기반 가상화 환경에 중점
    • 단일 공유 운영 체제, 메모리 하위 시스템과 IO의 상호 작용, 고도로 스택된 배포와 같은 컨테이너의 복잡성을 고려하지 않는다는 한계점이 존재한다.
  • Linux 커널의 IO 제어는 BFQ 또는 최대 대역폭 사용량(IOPS 또는 바이트) 기반의 제한에 의존
    • BFQ: 우선순위와 요청량에 따라 각 프로세스에 디스크 시간 예산을 할당하여 여러 프로세스 또는 애플리케이션 간에 디스크 대역폭을 공정하게 할당하는 것을 목표로 하는 알고리즘 => 특정 프로세스가 디스크 독점 방지/모든 프로세스가 공평하게 IO 리소스를 할당받을 수 있도록 함
    • 디바이스의 유휴 자원을 모두 활용하지 못하거나, 빠른 저장장치에 과도한 성능 오버헤드를 추가하는 한계점
  • 기존의 CPU 스케줄링은 가중 공정 큐잉과 같은 기술을 사용하여 CPU 사용 시간을 측정하여 CPU 점유율을 비례적으로 배분했는데, I/O 제어에 사용되는 IOPS나 바이트와 같은 지표는 특히 블록 디바이스의 다양성을 고려할 때 점유율을 측정하기 좋지 않다.
    • 최신 블록 디바이스는 내부 버퍼링과 가비지 컬렉션과 같은 복잡한 지연 작업에 크게 의존하므로, 디바이스 시간 공유에 의존하거나 주로 IOPS 또는 바이트를 기반의 공정성 보장 기술에 문제가 있다.

 

 

Method

IOCost는 디바이스의 복잡성과 무관하게 워크로드를 구성하기 위해 디바이스와 워크로드 구성을 분리한다.

  • 각 디바이스에 대해 IOCost는 비용 모델과 디바이스 동작을 정의하고 규제하는 일련의 서비스 품질(QoS) 파라미터를 설정
  • 각 워크로드에 대해 IOCost는 비례 구성을 위해 cgroup 가중치(hweight = 해당 cgroup이 받을 수 있는 IO 디바이스의 최종적인 비중)를 활용
  • 지연 시간이 짧은 issue path 와 주기적으로 동작하는 planning path 를 분리

1) IO 디바이스 점유율을 추정하는 모델링: 개별 bio 작업의 점유를 추정 => 이 점유 추정치를 사용하여 각 cgroup에 할당된 가중치에 따라 스케줄링 결정을 내린다.

  • 선형 모델을 사용하여 읽기/쓰기 초당 바이트 수, 초당 4kB 순차 및 랜덤 IO(IOPS) 등의 매개변수를 기반으로 IO 작업 비용을 계산한다.
    • fio 오픈소스 도구를 이용하여 다양한 블록 크기 및 액세스 패턴(순차 또는 임의), 읽기/쓰기과 같은 다양한 유형의 I/O 워크로드를 시뮬레이션하여 평가함
  • 단순한 선형 모델링만으로는 복잡한 캐싱 메커니즘, 요청 재정렬, 가비지 컬렉션이 있는 최신 SSD의 복잡성을 포착하기에 충분하지 않으므로, 모델의 부정확성을 보완하기 위해 IOCost는 cgroup 사용량과 IO 완료 대기 시간에 대한 실시간 통계를 기반으로 IO 제어를 런타임에 조정한다.
    • 요청 시 자원부족 및 지연 시간 목표 위반을 추적하여(디바이스 포화 상태를 파악) 디바이스로 전송되는 총 IO를 제한하고 QoS 매개변수를 사용하여 디바이스 동작을 규제함 => 일관된 지연 시간 제어
    • QoS 매개변수: 가상 워크로드(ResourceControlBench)를 사용하여 지연에 민감한 서비스의 동작을 관찰하고 그에 따라 QoS 매개변수를 조정하는 체계적인 접근 방식을 개발했다. 다양한 전송률 범위에서 성능을 분석하여 IO 제어를 위한 최적의 지점을 파악 => 일관된 지연 시간을 달성하기 위해 디바이스를 스로틀링하는 방법을 결정

2) 디바이스의 유휴자원을 모두 활용할 수 있는 작업 보존 알고리즘 => 컨테이너가 과도한 메모리를 사용할 때 다른 컨테이너가 불이익을 받는 리소스 격리 문제 해결 = 컨테이너에 대한 블록 IO 제어

  • 개별 cgroup에 할당된 IO 용량을 완전히 활용하지 못하는 경우에는 해당 cgroup의 가중치를 동적으로 낮춤으로써 다른 cgroup이 장치를 활용하고 사용하지 않은 리소스를을 공유할 수 있도록 하여 효율적인 리소스 활용을 보장한다.

3) 런타임 오버헤드를 최소화하기 위해 IO 제어를 빠른 IO별 이슈 경로와 느린 주기적 계획 경로로 분리

  • wall clock time: 작업을 수행하는 데 걸리는 실제 시간으로, 측정된 작업 완료 시간은 당시 시스템에서 수행 중인 다른 작업(CPU, I/O, Sub Program 등)의 영향을 받을 수 있다.
  • vtime(virtual time rate): Wall clock time과 함께 진행되는 글로벌 가상 시간으로, IOCost 시스템에서 각 cgroup 내의 IO 작업의 진행 상황을 추적하고 관리하기 위해 사용되는 개념이다.
    • 로컬 vtime: 특정 cgroup 내 IO 작업의 진행 상황을 나타낸다. 각 cgroup의 IO 작업에 따라 증가하며, 증가하는 속도는 해당 IO 작업의 상대적 비용(동일한 cgroup 내의 다른 작업과 비교하여 IO 작업이 얼마나 많은 리소스 또는 시간을 필요로 하는지)에 의해 결정됨
    • 글로벌 vtime: 모든 cgroup의 모든 IO 작업에 대한 전반적인 진행 상황을 나타낸다. 이는 가상 시간 속도(vrate)에 의해 결정된 Wall clock time과 함께 진행되는 가상 시간이다
    • 스로틀링 결정은 cgroup의 로컬 vtime과 글로벌 vtime 간의 차이(=cgroup의 현재 IO 예산)를 비교하여 이루어지며, 이를 통해 IO를 즉시 실행할 수 있는지 또는 할당된 예산 내에서 유지하기 위해 추가 진행 상황을 기다려야 하는지를 결정함
      • group의 현재 IO 예산이 IO의 상대적 비용을 충당하기에 충분하면 즉시 실행되고, 그렇지 않으면 글로벌 vtime이 더 진행될 때까지 IO가 대기한다.
  • issue path: 로컬 및 글로벌 vtime에 따라 IO 작업의 비용을 결정하고, features에 따라 비용모델을 이용하여 bio의 절대 비용을 계산하고, 스로틀링 결정을 내린다. 각 cgroup에는 가중치(형제 그룹에서의 IO 점유 비율)가 할당되며, 이러한 가중치는 hweight로 재계산되어 캐싱된다.
    • IOCost는 활성 cgroup과 비활성 cgroup을 구분하여, 유휴 cgroup의 남는 자원을 활성 cgroup에서 사용할 수 있도록 한다. 만약 활성/비활성 cgroup의 상태가 변경되면 hweight 가중치를 재계산한다.
      • 활성 cgroup: IO 요청이 있는 경우
      • 비활성 cgroup: IO 없이 전체 planning 기간(= IO 제어 결정이 내려지는 전체 기간)이 지나는 경우로, 가중치 계산 시 무시된다.
  • planning path: 글로벌 오케스트레이션을 담당하며, 데이터센터의 각 cgroup 운영을 조정하여 IO 리소스를 효율적이고 공정하게 분배할 수 있도록 한다. 주기적으로 각 cgroup의 IO 사용량을 평가하고, 그에 따라 가중치를 조정한다. 또한 서비스 품질(QoS) 매개변수를 통해 vrate를 수정하여 cgroup 당 발생하는 IO 요청량을 조정한다.

 

 

Evaluation

다양한 유형의 SSD가 장착된 단일 소켓 64GB 서버에서 테스트 수행

1) 데이터 센터의 고속 SSD에 대한 IO를 제어할 때는 오버헤드를 최소화하는 것이 중요하다.

=> 최대 초당 입출력 작업 수(IOPS)를 측정하는 실험

  • none = 소프트웨어 스케줄러나 컨트롤러가 실행되지 않은 상태로, 디바이스에서 블록 레이어의 달성 가능한 최대 처리량
  • IOCost: issue path/planning path로 분할하므로, 복잡한 스로틀링 로직을 가지고 있음에도 오버헤드가 발생하지 않는다.

2) 유휴 자원의 효율적 활용을 보장하는 것이 중요하다.

=> 서로 다른 우선순위를 가진 워크로드에 대해 요청 지연 시간의 특정 임계값을 초과하지 않고 원활하게 실행되는지 평가

  • cgroup 인식 IO 제어 메커니즘의 성능 => High:Low = 2:1 이 최적의 분배일때
  • IOCost: 예상되는 2:1 비율과 정확히 일치하여 비례 제어에 효과적임을 입증

=> 서로 다른 우선순위를 가진 워크로드에 대해 우선순위 및 지연 시간 요구 사항을 고려하면서, 얼마나 리소스를 비례적으로 잘 배분할 수 있는지 평가

** think time: 다음 연속적인 요청을 실행하기까지 대기/지연 시간 = 새로운 요청이 발생하지 않는 유휴시간

  • IOCost: 우선순위가 높은 워크로드의 레이턴시를 효과적으로 관리하면서, 우선순위가 낮은 워크로드가 유휴자원을 활용할 수 있도록 허용함을 입증

3) 다양한 워크로드 간의 공정성과 적절한 격리를 보장하는 것이 중요하다.

=> 각 메커니즘이 디바이스 점유 측면에서 얼마나 공정성을 제공하고 우선순위가 높은 워크로드와 낮은 워크로드 간에 원하는 비율을 유지하는지를 평가

  • IOCost: random/sequential IO의 비용을 모델링하고 디바이스 점유 측면에서 공정성을 보장함으로써 모든 시나리오에서 원하는 2:1의 비율을 유지한다. 

4) 최신 SSD의 경우, 단순한 모델링 접근 방식은 부정확하기 때문에 이를 효과적으로 보정하는 것이 중요하다.

=> SSD 워크로드에 대해 원하는 서비스 품질(QoS)을 유지하는 데 있어 IOCost의 동적 조정(vrate)의 효과를 평가

  • 처음에는 vrate가 약 100으로 유지
  • 0.5x of model(모델 매개변수 값을 감소 = 장치의 점유량이 이전보다 절반으로 줄어드는 경우): IOPs(읽기 속도)가 떨어지지만, QoS를 유지하며 발급 속도를 약 두 배로 상승시켜 IOPs를 유지한다.
  • 2x of model(모델 매개변수 값을 증가 = 디바이스의 점유량이 이전보다 두 배로 늘어나는 경우): 처음에는 디바이스를 과도하게 포화시켜 지연 시간이 급증하지만, QoS를 유지하기 위해 vrate가 초기 값의 약 절반으로 떨어지면서 지연 시간이 감소한다.

5) 효율적인 리소스 확보를 위해 메모리 관리 통합이 중요하다. (우선순위가 높은 워크로드에 리소스를 보장하면서 우선순위가 낮은 워크로드가 나머지 리소스를 활용할 수 있도록 리소스를 적절하게 회수하기 위해)

=> 여러 조건에서 웹 서버 처리량을 평가

  • IOCost: 메모리 누수 프로세스와 같은 까다로운 조건에서도 웹 서버 처리량을 일정하게 유지

=> 오버커밋 환경에서 IOCost의 성능을 평가

  • ramp up time: 본 실험에서 최대 부하의 40% -> 80%로 확장하는 데 걸리는 시간
  • IOCost: BFQ보다 약 5배 빠르게 스케일업을 완료할 수 있다

6) 여러 컨테이너 간에 IO 서비스의 공정한 할당 중요하다.

=>  Zookeeper와 유사한 워크로드에서 노이즈가 많은 이웃 앙상블과 스냅샷의 영향을 얼마나 잘 차단하는지, 그리고 읽기 및 쓰기 작업에 대한 레이턴시 서비스 수준 목표(SLO)를 충족할 수 있는지 평가

  • IOCost: 노이즈가 많은 이웃 앙상블과 스냅샷의 영향을 효과적으로 격리하여 서비스의 레이턴시 SLO 위반을 최소화한다.
    • SLO 위반이 1.5s, 1.04s 두번만 발생함

7) 퍼블릭 클라우드와 같은 원격 블록 스토리지 환경에도 적용 할 수 있어야한다.

=> 로컬 스토리지와 원격 연결 스토리지를 포함한 다양한 구성에서(AWS 및 Google Cloud와 같은 퍼블릭 클라우드) IOCost가 IO를 얼마나 잘 격리할 수 있는지 평가

  • 지연 시간에 민감한 워크로드(ResourceControlBench)를 AWS Elastic Block Store(gp3-3000iops, io2-64000iops) 및 Google Cloud persistent disk(balanced, SSD)를 사용하여 다양한 구성에서 메모리 누수 워크로드와 함께 배치
  • IOCost를 적용하지 않은 경우: 두 워크로드 간에 간섭이 발생하여 RPS(초당 요청)가 감소
  • IOCost가 적용된 경우: 메모리 누수 워크로드로 인한 간섭으로부터 ResourceControlBench를 효과적으로 격리하고 보호하여 RPS가 높은 값으로 나타난다.

8) 경합이 심한 조건에서도 시스템 서비스 및 워크로드 간에 IO 리소스를 공정하게 분배(비례 제어 기능)할 수 있어야한다.

** IO 고갈로 인한 통신 장애 및 성능 저하가 발생할 수 있는 데이터센터의 패키지 가져오기 장애/컨테이너 정리 장애에 유용

=> 비례 제어 기능을 갖춘 IOCost가 이러한 장애 발생을 크게 줄이고 시스템 안정성과 리소스 관리를 개선할 수 있음을 입증

- package fetching: 컨테이너화된 애플리케이션에 필요한 소프트웨어 패키지 또는 종속성을 검색하는 프로세스 => IO 고갈(충분한 입출력 리소스 부족)이 발생하면 패키지 검색에 실패하고 후속 컨테이너 업데이트에 실패할 수 있다.

  • IOCost가 활성화되면 해당 리전에서 package fetching 오류의 비율이 감소하여 IOLatency와 비교했을 때 오류가 10배 감소

- container cleanup: 데이터센터 환경에서 오래된 컨테이너를 제거하거나 삭제하는 프로세스 => 리소스 경합이나 기타 요인으로 인해 이 작업 중에 IO 고갈이 발생하면 오래된 컨테이너 정리가 지연되어 성능이 저하되거나 충분한 디스크 공간을 확보할 수 없을 수 있다.

  • IOCost가 활성화되면 container cleanup 실패 발생이 IOLatency에 비해 3배 감소

 

 

Discussion

요즘 연구해보고 싶은 분야와 비슷해서 정말 많이 도움됐다.

컨테이너 기술 동향에 대해서도 살펴볼 수 있었다.

** 컨테이너를 위한 아키텍처 및 OS 확장에 관한 연구

  • D. Skarlatos, Q. Chen, J. Chen, T. Xu and J. Torrellas, "Draco: Architectural and Operating System Support for System Call Security," 2020 53rd Annual IEEE/ACM International Symposium on Microarchitecture (MICRO), Athens, Greece, 2020, pp. 42-57, doi: 10.1109/MICRO50266.2020.00017.
  • D. Skarlatos, U. Darbaz, B. Gopireddy, N. Sung Kim and J. Torrellas, "BabelFish: Fusing Address Translations for Containers," in IEEE Micro, vol. 41, no. 3, pp. 57-62, 1 May-June 2021, doi: 10.1109/MM.2021.3073194.

 

 

💡 요약
- abstract: 컨테이너 라이브 마이그레이션의 사전 복사 알고리즘에 더티 페이지가 반복적으로 전송되는 문제를 해결하기 위한 연구
- introduction: 라이브 마이그레이션에서 가장 널리 사용되는 기술인 사전 복사 알고리즘은 메모리 집약적인 시나리오에서 메모리 페이지의 복사를 반복하기 때문에 전송되는 데이터의 양과 총 마이그레이션 시간이 늘어날 뿐만 아니라 반복 프로세스가 수렴되지 않아 마이그레이션이 실패할 수 있다는 문제가 있다.
- related works: 
- method: 프로그램 운영체제의 locality 원칙을 활용하고, 더티 페이지 예측을 위해 랜덤 포레스트 모델을 사용하여 반복되는 더티 페이지 전송을 건너뛴다. 페이지 압축과 함께 incremental page만 전송하여 기존의 사전 복사 방식에 비해 데이터 전송 및 마이그레이션 시간을 크게 단축했다. => 메모리 페이지와 인접한 페이지의 더티 레코드를 수집하고 머신러닝 모델을 사용하여 향후 페이지가 더티화될지 여부를 예측함
- experiment: 더티 페이지가 적게/많이 발생하는 환경에서 제안하는 알고리즘은 가장 최적의 결과를 보여주었다. 
- conclusion & discussion: ML 접근법을 사용하여 컨테이너 라이브 마이그레이션을 최적화하기 위한 알고리즘을 설계했으며, 전송 데이터를 줄이기 위해 압축 알고리즘을 함께 사용했다.

 

 

 

Introduction

  • 가상화
    • 가상 머신
    • 컨테이너 가상화:
      • 경량, 휴대성, 빠른 시작 속도
      • Docker, LXC, Podman 
  • 서비스 마이그레이션: 가상시스템을 다른 곳으로 이동
    • 콜드 마이그레이션: 전원이 꺼져 있거나 일시 중단된 가상 시스템을 새 호스트로 이동
    • 라이브 마이그레이션 (=핫 마이그레이션): 전원이 켜진 가상 시스템을 새 호스트로 이동
      • 마이그레이션 과정에서 서비스가 영향을 받지 않도록 하는 것을 목표로 한다.
  • 사전 복사(pre-copy) 알고리즘: 라이브 마이그레이션에서 가장 널리 사용되는 기술로, 컨테이너가 실행되는 동안 메모리 페이지가 소스에서 대상으로 전송된다.
    • 사전 복사는 일반적으로 남아 있는 더티 페이지 수가 충분히 낮을 때 종료된다. => 메모리 집약적인 시나리오에서 사전 복사 알고리즘은 반복 단계에서 메모리 페이지의 복사를 반복하기 때문에 전송되는 데이터의 양과 총 마이그레이션 시간이 늘어날 뿐만 아니라 반복 프로세스가 수렴되지 않아 마이그레이션이 실패할 수 있다.
    • c.f. Post-copy는 네트워크를 통해 각 페이지를 한 번만 전송하는 반면, Pre-copy는 마이그레이션 중에 페이지가 소스에서 반복적으로 write가 발생하는 경우 동일한 페이지를 여러 번 전송할 수 있다.
      • Pre-copy: 마이그레이션 중에 source에서 VM 상태를 최신 상태로 유지
      • Post-copy: source와 destination 간에 VM 상태를 분할
      • 실시간 마이그레이션 중에 대상이 실패하면 사전 복사는 VM을 복구할 수 있지만 사후 복사는 복구할 수 없다

 

Related Work

  • pre-copy: 더티 메모리 페이지를 반복적으로 복사하여 가상 머신 다운타임을 단축, 부하가 높고 메모리 쓰기 집약적인 시나리오에서는 반복 단계에서 일부 메모리 페이지가 자주 수정되어 매우 더티한 일부 메모리 페이지가 반복적으로 전송될 수 있어서 전송되는 데이터의 양을 증가시킬 뿐만 아니라 반복 프로세스가 수렴하지 못하는 원인이 될 수 있다
    • 페이지 중복 전송 문제를 피하기 위해, 자주 더럽혀지는 페이지를 추적하여 전송 종료 단계에 저장하는 재사용 거리 개념 제안
    • 마르코프 예측 모델을 사용하여 메모리 페이지의 기록된 과거 동작을 사용하여 상태 전이 행렬을 통해 페이지가 다시 수정될 확률을 계산하고 예측 확률이 낮은 페이지만 전송하는 방법을 제안
    • related dirty rate에 기반한 예측 방법을 제안했다. 페이지의 수정 확률 P를 실제 수정 시간 R과 결합하여 관련 더티 페이지 비율 PR을 얻은 후, R과 PR이 모두 설정된 임계값보다 큰 경우에만 페이지 전송을 건너뛴다.
    • => 페이지의 시간적 상관관계만 고려할 뿐 공간적 상관관계는 고려하지 않음
    • 마이그레이션 과정에서 메모리 더티 페이지의 생성 속도를 줄이기 위해 CPU 스케줄링을 사용하여 마이그레이션의 원활한 진행을 보장함 => 필연적으로 애플리케이션 성능 저하로 이어질 수 있으며 여러 번의 반복이 완전히 해결되지 않음
    • 대역폭 인식 기반의 데이터 압축 방법을 제안하여, 마이그레이션 대역폭에 따라 적절한 압축 알고리즘을 동적으로 선택하여 전송할 페이지를 압축한 후 목적지로 전송함으로써 전송 데이터의 양을 줄이고 전체 마이그레이션 시간을 단축함
  • post-copy
  • hybrid copy
  • 로그 기반 추적
  • 복제 마이그레이션

 

가상 머신에 비해 컨테이너 라이브 마이그레이션에 대한 연구는 상대적으로 적다.

  • Virtuozzo: 컨테이너에서 라이브 마이그레이션을 지원한다. 마이그레이션을 수행하는 데 필요한 주요 프로세스와 기능을 커널에 통합하여 구현하기 때문에 Virtuozzo 커널은 고도로 커스터마이징된 커널로, 메인스트림 Linux 커널에서 동일한 기능을 구현하기 위해 Virtuozzo는 CRIU 프로젝트를 설립했다.
    • CRIU: Linux에서 실행되는 오픈 소스 소프트웨어 도구로, 사용자 공간에서 프로세스를 체크포인트 및 복원할 수 있다. 즉, 체크포인트 메커니즘을 사용하여 실행 중인 애플리케이션을 동결하고 해당 상태 정보를 디스크에 이미지 파일로 덤프한 다음, 이 이미지 파일을 사용하여 복원 작업을 수행하여 동결된 지점에서 복구하고 프로그램을 다시 실행할 수 있다.
    • => 현재 Docker에는 컨테이너 마이그레이션 작업을 지원하는 통합 CRIU 도구가 있지만, 이 접근 방식은 상태 저장 컨테이너 마이그레이션을 지원할 수 없으며 과도한 다운타임을 초래한다.
  • P.Haul: Docker 컨테이너의 외부 마이그레이션을 가능하게 하는 OpenVZ에서 시작한 프로젝트로, 외부 마이그레이션은 P.Haul이 CRIU를 통해 Docker 컨테이너 내 프로세스의 실시간 마이그레이션만 구현하기 때문에, 이 방식은 destination 단의 Docker 엔진이 컨테이너를 관리할 수 있는 기능을 상실하게 된다.
  • 중첩 가상화와 컨테이너 마이그레이션을 통해 메모리 공간을 리매핑하여 동일한 호스트에서 컨테이너 메모리 마이그레이션을 최적화 => 호스트 간 컨테이너 마이그레이션을 지원하지 않는다.
  • NFS를 사용하여 데이터 전송을 용이하게 하는 방법도 있는데, 이는 메모리 집약적인 워크로드를 마이그레이션할 때 긴 다운타임을 유발한다.
  • 파일 시스템 공유 방식을 사용하여 포스트카피와 파일 시스템 유니온을 기반으로 실시간 마이그레이션을 하는 방법도 있다.

 

Method

  • original pre-copy algorithm: 
    • 반복 마이그레이션 단계: 첫 번째 라운드에서 모든 메모리 페이지를 대상 호스트로 전송하고, 이후 각 라운드에서는 이전 라운드 반복 후 수정된 메모리 페이지(더티 페이지)만 전송한다. 셧다운 마이그레이션 조건이 충족될 때까지 이 반복 프로세스를 반복한다.
    • 셧다운 마이그레이션 단계: source 호스트는 컨테이너를 잠시 중지하고, 더티 메모리 페이지의 마지막 라운드와 CPU 등의 기타 상태 정보를 대상 호스트에 전송한 후, 이러한 정보에 따라 destination 호스트에서 컨테이너를 재시작한다.
    • => 메모리 집약적인 쓰기 부하에서는 반복적인 마이그레이션 단계에서 일부 메모리 페이지가 자주 쓰기 되어 페이지 전송이 반복됨

 

  1. 일정 기간 동안 컨테이너 메모리의 변경 사항을 수집
  2. 수집한 메모리 더티 데이터를 사용하여 생성된 더티 페이지를 예측 => 더티 페이지가 많은 페이지의 전송을 생략하고 페이지 압축을 사용하여 데이터 전송을 줄인다.
  3. (반복적인 복사본 마이그레이션 단계) 페이지 예측과 페이지 압축을 사용하여 더티 페이지의 전송을 최적화한다. 최대 반복 횟수에 도달하거나 더티 페이지 데이터 크기가 수렴하는 등 셧다운 마이그레이션 조건이 충족되면 셧다운 마이그레이션 단계로 진입
  4. 셧다운 마이그레이션 단계로, source 호스트는 컨테이너 실행을 중지하고 컨테이너의 남은 더티 페이지를 압축한 후 CPU 및 기타 상태 정보와 함께 대상 호스트에 전송
  5. 컨테이너 복구 단계로, destination 호스트는 수신한 메모리 데이터 정보에 따라 컨테이너의 동작을 재개

 

더티 페이지의 반복 전송 문제를 해결하기 위해

  • 더티 페이지 예측: 일정 기간 동안 컨테이너 메모리의 변경 사항을 수집하여 더티 페이지가 향후에도 계속 더티 페이지가 될 확률을 예측하여 더티 비율이 낮은 페이지를 전송할 페이지 세트에 추가하고 더티 비율이 높은 페이지의 전송을 건너 뛴다.
    • 기존의 더티 페이지 예측 방법은 대부분 시계열 예측 방법을 사용하는데, 본 논문에서는 기존 방식과 달리 머신러닝 방식을 사용한다.
      • 특징 추출 방법: 지역성 원리에 기반한 특징 선택
        • 시간적 지역성: 현재 더럽혀진 페이지가 가까운 미래에 다시 더럽혀질 가능성이 높다는 것을 의미
        • 공간적 지역성: 현재 더럽혀진 페이지에 인접한 주소 페이지가 가까운 미래에 더럽혀질 가능성이 높다는 것을 의미
      • 오프라인에서 모델을 학습하는 데 사용하여 컨테이너의 온라인 마이그레이션에 적용한다. 
        • 랜덤 포레스트: 과적합이 쉽지 않고 일반화 능력이 높기 때문에 예측 모델로 선택했으며, 특징 윈도우 m*n이 증가함에 따라 모델 예측 정확도가 증가하는 것을 확인함
        • 실제 컨테이너 마이그레이션에서는 피처 윈도우가 커질수록 예측 시간 오버헤드가 발생하게 되므로, 본 논문에서는 예측 효과와 예측 시간의 균형을 맞추기 위해 피처 윈도우 m=5, n=5로 고정

더티 페이지 예측 방법을 기반으로 설계한 전송 페이지 선택 알고리즘

  • PageList: 컨테이너 메모리에 있는 모든 페이지의 집합 => 각 반복 후 관련 페이지 정보를 업데이트해야 한다.
  • Dirty: 이번 라운드의 더티 페이지 집합
  • ToTransmit: 이번 반복에서 최종적으로 전송할 페이지의 집합
  • ToSkip: 반복 중에 전송을 건너뛰는 페이지의 집합
  • RFModel: 랜덤 포레스트 예측 모델로, 예측이 1이면 향후에도 페이지가 dirty 할 것을 의미한다. 

각 반복 라운드에서 랜덤 포레스트 모델을 사용하여 현재 더티 페이지가 1로 예측되면 현재 라운드에서 해당 페이지의 전송을 건너뛰고 ToSkip에 추가하고, 예측이 0이면 이번 라운드에서 페이지가 destination에 전송된다.

이번 라운드의 클린 페이지의 경우 이전 반복에서 전송을 건너뛰고 ToSkip에 저장한 경우 이번 라운드에서 제 시간에 대상에 전송해야 한다.

 

 

  • 전송되는 메모리 페이지 압축: 전송할 페이지를 압축하여 네트워크에서 전송되는 데이터의 양을 더욱 줄인다.
    • 페이지를 전송할 때 페이지 단위가 아닌 바이트 단위로 전송한다. 한 반복 내에서 전송할 더티 메모리 페이지가 처음 전송되는지 여부를 확인하고, 처음 전송되는 페이지가 아니면 지난번에 destination으로 전송한 페이지 데이터와 XOR하여 페이지 증분값을 구한 뒤 페이지 증분값을 압축하여 전송한다.
      • 증분 압축의 장점은 페이지 증분에는 반복되는 0이 많이 포함되어 페이지의 압축률을 향상시키는 데 도움이 된다는 것이다.
      • 압축 알고리즘은 LZ4 압축 알고리즘을 선택했으며, 이는 무손실 압축 알고리즘으로 압축 속도가 빨라 메모리 데이터 압축 장면에 적합하고 압축 비율과 압축 속도를 넓은 범위에서 조정할 수 있다.
    • 데이터를 수신한 destination에서는 압축을 풀고 다시 XOR하여 이번 라운드의 실제 메모리 페이지 데이터를 얻는다.

 

 

Experiment

실험 환경: 두 개의 가상 머신에서 Docker 컨테이너의 라이브 마이그레이션을 테스트

  • Ubuntu 18.04 LTS, CPU 코어 2개, 8GB RAM 및 50GB 디스크
  • 두 가상 머신 간의 네트워크 대역폭은 100Mbps

 

컨테이너가 실행 중일 때 다양한 메모리 쓰기 부하를 시뮬레이션하기 위해 모든 크기의 연속 메모리를 신청하고 연속 쓰기 작업을 수행할 수 있는 MemChange 프로그램을 작성하고 이를 컨테이너화했다. 

사전 복사 알고리즘은 메모리 더티율이 전송 대역폭을 초과할 수 없어야 하며, 그렇지 않으면 반복 프로세스가 수렴하지 않거나 심지어 중지되므로 실험 환경의 네트워크 대역폭 100Mbps에 따라 두 가지 메모리 더티 레이트 부하를 설계했다. 
- 메모리 저속 부하: 메모리 더티율 1-2M/s 
- 메모리 고속 부하: 메모리 더티율 11~12M/s 

본 논문에서 제안한 마이그레이션 방법 = MBDPC

예측 알고리즘의 성능을 객관적으로 비교하기 위해 MBDPC에서 압축 알고리즘을 제거한 알고리즘 = MBDP

 

1. 더티 페이지 예측 비교

마르코프, PR 예측 알고리즘과 비교했을 때, 본 논문에서 제안하는 더티페이지 예측 알고리즘의 AUC 값이 약 17% 증가하여 더티 페이지 예측 효과가 더 우수하다는 것을 알 수 있다.

  • x축: FPR(오탐률)
  • y축: TPR(진탐률)
  • AUC: 곡선 아래 영역으로, 모델의 전반적인 랭킹 능력을 반영한다. 넓을수록 좋음

 

2. 다운 타임: 컨테이너가 중지된 후 서비스 복구까지 걸리는 시간 = T stop + T restore

그림은 다양한 메모리 크기일 때, 저속부하에서의 다운 타임을 보여주는데, 기존 사전 복사 알고리즘과 비교했을 때, 마르코프와 PR은 평균 1.1%, 1.3% 감소한 반면, MBDP와 MBDPC는 평균 2.1%, 2.8% 감소했다.

즉, 컨테이너 메모리가 작을 경우 마르코프, PR, MBDP, MBDPC는 최적화 효과가 거의 없는데, = 저속 시나리오에서는 컨테이너 메모리의 수정 속도가 낮기 때문에 1~2회 반복하면 더티 페이지 수가 즉시 종료 조건을 충족하고 종료 마이그레이션 단계로 들어감 (메모리 크기와 메모리 더티율이 낮은 경우 예측 알고리즘에는 일정한 한계가 있다)

 


다양한 메모리 크기에서 메모리 고속 부하의 다운타임을 살펴보면, 메모리 더티율이 높을 때 마르코프와 PR은 원본 사전 복사본에 비해 평균 3.3%와 4.1% 감소한 반면, MBDP와 MBDPC는 평균 3.7%와 34.9% 감소했다.

페이지 예측은 반복 프로세스의 속도를 높이고 셧다운 마이그레이션 단계에서 더티 페이지의 수를 줄일 수 있음을 알 수 있으며, MBDPC는 반복 프로세스의 속도를 더 빠르게 할 뿐만 아니라 페이지 압축을 통해 종료 단계에서 전송 시간 Tstop을 크게 단축할 수 있었다.

 

3. Total Migration Time: 첫 번째 iteration 부터 대상 컨테이너가 컨테이너를 재시작할 때까지의 시간 = ∑T iterative(i) + T stop + T restore

  • 저속 부하에서는 페이지 더티율이 낮음
    • 마코프와 PR이 원본 프리카피에 비해 총 마이그레이션 시간을 단축하는 효과가 적다
    • MBDP와 MBDPC는 평균 3%, 16.5% 단축하여 최적화 효과가 더 높다
  • 고속 로드에서는 페이지 더티율이 높다
    • 원래의 사전 복사 알고리즘은 높은 더티 페이지를 반복적으로 전송하여 라운드당 더티 페이지 수가 종료 임계값보다 낮아질 수 없다. 따라서 반복 시간 n i=1 Titerative(i)가 길어지고 총 마이그레이션 시간이 증가한다.
    • 원본 사전 복사본과 비교하여 예측 알고리즘은 매우 더티한 페이지의 반복 전송을 피함으로써 반복 시간을 줄일 수 있다.

=> 메모리 더티율이 높을 경우 MBDPC가 총 마이그레이션 시간을 더 잘 줄일 수 있다.

 

 

4. 총 전송 데이터: 전체 마이그레이션 과정에서 전송된 데이터의 총량 = TD iterative(i) + TD stop

  • 앞선 내용들과 마찬가지로 메모리 더티율이 낮으면 iteration 프로세스가 빠르게 멈추기 때문에 마르코프, PR, MBDP 알고리즘의 최적화 효과는 상대적으로 적다.
  • 고속 워크로드에서(메모리 더티율이 높으면) 예측 알고리즘은 원본 사전 복사본과 비교하여 반복 과정에서 매우 더티한 메모리 페이지의 반복 전송을 피함으로써 전송되는 데이터의 양을 줄일 수 있다. MBDPC는 페이지 압축을 사용하기 때문에 전송되는 데이터의 총량을 더 줄일 수 있다.

 

 

5. iteration 당 전송되는 데이터의 양:

제안한 MBDPC 방식은 각 라운드의 데이터 양과 마지막에 전송되는 총 데이터 양이 다른 알고리즘에 비해 낮다.

메모리 데이터를 미리 수집하고 첫 번째 라운드에서 예측을 하기 때문에 첫 번째 라운드에서 불필요한 페이지 전송을 피할 수 있다.

 

 

Discussion

본 논문에서는 머신러닝 기법의 더티 페이지 예측을 기반으로 하는 컨테이너 사전 복사 마이그레이션 방법인 MBDPC를 제안했다.

MBDPC는 프로그램 로컬리티 원리를 이용해 메모리 더티 페이지를 예측하여 페이지가 반복 전송되는 것을 방지했다.

본 논문은 단일 컨테이너의 마이그레이션에 대한 연구이며, 향후 쿠버네티스와 같이 널리 사용되는 컨테이너 오케스트레이션 도구와 결합하여 여러 컨테이너 간의 마이그레이션 문제를 해결하는 방법을 고려하면 좋을 것 같다.

 

메모리 접근 특성을 분석하여 시스템 내에서 메모리 관리에 사용하는 쪽만 생각했었는데, 이렇게 라이브 마이그레이션에도 사용할 수 있다는 것을 알 수 있었다. 라이브 마이그레이션에서는 아무래도 전송량이 중요하기 때문에 더티페이지 예측 뿐만 아니라 메모리 압축 기술을 사용했나보다

💡 요약
- abstract: 다양한 워크로드에 대처할 수 있는 유연성을 높이기 위해 컨테이너 기반 애플리케이션의 수평 및 수직 탄력성을 제어하기 위한 강화 학습(RL) 솔루션을 제안한 연구
- introduction: 컨테이너 기반 애플리케이션을 확장할 때 효율성을 최적화하기 위해 확장 정책이 유연하고 적응적으로 동작해야 한다는 과제가 있다.
- related works: 기존에는 수직적 탄력성과 수평적 탄력성을 결합한 연구가 소수만 이루어졌다. 또한 기존에는 탄력성을 구동하기 위해 임계값을 사용했다.
- method: 컨테이너 기반 애플리케이션의 탄력성을 제어하기 위해 모델 없는 솔루션과 모델 기반 솔루션을 포함한 다양한 RL 정책을 사용할 것을 제안했다.
- experiment: 시뮬레이션과 프로토타입 기반 실험을 통해 다양한 RL 접근 방식을 평가했다. 결과적으로 결과는 모델 기반 접근 방식이 사용자 정의 배포 목표에 따라 최상의 적응 정책을 성공적으로 학습할 수 있음을 보여주었다.
- conclusion & discussion: 모델 기반 RL 접근법을 사용하여 컨테이너 기반 애플리케이션의 탄력성을 제어하기 위한 RL 알고리즘을 설계했으며, 제안된 정책을 컨테이너 오케스트레이션 도구인 Docker Swarm에 자체 적응 기능을 도입하는 확장 기능인 Elastic Docker Swarm을 적용했다.

 

 

 

Introduction

컨테이너를 사용하면 수평 및 수직 확장을 통해 애플리케이션 배포를 쉽게 조정할 수 있다.

  • 수평적 탄력성: 애플리케이션 인스턴스(컨테이너) 수를 늘린다. => 컨테이너 구성을 변경하므로 새 구성을 적용하는 데 필요한 시간 동안 전체 애플리케이션을 사용할 수 없게 됨(다운타임 발생)
  • 수직적 탄력성: 애플리케이션 인스턴스에 할당된 컴퓨팅 리소스의 양을 늘리거나 줄인다 => 일반적으로 추가적인 다운타임이 발생하지는 않지만, Docker Swarm에서 스케일 아웃을 수행하면 적응 비용이 발생
    • Docker Swarm에서 스케일 아웃을 수행하면 적응 비용이 발생하는 이유?
    • Docker Swarm의 트래픽 라우팅 전략 때문이다. 새로운 인스턴스가 실행되기 전에도 Docker Swarm은 애플리케이션 요청을 새로 추가된 인스턴스로 보내려고 시도하는데, 이때 해당 인스턴스가 아직 실행 중이지 않으면 응답하지 못할 가능성이 있다. 따라서 이전 버전의 컨테이너를 사용하여 요청을 처리하기 위해 일시적으로 다운 타임이 발생할 수 있다.

이러한 컨테이너의 특성을 사용하여 런타임에 애플리케이션 배포를 적절히 조정하여 성능을 유지할 수 있다

 

 

Related Work

(1) 탄력성 조치를 적용하는 범위에 따른 연구

  • 인프라 수준: 인프라 수준에서 탄력성 컨트롤러는 일반적으로 VM을 획득하고 해제하여 컴퓨팅 리소스의 수를 변경한다.
  • 애플리케이션 수준: 애플리케이션 수준에서 컨트롤러는 애플리케이션에 직접 할당된 컴퓨팅 리소스를 조정한다(ex. 병렬 처리 수준 변경) 또는, 탄력성 컨트롤러를 애플리케이션 코드 내에 통합하는 임베디드 탄력성을 사용하는데, 이 경우 애플리케이션 자체에서 적응을 조정하는 메커니즘과 정책도 구현해야 한다. 

외부 컨트롤러를 사용하여 컴퓨팅 리소스를 조정하는 방식은 소프트웨어 모듈성과 유연성을 향상시키기 때문에, 본 논문에서는 분산 컨테이너화된 애플리케이션의 수평 및 수직 탄력성을 관리하기 위한 외부 컨트롤러를 제안하고 있다.

 

(2) 탄력성 조치 적용의 목표에 따른 연구

  • 애플리케이션 성능 개선: 로드 밸런싱 및 리소스 활용
  • 에너지 효율성
  • 배포 비용 절감

대부분의 연구는 단일 수준의 배포 목표를 고려하기 때문에, 본 연구 또한 단일 수준의 배포에 초점을 맞추고 컨테이너 수준에서 탄력성을 활용한다.

 

(3) 배포를 조정하는 데 사용되는 작업 및 방법론에 따른 연구

  • 수학적 프로그래밍 접근법: 컨테이너의 초기 배치와 런타임 배포 적응 비용(배포 재구성으로 인한 성능 저하) 을 고려하여 배포를 조정함
  • 휴리스틱: 임계값 기반 및 RL 기반 솔루션
    • 임계값 기반: 클라우드 인프라 레이어뿐만 아니라 런타임에 컨테이너를 확장하는 데 가장 많이 사용되는 접근 방식으로, 기존의 오케스트레이션 프레임워크(Kubernetes, Docker Swarm, Amazon ECS 등)는 일반적으로 일부 로드 메트릭(ex. CPU 사용률)에 기반한 최선의 임계값 기반 정책에 의존하고 있다. => 효과적인 임계값 기반 정책을 사용하려면 임계값 매개변수를 올바르게 설정해야 하며, 이를 위해서는 애플리케이션 리소스 소비에 대한 어느 정도 지식이 필요하며, 수직 확장에 대한 연구는 별로 이루어지지 못했다. = 매우 번거롭다
    • RL 기반 솔루션: Q-Learning 을 이용하여 가상 머신 할당 및 프로비저닝을 위한 정책 수립 등에 적용되어왔다. => 모델이 없는 솔루션이기 때문에 수렴이 느리다는 문제가 있다.

본 논문에서는 빠르게 학습시키기 위해 시스템 지식을 활용하는 새로운 모델 기반 RL 정책을 제안하며, 애플리케이션 배포를 런타임에 적응하기 위해 특히 수직 및 수평 탄력성을 공동으로 활용할 때의 이점을 조사한다.

 

 

Background

A. Docker Swarm

Docker는 컨테이너화된 애플리케이션을 생성하고 관리하기 위한 플랫폼으로, Docker를 사용하여 특정 리소스 할당량을 가진 컨테이너를 구성할 수 있고, 런타임에 업데이트하여 수직적 탄력성을 활성화할 수 있다.

Docker Swarm은 master-worker 패턴에 따라 여러 노드에서 컨테이너 실행을 단순화하는 클러스터링 시스템이다.

 

Method

  • 강화학습(Reinforcement Learning): 런타임에 컨테이너 기반 애플리케이션에 대한 최적의 적응 전략을 학습한다.
    • 에이전트(agent): 개별적인 시간 단위로 애플리케이션과 상호 작용하고, 애플리케이션 상태(state)를 관찰하며 예상되는 장기 비용을 최소화하기 위한 작업을 수행한다.
    • 상태(state): 컨테이너 수(=인스턴스 수), CPU 사용률, 각 컨테이너에 할당된 CPU 점유율의 튜플로 정의한다.
    • cost function: 애플리케이션 상태가 s에서 s'로 전환될 때 작업 a를 수행하는 데 드는 비용을 나타낸다. => 비용 최소화는 일반적으로 RL 에이전트의 제약 조건 또는 부차적인 목표이다.
      • 본 논문에서는 적응 비용(컨테이너 추가 또는 제거와 같은 작업에 대한 비용), 성능 비용(애플리케이션 응답 시간 제한을 초과할 때마다), 리소스 비용(애플리케이션 인스턴스 수 및 할당된 CPU 점유율에 비례) 을 단일 가중치 비용 함수로 결합하여 사용한다.
  • solution1) Q-learning: 강화 학습에서 샘플링 평균을 통해 최적의 Q-function을 추정하는 데 사용되는 모델 없는 알고리즘이다. 본 논문에서는 greedy 정책으로 기반으로 작업을 선택하지만, 낮은 확률로 차선의 작업도 탐색한다. 각 시간 슬롯이 끝날 때마다 학습 속도 매개변수와 향후 보상의 중요성을 결정하는 discount factor 를 사용하여 관찰된 state-action 쌍에 대한 Q값이 업데이트됨
    • Q-function=Q(s,a): 상태 s에서 액션 a를 실행 시 예상되는 장기 비용을 나타낸다. 컨테이너 확장에 대한 결정을 내리는 데 사용되며, 실제 발생한 비용을 관찰하여 시간이 지남에 따라 업데이트된다. => 높은 보상을 받을 가능성이 가장 높은 행동을 결정할 때 사용
    • Dyna-Q: 애플리케이션과 환경 간의 상호 작용을 시뮬레이션하여 학습 프로세스의 속도를 높이는 강화 학습 알고리즘이다. 런타임에 Dyna-Q는 애플리케이션 상태를 관찰하고 Q-러닝과 유사하게 Q(s, a)의 추정치를 사용하여 적응 action을 선택한 뒤, 탐색된 (s, a)에 대한 다음 상태 s'와 비용 c를 저장하여 런타임에 시스템 모델인 모델(s, a)을 업데이트한다. 모델은 Q-함수를 업데이트하는 데 사용된다.
  • solution2) 모델 기반 강화 학습: 본 논문에서는 경험적 데이터를 사용하여 시스템 모델의 전이 확률과 비용 함수를 추정하는 전체 백업 모델 기반 강화학습 접근 방식을 적용했다.
    • 전체 백업 접근법: 추정 가능한 시스템 모델에 의존하며, 벨만 방정식을 사용하여 Q 함수 계산한다. 
    • 샘플 값을 사용하여 비용 추정치를 업데이트하는 동안, 특정 속성을 적용하여 컨테이너 수가 감소하거나 CPU 사용률이 증가하거나 CPU 점유율이 감소할 때 Rmax 위반으로 인한 예상 비용이 낮아지지 않도록 한다

 

본 논문에서는 Docker Swarm에 모니터링/분석/계획/실행 제어 루프를 적용함으로써 Elastic Docker Swarm(EDS) 아키텍쳐로 확장했다. EDS는 RL 정책을 사용하여 애플리케이션 배포 및 재구성 시기/방법을 결정하며, Docker Swarm API를 활용한다.

  • Docker Monitor: 각 노드에서 실행되며, 노드에서 실행 중인 컨테이너의 CPU 사용률에 대한 정보를 메시지 브로커(Apache Kafka)에 주기적으로 게시
  • Container Manager: 메시지 브로커를 통해 모니터링 정보를 수신하고, RL 에이전트를 사용하여 분석 및 계획 단계를 수행
    1. RL 에이전트가 애플리케이션 상태를 판단하고 Q-function 을 업데이트한다.
    2. RL 에이전트를 사용하여 수행해야 할 확장 작업을 식별한다.
    3. 애플리케이션 배포를 조정하기 위해 EDS는 Docker Swarm API를 활용한다.

 

 

Experiment

  • 시뮬레이션 실험: 애플리케이션이 무작위적이고 독립적인 요청(M)을 수신하고, 서버가 요청을 처리하는 데 걸리는 시간(D)은 고정되어 있으며 변하지 않으며, 서버 수는 시간 단계 i에서 사용된 컨테이너 수(ki)와 같다고 가정한다.

시뮬레이션 실험에서 사용되는 변화하는 워크로드

  • 최대 응답 시간(Rmax) 위반을 방지하는 데 중점을 둔 경우, 모델 기반 솔루션은 응답 시간 위반이 2.85%에 불과하여 모델 없는 접근 방식보다 성능이 더 우수했다. 또한, 리소스 중점 시나리오에서는 모델 기반 솔루션이 애플리케이션 응답 시간이 길어지는 대신 리소스 활용도를 성공적으로 개선했다.
  • 5-액션 모델은 학습 작업을 간소화하고 모든 RL 정책에 대한 Rmax 위반을 줄이는 반면, 9-액션 모델은 학습 프로세스를 느리게 했다.

=> 모델 기반 접근 방식이 더 우수하다 = 시스템에 대한 사전 지식이 매우 중요하다

*모델 기반 접근 방식: 시스템의 동적인 특성을 사전에 알고 있는 경우, 이를 활용하여 강화학습 모델이 더욱 효과적으로 학습할 수 있도록 하는 방법

 

  • 프로토타입 기반 실험: Amazon EC2 인스턴스로 구성된 클러스터에서 EDS로 제안된 정책을 사용하여 실제 환경에서 강화 학습(RL) 알고리즘을 평가함

프로토타입 실험에서 사용되는 변화하는 워크로드

  • 목표 응답 시간 위반을 줄이고 워크로드 변동에 적응하는 측면에서 모델 기반 정책이 Q-러닝보다 우수한 것으로 나타났다.
    •  Q러닝은 모델 기반 솔루션과 유사한 솔루션을 찾지만 애플리케이션 배포를 더 자주 변경했다.
    • 모델 기반 솔루션은 Q-러닝보다 Rmax 위반 횟수를 줄이고 애플리케이션 배포를 덜 변경하여 애플리케이션 가용성을 높이는 것으로 나타났다.
  • 모델 기반 접근 방식은 수직 확장보다 수평 확장을 선호하는 것으로 관찰되었다.
    • Elastic 배포 시스템(EDS)의 적응 비용 정의에 따라 컨테이너 구성 변경(수직 확장)이 새 컨테이너 추가(수평 확장)보다 비용이 더 많이 들기 때문

=> RL 기반 접근 방식이 갑작스러운 워크로드 변화에 적응할 수 있어서 더 좋지만, 모델 기반 솔루션은 모든 상태, 동작, 다음 상태를 반복해야 하므로 계산이 많이 필요하며, 사용 가능한 동작과 전환 확률이 제한되어 있기 때문에 복잡성이 크다

+ Recent posts