今天,我们将深入探讨一个项目部署的演变过程。在这篇文章中,为了紧扣主题,我们将从 Docker 开始讲解,分析为什么一个传统的项目逐步演变成了今天流行的 Kubernetes(K8s)集群部署架构。我们将通过一个简单的 Java 项目来阐述这一过程。

为了更清晰地阐述,我在本地搭建了一个 gRPC 入门项目。考虑到篇幅和内容的专注性,我将这一部分的详细讲解单独撰写成了一篇文章。具体内容可以参考这篇文章获取更多信息和背景知识:https://www.cnblogs.com/guoxiaoyu/p/18555031

接下来,我们将逐步展开,深入讲解从 Docker 容器化到 K8s 集群化的过渡,分析这个过程中面临的挑战和技术演进,并讨论为什么 Kubernetes 已经成为现代云原生应用部署的标配。

好了,现在我们正式开始本篇文章的讲解。

Docker

这部分内容大家应该都比较熟悉了。对于个人开发者来说,Docker几乎是每个项目中不可或缺的一部分,学习如何使用 Docker 命令是每个开发者的必修课。因此,关于 Docker 的安装过程,我就不再赘述了。大家可以根据官方文档或者教程轻松完成安装,过程也相对简单明了。

在使用 Docker 进行项目部署时,首先需要一个名为 Dockerfile 的配置文件,它定义了如何构建和封装项目容器。具体内容如下:

# 使用官方的 OpenJDK 镜像作为基础镜像
FROM openjdk:8-jdk-alpine

# 将构建好的Spring Boot JAR文件复制到容器中
COPY grpc-server/target/grpc-server-1.0-SNAPSHOT.jar /app/grpc-server-1.0-SNAPSHOT.jar

# 设置工作目录
WORKDIR /app

# 暴露 gRPC 应用程序的端口
EXPOSE 9090

# 运行 gRPC 服务
CMD ["java", "-jar", "grpc-server-1.0-SNAPSHOT.jar"]

在使用 Maven 编译和打包 Java 项目时,如果我们需要启动一个可执行的 JAR 包,就必须指定一个入口类,即启动类。我们需要单独配置一个编译插件,通常是 spring-boot-maven-plugin(如果是 Spring Boot 项目)。

这样做可以确保在执行 mvn package 命令时,Maven 会将启动类作为项目的入口,并生成正确的可执行 JAR 文件。以下是一个配置示例:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

接下来,我们正常执行mvn命令即可。执行完后,我们通常可以在服务器上直接通过docker命令进行构建docker镜像。这样所有环境集成都有了,直接启动docker镜像即可。跟我们本地的环境一点关系没有。命令如下:

过程如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

我们暴露出来了一个服务器端口供外网调用。这里我测试一下,是正常的,效果如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

综合上述所有条件,我们可以得出一个重要结论:Docker 在环境隔离方面确实具有显著的优势。通过 Docker,我们无需在本地系统上手动安装和配置各种依赖环境,从而避免了因环境配置不同而导致的问题。只需要执行一个简单的 Docker 命令,就能自动拉取所需的镜像并启动项目,这大大简化了开发和部署过程。

尤其是对于大多数开源项目而言,几乎都提供了官方或社区维护的 Docker 镜像,这使得用户能够快速入门,无需深入了解复杂的配置细节。

那么问题来了?

公司项目的部署远远不止于简单地启动一个 Docker 容器,而是涉及到多个复杂的组件和服务的协同工作。具体来说,除了 Docker 容器之外,我们通常还需要部署和配置 Nginx、前端服务、后端服务、数据库等一系列基础设施组件。每个项目都会根据实际需求涉及到不同的服务和环境配置,处理起来并不简单。

更重要的是,考虑到每次部署时可能都需要执行大量的命令来启动这些服务,难道我们真的要把这些命令手动记录在记事本中,然后每次上线时都逐一敲入这些命令吗?看下这段命令:

docker run -d \
  --name grpc-server-container \                 # 设置容器名称
  -p 9090:9090 \                                 # 映射容器的 9090 端口到主机的 9090 端口
  -p 8080:8080 \                                 # 映射容器的 8080 端口到主机的 8080 端口
  -v /data/logs:/logs \                          # 将主机的 /data/logs 目录挂载到容器的 /logs 目录
  -v /path/to/app/config:/app/config \           # 挂载应用配置文件夹
  -v /path/to/db:/var/lib/postgresql/data \      # 将主机数据库目录挂载到容器的数据库目录
  -e LOG_LEVEL=debug \                           # 设置环境变量 LOG_LEVEL
  -e DB_USER=admin \                             # 设置数据库用户名环境变量
  --restart unless-stopped \                     # 设置容器的重启策略,除非手动停止,否则容器会自动重启
  --network host \                               # 使用主机网络模式,可以与主机共享网络资源
  --log-driver=json-file \                       # 设置日志驱动为 json-file(默认)
  grpc-server                                    # 使用 grpc-server 镜像

