Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast

Šajā rakstā mēs runāsim par to, kā un kāpēc mēs izstrādājām Mijiedarbības sistēma – mehānisms, kas pārsūta informāciju starp klientu lietojumprogrammām un 1C:Enterprise serveriem – no uzdevuma iestatīšanas līdz arhitektūras un ieviešanas detaļu pārdomāšanai.

Mijiedarbības sistēma (turpmāk tekstā — SV) ir izplatīta, pret defektiem izturīga ziņojumapmaiņas sistēma ar garantētu piegādi. SV ir izstrādāts kā augstas slodzes pakalpojums ar augstu mērogojamību, kas pieejams gan kā tiešsaistes pakalpojums (nodrošina 1C), gan kā masveidā ražots produkts, ko var izvietot jūsu servera iekārtās.

SV izmanto izplatīto krātuvi lazdu lējums un meklētājprogrammu Elastikas meklēšana. Mēs arī runāsim par Java un to, kā mēs horizontāli mērogojam PostgreSQL.
Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast

Problēmas paziņojums

Lai būtu skaidrs, kāpēc mēs izveidojām mijiedarbības sistēmu, es jums pastāstīšu nedaudz par to, kā darbojas biznesa lietojumprogrammu izstrāde 1C.

Sākumā nedaudz par mums tiem, kas vēl nezina, ko mēs darām :) Mēs veidojam 1C:Enterprise tehnoloģiju platformu. Platformā ir iekļauts biznesa lietojumprogrammu izstrādes rīks, kā arī izpildlaiks, kas ļauj biznesa lietojumprogrammām darboties starpplatformu vidē.

Klienta-servera attīstības paradigma

Biznesa lietojumprogrammas, kas izveidotas uz 1C: Enterprise, darbojas trīs līmeņos klients-serveris arhitektūra “DBVS – lietojumprogrammu serveris – klients”. Ierakstīts lietojumprogrammas kods iebūvēta 1C valoda, var izpildīt lietojumprogrammu serverī vai klientā. Viss darbs ar lietojumprogrammu objektiem (direktoriji, dokumenti utt.), kā arī datu bāzes lasīšana un rakstīšana tiek veikts tikai serverī. Veidlapu un komandu interfeisa funkcionalitāte ir ieviesta arī serverī. Klients veic veidlapu saņemšanu, atvēršanu un attēlošanu, “komunikāciju” ar lietotāju (brīdinājumi, jautājumi...), nelielus aprēķinus formās, kas prasa ātru atbildi (piemēram, cenu reizinot ar daudzumu), darbu ar lokālajiem failiem, strādājot ar aprīkojumu.

Lietojumprogrammas kodā procedūru un funkciju galvenēs ir skaidri jānorāda, kur kods tiks izpildīts, izmantojot &AtClient / &AtServer direktīvas (&AtClient / &AtServer valodas angļu valodas versijā). 1C izstrādātāji tagad mani izlabos, sakot, ka direktīvas patiesībā ir vairāk nekā, bet mums tas tagad nav svarīgi.

Jūs varat izsaukt servera kodu no klienta koda, bet nevarat izsaukt klienta kodu no servera koda. Šis ir būtisks ierobežojums, ko esam ieviesuši vairāku iemeslu dēļ. Jo īpaši tāpēc, ka servera kods ir jāraksta tā, lai tas tiktu izpildīts vienādi neatkarīgi no tā, kur tas tiek izsaukts - no klienta vai servera. Un gadījumā, ja servera kods tiek izsaukts no cita servera koda, klienta kā tāda nav. Un tāpēc, ka servera koda izpildes laikā klients, kas to izsauca, varēja aizvērt, iziet no lietojumprogrammas, un serverim vairs nebūs nevienam, kam zvanīt.

Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast
Kods, kas apstrādā pogas klikšķi: servera procedūras izsaukšana no klienta darbosies, klienta procedūras izsaukšana no servera nedarbosies.

