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

Kotlin与Docker容器化部署实践

2024-02-132.5k 阅读

Kotlin 基础回顾

Kotlin 是一种现代的编程语言,由 JetBrains 开发,与 Java 兼容,运行在 Java 虚拟机(JVM)上,也可以编译为 JavaScript 或本地代码。

Kotlin 语法特性

  1. 简洁性:Kotlin 语法简洁,例如定义一个简单的函数:
fun add(a: Int, b: Int): Int {
    return a + b
}

还可以使用更简洁的表达式函数体:

fun add(a: Int, b: Int) = a + b
  1. 空安全:在 Java 中,NullPointerException 是常见的错误。而 Kotlin 通过可空类型和安全调用操作符解决了这个问题。
var name: String? = null
// name.length() // 这行代码会报错,因为 name 可能为空
name?.length() // 使用安全调用操作符,当 name 不为空时才调用 length 方法
  1. 扩展函数:可以在不修改类的源代码的情况下为类添加新的函数。
fun String.addSuffix(suffix: String): String {
    return this + suffix
}

val str = "Hello"
val newStr = str.addSuffix(", World!")

Kotlin 面向对象特性

  1. 类与对象:定义一个简单的类:
class Person(val name: String, var age: Int) {
    fun introduce() {
        println("Hi, I'm $name and I'm $age years old.")
    }
}

val person = Person("Alice", 30)
person.introduce()
  1. 继承:Kotlin 中类默认是 final 的,要允许继承需使用 open 关键字。
open class Animal(val name: String) {
    open fun makeSound() {
        println("$name makes a sound.")
    }
}

class Dog(name: String) : Animal(name) {
    override fun makeSound() {
        println("$name barks.")
    }
}

Docker 基础概述

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

Docker 架构

  1. 镜像(Image):Docker 镜像是一个只读的模板,包含了创建 Docker 容器所需的所有文件系统对象,包括操作系统、应用程序及其依赖项。例如,官方的 java 镜像包含了 Java 运行时环境和相关的基础库。可以通过 docker pull 命令从 Docker 仓库拉取镜像,如 docker pull openjdk:11-jre-slim
  2. 容器(Container):容器是镜像的运行实例。一个镜像可以创建多个容器。容器之间相互隔离,它们共享宿主机的内核,但拥有自己独立的文件系统、进程空间等。可以使用 docker run 命令基于镜像创建并启动容器,如 docker run -d --name my-java-container openjdk:11-jre-slim,其中 -d 表示以守护进程模式运行,--name 为容器指定一个名称。
  3. 仓库(Repository):Docker 仓库是集中存放镜像的地方,分为公共仓库(如 Docker Hub)和私有仓库。开发团队可以将自己构建的镜像推送到私有仓库,供内部使用。

Docker 常用命令

  1. 镜像相关命令
    • docker images:列出本地所有镜像。
    • docker pull <image_name>:从仓库拉取镜像。
    • docker build -t <image_tag> <path>:基于 Dockerfile 在指定路径构建镜像,-t 用于指定镜像标签。
  2. 容器相关命令
    • docker ps:列出正在运行的容器,使用 docker ps -a 可以列出所有容器(包括已停止的)。
    • docker start <container_name_or_id>:启动一个已停止的容器。
    • docker stop <container_name_or_id>:停止一个正在运行的容器。
    • docker rm <container_name_or_id>:删除一个容器,容器必须是停止状态才能删除。

Kotlin 应用开发

创建一个简单的 Kotlin Web 应用

我们使用 Kotlin 和 Ktor 框架来创建一个简单的 Web 应用。首先,在项目的 build.gradle.kts 文件中添加 Ktor 依赖:

dependencies {
    implementation("io.ktor:ktor-server-core:2.2.4")
    implementation("io.ktor:ktor-server-netty:2.2.4")
    implementation("io.ktor:ktor-html-builder:2.2.4")
    implementation("io.ktor:ktor-auth:2.2.4")
    implementation("io.ktor:ktor-auth-jwt:2.2.4")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.2.4")
    implementation("io.ktor:ktor-server-content-negotiation:2.2.4")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
    implementation("ch.qos.logback:logback-classic:1.4.5")
}

然后创建一个简单的路由:

import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused.
fun Application.module() {
    routing {
        get("/") {
            call.respondText("Hello, World!", contentType = ContentType.Text.Plain)
        }
    }
}

