我的学习笔记

土猛的员外

Timescaledb中文手册-产品介绍

TimescaleDB是一个开源时间序列数据库,针对快速存储和复杂查询进行了优化。 它讲的是“full SQL”,并且像传统的关系数据库一样易于使用,又有着之前NoSQL数据库扩展性。

与这两种替代方案(关系型数据库与NoSQL)所需的权衡相比,TimescaleDB为时间序列数据提供了两全其美的优势:

简单易用

  • PostgreSQL支持的所有SQL的完整SQL接口(包括二级索引,非基于时间的聚合,子查询,JOIN,窗口函数)。
  • 无缝连接任何PostgreSQL的客户端或工具,无需更改。
  • 面向时间(Time-oriented)的功能,API函数和优化。
  • 对数据保留策略的强大支持。

扩展性

  • 用于放大(单个节点)和向外扩展(即将到来)的透明时间/空间分区
  • 高数据写入速率(包括批量提交,内存中索引,事务支持,数据回填支持)。
  • 单个节点上的正确大小的数据分块(二维数据分区),即使在大数据大小时也能确保快速摄取。
  • 跨块和服务器的并行操作

可靠性

  • 基于PostgreSQL开发,功能包形式地扩展。
  • 受益于20多年的PostgreSQL研究经验和基础(包括流式复制,备份)。
  • 灵活的管理选项(与现有的PostgreSQL生态系统和工具兼容)。

本节的其余部分描述了TimescaleDB体系结构的设计和动机,包括时间序列数据的独特之处,以及我们在构建TimescaleDB时如何利用其特性。

接下来:在部分理解TimescaleDB的设计选择时,让我们问:什么是时间序列数据?

什么是时间序列数据?

我们一直在谈论的这个“时间序列数据”是什么,以及它与其他数据的不同之处和原因是什么?

许多应用程序或数据库实际上采用过于狭隘的视图,并将时间序列数据与特定表单的服务器度量标准等同起来:

1
2
3
4
5
6
7
8
9
Name:    CPU

Tags: Host=MyServer, Region=West

Data:
2017-01-01 01:02:00 70
2017-01-01 01:03:00 71
2017-01-01 01:04:00 72
2017-01-01 01:05:01 68

但事实上,在许多监控应用中,通常会收集不同的指标(例如,CPU,内存,网络统计数据,电池寿命)。 因此,分别考虑每个指标并不总是有意义的。 考虑这种替代的“更广泛”的数据模型,它可以保持同时收集的指标之间的相关性。

1
2
3
4
5
6
7
8
9
Metrics: CPU, free_mem, net_rssi, battery

Tags: Host=MyServer, Region=West

Data:
2017-01-01 01:02:00 70 500 -40 80
2017-01-01 01:03:00 71 400 -42 80
2017-01-01 01:04:00 72 367 -41 80
2017-01-01 01:05:01 68 750 -54 79

这种类型的数据还包括更广泛的类别的数据,无论是来自传感器的温度读数,库存的价格,机器的状态,甚至是应用程序的登录次数,皆如此。

时间序列数据是收集以用来表示系统、流程或行为随时间变化的数据

Time-series数据的特征

如果你仔细观察它是如何产生和收集的,那么像TimescaleDB这样的时间序列数据库通常具有以下重要特征:

  • 以时间为中心:数据记录始终具有时间戳。
  • 只写:数据几乎完全仅是写入(INSERT),几乎很少Update。
  • 时间往前推:新数据通常是关于最近的时间间隔,我们更少关于旧间隔的更新或回填缺失数据。

但是,数据的频率或规律性并不重要; 它可以每毫秒或每小时收集一次。 它也可以以规则或不规则的间隔收集(例如,当某些事件发生时,而不是在预定的时间)。

但是有没有数据库长期有时间字段? 与标准关系“业务”数据等其他数据相比,时间序列数据(以及支持它们的数据库)之间的主要区别在于数据的变化是插入,而不是覆盖

Time-series数据到处存在

时间序列数据无处不在,但有些环境特别是在种子中创建。

  • 计算机监控系统:VM,服务器,容器指标(CPU,可用内存,网络/磁盘IOP),服务和应用程序指标(请求率,请求延迟)。
  • 金融交易系统:经典证券,较新的加密货币,支付,交易事件。
  • 物联网:来自工业机器和设备传感器的数据,可穿戴设备,车辆,物理容器,托盘,智能家居的消费设备等。
  • 活动应用程序:用户/客户交互数据,如点击流,综合浏览量,登录,注册等。
  • 商业智能:跟踪关键指标和业务的整体健康状况。
  • 环境监测:温度,湿度,压力,pH值,花粉数,空气流量,一氧化碳(CO),二氧化氮(NO2),颗粒物(PM10)。
  • (和更多)

Next: TimescaleDB’s data model

数据模型

TimescaleDB使用“宽表”数据模型,这在关系数据库的世界中非常普遍。 这使得Timescale与大多数其他时间序列数据库略有不同,后者通常使用“窄表”模型。

在这里,我们讨论为什么我们选择宽表模型,以及我们如何使用物联网(IoT)示例推荐将其用于时间序列数据。

想象一下,由1,000个IoT设备组成的分布式组,旨在以不同的间隔收集环境数据。 这些数据可能包括:

  • 主键: device_id, timestamp
  • 元数据: location_id, dev_type, firmware_version, customer_id
  • 设备指标: cpu_1m_avg, free_mem, used_mem, net_rssi, net_loss, battery
  • 传感器指标: temperature, humidity, pressure, CO, NO2, PM10

举例, 你收集的数据可能类似这样:

timestamp device_id cpu_1m_avg free_mem temperature location_id dev_type
2017-01-01 01:02:00 abc123 80 500MB 72 335 field
2017-01-01 01:02:23 def456 90 400MB 64 335 roof
2017-01-01 01:02:30 ghi789 120 0MB 56 77 roof
2017-01-01 01:03:12 abc123 80 500MB 72 335 field
2017-01-01 01:03:35 def456 95 350MB 64 335 roof
2017-01-01 01:03:42 ghi789 100 100MB 56 77 roof

现在,我们来看看模拟这些数据的各种方法。

窄表模型

大多数时间序列数据库将以下列方式表示此数据:

  • 将每个度量表示为单独的实体(例如,将cpu_1m_avgfree_mem表示为两个不同的事物);
  • 为该指标存储一系列“time”、“value”对;
  • 将元数据值表示为与该度量/标签集组合相关联的“标签集”

In this model, each metric/tag-set combination is considered an individual “time series” containing a sequence of time/value pairs.

Using our example above, this approach would result in 9 different “time series”, each of which is defined by a unique set of tags.

在该模型中,每个度量/标签集组合被认为是包含时间/值对序列的单独“时间序列”。

使用上面的示例,这种方法将产生9个不同的“时间序列”,每个“时间序列”由一组唯一的标签定义。

1
2
3
4
5
6
7
8
9
1. {name:  cpu_1m_avg,  device_id: abc123,  location_id: 335,  dev_type: field}
2. {name: cpu_1m_avg, device_id: def456, location_id: 335, dev_type: roof}
3. {name: cpu_1m_avg, device_id: ghi789, location_id: 77, dev_type: roof}
4. {name: free_mem, device_id: abc123, location_id: 335, dev_type: field}
5. {name: free_mem, device_id: def456, location_id: 335, dev_type: roof}
6. {name: free_mem, device_id: ghi789, location_id: 77, dev_type: roof}
7. {name: temperature, device_id: abc123, location_id: 335, dev_type: field}
8. {name: temperature, device_id: def456, location_id: 335, dev_type: roof}
9. {name: temperature, device_id: ghi789, location_id: 77, dev_type: roof}

