منطقه زمانی سرور: چرا ساعت اشتباه همه‌چیز را خراب می‌کند

اگر توکن API شما زودتر از موعد منقضی می‌شود یا لاگ‌ها ترتیب زمانی ندارند، احتمالاً منطقه زمانی سرور و NTP تنظیم نیست. راه تشخیص و اصلاح را ببینید.

۷ دقیقه به‌روزرسانی ۱۲ آبان ۱۴۰۵

توکن JWT شما با خطای Token used before issued رد می‌شود. یا گواهی SSL تازه‌نصب‌شده در مرورگر «منقضی شده» نشان داده می‌شود، در حالی که تاریخ انقضایش ماه‌ها بعد است. یا در Kibana دو لاگ پشت سر هم را می‌بینید که لاگ دوم زمان قدیمی‌تری از لاگ اول دارد. این سه نشانه یک ریشه دارند: ساعت سیستم شما با دنیای بیرون هم‌گام نیست. مشکل معمولاً از منطقه زمانی سرور شروع می‌شود، نه از ساعت سخت‌افزار.

اولین کاری که باید بکنید این است که بفهمید الان کجا ایستاده‌اید. یک دستور، سه خط خروجی:

timedatectl
               Local time: Sat 2025-01-11 14:22:07 +0330
           Universal time: Sat 2025-01-11 10:52:07 UTC
                 RTC time: Sat 2025-01-11 10:52:07
                Time zone: Asia/Tehran (+0330, +0330)
System clock synchronized: no
              NTP service: inactive

دو خط آخر همه‌چیز را می‌گوید. اگر System clock synchronized: no باشد، ساعت شما دارد آزادانه drift می‌کند. روی یک سرور فیزیکی معمولی، ساعت بدون همگام‌سازی روزی چند صد میلی‌ثانیه تا چند ثانیه عقب یا جلو می‌رود. روی ماشین مجازی این عدد بدتر است، چون مهمان زمان را از هایپروایزر می‌گیرد و اگر هایپروایزر شلوغ باشد، انحراف می‌تواند در چند روز به ده‌ها ثانیه برسد.

چرا ساعت نادرست امضای API را باطل می‌کند

بیشتر APIهای امضاشده (AWS Signature V4، امضای HMAC در درگاه‌های پرداخت، OAuth با exp و iat) یک پنجرهٔ تحمل زمانی دارند. در AWS این پنجره ۱۵ دقیقه است. اگر ساعت سرور شما ۱۶ دقیقه عقب باشد، هر درخواست با RequestTimeTooSkewed رد می‌شود. اگر ۱۶ دقیقه جلو باشد، همان اتفاق می‌افتد ولی با پیام متفاوت.

در JWT ماجرا بدتر است. سرور صادرکننده توکن را با iat (زمان صدور) امضا می‌کند و سرور مصرف‌کننده آن را با ساعت خودش مقایسه می‌کند. اگر ساعت مصرف‌کننده عقب‌تر باشد، توکن «از آینده آمده» و رد می‌شود. این خطا در لاگ معمولاً به شکل Token used before issued یا NotBefore claim is in the future ظاهر می‌شود و خیلی‌ها سراغ کتابخانهٔ JWT می‌روند، در حالی که مشکل در chronyd است.

گواهی SSL و ساعت: خطای واقعی که می‌بینید

اعتبارسنجی گواهی کاملاً وابسته به ساعت است. اگر ساعت سرور از notBefore گواهی عقب‌تر باشد، کلاینت پیام certificate is not yet valid می‌گیرد. اگر جلوتر از notAfter باشد، certificate has expired. هر دو خطا در مرورگر یک شکل دارند و کاربر فکر می‌کند گواهی خراب است.

این‌جا اشتباه می‌کنند: ادمین گواهی را دوباره صادر می‌کند، دوباره نصب می‌کند، باز هم خطا می‌بیند. چون مشکل گواهی نبوده. یک راه تشخیص سریع:

openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates
date -u

اگر تاریخ UTC سرور شما خارج از بازهٔ notBefore تا notAfter باشد، هیچ گواهی‌ای کار نمی‌کند.

منطقه زمانی سرور را درست تنظیم کنید، نه فقط ساعت را

دو مفهوم را قاطی نکنید. ساعت سیستم (UTC) باید هم‌گام با NTP باشد. منطقه زمانی فقط یک نمایش است و روی محاسبات داخلی اثر ندارد. یعنی وقتی date می‌زنید و +0330 می‌بینید، ساعت داخلی همچنان UTC است و همین درست است.

برای تنظیم منطقه زمانی:

timedatectl set-timezone Asia/Tehran
timedatectl set-ntp true
timedatectl status

روی سرورهای قدیمی‌تر که systemd ندارند، معادلش این است:

ln -sf /usr/share/zoneinfo/Asia/Tehran /etc/localtime
echo "Asia/Tehran" > /etc/timezone
hwclock --systohc

