Sonntag, November 02, 2008

Compression over WCF-Transports

Ich habe mich die letzten Wochen/-enden mit Komprimierung von WCF-Daten beschäftigt. Komprimierung könnte gerade bei der Anbindung von vielen Clients im WAN Bereich positive Ergebnisse erzielen. Im lokalen Bereich schadet es bei heutigen Prozessoren sicherlich wenig und für große Datenmengen ist es in diesen Fällen ideal.

Ich war allerdings nicht so verrückt mich auf eine Neuentwicklung einzulassen. Es musste so etwas bereits geben und so war es auch. In den WCF-Samples ist ein Beispiel enthalten, dies soll vor allem das erstellen von eigenen WCF-Transports demonstrieren. Mit der Lösung habe ich etwas herum gespielt, allerdings habe ich nur eine http-Transport damit zum laufen bekommen. Auf der Suche nach alternativen bin ich auf das CodePlex-Project WCF-Extensions gestoßen. Das Projekt hat bisher kein Release veröffentlicht, allerdings kann man den Code downloaden.

Statt den Code auszuprobieren, habe ich erst mal den Code erweitert. Ich habe zusätzlichen Komprimierungsalgorithmus hinzugefügt, so dass es nun ganze 5 Möglichkeiten gibt.

   1:  public enum CompressionAlgorithm
   2:  {
   3:      GZip,
   4:      Deflate,
   5:      BZip2,
   6:      Zip,
   7:      GZip2
   8:  }

Die letzten 3 Algorithmen verwenden die SharpZibLib zur Komprimierung. Ich finde die ZibLib eine super Implementierung, wer es noch ausgefallener/besser möchte kann dies auch noch um Xceed Zip for .Net aufbohren.

Nach den Anpassungen habe ich zumindest mal einige Tests mit dem integrierten Client gemacht. Es hat sich schnell gezeigt, dass geringe Datenmengen, < 200 Bytes, durch die Komprimierung sogar minimal vergrößert wurden. Die besten Ergebnisse habe ich immer mit BZip2 erhalten. Einen genauen Vergleich der Komprimierungsraten und der Verhalten im WCF-Streams habe ich nicht bis zu Ende durchgeführt. Mir hat es gereicht, nach dem es funktionierte.

So, nun aber zum schwierigeren Teil, die Konfiguration in der app.config. Ich hatte mich eine ganze Weile durch verschiedene MSDN-Pages gesucht und die Konfiguration zusammen zu frickeln. Ich bin froh, dass beim Startup der Anwendung die Konfiguration geprüft wird. Meine erste Konfiguration sah wie folgt aus:

   1:    <system.serviceModel>
   2:      <extensions>
   3:        <bindingElementExtensions>
   4:          <add name="compression" type="WcfExtensions.ServiceModel.Configuration.CompressionElement, WcfExtensions.ServiceModel"/>
   5:        </bindingElementExtensions>
   6:      </extensions>
   7:      <behaviors>
   8:        <serviceBehaviors>
   9:          <behavior name="CustomerService">
  10:            <serviceDebug httpHelpPageUrl="http://localhost:55557/Provisioning/CustomerService"
  11:              includeExceptionDetailInFaults="true" />
  12:            <serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:55556/Provisioning/CustomerService" />
  13:            <serviceTimeouts transactionTimeout="00:00:40" />
  14:          </behavior>
  15:        </serviceBehaviors>
  16:      </behaviors>
  17:      <bindings>
  18:        <customBinding>
  19:          <binding name="tcpCompressed" openTimeout="00:00:20" receiveTimeout="00:00:40"
  20:            sendTimeout="00:01:30">
  21:            <compression algorithm="BZip2" level="Normal"/>
  22:            <transactionFlow />
  23:            <binaryMessageEncoding maxReadPoolSize="32"
  24:                              maxWritePoolSize="32"
  25:                              maxSessionSize="4096">
  26:              <readerQuotas
  27:                  maxArrayLength="8000"
  28:                      maxBytesPerRead="4096"
  29:                      maxDepth="32"
  30:                      maxNameTableCharCount="16384"
  31:                      maxStringContentLength="65536" />
  32:            </binaryMessageEncoding>
  33:            <tcpTransport hostNameComparisonMode="WeakWildcard" transferMode="StreamedResponse"
  34:            maxReceivedMessageSize="1048576000">
  35:            </tcpTransport>
  36:          </binding>
  37:        </customBinding>
  38:      </bindings>
  39:      <services>
  40:        <service behaviorConfiguration="CustomerService" name="DE.CapeVision.Windows.Services.AgentService.Components.ProjectProvisioningWorkflow.CustomerService">
  41:          <endpoint address="net.tcp://localhost:20102/Provisioning/CustomerService"
  42:            binding="customBinding" bindingConfiguration="tcpCompressed"
  43:            contract="DE.CapeVision.Common.Contracts.Customers.ICustomerService" />
  44:        </service>
  45:      </services>
  46:    </system.serviceModel>

