Jeśli nie wiesz dlaczego jesteś sysadminem

Czym jest dla Ciebie administracja systemami? siedzeniem i aktualizowaniem pakietów na pracujących systemach? czy może pisaniem skryptów które zautomatyzują monotonne wykonywanie tych samych poleceń na dziesiątkach maszyn? a może administracja dla Ciebie to zwykły sposób na zarabianie pieniędzy? Ja myślę, że administracja jest czymś więcej niż wykonywaniem poleceń w terminalu, czy monitorowaniem pracy systemu. Administracja uczy odpowiedzialności, uczy pewnego toku rozumowania, pozwala uwierzyć, że problemy da się rozwiązywać na wiele sposobów.

Przecież nikt nie zmusza nas do brania emocjonalnej odpowiedzialności za nasze systemy, ale my robimy to zupełnie sami z własnej nieprzymuszonej woli, czujemy się odpowiedzialni za nasz sprzęt, za aplikacje, za sieć, chcemy by wszystkie elementy pracowały jak najlepiej. Potrafimy siedzieć godzinami nad rozwiązywaniem kolejnego problemu, debugujemy aplikacje krok po kroku, sprawdzając czy przypadkiem jakaś funkcja systemowa nie zachowuje się w podejrzany sposób, zamiast po prostu spróbować wykonać restart aplikacji czy serwera. Nikt nie każe nam czytać codziennie ogromu informacji na temat administrowania, podnoszenia wydajności, czy minimalizowania sytuacji wystąpienia awarii, ale robimy to i to sprawia nam przyjemność.

Administracja systemami to również pewien sposób na rywalizację z samym sobą, dla nas nie ma słów ‘nie da się’, czasami znalezienie rozwiązania może zająć tydzień, dwa, lub miesiąc, ale znajdziemy sposób by to zrobić, w ostateczności spróbujemy sami dopisać pewne funkcjonalności tak by na końcu z dumą móc powiedzieć najczęściej tylko samemu sobie, da się. Godzinami przeglądamy nasze konfiguracje ale ciągle nam coś w nich nie pasuje, a może zmiana MaxClients w Apache spowoduje zwiększenie wydajności… a może zastosowanie eacceleratora PHP skróci czas generowania strony… to wszystko przypomina bardzo dużą układankę z klocków, które rozsypane na podłodze nie tworzą nic, ale każdy może poskładać je zupełnie inaczej na swój sposób. Dzięki wielu próbom i błędom udaje nam się osiągnąć optymalne parametry które zadowolą nas na jakiś czas i nikt z nas nie żałuje poświęconego czasu, ponieważ wie zdobył kolejne doświadczenie które pozwoli w przyszłości budować coraz lepsze platformy.

Administracja to także nieprzespane noce, zgrzytanie zębów, rzucanie przekleństwami, zaciskanie pięści. Nie zawsze rozwiązanie problemu okazuje się oczywiste, szczególnie pracując podczas awarii krytycznego systemu, gdzie teoretycznie wszystko powinno działać, nie widać problemu na pierwszy rzut oka, a w praktyce nie działa nic, pół biedy jeśli mamy megabajty logów do analizy, gorzej kiedy żadna aplikacja nic nie zgłasza. To właśnie te sytuacje zmuszają nas do szukania problemu wszędzie gdzie on może wystąpić, adrenalina i stres tylko motywują do kolejnych testów, satysfakcja z rozwiązania problemu wynagradza nam każdą chwilę zwątpienia.

Bycie administratorem to również pewnego rodzaju skrzywienie, widzimy to czego inni nie dostrzegają, zegarek wyświetla godzinę 1o:1o a my zastanawiamy się ile to będzie binarnie, potęgowanie cyfry 2 … 4, 8, 16, 32 i tak do 65536 jest płynnym mówieniem z pamięci a nie liczeniem, albo nasze poczucie humoru… często niezrozumiałe dla innych słuchających, a do tego przeróżne gadżety, lub koszulki z nadrukami w stylu while ( ! ( succeed = try() ) ); które niesamowicie cieszą, ale to jest właśnie nasz sposób na codzienność.

Administracja to również temat do rozmów… potrafimy prowadzić wielogodzinne debaty na temat technologii, próbujemy rozgryzać jak coś może działać, opierając się głównie na domysłach, ale nikomu to nie przeszkadza… po prostu szukamy odpowiedzi. Przy piwie wspominamy jak ktoś wywrócił serwer bo chciał sprawdzić czy jego kolejny pomysł jest dobry i nikt nie obraża się za to na nikogo, niepowodzenia uczą pokory i otwierają nas na słuchanie starszych bardziej doświadczonych kolegów, tutaj własne ego często spływa pod stół i nie unosi się, bo każdy z nas wie, że jak dobry by nie był to i tak są lepsi od nas.

Tym dla mnie jest administracja… a czym jest dla Ciebie?

Otwarte SSH i walka z robotami

Dzisiaj z ciekawości przeprowadziłem analizę logów (auth.log) na jednym z serwerów, chciałem przekonać się czy boty próbują się logować, zgadywać hasła, skąd są, oraz które loginy najbardziej są narażone na atak. Opierając się na stosunkowo małej próbce (21 marzec - 24 kwiecień), spróbowałem wygrzebać trochę informacji...

# grep -c 'Apr 23.*Failed password' /var/log/auth.log
7546

Statystyka z wczoraj (23 kwietnia) mówi, że nieudanych prób logowania było 7546... czyli średnio co 11 sekund... Wziąłem więc wszystkie logi z ponad miesiąca i zrobiłem kilka zestawień.

Łączna ilość nieudanych logowań: 84974

