前言

通过垂直伸缩和水平伸缩方式构建可伸缩系统。

垂直伸缩:

水平伸缩:

本文为《分布式Java应用》读书笔记

构建可伸缩的系统

垂直伸缩和水平伸缩

通常将通过升级或增加单台机器的硬件来支撑访问量及数据量增长的方式称为垂直伸缩,垂直伸缩的好处是技术难度相对较低,对于小型应用而言是一种不错的选择。其缺点是机器的硬件是无法不断升级和增加的,很容易达到瓶颈,而如果想升级为更高级别的机器时通常带来的成本是指数级的,因此对于大型应用而言,垂直伸缩不是一种好的选择。

通常将通过增加机器来支撑访问量及数据量增长的方式称为水平伸缩,水平伸缩从理论上来说并没有瓶颈,其缺点是对技术上有较高的要求,另外在增加机器时,要考虑机器的增加对于空间、能源的占用。空间的占用要求机柜要足够充足,能源的占用则意味着电力、网络带宽的消耗,这些都会给应用的运营带来更高的成本。除此之外,当机器数量大幅度增加后,机器硬件出现故障的几率也将大幅度上升),此时软件上的容错、机器的管理和维护就显得至关重要了。

垂直伸缩

支撑高访问量Web应用随着访问量的增长,通常其瓶颈会出现在CPU或内存上,网络IO或磁盘IO出现瓶颈的几率较低,在此仅分析当增加CPU或内存时,应用如何做到线性地增长。

垂直伸缩支持高访问量

增加CPU后

增加CPU后要做到增加CPU后系统的服务能力线性地增长,要求系统能够随着CPU的增加,响应速度提升或同时可用于处理请求的线程增加,主要有以下三种情况会造成增加CPU后系统的服务能力无法线性增长。

  • 锁竞争激烈锁竞争激烈时造成很多线程都在等待锁,此时即使增加CPU,却无法让线程得到更快的处理,也无法开启更多的线程来处理请求,应对的策略是尽可能地降低系统中锁竞争的现象。具体的方法可参见性能调优中降低锁竞争的部分,在降低了锁竞争现象后,一方面是响应速度的提升,另一方面则可开启更多的线程来支撑高访问量。
  • 用于支撑并发请求的线程数是固定的在Java应用中,依靠启动多个线程来支撑高并发量,如启动的线程数是固定的,那么即使CPU增加了,系统的服务能力也不会得到提升,因此最佳的方法是根据CPU数(Runtime.getRuntime().availableProcessors())计算一个合理的线程数。例如Sun JDK的并行GC线程数就是根据CPU数计算得到的,这样当CPU增加后,GC的速度就可提升。
  • 单线程任务对于单线程的任务,增加CPU不会带来任何的提升。此时可以考虑按照CPU数对任务进行合理地划分,以能够通过启动多个线程来并行地将任务分解成多个任务完成,在Sun JDK 7中提供了Fork/Join来实现。

在解决了上述三个问题后,通常只要增加CPU就可提升系统所能支撑的访问量。

增加内存后

要做到增加内存后系统的服务能力线性增长,要求系统能够随着内存的增加,响应速度提升,主要有以下两种情况会造成增加内存后系统的服务能力无法线性增长。

  • cache的集合大小是固定的系统中通常会借助cache来提升性能,而为了避免内存资源消耗过多,会限制用于cache的集合的大小,如这个大小是固定的,那么即使增加了内存,能放入cache的数据也不会增多。解决这种问题的方法是根据可用的内存计算出一个比例来控制cache的数据的大小。
  • JVM堆内存是固定的JVM堆通常是在启动参数中设置的,因此有些时候可能会出现增加了内存,但JVM堆大小没有调整。因此在增加了内存后要相应地对JVM堆内存的大小进行调整,操作系统位数对可设置的大小有限制,对于要使用超过2GB内存的JVM堆而言,操作系统升级到64位是很有必要的,在调整了JVM堆内存大小后,值得注意的是要避免GC造成CPU出现瓶颈。

解决了上面两个问题后,通常只需要增加内存就可提升系统的响应速度,从而提升系统所能支撑的访问量。

垂直伸缩支持大数据量

数据量增加到一定程度通常会造成数据库的读写速度大幅度下降,除了数据库软件本身要做到在CPU、磁盘或内存增加后能提升响应速度外,从数据库使用的角度也可做出一些优化,主要的优化手段是分表。当单表中存储的数据量增大后,即使垂直伸缩了也有可能仍然达不到所期望的响应速度,此时可选择采取分表的方式。分表通常采用的方法为按主键ID、按时间等方式,具体的分表策略要根据业务而定,采用分表方式通常要求应用做一定的处理。

例如根据userid对user表进行分表,那么应用在访问user的信息时,就不能再用

select username from user where userID=1220

而必须根据userID及分表的规则先行计算出要查询的表,然后再组装出相应的sql。
分表带来的好处是单张表的数据量减少,因此其读写的速度可以得到一定的提升,其带来的问题一方面是开发的复杂,通常须借助DAL来解决;另一方面是分页查询会比较难处理及有可能产生跨表查询的需求。

水平伸缩

平伸缩相对垂直伸缩而言,在技术上要求高得多,但也可带来无限扩展的可能,这也是目前各大网站通常采用的支撑大访问量的方法,为了做到增加机器后系统的服务能力能线性增长,在软件方面要做很多的努力,以下来介绍一下软件方面要做的变动。

水平伸缩支撑高访问量

对于水平伸缩的方式而言,首先要解决的并不是能否做到线性增长的问题,而是系统能否水平伸缩。系统在运行时通常会存在一些状态,例如用户登录状态。对于这类系统,当从一台机器增加到两台时,就会出现一个问题:假设用户登录时访问到其中一台机器,如其在操作需要登录的页面时又访问到另外一台机器时,则需要重新登录,对于这类有状态的系统,需要借助一些技术方法来解决才可水平伸缩。

因此对于水平伸缩而言,最佳的情况是应用是无状态的,但系统很难做到完全没有状态,业界通常采用一种称为SNA(Share Nothing Architecture)的体系来指导如何构建无状态的应用。SNA架构在实现时通常采用的方法是将有状态的部分集中放入缓存或数据库中,通常数据库采用集中存储的方式,因此这里最需要关注的是放在缓存中的状态如何做到支持水平伸缩。

缓存状态的水平伸缩方法

例如对于用户登录的信息,通常采用缓存的方法来记录它们,当进行水平伸缩时,要保证的就是如何让各台机器获取的缓存信息一致,对于Java应用而言,通常可采取的方法有

  • 集群广播同步。集群广播在集群数量大的时候会产生集群广播风暴,占用宝贵网络等系统资源。
  • 粘性请求。存在单点故障丧失了水平扩展的能力。
  • 分布式缓存等。有状态的数据集中存储,应用集群不存储有状态数据,可以做到水平扩展,是常采用的方案。
分布式缓存


对于分布式缓存而言,需要解决的是Node A和Node B在操作同一用户的登录信息时能到分布式缓存集群的同一台机器上操作。最简单的方法是对用户ID进行hash,并用此hash值与缓存集群中可用机器的总数进行取模,这种方式通常又称为hash取模,其缺点在于当分布式缓存集群的机器发生增减时,可能会出现大量缓存失效的现象,一致性hash是解决这种问题常用的方法。

一致性hash


一致性hash的方法如图所示。它采用的方法为首先求出缓存的节点机器的hash值,将其落到0/232的圆环上,当有key要存储时,即按同样的方法计算出key的hash值,并同样落到圆环上,从此位置顺时针寻找圆环上的第一台节点机器。当hash值超过232仍然未找到节点机器,则以第一台机器作为该key存储的位置。

这种方式的好处是当增加节点时,仅影响落在节点逆时针方向的一小段范围的key,例如增加一个节点到node2和node4之间,那么影响到的仅为落在node2到这个新增加的node之间的key,当减少一个节点时,影响到的也只是其逆时针方向的一小段范围的key。

例如去掉上面的node2节点,影响的仅为之前存储在node2节点上的key,这相对hash取模的方式有了很大的改善。

由于一致性hash算法采用对机器做hash,然后落在圆上,当节点较少时,有可能会出现这些机器节点不是均匀分布的现象,这可以采用虚节点的方式来改进。例如现在有四个节点,直接按hash计算后,可能会出现某节点负责的区域更广,而其他区域更窄,虚节点的方式下则可划分为两百份,然后每五十个虚节点指向一个真实的节点机器,这样就可保证节点的均匀分布了。

文件的水平伸缩方法

在系统中通常会有上传文件这类场景,对于水平伸缩的情况而言,就必须实现在集群中Node A机器上传的文件,在Node B也能看到,通常采用分布式文件系统解决文件的水平伸缩问题。

分布式文件系统

分布式文件系统采用的方法由众多普通PC Server机器构成巨大的存储池,每台机器只存储一部分数据,其本身通常可非常好地支持水平伸缩。例如一台机器能存储500GB数据,那么当要存储2000GB数据时,只要增加到四台机器即可,在采用分布式文件系统后,上传文件的系统行为如图所示:

和分布式缓存一样,分布式文件系统也要保证在Node A上传helloworld.txt到存储机器B后,Node B、Node C在下载helloworld.txt时能找到存储机器B,典型的分布式文件系统有GFS及HDFS,HDFS遵循GFS实现,因此可以认为HDFS和GFS是相同的。

在GFS中,对于上面问题采取的解决方法是增加一个称为主服务器的单点机器。当Node A要上传文件时,Node A上的GFS Client会将文件按固定大小划分,并向主服务器提交文件名和块索引信息,从而得到要存储的目标机器及位置,主服务器根据目前各存储机器的存活状态、硬件使用率等来决定块需要存储到的目标机器,之后Node A将数据存储到目标机器的相应位置上。

主服务器负责记录文件和块的命名空间、文件到块的映射及每个块副本的位置。为了保证安全可靠,同时将数据复制到多个存储机器上,复制的份数可在主服务器上进行设置,当Node B要读取此文件时,则只要从主服务器上获取此文件划分的存储位置列表,然后随机挑选机器进行读取,最后根据块的索引进行合并即可。

主服务器与各个存储机器进行心跳,以保证存储机器是有效的。Node A这类应用端为了避免每次都要和主服务器进行通信,可在一定时间内缓存文件的元数据信息。GFS在存储时将文件分割为固定的块,块的大小默认情况下固定为64MB。对于大文件,划分为多块存储在不同的服务器上,读写时都是同时操作多台服务器,因此速度比较快。但对于小文件而言,则意味每次只能从一台服务器上读取,当存取大量小文件时,会对向主服务器的查询造成一定的压力。因此GFS并不适用于存储大量小文件的系统,GFS之所以将块设置为64MB的原因是Google的业务场景中大部分都是大文件,另外对于主服务器而言,块越大,要存储的信息就越少,这样可以降低主服务器的内存消耗和压力。由于GFS这类分布式文件系统只须采用大量廉价的机器就可构成一个巨大的存储空间,并且具备了很好的水平伸缩能力,因此目前多数互联网公司都选择采用分布式文件系统来存储海量文件。

应用的水平伸缩方法

在系统建设初期,会采用将各种业务都放在同一个系统的方式,这会导致这个系统日渐庞大,所需的资源(CPU、内存、数据库连接)越来越多,在进行水平伸缩时要考虑系统里各种业务会造成的资源增加的现象,这种状况会导致水平伸缩很难进行。

例如增加机器后就造成了多个数据库连接的增加,对于这样的状况,通常采取拆分应用的方式来解决。拆分应用通常按照业务领域来划分,即将原在同一系统中处理的功能拆分到各个不同的业务系统中,例如eBay将其业务系统拆分为商品、用户、评价、交易等,拆分后的结构如图:

拆分后单个系统的功能较为单一,在进行水平伸缩时更容易判断其伸缩会带来哪些资源的增加,并且由于之前由众多功能共同分享的机器资源现在变为独享,因此除能更好地支撑水平伸缩外,对于提升系统的响应速度也会起到很好的作用。

水平伸缩后带来的数据库问题

系统水平伸缩后,对于数据库而言,通常会带来的一个问题是数据库连接池的增加,而由于大多数数据库对水平伸缩支持得不好,因此通常要做一些处理来解决这个问题,避免由于数据库连接资源不够限制了系统的水平伸缩能力,通常采用的有以下四种方法。

缓存(cache)


通常可在系统中会采用多种缓存尽可能地避免访问数据库,其结构如图所示

  • 页面静态化对于一些信息变动不多,且无须根据访问用户来进行展示的页面而言,可转为生成静态的页面,这样当用户访问这些页面时,就无须再从数据库中读取了,一方面可提升系统的响应速度;另一方面可降低对后端的访问压力,从而降低数据库连接数,例如新闻页面或网站的通告页面等可如此。
  • 页面片段缓存对于一些不能静态化的页面,页面中仍然会有些部分相对变化不大,且无须根据访问用户来展示的片段。对于这样的页面,可将这些片段信息进行缓存,当用户访问时,Web服务器只需从缓存中获取这些片段信息即可,而无须对数据库进行访问,通常可基于ESI[插图]等方式实现。
  • 数据缓存对于一个系统而言,大部分的功能仍须与数据库交互才可完成,对于这些需要访问数据库的功能,可通过将数据缓存来提升响应速度和降低对数据库的压力。例如用户信息,需要缓存的数据量会比较大,因此通常会采用分布式缓存来实现。数据缓存仅适用于变化不多的数据信息,如变化太多,一方面无法保证缓存的高命中率,另一方面导致需要频繁地更新缓存,性能反而不如直接操作数据库好。

以上三种缓存方式对于降低数据库连接可起到明显的作用,这对于实现水平伸缩也会有一定的帮助。

分库

在系统发展的初期,通常会将各种不同的数据放在同一个数据库中,随着业务的多元化及业务系统的水平伸缩,数据库的连接数会成为稀有资源,对于这种情况,分库是个不错的选择。分库通常按照业务领域将原来存储在同一个数据库的数据拆分到多个数据库中。例如eBay就按照其业务领域拆分为商品、用户、评价、交易等数据库,每个数据库只用处理相关业务的数据,因此可用的数据库连接数就会得到很大提升。

对于系统而言,通常会带来如下几个问题:

  • 对于已有的系统,这意味着要将之前访问同一个库的地方修改为访问相应的库,通常这会造成已有众多系统都要修改;
  • 对于有些要跨数据库操作的业务而言,就变得较为复杂了,通常要从原来的联合查询转变为多次查询,而写入类的如要保证事务则可能要引入分布式事务。

分库是系统发展到一定规模后通常采用的手段,其对于支撑业务系统的水平伸缩可以起到很大的作用。

异步数据库访问

目前大部分数据库访问仍然采用同步方式,每进行一次数据库操作就要占用一个数据库连接,并且要等到数据库操作执行完毕才会将连接释放。这对于高并发的系统而言就很容易出现数据库连接不够用及数据库资源竞争激烈的现象,异步数据库访问是解决这种现象的一种方法。异步数据库访问要将传统的通过阻塞IO访问数据库的方式转变为采用非阻塞或异步IO的方式来访问。为了支持连接的复用,访问数据库端在执行数据库操作时要生成一个请求ID,数据库服务器在执行完毕后要将此请求ID传回访问端,在支持了连接复用后,就可做到用很少的连接来支撑大量的数据库访问及避免连接资源竞争的现象,在采用异步数据库访问后访问数据库的操作流程如图

DAL(Data Access Layer)

DAL是指系统直接提交给数据库的操作转变为通过DAL来进行提交,在这种情况下,可借助DAL来降低数据库连接的使用,其系统结构如图

在采用这种方式后,无论业务服务器如何水平伸缩,数据库连接都可在DAL上统一控制,但从图中也可看出,这种方式的问题是增加了一个中间层,会导致性能有一定的下降,其稳定性也会受到影响。DAL方式的另一个好处是可透明化分库、分表对于业务服务器带来的影响。由于所有的操作都经过DAL,无论DAL是直接内嵌到业务服务器应用的jar,还是作为中间层,它都有机会根据分库分表规则改变业务请求的sql及要连接的目标数据库,这也是在使用DAL后能够透明化分库、分表的原因。

从上可见,在依靠水平伸缩来支撑高访问量时,除须考虑应用本身是否可水平伸缩,还须考虑伸缩后对后端带来的压力,只有在同时解决了这两方面的问题后,才可依靠水平伸缩来支撑高访问量。

支撑大数据量

数据量增长后带来的问题主要是读写性能的下降。从数据库使用角度来看,可采用一些方法借助机器的增加来提升数据读写的性能,主要有读写分离和多master这两种方式。

读写分离

读写分离采用的方法为当写数据库时在一个数据库上写入,而要读取时则从多个其他的数据库中读取,通常将用于写入的库称为master库,用于读取的库称为slave库。多数数据库均提供了机制用于实现读写分离,例如mysql replication、oracle standby等,mysql replication支持对称复制和非对称复制两种方式。

对称复制是指从master库复制数据库到所有slave库,slave库的数据和master库的数据保持一致。对称复制的优点是各slave库均一致,系统在读取时可以任意挑选其中一个库读取,并且即使其中有几个slave库出现问题,也不会影响业务。其缺点是可能会造成每个slave上的数据量都非常大,从而使得slave库的硬件配置也要非常好,并且每次要从master复制到所有的slave,随着slave机器增加,延时现象可能会越来越严重。

非对称复制是指从master库复制部分数据到slave库,各slave库的数据可能不同,其优点在于各个slave库仅持有部分数据,可提升其读取数据的响应速度,并且每次master写入时无须复制到所有的slave,可以尽可能地减少延时现象;还可根据业务的不同为不同的slave库配置不同的索引,从而保证写入的速度;缺点在于一旦slave机器出现故障,就会导致一些数据要回到master读取,同时由于每个slave的数据不同,使业务服务器在访问的时候较为复杂,和分库后带来的挑战类似。

读写分离适用于读多写少,并允许一定延时的业务中,对于读写比例基本相等的业务而言,如采用读写分离反而会带来大幅度复制,造成系统运行更缓慢。在读写分离实现后,系统可通过水平伸缩来降低数据库读的压力和提升数据库读的响应速度。

多master

为提升数据写的速度,通常可采用的方法是建立多个master,在建立多个master时最理想的状态是多个master没有关联,也就是多个master的数据并不相同。例如按用户将其所拥有的物品划分到一个单独的master机器去写,这样当用户需要操作时只要操作所对应的master机器即可,这种仅适用于读取数据时无须联合读取的情况。

另外一种多master的状况则为每个master的数据要保持一致,此时一个明显的挑战是数据一致性的问题,另外一个明显的挑战是自增ID的问题。

数据一致性的问题通常采用复制、两阶段提交、三阶段提交或google paxos来解决,复制要求数据具备版本信息,而对于有些场景而言,必须做到同步的一致性。例如用户的存款余额,在master A数据库修改的同时,在其他的master数据库也必须同时修改成功,对于此类现象,需要借助两阶段提交、三阶段提交或google paxos来解决。

自增ID的问题通常要改为由程序来生成ID的方式解决。在解决了以上问题后,即可通过水平伸缩来提升数据库的写速度。

提升计算能力

在单机计算的情况下,即使进行垂直伸缩,PC Server的计算能力也是无法和小型机抗衡的,因此要有合理的设计来通过增加PC Server提升系统的计算能力。在垂直伸缩场景中采取的方法是将计算任务拆分,多线程并行计算然后汇总结果。在水平伸缩场景中则可演变为将计算任务拆分,然后分派给不同的机器进行计算,最后汇总。它复杂的地方在于拆分的方法、拆分后的分派调度及机器执行失败时的处理,在Java中主要可采用的方法如MapReduce,MapReduce=的方式都支持将任务拆分后分解到多台机器上执行。在计算任务不变的情况下,增加机器可使得每台机器上执行的计算任务减少,从而提高速度;倘若计算任务增加,增加机器可让每台机器需要执行的计算任务不变,从而保持系统的计算能力。

构建可伸缩系统:垂直伸缩、水平伸缩、应用无状态、一致性哈希、分布式文件系统、分布式计算框架、应用垂直拆分、数据库读写分离-《分布式Java应用》读书笔记相关推荐

  1. 使用Mycat构建MySQL读写分离、主从复制、主从高可用

    数据库读写分离对于大型系统或者访问量很高的互联网应用来说,是必不可少的一个重要功能. 从数据库的角度来说,对于大多数应用来说,从集中到分布,最基本的一个需求不是数据存储的瓶颈,而是在于计算的瓶颈,即S ...

  2. 架构垂直伸缩和水平伸缩区别_简单的可伸缩图神经网络

    架构垂直伸缩和水平伸缩区别 巨型图上的深度学习 (Deep learning on giant graphs) TL;DR: One of the challenges that have so fa ...

  3. 从RAID看垂直伸缩到水平伸缩的演化

    learn from 从0开始学大数据(极客时间) 大规模数据存储问题: 容量问题,数据量超过磁盘容量 读写速度,磁盘读写慢 数据可靠性,磁盘寿命问题 RAID(独立磁盘冗余阵列) 是将多块普通磁盘组 ...

  4. k8s HPA(HorizontalPodAutoscaler)-自动水平伸缩

    Horizontal Pod Autoscaling in Kubernetes 写在前面 我们平时部署web服务,当服务压力大撑不住的时候,我们会加机器(加钱):一般没有上容器编排是手动加的,临时加 ...

  5. k8s Pod的自动水平伸缩(HPA)

    HPA(Horizontal Pod Autoscaler ) pod的自动水平伸缩 有了HPA,我们就不用为上面的问题而烦恼,HPA会帮我们自动完成pod的扩缩容. 当资源需求过高时,会自动创建出p ...

  6. HPA 自动水平伸缩 POD

    ​ 前戏 我们知道,初始Pod的数量是可以设置的,同时业务也分流量高峰和低峰,那么怎么即能不过多的占用K8s的资源,又能在服务高峰时自动扩容pod的数量呢,在K8s上的答案是Horizontal Po ...

  7. [转]可伸缩系统的架构经验

    最近,阅读了Will Larson的文章Introduction to Architecting System for Scale,感觉很有价值.作者分享了他在Yahoo!与Digg收获的设计可伸缩系 ...

  8. 9个用于构建容错系统的开源工具

    我一直对 Web 开发和软件架构很感兴趣,因为我希望对一个可以工作的系统有一个整体的了解.无论你正在构建移动应用程序还是 Web 应用程序,它都必须连接到互联网,以便在不同的模块之间交换数据,这意味着 ...

  9. 开源分布式数据库中间件MyCat架构简介(一)——基于MyCat的分库分表,读写分离,水平切分和垂直切分实现原理

    目录 前言 开源分布式数据库中间件MyCat架构简介--MyCat源起 一.数据库切分概述:OLTP和OLAP 二.关系型数据库和NoSQL数据库 三.关系型数据库和NoSQL数据库的特点及优缺点 1 ...

  10. 使用Python、OpenCV翻转图像(水平、垂直、水平垂直翻转)

    使用Python.OpenCV翻转图像(水平.垂直.水平垂直翻转) 1. 效果图 2. 源码 参考 这篇博客将介绍如何使用Python.OpenCV翻转图像,类似于cv2.rotate(). 沿y轴水 ...

最新文章

  1. ASP.NET实现身份模拟
  2. 图论500 ---- HDU3631 Shortest Path Floyed 插点法 真正了解Floyed
  3. Shovels Shop
  4. 机器学习(二十九)——Temporal-Difference Learning
  5. 引起Java序列化失败的常见原因
  6. python中带*(单星号)的变量和**(双星号)的变量
  7. 基于centos5.8源码安装nginx之LNMP
  8. C++描述杭电OJ 2015.偶数求和 ||
  9. 浙江科技学院c语言考试试卷,浙江科技学院c语言C试卷A.doc
  10. mac mysql安装失败_Mac mysql安装失败解决方法
  11. 和preload_通过LD_PRELOAD绕过disable_functions
  12. 职业高中计算机网络试讲稿,《初识我的电脑》试讲稿+答辩
  13. Python入门--局部变量,全局变量,作用域,LEGB规则
  14. (转)UIWebView的基本用法,适合新手
  15. 瑞友天翼服务器ip地址怎么修改,瑞友天翼6.0版本iphone移动客户端操作手册
  16. 芯片测试服务器,检测服务器硬件软件
  17. recv( )函数返回值说明
  18. USYD悉尼大学DATA 2002 【Module 1】: Categorical data 学习笔记(week1-week3)
  19. 离散数学——图论中图的同构的应用
  20. 论如何使用Python进行微信公众号的开发

热门文章

  1. 编程序找出1000之内的所有完数
  2. 研究开源的C++的RTB广告系统,通过centos7镜像,解决各种环境问题,使用boost库
  3. java virt res_top命令里内存参数 VIRT, RES 和 SHR 分别是什么意思
  4. BLE安全之SM剖析(3)
  5. markdown文件怎么转换成html,将markdown文件转换为html文件(MarkdownPad)
  6. Snappy Installer
  7. linux查询数据库归档日志,关于 Oracle 归档日志
  8. BH1750 传感器实战教学 —— 硬件设计篇
  9. CRM客户细分的价值
  10. 高职计算机基础教案ppt,高职高专计算机基础幻灯片.ppt