如何在 Kubernetes 集群中使用MongoDB

引言

在开发Kubernetes服务时,我通常会从本地的Kind Kubernetes集群着手进行开发。我之前曾撰写过如何设置Kind 一文,并且在与本文相关的GitHub仓库 中,包含了完成这项工作所需的配置文件。

你可以通过以下命令克隆该仓库:

git clone [email protected]:MartinHodges/aquarium-with-mongo-db.git

为什么选择MongoDB?

我之前的文章探讨过究竟该选择SQL还是No-SQL的问题。如果你正在阅读本文,我假定你已经决定采用No-SQL。

一旦做出这个决定,紧接着就要考虑选择使用哪种No-SQL数据库。MongoDB的市场份额是其最接近的竞争对手的两倍,功能十分强大,同时拥有社区版和企业版。一般来说,它是大家首选的No-SQL数据库。

对MongoDB与其他数据库进行技术层面的比较,并非本文的重点。选择MongoDB,主要是基于它极高的人气,以及它完全能够出色地完成任务!

安装MongoDB

在我们的Kubernetes集群中安装MongoDB,和安装其他应用程序的方式类似,都是借助一个Operator来完成。

Kubernetes中的MongoDB

Kubernetes的Operator负责替你管理应用程序。它不仅能够安装和管控应用程序的生命周期,还能实时监控应用程序,在必要时采取相应措施。

就数据库而言,Operator可以创建数据库集群、实现扩展、执行备份等操作。一般来说,操作器依赖于安装自定义资源定义(CRDs),这些定义为它提供了专属的“Kubernetes配置语言”。Operator会监听将这些自定义资源添加到集群的请求,然后代你执行相应操作。

创建开发Kubernetes集群

假设你已经安装好了Kind,现在就可以使用以下配置来创建一个Kind集群:

kind/kind-config.yml

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraPortMappings:
  # apis
  - containerPort: 30080
    hostPort: 30080
- role: worker
- role: worker
- role: worker

上述配置会创建一个包含4个节点的集群,其中有1个控制节点和3个工作节点。同时,它还会在开发机器上开放30080端口。你可以使用以下命令创建本地Kubernetes集群:

kind create cluster --config kind/kind-config.yml

安装Operator

你可以通过Helm来安装一个受社区支持的Operator

首先,将Helm链接添加到本地仓库:

helm repo add mongodb https://mongodb.github.io/helm-charts

接着,查看该链接添加了哪些图表(chart):

helm search repo mongo

在列出的结果中,你会看到我们即将使用的社区Operator

我们打算将Operator和数据库放在一个名为mongo的独立命名空间中,先创建这个命名空间:

kubectl create namespace mongo

现在就可以安装Operator了:

helm install community-operator mongodb/community-operator -n mongo

如果你希望Operator监控在其他不同命名空间中创建的资源,可以在上述命令中添加--set operator.watchNamespace="<other namespace>"

安装完成后,你可以通过以下命令检查Operator的就绪状态是否为1/1 Running

kubectl get pods -n mongo

现在Operator已经启动并正常运行,我们可以查看它安装的自定义资源定义(CRDs):

kubectl get crds
kubectl describe crd mongodbcommunity.mongodbcommunity.mongodb.com

至此,我们已经准备好创建第一个MongoDB集群了。

创建集群

安装operator之后,它会持续监听任何创建MongoDB数据库的请求。我们可以借助operator加载的CRDs,通过将MongoDB清单应用到Kubernetes集群来发出创建请求。

在此之前,我们得为数据库用户设置一个以Kubernetes secret形式存在的密码。

按照下面的命令创建secret(记得把<…>替换成你自己选定的密码):

kubectl create secret generic my-user-password -n mongo --from-literal="password=<your password>"

你可以使用以下命令检查这个secret:

kubectl get secrets -n mongo my-user-password -o jsonpath={.data.password} | base64 -d; echo

你会发现,我使用了base64 -d来解码密码,这是因为所有Kubernetes的secret都是以base64编码存储的。由于我们使用了--from-literal参数,create secret命令会自动将密码进行base64编码。

现在有了密码,就可以创建一个MongoDB集群以及一个带有此密码的管理员用户的数据库了。

创建清单文件: k8s/my-mongo-db.yml

apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: my-mongo-db
  namespace: mongo
