1. Deep dive into low-latency microservices
Together we will build a microservice that can run in its own JVM, which can execute JDBC queries and updates through persistent request and result queues.
I would consider this a Gateway Service because it interacts with a system that is outside the microservice model.
2. What does this service do?
This service supports two functions executeQuery and executeUpdate. These methods mirror the same methods as PreparedStatement except that the result is passed as a message
Two asynchronous request handling functions are declared in the following interface:
1 2 3 4 5 6 | public interface JDBCStatement { void executeQuery(String query, Class<? extends Marshallable> resultType, Object... args); void executeUpdate(String query, Object... args); } |
Two asynchronous result handling functions are declared in the following interface:
1 2 3 4 5 6 7 8 | public interface JDBCResult { void queryResult(Iterator<Marshallable> marshallableList, String query, Object... args); void queryThrown(Throwable t, String query, Object... args); void updateResult(long count, String update, Object... args); void updateThrown(Throwable t, String update, Object... args); } |
3. Components wrapped as a Service
Look at the executorUpdate method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class JDBCComponent implements JDBCStatement { private final Connection connection; private final JDBCResult result; public JDBCComponent(ThrowingSupplier<Connection, SQLException> connectionSupplier, JDBCResult result) throws SQLException { connection = connectionSupplier.get(); this.result = result; } @Override public void executeUpdate(String query, Object... args) { try (PreparedStatement ps = connection.prepareStatement(query)) { for (int i = 0; i < args.length; i++) ps.setObject(i + 1, args[i]); int count = ps.executeUpdate(); // record the count. result.updateResult(count, query, args); } catch (Throwable t) { result.updateThrown(t, query, args); } } |
You can see that every input message produces an output message with the result. This will be useful later on to restart the service from where it started and monitor its progress, as well as obtain the results.
How to wrap this as a service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | public class JDBCService implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(JDBCService.class); private final ChronicleQueue in; private final ChronicleQueue out; private final ExecutorService service; private final ThrowingSupplier<Connection, SQLException> connectionSupplier; private volatile boolean closed = false; public JDBCService(ChronicleQueue in, ChronicleQueue out, ThrowingSupplier<Connection, SQLException> connectionSupplier) throws SQLException { this.in = in; this.out = out; this.connectionSupplier = connectionSupplier; service = Executors.newSingleThreadExecutor( new NamedThreadFactory(in.file().getName() + "-JDBCService", true)); (1) service.execute(this::runLoop); (2) service.shutdown(); // stop when the task exits. } void runLoop() { try { JDBCResult result = out.createAppender() (3) .methodWriterBuilder(JDBCResult.class) .recordHistory(true) .get(); JDBCComponent js = new JDBCComponent(connectionSupplier, result); MethodReader reader = in.createTailer().afterLastWritten(out).methodReader(js); (4) Pauser pauser = new LongPauser(50, 200, 1, 10, TimeUnit.MILLISECONDS); while (!closed) { if (reader.readOne()) (5) pauser.reset(); else pauser.pause(); } } catch (Throwable t) { LOGGER.error("Run loop exited", t); } } @Override public void close() { closed = true; } public JDBCStatement createWriter() { return in.createAppender() (6) .methodWriterBuilder(JDBCStatement.class) .recordHistory(true) .get(); } public MethodReader createReader(JDBCResult result) { return out.createTailer().methodReader(result); } } |
(1) Create a topic with a meaningful name. We use ExecutorService in case we want to do something more complex with it later.
(2) Add this quest to the group
(3) Create a proxy to write to the output queue
(4) Start reading after the last message is processed successfully.
(5) Read one message at a time.
(6) Add a helper method to create a logger to this service’s input
(7) Add a helper method to read the output of this service.
4. How does it perform?
I’ve tested this writing to HSQLDB is pretty fast, even writing to a file. Using it as a Service, though, can be useful for very explosive activity as we can handle a lot more requests in a period of time.
The performance test writes 200k messages as fast as possible and waits for them all to complete. The first is the average delay to write each request and the second is the average time to get the results.
Thời gian trung bình để ghi mỗi bản cập nhật 1,5 us, thời gian trung bình để thực hiện mỗi bản cập nhật 29,7 us
Although HSQLDB can persist over 33k updates per second , (1/29.7 us), the service plan can handle write bursts of more than 660k writes per second . (1/1.5 us) This represents a 20x improvement in the continuous throughput it can support.
5. How long can a burst be?
Both Linux and Windows tend to perform well when up to 10% of main memory is “dirty” or not written to disk. For example, if you have 256 GB, you may have 25 GB of “dirty” data. Even so, if the concurrency is faster than the service consumes, but slow enough that the disk subsystem can keep up, your contigs may exceed the main memory size. In a word, if your message is 256 bytes long, the service can be slower than a billion messages and it won’t run out of memory or fail. The main limitation in this case is the free disk space you have. At the time of posting, you can buy a 1 TB Enterprise SSD for under $600, and Samsung is selling a 16 TB SSD. I expect hosting costs to continue to drop.
6. Conclusion
Building a microservice by wrapping a component with an asynchronous API with a transport for messaging in and out worked without too much hassle.
The best way to go fast is to do less work.