8f6a1a9c9e104f789170c386e36d225b

Skalowanie Azure SQL Database przy pomocy TSQL i REST API

Jedną z niezaprzeczalnych zalet bazy Azure SQL Database jak i wielu innych usług Platform As a Service wewnątrz chmury Azure jest to, że jesteśmy w stanie dostosować poziom wydajności do określonych wymagań naszych aplikacji czy też procesów. Skalowanie, bo o nim tutaj mowa może odbywać się całkowicie automatycznie bez naszej ingerencji lub też na żądanie użytkownika.
O skalowaniu automatycznym dostępnym m.in w usługach Serverless dziś nie będziemy mówić ale powiemy sobie w jaki sposób jesteśmy w stanie zmieniać poziom wydajności Azure SQL Db z poziomu TSQL oraz REST API.

Na samym początku kilka słów wstępu na temat poziomów wydajności Azure SQL DB. Jak zapewne wiecie mamy wiele możliwości konfiguracji tej usługi od standardowego Azure SQL DB aż przez zbliżone do wydania On-Premise Managed Instance. Dodatkowo dochodzi nam cały szereg poziomów wydajności opartych o DTU czyli abstrakcyjną jednostkę wydajności będącą reprezentacją CPU, pamięci oraz operacji IO lub też poziomy wydajności oparte o vCore. W przypadku gdy mamy w użyciu wiele baz możemy współdzielić zasoby w ramach tzw. Elastic Pool dzięki czemu w niektórych scenariuszach mniejszym kosztem możemy obsłużyć więcej baz. Dużo tego prawda? A to jeszcze nie wszystko bo scenariuszów użycia jest naprawdę dużo, a mamy przecież jeszcze skalowanie horyzontalne/sharding czy wspomniane we wstępie instancje serverlessowe. Jedno co łączy te wszystkie aspekty pracy z bazą Azure SQL jest to, aby osiągnąć pożądany efekt wydajnościowy jak najmniejszym kosztem. W tym konkretnym aspekcie skalowanie jest jedną z najważniejszych o ile nie najważniejszą opcją – dlatego też postanowiłem napisać na ten temat kilka słów – zapraszam do lektury.

Przechodząc odrazu do meritum powiedzmy sobie gdzie możemy podjrzeć i zmienić tier wydajnościowy naszej bazy, Pierwszym najbardziej intuicyjnym miejscem jest oczywiście portal Azure. Wybierając określoną bazę i przechodząc na zakładkę Compute + storage znajdziemy słynny suwak pozwalający skalować usługę w górę bądź w dół:

Przesuwając suwak ustawiamy określony poziom wydajności bez potrzeby dodatkowej konfiguracji. Sama operacja nie trwa zbyt długo szczególnie jeśli skalujemy w obrębie pojedynczej grupy (np. ze standard S0 na standard S7). W przypadku gdy skalujemy pomiędzy grupami np. ze standard na premium może to zająć zdecydowanie więcej czasu ze względu na to, że w premium mamy do czynienia z innymi zasobami powoływanymi pod spodem co oczywiście trwa. Warto zatem zapamiętać, że jeśli jest taka możliwość to warto pomyśleć o skalowaniu w ramach pojedynczej grupy. Sama operacja zmiany tiera powoduje to, że następuje pewien moment “przełączenia” powodujący, że aktywne sesje zostaną przerwane – dlatego też skalowanie powinno nastąpić albo w zdefiniowanych wcześniej oknach utrzymaniowych albo też należy w kodzie naszej aplikacji/procesu zastosować logikę ponawiania operacji (ang. retry).

Podobne zastosowanie mają również komendy Azure CLI oraz Powershell:

Powershell:

Set-AzSqlDatabase -ResourceGroupName $resourceGroupName `
-ServerName $servername `
-DatabaseName $databasename `
-Edition "Standard" `
-RequestedServiceObjectiveName "S1"

Azure CLI:

az sql db update -g mygroup -s myserver -n mydb --edition Standard --service-objective S1 --max-size 250GB

Wspomniany wyżej suwak jak i Azure CLI/Powershell jest naturalną formą skalowania, która może być pomocna w przypadku gdy musimy reagować ad-hoc lub też zaraz po stworzeniu bazy. Dla tych, którzy używają pipeline CICD w jednym kroku tworzyć bazę z mniejszym tier np. Standard S0, a w kolejnym skalować w górę do żądanego tiera co znacznie upraszcza wiele rzeczy, a nie jest trudne w implementacji.

Powyższe metody to oczywiście nie jedyne dostępne możliwości. Drugą naturalną metodą jest oczywiście T-SQL posiadający składnię unikalną dla Azure SQL zmieniającą tier. Bieżący tier możemy sprawdzić używając widoku systemowego sys.database_service_objective:

