BFF 架构演进
单体服务
- 单体服务是指一个独立的应用程序,包含了所有的功能和业务逻辑。这种架构方式在小型应用程序中很常见。
- 随着应用程序的功能越来越多,代码库也会越来越大,维护起来也会变得更加困难。此外,单体服务的整体复杂度也会增加,这可能导致软件开发周期变长,质量下降,并且系统的扩展性也会受到限制。
微服务
- 为了应对这些问题,许多公司开始使用微服务架构。微服务是指将一个大型应用程序拆分成若干个小型服务,每个服务负责执行特定的任务。这种架构方式可以帮助公司更快地开发和部署新功能,并提高系统的可扩展性和可维护性。
- 这种方式会有以下问题:
- 域名开销增加。
- 各个端有大量的个性化需求:
- 数据聚合 某些功能可能需要调用多个微服务进行组合。
- 数据裁剪 后端服务返回的数据可能需要过滤掉一些敏感数据,以保证安全性。
- 数据适配 后端返回的数据可能需要针对不同端进行数据结构的适配,后端返回
XML
,但前端需要JSON
。 - 数据鉴权 不同的客户端有不同的权限要求。
- 数据聚合 某些功能可能需要调用多个微服务进行组合。
BFF
BFF
是Backend for Frontend
的缩写,指的是专门为前端应用设计的后端服务。- 主要用来为各个端提供代理数据聚合、裁剪、适配和鉴权服务,方便各个端接入后端服务。
- BFF 可以把前端和微服务进行解耦,各自可以独立演进。
网关
API
网关是一种用于在应用程序和API
之间提供安全访问的中间层。- API 网关还可以用于监控 API 调用,路由请求,以及在请求和响应之间添加附加功能(例如身份验证,缓存,数据转换,压缩、流量控制、限流熔断、防爬虫等)。
- 网关和 BFF 可能合二为一
集群化
- 单点服务器可能会存在以下几个问题:
- 单点故障:单点服务器只有一台,如果这台服务器出现故障,整个系统都会停止工作,这会导致服务中断。
- 计算能力有限:单点服务器的计算能力是有限的,无法应对大规模的计算需求。
- 可扩展性差:单点服务器的扩展能力有限,如果想要提升计算能力,就必须改造或者替换现有的服务器。
- 这些问题可以通过采用服务器集群的方式来解决。服务器集群是指将多台服务器组合在一起,共同完成一项任务。服务器集群可以提高系统的可用性和可扩展性,从而提升系统的整体性能。
环境搭建
微服务
- 微服务是一种架构模式,它将单个应用程序划分为小的服务,每个服务都独立运行并且可以使用不同的语言开发。这种架构模式使得应用程序变得更容易开发、维护和扩展。
- 微服务架构通常会有许多不同的服务,这些服务可能位于不同的机器上,因此需要使用某种通信协议来进行通信。
- 因为
RPC
协议比HTTP
协议具有更低的延迟和更高的性能,所以用的更多。
RPC
RPC(Remote Procedure Call)
是远程过程调用的缩写,是一种通信协议,允许程序在不同的计算机上相互调用远程过程,就像调用本地过程一样
sofa-rpc-node
sofa-rpc-node
是基于Node.js
的一个RPC
框架,支持多种协议。
Protocol Buffers
Protocol Buffers
(简称 protobuf)是Google
开发的一种数据序列化格式,可以将结构化数据序列化成二进制格式,并能够跨语言使用
Zookeeper
ZooKeeper
是一个分布式协调服务,提供了一些简单的分布式服务,如配置维护、名字服务、组服务等。它可以用于管理分布式系统中的数据。
缓存
- 一般会使用多级缓存,本地做一层 LRU 缓存,再做一层远程服务器的 Redis 缓存。这些缓存层的优先级通常是依次递减的,即最快的缓存层位于最顶层,最慢的缓存层位于最底层。越上层的缓存缓存时间一般越短。
RabbitMQ
RabbitMQ
是一个开源的消息代理,用于在应用程序之间进行消息传递。它实现了高级消息队列协议(AMQP
),并且支持多种消息协议。- 消息生产者将消息发送到
RabbitMQ
服务器。 RabbitMQ
服务器将消息存储在队列中。- 消息消费者从队列中获取消息并进行处理。
- 当消息消费者处理完消息后
RabbitMQ
服务器将消息删除。
- 消息生产者将消息发送到
docker 容器安装
docker pull mysql
docker pull redis
docker pull zookeeper
docker pull rabbitmq:management
# mysql 的密码初始化为 123456
docker run -d --hostname localhost -e MYSQL_ROOT_PASSWORD=123456 --name mysql -p 3306:3306 mysql
docker run -d --hostname localhost --name redis -p 6379:6379 redis
docker run -d --hostname localhost --name zookeeper -p 2181:2181 zookeeper
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 docker.io/rabbitmq:management
- -d 后台进程运行
hostname
主机名- name 容器名称
-p port: port
端口映射 本地端口:容器端口- -e 环境变量
执行完上面命令后执行 docker ps
看看我们是否所有都启动成功了。
# 在mysql中创建数据库 bff, 并且创建表 user
create table user (id varchar(64) comment '编号',username varchar(20) comment '用户名',phone varchar(15) comment '手机号码') comment '用户表'
项目初始化
- 项目目录
├── bff 用于和前端交互的 bff 胶水层
│ ├── index.js
│ ├── middleware
│ │ ├── cache.js
│ │ ├── mq.js
│ │ └── rpc.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── store
│ │ └── index.js
│ └── utils
│ └── log.js
├── user 用户相关的微服务函数
│ ├── client.js
│ ├── index.js
│ ├── package.json
│ └── pnpm-lock.yaml
└── write-logger 写入日志的服务
├── index.js
├── logger.txt
├── package.json
└── pnpm-lock.yaml
用户微服务
在 user 中再新建一个 index.js
文件,写入如下代码,主要是在函数中连接数据库,然后创建 Zookeeper
注册中心,创建微服务服务器,将其中的函数注册到 Zookeeper
中,用于给外部进行调用,我们这里主要注册两个函数。
- 创建有用户的函数
- 获取用户的函数
// user package.json
{
"name": "user",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa-logger": "^3.2.1",
"koa-router": "^12.0.0",
"mysql2": "^2.3.3",
"sofa-rpc-node": "^2.8.0",
"uuid": "^9.0.0"
}
}
const {
// 创建 rpc 服务器
server: { RpcServer },
// 创建注册中心,维护服务的注册信息,帮助节点和客户端找到对方
registry: { ZookeeperRegistry },
} = require('sofa-rpc-node')
const mysql = require('mysql2/promise')
const { v4: uuidv4 } = require('uuid')
const logger = console
let connection
// 创建一个注册中心,用于注册微服务
const registry = new ZookeeperRegistry({
// 记录日志
logger,
// zookeeper 的地址
address: 'localhost:2181',
})
// 创建 rpc 服务器的实例
// 客户端连接 rpc 服务器的时候可以通过 zookeeper, 也可以直连 rpc 服务器
const server = new RpcServer({
logger,
registry,
// 注意:这里端口要检查是否被占用
port: 10000,
})
;(async function () {
connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'bff',
})
// 添加服务接口
server.addService(
{
// 约定格式为域名反转+领域模型的名称
interfaceName: 'com.xmllein.user',
},
{
async createUser(username) {
const sql = `INSERT INTO user(id,username,phone) VALUES('${uuidv4()}','${username}',185);`
const rows = await connection.execute(sql)
return {
message: '创建成功',
data: rows,
success: true,
}
},
async getUserInfo(username) {
const sql = `SELECT id,username,phone from user WHERE username='${username}' limit 1`
const [rows] = await connection.execute(sql)
return {
message: '查询成功',
data: rows[0],
success: true,
}
},
}
)
// 启动 rpc 服务
await server.start()
// 把启动好的 rpc 服务器注册到 zookeeper 中
await server.publish()
console.log('微服务启动')
})()
client.js
用于连接 rpc 服务器,然后调用其中的函数
const {
client: { RpcClient },
registry: { ZookeeperRegistry },
} = require('sofa-rpc-node')
// 设置日志记录器
const logger = console
// 创建 Zookeeper 注册中心
const registry = new ZookeeperRegistry({
logger,
address: '127.0.0.1:2181',
})
;(async function () {
// 创建 RPC 客户端
const client = new RpcClient({ logger, registry })
// 创建 RPC 服务消费者
const userConsumer = client.createConsumer({
// 指定服务接口名称
interfaceName: 'com.xmllein.user',
})
// 等待服务就绪
await userConsumer.ready()
// 调用服务方法
const result = await userConsumer.invoke('createUser', ['test'], {
responseTimeout: 3000,
})
// 输出结果
console.log(result)
process.exit(0)
})()
日志微服务
// write-logger package.json
{
"name": "write-logger",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"fs-extra": "^11.1.0"
}
}
安装好依赖后,write-logger
目录下创建一个 index.js
文件,写入如下代码。
// 用于连接 RabbitMQ 服务器
const amqp = require('amqplib')
const fs = require('fs-extra')
;(async function () {
// 连接 MQ 服务器
const mqClient = await amqp.connect('amqp://localhost')
// 创建一个通道
const logger = await mqClient.createChannel()
// 创建一个名称为 logger 的队列,如果已经存在了,不会重复创建
await logger.assertQueue('logger')
// 消费队列里的消息
logger.consume('logger', async (event) => {
// 往本地写入日志文件,到时候 bff 端会以 Buffer 二进制的形式发送数据过来
await fs.appendFile('./logger.txt', event.content.toString() + '\n')
})
})()
bff 端
// package.json
{
"name": "bff",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"fs-extra": "^11.1.0",
"koa": "^2.14.1",
"koa-logger": "^3.2.1",
"koa-router": "^12.0.0",
"sofa-rpc-node": "^2.8.0"
}
}
- rpc 中间件
// bff/middleware/rpc.js
const {
// 创建 rpc 服务器
client: { RpcClient },
// 创建注册中心,维护服务的注册信息,帮助节点和客户端找到对方
registry: { ZookeeperRegistry },
} = require('sofa-rpc-node')
const logger = console
const rpcMiddleware = (options = {}) => {
return async function (ctx, next) {
// 创建一个注册中心,用于注册微服务
const registry = new ZookeeperRegistry({
// 记录日志
logger,
// zookeeper 的地址
address: 'localhost:2181',
})
const client = new RpcClient({
logger,
registry,
})
const interfaceNames = options.interfaceNames || []
const rpcConsumers = {}
for (let i = 0; i < interfaceNames.length; i++) {
const interfaceName = interfaceNames[i]
// 创建 RPC 服务器的消费者,通过消费者调用 rpc 接口
const consumer = client.createConsumer({ interfaceName })
// 等待服务就绪
await consumer.ready()
rpcConsumers[interfaceName.split('.').pop()] = consumer
}
ctx.rpcConsumers = rpcConsumers
await next()
}
}
module.exports = rpcMiddleware
- 缓存中间件
// bff/store/index.js
/*
CacheStore:管理 Store 容器的类,优先使用 LRU 算法调用本地内存缓存,命中不到再去调用 Redis 缓存
MemoryStore:本地内存 Store
RedisStore:远程 Redis 服务器 Store
*/
const LRUCache = require('lru-cache')
const Redis = require('ioredis')
// 一般越上层的缓存过期时间就越短
class CacheStore {
constructor() {
this.stores = []
}
add(store) {
this.stores.push(store)
return this
}
async set(key, value) {
for (const store of this.stores) {
await store.set(key, value)
}
}
async get(key) {
for (const store of this.stores) {
const value = await store.get(key)
if (value !== undefined) {
return value
}
}
}
}
class MemoryStore {
constructor() {
this.cache = new LRUCache({
max: 100,
// 一分钟过期
ttl: 1000 * 60,
})
}
async set(key, value) {
this.cache.set(key, value)
}
async get(key) {
return this.cache.get(key)
}
}
class RedisStore {
constructor(
options = {
host: 'localhost',
port: '6379',
}
) {
this.client = new Redis(options)
}
async set(key, value) {
this.client.set(key, JSON.stringify(value))
}
async get(key) {
const val = await this.client.get(key)
return val ? JSON.parse(val) : undefined
}
}
module.exports = {
CacheStore,
MemoryStore,
RedisStore,
}
bff/middleware/cache.js
中写入缓存中间件的代码。
const { RedisStore, CacheStore, MemoryStore } = require('../store')
const cacheMiddleware = (options) => {
return async function (ctx, next) {
// 创建一个缓存实例
const cacheStore = new CacheStore()
// 添加一些缓存层
cacheStore.add(new MemoryStore())
cacheStore.add(new RedisStore(options))
ctx.cache = cacheStore
await next()
}
}
module.exports = cacheMiddleware
- RabbitMQ 中间件
//bff/middleware/mq.js 中写入如下代码,用于连接 RabbitMQ 消息队列服务器
// RabbitMQ
const amqp = require('amqplib')
const MQMiddleware = (
options = {
url: 'amqp://localhost',
}
) => {
return async function (ctx, next) {
// 连接 MQ 服务器
const mqClient = await amqp.connect(options.url)
// 创建一个通道
const logger = await mqClient.createChannel()
// 创建一个名称为 logger 的队列,如果已经存在了,不会重复创建
await logger.assertQueue('logger')
// 将其挂载到 ctx.channels 中,其它地方就能进行调用了
ctx.channels = {
logger,
}
await next()
}
}
module.exports = MQMiddleware
- log 日志工具
// 在 bff/utils/log 路径下,封装日志记录函数用于发送消息到 RabbitMQ 消息队列供其它服务消费来写入日志
module.exports = log = (ctx, data) => {
// 把用户信息写入文件进行持久化,通过 Buffer 二进制发送
ctx.channels.logger.sendToQueue(
'logger',
Buffer.from(
JSON.stringify({
method: ctx.method,
path: ctx.path,
...data,
})
)
)
}
bff/index.js
中写入如下代码,启动 BFF 服务
const Koa = require('koa')
const router = require('koa-router')()
const logger = require('koa-logger')
const cacheMiddleware = require('./middleware/cache')
const MQMiddleware = require('./middleware/mq')
const rpcMiddleware = require('./middleware/rpc')
const log = require('./utils/log')
const app = new Koa()
app.use(logger())
app.use(
rpcMiddleware({
interfaceNames: ['com.xmllein.user'],
})
)
app.use(cacheMiddleware())
app.use(MQMiddleware())
app.use(router.routes()).use(router.allowedMethods())
router.get('/getUserInfo', async (ctx) => {
const username = ctx.query.username
const cacheKey = `${ctx.method}-${ctx.path}-${username}`
let cacheData = await ctx.cache.get(cacheKey)
// 把用户信息写入文件进行持久化,通过 Buffer 二进制发送
log(ctx, {
username,
})
if (cacheData) {
ctx.body = cacheData
return
}
const {
rpcConsumers: { user },
} = ctx
const userInfo = await user.invoke('getUserInfo', [username])
cacheData = {
userInfo,
}
await ctx.cache.set(cacheKey, cacheData)
ctx.body = cacheData
})
// 插入一条 user 数据
router.get('/createUser', async (ctx) => {
const username = ctx.query.username
log(ctx, {
username,
})
const {
rpcConsumers: { user },
} = ctx
const res = await user.invoke('createUser', [username])
ctx.body = res
})
app.listen(3000, () => {
console.log('启动成功')
})
然后我们把上面编写的三个服务通过命令行先跑起来,在各个目录下运行 npm run dev
即可
然后使用 rest-client
插件进行测试,postmane.http
文件如下
@BASE_URL = http://127.0.0.1:3000
### 添加用户
GET {{BASE_URL}}/createUser?username=test1 HTTP/1.1
### 获取用户信息
GET {{BASE_URL}}/getUserInfo?username=test1 HTTP/1.1