1
头图

Today we will implement the first component of the server - beacon_server .

Functional Analysis

In order to establish an Elixir cluster, all Beam nodes need to know a fixed node to connect to at the time of startup, and then Beam will automatically complete the link between nodes, that is, the default 全连接 mode, all nodes two There is a connection between the two. Regarding this point, I have not thought deeply about whether it is necessary to make adjustments, and I will see the situation later 🤪

Therefore, in order to allow all nodes in the server cluster to connect to a fixed node to form a cluster at startup, this fixed node is beacon_server .

beacon_server What function does it need? After some simple thinking, at least the following functions are required:

  1. Accept connections from other nodes
  2. Accept registration information from other nodes
  3. Corresponding to the demand of other nodes, return the information of the demand node

There are two important concepts here: 资源(Resource) and 需求(Requirement) . 资源 refers to the content type of a node itself, that is, its role in the cluster. For example, the resource of the gateway server is the gateway (gate_server); 需求 refers to the needs of a node Other nodes, such as the gateway node, need the gateway manager node (gate_manager) to register themselves, and the data service node needs the data contact node (data_contact) to synchronize the database to itself.

When a node registers with the beacon_server beacon_server , we hope it can provide its own node name, resources, requirements and other data to ---023fd82f85232fa09cf877566854ef72---, which is convenient for beacon_server after receiving When another node is registered, it can return the registered node as a request to other nodes.

data structure

I use a thread GenServer 21acdb78adb19ccc076b2f7a52613207 --- to be responsible for all the work mentioned above, and use the thread state to save the information about the nodes. At present, after thinking about it roughly, let's define the information storage format as follows:

 %{
  nodes: %{
    "node1@host": :online,
    "node2@host": :offline
  },
  requirements: [
    %{
      module: Module.Interface,
      name: [:requirement_name],
      node: :"node@host"
    }
  ],
  resources: [
    %{
      module: Module.Interface,
      name: :resoutce_name,
      node: :"node@host"
    }
  ]
}

I use a dictionary to store all the information, divided into three parts nodes , requirements and resources .

nodes store all connected nodes and their status, :online indicates normal online connection, :offline indicates that the node is disconnected;

requirements Store the requirement information provided by each node during registration. With list storage, each item in the list represents a node. Items use dictionaries to store module, name, and node information. Among them 名称 field, because some nodes may have more than one 需求 , so use list storage. 模块 field is reserved for later use, it is not useful at present... 节点 The field is used to obtain the node using this field to send messages to the target node, which is essential.

resources Stores the resource information provided by each node during registration. The fields are exactly the same as requirements , with one difference being that the data type of the 名称 field is no longer A list is an atom, because each node can only belong to a unique resource, and cannot belong to more than two types, so a single atom can be used to represent it.

Brief implementation

create project

This is the first implementation. Before implementation, we will create a umbrella project to store all the following code:

 mix new cluster --umbrella

Then create the beacon_server project of this section:

 cd apps/
mix new beacon_server --sup

--sup is used to generate the supervision tree.

After we have the project, we need to create a GenServer to act as an interface for other nodes to communicate, we will call it Beacon .

function function

According to the previous assumptions, we need the following functions:

  • register(credentials, state) - used to record the registered node information in state , and return the new state .
  • get_requirements(node, requirements, resources) - Used to return its requirements to a registered node.

Paste the code I roughly implemented below, of course, this will not be the final version, and there is room for optimization in the future:

 @spec register({node(), module(), atom(), [atom()]}, map()) :: {:ok, map()}
defp register(
        {node, module, resource, requirement},
        state = %{nodes: connected_nodes, resources: resources, requirements: requirements}
      ) do
  Logger.debug("Register: #{node} | #{resource} | #{inspect(requirement)}")

  {:ok,
    %{
      state
      | nodes: add_node(node, connected_nodes),
        resources: add_resource(node, module, resource, resources),
        requirements:
          if requirement != [] do
            add_requirement(node, module, requirement, requirements)
          else
            requirements
          end
    }
  }
end

@spec get_requirements(node(), list(map()), list(map())) :: list(map())
defp get_requirements(node, requirements, resources) do
  req = find_requirements(node, requirements)
  offer = find_resources(req, resources)
  offer
end

I will not post the other private functions used in the above code. In short, the data in the thread state is used to return new data.

In addition to these two necessary functions, I also want to add two functions that can monitor the on and off of nodes. These two functions are implemented by handle_info . First, you need to enable this function when the thread is initialized:

 :net_kernel.monitor_nodes(true)

Then implement two callbacks:

 # ========== Node monitoring ==========

@impl true
def handle_info({:nodeup, node}, state) do
  Logger.debug("Node connected: #{node}")

  {:noreply, state}
end

@impl true
def handle_info({:nodedown, node}, state = %{nodes: node_list}) do
  Logger.critical("Node disconnected: #{node}")

  {:noreply, %{state | nodes: %{node_list | node => :offline}}}
end

Instead of changing the node state to :online in the :nodeup callback, it is because the registration function has changed the node state to :online when the node is registered.

interface function

After having the function, we also need to provide external interface, GenServer has provided the relevant callback function for us to implement, here I use handle_call/3 , because the registration process needs to be synchronous , only After the registration is completed, the corresponding node can start to run normally.

Similarly, there are two external interfaces, namely :register and :get_requirements :

 @impl true
# Register node with resource and requirement.
def handle_call(
      {:register, credentials},
      _from,
      state
    ) do
  Logger.info("New register from #{inspect(credentials, pretty: true)}.")

  {:ok, new_state} = register(credentials, state)

  Logger.info("Register #{inspect(credentials, pretty: true)} complete.", ansi_color: :green)

  {:reply, :ok, new_state}
end

@impl true
# Reply to caller node with specified requirements
def handle_call(
      {:get_requirements, node},
      _from,
      state = %{nodes: _, resources: resources, requirements: requirements}
    ) do
  Logger.debug("Getting requirements for #{inspect(node)}")

  offer = get_requirements(node, requirements, resources)

  {:reply,
    case length(offer) do
      0 -> nil
      _ -> 
        Logger.info("Requirements retrieved: #{inspect(offer, pretty: true)}", ansi_color: :green)
        {:ok, offer}
    end, state}
end

So far, the Beacon function module is basically complete, and finally we need to add it to the supervision tree to make it run. In application.ex :

 def start(_type, _args) do
  children = [
    # Starts a worker by calling: BeaconServer.Worker.start_link(arg)
    {BeaconServer.Beacon, name: BeaconServer.Beacon}
  ]

  # See https://hexdocs.pm/elixir/Supervisor.html
  # for other strategies and supported options
  opts = [strategy: :one_for_one, name: BeaconServer.Supervisor]
  Supervisor.start_link(children, opts)
end

Adding the Beacon module to the supervisor's sub-thread list like this, beacon_server is temporarily complete.

effect test

Try running it:

 iex --name beacon1@127.0.0.1 --cookie mmo -S mix

In order for other nodes to connect, name and cookie must be set well.

I wrote some test code to call it and try:

Beacon Server Output

Finally, let's take a look at what the state Beacon module looks like:

Beacon State

Just like this, we will continue to implement other servers on this basis.


dyzdyz010
1.2k 声望30 粉丝

大道至简,修心为上