k8s环境下处理容器时间问题的多种姿势
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 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
- Gradle打包工具入门
- 服务网格和Istio初识-续
- 服务网格和Istio初识
- Golang与非对称加密
- ack集群Terway网络场景下的vSwitch扩容
- Golang与对称加密
- 基于ack k8s集群调度的方案设计
- 基于Dockerfile构建容器镜像的最佳实践
- Golang反射-下篇
- Golang反射-上篇
- Azure DevOps的使用入门
- Golang接口类型-下篇
- Golang接口类型-上篇
- 基于Python实现原生的登录验证码
- Golang开发命令行工具之flag包的使用
- Golang文件操作-下篇
- k8s环境下处理容器时间问题的多种姿势
- Golang基准测试
- 浅谈Prometheus的数据存储
- Golang单元测试