Zestawienie 10 loginów na które było najwięcej prób logowania (liczba z lewej to ilość prób):

24281  root
730    oracle
705    test
559    admin
351    user
306    webmaster
306    guest
231    nagios
226    webadmin
199    postgres

Sprawdźmy skąd było najwięcej takich prób:

35454  212.106.195.103 Spain
4673   110.45.144.120 Republic of Korea
4000   121.119.164.56 Japan
2068   202.75.221.232 China
1891   210.35.88.61 China
1553   222.35.137.146   China
1068   203.192.171.186  Philippines
986    121.131.210.92   Republic of Korea
850    89.146.41.95     Netherlands
729    84.243.245.210   Netherlands

W moim przypadku wygrała Hiszpania?!? a zaraz za nią Korea, Japonia i Chiny...

Zmierzam do tego, żeby zadbać, by hasła dla użytkowników w systemie były naprawdę silne, widać, że roboty próbują logować się zarówno na użytkowników związanych z usługami (oracle, nagios, postgres) jak i systemowych (root), ciekawy jest użytkownik test ;-) ciekawy jestem ilu z Was się przyzna, że ma takiego usera w systemie z hasłem typu test123? albo dupa.8? ;-) w tym momencie nie wiem jakie hasła były podawane, ale korci mnie, żeby delikatnie zmodyfikować sshd i zacząć je również logować ;-) prawdopodobnie będą to tylko jakieś słownikowe wyrazy, ale kto wie... ;-) Jeśli ktoś nie potrafi się zmusić do używania silnych haseł, polecam zatrudnić do tego zadania starego i sprawdzonego cracklib'a, najlepiej spinając go z pam'em który przed ustawieniem hasła sprawdzi je i podpowie, czy jest silne, czy możemy je lepiej sobie darować.

W przypadku gdyby ktoś chciał zniwelować liczbę prób nieudanych logowań może skorzystać z ciekawego narzędzia Sshguard, które podpięte do syslog'a na bieżąco analizuje nieudane próby logowania i podejmuje akcję zablokowania danego IP na firewallu. Implementacja jest bardzo prosta i szybka:

Instalacja sshguard'a (Ubuntu):

# apt-get install sshguard

Następnie konfiguracja syslog-ng (w przypadku gdyby ktoś używał innego loggera, na tej stronie znajdzie gotowy przepis):

filter f_sshguard { facility(auth, authpriv) and not program("sshguard"); };
destination sshguard { program("/usr/sbin/sshguard" template("$DATE $FULLHOST $MSGHDR$MESSAGEn")); };
log { source(s_all); filter(f_sshguard); destination(sshguard); };

  • filter f_sshguard - tworzymy filtr o nazwie f_sshguard, uwzględniający poziom logowania facility oraz auth, bez ciągu znaków "sshguard"
  • destination - określamy sposób logowania zdarzenia, w tym przypadku nie zapisuje danych nigdzie do pliku, ale wywołujemy program /usr/sbin/sshguard z parametrami data, pełny host, oraz treść komunikatu
  • log - spinamy w całość filtr + destination, biorąc jako źródło logów s_all, czyli wszystkie znane źródła komunikatów (/dev/log)

Mając taki wpis możemy wykonać, reload syslog-ng:

# /etc/init.d/syslog-ng reload
* Reload system logging syslog-ng

Kolejny krok to skonfigurowanie systemowego firewall'a, w Linuksie będzie to Iptables, zgodnie z dokumentacją wykonujemy:

# iptables -N sshguard
# ip6tables -N sshguard

Właśnie utworzyliśmy nowe łańcuchy (zarówno dla IPv4 jak i IPv6), przypnijmy teraz je do naszego INPUT'a:

# iptables -A INPUT -j sshguard
# ip6tables -A INPUT -j sshguard

Domyślne wartości dla sshguarda:

  • blokada po 4 nieudanych próbach zalogowania
  • czas blokowania - 420 sekund
  • czas po którym sshguard zapomni o potencjalnym adresie IP do zablokowania - 1200 sekund

Przetestujemy jak to działa:

Apr 24 18:36:22 katha sshd[3449]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=macbook  user=root
Apr 24 18:36:24 katha sshd[3449]: Failed password for root from 192.168.1.168 port 62690 ssh2
Apr 24 18:36:24 katha sshguard[3290]: Matched IP address 192.168.1.168
Apr 24 18:36:31 katha sshd[3449]: Failed password for root from 192.168.1.168 port 62690 ssh2
Apr 24 18:36:31 katha sshguard[3290]: Matched IP address 192.168.1.168
Apr 24 18:36:31 katha sshguard[3290]: Blocking 192.168.1.168: 4 failures over 28 seconds.
Apr 24 18:36:31 katha sshguard[3290]: Setting environment: SSHG_ADDR=192.168.1.168;SSHG_ADDRKIND=4;SSHG_SERVICE=100.
Apr 24 18:36:31 katha sshguard[3290]: Run command "case $SSHG_ADDRKIND in 4) exec /sbin/iptables -A sshguard -s $SSHG_ADDR -j DROP ;; 6) exec /sbin/ip6tables -A sshguard -s $SSHG_ADDR -j DROP ;; *) exit -2 ;; esac": exited 0.

4 próby nieudane w ciągu 28 sekund i zostaliśmy odcięci:

root@katha:/home/jamzed# iptables -n -L sshguard
Chain sshguard (2 references)
target     prot opt source               destination
DROP       all  --  192.168.1.168        0.0.0.0/0

Przed użyciem polecam zapoznać się z opcjami sshguarda, bo zawsze warto dodać w pierwszej kolejności swoje IP do whitelisty ;-)

