Primer de tot ens hem de fer unes preguntes sobre el que li caurà a sobre:
- Què aguantarà normalment, o sigui, quantes peticions per segon haurà de contestar?
- Quantes peticions de mitjana?
- Quantes peticions rebrà a l'hora punta?
- Hi haurà algun cas excepcional (però que igualment s'haurà de cobrir) a on les peticions per segon es disparin?
- Quin timeout tindrà el client que l'utilitzarà.
Següent, quins tipus de recursos hardware seran el coll d'ampolla?
- Utilitzarà molta RAM?
- Farà moltes lectures a disc (el disc és local o bé remot?)
- Utilitzarà molta CPU?
- Utilitza recursos remots, com bases de dades?
Lògicament això depen totalment del tipus de servei que estem implementant, tots són diferents, però en el meu cas es tracta d'un servei a on el coll d'ampolla és el disc. Estic llegint de diferents fitxers de text gegants, que canvien periòdicament i sense repetició de peticions del mateix valor, ah, i també necessito valors d'una base de dades remota. Què vol dir això?
- El fet de que siguin fitxers gegants i bastants vol dir que no es poden llegir una vegada i tenir-los guardats a memòria. Per tant, cada vegada s'haurà de llegir de disc i…
- Si mai es requereix el mateix valor vol dir que sempre s'hauran de llegir posicions diferents (i aleatòries) del fitxers i que per tant no val la pena tenir "valors populars" a la memòria cau perquè com a màxim es llegiran una vegada.
- I de regal en la majoria d'invocacions he de consultar un valor en una base de dades.
Bé, tinguent en compte tot això… ho implementem. Uns dies després, s'haurà de provar.
Com ho proves un servei d'aquests?
Lògicament el servei tindrà una sèrie de testos unitaris i una sèrie de comprovacions. Però en aquest cas no em refereixo als testos d'integritat de dades i de funcionalitat. Em refereixo als testos de càrrega. Perquè no podem posar un servei en viu sense saber perfectament que aguantarà el que hagi d'aguantar. Oi?
La solució immediata és implementar un client que utilitzi l'API del servei. Una cosa tant senzilla com un bucle que invoqui mètodes i controlar els temps de resposta, amb això aconseguirem un nivel de càrrega baix, però ja tindrem per començar. Els següent pas és paral·lelitzar aquest client, fer que crei diferents threads, i que cada thread faci les seves peticions al servidor, aquí ja podem aconseguir un nivell de càrrega decent, tot i que dependrà de l'ample de banda de la màquina a on estiguem corrent el client. El següent pas és utilitzar aquest client en diverses màquines a l'hora contra el mateix servidor, aquesta és la única manera amb la que es podrà recrear un volum de trànsit "real".
Ara bé, a l'hora d'implementar el client, uns punts que s'han de tenir _molt_ en compte:
- S'han d'invocar _tots_ els mètodes del servidor, amb paràmetres _reals_ i vàlids. Així recrearem les peticions òptimes que hauria de rebre el servidor.
- S'han d'invocar _tots_ els mètodes del servidor, amb paràmetres _reals_ i _invàlids_. Un cop tinguis el servidor instal·lat i de "cara al públic" mai sabràs el que t'arribarà. No té sentit optimitzar el servidor si llavors un paràmetre fora d'ordre o fora de rang fa que saltin excepcions de forma interna o bé que es bloquegi. En el món real arribaran peticions invàlides, s'han de tenir en compte.
- S'ha de provar el servidor a poder ser amb dades reals o bé de la mateixa magnitut de les dades amb que treballarà. El programa no tardarà el mateix en llegir un fitxer de 2Mb que un de 200Gb, depen del que facis fins i tot es pot quedar sense memòria. De la mateixa manera que si s'utilitza una base de dades no és el mateix fer una consulta a una taula amb 100 files d'una base de dades de test local que a una taula de centenars de milions de files en una base de dades en producció amb diversos usuaris utilitzant-la de forma concurrent.
Pel tema de provar el client a l'hora en diverses màquines vaig poder utilitzar 40 màquines a l'hora amb 100 threads cadascunta, això son 4000 threads fontent canya al servidor :) .
Ah, una eina molt pràctica i senzilla per paral·lelitzar comandes en bash és dsh.
Vaja, tarda molt, es queda sense memòria o es penja.
Bé, el servidor funciona bé amb un thread, ja li costa més amb 100 threads i deixa de respondre en 4000. Què passa?
Això ja depén bastant del llenguatge d'implementació. En el meu cas vaig utilitzar Java i una molt bona eina per trobar problemes d'eficiència és jstack que permet veure volcats de la pila d'una aplicació corrent sense matar-la.
Bàsicament es tracta d'executar aquesta aplicació al servidor i llavors buscar entra els volcats de pila dels diversos threads el que estan BLOCKED, en cas de que n'hi hagi vol dir que són threads que estan en l'espera d'alguna cosa. Això pot donar pistes de llocs a on es pot optimitzar el codi.
i finalment, us deixo amb:
les receptes de l'àvia per optimització en Java.
Algunes coses que convé recordar a l'hora de programar en Java (tot i que algunes són de calaix):
Vaja, tarda molt, es queda sense memòria o es penja.
Bé, el servidor funciona bé amb un thread, ja li costa més amb 100 threads i deixa de respondre en 4000. Què passa?
Això ja depén bastant del llenguatge d'implementació. En el meu cas vaig utilitzar Java i una molt bona eina per trobar problemes d'eficiència és jstack que permet veure volcats de la pila d'una aplicació corrent sense matar-la.
Bàsicament es tracta d'executar aquesta aplicació al servidor i llavors buscar entra els volcats de pila dels diversos threads el que estan BLOCKED, en cas de que n'hi hagi vol dir que són threads que estan en l'espera d'alguna cosa. Això pot donar pistes de llocs a on es pot optimitzar el codi.
i finalment, us deixo amb:
les receptes de l'àvia per optimització en Java.
Algunes coses que convé recordar a l'hora de programar en Java (tot i que algunes són de calaix):
- En cas de que s'utilitzi bases de dades. És important que les connexions estiguin en un "pool" i que no es crein/destrueixin cada vegada. En cas d'utilitzar Spring/Hibernate, cal saber què s'està fent. Aquests frameworks ténen moltes classes i cadascuna fa una cosa, convé utilitzar la que toca.
- També en cas de que s'utilitzi una base de dades. Vulguis o no, pot ser costós, sobretot si es fa moltes vegades. Cal pensar sobre el resultat que n'obtenim. Spring/Hibernate utilitzen Cache per emmagatzemar valors de forma transparent, això és una cosa que també es pot fer a nivell d'aplicació. Abans d'anar a la capa de dades, pensem-ho dues vegades, si ja hem buscat exactament allò a la base de dades quina és la probabilitat de que hagi canviat? si és poca, no cal fer la consulta. Posem el resultat en cache i ja ho preguntarem de nou al cap de 1 minut, 10 minuts o 1 hora.
- Utilitzem llistes? LinkedList o ArrayList? Utilitzem conjunts? HashSet o TreeSet? la elecció de l'estructura de dades òptima pot tenir implicar unes diferències de recursos i de velocitat impressionants. Ah, i lògicament si sabem més o menys la mida que tindrà la llista resultat, inicialitzem la talla de la llista al crear-la. En aquest cas, la api de sun és la vostra amiga.
- Estem concatenant strings? doncs StringBuilder.
Espero que l'article sigui clar i que almenys el checklist de coses a provar estalvïi problemes a algú :)