SELECT * FROM sys.database_service_objectives

Widzimy zatem, że łatwo jesteśmy w stanie odczytać bieżący tier. Jego zmiana również jest bardzo prosta i sprowadza się do wywołania odpowiedniej komendy ALTER DATABASE – w moim przypadku zmienię tier na S0:

ALTER DATABASE [PurviewTest] 
MODIFY(EDITION='Standard' , SERVICE_OBJECTIVE='S0')

Powyższa komenda działa asynchronicznie tzn. w momencie jej wykonania dostaniemy status “sukces” co nie oznacza zmiany samego tiera, a jedynie zainicjowanie samej operacji. Dlatego też jeżeli chcemy mieć pewność, że wszystko zostało zmienione zgodnie z oczekiwaniami to należy przygotować pewnego rodzaju pętle w narzędziu zewnętrznym, która będzie sprawdzać czy zmiana się dokonała. Pamiętajmy o tym o czym wspomniałem wcześniej czyli o fakcie że sam moment przepięcia wyrzuci wszystkie połączenia i dlatego nasze narzędzie skalujące nie może być zawarte wewnątrz samej bazy. Możemy sobie wyobrazić, że mamy pętle WHILE, która będzie wykonywać wypisanego wyżej SELECTa aż zwrócony zostanie docelowy poziom wydajności – otóż takie podejście nie ma prawa bytu ponieważ taka pętla zostanie przerwana w momencie samego przełączenia tiera i dostaniemy błąd. Dlatego też warto zaimplementować coś takiego w zewnętrznym narzędziu, które cyklicznie będzie sprawdzać tier (np. Azure Automation albo Logic Apps). Wracając do naszego skryptu to po niezbyt długim czasie oczekiwania po jego uruchomieniu baza została zeskalowana do poziomu S0:

Jeśli chcielibyśmy skalować przy pomocy tej metody to pamiętajmy, że wykonanie komendy ALTER wymaga stosunkowo wysokich uprawnień dlatego też warto przygotować procedurę składowaną, która dynamicznie zmieni tier, a będzie wywoływana tak jakby uruchomił ją określona tożsamość (przydatna będzie tutaj składnia EXECUTE AS). Dzięki temu podejściu narzędzie mające skalować może mieć uprawnienie do wywołania procedury, a nie do ALTER DATABASE co czyni je znacznie bezpieczniejszym.

Przykładowa procedura skalująca wygląda następująco:

CREATE PROC [tools].[spSetDatabaseTier]
    @paramTargetTier CHAR(2)
