GC 튜닝

: GC를 튜닝하고자 마음 먹었다면 할당, 중단 민감도, 처리율 추이, 객체 수명을 면밀히 관찰해야한다.

GC 힙 크기 조정 플래그

플래그 설명
-Xms<size> 힙 메모리의 최소 크기 설정
-Xmx<size> 힙 메모리의 최대 크기 설정
-XX:MaxPermSize=<size> 펌젠 메모리의 최대 크기 설정(Java 7 이전)
-XX:MaxMetaspaceSize=<size> 메타스페이스 메모리의 최대 크기를 설정(Java 8 이후)
  • Java7 이전에만 적용되던 펌젠은 Java8 이후로 메타스페이스로 교체되었다.
  • 튜닝 시 GC플래그는 한번에 하나씩만 추가하고, 각 플래그가 무슨 작용을 하는지 숙지해야하며 부수 효과를 일으키는 플래그 조합도 있음에 명시하고 사용하여야 한다.
  • 성능 문제를 일으키는 원인이 GC인지 아닌지를 판단하기 위해서는 vmstat 같은 고수준의 머신 지표를 체크한 뒤,
    • CPU 사용률이 100%에 가까운지?
    • 대부분의 시간(90%이상)이 user space에서 소비되는지?
    • GC 로그가 쌓이고 있다면 현재 GC가 실행중이라는 증거!
    위 항목들을 체크하자. 위 세가지 조건이 다 맞다면 GC가 성능 이슈를 일으키고 있을 가능성이 크다.


할당

  • 할당률 분석은 GC를 튜닝하면 성능이 개선될지 여부를 판단하는 데 반드시 필요한 과정이다.
  • Young generation 수집 이벤트 데이터를 활용하면 할당된 데이터양, 단위 수집 시간을 계산할 수 있고, 일정 시간 동안의 평균 할당률을 산출할 수 있다.
  • 1GB/s 이상의 할당률이 일정시간 지속한다면 GC 튜닝만으로는 해결할 수 없는 성능 문제가 발생한 경우일 확률이 크므로, 애플리케이션 핵심부의 할당 로직을 제거하는 리팩토링을 수행해 메모리 효율을 개선해야 한다.

할당과 관련된 주요 관찰 지점

  1. 굳이 없어도 그만인 사소한 객체 할당
    • 단순히 불필요한 객체를 제거한다.
    • 예: 로그 디버깅 메시지, JSON (un-)serialization 자동 생성코드, ORM 코드
  2. 박싱 비용
    • 불필요한 박싱을 언박싱한다.
  3. 도메인 객체
    • 다음 타입의 도메인 객체는 메모리를 많이 먹을 수 있으므로 참고하자.
    • char[]: 스트링을 구성하는 캐릭터
    • byte[]: 바이너리 데이터
    • double[]: 계산 데이터
    • Map Entry
    • Object[]
    • 내부 자료구조(methodOop, klassOop)
  4. 엄청나게 많은 non-JDK 프레임워크 객체

TLAB(Thread Local Allocation Buffer) 관련

  • TLAB는 스레드 당 크기가 동적으로 조정되며, 일반 객체는 남은 TLAB 공간에 할당된다. 만약 여유 공간이 없다면 스레드는 VM에게 새로운 TLAB를 달라고 요청한다.
  • 객체의 크기가 너무 커서 빈 TLAB에 들어가지 않으면 VM은 Eden에 직접 객체 할당을 시도하고, 이마저도 실패하면 영 GC를 수행한다. 만일 여기서도 실패하면 Tenured 영역에 객체를 직접 할당한다.
    • 덩치가 큰 배열(byte[], char[])가 테뉴어드에 곧바로 할당된다면 Weak generation hypothesis 이론에 의해 GC 성능이 급격히 안좋아질 수 있다.
  • 관련 튜닝 플래그는 다음과 같다.
    옵션 설명
    -XX:PretenureSizeThreshold=<n> 여기서 지정한 크기를 넘어가는 객체는 Young Genreation을 거치지 않고 Old Generation으로 바로 들어간다.
    -XX:MinTLABSize=<n> TLAB의 최소 크기를 설정한다
    -XX:MaxTenuringThreshold=<n> 테뉴어드 영역으로 승격되기 전까지 객체가 통과해야할 GC 횟수다.
    디폴트는 4회이고 1~15사이의 값을 가질 수 있다.
    한계치가 높을수록 장수한 객체가 더 많이 복사된다.
    한계치가 너무 낮으면 단명 객체가 승격되어 Tenured Memory pressure를 가중시킨다. 이는 full GC를 자주 발생시켜 성능을 저하시킨다.
  • 할당이 자주 발생하면 그만큼 Young GC의 발생 주기는 짧아지고 이렇게 짧은 시간 내에 Young GC가 자주 발생하면 Tenured 영역으로 잘못 승격되는 객체가 많아지므로 할당률은 GC의 성능에 큰 영향을 준다.

중단 시간

  • 중단 시간에 대한 정량적인 가치 판단의 기준은 존재하지 않는다. 다만 중단 시간 튜닝 시 참고가 될 만한 휴리스틱이 존재하는데, 다음의 표에서 나타내는 허용 중단시간 기준과 애플리케이션 힙 크기를 연관지어 적합한 초기 GC를 선택하길 바란다.
> 1sec 1sec - 100ms < 100ms -
Parallel Parallel CMS < 2GB
Parallel Parallel/G1 CMS < 4GB
Parallel Parallel/G1 CMS < 10GB
Parallel/G1 Parallel/G1 CMS < 20GB
Parallel/G1 G1 CMS > 20GB

 

