驯服时间:如何使用XTDB安装和开发
#database #elixir #bitemporality #clojure

previous article中,我们讨论了咬合性的概念以及如何用于解决复杂的建筑问题。

在Marleyspoon上,我们使用XTDB(或简称为简称)用于我们的新订单管理系统(OMS),并发现了许多有关数据库的有趣见解本身,咬合性的概念,以及使用不变的,Bitemoral Database开发项目的方式。

在本文中,我们将主要关注Elixir应用程序中的开发经验(使用XTDB安装,测试),并且我们介绍了有关部署,在群集中运行XT的更多详细信息,并在即将到来的文章中调整了生产。

什么是XTDB

XTDB或跨时间数据库是一个分布式和交易数据库系统,旨在轻松处理复杂和变化的数据。
它基于Bitemoral模型,该模型允许跟踪数据的有效时间和交易时间,从而实现了功能强大且灵活的查询功能。
使用XTDB,开发人员可以使用不可变的数据结构,从而简化开发并提高可靠性。
其图形查询语言Datalog提供了一种强大而表达的方法来导航数据中的关系。

正如我们之前说明的那样,XT有很多好处:

  • bitemoral
  • 支持追溯
  • 基于文档和基于图的文档
  • 灵活的数据模式
  • 未捆绑(可以在许多其他DB和持续解决方案之上部署)
  • 可以在JVM中或通过REST API
  • 使用

正如人们所看到的,XT与大多数广泛使用的SQL和NOSQL数据库完全不同 - 虽然它为处理不变的数据和追溯校正提供了巨大的好处,但它也需要了解其某些实施原则。

我们计划如何使用XTDB

MarleySpoon上,我们将配方和食材的盒子运送到客户。 orders 和我们的订阅模型是整个商品逻辑的骨干,这也反映在我们构建软件的方式上。

订单系统的核心是订单状态机器,尽管各州本质上很简单,但仍有可能存在差异的情况 - 在这种情况下,我们希望有更多选择来调试或还原订购以前的状态以及追溯性纠正其数据并将更改推向依赖子系统。

从传统巨石体系结构到面向服务的体系结构的转变以及新OMS的引入也要求我们在最终的一致性时要更加小心 - Transaction 有效时间在生成的系统中可能有所不同,因此我们可以通过使用我们曾经曾经使用过的持久性堆栈来解决它(例如,有更新的关系DB)。

最初,我们考虑将事务和有效时间列添加到PostgreSQL中以实现BITEMETALITY,因为这似乎是一个简单的解决方案。但是,经过进一步的分析,我们意识到这种方法将为系统设计带来重要的复杂性。特别是,任何外国钥匙都需要考虑到Bitemoral列,这意味着查询需要同时考虑实体关系及其时间上下文。这将需要对数据库架构,查询设计和应用程序代码进行重大更改,并可能导致更高的错误和数据不一致风险。

我们还考虑了满足我们需求的事件采购,但是它将增加偶然的复杂性,需要更改其他服务架构和大量的应用程序级代码更改,以确保捕获所有事件并持续正确。<<<<<<<<<<<<<< /p>

在考虑了系统中实现BITMETAME的各种方法之后,由于其本机对BITMEMEMETOMATIE和GRAPH DATABASE功能的支持,我们决定尝试XTDB。我们设计了围绕XTDB功能的数据模型,并将数据库合并到我们的Elixir应用程序中。

安装XTDB

有多种安装和使用XTDB的方法:

  • 通过简单地将其添加到您的JVM项目中,将其用作JVM依赖性:
        ; deps.edn
        com.xtdb/xtdb-core {:mvn/version "1.21.0"}
  • 在本地机器上使用预构建的XTDB罐
  • 通过Docker image

XTDB是在Clojure编程语言中开发的,从任何Clojure程序运行它非常方便 - 因此,我们决定在Clojure中编写一个简单的应用程序,该应用程序将为我们运行XTDB并提供所有必需的设置。<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< /p>

安装依赖项

我们使用 ASDF 版本管理器安装了Clojure,因为PIN JVM和Clojure版本非常方便:

    # .tool-versions
    openjdk-18
    clojure 1.11.0.1100

