Linux-Memcache分布式部署方案(magent代理解决单点故障)

Memcached的特点

Memcached作为高速运行的分布式缓存服务器具有以下特点。

  1. 协议简单:memcached的服务器客户端通信并不使用复杂的MXL等格式, 而是使用简单的基于文本的协议。

  2. 基于libevent的事件处理:libevent是个程序库,他将Linux 的epoll、BSD 类操作系统的kqueue等时间处理功能封装成统一的接口。memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能。

  3. 内置内存存储方式:为了提高性能,memcached中保存的数据都存储在 memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached,重启操作系统会导致全部数据消失。另外,内容容量达到指定的值之后  memcached  回自动删除不适用的缓存。

  4.Memcached不互通信的分布式:memcached  尽管是  “  分布式  ”  缓存服务器,但服务器端并没有分布式功能。各个  memcached  不会互相通信以共享信 息。他的分布式主要是通过客户端实现的。

Memcache内存管理

最近的memcached默认情况下采用了名为Slab Allocatoion的机制分配,管理内存。在改机制出现以前,内存的分配是通过对所有记录简单地进行malloc和free来进行的。但是这中方式会导致内存碎片,加重操作系统内存管理器的负担。

Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,已完全解决内存碎片问题。Slab Allocation  的原理相当简单。将分配的内存分割成各种尺寸的块(chucnk),并把尺寸相同的块分成组(chucnk的集合)如图:

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》
而且slab allocator 还有重复使用已分配内存的目的。也就是说,分配到的内存不会释放,而是重复利用。 
Slab Allocation 的主要术语

  Page :分配给Slab 的内存空间,默认是1MB。分配给Slab 之后根据slab 的大小切分成chunk.

  Chunk : 用于缓存记录的内存空间。

  Slab Class:特定大小的chunk 的组。 
在Slab 中缓存记录的原理  Memcached根据收到的数据的大小,选择最合适数据大小的Slab memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

 

Memcache删除数据

Memcached删除数据时数据不会真正从memcached中消失。Memcached不会释放已分配的内存。记录超时后,客户端就无法再看见该记录(invisible 透明),其存储空间即可重复使用。

memcached内部不会监视记录是否过期,而是在get时查看记录的时间戳,检查记录是否过期。这种技术称为Lazy Expriation。因此memcached不会再过期监视上耗费CPU时间。

LRU:从缓存中有效删除数据的原理

  Memcached会优先使用已超时的记录空间,但即使如此,也会发生追加新纪录时空间不足的情况。此时就要使用名为Least Recently Used (LRU)机制来分配空间。这就是删除最少使用的记录的机制。因此当memcached的内存空间不足时(无法从slab class)获取到新空间时,就从最近未使用的记录中搜索,并将空间分配给新的记录。

Memcached存在的问题

本身没有内置分布式功能,无法实现使用多台Memcache服务器来存储不同的数据,最大程度的使用相同的资源;无法同步数据,容易造成单点故障。(memagent代理实现集群)

在 Memcached中可以保存的item数据量是没有限制的,只要内存足够 。
Memcached单进程最大使用内存为2G,要使用更多内存,可以分多个端口开启多个Memcached进程
最大30天的数据过期时间,设置为永久的也会在这个时间过期,常量REALTIME_MAXDELTA 60*60*24*30控制
最大键长为250字节,大于该长度无法存储,常量KEY_MAX_LENGTH 250控制
单个item最大数据是1MB,超过1MB数据不予存储,常量POWER_BLOCK 1048576进行控制,
它是默认的slab大小
最大同时连接数是200,通过 conn_init()中的freetotal进行控制,最大软连接数是1024,通过settings.maxconns=1024 进行控制
跟空间占用相关的参数:settings.factor=1.25, settings.chunk_size=48, 影响slab的数据占用和步进方式

memcached是一种无阻塞的socket通信方式服务,基于libevent库,由于无阻塞通信,对内存读写速度非常之快。
memcached分服务器端和客户端,可以配置多个服务器端和客户端,应用于分布式的服务非常广泛。
memcached作为小规模的数据分布式平台是十分有效果的。

