تایم‌استمپ‌های یونیکس در Event Sourcing و لاگ‌های ممیزی

Event sourcing و audit logging هر دو به یک نیاز بنیادی متکی‌اند: هر رویداد باید یک تایم‌استمپ دقیق و قابل مقایسه داشته باشد که بتوان برای مرتب‌سازی به آن اعتماد کرد. تایم‌استمپ‌های یونیکس ابزار استاندارد برای این کار هستند — اما آن‌قدر لبه‌های تیز (edge case) دارند که اشتباه گرفتنشان دردسر واقعی ایجاد می‌کند.

از Unix Timestamp Converter استفاده کنید تا هر تایم‌استمپ را به تاریخ قابل خواندن تبدیل کنید یا یک تایم‌استمپ را در لاگ‌هایتان بررسی کنید. تمرکز این مقاله روی الگوها و دام‌های مخصوص سیستم‌های event sourcing و audit log است.

چرا تایم‌استمپ یونیکس برای سیستم‌های رویدادی مناسب است؟

یک event store یا audit log عملاً یک دنباله از «واقعیت‌ها»ست: «کاربر X در زمان T کار Y را انجام داد». تایم‌استمپ T باید:

  • بدون ابهام باشد: بدون تفسیر منطقه زمانی (timezone)، بدون قالب‌بندی وابسته به زبان/محلی‌سازی (locale)
  • قابل مقایسه باشد: باید بتوانید رویدادها را مرتب کنید و بپرسید «کدام اول اتفاق افتاد؟»
  • فشرده باشد: ذخیره یک عدد صحیح ۱۰ رقمی ارزان‌تر از یک رشته کامل datetime است
  • مناسب محاسبات باشد: «همه رویدادهای ۲۴ ساعت اخیر» یعنی WHERE ts > (now - 86400)، نه پارس کردن رشته‌ها

تایم‌استمپ یونیکس هر چهار مورد را پوشش می‌دهد. این همان تعداد ثانیه‌ها (یا میلی‌ثانیه‌ها) از ۱ ژانویه ۱۹۷۰ به وقت UTC است — یک عدد صحیح واحد که هر سیستمی می‌تواند آن را مقایسه کند، مرتب کند و روی آن محاسبه انجام دهد.

در مقابل، ذخیره تایم‌استمپ به‌صورت رشته قالب‌بندی‌شده، سربار پارس کردن، ابهام منطقه زمانی و رفتار وابسته به locale را وارد سیستم می‌کند. یک رشته ISO 8601 مثل 2026-04-08T14:30:00+02:00 قبل از مقایسه باید پارس شود؛ یک تایم‌استمپ یونیکس نیازی ندارد.

ثانیه یا میلی‌ثانیه: یکی را انتخاب کنید و ثابت بمانید

رایج‌ترین باگ تایم‌استمپ یونیکس در سیستم‌های رویدادی، قاطی کردن ثانیه و میلی‌ثانیه است. time.time() در پایتون ثانیه برمی‌گرداند. Date.now() در جاوااسکریپت میلی‌ثانیه برمی‌گرداند. EXTRACT(EPOCH FROM NOW()) در PostgreSQL ثانیه با دقت اعشاری برمی‌گرداند. UNIX_TIMESTAMP() در MySQL ثانیه برمی‌گرداند.

اگر تولیدکننده رویداد یک سرویس Node.js باشد و مصرف‌کننده یک سرویس Python، و هیچ‌کدام صراحتاً تبدیل نکنند، ممکن است تایم‌استمپ‌ها با ضریب ۱٬۰۰۰ خطا داشته باشند. یک تایم‌استمپ میلی‌ثانیه‌ای که جایی ذخیره شود که انتظار ثانیه دارند، باعث می‌شود رویداد انگار در سال 2001 رخ داده است. برعکس، یک تایم‌استمپ ثانیه‌ای که جایی استفاده شود که انتظار میلی‌ثانیه دارند، رویداد را شبیه اتفاقی در 1970 نشان می‌دهد.

قاعده عملی: در سطح معماری تصمیم بگیرید سیستم رویدادی شما با ثانیه کار می‌کند یا میلی‌ثانیه، آن را شفاف مستندسازی کنید و در همه تولیدکننده‌ها enforce کنید. نوشتن یک helper برای استانداردسازی خروجی — مثل toUnixSeconds() یا toUnixMs() — ارزشش را دارد که یک‌بار نوشته شود و همه‌جا استفاده شود.

برای بیشتر سناریوهای audit log و event sourcing، میلی‌ثانیه انتخاب بهتری است. رویدادهای داخل یک ثانیه رایج‌اند (چند کلیک، چند تغییر وضعیت) و دقت میلی‌ثانیه‌ای برای مرتب‌سازی درست کافی است بدون این‌که درگیر پیچیدگی نانوثانیه شوید.

ترتیب رویدادها: وقتی تایم‌استمپ کافی نیست

تایم‌استمپ یونیکس به شما می‌گوید رویداد چه زمانی توسط سیستم تولیدکننده ثبت شده است. در یک سیستم توزیع‌شده، این یک مشکل می‌سازد: دو رویدادی که «هم‌زمان» روی دو سرور مختلف اتفاق می‌افتند ممکن است تایم‌استمپ‌هایی با چند میلی‌ثانیه اختلاف داشته باشند — نه به این دلیل که رویدادها واقعاً زمان متفاوتی داشته‌اند، بلکه چون ساعت سرورها کمی از هم عقب/جلو هستند.

این مشکل بنیادیِ استفاده از تایم‌استمپ‌های ساعت واقعی (wall-clock) به‌عنوان تنها مکانیزم ترتیب‌دهی در سیستم‌های رویدادی توزیع‌شده است.

Clock skew — اختلاف ساعت دو سرور — می‌تواند میلی‌ثانیه یا حتی ثانیه باشد، خصوصاً در سیستم‌هایی بدون همگام‌سازی دقیق NTP. مصرف‌کننده‌ای که رویدادها را صرفاً بر اساس تایم‌استمپ مرتب می‌کند ممکن است ترتیب اشتباه بگیرد وقتی رویدادها از چند تولیدکننده با ساعت‌های متفاوت می‌آیند.

Clock drift بدتر است: ساعتی که کمی تند یا کند کار می‌کند، به‌مرور از زمان واقعی فاصله می‌گیرد. همگام‌سازی NTP این را دوره‌ای اصلاح می‌کند، اما خودِ اصلاح می‌تواند یک جهش ناگهانی در تایم‌استمپ ایجاد کند — جلو یا عقب — که یک بازه کوتاه می‌سازد که در آن تایم‌استمپ‌ها ممکن است یکنواخت (monotonic) نباشند.

Leap second گاهی یک ثانیه به UTC اضافه می‌کند تا با چرخش متغیر زمین هماهنگ شود. تایم‌استمپ یونیکس leap second را نمایش نمی‌دهد — مقیاس زمانی یونیکس عملاً آخرین ثانیه را تکرار می‌کند، یعنی بعضی ثانیه‌ها در یونیکس ۲ ثانیه طول می‌کشند. بیشتر سیستم‌ها این را شفاف مدیریت می‌کنند، اما سیستم‌های ممیزی که تایم‌استمپ‌ها را در مرز leap second مقایسه می‌کنند ممکن است ترتیب غیرمنتظره ببینند.

راه‌حل‌هایی برای ترتیب‌دهی قابل اتکا

Logical clocks / vector clocks: به‌جای اتکا صرف به زمان ساعت واقعی، از یک شمارنده منطقی استفاده کنید که با هر رویداد افزایش می‌یابد. Lamport timestamps و vector clocks ترتیب علّی را ثبت می‌کنند — «رویداد A قبل از رویداد B رخ داده» — بدون نیاز به ساعت‌های همگام.

Sequence number درون یک پارتیشن: Kafka و سیستم‌های مشابه داخل هر پارتیشن شماره‌های ترتیبیِ افزایشی اختصاص می‌دهند. داخل یک پارتیشن، sequence number مکانیزم ترتیب‌دهی قابل اعتماد است. بین پارتیشن‌ها، به منطق اضافی نیاز دارید.

Hybrid Logical Clocks (HLCs): ترکیبی از زمان ساعت واقعی و شمارنده منطقی. تایم‌استمپ به‌صورت یکنواخت افزایش می‌یابد، نزدیک به زمان واقعی می‌ماند و با clock skew کنار می‌آید چون وقتی تایم‌استمپ دریافتی جلوتر از ساعت محلی باشد، بخش منطقی را جلو می‌برد.

برای بیشتر سیستم‌های audit log که در آن‌ها ترتیب علّی سخت‌گیرانه بین نودهای توزیع‌شده لازم نیست، ثبت یک تایم‌استمپ یونیکس میلی‌ثانیه‌ای به‌همراه یک شناسه سرور کافی است. شناسه سرور رویدادهای هم‌زمان با تایم‌استمپ یکسان را از هم جدا می‌کند و تایم‌استمپ هم برای ممیزی عملی، به زمان واقعی نزدیک است.

ذخیره تایم‌استمپ‌ها در event store

نوع ستون عدد صحیح: تایم‌استمپ یونیکس را در دیتابیس‌های رابطه‌ای به‌صورت BIGINT (عدد ۸ بایتی) ذخیره کنید. INT (۴ بایتی) برای تایم‌استمپ‌های میلی‌ثانیه‌ای کافی نیست و حتی برای تایم‌استمپ ثانیه‌ای هم قبل از 2038 سرریز می‌کند. BIGINT تایم‌استمپ میلی‌ثانیه‌ای را تا سال 292,471,208 پشتیبانی می‌کند.

ایندکس کردن: ستون تایم‌استمپ در جدول‌های رویداد تقریباً همیشه باید ایندکس شود. الگوهای رایج کوئری — «رویدادهای بعد از زمان T»، «رویدادها بین T1 و T2»، «آخرین N رویداد» — همگی از ایندکس B-tree روی ستون تایم‌استمپ سود می‌برند.

پارتیشن‌بندی: جدول‌های پرحجم رویداد از پارتیشن‌بندی زمان‌محور (مثلاً ماهانه یا هفتگی) سود می‌برند. این کار اندازه ایندکس را قابل مدیریت نگه می‌دارد و اجازه می‌دهد پارتیشن‌های قدیمی آرشیو یا حذف شوند بدون این‌که ساختار اصلی جدول تحت تأثیر قرار گیرد.

تغییرناپذیری: رویدادها در event store باید تغییرناپذیر باشند — پس از ثبت، تایم‌استمپ نباید به‌روزرسانی شود. اگر لازم شد تایم‌استمپی اصلاح شود (که باید نادر باشد)، یک رویداد جبرانی (compensating event) ثبت کنید، نه این‌که رویداد اصلی را تغییر دهید.

تایم‌استمپ در لاگ ممیزی: چه چیزهایی را ثبت کنیم؟

یک رکورد خوب در audit log حداقل شامل موارد زیر است:

  • event_time: تایم‌استمپ یونیکس (میلی‌ثانیه) زمان وقوع رویداد
  • recorded_time: تایم‌استمپ یونیکس زمان ثبت رویداد در لاگ (ممکن است با event_time فرق داشته باشد اگر رویدادها batch یا با تأخیر ثبت شوند)
  • actor_id: چه کسی عمل را انجام داد
  • action: چه کاری انجام شد
  • resource_id: چه چیزی تحت تأثیر قرار گرفت
  • source_ip یا session_id: منبع اقدام از کجا بوده (برای ردگیری امنیتی)

تفاوت بین event_time و recorded_time در سیستم‌هایی با buffering، batching یا پردازش async مهم است. اگر رویدادی 500ms بعد از وقوعش ثبت شود، این فاصله هنگام همبستگی (correlate) رویدادهای ممیزی با سایر لاگ‌ها اهمیت دارد.

هر دو تایم‌استمپ را به‌صورت عدد صحیح میلی‌ثانیه‌ای ذخیره کنید. هنگام نمایش به کاربر، به منطقه زمانی کاربر تبدیل کنید — اما ذخیره و کوئری را با اعداد UTC انجام دهید.

تبدیل تایم‌استمپ‌های ممیزی برای بررسی انسانی

تایم‌استمپ یونیکس خام در audit log برای انسان قابل خواندن نیست. هنگام بررسی لاگ‌ها — در تحقیق رخداد (incident investigation)، ممیزی انطباق (compliance audit) یا بررسی امنیتی — باید تایم‌استمپ‌ها را به زمان محلیِ متناظر تبدیل کنید.

Unix Timestamp Converter برای تبدیل سریع تایم‌استمپ‌های تکی مفید است. برای تحلیل در مقیاس بزرگ، بیشتر ابزارهای مدیریت لاگ (Splunk, Datadog, Elastic) در رابط‌های کوئری خود تایم‌استمپ‌های یونیکس را خودکار به قالب قابل خواندن تبدیل می‌کنند.

یک نکته عملی: وقتی تایم‌استمپ تبدیل‌شده را در گزارش رخداد یا یافته‌های ممیزی مستند می‌کنید، همیشه منطقه زمانی را ذکر کنید. «رویداد ساعت 14:32:07 رخ داد» مبهم است؛ «14:32:07 UTC (Unix timestamp 1744123927)» مبهم نیست و قابل بازتولید است.

باگ‌های رایج در سیستم‌های رویدادیِ وابسته به تایم‌استمپ

ذخیره تایم‌استمپ به‌صورت رشته: تایم‌استمپ‌هایی که به‌صورت VARCHAR ذخیره شوند، به‌سختی ایندکس می‌شوند و برای بازه‌ها بهینه نیستند. همچنین تولیدکننده‌های مختلف می‌توانند فرمت‌های متفاوت تولید کنند.

استفاده از زمان محلی به‌جای UTC: تولیدکننده‌ای که localtime() را به‌جای UTC ثبت کند، تایم‌استمپ‌های وابسته به timezone می‌سازد. در تغییرات ساعت تابستانی (DST)، تایم‌استمپ‌های مبهم یا تکراری ایجاد می‌شود (دو رویداد متفاوت در «02:30:00» هنگام عقب‌کشیدن ساعت).

کاهش دقت به ثانیه وقتی رویدادها پشت سر هم رخ می‌دهند: اگر 50 رویداد داخل یک ثانیه رخ دهد و همه یک تایم‌استمپ یکسان بگیرند، ترتیب داخل آن ثانیه از دست می‌رود. برای هر سیستمی که ترتیب زیرثانیه‌ای مهم است، از دقت میلی‌ثانیه استفاده کنید.

نادیده گرفتن تأخیر پردازش رویداد: پیامی که در T=1000 صف می‌شود، در T=1500 از صف خارج می‌شود و در T=2000 پردازش می‌شود، سه تایم‌استمپ متفاوت دارد. این‌که کدام را ثبت کنید تعیین می‌کند audit log می‌گوید «چه زمانی اتفاق افتاد». برای بیشتر مقاصد، زمان وقوع واقعی رویداد را ثبت کنید (T=1000)، نه زمان پردازش آن را.

پذیرفتن تایم‌استمپِ ارسالیِ کلاینت بدون اعتبارسنجی: اگر کلاینت event_time را داخل payload درخواست بفرستد، اعتبارسنجی کنید که در یک پنجره منطقی نسبت به زمان فعلی سرور است (مثلاً ±5 دقیقه). پذیرفتن تایم‌استمپ‌های دلخواه کلاینت اجازه می‌دهد رویدادها backdate یا antedate شوند و یکپارچگی ممیزی را به هم می‌زند.

مقالات مرتبط