下一步是使用deps CLI创建一个新的clojure应用程序 - 所有必要的依赖项均在单个文件(deps.edn)中提供。
XTDB非常模块化,因此我们必须安装PostgreSQL支持,HTTP客户端和服务器,指标和其他工具作为单独的软件包:

    ;; deps.edn
    {:paths ["src"]
     :deps {org.clojure/clojure {:mvn/version "1.11.0"}
            com.xtdb/xtdb-core {:mvn/version "1.21.0"}
            ;; Persistence
            org.postgresql/postgresql {:mvn/version "42.2.18"}
            com.xtdb/xtdb-jdbc {:mvn/version "1.21.0"}
            com.xtdb/xtdb-rocksdb {:mvn/version "1.21.0"}
            ;; HTTP Client
            com.xtdb/xtdb-http-client {:mvn/version "1.21.0"}
            ;; HTTP Server
            com.xtdb/xtdb-http-server {:mvn/version "1.21.0"}}
    }

应用程序入口点和配置

我们为XTDB开发包装器的主要原因之一是使我们能够在Kubernetes环境中运行XTDB群集。我们希望通过通过环境变量允许配置来简化设置过程,而不是依靠外部配置文件。这使我们能够轻松地管理Kubernetes中XTDB的配置,并为管理XTDB群集提供了更大的灵活性。

