1 PAC

AMR64
提供了
PAC
(
P
ointer
A
uthentication
C
ode)机制。

所谓
PAC
,简单来说就是使用存储在芯片硬件上的「密钥」,一个「上下文」,与「指针地址」进行加密计算,得出一个「签名」,将这个「签名」写入指针的高
bit
上。

image

计算出来的「签名」之所以可以写入指针的高
bit
,是因为在实际使用中,并不是所有指针
64bit
都会被使用,通常高
bit
都会被保留用来做其他用途。

image

如果在程序运行过程中,有人恶意的修改了指针地址,在后面将签名后的指针地址,作用于验证指令,就会报错。

验证过程和计算签名过程一样,只是比较前后计算的签名值是否一样。如果验证成功,签名就会从高
bit
上移除。

image

2 指令语法

PACIBSP

3 指令语义

上面指令对函数返回地址进行
PAC
,也就是对
X30
的值进行
PAC
,或者说对
LR
寄存器的值进行
PAC


PAC
过程中,有
2
种密钥,一种是密钥
A
,另一种是密钥
B

指令
PACIBSP
中的
B
就代表使用密钥
B
,同时
SP
表示「上下文」使用的是
SP
寄存器的值。

下面是一个运行
PACIBSP
后,
X30
的值变化的例子:

// PAC 前
(lldb) p/x $x30
(unsigned long) 0x000000010817f7bc

// PAC 后
(lldb) p/x $x30
(unsigned long) 0x3f57fc010817f7bc

从上面的输出可以看到,
X30
的高
bit

PAC
后发生了变化。

4 同类指令

4.1 PACIB

PACIB <Xd>, <Xn|SP>

密钥
: 密钥
B

上下文
:
Xn
寄存器的值或者
SP
寄存器的值。

指针地址
:
Xd
寄存器的值

4.2 PACIB1716

PACIB1716

密钥
: 密钥
B

上下文
: 寄存器
X16
的值。

指针地址
: 寄存器
X17
的值。

4.3 PACIBZ

PACIBZ

密钥
: 密钥
B

上下文
:
0

指针地址
:
X30
寄存器或者说
LR
寄存器的值。

4.4 PACIZB

PACIZB <Xd>

密钥
: 密钥
B

上下文
:
0

指针地址
: 寄存器
Xd
的值。

4.5 密钥 A 指令

指令
PACIASP
PACIA
PACIA1716
PACIAZ
PACIZA
除了使用密钥
A
进行
PAC
之外,其它都与对应的
PACIB*
指令一样。

Microsoft 今天正式发布了 .NET 9,这是迄今为止最高效、最现代、最安全、最智能、性能最高的 .NET 版本。这是来自世界各地的数千名开发人员又一年努力的结果。此新版本包括数千项性能、安全性和功能改进。您将发现整个 .NET 堆栈中从编程语言、开发人员工具和工作负载的全面增强功能,使您能够使用统一平台进行构建,并轻松地将 AI 注入您的应用程序。

Overview of .NET with workloads, tools, ecosystem, and OS

.NET 9 的下载以及 Visual Studio 2022 的更新和适用于 Visual Studio Code 的 C# 开发工具包现已推出。

.NET开发团队在
.NET Conf
[3] 直播发布活动的主题演讲和后续会议中重点介绍了 .NET Aspire 和 AI。

image

.NET Aspire 是官方 .NET 9
发布公告
[4]的前沿和中心,是讨论的第一个组件,.NET Aspire 是一组强大的工具、模板和包,用于无缝开发可观察的生产就绪应用程序,自 .NET Aspire 首次发布以来,我们仅六个月时间,我们已经对堆栈的所有部分进行了改进,从遥测和指标仪表板中的新功能到更简化的云应用程序部署。很高兴看到 .NET Aspire 在所有类型的应用程序中被采用,并看到社区接受适用于其方案的集成和工具。

image

.NET Aspire 9.0 的新功能在于它引入了开发者最需要的功能来简化应用程序开发。用户现在可以从控制面板启动和停止资源,在调试会话之间保持容器持久性,并利用包括 WaitFor 在内的新 API 来改进资源管理。与 OpenAI、Ollama、Milvus 等的新集成增强了灵活性。新增了
.NET Aspire Community Toolkit
[5],这是一个开源的集成和扩展集合,用于使用 .NET Aspire 进行开发。

在 .NET Aspire 讨论之后,团队在直播和
发布公告
[4]帖子中继续讨论 .NET 9 中的 AI。Microsoft 的 Maria Naggaga 说:“从使用 Microsoft Copilot 的使用者应用程序到使用 GitHub Copilot 的开发人员应用程序,.NET 是这些顶级 AI 体验的核心。今年,我们看到整个行业的团队利用 .NET 构建了令人惊叹的 AI 解决方案”。

以下是 .NET 9 中 AI 新增功能的要点摘要:

  • 扩展的 AI 生态系统

AI ecosystem overview for .NET showing libraries and components

  • 新的学习材料和样例
  • 简化了与 .NET 生态系统的集成
  • 与合作伙伴合作,构建充满活力的 AI 社区
  • 改进了 AI 解决方案到云的部署
  • 适用于 .NET 的 AI 构建块:
    Microsoft 引入了新的抽象来简化 AI,包括:

      Diagram explaining how AI extensions work

    • Microsoft.Extensions.AI 和 Microsoft.Extensions.VectorData:它们为与 AI 服务交互提供了统一的 C# 抽象层,包括:
      • 小语言模型和大语言模型(SLM 和 LLM)
      • 嵌入
      • 矢量存储
      • 中间件
    • 改进了 Microsoft.ML.Tokenizers 中的分词器支持:
      • 针对常用模型系列(GPT、Llama、Phi、Bert)的增强标记化
      • 新增了对分词算法(字节级 BPE、SentencePiece、WordPiece)的支持
    • Tensor<T> 增强功能:
      • 表示多维数据的新类型
      • 简化库之间的互操作性
      • 改进了应用操作
  • AI 集成合作伙伴关系:
    .NET 9 包括与各种 AI 合作伙伴的协作,为开发人员提供强大的产品/服务,包括:
    • Azure
    • OpenAI
    • LlamaIndex
    • Qdrant
    • Pinecone
    • Milvus
    • AutoGen
    • Ollamasharp
    • ONNX runtime
  • 智能组件生态系统:
    Microsoft 表示,它与社区和控制供应商合作伙伴合作,构建了一个智能组件生态系统,从而可以更轻松地将注入 AI 的控件集成到 .NET 应用程序中
  • GitHub Copilot 增强功能:
    虽然严格来说不是 .NET 9 的一部分,但该公告强调了为 .NET 开发人员改进的 GitHub Copilot 集成,包括:
    • 用于调试的 AI 智能变量检查
    • AI 驱动的 IEnumerable 可视化工具
    • 改进的代码修复功能
    • 增强的 C# AI 补全
    • 协助调试失败的测试