A Wy jakimi narzędziami monitorujecie serwery? zwracacie uwagę na to kto i kiedy próbuje się logować do systemu? czy może preferujecie -j DROP/REJECT od razu na SSH i tylko ACCEPT dla konkretnych IP?

Resolver i NSS

Resolver w systemie operacyjnym odpowiada za tłumaczenie nazw domenowych na adresy IP. Jest to tak naprawdę zbiór bibliotek systemowych służących do rozwiązywania nazw. Posiada własne pliki konfiguracyjne /etc/resolv.conf który jest zapewne znany dla większości użytkowników Linuksa, /etc/hosts, bardzo często edytowany na potrzeby różnych testów, oraz /etc/host.conf chyba najrzadziej dotykany, ponieważ obowiązuje dla starej wersji biblioteki libc. Nad całością czuwa NSS (Name Service Switch) który pojawił się w libc'ach od wersji 5. Przede wszystkim to właśnie konfiguracja NSS (/etc/nsswitch.conf), mówi o sposobie w jaki należy zrealizować rozwiązanie nazwy i nie chodzi tu jedynie o DNS ale również o takie podstawowe elementy systemu jak użytkownicy (passwd, group, shadow), protokoły sieciowe, itd... te usługi muszą być tłumaczone w jakiś sposób, np. użytkownicy na UID, grupy na GID a protokoły na porty. Wszystkie te pliki tworzą jedną całość pozwalającą na swobodną pracę w Internecie.

Spróbuję od początku przedstawić jak odbywa się proces zamiany hosta na IP i który plik dokładnie na co ma wpływ. Zaczynając od /etc/hosts, jest to statyczna tabela zawierającą adres IP oraz HOST z nim powiązany, jest to plik do którego najczęściej sięgamy kiedy chcemy jakiejś domenie przypisać zupełnie inny IP, np. na czas testów. Za przykład posłuży taki wpis:

# /etc/hosts

127.0.0.1 google.com www.google.com

ta linijka spowoduje, że odwołując się do domeny google.com/www.google.com (np. wpisując w przeglądarce adres) resolver skieruje nas do 127.0.0.1, a nie do prawdziwego serwera Google, dzieje się tak ponieważ NSS posiada zdefiniowaną kolejność w której będzie sprawdzał hosty, przyjrzyjmy się plikowi /etc/nsswitch.conf

# /etc/nsswitch.conf

passwd: compat
group: compat
shadow: compat
hosts: files dns
networks: files
protocols: db files
services: db files
ethers: db files
rpc: db files
netgroup: nis

taki zapis, mówi, że obsługa rozwiązywania nazw (hosts: files dns), powinna odbywać się w kolejności, pliki (/etc/hosts) a dopiero w przypadku niepowodzenia, należy przekazać żądanie do zewnętrznych serwerów DNS zdefiniowanych w pliku /etc/resolv.conf:

# /etc/resolv.conf

search pl
nameserver 195.205.203.2
nameserver 194.204.159.1

  • search pl - określa człon domeny (w tym przypadku pl) który będzie doklejany do hosta jeśli nasze zapytanie nie będzie zawierało przynajmniej jednej kropki
  • nameserver 195.205.203.2 - pierwszy zewnętrzny serwer DNS który będzie odpytywany
  • nameserver 194.204.159.1 - drugi zewnętrzny serwer DNS, odpytywany w przypadku kiedy pierwszy zawiedzie

Trochę wyjaśnienia dla parametru search pl:

# host varlog
varlog.pl has address 195.205.203.232
varlog.pl mail is handled by 10 aspmx4.googlemail.com.
varlog.pl mail is handled by 10 aspmx5.googlemail.com.
varlog.pl mail is handled by 1 aspmx.l.google.com.
varlog.pl mail is handled by 5 alt1.aspmx.l.google.com.
varlog.pl mail is handled by 5 alt2.aspmx.l.google.com.
varlog.pl mail is handled by 10 aspmx2.googlemail.com.
varlog.pl mail is handled by 10 aspmx3.googlemail.com.

pomimo tego, że odpytaliśmy o domenę varlog, resolver dokleił .pl i całe zapytanie wyglądało tak:

# tcpdump -ni eth0 port 53
10:50:51.562311 IP 195.205.203.232.47005 > 195.205.203.2.53: 18839+ A? varlog.pl. (27)
10:50:51.583712 IP 195.205.203.2.53 > 195.205.203.232.47005: 18839 1/9/8 A 195.205.203.232 (351)

gdybyśmy chcieli odpytać tylko o hosta varlog, tak by resolver nie doklejał nic od siebie musimy dodać kropkę na końcu:

# host varlog.
Host varlog. not found: 3(NXDOMAIN)

# tcpdump -ni eth0 port 53
10:52:47.538299 IP 195.205.203.232.43781 > 195.205.203.2.53: 5424+ A? varlog. (24)
10:52:47.539653 IP 195.205.203.2.53 > 195.205.203.232.43781: 5424 NXDomain 0/1/0 (99)

Dlaczego tak się dzieje? Resolver daje możliwość określenia ile kropek w nazwie domeny musi się znajdować, aby search nie był brany pod uwagę, ten parametr domyślnie wynosi 1, dlatego odpytanie o varlog spowodowało doklejenie .pl a odpytanie o varlog. już nie. Za ilość kropek odpowiada parametr ndots, który jest konfigurowany w /etc/resolv.conf:

# /etc/resolv.conf

options ndots:n

n - jest liczbą kropek która musi wystąpić w zapytaniu.

Na koniec jeszcze jeden ciekawy przykład, dodajemy wpis w /etc/hosts:

# /etc/hosts

127.0.0.1 testtesttest.pl

