Architecture

最近在看后台相关的技术书籍,总结一下架构相关的知识。

复杂度

软件架构:一些列相关的抽象模式,用于指导大型软件系统各个方面的设计。

架构设计的目的:解决软件系统复杂度带来的问题

高性能

高性能带来的复杂度主要体现在:单机计算机内内部为了高性能带来的复杂度;多台计算机集群为了高性能带来的复杂度

单机复杂度

最关键的地方就是操作系统,操作系统和性能相关的就是进程和线程。

要考虑高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点

扫盲一下几种常见的服务器

Apache
Apache HTTP Server(简称 Apache)是 Apache 软件基金会的一款开放源码的 WEB 服务器软件

优点:稳定、开源、跨平台,适合处理动态请求
缺点:不支持高并发

发展时期很长,它兴起的年代,互联网产业远远比不上现在。所以它被设计为一个重量级的。在 Apache 上运行数以万计的并发访问,会导致服务器消耗大量内存。操作系统对其进行进程或线程间的切换也消耗了大量的 CPU 资源,导致 HTTP 请求的平均响应速度降低。

Nginx
同 Apache 一样都是一种 WEB 服务器,是轻量级高并发 HTTP 服务器和反向代理服务器。
优点:轻量级、高并发、处理静态文件好消耗内存少,提供负载均衡
缺点:需要配合其他后端使用,没有 Apache 稳定

正向代理最大的特点是客户端非常明确要访问的服务器地址
反向代理,”它代理的是服务端,代服务端接收请求”,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息

常见的网站架构有:Nginx + php、Nginx + tomcat、Nginx + apache + php 等

名称 是否支持多进程 是否支持多线程
Nginx
JBoss
Redis
Memcache

以上 Redis 是单进程单线程的,但是它的效率不比单进程多线程的同样基于内存的 KV 数据库 Memchache 差,官方提供的数据是可以达到 10万+ 的 QPS(每秒查询次数)。原因是它是基于内存存储,数据查找类似于 HashMap 时间复杂度是 O(1);数据结构简单;单线程模式不用上下文切换和竞争条件;使用多路 I/O 复用模型,非阻塞 IO。“多路”指多个网络连接,“复用”指复用同一个线程。多路 I/O 复用技术可以让单个线程高效的处理多个连接请求。

集群的复杂度

  • 任务分配:每台机器都可以处理完整的业务任务

任务分配器

硬件网络设备 (例如:F5、交换机)
软件网络设备 (例如:LVS)
负载均衡软件(例如:Nginx、HAProxy)

任务分配器从 1 台变为多台时,复杂度提高,需要将不同用户分配到不同的任务分配器上,常见方法包括

DNS 轮询、智能 DNS、CDN(Content Delivery Network 内容分发网络)、GSLB(Global Server Load Balance 全局负载均衡)设备

  • 任务分解:通过任务分解将原来大而统一的复杂业务系统拆分为小而简单的多个系统配合的业务系统

高可用

系统 无中断 地执行其功能的能力

  • 计算高可用
    同样输入同样输出,其复杂度表现为:加入任务分配器;任务分配器和真正的业务服务器之间的连接和交互选择合适的连接方式;任务分配器需要增加分配算法。
    ZooKeeper 采用 1 主多备
    Memcached 采用 全主 0 备

  • 存储高可用
    减少或者规避数据不一致对业务造成的影响。分布式领域著名的 CAP 定理,从理论上论证了存储高可用的复杂度。

CAP 理论告诉我们:一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求。

状态决策
决策方式

  • 独裁式
  • 协商式
  • 民主式, ZooKeeper 集群在选举 leader 时采用的这种方式。该方式有一个固有的缺陷:脑裂,为了解决脑裂问题,民主式决策系统一般都采用“投票节点数必须超过系统总结点数一半”规则来处理。

可扩展

数据库,例如 MySQL 替换为 Oracle
接口协议,例如 HTTP 接口协议支持 ProtocolBuffer
将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”

低成本、安全、规模

低成本,“创新”才能达到低成本目标,例如

  • NoSQL (Memcache、 Redis)的出现为了解决关系型数据库无法应对高并发访问带来的压力
  • 全文检索引擎 (Sphinx、Elasticsearch、Solr) 为了解决关系型数据库 like 搜索的低效的问题
  • Hadoop 为了解决传统文件系统无法应对海量数据存储和计算的问题

