异步日志:性能优化的金钥匙

news2024/9/23 15:22:48

异步日志:性能优化的金钥匙

一、背景

2024 年 4 月的一个宁静的夜晚,正当大家忙完一天的工作准备休息时,应急群里“咚咚咚”开始报警,提示我们余利宝业务的赎回接口成功率下降。

通过 Monitor 监控发现,该接口的耗时已经超过了网关配置的超时阈值(2s),我们临时调整超时阈值止血后,就在排查问题的根因。具体排查过程不是我这篇文章的重点,故忽略,但最终我们发现最近上线新加的相邻两行的日志中,时间相差近 1.5s,难道这就是问题的根源吗?

后来,我们去掉了这两行日志后紧急发布,事实证明我们的思路是对的。紧急发布后,该接口的耗时由之前的 2s 左右,优化到了 600ms 左右。后来我们分析发现:该接口在打印日志时,由于要实现日志脱敏,故在 Logger.info 入口处实现了脱敏功能,但是大日志脱敏比较耗时,从而导致该接口的同步调用耗时激增到 1.5s 左右(后面我们会说如何解决这个问题)。我的天呐,一行日志竟是性能优化的金钥匙!!!🤣

但这里有一个问题,我们是去掉了日志的打印,侵入了业务,同时该应用还是用的 log4j 的日志框架,log4j 原生框架是不支持日志异步化的,因此要从根本上业务无侵入的解决因日志而导致的性能问题,需要熟悉 log4j2 的框架原理。

二、原理

2.1 Log4j2 的优势

1)性能: Log4j2 使用基于 Lambda 的异步记录器,显著提高了日志记录的速度,减少了日志操作对应用性能的影响。相比之下,Logback 虽然也支持异步记录,但实现上不如 Log4j2 高效。通过减少对象创建、高效的字符串处理和池化技术,Log4j2 在高并发场景下表现更佳。

2)配置灵活性: 支持多种配置方式,包括 XML、JSON、YAML、properties 文件,甚至编程式配置,提供更大的灵活性。动态重新配置能力,允许在不重启应用的情况下修改日志配置。

3)插件架构: Log4j2 采用插件架构,几乎所有组件(如 Appenders、Layouts、Filters)都是可插拔的,易于扩展和自定义。内置丰富的插件库,开箱即用,简化集成过程。

4)内存和资源管理: 更高效的内存管理,减少内存泄漏的风险,尤其是在大量日志输出时。支持垃圾回收友好的设计,比如基于 Disrupter 的 RingBuffer 等数据结构减少 GC 压力。

5)可靠性: 强大的故障恢复机制,如重试和备用 Appenders,确保日志能够被记录下来,即使主要的日志输出目的地不可用。

6)先进的特性:

  • 支持条件日志记录(Conditionals),可以根据运行时条件决定是否记录日志。

  • 自动重新加载配置文件变化,无需重启应用。

  • 支持 JMX 监控和管理日志系统状态。

7)与 SLF4J 的集成:虽然这不是特有优势,但 Log4j2 提供了与 SLF4J(Simple Logging Facade for Java)的良好集成,使得从其他日志框架迁移更加平滑。

总的来说,Log4j2 的设计更现代化,强调高性能、易用性和灵活性,特别是在大规模分布式系统和高性能应用中表现突出。而 Logback 和 Log4j 1.x 虽有各自的优点,但在这些方面逐渐显得力不从心。至于 Java Util Logging (JUL),它是 Java 标准库的一部分,但功能相对基础,配置和扩展性不如 Log4j2 和 Logback 灵活。

2.2 Log4j2 的结构

Log4j2 的结构主要包括以下几个核心组件:

1)Logger: 这是开发者直接使用的接口,用于记录不同级别的日志信息(如 DEBUG, INFO, ERROR 等)。每个 Logger 都有一个名称,并且支持继承性,形成一个名为 Logger Hierarchy 的树状结构,根 Logger 的名称为'root'。

2)LoggerContext: 是日志系统的上下文环境,管理着一组 Logger 实例以及它们的配置。每个应用程序通常只有一个 LoggerContext,但它支持多个上下文以实现更细粒度的控制。

3)Configuration: 每个 LoggerContext 都关联一个有效的 Configuration,定义了日志的输出目的地(Appenders)、日志的过滤规则(Filters)、日志的格式化方式(Layouts)等。Configuration 可以通过配置文件(如 XML、JSON、properties)或编程方式动态加载。

4)Appender: 负责将日志事件发送到指定的目标,如控制台(Console)、文件(File)、数据库、网络 Socket 等。

5)Layout: 定义了日志信息的格式化方式,如模式字符串(Pattern String)决定了日期、时间、日志级别、线程名、日志信息等内容的排列和格式。

6)Filter: 可以在日志事件从 Logger 传递到 Appender 的过程中进行过滤,根据特定条件决定日志是否被输出。

7)Lookup: 提供动态值解析机制,如 ${ctx:variable}可以在日志中插入上下文变量的值。

那么,Log4j2 的日志是怎么将日志输出到文件/数据库/控制台等地方的?

2.3 Log4j2 日志输出流程

关键步骤源码分析:

1)1.1 主要是针对日志级别 Level 和指定的全局 Filter 组件进行过滤

2)ReliabilityStrategy 是 Log4j2 的日志可靠性策略实现,目前主要有以下四种:

  • AwaitCompletionReliabilityStrategy: 等待日志接收完成策略。这种策略主要是在应用关闭时,尽可能要等应用日志接收完成后再结束 Appender 的生命周期(这种策略只是说尽可能所有日志等待调用 Appender.append 方法完成,但在异步日志场景下,Appender.append 其实是落了 ringbuffer 或者其他队列里,实际上未持久化。因此该策略是尽可能保证接收完成而非处理完成)

  • AwaitUnconditionallyReliabilityStrategy: 无条件等待策略。这种策略会在 rootLogger 关闭时无条件等待一段时间,具体等待时间可以配置 log4j2.component.properties 文件的 log4j.waitMillisBeforeStopOldConfig 属性。

  • DefaultReliabilityStrategy: 默认策略。该策略不做任何等待。

  • LockingReliabilityStrategy: 锁等待策略。该策略当正在写入日志时,则会等待;否则即会停止等待。

3)1.2.1.3.1append 操作是将日志写入到对应的目的地,如 kafka、本地文件、邮件等。这里如果是异步日志,则会将日志追加到异步队列里,进而提高日志记录的性能。

4)1.2.1.3.1.1 调用 Layout encode 日志,是根据 log4j2.xml 中配置的 Layout 对日志进行格式化输出。

那么如果有一些个性化的日志输出需求,log4j2 能否帮我们实现?

2.4 如何实现日志脱敏

上面提到了 log4j2 的各种组件以及日志输出流程,log4j2 的强大很大程度上得益于其清晰且高度解耦的架构设计。例如其具有很强的扩展性,log4j2 的很多组件都可以自己定制插件,如:Appender、Filter、Layout 等。那么这里我结合我们实际业务中一个很常见的 case 去分析如何定制一个组件。

首先,作为一家强监管的金融公司,日志脱敏涉及数据保护和隐私安全等问题,非常重要。过去我们很多业务系统在实现业务脱敏时,很容易想到在打日志的入口统一封装一个格式化方法,造成日志输出在无形中把异步日志输出变成了同步输出(日志脱敏的耗时往往比日志集中持久化到磁盘耗时要高)。

那么如何优雅的实现日志脱敏的功能,既能实现其功能又可以保证日志的性能,是 log4j2 插件化的一个很重要的应用场景。前面我们提到日志输出流程中会使用 Layout encode 日志,而 PatternConverter 是 Layout 非常重要的组成部分。其通过定义一系列的占位符(如 %d、%m、 %t 等)帮助我们自定义格式输出日志对象,同时 PatternConverter 支持以高度可定制的插件集成到 Log4j2 框架中,因此我们可以借助其去定制脱敏组件。

话不多说,我们直接上日志脱敏 PatternConverter 插件源码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import java.util.Arrays;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.message.FormattedMessage;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MessageFormatMessage;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.message.StringFormattedMessage;
import org.apache.logging.log4j.util.PerformanceSensitive;

/**
 * @author baichun
 * @version ShieldMessagePatternConverter.java, v 0.1 2024年04月09日 21:13 baichun
 */