Spróbujmy wykonać polecenie host testtesttest.pl

# host testtesttest.pl
Host testtesttest.pl not found: 3(NXDOMAIN)

Polecenie host nic nam nie zwróciło... a wpis znajduje się w /etc/hosts... teraz dla odmiany ping testtesttest.pl

# ping -c 3 testtesttest.pl
PING testtesttest.pl (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.031 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.028 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.033 ms

--- testtesttest.pl ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.028/0.030/0.033/0.006 ms

Dziwne? ;-) wynika to z tego, że polecenie host nie korzysta z gethostbyname() do zamiany nazwy na IP, czyli przez to nie korzysta w ogóle z NSS'a. Pomija przez to /etc/hosts i od razu wysyła żądanie do zewnętrznych serwerów DNS. Dlatego ping, telnet zadziała, a host już nie...

Więcej informacji:

  • man resolv.conf
  • man host.conf
  • man hosts
  • man nss
  • man nsswitch.conf
  • man gethostbyname

Warto zapamiętać, że resolver systemowy nie jest daemonem ani usługą... jest tak naprawdę zbiorem bibliotek systemowych, które często nawet nieświadomie są pośrednio przez nas używane, siedzą gdzieś ukryte w systemie i odwalają za nas kawał dobrej roboty... ;-)

Hello FastCGI!

FastCGI jest bardzo często kojarzone wyłącznie z Perl'em lub PHP, w przypadku serwera Lighttpd, użycie FastCGI jest jedynym sposobem aby zmusić go do pracy z PHP, ale musimy wiedzieć, że jest niezależne od języka programowania i serwera WWW, jest to raczej coś w rodzaju sposobu komunikacji pomiędzy serwerem a aplikacją (pomostem). Zasadniczą różnicą pomiędzy CGI a FastCGI jest wydajność, która została znacznie podniesiona poprzez wyeliminowanie konieczności uruchamiania skryptu/aplikacji przy każdym odwołaniu. W przypadku CGI serwer WWW otrzymując żądanie od przeglądarki uruchamiał "na boku" aplikację i zwracał wynik, natomiast dzięki FastCGI istnieje możliwość uruchomienia konkretnej liczby instancji aplikacji, które w trybie ciągłym oczekują na wywołanie bez konieczności każdorazowego uruchamiania procesu.

Przewagą FastCGI nad CGI może być również to, że uruchomiona aplikacja jest w stanie przechowywać różne wartości w pamięci do których możemy się odwołać w dowolnym momencie, w przypadku CGI nie było to możliwe, ponieważ po obsłużeniu każdego żądania proces umierał i zawartość buforów była tracona.

Procesy FastCGI (aplikacje) są całkowicie niezależne od serwera, po prostu działają obok, a to zdecydowanie utrudnia wywołanie awarii w której padnie serwer WWW.

Jeśli chodzi o skalowanie, to sprawa wygląda tak, że serwer WWW otrzymuje request od użytkownika i przekazuje go w pełni do aplikacji fcgi, w serwerze WWW możemy określić w jaki sposób ma nastąpić połączenie (np. socket, lub tcp), dzięki temu możemy mieć odseparowane aplikacje od warstwy HTTP.

Spróbujmy napisać i uruchomić prostą aplikację FastCGI w C (Lighttpd + Ubuntu):

Instalacja niezbędnej biblioteki libfcgi do obsługi FastCGI w ANSI C
# apt-get install libfcgi-dev

Konfiguracja serwera Lighttpd:

# cat /etc/lighttpd/conf-enabled/10-fastcgi.conf
server.modules   += ( "mod_fastcgi" )

fastcgi.server    = ( ".fcgi" =>
((
"bin-path" => "/tmp/app-cgi",
"socket" => "/tmp/cgi.socket",
"max-procs" => 4,
"check-local" => "disable",
"bin-copy-environment" => (
"PATH", "SHELL", "USER"
),
))
)

  • server.modules - taki zapis pozwala na dodanie do listy modułów, modułu mod_fastcgi
  • fastcgi.server - ".fcgi" - określa dla których rozszerzeń żądanie ma być przekazane do aplikacji fcgi
  • bin-path - ścieżka do naszej aplikacji
  • socket - ściezka do socketu po których będzie odbywała się komunikacja (lighttpd -> fcgi), istnieje również możliwość komunikacji po tcp, wtedy nasza aplikacja fcgi może być uruchomiona na zdalnym serwerze (klastrze)
  • max-procs - maksymalna liczba procesow naszej aplikacji fcgi
  • idle-timeout - czas bezczynności
  • check-local - jeśli jest enable, to serwer lighttpd będzie najpierw szukał pliku ihha.fcgi w document.root serwera, jeśli nie znajdzie to zwróci 404, natomiast jeśli jest ustawiona na disable to przekaże od razu request do aplikacji fcgi
  • bin-copy-environment - pozwala na kopiowanie wartości zmiennych środowiskowych (PATH, SHELL, USER)

Kod naszej aplikacji:

  • #include "fcgi_stdio.h" - plik nagłówkowy dla fcgi
  • int count - deklaracja zmiennej count - licznika obsłużonych żądań (dla każdego procesu app-cgi jest unikalny)
  • while (FCGI_Accept() >= 0) - pętla główna naszej aplikacji (specyficzne dla fastcgi, ponieważ aplikacja nie kończy działania)
  • printf("Content-type: text/html") - wysyłamy nagłówek HTTP mówiący o tym, że kontent będzie danymi w postaci text/html (lighttpd przekazuje całe żądanie do aplikacji fastcgi, tak więc aplikacja musi odpowiedzieć wraz z nagłówkami, ten jest niezbędny aby wyświetlić tekst w przeglądarce)
  • ++count, getpid() - inkrementacja licznika count oraz pobranie pidu procesu (argumenty dla printf(%d %d))

