Go内存优化与垃圾收集

Go提供了自动化的内存管理机制,但在某些情况下需要更精细的微调从而避免发生OOM错误。本文介绍了如何通过微调GOGC和GOMEMLIMIT在性能和内存效率之间取得平衡,并尽量避免OOM的产生。原文: Memory Optimization and Garbage Collector Management in Go

alt

本文将讨论Go的垃圾收集器、应用程序内存优化以及如何防止OOM(Out-Of-Memory)错误。

Go中的堆(Heap)栈(Stack)

我不会详细介绍垃圾收集器如何工作,已经有很多关于这个主题的文章和官方文档(比如A Guide to the Go Garbage Collector[1]源码[2])。但是,我会提到一些有助于理解本文主题的基本概念。

你可能已经知道,Go的数据可以存储在两个主要的内存存储中: 栈(stack)和堆(heap)。

alt

通常,栈存储的数据的大小和使用时间可以由Go编译器预测,包括函数局部变量、函数参数、返回值等。

栈是自动管理的,遵循后进先出(LIFO)原则。当调用函数时,所有相关数据都放在栈的顶部,函数结束时,这些数据将从栈中删除。栈不需要复杂的垃圾收集机制,其内存管理开销最小,在栈中检索和存储数据的过程非常快。

然而,并不是所有数据都可以存储在栈中。在执行过程中动态更改的数据或需要在函数范围之外访问的数据不能放在栈上,因为编译器无法预测其使用情况,这种数据应该存储在堆中。

与栈不同,从堆中检索数据并对其进行管理的成本更高。

栈里放什么,堆里放什么?

正如前面提到的,栈用于具有可预测大小和寿命的值,例如:

  • 在函数内部声明的局部变量,例如基本数据类型变量(例如数字和布尔值)。
  • 函数参数。
  • 函数返回后不再被引用的返回值。

Go编译器在决定将数据放在栈中还是堆中时会考虑各种细微差别。

例如,预分配大小为64 KB的数据将存储在栈中,而大于64 KB的数据将存储在堆中。这同样适用于数组,如果数组超过10 MB,将存储在堆中。

alt

可以使用逃逸分析(escape analysis)来确定特定变量的存储位置。

例如,可以通过命令行编译参数-gcflags=-m来分析应用程序:

go build -gcflags=-m main.go

如果使用-gcflags=-m参数编译下面的main.go:

package main

func main() {
  var arrayBefore10Mb [1310720]int
  arrayBefore10Mb[0] = 1

  var arrayAfter10Mb [1310721]int
  arrayAfter10Mb[0] = 1

  sliceBefore64 := make([]int8192)
  sliceOver64 := make([]int8193)
  sliceOver64[0] = sliceBefore64[0]
}

结果是:

# command-line-arguments
./main.go:3:6: can inline main
./main.go:7:6: moved to heap: arrayAfter10Mb
./main.go:10:23: make([]int, 8192) does not escape
./main.go:11:21: make([]int, 8193) escapes to heap

可以看到arrayAfter10Mb数组被移动到堆中,因为大小超过了10MB,而arrayBefore10Mb仍然留在栈中(对于int变量,10MB等于10 * 1024 * 1024 / 8 = 1310720个元素)。

此外,sliceBefore64没有存储在堆中,因为它的大小小于64KB,而sliceOver64被存储在堆中(对于int变量,64KB等于64 * 1024 / 8 = 8192个元素)。

要了解更多关于在堆中分配的位置和内容,可以参考malloc.go源码[3]

因此,使用堆的一种方法是尽量避免用它!但是,如果数据已经落在堆中了呢?

与栈不同,堆的大小是无限的,并且不断增长。堆存储动态创建的对象,如结构体、分片和映射,以及由于其限制而无法放入栈中的大内存块。

在堆中重用内存并防止其完全阻塞的唯一工具是垃圾收集器。

浅谈垃圾收集器的工作原理

垃圾收集器(GC)是一种专门用于识别和释放动态分配内存的系统。

Go使用基于跟踪和标记和扫描算法的垃圾收集算法。在标记阶段,垃圾收集器将应用程序正在使用的数据标记为活跃堆。然后,在清理阶段,GC遍历所有未标记为活跃的内存并复用。

