From 9b5f7629fdf87bcbaade4b9baa349cddc631c63f Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 27 Sep 2026 07:37:38 +0100 Subject: Gather after a save until a just-answered client has come back After a round that saved, the owner waited for 100 us without a new request (1 ms at most) before the next round. Requests that queued up during the save made that quiet start at once, so the clients just answered missed the next round; the clients then settled into two halves taking turns, each save carrying half of them. It showed with two connection threads: 34 events a round and 15.8-16.0k posts/s with 64 connections, against about 61 and 22-24k/s with one or three. The quiet now counts only once a request has come after the answers were handed back: two threads carry 61.6 events a round, 22.9-23.9k/s. --- docs/design/008-module-format.md | 12 +++++++----- prototypes/connections/serve.c | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 5156e0a..3acaee6 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -307,16 +307,18 @@ text·리스트가 남을 때만 위치를 유지한다. 모듈은 상태의 소유자가 하나여야 하므로, 연결 스레드가 하나여도 따로 소유자 스레드가 앱을 돌린다. 연결 스레드는 요청을 받고 해석하고 답을 쓰는 일(시스템 호출이 대부분이다)을 나눠 맡고, 입력과 문서 요청을 소유자에게 넘긴다. 소유자는 모든 스레드의 요청을 한 묶음으로 적용하고 저장 한 번 뒤에 각 스레드에 돌려주며(eventfd로 깨운다), - 스레드는 돌려받은 것을 완료하고 답한다. + 스레드는 돌려받은 것을 완료하고 답한다. 작업 슬롯의 라우트와 입력은 완료될 때까지 그 스레드의 런타임 코어가 + 붙잡고, 빌려준 저장소 매핑의 참조 수는 원자적이다. +- **저장한 묶음 뒤에는 모은다.** 새 요청이 100 µs 동안 끊길 때까지(최대 1 ms) 기다렸다가 다음 묶음을 시작한다. 답을 + 받은 클라이언트의 다음 요청이 한 저장에 들어가게 하는 것으로, 한 스레드에서는 답하는 시간이 그 구실을 한다(묶음당 + 이벤트 30 → 50, p50은 오히려 5.4 → 3.5 ms). 끊김은 답을 돌려준 뒤에 온 요청이 하나라도 있어야 세기 시작한다. 저장을 + 기다리는 동안 줄에 선 요청만으로 끊김을 세면 막 답을 받은 클라이언트가 다음 묶음을 놓치고, 클라이언트가 두 무리로 + 갈려 번갈아 저장한다(연결 스레드 둘, 64연결에서 묶음당 34개, 15.8–16.0k/s였고, 고친 뒤 61.6개, 22.9–23.9k/s). - **보기는 스냅숏에서.** 소유자는 저장이 성공할 때마다, 답을 돌려주기 전에 저장된 상태의 스냅숏을 게시한다. 연결 스레드는 라운드마다(돌려받은 답을 처리한 뒤) 최신 스냅숏을 잡아 자기 읽기용 앱으로 그 라운드의 보기를 모두 평가하고 답을 모아 보낸다. 입력은 그보다 먼저 소유자에게 넘겨, 소유자가 그동안 적용하고 저장한다. 그래서 쓰기의 답을 받은 뒤의 읽기는 그 쓰기를 보고, 저장되지 않았거나 실패로 되돌린 상태는 아무도 보지 않으며, 소유자가 동기화하는 동안에도 읽기는 기다리지 않는다. 스냅숏을 만들지 못하면 보기도 소유자가 답한다. - 작업 슬롯의 라우트와 입력은 완료될 때까지 그 스레드의 런타임 코어가 붙잡고, 빌려준 저장소 매핑의 참조 수는 - 원자적이다. 저장한 묶음 뒤에는 새 요청이 100 µs 동안 끊길 때까지(최대 1 ms) 모은다. 답을 받은 클라이언트의 다음 - 요청이 한 저장에 들어가게 하는 것으로, 한 스레드에서는 답하는 시간이 그 구실을 한다(묶음당 이벤트 30 → 50, p50은 - 오히려 5.4 → 3.5 ms). - 저장된 문서를 읽는 GET 응답은 복사하지 않고 저장소 매핑의 익스텐트를 빌려준다. 보내는 동안 문서가 바뀌어도 읽은 버전이 나간다. 빌려준 응답도 복사한 응답처럼 예산에 계상된다. 매핑 참조는 조각으로 보내는 자원으로 런타임 코어에 넘어가, 응답이 소켓에 다 들어갈 때까지 예산을 차지하고 티켓과 함께 풀린다. 그래서 받지 않는 diff --git a/prototypes/connections/serve.c b/prototypes/connections/serve.c index 87485c0..24248b6 100644 --- a/prototypes/connections/serve.c +++ b/prototypes/connections/serve.c @@ -156,6 +156,7 @@ typedef struct { pthread_cond_t waiting; Job *jobs; size_t count, capacity; + uint64_t arrivals; /* requests handed to the owner, ever */ bool stopping; Host *hosts; unsigned host_count; @@ -1328,6 +1329,7 @@ static void serve_bursts(Host *host) { if (!grow(&owner->jobs, &owner->capacity, owner->count + handed, sizeof(*owner->jobs))) abort(); for (size_t i = 0; i < count; ++i) if (!snapshot || host->jobs[i].route.kind != ROUTE_VIEW) owner->jobs[owner->count++] = host->jobs[i]; + owner->arrivals += handed; pthread_cond_signal(&owner->waiting); pthread_mutex_unlock(&owner->lock); host->pending += handed; @@ -1367,6 +1369,7 @@ static void *run_owner(void *context) { Job *spare = NULL; size_t spare_capacity = 0; bool gather = false; + uint64_t answered_at = 0; /* arrivals when the last round's answers were handed back */ for (;;) { pthread_mutex_lock(&owner->lock); /* Wait for requests, or until the next timer is due. */ @@ -1387,15 +1390,20 @@ static void *run_owner(void *context) { /* After a round that saved, its answers bring requests back from the * connection threads: gather them until none has come for 100 µs (a * millisecond at most), so they share one save as they would on a - * single thread, where answering takes that long anyway. */ + * single thread, where answering takes that long anyway. The quiet + * counts only once a request came after the answers: requests that + * waited out the save alone would otherwise start the next round + * without the clients just answered, and the clients would settle + * into two halves taking turns, each save carrying half of them. */ struct timespec now, end; clock_gettime(CLOCK_REALTIME, &now); end = later(now, 1000000); while (gather && owner->count && !owner->stopping && before(now, end)) { size_t seen = owner->count; + bool returned = owner->arrivals != answered_at; struct timespec quiet = later(now, 100000); - pthread_cond_timedwait(&owner->waiting, &owner->lock, before(quiet, end) ? &quiet : &end); - if (owner->count == seen) break; + pthread_cond_timedwait(&owner->waiting, &owner->lock, returned && before(quiet, end) ? &quiet : &end); + if (returned && owner->count == seen) break; clock_gettime(CLOCK_REALTIME, &now); } if (!owner->count) { @@ -1411,6 +1419,9 @@ static void *run_owner(void *context) { owner->executor.wrote = false; execute(&owner->executor, jobs, count, hand_back, owner); gather = owner->executor.wrote; + pthread_mutex_lock(&owner->lock); + answered_at = owner->arrivals; + pthread_mutex_unlock(&owner->lock); spare = jobs; spare_capacity = capacity; } -- cgit v1.2.3