凤凰架构笔记

什么是“凤凰架构”

这其中的关键点便是承认细胞等这些零部件可能会出错,某个具体的零部件可能会崩溃消亡,但在存续生命的微生态系统中一定会有其后代的出现,重新代替该零部件的作用,以维持系统的整体稳定。在这个微生态里,每一个部件都可以看作一只不死鸟(Phoenix),它会老迈,而之后又能涅槃重生。

只要在整体架构设计有恰当的、自动化的错误熔断、服务淘汰和重建的机制,在系统外部来观察,整体上仍然有可能表现出稳定和健壮的服务能力。

将一个系统看作是一个像人一样的生物。每一刻内部都有细胞老去凋零,也有新细胞的诞生 ,将细胞(部件)的“死去“看作是正常生理过程,并为之设计恰当的容错机制,使得内部的错误得到处理,在内部这种不断迭代情况下,从整体上看依旧具有健壮性。

演进中的架构

原始分布式时代

探索过程中的产物

失败的原因

  • 机器硬件条件下性能上的差异
  • 摩尔定律稳定发挥作用
  • 分布式架构尚未成熟

“如同本地调用一般简单透明的”分布式系统这个目标,是软件开发者对分布式系统最初的美好愿景

原始的分布式时代比我猜想的要早太多太多,而且怀揣着美好的愿景对分布式进行了比较全面的尝试探索。

单体(巨石)系统时代

