Nagyszámú bejövő kapcsolat fenntartása (C daemon / fork / pthread)

Fórumok

Sziasztok!

Adott néhány rendszer mikrovezérlőkkel megvalósított hálózati kommunikációval.
Van ennek egy szerveroldali (Linux) daemonja, ami várja az eszközöket, ismeri a protokollt, titkosítást, és levezényli a kapcsolatot, majd bontja azt és x idő elteltével újrakezdődik az adatcsere.

Maga a kiszolgáló alkalmazás nem különösebben nagy méretű (bőven 100KB alatti). Ez az egyes kapcsolódások során forkolja magát, vár további kapcsolódásokra, és a felépült kapcsolaton vezényli a szükséges adatcserét.

Jelen esetben nincs szükség gyorsabb reakcióidőre, mint amit ez a rendszer tud.

Kérdésem, hogy milyen lehetséges problémákba futnék bele, ha olyan rendszert szeretnék létrehozni, ahol az egyes terminálok folyamatos kapcsolatot tartanak fenn (időnként életjelet küldve), és ezen a kapcsolaton szeretnék minél inkább real-time (max. néhány másodperces csúszással) adatokat átküldeni.
- Jelent-e problémát a sok forkolt process bizonyos egyidejűleg futó kommunikáció felett (fork miatti többletmemória-igényt nem számítva)?
- Van olyan szempont, ami miatt ellenjavallt folyamatosan fenntartani a kapcsolatokat?
Elvileg alapesetben socketenként 128 egyidejű kapcsolatot kezel a kernel, ez szükség esetén növelhető.
- Érdemes-e elgondolkozni inkább thread-ek használatán?
Maximális process-ek száma alapértelmezetten 32768

Egyáltalán nyernék valamit threadekkel, vagy jórészt csak annyit, hogy egy esetleges szoftverhiba felborítja az összes kliensem futó kapcsolatát? :)

Ha elmennék ebbe az irányba (folyamatos kapcsolatok), és továbbra is fork-olnám a klienseket, azzal elkövetnék kifogásolható "bűnt" vagy "szépséghibát"?

Szeretném a véleményeteket kérni ezzel kapcsolatban.
Mindenki javaslatát előre is köszönöm.

Hozzászólások

https://en.wikipedia.org/wiki/C10k_problem

Magyarul ha sok kapcsolatot akarsz fenntartani, akkor event loop-ot kell használnod a kernel megfelelő poll-ozási megoldásával (epoll, kqueue, végső esetben select).
Javasolt valami lib (libuv pl.) használata, esetleg olyan nyelv és runtime, ami alapból szolgáltatja (Go, pl. :)).

+1
Kiegészítésként annyi, hogy az event loop elég egy szálon. Bőven elboldogul majd több ezer kapcsolattal is. Amikor bejött egy request, akkor továbbadja a worker threadeknek. Aztán worker az eredményt visszaadja az event loopnak visszaküldésre.
Valszeg elég lesz pár workert fixen elindítani, bár ha ezek várnak is valamire, akkor persze többre is szükség lesz, mint a CPU-k száma.
Egy másik jól használható lib: libevent

