TDengine集群搭建与原生使用

一、准备工作与基础知识

TDengine官网:https://www.taosdata.com/

官方集群搭建方案:https://docs.taosdata.com/deployment/deploy/

高可用逻辑架构:https://docs.taosdata.com/tdinternal/arch/#

1、服务器资源分配:

节点
操作系统 TDengine版本
hostname
IP 备注
td1 Centos7 server-2.6.0.30 td1.jiguiquan.com 192.168.56.31 节点1
td2 server-2.6.0.30 td2.jiguiquan.com 192.168.56.32 节点2
td3 server-2.6.0.30 td3.jiguiquan.com 192.168.56.33 节点3
app client-2.6.0.30 app.jiguiquan.com 192.168.56.34 测试用客户端

之所以选择2.0而不是3.0是因为,此时TDengine3.0是2022年9月才刚发布的,太不稳定了,而且API跟TDengine2.0相差太大,遇到问题,很难找到解决方案!

参考:TDengine3.0 踩坑实录

2、TDengine高可用逻辑架构和重点概念:

1668413296933802.png

        一个完整的 TDengine 系统是运行在一到多个物理节点上的,逻辑上,它包含数据节点(dnode)、TDengine 应用驱动(taosc)以及应用(app)。系统中存在一到多个数据节点(dnode),这些数据节点组成一个集群(cluster)。应用通过 taosc 的 API 与 TDengine 集群进行互动

  • 物理节点(pnode):就是指的我们部署用的服务器、虚拟机、docker容器等物理资源;

  • 数据节点(dnode):dnode 是 TDengine 服务器侧执行代码 taosd 在物理节点上的一个运行实例,一个工作的系统必须有至少一个数据节点。dnode 包含零到多个逻辑的虚拟节点(vnode),零或者至多一个逻辑的管理节点(mnode),零或者至多一个逻辑的弹性计算节点(qnode),零或者至多一个逻辑的流计算节点(snode)。—— 可以理解为:物理节点上的一个TDEngine进程,所以一个物理节点(pnode)上可以多个数据节点(dnode)

  • 虚拟节点(vnode):为更好的支持数据分片、负载均衡,防止数据过热或倾斜,数据节点(dnode)被虚拟化成多个虚拟节点(vnode,图中 V2,V3,V4 等)。每个 vnode 都是一个相对独立的工作单元,是时序数据存储的基本单元,具有独立的运行线程、内存空间与持久化存储的路径。—— 每个数据库在被创建时需要指定副本数,也就是vnode的数量,这些vnode共同由所属的vgroup管理

  • 管理节点(mnode):一个虚拟的逻辑单元,负责所有数据节点运行状态的监控和维护,以及节点之间的负载均衡(图中 M)。同时,管理节点也负责元数据(包括用户、数据库、超级表等)的存储和管理,因此也称为 Meta Node。TDengine 集群中可配置多个(最多不超过 3 个)mnode,它们自动构建成为一个虚拟管理节点组(图中 M1,M2,M3)。

  • 虚拟节点组(vgroup):不同数据节点上的 vnode 可以组成一个虚拟节点组(vgroup),采用 RAFT 一致性协议,保证系统的高可用与高可靠。写操作只能在 leader vnode 上进行,系统采用异步复制的方式将数据同步到 follower vnode,这样确保了一份数据在多个物理节点上有拷贝。一个 vgroup 里虚拟节点个数就是数据的副本数。如果一个 DB 的副本数为 N,系统必须有至少 N 数据节点(dnode)。—— 有点类似于kafka的主题副本数必须 <= 节点数

  • 计算节点(qnode):一个虚拟的逻辑单元,运行查询计算任务,也包括基于系统表来实现的 show 命令(图中 Q)。集群中可配置多个 qnode,在整个集群内部共享使用(图中 Q1,Q2,Q3)。qnode 不与具体的 DB 绑定,即一个 qnode 可以同时执行多个 DB 的查询任务。每个 dnode 上至多有一个 qnode。

  • 流计算节点(snode):一个虚拟的逻辑单元,只运行流计算任务(图中 S)。集群中可配置多个 snode,在整个集群内部共享使用(图中 S1,S2,S3)。snode 不与具体的 stream 绑定,即一个 snode 可以同时执行多个 stream 的计算任务。每个 dnode 上至多有一个 snode。

  • Taosc:taosc 是 TDengine 给应用提供的驱动程序(driver),负责处理应用与集群的接口交互,应用都是通过 taosc 而不是直接连接集群中的数据节点与整个集群进行交互的。这个模块负责获取并缓存元数据;将插入、查询等请求转发到正确的数据节点;在把结果返回给应用时,还需要负责最后一级的聚合、排序、过滤等操作。taosc模块是在客户端节点上运行的,可以与taosAdapter交互,支持全分布式的RESTful接口