非缺点

  • 单体不仅易于开发、易于测试、易于部署,且由于系统中各个功能、模块、方法的调用过程都是进程内调用,不会发生进程间通信(Inter-Process Communication,IPC。
  • 在纵向分层上完全不会展露出丝毫的弱势。
  • 在以横向扩展(Scale Horizontally)的上可以,在负载均衡器之后同时部署若干个相同的单体系统副本(多个 JAR、WAR、DLL、Assembly 或者其他模块格式来构成),以达到分摊流量压力的效果。

缺点

  • 隔离(难以阻断错误传播)与自治能力的欠缺
  • 可维护性的欠缺(修改缺陷往往需要制定专门的停机更新计划,做灰度发布、A/B 测试也相对更复杂。)
  • 无法达到技术异构
  • 单体系统很难兼容“Phoenix”的特性。

单体系统很难兼容“Phoenix”的特性。这种架构风格潜在的观念是希望系统的每一个部件,每一处代码都尽量可靠,靠不出或少出缺陷来构建可靠系统。然而战术层面再优秀,也很难弥补战略层面的不足,单体靠高质量来保证高可靠性的思路,在小规模软件上还能运作良好,但系统规模越大,交付一个可靠的单体系统就变得越来越具有挑战性。如本书的前言开篇《什么是"凤凰架构"》所说,正是随着软件架构演进,构筑可靠系统从“追求尽量不出错”,到正视“出错是必然”的观念转变,才是微服务架构得以挑战并逐步开始取代运作了数十年的单体架构的底气所在。

单体架构并不是完全是缺点,只有在项目足够复杂成为一个“大型的单体系统”,项目开发人员较多时,对项目可靠性,可维护性具有一定要求时,使用分布式系统才具体有意义。

SOA时代

SOA 架构(Service-Oriented Architecture)面向服务的架构是一次具体地、系统性地成功解决分布式服务主要问题的架构模式。

  • 烟囱式架构(Information Silo Architecture):信息烟囱又名信息孤岛(Information Island),使用这种架构的系统也被称为孤岛式信息系统或者烟囱式信息系统。它指的是一种完全不与其他相关信息系统进行互操作或者协调工作的设计模式。

  • 微内核架构(Microkernel Architecture):微内核架构也被称为插件式架构(Plug-in Architecture)。将这些主数据,连同其他可能被各子系统使用到的公共服务、数据、资源集中到一块,成为一个被所有业务系统共同依赖的核心(Kernel,也称为 Core System),具体的业务系统以插件模块(Plug-in Modules)的形式存在,这样也可提供可扩展的、灵活的、天然隔离的功能特性,即微内核架构,如图 1-2 所示。

  • 事件驱动架构(Event-Driven Architecture):为了能让子系统互相通信,一种可行的方案是在子系统之间建立一套事件队列管道(Event Queues)每一个消息的处理者都是独立的,高度解耦的,但又能与其他处理者(如果存在该消息处理者的话)通过事件管道进行互动

SOA

“更具体”体现在尽管 SOA 本身还是属抽象概念,可以称为一套软件设计的基础平台了。有清晰软件设计的指导原则,譬如服务的封装性、自治、松耦合、可重用、可组合、无状态,等等;若仅从技术可行性这一个角度来评判的话,SOA 可以算是成功地解决了分布式环境下出现的主要技术问题。

  • 明确了采用 SOAP 作为远程调用的协议,依靠 SOAP 协议族(WSDL、UDDI 和一大票 WS-*协议)来完成服务的发布、发现和治理;
  • 利用一个被称为企业服务总线(Enterprise Service Bus,ESB)的消息管道来实现各个子系统之间的通信交互
  • 使用服务数据对象(Service Data Object,SDO)来访问和表示数据
  • 使用服务组件架构(Service Component Architecture,SCA)来定义服务封装的形式和服务运行的容器

“更系统”指的是 SOA 的宏大理想,SOA 不仅关注技术,还关注研发过程中涉及到的需求、管理、流程和组织。写出符合客户需求的软件会像写八股文一样有迹可循、有法可依.

SOAP 协议被逐渐边缘化的本质原因:过于严格的规范定义带来过度的复杂性

软件架构从烟囱式架构到事件驱动,再到后来的SOA中ESB~ 有一个很明显的痛点就是在一步一步地完善架构之间组件的信息的传递交互,这点也是跟单体比较区别大的点。另外到SOA时代也进行了更加全面的探索。不过很多组件之前…几乎没有听闻,从功能性来看跟工作中接触的很多微服务组件几乎相同的功能,似乎技术的迭代更应该倾向于减少单个组件的复杂性?

微服务时代

微服务架构(Microservices)

微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言,不同的数据存储技术,运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。

微服务的九个核心的业务与技术特征

  • 围绕业务能力构建 有怎样结构、规模、能力的团队,就会产生出对应结构、规模、能力的产品。
  • **分散治理 **微服务更加强调的是确实有必要技术异构时,应能够有选择“不统一”的权利
  • 通过服务来实现独立自治的组件 为组件带来隔离与自治能力
  • 产品化思维 避免把软件研发视作要去完成某种功能,而是视作一种持续改进、提升的过程。在微服务下,要求开发团队中每个人都具有产品化思维,关心整个产品的全部方面是具有可行性的。
  • 数据去中心化 微服务明确地提倡数据应该按领域分散管理、更新、维护、存储 同一个数据实体在不同服务的视角里,它的抽象形态往往也是不同的。
  • 强终端弱管道 如果服务需要额外通信能力,就应该在服务自己的 Endpoint 上解决,而不是在通信管道上一揽子处理。微服务提倡类似于经典 UNIX 过滤器那样简单直接的通信方式,RESTful 风格的通信在微服务中会是更加合适的选择。
  • 容错性设计 不再虚幻地追求服务永远稳定,而是接受服务总会出错的现实.,要求在微服务的设计中,有自动的机制对其依赖的服务能够进行快速故障检测,在持续出错的时候进行隔离,在服务恢复的时候重新联通。可靠系统完全可能由会出错的服务组成,这是微服务最大的价值所在.
  • 演进式设计(Evolutionary Design)。容错性设计承认服务会出错,演进式设计则是承认服务会被报废淘汰
  • 基础设施自动化(Infrastructure Automation)。微服务下运维的对象比起单体架构要有数量级的增长,使用微服务的团队更加依赖于基础设施的自动化

微服务追求的是更加自由的架构风格,摒弃了几乎所有 SOA 里可以抛弃的约束和规定,提倡以“实践标准”代替“规范标准”。在微服务中不再会有统一的解决方案,需要解决什么问题,就引入什么工具;团队熟悉什么技术,就使用什么框架。

技术架构者的第一职责就是做决策权衡,有利有弊才需要决策,有取有舍才需要权衡,如果架构者本身的知识面不足以覆盖所需要决策的内容,不清楚其中利弊,恐怕也就无可避免地陷入选择困难症的困境之中。

微服务时代充满着自由的气息,微服务时代充斥着迷茫的选择。

从SOA到微服务~从一个复杂的技术标准到开发自由的多种多样解决方案的微服务生态圈,减少了开发过程的繁琐却对于技术的选型权衡利弊有了更高的要求。

后微服务时代

Kubernetes 登基加冕是容器发展中一个时代的终章,也将是软件架构发展下一个纪元的开端。

传统 Spring Cloud 与 Kubernetes 提供的解决方案对比

Kubernetes Spring Cloud
弹性伸缩 Autoscaling N/A
服务发现 KubeDNS / CoreDNS Spring Cloud Eureka
配置中心 ConfigMap / Secret Spring Cloud Config
服务网关 Ingress Controller Spring Cloud Zuul
负载均衡 Load Balancer Spring Cloud Ribbon
服务安全 RBAC API Spring Cloud Security
跟踪监控 Metrics API / Dashboard Spring Cloud Turbine
降级熔断 N/A Spring Cloud Hystrix

从软件层面独力应对分布式架构所带来的各种问题,发展到应用代码与基础设施软、硬一体,合力应对架构问题的时代,现在常被媒体冠以“云原生”这个颇为抽象的名字加以宣传。云原生时代与此前微服务时代中追求的目标并没有本质改变,在服务架构演进的历史进程中,笔者更愿意称其为“后微服务时代”。

Kubernetes 成为容器战争胜利者标志着后微服务时代的开端,但 Kubernetes 仍然没有能够完美解决全部的分布式问题——“不完美”的意思是,仅从功能上看,单纯的 Kubernetes 反而不如之前的 Spring Cloud 方案。这是因为有一些问题处于应用系统与基础设施的边缘,使得完全在基础设施层面中确实很难精细化地处理。

基础设施是针对整个容器来管理的,粒度相对粗旷,只能到容器层面,对单个远程服务就很难有效管控。

虚拟化的基础设施很快完成了第二次进化,引入了今天被称为“服务网格”(Service Mesh)的“边车代理模式”(Sidecar Proxy)在应用毫无感知的情况下,悄然接管应用所有对外通信。这个代理除了实现正常的服务间通信外(称为数据平面通信),还接收来自控制器的指令(称为控制平面通信),根据控制平面中的配置,对数据平面通信的内容进行分析处理,以实现熔断、认证、度量、监控、负载均衡等各种附加功能。这样便实现了既不需要在应用层面加入额外的处理代码,也提供了几乎不亚于程序代码的精细管理能力。

服务网格将会成为微服务之间通信交互的主流模式,把“选择什么通信协议”、“怎样调度流量”、“如何认证授权”之类的技术问题隔离于程序代码之外,取代今天 Spring Cloud 全家桶中大部分组件的功能,微服务只需要考虑业务本身的逻辑,这才是最理想的Smart Endpoints解决方案。

业务与技术完全分离,远程与本地完全透明,也许这就是最好的时代了吧?

从微服务到后微服务时代,以虚拟化容器设备让软硬界限模糊,提供的新的服务之间管理方式,业务于技术完全分离,之间的跨越不亚于从先前看的单体到微服务的进程层次的差距。又是一种质的改变~~ 。

无服务时代

如果说微服务架构是分布式系统这条路的极致,那无服务架构,也许就是“不分布式”的云端系统这条路的起点。

无服务现在还没有一个特别权威的“官方”定义,但它的概念并没有前面各种架构那么复杂,本来无服务也是以“简单”为主要卖点的,它只涉及两块内容:后端设施(Backend)和函数(Function)。

  • 后端设施是指数据库、消息队列、日志、存储,等等这一类用于支撑业务逻辑运行,但本身无业务含义的技术组件,这些后端设施都运行在云中,无服务中称其为“后端即服务”(Backend as a Service,BaaS)。
  • 函数是指业务逻辑代码,这里函数的概念与粒度,都已经很接近于程序编码角度的函数了,其区别是无服务中的函数运行在云端,不必考虑算力问题,不必考虑容量规划(从技术角度可以不考虑,从计费的角度你的钱包够不够用还是要掂量一下的),无服务中称其为“函数即服务”(Function as a Service,FaaS)

无服务的愿景是让开发者只需要纯粹地关注业务,不需要考虑技术组件只涉及两块内容:后端设施(Backend)和函数(Function)。

  • 后端设施是指数据库、消息队列、日志、存储,等等这一类用于支撑业务逻辑运行,但本身无业务含义的技术组件,这些后端设施都运行在云中,
  • 函数是指业务逻辑代码,这里函数的概念与粒度,都已经很接近于程序编码角度的函数了,其区别是无服务中的函数运行在云端.

优点

  • 不需要考虑技术组件,技术组件是现成的
  • 不需要考虑如何部署,部署过程完全是托管到云端的,
  • 不需要考虑算力,算力可以认为是无限的;
  • 不需要操心运维,维护系统持续平稳运行是云计算服务商的责任

缺点

函数不便依赖服务端状态,也导致了函数会有冷启动时间,响应的性能不可能太好。函数不便依赖服务端状态,也导致了函数会有冷启动时间,响应的性能不可能太好

多种架构风格将会融合互补,“分布式”与“不分布式”的边界将逐渐模糊,两条路线在云端的数据中心中交汇。

架构师的视角

RPC(访问远程服务)

RPC 出现的最初目的,就是为了让计算机能够跟调用本地方法一样去调用远程方法.

两个进程之间如何交换数据?

  • 管道(Pipe)或者具名管道(Named Pipe)管道类似于两个进程间的桥梁,可通过管道在进程间传递少量的字符流或字节流。

    1
    ps -ef | grep java
  • 信号(Signal):信号用于通知目标进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程自身。

1
kill -9 pid
  • 信号量(Semaphore):信号量用于两个进程之间同步协作手段,它相当于操作系统提供的一个特殊变量,程序可以在上面进行wait()notify()操作。

  • 消息队列(Message Queue)

  • 共享内存

  • 套接字接口(Socket)

    通信的成本

这种基于套接字接口的通信方式透明的调用形式却反而造成了程序员误以为通信是无成本的假象,因而被滥用以致于显著降低了分布式系统的性能。本地调用与远程调用当做一样处理,这是犯了方向性的错误,把系统间的调用做成透明,反而会增加程序员工作的复杂度。

网络编程中经常被忽略的八大问题

  • 网络是可靠的。
  • 延迟是不存在的。
  • 带宽是无限的。
  • 网络是安全的。
  • 拓扑结构是一成不变的。
  • 总会有一个管理员。
  • 不必考虑传输成本。
  • 网络是同质化的。

RPC 应该是一种高层次的或者说语言层次的特征,而不是像 IPC 那样,是低层次的或者说系统层次的特征成为工业界、学术界的主流观点。

远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的方式使用带宽有限的信道来传输程序控制信息。

RPC 协议解决三个基本问题

  • 如何表示数据:将交互双方所涉及的数据转换为某种事先约定好的中立数据流格式来进行传输,将数据流转换回不同语言中对应的数据类型来进行使用,(序列化与反序列化)
  • 如何传递数据:如何通过网络,在两个服务的 Endpoint 之间相互操作、交换数据(应用层协议)。两个服务交互不是只扔个序列化数据流来表示参数和结果就行的,许多在此之外信息,譬如异常、超时、安全、认证、授权、事务,等等,都可能产生双方需要交换信息的需求。
  • 如何确定方法:一套语言无关的接口描述语言 (没想到UUID是这样出现的)

统一的的RPC

但无奈 CORBA 本身设计得实在是太过于啰嗦繁琐,甚至有些规定简直到了荒谬的程度——写一个对象请求代理(ORB,这是 CORBA 中的核心概念)大概要 200 行代码,其中大概有 170 行都是纯粹无用的废话——这句带有鞭尸性质的得罪人的评价不是笔者写的,是 CORBA 的首席科学家 Michi Henning 在文章《The Rise and Fall of CORBA》的愤怒批评。另一方面,为 CORBA 制定规范的专家逐渐脱离实际,做出 CORBA 规范晦涩难懂,各家语言的厂商都有自己的解读,结果各门语言最终出来的 CORBA 实现互不兼容,实在是对 CORBA 号称支持众多异构语言的莫大讽刺。

这段看着笑到我了~

CORBA :支持多种编程语言,由多家软件提供商共同参与的分布式规范;本身设计得实在是太过于啰嗦繁琐

Web Service:数据交互都包含大量的冗余信息,性能奇差。过于严谨。贪婪。

那些面向透明的、简单的 RPC 协议要么依赖于操作系统,要么依赖于特定语言,总有一些先天约束;那些面向通用的、普适的 RPC 协议;如 CORBA,就无法逃过使用复杂性的困扰,而那些意图通过技术手段来屏蔽复杂性的 RPC 协议,如 Web Service,又不免受到性能问题的束缚。简单、普适、高性能这三点,似乎真的难以同时满足。

分裂的 RPC

由于一直没有一个同时满足以上三点的“完美 RPC 协议”出现,所以远程服务器调用这个小小的领域里,逐渐进入了群雄混战、百家争鸣的战国时代

发展方向

  • 朝着面向对象发展
  • 朝着性能发展 (序列化效率和信息密度) 信息密度则取决于协议中有效荷载(Payload)所占总传输数据的比例大小,使用传输协议的层次越高,信息密度就越低
  • 朝着简化发展

到了最近几年,RPC 框架有明显的朝着更高层次(不仅仅负责调用远程服务,还管理远程服务)与插件化方向发展的趋势,不再追求独立地解决 RPC 的全部三个问题(表示数据、传递数据、表示方法),而是将一部分功能设计成扩展点,让用户自己去选择。框架聚焦于提供核心的、更高层次的能力,譬如提供负载均衡、服务注册、可观察性等方面的支持。

REST 设计风格

REST 与 RPC 在思想上差异的核心是抽象的目标不一样,即面向资源的编程思想与面向过程的编程思想两者之间的区别。

“REST”(Representational State Transfer)实际上是“HTT”(Hypertext Transfer)的进一步抽象,两者就如同接口与实现类的关系一般.

REST 中关键概念

  • 资源 譬如你现在正在阅读一篇名为《REST 设计风格》的文章,这篇文章的内容本身(你可以将其理解为其蕴含的信息、数据)我们称之为“资源”。无论你是购买的书籍、是在浏览器看的网页、是打印出来看的文稿、是在电脑屏幕上阅读抑或是手机上浏览,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。
  • 表征 当你通过电脑浏览器阅读此文章时,浏览器向服务端发出请求“我需要这个资源的 HTML 格式”,服务端向浏览器返回的这个 HTML 就被称之为“表征”,你可能通过其他方式拿到本文的 PDF、Markdown、RSS 等其他形式的版本,它们也同样是一个资源的多种表征。
  • 状态(State):当你读完了这篇文章,想看后面是什么内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息即被称为“状态”。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务器要完成“取下一篇”的请求,要么自己记住用户的状态:这个用户现在阅读的是哪一篇文章,这称为有状态;要么客户端来记住状态,在请求的时候明确告诉服务器:我正在阅读某某文章,现在要读它的下一篇,这称为无状态。
  • 转移(Transfer):无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。
  • 统一接口(Uniform Interface):上面说的服务器“通过某种方式”让表征状态发生转移,具体是什么方式?如果你真的是用浏览器阅读本文电子版的话,请把本文滚动到结尾处,右下角有下一篇文章的 URI 超链接地址,这是服务端渲染这篇文章时就预置好的,点击它让页面跳转到下一篇,就是所谓“某种方式”的其中一种方式。任何人都不会对点击超链接网页会出现跳转感到奇怪,但你细想一下,URI 的含义是统一资源标识符,是一个名词,如何能表达出“转移”动作的含义呢?答案是 HTTP 协议中已经提前约定好了一套“统一接口”,它包括:GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS 七种基本操作,任何一个支持 HTTP 协议的服务器都会遵守这套规定,对特定的 URI 采取这些操作,服务器就会触发相应的表征状态转移。
  • 超文本驱动(Hypertext Driven):尽管表征状态转移是由浏览器主动向服务器发出请求所引发的,该请求导致了“在浏览器屏幕上显示出了下一篇文章的内容”这个结果的出现。但是,你我都清楚这不可能真的是浏览器的主动意图,浏览器是根据用户输入的 URI 地址来找到网站首页,服务器给予的首页超文本内容后,浏览器再通过超文本内部的链接来导航到了这篇文章,阅读结束时,也是通过超文本内部的链接来再导航到下一篇。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。
  • 自描述消息(Self-Descriptive Messages):由于资源的表征可能存在多种不同形态,在消息中应当有明确的信息来告知客户端该消息的类型以及应如何处理这条消息。一种被广泛采用的自描述方法是在名为“Content-Type”的 HTTP Header 中标识出互联网媒体类型(MIME type),譬如“Content-Type : application/json; charset=utf-8”,则说明该资源会以 JSON 的格式来返回,请使用 UTF-8 字符集进行处理。

RESTful 的系统

一套理想的、完全满足 REST 风格的系统应该满足以下六大原则

  • 服务端与客户端分离
  • 无状态 (在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种是事实上存在,并将长期存在、被广泛使用的主流的方案。)
  • 可缓存REST 希望软件系统能够如同万维网一样,允许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。运作良好的缓存机制可以减少客户端、服务器之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。
  • 分层系统(Layered System)客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型的应用是内容分发网络(Content Distribution Network,CDN)。
  • 统一接口 : 这是 REST 的另一条核心原则,REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。
  • 按需代码指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术,按需代码赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。

REST 好处

  • 降低的服务接口的学习成本。
  • 资源天然具有集合与层次结构。以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。
  • REST 绑定于 HTTP 协议。

RMM 成熟度

  1. The Swamp of Plain Old XML:完全不 REST。
  2. Resources:开始引入资源的概念。
  3. HTTP Verbs:引入统一接口,映射到 HTTP 协议的方法上。
  4. Hypermedia Controls:超媒体控制在本文里面的说法是“超文本驱动”,在 Fielding 论文里的说法是“Hypertext As The Engine Of Application State,HATEOAS”,其实都是指同一件事情。

不足与争议

面向资源的编程思想只适合做 CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑

REST 与 HTTP 完全绑定,不适合应用于要求高性能传输的场景中

REST 不利于事务支持

REST 没有传输可靠性支持

REST 缺乏对资源进行“部分”和“批量”的处理能力

事务处理

ACID

  • 原子性Atomic):在同一项业务处理过程中,事务保证了对多个数据的修改,要么同时成功,要么同时被撤销。
  • 隔离性Isolation):在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。
  • 持久性Durability):事务应当保证所有成功被提交的数据修改都能够正确地被持久化,不丢失数据。