Kompilacja odbywa się w następujący sposób:

# gcc /tmp/cgi-app.c -lfcgi -o /tmp/app-cgi

  • -lfcgi - mówi kompilatorowi by zlinkował nasz program app-cgi z biblioteką fcgi

# ldd /tmp/app-cgi
linux-gate.so.1 => (0x0070c000)
libfcgi.so.0 => /usr/lib/libfcgi.so.0 (0x00ef8000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00ce9000)
libnsl.so.1 => /lib/tls/i686/cmov/libnsl.so.1 (0x005fc000)
libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x00234000)
/lib/ld-linux.so.2 (0x00665000)

Mając skompilowaną aplikację i skonfigurowany serwer WWW możemy spróbować uruchomić całość:

# /etc/init.d/lighttpd restart

Dodatkowo możemy sprawdzić czy nasza aplikacja FCGI została poprawnie zespawnowana:

# pstree | grep lighttpd
|-lighttpd---4*[app-cgi]

Widzimy 1 proces lighttpd i dodatkowe 4 procesy app-cgi ;-) czyli wszystko gra. Pobieramy stronę:

$ lwp-request http://192.168.1.194/app.fcgi
<h1>FastCGI App!</h1>Request: 1 <br />PID: 11282

Wszystko działa poprawnie, serwer nam zwrócił numer requestu: 1 dla konkretnego procesu app-cgi (PID). W przypadku dużego ruchu, Lighttpd rozrzuci ruch pomiędzy 4 procesy app-cgi, ale należy pamiętać, że zmienne aplikacji są tylko w obrębie jednego procesu - count dla każdego procesu app-cgi będzie inny.

Czy jest sens używania FastCGI? Oczywiście, że tak. W wielu zastosowaniach taka aplikacja będzie dużo bardziej wydajna niż skrypt napisany w PHP, czasami nie ma potrzeby zaprzęgania całego interpretera PHP czy Perl, do wykonania jakiejś prostej czynności, wszyscy przecież wiemy, że Linuks lubi C. ;-)

A Wy korzystacie z FastCGI? albo widzicie jakieś ciekawe zastosowanie?

Q&A - jak działają skracacze URLi?

Cała magia skracaczy zawarta jest w nagłówkach HTTP jakie otrzymuje przeglądarka od serwera WWW. Za przekierowanie najczęściej odpowiadają dwa kody HTTP, 301 oraz 302. Kod 301 (Moved Permanently) jest używany wtedy, gdy przekierowanie jest permanentne, czyli jest zrobione na stałe, natomiast kod 302 (Found), jest przekierowaniem tymczasowym i kolejne żądanie pobrania tej strony powinno odbywać się przy użyciu oryginalnego adresu. Kody HTTP są niezbędne w komunikacji, to dzięki nim przeglądarka wie, czy otrzymała stronę z danymi poprawnie (200), czy dana strona nie istnieje (404), lub czy np. wymagana jest autoryzacja (401) za pomocą loginu/hasła, więcej o kodach odpowiedzi HTTP przeczytasz tutaj.

Żeby dokładnie zrozumieć zasadę działania, spróbujmy zbudować własny skracacz... na początek prosta baza danych (MySQL):

mysql> create database DB_URL;
mysql> use DB_URL;
mysql> create table TB_SHORTURL ( id int not null auto_increment primary key, url text ) engine=innodb;
mysql> grant all privileges on DB_URL.* to 'short'@'localhost' identified by 'password';

Mając bazę, teraz pobawmy się trochę kodem PHP:

Zaznaczam od razu, że skrypt nie ma żadnej walidacji danych wejściowych/wyjściowych jest tylko PoC'em, za wyrządzone krzywdy bazie danych, serwerowi nie biorę odpowiedzialności ;-) dodatkowo taki skrypt powinien dbać o to, by nie było duplikatów. Ten zdecydowanie tego nie robi ;-)

Co robi ten skrypt?

Skrypt wykonuje połączenie do bazy danych DB_URL, w przypadku kiedy w GET pojawi się zmienna ?url= skrypt przyjmuje wartość (pełny URL) i wstawia ją do bazy, uzyskując w odpowiedzi ID pod którym znalazł się w bazie. Dzięki temu jesteśmy w stanie powiązać nasz krótki url (1, 2, 3, itd...) z całym URLem. W przypadku kiedy w GET pojawi się ?u=, skrypt pobierze ID, sprawdzi w bazie jaki URL się znajduje pod tym wpisem, przekaże pełny URL do PHP który wyśle odpowiedni nagłówek HTTP:

header('Location: http://www.domena.pl');

funkcja PHP header(), wysyła żądany przez nas nagłówek HTTP, w tym przypadku Location, co w praktyce oznacza, wysłanie kodu 302 do przeglądarki, zmuszając ją do przekierowania na adres www.domena.pl:

jamzed@makufka:~$ telnet shorturl 80
Trying 192.168.1.228...
Connected to katha.
Escape character is '^]'.
GET /?u=6 HTTP/1.0
Host: shorturl

HTTP/1.1 302 Found
Date: Sat, 17 Apr 2010 07:30:41 GMT
Server: Apache
Location: http://www.domena.pl
Vary: Accept-Encoding
Content-Length: 0
Connection: close
Content-Type: text/html

Connection closed by foreign host.

