架构是什么?为什么要学架构?从软件层面讲是将多个软件系统按照一定规则关联起来。架构设计的主要目的是为了解决软件系统复杂度带来的问题。而问题是阶段性、多变性的,所以架构也是阶段性的。问题中掺杂着大量的可变因素,主要有:人、财、物、时、事。所以从另一个层面架构设计的目的是支撑公司业务更好更快的发展。
架构设计复杂度
-
高性能
满足业务的增长
追求良好的用户体验
-
高可用
提高系统的稳定性和数据一致性
- 可扩展性
-
能够快速响应业务发展、基础环境变化
-
低成本、安全、规模
既然架构设计要具备高性能、高可用、可扩展性。那我们是不是一上来就奔着这个目标来设计做出高大上的系统。其实不然,架构设计是有原则指导的。
架构设计3原则
-
合适原则
架构无优劣,但存合适性。“汝之蜜糖,吾之砒霜”;架构一定要匹配企业所在的业务阶段;不要面向高大上去设计架构,高大上的架构不等于适用;所谓合适,一定要匹配业务所处阶段,能够合理地将资源整合在一起并发挥出最大功效,并能够快速落地,实现快速增长。
-
简单原则
简单才会高效,复杂度越高出错误率越高。在复杂的软件系统和业务中简单比复杂更加困难。
-
演化原则
而对于架构设计来说,变化才是主题。软件系统也是随着业务的不断发展而变化,应该认真分析当前业务的特点,明确业务面临的主要问题,设计合理的架构,快速落地以满足业务需要,然后在运行过程中不断完善架构,不断随着业务演化架构。
架构设计的主要目的是为了解决软件系统复杂性,了解了架构的设计原则,所以在设计架构时,首先要识别系统复杂度,只有正确分析了系统的复杂度,架构设计才不会偏离方向。将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。
识别系统复杂度
我们先看一张微博的核心业务图(如下),是不是非常复杂?
分析后微博的核心业务是:用户发布一条微博消息,关注者可以快速的看到发布的消息。假设微博系统用户每天发送 1000 万条微博,那么微博子系统一天会产生 1000 万条消息,再假设平均一条消息有 10 个子系统读取,那么其他子系统读取的消息大约是 1 亿次。转化成秒级,即 TPS 和 QPS,一天内平均每秒写入消息数为 115 条,每秒读取的消息数是 1150 条;再考虑系统的读写并不是完全平均的,设计的目标应该以峰值来计算。峰值一般取平均值的 3 倍,那么消息队列系统的 TPS 是 345,QPS 是 3450,这个量级的数据看起来似乎没那么吓人。当前业务规模计算的性能要求并不高,但业务会增长,因此系统设计需要考虑一定的性能余量。由于现在的基数较低,为了预留一定的系统容量应对后续业务的发展,我们将设计目标设定为峰值的 4 倍,因此最终的性能要求是:TPS 为 1380,QPS 为 13800。TPS 为 1380 并不高,但 QPS 为 13800 已经比较高了,因此高性能读取是复杂度之一。注意,这里的设计目标设定为峰值的 4 倍是根据业务发展速度来预估的,不是固定为 4 倍,不同的业务可以是 2 倍,也可以是 8 倍,但一般不要设定在 10 倍以上,更不要一上来就按照 100 倍预估。
对于用户来说,如果发布的消息经常丢掉,体验肯定大大折扣,导致用户流水而损失收入,所以高可用也是复杂度之一。
有了核心业务发布和阅读消息后,是不是就能直接用了呢,用户发布的内容需要不需要审核?如果用户发布了一些不合法内容或者谣言而导致平台被查封或者下线,那就得不偿失了。所以内容还是要审核的。这样就要求在架构设计时降低子系统之间的耦合度,支持对业务的灵活扩展,围绕着核心业务快速添加功能需求。因此可扩展性也是复杂度之一。
低成本、安全、规模只有在业务有了一定规模,才会开始考虑。当然前期也不是完全不考虑。在满足“高性能”、“高可用”的同时,可能增加了很多服务器引入了很多新技术。有了一定规模后,服务器的成本也是一笔不小的开销。安全,业务前期更多是为了快速发展,当系统变得庞大而又复杂时,安全漏洞也会变得越来越多。常见的XSS攻击、CSRF攻击、SQL注入、DDoS攻击等 。
微博技术体系
微博平台的技术体系,使用正交分解法建立模型:在水平方向,采用典型的三级分层模型,即接口层、服务层与存储层;在垂直方向,进一步细分为业务架构、技术架构、监控平台与服务治理平台。下面是平台的整体架构图:
水平分层
-
接口层主要实现与 Web 页面、移动客户端的接口交互,定义统一的接口规范。
平台最核心的三个接口服务分别是内容(Feed)服务、用户关系服务及通讯服务(单发私信、群发、群聊)。
-
服务层主要把核心业务模块化、服务化。
这里又分为两类服务,一类为原子服务,其定义是不依赖任何其他服务的服务模块,比如常用的短链服务、发号器服务都属于这一类。图中使用泳道隔离,表示它们的独立性。另外一类为组合服务,通过各种原子服务和业务逻辑的组合来完成服务,比如 Feed 服务、通讯服务,它们除了本身的业务逻辑,还依赖短链、用户及发号器服务。
-
资源层主要是数据模型的存储。
包含通用的缓存资源 Redis 和 Memcached,以及持久化数据库存储 MySQL、HBase,或者分布式文件系统 TFS 以及 Sina S3 服务。
水平分层有一个特点,依赖关系都是从上往下,上层的服务依赖下层,下层的服务不会依赖上层,构建了一种简单直接的依赖关系。
垂直分层
-
技术架构,面向功能模块的技术选型。
-
监控模块,面向运维需求的功能模块。
-
服务治理,面向服务管理的功能模块。
上面了解了微博平台架构,下面我们再进一步分析顶层架构设计(下图)
从用户发出请求到返回数据,整个过程依次穿透顶层架构的每一层,通过每一次的职责来分析架构的复杂度。
高性能架构模式
高性能负载均衡
常见的负载均衡系统包括 3 种:DNS 负载均衡、硬件负载均衡和软件负载均衡。
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS,其中 Nginx 是软件的 7 层负载均衡,LVS 是 Linux 内核的 4 层负载均衡。4 层和 7 层的区别就在于协议和灵活性。
软件和硬件的最主要区别就在于性能,硬件负载均衡性能远远高于软件负载均衡性能。Ngxin 的性能是万级,一般的 Linux 服务器上装一个 Nginx 大概能到 5 万 / 秒;LVS 的性能是十万级,据说可达到 80 万 / 秒;而 F5 性能是百万级,从 200 万 / 秒到 800 万 / 秒都有(数据来源网络,仅供参考,如需采用请根据实际业务场景进行性能测试)。当然,软件负载均衡的最大优势是便宜,一台普通的 Linux 服务器批发价大概就是 1 万元左右,相比 F5 的价格,那就是自行车和宝马的区别了。
-
DNS 负载均衡
DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。例如,北方的用户访问北京的机房,南方的用户访问深圳的机房。DNS 负载均衡的本质是 DNS 解析同一个域名可以返回不同的 IP 地址。下面是 DNS 负载均衡的简单示意图:
-
针对 DNS 负载均衡的一些缺点,对于时延和故障敏感的业务,有一些公司自己实现了 HTTP-DNS 的功能,即使用 HTTP 协议实现一个私有的 DNS 系统。这样的方案和通用的 DNS 优缺点正好相反。
-
优点:
-
简单、成本低:负载均衡工作交给 DNS 服务器处理,无须自己开发或者维护负载均衡设备。
-
就近访问,提升访问速度:DNS 解析时可以根据请求来源 IP,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能。
-
-
缺点:
-
更新不及时:DNS 缓存的时间比较长,修改 DNS 配置后,由于缓存的原因,还是有很多用户会继续访问修改前的 IP,这样的访问会失败,达不到负载均衡的目的,并且也影响用户正常使用业务。
-
扩展性差:DNS 负载均衡的控制权在域名商那里,无法根据业务特点针对其做更多的定制化功能和扩展特性。分配策略比较简单:DNS 负载均衡支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载);也无法感知后端服务器的状态。
-
-
-
硬件负载均衡
目前业界典型的硬件负载均衡设备有两款:F5 和 A10。这类设备性能强劲、功能强大,但价格都不便宜,一般只有“土豪”公司才会考虑使用此类设备。普通业务量级的公司一是负担不起,二是业务量没那么大,用这些设备也是浪费。
-
优点:
-
功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。
-
性能强大:对比一下,软件负载均衡支持到 10 万级并发已经很厉害了,硬件负载均衡可以支持 100 万以上的并发。
-
稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。
-
支持安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙、防 DDoS 攻击等安全功能。
-
-
缺点:
-
价格昂贵:最普通的一台 F5 就是一台“马 6”,好一点的就是“Q7”了。
-
扩展能力差:硬件设备,可以根据业务进行配置,但无法进行扩展和定制。
-
-
-
软件负载均衡
-
优点:
-
简单:无论是部署还是维护都比较简单。
-
便宜:只要买个 Linux 服务器,装上软件即可。
-
灵活:4 层和 7 层负载均衡可以根据业务进行选择;也可以根据业务进行比较方便的扩展,例如,可以通过 Nginx 的插件来实现业务的定制化功能。
-
-
缺点:
-
性能一般:一个 Nginx 大约能支撑 5 万并发。
-
功能没有硬件负载均衡那么强大。
-
一般不具备防火墙和防 DDoS 攻击等安全功能。
-
-
负载均衡典型架构
前面介绍了 3 种常见的负载均衡机制,每种方式都有一些优缺点,但并不意味着在实际应用中只能基于它们的优缺点进行非此即彼的选择,反而是基于它们的优缺点进行组合使用。具体来说,组合的基本原则为:DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。
DNS负载均衡和硬件负载均衡能优化的空间有限,能优化的空间更多的落在了软件负载均衡层面。软件负载均衡哪些层面可以优化呢?
软件负载均衡算法
Nginx负载均衡的常见5种算法:round robin(默认)、weight(轮询权值)、ip_hash、url_hash(第三方)、fair(第三方)。
-
round robin(默认)
轮询方式,依次将请求分配到各个后台服务器中,默认的负载均衡方式。
适用于后台机器性能一致的情况。挂掉的机器可以自动从服务列表中剔除。
upstream bakend {
server 192.168.0.1;
server 192.168.0.2;
}
-
weight(轮询权值)
weight的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。或者仅仅为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
upstream bakend {
server 192.168.0.1 weight=5;
server 192.168.0.2 weight=10;
}
-
ip_hash
每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题。
upstream bakend {
ip_hash;
server 192.168.0.1:88;
server 192.168.0.2:80;
}
-
url_hash(第三方)
根据请求的url的hash值将请求分到不同的机器中,当后台服务器为缓存的时候效率高。在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法
upstream backend {
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}
-
fair(第三方)
根据后台响应时间来分发请求,响应时间短的分发的请求多。
upstream backend {
server server1;
server server2;
fair;
}
另外Nginx结合使用OpenResty可以实现更强大的通用Web应用平台。
请求透过负载均衡会落到微服务上,如何通过提升微服务将性能发挥到极致呢?我们知道RPC 通信是微服务的核心。目前,很多微服务框架中的服务通信是基于 RPC 通信实现的,业界常用的就是SpringCloud、Dubbo。
SpringCloud 是基于 Feign 组件实现的 RPC 通信(基于 Http+Json 序列化实现),Dubbo 是基于 SPI 扩展了很多 RPC 通信框架,包括 RMI、Dubbo、Hessian 等 RPC 通信框架(默认是 Dubbo+Hessian 序列化)。那两者性能如何?以下是基于 Dubbo:2.6.4 版本进行的简单的性能测试。分别测试 Dubbo+Protobuf 序列化以及 Http+Json 序列化的通信性能。
通过以上测试结果可以发现:无论从响应时间还是吞吐量上来看,单一 TCP 长连接 +Protobuf 序列化实现的 RPC 通信框架都有着非常明显的优势。
什么是 RPC 通信
RPC(Remote Process Call),即远程服务调用,是通过网络请求远程程序服务的通信技术。RPC 框架封装好了底层网络通信、序列化等技术,我们只需要在项目中引入各个服务的接口包,就可以实现在代码中调用 RPC 服务同调用本地方法一样。那我们应该选择什么样的RPC通讯机制呢?
高并发场景下的 RPC 通信优化路径
机器间的网络通信是基于网络传输协议和传输数据的编码、解码实现的。其中网络传输协议有:TCP、UDP协议,这两个协议都是基于Socket接口编程之上,其通讯流程如下: TCP、UDP通讯协议
TCP 协议实现的 Socket 通信是有连接的,而传输数据是要通过三次握手来实现数据传输的可靠性,且传输数据是没有边界的,采用的是字节流模式。
UDP 协议实现的 Socket 通信,客户端不需要建立连接,只需要创建一个套接字发送数据报给服务端,这样就不能保证数据报一定会达到服务端,所以在传输数据方面,基于 UDP 协议实现的 Socket 通信具有不可靠性。UDP 发送的数据采用的是数据报模式,每个 UDP 的数据报都有一个长度,该长度将与数据一起发送到服务端。
通过对比,我们可以得出优化方法:为了保证数据传输的可靠性,通常情况下我们会采用 TCP 协议。
-
选择合适的通信协议
-
使用单一长连接
服务之间的通信不同于客户端与服务端之间的通信。客户端与服务端由于客户端数量多,基于短连接实现请求可以避免长时间地占用连接,导致系统资源浪费。但服务之间的通信,连接的消费端不会像客户端那么多,但消费端向服务端请求的数量却一样多,我们基于长连接实现,就可以省去大量的 TCP 建立和关闭连接的操作,从而减少系统的性能消耗,节省时间。
-
优化 Socket 通信
高效的 Reactor 线程模型。Reactor 模式有3种典型的实现方案:
1.单 Reactor 单进程 / 线程。
2.单 Reactor 多线程。
3.多 Reactor 多进程 / 线程。
高性能缓存架构
微服务之间请求是调用具体的业务接口,业务接口通过查询数据库返回相应的数据。关系数据库虽然有着强大的SQL功能和事务。但在高并发场景下I/O会很高或者海量数据下,性能会很差。实际业务中往往查询的数据并不是从一张表里获取,需要关联多张表甚至查询多个微服务聚合的数据,当遇到这种情况时,通常首先想到的方案就是NoSQL存储。
常用的NoSQL方案有4类
-
K-V存储:以Redis、MC为代表
-
文档数据库:以MongoDB为代表
-
列式数据库:以HBase为代表
-
全文搜索引擎:以ES为代表
NoSQL存储虽然没有不支持完整的事务操作,但有着较高的性能和灵活的存储格式。
Redis 的 Value 的数据结构包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog。
MongoDB文档型数据库最大特点就是no-schema,支持任意结构的JSON数据。
HBase列式数据库是按行存储在一起的,一次磁盘操作就能够把一行数据中的各个列都读取到内存中。而且还具备更高的存储压缩比,节省更多空间。
ES全文搜索引擎,通过倒排索引建立单词到文档的映射,以快速的速度返回结果。
高性能存储架构
关系数据库,单表在海量数据下即使创建了索引,性能也会很差。这个时候就要考虑读写分离或者分库分表。
读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上,减少单台服务的I/O压力。
读写分离的基本实现是:
-
数据库服务器搭建主从集群,一主一从、一主多从都可以。
-
数据库主机负责读写操作,从机只负责读操作。
-
数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
-
业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
读写分离的复杂性:
-
分配机制。查哪个库哪张表,用什么算法。一般采用中间件封装(MySQL Router、Atlas、Mycat、Sharding-JDBC)。
-
复制延迟。如果业务服务器将数据写入到主服务器后立刻进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。解决主从复制延迟有几种常见的方法:
写操作后的读操作指定发给数据库主服务器
读从机失败后再读一次主机
关键业务读写操作全部指向主机,非关键业务采用读写分离
分库分表
读写分离分散了DB读的压力,但没有分散写的压力,当数据量达到一定级别后,写能力会成为瓶颈。分库分表包括“分库”和“分表”两大类。
分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
分库的复杂性:
-
join 操作问题
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询
-
事务问题
原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改
-
成本问题
业务分库同时也带来了成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台
分表
单表数据拆分有两种方式:垂直分表和水平分表。
分表的复杂性:
-
二次查询
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去,当用到该列时需要二次查询
-
路由
水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。常见的路由算法有:范围路由、Hash 路由、配置路由
不管是读写分离还是分库分表,都是以空间换时间来提高性能。但也带来了很多的复杂性。鱼与熊掌不可兼得,还是要根据具体业务具体对待。
高可用架构
高可用架构核心准则:数据和服务的冗余备份以及失效转移。通俗讲就是数据服务器和业务服务器多节点(节点>=2)。谈到高可用架构必须要了解CAP原则。CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。高可用必须有多节点,必须选择P(分区容错性),所以分布式架构只能选择CP或者AP模式。
-
服务冗余
-
CP:优先保证一致性和分区容错性。在数据一致性要求比较高的场景中,一旦发生网络故障或者数据丢失,就会牺牲用户体验。恢复后才能访问。如:银行存钱,账户有1000元另外存入1000。此时,写库发生异常会提示用户存钱失败,等恢复之后用户才能使用。
-
AP:优先保证可用性和分区容错性。为了保证可用性,分区发生后,N1节点新增了数据Y,由于N1和N2节点通讯问题导致同步失败,N2节点还是数据X。当用户访问到N2节点时访问的还是旧数据,这就不满足一致性(Consistency)。典型的场景:A用户发布微博消息后,部分节点数据同步失败,导致部分用户看到A发布的消息还是老消息。这种场景不要强求一致性,能保证最终一致性就可以。
-
-
数据存储冗余
存储高可用方案的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来实现高可用,其复杂性主要体现在如何应对复制延迟和中断导致的数据不一致问题。因此,对任何一个高可用存储方案,我们需要从以下几个方面去进行思考和分析:
1.数据如何复制?
2.各个节点的职责是什么?
3.如何应对复制延迟?
4.如何应对复制中断?
常见的双机高可用架构:主备、主从、主备 / 主从切换和主主。
可扩展架构
可扩展性架构的设计方法很多,但万变不离其宗,所有的可扩展性架构设计,背后的基本思想都可以总结为一个字:拆!将大的变小,对小的进行改动来缩小范围、降低风险。但拆也不是随意拆,是要按照不同维度来分析。常见拆分思路有以下3种。
-
面向流程拆分:将整个业务流程拆分为几个阶段,每个阶段作为一部分。
-
面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。
-
面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。
设计模式
开发过程中如果能用好设计模式,在扩展的时候也能达到事半功倍的效果。常见的设计模式有以下几种。
-
工厂模式
-
抽象工厂模式
-
观察者模式:方便增加观察者,方便系统扩展
-
模板方法模式:方便的实现不稳定的扩展点,完成功能的重用
-
适配器模式:方便的对适配其他接口
-
代理模式:方便在原来功能的基础上增加功能或者逻辑
-
责任链模式:方便增加拦截器/过滤器实现对数据的处理,比如Tomcat的责任链
-
策略模式:通过新增策略从而改变原来的执行策略
写在最后
技术的发展和生物的进化是一样的,新的技术不断在衍生,旧的技术不断在淘汰。由简单到复杂,由低等到高等,不断发展变化。背后的一切源于企业生存度下降,为了更好的生存就需要不断的”盈利“,“盈利”需要作出更好的产品来满足更多人的需求,而人又是复杂多变的。知道了这些才会明白一切都在进化,需要我们能做的就是不断学习,不断进化。
成长篇
技术篇
扫码关注公众号,一起持续进化
👇