DevOps + 架构一

一、DevOps

1定义

开发,测试,运维,甚至运营。出一个东西,就能马上走完一个流程,看效果
软件交付的速度和质量很重要

二、架构

1、解决系统复杂度

首先找到系统复杂度在哪里。
经典案例:

假设我们需要设计一个大学的学生管理系统,其基本功能包括登录、注册、成绩管理、课程管理等。当我们对这样一个系统进行架构设计的时候,首先应识别其复杂度到底体现在哪里。
性能:一个学校的学生大约 1 ~ 2 万人,学生管理系统的访问频率并不高,平均每天单个学生的访问次数平均不到 1 次,因此性能这部分并不复杂,存储用 MySQL 完全能够胜任,缓存都可以不用,Web 服务器用 Nginx 绰绰有余。
可扩展性:学生管理系统的功能比较稳定,可扩展的空间并不大,因此可扩展性也不复杂。
高可用:学生管理系统即使宕机 2 小时,对学生管理工作影响并不大,因此可以不做负载均衡,更不用考虑异地多活这类复杂的方案了。但是,如果学生的数据全部丢失,修复是非常麻烦的,只能靠人工逐条修复,这个很难接受,因此需要考虑存储高可靠,这里就有点复杂了。我们需要考虑多种异常情况:机器故障、机房故障,针对机器故障,我们需要设计 MySQL 同机房主备方案;针对机房故障,我们需要设计 MySQL 跨机房同步方案。
安全性:学生管理系统存储的信息有一定的隐私性,例如学生的家庭情况,但并不是和金融相关的,也不包含强隐私(例如玉照、情感)的信息,因此安全性方面只要做 3 个事情就基本满足要求了:Nginx 提供 ACL 控制、用户账号密码管理、数据库访问权限控制。
成本:由于系统很简单,基本上几台服务器就能够搞定,对于一所大学来说完全不是问题,可以无需太多关注。
对应的架构如下:

在这里插入图片描述

1) 性能

a 单机的复杂度

计算机内部复杂度最关键的地方就是操作系统。计算机性能的发展本质上是由硬件发展驱动的,尤其是 CPU 的性能发展。著名的“摩尔定律”表明了 CPU 的处理能力每隔 18 个月就翻一番;而将硬件性能充分发挥出来的关键就是操作系统,所以操作系统本身其实也是跟随硬件的发展而发展的,操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度。

操作系统和性能最相关的就是进程和线程。最早的计算机其实是没有操作系统的,只有输入、计算和输出功能,用户输入一个指令,计算机完成操作,大部分时候计算机都在等待用户输入指令,这样的处理性能很显然是很低效的,因为人的输入速度是远远比不上计算机的运算速度的。

为了解决手工操作带来的低效,批处理操作系统应运而生。批处理简单来说就是先把要执行的指令预先写下来(写到纸带、磁带、磁盘等),形成一个指令清单,这个指令清单就是我们常说的“任务”,然后将任务交给计算机去执行,批处理操作系统负责读取“任务”中的指令清单并进行处理,计算机执行的过程中无须等待人工手工操作,这样性能就有了很大的提升。

批处理程序大大提升了处理性能,但有一个很明显的缺点:计算机一次只能执行一个任务,如果某个任务需要从 I/O 设备(例如磁带)读取大量的数据,在 I/O 操作的过程中,CPU 其实是空闲的,而这个空闲时间本来是可以进行其他计算的。

为了进一步提升性能,人们发明了“进程”,用进程来对应一个任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。此时的 CPU 还没有多核和多线程的概念,为了达到多进程并行运行的目的,采取了分时的方式,即把 CPU 的时间分成很多片段,每个片段只能执行某个进程中的指令。虽然从操作系统和 CPU 的角度来说还是串行处理的,但是由于 CPU 的处理速度很快,从用户的角度来看,感觉是多进程在并行处理。

多进程虽然要求每个任务都有独立的内存空间,进程间互不相关,但从用户的角度来看,两个任务之间能够在运行过程中就进行通信,会让任务设计变得更加灵活高效。否则如果两个任务运行过程中不能通信,只能是 A 任务将结果写到存储,B 任务再从存储读取进行处理,不仅效率低,而且任务设计更加复杂。为了解决这个问题,进程间通信的各种方式被设计出来了,包括管道、消息队列、信号量、共享存储等。

多进程让多任务能够并行处理任务,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。例如,一个餐馆管理进程,排位、点菜、买单、服务员调度等子任务必须能够并行处理,否则就会出现某个客人买单时间比较长(比如说信用卡刷不出来),其他客人都不能点菜的情况。为了解决这个问题,人们又发明了线程,线程是进程内部的子任务,但这些子任务都共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。

