MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

使用容器构建可扩展的 Web 应用

2023-07-054.4k 阅读

容器技术基础

在深入探讨如何使用容器构建可扩展的 Web 应用之前,我们先来了解一下容器技术的基础知识。

容器的概念

容器是一种轻量级的、可执行的独立软件包,它将软件及其所有依赖项打包在一起,确保软件在任何环境中都能以相同的方式运行。与传统的虚拟机不同,容器共享宿主机的操作系统内核,这使得它们更加轻量化、启动速度更快且资源消耗更低。

例如,我们有一个简单的 Python Web 应用,它依赖于特定版本的 Python 以及一些第三方库,如 Flask。在传统部署方式下,我们需要在服务器上安装 Python 及其依赖库,并且要确保版本与开发环境一致,这可能会遇到各种版本冲突问题。而使用容器,我们可以将 Python、Flask 以及应用代码一起打包到一个容器中,这个容器就像一个自给自足的“盒子”,无论部署到哪个支持容器运行的环境中,都能正常运行。

容器技术的优势

  1. 环境一致性:容器确保了应用在开发、测试和生产环境中的一致性。开发人员在本地开发环境中构建的容器,在测试和生产环境中可以以相同的状态运行,大大减少了“在我机器上能运行,在服务器上不行”的问题。
  2. 高效的资源利用:由于容器共享宿主机内核,相比于虚拟机,容器对资源的占用要少得多。一台物理服务器可以运行大量的容器,提高了硬件资源的利用率。
  3. 快速部署和扩展:容器的启动速度非常快,通常在秒级甚至毫秒级。这使得我们可以快速地部署新的实例,并且根据业务需求动态地扩展或缩减容器数量。
  4. 易于维护和管理:容器将应用及其依赖封装在一起,使得应用的维护和管理更加简单。如果需要更新应用或其依赖,只需要更新相应的容器镜像即可。

容器运行时

容器运行时是负责运行容器的软件。目前最流行的容器运行时是 runc(runtime for containers 的缩写)。runc 是一个轻量级的、符合 OCI(Open Container Initiative)标准的容器运行时。OCI 定义了容器镜像和运行时规范,这使得不同的容器工具和平台能够相互兼容。

除了 runc,还有一些更高层次的容器运行时,如 Docker。Docker 是一个开源的应用容器引擎,它提供了一个用户友好的界面来管理容器,包括构建、发布和运行容器。Docker 底层实际上也是使用 runc 来运行容器,但它为用户提供了更便捷的操作方式,如 Docker 命令行工具和 Dockerfile 来定义容器镜像的构建过程。

容器镜像

容器镜像是容器的基础,它包含了运行应用所需的所有文件系统内容,包括操作系统、应用代码、依赖库等。

镜像的构建

  1. Dockerfile 构建容器镜像最常用的方式是使用 Dockerfile。Dockerfile 是一个文本文件,其中包含了一系列的指令,用于定义如何构建镜像。下面是一个简单的 Python Flask 应用的 Dockerfile 示例:
# 使用官方 Python 基础镜像
FROM python:3.8-slim

# 设置工作目录
WORKDIR /app

# 将 requirements.txt 复制到工作目录
COPY requirements.txt.

# 安装应用依赖
RUN pip install -r requirements.txt

# 将应用代码复制到工作目录
COPY.

# 暴露应用端口
EXPOSE 5000

# 定义容器启动时执行的命令
CMD ["python", "app.py"]

在这个 Dockerfile 中:

  • FROM 指令指定了基础镜像,这里使用的是官方的 Python 3.8 轻量级镜像。
  • WORKDIR 设置了容器内的工作目录。
  • COPY 指令将本地的 requirements.txt 文件和应用代码复制到容器内的工作目录。
  • RUN 指令在容器内执行命令,这里用于安装应用的依赖库。
  • EXPOSE 声明容器运行时要监听的端口。
  • CMD 定义了容器启动时要执行的命令。
  1. 构建镜像 在包含 Dockerfile 的目录下,我们可以使用 docker build 命令来构建镜像。例如:
docker build -t my-flask-app:1.0.0.

其中,-t 选项用于指定镜像的标签(tag),格式为 repository:tag,这里 my-flask-app 是镜像仓库名,1.0.0 是版本号。最后的 . 表示 Dockerfile 所在的当前目录。

