软件质量模型

软件质量模型是一组特性及特性之间的关系,它提供规定质量需求和评价质量的基础。

特性 核心子特性
功能性 适合性、准确性、互操作性、安全性
可靠性 成熟性(无故障)、容错性(出了故障不影响主功能)、可恢复性(快速恢复)
易用性 易理解性、易学性、易操作性
效率 时间特性(RT≤2s)、资源利用率(CPU≤80%)
可维护性 可分析性、可修改性、稳定性、易测试性
可移植性 适应性(环境兼容)、易安装性、共存性(与其他系统共存)

测试思维与方法

测试思维

  • 批判性思维:基于观察、分析、推理,持续质疑系统,优化测试设计。
  • 系统性思维:从整体到局部分析被测对象,对系统进行分解、分层(如分层测试策略)。
  • 发散性思维:多角度探索用户场景(如探索式测试中的路径多样性)。

黑盒测试方法

方法 核心逻辑
场景法 模拟用户操作流程,覆盖基本流(正确流程)和备选流(异常流程)。
等价类划分 将输入划分为有效/无效等价类,每类选一个用例覆盖。
边界值分析 测试输入边界值(如最大值、最小值、空值)。
因果图 分析输入条件与输出的因果关系,生成判定表设计用例。
错误猜测法 基于经验测试易错点,以点带面地挖掘共性bug(如特殊字符、越权漏洞测试)。

白盒测试方法

  • 语句覆盖:每条代码至少执行一次。
  • 判定覆盖:每个分支的真/假至少执行一次。
  • 条件覆盖:每个条件表达式取真/假至少一次。
  • 路径覆盖:覆盖所有可能的执行路径(理论最高覆盖,但成本高)。

推荐的综合策略

对于大多数云原生系统的状态测试,推荐混合使用状态转换测试(主干)+ 因果图(异常分支)+ 场景测试(并发/时序),以平衡覆盖率和执行成本。

  1. 分层测试设计
    • 主干逻辑:使用状态转换测试覆盖实例状态机(如RunningStoppedDeleted)。
    • 复杂分支:用因果图覆盖异常场景(如资源不足、节点故障)。
    • 并发和时序:补充基于场景的测试或混沌实验。
  2. 用例混合设计示例
  • 用例1(状态转换):Running → Stop → Stopped
  • 用例2(因果图):Create + ResourceExhausted → Error
  • 用例3(并发场景):Create和Delete同时触发 → 操作冲突提示

测试流程与敏捷实践

传统测试流程

  1. 需求分析:明确测试范围与优先级。
  2. 测试计划:制定目标、策略、风险应对方案。
  3. 测试设计:转化为具体用例(等价类、场景法等)。
  4. 测试执行:执行自测、回归测试、缺陷跟踪。
  5. 发布维护:监控线上问题并修复。

敏捷测试流程

敏捷测试本质是短、频、快地反馈代码提交的质量,促进持续交付。

敏捷测试的特点如下:

  • 质量内建:测试左移(参与需求评审) + 测试右移(监控生产环境)。
  • 持续测试:自动化与探索式测试结合,快速反馈代码质量。
  • 分层策略:单元测试→接口测试→UI测试,平衡效率与覆盖。

探索式测试(ET)

探索式测试旨在将学习、测试设计、测试执行和测试结果分析做为一个循环快速地迭代,在较短的时间内(如1个小时)完成多次循环,以持续优化测试。该思路再次与敏捷软件开发小步快跑、持续反馈的理念不谋而合。

  • 核心:质疑系统存在漏洞(需求误解、实现错误、性能瓶颈等)。
  • 实施步骤
    1. 制定SMART目标(具体、可度量、可实现)。
    2. 分时间盒(如50分钟)执行“设计-执行-分析”循环。
    3. 通过测程管理(SBTM)记录测试结果并复盘。

性能测试

测试类型

类型 目标
负载测试 确定系统在满足性能指标下的最大负载,同时可找到性能下降拐点(如响应时间≤2s时支持1万用户)。
压力测试 测试系统在极端负载下的稳定性和可靠性(如用户量继续增加导致系统崩溃的临界点)。
容量测试 验证系统处理数据量/用户量的极限(如云存储最大数据量)。

性能指标

指标 定义
响应时间 (RT) 用户发起请求到收到响应的总时间(含网络传输、后端服务处理、前端渲染)。
吞吐率(TPS) 每秒处理的事务数(衡量系统整体吞吐能力)。
QPS 每秒处理的请求数(公式:QPS = 并发用户数 / 平均响应时间)。

可靠性测试

  • 定义:验证系统在特定条件下(如长时间运行、异常输入、资源限制等)持续稳定运行的能力,确保其在设计预期内无故障运行或快速恢复。

  • 核心目标

    • 发现系统潜在缺陷(如内存泄漏、死锁、资源耗尽)。
    • 验证系统在异常场景下的容错能力(如网络抖动、磁盘故障)。
    • 评估系统恢复时间(MTTR,Mean Time To Recovery)。
  • 典型测试场景

    • 长时间压力测试(7×24小时运行)。
    • 模拟硬件故障(如强制关闭虚拟机节点)。
    • 异常输入测试(如无效请求、超大数据包)。
  • 与高可用测试的区别

    维度 可靠性测试 高可用测试
    侧重点 系统整体稳定性与容错能力 故障时服务的持续可用性
    范围 更广泛(覆盖所有异常场景) 更聚焦(仅针对故障场景的可用性)
    典型指标 MTBF(平均无故障时间)、MTTR SLA(如99.99%可用性)、RTO(恢复时间目标)
    设计原则 容错设计、代码健壮性 冗余架构、故障转移机制

测试之道

在软件工程领域,测试不仅是发现缺陷的手段,更是贯穿产品生命周期的质量反馈机制。以下结合理论与测试实践,对测试工作的核心逻辑进行分析和总结,并探讨其深层意义。

测试的本质是反馈:反馈驱动质量改进

  • 根据ISTQB(国际软件测试资格认证委员会)的定义,测试的核心目标之一是提供质量相关信息。测试结果无论成功与否,均能为团队提供关键信息:
    • 通过:验证功能符合预期,增强信心。
    • 失败:暴露问题,推动改进。
  • 例如,自动化测试的稳定性监控(如每日构建)能够反馈代码变更对系统的影响,即使未发现新缺陷,也能反映当前版本的可靠性。

启示:测试不仅是技术的实践,更是质量的哲学。唯有将反馈思维融入每个环节,才能让测试从“成本中心”蜕变为“价值引擎”。

测试人员的价值:质量的全链路守护者

  • 在敏捷和DevOps实践中,测试角色已从“质检员”转变为质量赋能者。测试人员需:
    • 发现缺陷:通过设计用例覆盖复杂场景。
    • 定位问题:结合日志、监控和代码分析,缩短排查路径(如通过APM工具追踪性能瓶颈)。
    • 推动解决:参与根因分析,提出可落地的优化建议(如优化数据库索引)。
  • 例如,某电商系统在高并发下单时出现超卖,测试人员通过压力测试复现问题后,协助开发引入分布式锁机制,最终解决缺陷。

启示:测试人员的价值不仅在于“发现问题”,更在于加速问题闭环

测试的充分性:始于需求分析,而非用例设计

  • 测试遗漏常源于需求盲区。例如: 某支付系统因需求未明确“余额不足时的重试逻辑”,导致测试未覆盖,上线后引发重复扣款。
  • 解决方法:
    • 早期介入:测试参与需求评审,识别模糊点(如通过实例化需求方法)。
    • 需求可测性:将非功能性需求(如性能、安全)纳入分析范围。

启示:测试充分性的天花板在需求阶段已确定,测试设计只是实现手段。

测试的有效性:效率与价值的平衡

  • 风险驱动测试:优先覆盖高概率、高影响的场景(如核心支付链路)。
  • 自动化分层策略:金字塔模型(单元测试70%、接口测试20%、UI测试10%)优化执行效率。

启示:在有限资源下,测试需聚焦关键质量目标,而非盲目追求覆盖率100%。

缺陷的确定性:科学方法破除“玄学”

  • 无法重现的问题,就说明我们不知道问题到底是什么。
  • 缺陷复现三要素:环境、数据、操作步骤的一致性。
  • 案例:某服务间歇性超时,通过逐步隔离变量(网络延迟、线程池配置、依赖服务状态),最终定位为数据库连接泄漏。

启示:缺陷排查是系统性实验,需用控制变量法定位根因,而非运气游戏。

杀虫剂悖论:测试的进化法则

  • 悖论本质:重复相同的测试会逐渐降低缺陷发现率(Boris Beizer, 1990)。
  • 应对策略:
    • 用例维护:随需求变更同步更新(如版本迭代后删除过期用例)。
    • 变异测试:人工修改代码生成缺陷,验证用例是否捕获。
    • 探索式测试:每周2小时自由探索,记录意外问题。

启示:测试资产是活文档,需持续迭代以匹配系统演进。

测试左移与右移:质量共同体建设

  • 测试左移
    • 需求阶段:与产品共建,通过实例化需求(Specification by Example)定义验收标准,避免歧义。例如,使用Gherkin语法编写用户故事,直接生成自动化测试用例。
    • 开发阶段:与开发协作,推动单元测试与代码评审,结合SonarQube等工具检测代码坏味道。
  • 测试右移
    • 运维阶段:与运维协作,生产环境通过监控(如Prometheus)、日志(如ELK)和A/B测试验证实际用户场景。
    • 混沌工程:利用混沌工程工具(Chaos Mesh)模拟生产故障(如网络分区、节点宕机),验证系统韧性。

启示:测试活动应贯穿需求、开发、交付、运维的全生命周期。

质量指标可视化:从经验主义到科学决策

  • 缺陷模式分析
    • 通过历史缺陷数据识别高频问题模块(如支付服务占线上缺陷的40%),针对性增强测试覆盖。
    • 使用热图(Heatmap)标记代码改动频繁的区域,优先测试。
  • 效果度量
    • 定义核心指标:缺陷逃逸率(<5%)、测试用例有效性(缺陷发现数/用例数)、自动化ROI(>1:3);
    • 搭建质量看板:Grafana集成Jira、Jenkins数据;
    • 每周同步:在站会上展示指标趋势,驱动改进

启示:测试策略应基于数据而非直觉,包括测试优先级、资源分配和风险评估。

自动化不是银弹:平衡人机协作

  • 自动化适用场景
    • 高频回归(如核心链路)、数据驱动测试(如参数组合)、精准重复操作(如性能测试)。
  • 探索式测试优势
    • 发现“意料之外”的问题(如界面渲染异常、多步骤交互副作用)。
    • 快速验证新功能逻辑,弥补自动化滞后性。
  • 平衡策略
    • 80%核心场景自动化 + 20%探索式测试覆盖长尾场景。
    • 使用Session-Based Test Management(SBTM)管理探索式测试过程。

启示:自动化解决效率问题,探索式测试(ET)解决认知问题,二者缺一不可。

测试工程师的核心竞争力:技术深度与业务广度的平衡

技术能力

  • 读懂代码逻辑:快速定位缺陷根源(如空指针异常、事务未提交)。
  • 理解系统架构:设计分布式场景测试(如Redis缓存一致性、MQ消息幂等)。

业务能力

  • 掌握领域知识:金融测试需理解清算规则、医疗系统需符合HL7协议。
  • 用户视角思维:从用户操作路径设计端到端场景(如电商下单→支付→退货)。

提升路径

  • 技术深度:每月研究一个开源项目(如Kubernetes测试框架);
  • 业务广度:季度性轮岗(如参与产品需求设计);
  • 自动化思维:将重复工作脚本化(如用Python自动生成测试报告)

安装相关问题记录

1、没有list ep的权限

解决办法:手动部署一个rolebiding文件(为啥该文件没有自动部署?)

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: chaos-mesh-chaos-controller-manager-control-plane
namespace: chaos-testing
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: chaos-mesh-chaos-controller-manager-control-plane
subjects:
- kind: ServiceAccount
name: chaos-controller-manager
namespace: chaos-testing

2、chaos pod没有创建出来(偶现)

pod安全策略导致,错误如下

解决办法:手动部署一个psp文件

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: psp-chaos-testing
namespace: chaos-testing
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: psp-privileged
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:serviceaccounts:chaos-testing

3、ARM环境编译镜像遇到的问题记录

1
2
3
4
安装docker(20+版本以上,需要支持 --load 参数)
安装golang
git clone https://github.com/chaos-mesh/chaos-mesh.git
cd chaos-mesh && UI=1 make chaos-dashboard

1、make过程中报–platform错误

https://thenew[stack.io/how-to-enable-docker-experimental-features-and-encrypt-your-login-credentials/](http://stack.io/how-to-enable-docker-experimental-features-and-encrypt-your-login-credentials/)

2、make过程中遇到TLSHandshakeTimeout

在arm机器上go get则正常,容器内go get mod包出现TLSHandshakeTimeout 解决办法:在Makefile文件中增加docker run参数,把容器网络改为主机网络,重新make编译即可解决。 具体问题原因链接见:

https://github.com/goproxy/goproxy.cn/issues/26

模拟 Pod 故障

PodChaos 介绍

PodChaos 是 Chaos Mesh 中的一种故障类型,通过创建 PodChaos 类型的混沌实验,你可以模拟指定 Pod 或者容器发生故障的情景。目前,PodChaos 支持模拟以下故障类型:

  • Pod Failure:向指定的 Pod 中注入故障,使得该 Pod 在一段时间内处于不可用的状态。
  • Pod Kill:杀死指定的 Pod 。为了保证 Pod 能够成功重启,需要配置 ReplicaSet 或者类似的机制。
  • Container Kill:杀死位于目标 Pod 中的指定容器。

