eBPF-powered mesh ဘယ်လိုအလုပ်လုပ်လဲ

Resilient Networking မှာ network က microservice တွေအတွက် အားနည်းချက်တခုဖြစ်နေတဲ့အကြောင်းနဲ့ microservice တွေကို ပိုပြီး resilient ​ဖြစ်စေဖို့ဆိုရင် မဖြစ်မနေ ထည့်တည်ဆောက်ရမယ့် concept တွေရှိကြောင်း ပြောခဲ့ကြတယ်။ အစပိုင်းမှာ library-based mesh တွေကိုသုံးခဲ့ကြပေမယ့် တဖြေးဖြေးနဲ့ sidecar-based တွေက စံတခုဖြစ်လာခဲ့တာကိုလည်း ရှင်းပြခဲ့တယ်။ library အခြေပြု mesh တွေမှာ resource ဘယ်လောက်သုံးနေတယ်ဆိုတာမျိုး သိရဖို့ခက်ပေမယ့် sidecar ပုံစံမှာတော့ mesh ကသီးသန့် application တခုဖြစ်တဲ့အတွက် resource usage ကိုရှင်းရှင်းလင်းလင်း မြင်နိုင်တယ်။ နောက်တချက်က sidecar တွေက language independent ဖြစ်တဲ့အတွက် application က runtime penalty များမယ့် language မျိုး (ဥပမာ Java JVM, JavaScript စသဖြင့်) ဖြစ်နေခဲ့မယ်ဆိုရင်တောင် sidecar အတွက် C++, Rust, Zig စသဖြင့်သုံးပြီး ရေးလို့ရနိုင်တဲ့အတွက် resource usage ကို efficiency အကောင်းဆုံး ဖြစ်စေမယ်။ ဒါပေမယ့် sidecar တွေက အားနည်းချက်လုံးဝမရှိပဲ perfect ဖြစ်နေတာတော့ မဟုတ်ပါဘူး။

client နဲ့ server ကြားက TCP connection တကြောင်းစီတိုင်းမှာ sidecar ရဲ့ proxy hop ကနေ overhead ရှိလာမှာကို ရှောင်လို့မရနိုင်ဘူး။ ဥပမာ TCP connection state ကို application container မှာရော၊ proxy container မှာပါထိန်းရဖို့ ဖြစ်လာမယ်။ kernel မှာ buffer ၂ စုံစာလိုမယ်။ request ကို proxy မှာတကြိမ်၊ application မှာတကြိမ် ၂ ကြိမ် marshaling လုပ်ရမယ်။ network stack နှစ်ခုဖြတ်ရမယ်။ TCP backpressure အပြင် application နဲ့ proxy ကြားမှာလည်း လိုအပ်ရင် packet reassembly ပြန်လုပ်ရမယ် စသဖြင့်ပေါ့။

benchmark data ကိုကြည့်လိုက်ရင် sidecar proxy ကအလကား မရဘူးဆိုတာ သဘောပေါက်သွားလိမ့်မယ်။ 200 rps ရှိတဲ့ application တခုရဲ့ tail latency benchmark ဆိုပါတော့။ 20 ms ဝန်းကျင်ရှိတဲ့ request တခုက linkerd mesh နဲ့ဆိုရင် 90 ms (overhead 70 ms) လောက်ပါလာပြီး Istio နဲ့ဆိုရင် 220 ms (overhead 200) ဆယ်ဆအထိ တက်လာတာကို တွေ့ရတယ်။ ကိုယ့် application က cluster ထဲမှာ service ဘယ်နှစ်ခုကို span ရသလဲနဲ့ endpoint တခုစီရဲ့ routing rule တွေဘယ်လောက်ထိ ရှုပ်ထွေးလဲဆိုတဲ့အပေါ် မူတည်ပြီး benchmark က variable ဖြစ်ဦးမှာမို့ ဒါက အတော်လေး ယေဘုယျဆန်တဲ့ data ဖြစ်ပေမယ့် request တခုတိုင်းအတွက် ဒီလို latency tax ပေးနေရတယ်ဆိုတဲ့အချက်ကိုတော့ သိထားဖို့ လိုပါတယ်။

