XX公司服务器存储系统架构说明文档 当前版本:1.0.0.0 2016年 06月 20日 文件状态: [∨] 草稿 [ ] 正式发布 [ ] 正在修改 文件标识: HLD_SERVER_IFS 当前版本: 1.0.0.0 作 者: 井方南 完成日期: 2016/06/20 版 本 历 史 版本/状态 作者 参与者 日期 备注 1.0.0.0 井方南 2016-06-20 正式文档 第一部分 文档简介 1 文档说明 1.1编写目的 本说明书定义和阐述XX科技有限公司服务器数据存储的改造方案,通过横向扩展与集群的方式以及负载均衡的策略降低单台服务器的访问压力,降低宕机的可能性,最大限度的提高应用程序对数据的读写速度和并发量。 目的在于: 1) 描述清晰的改造方案; 2) 阐述基本的中心思想 3) 定义数据切分的基本规则 本说明书的预期读者包括: 1) 开发人员; 2) 技术管理人员; 3) 合作各方有关部门的负责人 4) DBA。 1.2项目背景 为了提高XX公司的服务效率,为后期数据库扩展提供结构基础,为提高处理大规模数据的性能,编写此文档。 1.3业务术语 业务术语 解释 备注 1.4参考资料 资料名称 版本或日期 作者 第二部分 数据存储改造 改造背景: 广州XX公司数据存储系统长期以mssql为唯一数据库系统,由于前期设计不足,业务理解不到位导致数据库设计整体性能比较差。目前数据存储情况为: 数据集中情况十分突出,数据分布不均匀,少数表数据量高达500万+级,更有单表数据量达3600万级,大部数据中位数集中在十万级,分布方差比较大。 加之项目重构之前(目前阶段),程序代码质量极差,没有任何缓存设施,对数据库访问过于频繁,造成生产服务器IO长期高负荷 100%运转,随着数据库数据量急剧增长,数据库服务器在访问高峰期面临高负荷高IO压力,甚至隐含重大宕机风险。 XX公司数据存储远景规划(关系型数据部分)方案是以开源数据库mysql完全代替mssql,近期规划是mysql与mssql共存,目前阶段以mssql为主要存储系统。鉴于此中远期规划以及业务发展状况,结合XX公司项目重构基础,以及目前所处的过度阶段状况,XX公司数据存储系统目前仍然以mssql为主要服务提供者。 为了提供稳定可扩展的数据库服务,解决数据库宕机带来的单点数据库不能访问的问题,最大限度的提高应用程序对数据的读写速度和并发量,考虑到云服务器向上扩展的局限性,现必须对现有数据服务器进行横向扩展以实现负载均衡。 结合目前的实际情况,程序中大量包含多表关联查询,大数据量的表体中不存在选择性比较高的域,对现存表强制进行水平拆分或者垂直拆分都将直接影响各个业务模块,波及范围无法预测。极有可能造成业务失败,甚至项目失败。在不允许停机的业务需求的背景下,强制拆分无疑会给项目重构带来不可预期的风险因素,给公司造成重大甚至无法挽回的损失。 在这一背景下,目前比较好的策略是通过表分区的操作达到表优化的目的。在一定程度上可以缓解数据库IO压力。程序代码的重构,通过批量化操作也可大幅度降低对数据库服务器的IO压力。 2.1 基于mssql表分区 经过对现存mssql 数据库的结构性分析,结合实际的业务需求以及现存的数据库使用情况,本篇讨论通过表分区的方法对mssql数据库进行深度结构性优化,以提高数据库服务器的查询性能。 2.1.1 表分区的基本概念 sqlserver引入的表分区技术,允许用户把数据分散存放到不同的磁盘中,提高数据库服务器的整体并行处理能力以优化查询性能。 2.1.2 表分区的基本原则 1) XX公司生产数据是否适合表分区操作? 分区有风险,操作需谨慎。 数据分区要满足几点要求,才能够考虑对表进行分区操作。 ① 单表数据规模庞大,已经极难或几乎不可能通过改善索引达到查询优化的目的。规模大小需要结合具体的软硬件情况以及业务需求。原则上单表数据达到百万级别即可考虑进行分区。 ② 数据呈结构性分段存储。当数据规模达到一定标准,如果数据散列无序也不可随意分区。这样只会增加存储和查询的复杂度,得不偿失。数据分段意为数据存储表现为历史数据和操作数据,例如经常进行的增删改操作只针对操作数据区(近期数据区),对于历史数据几乎不需要操作或者仅仅涉及到查询。 XX公司数据存储系统到本文截稿日期为止,数据检索和更新仍能满足业务需求,因此短期之内暂不考虑进行分区操作。 2) 何时使用本文方法对XX生产库进行表分区操作? 就目前硬件条件来说,XX公司数据库系统可以支撑千万级甚至亿级规模数据,当最大表数据规模达到或者接近亿级,查询极为缓慢已到达业务可接受边界值,已经无法再通过调整程序代码或者添加硬件进行优化的程度时,即可考虑通过对数据库表进行分区以达到查询优化的目的。 当查询不可接受时,首先不考虑对表进行分区,优先使用本文下面小节涉及到的优化方案进行优化,当获取不到预期的效果时才可考虑分区方案。 2.1.3 表分区的中心思想 通过对sqlserver 数据库表的分区操作, 将数据库表文件分配到不同的文件组,然后将文件组散布到不同的物理磁盘上,不同的文件组管理自己所在文件的数据,以此来极大提高数据库服务器的并发处理能力和IO能力。 sqlserver 2008 已经改变了分区表的内部表现形式,即已分区表将作为一个多列索引呈现给查询处理器。业务编码人员或者sql编码程序员无需知道分组文件的存在,只需要了解分区列并且在查询条件上追加分区列即可。 2.1.4 表分区的整体架构 XX公司涉及到的需要分区的表如下: 表名称 单表数据规模(截止2016-06-01) (单位/万) 暂定分区字段 表-2.1.3.1 基于mssql数据库的表分区,建立在sql server2008新内核的基础之上,通过文件分组的形式从物理层面对数据文件进行拆分,使得每一单一数据表数据分组文件规模控制在相对合理和查询可接受的范围之内。在可预期的时间周期内,保持单一分区数据增长规模相对可控制、查询可接受以满足业务需求。 本文对目标数据库表进行的分区操作,整体架构思想是仍以目前的mssql数据库实例为基础,对表-2.1.3.1 所涉及到的数据表进行实例内水平拆分。单一表按照相应计划内分区字段域进行分区,通过对数据库表中数据分布的分析,分区标准暂定如下 表名称 暂定分区字段 分区文件组 表-2.3.1.2 注: 分区文件组分别为五列 第一列: 2014-01-01以前的数据,不包含2014-01-01 第二列: 2014-01-01到2015-01-01的数据 包含2014-01-01 第三列: 2015-01-01到2016-01-01的数据 包含2015-01-01 第四列: 2016-01-01到2017-01-01的数据 包含2016-01-01 第五列: 2017-01-01以后的数据 2.1.5 表分区的流程方法 2.1.5.1 选取分区列 分区列的选取要求一定能够对分区表数据进行结构化的切分。数据整体分布要求尽可能均匀。根据 表-2.3.1.2 约束的表分区标准,以记录创建时间作为表分区列。 2.1.5.2 分区过程 进行分区之前必须设置数据库文件组和文件 最佳实践如下: 1)主文件组必须完全独立,它里面应该只存储系统对象,所有的用户对象都不应该放在主文件组中。主文件组也不应该设为默认组,将系统对象和用户对象分开可以获得更好的性能; 2)如果有多块硬盘,可以将每个文件组中的每个文件分配到每块硬盘上,这样可以实现分布式磁盘I/O,大大提高数据读写速度; 3)将访问频繁的表及其索引放到一个单独的文件组中,这样读取表数据和索引都会更快; 4)将访问频繁的包含Text和Image数据类型的列的表放到一个单独的文件组中,最好将其中的Text和Image列数据放在一个独立的硬盘中,这样检索该表的非Text和Image列时速度就不会受Text和Image列的影响; 5)将事务日志文件放在一个独立的硬盘上,千万不要和数据文件共用一块硬盘,日志操作属于写密集型操作,因此保证日志写入具有良好的I/O性能非常重要; 6)将“只读”表单独放到一个独立的文件组中,同样,将“只写”表单独放到一个文件组中,这样只读表的检索速度会更快,只写表的更新速度也会更快; 7)不要过度使用SQL Server的“自动增长”特性,因为自动增长的成本其实是很高的,设置“自动增长”值为一个合适的值,如一周,同样,也不要过度频繁地使用“自动收缩”特性,最好禁用掉自动收缩,改为手工收缩数据库大小,或使用调度操作,设置一个合理的时间间隔,如一个月。 [img]http://dl2.iteye.com/upload/attachment/0118/3898/e254f17d-5c24-39cb-bdd9-56010094971b.png[/img] [img]http://dl2.iteye.com/upload/attachment/0118/3900/91c50b20-f651-3adc-ae72-8ef299240e69.png[/img] [img]http://dl2.iteye.com/upload/attachment/0118/3902/2e1fbc1e-8676-34e9-94e3-255bac201117.png[/img] [img]http://dl2.iteye.com/upload/attachment/0118/3904/fae6d4a0-0c9c-3d48-a540-4656a8090268.png[/img] 这一步包含有两个可选项。 1) 将此表与选定的分区表并置。 这要求在同一数据库下存在另外一张已经分好区的表,该表的分区列和当前选中的列的类型完全一致。这样做的好处是当两张表在查询中有关联时并且其关联列就是分区列时,使用同样的分区策略更有效率。 2) 将存储区中的所有非唯一索引和唯一索引与索引分区列对齐。 这会将表中的索引也一同分区,实现对齐。这样做的好处是表和索引的分区一致,一方面查询时利用索引更为高效,而且移入移出分区也更为高效。 [img]http://dl2.iteye.com/upload/attachment/0118/3906/b52a45a1-3d85-3c28-855c-f9b4ccd178ba.png[/img] 分区函数 USE [kpmini] GO BEGIN TRANSACTION CREATE PARTITION FUNCTION [base_goods_f](datetime) AS RANGE LEFT FOR VALUES (N'2014-01-01T00:00:00', N'2015-01-01T00:00:00', N'2016-01-01T00:00:00', N'2017-01-01T00:00:00') CREATE PARTITION SCHEME [base_goods_p] AS PARTITION [base_goods_f] TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]) CREATE CLUSTERED INDEX [index_of_enterpriseKID] ON [dbo].[BaseGoods] ( [EnterpriseKID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [base_goods_p]([CreateTime]) COMMIT TRANSACTION [img]http://dl2.iteye.com/upload/attachment/0118/3908/febc96e8-8f0e-3414-a07d-8ab1c1571162.png[/img] [img]http://dl2.iteye.com/upload/attachment/0118/3910/78b7ab38-83ad-3e16-b718-5a087070cf2d.png[/img] [img]http://dl2.iteye.com/upload/attachment/0118/3912/380b01d3-5602-3f88-9545-9e4d23f2664a.png[/img] [img]http://dl2.iteye.com/upload/attachment/0118/3914/8558c633-59dd-3929-bad3-6fbff1096e1e.png[/img] 以上的分区方案在分区表存在索引和索引不存在的情况下都可以达到分区的目的,如果在分区的过程中没有选择索引分区方案,那么所有的索引文件仍将存在统一的索引文件处。可以通过修改分区函数和分区方案的过程来重新对索引数据进行分区。这在下一小节进行详细分析。 2.1.5.3 对索引进行分区 1.在表没有索引的情况下: BEGIN TRANSACTION CREATE PARTITION FUNCTION [TestFunction](datetime) AS RANGE LEFT FOR VALUES (N'2010-01-01T00:00:00', N'2010-02-01T00:00:00', N'2010-03-01T00:00:00', N'2010-04-01T00:00:00', N'2010-05-01T00:00:00', N'2010-06-01T00:00:00') CREATE PARTITION SCHEME [TestScheme] AS PARTITION [TestFunction] TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]) CREATE CLUSTERED INDEX [ClusteredIndex_on_TestScheme_634025264502439124] ON [dbo].[Account] ( [birthday] )WITH (SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [TestScheme]([birthday]) DROP INDEX [ClusteredIndex_on_TestScheme_634025264502439124] ON [dbo].[Account] WITH ( ONLINE = OFF ) COMMIT TRANSACTION 这里先创建Partition Function以及Partition Scheme,之后在分区列上创建聚集索引并按照分区方案分区,最后删除了这一索引。 2.在表有索引的情况下: 如果原先没有聚集索引: CREATE CLUSTERED INDEX [ClusteredIndex_on_TestScheme_634025229911990663] ON [dbo].[Account] ( [birthday] )WITH (SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [TestScheme]([birthday]) DROP INDEX [ClusteredIndex_on_TestScheme_634025229911990663] ON [dbo].[Account] WITH ( ONLINE = OFF ) 这和没有索引的情况一样,如果表原先存在聚集索引,则脚本变为: CREATE CLUSTERED INDEX [IX_id] ON [dbo].[Account] ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [TestScheme]([birthday]) 可以看到原有的聚集索引(IX_id)在分区方案上被重建了。 如果选择了“对齐索引”选项,则会对所有索引都应用分区: CREATE CLUSTERED INDEX [IX_id] ON [dbo].[Account] ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [TestScheme]([birthday]) CREATE NONCLUSTERED INDEX [UIX_birthday] ON [dbo].[Account] ( [birthday] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [TestScheme]([birthday]) CREATE NONCLUSTERED INDEX [UIX_name] ON [dbo].[Account] ( [name] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) 这里不仅对聚集索引IX_id进行了分区,也对非聚集索引UIX_name和UIX_birthday进行了分区。 对索引分区 XX公司的表分区方案根据数据实际情况分析得知,主键是UUID类型,且作为剧集索引存在,聚集索引是数据的一部分,即聚集索引和表数据在同一个文件组。为避免将主键和表数据置放到同一个文件组,建议除了依据列,其他索引不要建立在这个分区上,即选取依据列的时候的索引对齐。 对聚集索引进行分区 对聚集索引进行分区时,聚集键必须包含分区依据列。对非唯一的聚集索引进行分区时,如果未在聚集键中明确指定分区依据列,默认情况下 SQL Server 将在聚集索引键列表中添加分区依据列。如果聚集索引是唯一的,则必须明确指定聚集索引键包含分区依据列。 对非聚集索引进行分区 对唯一的非聚集索引进行分区时,索引键必须包含分区依据列。对非唯一的非聚集索引进行分区时,默认情况下 SQL Server 将分区依据列添加为索引的非键(包含性)列,以确保索引与基表对齐。如果索引中已经存在分区依据列,SQL Server 将不会向索引中添加分区依据列。 2.1.6 表分区的查询方法 1) 使用 $PARTITION 函数 若要着重查询单个分区,可以同时使用 $PARTITION 函数与分区函数名。也可以通过使用 $PARTITION 来执行以下操作: 访问已分区表的分区子集中的所有行。 检查每个分区中有多少行。 确定包含特定分区键值的行位于哪个分区中或将该行插入哪里。 ① 语法结构 [database_name.]$PARTITION.partition_function_name(expre) ② 参数 database_name 包含分区函数的数据库的名称。 partition_function_name 对其应用一组分区列值的任何现有分区函数的名称。 expre 其数据类型必须匹配或可隐式转换为其对应分区列数据类型的表达式。expression 也可以是当前参与 partition_function_name 的分区列的名称 ③ 示例说明 现在对BaseGoods 表中位于分区1当中的数据进行检索。T-Sql如下: SELECT * FROM BaseGoods WHERE $PARTITION.base_goods_createTime(CreateTime) = 1 ; 2) 利用sql server 2008 新内核 上文提到,sql server 2008 已经改变了已分区表的内部表现形式。外部执行sql 无需指定分区区间,只需要将分区依据列追加为查询条件,R2查询分析器会自动到指定的分区进行检索。当然如果不指定分区,又没有追加依据列作为查询条件,那么查询分析器会扫描每个分区,之后将各个结果集进行合并,以分区顺序输出。 如下为指定依据列: select * from BaseGoods where CreateTime <= '2014-01-01'; 2.1.7 删除与合并分区 通过拆分或合并边界值更改分区函数。通过执行 ALTER PARTITION FUNCTION,可以将使用分区函数的任何表或索引的某个分区拆分为两个分区,也可以将两个分区合并为一个分区。 简单一点说,删除(合并)一个分区,事实上就是在分区函数中将多余的分界值删除。 注意:多个表或索引可以使用同一分区函数。ALTER PARTITION FUNCTION 在单个事务中影响所有这些表或索引。 ALTER PARTITION FUNCTION 语法如下: ALTER PARTITION FUNCTION partition_function_name() { SPLIT RANGE ( boundary_value ) | MERGE RANGE ( boundary_value ) } [ ; ] 参数说明: partition_function_name 要修改的分区函数的名称。 SPLIT RANGE ( boundary_value ) 在分区函数中添加一个分区。boundary_value 确定新分区的范围,因此它必须不同于分区函数的现有边界范围。根据 boundary_value,Microsoft SQL Server 2005 数据库引擎将某个现有范围拆分为两个范围。在这两个范围中,新 boundary_value 所在的范围被视为是新分区。 合并分区: 分析示例分区表BaseGoods中的5个文件组 [~-20140101],[20140101-20150101],[20150101-20160101],[20160101-20170101] [2017-01-01-~] 发现,在[~-20140101],[20140101-20150101] 数据文件组的数据规模非常小,单独分区成独立的文件组不利于资源优化,需要合并这两个分区。 原始的分区函数为: CREATE PARTITION FUNCTION [base_goods_createTime](datetime) AS RANGE LEFT FOR VALUES (N'2014-01-01T00:00:00', N'2015-01-01T00:00:00', N'2016-01-01T00:00:00', N'2017-01-01T00:00:00') 修改之后的分区函数为: CREATE PARTITION FUNCTION [base_goods_createTime](datetime) AS RANGE LEFT FOR VALUES (N'2015-01-01T00:00:00.000', N'2016-01-01T00:00:00.000', N'2017-01-01T00:00:00.000') 通过比较发现 修改之后的分区函数取消了第一区间的分组,意思是14年之前的数据和15年合并。 修改合并分区函数: ALTER PARTITION FUNCTION base_goods_createTime () MERGE RANGE ('2014-01-01T00:00:00') 查看分区 已经完成合并。 select $PARTITION.base_goods_createTime(CreateTime) as 分区编号,count(KID) as 记录数 from BaseGoods group by $PARTITION.base_goods_createTime(CreateTime) 只剩下三个分区 添加分区: 首先查看分区方案,获取分区方案名称和文件组 修改分区方案,添加文件组 ALTER PARTITION SCHEME base_goods_program NEXT USED [PRIMARY] 修改分区函数 ALTER PARTITION FUNCTION base_goods_createTime() SPLIT RANGE ('20140101') 再次查看分区区间 这时候就含有了后期新增的分区 2.1.8 恢复普通表 第一种方法是使用上面提供的方式进行边界值的合并,这样最终剩下的唯一分区表就是原表。虽然剩下的分区表还是已经分区表,但是和普通表已经没有什么区别。 第二种方式是通过删除分区索引的方式将分区表转换成普通表。这要经过两个步骤: 1 删除分区索引 2 在原来的索引字段上重建一个索引。 使用T-SQL完成上面两部操作 CREATE CLUSTERED INDEX CT_Sale1 ON BaseGoods([CreateTime]) WITH ( DROP_EXISTING = ON) ON [PRIMARY] 2.2 mysql数据集群 mssql 与mysql 数据库共存阶段被列入XX公司数据存储发展计划。鉴于mssql数据库的非开源特性和windows服务器的依赖程度较高以及开源数据库mysql的诸多优秀性能表现,考虑在中长期发展规划中逐步弱化mssql存储服务器系统, 在这一过程中较长时间段内会出现mssql与mysql共同服务的局面。 mssql的存储特性和局限性在其他篇幅有详细说明(参考: kp-set-001\kp-doc\src\doc\代码重构\一期代码重构\XX公司数据库迁移项目可行性研究与风险评估.docx),本章节重点阐述关于mysql服务器上负载均衡、分表分库的优化策略。 基本要素: 长期发展的互联网项目会经历一个业务量由小到大到海量的过程。这一过程也要求数据库服务系统能够提供积极的容错容灾易扩展低延迟的服务。 企业级数据库发展的一般思路是单点式存储到库表分离、负载均衡、主从架构、读写分离分布式架构,大型企业采用的则是“小型机+高端商用数据库+高端存储阵列”的集中式架构。后者费用成本十分高昂,年使用成本高达数百甚至数千万美元,传统集中式架构受限于硬件的垂直扩展能力,难以应对XX公司未来海量数据的高并发需求,另一方面,涉及到的高端硬件全部被国外长期垄断,过度依赖国外技术存在一定的数据风险。 本着安全可控、成本优化、高效可扩展的数据库设计方案的原则,XX公司数据服务器系统中期规划仍将以云存储服务器为主,实施主从复制、多主多从、读写分离的技术方案,结合高速缓存系统对外提供高度容错、高效可扩展的数据服务。 2.2.1 数据集群的中心思想 数据分片包括垂直分片和水平分片,垂直分片又可以分为表垂直和列垂直。 列垂直: 列垂直是将一张含有多列的大表进行列分,把大字段和小字段分开,热字段和冷字段分离。列垂直一般复杂度较高,需要对业务过程有十分深刻的理解和分析,并且在可预见的未来,业务数据和业务规则不会产生较大变化。需要在设计之初就做好深度切分,后期对表进行垂直列分,是一场灾难。 表垂直: 表垂直是将切割的相对独立的业务表分置到不同的服务器,其中某些数据库服务器的崩溃不会影响其他业务模块的正常访问,而且也起到了负载分流的作用,提升了服务器的吞吐能力。 拓扑图大致如下: 表垂直的缺点也是显而易见的,这种架构直接排除了数据库表之间的关联,而且也没有从任何一个角度解决或者延缓数据库中的巨无霸大表的暴涨问题。XX公司数据库恰恰存在一部分规模庞大且增长迅猛的表体。 水平分片: 顾名思义是将单表数据按照一定路由规则存放到不同数据表的存储方案。mysql存在多种水平切分方式,这里我们重点讨论依据ID主键进行路由的规则。 查询请求根据其不同的ID主键被路由服务器分发到不同的数据库服务器执行查询。 2.2.2 数据集群的整体架构 针对XX公司数据规模来说,单表的数据量可以在极短的时间内完成快速增长,5个月或者更短的周期内即可迅速接近亿级表单,并且在可预期的时间范围内达到不可估量的业务规模。访问高峰期会对服务器造成巨大的读写压力,直接导致服务器IO到达峰值。 在这一业务背景下,单一的多表单实例的数据库架构已经不能满足需求。即使短时间内能解决业务需求,但是不久之后又会出现因单表数据规模过大、库表读写并发大、IO并发服务超时等严重影响用户体验的数据库问题,成为项目瓶颈。 因此,需要把XX重构项目架设在mysql集群之上。 为避免大表数据规模过大,首先对表进行水平拆分,实现分表。 以订单表为例,为避免数据规模过大,将订单表水平分割成256张表,ID主键是最常用的路由字段,这里携带主键ID,既可以完成查询又可以将数据分不到不同的表中。 已经分表的order表,客户端可以根据user_id%256取得对应的表进行存储,也可以根据user_id%256取得对应的表进行访问。 分表能够解决因单表数据量过大造成的查询效率低下的问题,但是无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问时,当数据库master服务器无法承受写操作压力时,无论如何扩展slave服务器,也都没有意义了。因此必须将IO分散开来,均衡分配访问压力,即负载均衡。 负载均衡策略将设置一个虚拟IP,通过心跳机制附属于一个可用数据库实例IP,当出现当前IP不可用的情况,虚拟IP主动切换到可用数据库实例上继续提供服务。 随着数据规模的不断增长,任何例外的服务终端都将造成不可估量的损失,服务的高可用性也越来越重要,因此对利用硬件和软件方法实现高可用、高可伸缩的网络服务的需求不断增长,这种需求可以归结为以下几点: 1) 可伸缩性(Scalability): 当服务的负载不断增长时,系统能被扩展来满足需求,且不降低服务质量。 2) 高可用性(Availability): 允许部分软件或者硬件发生故障,但是整个系统的服务必须是 7 X 24 小时可用的。 3) 可管理性(Manageability) : 整个系统可能在物理层面上很庞大,但是比较容易管理。管理包括系统部署与更新、数据迁移、数据负载、服务器切换、数据的冷热备份、故障恢复等。 4) 经济有效性(Cost-effectiveness): 整个系统实现是经济的、易支付的。单服务器显然不能支撑不断增长的负载,这种服务器升级有多种不足,一是升级过程繁琐,机器切换会使服务暂时中断,并造成原有计算资源的浪费,中断周期会随着数据规模和系统架构的复杂度不断延长,严重时可造成永久性失效。二是越往高端的服务器迁移,所花费的代价就越大。三是一旦 该服务器或软件失效,会导致整个服务的中断。通过高性能网络或局域网互联的服务器集群正成为实现高可用、高伸缩的网络服务的有效架构。这种松耦合的结构比紧耦合的多处理器系统具有更好的伸缩性和性能价格比,组成集群的PC服务器或RISC服务器和标准网络服务设备因为大规模生产,价格比较低,具有很高的性能价格比。根据经济学原理,产出和投入是正比关系,在硬件资源上的投入缩减,仍然期望达到预期的效果,就必须在其他方面加大投入。这里有很多挑战性的工作,如何在集群系统实现网络并行服务,让其对外是透明的,并且具有良好的可伸缩性和可用性。针对上述需求,这里给出基于IP层和内容请求分发的负载均衡调度解决方法,基于Linux内核实现,将一组服务器构成一个实现可伸缩的、高可用网络服务的服务器集群,称之为linux虚拟服务器(Linux Virtual Server),在LVS集群中,使得服务器集群的结构对客户是透明的,客户访问集群提供的网络服务就好像访问一台高可用、高性能的服务器一样。客户程序不受服务器集群的影响,不需要做任何修改。系统的伸缩性通过在集群中透明的加入和删除一个节点来达到,通过检测节点或服务进程故障和正确的重置系统来达到高可用性。 本文档采用mysql 同步集群。 图-2.2.2-1 mysql 负载均衡整体架构图 注: 因篇幅和排版原因,图中仅少数IP实例,具体部署可根据实际情况和业务需要适当增加服务器数量。 2.2.3 数据集群的流程方法 数据库集群方案将会在下一篇文档中给出详细分析以及操作步骤。届时将通过完整篇幅充分分析针对mysql数据库各个层面的集群方案,优中选优确定最适合XX当前的数据库存储集群方案。本篇幅先给出数据集群的整体流程方法。 1) 分配一组独立的IP,mysql官方建议不低于3台服务器。 2) 在独立的服务器上装配最新mysql数据库软件,并且进行优化配置。 3) 虚拟化一个VIP,通过keepalive机制实现此IP连续服务。 4) 对大表进行水平分片,部分大表需要切分成30张以上的小表(建议分成50-100张表)。 5) 独立的IP之间通过主主关联进行数据同步。 6) 如果预算足够,建议再次虚拟出keepalive IP,分配给读操作。如果超出预算,则读写都落在这一组IP服务器上。 2.3 mysql 分布式存储 数据分布式存储主要涉及: 分表,分库, M-S ,集群,负载均衡,读写分离 以及更深层次的大数据处理技术。 分表、集群技术、负载均衡技术在2.2小节已经进行比较详细的说明,集群技术将在下一篇福步骤化。M-S以及读写分离也将在下一篇幅同步讲解分析。 数据分布式存储的思想主要为大规模、高并发、高可用互联网应用提供解决方案。分布式数据库方案基本功能: 1) 提供分表分库规则和路由规则。 2) 引入集群(Group)概念,保证数据的高可用性。 3) 引入负载均衡策略(LB) 4) 引入集群节点可用性探测机制,确保系统的高度稳定性。可通过心跳机制或者节点定时探针实现。 5) 引入读写分离,提高数据的查询速度。 仅仅实现分表分库,并不能完全解决系统容错性的问题。当节点中的某些机器出现宕机,仍然会有 N分之一的数据不能正常访问,这时候需要引入集群技术,如此一来,当某些节点出现机器不能服务的情况时,负载均衡器会跳过这些问题节点,将负载分配给可用的机器,解决容错性问题。 为提高数据的读写效率,可对数据库操作进行读写分离。通过一定的机制将写操作负载到Master节点,将读操作负载到Slave节点。可以通过lua脚本或者shell 实现。 至此,基于mysql的数据集群以及分布式存储方案已经分析完毕,因为篇幅与排版的原因,mysql数据集群的详细方法步骤将在下一篇幅进行细节化与步骤化。 第三部分 存储优化 3.1 mssql 非结构性优化 3.1.1 索引碎片优化 当数据库增长,页拆分,删除数据时就会产生碎片。数据碎片会造成一些不利影响。 1) 碎片会造成空间的浪费,sql server每次会分配一个区段,如果一个页上只有一条数据,仍然会分配整个区段。 2) 散布在各处的数据会造成数据检索时的额外系统开销。为了获取10行记录,sqlserver 不是只读取一个页面,而是会读取10个页面来获取信息。 碎片分为两种,内部碎片和外部碎片。 外部碎片: 外部碎片指的是页拆分造成的碎片。如向表中新插入一行,这一行导致现有的页空间无法容纳新插入的行而导致页拆分。新的页随着数据的增长而增长,而聚集索引要求行之间连续,XX公司数据库聚集索引绝大部分都不是自增列,页拆分后和原来的页在磁盘上并不连续,这就是外部碎片。 由于页拆分,导致数据在页之间的移动,所以如果插入更新等操作需要经常分页,则会大大消耗IO资源,造成性能下降。对于查找来说,如果有特定的条件,即在where条件子句有非常详细的限制或者返回无序结果集时,外部碎片并不会对查询性能产生影响。但如果需要返回扫描聚集索引而且查找连续页时,外部碎片就会产生性能上的影响。连续数据不能预读,造成额外的物理读,增加磁盘的IO,外部碎片还会造成频繁的区切换。 如果页面排序是连续的,预读功能可以提前读取页面而不需要太多的磁头移动。 内部碎片: 内部碎片是指页拆分之后,导致索引页的数据并不满,有空行。同样读取一个索引页,却只能拿到百分之几的数据。 解决索引碎片问题: 碎片对查询的危害: 这里以测试数据库表 BizGoodsStock 为例,执行查询语句 DBCC DROPCLEANBUFFERS; DBCC FREEPROCCACHE; DBCC FREESYSTEMCACHE ('ALL'); select top 1000 * from BizGoodsStock where EnterpriseKID = '40391992-9dee-4053-98ea-9497eb26add9' and cast(DataTimestamp as bigint) > 500 多次执行取平均查询时间,中位数集中在6秒左右。 查看其索引碎片情况 SELECT index_type_desc,alloc_unit_type_desc,avg_fragmentation_in_percent,fragment_count,avg_fragment_size_in_pages, page_count,record_count,avg_page_space_used_in_percent FROM sys.dm_db_index_physical_stats(DB_ID('kpmini'),OBJECT_ID('BizGoodsStock'),NULL,NULL,'Sampled'); 结果如下 根据第三列的数据显示,不管是聚集索引还是普通索引,碎片情况非常严重,根据第四列数据显示,聚集索引的碎片最严重。 必须重新整理现有索引对索引碎片进行优化。重新建立索引是比较实际有效的方法: ALTER INDEX in_BizGoodsStock_3 ON BizGoodsStock REBUILD ALTER INDEX IN_BizGoodsStock_EnterpriseAndGoodsAndisDeleted ON BizGoodsStock REBUILD ALTER INDEX IN_BizGoodsStock_EnterpriseKID_DataTimestamp ON BizGoodsStock REBUILD -- 创建聚集索引([KID]) create clustered index [KID] on [dbo].[BizGoodsStock]([KID]) go 再次查看索引碎片情况 SELECT index_type_desc,alloc_unit_type_desc,avg_fragmentation_in_percent,fragment_count,avg_fragment_size_in_pages, page_count,record_count,avg_page_space_used_in_percent FROM sys.dm_db_index_physical_stats(DB_ID('kpmini'),OBJECT_ID('BizGoodsStock'),NULL,NULL,'Sampled'); 通过数据可以显示,聚集索引和普通索引的碎片超大幅度下降,从百分之90以上降低到0.01左右。索引碎片的数量也在大幅度降低,数据页的空间利用率从60% 提高到98%,数据的高度集中化大幅度提高了查询效率以及降低了IO。 清除查询缓存之后查看数据检索效率: DBCC DROPCLEANBUFFERS; DBCC FREEPROCCACHE; DBCC FREESYSTEMCACHE ('ALL'); select top 1000 * from BizGoodsStock where EnterpriseKID = '40391992-9dee-4053-98ea-9497eb26add9' and cast(DataTimestamp as bigint) > 500 查询同样数量的数据,查询时间从6秒降低到了2秒左右。 对于频繁insert和update的表,在索引数据非常大的情况下,重建索引会花费比较多的时间。也可以考虑通过调整填充因子来优化,避免页拆分,这里不做过多分析。 3.1.2 索引优化 索引优化涉及到的内容十分庞杂,这里仅仅介绍以XX公司数据优化为中心的索引操作。 1) 索引重建(碎片整理)。 索引碎片过多,需要及时重建。详细介绍见3.1.1章节。 2) 删除无用索引。及时删除没有被使用的索引。 3) 索引列尽量使用选择性高的字段,选择性低的字段比不建立索引的效率还要低。 4) 尽量避免在索引列上使用聚合函数。聚合函数的使用会规避索引。 3.1.3 识别低效TSQL 1) 在查询中不要使用 select * 2) 在select中避免不必要的列,在连接中避免不需要的表连接 3) 不要在子查询中使用count()求和和执行存在性检查 不要使用 SELECT column_list FROM table WHERE 0 < (SELECT count(*) FROM table2 WHERE ..) 使用 SELECT column_list FROM table WHERE EXISTS (SELECT * FROM table2 WHERE ...) 代替; 4) 避免使用两个不同类型的列进行表连接 5) 尽可能的缩小事务范围,会造成死锁。 6) 避免使用游标,尽量避免自定义函数,要始终相信你的结构化代码效率是十分低下的。 3.1.4 理解查询计划 查询计划的理解是非常重要的。目前阶段来说,XX公司数据库sql 几乎没有使用查询计划。不理解查询优化器的一般工作原理,是很难写出质量相对较高的TSQL代码的,即使无法使用TSQL编码查看sql执行计划,也必须学会使用Management Studio分析执行计划,切勿根据Management Studio 提示盲目修改或者新建索引,应根据实际情况操作。 3.2 mysql表结构设计原则 必须明确,性能源自于设计,而不是优化。 基于对业务的通透分析,优良的结构性设计将造就数据库整体性能的大幅度提升。粗糙的字段堆叠,只会让后续数据与系统维护成为灾难。 1) 不应针对整个系统进行数据库设计。秉着低耦合高内聚的设计原则,应该在设计之初基于对业务范围的分析进行组件化设计。尽量减少组件之间的关联,为系统或表结构的重构提供可能性。 2) 采用领域模型的方式和自顶向下的思路进行设计,根据职责定义定义对象。确保与职责相关的数据被定义在一个对象。 3) 一张表里至少存在一个关键字或者多个关键字组合,能够被其他非关键字属性依赖。此关键字必须保证唯一性。最优为自增长数值类型。 4) 不适用外键关联,不产生传递依赖。 5) 应对主键建立索引,有针对的建立联合属性索引,提高索引效率。 6) 适当的设计冗余。但是冗余范围必须得到控制。有效的减少冗余有助于提高数据库的性能。 7) 在表设计之初就必须要考虑查询的有效性。必须考虑业务的扩展性以及更复杂查询的扩展性。 8) 在设计之初就应该考虑将大字段和小字段分离。尽可能的将常访问数据和冷数据字段分离。 9) 数据库表的字段类型选择应尽可能的小。如果可以使用tinyint就不应该使用smailint更不应该使用int或者double.字段属性的长度也必须得到合理控制。尽可能的不使用缺省255。缺省值切忌使用NULL。 10) 表在设计之初就应该考虑分表分库的可能性。 11) 必须考虑表是否存在事务要求。当对事务完成性有要求时,InnoDB是更优的选择。 12) 必须考虑当前表读写比。MyISAM的批量写入操作性能更佳。 13) 切忌盲目遵循范式三原则,只可适当不可完全参照。范式定律只能被放在实验室。 3.3 mysql非结构性优化 mysql 非结构性的优化基本可参照mssql优化的基本原则。其过程也需要探索索引分布与碎片情况。在必要的时候进行索引重建和优化。索引的使用也必须遵循mysql的基本原则,例如最左前匹配原则。 3.4 mysql服务器优化 通常情况下,我们安装配置了服务器之后,mysql会产生默认的配置my.cnf 。其配置的原则是占用系统资源最小。所以默认的配置是完全不能满足需要的,这也是广大不熟悉mysql的用户对mysql的最大误解,这是不公平的。 为了使用强大的mysql,充分利用其优秀的高性能的数据处理能力。在正式使用之前必须对其进行深度定制。 1) max_connect_errors 连接异常数,默认10,建议10W次以上,也可以执行命令 flush hosts 或者重启mysqld服务。 2) interactive_timeout 处于交互连接状态的活动被服务器强制关 闭而等待的时间,单位秒,推荐设置:interactive_timeout = 100 wait_timeout = 100;也可以理解为 服务器关闭连接前等待其活动的秒数。默认值为28800s,8小时。需要同时设置上面两个参数值。 3) skip_external_locking 只对MyISAM起效,容易造成死锁,禁用。 4) innodb_adaptive_hash_index innodb 会根据数据访问的频繁程 度将数据缓存到内存中。 5) innodb_max_dirty_pages_pct 允许缓存中的数据量不实时刷回磁盘的百分比,建议在5%-90%之间,推荐值20。 6) innodb_commit_concurrency 从5.1.36开始mysql不允许将该值设置为非0,该值只能为默认值0,mysql不限制并发提交。 7) innodb_additional_mem_pool_size 用于存放Innodb 的字典信息和其他一些内部结构所需要的内存空间 推荐设置 15M。 8) innodb_buffer_pool_size 专用mysql服务器设置的大小:InnoDB使用缓冲池缓存索引和行数据,该值越大,磁盘IO越少,推荐设置为操作系统内存的80%最佳 9) back_log 当连接请求超过max_connections 的时候,允许放在堆栈中的最大连接请求数。根据请求数以及内存大小来决定,建议设置500.默认值为50. 10) key_buffer_size 索引快的缓冲区大小 ,如果MySQL主机内存为2G,参考设置为 400M。 11) max_connections 允许连接的最大客户端数,默认值为100,修改其值需要同步修改服务器能够打开的最大文件描述符的数量。linux 需要修改为65535,max_connections 推荐设置为 16384.其最大值为16384,如果超出此值,则以16384计算。该值的大小并不会占用太多系统资源,系统资源的占用主要取决于查询的密度和效率。 12) record_buffer_size 扫描表需要的缓冲区 ,默认数值是131072(128K),可改为16773120 (16M),内存不是很充足的情况下,设置为 4-8M。 13) read_rnd_buffer_size 随机读缓冲区大小 一般设置为 8M。 14) sort_buffer_size 排序缓冲区大小 默认值2M,可改为 4M。内存充足的情况下可设置为 16M。 15) table_cache 缓存表数量。 在资源足够的情况下尽可能缓存多的表。默认值为256,推荐设置为默认值。 16) max_heap_table_size 、 tmp_table_size ,内存中的每个临时表允许的最大大小,如果临时表超过该值,临时表将自动转为基于磁盘的表。设置为 50M。应尽量避免使用临时表。可能出现临时表的情况有使用union,同时使用order by 和group by 且字段不一致,同时使用order by 、group by 且后一个包含前一个字段。具体使用情况可参照 查询计划得出。默认为16M,可调到64-256最佳,线程独占,太大可能内存不够I/O堵塞 17) thread_cache_size 线程缓存大小,是mysql非常重要的参数,对于短连接来说效果非常明显,推荐设置为64. 参照内存设置。具体 1G —> 8 2G —> 16 3G —> 32 >4G —> 64 18) thread_concurrency 推荐设置为服务器核心数的2倍。 InnoDB 存储引擎的相关设置值参考 1 2 innodb_flush_log_at_trx_commit 建议设置为2 3 innodb_log_buffer_size 日志缓冲大小 默认1M,推荐4M。 4 innodb_log_file_size 每一个InnoDB事务日志的大小,一般默认为 innodb_buffer_pool_size 大小的 25%-100%,这里推荐位 50%; 5 innodb_thread_concurrency 推荐设置为 2*(NumCPUs+NumDisks),默认一般为8。 其他相关参数设置 1 开启MRR多路范围查询 show variables like '%optimizer_switch%' 查看是否开启了这个属性。 如果mrr=on,mrr_cost_based=off 则表示始终开启MRR优化。 也可以通过命令开启 mysql>set @@optimizer_switch='mrr=on,mrr_cost_based=off'; 第四部分 结束语 数据存储架构决定上层架构,直接影响编码结构的可扩展性和维护性以及保证系统整体性能。因此在具体决策时,必须谨慎斟酌,必须系统性的对现有架构进行编码层面的规模化调优和升级,对库表索引有针对性的进行优化,适当升级数据库管理软件版本,适当时候,可酌情对硬件进行规模化升级。可适当对查询延迟比较严重的业务进行拆分,即通过多重模式和查询来实现相同的逻辑,将大查询分解为小查询。在项目之初,还可以考虑对业务进行重新设计,以便适应后期的大规模查询请求。此时观察系统整体性能是否得到提升。 如果上述的方法没有让你的系统性能得到实质有效的提升,首先考虑的应该是对数据库表进行分区。将某些大表数据库文件在物理层面拆分为不同的数据文件存放,检索的时候只对需要的数据范围进行查询,降低数据库服务器压力。因为XX公司目前使用的是mssql数据库,因此本篇幅只讨论针对mssql数据库的分区操作,操作细节请见2.1章节。 如果分区还不能让你获得比较满意的性能,那么这时候就可以考虑数据分片了。 即使确定了数据库分片是架构发展的方向,也必须对数据库结构和业务模型进行详细透彻的分析,必须明确掌握业务范围和边界,必须预测在可见的未来业务发展的基本规律和大致规模。在此基础上仍然要透彻分析垂直分片和水平分片对业务系统的影响。 确定了分片技术之后,必须要面临一系列的问题。 1) 事务问题。 解决事务问题目前有两种方案: 分布式事务和通过应用程序和数据库共同控制实现事务。 方法一: 使用分布式事务。 优点: 交给数据库管理,简单方便。 缺点:性能代价高,特别是分片越来越多时。 方法二: 应用程序和数据库共同控制。 原理:将一个跨多个数据库的分布式事务分解成多个仅处于单个数据库节点上的小事务,并通过应用程序来控制各个小事务。 优点: 性能上有优势。 缺点: 需要应用程序在事务控制层上做灵活控制,如果使用了spring,事务改动起来会存在一定困难。 2) 跨节点join。 跨节点join的问题是数据库切分不可避免的问题。但是良好的设计和切分却可以减少此类问题的发生。通常的做法是分两次查询,在第一次查询的结果集中获取id,第二次查询根据第一次查询的id获取最终的结果集。 3) 跨节点的count,order by,group by以及聚合函数问题。 这一类问题都是需要基于全部数据进行计算。多数的代理并不会处理数据的合并,通常的做法是在程序里进行合并。由于可以并行查询,而且是小表,因此效率要比查询单一大表快很多。 数据库经过业务拆分及分库分表之后,虽然查询性能和 并发处理能力提高了,但也会带来一系列的问题。比如,原本跨表的事务上升为分布式事务;由于记录被切分到不同的库与不同的表当中,难以进行多表关联查询,并且不得不指定路由字段对数据进行查询。分库分表以后,如果需要对系统进行进一步扩容(路由策略变更),将变得非常不方便,需要重新进行数据迁移。 相较于MySQL的分库分表策略,后面要提到的HBase天生就能够很好地支持海量数据的存储,能够以更友好、更方便的方式支持表的分区,并且HBase还支持多个Region Server同时 写入,能够较为方便地扩展系统的并发写入能力。其搜索引擎技术,能够解决采用业务拆分及分库分表策略后,系统无法进行多表关联查询,以及查询时必须带路由字段的问题。搜索引擎能够很好地支持复杂条件的组合查询,通过搜索引擎构建的一张大表,能够弥补一部分数据库拆分所带来的问题。
XX公司服务器存储系统架构说明文档
最新推荐文章于 2021-06-14 20:14:39 发布