Jedyne o czym należy pamiętać przy zabawach z wysyłaniem nagłówków HTTP, to konieczność wysłania ich przed jakimkolwiek kontentem, czyli header() musi być wywołany jeszcze przed wyświetleniem , ponieważ nagłówki od kontentu są oddzielone pustą linią i jeśli przeglądarka otrzyma nagłówek w sekcji kontentu nie zrobi z tym nic, a PHP zgłosi błąd wyglądający mniej więcej tak:

Warning: Cannot modify header information - headers already sent by (output started at /var/www/test/index.php:15) in /var/www/test/index.php on line 15

That's all. ;-)

Praktyczne wykorzystanie Memcached + PHP

memcached, mała darmowa aplikacja, która odpowiednio wykorzystana pozwoli nam bardzo szybko i skutecznie podnieść wydajność aplikacji lub serwisów WWW. Słowo memcache na łamach serwisu pojawiało się już kilka razy (opis, charakterystyka memcache, zastosowanie memcache jako storage + nginx) i na pewno jeszcze się pojawi wielokrotnie, ale tym razem wystąpi w roli głównej ;-) pokażę jak można praktycznie wykorzystać memcache w celu zwiększenia wydajności aplikacji PHP poprzez odciążenie np. bazy danych. Za przykład posłuży nam prosty skrypt, ale najpierw instalacja...

Do prawidłowego działania potrzebujemy daemona memcached oraz modułu PHP php5-memcache.

Instalacja memcached (Ubuntu)

root@katha:/home/jamzed# apt-get install memcached php5-memcache

memcached do prawidłowego działania wymaga libevent (biblioteki służącej do obsługi zdarzeń, w skrócie, obsługa kolejki połączeń). Warto pamiętać o tym, żeby po instalacji php5-memcache wykonać restart Apache.

Jeśli apt-get install powiódł się, powinniśmy mieć działający serwer memcached:

root@katha:/home/jamzed# /etc/init.d/memcached start
Starting memcached: memcached disabled in /etc/default/memcached.

Komunikat ten oznacza, że do wystartowania memcached, musimy dokonać małej modyfikacji w pliku /etc/default/memcached:

ENABLE_MEMCACHED=no
zmieniamy na
ENABLE_MEMCACHED=yes

startujemy jeszcze raz:

root@katha:/home/jamzed# /etc/init.d/memcached start
Starting memcached: memcached.

Plik konfiguracyjny memcached znajduje się w lokalizacji /etc/memcached.conf i zawiera takie opcje jak:

  • -d - tryb pracy jako daemon
  • logfile - plik z logami naszego daemona
  • -v - verbose mode (dostępne: -v oraz -vv)
  • -m 64 - ilość pamięci jaką przeznaczamy memcache'owi (tyle MB danych pomieści)
  • -p 11211 - port na którym będzie słuchał
  • -u nobody - uprawnienia użytkownika z jakimi się uruchomi
  • -l 127.0.0.1 - adres IP na którym będzie słuchał
  • -c 1024 - limit połączeń

Mając działający daemon, możemy spróbować napisać prosty kod w PHP do sprawdzenia działania naszego serwera:

Co robi powyższy kod:

  • tworzy nowy obiekt memcached typu Memcache,
  • wykonuje połączenie do serwera memcache,
  • sprawdza czy może pobrać klucz "key",
  • jeśli wartość została pobrana, jest wyświetlana i skrypt końcy działanie,
  • jeśli nie ma wpisanej wartości, jest dodawana na 10 sekund, następnie pobierana oraz wyświetlana,

Wyjaśnienie argumentów $memcache->set ('nazwa_klucza', 'wartość_która_będzie_pod_kluczem', kompresja, czas_życia_obiektu);

Dlaczego tak? Ponieważ przekładając to na implementację gdzie będziemy mieli bazę danych, najpierw powinniśmy sprawdzić czy mamy w memcache'u wynik kwerendy, w przypadku kiedy będzie to pobierzemy go z memcached i zostawimy bazę w spokoju, natomiast jeśli nie będzie to pobierzemy go z bazy danych i wstawimy do memcache'a na przyszłość.

Kod pobierający 10000 razy dane z bazy może wyglądać następująco:

mamy tutaj:

  • połączenie do MySQL'a,
  • wybranie bazy danych,
  • pętlę wykonującą się 10,000 razy,
  • losowanie $rand_id z przediału 1,100,
  • pobranie z bazy danych dla ID = $rand_id,
  • wyświetlenie informacji na ekran,
  • zamknięcie połączenia z bazą danych

wszystko jest ok, zapytanie jest bardzo proste i prawdopodobnie baza i tak wrzuci to wszystko do pamięci, więc po zastosowaniu memcached w tym konkretnym przypadku raczej nie skróci się nam czas wykonywania skryptu, ale chodzi mi o przedstawienie samego sposobu, dlatego nie zwracajmy na to uwagi ;-) przepiszmy powyższy kod tak by przed pobraniem informacji, sprawdził czy jest ona w memcached i dopiero w przypadku kiedy jej nie ma, pobierał ją z bazy.

Skrypcik trochę nam się powiększył, ale nic skomplikowanego nie doszło ;-) pojawiła się funkcja memQuery(), która pobiera w argumencie zapytanie SQL, wylicza z niego sumę kontrolną md5, która tak naprawdę będzie naszym kluczem dla wpisu w memcached, pod którym zostanie umieszczona właściwa wartość (pobrana z bazy danych), jeśli taki klucz będzie w memcached to funkcja zwróci wartość, jeśli nie, to odpyta bazę danych, umieści wynik w memcached na 10 sekund i dopiero zwróci wartość. Taka implementacje daje nam możliwość szybkiego umieszczania w memcached wyników zapytań SQL, to jest tylko PoC, nie obsługiwane są żadne wyjątki ani skomplikowane struktury danych, podpowiem tylko, że jeśli wartości nie będą pojedyncze tak jak w tym przypadku, a np. zapytanie będzie takie SELECT a,b,c,d FROM d, to warto skusić się o serializację danych przed umieszczeniem ich w memcached ;-)