Én régen (8-10+ éve) írtam ilyen jellegű programot C-ben linuxra és windosra.
Akkoriban az FD_SETSIZE még csak 64 volt. :) amit írsz, az alapján most 128, meg is vagyok lepődve. :) )
A tapasztalatom az, hogy:
- a sok forkolt process-es megoldás is majdnem ugyan az, mintha threadek lennének. Talán a threadek kevesebb erőforrást emésztenek, valamint a threadok közti kommunikáció nekem magától értetődöbb volt mutexel message queue-kkel, mint IPC a fokrolt processek között. Nem tudom, hogy manapság milyen megoldás van erre, biztos sokat haladtak előre a libek ebben a témakörben.
- ez úgy működött, hogy a fő while() loop az gyakorlatilag csak a listen_socket-et figyelte select()-tel, és kapcsolat esetén indított is egy threadot, aminek átadta a socketet. Ami "nehézség", hogy a thread loopjában is futtatnod kell select()-et az egy darab socketedre, mégpedig timeouttal, hogy a message queue-vel is tudj foglalkozni, vagy pedig úgy szervezed meg a programod szerkezetét, hogy bármelyik thread tud írni, de csak írni (avagy üzenetet küldeni) bármelyik másik socketjére (ehhez praktikusan kell egy valamilyen struktúra, amit láncolva tartasz a memóriában, és írni hozzá függvényeket, amelyek ezt kezelik, és persze mutexekkel dolgoznak), viszont ekkor a bejövő adatokat csak az egyes threadok fogadják, s dolgozzák fel. Át kell gondolni a szerkezetet mindenképp, de biztos vannak erre már egyszerű megoldások, s nem neked kell mindent megírni.
- nézd meg, hogy egy gépen egyébként hány thread fut. szerintem nem lehet gond, ha párszáz, esetleg 1-2 ezer threadod fut, s ezek az idő nagy részében blokkolva vannak a select()-ben, bejövő adatra várva.
- a forkolás esetében a probléma a processe közötti kommunikáció lesz, ehhez unix socketekkel kell operálnod, ami mindenképp bonyolultabb üzenetkezelést kíván meg a programon belül, mintha threadekkel dolgoznál, s mutexelt változót/függvények intéznék a dolgokat. sőt, az biztos, hogy alssabb, s memóriazabálóbb is.
- lehet persze kombinánli is ezt, például úgy szervezni a programot, hogy egy-egy fork kiszolgál mondjuk 20-30 klienst threadekkel, bár ez a threadek/processek közötti kommunikációt nagoyn megbonyolítja, s csak akkor egyszerű, s jó megoldás, ha nincs a kliensek között kommunikáció, hanem mindössze kérdés-válasz jellegű kommunikáció történik a serverrel (egy webserver tipikusan ilyen).
- legtisztább, legszárazabb, legegyszerűbb az egészet UDP-n megoldani egyébként. egy darab socket, egy darab blokkolódó select(). Meg egy lista struktúrákból a kapcsoaltok adataival, benne a kapcsoaltok egyedi socket azonosítójával. Ha üzenet érkezik, a select() visszatér, feldolgozza az üzenetet, s nagyon könnyen tudsz a többi csatlakozott kliensnek is bármit továbbítani, még mutexelgetni sem kell. többszáz klienst egy röhögve elvisz. hátrány, hogy ki kell dolgozni valamiféle egyszerű mechanizmust, hogy detektálni tudd, ha esetleg egy csomag elveszik. na nem kell a TCP-t újra feltalálni, elég mondjuk minden fogadás után egy ack -ot küldeni, s ha a kliens nem kapott ack-ot x időn belül, megismétli a küldést. a szerver meg ha eredetileg is megkapta a csomagot, és az ack veszett el, s ezért újra kapja ugyan azt az üzenetet, könnyen tudja ezt detektálni, ha mondjuk a kapcsoalt-struktúrában elmenti az előző fogadott üzenet-blokkot egy crc-vel, s összehasonlítja minden üzenettel. nem tudom, hogy milyen programod van, nylíván nagy adatforgalmak esetében ez egy baromság, viszont modnjuk egy egyszerű chat-program, vagy egy 5 másodpercenként egy darab adatot elküldő valami esetében járható út.

egyébként kérdésedre válaszolva, elvileg nincs olyan szempont, ami miatt ellenjavallt kapcsoaltokat folymatosan fenntartani, nem találkoztam soha semmilyen erre voantkozó megkötéssel. sőt, a TCP eleve úgy van kitalálva, hogy addig nyitott a kapcsoalt, amíg le nem zárják...

amúgy mit csinál a program? mert annyi féle megvalósítás lehetséges, hogy a megfelelőt csak a program jellegének ismeretében lehet javasolni. nincs "univerzális" "Best-socket-programming-know-how", minden esetre más-más mód a megfelelő.
meg mennyi kliensre kell számítani, mert más 100-200 kliens, más 1-2 ezer, meg más 10-20 ezer esetében is a megfelelő megoldás..
nekem a 200+ nagyságrendben van tapasztalatom, addig tudok segíteni, többtíz-ezer kliens még soha sem csatlakozott egyidőben a régen készült szerverprogramjaimhoz. :)

