0%

[toc]

服务架构演进史#

1 原始分布式时代#

DCE(Distributed Computing Enviroment)也就是分布式运算环境,是一种很经典原始的设计理念。
DCE的设计主旨:“开发人员不必关心服务是远程还是本地,都能够透明地调用服务或者访问资源”

是很早由OSF指定的分布式技术体系理论,解答了很多问题,例如:

  • 远程服务在哪?——对应服务发现
  • 要部署多少个?——对应负载均衡
  • 请求超时怎么办?——对应熔断、隔离、降级
  • 方法参数和结果如何表示?——对应序列化方式
  • 信息如何传入?——对应传输协议选型
  • 服务权限?——对应认证、授权方案
  • 怎么保证不同机器间的状态最终一致?——对应分布式数据一致性

但最终发现解决这些问题的代价 远超 分布式所带来的收益,在DCE刚提出的年代(80年代),机器资源并没到那个程度, 于是暂时被搁置了。

2 单体系统#

单体系统并不一定就代表是坏的,不好的,需要综合分析:

单体架构的好处#

如果是相同资源的前提下, 单体系统的性能是比分布式要高的。
所有数据都是进程内通信
且开发、部署、测试都基于同一个对象进行处理,更加方便。
单体系统中的代码一般也是做好了分层、分模块的,也是易于敏捷开发和迭代的。

单体架构的坏处#

然而如果单体系统中一部分代码出现缺陷, 可能直接把进程空间耗光,或者直接打崩整个进程,也没有办法针对某个代码模块做单独的升级或者更新。

因此当系统规模较小的时候,单体系统有独特的优势。
系统规模越来越大, 则要求各功能模块能够自治和隔离,减少爆炸范围。
从“追求尽量不出错”到“追求出错是必然”,是微服务架构挑战并取代单体架构的底气所在。

3 SOA——面向服务架构#

SOA(Service-Oriented-Architecture),也叫做面向服务的架构。

它类似于各服务之间协议和通信方式高度一致性,各服务遵循完全相同的消息协议和管理机制

终极目标是总结出一套自上而下的软件研发方法论,最后新厂家要开发系统时,八股文一般照搬SOA架构和实现即可

有一种参考的SOA架构是事件驱动架构:
所有服务连接一个统一的消息管道,从管道中接收统一的事件消息和响应机制。

SOA最终落寞的原因:

  • 过于严格的规范定义带来了过度的复杂性
  • 过于精密的流程和理论需要懂得复杂概念的专业人员才能驾驭

4 微服务时代#

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

  1. 围绕业务能力构建:根据业务划分细粒度的服务和团队
  2. 分散治理: 各服务、各团队对服务质量各自负责,不受其他服务影响,可以各自演进而不用统一规化
  3. 通过服务而不是类库来实现自治
  4. 产品化思维:各服务开发人员关注整个微服务的全方位生命周期,大家不是为了仅仅完成某个功能,而是提供一个持续改进、提升的服务。
  5. 数据去中心化:允许不同的存储方式或者存储位置,但要考虑分布式一致性的成本
  6. 强终端弱管道:即弱化类似SOAP的通信机制(通信管道设计很重,所有服务强制依赖,多了很多不必要的管道功能), 如果有调用需要,提供服务终端的endpoint去调用而不是强制管道使用。
  7. 容错性设计:认为各服务是可以出错的,并不会直接影响所有服务的运行
  8. 演进式设计:不仅可以容错,也可以允许某个服务突然被淘汰
  9. 基础设置自动化:通过CI/CD等自动化构建、发布、运维,减少人工维护成本

微服务相比SOA的优势#

微服务不是SOA的变体或者衍生品,微服务中的每一部分可以自由的选择其中的各种可选方案。
例如远程调用有RMI、Dubbo、Rest,服务发现有ZK、Etcd等。
也正是因为选择很多,对于架构师而言是一个很沉重的挑战

5 后微服务时代(云原生时代)#

用硬件方案替代软件方案#

对于注册、跟踪治理、均衡等问题,能否脱离应用代码实现, 直接在硬件层面来实现?
很早以前行不通,因为硬件基础设置跟不上软件应用的灵活性。
直到docker和k8s的出现。

微服务时代离不开以docker为代表的早期容器化技术

微服务框架springCloud所支持的软件级别微服务治理功能,都能够在k8s中找到硬件层面的替代:

微服务功能 K8s SpringCloud
弹性伸缩 Autoscaling N/A
服务发现 KubeDNS SpringCloud Eureka
配置中心 ConfigMap SpringCloud Config
服务网关 Ingress Controller SpringCloud zuul
负载均衡 Load Balancer SpringCloud ribbon
服务安全 RBAC API SpringCloud security
跟踪监控 Dashboard SpringCloud turbine
降级熔断 N/A SpringCloud Hystrix

通过k8s和相关的虚拟化技术, 与业务无关的技术性问题可以从软件层面剥离,直接在硬件设置层面进行解决!

第二次进化#

当涉及调用链路的切换或者变更, 单纯依靠DNS的硬件层面来做切换还是比较困难的,不如软件方案灵活。
于是引入了“服务网格”的边车代理模式

类似于脱离应用代码,在容器中部署一个通信代理服务器,对于请求的熔断、变更、流量控制都可通过这个代理服务器来管控。这样微服务应用代码中无需再考虑任何和上面这些通信过程相关的逻辑了,全部通过第三方的代理服务器实现!

6无服务(ServerLess)时代#

无服务的定义#

  • 后端即服务: 数据库、存储、日志等业务无关的后端等都存储在云上
  • 函数即服务:供使用者调用的函数/接口都是运行在云端,调用者不需要考虑容量规划和算力问题

无服务的愿景#

  1. 开发者只需要纯粹地关注业务
  2. 不需要考虑技术组件,后端组件现成的,直接使用,不用考虑如何采购和选型
  3. 不用操心运维,运维能力交给云计算厂商。

无服务的缺点#

对于信息管理系统、网络游戏或者对后端接口响应速度较高的应用而言, 无服务并不是最佳选择, 因为无服务的函数肯定不会一直处理高活跃度状态,存在冷启动的情况,对于其响应性能会有影响

总结和思考#

在很多年前的架构或者介绍微服务的书中,基本都是从单体->SOA->微服务。但是现在,随着云原生和 serverless 等新概念的出现,微服务架构的发展已经越来越多元化。对于需要频繁接触云业务的开发者而言,这些新概念显得更加重要。在学习这个章节时,需要关注这些架构演进的原因和理由,比如SOA相比单体的优点和缺点,后微服务又是如何从理念上逐步领先了传统的微服务等。

而《凤凰架构》一书的后半章节内容,更多是聚焦容器、k8s等云原生的重要内容。

像基于容器、k8s的设计,云原生技术将原先软件能力中复杂的内容转移到了硬件层面进行替代,开发者能够用更集中的精力关心业务实现而非服务治理等繁杂的内容。

华为云CCE服务对于部署云业务的服务而言就是云原生时代的重要一环,CCE服务可以面向云原生2.0打造CCE Turbo容器集群,计算、网络、调度全面加速,学习 CCE 服务可以帮助开发者更深入地了解 Kubernetes 和容器技术,从而提高自己的微服务开发和容器化应用部署能力。

而无服务一般也是基于容器实现,对使用者而言基本不感知底层硬件资源,只需要调用即可,大大减少了创建和维护的学习和精力成本,即开即用的理念。 像华为云数据湖探索DLI华为云湖仓构建LakeFormation等都是基于serverless实现的云服务,云用户基于这些serverless服务并结合华为云MRS等大数据底座之后,可以快速运行自己的大数据作业或者数据统一管理等能力,构建数智融合的相关能力。

总而言之,相比于传统的微服务架构,云原生和 serverless 技术更加灵活、高效,能够更好地满足用户的需求。

[toc]

事务处理#