垃圾收集器不是免费工作的,需要消耗两个重要的系统资源: CPU时间和物理内存。

垃圾收集器中的内存由以下部分组成:

  • 活跃堆内存(在前一个垃圾收集周期中标记为"活跃"的内存)
  • 新的堆内存(尚未被垃圾收集器分析的堆内存)
  • 存储元数据的内存,与前两个实体相比,这些元数据通常微不足道。

垃圾收集器所消耗的CPU时间与其工作细节有关。有一种称为"stop-the-world"的垃圾收集器实现,它在垃圾收集期间完全停止程序执行,导致CPU时间被花在非生产性工作上。

在Go里,垃圾收集器并不是完全"stop-the-world",而是与应用程序并行执行其大部分工作(例如标记堆)。

但是,垃圾收集器的操作仍然有一些限制,并且会在一个周期内多次完全停止工作代码的执行,想要了解更多可以阅读源码[4]

alt
如何管理垃圾收集器

在Go中可以通过某些参数管理垃圾收集器: GOGC环境变量或runtime/debug包中的等效函数SetGCPercent

GOGC参数确定将触发垃圾收集的新未分配堆内存相对于活跃内存的百分比。

GOGC的默认值是100,意味着当新内存达到活跃堆内存的100%时将触发垃圾收集。

当新堆占用活跃堆的100%时,将运行垃圾收集器。
当新堆占用活跃堆的100%时,将运行垃圾收集器。

我们以示例程序为例,通过go tool trace跟踪堆大小的变化,我们用Go 1.20.1版本来运行程序。

在本例中,performMemoryIntensiveTask函数使用了在堆中分配的大量内存。这个函数启动一个队列大小为NumWorker的工作池,任务数量等于NumTasks

package main

import (
 "fmt"
 "os"
 "runtime/debug"
 "runtime/trace"
 "sync"
)

const (
 NumWorkers    = 4     // Number of workers.
 NumTasks      = 500   // Number of tasks.
 MemoryIntense = 10000 // Size of memory-intensive task (number of elements).
)

func main() {
 // Write to the trace file.
 f, _ := os.Create("trace.out")
 trace.Start(f)
 defer trace.Stop()

 // Set the target percentage for the garbage collector. Default is 100%.
 debug.SetGCPercent(100)

 // Task queue and result queue.
 taskQueue := make(chan int, NumTasks)
 resultQueue := make(chan int, NumTasks)

 // Start workers.
 var wg sync.WaitGroup
 wg.Add(NumWorkers)
 for i := 0; i < NumWorkers; i++ {
  go worker(taskQueue, resultQueue, &wg)
 }

 // Send tasks to the queue.
 for i := 0; i < NumTasks; i++ {
  taskQueue <- i
 }
 close(taskQueue)

 // Retrieve results from the queue.
 go func() {
  wg.Wait()
  close(resultQueue)
 }()

 // Process the results.
 for result := range resultQueue {
  fmt.Println("Result:", result)
 }

 fmt.Println("Done!")
}

// Worker function.
func worker(tasks <-chan int, results chan<- int, wg *sync.WaitGroup) {
 defer wg.Done()

 for task := range tasks {
  result := performMemoryIntensiveTask(task)
  results <- result
 }
}

// performMemoryIntensiveTask is a memory-intensive function.
func performMemoryIntensiveTask(task int) int {
 // Create a large-sized slice.
 data := make([]int, MemoryIntense)
 for i := 0; i < MemoryIntense; i++ {
  data[i] = i + task
 }

 // Latency imitation.
 time.Sleep(10 * time.Millisecond)

 // Calculate the result.
 result := 0
 for _, value := range data {
  result += value
 }
 return result
}

跟踪程序执行的结果被写入文件trace.out:

// Writing to the trace file.
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()

通过go tool trace,可以观察堆大小的变化,并分析程序中垃圾收集器的行为。

请注意,go tool trace的精确细节和功能可能因go版本不同而有所差异,因此建议参考官方文档,以获取有关其在特定go版本中使用的详细信息。

GOGC的默认值

GOGC参数可以使用runtime/debug包中的debug.SetGCPercent进行设置,GOGC默认设置为100%。