@Plugin(
        name = "ShieldPatternConverter",
        category = "Converter"
)
@ConverterKeys({"shield", "sd", "shieldMessage", "sm"})
@PerformanceSensitive({"allocation"})
public final class ShieldMessagePatternConverter extends LogEventPatternConverter {
    private final String[] options;

    private ShieldMessagePatternConverter(String[] options) {
        super("Shield", "shield");
        this.options = options == null ? null : (String[])Arrays.copyOf(options, options.length);
    }

    //必须要有newInstance方法,log4j2会调用该方法进行初始化
    public static ShieldMessagePatternConverter newInstance(String[] options) {
        return new ShieldMessagePatternConverter(options);
    }


    @Override
    public void format(LogEvent logEvent, StringBuilder output) {
        Message message = logEvent.getMessage();
        String format = message.getFormat();

        if (isFormatMessage(message)) {
            //在这里格式化脱敏日志
            String msgInfo = ShieldUtils.format(format, message.getParameters());

            output.append(msgInfo);
        } else {
            output.append(message.getFormattedMessage());
        }
    }

    private boolean isFormatMessage(Message message) {
        return message instanceof ParameterizedMessage || message instanceof StringFormattedMessage
                || message instanceof FormattedMessage || message instanceof MessageFormatMessage;
    }
}

定义好组件后,log4j2 即能够自动扫描识别到,不需要其他定义和配置。接下来看看如何使用。ConverterKeys 这个注解指定了在 log4j2.xml 中应如何使用该插件。以下是 log4j2.xml 应用示例:

    <RollingFile name="TEST_APPENDER" fileName="test.log"                     filePattern="test.log.%d{yyyy-MM-dd}"                     append="true">            <!-- %sm即为脱敏组件 -->            <PatternLayout pattern="%d %sm%n" charset="UTF-8"/>            <TimeBasedTriggeringPolicy/>            <DefaultRolloverStrategy/>        </RollingFile>

2.5 Log4j2 的异步日志

异步日志原理概述

前面提到了 Log4j2 的高可扩展性,同时 Log4j2 的性能也是极高的,下面是 Log4j2 官方的 benchmark 数据,仅供参考:

Log4j2 之所以性能如此之高,其中一个很重要的原因就是其基于 Disrupter 的环形缓冲区的无锁化结构 Ringbuffer 设计。Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,基于 Disruptor 开发的系统单线程能支撑每秒 600 万订单。目前,包括 Apache Strom、Log4j2 在内的很多知名项目都应用了 Disruptor 来获取高性能。关于 Disruptor 的原理,这里不再赘述,大家可以自行查阅:https://lmax-exchange.github.io/disruptor/#_what_is_the_disruptor

Disrupter 组件构成:

Disrupter 性能测试结果

如何使用异步日志

log4j2 开启异步日志的方法主要有以下两种方式:

1)全局异步日志

  • 通过 JVM 启动参数来全局启用异步日志功能。在启动应用程序时,向 JVM 传递以下系统属性:

 -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  • 在类路径(classpath)中添加一个名为 log4j2.component.properties 的文件,并包含以下内容(这个文件会在 Log4j2 初始化时被读取):

-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

复制代码

这两种方式下,所有 Logger 都会自动使用异步处理。

2)混合异步日志

在 log4j2.xml 配置文件中,可以手动指定特定的 Logger 使用异步处理,通过将 <Root>或<Logger>元素替换为<AsyncRoot>或<AsyncLogger>。例如:


<Configuration status='WARN'>
<Appenders>
        ... <!-- your appenders here -->
</Appenders>
<Loggers>
<AsyncRoot level='info' includeLocation='false'>
<AppenderRef ref='yourAppenderName'/>
</AsyncRoot>
<!-- 或者为特定logger配置 -->
<AsyncLogger name='com.example.MyClass' level='debug'>
<AppenderRef ref='yourAppenderName'/>
</AsyncLogger>
</Loggers>
</Configuration>

