先说总结

设置docker时区正确姿势。将下面内容写在Dockerfile里面。(有些alpine镜像可能需要手动安装tzdata)

ENV TZ='Asia/Shanghai'
RUN ln -sf  /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo $TZ > /etc/timezone

问题产生

开发这边需要一个tomcat9的镜像,我从官方拉了一个最新的tomcat:9-jdk8,并修改了时区

RUN ln -sf  /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

但是开发在使用的时候发现date命令显示的时间是对的,但是tomcat日志里面时间还是错的:

image-20211021183556046

于是问题来了,需要查询原因。

排查过程

首先TZ环境变量

因为网上搜索docker时区,大部分人都会修改这个,于是我测试了下

~]# docker run --rm -it -e  TZ='Asia/Shanghai'  tomcat:9-jdk8 bash

然后就发现时区正常了

image-20211021183939367

那么为什么是TZ环境变量

通过百度,我查询到了TZ环境变量和/etc/timezone有关,通过man手册,发现:

TZ环境变量优先级高于系统配置文件/etc/timezone,如果TZ变量设置且生效,那么/etc/timezone就不会加载

~]$ man timezone | cat
...
TZ     If this variable is set its value takes precedence over the system configured timezone.

所以问题就变成了 /etc/localtime/etc/timezone有什么区别?

localtime和timezone

关于它俩的区别,网上大佬已经写的很清楚了。我通过google查得的答案也是一样的。

# 参考:https://blog.csdn.net/kq1983/article/details/89913861
/etc/localtime是用来描述本机时间,而 /etc/timezone是用来描述本机所属的时区

在linux中,有一些程序会自己计算时间,不会直接采用带有时区的本机时间格式,会根据UTC时间和本机所属的时区等计算出当前的时间。 
比如jdk应用,时区为“Etc/UTC”,本机时间改为北京时间,通过java代码中new 出来的时间还是utc时间,所以必须得修正本机的时区。

java与时区

有大佬研究了java获取时区这块的源码:http://kaiwangchen.github.io/2018/09/30/java-timezone-revisited.html

java.base/unix/native/libjava/TimeZone_md.c

可以看到,java程序会先读取TZ环境变量,如果变量为空,则去读取timezone文件

/*
 * findJavaTZ_md() maps platform time zone ID to Java time zone ID
 * using <java_home>/lib/tzmappings. If the TZ value is not found, it
 * trys some libc implementation dependent mappings. If it still
 * can't map to a Java time zone ID, it falls back to the GMT+/-hh:mm
 * form.
 */
/*ARGSUSED1*/
char *
findJavaTZ_md(const char *java_home_dir)
{
    char *tz;
    char *javatz = NULL;
    char *freetz = NULL;

    tz = getenv("TZ");

    if (tz == NULL || *tz == '\0') {
        tz = getPlatformTimeZoneID();
        freetz = tz;
    }

    // snipped
}


#if defined(__linux__) || defined(MACOSX)

/*
 * Performs Linux specific mapping and returns a zone ID
 * if found. Otherwise, NULL is returned.
 */
static char *
getPlatformTimeZoneID()
{
    struct stat statbuf;
    char *tz = NULL;
    FILE *fp;
    int fd;
    char *buf;
    size_t size;
    int res;

#if defined(__linux__)
    /*
     * Try reading the /etc/timezone file for Debian distros. There's
     * no spec of the file format available. This parsing assumes that
     * there's one line of an Olson tzid followed by a '\n', no
     * leading or trailing spaces, no comments.
     */
    if ((fp = fopen(ETC_TIMEZONE_FILE, "r")) != NULL) {
        char line[256];

        if (fgets(line, sizeof(line), fp) != NULL) {
            char *p = strchr(line, '\n');
            if (p != NULL) {
                *p = '\0';
            }
            if (strlen(line) > 0) {
                tz = strdup(line);
            }
        }
        (void) fclose(fp);
        if (tz != NULL) {
            return tz;
        }
    }
#endif /* defined(__linux__) */

    /*
     * Next, try /etc/localtime to find the zone ID.
     */
    RESTARTABLE(lstat(DEFAULT_ZONEINFO_FILE, &statbuf), res);
    if (res == -1) {
        return NULL;
    }

    /*
     * If it's a symlink, get the link name and its zone ID part. (The
     * older versions of timeconfig created a symlink as described in
     * the Red Hat man page. It was changed in 1999 to create a copy
     * of a zoneinfo file. It's no longer possible to get the zone ID
     * from /etc/localtime.)
     */
    if (S_ISLNK(statbuf.st_mode)) {
        char linkbuf[PATH_MAX+1];
        int len;

        if ((len = readlink(DEFAULT_ZONEINFO_FILE, linkbuf, sizeof(linkbuf)-1)) == -1) {
            jio_fprintf(stderr, (const char *) "can't get a symlink of %s\n",
                        DEFAULT_ZONEINFO_FILE);
            return NULL;
        }
        linkbuf[len] = '\0';
        tz = getZoneName(linkbuf);
        if (tz != NULL) {
            tz = strdup(tz);
            return tz;
        }
    }

    /*
     * If it's a regular file, we need to find out the same zoneinfo file
     * that has been copied as /etc/localtime.
     * If initial symbolic link resolution failed, we should treat target
     * file as a regular file.
     */
    RESTARTABLE(open(DEFAULT_ZONEINFO_FILE, O_RDONLY), fd);
    if (fd == -1) {
        return NULL;
    }

    RESTARTABLE(fstat(fd, &statbuf), res);
    if (res == -1) {
        (void) close(fd);
        return NULL;
    }
    size = (size_t) statbuf.st_size;
    buf = (char *) malloc(size);
    if (buf == NULL) {
        (void) close(fd);
        return NULL;
    }

    RESTARTABLE(read(fd, buf, size), res);
    if (res != (ssize_t) size) {
        (void) close(fd);
        free((void *) buf);
        return NULL;
    }
    (void) close(fd);

    tz = findZoneinfoFile(buf, size, ZONEINFO_DIR);
    free((void *) buf);
    return tz;
}

#elif defined(__solaris__)

总结

  • /etc/localtime控制系统所在位置的时间,
  • /etc/timezone控制系统所在的位置

修改localtime不会影响timezone,但是修改timezone却会修改localtime

~]# docker run --rm -it -e  TZ='Asia/Shanghai'  tomcat:9-jdk8 bash
root@1d8059a42062:/usr/local/tomcat# date
Thu Oct 21 19:03:31 CST 2021

修改docker镜像时区时候既要修改localtime,又要修改timezone,保证万无一失

ENV TZ='Asia/Shanghai'
RUN ln -sf  /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo $TZ > /etc/timezone