使用限制:Chaos Mesh 不支持向独立的 Pod 中注入故障,独立的 Pod 指未绑定到 ReplicaSet 或 Deployment 的 Pod。

pod-failure 示例

*PodFailure 是通过 Patch Pod 对象资源,用错误的镜像替换 Pod 中的镜像。Chaos 只修改了 containersinitContainersimage 字段,这也是因为 Pod 大部分字段是无法更改的。默认用于引发故障的容器镜像是 **gcr.io/google-containers/pause:latest**, 如果在国内环境使用,大概率会水土不服,可以将 gcr.io 替换为 **registry.aliyuncs.com*

1、将实验配置写入到文件中 pod-failure.yaml,内容示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: pod-failure-example
namespace: chaos-testing # 指定ns下创建PodChaos实例,缺省default
spec:
action: pod-failure
mode: one
duration: '30s'
selector:
namespaces:
- ems
labelSelectors:
'application': 'ecp-dashboard'

2、启动该实验,Chaos Mesh 将向指定的 Pod 中注入 pod-failure 故障,将使该 Pod 在 30 秒内处于不可用的状态。

1
2
3
4
5
[root@node-4 experiments]# kos apply -f pod-failure-example.yaml
podchaos.chaos-mesh.org/pod-failure-example created
[root@node-4 experiments]# kos get PodChaos
NAME AGE
pod-failure-example 40s

3、执行命令 kos describe PodChaos pod-failure-example,可以查看到该实验的目标pod name,以及30s后实验结束并恢复故障

4、查看pod,可知目标pod出现了故障,被重启过一次

pod-kill 示例

PodKill 的具体实现其实是通过调用 API Server 发送 Kill 命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: pod-kill-example
namespace: chaos-testing
spec:
action: pod-kill
mode: all
#gracePeriod: 10
selector:
namespaces:
- ems
labelSelectors:
'application': 'ecp-dashboard'

依据此配置示例,Chaos Mesh 将向指定的 Pod 中注入 pod-kill 故障,将使该 Pod 被杀死一次。

Tips:

1、实验被创建后,pod立刻就被全部kill;pod在被杀死一次后,实验立刻就结束了。

2、GracePeriod 参数适用于 K8s 强制终止 Pod。例如在需要快速删除 Pod 时,我们使用 kubectl delete pod --grace-period=0 --force 命令。

3、如果要达到持续kill的场景,需要结合schedule来运行实验

container-kill 示例

ContainerKill 不同于 PodKill 和 PodFailure,后两个都是通过 K8s API Server 控制 Pod 生命周期,而 ContainerKill 是通过运行在集群 Node 上的 Chaos Daemon 程序操作完成。具体来说,ContainerKill 通过 Chaos Controller Manager 运行客户端向 Chaos Daemon 发起 grpc 调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: container-kill
namespace: chaos-testing
spec:
action: container-kill
mode: all
duration: '60s'
containerNames: ['ecp-dashboard']
selector:
namespaces:
- ems
labelSelectors:
'application': 'ecp-dashboard'

依据此配置示例,Chaos Mesh 将向指定的 Container 中注入 container-kill 故障,将使该 Container 被杀死一次。

字段说明

参数 类型 说明 默认值 是否必填 示例
action string 指定要注入的故障类型,仅支持 pod-failure、pod-kill、container-kill pod-kill
mode string 指定实验的运行方式,可选择的方式包括:one(表示随机选出一个符合条件的 Pod)、all(表示选出所有符合条件的 Pod)、fixed(表示选出指定数量且符合条件的 Pod)、fixed-percent(表示选出占符合条件的 Pod 中指定百分比的 Pod)、random-max-percent(表示选出占符合条件的 Pod 中不超过指定百分比的 Pod) one
value string 取决与 mode 的配置,为 mode 提供对应的参数。例如,当你将 mode 配置为 fixed-percent 时,value 用于指定 Pod 的百分比。 2
selector struct 指定注入故障的目标 Pod,详情请参考定义实验范围
containerNames []string 当你将 action 配置为 container-kill 时,此配置为必填,用于指定注入故障的目标 container 名 [‘prometheus’]
gracePeriod int64 当你将 action 配置为 pod-kill 时,需要填写此项,用于指定删除 Pod 之前的持续时间 0 0
duration string 指定实验的持续时间 30s

模拟压力场景

StressChaos 介绍

StressChaos 类型的混沌也是由 Chaos Daemon 实施的,Controller Manager 计算好规则后就将任务下发到具体的 Daemon 上。拼装的参数如下,这些参数会组合成命令执行的参数,附加到 stress-ng 命令后执行

Chaos Mesh 提供的 StressChaos 实验类型可用于模拟容器内压力的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: chaos-mesh.org/v1alpha1
kind: StressChaos
metadata:
name: stress
namespace: chaos-testing
spec:
mode: all
duration: ''
selector:
namespaces:
- ems
labelSelectors:
'application': 'ecp-dashboard'
stressors:
memory:
workers: 2
size: '1024MB'
cpu:
workers: 2
load: 100

该实验配置会在选中容器中创建worker进程,不断分配和在内存中进行读写,最多占用 1024MB 内存和2核cpu

模拟文件 I/O 故障

IOChaos 介绍

目前,IOChaos 支持模拟以下故障类型:

  • latency:为文件系统调用加入延迟
  • fault:使文件系统调用返回错误
  • attrOverride:修改文件属性
  • mistake:使文件读到或写入错误的值

注意事项

  1. 创建 IOChaos 实验前,请确保目标 Pod 上没有运行 Chaos Mesh 的 Controller Manager。
  2. IOChaos 可能会损坏你的数据,在生产环境中请谨慎使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
metadata:
name: io-latency-example
namespace: chaos-testing
spec:
action: latency
mode: one
selector:
namespaces:
- openstack
labelSelectors:
application: busybox
volumePath: /host-log/containers
path: '/host-log/containers/**/*'
delay: '1000ms'
percent: 50
duration: '400s'

依据此配置示例,Chaos Mesh 将向 /host-log/containers 目录注入延迟故障,使该目录下的所有文件系统操作(包括读,写,列出目录内容等)产生 1 秒延迟。

注入故障时遇到该问题

计划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: chaos-mesh.org/v1alpha1
kind: Schedule
metadata:
name: ovn-pod-random-kill
namespace: chaos-testing
spec:
concurrencyPolicy: Forbid
historyLimit: 10
podChaos:
action: pod-kill
duration: 48h
gracePeriod: 0
mode: one
selector:
labelSelectors:
application: ovn
namespaces:
- openstack
schedule: '*/30 * * * *'
startingDeadlineSeconds: 86400
type: PodChaos

Schedule实验场景

每隔30分钟,随机kill符合条件的pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: chaos-mesh.org/v1alpha1
kind: Schedule
metadata:
name: ovn-pod-kill
namespace: chaos-testing
spec:
concurrencyPolicy: Forbid
historyLimit: 10
podChaos:
action: pod-kill
gracePeriod: 0
mode: one
selector:
labelSelectors:
application: ovn
namespaces:
- openstack
schedule: '*/30 * * * *'
type: PodChaos

每隔10分钟,指定kill container proton-server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: chaos-mesh.org/v1alpha1
kind: Schedule
metadata:
name: ovn-container-kill
namespace: chaos-testing
spec:
concurrencyPolicy: Forbid
historyLimit: 10
podChaos:
action: container-kill
duration: 1h
containerNames:
- proton-server
gracePeriod: 0
mode: one
selector:
labelSelectors:
application: proton
namespaces:
- openstack
schedule: '*/10 * * * *'
type: PodChaos

每隔10分钟,随机注入pod产生网络延迟,持续2min

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kind: Schedule
apiVersion: chaos-mesh.org/v1alpha1
metadata:
name: ovndb-sb-network-delay
spec:
schedule: '*/10 * * * *'
startingDeadlineSeconds: 3600
concurrencyPolicy: Forbid
historyLimit: 10
type: NetworkChaos
networkChaos:
selector:
namespaces:
- openstack
labelSelectors:
component: ovn-ovsdb-sb
mode: one
action: delay
duration: 120s
delay:
latency: 1s
correlation: '100'
jitter: '0ms'
#direction: to

每隔10分钟,随机注入pod产生压力,持续2min

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kind: Schedule
apiVersion: chaos-mesh.org/v1alpha1
metadata:
name: ovn-controller-cpu-load-80
spec:
schedule: '*/60 * * * *'
startingDeadlineSeconds: 60
concurrencyPolicy: Forbid
historyLimit: 10
type: StressChaos
stressChaos:
selector:
namespaces:
- openstack
labelSelectors:
component: ovn-controller
mode: one
stressors:
memory:
workers: 1
size: '1024'
cpu:
workers: 2
load: 80
duration: 120s

混沌工程

混沌工程是什么

混沌工程(Chaos Engineering)是在分布式系统上进行实验的学科, 目的是建立对系统抵御生产环境中失控条件的能力以及信心。

为什么需要混沌工程

  1. 分布式系统的复杂性挑战
  • 服务依赖挑战:微服务架构下服务间调用链路复杂,单点故障可能引发级联失效
  • 环境动态性:云原生环境中的弹性伸缩、自动扩缩容&回滚增加了不确定性
  • 传统测试局限:单元测试/集成测试难以覆盖真实生产环境中的复杂故障场景
  1. 业务稳定性需求
  • 金融级SLA要求:99.999%可用性目标需要验证极端场景下的系统表现
  • 故障恢复能力验证:验证熔断、降级、重试等容错机制的有效性

总之,云原生的发展不断推进着微服务的进一步解耦,海量的数据与用户规模也带来了基础设施的大规模分布式演进。分布式系统天生有着各种相互依赖,可以出错的地方数不胜数,处理不好就会导致业务受损,或者是其他各种无法预期的异常行为。

混沌工程原理

混沌工程的核心特征是对照观测实验

为了具体地解决分布式系统在规模上的不确定性,可以把混沌工程看作是为了揭示系统弱点而进行的实验。这些实验通常包括如下步骤:

  1. 定义稳态:通过可量化的指标(响应时间、错误率等)定义系统正常运行的表现,为后续测试的有效性提供基准。
  2. 构建假设:提出系统对故障的预期反应,确保在实验过程中有明确的衡量标准。
  3. 故障注入:在实验组中主动注入特定的故障如 (Chaos Mesh、Chaos Monkey),以模拟生产环境中可能遇到的真实问题。如节点故障、硬盘故障、网络断开等。
  4. 监控和观察:监控系统指标变化及恢复时间,对比实验组/对照组之前的数据差异,评估是否符合实验假设。
  5. 系统改进:根据实验结果,优化系统的故障处理机制,提升其可靠性。

破坏稳态的难度越大,我们对系统行为的信心就越强。如果发现了一个弱点,那么我们就有了一个改进目标,避免在系统规模化之后被放大。

img

混沌工程的重要原则

以下原则描述了混沌工程的理想应用,应用于上述的实验过程。遵循这些原则的程度与我们对大规模分布式系统的信心密切相关。

在生产环境中运行实验

系统的行为因环境和流量模式而异。由于利用率的行为随时可能发生变化,因此对真实流量进行采样是可靠捕获请求路径的唯一方法。为了保证系统运行方式的真实性以及与当前部署系统的相关性,Chaos 强烈倾向于直接在生产流量上进行试验。

自动化实验以连续运行

手动运行实验是劳动密集型的,最终是不可持续的。自动化实验并连续运行它们。混沌工程将自动化构建到系统中,以驱动编排和分析。

最小化爆照半径

混沌工程具备导致生产环境崩溃的风险,所谓“最小化爆炸半径”就是尽量让薄弱环节暴露出来,又不会造成更大规模的故障。

可以采用分阶段实验策略:

  1. 单服务实例故障
  2. 单集群故障
  3. 跨区域故障

另外,《混沌工程》中详细介绍了几个步骤:

  • 采用递进的方式进行实验;
  • 只向一小部分终端(用户)注入故障;
  • 开始进行小规模的扩散实验(需要定义好指标来过滤真正通过测试的用户);
  • 小规模集中实验,不断扩大实验范围(有定义的路由实验,无定义路由的大规模实验……);
  • 在实验造成过多危害时,自动终止实验;
  • 避免在高风险时段进行实验(所有人都要在工作状态内)
  • 运行实验本身不能对系统造成过大影响,造成结果偏移。每次只检验一个可控故障;
img

混沌测试工具调研

Chaos Mesh

2019 年 12 月 31 日,PingCap在 GitHub 上正式开源了 Chaos Mesh。作为一个云原生的混沌测试平台,Chaos Mesh 提供在 Kubernetes 平台上进行混沌测试的能力。

Chaos Mesh 通过运行在 K8s 集群中的 “特权” 容器,依据 CRD 资源中的测试场景,在集群中制造混沌(模拟故障)。

架构设计

Chaos Mesh 的架构如下:

  • Chaos Dashboard: a Web UI for managing, designing, monitoring Chaos Experiments.
  • Controller-manager: used to schedule and manage the lifecycle of CRD objects
  • Chaos-daemon: runs as daemonset with privileged system permissions over network, Cgroup, etc. for a specific node

CRD 设计

Chaos Mesh 中使用 CRD 来定义 chaos 对象,在 Kubernetes 生态中 CRD 是用来实现自定义资源的成熟方案,又有非常成熟的实现案例和工具集,这样就可以避免重复造轮子。并且可以更好的融合到 Kubernetes 生态中。

