Concurrent Servers
웹게이트 사업부 SW파트 윤근식
목차
• Introduction
– The protocol
– Iterative server
– Multiple concurrent clients
• Threads
– Approach
– Multi-threaded
– Thread pools
• Event-driven
– Blocking, non-blocking
– Select and epoll
• libuv
Introduction
• Concurrent servers
– 여러 개의 clients 를 동시에 처리 할 수 있는 server
• 여러가지 Concurrent server 모델 확인
Protocol
• Stateful
– Server가 Client와의 통신 상태를
계속 추적
• Stateless
– 독립적인 쌍의 요청과 응답
– Client에게 세션 정보나 상태 보
관을 요구하지 않음
Stateful protocol, Server’s view
Wait for
Client
Wait for
message
Message
in
START
In: client connected
Out: send client accepted code
In: received message begin code
In: received message end code
In: received message
(message != end code)
Out: send processed message
Iterative server
• while문 한 cycle에 Client 하나 accept
• Client socket과 연결 후 데이터 처리
• 한번에 1개의 Client만 처리 가능!
while (1) {
...
int newsockfd =
accept(sockfd, (struct
sockaddr*)&peer_addr, &peer_addr_len);
...
report_peer_connected(&peer_addr,
peer_addr_len);
serve_connection(newsockfd);
printf("peer donen");
}
Multiple concurrent clients
14:14:17,763:conn1 connected...
14:14:17,763:conn1 sending b'^abc$de^abte$f'
14:14:17,763:conn1 received b'b'
14:14:17,802:conn1 received b'cdbcuf'
14:14:18,764:conn1 sending b'xyz^123'
14:14:18,764:conn1 received b'234'
14:14:19,764:conn1 sending b'25$^ab0000$abab'
14:14:19,765:conn1 received b'36bc1111'
14:14:19,965:conn1 disconnecting
14:14:19,966:conn2 connected...
14:14:19,967:conn2 sending b'^abc$de^abte$f'
14:14:19,967:conn2 received b'b'
14:14:20,006:conn2 received b'cdbcuf'
14:14:20,968:conn2 sending b'xyz^123'
14:14:20,969:conn2 received b'234'
14:14:21,970:conn2 sending b'25$^ab0000$abab'
14:14:21,970:conn2 received b'36bc1111'
14:14:22,171:conn2 disconnecting
14:14:22,171:conn0 connected...
14:14:22,172:conn0 sending b'^abc$de^abte$f'
14:14:22,172:conn0 received b'b'
14:14:22,210:conn0 received b'cdbcuf'
14:14:23,173:conn0 sending b'xyz^123'
14:14:23,174:conn0 received b'234'
14:14:24,175:conn0 sending b'25$^ab0000$abab'
14:14:24,176:conn0 received b'36bc1111'
14:14:24,376:conn0 disconnecting
• Client 하나가 연결되면 다음 Client는 이전 연결
이 끊길때까지 기다려야 한다!
• Clients가 많고 작업이 길수록 비 효율적인 구조
Threads
• Thread: 프로세스 내에서 실행되는 여러 흐름의 단위
– 프로세스 내에서 Stack만 따로 할당, Code, Data, Heap 공유
– 멀티프로세싱의 Context Switching 오버헤드 감소
– 공유 데이터 사용시 충돌 가능성 up
– 디버깅이 까다로움
• Multi-thread를 활용해서 Server의 concurrency를 해결
– One thread per client
– Thread pools
Approach to concurrent server design
Iterative server Multi-threaded server
Runtime
Runtime
: Arrival time
: Process time: Wait time
One Thread per Client
while (1) {
...
int newsockfd =
accept(sockfd, (struct
sockaddr*)&peer_addr, &peer_addr_len);
...
pthread_t the_thread;
thread_config_t* config =
(thread_config_t*)malloc(sizeof(*config
));
...
config->sockfd = newsockfd;
pthread_create(&the_thread, NULL,
server_thread, config);
pthread_detach(the_thread);
}
• Client와 socket 연결
• Thread를 생성하고 server_thread작업을 던
져준다.
One Thread per Client
void* server_thread(void* arg) {
thread_config_t* config =
(thread_config_t*)arg;
int sockfd = config->sockfd;
free(config);
unsigned long id = (unsigned
long)pthread_self();
printf("Thread %lu created to handle
connection with socket %dn", id,
sockfd);
serve_connection(sockfd);
printf("Thread %lu donen", id);
return 0;
}
• Thread에서 Client를 처리한다.
One Thread per Client
06:31:56,632:conn1 connected...
06:31:56,632:conn2 connected...
06:31:56,632:conn0 connected...
06:31:56,632:conn1 sending b'^abc$de^abte$f'
06:31:56,632:conn2 sending b'^abc$de^abte$f'
06:31:56,632:conn0 sending b'^abc$de^abte$f'
06:31:56,633:conn1 received b'b'
06:31:56,633:conn2 received b'b'
06:31:56,633:conn0 received b'b'
06:31:56,670:conn1 received b'cdbcuf'
06:31:56,671:conn0 received b'cdbcuf'
06:31:56,671:conn2 received b'cdbcuf'
06:31:57,634:conn1 sending b'xyz^123'
06:31:57,634:conn2 sending b'xyz^123'
06:31:57,634:conn1 received b'234'
06:31:57,634:conn0 sending b'xyz^123'
06:31:57,634:conn2 received b'234'
06:31:57,634:conn0 received b'234'
06:31:58,635:conn1 sending b'25$^ab0000$abab'
06:31:58,635:conn2 sending b'25$^ab0000$abab'
06:31:58,636:conn1 received b'36bc1111'
06:31:58,636:conn2 received b'36bc1111'
06:31:58,637:conn0 sending b'25$^ab0000$abab'
06:31:58,637:conn0 received b'36bc1111'
06:31:58,836:conn2 disconnecting
06:31:58,836:conn1 disconnecting
06:31:58,837:conn0 disconnecting
• Clients가 동시에 연결된다.
• One thread per client로 Concurrency 만족
One Thread per Client
• 처리속도보다 많은 Client가 들어올 경우 너무 많은 thread가 생성
된다.
– Context switching에 메모리와 cpu time이 많이 소모된다.
– 메모리가 부족해지고 처리시간이 급격하게 증가하거나 오류를 발생시킬 수 있다.
• 짧은 작업의 다수의 Client가 들어올 경우 thread를 생성하고 종료
할 때 너무 많은 자원이 소모된다.
• DoS attack과 같은 공격에 취약하다.
Thread pools
• Worker-crew 모델 이라고도 한다.
• 여러 개의 threads가 task가 오길 기다리며
concurrent server로서 기능한다.
• Thread의 개수는 workload, cpu 코어의 수,
network socket 등 여러 요인에 따라 바꿀
수 있다.
• 처리속도보다 Client 수가 많아지면 wait
time은 길어지지만 처리속도를 유지한다.
· · ·
· · ·
Task Queue
Completed Tasks
Thread Pool
Blocking vs. nonblocking I/O
Blocking I/O
• 일반적인 I/O API가 작동하는 방식이다.
• Data를 socket에서 받을 때 버퍼가 비어있
으면 block system call 호출
• Block된 프로그램은 CPU를 사용하지 않고
커널 응답을 기다린다.
• 응답이 되돌아오면 어플리케이션 블록이
풀린다.
Nonblocking I/O
• Nonblock 모드에서 recv와 같은 함수는 버
퍼가 비어 있어도 언제나 바로 리턴한다.
• 프로그램은 기다리지 않고 다음 코드를 실
행한다.
Blocking, Nonblocking
Synchronous, Asynchronous
• Blocking/NonBlocking
– 호출되는 함수가 바로 리턴 하지 않으면 Blocking
– 바로 리턴 하면 NonBlocking
• Synchronous/Asynchronous
– 호출되는 함수의 작업 완료를 호출한 함수가 신경 쓰면
Synchronous
– 호출되는 함수의 작업 완료를 호출된 함수가 신경 쓰면
Asynchronous
Blocking, Nonblocking
Synchronous, Asynchronous
Blocking NonBlocking
Synchronous
Asynchronous
Select
int select( int maxfdNum, //파일 디스크립터의 관찰 범위 (0 ~ maxfdNum -1)
fd_set *restrict readfds, //read I/O를 통지받을 FD_SET의 주소, 없으면 NULL
fd_set *restrict writefds,//write I/O를 통지받을 FD_SET의 주소, 없으면 NULL
fd_set *restrict errorfds,//error I/O를 통지받을 FD_SET의 주소, 없으면 NULL
struct timeval *restrict timeout //null이면 변화가 있을 때까지 계속 Block,
//아니면 주어진 시간만큼 대기후 timeout.
);
//반환값 : 오류 발생시 -1, timeout에 의한 반환은 0, 정상 작동일때 변경된 파일 디스크립터 개수
• Single thread로 다중 I/O를 처리하는 멀티플렉싱 통지모
델의 대표적인 방법이다.
Select example
int main(int argc, char** argv){
...
Fd_set readfds_master;
FD_ZERO(&readfds_master);
Fd_set writefds_master;
FD_ZERO(&writefds_master);
FD_SET(listener_sockfd, &readfds_master);
int fdset_max = listener_sockfd;
while (1) {
fd_set readfds = readfds_master;
fd_set writefds = writefds_master;
int nready = select(fdset_max + 1, &readfds,
&writefds, NULL, NULL);
...
• readfds와 writefds를 초기화한다.
• fdset의 크기를 설정하여 select시에
매번 FD_SETSIZE만큼 반복하는 것
을 방지한다.
• Select는 인자로 받은 FD_SET의 값
을 변경시키므로, 복사된 값을 넘긴
다.
Select example
...
for (int fd = 0; fd <= fdset_max && nready > 0; fd++) {
if (FD_ISSET(fd, &readfds)) {
nready--;
if (fd == listener_sockfd) {
...
} else {
fd_status_t status = on_peer_ready_recv(fd);
if (status.want_read) {
FD_SET(fd, &readfds_master);
} else {
FD_CLR(fd, &readfds_master);
}
if (status.want_write) {
FD_SET(fd, &writefds_master);
} else {
FD_CLR(fd, &writefds_master);
}
if (!status.want_read && !status.want_write) {
printf("socket %d closingn", fd);
close(fd);
}
}
• read 할게 있는지 확인한다.
• Server의 listener socket을 등
록한다.
• Read하고 상태를 set, clr 하는
단계이다.
• read, write할게 모두 없는 경
우 소켓을 닫는다.
Select example
05:29:15,864:conn1 connected...
05:29:15,864:conn2 connected...
05:29:15,864:conn0 connected...
05:29:15,865:conn1 sending b'^abc$de^abte$f'
05:29:15,865:conn2 sending b'^abc$de^abte$f'
05:29:15,865:conn0 sending b'^abc$de^abte$f'
05:29:15,865:conn1 received b'bcdbcuf'
05:29:15,865:conn2 received b'bcdbcuf'
05:29:15,865:conn0 received b'bcdbcuf'
05:29:16,866:conn1 sending b'xyz^123'
05:29:16,867:conn0 sending b'xyz^123'
05:29:16,867:conn2 sending b'xyz^123'
05:29:16,867:conn1 received b'234'
05:29:16,868:conn0 received b'234'
05:29:16,868:conn2 received b'234'
05:29:17,868:conn1 sending b'25$^ab0000$abab'
05:29:17,869:conn1 received b'36bc1111'
05:29:17,869:conn0 sending b'25$^ab0000$abab'
05:29:17,870:conn0 received b'36bc1111'
05:29:17,870:conn2 sending b'25$^ab0000$abab'
05:29:17,870:conn2 received b'36bc1111'
05:29:18,069:conn1 disconnecting
05:29:18,070:conn0 disconnecting
05:29:18,070:conn2 disconnecting
The limitations of select
• 최대 fd의 개수가 1024개로 제한되어 있다.
• fd의 주소를 넘겨주는 것이 아닌 개수만 넘겨주므로 최악의
경우 모든 fd를 다 돌아야 한다.
– fd가 많아질수록 이 overhead가 증가하면서 성능이 낮아진다.
• 단점을 보완한 기법으로는 epoll, iocp, RIO, kqueue 등이 있으
며 linux 환경에서 사용하는 I/O통지 기법은 epoll이다.
• epoll은 기본적인 동작방법은 select와 같지만 read나 write가
준비된 fd를 buffer에 채운다. 준비된 fd를 찾아주기 때문에
select의 O(N)의 복잡도에서 O(1)로 좋아진다.
Epoll
int epoll_create(int size);
//size는 epoll_fd의 크기정보를 전달한다.
//반환 값 : 실패 시 -1, 일반적으로 epoll_fd의 값을 리턴
int epoll_ctl(int epoll_fd,
int operate_enum,
//어떤 변경을 할지 결정하는 enum값
int enroll_fd,
//등록할 fd
struct epoll_event* event
//관찰 대상의 관찰 이벤트 유형
);
//반환 값 : 실패 시 -1, 성공시 0
int epoll_wait( int epoll_fd,
struct epoll_event* event,
//event 버퍼의 주소
int maxevents,
//버퍼에 들어갈 수 있는 구조체 최대 개수
int timeout
//select의 timeout과 동일 단위는 1/1000
);
//성공시 이벤트 발생한 파일 디스크립터 개수 반환, 실패시 -1
반환
• size는 epoll_fd의 크기 정보를
전달하고, return값은 epoll_fd
이다.
• 관찰 대상이 되는 fd를 등록,
삭제하는데 사용된다.
• select 의 select()와 같은 역할
이다.
Epoll
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef epoll_data
{
void* ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
enum Events
{
EPOLLIN, //수신할 데이터가 있다.
EPOLLOUT, //송신 가능하다.
EPOLLPRI, //중요한 데이터(OOB)가 발생.
EPOLLRDHUD,//연결 종료 or Half-close 발생
EPOLLERR, //에러 발생
EPOLLET, //엣지 트리거 방식으로 설정
EPOLLONESHOT, //한번만 이벤트 받음
}
• fd와 event, 기타 정보를 묶어
서 만든 구조체이다.
Epoll
struct epoll_event accept_event;
accept_event.data.fd = listener_sockfd;
accept_event.events = EPOLLIN;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listener_sockfd, &accept_event) < 0) {
perror_die(“epoll_ctl EPOLL_CTL_ADD”);
}
struct epoll_event* events = calloc(MAXFDS, sizeof(struct epoll_event));
if (events == NULL) {
die("Unable to allocate memory for epoll_events");
}
while (1) {
int nready = epoll_wait(epollfd, events, MAXFDS, -1);
for (int i = 0; i < nready; i++) {
if (events[i].events & EPOLLERR) {
perror_die("epoll_wait returned EPOLLERR");
}
if (events[i].data.fd == listener_sockfd) {
// The listening socket is ready; this means a new peer is connecting.
...
} else {
// A peer socket is ready.
if (events[i].events & EPOLLIN) {
// Ready for reading.
...
} else if (events[i].events & EPOLLOUT) {
// Ready for writing.
...
}
}
}
}
• Server의 listener socket을 등록한다.
• connect request, read, write
• epoll_wait()에서 event가 일어난 fd만 가져
오기 때문에 FD_ISSET으로 모든 client를 확
인하는 select보다 빠르다.
Libuv(Unicorn Velociraptor Library)
• 초기에는 Node.js를 위해 만들
어졌다.
• 멀티플랫폼 C library로 event
loop기반의 AIO를 제공한다.
• epoll, kqueue, IOCP, Solaris
event port를 지원한다.
libuv
• asynchronous, event-driven 스타일의 개발을 도와준다.
• event loop을 지원하고 I/O와 다른 작업들을 callback을 기
반으로 실행한다.
• timer, non-blocking network, asynchronous file system
access 등 여러 유틸리티를 제공한다.
libuv example
int portnum = 9090;
if (argc >= 2) {
portnum = atoi(argv[1]);
}
printf("Serving on port %dn", portnum);
int rc;
uv_tcp_t server_stream;
if ((rc = uv_tcp_init(uv_default_loop(),
&server_stream)) < 0) {
die("uv_tcp_init failed: %s", uv_strerror(rc));
}
struct sockaddr_in server_address;
if ((rc = uv_ip4_addr("0.0.0.0", portnum,
&server_address)) < 0) {
die("uv_ip4_addr failed: %s", uv_strerror(rc));
}
if ((rc = uv_tcp_bind(&server_stream, (const struct
sockaddr*)&server_address, 0)) < 0) {
die("uv_tcp_bind failed: %s", uv_strerror(rc));
}
if ((rc = uv_listen((uv_stream_t*)&server_stream,
N_BACKLOG, on_peer_connected)) < 0) {
die("uv_listen failed: %s", uv_strerror(rc));
}
• socket create, socket bind,
listen 과정이다.
• uv_listen 실행중에 새로운
connection을 받으면 callback
으로 on_peer_connected 를
호출한다.
libuv example
uv_run(uv_default_loop(),
UV_RUN_DEFAULT);
return
uv_loop_close(uv_default_
loop());
• 설정된 loop은 while과 비슷하
게 stop이나 error발생시에만
return한다.
• loop close 후 프로그램이 종료
된다.
libuv example
void on_peer_connected(uv_stream_t* server_stream, int status) {
...
uv_tcp_t* client = (uv_tcp_t*)xmalloc(sizeof(*client));
int rc;
if ((rc = uv_tcp_init(uv_default_loop(), client)) < 0) {
die("uv_tcp_init failed: %s", uv_strerror(rc));
}
client->data = NULL;
if (uv_accept(server_stream, (uv_stream_t*)client) == 0) {
struct sockaddr_storage peername;
int namelen = sizeof(peername);
if ((rc = uv_tcp_getpeername(client, (struct sockaddr*)&peername,
&namelen)) < 0) {
die("uv_tcp_getpeername failed: %s", uv_strerror(rc));
}
report_peer_connected((const struct sockaddr_in*)&peername, namelen);
peer_state_t* peerstate = (peer_state_t*)xmalloc(sizeof(*peerstate));
peerstate->state = INITIAL_ACK;
peerstate->sendbuf[0] = '*';
peerstate->sendbuf_end = 1;
peerstate->client = client;
client->data = peerstate;
uv_buf_t writebuf = uv_buf_init(peerstate->sendbuf, peerstate-
>sendbuf_end);
uv_write_t* req = (uv_write_t*)xmalloc(sizeof(*req));
req->data = peerstate;
if ((rc = uv_write(req, (uv_stream_t*)client, &writebuf, 1,
on_wrote_init_ack)) < 0) {
die("uv_write failed: %s", uv_strerror(rc));
}
} else {
uv_close((uv_handle_t*)client, on_client_closed);
}
}
• 새로운 client를 받을 tcp handle을 loop에
init 해준다.
• client와 server가 연결되었다.
– 함수명이 getpeername이지만 주소를
구한다.
• 연결 후 최초 response할 초기 설정을 한다.
• uv_write를 호출하고 on_wrote_init_ack를
callback한다.
libuv example
void on_wrote_init_ack(uv_write_t* req, int status) {
if (status) {
die("Write error: %sn", uv_strerror(status));
}
peer_state_t* peerstate = (peer_state_t*)req->data;
// Flip the peer state to WAIT_FOR_MSG, and start
listening for incoming data
// from this peer.
peerstate->state = WAIT_FOR_MSG;
peerstate->sendbuf_end = 0;
int rc;
if ((rc = uv_read_start((uv_stream_t*)peerstate-
>client, on_alloc_buffer,
on_peer_read)) < 0) {
die("uv_read_start failed: %s", uv_strerror(rc));
}
// Note: the write request doesn't own the peer state,
hence we only free the
// request itself, not the state.
free(req);
}
• sendbuf에 있는 data를 다 보냈으니 state를
WAIT_FOR_MSG로 변경하고 sendbuf_end
를 0으로 변경한다.
• uv_read_start를 호출하고 on_peer_read를
callback한다. on_peer_read는 읽을 data가
없거나 uv_read_stop이 호출될 때 까지 여
러 번 호출된다.
libuv example
void on_peer_read(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf) {
...
if (nread == 0) {
// from the documentation of uv_read_cb: nread might be 0, which does not
// indicate an error or eof. this is equivalent to eagain or ewouldblock
// under read(2).
} else {
// nread > 0
assert(buf->len >= nread);
peer_state_t* peerstate = (peer_state_t*)client->data;
if (peerstate->state == initial_ack) {
free(buf->base);
return;
}
for (int i = 0; i < nread; ++i) {
...
}
if (peerstate->sendbuf_end > 0) {
// we have data to send. the write buffer will point to the buffer
stored
// in the peer state for this client.
uv_buf_t writebuf =
uv_buf_init(peerstate->sendbuf, peerstate->sendbuf_end);
uv_write_t* writereq = (uv_write_t*)xmalloc(sizeof(*writereq));
writereq->data = peerstate;
int rc;
if ((rc = uv_write(writereq, (uv_stream_t*)client, &writebuf, 1,
on_wrote_buf)) < 0) {
die("uv_write failed: %s", uv_strerror(rc));
}
}
}
free(buf->base);
}
• data를 읽고 처리한다.
• uv_write를 호출하고 on_wrote_buf를
callback한다.
libuv example
void on_wrote_buf(uv_write_t* req, int
status) {
if (status) {
die("Write error: %sn",
uv_strerror(status));
}
peer_state_t* peerstate =
(peer_state_t*)req->data;
// The send buffer is done;
//move pointer back to 0.
peerstate->sendbuf_end = 0;
free(req);
}
• loop cycle 완료
Blocking calls to asynchronous calls
void on_after_work(uv_work_t* req, int status) {
free(req);
}
void on_work(uv_work_t* req) {
// "Work"
if (random() % 5 == 0) {
printf("Sleeping...n");
sleep(3);
}
}
void on_timer(uv_timer_t* timer) {
uint64_t timestamp = uv_hrtime();
printf("on_timer [%" PRIu64 " ms]n", (timestamp / 1000000) %
100000);
uv_work_t* work_req = (uv_work_t*)malloc(sizeof(*work_req));
uv_queue_work(uv_default_loop(), work_req, on_work,
on_after_work);
}
int main(int argc, const char** argv) {
uv_timer_t timer;
uv_timer_init(uv_default_loop(), &timer);
uv_timer_start(&timer, on_timer, 0, 1000);
return uv_run(uv_default_loop(), UV_RUN_DEFAULT);
}
• on_work를 직접 uv_timer_start의 callback
에 넣지 않고, uv_queue_work를 이용해
thread pool에서 실행시켰다.
• event loop은 멈추지 않고 돌고, on_work와
on_after_work는 thread pool에서 실행된다.
Blocking calls vs Asynchronous calls
on_timer [4840 ms]
on_timer [5842 ms]
on_timer [6843 ms]
on_timer [7844 ms]
Sleeping...
on_timer [11845 ms]
on_timer [12846 ms]
Sleeping...
on_timer [16847 ms]
on_timer [17849 ms]
on_timer [18850 ms]
...
on_timer [89571 ms]
on_timer [90572 ms]
on_timer [91573 ms]
on_timer [92575 ms]
Sleeping...
on_timer [93576 ms]
on_timer [94577 ms]
Sleeping...
on_timer [95577 ms]
on_timer [96578 ms]
on_timer [97578 ms]
...
• Sleeping 동안
block된다.
• Non-block으로
진행된다.
Concurrent servers

Concurrent servers

  • 1.
  • 2.
    목차 • Introduction – Theprotocol – Iterative server – Multiple concurrent clients • Threads – Approach – Multi-threaded – Thread pools • Event-driven – Blocking, non-blocking – Select and epoll • libuv
  • 3.
    Introduction • Concurrent servers –여러 개의 clients 를 동시에 처리 할 수 있는 server • 여러가지 Concurrent server 모델 확인
  • 4.
    Protocol • Stateful – Server가Client와의 통신 상태를 계속 추적 • Stateless – 독립적인 쌍의 요청과 응답 – Client에게 세션 정보나 상태 보 관을 요구하지 않음
  • 5.
    Stateful protocol, Server’sview Wait for Client Wait for message Message in START In: client connected Out: send client accepted code In: received message begin code In: received message end code In: received message (message != end code) Out: send processed message
  • 6.
    Iterative server • while문한 cycle에 Client 하나 accept • Client socket과 연결 후 데이터 처리 • 한번에 1개의 Client만 처리 가능! while (1) { ... int newsockfd = accept(sockfd, (struct sockaddr*)&peer_addr, &peer_addr_len); ... report_peer_connected(&peer_addr, peer_addr_len); serve_connection(newsockfd); printf("peer donen"); }
  • 7.
    Multiple concurrent clients 14:14:17,763:conn1connected... 14:14:17,763:conn1 sending b'^abc$de^abte$f' 14:14:17,763:conn1 received b'b' 14:14:17,802:conn1 received b'cdbcuf' 14:14:18,764:conn1 sending b'xyz^123' 14:14:18,764:conn1 received b'234' 14:14:19,764:conn1 sending b'25$^ab0000$abab' 14:14:19,765:conn1 received b'36bc1111' 14:14:19,965:conn1 disconnecting 14:14:19,966:conn2 connected... 14:14:19,967:conn2 sending b'^abc$de^abte$f' 14:14:19,967:conn2 received b'b' 14:14:20,006:conn2 received b'cdbcuf' 14:14:20,968:conn2 sending b'xyz^123' 14:14:20,969:conn2 received b'234' 14:14:21,970:conn2 sending b'25$^ab0000$abab' 14:14:21,970:conn2 received b'36bc1111' 14:14:22,171:conn2 disconnecting 14:14:22,171:conn0 connected... 14:14:22,172:conn0 sending b'^abc$de^abte$f' 14:14:22,172:conn0 received b'b' 14:14:22,210:conn0 received b'cdbcuf' 14:14:23,173:conn0 sending b'xyz^123' 14:14:23,174:conn0 received b'234' 14:14:24,175:conn0 sending b'25$^ab0000$abab' 14:14:24,176:conn0 received b'36bc1111' 14:14:24,376:conn0 disconnecting • Client 하나가 연결되면 다음 Client는 이전 연결 이 끊길때까지 기다려야 한다! • Clients가 많고 작업이 길수록 비 효율적인 구조
  • 8.
    Threads • Thread: 프로세스내에서 실행되는 여러 흐름의 단위 – 프로세스 내에서 Stack만 따로 할당, Code, Data, Heap 공유 – 멀티프로세싱의 Context Switching 오버헤드 감소 – 공유 데이터 사용시 충돌 가능성 up – 디버깅이 까다로움 • Multi-thread를 활용해서 Server의 concurrency를 해결 – One thread per client – Thread pools
  • 9.
    Approach to concurrentserver design Iterative server Multi-threaded server Runtime Runtime : Arrival time : Process time: Wait time
  • 10.
    One Thread perClient while (1) { ... int newsockfd = accept(sockfd, (struct sockaddr*)&peer_addr, &peer_addr_len); ... pthread_t the_thread; thread_config_t* config = (thread_config_t*)malloc(sizeof(*config )); ... config->sockfd = newsockfd; pthread_create(&the_thread, NULL, server_thread, config); pthread_detach(the_thread); } • Client와 socket 연결 • Thread를 생성하고 server_thread작업을 던 져준다.
  • 11.
    One Thread perClient void* server_thread(void* arg) { thread_config_t* config = (thread_config_t*)arg; int sockfd = config->sockfd; free(config); unsigned long id = (unsigned long)pthread_self(); printf("Thread %lu created to handle connection with socket %dn", id, sockfd); serve_connection(sockfd); printf("Thread %lu donen", id); return 0; } • Thread에서 Client를 처리한다.
  • 12.
    One Thread perClient 06:31:56,632:conn1 connected... 06:31:56,632:conn2 connected... 06:31:56,632:conn0 connected... 06:31:56,632:conn1 sending b'^abc$de^abte$f' 06:31:56,632:conn2 sending b'^abc$de^abte$f' 06:31:56,632:conn0 sending b'^abc$de^abte$f' 06:31:56,633:conn1 received b'b' 06:31:56,633:conn2 received b'b' 06:31:56,633:conn0 received b'b' 06:31:56,670:conn1 received b'cdbcuf' 06:31:56,671:conn0 received b'cdbcuf' 06:31:56,671:conn2 received b'cdbcuf' 06:31:57,634:conn1 sending b'xyz^123' 06:31:57,634:conn2 sending b'xyz^123' 06:31:57,634:conn1 received b'234' 06:31:57,634:conn0 sending b'xyz^123' 06:31:57,634:conn2 received b'234' 06:31:57,634:conn0 received b'234' 06:31:58,635:conn1 sending b'25$^ab0000$abab' 06:31:58,635:conn2 sending b'25$^ab0000$abab' 06:31:58,636:conn1 received b'36bc1111' 06:31:58,636:conn2 received b'36bc1111' 06:31:58,637:conn0 sending b'25$^ab0000$abab' 06:31:58,637:conn0 received b'36bc1111' 06:31:58,836:conn2 disconnecting 06:31:58,836:conn1 disconnecting 06:31:58,837:conn0 disconnecting • Clients가 동시에 연결된다. • One thread per client로 Concurrency 만족
  • 13.
    One Thread perClient • 처리속도보다 많은 Client가 들어올 경우 너무 많은 thread가 생성 된다. – Context switching에 메모리와 cpu time이 많이 소모된다. – 메모리가 부족해지고 처리시간이 급격하게 증가하거나 오류를 발생시킬 수 있다. • 짧은 작업의 다수의 Client가 들어올 경우 thread를 생성하고 종료 할 때 너무 많은 자원이 소모된다. • DoS attack과 같은 공격에 취약하다.
  • 14.
    Thread pools • Worker-crew모델 이라고도 한다. • 여러 개의 threads가 task가 오길 기다리며 concurrent server로서 기능한다. • Thread의 개수는 workload, cpu 코어의 수, network socket 등 여러 요인에 따라 바꿀 수 있다. • 처리속도보다 Client 수가 많아지면 wait time은 길어지지만 처리속도를 유지한다. · · · · · · Task Queue Completed Tasks Thread Pool
  • 15.
    Blocking vs. nonblockingI/O Blocking I/O • 일반적인 I/O API가 작동하는 방식이다. • Data를 socket에서 받을 때 버퍼가 비어있 으면 block system call 호출 • Block된 프로그램은 CPU를 사용하지 않고 커널 응답을 기다린다. • 응답이 되돌아오면 어플리케이션 블록이 풀린다. Nonblocking I/O • Nonblock 모드에서 recv와 같은 함수는 버 퍼가 비어 있어도 언제나 바로 리턴한다. • 프로그램은 기다리지 않고 다음 코드를 실 행한다.
  • 16.
    Blocking, Nonblocking Synchronous, Asynchronous •Blocking/NonBlocking – 호출되는 함수가 바로 리턴 하지 않으면 Blocking – 바로 리턴 하면 NonBlocking • Synchronous/Asynchronous – 호출되는 함수의 작업 완료를 호출한 함수가 신경 쓰면 Synchronous – 호출되는 함수의 작업 완료를 호출된 함수가 신경 쓰면 Asynchronous
  • 17.
  • 18.
    Select int select( intmaxfdNum, //파일 디스크립터의 관찰 범위 (0 ~ maxfdNum -1) fd_set *restrict readfds, //read I/O를 통지받을 FD_SET의 주소, 없으면 NULL fd_set *restrict writefds,//write I/O를 통지받을 FD_SET의 주소, 없으면 NULL fd_set *restrict errorfds,//error I/O를 통지받을 FD_SET의 주소, 없으면 NULL struct timeval *restrict timeout //null이면 변화가 있을 때까지 계속 Block, //아니면 주어진 시간만큼 대기후 timeout. ); //반환값 : 오류 발생시 -1, timeout에 의한 반환은 0, 정상 작동일때 변경된 파일 디스크립터 개수 • Single thread로 다중 I/O를 처리하는 멀티플렉싱 통지모 델의 대표적인 방법이다.
  • 19.
    Select example int main(intargc, char** argv){ ... Fd_set readfds_master; FD_ZERO(&readfds_master); Fd_set writefds_master; FD_ZERO(&writefds_master); FD_SET(listener_sockfd, &readfds_master); int fdset_max = listener_sockfd; while (1) { fd_set readfds = readfds_master; fd_set writefds = writefds_master; int nready = select(fdset_max + 1, &readfds, &writefds, NULL, NULL); ... • readfds와 writefds를 초기화한다. • fdset의 크기를 설정하여 select시에 매번 FD_SETSIZE만큼 반복하는 것 을 방지한다. • Select는 인자로 받은 FD_SET의 값 을 변경시키므로, 복사된 값을 넘긴 다.
  • 20.
    Select example ... for (intfd = 0; fd <= fdset_max && nready > 0; fd++) { if (FD_ISSET(fd, &readfds)) { nready--; if (fd == listener_sockfd) { ... } else { fd_status_t status = on_peer_ready_recv(fd); if (status.want_read) { FD_SET(fd, &readfds_master); } else { FD_CLR(fd, &readfds_master); } if (status.want_write) { FD_SET(fd, &writefds_master); } else { FD_CLR(fd, &writefds_master); } if (!status.want_read && !status.want_write) { printf("socket %d closingn", fd); close(fd); } } • read 할게 있는지 확인한다. • Server의 listener socket을 등 록한다. • Read하고 상태를 set, clr 하는 단계이다. • read, write할게 모두 없는 경 우 소켓을 닫는다.
  • 21.
    Select example 05:29:15,864:conn1 connected... 05:29:15,864:conn2connected... 05:29:15,864:conn0 connected... 05:29:15,865:conn1 sending b'^abc$de^abte$f' 05:29:15,865:conn2 sending b'^abc$de^abte$f' 05:29:15,865:conn0 sending b'^abc$de^abte$f' 05:29:15,865:conn1 received b'bcdbcuf' 05:29:15,865:conn2 received b'bcdbcuf' 05:29:15,865:conn0 received b'bcdbcuf' 05:29:16,866:conn1 sending b'xyz^123' 05:29:16,867:conn0 sending b'xyz^123' 05:29:16,867:conn2 sending b'xyz^123' 05:29:16,867:conn1 received b'234' 05:29:16,868:conn0 received b'234' 05:29:16,868:conn2 received b'234' 05:29:17,868:conn1 sending b'25$^ab0000$abab' 05:29:17,869:conn1 received b'36bc1111' 05:29:17,869:conn0 sending b'25$^ab0000$abab' 05:29:17,870:conn0 received b'36bc1111' 05:29:17,870:conn2 sending b'25$^ab0000$abab' 05:29:17,870:conn2 received b'36bc1111' 05:29:18,069:conn1 disconnecting 05:29:18,070:conn0 disconnecting 05:29:18,070:conn2 disconnecting
  • 22.
    The limitations ofselect • 최대 fd의 개수가 1024개로 제한되어 있다. • fd의 주소를 넘겨주는 것이 아닌 개수만 넘겨주므로 최악의 경우 모든 fd를 다 돌아야 한다. – fd가 많아질수록 이 overhead가 증가하면서 성능이 낮아진다. • 단점을 보완한 기법으로는 epoll, iocp, RIO, kqueue 등이 있으 며 linux 환경에서 사용하는 I/O통지 기법은 epoll이다. • epoll은 기본적인 동작방법은 select와 같지만 read나 write가 준비된 fd를 buffer에 채운다. 준비된 fd를 찾아주기 때문에 select의 O(N)의 복잡도에서 O(1)로 좋아진다.
  • 23.
    Epoll int epoll_create(int size); //size는epoll_fd의 크기정보를 전달한다. //반환 값 : 실패 시 -1, 일반적으로 epoll_fd의 값을 리턴 int epoll_ctl(int epoll_fd, int operate_enum, //어떤 변경을 할지 결정하는 enum값 int enroll_fd, //등록할 fd struct epoll_event* event //관찰 대상의 관찰 이벤트 유형 ); //반환 값 : 실패 시 -1, 성공시 0 int epoll_wait( int epoll_fd, struct epoll_event* event, //event 버퍼의 주소 int maxevents, //버퍼에 들어갈 수 있는 구조체 최대 개수 int timeout //select의 timeout과 동일 단위는 1/1000 ); //성공시 이벤트 발생한 파일 디스크립터 개수 반환, 실패시 -1 반환 • size는 epoll_fd의 크기 정보를 전달하고, return값은 epoll_fd 이다. • 관찰 대상이 되는 fd를 등록, 삭제하는데 사용된다. • select 의 select()와 같은 역할 이다.
  • 24.
    Epoll struct epoll_event { __uint32_t events; epoll_data_tdata; } typedef epoll_data { void* ptr; int fd; __uint32_t u32; __uint64_t u64; }epoll_data_t; enum Events { EPOLLIN, //수신할 데이터가 있다. EPOLLOUT, //송신 가능하다. EPOLLPRI, //중요한 데이터(OOB)가 발생. EPOLLRDHUD,//연결 종료 or Half-close 발생 EPOLLERR, //에러 발생 EPOLLET, //엣지 트리거 방식으로 설정 EPOLLONESHOT, //한번만 이벤트 받음 } • fd와 event, 기타 정보를 묶어 서 만든 구조체이다.
  • 25.
    Epoll struct epoll_event accept_event; accept_event.data.fd= listener_sockfd; accept_event.events = EPOLLIN; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listener_sockfd, &accept_event) < 0) { perror_die(“epoll_ctl EPOLL_CTL_ADD”); } struct epoll_event* events = calloc(MAXFDS, sizeof(struct epoll_event)); if (events == NULL) { die("Unable to allocate memory for epoll_events"); } while (1) { int nready = epoll_wait(epollfd, events, MAXFDS, -1); for (int i = 0; i < nready; i++) { if (events[i].events & EPOLLERR) { perror_die("epoll_wait returned EPOLLERR"); } if (events[i].data.fd == listener_sockfd) { // The listening socket is ready; this means a new peer is connecting. ... } else { // A peer socket is ready. if (events[i].events & EPOLLIN) { // Ready for reading. ... } else if (events[i].events & EPOLLOUT) { // Ready for writing. ... } } } } • Server의 listener socket을 등록한다. • connect request, read, write • epoll_wait()에서 event가 일어난 fd만 가져 오기 때문에 FD_ISSET으로 모든 client를 확 인하는 select보다 빠르다.
  • 26.
    Libuv(Unicorn Velociraptor Library) •초기에는 Node.js를 위해 만들 어졌다. • 멀티플랫폼 C library로 event loop기반의 AIO를 제공한다. • epoll, kqueue, IOCP, Solaris event port를 지원한다.
  • 27.
    libuv • asynchronous, event-driven스타일의 개발을 도와준다. • event loop을 지원하고 I/O와 다른 작업들을 callback을 기 반으로 실행한다. • timer, non-blocking network, asynchronous file system access 등 여러 유틸리티를 제공한다.
  • 28.
    libuv example int portnum= 9090; if (argc >= 2) { portnum = atoi(argv[1]); } printf("Serving on port %dn", portnum); int rc; uv_tcp_t server_stream; if ((rc = uv_tcp_init(uv_default_loop(), &server_stream)) < 0) { die("uv_tcp_init failed: %s", uv_strerror(rc)); } struct sockaddr_in server_address; if ((rc = uv_ip4_addr("0.0.0.0", portnum, &server_address)) < 0) { die("uv_ip4_addr failed: %s", uv_strerror(rc)); } if ((rc = uv_tcp_bind(&server_stream, (const struct sockaddr*)&server_address, 0)) < 0) { die("uv_tcp_bind failed: %s", uv_strerror(rc)); } if ((rc = uv_listen((uv_stream_t*)&server_stream, N_BACKLOG, on_peer_connected)) < 0) { die("uv_listen failed: %s", uv_strerror(rc)); } • socket create, socket bind, listen 과정이다. • uv_listen 실행중에 새로운 connection을 받으면 callback 으로 on_peer_connected 를 호출한다.
  • 29.
    libuv example uv_run(uv_default_loop(), UV_RUN_DEFAULT); return uv_loop_close(uv_default_ loop()); • 설정된loop은 while과 비슷하 게 stop이나 error발생시에만 return한다. • loop close 후 프로그램이 종료 된다.
  • 30.
    libuv example void on_peer_connected(uv_stream_t*server_stream, int status) { ... uv_tcp_t* client = (uv_tcp_t*)xmalloc(sizeof(*client)); int rc; if ((rc = uv_tcp_init(uv_default_loop(), client)) < 0) { die("uv_tcp_init failed: %s", uv_strerror(rc)); } client->data = NULL; if (uv_accept(server_stream, (uv_stream_t*)client) == 0) { struct sockaddr_storage peername; int namelen = sizeof(peername); if ((rc = uv_tcp_getpeername(client, (struct sockaddr*)&peername, &namelen)) < 0) { die("uv_tcp_getpeername failed: %s", uv_strerror(rc)); } report_peer_connected((const struct sockaddr_in*)&peername, namelen); peer_state_t* peerstate = (peer_state_t*)xmalloc(sizeof(*peerstate)); peerstate->state = INITIAL_ACK; peerstate->sendbuf[0] = '*'; peerstate->sendbuf_end = 1; peerstate->client = client; client->data = peerstate; uv_buf_t writebuf = uv_buf_init(peerstate->sendbuf, peerstate- >sendbuf_end); uv_write_t* req = (uv_write_t*)xmalloc(sizeof(*req)); req->data = peerstate; if ((rc = uv_write(req, (uv_stream_t*)client, &writebuf, 1, on_wrote_init_ack)) < 0) { die("uv_write failed: %s", uv_strerror(rc)); } } else { uv_close((uv_handle_t*)client, on_client_closed); } } • 새로운 client를 받을 tcp handle을 loop에 init 해준다. • client와 server가 연결되었다. – 함수명이 getpeername이지만 주소를 구한다. • 연결 후 최초 response할 초기 설정을 한다. • uv_write를 호출하고 on_wrote_init_ack를 callback한다.
  • 31.
    libuv example void on_wrote_init_ack(uv_write_t*req, int status) { if (status) { die("Write error: %sn", uv_strerror(status)); } peer_state_t* peerstate = (peer_state_t*)req->data; // Flip the peer state to WAIT_FOR_MSG, and start listening for incoming data // from this peer. peerstate->state = WAIT_FOR_MSG; peerstate->sendbuf_end = 0; int rc; if ((rc = uv_read_start((uv_stream_t*)peerstate- >client, on_alloc_buffer, on_peer_read)) < 0) { die("uv_read_start failed: %s", uv_strerror(rc)); } // Note: the write request doesn't own the peer state, hence we only free the // request itself, not the state. free(req); } • sendbuf에 있는 data를 다 보냈으니 state를 WAIT_FOR_MSG로 변경하고 sendbuf_end 를 0으로 변경한다. • uv_read_start를 호출하고 on_peer_read를 callback한다. on_peer_read는 읽을 data가 없거나 uv_read_stop이 호출될 때 까지 여 러 번 호출된다.
  • 32.
    libuv example void on_peer_read(uv_stream_t*client, ssize_t nread, const uv_buf_t* buf) { ... if (nread == 0) { // from the documentation of uv_read_cb: nread might be 0, which does not // indicate an error or eof. this is equivalent to eagain or ewouldblock // under read(2). } else { // nread > 0 assert(buf->len >= nread); peer_state_t* peerstate = (peer_state_t*)client->data; if (peerstate->state == initial_ack) { free(buf->base); return; } for (int i = 0; i < nread; ++i) { ... } if (peerstate->sendbuf_end > 0) { // we have data to send. the write buffer will point to the buffer stored // in the peer state for this client. uv_buf_t writebuf = uv_buf_init(peerstate->sendbuf, peerstate->sendbuf_end); uv_write_t* writereq = (uv_write_t*)xmalloc(sizeof(*writereq)); writereq->data = peerstate; int rc; if ((rc = uv_write(writereq, (uv_stream_t*)client, &writebuf, 1, on_wrote_buf)) < 0) { die("uv_write failed: %s", uv_strerror(rc)); } } } free(buf->base); } • data를 읽고 처리한다. • uv_write를 호출하고 on_wrote_buf를 callback한다.
  • 33.
    libuv example void on_wrote_buf(uv_write_t*req, int status) { if (status) { die("Write error: %sn", uv_strerror(status)); } peer_state_t* peerstate = (peer_state_t*)req->data; // The send buffer is done; //move pointer back to 0. peerstate->sendbuf_end = 0; free(req); } • loop cycle 완료
  • 34.
    Blocking calls toasynchronous calls void on_after_work(uv_work_t* req, int status) { free(req); } void on_work(uv_work_t* req) { // "Work" if (random() % 5 == 0) { printf("Sleeping...n"); sleep(3); } } void on_timer(uv_timer_t* timer) { uint64_t timestamp = uv_hrtime(); printf("on_timer [%" PRIu64 " ms]n", (timestamp / 1000000) % 100000); uv_work_t* work_req = (uv_work_t*)malloc(sizeof(*work_req)); uv_queue_work(uv_default_loop(), work_req, on_work, on_after_work); } int main(int argc, const char** argv) { uv_timer_t timer; uv_timer_init(uv_default_loop(), &timer); uv_timer_start(&timer, on_timer, 0, 1000); return uv_run(uv_default_loop(), UV_RUN_DEFAULT); } • on_work를 직접 uv_timer_start의 callback 에 넣지 않고, uv_queue_work를 이용해 thread pool에서 실행시켰다. • event loop은 멈추지 않고 돌고, on_work와 on_after_work는 thread pool에서 실행된다.
  • 35.
    Blocking calls vsAsynchronous calls on_timer [4840 ms] on_timer [5842 ms] on_timer [6843 ms] on_timer [7844 ms] Sleeping... on_timer [11845 ms] on_timer [12846 ms] Sleeping... on_timer [16847 ms] on_timer [17849 ms] on_timer [18850 ms] ... on_timer [89571 ms] on_timer [90572 ms] on_timer [91573 ms] on_timer [92575 ms] Sleeping... on_timer [93576 ms] on_timer [94577 ms] Sleeping... on_timer [95577 ms] on_timer [96578 ms] on_timer [97578 ms] ... • Sleeping 동안 block된다. • Non-block으로 진행된다.