AS
BEGIN
    SET NOCOUNT ON;
    /*

        This proc is changing service tier of current Azure SQL Db

        It will chanage tier and wait 2 minutes for effect

        Only standard tiers are allowed.
    */
    DECLARE @CurrentTier CHAR(2) =
    (
        SELECT 
            service_objective 
        FROM sys.database_service_objectives
    );

    DECLARE @ScalingQuery NVARCHAR(100)=
    'ALTER DATABASE '+QUOTENAME(DB_NAME())+' MODIFY(EDITION=''Standard'' , SERVICE_OBJECTIVE='''+@paramTargetTier+''')'
    PRINT @ScalingQuery
    IF @paramTargetTier = @CurrentTier
        BEGIN
            PRINT 'Target tier is the same as current tier'
        END
    ELSE
        BEGIN
            IF @paramTargetTier IN ('S0','S1','S2','S3','S4','S6','S7','S9')
                BEGIN
                    EXEC(@ScalingQuery) 
                END
            ELSE
                BEGIN
                    RAISERROR('Only following standard tiers from S0,S1,S2,S3,S4,S6,S7,S9 are available',16,1)
                END
        END
END
GO
W mojej procedurze dopuściłem tylko niektóre tiery bo nie chcę skalować do ani do Basic ani do Premium ale jest to oczywiście opcjonalna funkcjonalność. Jeśli chcielibyśmy być super poprawni powinniśmy dodać dodatkowe elementy chroniące nas przed SQL Injection jednakże w tej konstrukcji uznałem to za zbędne. Procedura ta jak już wspomniałem wyżej natychmiast zwróci nam sukces jak tylko Azure SQL przyjmie komende do przetworzenia. Dlatego też mamy drugą procedurę, która sprawdza tier – nie jestem zwolennikiem wysyłania zapytań adhoc z narzędzi zewnętrznych dlatego też na wszystko mam swoją procedurę 🙂
CREATE PROC [tools].[spGetDatabaseTier]
AS
BEGIN
    SET NOCOUNT ON;
    /*
        This proc returns current tier for current database.
    */
        SELECT 
            service_objective 
        FROM sys.database_service_objectives
END
GO

Powyższa procedura może być wywoływana z narzędzia zewnętrznego w celu sprawdzenia czy rzeczywiste przełączenie tiera zostało wykonane. Jako narzędzie zewnętrzne w naszej demonstracji wykorzystamy Azure Data Factory – przygotowany przeze mnie pipeline wygląda następująco:

Pierwszy komponent to nic innego jak wywołanie procedury skalującej, drugi z kolei to pętla, która będzie sprawdzać czy skalowanie nastapiło czy też nie. Konfiguracja wywołania procedury wygląda w następujący sposób:

Nieco ciekawiej wygląda konfiguracja pętli gdzie ustawiłem timeout pięciominutowy:

oraz sam warunek:

@equals('S3',activity('ACT_GEN_LKP_CheckDatabaseTier').output.value[0].service_objective)

Sprawdzam zatem czy output z aktywności ACT_GEN_LKP_CheckDatabaseTier jest równy “S3”. Aktywność ta znajduje się oczywiście wewnątrz pętli i zawiera wywołanie procedury [tools].[spGetDatabaseTier]:

Jak możecie zauważyć powyżej dodałem również zadanie typu Wait po to aby występowały 20 sekundowe przerwy pomiędzy poszczególnymi sprawdzeniami. Mając to wszystko skonfigurowane w ten sposób możemy skalować w górę jak i w dół nasza bazę danych np. przed jak i po procesie ETL co bywa naprawdę pomocne. Oczywiście wartość “S3” może zostać odpowiednio parametryzowana w ADF tak aby nie trzeba było zmieniać kodu w przypadku gdy będziemy chcieli skalować do nieco innych poziomów wydajności.

Jako to w chmurze bywa tą samą funkcjonalność możemy uzysakć na wiele różnych sposobów i nie inaczej jest w tym przypadku. Azure SQL jak każda usługa PaSS posiada REST API po którym możemy się odwołać do zasobu i ewentualnie go zmienić. Nie wchodząc w szczegóły samych wywołań RESTowych powiedzmy sobie jak takie żądanie ma wyglądać:

  • Metoda: PUT
  • Content-Type: Application/JSON
  • URI – pełne URI identyfikujące zasób jakim jest baza danych – poniżej pogrubiłem elementy, które należy podmienić:

https://management.azure.com//subscriptions/subscriptionId/resourceGroups/ResourceGroupName/providers/Microsoft.Sql/servers/SQLServer/databases/SQLDatabase?api-version=2017-10-01-preview

  • Body: ciało żądania zawierające operacje do wykonania – po raz kolejny pogrubiłem elementy do parametryzacji:

{
“location”: “Region“,
“sku”: {
“name”: “S3“,
“tier”: “Standard
}
}

Mając te elementy składowe żądania możemy je wywołać własciwie z każdego miejsca umożliwiającego taką operację jak np. Azure Functions, Azure Automation czy chociażby Logic App. Ja skorzystam z tego ostatniego i u mnie wygląda to w następujący sposób – po pierwsze stworzyłem zestaw parametrów odpowiadających pogrubionym powyżej elementom żądania:

Następnie użyłem komponentu HTTP gdzie umieściłem wskazane wyżej elementy. Zwróćcie proszę uwagę na fakt, że jako metodę uwierzytelnienia wybrałem System-assigned Managed Identity – oznacza to, że MI przypisane do tego Logic App musi mieć odpowiednie uprawnienia na docelowym zasobie aby móc tą operację wykonać:

Jako element wywołujący całego Logic App możemy wybrać według potrzeb np. Recurrence jeśli chcemy aby skalowanie było wykonywane cyklicznie albo po otrzymaniu requesta HTTP – ja wybrałem to pierwsze:

To na co nalezy zwrócić uwagę to fakt, że wywołanie przez REST API nie zachowuje się tak jak TSQL tzn. nie jest asynchroniczne. W tym wypadku jeśli Logic App zwróci sukces oznacza to, że skalowanie się zakończyło i rzeczywiście żądany tier jest już aktywny. Jeśli nie chcielibyśmy implementować logiki opartej o TSQL w Data Factory to nic nie stoi na przeszkodzie żeby wywołać takiego Logic App z poziomu ADF bądź też zaimplementować analogiczne odwołanie w samym ADF i jego Web Activity – zachęcam do eksperymentów.

Na ten moment to wszystko co chciałem Wam przekazać na ten temat. Mam nadzieję, że artykuł okazał się przydatny – jeśli tak i chcecie być na bieżąco z publikowanymi przez nas treściami zapraszam do polubienia nas na Facebooku oraz subskrypcji.

Leave a Reply