Es funktionierte mit der Konfiguration recht gut, allerdings kam irgendwann die Stelle, die ein weitersuchen erforderte, ich hatte keine Authentifizierung. Aber ich wollte unbedingt WindowsSecurity für die Anwendung haben. Es handelt sich um eine kleine Intranet-Anwendung. Authentifizierung ist ein wesentliches Merkmal zur minimalen Absicherung des Dienstes. Ein halber Tag und viele Kämpfe mit der MSDN Doku, habe ich es geschafft. Am Ende fehlten die beiden Elemente vor dem Transport (dick hervorgehoben 33-36), die Konfiguration sah so aus:

   1:    <system.serviceModel>
   2:      <extensions>
   3:        <bindingElementExtensions>
   4:          <add name="compression" type="WcfExtensions.ServiceModel.Configuration.CompressionElement, WcfExtensions.ServiceModel"/>
   5:        </bindingElementExtensions>
   6:      </extensions>
   7:      <behaviors>
   8:        <serviceBehaviors>
   9:          <behavior name="CustomerService">
  10:            <serviceDebug httpHelpPageUrl="http://localhost:55557/Provisioning/CustomerService"
  11:              includeExceptionDetailInFaults="true" />
  12:            <serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:55556/Provisioning/CustomerService" />
  13:            <serviceTimeouts transactionTimeout="00:00:40" />
  14:          </behavior>
  15:        </serviceBehaviors>
  16:      </behaviors>
  17:      <bindings>
  18:        <customBinding>
  19:          <binding name="tcpCompressed" openTimeout="00:00:20" receiveTimeout="00:00:40"
  20:            sendTimeout="00:01:30">
  21:            <compression algorithm="BZip2" level="Normal"/>
  22:            <transactionFlow />
  23:            <binaryMessageEncoding maxReadPoolSize="32"
  24:                              maxWritePoolSize="32"
  25:                              maxSessionSize="4096">
  26:              <readerQuotas
  27:                  maxArrayLength="8000"
  28:                      maxBytesPerRead="4096"
  29:                      maxDepth="32"
  30:                      maxNameTableCharCount="16384"
  31:                      maxStringContentLength="65536" />
  32:            </binaryMessageEncoding>
  33:            <security authenticationMode="SspiNegotiatedOverTransport"
  34:                   requireSecurityContextCancellation="false">
  35:            </security>
  36:            <windowsStreamSecurity protectionLevel="EncryptAndSign"/>
  37:            <tcpTransport hostNameComparisonMode="WeakWildcard" transferMode="StreamedResponse"
  38:            maxReceivedMessageSize="1048576000">
  39:            </tcpTransport>
  40:          </binding>
  41:        </customBinding>
  42:      </bindings>
  43:      <services>
  44:        <service behaviorConfiguration="CustomerService" name="DE.CapeVision.Windows.Services.AgentService.Components.ProjectProvisioningWorkflow.CustomerService">
  45:          <endpoint address="net.tcp://localhost:20102/Provisioning/CustomerService"
  46:            binding="customBinding" bindingConfiguration="tcpCompressed"
  47:            contract="DE.CapeVision.Common.Contracts.Customers.ICustomerService" />
  48:        </service>
  49:      </services>
  50:    </system.serviceModel>

Die Clientseitige Konfiguration unterscheidet sich nur minimal von der gegebenen, es sollte sich schnell erstellen lassen. Hier der Vollständigkeit die Client-Seite:

   1:    <system.serviceModel>
   2:      <extensions>
   3:        <bindingElementExtensions>
   4:          <add name="compression" type="WcfExtensions.ServiceModel.Configuration.CompressionElement, WcfExtensions.ServiceModel"/>
   5:        </bindingElementExtensions>
   6:      </extensions>
   7:      <bindings>
   8:        <customBinding>
   9:          <binding name="tcpCompressed" openTimeout="00:00:20" receiveTimeout="00:00:40"
  10:            sendTimeout="00:01:30">
  11:            <compression algorithm="GZip2" level="Fast"/>
  12:            <transactionFlow />
  13:            <binaryMessageEncoding maxReadPoolSize="32"
  14:                              maxWritePoolSize="32"
  15:                              maxSessionSize="4096">
  16:              <readerQuotas
  17:                  maxArrayLength="8000"
  18:                      maxBytesPerRead="4096"
  19:                      maxDepth="32"
  20:                      maxNameTableCharCount="16384"
  21:                      maxStringContentLength="65536" />
  22:            </binaryMessageEncoding>
  23:            <security authenticationMode="SspiNegotiatedOverTransport"
  24:                   requireSecurityContextCancellation="false">
  25:            </security>
  26:            <windowsStreamSecurity protectionLevel="EncryptAndSign"/>
  27:            <tcpTransport hostNameComparisonMode="WeakWildcard" transferMode="StreamedResponse"
  28:            maxReceivedMessageSize="1048576000"/>
  29:          </binding>
  30:        </customBinding>
  31:      </bindings>
  32:      <client>
  33:        <endpoint address="net.tcp://localhost:20102/Provisioning/CustomerService" binding="customBinding" bindingConfiguration="tcpCompressed" contract="DE.CapeVision.Common.Contracts.Customers.ICustomerService" />
  34:      </client>
  35:    </system.serviceModel>

