퍼사드패턴 (facade pattern)

어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다.
퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할수 있다.

 

퍼사드(Facede)는 '건물의 앞쪽 정면(전면)'이라는 사전적인 뜻을 가진다. 퍼사드패턴은 위의 그림과 같은 구조로 이루어지는 디자인패턴이다. 간단히 위의 그림을 설명하면 몇 개의 복잡한 서브시스템들과 클라이언트 사이에 Facede라는 객체를 세워놓음으로써 복잡한 관계를 정리 혹은 구조화하는 패턴이다. 

 

예를 들면 영화를 보기 위한 클라이언트가 있다. 조금은 억지스러운 예제이지만, 서브시스템으로는 Movie,Beverage라는 인터페이스와 이 인터페이스를 구현할 클래스가 있다.(영화를 보기 위해 음료를 구입하고 영화를 키고 음료를 다 마신다. 영화가 끝나면 영화를 끄고 다 먹은 음료컵을 버린다.) 물론 더 많은 서브시스템이 붙을 가능성도 있다. 퍼사드패턴을 적용하기 전에는 클라이언트가 영화를 보기위해서는 클라이언트 코드에 Movie,Beverage 등의 서브시스템을 의존하여 일일이 음료를 사고 영화를 보러 들어가고 등의 로직을 직접 호출해야한다.(만약 복잡한 서브시스템들이 더 많다면 더 많은 호출이 이루어질 것이다.) 그리고 하나의 기능을 수행하기 위해 여러개의 서브시스템들이 조합되어야 한다면 클라이언트는 여러개의 서비스를 직접 호출하여 로직을 만들어야 할 것이다.

 

그래서 퍼사드 패턴을 이용하여 모든 관계를 전면에 세워진 Facede 객체를 통해서만 이루어질 수 있게 단순한 인터페이스를 제공하는 것이다. 퍼사드 패턴을 이용하면 서브시스템 내부에서 작동하고 있는 많은 클래스들의 관계나 사용법을 의식하지 않고 퍼사드 객체에서 제공하는 단순화된 하나의 인터페이스만 사용하므로, 클래스 간의 의존 관계가 줄어들고 복잡성 또한 낮아지는 효과를 볼 수 있다.

 

여기서 퍼사드 객체는 클라이언트의 요청이 발생했을 때, 서브시스템 내의 특정한 객체에 요청을 전달하는 역할을 한다. 이 역할을 수행하려면 퍼사드 객체는 서브시스템의 클래스들에 어떤 기능이 있는지 알고 있어야 한다.(인스턴스 필드에 선언) 즉, 서브시스템은 자신의 기능을 구현하고 있으면 되고, 퍼사드 객체로부터 자신이 수행할 행위에 대한 요청을 받아 처리하면 되는 것이다. 또한 서브시스템은 퍼사드 객체의 어느 정보도 알고 있을 필요가 없다.

 

Facede Pattern의 장점

  • 퍼사드는 소프트웨어 라이브러리를 쉽게 사용할 수 있게 해준다. 또한 퍼사드는 소프트웨어 라이브러리를 쉽게 이해할 수 있게 해 준다. 퍼사드는 공통적인 작업에 대해 간편한 메소드들을 제공해준다.
  • 퍼사드는 라이브러리를 사용하는 코드들을 좀 더 읽기 쉽게 해준다.
  • 퍼사드는 라이브러리 바깥쪽의 코드가 라이브러리의 안쪽 코드에 의존하는 일을 감소시켜준다. 대부분의 바깥쪽의 코드가 퍼사드를 이용하기 때문에 시스템을 개발하는 데 있어 유연성이 향상된다.
  • 퍼사드는 좋게 작성되지 않은 API의 집합을 하나의 좋게 작성된 API로 감싸준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
 * 음료관련 서브시스템
 * @author yun-yeoseong
 *
 */
public interface BeverageService {
    
    public void prepare();
    public void takeout();
    public void gotoTrash();
    
}
 
public class BeverageServiceImpl implements BeverageService {
 
    @Override
    public void prepare() {
        System.out.println("음료준비");
    }
 
    @Override
    public void takeout() {
        System.out.println("음료주기");
    }
 
    @Override
    public void gotoTrash() {
        System.out.println("다먹은 음료컵 버리기");
    }
 
}
 
/**
 * 영화관련 서브 시스템
 * @author yun-yeoseong
 *
 */
public interface MovieService {
    
    public void prepare();
    public void turnOn();
    public void turnOff();
}
 
public class MovieServiceImpl implements MovieService {
 
    @Override
    public void prepare() {
        System.out.println("영화준비");
    }
 
    @Override
    public void turnOn() {
        System.out.println("영화틀기");
    }
 
    @Override
    public void turnOff() {
        System.out.println("영화끄기");
    }
 
}
 
 
/**
 * 영화,음료 시스템의 인터페이스를 하나의 인터페이스로
 * 몰아서 더 쉽게 서브 시스템들을 사용할 수 있게한다.
 * @author yun-yeoseong
 *
 */
public interface MovieFacede {
    
    public void prepareWatchMovie();
    public void watchMovie();
    public void endMovie();
    
}
 
public class MovieFacedeImpl implements MovieFacede {
    
    private MovieService movieService;
    private BeverageService beverageService;
    
    public MovieFacedeImpl(MovieService movieService,BeverageService beverageService) {
        
        this.movieService=movieService;
        this.beverageService=beverageService;
        
    }
    
    @Override
    public void prepareWatchMovie() {
        
        beverageService.prepare();
        beverageService.takeout();
        movieService.prepare();
        
    }
 
    @Override
    public void watchMovie() {
        
        movieService.turnOn();
        
    }
 
    @Override
    public void endMovie() {
        
        movieService.turnOff();
        beverageService.gotoTrash();
        
    }
 
}
cs

 

만약 Spring fw을 사용한다면 퍼사드 객체에 DI해주는 어노테이션을 붙이면 클라이언트에서 직접 의존 주입해줄 필요가 없어진다. 예제가 조금 억지스러운 면이 없지않지만 대략적으로 퍼사드패턴을 사용하는 이유와 사용법이 전달되었으면 좋겠다.

posted by 여성게
:
인프라/운영체제 2019. 7. 22. 23:48

컴퓨터 시스템은 데이터를 처리하는 물리적인 기계장치인 하드웨어와 어떤 작업을 지시하는 명령어로 작성한 프로그램인 소프트웨어로 구성된다. 운영체제는 컴퓨터 하드웨어를 관리하는 소프트웨어이다. 그러므로 운영체제를 이해하려면 먼저 컴퓨터 하드웨어에 대해 아는 것이 중요하다. 이번 포스팅에서는 하드웨어를 하나하나 깊숙히 알아간다기 보다는 어떠한 하드웨어가 있고 해당 하드웨어가 어떻게 구성되며 어떠한 역할을 하는지 정도만 알아보는 포스팅이 될듯하다.

 

컴퓨터 하드웨어는 크게 프로세서(CPU), 메모리(기억장치,RAM), 주변장치로 구성되고, 이들은 시스템 버스로 연결한다.

 

1.프로세서(CPU)

프로세서는 컴퓨터 하드웨어에 부착한 모든 장치의 동작을 제어하고 명령을 실행한다. 중앙처리장치(Central Processing Unit)라고도 한다. 프로세서는 밑의 그림과 같이 연산장치와 제어장치, 레지스터로 구성되고, 이들은 내부 버스로 연결한다.

제어장치에서 연산장치와 레지스터로 나가는 화살표는 제어흐름이고 레지스터와 연산장치 사이의 흐름은 데이터의 흐름이다. 위의 레지스터는 종류와 크기가 다양하다. 레지스터는 여러 관점으로 구분할 수 있다. 용도에 따라 전용 레지스터와 범용 레지스터로 구분할 수 있고, 사용자가 정보를 변경할 수 있는가에 따라 사용자 가시 레지스터와 사용자 불가시 레지스터로 구분할 수 있다. 그리고 저장하는 정보의 종류에 따라 데이터 레지스터, 주소 레지스터, 상태 레지스터 등으로 세분화할 수 있다. 사용자 가시 레지스터와 사용자 비가시 레지스터로 구분하면 아래표와 같이 레지스터들을 구분할 수 있다.

 

1-1 사용자 가시 레지스터

종류 설명
데이터 레지스터(DR) 함수 연산에 필요한 데이터를 저장한다. 값, 문자 등을 저장하므로 산술 연산이나 논리 연산에 사용하며, 연산 결과로 플래그 값을 저장한다.
주소 레지스터(AR) 주소나 유효 주소를 계산하는 데 필요한 주소의 일부분을 저장한다. 주소 레지스터에 저장한 값을 사용하여 산술 연산을 할 수 있다.
기준 주소 레지스터 프로그램을 실행할 때 사용하는 기준 주소 값을 저장한다. 기준 주소는 하나의 프로그램이나 일부처럼 서로 관련 있는 정보를 저장하며, 연속된 저장 공간을 지정하는 데 참조할 수 있는 주소이다. 따라서 기준 주소 레지스터는 페이지나 세그먼트처럼 블록화된 정보에 접근하는 데 사용한다.
인덱스 레지스터 유효 주소를 계산하는 데 사용하는 주소 정보를 저장한다.
스택 포인터 레지스터 메모리에 프로세서 스택을 구현하는 데 사용한다. 많은 프로세서와 주소 레지스터를 데이터 스택 포인터와 큐 포인터로 사용한다. 보통 반환 주소, 프로세서 상태 정보, 서브루틴의 임시 변수를 저장한다.

 

1-2 사용자 불가시 레지스터

사용자가 정보를 변경할 수 없는 레지스터이다. 보통 프로세서의 상태와 제어를 관리한다.

종류 설명
프로그램 카운터(PC) 다음에 실행할 명령어의 주소를 보관하는 레지스터이다. 계수기로 되어 있어 실행할 명령어를 메모리에서 읽으면 명령어의 길이만큼 증가하여 다음 명령어를 가리키며, 분기 명령어는 목적 주소로 갱신할 수 있다.
명렁어 레지스터(IR) 현재 실행하는 명령어를 보관하는 레지스터이다.
누산기(ACC) 데이터를 일시적으로 저장하는 레지스터이다.
메모리 주소 레지스터(MAR) 프로세서가 참조하려는 데이터의 주소를 명시하여 메모리에 접근하는 버퍼 레지스터이다.
메모리 버퍼 레지스터(MBR) 프로세서가 메모리에서 읽거나 메모리에 저장할 데이터 자체를 보관하는 버퍼 레지스터이다. 메모리 데이터 레지스터(MDR)라고도한다.

 

세분화된 레지스터로 프로세서(CPU)그림을 표현하면 대략 아래와 같다.

 

 

2.메모리

메모리는 컴퓨터 성능과 밀접하다. 당연히 크고 빠른 메모리는 가격이 비싸기 나름이다. 그렇기 때문에 메모리를 비용, 속도, 용량, 접근시간 대비 효율성을 높히기 위해 메모리 계층구조를 구성하여 상호보완한다. 메모리 계층구조는 아래와 같다.

위의 그림에서 메인 메모리를 중심으로 아래에는 대용량의 자기디스크, 이동이 편리한 광디스크, 파일을 저장하는 속도가 느린 자기테이프등이 있다.(현 시점에는 메모리 기술이 발달하면서 SSD라는 디스크가 나왔는데, 자기디스크로 된 하드디스크보다 입출력 속도가 빠르고 플래시 메모리로 구성되어 있다.) 그리고 메인 메모리 위에는 메인 메모리와 프로세서의 속도 차이를 보완하는 캐시가 있다. 최상위에는 프로세서가 사용한 데이터를 보관하는 가장 빠른 레지스터가 있다. 

 

프로그램을 실행하거나 데이터를 참조하려면 모두 메인 메모리에 올려야 한다. 그렇다고 무작정 메인 메모리를 크게할 수 없는데, 불필요한 프로그램과 데이터는 보조기억장치에 저장했다가 실행,참조할 때만 메인 메모리에 옮기는 원리를 적용하고 CPU에서 자주 사용되는 데이터는 캐시메모리에 임시저장한다거나 해서 서로 상호보완한다. 그리고 레지스터와 캐시, 메인메모리(메모리)는 프로세서(CPU)가 직접 접근할 수 있지만 보조기억장치(하드 디스크)는 프로세서(CPU)가 직접 접근할 수 없고 메인메모리에 올려야 접근할 수 있다.

 

2-1 메인메모리(주기억장치)

프로세서(CPU) 외부에 있으며, 프로세서에서 즉각적으로 수행할 프로그램과 데이터를 저장하거나 프로세서에서 처리한 결과를 메인 메모리에 저장한다. 입출력장치도 메인 메모리에 데이터를 받거나 저장한다. 메인 메모리는 다수의 셀로 구성되며, 각 셀은 비트로 구성된다. 셀이 k비트면 셀에 2^k 값을 저장할 수 있다. 메인 메모리에 데이터를 저장할 때는 셀 한 개나 여러 개에 나눠서 자장한다. 셀은 주소로 참조하는데, n비트라면 주소 범위는 0~2^n-1이다.

 

 

위처럼 컴퓨터에 주어진 주소를 물리적 주소라고 하는데, 프로그래머는 물리적 주소 대신 프로그래밍을 할때 함수(수식)나 변수를 사용한다. 그리고 컴파일러가 프로그램을 기계 명령어로 변환할 때 변수와 명령어에 주소를 할당하는데, 이 주소를 논리적 주소라고 한다. 논리적 주소는 별도의 주소 공간에 나타난다. 컴파일로 논리적 주소를 물리적 주소로 변환하는데, 이 과정을 매핑 또는 메모리 맵이라고 한다.

 

운영체제는 가상 메모리 방법을 사용하여 메인 메모리의 유효 크기를 늘릴 수도 있다. 메모리 속도는 메모리 접근시간과 메모리 사이클 시간으로 표현할 수 있다. 메모리 접근시간은 명령이 발생한 후 목표 주소를 검색하여 데이터 쓰기(읽기)를 시작할 때까지 걸린 시간이다(예:읽기 제어 신호를 가한 후 데이터를 메모리 버퍼 레지스터에 저장할 때까지 걸린 시간) 메모리 사이클 시간은 두 번의 연속적인 메모리 동작 사이에 필요한 최소 지연시간이다.(예:읽기 제어 신호를 가한 후 다음 읽기 제어 신호를 가할 수 있을 때까지 필요한 시간) 보통 사이클 시간이 접근시간보다 약간 길지만 메모리의 세부 구현 방법에 따라 다르다.

 

