[中文翻译] Kubernetes 中的时区和 k8tz

Kubernetes 中的时区和 k8tz

Kubernetes 已将自己确立为容器编排的事实标准。 它允许我们在节点集群上执行我们的容器并控制它们周围的所有配置,例如安装存储卷、Secret、网络管理等等。

容器在主机的内核上运行,并从那里获得时钟,但时区不是来自内核,而是来自用户空间。 因此,在大多数情况下,它们默认使用协调世界时 (UTC)。

即使代码与时区无关,即使将容器日志与系统日志相关联之类的事情也会让人头疼。一些应用程序使用机器的时区作为默认时区,并希望用户设置时区。当集群中容器的时区不一致时,问题会变得更大。只是没有标准。幸运的是,有几种方法可以解决这个问题并确定我们正在运行的容器的时区。但在我们找到解决方案之前,让我们先了解一下这个问题。

如何设定时区

在大多数 UNIX 系统中,不同的时区由时区信息格式 (TZif) 定义。它是 1980 年代引入的二进制文件格式。可以在 IANA 时区数据库中找到这些文件的可靠且准确的来源。这些文件通常位于 /usr/share/zoneinfo。在大多数发行版中,这些文件是发行版的一部分并且默认安装。当我们谈论 Docker 基础镜像时,它们中的大多数默认情况下都不包含此包,它们需要使用包管理器手动安装或从源代码编译,例如:

1
2
3
4
5
# debian/ubuntu
apt-get install tzdata

# alpine
apk add tzdata

一些基础镜像(例如 CentOS 和 Fedora)附带了开箱即用的 tzdata 包。

但是,拥有这些文件是不够的,我们仍然需要在机器(或容器)的级别上定义所需的时区是什么。 /etc/localtime 文件配置本地系统的系统范围内的时区。它通常是指向 /usr/share/zoneinfo 的符号链接,其次是时区数据库名称,例如“Europe/Amsterdam”(即 /usr/share/zoneinfo/Europe/Amsterdam)。您可以在 Wikipedia - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List上找到可用时区的列表。

指定时区的另一种方法是使用 TZ 环境变量。它可以设置为时区标识符(即 TZ=Europe/Amsterdam),但习惯仅在所需时区与机器时区不同的情况下设置它。设置时,它是在 /etc/localtime 之前。

Docker / Podman 中设置时区

现在,我们了解了时区的来源以及如何设置机器(或容器)的时区,让我们更深入地研究,并尝试证明Docker / Podman 的问题。为此,我们将在一个空的Ubuntu容器中执行命令 date

1
2
$ docker run -i --rm ubuntu:21.04 date
Tue Sep 29 00:00:00 UTC 2021

从输出来看,我们可以看到时区是 UTC。 Ubuntu基本映像不包含 /usr/share/zoneinfo/etc/etc/localtime。默认情况下,正如我们以前所学,需要设置容器时区。 如果我们有选择,我们可以创建将安装 tzdata 软件包并将链接 /etc/localtime 设置到我们所需的时区(Europe/Amsterdam)的链接的 Dockerfile,例如:

1
2
3
4
5
6
7
FROM ubuntu:21.04

