28、容器化与部署:Docker基础、Dockerfile编写、Docker Compose编排、Kubernetes简介、CI/CD流水线
说实话,很多C++后端开发者对容器化这件事有点抵触。我当年也一样——总觉得“我的二进制直接跑不就行了?搞什么容器?”直到有一次线上环境因为libc版本不一致,程序崩溃了整整两个小时……嗯,从那以后,我再也不敢小看容器化。
这一章,咱们就聊聊怎么把C++服务装进容器,怎么编排它们,以及怎么搭一套自动化的发布流水线。说白了,就是让你的服务在任何机器上都能“一键跑起来”。
为什么C++服务需要容器化?
你想想看,C++编译出来的二进制,对系统库的依赖有多强?glibc版本、OpenSSL版本、甚至CPU指令集,稍微差一点就崩给你看。容器化解决的就是这个“环境一致性”问题。
我个人习惯把容器理解成一个“轻量级虚拟机”。它共享宿主机的内核,但通过命名空间做了隔离。你的服务在容器里看到的文件系统、网络栈、进程空间,都是独立的。
Docker基础:镜像与容器
Docker有两个核心概念:镜像(Image)和容器(Container)。镜像是一个只读的模板,容器是镜像的运行实例。你可以把镜像理解成C++的类,容器就是实例化的对象。
常用的Docker命令其实没几个,我列一下日常高频使用的:
# 拉取镜像
docker pull ubuntu:22.04
# 查看本地镜像
docker images
# 运行容器(交互模式)
docker run -it --name my_container ubuntu:22.04 /bin/bash
# 后台运行容器
docker run -d --name cpp_server -p 8080:8080 my_cpp_image
# 查看运行中的容器
docker ps
# 进入容器内部
docker exec -it cpp_server /bin/bash
# 停止并删除容器
docker stop cpp_server && docker rm cpp_server
--restart=always 参数,这样服务挂了能自动重启。生产环境尤其有用。
Dockerfile编写:从源码到镜像
Dockerfile就是构建镜像的“配方”。对于C++项目,我一般用多阶段构建(Multi-stage Build)来减小镜像体积。你想想看,编译环境里装了一堆gcc、cmake、依赖库头文件,但运行时根本不需要它们。
下面是一个典型的C++服务Dockerfile:
# 第一阶段:编译
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y \
g++ \
cmake \
libboost-all-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN mkdir build && cd build && \
cmake .. -DCMAKE_BUILD_TYPE=Release && \
make -j$(nproc)
# 第二阶段:运行
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
libboost-system-dev \
libboost-thread-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/build/server .
EXPOSE 8080
CMD ["./server"]
注意看,第一阶段装了完整的编译工具链,第二阶段只复制了编译产物和运行时依赖。这样最终镜像只有几十MB,而不是几个GB。
apt-get upgrade,结果每次构建都拉新包,镜像层数暴增。后来我改成只装必要的包,并且用 --no-install-recommends 减少无用依赖。
Docker Compose编排:多服务管理
当你的系统不止一个服务时——比如C++后端 + Redis + Nginx——手动启动每个容器就太累了。Docker Compose用YAML文件定义多容器应用,一条命令全部拉起。
下面是一个典型的编排文件:
version: '3.8'
services:
cpp_server:
build: .
ports:
- "8080:8080"
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
redis_data:
用 docker-compose up -d 就能启动所有服务。我个人习惯把环境变量写在 .env 文件里,避免硬编码。
Kubernetes简介:生产级容器编排
Docker Compose适合单机场景,但到了生产环境,你需要考虑高可用、自动扩缩容、滚动更新——这时候Kubernetes(K8s)就登场了。
K8s的核心概念有几个:
- Pod: 最小的调度单元,里面可以跑一个或多个容器
- Deployment: 管理Pod的副本数和滚动更新
- Service: 提供稳定的网络入口,负载均衡到Pod
- ConfigMap / Secret: 管理配置和敏感信息
下面是一个简单的Deployment定义:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cpp-server
spec:
replicas: 3
selector:
matchLabels:
app: cpp-server
template:
metadata:
labels:
app: cpp-server
spec:
containers:
- name: server
image: myregistry/cpp-server:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
CI/CD流水线:自动化构建与部署
手动构建镜像、推送到仓库、再登录服务器拉取——这种操作太原始了。CI/CD流水线能帮你自动化这一切。
一个典型的流水线包含几个阶段:
- 代码提交: 开发者push代码到Git仓库
- 自动构建: CI工具(如GitHub Actions、Jenkins)拉取代码,执行编译和单元测试
- 镜像构建: 编译通过后,自动构建Docker镜像并推送到镜像仓库
- 自动部署: 通知K8s集群拉取新镜像,执行滚动更新
下面是一个GitHub Actions的示例片段:
name: Build and Deploy
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t cpp-server:${{ github.sha }} .
- name: Push to registry
run: |
docker tag cpp-server:${{ github.sha }} registry.example.com/cpp-server:latest
docker push registry.example.com/cpp-server:latest
- name: Deploy to K8s
run: kubectl set image deployment/cpp-server server=registry.example.com/cpp-server:latest
latest,结果回滚时根本不知道哪个版本是稳定的。后来我改用Git commit SHA作为tag,配合 git tag 打版本号,回滚时一目了然。
知识体系总览
下面这张图总结了本章的核心脉络,从开发到部署的全流程:
从图中可以看到,整个流程从源码开始,经过Dockerfile构建镜像,再通过编排工具部署到生产环境。左侧的CI/CD流水线贯穿始终,右侧的监控体系保障服务稳定运行。
写在最后
容器化这件事,说白了就是给C++服务穿上一件“防弹衣”。不管底层环境怎么变,你的服务都能稳定运行。我个人从手动部署到全自动CI/CD,走了不少弯路,但一旦搭好这套体系,发布效率提升了不止一个量级。
嗯,这一章的内容就到这里。记住:镜像要小、编排要稳、流水线要自动化。把这三点做到位,你的C++服务就能在云端自由驰骋了。