paint-brush
ClickHouse+Python+Nginx: Kurze Anleitung zum Umgang mit Ihren Protokollenvon@pbityukov
1,454 Lesungen
1,454 Lesungen

ClickHouse+Python+Nginx: Kurze Anleitung zum Umgang mit Ihren Protokollen

von Pavel Bityukov10m2023/10/20
Read on Terminal Reader

Zu lang; Lesen

Es gibt nichts Wichtigeres als eine gute Beobachtbarkeit des Dienstes, und ohne eine zuverlässige und schnelle Speicherung der Anwendungsprotokolle ist dies nicht möglich. Eine der beliebtesten Lösungen ist heutzutage ELK (ElasticSearch-Logstash-Kibana), aber sie ist nicht so universell, wie es scheint.
featured image - ClickHouse+Python+Nginx: Kurze Anleitung zum Umgang mit Ihren Protokollen
Pavel Bityukov HackerNoon profile picture

Motivation

Es gibt nichts Wichtigeres als eine gute Beobachtbarkeit des Dienstes, und ohne eine zuverlässige und schnelle Speicherung der Anwendungsprotokolle ist dies nicht möglich. Eine der beliebtesten Lösungen ist heutzutage ELK ( ElasticSearch - Logstash - Kibana ), aber sie ist nicht so universell, wie es scheint.

Problemstellung

ELK ist eine flexible, praktische und komplexe Lösung zum Sammeln und Analysieren von Protokollen. Es ist skalierbar, robust, flexible Abfragefilterung und universell. Die Verwendung von ELK hat jedoch Nachteile:

  • hoher Speicher- und CPU-Ressourcenverbrauch
  • Verschlechterung der Index- und Suchgeschwindigkeit bei zunehmender Anzahl von Datensätzen
  • Mehraufwand für den Volltextsuchindex
  • komplizierte QueryDSL


All das lässt mich fragen: Gibt es eine Alternative zu ELK, wenn wir über Protokolle sprechen?

Hier ist meine Liste der Anforderungen an die Protokollverarbeitungslösung:

  • schnelle und flexible Aggregation
  • Schnelles Einfügen, schnelle Auswahl
  • einfach einzurichten und ressourcenschonend
  • Grafana-kompatible Lösung


Es ist jedoch schön, ein flexibles und schnelles Tool für regelmäßige und Ad-hoc-Analysen zu haben. Lassen Sie uns genauer auf das System eingehen, das ich implementieren werde:

  • Verarbeiten Sie mehr als 20 Millionen Protokollzeilen, bei denen es sich um Nginx-Zugriffsprotokolle handelt
  • Ermitteln Sie die Anzahl fehlerhafter HTTP-Antworten (4xx, 5xx).
  • Erhalten Sie Bot- und Crawler-Zähler und die URL, die sie besuchen
  • Erhalten Sie die Top-5-IP-Adressen, bei denen es sich um verdächtige Bots handelt

Was ist ClickHouse und warum habe ich mich entschieden, es zu verwenden?

ClickHouse ist laut eigener Aussage ein leistungsstarkes, spaltenorientiertes SQL-Datenbankverwaltungssystem für die Online-Analyseverarbeitung.

Die wichtigsten Features waren für mich:

  • Spaltenorientierte Speicherung bedeutet, dass ClickHouse nur bei Bedarf von der Festplatte liest.
  • ClickHouse kann alle verfügbaren Ressourcen (CPU-Kerne und Festplatten) nutzen, um auch nur eine einzige Abfrage auszuführen.
  • Datenkompression
  • Zu guter Letzt SQL-Unterstützung

Technische Lösung

Github-Repo-Tutorial

Sie finden das Tutorial zusammen mit einem Protokollgenerator auf GitHub .

ClickHouse einrichten und verbinden