خط آخر مهم است و خیلی‌ها جا می‌اندازند. hwclock --systohc ساعت سخت‌افزار (RTC) را با ساعت سیستم هم‌گام می‌کند. اگر این کار را نکنید، بعد از ری‌استارت سرور ساعت به مقدار قبلی برمی‌گردد و شما فکر می‌کنید تنظیمات پاک شده.

chrony یا systemd-timesyncd؟

روی اکثر توزیع‌های مدرن، systemd-timesyncd به‌صورت پیش‌فرض فعال است. برای یک سرور معمولی کافی است. ولی اگر سرور شما مجازی است، بار سنگین دارد، یا ساعت به‌طور مکرر جهش می‌کند، chronyd را انتخاب کنید. تفاوت عملی: chronyd می‌تواند ساعت را به‌آرامی «کج» کند (slew) به‌جای پرش ناگهانی، و در شبکه‌های با تأخیر بالا بهتر عمل می‌کند.

معیارsystemd-timesyncdchronyd
نصب پیش‌فرضبله، روی اکثر توزیع‌هانیاز به نصب جداگانه
دقت در شبکهٔ پرتأخیرمتوسطخوب
پشتیبانی از سرور محلی NTPخیربله
مناسب برای ماشین مجازیقابل قبولبهتر

اگر انتخاب با من باشد: روی هر سروری که با API کار می‌کند یا لاگ متمرکز دارد، chronyd نصب می‌کنم. هزینه‌اش این است که یک سرویس بیشتر باید مدیریت شود و کانفیگ /etc/chrony/chrony.conf را باید بفهمید. روی یک وب‌سرور ساده که فقط PHP اجرا می‌کند، systemd-timesyncd را دست نمی‌زنم.

لاگ‌ها بدون ساعت درست بی‌ارزش‌اند

وقتی دو سرور دارید و یکی ساعتش ۴۰ ثانیه عقب است، ترتیب رویدادها در لاگ متمرکز شما دروغ می‌گوید. یک درخواست که در سرور A شروع شده و در سرور B تمام شده، در Kibana یا Loki برعکس نمایش داده می‌شود. دیباگ کردن یک باگ توزیع‌شده با لاگ ناهم‌گام تقریباً غیرممکن است.

برای اینکه بفهمید هر سرویس کجا لاگ می‌نویسد و چطور می‌توانید زمان‌ها را مقایسه کنید، محل لاگ لینوکس: هر سرویس کجا می‌نویسد؟ مرجع خوبی است. اگر سرویس‌ها را با systemd مدیریت می‌کنید، فایل unit در systemd: بخش‌ها، ری‌استارت و وابستگی نشان می‌دهد چطور ترتیب راه‌اندازی و وابستگی به سرویس زمان را تنظیم کنید.

یک اشتباه رایج در کانتینرها

کانتینر ساعت خودش را ندارد؛ ساعت را از هستهٔ میزبان می‌گیرد. پس اگر ساعت میزبان غلط باشد، همهٔ کانتینرها غلط‌اند. ولی این‌جا یک تله هست: بعضی ایمیج‌ها /etc/localtime را داخل خودشان دارند و آن را روی میزبان mount نمی‌کنند. نتیجه این می‌شود که date داخل کانتینر UTC نشان می‌دهد و بیرون +0330. این تناقض باعث می‌شود لاگ اپلیکیشن و لاگ میزبان با هم نخوانند.

راه‌حل ساده است، ولی باید آگاهانه انجام شود:

docker run -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro ...

یا در docker-compose.yml:

environment:
  - TZ=Asia/Tehran

متغیر TZ فقط روی برنامه‌هایی اثر دارد که آن را می‌خوانند. روی خود ساعت سیستم اثر ندارد. این تفاوت را خیلی‌ها نمی‌دانند و فکر می‌کنند با ست کردن TZ مشکل حل شده.

چطور بفهمیم ساعت واقعاً هم‌گام است

سه دستور، سه سطح اطمینان:

  1. timedatectl — وضعیت کلی و اینکه NTP فعال است یا نه.
  2. chronyc tracking — میزان offset فعلی نسبت به سرور مرجع، بر حسب میلی‌ثانیه.
  3. chronyc sources -v — لیست سرورهای NTP و وضعیت اتصال به هرکدام.

خروجی chronyc tracking چیزی شبیه این است:

Reference ID    : A29FC87B (time.cloudflare.com)
System time     : 0.000034 seconds fast of NTP time
Last offset     : +0.000021 seconds
RMS offset      : 0.000089 seconds

اگر System time بیشتر از چند صد میلی‌ثانیه باشد، یعنی هم‌گام‌سازی هنوز کامل نشده یا سرور NTP شما در دسترس نیست. مقدار زیر ۵۰ میلی‌ثانیه برای اکثر کاربردها کافی است. برای تراکنش‌های مالی یا سیستم‌های توزیع‌شده، زیر ۱۰ میلی‌ثانیه هدف بگذارید.