3、TDEngine2.0一个典型的消息插入过程泳道图:

1668495022290529.png

1、应用通过 JDBC 或其他 API 接口发起插入数据的请求。

2、taosc 会检查缓存,看是否保存有该表的 meta data。如果有,直接到第 4 步。如果没有,taosc 将向 mnode 发出 get meta-data 请求。

3、mnode 将该表的 meta-data 返回给 taosc。Meta-data 包含有该表的 schema,而且还有该表所属的 vgroup 信息(vnode ID 以及所在的 dnode 的 End Point,如果副本数为 N,就有 N 组 End Point)。如果 taosc 迟迟得不到 mnode 回应,而且存在多个 mnode,taosc 将向下一个 mnode 发出请求。

4、taosc 向 master vnode 发起插入请求。

5、vnode 插入数据后,给 taosc 一个应答,表示插入成功。如果 taosc 迟迟得不到 vnode 的回应,taosc 会认为该节点已经离线。这种情况下,如果被插入的数据库有多个副本,taosc 将向 vgroup 里下一个 vnode 发出插入请求。

6、taosc 通知 APP,写入成功。

4、TDEngine3.0一个典型的消息插入过程泳道图:

1668417127966199.png

1、应用通过 JDBC 或其他 API 接口发起插入数据的请求。

2、taosc 会检查缓存,看是否保存有该表所在数据库的 vgroup-info 信息。如果有,直接到第 4 步。如果没有,taosc 将向 mnode 发出 get vgroup-info 请求。

3、mnode 将该表所在数据库的 vgroup-info 返回给 taosc。Vgroup-info 包含数据库的 vgroup 分布信息(vnode ID 以及所在的 dnode 的 End Point,如果副本数为 N,就有 N 组 End Point),还包含每个 vgroup 中存储数据表的 hash 范围。如果 taosc 迟迟得不到 mnode 回应,而且存在多个 mnode,taosc 将向下一个 mnode 发出请求。

4、taosc 会继续检查缓存,看是否保存有该表的 meta-data。如果有,直接到第 6 步。如果没有,taosc 将向 vnode 发出 get meta-data 请求。

5、vnode 将该表的 meta-data 返回给 taosc。Meta-data 包含有该表的 schema。

6、taosc 向 leader vnode 发起插入请求。

7、vnode 插入数据后,给 taosc 一个应答,表示插入成功。如果 taosc 迟迟得不到 vnode 的回应,taosc 会认为该节点已经离线。这种情况下,如果被插入的数据库有多个副本,taosc 将向 vgroup 里下一个 vnode 发出插入请求。

8、taosc 通知 APP,写入成功。

  • 关于第二步:taosc第一次启动时,不知道哪个节点是mnode,所以就直接访问配置中的集群对外提供服务的EP,如果被访问的dnode中并没有配置mnode,那么会在返回结果中告知taosc关于mnode的列表,这样taosc就可以去访问具体的mnode节点了!

  • 关于第四步和第六步:taosc由于没有meta-data的缓存,不知道哪个vnode才是leader,那么会直接向列表中的第一个vnode发起请求,如果这个vnode不是leader,那么不会成功,在返回结果中会告诉哪一个vnode才是leader,这样taosc就可以去访问具体的leader节点了!

  • 上面的整个过程已经被封装在taosc中了,用户无感,另外taosc的缓存机制,可以很大程度上地减少mnode的压力,但是为了防止元数据发生改变,taosc会定时与mnode交互并更新缓存

  • 很明显,TDengine3.0与TDengine2.0的很大不同就是mnode管理节点不再存储meta-data信息,而是存储vgroup-info,meta-data改由vnode存储,可以大大滴减轻mnode管理节点的压力