该发布公告继续讨论了 Blazor、.NET MAUI 和其他属性中的新增功能,并提供了更深入地探讨新增功能的链接

以及更多内容,还有更多博客文章和指南即将推出。


相关链接:


ServiceMesh系列

1 什么是流量染色

在复杂的生产场景中,经常会有同一个服务中,存在多个版本长期共存的需求。为了让不同的用户在不一样的版本中使用,就需要对用户的请求进行采样和染色,打上不同的标识。
这样的目的有几个:

  1. 支撑分级发布,避免全量发布时可能遇到的大规模风险,如系统崩溃、用户流失。
  2. 支持染色实验,让部分人优先体验新版本或者实验功能
  3. QA的线上问题分析、验证、调试,甚至压测都可以放在染色部署区域去做,因为是强隔离模式,可以避免对线上其他用户的影响

使用Service Mesh的流量染色能力,可以在单个服务中根据特征值进行多元版本流量分发。特别是链路繁琐的巨型网格中,能够管理长达10个以上的链路分流调度,这个能力显得非常重要。常见的 Canary Release(金丝雀)、ABTesting、Diversified Version(多版本分流),都是基于此类算法实现。这边介绍在无侵入业务的情况下,Mesh如何实现流量染色。

1. Canary Release
image

2. Diversified Version
image

3. Diversified Version
image

2 Mesh使用标签特性进行染色

Mesh如果想要实现流量染色,需要具备以下几个条件:

  • 请求的流量中,需要附带某些特征,如流量的请求的Header、Cookies、queryParams等,它们带有某些信息。
  • 部署多版本服务
    • 部署在kubernetes上的服务(svc)的实例(pod)需要接入Mesh,并打上版本标签
    • 或者创建不同的服务(svc),后面把流量引入到这个新的服务上去
  • 在Mesh平台上对应的服务中配上策略:当请求的流量带有某些特征(如header中带有UserID=12345678)时,流量路由到对应标签(如 version = v1.7 )的服务实例上。
  • 不符合条件的路由则默认走到默认版本中(如 version = default)。

所以,Mesh的染色本质上是通过在流量中携带一些特征(如流量的请求的Header、Cookies、queryParams等),而Mesh会根据这些请求的特征进行路由匹配,转发到对应的带有某些特征的服务实例上。
未匹配成功的流量则走到默认版本中,从而实现多个版本和跟默认版本的业务隔离的目标。

image

2.1 Mesh 染色流转原理

2.1.1 Istio支持的策略模型

即Istio支持的流量特征包括uri、scheme、method、headers、queryParams等条件,可以根据这些特征进行路由转发:
image

image

完整参考官方文档:
https://istio.io/latest/docs/reference/config/networking/virtual-service/

2.1.2 流量转发实现

基于上述的策略模型,如果你想配置如下:请求的header 带有 username=brand 或者 dep=A1025 的时候,将流量转发到服务的v1版本,否着转发到default版本。
则策略代码如下:

# 说明:VirtualService 流量染色,根据不同的条件将流量发往不同特征的版本中,假设这边有default、v1、v2 版本
apiVersion: networking.istio.io/beta
kind: VirtualService
metadata:
  name: router-test-vs
spec:
  hosts:
  - router-test-vs  # 调度router-test服务的流量
exportTo:
- "."
http:  # 加各种路由条件,比如匹配人员、所属部门进行路由
- match  # 用户匹配 brand,部门匹配 A1025 时
  - headers:
    username:
      exact: brand
  - headers:
    department:
      exact: A1025
  route:
    destination:
    # todo 匹配条件的流量路由到对应的服务上,比如ServiceA-V1
route: 
  destination:
  # todo 不匹配条件的流量路由到其他服务上,比如ServiceA-V2

3 总结

本文介绍了在Mesh场景下如何使用流量染色,来对不同特征的流量进行分发的实现过程。流量染色在我们实际的生产环境中可以有很多收益和价值:

  1. 支撑分级发布,避免全量发布时出现问题
  2. 支持染色实验,让部分人进入实验环境
  3. QA的线上问题分析、验证、调试,甚至压测

问题的提出

在软件开发中,我们为了减少软件的复杂度,是不会把所有的功能都塞进一个模块之中的,塞在一个模块之中对于软件的管理无疑是极其困难且复杂的。所以把一个项目拆分为模块无疑是一个好方法

                        ┌ ─ ─ ─ ─ ─ ─ ┐
                          ┌─────────┐
                        │ │Module A │ │
                          └─────────┘
┌──────────────┐ split  │ ┌─────────┐ │
│Single Project│───────▶  │Module B │
└──────────────┘        │ └─────────┘ │
                          ┌─────────┐
                        │ │Module C │ │
                          └─────────┘
                        └ ─ ─ ─ ─ ─ ─ ┘

对于Maven工程来说,原来是一个大项目:

single-project
├── pom.xml
└── src

现在可以分拆成3个模块:

multiple-projects
├── module-a
│   ├── pom.xml
│   └── src
├── module-b
│   ├── pom.xml
│   └── src
└── module-c
    ├── pom.xml
    └── src

我们能看到的是每一个模块都有属于自己的
pom.xml
,然后模块A的
pom.xml
是这样的:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>module-a</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <name>module-a</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

之后B的
pom.xml
也大同小异,只用把
<artifactId>module-a</artifactId>

<name>module-a</name>
改为自己的就行。这个时候我们就会发现一个很麻烦的事,我们很多地方都是一样的,但是每一个模块的pom都需要我们重复声明出来,那我们能不能用像对象那样继承下来,这样就不用重复声明了呢?Maven无疑是有这样的功能的

问题的解决

简化后的结构

我们现在看看简化后的模块结构式如何的

multiple-project
├── pom.xml
├── parent
│   └── pom.xml
├── module-a
│   ├── pom.xml
│   └── src
├── module-b
│   ├── pom.xml
│   └── src
└── module-c
    ├── pom.xml
    └── src

