软件架构简介(单片架构、分层架构、微服务架构)
介绍
在这篇文章中,我们将回答以下 5 个问题:
- 为什么我们需要软件架构?
- 什么是单片架构?
- 什么是分层架构?
- 什么是微服务架构?
- 其他架构
如果你是自学成才的开发人员,或者行业新手,或者类似的情况,“软件架构”这个概念可能会让你望而生畏。企业架构师的薪水很高,因为构建高质量的软件架构非常困难,而且需要丰富的经验。
您可能有以下顾虑:
- 我该如何在缺乏经验的情况下设计解决方案?
- 架构和设计模式那么多,选错了怎么办?
- 如果不知道要编写的代码的所有细节,我该如何创建整个架构?
通过这篇文章,我们将解决这些问题并弄清楚这个架构到底是什么。
为什么我们需要软件架构?
任何聪明的傻瓜都能把事情弄得更大、更复杂、更暴力。而要反其道而行之,则需要一点天才——以及巨大的勇气。
EF Schumacher 的《小即是美》一书中的这句话,深刻地诠释了软件架构的诸多含义。作为一名开发者,用复杂的方法解决复杂问题总是更有趣。比起从 A 点走到 B 点,再把弹珠扔进杯子里,建造一台鲁布·戈德堡机械更有趣。
不幸的是,作为一名开发人员和架构师,间接解决问题并不能给你带来额外的加分。真正的收入来源是,用简单的方法解决复杂的问题。
设计软件架构是关于安排系统组件以最适合系统所需的质量属性。
- 用户关心你的系统是否快速、可靠、可用
- 项目经理关心系统是否按时、按预算交付
- CEO 关心的是该系统是否能为公司带来增量价值
- 安全主管关心系统是否受到保护,免受恶意攻击
- 应用程序支持团队关心系统是否易于理解和调试
如果不牺牲系统质量,就不可能让所有人都满意。因此,在设计软件架构时,必须确定哪些质量属性对于特定的业务问题最为重要。以下是一些质量属性的示例:
- 性能——您需要等待多长时间才能看到旋转的“加载”图标消失?
- 可用性——系统运行的时间百分比是多少?
- 可用性——用户能否轻松了解系统的界面?
- 可修改性——如果开发人员想向系统添加一个功能,这是否容易做到?
- 互操作性——该系统是否能与其他系统良好配合?
- 安全性——系统周围是否有安全堡垒?
- 可移植性——系统是否可以在许多不同的平台上运行(例如 Windows、Mac 和 Linux)?
- 可扩展性——如果您的用户群迅速增长,系统是否可以轻松扩展以满足新的流量?
- 可部署性——将新功能投入生产是否容易?
- 安全性——如果软件控制物理事物,它是否会对现实的人造成危害?
根据您正在构建或改进的软件类型,某些属性可能对成功更为关键。如果您是一家金融服务公司,那么您系统最重要的质量属性可能是安全性(安全漏洞可能导致您的客户损失数百万美元),其次是可用性(您的客户需要始终能够访问其资产)。如果您是一家游戏或视频流媒体公司(例如 Netflix),那么您的首要质量属性将是性能,因为如果您的游戏/电影一直卡顿,那么就没有人会玩/观看它们。
如你所见,构建软件架构的过程并非寻找最佳工具和最新技术,而是交付一个高效运行的系统。但为什么我们需要架构来实现这一点呢?
根据《软件架构实践》一书,软件架构对于项目成功至关重要的原因有 13 个。从这 13 个原因中,我挑选了一些可能与小型团队或个人开发者产生共鸣的原因:
- 架构赋能质量属性——例如,由于系统的去中心化特性,点对点架构自然具有高可用性。微服务架构由于职责分离,具有高度的可修改性。
- 架构使利益相关者之间的沟通成为可能——当架构与公司结构非常相似时,每个人都知道他们负责软件的哪个部分。
- 架构关注的是组件的组装而不是创建——架构不是关注代码的编写方式,而是迫使我们思考系统中的组件如何相互通信。
- 架构限制了设计选择,从而可以在其他领域发挥创造力——通过在软件项目中定义一些边界,您可以知道哪些领域可以发挥创造力而不会损害系统的进度或质量。
书中指出,项目开始时做出的设计决策具有不成比例的权重,并限制了以后更改软件某些区域的能力,因此从一开始就花时间了解软件的需求并尽最大努力进行设计非常重要。
经验丰富的架构师在这方面比新手更有优势。他们能够预见更长远的未来,并预测某些设计决策将如何影响系统。
对于我们这些在建筑设计方面经验不足的人来说(包括我自己),我们必须接受事物不可能完美,并无论如何都要进行设计。是的,必须进行修改,但还有什么其他选择呢?
市面上有很多架构可供选择,但并非所有架构都“对初学者友好”,有时甚至需要多年的经验才能正确实现。例如,创建一个有效的点对点架构(例如比特币、Bittorrent)并非易事。
为了简单起见,我将介绍三种涵盖各种用例的常见架构。除非您有特定的原因和设计经验来证明选择其他方案的合理性,否则无论您使用哪种技术栈,这三种架构中的一种通常都可以满足您的需求。
我选择这三种架构是因为它们在软件社区中出现的频率最高。当然还有其他一些有用的架构,例如事件驱动架构、客户端-服务器架构、微内核架构等等,但如果你不了解以下三种架构,那么尝试这些高级架构就毫无意义了。
更新(2020年10月24日):感谢Carlos G在评论中指出这一点——在讨论这三种架构时,它们并非完美的对比。单体架构和微服务架构探讨的是应用程序的分布式结构,而分层架构则更笼统地指代如何设计单体应用或单个微服务的内部组件。换句话说,单体架构并不意味着它的“分层”设计不佳。同样,微服务架构也并不意味着其代码库的“分层”设计完美。总而言之,下面的“单体架构”示例与其说是架构,不如说是缺乏职责分离(即“分层”)且代码编写糟糕的示例。此外,下面的“分层”示例更准确地应该被归类为“编写良好的单体架构”。
我认为学习软件架构最简单的方法就是在实践中观察。您在阅读代码库时肯定见过不同的架构,但可能还没有意识到它们。我下面的示例并非旨在演示编写应用程序的正确方法,而是明确指出您可以在代码库中使用的各种架构。为了演示,我将在一个 Web 应用程序环境中使用 NodeJS、ExpressJS 和 MongoDB。
单片架构(“分层”设计不佳)
下面提到的所有代码都存储在我的Github 上的单体架构存储库中
如果你曾经在网上学习过如何构建 Web 应用的教程,那么你很可能构建过单体应用。这是迄今为止最容易理解的入门方法。
单片架构描述了一种架构,其中所有以下组件都集中在一个代码库中:
- 视图(即 HTML、CSS、Javascript)
- 应用程序/业务逻辑(即 ExpressJS)
- 数据访问/数据库(例如 MongoDB)
尽管这种架构看似低效,但并非所有行业专家都认为它毫无用处。例如,Martin Fowler主张在启动新应用程序时使用单体架构。他指出,那些以微服务架构启动应用程序的人通常最终会浪费时间和精力,因为直到应用程序变得复杂时,你才会开始看到这种架构的好处。
他建议从单片架构开始,当架构变得太大而无法整体处理时,再将其重构为分层或微服务架构。
让我们看一下简单的单片架构的内部结构。
应用程序结构
下面提到的所有代码都存储在我的Github 上的单体架构存储库中
这段代码的主要特点是应用程序各部分之间缺乏区分。例如,在 中app.js
,你会看到与数据库、服务器甚至一些 API 端点的连接。
const express = require("express");
const app = express();
const mongoose = require("mongoose");
const cors = require("cors");
const bodyParser = require("body-parser");
// This will allow our presentation layer to retrieve data from this API without
// running into cross-origin issues (CORS)
app.use(cors());
app.use(bodyParser.json());
// ============================================
// ========== DATABASE CONNECTION ===========
// ============================================
// Connect to running database
mongoose.connect(
`mongodb://${process.env.DB_USER}:${process.env.DB_PW}@127.0.0.1:27017/monolithic_app_db`,
{ useNewUrlParser: true }
);
// User schema for mongodb
const UserSchema = mongoose.Schema(
{
name: { type: String },
email: { type: String },
},
{ collection: "users" }
);
// Define the mongoose model for use below in method
const User = mongoose.model("User", UserSchema);
function getUserByEmail(email, callback) {
try {
User.findOne({ email: email }, callback);
} catch (err) {
callback(err);
}
}
// set the view engine to ejs
app.set("view engine", "ejs");
// index page
app.get("/", function (req, res) {
res.render("home");
});
// ============================================
// ============ API ENDPOINT ================
// ============================================
app.post("/register", function (req, res) {
const newUser = new User({
name: req.body.name,
email: req.body.email,
});
newUser.save((err, user) => {
res.status(200).json(user);
});
});
// ============================================
// ============== SERVER =====================
// ============================================
app.listen(8080);
console.log("Visit app at http://localhost:8080");
如果我们想添加另一个 API 端点,则需要编辑app.js
。如果我们想添加另一个数据库模型,则需要编辑app.js
。即使我们想修改中的 API 调用home.ejs
,也可能需要更改app.js
。
我们看到的这种集中化在未来是不可持续的,但这并不意味着它全是坏事。如上所述,您可能会发现从这样的做法开始很有用,随着应用程序的增长,开始将各个部分重构为更易于管理的架构。我们可以采用分层架构的概念来进行重构。
单片架构(具有更好的“分层”或“n层”设计)
一段时间后,你的单体应用会变得越来越大,你开始招聘新员工,它很快就会变得一团糟。虽然许多现代架构师会采用微服务设计来解决这个问题(下一节会介绍),但更好地划分应用职责的另一个选择是重构你的单体应用。
分层架构将应用程序划分为多个层。通常,您将找到以下层(按顺序):
- 表示层
- 业务层
- 数据访问层
您可能还会偶然发现其他术语:
- 表示层
- 应用层
- 领域层
- 持久层
不管您如何称呼这些层,重点是创建“关注点分离”,其中每个层只允许使用其正下方的层。
在某些情况下,您可能有一个具有实用功能的共享层。在这种情况下,您可以创建一个额外的层,该层被视为“开放”,供所有层使用。其他层被视为“封闭”,这意味着它们只能使用其下方的层。
应用程序结构
下面提到的所有代码都存储在我的Github 分层架构存储库中
分层架构的关键在于,每一层只能使用其下一层。在上面链接的示例应用中,我创建了一个基本的用户身份验证流程来演示这一概念。此外,每层的代码都存储在一个标记清晰的文件夹中(即business-layer
)。
在我们的示例中,流程包含以下步骤:
- 表示层从 HTML 用户表单发出调用
- 表示层 javascript 处理表单并执行对业务层的调用
- 业务层处理表单信息并调用数据访问层
- 数据访问层处理信息并为用户向数据库进行查询
- 数据访问层将信息返回给业务层
- 业务层通过HTTP将信息返回给表示层
- 表示层使用新信息呈现视图
现在让我们通过代码来演示这些步骤。
1. 表示层通过 HTML 用户表单进行调用
<!-- File: home.ejs -->
<!-- On form submit, home.ejs executes the getDataFromBusinessLayer() function -->
<form id="emailform" onsubmit="getDataFromBusinessLayer()">
<input name="email" id="email" placeholder="Enter email..." />
<button type="submit">Load Profile</button>
</form>
2. 表示层javascript处理表单并执行对业务层的调用
// File: presentation-layer-user.js
function getDataFromBusinessLayer() {
event.preventDefault();
const email = $("#email").val();
// Perform the GET request to the business layer
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
$.ajax({
url: `http://localhost:8081/get-user/${email}`,
type: "GET",
success: function (user) {
// Render the user object on the page
// Ommitted for brevity
},
error: function (jqXHR, textStatus, ex) {
console.log(textStatus + "," + ex + "," + jqXHR.responseText);
},
});
}
3. 业务层处理表单信息并调用数据访问层
// File: business-layer-user.js
app.get("/get-user/:useremail", function (req, res) {
// Makes a call to the data access layer
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
const user = User.getUserByEmail(req.params.useremail, (error, user) => {
res.status(200).json({
name: user.name,
email: user.email,
profileUrl: user.profileUrl,
});
});
});
4. 数据访问层处理信息并为用户向数据库进行查询
// File: data-layer-user.js
module.exports.getUserByEmail = (email, callback) => {
try {
// Makes a call to the database
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
User.findOne({ email: email }, callback);
} catch (err) {
callback(err);
}
};
5. 数据访问层将信息返回给业务层
没有相关代码可显示
6. 业务层通过HTTP将信息返回给表示层
没有相关代码可显示
7. 表示层使用新信息呈现视图
没有相关代码可显示
在我们逐步讲解每个步骤的过程中,您可能已经注意到,每个层都负责一项非常具体的任务。这需要更多的思考和时间来实现,但随着项目的发展,可以更好地组织。这也允许多个团队成员同时开发应用程序。您可以让一个团队在数据层实现所有数据库调用,另一个团队在业务层实现 REST API,还有一个团队创建前端用户界面!
这听起来很棒,但这种架构有一个问题没有解决。分层架构仍然以单个应用程序的形式运行。如果要对单个层进行任何重大更改,则必须重新部署整个应用程序才能实现更改。换句话说,您将始终有一个每日/每周/每月的“发布计划”,在此期间,整个应用程序会短暂停机,然后将新的更改发布给公众。
微服务架构
下面提到的所有代码都存储在我的Github 微服务架构存储库中
分层的单体架构适用于许多应用程序,但最近软件行业的一个趋势是向微服务架构迁移。它解决了“发布时间表”问题,并允许开发人员独立设计大型应用程序的每个部分。
如果您的架构的每个部分都是自给自足的,并且不需要应用程序其他部分的任何东西,那么您就拥有了微服务架构。
应用程序结构
在上面提到的代码中,我们的微服务架构分为三个部分:
- 查看服务器(localhost:8080) - 该服务器运行所有前端应用程序逻辑,其中包括
index.html
利用多个微服务的主文件。 - 用户身份验证服务器(localhost:8081) - 该服务器管理所有用户身份验证。
- 游戏服务器(localhost:8082) - 该服务器控制屏幕上播放的游戏。
请注意,每个服务器都在不同的端口上独立运行。这意味着您可以将它们托管在完全不同的服务器上,并且应用程序仍然可以正常工作。应用程序的每个部分都通过 HTTP 协议进行通信,因此可以独立运行。
如果你查看上面链接的代码,你可能会注意到每个微服务内部都存在分层架构。这正是架构变得有点模糊的地方。你可以拥有一个微服务架构,在每个微服务内部都采用分层架构。虽然没有关于如何构建微服务的严格规定,但使用类似分层架构的方式来构建微服务是很常见的。
当我们逐步讲解这个应用程序的各个部分时,请注意我们不再讨论“调用链”,而是讨论 API 端点(即微服务之间的通信)。
微服务 #1 - 用户身份验证(http://localhost:8081)
注意:在生产应用程序中,密码验证并未按预期实现;它仅用于演示,您永远不应该像我在这里一样以纯文本形式存储用户的密码!
该微服务仅负责创建和验证用户。
在许多复杂的应用程序中,一整台服务器都会用于用户身份验证和管理。毕竟,没有用户,就没有应用程序。因此,不仅要实现用户功能,还要维护适当的安全性并保护用户数据,这一点至关重要。
要理解用户身份验证微服务为何如此有用,不妨想象一下一家为用户提供各种服务的大公司。Google 就是一个很好的例子,因为您不仅会使用自己的登录凭据登录 Gmail 和其他核心 Google 服务,还会使用它登录 YouTube 和许多其他应用程序。
想象一下,如果谷歌在每个应用程序中都实现一套用户身份验证方案!这效率极低,因此谷歌创建了一个“微服务”,它不仅为谷歌应用程序提供用户身份验证功能,还为越来越多的第三方应用程序提供类似的功能。这之所以成为可能,是因为身份验证微服务通过强大的 API 与底层基础架构解耦。这正是微服务的目标。
您将在应用程序中看到,我创建了一个比 Google 现有的身份验证微服务简单得多(我说得对吗?)。尽管如此,它演示了如何为一个或多个应用程序实现“身份验证 API”。
下面,您将看到此微服务公开的三个 API 端点:
app.post("/register", function (req, res, next) {
const newUser = new User({
email: req.body.email,
password: req.body.password,
});
User.createUser(newUser, (err, user) => {
res.status(200).json(user);
});
});
app.post("/authenticate", (req, res, next) => {
const email = req.body.email;
const password = req.body.password;
User.getUserByEmail(email, (error, user) => {
if (user && user.password == password) {
// User entered the correct password and we should authenticate them!
res.status(200).json({ authenticated: true });
} else {
// User entered the wrong password
res.status(200).json({ authenticated: false });
}
});
});
app.get("/get-user/:useremail", function (req, res) {
User.getUserByEmail(req.params.useremail, (error, user) => {
res.status(200).json({ email: user.email, password: user.password });
});
});
我们的前端用户应用程序可以使用这三个端点来localhost:8081
管理用户!
微服务 #2 - 游戏(http://localhost:8082)
该微服务全权负责管理通过身份验证微服务注册的所有应用程序用户的游戏结果。
游戏微服务比用户身份验证微服务稍微简单一些,但它展示了如何分离应用程序的核心功能。虽然这是一个简单的游戏,但你可以想象一个更复杂的场景,游戏中包含各种图形元素和用户数据。
以下是 Game 微服务公开的两个 API 端点:
// Register API Call
app.post("/score", function (req, res, next) {
const score = req.body.result;
const winValue = score == "win" ? 1 : 0;
const lossValue = score == "loss" ? 1 : 0;
const email = req.body.email;
// See if user has posted a score already
Game.getUserScoresByEmail(email, (err, user) => {
// If user hasn't posted a score yet, create an entry for their count in the database
if (!user) {
Game.createUserWithScore(
new Game({
email: email,
wins: winValue,
losses: lossValue,
}),
(err, user) => {
res.status(200).json(user);
}
);
} else {
// If user already has posted a score, update his/her win and loss count based on result
Game.updateUserScores(email, winValue, lossValue, (err, count) => {
res.status(200).json(count);
});
}
});
});
app.get("/score/:email", function (req, res, next) {
Game.getUserScoresByEmail(req.params.email, (err, user) => {
res.status(200).json(user);
});
});
用户界面将调用 localhost:8082 来更新用户的游戏统计数据。
整合所有元素:用户界面
我们已经介绍了每个微服务公开的 API 端点,但如果没有用户界面来帮助用户与它们交互,这些端点就毫无用处!
在这个用户界面中,index.html
有一堆点击监听器,当某些事件发生时,它们会执行 API 调用。例如,当用户在注册表单中输入信息并点击注册按钮时,就会触发以下函数,并向端点POST
发送请求/register
。

