《三次给你聊清楚Redis》之简单聊聊多机实现

举报
兔老大 发表于 2021/04/20 23:43:21 2021/04/20
【摘要】 四、多机实现4.1、旧版复制Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作:同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。命令传播操作用于在主服务器的数据库状态被修改, 导致主从服务器的数据库状态出现不一致时, 让主从服务器的数据库重新回到一致状态。同步当客户端向从服务器发送 SLAVEOF 命令, 要求从服务器复制...


四、多机实现

4.1、旧版复制

Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作:

  • 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
  • 命令传播操作用于在主服务器的数据库状态被修改, 导致主从服务器的数据库状态出现不一致时, 让主从服务器的数据库重新回到一致状态。

同步

当客户端向从服务器发送 SLAVEOF 命令, 要求从服务器复制主服务器时, 从服务器首先需要执行同步操作, 也即是, 将从服务器的数据库状态更新至主服务器当前所处的数据库状态。

从服务器对主服务器的同步操作需要通过向主服务器发送 SYNC 命令来完成, 以下是 SYNC 命令的执行步骤:

  1. 从服务器向主服务器发送 SYNC 命令。
  2. 收到 SYNC 命令的主服务器执行 BGSAVE 命令, 在后台生成一个 RDB 文件, 并使用一个缓冲区记录从现在开始执行的所有写命令。
  3. 当主服务器的 BGSAVE 命令执行完毕时, 主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器, 从服务器接收并载入这个 RDB 文件, 将自己的数据库状态更新至主服务器执行 BGSAVE 命令时的数据库状态。
  4. 主服务器将记录在缓冲区里面的所有写命令发送给从服务器, 从服务器执行这些写命令, 将自己的数据库状态更新至主服务器数据库当前所处的状态。

命令传播

在同步操作执行完毕之后, 主从服务器两者的数据库将达到一致状态, 但这种一致并不是一成不变的 —— 每当主服务器执行客户端发送的写命令时, 主服务器的数据库就有可能会被修改, 并导致主从服务器状态不再一致。

举个例子, 假设一个主服务器和一个从服务器刚刚完成同步操作, 它们的数据库都保存了相同的五个键 k1 至 k5

如果这时, 客户端向主服务器发送命令 DEL k3 , 那么主服务器在执行完这个 DEL 命令之后, 主从服务器的数据库将出现不一致: 主服务器的数据库已经不再包含键 k3 , 但这个键却仍然包含在从服务器的数据库里面

为了让主从服务器再次回到一致状态, 主服务器需要对从服务器执行命令传播操作: 主服务器会将自己执行的写命令 —— 也即是造成主从服务器不一致的那条写命令 —— 发送给从服务器执行, 当从服务器执行了相同的写命令之后, 主从服务器将再次回到一致状态。

缺陷

 其中可以明显看出重新连接主服务器之后,SYNC命令创建包含k1-k10089的RDB文件。而事实上只需要再同步断线后的k10087-k10089即可。SYNC的“全同步”对于从服务来说是不必要的。

           SYNC命令非常消耗资源,原因有三点:

    1)主服务器执行BGSAVE命令生成RDB文件,这个生成过程会大量消耗主服务器资源(CPU、内存和磁盘I/O资源)

    2)主服务器需要将自己生成的RBD文件发送给从从服务器,这个发送操作会消耗主从服务器大量的网络资源(带宽与流量)

    3)接收到RDB文件你的从服务器需要载入RDB文件,载入期间从服务器会因为阻塞而导致没办法处理命令请求。

4.2新版复制

sync虽然解决了数据同步问题,但是在数据量比较大情况下,从库断线从来依然采用全量复制机制,无论是从数据恢复、宽带占用来说,sync所带来的问题还是很多的。于是redis从2.8开始,引入新的命令psync。

psync有两种模式:完整重同步和部分重同步。

部分重同步主要依赖三个方面来实现,依次介绍。

offset(复制偏移量):

主库和从库分别各自维护一个复制偏移量(可以使用info replication查看),用于标识自己复制的情况:

在主库中代表主节点向从节点传递的字节数,在从库中代表从库同步的字节数。

每当主库向从节点发送N个字节数据时,主节点的offset增加N

从库每收到主节点传来的N个字节数据时,从库的offset增加N。

因此offset总是不断增大,这也是判断主从数据是否同步的标志,若主从的offset相同则表示数据同步量,不通则表示数据不同步。