တဖက်မှာလည်း sidecar-based mesh တွေရဲ့ resource usage ကို optimize လုပ်ဖို့ daemon-based mesh တွေဆီတကျော့ပြန်လည်ဖို့ ကြိုးစားလာတာကိုတွေ့ရမယ်။ သဘောက sidecar တွေကို pod တလုံးချင်းစီမှာ မထည့်တော့ပဲ node တလုံးချင်းစီမှာ ထားဖို့ရည်ရွယ်တဲ့ ဒီဇိုင်းမျိုးဖြစ်တယ်။ ဒီအချက်က resource consumption ကိုသိသိသာသာ လျှော့ချပေးနိုင်ပေမယ့် security boundary က pod-level မှာမဟုတ်တော့ပဲ node-level ကိုရောက်သွားမယ့် trade off ရှိနေပြန်တယ်။

လာမယ့်ဆယ်စုနှစ်မှာ ဒီအတွက်အဖြေက eBPF-powered mesh တွေဖြစ်လာမယ်လို့ ယူဆရတယ်။ eBPF ဆိုတာ kernel module တွေရေးဖို့မလိုပဲ kernel space ထဲ code တွေ run လို့ရစေမယ့် kernel API ပဲ။ သူ့အလုပ်လုပ်ပုံက trigger-based မို့လို့ မျက်လုံးထဲ မြင်အောင်ပြောရရင် Amazon ပေါ်က Lambda တွေနဲ့တူတယ်။ ဥပမာ process လုပ်စရာ packet တွေရောက်လာရင် အသိပေးပေးပါဆိုတာမျိုး register လုပ်ထားရင် NIC ကို packet တွေရောက်လာတဲ့အခါ network driver က eBPF program ကိုအရင်လှမ်းအသိပေးမယ်။ အဲဒီ eBPF က iptable ကလုပ်နေကျကိစ္စတွေဖြစ်တဲ့ packet တွေ filter လုပ်တာ တခြား NIC တခုကို forward တာ packet တွေကို drop ​ပစ်တာတွေအပြင် packet တွေကို ဖတ်တာ ပြင်တာ အသစ်ဆောက်တာ စသဖြင့် အကုန်လုပ်လို့ရတယ်။ တခုမေးစရာရှိတာက packet စစ်ဖို့ program က kernel ထဲ run လို့ရမှ kernel networking stack ထဲဝင်ထိုင်လို့ရမှာပေါ့လို့ဆိုရင် ဘာလို့ kernel module တွေမသုံးလဲ။ ဘာလို့ eBPF လဲလို့ မေးစရာရှိတယ်။

တချက်က portability ကြောင့်။ kernel module တွေက kernel version တွေ၊ configuration တွေနဲ့ tight coupling ဖြစ်နေတဲ့အတွက် eBPF လောက် port ဖို့မလွယ်ဘူး။ တဖက်မှာ eBPF API က backward compatibility ရော၊ (old kernel တွေသုံးထားတာကလွဲရင်) cross-distro portability ကိုပါအာမခံတယ်။ ဒုတိယအချက်က safety ပိုင်း။ kernel space ထဲ program run ရတယ်ဆိုတာ တကယ်တော့ သိပ်အန္တရာယ်များတဲ့ကိစ္စ။ kernel module တွေက kernel လုပ်လို့ရသမျှ သူအကုန်လုပ်လို့ရတယ်။ arbitrary C code တွေပဲမို့ bug တွေအကုန်ပေါ်အောင် static analysis လုပ်ဖို့လည်းခက်တယ်။ ဒီတော့ bug တခုက pod တွေတန်းစီတင်ထားတဲ့ node တလုံးလုံးကို ဆွဲချသွားနိုင်တယ်။ တဖက်မှာကျ eBPF က constraint တွေအများကြီးရှိတယ်။

Web Developer တယောက် မျက်လုံးထဲမြင်အောင်ပြောရရင် WASM လိုသဘောမျိုး။ OS feature တွေသုံးလို့မရမဟုတ်ဘူး ရတယ်။ ဒါပေမယ့် ပေးထားတဲ့ API နဲ့ကန့်သတ်ချက်ဘောင်ထဲကပဲပေါ့။ ဥပမာ eBPF က unbounded loop တွေမသုံးရဘူး။ stack ပေါ်မှာ local variable သိမ်းဖို့ 512 bytes ပဲခွင့်ပြုတယ်။ random memory access လည်းမရဘူး။ ဒါပေမယ့် 512 bytes ထက်ပိုသိမ်းချင်ရင် KV ပုံစံ eBPF map ဆိုတာရှိတယ်။ key နောက်မှာ ကြိုက်တဲ့ data structure ကိုသိမ်းလို့ရမယ်။ ဒါပေမယ့် ဘယ်နားသိမ်းထားလဲဆိုတဲ့ physical memory address ရော virtual memory address ကိုပါမသိရဘူးပေါ့။ ဆိုတော့ eBPF program တခုက safe ဖြစ်တယ် မဖြစ်ဘူး kernel ကကြိုစစ်လို့ရတယ်။

