KIELMAS.COM Kielmas Jarosław

Blog głównie o programowaniu

Zrobiłem malutki teścik zastosowania indexów na wybranej tabeli w bazie Mysql. Chciałem zobaczyć jakie będą  różnice czasowe zapytań podczas zastosowania różnych indexów na mojej tabeli.

Tabela ma postać:

CREATE  TABLE IF NOT EXISTS `media` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
  `acl_user_id` INT(11) DEFAULT NULL,
  `parent_id` INT(11) DEFAULT NULL,
  `size` INT(11) DEFAULT NULL,
  `extension` ENUM('mp3','mp4','mpg', 'mpeg', 'jpg', 'png', 'bmp' ) DEFAULT NULL,
  `type`  ENUM('audio','video','image', 'avatar')NOT NULL,
  `status` ENUM('active','inactive','deleted', 'processing', 'banned' ) NOT NULL ,
  `date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ,
  `date_activated` DATETIME DEFAULT NULL,
  `date_deleted` DATETIME DEFAULT NULL,
  `date_banned` DATETIME DEFAULT NULL,
  `name` VARCHAR(255) NOT NULL,
  `path` VARCHAR(255) DEFAULT NULL,
  `description` VARCHAR(255) DEFAULT NULL ,
  `guid` CHAR(255) DEFAULT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_polish_ci;

Użyłem procedury sql do wypełnienia tej tabeli pół milionem losowych danych.

W przypadku braku indexów wypełnianie tabeli danymi  trwało: 4 min 17.78 sec

Czas na pytania bez indexów:

SELECT COUNT(id) FROM media WHERE STATUS = 'active'(trwało: 1.08 sec)
SELECT id FROM media WHERE SIZE = 30;(trwało:  0.99 sec)
SELECT COUNT(id) FROM media WHERE name = 'nazawa'(trwało: 0.93 sec)
SELECT COUNT(id) FROM media WHERE SIZE = 30 AND STATUS = 'active' AND name = 'nazawa'; (trwało: 1.10 sec)

Następnie dodałem następujące indexy:

ALTER TABLE media ADD INDEX idx_media_status (STATUS);
ALTER TABLE media ADD INDEX idx_media_size (`size`);
ALTER TABLE media ADD INDEX idx_media_name (`name`);

(* dodawanie indeksów do istniejących pół miliona wierszy trwało odpowiednio: 18.11 sec, 23.24 sec oraz 39.00 sec)

W przypadku, gdy indexy istniały już w schemacie, napełnienie tabeli o pół mioliona danych trwało: 5 min 13.60 sec

Ponowiłem zapytania z wprowadzonymi indexami. Oto wyniki:

SELECT COUNT(id) FROM media WHERE STATUS = 'active'; (trwało: 0.02 sec)
SELECT id FROM media WHERE SIZE = 30;(trwało: 0.01 sec)
SELECT COUNT(id) FROM media WHERE name = 'nazawa'; (trwało: 0.02 sec)
SELECT COUNT(id) FROM media WHERE SIZE = 30 AND STATUS = 'active' AND name = 'nazawa'; (trwało: 0.10 sec)

Dodałem jeszcze jeden klucz, aby zoptymalizować ostatnie zapytanie, z którego bardzo często będę korzystał w aplikacji:

ALTER TABLE media ADD INDEX idx_media_name_status_size (`name`, STATUS, `size`);

A oto wyniki zapytania:

SELECT COUNT(id) FROM media WHERE SIZE = 30 AND STATUS = 'active' AND name = 'nazawa'; (0.03 sec)

Jak widać z powyższych przykładów stosowanie indexów ma ogromny wpływ na wydajność. Podobne wnioski można zauważyć na wielu blogach, jednak najlepiej wykonać taki prosty test samemu, aby przekonać się o sile stosowania indexów. Pamiętajmy jednak, aby stosować indexy z umiarem – ich wprowadzenie opóźnia zapis danych do bazy. Ponadto, tak jak mówi dokumentacja mysql,  należy zauważyć różnicę  pomiędzy indexem dla jednego pola i dla wielu:

ALTER TABLE media ADD INDEX idx_media_size (`size`);

a

ALTER TABLE media ADD INDEX idx_media_name_status_size (`name`, STATUS, `size`);

są to dwa różne klucze, służące różnym typom zapytań.

W piątek pojawiła się kolejna, już siódma,  wersja Zend Framework’a z serii 1.10. Poprawiono około 60 bug’ów, które opisane są w changelog. Zachęcam do aktualizacji Waszych projektów.

W ramach wydania wersji  dotknięto następujące pliki:

# git status
#	modified:   Application/Bootstrap/BootstrapAbstract.php
#	modified:   Application/Resource/Exception.php
 
#	modified:   Auth/Adapter/DbTable.php
 
#	modified:   Cache/Backend/Static.php
#	modified:   Cache/Backend/Test.php
#	modified:   Cache/Backend/TwoLevels.php
#	modified:   Cache/Core.php
#	modified:   Cache/Frontend/Class.php
#	modified:   Cache/Frontend/Function.php
#	modified:   Cache/Manager.php
 
#	modified:   Captcha/Image.php
 
#	modified:   Console/Getopt.php
 
#	modified:   Crypt/Math/BigInteger/Bcmath.php
 
#	modified:   Db/Adapter/Pdo/Pgsql.php
#	modified:   Db/Table/Rowset/Abstract.php
 
#	modified:   Feed/Pubsubhubbub/Model/Subscription.php
 
#	modified:   File/Transfer/Adapter/Http.php
 
#	modified:   Filter/StripTags.php
 
#	modified:   Gdata/App.php
 
#	modified:   Gdata/Gapps.php
 
#	modified:   Http/Client.php
#	modified:   Http/Client/Adapter/Socket.php
#	modified:   Http/Response.php
 
#	modified:   Json/Decoder.php
 
#	modified:   Ldap.php
 
#	modified:   Loader/PluginLoader.php
 
#	modified:   Locale.php
#	modified:   Locale/Format.php
 
#	modified:   Log.php
#	modified:   Log/Writer/Abstract.php
#	modified:   Log/Writer/Db.php
#	modified:   Log/Writer/Syslog.php
#	modified:   Log/Writer/ZendMonitor.php
 
#	modified:   Mail/Protocol/Smtp/Auth/Crammd5.php
#	modified:   Mail/Protocol/Smtp/Auth/Plain.php
 
#	modified:   OpenId.php
 
#	modified:   Paginator/AdapterAggregate.php
#	modified:   Paginator/SerializableLimitIterator.php
 
#	modified:   Pdf.php
#	modified:   Pdf/FileParser/Image/Png.php
#	modified:   Pdf/Filter/Ascii85.php
#	modified:   Pdf/Resource/Image/Png.php
 
#	modified:   Search/Lucene/Index/SegmentInfo.php
 
#	modified:   Service/Amazon/S3/Stream.php
#	modified:   Service/Flickr.php
#	modified:   Service/Twitter.php
 
#	modified:   Session.php
 
#	modified:   Test/PHPUnit/Constraint/DomQuery.php
 
#	modified:   Tool/Project/Provider/Manifest.php
 
#	modified:   Translate.php
#	modified:   Translate/Adapter.php
#	modified:   Translate/Adapter/Gettext.php
#	modified:   Translate/Plural.php
 
#	modified:   Uri.php
 
#	modified:   Validate/Alnum.php
#	modified:   Validate/Alpha.php
#	modified:   Validate/Barcode.php
#	modified:   Validate/Callback.php
#	modified:   Validate/CreditCard.php
#	modified:   Validate/Date.php
#	modified:   Validate/Db/Abstract.php
#	modified:   Validate/Digits.php
#	modified:   Validate/EmailAddress.php
#	modified:   Validate/File/Crc32.php
#	modified:   Validate/File/ExcludeExtension.php
#	modified:   Validate/File/Extension.php
#	modified:   Validate/File/Hash.php
#	modified:   Validate/File/ImageSize.php
#	modified:   Validate/File/IsCompressed.php
#	modified:   Validate/File/IsImage.php
#	modified:   Validate/File/Md5.php
#	modified:   Validate/File/MimeType.php
#	modified:   Validate/File/Sha1.php
#	modified:   Validate/File/Size.php
#	modified:   Validate/File/WordCount.php
#	modified:   Validate/Float.php
#	modified:   Validate/Hex.php
#	modified:   Validate/Hostname.php
#	modified:   Validate/Int.php
#	modified:   Validate/Ip.php
#	modified:   Validate/Isbn.php
#	modified:   Validate/NotEmpty.php
#	modified:   Validate/PostCode.php
#	modified:   Validate/Regex.php
#	modified:   Validate/Sitemap/Changefreq.php
#	modified:   Validate/Sitemap/Lastmod.php
#	modified:   Validate/Sitemap/Loc.php
#	modified:   Validate/Sitemap/Priority.php
#	modified:   Validate/StringLength.php
#	modified:   Version.php

i dodano :

	Gdata/Gapps/Extension/Property.php
#	Gdata/Gapps/GroupEntry.php
#	Gdata/Gapps/GroupFeed.php
#	Gdata/Gapps/GroupQuery.php
#	Gdata/Gapps/MemberEntry.php
#	Gdata/Gapps/MemberFeed.php
#	Gdata/Gapps/MemberQuery.php
#	Gdata/Gapps/OwnerEntry.php
#	Gdata/Gapps/OwnerFeed.php
#	Gdata/Gapps/OwnerQuery.php

Logowanie błędów jest kluczowym elementem minitoringu aplikacji. Przekonał się o tym chociażby każdy programista, który pracuje w firmie oferującej dodatkowo dla swoich produktów terminowy support.

Dzięki temu, że błędy są logowane jesteśmy w stanie szybko i czasami transparentnie wyeliminować bugi w aplikacji, tak że klient nawet się nie dowie o ich istnieniu:)

Jeśli Zend Framework jest podstawą twojego projektu oferuję Tobie bardzo wygodny i przejrzysty sposób na logowanie błędów aplikacji.

Potrzebna będzie jedna klasa, ponieważ aż się prosi aby system logowania błędów był ładowany jako resource, przez Zend_Application. Zarządzanie zasobów zaprezentowane w Zend Framework jest moim zdaniem rewelacyjne, ponieważ bardzo łatwo możemy wydzielić zasoby, dzięki czemu mamy wprowadzoną dodatkową  logikę i ład w naszej aplikacji,  ponadto  dowolny zasób  można łatwo i sprawnie włączyć/wyłączyć.

Przykładowa klasa mogłaby wygłądać następująco:

class Zextend_Application_Resource_Log extends Zend_Application_Resource_ResourceAbstract
{
 
    public function init()
    {
        /**
         * Pobranie głównej konfiguracji.
         */
        $config = $this->getBootstrap()->getContainer()->config;
 
        /**
         * Przygotowanie formatu zapisanej informacji.
         *
         * @example Postać zmiennej $config->format:
         * "%timestamp% %priorityName%: %message%"
         */
        $format = new Zend_Log_Formatter_Simple($config->format . PHP_EOL);
 
        /**
         * Sprawdzanie, ktory sposób logowania błędów został włączony.
         *
         * @example Postać zmiennej $config->log->stream:
         * "../temporary/log/error.log" 
         */
        $logger = new Zend_Log();
        if($config->log->file) {
           $logger->addWriter(
                   $this->_getStreamWriter($format, $config->log->stream));
        }
 
        if($config->log->stdoutput) {
           $logger->addWriter(
                   $this->_getStreamWriter($format, 'php://output'));
        }
 
        if($config->log->firebug) {
           $logger->addWriter(
                   $this->_getFirebugWriter($format));
        }
        if($config->log->mail) {
            $logger->addWriter($this->_getMailWriter($config));
        }
 
        /**
         * Używamy tego logger wszędzie w aplikacji.
         */
        Zend_Registry::set( 'log', $logger );
        return $logger;
    }
    /**
     * Zwraca obiekt zapisania informacja do Firebug.
     * 
     * @param Zend_Log_Formatter_Simple $format
     * @return Zend_Log_Writer_Firebug
     */
    private function _getFirebugWriter(Zend_Log_Formatter_Simple $format)
    {
        $writer = new Zend_Log_Writer_Firebug();
        $writer->setFormatter($format);
 
        return $writer;
    }
    /**
     * Zwraca obiekt zapisania informacji w postaci emaila.
     *
     * @param Zend_Config_Ini $config
     * @return Zend_Log_Writer_Mail
     */
    private function _getMailWriter(Zend_Config_Ini $config)
    {
        $mail = new Zend_Mail();
        $mail->setDefaultTransport(new Zend_Mail_Transport_Smtp());
 
        $mail->setFrom($config->email->from, $config->email->fromAlias);
        $mail->addTo($config->app->admin->email);
        $mail->addTo($config->app->webmaster->email);
        $mail->setSubject($config->app->name . ' - nastapil nieoczekiwany blad!');
 
        $writer = new Zend_Log_Writer_Mail($mail);
        $writer->addFilter(Zend_Log::WARN);
 
        return $writer;
    }
    /**
     * Zwraca obiekt zapisania informacji do PHP stream.
     * 
     * @param Zend_Log_Formatter_Simple $format
     * @param string $stream
     * @return Zend_Log_Writer_Stream
     */
    private function _getStreamWriter(Zend_Log_Formatter_Simple $format, $stream = '')
    {
        $writer = new Zend_Log_Writer_Stream($stream);
        $writer->setFormatter($format);
 
        return $writer;
    }
}

Jak widać oferuje ona możliwość logowania błędów na kilka sposobów: streaming do pliku, badź na ekran, JSON’em prosto do firebug’a (warunkiem odczytu jest zainstalowanie wtyczki FirePHP w Firefox) lub powiadomienie na email. W przykładzie część konfiguracji znajduje się w pliku /application/configs/application.ini, ktory ładowany jest poprzez Zend_Config_Ini do rejestru. Odczytany zostaje między innymi email do webmastera oraz do admina, czyli osób odpowiedzialnych za poprawne działanie aplikacji, ponadto zdefiniowane są takie parametry jak scieżka do pliku  z blędami (error.log) czy też format komunikatu z błędem.

Oczywiście trzeba tę funkcjonalność dołączyć  do projektu jako  nowy zasób. Nic prostszego – w pliku konfiguracyjnym /application/configs/application.ini dodajemy kolejny resource do listy:

resources.types      = "array()"
resources.view       = "array()"
resources.navigation = "array()"
resources.log        = "array()"

Dodatkowo musimy zdefniować ustawienia, które załączają odpowiednei przekazanie informacji o błędzie. Czyli znów w /application/configs/application.ini dopisujemy:

log.file      = true
log.firebug   = true
log.mail      = true
log.stdoutput = true

Rzecz jasna korzystamy z odpowiedniego typu logowania w zależności od potrzeb. Bardzo wygodnie jest w trakcie tworzenia kodu, czy też jego testowania, otrzymywać komunikaty do konsoli FF, wykorzystując firePHP, dodatkowo wygodne jest odczytywanie błędów z pliku error.log – otwieramy konsole i  dajemy polecenie:

# tail -f error.log

i mamy nasz system monitorujacy, w którym dokładnie zapisane jest gdzie wystąpił nieoczekiwany błąd (kupujemy telewizor LCD, który pokazuje te komunikaty przez cały czas 🙂 oczywiście żartuje :)).  Jeśli aplikacja jest już w wersji produkcyjnej to bardzo fajne jest rozwiązanie z przychodzącym emailem – można wtedy szybko interweniować.

Pamiętajmy jednak, że jeśli aplikacja jest już na produkcji, należy zablokować logowanie błędów do FireBug’a!!

Logowane błędy są tylko dla nas – developerów, adminów, …,  i mają stanowić wskazówkę do szybkiego ich  rozwiązania. Jeśli te informacje trafią do niepowołanych rąk to niestety możemy wskazać najsłabsze ogniwo naszej aplikacji, a tym samym stworzyć okazję do włamania się do niej np. hackerowi.

Kolejną sprawą jest miejsce, w którym nasz logger, trzymany w rejestrze, może dokonać zapisu błędu. Jednym z lepszym miejsc jest kontroler ErrorController.php i w nim metoda errorAction,  na którą domyślnie przekierowany jest błąd. Oczywiście gdzie będzie odbywało się logowanie błędów utożsamiony z tą linijką kodu:

if (Zend_Registry::isRegistered('log')) {
            $logger = Zend_Registry::get('log');
            if (!is_null($this->_error)) {
                $logger->log(
                    $this->_error->exception->getMessage() . "\n\n" .
                    $this->_error->exception->getTraceAsString(),
                    Zend_Log::ERR
                );
            }
        }

jest uzależnione od  potrzeb aplikacji i zaprojektowania jej przez programistę.

Taki sposób logowania błędów  można implementować w każdym projekcie – nie tylko opartym o Zend Framework. Po drobnych modyfikacjach, korzystając tylko z kilku komponentów (w wersji ZF 1.10.6), Log(20 plików), Wildfire (8 plików),  Json (14 plików), Config (8 plików) + Exception.php, można stworzyć taki sam system logowania błędów.

Svnsync to jeden z wielu programów wchodzący w skład pakietu subversion. Służy do synchronizacji  dwóch  repozytoriów – pierwotnego i kopii. Ze względu na tę funkcjonalność narzędzie to idealnie może służyć do tworzenia kopii całego repozytorium na innym komputerze.

Mi się przydało w osobliwej sytuacji, gdzie do dyspozycji w firmie  miałem „działowego” vps. Chciałem wprowadzić trac’a jako domyślny task manager dla wszystkich projektów z działu. Jednak repozytoria tych projektów znajdują się na innej maszynie, a dostanie się do nich wymusza otwarcie tunelu. Jednak obecna, stabilna wersja trac (0.11) nie obsługuje jeszcze zdalnego repozytorium, a administrator jest mega zajęty  i nie ma czasu na moje widzi misie 🙂

Z pomocą przyszedł program svnsync.

Zasada jest bardzo prosta – inicjujemy na komputerze (hostującym kopię) projekt kopię :

svnadmin create /home/services/subversion/kopia
cd /home/services/subversion/kopia
find -type f | xargs chmod 666
find -type d | xargs chmod 777
chown -Rf svnuser:svn *

Dalej na komputerze kopii dodajemy nowego uzytkownika w svn, który będzie mial możliwość zapisywania plików w tym repozytorium. Najprościej:

htpasswd -nmb svnsync moje_haslo >> /etc/htpasswd/svn_dav

Następnie w katalogu /home/services/subversion/kopia/hooks należy dodać pliki:

start-commit

#!/bin/sh
 
USER="$2"
 
if [ "$USER" = "svnsync" ]; then exit 0; fi
 
echo "Tylko użytkownik svnsync może dodawać nowe rewizje" >&2
exit 1

oraz plik pre-revprop-change

#!/bin/sh
 
USER="$2"
 
if [ "$USER" = "svnsync" ]; then exit 0; fi
 
echo "Tylko użytkownik svnsync może dodawać nowe rewizje" >&2
exit 1

Następnie:

chmod 775 /home/services/subversion/kopia/hooks/start-commit
chmod 775 /home/services/subversion/kopia/hooks/pre-revprop-change

Przeznaczenie tych plików? Sprawa prosta, kopia służy tak naprawdę tylko do odczytu. Nie chcę aby to repozytorium było robocze, ponieważ synchronizacja odbywa się tylko w jedną stronę. Udostępnienie kopii w formie roboczej doprowadziło by do powstania dwóch niespójnych repozytoriów.

Jest tylko jeden użytkownik tego repozytorium – svnsync, dzięki temu mam pewność, że nikt nie zaciągnie sobię tego repozytorium i nie będzie nic commit‚ował. W rezultacie uzyskam dokładną kopię repozytorium, na którym mi zależy.

Bardzo ważna uwaga! svnsync synchronizuje cały projekt, a nie jego rozgałęzienia. Nie możemy zatem synchronizować tylko wybranego katalogu.

Następnie musimy zainicjować synchronizację przez wywołanie polecenia svnsync z parametrem initialize. Jeśli w grę wchodzi kilka repozytoriów najlepiej napisać prosty skrypt np. taki:

#!/bin/sh
 
PROJECTNAME_FROM="$1"
PROJECTNAME_TO="$2"
SVNSYNC=/usr/bin/svnsync
FROM=https://host_oryginal/PROJECTNAME_FROM
TO=https://host_kopia/$PROJECTNAME_TO
SYNC_USER=svnsync
SYNC_PASS=skomplikowane_haslo
SOURCE_USER=uzytkownik_oryginalu
SOURCE_PASS=haslo_uzytkownika_oryginalu
 
$SVNSYNC initialize $TO $FROM --sync-username $SYNC_USER --sync-password $SYNC_PASS  --source-username $SOURCE_USER --source-password $SOURCE_PASS &  exit 0

Wtedy wywołanie takiego skryptu sprowadza się jedynie do:

svnsync_init oryginal kopia

Mi się przydał, bo synchronizowałem ok dziesięć projektów.

Nasuwa się dalej pytanie – jak synchronizować kopię z oryginałem?

Rozwiązań jest kilka. Można np. użyć crona,  który co okręślony interval czasu odpalał by skrypt podobny do tego:

#!/bin/sh
 
PROJECTNAME_TO="$1"
 
SVNSYNC=/usr/bin/svnsync
TO=https://host_kopia/$PROJECTNAME_TO
SYNC_USER=svnsync
SYNC_PASS=skomplikowane_haslo
SOURCE_USER=uzytkownik_oryginalu
SOURCE_PASS=haslo_uzytkownik_oryginalu
 
$SVNSYNC --non-interactive sync $TO --sync-username $SYNC_USER --sync-password $SYNC_PASS --source-username $SOURCE_USER --source-password $SOURCE_PASS &  exit 0
chmod 775 /home/services/subversion/kopia/hooks/svnsync_init

Polecenie svnsync z parametrem sync służy do synchronizowania repozytorium.

Wywołanie skryptu synchronizującego wyglądało by tak:

svnsync_sync kopia

Takie rozwiązanie jest jednak bardzo niewygodne. Poza tym, że dodatkowo co jakiś określony czas obciążamy maszynę kopię, to w dodatku zmiany w repozytorium pojawią się dopiero za chwilę (np. za 5 minut). Ja nie mogę czekać tak długo. Przypominam, że zależy mi na pracy z trac‚iem. Każdy commit musi być rejestrowany automatycznie. Poza tym w moim przypadku, aby wywolać polecenie svn up, muszę otworzyć tunel. Mogę to zrobić jedynie z ręki i nie mógłbym wtedy doprowadzić do zamknięcia tego tunelu.

Z pomocą znów przychodzą hooks na serwerze oryginał. W katalogu /home/services/subversion/oryginal/hooks edytujemy plik:

post-commit

#!/bin/sh
 
SVNSYNC=/usr/bin/svnsync
TO=https://host_kopia/kopia
SYNC_USER=svnsync
SYNC_PASS=skomplikowane_haslo
SOURCE_USER=uzytkownik_oryginal
SOURCE_PASS=haslo_uzytkownika_oryginal
 
$SVNSYNC --non-interactive sync $TO --sync-username $SYNC_USER --sync-password $SYNC_PASS --source-username $SOURCE_USER --source-password $SOURCE_PASS &  exit 0
chmod 775 /home/services/subversion/kopia/hooks/post-commit

Opcja –non-interactive spowoduje, że w razie niepowodzenia synchronizacji użytkownicy pracujący przy repozytrium nie zobaczą żadnego błędu podczas commit‚a, jeśli takowy miałby wystąpić. W praktyce takie rozwiązanie działa błyskawicznie i bezbłędnie.

Podsumowując, osiągnąłęm swój cel. Na maszynie kopia mam zainstalowanego trac‚a, który bazuje na repozytorium znajdujące się na maszynie oryginal. W praktyce Każdy commit w repozytorium oryginal spowoduje zarejestrowanie zmiany w trac’u.

Super od teraz możemy  zarządzać projektami 🙂