Redis on AEP

AEP 算是白干了,留个痕。

AEP测试目标

AEP在降低成本同时,是否能满足Redis SLA:
如果关键指标(最近两小时99分位响应耗时最高值)的日常水位在2ms以内,超过10ms且持续1min以上,认为服务不可用,计入不可用时长。
如果关键指标(最近两小时99分位响应耗时最高值)的日常水位在2ms到5ms以内,超过25ms且持续1min以上,认为服务不可用,计入不可用时长。
如果关键指标(最近两小时99分位响应耗时最高值)的日常水位在5ms到20ms以内,超过40ms且持续1min以上,认为服务不可用,计入不可用时长。
如果关键指标(最近两小时99分位响应耗时最高值)的日常水位在20ms以上, 超过1倍且持续1min以上,认为服务不可用,计入不可用时长。

AEP简介

持久内存(Persistent Memory)是Intel 推出的新型非易失OPTANE DC设备,代号Apache Pass,简称AEP。一般来说内存速度快、性能强,但成本高、容量小、断电会失去数据。SSD等一些块设备成本低,容量大,具备非易失性,但性能相对较差。虽然近些年来出现了以rocksdb/leveldb为代表的LSM-Tree结构的存储系统从一定程度上解决了块设备低效的写问题,但同时又引入了一个比较严重的写放大问题。SSD与DRAM的访问延迟差异导致在分界线处形成了一条巨大的鸿沟,从软件层面难以解决,持久化内存的出现正好给填补上了。业界将AEP持久内存定义为DRAM内存和Storage硬盘之间的缓冲,对存储层次模型进行重新划分和定义:


image.png

对比其他存储介质,AEP在读写速率上比NAND SSD快50-100倍。而对比DRAM内存,AEP在容量上比传统DRAM(16/32G)高出8-10倍,且具备非易失特性的优势。持久化内存模糊了传统层次结构中内存与外存的边界,它既具有DRAM一样的数据访问方式,也具有SSD一样的持久化特性。所以可以把它当成内存或者持久化的存储使用。

AEP支持三种不同模式:

Memory Mode:内存模式,DRAM做缓存。与传统DRAM类似,数据易失。
App Direct Mode:应用直接访问模式,作用字节可寻址易失性内存或者持久化块设备。
Mixed Mode:混合模式,即部分AEP用作内存模式,部分用作应用直接访问模式。


image.png

目前Redis是基于Memory Mode方式使用AEP,下面将着重介绍一下Memory Mode方式。

当AEP工作在Memory Mode下,操作系统将AEP识别为DRAM使用,而原来的DRAM将作为缓存。此时应用只能操作AEP内存,DRAM作为透明缓存,应用在读数据时首先在DRAM查找Cache,若命中缓存触发Cache Hit,此时从DRAM直接返回数据;若无缓存命中则触发Cache Miss,再从AEP中读取数据。应用在写数据时使用Write-Back写回策略,只有数据在DRAM中触发更新、标志位发生变化时,才会写入AEP内存中。.

image.png

AEP测试

测试大纲

硬件测试目标:不同AEP+DREAM配置对Redis性能的影响。

Redis压测目标:AEP对Redis性能影响。

集群压测目标:AEP对实际部署集群的性能影响。


image.png

硬件测试

三台AEP机器配置


image.png

cpu内存6通道


image.png

内存带宽
内存最大带宽=内存时钟频率×内存宽位数×通道数×Socket数。

8260 CPU config frequency为2933MHZ,DRAM为2933MHZ,AEP频率2666MHZ,内存时钟频率由这三者min决定,所以AEP和内存一起会降频。

内存最大带宽,硬件测试一般在110GB/S-130GB/S。实际Redis测试仅能跑到20GB/S左右,远低于硬件测试值,我们认为AEP不影响Redis服务使用内存带宽。

AEP机器硬件测试
从内存带宽测试结果来看,10.90.90.43性能最好;Redis性能表现(无论大小字节数)与上面硬件测试一致。

对比测试结果,得出以下结论:对Redis性能影响最大的是DRAM,AEP影响相对有限。


image.png

image.png

image.png

image.png

Redis压测

机器配置


截屏2023-06-30 下午2.53.45.png

AD模式(非搭载AEP)时,内存为251G;MM(搭载AEP)模式时,内存为991G。
测试结论

