Uwsgi multihosting s monitoringom cez grafana

32 minút

Nastavenie webového servera pre python aplikácie môže byť celkom náročná úloha hlavne kvôli nedostatku dostatočne komplexných návodov. V tomto článku skúsim vysvetliť ako ja nastavujem servery.

Ako ser­ver som zvo­lil Ubun­tu Ser­ver 23.10. Po­stup by mal s ma­lý­mi zme­na­mi fun­go­vať prak­tic­ky na ľu­bo­voľ­nej dis­tri­bú­cii za­lo­že­nej na De­bian Li­nu­xe. Všet­ky prí­ka­zy bu­dú vy­ko­ná­va­né ak to ne­bu­de uve­de­né inak pod po­u­ží­va­te­ľom root. V prí­ka­zoch pre­to ne­bu­dem uvá­dzať sudo a bu­dem pred­po­kla­dať, že po­u­ží­va­teľ je pria­mo pri­hlá­se­ný ako ro­ot (na­prí­klad prí­ka­zom sudo su).

Vytváranie používateľov a adresárovej štruktúry

Na ser­ve­ri bu­de nie­koľ­ko pro­jek­tov a po­ten­ciál­ne aj nie­koľ­ko po­u­ží­va­te­ľov. Pre­to som na­vrhol na­sle­du­jú­ci spô­sob sprá­vy po­u­ží­va­te­ľov a ich ad­re­sá­rov:

Pre strán­ku linuxos.sk by iš­lo o na­sle­du­jú­ce prí­ka­zy:

# vytvorenie skupiny
groupadd linuxos-team

# vytvorenie užívateľského adresára (s jedným podadresárom web)
mkdir -p /var/www/clients/linuxos-team/linuxos.sk/web

# zmena práv pre home adresár
chown linuxos.sk:linuxos-team /var/www/clients/linuxos-team/linuxos.sk/

# pridanie používateľa bez možnosti prihlásenia
useradd -d /var/www/clients/linuxos-team/linuxos.sk -g linuxos-team -M -s /bin/false linuxos.sk

# pridanie používateľa do skupiny www-data
usermod -a -G www-data linuxos.sk

# vytvorenie odkazu
ln -s /var/www/clients/linuxos-team/linuxos.sk /var/www/linuxos.sk

Keď­že pro­ces pri­dá­va­nia po­u­ží­va­te­ľov je váž­ne nud­ný a re­pe­ti­tív­ny a my prog­ra­má­to­ri ne­ra­di ro­bí­me tie is­té ve­ci stá­le a stá­le zno­vu ne­ho­vo­riac o vy­hľa­dá­va­ní ná­vo­du, pri­pra­vil som si bash skript, kto­rý tú­to úlo­hu vy­rie­ši za mňa. Po­ve­dz­me, že na­sle­du­jú­ci skript si po­me­nu­jem web_user_add:

#!/bin/bash

# Nápoveda
if [ "$#" -ne 3 ]; then
	echo "web_add host system_user system_group"
	exit -1
fi

host=$1
sys_user=$2
sys_group=$3

# Nie je vytvorená skupina? Vytvoríme
if [[ ! $(getent group $sys_group) ]]; then
	groupadd $sys_group
	usermod -a -G $sys_group www-data
fi

# Nie je vytvorený používateľ? Vytvoríme
if [[ ! $(getent passwd $sys_user) ]]; then
	mkdir -p /var/www/clients/$sys_group/$sys_user/web
	chown $sys_user:$sys_group -R /var/www/clients/$sys_group/$sys_user
	useradd -d /var/www/clients/$sys_group/$sys_user -g $sys_group -M -s /bin/false $sys_user
	usermod -a -G www-data $sys_user
fi

# Vytvorenie odkazu
ln -s /var/www/clients/$sys_group/$sys_user /var/www/$host

Te­raz už sta­čí len spus­tiť ./web_user_add linuxos.sk linuxos.sk linuxos-team.

Uwsgi server

Zvy­čaj­ne pre­vádz­ku­jem kla­sic­ké Web Ser­ver Ga­te­way In­ter­fa­ce ap­li­ká­cie. Exis­tu­je množ­stvo čis­tých ws­gi, ale­bo kom­bi­no­va­ných as­gi / ws­gi ser­ve­rov. Ja kon­krét­ne po­u­ží­vam uWS­GI, čo je dnes po­va­žo­va­né už prak­tic­ky za ne­u­dr­žia­va­nú vy­ko­páv­ku. Má však veľ­mi zau­jí­ma­vé vlast­nos­ti, kto­ré som v iných ser­ve­roch ne­na­šiel, na­prí­klad pod­po­ru soc­ke­to­vej ak­ti­vá­cie, au­to­ma­tic­ké šká­lo­va­nie, mo­ni­to­ring, rou­ting, plá­no­va­nie úloh na po­za­dí atď.

Naj­jed­no­duch­šou voľ­bou, ako za­čať s mul­ti-app pre­vádz­kou je uWS­GI Em­pe­ror. Ten­to re­žim umož­ňu­je spra­vo­vať nie­koľ­ko ap­li­ká­cii sú­bež­ne. Zá­ro­veň zvlá­da au­to­ma­tic­ké spúš­ťa­nie no­vých wor­ke­rov pri zá­ťa­ži, či na­opak ich vy­pí­na­nie v prí­pa­de, že už nie sú po­treb­ní. Ako če­reš­nič­ku na tor­te by som spo­me­nul mož­nosť che­ap, pri kto­rej sa len vy­tvo­rí soc­ket a pr­vý wor­ker sa spus­tí až pri po­žia­dav­ke.

Do­sť bo­lo mar­ke­tin­go­vé­ho blá­bo­lu, na­stal čas vy­hr­núť si ru­ká­vy a kon­fi­gu­ro­vať. Upo­zor­ňu­jem, že tá­to časť nie je nič pre slab­šie po­va­hy a bu­de za­hŕňať po­mer­ne po­kro­či­lé tech­ni­ky, na kto­ré au­tor pri­šiel pri všet­kej skrom­nos­ti sám a stá­lo ho to ne­ma­lé úsi­lie.

Za­čnem in­šta­lá­ci­ou uwsgi ser­ve­ru, kto­rá je na dis­tri­bú­ciach za­lo­že­ných na de­bia­ne veľ­mi jed­no­du­chá - apt install uwsgi-plugin-python3.

Kon­fi­gu­rá­ciu za­čnem pr­vým a hlav­ným kon­fi­gu­rač­ným sú­bo­rom pre uws­gi em­pe­ror - /etc/uwsgi/emperor.ini. Sú­bor od­ka­zu­je na ďal­šie kon­fi­gu­rač­né sú­bo­ry a ná­stro­je, kto­ré si po­stup­ne prej­de­me. Tu je už sľú­be­ná kon­fi­gu­rá­cia:

[uwsgi]
plugin = syslog
logger = syslog:emperor

master-fifo = /run/uwsgi/emperor.fifo

emperor = /etc/uwsgi/apps-enabled
emperor-stats = /run/uwsgi/emperor-stats.sock
emperor-on-demand-exec = /etc/uwsgi/make_rundir.py

vassals-inherit = /etc/uwsgi/default-inherit.ini
vassals-include-before = /etc/uwsgi/default-include-before.ini

#emperor-tyrant = true
#cap = setgid,setuid

Pr­vé 2 di­rek­tí­vy za­pí­na­jú lo­go­va­nie do sys­lo­gu pod iden­ti­fi­ká­to­rom emperor. Po­mo­cou toh­to iden­ti­fi­ká­to­ra bu­de ne­skôr mož­né iden­ti­fi­ko­vať lo­gy po­chá­dza­jú­ce z hlav­né­ho pro­ce­su. Ak je lo­go­va­nie na­sta­ve­né správ­ne, vý­sled­né lo­gy vy­ze­ra­jú pri­bliž­ne tak­to:

Sat Nov 18 13:13:19 2023 - [emperor] vassal linuxos.sk.ini is ready to accept requests
Sat Nov 18 13:13:21 2023 - [emperor] vassal linuxos.sk.ini is now loyal

Di­rek­tí­va master-fifo na­sta­vu­je ces­tu k rú­re pre ovlá­da­nie. Do rú­ry je mož­né po­sie­lať prí­ka­zy de­fi­no­va­né v do­ku­men­tá­cii. Osob­ne som z tých­to mož­nos­tí ne­vy­užil eš­te re­ál­ne nič ok­rem pre­ske­no­va­nia kon­fi­gu­rá­cie (echo "E" > /run/uwsgi/emperor.fifo).

Pod di­rek­tí­vou emperor sa skrý­va ces­ta ku kon­fi­gu­rá­cii k jed­not­li­vým we­bom (vas­sals). V tom­to prí­pa­de je to ad­re­sár s kon­fi­gu­rač­ný­mi sú­bor­mi, ale mô­že tu byť aj na­prí­klad kon­fi­gu­rá­cia z po­st­gre­sql da­ta­bá­zy. Pod­rob­nej­šie in­for­má­cie sa na­chá­dza­jú v do­ku­men­tá­cii.

Šta­tis­ti­ky o jed­not­li­vých we­boch a pro­ce­soch sa da­jú expor­to­vať cez di­rek­tí­vu emperor-stats.

Pod po­doz­ri­vou di­rek­tí­vou emperor-on-demand-exec sa do­kon­ca skrý­va ces­ta k skrip­tu. K je­ho úlo­he sa vrá­tim v čas­ti o spúš­ťa­ní on-de­mand.

Na­sle­du­jú­ce di­rek­tí­vy vassals-inherit a vassals-include-before de­fi­nu­jú spo­loč­nú kon­fi­gu­rá­ciu pre všet­ky we­by. Stá­le pla­tí, že som le­ni­vý prog­ra­má­tor, kto­ré­mu sa nech­ce všet­ko kon­fi­gu­ro­vať pre kaž­dý web. Pre­čo sú však di­rek­tí­vy 2?

