Puppeteer+RabbitMQ:Node.js 批量加工pdf服务架构设计与落地

news2024/10/6 20:32:21
  • 学情分析:根据学生阶段性的学习和考试情况进行学情分析、归纳、总结,汇总学情数据;
  • 精准推荐:推荐算法基于学情数据结合知识图谱进行精准练习题推荐;
  • 错题回顾:错题的阶段性回顾复习。
    第一部分学情分析的PDF由Node.js加工,与Java后端通过消息队列RabbitMQ进行数据交互,本文简单记录一下Node.js批量加工PDF服务的架构模式,以及基于现阶段发现的问题,梳理未来的迭代规划和演进方向。

业务特征

个册三个部分的PDF数据来源不同,生产逻辑独立由不同的服务生产,最终将三份PDF合并为一份,还要支持班级所有学生批量生产和压缩打包,所以这个功能在技术角度最主要的特征就是环节多耗时长

  • 环节多意味着在各个服务之间存在较多的网络通信和数据交互,核心挑战在于如何设计低耦合、高可用的服务架构;
  • 耗时长一方面体现在多个环节的总耗时,另一方面体现在三个PDF生产服务各自的加工耗时。
    基于以上业务特征,PDF加工服务架构设计的一个大方向就是将长耗时任务异步处理,各服务之间逻辑解耦,通过消息队列进行数据交互

技术选型

服务端生成PDF通常有两种方案:

  • 第一种是使用 pdfkit 之类的工具通过代码绘制,这种方案最大的问题是可渲染的内容类型有限,定制化不足;
  • 第二种是创建 headless browser用html渲染后截取pdf,这种方案的架构相对复杂,但是可以支持所有web端的内容类型。
    个册第一部分学情分析的某一页长这个样子:

仅这一页就涵盖文本、表格、图表以及各种自定义图案,内容类型多样并且后续迭代可能增加更多定制化内容,第一种方案的局限性很难满足需求,所以最终选定 headless browser 方案。

具体到 headless browser 的技术选型就非常有限了,可选的无非就是 Selenium/PhantomJS 这类老招牌,或者 Puppeteer/Playwright 这类新玩家。

严格来说Selenium只是一种类似按键精灵的工具,可通过代码在浏览器中模拟人的操作,本身并不是浏览器,所以需要搭配第三方浏览器使用,比如PhantomJS。
Selenium/PhantomJS 的最大的优点就是生态健全,支持多种编程语言,有相对繁荣的技术社区;缺点就是稳定性和性能较差,Selenium的稳定性出了名的糟糕,PhantomJS五年前就停止维护了。这哥俩通常用在对稳定性要求不是很高的场景,比如爬虫。

与之形成鲜明对比的,Puppeteer/Playwright 最大的优点就是稳定性高,性能更优;缺点就是对编程语言的支持有限,生态和技术社区相对没那么健全。

个册的业务特征一是对稳定性和性能要求很高;二是不要求跨浏览器(Playwright支持浏览器类型更丰富)。最终综合考虑API易用性、稳定性、性能、社区、风险等因素,在 Puppeteer 和 Playwright 之间选择了 Puppeteer。既然选定了 Puppeteer,配套的自然就是 Node.js了。

Puppeteer 和 Playwright 的对比可以参考这篇文章:Playwright vs Puppeteer: Core Differences。
这个需求是我第一次使用Puppeteer,还没完全摸透,下文涉及到Puppeteer相关的方案如果有问题,欢迎讨论指点

实现方案

智慧教育的分层架构如下:

Node.js PDF服务是本次需求新增的,为了方便分离部署和优化,PDF服务单独建立一个服务,不涉及Node.js接入层的改动。下图是个册PDF加工的完整流程:

每个环节的具体流程不细讲,Node.js PDF加工服务的细节下文详解。与Node.js PDF服务相关最关键的是与Java后端的数据交互流程。Java后端与Node.js PDF服务通过 RabbitMQ 消息队列进行数据交互,建立两个队列

队列生产者消费者说明
任务队列Java后端Node.js PDF服务Java 向队列中发送个册渲染数据,Node.js 消费
回传队列Node.js PDF服务Java后端Node.js 向队列中发送pdf加工结果数据,Java 消费

这部分没啥好讲的,Node.js与Java之间按照约定的数据规范组装数据即可,下面详细介绍一下Node.js加工pdf的具体逻辑。

这一版个册的第一部分学情分析控制在3页,早期规划的个册PDF大约25页左右,技术调研和架构设计都是基于这个预期进行的,所以现在这套模式多少有点杀鸡用牛刀的意思,不过前期打好基础给后续迭代留些空间也是好事。

单份PDF加工流程

