【青训营】Node.js实战

服务开发

Node.js适用场景

通用场景

  • 通用的http/rpc接口
  • 通用的定时任务/队列消费任务
  • 能够胜任的大部分的业务逻辑

优势场景

  • BFF前端“胶水层”
  • SSR服务端页面渲染
  • 同构的Web应用
  • 实时通讯服务(WebSocket)

Node.js能够胜任大部分与前端密切相关的任务,而且优势场景也是非常多,现在社交和直播的websocket底层就是利用Node.js提供的io库

那么我们怎么在复杂的业务中保障稳定性和性能呢?

稳定性与性能

稳定性的统计指标:SLA,指标分以下三种

  • 3个9:99.9% = 8760 * 0.1% = 8.72小时
  • 4个9:99.99% = 8760 * 0.01% = 0.872小时 = 52.6分钟
  • 5个9:99.999% = 8760 * 0.001% = 0.0872小时 = 5.26分钟

就是说一年的时间内,服务器宕机的时间有以上三个标准,一年宕机8.27个小时和52.6分钟和5.26分钟。我们知道,宕机就是抛锚,什么业务都无法展开,所以宕机时间越短越好,5个9的标准就是最高标准

性能的统计指标

  • 响应时间RTT(发起请求到接收到请求的时间)
  • 单位时间处理能力QRS/TPS(当前服务每秒能处理的最大请求或者最大事务的数量)
  • 并发数Coucurrency(当前服务同时能处理多少请求)
  • 错误率Error Rate(判断status code非200的数量,或者统计catch的数量)

资源的统计指标

  • CPU Load (cpu占用率)
  • Memory Usage(内存的使用量)
  • FD Count(文件描述符的用量)
  • Disk Read/Write(磁盘io读写)
  • Network Send/Recv(网络io的吞吐)

稳定性保障

在开发阶段包装服务稳定性

异常捕获

如果我的代码中有代码的粗心或者其他原因,引入第三方库没有禁止,可以使用异常捕获,拿到错误的原因和日志,帮助我们解决问题

process
    .on('unhandleRejection',(reson)=>{
        //send error logs
        //...
    })
    .on('uncaughException',err=>{
        //send error logs
        //...
        process.exit(1)
    })
复制代码
自动重启

基于class的模块下,在子进程退出捕获后,由父进程自动fork重启这个子进程,可以保证在多进程的架构下,单进程挂掉可以迅速重启一个,避免宕机

// in cluster mode
cluster.on('exit',function(worker,code,signal){
    if(worker.suicide){
        //send error logs
        cluster.fork()
    }
})
复制代码
健康检查

在代码中实现健康检查,隔一段时间通过soket链接一下服务是否还是可用的,及时发现进程在运行中的异常

//check whether the port is available
function connectToport(
     port:number,
     host?:string,
     timeout?:number
):Promise<void>{
     return new Promise((resolve,reject)=>{
         const socket = new net.Socket()
         const onError = (message:string)=>{
             socket.destroy(),
             reject(message)
         }
     })
     
     socket.setTimeout(timeout ?? 1000)
     socket.once("error",(e)=> onError(e.message))
     socket.once("timeout",()=> onError("TIMEOUT"))
     socket.connect({ port, host },()=>{
         socket.end();
         resolve()
     })
 }
复制代码

性能保障

Node.js指标统计工具

利用Node.js API获取指标

CPU Usage

  • user cpu time (用户CPU时间)
  • system cpu time(系统CPU时间)

Memory Usage

  • heapTotal/heapUsed(V8heap占用的内存数)
  • external(V8堆外内存占用数)
  • array buffers
  • rss

IO Usage

  • fsRead/fsWrite
  • ipcSent/ipcReceived
const {
    cpuUsage,
    memoryUsage,
    resourceUsage,
} = require('process')
​
console.log(cpuUsage())
//{ user:38579,system:6986}console.log(memoryUsage())
//Prints:{
//  rss:4935680,
//  heapTotal:1826816,
//  heapUsed:650472,
//  external:48979,
//  arrayBuffers:9386
//}
复制代码

通过系统指令获取服务容器指标

Linux的任务管理器,当前容器的一系列指标

image-20210908202029996.png
可以看到当前某个进程打开的所有的文件描述符