메인 메모리는 프로세서와 보조기억장치 사이에 있으며, 여기서 발생하는 디스크 입출력 병목 현상을 해결하는 역할도 한다. 그런데 프로세서와 메인 메모리 간에 속도 차이가 나면서 메인 메모리의 부담을 줄이려고 프로세서 내부나 외부에 캐시 메모리를 구현하기도 한다.

 

3. 캐시

프로세서 내부나 외부에 있으며, 처리 속도가 빠른 프로세서와 상대적으로 느린 메인 메모리의 속도 차이를 보완하는 고속 버퍼이다. 캐시는 메인메모리에서 데이터를 블록 단위로 가져와 프로세서에 워드 단위로 전달하여 속도를 높인다. 그리고 데이터가 이동하는 통로(대역폭)를 확대하여 프로세서와 메모리의 속도 차이를 줄인다.(캐시는 메인 메모리와 크기가 동일한 블록 여러 개로 구성되는데, 보통 8~64바이트 정도 크기의 블록으로 구성된다.)

 

캐시는 주소 영역을 한 번 읽어 들일 수 있는 크기로 나눈 후 각 블록에 번호를 부여하여 이 번호를 태그로 저장한다. 프로세서는 메인 메모리에 접근하기 전에 캐시에 해당 주소의 자료가 있는지 먼저 확인한다. 이를 위해 접근하려는 주소 24비트 중 태그에 해당하는 처음 22비트를 캐시의 모든 라인과 비교하여 일치하는 라인을 찾는다. 일치하는 라인이 있으면, 주소의 나머지 2비트를 이용하여 데이터 라인의 4개 바이트 중 해당하는 바이트를 가져온다. 

 

만약 해당 주소로 캐시에 자료가 있다면 캐시 적중, 없다면 캐시 실패(미스) 라고 한다. 보통 캐시에 데이터를 적재할 때, 적용하는 이론이 있다. 공간적 지역성(대부분의 프로그램이 참조한 주소와 인접한 주소의 내용을 다시 참조),시간적 지역성(한번 참조한 주소를 곧 다시 참조하는 특성)이다.

 

  • 프로그램이 명령어를 순차적으로 실행하는 경향이 있어 명렁어가 특정 지역 메모리에 인접해 있다.
  • 순환 때문에 프로그램을 반복하더라도 메모리는 일부 영역만 참조한다.
  • 대부분의 컴파일러를 메모리에 인접한 블록에 배열로 저장한다. 따라서 프로그램이 배열 원소에 순차적으로 자주 접근하므로 지역적인 배열 접근 경향이 있다.

4. 보조기억장치

주변장치 중 프로그램과 데이터를 저장하는 하드웨어로 2차 기억장치 또는 외부기억장치라고 한다. 자기디스크, SSD 등이 있다.

 

5. 시스템 버스

시스템 버스는 하드웨어를 물리적으로 연결하여 서로 데이터를 주고 받을 수 있게 하는 통로이다. 컴퓨터 내부의 다양한 신호(데이터 입출력신호, 프로세서 상태 신호, 인터럽트 요구와 허가 신호, 클록 신호 등)를 시스템 버스로 전달한다. 시스템 버스는 기능에 따라 데이터 버스, 주소 버스, 제어 버스로 구분한다.

 

종류 설명
데이터 버스 프로세서와 메인 메모리, 주변장치 사이에서 데이터를 전송한다. 데이터 버스를 구성하는 배선 수는 프로세서가 한 번에 전송할 수 있는 비트 수를 결정하는데, 이를 워드라고 한다.
주소 버스 프로세서가 시스템의 구성 요소를 식별하는 주소 정보를 전송한다. 주소 버스를 구성하는 배선 수는 프로세서와 접속할 수 있는 메인 메모리의 최대 용량을 결정한다.
제어 버스 프로세서가 시스템의 구성 요소를 제어하는 데 사용한다. 제어 신호로 연산장치의 연산 종류와 메인 메모리의 읽기나 쓰기 동작을 결정한다.

 

6. 주변장치

주변장치는 프로세서와 메인 메모리를 제외한 나머지 하드웨어 구성요소이다.

 

  • 입력장치 : 컴퓨터에서 처리할 데이터를 외부에서 입력하는 장치이다.
  • 출력장치 : 입력장치와 반대로 컴퓨터에서 처리한 데이터를 외부로 보내는 장치이다.
  • 저장장치 : 메인 메모리와 달리 거의 영구적으로 데이터를 저장하는 장치이다. 데이터를 입력하여 저장하며, 저장한 데이터를 출력하는 공간이므로 입출력장치에 포함하기도 한다.

 

컴퓨터 시스템의 동작

그렇다면 위의 하드웨어들을 종합하여 동작하는 컴퓨터 시스템의 동작은 어떻게 이루어질까?

 

  1. 입력장치로 정보를 입력받아 메모리에 저장한다.
  2. 메모리에 저장한 정보를 프로그램 제어에 따라 인출하여 연산장치에서 처리한다.
  3. 처리한 정보를 출력장치에 표시하거나 보조기억장치에 저장한다.

입력장치로 컴퓨터에 유입되는 정보는 명령어와 데이터로 분류한다. 명령어는 실행할 산술,논리 연산의 동작을 명시하는 문장으로, 어떤 작업을 수행하는 명령어의 잡합이 프로그램이다. 프로그램은 컴파일러 등을 이용하여 0과 1로 이진화된 기계 명령어로 변환해야 컴퓨터가 이해할 수 있다.

 

명령어의 구조

명령어는 프로세서가 실행할 연산인 연산부호와 명령어가 처리할 데이터, 데이터를 저장한 레지스터나 메모리 주소인 피연산자로 구성된다. 명령어는 프로세서에 따라 고정 길이나 가변길이를 구성한다. 연산 부호는 특별한 경우가 아니면 한 개이나 피연산자는 여러개 일 수 있다.

 

연산 부호 피연산자 1 ... 피연산자 n

 

  • 연산 부호(OPcode) : 프로세서가 실행할 동작인 연산을 지정한다. 예를 들어, 산순 연산(+,-,*,/), 논리연산(AND,OR,NOT..), 시프트, 보수 등 연산을 정의한다. 연산 부호가 n비트이면 최대 2^n개 연산이 가능하다.
  • 피연산자(operand) : 연산할 데이터 정보를 저장한다. 데이터는 레지스터나 메모리, 가상기억장치, 입출력장치 등에 위치할 수 있다. 보통 데이터 자체보다는 데이터의 위치 주소 값을 저장한다. 일반적으로는 아래와 같은 명령어 구조를 갖는다.

 

연산 부호 피연산자 1(목적지 피연산자) 피연산자 2(소스 피연산자)

 

y = x + b

y->목적지 피연산자

x,b->소스 피연산자

+->연산 부호

y = x + b
y->목적지 피연산자
x,b->소스 피연산자
+->연산 부호

 

명령어는 실행 전에 메인 메모리에 저장하며, 한 번에 하나씩 프로세서에 순차적으로 전송하여 해석,실행한다.

 

누산기 : 메모리에서 읽은 피연산자를 레지스터에 저장된 데이터와 연산할 때 사용하는 프로세서의 레지스터이다. 누산기는 프로그램의 명령어 수행 중에 산술,논리 연산의 결과를 일시적으로 저장한다.

누산기 : 메모리에서 읽은 피연산자를 레지스터에 저장된 데이터와 연산할 때 사용하는 프로세서의 레지스터이다. 누산기는 프로그램의 명령어 수행 중에 산술,논리 연산의 결과를 일시적으로 저장한다.

 

명령어에 피연산자의 위치를 명시하는 방법에는 두가지 방법이 있다. 피연산자에 데이터가 있는 레지스터나 메모리 주소를 지정하면 직접 주소라 하고, 데이터가 있는 레지스터나 메모리 주소 정보를 지정하면 간접 주소라고한다.

 

모드 비트 연산 부호 피연산자

위의 명령어 구조에서 모드 비트가 0이면 간접 주소, 1이면 직접 주소이다. 아래의 예를 보면,

 

<직접 주소>

1 101 001001

직접 주소지정 방식이며 001001(9)번 주소에 참조할 데이터가 있으므로 데이터는 한번의 메모리 참조로 가져올 수 있다.

 

<간접 주소>

0 101 001001

간접 주소지정 방식이며 001001(9)번 주소에 데이터의 주소 값이 저장되어 있어 001001 주소에 있는 주소 데이터를 다시 한번 참조하여 데이터를 가져온다. 즉, 데이터를 가져오기 위해 두번 참조한다.

 

명령어의 실행

  1. 명령어 인출 : 명령어 레지스터에 저장된 다음 명령어를 인출한다.
  2. 명령어 해석, 프로그램 카운터 변경 : 인출한 명령어를 해석하고 다음 명령어를 지정하려고 프로그램 카운터를 변경한다.
  3. 피연산자 인출 : 명령어가 메모리에 있는 워드를 한 개 사용하려면 사용 장소를 결정하여 피연산자를 인출하고, 필요하면 프로세서 레지스터로 보낸다.
  4. 명령어 실행
  5. 결과 저장
  6. 다음 명령어로 이동(1번부터 다시 시작)

프로세서의 제어장치가 명령어를 실행한다. 프로세서는 메모리에서 명령어를 한 번에 하나씩 인출하고 해석하여 연산한다. 명령어를 인출하여 연산 완료한 시점까지를 인출-해석-실행 사이클 또는 인출-실행 사이클이라고 한다. 간단히 명령어 실행 사이클이라고도 한다.

 

  인출 사이클 실행 사이클  
시작-> 인출-> 실행-> 종료

 

인출 사이클

인출 사이클은 명렁어 실행 사이클의 첫 번째 단계이다. 인출 사이클은 메모리에서 명령어를 읽어 명령어 레지스터에 저장하고, 다음 명령어를 실행하려고 프로그램 카운터를 증가시킨다. 

 

 

시간 레지스터 동작 설명
1 PC -> MAR PC에 저장된 주소를 프로세서 내부 버스를 이용하여 MAR에 전달한다.
2

Memory(MAR 저장된 주소)

->MBR

MAR에 저장된 주소에 해당하는 메모리 위치에서 명령어를 인출할 후 이 명령어를 MBR에 저장한다. 이때 제어장치는 메모리에 저장된 내용을 읽도록 제어 신호를 발생시킨다.
PC + 1 -> PC 다음 명령어를 인출하려고 PC를 증가시킨다.
3 MBR -> IR MBR에 저장된 내용을 IR에 전달한다.

 

실행 사이클

실행 사이클에서는 인출한 명령어를 해석하고 그 결과에 따라 제어 신호를 발생시켜 명령어를 실행한다.

 

간접 사이클

직접 주소 지정 방법을 사용하는 실행 사이클은 명령어를 즉시 수행하지만, 간접 주소 지정 방법을 사용하는 실행 사이클은 명령어를 수행하기 전에 실제 데이터가 저장된 주기억장치의 주소인 유효 주소를 한번 더 읽어 온다.

 

시간 레지스터 동작 설명
1 IR(addr) -> MAR IR에 저장된 명령어의 피연산자를 MAR에 전달한다.
2

Memoroy(MAR 저장된 주소)

-> MBR

MAR에 저장된 주소에 해당하는 메모리 위치에서 데이터를 인출한 후 이 데이터를 MBR에 저장한다. 이때 제어장치는 메모리에 저장된 내용을 읽도록 제어 신호를 발생시킨다.
3 MBR -> IR MBR에 저장된 내용을 IR에 전달한다.

 

인터럽트 사이클

인터럽트는 프로세서가 프로그램을 수행하는 동안 컴퓨터 시스템의 내부나 외부에서 발생하는 예기치 못한 사건을 의미한다. 프로세서는 실행 사이클을 완료한 후 인터럽트 요구가 있는 지 검사한다. 인터럽트 요구가 없다면 다음 명령어를 인출하고, 인터럽트 요구가 있으면 현재 수행 중인 프로그램의 주소(프로그램 카운터) 값을 스택이나 메모리의 0번지와 같은 특정 장소에 저장한다. 그리고 프로그램 카운터에는 인터럽트 처리 루틴의 시작 주소를 저장해 두었다가 인터럽트 처리를 완료하면 중단된 프로그램으로 복귀하여 계속 수행한다.

 

시간 레지스터 동작 설명
1 PC -> MBR PC의 내용을 MBR에 저장한다.
2 IntRoutine_Address -> PC 인터럽트 루틴 주소를 PC에 저장한다.
Save_Address -> MAR PC에 저장된 인터럽트 루틴 주소를 MAR에 저장한다.
3 MBR -> Memory(MAR) MBR의 주소에 있는 내용을 지시된 메모리 셀로 이동한다.

인터럽트는 현재 실행 중인 프로그램을 중단하고 다른 프로그램의 실행을 요구하는 명령어이다. 시스템의 처리 효율을 향상시키며, 프로그램이 실행 순서를 바꿔 가면서 처리하여 다중 프로그래밍에 사용한다.

또 인터럽트는 컴퓨터에 설치된 입출력장치나 프로그램 등에서 프로세서로 보내는 하드웨어 신호이다. 인터럽트를 받은 프로그램은 실행을 중단하고 다른 프로그램을 실행한다. 단일 프로세서의 컴퓨터는 명령어를 한 번에 한 개만 수행할 수 있지만, 인터럽트를 이용하면 중간에 다른 프로그램이나 명령어를 수행할 수 있다. 특히 인터럽트는 예상치 못한 사용자 입력, 갑작스런 정전, 컴퓨터 시스템에서 긴급 요청, 잘못된 명령어 수행, 입출력 작업 완료와 같은 상황을 시스템이 적절히 처리하는데 필요하다.

 

제어 버스 중에는 인터럽트 요청 회선(IRQ)이 있다. 인터럽트 요청 회선을 사용하면 키보드에서 입력이 발생하였을 때만 프로세서에 통보하여 처리하므로 프로세서가 일일이 입출력장치의 상태를 폴링하고 있을 필요가 없다.

 

