2016-05-03 3 views
9

Projekt ist MVC WebAPI basiert.Was ist der beste Weg, Ergebnisse einer json.net-Serialisierung im Speicher zwischenzuspeichern?

Wir übergeben den Berechtigungskontext eines Clients an unsere API-Server als serialisiertes JSON-Objekt in den Forderungsheadern der Anforderung. Dies ist kein riesiges Objekt: 6 Eigenschaften und eine Sammlung enum-basierter Schlüssel/Wert-Paare (bis zu 6 Elemente hier)

Die überwiegende Mehrheit der Anfragen an API tritt jede Minute (einige häufiger) aus dem gleichen Satz von Kunden. Wahrscheinlich 700-900 Clients (und wachsend), jede einzelne sendet die gleichen Ansprüche jede Minute.

Für jede Anfrage deserialisieren verschiedene Komponenten des Codes dieses Objekt wahrscheinlich 5-6 mal. Diese Deserialisierung verursacht einen erheblichen CPU-Verbrauch auf den Servern.

Was wäre der beste Weg, um diese Deserialisierungen im Speicher zwischenzuspeichern? Würde ein statisches Dictionary-Objekt mit Schlüsseln, die serialisierte JSON-Strings sind, gut funktionieren oder wäre es zu langsam, wenn diese Strings ziemlich groß wären?

EDIT: Jede Aktion eines jeden Controller wird durch dieses Attribut gefiltert, um sicherzustellen, dass Anrufe

public class AccountResolveAttribute : ActionFilterAttribute 
{ 
    public override void OnActionExecuting(HttpActionContext context) 
    { 
     var controller = (ControllerBase) context.ControllerContext.Controller; 
     var identity = (ClaimsIdentity) controller.User.Identity; 

     var users = identity.Claims 
      .Where(c => c.Type == ClaimTypes.UserData.ToString()) 
      .Select(c => JsonConvert.DeserializeObject<UserInformation>(c.Value)) 
      .ToList(); 

     var accountId = controller.ReadAccountIdFromHeader(); 

     if (users.All(u => u.AccountId != accountId)) 
     { 
      throw new ApplicationException(string.Format("You have no rights for viewing of information on an account Id={0}", accountId)); 
     } 
    } 
} 

Es richtigen Berechtigungen haben, werden Anrufe auch in der Basiscontroller, der die Ansprüche befragen, aber AccountResolve könnte wahrscheinlich cachen die Ergebnis der ersten Deserialisierung in den Controller, so dass diese Aufrufe nicht versuchen, erneut zu deserialisieren. Die Ansprüche sind jedoch immer wieder dieselben, und ich versuche nur einen Weg zu finden, nicht immer wieder die gleiche Zeichenfolge zu deserialisieren. Ich habe versucht, die Serialisierung Zeichenfolge als Schlüssel und Ergebnisobjekt in Speicher in einem globalen statischen ConcurrentDictionary, aber es scheint nicht geholfen haben

+0

Sie sagen, dass verschiedene Komponenten des Codes das Objekt deserialisieren. Wie gelangen die Komponenten aus Neugier in erster Linie zu den serialisierten Daten? Zum Beispiel: Ist es das Abrufen vom Hauptthread des aktuellen Threads, wird das Prinzipal als Parameter übergeben? –

+0

Eine andere Frage neben der, die ich gerade gefragt habe: Verwenden Sie einen IoC-Container, und wenn ja, welcher? –

+0

Yah, wir verwenden Autofac – Igorek

Antwort

1

Es scheint auf diese Frage zwei Aspekte zu sein:

  1. Was der Titel
  2. Etwas CPU-Zyklen fragt frisst; die Annahme ist, dass es auf die Deserialisierung von UserInformation Instanzen

Für 1., scheint wie ein ConcurrentDictionary passen würde unter der Annahme der Rechnung fällig ist, dass es wirklich eine ziemlich begrenzte Anzahl UserInformation Möglichkeiten (Sie dies in der Frage erwähnt) sind; Sonst würden Sie nicht nur die Kosten für die Serialisierung in Kauf nehmen, sondern auch etwas, das wie ein Speicherleck aussieht.

Wenn Sie sicher die Annahme machen, hier ein Beispiel:

public static class ClaimsIdentityExtensions 
{ 
    private static readonly ConcurrentDictionary<string, UserInformation> CachedUserInformations = new ConcurrentDictionary<string, UserInformation>(); 
    public static IEnumerable<UserInformation> GetUserInformationClaims(this ClaimsIdentity identity) 
    { 
     return identity 
      .Claims 
      .Where(c => c.Type == ClaimTypes.UserData) 
      .Select(c => CachedUserInformations.GetOrAdd(
       c.Value, 
       JsonConvert.DeserializeObject<UserInformation>)); 
    } 
} 

Sie erwähnt hatte, dass man versucht, ConcerrentDictionary zu verwenden, aber es half nicht. Ich wäre schockiert, wenn die Deserialisierung eines Objekts ein Nachschlagen in einem ConcurrentDictionary (wiederum unter Annahme der oben genannten Annahme) durchbrechen würde, selbst wenn die Schlüssel "lange" Strings wären. Ohne Beispiele für die UserInformation-Klasse ist es schwer, mit 100% Sicherheit von unserem Ende zu wissen ... hier ist jedoch ein Beispiel, das zeigt, dass die ConcurrentDictionary-Methode bei einer UserInformation mit einer AccountId-Eigenschaft den Brute-Force-Deserialisierungsansatz durchschlägt eine Größenordnung:

using System; 
using System.Collections.Concurrent; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 
using System.Security.Claims; 
using Newtonsoft.Json; 

namespace ConsoleApplication2 
{ 
    public class UserInformation 
    { 
     public int AccountId { get; set; } 
    } 

    public static class ClaimsIdentityExtensions 
    { 
     private static readonly ConcurrentDictionary<string, UserInformation> CachedUserInformations = new ConcurrentDictionary<string, UserInformation>(); 
     public static IEnumerable<UserInformation> GetUserInformationClaims(this ClaimsIdentity identity, bool withConcurrentDictionary) 
     { 
      if (withConcurrentDictionary) 
      { 
       return identity 
        .Claims 
        .Where(c => c.Type == ClaimTypes.UserData) 
        .Select(c => CachedUserInformations.GetOrAdd(
         c.Value, 
         JsonConvert.DeserializeObject<UserInformation>)); 
      } 

      return identity 
       .Claims 
       .Where(c => c.Type == ClaimTypes.UserData) 
       .Select(c => JsonConvert.DeserializeObject<UserInformation>(c.Value)); 
     } 
    } 

    class Program 
    { 
     static void Main() 
     { 
      var identity = new ClaimsIdentity(new[] 
      { 
       new Claim(ClaimTypes.UserData, "{AccountId: 1}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 2}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 3}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 4}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 5}"), 
      }); 

      const int iterations = 1000000; 
      var stopwatch = Stopwatch.StartNew(); 
      for (var i = 0; i < iterations; ++i) 
      { 
       identity.GetUserInformationClaims(withConcurrentDictionary: true).ToList(); 
      } 
      Console.WriteLine($"With ConcurrentDictionary: {stopwatch.Elapsed}"); 

      stopwatch = Stopwatch.StartNew(); 
      for (var i = 0; i < iterations; ++i) 
      { 
       identity.GetUserInformationClaims(withConcurrentDictionary: false).ToList(); 
      } 
      Console.WriteLine($"Without ConcurrentDictionary: {stopwatch.Elapsed}"); 
     } 
    } 
} 

Ausgang:

With ConcurrentDictionary: 00:00:00.8731377 
Without ConcurrentDictionary: 00:00:05.5883120 

eine schnelle Möglichkeit zu wissen, ob die Deserialisierung der UserInformation Fälle die Ursachen für die verdächtig hohe CPU-Zyklen ist, versuchen zu kommentieren und Anstoßen aus jedem Validierung gegen UserInformation und sehen, ob die Zyklen immer noch hoch sind.

0

Caching, da jedes GET andere Ergebnisse zurückgibt, müssen Sie wahrscheinlich implementieren eigenes Caching, das ist nicht besonders schwer. Sie können MemoryCache oder HttpRuntime.Cache verwenden, um alle gewünschten Daten zu speichern. Es gibt ein einfaches Beispiel am Ende der Dokumentation.

Für jeden Prozess gibt es einen Cache. Wenn IIS für mehr als einen Arbeitsprozess konfiguriert ist, enthält jeder Prozess seinen eigenen Cache.

Aber auf diese Weise können Sie alle gewünschten Daten im Cache halten. Dann rufen Sie es ab und manipulieren Sie es, bevor Sie Daten an den Client zurückgeben müssen.

Sie müssen nur eine Art von Sperren implementieren, um sicherzustellen, dass das gleiche zwischengespeicherte Element nicht von mehreren Threads gleichzeitig geschrieben wird. Ein paar Ideen hierzu finden Sie unter here.


Alte Antwort:

Wenn jeder Benutzer die gleichen Daten sieht, dann kann man Strathweb.CacheOutput.WebApi2 verwenden, die in NuGet zur Verfügung steht. Es könnte zu Ihren Bedürfnissen passen.

Die Ergebnisse werden basierend auf der gesendeten URL zwischengespeichert. Wenn also Daten für /api/getmydata zurückgegeben werden, erhält der nächste Aufruf von /api/getmydata Daten aus dem Cache. Sie legen den Cache-Ablauf fest.

Sie dekorieren Ihre Aktionen mit dem CacheOutputAttribute:

[CacheOutput(ServerTimeSpan = 100)] 
public List<string> GetMyData() { 
    ... 
} 

Aber wenn eine Aktion unterschiedliche Daten zurückgeben kann, je nachdem, wer der Benutzer ist, dann wird dies nicht so leicht funktionieren.

+0

Leider geben GET-Aktionen immer unterschiedliche Ergebnisse zurück und die meisten Aufrufe sind POSTs. Vielen Dank! – Igorek

+0

Sie müssen wahrscheinlich selbst etwas implementieren, damit Sie nur die gewünschten Daten zwischenspeichern können. Ich habe meine Antwort aktualisiert. –

+0

Das war, worum ich gefragt habe, wie ich am besten meine eigenen rollen soll. MemoryCache führt auch Serialisierung durch, so dass dies nicht sinnvoll ist.Ich denke, ich brauche eine gute Schlüssel-Lookup-Strategie – Igorek

Verwandte Themen