多进程多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到时间上真正的并行。解决这个问题的方式显而易见,就是让多个 CPU 能够同时执行计算任务,从而实现真正意义上的多任务并行。目前这样的解决方案有 3 种:SMP(Symmetric Multi-Processor,对称多处理器结构)、NUMA(Non-Uniform Memory Access,非一致存储访问结构)、MPP(Massive Parallel Processing,海量并行处理结构)。其中 SMP 是我们最常见的,目前流行的多核处理器就是 SMP 方案。

操作系统发展到现在,如果我们要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂。举一个最简单的例子:Nginx 可以用多进程也可以用多线程,JBoss 采用的是多线程;Redis 采用的是单进程,Memcache 采用的是多线程,这些系统都实现了高性能,但内部实现差异却很大。

b 集群

以下是任务分配器和任务分解,每句话都是重点
在这里插入图片描述
在这里插入图片描述

2) 高可用–(无中断)

1、高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。

通俗点来讲,就是一台机器不够就两台,两台不够就四台;一个机房可能断电,那就部署两个机房;一条通道可能故障,那就用两条,两条不够那就用三条(移动、电信、联通一起上)

2、数据+逻辑=业务。数据不一致,业务就会不一样
存储高可用的难点不在于如何备份数据,而在于如何减少或规避数据不一致对业务造成的影响
3、高可用决策状态分三种,独裁,主备和选举(会发生脑裂,为了防止这个现象,要求过半数。但是5个节点,三个节点坏了,也不会选出主节点。)
没有决策状态是完美的,这要看具体使用场景来做取舍

3)可扩展性

1、要预测变化

新需求能够不改代码甚至少改代码就可以实现,那当然是皆大欢喜的,否则来一个需求就要求系统大改一次,成本会非常高,程序员心里也不爽(改来改去),产品经理也不爽(做得那么慢),老板也不爽(那么多人就只能干这么点事)。因此作为架构师,我们总是试图去预测所有的变化,然后设计完美的方案来应对,当下一次需求真正来临时,架构师可以自豪地说:这个我当时已经预测到了,架构已经完美地支持,只需要一两天工作量就可以了!

2、封装变化
将变化封装到“变化层”,不会的封装到稳定层

4)低成本、安全、规模

1、考虑到低成本,有时加机器太费钱,需要创新或者引入新技术来解决问题
2、安全

  1. 功能安全–防小偷
    很多框架都自带了防守(常见的 XSS 攻击、CSRF 攻击、SQL 注入等)

XSS攻击是指攻击者在网页中注入恶意的客户端脚本(通常是JavaScript),当其他用户访问该网页时,这些脚本会在用户的浏览器中执行。XSS可以用来窃取用户的会话Cookie、篡改网页内容、进行钓鱼攻击等。
CSRF(跨站请求伪造)攻击者可以执行诸如转账、购买商品、更改用户设置等操作,冒充受害者的身份

2)架构安全–防强盗,防破坏。 防DDoS攻击,DDoS 攻击最大的影响是大量消耗机房的出口总带宽。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流

DDos攻击是一种试图通过大量恶意流量或请求来使目标服务器、服务或网络资源无法正常工作或崩溃的攻击。攻击者利用多个分布在不同位置的计算机(通常是被感染的僵尸网络)向目标发送大量请求,耗尽其资源,使合法用户无法访问服务。

互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
3、规模
MySQL 单表的数据因不同的业务和应用场景会有不同的最优值,但不管怎样都肯定是有一定的限度的,一般推荐在 5000 万行左右。再多就要拆表了

2、架构设计三原则

taobao最早是通过买,然后业务量上去后,买不行后,通过开发自己的系统来降低基础设施运营成本,必须根据自己的场景订制了。
qq是用户规模先上去,产生各种问题,倒逼技术架构升级
1)合适 优于业界领先

回到我前面提到的“亿级用户平台”失败的例子,分析一下原因。没有腾讯那么多的人(当然钱差得更多),没有 QQ 那样海量用户的积累,没有 QQ 那样的业务,这个项目失败其实是在一开始就注定的。注意这里的失败不是说系统做不出来,而是系统没有按照最初的目标来实现,上面提到的 3 个失败原因也全占了。
所以,真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。这也是很多 BAT 出来的架构师到了小公司或者创业团队反而做不出成绩的原因,因为没有了大公司的平台、资源、积累,只是生搬硬套大公司的做法,失败的概率非常高。

2)简单 功能少

团队的压力有时也会有意无意地促进我们走向复杂的方向,因为大部分人在评价一个方案水平高低的时候,复杂性是其中一个重要的参考指标。例如设计一个主备方案,如果你用心跳来实现,可能大家都认为这太简单了。但如果你引入 ZooKeeper 来做主备决策,可能很多人会认为这个方案更加“高大上”一些,毕竟 ZooKeeper 使用的是 ZAB 协议,而 ZAB 协议本身就很复杂。其实,真正理解 ZAB 协议的人很少(我也不懂),但并不妨碍我们都知道 ZAB 协议很优秀。