인터럽트는 크게 인터럽트 요청과 인터럽트 서비스 루틴으로 구분할 수 있다. 인터럽트 요청 신호에 따라 수행하는 루틴이 인터럽트 처리 프로그램, 즉 인터럽트 서비스 루틴이다.

 

인터럽트가 도달하기 전에 프로그램 A를 실행하고 있고 인터럽트가 발생했다고 가정하면 프로세서에 인터럽트 신호가 도달하여 현재 프로그램 A의 명령어를 종료한다. 레지스터의 모든 내용을 스택 영역에 보낸다. 그리고 프로그램 카운터는 인터럽트 처리 프로그램(프로그램 B)의 시작 위치를 저장하고 제어를 넘긴 프로그램 B를 실행한다. 프로그램 B가 수행이 완료되면 스택 영역에 있던 내용을 레지스터에 다시 저장하며, 프로그램 A가 다시 시작하는 위치를 저장하고 중단했던 프로그램 A를 재실행한다.

 

인터럽트는 서브루틴 호출과 매우 비슷하지만, 몇 가지 면에서 다르다. 보통 서브루틴은 자신을 호출한 프로그램이 요구한 기능을 수행하지만, 인터럽트 처리 프로그램은 인터럽트가 발생했을 때 실행 중인 프로그램과 관련이 없을 수 있다. 그러므로 프로세서는 인터럽트 프로그램을 처리하기 전에 프로그램 카운터를 비롯해 중단된 프로그램으로 복귀하여 실행할 때 영향을 미치는 정보를 모두 저장해야한다.

 

 

 

여기까지 컴퓨터의 핵심이 되는 하드웨어들을 아주 간단히 다루어보았다. 사실 필자가 요즘 느끼고 있는 것이 기초가 중요하다라는 것이다. 이전에는 이론이 아닌 스킬베이스의 공부만 주로 하다보니 시간이 지날수록 기초의 탄탄함의 중요성이 느껴지고 코딩에도 해당 이론이 아주 중요할 때가 있다. 오늘은 간단하게 하드웨어들을 뭔지 정도만 다루어 보았고 다음 포스팅은 아마 진짜 운영체제에 대한 포스팅이 될 듯하다.

posted by 여성게
:
IT이론 2019. 7. 16. 10:45

오늘 포스팅할 내용은 애플리케이션 성능 분석 및 튜닝관련된 포스팅이다. 정말 고마우신 분들이 많은 조언과 평가를 해주셨고 그 중에 가장 기억에 남는 성능관련 이야기를 찾아서 공부하던 중 조대협님 블로그에 정말 처음 읽기에 좋고 혹은 정말 고민하는 부분이었던 사람들도 읽기 좋았던 성능관련 글이 있어서 공유한다.

 

<성능 엔지니어링에 대한 접근 방법>

 

성능 엔지니어링 대한 접근 방법 (Performance tuning)

성능 엔지니어링에 대한 접근 방법 조대협 성능 개선, Performance Tuning, 용량 선정 과 같은 튜닝 관련 용어들은 모든 개발자나 엔지니어에게 모두 흥미가 가는 주제일 것이다. 그 만큼 소프트웨어에서 고성능을..

bcho.tistory.com

 

성능 개선, Performance Tuning, 용량 선정 과 같은 튜닝 관련 용어들은 모든 개발자나 엔지니어에게 모두 흥미가 가는 주제일 것이다. 그 만큼 소프트웨어에서 고성능을 내는 시스템은 만들기도 힘들뿐더러, 고성능 시스템이란 즉 잘 설계되고 구현된 소프트웨어를 뜻하는 것이니 관심을 가지는 것이 당연하지 않을까 싶다.

필자의 경우, 엔터프라이즈 시스템에 대한 약 6년간 장애 해결, 장애 회피 설계, 성능 개선, 고성능 시스템 설계 및 구현에 관련된 일을 해왔다.  특히 장애 해결과 성능 개선 작업은 하고 나면 뿌듯하기는 하지만, 특정한 기술이 필요하기 보다는 문제를 정의하고 접근하는 능력과 끝까지 목표를 달성할 때까지 지루한 작업을 반복적으로 할 수 있는 인내심을 필요로 하는 작업이다

이번 챕터에서는 Performance Engineering의 전반적인 접근 방법과, 용량 산정 방법 그리고 자바 기반의 서버 애플리케이션에 대한 성능 튜닝 및 병목 발견 방법에 대해서 설명하고자 한다. 

Performance Engineering 의 정의와 범위

Performance Engineering은 시스템의 목표 성능 (응답 시간과 동시 접속자수)을 정의 하고, 이를 달성하기 위해서, 시스템의 구조를 반복적으로 개선하는 작업을  이야기 한다.

좁게 생각하면, 코드상의 병목을 잡고, 시스템의 설정(Configuration)을 바꿔서 성능을 올리는 튜닝으로 생각할 수 있지만, 성능 목표의 정의에서 부터, 최적의 성능을 내기 위한 디자인 및 구현과 같은 개발 초기의 설계 부분와 개발후의 운영단계에서 모니터링 까지 전과정을 포함한다.

Performance Engineering은 언제 해야 하는가?

Performance Engineering은 전체 소프트웨어 개발 과정에 걸쳐서 크게 아래와 같이 4단계에 걸쳐서 일어난다. 

 

 

위의 개발 모델은 전형적인 Water fall model이다. 개발프로세스 챕터에서도 설명하였지만, 스크럼과 같은 애자일 방법론을 사용하더라도 큰 범위에서 개발 사이클은 Waterfall 모델과 크게 다르지 않게 된다. (각 단계별을 SPRINT 단위로 수행한다.)

   분석 단계

초기 요구 사항 분석 및 시스템 기획 단계에서는 성능에 대한 목표를 정해야 한다.

목표 응답시간은 어떻게 되는지, 시스템을 사용할 총 사용자수와 동시에 시스템을 사용하는 동시접속자 수가 어떻게 되는지와 같은 성능 목표를 정의한다. 

또한 고려해야 하는 사항중의 하나는 성능 모델이다. 시스템에 부하가 어떤 패턴으로 들어오는지를 정의할 필요가 있다. 

예를 들어 일반적인 웹컨텐츠 사이트의 경우 사용자가 들어와서 페이지 컨텐츠를 1~3분 내에 읽고 다른 페이지로 이동하다가, 20 여분 후에는 로그아웃하거나 다른 사이트로 이동한다. 즉 한 사용자의 체류 시간은 20분정도되며, 총 평균 20 페이지를 보는 트렌젝션을 발생 시키고 나간다고 할 수 있다. 한글로 만든 사이트이고, 육아나 주부를 대상으로 한 사이트라고 가정하면, 시스템의 부하는 한국 시간으로 아이들이 학교나 유치원을 간후인 10~11시와, 저녁시간대인 10~12시 사이에 몰린다고 가정할 수 있다.

다른 예로 게임 시스템을 예로 들어보자, 주로 초등학생을 타켓으로 한 게임이라면, 방과후 시간인 3~5시 대에 부하가 가장 몰릴 것이며, 게임의 종류에 따라 다르겠지만, 스타크래프트와 같은 게임의 경우 한번 플레이에 40분 정도 소요가 되고, 한 사용자가 하루에 두번정도 게임을 한다 가정 아래, 사용자당 체류 시간은 2시간, 게임 횟수는 2/. 그리고 주요 부하는 3~5시 오후대 라는 성능 모델을 만들 수 있다.

초기 성능 정의는 서비스의 종류(,게임,기업 시스템,쇼핑,뱅킹등) , 서비스를 사용하는 사용자층, 그리고 서비스를 사용하는 지역. 즉 전세계를 서비스하는 시스템이라면 시스템의 부하는 365,24시간 거의 다 걸린다고 봐야 한다. 그러나 한국만을 대상으로 서비스 하는 한국어로 된 사이트인 경우, 새벽 시간에는 일반적으로 로드가 없는 것과 같은 한국의 시간대에 영향을 받을 뿐만 아리나 명절,휴일,휴가와 같은 한국이라는 국가 특성에 따라 시스템의 부하가 영향을 받는다.

   디자인 단계

다음으로는 디자인 단계에서는 목표 성능과 용량을 달성할 수 있는 규모의 시스템으로 설계를 진행한다.

성능 관점에서 시스템 디자인은 항상 Peak Time (최대 성능)에 맞춰서 디자인이 된다. 최대 성능을 기반으로 전체 시스템이 받아낼 수 있는 용량과 응답 시간을 고려해야 한다.

특히 성능과 용량은 애플리케이션 디자인 뿐만 아니라 Technology selection에도 많은 영향을 받는다. 어떤 하드웨어를 사용할 것인지, 어떤 미들웨어나 프레임웍을 사용할 것인지이에 따라 용량과 성능의 차이가 많이 발생하기 때문에, 디자인 단계에서 부터 성능과 용량을 감안해서 시스템을 설계해야 한다.

하드웨어 관점에서는 예전에는 성능 모델을 산정한 후에, Peak Time 기준 (최대 성능 요구)으로 시스템을 설계하고, 하드웨어를 구매 하였으나, 근래에는 클라우드를 이용하여 필요시에만 하드웨어를 탄력적으로 사용하는 Auto Scale Out 모델을 많이 사용한다. 

기업 내부의 업무 처럼 (예를 들어 이메일), 부하가 일정하고 예측이 가능한 경우에는 Fixed 된 사이즈의 하드웨어를 사용하도록 설계 하고, 출시 이벤트 행사 사이트와 같이 부하가 갑자기 몰리는 시스템의 경우 클라우드를 고려해보는 것도 권장할만 하다.

또한 빠른 응답 시간이 필요할 경우 SSD 디스크를 사용하거나, RAID 구성도 5보다는 1+0 등을 고려하는 등, 성능 모델에 따라서 적절한 하드웨어 선정과 구성 설계가 필요하다.

미들웨어나 프레임웍 관점에서도 정의된 성능 모델에 따라 적절한 제품군과  설계 구조를 채택해야 한다. 100,000 사용자 정도의 시스템 규모에서는 RDBMS 를 사용해도 성능이나 용량상에 문제가 없다. 그러나 50,000,000 사용자 정도를 지원해야 하는 시스템의 경우 그냥 RDBMS 를 사용할 수 없다. Sharding이나, NoSQL과 같은 다른 차원의 접근이 필요하다.

또한 빠른 응답 시간을 요구하는 경우 Redis Memcached와 같은 Cache 솔루션을 적극적으로 활용하거나, 미들웨어 부분에서는 Tomcat과 같은 일반적은 Web Application Server 보다는 Netty Vertex와 같은 고성능 미들웨어를 고려해볼 수 있다.

이러한 성능이나 용량에 관련된 제품 선정이나 설계는 돌려 보지 않으면 사실 확신을 가지기 어렵다. 그래서 가능하면, Technology selection 후에, 간단한 프로토타입을 구현한후에 시나리오가 단순한 대규모의 성능 및 용량 테스트를 해보는 PoC (Proof Of Concept)과 같은 작업을 이 단계에서 수행하는 것을 권장한다.

   개발단계

개발 단계는 개발프로세스 챕터에서 설명하였듯이, risk 가 높은 부분과 아키텍쳐에 관련되는 부분, 난이도가 높은 부분, 핵심 기능등을 개발 초기의 스프린트에서 개발한다.

초기 스프린트가 끝나고 릴리즈가 되서 성능 테스트가 가능한 QA나 스테이징 환경으로 시스템이 이전되면, Performance Engineering 역량을 이 단계에 집중하여, 시스템의 아키텍쳐와 모듈들이 성능 목표를 달성할 수 있는지 지속적으로 테스트하고 튜닝을 수행한다.

초기 단계에 성능 목표의 달성 가능 여부가 판단되어야, 아키텍쳐 변경이 가능하고, 주요 성능 이슈들을 초반에 발견해야, 발견된 성능 문제들에 대해서는 같은 문제가 발생하지 않도록 디자인 가이드나 코딩 가이드를 개발자들에게 배포하여 성능에 대한 위험도를 줄일 수 있다.

   최종 테스트 단계

앞의 단계에서 성능과 용량을 고려해서 설계가 되었고, 개발 초기 단계에서 성능과 용량 부분을 검증을 제대로 하였다면, 최종 테스트 단계에서는 개발된 최종 시스템에 대한 성능과 용량 부분의 측정과 미세 튜닝 (애플리케이션의 병목을 찾아서 부분적으로 수정하거나, 하드웨어나 미들웨어의 Configuration 하는 수준)을 하는 정도로 마무리가 되어야 한다.

이 과정에서는 실수로 잘못한 설정(configuration) 이나 잘못된 코딩으로 된 부분에 대해서 검증이 이뤄지는데, 이 경우에는 보통 2배에서 크게는 10배까지의 성능 향상이 이루어진다. 이런 경우는 대부분 실수에 의한 것이고 성능이 터무니 없이 낮게 나오기 때문에 찾기가 쉽다.

예를 들어 로그 파일을 NFS와 같은 리모트 디스크에 쓴다던지, Intel 계열의 CPU에서 하이퍼쓰레딩을 ON을 안했다던지와 같이 실수에 의한 경우가 많다.

이런 오류성의 문제들이 해결되면 실제 미세 튜닝에 들어가게 되는데, JVM 튜닝이나 톰캣의 설정 튜닝, SQL 튜닝들이 이루어지는데, 이 미세 튜닝을 통해서는 비약적인 성능향상은 이루어나지 않는다. 보통 20% 내외 정도 성능이 올라간다고 보면 된다. 

   운영 단계