CMS를 사용할 경우, 중단 시간을 튜닝하려고 하기 전에 할당률부터 줄이는 것이 좋다. 할당률이 낮아지면 CMS에 가해지는 memory pressure 또한 낮아지면 GC 사이클이 스레드 할당 속도를 따라가기 쉽다.
또한 이는 CMF 발생확률을 감소시켜, 중단시간에 민감한 애플리케이션에서 일거양득의 효과를 준다.

 

수집기 스레드와 GC 루트

  • GC루트 탐색 시간은 다음의 요소에 영향을 받음에 주의하자.
    • 애플리케이션 스레드 갯수
    • 코드 캐시에 쌓인 Compiled code
    • Heap 크기
  • GC루트 탐색은 단일 스레드로 수행하므로, 이 단일 스레드의 탐색 시간이 전체 마킹 시간을 경정짓게 된다. 이는 객체 그래프가 복잡해질수록 더욱 심해지며, 그래프 내부에 객체 체인이 길게 늘어지면서 마킹 시간은 점점 더 길어진다.
  • 애플리케이션 스레드가 너무 많아도 스택 프레임을 더 많이 탐색해야하고 세이프포인트에 도달하는 시간도 길어지는 등 GC 시간에 많은 영향을 끼친다.
  • GC루트의 원천들에는 JNI 프레임, JIT Compiled code 도 존재한다.
  • 위 세가지 요소 중 Heap 영역, 애플리케이션 스레드의 스택 프레임은 비교적 병렬화가 잘 된다.

@Transactional

: @Transactional 어노테이션을 사용하면 tx.begin(), tx.commit()을 자동으로 수행해주고, 예외 발생시 rollback처리를 자동으로 해 주는 편리한 기능이다.

public void register(Member member) {
    try {
      tx.begin();
      memberRepository.save(member);
      tx.commit();
    } catch (Exception e) {
      tx.rollback();
    }
}

와 같은 메소드는 @Transactional 어노테이션 사용 시

@Transactional
public void register(Member member) {
    memberRepository.save(member);
}

로 바꿀 수 있다.

 

내부 구현

: Spring은 프록시 객체를 사용해 위와 같이 tx.being()등의 코드를 삽입해 줍니다. 프록시 객체는 다음과 같이 생성되는데

public class MemberServiceProxy {
  private final MemberService memberService;
  private final TransactonManager manager = TransactionManager.getInstance();

  public MemberServiceProxy(MemberService memberService) {
    this.memberService = memberService;
  }

  public void register(Member member) {
    try {
      manager.begin();
      memberService.register(member);
      manager.commit();
    } catch (Exception e) {
      manager.rollback();
    }
  }
}

개발자가 정의한 메소드를 한번 감싼 뒤, 해당 프록시 메소드를 호출함으로써 이루어집니다.

여기서 발생할 수 있는 문제점과 같이 더욱 자세한 내용은 Spring @Transactional 사용시 주의해야할 점를 참고해 주세요.



사용할 수 있는 Options

 

Propagation

: @Transactional 어노테이션이 붙여진 메서드는 하나의 트랜잭션 안에서 진행될 수 있도록 만들어주는 역할을 한다. 그런데 트랜잭션 안에서 트랜잭션이 호출된다면 어떻게 처리할지를 결정하는것이 propagation 옵션이다. @Transactional(propagation = Propagation.NOT_SUPPORTED)와 같은 방식으로 사용할 수 있다.

옵션 설명
REQUIRED - 기본 옵션
- 부모 트랜잭션이 존재한다면 부모 트랜잭션에 합류, 그렇지 않다면 새로운 트랜잭션을 만든다.
- 중간에 자식/부모에서 rollback이 발생된다면 자식과 부모 모두 rollback 한다.
REQUIRES_NEW - 무조건 새로운 트랜잭션을 만든다.
- nested한 방식으로 메소드 호출이 이루어지더라도 rollback은 각각 이루어 진다.
MANDATORY - 무조건 부모 트랜잭션에 합류시킨다.
- 부모 트랜잭션이 존재하지 않는다면 예외를 발생시킨다.
NESTED - 부모 트랜잭션이 존재하면 부모 트랜잭션에 중첩시키고, 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성한다.
- 부모 트랜잭션에 예외가 발생하면 자식 트랜잭션도 rollback한다.
- 자식 트랜잭션에 예외가 발생하더라도 부모 트랜잭션은 rollback하지 않는다. 이때 롤백은 부모 트랜잭션에서 자식 트랜잭션을 호출하는 지점까지만 롤백된다. 이후 부모 트랜잭션에서 문제가 없으면 부모 트랜잭션은 끝까지 commit 된다.
NEVER - 트랜잭션을 생성하지 않는다. 만약 부모 트랜잭션이 존재한다면 예외를 발생시킨다.


 

Isolation

: 격리수준을 정하는 옵션으로, 트랜잭션 내에서 일관성 없는 데이터를 허용하도록 하는 수준을 정한다. Spring에서는 기본적으로 READ_COMMITED를 사용하는데 Persistence Context를 사용함으로써 REPEATABLE_READ가 보장된다. 다음과 같이 @Transactional(isolation = Isolation.READ_COMMITTED) 사용할 수 있다.

옵션 설명
READ_UNCOMMITED - 트랜잭션에서 commit되지않은 데이터를 다른 트랜잭션에서 읽는 것을 허요한다.
- Dirty Read가 발생한다.
READ_COMMITED - 트랜잭션에서 commit되어 확정된 데이터만을 읽는 것을 허용한다.
REPEATABLE_READ - 한 트랜잭션 내에서 수차례 SELECT를 수행하더라도 동일한 값이 읽혀지는 것을 보장한다.
- Phantom read(새로운 row가 추가/삭제되어 새로 입력된 데이터를 읽는/사라지는 현상)는 여전히 발생 가능하다.
SERIALIZABLE - 모든 작업을 하나의 트랜잭션에 처리하는 것과 같은 높은 고립수준을 제공하는데, 이로인해 동시성 처리 효율은 매우 떨어진다.

