Index alias — Elastic Stack 实战手册

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 别名,是为一个或多个索引而命名的第二名称,第二名称不得与集群中任何索引同名;只要把第二名称和真实索引建立绑定关系,便可以使用别名对索引进行相关的操作。

970X90.png

· 更多精彩内容,请下载阅读全本《Elastic Stack实战手册》

· 加入创作人行列,一起交流碰撞,参与技术圈年度盛事吧

创作人:杨松柏

别名的优势

索引别名是一个非常好的”工具“,他可以帮助解决以下问题:

  • 如果对写入 Elasticsearch 的数据进行极少的修改,索引别名+ Rollover 可以很好控制每个索引的大小,零停机切换索引;合适的索引大小可以提升数据的查询性能,数据恢复性能。
  • 解耦 client 与索引的强耦合,Elasticsearch 维护人员可以对索引有更灵活的操作空间,且让用户侧无感知。
  • 结合 Reindex 可以很方便的完成索引重建。
  • 过滤别名和路由别名可以在一定程度上帮助提升查询性能。
  • 一个绑定多个索引的别名,如果要查询一次查询这多个索引,别名可以使 uri 变的简洁。

什么是别名

别名,是为一个或多个索引而命名的第二名称,第二名称不得与集群中任何索引同名;只要把第二名称和真实索引建立绑定关系,便可以使用别名对索引进行相关的操作。

别名管理

别名创建

索引别名的 REST 语法如下

#索引别名
PUT /<index>/_alias/<alias>?master_timeout=<time>&timeout=<time>
#过滤别名,路由别名
PUT /<index>/_alias/<alias>?master_timeout=<time>&timeout=<time>
{
  "routing" : "routing_value",
  "filter" : {
    "term" : {
      "filed" : value
    }
  }
}
#以下三种方式同上
POST /<index>/_alias/<alias>
PUT /<index>/_aliases/<alias>
POST /<index>/_aliases/<alias>

URI参数释意

必填参数,参数类型string;该参数可以由逗号分隔的索引,或者用通配符表达式。值也可为_all,表示作用于集群中的所有索引。

必填参数,参数类型string,索引别名,建议名字使用有意义的单词和数字组成。

master_timeout

可选参数,value值的单位可为dhmsmsmicrosnanos; 等待连接到主节点的时间。如果在超时时间阈值之前没有收到响应,则请求失败并返回错误,默认值为30s

timeout

可选参数,value值的单位可为dhmsmsmicrosnanos;请求等待响应的时间。如果在超时时间阈值之前没有收到响应,则请求失败并返回错误,默认值为30s

请求体

创建一个索引别名的时候,通常路由别名和过滤别名需要指定请求体。

filter

必填参数,将过滤参数绑定到别名,使别名具有特定的查询功能;包含此参数的别名,通常将其称作为过滤别名。

routing

可选string类型参数,自定义路由值用于将操作路由到特定分片的;包含此参数的我们通常将其称作为路由别名。

批量创建别名

批量创建别名 REST 语法如下

POST /_aliases?master_timeout=<time>&timeout=<time>
{
  "actions" : [
    { "<action>" : { "alias" : "index-alias", "<must_param>" : "value", "<option_param>" : "value",... } }
  ]
}

请求体参数

actions

必填参数,数组内包含一系列的动作<action>,支持的动作如下

add

为一个索引或多个索引添加一个别名

remove

将别名移除与索引的关联关系

remove_index

删除索引,等效于 delete index API。该动作只对索引别名生效,如果尝试删除索引别名,将会失败。

JSON 体内支持的参数包括必填参数和可选参数。

详情如下:

必填参数

index

参数类型string,支持通配符。如果indices没有指定,则该参数必须指定。

注意:不能向索引别名添加数据流。

indices

参数类型string数组,该数组内的索引将被执行相应的动作。如果index没有指定,则该参数必须指定。

注意事项与index参数相同。

alias

参数类型string,为以逗号分隔或者通配符表示的索引,add,remove,delete别名。如果aliases没有指定,则该参数必须指定。

aliases

参数类型string array,需要进行add,remove,delete的索引别名组。如果alias没有指定,则该参数必须指定。

可选参数 :

filter

query object查询对象体,绑定了过滤查询的别名。如果指定,使用别名进行空查询,将只返回满足过滤条件的文档。

is_hidden

