Projektując aplikacje, które używają dokumentowej bazy danych CosmosDB bardzo często stajemy przed wyzwaniem oszacowania ich kosztu oraz ich późniejszej optymalizacji. Zadanie to nie jest trywialne. Jeśli chodzi o koszt to zawsze możemy użyć kalkulatora. Wystarczy, że podamy tam parametry rozwiązania (liczbę nowych dokumentów, odczytów, …) oraz wgramy przykładowe dokumenty. Po uzupełnieniu tych informacji otrzymamy estymację kosztu rozwiązania:

Na podstawie tych informacji możemy zastanowić się, czy chcemy skorzystać z bazy CosmosDB? Czy nas na nią stać, czy może chcemy pomyśleć nad innym rozwiązaniem? Jeśli zdecydujemy się na CosmosDB to z moich obserwacji wynika, że programiści nie mają najmniejszych problemów z wystartowaniem prac z perspektywy użycia bibliotek do komunikacji z bazą danych. Czasami trochę więcej czasu potrzeba na przejście na inne tory myślenia i zaprzestania traktowania bazy CosmosDB jako relacyjnej.

Prędzej, czy później pojawia się potrzeba optymalizacji kosztów zapytań. Powody mogą być różne, a to przekraczamy budżet, a to któreś zapytania zaczynają działać zbyt wolno. Generalnie musimy coś zrobić z naszą bazą danych lub zapytaniami. W takich sytuacjach nigdy nie powinniśmy działać na oślep. Za każdym razem powinniśmy odnieść się do statystyk wykorzystania naszego systemu i z dużym prawdopodobieństwem zacząć optymalizację od najczęściej występujących zapytań.

I w tym przypadku pojawia się drobna przeszkoda. Większość z nas jest przyzwyczajona do bardzo dokładnych statystyk, które są udostępniane przez Azure SQL:

Analiza danych z Query Performance Insight pozwala na rozpoczęcie prac optymalizacyjnych w przypadku Azure SQL.

W przypadku CosmosDB nie jest już tak różowo… Dostępnych jest tylko kilka podstawowych charakterystyk. To co możemy sprawdzić pod kątem RU, to tylko ile jednostek RU zużywa nasza baza danych i czy nie przekracza limitu przydzielonego na partycję. Z tych danych ciężko jest wywnioskować, gdzie powinniśmy rozpocząć optymalizację. Możemy natomiast zobaczyć, czy dobrze dobraliśmy poziom RU do naszego rozwiązania.

Ja natomiast chciałbym mieć możliwość monitorowania na poziomie każdego zapytania. Z możliwością podejrzenia dokładnie zapytania, które zostało wysłane do bazy danych, jak również na poziomie ogólnym, gdzie możemy zagregować dane na podstawie podanego typu. Tak abym mógł albo uzyskać informację o konkretnym zapytaniu:

lub też w postaci tabelarycznej:

Na podstawie tych danych możemy już spróbować zaplanować optymalizację naszych zapytań. Jak widzicie przechowujemy następujące dane:

  • nazwę typu zapytania,
  • czas zapytania,
  • treść zapytania,
  • nazwę kolekcji, której zapytanie dotyczy,
  • koszt zapytania.

I najważniejsze możemy te dane wyeksportować i przeanalizować je zgodnie z naszymi potrzebami.

Aby to osiągnąć wystarczy napisać rozszerzenie do CosmosDB, które loguje dodatkowe parametry do Application Insights. Kod rozszerzenia wygląda następująco:

public static class ApplicationInsightsQueryTracker

{
  private static readonly TelemetryClient TelemetryClient = new TelemetryClient();
  public static string DependencyName { get; set; } = "";

  public static async Task<FeedResponse<T>> ExecuteWithStatsLogging<T>(this IQueryable<T> queryable, string commandName = null, string operationId = null)
  {
    var documentQuery = queryable.AsDocumentQuery();
    var now = DateTimeOffset.UtcNow;
    var stopwatch = Stopwatch.StartNew();
    var response = await documentQuery.ExecuteNextAsync<T>();
    stopwatch.Stop();
    LogStats(now, stopwatch.Elapsed, response.RequestCharge, commandName ?? string.Empty, operationId, response.ContentLocation ?? String.Empty, queryable.ToString());

    return response;
  }

  public static void LogStats(DateTimeOffset start, TimeSpan duration, double requestCharge, string commandName, string operationId, string contentLocation, string query)
  {
    var dependency = new DependencyTelemetry(DependencyName, commandName, start, duration, true);
    if (operationId != null)
    {
      dependency.Context.Operation.Id = operationId;
    }

    dependency.DependencyTypeName = "CosmosDB";
    dependency.Properties["contentLocation"] = contentLocation;
    dependency.Properties["query"] = query;
    dependency.Properties["requestCharge"] = requestCharge.ToString(CultureInfo.InvariantCulture);

    TelemetryClient.TrackDependency(dependency);
  }
}

Następnie wystarczy w momencie wywołania zapytania do CosmosDB wywołać logowanie:

IQueryable<Family> query = client.CreateDocumentQuery<Family>(
UriFactory.CreateDocumentCollectionUri(DatabaseName, CollectionName), "SELECT * FROM ....", queryOptions);

query.ExecuteWithStatsLogging("FamilyByLastName");

Dodatkowo wywołując zapytanie możemy ustawić jego nazwę, aby w przyszłości łatwo można było grupować informacje o zapytaniach tego samego typu.