目的

​ 保证系统中所有的数据都是符合期望的,且相互关联的数据之间不会产生矛盾,即数据状态的一致性Consistency)。

本地事务

仅仅适用于单个服务使用单个数据源

实现原子性和持久性的最大困难是“写入磁盘”这个操作并不是原子的,不仅有“写入”与“未写入”状态,还客观地存在着“正在写”的中间状态。正因为写入中间状态与崩溃都不可能消除,所以如果不做额外保障措施的话,将内存中的数据写入磁盘,并不能保证原子性与持久性。

原子性持久性

实现原子性于持久性的两个方案

  • 提交日志 (Commit Logging ) 将修改数据这个操作所需的全部信息(修改后数据,数据物理块位置,什么改成什么等)日志落盘后才会开始修改数据
  • 影子分页 (Commit Logging )将拷贝的数据复制一份副本,然后对副本进行操作最后将数据的引用指针指向(原子性)修改后的数据

按照事务提交时点为界划分

  • FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE
  • STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL

Write-Ahead Logging (NO-FORCE STEAL)

添加回滚日志UndoLog ,记录已经修改的数据,可以一边写写事务日志一般修改数据解决日志写入后才能一次性修改数据 占用大量内存

崩溃恢复

  • 分析阶段(Analysis):找出待恢复的事务集合 (已经commit完的与未commit的)
  • 重做阶段(Redo):根据待恢复的事务(已经commit完的)集合来重演历史(幂等)
  • 回滚阶段(Undo):根据 Undo Log 中的信息,回滚未commit完的数据(幂等)

image-20211213231725473

隔离性

实现方案 加锁

  • 写锁(Write Lock)
  • 读锁(Read Lock)
  • 范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。如下语句是典型的加范围锁的例子:

并发控制理论(Concurrency Control)决定了隔离程度与并发能力是相互抵触的,隔离程度越高,并发访问时的吞吐量就越低

隔离级别

  • 可串行化: 强度最高的隔离性

  • 可重复读: 读写锁,无范围锁 会产生幻读问题 在事务执行过程中,两个完全相同的范围查询得到了不同的结果集。

    1
    2
    3
    SELECT count(1) FROM books WHERE price < 100					/* 时间顺序:1,事务: T1 */
    INSERT INTO books(name,price) VALUES ('深入理解Java虚拟机',90) /* 时间顺序:2,事务: T2 */
    SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:3,事务: T1 */
  • 读已提交:读锁在查询操作完成后就马上会释放。 不可重复读问题 在事务执行过程中,对同一行数据的两次查询得到了不同的结果。

    1
    2
    3
    SELECT * FROM books WHERE id = 1;   						/* 时间顺序:1,事务: T1 */
    UPDATE books SET price = 110 WHERE id = 1; COMMIT; /* 时间顺序:2,事务: T2 */
    SELECT * FROM books WHERE id = 1; COMMIT;
  • 读未提交 完全不加读锁。 脏读问题 在事务执行过程中,一个事务读取到了另一个事务未提交的数据。

    1
    2
    3
    4
    5
    6
    SELECT * FROM books WHERE id = 1;   						/* 时间顺序:1,事务: T1 */
    /* 注意没有COMMIT */
    UPDATE books SET price = 90 WHERE id = 1; /* 时间顺序:2,事务: T2 */
    /* 这条SELECT模拟购书的操作的逻辑 */
    SELECT * FROM books WHERE id = 1; /* 时间顺序:3,事务: T1 */
    ROLLBACK; /* 时间顺序:4,事务: T2 */
  • 完全不隔离 脏写问题 即一个事务的没提交之前的修改可以被另外一个事务的修改覆盖掉,(没有原子性 所以不讨论)

无锁优化方案

多版本并发控制”(Multi-Version Concurrency Control,MVCC)

MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。(CREATE_VERSION 和 DELETE_VERSION,这两个字段记录的值都是事务 ID)

  • 插入数据时:CREATE_VERSION 记录插入数据的事务 ID,DELETE_VERSION 为空。
  • 删除数据时:DELETE_VERSION 记录删除数据的事务 ID,CREATE_VERSION 为空。
  • 修改数据时:将修改数据视为“删除旧数据,插入新数据”的组合,即先将原有数据复制一份,原有数据的 DELETE_VERSION 记录修改数据的事务 ID,CREATE_VERSION 为空。复制出来的新数据的 CREATE_VERSION 记录修改数据的事务 ID,DELETE_VERSION 为空。

将根据隔离级别来决定到底应该读取哪个版本的数据。

  • 隔离级别是可重复读:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。
  • 隔离级别是读已提交:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。

MVCC 是只针对“读+写”场景的优化,如果是两个事务同时修改数据,即“写+写”的情况

全局事务

一种适用于单个服务使用多个数据源场景

两段式提交”(2 Phase Commit,2PC)协议

  • 准备阶段:又叫作投票阶段,在这一阶段,协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复 Prepared,否则回复 Non-Prepared。准备操作是在重做日志中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record 而已,这意味着在做完数据持久化后并不立即释放隔离性,即仍继续持有锁,维持数据对其他非事务内观察者的隔离状态。
  • 提交阶段:又叫作执行阶段,协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条 Commit Record 而已,通常能够快速完成,只有收到 Abort 指令时,才需要根据回滚日志清理已提交的数据,这可能是相对重负载的操作。

上面所说的协调者、参与者都是可以由数据库自己来扮演的,

image-20211214231409011

XA的前提:

  • 必须假设网络在提交阶段的短时间内是可靠的,即提交阶段不会丢失消息。同时也假设网络通信在全过程都不会出现误差,即可以丢失消息,但不会传递错误的消息.XA 的设计目标并不是解决诸如拜占庭将军一类的问题。
  • 必须假设因为网络分区、机器崩溃或者其他原因而导致失联的节点最终能够恢复,不会永久性地处于失联状态。

拜占庭将军:In a Byzantine fault, a component such as a server can inconsistently appear both failed and functioning to failure-detection systems, presenting different symptoms to different observers. It is difficult for the other components to declare it failed and shut it out of the network, because they need to first reach a consensus regarding which component has failed in the first place.

缺点

  • 单点问题 协调者宕机,没有正常发送 Commit 或者 Rollback 的指令,那所有参与者都必须一直等待。
  • 性能问题:两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久化,提交阶段在日志写入 Commit Record),性能由参与者集群中最慢决定,故而性能比较差。
  • 一致性风险:前面已经提到,两段式提交的成立是有前提条件的,当网络稳定性和宕机恢复能力的假设不成立时,仍可能出现一致性问题。

FLP 不可能原理”,证明了如果宕机最后不能恢复,那就不存在任何一种分布式协议可以正确地达成一致性结果。

三段式提交”(3 Phase Commit,3PC)协议

三段式提交把原本的两段式提交的准备阶段再细分为两个阶段,分别称为

  • CanCommit(协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。)、
  • PreCommit,原准备阶段
  • DoCommit ,提交阶段 ,如果在 PreCommit 阶段之后发生了协调者宕机,即参与者没有能等到 DoCommit 的消息的话,默认的操作策略将是提交事务而不是回滚事务或者持续等待,这就相当于避免了协调者单点问题的风险。

image-20211215220806915

三段式提交对单点问题和回滚时的性能问题有所改善,但是它对一致性风险问题风险甚至反而是略有增加了的。回滚时超时反而提交了。

