Software Architecture with C++学习:第十四章:分布式系统架构 — 从零开始理解
SOA 是什么ESB 企业服务总线Web 服务消息队列 / 消息代理云计算微服务微服务优点微服务缺点设计模式扩展策略内存外包 Redis/Memcached存储外包 S3计算外包 任务队列SOA(Service-Oriented Architecture,服务导向架构) 是一种软件设计思想:把一个大系统拆分成许多小的、独立的"服务",这些服务通过网络互相通信。ESB(Enterprise Serv
本章学习路线图
一、什么是 SOA(服务导向架构)?
SOA(Service-Oriented Architecture,服务导向架构) 是一种软件设计思想:把一个大系统拆分成许多小的、独立的"服务",这些服务通过网络互相通信。
生活类比
想象一家餐厅:收银台、厨房、送餐部门各自独立工作。收银台不需要知道厨房怎么做菜,只需要把订单传过去。这就是 SOA 的精髓——每个部分专注自己的职责,通过标准接口合作。
一个服务必须具备的四个特性
| 特性 | 说明 | 类比 |
|---|---|---|
| 代表业务活动,有明确结果 | 比如"查询天气"服务,输入城市,输出温度 | 餐厅点菜,给你端上菜 |
| 自包含(Self-contained) | 服务内部的逻辑完全自己管理 | 厨房自己决定怎么做菜 |
| 对用户透明(内部实现隐藏) | 用户只管调用,不关心内部怎么实现 | 你不需要知道厨师的食谱 |
| 可由其他服务组合而成 | 多个小服务组成更大的服务 | 多道菜组成一桌满汉全席 |
二、SOA 的几种实现方式
2.1 ESB(企业服务总线)
ESB(Enterprise Service Bus) 是最经典的 SOA 实现之一。
硬件类比:就像电脑主板上的 PCI 总线——各种显卡、声卡都插在同一条总线上,只要遵守总线标准,彼此不需要直接相连。
ASCII 图:ESB 架构示意
┌─────────────────────────────┐
│ ESB 总线 │
│ (路由 / 转换 / 监控 / 加密) │
└──┬────┬────┬────┬────┬───────┘
│ │ │ │ │
┌──┘ ┌─┘ ┌─┘ ┌─┘ └──┐
▼ ▼ ▼ ▼ ▼
服务A 服务B 服务C 服务D 服务E
(地图) (天气)(支付)(用户) (日志)
ESB 负责的事情:
- 控制服务的部署和版本管理
- 保持服务冗余(备份)
- 在服务之间路由消息
- 监控消息交换过程
- 解决组件之间的竞争问题
- 提供公共服务(事件处理、加密、消息队列等)
- 保证服务质量(QoS)
| 优点 | 缺点 |
|---|---|
| 服务扩展性好 | 单点故障:ESB 挂了,整个系统都挂 |
| 工作负载分布 | 配置复杂,维护成本高 |
| 专注配置而非自定义集成 | 消息转换可能成为性能瓶颈 |
| 松耦合设计更容易实现 | |
| 服务可替换 |
2.2 Web 服务
通过互联网协议(主要是 HTTP)提供的服务,是另一种常见的 SOA 实现。
| 优点 | 缺点 |
|---|---|
| 使用成熟的 Web 标准 | 很多额外开销 |
| 丰富的工具生态 | 部分实现过于复杂(如 SOAP/WSDL/UDDI) |
| 可扩展性强 |
2.3 消息队列与消息代理
消息队列:就像一个传送带,一端放东西,另一端取东西,两端互不阻塞。
消息代理(Message Broker):在消息队列基础上增加了翻译、验证、路由等高级功能,是"发布-订阅"模式的核心。
ASCII 图:发布-订阅模式
发布者A ──┐
发布者B ──┼──► [消息代理] ──► 订阅者1(只收天气类)
发布者C ──┘ │
└──► 订阅者2(只收新闻类)
└──► 订阅者3(收所有消息)
消息代理能做的事:
- 把消息从一种格式转换成另一种格式
- 验证消息发送方、接收方或内容
- 将消息路由到一个或多个目标
- 聚合、拆分、重组消息
- 从外部服务获取数据
- 处理错误和事件
常见消息协议:
| 协议 | 特点 | 典型实现 |
|---|---|---|
| AMQP | 二进制协议,应用层 | RabbitMQ, ActiveMQ |
| STOMP | 文本协议,类似 HTTP | RabbitMQ, ActiveMQ |
| MQTT | 轻量级,适合物联网 | AWS IoT, Azure IoT Hub |
三、微服务(Microservices)
微服务 vs ESB:它们的关系
微服务和 ESB 在很多方面是相反的。ESB 是一个"中央集权"的大管家,而微服务是"去中心化"的小团队自治。但两者都属于 SOA 的范畴,微服务是 SOA 的现代化演进。
3.1 微服务的优点
模块化(Modularity)
每个微服务只做一件事,代码量小,容易理解和测试。
代码仓库策略对比:
ASCII 图:Mono-repo vs Multi-repo
Mono-repo(单仓库): Multi-repo(多仓库):
┌─────────────────────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ my-project/ │ │svc-A │ │svc-B │ │svc-C │
│ ├── service-A/ │ │repo │ │repo │ │repo │
│ ├── service-B/ │ └──────┘ └──────┘ └──────┘
│ └── service-C/ │
└─────────────────────┘
优点:依赖管理简单 优点:独立发布,扩展灵活
缺点:项目大后构建慢 缺点:代码重复,CI/CD 复杂
微前端(Micro Frontend)也是同样的思路——把前端拆成独立的小模块,甚至可以用 WebAssembly 让 C++ 运行在浏览器里。
可扩展性(Scalability)
| 扩展方式 | 说明 | 适用场景 |
|---|---|---|
| 垂直扩展(Scale Up) | 换更大的机器 | 单体应用唯一选项,成本高 |
| 水平扩展(Scale Out) | 加更多机器 | 微服务首选,成本低 |
微服务只需要对有瓶颈的那个服务单独扩展,而不是整个系统一起扩展,更省钱。
灵活性(Flexibility)
- 可以逐步替换技术栈,不用一次性全部迁移
- 使用金丝雀部署(Canary Deployment)降低风险
- 不同服务可以用不同的数据库、消息队列,甚至不同的云平台
与遗留系统集成
不必把老系统全部推倒重来。可以只把需要频繁迭代的部分拆成微服务,老的部分继续运行。
支持分布式团队开发
小型自治团队各自负责一个或几个微服务,减少沟通成本,适合远程协作。
3.2 微服务的缺点
| 缺点 | 说明 |
|---|---|
| 依赖成熟的 DevOps | 必须有 CI/CD 流水线,否则部署噩梦 |
| 调试更困难 | 需要日志聚合、分布式追踪等可观测性工具 |
| 额外开销 | RPC 框架、API 网关、数据库、消息队列……基础设施成本高 |
重要警告: 微服务不是银弹。小公司往往无法承受维护成本。有些大公司把微服务拆得太细,后来又不得不把它们合并成"宏服务(Macroservices)"。
四、微服务的设计模式
4.1 分解模式(Decomposition Patterns)
按业务能力分解(Business Capability):
- 关注"企业做什么"——比如商户管理、客户管理
- 适合有明确业务分工的组织结构
按子域分解(Subdomain / DDD): - 关注"企业要解决什么问题"——比如订单履行、支付处理、商品目录管理
- 来自领域驱动设计(Domain-Driven Design)
两者区别: - 业务能力分解 → 关注组织结构
- 子域分解 → 关注业务问题
4.2 数据库模式(Database per Service)
每个微服务拥有自己独立的数据库,互不干扰。
ASCII 图:数据库隔离
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户服务 │ │ 订单服务 │ │ 支付服务 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
[用户数据库] [订单数据库] [支付数据库]
(PostgreSQL) (MongoDB) (MySQL)
好处:各自优化,独立演进。
代价:数据一致性更难保证,基础设施开销更大。
4.3 部署策略
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 每主机单服务 | 一台机器只跑一种微服务 | 资源隔离好,可精细调优 | 部分机器可能利用率低 |
| 每主机多服务 | 一台机器跑多种微服务 | 机器利用率高 | 服务互相影响,依赖冲突 |
五、解决扩展瓶颈的技术
三大主要扩展瓶颈:内存、存储、计算
5.1 内存外包:Redis 与 Memcached
问题场景:
ASCII 图:多副本缓存浪费问题
副本1 副本2
┌─────────────────┐ ┌─────────────────┐
│ 本地缓存: │ │ 本地缓存: │
│ key="result" ✓ │ │ 还没计算,重复算 │
└─────────────────┘ └─────────────────┘
浪费!
解决方案:外置缓存
ASCII 图:外置缓存架构
副本1 ──┐
├──► [Redis / Memcached 外置缓存] ◄── 所有副本共享
副本2 ──┘
| 对比项 | Memcached | Redis |
|---|---|---|
| 发布时间 | 2003 | 2009 |
| 数据类型 | 仅字符串键值 | 字符串、列表、集合、哈希、地理数据等 |
| 数据持久化 | 有限支持(1.5.18+ 温重启) | 支持快照和持久化 |
| 发布订阅 | 不支持 | 支持 |
| Lua 脚本 | 不支持 | 支持 |
| 适用场景 | 简单数据库查询缓存 | 通用缓存、排行榜、会话、消息队列 |
| 推荐程度 | 轻量简单场景 | 大多数场景首选 |
Redis 使用示例(C++ / redis-plus-plus)
// 需要安装 redis-plus-plus 库
// 头文件
#include <sw/redis++/redis++.h> // Redis C++ 客户端库
#include <algorithm> // std::copy
#include <iostream> // 输出
#include <iterator> // std::ostream_iterator
#include <vector> // std::vector
using namespace sw::redis;
int main() {
try {
// 1. 连接到本地 Redis 服务器(默认端口 6379)
auto redis = Redis("tcp://127.0.0.1:6379");
// 2. 存储一个简单的字符串键值对
// set(key, value) → 相当于:key = value
redis.set("poem", "late goodbye");
// 3. 读取刚才存储的值
// get(key) 返回 std::optional<std::string>,可能为空
if (const auto val = redis.get("poem")) {
// *val 解引用 optional,获取实际字符串
std::cout << *val << std::endl; // 输出:late goodbye
}
// 4. 先删除列表(避免重复追加旧数据)
redis.del("students");
// 5. 向列表右端依次推入多个元素
// rpush(key, {元素1, 元素2, ...}) → 建立一个有序列表
redis.rpush("students", {"Allison", "John", "Brian", "Andrew", "Claire"});
// 6. 读取列表全部内容
// lrange(key, 起始索引, 终止索引)
// 0 表示第一个,-1 表示最后一个(即读取全部)
std::vector<std::string> vec;
redis.lrange("students", 0, -1, std::back_inserter(vec));
// 7. 把 vector 里的内容打印出来,用空格分隔
std::copy(
vec.begin(),
vec.end(),
std::ostream_iterator<std::string>(std::cout, " ")
);
// 输出:Allison John Brian Andrew Claire
} catch (const Error& e) {
// 捕获 Redis 操作异常并打印错误信息
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
编译方式(Linux):
g++ -std=c++17 redis_example.cpp -o redis_example \
-lredis++ -lhiredis -lpthread
5.2 存储外包:对象存储(S3)
传统存储(NFS、块设备)有目录结构,而 Amazon S3 引入了对象存储:只有"桶(Bucket)“和"键(Key)”,没有文件夹的概念。
ASCII 图:传统文件系统 vs 对象存储
传统文件系统: 对象存储(S3):
/ bucket: my-photos
├── images/ ├── photo-001.jpg
│ ├── photo-001.jpg ├── photo-002.jpg
│ └── photo-002.jpg └── video-001.mp4
└── videos/
└── video-001.mp4
AWS S3 创建存储桶示例(C++)
// AWS SDK for C++ 示例
#include <aws/core/Aws.h> // AWS SDK 核心
#include <aws/core/utils/UUID.h> // 生成唯一 ID
#include <aws/s3/S3Client.h> // S3 客户端
#include <aws/s3/model/CreateBucketRequest.h> // 创建桶请求
#include <spdlog/spdlog.h> // 日志库
#include <string>
#include <iostream>
// 指定创建的桶所在 AWS 区域(这里选欧洲法兰克福区)
constexpr auto region =
Aws::S3::Model::BucketLocationConstraint::eu_central_1;
/**
* 为新注册用户创建专属的 S3 存储桶
* @param username 用户名(用于构造桶名称的一部分)
* @return 创建成功返回 true,失败返回 false
*/
bool create_user_bucket(const std::string& username) {
// 构造创建桶的请求对象
Aws::S3::Model::CreateBucketRequest request;
// 生成随机 UUID 前缀,确保桶名全局唯一
// S3 桶名必须全局唯一且长度在 3~63 个字符之间
const Aws::String unique_prefix = Aws::Utils::UUID::RandomUUID();
const Aws::String bucket_name("games-" + username);
const Aws::String full_name = unique_prefix + bucket_name;
// 设置桶名称(转为小写,S3 要求桶名全部小写)
request.SetBucket(
Aws::Utils::StringUtils::ToLower(full_name.c_str())
);
// 配置桶所在的区域
Aws::S3::Model::CreateBucketConfiguration bucket_config;
bucket_config.SetLocationConstraint(region);
request.SetCreateBucketConfiguration(bucket_config);
// 创建 S3 客户端并发送请求
const Aws::S3::S3Client s3_client;
if (const auto outcome = s3_client.CreateBucket(request);
!outcome.IsSuccess()) {
// 如果失败,记录错误信息
const auto& err = outcome.GetError();
spdlog::error("ERROR: CreateBucket: {}: {}",
err.GetExceptionName(),
err.GetMessage());
return false;
}
return true; // 成功
}
int main() {
const std::string username = "david-lightman";
// 初始化 AWS SDK(必须在使用任何 AWS 功能前调用)
const Aws::SDKOptions options;
Aws::InitAPI(options);
if (create_user_bucket(username)) {
std::cout << "The bucket for " << username
<< " is ready" << std::endl;
}
// 关闭 AWS SDK(释放资源)
Aws::ShutdownAPI(options);
return 0;
}
MinIO 创建存储桶示例(兼容 S3 的开源方案)
// MinIO C++ SDK 示例(与 S3 API 兼容)
#include <miniocpp/client.h> // MinIO 客户端
#include <sole.hpp> // 生成 UUID(sole 库)
#include <spdlog/spdlog.h> // 日志库
#include <string>
#include <iostream>
// 注意:实际生产环境中凭据不应硬编码,应从环境变量或密钥管理系统获取
std::string access_key = "Q3AM3UQ867SPQQA43P2F";
std::string secret_key = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG";
/**
* 使用 MinIO 为用户创建存储桶
* @param username 用户名
* @return 成功返回 true
*/
bool create_user_bucket(const std::string& username) {
// 指定 MinIO 服务器地址(这里使用 MinIO 官方测试服务器)
minio::s3::BaseUrl base_url("play.min.io");
// 使用静态凭据提供者(access_key + secret_key)
minio::creds::StaticProvider provider(access_key, secret_key);
// 创建客户端实例
minio::s3::Client client(base_url, &provider);
// 生成 UUID 前缀,确保桶名唯一
std::string unique_prefix = sole::uuid4().str();
std::string bucket_name{"petticoat-acres-" + username};
std::string full_name{unique_prefix + bucket_name};
// 构造创建桶的参数
minio::s3::MakeBucketArgs args;
args.bucket = full_name;
// 发送创建请求
if (const minio::s3::MakeBucketResponse resp = client.MakeBucket(args)) {
spdlog::info("{} bucket is created successfully", args.bucket);
} else {
spdlog::error("Unable to create bucket {}: {}",
args.bucket,
resp.Error().String());
return false;
}
return true;
}
int main() {
const std::string username = "prayerincpp";
if (create_user_bucket(username)) {
spdlog::info("The bucket for {} is ready", username);
}
return 0;
}
5.3 计算外包:任务队列
思路:把耗时的长任务从主流程中剥离出去,异步处理。
ASCII 图:任务队列工作流程
主进程 任务队列 工作进程
│ │ │
│──提交任务(发送消息)──►│ │
│ │──────分配任务────────►│
│◄──────立即返回────────│ │ (异步,不阻塞)
│ (继续干别的事) │ 处理任务中...│
│ │◄──────返回结果────────│
│◄───查询结果───────────│ │
常见任务队列框架:
- Python: Celery
- Ruby: Sidekiq
- TypeScript/Node.js: BullMQ
- Go: Machinery
(目前 C++ 没有成熟的同类框架,可通过 Redis 的列表数据结构自行实现,或通过 SWIG 调用其他语言的任务队列)
六、微服务扩展策略
6.1 单服务单主机部署的扩展:自动扩缩容组
当 CPU 负载超过阈值时,自动添加新实例;流量下降时,自动缩减实例节省费用。
Terraform 自动扩缩容配置示例(附注释):
# 这段配置定义了一个自动扩缩容策略
autoscaling_policy {
max_replicas = 5 # 最多允许 5 个实例同时运行
min_replicas = 3 # 至少保持 3 个实例(保证可用性)
cooldown_period = 60 # 新实例启动后等待 60 秒再采集指标(让实例热身)
cpu_utilization {
target = 0.8 # 当 CPU 平均使用率达到 80% 时触发扩容
}
}
工作流程:
6.2 多服务多主机部署的扩展:容器编排
使用 Kubernetes 或 Docker Swarm 对容器化的微服务进行编排和扩展,实现更精细的资源控制(详见第十六章)。
七、云计算与 SOA 的结合
云计算是 SOA 自然的延伸。云提供商把基础设施封装成 API 可调用的"服务",与 SOA 的理念完美契合。
云计算 vs 传统主机托管
| 对比项 | 传统 VPS 托管 | 云计算 |
|---|---|---|
| 管理方式 | 手动配置 | API 驱动,可编程 |
| 扩展性 | 需要联系运营商 | 自动扩缩容 |
| 服务类型 | 仅虚拟机 | 虚拟机 + 数据库 + 消息队列 + 存储 + AI 等 |
| 供应商绑定 | 较低 | 较高(迁移成本大) |
重要提醒: 把应用搬到云上不等于"复制粘贴"到虚拟机。云计算只有在应用专为云设计(可扩展、云感知)时才划算。否则只是浪费钱。
访问云 API 的两种认证方式
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| API Token | 应用携带密钥访问 | 通用,不依赖宿主机 | Token 泄露风险,需加密存储 |
| 实例权限 | 运行在有权限的虚拟机上,无需单独认证 | 实现简单 | 机器被入侵后所有应用都受影响 |
使用云 CLI 脚本示例(Azure,附注释)
#!/bin/sh
# 定义变量
RESOURCE_GROUP=dominicanfair # 资源组名称
VM_NAME=dominic # 虚拟机名称
REGION=germanynorth # 部署区域(德国北部)
# 第一步:创建资源组(类似一个文件夹,用于管理相关资源)
az group create --name $RESOURCE_GROUP --location $REGION
# 第二步:在资源组中创建一台 Ubuntu 虚拟机
# --ssh-key-values 指定 SSH 公钥,用于安全登录
az vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image UbuntuLTS \
--ssh-key-values dominic_key.pub
IaC(基础设施即代码)工具
这类工具提供跨云平台的抽象层,用统一的配置语言描述基础设施,避免被单一云厂商锁定。
常见工具:Terraform、OpenTofu、Pulumi
八、全章知识总结
九、本章问题解答
Q1:SOA 中服务的四个属性是什么?
- 代表有明确结果的业务活动
- 自包含
- 对用户透明(隐藏内部实现)
- 可由其他服务组合而成
Q2:Web 服务的优点有哪些?
使用成熟的 Web 标准、丰富的工具生态、良好的可扩展性。
Q3:什么时候微服务不是好选择?
- 团队缺乏 DevOps 能力时
- 应用规模较小、复杂性较低时
- 无法承担基础设施开销的小公司
- 没有自动化 CI/CD 流水线时
Q4:微服务如何帮助更好地使用系统资源?
只对有瓶颈的服务进行水平扩展,而不是扩展整个系统,避免浪费。
Q5:微服务和单体应用如何共存?
只拆分需要频繁迭代的部分为微服务,保留运行良好的单体部分,渐进式迁移。
Q6:哪类团队最受益于微服务?
分布式、远程协作的大型团队,每个小团队可以自治地负责一个或多个服务。
Q7:为什么引入微服务需要成熟的 DevOps?
微服务数量多,手动部署不可行;没有 CI/CD 流水线,所有微服务优势都无法发挥,反而带来更多麻烦。
Q8:微服务的两种部署策略及各自优点?
| 策略 | 优点 |
|---|---|
| 单主机单服务 | 资源隔离,可针对性优化硬件配置 |
| 单主机多服务 | 提高机器利用率,降低基础设施成本 |
Q9:云平台与传统主机托管有何不同?
云平台通过 API 提供可编程的资源管理,并在虚拟机之外提供数据库、消息队列、存储等大量托管服务,整个基础设施都可以用代码定义和自动化。
第十五章:服务间通信(ISC)— 从零理解
本章学习路线图
一、什么是服务间通信(ISC)?
IPC vs ISC 的关系
IPC(Inter-Process Communication,进程间通信) 是更广的概念,包括:
- 文件、信号量、管道、共享内存(本地)
- 网络套接字、消息队列(可跨机器)
ISC(Interservice Communication,服务间通信) 是 IPC 的子集,专指: - 通过网络进行的
- 使用高层协议的
- 独立服务之间的数据交换
ASCII 图:IPC 与 ISC 的包含关系
┌─────────────────────────────────────────┐
│ IPC(进程间通信) │
│ ┌──────────────────────────────────┐ │
│ │ ISC(服务间通信) │ │
│ │ 网络 / 高层协议 / 独立服务 │ │
│ └──────────────────────────────────┘ │
│ 本地:共享内存、管道、信号量 ... │
└─────────────────────────────────────────┘
二、交互风格
2.1 两个维度
微服务之间的通信可以从两个维度来分类:
维度一:通信类型
| 类型 | 模型 | 特点 | 生活类比 |
|---|---|---|---|
| 同步(Synchronous) | 请求-响应 | 客户端发送请求后等待响应,可能被阻塞 | 打电话,对方不接你就一直等 |
| 异步(Asynchronous) | 发送即忘 | 客户端发送消息后继续干别的,不等响应 | 发短信,发完继续做其他事 |
维度二:交互模型
ASCII 图:四种交互模型
一对一(One-to-One): 一对多(One-to-Many):
服务A ──► 服务B 服务A ──► 服务B
├──► 服务C
└──► 服务D
多对一(Many-to-One): 多对多(Many-to-Many):
服务A ──┐ 服务A ──┬──► 服务D
服务B ──┼──► 服务D 服务B ──┤ 服务E
服务C ──┘ 服务C ──┴──► 服务F
| 模型 | 说明 | 适用场景 | 注意点 |
|---|---|---|---|
| 一对一 | 两个服务直接专属连接 | 两个服务高度依赖、实时交换数据 | 容易产生紧耦合 |
| 一对多 | 一个服务发消息给多个服务 | 需要扩展性和可用性的场景 | 消息可能丢失或重复 |
| 多对一 | 多个服务向一个中心服务发消息 | 集中处理共享功能或数据 | 中心服务需处理多种格式 |
| 多对多 | 多个服务互相交互 | 复杂动态系统 | 故障传播风险高,需容错设计 |
三、消息系统
3.1 为什么用消息系统?
核心价值:用技术中立的方式连接用不同语言、不同工具开发的服务。
典型使用场景:
- 金融交易处理
- 车队监控
- 物流数据采集
- 传感器数据处理
- 订单履行
- 任务队列
3.2 低开销消息系统
专为资源受限环境设计(IoT 设备、嵌入式系统)。
常见微控制器内存参考:
| 设备 | 典型 RAM | 适用消息系统 |
|---|---|---|
| Arduino | 2~4 KB | MQTT-SN |
| ESP32 | 520 KB | MQTT |
| STM32 | 16~192 KB | MQTT |
| Linux IoT 控制器 | 32 MB~4 GB | 均可 |
| 工业 PLC | 3~20 MB | 均可 |
MQTT(消息队列遥测传输)
MQTT 是一个轻量级发布-订阅协议,专为低带宽、高延迟或不稳定网络设计。
ASCII 图:MQTT 架构
发布者(传感器) 订阅者(应用程序)
┌─────────────┐ ┌─────────────┐
│ 温度传感器 │──publish──► │ 手机 App │
└─────────────┘ │ └─────────────┘
┌─────────────┐ ▼ ┌─────────────┐
│ 湿度传感器 │ [MQTT Broker] │ Web 控制台 │
└─────────────┘ │ └─────────────┘
└──────► ┌─────────────┐
│ 数据库服务 │
└─────────────┘
MQTT 三种服务质量(QoS)级别:
| 级别 | 名称 | 说明 | 适用场景 |
|---|---|---|---|
| QoS 0 | 最多一次(At most once) | 发了就算,不确认,可能丢消息 | 高频非关键数据(环境监测) |
| QoS 1 | 至少一次(At least once) | 确认收到,但可能重复 | 重要数据,允许少量重复 |
| QoS 2 | 恰好一次(Exactly once) | 不丢不重,开销最大 | 金融交易、医疗读数、安全警报 |
MQTT 的缺点:
- 所有通信经过 Broker,Broker 崩了全系统停摆
- 默认安全性较弱(只有用户名密码)
- 不适合大数据集或复杂结构
- 不支持视频流
- TLS 加密对极低功耗设备负担过重
MQTT 发布者完整 C++ 示例(Eclipse Paho 库):
// publisher.cpp - MQTT 发布者
// 编译:g++ publisher.cpp -o publisher -lpaho-mqttpp3 -lpaho-mqtt3a
#include <mqtt/async_client.h> // Eclipse Paho MQTT C++ 异步客户端
#include <iostream>
#include <string>
// 服务器地址、客户端ID、发布的主题
const std::string SERVER_ADDRESS("tcp://localhost:1883");
const std::string CLIENT_ID("publisher");
const std::string TOPIC("test/topic");
constexpr int QOS = 1; // 使用 QoS 1:至少一次投递
// 事件回调类,继承自 mqtt::callback
class callback final : public mqtt::callback {
public:
// 当连接断开时被触发
void connection_lost(const std::string& cause) override {
std::cout << "Connection lost: " << cause << std::endl;
}
// 当消息成功投递给 Broker 时被触发
void delivery_complete(mqtt::delivery_token_ptr token) override {
std::cout << "Message delivered" << std::endl;
}
};
int main() {
// 创建异步 MQTT 客户端
mqtt::async_client client(SERVER_ADDRESS, CLIENT_ID);
// 注册回调对象
callback cb;
client.set_callback(cb);
// 配置连接选项
mqtt::connect_options conn_opts;
conn_opts.set_keep_alive_interval(20); // 心跳保活间隔(秒)
conn_opts.set_clean_session(true); // 新会话,不恢复旧状态
try {
// 连接到 MQTT Broker,wait() 表示等待连接完成(同步等待)
client.connect(conn_opts)->wait();
std::cout << "Connected to MQTT broker" << std::endl;
// 构造要发布的消息
std::string payload = "It's a UNIX system!";
// make_message(主题, 内容, QoS级别, 是否保留)
mqtt::message_ptr pub_msg = mqtt::make_message(TOPIC, payload, QOS, false);
// 发布消息,wait() 等待投递确认
client.publish(pub_msg)->wait();
std::cout << "Message published: " << payload << std::endl;
// 断开连接
client.disconnect()->wait();
std::cout << "Disconnected" << std::endl;
} catch (const mqtt::exception& exc) {
std::cerr << "MQTT Exception: " << exc.what() << std::endl;
return 1;
}
return 0;
}
MQTT 订阅者完整 C++ 示例:
// subscriber.cpp - MQTT 订阅者
// 编译:g++ subscriber.cpp -o subscriber -lpaho-mqttpp3 -lpaho-mqtt3a
#include <mqtt/async_client.h>
#include <iostream>
#include <string>
const std::string SERVER_ADDRESS("tcp://localhost:1883");
const std::string CLIENT_ID("subscriber");
const std::string TOPIC("test/topic");
constexpr int QOS = 1;
// 事件回调类
class callback final : public mqtt::callback {
public:
// 连接断开时触发
void connection_lost(const std::string& cause) override {
std::cout << "Connection lost: " << cause << std::endl;
}
// 收到消息时触发(关键回调)
void message_arrived(mqtt::const_message_ptr msg) override {
// get_payload_str() 获取消息体字符串
std::cout << "Message arrived: " << msg->get_payload_str() << std::endl;
}
};
int main() {
mqtt::async_client client(SERVER_ADDRESS, CLIENT_ID);
callback cb;
client.set_callback(cb);
mqtt::connect_options conn_opts;
conn_opts.set_keep_alive_interval(20);
conn_opts.set_clean_session(true);
try {
client.connect(conn_opts)->wait();
std::cout << "Connected to MQTT broker" << std::endl;
// 订阅指定主题,wait() 等待订阅确认
client.subscribe(TOPIC, QOS)->wait();
std::cout << "Subscribed to topic: " << TOPIC << std::endl;
// 阻塞等待用户按下 Enter 键退出
std::cout << "Press Enter to exit..." << std::endl;
std::cin.get();
client.disconnect()->wait();
std::cout << "Disconnected" << std::endl;
} catch (const mqtt::exception& exc) {
std::cerr << "MQTT Exception: " << exc.what() << std::endl;
return 1;
}
return 0;
}
ZeroMQ(无 Broker 消息队列)
ZeroMQ 最大的特点是没有中心 Broker,消息直接在服务之间传递。
类比:MQTT 像邮局(必须经过邮局中转),ZeroMQ 像快递员直接登门(点对点,无中间商)。
ASCII 图:ZeroMQ 发布-订阅模式(无 Broker)
发布者 订阅者
┌────────────┐ TCP 直连 ┌────────────┐
│ zmq_bind │◄──────────────►│ zmq_connect│
│ ZMQ_PUB │ 127.0.0.1 │ ZMQ_SUB │
└────────────┘ :5556 └────────────┘
消息直接送达,无需中间节点
ZeroMQ 支持的通信模式:
- 发布-订阅(Pub-Sub)
- 请求-回复(Request-Reply)
- 推-拉(Push-Pull)
- 配对(Pair)
ZeroMQ 消息大小上限: 263−12^{63} - 1263−1 字节
ZeroMQ 发布者完整 C++ 示例(libzmq):
// zmq_publisher.cpp - ZeroMQ 发布者(无 Broker)
// 编译:g++ zmq_publisher.cpp -o zmq_publisher -lzmq
#include <zmq.h> // ZeroMQ C 库头文件
#include <cassert> // assert 断言
#include <cstring> // strlen
#include <cstdio> // printf
int main() {
// 1. 创建 ZeroMQ 上下文(类似于线程池,管理 socket 资源)
void* context = zmq_ctx_new();
// 2. 创建一个 PUB 类型的 socket(发布者)
void* publisher = zmq_socket(context, ZMQ_PUB);
// 3. 绑定到本地地址和端口(发布者主动绑定,订阅者主动连接)
int rc = zmq_bind(publisher, "tcp://127.0.0.1:5556");
assert(rc == 0); // 断言绑定成功
char msg[] = "Gravity cascades";
size_t msg_size = strlen(msg);
// 4. 持续发送消息(教学示例,实际使用应添加延时避免 CPU 占满)
while (true) {
// zmq_send 返回实际发送的字节数,与消息长度不等说明出错
rc = zmq_send(publisher, msg, msg_size, 0);
assert(rc == (int)msg_size);
}
// 5. 清理资源(实际上此处因为无限循环不会到达,仅示意)
zmq_close(publisher);
zmq_ctx_destroy(context);
return 0;
}
ZeroMQ 订阅者完整 C++ 示例:
// zmq_subscriber.cpp - ZeroMQ 订阅者
// 编译:g++ zmq_subscriber.cpp -o zmq_subscriber -lzmq
#include <zmq.h>
#include <cassert>
#include <cstdio>
#define MSG_SIZE 1024 // 接收缓冲区大小
int main() {
// 1. 创建上下文
void* context = zmq_ctx_new();
// 2. 创建 SUB 类型的 socket(订阅者)
void* subscriber = zmq_socket(context, ZMQ_SUB);
// 3. 连接到发布者的地址(订阅者主动连接,不绑定)
int rc = zmq_connect(subscriber, "tcp://127.0.0.1:5556");
assert(rc == 0);
// 4. 设置订阅过滤器
// 第二个参数 "" 表示订阅所有消息(空前缀 = 不过滤)
// 第三个参数 0 表示过滤字符串长度为 0
rc = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, "", 0);
assert(rc == 0);
char msg[MSG_SIZE + 1]; // +1 为 '\0' 预留空间
// 5. 持续接收消息
while (true) {
// zmq_recv 返回接收到的字节数,-1 表示错误
rc = zmq_recv(subscriber, msg, MSG_SIZE, 0);
assert(rc != -1);
// 关键:zmq_recv 不会自动添加 '\0'
// 必须手动在末尾加上字符串终止符
msg[rc] = '\0';
printf("%s\n", msg);
}
// 6. 清理(同样因无限循环不会到达)
zmq_close(subscriber);
zmq_ctx_destroy(context);
return 0;
}
3.3 有 Broker 的消息系统
Apache Kafka vs RabbitMQ 对比
ASCII 图:Kafka vs RabbitMQ 定位差异
Kafka(流处理): RabbitMQ(消息路由):
┌──────────────────────┐ ┌──────────────────────┐
│ Producer │ │ Publisher │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ [Topic/Partition] │ │ [Exchange] │
│ 持久化日志,可重放 │ │ 路由规则灵活 │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Consumer(拉取) │ │ [Queue] │
│ │ │ Consumer(推送) │
└──────────────────────┘ └──────────────────────┘
| 对比项 | Apache Kafka | RabbitMQ |
|---|---|---|
| 编写语言 | Java | Erlang |
| 主要范式 | 事件流 / 日志存储 | 消息路由 / 任务队列 |
| 消息获取方式 | Consumer 主动拉取(Pull) | Broker 主动推送(Push) |
| 扩展方式 | 水平扩展(加机器) | 垂直扩展(升配置) |
| 消息路由 | 简单(基于 Topic/Partition) | 丰富(Direct/Topic/Fanout/Headers) |
| 消息持久化 | 默认持久,可配置保留期 | 需声明 durable 队列 |
| 最大消息 | 默认 1 MB(不建议超过) | 默认 16 MB(v4.x) |
| 适用场景 | 实时流处理、日志聚合、大数据 | 微服务解耦、后台任务、复杂路由 |
| GUI 工具 | Kafbat UI, AKHQ, Kafdrop 等 | 内置管理插件(Management Plugin) |
Apache Kafka 架构详解
ASCII 图:Kafka 架构
Producer ──► [Topic: orders]
│
├── Partition 0 ──► Broker 1 (Leader)
│ │
├── Partition 1 ──► Broker 2 (Leader) ◄── KRaft 协调
│ │
└── Partition 2 ──► Broker 3 (Leader)
│
Consumer Group ◄──────────────────────┘
(Consumer A 读 P0, Consumer B 读 P1, Consumer C 读 P2)
Kafka 核心概念:
- Producer:生产消息,发送到 Topic
- Consumer:消费消息,通常组成消费者组(Consumer Group)分工
- Topic:消息的逻辑频道,按内容分类
- Partition:Topic 的物理分片,实现并行处理
- Broker:Kafka 服务节点,多个 Broker 组成集群
- KRaft:新的共识协调机制(替代旧版 Zookeeper),用于 Leader 选举
Kafka 生产者完整 C++ 示例(modern-cpp-kafka):
// kafka_producer.cpp
// 编译需要:modern-cpp-kafka 库(依赖 librdkafka)
// g++ kafka_producer.cpp -o kafka_producer -lrdkafka++
#include <kafka/KafkaProducer.h> // Kafka 生产者
#include <iostream>
#include <string>
#include <cstdlib> // std::getenv
using namespace kafka;
using namespace kafka::clients::producer;
int main() {
// 从环境变量读取 Broker 地址,默认本地
// 格式示例:192.168.0.1:9092,192.168.0.2:9092
const char* tmp = std::getenv("KAFKA_BROKER_LIST");
std::string brokers{tmp != nullptr ? tmp : "127.0.0.1:9092"};
// 从环境变量读取 Topic 名称,默认 "test-topic"
tmp = std::getenv("TOPIC");
const Topic topic{tmp != nullptr ? tmp : "test-topic"};
// 配置生产者属性
const Properties props({
{"bootstrap.servers", {brokers}}, // Broker 地址列表
{"auto.create.topics.enable", {"true"}} // 不存在的 Topic 自动创建
});
// 创建 Kafka 生产者实例
KafkaProducer producer(props);
// 准备消息内容
std::string line{"Ready player three"};
// ProducerRecord(topic, key, value)
// NullKey 表示不指定消息键(影响分区分配)
ProducerRecord record(topic, NullKey, Value(line.c_str(), line.size()));
// 定义投递回调(Lambda 函数),消息投递完成后异步调用
auto deliveryCb = [](const RecordMetadata& metadata, const Error& error) {
if (!error) {
// metadata.toString() 包含 Topic、Partition、Offset 等信息
std::cout << "Message delivered: " << metadata.toString() << std::endl;
} else {
std::cerr << "Message failed: " << error.message() << std::endl;
}
};
// 异步发送消息,投递结果通过 deliveryCb 回调通知
producer.send(record, deliveryCb);
// 关闭生产者(会等待所有消息投递完成)
producer.close();
return 0;
}
Kafka 消费者完整 C++ 示例:
// kafka_consumer.cpp
#include <kafka/KafkaConsumer.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <chrono>
using namespace kafka;
using namespace kafka::clients::consumer;
int main() {
const char* tmp = std::getenv("KAFKA_BROKER_LIST");
std::string brokers{tmp != nullptr ? tmp : "127.0.0.1:9092"};
tmp = std::getenv("TOPIC");
const Topic topic{tmp != nullptr ? tmp : "test-topic"};
const Properties props({
{"bootstrap.servers", {brokers}},
{"group.id", {"test-group"}}, // 消费者组 ID
{"auto.offset.reset", {"earliest"}} // 从最早的消息开始消费
});
// 创建 Kafka 消费者实例
KafkaConsumer consumer(props);
// 订阅 Topic(可订阅多个)
consumer.subscribe({topic});
// 轮询循环:持续拉取消息
while (true) {
// poll(timeout): 等待最多 100ms 拉取消息
auto records = consumer.poll(std::chrono::milliseconds(100));
for (const auto& record : records) {
if (!record.error()) {
// 打印消息的各个元数据字段
std::cout << "Got a new message..." << std::endl;
std::cout << "\tTopic : " << record.topic() << std::endl;
std::cout << "\tPartition: " << record.partition() << std::endl;
std::cout << "\tOffset : " << record.offset() << std::endl;
std::cout << "\tTimestamp: " << record.timestamp().toString() << std::endl;
std::cout << "\tKey : [" << record.key().toString() << "]" << std::endl;
std::cout << "\tValue : [" << record.value().toString() << "]" << std::endl;
} else {
std::cerr << record.toString() << std::endl;
}
}
}
consumer.close();
return 0;
}
RabbitMQ 架构详解
ASCII 图:RabbitMQ 消息路由流程
Publisher
│
│ publish(routing_key="user.created")
▼
[Exchange] ◄── 根据类型和 binding 决定路由
│
├── Direct Exchange:routing_key 精确匹配
├── Topic Exchange: routing_key 通配符匹配(* 单词,# 多词)
├── Fanout Exchange:广播给所有绑定队列
└── Headers Exchange:按消息头属性匹配
│
├──► [Queue A] ──► Consumer 1
└──► [Queue B] ──► Consumer 2
RabbitMQ 生产者 C++ 示例(AMQP-CPP + libevent):
// rabbitmq_producer.cpp
// 编译:g++ rabbitmq_producer.cpp -o rabbitmq_producer -lamqpcpp -levent
#include <amqpcpp.h> // AMQP-CPP 主头文件
#include <amqpcpp/libevent.h> // libevent 事件循环适配器
#include <event2/event.h> // libevent
#include <iostream>
#include <string>
int main() {
// 1. 创建 libevent 事件循环基础对象
auto evbase = event_base_new();
// 2. 创建 AMQP 事件处理器(与 libevent 集成)
AMQP::LibEventHandler handler(evbase);
// 3. 建立到 RabbitMQ 服务器的 TCP 连接
// 格式:amqp://用户名:密码@主机:端口/虚拟主机
AMQP::TcpConnection connection(
&handler, AMQP::Address("amqp://guest:guest@localhost:5672/"));
// 4. 在连接上创建信道(Channel)—— AMQP 中通信的逻辑单元
AMQP::TcpChannel channel(&connection);
// 5. 注册信道错误处理器
channel.onError([&evbase](const char* message) {
std::cout << "Channel error: " << message << std::endl;
event_base_loopbreak(evbase); // 出错时退出事件循环
});
// 6. 声明一个 Direct Exchange(如果不存在则创建)
auto exchange_name = "greet-exchange";
channel.declareExchange(exchange_name, AMQP::ExchangeType::direct);
// 7. 开启发布确认模式(Publisher Confirms)
// 保证消息被 Broker 接收后才算成功
channel.confirmSelect()
.onSuccess([&]() {
// 成功开启确认模式后,发布消息
auto routing_key = "greet-routing";
channel.publish(exchange_name, routing_key,
"Tommy, Chuckie, Phil and Lil");
})
.onAck([&](uint64_t delivery_tag, bool multiple) {
// Broker 确认消息已接收(ACK)
std::cout << "Message acknowledged" << std::endl;
event_base_loopbreak(evbase); // 任务完成,退出循环
})
.onNack([&](uint64_t delivery_tag, bool multiple, bool requeue) {
// Broker 拒绝消息(NACK)
std::cerr << "Message not acknowledged" << std::endl;
event_base_loopbreak(evbase);
});
// 8. 启动 libevent 事件循环(在此阻塞,等待事件)
event_base_dispatch(evbase);
// 9. 清理资源
event_base_free(evbase);
return 0;
}
RabbitMQ 消费者 C++ 示例:
// rabbitmq_consumer.cpp
#include <amqpcpp.h>
#include <amqpcpp/libevent.h>
#include <event2/event.h>
#include <iostream>
#include <string>
int main() {
auto evbase = event_base_new();
AMQP::LibEventHandler handler(evbase);
AMQP::TcpConnection connection(
&handler, AMQP::Address("amqp://guest:guest@localhost:5672/"));
AMQP::TcpChannel channel(&connection);
channel.onError([&evbase](const char* message) {
std::cout << "Channel error: " << message << std::endl;
event_base_loopbreak(evbase);
});
// 与生产者使用同一个 Exchange 名称
auto exchange_name = "greet-exchange";
channel.declareExchange(exchange_name, AMQP::ExchangeType::direct);
// 1. 声明队列(durable = 持久化,Broker 重启后队列不丢失)
auto queue_name = "greet-queue";
channel.declareQueue(queue_name, AMQP::durable);
// 2. 将队列绑定到 Exchange,指定 routing_key
// 只有 routing_key 匹配的消息才会进入这个队列
auto routing_key = "greet-routing";
channel.bindQueue(exchange_name, queue_name, routing_key);
// 3. 开始消费队列中的消息
channel.consume(queue_name)
.onReceived(
[&channel](const AMQP::Message& msg, uint64_t tag, bool redelivered) {
// 打印消息内容(msg.body() 是 char*,msg.bodySize() 是长度)
std::cout << "Received: "
<< std::string{msg.body(), msg.bodySize()}
<< std::endl;
// 发送 ACK 确认(告诉 Broker 消息已处理,可以从队列删除)
// 不发 ACK 则消息会被重新投递
channel.ack(tag);
});
// 启动事件循环(持续监听消息)
event_base_dispatch(evbase);
event_base_free(evbase);
return 0;
}
四、Web 服务
4.1 数据格式
ASCII 图:Web 服务数据格式演进
XML(冗长) JSON(简洁) 二进制(高效)
<FindMerchants> { [Protobuf 二进制]
<Lat>54.35</Lat> "lat": 54.35, 更小、更快
<Long>18.65</Long> "long": 18.65 不可读
</FindMerchants> }
HTTP 内容压缩算法对比:
| 算法 | 特点 | 适用场景 |
|---|---|---|
| Gzip | 历史最久,兼容性最好 | 通用场景 |
| Brotli | 压缩率更高 | 静态、可缓存内容 |
| Zstandard | 速度快,适合动态内容 | 动态内容 |
| Deflate | Gzip 的基础算法 | 较少单独使用 |
4.2 XML 系列 Web 服务
SOAP 示例
SOAP 消息结构(以查找商户为例):
POST /FindMerchants HTTP/1.1
Host: www.domifair.org
Content-Type: application/soap+xml; charset=utf-8
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="...">
<soap:Header>
<!-- 可选的扩展头 -->
</soap:Header>
<soap:Body xmlns:m="https://www.domifair.org">
<m:FindMerchants>
<m:Lat>54.350989</m:Lat> <!-- 纬度 -->
<m:Long>18.6548168</m:Long> <!-- 经度 -->
<m:Distance>200</m:Distance> <!-- 搜索半径(米) -->
</m:FindMerchants>
</soap:Body>
</soap:Envelope>
SOAP 的三层结构:
- Envelope(信封):定义消息结构
- Header(头部,可选):应用特定数据
- Body(主体):实际的 RPC 调用和响应
4.3 JSON 系列 Web 服务
同样的查找商户请求,用 JSON-RPC 2.0 表示:
{
"jsonrpc": "2.0",
"method": "FindMerchants",
"params": {
"lat": "54.350989",
"long": "18.6548168",
"distance": 200
},
"id": 1
}
对比 SOAP:紧凑得多,只有必要的元数据。
4.4 REST API 设计
REST 六大约束:
HTTP 动词与 CRUD 的对应关系:
| HTTP 动词 | CRUD 操作 | 说明 | 成功状态码 |
|---|---|---|---|
| POST | Create | 创建新资源 | 201 Created |
| GET | Read | 读取资源 | 200 OK |
| PUT | Update/Replace | 替换整个资源 | 200 OK |
| PATCH | Update/Modify | 部分更新资源 | 200 OK |
| DELETE | Delete | 删除资源 | 204 No Content |
REST API 完整 C++ 示例(Drogon 框架):
// items_controller.cpp - 基于 Drogon 框架的 RESTful CRUD API
// 编译:需要安装 drogon 框架
// 实现对 /api/items 资源的增删改查
#include <drogon/HttpController.h> // Drogon HTTP 控制器基类
#include <json/json.h> // JSON 处理(jsoncpp)
#include <mutex> // 互斥锁(线程安全)
#include <optional> // std::optional
#include <unordered_map> // 哈希表存储数据
#include <functional>
#include <string>
#include <iostream>
using namespace drogon;
class ItemsController final : public HttpController<ItemsController> {
public:
// 路由表:定义 URL 路径与处理函数的映射
METHOD_LIST_BEGIN
ADD_METHOD_TO(ItemsController::getItems, "/api/items", Get); // 获取全部
ADD_METHOD_TO(ItemsController::createItem, "/api/items", Post); // 创建
ADD_METHOD_TO(ItemsController::getItemById, "/api/items/{1}", Get); // 按 ID 获取
ADD_METHOD_TO(ItemsController::updateItem, "/api/items/{1}", Put); // 更新
ADD_METHOD_TO(ItemsController::deleteItem, "/api/items/{1}", Delete); // 删除
METHOD_LIST_END
// GET /api/items — 获取所有 Item
void getItems(const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback) {
Json::Value array(Json::arrayValue);
std::lock_guard lock(storage_mutex_); // 加锁保护共享数据
for (const auto& [id, name] : items_) {
Json::Value item;
item["id"] = id;
item["name"] = name;
array.append(item);
}
// 返回 JSON 响应,默认状态码 200 OK
callback(HttpResponse::newHttpJsonResponse(array));
}
// POST /api/items — 创建新 Item
void createItem(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) {
auto json = req->getJsonObject(); // 解析请求体 JSON
// 验证请求:必须包含 "name" 字段
if (!json || !json->isMember("name")) {
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest); // 400 Bad Request
resp->setBody("No name");
callback(resp);
return;
}
int id;
auto name = (*json)["name"].asString();
{
std::lock_guard lock(storage_mutex_);
items_.emplace(id = ++next_id_, name); // 插入数据,自增 ID
}
Json::Value result;
result["id"] = id;
result["name"] = name;
auto resp = HttpResponse::newHttpJsonResponse(result);
resp->setStatusCode(k201Created); // 201 Created
// Location 头告知客户端新资源的 URI
resp->addHeader("Location", "/api/items/" + std::to_string(id));
callback(resp);
}
// GET /api/items/{id} — 按 ID 获取单个 Item
void getItemById(const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback,
int id) {
// 用 Lambda 在锁内查找,锁的范围尽量小
auto item = [this, id]() -> std::optional<std::pair<int, std::string>> {
std::lock_guard lock(storage_mutex_);
if (const auto it = items_.find(id); it != items_.end()) {
return {{it->first, it->second}};
}
return std::nullopt; // 未找到
}();
if (!item.has_value()) {
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k404NotFound); // 404 Not Found
callback(resp);
return;
}
Json::Value json;
json["id"] = item->first;
json["name"] = item->second;
callback(HttpResponse::newHttpJsonResponse(json));
}
// PUT /api/items/{id} — 更新 Item
void updateItem(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
int id) {
auto json = req->getJsonObject();
if (!json || !json->isMember("name")) {
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("No name");
callback(resp);
return;
}
auto item = [this, id, json]() -> std::optional<std::pair<int, std::string>> {
std::lock_guard lock(storage_mutex_);
if (const auto it = items_.find(id); it != items_.end()) {
it->second = (*json)["name"].asString(); // 原地更新
return {{it->first, it->second}};
}
return std::nullopt;
}();
if (!item.has_value()) {
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k404NotFound);
callback(resp);
return;
}
Json::Value result;
result["id"] = item->first;
result["name"] = item->second;
callback(HttpResponse::newHttpJsonResponse(result));
}
// DELETE /api/items/{id} — 删除 Item
void deleteItem(const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback,
int id) {
auto item = [this, id]() -> std::optional<int> {
std::lock_guard lock(storage_mutex_);
if (const auto it = items_.find(id); it != items_.end()) {
items_.erase(it); // 从存储中删除
return {id};
}
return std::nullopt;
}();
auto resp = HttpResponse::newHttpResponse();
if (!item.has_value()) {
resp->setStatusCode(k404NotFound);
} else {
// 204 No Content:成功删除,没有响应体
resp->setStatusCode(k204NoContent);
}
callback(resp);
}
private:
std::unordered_map<int, std::string> items_; // 内存数据存储
int next_id_ = 0; // 自增 ID 计数器
std::mutex storage_mutex_; // 保护共享数据的互斥锁
};
int main() {
app().registerController(std::make_shared<ItemsController>());
app().addListener("0.0.0.0", 8080);
app().run();
return 0;
}
4.5 HATEOAS:超媒体驱动状态
核心思想:每个响应中不仅包含数据,还包含指向相关资源和可用操作的链接。客户端通过这些链接导航,而不是硬编码 URL。
ASCII 图:HATEOAS 响应示例(库存为 8 时 vs 库存为 0 时)
库存充足(stock=8): 库存耗尽(stock=0):
{ {
"itemId": 8, "itemId": 8,
"stock": 8, "stock": 0,
"links": [ "links": [
GET item/8, GET items/8,
POST item/8, POST items/8,
POST item/8/increaseStock, POST items/8/increaseStock
POST item/8/decreaseStock // decreaseStock 消失了!
] ]
} }
↑ 根据状态动态改变可用操作
4.6 GraphQL vs REST
ASCII 图:REST 需要多次请求 vs GraphQL 一次搞定
REST: GraphQL:
GET /users/1 → 用户数据 POST /graphql
GET /users/1/posts → 帖子列表 {
GET /posts/5/comments → 评论 user(id:1) {
name
三次请求 posts {
title
comments { text }
}
}
}
一次请求,精确获取所需字段
| 对比项 | REST | GraphQL |
|---|---|---|
| 端点数量 | 多个(每个资源一个) | 单一端点 |
| 数据获取 | 固定结构,可能过多或过少 | 客户端自定义,精确 |
| 缓存 | HTTP 缓存天然支持 | 缓存复杂,需额外方案 |
| 学习曲线 | 低 | 高(需学 SDL、Resolver) |
| 安全性 | 标准 HTTP 安全机制 | 需自行实现字段级权限 |
| 适合场景 | 简单资源、标准 CRUD | 复杂关联数据、灵活查询 |
GraphQL 操作类型:
- Query:只读查询
- Mutation:创建/修改数据
- Subscription:实时推送(通常基于 WebSocket)
五、远程过程调用(RPC)
5.1 为什么需要 RPC?
REST 和 Web 服务有大量的数据转换开销(HTTP 头、JSON 解析),对于高性能微服务来说代价太高。RPC 提供更轻量的替代方案。
5.2 Apache Thrift
ASCII 图:Thrift 工作流程
service.thrift (IDL)
│
▼
thrift 编译器
│
├──► gen-cpp/Service.h(生成的接口类)
│
├──► Server:实现 ServiceIf 接口
│
└──► Client:使用 ServiceClient 调用
Thrift 服务定义(service.thrift):
namespace cpp Service
service Service {
string sayHello(1: string name)
}
Thrift 服务端完整 C++ 示例:
// thrift_server.cpp - Apache Thrift 服务端
// 编译:需要先运行 thrift --gen cpp service.thrift
// g++ thrift_server.cpp gen-cpp/Service.cpp -o thrift_server -lthrift
#include "gen-cpp/Service.h" // 由 thrift 编译器生成的接口头文件
#include <thrift/protocol/TBinaryProtocol.h> // 二进制序列化协议
#include <thrift/server/TSimpleServer.h> // 单线程简单服务器
#include <thrift/transport/TServerSocket.h> // TCP 服务端 Socket
#include <thrift/transport/TBufferedTransport.h> // 带缓冲的传输层
#include <memory>
#include <iostream>
#include <string>
using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;
using namespace Service;
// 实现由 Thrift 生成的纯虚接口 ServiceIf
class ServiceHandler final : public ServiceIf {
public:
// sayHello 的实现:通过引用参数 _return 返回结果
// (这是 Thrift C++ 生成代码的固定模式)
void sayHello(std::string& _return, const std::string& name) {
std::cout << "Received: " << name << std::endl;
_return = "Trust me " + name; // 将返回值写入引用参数
}
};
int main() {
// 创建服务处理器(业务逻辑实现)
std::shared_ptr<ServiceHandler> handler(new ServiceHandler());
// 创建请求处理器(由生成代码提供,负责协议解析和调用分发)
std::shared_ptr<TProcessor> processor(new ServiceProcessor(handler));
// 创建 TCP 服务器 Socket,监听 9090 端口
std::shared_ptr<TServerTransport> serverTransport(new TServerSocket(9090));
// 使用缓冲传输提升效率
std::shared_ptr<TTransportFactory> transportFactory(
new TBufferedTransportFactory());
// 使用二进制协议序列化(比文本协议更快、更小)
std::shared_ptr<TProtocolFactory> protocolFactory(
new TBinaryProtocolFactory());
// 组装服务器并启动(阻塞)
TSimpleServer server(processor, serverTransport,
transportFactory, protocolFactory);
std::cout << "Thrift server listening on port 9090..." << std::endl;
server.serve(); // 进入事件循环,等待客户端连接
return 0;
}
Thrift 客户端完整 C++ 示例:
// thrift_client.cpp - Apache Thrift 客户端
// g++ thrift_client.cpp gen-cpp/Service.cpp -o thrift_client -lthrift
#include "gen-cpp/Service.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h> // TCP 客户端 Socket
#include <thrift/transport/TBufferedTransport.h>
#include <memory>
#include <iostream>
#include <string>
using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace Service;
int main() {
// 1. 创建到服务器的 TCP Socket(localhost:9090)
std::shared_ptr<TTransport> socket(new TSocket("localhost", 9090));
// 2. 包装成带缓冲的传输层
std::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
// 3. 指定通信协议(必须与服务端一致:都用二进制协议)
std::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));
// 4. 创建由 Thrift 生成的客户端 Stub(代理对象)
ServiceClient client(protocol);
try {
// 5. 打开连接
transport->open();
// 6. 发起 RPC 调用(看起来像普通函数调用)
std::string return_;
// 注意:return_ 在调用前的值不会传给服务器,只用于接收返回值
client.sayHello(return_, "I'm an engineer");
std::cout << return_ << std::endl; // 输出:"Trust me I'm an engineer"
// 7. 关闭连接
transport->close();
} catch (TException& ex) {
std::cerr << "Error: " << ex.what() << std::endl;
}
return 0;
}
5.3 gRPC + Protobuf
gRPC 是目前最流行的高性能 RPC 框架,由 Google 开发。
核心技术栈:
- 传输层:HTTP/2(支持多路复用、流式传输)
- 序列化:Protocol Buffers(二进制,比 JSON 更小更快)
ASCII 图:REST vs gRPC 通信对比
REST(文本,HTTP/1.1): gRPC(二进制,HTTP/2):
请求1 ──► 服务器 请求1 ──┐
等待... 请求2 ──┼──► 服务器(多路复用)
响应1 ◄── 服务器 请求3 ──┘
请求2 ──► 服务器 响应3 ◄──┐
等待... 响应1 ◄──┼── 服务器(乱序响应)
响应2 ◄── 服务器 响应2 ◄──┘
(串行,有队头阻塞问题) (并行,无队头阻塞)
Protobuf 服务定义(service.proto):
syntax = "proto3";
// 定义 Greeter 服务,包含一个 Greet RPC 方法
service Greeter {
rpc Greet(GreetRequest) returns (GreetResponse);
}
// 请求消息结构:包含一个字符串字段 name(字段编号=1)
message GreetRequest {
string name = 1;
}
// 响应消息结构:包含一个字符串字段 reply(字段编号=1)
message GreetResponse {
string reply = 1;
}
gRPC 服务端完整 C++ 示例:
// grpc_server.cpp - gRPC 同步服务端
// 先用 protoc 生成:protoc --grpc_out=. --cpp_out=. service.proto
// 编译:g++ grpc_server.cpp service.grpc.pb.cc service.pb.cc -o grpc_server -lgrpc++ -lprotobuf
#include <grpcpp/grpcpp.h> // gRPC C++ 库
#include <string>
#include <iostream>
#include "service.grpc.pb.h" // 由 protoc 生成的服务接口头文件
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using grpc::StatusCode;
// 继承生成的 Greeter::Service,实现具体 RPC 方法
class GreeterImpl : public Greeter::Service {
// 重写 Greet 方法:接收请求,填充响应
Status Greet(ServerContext* context,
const GreetRequest* request,
GreetResponse* reply) override {
auto name = request->name(); // 从请求中提取 name 字段
// 输入验证:name 不能为空
if (name.empty()) {
return Status(StatusCode::INVALID_ARGUMENT, "name is empty");
}
// 设置响应的 reply 字段
reply->set_reply("Get over here! " + name);
return Status::OK;
}
};
int main() {
std::string address("localhost:50000");
GreeterImpl service;
// 使用 ServerBuilder 配置并构建 gRPC 服务器
ServerBuilder builder;
// 添加监听端口(InsecureServerCredentials = 不加密,仅用于开发)
builder.AddListeningPort(address, grpc::InsecureServerCredentials());
// 注册服务实现
builder.RegisterService(&service);
// 构建并启动服务器
auto server(builder.BuildAndStart());
std::cout << "gRPC server listening on " << address << std::endl;
server->Wait(); // 阻塞,等待请求
return 0;
}
gRPC 客户端完整 C++ 示例:
// grpc_client.cpp - gRPC 同步客户端
// 编译:g++ grpc_client.cpp service.grpc.pb.cc service.pb.cc -o grpc_client -lgrpc++ -lprotobuf
#include <grpcpp/grpcpp.h>
#include <string>
#include <iostream>
#include "service.grpc.pb.h"
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
int main() {
std::string address("localhost:50000");
// 1. 创建 Channel(通信通道),指定服务器地址和凭据
auto channel = grpc::CreateChannel(address,
grpc::InsecureChannelCredentials());
// 2. 创建 Stub(客户端存根),是发起 RPC 的代理对象
// NewStub 是由 protoc 生成的工厂函数
auto stub = Greeter::NewStub(channel);
// 3. 构造请求消息
GreetRequest request;
request.set_name("Neosapien"); // 设置 name 字段
// 4. 准备响应对象和上下文
GreetResponse reply;
ClientContext context; // 可设置超时、元数据、取消信号等
// 5. 发起同步 RPC 调用(阻塞直到服务器响应)
Status status = stub->Greet(&context, request, &reply);
// 6. 检查结果
if (status.ok()) {
std::cout << reply.reply() << '\n'; // 输出:"Get over here! Neosapien"
} else {
std::cerr << "Error: " << status.error_code()
<< " - " << status.error_message() << '\n';
}
return 0;
}
5.4 各种通信技术横向对比
ASCII 图:通信技术选型决策树
需要 Web 标准兼容?
│
┌──────┴──────┐
Yes No
│ │
数据复杂度? 性能要求极高?
┌─────┴──────┐ ┌────┴─────┐
简单 复杂 Yes No
│ │ │ │
REST GraphQL gRPC Thrift
│
实时推送?
┌─────┴──────┐
Yes No
│ │
GraphQL GraphQL
Subscription Query/Mutation
| 技术 | 协议 | 数据格式 | 性能 | 适用场景 |
|---|---|---|---|---|
| SOAP | HTTP | XML | 低 | 企业遗留系统 |
| JSON-RPC | HTTP | JSON | 中 | 简单 RPC |
| REST | HTTP | JSON/XML | 中 | 通用 API,最广泛 |
| GraphQL | HTTP | JSON | 中 | 复杂关联查询 |
| gRPC | HTTP/2 | Protobuf(二进制) | 高 | 微服务内部通信 |
| Thrift | TCP | 多种(二进制/JSON) | 高 | 跨语言内部通信 |
| MQTT | TCP | 二进制 | 高(低功耗) | IoT、嵌入式 |
| ZeroMQ | 多种 | 自定义 | 极高 | 超低延迟场景 |
六、API 描述语言
6.1 OpenAPI(原名 Swagger)
用 YAML 或 JSON 描述 REST API,支持自动生成文档、代码、测试用例。
OpenAPI 规范示例(打招呼服务):
openapi: 3.0.4
info:
title: Greet API
version: 2.0.0
paths:
/customer/v1:
get:
operationId: greet
summary: Greet customers
parameters:
- name: name
in: query # 参数在查询字符串中
description: Customer name
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
status:
type: integer
response:
type: string
example: # 响应示例
status: 200
response: Hi, Scott
'400':
description: Bad Request
content:
application/json:
schema:
type: object
properties:
status:
type: integer
response:
type: string
example:
status: 400
response: No name
6.2 API 优先设计原则
ASCII 图:API 优先(API-First)开发流程
┌─────────────────────────────────────────────────┐
│ API 规范文档 │
│ (OpenAPI / AsyncAPI / Proto) │
└──────────────────────┬──────────────────────────┘
│
┌──────────────┼───────────────┐
▼ ▼ ▼
服务端代码生成 客户端SDK生成 自动化测试生成
(server stub) (client lib) (test cases)
│ │ │
└──────────────┼───────────────┘
▼
并行开发,速度更快
| 规范 | 适用场景 | 协议支持 |
|---|---|---|
| OpenAPI | 同步 HTTP API(REST) | HTTP |
| AsyncAPI | 异步消息驱动 API | AMQP, MQTT, Kafka, WebSocket 等 |
| Protobuf(.proto) | gRPC 服务 | HTTP/2 |
| WSDL | SOAP 服务(老系统) | HTTP, SMTP 等 |
七、全章知识总结
ASCII 图:服务间通信全景
服务间通信(ISC)
│
├── 低开销消息系统(资源受限环境)
│ ├── MQTT(发布-订阅,有 Broker,适合 IoT)
│ └── ZeroMQ(无 Broker,点对点,超低延迟)
│
├── 有 Broker 消息系统(高吞吐分布式)
│ ├── Apache Kafka(流处理,持久化日志,水平扩展)
│ └── RabbitMQ(灵活路由,任务队列,垂直扩展)
│
├── Web 服务(基于 HTTP,易调试)
│ ├── XML 系列:SOAP(企业遗留),XML-RPC
│ └── JSON 系列
│ ├── JSON-RPC(轻量 RPC)
│ ├── REST(最流行,资源导向)
│ │ └── HATEOAS(超媒体驱动)
│ └── GraphQL(查询语言,复杂数据)
│
└── RPC(高性能,内部通信)
├── Apache Thrift(跨语言,多传输方式)
├── gRPC(HTTP/2 + Protobuf,谷歌出品)
└── RabbitMQ RPC(请求-回复模式)
八、本章问题解答
Q1:不同交互类型的适用范围是什么?
- 一对一:两个服务紧密依赖,需实时数据交换(但注意紧耦合风险)
- 一对多:广播场景,需要多个服务同时响应同一事件
- 多对一:集中处理共享资源或功能(如认证中心)
- 多对多:复杂动态系统,需容错设计
Q2:消息队列的典型使用场景有哪些?
金融操作处理、车队监控、物流数据采集、传感器数据处理、订单履行、任务队列。
Q3:JSON 相比 XML 的优点有哪些? - 格式更紧凑、体积更小
- 解析速度更快、内存占用更低
- 语法简单,人更容易读写
- 直接对应大多数语言的原生数据类型
Q4:REST 如何建立在 Web 标准之上?
REST 完全依赖 HTTP 动词(GET/POST/PUT/DELETE)表达操作语义,用 URI 标识资源,响应支持 HTTP 缓存头(Cache-Control, ETag),可以复用现有的代理、负载均衡器等 Web 基础设施。
Q5:为什么 REST 不一定是连接微服务的最佳选择? - HTTP/JSON 有大量序列化/反序列化开销
- 每次请求都需要 HTTP 头的额外开销
- 单个请求的延迟比 gRPC 等二进制 RPC 高
- 当微服务间通信量极大时,这些开销会成为性能瓶颈
- 对于内部通信,gRPC(二进制 + HTTP/2 多路复用)是更高效的选择
第十六章:容器(Containers)— 从零理解
本章学习路线图
一、容器是什么?从历史讲起
1.1 容器的起源
容器不是新技术,其历史可以追溯到 1979 年的 Unix chroot 机制——“把一个进程的根目录切换到另一个目录,使它看不到外面的文件系统”。
ASCII 图:容器隔离的历史演进
1979 chroot(Unix)── 最原始的文件系统隔离
│
1999 FreeBSD Jails ── 进程、网络、用户隔离
│
2004 Solaris Zones ── 完整的操作系统级隔离
│
2008 LXC(Linux Containers)── Linux 内核原生支持
│
2013 Docker ────────── 应用容器爆发,成为行业标准
│
2014 Kubernetes ──────── 容器编排,管理大规模集群
│
至今 Podman / containerd / OCI 标准生态
1.2 容器、虚拟机与操作系统容器的区别
ASCII 图:三种隔离方式对比
虚拟机(VM): 操作系统容器: 应用容器(Docker):
┌──────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ App A │ App B │ │ App A │ App B │ │ App A │
├─────────┼────────────│ ├─────────┼────────────│ │(单进程) │
│ Guest │ Guest │ │ User │ User │ ├─────────────────────│
│ OS A │ OS B │ │ Space A │ Space B │ │ 最小依赖库 │
├─────────┴────────────│ ├─────────────────────┤ ├─────────────────────│
│ Hypervisor(虚拟层)│ │ 共享宿主机内核 │ │ 共享宿主机内核 │
├──────────────────────│ ├─────────────────────┤ ├─────────────────────│
│ 宿主机硬件 │ │ 宿主机硬件 │ │ 宿主机硬件 │
└──────────────────────┘ └─────────────────────┘ └─────────────────────┘
隔离级别:硬件级(最强) 隔离级别:用户空间 隔离级别:进程级(最轻)
资源开销:大 资源开销:中 资源开销:小
启动速度:慢(几十秒) 启动速度:中 启动速度:快(毫秒级)
| 对比项 | 虚拟机(VM) | 操作系统容器(LXC) | 应用容器(Docker) |
|---|---|---|---|
| 内核共享 | 否(各有独立内核) | 是 | 是 |
| 隔离粒度 | 硬件级 | 用户空间级 | 进程级 |
| 资源开销 | 大 | 中 | 小 |
| 启动速度 | 慢 | 中 | 毫秒级 |
| 镜像大小 | 几 GB | 几百 MB | 几 MB~几百 MB |
| 安全性 | 高 | 中 | 需额外配置 |
| 适合场景 | 完整系统隔离 | 多租户服务器 | 微服务 / CI/CD |
1.3 应用容器:每个容器只跑一个进程
为什么只跑一个进程?
- 违反"关注点分离"原则:一个容器同时跑数据库+Web服务,出问题不知道是谁的锅
- 日志混乱:不同进程的日志混在一起,难以排查
- 独立扩展:一个容器只跑一件事,才能精准扩缩容(Web 慢了多加 Web 容器,不影响数据库)
- 故障隔离:一个进程崩溃只影响这一个容器,不会拖垮整个系统
取代传统系统服务的方式: - 不用
syslog记录日志 → 直接写到标准输出(stdout),由容器运行时收集 - 不用
systemd/init管理生命周期 → 由容器运行时(Docker/Kubernetes)管理
二、微服务与容器:天作之合
2.1 十二因素方法论(Twelve-Factor App)
由 Heroku 开发者总结的云原生应用最佳实践,容器和微服务都与之高度吻合:
ASCII 图:十二因素全览
┌──────────────────────────────────────────────────────────────────┐
│ 十二因素方法论 │
├──────────────────────────────────────────────────────────────────┤
│ 1. 代码库 - 一份代码,多份部署(Git 版本控制) │
│ 2. 依赖 - 显式声明所有依赖,不依赖系统工具(容器解决此问题)│
│ 3. 配置 - 配置存在环境变量中,严格与代码分离 │
│ 4. 后端服务 - 数据库/缓存等视为"附加资源",随时可替换 │
│ 5. 构建/发布/运行 - 三阶段严格分离(CI/CD 流水线) │
│ 6. 进程 - 无状态进程,持久数据存外部服务 │
│ 7. 端口绑定 - 服务通过端口对外暴露,完全自包含 │
│ 8. 并发 - 通过增加进程数量(水平扩展)实现并发 │
│ 9. 易处理性 - 快速启动,优雅关闭,设计为可随时崩溃重启 │
│ 10. 开发/生产一致 - 开发、测试、生产环境尽量相同(容器保证此点) │
│ 11. 日志 - 日志作为事件流写入 stdout,不写文件 │
│ 12. 管理进程 - 数据库迁移等管理任务作为一次性进程运行 │
└──────────────────────────────────────────────────────────────────┘
三、容器的优缺点
3.1 优点
| 优点 | 说明 |
|---|---|
| 轻量级 | 比虚拟机少一个 OS 内核,启动更快,占用更少资源 |
| 环境一致性 | "在我机器上能跑"的问题消失,开发/CI/生产用同一镜像 |
| 隔离性 | 每个容器有独立的进程/用户/网络命名空间 |
| 可移植性 | 构建一次,到处运行(符合 OCI 标准的平台都能跑) |
| 资源控制 | 通过 cgroups 精确限制 CPU、内存、I/O、网络带宽 |
| 标准化 | 统一的构建、分发、运行接口,便于自动化 |
Linux 内核实现容器的三大机制
ASCII 图:Linux 容器三大底层机制
┌─────────────────────────────────────────────────────┐
│ Linux 容器技术栈 │
├────────────────┬────────────────┬────────────────────┤
│ Namespaces │ cgroups │ 安全增强 │
│(命名空间) │(控制组) │ │
├────────────────┼────────────────┼────────────────────┤
│ 隔离"看到什么" │ 限制"用多少" │ 控制"能做什么" │
│ │ │ │
│ - 文件系统 │ - CPU 时间 │ - SELinux │
│ - 网络接口 │ - 内存用量 │ - AppArmor │
│ - 进程 ID │ - I/O 带宽 │ - seccomp │
│ - 用户/组 ID │ - 网络带宽 │ - Linux 能力机制 │
│ - 主机名 │ │ │
└────────────────┴────────────────┴────────────────────┘
3.2 缺点
| 缺点 | 说明 |
|---|---|
| 非微服务应用难迁移 | 多进程、有状态、不走 TCP/IP 的老应用改造成本高 |
| 持久化存储复杂 | 容器理想上无状态,数据库等有状态服务需要外挂存储 |
| 需遵循容器规范 | 日志、配置、端口等都要按容器方式改造 |
| Windows 容器弱势 | 性能差、安全性弱、公共镜像少,生态不成熟 |
| 调试难度增加 | 精简容器里没有 shell,需要 gdb-server 等远程调试 |
四、Docker 架构详解
ASCII 图:Docker 完整架构栈
用户
│
│ docker build / run / push ...
▼
docker-cli(命令行工具)
│
│ REST API
▼
dockerd(Docker 守护进程)
│
│
▼
containerd(高级容器运行时)
│
├──► runc(默认,轻量 OCI 运行时,Go 编写)
├──► Wasmtime(WebAssembly 运行时,Rust 编写)
├──► GVisor / runsc(沙箱,系统调用拦截,增强安全)
└──► Kata Containers(轻量虚拟机,最强隔离,Rust 编写)
五、容器镜像 vs 容器实例
就像程序文件(
.exe)和运行中的进程的关系:
| 概念 | 类比 | 特点 |
|---|---|---|
| 容器镜像(Image) | 可执行文件 / 安装包 | 静态,分层存储,只读 |
| 容器(Container) | 运行中的进程 | 动态,从镜像创建,有读写层 |
联合文件系统(Union File System)
ASCII 图:Docker 分层文件系统(UFS)
┌─────────────────────────────┐ ← 容器读写层(运行时数据)
│ 容器层(读写) │ 只有这层可写
├─────────────────────────────┤
│ 镜像层 N(RUN 指令结果) │
├─────────────────────────────┤
│ 镜像层 N-1 │ ← 这些都是只读层
├─────────────────────────────┤ 每个 Dockerfile 指令生成一层
│ ... │
├─────────────────────────────┤
│ 基础镜像层(ubuntu:questing)│
├─────────────────────────────┤
│ bootfs(引导文件系统) │
└─────────────────────────────┘
每一层都有唯一的哈希摘要(digest),相同内容的层在不同镜像之间共享复用,避免重复下载。
六、构建容器镜像
6.1 方法一:标准单阶段构建(不推荐用于生产)
直接在容器里编译 C++ 代码:
Dockerfile(单阶段):
# 基础镜像:Ubuntu questing(25.10)
FROM ubuntu:questing
# 在同一个 RUN 指令里安装所有工具(减少层数)
# DEBIAN_FRONTEND=noninteractive 避免 apt 交互式提问卡住
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
build-essential gcc cmake git python3-pip python3-venv && \
python3 -mvenv /opt/venv && . /opt/venv/bin/activate && \
pip3 install conan && conan profile detect
# 将当前目录的所有文件复制到容器的 /root/src
ADD . /root/src
# 设置工作目录
WORKDIR /root/src
# 编译项目
RUN mkdir -p build && cd build && \
. /opt/venv/bin/activate && \
conan install .. --build=missing \
-s:a build_type=Release \
-s:a compiler.cppstd=gnu20 -of . && \
cmake .. -DCMAKE_BUILD_TYPE=Release && \
cmake --build . && cmake --install .
# 容器启动时运行的默认命令
CMD ["/usr/local/bin/customer"]
问题:最终镜像里包含编译器、源代码、Conan 等大量运行时不需要的东西,镜像约 3.57 GB。
6.2 方法二:只拷贝二进制(主机编译)
在宿主机编译好,只把可执行文件复制进容器:
FROM ubuntu:questing
# 只复制已编译好的二进制文件
COPY customer /bin/customer
# 启动时运行 customer
CMD ["/bin/customer"]
问题:需要宿主机和容器操作系统一致,否则动态库不匹配会报错。
6.3 方法三:多阶段构建(推荐)
用两个阶段:第一阶段编译,第二阶段只取二进制:
# ===== 第一阶段:构建环境 =====
# 给这个阶段命名为 builder,方便后续引用
FROM ubuntu:questing AS builder
# 安装编译工具链
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
build-essential gcc cmake ninja-build git python3-pip python3-venv
# 创建 Python 虚拟环境并安装 Conan 包管理器
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip3 install conan==2.* && conan profile detect
# 复制源码
ADD . /root/src
WORKDIR /root/src
# 编译项目
RUN mkdir -p build && cd build && \
conan install .. --build=missing \
-s:a build_type=Release \
-s:a compiler.cppstd=gnu20 -of . && \
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release && \
cmake --build . && cmake --install .
# ===== 第二阶段:运行环境(只有这个阶段会出现在最终镜像里)=====
FROM ubuntu:questing
# 从第一阶段(builder)复制编译好的二进制,不复制工具链和源码
COPY --from=builder /root/src/build/bin/customer /bin/customer
# 设置容器启动命令
CMD ["/bin/customer"]
效果对比:
| 构建方式 | 镜像大小 | 包含内容 |
|---|---|---|
| 单阶段(含编译器) | 约 3.57 GB | 源码 + 编译器 + 依赖 + 二进制 |
| 多阶段 | 约 137 MB | 基础 OS(77MB)+ 二进制 |
| scratch(纯静态) | 约 17 MB | 只有静态链接二进制 |
6.4 极简镜像:从 scratch 开始
FROM scratch
# 只有一个静态链接的二进制
COPY customer /bin/customer
CMD ["/bin/customer"]
注意:这种方式没有 shell,无法进入容器调试,但安全性最高。
七、镜像命名与分发
镜像名称的三要素
docker.io / tradefair/merchant : v2.0.3
│ │ │
注册表 镜像名称 标签(版本)
(默认可省略)
常用容器注册表
| 注册表 | 特点 | 适合场景 |
|---|---|---|
| Docker Hub(docker.io) | 最流行,公有/私有都支持 | 通用,开源项目 |
| GitHub Packages | 与 GitHub CI/CD 深度集成 | 已用 GitHub 的团队 |
| GitLab Container Registry | 与 GitLab CI/CD 集成 | 已用 GitLab 的团队 |
| Amazon ECR | 与 AWS 深度集成 | AWS 用户 |
| Azure Container Registry | 与 Azure 集成 | Azure 用户 |
| Google Artifact Registry | 与 GCP 集成 | GCP 用户 |
| Harbor | 开源自托管 | 私有化部署 |
八、多架构镜像
为什么需要多架构?
IoT、嵌入式、边缘计算设备往往是 ARM 架构,服务器多是 x86/AMD64,需要同一个镜像标签在两种平台都能正确运行。
ASCII 图:多架构镜像工作原理
docker pull tradefair/merchant:v2.0.3
│
▼
[manifest-tool 生成的 manifest]
│
┌─────┴─────────────────────────┐
│ │
▼ ▼
merchant:v2.0.3-amd64 merchant:v2.0.3-arm64
(x86 服务器拉这个) (ARM 设备拉这个)
manifest-tool 配置文件示例(YAML):
# 定义一个多平台镜像,包含三种架构
image: tradefair/merchant:v2.0.3
manifests:
- image: tradefair/merchant:v2.0.3-amd64 # x86_64 架构
platform:
architecture: amd64
os: linux
- image: tradefair/merchant:v2.0.3-arm64 # ARM 64 位(如树莓派 4、苹果 M 系列)
platform:
architecture: arm64
os: linux
- image: tradefair/merchant:v2.0.3-riscv64 # RISC-V 架构(新兴开源 CPU)
platform:
architecture: riscv64
os: linux
Docker buildx 跨平台构建命令:
# 1. 创建两台不同架构机器的 Docker 构建上下文
docker context create \
--docker host=ssh://docker-user@host1.domifair.org \
--description="Remote engine amd64" \
node-amd64
docker context create \
--docker host=ssh://docker-user@host2.domifair.org \
--description="Remote engine arm64" \
node-arm64
# 2. 创建多节点的 buildx 构建器
docker buildx create --use --name mybuild node-amd64
docker buildx create --append --name mybuild node-arm64
# 3. 同时为两种架构构建镜像
docker buildx build --platform linux/amd64,linux/arm64 .
九、替代构建工具
9.1 Buildah(无 Daemon,支持 rootless)
ASCII 图:Buildah + Podman + Skopeo 生态
┌──────────────────────────────────────────────────┐
│ Docker 替代方案三件套 │
├────────────┬─────────────────┬───────────────────┤
│ Buildah │ Podman │ Skopeo │
├────────────┼─────────────────┼───────────────────┤
│ 构建镜像 │ 运行/管理容器 │ 操作/传输镜像 │
│ (替代 │ (替代 │ (镜像检查/签名/ │
│ docker │ docker run) │ 跨仓库传输) │
│ build) │ │ │
└────────────┴─────────────────┴───────────────────┘
共同底层:Linux Namespaces + cgroups + SELinux
用 Buildah Shell 脚本替代 Dockerfile:
#!/bin/sh
# buildah_build.sh - 用 Buildah 脚本构建镜像(等价于 Dockerfile)
# 1. 基于 ubuntu:questing 创建一个新容器,返回容器 ID
ctr=$(buildah from ubuntu:questing)
# 2. 在容器中执行命令(安装编译工具)
buildah run $ctr -- /bin/sh -c 'apt update && apt install -y build-essential gcc'
# 3. 设置镜像元数据:容器启动时运行 gcc
buildah config --cmd '/usr/bin/gcc' "$ctr"
# 4. 将容器状态提交为镜像,命名为 tradefair-gcc
buildah commit "$ctr" tradefair-gcc
# 5. 删除临时容器
buildah rm "$ctr"
Buildah vs Docker 对比:
| 特性 | Docker | Buildah |
|---|---|---|
| 是否需要 Daemon | 是(dockerd) | 否(daemonless) |
| 是否需要 root | 否(但 dockerd 需要) | 否(完全 rootless) |
| 与 Kubernetes 集成 | 好 | 一般 |
| 支持 Docker Compose | 是 | 否 |
| 挂载容器文件系统到宿主机 | 否 | 是(特有功能) |
9.2 ansible-bender(用 Ansible 写构建逻辑)
# 等价于之前的 Dockerfile,但用 Ansible Playbook 语法
# 注意:基础镜像需要有 Python 解释器(ubuntu 默认没有,所以改用 python:3)
- name: Container image with ansible-bender
hosts: all
vars:
ansible_bender:
base_image: python:3 # 基础镜像(需含 Python)
target_image:
name: tradefair-gcc # 输出镜像名
cmd: /usr/bin/gcc # 启动命令
tasks:
- name: Install Apt packages
apt:
pkg:
- build-essential # C/C++ 编译工具集
- gcc # GCC 编译器
十、CMake 集成 Docker
10.1 CMake 生成 Dockerfile
# CMakeLists.txt 片段
# configure_file 会将 Dockerfile.in 中的 @变量@ 替换为 CMake 变量值
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/Dockerfile.in
${PROJECT_BINARY_DIR}/Dockerfile
@ONLY # 只替换 @VAR@ 格式,不替换 ${VAR} 格式
)
Dockerfile.in 模板(CMake 变量占位符):
FROM ubuntu:questing
# @PROJECT_VERSION@ 会被 CMake 替换为实际版本号,如 1.0.0
ADD Customer-@PROJECT_VERSION@-Linux.deb .
# 安装 DEB 包并清理缓存(同一 RUN 减少层大小)
RUN apt update && \
apt -y --no-install-recommends install ./Customer-@PROJECT_VERSION@-Linux.deb && \
apt autoremove -y && apt clean && \
rm -r /var/lib/apt/lists/* Customer-@PROJECT_VERSION@-Linux.deb
# 设置容器入口点(与 CMD 的区别:ENTRYPOINT 参数固定,CMD 可被覆盖)
ENTRYPOINT ["/usr/bin/customer"]
# 声明容器监听的端口(文档用途,不自动映射)
EXPOSE 8080
10.2 CMake 自定义目标构建镜像
# CMakeLists.txt 完整片段
# 1. 查找 Docker 可执行文件
find_program(Docker_EXECUTABLE docker)
if(NOT Docker_EXECUTABLE)
message(WARNING "Docker not found")
endif()
# 2. 创建打包 DEB 的自定义目标
add_custom_target(
customer-deb
COMMENT "Creating Customer DEB package"
# 调用 CPack 生成 DEB 格式安装包
COMMAND ${CMAKE_CPACK_COMMAND} -G DEB
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
VERBATIM # 推荐:让 CMake 正确处理特殊字符
)
# 确保先编译 libcustomer 库,再打包
add_dependencies(customer-deb libcustomer)
# 3. 创建构建 Docker 镜像的自定义目标
add_custom_target(
docker
COMMENT "Preparing Docker image"
COMMAND ${Docker_EXECUTABLE} build ${PROJECT_BINARY_DIR}
-t tradefair/customer:${PROJECT_VERSION} # 版本标签
-t tradefair/customer:latest # latest 标签
VERBATIM
)
# 确保先打包 DEB,再构建 Docker 镜像
add_dependencies(docker customer-deb)
构建命令:
cmake --build . --target docker # 触发:编译 → 打包 DEB → 构建 Docker 镜像
运行容器(compose.yaml):
services:
customer:
image: tradefair/customer:latest
ports:
- "8080:8080" # 宿主机端口:容器端口
docker compose up # 启动服务
# 访问:http://localhost:8080/customer/v1?name=your_name
十一、运行时注意事项
11.1 C 标准库的选择
| 标准库 | 使用场景 | 镜像大小 | 兼容性 | 代表发行版 |
|---|---|---|---|---|
| glibc | 通用服务器 | 较大 | 最好 | Ubuntu / Debian / Fedora |
| musl | 轻量容器 | 很小 | 一般 | Alpine Linux |
| uClibc | 嵌入式设备 | 极小 | 有限 | BusyBox |
Python 镜像大小对比(体现标准库影响):
| 基础系统 | 镜像大小 |
|---|---|
| Debian Bookworm(glibc) | 约 365 MB |
| Debian Slim | 约 43 MB |
| Alpine(musl) | 约 12 MB |
| BusyBox(uClibc) | 740 KB |
| BusyBox(musl) | 840 KB |
11.2 静态编译(配合 scratch 镜像)
# CMake 静态编译选项
rm -rf ./build/ && mkdir build && cd build
cmake .. -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXE_LINKER_FLAGS="-static" \ # 静态链接所有依赖
-DBUILD_SHARED_LIBS=OFF \ # 不生成动态库
-DCMAKE_FIND_LIBRARY_SUFFIXES=".a" # 只查找静态库 .a 文件
cmake --build .
注意:glibc 的 DNS 解析(NSS)和字符集转换(iconv)依赖动态加载,静态链接 glibc 可能出问题。
11.3 替代容器运行时
| 运行时 | 特点 | 适用场景 |
|---|---|---|
| Docker(runc) | 最流行,生态最全 | 通用 |
| Podman + Buildah | 无 Daemon,rootless,支持 FreeBSD | 安全要求高 |
| Kata Containers | 轻量虚拟机隔离,最强安全 | 多租户、高安全 |
| GVisor(runsc) | 系统调用沙箱,增强隔离 | 不信任工作负载 |
| CRI-O / containerd | Kubernetes 集群专用 | K8s 生产环境 |
十二、容器编排
当需要管理数十、数百个容器时,需要**编排器(Orchestrator)**来统一管理。
12.1 Kubernetes(K8s)
Kubernetes 是目前最流行的容器编排平台,由 Google 开发,现由 CNCF 维护。
ASCII 图:Kubernetes 核心架构
┌────────────────────────────────────────────────┐
│ 控制平面(Control Plane) │
│ API Server │ Scheduler │ Controller │ etcd │
└────────────────────────────────────────────────┘
│ │
kubectl apply │
│ ▼
┌────────────────────────────────────────────────┐
│ 工作节点(Worker Node) │
│ ┌─────────────────────────┐ │
│ │ Pod(最小调度单元) │ │
│ │ [容器A] [容器B] ... │ ← 同 Pod 共享网络│
│ └─────────────────────────┘ │
│ kubelet(节点代理) │
└────────────────────────────────────────────────┘
Kubernetes 核心概念对比 Docker:
| Docker 概念 | Kubernetes 对应概念 | 说明 |
|---|---|---|
| 容器(Container) | Pod(包含一或多个容器) | K8s 最小调度单元 |
docker run |
Deployment | 管理 Pod 副本数量 |
| 端口映射 | Service | 提供稳定的访问入口和负载均衡 |
docker-compose |
YAML Manifest | 声明式配置 |
| Docker Swarm | Kubernetes | 编排系统 |
Kubernetes YAML 配置示例(前端 + 后端):
# merchant.yaml - Kubernetes 资源配置文件
# ===== 前端:Nginx 代理 =====
apiVersion: apps/v1
kind: Deployment # 资源类型:部署(管理 Pod)
metadata:
labels:
app: tradefair-front
name: tradefair-front
spec:
selector:
matchLabels:
app: tradefair-front # 选择带此标签的 Pod
template:
metadata:
labels:
app: tradefair-front # Pod 的标签
spec:
containers:
- name: webserver
imagePullPolicy: Always # 每次都从注册表拉最新镜像
image: nginx # 使用官方 Nginx 镜像
ports:
- name: http
containerPort: 80 # 容器内监听 80 端口
protocol: TCP
restartPolicy: Always # Pod 崩溃后自动重启
--- # --- 分隔多个资源定义
# Service:为 Deployment 提供稳定的访问地址
apiVersion: v1
kind: Service
metadata:
labels:
app: tradefair-front
name: tradefair-front
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80 # 流量转发到 Pod 的 80 端口
selector:
app: tradefair-front # 选择带此标签的 Pod 作为后端
type: ClusterIP # 仅集群内部可访问
# ===== 后端:商户微服务 =====
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: tradefair-merchant
name: merchant
spec:
selector:
matchLabels:
app: tradefair-merchant
replicas: 3 # 运行 3 个副本(高可用)
template:
metadata:
labels:
app: tradefair-merchant
spec:
containers:
- name: merchant
imagePullPolicy: Always
image: tradefair/merchant:v2.0.3 # 使用特定版本
ports:
- name: http
containerPort: 8000
protocol: TCP
restartPolicy: Always
apiVersion: v1
kind: Service
metadata:
labels:
app: tradefair-merchant
name: merchant
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8000 # 服务监听 80,转发到 Pod 的 8000
selector:
app: tradefair-merchant
type: ClusterIP
# 应用配置(声明式,K8s 自动达到期望状态)
kubectl apply -f merchant.yaml
12.2 Docker Swarm(Docker 内置编排)
相比 Kubernetes 更简单,直接复用 Docker Compose 格式:
# compose.yaml(Swarm 模式)
services:
web:
image: nginx
ports:
- "80:80"
depends_on:
- merchant # 依赖 merchant 服务先启动
merchant:
image: tradefair/merchant:v2.0.3
deploy:
replicas: 3 # Swarm 特有:部署 3 个副本
ports:
- "8000" # 只声明容器内端口,不固定宿主机端口
docker stack deploy --compose-file compose.yaml tradefair
12.3 Nomad(HashiCorp,非容器专用)
Nomad 不仅支持 Docker 容器,还支持虚拟机(QEMU)、可执行文件等,更通用:
# merchant.nomad - HCL(HashiCorp Configuration Language)格式
job "merchant" {
datacenters = ["dc1"] # 部署到的数据中心
type = "service" # 任务类型:长期运行的服务
group "merchant" {
count = 3 # 运行 3 个实例
task "merchant" {
driver = "docker" # 使用 Docker 任务驱动
config {
image = "tradefair/merchant:v2.0.3"
port_map {
http = 8000 # 容器内端口映射名称
}
}
resources {
network {
port "http" {
static = 8000 # 静态端口号
}
}
}
service {
name = "merchant"
tags = ["tradefair-front", "merchant"]
port = "http"
check {
type = "tcp" # 健康检查:TCP 连接探活
interval = "10s" # 每 10 秒检查一次
timeout = "2s" # 超时 2 秒算失败
}
}
}
}
}
nomad job run merchant.nomad && nomad job run nginx.nomad
12.4 编排方案横向对比
| 编排器 | 复杂度 | 扩展性 | 社区支持 | 适用场景 |
|---|---|---|---|---|
| Docker Swarm | 低 | 低 | 中 | 小团队,熟悉 Docker,快速上手 |
| Nomad | 中 | 中 | 中 | 非容器工作负载混合,HashiCorp 生态 |
| Kubernetes | 高 | 高 | 极高 | 大规模生产环境,云原生 |
| OpenShift | 高 | 高 | 高 | 企业级,Red Hat 生态 |
| AWS ECS | 低 | 中 | AWS 生态 | 已在 AWS 且不需要 K8s 全部功能 |
| AWS Fargate | 极低 | 高 | AWS 生态 | 无需管理底层实例,按用量付费 |
| Azure Service Fabric | 中 | 高 | Azure 生态 | Windows 应用迁移 |
十三、全章知识总结
ASCII 图:容器技术全景
容器技术
│
├── 容器类型
│ ├── 应用容器(Docker / Podman)── 跑单进程,专为微服务
│ └── 操作系统容器(LXC)── 类虚拟机,完整用户空间
│
├── 镜像构建
│ ├── Dockerfile(docker build)── 最流行
│ ├── Buildah Shell 脚本 ── 无 daemon,rootless
│ ├── ansible-bender ── Ansible 生态
│ └── CMake 集成 ── 与 C++ 构建系统结合
│
├── 镜像策略
│ ├── 单阶段(不推荐)── 体积大,含编译器
│ ├── 多阶段(推荐)── 只保留运行时需要的文件
│ └── scratch(极简)── 纯静态二进制,最小最安全
│
├── 运行时
│ ├── Docker(runc)── 默认,最流行
│ ├── Podman ── 无 daemon,rootless
│ ├── Kata Containers ── 虚拟机隔离,安全性最高
│ └── GVisor ── 系统调用沙箱
│
└── 编排
├── 自托管:Kubernetes / Docker Swarm / Nomad / OpenShift
└── 托管:AWS ECS / AWS Fargate / Azure Service Fabric
十四、本章问题解答
Q1:应用容器与操作系统容器有何区别?
应用容器(Docker)只运行单个应用进程及其依赖,不包含完整 OS 服务(如 syslog、cron);操作系统容器(LXC)提供完整的用户空间隔离,更像轻量级虚拟机。
Q2:Unix 系统中早期沙箱机制的例子?
chroot(1979):切换根目录- FreeBSD Jails:进程和网络隔离
- Solaris Zones:完整 OS 级隔离
Q3:容器为何适合微服务?
两者都强调单一职责、无状态、独立部署、可横向扩展,容器为微服务提供了标准化的打包和分发机制,自动扩缩容和自愈不需要了解底层应用细节。
Q4:容器与虚拟机的主要区别?
容器共享宿主内核(进程级隔离),无需额外 OS,启动快,资源占用小;虚拟机有独立内核(硬件级隔离),隔离更彻底,但开销更大。
Q5:应用容器在什么情况下是坏选择? - 应用不是微服务架构(多进程、有状态、依赖内存 IPC)
- 需要大量改造现有代码才能适配容器规范
- Windows 应用(Windows 容器生态弱)
- 需要持久化本地存储的应用
Q6:多平台容器镜像的构建工具? manifest-tool:合并多架构镜像为单一 manifestdocker buildx:Docker 内置多架构构建,支持 QEMU 模拟- Docker 手动标签(不推荐)
Q7:除 Docker 外还有哪些容器运行时?
Podman、Buildah、Kata Containers、GVisor(runsc)、CRI-O、containerd、crun
Q8:常见的容器编排器有哪些?
自托管:Kubernetes、Docker Swarm、Nomad、OpenShift
托管服务:AWS ECS、AWS Fargate、Azure Service Fabric、Azure Kubernetes Service(AKS)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)