Không có gì quan trọng hơn việc có mức độ quan sát tốt đối với dịch vụ và điều đó là không thể nếu không có bộ nhớ nhanh và đáng tin cậy cho nhật ký của ứng dụng. Một trong những giải pháp phổ biến nhất hiện nay là ELK ( ElasticSearch- L ogstash- K ibana), nhưng nó không phổ biến như người ta tưởng.
ELK là một giải pháp linh hoạt, tiện lợi và phức tạp để thu thập và phân tích nhật ký. Nó có khả năng mở rộng, mạnh mẽ, lọc truy vấn linh hoạt và phổ biến. Tuy nhiên, có những nhược điểm khi sử dụng ELK:
Tất cả những điều này khiến tôi băn khoăn: liệu có giải pháp nào thay thế ELK khi chúng ta nói về nhật ký không?
Đây là danh sách các yêu cầu của tôi đối với giải pháp xử lý nhật ký:
Tuy nhiên, thật tuyệt khi có một công cụ linh hoạt và nhanh chóng để thực hiện phân tích thường xuyên và đặc biệt. Hãy nói cụ thể hơn về hệ thống mà tôi sắp triển khai:
ClickHouse là một hệ thống quản lý cơ sở dữ liệu SQL hướng theo cột hiệu suất cao để xử lý phân tích trực tuyến như đã tuyên bố.
Các tính năng quan trọng nhất đối với tôi là:
Bạn có thể tìm thấy hướng dẫn cùng với trình tạo nhật ký trên GitHub .
Trước hết, hãy tạo docker-compose.yml để xác định cả hai dịch vụ. ( 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:
Hãy chạy nó và kiểm tra xem mọi thứ có hoạt động hay không: kết nối với phiên bản ClickHouse bằng ứng dụng khách ClickHouse từ bộ chứa docker và kiểm tra xem có sẵn qua localhost và cổng không
> docker-compose up -d [+] Running 3/3 ⠿ Network nginxlogprocessor_default Created 0.1s ⠿ Container clickhouse Started 0.6s
Để kết nối với phiên bản, tôi thích ứng dụng khách ClickHouse hơn
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 :)
Bây giờ, khi mọi thứ đã được thiết lập, hãy tạo cơ sở dữ liệu và bảng: cơ sở dữ liệu để theo dõi nhật ký và cơ sở dữ liệu đã xử lý cho dịch vụ cụ thể, ví dụ: nginx và bảng đầu tiên nginx.access .
Một trong những ưu điểm đáng kể của ClickHouse là cú pháp SQL cho các định nghĩa và truy vấn. Nó không hoàn toàn theo tiêu chuẩn SQL nhưng rất gần với tiêu chuẩn đó.
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
Nhìn kỹ hơn vào câu lệnh CREATE TABLE , bạn có thể thấy các loại hơi khác một chút và các loại hoàn toàn mới như Enum và IPv4. ClickHouse cố gắng giảm mức sử dụng tài nguyên và để làm được điều đó, anh ấy tối ưu hóa nó bằng các tính năng thú vị như Enum. Về cơ bản, nó là ánh xạ khóa-giá trị của một chuỗi thành int 8 bit hoặc 16 bit, tự động chuyển đổi khi chèn, nó chuyển đổi giá trị chuỗi thành int và khi lựa chọn sẽ chuyển đổi theo cách ngược lại ( link ).
IPv4, IPv6 là những loại đặc biệt để lưu trữ địa chỉ theo cách tối ưu nhất (dưới dạng unsigned int) và thể hiện chúng theo cách con người có thể đọc được, vì vậy về cơ bản tại thời điểm chèn, bạn cung cấp biểu diễn chuỗi IP-addr và ClickHouse thực hiện mọi thứ cho bạn: lưu trữ nó dưới dạng int và máy chủ được giải nén khi lựa chọn.
Hệ tư tưởng ClickHouse là chèn nhanh. Để làm được điều đó, ClickHouse xử lý các phần chèn hàng loạt tốt hơn từng phần một.
Vì vậy, tập lệnh chèn không phức tạp lắm. Hàm readFile mang lại khối dữ liệu tối đa 50k bản ghi để ứng dụng khách ClickHouse chèn vào. Mỗi mục chunk đại diện cho danh sách các giá trị liên quan đến tên cột trong danh sách 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=}')
Thực tế và thời gian thực hiện tôi có trên PC của mình, bạn có thể thấy rằng việc phân tích cú pháp và chèn tệp bản ghi 800k mất 21 giây thời gian thực thi Python. Không tệ!
> .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 sử dụng SQL để truy vấn DB, điều này rất thoải mái đối với hầu hết các kỹ sư phần mềm và đơn giản về mặt trực quan.
Hãy bắt đầu bằng việc kiểm tra số lượng bản ghi chúng tôi có, đó là 22 triệu.
a8c8da069d94 :) select count(1) from nginx.access; SELECT count(1) FROM nginx.access Query id: f94881f3-2a7d-4039-9646-a6f614adb46c ┌──count()─┐ │ 22863822 │ └──────────┘
Thật dễ dàng để có các truy vấn với các phân tích khác nhau, điều này có thể hữu ích cho việc phát hiện và giải quyết sự cố, chẳng hạn như tôi muốn biết máy chủ đang được quét tìm lỗ hổng từ địa chỉ IP nào.
Truy vấn này cho thấy các truy vấn dữ liệu linh hoạt có thể được so sánh với ELK như thế nào. Câu lệnh WITH .. AS và IN / NOT IN, truy vấn con, tổng hợp và lọc làm cho ClickHouse trở nên rất thuận tiện.
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.
Hãy cùng điểm qua 5 uri phổ biến nhất trên mỗi tên miền. Truy vấn này sử dụng hàm LIMIT x BY <field> tiện dụng.
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 là một công cụ tuyệt vời để lưu trữ và thao tác dữ liệu cụ thể như nhật ký ở quy mô lớn. Nó chắc chắn đáng để học hỏi và hiểu thêm, chẳng hạn như cấu trúc dữ liệu lồng nhau, công cụ lấy mẫu, chức năng cửa sổ và các chức năng khác
Tôi hy vọng bạn thích bài viết nhỏ này và hữu ích cho bạn!