Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast

Sa artikulong ito ay pag-uusapan natin kung paano at bakit tayo nabuo Sistema ng Pakikipag-ugnayan – isang mekanismo na naglilipat ng impormasyon sa pagitan ng mga application ng kliyente at 1C:Enterprise server - mula sa pagtatakda ng gawain hanggang sa pag-iisip sa mga detalye ng arkitektura at pagpapatupad.

Ang Sistema ng Pakikipag-ugnayan (mula rito ay tinutukoy bilang SV) ay isang distributed, fault-tolerant messaging system na may garantisadong paghahatid. Ang SV ay idinisenyo bilang isang serbisyong may mataas na load na may mataas na scalability, available bilang isang online na serbisyo (ibinigay ng 1C) at bilang isang mass-produce na produkto na maaaring i-deploy sa sarili mong mga pasilidad ng server.

Gumagamit ang SV ng distributed storage Hazelcast at search engine Elasticsearch. Tatalakayin din namin ang tungkol sa Java at kung paano namin pahalang na sinusukat ang PostgreSQL.
Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast

Pahayag ng problema

Upang gawing malinaw kung bakit namin ginawa ang Sistema ng Pakikipag-ugnayan, sasabihin ko sa iyo ng kaunti tungkol sa kung paano gumagana ang pagbuo ng mga application ng negosyo sa 1C.

Upang magsimula, kaunti tungkol sa amin para sa mga hindi pa alam kung ano ang ginagawa namin :) Ginagawa namin ang platform ng teknolohiyang 1C:Enterprise. Kasama sa platform ang isang tool sa pagbuo ng application ng negosyo, pati na rin ang isang runtime na nagbibigay-daan sa mga application ng negosyo na tumakbo sa isang cross-platform na kapaligiran.

Paradigm sa pagbuo ng Client-server

Ang mga application ng negosyo na ginawa sa 1C:Enterprise ay gumagana sa tatlong antas client-server arkitektura "DBMS - server ng application - kliyente". Nakasulat ang code ng aplikasyon built-in na 1C na wika, ay maaaring isagawa sa server ng aplikasyon o sa kliyente. Ang lahat ng trabaho sa mga bagay ng application (mga direktoryo, dokumento, atbp.), Pati na rin ang pagbabasa at pagsulat ng database, ay ginagawa lamang sa server. Ang functionality ng mga form at command interface ay ipinapatupad din sa server. Nagsasagawa ang kliyente ng pagtanggap, pagbubukas at pagpapakita ng mga form, "pakikipag-usap" sa gumagamit (mga babala, mga tanong...), maliliit na kalkulasyon sa mga form na nangangailangan ng mabilis na tugon (halimbawa, pagpaparami ng presyo sa dami), pagtatrabaho sa mga lokal na file, nagtatrabaho sa kagamitan.

Sa application code, dapat na tahasang ipahiwatig ng mga header ng mga pamamaraan at function kung saan isasagawa ang code - gamit ang mga direktiba ng &AtClient / &AtServer (&AtClient / &AtServer sa English na bersyon ng wika). Itatama na ako ngayon ng mga developer ng 1C sa pagsasabi na ang mga direktiba talaga higit sa, ngunit para sa amin hindi ito mahalaga ngayon.

Maaari kang tumawag sa server code mula sa client code, ngunit hindi ka makakatawag sa client code mula sa server code. Isa itong pangunahing limitasyon na ginawa namin para sa ilang kadahilanan. Sa partikular, dahil ang server code ay dapat na nakasulat sa paraang ito ay nagsasagawa ng parehong paraan kahit saan man ito tinawag - mula sa kliyente o mula sa server. At sa kaso ng pagtawag sa server code mula sa isa pang server code, walang ganoong kliyente. At dahil sa panahon ng pagpapatupad ng server code, ang kliyente na tumawag dito ay maaaring magsara, lumabas sa application, at ang server ay wala nang matatawagan.

Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast
Code na humahawak sa isang pag-click sa pindutan: ang pagtawag sa isang pamamaraan ng server mula sa kliyente ay gagana, ang pagtawag sa isang pamamaraan ng kliyente mula sa server ay hindi

