Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast

En ĉi tiu artikolo ni parolos pri kiel kaj kial ni disvolviĝis Interaga Sistemo – mekanismo kiu transdonas informojn inter klientaplikoj kaj 1C:Enterprise-serviloj - de fiksado de tasko ĝis pripensado de la arkitekturo kaj efektivigdetaloj.

La Interaga Sistemo (ĉi-poste nomata SV) estas distribuita, mistolerema mesaĝsistemo kun garantiita livero. SV estas desegnita kiel alt-ŝarĝa servo kun alta skaleblo, havebla kaj kiel interreta servo (provizita de 1C) kaj kiel amasproduktita produkto kiu povas esti deplojita sur viaj propraj servilaj instalaĵoj.

SV uzas distribuitan stokadon hazelcast kaj serĉilo Elasta esploro. Ni ankaŭ parolos pri Java kaj kiel ni horizontale skalas PostgreSQL.
Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast

Formulado de la problemo

Por klarigi kial ni kreis la Interago-Sistemon, mi rakontos al vi iomete pri kiel funkcias la disvolviĝo de komercaj aplikaĵoj en 1C.

Komence, iom pri ni por tiuj, kiuj ankoraŭ ne scias, kion ni faras :) Ni faras la teknologian platformon 1C:Enterprise. La platformo inkluzivas komercan aplikaĵan evoluilon, same kiel rultempon, kiu permesas komercajn aplikaĵojn funkcii en transplatforma medio.

Kliento-servila evoluparadigmo

Komercaj aplikaĵoj kreitaj sur 1C:Enterprise funkcias en trinivelo kliento-servilo arkitekturo "DBMS - aplika servilo - kliento". Aplika kodo skribita enen enkonstruita 1C lingvo, povas esti efektivigita sur la aplikaĵoservilo aŭ sur la kliento. Ĉiuj laboroj kun aplikaj objektoj (dosierujoj, dokumentoj, ktp.), same kiel legado kaj skribo de la datumbazo, estas farita nur sur la servilo. La funkcieco de formoj kaj komanda interfaco ankaŭ estas efektivigita sur la servilo. La kliento plenumas ricevi, malfermi kaj montri formojn, "komuniki" kun la uzanto (avertoj, demandoj...), malgrandajn kalkulojn en formoj, kiuj postulas rapidan respondon (ekzemple, multobligi la prezon per kvanto), laborante kun lokaj dosieroj, laborante kun ekipaĵo.

En aplika kodo, la kaplinioj de proceduroj kaj funkcioj devas eksplicite indiki kie la kodo estos ekzekutita - uzante la direktivojn &AtClient / &AtServer (&AtClient / &AtServer en la angla versio de la lingvo). 1C programistoj nun korektos min dirante ke direktivoj estas fakte pli ol, sed por ni ĉi tio ne gravas nun.

Vi povas voki servilan kodon de klientokodo, sed vi ne povas voki klientan kodon de servila kodo. Ĉi tio estas fundamenta limigo, kiun ni faris pro kelkaj kialoj. Precipe, ĉar servila kodo devas esti skribita tiel, ke ĝi ekzekutas same, negrave kie ĝi estas vokita - de la kliento aŭ de la servilo. Kaj en la kazo de vokado de servila kodo de alia servila kodo, ne ekzistas kliento kiel tia. Kaj ĉar dum la ekzekuto de la servila kodo, la kliento, kiu vokis ĝin, povus fermiĝi, eliri la aplikaĵon, kaj la servilo ne plu havus iun por voki.

Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast
Kodo kiu manipulas butonklakon: voki servilan proceduron de la kliento funkcios, voki klientan proceduron de la servilo ne funkcios

Ĉi tio signifas, ke se ni volas sendi iun mesaĝon de la servilo al la klienta aplikaĵo, ekzemple, ke la generacio de "longdaŭra" raporto finiĝis kaj la raporto povas esti vidita, ni ne havas tian metodon. Vi devas uzi lertaĵojn, ekzemple, periode sondi la servilon de la klientokodo. Sed ĉi tiu aliro ŝarĝas la sistemon per nenecesaj vokoj, kaj ĝenerale ne aspektas tre eleganta.

