65.9K
CodeProject 正在变化。 阅读更多。
Home

Docker 概览

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (17投票s)

2017 年 3 月 8 日

CPOL

17分钟阅读

viewsIcon

24356

Docker 概览,附带实际示例

引言

不久前,我参与了一个使用该技术栈的项目。

  • Akka HTTP:(实际上我们使用的是 Spray.IO,但为了本文的讨论目的,它基本上是相同的)。对于不了解 Akka HTTP 的人来说,它是一个简单的、基于 Akka 的框架,也能够暴露 REST 接口与 actor 系统进行通信。
  • Cassandra 数据库:Apache Cassandra 是一个免费开源的分布式数据库管理系统,旨在处理大量跨多个通用服务器的数据,提供高可用性且没有单点故障。Cassandra 为跨多个数据中心的集群提供了强大的支持,通过异步的无主节点复制,为所有客户端提供低延迟操作。 它是一个多节点集群。

 

这项测试很麻烦,而且我们总是在互相干扰,因为我们都在共享一个数据库。我们真正需要的是我们自己的 Cassandra 集群实例, 正如你所能想象的,为了满足每个开发人员自己的测试需求而启动一个 5 节点集群的虚拟机有点过分。所以我们最终拥有了一些专用的测试环境,运行 5 个 Cassandra 节点。说实话,这仍然是一个麻烦事。

这让我开始思考,也许我可以使用 Docker 来帮助我,也许我可以在 Docker 容器中运行 Cassandra,甚至可以在 Docker 容器中运行我自己的、使用 Cassandra 的代码,然后将我的 UI 指向运行在 Docker 中的 Akka HTTP REST 服务器。嗯mmm

我开始深入研究,当然,这完全是可能的 (否则我现在就不会写这篇文章了)。

这对于 Codeproject 来说绝对不是一件新事,有很多关于 Docker 的文章, 但我从来没有找到一篇谈论 Cassandra 的,所以我想,为什么不写一篇呢。

 

代码在哪里?

代码可以在这里找到: https://github.com/sachabarber/DockerExamples

 

 

一些 Docker 基础知识

本节将概述 Docker 的一些基本概念。

 

图像

也许理解 Docker 最重要的概念是镜像/镜像层,以及它们如何与容器相关。 Docker 基于分层镜像的理念。每个镜像都是在其下方的镜像之上的新一层。

例如,请看下图

 

这里有 3 个镜像在起作用。

  • Ubuntu
  • Emacs
  • Apache

这些层中的每一层都将建立在下面一层的基础上。我说的建立在基础上是什么意思?很简单,它将添加/删除/修改 Docker 文件系统上的文件。正是通过以这种方式修改 Docker 文件系统,我们才能够构建非常复杂的镜像。这个例子将最终得到一个容器 (可以作为新镜像使用),它能够运行 Ubuntu 上的 Apache + Emacs。

 

好吧,这很酷,但这些镜像从哪里来?

正如我上面提到的, Docker 拥有一个充满活力的社区支持,它还有一个基础镜像的云存储库,由 Docker 或社区创建。

例如,假设我想使用 Cassandra,我只需转到 https://hub.docker.com/ 并搜索 cassandra,这将给我一个类似这样的页面。

 

点击查看大图 

在这种情况下,有一个来自 Cassandra 维护者的官方版本,但有时你也可以期望这是社区版本的。

在某些情况下,如果有一个官方镜像可用,你可能会得到一些很好的文档,告诉你诸如

  • 暴露了哪些端口
  • 使用了哪些环境变量/可能使用哪些环境变量
  • 如何链接容器

 

卷 (Volumes)

偶尔,你使用的某个软件会想使用某个目录来实现某些目的,最常见的是配置或日志记录。我们希望将这个目录暴露在容器之外,这样它就可以映射到主机上的某个物理位置,甚至可能在容器之间共享。

 

幸运的是, Docker 能够满足你的需求,它允许你创建一个从容器路径到主机路径的映射。你可以在此链接中阅读更多关于此的信息: 数据卷

