frontend လိုအပ်သလား

backend ရှေ့မှာ reverse proxy ခံတဲ့အခါ ကျွန်တော်တို့ ရွေးချယ်လေ့ရှိတာက load balancer service တွေ (ဥပမာ ALB, NLB) တခုခု ဒါမှမဟုတ် software proxy (ဥပမာ nginx, apache httpd) တွေဖြစ်ကြတယ်။ သူတို့ရဲ့ အဓိက​တာဝန်က static file serving, TLS termination, authentication နဲ့ authorization တွေဖြစ်ကြပေမယ့် request path မှာ extra hop တခုထပ်မထည့်ရအောင် reverse proxy component ထဲကို load balancing feature ပါတခါတည်း ထည့်ပေါင်းခဲ့​ကြတယ်။ ဒါပေမယ့် ဒီလို lb မျိုးတွေက generic software ဖြစ်တဲ့အတွက် coarse-grained load balancing လို့ခေါ်တဲ့ အကြမ်းထည်ဆန်တဲ့ lb မျိုးကိုပဲ ရနိုင်တယ်။ ဒါကြောင့် အဲဒီလို load balancer ကို dumb proxy လို့ခေါ်တယ်။

dumb proxy တွေက edge server တွေမှာ deploy ထားလို့ကောင်းတဲ့ component မျိုးတွေဖြစ်တယ်။ client ဆီက traffic ကို TLS termination လုပ်ပြီးတာနဲ့ အသင့်တော်ဆုံး data center ဆီ route ပေးဖို့ dumb proxy တွေကို သုံးနိုင်တယ်။ DNS infrastructure မှာ resolver ပါနေခြင်းက ip တွေကို geolocate လုပ်ရခက်တဲ့အတွက် client အတွက် အသင့်တော်ဆုံး dc ကို Geo DNS ကနေ တန်းရွေးပေးလိုက်ဖို့ မလွယ်ဘူး။ ဒါကြောင့် edge က load balancing အတွက် ဒုတိယခံတပ်ပဲ။ edge မှာ dumb proxy ရဲ့တာဝန်က dc အလိုက် healthcheck လုပ်ပြီး latency-first service တွေအတွက်ဆိုရင် အနီးဆုံး ဒါမှမဟုတ် underutilized dc ဆီ၊ နောက် throughput-first service တွေအတွက်ဆိုရင် underutilized dc ဆီ route ပေးဖို့ဖြစ်တယ်။

ဒီတော့ traffic က edge ကနေ သက်ဆိုင်ရာ az အလိုက်ရှိကြတဲ့ load balancer တွေဆီ ရောက်ပြီဆိုပါတော့။ ဒီနေရာမှာ lb လို့ပဲခေါ်ရတာ နှုတ်ကျိုးနေလို့သာ lb လို့သုံးလိုက်တာ။ တကယ်တမ်း ဆိုလိုချင်တာက dc-level နောက်ထပ် dumb proxy ပါပဲ။ ဒီကနေ traffic က fine-grained load balancing လုပ်ပေးမယ့် layer ဆီရောက်မယ်။ အဲဒီ layer ကိုတော့ frontend လို့ခေါ်တယ်။ frontend က smart proxy တွေဖြစ်ပြီး ဒီတာဝန်တွေ ယူရတယ်။

east west traffic အတွက် performance ကောင်းအောင် ပို complex ဖြစ်တဲ့ protocol တွေ ဥပမာ RPC လိုမျိုးသုံးထားရင် frontend က protocol translation ကို လုပ်တယ်။ နောက် backend တွေနဲ့ proactive handshake လုပ်ရတယ်။ အဲဒီ backend တွေကို healthcheck ပုံမှန်လုပ်တယ်။ နောက် backend တွေဆီက saturation state ကိုနားထောင်ပြီး lb weight ကိုအမြဲတွက်ရတယ်။ backend က degradation flag ပြလာခဲ့ရင် respect ထားလိုက်နာပြီး rebalancing ပြန်လုပ်ပေးရတယ်။ backend က stateless ဖြစ်တယ်ဆိုရင် performance-sensitive ဖြစ်တဲ့ read path တွေမှာ တခုထက်မကတဲ့ backend တွေဆီ request တွေအပြိုင်ပို့ပြီး instance တခုဆီက အဖြေရတဲ့အခါ ကျန်တဲ့ inflight တွေကို cancel ပေးရတယ်။ hedging လို့ခေါ်တယ်။ နောက် backend fleet တခုလုံး overload ဖြစ်လာရင် throttling နဲ့ဖြစ်ဖြစ် non-critical request တွေကို drop ပစ်တာပြီးတော့ဖြစ်ဖြစ် backpressure လုပ်ပေးရတယ်။ အရေးကြီးဆုံးက လိုအပ်ရင် retry လုပ်ပေးရတယ်။ retry တွေက backend ဆီက propagation status ကိုလိုက်နာဖို့နဲ့ per-request, per-process quota တွေရှိဖို့တော့ လိုတယ်။