3)演化
演化优于一步到位
考虑到软件架构需要根据业务发展不断变化这个本质特点,软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大

设计 Windows 和 Android 的人都是顶尖的天才,即便如此,他们也不可能在 1985 年设计出 Windows 8,不可能在 2009 年设计出 Android 6.0。

3、架构设计流程

1)识别复杂度

整个项目流量流程是怎么样的?数据+逻辑 选型,平台选择,单机房还是双机房
判断复杂度在哪里,将来会怎么增长

2)设计备选方案(有完整weibo方案)

成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。

虽然软件技术经过几十年的发展,新技术层出不穷,但是经过时间考验,已经被各种场景验证过的成熟技术其实更多。例如,高可用的主备方案、集群方案,高性能的负载均衡、多路复用,可扩展的分层、插件化等技术,绝大部分时候我们有了明确的目标后,按图索骥就能够找到可选的解决方案。
只有当这种方式完全无法满足需求的时候,才会考虑进行方案的创新,而事实上方案的创新绝大部分情况下也都是基于已有的成熟技术。

备选方案的数量以 3 ~ 5 个为最佳。

少于 3 个方案可能是因为思维狭隘,考虑不周全;多于 5 个则需要耗费大量的精力和时间,并且方案之间的差别可能不明显。

备选方案的差异要比较明显。

例如,主备方案和集群方案差异就很明显,或者同样是主备方案,用 ZooKeeper 做主备决策和用 Keepalived 做主备决策的差异也很明显。但是都用 ZooKeeper 做主备决策,一个检测周期是 1 分钟,一个检测周期是 5 分钟,这就不是架构上的差异,而是细节上的差异了,不适合做成两个方案。

备选方案的技术不要只局限于已经熟悉的技术。

设计架构时,架构师需要将视野放宽,考虑更多可能性。很多架构师或者设计师积累了一些成功的经验,出于快速完成任务和降低风险的目的,可能自觉或者不自觉地倾向于使用自己已经熟悉的技术,对于新的技术有一种不放心的感觉。就像那句俗语说的:“如果你手里有一把锤子,所有的问题在你看来都是钉子”。例如,架构师对 MySQL 很熟悉,因此不管什么存储都基于 MySQL 去设计方案,系统性能不够了,首先考虑的就是 MySQL 分库分表,而事实上也许引入一个 Memcache 缓存就能够解决问题。

备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显。

3)评估和选择备选方案

常见的方案质量属性点有:开发周期、性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则 1“合适原则”和原则 2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。
完成方案的 360 度环评后,我们可以基于评估结果整理出 360 度环评表,一目了然地看到各个方案的优劣点。
正确的做法是按优先级选择

即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。那会不会出现两个或者多个方案,每个质量属性的优缺点都一样的情况呢?理论上是可能的,但实际上是不可能的。前面我提到,在做备选方案设计时,不同的备选方案之间的差异要比较明显,差异明显的备选方案不可能所有的优缺点都是一样的。

在这里插入图片描述

4) 详细方案设计

例如,Nginx 的负载均衡策略,简单按照下面的规则选择就可以了。

轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器分配的请求数基本一致,如果后端服务器“down 掉”,能自动剔除。

加权轮询
根据权重来进行轮询,权重高的服务器分配的请求更多,主要适应于后端服务器性能不均的情况,如新老服务器混用。

ip_hash
每个请求按访问 IP 的 hash 结果分配,这样每个访客固定访问一个后端服务器,主要用于解决 session 的问题,如购物车类的应用。

fair
按后端服务器的响应时间来分配请求,响应时间短的优先分配,能够最大化地平衡各后端服务器的压力,可以适用于后端服务器性能不均衡的情况,也可以防止某台后端服务器性能不足的情况下还继续接收同样多的请求从而造成雪崩效应。

url_hash
按访问 URL 的 hash 结果来分配请求,每个 URL 定向到同一个后端服务器,适用于后端服务器能够将 URL 的响应结果缓存的情况。

这几个策略的适用场景区别还是比较明显的,根据我们的业务需要,挑选一个合适的即可。例如,比如一个电商架构,由于和 session 比较强相关,因此如果用 Nginx 来做集群负载均衡,那么选择 ip_hash 策略是比较合适的。

架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。

例如,架构师选择了 Elasticsearch 作为全文搜索解决方案,前提必须是架构师自己对 Elasticsearch 的设计原理有深入的理解,比如索引、副本、集群等技术点;而不能道听途说 Elasticsearch 很牛,所以选择它,更不能成为把“细节我们不讨论”这句话挂在嘴边的“PPT 架构师”。

