容器日志时间比北京时间慢 8 小时,问题出在 localtime 挂载方式

举报
phantomxjc 发表于 2026/10/18 08:53:22 2026/10/18
【摘要】 有天早上刚到工位,同事拿着截图过来问我:你们那个接口昨晚凌晨两点多是不是挂了?我说不可能啊,那个点我在睡觉,没人改过东西。我打开 Grafana 一看,图上确实显示 02:14 有一波 502。 再翻 nginx 的访问日志,同一批请求写的是 10:14。两张图对不上整整八个小时,一下子就知道是时区的事儿,但具体在哪儿偏差的,还是顺着排查了一遍。 先确认到底是谁的时间不对 服务是用 dockerc

有天早上刚到工位,同事拿着截图过来问我:你们那个接口昨晚凌晨两点多是不是挂了?我说不可能啊,那个点我在睡觉,没人改过东西。我打开 Grafana 一看,图上确实显示 02:14 有一波 502。

再翻 nginx 的访问日志,同一批请求写的是 10:14。两张图对不上整整八个小时,一下子就知道是时区的事儿,但具体在哪儿偏差的,还是顺着排查了一遍。

先确认到底是谁的时间不对

服务是用 docker-compose 起的,docker logs 出来的时间戳才是关键。同一时刻两边各打一次:

date                                   # 宿主机:CST
docker exec nginx-app date             # 容器里:UTC

结果一个 Fri Oct 17 10:14:32 CST,一个 Fri Oct 17 02:14:32 UTC。宿主机没问题,容器里还是 UTC。

大多数官方镜像默认不带时区信息,基础镜像里 /etc/localtime 指向的是 UTC,或者压根没这个文件。容器自己不知道你在东八区,它只知道格林尼治时间,于是每条日志都慢八小时。

第一反应是加 TZ 环境变量,但 alpine 不买账

网上最常见的做法是环境变量:

environment:
  - TZ=Asia/Shanghai

Debian、Ubuntu、CentOS 系的基础镜像一般都带 tzdata,这样写是生效的。但我那个容器用的是 alpine,加了 TZ 之后 date 出来的还是 UTC。原因是 alpine 精简掉了 tzdata,压根读不到 zoneinfo 文件,TZ 设了等于白设。

alpine 得先装:

RUN apk add --no-cache tzdata

嫌镜像变大的话还有个小技巧——装完把用不着的区域删掉,只留中国和 UTC:

RUN apk add --no-cache tzdata \
 && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
 && echo "Asia/Shanghai" > /etc/timezone \
 && apk del tzdata

注意最后那句 apk del tzdata,删掉的是 zoneinfo 文件,但 /etc/localtime 已经是拷出来的实体文件了,不影响。这招在官方镜像的文档里也提过。

更省事的方案是挂载,但 localtime 是个符号链接

不想改 Dockerfile 的话,挂载宿主机的时区文件更直接。很多人这么写:

volumes:
  - /etc/localtime:/etc/localtime:ro

这就是我踩的第二个坑。在 Debian/Ubuntu 宿主机上,/etc/localtime 通常不是真文件,是指向 /usr/share/zoneinfo/Asia/Shanghai 的软链接:

ls -l /etc/localtime
# /etc/localtime -> ../usr/share/zoneinfo/Asia/Shanghai

Docker 用 bind mount 挂这玩意儿的时候,处理的是链接本身而不是它指向的内容。容器里拿到的 /etc/localtime 就成了一个指向不存在路径的断链,时区没变,有时候还会报 "not a directory" 之类的错。

直接挂实体文件就没这个事了:

volumes:
  - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
  - /usr/share/zoneinfo/Asia/Shanghai:/etc/timezone:ro

或者命令行:

docker run -v /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro your-image

验证也简单,改完重启容器再跑一次 docker exec xxx date,看到 CST 就成了。

容器里跑 crond 的话记得重启一下定时任务

我有个定时清理脚本跑在容器里,时区改完之后第二天发现它还是按老时间执行的。

cron 是在进程启动时读取系统时区的,你后来才把 /etc/localtime 换掉,已经在跑的 cron 进程并不知道。得重新拉一次:

service cron restart
# alpine 上
crond restart

这个问题挺隐蔽,因为 date 明明已经对了,很容易误以为全部搞定。

还有两个不在操作系统层面的坑

就算容器时区对了,应用自己也可能再错一次。

一个是 Python 的日志。默认格式化器用的是本地时间,datetime.now() 拿的也是本地时区。我现在的习惯是代码里存 UTC,写时间戳或者带时区的 ISO 格式,展示的时候再转。真要统一输出 UTC,加一行就行:

logging.Formatter.converter = time.gmtime

另一个更常见——数据库连接。MySQL Connector/J 8.x 如果不在 JDBC URL 里指定时区,会按服务端时区去解释 timestamp 字段,写进去读出来差八个小时是家常便饭。补上参数即可:

jdbc:mysql://host:3306/db?serverTimezone=Asia/Shanghai&useSSL=false

最后还是建议统一 UTC

排查完之后我做了个决定:容器内部一律保持 UTC,日志统一 ISO 8601 带 Z,只有给人看的界面(Grafana、报表面板)才转成本地时区。

这么选是因为容器可能跑在任何地方——我自己的 NAS、公司的机器、云端节点,指望每台宿主机都配好东八区不现实。UTC 是所有环节里唯一不会变的锚点,跨机器对齐日志的时候省心得多。这条对我的场景管用,如果你的服务只跑在一台固定机器上,直接配时区也没毛病。

回到开头那个 502,查到最后跟时区毫无关系:那批请求是上游健康检查打过来的,早在上午十点发生。只是因为日志慢了八小时,看上去像凌晨的灵异事件。时间对不上的时候,先把数据源统一了再查业务,不然方向很容易跑偏。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。