RUN \
apt-get update && \
apt-get install -y tzdata && \
ln -sf /usr/share/zoneinfo/Europe/Amsterdam /etc/localtime && \
rm -rf /var/lib/apt/lists/*

如果我们使用 docker build -t ubuntu:amsterdam 构建新镜像并在其中运行 date 命令:

1
2
$ docker run -i --rm ubuntu-amsterdam:0 date
Tue Sep 29 02:00:00 CEST 2021

现在的输出有所不同,时间偏移为 +0200,时区缩写在本例中为 CEST(中欧夏令时)。

但这并不总是需要构建镜像,有几个原因不应该将其视为最佳实践:

  • 并非所有镜像都有包管理器,有些镜像只是从 FROM scratch 开始

  • 为我们依赖的任何镜像维护 Dockerfile 将是一项艰巨的任务

  • 我们必须将这些新镜像上传到我们的集群可以到达的地方

  • 我们可能现在才知道或决定构建时间中的时区应该是什么

我们稍后将讨论更多问题,但让我们首先解决这些问题。

我们可以从主机挂载所需的文件,而不是在构建时安装 tzdata 包,例如:

1
2
docker run -v /usr/share/zoneinfo/Europe/Amsterdam:/etc/localtime -i --rm ubuntu:21.04 date
Tue Sep 29 02:00:00 CEST 2021

所以这要容易得多,我们使用 -v 将 TZif 文件从主机直接挂载到容器的 /etc/localtime 并解决了上述所有问题,我们现在几乎可以运行任何具有所需时区的镜像。

此外,建议挂载整个 /usr/share/zoneinfo 目录,因为应用程序在运行时可能需要有关其他时区的信息,即 -v /usr/share/zoneinfo:/usr/share/zoneinfo:ro

所以现在我们知道如何解决这个问题,让我们继续讨论 Kubernetes。 为此,我们将创建一个 date-pod.yaml 文件,在 Ubuntu 镜像上运行命令 date,类似于我们的 docker run 命令:

1
2
3
4
5
6
7
8
9
10
11
# date-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: date-pod
spec:
containers:
- image: ubuntu:21.04
name: ubuntu
args: ["date"]
restartPolicy: OnFailure

让我们用 kubectl apply -f date-pod.yaml 执行这个 pod 并用 kubectl logs date-pod 打印日志,我们会看到打印的日期类似于:

1
Tue Sep 29 00:00:00 UTC 2021

再说一次,时区是 UTC。 为了解决这个问题,我们将使用 hostPath 卷和 volumeMounts 来挂载时区,这类似于 docker run 命令中的 -v 参数。 创建 date-pod-amsterdam.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: date-pod-amsterdam
spec:
containers:
- image: ubuntu:21.04
name: ubuntu
args:
- date
volumeMounts:
- name: zoneinfo
mountPath: /etc/localtime
subPath: Europe/Amsterdam
readOnly: true
volumes:
- name: zoneinfo
hostPath:
path: /usr/share/zoneinfo
restartPolicy: OnFailure

使用 kubectl apply -f date-pod-amsterdam.yaml 应用它并使用 kubectl logs date-pod-amsterdam 查看日志:

1
Tue Sep 29 02:00:00 CEST 2021

此外,建议挂载整个 /usr/share/zoneinfo 目录,因为应用程序在运行时可能需要有关其他时区的信息。

那么,问题解决了吗? 不完全是…

  • 如果 pod 已经定义了 TZ 环境变量,我们的 /etc/localtime 挂载将被忽略

  • 这是非常手动的工作,对于每个 pod,我们需要添加 hostPath 卷并具有 1 或 2 个 volumeMounts

  • 无法保证集群中的所有节点都安装了 tzdata

  • 我们并不总是有权使用 hostPath 也没有访问节点的权限

  • 如果一些 pod 有时区,而另一些则没有——这只会造成更多混乱

  • 大多数 Helm 包不支持通过 values.yaml 添加这些补丁

所以是时候见见 k8tz 了……

清理创建的资源:kubectl delete pod date-pod date-pod-amsterdam

k8tz 设定时区

k8tz 是一个 Kubernetes 准入控制器和一个将时区注入 Pod 的 CLI 工具。 它可以用作手动工具,在本地自动转换我们的 Deployment 和 Pod,也可以作为准入控制器安装,并使用注释来完全自动化任何创建的 Pod 的过程。 但是,不止于此。 为了解决前面提到的所有问题,k8tz 使用了稍微不同的策略。

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

k8tz准入控制器可以与Helm一起安装,很简单:

1
2
3
$ helm repo add k8tz https://k8tz.github.io/k8tz/

$ helm install k8tz k8tz/k8tz --set timezone=Europe/Amsterdam

现在可以创建 Pod,无需更多配置

1
2
$ kubectl run -i -t ubuntu --image=ubuntu:21.04 --restart=OnFailure --rm=true --command date
Tue Sep 29 02:00:00 CEST 2021

您还可以使用 k8tz.io/timezone 等注解来选择 pod 的时区:

1
2
$ kubectl run -i -t ubuntu --image=ubuntu:21.04 --restart=OnFailure --rm=true --command date --annotations k8tz.io/timezone=Europe/London
Tue Sep 29 01:00:00 BST 2021

或者使用 k8tz.io/inject 为 Pod 禁用时区注入:

1
2
$ kubectl run -i -t ubuntu --image=ubuntu:21.04 --restart=OnFailure --rm=true --command date --annotations k8tz.io/inject=false
Tue Sep 29 00:00:00 UTC 2021

如果出于某种原因你想使用 hostPath 而不是 initContainer,你可以使用 k8tz.io/strategy 注解:

1
2
$ kubectl run -i -t ubuntu --image=ubuntu:21.04 --restart=OnFailure --rm=true --command date --annotations k8tz.io/strategy=hostPath
Tue Sep 29 02:00:00 CEST 2021

这些注释也可以在命名空间中指定,并影响在命名空间中创建的所有 pod。

结论

Kubernetes 中的时区问题有多种解决方案,这些解决方案可以手动实现,但在此过程中存在一些挑战和限制。使用 k8tz,您可以自动执行该过程,并且无需额外设置或更改现有资源即可工作,即使节点上没有所需文件也是如此。确保系统中所有组件的时区一致,并且所有组件都可以访问有关不同时区的信息。

k8tz 是免费的开源项目,请查看:github.com/k8tz/k8tz

免责声明:我是 k8tz 的作者。

参考链接

[1] List of tz database time zones - Wikipedia - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List

[2] k8tz/k8tz: Kubernetes admission controller and a CLI tool to inject timezones into Pods and CronJobs - https://github.com/k8tz/k8tz


原文链接

Timezone in Kubernetes With k8tz. k8tz is a great tool to automate… | by Yonatan Kahana | Medium - https://medium.com/@yonatankahana/timezone-in-kubernetes-with-k8tz-fdefca785238