DateTime in der Cloud

14. September 2016

Eine Anwendung “in die Cloud zu heben” führt manchmal zu sehr unerwarteten Effekten. Einige entstehen nicht durch die Cloud an sich, sondern weil bislang “schlafende” Probleme plötzlich akut werden.

Stand heute haben viele Unternehmen ihre Anwendungen im eigenen Serverraum, Rechenzentrum oder beim klassischen Hoster. Mit einer bestehenden Anwendungen “in die Cloud zu gehen” bringt sicher einige Herausforderungen mit sich – Integration mit anderen Systemen, Backup und Administration schießen durch den Kopf – auf die man sich aber vorbereitet, wenn man einen solchen Schritt plant. Daneben kann es aber auch zu Effekten kommen, die man zunächst nicht auf dem Radar hatte. Ein Beispiel für einen solchen Effekt ist der Wechsel der Zeitzone des Servers.
Der .NET Datentyp DateTime ist die typische Wahl, wenn es um Datums- oder Zeitangaben geht. Leider ist er nicht unproblematisch wenn mehrere unterschiedliche Zeitzonen ins Spiel kommen. Dieses Problem wird dadurch verstärkt, dass das oft gar nicht auffällt, solange man sich in der gleichen Zeitzone bewegt.

Üblicherweise hat ein Entwickler seine eigene lokale Zeit im Blick, eventuell ergänzt um UTC (Greenwich-Zeit) als neutrale Bezugsgröße. Und wer im Code konsistent zwischen lokaler Zeit und UTC umrechnet (ToLocalTime(), ToUniversalTime()), der wird vermutlich nie ein Problem festgestellt haben, und ist auch für die Cloud gut vorbereitet.

Wer sich aber im Code bislang nicht um Zeitzonen gekümmert hat, der wird vermutlich ebenfalls nie ein Problem festgestellt haben… bist jetzt. Denn leider liegt ist das Kind dann womöglich schon im Brunnen. Zumindest, wenn man den “richtigen” Mix an Technologien im Einsatz hat, denn diese unterscheiden sich in einigen Details.

Eine durchaus übliche Mischung – zumindest wenn man an eine ASP.NET MVC-Anwendung denkt – ist dabei:

Der erste Unterschied ist die Umwandlung eines Strings im UTC-Format:

Zeitangabe als string DateTime.Parse() JsonConvert.DeserializeObject()
1 “2000-01-01T12:00:00Z” 13:00 local 12:00 UTC
2 “2000-06-01T12:00:00Z” 14:00 local 12:00 UTC
 
3 “2000-01-01T12:00:00+02:00” 11:00 local 11:00 local
4 “2000-06-01T12:00:00+02:00” 12:00 local 12:00 local
 
5 “2000-01-01T12:00:00” 12:00 unspecified 12:00 unspecified
6 “2000-06-01T12:00:00” 12:00 unspecified 12:00 unspecified

“12:00 UTC” ist dabei so zu verstehen, dass hier ein DateTime mit Uhrzeit 12:00 und DateTimeKind.Utc entsteht. Lokale Zeitzone zum Zeitpunkt des Tests war +02:00 (MESZ).

Die ersten beiden Zeilen zeigen Zeiten im UTC-Format, erkennbar am abschließenden ‘Z’. Wie man sieht rechnet DateTime.Parse() das gerne in die lokale Zeit um, während Json.NET (XmlSerializer verhält sich hier identisch) das Datum als UTC-Wert beibehält. Das wird gleich noch wichtig werden.
Ebenfalls fällt auf, dass dabei – völlig korrekt – unterschiedliche Uhrzeiten herauskommen. Das ist der Grund, warum ich zwei Datums-Werte – Winter- und Sommerzeit – angegeben habe. +02:00 im Januar entspricht osteuropäischer Zeit, daher der Versatz um eine Stunde.

Zeile 3 und 4 gehen von lokalen Zeitangaben, d.h. mit Offset im String aus, was auch von Json.NET berücksichtigt wird. Die Zeitumstellung führt wiederum dazu, dass es zu einer Stunde Versatz kommt.

Zeilen 5 und 6 zeigen, dass ohne Angabe von ‘Z’ oder eines Offsets jede Umrechnung unterbleibt, sowohl in lokale Zeit, als auch bzgl. Sommerzeit; das DateTime wird entsprechend mit DateTimeKind.Unspecified gekennzeichnet. Auch das bekommt gleich seine tiefergehende Bedeutung.
Der zweite Unterschied entsteht zwischen den Datentypen in .NET und in T-SQL. DateTime (.NET) und datetime2 (T-SQL) unterscheiden sich in einem wesentlichen Detail: datetime2 merkt sich nicht den Bezug zur Zeitzone.