用下面命令运行程序:

go run main.go

程序执行后,将会创建trace.out文件,可以使用go tool工具对其进行分析。要做到这一点,执行命令:

go tool trace trace.out

然后可以通过打开web浏览器并访问http://127.0.0.1:54784/trace来查看基于web的跟踪查看器。

GOGC = 100
GOGC = 100

在"STATS"选项卡中,可以看到"Heap"字段,显示了在应用程序执行期间堆大小的变化情况,图中红色区域表示堆占用的内存。

在"PROCS"选项卡中,"GC"(垃圾收集器)字段显示的蓝色列表示触发垃圾收集器的时刻。

一旦新堆的大小达到活动堆大小的100%,就会触发垃圾收集。例如,如果活跃堆大小为10 MB,则当当前堆大小达到10 MB时将触发垃圾收集。

跟踪所有垃圾收集调用使我们能够确定垃圾收集器处于活动状态的总时间。

GOGC=100时的GC调用次数
GOGC=100时的GC调用次数

示例中,当GOGC值为100时,将调用垃圾收集器16次,总执行时间为14 ms。

更频繁的调用GC

如果我们将debug.SetGCPercent(10)设置为10%后运行代码,将观察到垃圾收集器调用的频率更高。现在,如果当前堆大小达到活跃堆大小的10%时,将触发垃圾收集。

换句话说,如果活跃堆大小为10 MB,则当前堆大小达到1 MB时就将触发垃圾收集。

GOGC = 10
GOGC = 10

在本例中,垃圾收集器被调用了38次,总垃圾收集时间为28 ms。

GOGC=10时的GC调用次数
GOGC=10时的GC调用次数

可以观察到,将GOGC设置为低于100%的值可以增加垃圾收集的频率,可能导致CPU使用率增加并降低程序性能。

更少的调用GC

如果运行相同程序,但将debug.SetGCPercent(1000)设置为1000%,我们将得到以下结果:

GOGC = 1000
GOGC = 1000

可以看到,当前堆的大小一直在增长,直到达到活跃堆大小的1000%。换句话说,如果活跃堆大小为10 MB,则当前堆大小达到100 MB时将触发垃圾收集。

GOGC=1000时的GC调用次数
GOGC=1000时的GC调用次数

在当前情况下,垃圾收集器被调用一次并执行2毫秒。

关闭GC

还可以通过设置GOGC=off或调用debug.SetGCPercent(-1)来禁用垃圾收集。

下面是禁用垃圾收集器而不设置GOMEMLIMIT时堆的行为:

当GC=off时,堆大小不断增长。
当GC=off时,堆大小不断增长。

可以看到,在关闭GC后,应用程序的堆大小一直在增长,直到程序执行为止。

堆占用多少内存?

在活跃堆的实际内存分配中,通常不像我们在trace中看到的那样定期和可预测的工作。

活跃堆随着每个垃圾收集周期动态变化,并且在某些条件下,其绝对值可能出现峰值。

例如,如果由于多个并行任务的重叠,活跃堆的大小可以增长到800 MB,那么只有在当前堆大小达到1.6 GB时才会触发垃圾收集。

alt

现代开发通常在具有内存使用限制的容器中运行应用。因此,如果容器将内存限制设置为1 GB,并且总堆大小增加到1.6 GB,则容器将失效,并出现OOM(out of memory)错误。

让我们模拟一下这种情况。例如,我们在内存限制为10 MB的容器中运行程序(仅用于测试目的)。Dockerfile:

FROM golang:latest as builder


WORKDIR /src
COPY . .


RUN go env -w GO111MODULE=on


RUN go mod vendor
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -a -installsuffix cgo -o app ./cmd/


FROM golang:latest
WORKDIR /root/
COPY --from=builder /src/app .
EXPOSE 8080
CMD ["./app"]

Docker-compose描述:

version: '3'
services:
 my-app:
   build:
     context: .
     dockerfile: Dockerfile
   ports:
     - 8080:8080
   deploy:
     resources:
       limits:
         memory: 10M

让我们使用前面设置GOGC=1000%的代码启动容器。

可以使用以下命令运行容器:

docker-compose build
docker-compose up