Kiedy używać memcached? Zawsze! ;-) o ile oczywiście jest to możliwe, kiedy np. pobierasz dane z bazy, który przez pewien okres czasu są stałe. Natomiast w przypadku danych które szybko się zmieniają, wrzucanie tego dodatkowo do warstwy cache'ującej nie ma sensu, bo dochodzi Ci dodatkowe połączenie TCP i obsługa, takie zapytania najczęściej charakteryzują się tym, że zawierają jakiś przedział czasowy (WHERE data_dodania < aktulny_czas)...

Warto też wspomnieć, że samo tworzenie zapytań SQL jest bardzo istotną kwestią przy wydajności, często już od samej kolejności użycia klauzuli WHERE zależy szybkość wykonania zapytania, a o kwerendach zawierających timestamp nawet nie będę wspominał. ;-) tak więc nie traktujcie memcached jako lekarstwa na wszystkie problemy, zawsze w pierwszej kolejności optymalizujcie zapytania SQL, optymalizujcie i jeszcze raz optymalizujcie ;-)

Q&A - jak wylistować zmienne środowiskowe działającej aplikacji?

W systemie GNU/Linux wiele aplikacji pobiera parametry konfiguracyjne bezpośrednio ze zmiennych środowiskowych, jeśli chcesz ustawić jakąś zmienną, by program ją widział, musisz to zrobić przed jego wystartowaniem, najlepiej tworząc skrypt startowy, ale co w przypadku kiedy program już działa, a my nie jesteśmy pewni z jakimi zmiennymi się uruchomił?

Do wylistowania aktualnie ustawionych zmiennych dla naszego użytkownika/procesu, służy polecenie env, lub export, natomiast jeśli chcemy sprawdzić, jakie zmienne ma ustawiony konkretny proces w systemie, mamy na to dwa sposoby, pierwszy to skorzystanie z przełącznika -e w poleceniu ps (ps -xwwe):

wynik będzie następujący:

# ps xwwe
1165 ?        Ss     0:00 /usr/sbin/apache2 -k start APACHE_PID_FILE=/var/run/apache2.pid APACHE_RUN_USER=www-data PATH=/usr/local/bin:/usr/bin:/bin PWD=/ APACHE_RUN_GROUP=www-data LANG=C SHLVL=1 _=/usr/sbin/apache2

proces apache2 ma ustawione m.in. zmienne APACHE_PID_FILE, APACHE_RUN_USER, APACHE_RUN_GROUP, LANG, drugi sposób to odczytanie ich bezpośrednio z /proc'a. Mając PID procesu (1165), wykonujemy polecenie $ cat /proc/$PID/environ:

# cat /proc/1165/environ
APACHE_PID_FILE=/var/run/apache2.pidAPACHE_RUN_USER=www-dataPATH=/usr/local/bin:/usr/bin:/binPWD=/APACHE_RUN_GROUP=www-dataLANG=CSHLVL=1_=/usr/sbin/apache2r

Tak naprawdę ps "jest nakładką na /proc" i pobiera dane dokładnie z niego, przy okazji przygotowując wynik w czytelnej postaci, dla wygody powinniśmy korzystać po prostu z ps -e, a o /proc jedynie pamiętać ;-)

Q&A - jak sprawdzić ile adresów IP zmieści się w danej masce podsieci?

Jeśli nie chcesz wyliczać ręcznie ile adresów IP zmieści się w danej masce podsieci, możesz wykorzystać narzędzia typu kalkulator IP. Popularnym i bardzo prostym w obsłudze jest ipcalc (http://jodies.de/ipcalc). Jest to skrypt Perl'owy, który wykona za nas obliczenia i poda jakie IP mogą być użyte w danej podsieci. Skrypt przyjmuje zarówno zapis maski w postaci 255.255.255.0 jak i CIDR (/24, /32, /16).

Przykładowe użycie skryptu:

$ ./ipcalc 192.168.0.1/24
Address:   192.168.0.1          11000000.10101000.00000000. 00000001
Netmask:   255.255.255.0 = 24   11111111.11111111.11111111. 00000000
Wildcard:  0.0.0.255            00000000.00000000.00000000. 11111111
=>
Network:   192.168.0.0/24       11000000.10101000.00000000. 00000000
HostMin:   192.168.0.1          11000000.10101000.00000000. 00000001
HostMax:   192.168.0.254        11000000.10101000.00000000. 11111110
Broadcast: 192.168.0.255        11000000.10101000.00000000. 11111111
Hosts/Net: 254                   Class C, Private Internet

HostMin to minimalny IP jaki może zostać użyty, HostMax to maksymalny IP, Hosts to maksymalna liczba IP do wykorzystania.

Gdybyś jednak nie miał możliwości uruchomienia ipcalc'a na swoim systemie, możesz zawsze skorzystać z naszych narzędzi dostępnych przez WWW (ipcalc, ping, traceroute, whois).

Rekurencyjne zakładanie katalogów

Czasami przychodzi potrzeba założenia dużej ilości zagnieżdżonych katalogów, w celu przygotowania jakiejś struktury do przechowywania danych, można to zrobić na kilka sposobów, najlepiej w taki który jest dla nas najwygodniejszy. W przypadku kiedy takich struktur zakładamy wiele, albo są bardzo zagnieżdżone, warto pomyśleć również nad optymalizacją.

Jak myślicie, czy da się przyśpieszyć zakładanie katalogów poprzez użycie mkdir -p?

Posłużę się pewnym przykładem:

mamy 3 zagnieżdżone pętle i mkdir -p, skrypt tworzy taką strukturę katalogów:

0/0/0,
0/0/1,
... ,
f/f/e,
f/f/f,

założenie 4096 katalogów na moim sprzęcie trwa kilkanaście sekund, po przyjrzeniu się bliżej, z użyciem strace ;-)

