cgi-bin으로 하루 2억 리퀘스트 서비스하기
(jacob.gold)- 초기 웹 시대에 널리 쓰였던 CGI 프로그램이 현대 하드웨어에서 여전히 높은 성능을 낼 수 있음을 실험으로 확인함
- CGI는 프로세스별로 요청을 처리해 메모리 관리가 자동으로 이뤄지고, 배포가 단순한 장점이 있음
- 벤치마크 결과, 평범한 16스레드 CPU 서버에서도 초당 2400건 이상, 하루 2억 건 이상 요청 처리 가능성을 입증함
- Go 언어와 SQLite로 작성된 guestbook.cgi 예제 코드 및 Dockerfile을 오픈소스로 공개함
- CGI는 지금은 흔히 쓰이지 않지만, 여전히 실용적이고 현대적인 대안이 될 수 있음을 보여줌
CGI 프로그램과 동작 원리
- 2000년대 초반에는 CGI(Common Gateway Interface) 프로그램이 동적 웹사이트 구축의 주요 방식이었음
- 대부분 Perl이나 C 언어로 작성되었으며, 성능 향상을 위해 C가 선택되기도 했음
- CGI의 개념은 간단하지만 강력함
- 웹서버는 환경 변수에 요청 메타데이터(HTTP 헤더, 쿼리 등) 설정
- 별도의 프로세스 생성하여 CGI 프로그램 실행
- 요청 본문을 stdin으로 전달
- 프로그램의 stdout을 HTTP 응답으로 캡처
- stderr 출력을 서버 에러 로그로 전달함
- 프로그램이 요청을 마치면 프로세스가 종료되어 파일 디스크립터 및 메모리가 자동 해제됨
- 개발자 입장에서는 신규 버전 배포도 cgi-bin/ 디렉터리에 파일만 복사하면 배포가 끝나 매우 간단했음
Hug of death(트래픽 폭주)
- 2000년대 초반 웹서버 대부분은 1~2 CPU, 1~4GB 메모리 환경이 일반적임
- Apache 웹서버가 접속마다 httpd 프로세스를 fork하는 구조 특성상, 다수 접속 시 메모리 요구량이 증가함
- 동시 접속이 100개를 넘기 힘들었고, 유명 사이트에 링크만 걸려도 서버가 쉽게 과부하되는 경우가 많았음
- ( Slashdot Effect : 그 당시 유명했던 Slashdot에 링크가 올라오면 트래픽이 쏟아짐. 요즘의 해커뉴스 탑에 오르는 것과 비슷)
현대 서버 환경에서의 CGI
- 현재는 384개의 CPU 스레드를 가진 서버도 출현, 비교적 작은 VM에서도 16개의 CPU 제공 가능
- CPU 및 메모리 성능이 대폭 향상됨
- CGI 프로그램은 별도 프로세스 기반이므로 멀티코어를 자연스럽게 활용 가능함
- 이러한 점 때문에 현대 하드웨어에서 CGI 프로그램이 얼마나 빠른지 직접 벤치마크 테스트를 진행함
- 실험은 AMD 3700X(16스레드) 서버에서 수행함
벤치마크 주요 결과
- 간단한 CGI 프로그램을 Apache와 Go
net/http
서버 환경 모두에서 테스트 -
guestbook.cgi 프로그램 설명
- 방문자가 웹사이트 하단에 댓글을 남길 수 있는 간단한 방명록 프로그램 구현
- Go 언어와 SQLite 사용, 최대한 단순하지만 현실성 있도록 설계
- 소스코드와 Dockerfile 모두 GitHub에 공개함
- HTTP 부하 생성 도구
plow
를 사용해 16개 연결로 10만 건씩 요청 수행 - 평범한 하드웨어상에서도 초당 2,400건 이상, 즉 하루 2억 건 이상 요청 처리 가능함
- 현재 CGI가 대세는 아니나, 여전히 실제 서비스 운영에서도 사용 가능함
-
Apache 환경에서의 write 벤치마크
- 초당 약 2468건의 요청 처리, 평균 6.47ms의 응답 지연 시간
- 10만 건의 POST 요청을 40.5초 만에 처리
- 대부분 요청이 7ms 내 응답, 극소수만 100ms 초과
- 실질적으로 높은 쓰기 처리 성능 입증
-
Apache 환경에서의 read 벤치마크
- 초당 약 1959건의 요청 처리, 평균 8.16ms의 응답 지연 시간
- 10만 건의 GET 요청을 51초 만에 처리
- 절반 이상의 요청은 8ms 이내, 최대 지연도 31ms에 그침
- 읽기 성능 역시 충분히 우수함
-
Go net/http 환경에서의 write 벤치마크
- 초당 약 2742건의 요청 처리, 평균 5.83ms의 응답 지연 시간
- 10만 건의 POST 요청을 36.4초 만에 처리
- 처리량은 평균 2,742 RPS, 평균 지연 5.8ms로 Apache보다 수치상 더 나은 성능
- 95% 이상의 요청이 6ms 이내 처리됨
- Go 환경에서의 CGI도 충분한 실전 성능 보유
-
Go net/http 환경에서의 read 벤치마크
- 초당 약 2469건의 요청 처리, 평균 6.47ms의 응답 지연 시간
- 10만 건의 GET 요청을 40.4초 만에 처리
- 대부분의 요청이 7ms 안에 서비스 가능
- 읽기 처리량과 응답 속도 모두 Apache와 비슷하거나 우수함
결론 및 링크
- CGI 프로그램은 최신 하드웨어에서 초고속 동시성, 간단한 배포, 운영체제가 자동 자원 해제 등의 장점 보유
- 현대적인 프레임워크에 비해 극히 단순하지만, 일정 규모 서비스에는 지금도 실전 활용 가능
- 방명록 예제 및 벤치마크 실험 데이터는 아래 깃허브에 공개
https://github.com/Jacob2161/cgi-bin
Hacker News 의견
-
1990년대에도 C로 작성된 CGI 프로그램이 정말 빠른 속도를 보여줬던 환경 기억, 그러나 에러가 많이 발생하는 점이 단점이었던 점 인정, 기사에 언급된 Go 프로그램이나 Nim 같은 최신 언어, 데이터베이스 연결을 하지 않는 한 로컬호스트에서 굉장히 빠르고 지연시간이 적은 느낌, 마치 CLI 유틸리티에서 fork & exec을 사용하는 기분, 네트워크 레이턴시에 비하면 비용이 거의 무시할 만한 수준이었음
-
다만 특정 기술에 중독되기 쉬운 문화 언급, 예를 들어 파이썬 인터프리터 같이 시작 비용이 큰 언어에 익숙해지고 나면 멀티샷 혹은 영속적인 모델을 필요로 하게 됨
-
초창기 HTTP의 원샷 모델은 FTP 서버가 수백 개의 유휴 로그인 세션을 오래 유지할 만큼의 메모리가 부족했던 문제에서 출발한 것이었음
-
CGI에서 pre-forking(지연을 숨길 수 있음)과 Rust 같은 안전한 언어를 결합하면 뛰어난 시스템 설계 가능성 언급, TLS 종단 처리는 멀티스레드 웹 서버(또는 CloudFront 같은 레이어)에서 처리할 수 있어 편리성 강조
- 상태가 남지 않고, 코어 덤프 및 디버그가 매우 쉬운 환경, 주로 선형적인 요청 모델로 확장도 간단하게 가능
- stdin에서 읽고 stdout으로 쓰기만 하면 되는 간결함을 찬양, Websockets가 복잡도를 조금 높이긴 하지만 걱정할 수준 아님
- Java의 부상으로 인해 fork()의 비용과 C의 위험성 회피 목적에서 애플리케이션 서버로의 전환이 급격히 진행된 흐름 상기, 이제 다시 단순성으로 돌아갈 수 있음 주장
- Rust를 좋아하지 않지만 이런 방식의 웹 백엔드 코드가 손쉽게 작성될 수 있는 시대가 오면 node/js, php, python 개발자들에게도 매력적으로 다가올 것 기대
-
-
CGI 시절부터 개발을 시작하며 짧게 실행되는 서브프로세스를 돌리는 것에 대한 강한 반감을 가지게 된 경험
-
PHP와 FastCGI가 웹 요청마다 신규 프로세스를 만드는 성능 문제를 벗어나기 위해 만들어졌다는 배경 설명
-
최근 하드웨어의 발달 덕분에, 프로세스 시작 비용이 실제로 큰 문제는 아니라는 현실을 알게 됨
-
이 벤치마크가 초당 2000개의 요청을 처리할 수 있고, 수백 개 정도만 처리해도 여러 인스턴스로 확장하기 쉬운 현대적 환경 언급
-
AWS Lambda를 CGI 모델의 재탄생으로 묘사한 의견에 동의, 꽤 적절한 비유라고 생각함
-
만약 CGI 스크립트를 정적 링크된 C 바이너리로, 크기까지 신경 쓰며 배포했다면 실망이 덜했을 것이라 언급
- PHP 해석기나 각종 라이브러리 로딩, 파일 파싱 등 동적 링크의 프로세스 시작 비용이 훨씬 큼
- Go를 쓴다는 것은 25년 전에도 충분히 경쟁력이 있을 수 있었던 방식이라 확신
- SQLite 데이터베이스 오픈이 컨텍스트 스위치로 소켓을 넘기는 것과 거의 비슷한 성능이며, 원격 mysql 접속과 비교하면 훨씬 빠른 점 강조
- FastCGI가 새로운 애플리케이션에도 여전히 뛰어난 선택임을 주장
-
CGI는 저부하 환경에서 금전적·성능적으로 부담이 크지 않았음
- 고부하 상황에서는 FastCGI처럼 지속적으로 실행되는 프로세스가 더 유리
- CGI도 초당 2,000 rps까지 처리 가능하지만, FastCGI는 훨씬 높은 성능 달성 가능
- 별도 서버 프로세스 추가 및 업그레이드 시점에 재시작만 하면 되는데, 성능이 중요할 때 가치 있다고 밝힘
-
Go가 등장하기 전에는 CGI 프로그램을 C/C++로 만드는 게 안전성, 개발 난이도 모두 높았던 00년대 상황
- Perl과 Python은 해석기 시작 및 컴파일 비용이 상당히 컸고, Java는 실질적으로 더 느렸음
- AWS Lambda = CGI 모델의 환생에 가깝다는 점 동의
- 지금은 관리형 FastCGI와 거의 동일한 모델로 돌아온 느낌
- 단순히 실행파일만 업로드해서 돌리면 될 텐데 복잡도를 잔뜩 추가하는 기술의 홍수에 아쉬움
-
-
오늘날 서버에 384 CPU 스레드가 달려있고, 작은 VM조차 CPU 16개 가질 수 있는 시대
-
이런 하드웨어에서 Kestrel로 개발하면 하루에 수조 번의 요청도 무난히 처리 가능
-
PHP와 비슷한 개발 경험을 string interpolation 연산자로 제공 가능
-
LINQ와 String.Join()을 활용하면 HTML 테이블과 중첩 요소를 간단히 템플릿화
-
진짜 어려운 점은 MVC/Blazor/EF 같은 생태계의 지뢰밭을 잘 피하는 방법을 아는 것
-
프로그램 전체를 하나의 최상위 파일로 CLI에서 실행하는 방식도 가능한데, "Minimal APIs"라는 키워드를 모르면 잘못된 문서의 미로로 들어가기 쉬움
- 코어 기술 위에 추상화 레이어를 덧씌워서 Director/VP 자리를 승진하는 사례가 무척 많다는 점에 놀라움
-
-
CGI의 장점은 멀티테넌트 환경에서 격리 원시 기능을 새로 구축할 필요 없다는 점
- 한 요청에 버그가 있어도 프로세스 격리 덕분에 다른 요청에 영향 없음
- 무한 루프도 선점 스케줄링 덕에 서비스 거부(DoS)로 이어지지 않음
- rlimit으로 오래 걸리는 요청을 강제로 종료 가능
- cgroup을 사용해 테넌트별 메모리, CPU, 디스크/네트워크 IO 사용량을 공정하게 할당 가능
- 네임스페이스/감옥, 권한 분리로 요청마다 접근 권한을 제한할 수 있음
-
CGI 스크립트 덕분에 perl이 빠른 시작 시간을 위해 최적화됐던 이유
-
time perl -e ''
명령 실행 시 perl은 5ms, python3는 33ms, ruby는 77ms로 perl의 빠른 시작 시간 확인- tcc mob branch의
#!/bin/tcc -run
방식 스크립트가 perl보다 1.3배 빠르다는 점 언급 - Julia, Java VM, thread PHP 등도 시작 시간이 매우 길어지는 사례
- 사람들이 "큰 환경"에 습관적으로 의존하게 되는 현상
- Lisp 커뮤니티에서도 이미지를 사용함으로써 이게 반복되고, "emacs is bloated" 밈도 여기서 탄생
- 90년대 중후반 Perl의 전성기는 정말로 CGI 덕분에 가능했던 분위기
- 당시 getline조차 표준이 아니어서 서드파티 C 라이브러리를 몇백~몇천 라인으로 만들기도 했던 시기 회상
- 결국 "평판"에 따라 기술이 선택되고, 대부분 친구가 추천해주는 것으로 학습이 이뤄짐
- tcc mob branch의
-
-
apache tomcat 11을 사용해보면 .jsp 파일이나 전체 java servlet 애플리케이션(.war)을 ssh로 업로드만 하면 그냥 동작함
-
하나의 공유 JVM으로 최대 성능 확보
-
DB 커넥션 풀, 캐시 등도 어플리케이션끼리 공유 가능
-
정말 인상적인 경험
-
실제 사용 패턴에 따라 다름
-
대용량 서비스에는 훌륭하지만, 50개의 소형 어플리케이션을 각각 하루 수백 건만 처리해야 한다면, Tomcat의 메모리 오버헤드는 CGI 스크립트 기반 Apache/Nginx에 비해 너무 크다는 점 지적
-
파일을 단순히 복사해서 배포하는 시절이 그립다는 감상
-
왜 배포 과정이 이렇게 복잡해졌는지 아쉬움 토로
-
지금도 Jetty로 백엔드 웹앱을 즐겁게 사용 중이라는 경험 공유
-
Tomcat/Jakarta EE/JSP 스택이 의외로 상당히 견고하다는 소감
-
PHP처럼 HTML과 코드를 뒤섞어 쓸 수도 있고, 순수 Java 라우트도 가능
-
Websockets 지원, 싱글 프로세스 멀티스레드 모델이라 실시간 통신에도 강점
-
필요하면 요청마다 데이터 공유 가능, JSP 코드는 기본적으로 요청 범위로 제한
-
배포가 정말 쉽고, webapps 디렉토리에 신규 파일만 업로드하면 Tomcat이 자동으로 새로운 앱을 로드 및 기존 앱을 언로드
-
단점으로는 클래스로더 누수로 인해 garbage collection에 실패할 수 있다는 점, 싱글프로세스 모델의 숙명
-
-
-
apache 요청에 대한 시각화 도구 ibrahimdiallo.com/reqvis 제작
- 데스크톱 브라우저에서 최고의 경험 제공
- HN 트래픽 데이터를 바탕으로 실제 동작 흐름을 웹에서 확인 가능
-
요즘 복잡한 아키텍처로 가고 있는 상황이 의심스러웠음, 사실 좋은 하드웨어로 충분히 기존 기술을 쓸 수도 있다는 가능성 언급
-
수백만 명에게 실시간 주가 정보를 알려주는 시스템 설계 질문에 처음에는 Kafka, pubsub 등 복잡한 스트림 구조를 떠올렸지만, 결국 서버에 정적 파일을 두는 단순한 방식도 고민
-
이런 방식의 실제 운용 비용 궁금
- 실질적으로 모든 웹 API의 레이턴시는 DB 쿼리나 ML 모델 쿼리가 결정
- 나머지 프로세스는 Python 등 느린 언어를 써도 별 거 아닌 수준
- 변화가 드문 데이터만 반환하면 NIC 한계까지도 쉽게 도달 가능
-
-
serverless 아키텍처와 비슷하지만, 훨씬 간단하고 저렴한 점 강조
- 실제로 비즈니스 현장에서 이렇게 사용하는 사례가 있는지 궁금
-
이런 전통적인 구조를 재고하지 않고 단순히 "serverless functions"라는 새로운 패러다임만 만들어낸 것에 아쉬움
- Lambda 같은 serverless function이 별도의 보호 메커니즘(마이크로 VM 등)이 있기는 하지만, 사실상 CGI와 권한 조정만으로도 훨씬 적은 복잡성으로 멀리 갈 수 있었을 것이라 생각