为了更方便理解,在介绍pdf加工流程之前,有必要先简要一下Node.js PDF服务的架构,以及与PDF加工逻辑最相关的 worker角色。

Node.js PDF服务架构最核心的三个角色:

  • Scheduler:负责轮询调度,发起任务;
  • Executor:负责任务前置后置相关逻辑,包括worker pool管理、worker 调度、MQ任务队列消息拉取、MQ回传队列消息发送等;
  • Worker:负责实质执行任务,包括pdf渲染、生产、上传OSS;
    三者的关系如下所示:

Scheduler和 Executor的具体逻辑以及三个角色之间的调度逻辑下文再详解,PDF文件的实质生产逻辑都集中在 Worker中,流程如下:

图中「发送消息至MQ回传队列」实质是由 Executor执行,此处画出方便理解完整流程。

预启动

图中虚线部分的预启动是在启动 Node.js 服务之前执行的逻辑,预启动完成之后 Node.js 服务被拉起,所以预启动的耗时是一次性的。

预启动过程执行两个动作:

  • 读取磁盘中的html文件内容,写入内存,为后续环节「加载网页」提供数据;
  • 创建 Puppeteer browser 实例。

上图中只画出pdf加工逻辑相关的预启动工作,实际上预启动还包含一些其他逻辑,比如建立 MQ 连接信道。

冷启动(废弃)

虽然冷启动在后来开发过程中被废弃,但通过这个事情发现自己的不足,还是值得记录一下的。
最初之所以设想冷启动环节,是因为尝试用 worker 模拟多线程。每个worker会创建一个browser实例和多个page实例(目前是3个),如下所示:

这样做的目的是将每个worker的负载上限固定,便于服务器资源规模预估,避免服务器某个节点负载过高,进而也可以避免k8s集群pod的纵向伸缩。

k8s纵向伸缩的取舍见仁见智,我个人不太建议使用。
如果任务队列长时间为空会触发缓存清理逻辑,销毁browser和page实例以节省服务器资源,再次发起任务会触发冷****启动。冷启动执行两件事情:

  • 链接/创建browser实例
  • 创建page实例
    另外增加一个标识位_mounted代表冷启动是否完成,代码如下:
public async run(){
  if(!this._mounted){
    // 触发冷启动
    this._mount();
  }
  // ...其他逻辑
}
private async _mount(){
    if(!this._browser?.isConnected()){
      // 链接browser
      this._browser = await puppeteer.connect({
        browserWSEndpoint: this._wsEndpoint
      });
    }
    // 创建page实例
    if(isEmpty(this._pages)){
      for(let i =0;i<this._opts.maxPageCount;i++){
        const ctx = await this._browser.newPage();
        this._pages.push({
          ctx,
          busy: false
        });
      }
    }

    this._mounted = true;
}

乍看起来似乎没啥问题,但实际跑一跑代码会发现,在任务调度密集的时候,run函数短时间内被调用多次(具体的调度策略下文讲解),worker会触发多次冷启动,虽然不影响业务逻辑,但会引起服务器资源暴涨,这是因为冷启动会创建新的browser和page实例,但是旧实例并没有被清理,仍然在执行任务。
冷启动被调用多次的根本原因是Node.js不是多线程,如下图所示,假设冷启动耗时20ms,在此期间再次调用run函数,标识位_mounted还未被设置为true,就会又触发一次冷启动。

有没有解法?

当然有。多线程编程解决竞态最常用的就是:加锁。既然想模拟多线程那就彻底一点,把锁逻辑也加上呗。

worker本身是有“锁”的,每个worker有3个page实例,只有当存在空闲实例(busy为false)时run函数才可以执行,但是这个锁机制并不能避免多次冷启动问题,因为冷启动完成之前page实例还未被创建。

可能会有人说,那就加个限制,page实例不存在时也不让run函数执行不就得了?这么做的话run函数永远都不会被执行啊大聪明。

既然worker已有的锁不行,那就再加个冷启动锁,冷启动之前锁定,冷启动之后解锁。这么做当然是可以的,但是会增加逻辑复杂度,worker有两种锁,对后期迭代维护无疑是埋雷。

其实之所以有冷启动无非就是为了省点内存,用时间换空间,一个browser实例+3个空白page实例总共100m左右的内存,这年头内存这么便宜,为了省这点空间把逻辑搞那么复杂完全得不偿失。什么叫过度设计,这就是过度设计。

所以后来索性把冷启动过程干掉了,browser和page实例的创建放在worker初始化逻辑里。