与之对比的是根目录多了一个pom,然后多加了一个"模块"parent,里面没有代码src,只有一个裸的pom。

看了对比之后我们一个一个讲是怎么修改的,结构又是怎么样的

修改细则

parent

我们先来看parent里面的pom是怎么个事

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>parent</artifactId>
    <version>1.0</version>
    <packaging>pom</packaging>

    <name>parent</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

我们能发现的是,对于之前的模块A修改其实也不多,修改的分别是
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<name>parent</name>

这里我们着重讲一下
<packaging>pom</packaging>
,首先我们先明白
<packaging>
这个标签代表了什么,

<packaging>
这个标签他表示打包的方式,常见的值为
jar
(Java库)、
war
(Web应用)、
pom
(父项目)等。这个地方
parent

packaging
设置为
pom
,因为它不生成任何可执行的JAR文件,仅提供配置和依赖管理。

其他模块的简化思路

看完了parent的代码之后我们就慢慢地去理清简化的思路

编码与java版本配置

首当其冲的无疑就是这个部分,这个地方模块AB都是需要的,而且都是一样的,那么这个元素就是可以被继承的,也就是
是可以省略的

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>
公共依赖项

其次就是AB都需要的依赖项如
slf4j-api

logback-classic

junit-jupiter-engine
,以及作用域的设置

<dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

修改后的模块A的pom

我们再开看看修改后模块A的pom是怎么样的

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.itranswarp.learnjava</groupId>
        <artifactId>parent</artifactId>
        <version>1.0</version>
        <relativePath>../parent/pom.xml</relativePath>
    </parent>

    <artifactId>module-a</artifactId>
    <packaging>jar</packaging>
    <name>module-a</name>
</project>

不得不说有了parent之后,整个模块都变得简洁了起来

在设置好
parent
模块后,我们只需要引用
parent
作为其他模块的父模块。

首先通过


<parent>


标签引用


parent


模块

    <parent>
        <groupId>com.itranswarp.learnjava</groupId>
        <artifactId>parent</artifactId>
        <version>1.0</version>
        <relativePath>../parent/pom.xml</relativePath>
    </parent>

有了这些之后就相当于继承了parent里面的元素了。

之后我们再导入自己独有的元素就基本上完成了对此模块的配置

<artifactId>module-a</artifactId>
<packaging>jar</packaging>
<name>module-a</name>

继承
parent
模块后,模块A和模块B的
pom.xml
文件已经大幅简化。所有公共配置项,如
UTF-8
编码、Java编译版本、以及日志和测试的依赖库,均已在
parent
中配置好。这样,模块A和模块B仅需保留独有的内容,简化了配置并降低了维护成本。

相互的引用

如果模块A需要引用模块B的代码,可以在模块A的
<dependencies>
中增加对模块B的依赖项,如下:

<dependencies>
    <dependency>
        <groupId>com.itranswarp.learnjava</groupId>
        <artifactId>module-b</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

通过这一配置,Maven会在构建模块A时自动获取模块B生成的JAR文件,使得模块A可以使用模块B中的代码和功能。

根目录pom的配置

最后的最后,我们配置最后根目录pom的思路就是为了完成所有项目的统一编译:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>build</artifactId>
    <version>1.0</version>
    <packaging>pom</packaging>
    <name>build</name>

    <modules>
        <module>parent</module>
        <module>module-a</module>
        <module>module-b</module>
        <module>module-c</module>
    </modules>
</project>

这样,在根目录执行
mvn clean package
时,Maven根据根目录的
pom.xml
找到包括
parent
在内的共4个
<module>
,一次性全部编译。

这可能是最好的Spring教程!

感谢您看到这里
这可能是最好的Spring教程系列
更多的文章可以到这查看
这可能是最好的Spring教程!即便无基础也能看懂的入门Spring,仍在持续更新。
,我还在荔枝更新出最详细的Spring教程

大家好,我是vzn呀,又见面了。

前不久出了个有意思的事情:

某平台UP主发布了一段小米SU7碰撞的测评视频,表示碰撞后小米SU7的小电瓶出现故障导致车门打不开、紧急呼叫系统失灵等问题,引起不小轰动。就在大家都在吃瓜看小米如何应对时,小米官方抛出了一份内部调查报告,重点就一个:我还藏了个备用电源!我把过程上报到国家监控平台了!!你在黑我!!!。这剧情反转程度,像极了重生小说中的桥段,小米设计师似乎预料会有这么一出,提前藏了个小电瓶,就是为了等待这一刻的绝地反杀。

抛去吃瓜的爽文成分,深入其内核,我们不难发现,小米SU7备用电源的设计思路,在软件领域有着广泛的共鸣和深刻的意义。在软件开发与设计的广阔天地里,面对的是一个充满变数的世界,其中既包括
普通用户
的日常操作,也涵盖了
网络故障

硬件崩溃
乃至
恶意攻击
的种种挑战。按照
墨菲定律
的阐述,只要概率大于0%的事情就很容易会发生(说的通俗点,就是怕什么就会来什么)。做好异常场景的应对,是一个成熟程序员的进阶必修课,也是一个软件系统线上平稳运行的内在基石。

说到这里,就不得不提到系统架构设计的一个深层哲学,它强调的是
对未知风险的敬畏与思考
。一个优秀的系统架构设计,应当能够预见并应对未来可能出现的各种挑战,要容忍并接受局部错误存在客观性并努力将局部错误控制在一定范围内,同时当系统出现不可逆灾难时可以尽量最大程度的保障系统核心业务的持续可用。

本篇文章,我们就来聊聊软件开发中针对系统
容错能力
以及
灾难应对
能力的考量。

1. 容错设计,再给一次机会

我们开发的一款软件,上线运行后面临的情况是极其复杂的,说不出错,几乎是不可能的:

上游输入异常参数怎么办
外部接口挂了怎么办
依赖出现故障怎么办
网络抖动导致请求失败了怎么办?
硬盘坏了怎么办?

前面也说过,异常是必定存在的客观事实。如果我们一味的追求绝对的0偏差,其实是自己为难自己。所以呢,为了提升系统应对异常情况的能力,考虑增加一些容错设计,便是一个不错的思路。允许有限范围的异常、尝试去包容这些异常,尽量保证最终符合预期的目标达成即可。

容错能力的实现,有很多种方式,典型的就是
重试机制

补偿策略

1.1. 重试:浪子回头金不换

