Giter VIP home page Giter VIP logo

libring's Introduction

libring - A fast consistent hash ring for Elixir

Module Version Hex Docs Total Download License Last Updated

This library implements a stateful consistent hash ring. It's extremely fast (in benchmarks it's faster than all other implementations I've tested against, namely voicelayer/hash-ring and sile/hash_ring), it has no external dependencies, and is written in Elixir.

The algorithm is based on libketama. Nodes on the ring are broken into shards and each one is assigned an integer value in the keyspace, which is the set of integers from 1 to 2^32-1. The distribution of these shards is random, but deterministic.

Keys are then mapped to a shard by converting the key to a binary, hashing it with SHA-256, converting the hash to an integer in the keyspace, then finding the shard which is assigned the next highest value, if there is no next highest value, the lowest integer is used, which is how the "ring" is formed.

This implementation uses a general balanced tree, via Erlang's :gb_tree module. Each shard is inserted into the tree, and we use this data structure to efficiently lookup next-highest key and smallest key. I suspect this is why libring is faster than other implementations I've benchmarked against.

Usage

Add :libring to your deps, and run mix deps.get.

def deps do
  [
    {:libring, "~> 1.0"}
  ]
end

You have two choices for managing hash rings in your application:

HashRing

This API works with the raw ring data structure. It is the fastest implementation, and is best suited for when you have a single process which will need to access the ring, and which can hold the ring in its internal state.

ring = HashRing.new()
       |> HashRing.add_node("a")
       |> HashRing.add_node("b")

"a" = HashRing.key_to_node(ring, {:myworker, 123})

You can also specify the weight of each node, and add nodes in bulk:

ring = HashRing.new()
       |> HashRing.add_nodes(["a", {"b", 64}])
       |> HashRing.add_node("c", 200)
"c" = HashRing.key_to_node(ring, {:myworker, 123})

NOTE: Node names do not have to be strings, they can be atoms, tuples, etc.

HashRing.Managed

This API works with rings which are held in the internal state of a GenServer process. It supports the same API as HashRing. Because of this, there is a performance overhead due to the messaging, and the GenServer can be a potential bottleneck. If this is the case you are better off exploring ways to use the raw HashRing API. However this API is best suited for situations where you have multiple processes accessing the ring, or need to maintain multiple rings.

NOTE: You must have the :libring application started to use this API.

{:ok, pid} = HashRing.Managed.new(:myring)
:ok = HashRing.Managed.add_node(:myring, "a")
:ok = HashRing.Managed.add_node(:myring, "b", 64)
:ok = HashRing.Managed.add_node(:myring, "c", 200)
"c" = HashRing.Managed.key_to_node(:myring, {:myworker, 123})

You can configure managed rings in config.exs, and they will be created and initialized when the :libring application starts. Configured rings take two shapes, static and dynamic rings. Static rings are simply those where the nodes are provided up front, although you can always add/remove nodes manually at runtime; dynamic rings have Erlang node monitoring enabled, and add or remove nodes on the ring based on cluster membership.

You can whitelist/blacklist nodes when using dynamic rings, so that only those nodes which you actually want to distribute work to are used in calculations. This configuration is shown below as well.

If you provide a whitelist, the blacklist will have no effect, and only nodes matching the whitelist will be added. If you do not provide a whitelist, the blacklist will be used to filter nodes. If you do not provide either, a default blacklist containing the ~r/^remsh.*$/ pattern from the example below, which is a good default to prevent remote shell sessions (at least those done via releases) from causing the ring to change.

The whitelist and blacklist only have an effect when monitor_nodes: true.

Configuration

Below is an example configuration:

config :libring,
  rings: [
    # A ring which automatically changes based on Erlang cluster membership,
    # but does not allow nodes named "a" or "remsh*" to be added to the ring
    ring_a: [monitor_nodes: true,
             node_type: :visible,
             node_blacklist: ["a", ~r/^remsh.*$/]],
    # A ring which is composed of three nodes, of which "c" has a non-default weight of 200
    # The default weight is 128
    ring_b: [nodes: ["a", "b", {"c", 200}]]
  ]

Contributing

If you have changes in mind that are significant or potentially time-consuming, please open an RFC-style PR first, where we can discuss your plans first. I don't want you to spend all your time crafting a PR that I ultimately reject because I don't think it's a good fit or is too large for me to review. Not that I plan to reject PRs in general, but I have to be careful to balance features with maintenance burden, or I will quickly be unable to manage the project.

Please ensure that you adhere to a commit style where logically related changes are in a single commit, or broken up in a way that eases review if necessary. Keep commit subject lines informative, but short, and provide additional detail in the extended message text if needed. If you can, mention relevant issue numbers in either the subject or the extended message.

Copyright and License

Copyright (c) 2016 Paul Schoenfelder