镜像的存储和分发

  1. 镜像仓库 构建好的容器镜像需要存储在镜像仓库中,以便在不同环境中拉取和使用。常见的镜像仓库有 Docker Hub、Harbor 等。Docker Hub 是 Docker 官方的公共镜像仓库,提供了大量的官方和社区镜像。Harbor 则是一个开源的企业级镜像仓库,支持用户在内部网络中搭建私有的镜像仓库。
  2. 推送和拉取镜像 将镜像推送到镜像仓库需要先登录到相应的仓库。例如,要将镜像推送到 Docker Hub:
docker login
docker push my-flask-app:1.0.0

在其他环境中使用该镜像时,通过 docker pull 命令拉取:

docker pull my-flask-app:1.0.0

容器编排

当我们的 Web 应用由多个容器组成,如一个 Web 服务器容器、一个数据库容器等,并且需要对这些容器进行管理、部署和扩展时,就需要用到容器编排技术。

Kubernetes 简介

Kubernetes(简称 K8s)是目前最流行的容器编排平台。它提供了自动化的容器部署、扩展、管理和自愈能力。Kubernetes 中的一些重要概念包括:

  1. Pod:Pod 是 Kubernetes 中最小的可部署和可管理的计算单元,它可以包含一个或多个紧密相关的容器。例如,我们可以将一个 Web 应用容器和一个日志收集容器放在同一个 Pod 中,它们共享网络和存储资源。
  2. Deployment:Deployment 是用于管理 Pod 的对象,它定义了 Pod 的副本数量、更新策略等。通过 Deployment,我们可以轻松地实现应用的滚动升级和回滚。
  3. Service:Service 为一组 Pod 提供了一个稳定的网络接口,使得其他 Pod 或外部客户端可以通过这个接口访问到这些 Pod。例如,我们可以创建一个 Service 来暴露 Web 应用的 Pod,使得外部用户可以通过浏览器访问该应用。

使用 Kubernetes 部署 Web 应用

  1. 创建 Deployment 下面是一个简单的 Kubernetes Deployment 配置文件 flask-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
    spec:
      containers:
      - name: flask-container
        image: my-flask-app:1.0.0
        ports:
        - containerPort: 5000

在这个配置文件中:

  • apiVersion 指定了 Kubernetes API 的版本。
  • kind 表示这是一个 Deployment 对象。
  • metadata.name 定义了 Deployment 的名称。
  • spec.replicas 设置了 Pod 的副本数量为 3,即会创建 3 个运行该 Web 应用的 Pod。
  • spec.selector 用于选择要管理的 Pod,通过标签 app: flask-app 来匹配。
  • spec.template 定义了 Pod 的模板,其中 metadata.labels 为 Pod 添加了标签,spec.containers 定义了容器的相关信息,包括容器名称、使用的镜像以及要暴露的端口。
  1. 创建 Service 为了让外部能够访问到我们的 Web 应用,需要创建一个 Service。以下是 flask-service.yaml 的配置:
apiVersion: v1
kind: Service
metadata:
  name: flask-service
spec:
  selector:
    app: flask-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 5000
  type: LoadBalancer

这里:

  • kindService
  • metadata.name 是 Service 的名称。
  • spec.selector 通过标签 app: flask-app 选择对应的 Pod。
  • spec.ports 定义了服务的端口映射,将外部的 80 端口映射到 Pod 的 5000 端口。
  • type: LoadBalancer 表示这是一个负载均衡类型的 Service,会自动分配一个外部 IP 地址,使得外部用户可以通过该 IP 访问应用。
  1. 部署应用 在包含 flask-deployment.yamlflask-service.yaml 的目录下,使用 kubectl 命令进行部署:
kubectl apply -f flask-deployment.yaml
kubectl apply -f flask-service.yaml

通过 kubectl get podskubectl get services 命令可以查看 Pod 和 Service 的状态。

构建可扩展的 Web 应用

使用容器和容器编排技术,我们可以轻松地构建可扩展的 Web 应用。

水平扩展

水平扩展是指通过增加或减少 Pod 的副本数量来应对不同的负载情况。在 Kubernetes 中,我们可以使用 kubectl scale 命令手动扩展 Deployment 的副本数量。例如,将 flask-deployment 的副本数量扩展到 5:

kubectl scale deployment flask-deployment --replicas=5

此外,Kubernetes 还支持自动扩缩容。我们可以定义基于 CPU 使用率、内存使用率等指标的自动扩缩容策略。以下是一个基于 CPU 使用率的 HorizontalPodAutoscaler(HPA)配置文件 flask-hpa.yaml

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: flask-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: flask-deployment
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 50