重试
是为了降低异常情况的出现给业务请求造成的影响,尽量保障请求按预期被处理的一种常用方式。那是否所有的失败场景都需要重试呢?显然不是,比如登录密码校验的时候,输入了一个错误的密码导致鉴权失败,这种不管重试多少次永远都依旧是失败。一般而言,只有受一些瞬时偶然因素干扰的失败场景,才需要考虑重试策略。比如:

  1. 某次对外网络请求的时候,因为
    网络抖动
    等原因导致的请求失败,或者是请求处理超时,可以通过
    有限次数
    的重试,来提升对外交互处理的成功率
  2. 分布式系统中,某个
    节点服务异常
    ,网关将请求重新分给另一个节点进行重新处理,提升整个集群的容错能力
  3. 抢夺分布式锁的排队处理场景中,某次没有获取到锁,等待一段时间后再次尝试获取锁

按照重试
触发时机
的不同,重试策略可以分为
立即重试

延时重试

触发时机 适用场景
立即重试 适用于一些因为偶然因素导致的失败,比如请求的时候如果因为网络抖动导致的链接失败,可以尝试立即重试。
延时重试 适用于因为资源受限引发的失败场景。比如对外请求的时候,由于下游接口流量过大触发限流导致的失败,如果立即重试,大概率重试依旧失败,这种场景就可以考虑等待一定时间后再重试,以提升成功率。

而根据重试操作的具体实现逻辑,还可以分为
原路重试

差异重试

重试类别 场景举例
原路重试 (1)调用三方系统HTTP接口,出现响应超时或者网络不通等异常情况,重新发出一次请求
(2)获取分布式锁失败的时候,尝试重新请求获取
差异重试 (1)分布式系统中,一个节点的请求处理失败后,网关将请求分发到另一个节点进行重试
(2)从Redis中获取数据失败,尝试从MySQL中捞取数据

在重试机制落地的时候,还有2个基础的原则不能忽略:

  1. 限定重试次数
    : 保证极端情况下,系统不会陷入无限循环重试。
  2. 重试次数要合理
    :避免过多的重试,浪费系统资源,不要为了重试而重试。

此外,在一个较长的处理链路上,如果涉及到重试的环节过多,还需要考虑引发
请求风暴
的风险。比如下图的场景,假定重试的最大次数限制为N:

所以重试手段并非是零成本的,它的使用也是有副作用的,尤其是在一些复杂链路场景中。为了规避连环重试可能导致的连环风暴隐患,还需要引入一些辅助手段来应对。

  • 打破链式重试

请求风暴的形成,是因为最末端的异常被无限制透传给了所有上游环节,然后触发了上游环节的反复重试,将请求数量指数级放大。但是实际上,仅仅是最后一个节点与DB之间的请求出现问题,其实只需要重试这个操作即可,上游节点并不需要重试。为了实现这一效果,需在请求交互层面进行规划,通过返回值、返回码等方式,告知上游节点是否需要重试,将重试的范围限定在故障发生位置,而非全链路的链式连锁反应。

  • 结合熔断策略

结合熔断机制,根据该路请求处理的失败率进行判断,达到一定阈值的时候,直接执行熔断操作。后续通过一定的探测机制,分配少量的试探性流量,如果成功率达到设定阈值,则恢复此链路的后续处理。

1.2. 补偿:亡羊补牢犹未晚

上面介绍的重试手段,主要目的是为了尽可能的提升当次操作的成功率。但是,总有一些异常场景不是即时重试就可以解决的。比如在一些大型的微服务分布式系统中,一个请求流程会跨越多个服务进行处理,且请求的处理往往是异步的,如果出现重试也无法解决的异常问题,就需要额外的补偿机制,对处理结果的最终一致性进行保障。

补偿机制经常被使用在分布式系统中,它的一个核心前提是,允许并接受过程中的暂时性数据问题,并通过补偿措施,保证最终的数据一致性。那么,如何知晓是否需要执行补偿操作、哪些数据需要执行补偿操作呢?这就需要“
对账
”了。

所谓“对账”,就是定期将此前一段时间内的业务处理数据进行盘点比对一下,找到数据层面不符合预期的数据。基于对账发现的异常记录,再执行对应的补偿修正处理。

举个例子:

一个电商平台系统,其订单系统的设计,买家的订单和卖家的订单是分库存储的。一个订单创建并付款完成之后,订单信息会流转到下游消费服务中被各自处理,并分别写入到买家订单库和卖家订单库中。

在微服务化场景下,虽然可以通过一些分布式事务等手段来加以防范,但依旧可能会因为一些极端情况,导致一个订单没有被同时成功写入到买家订单库和卖家订单库中,这样就可能会用户的使用造成影响。这种情况下,就可以考虑搞个定时任务,定期扫描下一段时间内的订单数据,校准下两边的差异,然后针对异常数据进行处理修正。如下所示:

这样,基于事后
对账+补偿
的双重手段,保障了系统的“最终一致性”目标达成。

2. 顾全大局,舍小义而谋大利

还有一些业务场景,它可能是牵扯到多个并列的依赖方,并最终诉求是将多个依赖方的结果混合在一起。这种情形下,某一个依赖方出现问题,对最终用户的使用体验而言影响很有限、甚至是无感的。一损俱损显然不是最优解,
弃卒保车
会更为合理些。

举个例子, 一款新闻资讯类的软件,首页的内容流列表由多路数据源汇总而成:

  1. 即时突发新闻
  2. 热门时政要文
  3. 关注账号发文
  4. 可能感兴趣内容
  5. 付费推广内容
  6. xxx

最终多个来源的数据,会被混合成一个列表内容流展示给用户。这个过程中,如果其中某一路(比如:即时突发新闻)出现异常未获取到数据,对用户而言其实是无感的,因为用户也不知道究竟是系统出问题了、还是确实没有即时突发新闻。但是因为某一路数据的获取失败,直接给用户报错异常、或者给用户一个白屏显示,反而是将用户给放大了。

在实际的项目中,当故障的出现已经不可避免且无法规避或者重试解决的时候,为了避免问题的进一步扩大,通过一定程度的“妥协”与“舍弃”,以尽量小的损失、避免故障影响面的放大,也是一种常规操作,实现手段有很多,主流的有降级、限流、熔断、放通、隔离等。

2.1. 降级

降级
作为一种兜底策略,通常是在故障场景下从业务层面作出的一种
妥协
策略。 一般是遇到局部功能障碍、或者资源负载层面问题的时候的一种应对方案。当出现某些突发情况,导致系统资源不足以支撑全量业务功能的正常开展时,为了将有限资源集中起来保障核心功能的可用,而主动将一些非核心的功能停用的思路。