memcached是键值一一对应,key默认最大不能超过128个字节,value默认大小是1M,也就是一个slabs,如果要存2M的值(连 续的),不能用两个slabs,因为两个slabs不是连续的,无法在内存中存储,故需要修改slabs的大小,多个key和value进行存储时,即使 这个slabs没有利用完,那么也不会存放别的数据。

Memcache的分布式介绍

memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端仅包括内存存储功能,其实现非常简单。至于memcached的分布式,则是完全由客户端程序库实现的。这种分布式是memcached的最大特点。

如何理解Memcached的分布式

这里多次使用了“分布式”这个词,但并未做详细解释。 现在开始简单地介绍一下其原理,各个客户端的实现基本相同。
下面假设memcached服务器有node1~node3三台, 应用程序要保存键名为“tokyo”“kanagawa”“chiba”“saitama”“gunma” 的数据。

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

准备

首先向memcached中添加“tokyo”。将“tokyo”传给客户端程序库后,客户端实现的算法就会根据“键”来决定保存数据的memcached服务器。服务器选定后,即命令它保存“tokyo”及其值。

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

添加时

同样,“kanagawa”“chiba”“saitama”“gunma”都是先选择服务器再保存。

接下来获取保存的数据。获取时也要将要获取的键“tokyo”传递给函数库。 函数库通过与数据保存时相同的算法,根据“键”选择服务器。 使用的算法相同,就能选中与保存时相同的服务器,然后发送get命令。 只要数据没有因为某些原因被删除,就能获得保存的值。

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

获取时

这样,将不同的键保存到不同的服务器上,就实现了memcached的分布式。 memcached服务器增多后,键就会分散,即使一台memcached服务器发生故障
无法连接,也不会影响其他的缓存,系统依然能继续运行。

普通余数分散法

  余数计算的方法简单,数据的分散性也相当优秀, 但也有其缺点。那就是当添加或移除服务器时,  缓存重组的代价相当巨大。

  添加服务器后,余数就会产生巨变,这样就无法获取与保存时相同的服务器,从而影响缓存的命中率。

集群配置

由于Memcached服务器与服务器之间没有任何通讯,并且不进行任何数据复制备份,所以当任何服务器节点出现故障时,会出现单点故障,如果需要实现HA,则需要通过另外的方式来解决。

通过Magent缓存代理,防止单点现象,缓存代理也可以做备份,通过客户端连接到缓存代理服务器,缓存代理服务器连接缓存服务器,缓存代理服务器 可以连接多台Memcached机器可以将每台Memcached机器进行数据同步。如果其中一台缓存服务器down机,系统依然可以继续工作,如果其中 一台Memcached机器down掉,数据不会丢失并且可以保证数据的完整性。具体可以参考:http://code.google.com/p /memagent/

Memcache集群的实现

memcached尽管是“分布式”缓存服务器,但服务器端并没有分布式功能。各个memcached不会互相通信以共享信息。那么,怎样进行分布式呢?这完全取决于客户端的实现。

magent是一个memcached代理软件(memcached agent),又叫memagent,其项目网址为:
https://github.com/wangmh/memagent,防止单点现象,缓存代理也可以做备份,通过客户端连接到缓存代理服务器,缓存代理服务器连接缓存服务器。
它提供的功能及特点有:
1、和每个memcache server保持多个长连接,效果是减少memcache server保持的连接数量及创建销毁连接的开销。不过,memcache本身就支持大并发连接,这个功能也就没什么特别的说道。
2、支持memcache的binary协议命令,实现请求的转发。
3、和memcache一样,基于libevent的事件驱动来处理IO。
4、支持ketama 的一致性hash算法。
5、支持memcache backup集群,当memcache集群有机器挂了,memagent会将get请求转向memcache backup集群。这个功能对于cache的稳定性要求高的场景下会有用武之地。

就提供的功能而言,memagent是个很简单的东西。对于较大的memcache集群,可以考虑搭一套memagent作为proxy使用。

