前言
作为后端开发应该对整体系统架构有一定了解。所以需要学习有关软件系统架构知识。我采用读书的方式去了解整体软件系统架构,所读书名《从零开始学架构》。
学习目标:
1.架构设计目的及复杂度来源
2.架构设计流程
3.高性能架构
4.CAP理论和FMEA方法
5.高可用架构
6.可扩展架构
7.微服务架构最佳实践
8.互联网架构技术
写此博客的目的:
1.完成学习目标
2.对书中内容进行总结,得到自己的阅读心得
3.方便其他入门小伙伴快速得到干货
4.方便自己回顾架构知识
前几期帖子:
【架构学习(一)】架构设计目的及复杂度来源
【架构学习(二)】架构设计流程
一、高性能DB集群
读写分离
什么是高性能数据库集群两大特点?
“读写分离”,分散存储压力;
“分库分表”,分散访问压力和和存储压力。
什么是读写分离适用场景呢?
适用场景:读多写少,举例:消息记录业务,比如微博评论。
不适合场景:及时性高的业务,比如登录注册(及时性和准确性高的业务可以采用直接读主库,准确性不高可以引入缓存设计)。
读写分离的基本实现?
数据库服务器搭建主从集群,一主一从、一主多从都可以。
1)数据库主机负责读写操作,从机只负责读操作。
2)数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
3)业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
读写分离,既然分散了读写压力,那这种设计的复杂度体现在?
复制延迟
主从复制延迟可能达到1秒,如果有大量数据同步,延迟1分钟也是有可能的。主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻(1秒内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。这可能有些实时业务场景要求读主库。
如何解决复制延迟问题?
解决主从复制延迟有几种常见的方法:
1.读从机失败后再读一次主机
这就是通常所说的“二次读取”,二次读取和业务无绑定,只需要对底层数据库访问的API进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。
2.关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册+登录的业务读写操作全部访问主机,用户的介绍、爱好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
分配机制
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
1.程序代码封装
程序代码封装指在代码中抽象一个数据访问层,实现读写操作分离和数据库服务器连接的管理。个人理解就是在程序与mysql交互的时候,是通过代码(较为底层方法操作jdbc)编写。
2.中间件封装
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供SQL兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。(万事不觉就加一层)
举例:
订餐系统,在高峰时期,操作数据库很频繁。但当前mysql只有一台,用户需要去下单订餐,而后台管理人员也需要看当前系统的事实情况。出现磁盘IO报警问题。
需求分析:
需要解决磁盘IO报警,应对未来预期的业务需求增长。
解决方法:
读写分离,用户侧全部读写采用主库,后台管理人员写走走主库,读走从库。
分库分表
分库分表分散存储压力
主要体现在这几个方面:
数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
数据文件越大,极端情况下丢失数据的风险越高。(这也是为什么要求数据库为什么会有几千万归档)
单台服务器性能怎么样?
单台mysql服务器能够支撑10w用户量级查询,当单台mysql服务带到60%承载能力就需要考虑分库(正常mysql cpu到40%就会报警)
分库分表的总体思路是什么样的?
空间-》时间
单表-》多表同库-》分库
业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。
1.join查询问题,跨库不能使用join查询
2.事务问题,同库不同表可以使用事务,分布式事务性能低不推荐使用
3.成本问题,增加机器
分表方案
单表数据拆分有两种方式:垂直分表和水平分表。
为了形象地理解垂直拆分和水平拆分的区别,可以想象你手里拿着一把刀,面对一个蛋糕切一刀:
1)从上往下切就是垂直切分,因为刀的运行轨迹与蛋糕是垂直的,这样可以把蛋糕切成高度相等(面积可以相等也可以不相等)的两部分,对应到表的切分就是表记录数相同但包含不同的列。
2)从左往右切就是水平切分,因为刀的运行轨迹与蛋糕是平行的,这样可以把蛋糕切成面积相等(高度可以相等也可以不相等)的两部分,对应到表的切分就是表的列相同但包含不同的行数据。
1.垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。
垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取的字段,现在需要两次查询。
2.水平分表
水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:
1)范围路由:选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。以最常见的用户ID为例,路由算法可以按照1000000的范围大小进行分段.范围路由的缺点就是数据分配不均。
2)Hash路由:选取某个列(或者某几个列组合也可以)的值进行Hash运算,然后根据Hash结果分散到不同的数据库表中。同样以用户ID为例,假如我们一开始就规划了10个数据库表,路由算法可以简单地用user_id % 10的值来表示数据所属的数据库表编号,ID为985的用户放到编号为5的子表中,ID为10086的用户放到编号为6的字表中
hash路由的缺点就是增加新的表数据需要重新分配(存在数据迁移问题,头大了)
3)配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。同样以用户ID为例,我们新增一张user_router表,这个表包含user_id和 table_id 两列,根据user_id就可以查询对应的 table_id。
配置路由的缺点就是必须多查询一次,会影响整体性能。
4)join 操作
水平分表后,数据分散在多个表中,如果需要与其他表进行join查询需要在业务代码或者数居库中间件中进行多次join查询,然后将结果合并。
5)count()操作
count()相加:具体做法是在业务代码或者数据库中间件中对每个表进行count()操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低。例如,水平分表后切分为20张表,则要进行20次count(*)操作,如果串行的话,可能需要几秒钟才能得到结果。
记录数表︰具体做法是新建一张表,假如表名为“记录数表”,包含table_name、row_count两个字段,每次插入或者删除子表数据成功后,都更新“记录数表”,但需要每次更新业务表的同时更新记录表对数据库有压力。
6)order by 操作
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。(这点我想到了es)
二、NoSQL
nosql解决的是sql以下问题
1)关系数据库存储的是行记录,无法存储数据结构。
2)关系数据库的schema扩展很不方便,比如操作不存在列会报错,扩展需要ddl。
3)关系数据库的全文搜索功能比较弱,like扫表性能非常低(感觉这条才是需要nosql的核心)。
常见的NoSQL方案分为4类。
1)K-V存储:解决关系数据库无法存储数据结构的问题,以 Redis为代表。
2)文档数据库:解决关系数据库强schema约束的问题,以MongoDB为代表。
3)列式数据库:解决关系数据库大数据场景下的I/O问题,以 HBase为代表。
4)全文搜索引擎:解决关系数据库的全文搜索性能问题,以Elasticsearch为代表。
K-V存储
K-V存储的全称是Key-Value存储,其中Key是数据的标识,和关系数据库中的主键含义一样,Value 就是具体的数据。
Redis是K-V存储的典型代表,它是一款开源(基于BSD许可)的高性能K-V缓存和存储系统。Redis的 Value是具体的数据结构,包括string、hash、list、set、sorted set、bitmap和hyperloglog,所以常常被称为数据结构服务器。
Redis的缺点主要体现在并不支持完整的ACID事务,Redis虽然提供事务功能,但Redis的事务和关系数据库的事务不可同日而语,RedliS的事务不能保证原子性和持久性(A和D)(可以理解redis的事务就是一个脚本,当命令执行失败了,其他命令会继续执行)。
文档数据库
为了解决关系数据库schema带来的问题,文档数据库应运而生。文档数据库最大的特点就是no-schema,可以存储和读取任意的数据。目前绝大部分文档数据库存储的数据格式是JSON(或者BSON),因为JSON数据是自描述的,无须在使用前定义字段,读取一个JSON中不存在的字段也不会导致SQL那样的语法错误。
1.新增字段简单
业务上增加新的字段,无须再像关系数据库一样要先执行DDL语句修改表结构,程序代码直接读写即可。
2.历史数据不会出错
对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值。
文档数据库no-schema的特性带来的这些优势也是有代价的,最主要的代价就是不支持事务,无法实现关系数据库的join操作。
列式数据库
列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”,因为关系数据库是按照行来存储数据的。
关系数据库按照行式来存储数据,主要有以下几个优势:
业务同时读取多个列时效率高,因为这些列都是按行存储在一起的,一次磁盘操作就能够把一行数据中的各个列都读取到内存中。
能够一次性完成对一行中的多个列的写操作,保证了针对行数据写操作的原子性和一致性;否则如果采用列存储,可能会出现某次写操作,有的列成功了,有的列失败了,导致数据不一致。
全文搜索引擎
传统的关系型数据库通过索引来达到快速查询的目的,但是在全文搜索的业务场景下,索引也无能为力,主要体现在:
1)全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量会非常多。
2)全文搜索的模糊匹配方式,索引无法满足,只能用like查询,而like查询是整表扫描,效率非常低。
1.全文搜索基本原理(就是es的原理)
全文搜索引擎的技术原理被称为“倒排索引”(Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,其基本原理是建立单词到文档的索引。之所以被称为“倒排”索引,是和“正排“索引相对的,“正排索引”的基本原理是建立文档到单词的索引。我们通过一个简单的样例来说明这两种索引的差异。
2.全文搜索的使用方式
全文搜索引擎的索引对象是单词和文档,而关系数据库的索引对象是键和行,两者的术语差异很大,不能简单地等同起来。因此,为了让全文搜索引擎支持关系型数据的全文搜索,需要做一些转换操作,即将关系型数据转换为文档数据。
目前常用的转换方式是将关系型数据按照对象的形式转换为JSON文档,然后将JSON文档输入全文搜索引擎进行索引。我同样以程序员的基本信息表为例,看看如何转换。
总结
业务存储用db,减少db压力用redis,快速搜索用es
三、高性能缓存架构
缓存穿透
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:
1.存储数据不存在
第一种情况是被访问的数据确实不存在。一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。
通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。
这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
(这里其实查询这种还是没法设置默认值,理解这回导致缓存和db的不一致。理解恶意攻击数据库也是通过接口来攻击的,最好还是有人恶意攻击数据库的时候,我们应该关注接口,qps等指标和mysql指标,做好降级限流操作)
2.缓存数据生成耗费大量时间或者资源
第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
缓存雪崩
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制。
1.更新锁
必须需要分布式锁来控制更新,因为多台机器还是有可能同时更新缓存。
2.后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。(通过job或者mq去操作)
后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从
缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:
1)后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1秒或者100毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。
2)业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。(这种思路牛)
缓存热点
虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都请求同一个key,会出现性能下降问题。
1)锁竞争
某些数据结构访问时会加锁
2)内存访问
当大部分请求都集中在少数几个key上时,这些key的数据可能会频繁地在CPU缓存和主内存之间移动,这会导致缓存未命中率增加,进而降低Redis的性能。(这里指的缓存未命中值得是linux机器的l1,l2,l3缓存,与redis进行交互,因为热点key并不连续,造成cpu缓存利用率低)
缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。(我理解就是将一个key,变为多个key,在key后面做编号,通过简单的%或者hash去匹配)
缓存热点问题写的好的帖子:https://zhuanlan.zhihu.com/p/696632931
总结
存储这块的复杂度优化都是围绕着db来做的,当请求量上升先对db进行优化,优化方案有读写分离,分库分表。当db不满足业务需求需要对热点数据快速访问可采用缓存机制,但增加复杂度的同时也需要注意缓存热点,穿透,击穿以及雪崩问题。当出现大量模糊查询时,我们也可以采用全文搜索数据库去配合db搜索。在db优化中可以看出多出使用了消息中间件,定时任务等保证业务的可用连贯性。
我的目标
希望在年底学习一下内容:
java学习内容:
1.tomcat源码
2.dubbo源码
3.zookeeper源码
4.netty源码
go学习内容:
1.gin框架学习
2.简单go项目
3.go基础知识进阶(gmp,gc,channel,map,slice源码等)
中间件学习内容:
1.kafka使用及源码
框架学习内容:
1.从零开始学架构
算法学习内容:
1.复习leetcode中top 100
我平时喜欢没事还打游戏,因为有宝宝所以希望在平时时间能尽量完成上述学习内容(希望能戒掉游戏哈哈哈)。
也希望有和我一样的一起学习的小伙伴共同学习进步,我建一个qq后端交流群:279868576,希望小伙伴们加入共同督促进步。