架构那些事儿 - 核心思想
架构师,架构设计,如何做架构
2021-05-19
26 次浏览
架构的核心思想就5个方面,高性能,高可用,可靠性,扩展性,安全性。
1. 高性能
提高性能可以简单的从四个方面去考虑,分别是硬件环境、前端,应用服务器,数据库。
1.1. 硬件环境
一旦出现性能问题,简单粗暴的提高硬件实际上是最快的,但是成本也是显而易见的,而且是呈指数上升。这里的诀窍就是哪里差就翻倍哪里。内存吃紧就翻倍内存,硬盘IO瓶颈,就翻倍磁盘的IO或者转速。CPU4核心满负荷,那就翻倍成8核心等等。
1.1.1. 带宽
这种情况下,首先确定架构是可以支撑更大流量的情况的,如果架构本身不支持,那么更多的带宽意味着机器宕机会更快。
1.1.2. 硬盘
提升存储的思想就是提升IO的能力,存储其实又分成两种了,一种是企业级别的SSD,这就非常贵了。还有就是组成Raid磁盘阵列。
1.1.3. 内存
提升内存应对的就是高并发的情况下会大量的占用系统资源,而资源不足必然会导致并发能力下降。那么最直观的就是增大内存。
1.1.4. 计算
现在的技术框架很多都重视了多核心的能力,所以当出现性能瓶颈在核心满负载的情况下,直接翻倍核心后者增加运算速度是非常有效的手段。
1.1.5. 服务器
上面几种方式更多的可以称为纵向扩展,增加服务器更多的是横向扩展,这就得看应用程序本身是否支持横向了,如果架构是支持横向扩展的,那么就简单的增加服务器就可以了。
在架构设计初期,横向扩展的能力一定要考虑其中,包括横向扩展带来的一些复杂性也要一并考虑,预留设计,以便后期快速扩展。
1.2. 前端
前端优化主要考虑的是负载均衡、CDN、客户端或者浏览器优化。
1.2.1. 浏览器或者客户端优化
这里可以考虑的方面有本地缓存(浏览器/客户端),压缩技术,降低Cookie传输等手段。甚至合理规划前端,减少服务端调用都是不错的方法。
1.2.2. CDN
CDN是将静态内容分发至里用户最近的网络服务商机房,这样保证用户最短路径访问。是非常有效的缓解服务器压力的手段之一,也有非常多的CDN服务商可供选择,架构师要做的事情就是规划哪些静态资源需要放在CDN上。
1.2.3. 负载均衡(Load Balance)
当系统出现性能瓶颈的时候,首先想到的一定是增加硬件,因为这最方便,增加一台服务器,就多了一倍的服务能力,通常这个就叫水平扩展。那么如果其中一台服务器出现了崩溃宕机等情况不能提供服务了,这时候另外的服务器也能提供服务,而不至于导致整个服务停止(单点失败),通常这个叫冗余。
通过一个比喻来描述上述的情况,假设有三个工人在做事情,
- 如果把工作总是给其中一个工人做,那么结果就是这个工人累死了,两外两个工人还在休息。
- 三个工人都在工作中,其中一个工人突然生病离开了岗位,这个时候剩下的两个工人也能完成那个工人没有完成的工作。
这两种情况的处理都不开负载均衡技术。
负载均衡技术是分布式中重要的一环。通常它可以在三个地方使用:
- 用户与Web(前台)服务之间,通过应用服务器(例如Nginx)将用户的请求分发到Web服务进行处理。
- Web服务与后端(内部/微服务/中台)服务之间。
- 后端服务与数据库之间。
那么,负载均衡是如何“均衡”的呢?或者说它是如何判断应该将请求分发到哪个服务上的呢?这就涉及到了负载均衡的算法:
- 最少连接法:检查各个服务器的连接数,优选到最少连接的。这个方法适合长连接的场景。
- 最短响应法:检查各个服务器的连接和平均响应,优选平均响应最短和连接最少的。
- 最小流量法:检查各个服务器的流量情况,优选流量最少的。
- 循环法:按顺序依次选择,若到最末端,又从头开始。这个方法适合任一响应时间比较固定的。
- 加权循环法:根据一些指标给每个服务分配一个权重,然后从权重高的开始选择。权重高的总是能够获得更多的请求。
- IP哈希:将请求的客户端IP地址进行hash之后得到一个值,然后将请求分发到这个值对应的服务。
通常这些算法都被一些应用服务器(例如Nginx)或者微服务框架(例如Spring Cloud)所实现。成熟的框架或者应用服务器之间没有优劣,这里要考虑的就是学习曲线和使用习惯了。
1.3. 应用服务器
1.3.1. 缓存技术(Cacheing)
是否使用缓存技术的指征就是当前的访问频次远远高于写入频次。同时,还要考虑到服务器的存储,不要将缓存的容量设置到非常大,试图缓存所有的东西,如果想这么做,那么搜索引擎是一个不错的考虑。
1.3.1.1. 缓存设计
在使用缓存之前,可以考虑首先将数据库的缓存物尽其用,这些缓存是不需要编码的,通常数据库都有默认的缓存配置,但是优秀的DBA可以针对数据库访问的特点针对性的调整缓存策略,使得效率大大提高,I/O大大降低。
系统中使用缓存,通常的流程是请求数据的时候,通过数据的key,首先去检查缓存中是否存在,如果存在,那么直接返回,如果不存在,那么访问数据库查询结果,将结果存储到缓存中,然后返回结果。缓存的内容通常是访问最频繁的那一批。同时,需要分清晰哪些是可以本地缓存的,哪些是需要分布式缓存的,幸运的是当前有redis、memcached等中间件可以选择。
1.3.1.2. 置换策略
用上了缓存之后,你需要确定你的缓存置换策略,也就是说缓存的内容需要在什么时候和什么情况下被替换掉。这里有几种常见的策略:
- 先进先出:最先进缓存的最先被替换。
- 最近最少使用(LRU):最近最少使用的最先被替换。
- 最不经常使用(LFU):最不经常访问的内容最先被替换。(算法底层是双链表实现,有兴趣可以看看,Leetcode中的hard题)
- 非最近使用(NMRU):在最近没有使用的内容中随机挑选一个。
- 纯随机(RR):很直白,随机挑选一个。
当然,一般使用LRU即可。其他的可以根据具体情况具体分析。
1.3.1.3 缓存失效
缓存失效在分布式的情况下是一定要考虑的,因为通常这种情况下会有多个子系统会更新数据库、缓存和搜索索引。数据库中的数据一旦被更新,那么缓存一定要同时更新,或者失效,否则会造成数据不一致。通过缓存失效可以避免出现不一致的情况:
- 直写缓存:数据同时写入高速缓存和相应的数据库中。这样做的好处就是缓存的数据和数据库的数据同时更新,所以有一致性,而且访问的时候也不需要去数据库读取。同时,这个方式能够保证在突然断电或者崩溃宕机的情况下,没有数据丢失。但是,由于其每次都是同时写,所以会面临写操作会比较长的情况,同时如果也要考虑在数据库写入失败的情况下,缓存的处理。
- 绕写缓存:数据不写入缓存直接修改数据库,然后让删除这个数据对应的缓存。这个方法好处就是写操作变少了,但是如果这个内容访问比较频繁的话,那么很快就会产生缓存不命中的情况,也就是要从数据库里面读数据。
- 回写缓存:数据首先写入缓存,然后就返回响应了,数据异步或者定时或者定量等方式写入数据库中。这个方式的优点就是快,响应快,低延迟。缺点就是如果一旦出现断电或者崩溃等等,数据可能就会出现丢失的情况。
1.3.2. 使用异步
异步技术是最迷人的技术,没有之一,不过,同时也带来了一致性的问题。当前有很多技术框架底层已经是异步的了,例如vert.x,主流框架也在向异步靠拢。这样让异步开发的难度大大降低,大家可以像开发同步程序一下一样开发异步即可(例如Golang的协程)。另外一种实现异步的方式就是采用消息队列,这样以异步消息的方式去处理。总之,一切与一致性无关的事情,可以允许延迟处理的事情,都可以异步处理。
1.3.3. 使用集群
集群是一群服务组成的,其中最核心的部分就是管理和调度。但是也不需要架构师们带着工程师去实现了,因为很多集群中间件都做得非常好了,甚至微服务还有完全的一整套体系来支撑。集群不仅仅能够解决性能的问题,也可以解决高可用和健壮性的问题。
1.3.4. 代码优化
代码优化可以做的事情其实还蛮多的。像缓存技术、异步技术都可以在代码中去做,甚至包括内存消息队列。
1.3.4.1 多线程
多线程技术其实和异步算是一个思想,在现代的系统中,更多的是业务的处理,多线程更多的是对一整套事情异步处理的应用。要用到多线程,一定需要考虑到几个方面。
- 对象尽量是无状态的
- 尽量避免资源共享
- 有共享资源,要使用缩,但是也要小心锁,锁往往也是性能瓶颈
- 尽量选择一致性要求不那么高的
1.3.4.2 数据结构和算法
好的数据结构和算法,能够提升非常高的性能,如果一个操作是O(n3)那么如果降低到O(n)的话,比用什么其他技术都得劲。特别是一些集合的处理,在数据量不大的情况下,也许O(n2)还能接受,但是如果数据增大了,那么服务性能迅速就会降低。同时合理的数据结构也可以避免出现内存障的情况。
1.3.4.3 虚拟机(VM)
虚拟机在当今是绕不开的,因为Java在商业领域的成熟应用,以及新贵Golang的应用。它们两个特点就是有虚拟机,而虚拟机最大的问题就是垃圾回收,也就是著名的“世界暂停”,当垃圾回收的时候,必然是要暂停一切处理的,所以合理的回收机制可以极力避免“世界暂停”。
1.4. 数据库
1.4.1. 读写分离
读写分离其实数据库调优首先想到的了,主流SQL数据库的读写分离都已经很成熟了。配置配置就能用了,关键就在于应用程序这一块要区分开处理。同时一定要注意缓存。
1.4.2. 索引技术(Index)
当出现性能瓶颈的时候,数据库索引总是在最先考虑的序列中。如何建立数据库的索引,这个命题可以写一本书,当然作为技术总监/总负责/主管/架构师,也可以招聘一名专业的DBA,让他来解决。当然这里也需要了解一些基础的数据库索引知识。当然,这里指的都是SQL数据库。
- 首先,主键是有索引的。
- 第二,索引可以是一个列也可以是多个列。
- 第三,通常需要先分析慢查询的SQL语句。
- 第四,通常where条件就是候选索引列。
- 第五,太多索引也可能会导致数据库的插入、更新、删除变慢,也就是说,必须避免无脑建索引,同时,如果系统的写入远远大于查询,那么索引就只会起反效果。
1.4.3. 分库分表(Data Partitioning)
分库分表也是针对SQL数据库的一种优化技术,当一张表的数据库过大的时候,我们就需要考虑分表了。这样一来,不同的数据访问就分发到不同的表中。那么如何划分,什么情况下用这个划分方式,就需要仔细考虑。
1.4.3.1. 划分模式
- 水平划分(data sharding)
水平划分通常也叫做范围划分,例如有一张users表,那么id每隔100万划分在一个数据库中。这样划分的不好之处就是有些数据总是会被频繁访问,有些不是,那么如果这些被频繁访问的数据就刚刚好在一个范围中的话,那么就会导致划分之后的各个数据库访问不平衡。
- 垂直划分
垂直划分就是将数据库的各个表独立出来,例如有users和photos表,将它们分别放不同的库(其实微服务就天然的像这样分库了)。这样划分的不好之处就是某个原表增长比较快的话,例如photos到了10个亿,而用户只有几百万的情况下。这样的划分就没有起到很好的效果。
1.4.3.2. 划分方式
- 基于哈希的分配方式
通常可以设计一个哈希函数,通过哈希新数据得到哈希值,然后按照一定的规则,进行分配,例如有100个数据库,哈希值 mod 100就能得到具体在哪个数据库。这里比较著名的是一致性哈希。
- 逻辑分配的方式
对于一些明显存在逻辑分层关系的,可以采用此方式,例如地区,可以将属于某个城市或者省或者国家的数据分配到一起。
- 循环分配的方式
这是最简单的方式,对于n个数据库,依次将数据存储到第i个服务器上,周而复始。
- 多个方式结合的方式
上面几种方式可以结合在一起,例如可以首先进行逻辑分区,然后再进行一致性哈希。
1.4.3.3. 需要注意的地方
分库分表不可避免的带来系统设计的复杂性,同一张表的数据被放在了多个数据库中,如果产生多表联合查询,复杂度将会进一步上升。
- 跨表查询
如果系统中有不少的join查询,那么在分库分表的时候就需要着重考虑了,跨库查询将变得艰难,同时性能也会下降。应对这样的方式是在数据库表设计上尽量避免join查询或者是将join的数据单独设计成表或者是采用nosql进行处理。
- 数据完整性约束
和跨表查询一样,跨表存储更新数据也会存在困难,特别是有外键约束的情况下。应对的方式可以去除外键约束,通过程序编码进行处理,这同样对缓存也存在影响。
- 数据不平均
数据不平均的问题总是会出现,例如即便很好的分库了用户的照片数据,当前看起来也挺平均,但是总是会有突然出现某个分区的数据急剧增加的情况。这样会使得数据需要再次分区,在不停服务器的情况下重新平衡数据困难重重,所以这个预案是必须提前做好的,以便在出现这样的情况的时候能够立刻启动。
所幸的是,现代数据库或者围绕这些数据库开发的中间件都可以解决上述问题。例如自动完成分库分去的策略,通过中间件来访问数据库,甚至不需要关心它是如何划分数据的,只需要像访问单一数据库一样。通常这样的中间件是收费的,当然也有开源免费的。
1.4.4. 使用NoSQL
经历了分库分表,那么使用NoSQL的心理障碍也不存在了,技术是为业务服务的,业务需要就用,在系统中有很多数据其实并不需要关系,他们天生就是无状态的,或者可以被处理成无状态的,例如照片,博文等等,把他们放入NoSQL中,系统就获得了无限的扩展能力和更高的性能。所以使用NoSQL的关键在于辨识哪些可以使用NoSQL。
1.4.5. 使用搜索引擎
搜索引擎可以改善搜索功能,这是毋庸置疑的,而有些搜索引擎甚至可以做统计工作。能够让你的数据快速的被统计和组织起来。
2. 高可用
高可用的含义就是服务的时间有多长,如果一天24小时,一年365天系统都在运转,那么就是高可用的。那么如何保证高可用呢。
2.1. 设计
首先是要承认一切都是不可靠的。这样就会在设计阶段带入角色,尽量将模型建成无状态的,将部署设计成分布式的,将服务设计成可快速扩展的。
2.2. 测试
良好的测试可以给团队以信心,特别是devops思想中的捣乱猴子,通过随机停止掉某台服务来看整体的情况,就能最大限度的得知我们在某个服务失效的时候,需要处理哪些事情来防止无法高可用的发生。
2.3. 容器化
容器化可以让容灾变得更有效率,一个服务不可用了,可以通过快速更换容器来达到可用。
2.4. 监控
良好的监控可以有效的缩短不可用时间,心跳检测机制可以检测到所有的服务是否都处于可用状态。一旦出现问题,可以立即着手解决。
2.5. 集群
一切都避免单点,就能最大程度的支撑高可用,分布式缓存,分布式文件,分布式服务,然后通过集群有效的管理起来。
3. 可靠性
可靠性的定义实际上是给定时间范围内系统发生故障的频率。例如一个订单系统,多次都无法下单,那么可靠性就不是太好。提升可靠性的手段有:
3.1. 设计
良好的设计可以避免99%的不可靠,例如订单系统,考虑到系统承受力始终会有瓶颈的情况,就要拆分下单的流程(比如将下单、减库存和付款拆分),让下单这个动作本身够快,同时保证所有的人都能下单;另外还可以将下单异步处理,保证所有的人下单都能成功。
3.2. 测试
测试阶段可以将可靠提升到99.99%,测试的重要性无需赘述。要的就是防患于未然。
3.3. 冗余
将数据或者服务冗余可以将可靠性提升到99.9999%。冗余不能是单纯的,还是应该和集群服务联系起来,尽最大可能的保证即便错误发生,系统还是可以完成业务的。
很多人把高可用和可靠性联系在一起,确实,他们是有关系的。一个系统是可靠的,那么他一定是可用的,反之未必。系统的可靠性更多关注的是系统本身的能力,而可用性则关注的是真个服务层面。换句话说,即便服务不是十分可靠,例如在线商城有1%的几率会掉单,但是由于其通过快速修正的方式将掉单补好,或者通过返回下单失败等等手段,也能使得系统可用。
4. 扩展性
扩展性在以前的架构师设计时是需要去考虑的一件事情,但是当SOA和微服务,特别是微服务思想出现后,扩展性天然就具备了。尽管如此,一个具备扩展性的思维应该是常在架构师脑中的。这是一个前瞻性的问题,业务必然是向前发展的,架构师不能等着业务变大了,才去调整,而应该时刻最好准备,这样当业务猛增的时候,只需要增加服务就可以了,甚至还能做到淡季的时候减少服务,旺季的时候增加服务的可伸缩。要做到这一点其实也蛮容易,因为有太多现成的案例可以参考,但也不容易,因为每个业务可能是不同的,甚至也有过度设计的风险,例如规模现在还在100万用户,而直接选择1亿用户的架构,那完全就是自寻死路。不管如何,扩展性的思路都是大同小异的。
4.1. 降低耦合性
通过SOA或者微服务的思想,以及利用事件驱动的框架或者是分布式消息系统都可以有效降低耦合性。降低耦合性的好处不言而喻,不同领域之间的影响被降低到最小,可以使得扩展性更好。
4.2. 可复用的服务
通过规范合理的接口,将服务区分开来,可以显著提高扩展性。
4.3. NoSQL
NoSQL技术无疑是伟大的技术,其本身就具有极强的扩展性,同时还支持无固定格式的数据结构,这样在数据的扩展性上面,无疑是能够做到最好。
5. 安全性
安全总是绕不开的话题。除了聘请专业的安全顾问和第三方安全公司以外,架构师也要从技术层面上做好安全的把控。
5.1. SSL
Web网站必须采用SSL,这一点到现在为止已经是一个必然了。
5.2. SQL注入
SQL注入在以前是老生常谈的话题,当今很多框架可以最大程度的避免SQL注入,但是要时刻注意框架本身也可能会存在BUG,这样很可能会被注入攻击。
5.3. XSS
XSS攻击也是很著名的攻击了,黑客通过篡改网页来进行攻击。很多web框架都有应对这方面攻击的配置,需要时刻注意。
5.4. CSRF
黑客通过模拟正常用户的请求来达到目的,通常是窃取了cookie和session的数据。通过CSRF让每次请求加入一些随机数,来保证识别正常用户的合法请求。还有就是使用验证码,当然这个体验就很不好了。
5.5. 信息过滤
很多信息是非法或者无效的,例如:黄赌毒,以及政治敏感话题或者垃圾广告之类的信息,信息过滤特别是对于内容服务网站来说是必须要考虑的,当然成熟的方案也有不少,分类算法,黑名单等等都是有效的手段。
5.6. 密钥管理
密钥很重要,通常服务器启动的时候都会通过密钥连接数据库,或者第三方应用,或者是网站用户自己的密钥等等,这些信息都需要通过加密处理和管理,并且还要采用良好的定期更换策略。
5.7. 敏感信息
一些用户的隐私数据,金融数据等等都属于敏感信息,这些信息在显示的时候都要进行遮盖处理。
5.8. 风控
风险控制是一个很大的话题,而且通常有专门的团队来处理,把它放在这里不是太妥,但是架构师也需要考虑到各种风险的管理,例如交易的风险,欺诈的风险等等。涉及到金融、交易等等应用的系统都应该要提前做好准备。
黑客的手段层出不穷,架构师要时刻保持对这个领域的敏感性,来保证系统最大程度的安全。