image-20210908202040607.png

image-20210908202050593.png

image-20210908202059334.png

数据收集&&可视化

在服务端或容器镜像内封装通用的实时上报指标Metrics,收集数据并通过可视化看板展示,有助于我们时刻掌握当前服务的状态

Grafana埋点看板示例

image-20210908202250366.png

发布部署

早期部署方案

  • 购买/租用服务器/公网IP域名

  • 安装操作系统、搭建内网环境以及一系列基础设施工具

  • 通过FTP/RSYNC等手段上传生产环境代码包

  • 在对应路径下执行启动命令

    • 早期通过nohub实现以daemon方式运行

      nohub `NODE_ENV = production node test.js` 2>&1&
      复制代码
    • 通过pm2/框架相关启动脚本运行

      pm2 start app.js -i max
      复制代码
  • 购买域名,配置DNS,以及反向代理到服务下

所有的一切都是手工运行,对开发人员非常不友好

IaaS/Paas/FaaS的发展路线

IaaS:云计算的初级阶段

基础设置即服务(英语:Infrastructure as a Service,简称IssS)是云服务厂商提供消费者处理、储存、网络以及各种基础运算资源,以部署与执行操作系统或应用程序等各种软件

IaaS是云服务的最底层,主要提供一些基础资源。用户无须购买服务器、软件等网络设备,即可任意部署和运行处理、存储、网络和其他基本的计算资源,不能控管或控制底层的基础设施,但是可以控制操作系统、存储装置、已部署的应用程序

虚拟主机/VPS代表产品
  • AWS EC2
  • Aliyun ECS
  • 腾讯云 云服务器
技术实现
  • 虚拟机KVM/OpenVZ/Hyper-V
  • OpenStack
  • Docker

PaaS:主流的应用托管发布形态

平台即服务(Platform as a Service)是一种云计算服务,提供运算平台与解决方案

PaaS提供软件部署平台(runtime),抽象掉了硬件和操作系统的细节,可以无缝的扩展(scaling)。开发者只需要关注自己的业务逻辑,不需要关注底层

PaaS代表产品
  • Google AppEngine
  • Heroku
  • AWS Elastic Beanstalk
  • Vercel
技术实现
  • Docker容器部署应用/Docker Swarm

  • Kubernetes

    • 服务编排
    • 弹性扩缩容
    • ……
基于PaaS的发布流程

大多数PaaS平台都提供了Node.js服务的运行和支持

我们根据PaaS平台提供的Node.js Runtime规范给应用编写

构建脚本(npm install)、启动脚本(npm start)以及应用配置脚本(app.yml)

以Vercel为例,我们可以绑定Git Repository后直接发布

image-20210908203941706.png
基础Runtime不满足应用要求时,PaaS平台也支持通过Dockerfile定制特殊的功能以及启动命令

以Heroku为例,支持通过CLI的方式发布一个Container

image-20210908204054830.png

PaaS与DevOps

DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。通过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更快的快捷、频繁和可靠

image-20210908204323570.png

现代PaaS平台提供了基础的DevOps流程,通过绑定Git Branch实现自动化集成发布到Perview环境,极大地简化了发布测试上线的流程

PaaS与自动扩缩容

得益于Kubernetes的能力,以及通用的PaaS应用Runtime,现代PaaS服务支持定义实例的性能,并支持在请求激增、CPU/内存吃紧时快速扩容实例,以及在请求量小、资源充裕是缩容,从而减少运维成本和服务费用

当然,快速扩缩容的前提是在Runtime侧已经实现了性能指标监控与数据上报的能力

image-20210908204710779.png

serverless理念和产品

“Serverless computing is a cloud computing execution model in which the cloud provider allocates machine resources on demand, taking care of the servers on behalf of the customers”

image-20210908204900434.png

Serverless最佳形态:FaaS + BaaS

  • FaaS(Lambda):“函数即服务”,是一种在无状态容器中运行的事件驱动型计算执行模型,这些函数将利用服务来管理服务器端逻辑和转态。它允许开发人员以功能来构建、运行和管理这些应用包,无须维护自己的基础架构。

    • AWS Lambda
    • Google Cloud Function
    • Aliyun函数计算FC
    • 腾讯云函数
  • BaaS:后端即服务,使开发人员可以专注于应用程序的前端,无需构建或维护后端服务即可利用

    • Google Firebase