在这个配置中:

  • scaleTargetRef 指向要进行自动扩缩容的 Deployment。
  • minReplicas 定义了最小副本数为 3。
  • maxReplicas 定义了最大副本数为 10。
  • targetCPUUtilizationPercentage 设置了目标 CPU 使用率为 50%。当 Pod 的平均 CPU 使用率超过 50% 时,Kubernetes 会自动增加 Pod 的副本数量;当平均 CPU 使用率低于 50% 时,会自动减少副本数量。

服务发现与负载均衡

  1. 服务发现 在 Kubernetes 中,Service 提供了服务发现功能。当一个 Pod 创建时,Kubernetes 会为其分配一个唯一的 IP 地址,但是这个 IP 地址是动态变化的。通过 Service,其他 Pod 可以通过 Service 的 DNS 名称来访问目标 Pod,而不需要关心 Pod 的实际 IP 地址。例如,在同一个 Kubernetes 集群中,其他 Pod 可以通过 flask-service.flask-namespace.svc.cluster.local(假设 Service 名称为 flask-service,命名空间为 flask - namespace)来访问我们的 Flask 应用 Service。
  2. 负载均衡 Service 不仅提供服务发现,还具备负载均衡功能。当有多个 Pod 提供相同的服务时,Service 会将请求均匀地分发到这些 Pod 上。对于 type: LoadBalancer 类型的 Service,Kubernetes 会与云提供商的负载均衡器集成,将外部请求分发到集群内部的 Pod 上。在没有云提供商的环境中,也可以使用 MetalLB 等工具来实现负载均衡。

故障容错与自愈

  1. 故障检测 Kubernetes 会持续监控 Pod 的状态。如果一个 Pod 出现故障,如容器崩溃或应用进程异常退出,Kubernetes 会检测到并自动重启该容器或重新创建一个新的 Pod。
  2. 自愈能力 假设某个 Pod 因为硬件故障或网络问题而不可用,Kubernetes 的自愈机制会自动创建一个新的 Pod 来替代它,确保应用的可用性。同时,在进行滚动升级时,如果新的 Pod 出现问题,Kubernetes 会自动回滚到上一个稳定版本,保证应用的持续运行。

容器化 Web 应用的网络与存储

在容器化 Web 应用的部署中,网络和存储是两个重要的方面。

容器网络

  1. 容器网络模型 Kubernetes 使用的容器网络模型(CNM)是基于 CNI(Container Network Interface)规范。CNI 定义了一系列接口,用于容器运行时与网络插件之间的交互。常见的 CNI 插件有 Flannel、Calico 等。
  • Flannel:Flannel 是一个简单的容器网络解决方案,它通过在宿主机之间创建一个覆盖网络,为每个 Pod 分配一个唯一的 IP 地址。Flannel 使用 etcd 来存储网络配置信息和 IP 地址分配情况。
  • Calico:Calico 是一个更高级的容器网络解决方案,它支持多种网络策略和网络隔离方式。Calico 可以直接使用宿主机的网络,而不需要创建覆盖网络,这在性能上有一定优势。同时,Calico 提供了丰富的网络策略功能,可以实现细粒度的网络访问控制。
  1. 网络策略 Kubernetes 的网络策略允许我们定义 Pod 之间的网络访问规则。例如,我们可以限制只有特定的 Pod 可以访问数据库 Pod,从而增强应用的安全性。以下是一个简单的网络策略配置文件 db-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-network-policy
spec:
  podSelector:
    matchLabels:
      app: db
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: web

在这个配置中:

  • podSelector 选择了标签为 app: db 的 Pod,即数据库 Pod。
  • ingress.from 定义了允许访问这些数据库 Pod 的来源,这里是标签为 app: web 的 Web 应用 Pod。

容器存储

  1. 存储卷 在容器中,存储卷(Volume)用于持久化数据。例如,我们的 Web 应用可能需要将用户上传的文件或数据库数据保存下来,即使容器重启或重新创建,数据也不会丢失。Kubernetes 支持多种类型的存储卷,如 EmptyDir、HostPath、PersistentVolume 等。
  • EmptyDir:EmptyDir 是一种临时存储卷,它在 Pod 创建时创建,在 Pod 删除时销毁。它主要用于在同一个 Pod 内的多个容器之间共享数据。例如,一个 Web 应用容器和一个日志收集容器可以通过 EmptyDir 共享日志文件。
  • HostPath:HostPath 允许我们将宿主机上的目录挂载到容器内。这在开发和测试环境中很有用,例如将本地的代码目录挂载到容器内,方便调试。但在生产环境中,由于宿主机的不可靠性,一般不推荐使用。
  • PersistentVolume:PersistentVolume(PV)是 Kubernetes 中用于持久化存储的资源对象,它代表了集群中的一块存储。PersistentVolumeClaim(PVC)是用户对 PV 的请求,通过 PVC,Pod 可以动态地申请和使用 PV。例如,我们可以创建一个基于 NFS 或 Ceph 的 PV,然后通过 PVC 挂载到数据库 Pod 中,实现数据库数据的持久化存储。
  1. 存储类 存储类(StorageClass)用于动态配置存储卷。它定义了如何创建和管理 PV,不同的存储类可以对应不同的存储后端,如 AWS EBS、GCE PD 等。通过存储类,管理员可以根据不同的需求提供不同类型的存储服务,用户可以通过 PVC 选择合适的存储类。以下是一个简单的存储类配置文件 nfs-storage-class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage-class
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "false"