事务有四个经典的特性ACID:

  • 原子性 (Atomicity):事务中的所有操作都必须是原子的,即不可分割或撤销的。在一个事务执行期间,所有的操作都必须同时成功或同时失败,不存在中间状态。
  • 一致性 (Consistency):事务执行的结果必须保证数据库的一致性,即数据库中的数据必须在事务开始和结束时保持一致。
  • 隔离性 (Isolation):事务之间的操作相互隔离,即一个事务的操作不会受到其他事务的影响。
  • 可用性 (Availability):事务执行期间数据库必须保持可用,即可以在任何时候进行访问和修改。

这四个特性ACID中, C其实是目的, AID是手段。
只靠内部(单数据源)可以用AID实现C
但是外部(多数据源)的情况下没法用AID保证C。

1 本地事务#

本地事务是一种最基础的事务解决方案,适用单个服务使用单个数据源的场景。
(注意,对于MyISAM来说,代码层面调用的rollback其实是空操作,引擎内置了事务处理,不需要代码调用rollback)

本地事务的实现原理来自ARIES(基于语义的恢复与隔离算法)

1.1 本地事务如何实现原子性和持久性#

本地事务中, 写入磁盘的过程可能不是原子的,是会崩溃的。
因此要考虑2个异常情况:

  • 未提交事务(调用事务的应用层代码未返回成功),数据还没改完,写了一半崩溃了,导致数据不一致,非原子性
  • 已提交事务(调用事务的应用层代码已经反悔了),但是实际磁盘内容还没写就崩了,导致数据完全没变化,非持久性。

解决方式:
引入commit log, 即将事务对数据的修改先写入commit log,写入成功代表事务成功,写入完成后再写磁盘,如果中途崩溃了就重新写入
等同于熟知的redo-log!
这也是为什么redo-log中是针对某个物理块的修改,目的就是能正确重新,不用考虑我写到哪个位置了,直接全部重刷即可。

但是这样性能太慢,希望能在事务提交完成前提前写入磁盘,但是提前写的话可能会非原子。
这时候就可以引入 undolog, 即触发回滚时,可以讲已操作的数据进行undo回滚操作。
这也是为什么undo-log记录的是一条条不可重复执行的语句。

文中还提了2个特征:
NO-FORCE:事务提交后,不强求立刻全部写入磁盘,可以延迟(commit-log,有这的存在就不着急了)
STEAL:事务提交前,可以先写入一部分数据(undo-log)

1.2 本地事务如何实现隔离性#

隔离性主要就是依赖 数据库锁和数据库隔离级别实现。
书中用作者自己的话简述了一遍从 可串行化 到 可重复读 到 读已提交 到读未提交的 演变过程和实现原理, 也提了以下MVCC等内容。

看完后感觉和我这篇文章讲的内容基本对的上:将数据库9种锁、3种读、4种隔离级别一次性串联起来,用15张图呈现背后数据库事务背后的并发原理
里面有几句比较重要的话:

  • MYSQL/Innodb的“可重复读级别”只能在“只读”事务中解决幻读问题,但是读写事务还是会幻读
  • 读未提交仍然是包含了写锁的。
  • MVCC只是针对读+写的场景做了优化, 如果是写+写是没法优化的,只能用锁。
  • 范围锁不是指对范围内的每一条记录加锁, 而是整个范围内甚至都不能做插入了,即包含了间隙的锁。

2 全局事务#

这里的全局事务指的是 单个服务 使用多个数据源
核心在于是单个服务,不涉及多服务之间的关联, 视角只有单服务。

XA接口是双向的,能在一个事务管理器和多个资源管理器之间形成通信桥梁,协调多个数据源的一致动作,实现全局事务的统一提交和回滚。
Java基于XA接口衍生出的API叫做JTA(javax.transaction.TrancsactionManager和 XAResource)

注意对于全局事务,调用XA的应用者是可以不需要额外处理的,XA会协助做好以下全局事务的响应操作。

2.1 2PC协议(两阶段提交)#

准备阶段
数据源将需要做的事务操作记录在redolog中,完成了持久化,并仍旧持有锁,保持隔离性

提交阶段
协调者收到了所有数据源的回应后, 给所有数据源发送commit指令,如果有任一失败或者超时,则发送abort回滚指令。

2PC的缺点:

  • 协调者单点问题:协调者挂了其他的数据源都会一直在持有锁的情况下等待
  • 准备阶段的性能问题:整个过程将被最慢的那个数据源所拖累,包括如果连接超时也会影响,导致多余的回滚操作
  • 一致性风险:指令丢失、数据源机器崩溃且无法恢复(FLP不可能原理:如果岱机后无法恢复,那么没有任何分布式协议可以达成一致性)

2.2 3PC协议#

为了解决上面的单点问题和 准备阶段的性能问题,引入3PC协议
将准备阶段扩展为:
CanCommit询问阶段
这个阶段就是为了确认各机器是否还是正常的,如果经过确认都是正常负载的状态,再下发事务操作,这样就能避免被网络超时、不良负载拖累的风险

PreCommit预提交阶段
和之前一样,下发事务后各数据源写入重做日志

DoCommit阶段
这个过程有一个优化, 如果协调者挂了, 数据源迟迟无法收到,就会默认进行事务提交(注意并非默认回滚)

3PC仍然存在网络问题导致的一致性问题。
另外对于2PC,

3 共享事务#

书里说这个不常用,不写了,类似于提供共享的数据连接给不同进程使用,使用同一个事务逻辑

4 分布式事务#

4.1 CAP理论#

  • C一致性: 各节点同一时刻响应结果一致(数据一致)
  • A可用性: 各节点随时随地都要能正常响应,不能存在延迟或者阻塞的情况(快速响应)
  • P分区容忍性:某个节点挂了,其他节点能代替服务

科学家证明CAP只能同时满足2个

  • 放弃分区容忍性P: 意味着分布式系统不成立。这种情况只有类似于Oracle RAC这种数据通过磁盘共享的情况, 虽然是多个实例,但不算分布式。 基本是分布式系统一定都会包含P,否则没有考虑分布式事务的意义
  • 放弃可用性A: 这样可能因为数据同步过程的延迟或者超时,造成系统长时间不可用, 这是不能容忍的
  • 放弃一致性C: 数据有短暂不一致的响应。 放弃C是当前分布式系统的主流选择。 一般都是允许数据在中间过程出错, 但允许在输出时能够修正古来。 因此我们放弃了强一致性,追求“最终一致性”

4.2 BASE(可靠性队列)#

BASE指 基本可用性 + 柔性事务 + 最终一致性, 或者叫做最大努力交付

实现原理是引入一个消息队列,当某个事务动作发生异常时, 在轮询阶段不断重试,直到成功

要求满足幂等性

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

4.3 TCC事务#

TCC用于解决BASE中无法解决的隔离性问题,因为BASE不允许失败,一定会执行,如果涉及了超售等问题将无法解决。

  • Try: 尝试执行阶段, 会先进行业务可执行的检查,并提前预留好需要扣除的资源(类似于冻结那一块资源,但没有实际去扣)
  • Confirm:执行阶段,这个过程不再做任何检查,直接执行。如果网络出错等缘故则一直重试,符合幂等
  • Cancel:执行完成,释放try阶段中预留的业务资源,也要符合幂等。

和2PC很类似,但TCC是在用户应用代码层面实现的,业务侵入性很高, 而2PC是基础设施层面提供的。

4.4 SAGA事务#

TCC中的缺点在于 try阶段和cancel阶段依赖用户代码实现,但如果你的业务不支持这种操作就麻烦了,比如扣款动作是某个银行做的, 他不支持预扣款的功能。

SAGA会把事务拆成很多个小事务T,按顺序执行, 并根据情况给事务T失败时选择是继续重试T, 还是用补偿事务C来替代重试

这样像银行无法预扣款也无法撤销转账的问题,可以改成自己系统来做中间者做转账操作。

也要引入SAGAlog机制避免长串事务执行过程中崩溃

总结#

其实学习本文时,更重要的是思考为什么要学习这么多的事务概念和原理。在云原生时代。

像华为云提供的很多数据库类型的云服务也都支持了分布式事务的能力,例如

  • 华为云RDS分布式事务
    基于2PC原理实现的MSDTC分布式事务协调器

  • 华为云DDM事务模型:
    这里面的分布式事务模块基于 MySQL XA 协议实现,XA 协议是对 2PC(Two Phase Commit) 事务模型的一种实现。

  • 华为云DWS分布式事务:
    基于强一致性的CSN事务机制,使用GaussDB分布式框架下的一个组件GTM以及从中获取到的CSN值来处理事务。