Kaj ankaŭ necesas, ekzemple, kiam alvenas telefonvoko SIP- dum voko, informu la klientan aplikaĵon pri tio, por ke ĝi povu uzi la numeron de la alvokanto por trovi ĝin en la datumbazo de la kontraŭpartio kaj montri al la uzanto informojn pri la alvokanta kontraŭparto. Aŭ, ekzemple, kiam mendo alvenas al la magazeno, sciigu la klientan aplikaĵon de la kliento pri tio. Ĝenerale, estas multaj kazoj, kie tia mekanismo estus utila.

La produktado mem

Kreu mesaĝan mekanismon. Rapida, fidinda, kun garantiita livero, kun la kapablo flekseble serĉi mesaĝojn. Surbaze de la mekanismo, efektivigu mesaĝiston (mesaĝoj, videovokoj) funkcianta ene de 1C-aplikoj.

Desegni la sistemon por esti horizontale skalebla. La kreskanta ŝarĝo devas esti kovrita per pliigo de la nombro da nodoj.

Реализация

Ni decidis ne integri la servilan parton de SV rekte en la platformon 1C:Enterprise, sed efektivigi ĝin kiel apartan produkton, kies API povas esti nomita el la kodo de 1C aplikaj solvoj. Ĉi tio estis farita pro kelkaj kialoj, la ĉefa el kiuj estis, ke mi volis ebligi interŝanĝi mesaĝojn inter malsamaj 1C-aplikoj (ekzemple inter Komerca Administrado kaj Kontado). Malsamaj 1C-aplikoj povas funkcii per malsamaj versioj de la platformo 1C:Enterprise, troviĝi sur malsamaj serviloj ktp. En tiaj kondiĉoj, la efektivigo de SV kiel aparta produkto situanta "flanke" de 1C-instalaĵoj estas la optimuma solvo.

