Please enable Javascript to view the contents

理解容器时区问题的根源及常见解决方法

解决容器时区问题:配置正确的时区信息

 ·  ☕ 6 分钟

问题

为什么要设置时区?

代码处理时间时,同时考虑时区信息,便可避免时区问题带来的显示错误,为什么还要设置时区?

1、容器日志、系统日志信息中的时间显示UTC时间,不直观,影响问题定位与解决。

2、有些应用程序将机器的时区作为默认时区,并希望用户设置时区。

为什么宿主机时区已设置,容器时区还要单独配置?

容器在主机内核上运行,从内核中获取时钟,但时区并非来自内核,而是来自用户空间。因此,在大多数情况下,它们默认使用UTC时间。

1.简介

本文主要介绍Linux如何正确设置时区,然后介绍三种容器设置时区的方法:镜像中修改时区、容器运行时设置时区、K8S集群中设置时区。

2.时区说明

正确设置时区 必须做到以下两点:

  • 配置:指定时区 (通过环境变量$TZ、或通过配置文件/etc/localtime)
  • 文件:被指定的时区时区信息格式文件/usr/share/zoneinfo/$TZ存在(通过安装tzdata库、或从linux宿主机目录下拷贝)

2.1 时区信息格式

在大多数 UNIX 系统中,不同的时区由时区信息格式Time Zone Information Format)定义。这是 20 世纪 80 年代推出的一种二进制文件格式。这些文件可以在IANA 时区数据库中找到(通常位于/usr/share/zoneinfo)。在大多数Linux发行版中,这些文件都是作为发行版的一部分,默认安装。

对于容器基础镜像而言,大部分基础镜像(Ubuntu/Debian/alpine)默认情况下并不包含这个软件包,因此需要手动安装:

1
2
3
4
5
# debian/ubuntu
apt-get install tzdata
# alpine
apk add tzdata
# centos/fedora的基本镜像默认安装了 tzdata 软件包

2.2 如何指定时区

但仅有这些文件还不够,我们还需要在主机(或容器)中指定所需的时区。有两种方法(两种方法选一种即可):

2.2.1 修改/etc/localtime文件

/etc/timezone文件:只有Ubuntu/debian生效,不具备通用性,不推荐。

/etc/localtime文件配置本地系统的全系统时区。它通常是一个指向/usr/share/zoneinfo的软链接,后面跟一个时区数据库名称,如 “Asia/Shanghai”(即 /usr/share/zoneinfo/Asia/Shanghai)。

1
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

2.2.2 声明TZ 环境变量

优先级更高

TZ环境变量设置为时区标识符(如 TZ=Asia/Shanghai)来设置时区,通常只在程序所需时区与主机时区不同的情况下进行设置。它的优先级比/etc/localtime更高。

1
export TZ=Asia/Shanghai

3. 容器配置方法

alpine:3.19.1镜像举例说明

通过运行下面命令

1
docker run --rm -i alpine:3.19.1 date

输出为

1
Wed Mar 13 06:06:18 UTC 2024

可以看出,alpine:3.19.1镜像时区为UTC。Alpine基础镜像默认不包含/usr/share/zoneinfo或/etc/localtime,基于2.时区说明,这些文件是设置容器时区所必需的。

3.1 构建镜像时配置

可以在镜像构建时,安装tzdata软件包,并设置TZ环境变量来完成时区设置。具体Dockerfile内容如下:

1
2
3
4
5
6
7
8
9
FROM alpine:3.19.1

ENV TZ=Asia/Shanghai

# 国内源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

RUN apk upgrade --update \
    && apk add -U tzdata

如果以后时区都不需要更改,且想精简镜像体积。可以使用下面的Dockerfile

  1. 安装 tzdata 软件包
  2. 复制需要的时区信息(例如,/usr/share/zoneinfo/Asia/Shanghai)
  3. 卸载 tzdata 软件包
  4. 重新创建文件夹 /usr/share/zoneinfo/Asia/(因为卸载会删除该文件夹)
  5. 将之前复制的文件放回该目录
  6. 设置时区。可通过TZ环境变量;也可通过/etc/localtime 软连接。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM alpine:3.19.1

ENV TZ=Asia/Shanghai

# 国内源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

RUN \
  apk add tzdata && \
  cp /usr/share/zoneinfo/Asia/Shanghai /tmp && \
  apk del tzdata && \
  mkdir -p /usr/share/zoneinfo/Asia/ && \
  mv /tmp/Shanghai /usr/share/zoneinfo/Asia/

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM alpine:3.19.1

# 国内源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