参数类型bool值,默认值为false;如果设置为true,使用通配符表达式别名进行搜索排除,该别名关联的数据将查询不到;除非在请求中使用expand_wildcards参数重写。对于共享同一个别名的所有索引,必须将此属性设置为相同的值。

must_exist

参数类型bool值,默认值为false,如果设置为ture,移除别名时,该别名必须存在。

is_write_index

参数类型bool值,默认值为false;如果设置为true,则可以直接使用该别名对关联的索引进行数据写入或者配置修改等操作;若别名绑定多个索引,则只能存在一个is_write_index值为true的绑定。注意:在同一个索引is_hiddenis_write_index不能同时设置为true;当别名只绑定一个索引时无需现实设定该值为true,该别名具有写权限,但是当别名再次绑定另外一个索引,则别名的写权限取消,除非现实指定is_write_index的值为true

routing

参数类型string,自定义值作为路由计算值,将操作路由到对应的分片上。

index_routing

参数类型string,自定义值作为路由计算值,将写入操作路由到对应的分片上。

search_routing

参数类型string,自定义值作为路由计算值,将查询操作路由到对应的分片上。

别名创建与修改示例

假设已经存在表user1user2,为他们绑定别名,示例子如下:

#为 users1 表创建 index-alias-name1 别名
PUT /users1/_alias/index-alias-name1

#为 users1,users2 表创建 index-alias-name2 别名
PUT /users1,users2/_alias/index-alias-name2

#为 users 开头的索引创建 index-alias-name3别名
PUT /users*/_alias/index-alias-name3

#为 users1 索引添加具有路由和过滤功能的别名 routing-filter-index-alias
PUT users1/_alias/routing-filter-index-alias
{
  "routing" : "12",
  "filter" : {
    "term" : {
      "user_id" : 12
    }
  }
}
#为 users1 索引添加路由别名routing-index-alias,路由计算值为12
PUT users1/_alias/routing-index-alias
{
  "routing" : "12"
}

#为 users1 索引添加过滤别名filter-index-alias,过滤 user_id 为12
PUT users1/_alias/filter-index-alias
{
  "filter" : {
    "term" : {
      "user_id" : 12
    }
  }
}

别名创建成功之后,如果别名与索引的关系,为一个别名只对应一个索引,或者有一个绑定关系的 is_write_index (后文会介绍)值为 true ;那么我们可以通过别名往索引写如数据。

#通过别名,写入数据
PUT index-alias-name1/_bulk?refresh=true
{"index":{}}
{"user_id":"tom123456-user1"}

#通过路由别名,写入数据
PUT routing-index-alias/_bulk?refresh=true
{"index":{}}
{"user_id":"kimchy123456-routing"}

#通过索引,写入数据
PUT users2/_bulk?refresh=true
{"index":{}}
{"user_id":"kimchy123456-user2"}
{"index":{}}
{"user_id":"12"}

插入数据后可以通过别名或者索引查询

#以下三条查询语句的结果是等价的,返回2条数据
GET index-alias-name1/_search
GET users1/_search
GET routing-index-alias/_search

#以下三条查询语句的结果是等价的(如果没有其他 users 开头的索引),返回四条数据
GET users1,users2/_search
GET users*/_search
GET index-alias-name2/_search

#查询 user2 的索引,返回2条数据
GET users2/_search

#返回值为空,因为索引 users1 插入的文档没有 user_id 值为2的
GET filter-index-alias/_search

# 返回值为一条,因为索引 users2 有一条 user_id 值为2的文档
GET index-alias-name3/_search

批量创建索引别名示例

批量创建索引别名,即使用POST /_aliases中定义多个action。为test1test2索引绑定一个名称为alias1的别名。

POST /_aliases
{
  "actions" : [
    { "add" : { "index" : "test1", "alias" : "alias1" } }
    { "add" : { "index" : "test2", "alias" : "alias1" } }
  ]
}

重命名别名

如果我们需要对一个索引进行别名替换,只需要在同一个 API 中简单的先remove掉旧别名,然后绑定新的别名即可;该操作为原子型操作,无需担心别名在短时间内不指向索引

POST /_aliases
{
  "actions" : [
    { "remove" : { "index" : "test1", "alias" : "alias1" } },
    { "add" : { "index" : "test1", "alias" : "alias2" } }
  ]
}

将一个别名关联多个索引

test1添加别名alias1test2添加别名alias2,代码块中的两种方式等效。为索引绑定多个别名的语法与之类似,只需将alias替换aliases数组。