那么,针对这种繁琐的手动输入命令和配置的情况,是否存在一些工具或者方式,能够帮助我们提前将这些命令和配置都写好,并且每次只需执行一个文件,就能顺利启动整个项目,避免重复操作和人为失误呢?

答案是肯定的,正是基于这种需求,我们有了 Docker Compose 这样的编排工具。

Docker-compose

编排文件采用一种固定的格式来书写,目的是确保 Docker 在执行时能够正确地识别和启动所需的所有服务和容器。

接下来,我们将以 grpc-server 项目为例,展示如何将该项目配置到 Docker Compose 的编排文件中,文件内容如下:

version: '3.8'

services:
  grpc-server:
    image: grpc-server           # 使用 grpc-server 镜像
    ports:
      - "9090:9090"              # 映射端口 9090
    volumes:
      - /data/logs:/app/logs         # 挂载主机目录 /data/logs 到容器的 /logs 目录
    restart: unless-stopped      # 如果容器停止,除非手动停止,否则会重新启动容器

其实,这个过程非常简单和直观。如果你需要启动多个容器,只需在 Docker Compose 的编排文件中继续添加相应的服务配置,每个服务都会自动与其他服务进行协同工作。

对于每个容器的配置,你只需按需扩展,每新增一个容器,只要在文件中继续添加对应的服务定义即可,这样一来,整个项目中的所有服务都会被包含在编排文件内,实现一次性启动多个容器。命令如下:

启动多个指定容器:

后台启动所有容器:

通过使用编排文件,我们几乎不再需要手动维护各种 Docker 启动命令,而是可以通过统一的配置文件进行管理和部署。这种方式的最大优点在于,它显著简化了普通 Docker 启动命令的维护和执行过程,避免了手动操作带来的复杂性和出错风险。

同时,这种自动化的部署方法基本上解决了小型公司在项目部署中的诸多困难,使得项目的部署更加高效、稳定和可重复,从而大大提升了团队的生产力和项目的交付速度。

那么问题来了

尽管项目本身的复杂度没有显著增加,但随着用户量的不断上升,我们面临的挑战也随之加剧。由于机器的带宽和内存是有限的,单纯依靠一个 Docker 实例已经无法满足当前用户的访问需求。在这种情况下,我们通常需要通过增加机器来进行水平扩展,通常是在新的机器上重新执行相同的部署命令。

然而,这种反复手动操作的方式会导致运维人员的工作量呈指数级增长,每次版本发布和扩容时,运维人员不仅需要投入大量的精力,还容易感到身心疲惫,工作负担越来越重,效率也大大降低。

正因如此,集群部署的需求变得尤为迫切,而 Kubernetes(K8s)作为现代化容器编排平台应运而生,并迅速成为解决这一问题的利器。

K8s

很多人其实也听说过Docker Swarm,它是Docker原生的集群部署方式,具有一定的自动化和容错能力,但相比于Google设计的Kubernetes(K8s),其功能和生态系统的完善程度明显不足,因此未能获得像K8s那样广泛的应用和好评。

话不多说,我们现在回到重点,继续深入讲解K8s的部署方式。为了方便演示,我已经在本地提前配置好了一个简化版的K8s环境,接下来的内容将不再赘述其具体安装过程。

如果对Kubernetes(K8s)还不太了解的朋友,可以先参考我之前写的这篇文章,它将帮助你快速掌握K8s的基本概念和架构:https://www.cnblogs.com/guoxiaoyu/p/17876335.html

面临的一个实际问题是:大多数人手中都有现成的Docker Compose编排文件,而如何将这些文件高效地迁移到K8s环境中呢?幸运的是,这里有一个非常流行的工具,它能够帮助你轻松实现这一迁移,不需要手动编写复杂的K8s部署文件或进行繁琐的配置。

kompose:Convert your Docker Compose file to Kubernetes

Kompose的主要目标就是帮助开发者快速将现有的Docker Compose编排文件转换为Kubernetes(K8s)所需的资源配置文件,简化从Docker Compose到K8s的迁移过程。我们看下如何使用,需要通过以下命令安装Kompose:

上面的命令执行完后,基本上我们就可以正常使用了。效果如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

我们直接通过执行文件的方式转化我们的编排文件。命令如下:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

执行完后,通常情况下官方的教程是直接就可以进行kubelet apply部署,但是注意一些坑,我们一起来看下。