Do, ni decidis fari SV kiel apartan produkton. Ni rekomendas, ke malgrandaj kompanioj uzu la CB-servilon, kiun ni instalis en nia nubo (wss://1cdialog.com) por eviti la superkostojn asociitajn kun loka instalado kaj agordo de la servilo. Grandaj klientoj eble trovos konsilinde instali sian propran CB-servilon ĉe siaj instalaĵoj. Ni uzis similan aliron en nia nuba SaaS-produkto 1cFreŝa - ĝi estas produktita kiel amasproduktita produkto por instalo ĉe la retejoj de klientoj, kaj ankaŭ estas deplojita en nia nubo https://1cfresh.com/.

Apliko

Por distribui la toleremon pri ŝarĝo kaj misfunkciado, ni deplojos ne unu Java-aplikaĵon, sed plurajn, kun ŝarĝbalancilo antaŭ ili. Se vi bezonas translokigi mesaĝon de nodo al nodo, uzu publikigi/aboni en Hazelcast.

Komunikado inter la kliento kaj la servilo estas per websocket. Ĝi taŭgas por realtempaj sistemoj.

Distribuita kaŝmemoro

Ni elektis inter Redis, Hazelcast kaj Ehcache. Estas 2015. Redis ĵus publikigis novan areton (tro nova, timiga), ekzistas Sentinel kun multaj limigoj. Ehcache ne scias kiel kunveni en areton (ĉi tiu funkcio aperis poste). Ni decidis provi ĝin kun Hazelcast 3.4.
Hazelcast estas kunvenita en areton el la kesto. En ununoda reĝimo, ĝi ne estas tre utila kaj povas esti uzata nur kiel kaŝmemoro - ĝi ne scias kiel forĵeti datumojn al disko, se vi perdas la solan nodon, vi perdas la datumojn. Ni deplojas plurajn Hazelcasts, inter kiuj ni rezervas kritikajn datumojn. Ni ne rezervas la kaŝmemoron - ni ne ĝenas ĝin.

Por ni, Hazelcast estas:

  • Stokado de uzantsesioj. Necesas longa tempo por iri al la datumbazo por sesio ĉiufoje, do ni metas ĉiujn sesiojn en Hazelcast.
  • Kaŝaĵo. Se vi serĉas uzantprofilon, kontrolu la kaŝmemoron. Skribis novan mesaĝon - metu ĝin en la kaŝmemoron.
  • Temoj por komunikado inter aplikaj petskriboj. La nodo generas eventon kaj metas ĝin en la temo Hazelcast. Aliaj aplikaĵnodoj abonitaj al ĉi tiu temo ricevas kaj prilaboras la eventon.
  • Grapo-seruroj. Ekzemple, ni kreas diskuton uzante unikan ŝlosilon (ununura diskuto ene de la datumbazo 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Ni kontrolis, ke ne ekzistas kanalo. Ni prenis la seruron, kontrolis ĝin denove kaj kreis ĝin. Se vi ne kontrolas la seruron post preni la seruron, tiam estas ŝanco ke alia fadeno ankaŭ kontrolis en tiu momento kaj nun provos krei la saman diskuton - sed ĝi jam ekzistas. Vi ne povas ŝlosi per sinkronigita aŭ regula java Ŝlosilo. Per la datumbazo - ĝi estas malrapida, kaj estas domaĝe por la datumbazo; per Hazelcast - tion vi bezonas.

Elektante DBMS

Ni havas ampleksan kaj sukcesan sperton laborante kun PostgreSQL kaj kunlaborante kun la programistoj de ĉi tiu DBMS.

Ne estas facila kun PostgreSQL-grupo - ekzistas XL, XC, Citus, sed ĝenerale ĉi tiuj ne estas NoSQL-oj kiuj skalas el la skatolo. Ni ne konsideris NoSQL kiel la ĉefa stokado; sufiĉis, ke ni prenis Hazelcast, kun kiu ni antaŭe ne laboris.

Se vi bezonas skali rilatan datumbazon, tio signifas sharding. Kiel vi scias, kun sharding ni dividas la datumbazon en apartajn partojn, por ke ĉiu el ili estu metita sur apartan servilon.

La unua versio de nia sharding supozis la kapablon distribui ĉiun el la tabeloj de nia aplikaĵo tra malsamaj serviloj en malsamaj proporcioj. Estas multaj mesaĝoj sur la servilo A - bonvolu, ni movu parton de ĉi tiu tabelo al la servilo B. Ĉi tiu decido simple kriis pri antaŭtempa optimumigo, do ni decidis limigi nin al plur-luanto.

Vi povas legi pri plurluanto, ekzemple, en la retejo Citus Datumoj.

SV havas la konceptojn de aplikaĵo kaj abonanto. Apliko estas specifa instalaĵo de komerca aplikaĵo, kiel ERP aŭ Kontado, kun ĝiaj uzantoj kaj komercaj datumoj. Abonanto estas organizo aŭ individuo, en kies nomo la aplikaĵo estas registrita en la SV-servilo. Abonanto povas havi plurajn aplikojn registritajn, kaj ĉi tiuj aplikoj povas interŝanĝi mesaĝojn inter si. La abonanto fariĝis luanto en nia sistemo. Mesaĝoj de pluraj abonantoj povas troviĝi en unu fizika datumbazo; se ni vidas, ke abonanto komencis generi multe da trafiko, ni movas ĝin al aparta fizika datumbazo (aŭ eĉ aparta datumbaza servilo).

Ni havas ĉefan datumbazon, kie vojtabelo estas stokita kun informoj pri la loko de ĉiuj datumbazoj de abonantoj.

Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast

Por malhelpi la ĉefa datumbazo esti proplemkolo, ni konservas la enrutigan tabelon (kaj aliajn ofte bezonatajn datumojn) en kaŝmemoro.

Se la datumbazo de la abonanto komencas malrapidiĝi, ni tranĉos ĝin en sekciojn enen. En aliaj projektoj ni uzas pg_pathman.

Ĉar perdi uzantmesaĝojn estas malbona, ni konservas niajn datumbazojn kun kopioj. La kombinaĵo de sinkronaj kaj nesinkronaj kopioj permesas vin certigi en kazo de perdo de la ĉefa datumbazo. Mesaĝperdo nur okazos se la primara datumbazo kaj ĝia sinkrona kopio malsukcesas samtempe.

Se sinkrona kopio estas perdita, la nesinkrona kopio iĝas sinkrona.
Se la ĉefa datumbazo estas perdita, la sinkrona kopio iĝas la ĉefa datumbazo, kaj la nesinkrona kopio fariĝas sinkrona kopio.

Elasticsearch por serĉo

Ĉar interalie SV ankaŭ estas mesaĝisto, ĝi postulas rapidan, oportunan kaj flekseblan serĉon, konsiderante morfologion, uzante neprecizajn kongruojn. Ni decidis ne reinventi la radon kaj uzi la senpagan serĉilon Elasticsearch, kreita surbaze de la biblioteko Lucene. Ni ankaŭ deplojas Elasticsearch en areto (majstro - datumoj - datumoj) por forigi problemojn en la okazo de fiasko de aplikaj nodoj.

Sur github ni trovis Rusa morfologia kromaĵo por Elasticsearch kaj uzu ĝin. En la Elasticsearch-indekso ni konservas vortradikojn (kiujn la kromprogramon determinas) kaj N-gramojn. Dum la uzanto enigas tekston por serĉi, ni serĉas la tajpitan tekston inter N-gramoj. Se konservite al la indekso, la vorto "tekstoj" estos dividita en la sekvajn N-gramojn:

[tiuj, tek, tex, teksto, tekstoj, ek, eks, ekst, tekstoj, ks, kst, ksty, st, sty, vi],

Kaj la radiko de la vorto "teksto" ankaŭ estos konservita. Ĉi tiu aliro permesas serĉi ĉe la komenco, en la mezo, kaj ĉe la fino de la vorto.

Ĝenerala bildo

Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast
Ripeto de la bildo de la komenco de la artikolo, sed kun klarigoj:

  • Balancilo elmontrita en la Interreto; ni havas nginx, ĝi povas esti ajna.
  • Java aplikaĵkazoj komunikas unu kun la alia per Hazelcast.
  • Por labori kun ttt socket ni uzas Netty.
  • La Java aplikaĵo estas skribita en Java 8 kaj konsistas el pakaĵoj OSGi. La planoj inkluzivas migradon al Java 10 kaj transiron al moduloj.

Disvolviĝo kaj testado

En la procezo de disvolviĝo kaj testado de la SV, ni trovis kelkajn interesajn funkciojn de la produktoj, kiujn ni uzas.

Ŝarĝo-testado kaj memorfuĝoj

La liberigo de ĉiu SV-eldono implikas ŝarĝtestadon. Ĝi estas sukcesa kiam:

  • La testo funkciis dum pluraj tagoj kaj estis neniuj servaj misfunkciadoj
  • Responda tempo por ŝlosilaj operacioj ne superis komfortan sojlon
  • Efikecmalboniĝo kompare kun la antaŭa versio estas ne pli ol 10%

Ni plenigas la testan datumbazon per datumoj - por fari tion, ni ricevas informojn pri la plej aktiva abonanto de la produktservilo, multobligas ĝiajn nombrojn per 5 (la nombro da mesaĝoj, diskutoj, uzantoj) kaj provas ĝin tiel.

Ni faras ŝarĝtestadon de la interaga sistemo en tri agordoj:

  1. strestesto
  2. Nur konektoj
  3. Registrado de abonanto

Dum la streĉa provo, ni lanĉas plurajn centojn da fadenoj, kaj ili ŝarĝas la sistemon sen ĉesi: verki mesaĝojn, krei diskutojn, ricevi liston de mesaĝoj. Ni simulas la agojn de ordinaraj uzantoj (ricevi liston de miaj nelegitaj mesaĝoj, skribu al iu) kaj programajn solvojn (sendi pakon de malsama agordo, prilabori alarmon).

Ekzemple, jen kiel aspektas parto de la streĉa provo:

  • Uzanto ensalutas
    • Petas viajn nelegitajn diskutojn
    • 50% verŝajne legos mesaĝojn
    • 50% verŝajna al teksto
    • Sekva uzanto:
      • Havas 20% ŝancon krei novan diskuton
      • Hazarde elektas iun ajn el ĝiaj diskutoj
      • Eniras
      • Petoj mesaĝoj, uzantprofiloj
      • Kreas kvin mesaĝojn adresitajn al hazardaj uzantoj de ĉi tiu diskuto
      • Forlasas diskuton
      • Ripetas 20 fojojn
      • Elsalutas, reiras al la komenco de la skripto

    • Babilejo eniras la sistemon (imulas mesaĝojn de aplika kodo)
      • Havas 50% ŝancon krei novan kanalon por interŝanĝo de datumoj (speciala diskuto)
      • 50% verŝajne skribos mesaĝon al iu el la ekzistantaj kanaloj

La scenaro "Nur Konektoj" aperis ial. Estas situacio: uzantoj konektis la sistemon, sed ankoraŭ ne enmiksiĝis. Ĉiu uzanto ŝaltas la komputilon je 09:00 matene, establas konekton al la servilo kaj restas silenta. Ĉi tiuj uloj estas danĝeraj, estas multaj el ili - la nuraj pakaĵoj kiujn ili havas estas PING/PONG, sed ili konservas la konekton al la servilo (ili ne povas daŭrigi ĝin - kio se estas nova mesaĝo). La testo reproduktas situacion, kie granda nombro da tiaj uzantoj provas ensaluti al la sistemo en duonhoro. Ĝi similas al streĉtesto, sed ĝia fokuso estas ĝuste sur ĉi tiu unua enigo - por ke ne estu malsukcesoj (homo ne uzas la sistemon, kaj ĝi jam defalas - estas malfacile pensi pri io pli malbona).

La registra skripto de abonantoj komenciĝas de la unua lanĉo. Ni faris streĉan provon kaj estis certaj, ke la sistemo ne malrapidiĝis dum korespondado. Sed uzantoj venis kaj la registrado komencis malsukcesi pro tempodaŭro. Al la registri ni uzis / dev / hazarda, kiu rilatas al la entropio de la sistemo. La servilo ne havis tempon por amasigi sufiĉe da entropio kaj kiam nova SecureRandom estis petita, ĝi frostiĝis dum dekoj da sekundoj. Estas multaj manieroj eliri de ĉi tiu situacio, ekzemple: ŝanĝi al la malpli sekura /dev/urandom, instali specialan tabulon kiu generas entropion, generi hazardajn nombrojn anticipe kaj stoki ilin en naĝejo. Ni provizore fermis la problemon kun la naĝejo, sed ekde tiam ni faras apartan teston por registri novajn abonantojn.

Ni uzas kiel ŝarĝo generatoro JMeter. Ĝi ne scias kiel labori kun websocket; ĝi bezonas kromprogramon. La unuaj en serĉrezultoj por la demando "jmeter websocket" estas: artikoloj de BlazeMeter, kiuj rekomendas kromaĵo de Maciej Zaleski.

Tie ni decidis komenci.

Preskaŭ tuj post komenci seriozan testadon, ni malkovris, ke JMeter komencis liki memoron.

La kromaĵo estas aparta granda rakonto; kun 176 steloj, ĝi havas 132 forkojn sur github. La aŭtoro mem ne engaĝiĝis al ĝi ekde 2015 (ni prenis ĝin en 2015, tiam ĝi ne levis suspektojn), plurajn github-problemojn pri memorfuĝoj, 7 nefermitaj tirpetoj.
Se vi decidas fari ŝarĝtestadon per ĉi tiu kromaĵo, bonvolu atenti la jenajn diskutojn:

  1. En multfadena medio, regula LinkedList estis uzata, kaj la rezulto estis NPE en rultempo. Ĉi tio povas esti solvita aŭ per ŝanĝado al ConcurrentLinkedDeque aŭ per sinkronigitaj blokoj. Ni elektis la unuan opcion por ni mem (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Memorfluo; dum malkonekto, konektenformoj ne estas forigitaj (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. En fluanta reĝimo (kiam la retkonekto ne estas fermita ĉe la fino de la specimeno, sed estas uzata poste en la plano), Respondaj ŝablonoj ne funkcias (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Ĉi tiu estas unu el tiuj sur github. Kion ni faris:

  1. Prenis forko Elyran Kogan (@elyrank) - ĝi solvas problemojn 1 kaj 3
  2. Solvita problemo 2
  3. Ĝisdatigita ĝeto de 9.2.14 ĝis 9.3.12
  4. Envolvita SimpleDateFormat en ThreadLocal; SimpleDateFormat ne estas faden-sekura, kio kondukis al NPE ĉe rultempo
  5. Riparis alian memorfukon (la konekto estis malĝuste fermita kiam malkonektita)

Kaj tamen ĝi fluas!

La memoro komencis elĉerpiĝi ne en unu tago, sed en du. Restis absolute neniu tempo, do ni decidis lanĉi malpli da fadenoj, sed sur kvar agentoj. Ĉi tio devus esti sufiĉa por almenaŭ semajno.

Du tagoj pasis...

Nun Hazelcast mankas memoro. La protokoloj montris, ke post kelkaj tagoj da testado, Hazelcast komencis plendi pri manko de memoro, kaj post iom da tempo la areto disfalis, kaj la nodoj daŭre mortis unu post la alia. Ni konektis JVisualVM al hazelcast kaj vidis "leviĝantan segilon" - ĝi regule vokis la GC, sed ne povis forigi la memoron.

Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast

Montriĝis, ke en hazelcast 3.4, kiam vi forigas mapon / multiMap (map.destroy()), memoro ne estas tute liberigita:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

La cimo nun estas riparita en 3.5, sed ĝi estis problemo tiam. Ni kreis novajn multMapojn kun dinamikaj nomoj kaj forigis ilin laŭ nia logiko. La kodo aspektis kiel ĉi tio:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Vivo:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap estis kreita por ĉiu abono kaj forigita kiam ĝi ne estis bezonata. Ni decidis, ke ni komencos Map , la ŝlosilo estos la nomo de la abono, kaj la valoroj estos seancaj identigiloj (de kiuj vi tiam povas akiri uzantajn identigilojn, se necese).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

La furorlisto pliboniĝis.

Kiel kaj kial ni skribis altŝarĝan skaleblan servon por 1C: Entrepreno: Java, PostgreSQL, Hazelcast

Kion alian ni lernis pri ŝarĝtestado?

  1. JSR223 devas esti skribita en groovy kaj inkluzivi kompilkaŝmemoron - ĝi estas multe pli rapida. ligilo.
  2. Jmeter-Plugins-grafikoj estas pli facile kompreneblaj ol normaj. ligilo.

Pri nia sperto kun Hazelcast

Hazelcast estis nova produkto por ni, ni komencis labori kun ĝi de versio 3.4.1, nun nia produktadservilo funkcias version 3.9.2 (je la skribado, la plej nova versio de Hazelcast estas 3.10).

ID-generacio

Ni komencis per entjeraj identigiloj. Ni imagu, ke ni bezonas alian Longon por nova ento. Sekvenco en la datumbazo ne taŭgas, la tabeloj estas implikitaj en sharding - rezultas, ke estas mesaĝo ID=1 en DB1 kaj mesaĝo ID=1 en DB2, vi ne povas meti ĉi tiun ID en Elasticsearch, nek en Hazelcast. , sed la plej malbona afero estas se vi volas kombini la datumojn de du datumbazoj en unu (ekzemple, decidi ke unu datumbazo sufiĉas por ĉi tiuj abonantoj). Vi povas aldoni plurajn AtomicLongs al Hazelcast kaj konservi la nombrilon tie, tiam la agado akiri novan identigilon estas incrementAndGet plus la tempo por peto al Hazelcast. Sed Hazelcast havas ion pli optimuman - FlakeIdGenerator. Kiam ili kontaktas ĉiun klienton, ili ricevas ID-gamon, ekzemple la unua - de 1 ĝis 10, la dua - de 000 ĝis 10, ktp. Nun la kliento povas eldoni novajn identigilojn memstare ĝis la gamo eldonita al ĝi finiĝas. Ĝi funkcias rapide, sed kiam vi rekomencas la aplikaĵon (kaj la Hazelcast-klienton), komenciĝas nova sekvenco - do la saltoj ktp. Krome, programistoj ne vere komprenas kial la identigiloj estas entjeraj, sed estas tiel malkonsekvencaj. Ni pesis ĉion kaj ŝanĝis al UUID-oj.

Cetere, por tiuj, kiuj volas esti kiel Twitter, ekzistas tia Snowcast-biblioteko - ĉi tio estas efektivigo de Snowflake sur Hazelcast. Vi povas spekti ĝin ĉi tie:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Sed ni ne plu atingis ĝin.

TransactionalMap.replace

Alia surprizo: TransactionalMap.replace ne funkcias. Jen testo:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Mi devis skribi mian propran anstataŭigon uzante getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Testu ne nur regulajn datumstrukturojn, sed ankaŭ iliajn transakciajn versiojn. Okazas, ke IMap funkcias, sed TransactionalMap ne plu ekzistas.

Enmetu novan JAR sen malfunkcio

Unue, ni decidis registri objektojn de niaj klasoj en Hazelcast. Ekzemple, ni havas Aplikan klason, ni volas konservi kaj legi ĝin. Savi:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Ni legas:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Ĉio funkcias. Tiam ni decidis konstrui indekson en Hazelcast por serĉi per:

map.addIndex("subscriberId", false);

Kaj kiam ili skribis novan enton, ili komencis ricevi ClassNotFoundException. Hazelcast provis aldoni al la indekso, sed sciis nenion pri nia klaso kaj volis ke oni liveru al ĝi JAR kun ĉi tiu klaso. Ni faris ĝuste tion, ĉio funkciis, sed aperis nova problemo: kiel ĝisdatigi la JAR sen tute haltigi la grapolon? Hazelcast ne prenas la novan JAR dum nodo-post-noda ĝisdatigo. Je ĉi tiu punkto ni decidis, ke ni povus vivi sen indeksa serĉado. Post ĉio, se vi uzas Hazelcast kiel ŝlosilvaloran vendejon, tiam ĉio funkcios? Ne vere. Ĉi tie denove la konduto de IMap kaj TransactionalMap estas malsama. Kie IMap ne zorgas, TransactionalMap ĵetas eraron.

IMap. Ni skribas 5000 objektojn, legas ilin. Ĉio estas atendata.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Sed ĝi ne funkcias en transakcio, ni ricevas ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

En 3.8, la mekanismo de Deplojado de Uzanto Klaso aperis. Vi povas indiki unu majstran nodon kaj ĝisdatigi la JAR-dosieron sur ĝi.

Nun ni tute ŝanĝis nian aliron: ni mem seriigas ĝin en JSON kaj konservas ĝin en Hazelcast. Hazelcast ne bezonas koni la strukturon de niaj klasoj, kaj ni povas ĝisdatigi sen malfunkcio. Versiado de domajnaj objektoj estas kontrolita de la aplikaĵo. Malsamaj versioj de la aplikaĵo povas ruliĝi samtempe, kaj situacio eblas kiam la nova aplikaĵo skribas objektojn kun novaj kampoj, sed la malnova ankoraŭ ne scias pri ĉi tiuj kampoj. Kaj samtempe, la nova aplikaĵo legas objektojn skribitajn de la malnova aplikaĵo, kiuj ne havas novajn kampojn. Ni traktas tiajn situaciojn ene de la aplikaĵo, sed por simpleco ni ne ŝanĝas aŭ forigas kampojn, ni nur vastigas la klasojn aldonante novajn kampojn.

Kiel ni certigas altan rendimenton

Kvar vojaĝoj al Hazelcast - bonaj, du al la datumbazo - malbonaj

Iri al la kaŝmemoro por datumoj ĉiam estas pli bona ol iri al la datumbazo, sed vi ankaŭ ne volas konservi neuzatajn rekordojn. Ni lasas la decidon pri kio kaŝmemori ĝis la lasta etapo de disvolviĝo. Kiam la nova funkcio estas kodita, ni ŝaltas protokolon de ĉiuj demandoj en PostgreSQL (log_min_duration_statement al 0) kaj ruligas ŝarĝtestadon dum 20 minutoj.Uzante la kolektitajn protokolojn, utilecoj kiel pgFouine kaj pgBadger povas konstrui analizajn raportojn. En raportoj ni ĉefe serĉas malrapidajn kaj oftajn demandojn. Por malrapidaj demandoj, ni konstruas ekzekutplanon (KLARIGU) kaj taksas ĉu tia demando povas esti akcelita. Oftaj petoj por la samaj enigdatenoj bone konvenas en la kaŝmemoron. Ni provas konservi demandojn "plataj", unu tabelon per demando.

Ekspluato

SV kiel reta servo estis metita en operacion en la fonto de 2017, kaj kiel aparta produkto, SV estis liberigita en novembro 2017 (tiutempe en beta-versiostatuso).

En pli ol unu jaro da funkciado, ne estis gravaj problemoj en la funkciado de la reta servo CB. Ni kontrolas la interretan servon per Zabbikh, kolekti kaj deploji de Bambuo.

La distribuo de SV-servilo estas liverita en la formo de indiĝenaj pakaĵoj: RPM, DEB, MSI. Krome por Vindozo ni provizas ununuran instalilon en la formo de ununura EXE kiu instalas la servilon, Hazelcast kaj Elasticsearch sur unu maŝino. Ni komence nomis ĉi tiun version de la instalado kiel la "demo" versio, sed nun evidentiĝis, ke ĉi tiu estas la plej populara disfalda opcio.

fonto: www.habr.com

Aldoni komenton