szerk.:
UDP vonalon érdemes ezeken elgondolkozni:
http://enet.bespin.org
http://udt.sourceforge.net
ez egy szabvány, s a linuxban implemetálva is van: http://www.ietf.org/rfc/rfc4340.txt http://www.linuxfoundation.org/collaborate/workgroups/networking/

Köszönöm a válaszod.

A konkrét feladat még csak körvonalazódik, de néhány 100 byte, és legfeljebb néhány tíz kilobájt közötti forgalom valószínű, és a kliensek elsősorban nem egymással kommunikálnának (esetleg némi osztott memóriaterületen a fő folyamattal), hanem főként SQL-be mentenének, ill. időnként SQL-ből vehetnének át adatokat.

Nagyságrendileg néhány száz kapcsolatban gondolkoznék első nekifutásra, és jó volna továbbra is TCP socketen kommunikálni.

akkor a fork fele nézelődj, azzal lesz a legegyszerűbb dolgod.
ha nem kommunikálnak egymással a kliensek, akkor ez lesz a legegyszerűbb. nem kell mutexelni, nem kell threadokat szinkronizálgatni, stb.stb.
a fork()-olt processek között meg named pipe a fő folyamattal, arra pont elég, hogy a processek a fő folyamattal minimálisan kommunikáljanak.
a néhány száz kapcsolathoz szintén azt mondom, hogy bőven elég a fok(), sokkal egyszerűbb, mint pthread -dal "baszódni", indítgatni, leállítani, szinkronizálni, mutexelni, stb.stb.
néhány száz kapcsolatnál semmi hátrányát nem fogok tapasztalni a frok() -nak, s ezt a legegyszerűbb programozni.

A thread is a saját PID-jével fut és adminisztráció sem szükséges. Létrehozod a threadet és ennyi, akár szabadon is engedheted teljesen. Ami előnye van a fork-nak, az kb annyi ami a hátránya is. Nincs ráhatása a parent-re a child-nek

// Happy debugging, suckers
#define true (rand() > 10)

Ha a CPU-hoz jó közel megyünk, valóban nincs dráma különbség:) De lényegében igazad van, annyi különbséggel, hogy a forkolás miatt elég sok performanciát bukhatsz el nem csak a forkolás pillanatában (több alkalommal fordulunk a kernelhez így nagyobb a context switch-ek száma), hanem a teljes memóriaterület másolásra kerül.

Most majd jön a sok okos, hogy nem is másolódik le a memóriaterület mert nem is foglal több memóriát, etc etc. Részben igazuk van, gyakorlatilag nincs.
Egy új thread születésénél új stack jön létre , a heap teljes mértékben közös a szülő threaddel (shared memory). Fork esetében a teljes stacket lemásoljuk (idő), a heap pedig COW (Copy on write)-ként van megjelölve. Innentől ha COW-al megjelelölt helyre írunk bármelyik processből, a teljes page lemásolásra kerül az MMU által (szintén idő). Ez utóbbi dolog szokott gyönyörű lagpike-okat és hirtelen egekbe szökő load-ot okozni úgy, hogy a teljes rendszert befagyasztod vele

// Happy debugging, suckers
#define true (rand() > 10)

epoll. Nem select, nem poll. epoll. Selectnél és pollnál végig kell nézned az összes file descriptort, hogy történt-e esemény -> kapcsolatok számától lineárisan függ. Epoll azokkal a file descriptorokkal tér vissza, amelyeken esemény van. Emellett képes élvezérelt működésre: írható vs írhatóvá vált.

Minden kapcsolatnál fork: erőforrásokból ki tudsz futni. Érdemesebb lehet N kapcsolatonként újraforkolni.
Emlékeim szerint amúgy újabb Linux kernelnél drámaian sok különbség nincs már a processz és a szál közt, inkább azon múlik, milyen szoros kapcsolatot szeretnél a programok közt.