마지막으로 시스템이 운영 단계로 넘어가게 되면, 테스트시에 발견되지 않은 성능적인 문제가 있을 수 있기 때문에, 모니터링 도구를 사용하여 지속적으로 성능을 모니터링 하고, 성능상에 문제가 있는 부분을 지속적으로 수정해야 한다. 웹서버의 access로그에서 응답 시간을 모니터링 하거나, 제니퍼(http://www.jennifersoft.com)과 같은 전문적인 APM (Application Performance Monitoring)툴이나, Ganglia와 같은 시스템 모니터링 도구를 사용하면, 시스템의 성능 상태를 잘 알 수 있다.

더불어 용량 부분에 대해서도 운영단에서는 고민을 해야 하는데, 일반적으로 PEAK Time의 시스템 용량이 (CPU) 80% 정도에 다다르면, 시스템 용량 증설을 고려해야 한다.

그리고 업무에 특성에 맞게 미리미리 용량을 준비해놓는게 좋다. 예를 들어 대학의 수강 신청 시스템의 경우, 학기 시작하는 날에 부하가 폭주하기 때문에,  클라우드 기반일 경우 수강신청 전에 시스템 수를 미리 늘려놓는다던지, 클라우드가 아닌 경우, 수강 신청 기간을 앞뒤로 서버를 임대해서 용량을 늘려놓는 등의 대책을 미리 세워놓을 수 있다.

마지막으로, 운영 단계에서 Performance Engineering 관점으로 챙겨야 하는 부분은 운영 로그의 수집이다. 성능 및 용량 목표 설정은 매우 중요한 과정이다. 특히 용량 목표의 경우에는 기존의 업무 시스템의 사용 패턴을 분석 하는 것이 가장 효율적이기 때문에 운영 시스템의 로그를 수집하고 분석하여 운영 중인 업무 시스템의 성능 모델을 분석 및 보유 해놓는 것이 좋다.

시스템 용량 산정 (Capacity Planning)

  더 자세한 설명에 들어가기 앞서서, 성능에 관련된 용어와 함께 시스템의 목표 용량 산정 방법에 대해서 이야기 해보도록 하자.이 용어는 이 글에서 정의하는 의미의 용어이며, 다른 성능 이론에서 언급되는 용어와 다소 다를 수 있다.

l  Response Time (응답 시간) : 사용자가 서버에 요청을 한 시간에서 부터, 응답을 받을 때 까지의 모든 시간을 포함한다. 이 응답시간은 내부적으로 다음과 같이 조금 더 세분하게 분리된다.

 

 

Network Time (또는 Latency time). 서버에 요청을 했을때, Request를 보내고 받을 때 소요되는 네트워크 시간을 의미한다.

Transaction Time : 서버에서 실제 트렉젝션이 처리되는 시간을 의미 한다.

Think Time : 사용자가 요청에 대해서 응답을 받은 후에, 웹페이지를 보거나 화면을 보는 등의 작업을 하는 시간의 의미한다.

예를 들어 보면 한국의 사용자가 미국이 페이스북을 사용한다고 했을때, 사용자가 웹 브라우져에서 클릭을 하면, 요청이 서버로 도달할때 까지 걸리는 시간 Network time (Request), 서버가 요청을 받아서 처리를 하고, 응답을 하는 시간 (Transaction Time), 그리고 그 응답이 사용자의 브라우져 까지 도착하는 시간이 Network time (Response) 이다. 이 전체 시간을 합친 것이 Response Time이 된다.

응답을 받은 후에는 사용자가 페이스북 내용을 보는데 소요 되는 시간이 Think Time이 된다.

Think Time 까지 포함하여 다음 요청이 발생하기 까지의 전체 시간을 Request Interval 이라고 한다.

l  Concurrent User (동시 사용자) : 시스템을 현재 사용하고 있는 사용자를 정의한다. 웹사이트를 사용하기 위해서, 현재 브라우져를 열어놓고 웹사이트를 보고 있는 것과 같이 현재 시스템을 사용하고 있는 사용자 수를 의미 한다.

 

 

위의 그림을 보자, 5명의 사용자 A~E가 있다고 가정했을 때, 단위 시간 10분동안에 Transaction  Time Think Time중에 있는 사용자는 A,B,C  3명으로 해다 시간 10분간의 Concurrent User 3명이 된다.

l  Active User (액티브 사용자) : 현재 시스템에 트렌젝션을 실행하여 부하를 주고 있는 사용자를 정의한다.

기존에는 Concurrent User Active User간의 차이가 없었다. 이 개념은 웹이 생기면서 구체화된 개념인데, 웹 사이트를 사용하기 위해서 컴퓨터 앞에 앉아 있는다고 하더라도, 웹 페이지가 로딩 되는 순간에만 서버는 부하를 받고, 페이지가 웹 브라우져로딩 된 후에는 부하를 받지 않고 사용자는 로딩된 페이지를 보는데 시간이 발생한다. 이 시간동안에는 서버는 부하를 받지 않는다. 즉 시스템을 사용하기 위해서 웹 사이트를 열어 놓고 있는다 하더라도 지속적으로 서버에 부하를 주는 것이 아니기 때문에 Concurrent  User Active User 의 개념 차이가 발생한다.

Active User는 클릭을 발생시켜서 그 시간 당시에 서버에 트렌젝션을 발생 시키는 사용자를 의미한다.

Active User의 수는 서버에서 순간 실행되고 있는 Thread  (쓰레딩 기반의 자바 서버의 경우)  Process의 수와 같다.  Active User의 수는 실제로 서버가 동시에 처리할 수 있는 트렌젝션의 양을 판단할 수 있는 기준이 되기 때문에 매우 중요한 성능 Factor가 된다.

 

 

위의 그림을 보자, 위의 그림에서 특정 순간에 있는 사용자는 총 5 명으로 Concurrent User  5명이지만, Transaction Time 구간중의 있는 사용자는 A,B,C ,  Active User 3명이 된다.

l  Transaction (트렌젝션) : Transaction이란, 사용자로 부터의 요청을 다루는 단위를 정의 한다. 이 정의가 상당히 중요한데, 성능 모델링이나 성능 테스트 시 이 Transaction의 정의에 따라서 시스템의 성능이 매우 다르게 정의 된다.

예를 들어서 사용자가 웹 페이지를 클릭했을때, 그 페이지에 대한 응답을 받는 것 까지를 하나의 트렌젝션이라고 정의 하자.

이 때, 웹페이지에는 서버에서 생생된 HTML 이외에, 여기서 참고 하는 리소스 즉, 이미지나 동영상, 자바 스크립트들이 들어있을 수 있다. 이 경우 트렌젝션에 대한 응답 시간을 측정할때, HTML 생성 이외에 이러한 리소스들을 로딩 하는 것 까지 하나의 트렌젝션으로 정의 해야 하느냐를 고려해야 한다.리소스에 로딩을 트렌젝션의 범위로 넣게 되면 전체 시스템의 응답 시간은 떨어지게 된다. (리소스를 로딩할 때 까지 기다려야 하니). 

이러한 트렌젝션의 정의는 무엇을 판단 기준으로 할것인가에 따라 결정이 되는데, 예를 들어 리소스를 톰캣과 같은 WAS에서 처리하지 않고 앞단의 CDN이나 웹서버에서 처리할 경우 톰캣은 리소스에 대한 트렌젝션 요청을 받지 않기 때문에, 전체 시스템에서 비지니스 로직에 대한 처리 성능을 측정하고자 할 때는 리소스에 대한 로딩 시간을 계산하지 않고 트렌젝션을 정의 한다.  또한 리소스에 대한 로딩은 비지니스 로직에 대한 처리에 비해서 부하가 상대적으로 매우 적고, 일반적으로 브라우져에 캐쉬되기 때문에 보통 서버의 성능 측정시 이러한  리소스 로딩에 대한 부하는 트렌젝션의 단위로 처리하지 않는 경우가 많다.

l  TPS(Transaction Per Second) : 초당 처리할 수 있는 트렌젝션의 양을 정의 한다. 주로 서버의 성능 평가 기준이 된다.

Active 사용자가 순간 Transaction을 처리한다고 하면, 이를 목표 응답시간 (Response Time)으로 나눈 값이 목표 TPS가 된다. 예를 들어, Active User 50 명이고, 개당 Response Time 2초 라고 하면, 이 시스템의 TPS 25 TPS가 된다.
 Network time이 미세하다고 판단하여, Network time 0으로 가정하여 계산

l  HPS(Hit Per Second) : 시스템이 처리할 수 있는 모든 웹 request의 초당 처리량이다. TPS가 비지니스 트렌젝션에 대한 처리 시간만을 정의 한다면, HPS는 리소스 (이미지, 자바스크립트)에 대한 request 처리량을 포함하기 때문에, TPS에 비해서 10~20 배 정도 높게 나온다. 

l  Peak Time(피크 타임) : 서버가 순간적으로 가장 부하를 많이 받는 순간을 정의 한다. 보통 서버의 용량 산정이나 성능 설계는 이 시간의 부하량을 기준으로 한다

일반적인 업무 시스템의 경우, 출근 9~930분 사이가 가장 부하가 높다. 이 때 Peak (최고 정점)을 찍는 순간의 동시 사용자 수와 기준 응답 시간을 목표로 성능 목표를 정의 하는 것이 일반적이다.

위의 개념을 정리해서 공식화 해보자.

   TPS = (Active User) / (Average Response Time) – F1

   TPS = (Concurrent User) / (Request Interval) – F2

   Active User = TPS * (Average Response Time) – F3

   Active User = (Concurrent User) * (Average Response Time) / (Request Interval) – F4

   Active User = (Concurrent User) * (Average Response Time) / [ (Average Response Time) + (Average Think Time) ] – F5

예를 들어 Concurrent User 300명이고, 목표 응답시간이 3초 이내이며, Think Time 15초 인 시스템의 경우, F5 공식에 따라서 Active User 300*3/(3+15) = 50 이 되며, 시스템의 Thread 또는 적정 Process 양은 50개가 된다. 목표 TPS는 약 16.6 TPS가 된다.

위의 공식은 어디까지나 이론적인 공식이다. Network Latency 값은 가변적이며, Think Time 또한 유동적이다. 그러나 용량 산정에는 어느 정도의 산정 기준이 필요하기 때문에, 이 공식을 사용하면 대략적인 시스템에 대한 요구 용량을 예측할 수 있다. 

 

Performance Engineering 의 절차 

그러면 어떤 절차로 성능과 용량을 측정하고 개선하는 절차에 대해서 알아보도록 하자. 

성능 목표와 모델의 정의

먼저 주요 업무 패턴이나, 튜닝의 대상이 되는 시나리오에 대한 개별 성능 목표를 정의 한다. 예를 들어 전체 성능 목표가 1,000 동시 사용자에 대해서 응답 시간 1초내의 시스템이 전체 성능 목표라고 가정하고, 전체 성능 목표를 대략 1,000 TPS (Transaction Per Second)라고 하자. 이것이 바로 성능 목표가 된다.

다음으로 성능 모델을 정의 해야 하는데, 해당 시스템의 주요 사용자 시나리오가 여러개 있을 때, 각 시나리오별의 사용 비중을 정의 해야 한다.

예를 들어 사진을 저장하는 클라우드 서비스 시나리오가 있다고 하면, 이 서비스의 주요 사용자 시나리오는

   로그인

   사진 리스트

   사진 업로드

   사진 보기

   사진 다운로드

   로드 아웃

등이 된다. 이 중에서 한 사용자가 실행하는 비율을 따져야 한다. 즉 사용자가 로그인 한후, 리스트 보기를 10, 업로드를 2, 보기를 5, 그리고 다운로드를 1번 한후에 로그 아웃 한다고 하자. 그러면 비율은 다음과 같이 된다. (전체 트렌젝션 횟수 1+10+2+5+1+1 = 20)

성능 모델 :로그인의 비율 5%, 리스트 보기 50%, 업로드 10%, 보기 25%, 로그아웃 5%

이 비율을 기준으로 복합 시나리오 (전체 시나리오를 함께 돌리는) 부하테스트를 수행하였을때, 1000 TPS가 나와야 하고, 각 개별 시나리오에 대해서 최소한, 로그인의 경우 1000 TPS 5% 50 TPS, 리스트 보기는 500 TPS를 상회 해야 한다. 

부하 생성

성능 모델이 정의 되었으면, 이 모델에 따라서 부하를 생성해야 한다.

부하 생성 도구는 여러가지가 있다. 대표적인 오픈 소스 도구로는

가장 간단하게 쓸 수 있는 도구로는 Apache AB 라는 명령어 기반의 도구가 있으며, 복잡한 스크립트를 지원할 수 있는 도구로는 grinder apache JMeter 등이 있으며, NHN에서 grinder enhancement해서 만든 (GUI가 지원되는) nGrinder라는 도구가 있다. 

근래에는 국내에서는 nGrinder라는 도구가 많이 사용되고 있다.

성능 모델이 단순하고, 테스트 시나리오가 간단할 경우에는 Apache ab 등으로도 가능하지만, 스크립트가 복잡해지는 경우에는 nGrinder와 같은 도구가 유리 하다. 

또한 부하 생성에 사용되는 스크립트는 복잡도가 생각보다 높고, 향후 regression(회귀) 테스트에도 재 사용되기 때문에, 반드시 형상 관리 시스템을 통해서 (VCS) 관리 하는 것을 권장한다.

※ 자세한 부하 테스트에 대한 방법은 “4장 테스트의 시스템 테스트  부분을 참고하기 바란다.

 

※ 클라우드 컴퓨팅과 부하 테스트 툴 라이센스 모델에 대해서

 

예전에는 부하 테스트가 사내에서 사내에 있는 시스템을 대상으로 했었기 때문에 큰 문제가 없었다. 그러나 근래 들어서 클라우드 컴퓨팅을 사용하는 사례가 늘어남에 따라, 서비스 시스템이 회사 밖에 즉, 클라우드에 있는 경우가 많아 졌다.

상용 부하 테스트툴의 경우에는 부하 발생기의 위치와 툴 사용자에 대해서 제약을 두는 경우가 있는데, 툴을 구매했다 하더라도, 부하 테스터의 controller (부하 발생기 제외)는 반드시 사내에 있어야 하며, 사용자 역시 그 회사의 내부 직원으로만 한정하는 경우가 있다.

예를 들어, 내가 부하 테스트 도구를 서울에 있는 회사에서 구매하여, 이 툴을 Amazon 클라우드 미국에 설치하고 부하 테스트를 미국 지사 직원을 통해서 진행하는 것이 불가능 하다.

이 경우 부하 테스트 툴의 Controller는 한국 서울 사무소에 설치하고, 부하 생성기만 Amazon에 설치한후 한국 서울 사무소 직원을 통해서만 사용해야 한다.

 

간혹 (이럴리는 없어야 하겠지만) 부하 테스트 툴의 판매 회사 영업 사원이 이러한 사실을 제대로 통보하지 않아서, 툴을 잘 쓰다가 갑자기 영업 사원이 변경되거나, 부하 테스트 툴의 이전을 요청 하였을때, 갑자기 벤더로 부터, 추가 라이센스 구매 요청을 받을 수 있으니, 구매 전에 반드시 구매 조건에 사용 시나리오와  Controller 위치, 사용 주체 및 테스트 대상 시스테들에 대해서 명시적으로 기재 하고 구매 계약을 추진 하는 것이 좋다.

 

 

테스트 및 모니터링

부하 테스트 준비가 되었으면, 부하 테스트를 진행하고 진행중에 주요 성능 Factor에 대해서 지속적으로 모니터링 및 기록을 하여야 한다. 주로 모니터링해야하는 Factor들은 다음과 같다.

 

 

   애플리케이션 관점 

가장 기본적으로 애플리케이션 즉 시스템의 성능을 측정 해야 한다. 주요 모니터링 Factor는 다음과 같다.

Response Time : Request 별 응답 시간

TPS (Throughput per second) : 초당 요청(Request) 처리량

 Factor들이 궁극적으로 성능에 대한 최종 목표 값이 되기 때문에, 가장 중요한 성능 Factor가 되며, 부하 생성 도구를 통해서 손쉽게 측정할 수 있다.

   미들웨어 관점 

미들웨어는 애플리케이션이 동작하기 위한 기본적인 솔루션이다.. Apache와 같은 웹서버나 Tomcat과 같은 Web Application 서버 , RabbitMQ와 같은 Message Queue, MySQL과 같은 데이타 베이스 등이 이에 해당한다.

각 성능 시나리오별로, 거쳐 가는 모든 미들웨어들을 모니터링해야 하는데, 이를 위해서는 각 솔루션에 대한 개별적인 깊은 이해가 필요하다.

웹서버의 경우 거의 성능 문제가 되는 부분은 없다. 성능 문제가 발생하는 부분은 대부분 Network outbound io (bandwidth)쪽이 되는 경우가 많다. 웹서버가 설치된 하드웨어의 network out bound bandwidth를 모니터링 하는 것이 유용하다.

대부분의 성능 문제는 실제 애플리케이션 로직이 수행되는 Tomcat과 같은 application server와 데이타 베이스단에서 많이 발생하는데, application server의 경우에는 Thread의 수와 Queue의 길이가 1차 모니터링 대상이 된다.

서버가 용량을 초과 하게 되면, Idle Thread수가 떨어지게 되고, Idle Thread 0이 되면 request message가 앞단의 queue에 저장되게 된다. 그래서 이 두 개를 모니터링 하면 시스템이 병목 상태인지 아닌지를 판단할 수 있다. 이 값들은 JMX (Java Management Extension) API를 이용하여 모니터링 하면 된다.

DB의 경우에는 slow query를 모니터링하면 특히 느리게 수행되는 쿼리들을 잡아서 튜닝할 수 있다. MySQL 5.6의 경우 slow query http://dev.mysql.com/doc/refman/5.6/en/slow-query-log.html

를 사용하면 쉽게 잡아낼 수 있다.

Slow query를 찾았으면, EXPLAIN 명령어를 이용하여 query의 수행 내용을 분석한후 Index등의 튜닝을 수행할 수 있다. 

http://dev.mysql.com/doc/refman/5.0/en/using-explain.html

   인프라 관점 : CPU, Memory, Network IO, Disk IO

다음으로 하드웨어 인프라에 대한 부분을 지속적으로 모니터링해줘야 하는데, 이는 하드웨어가 해당 성능을 내기 위해서 용량이 충분한지 그리고 하드웨어 구간에서 병목이 생기지는 않는지, 생긴다면 어느 구간에서 생기는지를 모니터링하여, 해당 병목 구간에 대한 문제 해결을 하기 위함이다.

인프라에 대한 모니터링은 Ganglia Cacti와 같은 전문화된 인프라 모니터링 도구를 사용하거나 top이나 glance, sar와 같은 기본적인 Unix/Linux 커맨드를 사용해서도 모니터링이 가능하다. (부하 테스트주에 top 등을 띄워놓고 모니터링을 하는 것이 좋다. Load Runner와 같은 상용 도구의 경우에는 부하 테스트 툴 자체에서 테스트 대상 시스템에 대한 하드웨어 사용률을 함께 모니터링할 수 있게 제공해준다.)

CPU : 일반적으로 CPU는 대부분 잘 모니터링 한다. 목표 성능을 달성할 시에는 보통 70~80% 정도의 CPU 를 사용하는 것이 좋고, 20~30%의 여유는 항상 가지고 가는 것이 좋다 이유는, 70~80% 정도의 CPU가 사용된 후에, 하드웨어를 물리적으로 늘리는 시간에 대한 여유 시간을 가지기 위함이다. 하드웨어는 특성상 주문을한다고 해도, 바로 그 시간에 증설을 할 수 있는 것이 아니고, CPU  100%가 되는 순간에는 이미 애플리케이션이 CPU 부족으로 제대로 작동을 하지 못하는 경우가 많기 때문에, 항상 여유를 남겨 놓고 성능 목표를 정의 하는 것이 좋다. 그래서 성능 목표를 잡을 때는 “CPU 70%, 500 TPS, 응답시간 1.5초 내외 식으로 하드웨어에 대한 사용률을 포함하는 것을 권장한다.

Memory : 다음으로는 Memory 부분이다. Peak Time시에 Memory가 얼마나 사용되느냐가 중요한데, Java Application의 경우 특성상, 전체 JVM 프로세스가 사용할 메모리량을 미리 정해놓기 때문에, 부하 테스트 중에도 메모리 사용량 자체는 크게 변화하지 않는다. 다만 자주 놓치는 점이 swapping status 인데, Unix/Linux는 시스템의 특성상 물리 메모리 이상의 메모리를 제공하기 위해서 virtual memory 라는 개념을 사용하고 swapping space라는 디스크 공간에 자주 사용하지 않는 메모리의 내용을 dump해서 저장한 후 다시 사용할때 memory loading 하는 방식을 사용한다. 그런데 이 메모리의 내용을 디스크에 저장 및 로드 하는 과정 (swapping이라고 함)이 실제 disk io를 발생 시키기 때문에, 실제 메모리 access 성능이 매우 급격하게 떨어진다. 그래서 시스템에서 system에서 swapping이 발생하면 시스템의 성능이 장애 수준으로 매우 급격하게 떨어진다.

부하 테스트 중이나, 운영 중에 swapping이 발생하게 되면 전체 메모리 사용량을 줄이도록 튜닝을 하거나, 반대로 물리 메모리를 늘리는 증설 과정이 필요하다. 

Disk IO : Disk IO는 파일 시스템에 파일을 저장하는 시나리오나, Log를 저장하는 모듈 그리고 데이타 베이스와 같이 뒷단에 파일 시스템을 필요로 하는 모듈에서 많이 발생을 한다. Ganglia와 같은 도구를 사용하면, IOPS (Input Out per Second - 초당 read/write등의 IO 발생 횟수)를 통해서 모니터링할 수 있고, 또는 iostat sar와 같은 명령어를 이용하면 iowait 를 통해서 디스크 IO pending이 발생할 경우 디스크 병목이 있는지 없는지를 확인할 수 있다. 

 

 

Figure 1. iostat

또는 Process Disk IO iotop과 같은 툴을 사용하면 조금 더 상세한 정보를 얻을 수 있다.

 

 

Figure 2. iotop

[1]

Disk IO에 대한 Bottleneck은 여러가지 해결 방법이 있다. 먼저 하드웨어 인프라 ㅈ체에서 접근 하는 방식은, 디스크 자체를 SSD로 변경하거나, 버퍼가 크거나 RPM이 높은 디스크로 변경하는 방식, 인터페이스를 SATA에서 SAS SSD와 같은 높은 IO를 제공하는 디스크 인터페이스로 변경, Disk Controller iSCSI에서 FC/HBA와 같은 광케이블 기반의 고속 컨트롤러를 사용하는 방식 또는 RAID 구성을 Stripping 방식으로 변경해서 IO를 여러 디스크로 분산 시키는 방식 등이 있으며, 애플리케이션 차원에서는 데이타 베이스 앞에 memcache와 같은 캐슁을 사용하거나, 로깅의 경우에는 중간에 message queue를 써서 로그를 다른 서버에서 쓰도록 하여 IO를 분산하거나 또는 Back write와 같은 방식으로 로그 메세지가 발생할때 마다 disk writing 하는 것이 아니라 20 30개씩 한꺼번에 디스크로 flushing 하는 방식등을 이용할 수 있다.

또는 조금더 높은 아키텍쳐 레벨로는 디스크 IO가 많이 발생하는 로직의 경우 동기 처리에서 message queue를 사용하는 비동기 방식으로 시스템의 설계를 변경하는 방법을 고민할 수 있다. 예를 들어 사진을 올려서 변환하는 서비스의 경우 파일을 업로드 하는 시나리오와 변경하는 모듈을 물리적으로 분리하여, 파일 업로드가 끝나면, 사용자에게 동기 방식으로 바로 응답을 줘서 응답 시간을 빠르게 하고, 업로드된 파일은 뒷단에서 비동기 프로세스를 통해서 변환 과정을 다 끝낸 후에 사용자에게 변환이 끝나면 알려주는 방법을 사용할 수 있다.

Network IO: Network IO는 특히 고용량의 파일이나 이미지 전송에서 병목이 많이 발생하며, Reverse Proxy, NAT (Network address Translator), Router, Load Balancer 등에서 많이 발생한다. 여러가지 지점과 장비에 대해서 모니터링 해야 하기 때문에, 일반적인 unix/linux command 를 사용하는 방법보다는 Cacti Ganglia와 같은 RRD 툴이나 OpenNMS와 같은 NMS (Network Management System)을 사용하는게 좋다.

그래프를 보면서 추이를 지켜 보는 것이 중요한데, 부하를 넣으면 일정 수준이 되어도, 시스템들의 CPU나 메모리, Disk등의 기타 자원들은 넉넉한데, Network Input/Output이 일정 수준 이상으로 올라가지 않는 경우가 있다. 이 경우는 네트워크 구간의 병목일 가능성이 높다.

특히 소프트웨어 기반의 Load Balancer, 소프트웨어 기반의 NAT 장비에서 많이 발생하는데, 이미지와 같은 정적 컨텐츠는 가급적이면 CDN이나 분리된 Web Server를 이용해서 서비스 하도록 하는 것이 좋다. 클라우드의 경우에는 특히나 소프트웨어 기반의 NAT Load Balancer를 사용해서 문제가 되는 경우가 많은데, NAT의 경우에는 여러개의 NAT를 사용해서 로드를 분산하도록 하고, Load Balancer의 경우에도 충분히 큰 용량을 사용하거나 2개 이상의 Load Balancer를 배포한 후 DNS Round Robine등을 사용하는 방법을 고려 하는 것이 좋다.

개선 (Tuning)

병목을 찾았으면, 해당 병목 문제를 해결 및 반영해야 한다.

튜닝은 병목 구간이 발생하는 부분에 대한 전문적인 지식을 필요로 하지만, 기본적인 접근 방법은 거의 같다고 보면 된다.

   문제의 정의 : 성능 개선의 가장 기본은 문제 자체를 제대로 정의 하는 것이다. “그냥 느려요가 아니라, “성능 목표가 350TPS 1초내의 응답 시간인데, 현재 60 TPS 5초의 응답 시간에 WAS CPU 점유율이 100% 입니다.”와 같이 명확해야 하며, 문제점이 재현 가능해야 한다. 

특히 재현 가능성은 매우 중요한 점인데, 테스트 환경이 잘못되었거나, 외부적 요인 예를 들어 부하 테스트 당시 네트워크 회선이 다른 테스트로 인하여 대역폭이 충분히 나오지 않았거나 했을 경우 결과가 그 때마다 다르게 나올 수 있다.

즉 문제 자체를 명확하게 정의할 필요가 있다.

   Break down : 다음으로는 문제가 발생하는 부분이 어떤 부분인지를 판단해야 한다. 시스템은 앞단의 로드밸런서나 미들웨어, 데이타 베이스와 같은 여러 구간에서 발생을 한다. 그렇기 때문에, 성능 저하의 원인이 정확하게 어느 부분인지를 인지하려면, 먼저 성능 시나리오가 어떤 어떤 컴포넌트를 거치는지를 명확하게 할 필요가 있다. 이 과정을 break down이라고 한다. 이 과정을 통해서 전체 성능 구간중, 어느 구간이 문제를 발생 하는지를 정의한다.

   Isolate : 다음으로는 다른 요인들을 막기 위해서, 문제가 되는 구간을 다른 요인으로 부터 분리 (고립) 시킨다. 물론 완벽한 분리는 어렵다. 애플리케이션이 동작하기 위해서는 데이타 베이스가 필수적으로 필요하다. 이 경우에는 데이타 베이스를 분리할 수 는 없다. 그러나 예를 들어 시나리오 자체가 로그인 시나리오이고 Single Sign On을 통해서 로그인 하는 시나리오라서 SSO 시스템과 연동이 되어 있다면, SSO 연동을 빼고 다른 mock up을 넣어서 SSO와의 연결성을 끊고 테스트를 하는 것이 좋다. 

이렇게 문제에 대한 다른 요인과의 연관성을 최대한 제거 하는 작업이 isolation이다. 

   Narrow down : 문제를 isolation을 시켰으면, 근본적인 문제를 찾기 위해서 문제의 원인을 파 내려간다. Profiling을 하거나, 코드에 디버그 정보를 걸어서 문제의 원인을 분석하는 과정을 narrow down이라고 한다. 특히나 이 narrow down 과정은 분석을 위한 여러가지 기법이나 도구들을 사용해야 하고, 현상에 대한 이해를 하기 위해서는 해당 솔루션이나 기술 분야에 대한 전문성은 필수적으로 필요하다. 

   Bottleneck 발견 : Narrow down을 해서 문제의 원인을 계속 파해쳐 나가면 병목의 원인이 되는 근본적인 문제가 판별이 된다. 

   해결 : 일단 병목의 원인을 찾으면 해결을 해야 하는데, 찾았다고 모두 해결이 되는건 아니다. 데이타 베이스 index를 걸지 않아서 index를 걸어주면 되는 간단한 문제도 있을 수 있지만, 근본적인 솔루션 특성이나 설계상의 오류로 인해서 문제가 발생하는 경우도 있다. 하드웨어를 늘려서 해결하는 방법도 있지만, 비지니스 시나리오 자체를 바꾸거나 UX 관점에서 해결 하는 방법도 고려할 수 있다. 예를 들어 로그인 화면이 넘어가는데 시간이 많이 걸린다고 했을때, 이 문제가 근본적으로 솔루션의 특성이라면 애플리케이션이나 솔루션 수정으로는 해결이 불가능하다. 이런 경우에는 모래 시계 아이콘이나 progress bar등을 넣어서 UX 관점에서 사용자로 하여금 체감되는 응답 시간에 대해서 느리지 않고 몬가 진행이 되고 있다고 보여주는 형태로 접근을 해서 문제를 해결할 수 도 있다.

간단한 예를 하나 들어보자. Drupal 이라는 웹 CMS 기반의 웹사이트가 있다고 하자. 성능 테스트를 수행하였는데, CPU 점유율이 지나치게 높게 나오고 응답 시간이 느리게 나왔다. 이것이 문제의 정의이다.

성능의 문제점을 찾아내기 위해서, 성능 테스트 시나리오를 검토하였다 성능 테스트 시나리오는 1) 로그인 페이지 로딩, 2) id,password post로 전송 3) 초기 화면으로 redirect 4) 로그 아웃 4가지 과정을 거치고 있었다. 1,2,3,4 과정의 응답시간을 각각 체크해서 보니, 2) 과정에서 성능의 대부분을 차지 하고 있음을 찾아 내었다. 전체적으로 성능이 안나오는 것을 인지한 후, 문제를 여러 구간으로 나누어서 접근 하는 것이 Break down이다.