共享事务

多个服务共用同一个数据源。

实现共享事务,就必须新增一个“交易服务器”的中间角色,无论是用户服务、商家服务还是仓库服务,它们都通过同一台交易服务器来与数据库打交道。

image-20211215221913321

该方案是与实际生产系统中的压力方向相悖的,一个服务集群里数据库才是压力最大而又最不容易伸缩拓展的重灾区,所以现实中只有类似ProxySQLMaxScale这样用于对多个数据库实例做负载均衡的数据库代理(其实用 ProxySQL 代理单个数据库,再启用 Connection Multiplexing,已经接近于前面所提及的交易服务器方案了)

使用消息队列服务器来代替交易服务器。用户、商家、仓库的服务操作业务时,通过消息将所有对数据库的改动传送到消息队列服务器,通过消息的消费者来统一处理,实现由本地事务保障的持久化操作。这被称作“单个数据库的消息驱动更新

分布式事务

多个服务同时访问多个数据源的事务处理机制

CAP

彻底地击碎了XA 的事务机制可以在本节所说的分布式环境中也能良好地应用的美好的愿望

三个特性最多只能同时满足其中两个,由于一般不放弃分区容错性,故而只有AP(主流)跟CP

  • 一致性Consistency):代表数据在任何时刻、任何分布式节点中所看到的都是符合预期的。一致性在分布式研究中是有严肃定义、有多种细分类型的概念,以后讨论分布式共识算法时,我们还会再提到一致性,那种面向副本复制的一致性与这里面向数据库状态的一致性严格来说并不完全等同,具体差别我们将在后续分布式共识算法中再作探讨。
  • 可用性Availability):代表系统不间断地提供服务的能力,理解可用性要先理解与其密切相关两个指标:可靠性(Reliability)和可维护性(Serviceability)。可靠性使用平均无故障时间(Mean Time Between Failure,MTBF)来度量;可维护性使用平均可修复时间(Mean Time To Repair,MTTR)来度量。可用性衡量系统可以正常使用的时间与总时间之比,其表征为:A=MTBF/(MTBF+MTTR),即可用性是由可靠性和可维护性计算得出的比例值,譬如 99.9999%可用,即代表平均年故障修复时间为 32 秒。
  • 分区容忍性Partition Tolerance):代表分布式环境中部分节点因网络原因而彼此失联后,即与其他节点形成“网络分区”时,系统仍能正确地提供服务的能力。

本章讨论的话题“事务”原本的目的就是获得“一致性”,而在分布式环境中,“一致性”却不得不成为通常被牺牲、被放弃的那一项属性。

最终一致性”(Eventual Consistency),它是指:如果数据在一段时间之内没有被另外的操作所更改,那它最终将会达到与强一致性过程相同的结果,有时候面向最终一致性的算法也被称为“乐观复制算法”。

BASE

  • 基本可用性(Basically Available)
  • -柔性事务(Soft State)
  • 最终一致性(Eventually Consistent)

柔性事务实现方式

可靠事件队列

靠着持续重试来保证可靠性的解决方案。

image-20211215232245727

顺序

  • 事务排序:顺序就应该安排成最容易出错的最先进行。顺序就应该安排成最容易出错的最先进行
  • 执行第一个事务:账号服务进行扣款业务,如扣款成功,则在自己的数据库建立一张消息表,里面存入一条消息。
  • 信息服务轮询 :根据消息表 轮询 重发 执行未成功事务 直到全部成功。在系统中建立一个消息服务,定时轮询消息表,将状态是“进行中”的消息同时发送到库存和商家服务节点中去。

信息服务轮询的可重复性决定了所有被消息服务器发送的消息都必须具备幂等性,通常的设计是让消息带上一个唯一的事务 ID。

可靠事件队列只要第一步业务完成了,后续就没有失败回滚的概念,只许成功,不许失败。

消息服务可以由支持分布式事务的消息框架替代。

缺点

整个过程完全没有任何隔离性可言

TCC 事务

Try-Confirm-Cancel”

步骤

  • Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需用到的业务资源(保障隔离性)。
  • Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。Confirm 阶段可能会重复执行,因此本阶段所执行的操作需要具备幂等性。
  • Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。Cancel 阶段可能会重复执行,也需要满足幂等性。

image-20211215232220122

实际步骤

  1. 生成事务ID,记录到日志中
  2. Try 遍历每一个服务 检查业务可行性,可行的话冻结资源 进入Confirm 否则 进入Cancel
  3. Confirm 遍历每一个服务 完成业务操作 失败时重复Confirm 操作即进行最大努力交付。
  4. Cancel 取消所有业务操作 释放冻结支援

好处

  • 于用户代码层面,较高的灵活性,可以根据需要设计资源锁定的粒度。
  • 业务执行时只操作预留资源,几乎不会涉及锁和资源的争用,具有很高的性能潜力。

坏处

  • 更高的开发成本和更换事务实现方案的替换成本
  • 业务侵入性

基于某些分布式事务中间件(譬如阿里开源的Seata)去完成

SAGA 事务

将一个分布式环境中的大事务分解为一系列本地事务的设计模式。(通过数据补偿进行)

  • 大事务拆分若干个小事务,将整个分布式事务 T 分解为 n 个子事务,命名为 T1,T2,…,Ti,…,Tn。每个子事务都应该是或者能被视为是原子行为。如果分布式事务能够正常提交,其对数据的影响(最终一致性)应与连续按顺序成功提交 Ti等价。
  • 为每一个子事务设计对应的补偿动作,命名为 C1,C2,…,Ci,…,Cn。Ti与 Ci必须满足以下条件:
    • Ti与 Ci都具备幂等性。
    • Ti与 Ci满足交换律(Commutative),即先执行 Ti还是先执行 Ci,其效果都是一样的。
    • Ci必须能成功提交,即不考虑 Ci本身提交失败被回滚的情形,如出现就必须持续重试直至成功,或者要人工介入。

如果 T1到 Tn均成功提交,那事务顺利完成,否则,要采取以下两种恢复策略之一:

  • 正向恢复(Forward Recovery):如果 Ti事务提交失败,则一直对 Ti进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,譬如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
  • 反向恢复(Backward Recovery):如果 Ti事务提交失败,则一直执行 Ci对 Ti进行补偿,直至成功为止(最大努力交付)。这里要求 Ci必须(在持续重试后)执行成功。反向恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。

SAGA 必须保证所有子事务都得以提交或者补偿,但 SAGA 系统本身也有可能会崩溃,所以它必须设计成与数据库类似的日志机制(被称为 SAGA Log)以保证系统恢复后可以追踪到子事务的执行情况,譬如执行至哪一步或者补偿至哪一步了。

SAGA 事务通常也不会直接靠裸编码来实现,一般也是在事务中间件的基础上完成,前面提到的 Seata 就同样支持 SAGA 事务模式。

AT 事务模式

AT 事务是参照了 XA 两段提交协议实现的但在业务数据提交时自动拦截所有 SQL,将 SQL 对数据修改前、修改后的结果分别保存快照,生成行锁,通过本地事务一起提交到操作的数据源中,相当于自动记录了重做和回滚日志。如果分布式事务成功提交,那后续清理每个数据源中对应的日志数据即可;

大幅度地牺牲了隔离性,甚至直接影响到了原子性。会出现脏写。

脏写解决方式:

GTS 增加了一个“全局锁”(Global Lock)的机制来实现写隔离,要求本地事务提交之前,一定要先拿到针对修改记录的全局锁后才允许提交,没有获得全局锁之前就必须一直等待,这种设计以牺牲一定性能为代价,避免了有两个分布式事务中包含的本地事务修改了同一个数据,从而避免脏写。

分布式事务中没有一揽子包治百病的解决办法,因地制宜地选用合适的事务处理方案才是唯一有效的做法。

透明多级分流系统

不同的设施、部件在系统中有各自不同的价值。

系统进行流量规划设计原则

  • 第一条原则是尽可能减少单点部件
  • 奥卡姆剃刀原则。不是每一个系统都要追求高并发、高可用的,根据系统的用户量峰值流量团队本身的技术与运维能力来考虑如何部署这些设施才是合理的做法,在能满足需求的前提下,最简单的系统就是最好的系统

客户端缓存

客户端缓存(Client Cache)

HTTP 协议的无状态性决定了它必须依靠客户端缓存来解决网络传输效率上的缺陷。

强制缓存

假设在某个时点到来以前,资源的内容和状态一定不会被改变,因此客户端可以无须经过任何请求,在该时点前一直持有和使用该资源的本地缓存副本。

在用户主动刷新页面时应当自动失效

以下两类 Header 实现强制缓存

  • Expires:Expires 是 HTTP/1.0 协议中开始提供的 Header,后面跟随一个截至时间参数。

    • 受限于客户端的本地时间。
    • 无法处理涉及到用户身份的私有资源(私有资源如果缓冲到代理服务器或CDN则有泄露的风险)
    • 无法描述“缓存”的语义
  • Cache-Control:Cache-Control 是 HTTP/1.1 协议中定义的强制缓存 Header,它的语义比起 Expires 来说就丰富了很多

    1
    2
    HTTP/1.1 200 OK
    Cache-Control: max-age=600

    标准参数

    • max-ages-maxage:max-age 后面跟随一个以秒为单位的数字,表明相对于请求时间(在 Date Header 中会注明请求时间)多少秒以内缓存是有效的

    • publicprivate:指明是否涉及到用户身份的私有资源,如果是 public,则可以被代理、CDN 等缓存,如果是 private,则只能由用户的客户端进行私有缓存。

    • no-cacheno-store:no-cache 指明该资源不应该被缓存,必须从服务端获取,令强制缓存完全失效,但此时协商缓存机制依然是生效的;no-store 不强制会话中相同 URL 资源的重复获取,但禁止浏览器、CDN 等以任何形式保存该资源。

    • no-transform:禁止资源被任何形式地修改。譬如,某些 CDN、透明代理支持自动 GZip 压缩图片或文本,以提升网络性能

    • min-freshonly-if-cached:这两个参数是仅用于客户端的请求 Header。min-fresh 后续跟随一个以秒为单位的数字,用于建议服务器能返回一个不少于该时间的缓存资源(即包含 max-age 且不少于 min-fresh 的数字)。only-if-cached 表示客户端要求不必给它发送资源的具体内容,此时客户端就仅能使用事先缓存的资源来进行响应,若缓存不能命中,就直接返回 503/Service Unavailable 错误。

    • must-revalidateproxy-revalidate:must-revalidate 表示在资源过期后,一定需要从服务器中进行获取,即超过了 max-age 的时间后,就等同于 no-cache 的行为,proxy-revalidate 用于提示代理、CDN 等设备资源过期后的缓存行为,除对象不同外,语义与 must-revalidate 完全一致。

