知识分享--架构

架构

OOP

SOLID

SOLID是以下五个原则的简写

  1. Single Responsibility, SRP (单一职责原则)
  2. Open-Close, OCP (开闭原则)
  3. Liskov Substitution, LSP (里氏替换原则)
  4. Interface Segregation, ISP (接口隔离原则)
  5. Dependency Inversion, DIP (依赖倒置原则 )

再加一点:

组合优先于继承 Composition/Aggregation Principle (CRP)

DRY

不做重复的事(Don’t Repeat Yourself)

降低可管理单元复杂度的一个基本策略就是将他们拆解成更小的单元。

通常将其首字母缩写为DRY,出现在Andy Hunt和Dave Thomas所作的《The Pragmatic Programmer》一书中,但究其概念本身则由来已久。它指的是软件最小的部分。

当你在构建一个大的软件项目时,你将经常被无处不在的复杂事物所困扰。人们并不擅长管理复杂事物;他们更擅长于在特定范围内挖掘极具创造性的解决方案。降低可管理单元复杂度的一条最基本原则就是将系统切分成更为容易的单元。起初,你可能希望将你的系统切分成许多组件,每个组件都能体现它本身的子系统,而这个子系统包含可以实现特定功能的所有东西。

举例来讲,如果你在构建一个内容管理系统(CMS),负责人员管理的模块将会成为一个组件。这个组件可以继续被切分为诸如角色管理单元等等更多的子组件,并且它有可能会与其他的组件进行信息交互,比如安全组件。

就这样不断的将系统切分成组件,再进一步将组件切分为子组件,你终将会切分到一个层次,在这个层次上原本那些复杂单元被精简为一个个单一职责的单元。这些职责可以在一个类里面实现(假设我们在构建面向对象的应用)。类包含方法和属性,方法实现算法,算法及其子部分(取决于我们想要得到的意愿程度,不知道怎么翻译)计算或者囊括了构建业务逻辑的最小模块。

DRY 原则指出,这些小的业务模块在你整个系统中能且只能出现一次。在一个系统内,每个知识必须有单一的,明确的,权威的呈现。

在它内部必须有单一的呈现。

注意知识与其呈现的区别。如果我们正在CMS中实现数据库连接,那将会有个初始化数据库驱动的代码段,传递认证信息并将连接的引用保存到一个变量里。这个代码段就是知识的一部分,它是讲述某件事是如何完成的。而保存有连接引用的变量则是知识的体现,并且这是可以被别的模块使用的。如果数据库认证信息改变了,我们将不得不修改代码段,而不是它的呈现。

  • 一个完美的应用,每一个小的业务逻辑将他的知识封装在呈现里,并且声明为一个变量或者一个类属性。
  • 这个变量本身是被封装在一个类里,这个类可以被描述为一个职责的呈现。类又被封装在一个组件里,组件可以被描述为功能的呈现。
  • 除非我们达到软件项目的最顶层,即一堆日益复杂的呈现,否则我们无以继续这样的方案。这种看待软件复杂度的方式被称作模块化架构,而DRY便是它非常重要的一部分。

设计与架构模式

类型 模式 翻译 说明
类型 模式 翻译 说明
创建模式 Abstract Factory 抽象工厂 提供用于创建相关对象系列的接口
剖析模式 Timing Functions 时序功能 包装函数并记录执行
同步模式 Condition Variable 条件变量 为线程提供一种机制,以暂时放弃访问以等待某些条件
并行模式 N-Barrier N-二道闸 阻止进程继续进行,直到所有N个进程都到达屏障
架构模式 CQRS 命令查询职责分离
消息传递模式 Fan-In 扇入 该模块直接调用上级模块的个数,像漏斗型一样去工作
稳定模式 Bulkheads Bulkheads 实施故障遏制原则(即防止级联故障)
结构模式 Adapter 适配器 适配另一个不兼容的接口来一起工作
行为模式 Chain of Responsibility 职责链 避免通过提供超过对象处理请求的机会来将发送方与接收方耦合
Builder 生成器 使用简单对象构建复杂对象
Factory Method 工厂方法 将对象的实例化延迟到用于创建实例的专用函数
Object Pool 对象池 实例化并维护一组相同类型的对象实例
Singleton 单例 将类型的实例化限制为一个对象
Bridge 桥接 将接口与其实现分离,以便两者可以独立变化
Composite 组合 封装并提供对许多不同对象的访问
Decorator 装饰 静态或动态地向对象添加行为
Facade 外观 使用一种类型作为许多其他类型的API
Flyweight 享元 运用共享技术有效地支持大量细粒度的对象
Proxy 代理 为对象提供代理以控制其操作
Command 命令 捆绑命令和参数以便稍后调用
Mediator 中介者 连接对象并充当代理
Memento 备忘录 生成可用于返回先前状态的不透明令牌
Observer 观察者 提供回调以通知事件/数据更改
Registry 注册 跟踪给定类的所有子类
State 状态 根据内部状态封装同一对象的不同行为
Strategy 策略 允许在运行时选择算法的行为
Template 模板 定义一个将某些方法推迟到子类的框架类
Visitor 访问者 将算法与其运行的对象分开
Lock/Mutex 锁定/互斥 对资源实施互斥限制以获得独占访问权限
Monitor 监视器 互斥和条件变量模式的组合
Read-Write Lock 读写锁定 允许并行读取访问,但仅对资源的写入操作进行独占访问
Semaphore 信号 允许控制对公共资源的访问
Bounded Parallelism 有界并行 完成大量资源限制的独立任务
Broadcast 广播 把一个消息同时传输到所有接收端
Coroutines 协同程序 允许在特定地方暂停和继续执行的子程序
Generators 生成器 一次性生成一系列值
Reactor 反应 服务处理程序使用I/O多路复用策略来同步、有序的处理一个或多个客户端并发请求
Parallelism 并行 完成大量独立任务
Producer Consumer 生产者消费者 从任务执行中分离任务
Scheduler 调度器 协调任务步骤
Fan-Out 扇出 该模块直接调用的下级模块的个数
Futures & Promises Futures & Promises 扮演一个占位角色,对未知的结果用于同步
Publish/Subscribe Publish/Subscribe 将信息传递给订阅者
Push & Pull Push & Pull 把一个管道上的消息分发给多人
Circuit-Breaker 断路器 当请求有可能失败时,停止流动的请求
Deadline 截止日期 一旦响应变缓,允许客户端停止一个正在等待的响应
Fail-Fast机制 快速失败 检查请求开始时所需资源的可用性,如果不满足要求则失败
Handshaking 握手 询问组件是否可以承受更多负载,如果不能,则请求被拒绝
Steady-State 稳定状态 为每一个服务积累一个资源,其它服务必须回收这些资源
MVC 模型-视图-控制器
MVP 模型-视图-主持人
MVVM 模型-视图-视图模型
Tiers 分层架构
Actor
SOA 面向服务
Master-Slave 主从
Event-Bus 事件总线
Pipeline / Filter 流水线、过滤器 Servlet和filter, inceptor就是类似的模式
Gateway

通信

DNS

最开始确实就是直接使用 IP 地址来访问远程主机的。早期联网的每台计算机都是采用主机文件(即我们俗称的 hosts 文件)来进行地址配置和解析的,后来联网机器越来越多,主机文件的更新和同步就成了很大的问题。于是,1983 年保罗·莫卡派乔斯发明了域名解析服务和域名系统,在 1985 年 1 月 1 日,世界上第一个域名 nordu.net 才被注册成功。

域名比 IP 地址更容易记忆,本质上只是为数字化的互联网资源提供了易于记忆的别名,就像在北京提起「故宫博物院」就都知道指的是「东城区景山前街 4 号」的那个大院子一样。如果把 IP 地址看成电话号码,那域名系统就是通讯录。我们在通讯录里保存了朋友和家人的信息,每次通过名字找到某人打电话的时候,通讯录就会查出与之关联的电话号码,然后拨号过去。我们可能记不下多少完整的电话号码,但是联系人的名字却是一定记得的。

域名解析是怎么完成的

DNS 解析的过程是什么样子的呢?在开始这个问题之前,我们先看一看域名的层次结构。

域名的层级结构

