docker搭建RabbitMQ单机集群

0. 前言

实际生产应用中都会采用消息队列的集群方案,如果选择RabbitMQ那么有必要了解下它的集群方案原理

一般来说,如果只是为了学习RabbitMQ或者验证业务工程的正确性那么在本地环境或者测试环境上使用其单实例部署就可以了,但是出于MQ中间件本身的可靠性、并发性、吞吐量和消息堆积能力等问题的考虑,在生产环境上一般都会考虑使用RabbitMQ的集群方案。

1. 集群方案的原理

RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。

由上图可知,我们的集群需要一个HAProxy,HA是high available的意思,通过这个HAProxy去代理producer和consumer的请求。

2. 单机多实例部署

由于某些因素的限制,有时候你不得不在一台机器上去搭建一个rabbitmq集群,这个有点类似zookeeper的单机版。真实生成环境还是要配成多机集群的。有关怎么配置多机集群的可以参考其他的资料,这里主要论述如何在单机中配置多个rabbitmq实例。

2.1 回顾docker创建单个rabbitmq服务的过程

服务器上执行

ubuntu@VM-0-8-ubuntu:~$ docker exec -it 47b /bin/bash

进入rabbitmq容器,首先确保RabbitMQ运行没有问题

root@47b96c4e50ef:/# rabbitmqctl status

我们就将其定义为第一个结点

回顾一下这个结点创建的命令

docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 263c941f71ea

263c941f71ea是我rabbitmq(tag为3.8-management)镜像id,

2.2 docker搭建RabbitMQ集群

创建结点

docker run -d --hostname rabbit1 --name rabbit1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' rabbitmq:3.8-management
docker run -d --hostname rabbit2 --name rabbit2 -p 5673:5672 --link rabbit1:rabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' rabbitmq:3.8-management
docker run -d --hostname rabbit3 --name rabbit3 -p 5674:5672 --link rabbit1:rabbit1 --link rabbit2:rabbit2 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' rabbitmq:3.8-management

这里解释下 --hostname rabbit1 是指创建后集群内的第一个节点名为rabbit1, --name 是容器名 ,使用–link 进行容器之间互连,link不可或缺,使得三个容器能互相通信

Erlang Cookie

有些特殊的情况,比如已经运行了一段时间的几个单个物理机,我们在之前没有设置过相同的Erlang Cookie值,现在我们要把单个的物理机部署成集群,实现我们需要同步Erlang的Cookie值。

1.为什么要配置相同的erlang cookie?

因为RabbitMQ是用Erlang实现的,Erlang Cookie相当于不同节点之间相互通讯的秘钥,Erlang节点通过交换Erlang Cookie获得认证。

2.Erlang Cookie的位置

默认情况下,文件在 /var/lib/rabbitmq/.erlang.cookie

进入容器配置

设置结点1

docker exec -it rabbit1 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit

设置节点2,加入到集群:

docker exec -it rabbit2 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
exit

设置节点3,加入到集群:

docker exec -it rabbit3 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
exit

其中,参数“--ram”表示设置为内存节点,忽略次参数默认为磁盘节点。在输入命令的时候会有下列的提示,docker run 的时候加入RABBITMQ_ERLANG_COOKIE的方法未来会被移除,建议下面两种方法代替。

RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead.

配置好后,打开web控制台可以看到如下集群,我们启动了3个节点,1个磁盘节点和2个内存节点。

配置RABBITMQ_ERLANG_COOKIE,保持一致

输入docker ps查看rabbit1容器id,

运行docker logs bbaab774d75e命令查看rabbit1的logs找到home dir

我们知道这是容器1的目录,接下来把这个文件夹都拷贝出来后复制给其他容器:

物理机和容器之间复制命令如下:

  • 容器复制文件到物理机:docker cp 容器名称:容器目录 物理机目录
  • 物理机复制文件到容器:docker cp 物理机目录 容器名称:容器目录

首先建立主机目录,我这里建立了一个文件夹/home/ubuntu/copy_dir(我这里用户目录就是/home/ubuntu/)

对于rabbit1的容器ID,执行docker cp 容器ID:/var/lib/rabbitmq/. 主机目录

ubuntu@VM-0-8-ubuntu:~$ docker cp bbaab774d75e:/var/lib/rabbitmq/. /home/ubuntu/copy_dir
ubuntu@VM-0-8-ubuntu:~$ cd copy_dir/ && ls -a
.  ..  .bash_history  .erlang.cookie  mnesia

进入主机目录输入ls -a可以看到一个隐藏的文件.erlang.cookie,把它拷贝到其他容器中,即对于myrabbit2 , myrabbit3的容器,执行docker cp 主机目录/.erlang.cookie 容器ID:/var/lib/rabbitmq/

