서버 부하 테스트 및 최적화(2)

김태성·2024년 9월 15일

개인 프로젝트-1

목록 보기
41/53
post-thumbnail

TomCat 스래드 조절

뭔가 불만이 또 생겼다.

RPS를 더 올리고 싶다.

뭔가 사람을 더 넣고 돌려도 응답속도는 똑같이 18ms정도인데
RPS가 3000대로 떨어진다.
좀더 효율적이고 성능좋은 서버를 만들고 싶다는 생각을 했다.

그래서 서버의 성능을 어떻게 더 올릴수 있을까 고민을 했다.
그러던 도중, 스레드 풀이라는 개념을 발견했다.

레퍼런스 : https://en.wikipedia.org/wiki/Thread_pool

쉽게 설명하면 스레드를 만드는데 비용이 비싸니까 미리 만들어두고 계속 쓰겠다는거다.
음.. 스레드를 만드는게 비싸다..
이때 갑자기 드는 생각은 스레드 생성이 왜 비쌀까? 이다.

먼저 일반적인 프로그램의 동작 과정을 알아보았다. 그럼 스레드에 대해 좀더 알아보자.
레퍼런스 : https://www.geeksforgeeks.org/lifecycle-and-states-of-a-thread-in-java/

스레드의 라이프사이클이다.

  1. 만들어지고
  2. running하고
    2-1. 상황에 따라 blocked, waiting, time waiting 과정을 통해
  3. task를 완료하고
  4. 제거한다.

즉, 스레드는 하나의 일을 마치고 나면 사라지는 구조이다.
이전에 PintOS에서 사용했던 Thread Init 코드를 살펴보자.

tid_t
thread_create (const char *name, int priority,
		thread_func *function, void *aux) {
	struct thread *t;
	tid_t tid;

	ASSERT (function != NULL);

	/* Allocate thread. */
	t = palloc_get_page (PAL_ZERO);
	if (t == NULL)
		return TID_ERROR;

	/* Initialize thread. */
	init_thread (t, name, priority);
	tid = t->tid = allocate_tid ();

	/* Call the kernel_thread if it scheduled.
	 * Note) rdi is 1st argument, and rsi is 2nd argument. */
	#ifdef KERNEL_CODE_HIDE
	t->tf.rip = (uintptr_t) kernel_thread;
	t->tf.R.rdi = (uint64_t) function;
	t->tf.R.rsi = (uint64_t) aux;
	t->tf.ds = SEL_KDSEG;
	t->tf.es = SEL_KDSEG;
	t->tf.ss = SEL_KDSEG;
	t->tf.cs = SEL_KCSEG;
	t->tf.eflags = FLAG_IF;
	#endif
	#ifdef USERPROG
	list_push_back(&thread_current()->children, &t->child_elem);
	#endif
	/* Add to run queue. */
	thread_unblock (t);
	#ifdef USERPROG
	t->fd_list = palloc_get_multiple(PAL_ZERO, 2);
	if (NULL == t->fd_list)
		return TID_ERROR;
	#endif
	if (thread_get_priority() < priority) {
		thread_yield();
	}
	return tid;
}

PintOS/threads/thread.c 파일이다.
솔직히 이때 뭐하는건지도 모르고 막 덤볐던거라 잘 생각은 나지 않는다.
하지만 여기서 내 기억속에 남는거는 스레드는 함수를 가리킨다 라는 것이다.

위에서 부터 순서대로 보면

  1. palloc을 통해 메모리를 할당한다.
  2. init 스레드를 통해 thread를 만든다.
  3. rip는 현제 명령 실행 주소, rdi는 목적지, rsi는 출발지이다.

따라서, 스레드의 주요 목적은
메모리를 할당받고 실행할 함수를 가르켜 레지스터가 연산을 할 수 있게 도움을 준다

그건 알겠고 왜 비용이 비싼건데요?
라고 말하면 지금부터 중요한 개념이 나온다. 바로 시스템콜이다.

레퍼런스 : https://ko.wikipedia.org/wiki/System_call
시스템 콜은 운영체제의 커널이 제공하는 서비스에 운영체제가 접근하기 위한 인터페이스이다.
즉, 커널에서 제공하는 기능을 쓰고싶을때 사용하는 명령어 라고 보면 되겠다.

커널은 너무나도 소중하기때문에 OS는 커널을 과보호한다.
과보호라고 말하지만 시스템의 심장과 같기에 과보호 이상을 해도 전혀 이상하지 않은 수준이다.
하지만 여기서 문제가 발생하는 것은 유저가 프로그램을 사용할때 오버헤드가 너무 크게 발생할 수 있다는 것이다.

