k8s环境下处理容器时间问题的多种姿势

语言: CN / TW / HK

1、背景概述

Linux 环境下,默认安装操作系统时都需要正确设置系统的时区为当前所在的时区

在容器环境下,除了业务镜像外,我们有很多情况都是使用的官方镜像或第三方镜像,而这些镜像一般都不是国人制作。因此使用这些镜像的时候,自然会有一个问题,即容器镜像的默认时区不正确

简而言之,在容器环境中需要处理时间(时区)问题的原因一般有

  • 时间不对,和正确的(例如北京时间)有偏差
  • 时区不对,镜像默认时区和当前时区不符合
  • 某些特殊业务需要临时修改时间。例如电商秒杀业务,将时间设置超前或滞后,在内部测试业务的时间控制功能

2、硬件时钟和系统时间

先来看看操作系统以及容器是如何获取时间的

时钟一般分为硬件时钟(RTC,Real Time Clock)和操作系统时钟(OS,System Clock)

硬件时钟跟运行在 cpu 上的程序是独立不相关的,甚至在服务器关机之后仍然可以正常运行,这就保证了服务器时间的正常运行,硬件时间也有着各种各样的称呼,例如: hardware clock , real time clock , RTC , BIOS clock 以及 CMOS clock 等,在目前主流的服务器都采用 RTC 芯片实现

操作系统时间称为系统时钟或者系统时间,这就是平时在系统中经常接触到的时间,也是应用程序在执行与时间相关的操作会用到的时间,它只是在系统运行时存在,其记录形式为 UTC 时间(the number of seconds since 00:00:00 January 1, 1970 UTC)

硬件时钟和系统时间的关系

硬件时钟是用来保证在操作系统关机之后仍然可以正常计时的必要硬件,而系统时间是我们在日常操作中才会经常使用到的时间,仅仅在操作系统初始化时,操作系统才会去 RTC 芯片中拿到硬件时钟的值,之后便是独立运行和独立计时

时钟的运作机制如下

3、Linux中修改时间

时间依赖时间标准,时间的表示有两个标准: localtimeUTC (Coordinated Universal Time)

  • UTC 是与时区无关的全球时间标准。尽管概念上有差别,UTC 和 GMT (格林威治时间) 是一样的
  • localtime 标准则依赖于当前时区

时间标准由操作系统设定, Windows 默认使用 localtimeMac OS 默认使用 UTCUNIX 系列的操作系统两者都有。使用 Linux 时,最好将硬件时钟设置为 UTC 标准,并在所有操作系统中使用。这样 Linux 系统就可以自动调整夏令时设置,而如果使用 localtime 标准那么系统时间不会根据夏令时自动调整

通过如下命令可以检查当前设置,终端执行

timedatectl status | grep local

硬件时间可以用 hwclock 命令设置,将硬件时间设置为 localtime

timedatectl set-local-rtc 1

硬件时间设置成 UTC ,终端执行

timedatectl set-local-rtc 0

上述命令会自动生成 /etc/adjtime ,无需单独设置

在日常使用中,修改时间一般通过 date 修改日期时间,通过 hwclock 校准硬件时钟

这里提到了 夏令时 ,再分享一个有意思的事情,可能大多数人还不知道,我国在解放后是实行过夏令时的

4、尝试在容器中修改时间

在容器中能否通过 date 修改日期时间,通过 hwclock 校准硬件时钟?

事实上是不可以的,在容器内部通过默认权限修改时间会报错

这是因为容器的隔离是基于 LinuxCapability 机制实现的,可以通过给容器添加 --privileged--cap-add SYS_TIME 来实现目的,但并不推荐,因为这样会直接影响到容器所在主机的时间

Linux 内核中将 timekeeper 设置为全局变量,所以只要去修改系统时间,这个影响就是内核层面的,所以在 docker 的实现中默认是禁止在容器内修改时间的,因为容器与虚拟化的区别就在于是否共享内核,这就意味着一旦在容器中修改了时间,这个影响就是全局性的

5、处理时间问题的多种姿势

前面聊得有点多,该到重点了

k8s 环境下如何处理容器的时间,也就是 pod 的时间

在处理之前,先保证 pod 宿主机 node 的时间同步及时区设置正常,和当前时间一样

# timedatectl
      Local time: Thu 2021-08-26 00:16:28 CST
  Universal time: Thu 2021-08-26 16:16:28 UTC
        RTC time: Thu 2021-08-26 16:16:28
       Time zone: Asia/Shanghai (CST, +0800)
     NTP enabled: yes
NTP synchronized: yes
 RTC in local TZ: no
      DST active: n/a

下面分享处理容器时间的多种方法,主要分为两个方向,校准时间和调整时间

5.1 在Dockerfile中添加时区

为了便于操作,一劳永逸,可以通过在 Dockerfile 中添加时区

# Set timezone
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
		 && echo "Asia/Shanghai" > /etc/timezone