magent的hash算法

  magent采用的是:Consistent Hashing原理,Consistent Hashing如下所示:首先求出memcached服务器(节点)的哈希值, 并将其配置到0~232的圆(continuum)上。 然后用同样的方法求出存储数据的键的哈希值,并映射到圆上。 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。 如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

  从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化 而影响缓存的命中率,但Consistent Hashing中,只有在continuum上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

 

Linux下的Memcache集群环境搭建

环境:CentOS release 6.5

搭建memcached集群环境,先要安装gcc

yum -y install gcc

如果出现如下错误:

Error: Package: glibc-headers-2.12-1.132.el6.x86_64 (base)
    Requires: kernel-headers >= 2.2.1
Error: Package: glibc-headers-2.12-1.132.el6.x86_64 (base)
    Requires: kernel-headers
You could try using --skip-broken to work around the problem
You could try running: rpm -Va --nofiles --nodigest

解决方法:修改文件

vi /etc/yum.conf 

exclude=kernel* 前加注释即可解决,此参数的意思是排除安装或更新kernel开头的软件,而我们安装gcc需要依赖kernel相关的软件glibc-headers-2.12-1.80.el6_3.5.x86_64

在root目录下创建soft目录

cd /root/
mkdir soft

安装libevent

检查是否安装了libevent软件

rpm -qa|grep libevent

编译安装libevent

cd /root/soft/
wget http://github.com/downloads/libevent/libevent/libevent-2.0.21-stable.tar.gz
tar -zxvf libevent-2.0.21-stable.tar.gz
cd libevent-2.0.21-stable
./configure --prefix=/usr
make && make install
cd ../
 
#测试是否安装成功:
ls -al /usr/lib | grep libevent

yum安装libevent

也可以用yum直接安装libevent

yum install libevent libevent-devel

编译安装Memcached

cd /root/soft/
wget http://memcached.googlecode.com/files/memcached-1.4.15.tar.gz
tar -zxvf memcached-1.4.15.tar.gz
cd memcached-1.4.15/
./configure --with-libevent=/usr
make && make install
cd ../
 
#测试是否安装成功:
ls -al /usr/local/bin/mem*
 
#或启动一个memcached进程
memcached -m 1 -u root -d -l 192.168.1.151 -p 11211
 
#查看是否启动成功了
ps aux|grep memcached
 
#显示两行 root,则说明安装成功了,如下所示:
root     11952  0.0  0.0 331112  1104         Ssl  15:09   0:00 memcached -m 1 -u root -d -l 192.168.1.151 -p 11211
root     11959  0.0  0.0 103240   796 pts/1    S+   15:09   0:00 grep memcached

编译安装magent

