paint-brush
ClickHouse+Python+Nginx:如何处理日志的快速教程经过@pbityukov
1,303 讀數
1,303 讀數

ClickHouse+Python+Nginx:如何处理日志的快速教程

经过 Pavel Bityukov10m2023/10/20
Read on Terminal Reader

太長; 讀書

没有什么比拥有良好的服务可观察性更重要的了,如果没有可靠且快速的应用程序日志存储,这是不可能的。当今最流行的解决方案之一是 ELK(ElasticSearch-Logstash-Kibana),但它并不像看起来那么通用。
featured image - ClickHouse+Python+Nginx:如何处理日志的快速教程
Pavel Bityukov HackerNoon profile picture

动机

没有什么比拥有良好的服务可观察性更重要的了,如果没有可靠且快速的应用程序日志存储,这是不可能的。当今最流行的解决方案之一是 ELK( ElasticSearch - Logstash - Kibana ),但它并不像看起来那么通用。

问题陈述

ELK 是一种灵活、方便、复杂的日志收集和分析解决方案。它具有可扩展性、健壮性、灵活的查询过滤性和通用性。然而,使用ELK也有缺点:

  • 高内存和CPU资源消耗
  • 随着记录数量的增加,索引和搜索速度会下降
  • 全文搜索索引开销
  • 复杂的QueryDSL


所有这些都让我想知道:当我们谈论日志时,是否有 ELK 的替代方案?

以下是我对日志处理解决方案的要求列表:

  • 快速灵活的聚合
  • 快速插入、快速选择
  • 易于设置且资源最少
  • grafana兼容的解决方案


然而,拥有一个灵活且快速的工具来进行定期和临时分析是件好事。让我们更具体地了解我要实现的系统:

  • 处理 20M+ 日志行,这些日志行是 Nginx 访问日志
  • 获取错误 HTTP 响应的数量(4xx、5xx)
  • 获取机器人和爬虫计数器以及它们访问的 URL
  • 获取可疑机器人的前 5 个 IP 地址

什么是 ClickHouse 以及我为什么决定使用它

正如其宣称的那样,ClickHouse 是一个高性能的面向列的 SQL 数据库管理系统,用于在线分析处理。

对我来说最重要的功能是:

  • 面向列的存储意味着 ClickHouse 仅在需要时才从磁盘读取。
  • ClickHouse 可以利用所有可用资源(CPU 核心和磁盘)来执行单个查询。
  • 数据压缩
  • SQL 支持,最后但并非最不重要的

技术方案

Github 存储库教程

您可以在GitHub上找到该教程以及日志生成器。

设置 ClickHouse 并连接

首先,让我们创建 docker-compose.yml 来定义这两个服务。 ( 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:

让我们运行它并检查一切是否正常:使用 Docker 容器中的 ClickHouse 客户端连接到 ClickHouse 实例,并通过 localhost 和端口检查是否可用

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

要连接到实例,我更喜欢 ClickHouse 客户端

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 :)

创建数据库和表

现在,一切设置完毕后,让我们创建数据库和表:用于跟踪处理日志的数据库和特定服务的数据库,例如nginx ,以及第一个表nginx.access


ClickHouse 的显着优势之一是用于定义和查询的 SQL 语法。它不是严格的 SQL 标准,但非常接近它。

 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

仔细观察CREATE TABLE语句,您可以看到略有不同的类型和全新的类型,例如 Enum 和 IPv4。 ClickHouse 尝试减少资源使用,并通过 Enum 等很酷的功能对其进行优化。它基本上是字符串到 8 位或 16 位 int 的键值映射,在插入时自动转换,将字符串值转换为 int,在选择时以相反的方式转换(链接)。


IPv4、IPv6 是以最佳方式存储地址(如无符号整数)并以人类可读的方式表示这些地址的特殊类型,因此基本上在插入时您提供 IP 地址的字符串表示形式,ClickHouse 会为您完成所有操作you:将其存储为 int 并在选择时解压服务器。

日志插入

ClickHouse的理念是快速插入。为了做到这一点,ClickHouse 比逐一处理更好的批量插入。

所以插入脚本并不是很复杂。 readFile函数最多生成 50k 条记录的数据块,供 ClickHouse 客户端插入。每个块项表示与cols列表中的列名称相关的值列表

# 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=}')

我在PC上的实际执行和计时,可以看到解析和插入800k记录文件花了21s的Python执行时间。不错!

 > .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

日志分析与问题检测

ClickHouse 使用 SQL 来查询数据库,这对于大多数软件工程师来说非常方便,而且直观简单。

让我们首先检查一下我们拥有的记录数,它是 22M。

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

很容易进行不同故障的查询,这可能对问题检测和解决很有用,例如,我想知道正在从哪个 IP 地址扫描主机的漏洞。

该查询演示了与 ELK 相比数据查询的灵活性。 WITH .. AS语句和IN/NOT IN、子查询、聚合和过滤使ClickHouse非常方便。

 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.

让我们获取每个域前 5 个最受欢迎的 uri。此查询使用方便的 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.


结论

ClickHouse 是一个很好的工具,可以大规模存储和操作日志等特定数据。绝对值得进一步学习和理解,例如嵌套数据结构、采样工具、窗口函数等


我希望您喜欢这篇小文章并且对您有用!