在这个配置中:

  • metadata.name 定义了存储类的名称。
  • provisioner 指定了存储卷的供应者,这里使用的是 NFS 外部供应者。
  • parameters.archiveOnDelete 设置了当 PVC 删除时,是否保留 PV 中的数据。

容器化 Web 应用的监控与日志管理

对于容器化的 Web 应用,监控和日志管理是保障应用稳定运行和性能优化的关键。

监控

  1. 指标收集 Kubernetes 本身提供了一些基本的监控指标,如 Pod 的 CPU 和内存使用率等。我们可以通过 kubectl top 命令查看这些指标。然而,为了实现更全面的监控,我们通常会使用一些第三方监控工具,如 Prometheus 和 Grafana。
  • Prometheus:Prometheus 是一个开源的监控和告警系统,它通过拉取(pull)的方式从目标服务器收集指标数据。在 Kubernetes 环境中,我们可以部署 Prometheus 来收集容器和节点的各种指标,如 Pod 的 CPU 使用率、内存使用率、网络流量等。Prometheus 支持多种数据采集方式,包括直接通过 HTTP 接口采集和使用 Exporter 进行采集。例如,Kube - State - Metrics Exporter 可以收集 Kubernetes 集群的各种状态指标,Node Exporter 可以收集宿主机的系统指标。
  • Grafana:Grafana 是一个可视化工具,它可以与 Prometheus 集成,将 Prometheus 收集到的指标数据以图表的形式展示出来。我们可以在 Grafana 中创建各种仪表盘(Dashboard),实时监控 Web 应用的性能和运行状态。例如,我们可以创建一个仪表盘,展示 Web 应用的请求响应时间、每秒请求数、CPU 和内存使用率等指标的变化趋势。
  1. 告警 基于 Prometheus 收集的指标数据,我们可以设置告警规则。当某些指标超出设定的阈值时,Prometheus 会触发告警。Prometheus 可以将告警信息发送到 Alertmanager,Alertmanager 负责管理和分发告警通知。我们可以配置 Alertmanager 将告警信息通过邮件、Slack 等方式发送给相关人员,以便及时处理问题。