2) 과정을 분석하기 위해서 성능 테스트를 다시 진행한다. 다른 시나리오가 영향을 주는 것을 방지하기 위해서, 1,3,4 시나리오를 제외 하고, 2 시나리오만 가지고 성능 테스트를 진행한다. 이렇게 문제점을 다른 변수로 부터 분리하여 고립 시키는 것을 isolation이라고 한다.

다음으로 Xhprof 라는 프로파일링 툴을 사용하여 로직중 어느 부분이 가장 성능 문제가 발생하는 지를 profiling 하였다. 대 부분의 성능 저하가 SQL 문장 수행에서 발생함을 찾아내었다. 이렇게 하나의 포인트를 깊게 들어 가면서 범위를 좁혀가는 것을 narrow down이라고 한다.

SQL 수행이 문제가 있음을 정의하고(문제의 정의), 어떤 SQL 문장이 수행되는지(Break down) 각각을 정의한후, 가장 수행 시간이 긴 SQL 문장을 찾아서 원인을 분석하였더니(narrow down) index 가 걸려 있지 않음을 찾아내었다.

해당 테이블에 index를 적용하고, 성능 테스트를 다시 수행하여 성능 목표치를 달성하였음을 해결하였다.

가상의 시나리오지만 성능 튜닝의 접근 방법은 대부분 유사 하다. 관건은 문제를 어떻게 잘 정의하고, 문제가 어떤 요소로 구성이 되어 있으며 각각이 어떤 구조로 동작을 하고 있는지 잘 파고 들어갈 수 있는 문제에 대한 접근 능력과, 점점 솔루션의 아랫부분(low level)로 들어갈 수 있는 전문성이 필요하다.