POST /_aliases
{
  "actions" : [
    { "add" : { "index" : "test1", "alias" : "alias1" } },
    { "add" : { "index" : "test2", "alias" : "alias1" } }
  ]
}
# 等效于上面的方式
POST /_aliases
{
  "actions" : [
    { "add" : { "indices" : ["test1", "test2"], "alias" : "alias1" } }
  ]
}

除了以上两种方式外,我们还可以使用glob模式,将别名与多个索引相关联绑定,这种方式只会对集群中已存在的索引生效,不会对之后创建的索引生效。

POST /_aliases
{
  "actions" : [
    { "add" : { "index" : "test*", "alias" : "all_test_indices" } }
  ]
}

如果错误的创建了一个索引,同样可以通过别名的方式来解决。

例如:错误的创建了一个名称为test的索引,而实际需要的索引名称为test_2,但是已经有数据往索引里面写入数据了;为了解决这个问题,首先创建正确的索引名称,然后用一个原子操作,将test别名绑定test_2,同时删除索引test

集群状态中不会发生别名绑定不到索引的情况;但由于索引和搜索涉及多个步骤,正在运行或排队的请求,可能会由于临时不存在索引而失败。

# 创建索引test
PUT test   
#  创建索引test_2 
PUT test_2   
POST /_aliases
{
  "actions" : [
    { "add":  { "index": "test_2", "alias": "test" } },
    { "remove_index": { "index": "test" } }  
  ]
}

新建索引时绑定别名

上述给索引绑定别名,均需要提前创建索引;若需要别名能给对新索引生效,可以在创建索引时进行指定。

PUT test1
{
  "aliases" : {
      "alias1" : { },
      "alias2" : { }
    },
    "mappings" : { },
    "settings" : {}
}

除此之外,还可以在索引模板里面进行指定;

如下代码块创建了一个模版名称为testorder权重为0索引模板,之后只要是以test开头的索引都会绑定别名alias1alias2

PUT _template/test
{
   "order" : 0,
   "index_patterns" : [
      "test*"
    ],
     "aliases" : {
      "alias1" : { },
      "alias2" : { }
    },
     "mappings" : { },
    "settings" : {}
}

别名查看

Get index alias

别名的查看方式有如下几种方式


GET /_alias
GET /_alias/<alias>
GET /<index>/_alias/<alias>?allow_no_indices=true&expand_wildcards=all&local=false&ignore_unavailable=false

路径参数

可选参数,参数类型为string;参数支持单个索引或者以逗号分隔的多个索引再或者通配符表达式形式

可选参数,参数类型为string;参数支持单个别名或者以逗号分隔的多个别名再或者通配符表达式形式

查询参数

allow_no_indices

可选参数,参数类型为Boolean,默认值为true;如果设置为false,任何通配符表达式、索引别名或_all值只针对丢失或关闭的索引,则请求将返回一个错误。即使请求以其他开放索引为目标,此行为也适用。

expand_wildcards

可选参数,参数类型为string,该参数主要用于控制哪些特性的索引的别名可以被查看,参数可取如下几种类型的值:

  • all

匹配所有数据流或索引,包括隐藏的数据流或索引。默认值为 all。

  • open

匹配索引状态为open,非hidden的索引,以及非hidden的数据流。

  • closed

匹配索引状态为closed,非hidden的索引,以及非hidden的数据流(数据流不能够被关闭)。

  • hidden

匹配隐藏的数据流和索引,且索引必须是打开或者关闭状态。

  • none

不接受通配符表达式,即参数<index>中不能包含通配符表达式

ignore_unavailable

可选参数,参数类型Boolean,默认值为false。如果请求路径中<index>有索引不存在,则请求将会发生错误。

local

可选参数,参数类型Boolean,默认值为false。如果设置为true,则仅仅从本地节点获取集群元信息(包括索引别名信息);如果设置为true,则从master节点获取,master的信息最权威,可以避免因为网络等问题,造成的元信息下发不及时,造成的获取元信息有误,但也会增加网络开销。

_cat API

除了通过Get index alias API进行查看索引别名,还可以_cat API进行索引别名查看。

GET _cat/aliases
GET _cat/aliases/<alias>

别名删除

Delete index alias

RESTful API 语法如下

DELETE /<index>/_alias/<alias>?master_timeout=<time>&timeout=<time>
DELETE /<index>/_aliases/<alias>?master_timeout=<time>&timeout=<time>

参数释意