首先,在 Kubernetes 中,每个容器都可以通过挂载目录来持久化其数据。尽管 Kubernetes 默认会将容器的日志存储在 /var/log/containers/ 目录下,但对于像 MySQL 这样的数据库服务,仅依赖 Kubernetes 自带的日志目录是不足够的。因为容器的重启或停启操作可能会导致数据丢失,因此必须通过挂载持久化存储卷(例如,使用 Persistent Volumes 或 HostPath 等方式)来保证数据库的数据安全和持久性。

在本示例中,我仅仅是为了演示目的,简单地挂载了日志目录,而未涉及更复杂的数据存储挂载配置。

数据挂载

这里有几个新的关键名词,在之前我是没有讲过的,如下:

PV(Persistent Volume):是一个具体的存储资源(可以是本地磁盘、云存储等),由管理员配置。PV 存储的内容是持久化的,不会因容器的销毁而丢失。

PVC(Persistent Volume Claim):是用户或应用向 Kubernetes 请求存储资源的方式。用户声明他们需要多大的存储、访问模式等,Kubernetes 会根据这些要求找到合适的 PV。

PV 与 PVC 的关系:PVC 向 Kubernetes 提出存储需求。PV 提供实际的存储资源,且由 Kubernetes 根据 PVC 的要求动态绑定。

通过将 PVC 挂载到 Pod 中,容器就能使用持久化存储的数据。通过这种方式,Kubernetes 可以高效地管理存储,确保容器应用的数据不会丢失,即使容器被销毁或重新部署,数据仍然得以保留。

为了实现文件共享和数据存储,我们需要搭建一个NFS(Network File System)服务器。这个服务器可以选择由云服务商提供,也可以使用本地服务器的磁盘资源。在本例中,我们将以本地服务器为例,详细介绍如何搭建一个NFS 服务器,并进行相关配置。

NFS 服务器

我们不能直接使用kubelet apply部署,相反,我们需要对 PVC 文件进行一定的修改,以确保它能够正确地创建所需的存储资源。具体来说,我们需要在 PVC 文件中指定使用的 StorageClass。

使用以下命令获取当时集群的StorageClass,如果无StorageClass则需要先创建。

因为我这是刚搭建的K8s环境,所以我这里显示的是没有,那么先搭建一个本地NFS服务器。

查看系统是否已安装NFS

安装NFS 、RPC

启动服务

创建目录

编辑export文件

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

配置生效

启动rpcbind、nfs服务

自我测试一下是否可以联机

测试效果正常:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

Storageclass启动

首先,我们需要启动StorageClass,文件内容如下:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs
provisioner: kubernetes.io/nfs
parameters:
  server: 192.168.56.5
  path: /nfs-server

可以看到这里是以本地IP为NFS服务器的。直接使用命令启动即可。

效果如下,启动成功:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

PV启动

然后配置一下pv文件,并启动:

kind: PersistentVolume
apiVersion: v1
metadata:
  name: nfs-pv-test
  namespace: database
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 1.5Gi
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfs-server/log
    server: 192.168.56.5

启动成功,效果如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

项目启动

接下来,我们就需要启动当时kompose生成的三个文件,我们挨个执行,命令如下:

最后,正常启动service服务后可以看到正常分配了内网ip,但并不会被外网访问到。

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

为了方便演示,我们简单修改一下grpc-server-xiaoyu-service.yaml ,让外网ip可以访问到,内容如下:

apiVersion: v1
kind: Service
metadata:
  annotations:
    kompose.cmd: kompose convert -f docker-compose.yml
    kompose.version: 1.34.0 (cbf2835db)
  labels:
    io.kompose.service: grpc-server-xiaoyu
  name: grpc-server-xiaoyu
spec:
  ports:
    - name: "9091"
      port: 9093
      targetPort: 9093
  type: NodePort
  selector:
    io.kompose.service: grpc-server-xiaoyu

再次启动,效果如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

最后启动成功:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

改下我们本地的连接端口:

public class Main {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("192.168.56.5", 30238)
                .usePlaintext()
                .build();

        // 创建一元式阻塞式存根
        GreeterGrpc.GreeterBlockingStub blockingStub = GreeterGrpc.newBlockingStub(channel);

        // 创建请求对象
        HelloRequest request = HelloRequest.newBuilder()
                .setName("World")
                .build();

        // 发送请求麦位列表信息并接收响应
        HelloReply response = blockingStub.sayHello(request);

        // 处理麦位列表信息响应
        System.out.println("Received  response: " + response);

    }
}

日志输出成功:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