以下是官方 Docker 文档关于卷的说法:

数据卷是一个或多个容器内经过特殊指定的目录,它会绕过 Union 文件系统。数据卷为持久化或共享数据提供了几个有用的功能。
卷在创建容器时进行初始化。如果容器的基础镜像在指定挂载点包含数据,则在新卷初始化时,现有数据会被复制到新卷中。(请注意,当挂载主机目录时,这不适用)。

  • 数据卷可以在容器之间共享和重用。
  • 对数据卷的更改会直接进行。
  • 当你更新镜像时,对数据卷的更改不会被包含在内。
  • 即使容器本身被删除,数据卷也会持久存在。
  • 数据卷旨在持久化数据,独立于容器的生命周期。因此,Docker 在删除容器时绝不会自动删除卷,也不会“垃圾回收”不再被容器主机引用的卷。

创建一个将从容器映射到主机文件系统路径的卷的示例如下:

$ docker run -d -P --name web -v /src/webapp:/webapp training/webapp python app.py

此命令将主机目录 /src/webapp 挂载到容器内的 /webapp。如果路径 /webapp 已存在于容器镜像中,则 /src/webapp 挂载会覆盖但不会删除预先存在的内容。一旦卸载挂载,内容就会再次可用。

这与 mount 命令的预期行为一致。container-dir 必须始终是一个绝对路径,例如 /src/docs。host-dir 可以是绝对路径或名称值。如果你为 host-dir 提供一个绝对路径,Docker 将绑定挂载到你指定的路径。如果你提供一个名称,Docker 将创建一个具有该名称的命名卷。

 

环境变量 (Environment Variables) 

有些软件也可能设置使用环境变量。幸运的是,Docker 支持这一点,无论是通过 Docker 命令行还是更丰富的 Docker Compose 文件语法,我们将在后面看到更多。

这是从 Docker 命令行设置环境变量的一个例子,我们将在后面看到一个 Docker Compose 文件示例。-e CASSANDRA_BROADCAST_ADDRESS=10.42.42.42 正在设置一个环境变量,该变量将被传递给此处的镜像创建的容器。

$ docker run --name some-Cassandra -d -e CASSANDRA_BROADCAST_ADDRESS=10.42.42.42 -p 7000:7000 cassandra:tag

 

 

链接 (Linking)

Docker 带有这个链接容器的想法。但你为什么要这样做?所以,让我们想象一下,我们有一个网站,它可以在 Docker 容器中运行,并且它与也可以在 Docker 容器中运行的 MySQL 数据库进行通信。我们可以先启动 MySQL 数据库容器,获取它的 IP 地址,然后以某种方式修改网站的部署,让它知道数据库容器的 IP 地址,或者我们可以使用 Docker 链接。

 

这是从 Docker 命令行设置环境变量的一个例子,我们将在后面看到一个 Docker Compose 文件示例。--link some-cassandra:cassandra 将这个新的容器创建请求链接到一个名为“cassandra”的现有容器。

$ docker run --name some-cassandra2 -d --link some-cassandra:cassandra cassandra:tag

 

简而言之, Docker 链接允许你表达一个容器到另一个容器的依赖关系,这可以通过命令行或 docker-compose 来完成。本文将讨论 docker-compose 机制。

链接的另一个功能是你可以运行交互式命令,比如某种 SQL 命令行针对现有容器。例如,Apache Cassandra 自带一个名为 CQL 的命令行实用程序,你可以用它来执行数据库命令行操作。下面是一个针对已运行 Apache Cassandra 的容器运行它的示例。

$ docker run -it --link some-cassandra:cassandra --rm cassandra cqlsh cassandra

其中 some-cassandra 是你原来的 Cassandra 服务器容器的名称。

 

网络 (Networks)