协商缓存

基于检测的缓存机制,通常被称为“协商缓存”

在 HTTP 中协商缓存与强制缓存并没有互斥性,这两套机制是并行工作的

协商缓存有两种变动检查机制,分别是根据资源的修改时间进行检查,以及根据资源唯一标识是否发生变化来进行检查,

  • Last-Modified 和 If-Modified-Since:Last-Modified 是服务器的响应 Header,用于告诉客户端这个资源的最后修改时间。对于带有这个 Header 的资源,当客户端需要再次请求时,会通过 If-Modified-Since 把之前收到的资源最后修改时间发送回服务端。

    如果此时服务端发现资源在该时间后没有被修改过,就只要返回一个 304/Not Modified 的响应即可,无须附带消息体,达到节省流量的目的,如下所示:

    1
    2
    3
    HTTP/1.1 304 Not Modified
    Cache-Control: public, max-age=600
    Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT

    如果此时服务端发现资源在该时间之后有变动,就会返回 200/OK 的完整响应,在消息体中包含最新的资源,如下所示:

    1
    2
    3
    4
    5
    HTTP/1.1 200 OK
    Cache-Control: public, max-age=600
    Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT

    Content
  • Etag 和 If-None-Match:Etag 是服务器的响应 Header,用于告诉客户端这个资源的唯一标识。HTTP 服务器可以根据自己的意愿来选择如何生成这个标识,譬如 Apache 服务器的 Etag 值默认是对文件的索引节点(INode),大小和最后修改时间进行哈希计算后得到的。对于带有这个 Header 的资源,当客户端需要再次请求时,会通过 If-None-Match 把之前收到的资源唯一标识发送回服务端。

    如果此时服务端计算后发现资源的唯一标识与上传回来的一致,说明资源没有被修改过,就只要返回一个 304/Not Modified 的响应即可,无须附带消息体,达到节省流量的目的,如下所示:

    1
    2
    3
    HTTP/1.1 304 Not Modified
    Cache-Control: public, max-age=600
    ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"

    如果此时服务端发现资源的唯一标识有变动,就会返回 200/OK 的完整响应,在消息体中包含最新的资源,如下所示:

    1
    2
    3
    4
    5
    HTTP/1.1 200 OK
    Cache-Control: public, max-age=600
    ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"

    Content

Last-Modified 标注的最后修改只能精确到秒级,不能准确标注文件一秒内的修改时间;也可能内容没变,Last-Modified变了。

Etag 是 HTTP 中一致性最强的缓存机制,但是每次服务端都必须对资源进行哈希计算,故而是性能最差的缓存机制。

Etag 和 Last-Modified 是允许一起使用的,服务器会优先验证 Etag,在 Etag 一致的情况下,再去对比 Last-Modified,这是为了防止有一些 HTTP 服务器未将文件修改日期纳入哈希范围内。

单个资源识别

HTTP 协议设计了以 Accept*(Accept、Accept-Language、Accept-Charset、Accept-Encoding)开头的一套请求 Header 和对应的以 Content-*(Content-Language、Content-Type、Content-Encoding)开头的响应 Header,这些 Headers 被称为 HTTP 的内容协商机制。

多个资源标识

Vary Header 对于一个 URL 能够获取多个资源的场景中,缓存也同样也需要有明确的标识来获知根据什么内容来对同一个 URL 返回给用户正确的资源

1
2
HTTP/1.1 200 OK
Vary: Accept, User-Agent

根据 MIME 类型和浏览器类型来缓存资源,获取资源时也需要根据请求 Header 中对应的字段来筛选出适合的资源版本。

刷新页面(F5)时也同样是生效的,只有用户强制刷新(Ctrl+F5)或者明确禁用缓存(譬如在 DevTools 中设定)时才会失效,

域名缓存(DNS )

DNS 也许是全世界最大、使用最频繁的信息查询系统,如果没有适当的分流机制,DNS 将会成为整个网络的瓶颈。

震惊!世界根域名服务器的 ZONE 文件竟然只有 2MB 大小

DNS解析步骤

例如 www.icyfenix.com.cn

  • 域名还原 : www.icyfenix.com.cn.
  • 客户端先检查本地的 DNS 缓存:根据存活时间(Time to Live,TTL)来衡量缓存的有效情况。
  • 客户端将地址发送给本机操作系统中配置的本地 DNS(Local DNS)
  • 本地 DNS 收到查询请求后判断 有无对于的权威域名服务器
    • 是否有www.icyfenix.com.cn的权威服务器
    • 是否有icyfenix.com.cn的权威服务器
    • 是否有com.cn的权威服务器
    • 是否有cn的权威服务器
    • . 的根域名服务器
  • 假如到根域名服务器后
    • 通过根域名服务器 得到 cn的权威服务器 的地址记录
    • 通过“cn的权威服务器”,得到“com.cn的权威服务器”的地址记录
    • 以此类推,最后找到能够解释www.icyfenix.com.cn的权威服务器地址
  • 通过“www.icyfenix.com.cn的权威服务器”,查询www.icyfenix.com.cn的地址记录,(譬如 IPv4 下的 IP 地址为 A 记录,IPv6 下的 AAAA 记录、主机别名 CNAME 记录,等等。)

每种记录类型中还可以包括多条记录,以一个域名下配置多条不同的 A 记录为例,此时权威服务器可以根据自己的策略来进行选择,典型的应用是智能线路:根据访问者所处的不同地区、不同服务商等因素来确定返回最合适的 A 记录,将访问者路由到最合适的数据中心,达到智能加速的目的。

缺点优化

  • 极端情况下响应速度慢:“DNS 预取”在网页加载时生成一个 link 请求,促使浏览器提前对该域名进行预解释,譬如下面代码所示:
1
<link rel="dns-prefetch" href="//domain.not-icyfenx.cn">
  • 受到中间人攻击的威胁:位于递归链底层或者来自本地运营商的 Local DNS 服务器的安全防护则相对松懈。

    HTTPDNS(也称为 DNS over HTTPS,DoH)。它将原本的 DNS 解析服务开放为一个基于 HTTPS 协议的查询服务,替代基于 UDP 传输协议的 DNS 域名解析,通过程序代替操作系统直接从权威 DNS 或者可靠的 Local DNS 获取解析数据,从而绕过传统 Local DNS。

传输链路

优化链路传输为目的的前端设计原则,譬如经典的雅虎 YSlow-23 条规则

缺陷:

HTTP 传输对象的主要特征是数量多、时间短、资源小、切换快。把小文件合并成大文件,在 HTTP/2 下是毫无好处的。

TCP 协议要求必须在三次握手而且还有慢启动的特性,因此并不适合HTTP。为此优化为

  • 连接数优化 副作用:资源耦合 缓存效率下降

  • 连接复用技术(连接Keep-Alive 机制) 副作用是“队首阻塞

  • HTTP/2 多路复用 (最小单位由请求变成了 ,可同时多个连接数据混在一起 接收时数据重组,因此无需压缩请求数)

  • 数据压缩

  • 快速 UDP 网络连接(QUIC)以 UDP 协议为基础,提供可靠传输能力。

    ​ 优点如下

    • 能对每个流能做单独的控制 (一个流中发生错误,协议栈仍然可以独立地继续为其他流提供服务)
    • 使用连接标识符唯一地标识客户端与服务器之间的连接,切换IP(数据切换wifi),原始连接连接标识符依然是有效的。
    • QUIC 连接失败时以零延迟回退到 TCP 连接

内容分发网络

CDN 是一种十分古老而又十分透明,没什么存在感的分流系统,许多人都说听过它,但真正了解过它的人却很少。

互联网系统的速度取决于以下四点因素:

  • 网站服务器出口带宽
  • 用户客户端入口带宽。
  • 从网站到用户之间经过的不同运营商之间互联节点的带宽
  • 网站到用户之间的物理链路传输时延(Ping)

内容分发网络的工作过程

  • 路由解析

    1. 将服务器的 IP 地址在你的 CDN 服务商上注册为“源站”,注册后你会得到一个 CNAME

    2. 将得到的 CNAME 在你购买域名的 DNS 服务商上注册为一条 CNAME 记录。

    3. 当第一位用户来访你的站点时,将首先发生一次未命中缓存的 DNS 查询,域名服务商解析出 CNAME 后,返回给本地 DNS,至此之后链路解析的主导权就开始由内容分发网络的调度服务接管了。

    4. 本地 DNS 查询 CNAME 时,由于能解析该 CNAME 的权威服务器只有 CDN 服务商所架设的权威 DNS。DNS 服务将根据一定的均衡策略和参数,DNS 服务将根据一定的均衡策略和参数。

    5. 浏览器从本地 DNS 拿到 IP 地址,将该 IP 当作源站服务器来进行访问

      image-20211222223531779

  • 内容分发(CDN 获取源站资源的过程)

    • 主动分发(Push):分发由源站主动发起,将内容从源站或者其他资源库推送到用户边缘的各个 CDN 缓存节点上。通常需要源站、CDN 服务双方提供程序 API 接口层面的配合。一般用于网站要预载大量资源的场景。如:双十一抢购
    • 被动回源(Pull):被动回源由用户访问所触发全自动、双向透明的资源缓存过程。当某个资源首次被用户请求的时候,CDN 缓存节点发现自己没有该资源,就会实时从源站中获取。首次访问通常是比较慢的,不适合应用于数据量较大的资源。完全的双向透明,
  • CDN 应用

    1. 加速静态资源

    2. 安全防御:CDN 在广义上可以视作网站的堡垒机

    3. 协议升级:不少 CDN 提供商都同时对接(代售 CA 的)SSL 证书服务,可以实现源站是 HTTP 协议的,而对外开放的网站是基于 HTTPS 的。

    4. 状态缓存:不仅可以缓存源站的资源,还可以缓存源站的状态,

      譬如源站的

      • 301/302 转向,
      • OCSP 装订加速 SSL 证书访问,
      • CDN 开启HSTS
      • 404
    5. 修改资源:CDN 可以在返回资源给用户的时候修改它的任何内容,以实现不同的目的。如

      • 未压缩的资源自动压缩并修改 Content-Encoding
      • 未启用客户端缓存的内容加上缓存 Header
      • 修改CORS的相关 Header,将源站不支持跨域的资源提供跨域能力
    6. 访问控制:

      • CDN 可以实现 IP 黑/白名单功能

      • 不同的来访 IP 提供不同的响应结果

      • 根据 IP 的访问流量来实现 QoS 控制

      • 根据 HTTP 的 Referer 来实现防盗链

    7. 注入功能:在不修改源站代码的前提下,为源站注入各种功能