Chaos Mesh 中 CRD 的定义可以自由发挥,根据不同的错误注入类型,定义单独的 CRD 对象。如果新添加的错误注入符合已有的 CRD 对象定义,就可以拓展这个 CRD 对象;如果是一个完全不同的错误注入类型,也可以自己重新增加一个 CRD 对象,这样的设计可以将不同的错误注入类型的定义以及逻辑实现从最顶层就抽离开,让代码结构看起来更加清晰,并且降低了耦合度,降低出错的几率。另一方面 controller-runtime 提供了很好的 controller 实现的封装,不用去对每一个 CRD 对象去自己实现一套 controller 的逻辑,避免了大量的重复劳动。

目前在 Chaos Mesh 中设计了三个 CRD 对象,分别是 PodChaos、NetworkChaos 以及 IOChaos,从命名上就可以很容易的区分这几个 CRD 对象分别对应的错误注入类型。

工作原理

img

上图是 Chaos Mesh 的基本工作流原理图:

  • 目前 controller-manager 可以分为两部分,一部分 controllers 用于调度和管理 CRD 对象实例,另一部分为 admission-webhooks 动态的给 Pod 注入 sidecar 容器。
  • Chaos-daemon 以 daemonset 的方式运行,并具有 Privileged 权限,Chaos-daemon 可以操作具体 Node 节点上网络设备以及 Cgroup 等。
  • Sidecar contianer 是一类特殊的容器,由 admission-webhooks 动态的注入到目标 Pod 中

整体工作流如下:

  1. 用户通过 YAML 文件或是 Kubernetes 客户端往 Kubernetes API Server 创建或更新 Chaos 对象。
  2. Chaos-mesh 通过 watch API Server 中的 Chaos 对象创建更新或删除事件,维护具体 Chaos 实验的运行以及生命周期,在这个过程中 controller-manager、chaos-daemon 以及 sidecar 容器协同工作,共同提供错误注入的能力。
  3. Admission-webhooks 是用来接收准入请求的 HTTP 回调服务,当收到 Pod 创建请求,会动态修改待创建的 Pod 对象,例如注入 sidecar 容器到 Pod 中。第 3 步也可以发生在第 2 步之前,在应用创建的时候运行。

功能支持

故障注入

故障注入是混沌实验的核心。Chaos Mesh 充分考虑分布式系统可能出现的故障,提供全面、细粒度的故障类型,分为基础资源类型故障、平台类型故障和应用层故障三大类。

  • 基础资源类型故障:
    • PodChaos:模拟 Pod 故障,例如 Pod 节点重启、Pod 持续不可用,以及特定 Pod 中的某些容器故障。
    • NetworkChaos:模拟网络故障,例如网络延迟、网络丢包、包乱序、各类网络分区。
    • DNSChaos:模拟 DNS 故障,例如 DNS 域名解析失败、返回错误 IP 地址。
    • HTTPChaos:模拟 HTTP 通信故障,例如 HTTP 通信延迟。
    • StressChaos:模拟 CPU 抢占或内存抢占场景。
    • IOChaos:模拟具体某个应用的文件 I/O 故障,例如 I/O 延迟、读写失败。
    • TimeChaos:模拟时间跳动异常。
    • KernelChaos:模拟内核故障,例如应用内存分配异常。
  • 平台类型故障:
    • AWSChaos:模拟 AWS 平台故障,例如 AWS 节点重启。
    • GCPChaos:模拟 GCP 平台故障,例如 GCP 节点重启。
  • 应用层故障:
    • JVMChaos:模拟 JVM 应用故障,例如函数调用延迟。

混沌实验场景

用户运行混沌场景,可以通过一系列的混沌实验,不断地扩大爆炸半径(包括攻击范围)和增加故障类型。运行混沌实验后,用户可以方便地检查当前的应用状态,判断是否需要进行后续混沌实验。同时用户可以不断地迭代混沌实验场景,积累混沌实验场景,以及方便地将已有的混沌实验场景复用到其他应用混沌实验中,大大降低了混沌实验的成本。

目前混沌实验场景提供的功能有:

  • 编排串行混沌实验
  • 编排并行混沌实验
  • 支持状态检查步骤
  • 支持中途暂停混沌实验
  • 支持使用 YAML 文件定义和管理混沌实验场景
  • 支持通过 Web UI 定义和管理混沌实验场景

具体的实验场景配置,参考具体创建 Chaos Mesh Workflow

可视化界面操作

Chaos Mesh 为用户提供了单独的 Chaos Dashboard 组件,即可视化支持。Chaos Dashboard 极大地简化了混沌实验,用户可以直接通过可视化界面来管理和监控混沌实验,仅需点一点鼠标就能够定义混沌实验的范围、指定混沌注入的类型、定义调度规则,以及在界面上获取到混沌实验的结果等。

安全保障

Chaos Mesh 通过 Kubernetes 原生的 RBAC(基于角色的权限控制)功能对权限进行管理。

用户可以根据实际的权限需求自由地创建多种 Role,然后绑定到用户名 Service Account 上,最后生成 Service Account 对应的 Token。用户使用该 Token 登陆 Dashboard,只能在该 Service Account 允许的权限范围内进行 Chaos 实验。

此外 Chaos Mesh 还支持通过设置 Namespace Annotation 的方式开启特定 Namespace 下混沌实验的权限,进一步保障混沌实验的可控性。

如何安装

脚本安装

1
2
wget -O /tmp/install.sh https://mirrors.chaos-mesh.org/latest/install.sh
sh /tmp/install.sh -r containerd

helm安装

1
2
3
4
5
6
7
8
git clone https://github.com/pingcap/chaos-mesh.git
cd chaos-mesh
// 创建 CRD 资源
kubectl apply -f manifests/
// 安装 Chaos-mesh
helm install helm/chaos-mesh --name=chaos-mesh --namespace=chaos-testing
// 检查 Chaos-mesh 状态
kubectl get pods --namespace chaos-testing -l app.kubernetes.io/instance=chaos-mesh

⚠️ Tips:docker hub上只有Chaos-Mesh每个Release版本的X86镜像,但是目前没有ARM镜像!

如果安装环境是ARM,则需要在安装时需要指定仓库地址为 ghcr.io/chaos-mesh/chaos-mesh/ ,或者安装后手动修改镜像地址。

1
2
3
kubectl set image deploy chaos-controller-manager chaos-mesh=ghcr.io/chaos-mesh/chaos-mesh/chaos-mesh:latest-arm64 -nchaos-testing
kubectl set image deploy chaos-dashboard chaos-dashboard=ghcr.io/chaos-mesh/chaos-mesh/chaos-dashboard:latest-arm64 -nchaos-testing
kubectl set image ds chaos-daemon chaos-daemon=ghcr.io/chaos-mesh/chaos-mesh/chaos-daemon:latest-arm64 -nchaos-testing

Chaos Blade

Chaosblade 是内部 MonkeyKing 对外开源的项目,其建立在阿里巴巴近十年故障测试和演练实践基础上,结合了集团各业务的最佳创意和实践。

如何部署

容器内快速体验

1
2
3
docker pull chaosbladeio/chaosblade-demo

docker run -it --privileged chaosbladeio/chaosblade-demo

二进制包安装

获取 ChaosBlade 最新的 release 包,目前支持的平台是 linux/amd64 和 darwin/64,下载对应平台的包。

https://github.com/chaosblade-io/chaosblade/releases

下载完成后解压即可,解压之后的文件中有一个blade的可执行文件,这就是Chaosblade提供的客户端工具

架构设计

功能支持

ChaosBlade 不仅使用简单,而且支持丰富的实验场景,场景包括:

  • 基础资源:比如 CPU、内存、网络、磁盘、进程等实验场景;
  • Java 应用:比如数据库、缓存、消息、JVM 本身、微服务等,还可以指定任意类方法注入各种复杂的实验场景;
  • C++ 应用:比如指定任意方法或某行代码注入延迟、变量和返回值篡改等实验场景;
  • Docker 容器:比如杀容器、容器内 CPU、内存、网络、磁盘、进程等实验场景;
  • 云原生平台:比如 Kubernetes 平台节点上 CPU、内存、网络、磁盘、进程实验场景,Pod 网络和 Pod 本身实验场景如杀 Pod,容器的实验场景如上述的 Docker 容器实验场景

混沌实验模型

img

以上所有的实验场景都遵循混沌实验模型,此模型共分为四层,包含:

  • Target:实验靶点。指实验发生的组件,如容器、应用框架(Dubbo、Redis)等;
  • Scope:实验实施的范围。指具体触发实验的机器或者集群等;
  • Matcher:实验规则匹配器。根据所配置的 Target,定义相关的实验匹配规则,可以配置多个。由于每个 Target 可能有各自特殊的匹配条件,比如 RPC 领域的 Dubbo,可以根据服务提供者提供的服务和服务消费者调用的服务进行匹配,缓存领域的 Redis,可以根据 set、get 操作进行匹配;
  • Action:指实验模拟的具体场景,Target 不同,实施的场景也不一样,比如磁盘,可以演练磁盘满,磁盘 IO 读写高等。如果是应用,可以抽象出延迟、异常、返回指定值(错误码、大对象等)、参数篡改、重复调用等实验场景。

面向云原生

img

将混沌实验场景按照上述的实验模型,定义为 Kubernetes 中的资源,并通过自定义控制器来管理,可以通过 Yaml 配置或者直接执行 blade 命令执行。

如何使用

使用方式有两种:

  • 一种是通过配置 yaml 方式,使用 kubectl 执行;
  • 另一种是直接使用 chaosblade 包中的 blade 命令执行。

yaml 配置方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: [chaosblade.io/v1alpha1](http://chaosblade.io/v1alpha1)

kind: ChaosBlade
metadata:
name: cpu-load
spec:
experiments:
- scope: node
target: cpu
action: fullload
desc: "increase node cpu load by names"
matchers:
- name: names
value:
- "cn-hangzhou.192.168.0.205"
- name: cpu-percent
value:
- "80"

Blade相关命令

  • blade create - 创建一个混沌实验
  • blade_destroy.md - 销毁一个混沌实验
  • blade_prepare.md - 准备混沌实验环境,部分实验执行前必须执行
  • blade_revoke.md - 撤销混沌实验环境,与 prepare 操作对应
  • blade status - 查询混沌实验和混沌实验环境状态
  • blade_query.md - 查询部分实验所需的系统参数
  • blade version - 打印 blade 工具版本信息
  • blade server - server 模式
img

工具调研总结

基于对以上混沌工程的工具相关资料整理和学习,对三款工具进行对比总结如下:

Chaos Monkey Chaos Mesh(1.2.2) Chaos Blade(1.2.0)
安装是否有特定依赖 Spinnaker
部署方式 Spinnaker上配置启用 helm部署、脚本部署 二进制包安装、容器直接运行
功能和特点 功能和实验场景单一,仅支持终止实例 不能支持云原生的实验场景 场景丰富度高,支持云原生 应用无侵入,类似于istio的sidecar设计 场景丰富度极高,支持云原生 应用无侵入,扩展性强
易用性 使用上手较困难 除了yaml文件编排,提供了dashboard编排界面; 单独开发了grafana插件,易于观测实验前后结果 简洁易用 通过 CLI 方式执行,具有友好的命令提示功能
社区活跃度 release周期长,最近release版本是在2016年 国内成熟的实践案例和相关资料较少 活跃度高 活跃度高,且国内已有较多成熟的实践案例

参考资料:

https://www.infoq.cn/article/xbbm7mft8lecbzqh2bnw

https://github.com/chaosops/awesome-chaos-engineering

https://www.infoq.cn/article/gsqtykoa3uvrtqi1kkmo

https://github.com/chaosblade-io/chaosblade/blob/master/README_CN.md

https://chaos-mesh.org/docs/

https://chaos-mesh.org/blog/Securing-Online-Gaming-Combine-Chaos-Engineering-with-DevOps-Practices/

网络设备

设备 工作层级 功能
集线器 L1 广播数据包到所有端口,无智能过滤
交换机 L2 基于 MAC 地址对数据帧进行转发,支持 VLAN 划分
路由器 L3 基于 IP 地址路由数据包(Packet),隔离广播域,支持 NAT 和防火墙
三层交换机 L3 结合交换机和路由器的功能,高速转发并支持路由策略

交换机

工作原理

交换机根据每个端口接收到的数据帧源地址进行MAC地址学习,内部维护一张MAC地址表。在后续的通讯中,发往特定MAC地址的数据包仅被转发至该MAC地址对应的端口,而非所有端口。

MAC 地址表建立过程

初始状态下,交换机的MAC地址表为空。

  1. 接收数据包:交换机从某个端口接收数据包,交换机会检查数据包的头部信息(包括源MAC和IP地址、目的MAC和IP地址)。同时将源MAC地址和其端口号记录到MAC地址表中
  2. 查看MAC地址表
    • 找到目的MAC -> 交换机对数据包进行端口转发
    • 未找到目的MAC -> 交换机会泛洪数据包,所有主机均会收到该数据包(除了接收端口)
  3. 更新MAC地址表
    • 收到响应数据包 -> 交换机记录响应数据包的源MAC地址和端口号,并更新MAC地址表
    • 未收到响应数据包 -> 说明目的主机可能不在网络中或者没有响应,数据包丢弃(交换机默认行为)

三层交换机

三层交换机(L3 Switch)可以处理网络层协议,通过对缺省网关的查询学习来建立局域网(如企业内网、校园网)不同网段的直接连接。

首次路由与后续转发

  1. 当主机A与不同子网的主机B通信时,三层交换机需要通过缺省网关进行路由决策:
    • 主机A发送ARP请求到缺省网关(三层交换机的IP)。
    • 交换机查询路由表找到目标网段的下一跳地址,并通过ARP广播获取主机B的MAC地址。
  2. 交换机将IP地址与MAC地址的映射记录到转发表(如FIB表和邻接表),并更新二层MAC地址表。
  3. 当数据包再次到达交换机后,直接通过二层转发(MAC地址表),无需重复路由,再次拆包分析IP地址(即“直接连接”)。

网卡Bond

所谓bond,就是把多个物理网卡绑定成一个逻辑上的网卡,使用同一个IP工作,有时服务器带宽不够了也可以用作增加带宽。

借助于网卡bond技术,不仅可以提高网络传输速度,更重要的是,还可以确保在其中一块网卡出现故障时,依然可以正常提供网络服务。

网卡绑定mode共有七种(0~6) bond0、bond1、bond2、bond3、bond4、bond5、bond6。常用的有三种:

  • mode=0(平衡负载模式):平时两块网卡均工作,且自动备援,但需要在与服务器本地网卡相连的交换机设备上进行端口聚合来支持绑定技术。
  • mode=1(自动备援模式):平时只有一块主网卡工作,在它故障后自动替换为另外的网卡。
  • mode=6(平衡负载模式):平时两块网卡均工作,且自动备援,无须交换机设备提供辅助支持。

1.Bond准备工作

首先要确定服务器上的网卡规划用途,以及哪些网卡已插网线,一般是有两块网卡对应两根网线,分别连接不同的交换机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[root@master01 network-scripts]# ethtool p4p2
Settings for p4p2:
Supported ports: [ FIBRE ]
Supported link modes: 1000baseKX/Full
10000baseKR/Full
25000baseCR/Full
25000baseKR/Full
25000baseSR/Full
Supported pause frame use: Symmetric
Supports auto-negotiation: Yes
Supported FEC modes: None BaseR
Advertised link modes: 1000baseKX/Full
10000baseKR/Full
25000baseCR/Full
25000baseKR/Full
25000baseSR/Full
Advertised pause frame use: Symmetric
Advertised auto-negotiation: Yes
Advertised FEC modes: None
Speed: 10000Mb/s
Duplex: Full
Port: FIBRE
PHYAD: 0
Transceiver: internal
Auto-negotiation: on
Supports Wake-on: d
Wake-on: d
Current message level: 0x00000004 (4)
link
Link detected: yes

ethtool查看网卡信息,Link detected:yes表示有网线插入;

如果Link detected:no 的话,尝试用ifup ethxxx,如果依然为no的话,才能说明此网卡确实没有网线插入。

2.网卡Bond配置

方法一:命令行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建一个名为 storagepub 的网卡绑定接口,类型为 bond,模式为 802.3ad
ip link add storagepub type bond mode 802.3ad xmit_hash_policy layer3+4

# 关闭网卡
ip link set ens4np1 down

# 将网卡 ens4np1 加入到名为 storagepub 的绑定接口,成为其从属(slave)设备
ip link set ens4np1 master storagepub

# 启用网卡
ip link set ens4np1 up

# 启用绑定接口 storagepub,使其可用
ip link set storagepub up

# 查看绑定接口 storagepub 的详细信息,包括模式、成员网卡、负载均衡策略等。
cat /proc/net/bonding/storagepub

方法二:修改bond网卡的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@master01 network-scripts]# cat ifcfg-bond-storagepub
DEVICE=storagepub
BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=1"
TYPE=Bond
BONDING_MASTER=yes # 表示该设备是绑定主设备(master)。
BOOTPROTO=static # 使用静态 IP 配置(不依赖 DHCP)
PEERDNS=no
IPV4_FAILURE_FATAL=no
IPV6INIT=no # 不启用 IPv6 配置
NAME=bond-storagepub
ONBOOT=yes
IPADDR=172.22.88.177
NETMASK=255.255.255.0