日志管理

  1. 容器日志 每个容器在运行过程中都会产生日志,这些日志对于调试和故障排查非常重要。在 Docker 中,我们可以使用 docker logs 命令查看容器的日志。在 Kubernetes 中,通过 kubectl logs 命令可以查看 Pod 中容器的日志。然而,当容器数量众多时,集中管理和分析这些日志变得非常困难。
  2. 日志收集与分析 为了解决容器日志的集中管理和分析问题,我们通常会使用一些日志管理工具,如 Elasticsearch、Logstash 和 Kibana(ELK 栈)或 Fluentd、Elasticsearch 和 Kibana(Fluentd - Elasticsearch - Kibana,Fluentd 是一个轻量级的日志收集器,它可以从容器、文件、网络等多种来源收集日志,并将其发送到 Elasticsearch。Elasticsearch 是一个分布式搜索引擎,用于存储和索引日志数据。Kibana 是一个可视化工具,它可以与 Elasticsearch 集成,方便用户搜索、分析和可视化日志数据。

例如,我们可以在每个 Kubernetes 节点上部署 Fluentd 代理,它会自动收集该节点上所有容器的日志,并将其发送到 Elasticsearch 集群。在 Kibana 中,我们可以创建各种可视化面板,如按时间线展示日志、按日志级别统计日志数量、根据关键字搜索日志等,帮助我们快速定位和解决问题。

容器化 Web 应用的安全

容器化 Web 应用的安全至关重要,我们需要从多个方面来保障其安全性。

镜像安全

  1. 镜像扫描 在使用容器镜像之前,应该对镜像进行安全扫描,以检测其中是否存在已知的安全漏洞。常见的镜像扫描工具如 Trivy、Clair 等。这些工具可以扫描镜像中的操作系统包、应用依赖库等,识别出存在安全风险的组件,并提供相应的修复建议。例如,使用 Trivy 扫描 my-flask-app:1.0.0 镜像:
trivy image my-flask-app:1.0.0
  1. 镜像来源 只使用来自可信来源的镜像,尽量避免使用来源不明的镜像。对于从公共镜像仓库获取的镜像,要仔细检查其文档和社区反馈,确保镜像的安全性。同时,可以在内部搭建私有的镜像仓库,并对镜像的上传和下载进行严格的权限控制。

容器运行时安全

  1. 最小权限原则 在容器内运行应用时,遵循最小权限原则。例如,尽量不要以 root 用户运行容器内的进程,如果应用不需要特定的系统权限,应使用普通用户运行。在 Dockerfile 中,可以通过 RUN groupadd -r appuser && useradd -r -g appuser appuser 创建一个普通用户,并使用 USER appuser 切换到该用户运行应用。
  2. 内核安全加固 对宿主机的内核进行安全加固,启用一些内核安全模块,如 SELinux(Security - Enhanced Linux)或 AppArmor。这些模块可以对容器的运行进行更严格的安全控制,限制容器对宿主机资源的访问。

网络安全

  1. 网络隔离 利用 Kubernetes 的网络策略实现容器之间的网络隔离,确保不同功能的容器之间不会发生不必要的网络通信。例如,将数据库容器与 Web 应用容器进行隔离,只允许授权的 Web 应用容器访问数据库容器。
  2. 加密通信 对于容器之间以及容器与外部客户端之间的通信,使用加密协议,如 TLS(Transport Layer Security)。在 Kubernetes 中,可以通过配置 Ingress Controller 并启用 TLS 加密,使得外部用户可以通过 HTTPS 安全地访问 Web 应用。

容器化 Web 应用的最佳实践

在实际构建和部署容器化 Web 应用时,以下是一些最佳实践。

镜像构建

  1. 使用基础镜像:选择官方的、经过安全加固的基础镜像,并且定期更新基础镜像,以获取最新的安全补丁和功能改进。
  2. 精简镜像:在构建镜像时,尽量减少不必要的文件和依赖,只包含运行应用所需的最小集合。这不仅可以减小镜像的大小,加快镜像的拉取和部署速度,还可以降低安全风险。
  3. 多阶段构建:对于一些需要编译或打包的应用,使用多阶段构建。例如,在构建一个 Go 语言应用时,第一阶段可以安装编译工具并编译应用,第二阶段只将编译好的二进制文件和必要的运行时文件复制到一个轻量级的基础镜像中,这样可以得到一个非常精简的运行时镜像。

容器编排

  1. 资源限制:在 Kubernetes 的 Deployment 配置中,为容器设置合理的资源限制,如 CPU 和内存限制。这可以防止某个容器占用过多资源,影响其他容器的正常运行。例如:
spec:
  containers:
  - name: flask-container
    image: my-flask-app:1.0.0
    ports:
    - containerPort: 5000
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "200m"
        memory: "256Mi"

这里 requests 定义了容器启动时请求的资源量,limits 定义了容器可以使用的最大资源量。 2. 命名规范:对 Kubernetes 中的各种资源,如 Deployment、Service、Pod 等,使用统一的命名规范。这有助于提高资源的可管理性和可读性,特别是在大型集群中。

运维管理

  1. 持续集成与持续部署(CI/CD):建立 CI/CD 流程,确保代码的每次更新都能自动触发镜像构建、测试和部署。例如,使用 Jenkins、GitLab CI/CD 等工具,当代码推送到代码仓库时,自动构建新的镜像并部署到测试环境和生产环境。
  2. 定期备份:对于容器化应用中的重要数据,如数据库数据,定期进行备份。可以使用 Kubernetes 的存储卷快照功能结合外部存储系统来实现数据备份。
  3. 版本控制:对容器镜像和 Kubernetes 配置文件进行版本控制,使用 Git 等版本控制系统记录每次的更改,方便追溯和回滚。

通过以上对容器化 Web 应用各个方面的详细介绍,我们可以看到容器技术为构建可扩展、高性能、安全可靠的 Web 应用提供了强大的支持。从容器基础概念、镜像构建与管理,到容器编排、网络与存储、监控与日志管理以及安全保障和最佳实践,每个环节都紧密相连,共同构成了一个完整的容器化 Web 应用生态系统。在实际应用中,我们需要根据具体的业务需求和场景,灵活运用这些技术和方法,打造出满足企业需求的优秀 Web 应用。