Lihtsa võrgukoormuse jagaja ehitamine
Lihtne load balaning juhuks kui soovitakse koormust mitme erineva internetiühenduse vahel jagada
Näites üritatakse koormust jagada KOLME erineva pakkuja välisühenduse vahel
Koormuse jagamine kolme liini vahel (load balancing), mille puhul üks ühendus liigub küll ainult ühte liini mööda, kuid erinevad ühendused saavad kasutada erinevaid liine. On muidugi ilmselge, et niisuguse koormuse jagamise korral peavad tarbijad nägema internetti läbi NAT'i, samuti peab ilmselt eeldama, et sa ise mitte midagi ei serveeri.
Ladna, meil on ilmselt olemas mingi purk, mida me hakkame kasutama oma interneti lüüsina. Et oleks ilus, võiks meil selle purgi küljes olla neli võrguinterfeissi - üks iga liini jaoks pluss üks sisevõrku, kuid paremal puudumisel saab ka kahega hakkama - DSLid peaks siis sellisel juhul ühenduma hubi või switchi kaudu. Purgiks sobib suvaline arvutav seade, mis on võimeline mõnd valge inimese OS'i jooksutama - meie kasutame oma näidetes Linuxit. Kui klassikaline pessukas liiga suur või mürarikas tundub, võib seda tööd vabalt teha ka OpenWRT'd jooksutav WRT54G.
Kahjuks on aga nõnda, et tavaline linuxi tuum ei sisalda ühtegi antud olukorras kasutamiseks ühtegi mõistlikku netfiltri moodulit load balancingu tegemiseks. Võib vabalt olla, et mõnede distributsioonide tuumad sisaldavad juba neid mooduleid, vastasel juhul on vajalik oma tuuma patchida. Netfiltri projekt üllitab perioodiliselt patch-o-matic nimelisi pakke, tasub sikutada nendest viimane ning lasta tuuma vähemalt allpool kasutatud "random" patch, kuid huvi võiks pakkuda ka "nth" patch. Kuidas tuuma paigatakse, peab igaüks ise teadma, sest see väljub pisut antud teemast. Võib-olla saab abi Linuxi foorumist, kuid ma isiklikult soovitan google abil juhendeid otsida. Igaljuhul me eeldame, et vähemalt "random" patch on tuumas sees.
Oleme oma DSLi modemid ära ühendanud. Lihtsuse huvides eeldame, et kõik DSLid on staatilise IP aadressiga ning ei vaja meie poolset PPPoE seadistust. Meil on kolm subneti. tavaliselt on neex x.x.x.x/30, kus on ruumi meie masina aadressi ning default gw (mis on siis ISP poolne) aadressi jaoks. Enne tegema hakkamist veendume, kas meie süsteemis on olemas utiliidid ip (reeglina pakis iproute või iproute2) ning iptables (reeglina pakis iptables). Juhul kui ei ole, tuleb need utiliidid paigaldada.
Midagi, seadistame oma interfeisid. Meie näites olgu eth0 sisevõrgu jaoks ning eth1 - eth3 olgu meie DSLidele. Oletame, et sisevõrgu subnet on 192.168.2.0/24, oletame, et eth1 küljes olev subnet on 42.42.1.0/30, kus 42.42.1.1 on meile antud aadress ning 42.42.1.2 on default gw selle liini jaoks, oletame, et eth1 küljes olev subnet on 42.42.2.0/30, kus 42.42.2.1 on meile antud aadress ning 42.42.2.2 on default gw selle liini jaoks, oletame, et eth1 küljes olev subnet on 42.42.3.0/30, kus 42.42.3.1 on meile antud aadress ning 42.42.3.2 on default gw selle liini jaoks, oletame, et sisevõrgus on meie masin aadressiga 192.168.2.254. Kuidas iganes me neid interfeise ka ei seadista (see sõltub kasutatavast distributsioonist), oleks tulemus siis selline:
purk ~ # ip addr show dev eth0 10: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000 link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff inet 192.168.2.254/24 brd 192.168.2.255 scope global eth0
purk ~ # ip addr show dev eth1 11: eth1: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000 link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff inet 42.42.1.1/30 brd 42.42.1.3 scope global eth1
purk ~ # ip addr show dev eth2 12: eth2: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000 link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff inet 42.42.2.1/30 brd 42.42.2.3 scope global eth1
purk ~ # ip addr show dev eth3 13: eth3: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000 link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff inet 42.42.3.1/30 brd 42.42.3.3 scope global eth1
Igaks juhuks vaatame ka routingu tabelit. See võiks välja näha selline:
purk ~ # ip route 192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.254 42.42.1.0/30 dev eth1 proto kernel scope link src 42.42.1.1 42.42.2.0/30 dev eth2 proto kernel scope link src 42.42.2.1 42.42.3.0/30 dev eth3 proto kernel scope link src 42.42.3.1 127.0.0.0/8 via 127.0.0.1 dev lo scope link
Et aga igas enesest lugupidavas routingu tabelis võiks olla default route, siis valime mõne meie liinidest - näiteks kõige kiirema - ning lisame oma routingu tabelisse default route, näiteks: ip route add default via 42.42.3.2. Kindlasti tuleb meeles pidada, et ühes routingu tabelis saab olla ainult üks default route!!!
Olles interfeisid ära seadistanud, kontrollime, kas kõik meile antud default gatewayde aadressid on pingitavad. Juhul kui on, rüüpame lonksu kohvi, beer_yum.gift, veini, mahla.
Edasi seisame me probleemi ees. Meile on antud kolm default gatewway aadressi, kuid me teame, et igas rotingu tabelis saab olla ainult üks default gateway. Mida teha? Õnneks saab Linuxis olla rohkem kui üks routingu tabel. Üldiselt tunneb Linux routingu tabeleid numbrite järgi, kuid kuna tegemist on ikkagi inimestele mõeldud süsteemiga, on võimalik nendele tabelitele ka nimesid panna. Uurime faili /etc/iproute2/rt_tables. See võiks välja näha umbes selline:
purk ~ # cat /etc/iproute2/rt_tables
- reserved values
255 local 254 main 253 default 0 unspec
- local
- 1 inr.ruhep
Number rea alguses märgib routingu tabeli numbrit, järgnev tekst selle nime, kõik, mis järgneb # sümbolile, on kommentaar. Trükkides käsureale ip route, näeme me "main" tabeli sisu. See on routingu tabel, mida vaikimisi kasutatakse, vt. üles. Jätame ta esialgu rahule, meil on vaja paika sättida oma kolm default gatewayd. Teeme juurde kolm routingu tabelit, saagu nende numbriteks 10, 20 ja 30 ning nimedeks toru1, toru2 ja toru3. Selleks lisame faili /etc/iproute2/rt_tables järgmised read: 10 toru1 20 toru2 30 toru3 Loomulikult on need tabelid tühjad, kirjutades käsureale näiteks ip route show table toru3, ei väljastata meile mitte midagi, kuid ei viriseta kaa - tabel on olemas, kuid ei sisalda ühtegi kirjet. Mis seal siis ikka, täidame need tabelid, looma igasse routingu tabelisse ühe kirje - default route: ip route add default via 42.42.1.2 table toru1 ip route add default via 42.42.2.2 table toru2 ip route add default via 42.42.3.2 table toru3
Vaatame, kas tuli välja, näiteks sedasi:
purk ~ # ip route show table toru3 default via 42.42.3.2 dev eth3
Nõndaks. Lonks beer_yum.gift ning jätkame. Meil on terve hulk routingu tabeleid, igaüks oskab nendest kasutada mingit default gatewayd. Me peame kirjeldama mingid reeglid, mille alusel hakkab süsteem neid tabeleid pruukima. Et oleks lõbusam, oletame, et meie DSL liinid on erineva läbilaskevõimega, mistõttu me tahame, et neid läbiks erinev hulk liiklust. Olgu meil näiteks soov, et liiklus jaguneks liinide vahel järgmiselt: 20%, 30%, 50%. Me peame aru saama, et me ei saa suvaliselt igat n'ndat paketti saata ühe, teise või kolmanda liini peale. Peab olema selge, et kui ühenduse loomisel otsustati, et ühendus peab väljuma näiteks teist liini mööda, siis peavad kõik selle ühenduse juurde kuuluvad paketid seda teed käima - vastasel juhul ei saa välismaailmas olevad serverid mitte mütsigi aru, kes kellega siis lõpuks suhelda tahab. Et paketi kuuluvust ühenduse juurde saab üldiselt üheselt kindlaks teha vaid statefull protokollide korral, näiteks TCP, siis peavad kõik stateless ühendused käima "normaalset" teed mööda, meie näites siis põhiroutingutabeli järgi. Selleks, et süsteem oskaks vahet teha, millise ühendusega on tegemist, peame me oma ühendusi kuidagi märgistama. Linuxis on selleks connmark. Connmark on selline "virtuaalne" märk, mida kannavad kõik vastavalt märgitud ühenduse juurde kuuluvad paketid, st. see märk eksisteerib _ainult_ tuuma sisemuses olevates tabelites, mitte pakettide endi payloadis (ehk siis, mitte segamini ajada TOS'iga).
Kirjutame järgmised reeglid (eeldame siinkohal, et netfiltri kõik tabelid on tühjad):
- iptables -t mangle -A PREROUTING -o !eth0 -p tcp -m state --state NEW -m random --average 20 -j MARK --set-mark 10
- iptables -t mangle -A PREROUTING -o !eth0 -p tcp -m state --state NEW -m random --average 30 -j MARK --set-mark 20
- iptables -t mangle -A PREROUTING -o !eth0 -p tcp -m state --state NEW -m random --average 50 -j MARK --set-mark 30
Nende reeglitega märgistame me keskmiselt 20% uutest ühendustest märgiga 10, 30% uutest ühendustest märgiga 20 ning 50% uutest ühendustest märgiga 30. Kindlasti tuleb märgistada ainult uusi ühendusi, sest märgistus on ühenduse põhine ning me ei taha, et ühenduse kestel keegi märgi üle kirjutaks. Samuti ei taha me märgistada ühendusi, mis luuakse meie purgist sisevõrgu poole, selleks kasutame välistavat tingimust -o !eth0 (loogiline, eks). Edasi peame me kuidagi süsteemile ütlema, et erineva märgistusega pakettide korral tuleks kasutada erinevaid routingu tabeleid. Teeme järgmised reeglid:
- ip rule add fwmark 10 table toru1
- ip rule add fwmark 20 table toru2
- ip rule add fwmark 30 table toru3
Nende ridade tähendus peaks olema selge - me seome erinevad märgistused erinevate routingu tabelitega ning ütleme seega süsteemile, et kõiki pakette, mis kannavad märgistust 10, tuleb routida routingu tabeli toru1 abil, kõiki pakette, mis kannavad märgistust 20, toru2 abil ning märgistusega 30 pakette suunatakse tabelis toru3 sisalduva järgi. Alati, kui me oleme routingu reeglites midagi muutnud, tuleb anda käsk: # ip route flush cache Nimelt Linux, selleks, et ei peaks iga paketi peale routingu tabeleid läbi vaatama, ehitab omale sellise vahva route cache. On selge, et routingu reeglite muutmisel ei ole info cache'is enam korrektne, mistõttu tuleb sellele vesi peale tõmmata. Juhul kui seda juba tehtud ei ole, siis on nüüd paras aeg ümber lülitada paar sysctl'i: Lubame oma purgil pakette forwardida: # sysctl -w net.ipv4.ip_forward=1 Keelame reverse path filtreerimise: # sysctl -w net.ipv4.conf.all.rp_filter=0
Mis siis sai... Meie süsteem omab hulka routingu tabeleid, oskab teatud tingimuste alusel pakette märgistada, oskab märgistuste alusel kasutada erinevaid routingu tabeleid. Jääb üle vaid ehitada NAT'i reeglid. Kirjutame järgmiselt:
- iptables -t nat -A POSTROUTING -o eth1 -j SNAT --to-source 42.42.1.1
- iptables -t nat -A POSTROUTING -o eth2 -j SNAT --to-source 42.42.2.1
- iptables -t nat -A POSTROUTING -o eth3 -j SNAT --to-source 42.42.3.1
Ehk siis, pakettidel, mis tahavad väljuda eth1 kaudu (vastav otsus tehti routingu tabeli järgi enne POSTROUTING'usse jõudmist), muudetakse source aadressiks eth1 aadress, mis tahavad väljuda eth2 kaudu, eth2 aadress ning samuti ka eth3 puhul.
Võikski kõik olla. Kindlasti tuleb meie setupi testida - näiteks istutades eth1-eth3 otsa tcpdumbi, tehes sisevõrgust ühendusi ning uurides, millist teed mööda need paketid siis lähevad. Debugimiseks võib netfiltri ahelatesse enne igat rida panna logivad reeglid, mille abil on siis võimalik näha, mis meie pakettidega juhtus. Kuidagi tuleb ilmselt kirjutada skriptid, mis need seadistused süsteemi startupi ajal ära teevad, võib-olla ka miski tulemüüri funktsioon juurde ehitada... aga see on juba ilmselgelt teine teema.
Ahjaa, küsite, et mis saab siis nendest pakettidest, mida mangle tabelis ei märgistatud ning mis saab juhul kui random/average jättis mõne paketi märkimata? Well.. ei saa suurt midagi - need paketid rouditakse "main" tabeli alusel (vt. main tabeli default gw.) ning natitakse vastavalt.
Muide, kellele random ei meeldi ning tahab õiget round-robin load balancerit, peaks uurima "nth" moodulit, mis oskab matchida n'indale paketile X hulgast. Selle abil peaks pisukese kujutlusvõime korral round-robini realiseerimine üsna lihtne olema.
Küsite, miks nii keeruliselt, kui ometi ütleb iptables'i manual SNAT'i kohta, et: You can add several --to-source options. If you specify more than one source address, either via an address range or multiple --to-source options, a simple round-robin (one after another in cycle) takes place between these adresses. Jah, see on õige, kuid SNAT esineb ainult POSTROUTING ahelas, kuid selleks ajaks kui pakett jõuab POSTROUTING'usse, on routing decision juba tehtud ning seda enam muuta ei saa. Seega peame me oma load balanceri paigutama routing decisionist etepoole.
P.S. Kõik ülaltoodud näited on üsna sünteetilised. Mul puudub hetkel võimalus(tegelikult küll viitsimine) kirjeldatud olukorra läbi mängimiseks vajaliku testkeskkonna püsti püsti panekuks, mistõttu andestage võimalikud ebatäpsused.