반복

튜닝이 끝났으면 다시 테스트 및 모니터링 항목으로 돌아가서 성능 목표에 도달할때까지 위의 작업을 계속해서 반복해서 수행한다.

Performance Engineering을 위해 필요한 것들

그러면 성능 엔지니어링을 하기 위해서 필요한 것들은 무엇이 있을까? 먼저 도구 적인 측면부터 살펴보자. 

   부하 테스트기 : 가장 기초적으로 필요한 것은 부하 발생 도구 이다. HP Load Runner와 같은 상용 도구에서 부터, nGrinder와 같은 오픈 소스 기반의 대규모 부하 발생 도구를 사용할 수 도 있고, SOAP UI같은 micro benchmark 테스트 툴을 이용해서 소규모 (50 사용자 정도)를 발생 시키거나 필요에 따라서는 간단하게 Python등의 스크립트 언어로 부하를 발생시킬 수 도 있다.

   모니터링 도구 : 다음으로는 모니터링 도구이다. 어느 구간이 문제가 있는지 현상이 어떤지를 파악하려면 여러 형태의 모니터링 도구들이 필요하다. 

   프로파일링 도구 : 그리고, 문제되는 부분을 발견했을때, 그 문제에 대한 근본적인 원인을 찾기 위해서 프로파일링을 할 수 있는 도구들이 필요하다.

우리가 일반적으로 이야기 하는 프로파일링 도구들은 IDE와 같은 개발툴에서 debug 용도로 사용은 가능하지만, 대부분 대규모 부하 환경에서는 사용이 불가능한 경우가 많다.그래서 그런 경우에는 해당 시스템의 상태에 대한 스냅샷을 추출 할 수 있는 dump 도구들을 많이 사용하는데, unix process의 경우에는 ptrace를 통해서 system call을 모니터링 하거나, pmap을 이용하여 메모리 snapshot등을 추출할 수 도 있고, 자바의 경우에는 thread dump를 추출해서 병목 당시 애플리케이션이 무슨 동작을 하고 있었는지를 찾아낼 수 있다.

다음이 이 글에서 정말 언급하고 싶은 내용인데, 앞에서 도구를 언급했다면 다음은 엔지니어로써의 역량이나 지식적인 부분이다. 

   역량 : 당연한 것이겠지만, 기술적인 역량은 필수적이다. netstat를 통해서 TCP 소켓이 FIN_WAIT가 발생하였는데,  FIN_WAIT가 의미하는 것이 무엇인지 모르면 아무리 모니터링을 잘해도 소용이 없다. 기본적인 엔지니어로써의 컴퓨터와 프로그래밍, OS등에 대한 넓은 이해는 필수적이다.

   하드웨어 인프라, 미들웨어 , 애플리케이션에 대한 지식 : 다음은 사용하는 특정 솔루션에 대한 전문적인 지식이다. 톰캣의 내부 구조가 어떻게 되어 있으며, JVM의 동작원리가 어떻게 되는지와 같은 특정 지식인데, 사실 이러한 지식은 오랜 경험이나 습득할 시간이 없으면 가지기가 어렵다. 이런 경우는 해당 솔루션 제품 엔지니어를 통해서 지원을 받는 방법도 고려해볼만 하다.

   그리고 경험 : 성능 엔지니어링에 대한 경험인데, 대략 시스템의 상태마 봐도 어느 부분이 의심이 되는지 경험이 많은 엔지니어는 쉽게 접근을 한다. 성능 문제는 넓어보이기는 하지만, 결국 발생되는 패턴이 거의 일정하다. 그리고 특정 솔루션에 대한 지식이 없다하더라도, 문제에 대한 접근 하는 방법이나 모니터링 방법, 툴등은 사용법이 다르다 하더라도 그 의미하는 방법은 거의 비슷하기 때문에, 다른 기술로 구현되어 있는 시스템이라고 하더라도, 경험이 있는 엔지니어는 문제를 접근해서 풀어나가는 방식이 매우 익숙하다. 

   마지막으로 인내심 : 그리고 마지막으로 강조하고 싶은 점이 인내심인데, 사실 성능 엔지니어링은 상당히 지루한 작업이다. 반복적인 테스트와 모니터링 및 분석을 거쳐야 하고, 해당 솔루션에 대한 전문적인 지식이 없을 경우에는 보통 제품 문제라고 치부하고 하드웨어 업그레이드로 가는 경우가 많은데, 어짜피 솔루션이라고 해도 소스코드로 만들어진 프로그램이다. 디컴파일을 하건, 덤프를 추출하건, 꾸준히 보고, 오픈 소스의 경우 소스코드를 참고해서 로직을 따라가다 보변, 풀어낼 수 있는 문제가 대부분이다. 결국은 시간과 인내심의 싸움인데, 꾸준하게 인내심을 가지고 문제를 접근하고 풀어나가는 것을 반복하면 문제는 풀린다.



posted by 여성게
:
ChatBot 2019. 7. 6. 16:35

 

<챗봇 시스템 구성요소>

 

1) 대화 엔진(시나리오 엔진) : 흐름이 있는 대화는 하나의 질/답으로 끝나지 않는다. 이 말은 시나리오가 있는 대화흐름으로 이루어진다는 것이다. 이러한 시나리오 대화를 위해서는 대화엔진, 즉 시나리오 엔진이 필요하다. 단순 Q&A성 질/답은 해당 엔진이 필요 없을 수 있지만 말이다. 그리고 이 대화엔진 안에도 여러 구성요소가 존재한다.

 

  • 대화 세션 매니저 : 시나리오는 분명 흐름이 있는 대화라고 했다. 이를 위해서는 직전 질문/답변이 무엇인지등이 대화흐름에 사용되기 때문에 이를 저장하고 있는 대화 세션매니저가 필요하다. 그리고 기타 대화에 필요한 정보등을 담을 수도 있을 것이다.
  • 지식 Parser : 모든 챗봇에는 지식학습을 위한 데이터가 존재한다. 그리고 그 중 우리가 사용할 챗봇 엔진은 2가지 종류의 지식학습 데이터가 있다. 하나는 시나리오 엔진이 사용하기 위한 지식데이터, 그리고 나머지 하나는 의도파악,Q&A를 위한 지식데이터이다. 그 중 시나리오 엔진이 사용하는 지식데이터는 파일 형태로 특정 문법이 존재하는 데이터이다. 이러한 파일을 파싱해 메모리에 지식트리를 구성해주는 지식Parser 모듈이 필요하다.
  • Operation Module : 대화 세션 매니저에서 데이터를 읽고 쓰고 수정하며 지식 Parser에 의해 구성된 지식트리에서 질문을 전달하여 답변을 얻어오는 등의 역할을 하는 Operation Module이다.

사실 더욱 복잡한 대화엔진은 위의 요소보다 많은 요소로 구성되어 있을 것이다. 

 

2) 자연어 처리 엔진(의도파악, Q&A...) : 우리말 한글을 이용하지 않더라도 모든 챗봇 시스템은 자연어 처리라는 과정이 필요하다. 단순히 패턴기반의 챗봇이라면 얘기가 다를 수 있지만 해당 엔진을 통해 사용자의 질문에서 의도를 파악하고, 또는 Q&A성 답변을 제공하는 등의기능을 제공한다.

 

3) 관리자 페이지 : 챗봇을 사용하기 위해서는 지식을 학습시켜야 하고, 자연어 처리를 위한 사전 데이터 등록 그리고 채팅 로그등을 살펴볼 수 있는 챗봇 관리자 페이지가 필요할 것이다. 

 

  • 챗봇을 관리할 수 있는 메뉴
  • 지식데이터를 관리할 수 있는 메뉴
  • 사전 데이터를 관리할 수 있는 메뉴
  • 사용자들의 채팅 로그를 확인할 수 있는 메뉴
  • 지식을 학습하고 지식을 테스트 해볼 수 있는 테스트 툴

 

4) 웹 채팅 페이지 : 사용자들에게 채팅을 할 수 있는 인터페이스를 구현해야 서비스를 제공할 것이다. 

 

5) 사용자 채팅 로그 수집&분석 : 사실 챗봇시스템에서 가장 중요하다고 할 수 있는 기능 중 하나일 것이다. 사실 관리자가 직접 학습시키는 데이터는 주관적인 성격의 데이터이다. 실제 사용자들이 채팅을 사용하면 관리자의 지식으로만으로 커버리지 안에 모두 담기지는 않을 것이다. 그렇기에 사용자들의 채팅 내용(로그)을 수집하고 분석하여 통계를 내주어 지속적인 재학습 루틴을 만들어 주는 것이 중요하다. 여기서 중요한 것은 사실 아직까지 완벽한 자동 재학습이란 것은 없다.(최소한 필자가 아는 한은..)

 

여기까지 대략적인 챗봇 시스템의 구성요소이다. 사실 위의 구성요소 이외에도 많은 요소들이 있다.

'ChatBot' 카테고리의 다른 글

Spring으로 카카오톡 챗봇만들기  (27) 2018.02.05
posted by 여성게
:
Middleware/Kafka&RabbitMQ 2019. 7. 6. 15:20

 

2019/07/06 - [Kafka&RabbitMQ] - Apache Kafka - Kafka(카프카)란 ? 분산 메시징 플랫폼 - 1

 

Apache Kafka - Kafka(카프카)란 ? 분산 메시징 플랫폼 - 1

이전 포스팅들에서 이미 카프카란 무엇이고, 카프카 프로듀서부터 컨슈머, 스트리밍까지 다루어보았다. 하지만 이번 포스팅을 시작으로 조금더 내용을 다듬고 정리된 상태의 풀세트의 카프카 포스팅을 시작할 것..

coding-start.tistory.com

이전 포스팅에서 간단히 카프카란 무엇이며 카프카의 요소들에 대해 다루어보았다. 이번 포스팅에는 이전에 소개했던 요소중 카프카 프로듀서에 대해 다루어볼 것이다.

 

카프카 프로듀서란 카프카 클러스터에 대해 데이터를 pub, 기록하는 애플리케이션이다.

 

카프카 프로듀서의 내부 구조

우리는 카프카 프로듀서를 사용하기 위해 추상화된 API를 사용한다. 하지만 우리가 사용하는 API는 내부적으로 많은 절차에 따라 행동을 한다. 이러한 프로듀서가 책임지는 역할들은 아래와 같다.

 

  • 카프카 브로커 URL 부트스트랩 : 카프카 프로듀서는 카프카 클러스터에 대한 메타데이터를 가져오기 위해 최소 하나 이상의 브로커에 연결한다. 프로듀서가 연결하길 원하는 첫 번째 브로커가 다운될 경우를 대비하여 보통 한개 이상의 브로커 URL 리스트를 설정한다.
  • 데이터 직렬화 : 카프카는 TCP 기반의 데이터 송수신을 위해 이진 프로토콜을 사용한다. 이는 카프카에 데이터를 기록할 때, 프로듀서는 미리 설정한 직렬화 클래스를 이용해 직렬화 한 이후에 전송한다. 즉, 카프카 프로듀서는 네트워크 상의 브로커들에게 데이터를 보낵기 전에 모든 메시지 데이터 객체를 바이트 배열로 직렬화한다.
  • 토픽 파티션의 결정 :  어떤 파티션으로 데이터가 전송돼야 하는지 결정하는 일은 카프카 프로듀서의 책임이다. 만약 프로듀서가 데이터를 보내기 위해 API에 파티션을 명시하지 않은 경우 파티셔너에 의해 키의 해시값을 이용해 파티션을 결정하고 데이터를 보낸다. 하지만 키값이 존재하지 않으면 라운드 로빈 방식으로 데이터를 전송한다.
  • 처리 실패/재시도 : 처리 실패 응답이나 재시도 횟수는 프로듀서 애플리케이션에 의해 제어된다.
  • 배치처리 : 호율적인 메시지 전송을 위해서 배치는 매우 유용하다. 미리 설정한 바이트 임계치를 넘지 않으면 버퍼에 데이터를 유지하고 임계치를 넘으면 데이터를 토픽으로 전송한다.

 

