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:
- Accept connections from other nodes
- Accept registration information from other nodes
- 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 newstate
. - 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:
Finally, let's take a look at what the state
Beacon
module looks like:
Just like this, we will continue to implement other servers on this basis.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。