diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-11-02 20:39:26 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-11-02 20:39:26 +0100 |
| commit | 8f520e52d7d4081a0835374c7a448116baac64ab (patch) | |
| tree | fdb3c2e8d49aa8c94d6fff357ef2d18f3e0e553c | |
| parent | 7419a9524754dfc4222de2c4e4c22808b571477d (diff) | |
Turn the bucket count into a knob, but keep cleaner wake-ups at a constant rate
| -rw-r--r-- | DESIGN | 37 | ||||
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | ot_clean.c | 38 | ||||
| -rw-r--r-- | ot_clean.h | 13 | ||||
| -rw-r--r-- | tests/t_mutex.c | 2 | ||||
| -rw-r--r-- | trackerlogic.h | 5 |
6 files changed, 82 insertions, 19 deletions
| @@ -16,7 +16,7 @@ A connection carries one request and closes. Keep-alive works and is rarely | |||
| 16 | asked for. | 16 | asked for. |
| 17 | 17 | ||
| 18 | Statistics are approximate on purpose. Counters are per thread and summed | 18 | Statistics are approximate on purpose. Counters are per thread and summed |
| 19 | when read, so a /stats read can under-report once. Nothing drifts. | 19 | when read, so a /stats read can under-report once. |
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | Decisions | 22 | Decisions |
| @@ -60,9 +60,44 @@ connections, the filter serves more requests than no filter, not fewer, so it | |||
| 60 | is not the denial of service vector that FreeBSD-SA-02:26 once described. | 60 | is not the denial of service vector that FreeBSD-SA-02:26 once described. |
| 61 | 61 | ||
| 62 | 62 | ||
| 63 | The torrent table is split into 2^OT_BUCKET_COUNT_BITS buckets, 1024 by | ||
| 64 | default, set from the Makefile. Inserting a torrent opens a slot in a sorted | ||
| 65 | vector, so the bytes moved grow with the bucket's occupancy. Insert cost per | ||
| 66 | torrent, Linux: | ||
| 67 | |||
| 68 | 2^10 2^12 2^14 2^16 | ||
| 69 | 200k 466 ns 439 ns | ||
| 70 | 1M 834 ns 579 ns 529 ns | ||
| 71 | 4M 2348 ns 1163 ns 770 ns 674 ns | ||
| 72 | |||
| 73 | Re-announcing an existing torrent moves by under ten per cent over the same | ||
| 74 | range, so the cost is paid while the torrent population is built and not in | ||
| 75 | steady state. Below a few hundred thousand torrents there is nothing to win. | ||
| 76 | 2^14 captures most of the gain for two per cent more resident memory. | ||
| 77 | |||
| 78 | The cleaner's wake rate does not follow the bucket count. A cap of ten wakes | ||
| 79 | a second sets the sleep, and a wake cleans as many buckets as it must to | ||
| 80 | finish a sweep in OT_CLEAN_INTERVAL_MINUTES. At the default bucket count that | ||
| 81 | is one bucket every 117 ms. | ||
| 82 | |||
| 83 | |||
| 63 | Paths not taken | 84 | Paths not taken |
| 64 | --------------- | 85 | --------------- |
| 65 | 86 | ||
| 87 | A keyed hash for the bucket index. Info hashes are SHA-1 output and already | ||
| 88 | uniform, so it changes no distribution. It would buy resistance to deliberate | ||
| 89 | bucket flooding and cost the ordering a full scrape depends on: buckets are | ||
| 90 | ordered by hash prefix and sorted within, so walking them emits the sorted | ||
| 91 | dictionary bencode wants, with no sort and no copy. | ||
| 92 | |||
| 93 | Cache line padding for the per-bucket locks and vector headers. The | ||
| 94 | contention it was meant to remove is not there. On glibc, where a 48-byte | ||
| 95 | mutex straddles a line, padding gains 3 to 8 per cent, but gains as much at | ||
| 96 | one thread where no false sharing can exist, and scaling from one to eight | ||
| 97 | threads is identical either way. On FreeBSD, where eight 8-byte mutexes share | ||
| 98 | a line and the risk should be highest, padding is 15 per cent slower at one | ||
| 99 | thread and neutral to 6 per cent slower at four and eight. | ||
| 100 | |||
| 66 | accf_http, the HTTP-aware accept filter. It duplicates a parser the tracker | 101 | accf_http, the HTTP-aware accept filter. It duplicates a parser the tracker |
| 67 | has anyway, puts more kernel code in the path, and holds a connection that | 102 | has anyway, puts more kernel code in the path, and holds a connection that |
| 68 | sends "GET /" and stops, which accf_data hands straight over. | 103 | sends "GET /" and stops, which accf_data hands straight over. |
| @@ -20,6 +20,12 @@ STRIP?=strip | |||
| 20 | #FEATURES+=-DWANT_DEV_RANDOM | 20 | #FEATURES+=-DWANT_DEV_RANDOM |
| 21 | FEATURES+=-DWANT_FULLSCRAPE | 21 | FEATURES+=-DWANT_FULLSCRAPE |
| 22 | 22 | ||
| 23 | # Buckets the torrent table is split into, as a power of two, 8 to 16. More | ||
| 24 | # buckets make inserting a new torrent cheaper and cost memory, which is worth | ||
| 25 | # it for a tracker carrying millions of torrents and not for one carrying | ||
| 26 | # thousands. DESIGN has the measurements. | ||
| 27 | FEATURES+=-DOT_BUCKET_COUNT_BITS=10 | ||
| 28 | |||
| 23 | #FEATURES+=-D_DEBUG_HTTPERROR | 29 | #FEATURES+=-D_DEBUG_HTTPERROR |
| 24 | #FEATURES+=-D_DEBUG_RANDOMTORRENTS | 30 | #FEATURES+=-D_DEBUG_RANDOMTORRENTS |
| 25 | 31 | ||
| @@ -125,26 +125,34 @@ static void clean_sleep(void) { | |||
| 125 | pthread_mutex_unlock(&g_clean_mutex); | 125 | pthread_mutex_unlock(&g_clean_mutex); |
| 126 | } | 126 | } |
| 127 | 127 | ||
| 128 | /* Clean up all peers in current bucket, remove timedout pools and | 128 | /* Clean up all peers in one bucket, remove timedout pools and torrents */ |
| 129 | torrents */ | 129 | static void clean_bucket(size_t bucket) { |
| 130 | ot_vector *torrents_list = mutex_bucket_lock((int)bucket); | ||
| 131 | size_t toffs; | ||
| 132 | int delta_torrentcount = 0; | ||
| 133 | |||
| 134 | for (toffs = 0; toffs < torrents_list->size; ++toffs) { | ||
| 135 | ot_torrent *torrent = ((ot_torrent *)(torrents_list->data)) + toffs; | ||
| 136 | if (clean_single_torrent(torrent)) { | ||
| 137 | vector_remove_torrent(torrents_list, torrent); | ||
| 138 | --delta_torrentcount; | ||
| 139 | --toffs; | ||
| 140 | } | ||
| 141 | } | ||
| 142 | mutex_bucket_unlock((int)bucket, delta_torrentcount); | ||
| 143 | } | ||
| 144 | |||
| 130 | static void *clean_worker(void *args) { | 145 | static void *clean_worker(void *args) { |
| 131 | (void)args; | 146 | (void)args; |
| 132 | while (1) { | 147 | while (1) { |
| 133 | size_t bucket = OT_BUCKET_COUNT; | 148 | size_t bucket = OT_BUCKET_COUNT; |
| 134 | while (bucket--) { | 149 | while (bucket) { |
| 135 | ot_vector *torrents_list = mutex_bucket_lock(bucket); | 150 | size_t todo = bucket < OT_CLEAN_BUCKETS_PER_WAKE ? bucket : OT_CLEAN_BUCKETS_PER_WAKE; |
| 136 | size_t toffs; | 151 | |
| 137 | int delta_torrentcount = 0; | 152 | /* One bucket is locked at a time, however many a wake gets through */ |
| 153 | while (todo--) | ||
| 154 | clean_bucket(--bucket); | ||
| 138 | 155 | ||
| 139 | for (toffs = 0; toffs < torrents_list->size; ++toffs) { | ||
| 140 | ot_torrent *torrent = ((ot_torrent *)(torrents_list->data)) + toffs; | ||
| 141 | if (clean_single_torrent(torrent)) { | ||
| 142 | vector_remove_torrent(torrents_list, torrent); | ||
| 143 | --delta_torrentcount; | ||
| 144 | --toffs; | ||
| 145 | } | ||
| 146 | } | ||
| 147 | mutex_bucket_unlock(bucket, delta_torrentcount); | ||
| 148 | if (!g_opentracker_running) | 156 | if (!g_opentracker_running) |
| 149 | return NULL; | 157 | return NULL; |
| 150 | clean_sleep(); | 158 | clean_sleep(); |
| @@ -9,8 +9,17 @@ | |||
| 9 | /* The amount of time a clean cycle should take */ | 9 | /* The amount of time a clean cycle should take */ |
| 10 | #define OT_CLEAN_INTERVAL_MINUTES 2 | 10 | #define OT_CLEAN_INTERVAL_MINUTES 2 |
| 11 | 11 | ||
| 12 | /* So after each bucket wait 1 / OT_BUCKET_COUNT intervals */ | 12 | /* How often the cleaner may wake. A full sweep takes OT_CLEAN_INTERVAL_MINUTES. |
| 13 | #define OT_CLEAN_SLEEP (((OT_CLEAN_INTERVAL_MINUTES) * 60 * 1000000) / (OT_BUCKET_COUNT)) | 13 | The clean operations per wake is goverend by OT_BUCKET_COUNT_BITS, |
| 14 | set by the Makefile. | ||
| 15 | At the default of 1024 buckets this works out at one bucket every | ||
| 16 | 117 ms. */ | ||
| 17 | #define OT_CLEAN_MAX_WAKES_PER_SECOND 10 | ||
| 18 | |||
| 19 | #define OT_CLEAN_WAKE_BUDGET ((OT_CLEAN_INTERVAL_MINUTES) * 60 * (OT_CLEAN_MAX_WAKES_PER_SECOND)) | ||
| 20 | #define OT_CLEAN_BUCKETS_PER_WAKE (((OT_BUCKET_COUNT) + (OT_CLEAN_WAKE_BUDGET) - 1) / (OT_CLEAN_WAKE_BUDGET)) | ||
| 21 | #define OT_CLEAN_WAKES ((OT_BUCKET_COUNT) / (OT_CLEAN_BUCKETS_PER_WAKE)) | ||
| 22 | #define OT_CLEAN_SLEEP (((OT_CLEAN_INTERVAL_MINUTES) * 60 * 1000000) / (OT_CLEAN_WAKES)) | ||
| 14 | 23 | ||
| 15 | void clean_init(void); | 24 | void clean_init(void); |
| 16 | void clean_deinit(void); | 25 | void clean_deinit(void); |
diff --git a/tests/t_mutex.c b/tests/t_mutex.c index e54e2fa..c0c41cc 100644 --- a/tests/t_mutex.c +++ b/tests/t_mutex.c | |||
| @@ -24,7 +24,7 @@ static void hash_for_bucket(ot_hash hash, int bucket) { | |||
| 24 | } | 24 | } |
| 25 | 25 | ||
| 26 | static void case_a_hash_names_its_bucket(void) { | 26 | static void case_a_hash_names_its_bucket(void) { |
| 27 | static const int buckets[] = {0, 1, 2, 17, 511, 512, OT_BUCKET_COUNT - 1}; | 27 | static const int buckets[] = {0, 1, 2, 17, OT_BUCKET_COUNT / 2 - 1, OT_BUCKET_COUNT / 2, OT_BUCKET_COUNT - 1}; |
| 28 | size_t i; | 28 | size_t i; |
| 29 | 29 | ||
| 30 | test_case("a hash is served by the bucket its leading bits name"); | 30 | test_case("a hash is served by the bucket its leading bits name"); |
diff --git a/trackerlogic.h b/trackerlogic.h index 338e4e6..7292d36 100644 --- a/trackerlogic.h +++ b/trackerlogic.h | |||
| @@ -62,7 +62,12 @@ typedef struct { | |||
| 62 | 62 | ||
| 63 | /* We maintain a list of 1024 pointers to sorted list of ot_torrent structs | 63 | /* We maintain a list of 1024 pointers to sorted list of ot_torrent structs |
| 64 | Sort key is, of course, its hash */ | 64 | Sort key is, of course, its hash */ |
| 65 | /* Set from the Makefile, 8 to 16. More buckets means shorter vectors and a | ||
| 66 | cheaper insert, which only pays for a tracker carrying millions of | ||
| 67 | torrents; see DESIGN. */ | ||
| 68 | #ifndef OT_BUCKET_COUNT_BITS | ||
| 65 | #define OT_BUCKET_COUNT_BITS 10 | 69 | #define OT_BUCKET_COUNT_BITS 10 |
| 70 | #endif | ||
| 66 | 71 | ||
| 67 | #define OT_BUCKET_COUNT (1 << OT_BUCKET_COUNT_BITS) | 72 | #define OT_BUCKET_COUNT (1 << OT_BUCKET_COUNT_BITS) |
| 68 | #define OT_BUCKET_COUNT_SHIFT (32 - OT_BUCKET_COUNT_BITS) | 73 | #define OT_BUCKET_COUNT_SHIFT (32 - OT_BUCKET_COUNT_BITS) |
