Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring

Dette er ikke engang en spøk, det ser ut til at dette bildet mest nøyaktig gjenspeiler essensen av disse databasene, og til slutt vil det være klart hvorfor:

Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring

I følge DB-Engines Ranking er de to mest populære NoSQL-søyledatabasene Cassandra (heretter kalt CS) og HBase (HB).

Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring

Etter skjebnens vilje har vårt administrasjonsteam for datalasting i Sberbank allerede gjort det lenge siden og jobber tett med HB. I løpet av denne tiden studerte vi dens styrker og svakheter ganske godt og lærte å lage mat. Tilstedeværelsen av et alternativ i form av CS tvang oss imidlertid alltid til å plage oss selv litt med tvil: gjorde vi det riktige valget? Dessuten resultatene sammenligning, utført av DataStax, sa de at CS lett slår HB med nesten en knusende poengsum. På den annen side er DataStax en interessert part, og du bør ikke ta deres ord for det. Vi ble også forvirret over den ganske lille mengden informasjon om testforholdene, så vi bestemte oss for å finne ut på egenhånd hvem som er kongen av BigData NoSql, og resultatene som ble oppnådd viste seg å være veldig interessante.

Før du går videre til resultatene av testene som er utført, er det imidlertid nødvendig å beskrive de vesentlige aspektene ved miljøkonfigurasjonene. Faktum er at CS kan brukes i en modus som tillater tap av data. De. dette er når bare én server (node) er ansvarlig for dataene til en bestemt nøkkel, og hvis den av en eller annen grunn mislykkes, vil verdien av denne nøkkelen gå tapt. For mange oppgaver er dette ikke kritisk, men for banksektoren er dette unntaket snarere enn regelen. I vårt tilfelle er det viktig å ha flere kopier av data for pålitelig lagring.

Derfor ble bare CS-driftsmodusen i trippelreplikeringsmodus vurdert, dvs. Opprettelsen av saksområdet ble utført med følgende parametere:

CREATE KEYSPACE ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'datacenter1' : 3};

Deretter er det to måter å sikre det nødvendige konsistensnivået på. Generell regel:
NW + NR > RF

Noe som betyr at antall bekreftelser fra noder ved skriving (NW) pluss antall bekreftelser fra noder ved lesing (NR) må være større enn replikasjonsfaktoren. I vårt tilfelle er RF = 3, noe som betyr at følgende alternativer er passende:
2 + 2 > 3
3 + 1 > 3

Siden det er grunnleggende viktig for oss å lagre dataene så pålitelig som mulig, ble 3+1-ordningen valgt. I tillegg jobber HB etter et lignende prinsipp, d.v.s. en slik sammenligning vil være mer rettferdig.

Det skal bemerkes at DataStax gjorde det motsatte i sin studie, de satte RF = 1 for både CS og HB (for sistnevnte ved å endre HDFS-innstillingene). Dette er et veldig viktig aspekt fordi innvirkningen på CS-ytelsen i dette tilfellet er enorm. For eksempel viser bildet nedenfor økningen i tiden som kreves for å laste data inn i CS:

Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring

Her ser vi følgende: jo flere konkurrerende tråder skriver data, jo lengre tid tar det. Dette er naturlig, men det er viktig at ytelsesdegraderingen for RF=3 er betydelig høyere. Med andre ord, hvis vi skriver 4 tråder inn i 5 tabeller hver (20 totalt), så taper RF=3 omtrent 2 ganger (150 sekunder for RF=3 mot 75 for RF=1). Men hvis vi øker belastningen ved å laste data inn i 8 tabeller med 5 tråder hver (40 totalt), så er tapet av RF=3 allerede 2,7 ganger (375 sekunder mot 138).