单个redis-benchmark并发50客户端,无论部署单实例还是多实例:

  当与Redis交互的字节数较小时,没有性能差;当交互超过一定字节数(具体见下文)时,性能才会有下降;随着字节数越大(比如上万或者几万字节),性能下降会超过10%以上。

单key单member命令,当member约大于3000字节时,AEP性能下降超过5%。
单key多member命令,随着size(member)*num(member)约大于5000字节时,AEP性能下降超过5%。
返回数据量大的命令,当返回字节约大于5000字节时,AEP性能下降超过5%。

负载压测

测试将对比AD与MM模式时Redis性能,在AD模式下,Redis只访问DRAM;在MM模式下,Redis只访问AEP,DRAM作为m缓存。同时为了避免cpu影响(忙或者闲),会采用单、多实例(将整台cpu core压到掉底)对比测试。

单实例压测

分别启动一个redis-benchmark绑定在node 0上,一个redis-server绑定在node 1上,进行压测。测试string时redis-benchmark并发客户端为250,其他类型客户端为50。

string

从set命令的测试结果可以看出,并发客户端在250和50时候,字节数目大于3000之后,才出现性能差(超过5%),同时出现时延差。

注:横坐标均为字节数


image.png

image.png

其他数据类型

分别测试lpush、sadd、zadd、hmset测试结果显示,差不多写入数据超过5000字节后,性能才会出现差距。

下图中所有AD等同于DRAM(内存),MM等同于(AEP MM)。

image.png

image.png

image.png

image.png
多实例压测

分别启动48/150个redis-benchmark绑定在node 0上,48/150个redis-server绑定在node 1上,进行一对一压测。测试redis-benchmark并发客户端为50。

测试结果显示,set sadd输入或者zset range返回超过5000字节后,性能会出现差距。


image.png

image.png

内存增量测试

在MM模式下压测。分别启动48个redis-benchmark绑定在node 0上,48个redis-server绑定在node 1上,进行一对一压测。测试redis-benchmark并发客户端为50。随着内存增大,找到压测性能的拐点。当Redis使用内存到达DREAM最大内存256G时,必然会发生内存miss。

测试结论

当交互的字节数较小时,即使内存使用量超过DREAM最大内存,Redis性能并未降低(未超过5%)。


image.png

image.png

集群(Kedis)压测

在三台AEP机器上部署Redis集群,redis-benchmark通过vip对整个集群进行压测。单个redis-benchmark并发客户端100。

测试将对比AD与MM模式下的Redis集群性能,同时部署更多的实例,验证上述结论是否会因为集群增大,发生变化。

测试结论

由于proxy、vip、网卡等因素的影响,随着交互的字节数增大,QPS 会因此骤降,AEP影响反倒没有那么显示出来。

在5k字节以内,几乎没有影响;超过 5k字节,QPS下降过多(大概率受限于网卡),失去测试意义。

15proxy与24主24从
在整体负载不高的情况下,测试显示字节数较大时,AEP MM模式下产生slowlog较多(多次测试显示slowlog受网络波动影响大于AEP);但在P99和QPS上没有差距。


image.png

image.png

image.png

60proxy与96主96从
负载较高时,测试显示AEP对Redis服务没有影响。


image.png

image.png

image.png

AEP测试结论
小key场景的Redis业务基本上是不会出现性能问题的;大部分大key(均大于5k字节)场景的Redis业务也不会出现性能问题。

测试显示AEP能满足Redis SLA,可以满足大部分Redis业务场景。

当然,测试场景的模拟数据和线上实际数据还是有一定差距;对于复杂场景的核心业务,保险起见,我们还是需要引流来做测试。

Redis引入AEP(MM)问题

AEP定位为大容量内存,为了利用好这个大容量。 有以下两种方式:

  1. 单机部署Redis实例密度增大。
  2. Redis单实例规格增大,现在规格为6G。

单机部署Redis实例密度增大。

Redis单实例规格增大,现在规格为6G。
Redis服务现状:


image.png

解决方式:

  1. 服务分级,比如将核心业务资源保障与非核心业务分隔,合理分配CPU资源。方便低优先级服务灰度AEP。
  2. 优化软(Docker 、agent、内核)硬件,支撑高密度实例(长期)。
  3. 优化Redis服务,解决大规格Redis带来的毛刺问题(长期)。

成本核算:
一台内存128G的f1802的价格是29115元,AEP价格是每GB最多2.4刀(约16.56RMB),即512G需要8479元。可以看到,按照前面估计的混部能多一倍的计算,也就是原来需要29115*2=58230,现在需要29115+8479=37594, 即能节约35%的成本。

业界使用

不稳定使用:

陌陌是1U的服务器。1U服务器成本虽低,但在散热和供电方面表现较差。而插入AEP后比以前功耗大,导致Redis服务器不稳定,经常重启。

稳定使用:

快手Redis 服务:AEP全面运用于快手的Redis服务中,帮助Redis服务的TCO降低了30%。

阿里云:基于AEP AD模式推出Tair内存持久版,提供接近内存延时能力的同时保持持久化的能力;直接售卖AEP Memory Mode模式的Redis服务。

携程:选择规格32C CPU+4个128G的傲腾AEP,内存与傲腾AEP配比是1:4,单GB成本相对于纯内存节约50%。

头条:大规模引入AEP,目前主要是运用在Redis从库场景中;TerarkKV AEP+SSD存储数据,对低于512B与DRAM性能一致,单机千万覆盖所有场景。

腾讯:基于AD模式极速云盘,在DRAM和SSD中间加入AEP做存储缓冲。

百度:AEP+SPDK新一代用户态单机存储引擎,数据库:AEP+NVME;SPARK温存储:AEP+HDD,AEP做不同存储介质缓冲。

AEP介绍补充

App Direct Mode-AEP作为PMEM块设备使用
目前Linux Kernel中主要把PMEM看成一个类似于磁盘的块设备,所以可以在PMEM设备上创建文件系统,使它看起来和一般的磁盘没什么区别,可以使用传统VFS的读/写指令。但是设备的具体物理属性完全不一样,比如读写的latency,PMEM可以达到和DRAM接近的程度,磁盘当然是望尘莫及的。所以,这就带来一个问题,众所周知,一般在Linux上常见的文件系统,比如ext4、xfs等,都是给磁盘设计的,都用到了page cache来缓存磁盘上的数据来提高性能。但是,对于PMEM设备来说,它的访问延迟已经和内存接近了,为什么还需要内存中的page cache呢?所以,目前Linux Kernel中对这一块最大的改进就是支持DAX(Direct Access,linux4.0引入)。一句话解释DAX,就是DAX bypass了page cache。无论读写都是直接操作PMEM上的数据。DAX需要在文件系统层面支持,如果要使用DAX,那么需要在mount文件系统时传入“-o dax”参数,比如:

mount -o dax /dev/pmem0 /mnt/pmem0


image1.png

linux DAX两种使用方式
直接访问(Direct Access,DAX) 机制是一种支持用户态软件直接访问存储于持久内存(Persistent Memory,PM) 的文件的机制,用户态软件无需先将文件数据拷贝到页高速缓存(Page Cache)。上述描述对应到下面这张图(Typical NVDIMM Software Architecture2)中,就是说路径1(Memory)和路径2(File)这两条 IO 路径都绕过页高速缓存。

其中 File 路径(下称普通文件路径)表示,用户态软件通过标准文件接口(Pmem-Aware File System)访问持久内存文件系统。
其中 Memory 路径(下称映射文件路径)表示,用户态软件通过映射文件(Memory-mapped File)直接访问 PM。


image2.png

上图中的第三、四条路径(Storage over App Direct),分别对外提供block接口和标准的文件接口,应用无需额外的适配工作,这和使用SSD并没有什么区别,仅仅是延迟更小。但是这种使用方式软件栈上的开销所占的比例相当大,并不能发挥出很好发挥出AEP的极致性能,基本不用考虑这种用法。

DAX极大地提高了文件系统在PMEM设备上的性能,但是还有一些问题没有解决,比如:文件系统的metadata还是需要使用page cache或buffer cache、持久化问题、数据一致性等问题。
Linux DAX问题
持久化问题
PMem 是 DIMM 接口,可以直接通过 CPU 指令访问数据。读取 PMem 的时候,和读取一个普通的内存地址没有区别,CPU Cache 会做数据缓存,所有关于内存相关的知识,例如 Cache Line 优化,Memory Order 等等在这里都是适用的。而写入就更复杂了,除了要理解内存相关的特性以外,还需要理解一个重要的概念:Power-Fail Protected Domains。这是因为,尽管 PMem 设备本身是非易失的,但是由于有 CPU Cache 和 Memory Controller 的存在,以及 PMem 设备本身也有缓存,所以当设备掉电时,Data in-flight 还是会丢失。针对掉电保护,Intel 提出了 Asynchronous DRAM Refresh(ADR)的概念,负责在掉电时把 Data in-flight 回写到 PMem 上,保证数据持久性。目前 ADR 只能保护 iMC 里的 Write Pending Queue(WPQ)和 PMem 的缓存中的数据,但无法保护 CPU Cache 中的数据。在 Intel 下一代的产品中,将推出 Enhanced ADR(eADR),可以进一步做到对 CPU Cache 中数据的保护。

由于 ADR 概念的存在,所以简单的 MOV 指令并不能保证数据持久化,因为指令结束时,数据很可能还停留在 CPU Cache 中。为了保证数据持久化,我们必须调用 CLFLUSH (CLFLUSHOPT/CLWB/CLWB)指令,保证 CPU Cache Line 里的数据写回到 PMem 中。 Intel 最近出了一本书 “Programming Persistent Memory”,专门介绍如何在 PMem 上进行编程,一共有 400 多页。


image3.jpeg

一致性问题
x86保证最多64位的对齐加载和存储是原子的 ,大于8字节的数据对象可能写到一半出现机器掉电,重启后的数据是否完整无法得知。所以应用需要通过flag/redo log/undo log等方式判断数据是否完整,以及不完整时该怎么去处理,当然这势必会引入比较重的开销。比如将Redis的索引结构(hash table)及数据都写入到持久化内存中,那么当用户写入一条数据时,内部可能发生hash table扩容,hash entry搬迁等多个动作,要维护数据的一致性问题,那就也需要引入类似mini transaction的机制。所以把AEP当作持久化内存与易失性内存来使用时性能肯定是一定有差异的。

一旦考虑把AEP当作持久化的内存来使用时,所写下的每一行代码都考虑怎么处理数据一致性的问题,这并不是一件容易的事情。封装的库能帮我们解决大部分问题,下面介绍一下PMDK。

解决方式 PMDK
为了简化持久化内存在AppDirect下的使用,Intel开发了PMDK(Persistent Memory Development Kit)。可以直接在PMDK的基础上去开发自己的应用,但是这些通用的库也并不一定适合所有场景。


image4.png

目前pmdk一般是基于mmap,在高吞吐下比file方式要快。一旦映射建立后,用户虚拟地址空间通过MMU就直接映射到AEP的物理空间中,应用无需陷入内核态即可高效地以字节寻址的方式访问AEP。

上图所示,pmdk封装了一些通用功能:

  1. libpmem: 用来将数据持久化到介质上,以及提供一些优化的内存操作(memcpy, memset等)函数。

  2. libpmemlog: 基于这个库可以用来写顺序追加的log等。

  3. ibpmemobj: 这个库实现了一套事务机制,用来保证数据的一致性问题。

要注意的是mmap是以shared的方式建立映射,如果是以private的方式映射,更新数据时并不会写到介质上,而是写到进程私有的空间中(page cache中),然而这里AppDirect模式下是没有page cache的。这里引出了一个问题:如果进程在AEP中分配了一块内存,然后fork一个子进程,那么子进程也是能够看到父进程对这块内存的更新,因为两个父子进程的更新都会立即反映到同一块物理内存上。所以对于分配在AEP上的内存就没办法利用fork的copy on write机制来获取一致性的内存状态。(Redis正是利用fork的copy on write机制获取一对致性内存状态做备份操作)。

App Direct Mode-AEP作为PMEM NUMA Node使用
同样在App Direct Mode下,AEP能够像内存一样字节可寻址,同时在Linux Kernel 5.1中,引入了将AEP作为可插拔内存设备使用(KMEM DAX)的Patch,即可设定为具备单独内存的NUMA Node(无CPU)。此时应用能同时操作DRAM和AEP两种内存:


image5.png

image6.png

在默认情况下,应用将先分配DRAM NUMA Node内存再分配AEP NUMA Node内存,这导致在DRAM内存耗尽时性能将急剧恶化。这需要开发者将应用程序的内存分配方式进行修改(例如基于libmemkind接口),在编码时区分数据冷热,将频繁调用的数据放到DRAM内存,冷数据放到AEP内存,从而实现性能调优。