mesh ဖက်ကို ပြန်ဆက်ရရင် Kubernetes ပေါ်မှာ eBPF က sidecar တွေရဲ့ overhead ကိုအများကြီး လျှော့ချပေးနိုင်တယ်။ Kubernetes ရဲ့ networking model က CNI plugin သုံးပြီး pod တလုံးအတွက် network namespace တခုဆောက်တယ်။ namespace တခုကိုတော့ host ပေါ်မှာ veth အတွဲတခုနဲ့ ကိုယ်စားပြုတယ်။ veth ဆိုတာက တကယ့် physical NIC တခုလို့ပဲ kernel က pretending game ဆော့ပြီး virtual interface တခုဆောက်လိုက်တာပါပဲ။ အဲဒီ veth အတွဲက memory ပေါ်မှာပဲရှိတယ်။

host ရဲ့ NIC ကို packet တွေရောက်လာတဲ့အခါ NIC driver က L2 ethernet frame တွေကို metadata ထည့်ထပ်ပြီး xdp_md struct တွေအဖြစ်ဆောက်၊ ePBF XDP ပေါ်က program ကိုပေးလိုက်တယ်။ eBPF program က XDP action တွေထဲက XDP_REDIRECT နဲ့ NIC driver ကိုပြန်ခေါ်ပေးလိုက်တယ်။ ပုံမှန် XDP_PASS ဆိုရင်တော့ NIC driver က xdp_md struct တွေကို sk_buff struct ချိန်းပြီး kernel networking stack ကို checksum တွက်တာ၊ reordering လုပ်တာ၊ iptable ကြည့်ပြီး routing decision ချတာစသဖြင့် လုပ်စရာရှိတာ ဆက်လုပ်ဖို့ လက်ဆင့်ကမ်းလိုက်မှာ။

ဒါပေမယ့် XDP_REDIRECT ကြောင့် NIC driver က xdp_md တွေကို sk_buff ချိန်းပြီး pod ရဲ့ veth ထဲကို packet တွေ forward လုပ်ဖို့ dev_queue_xmit(skb) ကိုခေါ်လိုက်တယ်။ ဒါပေမယ့် အဲဒီ veth က memory ပေါ်မှာပဲရှိတာဖြစ်လို့ forward လုပ်တယ်ဆိုတာ sk_buff တွေကို memory ပေါ်ဟိုကနေ ဒီရွှေ့လိုက်တဲ့သဘောပဲ။ dev_queue_xmit(skb) ရဲ့ရလဒ်က veth ကို handle လုပ်မယ့် veth driver က pod ရဲ့ eth0 ဆီကို skb တွေ queue ပေးလိုက်တယ်။ eth0 ဆီရောက်လာတဲ့ အဲဒီ skb struct တွေကို kernel က byte stream ဆောက်ပြီး application ကနားထောင်နေတဲ့ socket (ဥပမာ port 8080) ကိုပို့ပေးလိုက်တယ်။ ဒီ process အစအဆုံးမှာ iptable မဖြတ်လိုက်ရပါဘူး။

ဒီတော့ eBPF ကခုလောက် performance ကောင်းတယ်ဆိုရင် ဘာလို့ eBPF-based mesh တွေမလုပ်ပဲ sidecar-based mesh တွေကိုပဲ ဆက်သုံးနေကြသေးတာလဲလို့ မေးစရာရှိတယ်။ အဖြေက eBPF က application protocol တွေကိုကောင်းကောင်း handle မလုပ်နိုင်လို့ပါပဲ။ kernel ထဲမှာ eBPF လုပ်လို့ရတဲ့ limitation ကတင်းကျပ်လွန်းနေတယ်။ 512 bytes ကန့်သတ်ချက်၊ exhaustive code path ဒါတွေကြောင့် eBPF-based mesh တခုအနေနဲ့ L7 feature တွေပေးဖို့မဖြစ်နိုင်ဘူး။ နောက်ပြီး stack ကန့်သတ်ချက်နဲ့ bounded loop တွေကြောင့် TLS handshake နဲ့ SSL Termination ကို eBPF ထဲမှာပဲ လုပ်ဖို့ဆိုတာ မဖြစ်နိုင်ပါဘူး။ retry နဲ့ circuit breaking စတဲ့ stateful session သဘောပါတဲ့ feature တွေကလည်း Lambda လိုမျိုး trigger-based သွားတဲ့ eBPF နဲ့မကိုက်ပြန်ပါဘူး။ ဒီ limitation တွေက technology ကြောင့်မဟုတ်ပဲ kernel space ကိုကာကွယ်ဖို့ ထားရတဲ့ security check တွေဖြစ်လို့ အနာဂတ်မှာလည်း အများကြီး ပြောင်းလဲလာဖို့မရှိဘူး။