降级的使用场景很多,比如:

  1. 电商每年的618或者双11等大促时节,为了保障抢购下单的正常推进,将订单评价、历史订单查询等非核心功能先降级停用,所有资源全力支撑商品的浏览、下单、付款等操作
  2. 互动社交平台,突发超级流量明星的大瓜新闻时,降低一些非核心功能(推广、关注流)的更新频率,将更多资源用以支撑爆炸性话题的访问与互动浏览操作
  3. 对于即时通信IM类场景,如果出现网络故障原因导致机房带宽承压有限,那就降级让视频和语音类服务不可用,尽力保障文字消息功能依旧可用

降级的本质,就是一个
取舍
的过程,舍弃不在乎的部分,保住最在乎的部分。舍弃谁、保全谁,需要根据自身业务的特征来判断。一般而言,有几个维度:

降级维度 场景举例
降低用户体验 界面刷新不及时、不展示动效、不展示高清图、不显示系统推送通知
舍弃部分功能 不允许查看历史订单、不允许数据导出操作、不允许上传文件操作
安全性让步 不做复杂二次校验、跳过风控判断、不记录操作日志
降低准确性 列表数据更新不及时、统计报表更新不及时
降低一致性 列表显示的评论数与点击进去正文显示的评论数不一致,已经删除的文章依旧出现在列表中
降低数据量 订单中心只显示最近100条记录,仅可以查询最近1年数据

实施降级操作的前提,需要系统业务规划层面进行配合,要做好系统业务功能的
SLA规划
,划分出核心功能与非核心功能。同时,在系统的架构层面要做好核心功能与非核心功能的解耦与隔离。

2.2. 限流

一般在春节、五一、或者国庆等节假日,一些热门的景区都会限制进入景区的客流量,以此保证游客的游览体验与人身安全。同样道理,软件系统受限于自身实现、业务规划以及硬件资源承载能力等诸多限制,其承压能力也是有上限的。如果请求流量突增且明显超出系统规划的可承受范围的时候,可能会引发系统宕机等事故。为了保障系统安全,避免突发流量对系统的正常运行造成冲击,就需要对进入系统的流量进行限制管控。

限流
一般可以依据两个维度进行实施:

限制维度 场景举例
限制并发数 比如限制连接池的连接数、线程池的线程数等。
限制QPS 限制每秒进入的请求量。

限流操作的实现,离不开限流算法,主流的有
漏桶算法

令牌桶算法

  1. 漏桶

漏桶算法
的原理很简单,它不限制流入的请求量,但是会以一个相对受限的速度从漏桶中获取请求进行消费处理,如果流出速度小于流入速度,请求就会在漏桶中积压暂存等待顺序被处理,一旦漏桶容量被积压的请求撑满,便会发生溢出,无法进入漏桶的请求将被丢弃。

正如其名字一般,漏桶的原理,像极了生活中使用的漏斗。这也是一个示例,再次印证了软件架构设计中的很多实现与处理策略,都是来源于最质朴的生活。

  1. 令牌桶

令牌桶
的逻辑与漏桶略有不同,它会有个令牌发放模块负责匀速生成令牌并放入到令牌桶中,然后每次请求处理前先尝试获取一个令牌,只有获取到令牌了才会去处理对应的请求。

值得注意的一点是,虽然令牌是设计成匀速生成并放入到令牌桶中的,但这并
没法保证
请求一定会被匀速处理。极端情况下,可能会出现短暂请求量突破限速值的情况(比如:大部分时候请求量小于令牌生成量,导致桶内蓄满令牌,突然来波大流量,会一口气消耗掉令牌桶中全部的存量令牌),所以需要根据系统设计的承压负载情况,合理设定限流的阈值。但这一设计也有其优势,偶尔短暂的脉冲波动可以尽量消化掉,同时又保证长期整体处理速率处于一个受控状态。

还有一种简陋的基于计数器的“
伪限速
”方案,这一思想很简单,每个计数周期维护一个计数器,然后来一个请求计数器就累加1次,计数满阈值后便拒绝后续请求,直到下一周计数器重新计数。这种本质上只能控制流量、无法控制过程流速,极端情况下的一些请求峰值,极有可能会击垮系统,要尽可能将流控计数周期设置的短一些,尽量避免在核心重要系统中使用此方案。

此外,对于一些集群化多节点部署的场景,规划限流的时候,还需要关注是
单机
的流量限制,还是
集群
整体的流量限制,选择适合自己业务的实现方案。

2.3. 熔断

熔断
在现实世界中最直观的应用,就是家里强电箱里空气开关中的保险丝了。当电流超载的时候,保险丝就会断开,以此来保护家里的整体电路不会烧掉,以及各种电器不受损坏。

同样道理,在软件实现中,也有类似电路保险丝一般的设计思路,通过在服务的对外请求调用处增加熔断器,当符合预设条件时,就会将对应依赖的服务从自身的请求链路中剔除,来避免自身节点耗费大量的资源在等待一个大概率错误的响应。

熔断,是自身的一种
自保手段
,目的是防止外部节点异常将自己耗死。熔断的策略一般有两种:

  1. 按照请求的失败率进行熔断

短期内发往某个目标服务的请求失败率高于某个阈值则执行熔断策略,不再继续调用此目标服务。然后通过定期的心跳探测机制,或者少量试探流量的方式,决定继续熔断还是恢复请求。

  1. 按照请求响应时长进行熔断

对于一些高并发量的处理场景,如果调用的目标服务的请求时延过大,势必会拖累整体系统的吞吐量。这种情况下,为了保障自身节点的处理性能,也可以按照请求响应时长,决定是否触发熔断操作。

此外,在集群部署环境下,网关节点也经常会将熔断作为基础功能进行提供,实现比服务熔断更细粒度的一种控制,当服务集群中某个节点出现故障时,直接将该节点剔出集群,待其恢复之后再加入到集群中。

具体应用的时候,可以直接使用一些成熟的开源方案,比如
Hystrix
或者
Sentinel
等。需要强调的一点是,熔断一般针对的目标是一些非核心、非必须的依赖服务,本质上,熔断也是降级的一种实现形式。

2.4. 隔离

隔离
作为一种故障控制手段, 其设计思想是通过将资源分隔开,互不干扰,这样系统出现故障的时候,就可以将故障限定在一定的传播范围内,避免出现滚雪球效应、波及全局。常见的隔离措施,有
数据隔离