4、高性能数据库集群

1)读写分离

将读和写分散到不同的节点上。业务服务器将写操作发给数据库主机,将读操作发给数据库从机。

在这里插入图片描述

需要注意的是,这里用的是“主从集群”,而不是“主备集群”。“从机”的“从”可以理解为“仆从”,仆从是要帮主人干活的,“从机”是需要提供读数据的功能的;而“备机”一般被认为仅仅提供备份功能,不提供访问功能。所以使用“主从”还是“主备”,是要看场景的,这两个词并不是完全等同的。

复杂度

1、主从复制延迟

主从复制延迟可能达到1秒甚至1分钟。如果用户注册后马上登录,会发生 “你还没有注册”

解决方案

  1. 写操作后的读操作指定发给数据库主服务器

例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个 bug。

  1. 读从机失败后再读一次主机

这就是通常所说的“二次读取”,二次读取和业务无绑定,只需要对底层数据库访问的 API 进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。

  1. 关键业务读写操作全部指向主机,非关键业务采用读写分离

例如,对于一个用户管理系统来说,注册 + 登录的业务读写操作全部访问主机,用户的介绍、爱好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。

实现
使用数据库中间件或代理(如MySQL Router、MaxScale、Mycat)来自动处理读写分离,简化应用程序逻辑。
在这里插入图片描述
也可以 在代码中抽象一个数据访问层(所以有的文章也称这种方式为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。
在这里插入图片描述

2)分库分表

读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的
存储能力会成为系统的瓶颈,主要体现在这几个方面:
数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。

a 业务分库

比如将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上

对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因:
初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。

业务分库后,表之间的 join 查询、数据库事务无法简单实现了。

业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏。

单台数据库服务器的性能其实也没有想象的那么弱,一般来说,单台数据库服务器能够支撑 10 万用户量量级的业务,初创业务从 0 发展到 10 万级用户,并不是想象得那么快。

b 分表

同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。切表可以横切和竖着切.分表能够有效地分散存储压力和带来性能提升
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。

例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。

水平分表适合表行数特别大的表
当看到表的行数达到千万级别时,作为架构师就要警觉起来

实现方法

和数据库读写分离类似,分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断 SELECT、UPDATE、INSERT、DELETE 几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。例如 order by 操作,需要先从多个库查询到各个库的数据,然后再重新 order by 才能得到最终的结果。

5、高性能NoSQL

1) 关系型数据库的缺点

1、“我关注的人”是一个用户 ID 列表,使用关系数据库存储只能将列表拆成多行,然后再查询出来组装,无法直接存储一个列表。
2、关系数据库的表结构 schema 是强约束,操作不存在的列会报错,业务变化时扩充列也比较麻烦,需要执行 DDL(data definition language,如 CREATE、ALTER、DROP 等)语句修改,而且修改时可能会长时间锁表(例如,MySQL 可能将表锁住 1 个小时)。
3、如果对一些大量数据的表进行统计之类的运算,关系数据库的 I/O 会很高,因为即使只针对其中某一列进行运算,关系数据库也会将整行数据从存储设备读入内存。
4、关系数据库的全文搜索只能使用 like 进行整表扫描匹配,性能非常低,在互联网这种搜索复杂的场景下无法满足业务要求。

Solution,使用Not only SQL数据库,但是它们不支持完整的 ACID 事务。用于不需要特别支持事务的场景
a、K-V 存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表。

Redis 的 Value 是具体的数据结构,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog

b、文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表。

1新增字段简单
2对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值,此时代码进行兼容处理即可。
3 JSON 是一种强大的描述语言,能够描述复杂的数据结构。例如,我们设计一个用户管理系统,用户的信息有 ID、姓名、性别、爱好、邮箱、地址、学历信息。其中爱好是列表(因为可以有多个爱好);地址是一个结构,包括省市区楼盘地址;学历包括学校、专业、入学毕业年份信息等。如果我们用关系数据库来存储,需要设计多张表,包括基本信息(列:ID、姓名、性别、邮箱)、爱好(列:ID、爱好)、地址(列:省、市、区、详细地址)、学历(列:入学时间、毕业时间、学校名称、专业),而使用文档数据库,一个 JSON 就可以全部描述。
文档数据库的这个特点,特别适合电商和游戏这类的业务场景。以电商为例,不同商品的属性差异很大。例如,冰箱的属性和笔记本电脑的属性差异非常大

c、列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表。

计算某个城市体重超重的人员数据,实际上只需要读取每个人的体重这一列并进行统计即可,而行式存储即使最终只使用一列,也会将所有行数据都读取出来。如果单行用户信息有 1KB,其中体重只有 4 个字节,行式存储还是会将整行 1KB 数据全部读取到内存中,这是明显的浪费。而如果采用列式存储,每个用户只需要读取 4 字节的体重数据即可,I/O 将大大减少。