Exis­tu­jú 2 di­rek­tí­vy pre vlo­že­nie spo­loč­nej kon­fi­gu­rá­cie inherit a include. Ok­rem to­ho exis­tu­jú eš­te v 2 va­rian­toch a to prí­po­nou -before ke­dy sú ako­by vlo­že­né na za­čia­tok kon­fi­gu­rač­né­ho sú­bo­ru a bez prí­po­ny, ke­dy sú na kon­ci. Roz­diel me­dzi inherit a include spo­čí­va v expan­zii en­vi­ron­ment pre­men­ných a v tom­to prí­pa­de roz­diel nie je dô­le­ži­tý. Čo je však dô­le­ži­té je prí­po­na -before, bez kto­rej by ne­bo­lo mož­né "pre­biť" glo­bál­ne na­sta­ve­nia špe­ci­fic­ký­mi na­sta­ve­nia­mi v kon­fi­gu­rá­cii kon­krét­ne­ho we­bu. Pod­rob­nos­ti o spô­so­be spra­co­va­nia kon­fi­gu­rač­né­ho sú­bo­ru sú v do­ku­men­tá­cii.

Pa­ra­no­id­ní po­u­ží­va­te­lia mô­žu vy­užiť re­žim emperor-tyrant, pri kto­rom we­by rov­no štar­tu­jú s ob­me­dze­ný­mi prá­va­mi. V bež­nom re­ži­me na­opak štar­tu­jú s prá­va­mi, kto­ré má em­pe­ror a na zá­kla­de kon­fi­gu­rá­cie svo­je prá­va de­gra­du­jú (eš­te v ča­se pred spus­te­ním kó­du po­u­ží­va­te­ľa).

Globálne nastavenia webov

Sú­bor default-inherit.ini bu­de ob­sa­ho­vať všet­ky na­sta­ve­nia, kto­ré ne­bu­de mož­né pre­pí­sať v kon­fi­gu­rač­ných sú­bo­roch jed­not­li­vých we­bov.

[uwsgi]
plugin = syslog
logger = syslog:%N
logformat = %(method) %(status) %(msecs)ms %(size)b %(uri)

master = 1

# limit pre beh skriptu
harakiri = 60
harakiri-verbose = 1

# automatické vypnutie procesu
cheap = true
idle = 3600
die-on-idle = true

# nastavenie ciest
chdir = /var/www/%N/web/app
pythonpath = /var/www/%N/web/app
virtualenv = /var/www/%N/web/virtualenv

# python modul
module = wsgi

chmod-socket = 660

stats = /run/uwsgi/stats/%N.sock
memory-report = true

safe-pidfile = /run/uwsgi/apps/%N/pid

#cgroup = /sys/fs/cgroup/cpu/uwsgi/%N
#cgroup = /sys/fs/cgroup/memory/uwsgi/%N
#cgroup-opt = memory.limit_in_bytes = 6442450944

Na za­čiat­ku sú­bo­ru je opäť na­sta­ve­nie lo­gov. Ma­gic­ká pre­men­ná %N ob­sa­hu­je ná­zov hlav­né­ho kon­fi­gu­rač­né­ho sú­bo­ru bez prí­po­ny. Kon­fi­gu­rač­ný sú­bor we­bu bu­de mať ná­zov linuxos.sk.ini, tak­že tá­to ma­gic­ká pre­men­ná bu­de ob­sa­ho­vať linuxos.sk. Sys­log iden­ti­fi­ká­tor bu­de pre­to ob­sa­ho­vať ná­zov do­mé­ny linuxos.sk, čo nám umož­ní fil­tro­vať lo­gy pod­ľa kon­krét­ne­ho we­bu.

Di­rek­tí­va master ur­ču­je spô­sob vy­tvo­re­nia pro­ce­su. V zá­sa­de pla­tí, že pro­ce­sy we­bu by ma­li mať na­sta­ve­nú di­rek­tí­vu master a em­pe­ror nie.

Po­mo­cou di­rek­tí­vy harakiri sa na­sta­vu­je čas, po kto­rom sa za­bi­je wor­ker ak ne­bu­de od­po­ve­dať.

Na­sle­du­jú­ca časť s di­rek­tí­va­mi cheap, idle a die-on-idle po­vo­ľu­je ak­ti­vá­ciu soc­ke­tom a ča­so­vač, kto­rý vy­pne in­štan­ciu, ak po zvo­le­nú do­bu ne­bu­de pri­ja­tá žia­da po­žia­dav­ka.

Ďa­lej tu má­me kon­fi­gu­rá­ciu ciest, v kto­rej sú po­u­ži­té ma­gic­ké pre­men­né. Hod­no­ta %N sa na­hra­dí náz­vom hlav­né­ho kon­fi­gu­rač­né­ho sú­bo­ru bez prí­po­ny.

Po­mo­cou di­rek­tí­vy module sa na­sta­vu­je pyt­hon mo­dul, kto­rý bu­de ob­sa­ho­vať en­try po­int ap­li­ká­cie (fun­kciu application).

Vy­tvo­re­ný soc­ket má mať prá­va 660, te­da pl­ný prí­stup pre po­u­ží­va­te­ľa a sku­pi­nu.

Kaž­dý pro­jekt bu­de mať vlast­ný soc­ket ur­če­ný di­rek­tí­vou stats, cez kto­rý sa da­jú čí­tať šta­tis­ti­ky. Di­rek­tí­vou memory-report sa za­pí­na export in­for­má­cií o ob­sa­de­nej pa­mä­ti do šta­tis­tík.

Na­ko­niec zo­stá­va už len na­sta­ve­nie PID sú­bo­ru.

Ak po­u­ží­va­te cg­roups v1, je mož­né na­sta­viť li­mi­ty pria­mo pre wor­ker. Žiaľ pod­po­ra v2 v do­be pí­sa­nia nie je im­ple­men­to­va­ná.

V sú­bo­re default-include-before.ini bu­dú di­rek­tí­vy, kto­ré sa mô­žu mô­žu v na­sta­ve­niach jed­not­li­vých we­bov pre­pí­sať.

[uwsgi]
processes = 4

cheaper = 1
cheaper-initial = 1
cheaper-step = 1

uid = www-data
gid = www-data
chown-socket = www-data:www-data

V tom­to sú­bo­re na­sta­vu­jem naj­skôr po­čet wor­ke­rov. We­bo­vá ap­li­ká­cia si v tom­to prí­pa­de mô­že spus­tiť ma­xi­mál­ne 4 pro­ce­sy.

Ďa­lej na­sle­du­je na­sta­ve­nie au­to­ma­tic­ké­ho šká­lo­va­nia po­mo­cou di­rek­tív cheaper. Šká­lo­va­nie sa za­čí­na na 1 pro­ce­se a po­čet pro­ce­sov sa zvy­šu­je / zni­žu­je po 1 pro­ce­se.

Na­ko­niec sa na­sta­vu­je po­u­ží­va­teľ / sku­pi­na pro­ce­su a prá­va pre soc­ket.

Spustenie on demand

Pri ini­cia­li­zá­cii on-de­mand in­štan­cie mô­že uws­gi em­pe­ror spus­tiť ľu­bo­voľ­ný skript. Úče­lom skrip­tu je príp­ra­va pro­stre­dia pre beh we­bu. Mô­že na­prí­klad vy­tvá­rať po­treb­né ad­re­sá­re. Na­ko­niec mu­sí vrá­tiť ces­tu k unix soc­ket sú­bo­ru. Tak­to kon­krét­ne vy­ze­rá môj /etc/uwsgi/make_rundir.py:

#!/usr/bin/env python3

import configparser
import grp
import os
import pwd
import socket
import sys

# uwsgi posiela názov konfiguračného úboru ako args[1]
confname = sys.argv[1]
basename = os.path.basename(confname)
appname = os.path.splitext(basename)[0]

# načítanie konfigurácie
config = configparser.ConfigParser(strict=False)
config.read(confname)

# vytvorenie adresára so štatistikami
dirname = '/run/uwsgi/stats/'
try:
	os.makedirs(dirname)
except OSError:
	pass
uid = pwd.getpwnam('www-data').pw_uid
gid = grp.getgrnam('www-data').gr_gid
os.chown(dirname, uid, gid)
os.chmod(dirname, 504) # 770

# vytvorenie socketu pre štatistiky
stats_socket_file = f'/run/uwsgi/stats/{appname}.sock'
try:
	os.unlink(stats_socket_file)
except OSError:
	pass
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(stats_socket_file)

# vytvorenie /run/uwsgi/apps
dirname = '/run/uwsgi/apps/%s' % appname
try:
	os.makedirs(dirname)
except OSError:
	pass

# načítanie uid / gid zo súboru
config = dict(config.items('uwsgi'))
config.setdefault('uid', 'www-data')
config.setdefault('gid', 'www-data')

# nastavenie práv
uid = pwd.getpwnam(config['uid']).pw_uid
gid = grp.getgrnam(config['gid']).gr_gid
perms = '%s:%s' % (config['uid'], config['gid'])

if uid == 0 or gid == 0:
	sys.stderr.write("Dangerous")
	sys.exit(-1)

os.chown(dirname, uid, gid)
os.chmod(dirname, 504) # 770
os.chown(stats_socket_file, uid, gid)
os.chmod(stats_socket_file, 432) # 660

# vrátenie cesty k socketu
sys.stdout.write('%s/socket' % dirname)

Konfigurácia webovej aplikácie

Kon­fi­gu­rá­ciu we­bo­vých ap­li­ká­cií umiest­ňu­jem štan­dard­ne do ad­re­sá­ra /etc/uwsgi/apps-available. V ad­re­sá­ri /etc/uwsgi/apps-enabled sú sym­bo­lic­ké od­ka­zy na sú­bo­ry v apps-available, čo umož­ňu­je jed­no­du­cho za­pí­nať / vy­pí­nať we­by vy­tvo­re­ním či zma­za­ním od­ka­zu.

Kom­plet­ná kon­fi­gu­rá­cia linuxos.sk.ini mô­že vy­ze­rať na­prí­klad tak­to:

[uwsgi]
plugins = python3

processes = 8

uid = linuxos.sk
gid = linuxos-team
chown-socket = linuxos.sk:linuxos-team

Webová aplikácia

Te­raz si v ad­re­sá­ri /var/www/linuxos.sk/web vy­tvo­rí­me ukáž­ko­vú ap­li­ká­ciu.

Naj­skôr v ad­re­sá­ri vy­tvo­rí­me pyt­hon vir­tu­alenv pro­stre­die prí­ka­zom python3 -m venv virtualenv. Ap­li­ká­cia bu­de umiest­ne­ná v po­dad­re­sá­ri app ako sú­bor wsgi.py. Kom­plet­ný sú­bor vy­ze­rá tak­to:

def application(env, start_response):
	start_response('200 OK', [('Content-Type','text/html')])
	return [b"Hello World"]

Spustenie systémovej služby

