Back to Home

Safely Accelerate Erlang Application Using NIF on Rust

erlang · rust · rustler · nifs · docker · docker-compose

Safely Accelerate Erlang Application Using NIF on Rust

    The article highlights the integration of Erlang and Rust by the example of the implementation of the Burton Bloom probabilistic data structure, which allows one to verify the element belongs to the set with the necessary accuracy.


    Language selection


    Performance tests based on computational tasks make it clear in which league Erlang plays.




    Since Erlang does not have ultrafast arithmetic, it seems strange to solve complex computational problems on it. However, it is well suited for issues that arise during the development and operation of queuing systems. Erlang, with its excellent scheduler and garbage collector, coupled with a fast network and binary data processing, handles a highly competitive distributed environment. Thus, for myself, I assigned Erlang the role of system glue in the architecture of distributed server applications.

    In real systems, local computing problems arise that slow down the system and degrade the overall UX. It often happens that it slows down 1% of the code, and negatively affects the remaining 99% of the system. To solve this problem in Erlang, starting with version R13B03, there is a mechanism of Native Implemented Functions (NIFs).


    In the Erlang myths list in paragraph 2.7, developers warn that using the NIF interface should be a last resort, since using NIF is dangerous due to possible VM crashes caused by defects in your NIF's implementation, and it does not always guarantee an increase in speed.


    Official examples of NIF implementations are available for C. C and C ++ code is quite easy to make unsafe, for example, going beyond the memory boundaries of a structure or array, or skipping the operation of freeing allocated resources. In my opinion, the problem is aggravated by a context switching factor: when a programmer, mainly developing Erlang code, switches to low-level C, the likelihood of the problems described above increases, especially within the deadlines.


    Thus, I would like to get a solution as fast as in C / C ++, but safe and easily maintained. Let's look at the most computationally efficient languages.




    In terms of language requirements, it is worth noting:
    1. Security. The solution should not under any circumstances break Erlang VM
    2. Performance. Be comparable in performance with C ++
    3. Ability to use in NIF mode
    4. Development speed. A good standard library and a large set of third-party libraries are needed to provide a convenient ecosystem of the language.

    From the cohort of productive languages, Rust seems to be the most suitable. It offers good performance and a secure development model, as well as an active community. An additional plus of Rust is data immutability and a transparent multithreading model.


    It should be noted that there is another optimization option. If we can neglect the time and overhead of an additional call through EPMD, then we can choose the way of writing Erlang Node, instead of NIF. To solve this problem, Java, Go, Rust, Ocaml (from personal experience) are suitable. Erlang Node can be run on the same machine or even on the other side of the earth.


    Implementation


    Overview of existing solutions in Rust


    After a quick search, there are several libraries for writing NIF in rust at once. Consider them:


    1. rustler . Perhaps the most popular and functional library, however, the authors have concentrated their efforts on supporting Elixir. At https://github.com/hansihe/rustler/issues/127 they suggest dragging mix into an erlang project. There is no documentation for use in Erlang.
    2. erlang-rust-nif . It is a low-level implementation of NIF and an approach to building the extension. The code looks like a simple translation from C. The assembly has boundary conditions and is not universal.
    3. erlang_nif-sys . Low-level and full-featured bundle. It is the basis for Rustler. It takes effort and time to write NIF.
    4. bitwise_rust . Demonstrates work with the scheduler. Also a wrapper without syntactic sugar over C api.

    Since one of the requirements is development speed, Rustler looks most attractive. However, I don’t want to make an additional dependency in the form of Elixir and the mix builder.


    Rustler


    Answering the question “why drag an elixir project into erlang?” And following the KISS principle, it was decided to use rustler, but without additional dependencies. As build system rebar3 is used. The easiest and fastest step is to define pre_hooks to compile our rust code.


    To do this, add a hook in the test profile:


    {pre_hooks, [
              {"(linux|darwin|solaris|freebsd)", compile, "sh -c \"cd crates/bloom && cargo build && cp target/debug/libbloom.so ../../priv/\""}
    ]}

    In the combat environment, add the option --release, so in the combat profile add:


    {pre_hooks, [
              {"(linux|darwin|solaris|freebsd)", compile, "sh -c \"cd crates/bloom && cargo build --release && cp target/release/libbloom.so ../../priv/\""}
    ]}

    After these manipulations, a dynamic library appears priv/libbloom.so, completely ready for loading into Erlang VM.
    Details and an example of using rustler in an erlang project can be found in the project repository https://github.com/Vonmo/erbloom


    Bloom filter


    Since the rust ecosystem provides ready-made implementations of the bloom filter, we select the appropriate one and add it to cargo.toml. This project usesbloomfilter = "0.0.12"


    The extension implements the following functions:


    1. new(bitmap_size, items_count)- initialization of the filter. bitmap_sizeand items_count- calculated values, there are many ready-made calculators.
    2. serialize() - filter packaging, for example, for subsequent saving to disk or transmission over the network.
    3. deserialize() - restoration of the filter from the saved state.
    4. set(key) - adds an element to the set.
    5. check(key) - Checks whether an element belongs to a set.
    6. clear() - cleans the filter.

    Erlang


    It should be noted that loading the extension in Erlang is an absolutely transparent process. After loading your module, on_load is called, in which you need to implement the nif loading via erlang: load_nif / 2. In this case, call processing will transparently occur already in Rust.


    The rule of thumb is to generate an erlang: nif_error / 1 error if NIF is not loaded.


    A detailed description of the environment for building the project can be found in this article .


    Summary


    As a result of the work done, we received a productive and safe expansion. In our projects, this extension allows us to reduce the volume of calls to the data warehouse in some cases by up to 10 times and to serve a stream of calls of more than 500k RPS per machine.


    The source code for the extension is available on github .

    Read Next