这些时间序列的数量与每个标签的基数的叉积成比例, 例如., (# names) × (# device ids) × (# location ids) × (device types).

然后,这些“时间序列”中的每一个都有自己的一组时间/值序列。

现在,如果您独立收集每个指标,几乎没有元数据,这种方法可能会有意义。

但总的来说,我们认为这种方法是有限的。 它失去了数据中的固有结构,使得提出各种有用的问题变得更加困难。 例如:

  • free_mem变为0时系统的状态是什么?
  • cpu_1m_avg如何与free_mem相关联?
  • location_id的平均温度是多少?

我们也发现这种方法在认知上令人困惑。 我们是否真的收集了9个不同的时间序列,或者只收集了一系列具有各种元数据和指标读数的数据?

宽表模型

相比之下,TimescaleDB使用宽表模型,它反映了数据的固有结构。

我们的宽表模型实际上看起来与初始数据流完全相同:

timestamp device_id cpu_1m_avg free_mem temperature location_id dev_type
2017-01-01 01:02:00 abc123 80 500MB 72 42 field
2017-01-01 01:02:23 def456 90 400MB 64 42 roof
2017-01-01 01:02:30 ghi789 120 0MB 56 77 roof
2017-01-01 01:03:12 abc123 80 500MB 72 42 field
2017-01-01 01:03:35 def456 95 350MB 64 42 roof
2017-01-01 01:03:42 ghi789 100 100MB 56 77 roof

这里,每一行都是一个新的读数,在给定的时间内有一组测量和元数据。 这使我们能够保留数据中的关系,并提出比以前更多有趣或探索性的问题。

当然,这不是一种新格式:它是人们在关系数据库中常见的内容。 这也是我们发现这种格式更直观的原因。

使用JOIN应用关联数据

TimescaleDB的数据模型还与关系数据库有另一种相似之处:它支持JOIN。 具体而言,可以在辅助表中存储其他元数据,然后在查询时关联使用该数据。

在我们的示例中,可以有一个单独的位置表,将location_id映射到该位置的其他元数据。 例如:

location_id name latitude longitude zip_code region
42 Grand Central Terminal 40.7527° N 73.9772° W 10017 NYC
77 Lobby 7 42.3593° N 71.0935° W 02139 Massachusetts

然后在查询时,通过加入我们的两个表,可以提出如下问题:zip_code 10017中我们设备的平均free_mem是多少?

如果没有join,则需要对其数据进行非规范化并将所有元数据存储在每个测量行中。 这会造成数据膨胀,并使数据管理变得更加困难。

通过join,可以独立存储元数据,并更轻松地更新映射。

例如,如果我们想要更新location_id 77的“region”(例如,从“Massachusetts”到“Boston”),我们可以进行此更改而无需返回并覆盖历史数据。

架构和概念

概览

TimescaleDB作为PostgreSQL的扩展实现,这意味着Timescale数据库在整个PostgreSQL实例中运行。 扩展模型允许数据库利用PostgreSQL的许多特性,例如可靠性、安全性以及与各种第三方工具的使用。 同时,TimescaleDB利用PostgreSQL的查询规划器、数据模型和在执行引擎中添加钩子,实现可用的高度自定义扩展。

从用户的角度来看,TimescaleDB公开了看起来像单个表(称为超表)的东西,这些表实际上是一个抽象或许多保存数据的单个表的虚拟视图,称为

hypertable and chunks

通过将超表的数据划分为一个或多个维度来创建:所有超表都按时间间隔划分,并且还可以通过诸如设备ID,位置,用户ID等密钥进行分区。我们有时将其称为跨“时空”分区。

术语

Hypertable—超表

与数据交互的主要点是一个超级的、跨所有空间和时间间隔的单个连续表的抽象,以便可以通过标准SQL查询它。

实际上,与TimescaleDB的所有用户交互都是使用超表。 创建表和索引,更改表,插入数据,选择数据等都可以(并且应该)在超表上执行。

超表由具有column名和类型的标准schema定义,至少一列指定时间值,一个(可选)列指定附加分区键。

提示:有关组织数据的各种方法的进一步讨论,请参阅我们的数据模型,具体取决于您的用例; 最简单和最自然的是像许多关系数据库一样的“宽表”。

单个TimescaleDB部署可以存储多个超表,每个超表具有不同的模式。

在TimescaleDB中创建超表需要两个简单的SQL命令:CREATE TABLE(使用标准SQL语法),然后是SELECT create_hypertable()

尽管还可以创建其他索引(并且TimescaleDB支持所有PostgreSQL索引类型),但会在超表上自动创建按时索引和分区键。

Chunk—块

在内部,TimescaleDB自动将每个超表分成,每个块对应于特定时间间隔和分区键空间的区域(使用散列)。 这些分区是不相交的(非重叠),这有助于查询计划程序最小化它必须触摸以解析查询的块集。

每个块都使用标准数据库表实现。(在PostgreSQL内部,块实际上是“父”超表的“子表”。)

块的大小合适,确保表的索引的所有B树都可以在插入期间驻留在内存中。 这可以避免在修改这些树中的任意位置时发生性能颠簸。

此外,通过避免过大的块,我们可以在根据自动保留策略删除已删除的数据时避免昂贵的数据移除操作。 运行时可以通过简单地删除块(内部表)来执行此类操作,而不是删除单个行。

单点vs集群

TimescaleDB在单节点部署和集群部署(开发中)中执行此广泛的分区。 虽然传统上分区仅用于跨多台计算机进行扩展,但它也允许我们甚至在单台计算机上扩展到高写入速率(以及改进的并行化查询)。

TimescaleDB的当前开源版本仅支持单节点部署。 值得注意的是,TimescaleDB的单节点版本已经基于商用机器上超过100亿行的超表进行基准测试,而不会损失INSERT性能。

单节点的优势

在单台机器上扩展数据库性能的一个常见问题是内存和磁盘之间的显着成本/性能折衷。最终,我们的整个数据集将不适合内存,我们需要将数据和索引写入磁盘。

一旦数据足够大以至于我们无法将所有索引页面(例如,B树)放入内存中,那么更新树的随机部分可能涉及从磁盘交换数据。像PostgreSQL这样的数据库为每个表索引保留了一个B树(或其他数据结构),以便有效地找到该索引中的值。因此,当您索引更多列时,问题会更复杂。

但是因为TimescaleDB创建的每个块本身都存储为一个单独的数据库表,所以它的所有索引只构建在这些小得多的表中,而不是代表整个数据集的单个表。因此,如果我们正确调整这些块的大小,我们可以将最新的表(及其B树)完全放入内存中,并避免这种交换到磁盘的问题,同时保持对多个索引的支持。

有关TimescaleDB自适应空间/时间分块的动机和设计的更多信息,请参阅我们的技术博客文章。

为什么使用TimescaleDB要优于关系型数据库?

TimescaleDB提供了超过PostgreSQL或其他传统RDBMS存储时间序列数据的三个主要优势:

1.更高的数据收集率,尤其是在较大的数据库大小。
2.查询性能范围从等效到数量级更大。
3.面向时间的功能。

而且因为TimescaleDB仍然允许你使用全系列的PostgreSQL特性和工具 - 例如,JOIN与关系表,通过PostGIS的地理空间查询,pg_dumppg_restore,任何PostgreSQL的连接器 ,没有理由不使用TimescaleDB来存储时间序列中的系列数据。

更高的收集效率

对于时间序列数据,TimescaleDB实现了比PostgreSQL更高、更稳定的摄取率。正如我们的架构讨论中所描述的那样,只要索引表不再适合内存,PostgreSQL的性能就会开始受到严重影响。

特别是每当插入新行时,数据库需要更新每个表的索引列的索引(例如,B树),这将涉及从磁盘交换一个或多个页面。在这个问题上,投入更多内存只是延迟了这个事情的发生,一旦您的时间序列表在数千万行中,您每秒1万-10万行的吞吐量就会瞬间崩溃到每秒数百行。

TimescaleDB通过大量利用时空分区来解决这个问题,即使在单台机器上运行也是如此。因此,对最近时间间隔的所有写入仅针对保留在内存中的表,因此更新任何二级索引也很快。

基准测试显示了这种方法的明显优势。以下基准测试到10亿行(在一台机器上)模拟一个常见的监控场景,数据库客户端插入包含时间的中等大小的数据批量,设备的标记集和多个数字度量(在本例中为10)。这里,实验是在具有网络连接SSD存储的标准Azure VM(DS4 v2,8核心)上进行的。

img

我们观察到PostgreSQL和TimescaleDB在最开始1M-20M每秒的请求的吞吐量(分别为106K和114K)还差别不大。然而,在大约从50M行开始,PostgreSQL的性能开始断崖式下降。它在最后100M行中的平均值仅为5K行/秒,而TimescaleDB保持其111K行/秒的吞吐量。

简而言之,Timescale在PostgreSQL的总时间的十五分之一内加载十亿行数据库,并且在更大数据写入量时,吞吐量甚至超过PostgreSQL20倍。

我们的TimescaleDB基准测试表明,即使使用单个磁盘,它仍可保持超过100亿行的恒定性能。

此外,当利用单个计算机上的许多磁盘时,用户已经报告了100亿行的稳定性能,无论是在RAID配置中还是使用TimescaleDB支持在多个磁盘上传播单个超级磁盘(通过多个表空间,这是不可能的传统的PostgreSQL表)。

高级或类似的查询性能

在单磁盘机器上,许多只执行索引查找或表扫描的简单查询在PostgreSQL和TimescaleDB之间具有相似的性能。

例如,在具有索引时间,主机名和cpu使用信息的100M行表上,对于每个数据库,以下查询将花费不到5毫秒:

1
2
3
4
5
SELECT date_trunc('minute', time) AS minute, max(user_usage)
FROM cpu
WHERE hostname = 'host_1234'
AND time >= '2017-01-01 00:00' AND time < '2017-01-01 01:00'
GROUP BY minute ORDER BY minute;

涉及对索引进行基本扫描的类似查询在两者之间也具有相同的性能:

1
2
3
SELECT * FROM cpu
WHERE usage_user > 90.0
AND time >= '2017-01-01' AND time < '2017-01-02';

涉及基于时间的GROUP BY的较大查询 - 在面向时间的分析中非常常见 - 通常在TimescaleDB中实现卓越的性能。

例如,当整个(超表)表为一亿行时,触及3300万行的以下查询在TimescaleDB中快5倍,当为十亿行时,大约快2倍。

1
2
3
4
5
6
SELECT date_trunc('hour', time) as hour,
hostname, avg(usage_user)
FROM cpu
WHERE time >= '2017-01-01' AND time < '2017-01-02'
GROUP BY hour, hostname
ORDER BY hour;

此外,在TimescaleDB中,其他可以明确说明时间排序的查询可以更加高效。

例如,TimescaleDB引入了基于时间的“合并追加”优化,以最小化必须处理以执行以下操作的组的数量(假设已知时间已经被排序)。 对于我们的一亿行表,这导致查询延迟比PostgreSQL快了396倍(82ms对32566ms)。

1
2
3
4
5
6
SELECT date_trunc('minute', time) AS minute, max(usage_user)
FROM cpu
WHERE time < '2017-01-01'
GROUP BY minute
ORDER BY minute DESC
LIMIT 5;

我们将很快发布PostgreSQL和TimescaleDB之间更完整的基准比较,以及复制我们基准测试的软件。

我们的查询基准测试的高级结果是,对于我们尝试过的几乎所有查询,TimescaleDB都能实现与PostgreSQL相似或更高(或更高级)的性能

与PostgreSQL相比,TimescaleDB的一个额外成本是更复杂的计划(假设单个超表可以由许多块组成)。 这可以转化为几毫秒的规划时间,这对于极低延迟的查询(<10ms)可能会产生不成比例的影响。

面向时间的特性

TimescaleDB还包括许多传统关系数据库中没有的面向时间的功能。 这些包括特殊的查询优化(如上面的合并追加),它为面向时间的查询提供了一些巨大的性能改进,以及其他面向时间的函数。

面向时间的分析函数

TimescaleDB包括面向时间分析的新功能,包括以下一些功能:

  • 时间分组:标准date_trunc函数的更强大版本,它允许任意时间间隔(例如,5分钟,6小时等),以及灵活的分组和偏移,而不仅仅是秒,分钟,小时, 等等.
  • 最后和第一个聚合:这些函数允许您按另一个列的顺序获取一列的值。 例如,last(temperature, time) 将基于组内的时间(例如,一小时)返回最新温度值。

这些类型的函数可以实现非常自然的面向时间的查询。 例如,以下财务查询打印每个资产的开盘价,收盘价,最高价和最低价。

1
2
3
4
5
6
7
8
SELECT time_bucket('3 hours', time) AS period
asset_code,
first(price, time) AS opening, last(price, time) AS closing,
max(price) AS high, min(price) AS low
FROM prices
WHERE time > NOW() - interval '7 days'
GROUP BY period, asset_code
ORDER BY period DESC, asset_code;

last函数的能力使得通过辅助列(甚至不同于聚合)的排序可以实现一些更强大的查询类型。 例如,财务报告中的一种常见技术是“双时态建模”,它分别说明了从记录观察时起观察的时间。 在这样的模型中,更正作为新行插入(具有更新的time_recorded字段)并且不替换现有数据。

译者注:关于双时态数据,开始比较难理解,看到后面,应该类似于CQRS等的数据建模类型,数据按时间一次次追加而不删除,这样利于溯源分析。双时态建模在时间上做了更多的改进,按事务时间再进行了划分,每个划分可以认为是一个全新的副本,只是这个副本内的事件是不断累加的。

以下查询返回每个资产的每日价格,按最新记录的价格排序。

1
2
3
4
5
6
7
SELECT time_bucket('1 day', time) AS day,
asset_code,
last(price, time_recorded)
FROM prices
WHERE time > '2017-01-01'
GROUP BY day, asset_code
ORDER BY day DESC, asset_code;

关于TimescaleDB’s的更多以时间特征,可以参看API章节。

面向时间数据的管理

TimescaleDB还提供了某些在PostgreSQL中不易获得或不具备的数据管理功能。 例如,在处理时间序列数据时,数据通常会非常快速地建立起来。 因此,您希望编写一个数据保留策略,即“仅存储一周的原始数据”。

事实上,将此与连续聚合的使用相结合是很常见的,因此您可能会保留两个超表:一个包含原始数据,另一个包含已经汇总为精细或小时聚合的数据。 然后,您可能希望在两个(超级)表上定义不同的保留策略,从而将聚合数据存储得更长。

TimescaleDB允许通过其drop_chunks功能在级别而不是在行级别高效删除旧数据。

1
SELECT drop_chunks(interval '7 days', 'conditions');

这将从超表“条件”中删除所有块(文件),这些条件仅包括早于此持续时间的数据,而不是删除块中任何单独的数据行。 这避免了底层数据库文件中的碎片,这反过来又避免了需要在非常大的表中非常昂贵的资料移除。

有关更多详细信息,请参阅我们的数据保留讨论,包括如何自动执行数据保留策略。

为什么使用TimescaleDB优于NoSQL?

与一般NoSQL数据库(例如,MongoDB,Cassandra)或甚至更专业的面向时间的数据库(例如InfluxDB,KairosDB)相比,TimescaleDB提供定性和定量差异:

  • 普通SQL:TimescaleDB为您提供了对时间序列数据的标准SQL查询的强大功能,即使在规模上也是如此。大多数(所有?)NoSQL数据库需要学习新的查询语言或使用最好的“SQL-ish”(它仍然会破坏与现有工具的兼容性)。
  • 操作简单:使用TimescaleDB,您只需要为关系数据和时间序列数据管理一个数据库。否则,用户通常需要将数据插入两个数据库:“正常”关系数据库和第二个时间序列数据库。
  • 可以跨关系数据和时间序列数据执行JOIN
  • 对于各种查询,查询性能更快。更复杂的查询通常是NoSQL数据库上的慢速或全表扫描,而某些数据库甚至不支持许多自然查询。
  • 像PostgreSQL一样管理并继承其对各种数据类型和索引(B树,散列,范围,BRIN,GiST,GIN)的支持。
  • 对地理空间数据的原生支持:存储在TimescaleDB中的数据可以利用PostGIS的几何数据类型,索引和查询。
  • 第三方工具:TimescaleDB支持任何说SQL的东西,包括像Tableau这样的BI工具。

什么时候不要用TimescaleDB?

如果你的应用场景符合以下情况,不要用TimescaleDB:

  • 简单的读取要求:如果您只是想要快速键值查找或单列汇总,则内存或面向列的数据库可能更合适。然而,前者显然不能扩展到相同的数据量,而后者的性能明显低于更复杂的查询。
  • 非常稀疏或非结构化数据:虽然TimescaleDB利用PostgreSQL对JSON / JSONB格式的支持并且非常有效地处理稀疏性(NULL值的位图),但在某些情况下,无模式架构可能更合适,如MongoDB。
  • 压缩是一个优先事项:基准测试显示在ZFS上运行的TimescaleDB大约需要4倍压缩,但压缩优化的列存储可能更适合更高的压缩率。
  • 不频繁或离线分析:如果响应速度慢可接受(或快速响应时间仅限于少量预先计算的指标),并且如果您不希望许多应用程序/用户同时访问该数据,则可能会避免使用完全是一个数据库,而只是将数据存储在分布式文件系统中。





关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


目前正在接受试用中!!!

请将您的称谓、公司名称和使用场景,发邮件到yuanwai@mengjia.net。

也可以直接加微信:lxdhdgss 咨询

更多详情,可查看文章:TorchV Bot开始对外试用了!


最新内容,关注“土猛的员外”公众号