除了节省 I/O,列式存储还具备更高的存储压缩比,能够节省更多的存储空间。普通的行式数据库一般压缩率在 3:1 到 5:1 左右,而列式数据库的压缩率一般在 8:1 到 30:1 左右,因为单个列的数据相似度相比行来说更高,能够达到更高的压缩率。

如果场景发生变化,列式存储的优势又会变成劣势。典型的场景是需要频繁地更新多个列。因为列式存储将不同列存储在磁盘上不连续的空间,导致更新多个列时磁盘是随机写操作;而行式存储时同一行多个列都存储在连续的空间,一次磁盘写操作就可以完成,列式存储的随机写效率要远远低于行式存储的写效率。此外,列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘。

一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。

d、全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表。
搜索条件是无法列举完全的,各种排列组合非常多。这要建多少索引够啊,模糊查询哪一类呢?这都是麻烦事
正排索引适用于根据文档名称来查询文档内容。例如,用户在网站上单击了“面向对象葵花宝典是什么”,网站根据文章标题查询文章的内容展示给用户。
在这里插入图片描述

倒排索引适用于根据关键词来查询文档内容。例如,用户只是想看“设计”相关的文章,网站需要将文章内容中包含“设计”一词的文章都搜索出来展示给用户。
在这里插入图片描述

全文搜索的使用方式

将关系型数据按照对象的形式转换为 JSON 文档,然后将 JSON 文档输入全文搜索引擎进行索引
全文搜索引擎能够基于 JSON 文档建立全文索引,然后快速进行全文搜索

6、高性能缓存架构

在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的,典型的场景有:

  • 需要经过复杂运算后得出的数据,存储系统无能为力

例如,一个论坛需要在首页展示当前有多少用户同时在线,如果使用 MySQL 来存储当前用户状态,则每次获取这个总数都要“count(*)”大量数据,这样的操作无论怎么优化 MySQL,性能都不会太高。如果要实时展示用户同时在线数,则 MySQL 性能无法支撑。

  • 读多写少的数据,存储系统有心无力

绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的 90% 以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。如果使用 MySQL 来存储微博,用户写微博只有一条 insert 语句,但每个用户浏览时都要 select 一次,即使有索引,几千万条 select 语句对 MySQL 数据库的压力也会非常大。

缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。

缓存能够带来性能的大幅提升,以 Memcache 为例,单台 Memcache 服务器简单的 key-value 查询能够达到 TPS 50000 (每秒50000次查询请求)以上,其基本的架构是:
在这里插入图片描述

1、缓存穿透

是指缓存中没有被查询的数据,业务需要去存储系统查询

  1. 存储数据不存在

第一种情况是被访问的数据确实不存在。一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。

通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。

这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
2. 缓存数据生成耗费大量时间或者资源

第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。

典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。

具体的场景有:

1)分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据。

2)通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页,因此第 10 页以后的缓存过期失效的可能性很大。

3)竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了。

4)由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit 操作),因此爬虫会将整个数据库全部拖慢。

这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是每天都来,也可能是每周,也可能是一个月来一次,我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是识别爬虫然后禁止访问,但这可能会影响 SEO 和推广;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。

这句话的意思是,通过设置良好的监控系统,可以及时发现爬虫带来的问题,并有时间进行处理,因为爬虫通常不会像恶意攻击那样立即破坏系统,而是逐步产生影响。

举例说明

假设你管理着一个电商网站。

  1. 监控设置

    • 你设置了服务器监控,能够实时查看CPU使用率、内存使用情况、网络流量等。
    • 你还设置了数据库监控,能够查看查询的响应时间和并发连接数。
  2. 正常情况

    • 平时,系统运行稳定,CPU使用率在30%左右,数据库查询响应时间在50毫秒以内。
  3. 爬虫访问

    • 某天开始,有多个爬虫程序不断访问你的网站,抓取商品信息。
    • 刚开始时,CPU使用率稍微上升,可能到40%左右,数据库响应时间增加到70毫秒,但系统仍能正常运行。
  4. 逐步影响

    • 随着时间推移,更多的爬虫开始访问,CPU使用率上升到70%,数据库响应时间增加到150毫秒,网站访问速度明显变慢。
    • 监控系统检测到这些异常,发出警报通知管理员。
  5. 及时处理

    • 管理员收到警报后,迅速分析情况,发现是爬虫导致的负载增加。
    • 管理员可以采取措施,比如增加服务器资源、限制爬虫的访问频率、使用反爬虫技术来阻止过度访问。

通过这样的监控和及时处理,网站管理员可以在爬虫的影响变得严重之前采取行动,确保网站的正常运行。这与恶意攻击(如DDoS)不同,后者会在短时间内使系统崩溃,没有足够的时间进行处理。