机器隔离

线程池隔离
以及
信号量隔离
等等。

  • 数据隔离

最直观的表现,就是
分库分表
了。比如对系统的数据,按照业务维度进行分库存储,或者按照业务的重要度进行识别,将数据识别为重点数据/非重点数据,亦或是保密数据/非保密数据,然后按照细分后的结果实施差异化的数据存储保障策略。比如,对于非重点数据,简单的搞个一主一从双副本即可, 而重点数据,可能得考虑异地多副本可靠存储与备份。

  • 机器隔离

不同的业务使用不同的机器,从
硬件资源
层面进行隔离。通过将机器分组的方式,针对重点服务或者是高危服务实现专机专用,而对应一般普通服务,则可以多个业务混用同一套机器,从而实现了差异化的隔离处置。

  • 线程池隔离

隔离的思想,不仅仅是体现在数据层面或者是进程机器节点等宏观层面,该思想同样适用于对单个进程内部的实现。因为同一个进程内处理很多不同的逻辑,如果某个处理逻辑无限制的创建执行线程,占据了全部的系统CPU资源,则整个进程中其余的逻辑就会受到影响。

为了应对这种情况,可以基于
线程池
进行隔离设计,为主要业务处理方法指定对应的执行线程池,限定具体业务方法仅可以按照分配线程池提供的线程资源进行调度与使用,禁止业务方法自行无度占用系统的线程与CPU执行资源。这样一来,即使某个业务占用了自己全部线程池资源,依旧不会影响到其余线程池的正常处理,保障了其余业务的正常开展。

因为线程池的维护也会占用额外的资源,所以隔离的粒度的把控也要做到
适可而止
,遵循适度原则。

3. 硬件灾备:钞能力带来的超能力

软件的顺畅运行与硬件的稳健支撑密不可分。尽管软件层面通过巧妙的容错设计、灵活的降级策略以及精准的限流机制等手段,能够显著提升其自恢复能力和可用性,但在面对硬件故障这一硬性挑战时,单纯依赖软件手段就显得力不从心。所以,在设计规划建设一套可靠的软件服务的整体架构时,
硬件部署规划
时的可靠性设计,也是无法回避的话题。

相较于软件层面的各种容错策略,硬件层面的应对就显得简单且粗暴————
堆资源
!即通过资源的
冗余部署
来增强系统的容错能力。当然,这一策略的实施不可避免地会增加经济成本,所以具体实施与规划的时候,还需要结合预算情况,在成本许可范围内实现可靠性的最大化保障能力。

常见的硬件层冗备的实践,一个是保障业务应用高可用的
多活机制
,另一个是为了保障数据可靠存储的
多副本存储机制

3.1. 多活

随着越来越多的生活场景被搬到线上处理,互联网白热化时代,对与业务的
7*24
小时持续可用提出了严峻的挑战。但对于一个软件服务来说,不管架构多么完美、代码多么优雅,最终程序都得运行在硬件基础之上,而硬件层面的风险,是代码无法左右的。那么如何应对硬件的各种损坏或者不可用风险呢?很简单,
花钱消灾!
多花点钱,多搞点硬件资源,多部署几套服务就行咯。但是这个部署多套,实际也是有讲究的。

为了应对不同层级的风险,也引申出了多种不同的堆硬件的方式:

  • 集群化

为了应对单台服务器硬件的损坏、比如硬盘损坏、电源烧毁等, 单个机房内部署多个节点,由多个不同的机器,共同组成一个
集群
,这样其中一个节点故障,其余节点依旧可以正常处理业务,有效避免了单点故障的出现概率,提升了业务的可靠性。

  • 同城双活

上述在同一机房内利用多个节点组成集群的方式,虽然能应对单台机器的故障场景,但如果机房出现整体故障,比如停电、着火、光缆被挖断等情况,依旧会导致全军覆没。为了应对这一可能的风险,自然而然的解决方案就是再建一个机房,这样两个机房互为备份,风险就大大降低了。通常而言,两套机房之间会涉及到数据的同步,所以对机房之间的网络传输速度与时延有极高要求,这就要求两个机房不能离得太远,最好在同一个城市。 —— 这便形成了常说的
同城双活
架构。

  • 两地三中心

基于同城双活的模式,其可靠性已经可以满足大部分普通业务场景对于系统可靠性的诉求了。但若业务系统极其重要,尤其是一些金融、社交、基础服务提供商等牵扯到国计民生的领域,对系统的可靠性与数据的安全性有更加苛刻的要求。在同城双活架构中,为了控制机房间网络时延,两个机房的距离都不会太远,万一出现某些不可抗力的自然灾害(比如地震)造成两个机房全部损坏,依旧会导致业务或数据受损。所以如何应对?答案已经呼之欲出了,跨不同城市多建一些机房呗!于是乎,
两地三中心

三地五中心
等等解决方案应运而生。

看到没?系统的可靠程度,一定程度上,取决于堆的钞票的厚度。

3.2. 多副本

冗余备份
,又叫做
多副本
。本质上就是为了防止单点故障造成数据层面的丢失,而采取的将同一份数据分散在多个位置存储多份的一种方式。这种方式会造成额外的资源成本支出,但其所带来的数据可靠性与高可用性,是“孤本”无法比拟的。

多副本的策略,广泛的被应用在各种数据存储组件中。比如:

  1. 本地缓存多副本
  2. Redis多副本
  3. MySQL的多副本
  4. Kafka多副本

最常见且最简单的多副本策略,就是
Master-Slave
这种架构,类似MySQL的一主多从架构。在这种架构中,通常由Master节点负责数据的写操作,然后通过内在的数据同步机制,将数据变更同步更新到各个Slave节点进行数据的多副本存储。为了提升硬件的利用率,Slave节点除了用于数据内容的多副本可靠存储,还可以对外提供只读查询操作,在必要场景下支撑业务的读写分离诉求。

Master-Slave
这种主从架构的多副本策略有个致命的问题,就是每台节点存储的都是全量的数据文件,这使得数据总量
受限于单机存储
,存在瓶颈。对于超大数据量场景,还会需要更加复杂的多副本方案,对总体数据进行切片,对每个切片数据进行多副本支持,进而可以支持容量的水平扩展。像Redis集群或者是kafka所采用的便是这种策略。

  • 分片存储形态1:分散在不同机器上存储

  • 分片存储形态2: 多集群承载分片模式