毕竟云原生应用程序通常由多个微服务组成,因此需要在微服务之间进行通信,并保证事务的一致性。在这种情况下,就需要一种适用业务场景的分布式事务解决方案。比如TCC可以在微服务之间实现分布式事务的ACID特性,而且相对于其他方案,TCC更轻量级,对性能影响更小,但其他方案也有各自的适应场景。

因此,分布式事务与云原生技术有很强的关联,可以帮助云原生应用程序实现高效的分布式事务处理。当使用某个关系型数据库产品时,关注他们的分布式事务处理能力并分析是否适合自己当前的业务场景,是非常重要的,也是本书该章节值得学习的一个理由。

[toc]

可靠通信#

1 零信任网络#

把安全措施集中在部署各个区域的边界之上(例如VPN、DMZ、防火墙、内网、外网)的安全模型称之为 基于边界的安全模型

本身是很合理的模型,缺陷是一旦其中一台内网机器被攻破,可能连锁反应一般能攻破内网的所有机器。

9.1.1 零信任安全模型的特征#

零信任安全的中心思想是: 不应当以某种固有特种来自动信任任何流量,除非得到了来自请求来源的身份凭证,否则一律不会有默认的信任关系。

详细观点如下:

  • 不等于放弃在边界上的保护设施(基本的边界安全仍然要保证)
  • 身份只来源于服务。(不适用IP、机器码等来识别身份, 因为云原生时代服务可能经常会扩容、伸缩,ip具有一定的局限性)
  • 服务之间没有固有的信任关系(主要是为了阻止攻击者通过服务节点中的代码漏洞来越权调用其他服务,减小攻击范围)
  • 集中、共享的安全策略实施点,安全需求可以从微服务的应用代码下沉到云原生的基础设施里
  • 受信的机器运行来源已知的代码, 所有代码应该通过CI/CD标准发布,不可以手动编译传输不可信的程序包。
  • 自动化、标准化的变更管理,避免手动操作,应该自动化。
  • 强隔离性的工作负载。 指的就是容器化相关。

9.1.2 谷歌的零信任安全理论实践#

这里讲述了当时谷歌怎么基于上面那个模型, 详细再出了一片论文讲怎么实现。
在传统时代基本没法做到,复杂度太高, 但是云原生时代让那些设计理念成为了可能。

9.2 服务安全#

9.2.1 建立信任#

依赖公开密钥基础设置PKI, 也就是TLS的基础

9.2.2 认证#

1.服务认证#

2.用户认证#

9.2.3 授权#

根据身份角色进行权限访问控制, RBAC。


以上认证、授权这2章节都介绍了Isto和spring cloud(spring security)的实现,内容比较多,就不笔记了。

[toc]

访问远程服务#

1 远程服务调用#

这一个章节主要讲解rpc的设计理念和发展历史。
先是讲解了IPC(进程间通信)所需要的各个必要因素
接着解释RPC 是IPC的一种特例(这是最初科学家们的想法)
但PRC存在很多可靠性的问题,并不能直接等同IPC的扩展。

接着提出了RPC的三个基本问题:

  1. 如何表示数据
    即序列化协议, 有grpc的proto-buffer、json、xml、java-rmi基于java自带序列化之列的
  2. 如何传递数据
    基于什么网络协议传输
    java-rmi的远程协议
    JSON-RPC协议
    SOAP协议(web service 简单对下访问)
  3. 如何表示方法
    如何定义方法,如何在不同语言、不同系统环境中保证 方法签名唯一。
    需要对接口描述定义语言有一个选型

再后面讲解了rpc的发展历史
CORBA的使用过于复杂,被抛弃
SOAP使用xml很简单,但是性能奇差
可以看出RPC想同时满足简单、普适、高性能是很难的
于是得出一个结论:

不存在完美的RPC框架

最近几年的RPC框架更倾向于往高层次、插件化发展。
即不再独立解决RPC的所有问题,而是提供一些扩展点由用户自己选择(比如dubbo)
可以任意切换json或者fastjson等序列化方式
可以切换传输协议等。

2 REST#

rest并不是一种远程调用协议, 他只能说是一种风格。
REST的传输效率提升潜力有限,但是可以简化调用。

2.1 REST核心概念(基于HTTP超文本传输协议)#

  • 资源
    资源指你希望获取或者修改的东西、信息本身,他的表现形式可以不同,但是里子是相同的
  • 表征
    就是表现资源的形式,用html还是pdf来返回
  • 状态
    指的是服务端对某次会话是否持有状态。
    例如读下一篇文章时,当前文章id由服务端存储还是浏览器存储,这就是有状态和无状态的区别。
  • 转移
    就是服务端提供的行为逻辑, 通常叫做 资源的转移
  • 统一接口
    就是HTTP协议里提供的GET\HEAD\POST\PUT\DELETE\TRACE\OPTIONS这七种操作,通过这些操作触发转移
  • 超文本驱动
    浏览器的行为经常是通过服务端返回的url或者各种信息触发了驱动行为
  • 自描述消息
    为了方便客户端识别表征,返回类似于Content-Type等内容,方便用何种字符集处理

2.2 REST核心设计原则#

  1. 客户端与服务端分离
    服务端不处理渲染, 由客户端来处理
  2. 无状态
    尽可能希望服务端不要存储rest会话状态,达到分布式的高价值回报。
    但是不太可能特别是客户端持有会话数量级很大的情况下,所以仍旧会持有一定状态
  3. 可缓存
    服务端的应答中要直接或者间接告知客户端是否可以缓存,缓存多久
  4. 分层系统
    客户端不需要知道是否真的连接到了最终的服务器
    代表是CDN内容分发网络
  5. 统一接口
    面向资源编程
    系统设计时聚焦在资源而不是行为上
    例如面向行为时, 登录就是login接口,注销就是logout接口
    而面向资源时,登录就是PUT Session, 注销就是DELETE Session
  6. 按需代码
    客户端的代码可以有一部分让服务端发回进行装载。

2.3 REST的好处#

  • 降低服务接口的学习成本
  • 资源天然具有集合与层次接口
    这样很多资源可以很容易组合在一起并让使用者想到接口url是什么
    例如获取 用户 icyfenix的购物车中的第2本书,这个是有层次的,那么接口就是GET /user/icyfenix/cart/2
  • REST绑定于HTTP协议,HTTP又是大家非常熟悉的

2.4 RMM(Richardson提出的restful成熟度模型)#

第0级:完全不rest#

提供的api定义里总是包含了各种动作,例如
/queryXXX
/applyXXX
类似于RPC的行为接口
坏处是一旦要提供其他对XXX的操作,必须重新编写接口,无法对XXX的查询能力进行复用!

第1级:开始引入资源的概念#

api的endpoint定义应该围绕名词+资源id而不是动词来定义。
POST /doctors/mjones 获取医生的档期
POST /schedules/{id} 触发对这个id的调度

第2级: 引入统一接口,映射到HTTP方法上#

上面的method都是POST,实际上可以把HTTP方法的method、返回码都给利用上

  • 对一个资源的增加用POST,删除用DELETE,更新用PUT
  • 依赖HTTP的返回码定义资源可能的异常情况。例如201代表创建成功,409代表冲突(被人抢先预约)
  • 利用HTTP自带的认证和授权信息。

第3级:超文本控制,转移行为通过响应控制#

第2级里, 所有接口的定义仍然需要使用者自己查询文档来记忆和应用
实际上应该只需要一个操作起始入口, 例如获取医生的档期接口
这个接口要返回的body里,已经告知了对应资源的名称,例如档期资源的key就叫做schedules
那么就能马上知道下一个接口查询用schedules了!
这样代码里可以做好适配,当你要把档期的key名做修改时,客户端代码根本不用变动!

