Rozdział 25
Optymalizacja wydajności
w aplikacjach typu klient/serwer
W rozdziale tym omówiliśmy kilka metod optymalizowania wydajności aplikacji
typu klient/serwer, tworzonych w środowisku Delphi. Trzy podrozdziały omawiają
kolejno: optymalizację wydajności aplikacji w Delphi, optymalizację serwera
i optymalizację wydajności w sieci. Zwróćmy uwagę na to, że podane tu
wskazówki w żaden sposób nie wyczerpują zagadnień związanych z wydajnością.
Autor opisuje w zwięzły sposób wiele sposobów optymalizacji. Głównym celem
tego rozdziału jest wprowadzenie Czytelnika do wielu różnych technik, jakie stoją
do jego dyspozycji. Nie zastąpi to jednak dokładnej i szczegółowej znajomości
posiadanego systemu DBMS, sieci i platformy serwera.
UWAGA:
W rozdziale niniejszym poruszamy szereg zagadnień związanych z optymalizacją
wydajności w
kilku różnych platformach DBMS. Szczegółowe informacje
o optymalizowaniu aplikacji klient/serwer w poszczególnych systemach DBMS
można znaleźć w rozdziałach 15-18.
Jaka szybkość działania jest wystarczająca?
Przed przystąpieniem do optymalizacji musimy się zastanowić, co konkretnie jest
naszym celem. Niestety optymalizacja wydajności rzadko może mieć charakter
prewencyjny; dopóki system działa w miarę zadowalająco, poświęcamy jej mało
uwagi. Tak więc trzeba zacząć od ustalenia, czego dokładnie oczekujemy. Jaka
szybkość okaże się wystarczająca dla naszych celów? Po ustaleniu, co chcemy
osiągnąć, możemy się zastanowić, jak to osiągnąć.
Kryteria oceny wydajności
Drugim etapem optymalizacji powinno być ustalenie, jakie cechy
optymalizowanego systemu możemy modyfikować. Musimy poznać parametry
wydajności, czyli takie wielkości, których zmiana może spowodować jej
zwiększenie. Przykładami takich parametrów są zmienne konfiguracyjne serwera,
716
Część IV
wartości pól obiektów w Delphi, ustawienia sieciowe itd. Jednym z głównych
celów niniejszego rozdziału jest zasugerowanie Czytelnikowi, jakie parametry
może on poddać modyfikacji w celu zoptymalizowania wydajności aplikacji typu
klient/serwer, napisanych i rozwijanych w Delphi.
Środowisko testowe
Środkiem stojącym do naszej dyspozycji jest serwer testowy. Na początku pracy
nie zmieniajmy kodu użytkowego ani parametrów serwera. Jest przynajmniej kilka
powodów, aby tego nie robić. Przede wszystkim nie chcemy przecież, żeby nasze
testy spowodowały utratę lub przekłamania danych. Wszak nigdy nie
uruchamiamy sprawdzanego kodu w systemach użytkowych (production systems)
lub takich, z których właśnie korzystają inne osoby. Drugi powód, dla którego
powinniśmy unikać testowania systemu podczas jego normalnej pracy wynika
z ryzyka, iż inni użytkownicy wywrą niepożądany i nieprzewidziany wpływ na
wyniki testowania. W takich sytuacjach testy często generują przedziwne rezultaty,
których na pozór nie sposób wyjaśnić.
Sposoby określania wydajności
Teraz, kiedy już poruszyliśmy podstawowe zagadnienia związane z optymalizacją,
czas na wyjaśnienie, co dokładnie należy rozumieć pod pojęciem wydajności
(performance). Wydajność systemu można określać na wiele sposobów.
Najczęściej korzystamy przy tym z następujących wskaźników:
Szybkość w transakcjach na sekundę, TPS (Transactions per second) - ogólna
przepustowość systemu, często stosowana jako miara wydajności. W przypadku
optymalizacji według tego wskaźnika uwagę skupiamy przede wszystkim na
ułatwieniu aplikacji dostępu do serwera, optymalizacji samego serwera
i umożliwieniu współbieżności.
Czas odpowiedzi na zapytanie (query response time) - wydajność systemu
można też mierzyć według czasu, przez jaki konkretne zapytanie zostaje
wykonanie. Przy stosowaniu tego wskaźnika skupiamy się głównie na środkach
prowadzących do skrócenia tego czasu.
Czas wykonania zadań wsadowych (batch job execution time) - nasza aplikacja
kliencka może wymagać, żeby dane zadanie wsadowe wykonywało się
w ustalonym przedziale czasu. Zadanie może obejmować wiele zapytań,
współpracę z urządzeniami zewnętrznymi lub inne procesy. W takim przypadku
naszym głównym celem staje się skrócenie całkowitego czasu wykonania
zadania.
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
717
Wydajność interfejsu aplikacji (application responsiveness) - wydajność
aplikacji klienckiej można określić według szybkości wyświetlania ekranów
i generacji raportów. Musimy wówczas skupić się na złożonych zagadnieniach
dotyczących wzajemnej współpracy między klientem a serwerem.
Sposób realizacji współbieżności (concurrency) - tworzonego przez nas
systemu mogą dotyczyć narzucone z góry wymagania, dotyczące możliwości
jednoczesnego dostępu. Wymaganie takie może na przykład narzucać na naszą
aplikację zdolność zapewnienia tysiącu użytkownikom wprowadzania wierszy
do tej samej tablicy w tym samym czasie. W takim przypadku musimy
przeprowadzić optymalizację systemu pod kątem maksymalnej redukcji liczby
procesów blokowania i konfliktów dostępu do zasobów (resource contention).
Wydajność najczęściej określamy w sposób złożony, za pomocą kombinacji
powyższych miar. Rzadko zdarza się, żeby wyłącznie jedna z nich mogła w sposób
wyczerpujący określić wydajność systemu. Dzieje się tak między innymi z powodu
wzajemnych powiązań między tymi miarami. Np. wskaźnik TPS możemy uznać za
najważniejszą miarę wydajności w konkretnym zadaniu do wykonania z użyciem
klienta, ale na ogólną przepustowość systemu z
wieloma użytkownikami
bezpośredni wpływ może mieć także sposób realizacji współbieżności. Jeżeli
wzajemne blokady procesów będą często występowały, to spadek średniej
wartości wskaźnika TPS jest bardzo prawdopodobny. Tak więc - nawet jeżeli
jedną z miar wydajności uznamy za ważniejszą od pozostałych, to i tak zapewne
okaże się, że optymalizacja wymaga doboru wielu powiązanych ze sobą
parametrów wydajności.
Optymalizacja wydajności aplikacji
Poniższe wskazówki dotyczą aplikacji w
układzie klient/serwer. Ponieważ
aplikacja pełni rolę bramy udostępniającej informacje zawarte w serwerze, to
przyjęte w niej podejście do nabywania i prezentacji danych może mieć wielki
wpływ na całkowitą wydajność systemu.
Minimalizowanie liczby połączeń z serwerem
Przede wszystkim należy unikać otwierania zbędnych połączeń z serwerem,
którego zasoby są przecież ograniczone. Powoduje to niepotrzebne obciążenie jego
zasobów, spowolnienie działania aplikacji klienckiej i może przyczynić się do
przepełnienia sieci. Poniżej podajemy niektóre z technik limitowania liczby
połączeń z serwerem.
718
Część IV
Rola komponentu TDatabase
Jeden ze sposobów ograniczania koniecznych połączeń z serwerem polega na
korzystaniu tylko z jednego komponentu typu
TDatabase
w całej aplikacji. Oto
wskazówki:
1. Umieścić komponent
TDatabase
w głównym formularzu aplikacji.
2. Nadać własności
AliasName
komponentu
TDatabase
wartość wskazującą
na ten alias BDE, którego ma używać aplikacja.
3. Własności
DatabaseName
nadać wartość będącą nazwą, która ma zostać
udostępniona w aplikacji jako lokalny alias.
4. Używając komponentu
DataSet
(np.
TTable
,
TQuery
lub
TStoredProc
), jego własności
DatabaseName
należy nadawać wartość
taką, jak dla
TDatabase (
zamiast aliasu BDE).
Po otwarciu tak zainicjowanego komponentu
TDataSet
, nie utworzy on swojego
własnego połączenia, ale skorzysta z już istniejącego - udostępnionego przez
komponent
TDatabase
.
Istnieje jednak jedno ograniczenie, o którym musimy pamiętać, gdy serwerowi
udostępniamy połączenie z użyciem komponentu
TDatabase
, a nie oddzielnego
aliasu BDE. Podczas pracy z kreatorem formularzy (form designer), otwierając
komponent typu
TDataSet
, dotyczący aliasu dla aplikacji, powinniśmy
przestrzegać następujących wskazówek:
formularz z komponentem
DTatabase
trzeba otwierać również w kreatorze
formularzy
Otwarcie komponentu
TDataSet
(przez nadanie wartości
True
własności
Active
w oknie Inspektora obiektów - Object Inspector), który dotyczy
komponentu
TDatabase,
automatycznie powoduje nawiązanie połączenia
z serwerem. Ponieważ
True
(prawda) jest domyślną wartością własności
KeepConnection
(utrzymaj połączenie) tego komponentu, zamknięcie
TDataSet
nie spowoduje zamknięcia sesji z serwerem. Zamiast tego, gdy
zachowamy projekt i zakończymy pracę z Delphi, status dla
TDatabase
zostanie zachowany wraz z nim. Powtórnie wczytując projekt, będziemy
musieli wpisać hasło, ponieważ komponent
TDatabase
spróbuje ponownie
nawiązać połączenie z serwerem. Zmienić to możemy nadając własności
KeepConnection
wartość
False
(falsz); wówczas jednak w sytuacji, gdy
nie ma aktywnych komponentów
DataSet
(bo wszystkie już zamknięto)
będziemy musieli ponownie logować się w serwerze przed każdą próbą
otwarcia nowego komponentu
DataSet
.
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
719
SQL PASSTHRU MODE
Wyrażenie
SQL PASSTHRU MODE
, służące do określania aliasu bazy danych,
daje nam następną możliwość redukcji liczby połączeń z serwerem. Parametr
SQL
PASSTHRU
może być ustawiony dla rodziny programów obsługi (driver) albo dla
wybranych aliasów. Jeżeli ustawiliśmy go dla rodziny programów obsługi, to
będzie on dotyczyć tylko nowo definiowanych aliasów, a nie już istniejących.
Parametr ten może przyjmować jedną z trzech wartości:
NOT SHARED
,
SHARED
AUTOCOMMIT i SHARED NOAUTOCOMMIT
. Dwie ostatnie wartości pomagają
w utrzymaniu małej liczby połączeń z serwerem, ponieważ zezwalają środowisku
BDE na współdzielenie (share) połączeń, nawiązanych z serwerem przez naszą
aplikację.
Formularze dynamiczne
Jeszcze jeden sposób ograniczania całkowitej liczby połączeń z serwerem to
tworzenie formularzy dopiero wówczas, gdy są rzeczywiście potrzebne. Domyślnie
wszystkie formularze w aplikacji tworzy się zaraz po jej uruchomieniu, nawet
jeżeli tylko część z nich jest kiedykolwiek jednocześnie używana. O wiele lepiej
jest samodzielnie tworzyć i usuwać formularze o drugorzędnym znaczeniu, niż
pozwolić im na niepotrzebne zajmowanie połączeń bazy danych z serwerem
i innych zasobów.
Zaprogramowanie jawnego tworzenia i usuwania formularzy, które nie mają być
automatycznie tworzone, nie jest trudne. Oto metoda dynamicznego tworzenia
i usuwania formularzy:
1. Otworzyć okno dialogowe
Project Options
w Delphi
2. Przesunąć wszystkie formularze o drugorzędnym znaczeniu z listy
Auto-create
forms
do listy
Available forms
3. Dla każdego dynamicznie tworzonego formularza w kodzie obsługi zdarzenia
OnClose
umieścić przypisanie
Action:=caFree
4. Do utworzenia formularza (w celu jego wyświetlenia) należy użyć konstrukcji
Application.CreateForm
(
TForm1
,
Form1
), wpisując - zamiast
TForm1
- typ klasy dla formularza, a zamiast
Form1
- konkretną zmienną
z nim związaną.
5. Wyświetlić formularz z użyciem metody
ShowModal
.
6. Usunąć formularz przez jego zamknięcie.
720
Część IV
Język SQL a wydajność
Kilka następnych wskazówek dotyczy optymalizowania komunikacji pomiędzy
aplikacją a serwerami, realizowanej z użyciem języka SQL. Ponieważ SQL jest
uniwersalnym językiem systemów DBMS typu klient/serwer, jego dobra
znajomość jest bardzo pożądana.
Stosowanie procedur pamiętanych
Kod skompilowany wykonuje się szybciej niż interpretowany, niezależnie od
języka programowania. Zasada ta jest ogólnie prawdziwa także w przypadku SQL.
Wszędzie, gdzie to możliwe, stosujmy procedury pamiętane (stored procedures)
z parametrami zamiast komponentów
TQuery
i dynamicznego języka SQL
środowiska Delphi. Zapewni to większą szybkość działania, niż w przypadku
wysyłania dynamicznych wyrażeń SQL, ponieważ serwer kompiluje i z góry
optymalizuje procedury pamiętane. Program interpretowany musi przechodzić
przez proces tłumaczenia i optymalizacji za każdym razem, gdy chcemy go
wykonać. Tak więc - im więcej możemy zaoszczędzić na etapie kompilacji, tym
lepiej.
Autor sądzi jednak, że jest jeden obszar zastosowań, w którym częste używanie
procedur pamiętanych jest niewłaściwe. Dotyczy on zwykłych modyfikacji danych
- w rodzaju tych, jakie zwykle przeprowadzamy przy pomocy komend
INSERT
,
UPDATE
lub
DELETE
. W ciągu ostatnich kilku lat pojawiła się tendencja do
modyfikowania danych z wykorzystaniem procedur pamiętanych, a nie kontrolek
obsługi danych (data-aware controls). Powody przytaczane ku temu są różne, od
lepszej ochrony i wydajności do większego zakresu kontroli nad sposobami
realizacji wyrażeń języka DML. Są to wszystko argumenty istotne, ale pomija się
w nich fakt, że podejście takie osłabia celowość stosowania narzędzi w rodzaju
Delphi, które służą do tworzenia i rozwijania aplikacji klient/serwer. W przypadku
aplikacji tworzonych w
Delphi jedynym sposobem automatycznego użycia
procedur pamiętanych do normalnych modyfikacji danych jest wykorzystanie
komponentu typu
TUpdateSQL
. Chociaż komponent taki radzi sobie zupełnie
dobrze, i tak wymaga od nas zaprogramowania w języku SQL konkretnego
sposobu aktualizacji tabel. Nasze procedury pamiętane będą niewątpliwie
wymagać listy wartości pól przy wstawianiu, usuwaniu lub modyfikacji wierszy
tabeli. Napisany przez nas kod wymaga więc aktualizacji przy każdej zmianie
struktury tabeli i dlatego podejścia takiego należy unikać.
Nie oznacza to braku przydatności komponentu
TUpdateSQL
. Zawsze jednak
powinniśmy najpierw próbować wykorzystać wbudowany w Delphi mechanizm
aktualizacji tabel. Modyfikacje z użyciem komponentu
TUpdateSQL
należy
przeprowadzać tylko w razie absolutnej konieczności.
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
721
Wykonując aktualizacje za pośrednictwem procedur pamiętanych bez komponentu
TUpdateSQL
, całkowicie tracimy wspomaganie ze strony udostępnianych przez
Delphi kontrolek obsługi danych. Musimy wtedy ręcznie konfigurować zwykłe
kontrolki tak, aby symulowały kontrolki obsługi danych na drodze ograniczenia
akceptowanych typów danych, ściągania dla nich wartości po pierwszym
wyświetleniu formularza i wreszcie wysyłania zmodyfikowanych wartości do
serwera poprzez procedury pamiętane po zamknięciu formularza. Opisana strategia
jest żmudna i prowadzi do błędów. Ponadto stawia pod znakiem zapytania sens
korzystania z narzędzi w rodzaju Delphi, a dokładniej z ich zdolności do działania
na danych z serwera. W takim przypadku moglibyśmy po prostu użyć narzędzia
bez wbudowanych udogodnień komunikacji z serwerem, bezpośrednio wywołując
funkcje API do obsługi baz danych.
Inną wadą takiej koncepcji jest fakt, że prowadzi ono do omijania mechanizmów
ochrony zapewnianych przez serwer. Większość narzędzi do administrowania
systemem nie pozwala nam zorientować się, czy np. procedura pamiętana XYZ ma
przywilej
DELETE
w
odniesieniu do tablicy XYZ. Tracimy możliwość
przeglądania z jednego punktu obserwacyjnego konfiguracji ochrony w systemie,
czyli praw nadanych (lub anulowanych) użytkownikom lub grupom do
indywidualnych obiektów bazy danych. Nie wystarczą wówczas informacje
o prawach, nadanych lub anulowanych przez nas za pomocą komend
GRANT
lub
REVOKE
- dodatkowo musimy jeszcze zapoznawać się z zawartością procedur
pamiętanych naszej bazy danych.
UWAGA:
Inaczej niż w przypadku systemów Sybase, Oracle i Microsoft SQL Server,
platforma InterBase dopuszcza, żeby procedurom zdarzeń nadawane były
(i anulowane) prawa do obiektów bazy danych, tak jakby procedury te były
użytkownikami. Związane z tym informacje dotyczące ochrony przechowuje się
jako część danych serwera. Informacji tych mogą dotyczyć zapytania ze strony
narzędzi do administrowania bazami danych - tak więc możliwe jest uzyskanie
obszernego przeglądu stanu ochrony w serwerze. Autor jednak w dalszym ciągu
uważa taki sposób realizacji ochrony baz danych za niewłaściwy.
Dawniej narzędzia do tworzenia aplikacji klient/serwer były na tyle prymitywne,
że przeprowadzanie prostych modyfikacji bazy danych poprzez procedury
pamiętane było złem koniecznym. Czas ten jednak powoli przechodzi do
przeszłości, w związku czym tylko na korzyść wyjdzie nam, jeśli postaramy się
w pełni wykorzystać wbudowane w Delphi mechanizmy służące modyfikowaniu
danych. Zapamiętajmy następującą regułę: procedur pamiętanych używamy do
realizacji złożonych zapytań oraz do zadań innych, niż proste manipulacje na
danych.
722
Część IV
Korzystanie z metody Prepare
Metodę
Prepare
(przygotuj) komponentów
TQuery
powinniśmy wywoływać,
zanim je otworzymy.
Prepare
wysyła zapytanie SQL do programu obsługi baz
danych (database engine), aby zostało ono poddane analizie syntaktycznej
(parsing) oraz optymalizacji. Jeżeli metodę tę wywołuje się jawnie dla
dynamicznego zapytania SQL, które ma być wykonywane wiele razy (a nie jeden
raz), Delphi wysyła tylko parametry takiego zapytania - a nie cały jego kod - za
każdą jego realizacją. Jeżeli metody
Prepare
nie wywoła się jawnie z góry, to
zapytanie będzie automatycznie przygotowywane przy każdym otwarciu. Dzięki
przygotowaniu zapytania z góry, zdejmujemy ten obowiązek z programu obsługi
baz danych - w efekcie będziemy mogli je otwierać i zamykać wiele razy pod rząd,
bez konieczności ponownego przygotowywania. Z całą pewnością przyczyni się do
przyspieszenia operacji często wykorzystujących zapytania.
Własność UpdateMode
Zarówno typ
TTable
, jak i
TQuery
propagują własność
UpdateMode
. Określa
ona typ klauzuli języka SQL
WHERE
, której używamy do modyfikowania danych
za pomocą kontrolek obsługi danych (data-aware controls). Wartością domyślną
jest
UpWhereAll -
co oznacza, że BDE generuje klauzulę
WHERE
, która
wyświetla każdą kolumnę tabeli. Takie działanie może okazać się bardzo mało
wydajne, zwłaszcza dla dużych tabel. Alternatywną i szybszą koncepcją jest
skorzystanie z ustawienia
UpWhereChanged
. Powoduje ono wygenerowanie
klauzuli
WHERE
, w której występują tylko kluczowe pola tabeli - wraz z polami,
które uległy zmianie. Ilustruje to poniższy przykład.
Załóżmy, że nasza aplikacja w Delphi zmieniła właśnie pole LastName (nazwisko)
tabeli CUSTOMER. Oto kod SQL, wygenerowany przy ustawieniu
UpWhereAll
- zwróćmy uwagę na długą klauzulę
WHERE
:
UPDATE CUSTOMER
SET LastName=’newlastname’
WHERE CustomerNumber=1
AND LastName=’Doe’
AND FirstName=’John’
AND StreetAddress=’123 SunnyLane’
AND City=’Anywhere’
AND State=’OK’
AND Zip=’73115’
A oto wyrażenie wygenerowane przy ustawieniu
UpWhereChanged
:
UPDATE CUSTOMER
SET LastName=’newlastname’
WHERE CustomerNumber=1
AND LastName=’Doe’
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
723
Zauważmy, o ile krótszy jest drugi program. Poza tym, dzięki umieszczeniu w nim
starej wartości pola
LastName
w klauzuli
WHERE
, uniknięto niebezpieczeństwa
nadpisania modyfikacji tego pola, wprowadzonych przez innego użytkownika.
Jeżeli inny użytkownik zmieniłby pole
LastName
w czasie pomiędzy odczytem
wiersza a jego uaktualnieniem przez użytkownika bieżącego, wyrażenie
UPDATE
wygenerowane przy ustawieniu
UpWhereChanged
zakończy się niepomyślnie
i tego właśnie sobie życzymy. Ta metoda zapewnia nieco mniejszą odporność na
błędy niż
UpWhereAll
. Inny użytkownik mógłby usunąć wiersz po jego odczycie
przez naszą aplikację, a potem dodać nowy rekord do tabeli, który przypadkiem
będzie miał tę samą wartość klucza i pola
LastName
, co stary rekord. Jeżeli dla
naszego rekordu użyto by jego wyrażenia
UPDATE
, uaktualniony zostałby
niewłaściwy rekord. Taki scenariusz jest jednak bardzo mało prawdopodobny.
Ustawienie własności
UpdateMode
na wartość
UpWhereKeyOnly
zmniejsza
jeszcze odporność na błędy, ale też ma swoje zalety. Powoduje ono sprawdzanie
tylko kluczowych wartości dla wiersza, który modyfikujemy - innymi słowy
zakłada się w niej, że niemożliwa jest modyfikacja uaktualnianego przez nas pola
w czasie, jaki upłynął od chwili pierwszego odczytania rekordu. Założenie takie
może być bezpieczne, ale nie musi.
OSTRZEŻENIE
W większości aplikacji wielodostępnych (multi-user applications) założenie, że
rekordu nie zmodyfikowano od chwili jego pierwszego odczytu przez naszą
aplikację kliencką, nie jest bezpieczne. Dlatego też przy ustawieniu
UpWhere-
KeyOnly
powinniśmy zachować dużą ostrożność. Przed wykorzystaniem go
w aplikacji dla wielu użytkowników, musimy dobrze poznać wszystkie związane
z nim ograniczenia.
Ustawienie
UpWhereKeyOnly
zapewnia taki rodzaj optymalizacji, który należy
stosować w
rzadkich przypadkach i
tylko w
razie konieczności. Ponieważ
generowana przy tym ustawieniu klauzula
WHERE
jest krótsza, w naturalny sposób
zapewnia szybsze działanie od klauzul uwzględniających więcej kolumn. Przed
użyciem tej opcji należy skonsultować się z administratorem bazy danych, gdyż
nieumiejętne jej wykorzystanie może prowadzić do katastrofalnych skutków.
„Aktualizowalne” zapytania typu TQuery
Z reguły powinniśmy unikać aktualizowalnych zapytań
TQuery
. Zamiast nich
należy używać perspektyw serwera. Jest kilka powodów dla tego zalecenia. Po
pierwsze, uaktualnianie zapytania typu
TQuery
zrzuca cały ciężar analizy
zapytania SQL i aktualizacji związanych z nim tabel na aplikację-klienta, a nie na
serwer (zwłaszcza w przypadku BDE), chociaż jest to zadanie dla serwera.
724
Część IV
To serwer ma odpowiednie środki i zasoby do przeprowadzania skomplikowanych
operacji związanych z obsługą baz danych. Serwer ponadto lepiej zrealizuje swój
własny dialekt SQL niż aplikacja kliencka. Zapytania „aktualizowalne” są
wówczas elastyczniejsze, a uaktualnienia - szybsze.
Uaktualnienia buforowane
Z udostępnianego przez Delphi mechanizmu uaktualnień buforowanych (cached
updates) powinniśmy korzystać przy minimalizowaniu kodu SQL, który jest
wysyłany do serwera i do zmniejszenia liczby blokad (locks) bazy danych, które
powoduje nasza aplikacja. Uaktualnienia buforowane przechowuje się lokalnie - aż
do chwili ich przeprowadzenia w bazie danych. Wtedy dopiero wysyła się je do
serwera. Redukuje to liczbę blokad w serwerze i całkowity czas ich trwania, może
więc znacznie przyspieszyć działanie aplikacji. Oto sposób wykorzystania
uaktualnień buforowanych:
1. W oknie Inspektor Obiektów (Object Inspector) nadać wartość
True
własności
CachedUpdates
tego komponentu
DataSet
, którego uaktualnienia chcemy
buforować
2. Nadać odpowiednią wartość własności
UpdateRecordTypes
komponentu
DataSet
, w celu ustalenia typu kontroli widocznych wierszy w buforowanym
zbiorze. Własność ta może przyjmować następujące wartości:
rtModified
,
rtInserted
,
rtDeleted
i
rtUnmodified
3. Utworzyć procedurę obsługi zdarzenia
OnUpdateError -
tak, żeby
obsługiwała wszystkie błędy, jakie wystąpią podczas wywołania
ApplyUpdates
4. Wprowadzić modyfikacje danych komponentu
DataSet
podczas działania
aplikacji
5. Zachować modyfikacje za pomocą
ApplyUpdates
lub unieważnić je poprzez
CancelUpdates
Monitor SQL
Monitor SQL (SQL Monitor) to narzędzie użyteczne przy przeglądaniu kodu SQL,
który generuje nasza aplikacja. Możemy go np. wykorzystać w sytuacji, gdy
chcemy sprawdzić kod SQL, wygenerowany dla operacji, które wydają się działać
stanowczo zbyt wolno. Konieczne może okazać się zamiana kodu na perspektywę
lub procedurę pamiętaną w serwerze. Możemy też odkryć, że nasz sposób
przeszukiwania lub aktualizowania danych jest mało wydajny i aplikację trzeba
zoptymalizować. Niezależnie od konkretnej sytuacji, powinniśmy skorzystać
z dodatkowych informacji, które udostępnia Monitor SQL. Znajduje się on
w menu Database środowiska Delphi.
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
725
Buforowanie schematów
System BDE udostępnia obecnie tzw. buforowanie schematów (schema caching),
czyli lokalne zapamiętywanie informacji o strukturze obiektów bazy danych.
Włączenie tej opcji może przyczynić się do zredukowania liczby zapytań
wysyłanych przez aplikację do serwera w celu uzyskania danych katalogowych
o bazie danych. Efektem może być znaczne przyspieszenie aplikacji, ponieważ
w takim przypadku BDE nie musi ciągle otrzymywać tych danych z serwera.
Buforowanie schematów włączamy za pomocą narzędzia BDE Administration.
Z buforowaniem schematów wiążą się cztery parametry (tabela 25.1).
Tabela 25.1. Parametry konfiguracji BDE, które wpływają na buforowanie
schematów
Parametr Akcja
ENABLE SCHEMA CACHE
Włączenie/wyłączenie buforowania schematów
(parametr na poziomie sterownika)
SCHEMA CACHE SIZE
Liczba tabel, dla których należy buforować
dane schematów
SCHEMA CACHE TIME
Czas (w sekundach) buforowania
SCHEMA CACHE DIR
Katalog, w
którym schematy mają zostać
zapamiętane (parametr na poziomie sterownika)
Domyślną wartością parametru
SCHEMA CACHE TIME
jest
-1
, co oznacza, że
schematy pozostaną w buforze aż do zamknięcia bazy danych. Dopuszczalne
wartości parametrów
SCHEMA CACHE TIME
należą do przedziału od 1 do
2 147 483 647 sekund.
Włączenie buforowania schematów może zauważalnie wpłynąć na wydajność
aplikacji, zwłaszcza w przypadku połączeń w sieciach rozległych (WAN). Przy
buforowaniu program BDE zakłada, że schemat bazy danych pozostaje statyczny (i
jest to założenie naturalne). Wynika stąd jednak, że buforowanie schematów nie
nadaje się do każdej bazy danych. W szczególności nie powinniśmy korzystać
z buforowania w przypadku baz danych, w których:
często dodajemy lub usuwamy kolumny
często dodajemy lub usuwamy indeksy tabel
często zmieniamy atrybuty
NULL
/
NOT NULL
dla kolumn.
Jeżeli jednak użyjemy buforowania w przypadku baz danych, które się do tego nie
nadają, możemy spodziewać się następujących błędów SQL:
726
Część IV
Unknown Column (nieznana kolumna)
Invalid Bind Type (nieprawidłowy typ wiązania)
Invalid Type (nieprawidłowy typ)
Invalid Type Conversion (nieprawidłowa konwersja typu)
Column Not a Blob (kolumna, nie blob)
Filtry
Filtry w Delphi służą do kwalifikowania zbiorów wynikowych po stronie klienta
naszej aplikacji typu klient/serwer. Dla małych komponentów
DataSet
użycie
procedury obsługi zdarzenia
OnFilterRecord
może okazać się wydajniejsze,
niż ponawianie zapytań, ponieważ powoduje ograniczenie liczby interakcji
z serwerem bazy danych i siecią. Małe zbiory wynikowe będą i tak często
buforowane w całości po stronie komputera klienckiego, tak więc ich lokalne
filtrowanie ma sens, jeśli tylko jest możliwe. Aby uaktywnić lokalne filtrowanie
rekordów w aplikacji Delphi wykonujemy następujące czynności:
1. Otwieramy program obsługi zdarzenia
OnFilterRecord
komponentu
DataSet (
tak żeby uwzględniał/nie uwzględniał wierszy) z wykorzystaniem
jego parametru
Accept
2. Nadać wartość
True
własności
Filtered
komponentu
DataSet
3. Przy wykorzystaniu komponentu
DataSet
przez naszą aplikację, będzie on
widziany tak, jakby zawierał tylko wiersze spełniające kryteria filtrowania
UWAGA
Powyższe czynności dotyczą raczej zdarzenia
OnFilterRecord
niż własności
Filter
- przy ograniczaniu liczby wierszy wysyłanych przez aplikację. Wynika
to z faktu, iż ustawienie
Filter
nie tworzy automatycznie filtrów lokalnych.
Chociaż wyrażenie opisujące filtr na pewno ograniczy liczbę zwróconych wierszy,
to ustawienie filtra z komponentem
Table
, połączonym z serwerem bazy danych,
spowoduje tylko modyfikację tworzonej klauzuli
WHERE
. Tak więc mamy tutaj do
czynienia z filtrem odległym, a nie lokalnym.
Komponenty TField
Powinniśmy je stosować (wszędzie, gdzie tylko można) zamiast własności
Fields
typu
TDataSet
lub
FieldByName
. Trwałe komponenty
TField
są
wydajniejsze, gdyż przechowują podstawowe informacje o polu razem z aplikacją,
dzięki czemu nie musi ona ich odtwarzać z BDE. Są one również bezpieczniejsze,
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
727
ponieważ automatycznie zgłaszają wyjątek, jeśli zmienił się typ danych kolumny.
Natomiast funkcja
FieldByName
oraz własność
Fields
muszą - by uzyskać
podstawowe dane o kolumnie - przeszukiwać dane o schemacie tablicy. Są więc
one wolniejsze i bardziej zawodne. Komponenty
TField
są łatwiejsze w użyciu,
bezpieczniejsze i odporniejsze na modyfikacje obiektów bazy danych.
Korzystanie ze słownika danych
Autor uważa, że reguły logiki aplikacji należy w pierwszej kolejności umieszczać
w serwerze, jeżeli w ogóle jest to możliwe. W pewnych jednak sytuacjach okaże
się, że reguły logiki aplikacji musimy umieścić w naszej aplikacji klienckiej.
Wbudowując reguły logiki aplikacji w aplikację, korzystajmy jak najczęściej
z udostępnianego przez środowisko Delphi słownika danych (Data Dictionary)
oraz jego zbiorów atrybutów (Attribute Sets). Po zdefiniowaniu w słowniku
danych reguł logiki aplikacji dotyczących strony klienta, resztę reguł definiujemy
z wykorzystaniem komponentów
DataSet
i atrybutów
TField
. Definiując
reguły logiki aplikacji po stronie klienta z użyciem słownika danych, a nie
w ramach konkretnej aplikacji, zapewniamy ich większą dostępność dla innych
aplikacji.
Pola tylko do odczytu a DBText
Do zapewnienia, że pole jest tylko do odczytu (read-only), używajmy komponentu
DBText (
a nie
DBEdit)
. Pola
DBText
zajmują mniej miejsca, a zapewniają tę
samą funkcjonalność. Możemy też rozważyć celowość skorzystania ze statycznych
komponentów
Label
dla kolumn danych, które nie mogą podlegać modyfikacjom
od chwili, gdy formularz zostanie wyświetlony na ekranie. Jeżeli dane mogą zostać
zmodyfikowane, musimy wykorzystać
DBText. W
przeciwnym razie możemy
zaoszczędzić na zasobach, wymaganych nawet dla tak prostych kontrolek obsługi
danych, jak
DBText
, i w zdarzeniu
OnShow
formularza - skorzystać z własności
Caption
komponentu
TLabel
. Używanie jak najprostszych kontrolek w naszej
aplikacji przyczyni się nie tylko do mniejszego obciążenia zasobów, ale też - ze
względu na mniejszą liczbę interakcji z bazą danych - do jej przyspieszenia.
Wielowątkowe aplikacje bazy danych
Jedną z najważniejszych zalet 32-bitowych systemów Windows jest umożliwienie
tworzenia aplikacji wielowątkowych. Wątki tworzymy w Delphi - za pomocą
obiektu
TThread
- w sposób łatwy i bezpieczny. Z wielowątkowości możemy też
skorzystać w aplikacjach baz danych. Niezwykle pożyteczne jest wykonywanie
zapytań w tle. Zapytanie o długim czasie realizacji można wykonać w jego
własnym wątku tak, by aplikacja mogła nadal działać niezależnie. Wątki można też
728
Część IV
wykorzystać do przyspieszenia dostępu do bazy danych. Jeżeli np. odczytujemy
rekordy ze zbioru plików systemowych i wstawiamy je do tabel w serwerze SQL,
to dla każdego pliku można przydzielić oddzielny wątek - tak, żeby wstawienia
z jednego pliku nie opóźniały wstawień z innego. Wielowątkowość można
wykorzystać w aplikacjach baz danych na wiele sposobów. Listingi od 25.1 do
25.4 przedstawiają prosty program bazy danych, korzystający z wielowątkowości
przy jednoczesnym dostępie do trzech wierszy na raz.
UWAGA
W tej przykładowej aplikacji wykorzystano alias IBLOCAL, dostarczany wraz ze
środowiskiem Delphi. Żeby go użyć musimy sprawdzić, czy uruchomiono już
serwer systemu InterBase oraz czy baza danych IBLOCAL jest dla niego dostępna.
Listing 25.1. Kod źródłowy projektu dla przykładowego
programu wykorzystującego wielowątkowość o
nazwie thrdex
program thrdex;
uses
Forms,
thrdex00 in ‘thrdex00.pas’ {Form1},
thrdex01 in ‘thrdex01.pas’;
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TForm1,
Form1);
Application.Run;
end.
Listing 25.2. Kod źródłowy modułu thrdex00.pas
- pierwszego
z
dwóch modułów przykładowego programu thrdex,
wykorzystującego wielowątkowość
unit thrdex00;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms, Dialogs,StdCtrls, DB, Grids, DBGrids,
DBTables, Thrdex01, ExtCtrls;
type
TForm1 = class(TForm)
Query1: TQuery;
DataSource1: TDataSource;
Button1: TButton;
Query2: TQuery;
DataSource2: TDataSource;
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
729
Database1: TDatabase;
Session1: TSession;
Session2: TSession;
Database2: TDatabase;
Query3: TQuery;
DataSource3: TDataSource;
DBGrid3: TDBGrid;
Button2: TButton;
Database3: TDatabase;
procedure Button1Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action:
➥
TCloseAction);
procedure Button2Click(Sender: TObject);
procedure Database1Login(Database: TDatabase;
➥
LoginParams: TStrings);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1:
TForm1;
implementation
var
QueryThread1, QueryThread2 : TQueryThread;
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Database1.Open;
Database2.Open;
QueryThread1:=TQueryThread.Create(Query1);
QueryThread2:=TQueryThread.Create(Query2);
QueryThread1.OnTerminate:=QueryThread1.OnTerm;
QueryThread2.OnTerminate:=QueryThread2.OnTerm;
Button1.Enabled:=False;
end;
procedure TForm1.FormClose(Sender: TObject; var Action:
TCloseAction);
begin
QueryThread1.Terminate;
QueryThread2.Terminate;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
with query3 do begin
730
Część IV
if active then close;
open;
end;
end;
procedure TForm1.Database1Login(Database: TDatabase;
LoginParams:
TStrings);
begin
LoginParams.Values[‘USER NAME’] := ‘SYSDBA’;
LoginParams.Values[‘PASSWORD’] := ‘masterkey’;
end;
end
Listing 25.3 Plik .DFM formularza dla pliku thrdex00.pas
z
modułem programu thrdex
object Form1: TForm1
Left = 106
Top = 67
Width = 595
Height = 434
Caption = ‘Form1’
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = ‘MS Sans Serif’
Font.Style = []
OnClose = FormClose
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left
=
72
Top
=
374
Width
=
121
Height
=
25
Caption = ‘Start Query Threads’
TabOrder
=
0
OnClick
=
Button1Click
end
object DBGrid3: TDBGrid
Left
=
72
Top
=
248
Width
=
497
Height
=
113
DataSource
=
DataSource3
TabOrder
=
1
TitleFont.Charset
=
DEFAULT_CHARSET
TitleFont.Color
=
clWindowText
TitleFont.Height
=
-11
TitleFont.Name = ‘MS Sans Serif’
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
731
TitleFont.Style
=
[]
end
object Button2: TButton
Left
=
240
Top
=
374
Width
=
145
Height
=
25
Caption = ‘Open Query in Main Thread’
TabOrder
=
2
OnClick
=
Button2Click
end
object Query1: TQuery
DatabaseName
=
‘dbthread1’
SessionName
=
‘Ses1’
SQL.Strings
=
(
‘select
*
from
EMPLOYEE’)
Left
=
16
Top
=
8
end
object DataSource1: TDataSource
DataSet
=
Query1
Left
=
16
Top
=
40
end
object Query2: TQuery
DatabaseName
=
‘dbthread2’
SessionName
=
‘Ses2’
SQL.Strings
=
(
‘select
*
from
SALES’)
Left
=
16
Top
=
80
end
object DataSource2: TDataSource
DataSet
=
Query2
Left
=
16
Top
=
120
end
object Database1: TDatabase
AliasName
=
‘IBLOCAL’
DatabaseName
=
‘dbthread1’
LoginPrompt
=
False
Params.Strings
=
(
‘USER
NAME=SYSDBA’
‘PASSWORD=masterkey’)
SessionName
=
‘Ses1’
OnLogin
=
Database1Login
Left
=
16
Top
=
152
end
object Session1: TSession
Active
=
True
SessionName
=
‘Ses1’
732
Część IV
Left
=
16
Top
=
224
end
object Session2: TSession
Active
=
True
SessionName
=
‘Ses2’
Left
=
16
Top
=
256
object
Database2:
TDatabase
AliasName
=
‘IBLOCAL’
DatabaseName
=
‘dbthread2’
LoginPrompt
=
False
Params.Strings
=
(
‘USER
NAME=SYSDBA’
‘PASSWORD=masterkey’)
SessionName
=
‘Ses2’
OnLogin
=
Database1Login
Left
=
16
Top
=
192
end
object Query3: TQuery
DatabaseName
=
‘dbdefaultthread’
SQL.Strings
=
(
‘SELECT
*
FROM
CUSTOMER’)
Left = 16
Top
=
288
end
object DataSource3: TDataSource
DataSet
=
Query3
Left
=
16
Top
=
320
end
object Database3: TDatabase
AliasName
=
‘IBLOCAL’
DatabaseName
=
‘dbdefaultthread’
LoginPrompt
=
False
Params.Strings
=
(
‘USER
NAME=SYSDBA’
‘PASSWORD=masterkey’)
SessionName
=
‘Default’
OnLogin
=
Database1Login
Left
=
16
Top
=
352
end
end
Listing 25.4. K
od źródłowy modułu thrdex01.pas programu
thrdex, wykorzystującego wielowątkowość
unit thrdex01;
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
733
interface
uses
Classes, Forms, DBTables, Windows;
type
TQueryThread = class(TThread)
private
{ Private declarations }
FQuery
:
TQuery;
protected
procedure
Execute;
override;
procedure
OpenQuery;
public
constructor
Create(Query:
TQuery);
procedure OnTerm(Sender : TObject);
end;
implementation
{ TQueryThread }
constructor TQueryThread.Create(Query: TQuery);
begin
inherited
Create(False);
FQuery := Query;
end;
procedure TQueryThread.OpenQuery;
begin
FQuery.Open;
With FQuery do begin
With Owner.Owner as TApplication do
➥
ProcessMessages;
While not EOF do Next;
Close;
With Owner.Owner as TApplication do
➥
ProcessMessages;
end;
end;
procedure TQueryThread.Execute;
var
Counter : Integer;
begin
{ Place thread code here }
For Counter:=0 to 100 do begin
OpenQuery;
If Terminated then exit;
end;
end;
734
Część IV
procedure TQueryThread.OnTerm(Sender : TObject);
begin
Application.MessageBox(PChar(‘Thread running
➥
‘+FQuery.Name+’ Âfinished.’),PChar(FQuery.Name),
➥
IDOK);
end;
end.
W programie powyższym definiuje się dwa komponenty
TSession
i dwa wątki
w tle. Każdy z tych wątków kolejno otwiera i zamyka dane zapytanie 100 razy.
Trzecie zapytanie można otworzyć na pierwszym planie. Jeżeli wpiszemy ten
program (lub wczytamy z dołączonego dysku CD) i wykonamy go, to przekonamy
się, że wątki w tle i wątki na pierwszym planie nie kolidują ze sobą. Każdy z nich
wykonuje się niezależnie od pozostałych. Możemy - dla zapytania
pierwszoplanowego - wiele razy klikać przycisk
Open
, aby je ponownie wykonać,
bez wywierania jakiegokolwiek wpływu na zapytania działające w tle. Delphi
umożliwia wykorzystanie wielowątkowości w aplikacjach typu klient/serwer do
przydzielania długo realizowanym zadaniom ich własnych wątków. Powyższy kod
źródłowy zawiera wiele praktycznych szczegółów dotyczących wielowątkowości.
Optymalizacja wydajności serwera
Na optymalizację po stronie serwera składa się usprawnianie kodu SQL, ustawień
sieciowych, konfiguracji bazy danych itd. Rzecz oczywista, optymalizacja serwera
wpływa pozytywnie na wszystkie korzystające z niego aplikacje, nie tylko
aplikacje Delphi.
Optymalizowanie konfiguracji serwera
Jest wiele metod optymalizacji serwerów baz danych w celu zwiększenia
wydajności. Na początku mało który serwer jest optymalnie dopasowany do
potrzeb choćby tylko nieco bardziej skomplikowanych aplikacji. Oto kilka
potencjalnych przedmiotów optymalizacji.
Pamięć
Autor często obserwuje, że chociaż serwer ma mnóstwo zainstalowanej pamięci
RAM, jego oprogramowanie obsługi baz danych nie zostało skonfigurowane
w sposób, umożliwiający jej wykorzystanie. Większość platform nie umie
automatycznie dopasować systemu do sprzętu, i trzeba przeprowadzić ich „ręczną”
konfigurację. Ogólnie rzecz ujmując, oprogramowaniu serwera musimy oddać do
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
735
dyspozycji tyle pamięci RAM, ile tylko możemy - bez powodowania
stronicowania (page swapping).
Buforowanie
Praktycznie rzecz ujmując, powinniśmy wyłączyć buforowanie na poziomie
systemu operacyjnego i zostawić resztę serwerowi baz danych. Wie on więcej od
nas o naszych danych i ich logicznej strukturze, i zwykle podejmuje lepsze decyzje
co do przedmiotu buforowania.
Innym ważnym zagadnieniem jest tzw. rozrost bufora procedur (procedure cache
bloat). Zarówno Sybase, jak i Microsoft SQL Server dzielą pamięć buforową na
dwie części: bufor danych i bufor procedur (procedure cache). Bufor procedur
określa się w procentach całkowitej pamięci buforowej. Resztę pamięci zajmuje
bufor danych. Oznacza to, że w systemach z dużą ilością pamięci RAM możliwy
jest przerost (ponad potrzeby) bufora procedur. Jeżeli np. zwiększymy pamięć - ze
128 do 256 megabajtów - to podwajamy też w efekcie rozmiar bufora procedur.
Jeżeli dołożyliśmy tyle pamięci w celu zwiększenia bufora danych, a nie
zmieniliśmy określenia wielkości bufora procedur, to część pamięci RAM
pozostanie niewykorzystana. Należy wówczas rozważyć obniżenie współczynnika
procentowego dla bufora procedur.
Procesory
Większość nowoczesnych systemów DBMS umożliwia rozproszenie obciążenia na
wiele procesorów. Na przykład w systemie Microsoft SQL Server konfigurujemy
bitową maskę powinowactwa (affinity mask), określającą, które procesory (w
komputerze wieloprocesorowym) serwer może wykorzystać. Sybase ma z kolei
parametr konfiguracyjny max online engines, określający, ile procesorów może
użyć w komputerze wieloprocesorowym. Najelastyczniejszy jest system Oracle,
w którym obciążenie serwera można rozproszyć nawet na wiele komputerów,
z wykorzystaniem udostępnianej przez ten system techniki serwerów
równoległych. Zauważmy jednak, że dodawania procesorów nie warto i nie można
kontynuować w nieskończoność. Podwojenie liczby procesorów nie gwarantuje
podwojenia wydajności. Dlaczego? Ponieważ wąskie gardło dla operacji DBMS
stanowią urządzenia wejścia/wyjścia, a nie procesory. Z drugiej jednak strony
urządzeniami tymi również zarządza procesor - tak że przyspieszenie
przetwarzania może również przyczynić się do pewnego zwiększenia ogólnej
wydajności.
Asynchroniczne wejście/wyjście
Większość producentów platform DBMS udostępnia asynchroniczne operacje
wejścia/wyjścia dla napędów dyskowych. Oczywiście asynchroniczny odczyt
i zapis danych jest lepszy od synchronicznego. Jeżeli nasza platforma obsługuje
736
Część IV
asynchroniczną komunikację wejścia/wyjścia, sprawdźmy, czy wpływa ona
dodatnio na wydajność. Zauważalna poprawa wydajności nastąpi najpewniej
w przypadku korzystania z technologii smart drive lub RAID. Już chociażby ze
względu na większą liczbę urządzeń, asynchroniczny odczyt/zapis zwiększa
(fizycznie) przepustowość.
Protokołowanie transakcji
Protokoły transakcji/wycofań powinniśmy umieszczać na innych urządzeniach, niż
dane. Nie tylko poprawia do wydajność, ale zwiększa niezawodność w zakresie
odzyskiwania danych po awarii. Powinniśmy też rozważyć celowość
zaoszczędzenia serwerowi pracy związanej z zarządzaniem protokołami, zwłaszcza
w przypadku właśnie tworzonych baz danych lub takich, których pełne kopie
bezpieczeństwa tworzymy w trakcie rutynowego zachowywania danych. Np. -
zarówno w systemie Microsoft SQL Server, jak i w Sybase SQL Server włączenie
opcji
trunc. log on chkpt
powoduje usuwanie rekordów dotyczących zakończonych
transakcji z protokołu - za każdym razem, gdy występuje kontrola systemowa
(system checkpoint). Dzięki temu protokół jest względnie mały, operacje na nim
wykonują się szybciej, a ogólna wydajność systemu poprawia się.
OSTRZEŻENIE
W systemach użytkowych należy ostrożnie korzystać z opcji usuwania rekordów
z protokołu. Uaktywnienie usuwania rekordów oznacza brak możliwości
wykonania kopii bezpieczeństwa protokołu. To zaś znaczy, że nie można
wykonywać kopii przyrostowych (incremental backup) i że po ewentualnej awarii
systemu nie będzie sposobu odtworzenia ostatnich modyfikacji. Musimy
zdecydować, czy automatyczne uaktywnianie usuwania rekordów z protokołu
transakcji odpowiada naszym celom. Czasem jest ono użyteczne, trzeba jednak
dokładnie wiedzieć, co się robi.
Optymalizowanie zapytań
W całej książce pojawia się w różnych momentach termin optymalizator zapytań
(query optimizer). Czym dokładnie on jest i co robi? Wszystkie lepsze platformy
optymalizują zapytania SQL, kierowane do nich od klientów. Analizują przysłany
kod SQL i ustalają (z różnym zresztą skutkiem) najbardziej wydajny sposób jego
wykonania. Analizę tę wykonuje właśnie optymalizator zapytań. W przypadku
większości serwerów strategię opracowaną przez optymalizator określa się jako
plan wykonania zapytania (query execution plan). Podczas układania tego planu
brane są pod uwagę takie czynniki, jak dostępność indeksów, obszar dysku do
przeszukania, statystyka dotycząca dystrybucji kluczy w indeksach itd. Najczęściej
plan taki jest optymalny, czasem jednak serwerowi trzeba pomóc w jego ułożeniu.
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
737
Pomagamy optymalizatorowi zapytań
Jednym ze sposobów pomocy jest aktualizacja statystyk dotyczących indeksu,
które przechowuje optymalizator. Zawierają one informacje o rozkładzie wartości
kluczowych w indeksie. Zapewniając aktualność tych informacji, pomagamy
optymalizatorowi zapytań w podejmowaniu właściwych decyzji.
W InterBase statystykę indeksu uaktualniamy za pomocą następującej konstrukcji:
SET STATISTICS INDEX INVOICES03
gdzie
INVOICES03
jest nazwą indeksu do ponownego przetworzenia.
W systemach Sybase i Microsoft kod jest następujący:
UPDATE STATISTICS INVOICES.INVOICES03
gdzie
INVOICES
jest nazwą tabeli, dla której zbudowano indeks, a
INVOICES03
jest nazwą samego indeksu. Ponieważ indeksy w SQL Server są unikalne tylko
w ramach swoich tabel, w komendzie musi wystąpić nazwa tabeli. Z drugiej strony
wszystkie indeksy można od razu utworzyć dla danej tabeli, omijając nazwę
konkretnego indeksu:
UPDATE STATISTICS INVOICES
W systemie Oracle statystykę indeksu uaktualniamy za pomocą komendy
ANALYZE
. Zbiera ona statystyki dotyczące tabel, indeksów i klastrów (clusters).
Poniższa konstrukcja służy do uaktualnienia statystyki jednego indeksu:
ANALYZE INDEX CUSTOMER03 COMPUTE STATISTICS;
Statystykę dla tabeli i jej indeksu można obliczyć następująco:
ANALYZE TABLE CUSTOMER COMPUTE STATISTICS CASCADE;
Statystykę dla całego klastra (łącznie z tabelami i indeksami) otwieramy zgodnie
z poniższym zapisem:
ANALYZE CLUSTER acctrecv COMPUTE STATISTICS CASCADE;
Wyświetlanie planu optymalizatora zapytań
Większość serwerów SQL umożliwia wyświetlenie planu działań serwera podczas
wykonywania naszego zapytania. Plan w systemie InterBase wyświetlamy za
pomocą edytora WISQL. W oknie dialogowym
Basic ISQL Set Options
z menu
WISQL’s Session
musimy w tym celu zaznaczyć opcję
Display Query Plan
(wyświetlenie planu dla zapytania), zgodnie z rysunkiem 25.1.
738
Część IV
Powyższe czynności są równoważne komendzie
SET PLAN ON
.
Komenda
SET STATS ON
wyświetla informacje statystyczne o każdym
zapytaniu po jego uruchomieniu. Nie należy jej mylić ze wspomnianą wcześniej
komendą
SET STAISTICS
(która uaktualnia informacje dotyczące indeksów).
W systemach Sybase i Microsoft, do wyświetlania planu optymalizatora służy
komenda
SET SHOWPLAN ON
. W połączeniu z opcją
SET NOEXEC
możemy za
jej pomocą przejrzeć ułożony przez serwer plan wykonania zapytania, bez
konieczności jego uruchamiania.
Informacje statystyczne związane z wykonaniem zapytania możemy również
przeglądać poprzez komendę
SET STATISTICS
systemu SQL Server.
Wyrażenie
SET STATISTICS IO ON
powoduje, że system SQL Server
wyświetli statystykę operacji wejścia/wyjścia dla każdego wykonywanego przezeń
zapytania. Z kolei wyrażenie
SET STATISTICS TIME ON
spowoduje
wyświetlenie informacji o
uzależnieniach czasowych dla już wykonanego
zapytania.
Plan w systemie Oracle wyświetla się za pomocą komendy
EXPLAIN PLAN
PL/SQL
. Komendzie
EXPLAIN PLAN
przekazujemy wyrażenie, które chcemy
wykonać, a Oracle kieruje opracowany przez siebie plan do specjalnej tablicy.
Jeżeli wcześniej włączyliśmy optymalizację według kosztów, względny koszt
zapytania również zostanie obliczony.
Dzięki zapoznaniu się z planem, wygenerowanym przez optymalizator zapytań,
lepiej zrozumiemy przyczyny takich, a nie innych zachowań zapytań i sami
nauczymy się lepiej przeprowadzać optymalizację. Rysunek 25.2 ilustruje
sytuację, w której plan optymalizatora uwidacznia nam, że dane zapytanie nie
wykorzystuje indeksu.
Słowo
NATURAL
w
planie wykonania wskazuje na to, że tablica jest właśnie
przeszukiwana według naturalnego porządku wierszy (tzn. beż użycia indeksu).
Przepiszmy teraz zapytanie tak, żeby indeks został w nim wykorzystany. Rysunek
25.3. ilustruje nowe zapytanie i plan, wygenerowany dla niego przez system
InterBase.
Rysunek 25.1.
Okno dialogowe
Basic ISQL Set
Options
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
739
Przykład powyższy powinien uświadomić nam, jak ważna jest umiejętność
przeglądania planów generowanych przez serwer. Dzięki uważnemu przejrzeniu
planu wykonania zapytania możemy lepiej rozeznać się w możliwych sposobach
jego optymalizacji.
Plan wymuszony
InterBase udostępnia rozszerzenie komendy
SELECT
, które pozwala nam nakazać
optymalizatorowi, żeby zamiast opracowywania własnego planu, użył naszego.
Oto odpowiednia konstrukcja składniowa:
SELECT LastName, FirstName
FROM CUSTOMER
PLAN (CUSTOMER ORDER CUSTOMER03)
ORDER BY LastName
Rysunek 25.2.
Plan wykonania
zapytania bez
użycia indeksu
Rysunek 25.3.
Plan wykonania
zapytania
z użyciem indeksu
740
Część IV
Na szczególną uwagę zasługuje klauzula
PLAN
. Pierwszy argument informuje
optymalizator, której tabeli związanej z zapytaniem dotyczy plan, a drugi - co jest
naszym celem (w tym przypadku jest nim uporządkowanie tabeli). Ostatni
argument informuje optymalizator, czego konkretnie chcemy użyć do
przeprowadzenia naszego planu - w tym przypadku jest to indeks
CUSTOMER03
.
Możliwość taka okazuje się użyteczna w sytuacjach, gdy mamy podstawy sądzić,
że optymalizator nie przyjął najlepszego strategii przy optymalizacji zapytania.
W systemie SQL Server plan możemy wymusić na kilka sposobów. Pierwszy
z nich służy do wymuszenia zastosowania konkretnego indeksu. Odpowiednia
konstrukcja składniowa wygląda następująco:
SELECT LastName, FirstName
FROM CUSTOMER
ORDER BY LastName
Zwróćmy uwagę, że cyfrę
3
ujęto w nawiasy. Informuje to optymalizator, że musi
wykorzystać trzeci indeks utworzony dla tabeli CUSTOMER (dlatego też dobrze
jest, gdy bazą nazwy indeksu jest nazwa tabeli, po której umieszczono liczbę;
zapamiętanie numeru indeksu jest wówczas banalnie proste).
Podanie liczby 0 wymusiłoby przeszukanie tabeli, a
podanie liczby 1 -
wykorzystanie wewnętrznego indeksu (clustered index) tabeli, jeżeli istnieje.
Możemy również podać nazwę indeksu, np.:
SELECT LastName, FirstName
FROM CUSTOMER (INDEX = CUSTOMER03)
ORDER BY LastName
Drugim sposobem jest wymuszenie na optymalizatorze systemu SQL Server
dołączenia tabel w konkretnej kolejności. Do tego celu służy komenda
SET
FORCEPLAN
. Włączenie opcji
FORCEPLAN
(dosł. wymuszenie planu) wymusi
złączenie tabel w kolejności podanej w klauzuli
FROM
zapytania. Przeważnie
dobrana przez optymalizator kolejność łączenia jest najlepsza, ale nie zawsze.
Jeżeli w wyniku wykonania komendy
SHOWPLAN
mamy podstawy sądzić, że
optymalizator nie dokonał trafnego wyboru, powinniśmy włączyć opcję
FORCEPLAN
i podać kolejność w klauzuli
FROM
naszego zapytania.
Optymalizacja wydajności w sieci
Niniejszy podrozdział zawiera szereg wskazówek dotyczących optymalizacji
wydajności naszych aplikacji typu klient/serwer pod kątem komunikacji poprzez
sieć. Szczególnie częstym źródłem wąskich gardeł w komunikacji między
klientami a serwerami są sieci rozległe (WAN).
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
741
UWAGA:
Termin sieci rozległe, czyli WAN (Wide Area Network) dotyczy sieci
komunikujących się poprzez linie cyfrowe o przepustowości 56Kb/s lub większej.
Ponieważ nawet tak szybka linia jak T-1 charakteryzuje się przepustowością równą
zaledwie jednej siódmej przepustowości sieci Ethernet (10Mb/s), kwestiom
optymalizacji należy, w odniesieniu do WAN, poświęcić szczególnie dużo uwagi.
Uwagi poniższe dotyczą optymalnego konfigurowania połączeń bazy danych
poprzez sieć:
Szybsza sieć (np. 100 megabitów na sekundę) zwiększa wydajność aplikacji
z niej korzystających, w tym aplikacji typu klient/serwer
Znaczący wpływ na wydajność mogą mieć szybkie karty sieciowe, dobra
segmentacja sieci i inne, fizyczne aspekty systemu. Przypuszczalnie jest to
oczywiste, ale czyż nigdy nie zdarzyło się nam przeoczyć rzeczy oczywistej?
Jeżeli protokół sieciowy obsługuje wielkie pakiety (oversized packets) (por.
Large Internet Packet firmy Novell), możemy spróbować obniżyć liczbę
pakietów przesyłanych przez sieć. Im mniej przekazujemy ich przez względnie
wolną sieć WAN, tym lepiej
Sensowne może okazać się dynamiczne, a nie automatyczne tworzenie
formularzy o drugorzędnym znaczeniu. Każde nowe połączenie sieciowe
obciąża dodatkowo sieć. We względnie szybkich sieciach lokalnych jest to
często niezauważalne. W przypadku połączeń WAN, jednakże, takie dodatkowe
obciążenie może znacząco obniżyć wydajność. Musimy jednak wyważyć dwa
przeciwstawne aspekty: z jednej strony powolność tworzenia i usuwania
formularzy, z drugiej - redukcję obciążenia sieci, i zdecydować, co bardziej się
nam opłaca
Tworząc aplikacje komunikujące się poprzez sieć rozległą, musimy dość
sceptycznie traktować możliwości, jakie dają nam
DBGrid
,
DBCrlGrid
itp.
Są to kontrolki odpowiednie dla sieci lokalnych, często jednak wymagają
znacznie więcej danych, niż aplikacja rzeczywiście potrzebuje w danym
przypadku. Powoduje to zbędne zajmowanie zasobów sieciowych
i spowolnienie działania aplikacji
Należy minimalizować powiązania między głównymi, a
szczegółowymi
strukturami danych. Każda czynność wykonana na głównej tabeli generuje
szereg zapytań dotyczących tabeli szczegółowej. Formularz z ich dużą ilością
może bardzo zwiększyć ruch w sieci
Warto zastanowić się, czy w odniesieniu do danych tylko do odczytu (read-
only data), które znajdują się w tabelach o drugorzędnym znaczeniu, nie lepiej
jest użyć komponentów typu
Label,
wraz ze statycznymi operacjami
742
Część IV
wyszukiwania i
lokalizacji (Lookup/Locate), zamiast komponentów typu
DBText
. Chociaż te ostatnie są łatwiejsze w konfiguracji, znacznie obciążają
zasoby sieciowe. Zbyt duże obciążenie sieci powodowane przez aplikację
oznacza w efekcie, że jest ona bezużyteczna
Rozważyć sensowność nadania wartości
False
własności
KeepConnection
komponentu
TDatabase
. Chociaż spowalnia to
aplikację za każdym razem, gdy ponownie łączy się ona z serwerem, ma tę
zaletę, że zostawia użytkownikom systemu odległego maksymalnie dużo
zasobów sieciowych. Ma to kluczowe znaczenie przy aplikacjach pracujących
w sieci rozległej. Inną zaletą takiej strategii jest minimalizacja połączeń
użytkowników z naszym serwerem baz danych. Jeżeli dysponuje on tylko
ograniczoną liczbą połączeń współbieżnych, praca w trybie usuwania połączeń
nieaktywnych (
KeepConnections
:=
False
) przyczyni się do zwiększenia
liczby użytkowników, którzy mogą skorzystać z naszego systemu.
Przed wysłaniem z naszego serwera przez sieć dużych ilości danych do naszej
aplikacji, należy je zblokować z procedurami pamiętanymi i zapytań. Innymi
słowy, aplikacja nie powinna przetwarzać danych wiersz po wierszu - jest to
zadanie procedur pamiętanych w serwerze.
Uaktualnienia należy grupować w postaci uaktualnień buforowanych, tak żeby
nie musiały być używane po jednym na raz.
Włączyć buforowanie schematów BDE tak, żeby odbywało się ono w lokalnym
komputerze. Buforowanie włącza się z poziomu BDE Configuration - przez
nadanie parametrowi
ENABLE SCHEMA CACHE
wartości
True
Wykorzystywać udostępniane przez Delphi lokalne filtry do kwalifikowania
małych zbiorów wynikowych na poziomie komputera lokalnego. Dzięki temu
korzystanie z tych zbiorów nie będzie wymagało sieci
Używać udogodnień BatchMove środowiska Delphi - do przesuwania wielu
wierszy między tabelami; niczego nie robić tylko po jednym wierszu na raz
Protokół TCP/IP czeka zwykle na zapełnienie pakietu przed jego wysłaniem.
Jest to działanie domyślne. Może ono pogorszyć wydajność, kiedy rozmiar
danych do wysłania przekracza aktualny rozmiar pakietu. Niewielka zmiana
konfiguracji pozwala na wyłączenie tego opóźnienia w prawie każdym
serwerze. Administrator systemu wie, jak to zrobić. Zwykle służy do tego opcja
konfiguracji dla całego komputera, albo samego programu obsługi serwera baz
danych. Np. platforma Sybase SQL Server w wersji System 11 udostępnia
specjalny przełącznik sp_configure, który kontroluje składowanie pakietów
TCP (TCP packet batching). W wersji System 10 realizuje się to poprzez flagę
śledzenia
(1610)
z poziomu linii komend serwera danych. W serwerach
Rozdział 25
Optymalizacja
wydajności w aplikacjach typu klient/serwer
743
pracujących w Systemie 10 opcję
TCP_NODELAY
włączamy w linii komend
za pomocą
-T1610
W niektórych systemach operacyjnych precyzyjna optymalizacja ustawień
przełączników, które dotyczą protokołu TCP/IP, może zwiększyć wydajność
naszej aplikacji. Autor zamieścił poniżej listę znanych mu parametrów
w systemie UNIX:
ndd -set /dev/tcp tcp_rexmit_interval_max value
ndd -set /dev/tcp tcp_conn_req_max value
ndd -set /dev/tcp tcp_close_vait_interval value
ndd -set /dev/tcp tcp_keep_alive_interval value
Szczegóły składniowe są różne w
różnych platformach, ale większość
zaawansowanych serwerów umożliwia nam szeroką kontrolę protokołu TCP/IP.
Jeżeli nasze aplikacje klienckie komunikują się z serwerem baz danych za
pośrednictwem tego właśnie protokołu, użyteczność systemu możemy znacznie
poprawić poprzez zoptymalizowanie jego działania