How Tencent Docs Seamlessly Switches Between Monolith and Microservices
Tencent Docs tackles the trade‑offs of monolithic and microservice architectures by implementing a flexible, automated system that merges selected microservices into a few monoliths for private‑cloud scenarios, reducing runtime costs, deployment overhead, image size, memory usage and CPU consumption while preserving performance and scalability.
1. Introduction: Architecture Challenges
Software architecture has no silver bullet; a good design must be continuously iterated. Tencent Docs faced the need to balance monolithic and microservice designs to meet diverse business scenarios, making its approach valuable for similar projects.
2. Balancing Monolith and Microservices
Microservices offer decoupling, isolation, and elasticity but bring high runtime, deployment, and image‑distribution costs, especially in private‑cloud deployments where only dozens to hundreds of users exist. A monolithic approach reduces these costs but risks losing microservice benefits. Tencent Docs therefore designed a flexible strategy that can merge many microservices into a few monoliths for private deployments while keeping a microservice layout for large‑scale C‑end scenarios.
3. Practice: Automated Switching Tool
The monolith tool automates service merging based on a YAML‑like configuration file that lists which microservices should be combined into each monolith module.
modules:
- name: monolith-module1
merged_servers:
- name: microserver1
...
- name: microserver2
...
- name: monolith-module2
merged_servers:
- name: microserver3
...
- name: microserver4
...This configuration drives a CI pipeline that detects changes and generates the merged monolith code for each merge request.
Challenge 1: High Runtime, Deployment, and Image Costs
Each microservice runs its own tRPC‑Go runtime, consuming excessive memory and CPU, and each requires a separate container image (total >10 GB). Merging into monoliths eliminates redundant runtimes and reduces image size.
Challenge 2: Divergent Configurations
Microservices use varied configuration styles (local files vs. trpc_go.yaml plugins). A unified configuration component was created, and example configurations are shown below.
# Microservice config
plugin:
config:
app:
providers:
serverA:
app.yaml:
foo: bar
foz: baz # Monolith config
plugin:
config:
app:
providers:
serverA:
app.yaml:
foo: bar
foz: baz
serverB:
app.yaml:
zoo: bar
zoz: baz
...Plugin configuration conflicts were resolved by namespacing each service under the plugin key.
# Original plugin config (conflict)
plugin:
type-foo:
name-bar:
key1: value1
key2: value2 # Refactored plugin config (namespaced)
plugin:
type-foo:
name-bar:
service1:
key1: value1
key2: value2
service2:
key1: value2
key2: value1
...Challenge 3: Global Variable Modifications
Modifying global variables such as http.DefaultServerCodec or restful.Marshaller in a microservice is safe, but in a monolith it can affect all merged services. Two strategies were adopted:
Strategy 1: Develop a common plugin that encapsulates required changes via a new cgi protocol (illustrated in the image below).
Strategy 2: Isolate services that need the same global modification into separate modules, preventing cross‑service impact.
Challenge 4: Hidden Bugs
Some bugs only surface after merging. Example:
func Register(s *server.Server) {
someserverpb.RegisterSomeServerSomeService(s, newSomeServiceImpl())
}In a monolith, the registration may overwrite previously registered services, causing 404 errors. The author recommends a “binary search‑style” debugging method to isolate the offending service.
Challenge 5: Continuous Changes
After merging, ongoing changes can break compatibility. Automated checks in each merge request verify that configuration and code still satisfy merging constraints, with plans to add interface and end‑to‑end tests.
4. Results: Benefits of the Monolith Architecture
Switching to a monolith dramatically reduces resource consumption. For a representative module (originally four microservices), the monolith version achieved:
Binary size: 68 MB vs. 264 MB (≈74% reduction)
Memory usage: 670 MB vs. 1 722 MB (≈61% reduction)
CPU usage: 4.99 cores vs. 6.43 cores (≈22% reduction)
Average request latency: 9.3 ms vs. 9.8 ms (≈5% reduction)
Extrapolating to full‑scale monolithization predicts a 75% drop in total image size and a 96% reduction in total memory consumption, while preserving the performance advantages of microservices.
The final generated template (simplified) looks like this:
// Code generated by backend/tools/monolith, DO NOT EDIT.
// Package service rpc entry layer.
package service
import (
{{- range .MergedServers}}
{{.}}service "docx/backend/application/{{.}}/service"
{{- end}}
"git.code.oa.com/trpc-go/trpc-go/server"
)
// Register registers pb service implementations.
func Register(s *server.Server) {
{{- range .MergedServers}}
{{.}}service.Register(s)
{{- end}}
}This concise implementation improves development efficiency, lowers maintenance cost, and enables flexible switching between monolithic and microservice deployments.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