很少有软件不需要暴露端口或 IP 地址来接受用户输入。幸运的是,这对于 Docker 来说完全是可能的。事实上, Docker 提供了非常丰富的网络功能,很抱歉,这超出了本文的范围。我们将研究如何从容器向主机暴露端口。但是,要进行更深入的探讨,我强烈建议你阅读更多关于这个主题的内容 (这是一个很大的主题): https://docs.dockerd.com.cn/engine/userguide/networking/

 

Dockerfile

Docker 可以通过读取 Dockerfile 中的指令自动构建镜像。Dockerfile 是一个文本文件,其中包含用户可以在命令行上调用以组装镜像的所有命令。使用 docker build,用户可以创建一个自动构建,该构建连续执行多个命令行指令。

 

Docker Compose

虽然 Docker 命令行允许你几乎用容器/镜像做任何事情,但有时你寻找的是对你的软件栈更集中的描述。比如:

  • 我有一个网站
  • 它有 2 个它依赖的数据库
  • 我想把日志暴露在这里和那里
  • 我需要这些端口开放

你想一次性启动所有这些。听起来很理想。幸运的是, Docker 可以通过一个叫做 Docker Compose 的东西来实现这一点,它允许我们在一个文件中描述我们所有的需求,并一次性启动所有这些。我们将在文章的其余部分看到 3 个这方面的例子。

 

 

3 个示例

好吧,我现在知道有一个地方可以获取镜像了,这很好,但如何让我的软件与这些镜像之一一起运行呢?

我们将通过几个示例来探讨这个问题,如下所示。

我决定将示例分为 3 类,从非常简单到中等到复杂。但是,我们对所有示例使用的原则都是相同的,我们只是每次都稍微增加一点示例。

 

但在我们开始示例之前,有几点是它们共同的:

 

Akka Http

它们都使用 Akka HTTP,正如我上面所说的,它是一个基于 Akka 的 REST API。一个骨架的 Akka Http 应用程序可能看起来像这样 (这是一个带有单个默认 GET 路由)

package SAS

import akka.actor.ActorSystem
import akka.event.Logging
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import com.typesafe.config.ConfigFactory


object DockerAkkaHttpMicroService {
  implicit val system = ActorSystem()
  implicit val executor = system.dispatcher
  implicit val materializer = ActorMaterializer()

  def main(args: Array[String]): Unit = {
    val config = ConfigFactory.load()
    val logger = Logging(system, getClass)

    val routes =
      get {
        pathSingleSlash {
          complete {
            "Hello, World from Code!"
          }
        }
      }

    Http().bindAndHandle(routes, config.getString("http.address"), config.getInt("http.port"))
  }
}

事实上,这就是下面第一个示例的全部代码库。如果有人在支持 Scala/SBT 的编辑器中打开示例 1 的源代码并运行它,然后使用类似 Postman 的工具来测试路由,我们将看到类似这样的结果。

 

点击查看大图 

所以,这就是我们期望一个运行中的 Akka Http 应用程序能够做到的,基本上是响应一个可行的 REST 调用。

 

Scala / SBT

我正在使用 Scala/SBT 堆栈,因此我将使用 SBT 来构建本文的代码。

 

Fat Jar

我还创建了一个 fat Jar。这意味着,一旦添加了所有依赖项,最终结果将是一个 .jar 文件。示例 1 的 SBT 文件看起来像这样,其他的也类似,只是依赖项不同。

name := "SimpleAkkaHttpService"
version := "1.0"
scalaVersion := "2.11.7"
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
assemblyJarName in assembly := "SimpleAkkaHttpService.jar"

libraryDependencies ++= {
  val akkaStreamVersion = "1.0"
  Seq(
    "com.typesafe.akka" %% "akka-actor" % "2.3.12",
    "com.typesafe.akka" % "akka-stream-experimental_2.11" % akkaStreamVersion,
    "com.typesafe.akka" % "akka-http-core-experimental_2.11" % akkaStreamVersion,
    "com.typesafe.akka" % "akka-http-experimental_2.11" % akkaStreamVersion
  )
}

Revolver.settings

 