2.2.4 REST的不足和争议#

  1. restful面向资源编程只适合做CRUD,不适合过于复杂的业务逻辑
  • 面向过程编程,以算法和处理过程为中心,这符合计算机世界中主流的交互方式
  • 面向对象编程,将数据和对象行为统一起来,因为这符合现实世界的主流交互方式
  • 面向资源编程,数据作为主体,行为看成统一接口,为了符合网络世界的主流交互方式
  1. rest不利于事务支持
    作者不同意这个观点, 认为不会有阻碍,取决于系统的事务设计
  2. rest没有传输可靠性支持
    虽然确实没有类似于重发等机制的保证,但rest接口一般尽可能要求幂等性,来做到应用代码做重发时可以不用担心重复的问题
  3. 缺少对资源做部分或者批量处理的能力
    rest语义里不能涵盖这种情况,得定义特殊的接口或者参数,那么低3级里面就不能涵盖了。

相关思考#

基于REST的规范,调用者可以非常快速地理解自己需要的接口内容是什么样的,例如华为云当前的很多云服务公开接口都会基于REST理念进行开放, 并且各云服务的开放接口都会集成到API Explorer华为云SDK中心供开发者直接调用,这些平台提供了丰富的接口文档和示例代码,帮助开发者更快地上手和使用 REST 接口。

相信未来REST 规范将会变得更加流行和普及。随着云计算和大数据技术的不断发展,REST 接口将会被广泛应用于各种领域,例如医疗、金融、电商等。REST 接口的开放性、可扩展性和易用性,将会为开发者带来更加高效、便捷和可靠的开发体验。

[toc]

透明多级分流系统#

对系统流量进行规划, 要注意以下2个原则

  1. 尽可能减少单点部件, 或者减少到达单点部件的流量或者作用
  2. 奥卡姆剃刀原则,确定有再有必要的时候才去使用,避免过度设计

1 客户端缓存#

即对于某些资源, 在客户端就做缓存,客户端不去重复请求。

1.1 强制缓存#

类似HTTP协议里在header里用到的两种标签,且都是服务端强行控制的,基于时间的

  1. Expires
    服务端直接返回数据不会变动的截止时间。
    缺点:受限于客户端本地时间、无法表示不缓存除非强制改时间戳、无法表示是否是私有资源(避免私有资源被其他节点缓存)
  2. Cache-Control
    这个请求头使用max-age、private、no-cache等标签解决了Expires里的3个缺点。

2.2 协商缓存#

协商缓存需要考虑是否真的发生变化。 协商和强制可以共同存在,即强制失效的时候就可以用上协商。
协商缓存不仅存在于地址输入、跳转,也存在F5中(但如果Ctrl+F5强制刷新则会让缓存失效)

  1. Last-Modified
    告诉客户端资源的最后修改时间, 客户端再次请求时也会对这个时间做修改
    如果服务端发现在那个时间之后资源未变动,返回304 Not Modified
    如果有变动,就返回OK,并携带完整的资源
  2. ETag
    需要对资源计算哈希值,客户端发请求也会带上自己存的ETag,每次会比对资源的哈希值是否一致,不一致则返回新资源。
    Etag是一致性最强的本地缓存机制,但也是性能最差的。

3 传输通道优化#

本章节大部分以熟知的HTTP协议作为主要传输通道协议,讲解如何进行优化

3.1 连接数优化#

HTTP是基于TCP的,每次都是重新建立一个TCP连接。 因此前端开发人员开发了很多小优化,来减少请求次数,例如雪碧图、分段文档、合并Ajax请求之类的。

HTTP1.0里的长连接(keep-alive连接复用)为什么不能解决这个问题?
因为存在队首阻塞问题,本质上是基于FIFO复用连接, 1个请求卡住了,后面9个请求都阻塞住了,但如果同时支持返回,在顺序混乱的情况下无法正常处理

HTTP2.0的多路复用解决了这个问题

  • 以帧作为最小粒度单位,每个帧都携带流ID识别是哪个流
  • 客户端可以很容易在不同流中重组HTTP请求和响应报文

3.2 传输压缩#

HTTP很早就支持GZip压缩来减少大资源的传输量

HTTP1.0中, 持久连接和传输压缩无法一起使用, 因为压缩后无法识别资源是否传输完毕。

HTTP1.1中引入了“分块传输编码”,来进行资源结束的判断。

3.3 用UDP来加快网络传输#

HTTP/3中,希望能替换掉HTTP on TCP的依赖、
谷歌推出了快速UDP网络连接, 即QUIC

  • QUIC以UDP为基础, 可靠传输能力由自己实现
  • QUIC专门面向移动设备支持, 移动设备的ip地址经常会切换,使用ip作为定位不合适, 因此提出了连接标识符来保持连接。
  • 对于不支持QUIC的情况,支持回退为TCP连接,实现兼容

4 内容分发网络CDN#

CND可以解决 互联网系统跨运营商、跨地域物理距离所导致的时延问题,为网站流量带宽起到分流、减负的作用。
主要包含以下4个工作部分

4.1 路由解析#

用户的静态资源请求访问CDN是通过DNS解析来完成的,甚至可能一个网站会有各种不同地域的CDN域名解析地址返回, 通过你的路由配置会自动选择符合地域的ip地址

4.2 内容分发#

如何分发内容有两种方式:

  1. 主动分发, 通过CND服务商提供的接口主动推送自己的资源,这样你需要额外编写资源推动的代码。大型活动例如双11会优先考虑主动分发预先准备资源。
  2. 被动回源, 由用户访问触发,当发现没有资源时,CDN会去源站请求并返回,则用你不需要新写相关代码,只要在CDN那边支持回源你的源站即可。小型站点基本都是用这个方法。

如何更新资源有两种方式:

  1. 超时被动失效,CDN的资源都有有效期,超时了就回源获取
  2. 手工主动失效, CDN服务商提供缓存失效接口,主动触发失效并进行被动回源更新。
    现在一般是1和2结合使用,二者不冲突

5 负载均衡#

负载均衡有两种大类

  • 四层负载均衡
    指的是计算机七层模型中四层及以下的均衡策略结合
    即 数据链路层 + 网络层 均可做均衡
  • 七层负载均衡
    指的是在应用层通过实际代码做均衡

5.1 数据链路层负载均衡(四层负载均衡)#

  • 通过链路层上的均衡器替换MAC地址,进行链路层的均衡
  • 各负载节点的IP是一样的(相同的虚拟IP)
  • 返回时无需经过均衡器,直接返回即可(因为目标ip、源ip基本没变)

缺点:
必须是同一个子网内,无法跨VLAN,只能作为最接近数据中心的均衡器

5.2 网络层负载均衡(四层负载均衡)#

有两种方式:

IP隧道模式#

均衡器在IP报文外面包了一层新的header,header里指定了目标机器的实际ip或者小网ip。 接收机器要支持解header,且同样要求作为返回的虚拟ip是一致的,也是直接返回无需经过均衡器。
缺点:

  1. 用到的服务器都要支持隧道解包能力(linux系统现在都支持)
  2. 虚拟ip仍然有较大限制,需要人工介入管理众多机器

NAT模式#

NAT模式中,就是进行真正的ip转换, 且返回时也要返回给NAT进行ip转换,这样只需要针对NAT进行人工管理即可。
缺点在于NAT容易成功性能瓶颈

SNAT会修改源IP改为NAT的ip, 可以做到对业务真正透明, 但是代价是如果需要对源IP做限制时容易有问题, 因为所有的来源ip都是一样的了。

5.3 应用层负载均衡(七层负载均衡)#

也叫做七层代理(应用层代理),因为这个负载均衡属于反向代理(即部署在服务端的代理,对客户端不感知)

不适合做下载站、视频站等流量应用
如果瓶颈在服务计算能力,则可以考虑做应用层均衡

七层代理除负载均衡外的其他功能:

  • 支持做CDN类似的缓存能力
  • 施行智能化路由,根据URL或者特定用户做特殊服务
  • 抵御安全工具,提前过滤攻击报文
  • 链路治理

