Blog: Tijdzones op een LAMP-server

Zondag 30 oktober 2011, 21:57

Lang heb ik het gesteld met een basismodel van gsm, eentje die op eender welke toets slechts na een goede seconde reageerde. Die vertraging is op een gsm even irritant als, pakweg, op een Telenet digibox. Maar ik was zo'n spaarzame gsm-gebruiker, dat ik er geen al te grijze haren van heb gekregen. Toen ik eerder dit jaar echter op reis naar de Verenigde Staten ging, had ik sowieso een nieuwe, triband gsm nodig, om er daar gebruik van te kunnen maken. Aldus kocht ik mij ineens een tweedehands HTC Wildfire smartphone aan. De levensduur van de batterij trekt wel op niets, maar er zitten toch ook een heleboel zeer nuttige snufjes aan. Zo is wireless erg handig, vooral op reis, want internetcafés verdwijnen tegenwoordig meer en meer, en hun plaats wordt ingenomen door wifi hotspots. Verder heb ik ook een gps-functie, die soms wel een beetje moeite heeft om de juiste locatie te vinden, maar toch zeer handig is voor navigatie. Ik heb dan wel geen mobiel internet om eender waar kaarten op te vragen, maar met een goede app (Maverick in combinatie met Mobile Atlas Creator) kan je de nodige kaarten op voorhand op de harde schijf zetten. (Helaas hebben deze programma's recent blijkbaar te kampen met copyright-moeilijkheden, zodat het aanbod van beschikbare kaarten gekrompen is. Gebruik oudere versies van de programma's om een groter aanbod te hebben.)

Door in Engeland te studeren, en regelmatig van tijdzone te veranderen, ben ik me zeer bewust van het belang van tijdzones goed te bij te houden in scripts en databases. Op een gsm, die meestal mee van de ene tijdzone in de andere reist, is dat nog het meest acuut van al. Op dit vlak is de HTC Wildfire echter een grote teleurstelling. Het toestel draait het Android besturingssysteem van Google, en van hen zou je wel beter verwachten, maar tot voor kort had de Google Calendar vrijwel geen ondersteuning voor tijdzones. Bijgevolg is de kalender van mijn smartphone geheel onbruikbaar voor het invoegen van bijvoorbeeld internationale vluchten. Het vertrekuur van een vlucht wil je in de tijdzone van de vertrekluchthaven in- en weergeven, ongeacht in welke tijdzone je je bevindt. Op de HTC Wildfire is dat dus onmogelijk. Telkens wanneer je van tijdzone verandert, verspringen alle tijden op de kalender mee. Omdat dit een tekort is in het onderliggende Google Calendar-systeem, is er geen enkele manier - zelfs geen app - die een uitweg kan bieden. Google Calendar heeft recent betere ondersteuning voor verschillende tijdzones ingevoerd, maar Android 2.2 is niet mee geëvolueerd, dus ik blijf met mijn miserie zitten.

Dat zelfs Google er zo'n zootje van maakt, illustreert goed dat het degelijk ondersteunen van tijdzones geen sinecure is. Iedereen die ooit al eens een website heeft gemaakt die ergens timestamps moet bijhouden en behandelen, zal daar wel over kunnen meespreken. Er zijn zoveel verschillende instellingen die invloed hebben, dat je er gemakkelijk in verloren loopt. Er is een overvloed van informatie te vinden op het internet, doch uitgespreid over veel verschillende pagina's en handleidingen. Daarom vond ik het wel de moeite waard om eens een overzicht te maken. Voor mijn eigen future reference, en voor iedereen die er ook baat bij zou kunnen hebben: alles wat je moet weten om tijdzones correct te ondersteunen op een LAMP (Linux-Apache-MySQL-PHP) server.

Alles hier is geschreven voor Ubuntu 11.04. Niet dat ik nog zo'n grote fan ben van Ubuntu. Ubuntu 11.10 heeft bijna zoveel bugs als Windows Millennium. En het idee achter Unity is wel ok, maar de huidige uitvoering schiet nog op veel vlakken zwaar tekort. Maar soit, ik ga nu niet meer alles herschrijven voor een andere distributie.

Here we go...

In elke computer tikt er een klokje, de hardwareklok. Deze werkt op batterijen en loopt dus ook door als de computer afstaat. De hardwareklok kan je aanpassen in de BIOS setup of met het Linux-commando hwclock.

De hardwareklok houdt enkel datum en uur bij, géén tijdzone. Je kan zelf kiezen of de hardwareklok als UTC (de wintertijd in Greenwich) of als locale tijd wordt afgelezen. UTC lijkt de meeste logische keuze, en is standaard voor Linux, maar Windows leest de hardwareklok als locale tijd, dus op dual boot systemen wordt meestal de hardwareklok als locale tijd ingesteld. Eventueel kan je Windows wel configureren om de hardwareklok als UTC te lezen. Configuratie is echter gemakkelijker in Linux, door gewoon een argumentje mee te geven aan hwclock (--localtime of --utc) bij het instellen van de hardwareklok. Deze keuze wordt dan bewaard in het bestandje /etc/adjtime. Als dit bestandje niet bestaat, wordt de hardwareklok als locale tijd geïnterpreteerd. Merk op dat het --date argument van hwclock altijd in locale tijd moet gegeven worden, zelfs bij gebruik van --utc.

Terwijl de computer opstaat, wordt de hardwareklok nauwelijks gebruikt. Hij wordt bij het opstarten van de computer eenmaal ingelezen, en dan begint Linux met zijn eigen softwareklok of systeemklok te werken. Deze systeemklok kan je raadplegen en aanpassen met het commando date.

Je zou dus verwachten dat wijzigingen aan de systeemklok bij het afzetten van de computer verloren gaan. Op mijn laptop gebeurt echter het tegengestelde, dankzij een scriptje /etc/init.d/hwclock dat bij het afsluiten wordt uitgevoerd en dat de systeemtijd kopieert naar de hardwareklok. Ook Ubuntu's clock applet zorgt er streng voor dat systeemtijd en hardwaretijd gesynchroniseerd blijven. Nog andere programma's kunnen hierop invloed hebben, zoals het NTP (Network Time Protocol), waarmee je via het internet automatisch de klok juist kan laten zetten.

De locale tijd, dus in welke tijdzone wordt gewerkt, wordt bepaald door /etc/localtime. Dit (binaire) bestandje bevat informatie over de plaatselijke tijdzone en is een kopie van een bestand uit de map /usr/share/zoneinfo/, waar informatie wordt bewaard over alle tijdzones. De propere manier om van tijdzone te veranderen is het uitvoeren van dpkg-reconfigure tzdata. Ook met Ubuntu's clock applet kan je natuurlijk eenvoudig van tijdzone veranderen.

Er is ook een environment variable TZ waarmee je eventueel de tijdzone voor bepaalde processen kan overschrijven.

De ingestelde tijdzone bepaalt natuurlijke hoe de tijd wordt afgebeeld. Dat gaat niet alleen over hoe laat het is (Ubuntu's klokje; of het date commando), maar ook bijvoorbeeld over de modification time van een bestand (zoals weergegeven door de Nautilus file browser of door ls -l). Die modification time van een bestand wordt door alle moderne filesystems intern opgeslagen in UTC. Zo is het veranderen van tijdzone enkel een kwestie is van weergave; de timestamp zelf moet niet aangepast worden. Ook winter- en zomertijd is enkel een kwestie van weergave: voor elke UTC timestamp gaat Linux kijken of die in winter- of zomertijd is van de locale tijdzone, en hij geeft steeds het goede uur weer.

Er is echter een belangrijke uitzondering hierop. FAT filesystems houden timestamps bij in locale tijd. FAT is derhalve een hopeloos verouderd systeem, maar het wordt toch nog dagelijks gebruikt, met name op geheugenkaartjes en memory sticks. Zo zal je op je fototoestel waarschijnlijk de tijd wel kunnen instellen, maar geen tijdzone. Als je vervolgens de foto's van het geheugenkaartje naar je computer kopieert, worden de timestamps gelezen als locale tijd. Dus als je foto volgens het toestel genomen is om 12:00, en je kopieert hem naar een computer in London, dan krijgt hij daar een modification time van 12:00 GMT, maar op een computer in Brussel krijgt hij een modification time van 12:00 CET. Moraal van het verhaal: zet de klok van je fototoestel in de tijdzone van het land waar je later de foto's naar je computer zult kopiëren. Dat is eigenlijk wel handig: zo moet je tijdens een reis door verschillende tijdzones ook niet telkens de klok van je camera aanpassen.

Als het toch nog fout zit met de timestamps van je foto's, kan je die nog manueel veranderen met het touch commando. Of je kan tijdelijk de tijdzone van je systeem veranderen, om de timestamps op een geheugenkaartje in de juiste tijdzone te lezen... Maar hierbij treed nog een extra complicatie op. De locale tijd die gebruikt wordt bij het mounten van een FAT filesystem, is eigenaardig genoeg niet de tijdzone die bijgehouden wordt in /etc/localtime, maar wel de tijdzone die de kernel zelf bijhoudt. Die wordt bij het opstarten van de computer ingesteld, op het moment dat de hardwareklok gelezen wordt. Maar later wordt de tijdzone van de kernel niet meer aangepast door bijvoorbeeld dpkg-reconfigure tzdata. Om de tijdzone van de kernel up to date te brengen, kan je natuurlijk de computer opnieuw opstarten, maar gemakkelijker is het uitvoeren van hwclock --systz of hwclock --hctosys.

Ook applicaties zoals de e-mailclient Thunderbird laten zich beïnvloeden door de ingestelde tijdzone. Natuurlijk wordt het tijdstip van ontvangen e-mail in locale tijd weergegeven. Maar ook als je een e-mail verstuurt, krijgt die een Date-header in locale tijd mee, samen met een indicatie van wat de locale tijdzone is.

De webserver Apache maakt ook gewoon gebruik van de systeemtijd en bijhorende tijdzone. Er lijkt geen manier te bestaan om dit aan te passen. Zelfs met een "SetEnv TZ" in apache2.conf, worden de logs bijvoorbeeld nog steeds in de tijdzone van het systeem geschreven. Maar normaal gezien is het ook wel logische op apache gewoon in de systeemtijd te laten draaien.

Waar je wel met tijdzone moet gaan spelen, dat is de gebruikerskant van de website. Timestamps worden gegenereerd door PHP of MySQL, opgeslagen in een MySQL database, en tenslotte aan de gebruiker getoond opnieuw met PHP.

In PHP kan je de tijdzone instellen met de ini-variabele date.timezone of met de functie date_default_timezone_set() (lijst van ondersteunde tijdzones). Indien je geen van beide methodes gebruikt, dan zal PHP terugvallen op de systeemtijd. Dus als je website in dezelfde tijdzone moet denken als je server, dan heb je normaal gezien niets in te stellen. Maar als je website bijvoorbeeld voornamelijk gericht is op Belgische bezoekers, terwijl hij draait op een Amerikaanse server, dan wil je PHP wel in CET laten lopen.

De output van date() en verwante functies wordt weergegeven in PHP's tijdzone. Indien je de DateTime class gebruikt voor het bewaren van tijdstippen, dan kan je aan elke timestamp een eigen standaard tijdzone meegeven. Je kan die standaard tijdzone van een DateTime object ook veranderen, zonder dat de eigenlijke timestamp verandert, want die is gewoon als Unix timestamp opgeslagen.

MySQL is een beetje ingewikkelder dan PHP. Ten eerste heeft MySQL drie tijdzone-variabelen. De eerste, global.system_time_zone, wordt bij het opstarten van MySQL ingesteld en daarna niet meer gewijzigd. Standaard is dit de tijdzone van het Linux-systeem, maar je kan dit overschrijven met de TZ environment variable, of met een --timezone argument voor mysqld_safe. Belangrijker is de tweede variabele, global.time_zone. Deze heeft standaard de waarde 'SYSTEM', wat wilt zeggen dat global.system_time_zone wordt overgenomen hier. Je kan echter een eigen tijdzone instellen met het command line argument --default-time-zone, of met het MySQL commando SET GLOBAL time_zone = ...;. Elke MySQL-connectie apart kan dan nog eens die tijdzone overschrijven met de variabele session.time_zone. Die kan ingesteld worden met het MySQL-commando SET time_zone = ...;.

Wat is de invloed van deze time_zone variabele? Wel, ten eerste werken alle datum- en tijdfuncties van MySQL met datums, uren en minuten, zonder specifieke tijdzone. Dus alle tijdstippen worden geïnterpreteerd als zijnde in de tijdzone van de time_zone variabele, en de NOW() functie heeft die plaatselijke tijd als uitvoer. Ten tweede moet je goed opletten hoe je een tijdstip in de database opslaat. In de meeste situaties is het waarschijnlijk best om gebruik te maken van het TIMESTAMP-type. Het invoeren en uitlezen van een TIMESTAMP-veld gebeurt in de lokale tijd van time_zone, maar het opslaan gebeurt in UTC. Bijgevolg wordt het uur correct aangepast als het opslaan en uitlezen in verschillende tijdzones gebeurt. Dit is niet het geval met bijvoorbeeld het type DATETIME. Een DATETIME-veld bevat letterlijk de uren en minuten, zonder een notie van tijdzone. Dit is erg gevaarlijk. Het wordt heel gemakkelijk om te vergeten in welke tijdzone de tijdstippen zijn ingevoerd. Zodanig riskeer je de waarden verkeerd uit te lezen, of nog erger: je zou in eenzelfde tabel tijdstippen kunnen krijgen, die geïnterpreteerd moeten worden in verschillende tijdzones... Het DATETIME-type zou je dus eigenlijk alleen mogen gebruiken voor gegevens waarvoor de wijzers van de klok hetzelfde moeten gezet worden, ongeacht waar ter wereld je je bevindt.

Er is nog een derde mogelijkheid, en dit is je enige optie als je langer wilt meegaan dan 2038: je tijdstippen opslaan als UNIX timestamps in een veld van het BIGINT type. Dit zou geen enkele dubbelzinnigheid mogen veroorzaken. Je kan wel geen gebruik maken van de datum- en tijdfuncties van MySQL, maar PHP kan die taken natuurlijk ook wel aan. Bovendien zijn de tijdstippen in je tabellen zo niet op het zicht leesbaar.

Als voorbeeld: de foto's op deze website. Een foto op de geheugenkaart van mijn fototoestel heeft een modification time, maar zonder tijdzone, dus hopelijk stond het klokje van mijn camera op de lokale tijd van de computer waarmee ik het uploaden doe. Anders moet ik vóór het uploaden de modification time nog even manueel aanpassen naar de juiste tijdzone. Vervolgens plaats ik de foto op de website, en het tijdstip wordt in de MySQL database opgeslagen in een TIMESTAMP veld, zodat er hier geen dubbelzinnigheid meer kan bestaan over de tijdzone. Het weergeven van het tijdstip is echter het meest logisch in de tijdzone van waar de foto genomen is. Anders krijg je zonnige foto's genomen om 2 uur 's nachts... Dus bij het toevoegen van de foto, geef ik manueel de tijdzone in, waarin de foto genomen is. Zodanig kan het tijdstip van de foto bij de weergave op de website naar deze tijdzone omgezet worden. Simpel, nietwaar?

Icons from Flaticon.