几秒钟后,容器将崩溃,并产生与OOM相对应的错误。

exited with code 137

这种情况非常令人不快: GOGC只控制新堆的相对值,而容器有绝对限制。

alt
如何避免OOM?

从1.19版本开始,在GOMEMLIMIT选项的帮助下,Golang引入了一个名为"软内存管理"的特性,runtime/debug包中名为SetMemoryLimit的类似函数(可以阅读48409-soft-memory-limit.md了解有关此选项的一些有趣的设计细节)提供了相同的功能。

GOMEMLIMIT环境变量设置Go运行时可以使用的总体内存限制,例如: GOMEMLIMIT = 8MiB。要设置内存值,需要使用大小后缀,在本例中为8 MB。

让我们启动将GOMEMLIMIT境变量设置为8MiB的容器。为此,我们将环境变量添加到docker-compose文件中:

version: '3'
services:
 my-app:
    environment:
      GOMEMLIMIT: "8MiB"
   build:
     context: .
     dockerfile: Dockerfile
   ports:
     - 8080:8080
   deploy:
     resources:
       limits:
         memory: 10M

现在,当启动容器时,程序运行没有任何错误。该机制是专门为解决OOM问题而设计的。

这是因为启用GOMEMLIMIT=8MiB后,会定期调用垃圾收集器,并将堆大小保持在一定限制内,结果就是会频繁调用垃圾收集器以避免内存过载。

运行垃圾收集器以使堆大小保持在一定的限制内。
运行垃圾收集器以使堆大小保持在一定的限制内。
成本是什么?

GOMEMLIMIT是强有力的工具,但也可能适得其反。

在上面的堆跟踪图中可以看到这种场景的一个示例。

当总内存大小由于活跃堆或持久程序泄漏的增长而接近GOMEMLIMIT时,将开始根据该限制不断调用垃圾收集器。

由于频繁调用垃圾收集器,应用程序的运行时可能会无限增加,从而消耗应用程序的CPU时间。

这种行为被称为死亡螺旋[5],可能导致应用程序性能下降,与OOM错误不同,这种问题很难检测和修复。

这正是GOMEMLIMIT机制作为软限制起作用的原因。

Go不能100%保证GOMEMLIMIT指定的内存限制会被严格执行,而是会允许使用超出限制的内存,并防止频繁调用垃圾收集器的情况。

为了实现这一点,需要对CPU使用设置限制。目前,这个限制被设置为所有处理器时间的50%,CPU窗口为2 * GOMAXPROCS秒。

这就是为什么我们不能完全避免OOM错误,而是会将其推迟到很久以后发生。

在哪里应用GOMEMLIMIT和GOGC

如果默认垃圾收集器设置在大多数情况下是足够的,那么带有GOMEMLIMIT的软内存管理机制可以使我们避免不愉快的情况。

使用GOMEMLIMIT内存限制可能有用的例子:

  • 在内存有限的容器中运行应用程序时,最好将 GOMEMLIMIT设置为保留5-10%的可用内存。
  • 在运行资源密集型库或代码时,对 GOMEMLIMIT进行实时管理是有好处的。
  • 当在容器中以脚本形式运行应用程序时(意味着应用程序在一段时间内执行某些任务,然后终止),禁用垃圾收集器但设置 GOMEMLIMIT可以提高性能并防止超出容器的资源限制。

避免使用GOMEMLIMIT的情况:

  • 当程序已经接近其环境的内存限制时,不要设置内存限制。
  • 在无法控制的执行环境中部署时,不要使用内存限制,特别是在程序的内存使用与其输入数据成正比的情况下,例如CLI工具或桌面应用程序。

如上所述,通过深思熟虑的方法,我们可以管理程序中的微调设置,例如垃圾收集器和GOMEMLIMIT。然而,仔细考虑应用这些设置的策略无疑非常重要。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

参考资料
[1]

A Guide to the Go Garbage Collector: https://tip.golang.org/doc/gc-guide

[2]

mgc.go: https://go.dev/src/runtime/mgc.go

[3]

malloc.go: https://go.dev/src/runtime/malloc.go

[4]

mgc.go: https://go.dev/src/runtime/mgc.go

[5]

