1 #include <uwsgi.h>
2
3 /*
4
5 Busyness cheaper algorithm (by Łukasz Mierzwa)
6
7 */
8
9 extern struct uwsgi_server uwsgi;
10
11 // this global struct containes all of the relevant values
12 struct uwsgi_cheaper_busyness_global {
13 uint64_t busyness_max;
14 uint64_t busyness_min;
15 uint64_t *last_values;
16 uint64_t *current_busyness;
17 uint64_t total_avg_busyness;
18 int *was_busy;
19 uint64_t tcheck;
20 uint64_t last_cheaped; // last time worker was cheaped due to low busyness
21 uint64_t next_cheap; // timestamp, we can cheap worker after it
22 uint64_t penalty; // penalty for respawning to fast, it will be added to multiplier
23 uint64_t min_multi; //initial multiplier will be stored here
24 uint64_t cheap_multi; // current multiplier value
25 int last_action; // 1 - spawn workers ; 2 - cheap worker
26 int verbose; // 1 - show debug logs, 0 - only important
27 uint64_t tolerance_counter; // used to keep track of what to do if min <= busyness <= max for few cycles in row
28 int emergency_workers; // counts the number of running emergency workers
29 #ifdef __linux__
30 int backlog_alert;
31 int backlog_step;
32 uint64_t backlog_multi; // multiplier used to cheap emergency workers
33 uint64_t backlog_nonzero_alert;
34 int backlog_is_nonzero;
35 uint64_t backlog_nonzero_since; // since when backlog is > 0
36 #endif
37 } uwsgi_cheaper_busyness_global;
38
39 struct uwsgi_option uwsgi_cheaper_busyness_options[] = {
40
41 {"cheaper-busyness-max", required_argument, 0,
42 "set the cheaper busyness high percent limit, above that value worker is considered loaded (default 50)",
43 uwsgi_opt_set_64bit, &uwsgi_cheaper_busyness_global.busyness_max, 0},
44
45 {"cheaper-busyness-min", required_argument, 0,
46 "set the cheaper busyness low percent limit, below that value worker is considered idle (default 25)",
47 uwsgi_opt_set_64bit, &uwsgi_cheaper_busyness_global.busyness_min, 0},
48
49 {"cheaper-busyness-multiplier", required_argument, 0,
50 "set initial cheaper multiplier, worker needs to be idle for cheaper-overload*multiplier seconds to be cheaped (default 10)",
51 uwsgi_opt_set_64bit, &uwsgi_cheaper_busyness_global.cheap_multi, 0},
52
53 {"cheaper-busyness-penalty", required_argument, 0,
54 "penalty for respawning workers to fast, it will be added to the current multiplier value if worker is cheaped and than respawned back too fast (default 2)",
55 uwsgi_opt_set_64bit, &uwsgi_cheaper_busyness_global.penalty, 0},
56
57 {"cheaper-busyness-verbose", no_argument, 0, "enable verbose log messages from busyness algorithm",
58 uwsgi_opt_true, &uwsgi_cheaper_busyness_global.verbose, 0},
59
60 #ifdef __linux__
61 {"cheaper-busyness-backlog-alert", required_argument, 0,
62 "spawn emergency worker(s) if any time listen queue is higher than this value (default 33)",
63 uwsgi_opt_set_int, &uwsgi_cheaper_busyness_global.backlog_alert, 0},
64 {"cheaper-busyness-backlog-multiplier", required_argument, 0,
65 "set cheaper multiplier used for emergency workers (default 3)",
66 uwsgi_opt_set_64bit, &uwsgi_cheaper_busyness_global.backlog_multi, 0},
67 {"cheaper-busyness-backlog-step", required_argument, 0,
68 "number of emergency workers to spawn at a time (default 1)",
69 uwsgi_opt_set_int, &uwsgi_cheaper_busyness_global.backlog_step, 0},
70 {"cheaper-busyness-backlog-nonzero", required_argument, 0,
71 "spawn emergency worker(s) if backlog is > 0 for more then N seconds (default 60)",
72 uwsgi_opt_set_64bit, &uwsgi_cheaper_busyness_global.backlog_nonzero_alert, 0},
73 #endif
74
75 {0, 0, 0, 0, 0, 0 ,0},
76
77 };
78
79
80 // used to set time after when we allow to cheap workers
set_next_cheap_time(void)81 void set_next_cheap_time(void) {
82 uint64_t now = uwsgi_micros();
83
84 #ifdef __linux__
85 if (uwsgi_cheaper_busyness_global.emergency_workers > 0) {
86 // we have some emergency workers running, we will use minimum delay (2 cycles) to cheap workers
87 // to have quicker recovery from big but short load spikes
88 // otherwise we might wait a lot before cheaping all emergency workers
89 if (uwsgi_cheaper_busyness_global.verbose)
90 uwsgi_log("[busyness] %d emergency worker(s) running, using %llu seconds cheaper timer\n",
91 uwsgi_cheaper_busyness_global.emergency_workers, uwsgi.cheaper_overload*uwsgi_cheaper_busyness_global.backlog_multi);
92 uwsgi_cheaper_busyness_global.next_cheap = now + uwsgi.cheaper_overload*uwsgi_cheaper_busyness_global.backlog_multi*1000000;
93 } else {
94 #endif
95 // no emergency workers running, we use normal math for setting timer
96 uwsgi_cheaper_busyness_global.next_cheap = now + uwsgi.cheaper_overload*uwsgi_cheaper_busyness_global.cheap_multi*1000000;
97 #ifdef __linux__
98 }
99 #endif
100 }
101
102
decrease_multi(void)103 void decrease_multi(void) {
104 // will decrease multiplier but only down to initial value
105 if (uwsgi_cheaper_busyness_global.cheap_multi > uwsgi_cheaper_busyness_global.min_multi) {
106 uwsgi_cheaper_busyness_global.cheap_multi--;
107 uwsgi_log("[busyness] decreasing cheaper multiplier to %llu\n", uwsgi_cheaper_busyness_global.cheap_multi);
108 }
109 }
110
111
112 #ifdef __linux__
spawn_emergency_worker(int backlog)113 int spawn_emergency_worker(int backlog) {
114 // reset cheaper multiplier to minimum value so we can start cheaping workers sooner
115 // if this was just random spike
116 uwsgi_cheaper_busyness_global.cheap_multi = uwsgi_cheaper_busyness_global.min_multi;
117
118 // set last action to spawn
119 uwsgi_cheaper_busyness_global.last_action = 1;
120
121 int decheaped = 0;
122 int i;
123 for (i = 1; i <= uwsgi.numproc; i++) {
124 if (uwsgi.workers[i].cheaped == 1 && uwsgi.workers[i].pid == 0) {
125 decheaped++;
126 if (decheaped >= uwsgi_cheaper_busyness_global.backlog_step) break;
127 }
128 }
129
130 // reset cheap timer, so that we need to start counting idle cycles from zero
131 set_next_cheap_time();
132
133 if (decheaped > 0) {
134 uwsgi_cheaper_busyness_global.emergency_workers += decheaped;
135 uwsgi_log("[busyness] %d requests in listen queue, spawning %d emergency worker(s) (%d)!\n",
136 backlog, decheaped, uwsgi_cheaper_busyness_global.emergency_workers);
137 } else {
138 uwsgi_log("[busyness] %d requests in listen queue but we are already started maximum number of workers (%d)\n",
139 backlog, uwsgi.numproc);
140 }
141
142 return decheaped;
143 }
144 #endif
145
146
cheaper_busyness_algo(int can_spawn)147 int cheaper_busyness_algo(int can_spawn) {
148
149 int i;
150 // we use microseconds
151 uint64_t t = uwsgi.cheaper_overload*1000000;
152
153 int active_workers = 0;
154 uint64_t total_busyness = 0;
155 uint64_t avg_busyness = 0;
156
157 for (i = 0; i < uwsgi.numproc; i++) {
158 if (uwsgi.workers[i+1].cheaped == 0 && uwsgi.workers[i+1].pid > 0) {
159 active_workers++;
160 uwsgi_cheaper_busyness_global.was_busy[i] += uwsgi_worker_is_busy(i+1);
161 } else {
162 uwsgi_cheaper_busyness_global.was_busy[i] = 0;
163 }
164 }
165
166 #ifdef __linux__
167 int backlog = uwsgi.shared->backlog;
168 #endif
169
170 uint64_t now = uwsgi_micros();
171 if (now - uwsgi_cheaper_busyness_global.tcheck >= t) {
172 uwsgi_cheaper_busyness_global.tcheck = now;
173 for (i = 0; i < uwsgi.numproc; i++) {
174 if (uwsgi.workers[i+1].cheaped == 0 && uwsgi.workers[i+1].pid > 0) {
175 uint64_t percent = (( (uwsgi.workers[i+1].running_time-uwsgi_cheaper_busyness_global.last_values[i])*100)/t);
176 if (percent > 100) {
177 percent = 100;
178 }
179 else if (uwsgi.workers[i+1].running_time-uwsgi_cheaper_busyness_global.last_values[i] == 0 && percent == 0 && uwsgi_cheaper_busyness_global.was_busy[i] > 0) {
180 // running_time did not change but workers were busy
181 // this means that workers had response times > busyness check interval
182 if (uwsgi_cheaper_busyness_global.verbose) {
183 uwsgi_log("[busyness] worker %d was busy %d time(s) in last cycle but no request was completed during this time, marking as 100%% busy\n",
184 i+1, uwsgi_cheaper_busyness_global.was_busy[i]);
185 }
186 percent = 100;
187 }
188 uwsgi_cheaper_busyness_global.was_busy[i] = 0;
189 total_busyness += percent;
190 if (uwsgi_cheaper_busyness_global.verbose && active_workers > 1)
191 uwsgi_log("[busyness] worker nr %d %llus average busyness is at %llu%%\n",
192 i+1, uwsgi.cheaper_overload, percent);
193 if (uwsgi.has_metrics) {
194 // update metrics
195 uwsgi_wlock(uwsgi.metrics_lock);
196 uwsgi_cheaper_busyness_global.current_busyness[i] = percent;
197 uwsgi_rwunlock(uwsgi.metrics_lock);
198 }
199 }
200 uwsgi_cheaper_busyness_global.last_values[i] = uwsgi.workers[i+1].running_time;
201 }
202
203 avg_busyness = (active_workers ? total_busyness / active_workers : 0);
204 if (uwsgi.has_metrics) {
205 uwsgi_wlock(uwsgi.metrics_lock);
206 uwsgi_cheaper_busyness_global.total_avg_busyness = avg_busyness;
207 uwsgi_rwunlock(uwsgi.metrics_lock);
208 }
209
210 if (uwsgi_cheaper_busyness_global.verbose)
211 uwsgi_log("[busyness] %ds average busyness of %d worker(s) is at %d%%\n",
212 (int) uwsgi.cheaper_overload, (int) active_workers, (int) avg_busyness);
213
214 if (avg_busyness > uwsgi_cheaper_busyness_global.busyness_max) {
215
216 // we need to reset this to 0 since this is not idle cycle
217 uwsgi_cheaper_busyness_global.tolerance_counter = 0;
218
219 int decheaped = 0;
220 if (can_spawn) {
221 for (i = 1; i <= uwsgi.numproc; i++) {
222 if (uwsgi.workers[i].cheaped == 1 && uwsgi.workers[i].pid == 0) {
223 decheaped++;
224 if (decheaped >= uwsgi.cheaper_step) break;
225 }
226 }
227 }
228
229 if (decheaped > 0) {
230 // store information that we just spawned new workers
231 uwsgi_cheaper_busyness_global.last_action = 1;
232
233 // calculate number of seconds since last worker was cheaped
234 if ((now - uwsgi_cheaper_busyness_global.last_cheaped)/uwsgi.cheaper_overload/1000000 <= uwsgi_cheaper_busyness_global.cheap_multi) {
235 // worker was cheaped and then spawned back in less than current multiplier*cheaper_overload seconds
236 // we will increase the multiplier so that next time worker will need to wait longer before being cheaped
237 uwsgi_cheaper_busyness_global.cheap_multi += uwsgi_cheaper_busyness_global.penalty;
238 uwsgi_log("[busyness] worker(s) respawned to fast, increasing cheaper multiplier to %llu (+%llu)\n",
239 uwsgi_cheaper_busyness_global.cheap_multi, uwsgi_cheaper_busyness_global.penalty);
240 } else {
241 decrease_multi();
242 }
243
244 set_next_cheap_time();
245
246 uwsgi_log("[busyness] %llus average busyness is at %llu%%, will spawn %d new worker(s)\n",
247 uwsgi.cheaper_overload, avg_busyness, decheaped);
248 } else {
249 uwsgi_log("[busyness] %llus average busyness is at %llu%% but we already started maximum number of workers available with current limits (%d)\n",
250 uwsgi.cheaper_overload, avg_busyness, active_workers);
251 }
252
253 // return the maximum number of workers to spawn
254 return decheaped;
255
256 #ifdef __linux__
257 } else if (can_spawn && backlog > uwsgi_cheaper_busyness_global.backlog_alert && active_workers < uwsgi.numproc) {
258 return spawn_emergency_worker(backlog);
259 #endif
260
261 } else if (avg_busyness < uwsgi_cheaper_busyness_global.busyness_min) {
262
263 // with only 1 worker running there is no point in doing all that magic
264 if (active_workers == 1) return 0;
265
266 // we need to reset this to 0 since this is not idle cycle
267 uwsgi_cheaper_busyness_global.tolerance_counter = 0;
268
269 if (active_workers > uwsgi.cheaper_count) {
270 // cheap a worker if too much are running
271 if (now >= uwsgi_cheaper_busyness_global.next_cheap) {
272 // lower cheaper multiplier if this is subsequent cheap
273 if (uwsgi_cheaper_busyness_global.last_action == 2) decrease_multi();
274 set_next_cheap_time();
275
276 uwsgi_log("[busyness] %llus average busyness is at %llu%%, cheap one of %d running workers\n",
277 uwsgi.cheaper_overload, avg_busyness, (int) active_workers);
278 // store timestamp
279 uwsgi_cheaper_busyness_global.last_cheaped = uwsgi_micros();
280
281 // store information that last action performed was cheaping worker
282 uwsgi_cheaper_busyness_global.last_action = 2;
283
284 if (uwsgi_cheaper_busyness_global.emergency_workers > 0)
285 uwsgi_cheaper_busyness_global.emergency_workers--;
286
287 return -1;
288 } else if (uwsgi_cheaper_busyness_global.verbose)
289 uwsgi_log("[busyness] need to wait %llu more second(s) to cheap worker\n", (uwsgi_cheaper_busyness_global.next_cheap - now)/1000000);
290 }
291
292 } else {
293 // with only 1 worker running there is no point in doing all that magic
294 if (active_workers == 1) return 0;
295
296 if (uwsgi_cheaper_busyness_global.emergency_workers > 0)
297 // we had emergency workers running and we went down to the busyness
298 // level that is high enough to slow down cheaping workers at extra speed
299 uwsgi_cheaper_busyness_global.emergency_workers--;
300
301 // we have min <= busyness <= max we need to check what happened before
302
303 uwsgi_cheaper_busyness_global.tolerance_counter++;
304 if (uwsgi_cheaper_busyness_global.tolerance_counter >= 3) {
305 // we had three or more cycles when min <= busyness <= max, lets reset the cheaper timer
306 // this is to prevent workers from being cheaped if we had idle cycles for almost all
307 // time needed to cheap them, than a lot min<busy<max when we do not reset timer
308 // and then another idle cycle than would trigger cheaping
309 if (uwsgi_cheaper_busyness_global.verbose)
310 uwsgi_log("[busyness] %llus average busyness is at %llu%%, %llu non-idle cycle(s), resetting cheaper timer\n",
311 uwsgi.cheaper_overload, avg_busyness, uwsgi_cheaper_busyness_global.tolerance_counter);
312 set_next_cheap_time();
313 } else {
314 // we had < 3 idle cycles in a row so we won't reset idle timer yet since this might be just short load spike
315 // but we need to add cheaper-overload seconds to the cheaper timer so this cycle isn't counted as idle
316 if (uwsgi_cheaper_busyness_global.verbose)
317 uwsgi_log("[busyness] %llus average busyness is at %llu%%, %llu non-idle cycle(s), adjusting cheaper timer\n",
318 uwsgi.cheaper_overload, avg_busyness, uwsgi_cheaper_busyness_global.tolerance_counter);
319 uwsgi_cheaper_busyness_global.next_cheap += uwsgi.cheaper_overload*1000000;
320 }
321 }
322 }
323
324 #ifdef __linux__
325 else if (can_spawn && backlog > uwsgi_cheaper_busyness_global.backlog_alert && active_workers < uwsgi.numproc) {
326 // we check for backlog overload every cycle
327 return spawn_emergency_worker(backlog);
328 }
329 else if (backlog > 0) {
330 if (uwsgi_cheaper_busyness_global.backlog_is_nonzero) {
331 // backlog was > 0 last time, check timestamp and spawn workers if needed
332 if (can_spawn && (now - uwsgi_cheaper_busyness_global.backlog_nonzero_since)/1000000 >= uwsgi_cheaper_busyness_global.backlog_nonzero_alert) {
333 uwsgi_log("[busyness] backlog was non-zero for %llu second(s), spawning new worker(s)\n", (now - uwsgi_cheaper_busyness_global.backlog_nonzero_since)/1000000);
334 uwsgi_cheaper_busyness_global.backlog_nonzero_since = now;
335 return spawn_emergency_worker(backlog);
336 }
337 }
338 else {
339 // this is first > 0 pass, setup timer
340 if (uwsgi_cheaper_busyness_global.verbose)
341 uwsgi_log("[busyness] backlog is starting to fill (%d)\n", backlog);
342 uwsgi_cheaper_busyness_global.backlog_is_nonzero = 1;
343 uwsgi_cheaper_busyness_global.backlog_nonzero_since = now;
344 }
345 }
346 else if (uwsgi_cheaper_busyness_global.backlog_is_nonzero) {
347 if (uwsgi_cheaper_busyness_global.verbose)
348 uwsgi_log("[busyness] backlog is now empty\n");
349 uwsgi_cheaper_busyness_global.backlog_is_nonzero = 0;
350 }
351 #endif
352
353 return 0;
354 }
355
356
357 // registration hook
uwsgi_cheaper_register_busyness(void)358 void uwsgi_cheaper_register_busyness(void) {
359 uwsgi_register_cheaper_algo("busyness", cheaper_busyness_algo);
360 }
361
uwsgi_cheaper_busyness_init(void)362 static int uwsgi_cheaper_busyness_init(void) {
363 if (!uwsgi.requested_cheaper_algo || strcmp(uwsgi.requested_cheaper_algo, "busyness")) return 0;
364 // this happens on the first run, the required memory is allocated
365 uwsgi_cheaper_busyness_global.last_values = uwsgi_calloc(sizeof(uint64_t) * uwsgi.numproc);
366 uwsgi_cheaper_busyness_global.was_busy = uwsgi_calloc(sizeof(int) * uwsgi.numproc);
367
368 if (uwsgi.has_metrics) {
369 // allocate metrics memory
370 uwsgi_cheaper_busyness_global.current_busyness = uwsgi_calloc(sizeof(uint64_t) * uwsgi.numproc);
371 }
372
373 // set defaults
374 if (!uwsgi_cheaper_busyness_global.busyness_max) uwsgi_cheaper_busyness_global.busyness_max = 50;
375 if (!uwsgi_cheaper_busyness_global.busyness_min) uwsgi_cheaper_busyness_global.busyness_min = 25;
376 if (!uwsgi_cheaper_busyness_global.cheap_multi) uwsgi_cheaper_busyness_global.cheap_multi = 10;
377 if (!uwsgi_cheaper_busyness_global.penalty) uwsgi_cheaper_busyness_global.penalty = 2;
378
379 #ifdef __linux__
380 if (!uwsgi_cheaper_busyness_global.backlog_alert) uwsgi_cheaper_busyness_global.backlog_alert = 33;
381 if (!uwsgi_cheaper_busyness_global.backlog_multi) uwsgi_cheaper_busyness_global.backlog_multi = 3;
382 if (!uwsgi_cheaper_busyness_global.backlog_step) uwsgi_cheaper_busyness_global.backlog_step = 1;
383 if (!uwsgi_cheaper_busyness_global.backlog_nonzero_alert) uwsgi_cheaper_busyness_global.backlog_nonzero_alert = 60;
384 #endif
385
386 // store initial multiplier so we don't loose that value
387 uwsgi_cheaper_busyness_global.min_multi = uwsgi_cheaper_busyness_global.cheap_multi;
388 // since this is first run we will print current values
389 uwsgi_log("[busyness] settings: min=%llu%%, max=%llu%%, overload=%llu, multiplier=%llu, respawn penalty=%llu\n",
390 uwsgi_cheaper_busyness_global.busyness_min, uwsgi_cheaper_busyness_global.busyness_max,
391 uwsgi.cheaper_overload, uwsgi_cheaper_busyness_global.cheap_multi, uwsgi_cheaper_busyness_global.penalty);
392 #ifdef __linux__
393 uwsgi_log("[busyness] backlog alert is set to %d request(s), step is %d\n",
394 uwsgi_cheaper_busyness_global.backlog_alert, uwsgi_cheaper_busyness_global.backlog_step);
395 uwsgi_log("[busyness] backlog non-zero alert is set to %llu second(s)\n", uwsgi_cheaper_busyness_global.backlog_nonzero_alert);
396 #endif
397
398 // register metrics if enabled
399 if (uwsgi.has_metrics) {
400 int i;
401 char buf[4096];
402 char buf2[4096];
403 for (i = 0; i < uwsgi.numproc; i++) {
404 if (snprintf(buf, 4096, "worker.%d.plugin.cheaper_busyness.busyness", i+1) <= 0) {
405 uwsgi_log("[busyness] unable to register busyness metric for worker %d\n", i+1);
406 exit(1);
407 }
408 if (snprintf(buf2, 4096, "3.%d.100.1", i+1) <= 0) {
409 uwsgi_log("[busyness] unable to register busyness metric oid for worker %d\n", i+1);
410 exit(1);
411 }
412 uwsgi_register_metric(buf, buf2, UWSGI_METRIC_GAUGE, "ptr", &uwsgi_cheaper_busyness_global.current_busyness[i], 0, NULL);
413 }
414 uwsgi_register_metric("plugin.cheaper_busyness.total_avg_busyness", "4.100.1", UWSGI_METRIC_GAUGE, "ptr", &uwsgi_cheaper_busyness_global.total_avg_busyness, 0, NULL);
415 uwsgi_log("[busyness] metrics registered\n");
416 }
417
418 // initialize timers
419 uwsgi_cheaper_busyness_global.tcheck = uwsgi_micros();
420 set_next_cheap_time();
421
422 return 0;
423 }
424
425 struct uwsgi_plugin cheaper_busyness_plugin = {
426
427 .name = "cheaper_busyness",
428 .on_load = uwsgi_cheaper_register_busyness,
429 .options = uwsgi_cheaper_busyness_options,
430 .init = uwsgi_cheaper_busyness_init
431 };
432