5.4 负载均衡策略#

  • 轮询均衡
    轮流分配,从1到N再到1
    适用于所有服务器硬件配置完全相同,服务请求需要相对均衡
  • 权重轮询
    根据服务器权重分配周期内的轮询次数
  • 随机均衡
    适用于数据量足够大的相对均衡分布
  • 权重随机均衡
    提升权重高的随机率
  • 一致性哈希均衡
    适用于服务器经常可能掉线或者加入,可以避免哈希键全部更新的情况
  • 响应速度均衡
    定期探测各个服务器的响应速度,根据速度分配权重
  • 最少连接数均衡
    根据连接数分配权重, 适用于长时处理服务例如FTP等

  • 软件均衡器包括基于操作系统内核的LVS、 基于应用程序的Nginx、KeepAlive、HAProxy
  • 硬件均衡器包括F5、A10等公司提供的硬件负载均衡产品

6 服务端缓存#

引入缓存的理由:

  • 减缓CPU计算压力
  • 缓存IO压力
    这2个缓解只是能峰值时的压力缓解,如果普通的响应都很慢,那就算用了缓存也意义不大。

6.1 缓存的几个属性#

缓存需要选型,选型时需要根据实际场景选择你匹配的缓存熟悉

吞吐量#

JDK8改进后的ConcurrentHashMap是并发场景下吞吐量最高的缓存容器,但除了吞吐量其他的能力就很弱了。

缓存状态更新思路:

  • GuavaCache: 同步处理机制,在访问数据时一并更新,分段加锁减少竞争
  • Caffeine:异步日志提交机制,参考数据库日志,并且还有环形缓冲区容忍有损失的状态变更,读性能非常快, 使用多读少写的情况。

命中率和淘汰策略#

基础的三种淘汰方案:

  • FIFO:先进先出,简单实现,但对于高频访问的缓存命中率低,越常用到越可能先进入队列
  • LRU:优先淘汰最久未被访问,基于时间, 用HashMap+链表List实现,但每个缓存都要记录时间,且可能淘汰短期内正好没访问且价值高的数据
  • LFU:优先淘汰最不频繁使用,基于使用次数,可以解决LRU的缺点。
    自身缺点:
  1. 每个缓存专门维护要更新次数的计数器,维护开销大还有加锁问题(LRU的更新时间不需要考虑加锁,直接覆盖最新即可)
  2. 如果某个缓存某时期访问很高,比其他缓存高了一个数量级,后面不再使用,想淘汰很困难

为了解决上面2个缺点,有2个新的策略:

  • TinyLFU: 解决修改计数器的开销问题, 采用Sketch分析访问数据,用少量数据估计全体数据特征,采用滑动时间窗、热度衰减等处理
  • W-Tinfy-LFU: 结合了LRU+LFU的特点, 考虑热度和时间。

分布式能力#

分布式缓存介绍了复制式缓存JbossCache以及集中式缓存Memcached。

jbosscache的缺点在于写入性能太差,容易因为网络同步速度跟不上写入速度,导致内存中积累过多待发对象引发omm

memcached是C语言实现的,好处在于读写性能高,缺点在于数据结构太过紧密,非常依赖序列化做跨语言传输,如果100个字段中的1个字段发生更新,要把100个字段都发出去更新

redis基本打败了各种分布式缓存,成为首选。

对于redis等分布式缓存, 是不会追求一致性C的
如果一定要一致性C, 那应该选用zk或者etcd等分布式协调框架(但他们一般就不会拿来做缓存,因为高并发下吞吐量太低,没有可用性)

进程内缓存和分布式缓存通常结合使用,但容易出现二者数据不一致,写维护策略导致缓存对开发者而言不透明。
一种设置原则是 变更以分布式缓存中的数据为主,访问以进程内缓存的数据优先。
大致做法是数据发生变动时, 在分布式缓存内推送通知, 让一级缓存失效。
访问缓存时,提供封装好的一二级联合查询接口, 让开发者对一二级缓存不感知。

6.2 缓存风险#

缓存穿透#

大量不存在的缓存打进来
要么是支持对不存在的数据缓存空值
要么是引入布隆过滤器

缓存击穿#

同一时间瞬间涌现很多请求,访问数据库有但是缓存里没有的数据,此时可能直接打穿数据库(缓存生效是有延迟的)
可以是用锁、队列来完成同步
对于热点缓存,提前预处理或者配置策略

[toc]

可观测性#

什么是微服务的可观测性?

微服务的可观测性是指在微服务架构中,系统的各个组件和服务能够被有效地监控、度量和分析,以便运维人员和开发团队可以了解系统的运行状况、性能指标和故障情况。可观测性能够提供对每个微服务的实时监控和分析,以及对整个系统的全局视图。

微服务的可观测性有3个具体的方向,分别是

  • 事件日志(记录离散事件,主流是ELK)
  • 链路追踪(排查故障用发生在哪个环节用的, k8s下的prometheus相对有优势)
  • 聚合度量(汇总生成一些统计信息,涉及很多定制化的内容,暂时没有一家独大的情况)。

1 事件日志#

对于事件日志, 现在主流是使用ELK。即 Elasticsearch、Logstash 和 Kibana的结合。
每个微服务可以使用 Logstash 将其日志数据发送到 Elasticsearch 进行存储和索引。然后,通过 Kibana 可以轻松地搜索、过滤和分析这些日志数据,以获得对整个系统的实时可视化监控和故障排查能力。

1.1 如何、怎样输出日志#

对于打印事件日志,作者建议遵循以下这些原则:

  • 避免打印敏感信息
  • 避免引用慢操作(应该都是直接能取到的信息,避免要调远程接口或者耗时较久的计算)
  • 避免打印追踪信息(这种信息交给链路跟踪做), 包括打印请求体之类的
  • 避免误导 他人(例如异常已经吃掉了处理了,后面却还往外打异常日志别人)
  • 尽量记录处理请求时的traceId
  • 记录关键事件
  • 记录启动时的配置信息便于观察配置中心对配置的修改是否生效

1.2 收集和缓冲#

所有日志需要集中收集,因为经常是分布式系统。
相关收集器: Logstash、filebeat。

Logstash适用于复杂的数据处理和转换场景,对于数据管道的灵活性和功能要求较高。

而Filebeat更适合轻量级的日志收集和实时传输,对于资源消耗和简单配置要求较低。

日志可能很大,要保证完全一致性很难,可以加kafka或者redis作为日志的缓冲层

1.3 加工与聚合#

日志一般是非结构化的字符串, 需要有工具处理成结构化的数据,例如处理成json输出到elasticsearch中。
Logstach就能够支持结构化处理, 且还支持引入各种聚合插件。

比如下面这3句日志:

1
2
2023-05-14 10:30:15 INFO: User 'john' logged in successfully.
2023-05-14 10:31:02 ERROR: Failed to process request. Error message: Timeout expired.

通过编写好Logstach的配置文件,然后执行logstach处理命令后,即可生成如下的json结构:

1
2
3
4
5
6
7
8
9
10
{
"timestamp": "2023-05-14 10:30:15",
"loglevel": "INFO",
"message": "User 'john' logged in successfully."
}
{
"timestamp": "2023-05-14 10:31:02",
"loglevel": "ERROR",
"message": "Failed to process request. Error message: Timeout expired."
}

后续即可导入到es中进行进一步的分析和检索了。

1.4 存储与查询#

存储和查询的选择大部分是elasticsearch(大部分时候简称ES)
为什么?

  • 从数据特征看,日志是基于时间的数据流,很容易让es按时间范围做索引。
  • 数据价值角度看, 日志只会以最近的数据为目标,因此可以进行冷数据和热数据的分开存储。
  • 从数据使用角度看,分析日志依赖全文检索 和 即席查询(近实时性)正好是es的强项。

es和kibaba搭配使用还能可视化查看。kibaba是一个

2 链路追踪#

2.1 追踪和跨度#

这是2个概念。

  • Trace追踪:从客户端发起请求抵达系统边界开始,记录请求流经的每一个服务,直到客户端返回响应。

  • Span跨度: 每次开始调用服务前都会预先埋入一个调用记录,记录时间点、时长。
    Span必须简单, Trace中包含多个span。

需要达成以下4个目标:

  • 低性能损耗
  • 对应用透明
  • 随应用收缩
  • 持续的监控