Erstellen wir zunächst docker-compose.yml, um beide Dienste zu definieren. ( https://github.com/bp72/nginxlogprocessor/blob/init-commit/docker-compose.yml )

 version: '3.6' services: ch: image: clickhouse/clickhouse-server container_name: clickhouse restart: always volumes: - clickhousedata:/var/lib/clickhouse/ ports: - '8123:8123' - '9000:9000' ulimits: memlock: soft: -1 hard: -1 nofile: soft: 262144 hard: 262144 # This capabilities prevents Docker from complaining about lack of those cap_add: - SYS_NICE - NET_ADMIN - IPC_LOCK volumes: clickhousedata:

Lassen Sie es uns ausführen und prüfen, ob alles funktioniert: Stellen Sie mit dem ClickHouse-Client aus dem Docker-Container eine Verbindung zur ClickHouse-Instanz her und prüfen Sie, ob über localhost und Port verfügbar

 > docker-compose up -d [+] Running 3/3 ⠿ Network nginxlogprocessor_default Created 0.1s ⠿ Container clickhouse Started 0.6s

Um eine Verbindung zur Instanz herzustellen, bevorzuge ich den ClickHouse-Client

 docker-compose exec ch clickhouse-client ClickHouse client version 23.9.1.1854 (official build). Connecting to localhost:9000 as user default. Connected to ClickHouse server version 23.9.1 revision 54466. a8c8da069d94 :)

Erstellen Sie Datenbanken und Tabellen

Wenn nun alles festgelegt ist, erstellen wir Datenbanken und Tabellen: eine Datenbank zur Verfolgung verarbeiteter Protokolle und eine Datenbank für einen bestimmten Dienst, z. B. nginx , und die erste Tabelle nginx.access .


Einer der wesentlichen Vorteile von ClickHouse ist die SQL-Syntax für Definitionen und Abfragen. Es entspricht nicht unbedingt dem SQL-Standard, kommt ihm aber sehr nahe.

 CREATE DATABASE IF NOT EXISTS nginx CREATE DATABASE IF NOT EXISTS logs CREATE TABLE IF NOT EXISTS nginx.access ( reqid String, ts DateTime64(3), level Enum(''=0, 'debug'=1, 'info'=2, 'warn'=3 ,'error'=4), domain String, uri String, ua String, ref String, is_bot Boolean, is_mobile Boolean, is_tablet Boolean, is_pc Boolean, client String, duration Float32, response_code UInt16, addrIPv4 Nullable(IPv4), addrIPv6 Nullable(IPv6), upstream_connect_time Float32, upstream_header_time Float32, upstream_response_time Float32 ) ENGINE MergeTree PRIMARY KEY reqid ORDER BY reqid CREATE TABLE IF NOT EXISTS logs.logfiles ( filename String ) ENGINE MergeTree PRIMARY KEY filename ORDER BY filename

Wenn Sie sich die CREATE TABLE- Anweisung genauer ansehen, können Sie leicht unterschiedliche Typen und völlig neue Typen wie Enum und IPv4 erkennen. ClickHouse versucht, den Ressourcenverbrauch zu reduzieren und optimiert es dazu mit coolen Funktionen wie Enum. Es handelt sich im Grunde um eine Schlüsselwertzuordnung einer Zeichenfolge zu einem 8-Bit- oder 16-Bit-Ganzzahlwert, der beim Einfügen automatisch den Zeichenfolgenwert in einen Ganzzahlwert umwandelt und bei der Auswahl in die entgegengesetzte Richtung konvertiert ( Link ).


IPv4 und IPv6 sind die speziellen Typen, um Adressen optimal zu speichern (als unsigned int) und diese auf eine für Menschen lesbare Weise darzustellen. Sie stellen also zum Zeitpunkt des Einfügens grundsätzlich eine Zeichenfolgendarstellung von IP-addr bereit, und ClickHouse erledigt alles dafür Sie: speichert es als int und wird bei Auswahl vom Server entpackt.

Protokolleinfügung

Die ClickHouse-Ideologie besteht darin, schnell einzufügen. Um dies zu erreichen, verarbeitet ClickHouse Batch-Einfügungen besser als einzelne.