Nangangahulugan ito na kung gusto naming magpadala ng ilang mensahe mula sa server patungo sa application ng kliyente, halimbawa, na natapos na ang pagbuo ng isang "matagalang" na ulat at maaaring matingnan ang ulat, wala kaming ganoong paraan. Kailangan mong gumamit ng mga trick, halimbawa, pana-panahong poll ang server mula sa client code. Ngunit ang diskarteng ito ay naglo-load sa system ng mga hindi kinakailangang tawag, at sa pangkalahatan ay hindi mukhang napaka-eleganteng.

At mayroon ding pangangailangan, halimbawa, kapag may dumating na tawag sa telepono SIP- kapag tumatawag, abisuhan ang application ng kliyente tungkol dito upang magamit nito ang numero ng tumatawag upang mahanap ito sa database ng counterparty at ipakita ang impormasyon ng user tungkol sa counterparty na tumatawag. O, halimbawa, kapag dumating ang isang order sa bodega, abisuhan ang application ng kliyente ng customer tungkol dito. Sa pangkalahatan, maraming mga kaso kung saan ang gayong mekanismo ay magiging kapaki-pakinabang.

Ang produksyon mismo

Lumikha ng mekanismo ng pagmemensahe. Mabilis, maaasahan, may garantisadong paghahatid, na may kakayahang flexible na maghanap ng mga mensahe. Batay sa mekanismo, magpatupad ng messenger (mga mensahe, video call) na tumatakbo sa loob ng mga 1C application.

Idisenyo ang system upang maging pahalang na nasusukat. Ang pagtaas ng load ay dapat na sakop sa pamamagitan ng pagtaas ng bilang ng mga node.

Pagpapatupad

Napagpasyahan naming huwag isama ang bahagi ng server ng SV nang direkta sa 1C:Enterprise platform, ngunit upang ipatupad ito bilang isang hiwalay na produkto, ang API na maaaring tawagan mula sa code ng mga solusyon sa aplikasyon ng 1C. Ginawa ito para sa maraming dahilan, ang pangunahing isa ay ang nais kong gawing posible ang pagpapalitan ng mga mensahe sa pagitan ng iba't ibang 1C application (halimbawa, sa pagitan ng Trade Management at Accounting). Maaaring tumakbo ang iba't ibang 1C application sa iba't ibang bersyon ng 1C:Enterprise platform, na matatagpuan sa iba't ibang server, atbp. Sa ganitong mga kondisyon, ang pagpapatupad ng SV bilang isang hiwalay na produkto na matatagpuan "sa gilid" ng mga pag-install ng 1C ay ang pinakamainam na solusyon.