Na do­kon­če­nie kon­fi­gu­rá­cie zo­stá­va je­di­ná drob­nosť - vy­tvo­re­nie sys­té­mo­vej služ­by pre spus­te­nie uws­gi. V ad­re­sá­ri /etc/systemd/system vy­tvo­rí­me sú­bor emperor.uwsgi.service.

[Unit]
Description=uWSGI Emperor
After=syslog.target

[Service]
ExecStartPre=/bin/mkdir -p /run/uwsgi
ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/emperor.ini
ExecReload=/bin/echo "E" > /run/uwsgi/emperor.fifo
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Tu sa na­sta­vu­jú prí­ka­zy pre vy­tvo­re­nie pra­cov­né­ho ad­re­sá­ra, spus­te­nie da­e­mo­na, ale­bo na­čí­ta­nie ad­re­sá­ra s kon­fi­gu­rač­ný­mi sú­bor­mi po­sla­ním pís­me­na E do ria­dia­cej rú­ry /run/uwsgi/emperor.fifo.

Služ­ba sa ná­sled­ne na­štar­tu­je prí­ka­zom systemctl start emperor.uwsgi.service.

Lo­gy sa da­jú sle­do­vať prí­ka­zom journalctl -f -u emperor.uwsgi.service. Je mož­né fil­tro­vať lo­gy pod­ľa jed­not­li­vých iden­ti­fi­ká­to­rov, na­prí­klad journalctl -f -u emperor.uwsgi.service SYSLOG_IDENTIFIER=emperor pre sle­do­va­nie hlav­né­ho pro­ce­su, ale­bo SYSLOG_IDENTIFIER=linuxos.sk pre sle­do­va­nie lo­gov pre kon­krét­ny web.

Webový server nginx

In­šta­lá­ciu we­bo­vé­ho ser­va­ra opäť vy­ko­ná­me kla­sic­kým de­bia­nov­ským spô­so­bom: apt install nginx-full.

Kon­fi­gu­rá­ciu za­čnem na­sta­ve­ním for­má­tu lo­gov v hlav­nom kon­fi­gu­rač­nom sú­bo­re /etc/nginx/nginx.conf. Do sek­cie http pri­dá­vam na­sta­ve­nia for­má­tu lo­gov, kto­ré sa bu­dú jed­no­du­cho stro­jo­vo čí­tať:

log_format syslog_format '$request_method $status $request_time(ms) $bytes_sent(B) upstream[response=$upstream_response_time(ms) ttfb=$upstream_header_time(ms)] $remote_addr $request_uri $http_user_agent';

Kon­fi­gu­rá­cia we­bu linxuos.sk bu­de opäť ulo­že­ná tak, aby sa da­la ľah­ko za­pí­nať / vy­pí­nať. Kom­plet­ná kon­fi­gu­rá­cia /etc/nginx/sites-available/linuxos.sk.conf vy­ze­rá tak­to:

server {
	listen 80;
	listen [::]:80;

	server_name linuxos.sk;

	access_log syslog:server=unix:/dev/log,facility=local7,tag=linuxos_sk,severity=info syslog_format;

	location / {
		include uwsgi_params;
		uwsgi_pass unix:///run/uwsgi/apps/linuxos.sk/socket;
	}
}

Ten­to sú­bor v pod­sta­te len ho­vo­rí, že má po­čú­vať na ad­re­se li­nu­xos.sk, por­te 80 a po­sie­lať všet­ky po­žia­dav­ky ser­ve­ru uwsgi. Prí­stu­po­vé lo­gy sú odo­sie­la­né do sys­lo­gu pod znač­kou linuxos_sk. Špe­ciál­ne zna­ky ako na­prí­klad bod­ka nie sú po­vo­le­né.

Aby bo­lo mož­né we­bo­vú strán­ku na­čí­tať, je po­treb­né eš­te pri­dať ad­re­su linuxos.sk do /etc/hosts.

127.0.1.1 linuxos.sk
::1 linuxos.sk

Po vy­tvo­re­ní od­ka­zu v sites-enabled a re­lo­ade ser­ve­ra systemctl reload nginx je mož­né na­čí­tať web:

curl http://linuxos.sk/
Hello World

Telemetria

Za dô­le­ži­tú časť sta­rost­li­vos­ti o mul­ti­hos­ting po­va­žu­jem mo­ni­to­ring zdro­jov. Mať pre­hľad o re­zer­vách vo vý­ko­ne, či o ne­po­kry­tých špič­kách, ale­bo ne­pos­luš­ných ro­bo­toch, kto­ré nech­tiac ro­bia na ser­ver DDoS útok mô­že byť cel­kom uži­toč­né.

Architektúra

Mo­ni­to­ring bu­de za­lo­že­ný na 3 sa­mos­tat­ných na­vzá­jom za­me­ni­teľ­ných kom­po­nen­toch:

Zber úda­jov
Ná­stroj te­le­graf je exce­lent­ným zbe­ra­čom úda­jov. Vie zís­ka­vať in­for­má­cie o sys­té­me z rôz­nych rôz­nych kon­fi­gu­ro­va­teľ­ných zdro­jov. Ná­sled­ne je schop­ný po­zbie­ra­né úda­je po­sie­lať do rôz­nych da­ta­báz, ale­bo sú­bo­rov. Ty­pic­ky kaž­dý ser­ver má spus­te­nú vlast­nú in­štan­ciu te­le­gra­fu, kto­rá buď ak­tív­ne za­sie­la úda­je v pra­vi­del­ných in­ter­va­loch, ale­bo po­čú­va na vy­bra­nom por­te a v prí­pa­de po­žia­dav­ky od­po­vie ak­tu­ál­ny­mi na­me­ra­ný­mi hod­no­ta­mi.
Za­zna­me­ná­va­nie úda­jov (da­ta­bá­za)
Te­le­met­ric­ké úda­je sa naj­lep­šie za­zna­me­ná­va­jú do špe­ciál­ne na to ur­če­nej da­ta­bá­zy. Na vý­ber je na­prí­klad Pro­met­he­us, ale vzhľa­dom na to, že za­zna­me­ná­vam aj nie­kto­ré tex­to­vé úda­je som na­ko­niec skon­čil pri po­u­ži­tí In­flu­xDB. Da­ta­bá­za zvy­čaj­ne be­ží na jed­nom stro­ji ak nie je ne­vy­hnut­né šká­lo­va­nie kvô­li vý­ko­nu, ale­bo väč­šia odol­nosť vo­či vý­pad­kom.
Vi­zu­ali­zá­cia a upo­zor­ne­nia
V ob­las­ti vi­zu­ali­zá­cie za­se exce­lu­je ná­stroj Gra­fa­na, kto­rý vie čí­tať a zo­bra­zo­vať dá­ta z rôz­nych da­ta­báz.

Inštalácia a nastavenie databázy InfluxDB 2.x

Keď­že te­le­graf aj gra­fa­na sú zá­vis­lé na da­ta­bá­ze, bu­de naj­ro­zum­nej­šie za­čať prá­ve in­šta­lá­ci­ou da­ta­bá­zy.

Pod­ľa strán­ky s okaz­mi na stia­hnu­tie je po­stup in­šta­lá­cie pre De­bian / Ubun­tu na­sle­dov­ný:

wget -q https://repos.influxdata.com/influxdata-archive_compat.key
echo '393e8779c89ac8d958f81f942f9ad7fb82a25e133faddaf92e15b16e6ac9ce4c influxdata-archive_compat.key' | sha256sum -c && cat influxdata-archive_compat.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg > /dev/null
echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg] https://repos.influxdata.com/debian stable main' | sudo tee /etc/apt/sources.list.d/influxdata.list

apt-get update && apt install influxdb2

Po úspeš­nej in­šta­lá­cii je mož­né da­ta­bá­zu na­štar­to­vať prí­ka­zom systemctl start influxdb. Na­sle­do­vať by ma­la kon­fi­gu­rá­cia, pri kto­rej sa vy­tvo­ria prí­stu­po­vé úda­je. Ja som pre všet­ky po­lia vy­pl­nil telegraf, pre­to­že som váž­ne le­ni­vý a služ­bu po­u­ží­vam len v lo­kál­nej sie­ti.

# influx setup
> Welcome to InfluxDB 2.0!
? Please type your primary username telegraf
? Please type your password ********
? Please type your password again ********
? Please type your primary organization name telegraf
? Please type your primary bucket name telegraf
? Please type your retention period in hours, or 0 for infinite 72
? Setup with these parameters?
  Username:          telegraf
  Organization:      telegraf
  Bucket:            telegraf
  Retention Period:  72h0m0s
 Yes
User            Organization    Bucket
telegraf        telegraf        telegraf

Pre au­ten­ti­fi­ká­ciu bu­de vy­ža­do­va­ný to­ken, kto­rý je mož­né zis­tiť prí­ka­zom influx auth list:

ID                      Description             Token                                                                                           User Name       User ID                 Permissions
0c262a48f390a000        telegraf's Token        _IQwe0LFcZgILzW-Blre9E9s80FUCo8SgU0lrxAZB-BPFg-HGJvd0zqEMXfL8-YcBR7olvMppvCyQY8_YX6izg==        telegraf        0c262a48de90a000        [read:/authorizations write:/authorizations read:/buckets write:/buckets read:/dashboards write:/dashboards read:/orgs write:/orgs read:/sources write:/sources read:/tasks write:/tasks read:/telegrafs write:/telegrafs read:/users write:/users read:/variables write:/variables read:/scrapers write:/scrapers read:/secrets write:/secrets read:/labels write:/labels read:/views write:/views read:/documents write:/documents read:/notificationRules write:/notificationRules read:/notificationEndpoints write:/notificationEndpoints read:/checks write:/checks read:/dbrp write:/dbrp read:/notebooks write:/notebooks read:/annotations write:/annotations read:/remotes write:/remotes read:/replications write:/replications]

Inštalácia a nastavenie telegrafu

Ná­vod na in­šta­lá­ciu pre Ubun­tu / De­bian sa prak­tic­ky ne­lí­ši od od In­flu­xDB a po­zos­tá­va z pri­da­nia re­po­zi­tá­ra a in­šta­lá­ciu cez apt:

wget -q https://repos.influxdata.com/influxdata-archive_compat.key
echo '393e8779c89ac8d958f81f942f9ad7fb82a25e133faddaf92e15b16e6ac9ce4c influxdata-archive_compat.key' | sha256sum -c && cat influxdata-archive_compat.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg > /dev/null
echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg] https://repos.influxdata.com/debian stable main' | tee /etc/apt/sources.list.d/influxdata.list
apt-get update && apt install telegraf