Kanskje er dette delvis hemmeligheten bak den vellykkede belastningstesten utført av DataStax for CS, fordi for HB på standen vår hadde det ingen effekt å endre replikasjonsfaktoren fra 2 til 3. De. disker er ikke HB-flaskehalsen for vår konfigurasjon. Det er imidlertid mange andre fallgruver her, for det skal bemerkes at vår versjon av HB ble litt lappet og tweaket, miljøene er helt forskjellige osv. Det er også verdt å merke seg at jeg kanskje ikke vet hvordan jeg skal forberede CS riktig, og det er noen mer effektive måter å jobbe med det på, og jeg håper vi finner ut av det i kommentarene. Men først ting først.

Alle testene ble utført på en maskinvareklynge bestående av 4 servere, hver med følgende konfigurasjon:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 tråder.
Disker: 12 stk SATA HDD
java-versjon: 1.8.0_111

CS-versjon: 3.11.5

cassandra.yml parameterenum_tokens: 256
hinted_handoff_enabled: sant
hinted_handoff_throttle_in_kb: 1024
max_hints_delivery_threads: 2
hints_directory: /data10/cassandra/hints
hints_flush_period_in_ms: 10000
max_hints_file_size_in_mb: 128
batchlog_replay_throttle_in_kb: 1024
autentisering: AllowAllAuthenticator
autorisator: AllowAllAuthorizer
role_manager: CassandraRoleManager
roles_validity_in_ms: 2000
permissions_validity_in_ms: 2000
credentials_validity_in_ms: 2000
partisjonerer: org.apache.cassandra.dht.Murmur3Partitioner
datafilkataloger:
- /data1/cassandra/data # hver dataN-katalog er en separat disk
- /data2/cassandra/data
- /data3/cassandra/data
- /data4/cassandra/data
- /data5/cassandra/data
- /data6/cassandra/data
- /data7/cassandra/data
- /data8/cassandra/data
commitlog_directory: /data9/cassandra/commitlog
cdc_enabled: usant
disk_failure_policy: stopp
commit_failure_policy: stopp
preparerte_utsagn_cache_størrelse_mb:
thrift_prepared_statements_cache_size_mb:
key_cache_size_in_mb:
key_cache_save_period: 14400
rad_cache_size_in_mb: 0
row_cache_save_period: 0
counter_cache_size_in_mb:
counter_cache_save_period: 7200
saved_caches_directory: /data10/cassandra/saved_caches
commitlog_sync: periodisk
commitlog_sync_period_in_ms: 10000
commitlog_segment_size_in_mb: 32
frøleverandør:
- klassenavn: org.apache.cassandra.locator.SimpleSeedProvider
parametere:
— frø: "*,*"
concurrent_reads: 256 # prøvd 64 - ingen forskjell lagt merke til
concurrent_writes: 256 # prøvd 64 - ingen forskjell lagt merke til
concurrent_counter_writes: 256 # prøvd 64 - ingen forskjell lagt merke til
concurrent_materialized_view_writes: 32
memtable_heap_space_in_mb: 2048 # prøvd 16 GB - det var tregere
memtable_allocation_type: heap_buffers
index_summary_capacity_in_mb:
index_summary_resize_interval_in_minutes: 60
trickle_fsync: usant
trickle_fsync_interval_in_kb: 10240
lagringsport: 7000
ssl_lagringsport: 7001
lytte_adresse: *
kringkastingsadresse: *
listen_on_broadcast_address: sant
internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator
start_native_transport: sant
native_transport_port: 9042
start_rpc: sant
rpc_adresse: *
rpc_port: 9160
rpc_keepalive: sant
rpc_server_type: sync
thrift_framed_transport_size_in_mb: 15
incremental_backups: false
snapshot_before_compaction: usant
auto_snapshot: sant
column_index_size_in_kb: 64
column_index_cache_size_in_kb: 2
concurrent_compactors: 4
compaction_throughput_mb_per_sec: 1600
sstable_preemptive_open_interval_in_mb: 50
read_request_timeout_in_ms: 100000 XNUMX
range_request_timeout_in_ms: 200000 XNUMX
write_request_timeout_in_ms: 40000
counter_write_request_timeout_in_ms: 100000 XNUMX
cas_contention_timeout_in_ms: 20000
truncate_request_timeout_in_ms: 60000
request_timeout_in_ms: 200000
slow_query_log_timeout_in_ms: 500
cross_node_timeout: usant
endpoint_snitch: GossipingPropertyFileSnitch
dynamic_snitch_update_interval_in_ms: 100
dynamic_snitch_reset_interval_in_ms: 600000
dynamic_snitch_badness_threshold: 0.1
request_scheduler: org.apache.cassandra.scheduler.NoScheduler
server_encryption_options:
internode_encryption: ingen
client_encryption_options:
aktivert: falsk
internode_compression: dc
inter_dc_tcp_nodelay: usant
tracetype_query_ttl: 86400
tracetype_repair_ttl: 604800
enable_user_defined_functions: usant
enable_scripted_user_defined_functions: usant
windows_timer_interval: 1
transparent_data_encryption_options:
aktivert: falsk
gravsteinsadvarselsterskel: 1000
tombstone_failure_threshold: 100000
batch_size_warn_threshold_in_kb: 200
batch_size_fail_threshold_in_kb: 250
unlogged_batch_across_partitions_warn_threshold: 10
compaction_large_partition_warning_threshold_mb: 100
gc_warn_threshold_in_ms: 1000
back_pressure_enabled: usant
enable_materialized_views: sant
enable_sasi_indexes: sant