replication backlog buffer(复制积压缓冲区):

  复制积压缓冲区是一个固定长度的FIFO队列,大小由配置参数repl-backlog-size指定,默认大小1MB。

需要注意的是该缓冲区由master维护并且有且只有一个,所有slave共享此缓冲区,其作用在于备份最近主库发送给从库的数据。

  在主从命令传播阶段,主节点除了将写命令发送给从节点外,还会发送一份到复制积压缓冲区,作为写命令的备份。


除了存储最近的写命令,复制积压缓冲区中还存储了每个字节相应的复制偏移量,由于复制积压缓冲区固定大小先进先出的队列,所以它总是保存的是最近redis执行的命令。

所以,重连服务器后,从服务器会发送自己的复制偏移量offset给主服务器,

如果offset偏移量之后的数据仍然存在于复制挤压缓冲区,就执行部分重同步操作。

相反,执行完整重同步操作。

run_id(服务器运行的唯一ID) 

  每个redis实例在启动时候,都会随机生成一个长度为40的唯一字符串来标识当前运行的redis节点,查看此id可通过命令info server查看。

  当主从复制在初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来,当断线重连时,从节点会将这个runid发送给主节点。主节点根据runid判断能否进行部分复制:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会更具offset偏移量之后的数据判断是否执行部分复制,如果offset偏移量之后的数据仍然都在复制积压缓冲区里,则执行部分复制,否则执行全量复制;
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的redis节点并不是当前的主节点,只能进行全量复制;


psync流程:

复制

客户端向服务器端发送:SLAVEOF

1、设置主服务器的地址和端口

存到masterhost和mastterport两个属性里之后,向客户端发送ok,然后开始复制工作。

2、建立套接字链接

从服务器根据命令设置的地址和端口,创建链接,并且为这个套接字创建一个专门处理复制工作的文件事件处理器。

主服务器也会为套接字创建相应的客户端状态,并且把从服务器当作一个客户端来对待。

3、发送ping命令(检查)

检查套接字状态是否正常

检查主服务器是否能正确处理请求。(如果不能,就重连)

4、身份认证


5、发送端口信息

从服务器向主服务器发送信息,主服务器记录。

6、同步

从服务器向主服务器发送psync命令。(主服务器也成为从服务器的客户端,因为主服务器会发送写命令给从服务器)

7、命令传播

完成同步后,进入传播阶段,主服务器一直发送写命令,从服务器一直接受,保证和主服务器一致。

心跳检测

默认一秒一次,从服务器向主服务器发送命令:REPLCONF ACK <offset>

三个作用:

检测网络连接状态:如果主服务器一秒没收到命令,就说明出问题了

辅助实现min-slaves配置:min-slaves-to-write 3   min-slaves-max-log 10:当从服务器小于3个或延迟都大于10,主服务器拒绝写命令。

检测命令丢失:如果命令丢失,主服务器会发现偏移量不一样,然后它就会根据偏移量,去积压缓冲区找到缺少的数据并发给从服务器。

4.3、哨兵

4.3.1什么是哨兵机制

Redis的哨兵(sentinel) 系统用于管理/多个 Redis 服务器,该系统执行以下三个任务:

·        监控: 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。

·        提醒:当被监控的某个 Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。

·        自动故障迁移:当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master; 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master。

例如下图所示:

在Server1 掉线后:

升级Server2 为新的主服务器:

4.3.2、哨兵模式修改配置

实现步骤:

1.拷贝到etc目录

cp sentinel.conf  /usr/local/redis/etc

2.修改sentinel.conf配置文件

sentinel monitor mymast  192.168.110.133 6379 1  #主节点 名称 IP 端口号 选举次数

sentinel auth-pass mymaster 123456 

3. 修改心跳检测 5000毫秒

sentinel down-after-milliseconds mymaster 5000

4.sentinel parallel-syncs mymaster 2 --- 做多多少合格节点

5. 启动哨兵模式

./redis-server /usr/local/redis/etc/sentinel.conf --sentinel &


1)Sentinel(哨兵) 进程是用于监控 Redis 集群中 Master 主服务器工作的状态

2)在 Master 主服务器发生故障的时候,可以实现 Master 和 Slave 服务器的切换,保证系统的高可用(High Availability)

工作方式

1)每个 Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他 Sentinel(哨兵)进程发送一个 PING 命令。

2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线。