Minimálna konfigurácia

Za­tiaľ bu­dem ig­no­ro­vať štan­dard­ný a za­čnem úpl­ne jed­no­du­chým čis­tým kon­fi­gu­rač­ným sú­bo­rom telegraf.conf:

[[outputs.file]]
	files = ["/tmp/telegraf.out"]

[[inputs.mem]]

Po spus­te­ní telegraf --config telegraf.conf --debug sa v kon­zo­le zo­bra­zí vý­pis po­dob­ný to­mu­to:

2023-11-25T16:09:09Z I! Starting Telegraf 1.22.3+ds1-0ubuntu2
2023-11-25T16:09:09Z I! Loaded inputs: mem
2023-11-25T16:09:09Z I! Loaded aggregators:
2023-11-25T16:09:09Z I! Loaded processors:
2023-11-25T16:09:09Z I! Loaded outputs: file
2023-11-25T16:09:09Z I! Tags enabled: host=linuxos
2023-11-25T16:09:09Z I! [agent] Config: Interval:10s, Quiet:false, Hostname:"linuxos", Flush Interval:10s
2023-11-25T16:09:09Z D! [agent] Initializing plugins
2023-11-25T16:09:09Z D! [agent] Connecting outputs
2023-11-25T16:09:09Z D! [agent] Attempting connection to [outputs.file]
2023-11-25T16:09:09Z D! [agent] Successfully connected to outputs.file
2023-11-25T16:09:09Z D! [agent] Starting service inputs
2023-11-25T16:09:19Z D! [outputs.file] Wrote batch of 1 metrics in 142.651µs
2023-11-25T16:09:19Z D! [outputs.file] Buffer fullness: 0 / 10000 metrics
2023-11-25T16:09:29Z D! [outputs.file] Wrote batch of 1 metrics in 126.871µs
2023-11-25T16:09:29Z D! [outputs.file] Buffer fullness: 0 / 10000 metrics

Zau­jí­ma­vej­šie však vy­ze­rá sú­bor /tmp/telegraf.out.

mem,host=linuxos total=8317620224i,inactive=2522996736i,shared=848220160i,write_back=0i,available=3538952192i,huge_pages_free=0i,sreclaimable=1049927680i,swap_total=536866816i,used=3613786112i,commit_limit=4695674880i,high_free=0i,mapped=1177395200i,vmalloc_total=35184372087808i,vmalloc_used=24809472i,buffered=84185088i,slab=1203593216i,swap_cached=87453696i,cached=3088490496i,committed_as=78690824192i,low_total=0i,available_percent=42.547653014843874,dirty=3010560i,high_total=0i,huge_pages_total=0i,vmalloc_chunk=0i,active=2959667200i,huge_page_size=2097152i,low_free=0i,sunreclaim=153665536i,used_percent=43.44735651157328,free=1531158528i,page_tables=32088064i,swap_free=2609152i,write_back_tmp=0i 1700928550000000000
mem,host=linuxos committed_as=78680104960i,huge_pages_total=0i,low_total=0i,vmalloc_total=35184372087808i,available=3531640832i,commit_limit=4695674880i,swap_cached=87453696i,swap_free=2609152i,swap_total=536866816i,vmalloc_used=24866816i,active=2950438912i,free=1522753536i,low_free=0i,vmalloc_chunk=0i,high_free=0i,huge_pages_free=0i,huge_page_size=2097152i,sunreclaim=153661440i,available_percent=42.45975094907146,buffered=84221952i,high_total=0i,shared=848220160i,slab=1203658752i,sreclaimable=1049997312i,write_back=0i,used_percent=43.535209332490915,dirty=4239360i,mapped=1167822848i,total=8317620224i,used=3621093376i,page_tables=30720000i,write_back_tmp=0i,cached=3089551360i,inactive=2523975680i 1700928560000000000
mem,host=linuxos cached=3089911808i,huge_pages_total=0i,mapped=1167765504i,swap_cached=87457792i,buffered=84238336i,commit_limit=4695674880i,high_total=0i,swap_total=536866816i,vmalloc_total=35184372087808i,total=8317620224i,dirty=1708032i,inactive=2524303360i,low_total=0i,free=1534955520i,high_free=0i,huge_pages_free=0i,shared=848220160i,sunreclaim=153698304i,vmalloc_used=24842240i,write_back=0i,available_percent=42.61103114293861,active=2949169152i,committed_as=78673256448i,page_tables=30531584i,sreclaimable=1050013696i,available=3544223744i,used=3608514560i,write_back_tmp=0i,huge_page_size=2097152i,slab=1203712000i,swap_free=2658304i,vmalloc_chunk=0i,used_percent=43.38397838347855,low_free=0i 1700928570000000000

Zo vstu­pov je po­vo­le­né len za­zna­me­ná­va­nie ob­sa­de­nej šta­tis­tík ope­rač­nej pa­mä­te. Te­raz si bliž­šie ro­zo­be­rie­me jed­not­li­vé po­lia sú­bo­ru.

Pr­vým po­ľom na kaž­dom riad­ku je vždy ná­zov met­ri­ky. V tom­to prí­pa­de je to mem. Na­sle­du­je zo­znam čiar­kou od­de­le­ných ta­gov vo for­me názov=hodnota. V tom­to prí­pa­de je ná­zov ta­gu host a hod­no­ta linuxos. Ta­gy sú po­lia, pod­ľa kto­rých sú dá­ta in­de­xo­va­né a je mož­né ich rých­lo pre­hľa­dá­vať. Do ta­gov by ma­li byť ukla­da­né len po­lia, kto­ré ma­jú ob­me­dze­ný po­čet hod­nôt. V riad­ku ďa­lej na­sle­du­je zo­znam na­me­ra­ných hod­nôt v rov­na­kej for­me, v akej bol zo­znam ta­gov. Po­sled­ným po­ľom je ča­so­vá pe­čiat­ka me­ra­nia.

Odosielanie dát do InfluxDB

Vo väč­ši­ne prí­pa­dov bu­de­me chcieť ukla­dať na­me­ra­né hod­no­ty do re­ál­nej da­ta­bá­zy. Po pri­da­ní na­sle­du­jú­cej sek­cie sa bu­dú úda­je za­sie­lať do In­flu­xDB:

[[outputs.influxdb_v2]]
	urls = ["http://127.0.0.1:8086"]
	organization = "telegraf"
	bucket = "telegraf"
	token = "_IQwe0LFcZgILzW-Blre9E9s80FUCo8SgU0lrxAZB-BPFg-HGJvd0zqEMXfL8-YcBR7olvMppvCyQY8_YX6izg=="

URL ad­re­su je po­treb­né na­sta­viť pod­ľa hos­ti­te­ľa a por­tu, na kto­rom be­ží In­flu­xDB. Prí­stu­po­vý to­ken je mož­né zís­kať prí­ka­zom influx auth list. Ná­zov or­ga­ni­zá­cie a buc­ke­tu mu­sia zod­po­ve­dať náz­vom zvo­le­ným pri in­šta­lá­cii.

Príprava parsovania logov

Ok­rem bež­ných met­rík bu­de­me za­zna­me­ná­vať aj nie­kto­ré zau­jí­ma­vé in­for­má­cie z lo­gov we­bo­vé­ho ser­ve­ru nginx a ap­li­kač­né­ho ser­ve­ru uws­gi. V na­sle­du­jú­com vý­pi­se je ukáž­ka nginx lo­gu zís­ka­né­ho prí­ka­zom journalctl -f -o short-iso -u nginx.service:

2023-11-25T17:30:47+0100 linuxos nginx[3852772]: linuxos linuxos_sk: GET 302 0.003(ms) 486(B) upstream[response=0.003(ms) ttfb=0.003(ms)] x.x.x.181 /currency/set/?currency=EUR Mozilla/5.0
2023-11-25T17:30:47+0100 linuxos nginx[3852774]: linuxos linuxos_sk: POST 200 0.028(ms) 867(B) upstream[response=0.027(ms) ttfb=0.027(ms)] x.x.x.54 /accounts/signup/ Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0

Do po­zor­nos­ti dá­vam ná­zov we­bu linuxos_sk, kto­rý sa ne­dal za­pí­sať s bod­kou. Na­šťas­tie je mož­né prob­lém veľ­mi jed­no­du­cho na­pra­viť po­mo­cou ná­stro­ja awk. Vý­sled­ný skript /etc/telegraf/scripts/read_nginx_log.sh bu­de vy­ze­rať na­sle­dov­ne:

#!/bin/bash
/usr/bin/journalctl -f -o short-iso -u nginx.service|awk '\
{\
	gsub(/_/, ".", $5);\
	print\
}'

Po­dob­ným spô­so­bom bu­de im­ple­men­to­va­ný skript pre čí­ta­nie uws­gi lo­gov. Kon­fi­gu­rá­cia bu­de o nie­čo zlo­ži­tej­šia, keď­že tu sa spra­cú­va dĺž­ka be­hu skrip­tu. Z rých­lych do­ta­zov sa od­strá­ni URL ad­re­sa a ota­gu­jú sa znač­kou fast. Po­ma­lé sa za­se ota­gu­jú znač­kou slow a URL ad­re­sa zo­sta­ne. Tak­to vy­ze­rá skript /etc/telegraf/scripts/read_uwsgi_log.sh:

#!/bin/bash
/usr/bin/journalctl -f -o short-iso -u emperor.uwsgi.service | awk '\
{\
	time = substr($6, 1, length($6)-2);\
	speed = "slow";\
	if (time+0 < 1000) {\
		$8 = "∅";\
		speed = "fast"\
	}\
	print $1, $2, $3, $4, $5, $6, $7, speed, $8\
}'

Vý­sled­ný log vy­ze­rá pri­bliž­ne tak­to:

2023-11-25T17:45:42+0100 linuxos linuxos.sk[288892]: GET 200 112ms 103501b fast ∅
2023-11-25T17:45:43+0100 linuxos linuxos.sk[288892]: GET 200 148ms 108156b fast ∅
2023-11-25T17:45:44+0100 linuxos linuxos.sk[288892]: GET 200 93ms 102613b fast ∅

Kompletná konfigurácia

V tej­to čas­ti si prej­de­me pl­ne funkč­ný kon­fi­gu­rač­ný sú­bor pre te­le­graf. Fi­nál­ny /etc/telegraf/telegraf.conf vy­ze­rá tak­to:

