다중 UNIX 네트워크 연결 관리
요약: 현대식 서버 애플리케이션을 빌드하려면 수백, 수천, 심지어는 수만 개의 이벤트를 동시에 수용할 방법이 필요하며, 이때 이벤트들이 내부 요청이든 운영 문제를 효과적으로 처리하는 네트워크 연결이든 상관없습니다. 사용 가능한 솔루션이 많이 있지만, libevent 라이브러리와 libev 라이브러리는 둘 다 성능과 이벤트 처리 기능 면에서 대변혁을 일으켰습니다. 본 기사에서는 UNIX® 애플리케이션 내에서 이런 솔루션을 사용하고 배치하기 위해 적용할 수 있는 기본적인 구조체와 메소드를 살펴볼 것입니다. libev와 libevent는 둘 다 많은 수의 동시 클라이언트 또는 연산을 지원할 필요가 있는 IBM Cloud 또는 Amazon EC2 환경 내에 배치된 애플리케이션을 포함한 고성능 애플리케이션에서 사용될 수 있습니다.
수많은 서버 배치 작업(특히, 웹 서버 배치 작업)에서 직면하는 최대의 문제점 중 하나는 많은 수의 연결을 처리할 수 있는 능력이 있느냐 하는 문제이다. 네트워크 트래픽을 처리하기 위한 클라우드 기반 서비스를 빌드하든, IBM Amazon EC 인스턴스에 애플리케이션을 분배하든, 웹 사이트용 고성능 컴포넌트를 제공하든 상관없이, 많은 수의 동시 연결을 처리할 수 있어야 한다.
최근에 이루어지고 있는 더욱 동적인 웹 애플리케이션, 특히 AJAX 기술을 사용하는 웹 애플리케이션으로의 움직임이 좋은 사례이다. 어떤 이벤트 또는 문제에 대한 라이브 모니터링 기능을 제공하는 시스템과 같이, 수천 개의 클라이언트가 한 웹 페이지 내에서 직접 정보를 업데이트하도록 허용하는 시스템을 배치하고 있는 경우에는 정보를 효과적으로 제공할 수 있는 속도가 매우 중요하다. 그리드 또는 클라우드 환경에서는 수천 개의 클라이언트로부터 동시에 계속 개방된 연결이 있을 수 있고, 각 클라이언트에 요청 및 응답 서비스를 제공할 수 있어야 한다.
libevent와 libev가 어떻게 다중 네트워크 연결을 처리할 수 있는지 살펴보기 전에, 이런 유형의 연결을 처리하기 위한 전통적인 솔루션 몇 가지를 간략히 살펴보자.
다중 연결을 처리하는 기존의 방법은 수도 없이 많을 정도이지만, 보통 그런 방법을 사용하면 메모리 또는 CPU를 너무 많이 사용하거나 어떤 형태의 운영 체제 한계에 이르기 때문에 많은 수의 연결을 처리하는 문제가 발생한다.
사용되는 기본 솔루션은 다음과 같다.
- 라운드 로빈: 초창기 시스템에서는 개방된 네트워크 연결 목록을 단순히 반복하면서 읽어야 할 데이터가 있는지 결정하는 라운드 로빈 선택이라는 간단한 솔루션을 사용한다. 이 솔루션은 느린데다가(특히, 연결 수가 증가할 때) 비효율적이다(현재 연결을 사용하는 동안 다른 연결에서 요청을 보내고 응답을 기다리고 있을 수 있으므로). 각 연결을 반복 수행하는 동안 다른 연결은 기다려야 한다. 100개의 연결이 있는데 한 연결에만 데이터가 있더라도, 서비스가 필요한 그 한 연결에 이르기 위해 다른 99개의 연결을 반복하여 작업해야 한다.
- poll, epoll 및 variation: 이 솔루션에서는 네트워크 소켓에서 데이터가 식별될 때 처리 함수가 호출되도록 콜백 메커니즘으로 모니터할 각 연결의 배열을 유지하는 구조체를 이용하는 라운드 로빈 접근 방식을 수정한 방식을 사용한다. Poll의 문제점은 구조체의 크기가 꽤 클 수 있다는 점이고, 목록에 새 네트워크 연결을 추가할 때 구조체를 수정하면 로드가 증가하여 성능에 영향을 줄 수 있다.
- select:
select()
함수 호출에서는 정적 구조체를 사용하며, 이 구조체는 이전에 비교적 적은 수(1,024개의 연결)로 하드코드되었기 때문에 매우 큰 배치에는 비실용적이다.
선택한 OS에서 더 나은 성능을 발휘할 수 있는 개별 플랫폼에서 다른 구현 방법이 있지만(예: Solaris에 /dev/poll 구현 또는 FreeBSD/NetBSD에 kqueue 구현), 이런 구현은 이식 불가능하고 반드시 요청 처리의 상위 레벨 문제를 해결하는 것도 아니다.
위에서 언급한 솔루션들은 모두 요청을 기다렸다가 처리한 후, 실제 네트워크 상호 작용을 처리하기 위한 별개의 함수로 그 요청을 발송하는 단순한 루프를 사용한다. 여기서 핵심은 서로 다른 연결과 인터페이스를 수신, 업데이트 및 제어하려면 루프와 네트워크 소켓에 많은 관리 코드가 필요하다는 점이다.
서로 다른 많은 연결을 처리하는 다른 방법은 대부분의 현대식 커널에서 멀티스레딩 지원을 이용해 연결을 수신하여 처리함으로써 각 연결마다 새 스레드를 여는 것이다. 이 방법에서는 연결 처리의 책임이 다시 운영 체제로 직접 전환되지만, 각각의 스레드가 고유한 실행 공간을 필요로 할 것이므로 RAM과 CPU의 측면에서 비교적 큰 오버헤드가 발생할 것임을 알 수 있다. 각 스레드(따라서 네트워크 연결)가 사용 중인 경우에는 각 스레드로의 컨텍스트 전환이 중요해질 수 있다. 마지막으로, 많은 커널들이 그처럼 많은 수의 활성 스레드를 처리하도록 디자인되지 않았다.
libevent 라이브러리가 실제로는 select()
, poll()
또는 다른 메커니즘의 기초를 대체하지는 못한다. 그 대신, libevent 라이브러리는 각 플랫폼에서 가장 효율적이고 고성능의 솔루션을 이용한 구현과 관련된 랩퍼를 제공한다.
각각의 요청을 실제로 처리하기 위해, libevent 라이브러리는 기본 네트워크 백엔드 주위에서 랩퍼 역할을 하는 이벤트 메커니즘을 제공한다. 이런 이벤트 시스템을 사용하면 기본 I/O 복잡성을 간소화하는 한편, 연결을 위한 핸들러를 매우 쉽고 간단하게 추가할 수 있다. 이것이 libevent 시스템의 핵심이다.
libevent 라이브러리의 추가 컴포넌트는 버퍼링된 이벤트 시스템(클라이언트와 주고받는 버퍼 데이터용)과 HTTP, DNS 및 RPC 시스템을 위한 코어 구현을 포함한 다양한 기능을 추가한다.
libevent 서버를 작성하는 기본적인 방법은 클라이언트에서의 연결을 승인하는 것과 같은 특정한 작업이 발생할 때 실행할 함수를 등록한 다음, 기본 이벤트인 loop event_dispatch()
를 호출하는 것이다. 이제는 libevent 시스템에서 실행 프로세스의 제어를 처리한다. 이벤트와 이벤트를 호출할 함수를 등록한 후에는 이벤트 시스템이 자율적으로 운영되고, 애플리케이션 실행 중에 이벤트 큐에 이벤트를 추가(등록)하거나 이벤트 큐에서 이벤트를 제거(등록 취소)할 수 있다. 새로 개방된 연결을 처리하기 위해 새 이벤트를 추가할 수 있으므로, 유연한 네트워크 처리 시스템을 빌드할 수 있는 것은 바로 이런 이벤트 등록의 자유로움 덕분이다.
예를 들어, 새 연결을 열기 위해 accept()
함수를 호출해야 할 때마다 수신 소켓을 연 다음 콜백 함수를 등록하여 네트워크 서버를 작성할 수 있다. 이에 대한 기본 사항을 정리한 내용이 목록 1에 표시되어 있다.
목록 1. 새 연결을 열기 위해 accept()
함수를 호출해야 할 때마다 수신 소켓을 열고 콜백 함수를 등록하여 네트워크 서버 작성
int main(int argc, char **argv) { ... ev_init(); /* Setup listening socket */ event_set(&ev_accept, listen_fd, EV_READ|EV_PERSIST, on_accept, NULL); event_add(&ev_accept, NULL); /* Start the event loop. */ event_dispatch(); } |
event_set()
함수는 새 이벤트 구조체를 작성하는 반면, event_add(
)는 이벤트 큐 메커니즘에 이벤트를 추가한다. 이번에는 event_dispatch()
가 이벤트 큐 시스템을 시작하고 요청을 수신하여 승인하기 시작한다.
목록 2에 더 완전한 예제가 주어져 있으며, 여기서는 매우 간단한 에코 서버를 빌드한다.
#include <event.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #define SERVER_PORT 8080 int debug = 0; struct client { int fd; struct bufferevent *buf_ev; }; int setnonblock(int fd) { int flags; flags = fcntl(fd, F_GETFL); flags |= O_NONBLOCK; fcntl(fd, F_SETFL, flags); } void buf_read_callback(struct bufferevent *incoming, void *arg) { struct evbuffer *evreturn; char *req; req = evbuffer_readline(incoming->input); if (req == NULL) return; evreturn = evbuffer_new(); evbuffer_add_printf(evreturn,"You said %s\n",req); bufferevent_write_buffer(incoming,evreturn); evbuffer_free(evreturn); free(req); } void buf_write_callback(struct bufferevent *bev, void *arg) { } void buf_error_callback(struct bufferevent *bev, short what, void *arg) { struct client *client = (struct client *)arg; bufferevent_free(client->buf_ev); close(client->fd); free(client); } void accept_callback(int fd, short ev, void *arg) { int client_fd; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); struct client *client; client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { warn("Client: accept() failed"); return; } setnonblock(client_fd); client = calloc(1, sizeof(*client)); if (client == NULL) err(1, "malloc failed"); client->fd = client_fd; client->buf_ev = bufferevent_new(client_fd, buf_read_callback, buf_write_callback, buf_error_callback, client); bufferevent_enable(client->buf_ev, EV_READ); } int main(int argc, char **argv) { int socketlisten; struct sockaddr_in addresslisten; struct event accept_event; int reuse = 1; event_init(); socketlisten = socket(AF_INET, SOCK_STREAM, 0); if (socketlisten < 0) { fprintf(stderr,"Failed to create listen socket"); return 1; } memset(&addresslisten, 0, sizeof(addresslisten)); addresslisten.sin_family = AF_INET; addresslisten.sin_addr.s_addr = INADDR_ANY; addresslisten.sin_port = htons(SERVER_PORT); if (bind(socketlisten, (struct sockaddr *)&addresslisten, sizeof(addresslisten)) < 0) { fprintf(stderr,"Failed to bind"); return 1; } if (listen(socketlisten, 5) < 0) { fprintf(stderr,"Failed to listen to socket"); return 1; } setsockopt(socketlisten, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); setnonblock(socketlisten); event_set(&accept_event, socketlisten, EV_READ|EV_PERSIST, accept_callback, NULL); event_add(&accept_event, NULL); event_dispatch(); close(socketlisten); return 0; } |
아래에서는 다른 함수와 연산에 대해 논한다.
main()
: main 함수는 연결 수신에 사용할 소켓을 작성한 다음, 이벤트 핸들러를 통해 각각의 연결을 처리하기 위해accept()
에 대한 콜백을 작성한다.accept_callback()
: 연결이 승인될 때 이벤트 시스템에서 호출되는 함수이다. 이 함수는 클라이언트에 대한 연결을 승인하고, 클라이언트 소켓 정보 및 버퍼 이벤트 구조체를 추가하고, 클라이언트 소켓의 읽기/쓰기/오류 이벤트에 대한 콜백을 이벤트 구조체에 추가하고, (임베디드 이벤트 버퍼 및 클라이언트 소켓이 있는) 클라이언트 구조체를 인수로 전달한다. 해당 클라이언트 소켓에 읽기, 쓰기 또는 오류 연산이 있을 때마다, 해당 콜백 함수가 호출된다.buf_read_callback()
: 클라이언트 소켓에 읽을 데이터가 있을 때 호출되는 함수이다. 이 함수는 에코 서비스로서 클라이언트에 다시 "you said..."를 쓴다. 이 소켓은 새 요청을 승인하기 위해 열린 상태로 유지된다.buf_write_callback()
: 쓸 데이터가 있을 때 호출되는 함수이다. 이런 간단한 서비스는 필요하지 않으므로, 정의가 비어 있다.buf_error_callback()
: 오류 조건이 존재할 때 호출되는 함수이다. 이 함수에는 클라이언트가 연결을 끊는 시점이 포함된다. 클라이언트 소켓이 닫히는 모든 상황에서, 클라이언트 소켓용 이벤트 항목이 이벤트 목록에서 제거된다. 클라이언트 구조체용 메모리를 사용할 수 있게 된다.setnonblock()
: 네트워크 소켓을 비블로킹 I/O로 설정한다.
클라이언트 연결 수가 늘어나면 클라이언트 연결을 처리하기 위한 새 이벤트가 이벤트 큐에 추가되고, 클라이언트 연결이 끊길 때 제거된다. 막후에서는 libevent가 네트워크 소켓을 처리하고, 어떤 클라이언트에서 서비스를 받아야 할지 식별하고, 각각의 경우에 해당 함수를 호출하고 있다.
애플리케이션을 빌드하려면 libevent 라이브러리를 추가하는 C 소스 코드 $ gcc -o basic basic.c -levent
를 컴파일한다.
클라이언트 관점에서, 서버는 서버로 전송되는 모든 텍스트를 그냥 다시 에코한다(아래의 목록 3 참조).
$ telnet localhost 8080 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Hello! You said Hello! |
이와 같은 네트워크 애플리케이션은 다중 연결을 사용할 필요가 있는 IBM Cloud 시스템과 같이 대규모로 분배된 배치 환경에서 유용할 수 있다.
이처럼 간단한 솔루션으로는 성능상의 이점과 많은 수의 동시 연결을 확인하기 어렵다. 임베디드 HTTP 구현을 사용하면 대량 확장성의 이해에 도움이 될 수 있다.
일반 네트워크 기반 libevent 인터페이스는 원시 애플리케이션을 빌드하려는 경우에 유용하지만, 정보를 로드하거나 더 공통적으로는 정보를 동적으로 다시 로드하는 웹 페이지 및 HTTP 프로토콜을 기반으로 애플리케이션을 개발하는 것이 점점 더 일반화되고 있다. AJAX 라이브러리를 사용 중인 경우, 리턴하는 정보가 XML 또는 JSON이더라도 다른 쪽 끝에서 HTTP를 예상한다.
libevent 내부의 HTTP 구현이 Apache의 HTTP 서버를 대체하지는 않겠지만, 클라우드 및 웹 환경 모두와 연관된 대규모 동적 컨텐츠에 실용적인 솔루션이 될 수 있다. 예를 들어, IBM Cloud 관리 또는 기타 솔루션에 libevent 기반 인터페이스를 배치할 수 있다. HTTP를 사용하여 통신할 수 있으므로, 서버가 다른 컴포넌트와 통합할 수 있다.
libevent 서비스를 사용하려면 기본 네트워크 이벤트 모델에 대해 이미 설명한 것과 같은 기본 구조체를 사용하지만, 네트워크 인터페이스를 처리하지 않아도 HTTP 랩퍼가 이를 자동으로 처리해준다. 따라서 전체 프로세스가 네 가지 함수 호출(초기화, HTTP 서버 시작, HTTP 콜백 함수 설정 및 이벤트 루프 진입)에다가 데이터를 되돌려 보내는 콜백 함수의 내용으로 전환된다. 목록 4에서 매우 간단한 예제를 제시한다.
목록 4. libevent 서비스를 이용한 간단한 예제
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <event.h> #include <evhttp.h> void generic_request_handler(struct evhttp_request *req, void *arg) { struct evbuffer *returnbuffer = evbuffer_new(); evbuffer_add_printf(returnbuffer, "Thanks for the request!"); evhttp_send_reply(req, HTTP_OK, "Client", returnbuffer); evbuffer_free(returnbuffer); return; } int main(int argc, char **argv) { short http_port = 8081; char *http_addr = "192.168.0.22"; struct evhttp *http_server = NULL; event_init(); http_server = evhttp_start(http_addr, http_port); evhttp_set_gencb(http_server, generic_request_handler, NULL); fprintf(stderr, "Server started on port %d\n", http_port); event_dispatch(); return(0); } |
이전 예제와 비교해 볼 때, 이 예제 코드의 기본적인 내용은 상대적으로 자명하다. 이 코드의 기본 요소는 HTTP 요청을 수신할 때 사용할 콜백 함수를 설정하는 evhttp_set_gencb()
함수와 응답 버퍼를 간단한 성공 메시지로 채우는 generic_request_handler()
콜백 함수 자체이다.
HTTP 랩퍼는 여러 가지 다양한 기능을 제공한다. 예를 들어, (CGI 요청에서 사용하는 것과 같은) 일반적인 요청에서 쿼리 인수를 추출하게 될 요청 구문 분석기가 있고, 요청되는 다른 경로 내에서 트리거되도록 다른 핸들러를 설정할 수도 있다. 적절히 다른 콜백 및 처리 함수와 함께, '/db/' 경로 또는 '/memc'로서 memcached를 통한 인터페이스를 사용하여 데이터베이스에 대한 인터페이스를 제공할 수 있다.
libevent 툴킷의 다른 요소 하나는 일반 타이머에 대한 지원이다. 이런 지원을 이용해 특정 주기 이후에 이벤트를 예약할 수 있다. HTTP 구현과 이 요소를 결합하면 파일 내용 수정 시 리턴되는 데이터를 업데이트하도록 파일 내용을 준비하기 위한 경량 서비스를 제공할 수 있다. 예를 들어, 뉴스 이벤트가 쉴 새 없이 쏟아지는 중에 프론트엔드 웹 애플리케이션이 주기적으로 계속 뉴스 항목을 다시 로드하는 라이브 업데이트 서비스를 제공하고 있다면, 뉴스 컨텐츠를 손쉽게 준비할 수 있을 것이다. 전체 애플리케이션과 웹 서비스가 메모리에 있으므로 응답 시간이 매우 빨라진다.
이것이 목록 5의 예제 이면에 숨어 있는 주 목적이다.
목록 5. 일반 타이머를 사용하여 뉴스 이벤트가 쉴 새 없이 쏟아지는 중에 라이브 업데이트 서비스 제공
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/stat.h> #include <event.h> #include <evhttp.h> #define RELOAD_TIMEOUT 5 #define DEFAULT_FILE "sample.html" char *filedata; time_t lasttime = 0; char filename[80]; int counter = 0; void read_file() { int size = 0; char *data; struct stat buf; stat(filename,&buf); if (buf.st_mtime > lasttime) { if (counter++) fprintf(stderr,"Reloading file: %s",filename); else fprintf(stderr,"Loading file: %s",filename); FILE *f = fopen(filename, "rb"); if (f == NULL) { fprintf(stderr,"Couldn't open file\n"); exit(1); } fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET); data = (char *)malloc(size+1); fread(data, sizeof(char), size, f); filedata = (char *)malloc(size+1); strcpy(filedata,data); fclose(f); fprintf(stderr," (%d bytes)\n",size); lasttime = buf.st_mtime; } } void load_file() { struct event *loadfile_event; struct timeval tv; read_file(); tv.tv_sec = RELOAD_TIMEOUT; tv.tv_usec = 0; loadfile_event = malloc(sizeof(struct event)); evtimer_set(loadfile_event, load_file, loadfile_event); evtimer_add(loadfile_event, &tv); } void generic_request_handler(struct evhttp_request *req, void *arg) { struct evbuffer *evb = evbuffer_new(); evbuffer_add_printf(evb, "%s",filedata); evhttp_send_reply(req, HTTP_OK, "Client", evb); evbuffer_free(evb); } int main(int argc, char *argv[]) { short http_port = 8081; char *http_addr = "192.168.0.22"; struct evhttp *http_server = NULL; if (argc > 1) { strcpy(filename,argv[1]); printf("Using %s\n",filename); } else { strcpy(filename,DEFAULT_FILE); } event_init(); load_file(); http_server = evhttp_start(http_addr, http_port); evhttp_set_gencb(http_server, generic_request_handler, NULL); fprintf(stderr, "Server started on port %d\n", http_port); event_dispatch(); } |
서버의 기본 작동 원리는 이전 예제와 동일하다. 우선, 이 스크립트는 기본 URL 호스트/포트 조합(요청 URI를 처리 안 함)에 대한 요청에 바로 응답할 HTTP 서버를 설정한다. 파일을 로드하는 것이 첫 단계이다(read_file()
). 이와 동일한 함수가 원본 로드에 사용되고 타이머 이벤트에 의한 콜백 중에도 사용될 것이다.
read_file()
함수에서는 stat()
함수 호출을 사용하여 파일 수정 시간을 검사하는데, 파일이 마지막으로 로드된 이후로 파일이 변경된 경우에만 파일 내용을 다시 읽는다. 이 함수는 데이터를 별개의 구조체로 복사하는 fread()
에 대한 단일 호출을 사용하여 파일 데이터를 로드한 후, strcpy()
를 사용하여 로드된 문자열에서 글로벌 문자열로 데이터를 이동한다.
load_file()
함수는 타이머가 트리거될 때 함수 역할을 하게 된다. 이 함수는 read_file()
을 호출하여 파일 내용을 로드하고, 파일 로드 시도가 이루어지기 전에 RELOAD_TIMEOUT 값을 초의 값으로 사용하여 타이머를 설정한다. libevent 타이머는 초와 마이크로초 단위로 모두 타이머를 지정할 수 있게 해주는 timeval 구조체를 사용한다. 타이머는 연속적이지 않다. 타이머 이벤트가 트리거될 때 타이머를 설정한 다음, 이벤트 큐에서 이벤트가 제거된다.
컴파일하려면 이전 예제와 같은 형식인 $ gcc -o basichttpfile basichttpfile.c -levent
를 사용한다.
이제 데이터로 사용할 정적 파일을 작성한다. 기본 파일은 sample.html이지만, 어떤 파일이든 명령행의 첫 인수로 지정할 수 있다(아래의 목록 6 참조).
$ ./basichttpfile Loading file: sample.html (8046 bytes) Server started on port 8081 |
현재, 이 프로그램은 요청을 수락할 준비가 되어 있지만, 다시 로드하기 위한 타이머도 작동 중이다. sample.html의 내용을 변경하면 로그에 기록된 메시지로 파일을 자동으로 다시 로드해야 한다. 예를 들어, 목록 7의 출력 결과에 최초 로드와 두 번 다시 로드한 데이터가 표시된다.
목록 7. 최초 로드 및 두 번 다시 로드한 내용을 보여주는 출력 결과
$ ./basichttpfile Loading file: sample.html (8046 bytes) Server started on port 8081 Reloading file: sample.html (8047 bytes) Reloading file: sample.html (8048 bytes) |
모든 이점을 누리려면 사용자 환경에서 열린 파일 디스크립터 수에 대한 ulimit가 없도록 해야 한다. ulimit 명령을 이용해 (적절한 사용 권한 또는 루트 액세스 권한으로) 이것을 변경할 수 있다. 정확한 설정은 사용하는 OS에 따라 다르겠지만, Linux®에서는 -n
옵션으로 열린 파일 디스크립터 수와 네트워크 소켓 수를 설정할 수 있다.
목록 8. -n
옵션을 사용하여 열린 파일 디스크립터 수 설정
$ ulimit -n 1024 |
한계를 늘리려면 숫자를 $ ulimit -n 20000
과 같이 지정한다.
서버의 성능을 확인하려면 Apache Bench 2(ab2)와 같은 벤치마킹 애플리케이션을 사용하면 된다. 총 요청 수뿐 아니라, 동시 쿼리 수도 지정할 수 있다. 예를 들어, 100,000개의 요청을 사용하여 벤치마크를 실행한다면, $ ab2 -n 100000 -c 1000 http://192.168.0.22:8081/
과 같이 그 중에서 1,000개를 동시 쿼리로 지정할 수 있다.
필자는 이 샘플 시스템을 실행하면서 서버 샘플에 표시된 8K 파일을 사용하여 초당 거의 11,000개의 요청 수를 기록했다. libevent 서버는 단일 스레드에서 실행 중이고, 요청 열기 방법으로도 제한될 것이므로 단일 클라이언트가 서버에 무리를 주지는 않을 것이라는 점을 유념하자. 비록 그렇다 하더라도, 그 정도 속도는 교환 중인 문서의 상대적 크기를 고려할 때 단일 스레드 애플리케이션으로는 인상적인 수준이다.
수많은 시스템 애플리케이션에 C가 실용적인 언어이긴 하지만, 스크립팅 언어가 더욱 유연하고 실용적으로 사용될 수 있는 현대적 환경에서는 C가 종종 사용되지 않는다. 다행히도, Perl 및 PHP와 같은 대부분의 스크립팅 언어는 기본적으로 C로 작성되므로, 확장 모듈을 통해 libevent와 같은 C 라이브러리를 사용할 수 있다.
예를 들어, 목록 9에서는 Perl 네트워크 서버용 스크립트의 기본 구조를 보여준다. accept_callback()
함수는 목록 1의 코어 libevent 예제에서 accept 함수와 동일할 것이다.
목록 9. Perl 네트워크 서버용 스크립트의 기본 구조
my $server = IO::Socket::INET->new( LocalAddr => 'localhost', LocalPort => 8081, Proto => 'tcp', ReuseAddr => SO_REUSEADDR, Listen => 1, Blocking => 0, ) or die $@; my $accept = event_new($server, EV_READ|EV_PERSIST, \&accept_callback); $main->add; event_mainloop(); |
이런 언어로 작성되는 라이브러리 구현에서는 libevent 시스템의 코어를 지원하는 경향이 있고, HTTP 랩퍼를 항상 지원하지는 않는다. 따라서 스크립트로 작성된 애플리케이션을 이용한 이런 솔루션을 사용하면 더 복잡해진다. C 기반 libevent 애플리케이션에 언어를 임베드하거나 스크립트 언어 환경을 기반으로 한 수많은 HTTP 구현 중 하나를 사용하는 두 가지 경로가 있다. 예를 들어, Python에는 매우 뛰어난 기능을 가진 HTTP 서버 클래스(httplib/httplib2)가 있다.
이런 기능이 있음에도, C로 다시 구현할 수 없는 스크립팅 언어에는 아무 것도 없다는 점을 지적해야 할 것이다. 하지만, 시간을 고려해야 하므로 기존 코드베이스와의 통합이 더 중요해질 수 있다.
libevent와 마찬가지로, libev 시스템은 이벤트 기반 루프를 제공하기 위해 poll()
, select()
등의 기본 구현을 바탕으로 하는 이벤트 루프 기반 시스템이다. 필자가 본 기사를 작성하는 시점에서는 libev 구현의 오버헤드가 낮아 벤치마크 성능이 더 높았다. libev API가 더 원시적 형태로서, HTTP 랩퍼는 없지만 구현에 내장된 더 많은 유형의 이벤트를 지원한다. 예를 들어, 목록 4의 HTTP 파일 솔루션에서 사용할 수 있었던 여러 파일에 대한 속성 변화를 모니터하는 데 사용할 수 있는 evstat 구현이 있다.
하지만, 기본적인 사항은 동일하다. 즉, 필요한 네트워크 수신 소켓을 작성하고, 실행 중에 호출할 이벤트를 등록한 다음, 프로세스의 나머지 부분을 처리하는 libev를 포함한 기본 이벤트 루프를 시작하는 것은 똑같다.
예를 들어, Ruby 인터페이스를 사용하면 목록 10에 표시된 첫 번째 코드 목록에 있는 것과 유사한 에코 서버를 제공할 수 있다.
목록 10. Ruby 인터페이스를 사용하여 에코 서버 제공
require 'rubygems' require 'rev' PORT = 8081 class EchoServerConnection < Rev::TCPSocket def on_read(data) write 'You said: ' + data end end server = Rev::TCPServer.new('192.168.0.22', PORT, EchoServerConnection) server.attach(Rev::Loop.default) puts "Listening on localhost:#{PORT}" Rev::Loop.default.run |
Ruby 구현은 HTTP 클라이언트, OpenSSL 및 DNS를 포함한 수많은 공통 네트워크 솔루션을 위해 랩퍼가 제공되었으므로 특히 훌륭하다. 다른 스크립트 언어로는 포괄적인 Perl 및 Python 구현이 포함되며, 아마 사용해보고 싶을지 모르겠다.
libevent와 libev는 모두 서버 쪽 또는 클라이언트 쪽 요청에 응답하기 위해 대용량 네트워크와 기타 I/O를 지원하는 유연하면서도 강력한 환경을 제공한다. (CPU/RAM 사용률이 낮은) 효율적인 형식으로 수천, 수만의 연결을 지원하는 것이 목표다. 본 기사에서는 IBM Cloud, EC2 또는 AJAX 기반 웹 애플리케이션 지원에 사용 가능한 libevent의 내장 HTTP 서비스를 포함하여, 이에 대한 여러 가지 예제를 살펴보았다.
교육
- C10K problem에서는 10,000개의 연결을 처리하는 문제를 훌륭하게 설명한 기사를 읽을 수 있다.
- IBM Cloud Computing 웹 사이트에서는 다른 클라우드 구현에 관한 정보를 제공한다.
- System Administration Toolkit: Standardizing your UNIX command-line tools(Martin Brown, developerWorks, 2006년 5월) 읽기: 여러 시스템에서 같은 명령을 사용하는 방법을 학습할 수 있다.
- bash에서 프로그램하는 방법을 배울 수 있는 시리즈 기사인 Bash by example, Part 1: Fundamental programming in the Bourne again shell(bash)(Daniel Robbins, developerWorks, 2000년 3월), Bash by example, Part 2: More bash programming fundamentals(Daniel Robbins, developerWorks, 2000년 4월) 및 Bash by example, Part 3: Exploring the ebuild system(Daniel Robbins, developerWorks, 2000년 5월)을 확인하자.
- Making UNIX and Linux work together(Martin Brown, developerWorks, 2006년 4월): 일반적인 UNIX 배포 버전과 Linux를 함께 어울리게 만드는 방법을 알려주는 안내서이다.
- 최적의 클라우드 컴퓨팅 플랫폼 찾기(Brett McLaughlin, developerWorks, 2009년 3월): 특정 애플리케이션 요구사항에 맞는 최상의 클라우드 컴퓨팅 플랫폼과 관련하여 현명한 결정을 하는 데 도움이 된다.
- Amazon Web Services를 사용한 클라우드 컴퓨팅(Prabhakar Chaganti, developerWorks, 2008년 7월): Amazon Web Services를 사용하기 위한 단계별 가이드를 제시한다.
- developerWorks Cloud Computing Resource Center에서 Amazon EC2 플랫폼에 적합한 IBM 제품을 찾을 수 있다.
- AIX and UNIX 입문: AIX와 UNIX 입문 페이지에서 자세한 정보를 볼 수 있다.
- developerWorks AIX 및 UNIX 영역에는 수백 건의 유익한 기사와 초급, 중간급, 고급 사용자를 대상으로 하는 튜토리얼이 있다.
- developerWorks 팟캐스트에서 소프트웨어 개발자의 흥미로운 인터뷰와 토론을 확인할 수 있다.
- developerWorks technical events and webcasts: developerWorks 기술 행사 및 웹 캐스트를 통해 최신 정보를 얻을 수 있다.
제품 및 기술 얻기
- 다운로드 및 문서를 포함한 libev 라이브러리를 구할 수 있다.
- libevent 라이브러리를 구할 수 있다.
- ruby libev(rev) 라이브러리 및 문서를 구할 수 있다.
- Memcached는 데이터를 저장하고 처리하기 위한 RAM 캐시로서, 코어에서 libevent를 사용할 뿐 아니라 다른 libevent 서버에서도 사용된다.
- DVD로 제공되거나 다운로드할 수 있는 IBM 시험판 소프트웨어를 사용하여 차기 오픈 소스 개발 프로젝트를 구현해 보자.
토론
- developerWorks 블로그를 통해 developerWorks 커뮤니티에 참여할 수 있다.
- Twitter의 developerWorks 페이지를 살펴보자.
- My developerWorks 커뮤니티에 참여하자.
- AIX 및 UNIX® 포럼에 참여하자.