레퍼런스 : https://www.quora.com/Why-it-is-expensive-to-switch-from-user-mode-to-kernel-mode

유저가 프로그램을 사용할때, 시스템 콜을 많이 발동시키면 어떻게 될까?
시스템 콜의 비용이 상당하기 때문에 프로그램이 많이 느려질것이라 판단된다.
위의 레퍼런스에 따르면,

  1. 시스템 호출을 위한 레퍼함수 호출
  2. 인수를 레지스터에 복사
  3. 시스템 호출 번호 식별
  4. 레퍼 함수가 트랩머신 명령어 실행, 트랩 벡터 위치를 가리키는 코드를 실행
  5. 이후systemcall 루틴 호출
    5-1. 커널 스택에 레지스터값 저장한 후 유효성 확인
    5-2. 시스템 호출번호를 시스템 호출 서비스 루틴표에 인덱싱하여 연관된 서비스 루틴 가져옴
    5-3. 서비스 루틴 작업 시작
    5-4. 서비스 루틴이 system_call 루틴 상태를 생성
    5-5. 커널 스택에 레지스터값 복원 후 시스템 호출 반환값을 스택에 저장
    5-6. 사용자 모드로 돌아감

솔직히.. 위의 내용을 대부분 이해하지 못했다고 생각한다.(진짜모름)
하지만 이전 PintOS에서 얼핏 들었던것과 합쳐서 생각해보면
시스템콜을 수행하는데 많은 복사와 데이터 불러오기가 필요하다
는 것을 알 수 있다.

이러한 오버헤드의 자세한 사항은 추후 알아보도록 하자.
그래서 결론은

스레드의 생성 작업은 SystemCall이 필요하고, 상당한 오버헤드를 가져온다 이다.
그럼 다시 한번 위의 스레드로 돌아가 보자.

만들어지고
잘 돌아가다가
뭔일있으면 잠시 쉬고
완료되었으면 삭제한다.

힘들게 만들었던 스레드를 그냥 지운다고 한다.

서버 한참 돌릴때 라이브 스레드만 300개가 넘고, 전체 스레드는 거의 400개나 된다.
일정한 숫자를 유지하고있어서 변동이 없는것이지, 자기 일 다하고 사라진다 생각하면 장난아니다..
왜냐하면

30분정도 서버 테스트하는데 거의 800만개 이상의 요청을 보냈었는데, 이 요청마다 스레드를 새로 만들고 지우면.. 생각하기도 싫어진다. 또한 몇 ms단위의 반응이 중요한 웹페이지에서 큰 오버헤드를 가진다는 것은 시스템의 성능이 팍팍 떨어진다는 것이고, 수용할 수 있는 user의 숫자가 크게 떨어지게 될 것이다.

따라서, Tomcat이 스레드풀을 만들고 운영하는 이유를 알았다.
단 1줄로 요약하자면

스레드 만드는데 오버헤드가 너무 크니 만들어놓고 계속 쓰자
이다.




그럼 스레드풀을 사용해야하는 이유에 대해서는 알게 되었는데,
어떻게하면 효율적으로 스레드풀을 사용할 수 있을까?
기본 설정이 되어있지만 내 시스템에 최적화된게 아니니까 오버헤드가 발생하지않을까?
발생한다면 어떻게 줄일 수 있을까?

일단 테스트부터 해보자.
비교군 부터 만들어 보자

비교군이다. user수가 너무 적은거같아 5000명으로 했다.

thread는 136개정도

다음은 Tomcat 스레드를 50개로 설정했을때이다.
RPS는 비슷하지만 스레드 개수가 증가했다.

이번엔 10개로 바꿔봤다.
톰캣 스레드의 기본 개수가 200개인거 보면 별로 효과가 있어보이진 않는다..

???
이상하다.. 분명히 스래드 조절로 성능 향상을 할 수 있다고 했는데..?
그래서 스래드 최적화 공식을 봤다.

그래서 공식을 다시 한번 자세하게 보니까
CPU 수 (CPU 목표 사용량) (1+대기 시간/서비스 시간)
라고 한다.

아 그럼 cpu 코어 개수가 아니라 cpu를 얼마나 사용하는지를 봐야겠구나!
라고 생각해서 Visual VM의 thread를 까보았다.

??

?????
이게 뭔가싶어서 한참을 처다봤다.
그러다 http-nio의 이름이 보여서 이걸 검색을 하니
레퍼런스 : https://px201226.github.io/tomcat/