Soft Memory Limit Death Spirals: https://github.com/golang/proposal/blob/master/design/48409-soft-memory-limit.md#death-spirals

本文由 mdnice 多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

回归预测 | Matlab实现POA-CNN-LSTM-Attention鹈鹕算法优化卷积长短期记忆网络注意力多变量回归预测(SE注意力机制)

回归预测 | Matlab实现POA-CNN-LSTM-Attention鹈鹕算法优化卷积长短期记忆网络注意力多变量回归预测&#xff08;SE注意力机制&#xff09; 目录 回归预测 | Matlab实现POA-CNN-LSTM-Attention鹈鹕算法优化卷积长短期记忆网络注意力多变量回归预测&#xff08;SE注意力机制&…

在windows的控制台实现贪吃蛇小游戏

欢迎来到博主的文章 博主id&#xff1a;代码小豪 前言&#xff1a;看懂这篇文章需要具有C语言基础&#xff0c;还要对单链表具有一定的理解。如果你只是想要试玩这个游戏&#xff0c;可以直接在文章末尾找到源码 由于实现贪吃蛇需要调用Win32 API函数&#xff0c;这些函数我会…

JVM 性能调优 - Java 虚拟机内存体系(1)

Java 虚拟机我们简称为 JVM&#xff08;Java Virtual Machine&#xff09;。 Java 虚拟机在执行 Java 程序的过程中&#xff0c;会管理几个不同的数据区域。如下图所示&#xff1a; 下面我会介绍这几个数据区的特点。 堆 堆区的几个特点&#xff1a; 线程共享。启动时创建堆…

滑块识别验证

滑块识别 1. 获取图片 测试网站&#xff1a;https://www.geetest.com/adaptive-captcha-demo 2. 点击滑块拼图并开始验证 # 1.打开首页 driver.get(https://www.geetest.com/adaptive-captcha-demo)# 2.点击【滑动拼图验证】 tag WebDriverWait(driver, 30, 0.5).until(la…

Spring Boot 整合 Redis 使用教程

作为开发者&#xff0c;相信大家都知道 Redis 的重要性。Redis 是使用 C 语言开发的一个高性能键值对数据库&#xff0c;是互联网技术领域使用最为广泛的存储中间件&#xff0c;它是「Remote Dictionary Service」的首字母缩写&#xff0c;也就是「远程字典服务」。 Redis 以超…

Mac电脑清空特别大型旧文件如何一键清理?

在我们的数字生活中&#xff0c;Mac电脑常常承载着大量个人资料和重要文件。但当我们决定把自己的Mac送给亲人或朋友使用时&#xff0c;面临的首要任务便是彻底且高效地清空所有个人数据&#xff0c;以保证隐私安全。传统的删除方法虽然简单&#xff0c;但往往不能彻底清除所有…

WebSocket+Http实现功能加成

WebSocketHttp实现功能加成 前言 首先&#xff0c;WebSocket和HTTP是两种不同的协议&#xff0c;它们在设计和用途上有一些显著的区别。以下是它们的主要特点和区别&#xff1a; HTTP (HyperText Transfer Protocol): 请求-响应模型&#xff1a; HTTP 是基于请求-响应模型的协…

VXLAN:虚拟化网络的强大引擎

1.什么是VXLAN VXLAN&#xff08;Virtual eXtensible Local Area Network&#xff0c;虚拟扩展局域网&#xff09;&#xff0c;是由IETF定义的NVO3&#xff08;Network Virtualization over Layer 3&#xff09;标准技术之一&#xff0c;是对传统VLAN协议的一种扩展。VXLAN的特…

嵌入式系统:挑战与机遇并存的领域

嵌入式系统&#xff1a;挑战与机遇并存的领域嵌入式系统是一个既具有挑战性又充满前景的领域。要成为一名合格的嵌入式系统工程师&#xff0c;需要经过大量的学习和实践。然而&#xff0c;进入这个领域时&#xff0c;刚入行可能会面临许多困境。让我们一起探讨一下嵌入式系统工…

JAVA反射总结学习

初始反射反射的基本操作反射安全性问题 反射是指在Java运行状态中: 给定一个类对象(Class对象)&#xff0c;通过反射获取这个类对象(Class对象)的所有成员结构&#xff1b; 给定一个具体的对象&#xff0c;能够动态地调用它的方法及对任意属性值进行获取和赋值&#xff1b; …