3. 如果一个 Master 主服务器被标记为主观下线,则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。

4. 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态, 则Master 主服务器会被标记为客观下线(ODOWN)。

5. 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master 主服务器、Slave 从服务器发送 INFO 命令。

6. 当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

7. 若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。

哨兵(sentinel) 的一些设计思路和zookeeper非常类似


我们从启动并初始化说起

4.3.3启动并初始化 Sentinel

启动一个 Sentinel 可以使用命令:

$ redis-sentinel /path/to/your/sentinel.conf

或者命令:

$ redis-server /path/to/your/sentinel.conf --sentinel

当一个 Sentinel 启动时, 它需要执行以下步骤:

初始化服务器。

首先, 因为 Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器, 所以启动 Sentinel 的第一步, 就是初始化一个普通的 Redis 服务器.

不过, 因为 Sentinel 执行的工作和普通 Redis 服务器执行的工作不同, 所以 Sentinel 的初始化过程和普通 Redis 服务器的初始化过程并不完全相同。

比如说, 普通服务器在初始化时会通过载入 RDB 文件或者 AOF 文件来还原数据库状态, 但是因为 Sentinel 并不使用数据库, 所以初始化 Sentinel 时就不会载入 RDB 文件或者 AOF 文件。

将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。

第二个步骤就是将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。

比如说, 普通 Redis 服务器使用 redis.h/REDIS_SERVERPORT 常量的值作为服务器端口:

#define REDIS_SERVERPORT 6379

而 Sentinel 则使用 sentinel.c/REDIS_SENTINEL_PORT 常量的值作为服务器端口:

#define REDIS_SENTINEL_PORT 26379

为什么在 Sentinel 模式下, Redis 服务器不能执行诸如 SET 、 DBSIZE 、 EVAL 等等这些命令 —— 因为服务器根本没有在命令表中载入这些命令。

初始化 Sentinel 状态。

在应用了 Sentinel 的专用代码之后, 接下来, 服务器会初始化一个 sentinel.c/sentinelState 结构(后面简称“Sentinel 状态”), 这个结构保存了服务器中所有和 Sentinel 功能有关的状态 (服务器的一般状态仍然由 redis.h/redisServer 结构保存):

struct sentinelState {

    // 当前纪元,用于实现故障转移
    uint64_t current_epoch;

    // 保存了所有被这个 sentinel 监视的主服务器
    // 字典的键是主服务器的名字
    // 字典的值则是一个指向 sentinelRedisInstance 结构的指针
    dict *masters;

    // 是否进入了 TILT 模式?
    int tilt;

    // 目前正在执行的脚本的数量
    int running_scripts;

    // 进入 TILT 模式的时间
    mstime_t tilt_start_time;

    // 最后一次执行时间处理器的时间
    mstime_t previous_time;

    // 一个 FIFO 队列,包含了所有需要执行的用户脚本
    list *scripts_queue;

} sentinel;

初始化 Sentinel 状态的 masters 属性

Sentinel 状态中的 masters 字典记录了所有被 Sentinel 监视的主服务器的相关信息:

  • 字典的键是被监视主服务器的名字。
  • 而字典的值则是被监视主服务器对应的 sentinel.c/sentinelRedisInstance 结构。

每个 sentinelRedisInstance 结构代表一个被 Sentinel 监视的 Redis 服务器实例(instance), 这个实例可以是主服务器、从服务器、或者另外一个 Sentinel 。

实例结构包含的属性非常多, 以下代码展示了一部分属性

typedef struct sentinelRedisInstance {

    // 标识值,记录了实例的类型,以及该实例的当前状态
    int flags;

    // 实例的名字
    // 主服务器的名字由用户在配置文件中设置
    // 从服务器以及 Sentinel 的名字由 Sentinel 自动设置
    // 格式为 ip:port ,例如 "127.0.0.1:26379"
    char *name;

    // 实例的运行 ID
    char *runid;

    // 配置纪元,用于实现故障转移
    uint64_t config_epoch;

    // 实例的地址
    sentinelAddr *addr;

    // SENTINEL down-after-milliseconds 选项设定的值
    // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
    mstime_t down_after_period;

    // SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
    // 判断这个实例为客观下线(objectively down)所需的支持投票数量
    int quorum;

    // SENTINEL parallel-syncs <master-name> <number> 选项的值
    // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs;

    // SENTINEL failover-timeout <master-name> <ms> 选项的值
    // 刷新故障迁移状态的最大时限
    mstime_t failover_timeout;

    // ...

} sentinelRedisInstance;

