Zazwyczaj do monitorowania i analizowania działania Nginx wykorzystywane są produkty komercyjne lub gotowe alternatywy typu open source, takie jak Prometheus + Grafana. Jest to dobra opcja do monitorowania lub analiz w czasie rzeczywistym, ale niezbyt wygodna do analizy historycznej. W przypadku każdego popularnego zasobu ilość danych z dzienników nginx szybko rośnie, a do analizy dużej ilości danych logiczne jest użycie czegoś bardziej wyspecjalizowanego.
W tym artykule opowiem Ci, jak możesz skorzystać
TL:DR;
Aby zbierać informacje, których używamy
Zbieranie logów Nginx
Domyślnie logi Nginx wyglądają mniej więcej tak:
4/9/2019 12:58:17 PM1.1.1.1 - - [09/Apr/2019:09:58:17 +0000] "GET /sign-up HTTP/2.0" 200 9168 "https://example.com/sign-in" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"
4/9/2019 12:58:17 PM1.1.1.1 - - [09/Apr/2019:09:58:17 +0000] "GET /sign-in HTTP/2.0" 200 9168 "https://example.com/sign-up" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36" "-"
Można je analizować, ale znacznie łatwiej jest poprawić konfigurację Nginx, aby generowała logi w JSON:
log_format json_combined escape=json '{ "created_at": "$msec", '
'"remote_addr": "$remote_addr", '
'"remote_user": "$remote_user", '
'"request": "$request", '
'"status": $status, '
'"bytes_sent": $bytes_sent, '
'"request_length": $request_length, '
'"request_time": $request_time, '
'"http_referrer": "$http_referer", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"http_user_agent": "$http_user_agent" }';
access_log /var/log/nginx/access.log json_combined;
S3 do przechowywania
Do przechowywania logów użyjemy S3. Dzięki temu możesz przechowywać i analizować logi w jednym miejscu, ponieważ Athena może pracować z danymi bezpośrednio w S3. W dalszej części artykułu opowiem jak poprawnie dodawać i przetwarzać logi, ale najpierw potrzebujemy czystego wiadra w S3, w którym nic więcej nie będzie przechowywane. Warto wcześniej zastanowić się, w jakim regionie będziesz tworzyć segment, ponieważ usługa Athena nie jest dostępna we wszystkich regionach.
Tworzenie obwodu w konsoli Athena
Utwórzmy w Athenie tabelę dla logów. Jest potrzebny zarówno do pisania, jak i czytania, jeśli planujesz używać Kinesis Firehose. Otwórz konsolę Athena i utwórz tabelę:
Tworzenie tabeli SQL
CREATE EXTERNAL TABLE `kinesis_logs_nginx`(
`created_at` double,
`remote_addr` string,
`remote_user` string,
`request` string,
`status` int,
`bytes_sent` int,
`request_length` int,
`request_time` double,
`http_referrer` string,
`http_x_forwarded_for` string,
`http_user_agent` string)
ROW FORMAT SERDE
'org.apache.hadoop.hive.ql.io.orc.OrcSerde'
STORED AS INPUTFORMAT
'org.apache.hadoop.hive.ql.io.orc.OrcInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat'
LOCATION
's3://<YOUR-S3-BUCKET>'
TBLPROPERTIES ('has_encrypted_data'='false');
Tworzenie strumienia węża strażackiego Kinesis
Kinesis Firehose zapisze dane otrzymane z Nginx do S3 w wybranym formacie, dzieląc je na katalogi w formacie RRRR/MM/DD/HH. Przyda się to podczas odczytywania danych. Można oczywiście pisać bezpośrednio do S3 z poziomu fluentd, ale w tym przypadku trzeba będzie napisać JSON, a to jest nieefektywne ze względu na duży rozmiar plików. Dodatkowo w przypadku korzystania z PrestoDB lub Atheny, JSON jest najwolniejszym formatem danych. Otwórz więc konsolę Kinesis Firehose, kliknij „Utwórz strumień dostaw”, wybierz „bezpośredni PUT” w polu „dostawa”:
W następnej zakładce wybierz „Konwersja formatu zapisu” - „Włączone” i wybierz „Apache ORC” jako format nagrywania. Według niektórych badań
Wybieramy S3 do przechowywania i wiadro, które utworzyliśmy wcześniej. Aws Glue Crawler, o którym opowiem później, nie może pracować z prefiksami w wiadrze S3, dlatego ważne jest, aby pozostawić go pustym.
Pozostałe opcje można zmieniać w zależności od obciążenia; ja zazwyczaj używam opcji domyślnych. Należy pamiętać, że kompresja S3 nie jest dostępna, ale ORC domyślnie używa kompresji natywnej.
Biegły
Teraz, gdy skonfigurowaliśmy przechowywanie i odbieranie dzienników, musimy skonfigurować wysyłanie. Użyjemy
Najpierw potrzebujemy pliku konfiguracyjnego fluent.conf. Utwórz go i dodaj źródło:
Port 24224
powiąż 0.0.0.0
Teraz możesz uruchomić serwer Fluentd. Jeżeli potrzebujesz bardziej zaawansowanej konfiguracji przejdź do
$ docker run
-d
-p 24224:24224
-p 24224:24224/udp
-v /data:/fluentd/log
-v <PATH-TO-FLUENT-CONF>:/fluentd/etc fluentd
-c /fluentd/etc/fluent.conf
fluent/fluentd:stable
Ta konfiguracja używa ścieżki /fluentd/log
do buforowania dzienników przed wysłaniem. Możesz się bez tego obejść, ale po ponownym uruchomieniu możesz stracić wszystko zapisane w pamięci podręcznej z katorżniczą pracą. Możesz także użyć dowolnego portu; 24224 jest domyślnym portem Fluentd.
Teraz, gdy mamy uruchomiony Fluentd, możemy wysyłać tam logi Nginx. Zwykle uruchamiamy Nginx w kontenerze Dockera, w takim przypadku Docker ma natywny sterownik rejestrowania dla Fluentd:
$ docker run
--log-driver=fluentd
--log-opt fluentd-address=<FLUENTD-SERVER-ADDRESS>
--log-opt tag="{{.Name}}"
-v /some/content:/usr/share/nginx/html:ro
-d
nginx
Jeśli uruchomisz Nginx w inny sposób, możesz użyć plików dziennika, tak jak Fluentd
Dodajmy analizę logów skonfigurowaną powyżej do konfiguracji Fluent:
<filter YOUR-NGINX-TAG.*>
@type parser
key_name log
emit_invalid_record_to_error false
<parse>
@type json
</parse>
</filter>
I wysyłanie logów do Kinesis za pomocą
<match YOUR-NGINX-TAG.*>
@type kinesis_firehose
region region
delivery_stream_name <YOUR-KINESIS-STREAM-NAME>
aws_key_id <YOUR-AWS-KEY-ID>
aws_sec_key <YOUR_AWS-SEC_KEY>
</match>
Athena
Jeżeli wszystko skonfigurowałeś poprawnie, to po chwili (domyślnie Kinesis rejestruje otrzymane dane co 10 minut) powinieneś zobaczyć pliki logów w S3. W menu „monitorowanie” Kinesis Firehose możesz zobaczyć, ile danych jest zarejestrowanych w S3, a także błędy. Nie zapomnij dać dostępu do zapisu w segmencie S3 roli Kinesis. Jeśli Kinesis nie mógł czegoś przeanalizować, doda błędy do tego samego segmentu.
Teraz możesz przeglądać dane w Athenie. Znajdźmy najnowsze żądania, dla których zwróciliśmy błędy:
SELECT * FROM "db_name"."table_name" WHERE status > 499 ORDER BY created_at DESC limit 10;
Skanowanie wszystkich rekordów dla każdego żądania
Teraz nasze logi zostały przetworzone i zapisane w S3 w ORC, skompresowane i gotowe do analizy. Kinesis Firehose zorganizował je nawet w katalogi na każdą godzinę. Jednakże, dopóki tabela nie jest podzielona na partycje, Athena będzie ładować dane wszechczasów na każde żądanie, z rzadkimi wyjątkami. Jest to duży problem z dwóch powodów:
- Ilość danych stale rośnie, spowalniając zapytania;
- Opłaty za usługę Athena są naliczane na podstawie ilości zeskanowanych danych, przy czym każde żądanie wynosi co najmniej 10 MB.
Aby to naprawić, używamy AWS Glue Crawler, który przeszuka dane w S3 i zapisze informacje o partycji w Glue Metastore. Umożliwi nam to użycie partycji jako filtra podczas wysyłania zapytań do Atheny i przeskanowanie tylko katalogów określonych w zapytaniu.
Konfigurowanie robota Amazon Glue Crawler
Amazon Glue Crawler skanuje wszystkie dane w zasobniku S3 i tworzy tabele z partycjami. Utwórz moduł Glue Crawler z konsoli AWS Glue i dodaj wiadro, w którym przechowujesz dane. Możesz użyć jednego przeszukiwacza do kilku zasobników. W takim przypadku utworzy on w określonej bazie danych tabele o nazwach pasujących do nazw zasobników. Jeśli planujesz regularnie korzystać z tych danych, pamiętaj o skonfigurowaniu harmonogramu uruchamiania Crawlera tak, aby odpowiadał Twoim potrzebom. Używamy jednego robota indeksującego dla wszystkich tabel, który działa co godzinę.
Podzielone tabele
Po pierwszym uruchomieniu przeszukiwacza tabele dla każdego przeskanowanego zasobnika powinny pojawić się w bazie danych określonej w ustawieniach. Otwórz konsolę Athena i znajdź tabelę z logami Nginx. Spróbujmy coś przeczytać:
SELECT * FROM "default"."part_demo_kinesis_bucket"
WHERE(
partition_0 = '2019' AND
partition_1 = '04' AND
partition_2 = '08' AND
partition_3 = '06'
);
To zapytanie wybierze wszystkie rekordy otrzymane między 6:7 a 8:2019 XNUMX kwietnia XNUMX roku. Ale o ile jest to bardziej wydajne niż zwykłe czytanie z tabeli nie podzielonej na partycje? Znajdźmy i wybierzmy te same rekordy, filtrując je według sygnatury czasowej:
3.59 sekundy i 244.34 megabajtów danych w zestawie danych obejmującym tylko tydzień dzienników. Spróbujmy filtrować według partycji:
Trochę szybciej, ale co najważniejsze – tylko 1.23 megabajta danych! Byłoby znacznie taniej, gdyby nie minimalne 10 megabajtów na żądanie w cenie. Ale nadal jest znacznie lepiej, a na dużych zbiorach danych różnica będzie znacznie bardziej imponująca.
Budowa dashboardu przy użyciu Cube.js
Do montażu dashboardu wykorzystujemy framework analityczny Cube.js. Posiada całkiem sporo funkcji, ale nas interesują dwie: możliwość automatycznego korzystania z filtrów partycji oraz wstępna agregacja danych. Wykorzystuje schemat danych
Stwórzmy nową aplikację Cube.js. Ponieważ używamy już stosu AWS, logiczne jest użycie Lambdy do wdrożenia. Możesz użyć szablonu ekspresowego do generowania, jeśli planujesz hostować backend Cube.js w Heroku lub Dockerze. Dokumentacja opisuje inne
$ npm install -g cubejs-cli
$ cubejs create nginx-log-analytics -t serverless -d athena
Zmienne środowiskowe służą do konfigurowania dostępu do bazy danych w kostce.js. Generator utworzy plik .env, w którym możesz określić swoje klucze
Teraz potrzebujemy
W katalogu schema
, utwórz plik Logs.js
. Oto przykładowy model danych dla nginx:
Kod modelu
const partitionFilter = (from, to) => `
date(from_iso8601_timestamp(${from})) <= date_parse(partition_0 || partition_1 || partition_2, '%Y%m%d') AND
date(from_iso8601_timestamp(${to})) >= date_parse(partition_0 || partition_1 || partition_2, '%Y%m%d')
`
cube(`Logs`, {
sql: `
select * from part_demo_kinesis_bucket
WHERE ${FILTER_PARAMS.Logs.createdAt.filter(partitionFilter)}
`,
measures: {
count: {
type: `count`,
},
errorCount: {
type: `count`,
filters: [
{ sql: `${CUBE.isError} = 'Yes'` }
]
},
errorRate: {
type: `number`,
sql: `100.0 * ${errorCount} / ${count}`,
format: `percent`
}
},
dimensions: {
status: {
sql: `status`,
type: `number`
},
isError: {
type: `string`,
case: {
when: [{
sql: `${CUBE}.status >= 400`, label: `Yes`
}],
else: { label: `No` }
}
},
createdAt: {
sql: `from_unixtime(created_at)`,
type: `time`
}
}
});
Tutaj używamy zmiennej
Ustalamy także metryki i parametry, które chcemy wyświetlić na dashboardzie oraz określamy wstępne agregacje. Cube.js utworzy dodatkowe tabele ze wstępnie zagregowanymi danymi i automatycznie zaktualizuje dane po ich otrzymaniu. To nie tylko przyspiesza zapytania, ale także zmniejsza koszty korzystania z Atheny.
Dodajmy te informacje do pliku schematu danych:
preAggregations: {
main: {
type: `rollup`,
measureReferences: [count, errorCount],
dimensionReferences: [isError, status],
timeDimensionReference: createdAt,
granularity: `day`,
partitionGranularity: `month`,
refreshKey: {
sql: FILTER_PARAMS.Logs.createdAt.filter((from, to) =>
`select
CASE WHEN from_iso8601_timestamp(${to}) + interval '3' day > now()
THEN date_trunc('hour', now()) END`
)
}
}
}
W tym modelu określamy, że konieczne jest wstępne agregowanie danych dla wszystkich używanych metryk i stosowanie podziału na miesiące.
Teraz możemy złożyć deskę rozdzielczą!
Backend Cube.js zapewnia
Serwer Cube.js akceptuje żądanie w
{
"measures": ["Logs.errorCount"],
"timeDimensions": [
{
"dimension": "Logs.createdAt",
"dateRange": ["2019-01-01", "2019-01-07"],
"granularity": "day"
}
]
}
Zainstalujmy klienta Cube.js i bibliotekę komponentów React poprzez NPM:
$ npm i --save @cubejs-client/core @cubejs-client/react
Importujemy komponenty cubejs
и QueryRenderer
aby pobrać dane i zebrać dashboard:
Kod panelu
import React from 'react';
import { LineChart, Line, XAxis, YAxis } from 'recharts';
import cubejs from '@cubejs-client/core';
import { QueryRenderer } from '@cubejs-client/react';
const cubejsApi = cubejs(
'YOUR-CUBEJS-API-TOKEN',
{ apiUrl: 'http://localhost:4000/cubejs-api/v1' },
);
export default () => {
return (
<QueryRenderer
query={{
measures: ['Logs.errorCount'],
timeDimensions: [{
dimension: 'Logs.createdAt',
dateRange: ['2019-01-01', '2019-01-07'],
granularity: 'day'
}]
}}
cubejsApi={cubejsApi}
render={({ resultSet }) => {
if (!resultSet) {
return 'Loading...';
}
return (
<LineChart data={resultSet.rawData()}>
<XAxis dataKey="Logs.createdAt"/>
<YAxis/>
<Line type="monotone" dataKey="Logs.errorCount" stroke="#8884d8"/>
</LineChart>
);
}}
/>
)
}
Źródła panelu kontrolnego są dostępne pod adresem
Źródło: www.habr.com