Tas nozīmē, ka, ja mēs vēlamies nosūtīt kādu ziņojumu no servera uz klienta aplikāciju, piemēram, ka “ilgstoša” atskaites ģenerēšana ir beigusies un atskaiti var apskatīt, mums šādas metodes nav. Jāizmanto triki, piemēram, periodiski jāaptaujā serveris no klienta koda. Bet šī pieeja noslogo sistēmu ar nevajadzīgiem zvaniem un parasti neizskatās īpaši eleganti.

Un ir arī vajadzība, piemēram, kad pienāk telefona zvans SIP- veicot zvanu, paziņot par to klienta lietojumprogrammai, lai tā varētu izmantot zvanītāja numuru, lai to atrastu darījuma partneru datubāzē un parādītu lietotājam informāciju par zvanošo darījumu partneri. Vai, piemēram, kad pasūtījums nonāk noliktavā, paziņojiet par to klienta klienta lietojumprogrammai. Kopumā ir daudz gadījumu, kad šāds mehānisms būtu noderīgs.

Pati ražošana

Izveidojiet ziņojumapmaiņas mehānismu. Ātri, uzticami, ar garantētu piegādi, ar iespēju elastīgi meklēt ziņas. Pamatojoties uz mehānismu, ieviesiet kurjeru (ziņojumus, videozvanus), kas darbojas 1C lietojumprogrammās.

Izstrādājiet sistēmu tā, lai tā būtu horizontāli mērogojama. Pieaugošā slodze jāsedz, palielinot mezglu skaitu.

Ieviešana

Nolēmām SV servera daļu neintegrēt tieši platformā 1C:Enterprise, bet ieviest to kā atsevišķu produktu, kura API var izsaukt no 1C aplikāciju risinājumu koda. Tas tika darīts vairāku iemeslu dēļ, no kuriem galvenais bija tas, ka es vēlējos nodrošināt iespēju apmainīties ar ziņojumiem starp dažādām 1C lietojumprogrammām (piemēram, starp tirdzniecības pārvaldību un grāmatvedību). Dažādas 1C lietojumprogrammas var darboties dažādās platformas 1C:Enterprise versijās, atrasties dažādos serveros utt. Šādos apstākļos SV kā atsevišķa produkta, kas atrodas 1C instalāciju “malā”, ieviešana ir optimāls risinājums.