Dockerfile

每个示例都期望运行我自己的代码在一个容器中。

那么,这究竟是如何发生的呢?我知道我们谈到了分层和能够基于其他镜像的分层进行构建。是的,完全正确,正是这样,我们使用一个基础镜像,然后在其上添加文件。

在我的例子中,dockerfile 包含了通过在一个现有镜像之上构建新层来构建我自己的容器 (即我自己的软件) 的新镜像的方法。

在我的例子中,基础镜像需要几个东西:

  • Scala
  • SBT

再次, DockerHub 能帮到你。 我们来看看示例 1 的 dockerfile。

FROM hseeberger/scala-sbt
MAINTAINER sacha barber <sacha.barber@gmail.com>
ENV REFRESHED_AT 2017-07-02

ADD . /root
WORKDIR /root

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "SimpleAkkaHttpService.jar"]

这是什么意思?

 

好吧,如果你分解它,它实际上是相当易懂的,这些都是标准的 Dockerfile 命令。

  1. FROM 使用基础镜像的名称 (scala + SBT),所以这个来自 DockerHub https://hub.docker.com/r/hseeberger/scala-sbt/
  2. MAINTAINER
  3. ENV REFRESHED AT 日期
  4. ADD 我们将文件添加到基础镜像中,我们使用根目录。
  5. WORKDIR 我们将工作目录设置为根目录。
  6. EXPOSE 我们希望从我们正在构建的新容器中暴露端口 8080 (这是我们的 Akka Http 应用暴露的)。
  7. ENTRYPOINT 我们将其设置为启动容器时运行的应用程序以及它将运行的命令,在这种情况下,java 运行我们的 fat jar。

 

所有示例都有一个非常相似的 dockerfile,所以我将不再重复介绍,只是想详细介绍一个。

 

我们还使用了 docker 工具包的另一个部分,即另一个名为“docker-compose”的命令行工具。docker-compose 用于组装你希望容器化的越来越复杂的互连设置。

有一个约定,docker-compose 文件应该命名为“docker-compose.yml”。

这是第一个示例的完整 docker-compose.yml 文件。

webservice:
  build: .
  ports:
    - "8080:8080"

这是什么意思?

与以前一样,如果我们一步一步地看,它很容易理解。它将 Akka Http REST 服务命名为“webservice”,并使用当前位置的 dockerfile 在容器中启动 Akka Http Web 服务,并将其容器的 8080 端口暴露给主机的 8080 端口。

如果我们在这三个文件 SimpleAkkaHttpService.jar/Dockerfile/这个 docker-compose.yml 所在的目录中运行这个命令行

docker-compose up --build

然后我们可以再次尝试访问网站,看看它是否有效。

 

 

点击查看大图 

哈,它奏效了。

我知道你们都在想我只是复制了上面的镜像,是的,我复制了,但请相信我,这是有效的。

完整的说明在 Git 仓库的 README.md 文件中。

信不信由你,有了这些一点点的知识,我们就能够构建非常复杂的设置,正如我们在下面的 3 个示例中看到的。

 

简单的 Akka HTTP 示例

这与上面相同,我们只是使用了第一个示例为其他示例铺平了道路。下次我们将在此基础上进行扩展,并添加对 MySQL 的依赖。

 

简单的 Akka HTTP + MySQL 示例

这个示例将建立在我们刚才所做的工作之上,主要区别在于这一次我们将运行一个 Akka Http REST API,它将与单个 MySQL 服务器通信,并获取 MySQL 服务器中一些可用的静态数据。这将引入 3 个新的 Docker Compose 概念,即:

  • 环境变量
  • 链接
  • 多个服务同步启动

 

这是我们要尝试运行的实际代码。

package SAS

import akka.actor.ActorSystem
import akka.event.Logging
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import com.typesafe.config.ConfigFactory
import java.sql.{ResultSet, Connection, DriverManager}


object DockerAkkaHttpMicroServiceUsingMySql {
  implicit val system = ActorSystem()
  implicit val executor = system.dispatcher
  implicit val materializer = ActorMaterializer()