frontend လိုအပ်မအပ် လက်တွေ့ implementation detail ကရလာတဲ့ သင်ခန်းစာတွေကို အခြေခံပြီး ပြောပြမယ်။ ledger ကိုကျွန်တော်တို့ architecture rework မလုပ်ခင်က backend အတွက် stateless (Java + Redis + Mysql) ဖြစ်ပြီး dumb proxy ခံခဲ့ကြတယ်။ ဒီ architecture အဟောင်းမှာ ပြဿနာတွေ အများကြီးကြုံခဲ့တဲ့အထဲက တခုက resiliency မရှိတာ။ သုံးထားတဲ့ dumb proxy က retry ကိုအဖြစ်လောက် support နိုင်တဲ့ generic lb ဖြစ်တဲ့အပြင် overload တွေမှာလည်း local နဲ့ global ခွဲလို့မရတဲ့အတွက် မကြာခဏ retry storm ဖြစ်ပြီး overload ကိုပိုဆိုးစေတယ်။ backend က overload ​ဖြစ်လာရင် အရေးမကြီးတဲ့ read query တွေကို drop ​ပစ်ပြီး critical write ကိုဦးစားပေးဖို့ logic ထည့်ထားပေမယ့် request တခုကို cache ကနေ serve လိုက်တာနဲ့ shed လိုက်တာနဲ့က CPU သုံးရတာချင်း သိပ်မကွာတော့ အပိုဖြစ်နေတယ်။ နောက်တချက်က 50K write throughput မှာ db layer က bottleneck ဖြစ်လာပြီး p95, p99 latency spike တွေကိုဘယ်လိုမှ ထိန်းမရဘူး။

ဒီတော့ architecture rework အနေနဲ့ event-sourcing CQRS project ကိုစခဲ့ကြတယ်။ တချိန်တည်းမှာပဲ stateful backend တွေဖြစ်လာတဲ့အတွက် smart proxy လိုလာပြီး ledger frontend ထည့်ဖို့ ဆုံးဖြတ်ခဲ့ကြတယ်။ ledger write အတွက် Go producer, ledger read အတွက် Java consumer, ledger frontend ကို Rust သုံးလိုက်တယ်။ ဒါပေမယ့် frontend layer ရဲ့ nature က short lived heap object တွေပဲများတာမို့လို့ GC cycle ကပြဿနာမဟုတ်ဘူး။ ဒါကြောင့် ရွေးမယ်ဆိုရင် GC language တွေဖြစ်တဲ့ Go ရော၊ Java ရောစဥ်းစားလို့ရနိုင်တယ်။

traffic peak မဟုတ်တဲ့အချိန်တွေမှာ 200K throughput ကိုထမ်းဖို့ frontend pod 100 အနည်းဆုံး ထားထားတယ်။ ဒီတော့ 2K rps ဆိုတာ rust proxy တလုံးအတွက် ချွေးတောင်ထွက်စရာမလိုတဲ့ ပမာဏပါ။ request တခုကို 10 microsecond လောက် CPU ပေးရတဲ့အတွက် proxy တလုံးကို 20 millicore အနည်းဆုံးလိုအပ်ပြီး head room + 3x traffic spike ကိုလိုက်နိုင်ဖို့ 100m CPU ထားတယ်။ inflight request ကတော့ partition locality အပေါ်မူတည်ပြီး ကွာသွားပေမယ့် backend ရဲ့ latency က upper bound 70 ms ထက်မပိုတဲ့အတွက် 140 concurrent request မှာ req 1 ခုကို 10 KB နှုန်းနဲ့တွက်ရင် 3x traffic peak ကိုထည့်စဥ်းစားပြီးရင်တောင်မှ proxy တလုံးအတွက် memory 15 MB ပဲလိုပါတယ်။ ဒီတော့ backend နဲ့စာရင် capacity planning လုပ်ရတာ အများကြီး ပိုလွယ်တယ်။ stateless လည်းဖြစ် resource လည်း cheap ဖြစ်တဲ့အတွက် overprovision လုပ်ထားလို့ရတယ်။