spec:
  members: 3
  type: ReplicaSet
  version: "7.0.11"
  security:
    authentication:
      modes: ["SCRAM"]
  users:
    - name: my-user
      db: admin
      passwordSecretRef: # 引用用于生成用户密码的secret
        name: my-user-password
        key: password
      roles:
        - name: clusterAdmin
          db: admin
        - name: userAdminAnyDatabase
          db: admin
      scramCredentialsSecretName: my-user-scram
  additionalMongodConfig:
    storage.wiredTiger.engineConfig.journalCompressor: zlib

需要注意的是,这会在standardstorageClass下创建持久卷声明(PVCs)。你必须确保你的持久卷(PV)operator能够正确地为这个请求的类创建PV。在Kind环境中这是可行的,但在其他环境中可能需要像nfs-client这类工具。如果这些PV不可用,你的集群将无法启动。

现在可以应用这个文件:

kubectl apply -f k8s/my-mongo-db.yml

并通过以下命令检查创建进度:

kubectl get pods -n mongo

你需要等待3个实例创建完成。在我的MacBook Pro(M2 Max Apple silicon)上,使用4节点的Kind集群,启动这3个实例大约需要5分钟。

一旦集群启动并运行,就可以检查服务是否已启动:

kubectl get svc -n mongo

返回结果应该类似这样:

NAME              TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
my-mongo-db-svc   ClusterIP   None         <none>        27017/TCP   6m

测试数据库

在我们的应用程序里,会直接从Kubernetes内部通过其服务的DNS名称连接到数据库。但为了进行测试,我们希望能从本地开发机器连接到它。

我第一次测试时,采用端口转发的方式,将其中一个MongoDB pod转发到本地开发机器上,结果在尝试进行任何更改时,收到了这样的错误信息:

MongoServerError[NotWriteablePrimary]: not primary

这是因为我选择进行端口转发的pod并非集群的主节点。从节点只是只读副本,因此所有写操作都必须通过主节点来执行。

为了避免这个问题,我们需要连接到主节点。

由于我们处于集群外部,要是使用像Compass这样的MongoDB客户端,它会尝试借助Kubernetes pod名称(例如:my-mongo-db-1.my-mongo-db-svc.mongo.svc.cluster.local )来查找主节点pod,但这肯定会失败,因为这些名称在外部是无法访问的。

要找出哪个节点是主节点,你可以查看任意节点的日志:

kubectl logs my-mongo-db-0 -n mongo -c mongod | grep "\"primary\":"

要是没有返回结果,那就说明你查看的这个节点就是主节点。

如果你得到了结果,即便只提取出几行,内容也会很长且难以阅读。要是你有像jq这样的JSON格式化工具,就可以使用:

kubectl logs my-mongo-db-0 -n mongo -c mongod | grep "\"primary\":" | jq

你会看到类似这样的一行:

...
"primary": "my-mongo-db-1.my-mongo-db-svc.mongo.svc.cluster.local:27017",
...

这就告诉你需要连接到哪个pod(在我的例子中是:my-mongo-db-1 )。现在,你可以通过以下命令转发这个pod的端口:

kubectl port-forward my-mongo-db-1 -n mongo 27017:27017

需要注意的是,你也可以通过其他pod进行连接,但会被限制只能执行只读命令。此外,主节点可能会发生变更。

一旦端口转发完成,我们就需要连接到数据库。为此,我们可以使用MongoDB Compass客户端。你可以从 https://www.mongodb.com/try/download/compass 下载这个客户端。