여기서 발생되는 Row Lock과 같은 내용은 Lock으로 이해하는 Transaction의 Isolation Level을 참고해 주세요.



rollbackFor

  • 특정 예외가 발생할 경우 rollback하는 것을 명시하는 옵션이다. Spring은 기본적으로 RuntimeException과 Error를 롤백 정책으로 가진다.
  • 사용은 @Transactional(rollbackFor = {RuntimeException.class, Error.class}) 과 같이 할 수 있다. 만약 모든 예외에 대해 rollback하고싶다면 Exception.class를 추가하면 된다.
  • 이와 반대로 특정 Exception에 rollback하고 싶지 않다면 noRollbackFor 옵션을 사용하자.


Read-only

  • 트랜잭션을 읽기 전용으로 설정할 수 있다.
  • 이 옵션이 적용된 트랜잭션은 UPDATE, INSERT, DELETE등과같은 쓰기 작업이 진행되면 예외를 발생시킨다.
  • 이 옵션이 적용되지 않은 트랜잭션은 영속성 컨텍스트에 스냅샷을 만들어두고 트랜잭션이 끝날 때 스냅샷과 Entity의 값을 비교해 쓰기지연 SQL 저장소에 UPDATE 쿼리를 생성해 DB에 flush하는 작업을 거치는데, 이러한 과정이 사라지게 되어 효율적인 작업이 수행될 수 있다.
  • @Transactional(readOnly=true)로 사용할 수 있다.

timeout

  • 지정한 시간 내에 해당 메소드가 수행 완료되지 않으면 rollback을 수행한다.
  • @Transactional(timeout=10)와 같이 사용가능하다. (단위는 sec) (default -1로 no-timeout)

'Backend > SpringBoot-JPA' 카테고리의 다른 글

2차 캐시 - @Cacheable  (0) 2021.04.30
@RestController 어노테이션  (0) 2021.04.29
Optimistic/Pessimistic Lock  (1) 2021.04.19

GC Logging

애플리케이션 실행 중 트래픽이 몰릴 때 사용자의 요청에 응답을 해 주지 못하거나 오류가 발생하는 원인은 다양하지만, 동적 메모리 할당에서 문제가 발생할 소지가 많다. 이러한 동적 메모리 할당은 GC에 의해 자동으로 메모리가 해제되는데 이러한 과정에서 GC가 적절히 동작하는지를 살펴보려면 GC 로그를 확인해 보는 것이 좋다.

GC Logging Options

플래그 설명
-Xloggc:gc.log GC 이벤트에 로깅할 파일을 지정한다
-XX:+PrintGCDetails GC 이벤트 세부 정보를 로깅한다
-XX:+PrintTenuringDistribution 툴링에 꼭 필요한, 부가적인 GC 이벤트 세부 정보를 추가한다
-XX:+PrintGCTimeStamps GC 이벤트 발생 시간을 VM 시작 이후 경과시간을 기준으로 sec 단위로 출력한다
-XX:+PrintGCDateStamps GC 이벤트 발생 시간을 wall-clock 기준으로 출력한다

: 위 옵션을 사용할 때 성능적인 관점에서 주의해야할 사항들이다.

  • verbose:gc는 지우고 PrintGCDetails를 사용한다.
  • PrintTenuringDistribution 옵션을 사용함으로써 이 플래그가 제공하는 정보를 사람이 사용하기에는 어렵다. 하지만 중요한 memory pressure, premature promotion 등의 이벤트 계산시 필요한 기초 데이터를 제공해 줍니다.
  • PrintGCDateStampsPrintGCTimeStamps는 둘 다 사용해 주는것이 좋은데, 전자는 GC 이벤트와 로그파일을 연관짓는 용도, 후자는 GC와 다른 내부 JVM이벤트를 연관짓는 용도로 쓸 수 있다.


JVM 모니터링 툴과 GC 로그

VisualVM

  • VisualVM은 NetBeans 플랫폼 기반의 시각화 툴로 JRE에 포함되어 있다.(Java 9 부터는 배포 파일에서 빠져있으므로 다음의 경로에서 내려받아 디폴트 경로에 visualvm 바이너리 경로를 추가해 주어야 한다.)
  • JMX(Java Management eXtension) 이 적용된 기술이라 볼 수 있는데, 이는 JVM과 그 위에서 동작하는 애플리케이션을 제어하고 모니터링하는 범용 툴로 클라이언트 애플리케이션처럼 메소드를 호출하고 매개변수를 바꿀 수 있다.
  • VisualVM은 attach mechanism을 이용해 실행 프로세스를 실시간 모니터링한다. 원격 프로세스에 연결하려면 원격지에 JMX를 통해 인바운드 접속이 허용되어야 한다(== 원격 호스트에 jstatd가 실행되어야 한다.)
  • 이 툴을 이용하면 CPU, 힙 사용량, 로드/언로드 된 클래스 갯수, 실행중인 스레드 갯수, 스레드별 상태와 짧은 변화추이(with 스레드 덤프), CPU & 메모리 사용률에 대한 단순 샘플링 결과를 볼 수 있다.


