从“如何设计用户超过1亿的应用”说起---数据库调优实战本文章来自于阿里云云栖社区作为SaaS服务提供商,数万乃至数十万级用户是业务架构设计上一开始就必须面对的问题。庞大的用户群以及海量的用户数据意味着基础设施的构建必须兼顾高效与稳定,更经济、扩展更方便的云服务平台就成为了首选,如何基于云服务平台设计并实施符合自身业务特点的系统架构,也是决定产品性能的关键。...本文首发于阿里云&《程序员》杂志联合出品的《凌云》专刊中。作者:杭州湖畔网络技术经理王鑫鹏杭州湖畔网络技术有限公司是一家专业提供SaaS化电商ERP服务的创业公司,主要用户群体为经营淘宝、天猫、京东等主流电商平台、自建商城、线下渠道的商家及中小企业。作为SaaS服务提供商,服务数万乃至数十万级用户是业务架构初期就必须考虑的问题。庞大的用户群以及海量的用户数据意味着基础设施的构建必须兼顾高效与稳定,而按照通用的基础设施建设方案的话,需要面对成本过高、实现复杂、需要投入太多精力等问题,这对当时的湖畔网络这样的初创公司来说,完全不能承受。因此,更经济、更方便扩展的云服务平台成为首选。在对比现有各家云服务后,我们选择了稳定性与成熟度都经过大量用户检验的阿里云。但要构建高性能的SaaS应用,仅凭云服务基础设施是不够的。如何基于云服务平台设计并实施符合自身业务特点的系统架构,也是决定产品性能的关键。本文将讲述我们如何利用云服务,使用相对经济的方案,解决海量用户的数据库使用问题。架构我们的SaaS化电商ERP服务的整体架构是基于阿里云服务平台实施的,如图1所示。■采用SLB(ServerLoadBalance,负载均衡)作为Web集群访问入口,负责为Web端的多台服务器进行流量分发。SLB是基于集群建设的,并且可以随时变配,按量付费。它不仅为我们实现了成熟的负载均衡方案,其稳定性与灵活性也为Web集群提供了更多可能。■后端配置多台ECS(ElasticComputeService,云服务器)实例,将主要应用服务都部署在ECS上。除了可弹性扩容这一特性,ECS提供的安全防护和快照备份为服务器安全和容灾提供了非常成熟的解决方案,这恰恰是我们这种业务型创业团队积累相对最薄弱的方面。另外,ECS多线接入骨干网络能保证网络的稳定和性能,使得任何网络的用户访问应用服务都非常顺畅。■DB集群由多台RDS(RelationalDatabaseService,关系型数据库服务)实例组成。RDS是云数据库,简单易用,使用方法与自行部署的数据库完全一样。其成熟的双机热备与底层资源隔离,保证了我们这两年来数据库的平稳运行。另外,强大的iDBCloud控制台、专业的DBA团队支持,为我们监控数据库运行状况、定位和解决数据库问题,提供了非常多的建议和帮助。■集群之间的共享资源统一存放在OCS(OpenCacheService,开放缓存服务)中。我们用OCS来存放数据路由和实时性不高的业务数据。缓存作为我们架构性能中非常重要的一个环节,在承受了来自整个集群各方面压力的同时,还要保证响应稳定高速。通过该方案,不仅发挥了阿里云的优势(不涉及物理机器的维护和折损,灵活地配置升级,成熟的备份与快照方案),而且通过集群,避免了系统可能会遇到的单点故障,提高了系统弹性扩容的灵活性和可用性。作为一个SaaS化、数据更集中、数据体量庞大的企业应用,数据库是我们整体架构中的关键节点,如何保证其稳定与性能,是本文讲述的重点。当用户进入快速增长期后,随着业务量迅速增加,核心业务表的存量数据和增长速度绝对不是单个DB所能承受的(几乎所有单DB配置都存在性能物理上限瓶颈,即使选择升级配置也会受到成本和资源上限的约束)。因此,我们一开始就将数据库分库分片(Sharding)作为一个可行方案优先考虑,主要分析如下。■场景:业务热点数据持续增加,团队有一定的数据库架构积累能支撑独立研发或熟悉成熟的中间件(如Cobar)。■优点:成本低(可以利用开源免费的数据库集群替代大型商业DB);可灵活扩容(不断增加新的数据库切片即可);相对均匀分布的数据读写,避免单点障碍。■缺点:需要研发团队在数据库架构上投入大量精力;数据库集群维护需要成本投入。考虑到业务特性,我们最终采用了行业比较通用的水平拆分+垂直拆分策略,并自主完成DAO与JDBC之间的数据访问封装层开发工作。水平拆分:按用户将数据拆分到多个库的相同表中水平拆分的思路,就是将原本存放在单个RDS数据库中的数据,根据业务ID不同,拆分到多个数据库中(参见图2)。拆分后,各库的表数量及表结构都保持一致。水平拆分首先需要确立唯一的业务主表,即其他所有表的数据都与主表ID(前文所说的业务ID)存在直接或间接的主从关系,可以通过主表ID对全部数据做很好的切分。我们选择的业务主表为用户表,其他业务表或表的父表都包含一个用户ID。因此,我们切分的目标就是将不同用户数据存放到不同的数据库中。确定了拆分规则后,下一步是着手封装Sping数据访问封装层(DBWrapper)。DBWrapper介于DAO与JDBC之间,每个业务DAO进行数据库基本操作,都会经过DBWrapper。它的主要作用是将数据库架构的变化对业务层透明,业务层可以如同操作单个DB一样,调用DBWrapper提供的数据库操作接口,而判断操作哪个数据库的逻辑,则全部交由DBWrapper封装完成(参见图3)。DBWrapper主要提供新用户初始化和数据库操作接口。在新增用户初始化到系统时,需先动态判断系统各库的负载分布情况。粗略一点的算法就是判断各库的用户数,如共有4个库,可以根据user_id%4的情况决定目标库;再精细一点可以挖掘下核心业务数据的分布情况,具体分配算法需要基于业务设定(如考虑不同用户的平均订单量)。通过各库压力综合计算后,分析出压力最小的目标数据库,并将该新增用户数据存放到指定的目标库,同时更新路由信息(Router)。当用户完成初始化进行业务操作时,则需由业务层调用DBWrapper的操作接口。DBWrapper接收到请求后,会根据业务层传入的User_id匹配Router,判断最终需要操作的RDS实例和数据库。判断完成后,只需要按部就班地开连接执行就可以了。具体的代码实现,需要结合自身的持久层框架,找一名研究过持久层框架实现的开发人员即可完成。这样就将系统用户整体数据压力,相对均匀地分布到多个RDS实例与数据库上。事实证明,这确实是一个非常有效的方案,尤其是对于数据量大、增长迅猛的表。只是在后续实施过程中,我们发现有时会有单个用户的业务压力比较突出,针对这种情况,我们可以通过一些人工干预(如迁移数据到单独的库)进行微调,当然最终的解决方案还是要不断调优路由算法。切分后,不可避免地需要考虑数据字典(DD)和数据路由(Router)的处理。暂时我们采用的方法是将所有数据字典与路由放入独立的库,这也是后文中垂直拆分的一种应用。需要说明的是,数据库仅是这两个业务的一种实现方式,一般还可以通过或结合分布式缓存来处理这些业务(我们选用了OCS)。而对于可能出现的单点障碍,预留的扩展方案为水平拆分或创建只读节点(只读节点可以使用RDS最新提供的只读实例,目前还在内测阶段)。垂直拆分:按业务将表分组拆分到多个库中与水平拆分相比,垂直拆分要更简单一些。其基本思路就是将存放在单个数据库的表分组,把其中业务耦合度较高、联系紧密的表分为一组,拆分到其他DB中(参见图4)。拆分后,各库的表结构及其业务意义将完全不同。虽然规则简单、实施方便,但垂直拆分总是需打断些关联,因为实际操作中,基础资源常常出现在各个业务场景,在切分时又不得不切分到两个库中,此时就需要业务层多次查询后,在内存处理数据,实现数据库Join的效果。垂直拆分同样需要DBWrapper,但封装规则与水平拆分略有不同,需要针对不同的业务,建立不同的DBWrapper。此时不再是完全业务层无感知,需要业务层根据业务场景有针对性使用。单个DBWrapper的实现与水平拆分一致。垂直拆分的好处在于,将整体业务数据切分成相对独立的几块,隔离了不同业务之间的性能影响。而由于拆分后的数据库业务比较集中,也更容易找到业务主表,更有利于水平拆分。对于垂直拆分,目前我们主要用于解决数据路由(包含了用户的基本信息)、数据字典模块,以及常见的冷数据问题。冷数据的处理一直是行业的常见问题(其实对于冷数据的划分,也是水平拆分),目前我们采用的方案是集中存储,即按自己的冷数据切分方式,通过自行开发的迁移程序将判定的冷数据增量迁移到一个库中。这个方案既能够分离冷数据对热点数据的操作影响,也可以为大数据的挖掘提供比较便利的条件。使用相对独立的冷数据存储结构,能方便以后采用更高效、成本更低廉的存储介质。当然该方案存在一些潜在问题,如果冷数据库满了该怎么办?目前我们预留的设计方案是,历史库的水平拆分,也可以考虑其他存储形式。水平拆分与垂直拆分组合使用拆分一直是数据库优化的关键词(无论是库表结构还是SQL写法),它是每个高并发产品最终都要经历的一步。拆分方案的核心主要在于可以通过添加更多RDS实例和数据库(常常为了节约成本,多个数据库可以部署在一个RDS实例上),灵活扩容系统的负载能力。在数据库架构中,水平拆分和垂直拆分一般都是搭配使用的,两者的先后顺序视具体情况而定。一般而言,垂直拆分更容易,也可以为水平拆分做铺垫,一是业务集中,便于提取主表,二是垂直拆分后,可以只水平拆分压力高的表,而业务增长缓慢的表则可以保留单DB,从而提高拆分效率以及降低实施成本(参见图5)。我们之所以优先水平拆分,主要原因还是成本和效率及当时的一些局限性。只按业务ID(用户)做好路由配置,这样各个库中的结构完全一致,保留了原本的业务逻辑与实现,避免了跨库关联,能大大节省实现成本。尽管拆分有种种好处,但由于分布式事务及跨库Join的实现复杂度较高及可用性较差,所以分布式事务一般都通过业务层使用乐观锁控制。而跨库的表间关联一定要打断,否则性能和实现复杂度都会超出可接受范围。对于跨库的Join、Groupby等问题,都需要在业务层处理。目前我们采用的是分批查询,在业务层组装结果的方式。有些遗憾的是,由于我们早期使用RDS时,阿里云尚未推出DRDS(分布式数据库)产品,所以上述拆分的数据库底层架构均是由我们自行研发的,投入了大量的精力。而现在有了DRDS,正准备做拆分的团队,则无需再自己造轮子,直接拿来用即可,这样团队可以将更多的精力放在业务上。小处大有可为虽然我们在架构上做了优化,但在产品发展过程中还是会出现性能不太理想的情况。在阿里云支持中心和论坛上,也可以看到其他业务型团队反馈使用RDS时遇到类似的情况。最初大家都怀疑是不是RDS的底层资源隔离有问题,多个用户共享资源时发生争抢,导致RDS的性能问题。但在阿里云DBA的指导和协助下,发现是由于产品设计时对数据库的使用太“不拘小节”,而随着并发压力与数据量增加,大量细小的性能问题被放大,集中暴露出来。解决灯下黑:修正业务层的数据库操作陋习■场景:数据库性能有问题,应优先从业务层分析。■优点:减轻数据库的直接压力,比执行数据库优化方案更加迅速有效。■缺点:业务研发需要关注一些数据库操作内容;有时会牺牲一些业务;产品规模越大实施越困难。在数据库的优化过程中,研发团队最容易忽视的往往是业务层中的数据库使用。一些优化方案可以作为开发的常态化准则。下面仅列举几个常用的优化方案。■延迟加载。很多页面展现时,单个实体实际只展现部分内容,因此可按需加载,减轻数据库压力,又节省网络流量。延迟加载也可体现在数据库表的拆分设计上。■适当缓存,以空间换时间。对于很多实时性较低或干脆就是数据字典的内容,无需实时到数据库中加载,只需在使用前加载到内存中,实际使用时到内存中获取即可。■减少不必要的开连接(连接池、批量查询及提交)。对于大部分的Web应用,连接池大大减少了系统因开数据库连接产生的开销。而在查询和提交中,将多个任务合并到一次数据库操作中,也可以大大提高数据库使用效率。■乐观锁是高并发下不错的解决方式。相比