diff options
| -rw-r--r-- | DESIGN | 99 |
1 files changed, 99 insertions, 0 deletions
| @@ -0,0 +1,99 @@ | |||
| 1 | opentracker design notes | ||
| 2 | ======================== | ||
| 3 | |||
| 4 | Decisions that cost measurement to reach, and the alternatives that were | ||
| 5 | tried and rejected. | ||
| 6 | |||
| 7 | What the tracker is optimised for | ||
| 8 | --------------------------------- | ||
| 9 | |||
| 10 | UDP carries most of the traffic. An HTTP request costs about ten times a UDP | ||
| 11 | announce, 26 microseconds of tracker CPU against 2.5. Nearly all of that is | ||
| 12 | the kernel setting up and tearing down a connection rather than tracker | ||
| 13 | code. | ||
| 14 | |||
| 15 | A connection carries one request and closes. Keep-alive works and is rarely | ||
| 16 | asked for. | ||
| 17 | |||
| 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. | ||
| 20 | |||
| 21 | |||
| 22 | Decisions | ||
| 23 | --------- | ||
| 24 | |||
| 25 | One UDP socket per worker, where the kernel spreads datagrams over a group | ||
| 26 | bound to one address. Cost per datagram then stays flat as workers are added, | ||
| 27 | 0.71 to 0.72 microseconds from one worker to eight. Sharing one socket grows | ||
| 28 | it eightfold over the same range, 0.71 to 6.07. | ||
| 29 | |||
| 30 | listen.udp.workers defaults to 1. One worker beats none by 19 to 37 per cent | ||
| 31 | everywhere, because a blocking read costs one system call where the event | ||
| 32 | loop costs two. Beyond one the gain depends on the kernel balancing, so | ||
| 33 | raising it is the operator's decision. | ||
| 34 | |||
| 35 | A thread parked in a blocking read is woken by a signal, not by closing or | ||
| 36 | shutting down its socket: | ||
| 37 | |||
| 38 | mechanism Linux FreeBSD macOS | ||
| 39 | close() blocked blocked 0 ms | ||
| 40 | shutdown(SHUT_RD) 1 ms 1 ms blocked | ||
| 41 | SO_RCVTIMEO works works works | ||
| 42 | pthread_kill 1 ms 1 ms 1 ms | ||
| 43 | |||
| 44 | Only the signal works everywhere. A receive timeout also works, but wakes | ||
| 45 | every thread on every tick for the life of the process to catch something | ||
| 46 | that happens once: at eight workers it doubled idle context switches, 255 to | ||
| 47 | 495 over thirty seconds. | ||
| 48 | |||
| 49 | Each binary is built in one compiler invocation. Without object files a build | ||
| 50 | cannot link objects compiled with different FEATURES, which was a real | ||
| 51 | failure mode. It costs incremental rebuilds, 0.075 seconds becoming 1.56. | ||
| 52 | |||
| 53 | A socket is read or written, never both. http_interest_set replaces an | ||
| 54 | add-and-drop pair so that the invariant cannot be broken by accident, and the | ||
| 55 | event layer hears about a change once instead of twice. | ||
| 56 | |||
| 57 | Accept waits until the client's request has arrived: TCP_DEFER_ACCEPT on | ||
| 58 | Linux, the accf_data filter on FreeBSD. Measured against a flood of silent | ||
| 59 | 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. | ||
| 61 | |||
| 62 | |||
| 63 | Paths not taken | ||
| 64 | --------------- | ||
| 65 | |||
| 66 | 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 | ||
| 68 | sends "GET /" and stops, which accf_data hands straight over. | ||
| 69 | |||
| 70 | recvmmsg and sendmmsg. No measurable gain on either FreeBSD machine, between | ||
| 71 | -4 and +10 per cent, and they do not exist on Darwin. | ||
| 72 | |||
| 73 | Link time optimisation. It cuts .text by 13 per cent, 93,497 bytes to 81,630, | ||
| 74 | and changes throughput not at all. A tracker waits on memory and system | ||
| 75 | calls, not on instruction fetch: the announce path is 6.6 KB of code against | ||
| 76 | a 32 KB instruction cache at worst. | ||
| 77 | |||
| 78 | A directory for object files. bmake has no pattern rules and the Makefile | ||
| 79 | builds under FreeBSD's make today. Building each binary in one invocation | ||
| 80 | clears the objects out of the tree without giving that up. | ||
| 81 | |||
| 82 | A src/ directory. 52 files sharing an ot_ prefix are a module list rather | ||
| 83 | than clutter, and build output was most of what made the tree look crowded. | ||
| 84 | |||
| 85 | |||
| 86 | Platform differences that cost us code | ||
| 87 | -------------------------------------- | ||
| 88 | |||
| 89 | SO_REUSEPORT means three different things. Linux spreads datagrams over the | ||
| 90 | group; FreeBSD permits the bind and needs SO_REUSEPORT_LB to spread. Darwin | ||
| 91 | hands every datagram to whichever socket bound last, so a group there starves | ||
| 92 | all but one reader. | ||
| 93 | |||
| 94 | Waking a blocked reader differs per kernel. see the table above. | ||
| 95 | |||
| 96 | Deferring accept is TCP_DEFER_ACCEPT, or accf_data, or nothing. | ||
| 97 | |||
| 98 | make is bmake on FreeBSD and GNU make elsewhere, and the Makefile has to | ||
| 99 | build under both. | ||
