Service System Architecture
第一章:系统体系结构的内涵及要点
系统体系结构概述
什么是系统体系结构
软件架构的定义:架构 = 构件 + 连接件 + 拓扑结构 + 约束 + 质量
为什么需要“软件系统体系结构”
结论:对于大规模的复杂软件系统来说,对系统全局结构的设计比起对算法的选择和数据结构的设计明显重要得多。
对于大型复杂软件系统,需要专业的架构师来负责系统架构的设计。
软件系统体系结构的目标与作用
目标
软件架构关注的是:
- 如何将复杂的软件系统划分为模块
- 如何规范模块的构成
- 如何将这些模块组织为完整的系统
- 以及保证系统的质量要求
主要目标:
- 建立一个一致的系统及其视图集,并表达为最终用户和软件设计者需要的结构形式,支持用户和设计者之间的交流与理解。
分为两方面:
- 外向目标:建立满足最终用户要求的系统需求;
- 内向目标:建立满足系统设计者需要以及易于系统实现、维护和扩展的系统构件构成。
作用(体现在软件生命周期中)
- 交流的手段
- 可传递的、可复用的模型
- 关键决策的体现
重要意义
- SA是软件开发过程初期的产品,在开发的早期阶段就考虑系统的正确设计与方案选择,为以后开发、测试、维护各个阶段提供了保证;
- 与其他后期的设计活动相比,SA设计的成本和代价要低得多;
- 正确有效的SA设计会给软件开发带来极大的便利;
- 在大型软件系统中,质量属性更多的是由系统结构和功能划分来实现的,而不再仅仅依靠所选择的算法或数据结构。
如何成为架构师
- 全局观(应有一种从全局的角度对软件进行设计的“视图”)
- 折中观(各类功能与非功能需求之间存在着大量的“矛盾”)
- 交流观(公共的沟通“媒介”)
- 复用观(不同的软件系统之间是否存在整体风格上的相似性)
架构师的关注点:软件质量(软件与明确的和隐含的需求相一致的程度)
注意:如果提高一个质量,经常会影响(提高或降低)其它质量
软件质量因素及对策:
- 性能(Performance):软件的“时间-空间”效率;
- Architect的手段:集群、缓存、异步
- 安全性(Security):在对合法用户提供服务的同时,阻止未授权用户的使用企图;
- Architect的手段:防火墙、入侵检测、加密、单一入口
- 易用性(Usability):用户使用软件的容易程度,用户容易使用和学习;
- Architect的手段:统一风格界面、定制
- 重用性(Reusability):减少代码编写,提高团队开发效率,降低维护代价;
- Architect的手段:构件Component、框架Framework、软件产品线ProductLine
- 健壮性/可用性(Robustness/Availability):在异常情况下,软件能够正常运行的能力
- Architect的手段:集群、灾备、故障转移
- 可修改性(Modifiability):软件适应“变化”的能力
- Architect的手段:稳定的功能分解、设计模式、分层、解耦
- 可测试性(Testability):软件是否易于被测试
- Architect的手段:一致的错误处理方式
- 集成性(Integrability):让分别开发的组件在一起正确工作的能力;
- Architect的手段:最小化接口复杂度、统一命名规范、遵循标准
- 移植性(Portability):是软件不经修改或稍加修改就可以运行于不同软硬件环境(CPU、OS和编译器)的能力;
- Architect的手段:开发语言选择、分层、遵循标准
- 兼容性(Compatibility):不同产品相互交换信息的能力;
- Architect的手段:API设计、遵循标准
- 经济性(Economy):开发成本、开发时间和对市场的适应能力。
- Architect的手段:重用性、可修改性
- 正确性(Correctness):软件按照需求正确执行任务的能力;
- 完备性(Completeness) :软件能够支持用户所需求的全部功能的能力;
- 其他商业质量:上市时间、成本/受益、目标市场、生命周期长短等。
架构师如何提高软件质量
-
架构的选择极大地影响部分软件质量,但不是全部
-
架构只为获得某个质量创造条件,但并不能保证肯定获得
软件系统体系结构的发展与演化
- 系统 = 算法 + 数据结构 (1960’s )
- 系统 = 子程序 + 子程序 (1970’s )
- 系统 = 对象 + 对象 (1980’s )
- 系统 = 构件 + 连接件 (1990’s )
- 系统 = 服务 + 服务总线 (2000’s)
- 系统 = 服务集群 + 中间件 (now)
系统规模:简单->复杂
系统开发:封闭->开发
模块粒度:细->粗
关注层面:模块->连接件
系统体系结构与中间件
由来
1968IBM的CICS:萌芽。1990BELL实验室的Tuxedo:正式成型。1994IBM的MQ系列:消息中间件。J2EE发布后:中间件核心。
定义
- 一组程序,应用于分布式系统各应用之中,为系统屏蔽底层通讯和提供公共服务,并保障系统的高可靠性、高可用性、高灵活性。
- 分布式应用借助中间件在不同技术之间共享资源。
- 中间件位于客户机/ 服务器的操作系统之上,管理计算机资源和网络通讯。
- 中间件是连接两个独立应用程序或独立系统的软件,即使它们具有不同的接口。
- 通过中间件,应用程序可以工作于多平台或OS环境
作用
- 屏蔽异构型
- 异构性表现在计算机的软硬件差异,包括硬件(CPU和指令集、硬件结构、驱动程序等),操作系统(不同OS的API和开发环境)、数据库(不同的存储和访问格式)等。
- 实现互操作
- 因为异构性,产生的结果是软件依赖于计算环境,使得各种不同软件之间在不同平台之间不能移植,或者移植困难。而且,因为网络协议和通信机制不同,这些系统不能有效相互集成。
- 共性凝练和复用
- 软件应用领域越来越多,相同领域的应用系统之间许多基础功能和结构是有相似性的。通过中间件提供简单、一致、集成的开发和运行环境,简化分布式系统的设计、编程和管理。
分类
- 应用服务类中间件
- 为应用系统提供一个综合的计算环境和支撑平台,包括对象请求代理(ORB)中间件、事务监控交易中间件、Java应用服务器中间件等。
- 应用集成类中间件
- 提供各种不同网络应用系统之间的消息通信、服务集成和数据集成的功能,包括常见的消息中间件、企业集成EAI、企业服务总线以及相配套的适配器。
- 业务架构类中间件
- 除了可以将底层共性技术的特征抽象到中间件,还可以将业务共性抽象至中间件,形成应用模式,如业务流程,业务模型,业务规则,交互应用等。
系统体系结构与中间件设计过程
软件架构设计的模型
(Kruchten)4+1视图模型
- 用例视图:描述系统的典型场景与功能,主要图形包括use casediagram等。
- 逻辑视图:描述系统的抽象概念与功能(类、对象、接口、模式等),主要图形包括class diagrams, Communication diagrams and sequence diagrams等;
- 开发视图:描述系统中的子系统、模块、文件、资源及其之间的关系,主要图形包括component diagrams, package diagrams等;
- 进程视图:描述系统的进程及其之间的通信协作关系,主要图形包括activity diagram, sequence diagram等;
- 物理视图:描述系统如何被安装、部署与配置在分布式的物理环境下,主要图形包括deployment diagram等。
第二章:软件设计模式基础
软件设计模式概述
软件设计模式简介
- 最佳实践
- 提供标准术语系统
- 代码编制工程化
软件设计模式类型
- 创建型模式
- 工厂模式
- 抽象工厂模式
- 单例模式
- 结构型模式
- 适配器模式
- 桥接模式
- 代理模式
- 行为型模式
- 中介者模式
- 观察者模式
- 访问者模式
- J2EE设计模式
软件设计模式原则(SOLID+Ex2)
- 单一职责原则
- 开闭原则
- 里氏代换原则
- 依赖倒转原则
- 接口隔离原则
- 迪米特法则
- 合成复用原则
常用软件设计模式
单例模式
模式意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
解决问题:一个全局使用的类频繁地创建与销毁。
使用时机:当你想控制实例数目,节省系统资源的时候。
解决方案:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的
- 线程不安全懒汉式
- 线程安全懒汉式(方法整体加锁)
- 饿汉式
- 双重校验锁懒汉式(初始化实例的代码块加锁)
- 静态内部类懒汉式
工厂模式
模式意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
解决问题:主要解决接口选择的问题。
使用时机:明确地计划不同条件下创建不同实例时。
解决方案:让其子类实现工厂接口,返回的也是一个抽象的产品。
关键代码:创建过程在其子类执行。
抽象工厂模式
模式意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
解决问题:主要解决接口选择的问题。
使用时机:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
解决方案:在一个产品族里面,定义多个产品。
关键代码:在一个工厂里聚合多个同类产品。
适配器模式
模式意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
解决问题:解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
使用时机:
- 系统需要使用现有的类,而此类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
- 通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
解决方案:继承或依赖(推荐)。
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
桥接模式
模式意图:将抽象部分(变化的一方面)与实现部分(变化的另一方面)分离,使它们都可以独立的变化。
解决问题:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
使用时机:实现系统可能有多个角度分类,每一种角度都可能变化。
解决方案:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
关键代码:抽象类依赖实现类(是接口,不是实现)。
代理模式
模式意图:为其他对象提供一种代理以控制对这个对象的访问。
解决问题:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
使用时机:想在访问一个类时做一些控制。
解决方案:增加中间层。
关键代码:实现与被代理类组合。
- 静态代理(每个类都要编写一个代理类)
- 动态代理(多个类可以复用一个代理逻辑,代理类在运行时生成)
中介者模式
模式意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
解决问题:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
使用时机:多个类相互耦合,形成了网状结构。
解决方案:将网状结构分离为星型结构。
关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。
观察者模式
模式意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
解决问题:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
使用时机:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
解决方案:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个ArrayList 存放观察者们。
访问者模式
模式意图:主要将数据结构与数据操作分离。
解决问题:稳定的数据结构和易变的操作耦合问题。
使用时机:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
解决方案:在被访问的类里面加一个对外提供接待访问者的接口。
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
第三章:计算层的软件架构技术
计算层的软件架构技术挑战
- 计算模式变革(计算复杂性、单机到单机多核众核到分布式计算、服务治理)
- 非功能度量指标(MIPS、Mbps、TFLOPS、TPS、QPS、扩展性、高可用性)
分布式编程模型
MapReduce模型
- MapReduce将复杂的、运行于大规模集群上的并行计算过程高度地抽象到了两个函数:Map和Reduce
- 编程容易,不需要掌握分布式并行编程细节,也可以很容易把自己的程序运行在分布式系统上,完成海量数据的计算
- MapReduce采用“分而治之”策略,一个存储在分布式文件系统中的大规模数据集,会被切分成许多独立的分片(split),这些分片可以被多个Map任务并行处理
- MapReduce设计的一个理念就是**“计算向数据靠拢”**,而不是“数据向计算靠拢”,因为,移动数据需要大量的网络传输开销
- MapReduce框架采用了Master/Slave架构,包括一个Master和若干个Slave。Master上运行JobTracker,Slave上运行TaskTracker
- Hadoop框架是用Java实现的,但是,MapReduce应用程序则不一定要用Java来写
模型架构:
任务执行:
==Shuffle阶段==(重要)
MapReduce on YARN:
负载均衡
常见的负载均衡包括:
- DNS负载均衡(一般用来实现地理级别的均衡)
- 简单、成本低
- 就近访问
- 更新不及时
- 扩展性差
- 分配策略比较简单
- 硬件负载均衡
- 功能强大
- 性能强大
- 稳定性高
- 支持安全防护
- 价格贵
- 扩展能力差
- 软件负载均衡
- 常见的有:LVS和Nginx
- 简单、便宜、灵活
- 性能一般
- 功能没有硬件负载均衡强大
- 一般不具备防火墙和防 DDoS 攻击等安全功能。
负载均衡算法:
- 轮询
- 加权轮询
- 负载最低优先
- 性能最优优先
- Hash类
消息队列
成功的分布式系统依赖于隐藏或简化消息传递的通信模型
分布在网络中的多进程通信(挑战:底层的网络是不可靠的)
大规模使用的通信模型:
- Remote Procedure Call(RPC)
- 隐藏了大多数复杂的信息传递
- 理想的客户端/服务器应用程序
- Message-Oriented Middleware(MOM)
- 高级消息排队模型,类似于电子邮件
- 通信并不遵循相当严格的客户机/服务器交互模式。
通信模型分类
- 基于寻址类型分类(直接、间接)
- 基于阻塞类型分类(同步、异步)
- 基于缓存类型分类(瞬态、持久)(区别在于消息是否被缓存)
- 基于内容类型分类(事件、命令、数据、流)
- 基于确认类型分类(不确认(单方向)、确认(有来有回)、三次握手确认)
- 基于接收节点数分类(点对点、多播、任播、地域性群播、广播)
- 基于通信方向分类(单向、双向半双工、双向全双工)
- 基于发起方分类(客户端拉取、服务端推送)
- 基于消息存储分类(持久化、非持久化)(区别在于消息是否被持久化)
MOM的形式
- 消息队列
- 持久异步通信
- 不需要双方同时在线
- 弥补双方速率差距
- 基于队列
- 存在缓冲区溢出风险
- 允许双方独立执行和失败
- 采用队列管理器,可作为代理(代理模式)
- 发布-订阅
分布式服务框架
分布式服务框架
主要功能
- 服务注册中心
- 负责服务的发布和通知,通常支持对等集群部署,某一个服务注册中心宕机并不会导致整个服务注册中心集群不可用。即便整个服务注册中心全部宕机,也只影响新服务的注册和发布,不影响已经发布的服务的访问。
- 服务治理中心
- 通常包含服务治理接口和服务质量Portal,架构师、测试人员和系统运维人员通过服务治理Portal对服务的运行状态、历史数据、健康度和调用关系等进行可视化的分析和维护,目标就是要持续化服务,防止服务架构腐化,保证服务高质量运行。
功能特性
- 服务订阅分布
- 配置化发布和引用服务:支持通过XML配置的方法发布和导入服务,降低对业务代码的侵入。
- 服务自动发现机制:支持服务实时自动发现,由注册中心推送服务地址,消费者不需要配置服务提供者地址,服务地址透明化。
- 服务在线注册和去注册:支持运行注册新服务,也支持运行态取消某个服务的注册。
- 服务路由、
- 路由策略:默认提供随机路由、轮循、基于权重的路由策略。避免每个框架使用者都重复开发。
- 粘滞连接:总是向同一个提供方发起请求,除非此提供方宕机,再切换到另一台。
- 路由定制:支持用户自定义路由策略,扩展平台的功能。
- 集群容错
- Failover:失败自动切换,当出现失败,重试其他服务器,通常用于读操作;也可用于幂等性写操作。
- Failback:失败自动恢复,后台记录失败请求,定时重发,通常用于消息通知操作。
- Failfast:快速失败,只发起一次调用,失败立即报错,通常用于非幂等性的写操作。
- 服务调用
- 同步调用:消费者发起服务调用之后,同步阻塞等待服务端响应。
- 异步调用:消费者发起服务调用之后,不阻塞立即返回,由服务端返回应答后异步通知消费者。
- 并行调用:消费者同时对多个服务提供者批量发起服务调用请求,批量发起请求后,集中等待应用。
- 多协议
- 私有协议:支持二进制等私有协议,支持私有协议定制和扩展。
- 公有协议:提供Web Service等公有协议,用于外部服务对接。
- 序列化方式
- 二进制类序列化:支持Thrift、Protocol buffer等二进制协议,提升序列化性能。
- 文本类序列化:支持JSON、XML等文本类型的序列化方式,提升通用性和可读性。
- 统一配置
- 本地静态配置:安装部署修改一次,运行态不修改的配置,可以存放到本地配置文件中。
- 基于配置中心的动态配置:运行态需要调整的参数,统一放到配置中心(服务注册中心),修改之后统一下发,实时生效。
分布式系统中的幂等性:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
幂等性的重要性:由于服务无状态的本质,对于业务的敏感性是很弱的。如果不支持幂等性的话,就会导致服务重复操作,对于业务数据进行违背业务逻辑的重复性操作。例如电商服务中的,超卖现象、重复转账、扣款或付款、重复增加金币、积分或优惠券。
微服务
概念
- 微服务是一种架构设计模式。在微服务架构中,业务逻辑被拆分成一系列小而松散耦合的分布式组件,共同构成了较大的应用。每个组件都被称为微服务。
- 每个微服务都在整体架构中执行着单独的任务,或负责单独的功能。
- 每个微服务可能会被一个或多个其他微服务调用,以执行较大应用需要完成的具体任务。
- 系统为任务执行——比如搜索或显示图片任务,或者其他可能需要多次执行的任务提供了统一的解决处理方式,并限制应用内不同地方生成或维护相同功能的多个版本。
- 微服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署。这些服务的集中化管理已经是最少的,它们可以用不同的编程语言编写,并使用不同的数据存储技术。
微服务架构的服务集成
- 点对点方式:直接调用服务,每个微服务都开放REST API,并且调用其它微服务的接口。
- API网关方式:其核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。
- 消息代理方式:微服务也可以集成在异步的场景下,通过队列和订阅主题,实现消息的发布和订阅。一个微服务可以是消息的发布者,把消息通过异步的方式发送到队列或者订阅主题下。作为消费者的微服务可以从队列或者主题共获取消息。通过消息中间件把服务之间的直接调用解耦。
微服务架构的服务发现
- 客户端发现模式
- 使用客户端发现模式时,客户端决定相应服务 实例的网络位置,并且对请求实现负载均衡。 客户端查询服务注册表,后者是一个可用服务 实例的数据库;然后使用负载均衡算法从中选 择一个实例,并发出请求。
- 客户端从服务注册服务中查询,其中是所有可 用服务实例的库。客户端使用负载均衡算法从 多个服务实例中选择出一个,然后发出请求。
- 服务实例的网络位置在启动时被记录到服务注 册表,等实例终止时被删除。服务实例的注册 信息通常使用心跳机制来定期刷新。
- 客户端发现模式优缺点兼有。这一模式相对直 接,除了服务注册外,其它部分无需变动。此 外,由于客户端知晓可用的服务实例,能针对 特定应用实现智能负载均衡,比如使用哈希一 致性。这种模式的一大缺点就是客户端与服务 注册绑定,要针对服务端用到的每个编程语言 和框架,实现客户端的服务发现逻辑。
- 服务端发现模式
- 客户端通过负载均衡器向某个服务提出请求, 负载均衡器查询服务注册表,并将请求转发到 可用的服务实例。如同客户端发现,服务实例 在服务注册表中注册或注销。
- AWS Elastic Load Balancer(ELB)是服务端发 现路由的例子,ELB 通常均衡来自互联网的外 部流量,也可用来负载均衡 VPC(Virtual private cloud)的内部流量。客户端使用 DNS 通过 ELB 发出请求(HTTP 或 TCP),ELB 在已注册的 EC2 实例或 ECS 容器之间负载均 衡。这里并没有单独的服务注册表,相反, EC2 实例和 ECS 容器注册在 ELB。
- 服务端发现模式兼具优缺点。它最大的优点是 客户端无需关注发现的细节,只需要简单地向 负载均衡器发送请求,这减少了编程语言框架 需要完成的发现逻辑。并且,有些部署环境免 费提供这一功能。这种模式也有缺点。除非负 载均衡器由部署环境提供,否则会成为一个需 要配置和管理的高可用系统组件。
微服务架构的服务注册(服务发现的核心部分,是包含服务实例的网络地址的数据库)
服务实例必须在注册表中注册和注销。注册和注销有两种不同的方法:
- 自注册模式
- 当使用自注册模式时,服务实例负责在服务注册表中注册和注销。另外,如果需要的话,一个服务实例也要发送心跳来保证注册信息不会过时。
- 自注册模式优缺点兼备。它相对简单,无需其它系统组件。然而,它的主要缺点是把服务实例和服务注册表耦合,必须在每个编程语言和框架内实现注册代码。
- 第三方注册模式
- 服务实例则不需要向服务注册表注册;相反,被称为服务注册器的另一个系统模块会处理。服务注册器会通过查询部署环境或订阅事件的方式来跟踪运行实例的更改。一旦侦测到有新的可用服务实例,会向注册表注册此服务。服务管理器也负责注销终止的服务实例。
- 第三方注册模式也是优缺点兼具。服务与服务注册表解耦合,无需为每个编程语言和框架实现服务注册逻辑;相反,服务实例通过一个专有服务以中心化的方式进行管理。它的不足之处在于,除非该服务内置于部署环境,否则需要配置和管理一个高可用的系统组件。
微服务架构有数据去中心化的特点
Docker
架构
镜像
镜像(Image)就是一堆只读层(read-only layer)的统一视角。
这些只读层,它们重叠在一起。 除了最下面一层,其它层都会有 一个指针指向下一层。
这些层是Docker内部的实现细节,并且能够在docker宿主机的文 件系统上访问到。统一文件系统 (Union File System)技术能够将不同的层整合成一个文件系统 ,为这些层提供了一个统一的视 角,这样就隐藏了多层的存在, 在用户的角度看来,只存在一个 文件系统。
仓库
集中存放镜像文件的场所。有时候会把仓库和仓库注册服务器(Registry)混为一谈,并不严格 区分。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。
仓库分为公开仓库(Public)和 私有仓库(Private)两种形式。最大的公开仓库是 Docker Hub ,存放了数量庞大的镜像供用户下载。国内的公开仓库包括 时 速云 、网易云 等,可以提供大 陆用户更稳定快速的访问。当然,用户也可以在本地网络内创建一个私有仓库。
当用户创建了自己的镜像之后就可以使用 push 命令将它上传到 公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来就可以了。
Docker 仓库的概念跟 Git 类似 ,注册服务器可以理解为 GitHub 这样的托管服务。
容器
Docker 利用容器(Container)来运行应用。容器是从镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。可以把容器看做是一个简易版的 Linux 环境(包括root用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。
容器的定义和镜像几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。
一个运行态容器被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。
文件系统隔离技术使得Docker成为了一个非常有潜力的虚拟化技术。一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层。
调度工具
Swarm
一个机器运行了一个Swarm的镜像(就像运行其他Docker镜像一样),它负责调度容器,在图片上鲸鱼代表这个机器。Swarm使用了和Docker标准API一致的API,这意味着在Swarm上运行一个容器和在单一主机上运行容器使用相同的命令。尽管有新的flags可用,但是开发者在使用Swarm的同时并不需要改变它的工作流程。
Swarm由多个代理(agent)组成,把这些代理称之为节点(node)。这些节点就是主机,这些主机在启动Docker daemon的时候就会打开相应的端口,以此支持Docker远程API。这些机器会根据Swarm调度器分配给它们的任务,拉取和运行不同的镜像。
当启动Docker daemon时,每一个节点都能够被贴上一些标签(label),这些标签以键值对的形式存在,通过标签就能够给予每个节点对应的细节信息。当运行一个新的容器时,这些标签就能够被用来过滤集群。
Mesos
Apache Mesos & Mesosphere Marathon的目的就是建立一个高效可扩展的系统,并且这个系统能够支持很多各种各样的框架。
Mesos的提出就是为了在底部添加一个轻量的资源共享层(resource-sharing layer),这个层使得各个框架能够适用一个统一的接口来访问集群资源。Mesos并不负责调度而是负责委派授权,毕竟很多框架都已经实现了复杂的调度。
取决于用户想要在集群上运行的作业类型,共有四种类型的框架可供使用,其中有一些支持原生的Docker。Marathon主要是由Mesosphere维护,并且提供了很多关于调度的功能,比如说约束(constraints),健康检查(healthchecks),服务发现(service discovery)和负载均衡(load balancing)。
k8s
基本组成部分
- Kubernetes成组地部署和调度容器,这个组叫Pod,常见的Pod包含一个到五个容器,它们协作来提供一个Service。
- Kubernetes默认使用扁平的网络模式。通过让在一个相同Pod中的容器共享一个 IP 并使用 localhost 上的端口,允许所有的Pod彼此通讯。
- Kubernetes 使用 Label 来搜索和更新多个对象,就好像对一个集合进行操作一样。
- Kubernetes 会搭设一个 DSN 服务器来供集群监控新的服务,然后可以通过名字来访问它们。
- Kubernetes 使用 Replication Controller 来实例化的Pod。作为一个提升容错性的机制,这些控制器对一个服务的中运行的容器进行管理合监控。
深化介绍
它使用label和pod的概念来将容器换分为逻辑单元。Pods是同地协作(co-located)容器的集合,这些容器被共同部署和调度,形成了一个服务,这是Kubernetes和其他两个框架的主要区别。相比于基于相似度的容器调度方式(就像Swarm和Mesos),这个方法简化了对集群的管理。
Kubernetes调度器的任务就是寻找那些PodSpec.NodeName为空的pods,然后通过对它们赋值来调度对应集群中的容器。相比于Swarm和Mesos,Kubernetes允许开发者通过定义PodSpec.NodeName来绕过调度器。调度器使用谓词(predicates)和优先级(priorites)来决定一个pod应该运行在哪一个节点上。通过使用一个新的调度策略配置可以覆盖掉这些参数的默认值。
谓词
谓词是强制性的规则,它能够用来调度集群上一个新的pod。如果没有任何机器满足该谓词,则该pod会处于挂起状态,知道有机器能够满足条件。可用的谓词如下所示:
- Predicate:节点的需求
- PodFitPorts:没有任何端口冲突
- PodFitsResurce:有足够的资源运行pod
- NoDiskConflict:有足够的空间来满足pod和链接的数据卷
- MatchNodeSelector:能够匹配pod中的选择器查找参数。
- HostName:能够匹配pod中的host参数。
优先级
如果调度器发现有多个机器满足谓词的条件,那么优先级就可以用来判别哪一个才是最适合运行pod的机器。优先级是一个键值对,key表示优先级的名字,value是该优先级的权重。可用的优先级如下:
- Priority:寻找最佳节点
- LeastRequestdPriority:计算pods需要的CPU和内存在当前节点可用资源的百分比,具有最小百分比的节点就是最优的。
- BalanceResourceAllocation:拥有类似内存和CPU使用的节点。
- ServicesSpreadingPriority:优先选择拥有不同pods的节点。
- EqualPriority:给所有集群的节点同样的优先级,仅仅是为了做测试。
第四章:数据层的软件架构技术
数据驱动的软件架构演化
数据与软件
数据的定义:略
数据是信息的表达、载体,信息是 数据的内涵,是形与质的关系。
数据本身没有意义,数据只有对实体行为产生影响时才成为信息。
数据的表现形式还不能完全表达其内容,需要经过解释,数据和关于数据的解释是不可分的。
数据+语义+逻辑=业务
代码+业务=软件应用系统
数据带来的架构变化
单机MySQL->Memcached+MySQL+垂直分离->MySQL主从读写分离->分库分表+水平拆分+MySQL集群->NoSQL=Not Only SQL
数据层非功能需求与架构技术
存储高性能:读写分离、数据缓存、分库分表、NoSQL
存储高可用:主从、CAP理论
存储高扩展:分库分表、NoSQL
数据读写与主从分离
读写分离
基本原理
将数据库读写操作分散到不同的节点上
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
复制延迟
主从复制延迟问题
- 如果业务服务器将数据写入到数据库主服务器后立刻进行读取 ,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。
方案
- 写操作后的读操作指定发给数据库主服务器
- 例如,注册账号完成后,登录时读取账号的读操作也发给数 据库主服务器
- 读从机失败后再读一次主机
- 关键业务读写操作全部指向主机,非关键业务采用读写分离
分配(路由)机制
- 程序代码封装(写到业务逻辑中)
- 中间件封装
主备与主从复制
主备复制基本实现逻辑
注意,在此类方案中,备机只起到备份作用。
主从复制基本实现逻辑
主备倒换与主从倒换
关键的设计点
主备间状态判断
- 状态传递渠道:P2P,或者第三方仲裁?
- 状态检测内容:如,机器掉电?进程是否存在?响应缓慢?等
倒换决策
- 倒换时机:即时,短时,长时的决策?
- 倒换策略:是否让原主机做主机还是切换? ➢ 自动程度:自动or半自动?
数据冲突
常见架构
-
互连式
-
中介式
-
模拟式
主主复制
基本设计思路
数据集群
数据集中集群
数据集中集群称为一主多备/从。数据都只能往主机写,而读操作可以参考主备,主从的架构进行灵活变化。
数据分散集群
数据分散集群指多个服务器组成一个集群,每台服务器都会负责存储一部分数据,同时,为了提升硬件利用率,每台服务器又会备份一部分数据。
需要思考的方面:
- 均衡性:保证数据分区基本均衡
- 容错性:部分服务器故障后,这些服务器上的数据分区需要分配给其他服务器
- 可伸缩性:当集群容量不够,扩充新的服务器后,算法能够自动将数据分区迁移到新服务器,并保证扩容后所有服务器的均衡性。
- 数据分散集群中,必须要有一个角色来负责执行数据分配算法(如何选?)
数据分库分表
分库分表的基本概念
分库分表的本质是数据拆分,是对数据进行分而治之的通用概念。
为了分散数据库的压力,采用分库分表将一个表结构分为多个表,或者将一个表的数据分片后放入多个表,这些表可以放在同一个库里,也可以放到不同的库里,甚至可以放在不同的数据库实例上。
数据拆分主要分为:垂直拆分和水平拆分
- 垂直拆分:根据业务的维度,将原本的一个库(表)拆分为多个库(表),每个库(表)与原有的结构不同。
- 水平拆分:根据分片(sharding)算法,将一个库(表)拆分为多个库(表),每个库(表)依旧保留原有的结构。
分库分表的发展阶段
单库单表->单库多表->多库多表
分库分表的操作时机
- 如果在数据库中表的数量达到了一定量级,则需要进行分表,分解单表的大数据量对索引查询带来的压力,并方便对索引和表结构的变更
- 如果数据库的吞吐量达到了瓶颈,就需要增加数据库实例,利用多个数据库实例来分解大量的数据库请求带来的系统压力
- 如果希望在扩容时对应用层的配置改变最少,就需要在每个数据库实例中预留足够的数据库数量
分库分表的典型实例
分库分表的解决方案
客户端分片
客户端分片就是使用分库分表的数据库的应用层直接操作分片逻辑,分片规则需要在同一个应用的多个节点间进行同步,每个应用层都嵌入一个操作切片的逻辑实现,一般通过依赖Jar包来实现。
- 在应用层直接实现
- 在ORM层实现
- 在JDBC层实现
代理分片
代理分片就是在应用层和数据库层之间增加一个代理层,把分片的路由规则配置在代理层,代理层对外提供与JDBC兼容的接口给应用层。
应用层的开发人员不用关心分片规则,只需关心业务逻辑的实现,待业务逻辑实现之后,在代理层配置路由规则即可。
支持事务的分布式数据库
现在有很多产品如OceanBase、TiDB等对外提供可伸缩的体系架构,并提供一定的分布式事务支持,将可伸缩的特点和分布式事务的实现包装到分布式数据库内部,对使用者透明,使用者不需要直接控制这些特性。
TiDB对外提供JDBC的接口,让应用层像使用MySQL等传统数据库一样,无需关注伸缩、分片、事务管理等任务。
目前不太适用于交易系统,较多用于大数据日志系统、统计系统、查询系统、社交网络等。
分库分表的架构设计
切分方法
垂直切分
垂直切分是指按照业务将表进行分类或分拆,将其分布到不同数据库上。
也可以冷热分离,根据数据的活跃度将数据进行拆分。
也可以人为将一个表中的内容划分为多个表,例如将查询较多,变化不多的字段拆分成一张表放在查询性能高的服务器,而将频繁更新的字段拆分并部署到更新性能高的服务器。
水平切分
水平切分不是将表进行分类,而是将其按照某个字段的某种规则分散到多个库中,在每个表中包含一部分数据,所有表加起来是全量数据。
简言之,将数据按一定规律,按行切分,并分配到不同的库表里,表结构完全一样。
两者共同点
- 存在分布式事务的问题
- 存在跨节点Join的问题
- 存在跨节点合并排序、分页的问题
- 存在多数据源管理的问题
- 垂直切分更偏向于业务拆分的过程
- 水平切分更偏向于技术性能指标
水平切分方式的路由过程和分片维度
路由过程
分库分表后,数据将分布到不同的分片表中,通过分库分表规则查找到对应的表和库的过程叫做路由。
我们在设计表时需要确定对表按照什么样的规则进行分库分表。例如,当生成新用户时,程序得确定将此用户的信息添加到哪个表中。
同样,在登录时我们需要通过用户的账号找到数据库中对应的记录。
分片维度
按哈希切片
- 对数据的某个字段求哈希,再除以分片总数后取模,取模后相同的数据为一个分片,这样将数据分成多个分片
- 好处:数据切片比较均匀,对数据压力分散的效果较好
- 缺点:数据分散后,对于查询需求需要进行聚合处理
按照时间切片
- 按照时间的范围将数据分布到不同的分片
分库分表引起的问题
扩容与迁移
通用的处理方法:
- 按照新旧分片规则,对新旧数据库进行双写
- 将双写前按照旧分片规则写入的历史数据,根据新分片规则迁移写入新的数据库
- 将按照旧的分片规则查询改为按照新的分片规则查询
- 将双写数据库逻辑从代码中下线,只按照新的分片规则写入数据
- 删除按照旧分片规则写入的历史数据
数据一致性问题
由于数据量大,通常会造成不一致问题,因此,通常先清理旧数据,洗完后再迁移到新规则的新数据库下,再做全量对比。还需要对比评估迁移过程中是否有数据更新,如果有需要迭代清洗,直至一致。
如果数据量巨大,无法全量对比,需要抽样对比,抽样特征需要根据业务特点进行选取。
注意:线上记录迁移过程中的更新操作日志,迁移后根据更新日志与历史数据共同决定数据的最新状态,以达到迁移数据的最终一致性。
动静数据分离问题
对于一些动静敏感的数据,如交易数据,最好将动静数据分离。选取时间点对静历史数据进行迁移。
查询问题
在分库分表以后,如果查询的标准是分片的主键,则可以通过分片规则再次路由并查询,但是对于其他主键的查询、范围查询、关联查询、查询结果排序等,并不是按照分库分表维度来查询的。
解决方案:
- 在多个分片表查询后合并数据集(效率很低)
- 按查询需求定义多分片维度,形成多张分片表(空间换时间)
- 通过搜索引擎解决,如果有实时要求,还需要实时搜索。(难度大)
分布式事务问题
同组数据跨库问题
分库分表的中间件简介
Mycat
Mycat是一个强大的数据库中间件,不仅仅可以用作读写分离、以及分表分库、容灾备份,而且可以用于多租户应用开发、云平台基础设施。
Mycat后面连接的Mycat Server,就好象是MySQL的存储引擎,如InnoDB,MyISAM 等,因此,Mycat 本身并不存储数据,数据是在后端的MySQL上存储的,因此数据可靠性以及事务等都是MySQL保证的。
基本原理:
- Mycat拦截了用户发送过来的SQL语句,首先对SQL语句做一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL发往后端的真实数据库, 并将返回的结果做适当的处理,最终再返回给用户。
- 当 Mycat收到一个SQL时,会先解析这个SQL,查找涉及到的表,然后看此表的定义,如果有分片规则, 则获取到SQL里分片字段的值,并匹配分片函数,得到该SQL对应的分片列表,然后将SQL发往这些分片去执行,最后收集和处理所有分片返回的结果数据,并输出到客户端。
Sharding JDBC
数据缓存
数据缓存的基本理论
基本概念
定义作用原理略
基本类型:
- 本地缓存
- 本地缓存指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单 应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用 本地缓存较合适;
- 实现方法:应用编码;中间件,如Ehcache、 Guava Cache等
- 分布式缓存
- 分布式缓存指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
- 常用的分布式缓存中间件:Memcached、Redis
- 反向代理缓存
- 反向代理位于应用服务器机房,处理所有对WEB服务器的请求。 如果用户请求的页面在代理服务器上有缓冲的话,代理服务器直接将缓冲内容发送给用户。如果没有缓冲则先向WEB服务器发出请求,取回数据,本地缓存后再发送给用户。通过降低向WEB服务器的请求数,从而降低了WEB服务器的负载。
- 常用的开源实现:Varnish、Nginx、Squid
- CDN缓存
- 通过在现有互联网中增加一层新的网络架构(CDNS),将网站内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容。
- CDN目标:解决由于网络带宽小、用户访问量大、网点分布不均等原因所造成 的用户访问网站响应速度慢的问题。
术语:
- 命中率=返回正确结果数/请求缓存次数
- 最大元素(或最大空间):缓存中可以存放的最大元素的数量
基本操作
-
命中与验证(HTTP的if-Modified-Since头部)
-
基本清洗策略
- FIFO(first in first out):先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
- LFU(less frequently used):最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略
- LRU(least recently used):最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
-
更新策略
- Cache aside(在需要时由应用程序主动从缓存加载数据,如果缓存未命中则从数据源获取并将其存入缓存)
- Read/Write Through Pattern(通过缓存层(自主)直接读写数据,所有的数据读写操作同时在缓存和数据源中进行,确保两者的一致性)
- Write Behind Caching Pattern(先将数据写入缓存,然后异步地将更新操作批量写入数据源,以提高写操作的性能和吞吐量)
本地缓存
实现方法
- 直接编程实现
- 成员变量或局部变量实现
- 静态变量实现
- 中间件Ehcache
分布式缓存
分布式缓存的分片与分库分表类似
分布式缓存的迁移
-
平滑迁移
-
一致性哈希
-
停机迁移
缓存问题讨论
数据一致性
数据不一致,一般是因为网络不稳定或节点故障导致问题出现的常见3个场景以及解决方案:
- 先写缓存,再写数据库:
- 【描述】缓存写成功,但写数据库失败或响应延迟 ,则下次读取(并发读)缓存时,就出现脏读;
- 【解决】这个写缓存的方式 ,本身就是错误的,需要改为先写持久化介质,再写缓存的方式
- 先写数据库,再写缓存:
- 【描述】写数据库成功,但写缓存失败,则下次读 取(并发读)缓存时,则读不到数据;
- 【解决1】根据写入缓存的响应来进行判断,如果缓存写入失败,则回滚数据库操作。该方法增加了程序的复杂度;
- 【解决2】缓存使用时,假如读缓存失败,先读数据库,再回写缓存
- 缓存异步刷新:
- 【描述】指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新;
- 【解决】根据日志中用户刷新数据的时间间隔,以及针对数据可能产生不一致的时间,进行同 步操作
缓存穿透
缓存穿透指的是使用不存在的key进行大量的高并发 查询,这导致缓存无法命中,每次请求都要穿透到后端数据库系统进行查询,使得数据库压力过大,甚至导致数据库服务崩溃。
解决方案:
- 通常将空值缓存起来,再次接收到同样的查询请求 时,若命中缓存并值为空,就会直接返回,不会透传到数据库,避免缓存穿透
- 对恶意的查询攻击,可以对查询条件设置规则,不符合条件产生规则的直接拒绝
缓存并发
缓存并发的问题通常发生在高并发的场景下,当一个缓存key过期时,因为访问这个缓存key的请求量较大,多个请求同时发现缓存过期,因此多个请求会同 时访问数据库来查询最新数据,并且回写缓存,这样会造成应用和数据库的负载增加,性能降低,由于并发较高,甚至会导致数据库崩溃。
解决方案:
- 分布式锁
- 使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。该方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
- 本地锁
- 与分布式锁类似,通过本地锁的方式来限制只有一个线程去数据库中查询数据,而其他线程只需等待,等前面的线程查询到数据后再访问缓存。但是,这种方法只能限制一个服务节点只有一个线程取数据库中查询,如果一个服务有多个节点,则会有多个数据库查询 操作,也就是说在节点数量较多的情况下并没有完全解决缓存并发 的问题。
- 软过期
- 软过期指对缓存中的数据设置失效时间,就是不使用缓存服务提供 的过期时间,而是业务层在数据中存储过期时间信息,由业务程序 判断是否过期并更新,在发现了数据即将过期时,将缓存的时效延长,程序可以派遣一个线程去数据库中获取最新的数据,其他线程 会先继续使用旧数据并等待,直至派遣线程获取最新数据后再更新 缓存。
- 也可以通过异步更新服务来更新设置软过期的缓存,这样应用层就不用关心缓存并发的问题。
缓存雪崩
缓存雪崩指缓存服务器重启或者大量缓存集中在某一 个时间段内失效,业务系统需要重新生成缓存,给后端数据库造成瞬时的负载升高的压力,甚至导致数据库崩溃。
解决方案:
- 更新锁机制
- 对缓存更新操作进行加锁保护,保证只有一个线程能进行缓存更新
- 失效时间分片机制
- 对不同的数据使用不同的失效时间,甚至对相同的数据、不同的请求使用不同的失效时间
- 例如,当对缓存的user数据中的每个用户的数据设置不同的缓存过期时 间,可以定义一个基础时间,如10秒,然后加上一个两秒以内的随机数 ,则过期时间为10~12秒,这样就可以避免雪崩。
- 后台更新机制
- 由后台线程来更新缓存,并不是业务线程来更新缓 存
- 后台线程除了定时更新缓存,还要频繁去读取缓存
- 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存(适合业务刚上线时缓存预热)
- 缓存集群
- 可以做缓存的主从与缓存水平分片
缓存高可用
缓存是否高可用,需要根据实际的场景而定,并不是 所有业务都要求缓存高可用,需要结合具体业务,具体情况进行方案设计,例如临界点是否对后端的数据库造成影响。
主要解决方案:
- 分布式:实现数据的海量缓存
- 复制:实现缓存数据节点的高可用
缓存热点
一些特别热点的数据,高并发访问同一份缓存数据, 导致缓存服务器压力过大。
解决:复制多份缓存副本,把请求分散到多个缓存服务 器上,减轻缓存热点导致的单台缓存服务器压力