创建连向主服务器的网络连接。

 Sentinel 将成为主服务器的客户端, 它可以向主服务器发送命令, 并从命令回复中获取相关的信息。

对于每个被 Sentinel 监视的主服务器来说, Sentinel 会创建两个连向主服务器的异步网络连接:

  • 一个是命令连接, 这个连接专门用于向主服务器发送命令, 并接收命令回复。
  • 另一个是订阅连接, 这个连接专门用于订阅主服务器的 __sentinel__:hello 频道。
为什么有两个连接?

在 Redis 目前的发布与订阅功能中, 被发送的信息都不会保存在
 Redis 服务器里面, 如果在信息发送时, 想要接收信息的客户
端不在线或者断线, 那么这个客户端就会丢失这条信息。

因此, 为了不丢失 __sentinel__:hello 频道的任何信息, 
Sentinel 必须专门用一个订阅连接来接收该频道的信息。

而另一方面, 除了订阅频道之外, Sentinel 还又必须向主服务
器发送命令, 以此来与主服务器进行通讯, 所以 Sentinel 还
必须向主服务器创建命令连接。

并且因为 Sentinel 需要与多个实例创建多个网络连接, 所以
 Sentinel 使用的是异步连接。

接下来介绍 Sentinel 如何通过命令连接和订阅连接与被监视主服务器进行通讯。

4.3.4、获取服务器信息

sentinel默认每十秒钟发送一次INFO命令给主服务器,并获取信息:

1)关于主服务器本身的信息

2)主服务器属下所有从服务器信息

sentinel发现主服务器有新的从服务器时,会创建相应的实例结构和命令连接,订阅连接

4.3.5、给服务器发送消息

4.3.6、主观下线

指的是单个Sentinel实例对服务器做出的下线判断,即单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。
如果服务器在down-after-milliseconds给定的毫秒数之内, 没有返回 Sentinel 发送的 PING 命令的回复, 或者返回一个错误, 那么 Sentinel 将这个服务器标记为主观下线(SDOWN )。
sentinel会以每秒一次的频率向所有与其建立了命令连接的实例(master,从服务,其他sentinel)发ping命令,通过判断ping回复是有效回复,还是无效回复来判断实例时候在线(对该sentinel来说是“主观在线”)。
sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度,如果实例在down-after-milliseconds毫秒内,返回的都是无效回复,那么sentinel回认为该实例已(主观)下线,修改其flags状态为SRI_S_DOWN。如果多个sentinel监视一个服务,有可能存在多个sentinel的down-after-milliseconds配置不同,这个在实际生产中要注意。

4.3.7、客观下线


客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断,然后开启failover。
客观下线就是说只有在足够数量的 Sentinel 都将一个服务器标记为主观下线之后, 服务器才会被标记为客观下线(ODOWN)。
只有当master被认定为客观下线时,才会发生故障迁移。
当sentinel监视的某个服务主观下线后,sentinel会询问其它监视该服务的sentinel,看它们是否也认为该服务主观下线,接收到足够数量(这个值可以配置)的sentinel判断为主观下线,既任务该服务客观下线,并对其做故障转移操作。
sentinel通过发送 SENTINEL is-master-down-by-addr ip port current_epoch runid

(ip:主观下线的服务id,port:主观下线的服务端口,current_epoch:sentinel的纪元,runid:*表示检测服务下线状态,如果是sentinel 运行id,表示用来选举领头sentinel)

来询问其它sentinel是否同意服务下线。
一个sentinel接收另一个sentinel发来的is-master-down-by-addr后,提取参数,根据ip和端口,检测该服务时候在该sentinel主观下线,并且回复is-master-down-by-addr,回复包含三个参数:down_state(1表示已下线,0表示未下线),leader_runid(领头sentinal id),leader_epoch(领头sentinel纪元)。
sentinel接收到回复后,根据配置设置的下线最小数量,达到这个值,既认为该服务客观下线。
客观下线条件只适用于主服务器: 对于任何其他类型的 Redis 实例, Sentinel 在将它们判断为下线前不需要进行协商, 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。只要一个 Sentinel 发现某个主服务器进入了客观下线状态, 这个 Sentinel 就可能会被其他 Sentinel 推选出, 并对失效的主服务器执行自动故障迁移操作。