ဒါပေမယ့် eBPF-powered mesh တွေကို မြင်လာရဖို့တော့ အလားအလာ အများကြီး ရှိတယ်။ SSL/ TLS, authentication, authorization စတဲ့ identity feature တွေ၊ retry, circuit breaking စတဲ့ resiliency feature တွေကို sidecar ကလုပ်ပြီး L3/L4 routing ကို eBPF ကလုပ်တာမျိုးပေါ့။ လက်ရှိလည်း kernel က L3/L4 ကိုလုပ်နေတာပဲလို့ ပြောလို့ရပေမယ့် တကယ်လို့ eBPF ပါလာခဲ့ရင် sidecar ရဲ့ eth0 ကနေ veth ကိုအဖြတ်မှာပဲ kernel ရဲ့ networking stack ကို traverse လုပ်ဖို့လိုတော့မယ်။ host side ကိုရောက်ရင် eBPF က packet တွေကို နောက် veth တခုဆီ (တကယ်လို့ destination pod က host တခုတည်းမှာပဲဆိုရင်) ဒါမှမဟုတ် eth ဆီ (destination pod နဲ့ host မတူဘူးဆိုရင်) REDIRECT လုပ်လိုက်နိုင်တာမို့ request တခုအတွက် kernel traversal 2 ခါသက်သာသွားမယ့် သဘော။

mesh တွေကို အပြီးဖြုတ်ပစ်လိုက်လို့ရော ရနိုင်မလား။ အဖြေက yes and no ပါ။ Kubernetes ပေါ်မှာ destination တိုင်းက routable မဖြစ်ဘူး။ ဥပမာ service endpoint တွေက virtual ip address တွေဖြစ်တဲ့အတွက် နောက်ကွယ်က pod ရဲ့ ip ကိုသိရဖို့ kernel က iptable တွေကိုဖတ်ရပါတယ်။ ဒီ iptable တွေကို kube-proxy က Kubernetes API ကိုဖတ်ပြီး up-to-date ဖြစ်အောင် ပြင်ရေးပေးနေတာဖြစ်တယ်။ ဆိုတော့ eBPF က destination တခု routable ဖြစ်မဖြစ် သိရဖို့ဆိုရင် sidecar နဲ့ eBPF ကြားမှာ sidecar ရဲ့ L4 decision ကိုပြောပြနိုင်မယ့် contract တခုလိုလာတယ်။ ဒါပေမယ့် application တိုင်းက L4-aware မဖြစ်တဲ့အတွက် eBPF routing (fast path) ကို CNI author တွေက လုပ်ရဖို့ဖြစ်လာတယ်။ လက်ရှိမှာ Calico, Cilium စတဲ့ CNI plugin တွေက Flannel, Weave Net တို့လို pod networking လုပ်ပေးနိုင်ရုံ static binary တွေလိုမဟုတ်ပဲ L4 routing ကိုပါတာဝန်ယူလာနိုင်ကြတယ်။ L4 decision ကို kernel ထဲမှာ အပြီးချပြီး kube-proxy ကို bypass နိုင်ဖို့ အဲဒီ plugin တွေက Daemon Set သုံးပြီး eBPF Map တွေကို up-to-date ဖြစ်အောင် maintain လုပ်ပေးကြတယ်။ ဒါပေမယ့် လက်ရှိ L7 feature တွေလိုနေဆဲဖြစ်သလို routing decision ကို client နဲ့၀ေးရာကို ပို့လိုက်ရင် optimal ဖြစ်ဖို့လည်း ခက်သွားလိမ့်မယ်။