二、TDengine高可用集群的部署

1、修改三台集群机器的hostname和hosts文件:

# 其他机器对应
[root@td1 ~]# hostnamectl set-hostname td1.jiguiquan.com
[root@td2 ~]# hostnamectl set-hostname td2.jiguiquan.com
[root@td3 ~]# hostnamectl set-hostname td3.jiguiquan.com
[root@app ~]# hostnamectl set-hostname app.jiguiquan.com

[root@td1 ~]# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
127.0.1.1 td1.jiguiquan.com td1

192.168.56.31 td1.jiguiquan.com
192.168.56.32 td2.jiguiquan.com
192.168.56.33 td3.jiguiquan.com

2、下载对应版本的server和client程序:

https://docs.taosdata.com/2.6/get-started/

将server安装包拷贝到集群节点,将client安装包拷贝到应用节点上;

3、解压3台server集群上的安装包并安装:

[root@td1 ~]# mkdir /tdengine

[root@td1 ~]# cd /tdengine/

[root@td1 tdengine]# tar -zxvf TDengine-server-2.6.0.30-Linux-x64.tar.gz

[root@td1 tdengine]# cd TDengine-server-2.6.0.30/
[root@td1 TDengine-server-2.6.0.30]# ls
driver  examples  install.sh  release_note  taos.tar.gz

直接使用 install.sh,即可完成taosd的安装:

[root@td2 TDengine-server-3.0.1.6]# ./install.sh

1668496277403420.png

这地方有2种方式:

  • 节点1时候直接回车,节点2和节点3时候,填写td1.jiguiquan.com:6030;

  • 所有节点都直接回车安装,之后再在节点2和节点3的配置文件/etc/taos/taos.cfg中修改;(我喜欢这种,安装与配置步骤独立开)

虽然安装成功了,但是此时taosd是没有启动的,先不要着急启动!

[root@td1 TDengine-server-2.6.0.30]# systemctl status taosd
● taosd.service - TDengine server service
   Loaded: loaded (/etc/systemd/system/taosd.service; enabled; vendor preset: disabled)
   Active: inactive (dead)

Nov 14 16:44:57 td1.jiguiquan.com systemd[1]: Started TDengine server service.
Nov 14 16:55:04 td1.jiguiquan.com systemd[1]: Stopping TDengine server service...
Nov 14 16:55:06 td1.jiguiquan.com systemd[1]: taosd.service: main process exited, code=killed, status=6/ABRT
Nov 14 16:55:06 td1.jiguiquan.com systemd[1]: Stopped TDengine server service.
Nov 14 16:55:06 td1.jiguiquan.com systemd[1]: Unit taosd.service entered failed state.
Nov 14 16:55:06 td1.jiguiquan.com systemd[1]: taosd.service failed.
Nov 14 16:55:06 td1.jiguiquan.com systemd[1]: Starting TDengine server service...
Nov 14 16:55:06 td1.jiguiquan.com systemd[1]: Started TDengine server service.
Nov 14 17:35:38 td1.jiguiquan.com systemd[1]: Stopping TDengine server service...
Nov 14 17:35:40 td1.jiguiquan.com systemd[1]: Stopped TDengine server service.

4、修改 /etc/taos/taos.cfg 配置文件:

  • firstEp:每个数据节点首次启动后连接的第一个数据节点,此参数每个数据节点的配置一样;

  • fqdn:必须是每个数据节点本地得FQDN;

td1节点:

firstEp                   td1.jiguiquan.com:6030
fqdn                      td1.jiguiquan.com

td2节点:

firstEp                   td1.jiguiquan.com:6030
fqdn                      td2.jiguiquan.com

td3节点:

firstEp                   td1.jiguiquan.com:6030
fqdn                      td3.jiguiquan.com

3.0的这里有个坑(网上包括官网都是按照2.0的教程来的):

  • 在TDengine2.0时候,supportVnodes不用配置,每个dnode支持的vnode数为1024个;

  • 但是盗了TDengine3.0后,如果不配置,默认是cpu*2数量,而我的虚拟机分配的是2个CPU,那么默认的supportVnode=4;

    • 而默认一个数据库副本会创建2个vnode,那么我随便创建一个3副本的数据库,就需要6个vnode,这样直接就会失败,所以一定要手动设置大一点!

