회고록(WebServ)

Myukang·2023년 9월 10일
0

시작하며

레포지토리 주소

23-1학기, 웹서버를 C++로 만들어봤습니다.
더 기억이 안나기 전에 프로젝트를 어떻게 진행하고자 했고, 어떤 문제점이 있었는지 정리하고하자합니다.

비동기-논블로킹으로 동작하는 웹서버이며, 이벤트 알림 메커니즘 중 하나인 kqueue를 사용했습니다.


kqueue

논블로킹 서버를 만들때, 커널 알림 메커니즘을 사용해 event기반 아키텍처를 구성해야합니다.

커널 알림 메커니즘에는 epoll, select, iocp 그리고 kqueue 등 다양한 인터페이스가 운영체제별로 다르게 지원됩니다.
libuv

NodeJS가 비동기-논블로킹 이벤트루프를 가지고 있는데, NodeJS의 libuv의 이벤트루프는 위의 인터페이스들을 운영체제별로 다르게 사용합니다.

NodeJS에서 async-await으로 호출하는, network관련 작업/dns작업/파일 시스템 작업 등은 이 libuv모듈에서 처리하게됩니다.(file i/o작업은 libuv의 스레드풀에서 실행됩니다.)

저는 웹서버를 만들기 전에 이런 선행지식을 가지고 시작했습니다.


스레드풀을 구현할까?

결론적으로, 스레드풀은 개념만 공부하고 프로젝트에 반영하진 않았습니다.
처음에는 스레드풀을 사용하면 막연히 성능이 향상될거라고만 생각했지만, nginx의 이 글을 보고 생각이 바뀌었습니다.

nginx thread pool

대부분의 read/write작업은 hdd에 직접 작업을 하는 것이 아닌, page cache에서 처리하며, 이는 매우 빠릅니다. RAM이 충분히 큰 상황에서는 스레드풀을 사용하지 않고도 충분히 빠른 방식으로 작동하게 됩니다.

Read작업을 스레드풀로 offload하는 것은 대용량 파일, 즉 스트리밍 서버의 경우에 적합합니다.

스레드풀을 구현하는 것은 상당히 까다로운 작업이기에 프로젝트 일정에 지연이 생길 것이라고 생각해 따로 구현하지 않고 공부하는 것에 만족했습니다.


작업분할, 객체지향

작업은 크게 3가지로 분할했습니다.

  1. http request parser담당
  2. http response 담당
  3. config file/event loop 담당

event loop쪽에서 http request/response 모듈을 가지고 인터페이스의 메서드만을 호출해 구현체에 의존하지 않는 lsp 원칙을 적용했습니다.

이에따라, 이벤트루프에서는 request/response 모듈의 실제 구현에 상관없이 이벤트루프를 작동할 수 있었습니다.

SOLID원칙이 단순하게 객체지향 코드를 작성하는게 목적이 아닌, 협업 속에서 분업을 명확하게 할 수 있는 원칙이라는 것을 깨달을 수 있었던 것 같습니다.


이벤트 관리하기

nginx의 소스코드를 보면 모든 event가 하나의 rb_tree 구조에 저장되는 것을 볼 수 있습니다.

nginx의 rbtree

처음에 nginx 소스코드를 보면서 구현할때 이벤트들을 rb_tree에 담아서 진행하려고 했습니다만, kqueue의 특징 중 하나가 udata 포인터에 이벤트가 발생했을때 사용할 특정 데이터를 받아올 수 있다는 것을 알고 별도의 자료구조에 담는 것이 아닌,
udata에 이벤트 발생했을때 처리할 객체의 주소를 담고, 이벤트가 발생하면 해당 이벤트 객체를 가져와서 처리하려고 했습니다.
이에따라, rb_tree에서 fd값을 key값으로 찾는 시간을 줄일 수 있었습니다.


CPU사용량 문제

100mb짜리 요청을 10명의 클라이언트가 1000번 보내는 부하테스트를 진행했습니다.

m1 mac에서는 문제없이 구동되었으나, 특정 하드웨어 스펙에서 CPU사용량이 100%를 넘어 180%까지 가다가 컴퓨터 자체가 뻗어버리는 경우가 발생했습니다.

문제를 추측하기로, event의 최대 개수를 정하지 않고, 1000 * 10개 = 10000개의 이벤트를 모두 커널에 non-blocking 작업을 위임하는것이 CPU에 너무 많은 부하를 준다는 것이었습니다.

최대 커넥션 개수를 정하지 않았기때문에 모든 클라이언트가 서버로부터 응답을 받기를 대기하고 있고, 서버측에서는 모든 클라이언트에게 응답하기 위해 커널 작업을 하기때문이었습니다.

저 당시에는 blocking write를 통해 커널 작업 감지를 지연해 딜레이를 주어 저 부하테스트를 통과했습니다.

지금와서 생각해보면, 최대 클라이언트 개수를 정하고, 그 개수를 넘어가면 queue에 넣고 처리하는 방법이 좋았을 것 같습니다.

profile
안녕하세요! 반갑습니다

0개의 댓글