2、缓存雪崩

缓存失效后,系统性能急剧下降。重新生成缓存耗时几十毫秒甚至上百秒,这段时间接到成百上千的请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

Solution

1、更新锁

具体解释

这段话主要讨论了如何在分布式系统中对缓存更新操作进行加锁保护,以避免缓存雪崩的问题。以下是详细的解释和示例:

背景知识

  • 缓存更新:在缓存失效后,需要从数据库或其他数据源中重新获取数据,并更新到缓存中。
  • 加锁保护:为了避免多个线程同时进行缓存更新操作,通常会对缓存更新操作进行加锁,确保一次只有一个线程能够更新缓存。
  • 分布式系统:在分布式系统中,有多台服务器共同提供服务,可能会有多个线程在不同的服务器上同时尝试更新同一个缓存。
  • 分布式锁:一种用于在分布式环境下控制对共享资源的访问的机制。分布式锁可以确保在分布式系统中,某个资源在同一时刻只有一个实例在访问。

问题描述

即使在单台服务器上对缓存更新操作进行加锁,但在分布式环境下,有几十到上百台服务器,每台服务器都有可能尝试更新缓存。这样一来,依然可能会有几十到上百个线程同时尝试更新缓存,导致缓存雪崩。

示例说明

假设我们有一个电子商务网站,使用缓存来存储商品的价格信息:

  1. 单台服务器场景

    • 服务器A需要更新商品A的价格信息。
    • 多个线程同时尝试更新商品A的价格信息,如果不加锁,可能会造成重复更新,增加数据库负载。
    • 通过加锁机制,只有一个线程能够更新商品A的价格信息,其他线程要么等待锁释放后读取缓存,要么返回默认值。
  2. 分布式环境

    • 网站运行在一个分布式集群中,有100台服务器。
    • 每台服务器上都有多个线程可能需要更新商品A的价格信息。
    • 如果每台服务器都只在本地加锁,那么在整个集群中,仍然有可能有100个线程同时尝试更新商品A的价格信息。

如何解决

为了在分布式环境中解决这个问题,需要使用分布式锁。以下是一个具体的例子,说明如何使用分布式锁(如ZooKeeper)来解决这个问题:

  1. 使用ZooKeeper实现分布式锁

    • 在更新商品A的价格信息之前,每个线程首先尝试获取一个分布式锁(在ZooKeeper中创建一个临时节点)。
    • 只有获取到锁的线程才能进行缓存更新操作。
    • 获取不到锁的线程要么等待锁释放后重新读取缓存,要么返回默认值。
  2. 操作流程

    • 线程1(服务器A)尝试获取分布式锁。
    • 线程2(服务器B)同时也尝试获取同样的分布式锁。
    • ZooKeeper确保只有一个线程(假设是线程1)获取到锁。
    • 线程1更新商品A的价格信息,并更新缓存。
    • 线程1释放分布式锁。
    • 线程2在检测到锁释放后,重新读取缓存,发现缓存已经更新,无需再次更新。

2、后台更新

在一个系统中,缓存用于存储经常访问的数据,以加快读取速度,减轻数据库的负载。通常,缓存有一个有效期,到了有效期后数据会失效,需要重新加载。如果缓存失效时没有及时更新,用户会感受到数据缺失。

场景

假设你管理一个电商网站,用户经常访问的商品价格信息都存储在缓存中,以提高读取速度和用户体验。由于网站访问量很大,你使用了缓存来减少数据库的负载。

初始状态
  • 商品A的价格信息被存储在缓存中。
  • 用户访问商品A时,系统会从缓存中快速读取价格信息。
缓存失效
  1. 缓存满了

    • 随着访问量增加,缓存中的数据越来越多,最终超过了缓存系统的内存容量。
    • 缓存系统决定清除一些不常访问的数据,以腾出空间。
    • 假设商品A的价格信息被清除(“踢掉”)。
  2. 数据不可用

    • 此时,缓存中不再有商品A的价格信息。
    • 如果有用户访问商品A,系统尝试从缓存读取价格信息,会发现缓存中没有这条数据。
    • 结果是系统返回一个空值或默认值,用户无法看到商品A的价格信息。
重新加载
  1. 后台线程检测到数据被清除

    • 一个后台线程发现商品A的价格信息被清除。
    • 后台线程从数据库中重新获取商品A的价格信息,并更新到缓存中。
  2. 数据重新可用

    • 商品A的价格信息重新加载到缓存中。
    • 再次有用户访问商品A时,系统能够从缓存中读取价格信息,并正确显示给用户。

方案一:频繁检查缓存并更新

机制:后台线程除了定时更新缓存外,还要频繁地读取缓存,确保数据没有被“踢掉”。
优点:实现简单,实时性较高。
缺点:频繁的读取操作会增加系统负载。如果检查间隔太长,用户可能会在缓存被“踢掉”期间访问到空数据。