JMX vs GC 로그

  • GC 로그 데이터는 실제 가비지 수집 이벤트가 발생해 쌓이지만, JMX는 데이터를 샘플링하여 얻는다.
  • GC 로그 데이터는 캡쳐 영향도가 거의 없으나, JMX는 프록시 및 원격 메소드 호출(Remote Method Invocation) 과정에서 비용이 발생한다.
  • GC 로그 데이터는 자바 메모리 관리에 연관된 성능 데이터가 50가지 이상이지만, JMX는 10가지도 안된다.
    JMX는 스트리밍된 데이터를 즉시 제공하지만 위와 같은 이유에서 GC 로그가 성능 관점에서 조금 더 좋아보일 수 있다. 추가로 요즘은 jClarity 센섬같은 툴도 GC 로그 데이터를 스트리밍하는 API를 제공하므로 큰 차이는 없다.


JMX의 단점

  • JMX를 이용해 애플리케이션을 모니터링하는 클라이언트는 대부분 런타임을 샘플링해 현재 상태를 업데이트 한다. 클라이언트는 데이터를 계속 넘겨받기 위해 런타임에있는 JMX 빈을 폴링한다. 문제는 GC가 언제 실행될지 클라이언트는 알 수 없으므로, GC 전후의 메모리 상태 역시 알 수 없으며 따라서 GC 데이터를 깊이있게 정확히 분석하기 힘들다.
  • JMXConnector를 구현한 코드는 내부적으로 RMI에 의존하는데, RMI기반 통신은 방화벽에 포트를 열어야하므로 secondary socket connection이 발생하며, 프록시 객체를 이용해 remove() 메소드 호출을 대행하고 Java finalization에 의존한다는 고질적인 문제점이 존재한다. (다음에 추가적으로 더 알아보자..)
  • 접속을 해제하는 작업은 finalize에 의존하는데 이는 GC를 돌려 객체를 회수해야하므로 종료화가 GC에 미치는 영향을 고려하지 않을 수 없다.
  • RMI를 사용하는 애플리케이션은 기본 1시간에 한번씩 full GC가 발생하는데, JMX를 사용하는 순간부터 추가적인 부하는 피할수 없을 것이다.

About JVM

  • JVM은 스택 기반의 해석 머신으로, 실행 결과를 스택에 쌓아두며 스택의 값들을 가져와 계산한다.
  • Java Compiler(javac)는 .java파일을 읽어 바이트코드로 구성된 .class 파일을 생성하는데, 이는 특정 아키텍쳐에 종속적이지 않은 중간 표현형(Intermediate Representation)이다. JVM은 이 바이트코드를 읽어 실제 동작을 수행한다.

자바 클래스 로딩 메커니즘

: 자바 프로세스가 초기화되면 연결된 클래스 로더가 차례차례 동작한다.

  1. 첫 번째로, 부트스트랩 클래스가 자바 런타임 코어 클래스를 로드한다. (JAVA 8 이전까지는 rt.jar에서 가져오지만, JAVA 9 이후부터는 런타임이 모듈화되고 클래스로딩 개념이 많이 바뀌었다.)
  2. 부트스트랩 클래스 로더는 다른 클래스로더가 나머지 시스템에 필요한 클래스를 로드할 수 있게 최소한의 필수 클래스(java.lang.Object 등)만 로드한다.
  3. 다음으로, 부트스트랩클래스로더를 부모로 설정하는 확장 클래스로더가 생기는데 이는 특정 OS나 플랫폼에 native code를 제공하거나 기본환경을 오버라이드 할 수 있다.
  4. 마지막으로, 애플리케이션 클래스로더가 생성되고 지정한 클래스패스에 위치한 유저 클래스를 로드한다.

이 과정에서 처음보는 새로운 클래스를 dependency에 로드하고, 클래스를 찾지못하면 자신의 부모 클래스로더에게 넘기는데, 이렇게 거슬러올라 부트스트랩도 찾지 못하면 ClassNotFoundException 예외가 발생한다.

About Hotspot & JIT(Just In Time)

Zero-cost abstraction(Zero-overhead principle)
C++과 같은 언어는 사용하지 않는 것에는 대가를 치르지 않는다.
다시말해 개발자가 작성한 언어보다 더 나은 코드를 컴퓨터는 건네주지않는다.
따라서, 컴퓨터와 OS가 실제로 어떻게 작동해야하는지 개발자가 아주 세세한 저수준가지 일러주어야 한다.

: Java는 개발자의 생산성에 무게를 두고 Zero-overhead abstraction에 반대하는 방향으로 발전했다. 따라서 C/C++과 같이 AOT(Ahead Of Time) 컴파일 방식이 아닌, 인터프리터를 이용해 바이트코드를 실행한다. 이와 더불어 자바는 JIT(Just In Time) 컴파일을 이용해, 프로그램 단위(메소드, 루프)로 바이트코드에서 네이티브 코드로 컴파일한다.
핫스팟은 인터프리트 모드로 실행하는 동안 애플리케이션을 모니터링하면서 가장 자주 실행되는 코드 파트를 발견해 JIT 컴파일을 수행한다(어느 threshold를 넘어가면 profiler가 특정 코드 섹션을 컴파일 & 최적화). 이는 컴파일러가 해석단계에서 추집한 추적 정보를 근거로 최적화를 결정하므로 상황별로 핫스팟이 올바른 방향으로 최적화 할 수 있게 만들어준다는 점이 큰 장점이다. 이러한 PGO(Profile Guided Optimization) 를 사용하면 AOT에서는 불가능했던 동적 인라이닝, 가상 호출, 특정 CPU에 최적화 등으로 성능을 개선할 수 있다. 하지만 바이트 코드를 네이티브 코드로 컴파일하는 비용은 런타임에 지불됨에 유의해야 한다.