카프카 프로듀서의 내부 흐름을 설명하면, 프로듀서는 키/값 쌍으로 이루어진 데이터 객체를 전달 받는다. 그리고 해당 객체를 미리 정의된 시리얼라이저 혹은 사용자 정의 시리얼라이저를 이용해 직렬화 한다. 데이터 직렬화 후 데이터가 전송될 파티션을 결정한다. 파티션 정보가 API 호출에 포함되어 있다면 해당 파티션에 직접 데이터를 보내지만 파티션정보가 포함되지 않았다면 데이터 객체의 키값의 해시를 이용해 파티셔너가 데이터를 보낼 파티션을 결정해준다. 만약 키값이 null이라면 라운드 로빈 방식으로 파티션을 결정한다. 파티션 결정이 끝나면 리더 브로커를 이용하여 데이터 전송 요청을 보낸다.

 

카프카 프로듀서 API

카프카 프로듀서 API를 사용하기 위한 필수파라미터들이다.

 

  • bootstrap.servers : 카프카 브로커 주소의 목록을 설정한다. 주소는 hostname:port 형식으로 지정되며 하나 이상의 브로커 리스트를 지정한다.
  • key.serializer : 메시지는 키 값의 쌍으로 이뤄진 형태로 카프카 브로커에게 전송된다. 브로커는 이 키값이 바이트 배열로 돼있다고 가정한다. 그래서 프로듀서에게 어떠한 직렬화 클래스가 키값을 바이트 배열로 변환할때 사용됬는 지 알려주어야한다. 카프카는 내장된 직렬화 클래스를 제공한다.(ByteArraySerializer, StringSerializer, IntegerSerializer / org.apache.kafka.common.serialization)
  • value.serializer : key.serializer 속성과 유사하지만 이 속성은 프로듀서가 값을 직렬화 하기 위해 사용하는 클래스를 알려준다.

 

1
2
3
4
5
6
7
8
9
10
11
public KafkaProducer<StringString> getProducer(){
        
        Properties producerProps = new Properties();
        
        producerProps.put("bootstrap.servers""localhost:9092,localhost:9093");
        producerProps.put("key.serializer""org.apache.kafka.common.serialization.StringSerializer");
        producerProps.put("value.serializer""org.apache.kafka.common.serialization.StringSerializer");
                
        return new KafkaProducer<>(producerProps);
        
}
cs

 

자바 클래스로 위의 필수 설정들을 사용하여 카프카 프로듀서를 생성하는 코드이다. Properties 객체를 이용해 모든 설정 값을 설정해주고 카프카 프로듀서 객체 생성자의 매개변수로 해당 설정을 넣어준다.

 

 

카프카 프로듀서 객체와 ProducerRecord 객체

프로듀서는 메시지 전송을 위해 ProducerRecord 객체를 이용한다. 해당 ProducerRecord는 토픽이름, 파티션 번호, 타임스탬프, 키, 값 등을 포함하는 객체이다. 파티션 번호, 타임스탬프, 키 등은 선택 파라미터 이지만, 데이터를 보낼 토픽과 데이터 값을 반드시 포함해야한다. 그리고 보낼 파티션 선정에는 3가지 방법이 존재한다.

 

  1. 파티션 번호가 지정되면, 지정된 파티션에 데이터를 전송한다.
  2. 파티션이 지정되지 않고 키값이 존재하는 경우 키의 해시 값을 이용하여 파티션을 정한다.
  3. 키와 파티션 모두 지정되지 않은 경우 파티션은 라운드 로빈 방식으로 할당된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public void sendMessageSync(String topicName, String data) throws InterruptedException, ExecutionException {
        
        try(KafkaProducer<StringString> producer = getProducer();){
        
            ProducerRecord<StringString> producerRecord = new ProducerRecord<StringString>(topicName, data);
            
            Future<RecordMetadata> recordMetadata = producer.send(producerRecord);
            
            RecordMetadata result = recordMetadata.get();
            
        }
 
}
cs

 

자바 코드를 이용해 데이터 객체를 생성한 후에 동기식으로 토픽에 데이터를 보내는 코드이다.

 

1
2
3
4
ProducerRecord(String topicName, Integer partition, K key, V value)
ProducerRecord(String topicName, Integer partition, Long timestamp, K key, V value)
ProducerRecord(String topicName, K key, V value)
ProducerRecord(String topicName, V value)
cs

 

ProducerRecord의 생성자 리스트의 일부를 보여준다.

 

일단 send()를 사용해 데이터를 전송하면 브로커는 파티션 로그에 메시지를 기록하고, 메시지에 대한 서버 응답의 메타데이터를 포함한 RecordMetadata를 반환하는데, 이 객체는 오프셋,체크섬,타임스탬프,토픽,serializedKeySize 등을 포함한다.

 

앞에서 일반적인 메시지 게시 유형에 대해 설명했지만, 메시지 전송은 동기 또는 비동기로 수행될 수 있다.

 

  • 동기 메시징 : 프로듀서는 메시지를 보내고 브로커의 회신을 기다린다. 카프카 브로커는 오류 또는 RecordMetadata를 보낸다. 일반적으로 카프카는 어떤 연결 오류가 발생하면 재전송을 시도한다. 그러나 직렬화, 메시지 등에 관련된 오류는 애플리케이션 단에서 처리돼야 하며, 이경우 카프카는 재전송을 시도하지 않고 예외를 즉시 발생시킨다.
  • 비동기 메시징 : 일부 즉각적으로 응답 처리를 하지 않아도 되는 혹은 원하지 않는 경우 또는 한두 개의 메시지를 잃어버려도 문제되지 않거나 나중에 처리하기를 원할 수 있다. 카프카는 send() 성공 여부와 관계없이 메시지 응답 처리를 콜백 인터페이스 제공한다.

 

1
send(ProducerRecord<K,V> record, Callback callback)
cs

 

위의 콜백 인터페이스는 onCompletion 메서드를 포함하는데, 오버라이드하여 구현하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
producer.send(producerRecord, new Callback() {
                
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    
                    if(exception != null) {
                        log.info("예외 발생");
                        /*
                         * 예외처리
                         */
                    }else {
                        log.info("정상처리");
                        /*
                         * 정상처리 로직
                         */
                    }
                    
                }
});
cs

 

onCompletion 메소드 내부에 성공했거나 오류가 발생한 메시지를 처리할 수 있다.

 

 

사용자 정의 파티셔닝

카프카는 내부적으로 내장된 기본 파티셔너와 시리얼라이저를 사용한다. 하지만 가끔은 메시지가 해당 브로커에서 동일한 키는 동일한 파티션으로 가도록 사용자 정의 파티션 로직을 원할 수 있는데, 카프카는 이렇게 사용자 정의 파티셔너 구현을 할 수 있도록 인터페이스를 제공한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CustomPartition implements Partitioner{
 
        @Override
        public void configure(Map<String, ?> configs) {
            // TODO Auto-generated method stub
            
        }
 
        @Override
        public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes,
                Cluster cluster) {
            List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
            
            int numPartitions = partitions.size();
            //TODO : 파티션 선택 로직
            return 0;
        }
 
        @Override
        public void close() {
            // TODO Auto-generated method stub
            
        }
        
}
cs

 

해당 커스텀 파티셔너를 사용하려면 Properties에 "partitioner.class"/"package.className" 키/값 형태소 설정해주면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public KafkaProducer<StringString> getProducer(){
        
        Properties producerProps = new Properties();
        
        producerProps.put("bootstrap.servers""localhost:9092,localhost:9093");
        producerProps.put("key.serializer""org.apache.kafka.common.serialization.StringSerializer");
        producerProps.put("value.serializer""org.apache.kafka.common.serialization.StringSerializer");
        producerProps.put("partitioner.class","com.kafka.exam.CustomPartition");
        
        return new KafkaProducer<>(producerProps);
        
}
cs

 

추가 프로듀서 설정

 

  • buffer.memory : 카프카 서버로 전송을 기다리는 메시지를 위해 프로듀서가 사용할 버퍼 메모리의 크기다. 간단히 전송되지 않은 메시지를 보관하는 자바 프로듀서가 사용할 전체 메모리다. 이 설정의 한계에 도달하면, 프로듀서는 예외를 발생시키기 전에 max.block.ms 시간 동안 메시지를 대기시킨다. 만약 배치 사이즈가 더 크다면 프로듀서 버퍼에 더 많은 메모리를 할당해야한다. 예를 들어 설명하자면 카프카 서버에 메시지를 전송하는 속도보다 프로듀서가 메시지를 전송하는 요청의 수가 더 빠를 경우(1개씩 메시지를 카프카 서버에 전송하는데 하나를 보내면 2개의 요청이 계속 쌓인다면) 아직 보내지 않은 메시지는 해당 버퍼에 쌓이게 된다. 이 내용에 더해서 큐에 있는 레코드가 계속 남아있는 것을 피하기 위해 request.timeout.ms를 사용해 지정된 시간동안 메시지가 보내지지 않고 큐에 쌓여있다면 메시지는 큐에서 제거되고 예외를 발생시킨다.
  • acks : 이 설정은 각 파티션의 리더가 팔로워(복제본)에게 ACK을 받는 전략을 설정한다. ack=0이라면 리더에 데이터가 쓰이자 마자, 팔로워(복제본)가 데이터 로그를 쓰는 것을 신경쓰지 않고 커밋을 완료한다. ack=1 하나의 팔로워에게 ack를 받으면 커밋을 완료한다. ack=all 모든 팔로워에게 복제완료 ack를 받으면 커밋을 완료한다. 각 설정은 장단점이 있으므로 잘 조율해서 사용해야한다. 만약 ack=0이면 빠른 처리 성능을 보장하지만 매우 높은 가능성의 메시지 손실이 있을 수 있고, ack=1도 역시 처리성능은 빠르지만 여전히 메시지 손실가능성이 존재한다. 마지막으로 ack=all은 메시지 손실 가능성이 매우 낮지만 처리 성능이 느려진다.
  • batch.size : 이 설정은 설정된 크기만큼 여러 데이터를 모아 배치로 데이터를 전송한다. 하지만 프로듀서는 단위 배치가 꽉 찰때까지 기다릴 필요는 없다. 배치는 특정 주기로 전성되며 배치에 포함된 메시지 수는 상관하지 않는다.
  • linger.ms : 브로커로 현재의 배치를 전송하기 전에 프로듀서가 추가 메시지를 기다리는 시간을 나타낸다.
  • compression.type : 프로듀서는 브로커에게 압축되지 않은 상태로 메시지를 전송하는 것이 기본이지만, 해당 옵션을 사용해 데이터를 압축해 보내 처리 성능을 높힐 수 있다. 배치처리가 많을 수록 유용한 옵션이다.
  • retrites : 메시지 전송이 실패하면 프로듀서가 예외를 발생시키기 전에 메시지의 전송을 다시 시도하는 수.

기타 많은 설정들이 존재하지만 여기서 모두 다루지는 않는다. 공식 홈페이지를 이용해자.

 

자바 카프카 프로듀서 예제

 

코드 작성 전에 Maven에 카프카 dependency 설정을 해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public KafkaProducer<StringString> getProducer(){
        
        Properties producerProps = new Properties();
        
        producerProps.put("bootstrap.servers""localhost:9092,localhost:9093");
        producerProps.put("acks""all");
        producerProps.put("retries"1);
        producerProps.put("batch.size"20000);
        producerProps.put("linger.ms"1);
        producerProps.put("buffer.memory"24568545);
        
        return new KafkaProducer<>(producerProps);
        
}
/*
     * 동기식 
     * 
     * ProducerRecord 생성자 매개변수(*표시는 필수 매개변수)
     * *1)토픽이름,2)파티션 번호,3)타임스탬프,4)키,*5)값
     * 
     * 파티션선정
     * 1)파티션 번호가 ProducerRecord의 매개변수로 명시되어 있다면 그 파티션으로 보낸다.
     * 2)파티션이 지정되지 않고 ProducerRecord 매개변수에 키가 지정된 경우 파티션은 키의 해시값을 사용해 정한다.
     * 3)ProducerRecord 매개변수에 키와 파티션 번호 모두 지정되지 않은 경우에는 라운드 로빈 방식으로 정한다.
     */
    public void sendMessageSync(String topicName, String data) throws InterruptedException, ExecutionException {
        
        try(KafkaProducer<StringString> producer = getProducer();){
        
            ProducerRecord<StringString> producerRecord = new ProducerRecord<StringString>(topicName, data);
            
            Future<RecordMetadata> recordMetadata = producer.send(producerRecord);
            
            RecordMetadata result = recordMetadata.get();
            
        }
    }
    
    /*
     * 비동기식
     */
    public void sendMessageAsync(String topicName, String data) {
        
        try(KafkaProducer<StringString> producer = getProducer();){
        
            ProducerRecord<StringString> producerRecord = new ProducerRecord<StringString>(topicName, data);
            
            producer.send(producerRecord, new Callback() {
                
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    
                    if(exception != null) {
                        log.info("예외 발생");
                        /*
                         * 예외처리
                         */
                    }else {
                        log.info("정상처리");
                        /*
                         * 정상처리 로직
                         */
                    }
                    
                }
            });
            
        }
    }
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none;color:white">cs

 

 