نکته‌ای که در ایران زیاد به آن برمی‌خوریم: دسترسی به سرورهای NTP عمومی مثل pool.ntp.org گاهی از بعضی دیتاسنترها فیلتر یا کند است. اگر chronyc sources نشان می‌دهد هیچ سروری reachable نیست، از یک سرور NTP داخلی یا سرورهای ntp.irancell.ir استفاده کنید. پورت NTP همان 123/udp است و اگر فایروال شما آن را بسته باشد، هیچ هم‌گام‌سازی‌ای رخ نمی‌دهد. برای مرور سریع پورت‌ها و اینکه کدام‌ها باید باز باشند، مرجع پورت سرویس‌های پرکاربرد و پورت‌هایی که نباید باز باشند را ببینید.

تنظیمات را روی سرورهای خودتان اعمال کنید

اگر روی سرور اختصاصی کار می‌کنید، دسترسی کامل به تنظیمات زمان و فایروال دارید و می‌توانید سرور NTP داخلی هم راه بیندازید. روی سرور اختصاصی این کار ساده‌تر است چون کنترل کامل روی هسته و شبکه دارید. روی سرور ابری هم همین تنظیمات اعمال می‌شود، ولی به یاد داشته باشید که بعضی هایپروایزرها ساعت مهمان را از میزبان می‌کشند و باید هم‌گام‌سازی داخل مهمان را غیرفعال یا تنظیم کنید تا دو منبع زمان با هم نجنگند.

اگر تازه سرور را راه انداخته‌اید و می‌خواهید از پایه درست تنظیمش کنید، انتخاب توزیع لینوکس سرور: راهنمای عملی و بی‌حاشیه کمک می‌کند بفهمید کدام توزیع کدام ابزار زمان را به‌صورت پیش‌فرض دارد. و اگر روی یک هاست مدیریت‌شده کار می‌کنید و دسترسی root ندارید، هاست لینوکس سرورنت گزینه‌ای است که تنظیمات پایهٔ زمان روی آن انجام شده و فقط باید مطمئن شوید اپلیکیشن شما منطقه زمانی درست را می‌خواند.

یک نکتهٔ آخر که در عمل زیاد می‌بینم: بعد از هر تغییر ساعت، سرویس‌هایی که به زمان وابسته‌اند را ری‌استارت کنید. دیتابیس‌ها، صف‌ها، و بعضی اپلیکیشن‌ها زمان را در حافظه کش می‌کنند و تا ری‌استارت، مقدار قدیمی را نگه می‌دارند. یک systemctl restart ساده جلوی ساعت‌ها دیباگ بی‌فایده را می‌گیرد.

پرسش‌های پرتکرار

چرا بعد از ری‌استارت سرور ساعت به حالت قبل برمی‌گردد؟

چون ساعت سخت‌افزار (RTC) با ساعت سیستم هم‌گام نشده است. بعد از هر تغییر، دستور hwclock --systohc را اجرا کنید تا مقدار در RTC ذخیره شود. اگر این کار را نکنید، سرور در بوت بعدی مقدار قدیمی را از سخت‌افزار می‌خواند.

آیا ست کردن TZ=Asia/Tehran برای کانتینر کافی است؟

نه. متغیر TZ فقط روی برنامه‌هایی اثر دارد که آن را می‌خوانند و روی ساعت سیستم اثری ندارد. ساعت کانتینر از هستهٔ میزبان می‌آید. اگر می‌خواهید فایل‌های /etc/localtime و /etc/timezone هم درست باشند، باید آن‌ها را به‌صورت read-only داخل کانتینر mount کنید.

چطور بفهمم ساعت سرور چقدر با زمان واقعی اختلاف دارد؟

دستور chronyc tracking مقدار offset فعلی را بر حسب ثانیه نشان می‌دهد. اگر chronyd نصب نیست، می‌توانید از ntpdate -q pool.ntp.org استفاده کنید که فقط اختلاف را گزارش می‌دهد و ساعت را تغییر نمی‌دهد. مقدار زیر ۵۰ میلی‌ثانیه برای اکثر کاربردها قابل قبول است.

منطقه زمانی سرور را روی UTC بگذارم یا Asia/Tehran؟

برای سرور، UTC را توصیه می‌کنم. لاگ‌ها، دیتابیس و ارتباط بین سرویس‌ها همه با UTC ساده‌تر و بدون ابهام کار می‌کنند. منطقه زمانی محلی را در لایهٔ نمایش (فرانت‌اند یا گزارش‌گیری) اعمال کنید. اگر مجبورید محلی باشد، حداقل مطمئن شوید همهٔ سرورهای یک خوشه یک منطقه زمانی دارند.

آیا این مطلب برایتان مفید بود؟