Tāpēc mēs nolēmām izgatavot SV kā atsevišķu produktu. Mēs iesakām maziem uzņēmumiem izmantot CB serveri, ko instalējām mūsu mākonī (wss://1cdialog.com), lai izvairītos no pieskaitāmām izmaksām, kas saistītas ar servera lokālo instalēšanu un konfigurēšanu. Lieliem klientiem var būt ieteicams savās telpās instalēt savu CB serveri. Mēs izmantojām līdzīgu pieeju mūsu mākoņa SaaS produktā 1cFresh - tas tiek ražots kā masveida produkts uzstādīšanai klientu vietnēs, kā arī tiek izvietots mūsu mākonī https://1cfresh.com/.

Pieteikums

Lai sadalītu slodzi un kļūdu toleranci, mēs izvietosim nevis vienu Java lietojumprogrammu, bet vairākas ar slodzes balansētāju priekšā. Ja jums ir jāpārsūta ziņojums no mezgla uz mezglu, izmantojiet publicēšanu/abonēšanu pakalpojumā Hazelcast.

Saziņa starp klientu un serveri notiek, izmantojot tīmekļa ligzdu. Tas ir labi piemērots reāllaika sistēmām.

Izplatīta kešatmiņa

Mēs izvēlējāmies starp Redis, Hazelcast un Ehcache. Ir 2015. gads. Redis tikko izlaida jaunu klasteru (pārāk jauns, biedējošs), ir Sentinel ar daudziem ierobežojumiem. Ehcache nezina, kā apvienoties klasterī (šī funkcionalitāte parādījās vēlāk). Mēs nolēmām to izmēģināt ar Hazelcast 3.4.
Hazelcast ir salikts klasterī no kastes. Viena mezgla režīmā tas nav īpaši noderīgs un to var izmantot tikai kā kešatmiņu - tas nezina, kā izmest datus diskā, ja pazaudējat vienīgo mezglu, jūs zaudējat datus. Mēs izvietojam vairākas Hazelcasts, starp kurām mēs dublējam kritiskos datus. Mēs neveidojam kešatmiņas dublējumu — mums tas nav iebildumi.

Mums Hazelcast ir:

  • Lietotāju sesiju glabāšana. Katru reizi, lai dotos uz datubāzi sesijai, ir nepieciešams ilgs laiks, tāpēc visas sesijas ievietojam Hazelcast.
  • Kešatmiņa. Ja meklējat lietotāja profilu, pārbaudiet kešatmiņu. Uzrakstīja jaunu ziņojumu - ievietojiet to kešatmiņā.
  • Tēmas saziņai starp lietojumprogrammu gadījumiem. Mezgls ģenerē notikumu un ievieto to tēmā Hazelcast. Citi šai tēmai abonētie lietojumprogrammu mezgli saņem un apstrādā notikumu.
  • Klasteru slēdzenes. Piemēram, mēs izveidojam diskusiju, izmantojot unikālu atslēgu (viena diskusija 1C datubāzē):

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

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

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

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

Mēs pārbaudījām, vai nav kanāla. Mēs paņēmām slēdzeni, pārbaudījām to vēlreiz un izveidojām. Ja nepārbaudīsiet slēdzeni pēc bloķēšanas, tad pastāv iespēja, ka tajā brīdī ir pārbaudījis arī cits pavediens un tagad mēģinās izveidot to pašu diskusiju, taču tā jau pastāv. Jūs nevarat bloķēt, izmantojot sinhronizēto vai parasto java bloķēšanu. Izmantojot datu bāzi - tas ir lēns, un ir žēl par datubāzi; izmantojot Hazelcast - tas ir tas, kas jums nepieciešams.

DBVS izvēle

Mums ir plaša un veiksmīga pieredze darbā ar PostgreSQL un sadarbojoties ar šīs DBVS izstrādātājiem.

Ar PostgreSQL klasteru tas nav viegli — tas ir XL, XC, Citus, bet kopumā tie nav NoSQL, kas tiek mērogoti no kastes. Mēs neuzskatījām NoSQL par galveno krātuvi; pietika ar to, ka paņēmām Hazelcast, ar kuru iepriekš nebijām strādājuši.

Ja jums ir nepieciešams mērogot relāciju datu bāzi, tas nozīmē sadalīšana. Kā zināms, ar sharding mēs sadalām datu bāzi atsevišķās daļās, lai katru no tām varētu ievietot atsevišķā serverī.

Pirmajā mūsu sadalīšanas versijā tika pieņemta iespēja sadalīt katru mūsu lietojumprogrammas tabulu dažādos serveros dažādās proporcijās. Serverī A ir daudz ziņojumu — lūdzu, pārvietosim daļu no šīs tabulas uz serveri B. Šis lēmums vienkārši kliedza par priekšlaicīgu optimizāciju, tāpēc mēs nolēmām aprobežoties ar vairāku nomnieku pieeju.

Par vairāku nomnieku var lasīt, piemēram, vietnē Citus dati.

SV ir lietojumprogrammas un abonenta jēdzieni. Lietojumprogramma ir noteikta biznesa lietojumprogrammas, piemēram, ERP vai grāmatvedības, instalācija ar tās lietotājiem un biznesa datiem. Abonents ir organizācija vai fiziska persona, kuras vārdā pieteikums ir reģistrēts SV serverī. Abonentam var būt reģistrētas vairākas lietojumprogrammas, un šīs programmas var apmainīties ar ziņojumiem savā starpā. Abonents kļuva par nomnieku mūsu sistēmā. Vairāku abonentu ziņas var atrasties vienā fiziskā datu bāzē; ja mēs redzam, ka abonents ir sācis ģenerēt lielu trafiku, mēs to pārvietojam uz atsevišķu fizisku datu bāzi (vai pat atsevišķu datu bāzes serveri).

Mums ir galvenā datu bāze, kurā tiek glabāta maršrutēšanas tabula ar informāciju par visu abonentu datu bāzu atrašanās vietu.

Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast

Lai galvenā datu bāze nebūtu šķērslis, maršrutēšanas tabulu (un citus bieži nepieciešamos datus) glabājam kešatmiņā.

Ja abonenta datu bāze sāks palēnināties, mēs to iekšā sadalīsim nodalījumos. Citos projektos mēs izmantojam pg_pathman.

Tā kā lietotāju ziņojumu pazaudēšana ir slikta, mēs uzturam savas datu bāzes ar replikām. Sinhrono un asinhrono kopiju kombinācija ļauj apdrošināt sevi galvenās datu bāzes nozaudēšanas gadījumā. Ziņojuma zudums notiks tikai tad, ja primārā datu bāze un tās sinhronā kopija vienlaikus neizdosies.

Ja tiek zaudēta sinhronā kopija, asinhronā kopija kļūst sinhrona.
Ja galvenā datu bāze tiek pazaudēta, sinhronā kopija kļūst par galveno datu bāzi, bet asinhronā kopija kļūst par sinhrono kopiju.

Elasticsearch meklēšanai

Tā kā cita starpā SV ir arī ziņnesis, tai nepieciešama ātra, ērta un elastīga meklēšana, ņemot vērā morfoloģiju, izmantojot neprecīzus sakritības. Mēs nolēmām neizgudrot riteni no jauna un izmantot bezmaksas meklētājprogrammu Elasticsearch, kas izveidota, pamatojoties uz bibliotēku Lucene. Mēs arī izvietojam Elasticsearch klasterī (galvenais — dati — dati), lai novērstu problēmas lietojumprogrammu mezglu kļūmes gadījumā.

Vietnē Github mēs atradām Krievu morfoloģijas spraudnis par Elasticsearch un izmantojiet to. Elasticsearch rādītājā mēs saglabājam vārdu saknes (ko spraudnis nosaka) un N-gramus. Kad lietotājs ievada tekstu, lai meklētu, mēs meklējam drukāto tekstu starp N-gramiem. Saglabājot indeksā, vārds “teksti” tiks sadalīts šādos N gramos:

[tie, tek, tex, teksts, teksti, ek, ex, ext, teksti, ks, kst, ksty, st, sty, jūs],

Un tiks saglabāta arī vārda “teksts” sakne. Šī pieeja ļauj meklēt vārda sākumā, vidū un beigās.

Kopējā aina

Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast
Atkārtojiet attēlu no raksta sākuma, bet ar paskaidrojumiem:

  • Līdzsvarotājs, kas ir pieejams internetā; mums ir nginx, tas var būt jebkurš.
  • Java lietojumprogrammu gadījumi savā starpā sazinās, izmantojot Hazelcast.
  • Lai strādātu ar tīmekļa ligzdu, ko mēs izmantojam Netty.
  • Java lietojumprogramma ir rakstīta Java 8 un sastāv no komplektiem OSGi. Plānos ietilpst migrācija uz Java 10 un pāreja uz moduļiem.

Izstrāde un testēšana

SV izstrādes un testēšanas procesā mēs saskārāmies ar vairākām interesantām mūsu izmantoto produktu funkcijām.

Slodzes pārbaude un atmiņas noplūdes

Katra SV laidiena izlaišana ietver slodzes pārbaudi. Tas ir veiksmīgs, ja:

  • Pārbaude darbojās vairākas dienas, un servisa kļūmes nebija
  • Reakcijas laiks galvenajām darbībām nepārsniedza ērtu slieksni
  • Veiktspējas pasliktināšanās salīdzinājumā ar iepriekšējo versiju ir ne vairāk kā 10%

Mēs aizpildām testa datubāzi ar datiem - lai to izdarītu, mēs saņemam informāciju par aktīvāko abonentu no ražošanas servera, reizinām tā skaitļus ar 5 (ziņu, diskusiju, lietotāju skaits) un pārbaudām tādā veidā.

Mēs veicam mijiedarbības sistēmas slodzes testēšanu trīs konfigurācijās:

  1. stresa tests
  2. Tikai savienojumi
  3. Abonentu reģistrācija

Stresa testa laikā mēs palaižam vairākus simtus pavedienu, un tie bez apstājas ielādē sistēmu: rakstot ziņojumus, veidojot diskusijas, saņemot ziņojumu sarakstu. Mēs simulējam parasto lietotāju darbības (saņemt manu nelasīto ziņojumu sarakstu, rakstīt kādam) un programmatūras risinājumus (pārsūtīt citas konfigurācijas pakotni, apstrādāt brīdinājumu).

Piemēram, šādi izskatās stresa testa daļa:

  • Lietotājs piesakās
    • Pieprasa jūsu nelasītās diskusijas
    • 50% iespēja lasīt ziņojumus
    • 50% iespēja sūtīt īsziņu
    • Nākamais lietotājs:
      • Ir 20% iespēja izveidot jaunu diskusiju
      • Nejauši atlasa kādu no savām diskusijām
      • Iet iekšā
      • Pieprasa ziņas, lietotāju profilus
      • Izveido piecus ziņojumus, kas adresēti nejaušiem lietotājiem no šīs diskusijas
      • Atstāj diskusiju
      • Atkārtojas 20 reizes
      • Iziet, atgriežas skripta sākumā

    • Sistēmā ienāk tērzēšanas robots (emulē ziņojumapmaiņu no lietojumprogrammas koda)
      • Ir 50% iespēja izveidot jaunu datu apmaiņas kanālu (īpaša diskusija)
      • 50% iespēja rakstīt ziņojumu kādam no esošajiem kanāliem

Scenārijs “Tikai savienojumi” parādījās kāda iemesla dēļ. Ir situācija: lietotāji ir pievienojuši sistēmu, bet vēl nav iesaistījušies. Katrs lietotājs 09:00 no rīta ieslēdz datoru, izveido savienojumu ar serveri un klusē. Šie puiši ir bīstami, viņu ir daudz — vienīgās pakotnes, kas viņiem ir, ir PING/PONG, taču viņi saglabā savienojumu ar serveri (viņi to nevar uzturēt — ja nu ir jauns ziņojums). Tests atveido situāciju, kad liels skaits šādu lietotāju mēģina pieteikties sistēmā pusstundas laikā. Tas ir līdzīgs stresa testam, bet tā fokuss ir tieši uz šo pirmo ievadi - lai nebūtu kļūmju (cilvēks neizmanto sistēmu, un tā jau nokrīt - grūti izdomāt ko sliktāku).

Abonenta reģistrācijas skripts sākas no pirmās palaišanas. Mēs veicām stresa testu un bijām pārliecināti, ka sarakstes laikā sistēma nepalēninās. Bet lietotāji ieradās, un reģistrācija sāka neizdoties noildzes dēļ. Reģistrējoties izmantojām / dev / izlases, kas ir saistīts ar sistēmas entropiju. Serverim nebija laika uzkrāt pietiekami daudz entropijas un, kad tika pieprasīts jauns SecureRandom, tas sastinga uz desmitiem sekunžu. No šīs situācijas ir daudz izeju, piemēram: pārslēdzieties uz mazāk drošu /dev/urandom, instalējiet īpašu plati, kas ģenerē entropiju, iepriekš ģenerējiet nejaušus skaitļus un saglabājiet tos baseinā. Mēs uz laiku novērsām problēmu ar pūlu, taču kopš tā laika esam veikuši atsevišķu testu jaunu abonentu reģistrēšanai.

Mēs izmantojam kā slodzes ģeneratoru Jmeter. Tas nezina, kā strādāt ar tīmekļa ligzdu; tam ir nepieciešams spraudnis. Pirmie meklēšanas rezultātos vaicājumam “jmeter websocket” ir: raksti no BlazeMeter, kas iesaka spraudnis Maciej Zaleski.

Tur mēs nolēmām sākt.

Gandrīz uzreiz pēc nopietnas pārbaudes sākšanas mēs atklājām, ka JMeter sāka noplūst atmiņa.

Spraudnis ir atsevišķs liels stāsts; ar 176 zvaigznēm tam ir 132 dakšas github. Pats autors to nav apņēmies kopš 2015. gada (uzņēmām 2015. gadā, tad tas neradīja aizdomas), vairāki github jautājumi saistībā ar atmiņas noplūdēm, 7 neaizvērti pull pieprasījumi.
Ja nolemjat veikt slodzes pārbaudi, izmantojot šo spraudni, lūdzu, pievērsiet uzmanību šādām diskusijām:

  1. Daudzpavedienu vidē tika izmantots parasts LinkedList, un rezultāts bija NPE izpildlaikā. To var atrisināt, pārslēdzoties uz ConcurrentLinkedDeque, vai ar sinhronizētiem blokiem. Mēs sev izvēlējāmies pirmo variantu (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Atmiņas noplūde; atvienojot, savienojuma informācija netiek dzēsta (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Straumēšanas režīmā (kad tīmekļa ligzda netiek aizvērta parauga beigās, bet tiek izmantota vēlāk plānā), atbildes modeļi nedarbojas (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Šis ir viens no tiem github. Ko mēs darījām:

  1. Ir paņemts dakša Elirans Kogans (@elyrank) – tas novērš 1. un 3. problēmu
  2. Atrisināta problēma 2
  3. Atjaunots mols no 9.2.14 uz 9.3.12
  4. Aplauzts SimpleDateFormat programmā ThreadLocal; SimpleDateFormat nav pavedienu drošs, tāpēc izpildlaikā tika izveidots NPE
  5. Novērsta cita atmiņas noplūde (atvienojot savienojumu tika nepareizi aizvērts)

Un tomēr tas plūst!

Atmiņa sāka izsīkt nevis dienā, bet divās. Laika vairs nebija, tāpēc nolēmām palaist mazāk pavedienu, bet uz četriem aģentiem. Ar to vajadzēja pietikt vismaz nedēļai.

Ir pagājušas divas dienas...

Tagad Hazelcast atmiņa beidzas. Žurnāli liecināja, ka pēc pāris dienām ilgas pārbaudes Hazelcast sāka sūdzēties par atmiņas trūkumu, un pēc kāda laika klasteris izjuka, un mezgli turpināja nomirt pa vienam. Mēs savienojām JVisualVM ar hazelcast un redzējām “augošu zāģi” — tas regulāri sauca GC, taču nevarēja notīrīt atmiņu.

Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast

Izrādījās, ka hazelcast 3.4, dzēšot karti / multiMap (map.destroy()), atmiņa netiek pilnībā atbrīvota:

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

Kļūda tagad ir novērsta versijā 3.5, taču toreiz tā bija problēma. Mēs izveidojām jaunas multikartes ar dinamiskiem nosaukumiem un izdzēsām tās saskaņā ar mūsu loģiku. Kods izskatījās apmēram šādi:

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();
    }
}

Zvanīt:

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

MultiMap tika izveidots katram abonementam un dzēsts, kad tas nebija nepieciešams. Mēs nolēmām, ka sāksim Map , atslēga būs abonementa nosaukums, un vērtības būs sesijas identifikatori (no kuriem pēc tam varat iegūt lietotāja identifikatorus, ja nepieciešams).

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

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

Diagrammas ir uzlabojušās.

Kā un kāpēc mēs rakstījām augstas slodzes mērogojamu pakalpojumu priekš 1C: Enterprise: Java, PostgreSQL, Hazelcast

Ko vēl esam iemācījušies par slodzes testēšanu?

  1. JSR223 ir jāraksta groovy un jāiekļauj kompilācijas kešatmiņa — tas ir daudz ātrāk. Saite.
  2. Jmeter-Plugins diagrammas ir vieglāk saprotamas nekā standarta. Saite.

Par mūsu pieredzi ar Hazelcast

Hazelcast mums bija jauns produkts, mēs sākām ar to strādāt no versijas 3.4.1, tagad mūsu produkcijas serverī darbojas versija 3.9.2 (raksta rakstīšanas laikā jaunākā Hazelcast versija ir 3.10).

ID ģenerēšana

Mēs sākām ar veselu skaitļu identifikatoriem. Iedomāsimies, ka mums vajag vēl vienu Garu jaunai būtnei. Secība datu bāzē nav piemērota, tabulas ir iesaistītas shardingā - izrādās, ka DB1 ir ziņojums ID=1 un DB1 ziņojums ID=2, jūs nevarat ievietot šo ID ne Elasticsearch, ne Hazelcast , bet sliktākais ir tad, ja vēlaties apvienot datus no divām datu bāzēm vienā (piemēram, izlemjot, ka šiem abonentiem pietiek ar vienu datu bāzi). Varat pievienot vairākus AtomicLongs Hazelcast un saglabāt tur skaitītāju, tad jauna ID iegūšanas veiktspēja ir incrementAndGet, kā arī laiks pieprasījumam Hazelcast. Bet Hazelcast ir kaut kas optimālāks - FlakeIdGenerator. Sazinoties ar katru klientu, tiek dots ID diapazons, piemēram, pirmais – no 1 līdz 10 000, otrais – no 10 001 līdz 20 000 utt. Tagad klients var pats izdot jaunus identifikatorus, līdz beidzas viņam izsniegtais diapazons. Tas darbojas ātri, bet, restartējot lietojumprogrammu (un Hazelcast klientu), sākas jauna secība - līdz ar to izlaidumi utt. Turklāt izstrādātāji īsti nesaprot, kāpēc ID ir veseli skaitļi, taču tie ir tik nekonsekventi. Mēs visu nosvērām un pārgājām uz UUID.

Starp citu, tiem, kas vēlas līdzināties Twitter, ir tāda Snowcast bibliotēka - tas ir Snowflake izpildījums Hazelcast virsū. To var apskatīt šeit:

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

Bet mēs vairs neesam tikuši pie tā.

TransactionMap.replace

Vēl viens pārsteigums: TransactionMap.replace nedarbojas. Lūk, tests:

@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

Man bija jāraksta savs aizstājējs, izmantojot 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);
}