Az epollt erre találták ki. Cserébe Linux only. BSD-n hasonló dolog a kqueue.
Forkra valószínűleg nem lesz szükséged. Az olyankor jó, ha:
- *nagyon* sok kapcsolat van - válaszidőn tud javítani: kevesebb fd-n kell végiggaloppoznod sok párhuzamosan beeső kérés esetén. Ha kérés ritkán jön, akkor nem lényeges
- egy kérés kiszolgálása sok idő, és CPU-intenzív
- több magod/processzorod van és szeretnéd kihasználni

Egy - szerintem - érdekes mérés a terheltség és a kapcsolatok számának, kommunikációs mintának függvényében N megválasztása.

Vannak jó library-k amik elmaszkolják neked ezeket a dolgokat, vagy éppen hasonlót valósítanak meg csak multiplatform módon.
A régi jól bevállt library erre a libevent (vagy ennek újraírt klónja a libev, ami egy teljes emulációs api-t biztosít a libevent utánzására). Ezeket már nem fejlesztik így itt ott vannak már jobb megközelítések, cserébe atomstabilak.
Illetve vannak újabbak is, elég masszív fejlesztői táborral, pl.: https://github.com/libuv/libuv

Ha én kezdenék hasonló projectet, akkor jó esélyel a libuv -t választanám most, mivel vannak benne olyan hasznos feature-ok (thread pool-s) amivel nem hogy pár száz, de egy vaskosabb gépen milliós tételben kezelheted a socketeket gond nélkül.

Ha konkrétabb kérdésed lenne megvalósításban, akkor nyugodtan kereshetsz privátban

// Happy debugging, suckers
#define true (rand() > 10)

Java esetén van a NIO és a NIO.2 (mint non-blocking IO), ami ezt támogatja, érdemes lenne megnézni (akár a JVM forrás natív Linux részét), hogy a kernel szintjén hogy van leképezve és az alapján felépíteni a Linux daemon-t, ha már ezt jól kitalálták.

Feladata válogatja, de például erre a use-case-re tökéletesen meg tud felelni a JVM, mert gond nélkül kezel több tízezer nyitott socket-et a NIO.2, egyszerűen csak az van, hogy nem ismered, illetve szokott lenni az átlag fejlesztőben valami 10-15 évvel ezelőtti berögződés a Java kapcsán.