/* 分配DRAM内存空间 */
#define malloc(size) memkind_malloc(MEMKIND_DEFAULT,size)
void *zmalloc_dram(size_t size) {
void *ptr = malloc(size+PREFIX_SIZE);
}
/* 分配AEP内存空间 */
static void *zmalloc_pmem(size_t size) {
    void *ptr = memkind_malloc(MEMKIND_DAX_KMEM, size+PREFIX_SIZE);
}
/* 设定分配阈值 */
void *zmalloc(size_t size) {
    return (size < pmem_threshold) ? zmalloc_dram(size) : zmalloc_pmem(size);
}
/* 释放内存空间 */
#define free_dram(ptr) memkind_free(MEMKIND_DEFAULT,ptr)
#define free_pmem(ptr) memkind_free(MEMKIND_DAX_KMEM,ptr)

上述是针对Redis进行冷热内存分层的示例,社区已提供了memKeyDB作为Redis支持AEP的开源版本,以pmem_threshold作为阈值(默认大小为UINT_MAX,2^32-1),将较大的内存申请分配在AEP上,而相对小的内存申请分配在DRAM上。

Mixed Mode
可以配置Intel Optane DC PMM使部分容量分配给内存模式,其余部分分配到App Direct模式。当部分或全部持久存储器模块容量设置为Memory Mode时,DRAM容量对应用程序是隐藏的,并成为最后一级缓存。

以下命令将60%的可用持久性内存容量分配给内存模式。 其余部分配置为App Direct模式的混合模式。

ipmctl create -goal MemoryMode=60

后续展望

AEP使用方式总结

image.png

后续可关注的技术方向

Redis基于ad模式的二次开发

MemKeyDB - Redis with Persistent Memory(numa node方式,基于pmem方式未开源)

https://pmem.io/2020/09/25/memkeydb.html

Rocksdb基于ad模式的二次开发

How Intel Optimized RocksDB Code for Persistent Memory with PMDK

RocksDB is known for its reliability and speed. Due to its architecture, however, the performance of reads aren’t as good as that of writes. The Block-based Table solution suffers from write and space amplification. Write amplification is the amount of data written to storage compared to the amount of data the application wrote. Space amplification is the space required by a data structure. Fragmentation increases space amplification, which requires temporary copies of the data. With large writes to the Block-based Table, compaction causes unpredictable and unexpected latency. The characteristics of a Plain Table make it very suitable for low-latency storage media. The team at Intel identified two areas for optimization within RocksDB: 1.Separate keys and values to optimize write and space amplification. 2.Use PMem and PMDK to improve the overall performance of the Plain Table.

https://software.intel.com/content/www/us/en/develop/articles/how-intel-optimized-rocksdb-code-for-persistent-memory-with-pmdk.html

linux内核持续支持

系统可以比较好的工作在PMEM上,但是还是会有很多不适合PMEM的地方,比如metadata还要经过page cache等。所以,NVDIMM专用文件系统就应用而生了。

NOVA Filesystem就是专门为PMEM设计的文件系统,可以参考NOVA的github link(https://github.com/NVSL/linux-nova) 。

ZUFS(https://github.com/NetApp/zufs-zuf/blob/zuf-upstream/Documentation/filesystems/zufs.txt)是来自于NetApp的一个项目,ZUFS的意思是Zero-copy User Filesystem。声称是实现了完全的zero-copy, 甚至文件系统的metadata都是zero-copy的。ZUFS主要是为了PMEM设计,但是也可以支持传统的磁盘设备,相当于是FUSE的zero-copy版本,是对FUSE的性能的提升。

为了解决DRAM Cache冲突的问题,Dan Williams提出了一组patch,mm: Randomize free memory。这组patch的想法很简单,就是通过randomize free area的方式来降低Cache>冲突。

https://www.nersc.gov/research-and-development/knl-cache-mode-performance-coe/

Linux 4.0开始支持DAX,Linux5.1支持KMEM DAX。

AEP二代三代发展

在 Intel 第二代的产品BPS,对多线程访问PMEM优化,同时性能得到了20%的提升,也解决了AEP内存降频问题。

在 Intel 第三代的产品CPS,将推出 Enhanced ADR(eADR),可以进一步做到对 CPU Cache 中数据的保护,解决持久化问题。同时性能相比BPS也得到很大提升。

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

推荐阅读更多精彩内容