public async init() {
    /**
     * 尽量禁用掉不需要的功能,提高性能
     */
    this._browser = await puppeteer.launch({
      headless: true,
      args: [
        '--incognito',
        '--disable-gpu',
        '--disable-dev-shm-usage',
        '--disable-setuid-sandbox',
        '--no-first-run',
        '--no-sandbox',
        '--no-zygote',
        '--single-process'
      ]
    });

    this._wsEndpoint = this._browser.wsEndpoint();
    // _mount函数逻辑不改动,调用_mount函数放在初始化逻辑中
    await this._mount();
}

加载网页

网页通过page.setContent(html)函数加载本地html文件,与通过page.goto(url)加载远程URL相比,既节省了部署网页的服务器资源,同时速度也更快

时间消耗执行时机性能瓶颈其他
远程URL
  • DNS耗时
  • 下载耗时
  • 解析html耗时
运行时网络IO异步下载html引用的静态资源会增加额外耗时
本地html
  • 读磁盘耗时
  • 解析html耗时
预启动阶段文件IO+常驻内存

上文提到过,本地html文件在预启动阶段提前从磁盘读取存放于内存,运行时无需实时读取。所以文件IO的耗时不算在pdf加工逻辑总耗时中,而加载远程URL只能在运行时执行,会增长pdf加工的总时长。

另外,加载的本地html文件中不能存在静态资源引用,比如js和css必须全部以行内 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/891886.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Springboot 封装整活 Mybatis 动态查询条件SQL自动组装拼接

前言 ps&#xff1a;最近在参与3100保卫战&#xff0c;战况很激烈&#xff0c;刚刚打完仗&#xff0c;来更新一下之前写了一半的博客。 该篇针对日常写查询的时候&#xff0c;那些动态条件sql 做个简单的封装&#xff0c;自动生成&#xff08;抛砖引玉&#xff0c;搞个小玩具&a…

ModStartBlog v8.0.0 博客归档页面,部分组件升级

ModStart 是一个基于 Laravel 模块化极速开发框架。模块市场拥有丰富的功能应用&#xff0c;支持后台一键快速安装&#xff0c;让开发者能快的实现业务功能开发。 系统完全开源&#xff0c;基于 Apache 2.0 开源协议。 功能特性 丰富的模块市场&#xff0c;后台一键快速安装会…

引领行业高质量发展|云畅科技参编《低代码开发平台创新发展路线图(2023)》

8月8日-9日&#xff0c;中国电子技术标准化研究院于北京顺利召开《低代码开发平台创新发展路线图&#xff08;2023&#xff09;》封闭编制会。云畅科技、浪潮、百度、广域铭岛等来自低代码开发平台解决方案供应商、用户方、科研院所等近30家相关单位的40余位专家参与了现场编制…

ArcGIS Pro技术应用(暨基础入门、制图、空间分析、影像分析、三维建模、空间统计分析与建模、python融合、案例应用)

GIS是利用电子计算机及其外部设备&#xff0c;采集、存储、分析和描述整个或部分地球表面与空间信息系统。简单地讲&#xff0c;它是在一定的地域内&#xff0c;将地理空间信息和 一些与该地域地理信息相关的属性信息结合起来&#xff0c;达到对地理和属性信息的综合管理。GIS的…

【腾讯云Cloud Studio实战训练营】用Vue+Vite快速构建完成交互式3D小故事

&#x1f440;前置了解&#xff1a;(官网 https://cloudstudio.net/) 什么是Cloud Studio&#xff1f; Cloud Studio 是基于浏览器的集成式开发环境&#xff08;IDE&#xff09;&#xff0c;为开发者提供了一个永不间断的云端工作站。用户在使用 Cloud Studio 时无需安装&#…

虚拟机怎么连接加密狗?USB Sever连接方法

公司想把软件都迁移到虚拟机&#xff0c;但是没法连接加密狗&#xff0c;怎么办&#xff1f; 让USB Sever来连接就行了&#xff01; 第一步&#xff0c; 根据加密狗的数量&#xff0c; 选一台合适的朝天椒USB Sever&#xff0c; 第二步&#xff0c; 将加密狗全部插在朝天椒U…

JMM内存模型之happens-before阐述

文章目录 一、happens-before的定义二、happens-before的规则1. 程序顺序规则&#xff1a;2. 监视器锁规则&#xff1a;3. volatile变量规则&#xff1a;4. 传递性&#xff1a;5. start()规则&#xff1a;6. join()规则&#xff1a; 一、happens-before的定义 如果一个操作hap…

C++坦克大战源代码

源码: #include <iostream> #include <time.h> #include <windows.h>#define W 1 //上 #define S 2 //下 #define A 3 //左 #define D 4 //右 #define L 5 // 坦克有4条命void HideCursor() { //隐藏光标 …

[LeetCode]两数相加