[root@master01 network-scripts]# cat ifcfg-p4p2
TYPE=Ethernet
BOOTPROTO=static
DEVICE=p4p2
ONBOOT=yes
MASTER=storagepub # 指定绑定接口的主设备为 storagepu。
SLAVE=yes # 指定该网卡是绑定接口的从设备(slave)

3.重启网络验证

1
2
3
4
5
6
[root@master01 ~]# systemctl restart network

[root@master01 ~]# ip a |grep storagepub
11: p4p2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master storagepub state UP group default qlen 1000
19: storagepub: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet 172.22.88.177/24 brd 172.22.88.255 scope global noprefixroute storagepub

常见问题

交换机的VLAN怎么划分的

为了解决广播域带来的问题,引入了VLAN (Virtual Local Area Network),即虚拟局域网技术:

通过在交换机上部署VLAN,可以将一个规模较大的广播域在逻辑上划分成若干个规模较小的广播域,由此可以有效地提升网络的安全性,同时减少垃圾流量,节约网络资源。

每个VLAN网络(子网)是一个独立的广播域,VLAN内的设备可以相互通信,而不同VLAN之间的设备默认不能直接通信,必须通过三层设备(如路由器或三层交换机)进行路由才能互通。

划分VLAN的方法有多种,常见的包括:

  • 基于端口划分(常用):将交换机的端口分配到不同的VLAN中。例如,将端口1和2分配到VLAN10,端口3和4分配到VLAN20。
  • 基于MAC地址划分:根据设备的MAC地址将设备分配到不同的VLAN中。
  • 基于IP子网划分:根据设备的IP地址和子网掩码来划分VLAN。
  • 基于协议划分:根据设备使用的协议类型来划分VLAN。

不同网段如何实现通信

不同网段的设备需要通过 路由设备 实现通信,具体流程如下:

  1. 主机A(192.168.1.10)向主机B(192.168.2.20)发送数据包:
    • 主机A发现目标IP不在同一网段,因此将数据包的目标MAC地址设为 默认网关(如L3交换机的IP 192.168.1.1)。
  2. 三层交换机接收数据包:
    • 根据目标IP 192.168.2.20 查询路由表,找到下一跳接口(如连接192.168.2.0网段的端口)。
    • 发送 ARP请求 到目标网段,获取主机B的MAC地址。
  3. 转发数据包:
    • L3交换机将数据包的目标MAC改为主机B的MAC,源MAC改为自己的接口MAC,然后通过二层转发到目标网段。
  4. 后续通信优化:
    • 三层交换机会缓存 IP-MAC映射 和 路由路径,后续数据包直接通过二层转发,无需重复路由。

路由器和交换机区别

  1. 交换机一般工作在数据链路层,而路由器则工作在网络层。
  2. 路由器可连接超过两个以上不同的网络,而交換机只能连接两个。
  3. 交换机是根据MAC地址转发数据帧,而路由器则是根据IP地址来转发IP数据包/分组。
  4. 交换机主要是用于组建局域网,而路由器则更多用于广域网(WAN)或异构网络互联。

TCP/IP 模型

TCP/IP 层级 对应 OSI 层级 功能描述 核心协议
应用层 L5-L7(会话层、表示层、应用层) 面向用户,处理具体应用需求 HTTP、FTP、DNS、SMTP、SSH
传输层 L4 确保数据“端到端”可靠传输 TCP、UDP
网络层 L3 转发和路由 IP、ICMP、ARP、BGP
链路层 L1-L2(数据链路层、物理层) 物理介质传输比特流 以太网、Wi-Fi、MAC 地址

常见网络协议

HTTP 协议

基本概念

HTTP 是超文本传输协议,也就是HyperText Transfer Protocol。具体来说,主要是来规范浏览器和服务器端的行为的。并且,HTTP 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。

常见状态码

1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

  • 101 Switching Protocols」协议切换,服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求。比如切换到一个实时且同步的协议(如 WebSocket)以传送利用此类特性的资源。

2xx 类状态码表示服务器成功处理了客户端的请求。

  • 200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
  • 204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
  • 206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向

  • 301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。

  • 302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

  • 304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

  • 400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。
  • 401 Unauthorized」表示客户端请求没有进行验证或验证失败。
  • 403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
  • 404 Not Found」表示请求的资源在服务器上不存在或未找到。

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

  • 500 Internal Server Error」与 400 类似,是个笼统通用的错误码。
  • 501 Not Implemented」表示请求的方法不被服务器支持。
  • 502 Bad Gateway」表示作为网关或者代理的服务器尝试执行请求时,从上游服务器接收到无效的响应。
  • 503 Service Unavailable」表示服务器尚未处于可以接受请求的状态,类似“网络服务正忙,请稍后重试”的意思。
  • 504 Gateway Timeout」表示作为网关或者代理的服务器无法在规定的时间内,从上游服务器获得想要的响应。

TCP 协议

基本概念

TCP 的全称叫传输控制协议(Transmission Control Protocol),

特点

面向连接、可靠传输(流量控制、超时重传、拥塞控制) 、基于字节流。

端口号

一台设备上可能会有很多应用在接收或者传输数据,因此需要用一个编号将应用区分开来,这个编号就是端口。比如 80 端口通常是 Web 服务器用的,22 端口通常是远程登录服务器用的。而对于浏览器(客户端)中的每个标签栏都是一个独立的进程,操作系统会为这些进程分配临时的端口号。由于传输层的报文中会携带端口号,因此接收方可以识别出该报文是发送给哪个应用。

UDP 协议

基本概念

UDP(User Datagram Protocol)是一个简单的面向消息的传输层协议。UDP 只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。当然,UDP 也可以实现可靠传输,则必须在用户应用程序中实现。

特点

无连接、不可靠、低延迟

IP 协议

基本概念

网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 报头组装成 IP 报文,以便它们可以跨网络传播并到达正确的目的地。如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文。

子网

IP 协议使用 IP 地址 来区分互联网上的每一台设备。但是一个单纯的 IP 地址,寻址起来还是特别麻烦,全世界那么多台设备,难道一个一个去匹配?这就产生了子网的概念,即寻址过程中,先要匹配找到同一个子网,才会去找对应的主机。

路由

IP 协议另一个重要的能力就是路由。实际场景中,两台设备并不是用一条网线连接起来的,而是通过很多网关、路由器、交换机等众多网络设备连接起来的,那么就会形成很多条网络的路径。路由器寻址工作中,就是要找到目标地址的子网,找到后进而把数据包转发给对应的网络内。

计算机网络面试题

HTTP/1.1 的优点有哪些

1. 简单

HTTP 基本的报文格式就是 header + body,头部信息也是 key-value 简单文本的形式,易于理解,降低了学习和使用的门槛。

2. 灵活和易于扩展

HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充

同时 HTTP 由于是工作在应用层( OSI 第七层),则它下层可以随意变化,比如 HTTPS 就是在 HTTP 与 TCP 层之间增加了 SSL/TLS 安全传输层;

3. 应用广泛和跨平台

互联网发展至今,HTTP 的应用范围非常的广泛,从台式机的浏览器到手机上的各种 APP,HTTP 的应用遍地开花,同时天然具有跨平台的优越性。

HTTP/1.1 的缺点有哪些

HTTP 协议里有优缺点一体的双刃剑,分别是「无状态队头阻塞」,同时还有一大缺点「不安全」。

1.无状态

HTTP协议的无状态特性意味着服务器不会记录客户端的请求历史,每个请求都被视为独立且无关的。这一设计虽然简化了服务器实现并提升了处理效率,但也带来了以下主要问题:

  • 无法直接识别客户端:服务器无法直接识别客户端,每次都要进行用户身份验证和授权,导致用户登录、个性化推荐等关联功能难以实现。
  • 重复传输大量信息:每个请求都要重复传输大量相同的信息,如Host、Authentication、Cookies等元数据,增加了数据传输量和服务器的处理负担。

2.队头拥塞

「请求 - 应答」的模式加剧了 HTTP 的性能问题。

因为当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」。

3.不安全

HTTP 比较严重的缺点就是不安全:

  • 通信使用明文(不加密),内容可能会被窃听。
  • 不验证通信方的身份,因此有可能遭遇伪装。
  • 无法证明报文的完整性,所以有可能已遭篡改。

怎么解决 HTTP 请求无状态带来的问题

1. Cookie机制

cookie技术
  • 原理:服务器通过响应头的Set-Cookie字段向客户端写入键值对数据,客户端后续请求时携带这些Cookie,从而使服务器能够识别并跟踪用户会话。
  • 局限
    • 数据存储在客户端,存在安全风险(如XSS攻击窃取Cookie)。
    • 单个域名下Cookie大小受限(通常4KB),且每次请求均会携带,可能影响性能

2. Session机制

image
  • 原理:服务器创建唯一Session ID并将其发送给客户端(通常通过Cookie传递),并将用户状态存储在服务端(如内存、数据库)。在后续的请求中,客户端会携带Session ID,服务器根据Session ID在服务器端检索对应的会话状态。
  • 优势:状态数据存储在服务端,安全性较高,适合保存敏感信息(如用户权限)。
  • 局限
    • 服务器需维护Session存储(比如会话同步),高并发场景下可能产生性能瓶颈,且在分布式系统中会限制负载均衡的能力。
    • Session ID依赖Cookie传递,若客户端禁用Cookie则需通过URL重写实现。

3. Token机制

  • 原理:服务器生成包含用户信息的令牌(如JWT),客户端在请求头(如Authorization)中携带该令牌。客户端每次请求时携带该Token,服务器验证后获取用户信息,服务器无需存储会话数据。
  • 优势
    • 无状态设计减轻服务器压力,适合分布式系统。
    • 支持跨域场景和多种客户端(如移动端App)
  • 局限:令牌需定期更新以防止泄露,且需处理令牌吊销问题

4. 其他优化技术

  • 持久连接(Keep-Alive) :HTTP/1.1默认启用,通过复用TCP连接减少握手开销,间接提升状态相关请求的效率(虽不直接解决无状态问题)。
  • 缓存机制:利用强制缓存(如Cache-Control)和协商缓存(如ETag)减少重复数据传输,缓解无状态导致的冗余问题

HTTP 与 HTTPS 有哪些区别?

  • HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输(通常是对称加密数据)。
  • HTTP 连接建立相对简单,TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 比 HTTP 多了 SSL/TLS 的握手过程,才可进入加密报文传输。
  • 端口号:HTTP 默认是 80,HTTPS 默认是 443。
  • HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。

从输入 URL 到页面展示到底发生了什么?

  1. 在浏览器中输入指定网页的 URL。
  2. 浏览器通过 DNS 协议,获取域名对应的 IP 地址。
  3. 浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求(创建套接字 Socket)。
  4. 浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。
  5. 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。
  6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
  7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。

ARP地址解析协议的工作过程

广播发送ARP请求,单播发送ARP响应。

  1. 首先,每个主机都会在自己的ARP缓冲区中建立一个ARP列表(通过arp -n查找),以表示IP地址和MAC地址之间的对应关系。
  2. 当源主机要发送数据时,首先检查ARP列表中是否有对应IP地址的目的主机的MAC地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送ARP数据包,该数据包包括的内容有:源主机 IP地址,源主机MAC地址,目的主机的IP 地址。
  3. 当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址。
  4. 源主机收到ARP响应包后。将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。
2378e655-5580-4f80-b2fc-138287c17ea2

DNS 的作用是什么?

DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是域名和 IP 地址的映射问题

DNS进行域名解析的过程

  1. 简单来说,当在浏览器地址栏中输入某个Web服务器的域名时。主机会发出一个DNS请求到本地DNS服务器(也就是客户端 TCP/IP 设置中填写的 DNS 服务器地址)。
  2. 本地DNS服务器会首先查询高速缓存记录,如果缓存中有此条记录,就可以直接返回结果。若没有查到,本地DNS则将请求发给根域DNS服务器。
  3. 根DNS服务器并不会记录域名和IP地址的对应关系,只会“指路”;即告知本地DNS去迭代查找至顶级域、二级域(.com 、.org),三级域,直至找到要解析的IP地址
  4. 本地DNS 再将IP地址返回给客户端,客户端与目标成功建立连接。同时把对应关系保存在缓存中,加快网络访问。
img

更详细的总结:

浏览器缓存——》系统hosts文件——》本地DNS解析器缓存——》本地域名服务器(本地配置区域资源、本地域名服务器缓存)——》根域名服务器——》主域名服务器——》下一级域名域名服务器 客户端——》本地域名服务器(递归查询) 本地域名服务器—》DNS服务器的交互查询是迭代查询

TCP连接是怎么建立和断开的

建立一个 TCP 连接需要“三次握手”,缺一不可:

  • 一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务端的确认;
  • 二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态;
  • 三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入ESTABLISHED 状态,完成 TCP 三次握手。

断开一个 TCP 连接则需要“四次挥手”,缺一不可:

  1. 第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。
  2. 第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
  3. 第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
  4. 第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。

WebSocket 和 HTTP 有什么区别

WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。

下面是二者的主要区别:

  • WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。并且,HTTP 协议下的通信只能由客户端发起,服务器无法主动通知客户端。
  • WebSocket 使用 ws:// 或 wss://(使用 SSL/TLS 加密后的协议,类似于 HTTP 和 HTTPS 的关系) 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。
  • WebSocket 可以支持扩展,用户可以扩展协议,实现部分自定义的子协议,如支持压缩、加密等。
  • WebSocket 通信数据格式比较轻量,用于协议控制的数据包头部相对较小,网络开销小,而 HTTP 通信每次都要携带完整的头部,网络开销较大(HTTP/2.0 使用二进制帧进行数据传输,还支持头部压缩,减少了网络开销)。

什么时候选择 TCP,什么时候选 UDP

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNSSNMP 等;
  • 视频、音频等多媒体通信;
  • 广播通信;

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输;
  • HTTP / HTTPS;

装饰器

在Python开发中,装饰器(Decorator)是一种强大而优雅的语法特性,它允许在不修改原函数代码的前提下,动态地为函数添加额外功能(如日志记录、性能分析、参数校验等)。本文将从闭包的基础概念出发,深入解析装饰器的实现原理,并通过实际示例展示其应用场景。


闭包:装饰器的基石

在理解装饰器之前,必须先了解闭包(Closure)的概念。闭包是指在函数内部定义的函数(嵌套函数),该函数可以访问外层函数的变量,即使外层函数已经执行完毕。

1
2
3
4
5
6
7
8
def outer():
x = 10
def inner():
print(f"通过闭包访问外层变量x的值: {x}")
return inner # 返回闭包

closure = outer()
closure() # 输出: 通过闭包访问外层变量x的值: 10

在这个例子中,outer 返回 inner,后者可以访问并使用 outer 中的变量 x

装饰器的实现

装饰器的本质是一个接收函数作为参数的外层函数,其内部定义一个闭包(通常命名为wrapper),用于包裹原函数并增强其功能。

1
2
3
4
5
6
7
8
# 装饰器模版
def decorator(func): # 外层函数(装饰器)
@wraps(func)
def wrapper(*args, **kwargs): # 内层函数(闭包)
# do something
result = func(*args, **kwargs) # 访问外层函数的参数 `func`
return result
return wrapper

在上面的例子中,decorator就是一个装饰器。它接受一个函数func作为参数,并返回一个包裹原函数的新函数wrapper

代码解析

  1. @wraps(func) :保留原函数的元数据(如函数名、文档信息)
  2. *args, **kwargs:接受任意、不定数量的参数,增强装饰器的通用性。
  3. 外层return wrapper:确保装饰器返回包装后的函数,其实就是一个闭包函数。
  4. 内层return result:确保原函数的返回值不被丢弃

装饰器应用场景

1.日志记录:在大型项目中,使用装饰器可以轻松地为多个函数添加日志功能,而无需在每个函数中重复编写日志代码。

2.性能分析:通过装饰器可以方便地测量函数的执行时间,帮助开发者识别性能瓶颈。

3.访问控制:在Web应用中,装饰器可以用于实现用户认证和授权,确保只有具有特定权限的用户才能访问某些功能。

4.缓存机制:对于计算密集型函数,使用缓存装饰器可以显著提高程序的执行效率,避免重复计算。

5.输入验证:在数据处理应用中,装饰器可以用于检查函数输入的有效性,提高代码的健壮性。

装饰器函数示例

日志记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import logging
from functools import wraps

def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# logging.getLogger().setLevel(logging.INFO)
logging.info(f"调用函数: {func.__name__}, 参数: {args}, {kwargs}")
result = func(*args, **kwargs)
logging.info(f"函数返回: {result}")
return result
return wrapper

@log_decorator
def add(x, y):
"""两数相加"""
return x + y

add(3, 5) # 自动记录日志

代码解析

  1. @log_decorator是装饰器的应用方式,相当于:add = log_decorator(add)
  2. 装饰器返回的wrapper函数能够访问外部函数log_decorator的作用域,因此可以在原函数执行前后插入自己的逻辑。

函数计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def timer(func):
def wrapper(*args, **kw):
t1=time.time()
# 这是函数真正执行的地方
func(*args, **kw)
t2=time.time()
# 计算下时长
cost_time = t2-t1
print("花费时间:{}秒".format(cost_time))
return wrapper

@timer
def want_sleep(sleep_time):
time.sleep(sleep_time)

want_sleep(10)

函数重试