다음으로 JIT 컴파일 로깅하는 방법을 살펴보자.
java -XX:+PrintCompilation ${JavaProject}을 통해 컴파일 이벤트 로그를 확인할 수 있다. 추가적으로 -XX:+LogCompilation-XX:+UnlockDiagnosticVMOptions로 핫스팟 JIT 컴파일러가 어떤 결정을 내렸는지 자세한 정보를 얻을 수 있다.

핫스팟 JVM에는 C1, C2라는 두 JIT 컴파일러가 있는데 각각 클라이언트 컴파일러, 서버 컴파일러라고 불린다. C1, C2 모두 메소드 호출 횟수에 따라 컴파일이 트리거링 되는데, 이 한계치에 이르면 VM이 해당 메소드를 컴파일 큐에 넣고 컴파일이 진행된다.
컴파일 프로세스는 가장 먼저 메소드의 내부 표현형을 생성한 뒤, 인터프리티드 단계에서 수집한 프로파일링 정보를 바탕으로 최적화 로직을 적용한다. 여기서, C1과 C2가 생성하는 내부 표현형은 전혀 다른데, C1은 C2보다 컴파일 시간이 짧고 더 단순하계 설계된 까닭에 풀 최적화는 되지 않는다.
자바 6부터는 단계별 컴파일(tierd compile) 을 지원하는데, 이는 인터프리티드 모드로 실행되다 단순한 C1 컴파일 형식으로 바뀌고, 이를 C2가 고급 최적화 수행하는 방식으로 단계를 바꾸는 것을 말한다.

JIT compiled code코드 캐시 라는 메모리 영역에 저장되는데 JVM 시작 시 코드 캐시는 설정된 값으로 고정되어 확장이 불가능하다. 따라서 코드 캐시 영역이 가득차면 더이상 JIT 컴파일은 수행되지 않으며, 컴파일되지 않은 코드는 인터프리터에서만 실행된다. 네이티브 메소드가 새로 저장되면 compiled code를 담기에 크기가 충분한 블록을 free list에서 찾는데 만약 그런 블록이 없으면 여유 공간이 충분한 코드 캐시 사정에 따라 unallocated region에서 새 블록을 생성한다. 또한 추측정 최적화를 적용한 결과 틀린것으로 판단되어 역 최적화(deoptimization)될 때, 다른 컴파일 버전으로 교체(단계별 컴파일)되었을 때, 메소드를 지닌 클래스가 언로딩 될 대 네이티브 코드는 코드 캐시에서 제거된다. 이러한 코드 캐시의 최대 크기는 -XX:ReservedCodeCacheSize=<n>으로 설정할 수 있다.

JIT 튜닝 방법

  1. PrintCompilation 옵션을 이용해 애플리케이션을 실행한다.
  2. 어떤 메소드가 컴파일되었는지 로그를 확인한다.
  3. ReservedCodeCacheSize를 통해 코드 캐시를 늘린다.
  4. 애플리케이션을 재 실행한다.
  5. 확장된 캐시에서 컴파일드 메소드를 살펴본다.

JVM의 메모리 관리

: C/C++에서는 개발자가 직접 동적 할당된 메모리를 관리해주어야 한다. 하지만 JAVA에서는 GC(Garbage Collector)를 이용해 힙 메모리를 자동으로 관리한다.

이 GC는 non-deterministic한 방식이므로 어느 순간에 GC가 실행되는지 정확히 알 수는 없다. 일반적으로 GC가 실행되면 애플리케이션은 모두 실행 중지(STW: Stop The World)되므로, 이 중단시간은 최대 짧도록 GC 알고리즘이 발전해 왔다. GC의 알고리즘에 대해서는 다음 게시물을 통해 더욱 상세히 알아보자.

Jenkins 환경변수

Jenkins Global variable

  1. env
    : env 환경변수는 다음과 같은 형식 env.VARNAME으로 참조될 수 있다. 대표적인 env의 property는 아래와 같다.
property 이름 설명
BUILD_ID 현재 빌드의 ID이며 이는 v1.597 이상에서는 BUILD_NUMBER와 같은 값을 가진다
JOB_NAME 현재 빌드중인 프로젝트의 이름으로 foo 또는 foo/bar와 같은 형식이다.
BRANCH_NAME multibranch 프로젝트인 경우 사용할 수 있으며, 현재 빌드되고 있는 브랜치명을 알려준다
CHANGE_ID multibranch 프로젝트의 change request에 대한 change ID(PR number)를 나타낸다.
CHANGE_URL multibranch 프로젝트의 change request에 대한 change URL을 나타낸다.
CHANGE_TARGET multibranch 프로젝트의 change request에 대해 merge될 base branch를 나타낸다.
CHANGE_BRANCH multibranch 프로젝트의 change request에 대해 현재 HEAD가 가리키고 있는 브랜치 명을 알려준다. 이는 BRANCH_NAME과 같을 수 있다.
BUILD_NUMBER 현재 build number를 나타낸다.
JENKINS_URL http://server:port/jenkins/와 같은 jenkins의 URL을 알려준다.
BUILD_URL http://server:port/jenkins/job/foo/15/와 같은 현재 build의 URL을 알려준다.
JOB_URL http://server:port/jenkins/job/foo/와 같은 job의 URL을 알려준다
  1. currentBuild
    : currentBuild 환경변수는 현재 빌드되고 있는 정보를 담고있다. 보통 readonly 옵션인데 일부 writable한 옵션이 존재한다. 대표적인 currentBuild의 property는 아래와 같다.