GC-innstillinger:

### CMS-innstillinger-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1
-XX:CMSInitiatingOccupancyFraction=75
-XX:+BrukCMSInitiatingOccupancyOnly
-XX:CMSWaitDuration=10000
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSEdenChunksRecordAlways
-XX:+CMSClassUnloadingEnabled

Minnet jvm.options ble tildelt 16 Gb (vi prøvde også 32 Gb, ingen forskjell ble lagt merke til).

Tabellene ble opprettet med kommandoen:

CREATE TABLE ks.t1 (id bigint PRIMARY KEY, title text) WITH compression = {'sstable_compression': 'LZ4Compressor', 'chunk_length_kb': 64};

HB-versjon: 1.2.0-cdh5.14.2 (i klassen org.apache.hadoop.hbase.regionserver.HRegion ekskluderte vi MetricsRegion som førte til GC da antallet regioner var mer enn 1000 på RegionServer)

Ikke-standard HBase-parameterezookeeper.session.timeout: 120000 XNUMX
hbase.rpc.timeout: 2 minutt(er)
hbase.client.scanner.timeout.periode: 2 minutt(er)
hbase.master.handler.count: 10
hbase.regionserver.lease.period, hbase.client.scanner.timeout.periode: 2 minutt(er)
hbase.regionserver.handler.count: 160
hbase.regionserver.metahandler.count: 30
hbase.regionserver.logroll.periode: 4 time(r)
hbase.regionserver.maxlogs: 200
hbase.hregion.memstore.flush.størrelse: 1 GiB
hbase.hregion.memstore.block.multiplikator: 6
hbase.hstore.compactionThreshold: 5
hbase.hstore.blockingStoreFiles: 200
hbase.hregion.majorcompaction: 1 dag(er)
HBase Service Advanced Configuration Snippet (sikkerhetsventil) for hbase-site.xml:
hbase.regionserver.wal.codecorg.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec
hbase.master.namespace.init.timeout3600000
hbase.regionserver.optionalcacheflushinterval18000000
hbase.regionserver.thread.compaction.large12
hbase.regionserver.wal.enablecompressiontrue
hbase.hstore.compaction.max.size1073741824
hbase.server.compactchecker.interval.multiplier200
Java-konfigurasjonsalternativer for HBase RegionServer:
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m
hbase.snapshot.master.timeoutMillis: 2 minutt(er)
hbase.snapshot.region.timeout: 2 minutt(er)
hbase.snapshot.master.timeout.millis: 2 minutt(er)
HBase REST Server Maks loggstørrelse: 100 MiB
HBase REST Server maksimale sikkerhetskopier av loggfiler: 5
HBase Thrift Server Maks loggstørrelse: 100 MiB
HBase Thrift Server maksimale sikkerhetskopier av loggfiler: 5
Master Max loggstørrelse: 100 MiB
Master maksimale sikkerhetskopier av loggfiler: 5
RegionServer Maks loggstørrelse: 100 MiB
RegionServer maksimale sikkerhetskopiering av loggfiler: 5
HBase Active Master Detection Window: 4 minutt(er)
dfs.client.hedged.read.threadpool.størrelse: 40
dfs.client.hedged.read.threshold.millis: 10 millisekund(er)
hbase.rest.threads.min: 8
hbase.rest.threads.max: 150
Maksimal prosessfilbeskrivelse: 180000 XNUMX
hbase.thrift.minWorkerThreads: 200
hbase.master.executor.openregion.threads: 30
hbase.master.executor.closeregion.threads: 30
hbase.master.executor.serverops.threads: 60
hbase.regionserver.thread.compaction.small: 6
hbase.ipc.server.read.threadpool.size: 20
Region Mover Threads: 6
Klient Java-haugstørrelse i byte: 1 GiB
HBase REST Server Standardgruppe: 3 GiB
HBase Thrift Server Standardgruppe: 3 GiB
Java Heap Størrelse på HBase Master i byte: 16 GiB
Java-heapstørrelse på HBase RegionServer i byte: 32 GiB

