libcluster

repository·main·Indexed 24 days ago

https://github.com/bitwalker/libcluster

An Elixir library providing a pluggable mechanism for automatically forming and healing clusters of Erlang nodes. It supports multiple clustering strategies including EPMD, Gossip, Kubernetes, Rancher, and DNS polling, and allows for the implementation of custom strategies via the Cluster.Strategy behavior.

Tokens
6.2K
Snippets
17
Records
28
Agent score
78%

What's inside libcluster

  1. How libcluster works: Topologies and Supervisors

    main

    To use libcluster, you define one or more topologies. A topology consists of a chosen strategy and its associated configuration.

    To activate clustering, you must start the Cluster.Supervisor module within your application's supervision tree, passing the list of topologies to it. The supervisor manages the lifecycle of the clustering processes based on your defined topologies.

    defmodule MyApp.App do
      use Application
    
      def start(_type, _args) do
        topologies = [
          example: [
            strategy: Cluster.Strategy.Epmd,
            config: [hosts: [:"a@127.0.0.1", :"b@127.0.0.1"]],
          ]
        ]
        children = [
          {Cluster.Supervisor, [topologies, [name: MyApp.ClusterSupervisor]]},
          # ..other children..
        ]
        Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
      end
    end
  2. Use alternative distribution plumbing (non-Erlang distribution)

    main

    If you are using a distribution protocol other than standard Distributed Erlang (e.g., Partisan), you must provide custom implementations for connecting, disconnecting, and listing nodes in your topology configuration.

    Use the connect, disconnect, and list_nodes keys. These keys accept a {module, fun, args} tuple, where the target node name is automatically appended to the args list.

  3. Use the Gossip clustering strategy

    main

    The Cluster.Strategy.Gossip strategy uses multicast UDP to dynamically form a cluster by gossiping node names across the network. Nodes listen for these packets and attempt to establish connections if they are reachable and share the same secret (encryption key).

    By default, it uses port 45892 and the multicast address 233.252.1.32. The protocol is not encrypted unless a secret is provided in the configuration. Providing a secret also allows you to run multiple isolated clusters on the same network using the same multicast settings, as nodes with different secrets will not connect.

    config :libcluster,
            topologies: [
              gossip_example: [
                strategy: Cluster.Strategy.Gossip,
                config: [
                  port: 45892,
                  if_addr: "0.0.0.0",
                  multicast_if: "192.168.1.1",
                  multicast_addr: "233.252.1.32",
                  multicast_ttl: 1,
                  secret: "somepassword"]
              ]
            ]
  4. Configure libcluster topologies in config.exs

    main

    You can define your topologies in your Mix configuration file (config.exs) under the :libcluster application key. When starting your application, retrieve this configuration using Application.get_env(:libcluster, :topologies) and pass it to Cluster.Supervisor.

    config :libcluster,
      topologies: [
        epmd_example: [
          # The selected clustering strategy. Required.
          strategy: Cluster.Strategy.Epmd,
          # Configuration for the provided strategy. Optional.
          config: [hosts: [:"a@127.0.0.1", :"b@127.0.0.1"]],
          # The function to use for connecting nodes. The node
          # name will be appended to the argument list. Optional
          connect: {:net_kernel, :connect_node, []},
          # The function to use for disconnecting nodes. The node
          # name will be appended to the argument list. Optional
          disconnect: {:erlang, :disconnect_node, []},
          # The function to use for listing nodes.
          # This function must return a list of node names. Optional
          list_nodes: {:erlang, :nodes, [:connected]},
        ],
        # more topologies can be added ...
        gossip_example: [
          # ...
        ]
      ]
  5. Use the Kubernetes DNS clustering strategy

    main

    The Cluster.Strategy.Kubernetes.DNS strategy enables clustering by fetching IP addresses via a Kubernetes headless service in the current namespace.

    Requirements:

    • A headless service must be configured to expose the pods.
    • All Erlang nodes must use longnames in the format <basename>@<ip>.
    • All nodes must share the same <basename> (configured via :application_name).
    • All nodes must have unique <ip> addresses.

    Note: If you want to avoid using a headless service, use Cluster.Strategy.Kubernetes instead.

    config :libcluster,
            topologies: [
              erlang_nodes_in_k8s: [
                strategy: Cluster.Strategy.Kubernetes.DNS,
                config: [
                  service: "myapp-headless",
                  application_name: "myapp",
                  polling_interval: 10_000
                ]
              ]
            ]
  6. Use the Erlang hosts file clustering strategy

    main

    The Cluster.Strategy.ErlangHosts strategy uses Erlang's built-in distribution protocol by reading a .hosts.erlang file. This file should contain host names written as Erlang terms (e.g., 'host.example.com'.).

    Erlang looks for the .hosts.erlang file in the following order:

    1. The current working directory.
    2. The user's home directory.
    3. $OTP_ROOT (the root directory of Erlang/OTP).

    If the file is not found, libcluster will log a warning and will not attempt to join the cluster.

      'super.eua.ericsson.se'.
      'renat.eua.ericsson.se'.
      'grouse.eua.ericsson.se'.
      'gauffin1.eua.ericsson.se'.
  7. Implement a custom clustering strategy using Cluster.Strategy

    main

    To create a custom clustering strategy, implement the Cluster.Strategy behaviour. Your module must provide the following callbacks:

    1. child_spec(strategy_args): Returns a Supervisor.child_spec() required for the strategy to be supervised. Using the Cluster.Strategy.__using__(opts) macro automatically provides a default implementation of child_spec/1.
    2. start_link(strategy_args): Starts the strategy and returns {:ok, pid}, :ignore, or {:error, reason}.

    When implementing your module, use use Cluster.Strategy to include the necessary boilerplate for supervision.

  8. Enable debug logging for Gossip strategy

    main

    Debug logging for the Gossip strategy is disabled by default. You can activate it by setting the debug key to true in your libcluster configuration. This can be toggled at runtime without requiring a node shutdown.

    config :libcluster,
            debug: true
  9. Use the Rancher clustering strategy

    main

    The Cluster.Strategy.Rancher strategy is designed for the Rancher container platform. It discovers nodes by querying the Rancher metadata API (http://rancher-metadata) to find containers belonging to the same service.

    Node Naming Requirement: This strategy assumes nodes use longnames in the format <basename>@<ip>, where <ip> is the unique IP of the container. To ensure this works in a Distillery release, you should use a wrapper script to interpolate the container's IP into the Erlang VM arguments.

    Example Wrapper Script:

    #!/
    sh
    
    export CONTAINER_IP="$(hostname -I | cut -f1 -d' ')"
    export REPLACE_OS_VARS=true
    
    /app/bin/app "$@"

    VM Arguments:

    -name app@${CONTAINER_IP}
    #!/bin/sh
    
    export CONTAINER_IP="$(hostname -I | cut -f1 -d' ')"
    export REPLACE_OS_VARS=true
    
    /app/bin/app "$@"
  10. Integrate Cluster.Supervisor into your application

    main

    To enable clustering, you must add Cluster.Supervisor to your application's supervision tree. You can provide the topologies configuration manually during application startup or via your Mix configuration file.

    Option 1: Manual configuration in start/2

    Define a list of topologies and pass them to Cluster.Supervisor as the first element in the child specification list. You can also provide supervisor options (like a custom :name) as the second element.

    Option 2: Using Mix configuration

    If you define your topologies in your config/config.exs under the :libcluster application, you can retrieve them using Application.get_env(:libcluster, :topologies) and pass them to the supervisor.

    # Example: Manual integration in your Application module
    defmodule MyApp.App do
      use Application
    
      def start(_type, _args) do
        topologies = [
          example: [
            strategy: Cluster.Strategy.Epmd,
            config: [hosts: [:"a@127.0.0.1", :"b@127.0.0.1"]],
          ]
        ]
    
        children = [
          {Cluster.Supervisor, [topologies, [name: MyApp.ClusterSupervisor]]},
          # ..other children..
        ]
    
        Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
      end
    end
    
    # Example: Using Mix config
    # In config/config.exs:
    # config :libcluster, topologies: [example: [...]]
  11. Set up Kubernetes for Kubernetes DNS strategy

    main

    To use the Cluster.Strategy.Kubernetes.DNS strategy, you must perform the following three steps in your Kubernetes deployment:

    1. Expose Pod IP via Environment Variable

    In your deployment.yaml, map the status.podIP field to an environment variable (e.g., POD_IP):

    env:
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP

    2. Create a Headless Service

    Define a service with clusterIP: None to allow DNS lookups of pod IPs:

    apiVersion: v1
    kind: Service
    metadata:
      name: myapp-headless
    spec:
      selector:
        app: myapp
      type: ClusterIP
      clusterIP: None

    3. Configure Erlang Node Name

    Set the Erlang node name using the exposed environment variables. If using Mix releases, update rel/env.sh.eex:

    export RELEASE_DISTRIBUTION=name
    export RELEASE_NODE=<%= @release.name %>@${POD_IP}