-
C#网络编程之容器化部署(Docker、Kubernetes)
第74章 容器化部署(Docker、Kubernetes)
一、我踩过的容器化坑:从“镜像1.2GB部署10分钟”到“K8s Pod重启找不到配置文件”
第一次做Docker镜像时,直接用了.NET SDK镜像,打包出来1.2GB,部署到服务器要10分钟——后来用多阶段构建,把SDK镜像用来构建,Runtime镜像用来运行,镜像体积直接降到200MB,部署1分钟搞定!还有一次部署K8s Pod,把配置文件直接打包到镜像里,Pod重启后配置改了但镜像没更新,导致服务用旧配置运行——后来用ConfigMap挂载配置文件,修改ConfigMap后Pod自动更新配置,问题解决!这节我把这些血泪经验揉进去,用大白话vb.net教程C#教程python教程SQL教程access 2010教程讲透Docker和Kubernetes的核心用法,结合代码逐行拆解,拓展底层原理和生产级技巧,让你一次搞定容器化部署!
二、Docker:跨平台容器引擎,把服务和依赖打包成“移动硬盘”
-
核心概念大白话
镜像:像一个只读的移动硬盘,里面装着服务和所有依赖(比如.NET Runtime、配置文件);
容器:像从镜像启动的虚拟机,是镜像的运行实例,可读写,停止后可以删除;
镜像分层:像叠积木,每个层是一个文件,修改只改上层,节省空间(比如SDK镜像有10层,Runtime镜像有5层,多阶段构建可以复用Runtime的层)。 -
Dockerfile逐行讲解:多阶段构建.NET服务
我踩过的坑:用SDK镜像做基础镜像,体积太大
dockerfile
# 错误写法:用SDK镜像做基础镜像,体积1.2GB
# FROM mcr.microsoft.com/dotnet/sdk:8.0
# WORKDIR /app
# COPY . .
# RUN dotnet publish -c Release -o out
# CMD ["dotnet", "out/MyService.dll"]
正确写法:多阶段构建,体积200MB左右
dockerfile
# 第一阶段:构建阶段,用SDK镜像(包含编译器、NuGet等)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src # 设置工作目录,后续命令都在这个目录下执行
COPY ["MyService.csproj", "."] # 先复制csproj文件,利用Docker缓存(如果csproj没改,就不用重新restore)
RUN dotnet restore "MyService.csproj" # 还原NuGet依赖
COPY . . # 复制所有代码文件
RUN dotnet build "MyService.csproj" -c Release -o /app/build # 构建项目,输出到/app/build目录
# 第二阶段:运行阶段,用Runtime镜像(只有.NET Runtime,体积小)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app # 设置工作目录
COPY --from=build /app/build . # 从构建阶段复制构建好的文件到当前镜像
ENTRYPOINT ["dotnet", "MyService.dll"] # 容器启动时执行的命令,跨平台兼容
代码逐行拆解(结合底层原理)
-
多阶段构建的核心优势
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
...
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
COPY --from=build /app/build .
底层原理:第一阶段用SDK镜像构建项目,第二阶段用Runtime镜像运行项目,只把构建好的文件复制到Runtime镜像里,去掉了SDK的编译器、NuGet等无用内容,体积从1.2GB降到200MB;
拓展知识:Runtime镜像分两种,aspnet镜像包含ASP.NET Core Runtime,runtime镜像包含.NET Runtime(适合控制台应用)。
2. 利用Docker缓存加速构建
dockerfile
COPY ["MyService.csproj", "."]
RUN dotnet restore "MyService.csproj"
COPY . .
底层原理:Docker会缓存每个指令的结果,如果csproj文件没改,dotnet restore就会用缓存,不用重新下载NuGet依赖,构建时间从5分钟降到30秒;
生产级技巧:先复制csproj文件,再restore,最后复制所有代码,这样csproj没改时,restore的结果会被缓存。
3. ENTRYPOINT vs CMD
dockerfile
ENTRYPOINT ["dotnet", "MyService.dll"]
ENTRYPOINT:容器启动时必须执行的命令,不能被覆盖(除非用docker run --entrypoint);
CMD:容器启动时的默认命令,可以被docker run的命令覆盖;
推荐用法:用ENTRYPOINT设置固定命令,用CMD设置参数,比如ENTRYPOINT ["dotnet"],CMD ["MyService.dll"],这样可以用docker run myservice MyOtherService.dll运行其他服务。
3. Docker核心命令逐行讲解
-
构建镜像
bash
# -t:给镜像打标签(名称:版本),比如myservice:v1
# .:指定Dockerfile所在的目录(当前目录)
docker build -t myservice:v1 .
底层原理:Docker从当前目录读取Dockerfile,执行每个指令,生成镜像的每个层,最后把层合并成一个镜像;
生产级技巧:用--no-cache参数强制不使用缓存,比如docker build -t myservice:v1 --no-cache .,适合依赖更新后重新构建。
2. 运行容器
bash
# -d:后台运行容器(守护进程模式)
# -p:映射端口,格式是“主机端口:容器端口”,比如把主机的5000端口映射到容器的5000端口
# -v:挂载目录,格式是“主机目录:容器目录”,比如把主机的/config目录挂载到容器的/app/config目录,实现配置文件持久化
# --name:给容器起名字,方便管理
# myservice:v1:用哪个镜像启动容器
docker run -d -p 5000:5000 -v /host/config:/app/config --name myservice myservice:v1
底层原理:Docker从镜像创建容器,给容器分配网络、存储,映射端口和目录,然后执行ENTRYPOINT命令;
生产级技巧:用--rm参数,容器停止后自动删除,比如docker run --rm -p 5000:5000 myservice:v1,适合临时测试。
3. 查看容器状态
bash
# 查看所有运行中的容器
docker ps
# 查看所有容器(包括停止的)
docker ps -a
# 查看容器日志,-f表示实时查看
docker logs -f myservice
# 进入容器的命令行,/bin/bash是Linux的Shell,Windows用cmd
docker exec -it myservice /bin/bash
-
推送镜像到仓库
bash
# 给镜像打标签,格式是“仓库地址/用户名/镜像名:版本”,比如Docker Hub的标签是username/myservice:v1
docker tag myservice:v1 username/myservice:v1
# 登录Docker Hub,输入用户名和密码
docker login
# 推送镜像到仓库
docker push username/myservice:v1
底层原理:Docker把镜像分成多个层,只推送本地没有的层,节省带宽;
生产级技巧:用私有仓库(比如Harbor)存储镜像,避免敏感代码泄露到公共仓库。
4. Docker Compose:多容器部署神器,一键启动服务和依赖
我踩过的坑:手动启动Web服务+Redis,每次都要输两个命令,容易忘参数
docker-compose.yml逐行讲解:Web服务+Redis
yaml
# 指定Docker Compose的版本,推荐用3.8以上
version: '3.8'
# 定义所有服务
services:
# Web服务
myservice:
# 用哪个镜像启动,或者用build指定Dockerfile目录
image: myservice:v1
# build: . # 如果要构建镜像,用这个参数,指定Dockerfile所在目录
ports:
- "5000:5000" # 映射端口
volumes:
- /host/config:/app/config # 挂载配置目录
environment:
- ASPNETCORE_ENVIRONMENT=Production # 设置环境变量
- Redis__ConnectionString=redis:6379 # Redis的连接字符串,用服务名redis(因为Docker Compose会给每个服务分配DNS)
depends_on:
- redis # 依赖Redis服务,启动myservice前先启动redis
restart: always # 容器崩溃或重启后自动重启
# Redis服务
redis:
image: redis:7.0 # 用官方Redis镜像
ports:
- "6379:6379" # 映射Redis端口
volumes:
- redis-data:/data # 挂载Redis数据目录,实现数据持久化
restart: always
# 定义卷(持久化存储)
volumes:
redis-data: # Redis的数据会存在这个卷里,容器删除后数据不会丢失
代码逐行拆解(结合底层原理)
-
服务之间的通信
yaml
environment:
- Redis__ConnectionString=redis:6379
底层原理:Docker Compose会创建一个默认的桥接网络,所有服务都在这个网络里,用服务名(比如redis)作为DNS名称,直接访问服务的端口,不用输IP地址;
拓展知识:用docker network ls查看Docker网络,用docker network inspect myservice_default查看服务的IP地址。
2. 卷(Volumes)持久化存储
yaml
volumes:
redis-data:
底层原理:卷是Docker管理的存储目录,比挂载主机目录更安全,跨平台兼容(Windows、Linux、macOS都支持);
生产级技巧:用命名卷存储数据,不用挂载主机目录,避免主机目录权限问题。
核心命令
bash
# 启动所有服务,-d表示后台运行
docker-compose up -d
# 停止所有服务
docker-compose down
# 查看服务日志
docker-compose logs -f myservice
# 重新构建服务(如果Dockerfile改了)
docker-compose build myservice
三、Kubernetes(K8s):容器编排引擎,管理成千上万的容器
-
核心概念大白话
Pod:K8s的最小部署单位,像一个小盒子,里面装着一个或多个容器(比如Web服务+日志收集容器),共享网络和存储;
Deployment:像Pod的“保姆”,管理Pod的副本数,比如指定3个Pod,挂了一个会自动启动新的;
Service:像Pod的“域名”,给Pod分配一个固定的虚拟IP,不管Pod怎么重启,Service的IP都不变,外部可以通过Service访问Pod;
ConfigMap:像Pod的“配置文件仓库”,把配置文件从镜像里抽出来,挂载到Pod里,修改ConfigMap后Pod自动更新配置;
Secret:像加密的ConfigMap,用来存储敏感数据(比如密码、API密钥),存储时会加密,挂载到Pod里是明文。 -
K8s YAML逐行讲解:Deployment+Service+ConfigMap
我踩过的坑:把配置文件打包到镜像里,修改配置要重新构建镜像 -
ConfigMap:存储配置文件
yaml
# 定义ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: myservice-config # ConfigMap的名字
data:
# 配置文件内容,key是文件名,value是文件内容
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"Redis": {
"ConnectionString": "redis:6379"
}
}
底层原理:ConfigMap存储在K8s的etcd数据库里,挂载到Pod里时会变成文件,Pod可以直接读取;
生产级技巧:用kubectl create configmap myservice-config --from-file=appsettings.json从本地文件创建ConfigMap,不用手动写YAML。
2. Deployment:管理Pod的副本数
yaml
# 定义Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myservice-deployment # Deployment的名字
labels:
app: myservice # 标签,用来和Service关联
spec:
replicas: 3 # 运行3个Pod副本
selector:
matchLabels:
app: myservice # 选择标签为app=myservice的Pod
template:
metadata:
labels:
app: myservice # Pod的标签,要和selector的matchLabels一致
spec:
containers:
- name: myservice # 容器的名字
image: username/myservice:v1 # 用哪个镜像启动容器
ports:
- containerPort: 5000 # 容器的端口
volumeMounts:
- name: config-volume # 挂载的卷名字,要和volumes的name一致
mountPath: /app/appsettings.json # 挂载到容器的哪个路径(覆盖原来的appsettings.json)
subPath: appsettings.json # 用ConfigMap里的哪个文件
resources:
requests:
cpu: "100m" # 每个Pod请求100m CPU(1核=1000m)
memory: "256Mi" # 每个Pod请求256MB内存
limits:
cpu: "500m" # 每个Pod最多用500m CPU
memory: "512Mi" # 每个Pod最多用512MB内存
env:
- name: ASPNETCORE_ENVIRONMENT # 设置环境变量
value: "Production"
volumes:
- name: config-volume # 卷的名字
configMap:
name: myservice-config # 用哪个ConfigMap
代码逐行拆解(结合底层原理)
-
副本数(Replicas)
yaml
replicas: 3
底层原理:Deployment会创建3个Pod,如果其中一个Pod崩溃,Deployment会自动启动一个新的Pod,保证始终有3个Pod运行;
生产级技巧:用HPA(Horizontal Pod Autoscaler)自动调整副本数,比如CPU使用率超过70%时自动增加到5个副本,低于30%时减少到2个。 -
资源请求和限制(Resources)
yaml
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
底层原理:K8s会根据资源请求调度Pod到有足够资源的节点上,资源限制防止Pod占用过多资源导致其他服务崩溃;
生产级技巧:不要设置超过节点资源的限制,比如节点只有2核CPU,不要设置每个Pod的CPU限制为3核。 -
挂载ConfigMap
yaml
volumeMounts:
- name: config-volume
mountPath: /app/appsettings.json
subPath: appsettings.json
底层原理:把ConfigMap里的appsettings.json文件挂载到容器的/app/appsettings.json路径,覆盖原来的文件,修改ConfigMap后,Pod会自动重新读取配置;
拓展知识:用kubectl rollout restart deployment myservice-deployment手动重启Pod,应用新配置。
3. Service:给Pod分配固定IP
yaml
# 定义Service
apiVersion: v1
kind: Service
metadata:
name: myservice-service # Service的名字
spec:
type: NodePort # Service的类型,NodePort表示把服务暴露到节点的端口上
selector:
app: myservice # 选择标签为app=myservice的Pod
ports:
- protocol: TCP
port: 80 # Service的端口
targetPort: 5000 # 容器的端口
nodePort: 30007 # 节点的端口,范围是30000-32767(可选,K8s会自动分配)
代码逐行拆解(结合底层原理)
-
Service类型
NodePort:把服务暴露到节点的端口上,外部可以用节点IP:节点端口访问;
ClusterIP:默认类型,只在K8s集群内部访问,适合服务之间的通信;
LoadBalancer:在云厂商(比如AWS、阿里云)上会自动创建负载均衡器,外部可以用负载均衡器的IP访问;
Ingress:像K8s的Nginx,用域名访问多个服务,比如api.example.com访问myservice,redis.example.com访问redis。 - K8s核心命令逐行讲解
-
部署服务
bash
# 应用YAML文件,创建或更新资源
kubectl apply -f configmap.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
# 一次性应用所有YAML文件
kubectl apply -f .
-
查看资源状态
bash
# 查看Pod状态
kubectl get pods
# 查看Deployment状态
kubectl get deployments
# 查看Service状态
kubectl get services
# 查看Pod日志
kubectl logs myservice-deployment-7f9d6d8b9c-2xqzk
# 进入Pod的命令行
kubectl exec -it myservice-deployment-7f9d6d8b9c-2xqzk -- /bin/bash
# 查看Pod的详细信息(比如资源使用情况、挂载的卷)
kubectl describe pod myservice-deployment-7f9d6d8b9c-2xqzk
-
升级和回滚服务
bash
# 升级镜像版本
kubectl set image deployment/myservice-deployment myservice=username/myservice:v2
# 查看升级状态
kubectl rollout status deployment/myservice-deployment
# 回滚到上一个版本
kubectl rollout undo deployment/myservice-deployment
# 查看升级历史
kubectl rollout history deployment/myservice-deployment
四、生产级容器化部署技巧
-
Docker镜像优化
多阶段构建:用SDK镜像构建,Runtime镜像运行,减少体积;
清理无用文件:在RUN指令里合并命令,清理临时文件,比如RUN dotnet restore && dotnet build && rm -rf /root/.nuget;
用Alpine镜像:Alpine是一个轻量级Linux发行版,体积只有5MB,比如mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine,适合控制台应用。 -
K8s生产级配置
用Ingress暴露服务:用域名访问服务,比如api.example.com,比NodePort更友好;
用HPA自动扩缩容:根据CPU或内存使用率自动调整Pod副本数;
用NodeSelector调度Pod:把Pod调度到指定节点,比如把数据库Pod调度到有SSD的节点;
用Taints和Tolerances:给节点打污点,只有有容忍度的Pod才能调度到这个节点,比如把GPU节点打污点,只有需要GPU的Pod才能调度到这个节点。 -
容器化监控与排查
Docker监控:用docker stats查看容器的CPU、内存使用率;
K8s监控:用Prometheus+Grafana监控Pod的CPU、内存、网络使用率;
日志收集:用ELK(Elasticsearch+Logstash+Kibana)或Loki收集容器日志,统一查看;
故障排查:用kubectl debug创建调试Pod,比如kubectl debug -it myservice-deployment-7f9d6d8b9c-2xqzk --image=busybox --share-processes,查看Pod的进程和文件。
五、总结与选型建议 - 选型表
| 场景 | 推荐工具 |
|---|---|
| 本地开发、测试环境 | Docker + Docker Compose |
| 生产环境、高并发场景 | Kubernetes + Helm |
| 多服务部署、依赖管理 | Docker Compose(开发)、Kubernetes(生产) |
| 跨平台部署 | Docker(打包镜像)、Kubernetes(管理容器) |
-
容器化部署流程
1.本地开发:用Docker Compose启动服务和依赖,测试功能;
2.构建镜像:用多阶段构建Docker镜像,推送到私有仓库;
3.测试环境部署:用K8s部署镜像,测试性能和稳定性;
4.生产环境部署:用K8s+Helm部署,设置自动扩缩容、监控、日志收集;
5.升级和回滚:用K8s的rollout功能升级镜像,出现问题时回滚到上一个版本。
现在你已经掌握了Docker和Kubernetes的核心用法,从镜像构建到容器运行,从多服务部署到K8s编排,以后容器化部署不用慌,按照这个流程来,90%的问题都能提前避免!下一节我们会学习“容器化网络与安全”,结合Docker和K8s的网络原理,教你保护容器化服务的安全!
转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49594.html










