Blog

microservices

From monolith to miniservices (indeed, not microservices)

Henk Dieter
Henk Dieter

Tweede golf has built quite a few big web applications over the last ten years. One of our specialties being the development of Symfony applications, some of these applications have become massive, with a lot of separate functionality baked into a single monolith. For now, this situation is being contained as we've been strict about minimizing technical debt. In practice, however, it's extremely hard to completely avoid accumulation of technical debt, which is one of the reasons we have started looking into introducing microservice architectures into our projects. Monolithic applications 'What's wrong with a good old monolithic application?', one might ask. Initially, not that much, actually. Especially in the initial phase of a software project, when the context and requirements and separate domains might not be entirely clear, monolithic applications provide the flexibility to refactor large parts of a project in a relatively easy way. Monoliths are quite easy to test. After a couple of months of development, the upsides of monolithic applications become less apparent. Refactoring code becomes progressively harder to do, and updating dependencies might introduce a lot of bugs that are hard to anticipate. As the user base of an application becomes larger and more is asked from the hardware running the application, scaling up becomes a necessity. Monolithic applications being complex beasts, scaling vertically is the only choice. One can only get so far by renting a higher-tier server. Monoliths are fragile. A small bug anywhere in the system, even some barely used non-critical part of it, might cause the complete application to fall over and introduce downtime. This can potentially affect many, many users, and deploying a patch or update can be hard.. And then there's deploying database updates. Monolithic applications often store a large portion of their data in a single relational database. When persistent data is to be updated, the service might be unavailable for a significant amount of time. Users might lose their work, or worse, their interest, as a result. Microservices and miniservices Microservice architectures are developed to tackle these problems. Classic microservices typically handle only a very small isolated task within the application, following the mantra of 'Do one thing, and do it well'. They're stateless and loosely coupled to one another. There might be a microservice for user authentication, one for sending e-mails and another for rendering thumbnails. Implemented properly, these systems are easy to scale horizontally: just identify bottleneck services and launch more instances accordingly. They are loosely coupled, enabling isolation of bug impact to single domains of the applications and can be updated without introducing downtime. || |:--:| |Monolith vs. microservices| | Monolith vs. microservices | Doing one thing, and doing it well is no universal solution, though. Integration and end-to-end testing a system with many microservices is rather difficult, even though testing a single microservice is simple. Deployment of microservices demands specialized knowledge about containerization and orchestration, or, if you're not into that, other complex deployment techniques. For a large project, this may introduce a considerable amount of overhead. Enter miniservices: a balance between monoliths and microservices. The classic microservice being fine-grained, we think that the domains within which the services operate should not be smaller than necessary. For example, we won't make a distinction between microservices generating HTML and another rendering that HTML into a PDF. Miniservices collect the data, and render the page in one go. However, the PDF rendering miniservice will not be doing any user authentication. This keeps the system as a whole maintainable, flexible as well as scalable. || |:--:| |Microservices vs. miniservices| | Microservices vs. miniservices | Pros How do you decide whether you should set up a monolith or a miniservice system? We've been looking into that and have come up with a couple of guidelines that can be used to find out whether miniservices are a good fit for your project. Here are some of the good things miniservice architectures may bring to your project: Dependency management Whenever an application has a set of dependencies, these always resolve to a tree of all the packages that need to be installed. When any dependency does not keep track of progress in the ecosystem, those dependencies become stale. In the case of strict semantic versioning, this means that the monolith as a whole is stuck on these stale dependencies. This is a problem due to either missing new functionality from new versions of your libraries, or missing security patches. In that case, either the dependency needs to be replaced with some equivalent functionality, the functionality needs to be removed, or the functionality needs to be split out to another application. Miniservices enable splitting up the list of dependencies. This keeps the amount of stale or complex dependencies contained. Independent development Miniservices can be developed independently from each other. As domains are separated clearly, having established API contracts, teams only have to worry about the implementation of the tasks within their domain. Independence of technology Monoliths depend on a single core technology, typically a single stack such as PHP+Symfony or Node. However, applications have varying requirements of their technology, depending on the required functionality. PHP is fine to define CRUD operations, but PHP is not sufficient when implementing graphics operations or processing large volumes of data. Following the creed pick the best tool for the job, mini-services enable a disjoint set of technologies to be employed for a project. For small, highly specialized functionalities, programming languages such as C++, Rust or Go can be employed to leverage their performance or correctness characteristics. Replaceability At Tweede golf we sometimes quickly prototype software, with the intention to implement a proper alternative later when adequate funding is secured. Often these decisions stem from business- or maintainability considerations. This is nearly impossible to do neatly when the project is implemented as a monolith, but trivial when the mini-service approach is taken. Reusability Certain problems are solved in multiple projects and don't need a tailor-made solution. These problems include rendering PDF files, thumbnailing images and sending e-mails. Currently, each project team solves these problems separately. These solutions can be developed once and deployed individually as a miniservice, but maintained commonly by a distinct team. The costs of the development of these functionalities can be shared across projects. In order to develop and maintain such a miniservice very specific knowledge might be required. Not every team has the luxury or budget of properly building out that functionality. A prime example of this is maintaining a mailserver, which is error-prone and difficult work, but which can easily be shared between projects. Development and release cycle duration Even though a set of miniservices as a whole is harder to deploy than a monolithic application, a large benefit comes from being able to individually deploy parts of your project. It is no longer required to always build and deploy the entire project. This can enable saving time in your development and release cycle. Also, the release cycle becomes simpler and therefore easier to maintain. Traceability Tracing the operations of a monolithic application involves intensive logging in all parts of the application. A fatal error might cause an execution log to be incomplete or even absent in some cases. This makes post-mortem inspection and debugging of a running application hard. An application built on a miniservice architecture exposes obvious interfaces in the system that are both accessible and uniform, namely the APIs between the miniservices. Logging communications or communication metadata between miniservices leaves a transparent trace of system operation. Resource management Resource usage, like memory or CPU, is hard to pin down on certain parts in a monolith, while individual mini-services make it straightforward to track. This enables more transparent tracking of excessive resource usage or other resource-related problems. Robustness A monolith's failure often propagates through the complete system. A miniservice architecture makes it more natural to isolate faults within their respective miniservice without unnecessarily impacting other parts of the system. The miniservice architecture forces the developer to implement error handling and fault tolerance when interacting with other parts of the system via an API. When implementing functionality that is prone to failures, like generating a PDF document, it is hard to handle all possible errors within a monolith PHP application. Especially when some of the possible errors cause the PHP thread to crash. Isolating this service makes it easier to recover from these errors. A fatal error could just cause some parts of the application being unavailable, while others can still keep working. Cons Like microservices, miniservices aren't perfect. Below are some downsides to consider. End-to-end testing End-to-end testing a miniservice architecture is hard. Running every service in the system locally can become a pain. If not properly set up, developers need to check out the correct code from source control and ensure all dependencies are of the correct version. Tools like docker-compose can be of great help here, though. Debugging In a system consisting of many miniservices, sometimes it's quite difficult to find out the cause of errors in a production setting. It's hard to recreate the state the system was in when it failed. Therefore, extensive logging should be built-in through all parts of the system. Communication overhead When multiple development teams are working on the same application, they must agree on API contracts beforehand and whenever an API is updated. To streamline these communications, a cross-project lead is required. Type system and IDE hints As not all code can reside in a single repository, developers cannot easily take advantage of type systems and IDE hints for inter-service communications. Especially when multiple languages are used, defining entity models might cause some discrepancies. Knowledge Maintaining multiple software stacks introduces a higher cost. Not every team will be able to contribute to all parts of the project. There's a need for knowledge about orchestrating many containers in a cloud setting, using tools such as Kubernetes. Conclusion Miniservice based applications are great. They're robust, scalable and multiple teams can work on them simultaneously. They are a good fit for certain large projects. But setting up such an application is not easy. Companies need to be mature as communication between teams and individual team members is vital. Setting up a good workflow for working on the projects needs to be thought out well. Also, specialist knowledge is required to get the system running smoothly. 'I like miniservices! But how do I start?'. My next blog post contains an incomplete how-to-migrate-from-monolith-to-miniservices to get you started.

