Translation and adaptation of Java Microservices: A Practical Guide . Previous parts of the guide:
Let's look at the inherent problems of microservices in Java, starting with abstract things and ending with concrete libraries.
How to make a Java microservice resilient?
Recall that when you create microservices, you are essentially trading JVM method calls for synchronous HTTP calls or asynchronous messaging. While a method call is mostly guaranteed to complete (barring an unexpected JVM shutdown), a network call is unreliable by default. It may work, but it may not work for various reasons: the network is overloaded, a new firewall rule has been implemented, and so on. To see how this makes a difference, let's take a look at the BillingService example.HTTP/REST Resilience Patterns
Let's say customers can buy e-books on your company's website. To do this, you've just implemented a billing microservice that can call your online store to generate actual PDF invoices. For now, we'll make this call synchronously, over HTTP (although it makes more sense to call this service asynchronously, since PDF generation doesn't have to be instantaneous from the user's perspective. We'll use the same example in the next section and look at the differences).@Service
class BillingService {
@Autowired
private HttpClient client;
public void bill(User user, Plan plan) {
Invoice invoice = createInvoice(user, plan);
httpClient.send(invoiceRequest(user.getEmail(), invoice), responseHandler());
// ...
}
}
To summarize, here are three possible outcomes of this HTTP call.
- OK: the call went through, the account was created successfully.
- DELAY: The call went through, but it took too long to complete.
- ERROR. The call failed, you may have sent an incompatible request, or the system may not be working.
Messaging Resilience Patterns
Let's take a closer look at asynchronous communication. Our BillingService program might now look something like this, assuming we're using Spring and RabbitMQ for messaging. To create an account, we now send a message to our RabbitMQ message broker, where there are several workers waiting for new messages. These workers create PDF invoices and send them to the appropriate users.@Service
class BillingService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void bill(User user, Plan plan) {
Invoice invoice = createInvoice(user, plan);
// преобразует счет, например, в json и использует его How тело messages
rabbitTemplate.convertAndSend(exchange, routingkey, invoice);
// ...
}
}
Potential errors look a little different now, as you no longer receive immediate OK or ERROR responses like you did with a synchronous HTTP connection. Instead, we may have three potential scenarios that could go wrong, which could raise the following questions:
- Was my message delivered and used by the employee? Or is it lost? (The user does not receive an invoice).
- Was my message only delivered once? Or delivered more than once and processed only once? (The user will receive multiple invoices).
- Configuration: From "Did I use the correct routing keys/names for the exchange" to "Is my message broker configured and maintained correctly or are its queues full?" (The user does not receive an invoice).
- If you're using JMS implementations like ActiveMQ, you can trade speed for the guarantee of two-phase (XA) commits.
- If you're using RabbitMQ, read this tutorial first, and then think carefully about confirmations, fault tolerance, and message reliability in general.
- Perhaps someone is well versed in configuring Active or RabbitMQ servers, especially in combination with clustering and Docker (anyone? ;))
Which framework would be the best solution for Java microservices?
On the one hand, you can install a very popular option such as Spring Boot . It makes it very easy to create .jar files, comes with a built-in web server like Tomcat or Jetty, and can be run quickly and anywhere. Ideal for building microservice applications. Recently, a couple of specialized microservice frameworks, Kubernetes or GraalVM , appeared, partially inspired by reactive programming. Here are a few more interesting contenders: Quarkus , Micronaut , Vert.x , Helidon . In the end, you'll have to choose for yourself, but we can give you a couple of recommendations that may not be completely standard: With the exception of Spring Boot, all microservices frameworks are usually marketed as incredibly fast, with almost instantaneous startup, low memory usage, scalability to infinity. Marketing materials typically feature impressive graphics that show off the platform next to the behemoth Spring Boot or to each other. This, in theory, spares the nerves of developers supporting legacy projects, which sometimes take several minutes to load. Or developers working in the cloud who want to start/stop as many microcontainers as they currently need within 50 ms. The problem, however, is that these (artificial) bare metal start-up times and redeployment times hardly contribute to the overall success of the project. At least they influence much less than a strong framework infrastructure, strong documentation, community and strong developer skills. So it's better to look at it this way: If so far:- You let your ORMs run rampant, generating hundreds of queries for simple workflows.
- You need endless gigabytes to run your moderately complex monolith.
- You have so much code and the complexity is so high (we're not talking about potentially slow starters like Hibernate now) that your application takes several minutes to load.
Which libraries are best for synchronous Java REST calls?
On the low-level technical side, you'll likely end up with one of the following HTTP client libraries: Java's native HttpClient (since Java 11), Apache's HttpClient , or OkHttp . Note that I say "probably" here because there are other options, ranging from good old JAX-RS clients to modern WebSocket clients . In any case, the trend is towards generating an HTTP client, moving away from fiddling around with HTTP calls yourself. To do this, you need to take a look at the OpenFeign project and its documentation as a starting point for further reading.What are the best brokers for asynchronous Java messaging?
Most likely, you will come across the popular ActiveMQ (Classic or Artemis) , RabbitMQ or Kafka .- ActiveMQ and RabbitMQ are traditional, full-fledged message brokers. They involve the interaction of a “smart broker” and “stupid users”.
- Historically, ActiveMQ has had the benefit of easy inlining (for testing), which can be mitigated with RabbitMQ/Docker/TestContainer settings.
- Kafka cannot be called a traditional “smart” broker. Instead, it is a relatively "dumb" message store (log file) that requires smart consumers to process.
What libraries can I use to test microservices?
It depends on your stack. If you have a Spring ecosystem deployed, it would be wise to use the framework's specific tools . If JavaEE is something like Arquillian . It might be worth taking a look at Docker and the really good Testcontainers library , which helps in particular to easily and quickly set up an Oracle database for local development or integration tests. For mock testing entire HTTP servers, check out Wiremock . To test asynchronous messaging, try implementing ActiveMQ or RabbitMQ and then writing tests using Awaitility DSL . In addition, all your usual tools are used - Junit , TestNG for AssertJ and Mockito . Please note that this is not a complete list. If you don't find your favorite tool here, please post it in the comments section.How to enable logging for all Java microservices?
Logging in the case of microservices is an interesting and rather complex topic. Instead of having one log file that you can manipulate with less or grep commands, you now have n log files, and you want them not to be too scattered. The features of the logging ecosystem are well described in this article (in English). Be sure to read it, and pay attention to the section Centralized logging from a microservices perspective . In practice, you will come across different approaches: The system administrator writes certain scripts that collect and merge log files from different servers into a single log file and put them on FTP servers for downloading. Running cat/grep/unig/sort combinations in parallel SSH sessions. This is exactly what Amazon AWS does, and you can let your manager know. Use a tool like Graylog or ELK Stack (Elasticsearch, Logstash, Kibana)How do my microservices find each other?
Until now, we have assumed that our microservices know about each other and know the corresponding IPS. Let's talk about static configuration. So, our banking monolith [ip = 192.168.200.1] knows that it needs to talk to the risk server [ip = 192.168.200.2], which is hardcoded in the properties file. However, you can make things more dynamic:- Use a cloud-based configuration server from which all microservices pull their configurations instead of deploying application.properties files on their microservices.
- Since your service instances can dynamically change their location, it's worth looking at services that know where your services live, what their IPs are, and how to route them.
- Now that everything is dynamic, new problems arise, such as the automatic election of a leader: who is the master who works on certain tasks, so as not to process them twice, for example? Who replaces a leader when he fails? On what basis does the replacement take place?
How to organize authorization and authentication using Java microservices?
This topic is also worthy of a separate story. Again, options range from hardcoded basic HTTPS authentication with custom security frameworks to running an Oauth2 installation with its own authorization server.How can I make sure that all my environments look the same?
What is true for deployments without a microservice is also true for deployments with one. Try a combination of Docker/Testcontainers and Scripting/Ansible.No question: briefly about YAML
Let's step away from libraries and related issues for a moment and take a quick look at Yaml. This file format is used de facto as a format for “writing configuration as code.” It is also used by simple tools like Ansible and giants like Kubernetes. To experience the pain of YAML indentation, try writing a simple Ansible file and see how much you have to edit the file before it works as expected. And this despite the format being supported by all major IDEs! After that, come back to finish reading this guide.Yaml:
- is:
- so
- great
What about distributed transactions? Performance testing? Other topics?
Maybe someday, in future editions of the manual. For now, that’s all. Stay with us!Conceptual Issues with Microservices
Besides the specific problems of microservices in Java, there are other problems, let's say, that appear in any microservice project. They relate mostly to organization, team and management.Frontend and Backend mismatch
Frontend and Backend mismatch is a very common problem in many microservice projects. What does it mean? Only that in the good old monoliths, web interface developers had one specific source for obtaining data. In microservice projects, front-end developers suddenly have n sources to obtain data from. Imagine that you are creating some kind of IoT (Internet of Things) microservices project in Java. Let's say you manage geodetic machines and industrial furnaces throughout Europe. And these ovens send you regular updates with their temperatures and the like. Sooner or later you might want to find ovens in the admin UI, perhaps using "furnace search" microservices. Depending on how strictly your backend counterparts apply domain-driven design or microservice laws, a “find oven” microservice may only return oven IDs and not other data such as type, model, or location. To do this, frontend developers will need to make one or n additional calls (depending on the paging implementation) in the “get furnace data” microservice with the IDs they received from the first microservice. And although this is just a simple example, albeit taken from a real (!) project, even it demonstrates the following problem: supermarkets have become extremely popular. That's because with them you don't have to go to 10 different places to buy vegetables, lemonade, frozen pizza and toilet paper. Instead, you go to one place. It's easier and faster. The same goes for front-end and microservice developers.Management Expectations
Management is under the mistaken impression that they now need to hire an infinite number of developers for an (overarching) project, since developers can now work completely independently of each other, each on their own microservice. Only a little integration work is required at the very end (shortly before launch). In fact, this approach is extremely problematic. In the following paragraphs we will try to explain why.“Smaller pieces” do not equal “better pieces”
It would be a big mistake to assume that code divided into 20 parts will necessarily be of higher quality than one whole piece. Even if we take quality from a purely technical standpoint, our individual services may still be running 400 Hibernate queries to select a user from the database, traversing layers of unsupported code. Once again, we return to the Simon Brown quote: if you fail to build monoliths properly, it will be difficult to build proper microservices. It is often extremely late to talk about fault tolerance in microservice projects. So much so that it is sometimes scary to see how microservices work in real projects. The reason for this is that Java developers are not always ready to study fault tolerance, networking and other related topics at the proper level. The “pieces” themselves are smaller, but the “technical parts” are larger. Imagine your microservices team is asked to write a technical microservice for logging into a database system, something like this:@Controller
class LoginController {
// ...
@PostMapping("/login")
public boolean login(String username, String password) {
User user = userDao.findByUserName(username);
if (user == null) {
// обработка варианта с несуществующим пользователем
return false;
}
if (!user.getPassword().equals(hashed(password))) {
// обработка неверного пароля
return false;
}
// 'Ю-ху, залогинorсь!';
// установите cookies, делайте, что угодно
return true;
}
}
Now your team may decide (and perhaps even convince the business people) that this is all too simple and boring, instead of writing a login service, it’s better to write a really useful UserStateChanged microservice without any real and tangible business implications. requirements. And since some people currently treat Java like a dinosaur, let's write our UserStateChanged microservice in fashionable Erlang. And let's try using red-black trees somewhere, because Steve Yegge wrote that you have to know them inside out to apply to Google. From an integration, maintenance, and overall design perspective, this is as bad as writing layers of spaghetti code inside a single monolith. An artificial and ordinary example? This is true. However, this can happen in reality.
Fewer pieces - less understanding
Then the question naturally comes up about understanding the system as a whole, its processes and work flows, but at the same time you, as a developer, are only responsible for working on your isolated microservice [95: login-101: updateUserProfile]. It harmonizes with the previous paragraph, but depending on your organization, level of trust and communication, this can lead to a lot of confusion, shrugs, and blame if there is an accidental breakdown in the microservice chain. And there is no one who would take full responsibility for what happened. And it’s not a matter of dishonesty at all. In fact, it is very difficult to connect different parts and understand their place in the overall picture of the project.Communications and service
The level of communication and service varies greatly depending on the size of the company. However, the general relationship is obvious: the more, the more problematic.- Who runs microservice #47?
- Did they just deploy a new, incompatible version of a microservice? Where was this documented?
- Who do I need to talk to to request a new feature?
- Who will support that microservice in Erlang, after the only one who knew this language left the company?
- All of our microservice teams work not only in different programming languages, but also in different time zones! How do we coordinate all this correctly?
GO TO FULL VERSION