这种做法对于自制的业务镜像来说很方便,也很容易操作,毕竟只需要在通过 Dockerfile 制作业务镜像添加此内容即可

5.2 将时区文件挂载到Pod中

在定义 pod 上层控制器的时候,添加一个用于挂载时区的卷,挂载宿主机的时区文件

...
  containers:
  - name: xxx
...
    volumeMounts:
      - name: timezone
        mountPath: /etc/localtime
  volumes:
    - name: timezone
      hostPath:
        path: /usr/share/zoneinfo/Asia/Shanghai

5.3 通过环境变量定义时区

同样的,在定义 pod 上层控制器的时候,添加一个用于指定时区的环境变量

TZ 环境变量用于设置时区。它由各种时间函数用于计算相对于全球标准时间 UTC (以前称为格林威治标准时间 GMT )的时间。格式由操作系统指定

...
  containers:
  - name: xxx
...
    env:
    - name: TZ
      value: Asia/Shanghai

5.4 通过PodPreset全局修改时间

往往遇到修改 Pod 时区的需求,都是要求所有的 Pod 都在同一个时区,按照前面的方式需要我们对每一个 Pod 手动做这样的操作,在 k8s 环境下更好的方式就是利用 PodPreset 来预设时间, PodPreset 可以在容器启动的时候注入一些信息

PodPreset1.20 版本后被移除了,我也没找到什么原因

如果是 1.20 以前的版本,具体配置方法如下

首先启用 PodPreset

# 在 kube-apiserver 启动参数 -runtime-config 增加 settings.k8s.io/v1alpha1=true;
—runtime-config=rbac.authorization.k8s.io/v1alpha1=true,settings.k8s.io/v1alpha1=true
# 然后在 --admission-control 增加 PodPreset 启用
—admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota,PodPreset

修改好后重启服务,查看是否有 podpresets api 类型

kubectl api-resources |grep podpresets

创建 PodPresents 资源对象

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: tz-env
spec:
  selector:
    matchLabels:
  env:
  - name: TZ
    values: Asia/Shanghai

这里需要注意的地方是,一定需要写 selector...matchLabels ,尽管 matchLabels 为空,表示应用于所有容器,创建上面这个资源对象,然后再去创建一个普通的 Pod 可以查看下是否注入了上面的 TZ 这个环境变量

需要注意的是, PodPresetnamespace 级别的对象,其作用范围只能是同一个命名空间下的容器

5.5 调整时间到预设值

以上方法都是用于校准时间,如果需要在 pod 容器中调整时间,也是有解决办法的,目的是将时间调整到一个预设的时间

这里的方法实现主要原理是在 OS 层面拦截系统时间欺骗应用,实现返回任意的时间给应用层使用

拦截的主要思路是以动态库的加载为基础的,采用 LD_PRELOAD 机制,自行实现这个方法并编译成动态库依靠动态库加载的先后顺序来覆盖原始的方法

已经有 libfaketime项目 实现,按照其文档,主要步骤为

  • 克隆代码进行编译
git clone http://github.com/wolfcw/libfaketime.git
cd libfaketime  && make install
  • 编译完成后,把库文件拷贝到容器中
docker cp /usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1 e6e239e5fba7:/usr/local/lib/
  • 再进入容器中执行命令改变环境变量
export LD_PRELOAD=/usr/local/lib/libfaketime.so.1 FAKETIME="-5d"

容器环境下,手动按照上面的步骤操作是可以生效的,唯一不足的就是一旦容器重启就会失效

在容器( k8s 环境)中如何解决?

前面的步骤可以将编译完的库文件通过 dockerfile 打包到镜像中,如果需要修改时间,只需要在 Pod 控制器定义时添加环境变量即可

...
  containers:
  - name: xxx
...
    env:
    - name: LD_PRELOAD
      value: "/usr/local/lib/libfaketime.so.1"
    - name: FAKETIME
      value: "-5d"

另外一种思路是,时间调整一般是暂时的,以及多 pod 时间同步的需求,将 LD_PRELOAD 的打开与否放到应用的运行环境中,采用 configmap 作为应用时间的标准,将时间变更值 faketime 作为 configmap

apiVersion: v1
kind: ConfigMap
metadata:
  name: faketimerc
  namespace: default
data:
  faketimerc: |
    +10d

最后所有的 pod 都以 volume 的形式挂载该 configmap

...
  containers:
  - name: xxx
...
    volumeMounts:
      - name: faketimerc
        mountPath: /etc/faketimerc
  volumes:
    - name: faketimerc
      configMap:
        name: faketimerc
        items:
        - key: faketimerc
          path: faketimerc

See you ~

参考:

http://developer.toradex.com/knowledge-base/how-to-use-the-real-time-clock-in-linux

http://wiki.deepin.org/wiki/%E6%97%B6%E9%97%B4%E5%92%8C%E6%97%B6%E5%8C%BA

http://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.20.md#deprecation