property 이름 설명
number build number를 나타낸다.
result SUCCESS, UNSTABLE, FAILURE를 가질 수 있으며, 빌드 중에는 null이 될 수 있다. writable한 옵션이다.
currentResult SUCCESS, UNSTABLE, FAILURE를 가질 수 있으며 null이 될 수 없다.
duration 빌드에 소요되고 있는 시간을 ms단위로 나타낸다.
keepLog true이면 해당 빌드에 대한 로그 파일은 지워지지 않는다. writable한 옵션이다.
displayName 보통 #123과 같으나 writable한 옵션이다.
  1. docker
    : TBD

환경변수의 사용

이들의 사용 예는 아래와 같다.

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
            }
        }
    }
}

: Jenkins에서 환경변수를 설정할 수 있으며 이 예는 아래와 같다.

pipeline {
    agent any
    environment { 
        CC = 'clang'
    }
    stages {
        stage('Example') {
            environment { 
                DEBUG_FLAGS = '-g'
            }
            steps {
                sh 'printenv'
            }
        }
    }
}
  • environment 지시어가 pipeline의 최 상단에 위치해 있으면, 이는 모든 step에 적용된다. 각 stage에 위치한 환경변수는 해당 stage에서만 적용된다.

pipeline-syntax/globals

Docker with GUI

: 도커를 사용하다 보면 GUI환경이 제공되지 않아 불편하거나 초기 GUI를 통해 무언가를 설정해야하는 경우가 있다. 이럴 때 유용한, 호스트의 GUI/sound를 공유하는 방법이다.

$ xhost +local:
$ docker run -it \\
    -v /tmp/.X11-unix:/tmp/.X11-unix \\ # mount the X11 socket
    -e DISPLAY=unix$DISPLAY \\ # pass the display
    --device /dev/snd \\ # sound
    --name jay \\
    ubuntu:16.04 /bin/bash
$ xhost -local:root

참고 링크

도커와 Host PC간 파일 전송

Host -> Container
docker cp /path/foo.txt myContainer:/path/foo.txt

Container -> Host
docker cp myContainer:/path/foo.txt /path/foo.txt

현재 OS 이미지를 Docker로 저장

: 현재 실행중인 부트 이미지를 Docker 이미지로 추출하는 방법이다.

  1. 현재 OS를 tar 파일로 묶음
    tar --numeric-owner --exclude=/proc --exclude=/sys -cvf centos6-base.tar(이름 지정) /
  2. docker 서버로 tar 파일을 복사 후 docker에 import
    cat centos6-base.tar | docker import - centos6-base(원하는 이미지 이름)
  3. import 정상 확인
    docker run -i -t centos6-base cat /etc/redhat-release

참고링크1참고링크2

docker error response from daemon no command specified 오류 발생 시

: 위와 같은 오류가 발생하는 경우는 보통 Docker 이미지의 실행 시 수행할 COMMAND가 지정되지 않아서이다. 따라서 다음과 같이 docker import --change 'CMD ["/bin/bash","-c","exec /sbin/init --log-target=journal 3>&1"]' tljh.tar myDockerName:tag CMD를 지정해주면된다.
: 위 방법으로 해결되지 않는 경우는, 발생원인이 위와는 다른 케이스이다. 나의 경우는 arm 아키텍쳐의 부팅 이미지를 Docker 컨테이너로 만들어 x86_64 아키텍쳐에서 실행하고자 하였을 때 위와 같은 오류를 접했다. 이는 Docker에 대한 이해가 부족했을 때 발생한 것이다. Docker는 어찌보면 컨테이너를 만들어 하나의 프로세스를 수행하는 것이지 가상화하는 것이 아니기때문에 다른 architecture의 부트 이미지를 돌릴수는 없다.
참고링크

'CICD > Docker' 카테고리의 다른 글

Docker - Docker 기본사항  (0) 2021.02.14

Docker 설치

: 도커의 설치는 아래 가이드를 수행하면 된다.

$ curl -fsSL <https://get.docker.com/> | sudo sh
$ sudo usermod -aG docker $USER # docker 실행 시 root 권한이 필요하므로 현재 user를 group에 추가해준다.
$ sudo service docker restart
$ sudo chmod 666 /var/run/docker.sock # /var/run/docker.sock 파일의 권한을 666으로 변경하여 그룹 내 다른 사용자도 접근 가능하게 변경

간단한 Docker 실행

  • 도커 실행은 다음 명령으로 할 수 있다. docker run --name jay ubuntu:16.04

위 명령을 실행한 후 실행중인 docker를 보면 아래와 같이 보인다.

$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                   CREATED          STATUS                     PORTS     NAMES
53c3dcd50980   ubuntu:16.04   "/bin/bash"               4 seconds ago    Exited (0) 3 seconds ago             jay

이렇게 Exited 상태가 된 것은 COMMAND인 /bin/bash를 수행하도록 설정되어있으므로 단순히 bash 쉘을 실행시키고 종료한다.
종료하지 않고 계속 실행중인 상태로 만들려면 docker run --name jay -it ubuntu:16.04 /bin/bash와 같이 옵션을 주면 된다.
-i는 interactively의 약어, -t는 attach to terminal 이라 생각하면 된다.

docker run 시 사용되는 옵션들은 다음과 같다.
: docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

옵션 설명
-d detached mode, 백그라운드 모드
-p 호스트와 컨테이너의 포트를 연결 (포워딩)
-v 호스트와 컨테이너의 디렉토리를 연결 (마운트)
-e 컨테이너 내에서 사용할 환경변수 설정
–name 컨테이너 이름 설정
–rm 프로세스 종료시 컨테이너 자동 제거
-it -i와 -t를 동시에 사용한 것으로 터미널 입력을 위한 옵션
–link 컨테이너 연결 [컨테이너명:별칭]

