我们的演示系统,使用 libfaketime 实现了历史时刻的简单复现。鉴于通过反复重启进行来实现 Java 服务的 faketime 重置过于缓慢。故阅读文档并整理了对 faketime 热更新的操作手册。

libfaketime 使用说明

libfaketime 通过使用 LD_PRELOAD 环境变量,在程序载入时优先加载 libfaketime.so.1 共享库。它通过特定虚假时间的逻辑代替 libc 原生提供的时间函数逻辑,实现了为程序指定绝对日期或相对日期的能力。

faketime 设置方式

libfaketime 接受五种提供 faketime 的方式,包括:

  1. 通过设置环境变量 FAKETIME
  2. 通过由环境变量 FAKETIME_TIMESTAMP_FILE 声明的文件
  3. 通过家目录下的 .faketimerc 文件
  4. 通过系统级别的 /etc/faketimerc 文件
  5. 通过设置环境变量FAKETIME_UPDATE_TIMESTAMP_FILE 并执行命令 date -s "<time>"

使用 2/3/4 方式,可以按照语法修改文件内容。由:

  • 环境变量 FAKETIME_CACHE_DURATION 确定的缓存时间,实现 faketime 的热更新
  • 或环境变量 FAKETIME_NO_CACHE=1 放弃缓存,实现 faketime 从文件的实时获取
  • 或未配置环境变量,使用默认缓存时间 10 秒

faketime 设置时间的语法

上述五种设置方式,均支持统一的语法规则。

绝对静止时刻

YYYY-MM-DD hh:mm:ss

该时刻一经设置,任意使用 faketime 获取时间的进程/线程,都将获得这一固定的时刻值

绝对起始时刻

@YYYY-MM-DD hh:mm:ss

以该时刻为起始时间,随自然时间流逝,任意使用 faketime 获取时间的进程/线程,时间顺序增长。

但需要注意,派生的子进程/子线程,如需复用父进程/父线程的时间,需要提前配置环境变量 FAKETIME_DONT_RESET=1

相对时间

[+/-][[:digit:]][m/h/d/y]

与真实系统时间的偏差值作为 faketime。使用如 -15d , -10m , +2y 的语法,实现偏差值的配置。

通过修改文件实现热更新

为了简化问题,我们仅以一个容器化的微服务举例说明。

通过使用文件提供 faketime 配置,libfaketime 将在收到获取时间的系统调用时,从缓存或文件中读取关于 faketime 的配置。

同时,对文件中 faketime 配置的变更。libfaketime 将在缓存超期后,主动从文件读取最新配置。

这个机制能够有效实现对运行中的 Java 程序 faketime 的热更新。

示例 Dockerfile

FROM openjdk:21-bookworm
LABEL maintainer="ffutop"

RUN apt-get update && apt-get install -y make gcc vim tzdata strace \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && wget https://github.com/wolfcw/libfaketime/archive/refs/tags/v0.9.10.tar.gz \
    && tar xvf v0.9.10.tar.gz && cd libfaketime-0.9.10 \
    && make && make install
		&& cd .. && rm -rf libfaketime-0.9.10 && rm v0.9.10.tar.gz

COPY Main.java Main.java
RUN javac Main.java
ENV TZ=Asia/Shanghai
ENV LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
ENV FAKETIME_DONT_RESET=1
ENV FAKETIME_DONT_FAKE_MONOTONIC=1
ENV FAKETIME_TIMESTAMP_FILE=".faketimerc"
ENV FAKETIME_CACHE_DURATION=1

示例 Java 程序

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Scanner;
import java.util.Date;

public class Main {
    public static void main(String... args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("Hit [Enter] to print the current time or Provide [yyyy-MM-dd HH:mm:ss] to update time: ");
            String datetime = scanner.nextLine();
            if ("".equals(datetime)) {
                System.out.printf("CurrentTime is: %s\n\n", new Date());
            } else {
                updateFaketimeFile('@'+datetime);
            }
        }
    }

    static void updateFaketimeFile(String datetime) {
        try {
            Path path = Paths.get(".faketimerc");
            Files.write(path, datetime.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
            System.out.printf("CurrentTime is: %s\n\n", new Date());
        } catch (IOException e) {
            System.err.println("Error update .faketimerc file: " + e.getMessage());
        }
    }
}

通过拦截系统调用 clock_settime 实现热更新

为了简化问题,我们仅以一个容器化的微服务举例说明。

通过使用文件提供 faketime 配置,libfaketime 将在收到获取时间的系统调用时,从缓存或文件中读取关于 faketime 的配置。

虽然获取时间已经被 faketime 取代,但是修改时间仍然通过 clock_settime 系统调用真实地修改了系统时间。

libfaketime 也提供了对修改时间的系统调用拦截,不过,这需要额外的编译指令方能启用。

export FAKETIME_COMPILE_CFLAGS="-DFAKE_SETTIME" && make && make install

示例 Dockerfile

FROM openjdk:21-bookworm
LABEL maintainer="ffutop"

RUN apt-get update && apt-get install -y make gcc vim tzdata strace \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && wget https://github.com/wolfcw/libfaketime/archive/refs/tags/v0.9.10.tar.gz \
    && tar xvf v0.9.10.tar.gz && cd libfaketime-0.9.10 \
    && export FAKETIME_COMPILE_CFLAGS="-DFAKE_SETTIME" && make && make install \
    && cd .. && rm -rf libfaketime-0.9.10 && rm v0.9.10.tar.gz

COPY Main.java Main.java
RUN javac Main.java
ENV TZ=Asia/Shanghai
ENV LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
ENV FAKETIME_DONT_RESET=1
ENV FAKETIME_DONT_FAKE_MONOTONIC=1
ENV FAKETIME_TIMESTAMP_FILE=".faketimerc"
ENV FAKETIME_UPDATE_TIMESTAMP_FILE=1
ENV FAKETIME_CACHE_DURATION=1

示例 Java 程序

这里为方便起见,使用 date -s <time> 来实现修改时间。还有更多操作可以实现触发 clock_settime 系统调用,比如 JNA, JNI 等。

import java.io.IOException;
import java.util.Scanner;
import java.util.Date;

public class Main {
    public static void main(String... args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("Hit [Enter] to print the current time or Provide [yyyy-MM-dd HH:mm:ss] to update time: ");
            String datetime = scanner.nextLine();
            if ("".equals(datetime)) {
                System.out.printf("CurrentTime is: %s\n\n", new Date());
            } else {
                updateDate(datetime);
            }
        }
    }

    static void updateDate(String newDateTime) {
        String command = "date -s '" + newDateTime + "'";
        try {
            Process process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", command });
            int exitCode = process.waitFor();

            if (exitCode == 0) {
                System.out.println("System time has been updated to: " + newDateTime);
            } else {
                System.err.println("Error setting system time. Exit code: " + exitCode);
            }
        } catch (IOException | InterruptedException e) {
            System.err.println("Error setting system time: " + e.getMessage());
        }
    }
}