安装完成后,应该就能连接到你的数据库了。它会给出一个连接字符串(mongodb://localhost:27017),但我们需要更改一些设置。

点击“Advanced Connection Options”,选择“Direct connection”(否则会出现找不到地址的错误,因为客户端会尝试使用Kubernetes内部地址)。

点击“Authentication”选项卡。选择“Username/Password”,输入之前设置的用户名(my-user)和密码。将数据库添加为Admin,然后选择SCRAM-SHA-256认证机制(如有必要,向下滚动查找)。

点击“Save and Connect”,为你的连接命名,之后你应该就能看到Compass控制台已连接到你的数据库。

你会看到它在我们的集群中创建了adminconfiglocal数据库。

要是你顺利完成了这些步骤,那就说明你的MongoDB集群已经成功启动并运行了。

创建应用程序用户

你或许会觉得,任何要连接到我们MongoDB的应用程序,都能使用我们之前创建的my-user。但遗憾的是,并非如此,因为这个用户实际上是用于数据库维护的。

要让应用程序能够使用我们的数据库集群,我们得创建一个数据库以及一个用于访问该数据库的用户。

在Compass窗口底部,你会看到一个“>_MONGOSH”的提示。点击它,就能进入命令行界面。

接下来,我们通过以下命令来创建用户:

use aquarium
db.createUser({
  user: "my-app-user",
  pwd: "<password>",
  roles: [{ db: "aquarium", role: "dbOwner" }]
})

这里有几点需要留意。首先,在数据库(aquarium)尚未创建时,我们就切换到了这个不存在的数据库。这遵循了MongoDB的规则,即数据库在使用前无需预先定义,数据库和任何集合都是在你首次添加文档时才创建的。

其次是分配给新用户的角色。MongoDB有一组少量的内置角色可供分配给用户。在这种情形下,dbOwner角色允许用户读取、写入以及管理数据库。不过要是在生产环境中,你得根据实际需求合理限制用户权限。

执行上述命令后,你会得到一段较长的响应,在开头部分会包含以下内容:

...
ok: 1,
...

为了检查这个新用户,我们打开一个新的Compass连接。你可以通过菜单操作,在Mac上也可以按Cmd+N。注意,可能会有几秒钟没有任何反应,所以只按一次就好,别重复操作!

新连接窗口出现后,我觉得复制我们之前保存的连接会更简便(使用连接旁边的“”菜单)。

修改用户名和密码,还需要把“Authentication Database”改为“aquarium”,然后进行连接。

现在,你应该就能看到新创建的“aquarium”数据库了。你可以在数据库名称旁边点击“+”来创建一个“fishes”集合用于测试。接着,就可以以文档的形式向数据库添加数据,比如:

{
  "_id": 123,
  "fish": "Guppy"
}

到目前为止,我们的MongoDB已经准备就绪,可以和Spring Boot应用程序配合使用了。

创建Spring Boot应用程序

在创建基于数据库的简单示例时,我通常从“aquarium”应用程序入手。这个应用程序的思路是,通过REST API,你能够创建和管理鱼类以及鱼缸,随后还可以把鱼添加到鱼缸里。

在本文中,我不会涉及将鱼添加到鱼缸的功能,因为我打算在另一篇文章里专门讨论这一关系相关的内容。

代码

我不打算在此处展示代码,不过在相关的GitHub仓库中可以找到。

依赖

借助Spring Initializr(https://start.spring.io/ )来启动Spring Boot应用程序通常会更简便。我假定你知道如何使用它。

针对这个项目,添加Spring Web和Spring Data MongoDB作为依赖,然后创建项目。

我还引入了Lombok,以此减少重复代码的编写量。

包结构

依据我正在构建的应用程序的特性,我可能会选择基于组件类型(例如controllersservicesrepositories)或者业务领域来构建包结构。

由于这是一个仅包含两个业务领域(鱼类和鱼缸)的小型应用程序,我将基于这些领域创建如下结构:

fishes
  FishController
  FishService
  FishRepository
fishtanks
  FishTankController
  FishTankService
  FishTankRepository

可以看出,我采用了标准的分层方式,包含控制器层、服务层和仓储层。

API端点

控制器为各自的API提供创建(Create)、读取(Read)、更新(Update)和删除(Delete),即CRUD端点。

Entities和Documents

如果你熟悉JPA和像Postgres这样的SQL数据库,那么对于Entitiesrepositories就不会陌生。

在No-SQL数据库中,表被集合所取代,表中的行被Documents所取代。

这就意味着,我们在No-SQL数据库中的存储库与SQL数据库中的略有不同。

由于No-SQL数据库能够处理任何结构,Entities(或者Documents)就变成了简单的普通Java对象(POJOs)。这意味着在我们的示例应用程序中,可以这样创建Entities

aquarium/fishes/Fish.java

@Setter
@Getter
@Document("fishes")
@NoArgsConstructor
public class Fish {

  @Id
  public UUID id;

  public String type;

  public Fish(String type) {
      this.id = UUID.randomUUID();
      this.type = type;
  }
 ...
}

这里有几点需要留意:

  • 我们定义的是@Document而非@Entity,它接收集合的名称。
  • 我使用@Id(这是可选的,因为如果不提供,MongoDB会自行添加)而不是@mongoId,这样我就能自行管理UUID类型的Id。
  • 我倾向于使用Lombok(例如@Getter)来减少样板代码。

我们现在可以用类似的方式创建鱼缸类:

aquarium/fishtanks/FishTank.java

@Setter
@Getter
@Document("fish tanks")
@NoArgsConstructor
public class FishTank {

    @Id
    public UUID id;

    public String name;

    public FishTank(String name) {
        this.id = UUID.randomUUID();
        this.name = name;
    }

    @Override
    public String toString() {
        return String.format(
                "FishTank[id=%s, type='%s']",
                id.toString(), name);
    }
}

Repositories

现在我们已经有了文档,那么该如何访问它们呢?

我来展示一下我们的存储库的变化。以鱼类的Repositories为例:

public interface FishRepository extends MongoRepository<Fish, UUID> {

    public List<Fish> findAll();

    public Optional<Fish> findFirstById(UUID id);

    public Optional<Fish> findFirstByType(String type);
}

可以看到,它几乎和我们在SQL数据库中看到的Repositories类型一样。唯一的区别在于,这个接口扩展的是MongoRepository,而不是CrudRepository。 我会把一对多和其他映射相关的内容留到另一篇文章中讲解,所以目前我们只能进行鱼类和鱼缸的创建与管理。

Application properties

当然,在使用任何数据库时,你必须告诉应用程序如何连接到它。我们通过应用程序属性来实现这一点,就像在使用SQL数据库时一样。

我更喜欢使用YAML文件作为Spring Boot的属性文件,因此我的配置如下所示(记得用你自己的值替换< >字段):

resources/application.yml

spring:
  application:
    name: aquarium-with-mongo-db

  data:
    mongodb:
      host: localhost
      port: 27017
      database: aquarium
      username: my-app-user
      password: <password>

稍后我会在讲到配置文件时再回到这个问题。

Controllers 和 services

现在我们可以像在SQL数据库中一样添加我们的Controllersservices。我不打算在这里展示它们,因为它们在GitHub仓库中可用。

单测

完成代码(或克隆我的仓库)然后在你的IDE中运行应用程序。如果你仍然在对primary进行端口转发,应用程序应该能够启动。

然后你可以使用这些curl命令进行测试:

curl localhost:8080/api/v1/fishes -H "Content-Type: application/json" -d '{"type": "guppy2"}'
curl localhost:8080/api/v1/fish-tanks -H "Content-Type: application/json" -d '{"name": "big one"}'
curl localhost:8080/api/v1/fishes
curl localhost:8080/api/v1/fish-tanks

现在,如果你打开你的Compass客户端并刷新aquarium数据库,你应该会看到两个集合:fishesfish tanks。在这些集合中,你会看到你创建的fishesfish tanks

最后一步

目前,我们已经有一个Spring Boot应用程序连接到了运行在Kubernetes集群中的MongoDB。现在,还需要完成最后一步,也就是把Spring Boot应用程序也部署到Kubernetes集群中。

要达成这个目标,我们需要完成以下几个步骤:

  1. 创建一个包含所有依赖项的可执行JAR文件。
  2. 基于这个JAR文件创建一个Docker镜像。
  3. 将镜像上传到我们的Docker仓库。
  4. 创建一个部署清单文件。
  5. 将部署清单应用到Kubernetes集群。

因为我使用的是Kind,所以第3步可以用一个简单的加载步骤替代,这样就不需要使用Docker仓库了。

配置文件

在创建JAR文件之前,创建两个Spring Boot配置文件是很有帮助的,这样我们就能在不同的模式下运行应用程序:一种是当前的连接模式(在集群外运行),另一种是在Kubernetes集群中运行。我们要创建的两个Spring Boot配置文件如下:

  • connected:用于在集群外运行应用程序时。
  • local-cluster:用于在集群内运行应用程序时。

第一种connected模式就是我们目前正在使用的运行模式。这意味着我们可以直接把application.yml(或者application.properties)文件复制一份,重命名为application-connected.yml。然后,在JVM命令行中添加一个JVM参数:

-Dspring.profiles.active=connected

对于local-cluster配置文件,我们也采取类似的复制操作,不过这次需要做一些修改。

...
  data:
    mongodb:
      host: my-mongo-db-svc.mongo.svc.cluster.local
      port: 27017
...

通过使用DNS名称,我们可以连接到正确的pod。需要注意的是,由于在pod中设置的DNS搜索规则,my-mongo-db-svc.mongo.svc.cluster.local中的部分名称(例如my-mongo-db-svc.mongo.svc)可以省略。这使得我们能够将应用程序部署到不同的集群中,并且依然保证其正常可用。

创建镜像

现在,我们来看看如何创建镜像。由于我在GitHub上的项目是基于Gradle构建的,在根项目文件夹中,使用以下命令就可以创建一个JAR文件:

gradle build

需要注意,gradle.build文件中添加了以下内容,以确保清单指向主应用程序文件:

gradle.build

jar {
    manifest {
        attributes "Main-Class": "com.requillion_solutions.aquarium.AquariumWithMongoDbApplication"
    }
}

执行上述命令后,会生成一个JAR文件:build/libs/aquarium-with-mongo-db-0.0.1-SNAPSHOT.jar

要创建Docker镜像,我们还需要一个Dockerfile文件,内容如下:

Dockerfile

FROM openjdk:17.0.2-slim-buster
RUN addgroup --system spring && useradd --system spring -g spring
USER spring:spring
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080

这个Dockerfile从Java 17基础镜像开始构建(这样做是为了避免与Lombok相关的问题),接着添加一个新用户(spring),这样我们的应用程序就不会以root身份运行。然后将JAR文件复制到镜像中,并设置入口点以运行应用程序。

使用以下命令来创建Docker镜像:

docker build -t aquarium.

如果你使用的是Kind环境,可以直接将镜像加载到Kubernetes集群中:

kind load docker-image aquarium

完成上述步骤后,我们就可以准备创建部署清单,以便在集群中运行应用程序了。

部署清单

现在,我们已经将Docker镜像加载到Kubernetes集群中,接下来就可以使用部署清单进行部署。创建以下文件:

k8s/deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aquarium
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: aquarium
  template:
    metadata:
      labels:
        app: aquarium
    spec:
      containers:
      - name: aquarium
        image: aquarium
        imagePullPolicy: IfNotPresent
        ports:
          - containerPort: 8080
        env:
          # 注意,以下环境变量在Spring读取时会转换为名为spring.profiles.active的属性覆盖
          - name: SPRING_PROFILES_ACTIVE
            value: local-cluster
---
apiVersion: v1
kind: Service
metadata:
  name: aquarium
  namespace: default
spec:
  selector:
    app: aquarium
  type: NodePort
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 30080

这里有几点需要留意:

  • 应用程序部署在默认命名空间(当未指定命名空间时,会使用该命名空间)。
  • 副本数量设置为1个。
  • 只有在镜像不存在的情况下才会拉取镜像(因为我们之前已经将镜像加载到集群中)。
  • 配置文件设置为local-cluster
  • 创建了一个服务,将应用程序的8080端口映射到开发机器上的30080端口。

现在,可以通过以下命令进行部署:

kubectl apply -f k8s/deployment.yml

部署完成后,使用以下命令检查是否成功启动:

kubectl get pods

部署成功后,我们可以使用之前测试API时用过的curl命令,只是将端口改为30080:

curl localhost:30080/api/v1/fishes -H "Content-Type: application/json" -d '{"type": "guppy2"}'
curl localhost:30080/api/v1/fish-tanks -H "Content-Type: application/json" -d '{"name": "big one"}'
curl localhost:30080/api/v1/fishes
curl localhost:30080/api/v1/fish-tanks

你还可以在Compass UI中查看新生成的文档(记得要确保端口转发仍然有效)。

总结

在本文中,我详细分享了如何在Kind Kubernetes集群中安装MongoDB,并将其集成到Spring Boot应用程序里的实操经验。

这个示例操作相对基础,主要是为了给大家演示整个流程。在实际应用场景中,还有许多关键因素需要着重考虑,比如安全性如何保障、怎样进行数据备份以及故障切换机制该如何设置等。

在后续的文章里,我还会进一步展示如何处理文档之间的关系,帮助大家更深入地掌握相关知识。

通过这个示范,我希望能让大家直观地看到,No-SQL数据库与Kubernetes和Spring Boot结合使用是非常便捷的。

衷心希望你喜欢这篇文章,并且在阅读过程中,哪怕只是收获了一点点新的知识,从而让自己的技能有所提升,我也会感到十分欣慰。

如果你觉得这篇文章对你有所帮助,不妨点个赞。这不仅能让我清楚哪些内容真正对大家有价值,也能为我今后确定写作主题提供方向。要是你有任何建议或者想法,欢迎在评论区畅所欲言,期待与你的交流!

原文阅读 https://medium.com/@martin.hodges/my-experience-adding-a-mongodb-no-sql-database-to-my-kubernetes-cluster-f43fe72fa0ba

文章来源: https://study.disign.me/article/202509/14.kubernetes-cluster-mongodb.md

发布时间: 2025-02-26

作者: 技术书栈编辑