异步日志的潜在问题及解决方案

  • 潜在问题:

  1. 日志丢失问题:如果机器发生意外重启、发布、掉电导致的 jvm 进程停止,停留在队列的未来得及输出到目的地的 LogEvent 可能会丢失

  2. 日志顺序问题:由于日志事件是在不同的线程中异步处理的,因此日志条目可能不会严格按照它们产生的顺序出现在日志文件中,这对于需要严格按时间顺序追踪日志的应用可能是个问题。

  3. 其他问题:如增加资源损耗、配置复杂度和调试复杂度等问题

  • 解决方案:

  1. 对于日志丢失问题:

  • 原生 Log4j2 有完整的生命周期管理,并监听了 jvm 关闭的事件。当 jvm 关闭时,Log4j2 会监听 Disrupter 队列中的 RingbufferLogEvent 数量,直到日志打印完(或超时)才释放关闭 Log4j2,jvm 才得以正常关闭。但是自然灾害或者机房掉电等不可抗力因素,无法避免丢失问题。

  • 我们基于 Log4j2 定制的 AsyncAbleRollingFileAppender,其中有独立的 Disrupter,且不在 Log4j2 生命周期管理当中,存在日志丢失风险。可以采用类似方案解决:

try {
            LoggerContextFactory factory = LogManager.getFactory();
            if (!(factory instanceof Log4jContextFactory)) {
                return;
            }
            Log4jContextFactory log4jContextFactory = (Log4jContextFactory) factory;
            ShutdownCallbackRegistry registry = log4jContextFactory.getShutdownCallbackRegistry();
            if (!(registry instanceof DefaultShutdownCallbackRegistry)) {
                return;
            }
            DefaultShutdownCallbackRegistry defaultShutdownCallbackRegistry = (DefaultShutdownCallbackRegistry) registry;
            Field hooksField = DefaultShutdownCallbackRegistry.class.getDeclaredField("hooks");
            hooksField.setAccessible(true);
            Collection<Cancellable> hooks = (Collection<Cancellable>) hooksField.get(defaultShutdownCallbackRegistry);
            Collection<Cancellable> newHooks = new CopyOnWriteArrayList<>();
            //将对Appender的队列消费监听和卸载放在首要位置,避免log4j2关闭后再卸载Appender
            newHooks.add(new Log4j2Cancellable(() -> {
               //负责监听AsyncAbleRollingFileAppender的队列消费情况,并在消费完成后关闭AsyncAbleRollingFileAppender
                new AppenderUnInstaller(register).run();
            }));
            newHooks.addAll(hooks);
            hooksField.set(defaultShutdownCallbackRegistry, newHooks);
        } catch (NoSuchFieldException e) {
            // This catch statement is intentionally empty
        } catch (IllegalAccessException e) {
            // This catch statement is intentionally empty
        }
 
  • AsyncAbleRollingFileAppender 使用独立的 disrupter,且 RingBufferLogEvent 未及时清理对象,容易导致内存泄漏,异步日志场景请慎用。

  1. 对于日志顺序性问题:

  • 异步线程池大小设置为 1,但是会影响日志打印的速度(现在的普遍做法)。

  • 延迟打印

三、效果

4 月份的这一问题发生后,我们从原理出发,对理财的核心应用做了升级和优化,整体服务耗时上取得了不错的性能优化效果。

应用 rpc 耗时:

应用网关耗时:

但与此同时,我们也发现升级后,应用的 fgc 次数更多了,经过 heapdump 分析后,发现 AsyncAbleRollingFileAppender 内部实现的 RingBufferLogEvent 执行后,不会释放引用的 LogEvent,导致 Disrupter 一直持有已打印的 LogEvent 的引用关系,进而导致了内存泄漏。后来,我们采取主动释放对象引用(RingBufferLogEvent.setLogEvent(null))优化的方案,发布以后前后 fgc 对比如下:

GC 优化前:

GC 优化后:

四、建议

日志作为诊断问题、监控系统健康状况与优化服务效能不可或缺的一环,其重要性不言而喻。熟练掌握并有效利用如 Log4j2 这样的高性能日志框架以及注意一些打印日志的策略(如动静分离、合理分割、合理设置日志级别等),对于开发者而言至关重要:

1)动静分离 :在一些大日志输出场景中,即使是异步日志也会给系统带来性能风险。因此建议合理识别大日志中的动态数据和静态数据。静态数据定时输出,动态数据关联唯一静态标识输出,在降低性能风险的同时又满足监控分析的需要;