Die Probleme entstehen, wenn diese Punkte in Kombination auftreten:

  • Für Daten die auf dem Client als lokale Zeit oder UTC vorliegen gilt entscheidet der Weg über das Format auf dem Server:
    • Werden sie in ASP.NET MVC über die URL übergeben, dann werden sie durch DateTime.Parse() verarbeitet, liegen also auf dem Server immer als lokale Zeiten vor, unabhängig davon wie sie auf dem Client vorlagen.
    • Werden sie als Teil der Nutzlast übergeben, dann liegen je nach Format auf dem Client wieder als UTC- oder lokale Zeit vor.
  • Beim Speichern in die Datenbank wird der aktuelle Wert – ohne Berücksichtigung der Zeitzone oder gar automatische Umrechnung – in der Datenbank abgelegt. Die Information, ob es sich um lokale oder UTC-Zeit handelte ist damit verloren.
  • Beim Lesen aus der Datenbank wird – korrekterweise! – das DateTime mit DateTimeKind.Unspecified geliefert. In der Folge findet dann keinerlei Umrechnung mehr statt.

Wie gesagt, das spielt oft keine große Rolle, solange man seine Zeitzone nicht verlässt. Json.NET behält alle numerischen Werte und den Bezug zu UTC bzw. lokaler Zeit bei und lokale Zeiten liegen bei Client und Server in der gleichen Zeitzone. DateTimes mit DateTimeKind.Unspecified verhalten sich bei den meisten Berechnungen neutral und fallen nicht weiter auf. Es ist also ziemlich egal, ob man ein DateTime von DateTime.Now ableitet (DateTimeKind.Local) oder aus der Datenbank liest (DateTimeKind.Unspecified).

Anders ausgedrückt: Der Informationsverlust, der durch den Wegfall des DateTime.Kind entsteht, lässt sich durch das implizit vorhandene Wissen leicht ausgleichen.
Und jetzt die Cloud…

Wer das freie Kontingent seines MSDN-Accounts nutzt, der bekommt automatisch “South Central US” als Region voreingestellt – und liegt damit in der Zeitzone Central Standard Time (UTC-6) oder Central Daylight Time (UTC-5):

image

Passieren kann jetzt zum Beispiel folgendes: Angenommen mein DateTime liegt am Client mit lokaler Zeit vor (MESZ, +02:00). Als Teil der Nutzdaten wird es von Json.NET übertragen. Am Server wird anhand des Formats erkannt, dass es sich um lokale Zeit bzgl. +02:00 handelt und diese – Achtung! – wird dann in lokale Zeit des Servers umgerechnet. Der Betrag verrutscht also um die Differenz der Zeitzonen. Und diesen Betrag nun einfach abzuspeichern war ja bislang kein Problem…
Wird das Datum danach neu gelesen, dann hat es den verschobenen Wert und DateTimeKind.Unspecified. Wie gesagt, bislang war es nicht notwendig das per SpecifyKind() zu korrigieren, jetzt ist das zwingend.

Ein ähnliches Problem lässt sich konstruieren, wenn die Datenübermittlung von Query-Parametern in der URL auf POST-Daten umgestellt wird, siehe das unterschiedliche Verhalten von DateTime.Parse() und der Serialisierung oben in der Tabelle.

Nochmal: Das ist eigentlich kein Problem der Cloud, aber die Wahrscheinlichkeit des Auftretens steigt in der Cloud deutlich an. DateTime (bzw. der unbedachte Umgang damit) ist meines Erachtens ein gutes Beispiel dafür, dass es bei einer Verlagerung in die Cloud zu sehr unerwarteten Seiteneffekten kommen kann.

Ergo: Gut testen, wenn man seine Anwendung in die Cloud verlagert!

PS: Unabhängig vom hier dargestellten Problem ist DateTime an sich schon nicht unproblematisch. Wer tiefer in dieses Thema einsteigen will findet hier vielleicht interessante Einstiegspunkte:

  • MSDN: “Dates, Times, and Time Zones” (link)
  • MSDN: “A Brief History of DateTime” (link)
  • Stack Overflow: “DateTime vs DateTimeOffset” (link)
  • Stack Overflow: “Daylight saving time and time zone best practices” (link)
  • Stack Overflow: “How to store repeating dates keeping in mind Daylight Savings Time” (link)
  • NodaTime: “What’s wrong with DateTime anyway?” (link)
  • NodaTime: “Noda Time Core concepts” (link)