负载均衡

调度后方的多台机器,以统一的接口对外提供服务,承担此职责的技术组件被称为“负载均衡”。

负载均衡,从形式上来说都可以分为两种:四层负载均衡和七层负载均衡。(经典的OSI 七层模型中第四层传输层和第七层应用层。)

最典型的 1500 Bytes MTU 的以太网帧结构说明

数据单元 功能
7 应用层 Application Layer 数据 Data 提供为应用软件提供服务的接口,用于与其他应用软件之间的通信。典型协议:HTTP、HTTPS、FTP、Telnet、SSH、SMTP、POP3 等
6 表达层 Presentation Layer 数据 Data 把数据转换为能与接收者的系统格式兼容并适合传输的格式。
5 会话层 Session Layer 数据 Data 负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。
4 传输层 Transport Layer 数据段 Segments 把传输表头加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。典型协议:TCP、UDP、RDP、SCTP、FCP 等
3 网络层 Network Layer 数据包 Packets 决定数据的传输路径选择和转发,将网络表头附加至数据段后以形成报文(即数据包)。典型协议:IPv4/IPv6、IGMP、ICMP、EGP、RIP 等
2 数据链路层 Data Link Layer 数据帧 Frame 负责点对点的网络寻址、错误侦测和纠错。当表头和表尾被附加至数据包后,就形成数据帧(Frame)。典型协议:WiFi(802.11)、Ethernet(802.3)、PPP 等。
1 物理层 Physical Layer 比特流 Bit 在物理网络上传送数据帧,它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等。

现在所说的“四层负载均衡”其实是多种均衡器工作模式的统称,“四层”的意思是说这些工作模式的共同特点是维持着同一个 TCP 连接,而不是说它只工作在第四层。

数据链路层负载均衡

每一块网卡都有独立的 MAC 地址,以太帧上这两个地址告诉了交换机,此帧应该是从连接在交换机上的哪个端口的网卡发出,送至哪块网卡的。

数据链路层负载均衡(三角传输模式 单臂模式 直接路由 )所做的工作,是修改请求的数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器(后文称为“真实服务器”,Real Server)的网卡上,这样真实服务器就获得了一个原本目标并不是发送给它的数据帧。需要把真实物理服务器集群所有机器的虚拟 IP 地址(Virtual IP Address,VIP)配置成与负载均衡器的虚拟 IP 一样。响应结果就不再需要通过负载均衡服务器进行地址交换,可将响应结果的数据包直接从真实服务器返回给用户的客户端。

image-20211222231334218

网络层负载均衡

分组数据包的 Headers 部分说明

长度 存储信息
0-4 Bytes 版本号(4 Bits)、首部长度(4 Bits)、分区类型(8 Bits)、总长度(16 Bits)
5-8 Bytes 报文计数标识(16 Bits)、标志位(4 Bits)、片偏移(12 Bits)
9-12 Bytes TTL 生存时间(8 Bits)、上层协议代号(8 Bits)、首部校验和(16 Bits)
13-16 Bytes 源地址(32 Bits)
17-20 Bytes 目标地址(32 Bits)
20-60 Bytes 可选字段和空白填充

网络层负载均衡通过改变这里面的 IP 地址来实现数据包的转发。

两种常见的修改方式

  • 保持原来的数据包不变,新创建一个数据包,这个新数据包的 Headers 中写入真实服务器的 IP 作为目标地址,原来数据包的 Headers 和 Payload 整体作为另一个新的数据包的 Payload(封包 影响效率)。真实服务器收到数据包后,必须在接收入口处设计一个针对性的拆包机制.(套娃)

    缺点: 服务器支持拆包与虚拟IP

    image-20211226162443263

  • NAT 模式的负载均衡器(较大性能损失)image-20211226162751661

应用层负载均衡

根据“哪一方能感知到”的原则,可以分为“正向代理”、“反向代理”和“透明代理”三类。

与四层均衡器对比的缺点:

  • 比四层均衡器至少多一轮 TCP 握手,有着跟 NAT 转发模式一样的带宽问题
  • 通常要耗费更多的 CPU,因为可用的解析规则远比四层丰富。

优点以及功能

  • 感知应用层通信的具体内容,往往能够做出更明智的决策,花样多。
  • 缓存
  • 可以实现更智能化的路由。
  • 某些安全攻击可以由七层均衡器来抵御
  • 链路治理措施

均衡策略与实现

常见均衡策略

  • 轮循均衡
  • 权重轮循均衡
  • 随机均衡
  • 权重随机均衡
  • 一致性哈希均衡 :根据请求中某一些数据(可以是 MAC、IP 地址,也可以是更上层协议中的某些参数信息)作为特征值来计算需要落在的节点上
  • 响应速度均衡(Response Time):负载均衡设备对内部各服务器发出一个探测请求(例如 Ping),然后根据内部中各服务器对探测请求的(负载均衡设备与服务器间的)最快响应时间来决定哪一台服务器来响应客户端的服务请求。
  • 最少连接数均衡 适合长时处理的请求服务,如 FTP 传输。

实现:

软件均衡器:

  • 操作系统内核(性能比较好):LVS

  • 应用程序形式:Nginx、HAProxy、KeepAlived

硬件均衡器:

服务端缓存

软件开发中的缓存并非多多益善,它有收益,也有风险。

在软件开发中引入缓存的负面作用要明显大于硬件的缓存.因此需要足够的理由:

  • 为缓解 CPU 压力而做缓存
  • 为缓解 I/O 压力而做缓存

缓存虽然是典型以空间换时间来提升性能的手段,但它的出发点是缓解 CPU 和 I/O 资源在峰值流量下的压力,“顺带”而非“专门”地提升响应性能(优先增强硬件)。

缓存属性
  • 吞吐量:缓存的吞吐量使用 OPS 值(每秒操作数,Operations per Second,ops/s)来衡量,反映了对缓存进行并发读、写操作的效率,即缓存本身的工作效率高低。

  • 命中率:缓存的命中率即成功从缓存中返回结果次数与总请求次数的比值,反映了引入缓存的价值高低,命中率越低,引入缓存的收益越小,价值越低。

    淘汰策略:

    • FIFO 优先淘汰最早进入被缓存的数据
    • LRU 优先淘汰最久未被使用访问过的数据。 添加 List作为最近时间排序列表。访问时调整对象到开头,优先淘汰末尾
    • LFU:优先淘汰最不经常使用的数据。 添加计数器,统计被访问次数。
    • TinyLFU : LFU 的改进版,首先采用 Sketch (用少量的样本估计全体数,采用了基于“滑动时间窗”的热度衰减算法)对访问数据进行分析,牺牲准确性,减少计数器维护成本。
    • W-TinyLFU :TinyLFU 的改进版本。应对稀疏突发访问(突然访问频率增高的数据)的问题。从整体上看是它是 LFU 策略,从局部实现上看又是 LRU 策略。
  • 扩展功能:缓存除了基本读写功能外,还提供哪些额外的管理功能,譬如加载器淘汰策略失效策略事件通知,并发级别容量控制,引用方式,统计信息,持久化等等。以下为几款主流进程内缓存方案对比

    ConcurrentHashMap Ehcache Guava Cache Caffeine
    访问性能 最高 一般 良好 优秀 接近于 ConcurrentHashMap
    淘汰策略 支持多种淘汰策略 FIFO、LRU、LFU 等 LRU W-TinyLFU
    扩展功能 只提供基础的访问接口 并发级别控制 失效策略 容量控制 事件通知 统计信息 …… 大致同左 大致同左
  • 分布式支持:缓存可分为“进程内缓存”和“分布式缓存”两大类,前者只为节点本身提供服务,无网络访问操作,速度快但缓存的数据不能在各个服务节点中共享,后者则相反。

    从访问的角度

    • 复制式缓存 (适合读多写少) : 缓存中所有数据在分布式集群的每个节点里面都存在有一份副本,读取数据时无须网络访问,直接从当前节点的进程内存中返回
    • 集中式缓存: 集中式缓存的读、写都需要网络访问.集中式缓存的读、写都需要网络访问,访问性能较差。但进程独立。(Redis牛逼)

    从数据一致性角度(分为 AP 和 CP 两种类型)

    • AP: redis 高性能高可用等特点,却并不保证强一致性
    • CP:保证强一致性的 ZooKeeper、Doozerd、Etcd ,不做缓存。通知、协调、队列、分布式锁等功能

image-20211227231252943

缺点:代码侵入性较大,需要由开发者承担多次查询、多次回填的工作,也不便于管理

缓存风险

缓存穿透

现象:如果查询的数据在数据库中根本不存在的话,缓存里自然也不会有,这类请求的流量每次都不会命中,每次都会触及到末端的数据库,缓存就起不到缓解压力的作用了,这种查询不存在数据的现象被称为缓存穿透。

出现原因:有可能是业务逻辑本身就存在的固有问题,也有可能是被恶意攻击的所导致

解决方式:

  • 在一定时间内对返回为空(异常不缓存)的 Key 值依然进行缓存
  • 缓存之前设置一个布隆过滤器来解决