安全

  • 功能安全:XSS 攻击、CSRF 攻击、SQL 注入等
  • 架构安全:防火墙(将网络划分成不同区域,制定出不同区域之间的访问控制策略)。因防火墙性能一般所以传统的银行和企业应用领域较多,互联网应用场景并不多。

规模
规模带来复杂度的主要原因就是“量变引起质变”

架构设计三原则

  • 合适原则,合适优于业界领先
  • 简单原则,简单优于复杂
  • 演化原则,演化优于一步到位

架构设计流程

第 1 步:识别复杂度

架构复杂度主要源于“高性能”“高可用”“可扩展”三个方面。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含某两个,即使认为同时需要满足这三个方面,也必须进行优先级排序。

  • 是否需要高性能

估算 TPS (Transactions Per Second 每秒需要处理的事务个数) 和 QPS (Queries Per Second) 每秒查询数据次数。估算一天内平均每秒的 TPS 和 QPS ,峰值一般取平均值的 3 - 4 倍,不要设定在 10 倍以上。如果这两个数的数值过万说明需要高性能是复杂度之一。

  • 根据业务需要衡量是否需要高可用性、可扩展性、安全及成本等

第 2 步:设计备选方案

常见错误:设计最优秀的方案、只做一个方案、备选方案过于详细

备选方案应该选择适合的即可,数量以 3 ~ 5 个为最佳,差异要比较明显,只关注技术选型而不是技术细节

例如,实现微博系统,复杂性体现在高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。为这样的系统设计备选方案如下:
方案一:采用开源的 Kafka
方案二:集群 + MySQL 存储,Netty 构建高性能系统,数据分散集群,集群中的服务器分组,每个分组包含一个主备 SQL,分组内主备数据复制,分组间数据不同步
方案三:集群 + 自研存储方案,方案二的基础上,将 MySQL 存储替换为自研实现存储方案
此外,开源方案还可能包括 Kafka、ActiveMQ、RabbitMQ;集群方案存储既可以考虑 MySQL,也可以考虑 HBase,或者 MySQL + Redis 结合;自研文件系统可以参考 Kafka 或者 LevelDB、HBase 等。

第 3 步:评估和选择备选方案

每个方案都有一定的缺点,例如方案一有性能缺点,方案二有成本缺点,方案三有技术不成熟缺点。
应当列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,综合挑选出合适的最优方案。

具体做法可以列出每个方案根据不同质量属性的优缺点,排出优先级,之后根据合适原则选出最终方案。

第 4 步:详细方案设计

根据最终选择的方案进行详细设计。例如 MySQL 分库分表要如何确定哪些表要拆分,按什么维度拆分,如何处理分库分表后的联合查询;加入引用 Nginx 做负载均衡,Nginx 的主备如何做,负载均衡策略选择哪个?

Nginx 负载均衡策略分类:

  • 轮询(默认),按时间顺序逐一分配到不同后端服务器
  • 加权轮询,根据权重进行轮询,权重高的分配请求多
  • ip_hash,每个请求按访问 IP 的 hash 结果分配,这样每个访客固定访问一个后端服务器,主要用于解决 session 问题
  • fair,按后端服务器响应时间来分配请求,响应时间短的优先分配
  • url_hash,每个 URL 定向访问到一个后端服务器

如果要做电商类业务,由于会和 session 强相关,所以选择 ip_hash 策略比较合适。

列出一些细化设计点,例如:
数据库表的设计;
数据如何复制?主从;主备服务器如何倒换?ZooKeeper 做主备决策,备机监听主机节点消息,发现主服务器节点断连后,备服务器修改自己的状态,对外提供消息读取服务;
业务服务器如何写入、读取消息;
业务服务器和消息队列服务器之间的通信协议如何设计?TCP 传输,数据格式 ProtocolBuffer

高性能数据库集群

读写分离

读写分实现方法是:数据库服务器搭建主从集群,一主一从或者一主多从。主机负责读写,从机只负责读取。主机将数据同步到从机。读写分离引入的设计复杂度主要是:主从复制延迟 和 分配机制。

主从复制延迟常见的解决方法

  • 写操作后的读制定发给主机。这类方法,容易使新来的程序员不知道有这样的逻辑而引发 Bug。
  • 读从机失败后再读一次主机,即“二次读取”。给读操作带来更大压力。
  • 关键业务读写全部指向主机,非关键业务读写分离