Das Einfügeskript ist also nicht sehr kompliziert. Die Funktion „readFile“ liefert Datenblöcke mit maximal 50.000 Datensätzen, die der ClickHouse-Client einfügen kann. Jedes Chunk-Element stellt die Liste der Werte relativ zu den Spaltennamen in der Spaltenliste dar

 # it's not an actual code. # the working implementation you can find at https://github.com/bp72/nginxlogprocessor import clickhouse_connect from config import CLICKHOUSE_HOST, CLICKHOUSE_PORT from log import log client = clickhouse_connect.get_client(host=CLICKHOUSE_HOST, port=CLICKHOUSE_PORT) def loadToClickHouse(client, chunk): cols = [ 'reqid', 'ts', 'level', 'domain', 'uri', 'ua', 'ref', 'is_bot', 'is_mobile', 'is_tablet', 'is_pc', 'client', 'duration', 'response_code', 'addrIPv4', 'addrIPv6', 'upstream_connect_time', 'upstream_header_time', 'upstream_response_time', ] client.insert('nginx.access', chunk, column_names=cols) def processFeed(feed, client, chunk_size=10_000): total = 0 for chunk in readFile(feed, chunk_size=chunk_size): total += len(chunk) loadToClickHouse(client, chunk=chunk) log.info(f'process {feed=} inserted={len(chunk)} {total=}')

Die tatsächliche Ausführung und das Timing, die ich auf meinem PC habe, zeigen, dass das Parsen und Einfügen der Datei mit 800.000 Datensätzen 21 Sekunden Python-Ausführungszeit in Anspruch nahm. Nicht schlecht!

 > .venv/bin/python ./main.py I:2023-10-15 12:44:02 [18764] f=transport.py:1893 Connected (version 2.0, client OpenSSH_8.9p1) I:2023-10-15 12:44:02 [18764] f=transport.py:1893 Authentication (publickey) successful! I:2023-10-15 12:44:02 [18764] f=fetcher.py:14 connect host='*.*.*.*' port=22 user='root' password=None I:2023-10-15 12:44:02 [18764] f=fetcher.py:18 run cmd='ls /var/log/nginx/*access*.log-*' I:2023-10-15 12:44:02 [18764] f=fetcher.py:34 download src=/var/log/nginx/access.log-2023100812.gz dst=/tmp/access.log-2023100812.gz I:2023-10-15 12:44:07 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=50000 I:2023-10-15 12:44:08 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=100000 I:2023-10-15 12:44:10 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=150000 I:2023-10-15 12:44:11 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=200000 I:2023-10-15 12:44:13 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=250000 I:2023-10-15 12:44:14 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=300000 I:2023-10-15 12:44:15 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=350000 I:2023-10-15 12:44:17 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=400000 I:2023-10-15 12:44:18 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=450000 I:2023-10-15 12:44:20 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=500000 I:2023-10-15 12:44:21 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=550000 I:2023-10-15 12:44:23 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=600000 I:2023-10-15 12:44:24 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=650000 I:2023-10-15 12:44:25 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=700000 I:2023-10-15 12:44:27 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=750000 I:2023-10-15 12:44:28 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=800000 I:2023-10-15 12:44:28 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=2190 total=802190 I:2023-10-15 12:44:28 [18764] f=fetcher.py:34 download src=/var/log/nginx/access.log-2023100814.gz dst=/tmp/access.log-2023100814.gz I:2023-10-15 12:44:31 [18764] f=main.py:20 process feed='/tmp/access.log-2023100814.gz' inserted=50000 total=50000 I:2023-10-15 12:44:32 [18764] f=main.py:20 process feed='/tmp/access.log-2023100814.gz' inserted=50000 total=100000 I:2023-10-15 12:44:33 [18764] f=main.py:20 process feed='/tmp/access.log-2023100814.gz' inserted=30067 total=130067

Protokollanalyse und Problemerkennung

ClickHouse verwendet SQL, um die Datenbank abzufragen, was für die meisten Softwareentwickler sehr komfortabel und intuitiv einfach ist.

Beginnen wir mit der Überprüfung der Anzahl der Datensätze, die wir haben. Sie beträgt 22 Millionen.

 a8c8da069d94 :) select count(1) from nginx.access; SELECT count(1) FROM nginx.access Query id: f94881f3-2a7d-4039-9646-a6f614adb46c ┌──count()─┐ │ 22863822 │ └──────────┘

Es kommt leicht zu Abfragen mit unterschiedlichen Aufschlüsselungen, die zur Problemerkennung und -lösung nützlich sein können. Ich möchte beispielsweise wissen, von welcher IP-Adresse aus der Host auf Schwachstellen gescannt wird.

