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中修改时间
时间依赖时间标准,时间的表示有两个标准:localtime
和UTC
(Coordinated Universal Time)
- UTC 是与时区无关的全球时间标准。尽管概念上有差别,UTC 和 GMT (格林威治时间) 是一样的
- localtime 标准则依赖于当前时区
时间标准由操作系统设定,Windows
默认使用localtime
,Mac OS
默认使用UTC
而UNIX
系列的操作系统两者都有。使用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
校准硬件时钟?
事实上是不可以的,在容器内部通过默认权限修改时间会报错
这是因为容器的隔离是基于Linux
的Capability
机制实现的,可以通过给容器添加--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
可以在容器启动的时候注入一些信息
PodPreset
在1.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
这个环境变量
需要注意的是,PodPreset
是namespace
级别的对象,其作用范围只能是同一个命名空间下的容器
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/时间和时区
https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.20.md#deprecation