上述代码创建了一个简单的 Ktor 应用,当访问根路径时,会返回 “Hello, World!”。

构建 Kotlin 项目

在项目根目录下,使用 Gradle 构建项目。如果是在命令行中,运行 ./gradlew build (在 Windows 下是 gradlew build)。构建成功后,会在 build/libs 目录下生成项目的 JAR 文件。例如,假设项目名称为 my-ktor-app,生成的 JAR 文件可能是 my-ktor-app-1.0.0.jar

Kotlin 应用的 Docker 容器化

创建 Dockerfile

在项目根目录下创建一个 Dockerfile,内容如下:

# 使用官方的 OpenJDK 11 运行时镜像
FROM openjdk:11-jre-slim

# 设置工作目录
WORKDIR /app

# 将构建好的 JAR 文件复制到容器中
COPY build/libs/my-ktor-app-1.0.0.jar app.jar

# 暴露应用运行的端口,这里假设 Ktor 应用运行在 8080 端口
EXPOSE 8080

# 定义容器启动时运行的命令
CMD ["java", "-jar", "app.jar"]

上述 Dockerfile 首先基于官方的 openjdk:11-jre-slim 镜像创建一个新的镜像。然后设置工作目录为 /app,将本地构建好的 JAR 文件复制到容器内,并命名为 app.jar。接着暴露 8080 端口,最后定义容器启动时运行的命令,即通过 java -jar 运行 JAR 文件。

构建 Docker 镜像

在项目根目录下,运行以下命令构建 Docker 镜像:

docker build -t my-ktor-app:1.0.0.

其中 -t 选项用于指定镜像的标签,格式为 镜像名称:版本号,最后的 . 表示当前目录,即 Dockerfile 所在的目录。构建过程中,Docker 会根据 Dockerfile 的指令逐步创建镜像,构建完成后,可以通过 docker images 命令查看本地新生成的镜像。

运行 Docker 容器

构建好镜像后,可以运行容器:

docker run -d -p 8080:8080 my-ktor-app:1.0.0

这里 -d 表示以守护进程模式运行容器,-p 选项用于端口映射,将宿主机的 8080 端口映射到容器的 8080 端口。运行命令后,容器会在后台启动,通过浏览器访问 http://localhost:8080 就可以看到之前 Kotlin Web 应用返回的 “Hello, World!”。

多阶段构建优化镜像

多阶段构建原理

多阶段构建允许在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 指令都可以使用不同的基础镜像,并且可以选择性地将前一个阶段的构建产物复制到后续阶段。这样可以有效地减小最终镜像的大小,因为可以在构建阶段使用功能丰富但体积较大的镜像(如包含编译工具的镜像),而在运行阶段使用精简的运行时镜像。

多阶段构建的 Dockerfile 示例

# 第一阶段:构建阶段
FROM gradle:7.5.1-jdk11 AS build
WORKDIR /app
COPY. /app
RUN gradle build --no-daemon

# 第二阶段:运行阶段
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=build /app/build/libs/my-ktor-app-1.0.0.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

在上述 Dockerfile 中,第一阶段使用 gradle:7.5.1-jdk11 镜像,这个镜像包含了 Gradle 和 JDK,用于构建 Kotlin 项目。RUN gradle build --no-daemon 命令在容器内执行 Gradle 构建。第二阶段使用 openjdk:11-jre-slim 镜像,这是一个精简的 Java 运行时镜像。通过 COPY --from=build 指令,将第一阶段构建生成的 JAR 文件复制到运行阶段的镜像中。这样最终生成的镜像只包含运行应用所需的内容,体积大大减小。

容器编排与部署

Docker Compose 基础

Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。通过一个 docker-compose.yml 文件,就可以配置应用程序的服务、网络和卷。例如,假设我们的 Kotlin 应用还依赖一个 PostgreSQL 数据库,docker-compose.yml 文件可以如下编写:

version: '3'
services:
  my-ktor-app:
    image: my-ktor-app:1.0.0
    ports:
      - 8080:8080
    depends_on:
      - db
  db:
    image: postgres:14
    environment:
      POSTGRES_PASSWORD: mysecretpassword
      POSTGRES_USER: myuser
      POSTGRES_DB: mydb
    volumes:
      - db-data:/var/lib/postgresql/data
volumes:
  db-data:

上述 docker-compose.yml 文件定义了两个服务:my-ktor-appdbmy-ktor-app 服务使用之前构建的 my-ktor-app:1.0.0 镜像,并将宿主机的 8080 端口映射到容器的 8080 端口,同时依赖 db 服务。db 服务使用官方的 postgres:14 镜像,设置了数据库的用户名、密码和数据库名,并通过卷挂载将容器内的数据库数据持久化到宿主机。

使用 Docker Compose 启动应用

在包含 docker-compose.yml 文件的目录下,运行以下命令启动应用:

docker-compose up -d

-d 选项表示以守护进程模式运行,Docker Compose 会根据 docker-compose.yml 文件的配置启动所有服务。可以通过 docker-compose ps 命令查看正在运行的服务,通过 docker-compose down 命令停止并删除所有服务。

Kubernetes 简介

Kubernetes 是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。它提供了诸如自动装箱、自我修复、水平扩展等功能。与 Docker Compose 相比,Kubernetes 更适合大规模、生产环境的容器化应用部署。

在 Kubernetes 上部署 Kotlin 应用

  1. 创建 Kubernetes 部署文件(Deployment):创建一个 my-ktor-app-deployment.yml 文件:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-ktor-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-ktor-app
  template:
    metadata:
      labels:
        app: my-ktor-app
    spec:
      containers:
      - name: my-ktor-app
        image: my-ktor-app:1.0.0
        ports:
        - containerPort: 8080

上述文件定义了一个 Kubernetes Deployment,指定副本数为 3,即会启动 3 个 my-ktor-app 容器实例。selector 用于选择要管理的 Pod,template 部分定义了 Pod 的配置,包括使用的镜像和暴露的端口。 2. 创建 Kubernetes 服务(Service):创建一个 my-ktor-app-service.yml 文件:

apiVersion: v1
kind: Service
metadata:
  name: my-ktor-app
spec:
  selector:
    app: my-ktor-app
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
  type: LoadBalancer

这个文件定义了一个 Kubernetes Service,类型为 LoadBalancer,它会将外部流量负载均衡到所有匹配 app: my-ktor-app 标签的 Pod 上。 3. 部署到 Kubernetes 集群:使用 kubectl 命令将 Deployment 和 Service 部署到 Kubernetes 集群:

kubectl apply -f my-ktor-app-deployment.yml
kubectl apply -f my-ktor-app-service.yml

通过 kubectl get pods 可以查看 Pod 的运行状态,通过 kubectl get services 可以查看服务的外部访问地址(如果集群支持外部负载均衡)。

总结常见问题与解决方案

Kotlin 应用在容器中运行异常

  1. 问题:容器启动后,应用无法正常响应请求。
    • 可能原因:端口映射错误或应用内部配置问题。
    • 解决方案:检查 Dockerfile 中的 EXPOSE 指令和 docker rundocker-compose.yml 中的端口映射是否正确。同时,检查 Kotlin 应用的配置文件,确保应用监听的端口与容器设置一致。
  2. 问题:容器启动时提示找不到主类。
    • 可能原因:JAR 文件打包错误或 DockerfileCMD 指令错误。
    • 解决方案:重新检查 Gradle 构建配置,确保 JAR 文件正确打包。同时,检查 Dockerfile 中的 CMD 指令,确保 JAR 文件路径和名称正确。

Docker 镜像构建失败

  1. 问题:拉取基础镜像失败。
    • 可能原因:网络问题或镜像仓库不可访问。
    • 解决方案:检查网络连接,确保可以访问互联网。如果是私有镜像仓库,检查认证配置是否正确。
  2. 问题:构建过程中某个步骤出错,如 Gradle 构建失败。
    • 可能原因:依赖项缺失或构建脚本错误。
    • 解决方案:检查项目的依赖项是否正确配置,特别是在 build.gradle.kts 文件中。同时,检查 Dockerfile 中的构建指令,如 RUN gradle build 是否正确。

Kubernetes 部署问题

  1. 问题:Pod 一直处于 Pending 状态。
    • 可能原因:资源不足或节点问题。
    • 解决方案:使用 kubectl describe pod <pod_name> 查看详细信息,检查是否因为资源不足(如 CPU、内存)导致 Pod 无法调度。如果是节点问题,检查节点的健康状态。
  2. 问题:无法通过 Service 访问应用。
    • 可能原因:Service 配置错误或网络策略限制。
    • 解决方案:检查 my-ktor-app-service.yml 文件中的 Service 配置,确保 selector 和端口设置正确。同时,检查 Kubernetes 网络策略,确保没有限制 Service 的访问。