这种对数据进行分片并分散在多台物理存储节点的方式,
打破了单机容量
的限制,但是也增加了数据读写与数据同步的复杂度。因为数据分散在多个节点上,所以在读写的时候,需要支持将请求路由分发到数据分片所在的节点上,比较常见的是使用
一致性Hash算法
来进行分片。此外,各节点上分片数据的同步与一致性保障也需要更加复杂的处理逻辑来支撑,比如Kafka就专门设计了
ISR算法
来处理多副本之间的数据同步。

4. 人工干预,保证对系统的控制权

我们按照业务场景与业务诉求进行功能实现的时候,会预先设想好各个场景的处理与应对,也会考虑一些可能异常场景的代码层面自动兼容与应对。但可能会有一些场景,它就是突破了我们预先对系统设定的一切合理规划,或者系统出现一些未曾预料到的场景无法自恢复的时候、亦或是自动恢复或回滚处理的影响面太大的时候,都可能需要
人工介入
处理。所以,系统在规划与实现的时候,很有必要构建一些人工干预手段与能力。

这种人工干预能力,有很多实际的应用场景,可以用来提升运维人员对系统的高度掌控力从而更好的应对各种突发场景,也可以作为运营人员的一种高权限后台处置权进行预留。

4.1. 人工介入应急处置能力

先看个例子:

背景:
某个业务需要从远端数据源获取数据并进行业务逻辑处理,由于业务本身属于特别核心且重要的服务,远端数据源数属于外部依赖,数据准确性与服务可用性不可控。

实现:

  1. 业务处理的时候,定期从远端进行数据源拉取更新,优先使用远端数据源的数据。
  2. 为了应对远端数据源不可控风险,定期更新的时候都会将远端获取到的数据写一份到本地磁盘中进行备份,本地磁盘保存最近N个的备份。
  3. 如果远端服务数据拉取失败,则业务自动尝试从本地读取最近的备份文件以支撑自身业务的继续运行。如果最近文件处理失败或数据异常,则自动加载前一个备份文件,以此类推,直到重试完本地所有备份文件后,如果依旧处理失败,则系统不可用,放弃挣扎。

结合上述背景,可以看出实现应对策略想的还算周到,做到了使用本地备份进行远端数据请求失败情况下的兜底处理,还考虑到了数据加载异常的情况增加了自动重试机制,自动往前加载直到尝试加载到一份可用的历史备份文件为止。但是考虑一种场景:假设远端服务接口正常,返回的数据响应格式也正确,但是由于远端数据源的服务开发人员昨天夜里升级了个版本,导致下发的数据内容本身存在严重的错误,这导致下游业务使用该数据的时候业务受损。这种情况下,前面规划的实现中的所有自恢复与自保手段都是失效的。

所以呢,如果在规划阶段,在上述实现的几点保障措施的基础上,再额外规划一条
人工指令干预通道
,在紧急情况下,可以人工下发指令,强制要求系统断开与服务端的实时更新逻辑,并强制加载第X份本地备份文件,便可以快速让自身服务摆脱远端数据源的故障影响,等到其故障修复后,再下发指令恢复对远端数据源的实时更新。改造后的示意图如下所示:

4.2. 非预期场景的人工处置权

人工干预能力,也可以算作是管理端系统的一个“
特权功能
”,为后台人员提供更高的操作权限,解决某些看似不合理、却极可能出现的业务层问题,比如处理某些难缠的客诉问题。

举个简单的例子:

一个证券公司开发了一款炒股APP,并提供了一个投顾付费功能,用户付费之后就可以使用对应的高级功能。业务规划的策略是用户购买之后不允许退订,并且界面以及用户协议里面也明确提示了购买之后不允许退订。

A用户购买之后就非要退订退款,然后不停地缠着客服并威胁不处理就去监管机构、证监会等地方去投诉。

理想情况下,我们预期是用户按照产品规划的策略进行购买,并且也已经尽到告知义务,不支持用户退订。但面对客户的胡搅蛮缠,本着维护公司形象的角度出发,为尽快平息争议,客服部门会私下同意后台操作为这个用户退单退款。如果系统设计与实现的时候,没有规划构建对应的后台人工退单退款能力,处理起来就会很被动了————正所谓:
可以不用、但不能没有

5. 监控预警,防患于未然

前面提及的容错设计以及一些灾备方案,其面向的是故障已经发生的情况下,如何去应对故障来保障系统业务的可用性。而更为稳妥的一种预期,是能够
在问题刚暴露一点苗头的时候,就能被发现并及时化解掉
,这里就需要在系统实现的时候进行一些必要的
数据埋点

指标采集
监测,及时将系统的预警信息告知具体维护人员,提醒维护人员及早介入处理。

并非“看不到的问题就是没问题、看不见的故障就是没故障”,作为系统的负责人员而言,应该是要知晓系统的整体运行状态以及系统的健康度,通过状态监控、指标监测等手段,让线上系统的运行状态从黑盒变为白盒。

5.1. 监控告警

一般而言,监控平台都是独立于业务进行构建,且提供
Push
或者
Pull
两种指标数据获取机制。在监控内容方面,可以涵盖
资源
使用情况、
系统
状态、
业务
运行数据等各维度。

监控告警是开发与运维人员知晓线上系统异常状态的一个重要手段,实施的时候需要注意不要滥用告警通道。告警消息的发送最好支持
分组聚合

消息抑制
等能力,避免出现无用告警消息的狂轰滥炸,麻痹接收人员的神经、淹没真正重要的“求救”信号。同时,在构建监控告警平台的时候,考虑尽量独立于业务,让告警相关的逻辑从业务中解耦,降低监控对业务逻辑的侵蚀性。

关于如何设计与规划构建监控告警平台,有兴趣的可以看下我此前的一篇文章
《搭建一个通用监控告警平台,架构上需要有哪些设计》

5.2. 实时Dashboard

既然要
防患于未然
,首先是要对系统整体的健康状态有个清晰的认知。这个时候,系统健康监测相关的能力的价值就会显现出来。这就像是一份系统的实施体检报告,基于这份体检报告,可以发现系统中潜在的压力点位、风险环节、可疑趋势,然后可以提前介入进行应对,将故障消灭在萌芽阶段。

5.3. 灾备演练

