Home

읽기 설정

웹 애플리케이션을 매일 20명의 사용자가 사용한다고 가정해 보겠습니다.00:00

어느 날, 1만 명의 사용자가 동시에 앱을 사용하려고 하는데, 서버가 그 정도 트래픽을 처리하지 못해서 응답을 시작하는 상황이 발생합니다.00:03

오류가 발생하고 있습니다. 문제는 하드웨어나 네트워크에 있지 않습니다.00:10

이것은 소프트웨어가 연결을 처리하는 방식 때문에 발생합니다. 그런데 연결이란 정확히 무엇일까요?00:14

브라우저가 서버로부터 무언가를 필요로 할 때, 먼저 채널을 엽니다. 열린 채널이 바로 연결이라고 할 수 있습니다.00:18

브라우저는 그 채널을 통해 요청을 보냅니다.00:24

예를 들어, 자바스크립트 파일의 경우 서버는 동일한 채널을 사용하여 데이터를 다시 전송합니다.00:26

하나의 연결은 여러 요청을 순차적으로 전달할 수 있으므로, 단일 요청과는 다른 개념입니다.00:31

요청이 전달되고, 아무것도 전송되지 않아도 계속 열려 있습니다. 90년대 후반부터 2000년대 초반까지 웹 서버는 연결당 스레드 모델을 사용했습니다.00:36

사용자가 연결하면 서버는 새로운 스레드를 생성했습니다.00:43

요청을 처리하기 위해 새로운 스레드나 프로세스를 사용하며, 해당 스레드는 연결이 유지되는 동안 계속 살아있습니다.00:48

방문자가 100명이라면 100개의 스레드가 필요하고, 방문자가 1만 명이라면 1만 개의 스레드가 필요합니다.00:54

각 스레드마다 자체 스택 메모리가 필요합니다. 스택 크기를 최대한 줄여도, 한 스레드가 1MB의 RAM을 사용할 수 있습니다.01:00

1만 개의 스레드라면 10GB의 메모리가 소비됩니다.01:07

애플리케이션이 어떤 작업도 수행하기 전에 연결을 계속 유지하기 위해서입니다.01:10

또 다른 문제는 CPU의 한계에서 비롯됩니다. 프로세서는 한 번에 하나의 작업만 처리할 수 있습니다.01:15

코어 당으로 말씀드리는데요, 실제 병렬 처리를 흉내내기 위해 운영체제가 빠르게 스레드들을 전환합니다.01:21

한 스레드의 상태를 저장하고 다른 스레드의 상태를 로드하는 데는 시간이 걸립니다.01:26

1만 개의 스레드를 사용하는 경우, CPU는 대부분의 시간을 페이지를 제공하는 대신 컨텍스트를 전환하는 데 사용하게 됩니다.01:29

이는 C10K 문제라고 불리는 잘 알려진 문제입니다.01:35

C는 동시성을 의미하고, 10K는 1만 개의 연결을 의미합니다.01:39

러시아의 한 엔지니어가 자신이 일하던 검색 포털 Rambler에서 정확히 이 문제를 겪었고, 이를 해결하기 위해 Nginx를 개발했습니다.01:43

이 스레드 당 연결 모델은 자원 낭비적인데요, 연결이 대부분의 시간을01:51

기다리는 데 사용되거든요. 클라이언트로부터 요청을 기다리거나, 디스크나 백엔드에서01:55

응답을 기다리고, 네트워크를 통해 바이트가 다시 전송되기를 기다립니다. 그동안 해당 연결에 묶여 있던 스레드는 차단됩니다. 다른 처리를 할 수 없습니다.01:59

Nginx는 하나의 프로세스를 사용하여 동시에 수천 개의 연결을 감시합니다.02:11

대부분의 시간 동안, 대부분 유휴 상태에 있습니다. 데이터가 들어오거나 응답을 보낼 때가 아니면 아무것도 하지 않죠.02:15

해당 연결을 처리한 후, 필요한 다음 연결로 이동합니다.02:21

과거의 스레드처럼 연결을 기다리는 것을 멈추지 않고, 하나의 프로세스가 수천 개의 연결을 계속 유지합니다.02:25

Nginx를 시작하면 하나의 마스터 프로세스와 여러 개의 워커 프로세스를 생성합니다.02:32

마스터 프로세스는 실제 작업을 거의 수행하지 않습니다. 설정 파일을 읽고, 네트워크 포트에 연결하며, 워커 프로세스를 생성합니다.02:37

워커들은 네트워크 트래픽을 처리하는 실제 작업을 수행합니다.02:43

일반적으로 Nginx는 CPU 코어 당 하나의 워커 프로세스를 실행하도록 설정합니다.02:48

8코어 서버를 사용하신다면 8개의 워커를 실행하게 됩니다. 이러한 비율은 의도적으로 설정된 것입니다.02:52

코어당 워커 하나씩 할당할 경우, 운영체제는 그들 간에 컨텍스트 스위칭을 거의 필요로 하지 않습니다. 워커 프로세스는 CPU 위에서 끊임없이 실행되며 네트워크 요청을 중단 없이 처리합니다. 워커 하나를 가지고02:57