4.3.8、选举大哥sentinel

一个redis服务被判断为客观下线时,多个监视该服务的sentinel协商,选举一个领头sentinel,对该redis服务进行故障转移操作。选举领头sentinel遵循以下规则:
1)所有的sentinel都有公平被选举成领头的资格。
2)所有的sentinel都只有一次将某个sentinel选举成领头的机会(在一轮选举中),一旦选举,不能更改。
3)先到先得,一旦当前sentinel设置了领头sentinel,以后要求设置sentinel为领头请求都会被拒绝。
4)每个发现服务客观下线的sentinel,都会要求其他sentinel将自己设置成领头。
5)当一个sentinel(源sentinel)向另一个sentinel(目sentinel)发送is-master-down-by-addr ip port current_epoch runid命令的时候,runid参数不是*,而是sentinel运行id,就表示源sentinel要求目标sentinel选举其为领头。
6)源sentinel会检查目标sentinel对其要求设置成领头的回复,如果回复的leader_runid和leader_epoch为源sentinel,表示目标sentinel同意将源sentinel设置成领头。
7)如果某个sentinel被半数以上的sentinel设置成领头,那么该sentinel既为领头。
8)如果在限定时间内,没有选举出领头sentinel,暂定一段时间,再选举。

为什么要选?
简单来说,就是因为只能有一个sentinel节点去完成故障转移。
sentinel is-master-down-by-addr这个命令有两个作用,一是确认下线判定,二是进行领导者选举。
过程:
1)每个做主观下线的sentinel节点向其他sentinel节点发送上面那条命令,要求将它设置为领导者。
2)收到命令的sentinel节点如果还没有同意过其他的sentinel发送的命令(还未投过票),那么就会同意,否则拒绝。
3)如果该sentinel节点发现自己的票数已经过半且达到了quorum的值,就会成为领导者
4)如果这个过程出现多个sentinel成为领导者,则会等待一段时间重新选举。

4.3.9、转移

1)挑一个新的主服务器

2)把其它从服务器的主服务器改成新的

3)把之前的主服务器改为新主服务器的从服务器

4.3.10、怎么挑新的主服务器

1)删除所有下线服务器

2)删除五秒内没回复INOF命令的服务器

3)删除数据旧的服务器(连接断开超过down-after-millseconds*10)

4)根据优先级,选出最高的。

4.3.11、重点提炼


  • Sentinel 是一个特殊模式下的 Redis 服务器, 它使用了不同的命令表, 所以 Sentinel 能使用的命令和普通服务器不同。
  • Sentinel 会读入用户指定的配置文件, 为每个要被监视的主服务器创建相应的实例结构, 并创建连向主服务器的命令连接和订阅连接, 其中命令连接用于向主服务器发送命令请求, 而订阅连接则用于接收指定频道的消息。
  • Sentinel 向主服务器发送 INFO 命令获得属下从服务器信息, 为这些从服务器创建实例结构、命令连接和订阅连接。
  • 默认 Sentinel 十秒一次向被监视的主服务器和从服务器发送 INFO 命令, 当主服务器处于下线状态, 或者 Sentinel 正在对主服务器进行故障转移操作时, Sentinel 向从服务器发送 INFO 命令的频率会改为每秒一次。
  • 对于监视同一个主服务器和从服务器的多个 Sentinel 来说, 它们会以每两秒一次的频率, 通过向被监视服务器的 __sentinel__:hello 频道发送消息来向其他 Sentinel 宣告自己的存在。
  • 每个 Sentinel 也会从 __sentinel__:hello 频道中接收其他 Sentinel 发来的信息, 并根据这些信息为其他 Sentinel 创建相应的实例结构, 以及命令连接。
  • Sentinel 只会与主服务器和从服务器创建命令连接和订阅连接, Sentinel 与 Sentinel 之间则只创建命令连接。
  • Sentinel 以每秒一次的频率向实例(包括主服务器、从服务器、其他 Sentinel)发送 PING 命令, 并根据实例对 PING 命令的回复来判断实例是否在线
  • 当 Sentinel 将一个主服务器判断为主观下线时, 它会向同样监视这个主服务器的其他 Sentinel 进行询问, 看它们是否同意这个主服务器已经进入主观下线状态。
  • 当 Sentinel 收集到足够多的主观下线投票之后, 它会将主服务器判断为客观下线, 并发起一次针对主服务器的故障转移操作。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200