  def main(args: Array[String]): Unit = {

    val config = ConfigFactory.load()
    val logger = Logging(system, getClass)


    def getSomeMySqlData() : String = {
      // connect to the database named "mysql" on port 3306 of localhost
      val url = "jdbc:mysql://db:3306/mysql"
      val driver = "com.mysql.jdbc.Driver"
      val username = "root"
      val password = "sacha"
      var connection:Connection = null
      var returnVal=""
      try {
        Class.forName(driver)
        connection = DriverManager.getConnection(url, username, password)
        val statement = connection.createStatement
        val rs = statement.executeQuery("SELECT table_name, table_schema FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';")

        realize(rs).take(10).foreach(v=>
        {
          val valuesList = v.values.toList
          val tableName = valuesList(0)
          val tableSchema = valuesList(1)
          returnVal = returnVal + s"tableName= $tableName, tableSchema= $tableSchema\r\n"
        })
      } catch {
        case e: Exception => e.printStackTrace
      }
      connection.close
      returnVal
    }


    def buildMap(queryResult: ResultSet,
                 colNames: Seq[String]): Option[Map[String, Object]] =
      if (queryResult.next())
        Some(colNames.map(n => n -> queryResult.getObject(n)).toMap)
      else
        None

    def realize(queryResult: ResultSet): Vector[Map[String, Object]] = {
      val md = queryResult.getMetaData
      val colNames = (1 to md.getColumnCount) map md.getColumnName
      Iterator.continually(buildMap(queryResult, colNames))
        .takeWhile(!_.isEmpty).map(_.get).toVector
    }

    val routes =
      get {
        pathSingleSlash {
          complete {
            getSomeMySqlData()
          }
        }
      }

    Http().bindAndHandle(routes,
      config.getString("http.address"),
      config.getInt("http.port"))

  }
}

注意与 MySQL 数据库的连接使用了这一行:

val url = "jdbc:mysql://db:3306/mysql"

 

这从哪里来?好吧,如果我们检查实际的 Docker Compose 文件,我们可能会明白。这是这个示例的 Docker Compose 文件。

version: '2'

services:
    db:
        image: mysql:latest
        volumes:
            - db_data:/var/lib/mysql
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: sacha
            MYSQL_USER: sacha
            MYSQL_PASSWORD: sacha
        ports:
            - "3306:3306"
    webservice:
        build: .
        ports:
            - "8080:8080"
        depends_on:
            - db
volumes:
    db_data:

这里有几件事情要谈,即:

 

卷 (Volumes)

“db”项目 (MySQL,参见 mySql 镜像 (再次来自 DockerHub)) 使用卷将 MySQL 世界映射到主机容器。

环境变量

“db”项目 (MySQL,参见 mySql 镜像 (再次来自 DockerHub)) 要求我们设置一些环境变量。我们已经看到如何从命令行设置它们,但上面显示了如何在 docker-compose.yaml 文件中设置它们。

链接 (Linking)

正如我们之前看到的,这次我们不仅要运行 Akka Http REST API,还要运行 MySQL 数据库,每个都在自己的容器中。显然,我们希望从 Akka Http API 代码连接到 MySQL 数据库。我们在 Akka Http 代码库中看到过类似这样的内容:

val url = "jdbc:mysql://db:3306/mysql"

所以,一旦容器启动 (docker-commpose.yaml 文件中有依赖关系声明,参见 depends_on),数据库就可以通过“db”名称在它暴露的“3306”端口上访问。所以我们只需在连接字符串中使用它。而且,由于 Docker 为我们处理了依赖关系,所以这是可以的,我们知道“db”将在 Akka Http REST 服务之前启动。

 

 

多个服务同步启动

这是我们第一次看到多个服务同时启动。这次我们有两个:

  • db:MySQL 服务
  • webservice:Akka Http 服务

每个都有自己针对容器需求的设置。