+ZooKeeper
maxClientCnxns: 601
maxSessionTimeout: 120000 XNUMX
Opprette tabeller:
hbase org.apache.hadoop.hbase.util.RegionSplitter ns:t1 UniformSplit -c 64 -f cf
endre 'ns:t1', {NAME => 'cf', DATA_BLOCK_ENCODING => 'FAST_DIFF', COMPRESSION => 'GZ'}

Det er ett viktig poeng her - DataStax-beskrivelsen sier ikke hvor mange regioner som ble brukt til å lage HB-tabellene, selv om dette er kritisk for store volumer. Derfor ble det for testene valgt kvantitet = 64, som tillater lagring av opptil 640 GB, dvs. medium størrelse bord.

På tidspunktet for testen hadde HBase 22 tusen tabeller og 67 tusen regioner (dette ville ha vært dødelig for versjon 1.2.0 hvis ikke for oppdateringen nevnt ovenfor).

Nå til koden. Siden det ikke var klart hvilke konfigurasjoner som var mer fordelaktige for en bestemt database, ble tester utført i ulike kombinasjoner. De. i noen tester ble 4 tabeller lastet samtidig (alle 4 noder ble brukt for tilkobling). I andre tester jobbet vi med 8 forskjellige tabeller. I noen tilfeller var batchstørrelsen 100, i andre 200 (batchparameter - se kode nedenfor). Datastørrelsen for verdi er 10 byte eller 100 byte (datastørrelse). Totalt ble 5 millioner poster skrevet og lest inn i hver tabell hver gang. Samtidig ble 5 tråder skrevet/lest til hver tabell (trådnummer - thNum), som hver brukte sitt eget utvalg av nøkler (antall = 1 million):

if (opType.equals("insert")) {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        StringBuilder sb = new StringBuilder("BEGIN BATCH ");
        for (int i = 0; i < batch; i++) {
            String value = RandomStringUtils.random(dataSize, true, true);
            sb.append("INSERT INTO ")
                    .append(tableName)
                    .append("(id, title) ")
                    .append("VALUES (")
                    .append(key)
                    .append(", '")
                    .append(value)
                    .append("');");
            key++;
        }
        sb.append("APPLY BATCH;");
        final String query = sb.toString();
        session.execute(query);
    }
} else {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        StringBuilder sb = new StringBuilder("SELECT * FROM ").append(tableName).append(" WHERE id IN (");
        for (int i = 0; i < batch; i++) {
            sb = sb.append(key);
            if (i+1 < batch)
                sb.append(",");
            key++;
        }
        sb = sb.append(");");
        final String query = sb.toString();
        ResultSet rs = session.execute(query);
    }
}

Følgelig ble lignende funksjonalitet gitt for HB:

Configuration conf = getConf();
HTable table = new HTable(conf, keyspace + ":" + tableName);
table.setAutoFlush(false, false);
List<Get> lGet = new ArrayList<>();
List<Put> lPut = new ArrayList<>();
byte[] cf = Bytes.toBytes("cf");
byte[] qf = Bytes.toBytes("value");
if (opType.equals("insert")) {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        lPut.clear();
        for (int i = 0; i < batch; i++) {
            Put p = new Put(makeHbaseRowKey(key));
            String value = RandomStringUtils.random(dataSize, true, true);
            p.addColumn(cf, qf, value.getBytes());
            lPut.add(p);
            key++;
        }
        table.put(lPut);
        table.flushCommits();
    }
} else {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        lGet.clear();
        for (int i = 0; i < batch; i++) {
            Get g = new Get(makeHbaseRowKey(key));
            lGet.add(g);
            key++;
        }
        Result[] rs = table.get(lGet);
    }
}

Siden oppdragsgiver i HB skal sørge for ensartet fordeling av data, så nøkkelsaltingsfunksjonen slik ut:

public static byte[] makeHbaseRowKey(long key) {
    byte[] nonSaltedRowKey = Bytes.toBytes(key);
    CRC32 crc32 = new CRC32();
    crc32.update(nonSaltedRowKey);
    long crc32Value = crc32.getValue();
    byte[] salt = Arrays.copyOfRange(Bytes.toBytes(crc32Value), 5, 7);
    return ArrayUtils.addAll(salt, nonSaltedRowKey);
}

Nå er den mest interessante delen - resultatene:

Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring

Det samme i grafform:

Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring

Fordelen med HB er så overraskende at det er mistanke om at det er en slags flaskehals i CS-oppsettet. Googling og søk etter de mest åpenbare parameterne (som concurrent_writes eller memtable_heap_space_in_mb) gjorde imidlertid ikke saken raskere. Samtidig er stokkene rene og banner ikke til noe.

Dataene ble fordelt jevnt over nodene, statistikken fra alle nodene var omtrent lik.

Slik ser tabellstatistikken ut fra en av nodeneTasterom: ks
Leseantall: 9383707
Leseforsinkelse: 0.04287025042448576 ms
Skriveantall: 15462012
Skriveforsinkelse: 0.1350068438699957 ms
Ventende skyllinger: 0
Tabell: t1
Antall SSTable: 16
Plass brukt (live): 148.59 MiB
Plass brukt (totalt): 148.59 MiB
Plass brukt av øyeblikksbilder (totalt): 0 byte
Off heap-minne brukt (totalt): 5.17 MiB
SSTabel kompresjonsforhold: 0.5720989576459437
Antall partisjoner (estimat): 3970323
Antall huskebare celler: 0
Memtable datastørrelse: 0 byte
Memtable off heap-minne brukt: 0 byte
Antall minnebare brytere: 5
Lokalt lesetall: 2346045
Lokal leseforsinkelse: NaN ms
Antall lokale skrivelser: 3865503
Lokal skriveforsinkelse: NaN ms
Ventende skyllinger: 0
Prosent reparert: 0.0
Bloom filter falske positiver: 25
Bloom filter falskt forhold: 0.00000
Bloom filter plass brukt: 4.57 MiB
Bloom-filter av heap-minne brukt: 4.57 MiB
Indekssammendrag av haugminne brukt: 590.02 KiB
Komprimeringsmetadata fra heap-minne brukt: 19.45 KiB
Minimum byte for komprimert partisjon: 36
Maks byte for komprimert partisjon: 42
Kompakt partisjon betyr byte: 42
Gjennomsnittlig levende celler per skive (siste fem minutter): NaN
Maksimalt antall levende celler per skive (siste fem minutter): 0
Gjennomsnittlig gravstein per skive (siste fem minutter): NaN
Maksimalt antall gravsteiner per skive (siste fem minutter): 0
Droppede mutasjoner: 0 byte

