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