2)合理分割 :日志文件需要合理分割,并设置合理的保留策略,及时释放磁盘空间。

3)合理设置日志级别 :避免日志滥用,尤其是 debug 日志,既有利于日志定位问题的速度,又能提高性能。

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

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

相关文章

【验收支撑】软件系统验收计划书(直接套用原件doc)

编写软件验收计划是软件开发过程中的一个关键步骤&#xff0c;其重要性体现在以下几个方面&#xff1a; 明确验收标准&#xff1a;软件验收计划详细列出了验收的标准、测试方法、测试环境等&#xff0c;确保所有相关人员对验收的期望和要求有清晰的认识。这有助于避免在验收阶段…

JavaWeb系列二十一: 数据交换和异步请求(JSON, Ajax)

文章目录 官方文档official documents官方文件官方文件official documentsJSON介绍JSON快速入门JSON对象和字符串对象转换应用案例注意事项和细节 JSON在java中使用说明JSON在Java中应用场景应用实例 Ajax基本介绍Ajax是什么Ajax经典应用场景 Ajax原理示意图传统的web应用Ajax原…

用微客云搭建一套外卖霸王餐系统赚CPS佣金

在当下数字化快速发展的时代&#xff0c;外卖行业作为餐饮业的重要分支&#xff0c;正在经历着前所未有的变革。为了满足市场需求&#xff0c;提高用户体验和增加商户收入&#xff0c;越来越多的外卖平台开始寻求创新&#xff0c;其中&#xff0c;搭建一套高效、稳定且功能丰富…

昇思25天学习打卡营第19天|CycleGAN图像风格迁移互换

CycleGAN图像风格迁移互换 模型介绍 模型简介 CycleGAN(Cycle Generative Adversarial Network) 即循环对抗生成网络&#xff0c;来自论文 Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks 。该模型实现了一种在没有配对示例的情况下学习将…

jenkins打包java项目报错Error: Unable to access jarfile tlm-admin.jar

jenkins打包boot项目 自动重启脚本失败 查看了一下项目日志报错&#xff1a; Error: Unable to access jarfile tlm-admin.jar我检查了一下这个配置&#xff0c;感觉没有问题&#xff0c;包可以正常打&#xff0c; cd 到项目目录下面&#xff0c;手动执行这个sh脚本也是能正常…

本地Kali系统开启SSH服务并使用内网穿透生成公网地址实现ssh远程连接

文章目录 前言1. 启动kali ssh 服务2. kali 安装cpolar 内网穿透3. 配置kali ssh公网地址4. 远程连接5. 固定连接SSH公网地址6. SSH固定地址连接测试 前言 本文主要介绍如何在本地Kali Linux系统启动ssh服务&#xff0c;并结合cpolar内网穿透软件生成公网地址&#xff0c;轻松…

提示词工程(Prompt Engineering)是什么?

一、定义 Prompt Engineering 提示词工程&#xff08;Prompt Engineering&#xff09;是一项通过优化提示词&#xff08;Prompt&#xff09;和生成策略&#xff0c;从而获得更好的模型返回结果的工程技术。 二、System message 系统指令 System message可以被广泛应用在&am…

平凯星辰黄东旭出席 2024 全球数字经济大会 · 开放原子开源数据库生态论坛

7 月 5 日&#xff0c;以“开源生态筑基础&#xff0c;数字经济铸未来”为主题的 2024 全球数字经济大会——开放原子开源数据库生态论坛在北京成功举办。平凯星辰&#xff08;北京&#xff09;科技有限公司联合创始人黄东旭发表了题为《TiDB 助力金融行业关键业务系统实践》的…

【TS】typescript 获取函数入参类型、返回值类型、promise返回值类型

