Skip to content

Commit a47864e

Browse files
docs : ch11.md 이경진 (#6)
1 parent 62ba812 commit a47864e

5 files changed

Lines changed: 312 additions & 0 deletions

File tree

img/amdahls_law.png

22.6 KB
Loading

img/comparing_queue.png

44.3 KB
Loading
59.7 KB
Loading

img/number_of_processors.png

116 KB
Loading

study/ch11.md

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
# 11. 성능, 확장성
2+
> 스레드를 사용하는 가장 큰 목적은?
3+
4+
성능을 높이기 위해.
5+
6+
> 멀티 스레드를 사용하는 큰 이유 중 하나는?
7+
8+
다중 CPU 하드웨어를 충분히 활용하고자.
9+
10+
**이 장의 전체적인 맥락**
11+
- 일반적인 경우에는 성능이 높은 것이 좋지만, 그렇다고 성능 때문에 안정성을 해칠 수는 없음.
12+
- 일단 프로그램이 돌아가게 만든 다음, 그 다음에 성능을 신경쓰는게 낫다.
13+
- 예상한 성능 기준이 있었다면, 그 기준에 미치지 못했을 때만 성능 문제를 살펴보는 것으로 충분 (특히 병렬 애플리케이션에서는)
14+
15+
## 11.1 성능에 대해
16+
**여러 개의 스레드를 사용**하려 한다면, 항상 단일 스레드를 사용할 때보다 **성능상의 비용**을 지불해야 함
17+
- 스레드 간의 작업 내용을 조율하는데 필요한 오버헤드 (e.g. 락 걸기, 신호 보내기, 메모리 동기화하기)
18+
- 컨텍스트 스위칭
19+
- 스레드 생성/제거가 빈번함
20+
- 여러 스레드를 효율적으로 스케쥴링 해야함
21+
22+
<br>
23+
24+
>이러한 비용을 지불하더라도, 스레드를 효율적으로 적용하면
25+
- 성능, 응답성이 높아짐
26+
- 처리 용량도 커지는 등
27+
28+
의 장점을 얻을 수 있음
29+
30+
>반대로 잘못 설계된 병렬 애플리케이션은?
31+
32+
순차적으로 작업을 처리하는 프로그램보다 오히려 느리게 동작할 수도 있음
33+
34+
- 먼저 프로그램이 확보할 수 있는 모든 자원을 최대한 활용
35+
- 남는 자원이 생길 때마다 최대한 활용할 수 있도록 해야함
36+
- (= CPU가 최대한 바쁘게 동작해야 함)
37+
38+
=> 이러한 부분들을 우선적으로 생각해야함
39+
40+
## 11.1.1 성능 vs 확장성
41+
- **성능** : 특정 작업을 처리하는 속도가 '얼마나 빠르냐'
42+
- **확장성** : 동일한 자원을 갖고 있을 때 '얼마나 많은' 양의 일을 할수 있냐
43+
44+
병렬 프로그램 환경에서 확장성을 충분히 가질 수 있도록 애플리케이션을 설계/튜닝하는 방법은 기존의 일반적인 성능 최적화 방법과 다른 경우가 많음
45+
46+
### 성능을 목표로
47+
동일한 일을 더 적은 노력으로 하고자 하는 것
48+
49+
(e.g. 캐싱해서 실행 속도 높이기, 알고리즘 바꿔서 시간복잡도 줄이기)
50+
51+
### 확장성을 목표로
52+
처리해야 할 작업을 병렬화해 시스템의 가용 자원을 더 많이 끌어다 사용하면서 더 많은 일을 처리할 수 있도록 하는 것
53+
54+
> 얼마나 많이 vs 얼마나 빠르게
55+
56+
단일 스레드 애플리케이션에서의 성능 개선 방안은 대부분 확장성의 측면에서 효과적이지 않음 (11.4.4 참고)
57+
58+
---
59+
60+
### 11장에서 성능보단 확장성을 중점적으로 다루는 이유
61+
" 처리 용량을 단시간에 급격하게 증가시키는 게 어렵기 때문"
62+
63+
- **별다른 튜닝을 하지 않은 단일 구조 애플리케이션**이 웬만큼 잘 만들어진 다중 티어 애플리케이션보다 성능이 나을 가능성이 많음
64+
- 단일 구조는 서로 다른 티어 간 작업을 주고받는 도중에 발생하는 **네트웍 시간 지연**도 없고, 연산 작업을 처리할 때의 **부하**도 없기 때문
65+
66+
- 하지만 단일 구조의 애플리케이션이 처리할 수 있는 최대 부하를 넘어서면?
67+
- '처리 용량을 단시간에 급격히 증가시키는 게 어렵기 때문'에 시스템 자원을 계~속 투입하면서도 서비스 시간이 훨씬 길어지는 걸 감수해야 함
68+
69+
> 그래서 서버 애플리케이션을 만들 때 '얼마나 빠르게' 보단 '얼마나 많이'를 더 중요하게 생각하는 경우가 많음
70+
71+
<br>
72+
73+
## 11.2 암달의 법칙
74+
대부분의 병렬 프로그램에는 **병렬화할 수 있는 작업****순차적으로 처리해야 하는 작업**이 뒤섞인 덩어리를 갖고 있음
75+
76+
> "애플리케이션의 확장성은 반드시 순차적으로 실행돼야만 하는 코드가 전체에서 얼마만큼의 비율을 차지하냐에 달렸다."
77+
78+
'암달의 법칙'을 사용하면 병렬 작업과 순차 작업의 비율에 따라 하드웨어 자원을 추가로 투입했을 때 **이론적으로 속도가 얼마나 빨라질지에 대한 예측 값**을 얻을 수 있음
79+
80+
![암달의법칙](../img/amdahls_law.png)
81+
82+
즉, 작업을 순차 처리하는 부분이 많아질 때 느려지는 정도가 얼마만큼인지 수치화 할 수 있음
83+
84+
![암달의법칙](../img/number_of_processors.png)
85+
86+
- 프로세서의 개수가 증가하면 할수록,
87+
- 순차적으로 실행해야 하는 부분이 아주 조금이라도 늘어나면,
88+
- 프로세서 개수에 비해 얻을 수 있는 속도 증가량이 크게 떨어짐
89+
90+
<br>
91+
92+
1. 애플리케이션 내부에서 순차적으로 처리해야 하는 작업이 얼마나 되는지를 먼저 확인
93+
2. 멀티프로세서 시스템에서 속도가 얼마만큼 빨라질지 예측해 볼 수 있다!
94+
(e.g. 작업 큐 사용 부분에서 `LinkedBlockingQueue` 사용 vs 동기화된 `LinkedList` 사용)
95+
96+
<br>
97+
98+
## 11.2.1 예제: 프레임웍 내부에 감춰져 있는 순차적 실행 구조
99+
Q. 내부 구조에 순차적으로 처리해야 하는 구조가 어떻게 숨겨져 있는지 모르겠어요.
100+
101+
A. 1) 스레드 개수를 증가시킬 때마다 성능이 얼마나 빨라지는지 기록해두고, 2) 성능상의 차이점을 기반으로 순차적으로 처리하는 부분이 얼마만큼인지 추측해볼 수 있음
102+
103+
![암달의법칙](../img/comparing_queue.png)
104+
- `ConcurrentLinkedQueue``synchronizedLinkedList` 모두 스레드 안전성은 보장되는 queue
105+
- `ConcurrentLinkedQueue`는 개별 링크 포인터마다 단일 연산으로 업데이트하는 정교한 큐 알고리즘을 사용, 개별 포인터에 대한 업데이트 연산만 순차적으로 처리하면 됨
106+
- 전체 큐의 상태를 하나의 락으로 동기화하는 `synchronizedLinkedList`에 비해 성능이 더 좋음
107+
108+
=> 단순히 적절한 자료구조 사용만으로도 확장성을 크게 늘릴 수 있음
109+
110+
<br>
111+
112+
**자주 발견되는 순차적 구조:**
113+
- `로깅 시스템` : 모든 스레드가 같은 로그 파일에 쓰기
114+
- `캐시 업데이트` : 전역 캐시 무효화로 인한 순차 처리
115+
- `통계 수집` : 공유 카운터 변수 업데이트
116+
- `설정 관리` : 런타임 설정 변경 시 전체 시스템 일시 정지
117+
118+
<br>
119+
120+
## 11.3 스레드와 비용
121+
단일 스레드 프로그램은 스케줄링 문제나 동기화 문제가 있지도 않거니와 그에 따른 부하도 발생하지 않음
122+
123+
스레드 사용 시,
124+
125+
> **병렬로 실행함으로써 얻는 이득 > 병렬로 실행하느라 드는 비용**
126+
127+
이렇게 되야 성능 향상을 시킬 수 있다.
128+
129+
## 11.3.1 컨텍스트 스위칭
130+
하나의 스레드가 실행되다가 다른 스레드가 실행되는 순간 **컨텍스트 스위칭**이 일어남
131+
132+
- 컨텍스트 스위칭 실행 구조
133+
1. 먼저 현재 실행 중인 스레드의 실행 상태를 보관
134+
2. 다음에 실행되기로 스케줄된 스레드의 실행 상태를 읽어들임
135+
136+
- 부하와 비용
137+
- `5,000` ~ `10,000` 클럭 사이클 또는 수 마이크로초 동안의 시간 소모 (플랫폼마다 다름)
138+
- **유닉스 시스템의 `vmstat` 명령** or **윈도우 시스템의 `perfmon` 유틸리티** 사용 시 컨텍스트 스위칭이 일어난 횟수를를 확인할 수 있고, 커널 수준에서 얼마만큼 시간을 소모했는지도 확인 가능
139+
140+
<br>
141+
142+
## 11.4 락 경쟁 줄이기
143+
- 락을 놓고 경쟁하는 상황에 벌어지면?
144+
- 순차적으로 처리함과 동시에 컨텍스트 스위칭도 많이 일어나므로 확장성과 성능을 동시에 떨어뜨리는 원인이 됨.
145+
- **즉, 락을 확보하고자 지속적으로 경쟁하는 상황에서는 확장성에 문제가 생김**
146+
147+
## 11.4.1 락 구역 좁히기
148+
> 락을 확보한 채로 유지되는 시간을 최대한 줄여보자!
149+
150+
1. 락이 꼭 필요하지 않은 코드를 `synchronized` 블록 밖으로 뽑아내서, 락이 영향을 미치는 구역 좁히기
151+
- (e.g. `I/O 작업`과 같이 대기 시간이 발생할 수 있는 코드)
152+
153+
## 11.4.2 락 정밀도 높이기
154+
> 스레드에서 해당 락을 덜 사용하도록 바꿔보자!
155+
1. 락 분할
156+
2. 락 스트라이핑
157+
158+
두 가지 모두 **서로 다른 락을 사용해 여러 개의 독립적인 상태 변수를 각자 묶어두는** 방법.
159+
160+
**락 분할**
161+
- **장점**
162+
- 락으로 묶이는 프로그램의 범위를 조밀하게 나누는 효과 (**락의 정밀도가 높아짐**)
163+
- 대기상태에 들어가는 경우가 크게 줄어듦
164+
- 따라서, 결국 애플리케이션의 확장성 높아짐
165+
- 단점
166+
- 락의 개수가 많아질수록 데드락 발생 위험 높아짐
167+
168+
- 경쟁조건이 아주 심하진 않지만 **그래도 어느 정도 경쟁이 발생하고 있는 경우** 가장 큰 효과
169+
- 대부분의 동기화 블록에서 락 경쟁이 일어나지 않도록 할 수 있음
170+
171+
## 11.4.3 락 스트라이핑
172+
락 분할만 가지고는 더 이상 락을 쪼갤 수 없는 때가 있음
173+
174+
- 락 스트라이핑: 때에 따라 독립적인 객체를 여러 가지 크기의 단위로 묶어내고, 묶인 블록을 단위로 락을 나누는 방법
175+
176+
### ConcurrentHashMap의 락 스트라이핑 구현 예시
177+
178+
```java
179+
public class LockStripingExample {
180+
// 16개의 락을 배열로 관리 (ConcurrentHashMap의 기본 세그먼트 수)
181+
private static final int LOCK_COUNT = 16;
182+
private final Object[] locks = new Object[LOCK_COUNT];
183+
184+
// 실제 데이터를 저장할 해시맵
185+
private final Map<String, String> data = new HashMap<>();
186+
187+
public LockStripingExample() {
188+
// 각 락 객체를 초기화
189+
for (int i = 0; i < LOCK_COUNT; i++) {
190+
locks[i] = new Object();
191+
}
192+
}
193+
194+
/**
195+
* 키의 해시값을 기반으로 해당하는 락을 선택
196+
* ConcurrentHashMap과 동일한 방식으로 해시값을 16으로 나눈 나머지를 사용
197+
*/
198+
private Object getLock(String key) {
199+
int hash = key.hashCode();
200+
// 해시값의 절댓값을 사용하여 음수 인덱스 방지
201+
int lockIndex = Math.abs(hash) % LOCK_COUNT;
202+
return locks[lockIndex];
203+
}
204+
205+
/**
206+
* 키-값 쌍을 저장
207+
* 해당 키의 해시값에 대응하는 락만 사용하여 동기화
208+
*/
209+
public void put(String key, String value) {
210+
Object lock = getLock(key);
211+
synchronized (lock) {
212+
data.put(key, value);
213+
}
214+
}
215+
216+
/**
217+
* 키에 해당하는 값을 조회
218+
* 해당 키의 해시값에 대응하는 락만 사용하여 동기화
219+
*/
220+
public String get(String key) {
221+
Object lock = getLock(key);
222+
synchronized (lock) {
223+
return data.get(key);
224+
}
225+
}
226+
227+
/**
228+
* 키에 해당하는 항목을 삭제
229+
* 해당 키의 해시값에 대응하는 락만 사용하여 동기화
230+
*/
231+
public String remove(String key) {
232+
Object lock = getLock(key);
233+
synchronized (lock) {
234+
return data.remove(key);
235+
}
236+
}
237+
}
238+
```
239+
240+
**락 스트라이핑의 핵심 원리:**
241+
- 전체 해시 범위를 16개의 세그먼트로 나누어 각각 독립적인 락으로 관리
242+
- 키의 해시값을 16으로 나눈 나머지(`hash % 16`)를 사용해 해당하는 락 선택
243+
- 해시 함수가 맵에 있는 항목을 적절히 분산시킨다면, 락 경쟁 확률을 1/16로 줄일 수 있음
244+
- 서로 다른 해시값을 가진 키들은 서로 다른 락을 사용하므로 동시에 접근 가능
245+
246+
## 11.4.4 핫 필드 최소화
247+
- **핫 필드** : 모든 연산을 수행할 때마다 한 번씩 사용해야 하는 카운터 변수와 같은 부분
248+
- 이렇게 모든 변수에 꼭 필요한 변수가 있다면 락의 정밀도를 세밀하게 쪼개는 방법을 적용할 수 없음
249+
- 성능과 확장성이 서로 공존하기 어렵게 만드는 요인
250+
- (e.g. 캐싱)
251+
252+
위에서 살펴본 `ConcurrentHashMap`의 경우 전체 카운트를 하나의 변수에 두지 않고, 락으로 분배된 각 부분마다 카운터 변수를 따로 두고 관리함
253+
254+
## 예제) 11.5 Map 객체의 성능 분석
255+
Q1. 병렬 처리 환경에서 동기화된 HashMap 클래스가 속도가 떨어지는 가장 큰 이유는?
256+
257+
A. **맵 전체가 하나의 락으로 동기화**되어 있기 때문
258+
259+
Q2. 병렬 처리 환경에서 `ConcurentHashMap`의 성능이 압도적으로 좋은 이유는?
260+
261+
A. 읽기 연산에는 락을 걸지 않았고, 쓰기 연산과 일부 읽기 연산에는 락 스트라이핑을 활용함
262+
263+
-> 따라서 대부분의 경우 대기 상태에 들어가지 않고도 다수의 스레드가 동시에 `ConcurentHashMap` 기능 활용 가능
264+
265+
![map성능비교](../img/comparing_scalability_of_map.png)
266+
- `synchronizedMap`으로 동기화된 맵들은 **스레드 개수가 늘어날수록** (= 경쟁이 발생하는 상황으로 넘어갈수록) **성능이 급격하게 저하**
267+
- 한 번 경쟁이 발생하기 시작하면 연산에 필요한 시간의 대부분이 **컨텍스트 스위칭****스케줄링에 필요한 대기 시간**으로 소모됨
268+
- 스레드를 추가한다 해도 성능 향상엔 효과 X
269+
270+
## 11.6 컨텍스트 스위치 부하 줄이기
271+
컨텍스트 스위치 횟수를 줄이면 서버의 처리량에 큰 도움을 줄 수 있음
272+
273+
### 예시: 요청을 처리할 때 로그를 출력하는 경우
274+
1. 필요한 곳 마다 `println` 문장 호출하기
275+
2. 로그 출력만을 전담하는 백그라운드 스레드에서 출력하기
276+
277+
<br>
278+
279+
- 1번의 경우
280+
- `I/O` 연산과 스트림에 대한 락에 직접적으로 연결
281+
- 따라서 컨텍스트 스위치가 빈번하게 발생할 가능성 높음
282+
- 이에따라 서비스 시간이 점점 늘어나면, 서비스의 품질에 직접적으로 타격
283+
- 2번의 경우
284+
- log 메소드를 호출하는 스레드: 출력할 로그 메세지를 큐에 쌓아두는 즉시 리턴
285+
- 로그 출력 스트림을 공유하지 않아도 됨
286+
- 즉, 스레드가 대기 상태에 들어갈 일이 거의 없다.
287+
288+
**다수의 스레드에서 발생할 I/O 연산을 단 하나의 스레드에서 처리하도록 한 군데에 몰아넣어서, 요청을 처리하는 평균 시간을 줄임**
289+
290+
<br>
291+
292+
## 11장 정리
293+
**확장성을 위해서는 락을 최소한만 사용하자!**
294+
295+
- 락으로 동기화하는 범위를 세분화해 정밀도를 높인다.
296+
- 락을 확보하는 시간을 최소한으로 줄인다.
297+
- 독점적인 락 대신 독점적이지 않은 방법을 사용한다.
298+
- 대기상태에 들어가지 않게 한다.
299+
300+
## 논의 해보고 싶은 점
301+
1. 애플리케이션의 내부에 숨겨져있는 '순차적으로 처리해야 하는 구조'를 어떻게 찾을 수 있을까
302+
2. 프로젝트를 하면서 성능 vs 확장성 사이에서 고민해 본 경험
303+
3. ConcurrentHashMap을 다른 구조에 적용해 본 경험
304+
- (기존 동기화 된 코드를 ConcurrentHashMap 등으로 리팩토링 한 경험)
305+
4. 각자 성능 튜닝을 할 때 우선순위가 있다면?
306+
5. 락 경합 때문에 성능이 떨어졌던 경험
307+
308+
309+
<br>
310+
311+
### 참고 자료
312+
- [암달의 법칙 위키백과](https://ko.wikipedia.org/wiki/%EC%95%94%EB%8B%AC%EC%9D%98_%EB%B2%95%EC%B9%99)

0 commit comments

Comments
 (0)