可选参数,参数类型为string;参数支持单个索引或者以逗号分隔的多个索引再或者通配符表达式形式。_all*表示对集群中所有索引。

可选参数,参数类型为string;参数支持单个别名或者以逗号分隔的多个别名再或者通配符表达式形式。_all*表示删除<index>的所有别名。master_timeouttimeout的默认值30s

bulk 删除

使用bulk的方式,同时为一个索引或者一组索引,移除关联的索引别名;假如已经为test1添加了别名alias1test2添加了别名alias2,现在需要进行别名移除,可以执行如下四种操作。

# 移除指定索引的指定别名
POST /_aliases
{
  "actions" : [
    { "remove": { "index" : "test1", "alias" : "alias1" } },
    { "remove": { "index" : "test2", "alias" : "alias2" } }
  ]
} 
POST /_aliases
{
  "actions" : [
    {  "remove": { "indices" : ["test1","test2"], "aliases" : ["alias1","alias2"]}}
  ]
} 
# 移除以test开头的索引的指定别名;使用通配符方式,需要注意影响范围
POST /_aliases
{
  "actions" : [
    { "remove": { "index" : "test*", "alias" : "alias1" } },
    { "remove": { "index" : "test*", "alias" : "alias2" } }
  ]
} 

POST /_aliases
{
  "actions" : [
    { "remove": { "index" : "test*", "aliases" : ["alias1","alias2"] } }
  ]
} 

别名的分类与应用

依据不同的使用场景,我们可以简单把别名分为三类:(1)索引别名(2)过滤别名(3)路由别名

过滤别名

一种创建同一索引的不同”视图“的简便方法。

通过将过滤条件绑定到对应别名,使用不同别名即获取满足不通条件的数据;使用Query DSL定义过滤器。

使用过滤别名,必须得保证过滤字段存在,因此提前创建好索引,并设定该字段的schema

如下示例,首先创建一个名为my-index-000001的索引

PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "user": {
        "properties": {
          "id": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

再为索引添加上过滤别名,并批量插入三个文档

#添加过滤别名
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "my-index-000001",
        "alias": "alias2",
        "filter": { "term": { "user.id": "kimchy" } }
      }
    }
  ]
}
#批量插入文档
PUT my-index-000001/_bulk
{"index":{}}
{"user.id":"kimchy"}
{"index":{}}
{"user.id":"tom"}
{"index":{}}
{"user.id":"jerry"}

使用别名执行一个空搜索GET alias2/_search,和预期的一致只返回user.idkimchy的文档

# 注意:返回内容中省略了与本节无关的内容
{
  "hits" : {
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_type" : "_doc",
        "_id" : "b_VkIXkB9LctWlE3HOtS",
        "_score" : 1.0,
        "_source" : {
          "user.id" : "kimchy"
        }
      }
    ]
  }
}

路由别名

将路由字段绑定到对应的别名,通过别名操作时,会执行默认的路由规则,也可以理解为索引的另外一种“视图”。使用路由别名,在一定程度提升写入和查询的性能,结合过滤别名,可以让操作发送到准确的分片上。

如下示例首先创建一个索引,因为本文演示的 Elasticsearch 集群只有两个数据节点,为了方便观察,设置副本数目为 0,主分片数目设置为 2。

PUT /routing-index-000001
{
  "settings" :{
    "index":{
      "number_of_shards":2,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "properties": {
      "user": {
        "properties": {
          "id": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

创建一个路由别名routing-index-alias1绑定索引routing-index-000001

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "routing-index-000001",
        "alias": "routing-index-alias1",
        "routing": "2"
      }
    }
  ]
}

创建成功之后,使用别名进行的所有操作,都将以 2 计算路由地址,使用别名写入一个文档。

PUT routing-index-alias1/_bulk
{"index":{}}
{"user.id":"kimchy"}

除了创建一个路由别名,也可以用索引在写入文档时,后面加上路由参数;往routing-index-000001索引写入两个文档,并指定路由参数为 12。

PUT routing-index-000001/_bulk?routing=12
{"index":{}}
{"user.id":"tom"}
{"index":{}}
{"user.id":"jerry"}

通过查看GET _cat/shards/routing-index-000001?v&h=index,shard,prirep,docs,node,可以发现routing=12的文档被写到了 1 号分片,routing=2的文档被写到了 0 号分片。

index                shard prirep docs node
routing-index-000001 1     p         2 es-cn-n6w24fib900797tgz-29e2dafd-0003
routing-index-000001 0     p         1 es-cn-n6w24fib900797tgz-29e2dafd-0001

为了验证结果是不是这样的,我可以执行以下 3 种查询进行验证。前两个查询的是等效的,都是以 2 为计算路由值,将返回user.idkimchy一个文档;第三个查询语句将返回以routing=12写入的文档。

GET /routing-index-alias1/_search
GET /routing-index-000001/_search?routing=2
GET /routing-index-000001/_search?routing=12

查询的时候,使用别名查询或者查询参数指定路由值,查询效果是等价的。routing的值并不一定要等于2,只要满足hash函数计算出来的结果一样,定位到分片结果一致。为了保证能够正确查询到文档,建议routing的值和写入的值保持一致。

除了统一设置一个routing,还可以分别设置对不同动作生效的路由。

如下面分别设置查询路由(search_routing)和写入路由(index_routing),使用别名进行操作,查询请求会发送到路由值 12 和 2 对应的分片,写入操作只会写入路由12对应的分片。

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "routing-index-000001",
        "alias": "routing-index-alias2",
        "search_routing": "12,2",
        "index_routing": "12"
      }
    }
  ]
}