缓存击穿

现象:缓存中某些热点数据忽然因某种原因失效了,譬如典型地由于超期而失效,此时又有多个针对该数据的请求同时发送过来,这些请求将全部未能命中缓存,都到达真实数据源中去,导致其压力剧增,

解决方式:

  • 加锁同步,以请求该数据的 Key 值为锁,使得只有第一个请求可以流入到真实的数据源中,其他线程采取阻塞或重试策略。
  • 热点数据由代码来手动管理:缓存击穿是仅针对热点数据被自动失效才引发的问题,对于这类数据,可以直接由开发者通过代码来有计划地完成更新、失效,避免由缓存的策略自动管理。

缓存雪崩

现象:大批不同的数据在短时间内一起失效,导致了这些数据的请求都击穿了缓存到达数据源,同样令数据源在短时间内压力剧增。

出现原因:大量数据一起加载进去

  • 系统有专门的缓存预热功能,也可能大量公共数据是由某一次冷操作加载的,
  • 缓存服务由于某些原因崩溃后重启,此时也会造成大量数据同时失效,

解决方式:

  • 提升缓存系统可用性,建设分布式缓存的集群。
  • 启用透明多级缓存,各个服务节点一级缓存中的数据通常会具有不一样的加载时间,也就分散了它们的过期时间。
  • 将缓存的生存期从固定时间改为一个时间段内的随机时间

缓存污染

现象:缓存污染是指缓存中的数据与真实数据源中的数据不一致的现象。

原因:由开发者更新缓存不规范造成的,譬如事务异常缓存没有回滚

解决方式:更新缓存可以遵循设计模式Cache Aside(最简单、成本最低的 )、Read/Write Through、Write Behind Caching 等。

Cache Aside :

  • 读数据时,先读缓存,缓存没有的话,再读数据源,然后将数据放入缓存,再响应请求。
  • 写数据时,先写数据源,然后失效(而不是更新)掉缓存。

在查询操作与回填缓存间隙时插入数据会出现数据不一致的情况。

透明多级分流系统这个小节看完了,整体从客户端出发到服务端之整个链路的缓冲,先前日常工作主要是后端,关注的更多是客户端上的缓存(Redis),相对来说狭隘许多。反而因为这种工作中很少感知的倒是加深了对“透明”的理解。

架构安全性

认证

系统如何正确分辨出操作用户的真实身份

一个架构安全性的经验原则:以标准规范为指导、以标准接口去实现。

标准

主流的三种认证方式

  • 通信信道上的认证:你和我建立通信连接之前认证,在网络传输(Network)场景中的典型是基于 SSL/TLS 传输安全层的认证。
  • 通信协议上的认证:你请求获取我的资源之前认证,在互联网(Internet)场景中的典型是基于 HTTP 协议的认证。
  • 通信内容上的认证:你使用我提供的服务之前认证,在万维网(World Wide Web)场景中的典型是基于 Web 内容的认证。
HTTP 认证

面向传输协议,认证由HTTP服务器完成

认证方案

  1. 未授权的用户意图访问服务端保护区域的资源时,应返回 401 Unauthorized 的状态码 ,在Header下加入

    1
    2
    WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
    Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>
  2. 客户端遵循服务端指定的认证方案,在请求资源的报文头中加入身份凭证信息

    1
    2
    Authorization: <认证方案> <凭证内容>
    Proxy-Authorization: <认证方案> <凭证内容>

HTTP 认证框架提出认证方案是希望能把认证“要产生身份凭证”的目的“具体如何产生凭证”的实现分离开来。无论客户端如何生成凭证的具体实现,都可以包容在 HTTP 协议预设的框架之内。

image-20211228231431347

HTTP 认证框架中的认证方案是允许自行扩展的。只要用户代理(User Agent,通常是浏览器,泛指任何使用 HTTP 协议的程序)能够识别这种私有的认证方案即可。

常见认证方案

  • RFC 规范
  • AWS4-HMAC-SHA256:亚马逊 AWS 基于 HMAC-SHA256 哈希算法的认证。
  • NTLM / Negotiate:这是微软公司 NT LAN Manager(NTLM)用到的两种认证方式。
  • Windows Live ID:微软开发并提供的“统一登入”认证。
  • Twitter Basic:一个不存在的网站所改良的 HTTP 基础认证。
Web 认证

面向具体传输内容来设计,由系统本身提供。实现形式以登陆表单为主。

认证方案WebAuthn 分为两大部分 :

注册:

image-20220102124653206

验证器可理解为用户设备上 TouchID、FaceID、实体密钥等认证设备的统一接口。

登陆

  1. 用户访问登录页面,填入用户名后即可点击登录按钮。
  2. 服务器返回随机字符串 Challenge、用户 UserID。
  3. 浏览器将 Challenge 和 UserID 转发给验证器。
  4. 验证器提示用户进行认证操作。由于在注册阶段验证器已经存储了该域名的私钥和用户信息,所以如果域名和用户都相同的话,就不需要生成密钥对了,直接以存储的私钥加密 Challenge,然后返回给浏览器。
  5. 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。

实现

具体的安全框架提供的功能都很类似,大致包括以下四类:

  • 认证功能:以 HTTP 协议中定义的各种认证、表单等认证方式确认用户身份,这是本节的主要话题。
  • 安全上下文:用户获得认证之后,要开放一些接口,让应用可以得知该用户的基本资料、用户拥有的权限、角色,等等。
  • 授权功能:判断并控制认证后的用户对什么资源拥有哪些操作许可,这部分内容会放到“授权”介绍。
  • 密码的存储与验证:密码是烫手的山芋,存储、传输还是验证都应谨慎处理,我们会放到“保密”去具体讨论。

授权

系统如何控制一个用户该看到哪些数据、能操作哪些功能?

授权所涉及到的问题

  • 确保授权的过程可靠
  • 确保授权的结果可控

RBAC

所有的访问控制模型,实质上都是在解决同一个问题:“(User)拥有什么权限(Authority)去操作(Operation)哪些资源(Resource)”。

RBAC 将权限从用户身上剥离,改为绑定到“角色”(Role)上,将权限控制变为对“角色拥有操作哪些资源许可

角色为的是解耦用户与权限之间的多对多关系

许可为的是解耦操作与资源之间的多对多关系

RBAC 模型的演进

  • RBAC-1 模型的角色权限继承关系
  • RBAC-1 模型的角色权限继承关系

image-20220102181803608

数据权限基本只能由信息系统自主来来完成,并不存在能放之四海皆准的通用数据权限框架(日常工作用解决方案是添加行级权限)。

OAuth2

面向于解决第三方应用(Third-Party Application)的认证授权协议。

直接使用密码给第三方的缺点

  • 密码泄漏
  • 访问范围
  • 授权回收

授权的流程

image-20220102182458407

授权方式

授权码模式 (最严谨)

image-20220102182627220

授权过程

前置条件 第三方应用先要到授权服务器上进行注册。向认证服务器提供一个域名地址,从授权服务器中获取 ClientID 和 ClientSecret.

  1. 第三方应用将资源所有者(用户)导向授权服务器的授权页面,并向授权服务器提供 ClientID 及用户同意授权后的回调 URI
  2. 授权服务器根据 ClientID 确认第三方应用的身份
  3. 用户同意授权,授权服务器将转向第三方应用在第 1 步调用中提供的回调 URI,并附带上一个授权码和获取令牌的地址作为参数,
  4. 第三方应用通过回调地址收到授权码,然后将授权码与自己的 ClientSecret 一起作为参数,通过服务器向授权服务器提供的获取令牌的服务地址发起请求,换取令牌。
  5. 授权服务器核对授权码和 ClientSecret,确认无误后,向第三方应用授予令牌(访问令牌与刷新令牌)。

缺点:

  • 第三方应用必须有应用服务器
  • 繁复的调用过程

隐式授权模式

image-20220102191631758

相对于授权码模式省略掉了通过授权码换取令牌的步骤。授权服务器在得到用户授权后,直接返回了访问令牌。需要在注册时提供回调域名,此时会要求该域名与接受令牌的服务处于同一个域内。明确禁止发放刷新令牌。

密码模式

“第三方”视作是系统中与授权服务器相对独立的子模块,逻辑上与授权服务器仍同属一个系统。

image-20220102193026796

第三方应用拿着用户名和密码向授权服务器换令牌

客户端模式

客户端模式是指第三方应用以自己的名义,向授权服务器申请资源许可。

image-20220102193149106

例如: 商品订单清理的定时服务,自动清理超过两分钟还未付款的订单时,直接以自己的名义向授权服务器申请一个能清理所有用户订单的授权。

凭证

系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的?

为了实现”让服务器至少有办法能够区分出发送请求的用户是谁“这个目的,RFC 6265规范定义了 HTTP 的状态管理机制,在 HTTP 协议中增加了 Set-Cookie 指令,该指令的含义是以键值对的方式向客户端发送一组信息,此信息将在此后一段时间内的每次 HTTP 请求中,以名为 Cookie 的 Header 附带着重新发回给服务端,以便服务端区分来自不同客户端的请求。

一个典型的 Set-Cookie 指令如下所示:

1
Set-Cookie: id=icyfenix; Expires=Wed, 21 Feb 2020 07:28:00 GMT; Secure; HttpOnly

收到该指令以后,客户端再对同一个域的请求中就会自动附带有键值对信息id=icyfenix,譬如以下代码所示:

1
2
3
GET /index.html HTTP/2.0
Host: icyfenix.cn
Cookie: id=icyfenix

一般来说,系统会把状态信息保存在服务端,在 Cookie 里只传输的是一个无字面意义的、不重复的字符串,习惯上以sessionid或者jsessionid为名。服务器拿这个字符串为 Key,在内存中开辟一块空间,以 Key/Entity 的结构存储每一个在线用户的上下文状态,再辅以一些超时自动清理之类的管理措施。这种服务端的状态管理机制就是今天大家非常熟悉的 Session,Cookie-Session .

优势:

  • 完全规避掉上下文信息在传输过程中被泄漏和篡改的风险。
  • 服务端有主动的状态管理能力

