【Linux基本指令(2)】几十条指令快速入手Linux/深入理解什么是指令
本文思维导图:Tips: *号匹配。 功能:可以匹配任何东西。比如:可以看到,*号可以匹配任何东西,2.ctrl + c指令:终止因为程序或者指令异常,而导致我们无法进行指令输入。3.Linux下一切皆文件对于显示器来说,显示器文件有写方法(fwrite),有读方法(fread),但是读方法为空。对于键盘文件来说,键盘文件有读方法(fread),有写方法(fwrite),但是写方法为空。对于普通文件,则也都有正常的读写方法。7.man指令(重要):语法:man [选项] 命令功能 :man指令相当于手册,可以查询一些指令的用法比如:man ls ,查询ls指令的相关用法常用选项:-k 根据关键字搜索联机帮助 num 只在第num章节找-a 将所有章节的都显示出来,比如 man printf 它缺省从第一章开始搜索,知道就停止,用a选项,当按 下q退出,他会继续往后面搜索,直到所有章节都搜索完毕。 解释一下,面手册分为8章 1 是普通的命令 2是系统调用,如open,write之类的(通过这个,至少可以很方便的查到调用这个函数,需要加什么头文 件) 3是库函数,如printf,fread4是特殊文件,也就是/dev下的各种设备文件 5 是指文件的格式,比如passwd,就会说明这个文件中各个字段的含义 6 是给游戏留的,由各个游戏自己定义 7是附件还有一些变量,比如向environ这种全局变量在这里就有说明 8 是系统管理用的命令,这些命令只能由root使用,如ifconfig echo指令和输出重定向,追加重定向,输入重定向echo指令可以把后面的内容原封不动地显示出来。本质是echo指令向显示器中打印,所以还可以向文件中打印内容。语法: echo “内容” > 指定文件echo指令会自动换行通过echo介绍一个新概念:什么是输出重定向?比如:echo ”Hello World“ > code.cecho本来是将"Hello World"写入到显示器文件的,但是通过 > 后,重定向写入到了 code.c文件中。重定向过程中,会做两件事情:1.清空文件2.写入新内容相当于每输出重定向一次,旧的内容就会被覆盖。1.清空文件有一种简化的写法:(大于号)> + 文件名2.创建新文件可以使用 > + 文件名(输出重定向)> + 文件名而追加重定向就是,直接在文件内容的末尾继续进行写入,不清空文件。比如:echo ”Hello World“ >> code.c(变成了两个 >>)输入重定向cat < out.c将out.c文件的内容输入到cat指令中,cat指令再输入到显示器文件中。8、cp指令(重要)cp指令:语法:cp 选项 src dest (原文件和目标文件不能相同)功能:将src文件的内容拷贝到dest文件中。可以拷贝到上级目录,也可以拷贝到下级目录。使用绝对路径或则相对路径即可实现。常用选项:-f 或 --force 强行复制文件或目录, 不论目的文件或目录是否已经存在-i 或 --interactive 覆盖文件之前先询问用户-r递归处理,将指定目录下的文件与子目录一并处理。若源文件或目录的形态,不属于目录或符号链接,则一律视为普通文件处理-R 或 --recursive递归处理,将指定目录下的文件及子目录一并处理 议,转载请附上原文出处链接及本声明。9.mv指令(重要)mv指令: mv 选项 src dest功能:1. 视mv命令中第二个参数类型的不同(是目标文件还是目标目录),mv命令将文件重命名或将其移至一个新的目录中。2. 当第二个参数类型是文件时,mv命令完成文件重命名,此时,源文件只能有一个(也可以是源目录名),它 将所给的源文件或目录重命名为给定的目标文件名。3. 当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv命令将各参数指定的源文件均移至目标目录中。 注意:mv指令本质上就是剪切+重命名只不过当我们仅仅移动到上级目录时,mv把src文件/目录的名字改成了自己的名字;当我们从当前目录移动到当前目录时,直接剪切到当前目录,并将名字改成原来的名字。注意:如果将一个目录移动到上级目录,并且想将该目录重命名为新的名字,但是这个新名字在上级目录已存在,此时不再是将这个原来的目录移动到上级目录然后重命名成新名字了,而是将该目录移动到上级的新的名字的那个目录里面了。此时dir1直接别剪切到了dir11里面,而不是剪切到上级目录并且改名成dir11.即为上面所述的功能的第三点。10.cat指令语法:cat [选项][文件]功能: 查看目标文件的内容常用选项:-b 对非空输出行编号-n 对输出的所有行编号-s 不输出多行空行注意:cat 指令和 ll 指令和echo 指令是三个完全没有关系的指令。cat指令是将文件内容写入到显示器文件中ll指令是将文件属性写入到显示器文件中echo指令是把它后面的东西写入到显示器文件中11.more指令语法:more [选项][文件]功能:more命令,功能类似 cat 常用选项:-n 对输出的所有行编号q 退出more12.less指令(重要)less 工具也是对文件或其它输出进行分页显示的工具,应该说是linux正统查看文件内容的工具,功能极其强大。less 的用法比起 more 更加的有弹性。在 more 的时候,我们并没有办法向前面翻, 只能往后面看但若使用了 less 时,就可以使用 [pageup][pagedown] 等按键的功能来往前往后翻看文件,更容易用来查看一个文件的内容!除此之外,在 less 里头可以拥有更多的搜索功能,不止可以向下搜,也可以向上搜。 语法: less [参数] 文件功能:less与more类似,但使用less可以随意浏览文件,而more仅能向前移动,却不能向后移动,而且less在查看之前 不会加载整个文件。选项:-i 忽略搜索时的大小写-N 显示每行的行号/字符串:向下搜索“字符串”的功能?字符串:向上搜索“字符串”的功能n:重复前一个搜索(与 / 或 ? 有关)N:反向重复前一个搜索(与 / 或 ? 有关)q:quit 什么是管道指令?“|” 是管道,是一种符号,会被系统解释为“管道文件“。一个进程或者程序将数据内容写入到管道文件中,另一个文件进程或数据再从这个管道文件中读取出来,这个过程就是管道指令需要做的。一般管道文件先加载到内存缓存中修改后再加载回磁盘文件。到底什么是指令?1.指令的本质是可执行程序,和我们自己写的代码编译好生成的可执行程序没有本质区别。指令的本质就是一个文件——可执行程序文件,安装在系统的某种路径下罢了。总结本文讲述了几个指令和一些小贴士,后续会跟进指令(3)。
WebAssembly 的 JavaScript API
WebAssemblyWebAssembly 的 JavaScript API作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343邮箱 :291148484@163.com本文地址:https://blog.csdn.net/qq_28550263/article/details/130879208【介绍】:本文介绍 WebAssembly 的 JavaScript API。上一节:《 如何加载和运行 WebAssembly 代码 》 | 下一节:《 暂无 》目 录1. 概述1.1 引言1.2 为什么需要用于 WebAssembly 的 JavaScript API1.3 ★ 浏览器 和 NodeJS 环境加载 wasm 模块 差异1.3.1 浏览器环境下1.3.2 NodeJS 环境下2. WebAssembly JavaScript API 详解2.1 API 概述2.1.1 JavaScript 的 WebAssembly 对象是一个命名空间对象2.1.2 API 一览表普通函数用作构造器函数2.2 WebAssembly API 中的普通函数2.2.1 instantiate() 函数2.2.2 instantiateStreaming() 函数2.2.3 compile() 函数2.2.4 compileStreaming() 函数2.3 WebAssembly API 中的构造函数2.3.1 Module() 构造函数 与 Module 对象WebAssembly.Module 对象的构造函数Module 对象解析customSections 方法exports 方法imports 方法2.3.2 Global() 构造函数 与 Global 对象WebAssembly.Global 对象的构造函数Global 对象解析2.3.3 Instance() 构造函数 与 Instance 对象WebAssembly.Instance 对象的构造函数WebAssembly.Instance 对象2.3.4 Memory() 构造函数 与 Memory 对象WebAssembly.Memory 对象的构造函数WebAssembly.Memory 对象2.3.5 Table() 构造函数 与 Table 对象WebAssembly.Table 对象的构造函数WebAssembly.Table 对象2.3.6 RuntimeError() 构造函数 与 RuntimeError 对象WebAssembly.RuntimeError 对象的构造函数WebAssembly.RuntimeError 对象2.3.7 CompileError() 构造函数 与 CompileError 对象WebAssembly.CompileError 对象的构造函数WebAssembly.CompileError 对象2.3.8 LinkError() 构造函数 与 LinkError 对象WebAssembly.LinkError 对象的构造函数WebAssembly.LinkError 对象2.3.9 Exception() 构造函数 与 Exception 对象WebAssembly.Exception 对象的构造函数WebAssembly.Exception 对象实例属性 stack2.3.10 Tag() 构造函数 与 Tag 对象WebAssembly.Tag 对象的构造函数WebAssembly.Tag 对象type() 方法1. 概述1.1 引言在之前的文章中我们讲解过将某些语言(如 Rust、 C++ 等)编译成 WebAssembly ,也简单讲解过 如何加载和运行 WebAssembly 代码 。当加载了一个 .wasm 模块 之后,你就想要去使用它。WebAssembly 的 JavaScript API, 就为我们提供了这样的交互能力。 本文在前面已经对 WebAssembly 有一定的使用基础上,争取对 WebAssembly 的 JavaScript API 有全面把握,以为应用自如打好基础。1.2 为什么需要用于 WebAssembly 的 JavaScript APIWebAssembly 是一种可移植、高性能的二进制格式,可以在现代 Web 浏览器中运行。虽然 WebAssembly 可以直接由编译器生成并在浏览器中运行,但为了能够与 JavaScript 交互和利用 Web 平台的功能,WebAssembly 需要使用 JavaScript API,比如用于操作 DOM,等等。归纳起来,WebAssembly 目前仍然需要使用 JavaScript API 的主要原因有以下几点:当前,WebAssembly 还没有直接访问浏览器 API 的能力。WebAssembly 本身只是一种二进制格式,没有直接访问浏览器 DOM、网络请求、图形渲染 等能力。通过 JavaScript API,WebAssembly 可以与 JavaScript 代码进行交互,并利用 JavaScript 来访问浏览器提供的功能。JavaScript API 提供了底层数据传输和交互的机制。WebAssembly 与 JavaScript 之间需要进行数据的传递和交互。JavaScript API 提供了一系列的函数和对象,用于在 WebAssembly 模块和 JavaScript 之间传递数据、调用函数、导入导出功能等。1.3 ★ 浏览器 和 NodeJS 环境加载 wasm 模块 差异本文后续介绍各个 API 中涉及大量的代码示例,这些示例都是假定在浏览器中使用的。实际上在实际开发中,也经常应用于 NodeJS 环境,这里会有一些不同。在这一小节我们提前把这里的问题讲清楚,后面则不再对该问题进行赘述。1.3.1 浏览器环境下浏览器环境下我们使用 Fetch API 起 HTTP 请求来获取 WebAssembly 模块,因为:WebAssembly 模块 通常是作为 独立的二进制文件存在,而不是内联在 HTML 文件中。因此,需要通过网络从服务器获取模块文件。WebAssembly 模块可能具有较大的文件大小,使用 HTTP 请求可以利用浏览器的缓存机制,减少模块的重复下载。Fetch API 提供了一种方便的方式来异步获取资源。它支持 Promise 和 async/await 语法,使得处理异步操作更加简洁和可读。使用 Fetch API 发起请求可以通过设置请求头、处理错误和响应等进行更灵活的控制。总之,使用 Fetch API 发起 HTTP 请求来获取 WebAssembly 模块是为了从服务器异步获取模块文件,并能够灵活地处理请求和响应过程。这样可以实现模块的动态加载和更好的控制。1.3.2 NodeJS 环境下在 Node.js 环境中,由于没有浏览器的环境和 Fetch API,不能直接使用 fetch 函数来获取 WebAssembly 模块(除非你硬要搭建一个静态文件服务器,再来使用代码请求到模块代码来运行)。通常情况下在 Node.js 环境中我们使用 fs 模块 来读取 模块文件 并 获取其 二进制数据。具体的,在 NodeJS 中,你可以参考下面示例给出的方式 加载 WebAssembly 模块:使用 fs.readFileSync 读取 WebAssembly 模块的二进制文件,并使用 WebAssembly.compile 编译模块。const fs = require('fs');
const buffer = fs.readFileSync('module.wasm');
const module = new WebAssembly.Module(buffer);使用 WebAssembly.instantiate 或 WebAssembly.instantiateStreaming 方法直接实例化 WebAssembly 模块。const fs = require('fs');
const buffer = fs.readFileSync('module.wasm');
const module = new WebAssembly.Module(buffer);
WebAssembly.instantiate(module).then(instance => {
// ...
});2. WebAssembly JavaScript API 详解2.1 API 概述2.1.1 JavaScript 的 WebAssembly 对象是一个命名空间对象在逐个讲解前,我们一定要先注意,类似于 Math 对象 或者 Intl 对象:JavaScript 中,WebAssembly 不是一个构造函数(它不是一个函数对象),而是所有 WebAssembly 相关功能的 命名空间。——和大多数全局对象不一样。一般我们主要通过 WebAssembly 对象上提供的:WebAssembly.instantiate() 函数 加载 编译好的 加载 WebAssembly 代码;WebAssembly.Memory() 构造函数 创建新的内存实例;WebAssembly.Table() 构造函数 创建新的表实例;WebAssembly.CompileError() 构造函数 来提供 WebAssembly 中的编译错误信息;WebAssembly.LinkError() 构造函数 来提供 WebAssembly 模块实例化期间的错误信息;WebAssembly.RuntimeError() 构造函数 来提供 WebAssembly 中的运行错误信息。2.1.2 API 一览表普通函数API描述instantiate()编译和实例化 WebAssembly 代码的主要的 API,它返回一个 Module 及其第一个 Instance 实例。instantiateStreaming()直接从流式底层源编译和实例化 WebAssembly 模块,同时返回 Module 及其第一个Instance实例。compile()把 WebAssembly 二次代码编译为一个 WebAssembly.Module,不进行实例化。compileStreaming()直接从流式底层源代码编译 WebAssembly.Module,将实例化作为一个单独的步骤用作构造器函数这些函数用于通过 JavaScript 函数的 构造调用 以创建他们表示的同名对象实例,相当于基于类的面向对象编程语言中某个类的构造方法,因此用的都是大写字母开头。不了解的可以参考我的另外一篇博文 https://blog.csdn.net/qq_28550263/article/details/123418894。API描述说明Global(descriptor, value)创建一个新的 Global 对象实例。Global 对象表示一个全局变量实例,可以被 JavaScript 和 importable/exportable 访问 ,跨越一个或多个WebAssembly.Module 实例。他允许被多个 modules 动态连接。Module(bufferSource)创建一个新的 Module 对象实例。Module 对象包含已经由浏览器编译的无状态 WebAssembly 代码,可以高效地与 Worker 共享和多次实例化。Memory(memoryDescriptor)创建一个新的 Memory 对象实例。Memory 对象是一个可调整大小的ArrayBuffer或SharedArrayBuffer,用于保存 WebAssembly.Instance 访问的原始内存字节。Table(tableDescriptor)创建一个新的 Table 对象实例。CompileError(message, fileName, lineNumber)创建一个新的 CompileError 对象实例。CompileError 对象表示 WebAssembly 解码或验证期间的错误。LinkError(message, options)创建一个新的 LinkError 对象实例。LinkError 对象指示模块实例化期间的错误(除了来自start函数的 traps 之外)。RuntimeError(message, options)创建一个新的 RuntimeError 对象实例。Instance(module, importObject)创建一个新的 Instance 对象实例。Instance 对象包含所有 导出的 WebAssembly 函数,这些函数允许从 JavaScript 调用 WebAssembly 代码。Tag(type)创建一个新的 Tag 对象实例。.Tag对象定义了一种可以向 WebAssembly 代码抛出或从中抛出的 WebAssembly 异常类型。(这是一个新API)Exception(tag, payload, options)创建一个新的 Exception 对象实例。Exception对象表示从 WebAssembly 抛出到 JavaScript 或 从 JavaScript 抛出到 WebAssembly 异常处理程序的 运行时异常。(这是一个新API)2.2 WebAssembly API 中的普通函数2.2.1 instantiate() 函数该方法从已编译的 WebAssembly 模块创建一个实例。其语法格式为:WebAssembly.instantiate(bufferSource, importObject)其中参数:bufferSource:包含 WebAssembly 模块字节码的 ArrayBuffer、TypedArray 或 DataView。importObject(可选):一个对象,用于传递给 WebAssembly 模块的导入对象。例如:// 发起对 WebAssembly 模块的 HTTP 请求
const response = await fetch('module.wasm');
// 将响应转换为 ArrayBuffer
const buffer = await response.arrayBuffer();
// 使用 ArrayBuffer 创建 WebAssembly 模块实例
const module = await WebAssembly.instantiate(buffer);
// 获取 WebAssembly 实例对象
const instance = module.instance;2.2.2 instantiateStreaming() 函数该方法通过流式加载已编译的 WebAssembly 模块,并创建一个实例。其语法格式为:WebAssembly.instantiateStreaming(source, importObject)其中参数:source:一个表示可通过 HTTP 请求获取 WebAssembly 模块的 URL 字符串或 Response 对象。importObject(可选):一个对象,用于传递给 WebAssembly 模块的导入对象。例如:// 流式加载并实例化 WebAssembly 模块
const module = await WebAssembly.instantiateStreaming(fetch('module.wasm'));
// 获取 WebAssembly 实例对象
const instance = module.instance;2.2.3 compile() 函数该方法将 WebAssembly 模块的字节码编译成一个可执行的模块对象。其语法格式为:WebAssembly.compile(bufferSource)其中参数:bufferSource:包含 WebAssembly 模块字节码的 ArrayBuffer、TypedArray 或 DataView。例如:// 发起对 WebAssembly 模块的 HTTP 请求
const response = await fetch('module.wasm');
// 将响应转换为 ArrayBuffer
const buffer = await response.arrayBuffer();
// 将 ArrayBuffer 编译成可执行的 WebAssembly 模块对象
const module = await WebAssembly.compile(buffer);2.2.4 compileStreaming() 函数该方法通过流式加载 WebAssembly 模块的字节码,并将其编译成一个可执行的模块对象。其语法格式为:WebAssembly.compileStreaming(source)其中参数:source:一个表示可通过 HTTP 请求获取 WebAssembly 模块的 URL 字符串或 Response 对象。例如:const module = await WebAssembly.compileStreaming(fetch('module.wasm'));2.3 WebAssembly API 中的构造函数2.3.1 Module() 构造函数 与 Module 对象WebAssembly.Module 对象的构造函数WebAssembly.Module() 构造函数 用于创建一个 WebAssembly.Module 实例对象。它接受一个 ArrayBuffer 作为参数,并返回一个模块对象,用于后续的实例化操作。该构造函数的类型签名即下一节 Module 对象的类型签名 中的 new 方法的签名,这里不重复给出。例如:fetch('module.wasm')
.then(response => response.arrayBuffer())
.then(buffer => {
const module = new WebAssembly.Module(buffer);
// 使用模块进行实例化等操作
});Module 对象解析WebAssembly.Module 对象类型签名为:var WebAssembly.Module: {
new (bytes: BufferSource): Module;
prototype: Module;
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
exports(moduleObject: Module): ModuleExportDescriptor[];
imports(moduleObject: Module): ModuleImportDescriptor[];
}customSections 方法该方法用于获取模块中自定义的节(section)。其中参数:moduleObject 是一个 WebAssembly.Module 对象,sectionName 是一个字符串,表示要获取的自定义节的名称。该方法返回一个数组,这个数组包含了指定名称的自定义节的内容。例如:const response = await fetch('module.wasm');
// 将响应转换为 ArrayBuffer
const buffer = await response.arrayBuffer();
// 将 ArrayBuffer 编译成可执行的 WebAssembly 模块对象
const module = await WebAssembly.compile(buffer);
const customSections = WebAssembly.Module.customSections(module, 'custom_section_name');
console.log(customSections);exports 方法该方法用于获取模块中的导出项(exports)信息。其中参数:module 是一个 WebAssembly.Module 对象。该方法返回一个数组,包含了模块中所有导出项的信息。例如:const response = await fetch('module.wasm');
// 将响应转换为 ArrayBuffer
const buffer = await response.arrayBuffer();
// 将 ArrayBuffer 编译成可执行的 WebAssembly 模块对象
const module = await WebAssembly.compile(buffer);
const customSections = WebAssembly.Module.customSections(module, 'custom_section_name');
console.log(customSections);imports 方法该方法用于获取模块中的导入项(imports)信息。其中参数:module 是一个 WebAssembly.Module 对象。该方法返回一个数组,包含了模块中所有导入项的信息。例如:const response = await fetch('module.wasm');
// 将响应转换为 ArrayBuffer
const buffer = await response.arrayBuffer();
// 将 ArrayBuffer 编译成可执行的 WebAssembly 模块对象
const module = await WebAssembly.compile(buffer);
const imports = WebAssembly.Module.imports(module);
console.log(imports);2.3.2 Global() 构造函数 与 Global 对象WebAssembly.Global 对象的构造函数WebAssembly.Global() 构造函数 用于创建一个 WebAssembly.Global 实例对象。该构造函数的类型签名即下一节 Global 对象的类型签名 中的 new 方法的签名,这里不重复给出。Global 对象解析Global 对象的类型签名为:var WebAssembly.Global: {
new (descriptor: GlobalDescriptor, v?: any): Global;
prototype: Global;
}这个对象没有需要解释的其它方法。2.3.3 Instance() 构造函数 与 Instance 对象WebAssembly.Instance 对象的构造函数WebAssembly.Instance() 构造函数 用于创建一个 WebAssembly.Instance 实例对象。它接受一个 WebAssembly.Module 对象和一个可选的导入对象作为参数,并返回一个实例对象,用于调用 WebAssembly 模块中的函数和访问导出的功能。该构造函数的类型签名即下一节 Instance 对象的类型签名 中的 new 方法的签名,这里不重复给出。例如:fetch('module.wasm')
.then(response => response.arrayBuffer())
.then(buffer => {
// 创建 wasm 的 Module 对象实例
const module = new WebAssembly.Module(buffer);
// 创建 wasm 的 Instance 对象实例
const instance = new WebAssembly.Instance(module, { /* imports */ });
// 使用实例调用导出的函数等操作
});WebAssembly.Instance 对象Instance 对象的类型签名为:var WebAssembly.Instance: {
new (module: Module, importObject?: Imports): Instance;
prototype: Instance;
}这个对象没有需要解释的其它方法。2.3.4 Memory() 构造函数 与 Memory 对象WebAssembly.Memory 对象的构造函数WebAssembly.Memory() 构造函数用于创建一个 WebAssembly.Memory 实例对象。它接受一个描述内存大小的页数作为参数,并返回一个内存对象,用于 WebAssembly 模块中的内存访问操作。该构造函数的类型签名即下一节 Memory 对象的类型签名 中的 new 方法的签名,这里不重复给出。例如:const memory = new WebAssembly.Memory({ initial: 10, maximumMemory 实例对象的 buffer 属性是一个可调整大小的 ArrayBuffer ,其内存储的是 WebAssembly 实例 所访问内存的原始字节码。WebAssembly.Memory 对象Memory 对象的类型签名为:var WebAssembly.Memory: {
new (descriptor: MemoryDescriptor): Memory;
prototype: Memory;
}这个对象没有需要解释的其它方法。2.3.5 Table() 构造函数 与 Table 对象WebAssembly.Table 对象的构造函数WebAssembly.Table() 构造函数用于创建一个 WebAssembly.Table 实例对象。Table() 构造函数主要传入的是以下两个参数:initial:指定表的初始大小(以元素数量表示),默认为 0。element:指定表中每个元素的类型(函数引用类型),默认为 {element: “anyfunc”}。可以参考该对象类型签名及进行理解。该构造函数的类型签名即下一节 Table 对象的类型签名 中的 new 方法的签名,这里不重复给出。例如:// 创建一个初始大小为 10 的 WebAssembly 表,存储任意函数引用类型
const table = new WebAssembly.Table({ initial: 10 });
// 向表中添加函数引用
table.grow(2); // 扩展表的大小为 12
table.set(0, myFunction); // 将 myFunction 设置为索引为 0 的元素
table.set(1, anotherFunction); // 将 anotherFunction 设置为索引为 1 的元素
// 调用表中的函数
table.get(0)(); // 调用索引为 0 的函数WebAssembly.Table 对象Table 对象的类型签名为:type TableKind = "anyfunc" | "externref";
interface TableDescriptor {
element: TableKind;
initial: number;
maximum?: number;
}
var WebAssembly.Table: {
new (descriptor: TableDescriptor, value?: any): Table;
prototype: Table;
}TableKind 类型表示一个对象,表示表中每个元素的类型,可以是以下值之一:“anyfunc”: 任意函数引用类型“funcref”: 函数引用类型这个对象没有需要解释的其它方法。2.3.6 RuntimeError() 构造函数 与 RuntimeError 对象WebAssembly.RuntimeError 对象的构造函数WebAssembly.RuntimeError() 构造函数用于创建一个 WebAssembly.RuntimeError 实例对象。通过构造调用该对象,可以创建 WebAssembly 运行时错误(Runtime Error)。其中参数:message:一个可选的字符串,表示错误消息。例如:// 创建一个自定义的 WebAssembly 运行时错误
const error = new WebAssembly.RuntimeError("Custom runtime error occurred.");
throw error; // 抛出错误WebAssembly.RuntimeError 对象RuntimeError 对象用于在 WebAssembly 模块执行期间抛出自定义的运行时错误。通过 抛出自定义的 WebAssembly.RuntimeError,你可以在 WebAssembly 模块 的 执行期间捕获并处理特定的运行时错误情况。该对象的类型签名为:var WebAssembly.RuntimeError: {
(message?: string): RuntimeError;
new (message?: string): RuntimeError;
prototype: RuntimeError;
}通过抛出自定义的 WebAssembly.RuntimeError,你可以在 WebAssembly 模块的执行期间 捕获并处理特定的运行时错误情况。【注】:WebAssembly.RuntimeError 是一个构造函数,用于创建错误对象。它不是 JavaScript 中内置的异常类型,而是用于在 WebAssembly 环境中表示运行时错误的特定对象。这个对象没有需要解释的其它方法。2.3.7 CompileError() 构造函数 与 CompileError 对象WebAssembly.CompileError 对象的构造函数WebAssembly.CompileError() 构造函数 用来创建一个 WebAssembly.CompileError 实例对象,以实现在编译 WebAssembly 模块时抛出自定义的编译错误。其中参数:message:一个可选的字符串,表示错误消息。例如:// 创建一个自定义的 WebAssembly 编译错误
const error = new WebAssembly.CompileError("Custom compile error occurred.");
throw error; // 抛出错误WebAssembly.CompileError 对象CompileError 对象的类型签名为:var WebAssembly.CompileError: {
(message?: string): CompileError;
new (message?: string): CompileError;
prototype: CompileError;
}通过抛出 自定义的 WebAssembly.CompileError,你可以在编译 WebAssembly 模块时捕获并处理特定的编译错误情况。【注】:WebAssembly.CompileError不是 JavaScript 中内置的异常类型,而是用于在 WebAssembly 环境中表示编译错误的特定对象。2.3.8 LinkError() 构造函数 与 LinkError 对象WebAssembly.LinkError 对象的构造函数WebAssembly.LinkError() 构造函数 用于创建一个 WebAssembly.LinkError 实例对象,从而在链接 WebAssembly 模块时抛出自定义的链接错误。其中参数:message:一个可选的字符串,表示错误消息。例如:// 创建一个自定义的 WebAssembly 链接错误
const error = new WebAssembly.LinkError("Custom link error occurred.");
throw error; // 抛出错误WebAssembly.LinkError 对象LinkError 对象的类型签名为:var WebAssembly.LinkError: {
(message?: string): LinkError;
new (message?: string): LinkError;
prototype: LinkError;
}通过抛出自定义的 WebAssembly.LinkError,你可以在 链接 WebAssembly 模块时 捕获并处理特定的链接错误情况。【注】:WebAssembly.LinkError 是一个构造函数,用于创建错误对象。它不是 JavaScript 中内置的异常类型,而是用于在 WebAssembly 环境中表示链接错误的特定对象。这个对象没有需要解释的其它方法。2.3.9 Exception() 构造函数 与 Exception 对象WebAssembly 技术仍在不断发展和演进中,新的功能和规范也可能随着时间推移而推出。目前谷歌 Chrome 浏览器等浏览器已经支持该对象,不过不能确保所有就有的浏览器都被部分支持。笔者尚未实际使用过,因此先介绍过来,待日后获取更多信息再对本小节内容进行调整。该对象的 W3C 文档位置为:https://webassembly.github.io/exception-handling/js-api/#runtime-exceptionsWebAssembly.Exception 对象的构造函数WebAssembly.Exception() 构造函数 用于创建一个 WebAssembly.Exception 实例对象 其语法格式为:new Exception(tag, payload)
new Exception(tag, payload, options)其中:tag:用于定义 WebAssembly.Tag 中每个值的预期数据类型payload。payload:包含异常负载的一个或多个数据字段的数组。元素必须匹配 中相应元素的数据类型 tag。如果有效负载中的数据字段数量与其类型不匹配,TypeError 则会抛出异常。options(可选):具有以下可选字段的对象:traceStack:如果 Exception 可能有一个堆栈跟踪附加到它的 stack 属性,则为 true,否则为 false。默认情况下,这是 false (如果未提供 options 或 options.traceStack)。WebAssembly.Exception 对象它表示表示从 WebAssembly 抛出到 JavaScript 或从 JavaScript 抛出到 WebAssembly 异常处理程序的运行时异常。Exception 访问抛出的异常的参数需要用于创建 的同一 Tag。提供了方法来测试异常是否与特定标记匹配,以及通过索引获取特定值(如果异常与指定标记匹配)。当共享关联标签时,JavaScript 和其他客户端代码只能访问 WebAssembly 异常值,反之亦然(您不能只使用恰好定义相同数据类型的另一个标签)。如果没有匹配的标记,异常可以被捕获并重新抛出,但它们无法被检查。用法例如:// 创建标记并使用它来创建异常
const tag = new WebAssembly.Tag({ parameters: ["i32", "f32"] });
const exception = new WebAssembly.Exception(tag, [42, 42.3]);实例属性 stack返回异常的堆栈跟踪,或 undefined。WebAssembly.Exception.prototype.stack非标准:此功能是非标准的,不在标准轨道上。不要在面向 Web 的生产站点上使用它:它不适用于每个用户。实现之间也可能存在很大的不兼容性,并且行为可能会在未来发生变化。2.3.10 Tag() 构造函数 与 Tag 对象WebAssembly 技术仍在不断发展和演进中,新的功能和规范也可能随着时间推移而推出。目前谷歌 Chrome 浏览器等浏览器已经支持该对象,不过不能确保所有就有的浏览器都被部分支持。笔者尚未实际使用过,因此先介绍过来,待日后获取更多信息再对本小节内容进行调整。该对象的 W3C 文档位置为:https://webassembly.github.io/exception-handling/js-api/#dom-tag-tagWebAssembly.Tag 对象的构造函数WebAssembly.Tag() 构造函数 用于创建一个 WebAssembly.Tag 实例对象 其语法格式为:new WebAssembly.Tag(type)WebAssembly.Tag 对象type() 方法该方法返回为标签定义数据类型数组的对象(在其构造函数中设置)。例如:// 该例子为 MDN 给出,但是目前笔者测试的浏览器似乎并不支持。
const tagToImport = new WebAssembly.Tag({ parameters: ["i32", "f32"] });
console.log(tag.type());
// Console output:
// Object { parameters: (2) […] }
// parameters: Array [ "i32", "i64" ]
// <prototype>: Object { … }
Python 元编程
一个高级 Python 知识点是元编程(Metaprogramming),它指的是编写代码来操作程序本身(例如,动态修改代码、添加新的类或函数等)。元编程可以通过以下方法实现:
使用元类(Metaclasses):元类是用于创建类的类。在 Python 中,元类是派生自 type 的类,并定义了 __new__ 和 __init__ 两个方法。使用元类可以动态创建类、添加特定功能以及自定义类的行为。
使用装饰器(Decorators):装饰器是一种用于修改函数或类的行为的语法结构。装饰器可以用于创建缓存、跟踪执行时间、执行前后的处理等。装饰器也可以增加或删除函数的参数或者修改函数的返回值等。
动态创建类和函数:Python 是一种动态语言,可以动态创建类或函数。通过使用 type() 函数,可以在运行时创建一个新的类。通过使用 exec() 函数或者 eval() 函数,可以在运行时动态创建函数,这可以在构建 DSL 时非常有用。
使用协议(Protocols):协议指的是一套规范,用于描述对象应该具有的属性或者方法。在 Python 中,协议是一种非正式的接口,它定义了哪些方法或属性需要以特定方式实现。通过使用协议,可以实现多态(Polymorphism)或者混入(Mixin)的设计模式。
元编程在 Python 中被广泛使用,在许多框架和库中都有应用。例如,Django 中的 ORM 使用元类来生成 SQL 查询;Flask 中的装饰器用于定义路由;PyTorch 中的动态图机制使用了 Python 的动态创建类和函数等技术。
缓存数据一致性探究
缓存的查询:先查询缓存,如果查询失败,那么去查询DB,之后重建缓存,基本上不存在异议。缓存的更新:先更新DB还是先更新缓存?是更新缓存还是删除缓存?在常规情况下,怎么操作都可以,但一旦面对高并发场景,就值得细细思量了。1、先更新数据库再更新缓存线程A:更新数据库(第1s)——> 更新缓存(第10s)线程B:更新数据库 (第3s)——> 更新缓存(第5s)并发场景下,这样的情况是很容易出现的,每个线程的操作先后顺序不同,这样就导致请求B的缓存值被请求A给覆盖了,数据库中是线程B的新值,缓存中是线程A的旧值,并且会一直这么脏下去知道缓存失效(如果你设置了过期时间的话)。2、先更新缓存再更新数据库线程A:更新缓存(第1s)——> 更新数据库(第10s)线程B:更新缓存(第3s)——> 更新数据库(第5s)和前面一种情况相反,缓存中是线程B的新值,而数据库中是线程A的旧值。前两种方式之所以会在并发场景下出现异常,本质上是因为更新缓存和更新数据库是两个操作,我们没有办法控制并发场景下两个操作之间先后顺序,也就是先开始操作的线程先完成自己的工作。如果把它化简,更新时只更新数据库,同时删除缓存。等待下一次查询时命中不到缓存,再去重建缓存,是不是就解决了这个问题?基于此,后面的两种方案应运而生。3、先删除缓存再更新数据库通过这种方式,我们很惊喜地发现,前面困扰我们的并发写场景的问题确实被解决了!两个线程都只修改数据库,不管谁先,数据库以之后修改的线程为准。但这个时候,我们来思考另一个场景:两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的。很显然,这种状况也不是我们想要的。延时双删在这种方案下,拓展出了延时双删的解决手段。删除缓存更新数据库睡眠一段时间再次删除缓存加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。但是具体睡眠多久其实是个玄学,很难评估出来,所以这个方案也只是尽可能保证一致性而已,极端情况下,依然也会出现缓存不一致的现象。因此,还是不太建议这种方案。4、先更新数据库再删除缓存(cache aside)这种方式,再方案3的基础上,又将二者的顺序进行了调换。我们再把前面的场景在这种方案下进行验证:一个是查询操作,一个是更新操作的并发,我们先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会方案3一样,后续的查询操作一直在取老的数据。而这,也正是缓存使用的标准的design pattern,也就是cache aside。包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。那么,是否这种方案就是万无一失的完美策略呢?其实也并不然,再来看看这种场景:一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。但是这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。所以,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间,这样,即使数据出现了不一致,也能在一段时间之后失效,更新上一致的数据。操作失败上面虽然列举了不少较为复杂的并发场景,但实际上还是理想情况:即,对数据库和缓存的操作都是成功的。然而在实际生产中,由于网络抖动、服务下线等等原因,操作是有可能失败的。举例说明:应用要把数据 X 的值从 1 更新为 2,先成功更新了数据库,然后在 Redis 缓存中删除 X 的缓存,但是这个操作却失败了,这个时候数据库中 X 的新值为 2,Redis 中的 X 的缓存值为 1,出现了数据库和缓存数据不一致的问题。那么,后续有访问数据 X 的请求,会先在 Redis 中查询,因为缓存并没有 诶删除,所以会缓存命中,但是读到的却是旧值 1。其实不管是先操作数据库,还是先操作缓存,只要第二个操作失败都会出现数据一致的问题。问题原因知道了,该怎么解决呢?有两种方法:重试机制。订阅 MySQL binlog,再操作缓存。重试机制我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。举个例子,来说明重试机制的过程。订阅 MySQL binlog,再操作缓存「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。下图是 Canal 的工作原理:所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。总结1、cache aside并非万能虽然说catch aside可以被称之为缓存使用的最佳实践,但与此同时,它引入了缓存的命中率降低的问题,(每次都删除缓存自然导致更不容易命中了),因此它更适用于对缓存命中率要求并不是特别高的场景。如果要求较高的缓存命中率,依然需要采用更新数据库后同时更新缓存的方案。2、缓存数据不一致的解决方案前面已经说了,在更新数据库后同时更新缓存,会在并发写的场景下出现数据不一致,那我们该怎么规避呢?方案也有两种。引入分布式锁,在更新缓存之前尝试获取锁,如果已经被占用就先阻塞住线程,等待其他线程释放锁后再尝试更新。但这会影响并发操作的性能。设置较短缓存时间设置较短的缓存过期时间能够使得数据不一致问题存在的时间也比较长,对业务的影响相对较小。但是于此同时,其实这也使得缓存命中率降低,又回到了前面的问题里...所以,综上所述,没有永恒的最佳方案,只有不同业务场景下的方案取舍。行文至此,不由得默念一声:“There is no silver bullet!”,并再次为《人月神话》作者的精准洞见而感叹。
Python中的装饰器
Python中的装饰器(Decorator)是一种高级语法,它可以用来动态地修改类或函数的行为。装饰器本质上是一个函数,它可以接受一个函数作为输入,并返回一个新的函数。通过装饰器,我们可以在不改变被装饰函数或类原有代码的前提下,对其进行扩展、修改或增强,使其具有更强的功能和灵活性。装饰器的使用非常广泛,比如可以使用装饰器来实现函数的缓存、输入参数的检查、日志的记录等。例如下面这个例子是一个带参数的装饰器:def decorator_with_args(arg1, arg2): def actual_decorator(func): def wrapper(*args, **kwargs): print("Decorator arguments:", arg1, arg2) return func(*args, **kwargs) return wrapper return actual_decorator@decorator_with_args("Hello", "World")def my_function(x, y): print("Function arguments:", x, y)my_function(1, 2)# Output:# Decorator arguments: Hello World# Function arguments: 1 2在这个例子中,decorator_with_args是一个带参数的装饰器,它接受两个参数arg1和arg2,并且返回一个新的函数actual_decorator。这个函数接受一个被装饰的函数func,并返回一个新的函数wrapper,wrapper函数负责将装饰器的功能应用到原来的函数上。通过这个装饰器,我们可以在调用my_function之前,输出一些Decorator arguments的信息。
Kepp-alive的实际运用场景(1)
kepp-alive简单介绍:将组件缓存,不更新数据,被kepp-alive包裹的路由的组件的钩子函数不会生效。运用场景: 假设我们有这样的一个功能需要实现,我们从主页进入到订单列表页,在从订单列表页进入到订单详情页,然后当我们从详情页返回到列表页时,我们的列表页不刷新,继续保持之前的数据。而从其他页面进入到订单列表页时,则照常刷新列表页的数据。我们给我们的订单页加入路由元信息,这个信息用来判断是否对该路由进行缓存在我们的设置项目框架的组件里,通过获取meta的值来判断是否缓存。当我们实现到这一步时,我么从详情页退回到订单列表页时,列表页不会刷新,但是,这里会有一个问题,当我们从其他页面进入到订单列表页时,列表页页没有刷新了,这里肯定是不符合需求的,所以这里我们得继续完善修改。 大概的完善逻辑是,我们要区分,在当前的订单列表页里,我们判断我们进入的是不是详情页,如果是详情页,那好,让订单页继续缓存,如果我们从订单页跳转的不是详情页,这时候,我们将订单页的meta里验证要不要缓存的信息修改为false 我们在订单页的组件配置数据里,加入路由后置守卫,通过守卫来判断我们从订单页面进入的是不是详情页。 到这里就已经算是实现了大部分的逻辑了,但是,这其中还有bug,下章节在讲
数据库内核那些事|深度解析PolarDB DDL锁的优化和演进
01-概述在日常数据库操作中,用户总是谈DDL色变,原因在于总是担心DDL的执行会影响业务SQL,这里面最核心的因素在于DDL持有的MDL表锁导致的锁堵塞问题。另一方面,由于DDL类型众多,用户难以区分不同类型DDL的锁行为,无法判断执行DDL可能导致的后果,这进一步加剧了该问题的复杂度。通过多年大量线上实例的经验积累, 我们非常理解用户在面对这类MDL锁问题时的困惑。本文整理总结了云原生数据库PolarDB MySQL内核团队在全链路MDL锁治理方面的经验和进展,鞭策我们为“DDL无锁”、为用户可以毫无担忧地执行DDL而持续努力。针对MDL锁的背景知识,我们有持续的内核月报在介绍相关原理,感兴趣的读者可以自行查询《常用SQL语句的MDL加锁及源码分析》[1]和《MDL锁实现分析》[2]。在开始全文前,我们首先回顾用户主要关注哪些方面的DDL锁问题:1.什么时候拿锁很不幸的是,无论是MySQL内核原生的DDL,还是各种第三方插件(gh-ost、pt-osc,以及云厂商们的“无锁变更”),几乎所有的DDL都会申请表级别的MDL互斥锁。这里的核心原因在于:DDL的目标是表结构/表定义变更,它必然会修改元数据/字典信息,因此DDL依赖MDL锁来完成元信息、文件操作和相应缓存信息的正确更新。当DDL修改元数据时,它申请表级别的MDL互斥锁,从而堵塞并发的元数据查询/修改操作,继而可以线程安全地更新元信息缓存,从而保证所有线程用正确版本的元数据解析对应版本的表数据。说到这里,很多熟悉MySQL的读者一定会问,那为什么gh-ost等第三方插件在做DDL时似乎呈现出一种类似“无锁”的表现呢?其实这里的核心差别在于,MySQL内核和第三方插件,在处理“拿不到锁”这个问题时采用了完全不一样的策略。2.拿不到锁会导致什么问题(雪崩vs饥饿,本文关注的核心问题)相比于第三方插件,MySQL内核的MDL拿锁机制简单粗暴:当DDL申请MDL-X(互斥锁)时,如果目标表存在未提交的长事务或大查询,DDL将持续等待获取MDL-X锁。由于MDL-X锁具有最高的优先级,DDL在等待MDL-X锁的过程中将阻塞目标表上所有的新事务,这将导致业务连接的堆积和阻塞,继而可能带来整个业务系统「雪崩」的严重后果。为了避免这个问题,MySQL社区开发了很多外部工具,比如pt-osc和github的gh-ost。它们均采用拷表方式实现,即创建一个空的新表,通过select + insert的方式拷贝存量数据,然后通过触发器或者Binlog的方式拷贝增量数据,最后通过rename操作切换新表和旧表。云厂商的各种工具,例如DMS的无锁变更也与这些外部工具原理类似。但很遗憾,这种方式也存在明显的劣势:1. 可能由于大事务/大查询的存在,DDL持续拿不到锁,持续等待直到反复失败(「饥饿」);2. 不管是Instant DDL(例如秒级加列),还是仅增加二级索引,第三方工具都无脑选择了全表重建的方式,通过大幅牺牲性能来追求稳定性。我们之前的测试表明(月报链接[3]),相比于内核原生的DDL执行方式(INSTANT / INPLACE / COPY),gh-ost有着10倍甚至几个数量级的性能下降,这在数据量快速增长的今天是完全无法忍受的。不管是第三方插件,还是MySQL内核,很遗憾,任何一种方式都不能在所有场景里都达到最优。PolarDB MySQL内核团队尝试在保留最佳性能的前提下,同时解决雪崩和饥饿这两个问题。3.拿到锁又会导致什么问题(持有锁的时间,Fast DDL将在后续文章中介绍)在解决了「拿不到锁」的问题后,我们同样要解决「拿到锁后」会有什么问题,即如果互斥锁持有时间过久,同样会导致业务堆积雪崩等问题。熟悉MySQL的用户都知道,MySQL有三种DDL类型,分别是「INSTANT DDL」、「INPLACE DDL」和「COPY DDL」。其中,Online DDL(用户常说的“非锁表”DDL,包括INSTANT DDL和绝大多数INPLACE DDL)在执行DDL期间绝大多数时刻并不锁表,只在修改元数据时短暂持有表的MDL-X锁(持有时间一般秒级),用户体验良好。当前的MySQL 8.0已经实现了常见高频DDL的Online能力,例如增加索引、秒级加列,加减主键等等。但是,因为涉及一些SQL层的操作,目前依然存在COPY类型的DDL,它在执行DDL期间「全程锁表」(只能读不能写),例如修改表的字符集、修改列类型等操作。针对这类COPY DDL,PolarDB MySQL的解决方案是扩展Online DDL(不锁表)的范围,例如支持Instant Modify Column(秒级修改列类型),例如尝试在SQL层支持所有DDL的Online能力,我们将这类能力统称为「Fast DDL」,笔者后续会统一介绍这方面的工作,本文不再赘述。相比于MySQL,PolarDB的集群架构使得这一问题变得更加复杂:MDL锁不仅要关注单个节点,更要关注集群多个节点/集群同步链路上的锁问题,需要集群维度的全链路解决方案。熟悉MySQL的用户,对基于Binlog的MySQL主备集群一定非常熟悉。在依赖Binlog的MySQL主备复制集群上,主备节点是逻辑隔离的。也就是说,主节点的MDL锁行为,并不会对备节点的MDL锁有任何影响,因此MySQL只需要考虑单个节点的MDL锁问题。然而,PolarDB MySQL是基于共享存储的架构。以一写多读集群为例,写节点和多个只读节点共享同一个分布式存储,依赖物理复制完成不同节点之间的数据同步。写节点在做DDL操作时,多个只读节点都会看到DDL过程中的实时数据。因此,PolarDB的MDL表锁,是一个集群维度的分布式锁,需要考虑多节点上的锁堵塞问题。基于PolarDB的架构特征,结合多年线上运维经验,我们认为从集群维度看,要实现用户体验良好的DDL锁机制,需要达到以下几个目标:1. 解决雪崩问题。不管是RW写节点上的大事务/大查询,还是只读节点集群上任何一个节点的大事务/大查询,抑或是RW->RO物理复制链路上任何可能的堵塞点,都可能导致DDL拿不到锁,从而触发业务雪崩。针对这类问题,PolarDB MySQL在去年发布了Non-Block DDL功能(用户文档[4],月报链接[3]),可以保证即使在无法获得MDL-X锁的情况下,依然允许新事务访问目标表,从而保证整个业务系统的稳定。该功能受到了很多客户的欢迎,多个客户认为这个功能是执行DDL的刚需能力;2. 解决饥饿问题。Non-Block DDL在拿不到锁时,通过Retry等方式避免DML的堆积和雪崩。然而如果存在大事务或者大查询,DDL可能一直拿不到锁而持续失败。进一步的,随着PolarDB MySQL的大客户越来越多,单实例不乏10+个只读节点的用户,这大大增加了集群维度出现大查询/大事务的概率,导致DDL拿不到锁。针对这类问题,PolarDB MySQL最近推出了Preemptive DDL能力(用户文档[5]),即赋予DDL最高的MDL锁权限,在满足条件的情况下主动kill堵塞它的事务/查询,保证DDL的顺利执行;3. 解决表「数据变更」、「元信息/元信息缓存变更」和「文件操作」 这三者之间的数据一致性和实时性问题。众所周知,TP数据库对事务的要求极高,而DDL过程中涉及的数据变更、表结构变更和文件操作这三者之间需要在任何一个时间点都要满足Consistency的要求。而在基于共享存储的PolarDB MySQL中,这一问题变得更加复杂:不仅在所有阶段(正常数据同步、数据库Recovery、按时间点还原等等)需要满足多节点在数据变更、表结构变更和文件操作这三者的一致性要求,而且需要保证良好的性能,满足强实时性的要求。针对这类问题,PolarDB MySQL做了一系列的优化,由于这部分内容要求的数据库背景和对代码的理解要求过高,并且用户业务无需感知,本文不展开介绍这一部分的工作;4. 解决DDL过程中RW->RO物理复制链路的堵塞问题。上线五年以来,PolarDB MySQL支持了大量行业,不同行业的业务场景对DDL的要求是不同,具体表现在: ● 高频DDL导致的高性能MDL锁需求,例如SaaS等行业场景,DDL是个非常常见和高频的操作。PolarDB需要避免分布式MDL锁和物理复制的耦合性,避免因为锁堵塞等行为影响整个集群的数据同步;● DDL伴随高负载的业务压力,例如在大压力场景下加索引。这种场景会产生大量的redo日志,PolarDB需要保证DDL过程下物理复制链路的稳定性、低延迟。针对上述问题,PolarDB MySQL在物理复制全链路做了优化(用户文档[6]),采用了异步线程池和反馈机制,解耦了MDL锁同步和物理复制的强耦合性,并优化了DDL过程中redo日志的同步&复制速度(用户文档[7]),满足了大压力DDL场景下的同步要求;5. 持续演进的能力:DDL & DML MVCC。如前文所述,在极限情况下,用户依然需要手动执行Preemptive DDL来解决饥饿问题。我们一直在想,有没有更理想的方式,用户可以完全无感知MDL锁的存在。熟悉InnoDB的读者一定知道,InnoDB提供了行级别的MVCC能力,即使修改某行数据的事务没有提交,这时候另一个事务查询同一行数据时,事务根据它的时间戳,通过undo list构建出对应的版本,无需等待锁的释放。细心的读者一定会问,为什么DDL没有提供DDL和DML互不堵塞这种MVCC的能力?原因在于,DDL操作涉及了文件操作/表数据/元信息/表结构缓存等多种信息的变更,因此为了达到DDL & DML的MVCC能力,涉及大量的模块/代码修改,带来的代码切口过大,稳定性风险较高。但是为了满足客户的诉求,PolarDB内核团队一直在这条路径上试图找到工程上的最优路径。在PolarDB 8.0.2的下个版本中,我们将提供给用户这一实验室功能,即满足Instant Add Column这种高频DDL与DML的「MVCC」能力,后续我们会陆续支持Add Index等高频DDL与DML的MVCC能力。02-Non-Block DDL(雪崩问题)2.1 功能概述如前文所述,非阻塞DDL(用户文档[4],月报链接[3])用于解决因MDL锁堵塞而导致的业务雪崩问题。非阻塞DDL功能采用了和第三方插件(gh-ost、pt-osc)类似的拿锁逻辑:当DDL操作获取MDL锁失败时,拿锁线程会进入短暂的Sleep阶段,接着重新尝试获取MDL锁。通过此种方式,非阻塞DDL保证了DDL执行过程中,业务真正的online。非阻塞DDL目前已经灰度一段时间,受到大量用户的欢迎,后面会尝试默认开启此功能。此外,我们将在8.0.2的2.2.15版本中,支持集群维度的Non-Block DDL:如果主节点已经获取MDL锁,但是只读节点同步MDL锁堵塞(当前默认堵塞时间为50s,由参数loose_replica_lock_wait_timeout控制),Non-Block DDL会在集群维度重试拿锁的操作,从而实现集群维度的非阻塞DDL。2.2 测试效果可以通过设置参数loose_polar_nonblock_ddl_mode为ON来打开非阻塞DDL功能(用户文档[4]),下面给出使用sysbench模拟用户业务,对比开启Non-Block DDL功能和使用原生DDL功能对业务的影响。1. 在目标表sbtest1上开启一个事务但不提交,该事务将持有目标表sbtest1的MDL锁。begin;
select * from sbtest1;2. 在新会话中,分别在开启和关闭Non-Block DDL情况下,对表sbtest1进行加列操作,观察TPS的变化情况。# 由于当前session 1大查询持有MDL锁,当前DDL无法获取MDL锁,被堵塞
alter table sbtest1 add column d int;▶︎ 关闭Non-Block DDL功能TPS持续跌零,默认超时时间为31536000,严重影响用户业务。▶︎ 开启Non-Block DDL功能TPS周期性下降,但未跌零。对用户业务影响较小,能保证业务系统的稳定。03-Preemptive DDL (饥饿问题)3.1 功能概述上文非阻塞DDL解决了DDL获取MDL锁阻塞导致的业务雪崩问题,但是如果DDL迟迟无法获取MDL锁,会导致DDL执行频繁失败。目前线上值班偶尔会遇到由于RO上面存在大查询、长事务导致的DDL执行失败问题,并返回错误ERROR 8007 (HY000): Fail to get MDL on replica during DDL synchronize。由于此报错与PolarDB共享存储的架构相关,与传统MySQL不一致,用户经常会一头雾水,无从下手。当前已有官方文档(执行DDL操作提示“获取不到MDL锁”[8])介绍这类问题的解决方案,用户可以根据此文档找到只读节点上持有表MDL锁的事务,手动进行Kill,来保证DDL同步MDL锁的成功。但是这种方式在部分场景下依然非常晦涩,一方面用户进行kill操作的时间窗口有限(当前同步MDL锁超时时间为50秒,可通过loose_replica_lock_wait_timeout进行调整),另一方面随着PolarDB上面客户不断增多,出现了许多10+个只读节点的集群,手动kill操作显得狼狈且低效,为此我们提供了抢占式DDL功能。当只读节点通过物理复制,解析到当前表上有DDL操作时,只读节点会尝试获取表的MDL锁。如果此时表上存在大查询或长事务时,开启Preemptive DDL后(用户文档[5]),如果只读节点在预期时间内无法获得MDL锁,便会尝试kill掉占有MDL锁的线程,从而保证MDL锁同步的成功,解决DDL的饥饿问题。3.2 测试效果可以通过设置参数loose_polar_support_mdl_sync_preemption为ON来打开抢占式DDL功能。下面给出DDL同步MDL锁被只读节点长事务堵塞时,开启和关闭抢占式DDL的实验效果。▶︎ 关闭抢占式DDL功能1. 在只读节点上查询test.t1:mysql> use test
Database changed
#大查询,执行100s
mysql> select sleep(100) from t1;2. 在主节点进行加列操作,被block,执行失败:mysql > alter table t1 add column c int;
ERROR 8007 (HY000): Fail to get MDL on replica during DDL synchronize由于只读节点存在大查询,同步MDL锁失败,DDL执行失败,并回滚。▶︎ 开启抢占式DDL功能1. 在只读节点上查询test.t1:mysql> use test
Database changed
#大查询,执行100s
mysql> select sleep(100) from t1;2. 在主节点进行加列操作操作,被block,等待一段时间,发生抢占,执行成功:mysql> alter table t1 add column c int;
Query OK, 0 rows affected (11.13 sec)
Records: 0 Duplicates: 0 Warnings: 0开启抢占式DDL功能后,加列操作完成,同时可以看到只读节点(右图),大查询连接已经断开。04-多版本DD:DDL & DML的MVCC能力4.1 功能概述不管是Non-Block DDL还是Preemptive DDL,都是在有互斥锁的场景下,尽可能最优地满足用户的DDL变更需求。然而,用户在部分场景下依然要感知MDL锁的存在,例如在极限场景下,用户依然需要手动触发Preemptive DDL,来解决DDL饥饿的问题。我们一直在探索,是否可以实现DDL与DML更细粒度的并发控制,类似于InnoDB MVCC能力。然而,如前文所述,DDL是个复杂操作,其执行过程涉及文件操作/表数据变更/元信息变更/表缓存处理等一系列流程。因此,考虑到MySQL代码的强耦合性,我们对这一目标做了切分,在控制代码切口和稳定性风险的情况下,逐步支持这一能力。在第一阶段,我们优先支持线上高频DDL与DML的MVCC能力,即按照statement维度,满足Instant Add Column与DML的MVCC能力(用户文档待新版本8.0.2上线)。为了兼容MySQL的默认表现,我们不仅支持DDL和未提交事务的并发,而且支持DD的readview,使得跨越了DDL的DML事务可以选择以RC或者RR的隔离级别读取表结构信息,从而让用户自行决定使用新或者旧的表定义。4.2 测试效果具体的效果如下:步骤一:开启会话A,创建一个新的表t1并插入一些数据;随后开启一个新事务,在事务中进行数据的插入和更新操作,但事务不提交:步骤二(DDL不会被未提交的事务所堵塞):开启一个新的会话B,查询performance_schema,此时t1的MDL正被会话A中未提交的事务持有。进行DDL操作(add column),该操作可以立即完成,而不会被未提交的事务阻塞。步骤三(跨DDL的事务可以选择访问表时使用的隔离级别):回到第一个会话A,将表访问的隔离级别参数table_def_isolation设置REPEATABLE-READ,因为DDL的执行在该事务之后,因此新增的列c不可见,该事务将始终看到与事务开始时一致的表定义。将table_def_isolation设置为READ-COMMITTED,因为DDL已经提交,列c将对该事务可见。提交事务后,DD的readview随之释放,随后将只能看到最新的表结构。05-全链路优化的分布式MDL锁(多节点数据同步问题)目前的云原生数据库,不论是PolarDB,或者其它厂商数据库,都以“存算分离”+“共享存储”的形态提供一写多读的能力。对这类架构感兴趣的读者,可以阅读我们之前的相关月报(PolarDB 物理复制解读[9],PolarDB 物理复制热点页优化[10])。对这类针对存算分离场景下IO优化感兴趣的读者,可以阅读我们去年发表在VLDB上的相关论文(CloudJump: Optimizing Cloud Databases for Cloud Storages[11])。简单来说,云原生数据库依赖物理复制(Redo日志)完成不同节点之间的数据同步,而DDL触发的元数据/表数据/文件变更同样随着物理复制完成多节点的同步,这三者之间依赖分布式MDL锁提供实时&一致性的保证。然而,当MDL锁和物理复制相耦合时,会产生一系列的问题,尤其是日志流 / 锁同步 / 文件操作这三者之间的一致性问题。这里,我们介绍与用户密切相关的两类问题:5.1 异步元数据锁同步高频DDL场景下分布式MDL锁的稳定性&实时性。尤其是在MDL锁被堵塞时,不能影响正常物理日志的进行。为了解决这个问题,PolarDB设计了全新的分布式MDL锁机制(用户文档[6],已默认开启),主要体现在以下两个方面:● 异步MDL锁复制:将分布式MDL锁与物理复制相互解耦,实现了即使在等待MDL锁时,只读节点仍能继续解析并应用物理日志,保证了物理复制的实时性;● 并行MDL锁:为了优化高频DDL场景下分布式MDL锁的性能,我们采用一组线程池来并发响应MDL锁的需求。即使某个MDL锁被堵塞,也不会影响其它线程去获取MDL锁,并且这部分线程池会随着DDL的情况动态调整,保证了MDL锁同步的高并发。5.2 DDL物理复制优化高压力DDL场景下物理复制的稳定性&实时性。PolarDB中的数据是通过B-Tree来维护索引的,然而大部分Slow DDL操作(如增加主键或二级索引、Optimize Table等)往往需要重建或新增B-Tree索引,导致大量物理日志的产生。而针对物理日志进行的操作往往出现在DDL执行的关键路径上,增加了DDL操作的执行时间。此外,物理复制技术要求只读节点解析和应用这些新生成的物理日志,DDL操作而产生的大量物理日志可能严重影响只读节点的日志同步进程,甚至导致只读节点不可用等问题。针对上述问题,PolarDB提供了DDL物理复制优化功能(用户文档[7],已默认开启),主要体现在以下两个方面:● 主节点加快DDL写日志速度:在主节点写物理日志和只读节点应用物理日志的关键路径上做了全面的优化,使得主节点在执行创建主键DDL操作的执行时间最多可减少20.6%;● 只读节点加快物理复制速度:只读节点解析DDL的复制延迟时间最多约可减少至原来的0.4%,并且明显降低了CPU / Memory / IO的硬件开销。以下面测试数据为例,在主节点上不论执行1个DDL还是8个DDL,只读节点非常稳定,没有明显抖动。06-总结DDL是PolarDB所有SQL操作中最繁重的一种,DDL的易用性是PolarDB良好使用体验非常重要的一环。本文总结介绍了PolarDB在全链路MDL锁治理的经验和进展,把简单留给客户,把复杂留给自己,持续优化用户的使用体验。后续将总结介绍PolarDB在Fast DDL方面的工作,PolarDB内核团队将始终如一地为用户打造最佳的云原生数据库。用户文档及相关技术链接[1] 常用SQL语句的MDL加锁及源码分析 : http://mysql.taobao.org/monthly/2018/02/01/[2] MDL锁实现分析 : http://mysql.taobao.org/monthly/2015/11/04/[3] 非阻塞DDL月报介绍 : http://mysql.taobao.org/monthly/2022/10/01/[4] 非阻塞DDL用户文档 :https://help.aliyun.com/document_detail/436462.html[5] 抢占式DDL用户文档 : https://help.aliyun.com/document_detail/2326304.html[6] PolarDB 并行元数据锁同步 : https://help.aliyun.com/document_detail/200678.html[7] DDL物理复制优化 : https://help.aliyun.com/document_detail/198213.html[8] 执行DDL操作提示“获取不到MDL锁” : https://help.aliyun.com/document_detail/611732.html[9] PolarDB 物理复制解读 : http://mysql.taobao.org/monthly/2018/12/05/[10] PolarDB 物理复制热点页优化 : http://mysql.taobao.org/monthly/2021/03/04/[11] VLDB论文链接CloudJump: Optimizing Cloud Databases for Cloud Storages :https://www.vldb.org/pvldb/vol15/p3432-chen.pdf
《云存储应用白皮书》——应用实践——一、本地数据上云方案——2. 数据归档上云方案
2. 数据归档上云方案 1)需求背景 IDC数据显示,到2025年,全球数据圈将增至175ZB,其中,企业存储的数据量将超过全球总数据量的80%。随着数量的急剧增长,企业陷入非结构化数据溢出的危险境地。问题不在于企业购置容量来存储全部数据,而是如何以低成本高效率的方式妥善管理数据以创造商业价值,尤其是长期数据保留。 合规和监管要求并非企业长期保留数据的唯一动力:在英国,互联网服务商必须保留通讯记录一年以上;在美国,美国联邦有关研究记录的准则规定,研究数据在研究工作结束后应至少保留三年,《健康保险可移植性和责任法案》要求患者数据至少保留6年。 2)解决方案 在数据归档场景中,可选择闪电立方上云做异地备份,如果文件想要自动增量同步上云做归档的,则混合云存储阵列的云文件网关的云复制功能或云缓存功能是较好的选择。对于包括医疗影像、科研数据、金融数据、视频素材等海量数据的长期归档,对象存储归档/冷归档/深度冷归档存储类型也可以实现。此类数据都有一些共同的特性: ∙ 存储周期长,几年、几十年甚至永久;∙ ∙ 实时访问要求不高,读取数据可以接受一定的等待时间;∙ ∙ 对数据安全性、可靠性要求高,某些数据需要符合特定的行业规范要求;∙ ∙ 在整个存储周期内,需要比传统存储解决方案更低的存储单价; 对象存储全面覆盖从有热点存在、频繁访问的各类音视频、图片数据,到低频访问的各类备份数据,再到长期归档的数据。配合生命周期机制,按照配置的时间周期,将数据转储到更低单价的存储类型上,优化存储成本。 方案优势: ∙ OSS支持标准、低频访问、归档、冷归档、深度冷归档五种存储类型,可以通过生命周期规则或者CopyObject的方式随时转换文件(Object)的存储类型。归档存储类型API支持用户直接把需要备份的文件存储到OSS归档存储,存储后的文件名称保持不变。∙ ∙ 使用OSS归档/冷归档/深度冷归档存储类型,可以降低归档数据的管理难度和相关管理资源的投入。∙ ∙ OSS支持多次读取特性,支持以“不可篡改、不可删除”的方式,满足数据合规保存或防止恶意删除等要求,并获得多项合规认证,满足行业合规要求。
《云存储应用白皮书》——应用实践——三、云上数据应用解决方案——6. 高性能计算存储在渲染行业的应用
6. 高性能计算存储在渲染行业的应用 1)需求背景 伴随着4K、VR、AI和仿真技术的高速发展,渲染、游戏等行业越来越依赖高性能计算平台来替代传统的IT分析系统,为业务进入快车道发展争取更多的时间窗口,而背靠在计算平台之后的存储系统是不可忽视的重要一环。 渲染属于典型的数字内容创作高性能计算应用,渲染的关键之处在于实现集群计算,让海量节点来分摊成百上千万的渲染小时。。随着文娱产业及工业的发展以及显示器硬件的高清化升级,用户和CG制作者对于图形质量如清晰度和画面细节的要求越来越高,行业对于渲染服务的需求规模和质量要求迅速扩大和提高。 2)解决方案 在渲染前,需要为大规模数据准备足够容量的存储设备;在渲染过程中,还需要高性能来完成大量的数据处理工作,以生成最终输出。所以,渲染业务对吞吐、IOPS和元数据OPS等要求都非常高,使用传统的存储解决方案将难以满足客户需求。为此可使用基于阿里云CPFS(并行文件系统)设计的计算存储解决方案。 目前CPFS支持公共云上即开即用,也支持通过阿里云定制的CPFS一体化软硬件服务用户的线下数据机房。由于渲染涉及的数据资产需要线下存储,因此本方案实践是通过线下实施部署的CPFS一体机。整体解决方案架构如下: 方案优势: ∙ 引入协议转换服务,解决多终端和多协议混用。∙ 通过分布式的协议节点和存储节点,实现节点存储容量和性能实现弹性扩容。∙ 在计算节点、CPFS客户端、协议节点和存储节点划分一定容量的内存缓存,加速计算过程的数据读写。∙ 采用SSD+HDD混合部署的模式,解决了冷热数据存储成本问题。∙ 存储单节点吞吐量达到2.6GB/s,并随节点数增加线性增长,最大可扩展到近千节点。
【React工作记录七十五】react前端优化小结笔记
编译阶段的优化主要是webpack开发环境时重复构建更快1.include缩小编译的范围
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
persets: [
'@babel/preset-react'
],
plugins: [
'@babel/plugin-proposal-class-properties'
]
}
}
],
include: path.resovle('src'),
exclude: /node_modules/
}
]2.resolve配置解析 ... const bootstrap = path.resolve("node_modules/boostrap/dist/css/bootstrap.css") ... resolve: { modules: [path.resolve('node_modules')] // 查找范围 extensions: ['.js', '.ts'], // 扩展 名 越少越好 alias: { bootstrap }, fallback: { cryto: false, // 假如说你引用的一个包里面有node核心模块 它会帮你跳过这个node核心模块的引用 让你最后打出来的包体积更小 buffer: false, stream: false } }, module: { rules: [...] },3.alias上面提到了4.external如果配置了这个插件 就不会打包lodash模块 会引入 _ 这个全局变量
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'lodash',
entry: 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/loadash.js',
global: '_'
}
]
})5.编译缓存{
loader: 'babel-loader',
options: {
cachDirectory: true, // 开启缓存
}
}
但如果是webpack5
可以配置持久化缓存 以前是没有的
devtool:...
cache: { // 开启持久化缓存
type: 'filesystem' // 改为 memory会更快
},
entry: ...6.开启多进程entry:... optimization: { minimize: isEnvProduction, // 生产环境会开启压缩 minimizer: [ new TerserPlugin({parallel: true}), // 压缩js 多进程压缩 new OptimizeCSSAssetsPlugin() // 压缩css ], splitChunks: ... }生产环境时文件更小 加载更快 1.开启tree-sharking import {join} from 'lodash' 仅仅只会打包join这一个方法 2.scope-hositing 将作用域提升 可以减小包的体积 3.splitChunks 代码分割 懒加载 optimization: { minimize: isEnvProduction, minimizer: [ new TerserPlugin({parallel: true}), new OptimizeCSSAssetsPlugin() ], splitChunks: { chunks: 'all', // 支持 asynchronous和同步 minSize: 0, // 最小大小 minRemainingSize: 0, //分割出去剩下的大小 maxSize: 0, // 最大大小 minChunks: 1, // 最小引用次数 maxAsyncRequests: 30, // 异步模块最大并发请求数 import('..') maxInitialRequests: 30, // 同步模块最大并发请求数 import enforceSizeThreshold: 50000, // 如果一个代码块的大小超过这个数就强行打包webpack新属性 cacheGroups: { // 缓存组 defaultVendors: { // 第三方的模块如果在node_modules里面的话会进行一个单独的分割 test: /[\\/]node_modules[\\/]/, priority: -10, // 优先级 reuseExistingChunk: true // 是否使用已经分割出的代码块 }, default: { minChunks: 2, // 如果一个模块被引用2次的话 会单独提取为一个代码块 priority: -20, reuseExistingChunk: true } } }, runtimeChunk: { // 运行时代码 单独分割 name: entrypoint => runtime-${entrypoint.name} }, moduleIds: isEnvProduction ? 'deterministic' : 'named', chunkIds: isEnvProduction ? 'deterministic' : 'named' } 4.提供node的空mocks 我们上面在fallback中已经配置了 5.持久化缓存 webpack5新增功能 上面在代码分割后的相关moduleIds配置项 moduleIds: 'deterministic'路由切换的优化 懒加载更新阶段的优化PureComponent 只要属性不更新 组件就不会重新渲染实现PureComponent以及memo但上述的属性对比仅仅是浅比较 所以存在缺陷 所以我们需要用到 immutable.js import {Map} from 'immutable'; Map 为不可变数据 每次操作都会返回一个不同的对象引用地址 但是每次会尽可能复用老的属性 另外可以非常快速进行深度比较 性能特别高class App ... { constructor() { ... this.state = {counter:Map({number:0})}; } add = (amount) => { let count = this.state.count.set('number', this.state.count.get('number') + amount);
this.setState(count);
}
render() {
...
return (
<button onClick={()=>this.add(1)}>
)
}
}可视化工具 React Developer Tools优化redux 通过reselect 简单实现一个createSelector大数据渲染优化时间分片优化前时间分片优化后虚拟列表骨架屏在真实内容渲染前 有一个loading效果 我们要写一个webpack插件 在编译阶段就生成一个svg图 插入值中去 skeleton.js (src/skeleton.js) 在根目录下(与webpack.config.js平行) 新建SkeletonWebpackPlugin.js weback.base.js webpack.config.js webpack.seleton.js通过webpack进行预渲染 由于spa项目普通的爬虫无法爬取项目的静态文本的内容 通过预渲染插件prerender-spa-plugin解决spa项目的seo问题 prerender-spa-plugin利用了Puppeteer的爬取页面的功能 Puppeteer是一个Chrome官方出品的headless Chrome node库 它提供了一系列的API 可以在无UI的情况下调用Chrome的功能 适用于爬虫 自动化处理等各种场景 原理是在webpack构建阶段的最后 在本地启动一个Puppeteer的服务 访问配置了预渲染的路由 然后将Puppeteer中渲染的页面输出到html文件中 并建立路由对应的目录 注意:不适合不同的用户看都会不同的页面 这种类型的页面不适用预渲染 对于一些经常发生变化的页面 如体育比赛等 会导致编译后的数据不是实时更新的虚拟列表还有 React hooks性能优化 Error Boundaries 通过DOM-DIFF原理进行性能优化 响应式数据的精细化渲染 图片懒加载等优化手段