文章目录 1. 准备工作2. 获取函数入参的类型3. 获取函数返回值类型4. 获取promise返回值类型 1. 准备工作 创建 utils.ts interface User {id: number;name: string;age: number; } interface Params {method: string;url: string; }function getUserList(params: Params,other…

RocketMQ 消费者之顺序消费和流程详解附源码解析

1. 背景 本文是 RocketMQ 消费者系列的第六篇&#xff0c;上一篇主要介绍并发消费&#xff0c;而本片主要介绍 RocketMQ 顺序消费的设计和流程。 我把 RocketMQ 消费分成如下几个步骤 重平衡 消费者拉取消息 Broker 接收拉取请求后从存储中查询消息并返回 消费者消费消息 顺序…

算法学习day10(贪心算法)

贪心算法&#xff1a;由局部最优->全局最优 贪心算法一般分为如下四步&#xff1a; 将问题分解为若干个子问题找出适合的贪心策略求解每一个子问题的最优解将局部最优解堆叠成全局最优解 一、摆动序列&#xff08;理解难&#xff09; 连续数字之间的差有正负的交替&…

GO channel 学习

引言 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。 虽然可以使用共享内存进行数据交换&#xff0c;但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性&#xff0c;必须使用互斥量对内存进行加锁&#…

视频语音转文字工具用哪个好?推荐6款优质的视频转文字工具

在沉浸于电影情节时&#xff0c;周遭的喧嚣往往成了享受视听的障碍&#xff0c;这时&#xff0c;字幕的重要性便不言而喻。 字幕的作用远不止于此&#xff0c;它是听力受限观众的桥梁&#xff0c;也是语言学习者的得力助手。幸运的是&#xff0c;将视频语音转文字字幕现已变得…

红酒与未来科技:传统与创新的碰撞

在岁月的长河中&#xff0c;红酒以其深邃的色泽、丰富的口感和不同的文化魅力&#xff0c;成为人类文明中的一颗璀璨明珠。而未来科技&#xff0c;则以其迅猛的发展速度和无限的可能性&#xff0c;领着人类走向一个崭新的时代。当红酒与未来科技相遇&#xff0c;一场传统与创新…

电脑自动重启是什么原因呢?99%人都不知道的解决办法,直接打破循环

当你的电脑突然毫无预警地自动重启&#xff0c;不仅打断了工作流程&#xff0c;还可能导致未保存的数据丢失&#xff0c;这无疑是一件令人沮丧的事情。那么&#xff0c;电脑自动重启是什么原因呢&#xff1f;有什么方法可以解决呢&#xff1f;别担心&#xff0c;在大多数情况下…

对象与键值对数组的相互转换Object.entries与Object.fromEntries

Object.entries是JavaScript中的一个内置方法&#xff0c;它可以将一个对象的属性和值转换为一个包含键值对的数组。 let obj {name: mike,age: 18,sex: man } Object.entries(obj)Object.entries的使用场景: 1、动态更新对象属性 let obj {name: mike, age: 18, sex: man…

《昇思25天学习打卡营第02天|qingyun201003》

日期 心得 通过这次的学习&#xff0c;主要是了解过张量的基础概念&#xff0c;同时也知道有关构造张量的方法。通过索引查询张量&#xff0c;张量的运算。通过concat\stack 将张量进行维度链接。Tensor与NumPy的互相转换&#xff0c;但是我似乎并不了解什么它们的概念。也认知…

电脑视频去水印软件哪个好用,电脑上视频去水印的软件

在数字化时代&#xff0c;视频创作已成为许多人展示才华和创意的重要途径。然而&#xff0c;视频中的水印常常让人感到头疼&#xff0c;尤其是当水印影响了视频的整体美观时。本文将为你揭秘如何在电脑上使用各种软件去除视频水印&#xff0c;让你的作品更加专业&#xff01; 方…

时光穿梭机:AI如何让老照片焕发新生,跃然“动”起来

在岁月的长河中&#xff0c;每一张老照片都是时间的低语&#xff0c;承载着过往的记忆与温情。它们静静地躺在相册的角落&#xff0c;或是泛黄的相纸上&#xff0c;定格了某个瞬间的欢笑与泪水&#xff0c;却也因此失去了那份生动的活力。然而&#xff0c;随着人工智能&#xf…

如何做一个迟钝不受伤的打工人?

一、背景 在当前激烈的职场环境中&#xff0c;想要成为一个相对“迟钝”且不易受伤的打工人&#xff0c;以下是一些建议&#xff0c;但请注意&#xff0c;这里的“迟钝”并非指智力上的迟钝&#xff0c;而是指在应对复杂人际关系和压力时展现出的豁达与钝感力&#xff1a; 尊重…