docker cp /home/ubuntu/copy_dir/.erlang.cookie 1a2929c26883:/var/lib/rabbitmq/
docker cp /home/ubuntu/copy_dir/.erlang.cookie edc76e910a0c:/var/lib/rabbitmq/

查看集群状态

进入rabbit1,查看集群状态

ubuntu@VM-0-8-ubuntu:~/copy_dir$ docker exec -it bba bash
root@rabbit1:/# rabbitmqctl cluster_status
RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead.
Cluster status of node rabbit@rabbit1 ...
Basics

Cluster name: rabbit@rabbit1

Disk Nodes

rabbit@rabbit1

RAM Nodes

rabbit@rabbit2
rabbit@rabbit3

Running Nodes

rabbit@rabbit1
rabbit@rabbit2
rabbit@rabbit3

Versions

rabbit@rabbit1: RabbitMQ 3.8.9 on Erlang 23.1.4
rabbit@rabbit2:   on Erlang
rabbit@rabbit3:   on Erlang
... ...
... ...

到此为止,我们已经完成了RabbitMQ集群的建立,启动了3个节点,1个磁盘节点和2个内存节点。

2.3 集群管理

rabbitmqctl join_cluster {cluster_node} [–ram]
将节点加入指定集群中。在这个命令执行前需要停止RabbitMQ应用并重置节点。

rabbitmqctl cluster_status
显示集群的状态。

rabbitmqctl change_cluster_node_type {disc|ram}
修改集群节点的类型。在这个命令执行前需要停止RabbitMQ应用。

rabbitmqctl forget_cluster_node [–offline]
将节点从集群中删除,允许离线执行。

rabbitmqctl update_cluster_nodes {clusternode}

在集群中的节点应用启动前咨询clusternode节点的最新信息,并更新相应的集群信息。这个和join_cluster不同,它不加入集群。考虑这样一种情况,节点A和节点B都在集群中,当节点A离线了,节点C又和节点B组成了一个集群,然后节点B又离开了集群,当A醒来的时候,它会尝试联系节点B,但是这样会失败,因为节点B已经不在集群中了。

rabbitmqctl cancel_sync_queue [-p vhost] {queue}
取消队列queue同步镜像的操作。

rabbitmqctl set_cluster_name {name}
设置集群名称。集群名称在客户端连接时会通报给客户端。Federation和Shovel插件也会有用到集群名称的地方。集群名称默认是集群中第一个节点的名称,通过这个命令可以重新设置。

设置节点类型

如果你想更换节点类型可以通过命令修改,如下:

rabbitmqctl stop_app

rabbitmqctl change_cluster_node_type dist

rabbitmqctl change_cluster_node_type ram

rabbitmqctl start_app

移除节点

如果想要把节点从集群中移除,可使用如下命令实现:

rabbitmqctl stop_app

rabbitmqctl restart

rabbitmqctl start_app

集群重启顺序

集群重启的顺序是固定的,并且是相反的。如下所述:

  • 启动顺序:磁盘节点 => 内存节点
  • 关闭顺序:内存节点 => 磁盘节点

最后关闭必须是磁盘节点,不然可能回造成集群启动失败、数据丢失等异常情况

2.4 镜像队列完成HA

镜像队列是Rabbit2.6.0版本带来的一个新功能,允许内建双活冗余选项,与普通队列不同,镜像节点在集群中的其他节点拥有从队列拷贝,一旦主节点不可用,最老的从队列将被选举为新的主队列。

镜像队列的工作原理:在某种程度上你可以将镜像队列视为,拥有一个隐藏的fanout交换器,它指示者信道将消息分发到从队列上。

上面已经完成RabbitMQ默认集群模式,但并不保证队列的高可用性,尽管交换机、绑定这些可以复制到集群里的任何一个节点,但是队列内容不会复制。虽然该模式解决一项目组节点压力,但队列节点宕机直接导致该队列无法应用,只能等待重启,所以要想在队列节点宕机或故障也能正常应用,就要复制队列内容到集群里的每个节点,必须要创建镜像队列。

镜像队列是基于普通的集群模式的,然后再添加一些策略,所以你还是得先配置普通集群,然后才能设置镜像队列,我们就以上面的集群接着做。

先进入rabbit1,新增一个用户名和密码,guest可能会出现问题

# 先添加个用户
rabbitmqctl add_user 新用户名 新密码
# 然后可以选择把原来那个删了
# rabbitmqctl delete_user 原来的用户名,即guest
# 查看用户组,发现新增用户没有权限,TAG=[]
rabbitmqctl  list_users
# 赋予root用户所有权限
rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
# 赋予root用户administrator角色
rabbitmqctl set_user_tags root administrator
# 再次查看用户组
rabbitmqctl  list_users
# 显示
Listing users ...
user    tags
guest   [administrator]
root    [administrator]

设置的镜像队列可以通过开启的网页的管理端Admin->Policies,也可以通过命令。

rabbitmqctl set_policy my_ha "^" '{"ha-mode":"all"}'

通过web控制台配置镜像队列

  • Name: 策略名称
  • Pattern: 匹配的规则,如果是匹配所有的队列,是^.
  • Definition: 使用ha-mode模式中的all,也就是同步所有匹配的队列,ha-sync-mode 表示设置节点间队列数据同步,如果不设置会出现不同步。问号链接帮助文档。

当你完成了这一步,那么恭喜你完成了高可用,如果一个或者两个节点掉了,第三个节点也能完成数据接收,在前两个节点恢复后,第三个节点也能迅速地同步数据到前两个节点。

ha-sync-mode : 如果 此节点 不进行设置 ,在其中一台 服务器 宕机 再 启动 后 会报 Unsynchronised Mirrors XXXX 错误。

这时候 在 队列详细信息 页面 需要 手动 点击 同步队列 或者 用命令行 执行 命令 rabbitmqctl sync_queue name

如果看到这样的队列,我们已经完成了 RabbitMQ 集群镜像队列的高可用性配置。集群中任意一台宕机都会自动切换到另一台宕机机器开启时会自动同步镜像队列,使其保持一致。

其中+2的意思是有三个节点,一个节点本身和两个镜像节点。

通过命令行控制镜像队列

rabbitmqctl set_policy my_ha "^" '{"ha-mode":"all","ha-sync-mode":"automatic"}'

可以看出设置镜像队列,一共有三个参数,每个参数用空格分割

  1. 参数一:名称,可以随便填;
  2. 参数二:队列名称的匹配规则,使用正则表达式表示;
  3. 参数三:为镜像队列的主体规则,是json字符串,分为三个属性:ha-mode | ha-params | ha-sync-mode,分别的解释如下:
  • ha-mode:镜像模式,分类:all/exactly/nodes,all存储在所有节点;exactly存储x个节点,节点的个数由ha-params指定;nodes指定存储的节点上名称,通过ha-params指定;

  • ha-params:作为参数,为ha-mode的补充;

  • ha-sync-mode:镜像消息同步方式:automatic(自动),manually(手动);

查看镜像队列

rabbitmqctl list_policies

删除镜像队列

rabbitmqctl clear_policy

3. 负载均衡-HAProxy

搭建完镜像队列,其实还有点问题,我们节点1挂了,控制台就打不开了,当然,可能是因为节点一开始run的时候没有设置

HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。

3.1 设置rabbitmqnet

ubuntu@VM-0-8-ubuntu:~/haproxy$ docker network create rabbitmqnet
ubuntu@VM-0-8-ubuntu:~/haproxy$ docker network connect rabbitmqnet rabbit1
ubuntu@VM-0-8-ubuntu:~/haproxy$ docker network connect rabbitmqnet rabbit2
ubuntu@VM-0-8-ubuntu:~/haproxy$ docker network connect rabbitmqnet rabbit3

3.2 查看该网络情况

查看rabbitmqnet的网络情况,可以看到我的3个rabbit加入这个网络是127.21.0.0/16