解题 思路1 : 先将每个链表的节点数求出来,将短的链表的每个节点的值加到长链表对应的节点上,在判断加上之后的值是否大于10,若大于则该节点减10,下一个节点加1,由此循环之后,只有长链表的最后一个节点的数可能大于10,则对最后一个节点进行判断,如果大于10,则new一个新的节点,将…

videojs 播放视频

背景&#xff1a;在项目中使用第三方插件videojs进行播放视频&#xff0c;点击事件更改播放的数据源。 一、视频相关理论 (一)、背景 网络流媒体的呈现形式分为两种&#xff1a;直播点播 (二)、流媒体的3种协议 分类&#xff1a;HTTPHLSRTMP定义&#xff1a;基于HTTP的流媒体…

踩坑串口通信 serialPort.RtsEnable = true

背景&#xff1a; 最近在调试一个激光模块&#xff0c;使用的是422通信&#xff0c;然后买了一个485转422的转换器。 通过串口监控软件观察&#xff0c;明明和串口助手发的东西一模一样&#xff0c;但是就是不返回&#xff01; 解决方案&#xff1a; 我加了&#xff0c;这句&…

2023年最新国内八款低代码平台盘点,国内低代码是否已经跑出独角兽?

什么是低代码平台&#xff1f;低代码平台的优势在哪里&#xff1f;低代码平台是否已经形成了自己的核心竞争力&#xff1f;低代码平台是否在国内跑出独角兽&#xff1f;本篇&#xff0c;我们将一起针对上述问题深入浅出的分析低代码平台的特点和前景&#xff0c;并且为大家盘点…

【推荐】深入浅出学习Spring框架【中】

目录 1.AOP是什么? 2.案列&#xff1a; 3.spring的aop的专业术语 4.代码模拟 4.1 前置通知 3.2.后置通知 3.3.环绕通知 3.4.异常通知 3.5.过滤通知 1.AOP是什么? 面向切面编程&#xff08;Aspect-Oriented Programming&#xff09;是一种编程范式&#xff0c;它的主要…

Python面向对象版本贪吃蛇实现

先来一波效果图吧 看看如何设计代码实现 import random import sysimport pygame# 游戏状态 ready 未开始 gameing 游戏中 end 游戏结束class Util:"""工具类&#xff1a; 提供静态方法"""staticmethoddef click_check(sprite):""&…

Java基础篇--错误处理机制

尽管人人希望自己身体健康&#xff0c;处理的事情都能顺利进行&#xff0c;但在实际生活中总会遇到各种状况&#xff0c;比如感冒发烧&#xff0c;工作时电脑蓝屏、死机等。同样&#xff0c;在程序运行的过程中&#xff0c;也会发生各种非正常状况&#xff0c;例如&#xff0c;…

Netty注解实现服务调用

在之前完成了原生服务间的简单通信&#xff0c;现在我们将它整合到Spring环境中&#xff0c;这里就以实现服务的远程调用&#xff0c;简单模拟即可&#xff0c;具体代码需要自己动手改造。 既然是服务调用&#xff0c;那我们就使用代理模式来实现。 新建代理类&#xff0c;这里…

java八股文面试[java基础]—— 重载 和 重写

Java中&#xff0c;有一个名叫方法签名的东西&#xff0c;它的定义是这样的 Definition: Two of the components of a method declaration comprise the method signature—the method’s name and the parameter types. 大概意思是&#xff0c;方法签名有两部分组成——方法…

14、缓存预热+缓存雪崩+缓存击穿+缓存穿透

缓存预热缓存雪崩缓存击穿缓存穿透 ● 缓存预热、雪崩、穿透、击穿分别是什么&#xff1f;你遇到过那几个情况&#xff1f; ● 缓存预热你是怎么做到的&#xff1f; ● 如何避免或者减少缓存雪崩&#xff1f; ● 穿透和击穿有什么区别&#xff1f;它两一个意思还是截然不同&am…

8月16日上课内容 部署LVS-DR群集

本章结构&#xff1a; 数据包流向分析: 数据包流向分析&#xff1a; &#xff08;1&#xff09;客户端发送请求到 Director Server&#xff08;负载均衡器&#xff09;&#xff0c;请求的数据报文&#xff08;源 IP 是 CIP,目标 IP 是 VIP&#xff09;到达内核空间。 &#xf…

Linux 网络发包流程

哈喽大家好&#xff0c;我是咸鱼 之前咸鱼在《Linux 网络收包流程》一文中介绍了 Linux 是如何实现网络接收数据包的 简单回顾一下&#xff1a; 数据到达网卡之后&#xff0c;网卡通过 DMA 将数据放到内存分配好的一块 ring buffer 中&#xff0c;然后触发硬中断CPU 收到硬中…