5、启动第一个数据节点td1:

[root@td1 TDengine-server-2.6.0.30]# systemctl start taosd
[root@td1 TDengine-server-2.6.0.30]# systemctl status taosd
● taosd.service - TDengine server service
   Loaded: loaded (/etc/systemd/system/taosd.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2022-11-14 10:48:09 UTC; 4s ago
  Process: 23931 ExecStartPre=/usr/local/taos/bin/startPre.sh (code=exited, status=0/SUCCESS)
 Main PID: 23937 (taosd)
   CGroup: /system.slice/taosd.service
           ├─23937 /usr/bin/taosd
           └─23948 /usr/bin/udfd -c /etc/taos/

Nov 14 10:48:09 td1.jiguiquan.com systemd[1]: Starting TDengine server service...
Nov 14 10:48:09 td1.jiguiquan.com systemd[1]: Started TDengine server service.

6、同样方式,我们快速地在app.jiguiquan.com这台机器上安装client客户端:

解压安装:

[root@td1 ~]# mkdir /tdengine

[root@td1 ~]# cd /tdengine/

[root@app tdengine]# tar -zxvf TDengine-client-2.6.0.30-Linux-x64.tar.gz

[root@app tdengine]# cd TDengine-client-2.6.0.30/

[root@app TDengine-client-2.6.0.30]# ls
driver  examples  install_client.sh  taos.tar.gz

[root@app TDengine-client-2.6.0.30]# ./install_client.sh

修改/etc/hosts文件:

[root@app TDengine-client-2.6.0.30]# vim /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
127.0.1.1 app.jiguiquan.com app

192.168.56.31 td1.jiguiquan.com
192.168.56.32 td2.jiguiquan.com
192.168.56.33 td3.jiguiquan.com

7、现在我们就可以用客户端连接我们集群的第一个节点了:

[root@app ~]# taos -h td1.jiguiquan.com
Welcome to the TDengine Command Line Interface, Client Version:3.0.1.6

执行一个简单查询:

taos> show dnodes;
id|        end_point       | vnodes | cores | status | role |      create_time        | offline reason |
=======================================================================================================
1 | td1.jiguiquan.com:6030 |   1    |   2   | ready  | any  | 2022-11-14 17:48:49.678 |                |
Query OK, 1 row(s) in set (0.005056s)

可以看到现在只有一个数据节点!(注意这个id,如果后期要移除节点,就是使用这个id进行操作!)

8、将其他节点动态加入到td1创建的集群中:

启动td2、td3节点的taosd服务:

systemctl start taosd

通过taos客户端sql方式,向集群中添加新节点:

taos> create dnode "td2.jiguiquan.com:6030";
Query OK, 0 row(s) affected in set (0.001902s)

taos> create dnode "td3.jiguiquan.com:6030";
Query OK, 0 row(s) affected in set (0.001275s)

现在查看查看集群中的dnode节点数:

taos> show dnodes;
id|        end_point       | vnodes | cores | status | role |      create_time        | offline reason |
=======================================================================================================
1 | td1.jiguiquan.com:6030 |   1    |   2   | ready  | any  | 2022-11-14 17:48:49.678 |                |
2 | td2.jiguiquan.com:6030 |   1    |   2   | ready  | any  | 2022-11-14 17:56:12.025 |                |
3 | td3.jiguiquan.com:6030 |   0    |   2   | ready  | any  | 2022-11-14 17:56:16.203 |                |
Query OK, 1 row(s) in set (0.005056s)

9、补充:集群搭建常见问题:

加入集群的节点,处于offline状态可能的原因?

  • 物理节点上的taosd服务没有正常启动;

  • 网络不通,检查端口,防火墙,hosts等;

  • 两个独立的集群无法合并为新的集群:即当后续节点加入集群时,如果 /var/lib/taos 目录下有数据,需要先清理掉后才可以成功加入集群;

rm -rf /var/lib/taos/

10、TDengine的卸载:

rmtaos
# server和client卸载命令相同;

三、TDengine集群的使用

1、动态添加节点:

上面集群搭建过程中已经描述;

2、动态删除节点:

当节点上的taosd服务stop时,dnode状态会变味offline状态;

taos> show dnodes;
id|        end_point       |vnodes|cores| status |role|     create_time        |  offline reason  |
===================================================================================================
1 | td1.jiguiquan.com:6030 |   1  |  2  | ready  |any |2022-11-14 17:48:49.678 |                  |
2 | td2.jiguiquan.com:6030 |   1  |  2  | offline|any |2022-11-14 17:56:12.025 |status msg timeout|
3 | td3.jiguiquan.com:6030 |   0  |  2  | ready  |any |2022-11-14 17:56:16.203 |                  |
Query OK, 1 row(s) in set (0.005056s)

然后执行移除操作:

taos> drop dnode 2;
Query OK, 0 row(s) affected in set (0.002013s)

taos> show dnodes;
id|        end_point       | vnodes | cores | status | role |      create_time        | offline reason |
=======================================================================================================
1 | td1.jiguiquan.com:6030 |   1    |   2   | ready  | any  | 2022-11-14 17:48:49.678 |                |
3 | td3.jiguiquan.com:6030 |   0    |   2   | ready  | any  | 2022-11-14 17:56:16.203 |                |
Query OK, 1 row(s) in set (0.005056s)

测试成功后,我们再把td2加回来:

taos> show dnodes;
id|        end_point       | vnodes | cores | status | role |      create_time        | offline reason |
=======================================================================================================
1 | td1.jiguiquan.com:6030 |   1    |   2   | ready  | any  | 2022-11-14 17:48:49.678 |                |
2 | td2.jiguiquan.com:6030 |   1    |   2   | ready  | any  | 2022-11-14 17:56:12.025 |                |
3 | td3.jiguiquan.com:6030 |   0    |   2   | ready  | any  | 2022-11-14 17:56:16.203 |                |
Query OK, 1 row(s) in set (0.005056s)

重新加入时,要记得清楚 /var/lib/taos/ 目录下的文件,同事重启taosd服务;

3、vnode虚拟节点的高可用(多副本):

创建一个3副本的testdb数据库(vnode副本数n <= dnode节点数):

taos> create database testdb replica 3;
Query OK, 0 row(s) affected(0.002795s)

taos> show databases;
name  |      created_time       | ntables | vgroups | replica | quorum |  days  | keep |...| status |
====================================================================================================
log   | 2022-11-14 17:48:50.682 |    21   |     1   |    1    |    1   |   10   |  30  |...|  ready |
testdb| 2022-11-14 18:05:49.103 |     0   |     0   |    3    |    1   |   10   | 3650 |...|  ready |
Query OK, 2 row(s) in set (0.001787s)

## 可以看到testdb的备份数为3,此时还没有vgroup,当创建具体table的时候,就会产生对应的vnode和vgroup;

使用testdb数据库,创建数据表,查看具体的vnode状态:

taos> use testdb;
Database changed.

taos> show vgroups;
Query OK, 0 row(s) in set (0.002732s)

taos> create table test_table(ts timestamp, info binary(40));
Query OK, 0 row(s) affected(0.317088s)

taos> insert into test_table values (now, "Hello jiguiquan!");
Query OK, 1 row(s) affected(0.008044s)

taos> select * from test_table;
           ts  |     info     |
===========================================================
 2022-11-14 18:18:02.035 | Hello jiguiquan! |
Query OK, 1 row(s) in set (0.003478s)

taos> show vgroups;
vgId| tables | status |onlines|v1_dnode|v1_status|v2_dnode|v2_status|v3_dnode|v3_status| compacting |
=====================================================================================================
3   |    1   | ready  |   3   |    3   | leader  |    1   |follower |    2   |follower |     0      |
Query OK, 1 row(s) in set (0.001750s)

## 可以看到,dnodeId=3的节点为这个vgroup的leader节点,而dnodeId=1/2的节点为follower节点;

此时,我们测试停止vnodeId=3的节点的taosd服务。看看是否重新选举:

taos> show vgroups;
vgId| tables | status |onlines|v1_dnode|v1_status|v2_dnode|v2_status|v3_dnode|v3_status| compacting |
=====================================================================================================
3   |    1   | ready  |   3   |    3   | offline |    1   |  leader |    2   |follower |     0      |
Query OK, 1 row(s) in set (0.001750s)

taos> select * from test_table;
           ts            |              info              |
===========================================================
 2022-11-14 18:18:02.035 | Hello jiguiquan!               |
Query OK, 1 row(s) in set (0.005275s)

## 可以看到,重新选举了dnodeId=1的节点为leader;

当dnodeId=3的节点恢复后,会自动加入集群,成为follower!

taos> show vgroups;
vgId| tables | status |onlines|v1_dnode|v1_status|v2_dnode|v2_status|v3_dnode|v3_status| compacting |
=====================================================================================================
3   |    1   | ready  |   3   |    3   |follower |    1   | leader  |    2   |follower |     0      |
Query OK, 1 row(s) in set (0.001750s)

4、mnode管理节点的高可用:

查询mnode管理节点列表:

taos> show mnodes;
id|       end_point        |  role  |        role_time        |       create_time       |
=========================================================================================
1 | td1.jiguiquan.com:6030 | leader | 2022-11-14 18:53:11.793 | 2022-11-14 17:48:49.678 |
Query OK, 1 row(s) in set (0.003479s)

## 明显,现在是有单点故障风险的。

所以我们需要修改 /etc/taos/taos.cfg 配置文件,增加mnode数量:

firstEp                   td1.jiguiquan.com:6030
fqdn                      td2.jiguiquan.com
numOfMnodes               3

之后再次查看mnode列表:

taos> show mnodes;
id|       end_point        |  role  |        role_time        |       create_time       |
=========================================================================================
1 | td1.jiguiquan.com:6030 | leader | 2022-11-14 18:53:11.793 | 2022-11-14 17:48:49.678 |
2 | td2.jiguiquan.com:6030 |follower| 2022-11-14 19:04:37.618 | 2022-11-14 19:04:29.740 |
3 | td3.jiguiquan.com:6030 |follower| 2022-11-14 19:04:48.374 | 2022-11-14 19:04:37.618 |
Query OK, 1 row(s) in set (0.003479s)

此时,我们测试将tdnodeId=1的节点服务停止,看看是否重新选举;

# 此时,由于我们通过td2.jiguiquan.com这个节点接进来:
taos> show mnodes;
id|       end_point        |  role  |        role_time        |       create_time       |
=========================================================================================
1 | td1.jiguiquan.com:6030 |offline | 2022-11-14 18:53:11.793 | 2022-11-14 17:48:49.678 |
2 | td2.jiguiquan.com:6030 | leader | 2022-11-14 19:04:37.618 | 2022-11-14 19:04:29.740 |
3 | td3.jiguiquan.com:6030 |follower| 2022-11-14 19:04:48.374 | 2022-11-14 19:04:37.618 |
Query OK, 1 row(s) in set (0.003479s)

## 可以看到,td1节点已经offline,td2被选为新的leader!

重启td1,会自动加入集群,变成follower:

taos> show mnodes;
id|       end_point        |  role  |        role_time        |       create_time       |
=========================================================================================
1 | td1.jiguiquan.com:6030 |follower | 2022-11-14 18:53:11.793 | 2022-11-14 17:48:49.678 |
2 | td2.jiguiquan.com:6030 | leader | 2022-11-14 19:04:37.618 | 2022-11-14 19:04:29.740 |
3 | td3.jiguiquan.com:6030 |follower| 2022-11-14 19:04:48.374 | 2022-11-14 19:04:37.618 |
Query OK, 1 row(s) in set (0.003479s)

5、对于客户端程序中我们配置连接的端点是固定的,岂不是有单点故障?

这点,client设计中已经考虑了,我们可以配置客户端的 /etc/taos/taos.cfg 配置文件,配置2个Ep:

[root@app ~]# vim /etc/taos/taos.cfg
firstEp                   td1.jiguiquan.com:6030
secondEp                  td2.jiguiquan.com:6030

之后,客户端的连接即可直接变为:

[root@app ~]# taos

官方已经考虑到了!

关于java项目中如果使用TDengine的最佳实践,我会在另一篇文章中描述:

Java项目整合TDengine最佳实践(多数据源)

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