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 https://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 ~

参考:

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

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

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