있습니다.03:03

수천 개의 연결을 담당하지만, 어떻게 주의가 필요한 연결을 알까요?03:09

대부분 아무것도 하지 않기 때문에, 전체 목록을 다 확인하는 데 시간을 낭비할 수 없죠.03:13

그리고 각 연결에 데이터가 있는지 하나씩 확인하는 대신, 연결 하나하나를 확인하는 대신에03:17

작업자는 커널에게 감시를 맡깁니다. 커널은 어차피 모든 네트워크 트래픽을 수신하고 있습니다.03:22

그래서 어떤 연결이 데이터를 가지고 있는지, 어떤 연결이 유휴 상태인지 이미 알고 있습니다.03:27

작업자(worker)는 ePoll이라는 리눅스 기능을 사용하는데, 작업자와 커널이 서로 소통하기 위해 사용하는 두 개의 목록이 있습니다.03:31

첫 번째 목록은 관심 목록인데요, 여기서 워커는 커널이 어떤 연결을 감시해야 하는지 기록합니다.03:38

두 번째 목록은 준비 목록이라고 하는데, 여기서 커널은 실제로 어떤 연결에서 무언가 발생하고 있는지 알려줍니다.03:43

관심 목록은 연결이 들어오면서 채워집니다.03:50

연결이 열리면, 워커는 ePollctl이라는 호출을 통해 한 번 등록합니다.03:52

그래서 그것이 관심 목록에 추가됩니다. 커널은 이 목록을 통해 어떤 연결이 이 워커에 속하는지 알아야 합니다.03:58

한편, 준비 목록은 커널에 의해 채워집니다.04:02

관심 목록에 있는 연결에 대한 데이터가 도착하면 커널은 해당 연결을 준비 완료 목록에 추가합니다.04:05

예를 들어, 사용자가 웹 앱을 열면 연결이 생성됩니다.04:12

작업자는 새로운 연결을 감지하고, 커널에게 이 연결을 주시하도록 알려 ePoll을 사용합니다. 그렇게 되면 해당 연결은 관심 목록에 추가됩니다.04:15

그 후 사용자가 주문 탭을 클릭하면, 프론트엔드에서 주문을 가져오기 위해 서버로 요청을 보냅니다.04:22

그 요청은 이미 열려 있는 연결을 통해 전송됩니다.04:27

커널이 들어오는 데이터를 감지하면, 관심 목록을 확인하고 연결을 즉시 준비 완료 목록에 표시합니다.04:30

준비 목록에 연결이 올라가면 커널의 작업은 완료됩니다. 워커는 아직 바이트를 읽고, 주문을 가져오고, 응답을 보내야 합니다.04:36

실제로 작업을 처리하기 위해 워커는 무한 루프를 실행합니다. 가장 먼저 하는 일은 ePoll 대기 함수를 호출하는 것입니다.04:43

이것은 ready list에 있는 연결을 커널에게 요청하는 것입니다.04:49

만약 저희 연결이 거기에 있다면, 워커가 그것을 가져와서 요청을 처리하고 데이터를 사용자에게 다시 보냅니다.04:52

그 다음, 전체 주기가 다시 시작됩니다. 준비 목록이 비어있다면, ePoll은 무한 루프를 차단합니다.04:58

작업자는 그 코드 한 줄에서 대기 상태로 들어가서 프로세서 자원을 전혀 소모하지 않습니다.05:04

이 루프가 단순히 커널에게 변화가 있는지 계속해서 물어보는 것만은 아닙니다.05:09

한 번 물어본 후 새로운 요청이 도착하면 커널에게 깨워달라고 기다립니다.05:13

이 무한 루프는 프로세스 전체 수명 동안 계속 실행되며, 이것을 이벤트 루프라고 합니다.05:18

작업자가 유휴 클라이언트에 시간을 낭비하지 않기 때문에, NGINX는 한 번에 1만 개의 연결을 처리할 수 있습니다.05:23

이벤트 루프는 완벽한 해결책처럼 보이지만, 큰 문제가 하나 있습니다.05:28

만약 워커가 하나의 연결에 멈춰 있다면, 다른 모든 연결은 기다려야 합니다.05:32

만약 서버에 느린 기계식 하드 드라이브에 큰 비디오 파일을 저장한다고 가정해 보겠습니다.05:37

어느 날, 사용자가 그 비디오 중 하나를 요청합니다. 그래서 연결이 들어오고, 워커가 질문을 던집니다.05:41

데이터를 저장하는 드라이브인데, 디스크가 느리기 때문에, 작업자는 아무것도 하지 못하고 그 자리에 앉아서 기다리고 있습니다.05:46

그 작업자는 하드웨어가 전체 파일을 읽고 응답할 때까지 기다리는 중입니다. 동시에 수천 개의 다른 활성 연결을 담당하고 있죠.05:52

비디오를 기다리는 동안, 디스크 응답을 받을 때까지 다른 모든 요청도 차단됩니다.05:59