# Ukladanie do InfluxDB
[[outputs.influxdb_v2]]
	urls = ["http://127.0.0.1:8086"]
	organization = "telegraf"
	bucket = "telegraf"
	token = "_IQwe0LFcZgILzW-Blre9E9s80FUCo8SgU0lrxAZB-BPFg-HGJvd0zqEMXfL8-YcBR7olvMppvCyQY8_YX6izg=="


# Aktivita CPU
[[inputs.cpu]]
	percpu = true
	totalcpu = true
	collect_cpu_time = false
	report_active = false


# Zaplnenie disku
[[inputs.disk]]
	ignore_fs = ["tmpfs", "devtmpfs", "devfs", "iso9660", "overlay", "aufs", "squashfs"]


# Monitorovanie RAM
[[inputs.mem]]


# Monitorovanie siete
[[inputs.net]]
	interfaces = ["eth0"]


# Monitorovanie procesov (+ pridanie tagov socket podľa používateľa)
[[inputs.procstat]]
	user = "linuxos.sk"
	[inputs.procstat.tags]
		socket = "linuxos.sk"

[[inputs.procstat]]
	user = "mirec"
	[inputs.procstat.tags]
		socket = "mirec.sk"


# Monitorovanie uWSGI procesov
[[inputs.uwsgi]]
	servers = ["unix:///run/uwsgi/stats/linuxos.sk.sock"]
	[inputs.uwsgi.tags]
		socket = "linuxos.sk"

[[inputs.uwsgi]]
	servers = ["unix:///run/uwsgi/stats/mirec.sk.sock"]
	[inputs.uwsgi.tags]
		socket = "mirec.sk"


# Ukladanie uWSGI logov (kvôli záznamu najpomalších URL)
[[inputs.execd]]
	command = ["/etc/telegraf/scripts/read_uwsgi_log.sh"]
	grok_patterns = ['%{TIMESTAMP_ISO8601:timestamp} %{DATA:username} uwsgi-%{DATA:socket:tag}\[%{NUMBER:pid:int}\]: %{DATA:method:tag} %{NUMBER:response_code:tag} %{NUMBER:uwsgi_request_time:float}ms %{NUMBER:bytes:int}b %{WORD:speed:tag} %{GREEDYDATA:url}']
	data_format = "grok"
	name_override = "uwsgi_requests"
	fielddrop = ["username", "pid"]


# Rovnaké spracovanie uWSGI logov, tentoraz za účelom výpočtu histogramu
[[inputs.execd]]
	command = ["/etc/telegraf/scripts/read_uwsgi_log.sh"]
	grok_patterns = ['%{TIMESTAMP_ISO8601:timestamp} %{DATA:username} uwsgi-%{DATA:socket:tag}\[%{NUMBER:pid:int}\]: %{DATA:method} %{NUMBER:response_code} %{NUMBER:uwsgi_request_time:float}ms %{NUMBER:bytes:int}b %{WORD:speed} %{GREEDYDATA:url}']
	data_format = "grok"
	name_override = "uwsgi_requests_histogram"
	fielddrop = ["timestamp", "username", "pid", "method", "response_code", "bytes", "speed", "url"]


# Rozdelenie počtu requestov do bucketov podľa trvanie requestu po 20ms
[[aggregators.histogram]]
	period = "120s"
	drop_original = true
	reset = true
	cumulative = false
	namepass = ["uwsgi_requests_histogram"]
	[[aggregators.histogram.config]]
		measurement_name = "uwsgi_requests_histogram"
		fields = ["uwsgi_request_time"]
		buckets = [20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0, 220.0, 240.0, 260.0, 280.0, 300.0, 320.0, 340.0, 360.0, 380.0, 400.0, 420.0, 440.0, 460.0, 480.0, 500.0, 520.0, 540.0, 560.0, 580.0, 600.0, 620.0, 640.0, 660.0, 680.0, 700.0, 720.0, 740.0, 760.0, 780.0, 800.0, 820.0, 840.0, 860.0, 880.0, 900.0, 920.0, 940.0, 960.0, 980.0, 1000.0, 60000.0]


# Spracovanie logov nginx
[[inputs.execd]]
	command = ["/etc/telegraf/scripts/read_nginx_log.sh"]
	data_format = "grok"
	grok_patterns = ["%{TIMESTAMP_ISO8601:timestamp} %{WORD:server} nginx\\[%{NUMBER:nginx_pid}\\]: %{DATA:server_name} %{DATA:socket:tag}: %{WORD:http_method:tag} %{NUMBER:http_status:tag} %{NUMBER:request_time:float}\\(ms\\) %{NUMBER:bytes_sent:int}\\(B\\) upstream\\[response=%{NUMBER:upstream_response_time:float}\\(ms\\) ttfb=%{NUMBER:upstream_ttfb:float}\\(ms\\)\\] %{IP:client_ip} %{DATA:request_path} %{GREEDYDATA:user_agent}"]
	fielddrop = ["server", "nginx_pid", "server_name", "client_ip", "request_path", "user_agent", "bytes_sent", "upstream_response_time", "ttfb"]
	name_override = "nginx_access"

Vstup inputs.cpu slú­ži na mo­ni­to­ro­va­nie sta­vu CPU. V ča­se pí­sa­nia člán­ku ten­to mo­dul expor­tu­je tag cpu s čís­lom jad­ra (cpux ale­bo cpu-to­tal) a po­lia:

Ďal­ší vstup inputs.disk slú­ži na za­zna­me­ná­va­nie za­pl­ne­nia dis­ku. Do­stup­né ta­gy sú:

Z po­lí má­me k dis­po­zí­cii:

Ďal­šie 2 sek­cie slú­žia na kon­fi­gu­rá­ciu za­zna­me­ná­va­nia sta­vu RAM a sie­ťo­vej kar­ty eth0.

Zau­jí­ma­vej­šou sek­ci­ou je inputs.procstat, kto­rá slú­ži na za­zna­me­ná­va­nie šta­tis­tík o pro­ce­soch. Na­kon­fi­gu­ro­va­ných je nie­koľ­ko vstu­pov, pri­čom vstu­py sú fil­tro­va­né pod­ľa po­u­ží­va­te­ľa. Kaž­dý vstup má na­kon­fi­gu­ro­va­nú znač­ku socket, aby bo­lo mož­né dá­ta zo­sku­piť pod­ľa kon­krét­ne­ho we­bu.

Kon­fi­gu­rá­ciu šta­tis­tík v na­sta­ve­ní uWS­GI sme sa­moz­rej­me ne­ro­bi­li len tak pre­nič-za­nič. Ná­stroj te­le­graf vsta­va­nú pod­po­ru pre čí­ta­nie šta­tis­tík uWS­GI po­mo­cou sek­cie inputs.uwsgi. Po­dob­ne, ako pri inputs.procstat je aj tu na­kon­fi­gu­ro­va­né znač­ko­va­nie we­bu do ta­gu socket.

Sku­toč­ná zá­ba­va sa za­čí­na až te­raz pri vstu­poch, kto­ré ob­sa­hu­jú gro­k_pat­terns. Te­da za­ča­la by … ne­byť ChatGPT 4. Ja som ne­poz­nal for­mát gro­k_pa­terns, do­kon­ca som ani ne­poz­nal spô­sob, ako na­kon­fi­gu­ro­vať telegraf tak, aby čí­tal log. Na­pí­sal som však po­žia­dav­ku pre ChatGP­T4, aby mi na­pí­sal kon­fi­gu­rá­ciu te­le­gra­fu pre čí­ta­nie lo­gu, pri­dal som je­den ria­dok z lo­gu ako ukáž­ku a on na mňa vy­ho­dil:

[[inputs.execd]]
	command = ["/etc/telegraf/scripts/read_uwsgi_log.sh"]
	grok_patterns = ['%{TIMESTAMP_ISO8601:timestamp} %{DATA:username} uwsgi-%{DATA:socket:tag}\[%{NUMBER:pid:int}\]: %{DATA:method:tag} %{NUMBER:response_code:tag} %{NUMBER:uwsgi_request_time:float}ms %{NUMBER:bytes:int}b %{WORD:speed:tag} %{GREEDYDATA:url}']
	data_format = "grok"
	name_override = "uwsgi_requests"
	fielddrop = ["username", "pid"]

Váž­ne, ne­viem ako sa to kon­fi­gu­ru­je a ne­byť ChatGPT by som sa trá­pil veľ­mi dl­ho. Vý­sled­kom je pek­ne štruk­tú­ro­va­ný vý­stup, kto­rý je správ­ne ota­go­va­ný a odo­sie­la­ný do In­flu­xDB.

Dru­hý prak­tic­ky iden­tic­ký vstup inputs.execd slú­ži ako vstup pre vý­po­čet his­to­gra­mu. Ag­re­gač­ná fun­kcia aggregators.histogram au­to­ma­tic­ky sčí­ta­va vý­sky­ty zá­zna­mu pod­ľa ich hod­no­ty a ta­gov do jed­not­li­vých buc­ke­tov. To je aj dô­vod, pre­čo dru­hý vstup ne­ob­sa­hu­je na­prí­klad tag speed.

Na­ko­niec tu má­me spra­co­va­nie lo­gu we­bo­vé­ho ser­ve­ra. Rov­na­ko ako v prí­pa­de uWS­GI som po­žia­dal ChatGPT o na­pí­sa­nie kon­fi­gu­rá­cie. Za­se bez prob­lé­mov a bez na­šep­ká­va­nia roz­poz­nal po­lia v lo­gu a všet­ko viac-me­nej správ­ne pri­ra­dil s po­u­ži­tím mi­ni­mál­ne­ho kon­tex­tu. Ko­neč­ne mám ko­le­gu, kto­ré­mu mô­žem na­kla­dať špi­na­vú prác!

Grafana

Pre in­šta­lá­ciu je pod­ľa do­ku­men­tá­cie po­treb­né vy­ko­nať nie­koľ­ko prí­ka­zov:

apt-get install -y apt-transport-https software-properties-common wget
mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com beta main" | tee -a /etc/apt/sources.list.d/grafana.list
apt-get update
apt-get install grafana

Ná­sled­ne sa gra­fa­na na­štar­tu­je prí­ka­zom systemctl start grafana-server. Te­raz sta­čí po­vo­liť pri­po­je­nie na port 3000 (na­prí­klad ufw allow 3000) a po na­čí­ta­ní ad­re­sy http://adresa-servera:3000/ v pre­hlia­da­či by sa ma­lo zo­bra­ziť pri­hla­so­va­cie ok­no.

