Website-Statistiken und mein kleines Archiv

Das Webalizer-Tool und Google Analytics haben mir jahrelang geholfen, ein VerstĂ€ndnis dafĂŒr zu bekommen, was auf Websites passiert. Jetzt erkenne ich, dass sie nur sehr wenig nĂŒtzliche Informationen liefern. Mit Zugang zu meiner access.log-Datei ist es sehr einfach, die Statistik zu verstehen, und dafĂŒr sind lediglich elementare Werkzeuge wie SQLite, HTML, die SQL-Sprache und jede Skriptsprache erforderlich.

Die Datenquelle fĂŒr Webalizer ist die Datei access.log Server. So sehen seine Spalten und Zahlen aus, aus denen lediglich das gesamte Verkehrsvolumen ersichtlich ist:

Website-Statistiken und mein kleines Archiv
Website-Statistiken und mein kleines Archiv
Werkzeuge wie Google Analytics sammeln die Daten von der geladenen Seite selbst. Sie zeigen uns ein paar Diagramme und Linien, anhand derer es oft schwierig ist, korrekte Schlussfolgerungen zu ziehen. Vielleicht hĂ€tte ich mehr Aufwand investieren sollen? Ich weiß es nicht.

Was wollte ich also in der Besuchsstatistik der Website sehen?

Den Verkehr der Benutzer und Bots

Oft hat der Website-Verkehr eine Begrenzung, und es ist notwendig zu sehen, wie viel nĂŒtzlicher Verkehr genutzt wird. Zum Beispiel so:

Website-Statistiken und mein kleines Archiv

SQL-Abfragebericht