နောက်တချက်က fine grained load balancing ဖြစ်သွားတဲ့အတွက် CPU balance အများကြီး ပိုဖြစ်သွားသလို GC pause တွေမှာလည်း aggressive timeout လုပ်ပြီး retry ကောင်းကောင်းလုပ်လို့ရသွားစေတယ်။ ရလဒ်က backend overloaded အခြေအနေတွေမှာ success rate 35% တက်လာတယ်။ frontend က system အပေါ် global view ရှိတဲ့အတွက် frontend layer ကနေ client တွေကို backpressure ပေးလို့ရသွားတာကလည်း backend layer ကိုသူ့ဆီရောက်လာသမျှ request တွေရဲ့ 100% နီးပါး optimal performance နဲ့ serve လို့ရသွားစေတယ်။ ဒီတော့ ရှေ့ architecture နဲ့တုန်းက backend မှာ backpressure လုပ်ရင်းနဲ့က CPU တွေအများကြီးဖြုန်းနေရတဲ့ ပြဿနာကို ဖြေရှင်းပြီးသားဖြစ်သွားတယ်။

frontend ကရော overload မဖြစ်နိုင်ဘူးလားမေးရင် ဖြစ်နိုင်ပါတယ်။ ဒါပေမယ့် သူကဒီ subsystem ရဲ့ load absorber ဖြစ်တဲ့အတွက် 100% fail မသွားအောင် ကာထားပေးနိုင်တယ်။ ရှေ့က incident တွေကို ပြန်ကြည့်ရင်လည်း scaling မလိုက်နိုင်ခင် load absorb လုပ်ပေးမယ့် layer မရှိခဲ့ခြင်းက cascading failure တွေရဲ့ root cause ဖြစ်နေတယ်။ frontend layer ထည့်အပြီးမှာ downtime ကတပတ်ပျမ်းမျှ ၁၂ မိနစ်ကနေ ၁ မိနစ်အထိ ကျသွားတယ်။ ဒါက အရင် 99.88% ကနေ 99.99% နီးပါး ခုန်တက်လာတာ။ relative improvement ဆိုရင် downtime က 92% ကြီးများတောင် လျော့သွားတာဖြစ်တယ်။ လပေါင်းများစွာ engineering resource တွေဒီထဲနှစ်ခဲ့ရပေမယ့် ဒီတခေါက် architecture rework က မျှော်မှန်းထားတာထက် အများကြီး ပိုကောင်းခဲ့တယ်။

လက်ရှိမှာ ledger write က invariant check နဲ့ append log သီးသန့်လုပ်တဲ့ binary မို့ အတော်လေး predictable ဖြစ်တယ်။ cache management ကလွဲရင် write path မှာရှုပ်ရှုပ်ထွေးထွေး ဘာမှမရှိဘူး။ ဒါကြောင့် frontend ခံလို့ များများစားစား အကျိုးမရှိသေးဘူးလို့ ယူဆတယ်။ ဒါပေမယ့် အခုထက်ပိုပြီး resiliency ရှိဖို့လိုအပ်လာတဲ့အခါ ဒါမှမဟုတ် per-user level မှာ backpressure လုပ်ချင်တဲ့အခါ ledger write frontend နဲ့ ledger read frontend နှစ်ခုလုံး လိုအပ်လာလိမ့်မယ်။ တခုရှိတာက ledger write က saga participant မဟုတ်တဲ့အတွက် write ကရိုးရှင်းနေတာဖြစ်တယ်။ saga ထဲက service/bounded context မျိုးဆိုရင်တော့ write path မှာ streaming ပါလာတဲ့အတွက် api နဲ့ write ကိုခွဲထုတ်မလား စဥ်းစားသင့်တယ်။ ဥပမာ state store ကို query ဖို့လည်းမလိုဘူးဆိုရင် api နဲ့ write ကို binary တခုတည်းမှာထားလို့ဖြစ်ပေမယ့် stateful participant ဆိုရင်တော့ api နဲ့ write ကိုမဖြစ်မနေ ခွဲထုတ်သင့်တယ်။ နောက်ပြီး bounded context အကူးအပြောင်းမှာ repartition/aggregation လုပ်ဖို့ aggregator path လည်းလိုအပ်လာမယ်။