在 Kubernetes 中,容器的日志默认存储在宿主机的 /var/log/containers/ 目录下。每个容器的日志文件名通常包括容器的名称、Pod 名称、命名空间和容器运行的唯一标识符(例如 Pod 的 UID)。这种路径结构确保了每个容器的日志都是唯一的,不会与其他容器的日志混合。

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

我们看下默认日志目录下生成的日志。

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

由于我们已经成功挂载了数据盘,因此相应的挂载目录也已创建并可供使用。效果如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

动态扩容

为了应对容器编排中可能出现的容器实例数量增加的问题,Kubernetes 提供了灵活的扩展机制,既可以通过自动扩容来根据负载自动增加或减少容器实例数量,也可以通过手动扩容的方式来快速响应突发的高并发用户访问需求。

在此,我们将演示如何手动增加容器实例的数量。命令如下:

执行成功,效果如图所示:

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

总体而言,Kubernetes 以其灵活性和强大的功能,基本上已经能够满足现代化项目的绝大部分需求,尤其是在容器实例扩展和自动化管理等方面,极大地降低了手动干预的复杂度。

那么问题来了

综上所述,虽然 Kubernetes(K8s)在日常运维和管理方面为开发者和运维团队提供了极大的便利,自动化的扩展、负载均衡、容错处理等功能也大大提升了系统的可靠性和可维护性,但在初期的服务器搭建和集群配置过程中,K8s 的复杂性却无可避免地带来了不少挑战。

由于 K8s 本身是一个高度模块化的系统,其组件间的依赖性较强,任何一项配置错误都可能导致集群运行异常。这就增加了集群部署和维护的难度,尤其对于中小型企业或者缺乏深厚运维经验的团队来说,如何快速部署并确保集群稳定性,依然是一个需要解决的难题。那么,面对这一挑战,如何能够降低 K8s 部署和管理的门槛,并使其更易于使用呢?

上云

在这方面,实际上不必过多赘述。当前,各大云服务提供商的 Kubernetes 集群部署服务已经相当成熟,基本上能够满足绝大多数企业对集群服务的需求,并且提供了完善的生态支持。比如,监控与报警系统、日志收集、自动化扩容、负载均衡等一整套解决方案,几乎涵盖了现代化应用所需的所有基础设施和运维功能。

一文详解:项目如何从Docker慢慢演变成了K8s部署-LMLPHP

相较于自行搭建和管理 Kubernetes 集群,使用云厂商提供的服务无疑是更加便捷和高效的。只需通过云平台的控制台进行几次点击,相关服务就能自动化部署,极大地缩短了上线周期。对于技术团队来说,这种便捷的集群部署方式,几乎可以做到即插即用,快速上手,降低了对复杂操作和配置的依赖。

此外,云服务商通常都会提供丰富的官方文档支持,帮助用户解决常见问题。在遇到更复杂的情况时,用户还能直接提交工单,享受一对一的专属技术支持,这对于解决实际运维中的疑难问题至关重要。因此,相比于传统的自行搭建集群,云服务的方案在稳定性、易用性和服务质量上都有着无可比拟的优势。

总结

通过本文的深入探讨,我们已经详细了解了一个项目从 Docker 容器化到 Kubernetes 集群化的演变过程。在这个过程中,我们不仅分析了 Docker 的基础使用方法和 Docker Compose 的便捷性,还介绍了 Kubernetes 在处理大型、复杂系统中的重要作用。

虽然 Kubernetes 在初期的配置和维护上可能带来一定的复杂性,但随着云服务的成熟,各大云平台提供了全面的 Kubernetes 集群管理解决方案,极大地简化了部署流程。云服务商的自动化工具和技术支持,帮助企业快速上手 Kubernetes,避免了许多传统集群部署中的难题。

因此,Kubernetes 已经成为现代云原生应用部署的标配,它的灵活性、扩展性和高度自动化特性,使得它在容器编排和微服务架构的管理中占据了无可替代的地位。对于开发者来说,掌握 Kubernetes 是未来工作中不可或缺的一项技能,它不仅能提高项目的交付速度,还能有效降低运维复杂度,为企业提供更高效、可靠的服务。

最后,无论采用何种方式,我们都应根据实际情况出发,避免盲目追求华而不实,无论是 Docker、编排工具,还是 Kubernetes,总有一种方式最适合你的需求。


我是努力的小雨,一名 Java 服务端码农,潜心研究着 AI 技术的奥秘。我热爱技术交流与分享,对开源社区充满热情。同时也是一位腾讯云创作之星、阿里云专家博主、华为云云享专家、掘金优秀作者。

💡 我将不吝分享我在技术道路上的个人探索与经验,希望能为你的学习与成长带来一些启发与帮助。

🌟 欢迎关注努力的小雨!🌟

11-25 10:28