Et forsøk på å redusere størrelsen på partiet (selv å sende det enkeltvis) hadde ingen effekt, det ble bare verre. Det er mulig at dette faktisk er den maksimale ytelsen for CS, siden resultatene oppnådd for CS er lik de oppnådd for DataStax - omtrent hundretusenvis av operasjoner per sekund. I tillegg, hvis vi ser på ressursutnyttelse, vil vi se at CS bruker mye mer CPU og disker:

Battle of two yakozuna, eller Cassandra vs HBase. Sberbank team erfaring
Figuren viser utnyttelsen under kjøringen av alle tester på rad for begge databasene.

Angående HBs kraftige lesefordel. Her kan du se at for begge databasene er diskutnyttelsen under lesing ekstremt lav (lesetester er siste del av testsyklusen for hver database, for eksempel for CS er dette fra 15:20 til 15:40). Når det gjelder HB, er årsaken klar - det meste av data henger i minnet, i memstore, og noen er bufret i blockcache. Når det gjelder CS, er det ikke veldig klart hvordan det fungerer, men diskresirkulering er heller ikke synlig, men for sikkerhets skyld ble det gjort et forsøk på å aktivere cachen row_cache_size_in_mb = 2048 og sette caching = {'keys': 'ALL', 'rows_per_partition': ' 2000000'}, men det gjorde det enda litt verre.

Det er også verdt å nevne nok en gang et viktig poeng om antall regioner i HB. I vårt tilfelle ble verdien spesifisert som 64. Hvis du reduserer den og gjør den lik for eksempel 4, faller hastigheten med 2 ganger ved lesing. Årsaken er at memstore vil fylles opp raskere og filer vil bli spylt oftere og ved lesing må flere filer behandles, noe som er en ganske komplisert operasjon for HB. Under reelle forhold kan dette behandles ved å tenke gjennom en forhåndssplittings- og komprimeringsstrategi; spesielt bruker vi et selvskrevet verktøy som samler søppel og komprimerer HFiles konstant i bakgrunnen. Det er godt mulig at for DataStax-tester tildelte de kun 1 region per tabell (noe som ikke er riktig), og dette ville noe avklare hvorfor HB var så dårligere i leseprøvene sine.

Følgende foreløpige konklusjoner trekkes fra dette. Forutsatt at det ikke ble gjort store feil under testingen, ser Cassandra ut som en koloss med føtter av leire. Mer presist, mens hun balanserer på ett ben, som på bildet i begynnelsen av artikkelen, viser hun relativt gode resultater, men i en kamp under samme forhold taper hun direkte. Samtidig, med tanke på den lave CPU-utnyttelsen på maskinvaren vår, lærte vi å plante to RegionServer HB-er per vert og doblet dermed ytelsen. De. Tatt i betraktning ressursutnyttelsen er situasjonen for CS enda mer beklagelig.

Selvfølgelig er disse testene ganske syntetiske og mengden data som ble brukt her er relativt beskjeden. Det er mulig at hvis vi byttet til terabyte ville situasjonen vært annerledes, men mens vi for HB kan laste terabyte, viste dette seg for CS å være problematisk. Det kastet ofte en OperationTimedOutException selv med disse volumene, selv om parametrene for å vente på et svar allerede var økt flere ganger sammenlignet med standard.

Jeg håper at vi gjennom felles innsats vil finne flaskehalsene til CS, og hvis vi kan få fart på det, vil jeg definitivt legge til informasjon om de endelige resultatene på slutten av innlegget.

UPD: Takket være råd fra kamerater klarte jeg å få fart på lesingen. Var:
159 644 operasjoner (4 tabeller, 5 strømmer, batch 100).
Lagt til av:
.withLoadBalancingPolicy(ny TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().build()))
Og jeg lekte med antall tråder. Resultatet er følgende:
4 bord, 100 tråder, batch = 1 (bit for bit): 301 969 ops
4 tabeller, 100 tråder, batch = 10: 447 608 ops
4 tabeller, 100 tråder, batch = 100: 625 655 ops

Senere vil jeg bruke andre tuning tips, kjøre en full testsyklus og legge til resultatene på slutten av innlegget.

Kilde: www.habr.com

Legg til en kommentar