Prihlasovacie okno grafana
Ob­rá­zok 1: Pri­hla­so­va­cie ok­no gra­fa­na

Úvod­né pri­hla­so­va­cie me­no a hes­lo je admin / admin.

Ďa­lej je po­treb­né na­sta­viť zdroj dát. Ten sa na­sta­vu­je cez Me­nu / Con­nec­ti­ons a z do­stup­ných spo­je­ní vy­be­rie­me In­flu­xDB. Dô­le­ži­té je vy­brať ja­zyk Flux. Ná­sled­ne je po­treb­né na­sta­viť mi­ni­mál­ne URL ad­re­su a pri­hla­so­va­cie úda­je tak, ako bo­li na­sta­ve­né v kon­fi­gu­rá­cii te­le­gra­fu.

Pridanie InfluxDB zdroja
Ob­rá­zok 2: Pri­da­nie In­flu­xDB zdro­ja

Ná­sled­ne sa mô­že­me za­čať hrať s dá­ta­mi klik­nu­tím na Ex­plo­re da­ta.

CPU

Na roz­diel do zdro­jov Pro­met­he­us, ale­bo In­flu­xDB s ja­zy­kom In­flu­xQL nie je pre ja­zyk Flux do­stup­ný gra­fic­ký edi­tor. Všet­ky do­ta­zy pre­to mu­sia byť pí­sa­né ruč­ne po­mo­cou tex­to­vé­ho edi­to­ru.

Dotaz na CPU
Ob­rá­zok 3: Do­taz na CPU

Graf CPU bol vy­kres­le­ný na­sle­du­jú­cim do­ta­zom:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "cpu" and
    r.cpu == "cpu-total" and
    r._field == "usage_idle"
  )

Prí­kaz from vy­be­rá buc­ket, z kto­ré­ho bud čí­ta­né dá­ta. Na­sle­du­je ob­me­dze­nie ča­so­vé­ho roz­sa­hu po­mo­cou vsta­va­nej pre­men­nej v. Tú­to pre­men­nú na­sta­vu­je au­to­ma­tic­ky gra­fa­na pod­ľa roz­sa­hu, kto­rý sa na­sta­vu­je v hor­nej čas­ti ro­z­hra­nia gra­fa­ny. Ďa­lej na­sle­du­je kon­krét­ny vý­ber dát. Tu zdô­raz­ňu­jem, že kaž­dé po­le v dá­tach je re­pre­zen­to­va­né ako sa­mos­tat­ný ria­dok. Riad­ky ma­jú pri­ra­de­né ta­gy ako r.tag. Riad­ky ob­sa­hu­jú aj im­pli­cit­né pre­men­né za­čí­na­jú­ce sa na znak _. Sú to na­prí­klad:

_me­a­su­re­ment
Ná­zov met­ri­ky
_field
Ná­zov po­ľa
_va­lue
Hod­no­ta po­ľa
_ti­me
Ča­so­vá pe­čiat­ka

V šta­tis­ti­kách CPU nie je k dis­po­zí­cii cel­ko­vé za­ťa­že­nie, ale má­me tu k dis­po­zí­cii po­le usage_idle. Ak by sme chce­li zo­bra­ziť cel­ko­vé za­ťa­že­nie, sta­čí nám od­čí­tať do­bu ne­čin­nos­ti od 100%. Na kon­ci do­ta­zu sta­čí jed­no­du­cho pri­dať fun­kciu map, kto­rá ap­li­ku­je fun­kciu na kaž­dý bod:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "cpu" and
    r.cpu == "cpu-total" and
    r._field == "usage_idle"
  )
  |> map(fn: (r) => ({ r with _value: 100.0 - r._value }))
Záťaž CPU
Ob­rá­zok 4: Zá­ťaž CPU

Ten­to do­taz je vy­so­ko ne­op­ti­mál­ny, pre­to­že vy­be­rá všet­ky dá­to­vé bo­dy bez ohľa­du na to, či je zo­bra­ze­ný graf ši­ro­ký, ale­bo úz­ky. Gra­fa­na má au­to­ma­tic­kú pre­men­nú v.windowPeriod, kto­rá ur­ču­je, aký ča­so­vý úsek za­be­rá pri­bliž­ne pi­xel v zo­bra­ze­nom gra­fe. Pri zo­bra­ze­ní zá­ťa­že CPU je ide­ál­ne roz­de­liť dá­ta na ča­so­vé ok­ná a v kaž­dom ča­so­vom ok­ne vy­po­čí­tať prie­mer­nú hod­no­tu. Pres­ne na to­to slú­ži fun­kcia aggregateWindow:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "cpu" and
    r.cpu == "cpu-total" and
    r._field == "usage_idle"
  )
  |> map(fn: (r) => ({ r with _value: 100.0 - r._value }))
  |> aggregateWindow(every: v.windowPeriod, fn: mean)

Mo­men­tál­ne pri­jí­ma­me sí­ce dá­ta len z je­di­né­ho ser­ve­ru, ale v re­ál­nom na­sa­de­ní mô­že­me mať ser­ve­rov vý­raz­ne via­cej. Pre­to je vhod­né na­sta­viť zo­sku­pe­nie dát pod­ľa hos­ti­te­ľa, vďa­ka čo­mu bu­dú v gra­fe jed­not­li­vé ser­ve­ry zo­bra­ze­né sa­mos­tat­nou kriv­kou. Zo­sku­pe­nie sa na­sta­vu­je vo­la­ním fun­kcie group pred ag­re­gač­nou fun­kci­ou.

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "cpu" and
    r.cpu == "cpu-total" and
    r._field == "usage_idle"
  )
  |> map(fn: (r) => ({ r with _value: 100.0 - r._value }))
  |> group(columns: ["host"])
  |> aggregateWindow(every: v.windowPeriod, fn: mean)

No­vý dash­bo­ard sa dá vy­tvo­riť klik­nu­tím na tla­čid­lo Add to dash­bo­ard.

Na­sle­du­je ma­lá ukáž­ka na­sta­ve­ní gra­fu CPU:

Nastavenia CPU
Ob­rá­zok 5: Na­sta­ve­nia CPU

Vý­sled­ný graf mô­že vy­ze­rať cel­kom ele­gan­tne:

Nastavený graf
Ob­rá­zok 6: Na­sta­ve­ný graf

Disk

Ak­tu­ál­ne za­pl­ne­nie dis­ku sda sa dá zis­tiť na­sle­du­jú­cim do­ta­zom:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "disk" and
    r.device == "sda" and
    r._field == "used_percent"
  )
  |> group(columns: ["host"])
  |> last()

Fun­kcia last zna­me­ná, že z dát chce­me vy­brať len po­sled­nú hod­no­tu. Typ gra­fu je v tom­to prí­pa­de Gau­ge. Po na­sta­ve­ní prí­sluš­ných li­mi­tov a far­bi­čiek je na sve­te pek­ný uka­zo­va­teľ za­pl­ne­nia dis­ku. Rov­na­kým spô­so­bom je mož­né vy­tvo­riť aj ča­so­vé prie­be­hy, aku­rát sa na­mies­to last po­u­ži­je aggregateWindow.

Zaplnenie disku
Ob­rá­zok 7: Za­pl­ne­nie dis­ku

Sieťová aktivita

Pri mo­ni­to­ro­va­ní sie­ťo­vej ak­ti­vi­ty zvy­čaj­ne po­ža­du­je­me zo­bra­ze­nie pre­no­so­vej rých­los­ti. Me­dzi dá­ta­mi, kto­ré sprí­stup­ňu­je te­le­graf však nie je pre­no­so­vá rých­losť, ale iba cel­ko­vé množ­stvo pre­ne­se­ných dát jed­ným či dru­hým sme­rom. Pre­to bu­de­me mu­sieť v tom­to prí­pa­de vy­užiť no­vé prí­ka­zy pivot a derivative.

Kom­plet­ný do­taz pre zís­ka­nie sie­ťo­vej ak­ti­vi­ty vy­ze­rá ta­to:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "net" and
    (r._field == "bytes_recv" or r._field == "bytes_sent")
  )
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({ _value: r.bytes_recv + r.bytes_sent, _time: r._time, host: r.host }))
  |> group(columns: ["host"])
  |> aggregateWindow(every: v.windowPeriod, fn: max)
  |> derivative(unit: 1s, nonNegative: true)

Do­ta­zy v da­ta­bá­ze In­flu­xDB umož­ňu­jú zís­kať len je­di­nú hod­no­tu pa­ra­met­ra, kto­rá bu­de ulo­že­ná vo vsta­va­nej pre­men­nej r._value. Je sí­ce mož­né vy­brať všet­ky hod­no­ty, kto­ré ma­jú v po­li r._field ná­zov bytes_recv ale­bo bytes_sent, ale v ta­kom prí­pa­de by sa pra­co­va­lo ako so sa­mos­tat­ný­mi riad­ka­mi v ta­buľ­ke.

Pre spo­je­nie nie­koľ­kých hod­nôt do je­di­né­ho zá­zna­mu slú­ži fun­kcia pivot, kto­rá pod­ľa kľú­ča rowKey a vy­bra­ných pa­ra­met­rov, na­prí­klad _field po­zbie­ra hod­no­ty z valueColumn a umiest­ní ich do zá­zna­mu. Vý­sled­kom je v tom­to prí­pa­de zá­znam, kto­ré­mu pri­bud­li stĺp­ce bytes_recv a bytes_recv.

Fun­kci­ou map sa ná­sled­ne hod­no­ty sčí­ta­jú a ulo­žia do vsta­va­nej pre­men­nej _value. Po zo­sku­pe­ní pod­ľa hos­ti­te­ľa a ag­re­gá­cii v ča­so­vých ok­nách do­sta­ne­me hod­no­tu ma­xi­ma pre­ne­se­ných dát na za­ria­de­ní. To by však vy­kres­li­lo len stú­pa­jú­cu kriv­ku. My však chce­me vy­kres­liť pre­no­so­vú rých­losť, te­da zme­nu pre­ne­se­ných dát v ča­se. Prá­ve na to slú­ži fun­kcia derivative.

Prenos dát
Ob­rá­zok 8: Pre­nos dát

Záťaž CPU podľa webu