方案二:消息队列通知更新

机制:业务线程在发现缓存失效后,通过消息队列发送通知,后台线程接收到通知后更新缓存。
优点:缓存更新更及时,用户体验更好。
缺点:依赖消息队列,系统复杂度增加。

3、缓存热点

明星发微博,上万人来围观,都命中同一份缓存数据,缓存也扛不住。
解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,缓解压力。

7、单服务器高性能模式:PPC与TPC

磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等,每个都有可能影响系统达到高性能。

PPC(Process Per Connection):每个连接都会创建一个新的进程来处理。
Prefork 模式:预先创建一些 worker 进程来处理连接,避免频繁地 fork 新的进程。
TPC(Thread Per Connection):每个连接都会创建一个新的线程来处理,相比于 PPC 模式,资源消耗更少。

TPS(Transactions Per Second,事务每秒)是一个性能指标,用于衡量系统在一秒钟内能够处理的事务数量。

举例说明TPS的影响

场景设置

假设我们管理一个在线支付系统,该系统需要处理大量的支付请求。为了确保系统的高性能,我们关注TPS和响应时间。

一行不恰当的 debug 日志的影响

  1. 初始状态

    • 系统的TPS为30,000,这意味着每秒钟能够处理30,000个支付请求。
  2. 添加不恰当的 debug 日志

    • 在代码的关键路径中添加了一行不必要的debug日志。

    • 这行日志记录了每个支付请求的详细信息,例如:

      def process_payment(request):
          # 不恰当的 debug 日志
          print(f"Processing payment for request: {request}")
          # 处理支付逻辑
          ...
      
  3. 性能下降

    • 由于每个支付请求都记录了日志,系统的I/O操作显著增加,CPU和磁盘资源被过度消耗。
    • 结果,系统的TPS从30,000下降到8,000。这意味着每秒只能处理8,000个支付请求,系统性能严重受损。

一个tcp_nodelay参数的影响

  1. 初始状态

    • 系统的响应时间为2毫秒,这意味着从接收到支付请求到返回响应的时间为2毫秒。
  2. 修改tcp_nodelay参数

    • tcp_nodelay是一个网络参数,用于控制TCP协议的延迟和吞吐量。

    • 如果设置不当,例如禁用了Nagle算法(用于减少小包发送),会增加网络延迟。

    • 代码示例:

      import socket
      
      # 创建TCP套接字
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      # 禁用Nagle算法
      sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
      
  3. 响应时间延长

    • 由于参数设置不当,系统的响应时间从2毫秒增加到40毫秒。
    • 这意味着用户在发起支付请求后,需要等待更长时间才能收到响应,影响用户体验。

8、 单服务器高性能模式:Reactor与Proactor

单服务器高性能的 PPC 和 TPC 模式,它们的优点是实现简单,缺点是都无法支撑高并发的场景,尤其是互联网发展到现在,各种海量用户业务的出现,PPC 和 TPC 完全无能为力。

Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。

理论上 Proactor 比 Reactor 效率要高一些,异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠,但要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下的 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 Reactor 模式为主

9、高性能负载均衡:分类及架构

常见的负载均衡系统包括 3 种:DNS 负载均衡、硬件负载均衡和软件负载均衡。

1、DNS 负载均衡

本质是解析同一域名可以返回不同的ip。例如,同样是 www.baidu.com,北方用户解析后获取的地址是 61.135.165.224(这是北京机房的 IP),南方用户解析后获取的地址是 14.215.177.38(这是深圳机房的 IP)。

2、硬件负载均衡

硬件负载均衡的优点是:

功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。

性能强大:对比一下,软件负载均衡支持到 10 万级并发已经很厉害了,硬件负载均衡可以支持 100 万以上的并发。

稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。

支持安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙、防 DDoS 攻击等安全功能。

硬件负载均衡的缺点是:

价格昂贵:最普通的一台 F5 就是一台“马 6”,好一点的就是“Q7”了。

扩展能力差:硬件设备,可以根据业务进行配置,但无法进行扩展和定制。

3、软件负载均衡

常见的有 Nginx 和 LVS

负载均衡层次

  1. 4 层负载均衡(LVS - Linux Virtual Server)

    • 工作层次:工作在 OSI 模型的第 4 层(传输层)。
    • 处理内容:基于 IP 地址和端口号进行流量分发,不关心上层应用协议(例如 HTTP、SMTP)。
    • 应用范围:由于与具体应用协议无关,适用于几乎所有类型的网络应用,例如数据库连接、聊天服务器、文件传输等。
  2. 7 层负载均衡(Nginx)

    • 工作层次:工作在 OSI 模型的第 7 层(应用层)。
    • 处理内容:基于应用层协议内容(例如 HTTP 请求的 URL、HTTP 头)进行流量分发。
    • 应用范围:特定于应用层协议,主要用于 HTTP、HTTPS、E-mail 协议等,需要对具体的应用数据进行处理和分配的场景。