RUN \
  apk add tzdata && \
  cp /usr/share/zoneinfo/Asia/Shanghai /tmp && \
  apk del tzdata && \
  mkdir -p /usr/share/zoneinfo/Asia/ && \
  mv /tmp/Shanghai /usr/share/zoneinfo/Asia/ && \
  ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

缺点:

  • 并非所有镜像都有包管理器,有些镜像只是FROM scratch
  • 要为所有镜像(尤其是一些不重新构建的中间件镜像)维护一个 Dockerfile,增加复杂性
  • 需要在构建时就确定时区

3.2 容器运行时配置

除了在构建时安装 tzdata 软件包,还可以在容器运行时,从宿主机上加载所需的时区文件,例如:

1
docker run -v /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime -i --rm alpine:3.19.1 date

使用 -v 将主机上的TZif文件直接挂载到容器的/etc/localtime,从而解决了容器时区问题。

如果容器后续需要修改其他时区,建议挂在整个/usr/share/zoneinfo目录

缺点:

  • 提高了运维人员的维护成本,容易出现人为失误
  • 需要提前确定,主机是否安装tzdata

3.3 Pod中手动设置

使用hostPath将K8S节点上的时区文件挂载至容器中。创建tz-test.yaml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: tz-test
spec:
  containers:
  - image: alpine:3.19.1
    name: tz-test
    args:
    - date
    volumeMounts:
    - name: zoneinfo
      mountPath: /etc/localtime
      subPath: Asia/Shanghai
      readOnly: true
  volumes:
  - name: zoneinfo
    hostPath:
      path: /usr/share/zoneinfo
  restartPolicy: OnFailure

缺点:

  • 手动操作,维护成本高
  • 无法保证集群中的所有节点都安装了 tzdata
  • hostPath权限问题,且有安全隐患
  • 如果使用helm chart,则需要定制化修改

3.4 通过k8tz管理

k8tz 可以将时区注入PodCronJobs ,只需极少的工作就能在 pod 和命名空间中自动标准化选定的时区。它可以作为手动工具,在本地自动转换部署yaml和 Pod;也可以作为准入控制器,使用annotations为创建的任何 Pod 完全自动执行流程。

k8tz不使用hostPath,而是分配emptyDir,并注入initContainer,用 TZif 文件填充卷。然后,使用emptyDir/etc/localtime/usr/share/zoneinfo挂载到Pod中的每个容器。为了确保所需的时区有效,它会在所有容器中添加TZ环境变量。

3.4.1 安装

1
2
helm repo add k8tz https://k8tz.github.io/k8tz/
helm install k8tz k8tz/k8tz --set timezone=Asia/Shanghai

运行下面命令测试:

1
kubectl run -it ubuntu --image=alpine:3.19.1 --restart=OnFailure --rm=true --command date

3.4.2 配置

  1. 安装时全局设置
1
helm install k8tz k8tz/k8tz --set timezone=Asia/Shanghai 
  1. 通过Pod注解设置
1
kubectl run -it ubuntu --image=alpine:3.19.1 --restart=OnFailure --rm=true --command date --annotations k8tz.io/timezone=Asia/Shanghai
  1. 通过Namespace注解设置(与Pod相同)

3.4.3 注解说明

Controller的行为可通过 Pod、Namespace对象上的注解进行控制。如果在两个对象中指定了相同的注解,则Pod 优先级更高。

Annotation描述默认值
k8tz.io/injectk8tz 是否注入时区true
k8tz.io/timezone决定设置哪个时区, e.g: Asia/ShanghaiUTC
k8tz.io/strategy决定使注入策略, i.e: hostPath/initContainerinitContainer

缺点:

  • 如果容器下集群,通过docker运行时,运维人员容易失误,遗漏时区设置

4. 总结

Linux系统中设置东八时区,需要两步:

  1. 设置时区:export TZ=Asia/Shanghai
  2. 获取时区文件:从宿主机拷贝文件/usr/share/zoneinfo/Asia/Shanghai, 或者安装tzdata

容器时区设置建议:

  1. 镜像中设置东八时区
  2. k8s集群中通过k8tz

由于国内时区不需要频繁修改,建议需要重新构建的镜像在基础镜像中将时区配置正确;k8s集群中部署k8tz,解决中间件时区设置问题;docker-run运行的中间件,单独维护,并将运行脚本版本化控制。

5.参考

k8tz/k8tz: Kubernetes admission controller and a CLI tool to inject timezones into Pods and CronJobs (github.com)

分享

Hex
作者
Hex
CloudNative Developer

目录