分配机制:将读写操作区分开,然后访问不同的数据服务器。一般有两种方式:程序代码封装和中间件封装。

  • 程序代码封装:即代码中抽象一个数据访问层,实现读写操作分离和数据库服务器连接的管理,目前开源方案中 TDDL 是比较有名的。其特点是实现简单,可定制;每个编程语言都需要实现,无法通用,重复工作比较多;故障情况下,主从需要切换时需要所有系统修改配置
  • 中间件封装:指独立一套系统实现读写操作分离和数据库服务连接的管理。目前开源的中间件方案有 MySQL Router、Atlas。其特点是支持多种编程语言;但是为了支持完整 SQL 语法和数据库服务协议(如 MySQL 客户端和服务器连接协议),实现比较复杂,细节比较多,需要较长时间才能稳定;中间件性能要求较高;数据库中从切换对业务服务器无感知。

分库分表

读写分离分散了数据库读写操作压力,但没有分散存储压力,当数据量达到千万甚至上亿时,单台数据库服务器的存储能力会成为系统瓶颈。
主要体现为

  • 数据量太大,读写性能下降
  • 数据文件变大,数据库备份和恢复需要较长时间
  • 数据文件较大,极端情况下丢失数据的风险越高

业务分库:按照业务模块将数据分散到不同数据服务器。例如电商网站,将用户数据、商品数据、订单数据分开到三台不同数据库服务器上。

分库带来的问题

  • join 操作问题:分库带来的无法 join 查询问题,只能采取先到一个库中查询一部分数据,再到另一个库中查询。
  • 事务问题:原来用事务可以解决的问题,现在需要程序自己控制,例如电商库存扣除的问题。
  • 成本问题:原来 1 台服务器可以解决的问题,现在需要多台服务器。

分表:单表数据拆分有两种方式垂直分表和水平分表。垂直分表,按列拆分,引入的复杂度主要体现在对表操作数量的增加。 水平分表,按行拆分,会引入更多复杂性。

水平分表引入的复杂性问题

  • 路由,常见的路由算法有范围路由、Hash 路由、配置路由
  • join 操作,需要在业务代码或者中间件进行多次 join 查询,再将结果合并
  • count 操作,count 需要每个表 count 后相加,效率会降低;建立“记录数表”,每次记录增加就修改记录表数,这样会更写操作带来压力,可以使用 count 相加与记录数表结合的方式,定时通过 count 相加计算表的记录数,更新记录表中数据。
  • order by 操作,数据只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总排序。

NoSQL

NoSQL 非关系型数据库方案分类

  • K-V 存储:解决关系数据库无法存储数据结构问题,如 Redis
  • 文档数据库:解决关系数据库强 schema 约束的问题,如 MongoDB
  • 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表
  • 全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表

高性能缓存架构设计要点

缓存穿透:缓存没有发挥作用,业务系统去缓存查询数据,缓存中没有数据,需要再去存储系统查询。
要注意以下两种情况

  1. 存储数据不存在时,应当设置一个默认值存到缓存中
  2. 缓存数据需要耗费较长时间或大量资源,数据要做缓存,但是经常变化的数据可以设置缓存有效期较短

缓存雪崩:缓存失效后引起系统性能急剧下降。常见解决方案有两种:更新锁机制和后台更新机制

缓存热点:特别热点的数据要特殊处理,例如微博上明星官宣,短时间内会有千万用户围观。解决方案就是复制多份缓存副本,将请求分到到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。

高性能负载均衡

高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。任务分配器,现在更流行的通用叫法是“负载均衡器”,但这个名称有一定误导性,它会让人认为其目的是保持各个计算单元的负载达到均衡状态。而实际上任务分配的不只是考虑负载均衡,有的任务分配算法的目标是基于负载考虑,有的则基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。

负载均衡的分类主要包括 DNS 负载均衡、硬件负载均衡和软件负载均衡。

  • DNS 负载均衡,解析同一个域名返回不同 IP 地址。其优点是:简单、成本低;就近访问,提升访问速度。缺点是更新不及时;扩展性差;分配策略简单。h针对 DNS 的缺点有公司自己实现 HTTP-DNS 功能。方案和通用的 DNS 优缺点正好相反。
  • 硬件负载均衡,通过硬件设备实现负载均衡,目前业界典型的硬件负载均衡设备有两款 F5 和 A10。其优点是:功能强大,全面支持各层级负载均衡,支持全面的负载均衡算法,支持全局负载均衡;性能强大;稳定性高;支持安全防护,具备防火墙、防 DDos 攻击安全功能。缺点是价格高昂;扩展能力差。
  • 软件负载均衡,通过软件来实现负载均衡,常见的有 Nginx 和 LVS。软硬件负载均衡的最主要区别在于性能。 Ngxin 的性能是万级,一般 Linux 服务器装一个 Nginx 大概能到 5 万 / 秒;LVS 性能是十万级,据说能达到 80 万 / 秒。而 F5 能达到百万级,从 200 万 到 800 万 / 秒都有。