function register() {
const email = $("#register-email").val();
const pw = $("#register-pw").val();
event.preventDefault();
$.ajax({
type: "POST",
url: "http://localhost:8081/register",
data: JSON.stringify({ email: email, password: pw }),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
setTimeout(() => {
$("#register-message").toggleClass("hide");
}, 3000);
$("#register-message").toggleClass("hide");
},
});
}
您可以看到该url
属性已设置为我们的用户身份验证微服务。
在启动和停止游戏的按钮上还有另一个点击监听器。当游戏停止时,会触发以下函数,并向端点POST
发送请求/score
:

function stopSpinner(position) {
let coordinatesArray = position.slice(7).split(",");
let c1 = Math.abs(parseFloat(coordinatesArray[0]));
let c2 = Math.abs(parseFloat(coordinatesArray[1]));
let result;
let email = localStorage.getItem("email");
if (!email) {
setTimeout(() => {
$("#logged-out-warning").toggleClass("hide");
}, 3000);
$("#logged-out-warning").toggleClass("hide");
}
if (c1 >= 0.9935 && c1 <= 1 && c2 >= 0 && c2 <= 0.12) {
result = "win";
setTimeout(() => {
$("#winner").toggleClass("hide");
}, 3000);
$("#winner").toggleClass("hide");
} else {
result = "loss";
setTimeout(() => {
$("#loser").toggleClass("hide");
}, 3000);
$("#loser").toggleClass("hide");
}
if (email) {
$.ajax({
type: "POST",
url: "http://localhost:8082/score",
data: JSON.stringify({ email: email, result: result }),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
console.log(result);
},
});
}
}
最后,当您单击按钮查看您的分数时,将调用以下函数,并向端点GET
发送请求/score/:email
。

function seeScores() {
const email = localStorage.getItem("email");
$.ajax({
url: `http://localhost:8082/score/${email}`,
type: "GET",
success: function (user) {
$("#wins").text(user.wins);
$("#losses").text(user.losses);
},
error: function (jqXHR, textStatus, ex) {
console.log(textStatus + "," + ex + "," + jqXHR.responseText);
},
});
}
其他架构
虽然我只讨论了单体架构、分层架构和微服务架构,但还有很多其他架构模式可供选择。随着经验的积累,你可能会开始意识到其他架构模式的实用性。
例如,如果你尝试构建一个像 Wordpress 这样的平台,其核心系统可以通过插件进行扩展,那么你可能会选择微内核架构。如果你尝试构建比特币,那么你可能会考虑点对点架构。如果你
想构建一个即时通讯系统或聊天应用程序,那么你可能会考虑事件驱动架构。
最后一点
不要像我最初那样,把软件架构想象成互相排斥的。正如我们上面看到的,你可以拥有一个单体应用,但内部采用“分层”方法。如果你愿意,甚至可以添加一些事件驱动的架构。
因此,当您考虑架构时,只需记住一个应用程序(或微服务)可以有多个“架构”。
结论
无论您的情况如何,总有一款架构适合您。请记住,构建软件解决方案的最终目标有两个:
- 用最简单的方式解决你的问题
- 设计您的架构以满足您在系统中所需的质量属性
如果你能满足这两个要求,你就成功了。别掉进“架构宇航员”的陷阱。别陷入分析瘫痪。构建一个能用的东西,然后就完事了。
文章来源:https://dev.to/zachgoll/introduction-to-software-architecture-monolithic-vs-layered-vs-microservices-452