Pārbaudi ne tikai parastās datu struktūras, bet arī to transakciju versijas. Gadās, ka IMap darbojas, bet TransactionalMap vairs nepastāv.

Ievietojiet jaunu JAR bez dīkstāves

Pirmkārt, mēs nolēmām ierakstīt mūsu klašu objektus Hazelcast. Piemēram, mums ir Application klase, mēs vēlamies to saglabāt un izlasīt. Saglabāt:

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

Mēs lasām:

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

Viss darbojas. Pēc tam mēs nolēmām izveidot indeksu Hazelcast, lai meklētu pēc:

map.addIndex("subscriberId", false);

Un, rakstot jaunu entītiju, viņi sāka saņemt ClassNotFoundException. Hazelcast mēģināja papildināt indeksu, bet neko nezināja par mūsu klasi un gribēja, lai tai tiktu piegādāts JAR ar šo klasi. Mēs to darījām, viss strādāja, taču parādījās jauna problēma: kā atjaunināt JAR, pilnībā neapturot klasteru? Hazelcast neuzņem jauno JAR, veicot atjaunināšanu mezglā pēc mezgla. Šajā brīdī mēs nolēmām, ka varam dzīvot bez indeksa meklēšanas. Galu galā, ja izmantojat Hazelcast kā atslēgas vērtību veikalu, tad viss darbosies? Ne īsti. Arī šeit IMap un TransactionalMap darbība atšķiras. Ja IMap nav vienalga, TransactionalMap rada kļūdu.