负载均衡算法答题分为以下几类

  • 任务平分类,可以是绝对数量的平均,也可以是比例或权重上的平均。通过轮询方式分配服务器,只要服务器在运行就会被分配,不关心运行状态,优化轮询的方式可以使用加权轮询。
  • 负载均衡类,负载可以是 CPU 负载、连接数、I / O 使用率、网卡吞吐量
  • 性能最优类,根据响应时间来分配,优先分配给响应最快的服务器
  • Hash 类,将 Hash 值分配到同一台服务器上,常见的有源地址 Hash、目标地址 Hash、SessionID Hash、用户 ID Hash 等

高可用存储架构

FMEA 方法排除可用性隐患的利器,样例

功能点 故障模式 故障影响 严重程度 故障原因 故障概率 风险程度 已有措施 规避措施 解决措施 后续规划
登录 MySQL 无法访问 当 MC 中无缓存时,用户无法登录,预计有 60%的用户 MySQL 服务器断电 增加备份 MySQL

集群

多台机器组成一起形成一个系统。
集群可以分为两类:数据集中集群(与主备、主从架构类似)、数据分散集群(多个服务器组成一个集群,每台服务都会负责存储一部分数据;同时,为了提升硬件利用率,每台服务又会备份一部分数据)。集中集群架构中,客户端只能将数据写入到主机;分散集群架构中客户端可以向任意服务器读写数据。

数据分区

将数据按照一定的规则进行分区,不同分区分布在不同的地理位置上,每个分区存储一部分数据,以此规避地理界别故障所造成的巨大影响。设计分区架构时应考虑以下几点:

  • 数据量大小,数据量越大,分区规则越复杂
  • 分区规则,按地理位置远近,分区主要包括洲际分区、国家分区、城市分区
  • 复制规则,常见的复制规则包括集中式、互备式和独立式

异地多活架构模式

  • 同城异区,业务部署在同一个城市不同区的多个机房。
  • 跨城异地,业务部署在不同城市的多个机房,且距离最好远一些。跨城异地距离较远带来的网络传输延迟问题,给异地多活架构带来了复杂性,例如业务系统需要考虑不熟在不同地点的两个机房,在数据短时间不一致的情况下,还能正常提供业务。一般对于强一致性要求的数据,例如银行存款余额、支付宝余额等,这类数据实际上无法做到跨城异地多活。
  • 跨国异地多活,业务部署在不同国家的多个机房。应用场景一般有为不同地区用户提供服务,只读类业务做多活

实现异地多活架构的技巧有

  • 保证核心业务的异地多活
  • 保证核心数据最终一致性。尽量减少异地多活机房的距离,搭建高速网络;尽量减少数据同步,只同步核心业务相关的数据;保证最终一致性,不保证实时一致性
  • 采用多种手段同步数据。消息队列方式、二次读取方式、存储系统同步方式、回源读取方式、重新生成数据方式
  • 只保证绝大部份用户的异地多活

设计步骤:

  1. 业务分级,常见分级标准有访问量大的业务;核心业务;产生大量收入的业务
  2. 数据分类,数据量、唯一性、实时性、可丢失性、可恢复性
  3. 数据同步,存储系统同步、消息队列同步、重复生成
  4. 异常处理,多通道同步、同步和访问结合、日志记录(故障恢复后对数据进行恢复)、用户补偿

接口级故障

异地多活主要应对系统级的故障,例如机器宕机、机房故障、网络故障等问题。
接口级故障主要为业务出现问题,例如业务响应缓慢、大量访问超时、大量访问出现异常,这类问题主要原因在于系统压力太大、负载太高、导致无法快速处理业务请求引发的后续问题,应对方式主要有

  • 降级:系统将某些业务或者接口的功能降低,可以只提供部分功能,也可以完全停掉所有功能
  • 熔断:类似于降级,当某服务出现不可用或者响应超时的情况时,暂时停止对服务的应用。两者区别是熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑。Netflix 的开源组件 Hystrix
  • 限流:只允许系统能够承受的访问进来,超出系统访问能力的请求被丢弃
  • 排队

参考文章

Nginx 介绍
Redis 单线程为什么效率很高
从分布式一致性谈到 CAP 理论、BASE 理论

请我喝汽水儿