##// END OF EJS Templates
config: update gunicorn config examples
super-admin -
r5272:ab333ff0 default
parent child Browse files
Show More
@@ -1,510 +1,518 b''
1 1 """
2 2 Gunicorn config extension and hooks. This config file adds some extra settings and memory management.
3 3 Gunicorn configuration should be managed by .ini files entries of RhodeCode or VCSServer
4 4 """
5 5
6 6 import gc
7 7 import os
8 8 import sys
9 9 import math
10 10 import time
11 11 import threading
12 12 import traceback
13 13 import random
14 14 import socket
15 15 import dataclasses
16 16 from gunicorn.glogging import Logger
17 17
18 18
19 19 def get_workers():
20 20 import multiprocessing
21 21 return multiprocessing.cpu_count() * 2 + 1
22 22
23 23
24 24 bind = "127.0.0.1:10020"
25 25
26 26
27 27 # Error logging output for gunicorn (-) is stdout
28 28 errorlog = '-'
29 29
30 30 # Access logging output for gunicorn (-) is stdout
31 31 accesslog = '-'
32 32
33 33
34 34 # SERVER MECHANICS
35 35 # None == system temp dir
36 36 # worker_tmp_dir is recommended to be set to some tmpfs
37 37 worker_tmp_dir = None
38 38 tmp_upload_dir = None
39 39
40 40 # use re-use port logic
41 41 #reuse_port = True
42 42
43 43 # Custom log format
44 44 #access_log_format = (
45 45 # '%(t)s %(p)s INFO [GNCRN] %(h)-15s rqt:%(L)s %(s)s %(b)-6s "%(m)s:%(U)s %(q)s" usr:%(u)s "%(f)s" "%(a)s"')
46 46
47 47 # loki format for easier parsing in grafana
48 48 access_log_format = (
49 49 'time="%(t)s" pid=%(p)s level="INFO" type="[GNCRN]" ip="%(h)-15s" rqt="%(L)s" response_code="%(s)s" response_bytes="%(b)-6s" uri="%(m)s:%(U)s %(q)s" user=":%(u)s" user_agent="%(a)s"')
50 50
51 51
52 52 # Sets the number of process workers. More workers means more concurrent connections
53 53 # RhodeCode can handle at the same time. Each additional worker also it increases
54 # memory usage as each has it's own set of caches.
55 # Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
54 # memory usage as each has its own set of caches.
55 # The Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
56 56 # than 8-10 unless for huge deployments .e.g 700-1000 users.
57 57 # `instance_id = *` must be set in the [app:main] section below (which is the default)
58 58 # when using more than 1 worker.
59 59 workers = 4
60 60
61 61 # self adjust workers based on CPU count, to use maximum of CPU and not overquota the resources
62 62 # workers = get_workers()
63 63
64 64 # Gunicorn access log level
65 65 loglevel = 'info'
66 66
67 67 # Process name visible in a process list
68 68 proc_name = 'rhodecode_enterprise'
69 69
70 # Type of worker class, one of `sync`, `gevent`
71 # currently `sync` is the only option allowed.
70 # Type of worker class, one of `sync`, `gevent` or `gthread`
71 # currently `sync` is the only option allowed for vcsserver and for rhodecode all of 3 are allowed
72 # gevent:
73 # In this case, the maximum number of concurrent requests is (N workers * X worker_connections)
74 # e.g. workers =3 worker_connections=10 = 3*10, 30 concurrent requests can be handled
75 # gtrhead:
76 # In this case, the maximum number of concurrent requests is (N workers * X threads)
77 # e.g. workers = 3 threads=3 = 3*3, 9 concurrent requests can be handled
72 78 worker_class = 'gevent'
73 79
74 80 # The maximum number of simultaneous clients. Valid only for gevent
81 # In this case, the maximum number of concurrent requests is (N workers * X worker_connections)
82 # e.g workers =3 worker_connections=10 = 3*10, 30 concurrent requests can be handled
75 83 worker_connections = 10
76 84
77 85 # Max number of requests that worker will handle before being gracefully restarted.
78 86 # Prevents memory leaks, jitter adds variability so not all workers are restarted at once.
79 87 max_requests = 2000
80 88 max_requests_jitter = int(max_requests * 0.2) # 20% of max_requests
81 89
82 90 # The maximum number of pending connections.
83 91 # Exceeding this number results in the client getting an error when attempting to connect.
84 92 backlog = 64
85 93
86 94 # The Amount of time a worker can spend with handling a request before it
87 95 # gets killed and restarted. By default, set to 21600 (6hrs)
88 96 # Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
89 97 timeout = 21600
90 98
91 99 # The maximum size of HTTP request line in bytes.
92 100 # 0 for unlimited
93 101 limit_request_line = 0
94 102
95 103 # Limit the number of HTTP headers fields in a request.
96 104 # By default this value is 100 and can't be larger than 32768.
97 105 limit_request_fields = 32768
98 106
99 107 # Limit the allowed size of an HTTP request header field.
100 108 # Value is a positive number or 0.
101 109 # Setting it to 0 will allow unlimited header field sizes.
102 110 limit_request_field_size = 0
103 111
104 112 # Timeout for graceful workers restart.
105 113 # After receiving a restart signal, workers have this much time to finish
106 114 # serving requests. Workers still alive after the timeout (starting from the
107 115 # receipt of the restart signal) are force killed.
108 116 # Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
109 117 graceful_timeout = 21600
110 118
111 119 # The number of seconds to wait for requests on a Keep-Alive connection.
112 120 # Generally set in the 1-5 seconds range.
113 121 keepalive = 2
114 122
115 123 # Maximum memory usage that each worker can use before it will receive a
116 124 # graceful restart signal 0 = memory monitoring is disabled
117 125 # Examples: 268435456 (256MB), 536870912 (512MB)
118 126 # 1073741824 (1GB), 2147483648 (2GB), 4294967296 (4GB)
119 127 # Dynamic formula 1024 * 1024 * 256 == 256MBs
120 128 memory_max_usage = 0
121 129
122 130 # How often in seconds to check for memory usage for each gunicorn worker
123 131 memory_usage_check_interval = 60
124 132
125 133 # Threshold value for which we don't recycle worker if GarbageCollection
126 134 # frees up enough resources. Before each restart, we try to run GC on worker
127 135 # in case we get enough free memory after that; restart will not happen.
128 136 memory_usage_recovery_threshold = 0.8
129 137
130 138
131 139 @dataclasses.dataclass
132 140 class MemoryCheckConfig:
133 141 max_usage: int
134 142 check_interval: int
135 143 recovery_threshold: float
136 144
137 145
138 146 def _get_process_rss(pid=None):
139 147 try:
140 148 import psutil
141 149 if pid:
142 150 proc = psutil.Process(pid)
143 151 else:
144 152 proc = psutil.Process()
145 153 return proc.memory_info().rss
146 154 except Exception:
147 155 return None
148 156
149 157
150 158 def _get_config(ini_path):
151 159 import configparser
152 160
153 161 try:
154 162 config = configparser.RawConfigParser()
155 163 config.read(ini_path)
156 164 return config
157 165 except Exception:
158 166 return None
159 167
160 168
161 169 def get_memory_usage_params(config=None):
162 170 # memory spec defaults
163 171 _memory_max_usage = memory_max_usage
164 172 _memory_usage_check_interval = memory_usage_check_interval
165 173 _memory_usage_recovery_threshold = memory_usage_recovery_threshold
166 174
167 175 if config:
168 176 ini_path = os.path.abspath(config)
169 177 conf = _get_config(ini_path)
170 178
171 179 section = 'server:main'
172 180 if conf and conf.has_section(section):
173 181
174 182 if conf.has_option(section, 'memory_max_usage'):
175 183 _memory_max_usage = conf.getint(section, 'memory_max_usage')
176 184
177 185 if conf.has_option(section, 'memory_usage_check_interval'):
178 186 _memory_usage_check_interval = conf.getint(section, 'memory_usage_check_interval')
179 187
180 188 if conf.has_option(section, 'memory_usage_recovery_threshold'):
181 189 _memory_usage_recovery_threshold = conf.getfloat(section, 'memory_usage_recovery_threshold')
182 190
183 191 _memory_max_usage = int(os.environ.get('RC_GUNICORN_MEMORY_MAX_USAGE', '')
184 192 or _memory_max_usage)
185 193 _memory_usage_check_interval = int(os.environ.get('RC_GUNICORN_MEMORY_USAGE_CHECK_INTERVAL', '')
186 194 or _memory_usage_check_interval)
187 195 _memory_usage_recovery_threshold = float(os.environ.get('RC_GUNICORN_MEMORY_USAGE_RECOVERY_THRESHOLD', '')
188 196 or _memory_usage_recovery_threshold)
189 197
190 198 return MemoryCheckConfig(_memory_max_usage, _memory_usage_check_interval, _memory_usage_recovery_threshold)
191 199
192 200
193 201 def _time_with_offset(check_interval):
194 202 return time.time() - random.randint(0, check_interval/2.0)
195 203
196 204
197 205 def pre_fork(server, worker):
198 206 pass
199 207
200 208
201 209 def post_fork(server, worker):
202 210
203 211 memory_conf = get_memory_usage_params()
204 212 _memory_max_usage = memory_conf.max_usage
205 213 _memory_usage_check_interval = memory_conf.check_interval
206 214 _memory_usage_recovery_threshold = memory_conf.recovery_threshold
207 215
208 216 worker._memory_max_usage = int(os.environ.get('RC_GUNICORN_MEMORY_MAX_USAGE', '')
209 217 or _memory_max_usage)
210 218 worker._memory_usage_check_interval = int(os.environ.get('RC_GUNICORN_MEMORY_USAGE_CHECK_INTERVAL', '')
211 219 or _memory_usage_check_interval)
212 220 worker._memory_usage_recovery_threshold = float(os.environ.get('RC_GUNICORN_MEMORY_USAGE_RECOVERY_THRESHOLD', '')
213 221 or _memory_usage_recovery_threshold)
214 222
215 223 # register memory last check time, with some random offset so we don't recycle all
216 224 # at once
217 225 worker._last_memory_check_time = _time_with_offset(_memory_usage_check_interval)
218 226
219 227 if _memory_max_usage:
220 228 server.log.info("pid=[%-10s] WORKER spawned with max memory set at %s", worker.pid,
221 229 _format_data_size(_memory_max_usage))
222 230 else:
223 231 server.log.info("pid=[%-10s] WORKER spawned", worker.pid)
224 232
225 233
226 234 def pre_exec(server):
227 235 server.log.info("Forked child, re-executing.")
228 236
229 237
230 238 def on_starting(server):
231 239 server_lbl = '{} {}'.format(server.proc_name, server.address)
232 240 server.log.info("Server %s is starting.", server_lbl)
233 241 server.log.info('Config:')
234 242 server.log.info(f"\n{server.cfg}")
235 243 server.log.info(get_memory_usage_params())
236 244
237 245
238 246 def when_ready(server):
239 247 server.log.info("Server %s is ready. Spawning workers", server)
240 248
241 249
242 250 def on_reload(server):
243 251 pass
244 252
245 253
246 254 def _format_data_size(size, unit="B", precision=1, binary=True):
247 255 """Format a number using SI units (kilo, mega, etc.).
248 256
249 257 ``size``: The number as a float or int.
250 258
251 259 ``unit``: The unit name in plural form. Examples: "bytes", "B".
252 260
253 261 ``precision``: How many digits to the right of the decimal point. Default
254 262 is 1. 0 suppresses the decimal point.
255 263
256 264 ``binary``: If false, use base-10 decimal prefixes (kilo = K = 1000).
257 265 If true, use base-2 binary prefixes (kibi = Ki = 1024).
258 266
259 267 ``full_name``: If false (default), use the prefix abbreviation ("k" or
260 268 "Ki"). If true, use the full prefix ("kilo" or "kibi"). If false,
261 269 use abbreviation ("k" or "Ki").
262 270
263 271 """
264 272
265 273 if not binary:
266 274 base = 1000
267 275 multiples = ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
268 276 else:
269 277 base = 1024
270 278 multiples = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
271 279
272 280 sign = ""
273 281 if size > 0:
274 282 m = int(math.log(size, base))
275 283 elif size < 0:
276 284 sign = "-"
277 285 size = -size
278 286 m = int(math.log(size, base))
279 287 else:
280 288 m = 0
281 289 if m > 8:
282 290 m = 8
283 291
284 292 if m == 0:
285 293 precision = '%.0f'
286 294 else:
287 295 precision = '%%.%df' % precision
288 296
289 297 size = precision % (size / math.pow(base, m))
290 298
291 299 return '%s%s %s%s' % (sign, size.strip(), multiples[m], unit)
292 300
293 301
294 302 def _check_memory_usage(worker):
295 303 _memory_max_usage = worker._memory_max_usage
296 304 if not _memory_max_usage:
297 305 return
298 306
299 307 _memory_usage_check_interval = worker._memory_usage_check_interval
300 308 _memory_usage_recovery_threshold = memory_max_usage * worker._memory_usage_recovery_threshold
301 309
302 310 elapsed = time.time() - worker._last_memory_check_time
303 311 if elapsed > _memory_usage_check_interval:
304 312 mem_usage = _get_process_rss()
305 313 if mem_usage and mem_usage > _memory_max_usage:
306 314 worker.log.info(
307 315 "memory usage %s > %s, forcing gc",
308 316 _format_data_size(mem_usage), _format_data_size(_memory_max_usage))
309 317 # Try to clean it up by forcing a full collection.
310 318 gc.collect()
311 319 mem_usage = _get_process_rss()
312 320 if mem_usage > _memory_usage_recovery_threshold:
313 321 # Didn't clean up enough, we'll have to terminate.
314 322 worker.log.warning(
315 323 "memory usage %s > %s after gc, quitting",
316 324 _format_data_size(mem_usage), _format_data_size(_memory_max_usage))
317 325 # This will cause worker to auto-restart itself
318 326 worker.alive = False
319 327 worker._last_memory_check_time = time.time()
320 328
321 329
322 330 def worker_int(worker):
323 331 worker.log.info("pid=[%-10s] worker received INT or QUIT signal", worker.pid)
324 332
325 333 # get traceback info, when a worker crashes
326 334 def get_thread_id(t_id):
327 335 id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
328 336 return id2name.get(t_id, "unknown_thread_id")
329 337
330 338 code = []
331 339 for thread_id, stack in sys._current_frames().items(): # noqa
332 340 code.append(
333 341 "\n# Thread: %s(%d)" % (get_thread_id(thread_id), thread_id))
334 342 for fname, lineno, name, line in traceback.extract_stack(stack):
335 343 code.append('File: "%s", line %d, in %s' % (fname, lineno, name))
336 344 if line:
337 345 code.append(" %s" % (line.strip()))
338 346 worker.log.debug("\n".join(code))
339 347
340 348
341 349 def worker_abort(worker):
342 350 worker.log.info("pid=[%-10s] worker received SIGABRT signal", worker.pid)
343 351
344 352
345 353 def worker_exit(server, worker):
346 354 worker.log.info("pid=[%-10s] worker exit", worker.pid)
347 355
348 356
349 357 def child_exit(server, worker):
350 358 worker.log.info("pid=[%-10s] worker child exit", worker.pid)
351 359
352 360
353 361 def pre_request(worker, req):
354 362 worker.start_time = time.time()
355 363 worker.log.debug(
356 364 "GNCRN PRE WORKER [cnt:%s]: %s %s", worker.nr, req.method, req.path)
357 365
358 366
359 367 def post_request(worker, req, environ, resp):
360 368 total_time = time.time() - worker.start_time
361 369 # Gunicorn sometimes has problems with reading the status_code
362 370 status_code = getattr(resp, 'status_code', '')
363 371 worker.log.debug(
364 372 "GNCRN POST WORKER [cnt:%s]: %s %s resp: %s, Load Time: %.4fs",
365 373 worker.nr, req.method, req.path, status_code, total_time)
366 374 _check_memory_usage(worker)
367 375
368 376
369 377 def _filter_proxy(ip):
370 378 """
371 379 Passed in IP addresses in HEADERS can be in a special format of multiple
372 380 ips. Those comma separated IPs are passed from various proxies in the
373 381 chain of request processing. The left-most being the original client.
374 382 We only care about the first IP which came from the org. client.
375 383
376 384 :param ip: ip string from headers
377 385 """
378 386 if ',' in ip:
379 387 _ips = ip.split(',')
380 388 _first_ip = _ips[0].strip()
381 389 return _first_ip
382 390 return ip
383 391
384 392
385 393 def _filter_port(ip):
386 394 """
387 395 Removes a port from ip, there are 4 main cases to handle here.
388 396 - ipv4 eg. 127.0.0.1
389 397 - ipv6 eg. ::1
390 398 - ipv4+port eg. 127.0.0.1:8080
391 399 - ipv6+port eg. [::1]:8080
392 400
393 401 :param ip:
394 402 """
395 403 def is_ipv6(ip_addr):
396 404 if hasattr(socket, 'inet_pton'):
397 405 try:
398 406 socket.inet_pton(socket.AF_INET6, ip_addr)
399 407 except socket.error:
400 408 return False
401 409 else:
402 410 return False
403 411 return True
404 412
405 413 if ':' not in ip: # must be ipv4 pure ip
406 414 return ip
407 415
408 416 if '[' in ip and ']' in ip: # ipv6 with port
409 417 return ip.split(']')[0][1:].lower()
410 418
411 419 # must be ipv6 or ipv4 with port
412 420 if is_ipv6(ip):
413 421 return ip
414 422 else:
415 423 ip, _port = ip.split(':')[:2] # means ipv4+port
416 424 return ip
417 425
418 426
419 427 def get_ip_addr(environ):
420 428 proxy_key = 'HTTP_X_REAL_IP'
421 429 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
422 430 def_key = 'REMOTE_ADDR'
423 431
424 432 def _filters(x):
425 433 return _filter_port(_filter_proxy(x))
426 434
427 435 ip = environ.get(proxy_key)
428 436 if ip:
429 437 return _filters(ip)
430 438
431 439 ip = environ.get(proxy_key2)
432 440 if ip:
433 441 return _filters(ip)
434 442
435 443 ip = environ.get(def_key, '0.0.0.0')
436 444 return _filters(ip)
437 445
438 446
439 447 class RhodeCodeLogger(Logger):
440 448 """
441 449 Custom Logger that allows some customization that gunicorn doesn't allow
442 450 """
443 451
444 452 datefmt = r"%Y-%m-%d %H:%M:%S"
445 453
446 454 def __init__(self, cfg):
447 455 Logger.__init__(self, cfg)
448 456
449 457 def now(self):
450 458 """ return date in RhodeCode Log format """
451 459 now = time.time()
452 460 msecs = int((now - int(now)) * 1000)
453 461 return time.strftime(self.datefmt, time.localtime(now)) + '.{0:03d}'.format(msecs)
454 462
455 463 def atoms(self, resp, req, environ, request_time):
456 464 """ Gets atoms for log formatting.
457 465 """
458 466 status = resp.status
459 467 if isinstance(status, str):
460 468 status = status.split(None, 1)[0]
461 469 atoms = {
462 470 'h': get_ip_addr(environ),
463 471 'l': '-',
464 472 'u': self._get_user(environ) or '-',
465 473 't': self.now(),
466 474 'r': "%s %s %s" % (environ['REQUEST_METHOD'],
467 475 environ['RAW_URI'],
468 476 environ["SERVER_PROTOCOL"]),
469 477 's': status,
470 478 'm': environ.get('REQUEST_METHOD'),
471 479 'U': environ.get('PATH_INFO'),
472 480 'q': environ.get('QUERY_STRING'),
473 481 'H': environ.get('SERVER_PROTOCOL'),
474 482 'b': getattr(resp, 'sent', None) is not None and str(resp.sent) or '-',
475 483 'B': getattr(resp, 'sent', None),
476 484 'f': environ.get('HTTP_REFERER', '-'),
477 485 'a': environ.get('HTTP_USER_AGENT', '-'),
478 486 'T': request_time.seconds,
479 487 'D': (request_time.seconds * 1000000) + request_time.microseconds,
480 488 'M': (request_time.seconds * 1000) + int(request_time.microseconds/1000),
481 489 'L': "%d.%06d" % (request_time.seconds, request_time.microseconds),
482 490 'p': "<%s>" % os.getpid()
483 491 }
484 492
485 493 # add request headers
486 494 if hasattr(req, 'headers'):
487 495 req_headers = req.headers
488 496 else:
489 497 req_headers = req
490 498
491 499 if hasattr(req_headers, "items"):
492 500 req_headers = req_headers.items()
493 501
494 502 atoms.update({"{%s}i" % k.lower(): v for k, v in req_headers})
495 503
496 504 resp_headers = resp.headers
497 505 if hasattr(resp_headers, "items"):
498 506 resp_headers = resp_headers.items()
499 507
500 508 # add response headers
501 509 atoms.update({"{%s}o" % k.lower(): v for k, v in resp_headers})
502 510
503 511 # add environ variables
504 512 environ_variables = environ.items()
505 513 atoms.update({"{%s}e" % k.lower(): v for k, v in environ_variables})
506 514
507 515 return atoms
508 516
509 517
510 518 logger_class = RhodeCodeLogger
General Comments 0
You need to be logged in to leave comments. Login now