This library is MIT licensed. See the LICENSE for details.

libring's People

Contributors

bitwalker avatar davidwebster48 avatar edescourtis avatar enjolras1205 avatar gazler avatar hoyon avatar kevboh avatar kianmeng avatar meadsteve avatar mikaak avatar mopp avatar peillis avatar timbuchwaldt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

libring's Issues

HashRing.add_node error - gb_trees.insert_1/4 {:key_exists}

I tried creating a hash ring with nodes 1 -> 1024, and noticed that it would error out at 805 every time.

For example, you can test with this:

0..804 |> Enum.reduce(HashRing.new, fn (n, r) -> IO.inspect(n); r |> HashRing.add_node(n) end)  # => works
0..805 |> Enum.reduce(HashRing.new, fn (n, r) -> IO.inspect(n); r |> HashRing.add_node(n) end)  # => breaks

Error output

** (ErlangError) erlang error: {:key_exists, 2854800734}
     (stdlib) gb_trees.erl:318: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:297: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:297: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:280: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:297: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:280: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:297: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:280: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:297: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:280: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:297: :gb_trees.insert_1/4
     (stdlib) gb_trees.erl:277: :gb_trees.insert/3
    (libring) lib/ring.ex:104: anonymous fn/3 in HashRing.add_node/3
     (elixir) lib/enum.ex:1785: Enum.reduce_range_inc/4

Local node never returned in key lookup

I was testing a lookup with 3 nodes in the ring. For the same key, running the lookup in nodes b and c returned a. Running the lookup in node a, returned b.

Turns out the managed ring is not initialized with the local node. Line 32 in HashRing.Worker.init/1 should be changed to:

nodes = Node.list([:this, :connected])

`:simple_one_for_one` strategy deprecation warning in Elixir 1.10

When running an app which uses libring in Elixir 1.10 I get the following deprecation message:

warning: :simple_one_for_one strategy is deprecated, please use DynamicSupervisor instead
  (elixir 1.10.0) lib/supervisor.ex:604: Supervisor.init/2
  (elixir 1.10.0) lib/supervisor.ex:556: Supervisor.start_link/2
  (libring 1.4.0) lib/app.ex:13: HashRing.App.start/2
  (kernel 6.5.1) application_master.erl:277: :application_master.start_it_old/4

Don't rename process (suggestion)

My expectation was that the name option would work the same way as a GenServer. The libring prefix made finding the process non-trivial:

def start_link(options) do
    name = Keyword.fetch!(options, :name)
    GenServer.start_link(__MODULE__, options, name: :"libring_#{name}")
  end

issue adding pids with the same number in different nodes

I don't know if this is an issue of this lib or is elsewhere, but when adding pids to a HashRing I get this problem:
Suppose pid1 = #PID<0.433.0> and pid2 = #PID<21396.433.0>
Processes in different nodes.

> r = HashRing.new()
#<Ring[]>
> r = HashRing.add_node(r, pid1)
#<Ring[#PID<0.433.0>]>
> r = HashRing.add_node(r, pid2)
#<Ring[#PID<0.433.0>]>

It's like they were the same for HashRing

Tests cannot be run

I'm trying to run the tests but it throws me the following error:

(CompileError) test/hashring_test.exs:3: module :eqc_gen is not loaded and could not be found

I'm using Elixir 1.7.3 and Erlang 21.0.5

Missing :logger dependency

:logger needs to be listed in dependencies otherwise you might get this error if :libring happens to be started first:

=INFO REPORT==== 15-Oct-2018::21:41:03 ===
    application: libring
    exited: {bad_return,
                {{'Elixir.HashRing.App',start,[normal,[]]},
                 {'EXIT',
                     {#{'__exception__' => true,
                        '__struct__' => 'Elixir.RuntimeError',
                        message =>
                            <<"cannot use Logger, the :logger application is not running">>},
                      [{'Elixir.Logger.Config','__data__',0,
                           [{file,"lib/logger/config.ex"},{line,53}]},

See PR #16

HashRing.Managed.new doesn't work as expected

{:ok, pid} = HashRing.Managed.new(:myring)
** (exit) exited in: GenServer.call(HashRing.Supervisor, {:start_child, [[name: :myring]]}, :infinity)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
    (elixir) lib/gen_server.ex:729: GenServer.call/3

:erlang.phash2 does not uniformly distribute atoms across the hash range, should this be documented in README?

This can be verified pretty easily in the shell, but there's an erlang mailing list thread that gives examples but here's salient bit

2> erlang:phash2(aaa).
26481
3> erlang:phash2(bbb).
26754
4> erlang:phash2(ccc).
27027

The takeaway seems to be it works great for any term except atoms. One possibility is to call :erlang.term_to_binary on the atom, or alternatively wrap it in a tuple or list.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.