Procesarea fișierelor relativ mari nu este o operație atât de ușoară care să poată fi realizată prin apelul la instrumente, care au tradiție în aceste chestiuni, precum Hadoop, mașini foarte puternice, librării pentru concurență, altele decât cele standard Java. Folosirea acestora are drept efect costuri suplimentar de timp, de bani și de personal specializat.
De exemplu, dacă în timpul procesării trebuie să validezi părți din conținut cu ajutorul unui serviciu extern și folosim Hadoop-ul pentru procesarea fișierului, se demonstrează că această practică este greșită. Toate informațiile cu care interacționează procesul ar trebui să fie parte din HDFS.
Folosirea mașinilor mai puternice (virtuale sau fizice - scalare orizontală) este un subiect destul de sensibil, pentru că unii clienți nu doresc să plătească sume mai mari de bani doar pentru a face o singură funcționalitate să fie mai rapidă, care de multe ori nici nu este prea des folosită.
Utilizarea tehnologiilor pentru concurență încă reprezintă un impediment destul de mare, chiar și cu noile abstractizări care ne sunt puse la dispoziție precum tehnologiile bazate pe conceptul de actori. Lucrurile nu s-au simplificat ci au devenit mai neclare deocamdată.Din păcate, trendul este să înțelegem/cunoaștem cât mai puțin din aceste librarii, chiar și din pachetul de concurență pus la dispoziție de Java.
Știu că pentru unii, realizarea acestei funcționalități e deja clară: citirea fișierului linie cu linie, o procesăm, apoi salvăm starea folosind clase de bază din Java. Banal! - notez această idee de acțiune cu 1.
Dacă adaug ca procesarea să fie o acțiune de tipul totul-sau-nimic, unde avem proces de validare pentru fiecare linie în parte, contorizări ale lor, alte detalii din antet sau subsoluri chiar și grupuri de entități, adică grupuri de linii care de fapt reprezintă o singură entitate. Apoi, dacă fișierul e validat cu cerințele specifice fiecărei funcționalități, salvăm entitățile create pentru fiecare linie, sau grup de linii. Cu noua cerință observăm că ideea notată cu 1, nu ne mai ajută pentru că mai întâi trebuie să validăm fișierul, iar dacă acesta e valid, atunci să salvăm datele create pe baza lui. Salvarea pe heap a datelor cu dorința de a le persista la final reprezintă o soluție pentru fișierele mici - dar totuși să nu uităm că într-o aplicație "multitenancy" resursele fizice trebuie atent distribuite - astfel că memorarea lor temporară poate cauza OOME (JVM-ul rămâne fără resurse, ceea ce cauzează ca aplicația să fie oprită) pentru fișierele mai mari (de ex. > 500 MB).
Soluția aplicată:
Chronicle. Java Chronicle.
Ce dorește să fie acestă tehnologie:
"Comunicare între procese ( IPC ) cu latență foarte mică de sub o milisecundă și capabilă să salveze fiecare mesaj."
Img. Schema pentru model de utilizare - Chronicle-Queue (2015)
Proces:
ChronicleConfig config = ChronicleConfig.DEFAULT.clone();
Chronicle entitiesChronicle = new IndexedChronicle("path", config);
citirea liniilor din fișier.
deserializarea liniilor (folosind tehnologia BeanIO - nu intră în scopul acestui articol) în POJO.
validarea entității citite (în funcție de tip).
crearea de entități adiacente (legate de logica aplicației) folosind detalii din entitatea citită anterior.
BytesMarshallable
) entitatății/entităților :
public void writeMarshallable(@NotNull Bytes out) {
if (null == entityUuid) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
out.writeUTFΔ(entityUuid.toString());
}
...
writeListEntityMessages(messages, out);
out.writeStopBit(-1);
}
scrierea lor în cronică (ExcerptAppender
):
// Start an excerpt with given chunksize
int objectSize = getObjectSize(entity); //how many bytes
entitiesAppender.startExcerpt(objectSize);
// Write the object bytes
entity.writeMarshallable(entitiesAppender);
// pad it for later.
entitiesAppender.position(objectSize);
citirea fișierului dacă toate condițiile sunt trecute cu succes.
ExcerptTailer
):
ExcerptTailer reader = entitiesChronicle.createTailer();
Entity entity = new Entity ();
entity.readMarshallable(reader);
public void readMarshallable(\@NotNull Bytes in)
throws IllegalStateException {
StringBuilder valueToRead = new StringBuilder(100);
boolean hasId = in.readBoolean();
if (hasId) {
entityUuid = readUuidValue(valueToRead, in);
}
…
messages = readListEntityMessages(in);
in.readStopBit();
}
salvăm entitățile și alte stări în Cassandra:
entitiesChronicle.close();
entitiesChronicle.clear();
Întrucât un număr poate exprima mai mult decât o mie de cuvinte, rezultatele sunt în tabelul de mai jos. Ținerea acestor entități în memoria heap nu este ceva realizabil pe hardware ieftin, dar memorarea folosind chronicle-queue devine ceva trivial.
CPU Intel i5-2410M @2.3GHz, 16GB Ram, JVM - 1GB
După cum se poate observa,timpul necesar pentru scrierea și citirea din cronică (memory-mapped file) durează aproximativ cât citirea fișierului. Așadar, dacă optați să citiți fișierul încă o dată (după validare) și să folosiți chronicle, serializarea și deserializarea obiectelor vin fără alt cost, întrucât timpii de mai sus includ acest lucru. În plus, a durat doar două ore până am avut capacitatea să salvam entitățile în cronică, deoarece API-ul e foarte simplu de utilizat și înțeles.
Versiuni mai noi ale librăriei aduce funcționalități mai specializate:
chronicle queue,
chronicle map,
chronicle logger,
chronicle engine,
Este evident că exista mulți alți factori care ar trebui luați în considerare, dar bazat pe nevoile noastre, această soluție este scalabilă și a fost aplicată cu cel mai mic efort.