1 核心概念,单一服务器到更强的服务器
垂直伸缩
- 通过使用RAID增加I/O吞吐能力
- 通过切换SSD改善I/O访问速度
- 通过增加内存减少I/O操作(意味着文件系统有更多的缓存空间,应用程序有更多的工作内存)
- 通过升级/增加网络接口提高网络吞吐能力
- 更新服务器获得更多处理器或者更多虚拟核
- 主要制约:成本,当越过某个点后,垂直伸缩会变得格外昂贵;CPU速度;每台服务器的虚拟核数目;硬盘速度。
服务分离
- 不同的物理机上安装不同的服务类型,比如FTP,DNS,缓存,等。
- 功能分割:将整个Web应用切分成一组不同的功能模块,将他们独立部署。
内容分发网络(CDN)
CDN 缓存静态文件。用户将从最近的CDN服务器获取静态内容,进而加速页面加载时间。
分散访问流量:水平伸缩
通过增加服务器提升计算能力。
服务全球用户的伸缩性架构。
- 使用GeoDNS服务:基于客户地理位置进行ip地址解析的DNS服务。用户会访问到离他最近的一个Web服务器,将网络延迟减到最小。
- 边缘缓存(edge-cache)服务器:边缘缓存是一种距离用户较劲的HTTP缓存服务器,便于部分缓存用户的HTTP流量。
分层架构
- 将不同功能划分到不同层次的架构。
- 低层的组件暴露一组API给高层组件调用。
- 每一层都消费其低层提供的服务,但是低层永远不会消费上层提供的服务。
- 分层可以强制结构化并减少耦合,低层组件变得更简单和系统。越底层稳定性越强。
支持技术
- Web周边,通常用一些第三方软件技术实现,比如消息队列,应用缓存,主数据存储,搜索引擎等。
- 这些应该是一种即插即用的扩展组件,保证在组件进行更换时,整个架构不受影响。
2 软件设计原则
简单
简化从以下四个基本步骤进行
隐藏复杂与构建抽象
- 系统很庞大时是无法保持整体简单的,能做的只是保持局部简单。
- 达到局部简单,最主要的方式是确保任何单个的类/模块/应用的设计目标及工作原理都能被快速理解。
- 好的设计原则是类之间的依赖关系尽量少。
- 在庞大复杂的系统中,当创建一个独立的服务时,需要添加一个抽象层,所有的功能层都在这些抽象层基础上去实现,从而隐藏功能层的复杂性。
避免过度设计
- 好的设计方法是可以在后期逐渐添加新的功能和特性,而不是一开始就开发一个超级大的系统。
- 早期构建一个合理的抽象层次,然后迭代的增加新特性。
尝试测试驱动开发(TDD)
- 先写测试代码,然后写功能实现代码。
- 有助于工程师可以更好地把握工作重点,从用户视角看待问题,使开发的接口更简单清晰。
从软件设计的简化范例中学习
低耦合
- 低耦合可以保证复杂性局部化。
- 来更高层面上进行结构,意味着将系统分成多个应用,每个应用都只关注相对较小的一部分功能。这样每个应用就可以按需分别伸缩。
- 促进低耦合:要在较高的抽象层面降低耦合度,需要让系统不同的部分接触面尽量少。
- 避免不必要的耦合:一般来说,画系统架构设计图很容易暴露循环依赖,所以在设计阶段一定要画架构图。一个良好设计的模块儿叫扣图,看起来像一棵树,而不像一个社交网络图。
- 低耦合范式:一个很好的低耦合设计例子是UNIX命令行编程及管道pipe用法。
不用重复自己
基于约定编程。
画架构图
- 用例图:不考虑技术方案,仅仅关注业务需求,可以将功能特性和业务需求提炼出来。
- 类图:展现独立模块的结构。一个典型的类图包含接口,类,关键方法名,以及他们的关系。依赖可以用一条线来表示,所以从类图上可以很容易的看出耦合关系。
- 模块图:模块图描述了结构和依赖关系。他关注的抽象层次更高,可以认为是代码层面的缩略图,是类图的上面一层架构图,展示模块之间的依赖和交互。
单一职责
开闭原装
向扩展开放,向修改关闭。
依赖注入
- 依赖注入的核心思想是知道的越少越好。
- 依赖注入对类需要依赖的对象提供一个引用实例,而无需类自己创建依赖对象。
为伸缩而设计
- 增加副本:通过增加副本,实现伸缩性,负载均衡。对于Web层,这样是最简单的,只需要增加服务器即可。
- 功能分割:适用于各个层面,核心是发现系统的各个功能组成部分,然后将它们创建成为独立的子系统。
- 数据分片:将不同的分片数据保存在不同的机器上,以保证每台服务器上存储的数据量都不会太大,也被称为无共享架构原则。
自愈设计
- 设计软件一定要考虑高可用和系统自愈能力。
- 高可用一般用几个9来衡量。
- 系统自愈能力主要体现在系统宕机等情况下,无需人工干预,可以自动恢复工作,并修复必要的错误数据。
4 Web服务
Web服务设计
Web服务作为一种备用表示层
- 其实就是前后端不分离,通常使用MVC框架开发。
- 好处是可以延迟实现各种Web服务的代码,直到你证明了这个产品确实是可行的。
- 在开发的早期阶段可以在代码层面快速增加新功能和应对变化,同时也降低了系统的总体复杂性。
API优先方式
- 前后端分离开发,有助于客户端和服务端解耦,独立伸缩。
- 需要避免过度设计
- 业务逻辑的复杂性,从客户端代码推到了Web服务层。
- 系统伸缩更容易,可以进行功能分割,将Web服务分割成一组更小的独立的服务。
务实的方式
- 结合以上两种方式构建应用
- 当你发现某个需求很容易就分离出一个独立的服务,而且这个功能也会被多个客户端调用,那么就可以考虑将其服务化。
Web服务类型
以功能为中心的服务
- 是指能够调用远程机器上的功能或者对象方法,无需知道这些功能或者对象是如何实现的,以何种语言编写以及运行在什么样的架构上。
- 将请求参数和数据序列化,然后传输到服务上,服务把结果返给你,本地再将结果进行反序列化,即可使用。
- SOAP
以资源为中心的服务
REST服务
伸缩REST Web 服务
主要是把Web服务层按功能切分成比较小的粒度,然后增加备份,实现伸缩。
保持服务无状态
把Web服务所有的共享状态移出共享数据存储设备上,比如缓存,数据库,消息队列等。好处如下:
- 可以基于每个请求进行流量分派。
- 方便对Web服务节点操作:切换,关闭,移除,切换部署版本等,易于操作。
- 系统伸缩性方便,比如使用负载均衡器实现自动伸缩。
缓存服务响应
通常在客户端和Web服务之间部署一个反向代理。做HTTP缓存。
功能分割
功能分割就是拆分出系统的子功能,构成一个独立的子系统。
5 数据存储层
MySQL伸缩性
复制
- 主从复制,例如部署多台从服务器,实现分布式查询。
- 主从配置方案:将只读操作分布在多个从服务器上;针对不同的查询使用不同的从服务器,例如慢查询一台,一般查询一台;增加可用性,实现一个零停机时间的备份
- 主-主结构,AB两台机器互相复制数据,MySQL支持环形复制,一般不建议这么做,比较复杂,且数据不一致风险大。
- 环形复制会导致系统可用性降低,失效恢复困难,额外增加复制延迟,主服务器越多,宕机概率越高。
数据分区/分片
选择分片键
- 核心原理是将数据以某种方式切分,以便每台服务器都只存储一部分数据。
- 或选择分片键和映射算法,将数据分布到多台服务器上。比如两个服务器分别存储用户ID为奇数和偶数的用户信息。
- 优点:可以根据数据量对数据库进行水平伸缩;可以在应用层实现,而在数据层看到效果。
- 缺点:无法对多分片数据进行联合查询;无法使用数据库的ac id事务;随着数据增长,分片键和服务器编号映射算法难度增加;往集群中添加新的服务器难度极大。
- 可以利用一个独立的数据库存储用户和服务器编号的映射关系,方便在不同分片之间进行数据迁移。
6 缓存
缓存命中率
影响缓存命中率的因素:
- 缓存键集合大小:尽量减少可能的缓存键数量,数量越少,命中率越高
- 缓存可使用内存空间的大小,理论上能缓存的对象越多,命中率就越高
- 平均生存时间(TTL),理论上缓存时间越长,命中率就越高
基于HTTP的缓存
HTTP 缓存头 Cache-Control
- private 只能特定响应给请求用户,不能提供给其他用户
- public 可以在用户间被共享
- no-store 响应可以缓存在内部,不能持久化到磁盘
- no-cache 响应不能被缓存
- max-age 定义响应的TTL,缓存时间
- no-transfer 响应不做任何修改,直接用缓存
- Must-revalidate 一旦响应过于陈旧,不重新验证就不能返回给客户端
- 要避免使用缓存相关的 HTML 元标记,因为他们不能用来控制中间缓存,还会带来混乱。
HTTP 缓存技术
- 浏览器缓存
- 缓存代理
- 反向代理
- CDN
7 异步处理
- 同步处理和异步处理:
简单来讲,同步处理是阻塞的,必须先做完一件事,才能做第二件事;异步处理是非阻塞的,第一件事做到一半可以在后台另开一个线程去做,直接做第二件事情。 - 消息队列:实现异步处理的工具。有消息生产者创建消息,由消息队列存储消息,有消费者去对消息进行处理。一个可伸缩系统中的生产者和消费者通常是独立的进行或线程运行,二者解耦,且可以独立伸缩。
消息生产者
一般消息使用XML或JSON格式。
消息代理
消息队列有很多实现方式,比如最简单的共享文件夹,也可以是SQL数据库,也可以是支持接收、路由、持久化和发送消息的专用消息代理。消息代理是解耦生产者和消费者的关键元素。
消息消费者
实现模式主要有两种,周期模式和守护模式。路由方法主要有以下几种
- 直接工作队列模型:消费者只需要指定队列的名称去消费信息,多个消费者竞争消费消息,很适合多个机器上分配一些耗时任务。主要适用于发送邮件,处理视频,调整图片大小或向第三方Web服务上传内容等。
- 发布订阅模型:生产者发送消息到一个主题,而不是队列,消费者都可以获取这个主题的消息。
- 定制路由规则:有些消息代理也支持各种形式的路由定制,可以更灵活的处理消息,比如:RabbitMQ中,可以绑定一个概念来创建灵活的路由规则(基于文本模式匹配),在ActivateMQ中,可以使用Camel扩展来创建更高级的路由规则。
消息协议
- AMQP(高级消息队列协议):明确定义消息发布、消费和传输的协议,RabbitMQ和在ActivateMQ都支持。
- STOMP(流式面向文本的消息协议):极简化的协议,无状态,类似HTTP的,基于文本的协议,在协议层不需要更多的性能开销,一些高级特性需要通过定制头信息来扩展实现,比如预取数量就是一个很好的特性,可以通过声明想批量获取的消息数来提高吞吐量。RabbitMQ和在ActivateMQ都支持。
- JMS(Java消息服务):是一个Java消息标准,有语言限制。
消息基础设施
- 将队列分隔成几组不同的服务器,这样独立的队列就不依赖于其他组件,你简单的通过添加机器实现水平伸缩。
- RabbitMQ和ActiveMQ能胜任每秒数万级别消息的吞吐量,如果计划处理每秒10万级别的消息,需要自己制定分区机制,将负载分散到多个消息代理实例。
评估消息代理软件
- 每秒发送的消息数。
- 平均每条消息的大小。
- 每秒消费的消息数。
- 并发发布者数量。
- 并发消费者数量。
- 是否考虑消息持久化?
- 是否需要消息通知,比如消费者崩溃的情况下,消息不丢失。
消息队列的好处
- 异步处理。
- 易伸缩。
- 使峰值变平缓。
- 隔离失效机器及自我修复。
- 解耦。
消息平台比较
- 亚马逊简单队列服务 SQS:具有无设置,无匹配,无维护,无意外的优点,使用成本最低,适合初创公司,吞吐量高。有消息大小限制,不保证顺序,有偶发的消息重发情况。
- RabbitMQ:灵活性强,能动态地配置路由,实现生产者与消费者解耦,缺少局部消息排序和会坏消息支持。
- ActiveMQ:用Java编写,可以使用Camel实现RabbitMQ高级路的一些复杂特性。不能使用远程api实现完全控制,发送大流量消息时,对峰值敏感,可能会因为那内存不足崩溃。
8 数据搜索
索引
- 索引是结构的,按特定顺序排序的,并为特定类型的搜索进行优化。
- 数据集在大小上减少了,因为索引比整个文本内容少很多,所以能很快的加载和处理。
- 大部分索引都是排序的。一个简单快速搜索算法的例子就是二分查找算法。
- 索引对搜索来说非常有用,但是他需要增加一些额外的开销。维护索引需要排序项来维护额外的数据结构,随着数据集增长,数据结构会比较大,代价比较高。
- 添加索引需要仔细的规划,明确哪些数据值得索引。
- 基数越高,索引性能越好。
- 均匀分布时,索引性能更好。
- 复合索引的索引字段顺序需要仔细考虑。
NoSQL数据建模
- 键值数据存储:适用于一对一查询,例如Redis,Memcached常用于缓存。
- 宽列数据存储:这些数据存储按照复合索引对数据建模,构建了排序列表,例如BigTable,HBase,Cassandra。
- 面向文档的数据存储:允许构建更复杂的对象和索引,文档数据结构包含数组,映射和嵌套数据结构,比如JSON和XML。常用数据库有MongoDB,CouchDB,CouchBase。
搜索引擎
- 全文本搜索使用倒排索引
- 使用专业搜索引擎,比如Amazon CloudSearch,Azure Search,Elassticsearch,Solr,Sphinx。