This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="05 | 后端BaaS化NoOps的微服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d6/11/d63a54ce8d097aca2b5ff55a41dc1d11.mp3"></audio>
你好我是秦粤。现在我们知道了在网络拓扑图中只有Stateless节点才能自由扩缩容而Stateful节点因为保存了重要数据我们要谨慎对待因此很难做扩缩容。
FaaS连接并访问传统数据库会增加额外的开销我们可以采用数据编排的思想将数据库操作转为RESTful API。顺着这个思路我引出了后端应用的BaaS化一句话总结后端应用BaaS化就是将后端应用转换成NoOps的数据接口。那怎么理解这句话呢后端应用BaaS化究竟应该怎么做接下来的几节课我们会展开来讲。
我们先回忆一下上节课的“待办任务”Web应用这个项目前端是单页应用中间用了FaaS做SFF数据网关后端数据接口还要BaaS化。这个案例会贯穿我们的课程所以你一定要动手run一下。为了让你对我们的项目有个宏观上的认识我还是先交付你一张大图。
<img src="https://static001.geekbang.org/resource/image/ba/c9/bab7e22b588d69cbe0197d36696411c9.jpg" alt="" title="“待办任务”Web应用架构图">
这个架构的优势是什么呢?我们将这个图变个形,你就更容易理解了。
<img src="https://static001.geekbang.org/resource/image/66/8f/66aeb01686c94478b2847be5bb2a398f.jpg" alt="" title="架构变形示意图">
咱从左往右看上面这张图。用户从浏览器打开我们网站时前端应用响应返回index.html然后浏览器去CDN下载我们的静态资源完成页面静态资源的加载与此同时浏览器也向前端应用发起数据请求前端应用经过安全签名后再将数据请求发送给SFFSFF再根据数据请求调用后端接口或服务最终将结果编排后返回。
从图里你可以看到除了数据库是Stateful的其它节点都已经变成了Stateless。如果你公司业务量不大的话这个架构其实已经足够了。就像传统的MVC架构一样单点数据库承载基本的并发量不是问题而且数据也可以通过主从结构和客户端读写分离的方式来优化。
但MVC架构最大的问题就是累积当一个MVC架构的应用在经历长期迭代和运营后数据库一定会变得臃肿极大降低数据库的读写性能。而且在高并发达到一定量级Stateful的数据库还是会成为瓶颈。那我们可以将自己的数据库也变成BaaS吗
要解决数据库的问题也可以选择我上节课和你说的云服务商提供的BaaS服务比如DynamoDB。但云服务商BaaS服务究竟是怎么做到的如果BaaS服务能力不全不够满足我们的需要时怎么办今天我就先带你看看传统的MVC应用中的数据库怎么改造成BaaS。
当然BaaS化的过程有些复杂这也正是我们后面需要用几节课才能跟你解释清楚的核心知识点。正如我们本节课的标题后端应用BaaS化就是NoOps的微服务。在我看来后端应用BaaS化跟微服务高度重合微服务几乎涵盖了我们BaaS化要做的所有内容。
所以我们先来学习一下微服务是什么。
## 微服务的概念
微服务的概念对很多做后端同学来说并不陌生尤其是做Java的同学因为早些年Java就提出SOA面向服务架构。微服务算是SOA的一个子集2014年由ThoughtWorks的Martin Fowler提出。微服务设计之初是为了拆解巨石应用巨石应用就是指那些生命周期较长的累计了大量业务高度耦合和冗余代码的企业应用。
跟Serverless的概念还在发展中不同微服务的概念在这么多年的发展中已经有了明确的定义了。下面是AWS官方的解释
>
微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成,这些服务由各个小型独立团队负责。微服务架构使应用程序更易于扩展和更快地开发,从而加速创新并缩短新功能的上市时间。
那在我看来微服务就是先拆后合它将一个复杂的大型应用拆解成职责单一的小功能模块各模块之间的数据模型相互独立模块采用API接口暴露自己对外提供服务然后再通过这些接口组合出原先的大型应用。拆解的好处是小模块便于维护可以快速迭代跨应用复用。
我们的Serverless专栏并不打算给你详细地讲解微服务但是希望你能一定程度上了解微服务。FaaS和微服务架构的诞生几乎是在同一时期它俩的很多理念都是来自12要素Twelve-Factor App[1]所以微服务概念和FaaS概念高度相似也有不少公司用FaaS实现微服务架构不同的是微服务的领军公司ThoughtWorks和NetFlix到处宣扬他们的微服务架构带来的好处而且他们提出了一整套方法论包括微服务架构如何设计如何拆解微服务尤其是数据库如何设计等等。
我们后端应用BaaS化首先要将复杂的业务逻辑拆开拆成职责单一的微服务。**因为职责单一,所以服务端运维的成本会更低。**而且拆分就像治理洪水时的分流一样,它能减轻每个微服务上承受的压力。
我在Serverless的专栏里向你介绍微服务主要就是想引入微服务拆解业务的分流思想和微服务对数据库的解耦方式。
## 微服务10要素
谈起微服务很多人都要说12要素[2]认为微服务也应该满足12要素。12要素当初是为了SaaS设计的用来提升Web应用的适用性和可移植性。我在做微服务初期也学习过12要素但我发现这个只能提供理论认识。
所以在2018年GIAC的大会上我将我们团队在微服务架构落地中的经验总结为**微服务的10要素API、服务调用、服务发现日志、链路追踪容灾性、监控、扩缩容发布管道鉴权。**
我们回想一下[[第1课]](https://time.geekbang.org/column/article/224559) 中小服的工作职责:
1. 无需用户关心服务端的事情(容错、容灾、安全验证、自动扩缩容、日志调试等等)。
1. 按使用量(调用次数、时长等)付费,低费用和高性能并行,大多数场景下节省开支。
1. 快速迭代&amp;试错能力多版本控制灰度CI&amp;CD等等
你有没有发现跟微服务的要素有大量的重合。API就是RESTful的HTTP数据接口服务调用你可以理解为就是HTTP请求服务发现你可以理解为我们只能用域名调用我们的HTTP请求不能用IP日志、容灾、监控都不难理解链路追踪是微服务重要的一环因为相对传统MVC架构我们一个请求在后端的调用链增长了为了快速定位问题我们都需要打印整个调用链路的异常栈发布管道和鉴权和我们的FaaS也有很重要的关联我将放在下一课讲解。
<img src="https://static001.geekbang.org/resource/image/45/f1/4558c4088c38623cf366e7d686654ff1.png" alt="" title="木桶效应示意图">
接下来我不准备按照微服务的10要素一一操作但我希望可以通过Serverless帮你建立知识体系的索引所以关联的技术栈我都尽量点出来你可以自己进一步了解学习。
我们再次拿出我们的创业项目“待办任务”Web网站这次我将带你一起看看微服务如何让数据库解耦。
我们先分析一下“待办任务”Web服务。上一讲末尾我的作业其实已经为你准备好了我们一起看看index.js文件这是一个典型的MVC架构的应用。View层就是index.html和静态资源文件Model层我们引入lowdb用一个db.json文件代替数据库Control层就是app.METHOD处理/api/*的数据逻辑。微服务是解决后端应用的所以我们只需要关注Control层。
<img src="https://static001.geekbang.org/resource/image/25/a4/25981be7fb564da00a736bfe93f101a4.jpg" alt="" title="index.js文件">
API的数据处理主要有两个一个是/api/rule处理待办任务的一个是/api/user负责用户登录态而且我们“待办任务”主要的数据模型也就是待办任务和用户这两个。那我们可以将后端拆分出两个微服务待办任务微服务和用户微服务。这里我要强调下我只是为了向你演示微服务才这样做在实际业务中这么简单的功能没必要过早地拆分。
<img src="https://static001.geekbang.org/resource/image/3a/ab/3abe89510aa6897b0b1f50e239c91dab.jpg" alt="" title="微服务拆解演示">
初步拆解我们将index.js中的数据库移走了而且拆分后各微服务的数据库比原来混杂在一起简单且容易维护多了。user.js负责用户相关业务逻辑只维护用户信息的数据库并且暴露RESTful的HTTP方法rule.js负责待办任务的增删改查只维护待办任务的数据库并且暴露RESTful的HTTP方法index.js只需要负责将请求HTTP返回给数据编排。
HTTP协议要满足我们的服务调用与发现发布的user.js和rule.js必须使用域名例如api.jike-serverless.online/user和api.jike-serverless.online/rule。这样新扩容的机器IP只需要注册到这个域名下就可以被服务发现IP并调用了。
我们现在拆分后只是将数据库从index.js移出分给到了user.js和rule.js但两个节点到目前为止数据库仍然是Stateful的。我们如何再让这两个数据库节点变成Stateless呢你可以按一下暂停键思考一下。
我们先想想对于rule.js这个微服务来说我要再扩容一个新的实例而且让两边的实例保持一致那么我们是否只需要让新的数据库和旧的数据库保持同步更新就可以了呢
## 解耦数据库
没错,其实要解决这个问题的核心就是让数据启动时,可以更新到最新的数据库。在其中一个数据库更新时,通知另外一个数据库更新。
这里就要引出一个关键的Stateful对象消息队列[3]。它是一个稳定的绝对值得信赖的Stateful节点而且对你的应用来说消息队列是全局唯一的。解耦数据库的思路就是通过消息队列解决数据库和副本之间的同步问题有了消息队列剩下的就简单了。
我们每个微服务中的数据库例如MySQL写的操作都会产生binary log通过额外的进程将binary log变更同步到消息队列并监听消息队列将binary log更新在本地执行修改MySQL。比较著名的解决方案就是领英开源的Kafka现在云服务商基本也都会提供Kafka服务。当然数据库和副本之间会有短暂的同步延迟问题但问题其实也不大因为我们通常对数据库进行写操作时也会锁表。
如果你要细究这个问题那就会碰到分布式架构无法同时满足的CAP[4] 课题。我们在此也不深入展开CAP话题了目前异步同步的时间差在很多场景下我觉得是可以接受的。
<img src="https://static001.geekbang.org/resource/image/96/36/966b47dfea82a3d841e45cd260270636.jpg" alt="" title="微服务拆解演示">
拿我们自己的例子来说也很简单我们用消息队列RocketMQ写操作我们都写入RocketMQrule.js进程中我们监听RocketMQ一旦有rule写入的消息我们就更新我们的lowdb数据。围绕消息队列建立的同步机制让每个微服务的数据库和它的扩容副本之间自动同步了。对于微服务来说它本身的数据库还是Stateful的但在微服务外部看来这个微服务是Stateless的。如下图所示
<img src="https://static001.geekbang.org/resource/image/7a/3a/7ab3bebbffabd18282d54e17093d0c3a.jpg" alt="" title="同步机制">
跟传统的主从数据库方式不同的是我们每个微服务的单点实例都是独享一个数据库的rule这个微服务单点可以对外提供所有待办任务的RESTful API接口。
你要知道消息队列例如RocketMQ的可靠性是99.99999999%而我们常用的虚拟机ECS的可靠性也只不过99.95%。在高并发架构的设计中我们通常会要求Stateful节点一定要稳定而且越少越好所以消息队列对于微服务和FaaS来说都是可以作为支撑业务的核心。我们依赖消息队列会让整个架构更稳定。
## 总结
我们为了避免在FaaS中直接操作数据库而将数据库操作变成BaaS服务。为了理解BaaS化具体的工作我们引入了微服务的概念。微服务先将业务拆解成单一职责和功能的小模块便于独立运维再将小模块组装成复杂的大型应用程序。这一点上跟我们要做的后端应用BaaS化相似度非常高。所以参考微服务我们先将MVC架构的后端解成了两个微服务再用微服务解耦数据库的操作先将数据库拆解成职责单一的小数据库再用消息队列同步数据库和副本从而将数据变成了Stateless。
现在我们再来回顾一下这节课的重要知识点。
1. 微服务的概念它就是将一个复杂的大型应用拆解成职责单一的小功能模块各模块之间数据模型相互独立模块采用API接口暴露自己对外提供的服务然后再通过这些API接口组合成大型应用。这个小功能模块就是微服务。
1. 微服务10要素API、服务调用、服务发现日志、链路追踪容灾性、监控、扩缩容发布管道鉴权。这跟我们要做的BaaS化高度重合我们可以借助微服务来实现我们的BaaS化。
1. 解耦数据库通过额外进程让数据库与副本直接通过消息队列进行同步所以对于微服务应用来说只需要关注自身独享的数据库就可以了。微服务通过数据库解耦将后端应用变成Stateless的了但对后端应用本身而言数据库还是Stateful的。
## 作业
由于Kafka云服务比较贵所以这节课的作业我们采用表格存储[5]来模拟Kafka以实现DB的同步功能。**你可以尝试自己部署一下rule-faas到一个新的域名。**下面是具体的代码地址:
- 仓库地址:[https://github.com/pusongyang/todolist-backend/tree/lesson05](https://github.com/pusongyang/todolist-backend/tree/lesson05)
- 前端地址:[https://github.com/pusongyang/todolist-frontend/tree/lesson05](https://github.com/pusongyang/todolist-frontend/tree/lesson05)
- 实现后效果:[http://lesson5.jike-serverless.online/list](http://lesson5.jike-serverless.online/list)
你可以根据自己的域名,替换一下前端文件中的这个地址:[http://faas.jike-serverless.online/api/rule](http://faas.jike-serverless.online/api/rule),或者用我的前端代码[lesson5](https://github.com/pusongyang/todolist-frontend/tree/lesson05)分支修改/src/pages/ListTableList/service.ts中的地址自己 `npm run build` 一下替换掉项目public目录压缩上传到函数服务目录。
这次的部署会有些复杂我们要部署两个FaaS服务一个是rule-faas.js用来处理/api/rule另一个是index-faas.js用来处理我们的静态资源请求。原理图如下所示
<img src="https://static001.geekbang.org/resource/image/98/b0/987db6d15137ce820b56fcf97e91e8b0.jpg" alt="" title="部署示意图">
另外.aliyunConfig文件格式如下你需要填入自己的配置情况切记这个文件不可以上传到Git仓库
```
//.aliyunConfig文件保存秘钥切记不可以上传Git
const endpoint = &quot;https://rule.cn-shanghai.ots.aliyuncs.com&quot;;
// AccessKey 阿里云身份验证,在阿里云服务器管理控制台创建
const accessKeyId = &quot;AccessKey&quot;;
// SecretKey 阿里云身份验证,在阿里云服务器管理控制台创建
const accessKeySecret = &quot;SecretKey&quot;;
// 在数据链表中查看
const instancename = &quot;rule&quot;;
const tableName = 'todos';
const primaryKey = [{ 'key': 'list' } ];
module.exports = {endpoint, accessKeyId, accessKeySecret, instancename, tableName, primaryKey};
```
期待你的作业和思考,如果今天的内容让你有所收获,也欢迎你把文章分享给身边的朋友,邀请他加入学习。
## 参考资料
[1] [https://en.wikipedia.org/wiki/Twelve-Factor_App_methodology](https://en.wikipedia.org/wiki/Twelve-Factor_App_methodology)
[2] [https://12factor.net/zh_cn/](https://12factor.net/zh_cn/)
[3] [https://help.aliyun.com/document_detail/29532.html?spm=5176.11065259.1996646101.searchclickresult.6936139bcS8BlU](https://help.aliyun.com/document_detail/29532.html?spm=5176.11065259.1996646101.searchclickresult.6936139bcS8BlU)
[4] [https://www.ruanyifeng.com/blog/2018/07/cap.html](https://www.ruanyifeng.com/blog/2018/07/cap.html)
[5] [https://ots.console.aliyun.com/](https://ots.console.aliyun.com/index#/list/cn-shanghai)

View File

@@ -0,0 +1,135 @@
<audio id="audio" title="06 | 后端BaaS化业务逻辑的拆与合" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/90/33ccbfc021b2b37321afe8e1cb955390.mp3"></audio>
你好我是秦粤。上一课中我们学习了后端BaaS化的重要模块微服务。现在我们知道微服务的核心理念就是先拆后合拆解功能是为了提升我们功能的利用率。同步我们也了解了实现微服务的10要素这10要素要真讲起来够单独开一门课的。如果你不熟悉我向你推荐杨波老师的[《微服务架构核心20讲》](https://time.geekbang.org/course/intro/100003901)课程。
BaaS化的核心其实就是把我们的后端应用封装成RESTful API然后对外提供服务而为了后端应用更容易维护我们需要将后端应用拆解成免运维的微服务。这个逻辑你要理解这也是为什么我要花这么多篇幅给你谈微服务的关键原因。
上节课我们将“待办任务”Web服务的后端拆解为用户微服务和待办任务微服务。但为什么要这样拆是凭感觉还是有具体的方法论这里你可以停下来想想。
微服务的拆解和合并,都有一个度需要把握,因为我们在一拆一合之间,都是有成本产生的。如果我们拆解得太细,就必然会导致我们的调用链路增长。调用链路变长,首先影响的就是网络延迟,这个好理解,毕竟你路远了,可能“堵车”的地方也会变多;其次是运维成本的增加,调用链路越长,整个链条就越脆弱,因为其中一环出现问题,都会导致整个调用链条访问失败,而且我们排查问题也变得更加困难。
反过来看,如果我们拆解得太粗,调用链路倒是短了,但是这个微服务的复用性就差了,更别提因为高耦合带来的复杂且冗余的数据库表结构,让我们后续难以维护。我画了个图,你感受下。
<img src="https://static001.geekbang.org/resource/image/e3/81/e3809702efd712b18d28465b2512e681.png" alt="" title="拆解程度示意图">
## 拆之,领域驱动设计
那我们要合理地拆解微服务应该怎么拆解呢上节课其实我有提到目前主流的解决方案就是领域驱动设计也叫DDD[1]。DDD是Eric Evans在其2004年的同名书中提出来的一个思想但一直仅仅局限在Java的圈子里直到2014年微服务兴起后大家才发现它可以指导微服务的拆分这才走进了大多数人的视野。用一句话简单总结DDD就是一套方法论**通过对业务分层抽象,分析定义出领域模型,用领域模型驱动我们设计系统,最终将复杂的业务模型拆解为独立运维的领域模型。**
实际我自己在使用微服务开发的过程发现,微服务整体应该是一个动态网络结构[2]随着业务的发展这个网络结构也会发生变化。DDD能帮助我们前期分析出一个较好的网络结构但实际上我们更应该思考的是如何整体优化动态网络**减少核心节点,保护核心节点,降低网络深度等等。**
怎么理解动态网络优化呢?我们可以做个思维实验:假设我们将所有的功能都拆解成微服务,任意的微服务节点之间都可以相互调用,调用越频繁它们之间的距离就越近。那么我们考虑一下,当我们网站的访问请求流量稳定后,我们整个微服务节点组成的网络状态是怎么样的?
首先网络节点的相互制约总会让那些相互之间强依赖的、高耦合的节点,越走越近,最后聚集成一团节点。其次那些跟业务逻辑无关的节点,逐渐被边缘化,甚至消失。我们看这些聚集成团的节点,如果团里的点聚合太近了,其实是不适合拆分的,它们整体应该作成一个微服务。等这些节点太近的团合并成一个微服务节点后,我们再看那些聚集在一起、又不太近的节点就是一个个微服务了。
所以,我们在启动项目时,不用太过纠结应该如何去拆解微服务。而应该持续关注,并思考每个微服务节点的合理性。就像看待动态网络一样,持续地调整优化,去除核心节点。最终它会伴随你业务的发展阶段,达到各个阶段的稳定动态网络结构。
就像我们上节课“待办任务”Web服务一样我们可以先简单地将我们的项目后端分为用户微服务和待办任务微服务。当然这里我们目前的业务太简单了用DDD去分析也是大材小用。随着我这个项目的业务发展我们添加的功能会越来越多。让微服务根据业务一起成长演变就可以了。这并不是说我们就放任微服务不管了而是从整体网络的角度思考去看我们的微服务如何演进。
<img src="https://static001.geekbang.org/resource/image/58/cf/589469933bdc3b0335f9754ff2f555cf.png" alt="" title="微服务演进过程">
## 合之Streaming
看完拆解我们再看合并。合并呢换个高大上的词其实就是前面课程中提到的编排。目前为止我们整个“待办任务”Web应用架构的设计基本完成了而且所有节点都是Stateless的了。变成Stateless节点后其实对于前端的同学来说一点都不陌生比如React的单向数据流中的State也要求我们ImmutableImmutable其实就是Stateless。
我们上面已经看到了拆解后的架构是个动态网络那我们应该怎么合并或者编排呢当然你像SFF那样通过传统的函数将每个HTTP数据的请求结果通过数组或对象加工处理再将这些结果返回也是可以的。但我在这里想向你介绍另外一种编排思路工作流。
<img src="https://static001.geekbang.org/resource/image/d2/57/d267851c983200b430dfb5d53fd4d557.gif" alt="" title="数据流演示图">
我们可以将用户的请求想象成我们的呼吸系统我们的肺就是SFF而微服务和FaaS节点就是需要氧气的各个器官。我们吸一口气氧气进入肺部血液循环将氧气按顺序流经我们每个器官这就是请求链路。每个器官一接收到新鲜血液就会吸取氧气返回二氧化碳最终血液循环将二氧化碳带到肺部呼出这个就是数据返回链路。我们的各个器官就被请求链路通过新鲜血液到来的这个事件串联起来了这个就是事件流也就是用一个个事件去串联FaaS或微服务。
现在我们用[[第3课]](https://time.geekbang.org/column/article/227454) 讲的PHP发邮件改造一下举个例子。当用户注册时我们完全可以将用户的信息和注册验证码存入数据库PHP发邮件的FaaS触发器改为数据库插入新记录触发事件用户从邮箱验证获取验证码把验证码写到输入框后点击验证则是另一个HTTP触发器触发FaaS函数校验验证码通过修改数据库注册成功并且返回302跳转到登录成功页面。具体流程可参考下图
<img src="https://static001.geekbang.org/resource/image/71/38/712127c53b9ba90fe69cf00179862338.png" alt="" title="案例流程图">
当然现在这个解决方案也有成熟对应的云BaaS服务Serverless工作流[3]。
## 安全门神
理解了拆合的思想我们就可以将目前“待办项目”的架构再演进一下静态文件我们用CDN托管前端项目只负责域名支撑和index.html剩下的请求直接访问FaaS微服务。这时候我估计你会问咱们数据的安全性如何保障呢是的到目前为止我们的FaaS都一直在用匿名模式访问完全没有任何安全防护可言也就是说目前我们FaaS服务的接口一直都在互联网上“裸奔”。
### 鉴权
其实FaaS提供的安全防护通常是放在触发器上的。触发器的授权类型或认证方式我们可以设置为匿名anonymous或函数function。匿名方式就是不需要签名认证匿名的用户也能访问而函数方式则是需要签名认证[4]这个签名认证的算法参数需要用到我们账户的访问秘钥ak/sk[5]ak/sk相当于我们云账户的银行卡密码这么重要的账户信息我们只能限定在服务端使用前端代码里绝对不可以出现。
也就是说,我们只能在服务端使用函数安全认证方式。如果是这种方案,我们的“待办任务”架构就演进成下图这样了。
<img src="https://static001.geekbang.org/resource/image/1f/b3/1f30750203dddab945fb1ff471e40ab3.png" alt="" title="“待办任务”架构图">
那有没有针对匿名认证方式的安全策略呢当然有这里我们同样需要借鉴一下微服务的鉴权设计JSON Web Token简称JWT[6]。JWT简单来说就是将用户身份信息和签名信息一起传到客户端去了。用户在请求数据时带上这个JWT服务端通过校验签名确定用户身份。JWT存在于客户端JWT验证只需要通过服务端的sk和算法验证签名。同样我画了张图以帮助你理解。
<img src="https://static001.geekbang.org/resource/image/f1/f1/f187db1d21b27d58c55000272f4825f1.png" alt="" title="JWT示意图">
要解决后端互调的安全性我们用VPC或IP白名单都很容易解决。比较难处理的是前后端的信任问题JWT正好就提供了一种信任链的解决思路。当然关于鉴权也有一些云服务商推出了一些更加安全易用的BaaS服务例如AWS的IAM和Cognito[7]。
安全性是我们考虑架构设计时重要的一环因为安全架构设计的失败会直接导致我们资产的损失。鉴权是识别用户身份防止用户信息泄漏和恶意攻击使用的。但根据我统计的数据我们在日常99%的问题,都发生在新版本上线的环节。
那我们该怎么稳定持续地快速迭代,发布新版本上线呢?我们可以回想一下[[第 1 课]](https://time.geekbang.org/column/article/224559)小程和小服的例子小程最后实现NoOps后小服则只要将代码合并到指定分支就可以发布上线了。那现实中这点该怎么实现呢
当我们的项目Serverless化以后代码的质量变得尤为重要。你可以想想Serverless化之前你不小心上线了一个bug影响的范围最大也就只有一个应用。但是Serverless化之后如果是核心节点发布了严重的bug上线那么影响的范围就是所有依赖它的线上应用了。
不过你也不用太担心微服务和FaaS都具备快速独立迭代的能力。以前我们一个应用的迭代周期通常要一周到两周。但对于Serverless化后的应用来说每个节点借助独立运维的特性可以随时随地的发布上线。
综上我们知道了微服务和FaaS都是快速迭代的修复问题很快但我们也不能每次都等问题出现再去依赖这个能力呀。有没有什么办法可以提前发现问题保证我们既快又稳目前软件工程的最佳做法就是代码流水线的发布管道。
### 发布管道
发布管道的流水线主要有3个部分
1. 代码发布前的验证代码测试覆盖率CI/CD
1. 模拟流量回归测试通过,发布到灰度环境;
1. 代码正式上线,灰度环境替换正式环境。流水线的每个节点产生的结果,都会作为下一个节点必要的启始参数。
<img src="https://static001.geekbang.org/resource/image/8e/4b/8e3a2a76cfcf51d2cc2eafaaf0a2db4b.png" alt="" title="发布管道流水线">
我们先看看上图,我来解释下这个流程。
- 我们的代码合并到指定分支后通常我会用Develop分支。
- Git的钩子就会触发后续的流水线开始进入构建打包、测试流程。
- 测试节点做的事情就是跑所有测试Case并且统计覆盖率。
- 覆盖率验证通过,代码实例用录制流量模拟验证。
- 模拟验证通过,发布代码实例到灰度环境。
- 线上根据灰度策略,将小部分流量导入灰度环境验证灰度版本。
- 在灰度窗口期,比如两个小时,灰度验证没有异常则用灰度版本替换正式版本;反之则立即丢弃这个灰度版本,止损。
这套流程目前规模大一些的互联网公司发布流程基本都在这样跑如果你不是很了解可以自己尝试用我们介绍的Serverless工作流或者云服务商提供的工作流工具[8]动手搭建下。
在这套流程的基础上,很多企业为了追求更高的稳定性,还会设定环境隔离的流水线和安全卡口。比如隔离测试环境和线上环境,测试环境用来复现故障。每次代码进入发布管道,都必须先在测试环境跑通,跑通后安全卡口放行,才能进入线上环境的流水线。
## 总结
这节课我们继续讲后端的BaaS化。我们再梳理一下这节课的重要知识点吧。
1. 如何拆解BaaS应用我们学习了微服务的重要拆解思想DDD**通过对业务分层抽象,分析定义出领域模型,用领域模型驱动我们设计系统,最终将复杂的业务模型拆解为独立运维的领域模型。**另外我也介绍了另一种更适合初创企业的拆分思路:动态网络演进。
1. 拆解完之后我们就要考虑合并。这里我们介绍了代码编排以外的另一种编排方式事件流编排它就是通过一个个事件顺序将我们的微服务或FaaS串联起来。
1. 为了解决拆解后微服务之间的信任问题。我们先了解了FaaS触发器的安全方案数字签名。还借鉴了微服务的鉴权做法JWT将用户鉴权加密信息放在客户端让鉴权服务变成Stateless。最后为了让微服务又快又稳地发布版本我们借鉴了微服务的发布管道打造自动灰度流水线。
## 作业
这节课的作业就是我们JWT鉴权的“待办任务”Web应用你来部署上线。
后端代码GitHub地址[https://github.com/pusongyang/todolist-backend/tree/lesson06](https://github.com/pusongyang/todolist-backend/tree/lesson06)
前端代码GitHub地址[https://github.com/pusongyang/todolist-frontend](https://github.com/pusongyang/todolist-frontend)
演示预览地址:[http://lesson6.jike-serverless.online/list](http://lesson6.jike-serverless.online/list)
期待你的作业,如果今天的内容让你有所收获,也欢迎你把文章分享给身边的朋友,邀请他加入学习。
## 参考资料
[1] [https://en.wikipedia.org/wiki/Domain-driven_design](https://en.wikipedia.org/wiki/Domain-driven_design)
[2] [https://en.wikipedia.org/wiki/Dynamic_network_analysis](https://en.wikipedia.org/wiki/Dynamic_network_analysis)
[3] [https://www.aliyun.com/product/fnf](https://www.aliyun.com/product/fnf)
[4] [https://github.com/aliyun/fc-nodejs-sdk/blob/master/lib/client.js?spm=a2c4g.11186623.2.15.16e016d7lo8NBQ#L840](https://github.com/aliyun/fc-nodejs-sdk/blob/master/lib/client.js?spm=a2c4g.11186623.2.15.16e016d7lo8NBQ#L840)
[5] [https://help.aliyun.com/document_detail/154851.html?spm=5176.2020520153.0.0.371a415dLXyltZ](https://help.aliyun.com/document_detail/154851.html?spm=5176.2020520153.0.0.371a415dLXyltZ)
[6] [https://jwt.io/](https://jwt.io/)
[7] [https://docs.aws.amazon.com/zh_cn/cognito/latest/developerguide/what-is-amazon-cognito.html](https://docs.aws.amazon.com/zh_cn/cognito/latest/developerguide/what-is-amazon-cognito.html)
[8] [https://www.aliyun.com/product/yunxiao/devops?spm=5176.10695662.1173276.1.6c724a38akCjgo](https://www.aliyun.com/product/yunxiao/devops?spm=5176.10695662.1173276.1.6c724a38akCjgo)

View File

@@ -0,0 +1,201 @@
<audio id="audio" title="07 | 后端BaaS化Container Serverless" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fc/31/fc3b6496365e041c16f88119b661ba31.mp3"></audio>
你好我是秦粤。上节课我重点给你讲了业务逻辑的拆和合拆的话可以借助DDD的方法论也可以用动态网络的思想让微服务自然演进合的话我们可以用代码编排也可以用事件流来驱动。另外我们还了解了微服务拆解后会带来的安全信任问题这可以通过微服务的跨域认证JWT方案解决。我们还了解了后端应用要支持快速迭代或发布可以参考微服务搭建灰度发布流水线也就是发布管道。其实我们在使用FaaS过程中遇到的很多问题都可以借助或参考微服务的解决方案。
现在我们再回顾一下BaaS化后的“待办任务”Web服务我们已经将后端拆解为用户微服务和待办任务微服务前端用户访问我们的FaaS服务登录后获取到JWT通过数据接口+JWT安全地访问我们的微服务。而且我们的FaaS和微服务都具备了快速迭代的能力。
<img src="https://static001.geekbang.org/resource/image/c8/15/c8ee82521e5c965d8955afe8c210b615.jpg" alt="" title="BaaS化后的“待办任务”Web服务">
到这里我要指出我之前rule-faas.js的一个Bug如果你之前亲自动手做过实验的话估计也有发现。这个Bug的直接表现是用户初次请求数据时如果触发了冷启动返回的待办任务列表就会为空。
究其原因是因为冷启动时连接数据库会有延时这直接导致了第一个请求返回的待办任务列表还未同步到消息队列的数据。要解决这个bug我们可以用之前讲过的预热FaaS或预留实例的方式但你也要知道FaaS函数扩容时新启动的函数副本也会出现同样的问题。
我前面卖了很多关子其实FaaS在设计时已经考虑到这个问题了所以在FaaS的“函数配置”里都会提供一项“函数初始化入口”的选项。但你会发现它同时也会让你配置初始化时间最少1秒。还记得我们在[[第 2 课]](https://time.geekbang.org/column/article/226574),讲函数执行阶段的一人一半的函数代码初始化时间吗?没错,就是那个时间。
当你配置了“函数初始化入口”每次启动FaaS实例时系统都会先等待初始化函数执行而且在函数初始化时间结束后才会继续执行函数。而从目前平台的配置来看初始化时至少也需要1秒的时间你去云服务商后台就能看到
对很多事件触发的应用场景FaaS增加几秒的初始化时间影响并不大。但对很多后端应用则不然尤其是Java等语言如果软件包比较大启动和初始化的时间会更长。
要缩短或绕过初始化的时间我们要尽可能地利用Runtime里面给我们提供的内置能力例如我们BaaS化一直提倡使用服务编排和HTTP接口就是因为云服务商SDK和HTTP协议所有语言的Runtime里都内置了。
那什么是Runtime呢Runtime其实就是运行我们代码所需要的函数库和二进制包。FaaS中的Runtime是云服务商预先设计好的会放一些常用的函数库和二进制包进去相当于是平台的能力。
但是当我们后端应用BaaS化后想采用FaaS方案部署的话则会碰到Runtime这个拦路虎。FaaS虽然已经支持多数主流后端语言但后端应用根据业务需求要依赖特殊的函数库或二进制包FaaS的官方Runtime就无法支持。而且像Java等语言在代码包较大的情况下FaaS启动速度很慢也不适合。例如Node.js的Runtime其实也包括我们自己安装的NPM包所以相当于我们可以部分定制。但是你也注意到了我们是整个目录包括node_modules 一起上传的也就是说涉及编译的NPM包是无法跨操作系统生效的。这种场景下FaaS的Runtime不可控就会成为我们难以绕过的问题了。当我们遇到FaaS无法解决的场景我们就可以考虑下降一层使用FaaS的底层支撑技术Docker容器了。
<img src="https://static001.geekbang.org/resource/image/09/d7/09bb10fa7d1a95f88828bc413c7c9bd7.png" alt="" title="广义Serverless">
还记得我们[[第 1 课]](https://time.geekbang.org/column/article/224559) 中的广义Serverless的图吗基础设施中的容器一般情况下指的就是Docker容器。Docker 这个技术你肯定或多或少已经用过了使用它们可以将应用代码和代码依赖的Runtime一起打包成一个Docker镜像。这个Docker镜像可以在云上、自己的笔记本电脑、同事的电脑上无畅运行并且完全不用担心环境依赖的问题因为我们应用的依赖也打包在一起了。
到这里我们又引入了Docker的概念Docker出现之后CaaSContainer as a Service很快也就流行起来了。为了帮助你理解IaaS、PaaS、BaaS、CaaS这几者的关系我画了张图希望能从云服务发展的角度帮你梳理下脉络。
<img src="https://static001.geekbang.org/resource/image/f0/3e/f0d74468ff49c43c9367fff9cc58833e.png" alt="" title="云服务发展进程">
图片很好理解我就不解释了。不过这里要解决一个你可能会有的疑惑为什么BaaS的出现比Serverless FaaS还要早三年那是因为早期出现的BaaS其实是mBaaS移动后端即服务这概念当时也曾经火过一段时间现在已经被Google收购的FireBase其实就是做的这个生意。mBaaS设计之初是专门为移动端提供后端服务的例如用户管理、角色服务、文件存储服务等等。除了服务对象是移动端之外跟我们说的BaaS概念很像。移动端可以将BaaS的所需鉴权算法放到客户端中并随着客户端的版本定期更新秘钥。但前端却做不到因此mBaaS只局限在移动端没有火起来。直到FaaS的出现才诞生了BaaS的使用场景mBaaS也开始转型支持更广范围的前端场景了。
VM是一种虚拟化技术这个我们都知道其实Docker也是一种虚拟化技术只不过它是利用新版Linux内核提供Namespace、Cgroups和Union File System特性模拟操作系统的运行环境。它跟虚拟机VM最大的区别在于Docker是进程模型的。怎么理解这句话呢我们需要画一张图。
<img src="https://static001.geekbang.org/resource/image/2f/18/2f941369b366fff8883499d200cccb18.png" alt="" title="Docker进程模型">
从图中我们可以看出虚拟机是在宿主机上启动一个虚拟机监视器HyperVisor管理控制虚拟机的。而虚拟机自身包含整个客户操作系统、函数库依赖和用户的应用虚拟机操作系统之间相互都是隔离的。经典的HyperVisor就是VirtualBox[1],你感兴趣的话可以下载体验一下。你也可以试想一下,如果我们我们一台虚拟机部署一个微服务,其实资源利用率是很低的。
容器实例则只包含函数库依赖和用户的应用操作系统是依赖宿主机的用户态模拟的也就是说容器之间是共享宿主机内核的。所以Docker更加轻量启动速度更快代码执行速度也更快。
Docker的容器呢因为只包含函数库依赖和用户的应用可以部署到任意Docker引擎上而Docker引擎呢可以安装在你的个人电脑、公司机房或者云上。通常我们容器移动时是移动容器的硬盘快照内存中的状态我们比较难复制这个硬盘快照就是Docker镜像。我们给Docker的镜像打上标签标签就像是镜像的唯一标识符URI打上后它就可以到处移动了。这个也是Docker的核心概念build、ship、run。
其实你会不会觉得Docker的这个层级结构有些眼熟是的这正是我们[[第 2 课]](https://time.geekbang.org/column/article/226574) 中讲的FaaS分层。我们回想一下我当时所说的操作系统容器就是Docker模拟的Runtime其实就是Bins/Libs层。云服务商的冷启动加速就是利用Docker镜像缓存加速。我想你也应该猜到了其实很多云服务商FaaS和PaaS的底层技术就是容器即服务CaaS。
那么接下来我们就体验一下Docker的便利吧。我们的“待办任务”Web服务只要添加一个文件就可以让它变成Docker镜像了。
## Docker再探
这里我建议你最好自己在电脑上安装体验一下Docker。在主流的操作系统数安装Docker都不难的只需要下载安装包就可以了。
### build: Dockerfile
构建是Docker最重要的一环。Docker镜像是硬盘快照那我们就看看标准的Docker硬盘快照如何制作吧。下面就是我们在代码根目录下增加的文件供你参考
```
# FROM是指我们的镜像基于哪个镜像来构建我们这里是基于jessie linux的Node.js镜像
FROM registry.cn-shanghai.aliyuncs.com/jike-serverless/nodejs
# ENV是设置环境变量
ENV HOME /home/myhome/myapp
# RUN是执行一段命令
RUN mkdir -p /home/myhome/myapp/public
# COPY是将当前目录下的内容拷贝到容器中
COPY . /home/myhome/myapp
COPY public /home/myhome/myapp/public/
COPY node_modules /home/myhome/myapp/node_modules/
# WORKDIR是设置进入容器后的工作目录
WORKDIR /home/myhome/myapp/
# ENTRYPOINT是容器启动后执行的脚本
ENTRYPOINT [ &quot;node&quot;,&quot;index.js&quot; ]
```
通常我们使用Docker前先去Docker Hub上找合适的基础镜像。看看基础镜像上都安装了哪些函数库或二进制包再对比一下要运行自己的应用还缺少哪些函数库和二进制包。在基础镜像的层级上加上自己的依赖。这样我们就构建好适合自己的镜像了。以后我们就能FROM自己的基础镜像构建自己的应用了。
然后你在项目下执行:`docker build` 命令就可以帮你构建Docker镜像了。
```
docker build --force-rm -t myapp -f Dockerfile .
```
构建好的镜像,我们可以通过 `docker run` 这个命令在本地调试。
```
docker run -d -p 3001:3001 -e TEST=abc myapp:latest
```
然后我们通过浏览器访问[http://127.0.0.1:3001](http://127.0.0.1:3001)就可以看到我们刚刚构建的Docker内容了。你会发现这不就是我们云上运行的版本吗是的既然FaaS是用CaaS技术实现的。我们当然也可以利用Docker实现我们的“待办任务”Web服务。
为了方便Docker例子的展示这节课的项目代码index.js包含了rule.js的逻辑。你会发现index.js中我们启动的时候不用关心初始化需要等待多少秒了而是直到初始化完成后才监听3001端口而Node.js连接数据库的时间通常也只需要几十毫秒。
<img src="https://static001.geekbang.org/resource/image/9f/c4/9f7572a1ee36d167d30940f08d96ccc4.png" alt="" title="Docker版本的“待办任务”Web服务">
另外为了方面你观察和调试Docker容器实例我这里给出一个登录Docker容器实例的命令。
```
docker exec -it 容器ID bash
```
### ship: Docker Registry
本地调试完我们再看看如何部署到云上。Docker官方的镜像仓库[2]速度太慢,现在的云服务都支持私有的容器镜像仓库[3]上面构建Dockerfile里面的Node.js镜像就是用我自己的私有容器仓库搭建的。
你可以登录云服务的容器镜像服务,他们都会告诉你如何 `Docker login` 到私有Registry如何打标签以及如何上传。
用我们的例子来说,大致会有下面的命令。
未登录过的话要先登录Registry。
```
docker login --username=极客时间serverless registry.cn-shanghai.aliyuncs.com
```
根据容器镜像仓库的URI打标签。镜像ID可以通过 `docker images` 查看。
```
docker tag 你本地的镜像ID registry.cn-shanghai.aliyuncs.com/jike-serverless/todolist
```
上传镜像到私有的Registry仓库。
```
docker push registry.cn-shanghai.aliyuncs.com/jike-serverless/todolist
```
### run: Docker Engine
运行Docker镜像就很简单了云服务都支持容器服务CaaS。你只需要购买好容器服务就可以从自己私有的容器镜像仓库Docker Registry中下载镜像并运行了。你要是执行过上面的例子那相信你也能体会到为什么FaaS的冷启动速度这么快了。不过需要提醒你一下这里会产生费用。
<img src="https://static001.geekbang.org/resource/image/b0/32/b0f0839d924252943ab9cd67df923b32.png" alt="" title="运行Docker镜像示意图">
另外我们的专栏是讲Serverless但是为了让你理解Serverless底层的实现所以我们也讲到了Docker容器的部分内容。对于我们专栏来说你不用尝试部署云上容器了。
恭喜你获得DevOps奖章一枚
现在我们了解了容器也知道了FaaS是构建在容器上的。我们的微服务和整个应用都可以部署在CaaS之上容器对我们来说更加可控因为我们可以通过Dockerfile安装我们需要的各种函数库依赖或者二进制文件。但这里还有一个问题FaaS的扩缩容是怎么做到的呢我们如果采用了容器容器如何做到扩缩容呢
## FaaS与Docker的扩缩容
FaaS中的扩缩容我们可以通过云控制台看到在FaaS函数配置中的“单实例并发度”。FaaS的做法比较简单粗暴需要我们告诉函数服务我们的单个函数实例可以承载多少的并发量如果事件触发并发量超出了这个并发度则会自动扩容。扩容的数量N就是这个事件触发量除以单机并发量取整。例如我们设定我们rule-faas函数的单实例并发度为10那么当用户并发量是11时就会扩容2个实例。如果用户并发量达到100就会扩容出10个实例。
这种做法其实是比较机械式的我们再看看FaaS的“函数指标”监控图。你有没有想到我们其实可以利用实时监控去控制扩缩容例如当单个函数实例承受不了时内存使用率会越来越高它的执行时间也会越来越长。
<img src="https://static001.geekbang.org/resource/image/5e/2c/5e4d42ca0b4f27dbf04d91c31fa8f62c.png" alt="" title="FaaS的“函数指标”监控图">
是的我们在使用Docker时要考虑的就是监控指标metrics以及扩容水位。
监控指标metrics就像FaaS“函数指标”监控图那样是一系列需要我们关心的单个容器核心指标包括CPU利用率、内存使用率、硬盘使用率、网络连接数等等。
我们将监控指标中的各项数值想象成蓄水,我们监控就像一个蓄水池,一旦某一项超过了蓄水池,水就会溢出。所以,我们要设定**水位**告警。我们在蓄水池里面画上刻度,当水位到这个刻度,我们就马上给这个蓄水池扩容。
<img src="https://static001.geekbang.org/resource/image/ab/ba/abf169adcc352cc0c37c6e66b710c6ba.png" alt="" title="蓄水池案例">
Docker容器本身并不提供扩缩容机制但我们要让Docker自动化扩缩容就可以用监控指标和水位去设计扩缩容机制。我们需要实时监控容器状态当容器状态的某一项数值过高时我们就需要给容器扩容。FaaS的弹性做法很简洁而Docker的扩缩容机制弹性更高、更加可控。但是Docker容器通常需要我们至少保持1个容器实例在线。相信你也知道了FaaS的预留实例是怎么做到的了吧
讲到这里不知道你发现没有我们BaaS化的三讲已经默默地实现了微服务的10要素。这也是为什么我一直说BaaS化和微服务高度重叠。
我们再来回顾一下微服务10要素。其中API、服务调用、服务发现我们可以通过RESTful HTTP接口实现日志、链路追踪我们没有展开但我们可以依赖云服务商提供的日志采集解决鉴权我们可以用跨域认证JWT解决发布管道需要我们搭建流水线发布管道容灾性、监控、扩缩容我们可以通过实时监控和扩缩容解决。到这儿呢我们的高铁车厢也终于完成了。
这节课的内容挺多的需要你动手消化吸收一下。下节课我们再看看如何利用K8s调度我们的Docker容器。
## 总结
BaaS化的内容到今天我们用了三节课讲完了现在我们一起回顾一下。
我们讲后端应用BaaS化的问题转换为后端应用NoOps微服务。所以我们先了解了微服务的10要素并且看到微服务中如何通过消息队列将Stateful的节点变成Stateless的。在微服务的拆解过程中我们学习了微服务的拆解思想DDD和动态网络演进以及拆解后带来的信任问题需要我们加密身份认证。合并时除了代码里面编排HTTP请求结果我们还学习了事件流触发获取结果。为了让微服务能够快速迭代我们还需要自己搭建流水线发布管道。
这节课我们通过FaaS和BaaS的初始化问题向你介绍了FaaS和BaaS依赖的底层实现容器Docker。Docker也是一种虚拟化技术它的核心理念是build、ship、run。通过将我们的应用代码和应用依赖的函数库、二进制包打包在一起的方式解决应用开发环境和运行环境的一致性问题。我们可以借助Docker容器维持我们的应用实例来解决初始化慢的问题。
我们还了解了FaaS的扩缩容逻辑是通过用户配置的“函数初始化入口”以及固定的“初始化超时时间”配合“单实例并发度”来实现的。而我们在容器Docker其实可以采用实时监控配合扩容水位来做到更加弹性的扩缩容策略。
接下来我会通过K8s实践我们目前所学到的内容我们的“待办任务”Web服务将在我们本地搭建的CaaS环境和Serverless环境中开发和调试。
## 作业
首先,拉取我们[lesson07](https://github.com/pusongyang/todolist-backend/tree/lesson07)的代码为“待办任务”部署的rule-faas函数添加初始化入口。
这节课的作业呢就是我们要在本地完全通过Docker容器搭建起我们的“待办任务”Web服务。除了css和js静态资源是来自CDN其它内容都将运行在Docker容器里。
相信你可以通过这个作业体验到FaaS的底层CaaS的运行机制。
当然如果你有预算也可以将Docker镜像上传到云服务商的Registry在云上购买容器服务就可以部署你的Docker镜像并在云上运行我们的“待办任务”Docker版本了。这样你就拥有了一个永不停机的Docker服务。
另外我也希望你可以帮助我继续优化我们的课程作业代码。如果你有更好的建议也可以通过Github的MergeRequest告知我。
接下来就期待你的作业和建议了。如果今天的内容让你有所收获,也欢迎你把文章分享给身边的朋友,邀请他加入学习。
## 参考资料
[1] [https://www.virtualbox.org/](https://www.virtualbox.org/)
[2] [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/)
[3] [https://hub.docker.com/](https://hub.docker.com/)