分布式环境中的状态管理一定会受到 CAP 的局限,无论怎样都不可能完美。但如果只是解决分布式下的认证授权问题,并顺带解决少量状态的问题,就不一定只能依靠共享信息去实现。

JWT

当服务器存在多个,客户端只有一个时,把状态信息存储在客户端,每次随着请求发回服务器去。

确保信息不被中间人篡改

使用方式

附在名为 Authorization 的 Header 发送给服务端,前缀在RFC 6750中被规定为 Bearer。

JWT 令牌结构

  • 令牌头(Header),内容如下所示:

    1
    2
    3
    4
    {
    "alg": "HS256",
    "typ": "JWT"
    }

    它描述了令牌的类型(统一为 typ:JWT)以及令牌签名的算法,示例中 HS256 为 HMAC SHA256 算法的缩写,其他各种系统支持的签名算法可以参考https://jwt.io/网站所列。

  • 负载(Payload),这是令牌真正需要向服务端传递的信息。包括

    1. 这个用户是谁
    2. 这个用户是谁
  • 签名 确保负载中的信息是可信的、没有被篡改的,也没有在传输过程中丢失任何信息。

缺点:

  • 令牌难以主动失效
  • 相对更容易遭受重放攻击
  • 只能携带相当有限的数据
  • 必须考虑令牌在客户端如何存储
  • 无状态也不总是好的

保密

系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用?

保密是有成本的,追求越高的安全等级,就要付出越多的工作量与算力消耗。

一次性密码(One Time Password)具有绝对安全性,但是却需要提前安全地把密码或密码列表传达给对方。因而对于互联网没有任何的可行性。

客户端加密

为了保证信息不被黑客窃取而做客户端加密没有太多意义,客户端加密在意义在于可以最早时候消灭掉明文密码。

真正防御性的密码加密存储确实应该在服务端中进行,但这是为了防御服务端被攻破而批量泄漏密码的风险,并不是为了增加传输过程的安全。

密码存储和验证

密码创建过程

  1. 用户在客户端注册,输入明文密码:123456

    1
    password = 123456
  2. 客户端对用户密码进行简单哈希摘要

    1
    client_hash = MD5(password) // e10adc3949ba59abbe56e057f20f883e
  3. 为了防御彩虹表攻击应加盐处理,客户端加盐只取固定的字符串即可,如实在不安心,最多用伪动态的盐值(“伪动态”是指服务端不需要额外通信可以得到的信息,譬如由日期或用户名等自然变化的内容,加上固定字符串构成)。

    1
    client_hash = MD5(MD5(password) + salt)  // SALT = $2a$10$o5L.dWYEjZjaejOmN3x4Qu
  4. 为了预防暴力破解,建议使用慢哈希函数

  5. 盐值可以由密码学安全伪随机数生成器生成。、

    1
    2
    3
    SecureRandom random = new SecureRandom();
    byte server_salt[] = new byte[36];
    random.nextBytes(server_salt); // tq2pdxrblkbgp8vt8kbdpmzdh1w8bex
  6. 将动态盐值混入客户端传来的哈希值再做一次哈希,产生出最终的密文,并和上一步随机生成的盐值一起写入到同一条数据库记录中。BCryptPasswordEncoder本身就会自动调用 CSPRNG 产生盐值,并将该盐值输出在结果的前 32 位之中

    1
    2
    server_hash = SHA256(client_hash + server_salt);  // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
    DB.save(server_hash, server_salt);

验证的过程

  1. 客户端,用户在登录页面中输入密码明文:123456,经过与注册相同的加密过程,向服务端传输加密后的结果。

    1
    authentication_hash = MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2
  2. 服务端,接受到客户端传输上来的哈希值,从数据库中取出登录用户对应的密文和盐值,采用相同的哈希算法,对客户端传来的哈希值、服务端存储的盐值计算摘要结果。

    1
    result = SHA256(authentication_hash + server_salt);  // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
  3. 比较上一步的结果和数据库储存的哈希值是否相同,如果相同那么密码正确,反之密码错误。

    1
    authentication = compare(result, server_hash) // yes

传输

系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充?

现代密码学算法的三种主要用途:摘要加密签名

摘要

摘要的意义是在源信息不泄漏的前提下辨别其真伪。

理想的哈希算法都具备两个特性:

  • 易变性
  • 不可逆性

加密

加密与摘要的本质区别在于加密是可逆的,逆过程就是解密。

根据加密与解密是否采用同一个密钥,现代密码学算法可分为对称加密算法和非对称加密两大类型

类型 特点 常见实现 主要用途 主要局限
哈希摘要 不可逆,即不能解密,所以并不是加密算法,只是一些场景把它当作加密算法使用。 易变性,输入发生 1 Bit 变动,就可能导致输出结果 50%的内容发生改变。 无论输入长度多少,输出长度固定(2 的 N 次幂)。 MD2/4/5/6、SHA0/1/256/512 摘要 无法解密
对称加密 加密是指加密和解密是一样的密钥。 设计难度相对较小,执行速度相对较块。 加密明文长度不受限制。 DES、AES、RC4、IDEA 加密 要解决如何把密钥安全地传递给解密者。
非对称加密 加密和解密使用的是不同的密钥。 明文长度不能超过公钥长度。 RSA、BCDSA、ElGamal 签名、传递密钥 性能与加密明文长度受限。

数字证书

公开密钥基础设施借着数字证书认证中心(Certificate Authority,CA)将用户的个人身份跟公开密钥链接在一起。对每个证书中心用户的身份必须是唯一的。

权威的 CA 中心则应是可数的,“可数”意味着可以不通过网络,而是在浏览器与操作系统出厂时就预置好.

PKI 中采用的证书格式是X.509 标准格式,。一个数字证书具体包含以下内容:

  1. 版本号(Version)

    1
    Version: 3 (0x2)
  2. 序列号(Serial Number)

    1
    Serial Number: 04:00:00:00:00:01:15:4b:5a:c3:94
  3. 签名算法标识符(Signature Algorithm ID):用于签发证书的算法标识,由对象标识符加上相关的参数组成,用于说明本证书所用的数字签名算法。

    1
    Signature Algorithm: sha1WithRSAEncryption
  4. 认证机构的数字签名(Certificate Signature):这是使用证书发布者私钥生成的签名,以确保这个证书在发放之后没有被篡改过。

  5. 认证机构(Issuer Name): 证书颁发者的可识别名。

    1
    Issuer: C=BE, O=GlobalSign nv-sa, CN=GlobalSign Organization Validation CA - SHA256 - G2
  6. 有效期限(Validity Period): 证书起始日期和时间以及终止日期和时间,指明证书在这两个时间内有效。

    1
    2
    3
    Validity
    Not Before: Nov 21 08:00:00 2020 GMT
    Not After : Nov 22 07:59:59 2021 GMT
  7. 主题信息(Subject):证书持有人唯一的标识符(Distinguished Name),这个名字在整个互联网上应该是唯一的,通常使用的是网站的域名。

    1
    Subject: C=CN, ST=GuangDong, L=Zhuhai, O=Awosome-Fenix, CN=*.icyfenix.cn
  8. 公钥信息(Public-Key): 包括证书持有人的公钥、算法(指明密钥属于哪种密码系统)的标识符和其他相关的密钥参数。

传输安全层

在计算机科学里,隔离复杂性的最有效手段(没有之一)就是分层,如果一层不够就再加一层

在传输层之上、应用层之下加入专门的安全层来实现安全通信.

TLS

image-20220103181308215

在传输性能上会有下降,但在功能上完全不会感知到有 TLS 的存在。建立在这层安全传输层之上的 HTTP 协议,就被称为“HTTP over SSL/TLS”,也即是大家所熟知的 HTTPS。

从上面握手协商的过程中我们还可以得知,HTTPS 并非不是只有“启用了 HTTPS”和“未启用 HTTPS”的差别,采用不同的协议版本、不同的密码学套件、证书是否有效、服务端/客户端对面对无效证书时的处理策略如何都导致了不同 HTTPS 站点的安全强度的不同,因此并不能说只要启用了 HTTPS 就必定能够安枕无忧。

校验

系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险?

Java 里验证的标准做法:

  • 对于无业务含义的格式验证,可以做到预置。(在Bean中预设)

  • 对于有业务含义的业务验证,可以做到重用,一个 Bean 被用于多个方法用作参数或返回值是很常见的,针对 Bean 做校验比针对方法做校验更有价值。利于集中管理,譬如统一认证的异常体系,统一做国际化、统一给客户端的返回格式等等。(使用自定义校验注解)

  • 避免对输入数据的防御污染到业务代码,如果你的代码里面如果很多下面这样的条件判断,就应该考虑重构了:

    1
    2
    3
    4
    // 一些已执行的逻辑
    if (someParam == null) {
    throw new RuntimeExcetpion("客官不可以!")
    }
  • 利于多个校验器统一执行,统一返回校验结果,避免用户踩地雷、挤牙膏式的试错体验。

实践时建议:

  • 自定义校验注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 表示一个用户的信息是无冲突的
    *
    * “无冲突”是指该用户的敏感信息与其他用户不重合,譬如将一个注册用户的邮箱,修改成与另外一个已存在的注册用户一致的值,这便是冲突
    **/
    @Documented
    @Retention(RUNTIME)
    @Target({FIELD, METHOD, PARAMETER, TYPE})
    @Constraint(validatedBy = AccountValidation.NotConflictAccountValidator.class)
    public @interface NotConflictAccount {
    String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    }
  • 将不带业务含义的格式校验注解放到 Bean 的类定义之上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Account extends BaseEntity {
    @NotEmpty(message = "用户不允许为空")
    private String username;

    @NotEmpty(message = "用户姓名不允许为空")
    private String name;

    private String avatar;

    @Pattern(regexp = "1\\d{10}", message = "手机号格式不正确")
    private String telephone;

    @Email(message = "邮箱格式不正确")
    private String email;
    }
  • 对于“需要触发一部分校验”的非典型情况,启用分组校验来处理,设计一套“新增”、“修改”、“删除”这样的标识类,置入到校验注解的groups参数中去实现。