软件和硬件的最主要区别就在于性能,硬件负载均衡性能远远高于软件负载均衡性能。Ngxin 的性能是万级,一般的 Linux 服务器上装一个 Nginx 大概能到 5 万 / 秒;LVS 的性能是十万级,据说可达到 80 万 / 秒;而 F5 性能是百万级,从 200 万 / 秒到 800 万 / 秒都有(数据来源网络,仅供参考,如需采用请根据实际业务场景进行性能测试)。当然,软件负载均衡的最大优势是便宜,一台普通的 Linux 服务器批发价大概就是 1 万元左右,相比 F5 的价格,那就是自行车和宝马的区别了。
这里的“5万每秒”是指每秒能够处理的请求数(Requests Per Second,RPS)。在负载均衡和服务器性能评估中,RPS 是一个关键的指标,表示服务器在一秒钟内可以处理的客户端请求数量。

4、负载均衡典型架构

:DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。
在这里插入图片描述
一般在大型业务场景下才会这样用,如果业务量没这么大,则没有必要严格照搬这套架构。例如,一个大学的论坛,完全可以不需要 DNS 负载均衡,也不需要 F5 设备,只需要用 Nginx 作为一个简单的负载均衡就足够了。

10、高性能负载均衡:算法

1)轮询:只要服务器在运行,运行状态不管
2)加权轮询
负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。

加权轮询是轮询的一种特殊形式,其主要目的就是为了解决不同服务器处理能力有差异的问题。例如,集群中有新的机器是 32 核的,老的机器是 16 核的,那么理论上我们可以假设新机器的处理能力是老机器的 2 倍,负载均衡系统就可以按照 2:1 的比例分配更多的任务给新机器,从而充分利用新机器的性能。
3)负载最低优先
将任务分配给当前负载最低的服务器
如果我们自己开发负载均衡系统,可以根据业务特点来选择指标衡量系统压力。如果是 CPU 密集型,可以以“CPU 负载”来衡量系统压力;如果是 I/O 密集型,可以以“I/O 负载”来衡量系统压力。

负载最低优先算法基本上能够比较完美地解决轮询算法的缺点,因为采用这种算法后,负载均衡系统需要感知服务器当前的运行状态。当然,其代价是复杂度大幅上升。通俗来讲,轮询可能是 5 行代码就能实现的算法,而负载最低优先算法可能要 1000 行才能实现,甚至需要负载均衡系统和服务器都要开发代码。负载最低优先算法如果本身没有设计好,或者不适合业务的运行特点,算法本身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好,但实际上真正应用的场景反而没有轮询(包括加权轮询)那么多。

4)性能最优类
目的是最快响应客户。优先将任务分配给处理速度最快的服务器。
5)Hash 类
负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求
比如将来源于同一个源 IP 地址的任务分配给同一个服务器进行处理

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DevOps中构建服务架构时,以下是一些关键步骤和实践: 1. 容器化和微服务架构:将应用程序拆分为小而自治的微服务,并将它们部署在容器化平台上,如Docker和Kubernetes。这样可以提高可伸缩性、部署灵活性和应用程序的可靠性。 2. 持续集成和持续交付(CI/CD):建立自动化的CI/CD流水线,确保代码的频繁集成和交付。这包括自动化的构建、测试、部署和监控过程,以实现快速迭代和快速交付。 3. 基础设施即代码(IaC):使用基础设施即代码的原则,通过代码来管理和配置基础设施资源,如服务器、网络和存储。这样可以实现可重复、可管理和可伸缩的基础设施环境。 4. 自动化配置管理:使用配置管理工具,如Ansible、Chef或Puppet,来自动化管理和配置应用程序的环境。这包括安装依赖项、配置文件管理和应用程序部署等。 5. 监控和日志管理:集成监控和日志管理工具,如Prometheus、Grafana和ELK堆栈,以实时监控应用程序的性能和健康状况,并记录和分析应用程序的日志。 6. 自动化测试:建立自动化测试策略和框架,包括单元测试、集成测试和端到端测试,以确保应用程序的质量和稳定性。 7. 安全和合规性:将安全性和合规性考虑纳入服务架构中,包括身份验证、访问控制、数据加密和漏洞扫描等。 8. 协作与文化:建立跨职能团队之间的良好协作和沟通,倡导DevOps文化和原则,促进快速迭代、持续改进和自主决策。 以上是构建DevOps服务架构的一些关键步骤和实践。但需要根据具体的应用场景和组织需求进行定制化的实施。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值