如上代码块所示search_routing可以由多个以逗号分隔的路由值,index_routing只能有一个值。

其实这也比较好理解,写入的时候,通过一个路由值计算数据,应该写到具体哪个分片,如果写入有多个路由值,将无法确定写入到哪一个分片;一个给定的分片上,可以有很多拥有不同路由值的文档,因此在查询的时候可以写多个路由值。

使用路由别名查询并且参数重新指定routing值;若search_routing的和路径参数routing的个数大于 2,则取两者的交集作为路由参数;若小于等于2且没有交集,则取search_routing的值(写入路由同理)。如下代码是查询不到任何文档的,因为其取交集后的路由值为 2,而包含tom这个文档写入的路由值为 12,所以将查询到对应的内容。

GET /routing-index-alias2/_search?q=user.id:tom&routing=2

正如所预期的一样返回内容如下

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

索引别名(写权限)

当一个索引别名绑定较多的索引,这时若需要使用别名进行写操作,则需要对其中一对绑定关系进行标注,指定别名对特定索引具有写操作权限,没有标注具有写权限的索引别名即为普通索引别名。具有写权限的索引别名,操作索引别名时,会转化为对真实索引的操作。

索引别名(索引为动词)的应用场景主要包括,reindex索引和Rollover索引。如下所示,将别名write-index-alias1同时绑定索引testtest2,别名与test索引的绑定关系标注了写权限。

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "test",
        "alias": "write-index-alias1",
        "is_write_index": true
      }
    },
    {
      "add": {
        "index": "test2",
        "alias": "write-index-alias1"
      }
    }
  ]
}

使用索引别名进行数据写入

PUT /write-index-alias1/_doc/1
{
  "foo": "bar"
}

通过以下方式可以验证,数据写入到了test索引

#可以获取得到对应文档
GET test/_doc/1

#可以查看到两个索引的文档数量,发现 test 表增加了一个文档
GET _cat/indices/write-index-alias1?v

在进行索引Rollover或者Reindex时,为了做到零停机切换索引;还可以通过Bulk API切换别名与索引绑定的写权限标注,该 API 为原子操作,actions中的动作编写顺序不影响交换执行。

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "test",
        "alias": "write-index-alias1",
        "is_write_index": false
      }
    },
    {
      "add": {
        "index": "test2",
        "alias": "write-index-alias1",
        "is_write_index": true
      }
    }
  ]
}
创作人简介:
杨松柏,目前就职于好未来教育科技集团,任数据平台资深研发工程师。 长期关注
ELK、TiDB、clickhouse 等分布式存储技术,对于 Elasticsearch 和 TiDB 都有深入的
理解。
博客: https://blog.csdn.net/yang52017
相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
索引
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(4)
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(4)
|
JSON API 数据格式
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(2)
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(2)
|
存储 API 索引
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(6)
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(6)
|
API 网络架构 索引
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(1)
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(1)
|
索引
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(5)
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(5)
|
API 索引
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(3)
带你读《Elastic Stack 实战手册》之26:——3.4.2.11.Index alias(3)
|
Java 大数据 索引
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(8)
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(8)
|
索引
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(7)
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(7)
|
索引
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(5)
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(5)
|
API 索引
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(3)
带你读《Elastic Stack 实战手册》之23:——3.4.2.8.Index template(3)
101 0
下一篇
无影云桌面