我们创建了一个 xtdb.clj 是数据库包装器的输入点,并且在那里还具有所有必需的配置:

    (def db-spec
      {:host (System/getenv "POSTGRES_HOST")
       :port (System/getenv "POSTGRES_PORT")
       :dbname (System/getenv "POSTGRES_DB")
       :user (System/getenv "POSTGRES_USER")
       :password (System/getenv "POSTGRES_PASSWORD")})

    (def config
      {:xtdb.http-server/server {:port 3000
                                 :jwks (System/getenv "XTDB_JWKS")} ; auth
       :xtdb.rocksdb/block-cache {:xtdb/module 'xtdb.rocksdb/->lru-block-cache
                                  :cache-size (* 1024 1024 1024)} ; RocksDB cache size
       :xtdb/index-store {:kv-store {:xtdb/module 'xtdb.rocksdb/->kv-store
                                     :db-dir "/tmp/xtdb/indexes"
                                     :checkpointer checkpoint-config
                                     :block-cache :xtdb.rocksdb/block-cache
                                     :metrics {:xtdb/module 'xtdb.rocksdb.metrics/->metrics}}}
       :xtdb.jdbc/connection-pool {:dialect {:xtdb/module 'xtdb.jdbc.psql/->dialect}
                                   :pool-opts {:maximumPoolSize 10}
                                   :db-spec db-spec}
       :xtdb/tx-log {:xtdb/module 'xtdb.jdbc/->tx-log
                     :connection-pool :xtdb.jdbc/connection-pool}
       :xtdb/document-store {:xtdb/module 'xtdb.jdbc/->document-store
                             :connection-pool :xtdb.jdbc/connection-pool}})

完成初始配置后,我们可以提供一个简单的入口点功能,该功能将启动XT节点:

    ;; XT node - hydrated on start
    (def xt-node (atom nil))

    (defn -main
      "Starts a new XTDB node"
      []
      (let [node (xtdb/start-node config)]
        (log/info "Started a new XT node ...")
        (seed/seed node) ; seed data that we need on start
        (xtdb/sync node)
        (log/info "Loaded data into a new XT node")
        (reset! @xt-node node) ; set xt-node with the started node
        node))

    ;; Can be used to run REPL or local XT instance
    ;;
    ;; As running it from CLI can assume passing some command line arguments
    ;; we should accept a list of optional arguments as a function param
    (defn start
      [&args]
      ;; Runs -main function to start a new XT node
      (-main))

在本地机器上运行XT

我们的XT应用程序安装和配置后,我们可以运行clj -X xtdb.core/start以便在本地计算机上启动。
这将在 http://localhost:3000 上启用Web UI和REST API。

连接起来

要在repl中运行XT,我们可以在shell中执行 clj ,鉴于我们在clojure项目的根目录中。

将启动一个新的clojure repl,如果我们想从那里开始XT,就足以使用我们事先实现的功能:

    (in-ns 'xtdb.core) ; switches current namespace to XT wrapper's core namespace
    (-main)

连接到远程节点

XT的另一个重要好处,不幸的是,我们没有足够的探索是,通过使用 http-client 依赖关系,我们能够连接到我们可访问的任何远程节点由http:

    (def remote-node (xt/new-api-client remote-url))
    (xt/submit-tx remote-node [[::xt/put {:xt/id :foo :bar :bar}]]) ; submits a transaction on the remote node

因此,我们可以连接到任何生产XT实例并运行查询 /在此处提交交易。< / p>

REST API

XT非常方便地使用任何基于Clojure或基于JVM的应用程序,但是,对于其他编程语言或其他虚拟机的客户端,我们应该使用B XT’s REST API
XT具有非常丰富的HTTP API,涵盖了其大多数功能(尽管其中有些仅由Clojure/Java客户端可用)

API格式

XT支持多种格式: application/edn,application/json and application/transit+json

由于XT是用clojure编写的,并且它本地支持Clojure的数据类型,因此我们对可用的JSON类型不满意,因此决定尝试EDN尝试一下 - 这样,我们将拥有更多支持的类型:

  • 符号,例如长生不素原子
  • 小数
  • 日期和时间戳

但是,我们遇到了一些问题,将ELIXIR/BEAM VM术语编码为EDN,并且通常是该格式的性能 - 因此,Transit/JSON将是一个改进,除了与常规JSON兼容,并且基本上具有更大的性能,它还具有更高的性能。具有更精确的类型转换。

主要端点

  • get/_xtdb/status - 可以用作XT节点的健康检查
  • get/_xtdb/entity - 从数据库获取一个实体
  • get/_XTDB/QUERY - 执行单个数据质查询
  • POST/_XTDB/submit -tx - 提交数据库交易
  • get/_xtdb/等待-tx - 等待交易在节点上索引

Elixir HTTP客户端

当时我们开始在Marleyspoon应用XT时,没有精灵库完全支持XT的REST API并满足我们的需求,因此我们开始编写自己的HTTP适配器。

雨伞应用

由于OMS是我们尝试XTDB的唯一项目,我们决定将所有Clojure代码以及新的Elixir HTTP移动到同一伞应用程序中:

apps/
  ...
  xtdb/

这样,我们也可以在项目中轻松启动XTDB节点,并从Elixir应用程序中使用它,例如通过从Docker-Compose开始。

http2

xt的REST API支持框架外的HTTP格式的第二个修订版 - 这意味着我们可以在XT客户端(应用程序服务器)和数据库实例之间建立稳定且更具性能的连接。

在生产中运行XTDB群集时,HTTP2可能特别有用。通过使用HTTP2,可以在客户端应用程序和XTDB节点之间建立直接连接,而不是依靠多个实例的负载平衡。由于XTDB在节点之间没有执行平等的状态,因此相同的请求可能会在不同的节点上产生不同的结果 - 但是使用HTTP2消除了问题并确保每个请求的一致结果。

但是,并非每个Elixir的HTTP客户端都支持使用HTTP2发送请求 - 因此,我们必须搜索另一个选项,而不是使用我们在其他项目中广泛使用的HTTPOISON。 我们决定使用Finch,因为除了支持HTTP2之外,它还侧重于性能,并在开箱即用提供遥测支持 - 我们发现这对于跟踪和调试目的非常有用。

与Finch一起使用HTTP2需要一些初始配置,因为我们必须在连接池中具有一个连接:

    # apps/xtdb/lib/application.ex
    children = [
          {
            Finch,
            name: Xtdb.ConnectionPool,
            pools: %{
              default: [count: 1, protocol: :http2, size: 1]
            }
          }
        ]

    Supervisor.start_link(children, strategy: :one_for_one, name: Xtdb.Supervisor)

遥测和OpentElemetry

如前一节所述,使用Finch作为HTTP客户库库是具有无缝遥测集成的好步骤。

Finch提供下一个遥测事件:

  • 请求开始/停止
  • 请求例外
  • 队列开始/停止/异常
  • 连接开始/停止/异常

和其他人,但是出于我们的目的,我们只会在前两个事件中更感兴趣。

为了将XTDB客户端与opentelemetry集成,我们编写了一个简单的模块,该模块观看了Finch的遥测事件,并将它们推到OpenTelemetry Collector:

    # Checking for optional opentelemetry dependency
    case Code.ensure_compiled(OpenTelemetry) do
      {:module, _} ->
        defmodule Xtdb.OpenTelemetry do
          @tracer_id __MODULE__

          @spec setup :: :ok
          def setup do
            attach_http_request_start()
            attach_http_request_stop()

            :ok
          end

          defp attach_http_request_start() do
            :telemetry.attach(
              {__MODULE__, :http_request_start},
              [@http_client, :send, :start],
              &__MODULE__.handle_http_request_start/4,
              %{}
            )
          end

          defp attach_http_request_stop() do
            :telemetry.attach(
              {__MODULE__, :http_request_stop},
              [@http_client, :request, :stop],
              &__MODULE__.handle_http_request_stop/4,
              %{}
            )
          end

          def handle_http_request_start(
                _event,
                _measurements,
                %{request: request},
                _config
              ) do
            ...
            Otel.start_telemetry_span(@tracer_id, "XT #{request_url}", %{}, %{
              kind: :internal,
              attributes: attributes
            })
          end

          def handle_http_request_stop(
                _event,
                _measurements,
                %{request: _request, name: ConnectionPool, result: {_, %{status: status} = result}},
                _config
              ) do
            context = Otel.set_current_telemetry_span(@tracer_id, %{})
            Span.set_attribute(context, :"http.status", status)
            ...
            OpenTelemetry.end_telemetry_span(@tracer_id, %{})
          end
        end

      _ ->
        nil
    end

编码EDN

长生不老药正在使用Eden库,以解码和编码来自EDN格式的数据。但是,在大多数情况下,它没有任何问题,对于小数,我们必须实施针对小数十进制的协议支持:

    # Implements Eden.Encode protocol for Decimal structs
    defimpl Eden.Encode, for: Decimal do
      # Adds a decimal digit number so it can be picked by EDN
      @spec encode(Decimal.t()) :: String.t()
      def encode(decimal), do: decimal |> Decimal.to_string() |> Kernel.<>("M")
    end

这样,您还可以实施对您可能需要在XT中持续存在的任何其他自定义类型或结构的支持。

测试XTDB

因为XTDB是一个不变的数据库,因此从中删除数据并不是那么简单。在测试方面,这可能会产生影响,因为清除和重置数据库的传统方法可能无效。要将XT集成到测试套件中,最直接的方法是在套件旁边运行一个内存节点。这允许对数据进行更多的颗粒状控制,因为可以根据需要重置或重新创建内存中的节点。

我们通过在我们的docker-compose设置中使用内存XTDB docker图像来实现:

    # Wait for Docker container to be ready
    wait:
      image: dokku/wait

    xtdb_test:
      image: juxt/xtdb-in-memory:1.21.0
      ports:
        - 3000:3000

,还创建了一个简单的外壳脚本,我们使用该脚本来运行集成测试套件:


    docker-compose up --remove-orphans -d xtdb_test
    docker-compose run wait -c xtdb_test:3000
    docker-compose run mix_test
    docker-compose stop xtdb_test mix_test

在单位测试的情况下,由于XTDB客户端的任何请求基本上都是HTTP请求,因此我们也可以将VCR盒作为模拟,以避免向测试实例发送真实请求。

>

接下来是什么?

在本文中,我们讨论了如何与Elixir中编写的应用程序一起使用XTDB,并演示了如何实现与数据库合作的简单HTTP客户端。我们还介绍了如何使用XTDB开发和测试应用程序,以及用于测试目的的内存中节点的重要性。

在本系列的第三部分也是最后一部分中,我们将分享如何使用Docker和Docker-Compose为XTDB建立本地开发环境,以及我们如何部署并在生产中运行它。我们还将讨论我们遇到的警告和问题,以及如何解决这些问题。

,如果您有兴趣使用XTDB:

快乐的黑客并保持关注!

感谢Carsten的评论!