FaaS实现(Lambda)的局限性

FaaS按量计费的原则在传统容器部署的技术方案下难实现

  • 容器冷启动+服务启动需要秒级的事件
  • 常驻实例+待命模式可以保证首次访问的效率,则按量计费要求无法满足

Node.js基于VM模块实现“高密度部署”

  • 函数之间的隔离性
  • 死循环的恢复
const vm = require('vm')
​
const runFunction = async (code)=>{
    const result = await new Promise((resolve,reject)=>{
        const sandbox = { require,console }
        try {
            timer = setTimeout(()=>{
                reject(new Error('Execute function time out'))
            },1000)
            vm.createContext(sandbox)
            const data = vm.runInNewContext(code,sandbox)
            resolve(data)
        }catch (error){
            reject(error)
        }
    }).catch((err)=>{
        return err instanceof Error ? err : New Error(err.stack)
    })
    
    if(timet){
        clearTimeout(timer)
        timer = null
    }
    return result
}
复制代码

另辟蹊径:WASM/V8 Worker替代Node.js

  • Deno Deploy
  • Cloudflare Workers
  • Wasm Edge
FaaS vs PaaS

FaaS 与PaaS在开发体验上的对比

  • 函数模型过于简单
  • 编写多个云函数对工程化不友好
  • 应用开发者更希望编写/发布一个完成的Node.js WebApp

Jamstack模式与Vercel的探索:将PaaS应用构建成若干FaaS函数后发布

image-20210908210329289.png

监控运维

日志埋点与监控报警

  • 日志

    • process.stdout / process.stderr
    • send through udp socket
  • 埋点报警

    • Metrics
    • Span
    • Trace

image-20210908210537329.png

线上排查问题

image-20210908210612691.png

这段生产服务前,请务必拉出集群,以免影响外部用户

Node.js Inspector

Node.js提供了Inspector模块,支持对运行中的服务开启调试

const inspector  = require('inspector')
​
inspector.open()
​
console.log(inspector.url())
复制代码

Inspector还支持在运行时打HeapSnapshot/CPUProfile,排查CPU/内存问题

const inspector  = require('inspector')
const fs = require('fs')
const session = new inspector.Session()
​
const fd = fs.openSync('profile.heapsnapshot','w')
​
session.connect()
​
session.on('HeapProfiler.addHeapSnapshotChunk',(m)=>{
    fs.writeSync(fs,m.params.chunk)
})
​
session.post('HeapProfiler.takeHeapSnapshot',null,(err,r)=>{
    console.log('HeapProfiler.takeHeapSnapshot done:',err,r)
    session.disconnect()
    fs.closeSync(fd)
})
复制代码

strce与tcpdump:更通用的系统诊断工具

image-20210908211352686.png

  • tcpdump可以捕获实际网络传输中的数据,对于Web Server的开发场景来说十分有用
  • strace则可以明确输出applications和kernel之间的每一个syscall的参数及返回结果,是了解系统调用的万用工具

strace:查看syscall的利器

编写一个http server,启动后通过strace -p查看并分析系统调用

const http = require('http')
//输出pid方便后面strace使用
console.log(process.pid)
​
const server = http.createServer((req,res)=>{
    res.end('hello')
})
​
server.listen(()=>{
    const {port} = server.address()
    
    setInterval(()=>{
        const req = http.get(`http://127.0.0.1:${port}`,(res)=>{
            res.on('data',()=>{})
        })
        req.end('hello')
    },1000)
})
复制代码

image-20210908212106945.png

tcpdump:通用抓包工具

tcpdump作为跨平台抓包工具,可以让我们看到在网络设备中传输的每一个请求包。并且在Windows/Mac/Linux上使用

常用筛选命令

  • host/net用于指定请求host/ip
  • port用于指定请求的端口
  • dst和src用于指定规则是用于指定一个packet的来源还是去向
  • and和or逻辑上的与、或关系,用户组合起多组筛选规则

image-20210908212407390.png

tcpdump:使用Wireshark查看抓包结果

tcpdump支持将抓包的报文写入文件,并通过Wireshark查看结果

image-20210908212510435.png

image-20210908212520121.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享