2.2 数据收集#

有三种方式

  • 基于日志的追踪思路。 将Trace、span输出到日志中,然后日志收集后进行分析。
    优点:实现简单,侵入性小。
    缺点:日志本身不追求决定的连续性和一致,容易失真。
    典型代表: Spring Cloud Sleuth

  • 基于服务的追踪。是最常见的追踪方式。 通过某些手段给应用注入探针,例如java Agent,然后将调用过程发送请求给追踪系统。
    有比较强的侵入性,但是追踪的精确性和稳定有保证
    典型代表:SkyWalking\zipkin\pinpoint

  • 基于边车代理的追踪。
    依赖服务网格的实现, 追踪数据通过控制平面上报,避免了追踪影响通信和日志。但是服务网格还不太普及,而且只能是服务调用层面无法细化到方法层面。
    典型代表: Envoy

2.3 追踪规范化#

在过去,有几个不同的追踪规范和工具被开发出来,其中最著名的是OpenTracing和OpenCensus,而这两者已经合并为一个名为OpenTelemetry的项目。

OpenTelemetry是一个全面的观测框架,集成了追踪、度量和日志记录。它提供了一组跨语言的API和库,可以在应用程序中插入代码以收集观测数据。

3 聚合度量#

度量的目的是揭示系统的总体运行状态。 经过聚合统计后的高维度信息,以最简单直观的形式来总结复杂的过程,为监控预警提供决策支持。

本章节主要介绍了Prometheus的功能,是云原生时代的事实标准。

Prometheus是一款开源的系统监控和警报工具,最初由SoundCloud开发,并于2012年发布。

它专注于收集、存储、查询和可视化时间序列数据,并提供强大的监控和警报功能。 以下会介绍这款的3个核心能力

3.1 指标收集#

如何定义指标(有哪些类型):

  • 计数度量器
  • 瞬态度量器
  • 吞吐率度量器
  • 直方图度量器
  • 采样点分位图度量器
    prometheus支持计数、瞬态、直方图和采样点分位。

如何把指标告诉服务端

  • 拉取式
  • 推送式
    prometheus选择拉取式, 并且支持有限兼容推送式。

对于网络协议, promethus只支持HTTP但提供了强大的客户端包供使用。

3.2 存储查询#

对于度量数据的存储, 应该优选“时序数据库”
即存储跟随时间而变化, 并且以时间来建立索引。

时序数据库的特点:

  • 以日志结构的合并数代替B+树
  • 设置激进的数据保留策略, 用过期时间TTL删除相关数据
  • 对数据采样节省空间

prometheus就是内置了时序数据库

3.3 监控预警#

prometheus提供了专门用于预警的alert Manager。

通过将Prometheus与Alertmanager集成,用户可以根据监控数据的变化和指定的规则,及时收到关键问题的警报。Alertmanager提供了一个集中化的警报处理和管理平台,使用户能够根据自己的需求定制和配置警报路由,并将警报发送给适当的团队成员和通知渠道,以便及时响应和解决问题。

[toc]

流量治理#

1 服务容错#

1.1 容错策略#

文章中介绍了故障转移、快速失败、安全失败、沉默失败、故障恢复、并行调用、广播调用等几种容错策略,我用表格的形式直观呈现一下这几种策略的区别,方便理解和选型:

容错策略 介绍 优点 缺点 应用场景
故障转移Fail-over 服务出现故障时,自动切换到其他服务副本获取结果 系统自动处理,调用者不感知第一次失败 会增加调用时间,在有超时时间的限制下即使第二次调用成功也会造成超时。 调用幂等服务(可支持多次调用),对调用时间不敏感
快速失败Fail-fast 出错的话就直接报错抛异常给调用者自行处理。 调用者对失败的处理完全自主可控。 调用者必须处理增加了开发成本,不可以随意外抛。 调用非幂等的服务,超时阈值比较低(因此不允许失败后做其他的处理)的情况
安全失败fail-safe 不抛异常,只记录日志 不影响主链路逻辑 只适合旁路调用。 调用链中的旁路服务
沉默失败fail-slient 失败后暂停一段时间服务,避免引起服务间雪崩 控制错误避免影响全局 出错的地方可能会在一段时间内不可用 容易引发频繁超时的服务
故障恢复 fail-back 调用失败后,把错误放入一个异步队列,延迟恢复。 自动重试且不会容易引发超时,也不影响主路逻辑 重试任务如果短时间内堆积过多,也会造成重试失败或者超时 调用链中的旁路服务,对实时性要求不高的主路服务。
并行调用forking 一开始就对多个服务副本发起调用,只有有任何一个返回成功,就宣告成功。会额外消耗资源可能大部分都是无用调用,适合资源充足且对失败容忍度低的高敏感场景 能够快速获得响应,提高系统的吞吐量 可能会浪费大量的资源 适合资源充足且对失败容忍度低的高敏感场景
广播调用broadcast 一开始就对多个服务副本发起调用,必须所有都返回成功才宣告成功,常用语刷新分布式缓存。只适合批量操作的场景,失败概率高。 能够同时快速更新多个副本的状态,提高系统的数据更新速度 失败概率高 只适合批量操作的场景,例如刷新分布式缓存等

1.2 容错设计模式#

1.断路器模式#

即服务中发请求的地方都通过一个断路器模块来转发发送
当10秒内请求数量达到20,且失败阈值达到50%以上(这些参数都可以调整), 则认为出现问题, 于是主动进行服务熔断, 断路器收到的请求自动返回错误,不再去调用远程服务, 这样可避免请求线程各种阻塞,能及时返回报错。
中间会保持有间隔的重试直到恢复后,关闭断路。

2.舱壁隔离模式#

如果一个服务中,可能要同时调用A\B\C三个服务,但是却共用一个线程池。
如果调用C服务超时,而调用C的请求源源不断打来,会造成C服务的请求线程全在阻塞,直接把整体线程池给占满了,影响了对A\B服务的调用。

一种隔离措施是对每个调用服务分别维护一个线程池。缺点是额外增加了排队、调度、上下文切换的开销,据说Hystrix线程池如果开启了服务隔离,会增加3~10ms的延迟。

另一种隔离措施是直接自己定义三个服务的计数器,当服务线程数量到达阈值,自动对这个服务调用做限流。

3.重试模式#

故障转移和故障恢复这2个策略一般都是借助重试模式来处理的,进行重复调用。
重试模式应该满足以下条件才能使用:

  • 仅在主路核心逻辑的关键服务上进行同步的重试, 而非关键的服务
  • 只对瞬时故障进行重试,对于业务故障不进行重试
  • 只对幂等型的服务进行重试

重试模式应该有明确的终止条件,例如:

  • 超时终止
  • 次数终止

重试一定要谨慎开启, 有时候在网关、负载均衡器里也会配置一些默认的重试, 一旦链路很长且都有重试,那么系统中重试的次数将会大大增加。

2 流量控制#

流量控制需要解决以下3个问题

  • 依据什么指标来限流
  • 如何限流
  • 超额流量如何处理

2.1 流量统计指标(依据什么指标来限流)#

  • 每秒事务数TPS: 事务是业务逻辑上具有原子操作的业务操作,对于对买书接口而言, 买书就是一个事务, 背后的其他请求是不感知的。
  • 每秒请求数HPS: 就是系统每秒处理的请求数, 如果1事务中只有1个请求, 那么TPS=HPS, 否则HPS>TPS
  • 每秒查询书QPS: 是一台服务器能够响应的查询次数。 对于单节点系统而言,QPS=HPS,对于一个分布式系统而言HPS>TPS

通过限制最大TPS来限流的话,不能够准确反映出系统的压力, 因此主流系统倾向使用HPS作为首选的限流指标。

2.2 限流设计模式(如何限流)#

流量计数器模式#

统计每秒内的请求数是否大于阈值
缺点:

  1. 每秒是基于1.0s-2.0这样的区间统计, 但如果是0.5-1.5 和1.5-2.5分别超出阈值,但是1.0-2.0没有超过阈值,则会出现问题。
  2. 每秒的请求超过阈值,也不代表系统就真的承受不住,导致五杀