Tomcat의 워커스레드라고 한다....
이제야 모든게 이해되기 시작한다.

  1. CPU 코어 개수가 아닌 CPU 사용량을 기준으로 계산했어야 했다.
  2. 스레드가 100개씩 생성되었어도 대부분의 스레드 running time은 0.a%에 불구하다.
  3. 따라서 스레드 개수를 줄여야 한다.

이러한 사고의 과정으로 Thread 개수를 1개로 줄여버리니

running time 31%, RPS 1.6만을 기록했다.(스레드가 100개일때도 RPS 1.6만대)
즉, 사용하지도 않은 스레드를 많이 만들어서 쓸대없이 메모리 낭비를 하고 있었다.
앞으로 서버를 만들면서 로직이 복잡하면 그때 스레드를 최적화 시켜야 겠지만...
지금상황에서는 스레드 수를 줄이는게 올바른 판단이라고 생각했다.

그래서 max값을 5로 바꿨다.
요청이 늘어나도 thread를 만드느라 서버 자원이 낭비되지 않을것으로 예상된다.

이후 실험을 해봤다.

위는 스레드 1개, 아래는 약 290개 정도의 스레드가 만들어졌을때의 상황이다.

추가

실험한다고 스레드 1000개, user 100만명 집어넣었더니 파이썬이 터져버렸다

WebFlux

WebFlux는 항상 MVC보다 빠르다고 알고 있었다.
그런데.. 그런데!

(위는 WebFlux, 아래는 MVC)


이건 WebFlux의 반응값

이건 MVC 반응값 이다.
머릿속에 혼란이 온다. WebFlux가 빠르지 않은 경우가 있다고는 들었지만, 이렇게 차이가 나는건 생각하지도 못했다.

(활활타는 cpu)
아직은 정확한 이유를 찾지 못했다.
하지만 확실한건... WebFlux는 정말 어렵고, 막 사용할 수는 없다는 것이다.
검색을 하다가 약간의 실마리를 찾은건 비동기 연결을 하기 위한 오버헤드가 존재한다는 것이다.

Redis 적용

곰곰히 생각을 해 봤다.
이전 글에서도 말했지만, 절대 다수의 상황에서 캐싱은 옳다.
메모리와 Disk의 데이터 불러오는 속도는 천차만별이다.

그리고 프로젝트의 구조를 살펴보자.

음.. 나름 ERD를 짜봤는데 뭔가 불편하다 시간이 부족해서 정규화에 대해서 공부를 못했다.
그리고 실제 데이터를 운영해본 경험이 없어서 어떻게 구조를 짜야할지도 감이 잘 안잡혔다.
실제로 데이터를 추가하면서 생각해보니 확장성을 고려하면 추가적인 변경이 필요하다는 생각을 하였다.
하지만... 지금은 우선순위가 있으니 Todo로 남겨두도록 하자.

내가 불편한 이유는 다음과 같다.

저거 빼고는 CREATE가 없다.
뭔소리냐?

저거빼고는 싹다 READ만 쓰인다는 소리다.
그것도 데이터의 양이 그렇게 크지도 않고(다 해봤자 한 10MB 나올라나?)

병원 데이터의 크기도 이정도밖에 되지 않으며

거의 4000줄에 달하는 기상청 구역코드조차 16KB밖에 되지 않는다.
따라서, 이러한 소규모 데이터를 Client에 맞게 제공하는 사이트를 기획하고 있기에
캐싱을 도와주는 Redis는 정말 신경 안쓸래야 안쓸수가 없다.

이제, redis를 깔아보자.

정말 간편하다. 도커 세팅이 되어 있음으로 딸깍딸깍 해서 Redis 이미지를 받아와 실행해주자.

실행할때 6379:6379⁠로 포트포워딩 해주는것도 잊지 말자!

이후 SpringBoot에서 DB 연결을 하면 잘 되어있는걸 확인할 수 있다.

확인 해 보기 위해서 @Cachable을 사용한 서비스를 하나 만들고 테스트했다.


그리고 비교군으로 Cache를 사용하지 않는 똑같은 로직.

당연한 결과겠지만
Cache를 사용했을때는 RT가 일정하다.
아무래도 DB에서 특정 데이터를 요청하는 작업이 큰 부담이 되는듯 하다.

Cache는 정상적으로 작동되는 모습이다.
앞으로 서비스 로직을 추가할때, 왠만한 것들은 다 Cache를 사용할 것 같다.
중요 Client 데이터를 제외하면, DB의 데이터를 제공해주는 것 뿐이기에, 적극적으로 사용해도 괜찮다는 판단을 내렸다.

profile
닭이 되고싶은 병아리

0개의 댓글