mkdir("a", 0755) = 0
open("a", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_NOFOLLOW) = 3
fchdir(3) = 0
close(3) = 0
mkdir("a", 0755) = 0
open("a", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_NOFOLLOW) = 3
fchdir(3) = 0
close(3) = 0
mkdir("a/", 0755) = 0

widać, że podczas zakładania katalogów, mkdir -p za każdym razem wywołuje funkcje open(), fchdir() i dopiero mkdir(), co można analogicznie przełożyć na:

# mkdir a
# cd a
# mkdir a
# cd a
# mkdir a

znacznie szybciej byłoby to zrobić następująco:

# mkdir a
# mkdir a/a
# mkdir a/a/a

dodatkowo, najbardziej kosztownym czynnikiem jest to, że przy każdej iteracji pętli, mkdir musi zostać ponownie uruchomiony (dziękuję BartOwl za zwrócenie uwagi), czyli pozostają 2 możliwości, zmodyfikować powyższą pętle w bashu lub iść na łatwiznę i użyć Perla ;-)

który zachowuję się następująco:

mkdir("3", 0777) = 0
mkdir("3/0", 0777) = 0
mkdir("3/0/0", 0777) = 0
stat64("3/0/1", 0x81530c8) = -1 ENOENT (No such file or directory)
stat64("3/0", {st_mode=S_IFDIR|0755, st_size=72, ...}) = 0
mkdir("3/0/1", 0777) = 0
stat64("3/0/2", 0x81530c8) = -1 ENOENT (No such file or directory)
stat64("3/0", {st_mode=S_IFDIR|0755, st_size=96, ...}) = 0
mkdir("3/0/2", 0777) = 0

dla tej ilości katalogów operacja wykonuje się ewidentnie dużo szybciej, poniżej czasy wykonania dla skryptu bash'owego:

root@getpost:/home/jamzed/test# time ./createdir.sh
real 0m12.845s
user 0m7.133s
sys 0m5.692s

oraz dla skryptu napisanego w Perlu:

root@getpost:/home/jamzed/test# time ./createdir.pl
real 0m0.558s
user 0m0.332s
sys 0m0.226s

Wiem, że może być trudno sobie wyobrazić potrzebę zakładania takich struktur, ale tym wpisem chciałem pokazać, że czasami jedna bardzo mała zmiana może spowodować wzrost wydajności i spadek czasu wykonywania kilkadziesiąt razy. Poza tym, zawsze warto wiedzieć jak dokładnie działa jakieś narzędzie i ile można z niego jeszcze wycisnąć ;-)

A Wy które elementy systemów optymalizujecie najczęściej?

Q&A - jak przekierować STDOUT oraz STDERR do plików?

STDOUT jest standardowym wyjściem, czyli jest to miejsce do którego zwracają wyniki nasze polecenia, STDERR jest standardowym wyjściem błędów, czyli miejscem gdzie nasze polecenia zwracają błędy. Jeśli uruchamiamy coś z konsoli to automatycznie widzimy STDOUT (deskryptor 1) oraz STDERR (deskryptor 2) i jesteśmy w stanie zareagować, ale co w przypadku kiedy nasz skrypt jest uruchamiany z automatu? Pozostaje nam przekierować STDOUT oraz STDERR do plików.

Mam 3 pliki, plik1, plik2 oraz plik3, plik4 nie istnieje.

jamzed@katharsis:~$ cat plik1 plik2 plik3
zawartosc pliku 1
zawartosc pliku 2
zawartosc pliku 3

jamzed@katharsis:~$ cat plik4
cat: plik4: No such file or directory

wydając pierwsze polecenie wynik trafił na STDOUT (nie było błędów), natomiast w drugim przypadku, wynik trafił na STDERR (plik nie istnieje), spróbujmy teraz zapisać wynik do pliku:

jamzed@katharsis:~$ cat plik1 plik2 plik3 plik4 > wynik_stdout
cat: plik4: No such file or directory

do pliku wynik_stdout nie trafi komunikat: "cat: plik4: No such file or directory", ponieważ przekierowaliśmy jedynie STDOUT, a STDERR poszedł na konsolę. Spróbujmy więc przekierować STDOUT oraz STDERR do dwóch różnych plików:

jamzed@katharsis:~$ cat plik1 plik2 plik3 plik4 1> wynik_stdout 2> wynik_stderr

jamzed@katharsis:~$ cat wynik_stdout
zawartosc pliku 1
zawartosc pliku 2
zawartosc pliku 3

jamzed@katharsis:~$ cat wynik_stderr
cat: plik4: No such file or directory

Teraz, znacznie lepiej, mamy dwa pliku (poprawne wykonanie oraz błędy), gdybyśmy chcieli zapisać i STDOUT i STDERR w jednym pliku, użyjemy takiego zapisu:

jamzed@katharsis:~$ cat plik1 plik2 plik3 plik4 > wynik_stdout_stderr 2>&1

jamzed@katharsis:~$ cat wynik_stdout_stderr
zawartosc pliku 1
zawartosc pliku 2
zawartosc pliku 3
cat: plik4: No such file or directory

Więcej przykładów oraz możliwości zarządzania wejściem/wyjściem znajdziecie w manualu dla bash'a (man bash) ;-)