SELECT
1 AS 'StackedArea: Verkehr erzeugt von Benutzern und Bots',
strftime('%d.%m', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Tag',
SUM(CASE WHEN USG.AGENT_BOT!='n.a.' THEN FCT.BYTES ELSE 0 END) / 1000 AS 'Bots, KB',
SUM(CASE WHEN USG.AGENT_BOT='n.a.' THEN FCT.BYTES ELSE 0 END) / 1000 AS 'Benutzer, KB'
FROM
  FCT_ACCESS_USER_AGENT_DD FCT,
  DIM_USER_AGENT USG
WHERE FCT.DIM_USER_AGENT_ID=USG.DIM_USER_AGENT_ID
  AND datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-14 days')
GROUP BY strftime('%d.%m', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDER BY FCT.EVENT_DT

Aus dem Diagramm ist eine stÀndige AktivitÀt der Bots zu erkennen. Es wÀre interessant, die aktivsten Vertreter genauer zu analysieren.

Nervige Bots

Wir klassifizieren Bots basierend auf den Informationen des Benutzeragenten. ZusĂ€tzliche Statistiken ĂŒber den tĂ€glichen Verkehr, die Anzahl der erfolgreichen und nicht erfolgreichen Anfragen geben ein gutes Bild ĂŒber die AktivitĂ€t der Bots.

Website-Statistiken und mein kleines Archiv

SQL-Abfragebericht

SELECT 
1 AS 'Tabelle: Nervige Bots',
MAX(USG.AGENT_BOT) AS 'Bot',
ROUND(SUM(FCT.BYTES) / 1000 / 14.0, 1) AS 'KB pro Tag',
ROUND(SUM(FCT.IP_CNT) / 14.0, 1) AS 'IPs pro Tag',
ROUND(SUM(CASE WHEN STS.STATUS_GROUP IN ('Client Error', 'Server Error') THEN FCT.REQUEST_CNT / 14.0 ELSE 0 END), 1) AS 'Fehleranfragen pro Tag',
ROUND(SUM(CASE WHEN STS.STATUS_GROUP IN ('Successful', 'Redirection') THEN FCT.REQUEST_CNT / 14.0 ELSE 0 END), 1) AS 'Erfolgsanfragen pro Tag',
USG.USER_AGENT_NK AS 'Agent'
FROM FCT_ACCESS_USER_AGENT_DD FCT,
     DIM_USER_AGENT USG,
     DIM_HTTP_STATUS STS
WHERE FCT.DIM_USER_AGENT_ID = USG.DIM_USER_AGENT_ID
  AND FCT.DIM_HTTP_STATUS_ID = STS.DIM_HTTP_STATUS_ID
  AND USG.AGENT_BOT != 'n.a.'
  AND datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-14 days')
GROUP BY USG.USER_AGENT_NK
ORDER BY 3 DESC
LIMIT 10

In diesem Fall war das Ergebnis der Analyse die Entscheidung, den Zugang zur Website durch HinzufĂŒgen zur Datei robots.txt einzuschrĂ€nken.

User-Agent: AhrefsBot
Disallow: /
User-Agent: dotbot
Disallow: /
User-Agent: bingbot
Crawl-Delay: 5

Die ersten beiden Bots sind aus der Tabelle verschwunden, wÀhrend die MS-Roboter von den obersten PlÀtzen nach unten gerutscht sind.

Tag und Uhrzeit der höchsten AktivitÀt

Im Traffic sind Anstiege zu erkennen. Um diese genauer zu untersuchen, ist es notwendig, die Zeit ihrer Entstehung zu berĂŒcksichtigen, wobei es nicht erforderlich ist, alle Stunden und Tage der Zeitmessung anzuzeigen. So lĂ€sst sich bei Bedarf einfacher nach einzelnen Anfragen im Logfile suchen, wenn eine detaillierte Analyse erforderlich ist.

Website-Statistiken und mein kleines Archiv

SQL-Abfragebericht

SELECT
1 AS 'Zeile: Tag und Uhr der Zugriffe von Nutzern und Bots',
strftime('%d.%m-%H', datetime(EVENT_DT, 'unixepoch')) AS 'Datum Uhrzeit',
HIB AS 'Bots, Zugriffe',
HIU AS 'Nutzer, Zugriffe'
FROM (
	SELECT
	EVENT_DT,
	SUM(CASE WHEN AGENT_BOT!='n.a.' THEN LINE_CNT ELSE 0 END) AS HIB,
	SUM(CASE WHEN AGENT_BOT='n.a.' THEN LINE_CNT ELSE 0 END) AS HIU
	FROM FCT_ACCESS_REQUEST_REF_HH
	WHERE datetime(EVENT_DT, 'unixepoch') >= date('now', '-14 day')
	GROUP BY EVENT_DT
	ORDER BY SUM(LINE_CNT) DESC
	LIMIT 10
) ORDER BY EVENT_DT

Wir beobachten die aktivsten Stunden 11, 14 und 20 am ersten Tag im Diagramm. Am nÀchsten Tag waren die Bots um 13 Uhr aktiv.

Durchschnittliche tÀgliche AktivitÀt der Nutzer nach Wochen

Wir haben uns ein wenig mit der AktivitĂ€t und dem Traffic beschĂ€ftigt. Die nĂ€chste Frage war die AktivitĂ€t der Nutzer selbst. FĂŒr solche Statistiken sind lĂ€ngere AggregationszeitrĂ€ume wĂŒnschenswert, beispielsweise eine Woche.

Website-Statistiken und mein kleines Archiv

SQL-Abfragebericht

SELECT
1 AS 'Zeile: Durchschnittliche tÀgliche NutzeraktivitÀt nach Woche',
strftime('%W Woche', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Woche',
ROUND(1.0*SUM(FCT.PAGE_CNT)/SUM(FCT.IP_CNT),1) AS 'Seiten pro IP pro Tag',
ROUND(1.0*SUM(FCT.FILE_CNT)/SUM(FCT.IP_CNT),1) AS 'Dateien pro IP pro Tag'
FROM
  FCT_ACCESS_USER_AGENT_DD FCT,
  DIM_USER_AGENT USG,
  DIM_HTTP_STATUS HST
WHERE FCT.DIM_USER_AGENT_ID=USG.DIM_USER_AGENT_ID
  AND FCT.DIM_HTTP_STATUS_ID = HST.DIM_HTTP_STATUS_ID
  AND USG.AGENT_BOT='n.a.' /* nur Nutzer */
  AND HST.STATUS_GROUP IN ('Erfolgreich') /* gute Seiten */
  AND datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-3 month')
GROUP BY strftime('%W Woche', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDER BY FCT.EVENT_DT

Die Statistik fĂŒr die Woche zeigt, dass ein Nutzer im Durchschnitt 1,6 Seiten pro Tag öffnet. Die Anzahl der angeforderten Dateien pro Nutzer hĂ€ngt in diesem Fall von der Anzahl der neuen Dateien ab, die auf die Website hochgeladen werden.

Alle Anfragen und deren Status

Webalizer hat stets die spezifischen Seitenkodierungen angezeigt und man wollte immer einfach die Anzahl der erfolgreichen Anfragen und Fehler sehen.

Website-Statistiken und mein kleines Archiv

SQL-Abfragebericht

SELECT
1 AS 'Zeile: Alle Anfragen nach Status',
strftime('%d.%m', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Tag',
SUM(CASE WHEN STS.STATUS_GROUP='Erfolgreich' THEN FCT.REQUEST_CNT ELSE 0 END) AS 'Erfolg',
SUM(CASE WHEN STS.STATUS_GROUP='Umleitung' THEN FCT.REQUEST_CNT ELSE 0 END) AS 'Umleitung',
SUM(CASE WHEN STS.STATUS_GROUP='Client-Fehler' THEN FCT.REQUEST_CNT ELSE 0 END) AS 'Kundenfehler',
SUM(CASE WHEN STS.STATUS_GROUP='Server-Fehler' THEN FCT.REQUEST_CNT ELSE 0 END) AS 'Serverfehler'
FROM
  FCT_ACCESS_USER_AGENT_DD FCT,
  DIM_HTTP_STATUS STS
WHERE FCT.DIM_HTTP_STATUS_ID=STS.DIM_HTTP_STATUS_ID
  AND datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-14 day')
GROUP BY strftime('%d.%m', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDER BY FCT.EVENT_DT

Der Bericht zeigt Anfragen und nicht Klicks (Hits). Im Gegensatz zur LINE_CNT-Metrik wird REQUEST_CNT als COUNT(DISTINCT STG.REQUEST_NK) betrachtet. Ziel ist es, effektive Ereignisse zu zeigen, zum Beispiel fragen MS-Bots hundertmal am Tag die Datei robots.txt ab, und in diesem Fall werden solche Anfragen nur einmal gezĂ€hlt. Dies ermöglicht es, SprĂŒnge im Diagramm zu glĂ€tten.

Aus dem Diagramm sind viele Fehler ersichtlich – dies sind nicht existierende Seiten. Das Ergebnis der Analyse war die HinzufĂŒgung von Weiterleitungen von entfernten Seiten.

Fehlerhafte Anfragen

FĂŒr eine detaillierte Betrachtung der Anfragen kann eine detaillierte Statistik erstellt werden.

Website-Statistiken und mein kleines Archiv

SQL-Abfragebericht

SELECT
  1 AS 'Tabelle: HĂ€ufigste Fehleranfragen',
  REQ.REQUEST_NK AS 'Anfrage',
  'Fehler' AS 'Anfragestatus',
  ROUND(SUM(FCT.LINE_CNT) / 14.0, 1) AS 'Hits pro Tag',
  ROUND(SUM(FCT.IP_CNT) / 14.0, 1) AS 'IPs pro Tag',
  ROUND(SUM(FCT.BYTES)/1000 / 14.0, 1) AS 'KB pro Tag'
FROM
  FCT_ACCESS_REQUEST_REF_HH FCT,
  DIM_REQUEST_V_ACT REQ
WHERE FCT.DIM_REQUEST_ID = REQ.DIM_REQUEST_ID
  AND FCT.STATUS_GROUP IN ('Client Error', 'Server Error')
  AND datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-14 day')
GROUP BY REQ.REQUEST_NK
ORDER BY 4 DESC
LIMIT 20

In dieser Liste werden auch alle Anfragen enthalten sein, z.B. die Anfrage an /wp-login.php. Durch Anpassung der Regeln zur Umleitung von Anfragen Server kann die Reaktion des Servers auf solche Anfragen angepasst werden und sie zur Startseite weitergeleitet werden.

Daher liefern einige einfache Berichte auf Basis der Serverprotokolldatei ein recht umfassendes Bild dessen, was auf der Website geschieht.

Wie erhÀlt man Informationen?

Die SQLite-Datenbanken sind völlig ausreichend. Wir werden Tabellen erstellen: eine Hilfstabelle zur Protokollierung der ETL-Prozesse.

Website-Statistiken und mein kleines Archiv

Staging-Tabelle, in die wir die Protokolldateien mit PHP schreiben werden. Zwei Aggregat-Tabellen. Wir erstellen eine tĂ€gliche Tabelle mit Statistiken zu den Benutzeragenten und Anfragestati. Eine stĂŒndliche Tabelle mit Statistiken zu Anfragen, Statusgruppen und Agenten. Vier Tabellen fĂŒr die entsprechenden Dimensionen.

Das Ergebnis war das folgende relationale Modell:

DatenmodellWebsite-Statistiken und mein kleines Archiv

Skript zum Erstellen eines Objekts in der SQLite-Datenbank:

DDL zur Erstellung des Objekts

DROP TABLE IF EXISTS DIM_USER_AGENT;
CREATE TABLE DIM_USER_AGENT (
  DIM_USER_AGENT_ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  USER_AGENT_NK     TEXT NOT NULL DEFAULT 'n.a.',
  AGENT_OS          TEXT NOT NULL DEFAULT 'n.a.',
  AGENT_ENGINE      TEXT NOT NULL DEFAULT 'n.a.',
  AGENT_DEVICE      TEXT NOT NULL DEFAULT 'n.a.',
  AGENT_BOT         TEXT NOT NULL DEFAULT 'n.a.',
  UPDATE_DT         INTEGER NOT NULL DEFAULT 0,
  UNIQUE (USER_AGENT_NK)
);
INSERT INTO DIM_USER_AGENT (DIM_USER_AGENT_ID) VALUES (-1);

Staging

Im Fall der access.log-Datei mĂŒssen alle Anfragen gelesen, geparsed und in die Datenbank geschrieben werden. Dies kann entweder direkt mit Hilfe einer Skriptsprache oder mithilfe von SQLite-Tools geschehen.

Dateiformat des Protokolls:

//67.221.59.195 - - [28/Dec/2012:01:47:47 +0100] "GET /files/default.css HTTP/1.1" 200 1512 "https://project.edu/" "Mozilla/4.0"
//host ident auth time method request_nk protocol status bytes ref browser
$log_pattern = '/^([^ ]+) ([^ ]+) ([^ ]+) ([[^]]+]) "(.*) (.*) (.*)" ([0-9-]+) ([0-9-]+) "(.*)" "(.*)"$/';

Key-Promotion

Wenn die Rohdaten in der Datenbank sind, mĂŒssen die SchlĂŒssel, die dort fehlen, in die Maßtabelle geschrieben werden. Dann wird der Verweis auf die Maße möglich. Zum Beispiel ist der SchlĂŒssel in der DIM_REFERRER-Tabelle eine Kombination aus drei Feldern.

SQL-Abfrage zur Key-Promotion

/* Propagate the referrer from access log */
INSERT INTO DIM_REFERRER (HOST_NK, PATH_NK, QUERY_NK, UPDATE_DT)
SELECT
	CLS.HOST_NK,
	CLS.PATH_NK,
	CLS.QUERY_NK,
	STRFTIME('%s','now') AS UPDATE_DT
FROM (
	SELECT DISTINCT
	REFERRER_HOST AS HOST_NK,
	REFERRER_PATH AS PATH_NK,
	CASE WHEN INSTR(REFERRER_QUERY,'&sid')>0 THEN SUBSTR(REFERRER_QUERY, 1, INSTR(REFERRER_QUERY,'&sid')-1) /* ĐŸŃ‚Ń€Đ”Đ·Đ°Đ”ĐŒ sid - спДцОфОĐșа Ń†ĐŒŃ */
	ELSE REFERRER_QUERY END AS QUERY_NK
	FROM STG_ACCESS_LOG
) CLS
LEFT OUTER JOIN DIM_REFERRER TRG
ON (CLS.HOST_NK = TRG.HOST_NK AND CLS.PATH_NK = TRG.PATH_NK AND CLS.QUERY_NK = TRG.QUERY_NK)
WHERE TRG.DIM_REFERRER_ID IS NULL

Die Promotion in die Tabelle der Benutzeragenten kann Bot-Logik enthalten, zum Beispiel ein SQL-Ausschnitt:


CASE
WHEN INSTR(LOWER(CLS.BROWSER),'yandex.com')>0
	THEN 'yandex'
WHEN INSTR(LOWER(CLS.BROWSER),'googlebot')>0
	THEN 'google'
WHEN INSTR(LOWER(CLS.BROWSER),'bingbot')>0
	THEN 'microsoft'
WHEN INSTR(LOWER(CLS.BROWSER),'ahrefsbot')>0
	THEN 'ahrefs'
WHEN INSTR(LOWER(CLS.BROWSER),'mj12bot')>0
	THEN 'majestic-12'
WHEN INSTR(LOWER(CLS.BROWSER),'compatible')>0 OR INSTR(LOWER(CLS.BROWSER),'http')>0
	OR INSTR(LOWER(CLS.BROWSER),'libwww')>0 OR INSTR(LOWER(CLS.BROWSER),'spider')>0
	OR INSTR(LOWER(CLS.BROWSER),'java')>0 OR INSTR(LOWER(CLS.BROWSER),'python')>0
	OR INSTR(LOWER(CLS.BROWSER),'robot')>0 OR INSTR(LOWER(CLS.BROWSER),'curl')>0
	OR INSTR(LOWER(CLS.BROWSER),'wget')>0
	THEN 'other'
ELSE 'n.a.' END AS AGENT_BOT

Aggregat-Tabellen

Zuletzt werden wir die Aggregat-Tabellen laden, zum Beispiel kann die tĂ€gliche Tabelle folgendermaßen geladen werden:

SQL-Abfrage zum Laden des Aggregats

/* Load fact from access log */
INSERT INTO FCT_ACCESS_USER_AGENT_DD (EVENT_DT, DIM_USER_AGENT_ID, DIM_HTTP_STATUS_ID, PAGE_CNT, FILE_CNT, REQUEST_CNT, LINE_CNT, IP_CNT, BYTES)
WITH STG AS (
SELECT
	STRFTIME( '%s', SUBSTR(TIME_NK,9,4) || '-' ||
	CASE SUBSTR(TIME_NK,5,3)
	WHEN 'Jan' THEN '01' WHEN 'Feb' THEN '02' WHEN 'Mar' THEN '03' WHEN 'Apr' THEN '04' WHEN 'May' THEN '05' WHEN 'Jun' THEN '06'
	WHEN 'Jul' THEN '07' WHEN 'Aug' THEN '08' WHEN 'Sep' THEN '09' WHEN 'Oct' THEN '10' WHEN 'Nov' THEN '11'
	ELSE '12' END || '-' || SUBSTR(TIME_NK,2,2) || ' 00:00:00' ) AS EVENT_DT,
	BROWSER AS USER_AGENT_NK,
	REQUEST_NK,
	IP_NR,
	STATUS,
	LINE_NK,
	BYTES
FROM STG_ACCESS_LOG
)
SELECT
	CAST(STG.EVENT_DT AS INTEGER) AS EVENT_DT,
	USG.DIM_USER_AGENT_ID,
	HST.DIM_HTTP_STATUS_ID,
	COUNT(DISTINCT (CASE WHEN INSTR(STG.REQUEST_NK,'.')=0 THEN STG.REQUEST_NK END) ) AS PAGE_CNT,
	COUNT(DISTINCT (CASE WHEN INSTR(STG.REQUEST_NK,'.')>0 THEN STG.REQUEST_NK END) ) AS FILE_CNT,
	COUNT(DISTINCT STG.REQUEST_NK) AS REQUEST_CNT,
	COUNT(DISTINCT STG.LINE_NK) AS LINE_CNT,
	COUNT(DISTINCT STG.IP_NR) AS IP_CNT,
	SUM(BYTES) AS BYTES
FROM STG,
	DIM_HTTP_STATUS HST,
	DIM_USER_AGENT USG
WHERE STG.STATUS = HST.STATUS_NK
  AND STG.USER_AGENT_NK = USG.USER_AGENT_NK
  AND CAST(STG.EVENT_DT AS INTEGER) > $param_epoch_from /* load epoch date */
  AND CAST(STG.EVENT_DT AS INTEGER) < strftime('%s', date('now', 'start of day'))
GROUP BY STG.EVENT_DT, HST.DIM_HTTP_STATUS_ID, USG.DIM_USER_AGENT_ID

Die SQLite-Datenbank erlaubt komplizierte Abfragen. WITH enthĂ€lt die Vorbereitung der Daten und SchlĂŒssel. Die Hauptabfrage sammelt alle Verweise auf die Maße.

Die Bedingung lÀsst demnach die Historie nicht noch einmal laden: CAST(STG.EVENT_DT AS INTEGER) > $param_epoch_from, wobei der Parameter das Ergebnis der Abfrage ist
‘SELECT COALESCE(MAX(EVENT_DT), ‘3600’) AS LAST_EVENT_EPOCH FROM FCT_ACCESS_USER_AGENT_DD’

Die Bedingung lĂ€dt nur den vollen Tag: CAST(STG.EVENT_DT AS INTEGER) < strftime(‘%s’, date(‘now’, ‘start of day’))

Die ZĂ€hlung der Seiten oder Dateien erfolgt auf einfache Weise, indem ein Punkt gesucht wird.

Berichte

In komplexen Visualisierungssystemen gibt es die Möglichkeit, ein Metamodell auf Basis von Datenbankobjekten zu erstellen, Filter und Aggregationsregeln dynamisch zu steuern. Letztendlich erzeugen alle anstÀndigen Werkzeuge SQL-Abfragen.

In diesem Beispiel erstellen wir bereits fertige SQL-Abfragen und speichern sie als Views in der Datenbank – das sind die Berichte.

Visualisierung

Als Visualisierungswerkzeug wurde Bluff: Beautiful graphs in JavaScript verwendet.

DafĂŒr war es notwendig, mit PHP durch alle Berichte zu gehen und eine HTML-Datei mit Tabellen zu generieren.

$sqls = array(
'SELECT * FROM RPT_ACCESS_USER_VS_BOT',
'SELECT * FROM RPT_ACCESS_ANNOYING_BOT',
'SELECT * FROM RPT_ACCESS_TOP_HOUR_HIT',
'SELECT * FROM RPT_ACCESS_USER_ACTIVE',
'SELECT * FROM RPT_ACCESS_REQUEST_STATUS',
'SELECT * FROM RPT_ACCESS_TOP_REQUEST_PAGE',
'SELECT * FROM RPT_ACCESS_TOP_REQUEST_REFERRER',
'SELECT * FROM RPT_ACCESS_NEW_REQUEST',
'SELECT * FROM RPT_ACCESS_TOP_REQUEST_SUCCESS',
'SELECT * FROM RPT_ACCESS_TOP_REQUEST_ERROR'
);

Das Tool visualisiert einfach die Ergebnistabellen.

Ausgabe

Anhand des Beispiels der Webanalyse beschreibt der Artikel die Mechanismen, die zur Erstellung von Data Warehouses erforderlich sind. Wie aus den Ergebnissen hervorgeht, sind die einfachsten Werkzeuge ausreichend fĂŒr eine tiefgehende Analyse und Visualisierung der Daten.

Im Folgenden werden wir anhand dieses Data Warehouses versuchen, Strukturen wie langsam sich Àndernde Dimensionen, Metadaten, Aggregationsebenen und die Integration von Daten aus verschiedenen Quellen zu implementieren.

Außerdem werden wir ein einfaches Tool zur Verwaltung von ETL-Prozessen auf der Basis einer einzigen Tabelle nĂ€her betrachten.

Kehren wir zum Thema der Messung der DatenqualitĂ€t und der Automatisierung dieses Prozesses zurĂŒck.

Wir werden die Probleme der technischen Umgebung und der Wartung von Data Warehouses untersuchen, indem wir einen Speicher-Server mit minimalen Ressourcen implementieren, beispielsweise auf Basis eines Raspberry Pi.

Quelle: habr.com

60GB SSD 8Gb DDR4