如前所述,系统在构建的时候规划了一系列高大上的异常应对与灾难恢复手段,但如何确保这些手段在异常出现的时候能达到预期效果呢?这就得通过灾备演练来检验了。如同和平年代的军事演习,灾备演练也是很多大型项目的定期“军演”。通过模拟一些可能的灾难故障场景,去验证系统的容错与异常保障手段的有效性,发现应急方案中存在的问题并及时修复。

对于一些大型系统而言,其整个业务流程的处理会牵扯到上下游以及周边等众多依赖,一些灾备预案的实施也是上下游联动触发的。所以定期灾备演练的另一个目的,也是锻炼开发运维人员的应急预案实施的默契度。

6. 意识养成,保持对风险的敏锐识别力

如前所述,在实现层面有很多种成熟且可落地的方案可以将系统的异常应对与灾难恢复等场景变为现实,但这些其实都是具体的“

”,是我们已知有这个风险或者诉求的前提下,为了应对这些已知可能场景而作出的具体应对之法。而身为一名IT从业者,一方面要经历将业务诉求变为现实的落地过程,同时也是与各种异常情况博弈的历险之旅。
对风险的敏锐洞察力
,应该是一个优秀程序员刻在骨子里的品质。这种品质,不仅体现在编码层面、亦非局限于架构设计,而是各个方面的,它是一种
思维模式
、是一种本能的
条件反射

保持对风险的敏锐识别力,才能让自己看见潜在风险,才能让各种风险防备之术得以落地。

举个简单的、非技术实现层面的例子。

线上系统临时有个问题,需要紧急手动换个包并重启下进程修复。
  • 头铁勇士的梭哈

多简单的一件事,进程停掉,删掉旧包,上传新包,然后进程已启动,完美解决。

也许,大部分情况,的确也没出现过问题。但对于有一定资历的人员而言,看到对线上环境的这一操作,往往会有点“心惊胆战”的感觉。比如:

万一上传的包有问题,启动失败了,这个时候线上包也删了,服务也没法启动了,线上服务直接就报废了

  • 吃过小亏之后的低头

结合上述操作可能存在的风险点,改良版的做法,自然就是旧包不删、改为重命名备份起来,这样新包万一有问题,可以直接用旧包回滚恢复线上服务即可。

这种改良之后的操作方法,从可靠性层面而言,的确有很大改进,给自己留了充足的回滚与回退的余地。但是仔细审视一下,依旧有改进的余地:

  1. 上传新包的操作,由于要走网络传输、受网络波动的影响较大,存在失败风险。
  2. 如果包的体积很大、或者上传的时候要走vpn、堡垒机等层层关卡,可能速度会比较慢,整个传输过程的耗时会很长。这种情况下,可能会导致线上进程停机时间太久。
  • 受尽毒打而幸存后的谨慎

进一步的优化上述操作的步骤,可以将整个动作分为前置准备环节和线上操作环节两部分,将一些比较耗时且风险比较大的操作,放在前置准备环节中预先完成。

这样一来,在正式线上操作的环节中,仅需要执行一些确定性较高的动作,这样既可以保证执行动作的快速结束,也可以降低执行动作的不确定性。通过风险前置操作,降低了整个操作过程中出现问题的概率。

说到这里,也许有的小朋友会反驳,觉得公司带宽很高、传输文件很快,不需要这么麻烦,直接梭哈干就行了。这其实就是一个意识层面的共识问题,也是一种对风险的应对策略问题。其实还是之前的那句话,能意识到的风险并非真正的风险,往往是那些看似不可能的风险才是真正的风险。所有行为的出发点其实就一条:这个动作
操作失败的后果,是不是你能够承担的
。如果可以,那可以直接梭哈,否则的话,就要三思。

7. 再论本心:扁鹊三兄弟的故事

最后,讲个故事吧。

传说扁鹊周游到魏国的时候,魏文王接待他并问他:你家兄弟三人都是学医的,那么你们三个人中谁的医术最高呢?扁鹊回答说:“我大哥医术最高,二哥次之,我医术最差”。魏文王很困惑:“为何世人皆尊你为神医、却不曾听闻你大哥二哥?”,扁鹊解释道:

  • 我大哥的医术最好,是因为他能够在你没有发病之前就能看出你是否有病。那个时候,病人是不会觉得自己患病了的,我大哥就在病人发现之前就将病给治好了。这是因为这个缘故,大哥的医术一直不被他人认可,也没有什么名气。

  • 二哥是家中医术第二好的,因为他能够在病人发病初期就看出来,然后将病人给治好,这样一来,病人们都认为我二哥只擅长治疗一些小病症。

  • 病人找我治病时,已经到了中晚期,病情已经十分的严重了。我将那些患了重病的病人给医治好后,我就更加出名了。但从根本上来讲,我的医术比不上我的两位哥哥。

放到当前日益内卷的IT行业,扁鹊大哥、扁鹊二哥这种人,也许是属于技术高超的一类人,他们默默守护自己的代码、不给异常爆发的机会。于是呢?始终稳定的线上服务,让人慢慢淡忘了相关开发人员的存在,使其反而成为被边缘化的透明人。真正可以有机会崭露头角、博得领导青睐的,往往都是团队里面的
救火队员
般存在的人,这些人,不停的在前线冲锋陷阵,去解决线上那些按起葫芦起了瓢的问题,久而久之便成为领导心中信赖的
柱石
,相关的机会与资源也向其倾斜。

技术之外的事情,虽然发人深省,却也似乎无解。正所谓
圣人治未病,不治已乱、治未乱
,反观我们自身,如何抉择,主动权在个人、遵从本心最重要。但是相信的是,时间会证明一切,一切的坚守与技术上的追求,最终一定会被看见(有点鸡汤的味道)。所以呢,技术上有点追求,永远是个正解。

8. 小结

好啦,关于软件开发与设计过程中的异常应对与灾备能力的探讨,暂且告一段落。这里所提到的
容错应对

灾难应对
能力,其重要性犹如生活购买的保险——平日里或许显得默默无闻,甚至让人有些“成本浪费”的错觉,但在关键时刻,它们却能成为抵御风险的坚固防线,其价值无可估量。

正如小米SU7配备的备用电池,这一设计在紧急情况下为用户多了一层求生保障。在软件开发与设计的世界里,是否需要构建类似的
备用方案

灾备系统
,取决于业务对潜在损失的容忍度。若灾难性后果是业务所无法承受的,那么增加一些额外的成本,构建一套完善的灾备兜底、异常保障以及监控告警机制,就显得尤为重要了。

亦如古语云:
未雨绸缪,有备无患

我是vzn呀,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【是vzn呀】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。