카프카 프로듀서를 사용할 때 유의해야 하는 사항

  • 데이터 유효성 검증 : 보낼 데이터의 유효성 검증은 간과하기 쉽다. 꼭 유효성 검증을 포함하는 로직을 작성하자.
  • 예외처리 : 연결 실패,네트워크 시간 초과 등의 예외로는 프로듀서가 내부적으로 재전송을 한다. 하지만 모든 예외에 대해서 재전송을 하는 것은 아니다. 즉, 예외 발생에 대해 처리의 책임은 온전히 프로듀서 애플리케이션에 있는 것이다.
  • 부트스트랩 URL 수 : 클러스터 노드수가 적다면 모든 URL를 목록으로 작성하자. 하지만 많약 노드가 아주 많다면 대표하는 브로커의 수만 목록으로 설정해주자.
  • 잘못된 파티셔닝 방식의 사용방지 : 파티션은 카프카에서 병렬 처리의 단위다. 메시지가 모든 토픽 파티션으로 균일하게 분배되도록 올바른 파티셔닝 전략을 선택하자. 만약 잘못된 파티셔닝 방식을 도입하게 되면 특정 파티션에만 요청이 집중되는 최악의 상황이 발생할 수 있으니 메시지가 모든 가용한 파티션에 분배 되도록 올바른 파티셔닝 방식을 정의하자.
  • 기존 토픽에 새로운 파티션 추가 방지 : 메시지를 분산시키기 위해서 키 값을 사용하여 파티션을 선택하는 토픽에는 파티션을 추가하는 것을 피해야한다. 새로운 파티션 추가는 각 키에 대해 산출된 해시 코드를 변경시키며, 이는 파티션의 수도 산출에 필요한 입력 중 하나로 사용하기 때문이다.

 

posted by 여성게
:
Middleware/Kafka&RabbitMQ 2019. 7. 6. 13:28

 

 

이전 포스팅들에서 이미 카프카란 무엇이고, 카프카 프로듀서부터 컨슈머, 스트리밍까지 다루어보았다. 하지만 이번 포스팅을 시작으로 조금더 내용을 다듬고 정리된 상태의 풀세트의 카프카 포스팅을 시작할 것이다.

 

카프카 시스템의 목표

  • 메시지 프로듀서와 컨슈머 사이의 느슨한 연결
  • 다양한 형태의 데이터 사용 시나리오와 장애 처리 지원을 위한 메시지 데이터 유지
  • 빠른 처리 시간을 지원하는 구성 요소로 시스템의 전반적인 처리량을 최대화
  • 이진 데이터 형식을 사용해서 다양한 데이터 형식과 유형을 관리
  • 기존의 클러스터 구성에 영향을 주지 않고 일정한 서버의 확장성을 지원

 

카프카의 구조

카프카 토픽에서 모든 메시지는 바이트의 배열로 표현되며, 카프카 프로듀서는 카프카 토픽에 메시지를 저장하는 애플리케이션이다. 이렇게 프로듀서가 보낸 데이터를 저장하고 있는 모든 토픽은 하나 이상의 파티션으로 나뉘어져있다. 각 토픽에 대해 여러개로 나뉘어져 있는 파티션은 메시지를 도착한 순서에 맞게 저장한다.(내부적으로는 timestamp를 가지고 있다.) 카프카에서는 프로듀서와 컨슈머가 수행하는 두 가지 중요한 동작이 있다. 프로듀서는 로그 선행 기입 파일 마지막에 메시지를 추가한다. 컨슈머는 주어진 토픽 파티션에 속한 로그 파일에서 메시지를 가져온다. 물리적으로 각 토픽은 자신에게 할당된 하나 이상의 파티션을 다른 브로커(카프카 데몬 서버)들에게 균등하게 분배된다.

 

이상적으로 카프카 파이프라인은 브로커별로 파티션과 각 시스템의 모든 토픽에 대해 균등하게 분배되어야 한다. 컨슈머는 토픽에 대한 구독 또는 이런 토픽에서 메시지를 수신하는 애플리케이션이다.

 

이러한 카프카 시스템은 고가용성을 위한 클러스터를 지원한다. 전형적인 카프카 클러스터는 다중 브로커로 구성된다. 클러스터에 대한 메시지 읽기와 쓰기 작업의 부하 분산을 돕는다. 각 브로커는 자신의 상태를 저장하지 않지만 Zookeeper를 사용해 상태 정보를 유지한다. 각각의 토픽 파티션에는 리더로 활동하는 브로커가 하나씩 있고, 0개 이상의 팔로워를 갖는다. 여느 리더/팔로워 관계와 비슷하게 리더는 해당하는 파티션의 읽기나 쓰기 요청을 관리한다. 여기서 Zookeeper는 카프카 클러스터에서 중요한 요소로, 카프카 브로커와 컨슈머를 관리하고 조정한다. 그리고 카프카 클러스터 안에서 새로운 브로커의 추가나 기존 브로커의 장애를 감시한다. 예전 카프카 버전에서는 파티션 오프셋 관리도 Zookeeper에서 관리하였지만 최신 버전의 카프카는 오프셋 관리를 위한 토픽이 생성되어 오프셋관련된 데이터를 하나의 토픽으로 관리하게 된다.

 

 

메시지 토픽

메시징 시스템에서 메시지는 어디간에 저장이 되어 있어야한다. 카프카는 토픽이라는 곳에 메시지를 바이트 배열 형태로 저장한다. 각 토픽은 비즈니스 관점에서 하나의 카테고리가 될 수 있다. 다음은 메시지 토픽에 대한 용어 설명이다.

 

  • 보관 기간 : 토픽 안의 메시지는 처리 시간과 상관없이 정해진 기간 동안에만 메시지를 저장하고 있는다. 기본 값은 7일이며 사용자가 값 변경이 가능하다.
  • 공간 유지 정책 : 메시지의 크기가 설정된 임계값에 도달하면 메시지를 지우도록 설정가능하다. 카프카 시스템 구축시 충분한 용량 계획을 수립해야 원치않은 삭제가 발생하지 않는다.
  • 오프셋 : 카프카에 할당된 각 메시지는 오프셋이라는 값이 사용된다. 토픽은 많은 파티션으로 구성돼 있으며, 각 파티션은 도착한 순서에 따라 메시지를 저장하고, 컨슈머는 이러한 오프셋으로 메시지를 인식하고 특정 오프셋 이전의 메시지는 컨슈머가 수신했던 메시지로 인식한다.
  • 파티션 : 카프카 메시지 토픽은 1개 이상의 파티션으로 구성되어 분산 처리된다. 해당 파티션의 숫자는 토픽 생성시 설정가능하다. 만약 순서가 아주 중요한 데이터의 경우에는 파티션 수를 1개로 지정하는 것도 고려할 수 있다.(아무리 파티션이 시계열로 데이터가 저장되지만 여러 파티션에 대한 메시지 수신은 어떠한 순서로 구독될지 모르기 때문이다.)
  • 리더 : 파티션은 지정된 복제 팩터에 따라 카프카 클러스터 전역에 걸쳐 복제된다. 각 파티션은 리더 브로커와 팔로워 브로커를 가지며 파티션에 대한 모든 읽기와 쓰기 요청은 리더를 통해서만 진행된다.

 

메시지 파티션

각 메시지는 파티션에 추가되며 각 단위 메시지는 오프셋으로 불리는 숫자에 맞게 할당된다. 카프카는 유사한 키를 갖고 있는 메시지가 동일한 파티션으로 전송되도록 하며, 메시지 키의 해시 값을 산출하고 해당 파티션에 메시지를 추가한다. 여기서 중요하게 짚고 넘어가야 할 것이 있다. 각 단위 메시지에 대한 시간적인 순서는 토픽에 대해서는 보장되지 않지만, 파티션 안에서는 항상 보장된다. 즉, 나중에 도착한 메시지가 항상 파티션의 끝 부분에 추가됨을 의미한다. 예를 들어 설명하자면,

 

A 토픽은 a,b,c라는 파티션으로 구성된다는 가정이다. 만약 라운드 로빈 방식으로 메시지가 각 파티션에 분배가 되는 옵션을 적용했다고 생각해보자. 1,2,3,4,5,6이라는 메시지가 pub되었고 각 메시지는 a-1,4 / b-2,5 / c-3,6 와 같이 파티션에 분배된다. 1,2,3,4,5,6이라는 순서로 메시지가 들어왔고 해당 메시지는 시간적인 순서와 라운드로빈 방식으로 a,b,c라는 파티션에 배분되었다. 또한 각 파티션 안을 살펴보면 확실히 시간 순서로 배분되었다. 하지만 컨슈머 입장에서는 a,b,c파티션에서 데이터를 수신했을 경우 1,4,2,5,3,6이라는 메시지 순서로 데이터를 수신하게 될것이다.(a,b,c순서로 수신, 하지만 파티션 수신순서는 바뀔수 있다.) 무엇을 의미할까? 위에서 말한것과 같이 파티션 내에서는 시간적인 순서를 보장하지만 전체적인 토픽관점에서는 순서가 고려되지 않는 것이다. 즉, 순서가 중요한 데이터는 동일한 키를 사용해 동일 파티션에만 데이터를 pub하거나 혹은 토픽에 파티션을 하나만 생성해 하나의 파티션에 시간적인 순서로 데이터를 저장되게 해야한다.

 

많은 수의 파티션을 구성하는 경우의 장단점

  • 높은 처리량 보장 : 파티션을 여러개 구성한다면 병렬 처리되어 높은 처리량을 보장할 것이다. 왜냐하면 여러 파티션에 대해 쓰기 동작이 여러 스레드를 이용해 동시에 수행되기 때문이다. 또한 하나의 컨슈머 그룹 내에서 하나의 파티션에 대해 하나의 컨슈머가 할당되기 때문에 여러 파티션의 메시지를 여러 컨슈머가 동시에 수신하여 병렬처리가 가능하다. 여기서 중요한 것은 동일한 컨슈머 그룹 안의 하나의 파티션에 대해 여러 컨슈머가 읽을 수 없다는 것이다.
  • 프로듀서 메모리 증가 : 만약 파티션 수가 많아 진다면 일시적으로 프로듀서의 버퍼의 메모리가 과도해질 수 있어, 토픽에 메시지를 보내는데 문제가 생길 수 있으므로 파티션 수는 신중히 고려해야한다.
  • 고가용성 문제 : 여러 파티션을 생성하므로서 카프카 클러스터의 고가용성을 지원한다. 즉, 리더인 파티션이 중지되면 팔로워중에 하나가 리더로 선출될 것이다. 하지만 너무 과중한 파티션 수는 리더 선출에 지연이 생길 가능성이 있기 때문에 신중히 고려해야한다.

복제와 복제로그

복제는 카프카 시스템에서 신뢰성있는 시스템을 구현하기 위해 가장 중요한 부분이다. 각 토픽 파티션에 대한 메시지 로그의 복제본은 카프카 클러스터 내의 여러 서버에 걸쳐서 관리되고, 각 토픽마다 복제 팩터를 다르게 지정가능하다.

 

일반적으로 팔로워는 리더의 로그 복사본을 보관하는데, 이는 리더가 모든 팔로워로부터 ACK를 받기 전까지는 메시지를 커밋하지 않는다는 것을 의미한다.(ack=all 설정시)

 

메시지 프로듀서 

일반적으로 프로듀서는 파티션으로 데이터를 쓰기 않고, 메시지에 대한 쓰기 요청을 생성해서 리더 브로커에게 전송한다. 그 이후 파티셔너가 메시지의 해시 값을 계산하여 프로듀서로 하여금 어느 파티션에 메시지를 pub할지 알 수 있도록 한다.

 

일반적으로 해시 값은 메시지 키를 갖고 계산하며, 메시지 키는 카프카의 토픽으로 메시지를 기록할 때 제공된다. null 키를 갖는 메시지는 분산 메시징을 지원하는 파티션에 대해 라운드 로빈 방식으로 분배된다. 카프카에서의 각 파티션은 한 개의 리더를 가지며, 각 읽기와 쓰기 요청은 리더를 통해 진행된다.

 

프로듀서는 설정에 따라 메시지의 ACK를 기다리며, 일반적으로 모든 팔로워 파티션에 대해 복제가 완료되면 커밋을 완료한다. 또한 커밋이 완료되지 않으면 읽기 작업은 허용되지 않는다. 이는 메시지의 손실을 방지한다. 그렇지만 ACK 설정을 1로 설정할 수 있는데, 이경우 리더 파티션이 하나의 팔로워에게만 복제 완료 응답을 받으면 커밋을 완료한다. 이 경우 처리 성능은 좋아지지만 메시지의 손실은 어느정도 감수 해야한다. 즉, 메시지의 손실을 허용하고 빠른 처리 시간을 원하는 경우에 사용할 수 있는 설정이다.

 

메시지 컨슈머

카프카 토픽을 구독하는 역할을 하는 애플리케이션이다. 각 컨슈머는 컨슈머 그룹에 속해 있으며, 일부 컨슈머 그룹은 여러개의 컨슈머를 포함한다. 위에서도 간단히 설명하였지만 동일 그룹의 컨슈머들은 동시에 같은 토픽의 서로다른 파티션에서 메시지를 읽어온다. 하지만 서로 다른 그룹의 컨슈머들은 같은 토픽에서 데이터를 읽어와 서로 영향을 미치지 않으면서 메시지 사용이 가능하다.

 

주키퍼의 역할

  • 컨트롤러 선정 : 컨트롤러는 파티션 관리를 책임지는 브로커 중에 하나이며, 파티션 관리는 리더 선정, 토픽 생성, 파티션 생성, 복제본 관리 등을 포함한다. 하나의 노드 또는 서버가 꺼지면 카프카 컨트롤러는 팔로워 중에서 파티션 리더를 선정한다. 카프카는 컨트롤러를 선정하기 위해 주키퍼의 메타데이터를 정보를 이용한다.
  • 브로커 메타데이터 : 주키퍼는 카프카 클러스터의 일부인 각 브로커에 대해 상태 정보를 기록한다.
  • 토픽 메타데이터 : 주키퍼는 또한 파티션 수, 특정한 설정 파라미터 등의 토픽 메타 데이터를 기록한다.

이외에 주키퍼는 더 많은 역할을 담당하고 있다.

 

여기까지 간단히 카프카에 대한 소개였다. 이후 포스팅들에서는 프로듀서,컨슈머,스트리밍,클러스터 구축 등의 내용을 몇 차례에 거쳐 다루어 볼 것이다.

posted by 여성게
: