要资料 文章 文库 视频 Code iProcess 课程 认证 咨询 工具 讲座吧   专家招募  
会员   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
 
MySQL性能调优与架构设计-架构篇
 

2011-04-06 来源:网络

 

架构篇(1) 读书笔记

1.Scale(扩展):从数据库来看,就是让数据库能够提供更强的服务能力

ScaleOut: 是通过增加处理节点的方式来提高整体处理能力

ScaleUp: 是通过增加当前处理节点的处理能力来提高整体的处理能力

2.事务最小化原则:

避免分布式事务的解决方案

a)进行ScaleOut 设计的时候合理设计切分规则,尽可能保证事务所需数据在同一个MySQLServer 上,避免分布式事务。大多数时候也只能兼顾到一些大部分的核心事务,不是一个很完美的解决方案。

b)大事务切分成多个小事务,数据库保证各个小事务的完整性,应用控制各个小事务之间的整体事务完整性。

c)结合上述两种解决方案,整合各自的优势,避免各自的弊端。核心业务的事务用a)方案保证,其他的用b)保证,要仔细分析,是否需要事务,如果不需要的话,就不要引入事务.

3.数据一致性原则.

如何在ScaleOut 的同时又较好的保证数据一致性呢?

==>BASE模型,即:基本可用,柔性状态,基本一致和最终一致。这几个词看着挺复杂挺深奥,其实大家可以简单的理解为非实时的一致性原则。也就是说,应用系统通过相关的技术实现,让整个系统在满足用户使用的基础上,允许数据短时间内处于非实时状态,而通过后续技术来保证数据在最终保证处于一致状态。

但也有问题:

第一个问题就是我们需要让所有数据都是非实时一致吗?

==>如果不是所有的数据都是非实时一致,那我们又该如何来确定哪些数据需要实时一致哪些数据又只需要非实时的最终一致呢?其实这基本可以说是一个各模块业务优先级的划分,对于优先级高的自然是规属于保证数据实时一致性的阵营,而优先级略低的应用,则可以考虑划分到允许短时间端内不一致而最终一致的阵营。这是一个非常棘手的问题。需要通过非常详细的分析和仔细的评估才能作出决定。因为不是所有数据都可以出现在系统能不短时间段内不一致状态,也不是所有数据都可以通过后期处理的使数据最终达到一致的状态,所以之少这两类数据就是需要实时一致的。

如何让系统中的不一致数据达到最终一致?

==>一般来说,我们必须将这类数据所设计到的业务模块和需要实时一致数据的业务模块明确的划分开来。然后通过相关的异步机制技术,利用相应的后台进程,通过系统中的数据,日志等信息将当前并不一致的数据进行进一步处理,使最终数据处于完全一致状态。对于不同的模块,使用不同的后台进程,既可以避免数据出现紊乱,也可以并发执行,提高处理效率。

避免实时一致与最终一致两类数据的前台在线交互。

==>由于两类数据状态的不一致性,很可能会导致两类数据在交互过程中出现紊乱,应该尽量让所有非实时一致的数据和实时一致数据在应用程序中得到有效的隔离。甚至在有些特别的场景下,记录在不同的MySQLServer中来进行物理隔离都是有必要的。

4.高可用以及数据安全原则:

经过ScaleOut设计之后,系统整体可扩展性确实是会得到很大的提高,整体性能自然也很容易得到较大的改善。但是,系统整体的可用性维护方面却是变得比以前更为困难。因为系统整体架构复杂了,不论是应用程序还是数据库环境方面都会比原来更为庞大,更为复杂。这样所带来的最直接影响就是维护难度更大,系统监控更难。

ScaleOut 设计过程中另一个原则,也就是高可用性的原则。不论如何调整设计系统的架构,系统的整体可用性不能被降低。

数据安全:

==>我们必须保证在出现软/硬件故障的时候,能够保证我们的数据不会出现丢失。数据一旦丢失,根本就无可用性可言了。

==>最好的办法就是通过冗余机制来保证。所有软硬件设备都去除单点隐患,所有数据都存在多份拷贝。可以通过MySQLReplication,MySQLCluster 等技术来实现。

架构篇(2) 读书笔记

mysqlreplication:

原理:

Mysql的Replication是一个异步的复制过程,在Master与Slave之间的实现整个复制过程主要由三个线程来完成,其中两个线程(Sql线程和IO线程)在Slave端,另外一个线程(IO线程)在Master端。

要实现MySQL的Replication,首先必须打开Master端的BinaryLog(mysqlbin.xxxxxx)功能,否则无法实现。因为整个复制过程实际上就是Slave从Master端获取该日志然后再在自己身上完全顺序的执行日志中所记录的各种操作。

复制的过程:

1.Slave 上面的IO线程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;

2.Master 接收到来自Slave的IO线程的请求后,通过负责复制的IO线程根据请求信息读取指定日志指定位置之后的日志信息,返回给Slave端的IO线程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息在Master端的BinaryLog文件的名称以及在BinaryLog 中的位置;

3.Slave 的IO线程接收到信息后,将接收到的日志内容依次写入到Slave端的RelayLog 文件(mysql-relay-bin.xxxxxx)的最末端,并将读取到的Master端的binlog的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的高速Master“我需要从某个bin-log的哪个位置开始往后的日志内容,请发给我”

4.Slave 的SQL线程检测到RelayLog 中新增加了内容后,会马上解析该Log文件中的内容成为在Master端真实执行时候的那些可执行的Query语句,并在自身执行这些Query。这样,实际上就是在Master端和Slave端执行了同样的Query,所以两端的数据是完全一样的。

复制的级别:可以基于语句的/也可以基于一条记录的

记录级别:为每一行都生成sql,信息量大.

语句级别:性能高.但是bug多.尽量少使用存储过程.

常用复制架构:

Master-Slaves:

90%的场合都是这种一个master,多个slave的架构模式.主要用于读压力比较大的应用.对于对于数据实时性要求不是太高的系统,只要通过廉价的pcserver就可以扩展slave的数量.将读压力分散到多台slave机器上.架构图:

w repl r

client---> master -----> slave <----client

|---> salve <----client

|---> salve <----client

dualmaster 复制架构:

为了解决主机down机,从机迅速切换成主机的架构方式.

实际上就是两个MySQLServer 互相将对方作为自己的Master,自己作为对方的Slave来进行复制。这样,任何一方所做的变更,都会通过复制应用到另外一方的数据库中。

不会造成循环复制:在MySQL的BinaryLog 中记录了当前MySQL的server-id,而且这个参数也是我们搭建MySQLReplication 的时候必须明确指定,而且Master和Slave的server-id参数值比需要不一致才能使MySQLReplication搭建成功

r/w

client------> master

|

| REPL (相互)

|

r/w

client------> master

通过DualMaster 复制架构,我们不仅能够避免因为正常的常规维护操作需要的停机所带来的重新搭建Replication环境的操作.Dual Master 复制架构和一些第三方的HA管理软件结合,还可以在我们当前正在使用的Master出现异常无法提供服务之后,非常迅速的自动切换另外一端来提供相应的服务,减少异常情况下带来的停机时间,并且完全不需要人工干预。

搭建成一个DualMaster环境,并不是为了让两端都提供写的服务。在正常情况下,我们都只会将其中一端开启写服务,另外一端仅仅只是提供读服务,或者完全不提供任何服务,仅仅只是作为一个备用的机器存在。

级联复制架构(Master- Slaves - Slaves:

在某些场合读的压力特别大,一个master可能需要10台或者更多的slave才鞥支撑住读的压力.这样的话,master的压力比较大,因为光是slave的io线程就比较多,这样写的压力稍微大一点,就容易造成复制的延时.

解决==>

以利用MySQL可以在Slave端记录复制所产生变更的BinaryLog 信息的功能,也就是打开—log-slave-update选项。然后,通过二级(或者是更多级别)复制来减少Master端因为复制所带来的压力。

w repl repl

client--->master -----> slave -----------> slave

| repl

|-->slave ----------> slave

|repl

|------> slave

所有的slave都是对客户只读

风险:级联过多,容易产生延时较长.

dualmaster 与级联复制结合:

w repl repl

client--->master <------> master ----------> slave

|repl

|-------->slave

|repl

|-------->slave

最大的好处就是既可以避免主Master的写入操作不会受到Slave集群的复制所带来的影响,同时主Master需要切换的时候也基本上不会出现重搭Replication的情况.

搭建实现:

1.Master端准备工作

在搭建Replication环境之前,首先要保证Master端MySQL记录BinaryLog 的选项打开.使用log-bin[=pathfor binary log]参数选项。

还需要准备一个用于复制的MySQL用户。

mysql>CREATEUSER 'repl'@'192.168.0.2'

->IDENTIFIED BY 'password';

mysql>GRANTREPLICATION SLAVE ON *.*

->TO 'repl'@'192.168.0.2';

2.获取Master端的备份“快照”

快照:所有数据均是基于某一特定时刻的,数据完整性和一致性都可以得到保证的备份集.同时还需要取得该备份集时刻所对应的Master端BinaryLog 的准确LogPosition,因为在后面配置Slave的时候会用到

方法:

a)通过数据库全库冷备份:

冷备份.

如在Master刚刚启动之后,还没有应用程序连接上Master之前,通过执行SHOWMaster STATUS 命令从Master端获取到我们可以使用的LogPosition。如果我们无法在Master启动之后控制应用程序的连接,那么可能在我们还没有来得及执行SHOWMaster STATUS 命令之前就已经有数据写进来了,这时候我们可以通过mysqlbinlog客户端程序分析Master最新的一个BinaryLog 来获取其第一个有效的LogPosition。

b)通过LVM或者ZFS等具有snapshot功能的软件进行"热备份"

文件系统运行在LVM上面,那么我们都可以通过相关的命令对MySQL的数据文件和日志文件所在的目录就做一个Snapshot,这样就可以得到了一个基本和全库冷备差不多的备份集。为了保证我们的备份集数据能够完整且一致,我们需要在进行Snapshot过程中通过相关命令(FLUSHTABLES WITH READ LOCK)来锁住所有表的写操作,在做完Snapshot之后,我们就可以UNLOCKTABLES 了

因为加了锁,所以更容易获得logposition : SHOW MASTER STATUS

c)mysqldump 客户端程序

如果不能停机冷备份,而且也没有运行在b)上的文件系统,就需要使用mysqldump客户端程序.

可以锁定表(不支持事务,FLUSH TABLES WITH READ LOCK), 或者—single-transaction选项(支持事务)来保持数据的完整性.

获得logposition: 使用mysqldump的 --master-data

d)通过现有某一个Slave端进行“热备份”

如果现在已经有Slave从我们需要搭建Replication环境的Master上进行复制的话,那我们这个备份集就非常容易取得了。。我们可以暂时性的停掉现有Slave(如果有多台则仅仅只需要停止其中的一台).同时执行一次FLUSHTABLES 命令来刷新所有表和索引的数据。这时候在该Slave上面就不会再有任何的写入操作了,我们既可以通过copy所有的数据文件和日志文件来做一个全备份,同时也可以通过Snapshot(如果支持)来进行备份。

通过现有Slave来获取备份集的方式,不仅仅得到数据库备份的方式很简单,连所需要LogPosition,甚至是新Slave后期的配置等相关动作都可以省略掉,只需要新的Slave完全基于这个备份集来启动,就可以正常从Master进行复制了。

整个过程中我们仅仅只是在短暂时间内停止了某台现有Slave的复制线程,对系统的正常服务影响很小,所以这种方式也基本可以称之为“热备份”。

3.Slave端恢复"快照"

a)恢复全库冷备份集

由于这个备份集是一个完整的数据库物理备份,我们仅仅只需要将这个备份集通过FTP或者是SCP之类的网络传输软件复制到Slave所在的主机,根据Slave上my.cnf配置文件的设置,将文件存放在相应的目录,覆盖现有所有的数据和日志等相关文件,然后再启动Slave端的MySQL,就完成了整个恢复过程。

b)恢复对Master进行Snapshot得到的备份集

对于通过对Master进行Snapshot所得到的备份集,实际上和全库冷备的恢复方法基本一样,唯一的差别只是首先需要将该Snapshot通过相应的文件系统mount到某个目录下,然后才能进行后续的文件拷贝操作。之后的相关操作和恢复全库冷备份集基本一致,就不再累述。

c)恢复mysqldump得到的备份集

通过mysqldump客户端程序所得到的备份集,和前面两种备份集的恢复方式有较大的差别。因为前面两种备份集的都属于物理备份,而通过mysqldump客户端程序所做的备份属于逻辑备份。恢复mysqldump备份集的方式是通过mysql客户端程序来执行备份文件中的所有SQL语句。

恢复之前,注销掉CHANGEMASTER TO 命令部分,

d)恢复通过现有Slave所得到的热备份

通过现有Slave所得到的备份集和上面第一种或者第二种备份集也差不多。如果是通过直接拷贝数据和日志文件所得到的备份集,那么就和全库冷备一样的备份方式,如果是通过Snapshot得到的备份集,就和第二种备份恢复方式完全一致。

4.配置并启动Slave:

通过CHANGEMASTER TO 命令来配置然后再启动Slave

root@localhost: mysql 08:32:38> CHANGE MASTER TO

->MASTER_HOST='192.168.0.1',

->MASTER_USER='repl',

->MASTER_PASSWORD='password',

->MASTER_LOG_FILE='mysql-bin.000035',

->MASTER_LOG_POS=399;

root@localhost: mysql 08:33:49> START SLAVE;

成功!

架构篇(3) 读书笔记

replication的限制:

一旦数据库过于庞大,尤其是当写入过于频繁,很难由一台主机支撑的时候,我们还是会面临到扩展瓶颈。

数据切分(sharding):通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)上面,以达到分散单台设备负载的效果。。数据的切分同时还可以提高系统的总体可用性,因为单台设备Crash之后,只有总体数据的某部分不可用,而不是所有的数据。

数据的切分(Sharding)模式:

一种是按照不同的表(或者Schema)来切分到不同的数据库(主机)之上,这种切可以称之为数据的垂直(纵向)切分;

另外一种则是根据表中的数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上面,这种切分称之为数据的水平(横向)切分。

垂直切分:

一个架构设计较好的应用系统,其总体功能肯定是由很多个功能模块所组成的,而每一个功能模块所需要的数据对应到数据库中就是一个或者多个表。而在架构设计中,各个功能模块相互之间的交互点越统一越少,系统的耦合度就越低,系统各个模块的维护性以及扩展性也就越好。这样的系统,实现数据的垂直切分也就越容易。

一般来说,如果是一个负载相对不是很大的系统,而且表关联又非常的频繁,那可能数据库让步,将几个相关模块合并在一起减少应用程序的工作的方案可以减少较多的工作量,这是一个可行的方案。

一个垂直拆分的例子:

1.用户模块表:user,user_profile,user_group,user_photo_album

2.群组讨论表:groups,group_message,group_message_content,top_message

3.相册相关表:photo,photo_album,photo_album_relation,photo_comment

4.事件信息表:event

拆分:

◆群组讨论模块和用户模块之间主要存在通过用户或者是群组关系来进行关联。一般关联的时候都会是通过用户的id或者nick_name以及group的id来进行关联,通过模块之间的接口实现不会带来太多麻烦;

◆相册模块仅仅与用户模块存在通过用户的关联。这两个模块之间的关联基本就有通过用户id关联的内容,简单清晰,接口明确;

◆ 事件模块与各个模块可能都有关联,但是都只关注其各个模块中对象的ID信息,同样可以做到很容易分拆。

app====> [users]database

====>[group message]database

====>[photto albums]database

====>[events]database

所以,通过拆分,把以前的一个db存储这些表,分成了4个db写入,这样就减轻了压力.

垂直切分的优点

◆ 数据库的拆分简单明了,拆分规则明确;

◆ 应用程序模块清晰明确,整合容易;

◆ 数据维护方便易行,容易定位;

垂直切分的缺点

◆ 部分表关联无法在数据库级别完成,需要在程序中完成;

◆ 对于访问极其频繁且数据量超大的表仍然存在性能瓶颈,不一定能满足要求;

◆ 事务处理相对更为复杂;

◆ 切分达到一定程度之后,扩展性会遇到限制;

◆ 过读切分可能会带来系统过渡复杂而难以维护。

水平切分

将某个访问极其频繁的表再按照某个字段的某种规则来分散到多个表之中,每个表中包含一部分数据。

对于上面的例子:

所有数据都是和用户关联的,那么我们就可以根据用户来进行水平拆分,将不同用户的数据切分到不同的数据库中。

现在互联网非常火爆的Web2.0类型的网站,基本上大部分数据都能够通过会员用户信息关联上,可能很多核心表都非常适合通过会员ID来进行数据的水平切分。而像论坛社区讨论系统,就更容易切分了,非常容易按照论坛编号来进行数据的水平切分。切分之后基本上不会出现各个库之间的交互。

水平切分的优点

◆ 表关联基本能够在数据库端全部完成;

◆ 不会存在某些超大型数据量和高负载的表遇到瓶颈的问题;

◆ 应用程序端整体架构改动相对较少;

◆ 事务处理相对简单;

◆ 只要切分规则能够定义好,基本上较难遇到扩展性限制;

水平切分的缺点

◆ 切分规则相对更为复杂,很难抽象出一个能够满足整个数据库的切分规则;

◆ 后期数据的维护难度有所增加,人为手工定位数据更困难;

◆ 应用系统各模块耦合度较高,可能会对后面数据的迁移拆分造成一定的困难。

两种切分结合用:

一般来说,我们数据库中的所有表很难通过某一个(或少数几个)字段全部关联起来,所以很难简单的仅仅通过数据的水平切分来解决所有问题。而垂直切分也只能解决部分问题,对于那些负载非常高的系统,即使仅仅只是单个表都无法通过单台数据库主机来承担其负载。我们必须结合“垂直”和“水平”两种切分方式同时使用

每一个应用系统的负载都是一步一步增长上来的,在开始遇到性能瓶颈的时候,大多数架构师和DBA都会选择先进行数据的垂直拆分,因为这样的成本最先,最符合这个时期所追求的最大投入产出比。然而,随着业务的不断扩张,系统负载的持续增长,在系统稳定一段时期之后,经过了垂直拆分之后的数据库集群可能又再一次不堪重负,遇到了性能瓶颈。

==>如果我们再一次像最开始那样继续细分模块,进行数据的垂直切分,那我们可能在不久的将来,又会遇到现在所面对的同样的问题。而且随着模块的不断的细化,应用系统的架构也会越来越复杂,整个系统很可能会出现失控的局面。

==>这时候我们就必须要通过数据的水平切分的优势,来解决这里所遇到的问题。而且,我们完全不必要在使用数据水平切分的时候,推倒之前进行数据垂直切分的成果,而是在其基础上利用水平切分的优势来避开垂直切分的弊端,解决系统复杂性不断扩大的问题。而水平拆分的弊端(规则难以统一)也已经被之前的垂直切分解决掉了,让水平拆分可以进行的得心应手。

示例数据库:

假设在最开始,我们进行了数据的垂直切分,然而随着业务的不断增长,数据库系统遇到了瓶颈,我们选择重构数据库集群的架构。如何重构?考虑到之前已经做好了数据的垂直切分,而且模块结构清晰明确。而业务增长的势头越来越猛,即使现在进一步再次拆分模块,也坚持不了太久。

==>选择了在垂直切分的基础上再进行水平拆分。

==>在经历过垂直拆分后的各个数据库集群中的每一个都只有一个功能模块,而每个功能模块中的所有表基本上都会与某个字段进行关联。如用户模块全部都可以通过用户ID进行切分,群组讨论模块则都通过群组ID来切分,相册模块则根据相册ID来进切分,最后的事件通知信息表考虑到数据的时限性(仅仅只会访问最近某个事件段的信息),则考虑按时间来切分。

数据切分以及整合方案.

数据库中的数据在经过垂直和(或)水平切分被存放在不同的数据库主机之后,应用系统面临的最大问题就是如何来让这些数据源得到较好的整合

存在两种解决思路:

1.在每个应用程序模块中配置管理自己需要的一个(或者多个)数据源,直接访问各个数据库,在模块内完成数据的整合;

2.通过中间代理层来统一管理所有的数据源,后端数据库集群对前端应用程序透明;

第二种方案,虽然短期内需要付出的成本可能会相对更大一些,但是对整个系统的扩展性来说,是非常有帮助的。

针对第二种方案:

1.利用MySQLProxy 实现数据切分及整合.

可用来监视、分析或者传输他们之间的通讯信息。他的灵活性允许你最大限度的使用它,目前具备的功能主要有连接路由,Query分析,Query过滤和修改,负载均衡,以及基本的HA机制等。

MySQLProxy 本身并不具有上述所有的这些功能,而是提供了实现上述功能的基础。要实现这些功能,还需要通过我们自行编写LUA脚本来实现。

原理:

MySQLProxy 实际上是在客户端请求与MySQLServer 之间建立了一个连接池。所有客户端请求都是发向MySQLProxy,然后经由MySQLProxy 进行相应的分析,判断出是读操作还是写操作,分发至对应的MySQLServer 上。对于多节点Slave集群,也可以起做到负载均衡的效果。

2.利用Amoeba实现数据切分及整合

Amoeba是一个基于Java开发的,专注于解决分布式数据库数据源整合Proxy程序的开源框架

Amoeba已经具有Query路由,Query过滤,读写分离,负载均衡以及HA机制等相关内容。

Amoeba主要解决的以下几个问题:

a)数据切分后复杂数据源整合;

b)提供数据切分规则并降低数据切分规则给数据库带来的影响;

c)降低数据库与客户端的连接数;

d)读写分离路由;

AmoebaFor MySQL 主要是专门针对MySQL数据库的解决方案,前端应用程序请求的协议以及后端连接的数据源数据库都必须是MySQL。对于客户端的任何应用程序来说,AmoebaForMySQL 和一个MySQL数据库没有什么区别,任何使用MySQL协议的客户端请求,都可以被AmoebaFor MySQL 解析并进行相应的处理。

AmoebaFor MySQL 的使用非常简单,所有的配置文件都是标准的XML文件,总共有四个配置文件。分别为:

◆ amoeba.xml:主配置文件,配置所有数据源以及Amoeba自身的参数设置;

◆ rule.xml:配置所有Query路由规则的信息;

◆ functionMap.xml:配置用于解析Query中的函数所对应的Java实现类;

◆rullFunctionMap.xml:配置路由规则中需要使用到的特定函数的实现类;

Proxy程序常用的功能如读写分离,负载均衡等配置都在amoeba.xml中进行。Amoeba已经支持了实现数据的垂直切分和水平切分的自动路由,路由规则可以在rule.xml进行设置。

3.利用HiveDB实现数据切分及整合

HiveDB同样是一个基于Java针对MySQL数据库的提供数据切分及整合的开源框架,只是目前的HiveDB仅仅支持数据的水平切分。主要解决大数据量下数据库的扩展性及数据的高性能访问问题,同时支持数据的冗余及基本的HA机制。

HiveDB的实现机制与MySQLProxy 和Amoeba有一定的差异,他并不是借助MySQL的Replication功能来实现数据的冗余,而是自行实现了数据冗余机制,而其底层主要是基于HibernateShards 来实现的数据切分工作。

数据切分与整合中可能存在的问题

◆ 引入分布式事务的问题;

◆ 跨节点Join的问题;

◆ 跨节点合并排序分页问题;

引入分布式事务的问题?

一旦数据进行切分被分别存放在多个MySQLServer中之后,不管我们的切分规则设计的多么的完美(实际上并不存在完美的切分规则),都可能造成之前的某些事务所涉及到的数据已经不在同一个MySQLServer 中了。

==>将一个跨多个数据库的分布式事务分拆成多个仅处于单个数据库上面的小事务,并通过应用程序来总控各个小事务。

跨节点Join的问题?

==>先从一个节点取出数据,然后根据这些数据,再到另一个表中取数据.

==>使用Federated存储引擎,问题是:乎如果远端的表结构发生了变更,本地的表定义信息是不会跟着发生相应变化的。

跨节点合并排序分页问题?

==>Join本身涉及到的多个表之间的数据读取一般都会存在一个顺序关系。但是排序分页就不太一样了,排序分页的数据源基本上可以说是一个表(或者一个结果集),本身并不存在一个顺序关系,所以在从多个数据源取数据的过程是完全可以并行的。这样,排序分页数据的取数效率我们可以做的比跨库Join更高,所以带来的性能损失相对的要更小。

架构篇(4) 读书笔记

分布式内存Cache软件Memcached:

1.作为提升系统性能的Cache工具:

如果我们将Memcached作为应用系统的一个数据Cache服务,那么对于MySQL数据库来说基本上不用做任何改造,仅仅通过应用程序自己来对这个Cache进行维护更新。这样作最大的好处就在于可以做到完全不用动数据库相关的架构,但是同时也会有一个弊端,那就是如果需要Cache的数据对象较多的时候,应用程序所需要增加的代码量就会增加很多,同时系统复杂度以及维护成本也会直线上升。

架构图:

appserver ---> ds proxy layer ---> db master --> db slave1

| |--> db slave2

|

|---> memcached1

|---> memcached2

整体来看:

所有数据都会写入MySQLMaster 中,包括数据第一次写入时候的INSERT,同时也包括对已有数据的UPDATE和DELETE。

如果是对已经存在的数据,则需要在UPDATE或者DELETEMySQL 中数据的同时,删除Memcached中的数据,以此保证整体数据的一致性。

所有的读请求首先会发往Memcached中,如果读取到数据则直接返回,如果没有读取到数据,则再到MySQLSlaves 中读取数据,并将读取得到的数据写入到Memcached中进行Cache。

这种使用方式一般来说比较适用于需要缓存对象类型少,而需要缓存的数据量又比较大的环境,是一个快速有效的完全针对性能问题的解决方案。

2.和MySQL整合为数据服务层

有两种方式将Memcached和MySQL数据库整合成一个整体来对外提供数据服务:

直接利用Memcached的内存容量作为MySQL数据库的二级缓存,提升MySQLServer的缓存大小,

通过MySQL的UDF来和Memcached进行数据通信,维护和更新Memcached中的数据,而应用端则直接通过Memcached来读取数据。

第一种方式,主要用于业务要求非常特殊,实在难以进行数据切分,而且有很难通过对应用程序进行改造利用上数据库之外的Cache的场景。

==>通过开源项目WaffleGrid实现,将Memcached成功实现成为MySQL主机的外部“二级缓存”,目前仅支持用于Innodb的BufferPool。

架构图:

appserver ---> ds proxy layer ---->Waffle Grid --->memcached1

|---> memcached2

|---> memcached3

|---> memcached4

|---> memcached5

这里面所有的memcached都是innodb的外部bufferpool, 而memcached和mysql之间一定要使用具有高带宽的私有网络.

第二种方案:

是通过MySQL所提供的UDF功能,自行编写相应的程序来实现MySQL与Memcached的数据通信更新操作。

原理:

这种方式和WaffleGrid 不一样的是Memcached中的数据并不完全由MySQL来控制维护,而是由应用程序和MySQL一起来维护数据。每次应用程序从Memcached读取数据的时候,如果发现找不到自己需要的数据,则再转为从数据库中读取数据,然后将读取到的数据写入Memcached中。而MySQL则控制Memcached中数据的失效清理工作,每次数据库中有数据被更新或者被删除的时候,MySQL则通过用户自行编写的UDF(user define unction) 来调用Memcached的API来通知Memcached某些数据已经失效并删除该数据。

对于使用Memcached等感到成本高,可以考虑使用BerkeleyDB, TokyoTyrant

使用Search:

使用搜索引擎来提供全文检索.主要是基于Lucene.

把数据库的数据通过应用程序调用Lucene的相关API写入,并利用Lucene创建好索引,然后就可以通过调用Lucene所提供的数据检索API得到需要访问的数据,而且可以进行全模糊匹配

虽然Lucene的数据也是存放在磁盘上而不是内存中,但是由于高效的分词算法和索引结构,其效率也是非常的好。。看到很多网友在网上讨论,当数据量稍微大一些如几十个G之后Lucene的效率会下降的非常快,其实这是不科学的说法,就从我亲眼所见的场景中,就有好几百G的数据在Lucene中,性能仍然很出色。这几年性能优化的工作经历及经验中我有一个很深的体会,那就是一个软件性能的好坏,实际上并不仅仅只由其本身所决定,很多时候一个非常高效的软件不同的人使用会有截然不同效果。所以,很多时候当我们使用的第三方软件性能出现问题的时候,不要急着下结论认为是这个软件的问题,更多的是先从自身找找看我们是否真的正确使用了他。

除了使用第三方的Search软件如Lucene之外,我们也可以自行研发更适用于我们自身应用场景的Search软件。比如:自行研发了一套纯内存存储的更适合于自身应用场景的高性能分布式Search软件

自行实现Cache服务:

如果目前的第三方软件已经基本解决了我们系统当前遇到的80%以上的问题,可能就需要考虑是否有必要完全自主研发了。

利用分布式用分布式并行计算实现大数据量的高性能运算:

MapReduce(任务分解和任务合并功能)+ HDFS(分布式文件系统)+ Hbase(高性能的分布式数据库)

架构篇(5) 读书笔记

任何设备(或服务),只要是单点,就存在着很大的安全隐患。因为一旦这台设备(或服务)crash之后,在短时间内就很难有备用设备(或服务)来顶替其功能。所以稍微重要一些的服务器或者应用系统,都会存在至少一个备份以供出现异常的时候能够很快的顶替上来提供服务。

对于数据库来说,主备配置是非常常见的设计思路。而对于MySQL来说,其Replication功能在实际应用中被广泛的用来实现主备配置的功能。

常规的Master- Slave 解决基本的主备设计:

在普通的一个Master后面复制一个或者多个Slave的架构设计中,当我们的某一台Slave出现故障不能提供服务之后,我们还有至少一台MySQL服务器(Master)可以提供服务,不至于所有和数据库相关的业务都不能运行下去。如果Slave超过一台,那么剩下的Slave也仍然能够不受任何干扰的继续提供服务。

这种架构方式很容易解决Slave出现故障的情况,而且不需要进行任何调整就能继续提供服务。

Master单点问题的解决:

两种解决方案:

a)将Slave中的某一台切换成Master对外提供服务,同时将其他所有的Slave都以通过CHANGEMASTER 命令来将通过新的Master进行复制。

b)新增一台Master,也就是DualMaster 的解决方案。

c)方案最大的一个弊端就是切换步骤比较多,实现比较复杂。而且,在Master出现故障crash的那个时刻,我们的所有Slave的复制进度并不一定完全一致,有可能有少量的差异。这时候,选择哪一个Slave作为Master也是一个比较头疼的问题。所以这个方案的可控性并不是特别的高。

b)方案实际上就是通过DualMaster 来解决Master单点问题

通过两台MySQLServer 搭建成DualMaster 环境,正常情况下,所有客户端的Write请求都写往MasterA,然后通过Replication将MasterA 复制到MasterB。一旦MasterA 出现问题之后,所有的Write请求都转向MasterB。而在正常情况下,当MasterB 出现问题的时候,实际上不论是数据库还是客户端的请求,都不会受到实质性的影响。

当我们的MasterA 出现问题的时候,应用如何做到自动将请求转向到MasterB 呢?

==>只需要通过相应的硬件设备如F5或者Cluster管理软件如Heartbeat来设置一个VIP,正常情况下该VIP指向MasterA,而一旦MasterA 出现异常crash之后,则自动切换指向到MasterB,前端所的应用都通过这个VIP来访问Master。

DualMaster 与级联复制结合解决异常故障下的高可用:

通过前面的架构分析,我们分别得到了Slave出现故障后的解决方案,也解决了Master的单点问题。现在我们再通过DualMaster 与级联复制结合的架构,来得到一个整体的解决方案,解决系统整体可靠性的问题。

首先考虑Slave出现异常的情况。

在这个架构中,Slave出现异常后的处理情况和普通的Master- Slave 架构的处理方式完全一样,仅仅需要在应用访问Slave集群的访问配置中去掉一个Slave节点即可解决,不论是通过应用程序自己判断,还是通过硬件解决方案如F5都可以很容易的实现。

当MasterA 出现故障crash之后,MasterA 与MasterB 之间的复制将中断,所有客户端向MasterA 的Write请求都必须转向MasterB。这个转向动作的实现,可以通过上面介绍的第二中方案中所介绍的通过VIP的方式实现。由于之前所有的Slave就都是从MasterB 来实现复制,所以Slave集群不会受到任何的影响,客户端的所有Read请求也就不会受到任何的影响,整个过程可以完全自动进行,不需要任何的人为干预。不过这里有一个隐患就是当MasterA crash 的时候如果MasterB 作为Slave的IO线程如果还没有读取完MasterA 的二进制日志的话,就会出现数据丢失的问题。要完全解决这个问题,我们只能通过第三方patch(google开发)来镜像MySQL的二进制日志到MasterB上面,才能完全避免不丢失任何数据。

那么当MasterB 出现故障crash之后的情况又如何呢?

首先可以确定的是我们的所有Write请求都不会受到任何影响,而且所有的Read请求也都还是能够正常访问。但所有Slave的复制都会中断,Slave上面的数据会开始出现滞后的现象。这时候我们需要做的就是将所有的Slave进行CHANGEMASTER TO 操作,改为从MasterA 进行复制。由于所有Slave的复制都不可能超前最初的数据源,所以可以根据Slave上面的RelayLog中的时间戳信息与MasterA 中的时间戳信息进行对照来找到准确的复制起始点,不会造成任何的数据丢失。

DualMaster 与级联复制结合解决在线DDL变更问题:

使用DualMaster 加级联复制的组合架构的时候,对于MySQL的一个致命伤也就是在线DDL变更来说,也可以得到一定的解决。如当我们需要给某个表tab增加一个字段,可以通过如下在上述架构中来实现:

1、在Slave集群中抽出一台暂时停止提供服务,然后对其进行变更,完成后再放回集群继续提供服务;

2、重复第一步的操作完成所有Slave的变更;

3、暂停MasterB 的复制,同时关闭当前session记录二进制日志的功能,对其进行变更,完成后再启动复制;

4、通过VIP切换,将应用所有对MasterA 的请求切换至MasterB;

5、关闭MasterA 当前session记录二进制日志的功能,然后进行变更;

6、最后再将VIP从MasterB 切换回MasterA,至此,所有变更完成。

变更过程中有几点需要注意的:

1、整个Slave集群需要能够承受在少一台MySQL的时候仍然能够支撑所有业务;

2、Slave集群中增加或者减少一台MySQL的操作简单,可通过在线调整应用配置来实现;

3、DualMaster 之间的VIP切换简单,且切换时间较短,因为这个切换过程会造成短时间段内应用无法访问Master数据库。

4、在变更MasterB 的时候,会出现短时间段内Slave集群数据的延时,所以如果单台主机的变更时间较长的话,需要在业务量较低的凌晨进行变更。如果有必要,甚至可能需要变更MasterB 之前将所有Slave切换为以MasterB 作为Master。

使用DRBD保证数据的高可靠:

在MySQL的官方文档手册的HighAvailability and Scalability 这一章中将DRBD作为MySQL实现高可用性的一个非常重要的方式来介绍的。

DRBD其实就是通过网络来实现块设备的数据镜像同步的一款开源Cluster软件,也被俗称为网络RAID1。

DRBD介于文件系统与磁盘介质之间,通过捕获上层文件系统的所有IO操作,然后调用内核中的IO模块来读写底层的磁盘介质。当DRBD捕获到文件系统的写操作之后,会在进行本地的磁盘写操作的同时,以TCP/IP协议将,通过本地主机的网络设备(NIC)将IO传递至远程主机的网络设备。当远程主机的DRBD监听到传递过来的IO信息之后,会立即将该数据写入到该DRBD所维护的磁盘设备。至此,整个IO才做完成。

DRBD在处理远程数据写入的时候有三种复制模式(或者称为级别)可以选择,不同的复制模式保证了远程数据写入的三种可靠性。三种级别的选择可以通过DRBD的通用配置部分的protocal。不同的复制模式,实际上是影响了一个IO完成所代表的实际含义。因为当我们使用DRBD的时候,一个IO完成的标识(DRBD返回IO完成)是本地写入和远程写入这两个并发进程都返回完成标识。下面我来详细介绍一下这三种复制模式所代表的含义:

ProtocolA:这种模式是可靠性最低的模式,而且是一个异步的模式。当我们使用这个模式来配置的时候,写远程数据的进程将数据通过TCP/IP协议发送进入本地主机的TCPsendbuffer 中,即返回完成。

ProtocolB:这种模式相对于ProtocolA 来说,可靠性要更高一些。因为写入远程的线程会等待网络信息传输完成,也就是数据已经被远程的DRBD接受到之后返回完成。

ProtocolC:ProtocolC 复制模式是真正完全的同步复制模式,只有当远程的DRBD将数据完全写入磁盘成功后,才会返回完成。

其他高可用方案:

RaiDB:其全称为RedundantArrays of Inexpensive Databases。也就是通过Raid理念来管理数据库的数据

raiddb-0:

sqlrequest --> raidb controller --> table 1

--> table 2

--> table 3

raiddb-1:

sqlrequest --> raidb controller --> db full

--> db full

--> db full

raiddb-2:

sqlrequest --> raidb controller --> db full

--> table 1

--> table 2

raiddb-0-1:

sqlrequest --> raidb controller 0 --->raidb-1 controler -->table1

--> table1

--->raidb-1 controler --> table2

--> table2

--->raidb-1 controler --> table3

--> table3

raiddb-1-0:

sqlrequest --> raidb controller 1 --> raidb controller 0 -->table 1

--> table 2

--> table 3

--> raidb controller 0 --> table 1

--> table 2

--> table 3

--> raidb controller 0 --> table 1

--> table 2

--> table 3

方案比较:

1、MySQLReplication

优势:部署简单,实施方便,维护也不复杂,是MySQL天生就支持的功能。且主备机之间切换方便,可以通过第三方软件或者自行编写简单的脚本即可自动完成主备切换。

劣势:如果Master主机硬件故障且无法恢复,则可能造成部分未传送到Slave端的数据丢失;

2、MySQLCluster

优势:可用性非常高,性能非常好。每一分数据至少在不同主机上面存在一份拷贝,且冗余数据拷贝实时同步。

劣势:维护较为复杂,产品还比较新,存在部分bug,目前还不一定适用于比较核心的线上系统。

3、DRBD磁盘网络镜像方案

优势:软件功能强大,数据在底层快设备级别跨物理主机镜像,且可根据性能和可靠性要求配置不同级别的同步。IO操作保持顺序,可满足数据库对数据一致性的苛刻要求。

劣势:非分布式文件系统环境无法支持镜像数据同时可见,性能和可靠性两者相互矛盾,无法适用于性能和可靠性要求都比较苛刻的环境。维护成本高于MySQLReplication。

一个经过高可用可扩展设计的MySQL数据库集群,如果没有一个足够精细足够强大的监控系统,同样可能会让之前在高可用设计方面所做的努力功亏一篑。

MySQL分布式集群的监控系统整体架构体系:

mysqldb--->|

mysqldb--->| |-->报警

mysqldb--->|---->信息采集 --->信息存储 --->|-->状态

mysqldb--->| |-->趋势

mysqldb--->|

信息采集:

一般来说,较小规模的监控点可以采用轮询的方式主动采集数据,但是当监控点达到一定规模以后,轮询的主动采集方式可能就会遇到一定的性能瓶颈和信息延时问题,尤其是当需要采集的数据比较多的时候尤为突出。而如果要采用从各个MySQL节点进行被动的推送,则可能需要开发能够支持网络通信的监控程序,使采集的信息能够顺利的到达信息分析模块以即时得到分析,成本会稍微高一些。

不论是采用主动还是被动的方式来进行数据采集,我们都需要在监控主机上面部署采集相关信息的程序或脚本,包括主机信息和数据库信息。

主机健康状态监控:

●网络通信:网络通信基本上可以说是最容易检测的了,基本上只需要通过网络ping就可以获知是否正常。

●系统软硬件错误:系统软硬件错误,一般使用文本监控软件,如sec、logwatch等日志监控专用软件,通过配置相应的匹配规则,从日志文件中捕获满足条件的错误信息,再发送给信息分析模块。

● 磁盘空间:对于磁盘空间的使用状况监控,我们通过最简单的shell脚本就可以轻松搞定

●内存使用:系统物理内存使用量的信息采集同样非常简单,只需要一个基本的系统命令“free”,就可以获得当前系统内存总量,剩余使用量,以及文件系统的buffer和cache两者使用量。

●进程数量:系统进程总数,或者某个用户下的进程数,都可以通过“ps”命令经过简单的处理来获得。

数据库健康状态信息

服务端口(3306)服务端口状态的监控和主机网络连接的监控同样非常简单,只需要对3306端口进行telnet尝试即可。

mysqld和mysqld_safe进程:mysqld进程是MySQLServer 最核心的进程。mysqld进程crash或者出现异常,MySQLServer 基本上也就无法正常提供服务了。当然,如果我们是通过mysqld_safe来启动MySQLServer,则mysqld_safe会帮助我们来监控mysqld进程的状态,当mysqld进程crash之后,mysqld_safe会马上帮助我们重启mysqld进程。

Errorlog:Errorlog 的监控目的主要是即时检测MySQLServer 运行过程中发生的各种错误,如连接异常,系统bug等。

复制状态:如果我们的MySQL数据库环境使用了MySQLReplication,就必须增加对Slave复制状态的监控。对Slave的复制状态的监控包括对IO线程和SQL线程二者的运行状态的监控。当然,如果希望能够监控Replication更多的信息,如两个线程各自运行的进度等,同样可以在Slave节点上执行相应命令轻松得到

sky@localhost: (none) 04:30:38> show slave status\G

性能状态监控:

系统load值:系统load所包含的最关键含义是CPU运行等待的数量,

sky@sky:~$uptime

17:27:44up 4:12, 3 users, load average: 0.87, 0.66, 0.61

“loadaverage: 0.87, 0.66, 0.61”中的三个数字,分别代表了1秒、5秒和15秒的load平均值。

CPU使用率:最为常用的方法是使用命令top和vmstat来获取。

磁盘IO量:可以通过vmstat, iostat来获取

swap进出量:swap 的使用主要表现了系统在物理内存不够的情况下使用虚拟内存的情况。

free命令只能获得当前系统swap的总体使用量。如果希望获得实时的swap使用变化,还是得依赖vmstat来得到

网络流量:第三方软件如ifstat、iftop和nload

数据库性能状态:

MySQL数据库的性能状态监控点非常之多,其中很多量都是我们不能忽视的必须监控的量,且90%以上的内容可以在连接上MySQLServer 后执行“SHOW/*!50000 GLOBAL */STATUS” 以及“SHOW/*!50000 GLOBAL */ VARIABLES”的输出值获得。需要注意的是上述命令所获得状态值实际上是累计值,所以如果要计算(单位/某个)时间段内的变化量还需要稍加处理,可以在附录中找到两个命令输出值的详细说明。下面看看几项需要重点关注的性能状态:

QPS(每秒Query量):

QPS= Questions(or Queries) / Seconds

获取所需状态变量值:

SHOW/*!50000 GLOBAL */ STATUS LIKE 'Questions'

SHOW/*!50000 GLOBAL */ STATUS LIKE 'Queries'

TPS(每秒事务量):在MySQLServer 中并没有直接事务计数器,我们只能通过回滚和提交计数器来计算出系统的事务量。

TPS= (Com_commit + Com_rollback) / Seconds

KeyBuffer 命中率:KeyBuffer 命中率代表了MyISAM类型表的索引的Cache命中率。该命中率的大小将直接影响MyISAM类型表的读写性能。

key_buffer_read_hits= (1 - Key_reads / Key_read_requests) * 100%

key_buffer_write_hits=(1 - Key_writes / Key_write_requests) * 100%

mysq>SHOW/*!50000 GLOBAL */ STATUS

->LIKE 'Key%';

-----------------------------------

|Key_read_requests | 10 |

|Key_reads | 4 |

|Key_write_requests | 0 |

|Key_writes | 0 |

+------------------------+-------+

InnodbBuffer 命中率:

这里InnodbBuffer 所指的是innodb_buffer_pool,也就是用来缓存Innodb类型表的数据和索引的内存空间。

innodb_buffer_read_hits=(1-Innodb_buffer_pool_reads/Innodb_buffer_pool_read_requests)* 100%

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Innodb_buffer_pool_read%';

|Innodb_buffer_pool_read_requests | 5367 |

|Innodb_buffer_pool_reads | 507 |

+-----------------------------------+-------+

QueryCache 命中率:

Query_cache_hits=(Qcache_hits / (Qcache_hits + Qcache_inserts)) * 100%

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Qcache%';

|Qcache_hits | 0 |

|Qcache_inserts | 0 |

TableCache 状态量:

判断系统参数table_open_cache的设置是否合理。Open_tables与Opened_tables之间的比率过低,则代表TableCache 设置过小,个人认为该值处于80%左右比较合适。

mysql>SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Open%';

|Open_tables | 51 |

Opened_tables| 61 |

ThreadCache 命中率:

ThreadCache 命中率能够直接反应出我们的系统参数thread_cache_size设置的是否合理.一个合理的thread_cache_size参数能够节约大量创建新连接时所需要消耗的资源。

Thread_cache_hits= (1 - Threads_created / Connections) * 100%

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Thread%';

|Threads_created | 3 |

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Connections';

Connections| 11 |

锁定状态:锁定状态包括表锁和行锁两种

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE '%lock%';

|Innodb_row_lock_current_waits | 0 |

|Innodb_row_lock_time | 0 |

|Innodb_row_lock_time_avg | 0 |

|Innodb_row_lock_time_max | 0 |

|Innodb_row_lock_waits | 0 |

|Table_locks_immediate | 44 |

|Table_locks_waited | 0 |

如当Table_locks_waited与Table_locks_immediate的比值较大,则说明我们的表锁造成的阻塞比较严重

Innodb_row_lock_waits较大,则说明Innodb的行锁也比较严重,且影响了其他线程的正常处理

复制延时量:复制延时量将直接影响了Slave数据库处于不一致状态的时间长短。如果我们是通过Slave来提供读服务,就不得不重视这个延时量。可以通过在Slave节点上执行“SHOWSLAVE STATUS”命令,取Seconds_Behind_Master项的值来了解Slave当前的延时量(单位:秒)

Tmptable 状况:

TmpTable 的状况主要是用于监控MySQL使用临时表的量是否过多,是否有临时表过大而不得不从内存中换出到磁盘文件上。

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Created_tmp%';

Created_tmp_disk_tables| 1 |

Created_tmp_tables | 46 |

从上面可以看出系统使用了46次临时表,其中有1次临时表比较大,无法在内存中完成,而不得不使用到磁盘文件

BinlogCache 使用状况:BinlogCache 用于存放还未写入磁盘的Binlog信息。

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Binlog_cache%';

|Binlog_cache_disk_use | 0 |

|Binlog_cache_use | 0 |

如果Binlog_cache_disk_use值不为0,则说明BinlogCache 大小可能不够

Innodb_log_waits量:

Innodb_log_waits状态变量直接反应出InnodbLog Buffer 空间不足造成等待的次数。

mysql> SHOW /*!50000 GLOBAL*/ STATUS

->LIKE 'Innodb_log_waits';

Innodb_log_waits| 0

常用开源监控软件:

RRDTool。RRDTool全称为RoundRobinDatabase Tool,也就是环状循环数据库工具

Nagios:Nagois 是一个非常著名的运行在Linux/Unix上的对IT设备或服务的运行状态进行监控的软件。

MRTG

MRTG应该算是一款比较老牌的监控软件了,功能比较简单,最初是为了监控网络链路流量而产生的。

Cacti

Cacti和Nagios最大的区别在于前者具有非常强大的数据采集、存储以及展现功能



MySQL索引背后的数据结构
MySQL性能调优与架构设计
SQL Server数据库备份与恢复
让数据库飞起来 10大DB2优化
oracle的临时表空间写满磁盘
数据库的跨平台设计
更多...   


并发、大容量、高性能数据库
高级数据库架构设计师
Hadoop原理与实践
Oracle 数据仓库
数据仓库和数据挖掘
Oracle数据库开发与管理


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...   
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号