编译安装magent-0.6到/usr/local/下(推荐安装magent-0.5稳定版本,因为测试发现magent-0.6虽是最新版 本,但是还存在问题,不稳定,第二次访问magent始终会堵塞在那里,只能set一个值。测试了magent-0.5是稳定版本,没有出现只能set一 个值的现象,遂推荐安装magent-0.5版本,不过此问题也可以通过修改源码的方式来解决,解决方案参考(重点参考第8 楼):https://code.google.com/p/memagent/issues/detail?id=4#makechanges )

cd /usr/local
mkdir magent
cd magent/
wget http://memagent.googlecode.com/files/magent-0.6.tar.gz
解压的文件中ketama.* 是一致性hash的实现
tar -zxvf magent-0.6.tar.gz
 
vi ketama.h
在开头加入
#ifndef SSIZE_MAX
#endif
 
############magent-0.6版本修改内容start############
vi Makefile (magent-0.6版本)
 
改为
############magent-0.6版本修改内容end##############
 
############magent-0.5版本修改内容start############
vi Makefile (magent-0.5版本)
 
CFLAGS = -Wall -O2 -g
改为
CFLAGS = -lrt -Wall -O2 -g
 
sed -i "s#LIBS = -levent#LIBS = -levent -lm#g" Makefile
############magent-0.5版本修改内容end##############
 
/sbin/ldconfig
 
make
cp magent /usr/bin/magent
cd ../
 
测试magent是否安装成功:
 
1.启动一个magent进程
 
 
2.查看是否启动成功了
 
ps aux|grep magent

显示两行 root,则说明安装成功了,如下所示:

root 11720 0.0 0.0 10972 588 Ss 13:51 0:00 magent -u root -n 51200 -l 192.168.1.151 -p 12000 -s 192.168.1.151:11211 -s 192.168.1.151:11212 -b 192.168.1.151:11213
root 11974 0.0 0.0 103240 792 pts/1 R+ 15:12 0:00 grep magent[/code]

注: 之前的安装过程中,我总是只显示一行:root 11974 0.0 0.0 103240 792 pts/1 R+ 15:12 0:00 grep magent,但是执行magent命令又不报错,可是查看magent进程,老是没有,老是启动不起来,其实还是magent没有安装成功,所以这边必 须要看到两行,才能说明启动成功。

安装可能会出错,可以到该地址看看:http://ruancl.blog.51cto.com/2699729/850951
其它参考地址:http://blog.csdn.net/laosubitu/article/details/22198613

magent工作方式

1.get操作,先到普通mem上读取;如果失败,再到备份mem上读取(即服务器失效会导致两次读,不过只是特殊情况下的处理,可以接受)
2.如果一次get多个key的值,会逐个执行1中的步骤
3.delete,incr,decr,add,set,replace,prepend,append,cas,同时操作{普通mem,备份mem}

* 写操作:先操作备份men,再操作普通mem
* 不关联两者之间操作的成功/失败

4.client从magent上断开,不影响magent保持与集群server的链接
5.建议:client和magent部署在一起,client通过127.0.0.1访问magent时,效率会更高
————-

magent命令参数

-h this message
-u uid
-g gid
-p port, default is 11211. (0 to disable tcp support)
-s ip:port, set memcached server ip and port
-b ip:port, set backup memcached server ip and port
-l ip, local bind ip address, default is 0.0.0.0
-n number, set max connections, default is 4096
-D don’t go to background
-k use ketama key allocation algorithm
-f file, unix socket path to listen on. default is off
-i number, max keep alive connections for one memcached server, default is 20
-v verbose

准备测试工具

接着就是进行测试学习了,首先确保telnet服务安装了

查看telnet客户端是否安装:

rpm -q telnet

若无安装,则执行:

yum -y install telnet

查看telnet服务端是否安装:

rpm -q telnet-server

若无安装,则执行:

yum -y install telnet-server

测试流程

参照:http://blog.s135.com/post/393/ 其中使用的是magent-0.5

我尝试了0.5和最新的0.6,但里面提到的重启后的” 由于这两台Memcached重启后无数据,因此magent取得的将是空值,尽管11213端口的Memcached还有数据(此问题尚待改进)“,仍然存在

PHP+Memcache实现分布式

我们PHP的PECL中的Memcache扩展能够有效的解决Memcache的分布式问题,主要的接口就是 addServer() 函数,具体关于addServer()函数的实现可以参考该扩展源代码。那么现在就存在第二个问题,就是说无法同步数据,可以理解为MySQL中 Master/Slave的机制,就是说如果我们有多台的Memcache服务器,使用addServer函数的话,每个服务器存储的数据都是唯一的,也 就是说每个memcached服务器上存储的数据不是统一的,而是各自保存了不通的数据。

配置使用memcache存储session数据

session.save_handler = memcache
session.save_path ="tcp://127.0.0.1:11211"

注意:如果使用的是php的memcached扩展(注意不是memchache扩展)。

按照网上的资料配置php.ini

session.save_handler=memcached
session.save_path="tcp://127.0.0.1:11211"

会发现报错

Warning: Unknown: Failed to write session data (memcached). Please verify that the current setting of session.save_path is correct (tcp://127.0.0.1:11211) in Unknown on line 0

解决方案是把tcp://去掉。经测试,用memcache(不带d)扩展,去掉 tcp://后也能正常连接。测试中还发现用memcached扩展时,通过session_id()做为key获取缓存内容始终是空值,但用 memcache扩展却是正常,一旦通过memcache扩展设置session后,再通过memcached获取session_id()做为key获 取缓存内容就能正常输出,由些我猜测memcached设置不了session_id(此猜测在后面的测试中发现并非如此,而是memcached生成的 key为session_id的,自动在前面加了’memc.sess.key.’前缀),但获取值却是正常的。不知道是我配置过程中出错还是什么原因, 目前尚未解决这个问题。

其它发现:
1、memcache + magent 不支持session,但支持其它变量的存取
2、memcached + magent 支持session,但是不能通过key为session_id()获取session_id内容,要加上’memc.sess.key.’前缀才可以获取session_id的内容
3、memcache和memcached 不加magent都支持session,但是memcache可通过key为session_id()获取session_id内容,而 memcached则不能,memcached的key要加上’memc.sess.key.’前缀才可以获取session_id的内容,由此可知 memcache和memcached保存key为session_id()的方式不一样。

参考地址:
http://www.oschina.net/question/191760_88446
http://cubstag.blog.163.com/blog/static/139826788201072351259638/

session.save_handler=memcached
session.save_path="127.0.0.1:11211"

tcp://开头的是memcache扩展的写法,memchached扩展不需要。

参考地址:http://leo108.com/pid-1900.asp

或者某个目录下的 .htaccess :

php_value session.save_handler"memcache"
php_value session.save_path "tcp://127.0.0.1:11211"

再或者在某个一个应用中:

ini_set("session.save_handler","memcache");
ini_set("session.save_path","tcp://127.0.0.1:11211");

使用多个 memcached server 时用逗号”,”隔开,并且和Memcache::addServer() 文档中说明的一样,可以带额外的参数”persistent”、”weight”、”timeout”、”retry_interval”等等,类似这样 的:”tcp://host1:port1?persistent=1&weight=2,tcp://host2:port2″。

Memcache存取session测试

session_start();
if (!isset($_SESSION['TEST'])) {
    $_SESSION['TEST'] = time();
}
 
$_SESSION['TEST3'] = time();
 
echo $_SESSION['TEST'];
echo "\n";
echo $_SESSION['TEST3'];
echo "\n";
echo session_id();

从memcache中取回session数据

$memcache = memcache_connect('localhost',11211); var_dump($memcache->get('19216821213c65cedec65b0883238c278eeb573e077'));

会有看到

string(37)"TEST|i:1177556731;TEST3|i:1177556881;"
这样的输出,证明 session 正常工作
用 memcache 来存储 session 在读写速度上会比 files 时快很多,而且在多个服务器需要共用 session 时会比较方便,将这些服务器都配置成使用同一组 memcached 服务器就可以,减少了额外的工作量。缺点是 session 数据都保存在 memory 中,持久化方面有所欠缺,但对 session 数据来说也不是很大的问题。

另外,WS Memcached Session Handler for PHP 提供一种用session_set_save_handler 来利用 memcached 的方法。

memcached 与 magent组合搭建方案示例

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

magent与memcached 是可以混搭的,不必死板的一个magent s-memcached s-memcached b-memcached

上图此模型已经能够很好的解决一个节点,一组服务器的缓存数据服务,但是如果在北方网通架设了一组服务器,同时在南方电信又架设了另外一组服务器,那么这两组相对独立的节点之间如何做到数据的同步与共享,基于magent与memcached的解决方案如下:

《Linux-Memcache分布式部署方案(magent代理解决单点故障)》

需要注意的是,两组magent的配置最好完全一致,比如:

北方的magent配置为:magent s-memcached1 s-memcached2 b-memcached3

那么南方的magent配置也为:magent s-memcached1 s-memcached2 b-memcached3

其顺序都是一致的,因为magent在分配key到memcached上时只是简单的使用散列余数算法。(这是网上部分文章的说法,但根据相关资 料,我觉得此处的算法应该是Consistent Hashing算法(http://blog.csdn.net/sparkliang/article/details/5279393),因此我认为 跟配置顺序没有关系)

当然如果你够懒,那么你可以直接连接备份magent,因为所有的数据上面都有。

有个特别要注意的地方是:

1.其中一台Memcached死掉,从magent取数据,数据会从备份的Memcached取出,保证用户不受影响.

2.Memcached重启复活,由于这两台Memcached重启后无数据,因此magent取得的将是空值,尽管备份Memcached还有数据。可采用定时维护服务器,恢复memcached。

3.如果Memcached死掉,备份机同时死掉,那么只能说明你够倒霉,此时此刻你或许能见到上帝。

缓存与DB的同步

比较保险的做法是:查询的时候从缓存中取,add、updae、delete的时候同时操作缓存与DB。

当然你也可以定时同步缓存与DB的数据,个人认为不同的业务应该有不同的选择!

我在实际的应用中是同时使用这两种方式,比如用户个人信息之类的内容,就用定时同步的方式

缓存集群的形成历程

缓存集群不是天生就有的,只有应用场景的发展才会引发对集群的需要。

STEP1:无缓存—>STEP2:单机memcached—>STEP3:memcached集群+请求端分片—>STEP4:memcached集群+单台代理—>STEP5:memcached集群+代理集群—>再以后,就没有以后了

1~3阶段都是很轻松的(很多业务能到3阶段已经是规模不小,值得庆幸了);
4阶段后期就会遇到各种压力,要考虑memcached单台失效对后端(很可能是DB或其他持久化存储)突发压力的影响;magent的备份服务器是方法 之一,但具体的备份方案需要琢磨,简单的2倍存储代价不小;5阶段要考虑集群+集群的失效,需要引入DNS/名字服务等更低层的网络措施;

经验技巧

最后提出两个使用Memcache的小技巧,第一个是如果是一个有效分布式存储的数据,key的取名是很有学问的,这个可以按照项目需要去做,但是 key值不要太长,不会冲突就行。第二个就是每个保存在memcache中的数据不要超过1MB。第三个就是开启一个Memcache进程设置内存不要太 多也不要太少,按照自己合适设置就行,尽量最大程度提高对硬件的使用,同样可以采取在一台服务器上开启多个memcached来分担一个 memcached并发链接的压力。更多技巧在实际使用长慢慢去总结发现,会发现其实memcahe虽然简单,但是很好用。

magent能够消除单点故障问题,但是不能实现负载均衡。如果要实现负载均衡的话,可能还要再研究一下其他工具。

参考资料

Memcached的代理服务器软件:magent使用小记:http://blog.s135.com/post/393/
一致性hash算法 – consistent hashing:http://blog.csdn.net/sparkliang/article/details/5279393
memcached全面剖析–4. memcached的分布式算法:http://blog.charlee.li/memcached-004/
memcache proxy之memagent介绍分析:http://www.kafka0102.com/2010/01/9.html
Memcached代理软件magent安装小结:http://www.elesos.com/index.php?title=Memcached %E4%BB%A3%E7%90%86%E8%BD%AF%E4%BB%B6magent%E5%AE%89%E8%A3%85%E5%B0%8F%E7%BB%93
memcached 和它的代理:http://my.oschina.net/kakablue/blog/187270
M​e​m​c​a​c​h​e​d​内​存​分​析​、​调​优​、​集​群:http://wenku.baidu.com/view/23821cebe009581b6bd9ebc2.html
Linux下安装搭建Memcached集群环境:http://tim-fly.iteye.com/blog/1756936
Memcached 集群架构方面的问题:http://kb.cnblogs.com/page/69074/
Memagent 负载均衡配置.pptx:http://www.open-open.com/doc/view/97ce1f7d85b7425c9c5bcc09a6bb0b39

 

转自:http://www.3mu.me/linux%E4%B8%8Bmemcache%E5%88%86%E5%B8%83%E5%BC%8F%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88%E4%B8%8E%E4%BD%BF%E7%94%A8magent%E4%BB%A3%E7%90%86%E5%AE%89%E8%A3%85%E6%90%AD%E5%BB%BAmemcached%E9%9B%86%E7%BE%A4/

    原文作者:memcached
    原文地址: https://www.cnblogs.com/JohnABC/p/5752994.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