ubuntu@VM-0-8-ubuntu:~/haproxy$ docker network inspect rabbitmqnet
[
    {
        "Name": "rabbitmqnet",
        "Id": "21071d91f6b5150072e55628d90ca78542529cc4a48cd0cb08d44687cec9b99d",
        "Created": "2020-12-09T23:59:38.92208807+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "1a2929c26883bfccb1794d556201bdd7c3462b1629615d802121587d7a7cc390": {
                "Name": "rabbit2",
                "EndpointID": "f7bff6d9b49187ec7ec7500bcbd725eb39c743f032886d7e97baf2fb82b48177",
                "MacAddress": "02:42:ac:15:00:03",
                "IPv4Address": "172.21.0.3/16",
                "IPv6Address": ""
            },
            "6f99f26d8d8a928d7faea94b682c19ebd72af523b7f9a229e6ce12acd5efcc4a": {
                "Name": "haproxy",
                "EndpointID": "dfc610fba529a2b30218a27eaa02f56de569f0c21eb952445c3bc51d215b0b8a",
                "MacAddress": "02:42:ac:15:00:05",
                "IPv4Address": "172.21.0.5/16",
                "IPv6Address": ""
            },
            "bbaab774d75e34bd479982cc5791a5a7e7447b02b86cf7b7388b41c356f0c364": {
                "Name": "rabbit1",
                "EndpointID": "70f9ada99b683722ec3889ae5920e2ba3c66a99d64551aaa69a17856353b5e67",
                "MacAddress": "02:42:ac:15:00:02",
                "IPv4Address": "172.21.0.2/16",
                "IPv6Address": ""
            },
            "edc76e910a0c55eaf3ed2587420bade7c79da13a7f45e098214ad4dcdf53a718": {
                "Name": "rabbit3",
                "EndpointID": "e63966a7f727db36aa2b5ea7c831c1cf9d853a55d1662cefdc8a0e24d9b9bbd5",
                "MacAddress": "02:42:ac:15:00:04",
                "IPv4Address": "172.21.0.4/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

3.3 配置haproxy.cfg

在/home/ubuntu/haproxy/下vi haproxy.cfg,配置haproxy,在失败了很多次之后,终于找到了一个解决办法,就是直接绑定这个网络的私有ip,参考了https://blog.csdn.net/fqydhk/article/details/80430503这篇文章

global
       maxconn 10000                   #默认最大连接数
       log 127.0.0.1 local0            #[err warning info debug]
       chroot /usr/local/sbin            #chroot运行的路径
       daemon                          #以后台形式运行haproxy
       pidfile /var/run/haproxy.pid    #haproxy的pid存放路径,启动进程的用户必须有权限访问此文件
defaults
       log 127.0.0.1 local3
       mode http                       #所处理的类别 (#7层 http;4层tcp  )
       maxconn 10000                   #最大连接数
       option dontlognull              #不记录健康检查的日志信息
       option redispatch               #serverId对应的服务器挂掉后,强制定向到其他健康的服务器
       #stats refresh 30                #统计页面刷新间隔
       retries 3                       #3次连接失败就认为服务不可用,也可以通过后面设置
       balance roundrobin              #默认的负载均衡的方式,轮询方式
      #balance source                  #默认的负载均衡的方式,类似nginx的ip_hash
      #balance leastconn               #默认的负载均衡的方式,最小连接
       timeout connect 5000                 #连接超时
       timeout client 50000                #客户端超时
       timeout server 50000                #服务器超时
       timeout check 2000              #心跳检测超时
####################################################################
listen http_front
        bind 0.0.0.0:5669           #监听端口  
        stats refresh 30s           #统计页面自动刷新时间  
        stats uri /haproxy?stats            #统计页面url  
        stats realm Haproxy Manager #统计页面密码框上提示文本  
        stats auth admin:admin      #统计页面用户名和密码设置  
        #stats hide-version         #隐藏统计页面上HAProxy的版本信息
#####################我把RabbitMQ的管理界面也放在HAProxy后面了###############################
listen rabbitmq_admin 
    bind 0.0.0.0:5671
    server rabbitmq3 172.21.0.4:15674
    server rabbitmq2 172.21.0.3:15673
    server rabbitmq1 172.21.0.2:15672
####################################################################
listen rabbitmq_cluster 
    bind 0.0.0.0:5670
    option tcplog
    mode tcp
    timeout client  3h
    timeout server  3h
    option          clitcpka
    balance roundrobin      #负载均衡算法(#banlance roundrobin 轮询,balance source 保存session值,支持static-rr,leastconn,first,uri等参数)
    #balance url_param userid
    #balance url_param session_id check_post 64
    #balance hdr(User-Agent)
    #balance hdr(host)
    #balance hdr(Host) use_domain_only
    #balance rdp-cookie
    #balance leastconn
    #balance source //ip
    server   rabbitmq3 172.21.0.4:5674 check inter 5s rise 2 fall 3   #check inter 2000 是检测心跳频率,rise 2是2次正确认为服务器可用,fall 3是3次失败认为服务器不可用
    server   rabbitmq2 172.21.0.3:5673 check inter 5s rise 2 fall 3
    server   rabbitmq1 172.21.0.2:5672 check inter 5s rise 2 fall 3

3.4 创建haproxy容器

创建容器,绑定外部和内部的5669-5671端口

docker run -d -p 5669-5671:5669-5671 \
-v /home/ubuntu/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg \
--name=haproxy \
--net=rabbitmqnet \
haproxy

3.5 查看haproxy控制台

输入{主机ip}:5669/haproxy?stats,可以看到haproxy的控制台,大功告成!

3.6查看rabbitmq的web控制台

{主机ip}:5671,登陆,是熟悉的界面

3.7 代码测试

代码中访问mq集群地址,则变为访问haproxy地址:5670

#配置RabbitMQ的基本信息 ip 端口 username password
spring:
  rabbitmq:
    host: asjunor.site
    port: 5670
    username: xxx
    password: xxx
    virtual-host: /example

复用原来的代码,修改applicationn.yml,将端口改为5670,发一条消息来测试能否使用集群。

如果出现以下错误:

An unexpected connection driver error occured (Exception message: Socket closed)

有可能是你的用户权限问题,在web控制端修改权限

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容