# Use the most appropriate JSON API
Moleculer allows you to select the JSON API to use for serialization and deserialization. See this section on how to set the JSON API type. A couple of Performance tests were made for JSON APIs with with this test file. There is quite a difference in speed between the various JSON and binary APIs.
# Deserialization
The graph below shows the speed of the various JSON parsers. The vertical axis is the parsed/deserialized JSON packets per second per CPU core. A higher value means a faster parser. There is more than a 10-fold difference between the slowest and the fastest APIs. The fastest three APIs are "Boon", "Jodd" and "Jackson":
# Serialization
The graph below shows the speed of the various JSON writers. The vertical axis is the generated/serialized JSON packets per second per CPU core. A higher value means a faster generator. There is more than a 6x difference between the slowest and the fastest APIs. The fastest two APIs are "Jackson" and "FastJson":
Conclusion
The fastest JSON APIs are faster than most binary APIs, at least for smaller files. Among the JSON APIs, the performance of the Jackson API is outstanding when looking at the average read-write. If you want to use one JSON API, same for writing and reading, use Jackson:
ServiceBroker broker = ServiceBroker.builder()
.nodeID("server-1")
.transporter(transporter)
.readers("jackson")
.writers("jackson")
.build();
If you want to configure different JSON APIs for reading and writing, use the Boon API for reading, and the Jackson API for writing:
ServiceBroker broker = ServiceBroker.builder()
.nodeID("server-1")
.transporter(transporter)
.readers("boon,jackson") // Use "Boon", fallback API is "Jackson"
.writers("jackson") // Always use "Jackson" as serializer
.build();
MessagePack serializer is a bit slower than faster JSON APIs. It makes sense to use the MessagePack serializer when sending a lot of data through streams. MessagePack can more efficiently serialize the binary contents than the JSON APIs.
Add dependencies
Remember to add the dependencies of the selected JSON API to "pom.xml" or "build.gradle".
Test the selected API
Not only are there differences in speed between JSON APIs, there is a huge difference in their knowledge. For this reason, other aspects are worth considering. This chapter just focused on speed, not capabilities.
# Thread pools
ServiceBroker
uses an ExecutorService
and a ScheduledExecutorService
to run the tasks.
The default ExecutorService
is the common ForkJoinPool
.
The default ScheduledExecutorService
is a ScheduledThreadPool
with the same size (same number of Threads
).
This setup is fast enough, but uses few Threads
for some cases.
Both pool types and sizes can be configured when creating ServiceBroker
, via ServiceBrokerConfig
or Builder
:
// Create Service Broker config
ServiceBrokerConfig cfg = new ServiceBrokerConfig();
// Set executors
cfg.setExecutor(Executors.newFixedThreadPool(8));
cfg.setScheduler(Executors.newScheduledThreadPool(4));
// Set other broker properties
cfg.setNodeID("node1");
cfg.setTransporter(...);
// Create Service Broker by config
ServiceBroker broker = new ServiceBroker(cfg);
One NioEventLoopGroup
can be specified for both executors (same for "Executor" and "Scheduler").
Experience has shown that an outsized pool is slower than a pool that is exactly the size of the load.
There are several graphical utilities to match the pool type and size.
One of these is
VisualVM
, which can accurately track the number of Threads
running and their load.
Apache JMeter
can be used to generate the appropriate high load.
# Coding style
This chapter outlines some important Moleculer-specific coding suggestions that can affect speed or reliability.
# Collect partial results
The blocks of waterfall model are executed by separate Threads
.
The individual "then" blocks do not reach each other's variables.
Therefore, sharing local variables between blocks would be problematic.
Fortunately, the blocks reach request-level "global" variables of the Action
.
If any data in the blocks is needed later, it must be stored in this global containers.
Since these variables must be "final", we cannot use "primitive" types (int, String, etc.), only containers (eg. Tree
, AtomicReference
, array, map).
The following example uses a single Tree
object to store the processing variables of the request:
Action action = ctx -> {
// --- GLOBAL VARIABLE CONTAINER OF THE REQUEST ---
final Tree global = new Tree();
// --- WATERFALL SEQUENCE ---
return Promise.resolve().then(rsp -> {
// Executed using Thread #1
global.put("key", 123);
}).then(rsp -> {
// Executed using Thread #2
int value = global.get("key", 0);
// Create aggregated response
Tree out = new Tree();
out.put("num", value * 2);
return out;
});
};
There are other solutions besides using a single "global" Tree
object.
It might be a good idea to use single-length arrays or store operation variables in multiple Atomic containers:
Action action = ctx -> {
// --- GLOBAL VARIABLE CONTAINERS OF THE REQUEST ---
// Method #1: Variables in single-length arrays
final String[] var1 = new String[1];
final Tree[] var2 = new Tree[1];
final boolean[] var3 = new boolean[1];
// Method #2: Variables in Atomic containers
final AtomicReference<Tree> var4 = new AtomicReference<>();
final AtomicLong var5 = new AtomicLong();
final AtomicBoolean var6 = new AtomicBoolean();
// --- WATERFALL SEQUENCE ---
return Promise.resolve().then(rsp -> {
// Executed using Thread #1
var1[0] = rsp.get("var1", "");
var2[0] = rsp.get("var2");
var4.set(rsp);
var5.set(rsp.get("var5", 0));
// ...
}).then(rsp -> {
// Executed using Thread #2
String val1 = var1[0];
Tree val4 = var4.get();
long val5 = var5.get();
// ...
});
};
There is no need to synchronize global variables because the execution of "then" blocks is always sequential in the waterfall logic.
# Use Trees instead of Maps
JSON APIs dynamically select which Java object to map to a JSON field during deserialization.
For example, smaller numbers arriving in JSON (eg. "1234") are usually converted to Integer
.
If the number contains a decimal point, it can be Float
.
If the number contains more digits, it will become Double
or Long
, depending on whether it contains a decimal point.
Some JSON parsers can convert larger numbers to BigInteger
or BigDecimal
.
The situation is similar with the JSON serializer for Date objects;
one of the JSON APIs uses the ISO 8601 format or one of the Epoch Time formats to convert Date objects to JSON.
These differences are automatically handled by the Tree
API.
In the example below, the function params.get.("balance").asDouble()
returns a Double in every case,
no matter what type the JSOS parser mapped to.
Either the "date" can be in Epoch Time or ISO format, each type will be a Date
.
Action action = ctx -> {
// Unsafe casting, preferably DO NOT use the solution below,
// this can cause ClassCastException error at runtime
Map<String, Object> map = (Map<String, Object>) ctx.params.asObject();
double balance = (Double) map.get("balance");
Date date = dateFormat.parse((String) map.get("date"));
// Safe casting with automatic type conversion,
// this is how you get the values from "params" structure
double balance = ctx.params.get("balance").asDouble();
Date date = ctx.params.get("date").asDate();
};
# Use field name constants
Use String constants to avoid misspelled variable names.
String constants can be placed in a separate Interface
so that we can refer to them from multiple Services
.
The Code Completion feature of your IDE helps you complete these field names faster and more accurately.
public interface CommonFields {
// --- COMMON FIELD NAMES ---
public static final String FIELD_NAME = "name";
public static final String FIELD_AGE = "age";
public static final String FIELD_BALANCE = "balance";
public static final String FIELD_VIP = "vip";
}
At the beginning of the services you can list the names of the actions and event subscriptions.
Commonly used field names can be imported by the CommonFields
interface:
public class UserService extends Service implements CommonFields {
// --- ACTION NAMES OF THIS SERVICE ---
public static final String ACTION_ADD_USER = "userService.addUser";
public static final String ACTION_REMOVE_USER = "userService.removeUser";
// --- EVENT SUBSCRIPTIONS OF THIS SERVICE ---
public static final String EVENT_USER_MODIFIED = "user.modified";
public static final String EVENT_CONFIG_MODIFIED = "config.modified";
// --- IMPLEMENTATIONS OF ACTIONS ---
@Name(ACTION_ADD_USER)
Action addUser = ctx -> {
String name = ctx.params.get(FIELD_NAME, "");
int age = ctx.params.get(FIELD_AGE, 0);
double balance = ctx.params.get(FIELD_BALANCE, 0d);
boolean isVip = ctx.params.get(FIELD_VIP, false);
// ...
ctx.broadcast(EVENT_USER_MODIFIED, FIELD_NAME, userName, ...);
return null;
};
@Name(ACTION_REMOVE_USER)
Action removeUser = ctx -> {
String userName = "...";
ctx.broadcast(EVENT_USER_MODIFIED, FIELD_NAME, userName);
return null;
};
// --- IMPLEMENTATIONS OF LISTENERS ---
@Subscribe(EVENT_USER_MODIFIED)
Listener userModifiedListener = ctx -> {
boolean vip = ctx.params.get(FIELD_VIP, false);
// ...
ctx.call(ACTION_ADD_USER, FIELD_VIP, vip);
};
@Subscribe(EVENT_CONFIG_MODIFIED)
Listener configModifiedListener = ctx -> {
String name = ctx.params.get(FIELD_NAME, "");
};
}
# Don't repeat queries
If you need to get the same field more than once from a Tree
structure,
you should rather assign it to a local variable.
The code will be more readable and the program will be faster.
So, do not use the "copy-paste" function in such cases, and instead of this...
if (ctx.params.get("addresses[0].zip", (String) null) != null &&
ctx.params.get("addresses[0].zip").asString().length() > 0 &&
ctx.params.get("addresses[0].zip").asString().startsWith("xyz")) {
store(user, ctx.params.get("addresses[0].zip").asString());
}
... write something like this:
String zip = ctx.params.get("addresses[0].zip", "");
if (!zip.isEmpty() && zip.startsWith("xyz")) {
store(user, zip);
}
# Use shorter JSON Paths
Using such long JSON Path values will consume resources unnecessarily:
String firstName = ctx.params.get("transaction.customer.personalProperties.firstName", "");
String lastName = ctx.params.get("transaction.customer.personalProperties.lastName", "");
String address = ctx.params.get("transaction.customer.personalProperties.address", "");
It is better to extract the root of the query into a separate sub-structure. The "customer" Tree is just a pointer to a sub-element of the root Tree, the following operation does not involve extra data copying or other resource intensive operation:
Tree personalProperties = ctx.params.get("transaction.customer.personalProperties");
String firstName = personalProperties.get("firstName", "");
String lastName = personalProperties.get("lastName", "");
String address = personalProperties.get("address", "");
# Cache the responses
Take some time to learn about Moleculer's caching capabilities.
Well-designed caching, using cache keys, can multiply the performance of a Service
with magnitudes.
# Use local methods if possible
If a Service is definitely a local Service (always located in the same JVM),
it can also be accessed through Spring Context (eg. using the @Autowired
annotation).
Local Service methods can be called directly without EventBus or Transporter.
For the most important functions,
it is worth creating "traditional" Java methods that are not "network-ready" Actions
,
and can be easily called from other Services/Components without an intermediate layer.
An exception to the above is when a local Service is called through ServiceBroker for caching purposes.
# Use non-blocking APIs
Blocking is not forbidden in Moleculer, but it wastes resources anyway. If you need to access the full hardware performance, as much of the program as possible must be coded in a non-blocking manner. Unfortunately, not all backend services have a non-blocking API in Java, but if you have one, use it and don't block the Thread. If there is a non-blocking API for a backend service, it can be converted to Promise.
The following section describes how to convert various non-blocking techniques to Promise-based methods.
# Converting callback to Promise
Callback-based processing can be converted to Promise-based processing according to the following scheme.
The structure of the Callback
interface is as follows:
public interface Callback {
public void onFinised(Object data);
public void onError(Throwable error);
}
And the "callbackMethod" using the Callback
interface is as follows:
public void callbackMethod(Object input, Callback callback) {
ForkJoinPool.commonPool().execute(() -> {
callback.onFinised("123");
});
}
To convert it to Promise-based method,
the call must be embedded in the constructor of Promise
, as follows
(this manner is similar to the ES6 Promise
syntax):
public Promise promiseMethod(Object input) {
return new Promise(resolver -> {
callbackMethod(input, new Callback() {
public void onFinised(Object data) {
resolver.resolve(data);
}
public void onError(Throwable error) {
resolver.reject(error);
}
});
});
}
Hereinafter we can use the "promiseMethod" in waterfall-like processing:
Action add = ctx -> {
String input = ctx.params.get("input", "default");
// Waterfall processing of asynchronous events
return promiseMethod(input).then(rsp -> {
// ...
}).catchError(err -> {
// ...
});
};
# Converting CompletableFuture to Promise
For a consistent programming style, you should also convert frequently used CompletableFuture
objects to Promise
.
Example function that returns a CompletableFuture
object:
public CompletableFuture<String> futureMethod(Object input) {
CompletableFuture<String> future = new CompletableFuture<String>();
ForkJoinPool.commonPool().execute(() -> {
future.complete("123"); //
});
return future;
}
To convert it to Promise
-based method,
simply pass the CompletableFuture
as a Promise
constructor parameter:
public Promise promiseMethod(Object input) {
return new Promise(futureMethod(input));
}
If the CompletableFuture
returns with a POJO object, you should convert it to a Tree
object.
This way, both remote and local calls will return with the same Tree
(~=JSON) object:
public Promise promiseMethod(Object input) {
return new Promise(futureMethod(input)).then(rsp -> {
User user = (User) rsp.asObject(); // Convert "User" object to serializable Tree
Tree tree = new Tree();
tree.put("id", user.getID());
tree.put("name", user.getName());
return tree;
};
}
The Promise-based function can be published for other (eg. Node.js-based) Services
via Actions
,
or converted directly into a REST service using the
@HttpAlias Annotation:
@HttpAlias(method = "POST", path = "api/action")
Action action = ctx -> {
return promiseMethod(ctx.params);
};