若需要为装饰器传递参数(如重试次数),需定义一个装饰器工厂函数,它可以根据参数“生产”出不同的装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def repeat(num_times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(num_times=3)
def greet(name):
print(f"Hello {name}!")

greet("Alice") # 输出3次"Hello Alice!"

从以上例子中可以看到,在带参数的装饰器函数中一共有三层函数嵌套,其实剥离出最外面的一层,我们可以发现和简单(不带参数)的装饰器是一样的。

代码解析

  • repeat是一个装饰器工厂函数,负责“生产”返回一个实际的装饰器decorator
  • @repeat(num_times=3)等价于greet = repeat(3)(greet)

类装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器(@decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象

基于类装饰器的实现,必须实现 __call____init__两个内置函数。

不带参数的类装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
class Decorator(object):
def __init__(self, func): # 接收被装饰函数
self.func = func

def __call__(self, *args, **kwargs): # 实现装饰逻辑。
print(f"函数 {self.func.__name__} 被调用")
return self.func(*args, **kwargs)

@Decorator
def say_hello():
print("Hello!")

say_hello() # 输出: 函数 say_hello 被调用 → Hello!

在不带参数的类装饰器中,等价于 func = class(func),此时函数作为了类的实例参数。

带参数的类装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class logger(object):
def __init__(self, level='INFO'): # 接受装饰器参数
self.level = level

def __call__(self, func): # 接受被装饰函数
def wrapper(*args, **kwargs):
print("[{level}]: the function {func}() is running..."\
.format(level=self.level, func=func.__name__))
func(*args, **kwargs)
return wrapper #返回函数

@logger(level='WARNING')
def say(something):
print("say {}!".format(something))

say("hello")

在带参数的类装饰器中,等价于func = class(para)(func)。也就是说,此时函数并不是实例化参数,而变成了callable 的参数,这也就是为什么需要在__call__() 中传入func。

内置装饰器

Python还提供了一些常用的内置装饰器。例如:

  • @property:用于将一个方法转换为属性。广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查。
  • @staticmethod@classmethod:用于定义静态方法和类方法。

总结

  1. 装饰器是闭包的一种应用,闭包为装饰器提供了“记住外部函数参数(如被装饰函数 func)”的能力。
  2. 一切 callable 的对象都可以被用来实现装饰器,比如函数、类。
  3. 装饰器会改变原函数的原始签名,需要使用 functools.wraps
  4. 在内层函数修改外层函数的变量时,需要使用 nonlocal 关键字。
  5. “装饰器”并没有提供某种无法替代的功能,等同于func = decorator(func)。它只是一颗“语法糖”而已,就像乐高积木,可以自由组合各种功能模块,在某些特定场景里可以使代码更简洁易维护。

Python 面试题

数据操作

如何将字符串 “ilovechina” 进行反转

1
2
s = 'ilovechina'
s = s[::-1]

如何将 “1,2,3” 分割成 [“1”,”2”,”3”]?

1
2
s = "1,2,3"
stdout = s.split(',')

Python 中的字符串格式化方式你知道哪些?

Python3.6之后的版本提供了三种字符串格式化的方式

1
2
3
4
5
6
7
8
9
10
11
# 1. %s占位符
def foo(name):
return 'hello %s' % name

# 2. format()
def foo(name):
return 'hello {}'.format(name)

# 3.f-string
def foo(name):
return f'hello {name}'

列表元素怎么去重

自己动手写函数实现(保留原顺序)

1
2
3
4
5
6
old_list = [2, 3, 4, 5, 1, 2, 3]
new_list = []
for i in old_list:
if i not in new_list:
new_list.append(i)
print(new_list) # [2, 3, 4, 5, 1]

用字典dict去重(保留原顺序)

1
2
3
4
# 使用list元素值创建一个dict,这将自动删除任何重复项
old_list = [2, 3, 4, 5, 1, 2, 3]
new_list = list(dict.fromkeys(old_list))
print(new_list) # [2, 3, 4, 5, 1]

使用sort函数来去重(保留原顺序)

1
2
3
lst1 = [2, 1, 3, 4, 1]
lst2.sort(key=lst1.index)
print(lst2) # [2, 1, 3, 4]

使用内置set方法(不保证原顺序)

1
2
3
4
# 将list转化为set再转化为list
list1 = [1, 3, 4, 3, 2, 1]
list2 = list(set(list1))
print(list2) # {1, 2, 3, 4}

两个列表怎么转成字典

  • 动手写一个函数
1
2
3
4
5
6
list1 = ['name', 'age', 'sex']
list2 = ['张三', 18, '男']
dict = {}
for i in range(len(list1)):
dict[list1[i]] = list2[i]
print(dict, type(dict)) # {'name': '张三', 'age': 18, 'sex': '男'} <class 'dict'>
  • 使用内置函数zip
1
2
3
4
list1 = ['name', 'age', 'sex']
list2 = ['张三', 18, '男']
d = dict(zip(list1, list2))
print(d) # {'name': '张三', 'age': 18, 'sex': '男'}

如何合并两个列表

1
2
3
4
5
6
7
8
9
10
11
a = [1,5,7,9]
b = [2,2,6,8]

# 方法1
a.extend(b)

# 方法2
a[0:0] = b

# 方法3
a += b

举例 sort 和 sorted 的区别

1
sort()与sorted()的不同在于,sort是在原位重新排列列表,而sorted()是产生一个新的列表

如何把 元组 (“a”,”b”) 、(1,2),变为字典{“a”:1,”b”:2}?

1
2
3
4
a = ('a','b')
b = (1,2)
z = zip(a,b)
c = dict(z)

字典操作中 del 和 pop 有什么区别?

1
2
del 操作删除键值对,不返回值;
pop 操作删除键值对的同时,返回键所对应的值。

如何合并两个字典

1
2
3
4
5
6
7
8
9
10
11
12
13
# python3合并字典有三种方式
# 1.
a = {'a':1,'b':2}
b = {'c':3,'d':4}
c = {}
c.update(a)
c.update(b)

# 2.
c = dict(a,**b)

# 3.动态解包
c = {**a,**b} # 官方推荐这种方式

文件处理

在读文件操作的时候会使用 read、readline 或者 readlines,简述它们各自的作用

  • read():一次读取文本全部内容为字符串(可以指定size去读取),占用空间大
  • readline():读取文本为一个生成器,支持遍历和迭代,占用空间小
  • readlines():一次读取文本全部内容为列表,占用空间大

Python 字典和 json 字符串相互转化方法

1
2
json.dumps()   将Python中的对象转换为JSON中的字符串对象
json.loads() 将JSON中的字符串对象转换为Python中的对象

语言特性

Python 中的基本数据结构

基本结构通常指列表、元组、字典、集合,而字符串可能作为序列类型单独列出

数据结构 特点 实现原理 应用场景
列表 有序、可变、允许重复元素 动态数组存储,支持自动扩容 动态数据读取/更新、栈/队列实现
元组 有序、不可变、允许重复元素 紧凑数组存储 存储固定数据(如函数参数传递、配置参数),比列表更节省内存
字典 无序(Python 3.7+ 有序)、可变、键值对 哈希表 快速查询、JSON 数据处理
集合 无序、可变、唯一性(自动去重) 哈希表(仅键) 去重、集合运算(交集、并集)、成员判断
字符串 有序、不可变、字符序列 动态编码机制存储字符,如UTF-8、GBK 文本处理、数据序列化(JSON/XML格式)

Python 函数参数的传递机制

Python的函数参数传递机制是对象引用传递(Pass by Object Reference),其核心是传递对象的引用,而非对象本身或对象的值。这一机制的行为取决于对象是否可变。

1. 如何理解对象引用传递

  • 所有变量都是对象的引用:Python中变量名本质上是对象的引用(指针),即所有数据(如整数、字符串、列表等)都以对象形式存在,变量仅指向这些对象。
  • 传递的是引用的拷贝:函数调用时,实参对象的引用(地址)会被拷贝给形参。因此,形参和实参指向同一个对象,但引用本身是独立的。

2. 不可变对象的传递

不可变对象(如 intstrtuple 等)的值不能被修改,因此:

  • 函数内部无法修改原对象:对形参的“修改”都会创建新对象(如重新赋值),外部变量不变,行为类似“值传递”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def modify_immutable(x):
print(f"函数内原始x的id: {id(x)}")
x = 10 # 创建新对象,原引用被替换
print(f"修改后x的id: {id(x)}")

x = 5
print(f"调用前x的id: {id(x)}")
modify_immutable(x)
print(f"调用后x的值: {x}") # 输出:5(未改变)

# 输出:
# 调用前x的id: 4353896768
# 函数内原始x的id: 4353896768
# 修改后x的id: 4353896928
# 调用后x的值: 5

3.可变对象的传递

可变对象(如 listdictset 等)的值可以被修改,因此:

  • 函数内部可直接修改原对象:通过原地操作(如 appendupdate)会修改原对象,会影响外部变量,行为类似“引用传递”
  • 特殊情况:如果在函数内部重新赋值(如 lst = new_list),形参的引用会指向新对象,外部变量不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def modify_mutable(lst):
print(f"函数内原始lst的id: {id(lst)}")
lst.append(4) # 原地修改,对象内容改变
print(f"原地修改后lst的id: {id(lst)}")
lst = [100, 200] # 此处重新赋值,形参lst指向新对象,但原my_list未受影响
print(f"重新赋值后lst的id: {id(lst)}")

my_list = [1, 2, 3]
print(f"调用前my_list的id: {id(my_list)}")
modify_mutable(my_list)
print(f"调用后my_list的内容: {my_list}") # 输出:[1, 2, 3, 4]

# 输出:
# 调用前my_list的id: 4408370176
# 函数内原始lst的id: 4408370176
# 原地修改后lst的id: 4408370176
# 重新赋值后lst的id: 4404760832
# 调用后my_list的内容: [1, 2, 3, 4]

Python 垃圾回收机制

Python的垃圾回收(Garbage Collection, GC)机制旨在自动管理内存,回收不再被使用的对象,避免内存泄漏。其核心机制结合了引用计数和分代回收(Generational Collection),同时通过gc模块提供手动控制的灵活性。

1. 核心机制:引用计数

基本原理

  • 每个对象都有一个引用计数器:记录当前有多少变量或对象引用它。
  • 引用计数的增减:
    • 当对象被创建时,初始引用计数为1。
    • 每次有新的变量或对象引用该对象时,计数器加1。
    • 当引用失效(如变量被删除、超出作用域、重新赋值)时,计数器减1。
    • 当计数器降为0时,Python立即释放该对象的内存。

示例

1
2
3
4
a = [1, 2, 3]  # 列表对象的引用计数为1(被a引用)
b = a # 引用计数变为2(被a和b引用)
del a # 引用计数减为1(仅b引用)
b = None # 引用计数降为0,列表对象被回收

引用计数的局限性

  • 无法处理循环引用:当两个或多个对象互相引用时,引用计数永远不会为0,导致内存泄漏。
1
2
3
4
5
a = []
b = []
a.append(b) # a引用b
b.append(a) # b引用a
del a, b # 循环引用的计数仍为1,无法回收
  1. 分代回收(Generational Collection)

作用:解决循环引用问题,通过周期性扫描检测并回收被循环引用的对象。

分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。

函数装饰器有什么作用?请列举说明

函数装饰器可以在不修改原函数的条件下,为原函数添加额外的功能,例如记录日志,运行性能,缓存等

简述 @classmethod 和 @staticmethod 用法和区别

对比项 @classmethod @staticmethod
参数 必须有 cls(类本身) 无特殊参数,类似普通函数
访问权限 可访问/修改类属性 无法访问类或实例属性
依赖性 依赖类的上下文 与类或实例无关
典型用途 工厂方法(替代构造函数来创建对象)、操作类属性、多态场景 工具函数、逻辑相关但独立的功能
调用灵活性 推荐通过类名调用 推荐通过类名调用

你了解 Python 中的反射吗?

Python 的反射能力由一系列内置函数和模块支持,允许代码在运行时根据字符串名称操作对象,实现高度动态的编程。比如动态操作对象属性、调用方法、模块导入等

内置函数

  1. hasattr(obj, name): 检查对象是否包含指定属性/方法
  2. getattr(obj, name, default): 获取对象的属性/方法,若不存在则返回 default 或抛出错误
  3. setattr(obj, name, value): 动态设置对象的属性值
  4. delattr(obj, name): 删除对象中的属性/方法

应用场景

1、动态调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests

class Http(object):

def get(self,url):
res = requests.get(url)
response = res.text
return response

def post(self,url):
res = requests.post(url)
response = res.text
return response

# 使用反射后
url = "https://www.jianshu.com/u/14140bf8f6c7"
method = input("请求方法>>>:")
h = Http()

if hasattr(h,method):
func = getattr(h,method)
res = func(url)
print(res)
else:
print("你的请求方式有误...")

2、动态属性操作

1
2
3
4
5
6
class DynamicObject:
pass

obj = DynamicObject()
setattr(obj, "dynamic_attr", 42)
print(getattr(obj, "dynamic_attr")) # 输出 42

3、动态加载模块或类

1
2
3
module_name = "math"
module = __import__(module_name)
print(getattr(module, "sqrt")(16)) # 输出 4.0

inspect 模块

inspect 模块提供更强大的内省功能。

严格来说,内省是反射的一部分,主要用于查看分析函数/类的元数据。简单一句就是运行时能够获得对象的类型.比如type(),dir(),getattr(),hasattr(),isinstance()。

1
2
3
4
5
6
7
8
9
10
11
12
import inspect

def my_function(a, b):
"""示例函数"""
return a + b

# 获取函数参数信息
print(inspect.signature(my_function)) # 输出 (a, b)
# 获取文档字符串
print(inspect.getdoc(my_function)) # 输出 "示例函数"
# 获取源代码
print(inspect.getsource(my_function)) # 输出函数定义的源代码

copy 和 deepcopy 的区别是什么?

浅拷贝: 创建新对象,赋值的时候非递归地复制子对象的引用。

深拷贝:创建新对象,赋值的时候递归地复制子对象的引用。即拷贝了对象的所有元素,包括多层嵌套的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import copy
a = [1, 2, 3, 4, ['a', 'b']] #原始对象

b = a #赋值,传对象的引用
c = copy.copy(a) #对象拷贝,浅拷贝
d = copy.deepcopy(a) #对象拷贝,深拷贝

a.append(5) #修改对象a
a[4].append('c') #修改对象a中的['a', 'b']数组对象

print('a = ', a)
print('b = ', b)
print('c = ', c)
print('d = ', d)

输出结果:
a = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
b = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
c = [1, 2, 3, 4, ['a', 'b', 'c']]
d = [1, 2, 3, 4, ['a', 'b']]

进程与线程的区别

  • 进程是CPU资源分配的基本单位,线程是独立运行与调度的基本单位(CPU上真正运行的是线程)
  • 进程拥有自己的资源空间,同一个进程中的多个线程并发执行,这些线程共享进程所拥有的资源,但需处理线程安全问题(如锁机制)
  • 线程的调度与切换比进程快很多

python中的GIL

CPython环境下,每个进程都拥有一个GIL(Global Interpreter Lock),单进程下的多线程,某个线程想要执行,必须先获取GIL,才可以进入CPU执行。因此,一个Python进程中的多个线程不能同时使用多个CPU核心(java中一个进程中的多线程可以利用多核cpu)。

GIL释放的情况:

  1. 执行的字节码行数到达一定阈值
  2. 通过时间片划分,到达一定时间阈值
  3. 在遇到IO操作时,主动释放

GIL(全局解释器锁)带来的影响

多线程

  • Python 的 GIL 会强制解释器在同一时刻只能执行一个线程的字节码。因此,多线程在 CPU 密集型任务中无法真正并行,反而可能因线程切换导致效率下降。
  • 不过Python标准库中的所有阻塞型I/O函数都会释放GIL,等待 IO 时可以切换线程执行其他任务,因此多线程适合 IO 密集型任务(如网络请求、爬虫、文件读写)

多进程

  • 每个进程拥有独立的 Python 解释器和 GIL,因此多进程可以并行利用多核 CPU,适合 CPU 密集型任务(如科学计算、数据处理)

python 协程是什么

协程是一种比线程更加轻量级的存在,最重要的是,协程不被操作系统内核管理,协程是完全由程序控制的。

  • 运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销,线程数量越多,协程的优势就越明显。
  • 协程不需要多线程的锁机制,因为只有一个线程,不存在变量冲突。
  • 对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。

Python里最常见的yield就是协程的思想!

什么是单例模式

单例模式的主要目的是保证在系统中,某个类只能有一个实例存在。比如保存系统基本配置信息的类,在很多地方都要用到,没有必要频繁创建实例与销毁实例,只需要保存一个全局的实例对象即可,这样可以减少对内存资源的占用。

1、可以在模块中定义单例类并实例化对象,在用的时候直接导入这个模块的实例对象即可

1
2
3
4
5
6
7
class GlobalConfig:
host = 'xxx.xxx'
port = 3306
username = 'username'
password = '123123'

g = GlobalConfig()

2、__new__()是类的构造方法,在实例创建前被调用,它的作用就是创建实例对象。利用这个方法和类的属性的特点可以实现设计模式的单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 多线程下创建单例对象需要加锁

import threading

class Singleton:
_instance_lock = threading.Lock()

def __new__(cls, *args, **kwargs):
# 加锁
with cls._instance_lock:
if not hasattr(cls, 'instance'):
cls.instance = super().__new__(cls)
return cls.instance

def func():
s = Singleton()
print('s:{}'.format(id(s)))

if __name__ == '__main__':
for _ in range(5):
td = threading.Thread(target=func)
td.start()

环境准备工作

环境角色

IP 角色 软件安装
192.168.0.16 K8S Master kube-apiserver kube-schduler kube-controller-manager kube-proxy docker flannel kubelet etcd
192.168.0.17 K8s Node kubelet kube-proxy docker flannel
192.168.0.18 K8s Node kubelet kube-proxy docker flannel

以下所有1.x操作,在三台节点都执行

统一设置主机名

1
2
3
$ hostnamectl set-hostname test01
$ hostnamectl set-hostname test02
$ hostnamectl set-hostname test03

部署机免密登录到test01、test02、test03

前提是目标主机需要生成公钥以及密钥 ssh-keygen -t rsa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@harbor ~]# ssh-copy-id -i ~/.ssh/id_rsa.pub root@test02
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"
The authenticity of host 'test02 (192.168.0.17)' can't be established.
ECDSA key fingerprint is SHA256:Omg7RAlyXPvDcwNyJdufEmAMHnwcS3eh/gsaHPZVP6I.
ECDSA key fingerprint is MD5:5d:55:0f:b2:75:4d:39:ee:47:c1:a8:3f:0f:12:96:30.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
root@test02's password:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'root@test02'"
and check to make sure that only the key(s) you wanted were added.

关闭防火墙及selinux

docker selinux-enabled作用和原理看介绍

https://www.cnblogs.com/elnino/p/10845449.html

防火墙的文档介绍

https://wangchujiang.com/linux-command/c/firewall-cmd.html

1
2
3
4
5
6
7
8
9
# 关闭selinux
$ systemctl stop firewalld && systemctl disable firewalld
$ sed -i --follow-symlinks 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/sysconfig/selinux && setenforce 0
# 关闭防火墙
[root@test01 ~]# systemctl start firewalld
[root@test01 ~]# firewall-cmd --set-default-zone=trusted
success
[root@test01 ~]# firewall-cmd --complete-reload
success

关闭 swap 分区

对于禁用swap内存,具体原因可以查看Github上的Issue:https://github.com/kubernetes/kubernetes/issues/53533

1
2
$ swapoff -a # 临时
$ sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab #永久

内核调整,将桥接的IPv4流量传递到iptables的链

https://kubernetes.io/zh/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/

1
2
3
4
5
6
开启Linux的路由转发功能:
sysctl -w net.ipv4.ip_forward=1

cni插件确保简单的配置(如带网桥的 Docker )与 iptables 代理正常工作:
sysctl -w net.bridge.bridge-nf-call-iptables=1
sysctl -w net.bridge.bridge-nf-call-ip6tables=1
1
2
3
4
5
6
[root@test01 ~]# cat /proc/sys/net/ipv4/ip_forward
1
[root@test01 ~]# cat /proc/sys/net/bridge/bridge-nf-call-iptables
1
[root@test01 ~]# cat /proc/sys/net/bridge/bridge-nf-call-ip6tables
1

设置系统时区并同步时间服务器

1
2
$ yum install -y ntpdate
$ ntpdate time.windows.com

安装dokcer

1
2
3
4
5
6
[root@test01 ~]# yum install docker -y
[root@test01 ~]# systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
[root@test01 ~]# systemctl start docker
[root@test01 ~]# docker --version
Docker version 1.13.1, build 4ef4b30/1.13.1

安装K8s

添加kubernetes YUM软件源

1
2
3
4
5
6
7
8
9
$ cat > /etc/yum.repos.d/kubernetes.repo << EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

安装kubeadm,kubelet和kubectl

1
2
$ yum install -y kubelet-1.16.4 kubeadm-1.16.4 kubectl-1.16.4
$ systemctl enable kubelet

部署Kubernetes Master

只需要在Master 节点执行,这里的apiserver需要修改成自己的master地址。
由于默认拉取镜像地址k8s.gcr.io国内无法访问,这里指定阿里云镜像仓库地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
[root@test01 ~]# kubeadm init --apiserver-advertise-address=192.168.0.16 --image-repository registry.aliyuncs.com/google_containers --kubernetes-version v1.16.4 --service-cidr=10.1.0.0/16 --pod-network-cidr=10.244.0.0/16
[init] Using Kubernetes version: v1.16.4
[preflight] Running pre-flight checks
[WARNING Firewalld]: firewalld is active, please ensure ports [6443 10250] are open or your cluster may not function correctly
[WARNING Hostname]: hostname "test01" could not be reached
[WARNING Hostname]: hostname "test01": lookup test01 on 10.16.140.4:53: no such host
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Activating the kubelet service
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [test01 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.1.0.1 192.168.0.16]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [test01 localhost] and IPs [192.168.0.16 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [test01 localhost] and IPs [192.168.0.16 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
[apiclient] All control plane components are healthy after 33.002262 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config-1.16" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs1
[mark-control-plane] Marking the node test01 as control-plane by adding the label "node-role.kubernetes.io/master=''"
[mark-control-plane] Marking the node test01 as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
[bootstrap-token] Using token: 7e169l.60ykxxr8md7sb8ak
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.0.16:6443 --token 7e169l.60ykxxr8md7sb8ak \
--discovery-token-ca-cert-hash sha256:c0b04a34ac7bb55fdf5b04a70233c14c15a11341a2bde0ea33b30579d84c0ce4

根据kubeadm日志输出提示操作:

1
2
3
4
5
6
7
8
9
10
11
12
[root@test01 ~]# mkdir -p $HOME/.kube
[root@test01 ~]# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
[root@test01 ~]# sudo chown $(id -u):$(id -g) $HOME/.kube/config
[root@test01 ~]# kubectl get po --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-58cc8c89f4-jrfqj 0/1 Pending 0 12m
kube-system coredns-58cc8c89f4-qh5bg 0/1 Pending 0 12m
kube-system etcd-test01 1/1 Running 0 11m
kube-system kube-apiserver-test01 1/1 Running 0 11m
kube-system kube-controller-manager-test01 1/1 Running 0 11m
kube-system kube-proxy-m6hp2 1/1 Running 0 12m
kube-system kube-scheduler-test01 1/1 Running 0 11m

继续安装网络插件
kube-flannel.yml下载地址如下
https://raw.githubusercontent.com/coreos/flannel/a70459be0084506e4ec919aa1c114638878db11b/Documentation/kube-flannel.yml

1
2
3
4
5
6
7
8
9
10
11
[root@test01 ~]# kubectl apply -f kube-flannel.yml
podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds-amd64 created
daemonset.apps/kube-flannel-ds-arm64 created
daemonset.apps/kube-flannel-ds-arm created
daemonset.apps/kube-flannel-ds-ppc64le created
daemonset.apps/kube-flannel-ds-s390x created

添加worknode节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[root@test02 yum.repos.d]# kubeadm join 192.168.0.16:6443 --token 7e169l.60ykxxr8md7sb8ak \
> --discovery-token-ca-cert-hash sha256:c0b04a34ac7bb55fdf5b04a70233c14c15a11341a2bde0ea33b30579d84c0ce4
[preflight] Running pre-flight checks
[WARNING Hostname]: hostname "test02" could not be reached
[WARNING Hostname]: hostname "test02": lookup test02 on 10.16.140.4:53: no such host
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.16" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Activating the kubelet service
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

[root@test01 ~]# kubectl get po --all-namespaces -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system coredns-58cc8c89f4-jrfqj 1/1 Running 0 46m 10.244.0.3 test01 <none> <none>
kube-system coredns-58cc8c89f4-qh5bg 1/1 Running 0 46m 10.244.0.2 test01 <none> <none>
kube-system etcd-test01 1/1 Running 0 45m 192.168.0.16 test01 <none> <none>
kube-system kube-apiserver-test01 1/1 Running 0 45m 192.168.0.16 test01 <none> <none>
kube-system kube-controller-manager-test01 1/1 Running 0 45m 192.168.0.16 test01 <none> <none>
kube-system kube-flannel-ds-amd64-7cwtb 1/1 Running 0 5m40s 192.168.0.16 test01 <none> <none>
kube-system kube-flannel-ds-amd64-gcw85 1/1 Running 1 3m47s 192.168.0.18 test03 <none> <none>
kube-system kube-flannel-ds-amd64-sqj75 1/1 Running 0 3m50s 192.168.0.17 test02 <none> <none>
kube-system kube-proxy-fr7wk 1/1 Running 0 3m47s 192.168.0.18 test03 <none> <none>
kube-system kube-proxy-m6hp2 1/1 Running 0 46m 192.168.0.16 test01 <none> <none>
kube-system kube-proxy-qpv2g 1/1 Running 0 3m50s 192.168.0.17 test02 <none> <none>
kube-system kube-scheduler-test01 1/1 Running 0 45m 192.168.0.16 test01 <none> <none>
[root@test01 ~]# kubectl get no
NAME STATUS ROLES AGE VERSION
test01 Ready master 46m v1.16.4
test02 Ready <none> 4m1s v1.16.4
test03 Ready <none> 3m58s v1.16.4

测试K8s集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[root@test01 ~]# kubectl create deployment nginx --image=nginx
deployment.apps/nginx created
[root@test01 ~]# kubectl expose deployment nginx --port=80 --type=NodePort
service/nginx exposed
[root@test01 ~]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/nginx-86c57db685-8mmgz 1/1 Running 0 21s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 55m
service/nginx NodePort 10.1.210.90 <none> 80:30580/TCP 13s

[root@test01 ~]# curl -I 10.1.210.90
HTTP/1.1 200 OK
Server: nginx/1.17.8
Date: Thu, 13 Feb 2020 15:04:16 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 21 Jan 2020 13:36:08 GMT
Connection: keep-alive
ETag: "5e26fe48-264"
Accept-Ranges: bytes

[root@test01 ~]# curl -I 192.168.0.18:30580
HTTP/1.1 200 OK
Server: nginx/1.17.8
Date: Thu, 13 Feb 2020 15:04:25 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 21 Jan 2020 13:36:08 GMT
Connection: keep-alive
ETag: "5e26fe48-264"
Accept-Ranges: bytes

部署Dashboard

安装参考官方https://github.com/kubernetes/dashboard

1、预先在$HOME/certs目录下放置apiserver证书

1
2
3
4
5
6
7
[root@test01 ~]# ll
-rw-------. 1 root root 1348 8月 13 2018 anaconda-ks.cfg
drwxr-xr-x 2 root root 4096 2月 23 04:36 certs
-rw-r--r-- 1 root root 7682 2月 23 05:01 dashboard-recommended.yaml
-rw-r--r-- 1 root root 14428 2月 13 22:46 kube-flannel.yml
[root@test01 ~]# kubectl create secret generic kubernetes-dashboard-certs --from-file=$HOME/certs -n kubernetes-dashboard
secret/kubernetes-dashboard-certs created

2、部署dashboard-recommended.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@test01 ~]# kubectl apply -f dashboard-recommended.yaml
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created

[root@test01 ~]# kubectl get po -n kubernetes-dashboard
NAME READY STATUS RESTARTS AGE
dashboard-metrics-scraper-7b8b58dc8b-x5p9p 1/1 Running 0 18m
kubernetes-dashboard-557f4b4587-zjglq 1/1 Running 0 18m
[root@test01 ~]#
[root@test01 ~]# kubectl get svc -n kubernetes-dashboard
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
dashboard-metrics-scraper ClusterIP 10.1.80.221 <none> 8000/TCP 21m
kubernetes-dashboard NodePort 10.1.241.233 <none> 443:30001/TCP 21m

3、重新创建serviceaccount并绑定默认cluster-admin管理员集群角色

1
2
3
4
[root@test01 ~]# kubectl delete clusterrolebinding kubernetes-dashboard
clusterrolebinding.rbac.authorization.k8s.io "kubernetes-dashboard" deleted
[root@test01 ~]# kubectl create clusterrolebinding kubernetes-dashboard --clusterrole=cluster-admin --serviceaccount=kubernetes-dashboard:kubernetes-dashboard
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created

4、获取token,访问30001端口输入token即可成功登录

1
2
3
4
5
6
7
8
9
10
[root@test01 ~]# kubectl get secret -n kubernetes-dashboard
NAME TYPE DATA AGE
default-token-vzv7l kubernetes.io/service-account-token 3 4m26s
kubernetes-dashboard-certs Opaque 2 3m24s
kubernetes-dashboard-csrf Opaque 1 4m26s
kubernetes-dashboard-key-holder Opaque 2 4m26s
kubernetes-dashboard-token-hpff2 kubernetes.io/service-account-token 3 4m26s

[root@test01 ~]# kubectl describe secret kubernetes-dashboard-token-hpff2 -n kubernetes-dashboard |grep token:
token: eyJhbGciOiJSUzI1NiIsImtpZCI6InBYV1Utc2Yxc0JvWE5zQkFpTW1lX2hpRmJKY3ZWOWhkSDhWWHFscHBSMTAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC10b2tlbi1ocGZmMiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjkyOWI3NTczLWU5OGQtNGU1NC04NTBiLWQyZmYyODIxYWM3ZSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlcm5ldGVzLWRhc2hib2FyZDprdWJlcm5ldGVzLWRhc2hib2FyZCJ9.gK7EsvOnaeCUBcIoDTwsH323ZhQ_tmtYFuC9v8DRlKiX_nMbzBiMzmrvCxGRvXeD-ntlmLBE56R9s2yrUEt-3cx0_QFWYBZGmayVMqY7I-sj8fwSwJOYizq6SE4KZO8Ek3bc7qpV2BxKktWVZl77v7jU7upbtMtNRogFCKEAJWdGztpYoSD9F8s-osvcPJMYhHAYhLs002d84ux-CZUTadTMu9Y-mJ-0M5oulNLXB8m3TueAva-Tav6Hgw-1_ABPtpnOLbPJoaHqIzBXhMAgiKKpF6gs0GCui0-8sKGTFlY9jBNuUZSzIF4K9yKEHeF82ZNj9PxlBVajeej7pbCmOg

部署遇到的问题记录(待确认)

问题1、kubeadm join时出现warn信息

1
2
[WARNING Hostname]: hostname "test02" could not be reached
[WARNING Hostname]: hostname "test02": lookup test02 on 10.16.140.4:53: no such host

问题2、kube-proxy没有开启ipvs,默认用了iptables Proxier

1
2
3
4
5
6
7
8
9
[root@test01 ~]# kubectl logs kube-proxy-fr7wk  -n kube-system
W0213 14:48:50.804540 1 proxier.go:592] Failed to load kernel module ip_vs with modprobe. You can ignore this message when kube-proxy is running inside container without mounting /lib/modules
W0213 14:48:50.805641 1 proxier.go:592] Failed to load kernel module ip_vs_rr with modprobe. You can ignore this message when kube-proxy is running inside container without mounting /lib/modules
W0213 14:48:50.806607 1 proxier.go:592] Failed to load kernel module ip_vs_wrr with modprobe. You can ignore this message when kube-proxy is running inside container without mounting /lib/modules
W0213 14:48:50.807514 1 proxier.go:592] Failed to load kernel module ip_vs_sh with modprobe. You can ignore this message when kube-proxy is running inside container without mounting /lib/modules
W0213 14:48:50.811786 1 server_others.go:330] Flag proxy-mode="" unknown, assuming iptables proxy
I0213 14:48:50.819927 1 node.go:135] Successfully retrieved node IP: 192.168.0.18
I0213 14:48:50.819966 1 server_others.go:150] Using iptables Proxier.
I0213 14:48:50.820484 1 server.go:529] Version: v1.16.4

