CGI-bin으로 하루 2억 건 요청 처리하기
(simonwillison.net)- Jake Gold는 16스레드 AMD 3700X에서 Go + SQLite CGI 프로그램을 돌려, 1990년대식 CGI가 현대 하드웨어에서 어느 정도 버틸 수 있는지 확인함
- 결과는 평범한 하드웨어에서도 초당 2400건 이상, 하루 2억 건 이상 요청 처리가 가능하다는 점을 보여줌
- CGI는 요청마다 프로세스를 시작·실행·종료하기 때문에 과거에는 오버헤드가 컸고, 이를 줄이기 위해 PHP와 FastCGI 같은 방식이 등장함
- Simon Willison은 2020년 datasette-ripgrep에서 Rust 기반 ripgrep CLI를 호출해 검색을 처리하며, 웹 요청 중 프로세스 실행을 피해야 한다는 오랜 믿음을 바꾸게 됨
- Go와 Rust처럼 시작이 빠른 언어와 다중 CPU 환경에서는 CGI식 처리가 예전보다 현실적인 선택지가 될 수 있지만, 일반적인 권장 방식은 아님
현대 하드웨어에서 다시 본 CGI 성능
- Serving 200 million requests per day with a cgi-bin은 Jake Gold가 Go + SQLite CGI 프로그램으로 1990년대식 CGI의 성능을 테스트한 글임
- 테스트 환경은 16스레드 AMD 3700X 기반 시스템임
- 핵심 결과는 CGI로도 평범한 하드웨어에서 초당 2400건 이상, 하루 2억 건 이상 요청을 처리할 수 있다는 점임
- CGI는 들어오는 요청마다 별도 프로세스를 시작하고, 실행한 뒤 종료하는 방식임
- 초기 웹 커뮤니티는 이 오버헤드를 피하려고, 코드를 메모리에 상주시켜 추가 비용을 줄이는 PHP와 FastCGI를 만들었음
CGI가 예전만큼 나쁘지 않을 수 있는 이유
- 과거 CGI의 병목 중 하나는 Perl, Python, Java처럼 매우 빠른 시작 속도를 목표로 설계되지 않은 언어로 웹 스크립트를 작성했다는 점임
- 오늘날 Go와 Rust를 사용하면 CGI 스타일 요청 처리가 훨씬 효과적으로 동작할 수 있음
- Simon Willison은 2020년 datasette-ripgrep을 만들며, Rust로 작성된 ripgrep CLI 도구를 셸로 호출해 검색을 실행했고 좋은 결과를 얻음
- CGI 프로그램은 별도 프로세스로 실행되므로 여러 CPU를 활용하는 구조와도 잘 맞을 수 있음
- 현대 서버는 384 CPU 스레드를 가질 수 있음
- 작은 VM도 16 CPU를 가질 수 있음
- CPU와 메모리 성능도 과거보다 크게 빨라짐
- 1998년식 웹 애플리케이션 작성 방식도 Go와 Rust를 만나면 흥미로운 실험 대상이 되지만, 대부분의 서비스가 따라야 할 기본 선택지는 아님
댓글과 토론
Hacker News 의견들
-
Python 같은 걸 써도 요즘 CGI는 꽤 빠름
CGI 스크립트 시작에 CPU 400ms가 걸리고 서버가 64코어라면 초당 160요청, 서버 한 대당 하루 1,400만 히트를 처리할 수 있음
정적 자산을 제외하고 하루 수백만 요청도 버거운 웹 서비스라면, 병목은 CGI 프로세스 시작 시간이 아님
예전 같으면 “Python 표준 라이브러리에서 오래 지원된 지루한 기술”이라고 했겠지만, 남은 Python 유지보수자들은 안정성과 하위 호환성을 해롭다고 보는 듯하고, 너무 지루하고 안정적인 모듈을 표준 라이브러리에서 제거해 왔음
cgi 모듈은 Python 3.13에서 제거됨
지난 25년 대부분을 매일 써 와서 아직도 프로토타이핑에는 Python을 쓰는 습관이 있지만, 이제는 그게 아쉽고 JS와 Lua 사이에서 고민 중임- cgi 제거의 근거는 https://peps.python.org/pep-0594/#cgi에 있음
흥미롭게도 거기서 2000년 7월 14일, 즉 25년 전의 https://peps.python.org/pep-0206/를 링크하는데, 그때도 cgi 패키지를 “설계가 좋지 않고 이제 고치기 거의 불가능하다”고 표현했음
https://github.com/jackrosenthal/legacy-cgi 패키지가 표준 라이브러리 모듈의 드롭인 대체품을 제공하는 듯함 - Python 유지보수자들이 제거하는 건 cgi라는 이름의 모듈이지, CGI 스크립트 구현 지원 자체가 아님
그 지원은 http.server 모듈의 CGIHTTPRequestHandler에 남아 있음
cgi 모듈에 있던 건 HTML 폼 데이터를 파싱하는 몇 가지 함수뿐이었음 - Python이 표준 라이브러리에서 CGI를 제거하는 데 불만이 있는 건 이해됨
그런데 그다음 대안으로 표준 라이브러리 자체가 없는 JS를 고려한다는 건 잘 모르겠음
Lua도 표준 라이브러리에 CGI 모듈이 없음 - 예전에는 고성능 웹이 일종의 기술 예술이었음
지금은 빠르게 출시하려고 넣은 말도 안 되게 낭비적인 부분을 찾아서 그걸 멈추는 쪽에 가까움
제대로 시도하면 앱은 저장소 접근 외에는 거의 지연 시간을 더하지 않을 수도 있음 - 이런 경우에는 JIT가 기본으로 있는 PHP나 JS 쪽을 더 선호함
Python은 1.6부터 배웠지만 주로 운영체제 스크립팅 용도로 써 왔음
1999~2003년에 Apache와 IIS 모듈에서 Tcl을 쓰고, 모듈을 계속 C로 다시 작성하면서 너무 많은 교훈을 어렵게 배웠음
- cgi 제거의 근거는 https://peps.python.org/pep-0594/#cgi에 있음
-
최근에 350달러짜리 미니 서버에서 Golang 바이너리, RabbitMQ, Redis, MySQL을 모두 같은 장비에 올리고 테스트했는데, 초당 5,000요청을 지속 처리했음
하루 24시간으로 환산하면 4억 요청임
요즘 무료 도구들이 얼마나 훌륭한지 놀랍고, 그런데도 우리가 클라우드 제공자에게 그렇게 많은 돈을 내는 것도 놀라움
완전히 같은 비교는 아니지만, 지하실의 장비에서 전부 개발하고 튜닝하는 과정이 정말 좋았음- 서버가 초당 1요청보다 훨씬 많이 처리할 수 있다는 걸 모르는 채, 개발 속도를 10배 늦추는 Kubernetes 마이크로서비스 괴물을 배포하는 경우를 너무 많이 봄
“Google이 그렇게 하니까” 말고는 이유도 없이 이런 오버헤드를 계속 감수하는 게 이상함
우리가 잘 쓰고 있는 모듈식 모놀리스 아키텍처에 대한 글을 꼭 써야겠음 - 꼭 클라우드 제공자에게 돈을 낼 필요는 없음
호스팅 업체에서 전용 서버를 빌려 마음껏 쓰면 됨. 다만 대역폭이나 전송량 제한에 크게 묶임
클라우드가 쓰이는 건 이해관계가 많기 때문임. 예를 들어 VC와 투자자가 클라우드 회사 지분도 갖고 있고, 실제로는 오지 않을 상상의 트래픽 폭증 때문에 투자가 망할까 봐 두려워함
클라우드 영업은 투자자의 불안을 건드리는 데 능숙함 - 사이드 프로젝트를 지하실에서 호스팅할까 생각해 봤지만, 그러면 정전과 ISP 장애에 취약해짐
여행 중에 드라이브가 고장 나면 답이 없고, 실수로 잠가 버리면 fallback용 직렬 터미널도 없음
r/homelab 사람들처럼 구성할 수도 있지만, 그러면 특히 내 시간을 포함했을 때 실제로 돈을 아끼는지도 불분명해짐
결론적으로 클라우드 제공자는 규모의 경제 덕분에 꽤 좋은 거래라고 봄 - VM 비용을 보면 높은 이유는 많은 연산이 아니라 로컬 디스크가 많은 서버가 필요하기 때문임
이상하게도 하나만 따로 얻을 수가 없음
20TB 드라이브 4개와 적당한 CPU만 붙어 있으면 얼마나 큰 사업이나 서비스를 운영할 수 있을지 상상해 보라
클라우드 제공자에게 그런 구성을 얻기는 쉽지 않음
- 서버가 초당 1요청보다 훨씬 많이 처리할 수 있다는 걸 모르는 채, 개발 속도를 10배 늦추는 Kubernetes 마이크로서비스 괴물을 배포하는 경우를 너무 많이 봄
-
“초기 웹 커뮤니티는 이게 나쁜 생각이라는 걸 빨리 배웠고 PHP 같은 기술을 발명했다”는 부분은 엄밀히 말하면 핵심 기술은 mod_php였음
PHP 자체는 실행 방식에서 Perl과 다르지 않았지만, mod_perl과 비교한 mod_php의 설계 선택 덕분에 PHP 스크립트는 서버에 그냥 던져 넣어도 빠르게 실행됐고, mod_perl은 동작시키려면 약간의 생각과 마법이 필요했음- 그 시절 친구와 함께 나중에 학습 관리 시스템이라고 불릴 만한 걸 개발하고 있었음
콘텐츠 관리, 과제 업로드, 이벤트 캘린더, 성적 관리, 실시간 채팅, 포럼이 있었고 전부 CGI 위의 순수 C였는데 작업하기가 지옥 같았음
PHP를 알게 된 날 거의 눈물이 날 뻔했던 건, RFC를 읽거나 HTTP를 역공학하면서 처음부터 힘들게 짜던 모든 것이 PHP에서는 단순한 함수 호출이었다는 점임
더는 엉성한 urlencode 구현을 디버깅하거나 HTTP 헤더의 이상한 캐리지 리턴 하나 때문에 하루를 날리지 않아도 됐음 - 맞지만 mod_php는 PHP 생태계에 초기에 추가됐고 빠르게 기본 배포 방식이 됐음
Apache 모듈의 첫 버전은 1996년 PHP/FI Version 2.0용이었던 것으로 봄: https://www.php.net/manual/phpfi2.php#module
- 그 시절 친구와 함께 나중에 학습 관리 시스템이라고 불릴 만한 걸 개발하고 있었음
-
cgi-bin이 DB 접근을 해야 한다면 프로세스가 시작될 때마다 연결을 열어야 함
FastCGI처럼 코드를 메모리에 남겨 두는 건 시작 시간 비용을 피하려는 것만이 아니라, DB 연결 풀이나 적어도 스레드별 지속 연결을 가질 수 있기 때문임- 규모가 커지면 연결 수 때문에 데이터베이스가 힘들어함
“Python은 단일 스레드니까 많이 띄우자”와 “Python은 느리니까 많이 띄우자”를 같이 했을 때 그랬음
규모가 커지면 결국 pgbouncer 같은 Python 바깥의 공유 연결 풀과 많은 튜닝으로 부하를 처리하면서 DB를 죽이지 않게 만들어야 함
물론 그다음 어느 정도 성능이 나오는 다중 스레드 언어로 다시 구현하니 다시 아주 단순해졌음 - 그래서 CGI는 결국 요청 사이에 일부 상태를 유지하는 모델로 진화했음
- 이를 처리하는 표준적인 방식도 있었음
사실상 프록시 역할을 하는 별도 데몬을 띄우는 식임
TCP/IP 대신 Unix 소켓을 쓰면 연결 비용이 비교적 낮아짐 - UDP를 쓰면 됨
- 규모가 커지면 연결 수 때문에 데이터베이스가 힘들어함
-
이 하드웨어에서 hello world 앱이 초당 2,400요청이면 좀 안 좋은 것 아닌가
성능을 포기하고 정확히 무엇을 얻는 건지도 모르겠음. 코드가 더 단순해진 것도 아님- 초당 2,000요청을 넘겨야 할 때만 나쁜 수치임
그런 사이트는 전체 중 일부에 불과함 - 훌륭하진 않지만 많은 사용 사례에는 충분함
HN의 트래픽 폭탄도 버틸 수 있을 듯함
- 초당 2,000요청을 넘겨야 할 때만 나쁜 수치임
-
회사에서는 가끔 빠르게 만드는 내부 웹 앱용으로 아직도 cgi-bin 디렉터리를 서비스하고 있음
단순하게 유지하면 사용성이 좋음
CGI라고 해서 HTTP/1.0을 stdout에 직접 print해야 한다는 뜻은 아님
예를 들어 Python의 내장 wsgiref.handlers.CGIHandler를 쓰면 어떤 WSGI 앱이든 CGI 스크립트로 실행할 수 있음
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
우리는 스크립트를 uwsgi와 그 CGI 플러그인[1]으로 실행함
mod_cgi 때문에 Apache나 lighttpd를 띄우는 것보다 더 단순하고 유연하다고 느낌
uwsgi가 systemd 유닛으로 실행되므로 systemd의 강화와 샌드박싱 기능도 모두 활용할 수 있음
uwsgi의 CGI 처리에서 mod_cgi에 없는 편리한 점은 특정 파일 형식에 사용할 인터프리터를 지정할 수 있다는 것임
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
첫 바이트까지 시간은 250~350ms이고, 우리 용도에는 받아들일 만함
[1]: https://uwsgi-docs.readthedocs.io/en/latest/CGI.html- 좋은 정보이고, wsgiref.handlers.CGIHandler는 아직 폐기 예정이 아닌 듯함
-
최근 사이드 프로젝트에 Apache를 썼는데, 이유 중 하나가 .htaccess 기능이어서 비슷한 대화를 했음
.htaccess 파일을 어디든 놓으면 Apache가 매 요청마다 추가 서버 설정으로 읽어 들일 수 있음: https://httpd.apache.org/docs/2.4/howto/htaccess.html
이를 피하던 큰 이유는 성능이었음. 매 요청마다 추가 디스크 접근이 필요했고, 가능하면 메인 설정 파일에 넣는 편이 항상 더 나았음
하지만 지금은 대부분 서버에 SSD가 있고, Linux가 파일 시스템 캐시에 쓸 여유 RAM도 있을 가능성이 큼
물론 Apache가 설정을 한 번이 아니라 매 요청마다 파싱해야 하니 성능은 여전히 조금 나빠짐
그래도 요즘 대부분 서버 CPU가 더 강력하다는 점을 보면, 많은 사용 사례에서는 감수할 수 있음
사이드 프로젝트는 아주 초기 버전이지만 이미 사용 중임: https://github.com/StaticPatch/StaticPatch/tree/main- Rasmus Lerdorf의 말을 인용하면:
“나는 진짜 프로그래머가 아니다. 동작할 때까지 이것저것 붙여 보고 나면 넘어간다. 진짜 프로그래머들은 ‘그래, 동작은 하는데 여기저기 메모리가 새고 있네. 고쳐야 하지 않을까?’라고 할 것이다. 나는 Apache를 10요청마다 재시작하면 된다.”
PHP는 그 이후로 아주 멀리 왔지만, 그 큰 부분은 초기 실수를 바로잡는 과정이었음
“PHP 8이 훨씬 나은 이유는 내 코드가 훨씬 적게 들어 있기 때문이다.” - Apache가 왜 파일 시스템을 그냥 감시하지 않는지 이해가 안 됨
이 선택 때문에 HTTP 요청의 99.99%가 불필요한 디스크 읽기로 느려지게 됨
- Rasmus Lerdorf의 말을 인용하면:
-
빠른 프로토타이핑 워크플로의 일부로 이걸 더 많이 생각해 봤음
현대적인 JIT 언어들은 FastCGI 모델로 가지 않는 한 시작 시간이 대부분 import에 지배될 것 같음
로컬 스크립트에 h2o 웹 서버를 쓰기 시작하면서 이 생각이 들었는데, mruby와 FastCGI 핸들러로 설정 파일을 깔끔하고 빠르게 작성할 수 있고 속도도 매우 빠름: https://h2o.examp1e.net/configure/fastcgi_directives.html
또 유용한 곳은 고객이 로컬 소프트웨어를 자기 코드로 확장하게 하는 경우임
예를 들어 AI 도구를 확장하려고 MCP를 쓰게 하는 대신, 특정 요청 구조를 CGI로 구현하게 할 수 있음- 최종 사용자 환경에서 CGI 프로그램을 위한 MCP 프런트엔드는 나쁘지 않은 아이디어임
MCP 서비스 자체도 CGI로 구현할 수 있지 않을까 궁금해짐
MCP 프레임워크가 두 실행 모드를 모두 지원하는 프로그램 형태로 기능을 노출할 수도 있음
명세를 좀 파봐야겠음 - FastCGI는 CGI의 장점을 거의 다 잃는 편임
- 최종 사용자 환경에서 CGI 프로그램을 위한 MCP 프런트엔드는 나쁘지 않은 아이디어임
-
내게는
inetd가 곧 CGI였음
덕분에 인터넷이 훨씬 더 재미있어졌음
직접 inetd로 여러 셸 스크립트를 호스팅했고, 완전히 Bash로 작성한 HTTP 서버도 있었음
그 VPS는 오래전에 사라졌고, 당시에는 버전 관리도 쓰지 않았음
그걸 작성했던 노트북도 사라짐
그래도 정말 재미있었음
배포는 Makefile + scp만큼 쉬웠고, 테스트는netcat여러 개와 grep을 섞은 또 다른 Bash 스크립트였음
참 좋은 시절이었음