一文吃透 TDengine 上的时区设置

小 T 导读:很多新用户在配置TDengine的时候,偶尔会因为配置了错误的时区(timezone),而导致写入和查询的时间出现错位。今天希望这篇文章,能将日期时间、时间戳以及TDengine在写入和查询时处理时区的行为等描述清楚。

TDengine是涛思数据专为物联网、车联网、工业互联网、IT运维等设计和优化的大数据平台,核心的时序数据库在处理时序数据上有着十分优异的性能。

一般来说,时序数据就是带有时间序列属性的数据。在处理时序数据时,TDengine有着自己独特的方式。但是如果你没有正确理解TDengine在写入和查询上的行为,极可能会因为配置了错误的时区(timezone),而导致写入和查询的时间出现错位。

下面是一个真实用户的例子:

一文吃透 TDengine 上的时区设置 - TDengine Database 时序数据库

从上图中可以看到,用户执行的一条SQL写入了“2021-07-23 07:04:00.000″这个时刻的数据,可是在不同的客户端中,查询出的结果却相差了13个小时。

今天希望通过这篇文章,将日期时间、时间戳以及TDengine在写入和查询时处理时区的行为等描述清楚,并给出如何设置timezone参数的意见,供大家参考。

在开始之前,你需要先了解以下这三点

1. TDengine中用时间戳表示日期时间,以标准的Unix元年时间(UTC时区1970年1月1日0点0分0秒)为原点,支持毫秒、微秒、纳秒三种精度;

2. 在写入时,如果SQL中是本地日期时间格式,TDengine的客户端使用当前生效的timezone配置,将SQL中的日期时间转换为timestamp;同时,也支持使用RFC-3339格式的日期时间进行写入;

3. 在shell中查询时,客户端使用当前生效的timezone配置,将TDengine中存储的timestamp转换为日期时间格式进行显示。

本文所用相关概念

  • 本地日期时间:表示当地的日期时间。12:00是中午吃饭的时间,8:00是早上上班的时间,这是人类习惯的一种表示时间的方式,是不带时区信息的日期和时间,可以当成一个String。例如:2021-07-21 12:00:00.000,表示2021年7月21日正午,时间精度以毫秒记,这个日期时间的表示方法,不带任何时区信息。
  • 时区:地理概念,按照UTC/格林威治时区,把地球划分成向东和向西各12个时区,其中东12区和西12区是一个区。时区可以通过’Asia/shanghai’这样的’地区/城市’的方式表示,也可以用UTC偏移的方式表示。例如:UTC+8,代表东八区,当协调世界时(UTC)时间为凌晨2点的时候,当地的时间为2+8点,即早上10点。
  • RFC 3339:一种表示日期时间的标准格式。RFC 3339是带时区信息的格式,即包含日期时间信息,也有时区信息。例如,以下两个时间在地球上是同一时刻:2019-10-12T07:20:50+00:00,这个表示2019年10月12日,上午7点20分50秒(UTC+0时区),2019-10-12T15:20:50+08:00,这个表示2019年10月12日,下午3点20分50秒(UTC+8时区)。
  • 时间戳:是机器存储和计算时间的方式。以Unix元年(UTC时区1970年1月1日0点0分0秒)开始经过的秒数计算,不同精度的计时方式,可以有不同的时间戳。例如:0,表示UTC时区1970年1月1日凌晨的时间。

本地日期时间、时区信息、时间戳的关系可以参考下面这张图:

一文吃透 TDengine 上的时区设置 - TDengine Database 时序数据库

TDengine如何处理日期时间?

写入

如果在insert语句中,用一个String表示日期时间,插入到TDengine,存在着将这个String解析成timestamp的过程。这个String存在不同的格式,合法的格式包括:

(1)RFC 3339标准的表示方式

(2)yyyy-MM-dd hh:mm:ss

第1种情况——采用RFC 3339标准,那么这个String是带时区信息的,可以明确地将其转换成timestamp。例如:

这里,介绍一个小技巧:使用-r参数启动taos shell时,timestamp类型的数据,将会以时间戳(long值)的形式显示。

# taos -r 
taos> drop table test.weather;
Query OK, 0 of 0 row(s) in database (0.004202s) 

taos> create table test.weather(ts timestamp, f1 float) ;
Query OK, 0 of 0 row(s) in database (0.012690s) 

taos> insert into test.weather values('1970-01-01T08:00:00.000+08:00',22.00) ;
Query OK, 1 of 1 row(s) in database (0.002363s) 

taos> select * from test.weather;
       ts       |     f1      |
========================================
              0        |     22.00000 |
Query OK, 1 row(s) in set (0.001476s)

可以看到,1970-01-01T08:00:00.000+08:00,代表UTC+8时区1970年1月1日上午8:00,这正好对应UTC时区的凌晨,所以在timestamp是0。

第2种情况——在insert语句中使用yyyy-MM-dd hh:mm:ss格式的时间字符串,不含时区信息。这时,taos客户端会采用当前timezone信息,将字符串转化成timestamp。例如:

可以看到,1970-01-01T08:00:00.000+08:00,代表UTC+8时区1970年1月1日上午8:00,这正好对应UTC时区的凌晨,所以在timestamp是0。

第2种情况——在insert语句中使用yyyy-MM-dd hh:mm:ss格式的时间字符串,不含时区信息。这时,taos客户端会采用当前timezone信息,将字符串转化成timestamp。例如:

taos> show variables;
           name           |             value              |
