上周把公司一个 .NET 服务从 Windows 搬到 Linux 容器里跑,本地调试一切正常,上了测试环境才发现所有时间都晚了 8 小时。排查下来是两个原因叠在一起,记一下免得下次又忘。
现象:容器里的时间比北京时间晚 8 小时
本地是 Windows,时区是 UTC+8,代码里用 DateTime.Now 取当前时间,没问题。容器基础镜像是 mcr.microsoft.com/dotnet/aspnet:8.0,默认时区是 UTC。所以 DateTime.Now 拿到的是 UTC 时间,比北京时间晚 8 小时。写进库的时间全是错的,下游按时间去查的报表自然也对不上。
顺便说一句,这个 bug 一开始不是我自己发现的,是测试同学凌晨跑批后发现「创建时间」比实际晚 8 小时才提的。我当时还以为是他的机器时区不对,自己本地一跑又正常,差点让他去改电脑设置,现在想想有点不好意思。事实证明本地正常不代表线上正常,时区这种事必须进容器里验证。
根因:两个坑叠在一起
第一个坑是基础镜像(Debian 系)默认没装 tzdata,所以我在代码里想用 TimeZoneInfo.FindSystemTimeZoneById("China Standard Time") 去转时区时,直接抛了异常——因为 Windows 上的时区名在 Linux 上根本不存在,Linux 用的是 "Asia/Shanghai"。第二个坑更隐蔽:代码里到处用 DateTime.Now 而不是 DateTime.UtcNow,时区一变全乱。
定位的过程也费了点劲:先是怀疑代码里哪处硬编码了时区,全局搜了一遍没找到;再怀疑容器时区,进容器执行 date 才发现显示的是 UTC;最后才想到基础镜像压根没装 tzdata,连转换的源数据都没有。这一步当时花了快一个小时,主要是没想到 Debian 精简镜像会缺这个,默认就假设它有时区库。
# Dockerfile 里补上这两行
ENV TZ=Asia/Shanghai
RUN apt-get update && apt-get install -y tzdata && rm -rf /var/lib/apt/lists/*
正确的改法
Dockerfile 加一行 ENV TZ=Asia/Shanghai 并装上 tzdata,是为了让容器内时间对得上,方便看日志。但更关键的是代码:统一用 DateTime.UtcNow 存,展示时再按需要转成北京时间。改完重新部署,库里的时间和日志就对上了。
另外提醒一句,ENV TZ 只对运行时生效,构建阶段如果代码里有依赖时区的逻辑,得在对应的 RUN 步骤之前就设好,否则构建期拿到的还是 UTC。这个我当时没踩到,是后来review 别人的 Dockerfile 才注意到,顺手记下来。
我不确定生产环境是不是该让容器带时区,还是统一 UTC 更干净。现在我们是容器设了 Asia/Shanghai,但代码改成了存 UTC、展示层再转,这样即使哪天容器时区配错了,数据本身也不受影响。这点我没有深究过,只是目前觉得稳妥。