Nade, ha mindez C nyelven kell, akkor tényleg azt javaslom, hogy nézd meg a JVM Linux natív forrását (például: http://hg.openjdk.java.net/nio/nio/hotspot/file/359445e739ac/src/os/lin…), sokat lehet tanulni belőle, nem hülyék dobták össze... :)

Néhány száz, az mai PC léptékben nem sok. Pláne ha csak időnként jön 1-1 néhány kB-os csomag (bár a frekvenciát nem írtad). Számold ki az összes hálózati sávszélesség igényt, ha ez jóval kisebb, mint egy 100-as Ethernet, akkor ezzel nem lesz probléma.

A néhány száz nyitott kapcsolat szintén smafu egy mai oprendszeren.

Számold ki a forkok memória igényét is. Pl ha 100-at indítasz, akkor minden forknak jut 10 mega, akkor jársz 1 gigánál. Az SQL kapcsolat lehet esetleg memória igényes, azt mindenképpen mérni kell.

Az általad leírt rendszerben szűk keresztmetszet egyedül az SQL oldalon léphet fel. Tapasztalatom szerint ha lassú a háttértár és/vagy bonyolultak az SQL-ek, akkor itt lehet gond. Legjobb először az adatbázist megtervezni, és a tervezett terheléssel szimuláltan írni/olvasni.

A mások által leírt sok kapcsolatot kezelő megoldások valóban hasznosak, de ahogy a C10k nevében is benne van, akkortól válnak ezek a kérdések izgalmassá, ha 10000 kapcsolat van egyszerre, és ezeken aktív forgalmazás is történik. Gondolt el, egy gigás Etherneten elvileg még mindig kb 10kB adatot el lehet küldeni minden egyes kliensnek egy másodpercen belül. Na, ez az a nagyságrend, amikor a socket kezelés módjával, meg a szálakkal, meg a multicore processzorral már nem árt számolni. Ez alatt felesleges erőfeszítés. Azzal csináld, amit a legjobban ismernek a fejlesztők.

Nem feltétlen egyfajta projektben gondolkozom, leginkább azért tettem fel a kérdést, hogy lássam, a meglévő megoldásom helyett milyen irányban érdemes majd más projekt(eke)t alapozni. A frekvencia emiatt egyelőre nem meghatározott, de azért alapvetően 100 Mbit-es interfésznek a töredékét sem használná ki.
Az SQL része ezen az oldalon nem valószínű, hogy különösebben bonyolulttá válna, legfeljebb frontend részen várható komolyabb lekérdezés.

Érdekes hozzászólások születtek a témában, és van néhány technológia, amivel hasznos lehet megismerkedni.

Off:
Elvileg alapesetben socketenként 128 egyidejű kapcsolatot kezel a kernel, ez szükség esetén növelhető.

Bocsi az off-ért, ezt a részt elmagyarázná valaki? Én is tákoltam már pár szerver-programot, de nem láttam ezt a korlátozást.

Ja, hogy FD_SETSIZE. Arra vonatkozóan jó hírem van: az senkit nem kötelez semmire, akármekkora bitvektort használhatunk; méréseim szerint, ha kompatibilisek akarunk maradni a FD_SET/CLR/ISSET makrókkal, akkor ilyesmit tákolhatunk (fejből írom, tesztelni kellene):


size_t longbits= 8*sizeof (long);
size_t vectbits= (maxdb + longbits - 1) / longbits * longbits;
size_t vectbytes= vectbits/8;
long *bitvector= calloc (vectbytes, 1);
FD_SET (sock, (fd_set *)bitvector);

Kiegészítő olvasmány: What is the maximum numeric value for a socket, and what is the maximum number of sockets a Windows program can create?

Ha jol tudom a select() annyiban jobb, hogy minden tipusu filedescriptorral megy amig pl. a poll() es az epoll() nem tud figyelni pl. fileokra. Ez bizonyos esetekben erdekes lehet. Tovabba a select() a leguniverzalisabban elerheto (BSD-n ha jol tudom nincs epoll(), ott kqueue() van). Jo tudnod, hogy bizonyos beagyazott platformokon az epoll nem tamogatott, vagy rosszul mukodik. A select()-et valoban kevesbe skalazhatonak tartjak, de altalaban az az okolszabaly, hogy csak akkor kell mason gondolkodni, ha 10k-nal tobb fd-t akarsz kezelni.

En a helyedben libevent-et (C) vagy asio-t (C++) hasznalnek. Egyfelol mert elrejti az alatta levo API-t, tehat a programod ugyanugy fog kinezni akkor is ha select() ketyeg alatta, es ugyanugy ha epoll() es minden platformon menni fog. Masfelol mert kapsz meg egy halom egyeb nagyon hasznos funkcionalitast.

libevent: http://www.wangafu.net/~nickm/libevent-book/
asio: http://think-async.com/Asio/asio-1.11.0/doc/

Itt nyilvan a szokasos dolgok adodnak: az asio egy brutalisan flexibilis framework (privat velemenyem szerint a legjobban megirt c++ konyvtar), de mivel c++ template-eken alapul ezert a binarisod nagyobb lesz (ha parmegas vegso binaris zavar, akkor inkabb ne ezt valaszd). A libeventel kisebbek lesznek a binarisok, de maga a konyvtar egy kicsit fapadosabb API-t mutat.

Napi szinten hasznaljuk mindkettot a tiedhez nagyon hasonlo problemakra, szoval egyikkel sem fogod megegetni magad.

minden tipusu filedescriptorral megy

ez konkretan nem igaz. A poll(), meg a tobbi ugyanugy int tipusu descriptor leirot fogad a set-be, mint a select().

A select()-et valoban kevesbe skalazhatonak tartjak, de altalaban az az okolszabaly, hogy csak akkor kell mason gondolkodni, ha 10k-nal tobb fd-t akarsz kezelni.

ez is fals: 9999 kapcsolatot sem fogsz tudni lekezelni select()-tel (hint: 1024-es limit)

Egyfelol mert elrejti az alatta levo API-t,

ez altalaban jo, de nekem egy spec. feladat miatt (starttls implementalasa) buko volt a libevent, igy a select()-rol epoll()-ra valtoban vagyok. Meg a kqueue-val kene megbaratkozni, hogy a bsd-s nepek se utaljanak meg.

a man 2 select szerint van:

"POSIX allows an implementation to define an upper limit, advertised via the constant FD_SETSIZE, on the range of file descriptors that can be specified in a file descriptor set. The Linux kernel imposes no fixed limit, but the glibc implementation makes fd_set a fixed-size type, with FD_SETSIZE defined as 1024, and the FD_*() macros operating according to that limit. To monitor file descriptors greater than 1023, use poll(2) instead."

Bar ez egy elmeleti dilemma csak, mert aki ~200+ connectiont select()-tel akar kezelni, az boldog ember nem lehet...

On:
Nahát, ezt a részt nem is olvastam. Vagy olvastam, de simán figyelmen kívül hagytam. Elnézést kérek. Különösen akkor, ha van olyan rendszer, ahol ez valóban korlát. My fault.

Off:
Eddig is sejtettem, hogy a szabványokat nem feltétlenül a szakma elitje készíti... Talán nem jutott át a tudatukat védő pajzson, hogy a FD_SETSIZE-ot tartalmazó header megírása, a program fordítása, és a program futtatása három különböző esemény, amely három különböző gépen történik, három különböző időpontban.

ez nem feltetlen szabvany kerdese. A select mar regota velunk van, es amikor letrehoztak, aligha gondoltak arra, hogy a hivatkozott valtozo erteke majd keves lesz (vo. 640 kB memoria...).

Hanem arra probaltam utalni, hogy meg ha az adott platformon a valtozo erteke akar 10k-ra is beloheto (http://www.kegel.com/c10k.html#nb.select), ez nem jelenti azt, hogy jo otlet 10k kapcsolatot select-tel feldolgozni. Bar a poll-nal irja, hogy "it does get slow about a few thousand, since most of the file descriptors are idle at any one time, and scanning through thousands of file descriptors takes time.", de a select-re is eppugy igaz, mert annal is a teljes fd set-en vegig kell szaladnod egy vegtelen ciklusban...

@file fd: Nem azt mondtam, hogy mas parametert fogad, hanem hogyha egy megnyitott file-t probalsz figyelni, az epoll() (es lehet, hogy a poll() is, ezt nem tudom biztosan), hibaval (EPERM talan??) jon vissza a hivas... A select() pedig mukodni fog, ahogy varnad (probald ki!). A poll() ebbol a szempontbol duplan erdekes, mert vannak olyan (regi) poll() implementaciok, amik siman a select()-et hasznaljak belul).

@limit: ez igaz, ha a limit fole akarsz menni, akkor struktualtan kell megoldanod (ami tobb szalat es valszeg eleg sok szivast jelent)...

@libevent API: pontosan mi az a limit amibe utkoztel StartTLS-libevent eseten? Filtering bufferevent nem segit? Altalaban az a 'hivatalos' javaslat, ha valamilyen encryption-t akarsz libeventben faragni. Asio-ban csinaltunk mar hasonlot, szerintem libevent-ben is ment volna a dolog. Ha neked erdekes a hordozhatosag BSD-Linux viszonylatban, akkor szerintem eleg sokat er az, amit a libevent/asio tud adni.

Afelol szerintem nincs vita, hogy az esetek nagy reszeben az epoll()/kqueue() a trendi polling framework. Azert van 1-2 trukkos eset, amikor visszabb kell lepni.

hibaval (EPERM talan??) jon vissza a hivas...

utananeztem, mar elhiszem :-)

pontosan mi az a limit amibe utkoztel StartTLS-libevent eseten?

starttls-nel kripto nelkul indul el a socket, aztan az smtp chat egy fazisaban valt at kriptora. Na ezt a valtast nem sikerult osszehozni libevent-tel. Turtam utana a netet, es megtalaltam, hogy masok is szaladtak ebbe bele, de megoldas nem lett ra. En is egyetertek, hogy a libevent hordozhatosaga nagy kiralysag, de az emlitett problema sajnos showstopper volt az esetemben.