네트워크 소켓을 통해 최대로 커버 가능한 동시 접속자의 작업수는 1만개였다. 이때 당시는 사용자 수 = 프로세스 개수(혹은 쓰레드 개수)로 서버 인프라가 설계되어 CPU, 메모리 등 자원들이 사용되었다.
해결 동시 접속에 특화된 Nginx를 쓰자!
효율적인 서버 자원(CPU, 메모리 등) 사용
1. Nginx 개요
세계에서 가장 많이 사용하고 있는 웹서버이다. (웹서버의 TOP2 아파치와 엔진엑스)
동시접속 처리에 특화된 웹 서버로, 가벼움과 높은 성능을 지향하고 있다.
Nginx는 요청에 응답하기 위해 비동기 이벤트 기반 구조이며 이는 Apache 서버의 스레드/프로세스 기반 구조와 대조적이다.
Nginx의 이벤트 기반(Event-Driven) 구조는 서버에 많은 부하가 생길 경우 성능을 예측하기 쉽게 해주며, 고정된 프로세스만 생성하고, 내부에서 비동기방식으로 작업들을 처리하기 때문에 스레드/프로세스를 효율적으로 처리할 수 있다.
* 웹 서버의 역할
1. 정적 파일을 처리하는 HTTP 서버
- 클라이언트가 정적 파일(HTTP, CSS, Javascript, 이미지)만 요청했다면 웹 서버가 직접 응답할 수 있다.
2. 응용프로그램 서버에 요청을 보내는 리버스 프록시
- 프록시는, 클라이언트와 서버 통신 중간에서 대신 통신을 해주는 서버를 의미한다.
- 포워드 프록시는, 내부망에 함께 있는 클라이언트가 인터넷을 통해 어딘가에 있는 서버로 요청을 보내려고 하면 이 요청을 받아 연결해준다. 클라이언트 앞단에서 처리!
- 리버스 프록시는, 내부망의 서버 앞단에서 요청을 배분하여 처리한다. 클라이언트가 리버스 프록시에 요청하면 프록시 서버가 배후 서버(reverse server, 응용 프로그램 서버)로부터 데이터를 가지고 온다. (nginx.conf 파일에서 location 지시어를 사용하여 요청을 배분한다.)
- WAS(Web Application Server)는 대부분 DB 서버와 연결되어 있으므로, WAS가 직접 통신하게 되면 보안에 취약해진다. 리버스
Apache와 Nginx 차이?
과거 멀티 쓰레드를 사용하던 시절엔Blocking I/O 방식으로 처리가 이루어진다. (*멀티 쓰레드: 하나의 프로그램에 동시에 여러 작업을 돌리는 작업으로, 프로세스 하나의 자원을 공유하다보니 처리 속도가 빠르다는 장점이 있지만, 한정된 프로세스 자원을 쪼개서 사용하는 만큼 쓰레드별로 자원을 효율적으로 분배해야 한다.)
동시 다발적으로 오는 작업에 부적합했다. 요청에 따라 쓰레드/프로세스를 생성하다보니 요청이 많아지거나 오랜 시간이 소요되는 작업에도 처리 방식이 무거워졌다.
그래서 나온 방법 =>Non-Blocking !
Non-Blocking 방식은 앞의 프로세스가 작업을 수행중이더라도, 다음 프로세스는 기다리면서 다른 작업을 할 수 있다.
Nginx는 Non-Blocking 방식을 사용하고 있다.
이에 따라 Apache와 Nginx의 차이점을 살펴볼 수 있다.
1) Apache
1995년부터 웹서버 기반 서버엔진.
Prefork와 Worker 방식으로 동작한다.
하나의 요청 당 서버에서는 하나의 프로세스(혹은 쓰레드)를 다룬다. 하지만 서버 내 CPU/메모리는 한정적임에 반해 프로세스(쓰레드)는 요청이 들어온 만큼 계속 생성/작업을 해 무리한 작업이 될 수 있다.
Process가 Blocking시, 프로세스 작업이 끝날 때까지 대기해야 한다.
주로 요청 하나당 프로세스(혹은 쓰레드) 하나가 쓰인다.
자원(CPU, 메모리 등) 사용이 유동적이다. (요청에 따라 쓰레드, 프로세스 할당 개수가 증감된다.)
서버내 자원(CPU, 메모리 등) 활용이 비효율적이다.
2) Nginx
2007년 오픈소스로 공개
프로세스를 효율적으로 활용함으로서 서버 자원을 최대한 활용하고, Event Driven을 활용한 비동기 Non-Blocking 방식을 선호하여 프로세스 작업이 끝날 때까지 대기하지 않아도 된다.
서버가 실행되면 쓰레드를 미리 생성한다. 클라이언트로부터 요청이들어오면 서버에서는 한정된 쓰레드 개수를 기반으로 요청을 처리한다. 하지만 요청이 더 많아질 경우, 늦게 들어온 요청은 잠시 큐에 대기를 하고 작업이 끝난 쓰레드는 큐에 대기하고 있는 요청을 처리한다.
2. Nginx 작동 과정
1. 쓰레드 개수(Thread Pool) 해당 스레드 풀 내에 한정된 자원만을 가지고 HTTP 요청에 대한 작업을 처리한다.
2. 요청에 대한 응답은 Nginx 내부에서 실행되고 있는 Worker Process에서 진행된다. Worker Process는 개발자가 따로 설정하지 않았다면 CPU에 맞게 자동으로 Worker Process가 생성된다.
3. Nginx는 Event Driven(비동기 처리방식)의 특성을 가지고 있다. Event Driven은 Event Loop 기반으로 요청에 대한 작업을 처리한다. (Event Loop에서 작업이 처리될 때, 비동기 방식으로 작업이 돌아간다.)
Nginx Event Driven 내에서 Event Handler에서 작업을 마치게 되면 완료된 순서대로 Queue에 쌒이고 Event Loop를 돌면서 Queue에 완료된 작업이 있는지 체크하면서 CPU가 IDLE한 상태가 없도록 활동한다. Queue에 완료된 작업이 있다면 클라이언트에 응답(Response)한다.
파이썬에서 특정 시간마다 배치를 돌릴 수 있는 스케쥴링을 수행할 수 있는 모듈이 2개가 있다.
1) schedule
2) apscheduler
1) schedule
schedule은 명령어가 직관적으로 알아볼 수 있어 사용이 용이하다.
설정이 복잡해질 경우 사용 여부를 고려하는 것이 좋다.
# schedule 설치
> pip install schedule
# 사용 방법
import schedule
import time
def job():
print("Hello world!")
# 10초에 한번씩 실행
schedule.every(10).seconds.do(job)
# 10분에 한번씩 실행
schedule.every(10).minutes.do(job)
# 매 시간 실행
schedule.every().hour.do(job)
# 매일 10:30 에 실행
schedule.every().day.at("10:30").do(job)
# 매주 월요일 실행
schedule.every().monday.do(job)
# 매주 수요일 13:15 에 실행
schedule.every().wednesday.at("13:15").do(job)
while True:
schedule.run_pending()
time.sleep(1)
- 함수에 인자를 전달하는 방법
# 사용 방법
import schedule
import time
def job(text):
print(text)
# job("Hello world!") 를 10초에 한번씩 실행
schedule.every(10).seconds.do(job, "text")
while True:
schedule.run_pending()
time.sleep(1)
2) apscheduler
수행 방식은 3가지가 있다.
- Cron 방식: Cron 표현식으로 수행
- Date 방식: 특정 날짜에 수행
- Interval 방식: 일정 주기로 수행
각 방식마다 파라미터가 달라집니다.
스케줄 종류에는 BlockingScheduler, BackgroundScheduler 가 있다.
BlockingScheduler는 단일 수행에, BackgroundScheduler는 다중 수행에 사용된다.
# apschedule 설치
> pip install apschedule
# 사용방법
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.base import JobLookupError
import time
def job_1():
print("Job1 실행: ", "| [time] "
, str(time.localtime().tm_hour) + ":"
+ str(time.localtime().tm_min) + ":"
+ str(time.localtime().tm_sec))
def job_2():
print("Job2 실행: ", "| [time] "
, str(time.localtime().tm_hour) + ":"
+ str(time.localtime().tm_min) + ":"
+ str(time.localtime().tm_sec))
# BackgroundScheduler을 사용하면 start를 먼저 하고 add_job을 이용해 수행할 것을 등록해줍니다.
sched = BackgroundScheduler()
sched.start()
# interval - 매 3초마다 실행
sched.add_job(job, 'interval', seconds=3, id="test_2")
# cron 사용 - 매 5초마다 job 실행
# : id 는 고유 수행번호로 겹치면 수행되지 않습니다.
# 만약 겹치면 다음의 에러 발생 => 'Job identifier (test_1) conflicts with an existing job'
sched.add_job(job_1, 'cron', second='*/5', id="test_1")
# cron 으로 하는 경우는 다음과 같이 파라미터를 상황에 따라 여러개 넣어도 됩니다.
# 매시간 59분 10초에 실행한다는 의미.
sched.add_job(job_2, 'cron', minute="59", second='10', id="test_10")
while True:
print("Running main process...............")
time.sleep(1)
댓글 개