一路繁花似锦绣前程
失败的越多,成功才越有价值

导航

 

十四、服务端技术选型-磨刀不误砍柴工

1、导学
* 服务端技术选型
    - 【基层程序员】:对技术有感情,热衷于追求新技术,有技术选型有偏见,会为“什么是最好的语言”而
      争吵。【架构师】:冷静、平和的看待所有技术,不管新、旧,只要场景合适即可采纳,把技术当做实
      现业务功能的手段,而不会被技术所束缚。另,还要考虑整体团队的学习和使用成本。
* 将收获什么
    - nodejs做服务端常用的技术
    - 如何以架构师思维做技术选型
    - 扩展技术广度
* 主要内容
    - nodejs框架选型:Koa2
    - 数据库:Mysql Mongodb Redis
    - 登录校验:JWT
    - 单元测试和接口测试:Jest
    - 线上服务:PM2 + nginx
* 关键词
    - 上述的各个技术名词,不再赘述
    - 技术广度
* 学习方法
    - 抛开对框架的成见,要让它们平等的为你服务。只选择最合适的,不要秀技术
    - 没有银弹,不要纠结,执行起来,Just do it!
* 注意事项
    - 课程不会带着写代码,但你不要眼高手低,要亲自实践
    - 要考虑各种成本:学习成本、开发成本、运维成本
    - 要考虑稳定性,考虑社区大环境
    - 我们还是前端技术人员,不要和纯后端人员或者DBA比拼
    - 不要觉得讲解基础知识是在浪费时间。真正浪费时间的是:你因节省这几十分钟而踩坑或者走弯
2、nodejs框架选型
* 选择Koa2框架
    - 所有常见的nodejs框架中,Koa2是最简单、最小的
* 主要产出
    - 了解各个nodejs框架,扩充知识广度
* 主要内容
    - Koa2和Express
    - Egg.js
    - Nest.js
* 注意事项
    - 架构师不纠结于框架。主要看业务需求,研发成本和效率,稳定性。
    - Koa2并不一定是你实际工作中的最佳答案,根据自己的情况来选择。
3、介绍koa2和express
* Koa2和Express
    - express是非常优秀的框架,npm下载量也远在koa2之上。
    - 不过,koa2比express更加简单,而且社区也已经完善,也经历了很多年的验证。我个人更加喜欢
      小而美的东西。所以选择koa2。
    - 当然,选择express也是非常正确的。这俩都可以。不要纠结于框架。
        ~ koa2:https://koa.bootcss.com/
        ~ express:https://expressjs.com/zh-cn/
* 用脚手架创建项目
    - koa2:https://www.npmjs.com/package/koa-generator
    - express:https://expressjs.com/zh-cn/starter/generator.html
* 用koa2创建项目
    - 创建repo:biz-editor-server(为何设置私有private?)
    - 本地,用脚手架生成项目
    - 调整项目目录,代码都放在src目录下
    - 发布到github
* 小结
    - 选择koa2并创建项目
    - express和koa2用法非常相似,也可以选择
4、egg.js
* egg是现成的框架,也是基于koa2封装的。
* egg拿来讲课,直接使用,会隐藏很多细节。拿就不如我们自己拿koa2,从头封装一个egg。
* egg文档https://eggjs.org/zh-cn/,看完你会发现:它的目录结构、功能模块拆分,和我们最后的代码,
  是一样的。
* 可以参考课程已经开发完成的开源项目biz-editor-server,做一个目录的对比。
5、nest.js
* nest.js也是一个框架,默认基于express封装的。
* 使用ts语法的,大量使用装饰器,学习成本会比较高。国内的普及程度没有express和koa广泛,从百度
  搜索结果数量能看出来,其实Google也是。
* 所以,相对小众,又比较难入门的框架,一定要慎重选择。架构师思维,千万不要秀技术,不要个性
* nest.js文档https://docs.nestjs.cn/,去了解一下它的设计思想。
########
# 常用命令
nest g module <module-name>
nest g controller <module-name>
nesr g service <module-name>
########
* 路由的完整示例https://docs.nestjs.cn/7/controllers?id=%e5%ae%8c%e6%95%b4%e7%a4%ba%e4%be%8b。
* PS:关于装饰器,不会再讲了,可以去看下《Javascript设计模式》课程https://coding.imooc.com/class/255.html。
6、数据库
* 引言
    - Mysql Mongodb Redis都有使用,体现技术广度。注意,真的有使用场景,并不是为了使用而使用。
* 主要产出
    - Mysql和Sequelize
    - Mongodb和Mongoose
    - Redis
* 主要内容
    - 介绍Mysql和Sequelize
    - 介绍Mongodb和Mongoose
    - 介绍Mysql和Mongodb的区别
    - 介绍Redis
* 注意事项
    - 前端程序员对数据库了解可能不多,如果遇到不懂的一定要自己去补课(相关的实战课)
    - 将演示代码,但不会从0带着写,但切忌眼高手低
    - 毕竟我们还是前端技术人员,别要求自己有DBA的技术
7、复习数据结构设计
* 回顾之前的数据结构设计和数据流转关系,温故知新。
* 全面考虑一下需要存储的数据,以及需要的存储能力。
    - JSON数据存储作品内容
    - 作品的其他信息,用户的信息-表格形式存储
    - 一些缓存-大型网站,复杂业务,肯定会用到缓存