Pri tom­to do­ta­ze sí­ce nie sú po­u­ži­té žiad­ne no­vé prí­ka­zy, ale na­priek to­mu si za­slú­ži po­zor­nosť vďa­ka spô­so­bu po­stup­né­ho po­u­ži­tia ag­re­gač­ných fun­kcií. Ce­lý do­taz vy­ze­rá na­sle­dov­ne:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "procstat" and
    (r._field == "cpu_usage" or r._field == "pid")
  )
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({ _time: r._time, _value: r.cpu_usage, pid: r.pid, socket: r.socket }))
  |> window(every: v.windowPeriod)
  |> group(columns: ["_start", "pid", "socket"])
  |> mean()
  |> group(columns: ["_start", "socket"])
  |> sum()
  |> group(columns: ["_time", "socket"])

Pri vý­be­re dát nás bu­dú zau­jí­mať hod­no­ty cpu_usage a pid. Keď­že sú to 2 hod­no­ty, je po­treb­né zno­vu dá­ta z via­ce­rých riad­kov uspo­ria­dať do stĺp­cov fun­kci­ou pivot. Dá­ta sú ná­sled­ne uspo­ria­da­né do no­vé­ho zá­zna­mu fun­kci­ou map a roz­de­le­né na ča­so­vé úse­ky fun­kci­ou window.

Na­sle­du­je zvlášt­na sé­ria ag­re­gač­ných fun­kcií. V pr­vom ra­de sa pre kaž­dý úsek vy­po­čí­ta prie­mer­ná hod­no­ta zá­ťa­že CPU kaž­dé­ho pro­ce­su. Ná­sled­ne sa vy­po­čí­ta sú­čet (sum) zá­ťa­že CPU jed­not­li­vých pro­ce­sov pat­ria­cich we­bu (socket). Vý­sle­dok je na­ko­niec pre­sku­pe­ný tak, aby sa dal vy­kres­liť v gra­fe. Ten­to­raz som pre vi­zu­ali­zá­ciu vy­bral typ gra­fu He­at­map s fa­reb­nou šká­lou Tur­bo.

Použitie CPU podľa používateľa
Ob­rá­zok 9: Po­u­ži­tie CPU pod­ľa po­u­ží­va­te­ľa

Gra­fa­na v ak­tu­ál­nej ver­zii zo­bra­zu­je náz­vy na­me­ra­ných hod­nôt ako _value názov webu. Od­strá­ne­nie pre­by­toč­né­ho _value je mož­né vy­ko­nať na kar­te trans­for­má­cií.

Odstránenie prefixu _value
Ob­rá­zok 10: Od­strá­ne­nie pre­fi­xu _va­lue

Zau­jí­ma­vej­šia by bo­la šta­tis­ti­ka prie­mer­né­ho vy­uži­tia CPU pod­ľa we­bu za ur­či­té ob­do­bie. V tom­to prí­pa­de bu­de stra­té­gia vý­poč­tu na jed­nej stra­ne tro­chu jed­no­duch­šia, ale na dru­hej stra­ne spô­sob zá­pi­su tak jed­no­du­chý ne­bu­de. Prin­cí­pom bu­de sčí­ta­nie všet­kých hod­nôt cpu_usage zo­sku­pe­ných pod­ľa we­bu (socket) a ná­sled­ne ich vy­de­le­nie po­čtom ča­so­vých oka­mi­hov, v kto­rých bo­lo vy­ko­na­né me­ra­nie. Na­sle­du­jú­ci do­taz vy­ze­rá po­mer­ne zlo­ži­to, ale v sku­toč­nos­ti sa skla­dá z 2 jed­no­du­chých čas­tí.

countTimestamps = from(bucket: "telegraf")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "procstat" and r._field == "cpu_usage")
  |> keep(columns: ["_time", "_measurement"])
  |> distinct(column: "_time")
  |> map(fn: (r) => ({ r with _value: 1 }))
  |> count(column: "_value")
  |> findColumn(fn: (key) => true, column: "_value")

from(bucket: "telegraf")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "procstat" and r._field == "cpu_usage")
  |> window(every: 24h)
  |> group(columns: ["socket"])
  |> sum()
  |> map(fn: (r) => ({ r with _value: r._value / float(v: countTimestamps[0]) }))
  |> group()

Pr­vá časť zis­tí po­čet ča­so­vých oka­mi­hov a ulo­ží ich do jed­no­prv­ko­vé­ho po­ľa countTimestamps. Pr­vou no­vou fun­kci­ou v tej­to čas­ti je keep, kto­rá po­ne­chá len vy­bra­né stĺp­ce. Mi­mo­cho­dom opa­kom fun­kcie keep je fun­kcia drop, kto­rá na­opak vy­bra­né stĺp­ce za­ho­dí.

Fun­kcia distinct po­ne­chá uni­kát­ne hod­no­ty vy­bra­né­ho stĺp­ca. Ná­sled­ne sa hod­no­ty fun­kci­ou mo­di­fi­ku­jú na for­mát {_value: 1, _time: čas}. Dô­vo­dom tej­to div­nej trans­for­má­cie je, že na­sle­du­jú­ca ag­re­gač­ná fun­kcia count sa ne­dá po­u­žiť na vý­sle­dok distinct.

Na­sle­du­je už len ex­trak­cia hod­no­ty z prú­du dát. Fun­kcia findColumn náj­de v prú­de všet­ky riad­ky zod­po­ve­da­jú­ce pre­d­i­ká­tu v fn a ex­tra­hu­je z nich hod­no­tu column do po­ľa. V tom­to prí­pa­de chce­me ex­tra­ho­vať hod­no­tu _value. Vý­sled­kom je te­da jed­no­prv­ko­vé po­le keď­že na vstu­pe bol len je­den ria­dok.

Dru­hou čas­ťou je kla­sic­ký sú­čet zo­sku­pe­ný pod­ľa we­bu, kto­ré­ho hod­no­ta je vy­de­le­ná po­čtom ča­so­vých oka­mi­hov vo vzor­ke dát.

Vý­sled­kom pri po­u­ži­tí Bar gau­ge je ele­gant­ný stĺp­co­vý graf.

Záťaž CPU za 24 hodín
Ob­rá­zok 11: Zá­ťaž CPU za 24 ho­dín

Aby bo­li dá­ta pek­ne zo­ra­de­né od naj­viac vy­ťa­že­né­ho we­bu, po naj­me­nej vy­ťa­že­ný, je mož­né na kon­ci pri­dať vo­la­nie fun­kcie |> sort(columns: ["_value"], desc: true), ale­bo jed­no­du­cho trans­for­mo­vať úda­je v gra­fa­ne vsta­va­nou trans­for­mač­nou fun­kci­ou Sort by.

Zoradenie údajov pomocou transformácie
Ob­rá­zok 12: Zo­ra­de­nie úda­jov po­mo­cou trans­for­má­cie

Počet dotazov na uWSGI

Na­sle­du­jú­ci graf bu­de zo­bra­zo­vať prie­mer­ný po­čet po­žia­da­viek za 5 mi­nút. Dá­ta bu­dú zís­ka­va­né z met­ri­ky uwsgi_apps, kto­rú po­sky­tu­je inputs.uwsgi. Špe­cia­li­tou v tom­to prí­pa­de bu­de, že ok­no bu­de sí­ce va­ria­bil­né, ale mi­ni­mál­na dĺž­ka ok­na bu­de 300s. Zá­ro­veň ak bu­de pe­ri­ó­da dl­h­šia, bu­de nut­né sú­čet vy­de­liť prí­sluš­ným ko­efi­cien­tom, aby bol stá­le zo­bra­ze­ný po­čet do­ta­zov za ob­do­bie 5 mi­nút. Vý­sled­ný do­taz vy­ze­rá tak­to:

minPeriod = 300s
windowPeriod = if int(v: v.windowPeriod) > int(v: minPeriod) then v.windowPeriod else minPeriod
periodValueAdjustment = float(v: int(v: minPeriod)) / float(v: int(v: windowPeriod))

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "uwsgi_apps" and r._field == "requests")
  |> difference(nonNegative: true)
  |> group(columns: ["socket"])
  |> aggregateWindow(every: duration(v: windowPeriod), fn: sum)
  |> map(fn: (r) => ({ r with _value: float(v: r._value) * periodValueAdjustment }))

Na za­čiat­ku je de­fi­no­va­ná mi­ni­mál­na pe­ri­ó­da. Ná­sled­ne sa do windowPeriod ulo­ží po­u­ží­va­te­ľom na­sta­ve­ná hod­no­ta, ale­bo mi­ni­mál­na. Ak sa pý­ta­te, pre­čo sa pe­ri­ó­da stá­le pre­ty­pu­je vo­la­ním int na ce­lé čís­lo, je to pre­to, le­bo ope­rá­cie po­rov­na­nia nie sú pre typ duration de­fi­no­va­né. Po­sled­nou z pre­men­ných je ko­efi­cient, kto­rým sa mu­sí ná­so­biť sú­čet do­ta­zov v prí­pa­de, že po­ža­do­va­ná pe­ri­ó­da je dl­h­šia než 300s.

Na­sle­du­je už kla­sic­ký vý­ber a trans­for­má­cia dát zo­sku­pe­ných pod­ľa we­bu. Vý­sle­dok je vhod­né zo­bra­ziť na­prí­klad v čia­ro­vom gra­fe s na­sta­ve­ním Stack se­ries na Nor­mal.

Počet dotazov za 5 minút
Ob­rá­zok 13: Po­čet do­ta­zov za 5 mi­nút

Po­dob­ným spô­so­bom je mož­né zís­kať aj cel­ko­vý po­čet do­ta­zov za ur­či­té ob­do­bie. Vý­sle­dok sa dá opäť zo­bra­ziť na­prí­klad cez Bar gau­ge.

from(bucket: "telegraf")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "uwsgi_apps" and r._field == "requests")
  |> difference(nonNegative: true)
  |> group(columns: ["socket"])
  |> sum()
  |> group()
  |> sort(columns: ["_value"], desc: true)
Počet dotazov za 24h
Ob­rá­zok 14: Po­čet do­ta­zov za 24h

Monitoring RAM

Pri zo­bra­ze­ní ob­sa­de­nej pa­mä­te po­sta­čia už na­do­bud­nu­té ve­do­mos­ti. Aby bo­la vý­sled­kom per­cen­tu­ál­na hod­no­ta, sta­čí vy­de­liť ob­sa­de­nú pa­mäť cel­ko­vou do­stup­nou pa­mä­ťou, čo sa de­je vo vo­la­ní fun­kcie map:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "mem" and (r._field == "total" or r._field == "available"))
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({ _value: float(v: r.total - r.available) / float(v: r.total), _time: r._time }))
  |> group(columns: ["host"])
  |> aggregateWindow(every: v.windowPeriod, fn: mean)