IMap. Mēs uzrakstām 5000 objektus, lasām tos. Viss ir sagaidāms.

@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());
    }
}

Bet darījumā tas nedarbojas, mēs iegūstam 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();
        }
    });
}

Programmā 3.8 parādījās User Class Deployment mehānisms. Varat norādīt vienu galveno mezglu un atjaunināt tajā esošo JAR failu.

Tagad mēs esam pilnībā mainījuši savu pieeju: paši to serializējam JSON formātā un saglabājam Hazelcast. Hazelcast nav jāzina mūsu nodarbību struktūra, un mēs varam atjaunināt bez dīkstāves. Domēna objektu versijas kontrolē lietojumprogramma. Vienlaicīgi var darboties dažādas aplikācijas versijas, un iespējama situācija, kad jaunā aplikācija ieraksta objektus ar jauniem laukiem, bet vecā par šiem laukiem vēl nezina. Un tajā pašā laikā jaunā lietojumprogramma nolasa vecās lietojumprogrammas rakstītos objektus, kuriem nav jaunu lauku. Šādas situācijas risinām aplikācijas ietvaros, taču vienkāršības labad laukus nemainām un nedzēšam, tikai paplašinām klases, pievienojot jaunus laukus.

Kā mēs nodrošinām augstu veiktspēju

Četri braucieni uz Hazelcast - labi, divi uz datubāzi - slikti

Pāriet uz kešatmiņu, lai meklētu datus, vienmēr ir labāk nekā doties uz datu bāzi, taču jūs arī nevēlaties saglabāt neizmantotos ierakstus. Mēs atstājam lēmumu par to, ko saglabāt kešatmiņā, līdz pēdējam izstrādes posmam. Kad jaunā funkcionalitāte ir kodēta, mēs ieslēdzam visu vaicājumu reģistrēšanu programmā PostgreSQL (log_min_duration_statement līdz 0) un 20 minūtes veicam slodzes testēšanu. Izmantojot apkopotos žurnālus, utilītas, piemēram, pgFouine un pgBadger, var izveidot analītiskos pārskatus. Pārskatos mēs galvenokārt meklējam lēnus un biežus vaicājumus. Lēniem vaicājumiem mēs veidojam izpildes plānu (EXPLAIN) un izvērtējam, vai šādu vaicājumu var paātrināt. Bieži vien un to pašu ievades datu pieprasījumi labi iekļaujas kešatmiņā. Mēs cenšamies saglabāt vaicājumus “plakanus”, viena tabula katram vaicājumam.

Ekspluatācija

SV kā tiešsaistes pakalpojums tika nodots ekspluatācijā 2017. gada pavasarī, un kā atsevišķs produkts SV tika izlaists 2017. gada novembrī (tolaik tas bija beta versijas statusā).

Vairāk nekā gada darbības laikā CB tiešsaistes servisa darbībā nopietnas problēmas nav bijušas. Mēs uzraugām tiešsaistes pakalpojumu, izmantojot Zabbix, savāc un izvieto no Bambuss.

SV servera izplatīšana tiek piegādāta vietējo pakotņu veidā: RPM, DEB, MSI. Turklāt sistēmai Windows mēs piedāvājam vienu instalēšanas programmu viena EXE formātā, kas vienā datorā instalē serveri, Hazelcast un Elasticsearch. Sākotnēji mēs šo instalācijas versiju saucām par “demonstrācijas versiju”, taču tagad ir kļuvis skaidrs, ka šī ir vispopulārākā izvietošanas iespēja.

Avots: www.habr.com

Pievieno komentāru