Kaya, nagpasya kaming gawin ang SV bilang isang hiwalay na produkto. Inirerekomenda namin na gamitin ng maliliit na kumpanya ang CB server na na-install namin sa aming cloud (wss://1cdialog.com) upang maiwasan ang mga gastos sa overhead na nauugnay sa lokal na pag-install at configuration ng server. Maaaring makita ng malalaking kliyente na maipapayo na mag-install ng sarili nilang CB server sa kanilang mga pasilidad. Gumamit kami ng katulad na diskarte sa aming produkto ng cloud SaaS 1cSariwa – ito ay ginawa bilang isang mass-produce na produkto para sa pag-install sa mga site ng mga kliyente, at naka-deploy din sa aming cloud https://1cfresh.com/.

App

Upang maipamahagi ang pag-load at fault tolerance, hindi isang Java application ang ilalagay namin, ngunit marami, na may load balancer sa harap nila. Kung kailangan mong maglipat ng mensahe mula sa node patungo sa node, gamitin ang publish/subscribe sa Hazelcast.

Ang komunikasyon sa pagitan ng kliyente at ng server ay sa pamamagitan ng websocket. Ito ay angkop para sa mga real-time na system.

Ibinahagi ang cache

Pumili kami sa pagitan ng Redis, Hazelcast at Ehcache. 2015 na. Kakalabas lang ni Redis ng bagong cluster (napakabago, nakakatakot), may Sentinel na maraming restrictions. Hindi alam ng Ehcache kung paano mag-assemble sa isang cluster (lumabas ang functionality na ito). Nagpasya kaming subukan ito sa Hazelcast 3.4.
Ang Hazelcast ay binuo sa isang kumpol sa labas ng kahon. Sa single node mode, hindi ito masyadong kapaki-pakinabang at maaari lamang gamitin bilang cache - hindi nito alam kung paano i-dump ang data sa disk, kung mawala mo ang nag-iisang node, mawawala ang data. Nag-deploy kami ng ilang Hazelcast, kung saan nagba-backup kami ng kritikal na data. Hindi namin bina-back up ang cache - hindi namin ito iniisip.

Para sa amin, ang Hazelcast ay:

  • Imbakan ng mga session ng user. Ito ay tumatagal ng mahabang oras upang pumunta sa database para sa isang session sa bawat oras, kaya inilalagay namin ang lahat ng mga session sa Hazelcast.
  • Cache. Kung naghahanap ka ng profile ng user, tingnan ang cache. Nagsulat ng bagong mensahe - ilagay ito sa cache.
  • Mga paksa para sa komunikasyon sa pagitan ng mga pagkakataon ng aplikasyon. Ang node ay bumubuo ng isang kaganapan at inilalagay ito sa paksa ng Hazelcast. Ang ibang mga node ng application na naka-subscribe sa paksang ito ay tumatanggap at nagpoproseso ng kaganapan.
  • Cluster lock. Halimbawa, lumikha kami ng isang talakayan gamit ang isang natatanging susi (isang talakayan sa loob ng 1C database):

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

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

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

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

Sinuri namin na walang channel. Kinuha namin ang lock, sinuri ito muli, at ginawa ito. Kung hindi mo susuriin ang lock pagkatapos kunin ang lock, may pagkakataon na may isa pang thread na nasuri sa sandaling iyon at susubukan na ngayong gumawa ng parehong talakayan - ngunit mayroon na ito. Hindi ka makakanda-lock gamit ang naka-synchronize o regular na java Lock. Sa pamamagitan ng database - ito ay mabagal, at ito ay isang awa para sa database; sa pamamagitan ng Hazelcast - iyon ang kailangan mo.

Pagpili ng isang DBMS

Mayroon kaming malawak at matagumpay na karanasan sa pagtatrabaho sa PostgreSQL at pakikipagtulungan sa mga developer ng DBMS na ito.

Hindi madali sa isang PostgreSQL cluster - mayroon XL, XC, Citus, ngunit sa pangkalahatan ay hindi ito mga NoSQL na lumalabas sa kahon. Hindi namin itinuring ang NoSQL bilang pangunahing imbakan; sapat na na kinuha namin ang Hazelcast, na hindi namin nakatrabaho noon.

Kung kailangan mong sukatin ang isang relational database, ibig sabihin sharding. Tulad ng alam mo, sa sharding, hinahati namin ang database sa magkakahiwalay na bahagi upang ang bawat isa sa kanila ay mailagay sa isang hiwalay na server.

Ipinagpalagay ng unang bersyon ng aming sharding ang kakayahang ipamahagi ang bawat isa sa mga talahanayan ng aming aplikasyon sa iba't ibang mga server sa iba't ibang sukat. Mayroong maraming mga mensahe sa server A - mangyaring, ilipat natin ang bahagi ng talahanayang ito sa server B. Ang desisyong ito ay sumisigaw tungkol sa napaaga na pag-optimize, kaya nagpasya kaming limitahan ang aming sarili sa isang multi-tenant na diskarte.

Maaari mong basahin ang tungkol sa multi-tenant, halimbawa, sa website Data ng Citus.

Ang SV ay may mga konsepto ng aplikasyon at subscriber. Ang isang application ay isang partikular na pag-install ng isang application ng negosyo, tulad ng ERP o Accounting, kasama ang mga user at data ng negosyo nito. Ang subscriber ay isang organisasyon o indibidwal kung saan nakarehistro ang aplikasyon sa SV server. Ang isang subscriber ay maaaring magkaroon ng ilang mga application na nakarehistro, at ang mga application na ito ay maaaring makipagpalitan ng mga mensahe sa bawat isa. Ang subscriber ay naging nangungupahan sa aming system. Ang mga mensahe mula sa ilang mga subscriber ay matatagpuan sa isang pisikal na database; kung nakita namin na ang isang subscriber ay nagsimulang bumuo ng maraming trapiko, inililipat namin ito sa isang hiwalay na pisikal na database (o kahit isang hiwalay na database server).

Mayroon kaming pangunahing database kung saan naka-imbak ang isang routing table na may impormasyon tungkol sa lokasyon ng lahat ng database ng subscriber.

Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast

Upang maiwasang maging bottleneck ang pangunahing database, pinananatili namin ang routing table (at iba pang data na madalas na kailangan) sa isang cache.

Kung magsisimulang bumagal ang database ng subscriber, puputulin namin ito sa mga partisyon sa loob. Sa ibang projects na ginagamit namin pg_pathman.

Dahil ang pagkawala ng mga mensahe ng user ay masama, pinapanatili namin ang aming mga database na may mga replika. Ang kumbinasyon ng mga kasabay at asynchronous na mga replika ay nagbibigay-daan sa iyo upang masiguro ang iyong sarili sa kaso ng pagkawala ng pangunahing database. Ang pagkawala ng mensahe ay magaganap lamang kung ang pangunahing database at ang kasabay na replica nito ay nabigo nang sabay.

Kung nawala ang isang kasabay na replika, ang asynchronous na replika ay magiging kasabay.
Kung ang pangunahing database ay nawala, ang kasabay na replica ay magiging pangunahing database, at ang asynchronous na replika ay magiging isang kasabay na replika.

Elasticsearch para sa paghahanap

Dahil, bukod sa iba pang mga bagay, ang SV ay isa ring messenger, nangangailangan ito ng mabilis, maginhawa at nababaluktot na paghahanap, na isinasaalang-alang ang morpolohiya, gamit ang hindi tumpak na mga tugma. Nagpasya kaming hindi muling likhain ang gulong at gamitin ang libreng search engine na Elasticsearch, na nilikha batay sa library Si Lucene. Nag-deploy din kami ng Elasticsearch sa isang cluster (master – data – data) para maalis ang mga problema kung sakaling mabigo ang mga node ng application.

Sa github nakita namin Plugin ng morpolohiya ng Russia para sa Elasticsearch at gamitin ito. Sa Elasticsearch index nag-iimbak kami ng mga ugat ng salita (na tinutukoy ng plugin) at N-grams. Habang ang gumagamit ay nagpasok ng teksto upang maghanap, hinahanap namin ang nai-type na teksto sa mga N-gram. Kapag nai-save sa index, ang salitang "mga teksto" ay mahahati sa mga sumusunod na N-grams:

[mga, tek, tex, text, mga text, ek, ex, ext, mga text, ks, kst, ksty, st, sty, ikaw],

At ang ugat ng salitang "teksto" ay mapangalagaan din. Binibigyang-daan ka ng diskarteng ito na maghanap sa simula, sa gitna, at sa dulo ng salita.

Ang malaking larawan

Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast
Ulitin ang larawan mula sa simula ng artikulo, ngunit may mga paliwanag:

  • Balancer na nakalantad sa Internet; mayroon kaming nginx, maaari itong maging anuman.
  • Ang mga Java application instance ay nakikipag-usap sa isa't isa sa pamamagitan ng Hazelcast.
  • Upang gumana sa isang web socket na ginagamit namin Netty.
  • Ang Java application ay nakasulat sa Java 8 at binubuo ng mga bundle OSGi. Kasama sa mga plano ang paglipat sa Java 10 at paglipat sa mga module.

Pag-unlad at pagsubok

Sa proseso ng pagbuo at pagsubok sa SV, nakatagpo kami ng ilang kawili-wiling feature ng mga produktong ginagamit namin.

Pagsubok sa pag-load at pagtagas ng memorya

Ang paglabas ng bawat paglabas ng SV ay nagsasangkot ng pagsubok sa pagkarga. Ito ay matagumpay kapag:

  • Ang pagsubok ay gumana nang ilang araw at walang mga pagkabigo sa serbisyo
  • Ang oras ng pagtugon para sa mga pangunahing operasyon ay hindi lumampas sa kumportableng threshold
  • Ang pagkasira ng pagganap kumpara sa nakaraang bersyon ay hindi hihigit sa 10%

Pinupuno namin ang database ng pagsubok ng data - upang gawin ito, tumatanggap kami ng impormasyon tungkol sa pinaka-aktibong subscriber mula sa production server, i-multiply ang mga numero nito sa 5 (ang bilang ng mga mensahe, talakayan, user) at subukan ito sa ganoong paraan.

Nagsasagawa kami ng pagsubok sa pagkarga ng sistema ng pakikipag-ugnayan sa tatlong mga pagsasaayos:

  1. pagsubok ng stress
  2. Mga koneksyon lamang
  3. Pagpaparehistro ng subscriber

Sa panahon ng stress test, naglulunsad kami ng ilang daang mga thread, at nilo-load nila ang system nang walang tigil: pagsusulat ng mga mensahe, paglikha ng mga talakayan, pagtanggap ng listahan ng mga mensahe. Ginagaya namin ang mga aksyon ng mga ordinaryong user (kumuha ng listahan ng aking mga hindi pa nababasang mensahe, sumulat sa isang tao) at mga solusyon sa software (nagpapadala ng package ng ibang configuration, magproseso ng alerto).

Halimbawa, ganito ang hitsura ng bahagi ng stress test:

  • Nag-log in ang user
    • Hinihiling ang iyong mga hindi pa nababasang talakayan
    • 50% malamang na magbasa ng mga mensahe
    • 50% ang posibilidad na mag-text
    • Susunod na gumagamit:
      • May 20% na pagkakataong lumikha ng bagong talakayan
      • Random na pinipili ang alinman sa mga talakayan nito
      • Papasok sa loob
      • Humihiling ng mga mensahe, profile ng user
      • Lumilikha ng limang mensahe na naka-address sa mga random na user mula sa talakayang ito
      • Umalis ng talakayan
      • Umuulit ng 20 beses
      • Nag-log out, babalik sa simula ng script

    • Ang isang chatbot ay pumapasok sa system (emulates messaging mula sa application code)
      • May 50% na pagkakataong lumikha ng bagong channel para sa pagpapalitan ng data (espesyal na talakayan)
      • 50% malamang na magsulat ng mensahe sa alinman sa mga kasalukuyang channel

Ang senaryo na "Mga Koneksyon Lang" ay lumitaw para sa isang dahilan. Mayroong isang sitwasyon: ang mga gumagamit ay nakakonekta sa system, ngunit hindi pa nakikibahagi. Binubuksan ng bawat user ang computer sa 09:00 ng umaga, nagtatatag ng koneksyon sa server at nananatiling tahimik. Ang mga taong ito ay mapanganib, marami sa kanila - ang tanging mga pakete na mayroon sila ay PING/PONG, ngunit pinapanatili nila ang koneksyon sa server (hindi nila ito mapanatili - paano kung may bagong mensahe). Ang pagsubok ay muling gumagawa ng isang sitwasyon kung saan ang isang malaking bilang ng mga naturang user ay sumusubok na mag-log in sa system sa loob ng kalahating oras. Ito ay katulad ng isang stress test, ngunit ang focus nito ay tiyak sa unang input na ito - upang walang mga pagkabigo (ang isang tao ay hindi gumagamit ng system, at ito ay bumagsak - mahirap mag-isip ng mas masahol pa).

Ang script ng pagpaparehistro ng subscriber ay nagsisimula sa unang paglulunsad. Nagsagawa kami ng stress test at natitiyak namin na hindi bumagal ang sistema sa panahon ng pagsusulatan. Ngunit dumating ang mga user at nagsimulang mabigo ang pagpaparehistro dahil sa isang timeout. Noong nagparehistro kami ay ginamit / dev / random, na nauugnay sa entropy ng system. Ang server ay walang oras upang makaipon ng sapat na entropy at kapag ang isang bagong SecureRandom ay hiniling, ito ay nagyelo sa loob ng sampu-sampung segundo. Mayroong maraming mga paraan sa labas ng sitwasyong ito, halimbawa: lumipat sa hindi gaanong secure na /dev/urandom, mag-install ng espesyal na board na bumubuo ng entropy, bumuo ng mga random na numero nang maaga at iimbak ang mga ito sa isang pool. Pansamantala naming isinara ang problema sa pool, ngunit mula noon ay nagpapatakbo na kami ng hiwalay na pagsubok para sa pagpaparehistro ng mga bagong subscriber.

Ginagamit namin bilang generator ng pagkarga JMeter. Hindi nito alam kung paano gumana sa websocket; kailangan nito ng plugin. Ang una sa mga resulta ng paghahanap para sa query na "jmeter websocket" ay: mga artikulo mula sa BlazeMeter, na nagrerekomenda plugin ni Maciej Zaleski.

Doon na kami nagpasya na magsimula.

Halos kaagad pagkatapos magsimula ng seryosong pagsubok, natuklasan namin na nagsimulang mag-leak ng memory ang JMeter.

Ang plugin ay isang hiwalay na malaking kuwento; na may 176 na bituin, mayroon itong 132 na tinidor sa github. Ang may-akda mismo ay hindi nakatuon dito mula noong 2015 (kinuha namin ito noong 2015, pagkatapos ay hindi ito nagtaas ng mga hinala), maraming mga isyu sa github tungkol sa mga pagtagas ng memorya, 7 hindi sarado na mga kahilingan sa paghila.
Kung magpasya kang magsagawa ng pagsubok sa pagkarga gamit ang plugin na ito, mangyaring bigyang pansin ang mga sumusunod na talakayan:

  1. Sa isang multi-threaded na kapaligiran, isang regular na LinkList ang ginamit, at ang resulta ay NPE sa runtime. Ito ay malulutas sa alinman sa pamamagitan ng paglipat sa ConcurrentLinkedDeque o sa pamamagitan ng mga naka-synchronize na bloke. Pinili namin ang unang pagpipilian para sa aming sarili (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Memory leak; kapag dinidiskonekta, hindi tinatanggal ang impormasyon ng koneksyon (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Sa streaming mode (kapag hindi nakasara ang websocket sa dulo ng sample, ngunit ginamit sa ibang pagkakataon sa plano), hindi gumagana ang mga pattern ng pagtugon (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Isa ito sa mga nasa github. Ang ginawa namin:

  1. Kinuha tinidor si Elyran Kogan (@elyrank) – inaayos nito ang mga problema 1 at 3
  2. Nalutas ang problema 2
  3. Na-update ang jetty mula 9.2.14 hanggang 9.3.12
  4. Nakabalot sa SimpleDateFormat sa ThreadLocal; SimpleDateFormat ay hindi thread-safe, na humantong sa NPE sa runtime
  5. Inayos ang isa pang memory leak (ang koneksyon ay naisara nang hindi tama kapag nadiskonekta)

At gayon pa man ito ay dumadaloy!

Ang memorya ay nagsimulang maubos hindi sa isang araw, ngunit sa dalawa. Wala na talagang oras, kaya nagpasya kaming maglunsad ng mas kaunting mga thread, ngunit sa apat na ahente. Ito ay dapat na sapat para sa hindi bababa sa isang linggo.

Dalawang araw na ang lumipas...

Ngayon ay nauubusan na ng memorya ang Hazelcast. Ang mga log ay nagpakita na pagkatapos ng ilang araw ng pagsubok, si Hazelcast ay nagsimulang magreklamo tungkol sa kakulangan ng memorya, at pagkaraan ng ilang oras ang kumpol ay nahulog, at ang mga node ay patuloy na namamatay nang paisa-isa. Ikinonekta namin ang JVisualVM sa hazelcast at nakakita ng "rising saw" - regular itong tinatawag na GC, ngunit hindi ma-clear ang memorya.

Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast

Ito ay lumabas na sa hazelcast 3.4, kapag nagtanggal ng isang mapa / multiMap (map.destroy()), ang memorya ay hindi ganap na napalaya:

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

Ang bug ay naayos na ngayon sa 3.5, ngunit ito ay isang problema noon. Gumawa kami ng mga bagong multiMaps na may mga dynamic na pangalan at tinanggal ang mga ito ayon sa aming lohika. Ang code ay mukhang ganito:

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

Tumawag:

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

multiMap ay nilikha para sa bawat subscription at tinanggal kapag ito ay hindi kinakailangan. Nagpasya kaming sisimulan namin ang Map , ang susi ay ang pangalan ng subscription, at ang mga halaga ay magiging session identifier (kung saan maaari kang makakuha ng mga identifier ng user, kung kinakailangan).

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

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

Ang mga tsart ay bumuti.

Paano at bakit kami nagsulat ng high-load scalable na serbisyo para sa 1C: Enterprise: Java, PostgreSQL, Hazelcast

Ano pa ang natutunan natin tungkol sa pagsubok sa pagkarga?

  1. Ang JSR223 ay kailangang isulat sa groovy at isama ang compilation cache - mas mabilis ito. Link.
  2. Ang mga graph ng Jmeter-Plugins ay mas madaling maunawaan kaysa sa mga karaniwang graph. Link.

Tungkol sa aming karanasan sa Hazelcast

Ang Hazelcast ay isang bagong produkto para sa amin, nagsimula kaming magtrabaho kasama nito mula sa bersyon 3.4.1, ngayon ang aming production server ay nagpapatakbo ng bersyon 3.9.2 (sa oras ng pagsulat, ang pinakabagong bersyon ng Hazelcast ay 3.10).

Pagbuo ng ID

Nagsimula kami sa mga integer identifier. Isipin natin na kailangan natin ng isa pang Long para sa isang bagong entity. Ang pagkakasunud-sunod sa database ay hindi angkop, ang mga talahanayan ay kasangkot sa sharding - lumalabas na mayroong isang mensahe ID=1 sa DB1 at isang mensahe ID=1 sa DB2, hindi mo maaaring ilagay ang ID na ito sa Elasticsearch, o sa Hazelcast , ngunit ang pinakamasama ay kung gusto mong pagsamahin ang data mula sa dalawang database sa isa (halimbawa, ang pagpapasya na ang isang database ay sapat para sa mga subscriber na ito). Maaari kang magdagdag ng ilang AtomicLongs sa Hazelcast at panatilihin ang counter doon, pagkatapos ay ang pagganap ng pagkuha ng bagong ID ay incrementAndGet kasama ang oras para sa isang kahilingan sa Hazelcast. Ngunit ang Hazelcast ay may mas mahusay na bagay - FlakeIdGenerator. Kapag nakikipag-ugnayan sa bawat kliyente, binibigyan sila ng hanay ng ID, halimbawa, ang una - mula 1 hanggang 10, ang pangalawa - mula 000 hanggang 10, at iba pa. Ngayon ang kliyente ay maaaring mag-isyu ng mga bagong identifier nang mag-isa hanggang sa matapos ang saklaw na ibinigay dito. Mabilis itong gumagana, ngunit kapag na-restart mo ang application (at ang Hazelcast client), magsisimula ang isang bagong sequence - kaya ang mga paglaktaw, atbp. Bilang karagdagan, hindi talaga nauunawaan ng mga developer kung bakit integer ang mga ID, ngunit sobrang hindi pare-pareho. Tinitimbang namin ang lahat at lumipat sa mga UUID.

Sa pamamagitan ng paraan, para sa mga nais maging tulad ng Twitter, mayroong isang Snowcast library - ito ay isang pagpapatupad ng Snowflake sa ibabaw ng Hazelcast. Maaari mo itong tingnan dito:

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

Ngunit hindi na namin ito naabutan.

TransactionalMap.palitan

Isa pang sorpresa: TransactionalMap.replace ay hindi gumagana. Narito ang isang pagsubok:

@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

Kinailangan kong magsulat ng sarili kong palitan gamit ang 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);
}

Subukan hindi lamang ang mga regular na istruktura ng data, kundi pati na rin ang kanilang mga transactional na bersyon. Ito ay nangyayari na gumagana ang IMap, ngunit ang TransactionalMap ay wala na.

Magpasok ng bagong JAR nang walang downtime

Una, nagpasya kaming i-record ang mga bagay ng aming mga klase sa Hazelcast. Halimbawa, mayroon kaming klase ng Application, gusto naming i-save at basahin ito. I-save:

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

Nabasa namin:

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

Lahat ay gumagana. Pagkatapos ay nagpasya kaming bumuo ng isang index sa Hazelcast upang maghanap sa pamamagitan ng:

map.addIndex("subscriberId", false);

At nang sumulat ng bagong entity, nagsimula silang makatanggap ng ClassNotFoundException. Sinubukan ni Hazelcast na magdagdag sa index, ngunit wala siyang alam tungkol sa aming klase at gusto niya ang isang JAR na may ganitong klase na maibigay dito. Ginawa lang namin iyon, gumana ang lahat, ngunit lumitaw ang isang bagong problema: kung paano i-update ang JAR nang hindi ganap na huminto sa kumpol? Hindi kinukuha ng Hazelcast ang bagong JAR sa panahon ng pag-update ng node-by-node. Sa puntong ito napagpasyahan namin na mabubuhay kami nang walang paghahanap ng index. Pagkatapos ng lahat, kung gagamitin mo ang Hazelcast bilang isang key-value store, kung gayon ang lahat ay gagana? Hindi naman. Dito na naman iba ang ugali ng IMap at TransactionalMap. Kung saan walang pakialam ang IMap, naghagis ng error ang TransactionalMap.

IMap. Sumulat kami ng 5000 bagay, basahin ang mga ito. Lahat ay inaasahan.

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

Ngunit hindi ito gumagana sa isang transaksyon, nakakakuha kami ng 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();
        }
    });
}

