Chinaunix首页 | 论坛 | 博客
  • 博客访问: 19347
  • 博文数量: 11
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 280
  • 用 户 组: 普通用户
  • 注册时间: 2014-08-01 10:13
文章分类

全部博文(11)

文章存档

2015年(10)

2014年(1)

我的朋友

分类: IT业界

2015-01-20 14:18:20

阿里云:OTS产品设计和技术实践
思朴科技是万网最著名的代理商,万网钻石级别代理商,近5年,一直以优惠价格累计为上万客户提供万网主机,阿里云主机,并且一直保持高速增长,一对一的技术服务理念受到很多用户好评,2012年,2013年连续多年荣获万网和阿里云5星级代理商 ,思朴科技同时是万网代理和阿里云代理。




2014年12月27日,阿里云课堂第五期在深圳开课,“结构化存储与结构化数据服务的技术实践”主题分享在众多朋友的期待下精彩上演,现场观众再次爆满。本次活动中,仇应俊和杨成虎(花名:叶翔)两位讲师为大家献上了精彩演讲,并在OpenSpace环节与观众展开讨论,积极互动。应广大用户要求,我们将云课堂讲师现场分享内容全文整理出来,供大家参考。阿里云课堂会继续在全国各地陆续开课,欢迎大家继续支持!


 


以下为讲师仇应俊的分享内容:


 


 


    今天我想从几个方面跟大家分享一下OTS产品相关的工作,首先会介绍一下OTS产品主要是干什么的,它有哪些功能;第二是跟大家分享一下我们在做这个产品的过程中可能会遇到一些技术的挑战是什么,之后我会从系统的架构、具体实现等方面来分享我们的各种考虑,可能不会涉及到太细节的东西,大家有什么问题可以随时提出来。


    OTS是构建于阿里云飞天分布式系统之上的NoSQL数据,提供海量结构化的存储和实时的访问,它主要以实例和表的形式组织用户的数据,通过数据分区和负载均衡实现规模的扩展。我们提供API和SDK给应用来访问OTS,另外应用也可以通过产品官网的控制台做一些简单的访问。


 


 


    我列了这个产品的几个主要特点,这也是我们在设计这个产品时的目标。


    1,大家了解NoSQL的话,它的目标是要解决规模的问题,实际上通过简化很多数据库的功能,然后把规模能做上去,这也是OTS产品第一个要解决的问题,也是我们产品的特性,是它的规模是可以水平的扩展。


    2,对应用来讲,数据库的服务必须是一个很高可用的产品,而不是经常出现各种各样的问题不可用。应用写到OTS中的数据必须是高可靠的,应用不需要担心数据丢失的问题。


    3,数据库的服务,规模在扩展的时候性能必须是可预期的,而不是随着数据规模、访问量变化的时候性能会有大的波动和变化。


    4,灵活的数据模型,NoSQL和传统数据库有一个区别,它的数据模型比较灵活。可以是非常稀疏的一张表,表中的每一行的列可以不相同,可以动态的增加和减少。


    5,读写强一致性,应用写进去的数据,只要成功返回了,后面的读操作是能立即读取到的。


    6,访问安全,访问方面我们做了一些安全的身份认证,包括一些权限的管理。


    7,全托管服务,实际上是免去应用自己维护很多后面的运维、升级、部署的操作,包括一些调优,这些东西都是托管,应用只需要去购买服务,按使用量付费就可以了,系统也是资源共享的池子,有很多应用可以在里面申请资源,不用关心后面的系统是怎么部署的,升级怎么做。


 


 


    我再介绍一下里面数据模型,数据存在OTS里,应用看到的是什么东西?最核心的是一个表的结构,表里面我们可以看到有很多行,每一行里面刚才也讲了,列的属性是可以不一样的,有些行里面有些属性是另外一行没有的,对于同一列上的内容在不同行上语义和类型也是可以不一样的,实际上他是每一行是完全独立的存储。


    这里面有一个关键地方,一行里面有一个主键(PrimaryKey)标识数据,主键包含多个列、对于每一行来说这些列是都必须填的,而且组合起来必须是唯一的东西。整个表的数据是以主键做索引的。主键里面有一个关键的“第一列”,我们叫做数据分区键,OTS表的规模可以扩展到很大,最终需要分布到多个存储节点上,一台机器也提供不了。我们采用水平切分的方法,每个切分叫数据分片,分片会被系统调度在不同的服务节点上。分片是根据主键里的第一列,根据这个值的范围做切分,每个分片的数据范围拼起来是一个更大的连续的范围。在具体操作的时候,任何一行数据进入系统的时候,先去看一下主键里的第一列落在哪个范围里,是属于哪个分片,然后由相应的节点进行处理。这是OTS产品最关键的数据模型。


    我们这个产品提供的主要功能API非常简单,有表的管理,创建表和删除表、更新表的一些属性,应用可以去遍历一个实例下面有多少张表。数据的读写操作,这里面有两组,一组是单行的,插入、更新和删除一行,读一行,这个时候每一个操作需要给一个唯一的PK,通过这个PK定位这一行对应的数据在哪个数据分区和存储节点。另外一组是批量的操作,可以批量写和读,还可以给一个PK的范围来读取这个范围内的数据。看功能是非常简单,就是行的读写操作,相对于传统数据库的功能非常简单。


 


 


    那么很多同学可能会问,这么简单的东西有什么用呢?我们在做OTS产品的时候看到很多相关的应用场景,我列了几个比较主要的,第一个是互联网上用户数据的存储,比如说用户的邮件、联系人和日程安排,还有很多个人网站上的客户数据,这些客户的属性数据都可以存在里面,这是一类比较大的应用场景,这个场景在我们阿里集团里也是有很广泛的应用,阿里集团里的云邮箱和云OS手机联系人短信,都存在这个产品里。


    另外一大类应用产品是智能设备的状态数据,今天在移动互联网和物联网的时代,智能设备越来越多,智能设备产生的数据量也是越来越大,这些设备产生的数据有两类需求:一是数据存进去以后,希望什么时候拿出来看一看,这个设备的状态是不是有问题,设备异常的时候去OTS里查一下具体是什么情况。二是数据进了OTS以后,可能应用需要把数据同步到一个离线分析的产品里做后续的数据分析,实际上这个也是OTS很重要的场景,数据进入OTS之后同步到ODPS进行离线的数据分析。三是很多系统构建分布式的架构,很多节点之间需要同步一些meta的数据,选择OTS做基础的组件也是非常典型的场景,这个在阿里集团里也是有很广泛的应用,比如ODPS和SLS。这些主要场景的数据量规模都会比较大,对于可用性要求比较高,但是对于查询的未读和复杂性都是比较简单的,比较契合OTS设计上的考虑。


 


 


    那么我们在做这个产品的过程里可能会碰到什么样的技术挑战?第一个在分布式的环境里,各种故障和错误会经常发生,有软件的bug导致进程crash,也有硬件的问题导致断网断电,有的时候网络风暴等,这些问题都会导致整个环境很不稳定,那么我们的系统怎么处理这些问题,能够对于用户、对于我们的客户呈现出比较稳定的服务?这是我们第一个会考虑的,怎么解决这类问题。第二个是因为整个服务是共享的资源,在共享的资源里,有很多应用在里面,访问量也在变化,数据量也在增长,这样的环境里怎么保证用户操作的性能是可预期的,涉及到很多资源的隔离、资源的调度。第三个是提供在线服务,难免会出现一些紧急的线上状况,运维管理大型的系统时,他有非常多的工作要做,怎么能让他们很实时的看到这样一个系统的状态,系统里面的问题能够实时的反馈给运维人员来做后续的动作,包括怎么提高他们的生产力。实际上在我们做的过程中,我们会发现运维的人员是非常苦的,他们的生活质量取决于我们的开发人员怎么把这些事情考虑到,考虑到运维的线上工作。我后面会围绕这三个挑战来讲我们的工作。


    今天阿里云的每个服务会在不同的区域(Region)里进行部署,对于OTS而言同样如此。一个区域内会有多个OTS的服务集群,这些服务集群通过我们后台的一个集群管理的模块(OCM)负责管理。用户创建的OTS实例会被分配到不同的集群上,系统会为每个实例在DNS里注册一个相应的域名,应用要通过这个域名来访问OTS服务中的表和数据,这是从最外层的框架来看OTS部署的结构。


    然后我们再看一下单集群内部的架构,单集群内部是有这么几个模块,底下是飞天的模块,有盘古、伏羲、女娲、夸父、神农、大禹。盘古是分布式文件系统,负责管理数据的存储,伏羲管机器资源状态的服务组件;女娲是一个名字解析的服务,同时提供一个分布式锁;夸父是网络通讯的组件;神农是提供监控的一套工具和平台;大禹主要是负责整个集群的部署。


    再往上是OTS核心的结构化存储引擎,这里面主要是有Master<->Worker的架构,这个我后面还会讲。再往上是服务协议的处理,所有的用户请求要经过这一层做一些身份认证,做一些参数的检查、协议的处理。这层也是分布式的部署结构,每一个节点都是一个无状态的服务。


    实际上它也是一个单Master节点,多worker节点的经典架构,在OTS整个服务里面有很多表,每个表会有很多的分区,Master会把表的分区均匀的调度每个机器上。每个分区(Partition)都会在女娲上注册一个锁,后面会讲到为什么会有这个锁。Partition有几个相关的内容,在Partition内部有一个memory table,它是一个内存结构,为了防止出现故障的时候进程crash数据丢了,在数据写Memeory Table之前先要写CommitLog。一个Partition有三部分,CommitLog,MemoryTable,还有MemoryTable在后台dump成的data files。


    我们可以仔细看一下,每个Partition对应盘古上的一个目录,数据在盘古上到底怎么存呢?一个目录下的数据包含这个partition对应的CommitLog和Datafiles文件,这些文件的存储是交给盘古做的,但OTS这一层有一些参数需要指定,指定所有的文件都是有三份拷贝,确保数据的安全性。


    我们可以看到,任何一个请求进入系统以后,首先会经过Partition的查找确定请求应该发送到哪个机器上,这个路由信息由master来管理,但这个信息会缓存在client这一端,这样就不需要每个请求都和Master有进行交互,只要通过client本地cache可以找到对应的worker机器,把请求发到正确的Worker。


 


 


    我们看一下数据在OTS里是怎么持久化的,这个对于服务来说是非常关键的一环,数据必须是持久化的,不能丢失的。第一个点是数据先进入Tengie+OTSServer这一层,服务器接收收据进行协议解析、参数检验、身份的认证,确保这一条请求是合法的请求。Client根据数据的Partitionkey找到正确的Partition,然后通过Partition找到正确的Worker,把数据请求送到正确的worker上,worker拿到数据以后会为数据获取行锁,确保行上的并发写。然后根据数据生成一个Commitlogrecord,把commit log record写入到partition对应的CommitLog文件中。如果确保CommitLog的数据不会丢失呢,写CommitLog时会写3份数据,这里有一些讲究,3分数据在不同的机柜上,机柜在供电上都是有区别的,三份都写成功了以后盘古才返回,确保这个数据在盘古里不会丢了。后面把它对应的数据写入分区对应的Memory table里,后面把行锁释放掉,返回成功,那么数据就立即被读到。


    Memorytable数据越来越多,后面万一出现Worker进程cransh,那么CommitLog一点一点重放的话,时间会非常长,在重放完成之前,服务不可用,所以对服务的影响会非常大,所以这里有一个机制,Memory table的数据会在后台dump而成盘古的data file。由于Memory table的数据不停的dump到盘古内,随着分区的数据越来越多,也会有越来越多的data files文件,这对盘古是一个压力,整个系统对文件的个数是有一个上限的。文件之间是有些重复的数据,应用先写了第一行到Memory table,dump成一个data file,过两天应用又写了同一行数据,又dump成另外一个data file。同一行的两次写,会出现在不同的data files,实际上只有最新的数据是有用的,其他的都是垃圾。在线读的时候需要把这些数据都读出来做归并代价也很大,所以要对同一个partition下的data files定期做compaction,把data file的个数降下来,并且去除重复的数据。这个是整个数据持久化的流程。大家如果看过Big table的论文都会比较清楚,这就是LSM的架构。


 


 


    我们再讲一下数据分区的管理,OTS里数据分区管理是非常重要的功能和模块,不管是新创建表还是删除表都会导致分区的调度,表对应的分区必须是能调度到一个新的节点上,对于这些旧的表如果有删除,他对应的Partition,对应分区的资源就得很快的释放掉,这里会涉及到分区的加载和卸载。我们在做分区调度的时候,一张表的分区尽量被调度到不同的SQL Worker的节点上,这里主要的考虑是:当有些机器的节点出现故障时,对一张表的影响比较小,如果一张表的分区都在一个节点上,只要出现问题,整张表的访问都会受影响,这是一个考虑的因素。还有一个因素,分区被分散到多个机器节点上,能够更好的分担表上的访问压力。不同的SQL Worker的节点分区的个数应该尽量均匀,使机器的资源不会闲置太多。


    在做分区调度和管理的时候,还需要考虑黑名单和白名单,黑名单是有些机器肯定不能往上调度分区,经常发现有些机器莫名其妙,比如说系统会hung住,或是出现一些抖动,时通时不通。对这些机器来说分区调度的模块必须得考虑到,一定不能调度分区到这些节点上。调度考虑白名单,主要是在产品的升级流程中,如果两个版本之间有些数据兼容性的变化,升级完的节点产生的数据肯定是新的格式,一旦这些分区产生了新的格式,当出现机器宕机、节点坏了的情况需要迁移这些数据分区的时候,这些分区就不能再往老的节点上调度,因为一旦调度上去就会发生数据的错误,老的节点不认识新的分区里的数据格式。在这个时候必须得有一个白名单,升级完的节点上的数据分区也只能往已经升级过的节点上调度,不能往老的节点上调度,有这样的一些边界的情况需要来考虑。


    还有分区迁移,分区迁移过程是在老的节点上先去卸载,新的节点上在加载的过程。什么时候会进行分区迁移呢?主要是在升级的过程里,我们现在为了尽量减少服务升级对用户的影响,会一台一台的去升级,在升级的过程中,先把这台机器上的Partition迁移到别的机器,迁移过去以后再去升级这个机器上的版本,进程重启一遍,这样对客户的影响最小。刚才也会提到,这个过程中会采用白名单的机制。还有一些特殊的情况,有的时候一些Partition分区很热,有些时候并不是特别容易解这种热点的问题,运维可以先把它快速隔离到一些特殊的节点上,这样不会影响机器节点其他的分区访问情况。还有一些非常极端的情况,有的时候分区内的数据会因为一些Bug导致它坏掉,加载的过程中不能影响后续Partition的加载,我们需要把它卸载掉,或是迁移到特殊的机器节点上,让他自己在那边,不会影响别的数据分区,然后有足够的时间来进行修复。在这个过程中,数据分区的迁移,我们需要一秒钟左右来完成。对应用的影响是比较小的,这个过程中尽量把后端的错误隐藏掉。


    分区随着应用请求数据不断产生,数据分区必须分裂掉,分区在一个节点上的存储有上限,访问量越来越大的话,一个单节点也做支持不了。这个过程中要做数据的分区分裂。分列的时候根据什么样的指标来判断,什么时候该去做分列?这个过程中会有一些指标参考,比如说今天参考数据分区的大小,后续会考虑资源的占用,比如说这个分区访问的QPS是不是很高,延时是不是很大,QPS带来的问题,这些数据反映一个特点,某些Partition是不是已经是过载了,我需要把它做分裂分散到多个数据节点上。


    在做分裂的过程中有几点要特别关注,做分裂要尽量减小对数据分区访问的影响,不能分裂就导致分区长时间不能访问。分裂是要把大的分区分裂成两个小的分区,P1和P2,数据肯定是需要一些分裂的,分裂的过程中不能拷贝数据,拷贝数据会导致分区时间过长,代价太大,所以我们通过linkfile的方式来实现,确保不去拷底下的盘古文件,在新的Partition加载的时候处理这些link文件也能读到数据。通过Linkfile,分裂的过程可以很快完成。这是一个考虑的点。


    第二个是Memorytable里有如果有很多数据,分裂出来的partition在加载的时候就需要重放很多commitlog数据,导致新的partition不可服务时间变长,这是我们不希望发生的。所以在分裂之前会先把Memory  table的数据dump到成data files,把Commitlog的checkpoint提升上去,这样分裂结束以后,新的Partition加载起来的时候,需要重放的Commitlog数据就比较少,减少对应用的影响。前端对于分区分裂的情况,也会做一些特别的处理,一个请求原来访问的老的数据分区,现在要到新的分区上面去,我们会在前端做一些重试,减少错误发生的概率。通过这些方式,我们尽量把一个分区的分裂对应用影响时间控制在3到5秒的时间范围里。


 


 


    下面在再讲一下系统在实际生产的过程中会有哪些异常和错误的情况需要处理,以及怎么处理。故障主要包含软件上的,比如说进程的crash,有的时候内存有泄漏也会导致系统挂掉,还有一些硬件方面,比如说机器的断网、断电、死机等。在处理的时候,我们一般有一个基本的原则,先把故障的范围控制住,比如说能通过单个请求的报错那么就报错,不会crash进程,crash影响的范围会很广。再就是把错误局限在一个进程之内,不会让一个进程的内存泄漏导致整个机器出问题,而应该把这个进程的问题修复掉。错误怎么快速的恢复?我这里举两个例子:一个是SQLWorker的进程发生故障了,crash怎么处理,第二是节点断网、宕机怎么处理。SQLWorker进程如果crash,系统会很快把该进程启动起来进行数据分区的加载,加载的时候需要做并行化的处理。比如说一个节点上会有几十个、上百个数据分区,重新加载的时候就要并行的加载,加快恢复的速度。对于每个分区恢复的时候,从Commitlog读数据进行恢复,恢复到Memorytable中。恢复完了以后会从data files读索引等数据,之后就可以提供服务。这个过程大概在10秒钟左右。整个分区是并行的加载,有的分区会先完成,有的会后完成,所以在完全加载之前,有些分区已经能够提供服务了。


    对于SQLWorker节点断网和宕机的情况,我们是通过心跳的检测,这个机器已经访问不通了,这个时候需要把机器从系统里摘除掉,摘除之后会自动触发数据分区的迁移和加载,这个过程里,是先把机器失效的信息告诉master,他会做重新的调度,也会并行的把这些分区分散到其他的节点上并行的加载。


    这个过程中有一个东西很重要,有的时候会发现那个节点是假死的状态,你去心跳检测不通了,实际上还能正常的工作,还在往盘古底下写数据,这个时候很麻烦,你把他服务的Partition在另外一个节点上调度起来,这个时候就会出现有两个SQLWorker进程同时处理一个Partition数据的情况,这个在我们的设计里是非常有问题的,这个会导致数据的错误和丢失。这个过程中必须要确保一个数据分区不能同时被两个Worker处理,这是前面提到的parition锁的用处,Partition起来以后,需要去女娲上申请一把锁,等这个锁失效了以后,另外的机器上才能加载这个Partition,重新注册锁的时候才能注册成功,这样确保一个分区是不可能同时被两个Worker处理。同样也是前端端对请求做些重试,减小故障的时间,断网和宕机的产品是需要一分钟左右的恢复时间。这一分钟里,所有的分区加载是并行的,有快有慢,有些分区在一分钟之内就可以提供服务。


 


 


    还有一个问题,在共享的环境里怎么样确保每个应用读写操作性能是可预期的,在隔离方面我们通过两个方面做,一是把机器上的资源规一化成读写能力单元,一个机器能提供多少读的能力,能提供多少写的能力,这就是它的资源上限。应用在创建表的时候需要做一个事情,我要指明,这张表需要多少读写能力单元,这个应用怎么来估算呢?应用需要去预计表上面的QPS是什么样的,每次操作是多大数据量的东西,我们提供换算的方式,最后能够换算成一个表需要多少个读写能力单元。系统会根据读写能力单元往机器上做分区调度,调度的时候会把机器上的能力单元预留给这张表,确保我一个机器上不会有太多的读写访问。


    因为应用的操作我们是没办法控制的,如果真的发生读写量很大的时候,我们发现表上预留的读写能力单元已经不能满足表上的请求了,系统直接会把这个请求拒绝掉,应用在开发的过程中要非常注意这种错误,会出现读写能力单元不足,这个时候要做一些处理,对读写能力单元进行一些处理和上调。我们在控制台上也会提供监控信息,让应用可以看到过去一段时间里实际读写能力曲线的变化,提前做一些规划。这是一个方面,我先把机器上的资源能规定好,确保机器上总的资源不会被过量的使用。


    第二是要确保每个请求的大小,应用的一个请求本身很大,一下子把我后面的资源用光了,我做这个模型也是没有任何意义的。所以我们在API上做了很严格的限制,希望任何一个请求是能在可控的范围里,不会对资源有过渡的使用,这里面细化下来其实有很多方面,我们对PK里面的列数、行的大小,以及一行里面包含的列个数都有一些限制。还有单个String和Binary类型的Cell size不能超过64K,否则很容易产生毛刺出来。对于Batch的操作也有100行的限制。对个GetRange操作如果应用指定的范围很大的时候,系统是不能一次性把这些数据全部返回,需要做一些请求截断,一部分一部分返回,并且告诉应用,下一次取的时候从什么地方开始取,对应用的请求做精细化的处理。


 


    最后是线上运维,这是非常有挑战的工作,线上运维不仅仅是运维人员的事情。系统本身也要做非常多的处理,把一些关键指标QPS、队列大小、吞吐量实际展示出来,让运维人员对线上的服务状态有非常清楚的了解,根据这些指标,去做一些设置,一旦某一个指标超过预值的话,会触发报警让运维人员会做一些相应的处理。


    对于集群资源的使用,我们也会提供运维的报表,对一个较长的时间,比如说一周里使用的趋势是什么?有的时候看一个点不一定是有问题的,但看一段的趋势涨速是非常快的,要引起运维人员的关注,比如请求延时的趋势,比如内部的业务指标涨得非常快,需要做一些趋势的关注和判断。


    再精细一点,可以看到内部各个模块的运行状态,比如说各个地方使用内存的情况怎么样,对于一些资源的消耗,比如说请求队列的情况可以实时的看到。


    除了实时的监控系统,部署和升级对运维来讲是经常发生的操作,这个必须是非常高效的,否则运维会被这个工作脱垮掉,部署和升级方面我们提供了配置的集中管理,包括部署的升级和管理是白屏化的方式,运维做一些简单的点击操作就可以完成。另外,集群可能有几百、上千机器的规模,升级持续时间比较长,不可能马上做完,需要显示进度,甚至是暂停升级,有些时候不是大范围的升级,可能是局部的升级。


    除了这些部署,对于线上的一些仅仅情况,很多时候需要有能力进行人工的干预,现在的情况依赖于运维的快速处理、快速响应、判断、处理,但同时也需要有非常高效的工具来帮助运维做判断和处理。这个时候机器的上线、下线,黑名单白名单的管理,业务涨得非常快,希望快速的扩容,对数据进行一些平衡。还有对于整个分区的管理,有的时候运维需要这样的能力能做到干预。系统关键的参数希望能调整。


    很多时候我们看到一些请求会产生一些毛刺,运维包括开发自己,希望看到这个请求执行的路径,瓶颈在什么地方。对新的功能我们要提供一些开关能关闭掉,新上一些功能可能会存在一些不稳定的情况,运维要有能力进行动态的关闭,规避问题。


    前面提到SQLWorker进程使用内存是比较多的,对于这个进程使用内存的情况运维平时必须非常关注,一旦内存使用较高,就需要作出相应的动作。举一个例子,我们发生过次业务接入数据量在一天内涨得非常多,机器的内存突然一下子接近80%,触发了大量的内存预警。这个时候对于运维来讲,留给他的时间非常短,业务的量已经上来了,不可能让业务降量或是把业务的数据删除掉。这个过程里需要运维的同学能快速的应对,这个时候我们主要采取了一些措施,比如说把Block cache暂时调小,调整系统使用的Memory,释放更多的内存,对于表中的Bloom Filter进行优化,减小内存的占用,当时集群的规模和容量已经不够了,于是要做紧急的扩容,需要有工具快速应对扩容的需求。通过这些处理把内存降到一个安全的阈值,这个过程看起来不是特别的复杂,但是如果没有相应的工具的话,可能影响就会非常严重了。


 


 


    我主要就讲这些,讲我们在做OTS产品的过程里碰到的一些问题,我们是怎么考虑,做了一些什么样的取舍,可能有些地方不是特别细,下面有同学比较关心的话,可以接着聊。我也插播一个广告,OTS产品在今年10月份的时候已经正式售卖了,大家可以关注一下这个产品。另外,我们现在在杭州有一个17人左右的开发团队,我们比较缺人,希望有兴趣的同学联系我们,谢谢大家。
买主机到 ,思朴科技:
买主机到   ,思朴科技:




阅读(1694) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~