在开发过程中,发现服务器数据库中保存的时间比期望写入的时间晚 8 小时,于是开始排查问题原因。
项目环境:K8s 集群中运行的 Java 微服务项目,使用 MySQL 数据库。
问题排查
1. 检查数据库时区配置
执行语句
show variables like '%time_zone';
time_zone
的值 SYSTEM
表示其值跟随 system_time_zone
,而在 MySQL 里 CST 既表示中国标准时间 China Standard Time UTC+8:00,也表示美国中部时间 Central Standard Time (USA) UTC-6:00,以及澳大利亚中部时间 Central Standard Time (Australia) UTC+9:30 和古巴标准时间 Cuba Standard Time UTC-4:00 这四个时区的缩写。
为了防止歧义,数据库最保险的配置是将 time_zone
设置为 +08:00
,但是基于实际业务,此操作会影响整个数据库。而且这个时区是可以被 JDBC url 中的时区覆盖的,所以不对数据库配置进行修改。
另外,数据库的时区配置只对 timestamp 类型产生影响,在读取 timestamp 值时会根据时区转换;而 datetime 类型不受影响,存入的值和读出的值始终保持一致。
2. 检查数据库当前时间
执行语句
select now();
数据库时间无误。
3. 检查 Java 配置文件
前面提到数据库时区可以被 JDBC url 中参数覆盖,于是检查 Java 配置文件。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql://${db.host:127.0.0.1}:${db.port:3306}/${db.instance}?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
GMT%2B8
即 GMT+8(GMT:Greenwich Mean Time 格林尼治标准时间),配置无误。
4. 检查虚拟机时间
虚拟机时间无误。
这时候想起来日志打印的时间也不正确,提前了 8 小时。
这时使用微服务提供的 API 查询数据库中的数据,后端返回的查询结果又自动减去了 8 小时,变成了正确时间。于是将问题锁定在 Docker 容器和 JVM 中。
5. 检查 Docker 容器
登入服务器,执行命令。
kubectl exec -it POD_NAME -- /bin/bash
前者为容器的时间,后者为宿主机时间。容器默认采用的是 UTC 时区即协调世界时,但是宿主机默认采用的是 CST 时区,容器的时间比宿主机的时间早 8 小时。
问题定位完成。
解决方案
通过 Dockerfile 对容器进行修改,来同步容器和宿主机的时区。
在构建 Docker 镜像的时候, Dockerfile 加上以下命令:
RUN rm -f /etc/localtime && ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone
重新构建后,容器和宿主机时间一致。
另外,也可以通过将宿主机的时区文件/环境变量挂载到 Docker 容器中,或是在启动 Docker 镜像时通过Java参数 -Duser.timezone=GMT+8
指定 JVM 时区等方法解决该问题。