* 以上需求,就需要使用:Mongodb Mysql Redis。可见是真的有使用场景,并不是为了使用而使用。
8、Mysql和Sequelize
* 引言
    - Mysql是Web应用中最常见的关系型数据库。
* 安装
    - Mysql Server
    - GUI客户端Workbench(或者navicat)
    - 课程不讲。但我会整理一个文档,供大家参考。
* 基本使用
    - 从入门到基本使用,包括SQL语句,课程不讲,不了解的同学可以去学习实战课。
    - 新建一个数据库imooc_lego_course,使用mysql2测试数据库连接。并新建路由/api/db-check,
      用于展示结果。直接查看代码演示。
* Sequelize
    - 最常用的数据库ORM框架。它让开发者不用写繁琐的SQL语句,通过API即可操作数据。
        ~ 数据库连接
        ~ 数据模型
        ~ 模型和数据表的同步,注意bin/www启动服务时的改动
    - 直接看代码演示。
    - 代码中没有演示增删改查的API,可以先去看看sequelize文档。
* 小结
    - Mysql
    - Mysql2
    - Sequelize(重点)
* 【附】相关实战课
    - 学习一门大型、复杂、非0基础的课程,学习过程中,暂停下来去补充其他知识,是很正常的事情。
        ~ 《Node开发Web Server博客》:https://coding.imooc.com/class/320.html
        ~ 《Node.js-Koa2从零模拟新浪微博》:https://coding.imooc.com/class/388.html
9、Mongodb
* 引言
    - Mongodb是Web应用中最常见的NoSQL数据库。
* 安装
    - 服务端
    - 客户端compass
    - 课程不讲。但我会整理一个文档,供大家参考。
    - 新建一个数据库imooc_lego_course,work collection。
* 基本使用
    - 概念和使用,课程不再讲。
        ~ NoSQL
        ~ db数据库
        ~ collection集合
        ~ document文档
* Mongoose
    - 基本概念
        ~ Schema
        ~ Model
        ~ Document
    - 直接看代码演示
        ~ 数据库连接
        ~ 定义Schema Model-在此回顾一下数据结构设计
        ~ 修改路由/api/db-check
    - 代码中没有演示增删改查的API,可以先去看看Mongoose文档。
* 【附】相关实战课
    - 《Node开发Web Server博客》:https://coding.imooc.com/class/320.html
10、date和时区
* 引言
    - Mysql和Mongodb查看数据时,看着时间都不对。其实这和时区有关。
* new Date()
    - 控制台运行nodejs,不要用浏览器控制台。
    - new Date()直接打印,会显示世界标准时间,和北京时间差8个时区,即8h。同理,new Date(
      '2020-11-30 14:15:10')直接打印,结果是2020-11-30T06:15:10.000Z,差着8h。
    - 想要获取当前时区的时间,只需要toString()就可以了。
    - 如果想要格式,可使用date-fns的format,简单易用。
* mongodb和mysql
    - 和js Date一样,mongodb mysql存储的时间,直接显示也是时间标准时间,和北京时间差8h。
      例如显示"createdAt": "2020-11-30T06:19:14.000Z"。
    - 同理,如果想要显示为当前时区的时间,也只需要toString()即可。所以,可以直接把这个时间返回
      给前端,让前端自己去显示当前时区的时间。
########
const d = new Date('2020-11-30T06:19:14.000Z')
d.toString() // 获取使用 date-fns format
########
    - 如果在查询数据库时,要进行时间比较,那可以按统一标准:把时间都换成世界标准时间,进行比较。
      如要查询mongodb里createdAt在2020-11-20: 0:00:00到2020-11-30 23:59:59的数据
########
const d1 = new Date('2020-11-20: 0:00:00') // 转换为世界标准时间
const d2 = new Date('2020-11-30 23:59:59') // 转换为世界标准时间
// 对d1和d2,使用mongoose进行比较即可
########
* 总结
    - 世界上有那么多时区,那么多显示规则。而计算机存储时间的形式都是统一的,也必须统一,否则多乱。
    - 所以,只有toString()才分时区,而Date数据不分时区,无论在js中还是在数据库中。
    - 另外,Docker虚拟机里,默认没有各个时区,需要自己在Dockerfile里配置。课程后面会讲到,
      在此先看一眼。
########
# Dockerfile
FROM node:14
WORKDIR /app
COPY . /app

# 设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

CMD npm i && npm run prd-dev && npx pm2 log
########
11、Mysql和Mongodb的区别
* Mysql vs Mongodb
    - redis是内存数据库,做缓存。这一点很容易理解,不用过多解释。
    - 但Mysql和Mongodb有何区别,分别适用于什么场景呢?光一个“关系型”和“NoSQL”,不足以理解。
* 区别
    - Mysql是关系型数据库,Mongodb是文件数据库。
    - 一个用于存储表格形式,格式规整的数据。一个用于存储文件,格式零散的数据。
* 场景
    - 作品信息,用户信息,适合存储在Mysql中
    - 作品的内容JSON数据,适合存储在Mongodb中
* 疑问
    - 为何不把work信息全部存储到Mongodb?因为work要和user做连表查询。
    - 为什么入门课程只讲mongodb?
12、介绍Redis
* Redis
    - 大型网站,复杂业务,肯定会用到缓存。
* 多进程内存模型
    - 多核CPU擅长处理多进程任务,所有web server也都是多进程的,无论PM2、nginx还是其他。
      但进程之间有内存隔离(操作系统的基本功能),所以需要第三方缓存服务。
* 安装
    - redis-server
    - redis-cli
    - 课程不讲。但我会整理一个文档,供大家参考。
* 基本使用
    - 安装npm redis插件https://www.npmjs.com/package/redis,参考代码。
    - 数据库连接
    - 修改路由/api/db-check
* 【附】相关实战课
    - 《Node开发Web Server博客》:https://coding.imooc.com/class/320.html
13、登录校验
* 引言
    - 选择JWT,放弃Session
* 主要产出
    - 了解Web常用的登录鉴权方式
* 主要内容
    - cookie和Session
    - JWT
    - SSO和OAuth2
* 注意事项
    - 技术选型要考虑成本:服务租赁成本,维护成本
    - 要亲自实践,才能真正理解
* 关于短信验证码登录:
    - 用户体验好,无需注册,无需记住密码。
    - 但是,它要花钱。而且还要防止攻击,恶意刷接口的。
14、cookie和Session
* cookie做登录校验的过程
    - 前端输入用户名密码,传给后端
    - 后端验证成功,返回信息时set-cookie
    - 接下来所有接口访问,都自动带上cookie(浏览器的默认行为,http协议的规定)
* 为何会有Session?
    - cookie只存储userId,不去暴露用户信息
    - 用户信息存储在session中
* Session优点
    - 原理简单,易于学习
    - 用户信息存储在服务端,可以快速封禁某个登录的用户
        ~ 有这方强需求的人,一定选择Session
* Session的缺点
    - 占用服务端内存,有硬件成本
    - 多进程、多服务器时,不好同步
        ~ 一般使用第三方redis存储,成本高
    - 跨域传递cookie,需要特殊配置
15、JWT
* JWT === JSON Web Token
* JWT的过程
    - 前端输入用户名密码,传给后端
    - 后端验证成功,返回一段token字符串
        ~ 将用户信息加密之后得到的
    - 前端获取token之后,存储下来
    - 以后访问接口,都在header中带上这段token
* JWT的优点
    - 不占用服务器内存
    - 多进程、多服务器,不受影响
    - 不受跨域限制
* JWT的缺点
    - 无法快速封禁登录的用户
* JWT的Session的重要区别
    - JWT用户信息存储在客户端
    - Session用户信息存储在服务端
* 为何选择JWT?
    - 没有快速封禁登录用户的需求(或者极少有这种情况)
    - JWT成本低,维护简单
    - 需要考虑跨域的扩展性(虽然目前还没有这个需求)
* 代码演示
    - 安装npm插件koa-jwt jsonwebtoken
    - 封装jwt中间件,并使用
    - 封装loginCheck中间件
    - 相关的配置项、构造函数等
16、SSO和OAuth2
* 引言
    - SSO单点登录
    - OAuth2第三方鉴权的常用方式
* 使用cookie实现
    - 简单的,如果业务系统都在同一主域名下,比如wenku.baidu.com tieba.baidu.com,就好办了。
      可以直接把cookie domain设置为主域名baidu.com,百度也就是这么干的。
* SSO
    - 复杂一点的,滴滴这么潮的公司,同时拥有didichuxing.com xiaojukeji.com didiglobal.com
      等域名,种cookie是完全绕不开的。
* OAuth2验证
    - 上述SSO是oauth的实际案例,其他常见的还有微信登录、github登录等。即,
      当设计到第三方用户登录校验时,都会使用OAuth2.0标准。流程参考RFC 6749
    - 了解一下大概得流程即可,细节不用太详细看,用到了再说。
17、单元测试和接口测试
* 引言
    - 安全感,是人的一种本能需求。男/女朋友需要,你需要,老板也需要。
    - 架构师要保证软件质量,得讲求方式方法:单元测试和接口测试,就是重要手段。
* 主要产出
    - 单元测试和接口测试
    - 单元测试的正确套路
* 主要内容
    - Jest和Mocha
    - 单元测试为何难以落实
    - supertest接口测试
* 注释事项
    - 从此开始意识到单元测试的重要性,代码就一定要有测试
* 测试驱动开发TDD
    - Test-Driven Development。是一种不同于传统软件开发流程的新型的开发方法。它要求在
      编写某个功能的代码之前先编写测试代码,然后只编写使用测试通过的功能代码,通过测试来推动
      整个开发的进行。这有助于编写简介可用和高质量的代码,并加速开发过程。
    - 我们到没必要这么做,但要知道这个术语。
18、Jest和Mocha
* 引言
    - 这两个都是非常优秀的单元测试工具,Jest的npm下载量多一些,不过两者都是一个数量级的。
      选择哪个都可以。
* 入门
    - 对单元测试未了解的同学,可以通过文档学习
        ~ https://jestjs.io/docs/zh-Hans/getting-started
        ~ https://mochajs.cn/#getting-started
* 代码演示
    - 先把流程跑起来,测试代码等开发时再写。
        ~ 安装jest
        ~ 配置package.json
        ~ __test__/demo.test.js
* 课外分享
    - wangEditor开源项目的单元测试开发指南,最佳实践。
19、单元测试为何难以落实
* 引言
    - 单元测试,往往说起来都点头称赞,但做起来都犯难
* 使用方式不合理
    - 真正原因:混淆了单元测试和集成测试,导致单元测试代码中有太多Mock!!!如,需要服务器启动
      才能执行的代码,就不是单元测试了。
        ~ 单元测试,是针对一个单元,即单一的功能
        ~ 单元测试,是针对一段逻辑,有if...else for逻辑的,平铺直叙的代码不用测试
    - 如果单元没有被拆分,则拆分出来,再做单元测试。正好,这也是非常好的代码重构。
    - 摘抄一段《聊聊架构》里的代码
########
// 改造之前的。不容易进行单元测试,因为有request,需要启动http服务,单元测试就需要Mock
public OrderDTO getUserOrder(HttpRequest request) {
    String userId = request.getParameter("userId");
    String orderId = request.getParameter("orderId");
    UserDTO user = userManager.getUser(userId);
    OrderDTO order = orderManager.getOrder(orderId);
    if (order != null && order.getUserId != null && order.getUserId.equals(userId)) {
        order.setUser(user);
        return order;
    }
    return null;
}

// ---------------- 我是分割线 ----------------

// 平铺直叙的代码不需要单元测试
public OrderDTO getUserOrder(HttpRequest request) {
    String userId = request.getParameter("userId");
    String orderId = request.getParameter("orderId");
    UserDTO user = userManager.getUser(userId);
    OrderDTO order = orderManager.getOrder(orderId);
    return checkUser(order, user, userId);
}
// 逻辑单元,单独抽离出来,测试输入输出即可,不用Mock
public OrderDTO checkUser(OrderDTO order, UserDTO user, String userId) {
    if (order != null && order.getUserId != null && order.getUserId.equals(userId)) {
        order.setUser(user);
        return order;
    }
    return null;
}
########
    - 另外,测试覆盖率,应该看抽离出来的单元模块,而非所有代码的。因为有些代码根本就不需要、
      或者不适合单元测试。
* 另一个现状
    - 除了以上代码和技术方面的原因,阻碍单元测试开展的还有另外的原因:研发流程不规范。
        ~ 研发团队和测试团队分离
        ~ 如果项目延期,测试团队就抱怨提测延期了,测试时间不够
        ~ 研发团队也想多一事不如少一事,反正有人做测试,也不用我们写单元测试了
    - 上述情况,有工作经验的人相应很熟悉。这种情况,没有太好的解决办法,只能规范研发流程,
      规范各个流程的产出。这也是架构师的职责之一。
    - 所以,很多现代公司也在做组织架构的转型,特别是现代互联网公司。
        ~ 以不信任软件工程师为基础。传统软件公司、大公司多采用。特点是:文档规范详细,具有
          较大规模的测试团队,研发流程正规且冗长,职责分工明确,程序员就照着文档写代码即可,
          写完了就交付测试。
        ~ 以信任软件工程师为基础。现代互联网中下公司多采用,推荐程序员去理解业务,去承担测试。
          这样文档简单,执行效率高。但是要求团队必须敏捷管理,高效沟通,以及要求程序员要了解
          业务。
* 小结
    - 判断哪些代码需要单元测试,并抽离
    - 规范研发流程的步骤和产出
20、supertest接口测试
* 引言
    - 有接口测试的保护,让所有接口稳如老狗。
* 本地测试
    - jest + supertest
########
// 文档中的一个实例
describe('GET /user', function () {
  it('responds with json', function (done) {
    request(app)
      .get('/user')
      .auth('username', 'password')
      .set('Accept', 'application/json')
      .expect('Content-Type', /json/)
      .expect(200, done)
  })
})
########
* 远程测试
    - jest + axios,待系统发布到测试机,再使用。
* 代码演示
    - 安装supertest axios
    - package.json中增加test:remote,不过目前还用不到
    - 接口测试代码目录__test__/apis/
* 和单元测试的关系
    - 接口测试和单元测试,代码可能都放在__test__下,但两者概念要区分开。
21、pm2和nginx
* 引言
    - 线上服务关键:稳定 + 高效
* 主要产出
    - pm2和nginx的配置
    - pm2和nginx日志拆分
* 主要内容
    - pm2
    - nginx
* 注意事项
    - pm2和nginx都不急于实践,项目上线时会详细讲解
    - 但这些内容还是要知道,知道但不熟悉 vs 不知道--这俩是完全不同的
22、pm2
* 特点
    - 进程守护 - 稳定
    - 多进程 - 高效
    - 日志记录 - 问题可追溯
* 基本使用
    - 课程不再从0讲解
        ~ pm2 start xxx.js
        ~ pm2 restart <id/name>
        ~ pm2 reload
        ~ pm2 list
        ~ pm2 logs <id/name>
        ~ pm2 stop <id/name>
        ~ pm2 delete <id/name>
        ~ pm2 monit
* 配置
########
const os = require('os')
const cpuCoreLength = os.cpus().length // CPU几核

module.exports = {
  apps: {
    name: 'your-server-name',
    script: 'bin/www',
    // watch: true, // 无特殊情况,不用实时监听文件,否则可能会导致很多restart
    ignore_watch: ['node_modules', '__test__', 'logs'],
    // instances: cpuCoreLength, // 线上环境,多进程
    instances: 1, // 测试环境,一个进程即可
    error_file: './logs/err.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z', // Z表示使用当前时区的时间格式
    combine_logs: true, // 多个实例,合并日志
    max_memory_restart: '300M', // 内存占用超过300M,则重启
  }
}
########
* 代码修改
    - 安装:npm i pm2 -g
    - pm2配置,在bin/目录下
    - 修改package.json scripts(临时修改NODE_ENV=dev然后运行)
* 日志拆分
    - 使用pm2-logrotate
    - 安装pm2 install pm2-logrotate -g,运行pm2 list即可看到pm2-logrotate的进程
########
# 默认配置如下
$ pm2 set pm2-logrotate:max_size 10M # 日志文件最大10M
$ pm2 set pm2-logrotate:retain 30 # 保留30个文件,多了就自动删掉
$ pm2 set pm2-logrotate:compress false # gzip压缩文件
$ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
$ pm2 set pm2-logrotate:workerInterval 30 # 单位s,日志检查的时间间隔
$ pm2 set pm2-logrotate:rotateInterval 0 0 * * * # 定时规则
$ pm2 set pm2-logrotate:rotateModule true # 分割pm2模块的日志

# 可修改配置 pm2 set pm2-logrotate:<key> <value>
########
    - 可以修改一下rotateInterval试一试。不过,别忘了改回去。
23、nginx
* 引言
    - nginx一直是web server的必备神器,以稳定和高性能著称
        ~ 静态服务
        ~ 反向代理
        ~ 负载均衡(课程暂时用不到)
        ~ access log
* 安装
    - 课程不讲。
* 常用命令
    - nginx
    - nginx -s reload
    - nginx -s stop
    - nginx -t
    - nginx -c xxx.conf
* 配置
    - 参考www.imooc-lego.com服务器的nginx配置
########
user    root;
worker_processes    auto; # 多进程

error_log   /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include /etc/nginx/mime.types;
    default_type    application/octet-stream;
    
    log_format  main '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';
                     
    access_log  /var/log/nginx/access.log main;
    
    sendfile    on;
    #tcp_nopush on;
    
    keepalive_timeout   65;
    
    gzip on;
    
    # include /etc/nginx/conf.d/*.conf;
    
    # www.imooc-lego.com
    server {
        listen  80;
        server_name imooc-lego-editor;
        charset utf-8;
        
        index   index.html;
        root    /home/work/lego-team/editor;
        
        location / {
            try_files $uri $uri/ /index.html; # 支持前端h5 history路由
        }
    }
    
    # admin.imooc-lego.com
    server {
        listen  8000;
        server_name imooc-lego-admin;
        charset utf-8;
        
        location / {
            index   index.html;
            root    /home/work/lego-team/admin-fe/dist;
        }
        
        location /api/ {
            proxy_pass  http://127.0.0.1:3003;
            proxy_set_header Host $host:$server_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}
########
* 日志拆分
    - 系统上线时,还会讲解和演示。
* 使用logrotate
    - 阿里云服务器,按照之前配置nginx的方法,已经自带了,无需自己配置。
    - 可查看/etc/logrotate.d/nginx文件。
    - 这些不急于学习,等项目上线时再说。
* 使用crontab
    - crontab即linux的定时任务。可使用它来定时拆分nginx日志,需要手写代码。
    - 第一,创建脚本nginxLogRotate.sh
########
#!/bin/bash
base_path='/xxx/xxx/nginx' # 日志目录
log_path=$(date -d yesterday +"%Y%m")
day=$(date -d yesterday +"%Y%m%d")
mkdir -p $base_path/$log_path
mv $base_path/access.log $base_path/$log_path/access_$day.log
mv $base_path/error.log $base_path/$log_path/error_$day.log
########
    - 第二,执行crontab -e编辑定时任务,添加这一行
########
0 0 * * * sh /xxx/xxx/nginxLogRotate.sh
########
    - 第三,查看已有的定时任务crontab -l
24、开发环境
* 引言
    - 高效开发,全面武装
* 效率工具
    - eslint prettier
    - pre-commit
    - commit规范
* 功能
    - validator
    - cors

十五、服务端CI-CD:github自动化

1、导学
* CI/CD
    - 持续集成Continuous Integration(CI)和持续交付Continuous Delivery(CD)
    - 要让开发人员更加关注于业务代码的开发,那么架构师就得保证研发流程。
    - 合理全面的CI/CD,自动化研发流程,提高研发效率,增加系统稳定性。
* 标题
    - 服务端CI/CD流程:让github自动化为我们服务
* 将收获什么
    - 使用Github actions进行CI/CD
    - 学会Docker在nodejs项目中的应用
    - 搭建测试环境
* 主要内容
    - 使用Github actions
    - 使用Docker
    - 使用Docker-compose
    - 自动发布到测试机
* 关键词
    - CI/CD
    - Github actions
    - Docker Docker-compose
    - 新技能,有点难度
* 学习方法
    - 一定要亲自实践起来
    - 买一个便宜、低配的云服务器(系统centos 7.x)
* 注意事项
    - Linux系统使用和基础命令,不会从0开始讲
2、Github actions
* 引言
    - 网上的CI/CD有很多,例如travis,任选一个即可。
    - Github actions高效稳定,功能强大,易学易用。
* 主要产出
    - 使用Github actions自动化构建和测试
* 主要内容
    - 认识Github actions
    - 使用Github actions做构建和测试
* 注意事项
    - 接口测试,依赖于测试机搭建,后面会讲解
3、认识Github actions
* 引言
    - Github 2019年秋天发布的CI/CD工具,功能强大且稳定。
    - PS:Github被微软收购之后,越来越强大了,正在由一个git托管服务,变为一个研发项目解决方案。
* 基本认识
    - 介绍:https://github.com/imooc-lego/biz-editor-server-open/actions/new
    - 中文文档:https://docs.github.com/cn/free-pro-team@latest/actions/learn-github-actions
    - 代码在项目的.github/workflows目录下,.yml格式文件。
* 应用场景
    - 参考:https://github.com/imooc-lego/biz-editor-server-open/tree/master/.github/workflows
    - master分支,自动化测试
    - dev分支,自动部署到测试机
    - v*.*.*格式的tag,自动上线(支持回滚)
* 代码演示
    - 触发条件on
        ~ push
        ~ branches
        ~ paths
    - 任务jobs
    - 步骤steps,可自定义,也可使用第三方
########
# 直接使用uses第三方
- uses: actions/checkout@v2

# 使用name和uses第三方
- name: Use Node.js
  uses: actions/setup-node@v1
  with:
    node-version: 14

# 使用name和run
- name: lint and test
  run: |
    npm i
    npm run lint
    npm run test

# 直接使用run
- run: npm i
- run: npm run lint
- run: npm run test
########
* 小结
    - 认识Github action
    - 使用场景
    - yml语法格式
4、Github actions自动测试
* 新建workflow
    - 新建test.yml
########
name: test

on:
    push:
        branches:
            - master
        paths:
            - '.github/workflows/**'
            - '__test__/**'
            - 'src/**'

jobs:
    test:
        runs-on: ubuntu-lastest
        
        steps:
            - uses: actions/checkout@v2
            - name: Use Node.js
              uses: actions/setup-node@v1
              with:
                  node-version: 14
            - name: lint and test
              run: |
                  npm i
                  npm run lint
                  npm run test:remote
########
* 关于测试流程
    - pre-commit时执行本地接口测试npm run test:local
    - master push时执行远程接口测试npm run test:remote
    - 到本节为止,测试机尚未部署,所以暂时使用一个现成的测试机182.92.168.192:8081。
5、Docker
* 引言
    - 基于Docker,我们可以把开发、测试环境,一键部署到任何一台机器上。只要该机器安装了Docker。
    - 有了Docker就有了一切。
* 主要产出
    - 使用Docker构建nodejs项目
* 主要内容
    - 认识Docker
    - Dockerfile
* 注意事项
    - 专业的运维工程师对Docker还有更全面的应用:弹性扩展、微服务等
6、认识Docker
* 介绍
    - Docker就是一种虚拟机技术,比传统虚拟机(如vmware、virtualbox)要更加简单、轻量。
        ~ 启动快
        ~ 资源占用少
        ~ 体积小
* 安装
    - 不再演示,直接看文档。安装完记得镜像加速,搜索“docker镜像加速”即可。
    - 安装完,运行docker version可查看版本。
* 基本概念
    - 回忆之前使用vmware、virtualbox时,步骤如下
        ~ 下载一个centos.iso文件
        ~ 使用vmware安装一个系统A
        ~ 使用vmware安装一个系统B
        ~ ...
    - 此处的centos.iso文件就是一个image镜像,安装出来的系统就是一个一个的container容器。
    - docker的所有image都可以在https://hub.docker.com/搜索并下载,还可以自定义image上传到这个仓库。
7、常用命令
* 引言
    - 如果没有安装,可以在play with docker体验一下
* image镜像
    - 下载镜像docker pull <image-name>:<tag>
    - 查看所有镜像docker images
    - 删除镜像docker rmi <image-id>
    - 上传镜像docker push <username>/<repository>:<tag>,要先注册hub.docker.com
    - PS:如果docker images出现REPOSITORY是<none>的情况,可以运行docker image prune删除。
* container
    - 启动容器:docker run -p xxxx:xxxx -v=hostPath:containerPath -d --name <container-name> <image-name>
        ~ -p:端口映射
        ~ -v:数据卷,文件映射
        ~ -d:后台运行
        ~ --name:定义容器名称
    - 查看所有容器docker ps,加-a显示隐藏的容器
    - 停止容器docker stop <container-id>
    - 删除容器docker rm <container-id>,加-f是强制删除
    - 查看容器信息,如IP地址docker inspect <container-id>
    - 查看容器日志docker logs <container-id>
    - 进入容器控制台docker exec -it <container-id> /bin/sh
* 功能演示
    - 以nginx为例。
########
docker run -p 81:80 -d --name nginx1 nginx
docker ps

# 访问 localhost:81,并查看log

docker exec -it <container-id> /bin/sh
cd /usr/share/nginx/html
echo hello docker world index.html
exit

# 重新访问localhost:81,强制刷新

docker stop <container-id>
docker rm <container-id>
########
    - 单独演示一下-v数据卷。掌握好这个概念,后面Docker-compose会继续用到。
########
# 1.新建/Users/wfp/html/index.html,内容自定义即可

# 2.运行
docker run -p 81:80 -v=/Users/wfp/html:/usr/share/nginx/html -d --name nginx1 nginx

# 3.访问 重新访问localhost:81,看是否你创建的页面?
########
* 小结
    - docker概念
    - docker命令,run时的各个参数
8、Dockerfile
* 引言
    - 一个简单的配置文件,描述如何构建一个新的image镜像。
    - 注意:必须是Dockerfile这个文件名,必须在项目的根目录。
* 语法
########
FROM node:14
WORKDIR /app
COPY . /app

# 构建镜像时,一般用于做一些系统配置,安装必备的软件。可有多个RUN
RUN xxx
RUN xxx
RUN xxx

# 启动容器时,只能有一个CMD
CMD xxx

# 环境变量
ENV K1=v1
ENV K2=v2
########
* 构建
########
docker build -t <name> . # 最后的`.`指Dockerfile在当前目录下。
docker images
########
* 代码演示
    - .dockerignore文件
    - Dockerfile文件
    - 本地安装pm2:npm i pm2 --save-dev,或者Dockerfile中全局安装pm2
    - 由于Docker容器中没有数据库环境,服务启动不会成功。
    - 可以临时修改一下,再测试:
        ~ 去掉数据库连接
        ~ 去掉bin/www数据同步
        ~ Dockerfile中改为npm run dev
    - 测试过程如下:
########
docker build -t editor-server . # 构建image
docker images

docker run -p 8081:3000 -d --name server1 editor-server # 创建容器,注意端口映射
docker ps
docker logs <container-id> # 需等待构建完成

# 访问localhost:8081,查看docker logs

docker stop <container-id>
docker rm <container-id>
docker rmi <image-id>
########
    - 测试完记得git checkout .还原回所有代码。
* 小结
    - Dockerfile语法
    - 构建镜像
9、Docker-compose
* 引言
    - 基于Docker和Docker-compose
    - 通过一个配置文件,就可以让你的系统一键启动所有的运行环境:nodejs mysql mongodb redis
* 主要产出
    - 通过Docker-compose搭建开发环境
* 主要内容
    - 介绍Docker-compose
    - Docker-compose配置Redis
    - Docker-compose配置Mysql
    - Docker-compose配置Mongodb
* 注意事项
    - 请预先docker pull用到的镜像,否则Docker-compose初次启动会很慢
10、介绍Docker-compose
* 引言
    - 软件设计和开发,有单一职责原则。Docker也一样,每个容器都只负责一个服务。
    - 如果开发环境需要多个服务(nodejs mysql mongodb redis),就需要启动多个Docker容器。
    - 要连同这多个Docker容器,就需要Docker-compose。
* 安装
    - 课程不讲,一行命令即可安装。完成之后运行docker-compose --version可以看到当前版本。
* 配置文件
    - 新建docker-compose.yml文件。先以redis为例,演示多个容器如何关联。
########
version: '3'
services:
    editor-server: # service name
        build:
            context: . # 当前目录
            dockerfile: Dockerfile # 基于Dockerfile构建
        image: editor-server # 依赖于当前Dockerfile创建出来的镜像
        container_name: editor-server
        ports:
            - 8081:3000 # 宿主机通过8081访问
    editor-redis: # service name,重要!
        image: redis # 引用官网redis镜像
        container_name: editor-redis
        ports:
            # 宿主机,可以用127.0.0.1:6378即可连接容器中的数据库`redis-cli -h 127.0.0.1 -p 6378`
            # 但是,其他docker容器不能,因为此时127.0.0.1是docker容器本身,而不是宿主机
            - 6378:6379
        environment:
            - TZ=Asia/Shanghai # 设置时区
########
* 命令
    - 构建容器docker-compose build <service-name>
    - 启动所有服务器docker-compose up -d,后台启动
    - 停止所有服务docker-compose down
    - 查看服务docker-compose ps
* 修改代码
    - 本地安装pm2 npm i pm2 --save-dev,或者Dockerfile中全局安装pm2
    - 新建docker-compose.yml
    - 增加config/envs/prd-dev.js
* 演示
    - 需要临时改代码,再测试
        ~ 去掉mysql mongodb连接的代码,只保留redis的代码
        ~ 去掉bin/www数据同步
    - 再测试
########
docker-compose build editor-server # 配置文件的service name
docker-compose up -d
docker-compose ps

# 访问localhost:8081,查看docker logs

docker-compose down
########
* 小结
    - Docker-compose的作用
    - 配置文件docker-compose.yml
    - Docker-compose常用命令
    - 容器之间的连接,重要!
        ~ host: 'editor-redis'
        ~ 端口依然是6379
11、连接Mysql和Mongodb
* 引言
    - 成功连接redis之后,要继续连接mysql和mongodb
* 区别
    - redis无数据库,而mysql和mongodb需要创建数据库
    - redis是缓存,无需数据持久化,而mysql和mongodb需要
* 代码修改
    - 修改docker-compose.yml,代码如下
    - 增加mysql/init/init.sql,初始化mysql
    - 修改config/envs/prd-dev.js,增加mysql和mongodb的配置
    - 修改.gitignore文件,增加一行.docker-volumes/,重要!
########
    editor-mysql:
        image: mysql # 引用官网mysql镜像
        container_name: editor-mysql
        restart: always # 出错则重启
        privileged: true # 高权限,执行下面的mysql/init
        command: --default-authentication-plugin=mysql_native_password # 远程访问
        ports:
            - 3305:3306 # 宿主机可以用127.0.0.1:3305即可连接容器中的数据库,和redis一样
        volumes:
            - .docker-volumes/mysql/log:/var/log/mysql # 记录日志
            - .docker-volumes/mysql/data:/var/lib/mysql # 数据持久化
            - ./mysql/init:/docker-entrypoint-initdb.d/ # 初始化sql
        environment:
            - MYSQL_DATABASE=imooc_lego_course # 初始化容器时创建数据库
            - MYSQL_ROOT_PASSWORD=Mysql_2019
            - TZ=Asia/Shanghai # 设置时区
    editor-mongo:
        image: mongo # 引用官网mongo镜像
        container_name: editor-mongo
        restart: always
        volumes:
            - '.docker-volumes/mongo/data:/data/d' # 数据持久化
        environment:
            - MONGO_INITDB_DATABASE=imooc_lego_course
            - TZ=Asia/Shanghai # 设置时区
        ports:
            - '27016:27017' # 宿主机可以用127.0.0.1:27016即可连接容器中的数据库
########
* 演示
########
docker-compose build editor-server # 配置文件的service name
docker-compose up -d
docker-compose ps

# 访问localhost:8081,查看docker logs

docker-compose down
########
* 小结
    - volumes和数据持久化
    - environment和创建数据库,设置密码、时区等
12、自动发布到测试机
* 引言
    - github actions监听git提交,并执行自定义命令
    - docker一键部署开发环境
    - 两者集合,即可自动发布到测试机
* 主要产出
    - dev分支push时,自动部署到测试机
* 主要内容
    - 配置测试机
    - 自动发布到测试机
    - 远程接口测试
* 注意事项
    - 去买一个便宜的服务器,操作系统选择CentOS 7.x
    - 思考:线上环境,是否也是这样部署的?
13、配置测试机
* 购买
    - 阿里云较贵,可以考虑腾讯云、华为云等。
    - 学习用,只是要一个服务器而已,不用太考虑稳定性和其他扩展能力。
* 创建work账号
    - 出于安全考虑,日常不会用root账号登录,权限太高了。
    - 用root登录,创建work账号。
########
adduser work
passwd work
########
    - 添加work的sudo权限。
########
whereis sudoers # 找到文件位置 /etc/sudoers

chmod u+w /etc/sudoers # 修改权限,u表示所有者,w表示写权限,+表示添加

vim /etc/sudoers # 编辑该文件
# 找到    `root   ALL=(ALL)   ALL`
# 再加一行  `work   ALL=(ALL)   ALL`

chmod u-w /etc/sudoers
########
    - 然后使用work登录机器。输入su,再输入root账号的密码,即可拥有超级权限。
* 登录信任
    - 使用work登录机器,创建~/.ssh/authorized_keys文件。
########
# 修改文件夹权限
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
########
    - 将本机的id_rsa.pub内容粘贴进来。
    - 退出重新用work登录,将不用再输入密码。
* 安装必备软件
    - 以下都需要su权限。
* git
########
yum -y install git
git --version
########
* docker
    - 安装docker:https://docs.docker.com/engine/install/centos/
    - docker镜像加速
    - 安装docker-compose:https://docs.docker.com/compose/install/
########
docker version
docker-compose --version
########
* 开发端口
    - 开发需要的端口,否则外网无法访问该端口。
        ~ B端FE-80
        ~ B端server-8081
        ~ C端-8082
        ~ 统计服务,收集日志-8083
        ~ 统计服务OpenAPI-8080
        ~ admin FE-8085
        ~ admin server-8084
    - PS:线上环境,不会开放这么多端口,而是用nginx反向代理。
* 小结
    - 购买
    - work账号
    - 必备软件:git docker
    - 开放端口,重要!
14、发布到测试机
* 引言
    - 回顾github actions
* 梳理思路
    - 使用github actions监听dev分支push
    - 登录测试机,获取最新dev分支代码
    - 重新构建镜像docker-compose build editor-server
    - 重启所有容器docker-compose up -d
* 代码演示
    - 新建deploy-dev.yml内容如下:
########
name: deploy for dev

on:
    push:
        branches:
            - 'dev' # 只针对dev分支
        paths:
            - '.github/workflows/*'
            # - '__test__/**' # dev不需要立即测试
            - 'src/**'
            - 'Dockerfile'
            - 'docker-compose.yml'
            - 'bin/*'
jobs:
    deploy-dev:
        runs-on: ubuntu-latest
        
        steps:
            - uses: actions/checkout@v2
            - name: set ssh key # 临时设置ssh key
              run: |
                  mkdir -p ~/.ssh/
                  echo "${{secrets.WFP_ID_RSA}}" > ~/.ssh/id_rsa
                  chmod 600 ~/.ssh/id_rsa
                  ssh-keyscan "182.92.xxx.xxx" >> ~/.ssh/known_hosts
            - name: deploy # 部署
              run: |
                  ssh work@182.92.xxx.xxx "
                    # 【注意】用work账号登录,手动创建/home/work/imooc-lego目录
                    # 然后git clone https://username:password@github.com/imooc-lego/biz-editor-server.git -b dev(私有仓库,使用github用户名和密码)
                    # 记得删除origin,否则会暴露github密码
                    
                    cd /home/work/imooc-lego/biz-editor-server;
                    git remote add origin https://wangfupeng1988:${{secrets.WFP_PASSWORD}}@github.com/imooc-lego/biz-editor-server.git;
                    git checkout dev;
                    git pull origin dev; # 重新下载最新代码
                    git remote remove origin; # 删除origin,否则会暴露github密码
                    # 启动docker
                    docker-compose build editor-server; # 和docker-compose.yml service名字一直
                    docker-compose up -d
                  "
            - name: delete ssh key # 删除ssh key
              run: rm -rf ~/.ssh/id_rsa
########
* 功能演示
    - 教学演示的代码库,和实际项目的代码库,不一样。所以,不能公用一个测试机。
    - 可以用实际项目的代码,演示这个过程。
* 远程接口测试
    - 待测试机部署完成,即可进行远程接口测试npm run test:remote,即test.yml的内容。
    - 注意,一定要等待测试机部署完成,否则测试不通过。
* 小结
    - 思路
    - 代码实现
    - 【特别提醒】如果github actions任务出错,要第一时间去看日志!!!
posted on 2023-10-07 23:16  一路繁花似锦绣前程  阅读(34)  评论(0编辑  收藏  举报