Hier natürlich noch der manipulierte Source Code der WCF-Extensions: Download. Um es produktiv einzusetzen muss allerdings noch etwas geschraubt werden, damit Performance Informationen abgegriffen werden können. Bis auf die 2 Trace-Ausgaben ist derzeit nichts enthalten.

UPDATE: Ich habe heute eine kleine Unschönheit gefixed, der Code in der Quelle ist entsprechen aktualisiert. Die Channels werden nicht sauber durch gereicht, so dass es unter Umständen zu einer Cast-Exception in der CompressionChannelBase kommt.

Technorati-Tags: ,,

3 Kommentare:

tobias@krasinger.at hat gesagt…

Hallo Jan,

Hast du eigentlich auch getestet, wieviel "Zeit"-Aufwand die Komprimierung bedeutet und ob sich das im Gegensatz zur gewonnenen Übertragungszeit rentiert?

Ich hab ein paar ganz einfache Beispiele geschrieben und ein paar Listen mit dem Built-In GZip komprimiert.

Z.b:
Eine meiner Liste mit ca. 38000 Einträgen (EF-Entities) (ich würde diese natürlich nie übertragen) wäre mit dem DataContractSerializer in etwa 38MB groß, dank Komprimierung von 78% nur mehr 8MB, die Komprimierung braucht aber etwa 3sec.

Nehmen wir eine 20MBit Leitung an, könnte ich in den verlorenen 6 Sekunden (muss es ja auch noch dekomprimieren) etwa 15MB übertragen, gewinne aber knapp 30.

Das ganze wäre also immer noch rentabel.

Kannst du da Aussagen zu deiner verwendeten Komprimierung machen. Wieviel Zeit diese in etwa pro Aufruf braucht um Daten zu komprimieren und zu dekomprimieren?

Danke, sg tobias

Jan Zieschang hat gesagt…

Hallo Tobias,
ich habe die Zeiten bei der Übertragung nicht gemessen. In einem Projekt, in dem wir den Code später eingesetzt haben, hat sich das Komprimieren definitiv gelohnt. Nicht alle Lokationen des Kunden waren über gute Bandbreiten angebunden, so dass jedes Byte zählte. Zu dem ist, nach meiner Erfahrung, die Rechenleistung der Server heutzutage so gut, dass Komprimierung eigentlich kaum ins Gewicht fallen sollte. Was natürliche eine große Rolle spielt ist der Komprimierungsalgorithmus und die Stärke. Hast du es mit Level "Normal" und BZip2 probiert?
In dem obigen Projekt hatten wir durchschnittliche Komprimierung von 90% und Paketen von 500kb bis 5MB, Komprimierung BZip2, Level=Max. (glaube ich)
Wäre cool, wenn du noch einige Details zu deinen Tests und deiner Umgebung liefern würdest. Vielleicht sind andere auch interessiert. Wenn eine XCeed-Lizenz vorliegt, dann würde ich empfehlen, die Implementierung für den XCeed-Compression-Stream anzupassen.

(Ich würde übrigens auch auf den meisten Webservern die Komprimierung aktivieren, in den meisten Fällen rentiert sich der Gewinn.)

tobias@krasinger.at hat gesagt…

Also ich hab meinen ganzen Test nochmal mit BZip2 mit der neuesten SharpZibLib durchlaufen lassen.
Ich weiß nicht wie du das verwenden konntest, aber bei mir braucht die Komprimierung der 38MB Listen fast eine Minute und das Ergebnis ist nicht überragend besser. Im Schnitt nur 10% mehr Komprimierung.

Also das hat bei mir gar nichts gebracht.

Wobei ich derzeit aber keine WCF Tests mache sondern nur den Output der Methoden komprimiere.

Ich werde jetzt mal versuchen, dein gesamtes "Projekt" bei mir einzubinden.