多维时序 | Matlab实现RF-Adaboost随机森林结合Adaboost多变量时间序列预测

多维时序 | Matlab实现RF-Adaboost随机森林结合Adaboost多变量时间序列预测 目录 多维时序 | Matlab实现RF-Adaboost随机森林结合Adaboost多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现RF-Adaboost随机森林结合Adaboost多变量时间序列预…

专业135+总400+中国科学院大学859国科大信号与系统考研经验电子信息与通信,真题,大纲,参考书

今年考研专业课859信号与系统135&#xff0c;总分400上岸国科大&#xff0c;总结一下自己这一年的复习经验&#xff0c;希望对后面报考中科院大学的同学有所帮助。 专业课&#xff1a; 国科大不同研究所都是统一命题&#xff0c;859信号与系统的参考书目是郑君里的《信号与系…

前后端通讯:前端调用后端接口的五种方式,优劣势和场景

Hi&#xff0c;我是贝格前端工场&#xff0c;专注前端开发8年了&#xff0c;前端始终绕不开的一个话题就是如何和后端交换数据&#xff08;通讯&#xff09;&#xff0c;本文先从最基础的通讯方式讲起。 一、什么是前后端通讯 前后端通讯&#xff08;Frontend-Backend Commun…

【十】【C++】string类的模拟实现

浅拷贝 浅拷贝&#xff08;Shallow Copy&#xff09;是对象复制的一种方式&#xff0c;其中复制对象的过程仅仅复制对象的值&#xff0c;而不复制引用所指向的实际对象或数据。这意味着原始对象和拷贝对象会共享相同的引用或指针指向的数据。 浅拷贝的特点&#xff1a; 共享…

中创ET4410 台式LCR数字电桥 简单开箱测评

最近买了一台LCR电桥&#xff0c;完善一下自己实验室的设备&#xff0c;选了中创ET4410&#xff0c;这款性价比高一点。 1199元在PDD买的&#xff0c;好像胜利的VC4090C也是找中创代工的。 ET4410介绍 本系列LCR数字电桥是采用自动平衡电桥原理设计的元件参数分析仪&#xf…

【Linux】学习-深入了解文件的读与写

深入了解语言级别(C语言)文件操作的"读"与"写" 在学习前&#xff0c;我们先要知道在Linux下的一个原则&#xff1a;一切皆是文件 如何理解呢&#xff1f;举个外设的例子&#xff0c;比如键盘和显示器&#xff0c;这两个外设也可以其实本质上也是文件&…

强敌环伺:金融业信息安全威胁分析——整体态势

从早期的Zeus和其他以银行为目标的特洛伊木马程序&#xff0c;到现在的大规模分布式拒绝服务&#xff08;DDoS&#xff09;攻击&#xff0c;再到新颖的钓鱼攻击和勒索软件&#xff0c;金融服务业已成为遭遇网络犯罪威胁最严重的行业之一。金融服务业的重要性不言而喻&#xff0…

[office] excel如何计算毛重和皮重的时间间隔 excel计算毛重和皮重时间间隔方法 #笔记#学习方法

excel如何计算毛重和皮重的时间间隔 excel计算毛重和皮重时间间隔方法 在日常工作中经常会到用excel&#xff0c;有时需要计算毛重和皮重的时间间隔&#xff0c;具体的计算方式是什么&#xff0c;一起来了解一下吧 在日常工作中经常会到用excel&#xff0c;在整理编辑过磅数据…

Debezium发布历史120

原文地址&#xff1a; https://debezium.io/blog/2022/04/07/read-only-incremental-snapshots/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. Read-only Incremental Snapshots for MySQL April 7, 2022 by K…

软件应用实例分享,电玩计时计费怎么算,佳易王PS5游戏计时器系统程序教程

软件应用实例分享&#xff0c;电玩计时计费怎么算&#xff0c;佳易王PS5游戏计时器系统程序教程 一、前言 以下软件教程以 佳易王电玩计时计费管理系统软件V17.9为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 点击开始计时后&#xff0c;图片…