Docker run에 대한 이해

: docker run 실행 시 docker: Error response from daemon: Conflict. The container name "/ubuntu_test" is already in use by container와 같은 에러를 발견하는 경우가 있다. 이는 docker run 명령이 "create" 와 "start" 명령을 한번에 실행시키는 명령이기 때문에 create 시 이미 동일한 이름의 컨테이너가 존재하기 때문에 발생한다. 따라서 docker rm <id or name>으로 삭제한 후 다시 실행하면 된다.
: detached 모드로 docker가 종료되지 않고 계속 실행되도록 하려면 docker run -d -it --name ubuntu_test ubuntu을 수행해주어야 한다.

도커를 처음 접할때 오는 멘붕 몇 가지

Docker Life-Cycle command

명령어 의미
run 배포 및 실행 docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true --name mysql -v /my/own/datadir:/var/lib/mysql mysql:5.7
stop 실행 중인 Docker 컨테이너를 정지 docker stop ${CONTAINER_ID}
start 정지 중인 Docker 컨테이너를 실행  
rm 배포한 Docker 컨테이너를 삭제  
ps (-a) 실행 중인 (모든) 컨테이너를 표시  

Docker control command

명령어 의미
exec 실행 중인 컨테이너에 명령을 실행 docker exec -it mysql /bin/bash,,, docker exec nginx-vol ls -al
logs 컨테이너의 로그를 표시 docker logs ${CONTAINER_ID}
inpsect 컨테이너/이미지의 상세 정보를 표시  

Docker image-relative command

명령어 의미
images Docer 데몬에 있는 Docker 이미지 목록을 표시  
rmi Docer 데몬에 있는 Docker 이미지를 삭제 docker rmi ${IMAGE_ID}

Tips

|명령어|의미|
|---|---|
|docker rm -v $(docker ps -a -q -f status=exited)|종료 상태인 모든 Docker 컨테이너를 삭제||

Dockerfile

FROM ubuntu:latest
MAINTAINER jay <woghd8754@yonsei.ac.kr>
RUN apt-get -y update
RUN apt-get -y install valgrind
EXPOSE 80 443
CMD ["ls", "-al"]
# 1. ubuntu 설치 (패키지 업데이트 + 만든사람 표시)
FROM       ubuntu:16.04
MAINTAINER subicura@subicura.com
RUN        apt-get -y update

# 2. ruby 설치
RUN apt-get -y install ruby
RUN gem install bundler

# 3. 소스 복사
COPY . /usr/src/app

# 4. Gem 패키지 설치 (실행 디렉토리 설정)
WORKDIR /usr/src/app
RUN     bundle install

# 5. Sinatra 서버 실행 (Listen 포트 정의)
EXPOSE 4567
CMD    bundle exec ruby app.rb -o 0.0.0.0

docker build -t {tag_name} .

'CICD > Docker' 카테고리의 다른 글

Docker - GUI 설정 및 현재 부팅 이미지 이용  (0) 2021.02.14

학습하며 정리한 내용을 올리는 것이니, 참고용으로만 봐 주시길 바랍니다.
자바 스프링 프레임워크(renew ver.) - 신입 프로그래머를 위한 강좌

데이터베이스

: (시스템계정) system / root
: sqlplus
: create user root idientified by root;
: grant connect, resource to root;
: exit
: show user
: drop user root cascade; (system 계정에서 실행)

CREATE TABLE member (
    memId VARCHAR2(10) CONSTRAINT memId_pk PRIMARY KEY,
    memPw VARCHAR2(10),
    memMail VARCHAR2(15),
    memPurcNum NUMBER(3) DEFAULT 0 CONSTRAINT memPurNum_ck CHECK (memPurcNum < 3)
);

INSERT INTO member (memId, memPw, memMail) values ('b', 'bb', 'bbb@gamil.com');

SELECT * FROM member;

DELETE FROM member WHERE memId = 'b';

DROP TABLE member;

commit;

JDBC

: 드라이버 로딩 -> DB 연결 -> SQL 작성 및 전송 -> 자원 해제

// MemberDao.java

private String driver = "oracle.jdbc.driver.OracleDriver";
private String url = "jdbc:oracle:thin:@localhost:1521:xe";
private String userid = "root";
private String userpw = "root";

private Connection conn = null;
private PreparedStatement pstmt = null;
private ResultSet rs = null;

