本文为《全栈Monorepo开发实战(Vue 3+Fastify+Deno+pnpm)》、《前端Serverless 面向全栈的无服务器架构实战》的学习笔记。
全栈Monorepo开发实战
以编写报名登记应用为主线,从零搭建一个基于pnpm的项目。从Deno开始构建模拟的FaaS环境,构建简单的函数注册中心、文件函数服务、邮箱函数服务、计时器函数服务等FaaS服务。接着在Node.js环境中使用时下流行的Prisma、Fastify和MySQL构建后端服务程序。最后使用Vue 3、Vite和Tailwind构建报名登记应用的前端应用。
第1章 Monorepo架构
- 常用的代码组织架构
1.Single-repo
即一个应用的所有项目代码全部在一个代码仓库中管理,模块之间仅仅通过简单的划分或不划分来进行组织。这种方法通常适用于Monolith(巨石)应用,即整个应用作为一个整体进行开发和管理。
2.Multirepo
即多代码仓库,是一种把应用内的各模块彻底隔离和封装的方法,每个模块都存放在一个独立的代码仓库中进行管理。通常,每一个代码仓库有独立的版本号管理、依赖管理、CI/CD(Continuous Integration/Continuous Delivery,持续集成/持续部署)流程。
3.Monorepo
虽然Monorepo和Single-repo都是把一个应用的所有代码放在一个仓库,但是两者有一个本质区别,即Monorepo项目中的各个子项目与Multirepo一样,要进行彻底的隔离和封装。 - Monorepo的优点如下
1.有助于更好、更高效的工作流程
2.更容易管理应用内部之间的依赖关系
3.提供统一的Git提交视图
4.便于统一CI/CD、打包等自动化构建和测试流水线
5.可以极大简化依赖的管理
6.降低多技术融合的成本 - Monorepo的缺点如下
1.相关开发工具不成熟
2.CI/CD流水线较为复杂
3.测试复杂
4.固有复杂性
5.隔离性差 - 全栈Monorepo的关键设计原则
1.从Single-repo逐渐演进到Monorepo
2.明确划分不同技术栈的定位
3.应用层类型协议与JSON Schema
4.最小配置、统一尽可能严格的标准
5.每一个子项目都视为独立的项目
6.任何非官方工具,准备至少两种解决方案第2章 基于pnpm和TypeScript构建 Monorepo项目
pnpm是新一代的npm包管理工具,其名称来源于“performant npm”,旨在提高npm的性能。
为了解决依赖重复下载的问题,pnpm创建了一个全局的包仓库,默认位于${os.homedir}/.pnpm-store/。所有使用pnpm管理的项目的依赖包都存储在该目录下,每个版本只有一份。在单个项目的node_modules/.pnpm中的包通过硬链接的方式引入。这种机制可以节省大量磁盘空间,并且在很多场景下能提高开发效率。
pnpm内置了对Monorepo的支持,在项目根目录创建workspace配置文件pnpm-workspace.yaml,并声明工作空间包含/排除的目录。
通常将全局的开发环境或想要控制的全局依赖放在根目录进行管理。pnpm能从命令行、环境变量以及.npmrc文件中获取配置信息。通过执行pnpm config命令,可以对用户和全局的.npmrc进行更新和编辑。为了方便管理,建议每个Monorepo项目都独立维护自己的.npmrc文件。
pnpm提供了两个钩子函数readPackage、afterAllResolved。钩子函数都定义在.pnpmfile.cjs文件。如果package.json不加type:module字段,Node.js会把.js和.cjs结尾的文件按照CJS解析,把.mjs结尾的文件按照ESM来解析。
使用pnpm init命令初始化一个pnpm项目,并在当前目录生成了package.json文件。为了统一全局的TypeScript配置,并强化对TypeScript的理解,本书准备了一份较为严格tsconfig:@skimhugo/tsconfig。这份配置是根据@sindresorhus/tsconfig修改而来。JavaScript/TypeScript最流行的代码风格检查工具是ESLint。配置一个完整的ESLint较为烦琐,本书准备了一份配置@skimhugo/eslint-config放在npm仓库供读者使用。
在项目根目录新建ESLint配置文件.eslintignore,用于指定不需要检查的文件或文件夹。整个项目是使用VS Code开发,还需要安装ESLint VS Code扩展,将ESLint的检查结果在IDE中展示。整个项目是使用VS Code开发,还需要安装ESLint VS Code扩展,将ESLint的检查结果在IDE中展示。Prettier是一款开源的JavaScript/TypeScript的代码格式化工具。
Prettier的设计理念是“约定大于配置”,为了实现这个理念,Prettier坚持具备很少的配置项,这样做的好处是减少在代码风格上的争论,让开发者更专注于代码的功能实现。在项目根工作目录新建Prettier配置文件.prettierrc.cjs,用于配置具体格式化规则。新建.prettierignore文件,用于设置Prettier忽略的文件列表。最后安装VS Code的Prettier插件。第3章 使用Deno构建简单的注册中心
Deno是基于V8引擎,使用Rust语言和Tokio库构建的JavaScript/TypeScript安全的运行时。
Deno可以在macOS、Linux和Windows平台上运行,不需要安装任何外部依赖。在Linux和Windows平台上,目前仅支持64位系统。Deno还提供了手动安装方式,在地址 https://github.com/denoland/deno/releases 上下载ZIP压缩包。在VS Code扩展菜单栏搜索Deno,选择发布者是denoland的扩展。
Deno的特点:
1.原生TypeScript支持
2.仅支持ESM模块解析
3.增强包管理机制
4.强大的标准库
5.内聚的全局变量
6.安全性
在Monorepo项目中当多种运行时共存时,通常lint工具是与运行时绑定的,而格式化工具希望尽量保持统一。
使用Deno的oak框架开发一个基于localStorage的服务注册中心,作为所有对外提供服务的出口,为了简化烦琐的端口管理和服务地址记录,将会构建一个简单的注册中心来集中管理所有新启动的服务地址,并使用HTML构建简单的注册中心管理页面,最后使用注册中心在线提供注册函数以便后续代码使用。
通常,API会随着时间推移而慢慢改变,有时候会有不兼容的改变。API的URL增加一个前缀,如/v1/registry、/v2/registry。入参和出参可以根据不同版本要求进行改变。
如无特殊说明的话,Deno的程序按照controller、service、model分为三层。
• controller层:负责接收请求并将请求转发给service层,然后将service层的结果返回给客户端。
• service层:负责业务逻辑的处理。
• model层:负责数据的存储和检索。
在Deno中,deps.ts文件是一个特殊的文件,通常位于Deno项目的根目录,用于将外部依赖项集中导入Deno应用程序。在Deno中,数据存储可以分为会话存储和本地存储。Deno在1.10版本引入了Web Storage API,行为和浏览器保持一致,可以存储最大10MB(10485760 Byte)数据的键值对。Deno一个非常优秀的设计是内置了HTTP import,只要一个TypeScript源码文件被HTTP服务器托管,在编写Deno脚本时,就可以import该文件。依赖于这个特性,注册中心就可以像使用Deno标准库一样提供一个内置的在线函数。第4章 函数服务的实现
函数即服务(Function-as-a-Service, FaaS)是一种以函数为单位的云计算服务,允许用户仅上传所需的函数代码,并在需要时自动执行该代码。
Deno标准库提供了一个简单易用的日志库,本项目使用一个轮转日志的设置,由于日志需要文件写权限,在创建日志的文件中加上权限断言。Deno标准库中的Streams是用于处理流数据的标准库,提供了一种方便和高效的方式来处理大量的数据。流式写文件指的是将数据逐个或逐块地写入文件,而不是将整个数据一次性写入文件。这种方法可以减少内存的使用,降低系统开销,并且可以提高处理大型文件的效率。通过Deno.stat()来确认文件是否存在,如果不存在会报NotFound的错误,返回版本号为0;如果为其他错误,则可能发生了其他问题,把错误抛出即可。
使用Deno.writeTextFile函数写入文件元数据。Deno.stat函数获取文件的大小信息;流式读文件是按照一定的数据流顺序,逐个读取文件中的数据并进行处理的一种方式。extname函数是Deno标准库中的一个函数,用于获取一个文件名的扩展名。
基于HTTP的计时器:为了增加计时器程序的健壮性,在计时器程序收到请求时,会把请求落地为JSON文件。假设计时器因为某种原因崩溃,重新启动时,会读取这些落地的JSON文件,如果读取到的任务已经超时,即活动结束时间小于当前时间,则立即执行这个任务。为提升响应速度,在Deno的localStorage中存储落地JSON文件的索引。计时器在任务触发时,需要登录获取token。计时器的任务协议使用zod编写,zod可以生成声明协议的JSON Schema,计时器可以使用JSON Schema对传入的协议进行校验。启动计时器时,会读取所有的元数据,如果触发时间已经到了,则立刻触发结束请求并删除元数据文件。使用Deno内置的expandGlob遍历文件夹,获取所有JSON文件,对JSON的内容做一些解析,然后初始化计时器。
实现邮箱服务SMTP的全称是“Simple Mail Transfer Protocol”,即简单邮件传输协议,是一组用于从源地址到目的地址传输邮件的规范。SMTP服务器就是遵循SMTP协议的发送邮件服务器。邮箱开启POP3/SMTP/IMAP。邮箱会为每个应用分配一个授权码,需要妥善保管。采用提供不同邮件模板的方式,通过HTTP接口选择不同的模板,传入对应模板的内容,生成并发送邮件。邮件完成渲染以后,使用邮箱客户端发送即可,这里使用Deno原生的denomailer。第5章 使用Prisma构建数据模型
- 使用Docker部署MySQL
在Windows和macOS平台可以直接下载安装Docker Desktop。Docker Desktop将会在本地创建一个Linux虚拟机,简化安装过程。Docker安装成功后,可以在 https://hub.docker.com/ 下载所需要的Docker镜像。Docker Hub是由Docker公司运营的镜像仓库。
注意:参考文章Docker Hub 国内镜像源配置
Docker镜像来源:
• DOCKER OFFICIAL IMAGE:由Docker官方维护的镜像资源。
• VERIFIED PUBLISHER:由Docker认证的高质量镜像资源,通常由商业公司维护。
• SPONSORED OSS:由Docker官方赞助的开源项目镜像。
使用Docker时,需要注意镜像的来源,因为任何人都可以上传镜像到Docker Hub。12345678使用docker run命令,运行MySQL服务。• -name参数:是容器的名字,如果不指定,Docker会自动生成一个名字。• -d参数:容器作为服务在后台运行。如果不设置-d参数,容器将以默认前台模式运行。使用docker logs命令查看容器的日志输出。使用docker ps命令可以查看当前运行的容器状态,-a参数可以查看所有状态的容器。使用docker stop命令可以指定某一容器停止。使用docker start命令可以启动某一容器。使用docker rm-f命令可以强制删除某一容器。
容器中的时区默认使用世界标准时间(UTC),MySQL会使用系统中的时区信息。为了后续使用方便,需要将Docker容器中的时区改为东八区。进入容器,修改时区配置信息,退出重启容器,执行date会发现容器时间修改成功。
MySQL的默认监听端口是3306,如果同时启动多个MySQL容器,可以通过-p参数指定容器端口到主机端口的映射。为了方便操作数据库,安装SQLPad镜像来可视化管理数据库,具体操作可以参考线上资料。在实际应用中,可根据需求修改MySQL数据库的参数,具体操作可参考 https://hub.docker.com/_/mysql/ ,本书不做详细介绍。
默认情况下,容器内创建的所有文件都存储在可写的容器层上。这意味着如果容器重启,存储在容器中的所有数据都将消失。存储需要满足三个基本条件:一是与容器的生命周期无关;二是所有节点都可访问,满足高可用性;三是即使应用崩溃,存储数据不丢失。
根据官方文档,Docker挂载存储的方式有三种。
1)Volume:将数据存储在单独的容器或目录中,并将其作为卷挂载到需要持久化数据的容器中,优点是Docker的核心特性,并且非常简单。
2)Bind mount:这种方法通过把宿主机目录映射到容器中来持久化数据,优点是可以通过宿主机直接访问容器中的数据,并且可以方便地在多个容器间共享数据。
3)tmpfs:这种方法通过在宿主机上创建临时文件系统来持久化数据,优点是可以在容器停止或重启时删除数据,并且不会对宿主机磁盘造成影响。
使用docker volume create创建mysql-db存储卷。删除容器,使用docker volume ls查看卷mysql-db仍存在。
- 使用Prisma作ORM
使用ORM有如下好处:
• 数据库模型与业务模型的统一,可以专注于真正的业务概念而不是数据库结构和SQL语义。
• 减少了代码量,不必为常见的CRUD(创建、读取、更新、删除)操作编写重复的SQL语句。
• 降低了对SQL的依赖。使用熟悉的编程语言就可以操作数据库,如Java、JavaScript、TypeScript。这对那些不熟悉SQL但仍想使用数据库的开发者来说是十分友好的。
• 抽象了数据库的具体细节。在理论上,使用ORM使得数据库迁移变得更为容易,如从MySQL迁移到PostgreSQL。
• 支持很多高级特性如事务、连接池、迁移等。
事务最核心的特性就是ACID:
• 原子性(Atomicity):确保事务的所有操作都成功或都失败。事务要么成功提交,要么中止并回滚。
• 一致性(Consistency):确保数据库在事务之前和之后的状态是有效的,数据库的完整性没有被破坏。
• 隔离性(Isolation):确保多个并发事务产生的效果与他们并行执行的效果相同。
• 持久性(Durability):确保在事务成功之后,所有修改都是永久的。
乐观锁(Optimistic Concurrency Control, OCC)又称为乐观并发控制,是一种关系型数据库中的并发控制模型,假设多用户并发的事务在处理时不会相互影响
乐观锁意味着读取一条记录,记录下版本号,并在记录被写回之前检查版本是否改变。在写回记录时,对版本的更新进行过滤,以确保它是原子的。
执行pnpm exec prisma init命令。这个命令完成了三件事:一是生成一个名为prisma的新文件夹,里面包含一个名为schema.prisma的文件。二是在prisma-demo目录创建.env文件,该文件用于定义环境变量,如数据库连接。三是在prisma-demo目录下生成.gitignore文件,用于指定哪些文件不应被包含在版本控制中。
在VS Code扩展菜单栏搜索Prisma,选择发布者是Prisma的扩展,这个插件为.prisma文件添加语法高亮显示、格式化、自动补全、跳转到定义和语法检查等功能。
执行命令pnpm exec prisma migrate dev-name init,该命令会在prisma/migrations目录下生成migration.sql,此文件包含数据库中执行的DDL语句,如果需要修改prisma schema,如增加表中字段或是修改关系,需要再次执行prisma migrate dev命令。此操作会在该目录下生成新的SQL文件,确保数据库模型和Prisma模型同步。
在数据库中,关系是将一个表中的值连接到另一个表中。数据库可以将这样的链接存储为键(主键或外键)。在Prisma模式中,外键/主键由@relation属性表示,总体可分为一对一、一对多、多对多三种关系。
第6章 以Fastify为核心的服务器设计
在Fastify中一切皆插件,路由、工具在Fastify中都可以是一个插件。使用插件可以帮助开发人员快速构建模块化、可定制、可扩展和易于维护的应用程序。
Fastify官方制作的插件帮助项目fastify-plugin,可以帮助检查Fastify的版本,给插件添加skip-override隐藏属性,传递自定义元数据给Fastify实例。为了让展示的日志更美观,还需要安装pino-pretty包。
注意通常只有在开发环境中才会开启日志美观的功能,在生产时开启会影响性能。默认情况下,日志记录是禁用的,可以在创建Fastify实例时通过传递{logger:true}或{logger:{level:ˈinfoˈ}}来启用。在注册完插件之后,使用server.log.info打印启动日志。使用req.log.info方法来记录路由被访问信息。
Fastify对JSON Schema做了开箱即用的支持,内置使用AJV验证请求与响应的JSON数据,开发者只需要声明相应的JSON Schema,填入POST/GET等函数中即可使用。TypeBox比较简单,适合入门。还有一个熟知的JSON Schema类型工具叫zod。使用插件fastify-zod可以简化在Fastify里使用zod的过程。
Vitest是一个轻量级的JavaScript单元测试框架,可以用于测试Fastify框架接口。
由于代码使用了process,而process是Node.js名字空间的全局变量,需要安装@types/node类型。
第7章 实现用户管理服务
@fastify/jwt
是Fastify官方维护的JWT工具插件。使用这个插件可以构建一个非常基本的身份验证功能。安装http-errors
包来返回错误码。
env-schema是一个环境变量验证库,使用Dotenv将环境变量从.env文件加载到process.env中,对环境变量进行运行时检查。
在VS Code扩展菜单栏搜索REST Client,本书将使用此插件在VS Code内部发送HTTP请求并查看响应。
第9章 实现报名登记应用的前端
Vite是一款轻量级的前端构建工具,它采用原生ESM模块系统,避免了复杂的构建流程,从而在开发过程中可以获得快速反应时间和低性能开销。Vite支持ESM、JSX和TypeScript,并且通过充分利用浏览器的ESM支持,可以在生产环境中提供高效的代码分离和懒加载。Vite使用了esbuild和Rollup双引擎,在打包时使用更成熟的Rollup,在开发时使用性能更快的esbuild。严格来说,这造成了一定的开发生产不一致的问题,但是由于esbuild确实带来了极好的开发体验,总体的收益还是大于其不足。
Tailwind是一个以工具类为中心的CSS框架,主要包括工具类和围绕这些工具类构建的工具。文件postcss.config.cjs,用于导入Tailwind插件。Autoprefixer是一个用于自动添加CSS兼容性前缀的工具。
Tailwind配置文件tailwind.config.cjs,用于定制Tailwind的样式和行为,使其适合特定的项目需求。base是Tailwind的CSS重置集。使用重制CSS集是为了相对好地抹去一部分浏览器差异。这部分CSS在目录/nodemodules/tailwindcss/lib/css/preflight.css(3.1.8版本)中。components是组件相关的CSS集,utilities是工具类相关的CSS集。Tailwind提供了w-{size}和h-{size},即宽度和高度工具类,方便快速设置元素的尺寸,而无须编写自定义CSS。
Tailwind官方维护了heroicons项目。该项目有超过450个MIT协议的SVG图标,可以满足常见的用途。
Tailwind标准化了CSS的使用,但是纯粹的CSS并不能完成常用的UI需求,Tailwind官方还维护了一套非常优秀的跨React、Vue的无样式组件库Headless UI。
Pinia是Vue官方的状态管理库,由Eduardo San Martin Morote编写,同时他也是Vue Router的作者。作为Vuex继承者,Pinia的主要目的是帮助开发者构建跨应用程序的组件管理响应性数据和状态。对于存入Pinia的一些关键变量,需要使用vueuse项目提供的localStorage能力进行存储,安装@vueuse/core
依赖。
VITE为前缀的变量是Vite的特殊环境变量,在编译以后,VITE_的环境变量会暴露给客户端的代码。可以通过修改Vite的配置文件中envPrefix来修改这个前缀的命名。
安装@fastify/cors
插件。Fastify CORS是一个Fastify插件,用于处理跨域资源共享(Cross-Origin Resource Sharing, CORS)请求。它允许开发者定义CORS选项,如允许的来源、方法、标头和暴露的标头。它还可以在请求头中添加额外的信息,如Access-Control-Allow-Credentials。使用该插件可以更轻松地管理API的跨域请求,并且比手动设置CORS选项更简单。
VeeValidate是一个用于验证表单有效的库,提供了两种表单验证方式,即组件和组合式接口。声明表单填写的schema,使用yup提供的校验功能对需要填写的字段进行业务的限制。toLocaleDateString和toLocaleTimeString可以根据浏览器的区域设置来格式化日期和时间。
随着Vite的稳定,只有热更新、Vite-node相关的功能主要和Vite有关,很多其他的问题可能需要到上游Rollup和esbuild寻找答案。所以学习、使用Vite和tsup,仍然要学习esbuild和Rollup。
第11章 项目的部署、监控与高可用
- 使用DockerFile实现后端服务容器化
Docker可以通过从Dockerfile中读取指令来自动构建映像。Dockerfile是一个文本文档,官方文档地址为 https://docs.docker.com/reference/dockerfile/, 它包含了一系列指令和参数,用于指导Docker在构建镜像时如何运行命令和配置环境。用户可以在命令行上调用的所有命令来组装镜像。
• HEALTHCHECK:用于检查应用程序的运行情况,以确定该资源是否在正常运行。当应用程序可以运行,但由于陷入无限循环而无法处理新请求时,检查运行状况非常有用。
每一个Dockerfile中只能有一条HEALTHCHECK指令。如果列出多个HEALTHCHECK,则只有最后一条HEALTHCHECK生效。
• CMD:指定镜像启动时的命令,在镜像构建过程结束后执行。每个Dockerfile只有一条CMD命令,如果指定了多条,只有最后一条会被执行。
将Dockerfile文件放到Monorepo项目的根目录,新建.dockerignore文件,用于告诉Docker在构建过程中忽略哪些文件。注意,需要安装is-docker包,判断程序是否在Docker环境中运行。
在根目录执行docker build命令,可以根据Dockerfile生成所需的映像。“-t”参数用于指定生成的镜像名称。“.”表明是当前目录的Dockerfile。
使用docker images命令查看生成的镜像信息。
使用docker ps命令可以看到健康检查的结果,刚开始启动时状态是health: starting。等待几秒钟后,再次执行docker ps,就会看到健康状态变为了healthy。 - Docker Compose工具
Docker提供了Compose工具,借助docker-compose.yaml文件,开发者可以定义一组相关服务和相关的环境要求。然后,使用docker compose up命令创建并启动配置中的所有服务,并将其部署为整个应用程序。docker-compose down命令用于关闭和删除运行的容器、网络和卷。 - 服务监控原理与部署
作为一种强大的监控工具,Prometheus可以帮助用户实时监测系统的各项指标,如CPU、内存、网络等资源使用情况,同时还可以提供强大的警报机制,帮助用户快速发现并解决系统问题。Grafana是一款强大的开源可视化和分析软件,可以帮助用户通过仪表盘展示各种数据。Grafana不存储任何数据,而是依赖于多种数据源,如Prometheus、Graphite、InfluxDB、ElasticSearch等。此外,Grafana还提供了基于各种阈值发送通知和邮件警报的功能,帮助用户及时发现并解决问题。
使用docker pull
命令下载MySQL Server Exporter Docker、Prometheus、Grafana
镜像,使用docker image
命令确认镜像下载成功。
在MySQL容器中创建exporter用户,用于监控并赋权,可以通过SQLPad连接数据库执行SQL,也可以使用docker exec-it命令连接Docker容器。建议为用户exporter设置最大连接限制,以避免在重负载下使用监视刮片造成服务器超载。另外mysql-exporter并不支持所有的MySQL版本,使用前请查看https://github.com/prometheus/mysqld_exporter。 - 高可用的部署方式
MySQL数据库可以使用binlog实现主从数据库的同步。应用层可通过部署多个实例实现高可用,有两种方式:一种是传统的部署方式,基于F5或Nginx实现负载均衡和高可用性;另一种是容器化的部署方式,使用Kubernetes的功能来实现。防火墙、交换机故障可以使用堆叠、VRRP等技术实现多台设备的主备。
Nginx是当前流行的轻量级、高性能、开源的HTTP和反向代理服务器。正向代理类似于跳板机,主要用途就是帮助用户访问原来无法访问的资源,如VPN,对外隐藏用户信息。
反向代理的作用有:保证内网的安全,阻止Web攻击;负载均衡和高可用,通过反向代理服务器来优化网站的负载。
Nginx作为反向代理,将内部应用映射为一个地址对外提供服务,在接收到外部请求时,又通过轮询等不同的算法规则转发到内部的应用服务器上。
通过负载均衡,实现服务的水平扩缩容,使应用具备高可用能力。当Nginx收到一个HTTP请求后,会根据负载策略将请求转发到不同的后端服务器上。前端Serverless 面向全栈的无服务器架构实战
通过对 Serverless 技术原理的学习,从而不再将自己局限于前端或特定领域,而是站在更高的角度看待软件研发。什么是Serverless
Serverless,中文译为“无服务器的”;但这里所说的并不是真实意义上没有服务器的架构,而是针对开发者来说,无须关心服务器。在此,“Serverless”指的是,包括服务器的资源情况、部署情况、操作系统以及依赖软件等在内的所有细节,开发者均无须关注,这一切由平台完成,开发者只需要专注于业务实现。
广义上来说,我们认为 Serverless 是一种理念,即指对开发者来说,那些无须关心计算资源就可以直接使用的服务,都可以算作一种 Serverless 服务。而从狭义上来说,它指提供给开发者的标准化能力,即 FaaS 和 BaaS。另外,Serverless 提出了按用量付费的理念,即当对应的任务被触发执行时,才会计费。Serverless的核心特性是按用量付费(Pay As You Go)和弹性计算(Elastic Compute)。作为 Serverless 平台,至少需要提供 FaaS 和 BaaS 这两种能力中的一种。
FaaS(Functions as a Service),译为“函数即服务”。它基于事件驱动的理念,提供了让开发者以函数为基本粒度的代码,且具有像HTTP或其他事件一样被触发并被执行的能力。开发者只需编写业务代码,无须关注服务器资源。
BaaS(Backend as a Service),译为“后端即服务”,指的是一些通过 API 的方式提供的第三方服务,这些第三方服务通常是我们使用的各个中间件服务,比如数据库、缓存、消息队列等,并且这些第三方服务的可用性由它们的提供者自行管理,使用它们的开发者无须关心这些服务背后的部署情况。何时应用Serverless
FaaS 具有以下优点(优势):◎ 更高的研发效率。开发者无须关注函数的可用性问题,可以更聚焦于函数本身。◎ 更低的部署成本。无须登录到服务器,通过控制台或命令行工具,即可完成服务的部署。◎ 更低的运维成本。开发者无须担心容量问题,函数将根据负载情况来实现自动扩容/缩容。◎ 更低的学习成本。操作系统、容器、运行环境等,对开发者都是不可见的。◎ 更低的服务器费用。采用了按调用量付费的方式,可以大大缩减开发者的服务器成本。◎ 更灵活的部署方案。每个函数的发布是根据版本进行的,可以更容易地实现灰度发布的能力。◎ 更高的系统安全性。统一托管运行环境,开发者自身无须接触服务器信息。
目前的缺点(劣势):◎ 平台学习成本高。由于其是一种全新的架构,因此缺乏文档、示例、工具以及最佳实践。◎ 调试成本较高。由于运行环境(Runtime)由云计算供应商提供,因此本地调试和日志查询比较困难。◎ 冷启动时性能可能下降。由于部分语言自身的特性和限制,冷启动时间会较长。◎ 供应商锁定。缺乏标准化实现以及成熟的开源生态,不同云计算供应商的实现不一致,这导致其迁移困难。Serverless与服务端技术
所有的业务代码都在同一个系统中组织,我们一般将这样的应用称为单体应用程序(Monolith Application,简称单体应用)。
分层设计中最经典的是3层架构(3-tier architecture),它将单体应用程序划分为3层,即表现层(User Interface Layer,又称用户接口层)、业务逻辑层(Business Logic Layer)和数据访问层(Data Access Layer)。
微服务(Microservices)是一种软件架构风格。它从分布式架构发展演变而来。它通过将一个大型单体应用程序切分为多个独立小型服务的方式来实现更轻量、可控的软件研发管理。服务间通过 RPC(Remote Procedure Call,远程过程调用)来实现相互通信。
传统分层架构对于大型应用来说,很难保持良好的可维护性和可扩展性。
康威定律(Conway’s Law):设计系统的架构受制于产生这些设计的组织的沟通结构。——Melvin Conway(1967年),同时这也是微服务架构的理论基础。
通过微服务,我们可以让每个应用只关注自己的业务,屏蔽不必要的信息,以此降低沟通成本。但微服务架构与其他新技术一样,它在解决一个问题的时候,也带来了一些新的问题。这主要包括服务治理(主要指服务的可用性,包括集群容错、服务伸缩、服务限流、服务降级等)和服务通信成本问题。
云计算,是通过将计算资源“虚拟化”,让用户无须了解计算资源背后的细节,也不必具备相应的专业知识,就能直接使用。
目前云计算按服务模式分类,通常被划分为以下三大类型。
◎ 基础设施即服务(IaaS,Infrastructure as a Service)。
◎ 平台即服务(PaaS,Platform as a Service)。这些服务有时候也被称为中间件。
◎ 软件即服务(SaaS,Software as a Service)。
与虚拟机相比,容器化有轻量化、易于配置、快速扩展、易于迁移等优点。从原理上来说,容器化技术也是一种虚拟化技术。
NoOps 指的是无须运维。NoOps 从 DevOps 发展而来。二者都是一种开发模式,旨在通过技术手段,尽量降低甚至消除运维成本。Serverless与前端技术
BFF(Backend For Frontend),即服务于前端的后端。BFF 主要通过在前端和后端之间增加一个“胶水层”(“这个”胶水层“由对应的客户端负责编写),实现对客户端所需要的 API 的聚合和裁剪,从而解决前后端分离之后所带来的协作问题。聚合指的是根据客户端当前页面所需要的数据,合并不同的微服务,最终提供一个统一的接口,以避免客户端多次发送请求来获取数据;而裁剪指的是基于聚合后的接口,将接口中的字段转换为客户端需要的格式,并移除那些客户端中不需要的字段。这样可以避免在客户端中的格式转换,减少不必要的数据传输。
BFF 具备以下这些优势:
◎ 接口可灵活装配,客户端需要的数据可调用对应的微服务获取,格式也可在服务端完成转换。
◎ 可降低沟通成本。
◎ 有利于客户端的性能优化。
◎ 有利于提升客户端的安全性,减少数据的暴露。
GraphQL 是一种查询语言。实际上它与 SQL 一样,是一种 DSL(Domain-Specific Language,领域特定语言),即专门用于某一特定领域(这里指数据查询)的计算机语言。GraphQL 查询不仅能够获得资源的属性,而且还能沿着当前资源进一步检索关联资源。
前端提出了 NoBackend 理念,而后端则通过 BaaS 实现这一诉求。NoBackend已经随着 Serverless 的普及而退出了历史的舞台,但我们应该知道,它是 Serverless 理念的“先驱”和“践行者”。NoBackend 希望以前端驱动的设计过程来实现一个产品的构建。这个概念其实和“大中台,小前台”的理念十分相似,通过将大量的服务端业务抽象为通用能力,不同产品线,统一由中台提供基础能力,从而实现在前台产品构建的过程中只需要关注产品自身的功能,而不受后端技术的制约。FaaS技术
事件驱动(Event-driven)是一种程序的设计模型。这种模型的程序运行流程需要由用户的动作(比如鼠标的点击或键盘的输入)或者由其他程序的消息来触发。
FaaS 函数同样也是通过事件进行驱动的,我们需要通过定义一种事件源(Event Source)来调用对应的函数。我们将这种事件源的定义称为触发器(Trigger)。FaaS 内部的控制器(FaaS Controller)将监听这一事件。目前使用最广泛的触发器是 HTTP 触发器,它通过域名绑定、路径配置等功能,提供了基于 HTTP 协议来触发函数的能力;这就是 Web API。无状态,是针对所提供的计算服务来讲的,它指的是一种把每个请求都作为与之前任何请求无关的独立请求进行处理的服务。在 FaaS 中,函数的无状态与 HTTP 提供的 Web API 十分相似,函数自身也不会保存上下文信息。如果在函数中需要鉴定用户的身份,则需要通过调用其他第三方服务来实现。
- FaaS的优点: 1 更高的研发效率;2 更低的部署成本;3 更低的运维成本;4 更低的学习成本;5 更低的服务器费用;6 更灵活的部署方案;7 更高的系统安全性。
- FaaS的缺点:1 存在平台学习成本;2 较高的调试成本;3 潜在的性能问题;4 供应商锁定问题。
对于这种无法轻易从一个供应商迁移到另一个供应商的场景,我们将它称为“供应商锁定”问题(Vendor lock-in)。 - 函数的生命周期
通过将函数的别名指向不同的函数版本,我们可以实现函数的快速发布。通过函数别名的权重配置功能,我们可以轻松地实现灰度发布能力。除此之外,在发布一些重要功能时,我们还会采用 A/B 测试(A/B Testing)的方式,以达到更好的效果。
A/B 测试是一种随机测试,即为同一个目标制定两个方案,让一部分用户使用 A 方案,另一部分用户使用 B 方案,同时记录这些用户的使用情况,之后分析哪个方案取得了更好的效果,最终统一使用一套方案。 - 理解函数运行时
根据事件来源的不同,可以将触发器的类型大致分为4种类型:客户端触发器、消息触发器、存储触发器以及其他触发器。
“发布/订阅模式(publish-subscribe pattern)”,它通过发布者和订阅者之间的约定,实现松散的一对多依赖关系。当发布者发送一个事件时,所有订阅了该事件的接收者都会得到通知。
使用消息队列机制,将瞬时流量转换为排队的消息,再通过固定流量(如每 1000 条消息/秒)来消费。这种通过排队来应对瞬时高并发场景的措施,通常也被称为“削峰填谷”。消息队列还有可以实现系统解耦的效果。
通过消息发送的方式,将同步处理转为异步处理,就可以确保主要业务的正常运转。消息队列还能应用于保障消息按顺序接收、支持任务分发和流量分发等多种场景。
存储触发器指的是当存储动作(如插入、更新、删除等)发生时,将通过事件的方式通知该事件的订阅方,从而进行一些额外的处理。存储触发器通常由存储服务提供,而不同云计算供应商所提供的存储触发器可能所有不同,主要分为面向数据库和面向文件两种触发器。与传统触发器不同的是,面向函数计算的存储触发器通常不会与业务形成强耦合的关系,而是以异步调用的方式实现某些优化或增强能力。
云计算供应商还会根据自身的云服务,提供不同的触发器类型,如定时触发器,代码提交触发器,CDN 触发器。
实现不同函数隔离的关键技术被称为“沙箱”(Sandbox)。目前在 FaaS 服务中,应用得最为普遍的是基于 Docker 技术实现容器级别的隔离。BaaS技术
CaaS(Containers as a Service,容器即服务)。CaaS 配置的灵活性,使得应用程序可以在任何容器服务的环境中部署和运行,而无须为不同的环境重新配置。并且,与虚拟机相比,由于容器的实现更加轻量,因此这能够使得在它之上的服务可以更容易、更快速地实现伸缩。灵活和高效是 CaaS 的最大特点。
BaaS 就是将 PaaS 层的能力,直接针对终端(如移动 APP、Web 站点、瘦客户端等)进行了封装,研发人员能够直接在终端调用这些 PaaS 能力。
Firebase 于 2004 年被 Google 收购,它提供了丰富的 BaaS 服务,可以让移动应用或 Web 应用直接接入。并且针对 Android 和 iOS,Firebase 还提供了特定的功能,以简化移动应用的开发流程。同时,通过它与 Google 云的整合,开发者能够十分便捷地在前端使用各种 Google 的云计算服务。
从整体上来说,BaaS 具备以下优势:
◎ 提高开发效率
◎ 缩减人力成本
◎ 降低运维及服务器成本
◎ 降低运营成本
◎ 良好的用户体验数据的持久化
对于前端研发来说,我们通常会使用 Node.js 作为承载后端服务的运行环境。因此,在数据存储方面,MongoDB 因其基于 BSON 的数据存储格式而与 JavaScript 的 JSON 具有良好的兼容性,也自然成为数据持久化方案的不二选择。
MongoDB 实际上属于 NoSQL 数据库的一种。NoSQL,即 Not Only SQL,它是与 SQL 这种关系型数据库相对来说的。它之所以被称为 NoSQL,是因为无须遵循传统关系型数据库中的 ACID(Atomicity,Consistency,Isolation,Durability)原则。正因如此,它在分布式能力、可扩展性、读/写性能和灵活性等多个方面具有显著优势。
NoSQL 数据库从分类方面来看,可以分为4大类型,分别是键值存储数据库、列存储数据库、图形数据库、文档型数据库。
Redis、Memcached 均属于键值存储数据库。Cassandra、HBase 是列存储数据库的代表产品。图形数据库的代表是 Neo4J。
MongoDB 数据库就是文档型数据库的一种。在所有的 NoSQL 数据库类型中,文档型数据库与关系型数据库是最接近的一种。文档型数据库和关系型数据库一样,支持对数据进行动态查询和索引的能力,其整个数据的存储方式也与关系型数据库相似。在关系型数据库中,不同的表是通过外键的概念来关联的;而在文档型数据库中,则直接将对应的数据嵌套在需要关联的属性之下。文档型数据库与其他 NoSQL 数据库也有一个显著差异,那就是查询能力。文档型数据库可以对整个数据内容进行检索。
BSON,即Binary JSON(二进制 JSON),是 MongoDB 数据库中数据存储和传输的格式。BSON 是在 JSON 的基础上进行扩展得到的,因此它和 JSON 一样,支持内嵌对象和数组。
MongoDB 的一些常用数据类型。
◎ 字符串(String)
◎ 整型(Integer)
◎ 布尔型(Boolean)
◎ 双精度浮点型(Double)
◎ 数组(Arrays)
◎ 对象(Object)
◎ 日期(Date):该类型用来保存特定日期和时间,它以 UNIX 的时间格式进行存储。
◎ 二进制数据(Binary Data):该类型用来存储二进制数据。有了它之后我们就无须将JavaScript 中的二进制数据(如ArrayBuffer)转换为字符串进行保存了。 - 数据库设计的三大范式(也被称为数据库规范化)
是数据库设计的一系列指导性原则,目的是减少数据库中的数据冗余,保障数据的一致性。具体内容如下:
◎ 第一范式(1NF):
字段内的数据,不能够再进行拆分。其主要目的是为了避免数据库中出现重复的数据。
◎ 第二范式(2NF):
表中的每一个字段,都与本表有直接依赖,而不是间接相关,不可再拆分到另一张表中。在实践中,这指的是不能将多种类型的数据保存在同一张表中(比如,不能同时将商品信息和订单信息保存在同一张表中)。
◎ 第三范式(3NF):要求所有非主键属性都只和主键有相关性。也就是说,多个非主键属性之间应该是独立无关的。
在实际应用中,应该避免非主键的字段是依赖另一个非主键计算产生的。
NoSQL数据库的最大优势是它极其高效的查询能力。若只是简单地遵循范式设计,就可能会导致其性能受损。
引用方式也是关系型数据库保存数据的主要方式,它通过引用信息(一般是 ID)来实现多个集合直接的文档关联。虽然引用方式提供了灵活的查询模式,但是它也会带来经典的 SELECT 1+N 问题。
SELECT 1+N 问题指的是为了取得目标数据,需要先完成一次查询返回一个包含 N 条数据的列表,然后再通过 N 次查询将这些数据中的附带信息找出来。
在关系型数据库中,正确的做法是通过 JOIN 关联相关数据,实现一次查询可获取所需的数据。
引用方式可以提供十分灵活的查询方式,但往往需要应用程序使用多次查询来获取所需的数据。也就是说,它以牺牲查询性能的方式换取更大的灵活性。我们可以通过数据冗余的方式,提前准备数据,以实现更佳的查询性能。
内嵌方式指的是将含有层级结构的数据保存在一个单一集合中,其中的子集(数组或对象)挂载到父集合的某一个字段下。因为只需要维护一个对象,所以这种数据模型可以让数据的读/写操作更加便捷,一致性也更强。
一般来说,在项目初期我们会采用内嵌方式设计,以最小的成本快速实现相关功能并发布上线。当产品得到市场认可后,再根据具体的数据量情况逐步进行优化(以空间换时间)。实现成本、效率与性能之间的平衡。
索引,即数据库的索引,它是数据库系统中一个用来排序的数据结构,以协助数据库引擎能够快速查询、更新数据库表中的数据。索引在关系型数据库中使用得较为频繁。我们可以将最常排序的字段设置为索引字段,以提高其整体的检索速度。
索引按类型来分,可以分为聚集索引(Clustered Index)和非聚集索引(Nonclustered Index)。
由于真实的物理顺序只有一种,因此在一个数据库集合中,也就只允许存在一个聚集索引(默认是 id 字段)。非聚集索引是允许存在多个的。另外,由于聚集索引是按物理顺序排序的,因此聚集索引的查找效率比非聚集索引要高一些。非聚集索引由于最终指向的是不同的位置,因此它并不适用于那些需要查找某个范围内多条数据的场景。
用户身份识别与授权
认证(Authentication)与授权(Authorization)是两个经常被混淆的概念。认证指的是确认身份,证明“我是我”,它主要解决“是不是”的问题;而授权表示是否允许该应用获取“我的数据”,解决的是“能不能”的问题,两者的目的是不同的。
OAuth 在第三方应用程序和资源服务器之间增加了一个授权层的概念,要求第三方应用不能直接访问用户的数据资源,而是需要先通过授权层,让用户完成授权并获得相应的令牌(Token)后,再通过令牌在一定时间内访问用户的特定数据。
在 OAuth 的具体实现中,它定义了四种授权模式,应用于不同的场景。它们分别是授权码(Authorization Code)模式、简化(Implicit)模式、密码(Resource Owner Password Credentials)模式和客户端凭证(Client Credentials)模式。
授权码模式:这是 OAuth 的标准模式,是默认采用的模式,也是在安全性、功能性方面考虑得最全面的一种模式。它需要第三方应用的服务端参与,实现第三方应用服务端与服务提供方认证服务器的交互来获取令牌。我们通指的 OAuth 第三方授权,说的就是这种模式。
通过其定义来看,OAuth 是为授权而设计的。若要获取用户授权,则需要先明确用户是谁。因此,OAuth 实际上也包含了用户身份认证的能力。
由于 OAuth 的核心目标是解决用户授权,而不是专门为用户认证所设计的;因此,OAuth提供的认证服务有一定的缺陷。其中最主要的问题是,第三方应用与认证服务器之间由于通过令牌(Token)进行授权,而每次登录令牌的值都是不同的,因此我们无法通过令牌确定某一个用户。这导致了第三方应用如果需要确定用户身份,那么得让对应的认证服务器提供特殊的定制接口,返回用户标识之类的信息才行。
身份认证即服务(IDaaS,Identity as a Service),是指一种提供身份认证以及账户授权,且基于 PaaS 的云服务。
在软件领域,令牌(Token)是指有权力执行某些操作或访问某些数据的凭据。实际上,在 OAuth 授权时,授权服务器将返回两种不同的令牌给第三方应用服务器,分别是访问令牌(Access Token)和刷新令牌(Refresh Token)。访问令牌,就是用于直接请求数据的凭据。访问令牌有较强的时效性,其有效期通常较短。刷新令牌,则是用来获取访问令牌的令牌。刷新令牌的有效期通常很长,除非出现因数据泄露等极端情况而导致令牌重置等,否则可以认为它是长期可用的。通常,访问令牌允许直接保存在第三方应用的客户端中,以便能够直接向数据服务器发起请求;而刷新令牌则需要保存在第三方应用的服务端中,以确保其安全性。
JWT 的核心工作原理,即通过数字签名,防止数据在互联网的传输中被篡改,以实现内容的安全传输。
JWT 实例实际上是一个字符串,从结构上来说,它由三部分组成,分别是头信息(Header)、消息内容(Payload)和签名(Signature),它们之间使用小数点符号“.”连接
第二部分的消息内容(Payload)为消息的主体部分。与 Header 相同,这里也使用 JSON格式,并采用 Base64 URL 编码。
在JWT的这些键值对中,对Key 所能使用的值做了一个简单的归类,JWT 将这些键值对的 Key 称为声明(Claim)。声明的类型主要分为3种,分别是注册的声明(Registered Claim)、公开的声明(Public Claim)和私有的声明(Private Claim)。注册的声明指的是在 JWT 标准(RFC 7519)中声明的 Key,它一共包含 7 个 Key。在实际应用中,若验证工作是由自身服务以外的第三方服务完成的,则为了确保密钥的安全性,我们应该使用RS256 (HMAC-SHA256)之类的非对称算法。该算法分别提供了不同的私钥和公钥,以生成签名和验证。生成签名的私钥应由服务的提供方妥善保管,而对外仅提供用于验证的私钥。
优势:
◎ 防篡改能力
◎ 安全性高
◎ 格式通用
要使用 JWT,它应该具有两大特点:
(1)跨系统的数据交换:出于安全性的考虑,JWT 应该使用于服务端多个应用之间的数据交换场景,而不是服务器与客户端之间的数据交换场景。
(2)数据的有效期较短