Diese Abfrage zeigt, wie flexibel Datenabfragen im Vergleich zum ELK sein können. WITH .. AS-Anweisung und IN/NOT IN, Unterabfrage, Aggregation und Filterung machen ClickHouse sehr praktisch.

 a8c8da069d94 :) with baduri as (select uri, count(1) from nginx.access where response_code = 404 and uri not in ('/about/', '/favicon.ico') group by 1 having count(1) > 3 order by 2 desc limit 10) select IPv4NumToStringClassC(addrIPv4), count(1) from nginx.access where uri in (select uri from baduri) and addrIPv4 is not null group by 1 order by 2 desc limit 5 WITH baduri AS ( SELECT uri, count(1) FROM nginx.access WHERE (response_code = 404) AND (uri NOT IN ('/about/', '/favicon.ico')) GROUP BY 1 HAVING count(1) > 3 ORDER BY 2 DESC LIMIT 10 ) SELECT IPv4NumToStringClassC(addrIPv4), count(1) FROM nginx.access WHERE (uri IN ( SELECT uri FROM baduri )) AND (addrIPv4 IS NOT NULL) GROUP BY 1 ORDER BY 2 DESC LIMIT 5 Query id: cf9bea33-212b-4c58-b6af-8e0aaae50b83 ┌─IPv4NumToStringClassC(addrIPv4)─┬─count()─┐ │ 8.219.64.xxx │ 961 │ │ 178.128.220.xxx │ 378 │ │ 103.231.78.xxx │ 338 │ │ 157.245.200.xxx │ 324 │ │ 116.203.28.xxx │ 260 │ └─────────────────────────────────┴─────────┘ 5 rows in set. Elapsed: 0.150 sec. Processed 45.73 million rows, 1.81 GB (303.88 million rows/s., 12.01 GB/s.) Peak memory usage: 307.49 MiB.

Lassen Sie uns die 5 beliebtesten URIs pro Domain ermitteln. Diese Abfrage verwendet die praktische Funktion LIMIT x BY <field>.

 a8c8da069d94 :) select domain, uri, count(1) from nginx.access where domain in ('example.com', 'nestfromthebest.com', 'az.org') group by 1, 2 order by 1, 3 desc limit 5 by domain SELECT domain, uri, count(1) FROM nginx.access WHERE domain IN ('example.com', 'nestfromthebest.com', 'az.org') GROUP BY 1, 2 ORDER BY 1 ASC, 3 DESC LIMIT 5 BY domain Query id: 2acd328c-ed82-4d36-916b-8f2ecf764a9d ┌─domain──────┬─uri────────────┬─count()─┐ │ az.org │ /about/ │ 382543 │ │ az.org │ /contacts/ │ 42066 │ │ az.org │ /category/id7 │ 2722 │ │ az.org │ /category/id14 │ 2704 │ │ az.org │ /category/id2 │ 2699 │ │ example.com │ /about/ │ 381653 │ │ example.com │ /contacts/ │ 42023 │ │ example.com │ /category/id2 │ 2694 │ │ example.com │ /category/id8 │ 2688 │ │ example.com │ /category/id13 │ 2670 │ └─────────────┴────────────────┴─────────┘ ┌─domain──────────────┬─uri────────────┬─count()─┐ │ nestfromthebest.com │ /about/ │ 383377 │ │ nestfromthebest.com │ /contacts/ │ 42100 │ │ nestfromthebest.com │ /category/id8 │ 2726 │ │ nestfromthebest.com │ /category/id14 │ 2700 │ │ nestfromthebest.com │ /category/id4 │ 2696 │ └─────────────────────┴────────────────┴─────────┘ 15 rows in set. Elapsed: 0.062 sec. Processed 23.97 million rows, 918.43 MB (388.35 million rows/s., 14.88 GB/s.) Peak memory usage: 98.67 MiB.


Abschluss

ClickHouse ist ein großartiges Tool zum Speichern und Bearbeiten spezifischer Daten wie Protokolle in großem Umfang. Es lohnt sich auf jeden Fall, weiter zu lernen und beispielsweise die verschachtelte Datenstruktur, Sampling-Tools, Fensterfunktionen und andere zu verstehen


Ich hoffe, Ihnen hat dieser kleine Artikel gefallen und er war nützlich für Sie!