在讨论域名的时候,我们经常听到有人说「顶级域名」、「一级域名」、「二级域名」等概念,域名级别究竟是怎么划分的呢?

  • 根域名。还是以百度为例,通过一些域名解析工具,我们可以看到百度官网域名显示为 [www.baidu.com](http://www.baidu.com/).,细心的人会注意到,这里最后有一个 .,这不是 bug,而是所有域名的尾部都有一个根域名。[www.baidu.com](http://www.baidu.com/) 真正的域名是 [www.baidu.com](http://www.baidu.com/).root,简写为[www.baidu.com](http://www.baidu.com/).,又因为根域名 .root 对于所有域名都是一样的,所以平时是省略的,最终就变成了我们常见的样子。

  • 根域名的下一级叫做顶级域名(top-level domain,缩写为 TLD),也叫做一级域名,常见的如 .com / .net / .org / .cn 等等,他们就是顶级域名。

  • 再下一级叫做二级域名(second-level domain,缩写为 SLD),比如 baidu.com。这是我们能够购买和注册的最高级域名。

  • 次级域名之下,就是主机名(host),也可以称为三级域名,比如 www.baidu.com,由此往下,基本上 N 级域名就是在 N-1 级域名前追加一级。

总结一下,常见的域名层级结构如下:

主机名.次级域名.顶级域名.根域名
# ie
www.baidu.com.root

一般来说我们购买一个域名就是购买一个二级域名(SLD)的管理权(如 leancloud.cn),有了这个管理权我们就可以随意设置三级、四级域名了。

域名解析的过程

与域名的分级结构对应,DNS 系统也是一个树状结构,不同级别的域名由不同的域名服务器来解析,整个过程是一个「层级式」的。

层级式域名解析体系的第一层就是根域名服务器,全世界 IPv4 根域名服务器只有 13 台(名字分别为 A 至 M),1 个为主根服务器在美国,其余 12 个均为辅根服务器,它们负责管理世界各国的域名信息。在根服务器下面是顶级域名服务器,即相关国家域名管理机构的数据库,如中国互联网络信息中心(CNNIC)。然后是再下一级的权威域名服务器和 ISP 的缓存服务器。

一个域名必须首先经过根数据库的解析后,才能转到顶级域名服务器进行解析,这一点与生活中问路的情形有几分相似。

分级查询的实例

现在我们来看一个实际的例子。如果我们在浏览器中输入 [https://news.qq.com](https://news.qq.com/),那浏览器会从接收到的 URL 中抽取出域名字段(news.qq.com),然后将它传给 DNS 客户端(操作系统提供)来解析。

首先我们说明一下本机 DNS 配置(就是 /etc/resolv.conf 文件,里面指定了本地 DNS 服务器的地址,Windows 系统可能会有所不同):

$ cat /etc/resolv.conf
nameserver 202.106.0.20
nameserver 202.106.196.115

然后我们用 dig 这个工具查看一下 news.qq.com 的解析结果(其中中文部分是解释说明):

$ dig news.qq.com

; <<>> DiG 9.10.6 <<>> news.qq.com
这是 dig 程序的版本号与要查询的域名

;; global options: +cmd
;; Got answer:
以下是要获取的内容。

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47559
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
这个是返回应答的头部信息:
1. opcode:操作码,QUERY 代表查询操作;
2. status: 状态,NOERROR 代表没有错误;
3. id:编号,在 DNS 协议中通过编号匹配返回和查询;
4. flags: 标志,含义如下:
- qr:query,查询标志,代表是查询操作
- rd:recursion desired,代表希望进行递归查询操作;
- ra:recursive available,代表查询的服务器支持递归查询操作;
5. QUERY 查询数,与下面 QUESTION SECTION 的记录数一一对应;
6. ANSWER 结果数,与下面的 ANSWER SECTION 的记录数一一对应;
7. AUTHORITY 权威回复数,如果查询结果由管理域名的域名服务器而不是缓存服务器提供的,则称为权威回复。
0 表示所有结果都不是权威回复;
8. ADDITIONAL 额外记录数;

;; QUESTION SECTION:
;news.qq.com.            IN    A
查询部分,从左到右部分意义如下:
1、要查询的域名;
2、要查询信息的类别,IN 代表类别为 IP 协议,即 Internet。
3、查询的记录类型,A 记录(Address)代表要查询 IPv4 地址。

;; ANSWER SECTION:
news.qq.com.        136    IN    CNAME    https.qq.com.
https.qq.com.        476    IN    A    125.39.52.26
回应部分,从左到右各部分意义:
1、对应的域名
2TTL,time to live,缓存时间,单位秒,代表缓存域名服务器可以在缓存中保存的期限。
3、查询信息的类别
4、查询的记录类型,CNAME 表示别名记录,A 记录(Address)代表 IPv4 地址。
5、域名对应的 ip 地址。

;; Query time: 56 msec
;; SERVER: 202.106.0.20#53(202.106.0.20)
查询使用的服务器地址和端口,其实就是本地 DNS 域名服务器
;; WHEN: Thu Jul 11 15:59:37 CST 2019
;; MSG SIZE  rcvd: 65
查询的时间与回应的大小,收到 65 字节的应答数据。

从这个应答可以看到,我们得到的结果不是权威回复,只是本地 DNS 服务器从缓存中给了应答。

接下来我们在 dig 命令中增加一个参数 +trace,看看完整的分级查询过程:

$ dig +trace news.qq.com

; <<>> DiG 9.10.6 <<>> +trace news.qq.com
;; global options: +cmd
.            432944    IN    NS    g.root-servers.net.
.            432944    IN    NS    k.root-servers.net.
.            432944    IN    NS    b.root-servers.net.
.            432944    IN    NS    h.root-servers.net.
.            432944    IN    NS    i.root-servers.net.
.            432944    IN    NS    f.root-servers.net.
.            432944    IN    NS    d.root-servers.net.
.            432944    IN    NS    e.root-servers.net.
.            432944    IN    NS    j.root-servers.net.
.            432944    IN    NS    l.root-servers.net.
.            432944    IN    NS    c.root-servers.net.
.            432944    IN    NS    m.root-servers.net.
.            432944    IN    NS    a.root-servers.net.
;; Received 228 bytes from 202.106.0.20#53(202.106.0.20) in 45 ms
这些就是神秘的根域名服务器,由本地 DNS 服务器返回了所有根域名服务器地址。

com.            172800    IN    NS    g.gtld-servers.net.
com.            172800    IN    NS    a.gtld-servers.net.
com.            172800    IN    NS    b.gtld-servers.net.
com.            172800    IN    NS    m.gtld-servers.net.
com.            172800    IN    NS    d.gtld-servers.net.
com.            172800    IN    NS    c.gtld-servers.net.
com.            172800    IN    NS    j.gtld-servers.net.
com.            172800    IN    NS    h.gtld-servers.net.
com.            172800    IN    NS    f.gtld-servers.net.
com.            172800    IN    NS    l.gtld-servers.net.
com.            172800    IN    NS    e.gtld-servers.net.
com.            172800    IN    NS    k.gtld-servers.net.
com.            172800    IN    NS    i.gtld-servers.net.
;; Received 1171 bytes from 192.36.148.17#53(i.root-servers.net) in 57 ms
这里显示的是 .com 域名的 13NS 记录,本地 DNS 服务器向这些顶级域名服务器发出查询请求,
询问 qq.com 的 NS 记录。

qq.com.            172800    IN    NS    ns1.qq.com.
qq.com.            172800    IN    NS    ns2.qq.com.
qq.com.            172800    IN    NS    ns3.qq.com.
qq.com.            172800    IN    NS    ns4.qq.com.
;; Received 805 bytes from 192.48.79.30#53(j.gtld-servers.net) in 331 ms
这里显示的是 qq.com 的 4NS 记录,由 j.gtld-servers.net 这台服务器最先返回。
然后本地 DNS 服务器向这四台服务器查询下一级域名 news.qq.com 的 NS 记录。

news.qq.com.        86400    IN    NS    ns-cnc1.qq.com.
news.qq.com.        86400    IN    NS    ns-cnc2.qq.com.
;; Received 180 bytes from 58.144.154.100#53(ns4.qq.com) in 37 ms
这里显示的是 news.qq.com 的 NS 记录,它们是由上面的 ns4.qq.com 域名服务器返回的。
然后本地 DNS 服务器向这两台机器查询 news.qq.com 的主机名。

news.qq.com.        600    IN    CNAME    https.qq.com.
https.qq.com.        600    IN    A    125.39.52.26
;; Received 76 bytes from 223.167.83.104#53(ns-cnc2.qq.com) in 29 ms
这是上面的 ns-cnc2.qq.com 返回的最终查询结果:
news.qq.com 是 https.qq.com 的别名,而 https.qq.com 的 A 记录地址是 125.39.52.26

实际的流程里面,本地 DNS 服务器相当于门卫大爷,根域名服务器相当于局长同志,其余以此类推。客户端与本地 DNS 服务器之间的查询叫递归查询,本地 DNS 服务器与其他域名服务器之间的查询就叫迭代查询。

域名记录的类型

域名服务器之所以能知道域名与 IP 地址的映射信息,是因为我们在域名服务商那里提交了域名记录。购买了一个域名之后,我们需要在域名服务商那里设置域名解析的记录,域名服务商把这些记录推送到权威域名服务器,这样我们的域名才能正式生效。

在设置域名记录的时候,会遇到「A 记录」、「CNAME」 等不同类型,这正是前面做域名解析的时候我们碰到的结果。这些类型是什么意思,它们之间有什么区别呢?接下来我们看看常见的记录类型。

  • A 记录。A (Address) 记录用来直接指定主机名(或域名)对应的 IP 地址。主机名就是域名前缀,常见有如下几种:

www:解析后的域名为 [www.yourdomain.com](http://www.yourdomain.com/),一般用于网站地址。

@:直接解析主域名。

*:泛解析,指将 *.yourdomain.com 解析到同一 IP。

  • CNAME 记录。CNAME 的全称是 Canonical Name,通常称别名记录。如果需要将域名指向另一个域名,再由另一个域名提供 IP 地址,就需要添加 CNAME 记录。

  • MX 记录。邮件交换记录,用于将以该域名为结尾的电子邮件指向对应的邮件服务器以进行处理。

  • NS 记录。域名服务器记录,如果需要把子域名交给其他 DNS 服务器解析,就需要添加 NS 记录。

  • AAAA 记录。用来指定主机名(或域名)对应的 IPv6 地址,不常用。

  • TXT 记录。可以填写任何东西,长度限制 255。绝大多数的 TXT 记录是用来做 SPF 记录(反垃圾邮件),MX 记录的作用是给寄信者指明某个域名的邮件服务器有哪些。SPF 的作用跟 MX 相反,它向收信者表明,哪些邮件服务器是经过某个域名认可会发送邮件的。

  • 显性 URL。从一个地址 301 重定向(也叫「永久性转移」)到另一个地址的时候,就需要添加显性 URL 记录。

  • 隐性 URL。从一个地址 302 跳转(也叫「临时跳转」)到另一个地址,需要添加隐性 URL 记录。它类似于显性 URL,区别在于隐性 URL 不会改变地址栏中的域名。

在填写各种记录的时候,我们还会碰到一个特殊的设置项——TTL,生存时间(Time To Live)。

TTL 表示解析记录在 DNS 服务器中的缓存时间,时间长度单位是秒,一般为3600秒。比如:在访问 [news.qq.com](http://news.qq.com/)时,如果在 DNS 服务器的缓存中没有该记录,就会向某个 NS 服务器发出请求,获得该记录后,该记录会在 DNS 服务器上保存 TTL 的时间长度,在 TTL 有效期内访问 [news.qq.com](http://news.qq.com/),DNS 服务器会直接缓存中返回刚才的记录。

DNS 智能解析

DNS 主要的工作就是完成域名到 IP 的映射,但是也不是简单到查查字典就可以搞定的程度。在设置 DNS 解析的时候,我们还有一些额外的需求,例如:

  • 将一个域名解析到多个 IP

例如我们一个网站有多台前端机,希望用户访问的时候,可以随机分散到这些机器上,以增加网站承载能力。有一种解决的办法就是对同一个域名设置多条 A 记录,分别指定到不同的 IP 上。

  • 根据特征差异将不同请求解析到不同 IP(智能解析)

国内互联网的架构其实远比我们想象的复杂,基本上还是根据运营商的不同切割成多个平行网络,只有在固定的几个节点这些平行网络才会有交叉。例如电信和联通之间的互联是通过「国家级互联网骨干直联点」接入的,目前我们一共建设了三批国家级互联网骨干直联点:

1.第一批 2001 年投入使用:北京,上海,广州

2.第二批 2014 年投入使用:成都,郑州,武汉,西安,沈阳,南京,重庆

3.第三批 2017 年投入使用:杭州,贵阳/贵安,福州

教育网目前还只能通过北上广三个点进行连接。这样的网络拓扑结构,给 DNS 解析带来了新的挑战。

传统 DNS 解析,不判断访问者来源,会随机选择其中一个 IP 地址返回给访问者。如果让电信用户使用了联通 IP 来访问网站,那结果自然不如使用电信 IP 访问来的快捷。而智能 DNS 解析,会判断访问者的来源特征,为不同的访问者返回不同的 IP 地址,能够减少解析时延,并提升网络访问速度。例如,国内某著名 DNS 服务商不光可以区分网络运营商,还可以根据访问者的地理位置来设置不同的解析线路,而且甚至还可以为搜索引擎设置特定的解析地址。

  • CNAME 和 A 记录区别

按照前面的解释,A 记录就是把一个域名解析到一个 IP 地址,而 CNAME 记录就是把一个域名解析到另外一个域名,其功能差不多。但是 CNAME 相当于将域名和 IP 地址之间加了一个中间层,可以带来很大的灵活性,特别是当你要使用但是并不拥有那些域名的时候。

例如我们使用 CDN 服务,服务商提供给我们的是一个 CNAME 地址,我们可以把自己的域名绑定到这一个地址上,这样万一以后服务商的 IP 地址更换了,我们自己的域名解析是不需要做任何变更的,只要服务商调整一下 CNAME 地址的解析结果,所有使用者都可以无感知的切换。

从 6 月底开始,LeanCloud 新推出了 绑定自定义域名 的功能,全面支持开发者设置自己的 API、文件、云引擎域名,也正是依赖于 CNAME 记录的这一特点来实现的。

DNS 污染与安全挑战

DNS 是最早商用的大型分布式系统,虽然现在看起来已经很完备了,但是实际使用的时候,特别是国内复杂的网络环境,我们还是会遇到很多问题。

作为互联网早期产物,DNS 使用无连接的 UDP 协议虽然降低了开销也保证了高效的通信,但是没有太考虑安全问题。由于它使用目的端口为 53 的 UDP 明文进行通信,DNS 解析器识别是自己发出的数据包的唯一标准就是随机的源端口号,如果端口号匹配则认为是正确回复,而不会验证来源。所以也带来了诸如 DNS 欺骗、DNS Cache 污染、DNS 放大攻击等问题,同时给一些区域运营商带来了「商机」。

为此业界提出了 DNSSec(Domain Name System Security Extensions,也叫「DNS安全扩展」)机制,使用密码学方法,让客户端对域名来源身份进行验证,并且检查来自 DNS 域名服务器应答记录的完整性,以及验证是否在传输过程中被篡改过,等等一系列措施来保证数据通信的安全性。

HTTP

1. HTTP/0.9

1990年提出的,是最早期的版本,只有一个命令GET。

2. HTTP/1.0

1996年5月提出的。

  • 缺点:每个TCP连接只能发送一个请求。
  • 解决方法:Connection:keep-alive

3. HTTP/1.1

1997年1月提出,现在使用最广泛的。

3.1 特性
  1. 长连接:TCP连接默认不关闭,可以被多个请求复用。对于同一个域名,大多数浏览器允许同时建立6个持久连接。默认开启Connection:keep-alive。
  2. 管道机制:在同一个TCP连接里,可以同时发送多个请求。但是服务器还是要按照请求的顺序进行响应,会造成“队头阻塞”。
3.2 HTTP首部

HTTP首部分为请求报文和响应报文。它们的格式如下所示:

  • 请求报文:

知识分享

  • 响应报文:

知识分享

其中首部字段又分为很多种,我们先看通用首部字段,这是请求报文和响应报文种都会使用的首部。

3.2.1 通用首部字段

1、Cache-Control:操作缓存的工作机制

参数:

  • public:明确表明其他用户也可以利用缓存
  • private:缓存只给特定的用户
  • no-cache:客户端发送这个指令,表示客户端不接收缓存过的响应,必须到服务器取;服务器返回这个指令,指缓存服务器不能对资源进行缓存。其实是不缓存过期资源,要向服务器进行有效期确认后再处理资源。
  • no-store:指不进行缓存
  • max-age:缓存的有效时间(相对时间)

2、Connection

  • Connection:keep-Alive (持久连接)
  • Connection:不再转发的首部字段名

3、Date:表明创建http报文的日期和时间

4、Pragma:兼容http1.0,与Cache-Control:no-cache含义一样。但只用在客户端发送的请求中,告诉所有的中间服务器不返回缓存。形式唯一:Pragma:no-cache

5、Trailer:会事先说明在报文主体后记录了哪些首部字段,该首部字段可以应用在http1.1版本分块传输编码中。

6、Transfer-Encoding:chunked (分块传输编码),
规定传输报文主体时采用的编码方式,http1.1的传输编码方式只对分块传输编码有效

7、Upgrade:升级一个成其他的协议,需要额外指定Connection:Upgrade。服务器可用101状态码作为相应返回。

8、Via:追踪客户端和服务器之间的请求和响应报文的传输路径。可以避免请求回环发生,所以在经过代理时必须要附加这个字段。

3.2.2 请求首部字段

1、Accept:通知服务器,用户代理能够处理的媒体类型及媒体类型的相对优先级
q表示优先级的权重值,默认为q = 1.0,范围是0~1(可精确到小数点后3位,1为最大值)
当服务器提供多种内容时,会先返回权重值最高的媒体类型

2、Accept-Charset:支持的字符集及字符集的相对优先顺序,跟Accept一样,用q来表示相对优先级。这个字段应用于内容协商机制的服务器驱动协商。

3、Accept-Encoding:支持的内容编码及内容编码的优先级顺序,q表示相对优先级。
内容编码:gzip、compress、deflate、identity(不执行压缩或者不会变化的默认编码格式)。
可以使用*作为通配符,指定任意的编码格式。

4、Accept-Language:能够处理的自然语言集,以及相对优先级。

3.3 状态码

101 协议升级,主要用于升级到websocket,也可以用于http2

200 OK

204 No content,服务器成功处理请求,但是返回的响应报文中不含实体的主体部分

206 Partial Content,表示客户端像服务器进行了范围请求(Content-Range字段),服务器成功返回指定范围的实体内容

301 永久性重定向,表示请求的资源已经被分配了新的url,旧地址以后都不能再访问了,服务器会返回location字段,包含的是新的地址。

302 临时性重定向,表示请求的资源临时移动到一个新地址

注意:尽量使用301跳转,因为302会造成网址劫持,可能被搜索引擎判为可疑转向,甚至认为是作弊。

原因:从网站A(网站比较烂)上做了一个302跳转到网站B(搜索排名很靠前),这时候有时搜索引擎会使用网站B的内容,但却收录了网站A的地址,这样在不知不觉间,网站B在为网站A作贡献,网站A的排名就靠前了。

303 See Other,与302功能相同,但是它明确规定客户端应采用GET方法获取资源

304 未修改,协商缓存中返回的状态码

307 临时重定向,与302功能相同,但规定不能从POST变成GET

当301、302、303响应状态码返回时,几乎所有浏览器都会把post改成get,并删除请求报文内的主体,之后请求会自动再次发送。然而301、302标准是禁止将post方法改变成get方法的,但实际使用时大家都会这么做。所以需要307。

400 Bad Request,表示请求报文中存在语法错误。当错误发生时,需要修改请求的内容再次发送请求

401 unauthorized,表示发送的请求需要有通过HTTP认证(BASIC认证、DIGEST认证)的认证信息。如果之前已经进行过一次请求,表示用户认证失败。

403 禁止,表示拒绝对请求资源的访问

404 Not Found,表明服务器上无法找到请求的资源

500 Internet Server Error,该状态码表示服务器在执行请求时发生了错误

500 Service Unavailable,表示服务器暂时处于超负荷或者处于停机维护状态,现在无法处理请求

4. SPDY协议 、HTTP / 2的原型

2009年谷歌提出。

  • SPDY结构

知识分享

  • 新增特性

(1)多路复用:通过一个TCP连接,可以无限制处理多个HTTP请求。

(2)赋予请求优先级:给请求逐个分配优先级顺序。可以解决在发送多个请求时,因带宽低而导致响应变慢的问题。

(3)压缩HTTP首部:压缩方式:DELEFT

(4)推送功能

(5)服务器提示功能:服务器可以主动提示客户端请求所需的资源。

  • 缺点

SPDY强制使用https。而且SPDY基本上只是将单个域名下的通信多路复用,所以当一个web网站上使用多个域名下的资源时,改善效果就会受到限制。

5. WebSocket

html5新提出来的,是web浏览器与web服务器之间的全双工通信标准。主要是为了解决ajax和comet里的xmlhttprequest附带的缺陷所引起的问题。

5.1 特性

(1)推送功能:服务器可直接发送数据,不需要等待客户端的请求;

(2)基于TCP传输协议,并复用HTTP的握手通道;

(3)支持双向通信,用于实时传输消息;

(4)更好的二进制支持;

(5)更灵活,更高效。

5.2 建立连接过程

1、客户端:发起协议升级请求

GET / HTTP/1.1      `采用HTTP报文格式,只支持get请求`
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade   `表示要升级协议`
Upgrade: websocket    `表示升级到websocket协议`
Sec-WebSocket-Version: 13    `表示websocket 的版本`
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==   `是一个 Base64 encode 的值,是浏览器随机生成的`
Sec-WebSocket-Protocol:chat, superchat  `用来指定一个特定的子协议,一旦这个字段有设置,那么服务器需要在建立连接的响应头中包含同样的字段,内容就是选择的子协议之一。`

2、服务端:响应协议升级

HTTP/1.1 101 Switching Protocols    `101表示协议切换==`
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU= `经过服务器确认,并且加密过后的 Sec-WebSocket-Key`
Sec-WebSocket-Protocol:chat  `表示最终使用的协议`

Sec-WebSocket-Key 的加密过程为:

  1. 将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
  2. 通过SHA1计算出摘要,并转成base64字符串。

3、双方握手成功后,就是全双工的通信了,接下来就是用websocket协议来进行通信了。

5.3 Ajax 轮询、长轮询、WebSocket原理解析

1、ajax轮询

让浏览器每隔一定的时间就发送一次请求,询问服务器是否有新信息。

2、长轮询(Long Poll)

采用的阻塞模式。客户端发起连接后,如果没消息,服务器不会马上告诉你没消息,而是将这个请求挂起(pending),直到有消息才返回。返回完成或者客户端主动断开后,客户端再次建立连接,周而复始。Comet就是采用的长轮询。

3、websocket

WebSocket 是类似 Socket 的TCP长连接通讯模式。一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。而且浏览器和服务器就可以随时主动发送消息给对方,是全双工通信。

优点:在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

6. HTTP2

2015年发布,它是基于SPDY的,以下是它的一些新特性:

1、 二进制分帧

http/1.x 是一个超文本协议,而 http2 是一个二进制协议,被称之为二进制分帧。
二进制格式在协议的解析和优化扩展上带来更多的优势和可能。

协议格式为帧,帧由 Frame Header(头信息帧)和 Frame Payload(数据帧)组成,如下所示:

知识分享

  • Length 字段用来表示 Frame Payload 数据大小。
  • Type 字段用来表示该帧中的 Frame Payload 保存的是 header 数据还是 body 数据。除了用于标识 header/body,还有一些额外的 Frame Type。
  • Stream Identifier 用来标识该 frame 属于哪个 stream。
  • Frame Payload 用来保存 header 或者 body 的数据。

2、头部压缩 HPACK

请求和响应首部压缩,客户端和服务端共同维护一张头信息表,所有字段存入这个表,生成一个索引号,通过发送索引号提高速度。HPACK压缩会经过两步:

  • 传输的value,会经过一遍Huffman coding来节省资源;
  • 为了server和client同步, 两边都需要保留一份Header list, 并且,每次发送请求时,都会检查更新。

3、服务端推送

服务端主动向客户端推送数据。如果客户端请求一个html文件,服务端把html文件返回给客户端之后,还会相应的把html文件中的js、css、图片推送给客户端。

4、多路复用

只需要建立一个TCP连接,浏览器和服务器可以同时发送多个请求或者回应,而且不需要按照顺序一一对应,避免了“队头阻塞”。

5、数据流

当客户端同时向服务端发起多个请求,那么这些请求会被分解成一一个的帧,每个帧都会在一个 TCP 链路中无序的传输,同一个请求的帧的 Stream Identifier 都是一样的。当帧到达服务端之后,就可以根据 Stream Identifier 来重新组合得到完整的请求。

并且规定:客户端发出的数据流ID为奇数,服务器发出的ID为偶数。Stream Identifier (数据流ID)就是用来标识该帧属于哪个请求的。

7. HTTPS

HTTPS = HTTP+加密+认证+完整性保护

它的加密过程是:

  1. server生成一个公钥和私钥,把公钥发送给第三方认证机构(CA);
  2. CA把公钥进行MD5加密,生成数字签名;再把数字签名用CA的私钥进行加密,生成数字证书。CA会把这个数字证书返回给server;
  3. server拿到数字证书之后,就把它传送给浏览器;
  4. 浏览器会对数字证书进行验证,首先,浏览器本身会内置CA的公钥,会用这个公钥对数字证书解密,验证是否是受信任的CA生成的数字证书;
  5. 验证成功后,浏览器会随机生成对称秘钥,用server的公钥加密这个对称秘钥,再把加密的对称秘钥传送给server;
  6. server收到对称秘钥,会用自己的私钥进行解密,之后,它们之间的通信就用这个对称秘钥进行加密,来维持通信。

下图是加密过程的图解,可以对照着图片理一遍。

知识分享

8. HTTP缓存机制

8.1 缓存分类

HTTP的缓存分为强缓存和协商缓存(对比缓存)。

  1. 强制缓存

在缓存数据未失效的情况下,可以直接使用缓存数据;在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应header中。

  • Expires:缓存过期时间(HTTP1.0)

缺点:生成的是绝对时间,但是客户端时间可以随意修改,会导致误差。

  • Cache-Control :HTTP1.1,优先级高于Expires

可设置参数:

private: 客户端可以缓存

public: 客户端和代理服务器都可缓存

max-age=xxx: 缓存的内容将在 xxx 秒后失效

no-cache: 需要使用协商缓存来验证缓存数据(后面介绍)

no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发

Expires和Cache-Control决定了浏览器是否要发送请求到服务器,ETag和Last-Modified决定了服务器是要返回304+空内容还是新的资源文件。

  1. 协商缓存

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

  • Last-Modified / If-Modified-Since

Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间。
If-Modified-Since:再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。

缺点:Last-Modified 标注的最后修改时间只能精确到秒,如果有些资源在一秒之内被多次修改的话,他就不能准确标注文件的新鲜度了。如果某些资源会被定期生成,当内容没有变化,但 Last-Modified 却改变了,导致文件没使用缓存有可能存在服务器没有准确获取资源修改时间,或者与代理服务器时间不一致的情形。

  • Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since)

Etag:给资源计算得出的一个唯一标志符。
If-None-Match:再次请求服务器时,通过此字段通知服务器客户端缓存数据的唯一标识。

8.2 缓存判断顺序
  1. 先判断Cache-Control,在Cache-Control的max-age之内,直接返回200 from cache;
  2. 没有Cache-Control再判断Expires,再Expires之内,直接返回200 from cache;
  3. Cache-Control=no-cache或者不符合Expires,浏览器向服务器发送请求;
  4. 服务器同时判断ETag和Last-Modified,都一致,返回304,有任何一个不一致,返回200。

具体过程如下图:

知识分享

8.3 cookie、session
8.3.1 cookie

解决http的无状态问题,是客户端保存用户信息的一种机制,用来记录用户的一些信息,来实现session的跟踪。

  1. cookie属性

name、value :以key/value的形式存在

comment:说明该cookie的用处

domain:可以访问该cookie的域名

Expires/maxAge:cookie失效时间。负数:临时cookie,关闭浏览器就失效;0:表示删除cookie,默认为-1

path:可以访问此cookie的页面路径

size:cookie的大小

secure:是否以https协议传输

version:该cookie使用的版本号,0遵循Netscape规范,大多数用这种,1遵循W3C规范

HttpOnly:此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie,能防止XSS攻击。

  1. cookie机制原理:

客户端请求服务器时,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。而客户端浏览器会把Cookie保存起来。当浏览器再请求服务器时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器通过检查该Cookie来获取用户状态。

  1. cookie同源和跨域:

cookie的同源是域名相同,忽略协议和端口,不可跨域。

8.3.2 session

session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中。

  1. session的运行依赖session id,而session id是存在cookie中的
  2. session机制原理:

当客户端请求创建一个session的时候,服务器会先检查这个客户端的请求里是否已包含了一个session标识——sessionId。如果已包含这个sessionId,则说明以前已经为此客户端创建过session,服务器就按照sessionId把这个session检索出来使用(如果检索不到,可能会新建一个。如果客户端请求不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关联的sessionId。

  1. 如果禁用cookie怎么办?

使用URL重写技术来进行会话跟踪。在 url 中传递 session id,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。

8.3.3 区别
  1. cookie和session的区别
  • cookie 数据存放在客户的浏览器上,session数据放在服务器上;
  • cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗考虑到安全应当使用 session;
  • session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用 cookie;
  • 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个 cookie。

鉴于上述区别我们建议:

(1)将登陆信息等重要信息存放为 session;

(2)其他信息如果需要保留,可以放在 cookie 中。

  1. localStorage,sessionStorage和cookie的区别

共同点:都是保存在浏览器端、且同源的。

  • 数据存储方面

  • cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下。

  • sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。

  • 存储数据大小

  • 存储大小限制也不同,cookie数据不能超过4K,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识。

  • sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。

  • 数据存储有效期

  • sessionStorage:仅在当前浏览器窗口关闭之前有效;

  • localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;

  • cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭。

  • 作用域不同

  • sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;

  • localstorage在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在。

  • cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在。

9. 跨域

跨域产生的原因,是因为受到同源策略的限制。同源策略指的是协议、域名、端口不相同。这里我将介绍三种跨域的方式:JSONP、CORS(跨域资源共享)、document.domain + iframe。

9.1 JSONP

1. 原理

动态插入script标签(因为script标签不受同源策略的限制),通过插入script标签引入一个js文件,这个js文件加载成功之后会执行我们在url中指定的回调函数,并且会把我们需要的json数据作为参数传入。

2. 实现

(1)原生实现:

var script = document.createElement('script');
script.type = 'text/javascript';

// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);

// 回调函数
function onBack(res) {
alert(JSON.stringify(res));
}

//服务端返回如下(返回时即执行全局函数):
onBack({"status": true, "user": "admin"})

(2)jquery ajax:

$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp',  // 请求方式为jsonp
jsonpCallback: "onBack",    // 自定义回调函数名
data: {}
});
9.2 CORS

1. 原理

服务器在响应头中设置相应的选项,浏览器如果支持这种方法的话就会将这种跨站资源请求视为合法,进而获取资源。

2. 实现

CORS分为简单请求和复杂请求,简单请求指的是:

(1)请求方法是以下三种方法之一:HEAD、GET、POST;

(2)HTTP的头信息不超出以下几种字段:
Accept、Accept-Language、Content-Language、Last-Event-ID、
Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)。

其他情况就是非简单请求了。

  • 简单请求

(1)请求头

Origin: http://www.domain.com

(2)响应头

Access-Control-Allow-Origin: http://www.domain.com
Access-Control-Allow-Credentials: true   `是否允许传送cookie`
Access-Control-Expose-Headers: FooBar `CORS请求时,只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须指定。`

(3)另外,ajax请求中,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名,还要设置以下内容:

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
  • 非简单请求

(1)预检请求:

OPTIONS /cors HTTP/1.1  `OPTIONS请求是用来询问的`
Origin: http://www.domian.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

(2)响应头

Access-Control-Allow-Origin: http://www.domain.com
Access-Control-Allow-Methods: GET, POST, PUT  `服务器支持的所有跨域请求的方法`
Access-Control-Allow-Headers: X-Custom-Header  `服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。`
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000  `指定本次预检请求的有效期,单位为秒`

(3)之后的步骤就同简单请求了

这是CORS的整个流程图:

与JSOP的比较

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
9.3 document.domain + iframe

此方案仅限主域相同,子域不同的跨域应用场景。

1.原理

两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

2.实现

(1)父窗口:(www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>

<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

(2)子窗口:(child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

10. 安全

10.1 HTTP Security Headers
Content-Security-Policy

CSP 通过指定允许加载哪些资源来防止跨站点脚本。在此列表的所有项目中,这可能是创建和维护最耗时的,也是最容易出现风险的。在开发 CSP 期间,请务必仔细测试它 —— 以有效的方式阻止您的站点使用的内容源将会破坏站点的功能。

一个创建初稿的好工具是 Mozilla laboratory CSP 浏览器扩展。在浏览器中安装这个,彻底浏览要为其创建 CSP 的站点,然后在您的站点上使用生成的 CSP。理想情况下,还可以重构 JavaScript,因此不会保留内联脚本,因此您可以删除“unsafe inline”指令。

CSP 是复杂而令人困惑的,所以如果你想要更深入的研究,请参阅官方网站

一个好的 CSP 的开始可能是这样的(这可能需要在一个真实的站点上进行大量的修改)。在站点包含的每个部分中添加域。

# 默认只允许来自当前站点的内容
# 允许来自当前网站和 imgur.com 的图片
# 不允许使用 Flash 和 Java 等对象
# 只允许来自当前站点的脚本
# 仅允许当前站点的样式
# 只允许当前站点的 frame
# 将 <base> 标记中的 URL 限制为当前站点
# 允许表单仅提交到当前站点
Content-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';
Strict-Transport-Security

这个 Header 告诉浏览器,该网站应仅允许 HTTPS 访问 —— 始终在您的网站启用 HTTPS 时启用。如果您使用子域名,我也建议在任何被使用的子域名上强制开启它。

Strict-Transport-Security: max-age=3600; includeSubDomains
X-Content-Type-Options

此 header 确保浏览器遵守应用程序设置的 MIME 类型。这有助于防止某些类型的跨站点脚本绕过。

它还减少了由于浏览器可能不正确猜测某些内容导致的意外应用程序行为,例如当开发人员标记一个页面 HTML,但浏览器认为它看起来像 JavaScript,并试图将其作为 JavaScript 来渲染。这这个 Header 将确保浏览器始终遵守服务器设置的 MIME 类型。

X-Content-Type-Options: nosniff
Cache-Control

这一个比其他的稍微复杂一些,因为您可能需要针对不同的内容类型使用不同的缓存策略。

任何具有敏感数据的页面,例如用户页面或客户结帐页面,都应该设置为无缓存。原因之一是防止其他使用共享计算机的人按下后退按钮或浏览历史并查看个人信息。

但是,很少更改的页面,如静态资源(图像,CSS 文件和 JavaScript 文件)很适合缓存。这可以在逐页的基础上完成,也可以在服务器配置上使用正则表达式完成。

# 默认情况下不缓存
Header set Cache-Control no-cache

# 缓存静态资源 1<filesMatch ".(css|jpg|jpeg|png|gif|js|ico)$">
Header set Cache-Control "max-age=86400, public"
</filesMatch>
Expires

这将设置当前请求缓存到期的时间。如果设置了 Cache-Control max-age 的 Header,它将被忽略。所以我们只在一个简单的扫描器测试它而不考虑 cache-control 的情况下设置它。

出于安全考虑,我们假设浏览器不应该缓存任何东西,因此我们将把这个设置为一个日期,该日期的计算值总是为过去。

Expires: 0
X-Frame-Options

这个 Header 指是否应该允许站点在 iFrame 中显示。

如果恶意网站将您的网站置于 iFrame 中,则恶意网站可以通过运行一些 JavaScript 来执行点击攻击,该 JavaScript 会捕获 iFrame 上的鼠标点击,然后代表用户与该网站进行交互(不一定点击他们认为他们点击的地方!)。

这应该总是设置为 deny,除非您特别使用 Frames, 在这种情况下,它应该设置为同源(same-origin)。如果您在设计中将 Frames 与其他网站一起使用,您也可以在此处白名单列出其他域名。

还应注意,此 Header 已被 CSP frame-ancestrs 指令取代。我仍然建议现在就设置它以作为缓冲工具,但将来它可能会逐步被淘汰。

X-Frame-Options: deny
Access-Control-Allow-Origin

告诉浏览器哪些其他站点的前端 JavaScript 代码可能会对该页面发出请求。除非需要设置此值,否则默认值通常是正确的设置。

例如,如果 SiteA 提供了一些想要向 SiteB 发出请求的 JavaScript,那么 SiteB 必须提供带有 Header 的响应,这个 Header 指定 SiteA 被允许发出这个请求。如果需要设置多个源,请参阅 MDN 上的详细信息页面.

这可能有点令人困惑,所以我绘制了一个图表来说明这个 Header 如何运作:

知识分享

具有 Access-Control-Allow-Origin 的数据流

Access-Control-Allow-Origin: http://www.one.site.com
Set-Cookie

确保您的 Cookie 仅通过 HTTPS(加密)发送,并且不能通过 JavaScript 访问它们。如果您的站点也支持 HTTPS,则只能发送 HTTPS Cookie,这是应该的。您应该始终设置以下标志:

  • Secure
  • HttpOnly

Cookie 定义示例:

Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

有关更多 Cookie 的信息,请参阅有关 Cookie 的优秀 Mozilla 文档

X-XSS-Protection

这个 Header 指示浏览器停止检测到的跨站点脚本攻击的执行。它通常是低风险设置,但仍应在投入生产前进行测试。

X-XSS-Protection: 1; mode=block

10. HTTP / 3 (QUIC)

HTTP/3 协议即将标准化。作为一个老协议使用者,我想我该写一些看法了。

Google(pbuh) 公司拥有最流行的 web 浏览器(Chrome)和两个最流行的网站(#1 Google.com #2 Youtube.com)。因此谷歌可以控制 web 协议的发展。他们的第一次升级称之为 SPDY (发音”speedy”),这次更新最终成为 HTTP 协议第二版标准,即 HTTP/2 。他们的第二次升级称之为 QUIC(发音”quick”),将成为 HTTP/3 协议标准。

QUIC实际上更像是TCP (TCP/2???)的新版本,而不是HTTP (HTTP/3)的新版本。它并没有真正改变HTTP/2的功能,而是改变了传输的工作方式。因此,我下面的评论集中在传输问题上,而不是HTTP问题。

主要的标题特性是更快的连接设置和延迟。TCP要求在建立连接之前来回发送大量数据包。SSL同样需要在建立加密之前来回发送大量数据包。如果网络延时很大,比如人们使用半秒ping时间的卫星互联网,建立连接需要相当长的时间。通过减少往返,连接可以更快地建立,这样当您单击链接时,链接的资源就会立即弹出

下一个主要特性是带宽。网络连接的源和目的之间总是存在带宽限制,这几乎总是由于拥塞。双方都需要使用这个速度,以便他们能够以适当的速度发送数据包。如果发送数据包太快,那么它们就会被丢弃,这会在不提高传输速率的情况下给其他数据包造成更大的拥塞。发送数据包太慢意味着不能最优地使用网络。

HTTP 传统上这一点做得很糟糕。 使用单个 TCP 连接不适用于 HTTP,因为与网站的交互需要同时传输多个内容,因此浏览器打开了与 Web 服务器的多个连接(通常为 6 个)。但是,这会打破对带宽的估计,因为每个 TCP 连接都尝试独立完成,就像其他连接不存在一样。SPDY 通过其多路复用功能解决了这个问题,该功能将浏览器/服务器之间的多个交互与单个带宽计算相结合。

QUIC 扩展了这种多路复用,使得处理浏览器/服务器之间的多个交互变得更加容易,而没有任何一个交互阻止另一个交互,且具有共同带宽。 从用户的角度来看,这将使交互更加顺畅,同时减少路由器遇到的拥塞。

我们现在来谈谈用户模式栈。 特别是在服务器上,TCP连接由操作系统内核处理,而服务本身在用户模式中运行。跨内核/用户模式边界移动会导致性能降低。追踪大量TCP连接会导致扩展性问题。有些人尝试将服务放入内核来避免转换,这并不可取,因为它破坏了操作系统的稳定性。我的解决方案是用BlackICE IPS和masscan,使用自定义TCP栈,利用硬件的用户模式驱动程序,将数据包从网络芯片直接传送到用户模式进程,绕过内核(参见PoC || GTFO#15)。近年来,DPDK套件已经变得流行。

但是,从TCP迁移到UDP可以在没有用户模式驱动程序的情况下获得相同的性能。您可以调用recvmmsg()一次接收一堆UDP数据包,而不是调用众所周知的recv()函数来一次接收一个数据包。它仍然是内核/用户模式转换,但是一次性收到的一百个数据包分摊,而不是每个数据包的转换。

在我自己的测试中,使用典型的recv()函数限制为大约500,000 UDP数据包 /秒,但使用recvmmsg()和其他一些优化(使用RSS的多核),可以在低端四核服务器上获得5,000,000 UDP数据包/秒。由于每个核心的扩展性很好,因此迁移到具有64个核心的强大服务器可以进一步提高。

BTW,“RSS”是网络硬件的一个特点,它将传入的数据包分成多个接收队列。多核扩展性的最大问题是两个CPU核心需要同时读取/修改同一个东西,因此共享相同的UDP队列成为最大的瓶颈。因此,首先英特尔和其他以太网供应商添加了RSS,为每个核心提供了自己的非共享数据包队列。 Linux和其他操作系统升级UDP以支持单个套接字(SO_REUSEPORT)的多个文件描述符来处理多个队列。现在,QUIC使用这些改进使得每个核心管理自己的UDP数据包流,不会有与其他CPU核心共享内容的导致可扩展性问题。之所以提到这一点,是因为在2000年,我亲自与英特尔硬件工程师讨论过有多个数据包队列问题。这个问题很普遍,也有对应的解决方案,在HTTP / 3出现之前,看看在过去二十年中的发展也是很有意思。如果没有网络硬件中的RSS,QUIC就不太可能成为标准。

QUIC 的另一个优美的解决方案是对移动的支持。当你带着你的笔记本电脑四处移动到不同的 WIFI 网络时,或者带着你的手机四处移动时,你设备的 IP 地址会发生变化的。操作系统以及协议不会优雅的关闭掉老的连接而打开新的连接。然而,QUIC,网络连接的标识符并不是传统概念上的一个“socket”(源/目标 端口/地址 协议的绑定),而是一个64位的赋值到连接上的标识符。

这意味着当你移动时,即使 IP 地址改变了,你依然能够和 YouTube 继续保持一个持续不间断的视频流,或者继续拨打一个视频电话而不被异常中断。网络工程师们已经和“移动IP”的技术问题攻关了几十年,试图想出一个有效的解决方案。他们专注于端到端原则,也就是在你移动时以某种方式保持一个恒定的 IP 地址,这不是一个实际的解决方案。很高兴看到 QUIC / HTTP/3 最终解决了这个问题。

如何使用这种新的交通工具?几十年来,网络编程的标准一直是被称为“sockets”的传输层API。调用recv()之类的函数来接收代码中的包。使用QUIC/HTTP/3,我们不再拥有操作系统传输层API。相反,它是一个更高层次的特性,可以在go编程语言中使用,或者在OpenResty nginx web服务器中使用Lua。

我之所以提到这一点,是因为在您对OSI模型的学习中,有一件事遗漏了,那就是它最初设想每个人都编写应用层(7)api,而不是传输层(4)api。应该有一些应用程序服务元素,它们可以以标准的方式为不同的应用程序处理文件传输和消息传递之类的事情。我认为人们正越来越多地转向这种模式,尤其是由带有go、QUIC、protobufs等的谷歌驱动。

我之所以提到这一点,是因为谷歌和微软之间的差异。微软拥有一个流行的操作系统,所以它的创新是由它在该操作系统中所能做的事情驱动的。谷歌的创新是由它可以放在操作系统上的东西驱动的。然后是Facebook和亚马逊自己,它们必须在谷歌提供的堆栈之上(或之外)进行创新。世界上排名前五的公司依次是苹果、谷歌、微软、亚马逊和facebook,因此,每一家公司推动创新的地方都很重要。

**结论**

在 20 世纪 70 年代,TCP 被创造出来的时候,是非常了不起的。它处理的事情,如拥塞,比其他竞争性协议要好很多。人们没有办法预料到会有 40 亿个 IPV4 地址的出现,那时候预计现代互联网的竞争性设计会比七八十年代要好。

IPv4 到 IPv6 的升级,很大程度上保持了 IP 的优势。 从 TCP 到 QUIC 的升级同样是基于 TCP 的优点的,并将其扩展到现代需求上。实际上令人惊讶的是,TCP 已经持续了如此长的时间,它在没有升级的情况下,做得很好。

TCP

UDP

Websorcket

随着 Web 的发展,用户对于 Web 的实时推送要求也越来越高,在 WebSocket 出现之前,大多数情况下是通过客户端发起轮询来拿到服务端实时更新的数据,因为 HTTP1.x 协议有一个缺陷就是通信只能由客户端发起,服务端没法主动给客户端推送。这种方式在对实时性要求比较高的场景下,比如即时通讯、即时报价等,显然会十分低效,体验也不好。为了解决这个问题,便出现了 WebSocket 协议,实现了客户端和服务端双向通信的能力。介绍 WebSocket 之前,还是让我们先了解下轮询实现推送的方式。

短轮询(Polling)

短轮询的实现思路就是浏览器端每隔几秒钟向服务器端发送 HTTP 请求,服务端在收到请求后,不论是否有数据更新,都直接进行响应。在服务端响应完成,就会关闭这个 TCP 连接,代码实现也最简单,就是利用 XHR , 通过 setInterval 定时向后端发送请求,以获取最新的数据。

setInterval(function() { fetch(url).then((res) => { // success code }) }, 3000);
  • 优点:实现简单。
  • 缺点:会造成数据在一小段时间内不同步和大量无效的请求,安全性差、浪费资源。

长轮询(Long-Polling)

客户端发送请求后服务器端不会立即返回数据,服务器端会阻塞请求连接不会立即断开,直到服务器端有数据更新或者是连接超时才返回,客户端才再次发出请求新建连接、如此反复从而获取最新数据。大致效果如下:

知识分享--架构

function async() {
    fetch(url).then((res) => {
        async();
        // success code
    }).catch(() => {
        // 超时
        async();
    })
}
  • 优点:比 Polling 做了优化,有较好的时效性。
  • 缺点:保持连接挂起会消耗资源,服务器没有返回有效数据,程序超时。

WebSocket

前面提到的短轮询(Polling)和长轮询(Long-Polling), 都是先由客户端发起 Ajax 请求,才能进行通信,走的是 HTTP 协议,服务器端无法主动向客户端推送信息。

当出现类似体育赛事、聊天室、实时位置之类的场景时,轮询就显得十分低效和浪费资源,因为要不断发送请求,连接服务器。WebSocket 的出现,让服务器端可以主动向客户端发送信息,使得浏览器具备了实时双向通信的能力。

没用过 WebSocket 的人,可能会以为它是个什么高深的技术。其实不然,WebSocket 常用的 AP前面提到的短轮询(Polling)和长轮询(Long-Polling), 都是先由客户端发起 Ajax 请求,才能进行通信,走的是 HTTP 协议,服务器端无法主动向客户端推送信息。

当出现类似体育赛事、聊天室、实时位置之类的场景时,轮询就显得十分低效和浪费资源,因为要不断发送请求,连接服务器。WebSocket 的出现,让服务器端可以主动向客户端发送信息,使得浏览器具备了实时双向通信的能力。

没用过 WebSocket 的人,可能会以为它是个什么高深的技术。其实不然,WebSocket 常用的 API 不多也很容易掌握,不过在介绍如何使用之前,让我们先看看它的通信原理。

通信原理

当客户端要和服务端建立 WebSocket 连接时,在客户端和服务器的握手过程中,客户端首先会向服务端发送一个 HTTP 请求,包含一个 Upgrade 请求头来告知服务端客户端想要建立一个 WebSocket 连接。

在客户端建立一个 WebSocket 连接非常简单:

let ws = new WebSocket('ws://localhost:9000');

类似于 HTTP 和 HTTPS,ws 相对应的也有 wss 用以建立安全连接,本地已 ws 为例。这时的请求头如下:

Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: Upgrade // 表示该连接要升级协议 Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; JSESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264 Host: localhost:9000 Origin: http://localhost:9000 Pragma: no-cache Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: 5fTJ1LTuh3RKjSJxydyifQ== // 与响应头 Sec-WebSocket-Accept 相对应 Sec-WebSocket-Version: 13 // 表示 websocket 协议的版本 Upgrade: websocket // 表示要升级到 websocket 协议 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36

响应头

Connection: Upgrade

Sec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE=

Upgrade: websocket

知识分享--架构

此时响应行(General)中可以看到状态码 status code 是 101 Switching Protocols , 表示该连接已经从 HTTP 协议转换为 WebSocket 通信协议。 转换成功之后,该连接并没有中断,而是建立了一个全双工通信,后续发送和接收消息都会走这个连接通道。

注意,请求头中有个 Sec-WebSocket-Key 字段,和相应头中的 Sec-WebSocket-Accept 是配套对应的,它的作用是提供了基本的防护,比如恶意的连接或者无效的连接。Sec-WebSocket-Key 是客户端随机生成的一个 base64 编码,服务器会使用这个编码,并根据一个固定的算法:

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";    //  一个固定的字符串
accept = base64(sha1(key + GUID));  // key 就是 Sec-WebSocket-Key 值,accept 就是 Sec-WebSocket-Accept 值
  • 其中 GUID 字符串是 RFC6455 官方定义的一个固定字符串,不得修改。
  • 客户端拿到服务端响应的 Sec-WebSocket-Accept 后,会拿自己之前生成的 Sec-WebSocket-Key 用相同算法算一次,如果匹配,则握手成功。
  • 然后判断 HTTP Response 状态码是否为 101(切换协议),如果是,则建立连接,大功告成。

PingPong一个关于维持链接的websocket设计技术细节

虽然说webSocket解决了服务器和客户端的链接问题,但是网络应用除了客户端和服务器还存在中间的网络链路,一个http/websocket链接往往还需要进过无数的路由,防火墙,在这个过程中,中间节点的处理方法可能会让人想不到,这些坑爹的中间节点可能会认为一份连接在一段时间内没有数据发送就等于失效,它们会自作主张的切断这些连接。在这种情况下,不论服务器还是客户端都不会收到任何提示,它们只会一厢情愿的以为彼此间的红线还在,徒劳地一边又一边地发送抵达不了彼岸的信息。而计算机网络协议栈的实现中又会有一层套一层的缓存,除非填满这些缓存,你的程序根本不会发现任何错误。

解决方案:

  • WebSocket 的设计者们也早已想过。就是让服务器和客户端能够发送 Ping/Pong Frame。这种 Frame 是一种特殊的数据包,它只包含一些元数据而不需要真正的 Data Payload,可以在不影响 Application 的情况下维持住中间网络的连接状态。
  • 总结:在实时消息的应用场景下,比起轮询,长链接等方案,webSocket确实给我们提供了一个比较完美的解决方案。

STOMP传输协议介绍

  • STOMP 中文为: 面向消息的简单文本协议
  • websocket定义了两种传输信息类型:文本信息和二进制信息。类型虽然被确定,但是他们的传输体是没有规定的。所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议。
  • STOMP是基于帧的协议,客户端和服务器使用STOMP帧流通讯

一个STOMP客户端是一个可以以两种模式运行的用户代理,可能是同时运行两种模式。

  • 作为生产者,通过SEND框架将消息发送给服务器的某个服务
  • 作为消费者,通过SUBSCRIBE制定一个目标服务,通过MESSAGE框架,从服务器接收消息。

例如:

COMMAND
header1:value1
header2:value2
Body^@

注:帧以commnand字符串开始,以EOL结束。其中包括可选回车符(13字节),紧接着是换行符(10字节)。command下面是0个或多个<key>:<value>格式的header条目, 每个条目由EOL结束。

一个空白行(即额外EOL)表示header结束和body开始。body连接着NULL字节。本文档中的例子将使用^@代表NULL字节。

NULL字节可以选择跟多个EOLs。欲了解更多关于STOMP帧的详细信息,请参阅STOMP1.2协议规范

简单单聊(一个纯文字消息类型的一对一聊天(单聊)功能)
客户端:


function connectWebsocket() {
    ws = new WebSocket('ws://localhost:9000');
    // 监听连接成功
    ws.onopen = () => {
        console.log('连接服务端WebSocket成功');
        ws.send(JSON.stringify(msgData));   // send 方法给服务端发送消息
    };

    // 监听服务端消息(接收消息)
    ws.onmessage = (msg) => {
        let message = JSON.parse(msg.data);
        console.log('收到的消息:', message)
        elUl.innerHTML += `<li class="b">小秋:${message.content}</li>`;
    };

    // 监听连接失败
    ws.onerror = () => {
        console.log('连接失败,正在重连...');
        connectWebsocket();
    };

    // 监听连接关闭
    ws.onclose = () => {
        console.log('连接关闭');
    };
};
connectWebsocket();

从上面可以看到 WebSocket 实例的 API 很容易理解,简单好用,通过 send() 方法可以发送消息,onmessage 事件用来接收消息,然后对消息进行处理显示在页面上。 当 onerror 事件(监听连接失败)触发时,最好进行执行重连,以保持连接不中断。

服务端 Node : (这里使用 ws 库)

const path = require('path');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const WebSocket = require('ws');

const wss = new WebSocket.Server({ server: server });

wss.on('connection', (ws) => {

  // 监听客户端发来的消息
  ws.on('message', (message) => {
    console.log(wss.clients.size);
    let msgData = JSON.parse(message);  
    if (msgData.type === 'open') {
      // 初始连接时标识会话
      ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`;
    } else {
      let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`;
      wss.clients.forEach(client => {
        if (client.sessionId === sessionId) {
          client.send(message);  // 给对应的客户端连接发送消息
        }
      }) 
    }
  })

  // 连接关闭
  ws.on('close', () => {
    console.log('连接关闭'); 
  });
});

同理,服务端也有对应的发送和接收的方法。完整示例代码见 这里

这样浏览器和服务端就可以愉快的发送消息了,效果如下:

知识分享--架构

其中绿色箭头表示发出的消息,红色箭头表示收到的消息。

心跳保活

  • 在实际使用 WebSocket 中,长时间不通消息可能会出现一些连接不稳定的情况,这些未知情况导致的连接中断会影响客户端与服务端之前的通信,
  • 为了防止这种的情况的出现,有一种心跳保活的方法:客户端就像心跳一样每隔固定的时间发送一次 ping ,来告诉服务器,我还活着,而服务器也会返回 pong ,来告诉客户端,服务器还活着。ping/pong 其实是一条与业务无关的假消息,也称为心跳包。

可以在连接成功之后,每隔一个固定时间发送心跳包,比如 60s:

setInterval(() => {
    ws.send('这是一条心跳包消息');
}, 60000)

总结

知识分享--架构

当创建 WebSocket 实例的时候,会发一个 HTTP 请求,请求报文中有个特殊的字段 Upgrade ,然后这个连接会由 HTTP 协议转换为 WebSocket 协议,这样客户端和服务端建立了全双工通信,通过 WebSocket 的 send 方法和 onmessage 事件就可以通过这条通信连接交换信息。

并发

系统的并发能力通常以每秒请求数来衡量,指服务器在单位时间内(秒)可以处理的请求数量,一般以RPS。有些地方会说TPS, T表示 Transaction事务,是类似的概念,不过稍有区别.

影响一个系统的并发性能的因素有以下这些(不考虑前端负载优化的情况),不同场景下,各个因素的影响程度不同。

并非只有服务代码对性能有影响,从硬件,系统,虚拟机,框架/web服务器,缓存,数据库,算法, I/O都有重要的影响.

硬件

硬件配置–CPU, 内存,存储设备,网络带宽

  • 不同CPU的计算能力

    • 频率
    • 核心
    • 超线程
    • L1-L2-L3缓存大小
    • 指令集 (对特定应用,指令集有极大的性能提升,带来这种巨大差异的原因在于,专用指令一个指令可以完成的任务,
      CPU有专用电路来完成,用替代指令需要数十甚至数百个指令才可以完成)
  • GPU

    • GPU对矩阵类计算的数量级提升
    • GPU的单个节点计算能力不如CPU完整和强大,但是有超过CPU几个数量级的并行处理单元
  • 内存存储的优越性

    • 数百万IOPS
    • 低延迟, 几十ns
  • 传统硬盘和SSD的IOPS的差距

    • 传统磁盘 100-300 IOPS
      • 擅长连续写
      • 随机读很差
      • 随机访问延迟10ms左右
      • RAID方案0,1,5,6,10
      • LVM
    • SSD 数千-数十万IOPS (高端企业级产品部分场景可达到数百万IOPS, 如Intel 傲腾Optane, 和个别企业级SSD)
      • 随机读和随机写性能没有太大差距
      • 延迟低,小于1ms,最低可达到几us
  • 网络带宽可以支撑的最大RPS计算 = 带宽/请求数据包TCP消息size

  • 网络延迟影响

通信

通信协议对速度影响

  • 请求和相应数据包的大小

    • HTTP数据包
    • TCP数据包
  • 协议格式的影响
    HTTP类的协议头部尺寸比TCP报文头明显大一些,且无法压缩

    • TCP
    • UDP
    • WebSocket
    • HTTP 1.1 keep-alive技术
    • HTTP 2.0 多路复用, HTTP头部压缩
    • HTTP 3.0 基于UDP,消除连接建立和重试时间
    • HTTPS
    • HTTP Pipeline技术
  • 协议栈实现的影响

    • 不同通信协议底层实现的优劣
    • 例如单个链接的资源消耗
    • 链接的重用机制等
  • 通信方式的优劣

    • poll/epoll, select, IOCP
    • NIO/AIO

编译相关

  • 静态编译

    • 执行编译和链接,直接生成机器码, 如: C/C++/Rust/Object-C/golang
    • 优点是启动速度快,直接就是原生的机器码执行
    • 缺点是需要为每个目标平台单独编译版本,甚至调整代码,不能直接跨平台
    • 编译时优化手段:内联展开, 无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块冲排序(Basic Block Reordering)
  • JIT

    • 编译为中间语言,在运行时在动态编译(JIT)为机器码,如: Java, .Net
    • 编译时会使用静态语言编译时的优化手段+加上特定场景的优化
    • 优点:
      • 编译为中间语言,运行时动态编译;
      • 方便跨平台;
      • 可以根据不同型号的CPU动态优化翻译的机器码
    • 缺点:
      • 第一次执行需要编译为机器码的时间,在程序运行之初稍微影响性能
        • 提升性能方法
          • AOT (部分代码静态编译)
          • 方舟编译器 ??
          • 预热 (启动以后,将常用代码分支主动先执行N次,注意JVM 热点代码阈值)
      • 运行的机器需要安装runtime
  • 解释执行

    • 执行时逐段进行翻译,如: Javascript, python, php
    • 优点: 1. 编写代码灵活 2. 修改方便
    • 缺点:
      • 性能不佳,因为需要即时翻译(可以缓存翻译结果,即时这样,翻译结果也不如机器码优化)
      • 注意: 解释执行的主要是脚本语言,因为脚本语言本身的执行引擎一般是C系列语言编写的,所以性能也不会太差。加上后续会缓存第一次解释的结果

序列化

  • JSON
  • 二进制
    • 二进制序列化优势是数据包比json小很多,这是为什么RPC一般采用二进制序列化
    • Dubbo
    • Protobuf
    • Bolt
    • 其它
  • XML

压缩

  • 压缩的必要性
    • 减少传输时间
    • 需要平衡压缩率和压缩吞吐量
  • HTTP压缩协议
    • Gzip
    • Brotli (google)
  • 文件压缩
    • 图片压缩
      • optiPNG
      • webP (google)
      • mozJPG
      • svg/gif压缩
    • 视频/音频
      • H264/H265/VP9/webM
      • MP3/AAC
    • 其他文件
      • deflate
      • lz4/snappy (**推荐)
      • gzip

多线程/进程/协程

  • 线程、进程模型
    • 线程池
    • 线程创建和切换的代价
    • 进程/线程 绑定CPU核心(逻辑CPU, 物理CPU)的技巧
    • 多进程和多线程的优劣
    • 进程通信
      • IPC/内存映射/管道操作
    • Actor
    • Reactor
    • Proactor
  • 协程
    • 协程的优势
    • golang
    • rust
    • php-swoole

竞争

竞争处理

    • 可重入/不可重入锁
    • 公平锁/非公平锁
    • 乐观锁/悲观锁
    • 读写锁
    • 自旋锁
    • 优化
      • 乐观锁
      • 偏向锁
      • 缩小锁范围
      • 锁分段
      • 无锁编程
  • 信号量和互斥
  • 原子操作 (atomic operation) CAS
    • ABA问题
    • 自旋等待问题
  • 伪共享陷阱false-sharing
  • Locality
  • CopyOnWrite (Java: CopyOnWriteArrayList等)

IO

  • I/O模型
    • BIO, NIO, AIO
    • select, poll, epoll, iocp
  • I/O优化
    • 异步写
    • AOF/WAL (Append Only写入)
    • 写合并
    • 防止写放大 (write-amplification)
    • 文件压缩(适当压缩,平衡压缩/解压缩速度和读写时间,如snappy算法)
    • 磁盘缓存(文件系统缓存)
    • Linux提供了cfq, deadline和noop三种调度策略
    • SSD-关闭日志功能
      启用 TRIM 功能
    • 合适的文件系统(EXT3,4,XFS, btrfs…)
    • 零拷贝(zero-copy)
    • 连接池

GC

内存管理/垃圾回收

  • 不同垃圾回收算法的优点和缺点 (JAVA, PHP, Golang)
    • Java
      • CMS
      • G1
      • ZGC
      • JVM GC相关参数对GC的影响
  • 操作系统内存的直接使用 (Java: DirectBuffer)
  • 自建内存池 (例子: netty)
  • 对象重用,对象池(如连接池技术),减少内存分配

缓存

缓存使用

  • CPU高速缓存
  • 进程内缓存
    • 缓存常用对象
    • 缓存常用配置
    • 缓存高频使用的数据
    • 控制进程内缓存大小,理解缓存大小带来的GC压力
  • 文件系统缓存(buffer) - 前文提到过磁盘缓存
  • 代理服务器缓存
  • 分布式缓存
    • redis
      • 数据大小优化(key, value)
      • 合理数据结构的采用
      • 缓存逐出策略
      • 避免穿透,击穿和雪崩
      • redis pipeline
      • 连接池

aysnc

异步编程

  • Reactive
  • Future
  • Async/Await
  • 消息队列
  • CQRS

programming

  • 编程语言注意点
    • 类型转换的消耗
    • Collection类型扩容的行为特点和陷阱要了解
    • 字符串处理最佳实践
    • 数组(或连续内存区域)的批量操作
    • 尽可能让编译器内联编译
    • 异常不作为逻辑的一部分
    • 使用bit操作
    • 使用缓冲流
    • 不在循环中耗时操作
    • 尽可能不返回大对象或过多数据
    • 超时机制(避免长时间资源占用)
    • 避免大对象直接存储在缓存
    • 避免读取文件全部内容到内存
    • 尽可能减少通过jni调用操作系统C函数 (减少用户态到内核态的切换)
    • Fail-fast/fail-safe机制
  • 算法和数据结构
    • 合适的算法和数据结构提供数倍,数十倍甚至更高的效率
    • 排序
    • 查找
    • HASH
    • 数组
    • 链表

database

数据库性能

  • 为不同的数据,选择合适的数据库类型

  • MySQL

    • 查询优化

      • 减少返回字段
      • 减少Join
      • 确保索引生效 (生效的条件,以及不生效的情况要了解)
      • 索引类型
      • 索引覆盖
    • 表优化

      • 字段设计尽量短
      • 字段内容尽可能使用数值
      • 字段不存复杂字符串
      • 允许少量冗余加速避免JOIN
      • 索引碎片
    • 分区

      • 多个磁盘可考虑
      • 分区键选择
    • 分库分表

      • 分区主键选择
    • 性能诊断

      • Explain
      • 慢查询日志
    • 数据库服务器设置

      • 磁盘缓存

      • MySQL配置

        • key_buffer_size指定用于索引的缓冲区

        • back_log参数的值指出在MySQL请求缓存队列

        • sort_buffer_size = 6M #查询排序时所能使用的缓冲区大小。注意:该参数对应的分配内存是每连接独占

        • max_allowed_packet = 4M
          thread_stack = 256K
          table_cache = 128K

        • read_buffer_size = 4M #读查询操作所能使用的缓冲区大小。和sort_buffer_size一样,该参数对应的分配内存也是每连接独享

        • join_buffer_size = 8M #联合查询操作所能使用的缓冲区大小,和sort_buffer_size一样,该参数对应的分配内存也是每连接独享。

        • table_cache = 512
          thread_cache_size = 64
          query_cache_size = 64M

          #指定MySQL查询缓冲区的大小

        • tmp_table_size = 256M #临时表大小

        • max_connections = 768 #指定MySQL允许的最大连接进程数

        • wait_timeout = 10 #指定一个请求的最大连接时间

        • thread_concurrency = 8 #该参数取值为服务器逻辑CPU数量*2

        • 等等,有很多可以探索

  • Postgresql

  • 分布式MPP数据库(TiDB, Greenplum)

  • 文档数据库(Mongodb)

  • 图数据库 (GraphDB)

  • 时间序列数据库

system

  • 系统调用

    • 尽可能减少内核态–用户态上下文切换的开销
  • 操作系统配置

    • 最大文件描述符
    • 文件系统缓存等设置
    • TCP设置(超时,帧大小,滑动窗口,重试,缓冲区)
    • 任务调度设置
  • 服务器软件配置

    • 连接数
    • 线程池
    • 各种buffer
    • 网络设置
    • 超时时间

loadbalance

负载均衡

  • F5 (作为硬件级别的均衡器,其性能无与伦比,价格也是)
  • LVS
  • Haproxy
  • Nginx
    • OpenResty
    • Apisix
    • Kong
  • 负载均衡算法
    • 随机
    • 轮询
    • 加权最小连接数
    • 加权最小工作负载

分布式

知识分享--架构

补充

  1. 理论
    1. CAP
    2. BASE
  2. 分布式事务
    1. 两阶段提交
    2. TCC (Try-Confirm-Cancel)
    3. 三阶段提交
    4. 分布式事务协议

基础理论

SOA 到 MSA 的进化

SOA 面向服务架构

由于业务发展到一定程度后,需要对服务进行解耦,进而把一个单一的大系统按逻辑拆分成不同的子系统,通过服务接口来通讯。面向服务的设计模式,最终需要总线集成服务,而且大部分时候还共享数据库,出现单点故障时会导致总线层面的故障,更进一步可能会把数据库拖垮,所以才有了更加独立的设计方案的出现。

知识分享--架构

MSA 微服务架构

微服务是真正意义上的独立服务,从服务入口到数据持久层,逻辑上都是独立隔离的,无需服务总线来接入,但同时也增加了整个分布式系统的搭建和管理难度,需要对服务进行编排和管理,所以伴随着微服务的兴起,微服务生态的整套技术栈也需要无缝接入,才能支撑起微服务的治理理念。

知识分享--架构

节点与网络

节点

传统的节点也就是一台单体的物理机,所有的服务都揉进去包括服务和数据库;随着虚拟化的发展,单台物理机往往可以分成多台虚拟机,实现资源利用的最大化,节点的概念也变成单台虚拟机上面服务;近几年容器技术逐渐成熟后,服务已经彻底容器化,也就是节点只是轻量级的容器服务。总体来说,节点就是能提供单位服务的逻辑计算资源的集合。

网络

分布式架构的根基就是网络,不管是局域网还是公网,没有网络就无法把计算机联合在一起工作,但是网络也带来了一系列的问题。网络消息的传播有先后,消息丢失和延迟是经常发生的事情,我们定义了三种网络工作模式:

  • 同步网络
    • 节点同步执行
    • 消息延迟有限
    • 高效全局锁
  • 半同步网络
    • 锁范围放宽
  • 异步网络
    • 节点独立执行
    • 消息延迟无上限
    • 无全局锁
    • 部分算法不可行

常用网络传输层有两大协议的特点简介:

  • TCP 协议
    • 首先 tcp 协议传输可靠,尽管其他的协议可以更快传输
    • tcp 解决重复和乱序问题
  • UDP 协议
    • 常量数据流
    • 丢包不致命

时间与顺序

时间

慢速物理时空中,时间独自在流淌着,对于串行的事务来说,很简单的就是跟着时间的脚步走就可以,先来后到的发生。而后我们发明了时钟来刻画以往发生的时间点,时钟让这个世界井然有序。但是对于分布式世界来说,跟时间打交道着实是一件痛苦的事情。

分布式世界里面,我们要协调不同节点之间的先来后到关系,不同节点本身承认的时间又各执己见,于是我们创造了网络时间协议(NTP)试图来解决不同节点之间的标准时间,但是 NTP 本身表现并不尽如人意,所以我们又构造出了逻辑时钟,最后改进为向量时钟:

NTP 的一些缺点,无法完全满足分布式下并发任务的协调问题

  • 节点间时间不同步
  • 硬件时钟漂移
  • 线程可能休眠
  • 操作系统休眠
  • 硬件休眠

知识分享--架构
逻辑时钟

  • 定义事件先来后到
  • t’ = max(t, t_msg + 1)

知识分享--架构

  • 向量时钟
    • t_i’ = max(t_i, t_msg_i)
  • 原子钟
顺序

有了衡量时间的工具,解决顺序问题自然就是水到渠成了。因为整个分布式的理论基础就是如何协商不同节点的一致性问题,而顺序则是一致性理论的基本概念,所以前文我们才需要花时间介绍衡量时间的刻度和工具。

一致性理论

一致性强弱对系统建设影响的对比图

知识分享--架构
该图对比了不同一致性算法下的事务、性能、错误、延迟的平衡。

强一致性 ACID

单机环境下我们对传统关系型数据库有苛刻的要求,由于存在网络的延迟和消息丢失,ACID 便是保证事务的原则,这四大原则甚至我们都不需要解释出来就耳熟能详了:

  • Atomicity:原子性,一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节;
  • Consistency:一致性,在事务开始之前和事务结束以后,数据库的完整性没有被破坏;
  • Isolation:隔离性,数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时,由于交叉执行而导致数据的不一致;
  • Durabilit:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
分布式一致性 CAP

分布式环境下,我们无法保证网络的正常连接和信息的传送,于是发展出了 CAP/FLP/DLS 这三个重要的理论:

  • CAP:分布式计算系统不可能同时确保一致性(Consistency)、可用性(Availablity)和分区容忍性(Partition);
  • FLP:在异步环境中,如果节点间的网络延迟没有上限,只要有一个恶意的节点存在,就没有算法能在有限的时间内达成共识;
  • DLS:
    • 在一个部分同步网络的模型(也就是说:网络延时有界限但是我们并不知道在哪里)下运行的协议可以容忍 1/3 任意(换句话说,拜占庭)错误;
    • 在一个异步模型中的确定性的协议(没有网络延时上限)不能容错(不过这个论文没有提起随机化算法可以容忍 1/3 的错误);
    • 同步模型中的协议(网络延时可以保证小于已知 d 时间),可以令人吃惊的达到 100% 容错,虽然对 1/2 的节点出错可以发生的情况有所限制。
弱一致性 BASE

多数情况下,其实我们也并非一定要求强一致性,部分业务可以容忍一定程度的延迟一致,所以为了兼顾效率,发展出来了最终一致性理论 BASE。BASE 是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency):

  • 基本可用(Basically Available):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用;
  • 软状态(Soft State):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现;
  • 最终一致性(Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
一致性算法

分布式架构的核心就在于一致性的实现和妥协,那么如何设计一套算法来保证不同节点之间的通信和数据达到无限趋向一致性,就非常重要了。保证不同节点在充满不确定性网络环境下能达成相同副本的一致性是非常困难的,业界对该课题也做了大量的研究。

首先我们要了解一致性的大前提原则 (CALM): CALM 原则的全称是 Consistency and Logical Monotonicity ,主要描述的是分布式系统中单调逻辑与一致性的关系,它的内容如下,参考 consistency as logical monotonicity

  • 在分布式系统中,单调的逻辑都能保证 “最终一致性”,这个过程中不需要依赖中心节点的调度;
  • 任意分布式系统,如果所有的非单调逻辑都有中心节点调度,那么这个分布式系统就可以实现最终“一致性”。

然后再关注分布式系统的数据结构 CRDT(Conflict-Free Replicated Data Types): 我们了解到分布式一些规律原则之后,就要着手考虑如何来实现解决方案,一致性算法的前提是数据结构,或者说一切算法的根基都是数据结构,设计良好的数据结构加上精妙的算法可以高效的解决现实的问题。经过前人不断的探索,我们得知分布式系统被广泛采用的数据结构 CRDT。 参考《谈谈 CRDT》,A comprehensive study of Convergent and Commutative Replicated Data Types

  • 基于状态(state-based):即将各个节点之间的 CRDT 数据直接进行合并,所有节点都能最终合并到同一个状态,数据合并的顺序不会影响到最终的结果;
  • 基于操作(operation-based):将每一次对数据的操作通知给其他节点。只要节点知道了对数据的所有操作(收到操作的顺序可以是任意的),就能合并到同一个状态。

了解数据结构后,我们需要来关注一下分布式系统的一些重要的协议HATs(Highly Available Transactions),ZAB(Zookeeper Atomic Broadcast): 参考《高可用事务》《ZAB 协议分析》

最后要学习的是业界主流的一致性算法 : 说实话具体的算法我也还没完全搞懂,一致性算法是分布式系统最核心本质的内容,这部分的发展也会影响架构的革新,不同场景的应用也催生不同的算法。

场景分类

文件系统

单台计算机的存储始终有上限,随着网络的出现,多台计算机协作存储文件的方案也相继被提出来。最早的分布式文件系统其实也称为网络文件系统,第一个文件服务器在 1970 年代被发展出来。在 1976 年迪吉多公司设计出 File Access Listener(FAL),而现代分布式文件系统则出自赫赫有名的 Google 的论文,《The Google File System》奠定了分布式文件系统的基础。现代主流分布式文件系统参考《分布式文件系统对比》,下面列举几个常用的文件系统:

  • HDFS
  • FastDFS
  • Ceph
  • mooseFS

数据库

数据库当然也属于文件系统,主数据增加了事务、检索、擦除等高级特性,所以复杂度又增加了,既要考虑数据一致性也得保证足够的性能。传统关系型数据库为了兼顾事务和性能的特性,在分布式方面的发展有限,非关系型数据库摆脱了事务的强一致性束缚,达到了最终一致性的效果,从而有了飞跃的发展,NoSql(Not Only Sql) 也产生了多个架构的数据库类型,包括 KV、列式存储、文档类型等。

  • 列式存储:Hbase
  • 文档存储:Elasticsearch,MongoDB
  • KV 类型:Redis
  • 关系型:Spanner

计算

分布式计算系统构建在分布式存储的基础上,充分发挥分布式系统的数据冗余灾备,多副本高效获取数据的特性,进而并行计算,把原本需要长时间计算的任务拆分成多个任务并行处理,从而提高了计算效率。分布式计算系统在场景上分为离线计算、实时计算和流式计算。

  • 离线:Hadoop
  • 实时:Spark
  • 流式:Storm,Flink/Blink

事务

分布式事务大概有以下这些机制:

  • 2pc(两段式提交)
  • 3pc(三段式提交)
  • TCC(Try、Confirm、Cancel)
  • 最大努力通知
  • XA
  • 本地消息表(ebay研发出的)
  • 半消息/最终一致性(RocketMQ)

缓存

缓存作为提升性能的利器无处不在,小到 CPU 缓存架构,大到分布式应用存储。分布式缓存系统提供了热点数据的随机访问机制,大大了提升了访问时间,但是带来的问题是如何保证数据的一致性,引入分布式锁来解决这个问题,主流的分布式存储系统基本就是 Redis 了。

  • 持久化:Redis
  • 非持久化:Memcache

消息

分布式消息队列系统是消除异步带来的一系列复杂步骤的一大利器,在多线程高并发场景下,我们常常需要谨慎设计业务代码,来保证多线程并发情况下不出现资源竞争导致的死锁问题。而消息队列以一种延迟消费的模式将异步任务都存到队列,然后再逐个消化。

  • Kafka
  • RabbitMQ
  • RocketMQ
  • ActiveMQ

监控

分布式系统从单机到集群的形态发展,复杂度也大大提高,所以对整个系统的监控也是必不可少。

  • Zookeeper

应用

分布式系统的核心模块就是在应用如何处理业务逻辑,应用直接的调用依赖于特定的协议来通信,有基于 RPC 协议的,也有基于通用的 HTTP 协议。

  • HSF
  • Dubbo

日志

错误对应分布式系统是家常便饭,而且我们设计系统的时候,本身就需要把容错作为普遍存在的现象来考虑。那么当出现故障的时候,快速恢复和排查故障就显得非常重要了。分布式日志采集存储和检索则可以给我们提供有力的工具来定位请求链路中出现问题的环节。

  • 日志采集:flume
  • 日志存储:ElasticSearch/Solr,SLS
  • 日志定位:Zipkin

账本

前文我们提到所谓分布式系统,是迫于单机的性能有限,而堆硬件却又无法无休止的增加,单机堆硬件最终也会遇到性能增长曲线的瓶颈。于是我们才采用了多台计算机来干同样的活,但是这样的分布式系统始终需要中心化的节点来监控或者调度系统的资源,即使该中心节点也可能是多节点组成。区块链则是真正的区中心化分布式系统,系统里面只有 P2P 网络协议各自通信,没有真正意义的中心节点,彼此按照区块链节点的算力、权益等机制来协调新区块的产生。

  • 比特币
  • 以太坊

分布式唯一ID生成

  1. Twitter 方案(Snowflake 算法):41位时间戳+10位机器标识(比如IP,服务器名称等)+12位序列号(本地计数器)
  2. Leaf - 美团点评方案基于Snowflake和MySQL方案扩展出了两套服务 Leaf Segment 和 Leaf Snowflake

设计模式

如何考虑架构设计的、不同设计方案直接的区别和侧重点、不同场景需要选择合作设计模式,来减少试错的成本

可用性

可用性是系统运行和工作的时间比例,通常以正常运行时间的百分比来衡量。它可能受系统错误、基础架构问题、恶意攻击和系统负载的影响。分布式系统通常为用户提供服务级别协议(SLA),因此应用程序必须设计为最大化可用性。

  • 健康检查:系统实现全链路功能检查,外部工具定期通过公开端点访问系统
  • 负载均衡:使用队列起到削峰作用,作为请求和服务之间的缓冲区,以平滑间歇性的重负载
  • 节流:限制应用级别、租户或整个服务所消耗资源的范围

数据管理

数据管理是分布式系统的关键要素,并影响大多数质量的属性。由于性能,可扩展性或可用性等原因,数据通常托管在不同位置和多个服务器上,这可能带来一系列挑战。例如,必须维护数据一致性,并且通常需要跨不同位置同步数据。

  • 缓存:根据需要将数据从数据存储层加载到缓存
  • CQRS(Command Query Responsibility Segregation): 命令查询职责分离
  • 事件溯源:仅使用追加方式记录域中完整的系列事件
  • 索引表:在经常查询引用的字段上创建索引
  • 物化视图:生成一个或多个数据预填充视图
  • 拆分:将数据拆分为水平的分区或分片

设计与实现

良好的设计包括诸如组件设计和部署的一致性、简化管理和开发的可维护性、以及允许组件和子系统用于其他应用程序和其他方案的可重用性等因素。在设计和实施阶段做出的决策对分布式系统和服务质量和总体拥有成本产生巨大影响。

  • 代理:反向代理
  • 适配器: 在现代应用程序和遗留系统之间实现适配器层
  • 前后端分离: 后端服务提供接口供前端应用程序调用
  • 计算资源整合:将多个相关任务或操作合并到一个计算单元中
  • 配置分离:将配置信息从应用程序部署包中移出到配置中心
  • 网关聚合:使用网关将多个单独的请求聚合到一个请求中
  • 网关卸载:将共享或专用服务功能卸载到网关代理
  • 网关路由:使用单个端点将请求路由到多个服务
  • 领导人选举:通过选择一个实例作为负责管理其他实例管理员,协调分布式系统的云
  • 管道和过滤器:将复杂的任务分解为一系列可以重复使用的单独组件
  • 边车:将应用的监控组件部署到单独的进程或容器中,以提供隔离和封装
  • 静态内容托管:将静态内容部署到 CDN,加速访问效率

消息

分布式系统需要一个连接组件和服务的消息传递中间件,理想情况是以松散耦合的方式,以便最大限度地提高可伸缩性。异步消息传递被广泛使用,并提供许多好处,但也带来了诸如消息排序,幂等性等挑战

  • 竞争消费者:多线程并发消费
  • 优先级队列: 消息队列分优先级,优先级高的先被消费

管理与监控

分布式系统在远程数据中心运行,无法完全控制基础结构,这使管理和监视比单机部署更困难。应用必须公开运行时信息,管理员可以使用这些信息来管理和监视系统,以及支持不断变化的业务需求和自定义,而无需停止或重新部署应用。

性能与扩展

性能表示系统在给定时间间隔内执行任何操作的响应性,而可伸缩性是系统处理负载增加而不影响性能或容易增加可用资源的能力。分布式系统通常会遇到变化的负载和活动高峰,特别是在多租户场景中,几乎是不可能预测的。相反,应用应该能够在限制范围内扩展以满足需求高峰,并在需求减少时进行扩展。可伸缩性不仅涉及计算实例,还涉及其他元素,如数据存储、消息队列等。

弹性

弹性是指系统能够优雅地处理故障并从故障中恢复。分布式系统通常是多租户,使用共享平台服务、竞争资源和带宽,通过 Internet 进行通信,以及在商用硬件上运行,意味着出现瞬态和更永久性故障的可能性增加。为了保持弹性,必须快速有效地检测故障并进行恢复。

  • 隔离:将应用程序的元素隔离到池中,以便在其中一个失败时,其他元素将继续运行
  • 断路器:处理连接到远程服务或资源时可能需要不同时间修复的故障
  • 补偿交易:撤消一系列步骤执行的工作,这些步骤共同定义最终一致的操作
  • 健康检查:系统实现全链路功能检查,外部工具定期通过公开端点访问系统
  • 重试:通过透明地重试先前失败的操作,使应用程序在尝试连接到服务或网络资源时处理预期的临时故障

安全

安全性是系统能够防止在设计使用之外的恶意或意外行为,并防止泄露或丢失信息。分布式系统在受信任的本地边界之外的 Internet 上运行,通常向公众开放,并且可以为不受信任的用户提供服务。必须以保护应用程序免受恶意攻击,限制仅允许对已批准用户的访问,并保护敏感数据。

  • 联合身份:将身份验证委派给外部身份提供商
  • 看门人: 通过使用专用主机实例来保护应用程序和服务,该实例充当客户端与应用程序或服务之间的代理,验证和清理请求,并在它们之间传递请求和数据
  • 代客钥匙:使用为客户端提供对特定资源或服务的受限直接访问的令牌或密钥

工程应用

资源调度

巧妇难为无米之炊,我们一切的软件系统都是构建在硬件服务器的基础上。从最开始的物理机直接部署软件系统,到虚拟机的应用,最后到了资源上云容器化,硬件资源的使用也开始了集约化的管理。本节对比的是传统运维角色对应的职责范围,在 devops 环境下,开发运维一体化,我们要实现的也是资源的灵活高效使用。

弹性伸缩

过去软件系统随着用户量增加需要增加机器资源的话,传统的方式就是找运维申请机器,然后部署好软件服务接入集群,整个过程依赖的是运维人员的人肉经验,效率低下而且容易出错。微服务分布式则无需人肉增加物理机器,在容器化技术的支撑下,我们只需要申请云资源,然后执行容器脚本即可。

  • 应用扩容:用户激增需要对服务进行扩展,包括自动化扩容,峰值过后的自动缩容
  • 机器下线:对于过时应用,进行应用下线,云平台收回容器宿主资源
  • 机器置换:对于故障机器,可供置换容器宿主资源,服务自动启动,无缝切换
网络管理

有了计算资源后,另外最重要的就是网络资源了。在现有的云化背景下,我们几乎不会直接接触到物理的带宽资源,而是直接由云平台统一管理带宽资源。我们需要的是对网络资源的最大化应用和有效的管理。

  • 域名申请:应用申请配套域名资源的申请,多套域名映射规则的规范
  • 域名变更:域名变更统一平台管理
  • 负载管理:多机应用的访问策略设定
  • 安全外联:基础访问鉴权,拦截非法请求
  • 统一接入:提供统一接入的权限申请平台,提供统一的登录管理
故障快照

在系统故障的时候我们第一要务是系统恢复,同时保留案发现场也是非常重要的,资源调度平台则需要有统一的机制保存好故障现场。

  • 现场保留:内存分布,线程数等资源现象的保存,如 JavaDump 钩子接入
  • 调试接入:采用字节码技术无需入侵业务代码,可以供生产环境现场日志打点调试

流量调度

在我们建设好分布式系统后,最先受到考验的关口就是网关了,进而我们需要关注系统流量的情况,也就是如何对流量的管理,我们追求的是在系统可容纳的流量上限内,把资源留给最优质的流量使用、把非法恶意的流量挡在门外,这样节省成本的同时确保系统不会被冲击崩溃。

负载均衡

负载均衡是我们对服务如何消化流量的通用设计,通常分为物理层的底层协议分流的硬负载均衡和软件层的软负载。负载均衡解决方案已经是业界成熟的方案,我们通常会针对特定业务在不同环境进行优化,常用有如下的负载均衡解决方案

  • 交换机
  • F5
  • LVS/ALI-LVS
  • Nginx/Tengine
  • VIPServer/ConfigServer
网关设计

负载均衡首当其冲的就是网关,因为中心化集群流量最先打到的地方就是网关了,如果网关扛不住压力的话,那么整个系统将不可用。

  • 高性能:网关设计第一需要考虑的是高性能的流量转发,网关单节点通常能达到上百万的并发流量
  • 分布式:出于流量压力分担和灾备考虑,网关设计同样需要分布式
  • 业务筛选:网关同设计简单的规则,排除掉大部分的恶意流量
流量管理
  • 请求校验:请求鉴权可以把多少非法请求拦截,清洗
  • 数据缓存:多数无状态的请求存在数据热点,所以采用 CDN 可以把相当大一部分的流量消费掉
流控控制

剩下的真实流量我们采用不同的算法来分流请求。

  • 流量分配

    • 计数器
    • 队列
    • 漏斗
    • 令牌桶
    • 动态流控
  • 流量限制在流量激增的时候,通常我们需要有限流措施来防止系统出现雪崩,那么就需要预估系统的流量上限,然后设定好上限数,但流量增加到一定阈值后,多出来的流量则不会进入系统,通过牺牲部分流量来保全系统的可用性。

    • 限流策略
    • QPS 粒度
    • 线程数粒度
    • RT 阈值
    • 限流工具 - Sentinel, Zuul, Spring Cloud Gateway, Apisix, Kong, …

服务调度

流量做好了调度管理后,剩下的就是服务自身的健壮性了。分布式系统服务出现故障是常有的事情,甚至我们需要把故障本身当做是分布式服务的一部分。

注册中心

我们网络管理一节中介绍了网关,网关是流量的集散地,而注册中心则是服务的根据地。

  • 状态类型:第一好应用服务的状态,通过注册中心就可以检测服务是否可用
  • 生命周期:应用服务不同的状态组成了应用的生命周期
版本管理
  • 集群版本:集群不用应用有自身对应的版本号,由不同服务组成的集群也需要定义大的版本号
  • 版本回滚:在部署异常的时候可以根据大的集群版本进行回滚管理
服务编排

服务编排的定义是:通过消息的交互序列来控制各个部分资源的交互。参与交互的资源都是对等的,没有集中的控制。微服务环境下服务众多我们需要有一个总的协调器来协议服务之间的依赖,调用关系,K8s 则是我们的不二选择。

  • K8s
  • Spring Cloud
    • HSF
    • ZK+Dubbo
服务控制

前面我们解决了网络的健壮性和效率问题,这节介绍的是如何使我们的服务更加健壮。

  • 发现资源管理那节我们介绍了从云平台申请了容器宿主资源后,通过自动化脚本就可以启动应用服务,启动后服务则需要发现注册中心,并且把自身的服务信息注册到服务网关,即是网关接入。注册中心则会监控服务的不同状态,做健康检查,把不可用的服务归类标记。

    • 网关接入
    • 健康检查
  • 降级:当用户激增的时候,我们首先是在流量端做手脚,也就是限流。当我们发现限流后系统响应变慢了,有可能导致更多的问题时,我们也需要对服务本身做一些操作。服务降级就是把当前不是很核心的功能关闭掉,或者不是很要紧的准确性放宽范围,事后再做一些人工补救。

    • 降低一致性约束
    • 关闭非核心服务
    • 简化功能
  • 熔断:当我们都做了以上的操作后,还是觉得不放心,那么就需要再进一步操心。熔断是对过载的一种自身保护,犹如我们开关跳闸一样。比如当我们服务不断对数据库进行查询的时候,如果业务问题造成查询问题,这是数据库本身需要熔断来保证不会被应用拖垮,并且访问友好的信息,告诉服务不要再盲目调用了。

    • 闭合状态
    • 半开状态
    • 断开状态
    • 熔断工具- Hystrix
  • 幂等:我们知道,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。那么就需要对单次操作赋予一个全局的 id 来做标识,这样多次请求后我们可以判断来源于同个客户端,避免出现脏数据。

    • 全局一致性 ID
    • Snowflake

数据调度

数据存储最大的挑战就是数据冗余的管理,冗余多了效率变低而且占用资源,副本少了起不到灾备的作用,我们通常的做法是把有转态的请求,通过转态分离,转化为无状态请求。

状态转移

分离状态至全局存储,请求转换为无状态流量,比如我们通常会将登陆信息缓存至全局 redis 中间件,而不需要在多个应用中去冗余用户的登陆数据。

分库分表

数据横向扩展。

分片分区

多副本冗余。

自动化运维

我们从资源申请管理的时候就介绍到 devops 的趋势,真正做到开发运维一体化则需要不同的中间件来配合完成。

配置中心

全局配置中心按环境来区分,统一管理,减少了多处配置的混乱局面。

  • switch
  • diamend
部署策略

微服务分布式部署是家常便饭,如何让我们的服务更好地支撑业务发展,稳健的部署策略是我们首先需要考虑的,如下的部署策略适合不同业务和不同的阶段。

  • 停机部署
  • 滚动部署
  • 蓝绿部署
  • 灰度部署
  • A/B 测试
作业调度

任务调度是系统必不可少的一个环节,传统的方式是在 Linux 机器上配置 crond 定时任务或者直接在业务代码里面完成调度业务,现在则是成熟的中间件来代替。

  • SchedulerX
  • Spring 定时任务
应用管理

运维工作中很大一部分时间需要对应用进行重启,上下线操作,还有日志清理。

  • 应用重启
  • 应用下线
  • 日志清理

容错处理

既然我们知道分布式系统故障是家常便饭,那么应对故障的方案也是不可或缺的环节。通常我们有主动和被动的方式来处理:

  • 主动是在错误出现的时候,我们试图再试试几次,说不定就成功了,成功的话就可以避免了该次错误
  • 被动方式是错误的事情已经发生了,为了挽回,我们只是做时候处理,把负面影响降到最小
重试设计

重试设计的关键在于设计好重试的时间和次数,如果超过重试次数,或是一段时间,那么重试就没有意义了。开源的项目 spring-retry 可以很好地实现我们重试的计划。

事务补偿

事务补偿符合我们最终一致性的理念。补偿事务不一定会将系统中的数据返回到原始操作开始时其所处的状态。 相反,它补偿操作失败前由已成功完成的步骤所执行的工作。补偿事务中步骤的顺序不一定与原始操作中步骤的顺序完全相反。 例如,一个数据存储可能比另一个数据存储对不一致性更加敏感,因而补偿事务中撤销对此存储的更改的步骤应该会首先发生。对完成操作所需的每个资源采用短期的基于超时的锁并预先获取这些资源,这样有助于增加总体活动成功的可能性。 仅在获取所有资源后才应执行工作。 锁过期之前必须完成所有操作。

全栈监控

由于分布式系统是由众多机器共同协作的系统,而且网络也无法保证完全可用,所以我们需要建设一套对各个环节都能监控的系统,这样我们才能从底层到业务各个层面进行监控,出现意外的时候可以及时修复故障,避免更多的问题出现。

基础层

基础层面是对容器资源的监测,包含各个硬件指标的负载情况

  • CPU、IO、内存、线程、吞吐
中间件

分布式系统接入了大量的中间件平台,中间件本身的健康情况也需要监控。

应用层
  • 性能监控:应用层面的需要对每个应用服务的实时指标(qps,rt),上下游依赖等进行监控
  • 业务监控:除了应用本身的监控程度,业务监控也是保证系统正常的一个环节,通过设计合理的业务规则,对异常的情况做报警设置
监控链路
  • zipkin/eagleeye
  • sls
  • goc
  • Alimonitor

故障恢复

当故障已经发生后,我们第一个要做的就是马上消除故障,确保系统服务正常可用,这个时候通常做回滚操作。

应用回滚

应用回滚之前需要保存好故障现场,以便排查原因。

基线回退

应用服务回滚后,代码基线也需要 revert 到前一版本。

版本回滚

整体回滚需要服务编排,通过大版本号对集群进行回滚。

性能调优

性能优化是分布式系统的大专题,涉及的面非常广,这块简直可以单独拿出来做一个系列来讲,本节就先不展开。本身我们做服务治理的过程也是在性能的优化过程。
参考《高并发编程知识体系》

分布式锁

缓存是解决性能问题的一大利器,理想情况下,每个请求不需要额外计算就立刻能获取到结果时最快。小到 CPU 的三级缓存,大到分布式缓存,缓存无处不在,分布式缓存需要解决的就是数据的一致性,这个时候我们引入了分布式锁的概念,如何处理分布式锁的问题将决定我们获取缓存数据的效率。常见方案, redis, zookeeper

高并发

了多线程编程模式提升了系统的吞吐量,但也同时带来了业务的复杂度。

异步

事件驱动的异步编程是一种新的编程模式,摒弃了多线程的复杂业务处理问题,同时能够提升系统的响应效率。

总结

最后总结一下,如果有可能的话,请尝试使用单节点方式而不是分布式系统。分布式系统伴随着一些失败的操作,为了处理灾难性故障,我们使用备份;为了提高可靠性,我们引入了冗余。

分布式系统本质就是一堆机器的协同,而我们要做的就是搞出各种手段来然机器的运行达到预期。这么复杂的系统,需要了解各个环节、各个中间件的接入,是一个非常大的工程。庆幸的是,在微服务背景下,多数基础性的工作已经有人帮我们实现了。前文所描述的分布式架构,在工程实现了是需要用到分布式三件套 (Docker+K8S+Srping Cloud) 基本就可以构建出来了。

分布式架构核心技术分布图如下:

知识分享--架构
原图来源:dzone.com/articles/de…

分布式技术栈使用中间件:

知识分享--架构

分布式事务

什么是事务

事务是恢复和并发控制的基本单位。

事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性

原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(durability)持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

TPC (两阶段提交)

知识分享--架构

2pc(两段式提交)可以说是分布式事务的最开始的样子了,像极了媒婆,就是通过消息中间件协调多个系统,在两个系统操作事务的时候都锁定资源但是不提交事务,等两者都准备好了,告诉消息中间件,然后再分别提交事务。

但是我不知道大家看到问题所在没有?

是的你可能已经发现了,如果A系统事务提交成功了,但是B系统在提交的时候网络波动或者各种原因提交失败了,其实还是会失败的。

TPC的弊端

例如长时间锁定数据库资源,导致系统的响应不快并发上不去

网络抖动出现脑裂情况,导致事物参与者,不能很好地执行协调者的指令,导致数据不一致

单点故障:例如事物协调者,在某一时刻宕机,虽然可以通过选举机制产生新的Leader,但是这过程中,必然出现问题,而TCC,只有强悍的技术团队,才能支持开发,成本太高

出处:北京英浦教育

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1

排版再弄好一点就好了

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!