Rust als webplatform?!

Wouter
Wouter

Wat is over 5 jaar het winnende open source webplatform? Inmiddels begint PHP - ondanks PHP7 - zijn ouderdom te tonen. Alternatieven als NodeJS zijn wel werkbaar, maar in lang niet alle scenario’s geschikt. We vertellen je graag waarom wij denken dat Rust de nieuwe speler kan worden voor high-performance applicaties op het web. Ja, PHP is oud: het barst van de legacy code, de snelheid laat te wensen over, en er is eigenlijk nooit sprake geweest van een net ontwerp van de taal \[1\]. Wij gebruiken PHP - d.w.z. het backend framework Symfony - omdat het ecosysteem, bestaande uit de tooling en beschikbare open source bundles, inmiddels heel volwassen is. Wil je snel een goed schaalbare web app bouwen met open source technieken? Dan is PHP met Symfony momenteel nog steeds The Way To Go. Kijken we verder in de toekomst en nemen we de steeds hogere eisen aan security en performance mee, dan is het duidelijk tijd voor ons om op zoek te gaan naar een moderner, veiliger en sneller alternatief voor PHP/Symfony. Waarom geen Node.js? Javascript is booming. Nieuwe open source projecten rondom het Javascript-platform schieten als paddenstoelen uit de grond. Helaas wordt Node.js veelal geplaagd door vergelijkbare problemen als PHP. Het is óók heel traag. De tooling rondom Javascript is in elkaar gebeund \[2\]. En ook het taalontwerp is langzaamaan gegroeid, wat de taal niet altijd even logisch maakt \[3\]. Browsers hebben dan ook moeite om deze ontwikkelingen bij te houden \[4\]. Nog meer dan bij PHP is het lastig om correcte code in Javascript te schrijven. De opkomst van (gecompileerde) talen voor type annotaties in Javascript \[5\] \[6\] helpen hier een beetje bij. Deze annotaties geven echter geen feitelijke garanties wanneer je de code draait, en zijn daarmee eerder een pleister over het probleem dan een echte oplossing. Rust Een snelle opkomer is de programmeertaal Rust \[7\]. Deze taal bestaat sinds 2010, en wordt actief gesponsord door Mozilla. In de basis is het een systeemtaal: het lijkt erg op C++, en het genereert ook een uitvoerbaar bestand met AMD64 assembly. Het is dus geen scriptingtaal zoals PHP en Javascript. Voordeel hiervan is dat je code volledig gecontroleerd is, voordat er ook maar iets uitgevoerd wordt. Zo heb je meer zekerheid dat wat op je servers draait ook ècht goed is. Veiliger dan C++, sneller dan Go Ditzelfde geldt ook voor de C++, maar bij C++ kan gemakkelijk ongeldig geheugen geadresseerd worden. Het komt vrij vaak voor dat er achteraf buffer overflows in C++-code blijken te zitten. Dit probleem is zo goed als onmogelijk als de code wordt geschreven in Rust, dankzij een goed ontwerp van de standaard library en de aanwezigheid van het Borrow Checker-mechanisme \[8\]. Dit component controleert bij het compileren dat er alleen kan worden gewerkt met bestaande objecten in goed georganiseerde stukken geheugen. Garbage Collection is het gangbare alternatief voor het Borrow Checker-mechanisme. Een Garbage Collected taal zoals Go biedt op die manier vergelijkbare voordelen als Rust. Helaas heeft Garbage Collection performance-nadelen, en geeft het minder grip op geheugengebruik. Samengevat is Rust dus minstens zo snel als C++, en heeft het bovendien de correctheidsgaranties van talen als Java, Go en functionele programmeertalen. Dat maakt Rust zeer geschikt voor toepassingen waarbij performance en security cruciaal zijn. Deze garanties komen ook goed uit bij het ontwikkelen van embedded systemen zoals in een Internet of Things-context. Rust compileert naar LLVM, en kan dus ook code genereren voor ARM chipsets \[9\]. Nog een klein zijspoortje naar Go, omdat het gezien wordt als een uitdager voor Rust: Bij Tweede golf kiezen we niet voor Go o.a. omdat we het typeringssysteem te beperkt vinden. Zo ontbreekt het bij Go aan generics, waardoor de correctheidsgaranties toch minder sterk zijn in vergelijking met Rust. Er valt veel meer te zeggen over deze afweging, maar dit is geen "Rust vs. Go"-artikel \[10\]. Rust als webplatform Als we de Fibonacci-reeks kunnen uitrekenen hebben we nog geen webapplicatie. Eerst hebben we een webplatform voor Rust nodig dat de functionaliteiten biedt die normaal Symfony voor ons regelt. Dit webplatform bestaat simpelweg nog niet \[11\] \[12\]. Omdat we niet bang zijn om onze handen vuil te maken, zijn we een paar maanden geleden begonnen zelf zo'n raamwerk voor backends te ontwikkelen. Hiermee hopen we de drempel om Rust te gebruiken weg te nemen, in eerste instantie voor onze eigen ontwikkelaars. Ons raamwerk bestaat uit en gebruikt: REST, HTTP server en routering: Rocket \[13\] Input validatie: Serde \[14\] ORM / Database: Diesel \[15\] + Postgres Authenticatie: JSON Web Tokens met Medallion \[16\] Onze ervaringen tot nu toe? Wat we ten eerste merken is dat Rust en Rocket moeiteloos onze performance-eisen halen \[17\]. Verder wordt de documentatie van elk stuk Rust-software via Cargo \[18\] met een eenduidige stijl gegenereerd. Er is dus een basisniveau van referentiemateriaal voor alles in het Rust-ecosysteem. We zien ook dat elk van deze libraries en Rust als taal een heel levendige community hebben. Issues en merge requests worden ongelofelijk snel opgepikt en opgelost. Zijn er ook nadelen? Rust voor web klinkt dus veelbelovend. Soms is de community wel net iets tè levendig: onder andere Diesel heeft tijdens de ontwikkeling van ons platform ten minste eenmaal de API volledig omgegooid. Hopelijk kalmeert dit wanneer libraries meer in productie worden ingezet. Ook heeft Rust een steile leercurve: het is moeilijk om de Borrow Checker gerust te stellen dat het geheugen overal goed wordt gebruikt. Even snel een web app programmeren in Rust zit er hierdoor waarschijnlijk niet in. En ook niet iedere programmeur zal affiniteit kunnen kweken en in de taal kunnen werken. Toekomst voor Rust Hoewel we bij Tweede golf erg gecharmeerd zijn van Rust, zullen we voor minder veeleisende applicaties Node.js als backend platform blijven gebruiken. Ook Symfony zal voorlopig belangrijk blijven. Voor high performance backendsystemen die degelijk en duurzaam moeten zijn denken we echter dat Rust de toekomst is. In een volgende blogpost zullen we dieper ingaan op de ontwikkeling van ons Rust webplatform. Overweeg je Rust al voor je product (applicatie of embedded systeem) of wil je de mogelijkheden verkennen? Onze teams helpen je met prototyping of implementatie. Neem contact op met Erik of Hugo. Referenties: \[1\] https://whydoesitsuck.com/why-does-php-suck/ \[2\] https://ponyfoo.com/articles/npm-meltdown-security-concerns \[3\] https://www.destroyallsoftware.com/talks/wat \[4\] https://caniuse.com/ \[5\] https://coffeescript.org/ \[6\] http://www.typescriptlang.org/ \[7\] https://www.rust-lang.org/ \[8\] https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html \[9\] http://blog.japaric.io/quickstart/ \[10\] http://julio.meroh.net/2018/07/rust-vs-go.html \[11\] https://www.arewewebyet.org/ \[12\] https://github.com/flosse/rust-web-framework-comparison \[13\] https://rocket.rs/ \[14\] https://serde.rs/ \[15\] https://diesel.rs/ \[16\] https://docs.rs/medallion/2.2.3/medallion/ \[17\] https://medium.com/sean3z/rest-api-node-vs-rust-c75aa8c96343 \[18\] https://doc.rust-lang.org/beta/rustdoc/what-is-rustdoc.html