Sa 3.8, lumitaw ang mekanismo ng User Class Deployment. Maaari kang magtalaga ng isang master node at i-update ang JAR file dito.

Ngayon ay ganap na naming binago ang aming diskarte: ini-serialize namin ito sa JSON at i-save ito sa Hazelcast. Hindi kailangang malaman ng Hazelcast ang istruktura ng aming mga klase, at maaari kaming mag-update nang walang downtime. Ang pag-bersyon ng mga bagay sa domain ay kinokontrol ng application. Ang iba't ibang bersyon ng application ay maaaring tumakbo nang sabay-sabay, at ang isang sitwasyon ay posible kapag ang bagong application ay nagsusulat ng mga bagay na may mga bagong field, ngunit ang luma ay hindi pa alam ang tungkol sa mga field na ito. At sa parehong oras, ang bagong application ay nagbabasa ng mga bagay na isinulat ng lumang application na walang bagong mga patlang. Pinangangasiwaan namin ang mga ganitong sitwasyon sa loob ng application, ngunit para sa pagiging simple hindi namin binabago o tinatanggal ang mga field, pinapalawak lang namin ang mga klase sa pamamagitan ng pagdaragdag ng mga bagong field.

Paano namin tinitiyak ang mataas na pagganap

Apat na biyahe sa Hazelcast - mabuti, dalawa sa database - masama