滑动时间窗模式#

滑动时间窗专门解决了流量计数器模式的缺点。
准备一个长度为10的数组,每秒触发1次的定时器
①将数组最后一位的元素丢弃,并把所有元素都后移一位,然后在数组的第一位插入一个新的空元素
②将计数器中所有的统计信息写入第一位的空元素
③对数组中所有元素做统计,清空计数器数据
可以保证在任意时间片段内,只通过简单的调用计数比较, 控制请求次数不超过阈值

缺点在于只能用于否决式限流, 必须强制失败或者降级,无法进行阻塞等待的处理。

漏桶模式#

漏桶和令牌桶可以适用于阻塞等待的限流。
漏桶就是一个以请求对象作为元素的先入先出队, 队列程度等于漏桶大小,当队列已满拒绝信的请求进入。
比较困难的原因在于很难确定通的大小和水的流出速度,调参难度很大。

令牌桶模式#

每隔一定时间,往桶里放入令牌,最多可以放X个
每次请求消耗掉一个。

可以不依赖定时器实现令牌的放入,而是根据时间戳,在取令牌的时候当发现时间戳满足条件则在那个时候放入令牌即可

2.3 分布式限流#

前面的4个限流模式都只是单机限流,经常放在网关入口处,不适用于整个服务集群的复杂情况,例如有的服务消耗多有的服务消耗少,都放在入口处限流情况其实很多。

可以基于令牌桶的基础上,在入口网关处给不同服务加不同的消耗令牌权重,达到分布式集群限流的目的

总结#

流量治理技术对云原生场景的重要性#

以上主要介绍了服务容错和容错设计模式,涉及到不同的容错策略和容错设计模式,如故障转移、快速失败、安全失败、沉默失败、故障恢复、并行调用和广播调用。

这2个设计可以保证系统的稳定性和健壮性。这篇文章涉及的话题与云原生服务息息相关,因为云原生应用程序之间会频繁通过进行请求和交互,需要通过容错和弹性来保证高可用性。

因此,对于那些希望使用华为云的云原生服务的人来说,这篇文章提供了很好的指导,让他们了解如何通过容错来保证他们的服务的可用性和稳定性。

华为云如何在流量治理中体现作用#

如果能通过将服务API注册到华为云提供的APIG网关上,似乎能够很方便地达成上述2个设计。
比如APIG支持断路器策略,是API网关在后端服务出现性能问题时保护系统的内置机制。当API的后端服务出现连续N次超时或者时延较高的情况下,会触发断路器的降级机制,向API调用方返回固定错误或者将请求转发到指定的降级后端。当后端服务恢复正常后,断路器关闭,请求恢复正常。APIG-断路器策略

同时APIG还提供了流量控制策略,支持从用户、凭据和时间段等不同的维度限制对API的调用次数,保护后端服务。支持按分/按秒粒度级别的流量控制,阅读了上文中提到的几个流量策略,再去看APIG里配置的流量策略值,则会很容易理解。APIG-流量控制策略
image.png

可以看到对于这些常见的经典服务设计策略,无需再重复造轮子,使用已有云服务,可以很快地实现相关功能,提升产品的上线速度和迭代效率。

[toc]

虚拟化容器#

经典的兼容性问题有

  • ISA兼容:目标机器指令集的兼容性
  • ABI兼容:目标系统或者依赖库的二进制兼容性
  • 环境兼容: 目标环境的兼容性。例如环境变量、配置、注册中心等。

虚拟化技术则分为

  • 指令集虚拟化。用软件模拟不同ISA架构的处理器过程。 例如QEMU和Bochs,甚至可以做到在web上运行操作系统。但是性能损失大
  • 硬件抽象层虚拟化。 用软件来模拟计算机里的各种硬件设施。例如VMware等虚拟机。
  • 操作系统层虚拟化。 不会提供真实的操作系统,而是采用隔离手段使不同进程拥有独立的系统资源和资源配额。我们熟知的容器化就是指在这个层面的虚拟化
  • 运行库层虚拟化。 使用软件翻译的方法来模拟系统。例如WINE和WSL。
  • 语言层虚拟化。例如JVM。

1 容器崛起的全部阶段#

这个章节的详细部分非常很精彩,很值得大家去原文完整看看:

1.1 隔离文件:chroot#

1979年unix提供了这个命令,让进程的根目录被锁定在指定参数位置中,不能放外该目录之外的其他目录。经常用来作为监控黑客行动的黑盒。
因此这个命令实现了容器最基础的文件隔离能力

1.2 隔离访问:名称空间#

2022年 linux引入了 linux名称空间namespace。
进程在一个独立的名称空间中,享有独立的文件系统、PID、UID、网络等带有名称的资源。

1.3 隔离资源: cgroups#

linux的cgroups可用来隔离或者分配进程可以使用的资源, 例如处理器、内存大小、磁盘IO速度等, 能够有效避免一个进程出现问题把同机器上的其他进程直接占用崩溃。

1.4 封装系统:LXC#

lxc是linux发布的系统级虚拟化功能。
lxc的理念在于封装系统,而docker的理念在于封装应用。
当系统内的部件需要修改,必须重写很多配置,构建无法很便捷快速。

1.5 封装应用:Docker#

docker的容器化能力直接来源自lxc。
后面又自己用go语言开发了libcontainer避免了对lxc的强依赖。

问题:为什么选择docker而不是LXC?

答:docker相比于lxc,有以下七点优势

  • 跨机器的绿色部署: 将所有环境依赖打包到一起,避免对机器的依赖。
  • 以应用为中心的封装。
  • 自动构建: 无需关注目标机器具体配置,使用任务构建工具在容器中自动构建。
  • 多版本支持: 支持git一样管理容器版本。
  • 组件重用, 可以在基础镜像上构建专业化镜像。
  • 共享,有公共的镜像仓库。
  • 工具生态可以很方便扩展。

1.6 封装集群:kubernetes#

k8s是容器编排框架, 把大型软件系统运行所依赖的集群环境也进行了虚拟化,令集群得以实现跨数据中心的绿色部署,实现自动扩缩。
k8s最开始完全绑定依赖docker, 后面慢慢更新依赖路线,最终在调用链中可以解耦对docker engine的依赖了, k8s最终关注的是container而不是背后的docker

1684798983851

2 以容器构建大型系统(容器编排)#

分布式系统里应用需要多个进程共同协作,通过集群形式对外提供服务,实现这个目标的过程称为“容器编排”

2.1 隔离与协作(Pod的概念)#

这里作者先提出了一个问题:如果web服务进程和日志收集进程放在同一个容器中,有什么影响?

回答:这其实会违反了单进程应用理念, dockerfile只允许一个entrypoint,只能监视pid为1的进程来决定是否重启。

如果自己额外弄一个健康检查进程,可能因为健康检查进程失效却无法被容器侦测到。

因此这2个进程为了维持健康检查的有效性,必须被放到2个不同的容器中, 并且还要支持通过同节点目录挂载进行日志关联。

另外,如果同节点、不同容器之间需要进行基于操作系统的信号通信(不经过网络),则需要这2个容器依赖相同的IPC名称空间。

同节点上不同容器如果要支持共享部分命名空间,则可以用Pod来实现。
因为pod能共享以下内容:

  • UTS名称空间:所有容器都有相同的主机名和域名
  • 网络名称空间:所有容器都共享一样的网卡、网络栈、IP地址等。同一个Pod中容器占用的端口不能冲突
  • IPC名称空间:可以用信号量或者POSIX共享内存
  • 时间名称空间: 共享相同的系统时间
    但POD中PID名称空间和文件名称空间仍然是隔离的。

注:POD中共享空间的能力是通过pause容器实现的,里面大部分都是在执行pause操作,其他时候用来传递各种状态。

POD的另一个关键点: 可以作为资源调度的最小单位(而不是容器)
如果日志收集和web服务因为资源分配问题被调度到了2个不同的节点上,那么功能就会出现问题。

因此当这2个服务整合成pod后,无论如何重启或升级,都会统一调度分到同一个节点上。