public int memberInsert(Member member) {
    int result = 0;

    try {
        // Driver 로딩
        Class.forName(driver); 

        // DB 연결
        conn = DriverManager.getConnection(url, userid, userpw);

        // Query 전송
        String sql = "INSERT INTO member (memId, memPw, memMail) values (?, ?, ?)";
        pstmt = conn.preparedStatement(sql);
        pstmt.setString(1, member.getMemId());
        pstmt.setString(2, member.getMemPw());
        pstmt.setString(3, member.getMemMail());
        result = pstmt.executeUpdate();

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally { // 자원 해제
        try {
            if (pstmt != null) pstmt.close();
            if (conn != null) conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }   
    }
    return result;
}

public Member memberSelect(Member member) {
    Member mem = null;
    try {
        // Driver 로딩
        Class.forName(driver);

        // DB 연결
        conn = DriverManager.getConnection(url, userid, userpw);

        // Query 전송
        String sql = "SELECT * FROM member WHERE memId = ? AND memPw = ?";
        pstmt = conn.preparedStatement(sql);
        pstmt.setString(1, member.getMemId());
        pstmt.setString(2, member.getMemPw());
        rs = pstmt.executeUpdate();

        while (rs.netx()) {
            String memId = rs.getString("memid);
            String memPw = rs.getString("mempw);
            ...

            mem = new Member();
            mem.setMemId(memId);
            ...

        }
    } catch (Exception e) {
        ...
    } finally { // 자원해제
        try {
            if (rs != null) rs.close();
            if (pstmt != null) pstmt.close();
            if (conn != null) conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    return mem;
}

: 매번 Driver 로딩, DB 연결, 자원 해제를 해 주어야 한다는 단점 존재한다. 이를 간편하게 하기위해 JdbcTemplate을 사용한다.

JDBC Template 사용

: JDBC 사용시 Driver 로딩, DB 연결, 자원 해제를 JDBC Template에서 자동으로 해 주고, 각각의 함수에서는 SQL Query문만 작성해서 실행해주면 된다.

// pom.xml

<!-- DB -->
<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>12.1.0.2</version>
</dependency>
<dependency>                            // 스프링 JDBC 추가
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.1.6.RELEASE</version>
</dependency>
<dependency>                            // 추가
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5</version>
</dependency>
// MemberDao.java

public class MemberDao implements IMemberDao {

    private DriverManagerDataSource dataSource;
    private JdbcTemplate template;

    /*
    private Connection conn = null;
    private PreparedStatement pstmt = null;
    private ResultSet rs = null;
    */

    public MemberDao() {
        /* c3p0 의 DriverManagerDataSource 사용 */
        dataSource = new DriverManagerDataSource();
        dataSource.setDriverClass(dirver); //  드라이버 로딩
        dataSource.setJdbcUrl(url); // DB 연결
        dataSource.setUser(userid);
        dataSource.setPassowrd(iserpw);

        /* spring의 DriverManagerDataSource 사용
        dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dirver); 
        dataSource.setUrl(url); 
        dataSource.setUsername(userid);
        dataSource.setPassowrd(iserpw);
        */

        template = new JdbcTemplate(); // JDBC Template 사용
        template.setDataSource(dataSource);
    }
}

public int memberInsert(Member member) {
    int result = 0;

    String sql = "INSET INTO member (memId, memPw, memMail) values (?, ?, ?)";

    // 1st 방법
    result = template.update(sql, member.getMemId(), ...);

    /*
    // 2nd 방법 (이 때에는, sql문이 외부에서 사용될 수 있으므로 final로 선언해야한다 > final String sql)
    result = template.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, member.getMemId());
            ...

            return pstmt;
        }
    });

    // 3rd 방법 (이 때에는, sql문이 외부에서 사용될 수 있으므로 final로 선언해야한다 > final String sql)
    result = template.update(sql, new PreparedStatementSetter()) {
        @Override
        public void setValues(PreparedStatement pstmt) throws SQLException {
            pstmt.setString(1, member.getMemId());
            ...
        }
    });
    */

    return result;
}

public Member memberSelect(Member memeber) {
    List<Member> members = null;

    String sql = "SELECT * FROM member WHERE memId = ? AND memPw = ?";

    // 1st
    members = template.query(sql, new PreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement pstmt) throws SQLException {
            pstmt.setString(1, member.getMemId());
            pstmt.setString(2, member.getMemPw());
        }
    }, new RowMapper<Member>() {
        @Override
        public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
            Member mem = new Member();

            mem.setMemId(rs.getString("memId"));
            ...

            return mem;
        }
    });

    if (member.isEmpty()) return null;

    /*
    // 2nd
    members = template.query(new PreparedStatementCreator() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn)
                throws SQLException {
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, member.getMemId());
            pstmt.setString(2, member.getMemPw());
            return pstmt;
        }
    }, new RowMapper<Member>() {

        @Override
        public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
            Member mem = new Member();
            mem.setMemId(rs.getString("memId"));
            mem.setMemPw(rs.getString("memPw"));
            mem.setMemMail(rs.getString("memMail"));
            mem.setMemPurcNum(rs.getInt("memPurcNum"));
            return mem;
        }
    });

    // 3rd
    members = template.query(sql, new RowMapper<Member>() {

        @Override
        public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
            Member mem = new Member();
            mem.setMemId(rs.getString("memId"));
            mem.setMemPw(rs.getString("memPw"));
            mem.setMemMail(rs.getString("memMail"));
            mem.setMemPurcNum(rs.getInt("memPurcNum"));
            return mem;
        }

    }, member.getMemId(), member.getMemPw());

    // 4th
    members = template.query(sql, new Object[]{member.getMemId(), member.getMemPw()}, new RowMapper<Member>() {

        @Override
        public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
            Member mem = new Member();
            mem.setMemId(rs.getString("memId"));
            mem.setMemPw(rs.getString("memPw"));
            mem.setMemMail(rs.getString("memMail"));
            mem.setMemPurcNum(rs.getInt("memPurcNum"));
            return mem;
        }

    });
    */

    return members.get(0);
}

커넥션풀

: 다수의 사용자가 DB 접속 시 드라이버를 로딩하고, 커넥션을 만드는 작업은 메모리를 상당히 많이 차지하고 이는 서버에 상당한 부하를 줄 수 있다. 따라서 사용자 요청이 들어오지 않아도 미리 커넥션을 만들어 놓고 요청 있을 때 미리 만들어 놓은 것을 이용하는 방법.
: c3p0모듈의 ComboPooledDataSource를 사용
: 사용방법은 이전(public MemberDao() { ... })과 동일. 하지만,

dataSource = new ComboPooledDataSource();
try {
    dataSource.setDriverClass(driver);
    ...
} catch (PropertyVetoException e) {
    e.printStackTrace();
}

와 같이 try-catch 문을 사용해 주어야 한다.

+ Recent posts