Hvordan kører man Analytics på MySQL?
MySQL er en fantastisk database til OLTP-arbejdsbelastninger (Online Transaction Processing). For nogle virksomheder plejede det at være mere end nok i lang tid. Tiderne har ændret sig og forretningskravene med dem. Efterhånden som virksomheder stræber efter at være mere datadrevne, lagres flere og flere data til yderligere analyse; kundeadfærd, præstationsmønstre, netværkstrafik, logfiler osv. Uanset hvilken branche du er i, er det meget sandsynligt, at der er data, som du vil beholde og analysere for bedre at forstå, hvad der foregår, og hvordan du kan forbedre din forretning. Desværre er MySQL ikke den bedste mulighed for at gemme og forespørge på den store mængde data. Selvfølgelig kan det gøre det, og det har værktøjer til at hjælpe med at rumme store mængder data (f.eks. InnoDB-komprimering), men brug af en dedikeret løsning til Online Analytics Processing (OLAP) vil højst sandsynligt forbedre din evne til at gemme og forespørge en stor mængde af data.
En måde at løse dette problem på vil være at bruge en dedikeret database til at køre analyser. Typisk vil du bruge et søjleformet datalager til sådanne opgaver - de er mere velegnede til at håndtere store mængder data:data gemt i kolonner er typisk nemmere at komprimere, det er også lettere at få adgang til pr. kolonne - typisk beder du om nogle data gemt i et par kolonner - en evne til at hente kun disse kolonner i stedet for at læse alle rækkerne og filtrere unødvendige data fra gør adgangen til data hurtigere.
Hvordan replikeres data fra MySQL til ClickHouse?
Et eksempel på det kolonneformede datalager, der er velegnet til analyse, er ClickHouse, en open source-søjlebutik. En udfordring er at sikre, at dataene i ClickHouse er synkroniserede med dataene i MySQL. Selvfølgelig er det altid muligt at opsætte en datapipeline af en slags og udføre automatisk batch-indlæsning i ClickHouse. Men så længe du kan leve med nogle begrænsninger, er der en bedre måde at opsætte næsten realtidsreplikering fra MySQL til ClickHouse. I dette blogindlæg vil vi tage et kig på, hvordan det kan gøres.
ClickHouse Installation
Først og fremmest skal vi installere ClickHouse. Vi bruger hurtigstarten fra ClickHouse-webstedet.
sudo apt-get install dirmngr # optional
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv E0C56BD4 # optional
echo "deb http://repo.yandex.ru/clickhouse/deb/stable/ main/" | sudo tee /etc/apt/sources.list.d/clickhouse.list
sudo apt-get update
sudo apt-get install -y clickhouse-server clickhouse-client
sudo service clickhouse-server start
Når dette er gjort, skal vi finde et middel til at overføre data fra MySQL til ClickHouse. En af de mulige løsninger er at bruge Altinitys clickhouse-mysql-data-reader. Først og fremmest skal vi installere pip3 (python3-pip i Ubuntu), da Python i version mindst 3.4 er påkrævet. Så kan vi bruge pip3 til at installere nogle af de nødvendige Python-moduler:
pip3 install mysqlclient
pip3 install mysql-replication
pip3 install clickhouse-driver
Når dette er gjort, er vi nødt til at klone depotet. For Centos 7 er RPM'er også tilgængelige, det er også muligt at installere det ved hjælp af pip3 (clickhouse-mysql-pakke), men vi fandt ud af, at versionen tilgængelig via pip ikke indeholder de seneste opdateringer, og vi ønsker at bruge master branch fra git repository:
git clone https://github.com/Altinity/clickhouse-mysql-data-reader
Derefter kan vi installere det ved hjælp af pip:
pip3 install -e /path/to/clickhouse-mysql-data-reader/
Næste trin vil være at oprette MySQL-brugere, der kræves af clickhouse-mysql-data-reader for at få adgang til MySQL-data:
mysql> CREATE USER 'chreader'@'%' IDENTIFIED BY 'pass';
Query OK, 0 rows affected (0.02 sec)
mysql> CREATE USER 'chreader'@'127.0.0.1' IDENTIFIED BY 'pass';
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE USER 'chreader'@'localhost' IDENTIFIED BY 'pass';
Query OK, 0 rows affected (0.02 sec)
mysql> GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE, SUPER ON *.* TO 'chreader'@'%';
Query OK, 0 rows affected (0.01 sec)
mysql> GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE, SUPER ON *.* TO 'chreader'@'127.0.0.1';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE, SUPER ON *.* TO 'chreader'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)
Du bør også gennemgå din MySQL-konfiguration for at sikre, at du har binære logfiler aktiveret, max_binlog_size er sat til 768M, binlogs er i "række"-format, og at værktøjet kan oprette forbindelse til MySQL. Nedenfor er et uddrag af dokumentationen:
[mysqld]
# mandatory
server-id = 1
log_bin = /var/lib/mysql/bin.log
binlog-format = row # very important if you want to receive write, update and delete row events
# optional
expire_logs_days = 30
max_binlog_size = 768M
# setup listen address
bind-address = 0.0.0.0
Import af data
Når alt er klar, kan du importere dataene til ClickHouse. Ideelt set ville du køre importen på en vært med tabeller låst, så der ikke sker nogen ændringer under processen. Du kan bruge en slave som kilde til dataene. Kommandoen til at køre vil være:
clickhouse-mysql --src-server-id=1 --src-wait --nice-pause=1 --src-host=10.0.0.142 --src-user=chreader --src-password=pass --src-tables=wiki.pageviews --dst-host=127.0.0.1 --dst-create-table --migrate-table
Den vil oprette forbindelse til MySQL på vært 10.0.0.142 ved hjælp af givne legitimationsoplysninger, den vil kopiere tabellen 'sidevisninger' i skemaet 'wiki' til et ClickHouse, der kører på den lokale vært (127.0.0.1). Tabellen oprettes automatisk, og data vil blive migreret.
Til formålet med denne blog importerede vi omkring 50 millioner rækker fra "sidevisninger"-datasæt, som Wikimedia Foundation har stillet til rådighed. Tabelskemaet i MySQL er:
mysql> SHOW CREATE TABLE wiki.pageviews\G
*************************** 1. row ***************************
Table: pageviews
Create Table: CREATE TABLE `pageviews` (
`date` date NOT NULL,
`hour` tinyint(4) NOT NULL,
`code` varbinary(255) NOT NULL,
`title` varbinary(1000) NOT NULL,
`monthly` bigint(20) DEFAULT NULL,
`hourly` bigint(20) DEFAULT NULL,
PRIMARY KEY (`date`,`hour`,`code`,`title`)
) ENGINE=InnoDB DEFAULT CHARSET=binary
1 row in set (0.00 sec)
Værktøjet oversatte dette til følgende ClickHouse-skema:
vagrant.vm :) SHOW CREATE TABLE wiki.pageviews\G
SHOW CREATE TABLE wiki.pageviews
Row 1:
──────
statement: CREATE TABLE wiki.pageviews ( date Date, hour Int8, code String, title String, monthly Nullable(Int64), hourly Nullable(Int64)) ENGINE = MergeTree(date, (date, hour, code, title), 8192)
1 rows in set. Elapsed: 0.060 sec.
Når importen er færdig, kan vi sammenligne indholdet af MySQL:
mysql> SELECT COUNT(*) FROM wiki.pageviews\G
*************************** 1. row ***************************
COUNT(*): 50986914
1 row in set (24.56 sec)
og i ClickHouse:
vagrant.vm :) SELECT COUNT(*) FROM wiki.pageviews\G
SELECT COUNT(*)
FROM wiki.pageviews
Row 1:
──────
COUNT(): 50986914
1 rows in set. Elapsed: 0.014 sec. Processed 50.99 million rows, 50.99 MB (3.60 billion rows/s., 3.60 GB/s.)
Selv i så lille en tabel kan du tydeligt se, at MySQL krævede mere tid at scanne gennem det end ClickHouse.
Når du starter processen med at se den binære log for hændelser, vil du ideelt set videregive oplysningerne om den binære logfil og positionen, hvorfra værktøjet skal begynde at lytte. Du kan nemt kontrollere det på slaven efter den første import er fuldført.
clickhouse-mysql --src-server-id=1 --src-resume --src-binlog-file='binlog.000016' --src-binlog-position=194 --src-wait --nice-pause=1 --src-host=10.0.0.142 --src-user=chreader --src-password=pass --src-tables=wiki.pageviews --dst-host=127.0.0.1 --pump-data --csvpool
Hvis du ikke vil bestå den, begynder den bare at lytte efter alt, der kommer ind:
clickhouse-mysql --src-server-id=1 --src-resume --src-wait --nice-pause=1 --src-host=10.0.0.142 --src-user=chreader --src-password=pass --src-tables=wiki.pageviews --dst-host=127.0.0.1 --pump-data --csvpool
Lad os indlæse nogle flere data og se, hvordan det vil fungere for os. Vi kan se, at alt ser ud til at være ok, ved at se på logfilerne for clickhouse-mysql-data-reader:
2019-02-11 15:21:29,705/1549898489.705732:INFO:['wiki.pageviews']
2019-02-11 15:21:29,706/1549898489.706199:DEBUG:class:<class 'clickhouse_mysql.writer.poolwriter.PoolWriter'> insert
2019-02-11 15:21:29,706/1549898489.706682:DEBUG:Next event binlog pos: binlog.000016.42066434
2019-02-11 15:21:29,707/1549898489.707067:DEBUG:WriteRowsEvent #224892 rows: 1
2019-02-11 15:21:29,707/1549898489.707483:INFO:['wiki.pageviews']
2019-02-11 15:21:29,707/1549898489.707899:DEBUG:class:<class 'clickhouse_mysql.writer.poolwriter.PoolWriter'> insert
2019-02-11 15:21:29,708/1549898489.708083:DEBUG:Next event binlog pos: binlog.000016.42066595
2019-02-11 15:21:29,708/1549898489.708659:DEBUG:WriteRowsEvent #224893 rows: 1
Det, vi skal huske på, er værktøjets begrænsninger. Den største er, at den kun understøtter INSERT'er. Der er ingen understøttelse af SLET eller OPDATERING. Der er heller ingen understøttelse af DDL'er, derfor vil eventuelle inkompatible skemaændringer, der udføres på MySQL, bryde MySQL til ClickHouse-replikeringen.
Også værd at bemærke er det faktum, at udviklerne af scriptet anbefaler at bruge pypy til at forbedre ydeevnen af værktøjet. Lad os gennemgå nogle nødvendige trin for at konfigurere dette.
Først skal du downloade og dekomprimere pypy:
wget https://bitbucket.org/squeaky/portable-pypy/downloads/pypy3.5-7.0.0-linux_x86_64-portable.tar.bz2
tar jxf pypy3.5-7.0.0-linux_x86_64-portable.tar.bz2
cd pypy3.5-7.0.0-linux_x86_64-portable
Dernæst skal vi installere pip og alle kravene til clickhouse-mysql-data-reader - nøjagtig de samme ting, som vi dækkede tidligere, mens vi beskrev almindelig opsætning:
./bin/pypy -m ensurepip
./bin/pip3 install mysql-replication
./bin/pip3 install clickhouse-driver
./bin/pip3 install mysqlclient
Sidste trin vil være at installere clickhouse-mysql-data-reader fra github-depotet (vi antager, at det allerede er blevet klonet):
./bin/pip3 install -e /path/to/clickhouse-mysql-data-reader/
Det er alt. Fra nu af skal du køre alle kommandoerne ved at bruge det miljø, der er oprettet til pypy:
./bin/pypy ./bin/clickhouse-mysql
Tests
Data er blevet indlæst, vi kan bekræfte, at alt gik glat ved at sammenligne størrelsen af tabellen:
MySQL:
mysql> SELECT COUNT(*) FROM wiki.pageviews\G
*************************** 1. row ***************************
COUNT(*): 204899465
1 row in set (1 min 40.12 sec)
ClickHouse:
vagrant.vm :) SELECT COUNT(*) FROM wiki.pageviews\G
SELECT COUNT(*)
FROM wiki.pageviews
Row 1:
──────
COUNT(): 204899465
1 rows in set. Elapsed: 0.100 sec. Processed 204.90 million rows, 204.90 MB (2.04 billion rows/s., 2.04 GB/s.)
Alt ser korrekt ud. Lad os køre nogle forespørgsler for at se, hvordan ClickHouse opfører sig. Vær opmærksom på, at alt denne opsætning er langt fra produktionskvalitet. Vi brugte to små VM'er, 4 GB hukommelse, en vCPU hver. Derfor, selvom datasættet ikke var stort, var det nok til at se forskellen. På grund af lille stikprøve er det ret svært at lave "rigtige" analyser, men vi kan stadig sende nogle tilfældige forespørgsler.
Lad os tjekke, hvilke dage i ugen vi har data fra, og hvor mange sider der er blevet set om dagen i vores eksempeldata:
vagrant.vm :) SELECT count(*), toDayOfWeek(date) AS day FROM wiki.pageviews GROUP BY day ORDER BY day ASC;
SELECT
count(*),
toDayOfWeek(date) AS day
FROM wiki.pageviews
GROUP BY day
ORDER BY day ASC
┌───count()─┬─day─┐
│ 50986896 │ 2 │
│ 153912569 │ 3 │
└───────────┴─────┘
2 rows in set. Elapsed: 2.457 sec. Processed 204.90 million rows, 409.80 MB (83.41 million rows/s., 166.82 MB/s.)
I tilfælde af MySQL ser denne forespørgsel ud som nedenfor:
mysql> SELECT COUNT(*), DAYOFWEEK(date) AS day FROM wiki.pageviews GROUP BY day ORDER BY day;
+-----------+------+
| COUNT(*) | day |
+-----------+------+
| 50986896 | 3 |
| 153912569 | 4 |
+-----------+------+
2 rows in set (3 min 35.88 sec)
Som du kan se, brugte MySQL 3,5 minut til at lave en fuld tabelscanning.
Lad os nu se, hvor mange sider der har en månedlig værdi større end 100:
vagrant.vm :) SELECT count(*), toDayOfWeek(date) AS day FROM wiki.pageviews WHERE monthly > 100 GROUP BY day;
SELECT
count(*),
toDayOfWeek(date) AS day
FROM wiki.pageviews
WHERE monthly > 100
GROUP BY day
┌─count()─┬─day─┐
│ 83574 │ 2 │
│ 246237 │ 3 │
└─────────┴─────┘
2 rows in set. Elapsed: 1.362 sec. Processed 204.90 million rows, 1.84 GB (150.41 million rows/s., 1.35 GB/s.)
I tilfælde af MySQL er det igen 3,5 minutter:
mysql> SELECT COUNT(*), DAYOFWEEK(date) AS day FROM wiki.pageviews WHERE YEAR(date) = 2018 AND monthly > 100 GROUP BY day;
^@^@+----------+------+
| COUNT(*) | day |
+----------+------+
| 83574 | 3 |
| 246237 | 4 |
+----------+------+
2 rows in set (3 min 3.48 sec)
Endnu en forespørgsel, blot et opslag baseret på nogle strengværdier:
vagrant.vm :) select * from wiki.pageviews where title LIKE 'Main_Page' AND code LIKE 'de.m' AND hour=6;
SELECT *
FROM wiki.pageviews
WHERE (title LIKE 'Main_Page') AND (code LIKE 'de.m') AND (hour = 6)
┌───────date─┬─hour─┬─code─┬─title─────┬─monthly─┬─hourly─┐
│ 2018-05-01 │ 6 │ de.m │ Main_Page │ 8 │ 0 │
└────────────┴──────┴──────┴───────────┴─────────┴────────┘
┌───────date─┬─hour─┬─code─┬─title─────┬─monthly─┬─hourly─┐
│ 2018-05-02 │ 6 │ de.m │ Main_Page │ 17 │ 0 │
└────────────┴──────┴──────┴───────────┴─────────┴────────┘
2 rows in set. Elapsed: 0.015 sec. Processed 66.70 thousand rows, 4.20 MB (4.48 million rows/s., 281.53 MB/s.)
En anden forespørgsel, laver nogle opslag i strengen og en betingelse baseret på 'månedlig' kolonne:
vagrant.vm :) select title from wiki.pageviews where title LIKE 'United%Nations%' AND code LIKE 'en.m' AND monthly>100 group by title;
SELECT title
FROM wiki.pageviews
WHERE (title LIKE 'United%Nations%') AND (code LIKE 'en.m') AND (monthly > 100)
GROUP BY title
┌─title───────────────────────────┐
│ United_Nations │
│ United_Nations_Security_Council │
└─────────────────────────────────┘
2 rows in set. Elapsed: 0.083 sec. Processed 1.61 million rows, 14.62 MB (19.37 million rows/s., 175.34 MB/s.)
I tilfælde af MySQL ser det ud som nedenfor:
mysql> SELECT * FROM wiki.pageviews WHERE title LIKE 'Main_Page' AND code LIKE 'de.m' AND hour=6;
+------------+------+------+-----------+---------+--------+
| date | hour | code | title | monthly | hourly |
+------------+------+------+-----------+---------+--------+
| 2018-05-01 | 6 | de.m | Main_Page | 8 | 0 |
| 2018-05-02 | 6 | de.m | Main_Page | 17 | 0 |
+------------+------+------+-----------+---------+--------+
2 rows in set (2 min 45.83 sec)
Altså næsten 3 minutter. Den anden forespørgsel er den samme:
mysql> select title from wiki.pageviews where title LIKE 'United%Nations%' AND code LIKE 'en.m' AND monthly>100 group by title;
+---------------------------------+
| title |
+---------------------------------+
| United_Nations |
| United_Nations_Security_Council |
+---------------------------------+
2 rows in set (2 min 40.91 sec)
Selvfølgelig kan man argumentere for, at du kan tilføje flere indekser for at forbedre forespørgselsydeevnen, men faktum er, at tilføjelse af indekser vil kræve yderligere data, der skal gemmes på disken. Indekser kræver diskplads, og de udgør også operationelle udfordringer - hvis vi taler om OLAP-datasæt i den virkelige verden, taler vi om terabyte af data. Det tager meget tid og kræver en veldefineret og testet proces at køre skemaændringer på et sådant miljø. Dette er grunden til, at dedikerede søjleformede datalagre kan være meget praktiske og hjælpe enormt med at få bedre indsigt i alle de analysedata, som alle gemmer.