K8S里其他资源概念的解释:从小到大

  • 容器container:镜像管理的最小单位
  • 生产任务Pod:容器组,资源调度最小单位
  • 节点Node:对应集群中的单台机器,是硬件单元的最小单位
  • 集群Cluster:对应整个集群,是处理元数据的最小单位
  • 集群联邦Federation:对应多个集群,是满足跨可用区域多活、跨地域容灾的要求

2.2 韧性和弹性(k8s自动维护原理)#

控制器模式是k8s的核心设计理念。

对于上文中定义的各种资源,用户只需要管自己设定好期望的资源状态即可,中间由k8s通过检视资源的控制器来逐步实现往期望状态的靠拢。
换言之,使用者只需要设置集群的期望状态即可, 而不需要你手动调用什么API去操作。

k8s设计了统一的控制器框架kube-controller-manager来维护控制器的正常运作,以及统一的指标监视器kube-apiserver来为控制器提供其工作时追踪资源的度量数据。

  • pod出现故障时,希望自动恢复且能不中断服务,则可以利用ReplicaSet副本集实现, 副本集中包含期望数量个pod。

  • pod升级时不希望中断,可以由deployment部署资源实现,由它来创建replicaset和pod,外部用户不感知,只管设置deployement参数。

  • pod压力过大希望扩容,则可以由AutoScaling(资源和自动扩缩通知其)来实现,自动根据度量指标、cpu、内存等进行后续从AutoScaling->deployement->relicaset->pod的自动化变更,无需用户参与任何手动调用。

3 以应用为中心的封装(介绍其他对k8s增强的能力)#

k8s本身还是有缺陷,写配置非常繁琐,需要懂很多方面才能写好一个配置文件。因此衍生出了其他的增强工具或者能力

3.1 kustomize#

kustomize支持根据环境来生成不同的部署配置, 可以建立多kust文件,开发人员就能以基于基准派生的方式,对不同模式(生产、调试模式)、不同的项目(同一个产品对不同客户的客制化)定制出不同的资源整合包。
即使用Base、overlay、patch来生成最终的k8s配置文件。

3.2 Helm和Chart#

如果k8s是云原生操作系统,则Helm是这个操作系统上的应用商店和包管理工具。
类似linux里的yum命令。
Chart则就是linux里的rpm包,封装k8s应用涉及的资源。
缺点在于无法很好处理有状态的服务,因为会产生依赖关系,不能直接部署安装。

3.3 Operator#

k8s里对于有状态应用,是通过有状态工作负载statefulSet来管理的,满足对pod的持久化存储、按顺序创建、唯一网络名称等,但很多应用特殊的运维操作无法满足。

Operator则支持自定义资源, 即脱离了k8s内置资源的限制。

例如它可以让k8s学会怎样操作es,只需要几行配置即可,而无需手写将近一百行的es依赖配置。

3.4 其他应用封装#

这里介绍了一些软件公司所推出的OAM开放应用模型
核心思想是开发人员关注业务逻辑,运维人员关注程序平稳运行,平台人员关注基础设置, 避免不同人员关注同一个all-in-one资源文件。

[toc]

资源调度#

1 k8s资源模型#

Node是资源的提供者,Pod是资源的使用者。
Node能提供三方面资源:

  • 计算资源(CPU\GPU\内存)
  • 存储资源(磁盘、其他介质)
  • 网络资源(贷款、网咯地址)

CPU是可压缩资源,资源不足时pod只会变慢
内存是不可压缩资源,资源不足时pod会被杀死。

1Core = 1MillCores, 因此0.5核有时也写成500m

2 服务质量和优先级#

对于资源,k8s中给出了request和limit两个设置下。
request是供调度器使用,k8s选择节点部署pod时,根据request决定
limit是给cgroups使用,k8s向cgroups传递资源配额时,按limit设置

设计理念是 用户提交工作负载设置的资源配额,并不是容器调度必须严格遵守的之,往往实际使用的都远小于所请求的。

但为了避免总是按最大申请, 引入了pod驱逐机制。
如果有多个pod要驱逐掉其中1个, 则需要服务质量等级和优先级。

  • 服务质量等级:Guaranteed(数据库等重要应用)、Burstable、BestEffort。
  • 优先级:管理员自行决定(priorityClass), 多个pod被调度时,高优先级的pod会优先被调度。如果是杀死pod时,从低优先级开始杀

3 驱逐机制#

有两种pod的驱逐机制:

  • 软驱逐:例如低于可用内存20%时,先观察一段时间,如果能恢复说明只是抖动,如果持续低则会进行pod优雅退出(先清理、落盘再结束pod)
  • 硬驱逐:例如低于可用内存10%,则立刻结束pod且不会优雅退出

为了避免重复调度到刚才资源不足的节点上,还会有一个k8s参数用来约束调度器,多久内不能把pod调度到刚才的节点上。

4 调度器原理#

  • informer loop:持续循环监控etcd中关于pod和node的资源变化情况,将资源变化封装后发送给调度队列和调度缓存。
  • scheduler Loop: 不断循环从上面的队列中取数据,并使用过滤代码进行过滤,可根据cpu、磁盘卷、节点状态进行过滤,然后再根据排序策略决定最终可以用哪些节点用来调度最新pod。
    image.png

[toc]

13 容器的持久化存储#

13.1 k8s存储设计#

k8s里因为历史兼容因素,导致的存储概念特别多,通过手册学习非常困难。本书作者会以volume概念到docker到k8s的演进历程为主线,梳理和讲解存储设计

13.1.1 mount和volume#

docker内置三种存储挂载类型: bind 、 volume 和tmpfs
image.png

bind就是把宿主机的目录或者文件挂载到容器的指定目录下。

缺点在于只允许容器和宿主机之间映射, 无法实现不同宿主机上的容器共享同一个存储,除非在宿主机外面再对接共享存储系统挂载, 此时对存储的管理更多是宿主机上的工作而不是容器的工作了。下图这个就是在宿主机外面再对接一个共享存储系统的情况
image.png

volumn则能提升docker对不同存储介质的自称能力, 用抽象的资源来代表宿主机或者网络中的存储区域,让docker能够管理这些资源。

tmpfs则是在内存中临时读写的数据,和持久化存储关系不大。

13.1.2 静态存储分配#

指的是k8s里的 persistentVolumn机制
需要系统管理员手动分配persistentVolumn, 这个东西是在pod之外的部署在宿主机上的。

然后容器使用者指定pod期望容量persistenVolumn
k8s帮忙做撮合并实现“1对1”的匹配, 这会导致独占的情况,即使空间有剩余,而且系统管理员手动分配问题会很多。如下图所示:
image.png

13.1.3 动态存储分配#

k8s为了改进静态存储分配的问题, 开发出了 dynamic provisioning的动态存储解决方案。

管理员不再手工分配persistentVolume,而是配置storageClass。 用户依旧使用persistentVolumeClaim来声明存储。
资源分配器自己完成中间宿主机和pod之间的分配, 省去了管理员人工操作的中间层,也不用暴露pesistentVolumn的概念。
image.png

另外回收方面动态也比静态有优势。 当回收pod时,静态分配会要求设置成recycle策略并让系统执行rm -rf /volume/* 这种粗暴命令, 而动态分配汇总如何删除由资源分配器代码管理,不需要人工接入删除行为

13.2 容器存储生态#

13.2.1 k8s存储架构#

k8s对容器的外部存储有以下三种操作

  • privision/delete 准备或移除存储
  • Attach/Detach 将存储接入/分离到系统中,让系统通过fdisk -l可以查看
  • Mount/Unmount 将设备挂载/卸载到系统的指定位置,确定了设备的访问目录、文件系统各种等应用侧信息

上面六个操作是存储插件自己实现的,k8s只负责调用。
k8s使用了以下3个状态控制器来进行调用:

  • PV控制器: 所有处于等待状态的persistentVolumeClaim都能匹配到与之绑定的PersistentVolume
    用来根据需要进行provision/Delete操作
  • AD控制器: 所有被调度到的pod节点都会附加要使用的存储设备,pod销毁后pod节点也会分离存储。
    用来调用attach/Detach
  • Volume管理器
    在本节点中的volume中执行mount/unmount操作