Ang pagpunta sa cache para sa data ay palaging mas mahusay kaysa sa pagpunta sa database, ngunit ayaw mo ring mag-imbak ng mga hindi nagamit na tala. Iniiwan namin ang desisyon tungkol sa kung ano ang i-cache hanggang sa huling yugto ng pag-unlad. Kapag na-code ang bagong functionality, ino-on namin ang pag-log ng lahat ng query sa PostgreSQL (log_min_duration_statement to 0) at patakbuhin ang load testing sa loob ng 20 minuto. Gamit ang mga nakolektang log, ang mga utility gaya ng pgFouine at pgBadger ay maaaring bumuo ng mga analytical na ulat. Sa mga ulat, pangunahing hinahanap namin ang mabagal at madalas na mga query. Para sa mabagal na query, bumuo kami ng execution plan (EXPLAIN) at sinusuri kung ang naturang query ay maaaring mapabilis. Ang mga madalas na kahilingan para sa parehong data ng pag-input ay akma sa cache. Sinusubukan naming panatilihing "flat" ang mga query, isang talahanayan bawat query.

Pagsasamantala

Ang SV bilang isang online na serbisyo ay inilagay sa operasyon noong tagsibol ng 2017, at bilang isang hiwalay na produkto, ang SV ay inilabas noong Nobyembre 2017 (sa panahong iyon ay nasa beta version status).

Sa mahigit isang taon ng operasyon, walang mga seryosong problema sa pagpapatakbo ng CB online na serbisyo. Sinusubaybayan namin ang online na serbisyo sa pamamagitan ng Zabbix, kolektahin at i-deploy mula sa Kawayan.

Ang pamamahagi ng SV server ay ibinibigay sa anyo ng mga native na pakete: RPM, DEB, MSI. Dagdag pa para sa Windows, nagbibigay kami ng isang installer sa anyo ng isang EXE na nag-i-install ng server, Hazelcast at Elasticsearch sa isang makina. Una naming tinukoy ang bersyong ito ng pag-install bilang ang bersyon ng "demo", ngunit naging malinaw na ngayon na ito ang pinakasikat na opsyon sa pag-deploy.

Pinagmulan: www.habr.com

Magdagdag ng komento