换个角度看Aurora:缘何“万能”?对比TiDB有何不同?
作者介绍
房晓乐(葱头巴巴),PingCAP 资深解决方案架构师,前美团数据库专家、美团云 CDS 架构师、前搜狗、百度资深 DBA,擅长研究各种数据库架构,NewSQL 布道者。
2017年,Amazon 在 SIGMOD 上发表了论文《Amazon Aurora: Design Considerations for High Throughput CloudNative Relational Databases》,本文按照该论文的主线并结合作者的理解,对Aurora进行解读。这里面涉及到很多知识点,笔者会在每章的末尾标注关键字,如果需要更好的理解,需要读者对关键字进行搜索理解。
前言
首先,Aurora 是依托于 AWS 云高度融合 MySQL 定位于 OLTP 的企业级关系数据库,Aurora 的设计者认为,当前海量数据处理的主要瓶颈已经从计算、存储 IO 转移到了网络 IO (1),为了解决这个问题,Aurora 通过将重做日志推到多租户的大规模存储服务当中(Aurora 的设计哲学是 log is database)(2),从而不仅大幅度的减少了网络 IO,而且可以数据的多副本进行快速故障恢复,以及容错损失,建立起一套自愈存储(Aurora 将恢复子系统委托给底层可靠的存储系统,依赖这个来保障系统服务层级(ServiceLevel Agreement, SLA))。
当前数据库在 M-S 的基础上,产生有很多优化变种,比如 Sharding、扩容,这样单个节点的存储 IO 可以有效进行控制,但由于节点间的不经济的复制、交互导致网络 IO 扩大。
下文有专门阐述章节。
背景介绍
现在 IT 逐渐在公有云迁移,其中大多需要 OLTP 数据库系统,能够提供一个等同或者更优的数据库是加速这种转型的关键。在现在分布式云服务中,通过对计算与存储的解耦,以及对存储的多节点复制,增加系统的弹性及扩展性,这样我们也能够进行删除问题节点、增加副本、写节点切换等。但在这种环境下,传统数据库系统所面临的 I/O 瓶颈发生变化。由于 I/O 可以在多个节点和多个磁盘传播,单个磁盘和节点瓶颈不在。瓶颈转移到数据库与多节点存储直接的网络交互,尤其在对存储进行并行写的时候,网络带宽的带宽或者流量(PPS)可能导致响应时间变慢。
同时传统(包括分布式)数据库需要复杂的脏数据刷机机制,事务提交也需要很多交互,事务提交普遍采用 2-phase commit 机制(2PC),这些在云环境下都需要进行改变(优化)(Aurora 的设计哲学是 log is database,对数据的更改只写日志,也即 write-once,大大简化了传统数据库的写入机制)。
Aurora 使用了一种新的体系结构(见下图)它将 logging 和存储从数据库引擎中剥离到分布式的云存储环境中,虽然实例还包括大部分的传统内核组件(查询处理器、事务、锁、缓存、访问机制和回滚管理),但其中的几个功能(重做日志、数据持久化、闪崩恢复、备份/还原)则被下传到了存储服务层。
这样 Aurora 的体系结构比传统方法有三个显著的优势。
通过将存储构建为跨多个数据中心的独立容错和自愈的服务,使存储层中数据不受性能差异和单节点瞬态或永久故障的影响。
只写重做日志记录存储,我们可以减少一个数量级的网络 IOPS 。一旦消除了这个瓶颈,就可以积极地优化许多其它争点,在 MySQL 基础代码基础上获得显著的吞吐量改进。
Aurora 用存储层持续异步复制操作,替代数据库引擎中最复杂、最关键、最昂贵的备份和重做恢复功能(backup and redo recovery),这是不需进行保存检查点(checkpoint)、不对数据库进程处理进行干扰的低成本的备份恢复机制。
但同时同样引入三个问题:
如何在云存储上进行数据持久性,以及如何对节点故障进行选举仲裁。
如何使用这个自动同步存储系统,并发挥其最大作用。
如何消除分布式存储中的多阶段同步、崩溃恢复和检查点。
在解决上面三个问题前,我们先回顾下传统关系数据库体系结构,DB 在处理事务的过程通常被视为一种分层的行为。系统在顶层对 SQL 语句进行解析,然后将得到的语法树传递给查询优化器层。查询优化器通过统计信息进行最优路径选择(CBO),这个阶段产生的物理执行计划与逻辑存储层交互,完成相应的操作。
在本文中,将事务处理引擎简化为两层模型:SQL 解析、SQL 执行以及优化视为查询处理引擎(Process Engine,PE);逻辑层存储和物理层存储统称存储引擎(Storage Engine,SE)。这对应 MySQL 可插拔存储的两层架构。
定义数据库服务器集群的架构决策的关键点在于集群共享、复制发生的阶段,协调动作发生在什么层以及哪个层。这不仅确定了系统在可扩展性和灵活性上的权衡,而且关系到每一种架构在现成的数据库服务器上的适用性。以下是具有代表性的架构:
我们对这四种主要的架构进行下整理汇总(除此之外还有 shared nothing 纯分布式架构):
Aurora 的架构可谓独树一帜,它的体系架构类似 SDP,但是它将更新限制在一个 DB 实例上,避免了分布式并发控制协议。Aurora 设计者们认为,传统的数据库实现可扩展不管是做 Sharding、还是分布式、或者共享存储(Oracle RAC),本质上都是在数据库的不同层面耦合(SNA 在应用层连接层,SDP 是在进程和缓存层),扩展后的每个实例的程序栈仍然是原来的多层结构。Aurora 认为从成本、部署灵活性及可用性等因素考虑,应该考虑把数据库的各层打开,然后在每个层单独做扩展。传统的数据库系统,例如 MySQL、PostgreSQL 以及Oracle,将所有的功能模块封装成一个整体,而 Aurora则是将数据库的缓冲区管理、恢复系统(recovery)从这个整体剥离出来,单独定制扩展。
Aurora 复制及相关失败处理:
磁盘、网络、机柜、机房等都有一定的故障率,并且日常维护重启、升级等操作会增加这类临时不可用时间。一般采用多副本选举来解决该问题:例如 3 个副本的情况下,写必须要有 2 个以上(Majority)副本写成功才可以,在读的情况下也必须保证有 2 个:V-Read + V-Write> V-Copy。Aruora 设计中使用了 3 AZ(可用区),每个 AZ 下 2 Copy 的设置,共计 6 个副本。AWS 传统的服务,例如 S3、Kinesis 等都是 3 副本设计。这样的设置主要从实际观察情况出发:
在同一个 AZ 下下硬盘、节点损坏、维修等是常态。在同一个 AZ 下设置 2 Copy 可以尽可能减少这类操作对用可用性影响。
机房级 AZ 问题不常发生,例如水灾、火灾、地震等故障。
在写入场景下 6 Copy 需要保证 4 个副本完成,在读场景下只要 7-4=3 个副本一致就可以了。
Aurora 下存储最小单位叫 Segment,每个 Segment 大小为 10GB,Protection Group(PG)是一个逻辑单位,代表的是 6 个Segment,Segment 是维护与调度的最小单元,一个Segment 故障时,同 AZ 的万兆网络可以在 10 秒内就完成修复(这也是选择 10G 的原因),存储节点 Segement 这种设计对于日常运维比较重要,例如在热点问题上(Heat Management),我们可以在一个热磁盘或节点上标记一个片段是坏的,冲裁系统(quorum)可以通过快速迁移到另一个较冷的节点,操作系统和安全修补程序甚至存储机群升级都是该节点的一个简短的不可用事件,一次执行一次,确保各 PG 的成员不同时被修补。实现了在存储服务中使用敏捷和快速部署。
传统数据库中的在写入页面对象(如堆表、B-TREE 等)之前,都需要先写入重做日志,(wirite-ahead log WAL机制)。并且每个重做日志记录还包含修改前、修改后的数据。除此之外,其它数据也需要写入,在下面基于多数据中心的热备架构(active-standy)(构建在 EBS 上)(下图)中,一个简单的写入被放大很多倍(自己算吧)。
Redo Log
Binlog(临时存储最后 N个)
Data(最新数据库结构数据,内存中定时 Dump)
FRM Files(这个数据量一般比较小)
Double-Write ( MySQL 数据刷新的一个优化,将 dirty page 拷贝到 double write buffer 内存区域,便于脏数据刷新过程中失败恢复。)
为了解决这个写入放大的问题,Aurora 提出的思路是,唯一的写入只包括通过网络写入 Redo log(不在有后台的写入、checkpoint、脏数据),直接将 Redo log 下推到存储节点,每个存储节点根据 Redo log 来构成本地(Local Segment)存储状态,多个存储可靠性通过副本数来保障。每个存储节点也可以将Segment 定时同步到 S3 上增加可靠性。
下图显示了一个主多从的集群。主库只将日志记录写入存储服务,并将这些日志记录发送到从实例。IO 批量流入一个共同的目的地(逻辑段,即一个 PG)来完全记录日志记录,并将每个批次传给所有 6 个副本继续持久化,数据库引擎从从 4 处(4/6)等待确认,从库使用重做日志将更改应用到其缓冲区缓存。
闪崩处理,在传统数据库中,闪崩后数据库需要从最近的一次 checkpoint 开始应用所有重做日志进行恢复,在 Aurora 中,持久化重做日志会多节点连续的、异步同步,任何一个读请求都有可触发重做日志对旧的页进行应用(重做日志随时应用),所以正常情况下,数据启动后基本上不用做什么恢复操作。
在 Aurora 的设计思路中,存储服务的核心设计原则是最小化前台写请求的延迟(快速响应),将大多数存储处理移到后台,同时他也有机制去避免后台存储高峰导致前台延迟,例如,当存储节忙于处理前台写请求时,不需要运行旧版本数据页的收集(GC),除非磁盘接近容量,在 Aurora 后台处理与前台处理是负相关的(传统数据库往往是正相关),后台页面和检查点的后台写入与系统上的前台负载正相关,如果在存储系统上建立了一个积压,将关闭前台活动,以防止长时间的队列堆积(这个思想,类似单实例的刷新机制,会自动选择空闲情况下进行脏数据持久化)。由于在系统中的各个存储节点上放置了高熵值,所以在一个存储节点上的节流很容易被 4/6 次仲裁写入处理,以慢节点的形式出现。(有点类似传统的 M-S 架构中,从库延迟后,摘掉读流量)(这里面隐含另外一个问题,如果是整体负载导致的存储积压,关闭一个节点可能导致雪崩,在论文里没有发现相关的解读,可能 Aurora 的设计者认为这种情况概率很低,或者认为存储处理能力应用远大于计算节点。)
让我们更详细地看下存储节点上的各步骤。如上图所示,它包括以下步骤:
接收日志记录并添加到内存中的队列
记录在磁盘上并进行 ack 返回
对于批量 Redo log 进行整理,确定没有批次丢失
与其它副本存储节点进行对齐(通过 Gossip 协议,它们可以知道集群中有哪些节点,以及这些节点的状态,每一条 Gossip 消息上都有一个版本号,节点可以对接收到的消息进行版本比对,确保二者得到的信息相同)
日志记录合并到新的数据页
定期将日志和新的页面复制到 S3
定期进行过期版本垃圾数据回收
定期验证 CRC 码页,如果发现损坏数据块,与相邻节点进行通信获取完好的数据块。这是 Aurora 实现可自主修复损坏数据块的关键技术
注意,只有步骤 1 和 2 完成,前台即完成(commit),其它操作都是异步进行。
Aurora 读写处理过程
这块内容比较烧脑,有些地方原论文写得也很模糊,需要加上一些猜测和理解,其中里面也有很多术语,我们先把主要的进行简单解释:
LSN:LogSequence Number 日志顺序号,每个日志都有一个自动增长顺序号(传统的MySQL 也有这个概念,类似 Oracle 的 SCN)
VCL:VolumeComplete LSN 存储可以保障的可用的最高 LSN
CPLs:ConsistencyPoint LSNs 一致点的 LSN
VDL:VolumeDurable LSN 已经持久化最大的 LSN,最大的 CPLs
SCL:SegmentComplete LSN 已经完成的段的日志号
在 Aurora 中,数据库不断与存储服务交互,并维护节点状态以便进行仲裁、更新最新的持久化的卷(VDL)。在正常情况下,数据库根据每个节点的重做日志记录,来更新目前 VDL 。数据库随时都会有大量并发事务,它们各自生成自己的重做日志记录。数据库为每个节点分配唯一顺序的 LSN 对其进行约束,该 LSN 等于当前 VDL 和定量 LSN Allocation Limit (LAL)之和(目前为 1000 万)(VDL + LAL)。此限制确保数据库不会超前于存储系统很多,如果存储或网络无法跟上,则可降低传入写入的压力。注意,每个 PG 片段只看到已影响该卷中日志记录的一个子集(affect the pagesresiding on that segment)。每个日志记录包含一个链接,指向之前的记录,通过这个反向链接,可用于跟踪已经在每个段建都完成的日志点,并根据这个确定已经完成的 LSN 终点(SCL),最终知道哪些记录已经到达该节点上。每个存储节点互相通信(gossip),通过 SCL 用于查找和交换丢失的日志记录。
在 Aurora 中,事务提交是异步完成的。当用户提交一个事务,线程将事务移动到提交列表,写下该事务的 commit LSN ,然后线程继续执行其它工作(和传统不一样)。这相当于在 WAL 的基础上,完成一个承诺,当且仅当新的 VDL 大于或等于事务提交 LSN(commitLSN ≤ VDL)。等待提交会使用专用线程发送到存储节点,收到对应批次的日志记录的 4 个 ACK。
在传统 MySQL 数据库中,为了减少磁盘 IO 采用成组提交技术。第一个写日志缓冲区的线程需要等待预先设定的时间,然后再执行磁盘 IO 操作;或者等到日志缓冲区页写满以后再执行 IO 操作。这样子做的结果是第一个写日志缓冲区的线程需要挂起等待,耗费时间。这是一个同步操作,在持久层存储的 ACK 返回之前不能进行其它工作。
下图提交队列里面有 3 个挂起的提交请求,分别是 Pending commit group1,Pending commit group2,以及 Pending commitgroup3。主实例收到针对 Pending group commit1 的 4 个以上的日志持久化 ACK以后,将系统 VDL 前移至 22,第一组的状态从pending 变成 committed。此时后台线程检查提交队列,然后成批提交 LSN 小于等于 22 的事务 T1,T2,T3。
注意,即使后面 Pending commit group3 先于 Pending commitgroup2 收集 4 个以上的存储节点返回的持久化 ACK,那么也不能移动数据库持久化位点。因为,这个数据库持久化位点是 Aurora 崩溃恢复以后决定开始重做的位点。跳过前面的成组提交的 LSN 会导致数据库丢失某些数据。在系统崩溃恢复的时候,系统检索最新的数据快照与相应的日志记录(其 LSN 大于数据库持久化位点),即可将数据库恢复到最新的一致性状态。
和大多数数据库一样,Aurora 读操作首先会在缓存区里进行扫描,如果缓存里没有对应数据,才会将请求下发到存储系统,如果缓存区满了,则需要将一些页面进行挤出(部分页面是受保护的),如果缓存区里有脏数据,则需要像将脏数据持久化到磁盘,这样才能确保其他的事务可以读到最新的数据。
只有当缓存区页面的 LSN (标记该页面的最新版本日志号)大于或等于 VDL 时,该页面才可能被刷出,在这之前这个页面是受保护的,这个机制确保了:
所有页面的更新都已经持久化到日志。
在缓存区没有该数据页的情况下,可以根据 VDL 获取最新版本数据。
正常情况下,数据库读操作并不需要采用多数投票的方式,当从磁盘读取数据时,会分配一个读位点(read point),这个点代表产生时刻的 VDL,同时数据库在存储节点维护 SCL,系统根据读位点和 SCL 来确定从哪个存储节点进行读取,只有当故障恢复或者必要的时候,才会根据多数投票的方式来确定系统的 VDL。
在 Aurora 中,多达 15 个副本都可以装入单个共享存储卷。因此,读副本不会在写操作方面增加额外的开销。为了最小化延迟,写入同时传送到所有副本上。
Aurora 将数据库文件切分成 10GB 大小的块,每个块都有专属的日志记录。在崩溃恢复的时候,系统利用多数派读,确定运行时的一致性状态。恢复模块首先确定最大 VCL(最大的顺序 LSN),截断此后日志。进一步,可以将需要重放的日志限制在其 LSN ≤ CPL。VDL 取最大的CPL,LSN 大于 VDL 的日志记录都可以安全地截断。例如,最大已完成的日志记录的 LSN 为 1007(尚未提交),但是系统的 CPL为 990,1000, 1100。系统可以确定 LSN 大于 1000 的日志记录都可以忽略。确定完重放日志的 LSN 最大值以后,Redo 操作就可以并行在不同 Segment 上执行了。
在原论文里,该章节的标题直接写成了 PUTTING IT ALL TOGETHER,笔者的理解是它想表达 Aurora 和 AWS 现有框架下的多种云产品进行集成,形成了一个整体数据库云服务。先来幅鸟瞰图:
如图,应用通过 VPC(私有虚拟网络) 接入,然后可以读写位于不同 AZ ( 可用区 ) 的数据库。数据库的部署是一主多从的集群架构,一个写入的主节点( Aurora 不属于完全的分布式架构),多个只读的从节点,从节点+备节点最多可以有 15 个(为啥?AWS共享存储系统可以让 15 个副本装入单个共享存储卷)。节点之间通过 RDS ( RelationalDatabase Service ) 来交互,RDS 在这里充当 HM ( Host Manager ) 角色,它通过 Agent 来获取主从集群的状态监控、当主节点 Failover 的时候,它会进行 HA 调度、或者某个从节点 Failover 它会进行下线替换。这个负责监控、管理的节点,称为 Controlplane 。
这个直接转载自论文的数据,目前还无从实测,不过从上面的设计理念上,有可能是会大幅度提升的,不做过多评论,我们只关心写入性能。
Aurora vs TiDB(Spanner&F1)
回顾数据库的发展历史,我们可以简单分成三个阶段,从上实际 70 年代 E.F. Codd 提出关系模型到本世纪初,基本上是以单机关系型数据库为主,如 Oracle、MySQL、PostgreSQL,但随着互联网搜索、社交的发展、数据量爆发增长,传统数据库高成本,无法线性扩展问题日益凸显,分布式 NoSQL 快速发展,如 HBase、MongoDB,近几年来,大家发现很多的业务并没有办法直接使用 NoSQL 的模型,应用需要很复杂的开发才能实现 SQL 和事务能很简单实现的功能,所以新一轮的数据库开始了 SQL 回归,这种可扩展、高性能、支持 SQL 和事务的数据库被大家泛称为 NewSQL。
NewSQL 又分为不同的方向,除了本文提到的云计算 RDS,还有分布式关系数据库,其中的代表产品如谷歌的 Spanner&F1,还有国内人气厂商 PingCAP 的 TiDB,TiDB 具有支持 SQL、水平弹性扩展、数据强一致性的高可用等特性,篇幅限制,这里不展开,详情可参考 TiDB 的官方文档 (https://pingcap.com)。
虽然 Aurora、TiDB 是两个不同方向的产物,但作为两个引领潮流的数据库产品,笔者按照体系结构、数据格式、存储引擎、容量能力、扩展性、兼容性、移植性、灵活性、读写性能、内存机制、异地多活等若干维度进行下对比:
架构:属于 Shared disk 架构(存储可以简单理解为共享,实际上是一套多副本复制存储)。
存储:数据格式属于传统的表结构(InnoDB 引擎)。
写扩展:单点写入,系统写入性能有上限(虽然正在计划中的 Multimaster 理论上也会提升写吞吐,但是仍然受限于实例之间的内存交互及存储之间的 Apply log),从写流量看不属于真正的 Scale out。
读扩展:实例可弹性扩展。
大小限制:数据大小上限 64 T。
读写响应:大大简化 commit 的机制,读写响应时间很好。
兼容性:由于实例层本身就源于 MySQL,所以对 MySQL 兼容性好(100%)。
移植性:移植性很差,它依赖 AWS 很多组件,尤其是底层的共享存储系统,所以基本不具有移植性,它定位就是一套云环境下 RDS。
灵活性:架构可改造性很差。
内存交互:日志会在实例节点 Buffer 之间进行复制应用,通过这种“内存交互”方式读性能会更优。
事务性能:不存在 2 PC 及写扩大的问题,写入性能上更优。
高可用:不是强一致机制,极端情况下通过其它节点可能读不到最新的数据。
节点状态:实例分主次,只能通过从库选举方式来进行实例层的高可用。
适合场景:主要解决的 OLTP 场景,它的优化器和 MySQL 一样,不是为分布式系统设计。目前还不支持Hashjoin 、并行等技术(从2017 AWS Re:Invent 最新消息看,后面会逐步支持,包括 Pushdown query),但综合比较看仍不适合 OLAP 分析场景。
开源状态:商业数据库。
架构:属于 Shared nothing 的分布式架构,他通过 Raft 协议进 同步,实现每个 Raft group 内的数据强一致性。
存储:数据格式是 KV,数据模型是基于 LSMTree 的模型,底层(Rocksdb)。
写扩展:写入分布式写入,线性扩展,写入容量理论上限很高,LSM 写入性能更好。
读扩展:存储节点、实例节点都可进行线性扩展(读写扩展),属于典型的 Scale out。
大小限制:理论无上限。
读写响应:两阶段提交、Raft 复制对响应时间都会有一定延迟成本。
兼容性:高兼容 MySQL 连接协议,但和 Aurora 比,不支持存储过程,触发器,兼容性略差。
移植性:具有很好的移植性,支持多种部署方式,可以在公有云、私有云、私有环境下快速搭建。
灵活性:实例层、存储层拆分的很清晰,独立,可在 TiKV 上直接调研 API 、或者部署诸如 Tispark 完成架构的灵活多样性。
内存交互:实例节点 Buffer 不存在交互机制,不能共享内存。
事务性能:分布式事务锁机制,写入性能,尤其是冲突比较多写入场景性能较差。
高可用:只要网络条件允许,可以创建真正意义的异地多活分布式数据库。
节点状态:实例层无状态,实例层的高可用更胜一筹。
适合场景:首先解决了 OLTP 场景,同时通过 Tispark 可以比较好的支持 OLAP 场景,而且由于共用一份 TiKV 集群或者实时同步机制,能实现实时分析场景。
开源状态:开源数据库,社区活跃。
两个产品作为 NewSQL 的两个方向,都属于跨时代的产品,也都有很多令人激动的 Feature,他们用各自的方式提升、扩展了数据库的写入容量,都是替换 Sharding + Proxy 很好的方案,Sharding + Proxy 虽然是目前主流方案,但它存在很多问题,具体可关注笔者近期将发布的另一篇文章《数据库 Sharding +Proxy 方案成本汇总》。
Aurora 在内存一致性、读写性能、MySQL 兼容性、事务限制、内存管理、Cash recovery 等方面优于 TiDB。但反过来 TiDB 在弹性扩展、架构的解耦性、灵活性、场景覆盖等方面更胜一筹,更重要的是 TiDB 移植性很好,笔者觉得这点会成为两类产品普及度一个很重要的因素,同时 TiDB是可以真正实现跨区域的异地多活的分布式数据库。
总结
云计算的发展为存储分离的提供了更多可能,也加速了这种趋势,传统关系型数据库需要在单机层面花费了昂贵的成本去实现 ACID,这样导致单机的容量非常有限,以 MySQL 为例,M-S 的架构虽然解决了读流量的扩展问题,但写流量成了最大的瓶颈(虽然有各种 Sharding、多主等方案,但本质上并未解决写流量扩展问题),Aurora 依托自身的云产品,把数据库这个盒子打开进行抽茧剥丝,通过把写入、恢复等昂贵操作下沉到存储服务中的方式,很大程度了提升了集群的写入容量,粗略估计可能会提升一个数量级,在这个容量范围内的 OLTP 业务覆盖量还是非常大的,但反过来说,这种架构本质上还是单节点写入,而且从目前看上去 Aurora 在实例层改动很少,这么看上去有点小马拉大车的感觉,笔者把它理解为变向的Scale up。同时,Aurora 在自动拓展存储容量、自动修复数据、闪崩恢复、将缓存从实例中分离持久化进而解决了热数据问题上都是值得借鉴的很好创新。Aurora 可以说是在云环境下(AWS)量身定做的产物,它所依赖的共享存储,不是简单的共享,而是一个包含计算,能自动复制的共享存储服务系统,但这一点就就很难进行复制。
最后,不管是计算与存储分离的云数据库,还是分布式数据库,NewSQL 的时代已经来临。
https://amazonaws-china.com/cn/rds/Aurora/
http://www.allthingsdistributed.com/files/p1041-verbitski.pdf
https://www.slideshare.net/AmazonWebServices/aws-reinvent-2016-deep-dive-on-amazon-aurora-dat303
http://mp.weixin.qq.com/s/COa2Q8iwhFUSbdB5LrJ9sA
http://www.tuicool.com/articles/2uMR3y7
近期热文: