此页面需要javascript支持,请在浏览器中启用javascript

3月来了,给自己做一个简单的nodejs后端技术总结

nodejs
docker
serverless
共2912个字,阅读时间 15 分钟
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://icebreaker.top/articles/2023/2/28-welcome-back

Image

3 月来了,给自己做一个简单的 nodejs 后端技术总结

我又滚回来写文章了,从去年 11 月底到今年 2 月底,算起来整整 1/4 年没有写博客了,自己的博客站都长满了坟头草,我在上面跳舞。在这 3 月来临之际,我们还是来聊聊技术,就当给自己做个阶段性总结。

完全重构

这几个月我经历了一次对自己前后端栈的一次彻底的重构。由于日益增长的需求,旧有的技术架构很多变成了技术债,拖累着后续的开发维护工作,来看看我具体做了哪些吧:

  1. 数据库切换 mongodb -> postgres
  2. 后端框架切换 koajs -> nestjs
  3. mongodb native client -> typeorm
  4. serverless -> serverless & docker compose swarm mode
  5. restful API -> restful API & graphQL
  6. github action CI/CD
  7. tensorflow 内容合规性检查(说白了就是鉴黄)

数据库切换迁移

mongodb 是个好东西,很多场景下使用起来非常方便。然而一天,我阅读几个月前写的代码的时候,突然感觉它很丑陋,尤其是聚合查询,一个aggregate管道,中间还夹杂着几个 $facet 几个 $replaceRoot,里面作为参数的索引字符串里,还套了许许多多的$与预留关键字。

阅读完后,我开始思考人生,我想起了那天在夕阳下的感叹,啊!那个少年竟然写出了如此精妙的聚合代码!而如今再与它两眼相望,竟无语凝噎。于是我果断把这段狗屎代码注释了。

当然我知道这也不怪 mongodb,这马是好马,只是骑的人没本事而已。经过考虑之后,为了代码的 TypeStrong 考虑,还是选择了 RDS。于是选用了postgres,依靠之前一定的 MS SQL Servermysql 的使用经验,还是非常好上手的。加上使用了ORM框架,隐藏了一部分直接写SQL的数据库操作。也写了一些脚本,定时pg_dump备份啥的,暂时够用,其他高阶用法,比如像 supabase 里什么行数据权限啥的就不是很了解了。

Why Nestjs?

把后端框架换了,为啥换 Nestjs 而不是基于koaegg,midway?究其根本是生态好,加上不是 KPI 项目。

生态好,体现在高质量的官方包与丰富的第三方最佳实践,另外其他开发人员踩过的坑多,好借鉴 (抄) 也是一个重要因素。

另外,我以前写 ASP.NET MVC/Core 的时候,就很喜欢 IOC DINestjs的核心就是这套机制。何况即使我可以利用 inversify 自己设计一套,但这哪有白嫖的香呢?

而且相比koa-compose那种,遇事不决middleware的开发方式。Nestjs提供了更多细粒度的切面方法,帮助我们更好的 aop,开发体验相当舒服。

prisma or typeorm?

当时在选型的时候,两边都看了一下文档,并且也都forkexamples项目跑了一下,最终选择 typeorm 了,原因有以下几点:

  1. prisma 定义实体对象,靠的是它自己的DSL。虽然这个DSL通俗易懂,但是schema.prisma这玩意实际上和 schema.gql 这类一样,在应用调试和测试的时候,都需要修修补补的。与其多个 DSL 一起修修补补,不如我一份 entity 利用 meta data 同时生成 db schemagql schema
  2. typeormnestjs 的适配性相当的好
  3. prisma client 需要初始化,同时在 schema 发生改变后,需要重新生成 (prisma generate)。

出于这些原因,选择了 bug 依然大堆的 typeorm,还妄想着通过定义一份 entity,可以派生出 dto,input,model... 这些其他的对象。实践事实证明,是可行的,但是需要你对元数据有比较精确的掌控,比如:

import { OmitType, PartialType, PickType, IntersectionType } from '@nestjs/mapped-types';
import { OmitType, PartialType, PickType, IntersectionType } from '@nestjs/swagger';
import { OmitType, PartialType, PickType, IntersectionType } from '@nestjs/graphql';

这些方法,它们各自取,或者说利用的元数据 (meta data) 是不咋一样的,(根本的原因来自于取的元数据的key的不同,这个藏在每个包的 *.constants.ts文件中),你需要根据你的需求,来调用对应包的中的对应方法。

serverless 函数辅助

更换后端框架的背后,也付出了一些代价。

当我把 Nestjs 应用部署到阿里云函数计算 / 腾讯云云函数的时候,这个冷启动时间远超 koa/express 这类应用,后来我又把整个 Nestjs app 经过 webpack进行打包压缩,再次进行部署测试。结果也只是稍稍优化了一点点冷启动时间,总的时间还是差强人意。这也是我选择自己买服务器部署的原因所在了。

对于目前 serverless 情况,很多实践还都是把传统的 web server, 或者通过http triggereventshim,或者通过端口监听,又或者通过 docker image 部署的方式部署到单个函数的。这种遇到业务规模,体量稍微大点的,就很难满足实时性的需求。

假如你说保留实例,那我为啥不自建? 另外多函数部署运维的serverless框架也有,可是投入这么大精力搞这些玩意,效果和产出还不如用k8s,你请得起高薪的程序员,难道不舍得花钱买十几二十台服务器吗?

但是这不意味着我要抛弃 serverless,只是让它从打二号位,转移到了五号位,打打辅助或者快速成型还是相当好用的。何况只要大家还在一个 VPC(私有网络) 里面,通信是比较方便的,打打辅助更是得心应手。另外我没有上 k8s 的原因,核心原因是 swarm 够用,根本原因是我不会。

GraphQL

这次总算把 GraphQL 加上了,前端爽歪歪,后端爽歪歪,什么?前端后端都是我?这下苦逼了。

首先,由于 GraphQL SDL schema 会按照图这样的数据结构进行分析来调用我们定义的字段节点处理方法,导致我们在定义通信 model的时候,一旦遇到相互关联的其他 model,就需要定义这个关系的处理方式,即 ResolveField Function。然而这种处理方式会带来一个问题,原先我们写 restful的时候,去取有关联的数据,几个 LEFT JOIN 就完事了,但是到了 GraphQLresolver 里面,由于 ResolveField Function 的独立性,它从一次数据库查询,变成了多次数据库查询,获取结果后再进行处理组合,变成前端想要的对象结构。

在这种情况下,数据缓存就显得及其重要了,另外缓存的key也必须很好的体现数据的特征。于是我果断拉取了 redis/redis-stack-server:latest,还是相当好用的。

另外有一点,个人不是很理解,感觉应该分开定义有可能很大的字段在不同的 model 里。举个例子,数据库有张表,表里面有个字段,每一数据行,这个字段的数据大小都超过了 1MB。然后后端去取数据的时候,又是 SELECT *,这很明显数据一多,内存就爆掉了呗。但是对于前端来说,我又没有去取你这个字段,你为啥要去取这个字段呢?所以后端也应该根据前端传过来的 gql语句,去预估数据的projection

Github Action CI/CD

目前我用的是云主机部署的,代码在 Github 私有仓库,那么有啥办法可以稍稍优化一下目前的部署方式呢?

这里我的做法不是很好,但是我必须说出来,这样大牛看到我的方案之后,才能对我进行降维打击。

首先我的前后端部署方案,都是依赖各个云的,出于历史原因,我的前端 OSS,CDN 这些在腾讯云上。我的云函数,域名,服务器,还有一些其他在阿里云上。另外手里还有一台华为云云主机。

首先是后端部署,我肯定不会在云服务器把后端的代码全部拉下来,打包编译运行的,这样又蠢又笨重。

在后端的代码仓库里,我仅仅保留 Dockerfiledocker-compose.yml,一个用来打镜像,一个用来起本地配套的服务,比如我会拉 redispostgres 的镜像,本地起几个用来开发和调试,还有数据库 schema 的同步和本地 migration 的预演。

另外我还有一个部署仓库,里面用来存放配置,比如 nginx.conf,各种ssl证书,环境变量,加上一大坨 shell脚本和不同的 compose文件,这些 compose 会映射各种容器卷这个目录下。

这样,我拿到一台机器之后,首先要做的,就是生成 ssh key,然后把 key 注册进 github,给它对应仓库的访问权限。拉下来之后,直接 docker compose up -d 就部署成功了。

当然,情况肯定不这么简单的,因为部署仓库里,存放的compose文件,里面都写的都是阿里云私有镜像的地址,所以还要 login 一下。

另外我还使用 Github Action 来进行镜像的打包,并推送到私有仓库,这一块非常简单,懒得说了。还有一个注意点就是,每次推送镜像,都会生成一个新的镜像 tag。那如何在推送之后,动态改变另外那个部署仓库的 IMAGE_TAG 呢? 实际上也很简单,在部署仓库也建立一个 Github Action 用来被动的接收,后端仓库推送完成后的 repository_dispatch里的参数就行。

参数 (payload) 里,存放着推送完成后的 tag,然后执行一行:

sed -i.bak '/^IMAGE_TAG=/s/=\w*/='${{ github.event.client_payload.tag }}'/' .env

然后 git commit 就搞定了。这样每次后端仓库镜像一推送,部署仓库的IMAGE_TAG就实时变化,云主机只要重新 git pull 然后 docker compose up -d 部署就完成了。

至于前端部署,这就看是 ssr, 还是 spa/ssg 这类的,这块之前搞 vue/react 应用开发,用 nuxt/next的比较多,这里就不继续说了。

部署 tensorflow 模型

之前我的技术群里,有小伙伴说它的小程序因为涉黄被封了,原因是用户自己上传了黄色图片,然后自己截了图,举报了这个小程序,然后就被微信封了。

于是我决定还是要防一手的,咱训练模型不会,工程化能力还是有一点点的,所以起码用这种还是非常容易的。

简单到,在 github 里找到并下载模型,然后本地加载调用就 ok 了。不过我在使用中,遇到了一个坑点就是,不能使用 alpine 作为基座,具体原因可以看这个 issue/1425,于是更换成了 debian-bullseye-slim 后运行良好。