![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Ang katatagan sa software ay tumutukoy sa kakayahan ng isang application na magpatuloy sa paggana ng maayos at mapagkakatiwalaan, kahit na sa harap ng mga hindi inaasahang isyu o pagkabigo. Sa mga proyekto ng Fintech, ang katatagan ay lalong mahalaga dahil sa ilang kadahilanan. Una, obligado ang mga kumpanya na matugunan ang mga kinakailangan sa regulasyon at binibigyang-diin ng mga regulator ng pananalapi ang katatagan ng pagpapatakbo upang mapanatili ang katatagan sa loob ng system. Bukod dito, ang paglaganap ng mga digital na tool at pag-asa sa mga third-party na service provider ay naglalantad sa mga negosyo ng Fintech sa mas mataas na banta sa seguridad. Nakakatulong din ang resilience na mapagaan ang mga panganib ng mga outage na dulot ng iba't ibang salik gaya ng mga banta sa cyber, pandemya, o geopolitical na kaganapan, pagprotekta sa mga pangunahing operasyon ng negosyo at mga kritikal na asset.
Sa pamamagitan ng mga pattern ng resilience, nauunawaan namin ang isang hanay ng pinakamahuhusay na kagawian at diskarte na idinisenyo upang matiyak na ang software ay makatiis sa mga pagkagambala at mapanatili ang mga operasyon nito. Ang mga pattern na ito ay kumikilos tulad ng mga safety net, na nagbibigay ng mga mekanismo upang mahawakan ang mga error, pamahalaan ang pagkarga, at makabawi mula sa mga pagkabigo, sa gayon ay tinitiyak na ang mga application ay mananatiling matatag at maaasahan sa ilalim ng masamang mga kondisyon.
Kabilang sa mga pinakakaraniwang diskarte sa resilience ang bulkhead, cache, fallback, retry, at circuit breaker. Sa artikulong ito, tatalakayin ko ang mga ito nang mas detalyado, kasama ang mga halimbawa ng mga problemang matutulungan nilang lutasin.
Tingnan natin ang setting sa itaas. Mayroon kaming isang napaka-ordinaryong application na may ilang mga backend sa likod namin upang makakuha ng ilang data mula sa. Mayroong ilang mga HTTP client na konektado sa mga backend na ito. Iisa pala ang connection pool nilang lahat! At iba pang mga mapagkukunan tulad ng CPU at RAM.
Ano ang mangyayari, Kung ang isa sa mga backend ay nakakaranas ng ilang uri ng mga problema na nagreresulta sa mataas na latency ng kahilingan? Dahil sa mataas na oras ng pagtugon, ang buong pool ng koneksyon ay magiging ganap na abala ng mga kahilingang naghihintay ng mga tugon mula sa backend1. Bilang resulta, ang mga kahilingan para sa malusog na backend2 at backend3 ay hindi na magpapatuloy dahil ang pool ay naubos na. Nangangahulugan ito na ang pagkabigo sa isa sa aming mga backend ay maaaring magdulot ng pagkabigo sa buong application. Sa isip, gusto lang namin ang functionality na nauugnay sa bagsak na backend na makaranas ng pagkasira, habang ang natitirang bahagi ng application ay patuloy na gumagana nang normal.
Ano ang Bulkhead Pattern?
Ang terminong, Bulkhead pattern, ay nagmula sa paggawa ng barko, ito ay nagsasangkot ng paglikha ng ilang nakahiwalay na mga compartment sa loob ng isang barko. Kung ang pagtagas ay nangyari sa isang kompartimento, ito ay napupuno ng tubig, ngunit ang iba pang mga kompartamento ay nananatiling hindi naaapektuhan. Pinipigilan ng paghihiwalay na ito ang buong sisidlan na lumubog dahil sa isang paglabag.
Ang Bulkhead pattern ay maaaring gamitin upang ihiwalay ang iba't ibang uri ng mga mapagkukunan sa loob ng isang application, na pumipigil sa isang pagkabigo sa isang bahagi na makaapekto sa buong system. Narito kung paano natin ito mailalapat sa ating problema:
Ipagpalagay natin na ang aming mga backend system ay may mababang posibilidad na makatagpo ng mga error nang paisa-isa. Gayunpaman, kapag ang isang operasyon ay nagsasangkot ng pag-query sa lahat ng mga backend na ito nang magkatulad, ang bawat isa ay maaaring independiyenteng magbalik ng isang error. Dahil ang mga error na ito ay nangyayari nang hiwalay, ang pangkalahatang posibilidad ng isang error sa aming aplikasyon ay mas mataas kaysa sa posibilidad ng error ng anumang solong backend. Maaaring kalkulahin ang pinagsama-samang posibilidad ng error gamit ang formula na P_total=1−(1−p)^n, kung saan ang n ay ang bilang ng mga backend system.
Halimbawa, kung mayroon kaming sampung backend, bawat isa ay may posibilidad ng error na p=0.001 (naaayon sa isang SLA na 99.9%), ang resultang probabilidad ng error ay:
P_total=1−(1−0.001)^10=0.009955
Nangangahulugan ito na ang aming pinagsamang SLA ay bumaba sa humigit-kumulang 99%, na naglalarawan kung paano bumababa ang pangkalahatang pagiging maaasahan kapag nag-query ng maraming backend nang magkasabay. Para mabawasan ang isyung ito, maaari tayong magpatupad ng in-memory cache.
Ang isang in-memory na cache ay nagsisilbing isang high-speed data buffer, na nag-iimbak ng mga madalas na naa-access na data at inaalis ang pangangailangan na kunin ito mula sa mga potensyal na mabagal na mapagkukunan sa bawat oras. Dahil ang mga cache na nakaimbak sa memorya ay may 0% na posibilidad ng error kumpara sa pagkuha ng data sa network, malaki ang pagtaas ng pagiging maaasahan ng aming application. Bukod dito, binabawasan ng caching ang trapiko sa network, na higit pang nagpapababa ng pagkakataon ng mga error. Dahil dito, sa pamamagitan ng paggamit ng in-memory na cache, makakamit namin ang mas mababang rate ng error sa aming application kumpara sa aming mga backend system. Bukod pa rito, ang mga in-memory na cache ay nag-aalok ng mas mabilis na pagkuha ng data kaysa sa pagkuha na nakabatay sa network, at sa gayon ay binabawasan ang latency ng application—isang kapansin-pansing kalamangan.
Para sa personalized na data, tulad ng mga profile ng user o rekomendasyon, ang paggamit ng mga in-memory na cache ay maaari ding maging lubos na epektibo. Ngunit kailangan naming tiyakin na ang lahat ng kahilingan mula sa isang user ay patuloy na napupunta sa parehong instance ng application upang magamit ang data na naka-cache para sa kanila, na nangangailangan ng mga malagkit na session. Maaaring maging mahirap ang pagpapatupad ng mga sticky session, ngunit para sa sitwasyong ito, hindi namin kailangan ng mga kumplikadong mekanismo. Katanggap-tanggap ang menor na traffic rebalancing, kaya sapat na ang isang stable na load balancing algorithm tulad ng pare-parehong pag-hash.
Higit pa rito, kung sakaling magkaroon ng pagkabigo sa node, tinitiyak ng pare-parehong pag-hash na ang mga user lang na nauugnay sa nabigong node ang sumasailalim sa muling pagbabalanse, na pinapaliit ang pagkagambala sa system. Pinapasimple ng diskarteng ito ang pamamahala ng mga personalized na cache at pinapahusay ang pangkalahatang katatagan at pagganap ng aming application.
Kung ang data na nilalayon naming i-cache ay kritikal at ginagamit sa bawat kahilingang pinangangasiwaan ng aming system, gaya ng mga patakaran sa pag-access, mga plano sa subscription, o iba pang mahahalagang entity sa aming domain—ang pinagmulan ng data na ito ay maaaring magdulot ng malaking punto ng pagkabigo sa aming system. Upang matugunan ang hamon na ito, ang isang diskarte ay ang ganap na kopyahin ang data na ito nang direkta sa memorya ng aming aplikasyon.
Sa sitwasyong ito, kung ang dami ng data sa pinagmulan ay mapapamahalaan, maaari naming simulan ang proseso sa pamamagitan ng pag-download ng snapshot ng data na ito sa simula ng aming aplikasyon. Kasunod nito, makakatanggap kami ng mga kaganapan sa pag-update upang matiyak na mananatiling naka-synchronize ang naka-cache na data sa pinagmulan. Sa pamamagitan ng paggamit ng pamamaraang ito, pinapahusay namin ang pagiging maaasahan ng pag-access sa mahalagang data na ito, dahil ang bawat pagkuha ay direktang nangyayari mula sa memorya na may 0% na posibilidad ng error. Bukod pa rito, ang pagkuha ng data mula sa memorya ay napakabilis, sa gayon ay na-optimize ang pagganap ng aming aplikasyon. Ang diskarte na ito ay epektibong nagpapagaan sa panganib na nauugnay sa pag-asa sa isang panlabas na pinagmumulan ng data, na tinitiyak ang pare-pareho at maaasahang pag-access sa kritikal na impormasyon para sa pagpapatakbo ng aming application.
Gayunpaman, ang pangangailangang mag-download ng data sa pagsisimula ng aplikasyon, sa gayon ay naantala ang proseso ng pagsisimula, ay lumalabag sa isa sa mga prinsipyo ng '12-factor na aplikasyon' na nagtataguyod para sa mabilis na pagsisimula ng aplikasyon. Ngunit, hindi namin nais na mawala ang mga benepisyo ng paggamit ng caching. Para matugunan ang dilemma na ito, tuklasin natin ang mga potensyal na solusyon.
Napakahalaga ng mabilis na pagsisimula, lalo na para sa mga platform tulad ng Kubernetes, na umaasa sa mabilis na paglipat ng application sa iba't ibang pisikal na node. Sa kabutihang palad, maaaring pamahalaan ng Kubernetes ang mga mabagal na pagsisimula ng mga application gamit ang mga feature tulad ng mga startup probe.
Ang isa pang hamon na maaari naming harapin ay ang pag-update ng mga configuration habang tumatakbo ang application. Kadalasan, kailangan ang pagsasaayos ng mga oras ng cache o paghiling ng mga timeout upang malutas ang mga isyu sa produksyon. Kahit na mabilis naming mai-deploy ang mga na-update na configuration file sa aming application, ang paglalapat ng mga pagbabagong ito ay karaniwang nangangailangan ng pag-restart. Sa pinahabang oras ng pagsisimula ng bawat application, ang isang rolling restart ay maaaring makabuluhang maantala ang pag-deploy ng mga pag-aayos sa aming mga user.
Upang matugunan ito, isang solusyon ay ang pag-imbak ng mga configuration sa isang kasabay na variable at magkaroon ng background thread na pana-panahong i-update ito. Gayunpaman, ang ilang partikular na parameter, gaya ng mga pag-timeout ng kahilingan sa HTTP, ay maaaring mangailangan ng muling pagsisimula ng mga kliyente ng HTTP o database kapag nagbago ang kaukulang configuration, na nagdudulot ng potensyal na hamon. Gayunpaman, ang ilang mga kliyente, tulad ng driver ng Cassandra para sa Java, ay sumusuporta sa awtomatikong pag-reload ng mga pagsasaayos, na pinapasimple ang prosesong ito.
Maaaring mabawasan ng pagpapatupad ng mga reloadable na configuration ang negatibong epekto ng mahabang oras ng pagsisimula ng application at mag-alok ng mga karagdagang benepisyo, gaya ng pagpapadali sa mga pagpapatupad ng feature flag. Nagbibigay-daan sa amin ang diskarteng ito na mapanatili ang pagiging maaasahan at kakayahang tumugon ng application habang mahusay na pinamamahalaan ang mga update sa configuration.
Ngayon tingnan natin ang isa pang problema: sa aming system, kapag ang isang kahilingan ng user ay natanggap at naproseso sa pamamagitan ng pagpapadala ng isang query sa isang backend o database, paminsan-minsan, isang error na tugon ang natatanggap sa halip na ang inaasahang data. Kasunod nito, tumutugon ang aming system sa user nang may 'error'.
Gayunpaman, sa maraming mga sitwasyon, maaaring mas mainam na magpakita ng bahagyang luma na data kasama ng isang mensaheng nagsasaad na mayroong pagkaantala sa pag-refresh ng data, sa halip na iwan ang user na may malaking pulang mensahe ng error.
Upang matugunan ang isyung ito at mapabuti ang pag-uugali ng aming system, maaari naming ipatupad ang Fallback pattern. Ang konsepto sa likod ng pattern na ito ay nagsasangkot ng pagkakaroon ng pangalawang data source, na maaaring maglaman ng data na mas mababa ang kalidad o pagiging bago kumpara sa pangunahing source. Kung hindi available ang pangunahing data source o nagbalik ng error, maaaring bumalik ang system sa pagkuha ng data mula sa pangalawang source na ito, na tinitiyak na may ilang uri ng impormasyon na ipapakita sa user sa halip na magpakita ng mensahe ng error.
Kung titingnan mo ang larawan sa itaas, mapapansin mo ang isang pagkakatulad sa pagitan ng isyung kinakaharap namin ngayon at ang naranasan namin sa halimbawa ng cache.
Upang malutas ito, maaari naming isaalang-alang ang pagpapatupad ng isang pattern na kilala bilang muling subukan. Sa halip na umasa sa mga cache, maaaring idisenyo ang system upang awtomatikong ipadala muli ang kahilingan kung sakaling magkaroon ng error. Nag-aalok ang retry pattern na ito ng mas simpleng alternatibo at epektibong makakabawas sa posibilidad ng mga error sa aming aplikasyon. Hindi tulad ng pag-cache, na kadalasang nangangailangan ng mga kumplikadong mekanismo ng invalidation ng cache upang mahawakan ang mga pagbabago ng data, ang muling pagsubok sa mga nabigong kahilingan ay medyo diretsong ipatupad. Dahil malawak na itinuturing ang cache invalidation bilang isa sa pinakamahirap na gawain sa software engineering, ang pagpapatibay ng diskarte sa muling pagsubok ay maaaring mag-streamline ng paghawak ng error at mapabuti ang system resilience.
Gayunpaman, ang paggamit ng diskarteng muling subukan nang hindi isinasaalang-alang ang mga potensyal na kahihinatnan ay maaaring humantong sa mga karagdagang komplikasyon.
Isipin natin na ang isa sa aming mga backend ay nakakaranas ng pagkabigo. Sa ganoong sitwasyon, ang pagsisimula ng mga muling pagsubok sa bagsak na backend ay maaaring magresulta sa isang makabuluhang pagtaas sa dami ng trapiko. Ang biglaang pag-agos ng trapiko na ito ay maaaring matabunan ang backend, na magpapalala sa pagkabigo at posibleng magdulot ng kaskad na epekto sa buong system.
Upang makayanan ang hamon na ito, mahalagang dagdagan ang pattern ng muling pagsubok sa pattern ng circuit breaker. Ang circuit breaker ay nagsisilbing mekanismo ng pag-iingat na sumusubaybay sa rate ng error ng mga serbisyo sa ibaba ng agos. Kapag ang rate ng error ay lumampas sa isang paunang natukoy na threshold, ang circuit breaker ay nakakaabala sa mga kahilingan sa apektadong serbisyo para sa isang tinukoy na tagal. Sa panahong ito, pinipigilan ng system ang pagpapadala ng mga karagdagang kahilingan upang payagan ang bagsak na oras ng serbisyo na mabawi. Pagkatapos ng itinalagang agwat, maingat na pinapayagan ng circuit breaker ang isang limitadong bilang ng mga kahilingan na dumaan, na nagpapatunay kung ang serbisyo ay naging matatag. Kung ang serbisyo ay nakabawi, ang normal na trapiko ay unti-unting naibabalik; kung hindi, ang circuit ay nananatiling bukas, na patuloy na hinaharangan ang mga kahilingan hanggang ang serbisyo ay nagpapatuloy sa normal na operasyon. Sa pamamagitan ng pagsasama ng pattern ng circuit breaker kasama ng retry logic, mabisa naming mapapamahalaan ang mga sitwasyon ng error at maiwasan ang overload ng system sa panahon ng mga pagkabigo sa backend.
Bilang konklusyon, sa pamamagitan ng pagpapatupad ng mga pattern ng resilience na ito, mapapalakas namin ang aming mga application laban sa mga emerhensiya, mapanatili ang mataas na kakayahang magamit, at makapaghatid ng tuluy-tuloy na karanasan sa mga user. Bukod pa rito, gusto kong bigyang-diin na ang telemetry ay isa pang tool na hindi dapat palampasin kapag nagbibigay ng katatagan ng proyekto. Ang mahusay na mga log at sukatan ay maaaring makabuluhang mapahusay ang kalidad ng mga serbisyo at magbigay ng mahahalagang insight sa kanilang pagganap, na tumutulong sa paggawa ng matalinong mga pagpapasya upang mapabuti pa ang mga ito.