다른 사용자는 간단한 자바스크립트 파일을 요청하고 있을 수도 있지만, 디스크 읽기가 완료될 때까지는 응답을 받을 수 없습니다.06:03

시스템은 모든 사람이 기다리지 않고도 느린 작업을 처리할 수 있는 방법이 필요했습니다.06:09

그래서, 그러한 경우들을 처리하기 위해 개발자들은 스레드 풀을 만들었습니다.06:14

작업자가 느린 디스크 읽기가 필요한 요청을 받으면, 직접 처리하지 않습니다.06:18

요청을 큐에 넣고, 별도의 백그라운드 스레드가 해당 작업을 처리합니다.06:23

백그라운드 스레드는 느린 디스크와 통신하는 작업을 대신 처리하여, 메인 워커는 이벤트 루프로 다시 돌아갈 수 있습니다.06:27

바로 epollwait를 호출하여 다른 사용자에게 서비스를 제공하게 됩니다.06:33

배경 스레드가 드디어 디스크에서 파일을 가져오면, 데이터를 메모리에 넣고 신호를 보냅니다.06:37

메인 워커가 다음에 준비 완료 목록을 확인할 때, 데이터가 준비되었음을 확인합니다.06:42

바이트를 가져와서 사용자에게 전송한 다음, 바로 다음 작업으로 넘어갑니다.06:47

메인 프로세스는 느린 작업 때문에 모든 연결이 막힐 때까지 멈춰서 기다릴 필요가 없었습니다.06:52

방금 전에 워커가 연결을 가져다가 요청을 처리하고 데이터를 다시 보내는 거라고 말씀드렸습니다.06:57

하지만 요청을 처리한다는 것이 정확히 무엇을 의미하는지 정의해야 할 것 같습니다.07:02

그래서 이 영상에서 다룰 마지막 내용은 Nginx가 실제로 어떤 역할을 담당하는 것인지입니다.07:06

Nginx는 여러분의 애플리케이션 코드를 실행하지 않습니다. 리버스 프록시 역할을 합니다.07:11

요청이 들어오면, 백엔드, 예를 들어 Node API와 같은 곳에 별도의 연결을 엽니다.07:16

들어오는 데이터를 Node로 전달하고, 백엔드가 작업을 완료할 때까지 기다린 다음, 응답을 사용자에게 다시 전달합니다.07:21

NGINX가 이미 중간에서 모든 트래픽을 가로채기 때문에, 로드 밸런서로도 사용할 수 있습니다.07:28

애플리케이션 인스턴스를 여러 개 실행하면, NGINX가 요청을 모든 서버에 분산해 줄 것입니다.07:33

따라서 트래픽이 급증하는 순간, 모든 연결에 대해 개별 스레드를 할당하는 기존 방식은 웹 서버의 성능을 저하시킵니다.07:39

엔진 엑스는 이벤트 루프에 의존하여 이 역동성을 완전히 바꾸어 단일07:45

대신, 수천 명의 클라이언트를 동시에 효율적으로 관리하는 워커 프로세스를 사용합니다. 그렇지 않으면 낭비하게 됩니다.07:50

아무것도 하지 않는 유휴 연결에 막대한 양의 메모리와 처리 능력을 낭비하는 것입니다.07:55

실제 데이터 처리가 필요할 때만 서버 자원을 사용합니다. 이 영상이 재미있으셨다면,08:00

좋아요를 눌러주시고 구독 부탁드립니다. 그리고 댓글로 다음에 보고 싶으신 주제를 알려주세요.08:07

가장 많은 좋아요를 받은 주제로 영상을 만들 거예요. 시청해 주셔서 감사합니다. 다음 영상에서 만나요.08:12

AI Summary

이 텍스트는 웹 애플리케이션의 동시 접속자 증가로 인한 서버 과부하 문제인 C10K 문제를 해결하기 위해 등장한 Nginx의 작동 원리와 역할에 대해 설명하고 있어요. 과거의 스레드 당 연결 방식은 메모리 및 CPU 자원을 과도하게 사용해서 비효율적이었는데, Nginx는 이벤트 루프, 워커 프로세스, 스레드 풀을 활용하여 이러한 문제점을 개선했고요. 또한, Nginx는 리버스 프록시와 로드 밸런서로서의 기능도 수행하며 트래픽을 효율적으로 분산하는 역할을 한다는 점이 중요해요.

Key Highlights

  • C10K 문제는 1만 동시 접속 시 발생하는 서버 과부하 문제예요.
  • 과거 스레드 당 연결 방식은 메모리 과소비 및 CPU 자원 낭비라는 단점이 있었어요.
  • Nginx는 이벤트 루프, 워커 프로세스, 스레드 풀을 통해 이러한 문제점을 해결했어요.
  • Nginx는 리버스 프록시 역할을 통해 백엔드 서버와 사용자 사이에서 중개 역할을 해요.
  • 로드 밸런서 역할을 통해 트래픽을 여러 서버에 분산하여 효율성을 높여요.

Related Videos