问题3、etcd有异常日志出现

1
2
3
4
5
6
7
8
2020-02-13 14:48:04.732974 W | wal: sync duration of 1.126809415s, expected less than 1s
2020-02-13 14:48:04.733537 W | etcdserver: read-only range request "key:\"/registry/services/endpoints/kube-system/kube-scheduler\" " with result "range_response_count:1 size:429" took too long (760.032104ms) to execute
2020-02-13 14:48:04.733800 W | etcdserver: read-only range request "key:\"/registry/minions/test02\" " with result "range_response_count:0 size:5" took too long (975.286916ms) to execute
2020-02-13 14:48:04.733918 W | etcdserver: read-only range request "key:\"/registry/jobs/\" range_end:\"/registry/jobs0\" limit:500 " with result "range_response_count:0 size:5" took too long (332.81178ms) to execute
2020-02-13 14:48:04.734030 W | etcdserver: read-only range request "key:\"/registry/pods\" range_end:\"/registry/podt\" count_only:true " with result "range_response_count:0 size:7" took too long (377.827782ms) to execute
2020-02-13 14:48:04.734145 W | etcdserver: read-only range request "key:\"/registry/services/endpoints/kube-system/kube-controller-manager\" " with result "range_response_count:1 size:447" took too long (431.157766ms) to execute
2020-02-13 14:48:04.734246 W | etcdserver: read-only range request "key:\"/registry/minions/test03\" " with result "range_response_count:0 size:5" took too long (1.05179271s) to execute
2020-02-13 14:48:04.734349 W | etcdserver: read-only range request "key:\"/registry/pods/\" range_end:\"/registry/pods0\" limit:500 " with result "range_response_count:8 size:18170" took too long (292.020624ms) to execute

