我们的演示系统,使用 libfaketime 实现了历史时刻的简单复现。鉴于通过反复重启进行来实现 Java 服务的 faketime 重置过于缓慢。故阅读文档并整理了对 faketime 热更新的操作手册。
libfaketime 使用说明
libfaketime 通过使用 LD_PRELOAD
环境变量,在程序载入时优先加载 libfaketime.so.1
共享库。它通过特定虚假时间的逻辑代替 libc
原生提供的时间函数逻辑,实现了为程序指定绝对日期或相对日期的能力。
faketime 设置方式
libfaketime 接受五种提供 faketime 的方式,包括:
- 通过设置环境变量
FAKETIME
- 通过由环境变量
FAKETIME_TIMESTAMP_FILE
声明的文件 - 通过家目录下的
.faketimerc
文件 - 通过系统级别的
/etc/faketimerc
文件 - 通过设置环境变量
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());
}
}
}