Vý­sle­dok sa dá vy­kres­liť na­prí­klad v jed­no­du­chom čia­ro­vom gra­fe:

Využitie RAM
Ob­rá­zok 15: Vy­uži­tie RAM

Rov­na­kou tech­ni­kou, ako pri CPU je mož­né roz­de­liť spot­re­bo­va­nú pa­mäť me­dzi jed­not­li­vé we­by:

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "procstat" and
    (r._field == "memory_usage" or r._field == "pid")
  )
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({ r with _value: r.memory_usage }))
  |> window(every: v.windowPeriod)
  |> group(columns: ["_start", "pid", "socket"])
  |> max()
  |> group(columns: ["_start", "socket"])
  |> sum()
  |> group(columns: ["_time", "socket"])

Pri vy­kres­le­ní je za­se po­u­ži­tý He­at­map s fa­reb­nou sché­mou Mag­ma.

Využitie RAM podľa používateľa
Ob­rá­zok 16: Vy­uži­tie RAM pod­ľa po­u­ží­va­te­ľa

Štatistiky HTTP stavov

Nie­ke­dy mô­že chyb­né fun­go­va­nie we­bo­vé­ho ser­ve­ru pre­zra­diť aj po­mer HTTP sta­vo­vých kó­dov. Na­prí­klad zvý­še­ný po­čet sta­vu 503 mô­že zna­me­nať pre­ťa­že­ný ser­ver. Po­čet po­žia­da­viek pod­ľa sta­vu sa dá zís­kať jed­no­du­chým do­ta­zom met­ri­ky nginx_access.

from(bucket: "telegraf")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "nginx_access" and r._field == "request_time")
  |> group(columns: ["http_status"])
  |> count()
  |> group()

Vhod­nou vi­zu­ali­zá­ci­ou pre ten­to typ dát je na­prí­klad Stat.

HTTP stavy
Ob­rá­zok 17: HTTP sta­vy

Najpomalšie URL adresy

Veľ­mi uži­toč­nou po­môc­kou pri diag­nos­ti­ke a opti­ma­li­zá­cii je zo­znam naj­po­mal­ších URL ad­ries. Na­sle­du­jú­ci do­taz vy­uží­va po­lia uwsgi_request_time a url z met­ri­ky uwsgi_requests, pri­čom sa vy­uží­va­jú len URL ad­re­sy ozna­če­né ta­gom slow.

from(bucket: "telegraf")
  |> range(start: -1h)
  |> filter(fn: (r) =>
    r._measurement == "uwsgi_requests" and
    r.speed == "slow" and
    (r._field == "uwsgi_request_time" or r._field == "url")
  )
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({ r with _value: r.uwsgi_request_time }))
  |> group(columns: ["socket", "url"])
  |> mean()
  |> group()
  |> top(n: 100, columns: ["_value"])

Pre kaž­dú URL ad­re­su sa naj­skôr vy­po­čí­ta prie­mer­ný čas od­po­ve­de. Na­ko­niec sa vy­be­rie 100 zá­zna­mov s naj­vyš­šou hod­no­tou stĺp­ca _value. Vhod­nou vi­zu­ali­zá­ci­ou pre ten­to typ dát je Tab­le.

Pomalé stránky
Ob­rá­zok 18: Po­ma­lé strán­ky

Stĺp­ce ta­buľ­ky štan­dard­ne zo­bra­zu­jú tex­to­vú hod­no­tu. V prí­pa­de, že chce­me zme­niť spô­sob zo­bra­ze­nia stĺp­cov na­prí­klad na stĺp­co­vý graf, dá sa to cez na­sta­ve­nia ta­buľ­ky na kar­te over­ri­des. Na na­sle­du­jú­com ob­ráz­ku je na­sta­ve­nie pre po­le s náz­vom _value.

Nastavenie stĺpca _value
Ob­rá­zok 19: Na­sta­ve­nie stĺp­ca _va­lue

Šablóny

Čas­to je pre­hľad­nej­šie zo­bra­ze­nie sa­mos­tat­nej sa­dy gra­fov pre kaž­dý web. Opa­ko­va­ná kon­fi­gu­rá­cia pres­ne tých is­tých do­ta­zov a gra­fov, aku­rát s je­di­ným roz­diel­nym fil­trom socket = "projekt" by bo­la pek­ná nu­da. Na­šťas­tie je tu mož­nosť ge­ne­ro­vať sa­du gra­fov po­mo­cou šab­lón.

Kon­fi­gu­rá­ciu za­čne­me pri­da­ním pre­men­nej so zo­zna­mom we­bov. Pre­men­né sa edi­tu­jú cez tla­čid­lo na­sta­ve­nia dash­bo­ar­du (ozu­be­né ko­lies­ko v hor­nej čas­ti) a sek­ciu Va­riab­les.

Na­ša pre­men­ná bu­de ty­pu Cus­tom, bu­de sa vo­lať Project a bu­de mať viac hod­nôt (Mul­ti-va­lue). Hod­no­ty sa vkla­da­jú ako text od­de­le­ný čiar­ka­mi.

V zo­bra­ze­ní dash­bo­ar­du na vr­ch­nej čas­ti pri­bud­lo po­le vý­be­ru pro­jek­tu. Aby bo­li zo­bra­ze­né gra­fy pre kon­krét­ne pro­jek­ty, je po­treb­né vy­tvo­riť no­vé ele­men­ty, kto­ré ma­jú po­vo­le­né opa­ko­va­nie. Osob­ne od­po­rú­čam naj­skôr vy­tvo­riť Row a cez tla­čid­lo ozu­be­né­ho ko­le­sa mu na­sta­viť šab­ló­nu ti­tul­ku a opa­ko­va­nie pod­ľa pro­jek­tu.

Opakovanie riadku
Ob­rá­zok 20: Opa­ko­va­nie riad­ku

Aké­koľ­vek ďal­šie zo­bra­ze­nia pri­da­né do ele­men­tu Row bu­dú au­to­ma­tic­ky opa­ko­va­né pre pro­jekt.

S po­dob­ným do­ta­zom, kto­rý zo­bra­zu­je CPU zá­ťaž we­bu sme sa už stret­li. Tu však ubud­lo zo­sku­pe­nie pod­ľa we­bu a na­opak pri­bu­dol kon­krét­ny web do vo­la­nia fun­kcie filter. Na­mies­to náz­vu pro­jek­tu sa po­u­ží­va zá­stup­ný sym­bol ${Project:raw}.

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "procstat" and
    r.socket == "${Project:raw}" and
    (r._field == "cpu_usage" or r._field == "pid")
  )
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({ _time: r._time, _value: r.cpu_usage, pid: r.pid, socket: if exists r.socket then r.socket else "" }))
  |> window(every: v.windowPeriod)
  |> group(columns: ["_start", "pid"])
  |> mean()
  |> group(columns: ["_start"])
  |> sum()
  |> group(columns: ["_time"])

Histogram s časom odpovede

Na­šu úmor­nú a stras­ti­pl­nú ces­tu za­kon­čím svä­tým grá­lom vi­zu­ali­zá­cií. Sa­mot­ný do­taz pat­rí skôr k tým jed­no­duch­ším, aj keď má svo­je špe­ci­fi­ká:

import "math"

windowPeriodBase = int(v: 120s)
windowPeriodCount = int(v: math.ceil(x: float(v: int(v: v.windowPeriod)) / float(v: windowPeriodBase)))
windowPeriod = duration(v: windowPeriodBase * windowPeriodCount)

from(bucket: "telegraf")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) =>
    r._measurement == "uwsgi_requests_histogram" and
    r.socket == "${Project:raw}" and
    r._field == "uwsgi_request_time_bucket"
  )
  |> group(columns: ["le"])
  |> aggregateWindow(every: windowPeriod, fn: sum)
  |> map(fn: (r) => ({
    r with _value:
      if exists(r._value) then
        math.log2(x: float(v: r._value) + 1.0)
      else
        0.0
  })
)

Na za­čiat­ku sú­bo­ru je vý­po­čet pe­ri­ó­dy pod­ľa hod­no­ty, kto­rú na­sta­vil po­u­ží­va­teľ. Keď­že dá­ta his­to­gra­mu sa ukla­da­jú kaž­dých 120s, mi­ni­mál­na hod­no­ta je 120s. Aby sa dá­ta zo­bra­zo­va­li bez ano­má­lií keď­že sa zo­bra­zu­je sú­čet v ok­ne, je nut­né po­u­ží­vať ce­lé ná­sob­ky hod­no­ty 120. Prá­ve pre­to sa po­čet pe­ri­ód za­ok­rúh­ľu­je na ce­lé čís­la sme­rom na­hor vo­la­ním math.ceil.

Dá­ta sú zo­sku­pe­né pod­ľa stĺp­ca le, kto­rý ob­sa­hu­je hor­nú hod­no­tu buc­ke­tu. Na­opak spod­ná hod­no­ta má stĺpec gt a je jed­no, či sa pri zo­sku­pe­ní po­u­ži­je jed­na, či dru­há.

Vi­zu­ali­zá­cia po­čtu po­žia­da­viek je lo­ga­rit­mic­ká (dvoj­ko­vý lo­ga­rit­mus na­me­ra­nej hod­no­ty). K ak­tu­ál­nej hod­no­te je pri­po­čí­ta­ná hod­no­ta 1, aby bo­li zo­bra­ze­né aj jed­not­li­vé po­žia­dav­ky. Tým­to spô­so­bom bu­dú dob­re roz­poz­na­teľ­né aj tie po­žia­dav­ky, kto­rých je sí­ce má­lo, ale trva­jú na­prí­klad veľ­mi dl­ho.

Histogram latencie v čase
Ob­rá­zok 21: His­to­gram la­ten­cie v ča­se

Kon­fi­gu­rá­cia dash­bo­ar­du je as­poň z môj­ho po­hľa­du na­te­raz fi­nál­na. Na­ko­niec pri­kla­dám sc­re­ens­hot kom­plet­né­ho dash­bo­ar­du so všet­ký­mi je­ho kur­lin­ka­mi.

Finálny dashboard
Ob­rá­zok 22: Fi­nál­ny dash­bo­ard