============================================================
timezone                 |  (CST, +0800)                  |  

taos> insert into test.weather(ts, f1) values('1970-01-01 00:00:00.000', 22.00);
Query OK, 1 of 1 row(s) in database (0.001290s) 

taos> select * from test.weather;
       ts       |          f1          |
========================================
 -28800000 |             22.00000 |
Query OK, 1 row(s) in set (0.002220s)

可以看到,insert语句使用了配置文件中的时区信息,和insert语句中的日期时间信息,即“1970-01-01 00:00:00+08:00”,这个值在时间戳中正好代表-28800000。

由此可见,在TDengine中,时间原点是国际通用的Unix元年(UTC时区1970年1月1日凌晨)。

查询

# 在taos.cfg内配置timezone
# cat /etc/taos/taos.cfg | grep timezonetimezone    UTC+0 

# 在shell中查询timezonetaos
> show variables;
           name           |             value              |
============================================================
timezone                 |  (CST, +0800)                  | 

# taos -s "select * from test.weather" -r
Welcome to the TDengine shell from Linux, Client Version:2.0.20.11
Copyright (c) 2020 by TAOS Data, Inc. All rights reserved.
taos> select * from test.weather
       ts       |          f1          |
========================================
            -28800000 |             22.00000 |
Query OK, 1 row(s) in set (0.002564s) 

# taos -s "select * from test.weather"
Welcome to the TDengine shell from Linux, Client Version:2.0.20.11
Copyright (c) 2020 by TAOS Data, Inc. All rights reserved.
taos> select * from test.weather;
           ts            |          f1        |
================================================= 
1969-12-31 16:00:00.000 |             22.00000 |
Query OK, 1 row(s) in set (0.002306s)

可以看到,select语句在查询时,依然存在着从ts转换为一个string串的情况,Tdengine会将ts转换成当前taos client中的时区。

Timezone配置为UTC-8

有些用户不理解,为什么在TDengine中timezone会被配置为UTC-8?原因是,在POSIX标准中,表示时区偏移量的方式和地理的表示方式不一致。参考Wikipedia中的定义,在ISO 8601中,UTC+8为东八区,该时区是以中文为主的时区。那在Unix中,东八区又应该如何表示?请参考下面这个例子:

# date --help
用法:date [选项]... [+格式] 
或:date [-u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]
Display the current time in the given FORMAT, or set the system date.%z +hhmm        
数字时区(例如,-0400)%:z +hh:mm      
数字时区(例如,-04:00)%Z              
按字母表排序的时区缩写 (例如,EDT)
Examples:Show the time on the west coast of the US (use tzselect(1) to find TZ)  
$ TZ='America/Los_Angeles' date 

# 使用TZ='UTC-8'查看当前时间
# TZ='UTC-8' date +'%Y-%m-%d %H:%M:%S %Z %z'
2021-08-01 22:31:29 UTC +0800 

# 使用TZ='UTC'查看当前时间
# TZ='UTC' date +'%Y-%m-%d %H:%M:%S %Z %z'
2021-08-01 14:31:51 UTC +0000 

# 使用TZ='UTC+8'查看当前时间
# TZ='UTC+8' date +'%Y-%m-%d %H:%M:%S %Z %z'
2021-08-01 06:32:06 UTC -0800

可见,在POSIX标准中,UTC-8代表东八区,UTC+8代表西八区。这里与地理上表示时区的习惯是不一致的。在taos.cfg中,TDengine使用的是POSIX Timezone标准。

在JDBC中设置Timezone

在使用JDBC Connector连接TDengine时,可以通过3个途径设置timezone参数,分别为:url、properties和taos.cfg配置文件。

// url
Connection conn = DriverManager.getConnection("jdbc:TAOS://taosdemo.com:6030/test?timezone=UTC-8", "root", "taosdata"); 

// properties
Properties connProps = new Properties();
connProps.setProperty(TSDBDriver.PROPERTY_KEY_USER, "root");
connProps.setProperty(TSDBDriver.PROPERTY_KEY_PASSWORD, "taosdata");
connProps.setProperty(TSDBDriver.PROPERTY_KEY_TIME_ZONE, "UTC-8");

// 当url和properties中都没有制定timezone的情况下,会使用本地配置文件taos.cfg中timezone的配置参数
Connection conn = DriverManager.getConnection("jdbc:TAOS://taosdemo.com:6030/test", connProps); 

参考文档: https://tdengine.com/docs/cn/v2.0/connector/java

总结

最后,我们再回顾一下前文中描述的用户问题:“为什么在不同的客户端中,日期时间会相差13个小时?”执行的insert语句SQL为:

INSERT INTO n802344030600001_w21003 USING mnt_factor_item_data TAGS ("N802344030600001", 'w21003') VALUES ('2021-7-23 07:04:00:000',3, 999,'N802344030600002','w21003',19,'COD','mg/L',4,2,'大空港片区',1,'龙翔北路监测控制站',1,'龙翔北路水监测设备',3,'龙翔北路监测终端');

SQL中是以本地日期时间的格式表示时间戳的,客户端使用了本地的timezone,将这个“2021-7-23 07:04:00:000”转换为timestamp;在查询时,Windows上的shell和Linux的shell都会将timestamp,根据当前生效的timezone,转换成日期时间格式。

参考文献

1. 为什么用UTC+8变成了美国时区?

2. 理解RFC 3339标准

3. RFC 3339标准

4. UTC+8时区的定义