与以前一样,我们需要确保 Dockerfile/docker-compose.yaml 和 SimpleAkkaHttpServiceWithMySql.jar (如 Dockerfile 和 build.sbt 文件中所指定) 位于同一个目录中。

然后发出此命令:

docker-compose up --build

如果一切顺利,我们应该看到类似这样的内容:

点击查看大图

我们现在还可以做的另一件事是打开另一个命令行并发出此命令来检查正在运行的容器。

点击查看大图

 

现在我们已经确信一切都已启动并正在运行,我们可以使用 postman 来测试 REST API。

 

看看它是如何从正在运行的 MySQL 数据库中获取这些元数据的。是不是很酷?我们现在能够设置相当复杂的软件需求,我刚刚介绍的主题应该能很好地缓解你的开发痛苦。对我来说,这肯定是非常有用的。

 

简单的 Akka HTTP + Cassandra 可伸缩集群示例

我想要介绍的最后一个示例在某种程度上是基于上面示例 2 的,只有一个新的概念需要介绍,但它是个不错的概念。但在我们深入之前,我想先告诉你为什么我选择 Apache Cassandra 作为最终示例。

Apache Cassandra 是一个多节点分布式数据库,当新节点可能会进出所谓的“cassandra ring”时。该环实际上是一个分布式哈希表,但 Cassandra 还有一个更有趣的功能,即它使用 GOSSIP 协议来确保每个节点都知道并相互通信。它是如何收集共识的,你需要达成共识才能认为一个数据块确实已持久化。

这张图可能有所帮助

点击查看大图

某些表/查询的设计可能在拥有正确数量的节点时才有效 (通常是奇数,以便能够达成共识)。

与以前一样,这是我们为这个示例尝试运行的代码。

package SAS

import akka.actor.ActorSystem
import akka.event.Logging
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import com.datastax.driver.core.{Session, ResultSet, Cluster}
import com.typesafe.config.ConfigFactory
import scala.collection.JavaConversions._


object DockerAkkaHttpMicroServiceUsingCassandra {
  implicit val system = ActorSystem()
  implicit val executor = system.dispatcher
  implicit val materializer = ActorMaterializer()

  def main(args: Array[String]): Unit = {

    val config = ConfigFactory.load()
    val logger = Logging(system, getClass)

    def getSomeCassandraData() : String = {

      var cluster:Cluster = null
      var returnVal="getSomeCassandraData\r\n"
      try {
        cluster = Cluster.builder()
          .addContactPoint("cassandra-1")
          .withPort(9042)
          .build()
        val session = cluster.connect()


        returnVal = returnVal + "Before keyspace\r\n"
        createKeySpace(session)
        returnVal = returnVal + "After keyspace\r\n"

        createTable(session)
        returnVal = returnVal + "After table\r\n"

        seedData(session)
        returnVal = returnVal + "After seeding\r\n"

        returnVal = returnVal + "\r\n"
        val rs = session.execute("select category, points, lastname from dockercassandra.cyclist_category")

        val iter = rs.iterator()
        while (iter.hasNext()) {
          if (rs.getAvailableWithoutFetching() == 100 && !rs.isFullyFetched())
            rs.fetchMoreResults()
          val row = iter.next()
            returnVal = returnVal + s"category= ${row.getString("category")}, " +
              s"points= ${row.getInt("points")}, lastname= ${row.getString("lastname")}\r\n"

        }
      }
      catch {
        case e: Exception => {
          e.printStackTrace
          returnVal = e.printStackTrace().toString
        }
      } finally {
        if (cluster != null) cluster.close()
      }
      returnVal
    }

    def createKeySpace(session : Session) : Unit = {
      session.execute(
        """CREATE KEYSPACE IF NOT EXISTS DockerCassandra
            WITH replication = {
                 'class':'SimpleStrategy',
                 'replication_factor' : 3
               };""")
    }


    def createTable(session : Session) : Unit = {
      session.execute(
        """CREATE TABLE IF NOT EXISTS dockercassandra.cyclist_category (
             category text,
             points int,
             lastname text,
             PRIMARY KEY (category, points))
          WITH CLUSTERING ORDER BY (points DESC);""")
    }

    def seedData(session : Session) : Unit = {
      session.execute("TRUNCATE dockercassandra.cyclist_category;")
      session.execute("INSERT INTO dockercassandra.cyclist_category (category, points, lastname) VALUES ('cat1', 15, 'barber');");
      session.execute("INSERT INTO dockercassandra.cyclist_category (category, points, lastname) VALUES ('cat2', 25, 'smith');");
      session.execute("INSERT INTO dockercassandra.cyclist_category (category, points, lastname) VALUES ('cat3', 45, 'evans');");
      session.execute("INSERT INTO dockercassandra.cyclist_category (category, points, lastname) VALUES ('cat4', 65, 'harth');");
    }

    val routes =
      get {
        pathSingleSlash {
          complete {
            getSomeCassandraData()
            //"getting cass"
          }
        }
      }

    Http().bindAndHandle(routes,
      config.getString("http.address"),
      config.getInt("http.port"))

  }
}

 