问题4、api-server报错

1
I0213 16:31:39.167022       1 log.go:172] http: TLS handshake error from 192.168.0.10:59610: remote error: tls: bad certificate

问题5、kube-scheduler报错

1
User "system:kube-scheduler" cannot list resource "replicationcontrollers" in API group "" at the cluster scope

问题6、kubectl get cs问题(上游问题,影响版本从1.16之后。)

1
2
3
4
5
6
7
8
9
10
11
[root@test01 ~]# kubectl get cs
NAME AGE
scheduler <unknown>
controller-manager <unknown>
etcd-0 <unknown>
#workround方案:
[root@test01 ~]# kubectl get cs -o=go-template='{{printf "|NAME|STATUS|MESSAGE|\n"}}{{range .items}}{{$name := .metadata.name}}{{range .conditions}}{{printf "|%s|%s|%s|\n" $name .status .message}}{{end}}{{end}}'
|NAME|STATUS|MESSAGE|
|controller-manager|True|ok|
|scheduler|True|ok|
|etcd-0|True|{"health":"true"}|

参考链接:
https://cloud.tencent.com/developer/article/1509412
https://www.kubernetes.org.cn/6632.html

代码拉取到本地

与主分支保持同步

1
2
3
4
git checkout main
git pull origin main
git checkout feature/my-new-feature
git merge main

代码拉取冲突处理

1、保留并继续合并代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
localhost:bookstore liaoxb$ git pull
Updating d369e85..67e8e8a
error: Your local changes to the following files would be overwritten by merge:
test.txt
Please commit your changes or stash them before you merge.
Aborting
localhost:bookstore liaoxb$
localhost:bookstore liaoxb$ echo 'local change test' > test.txt
localhost:bookstore liaoxb$ git pull
Updating d369e85..67e8e8a
error: Your local changes to the following files would be overwritten by merge:
test.txt
Please commit your changes or stash them before you merge.
Aborting

git stash 暂存本地的代码修改
git pull 拉取远端最新代码
git stash pop 合并暂存的本地代码,解决已冲突的文件

1
2
3
4
5
6
7
8
9
10
localhost:bookstore liaoxb$ git stash
Saved working directory and index state WIP on master: d369e85 update
localhost:bookstore liaoxb$ git pull
Updating d369e85..67e8e8a
Fast-forward
test.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
localhost:bookstore liaoxb$ git stash pop
Auto-merging test.txt
CONFLICT (content): Merge conflict in test.txt

2、远端覆盖本地代码

git reset –hard 回滚到上个版本
git pull

1
2
3
4
5
6
7
localhost:bookstore liaoxb$ git reset --hard
HEAD is now at d369e85 update
localhost:bookstore liaoxb$ git pull
Updating d369e85..67e8e8a
Fast-forward
test.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

创建分支并推送到远端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 创建新分支,同时切换到bugfix1
localhost:bookstore liaoxb$ git pull
Already up to date.
localhost:bookstore liaoxb$ git checkout -b bugfix1
Switched to a new branch 'bugfix1'

# 修改并提交代码
localhost:bookstore liaoxb$ echo 'bugfix test' > test.txt
localhost:bookstore liaoxb$ git add .
localhost:bookstore liaoxb$ git commit -m "bugfix test"
[bugfix1 2eafe98] bugfix test
1 file changed, 1 insertion(+), 3 deletions(-)

# 新分支push到远端
localhost:bookstore liaoxb$ git push --set-upstream origin bugfix1
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 259 bytes | 259.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Powered by GITEE.COM [GNK-3.0]
remote: Create a pull request for 'bugfix1' on Gitee by visiting:
remote: https://gitee.com/wisecloud/bookstore/pull/new/wisecloud:bugfix1...wisecloud:master
To https://gitee.com/wisecloud/bookstore.git
* [new branch] bugfix1 -> bugfix1
Branch 'bugfix1' set up to track remote branch 'bugfix1' from 'origin'.

代码合并到主分支

方法一:git merge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 切至合并分支
localhost:bookstore liaoxb$ git branch
* bugfix1
master
localhost:bookstore liaoxb$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

# 执行合并操作
localhost:bookstore liaoxb$ git merge bugfix1
Updating d369e85..2eafe98
Fast-forward
test.txt | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)

# git push到远端
localhost:bookstore liaoxb$ git push
Total 0 (delta 0), reused 0 (delta 0)
remote: Powered by GITEE.COM [GNK-3.0]
To https://gitee.com/wisecloud/bookstore.git
d369e85..2eafe98 master -> master

方法二:git cherry-pick

1
2
3
4
5
6
7
8
# 切换到master分支
git checkout master

# Cherry pick 操作
git cherry-pick <commit_1> <commit_2>

# 推送
git push

执行合并操作期间,若遇到代码冲突,需要先解决后,才能继续合并提交

代码合并冲突处理

1
2
3
4
5
6
7
8
# 解决完冲突后,将修改的文件重新加入暂存区
git add .

# 继续剩余的提交
git cherry-pick --continue

# 推送
git push
1
2
3
4
5
# 遇到冲突,放弃合并,回到操作前的样子
git cherry-pick --abort

# 遇到冲突,放弃合并,不回到操作前的样子
git cherry-pick --quit

本地和远端删除已废弃分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 删除远端分支
localhost:bookstore liaoxb$ git branch -r
origin/HEAD -> origin/master
origin/bugfix1
origin/master
localhost:bookstore liaoxb$ git branch -d origin/bugfix1
Deleted remote-tracking branch origin/bugfix1 (was 2eafe98).

# 删除本地分支
localhost:bookstore liaoxb$ git push origin --delete origin/bugfix1
remote: Powered by GITEE.COM [GNK-3.0]
To https://gitee.com/wisecloud/bookstore.git
- [deleted] bugfix1
localhost:bookstore liaoxb$ git branch -r
origin/HEAD -> origin/master
origin/master

分支引入错误代码,需要回滚

回滚至某次commit,之后的所有commit全部会丢失

1
2
git reset --hard <commit_id>
git push --force

需求背景

公司项目搭建了一套CICD,每天可以自动地构建镜像并部署至测试环境,此时需要对接自动化测试,实现持续集成测试。

安装Jenkins

1
docker run -u root -d  --name jenkins-ci -p 8081:8080 -p 50000:50000 -v /etc/localtime:/etc/localtime -v /etc/timezone:/etc/timezone -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):/usr/bin/docker jenkinsci/blueocean

命令说明:

1、通过 -v $(which docker):/usr/bin/docker-v /var/run/docker.sock:/var/run/docker.sock,容器内可以直接调用宿主机的 Docker命令,从而实现构建、测试等任务。即DooD(Docker-outside-of-Docker)方案

2、如果宿主机不存在timezone文件,自己手动创建并写入Asia/Shanghai即可

执行命令截图:

为了省心方便,在此默认选择了jenkins推荐的插件安装,这需要耐心等待一段时间:

配置Jenkins

配置gitlab仓库拉取权限

1、jenkins容器内生成SSH密钥对

2、拷贝SSH公钥至gitlab账号

3、Jenkins容器内验证SSH密钥是否已正确添加,参考以下文档

配置邮件服务器

1、配置发件人邮箱

2、jenkins配置smtp服务器

以配置qq邮箱服务器为例(密码填qq邮箱的授权码),测试配置如果发送成功,说明邮件配置成功

安装报告插件

由于项目里的自动化脚本基于RF编写开发,所以Jenkins需要安装robot-framework插件。该插件在Jenkins中收集并发布Robot Framework测试结果。

集成自动化测试项目

项目介绍

框架搭建时,主要用了以下工具:

  • gitee:管理多分支代码
  • Robot Framework:作为测试框架
  • docker:打包测试脚本和python包依赖,构建测试镜像

配置数据管理

  • 目前是把不同的测试集群信息放在wisecloud.yaml文件里管理,另外basedata.py文件管理其它配置数据。比如公共登录账号、harbor地址账号等。
  • 配置读取:单独创建一个Rest类,并继承REST父类。get_baseurl实例方法会根据env_type变量值,去动态获取目标集群的访问地址、集群ip地址,并调用BuiltIn().set_global_variable方法去设为全局变量。供用例执行过程中使用

分层设计与解耦

  • 用例、方法(关键字)、测试库分层

  • 用例按模块划分成多个测试用例文件,集中放进测试套件Suites

  • 方法按模块划分成多个资源文件,集中放进资源文件夹Resource

  • 自定义创建测试库和方法,集中放进Library。以关键字的形式以供调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
├── Library
│ ├── Basedata.py
│ ├── Rest.py
│ ├── ServiceAdd.py
│ └── wisecloud.yaml
├── Resource
│ ├── BackupRestore.robot
│ ├── Common.robot
│ ├── Configcenter.robot
│ ├── Dashboard.robot
│ ├── Monitor.robot
│ ├── Orche_App.robot
│ ├── Orche_Stack.robot
│ ├── Resource_Manager.robot
│ ├── Workflow.robot
│ └── pipeline_workflow.robot
├── Suites
│ ├── backuprestore.robot
│ ├── configcenter.robot
│ ├── dashboard.robot
│ ├── ingress
│ │ └── ingress.robot

编写Pipeline脚本

直接贴脚本,pipeline支持的语法可查看官方手册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
pipeline {
agent any
options {
// 添加日志打印时间
timestamps()
// 设置全局超时间
// timeout(time:10,unit:'MINUTES')
}
environment {
// GIT_BRANCH = 'master' # 已设置全局变量
GIT_USER_ID = '76f84542-2dba-4128-8651-bed7a849eddd'
}
stages {
stage('Git Clone') {
steps {
// sh 'printenv |sort'
git branch: "${GIT_BRANCH}", credentialsId: "${GIT_USER_ID}", url: 'https://gitee.com/wisecloud/wise2c-robot.git'
}
}
stage('Build Image') {
steps {
sh 'docker build -t ${JOB_NAME}:${GIT_BRANCH} .'
}
}
stage('API Test') {
steps {
script{
sh 'ls -a'
sh 'docker run --rm -v jenkins-data:/robot-results --name ${JOB_NAME} ${JOB_NAME}:${GIT_BRANCH} robot --outputdir /robot-results /wise2c-robot/Project/Suites/resource_manager.robot'
}
}
}
}
post {
always {
echo 'Publish Test Report'
robot logFileName: 'log.html', outputFileName: 'output.xml', outputPath: '/var/jenkins_home/', passThreshold: 100.0, reportFileName: 'report.html', unstableThreshold: 90.0
// deleteDir() /* clean up our workspace */
}
success {
mail bcc: '', body: "API自动化测试通过\n测试版本分支:${GIT_BRANCH}\n测试报告地址:${BUILD_URL}", cc: '', from: '1219199895@qq.com', replyTo: '', subject: "${JOB_NAME}测试报告", to: 'liaoxb@wise2c.com'
}
failure {
mail bcc: '', body: "API自动化测试未通过,请相关模块的同事分析定位问题,谢谢大家。\n测试版本分支:${GIT_BRANCH}\n测试报告地址:${BUILD_URL}", cc: '', from: '1219199895@qq.com', replyTo: '', subject: "${JOB_NAME}测试报告", to: 'liaoxb@wise2c.com'
}
}
}

webhook触发流水线

1、新建pipeline时,构建触发器选择‘触发远程构建’这项,输入token name。比如apitest
这时就可以获得一个webhhok地址,提供给维护CICD平台的同事即可。
2、把pipeline脚本内容粘贴到流水线里,检查下有没有语法错误,最后点击保存
3、现在可以用curl模拟发送一次webhhok请求,不过记得临时把全局安全配置里的授权策略给放开,改成没有任何限制。

1
curl JENKINS_URL/job/wise2c-robot/build?token=TOKEN_NAME

测试结果报告展示

job首页测试结果展示,失败了1条case,同时收到了一封关于api测试失败的邮件通知:

测试结果详情页面展示,点击log.html链接可以直接查看日志;

如果到打开失败,可以参考这个解决办法。https://stackoverflow.com/questions/36607394/error-opening-robot-framework-log-failed
管理jenkins–>脚本命令行输入如下脚本:

1
System.setProperty("hudson.model.DirectoryBrowserSupport.CSP","sandbox allow-scripts; default-src 'none'; img-src 'self' data: ; style-src 'self' 'unsafe-inline' data: ; script-src 'self' 'unsafe-inline' 'unsafe-eval' ;")
0%