正如我们之前通过链接所知道的,我们可以使用“cassandra-1”容器并使用其端口。

但是,对于这样一个可能很大的、包含许多节点的数据库,我们如何在 Docker Compose 文件中表达它呢?我们肯定不需要在 Docker Compose 文件中声明 5 个或 7 个或 9 个单独的 Cassandra 服务,对吗?

不,实际上我们不需要, Docker 有一个很好的辅助功能。但我们先来看 Docker Compose 文件,这是它:

version: '2'

services:
    cassandra-1:
        image: cassandra:latest
        command: /bin/bash -c "echo ' -- Pausing to let system catch up ...' && sleep 10 && /docker-entrypoint.sh cassandra -f"
        expose:
            - 7000
            - 7001
            - 7199
            - 9042
            - 9160
       # volumes: # uncomment if you desire mounts, also uncomment cluster.sh
       #   - ./data/cassandra-1:/var/lib/cassandra:rw

    cassandra-N:
        image: cassandra:latest
        command: /bin/bash -c "echo ' -- Pausing to let system catch up ...' && sleep 30 && /docker-entrypoint.sh cassandra -f"
        environment:
            - CASSANDRA_SEEDS=cassandra-1
        links:
            - cassandra-1:cassandra-1
        expose:
            - 7000
            - 7001
            - 7199
            - 9042
            - 9160
        # volumes: # uncomment if you desire mounts, also uncomment cluster.sh
        #   - ./data/cassandra-2:/var/lib/cassandra:rw


    webservice:
        build: .
        ports:
            - "8080:8080"
        depends_on:
            - cassandra-1

这基本上是我们已经看到的 (更多相同的),唯一的区别是我们现在有这个新的容器“cassandra-N”,它通过链接连接到主“cassandra-1”容器,并且它还设置了一个特定于 Cassandra 的环境变量来确保环的形成。

与以前一样,我们需要确保 Dockerfile/docker-compose.yaml 和 SimpleAkkaHttpServiceWithCassandra.jar (如 Dockerfile 和 build.sbt 文件中所指定) 位于同一个目录中。

然后发出此命令:

docker-compose up --build

一切都会顺利进行,如下图所示:

 

点击查看大图

正如我们运行老朋友 postman 时可以看到的那样。

 

点击查看大图

这一切都很好,并且工作正常,但如果我们想要更多节点加入我们的环怎么办?嗯,事实证明 Docker Compose 允许我们这样做,我们只需使用这个魔术命令,注意“scale”部分。

docker-compose scale webservice=1 cassandra-1=1 cassandra-N=4

 

 

 

 

进一步阅读

其他 codeproject 文章

在我看来,这些也是非常重要的阅读材料。

 

结论

一如既往,如果你喜欢你在这里看到的内容,非常欢迎投票/评论。

© . All rights reserved.