分类 其它 下的文章

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Target-Aware Language Modeling via Granular Data Sampling

创新点


  • 提出了一种将预先训练好的标记符与多粒度标记符合并的算法,生成高效的
    n-gram
    特征,而且与下游任务的性能有很高的相关性。
  • 利用上述研究成果,改进了基于重要性的数据采样技术,将通用词汇集调整为目标词汇集。这样就能更好地代表数据,提高模型在目标任务中的性能,同时在非目标任务中保持良好的性能。

内容概述


语言模型的预训练通常针对广泛的使用场景,并结合来自多种来源的数据。然而,有时模型需要在特定领域中表现良好,同时又不影响其他领域的性能。这就需要使用数据选择方法来确定潜在核心数据,以及如何有效地对这些选定数据进行抽样训练。

论文使用由多粒度标记组成的
n-gram
特征进行重要性抽样,这在句子压缩和表征能力之间取得了良好的平衡。抽样得到的数据与目标下游任务性能之间有很高的相关性,同时保留了其在其他任务上的有效性,使得语言模型可以在选定文档上更高效地进行预训练。

在八个基准测试中,在使用约
1%
的数据时,预训练模型的表现与完整的
RefinedWeb
数据相当,并且在模型规模范围为
125M

1.5B
时,超越了随机选择的样本。

方法


从大规模数据集(如
RefinedWeb
)中选择样本是缓慢且昂贵的,一个可行的解决方案是使用容易计算的
n-gram
特征将每个文档编码为向量。

假设从目标分布
\(p\)
中获取了一小部分目标文本示例
\(D_{task}\)
,以及从分布
\(q\)
中获取的大量原始数据集
\(D_{raw}\)
,其中包含
\(N\)
个示例,目标是从原始数据集中选择
\(k\)
个示例(
\(k \ll N\)
),这些示例与目标相似。

重要性采样

重要性采样技术选择与目标分布对齐的示例,为每个文本提供可处理的重要性估计,并在提供必要结构的特征空间
\({\mathbb{Z}}\)
上应用重要性采样。

特征提取器
\(h: {\mathbb{X}} \rightarrow {\mathbb{Z}}\)
用于转换输入为特征,得到的原始特征分布
\(q_{\text{feat}}\)
和目标特征分布
\(p_{\text{feat}}\)
,目标是选择特征与目标特征分布
\(p_{\text{feat}}\)
对齐的数据。

为了提取特征
\(q_{\text{feat}}\)

\(p_{\text{feat}}\)
,从每个分词文档中提取
n-grams
。每个
n-gram
被映射到哈希表中的一个键,每个键映射到
n-gram
计数。将从
\(N\)
个原始示例中获得的每个特征
\(z_i = h(x_i)\)
计算重要性权重,权重为
\(w_i = \frac{\hat{p}_{\text{feat}}(z_i)}{\hat{q}_{\text{feat}}(z_i)}\)

最后进行采样,从一个分布中选择
\(k\)
个示例,且不进行替换,其概率由
\(\frac{w_i}{\sum_{i=1}^N w_i}\)
给出。

分词器适配

为了推导目标词汇
\(V(t)\)
,使用
Llama-3
分词器的词汇
\(V_{start}\)
作为起点,并将
\(V_{start}\)
与从任务数据
\(D_{task}\)
中学习到的
\(V_{task}\)
合并。在构建
\(V_{task}\)
时,确保包含多粒度的标记(即单词和多词组合),然后将
\(V_{task}\)

\(V_{start}\)
合并形成
\(v(t - 1)\)

接下来,逐步从
\(v(t - 1)\)
中移除标记,以获得
\(v(t)\)
,在此过程中,最小化与原始词汇集的距离,以便提取更少偏倚的文档特征作为
n-gram
向量。

首先定义一个度量来衡量语料库中词汇集的质量,然后通过最大化词汇效用度量 (
\(\mathcal{H}_{v}\)
) 来学习最佳词汇,该度量的计算公式为:

\[\begin{equation}
\mathcal{H}_{v} = - \frac{1}{l_{v}}\sum_{j \in v } P(j)\log P(j),
\end{equation}
\]

其中,
\(P(j)\)
是来自目标数据的标记
\(j\)
的相对频率,而
\(l_{v}\)
是词汇
\(v\)
中标记的平均长度。对于任何词汇,其熵得分
\(\mathcal{H}_{v}\)
基于其前一步的词汇进行计算,优化问题可以表述为:

\[\begin{equation}
\text{arg\ min}_{v(t-1), v(t)} \big [ \mathcal{H}{v(t)} - \mathcal{H}{v(t-1)} \big ]
\end{equation}
\]

其中,
\(v(t)\)

\(v(t - 1)\)
是包含所有词汇的两个集合,大小的上限分别为
\(|v(t)|\)

\(|v(t - 1)|\)
。设置
\(|v(t)| = 10k\)
,其中
\(t=10\)
,而
\(|v(0)|\)
是默认的
Llama-3 tokenizer
的词汇大小。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

Java系列

Java核心知识体系1:泛型机制详解
Java核心知识体系2:注解机制详解
Java核心知识体系3:异常机制详解
Java核心知识体系4:AOP原理和切面应用
Java核心知识体系5:反射机制详解
Java核心知识体系6:集合框架详解
Java核心知识体系7:线程不安全分析
Java核心知识体系8:Java如何保证线程安全性
Java核心知识体系9-并发与多线程:线程基础

在Java程序开发中,线程管理是一个至关重要的方面。它涉及到如何有效地创建、调度、同步和销毁线程,以确保程序的性能、响应性和稳定性。以下是对Java线程管理的详细探讨。

1 线程的基本概念

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。每个线程都有一个独立的执行路径,但共享进程的资源,如内存和文件句柄。在Java中,线程可以通过继承
Thread
类或实现
Runnable
接口来创建。

此外,
Java 5
开始,引入了
java.util.concurrent
包,提供了更多的并发工具,如Callable接口与Future接口,它们主要用于任务执行。

2 线程的创建与启动

2.1 继承Thread类

  • 创建一个类继承自
    Thread
    类。
  • 重写
    run()
    方法,该方法包含了线程要执行的任务。
  • 创建该类的对象,并调用
    start()
    方法启动线程。
class MyThread extends Thread {
    public void run() {
        System.out.println("线程运行中");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // 调用start()方法来启动线程
    }
}

2.2 实现Runnable接口

  • 创建一个类实现
    Runnable
    接口。
  • 实现
    run()
    方法,该方法同样包含了线程要执行的任务。
  • 将该类的对象作为参数传递给
    Thread
    类的构造函数,创建
    Thread
    对象。
  • 调用
    Thread
    对象的
    start()
    方法启动线程。
class MyRunnable implements Runnable {
    public void run() {
        System.out.println("线程运行中");
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 调用start()方法来启动线程
    }
}

3 线程的同步与通信

由于多个线程可能会同时访问共享资源,因此需要使用同步机制来确保数据的正确性和一致性。Java提供了多种同步机制,如
synchronized
关键字、
wait()

notify()
方法、以及
ReentrantLock
等。

3.1 synchronized关键字

  1. 可以用于方法或代码块上,以确保同一时刻只有一个线程能够执行该方法或代码块。
  2. 当一个线程持有某个对象的锁时,其他线程将无法访问该对象的同步方法或代码块,直到锁被释放。
public class SynchronizedExample {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedExample example = new SynchronizedExample();

        // 创建多个线程来测试同步
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        t1.start();
        t2.start();

        // 等待线程执行完毕
        t1.join();
        t2.join();

        // 输出最终结果
        System.out.println("Final count: " + example.getCount()); // 最终输出2000
    }
}

3.2 wait()和notify()方法

这两个方法用于在线程之间进行通信。

  1. wait()
    方法使当前线程等待,直到其他线程调用
    notify()

    notifyAll()
    方法唤醒它。
  2. notify()
    方法唤醒一个等待该对象的线程(如果有多个线程在等待,则选择其中一个),而
    notifyAll()
    方法唤醒所有等待该对象的线程。

image

# 先写后读
public class WaitNotifyExample {
    private final Object lock = new Object();
    private boolean ready = false;

    public void writer() throws InterruptedException {
        synchronized (lock) {
            // 模拟写操作
            Thread.sleep(1000); // 假设写操作需要1秒
            System.out.println("Data is ready");
            ready = true;
            lock.notify(); // 唤醒等待的线程
        }
    }

    public void reader() throws InterruptedException {
        synchronized (lock) {
            while (!ready) {
                lock.wait(); // 等待数据准备好
            }
            // 读取数据
            System.out.println("Data has been read");
        }
    }

    public static void main(String[] args) {
        WaitNotifyExample example = new WaitNotifyExample();

        Thread writerThread = new Thread(example::writer);
        Thread readerThread = new Thread(example::reader);

        writerThread.start();
        readerThread.start();
    }
}

3.3 ReentrantLock

  1. 提供了比
    synchronized
    更灵活的锁机制。
  2. 可以显式地加锁和解锁,还支持公平锁和非公平锁等特性。

四、线程的生命周期与状态

Java线程在其生命周期中会经历多种状态,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed Waiting)和终止(Terminated)。

  • 新建(New)
    :线程被创建但尚未启动。
  • 就绪(Runnable)
    :线程已启动且正在等待CPU分配时间片。
  • 运行(Running)
    :线程正在执行其任务。
  • 阻塞(Blocked)
    :线程因等待某个条件而暂时停止执行。
  • 等待(Waiting)
    :线程因调用
    wait()
    方法而等待其他线程唤醒。
  • 超时等待(Timed Waiting)
    :线程在等待某个条件的同时还设置了一个超时时间。
  • 终止(Terminated)
    :线程已完成任务并退出。

image

5 线程池

为了更有效地管理线程,Java提供了线程池机制。线程池是一种用于管理和复用线程的框架,它允许开发者以较小的开销来创建和管理大量的线程。Java中的
ExecutorService
接口及其实现类(如
ThreadPoolExecutor
)提供了强大的线程池功能。
Java中提供了几种常见的线程池类型,包括:

  1. FixedThreadPool(固定大小线程池):包含固定数量的线程,适用于需要限制并发线程数量的场景。
  2. CachedThreadPool(缓存线程池):不固定线程数量,可以根据需要自动创建新线程,适用于短期异步任务。
  3. SingleThreadPool(单线程池):只包含一个工作线程,保证所有任务按顺序执行,适用于需要保持任务顺序执行的场景。
  4. ScheduledThreadPool(定时线程池):可以执行定时任务和周期性任务。
  5. WorkStealingPool(工作窃取线程池):Java 8中引入的一种新类型的线程池,主要用于处理耗时任务,适用于需要大量并行任务、任务之间没有依赖关系的情况。

在后续的章节里面,我们会专门来详细介绍下线程池的使用

6 最佳实践

  • 避免创建过多的线程
    :过多的线程会导致上下文切换频繁,从而降低系统性能。
  • 合理设置线程优先级
    :根据任务的紧急程度和重要性来设置线程的优先级。
  • 使用线程安全的集合
    :在多线程环境下使用线程安全的集合来避免数据不一致的问题。
  • 避免死锁
    :在设计多线程程序时要特别注意避免死锁的发生。

综上所述,Java线程管理是一个复杂而重要的领域。通过合理地创建、调度、同步和销毁线程,可以显著提高程序的性能、响应性和稳定性。同时,开发者还需要遵循一些最佳实践来避免常见的问题和陷阱。

Cone

Manim
中专门用于创建和操控
锥形几何对象
的类。

Cone
允许用户定义锥体的底面半径、高度、颜色、不透明度等属性,并提供了一系列方法来操控这个锥体,如移动、缩放、旋转等。

通过这些属性和方法,用户可以灵活地创建出符合自己需求的锥形对象,并将其融入到动画或演示中。

1. 主要参数

Cone
的主要参数有:

参数名称 类型 说明
base_radius float 圆锥底部的半径
height float 圆锥的高度,也就是圆锥从底面中心到顶点的垂直距离
direction np.ndarray 圆锥的朝向
show_base bool 是否显示圆锥的底面
v_range list[float] 圆锥纵向扫描的范围,用于控制圆锥侧面的纵向形状
u_min float 圆锥横向扫描的最小角度,影响圆锥侧面的横向分布
checkerboard_colors bool 是否使用棋盘格颜色模式,有助于在视觉上区分圆锥的不同部分

这些参数允许用户灵活地创建和自定义圆锥体对象。

例如,

  • 通过调整
    base_radius

    height
    参数,可以改变圆锥的大小和形状;
  • 通过
    direction
    参数,可以控制圆锥的朝向;

  • show_base

    v_range

    u_min

    checkerboard_colors
    参数则提供了对圆锥视觉表现的进一步定制。

2. 主要方法

Cone
类有两个自己特有的改变方向的方法:

名称 说明
get_direction 用于获取圆锥体的方向向量。这个方向向量通常是一个三维向量,表示圆锥体在空间中的朝向。
set_direction 用于设置圆锥体的方向向量。通过传入一个新的三维向量,可以改变圆锥体在空间中的朝向。

3. 使用示例

下面的示例将展示如何创建圆锥体对象、设置其参数、以及调用其方法来改变圆锥体的属性或进行动画。

3.1. 基本的圆锥体

本示例展示了如何在
Manim
场景中创建一个基本的圆锥体。

圆锥体具有指定的底面半径和高度,并且默认方向朝上。

# 创建一个底面半径为1,高度为2的圆锥体,
# 方向朝上(默认方向)
cone = Cone(base_radius=1, height=2)

3.2. 自定义圆锥体

在此示例中,创建了一个圆锥体,并对其进行了自定义设置。

这包括设置圆锥体的
填充颜色

边框颜色

边框宽度
,以及将其移动到场景中的
特定位置

这样,圆锥体不仅具有独特的外观,还位于用户指定的位置。

# 创建一个底面半径为1.5,高度为3的圆锥体,
# 设置填充颜色和边框颜色
cone = Cone(
    base_radius=1.5,
    height=3,
    fill_color=RED,
    stroke_color=YELLOW,
    stroke_width=2,
)
# 将圆锥体移动到场景中的特定位置
cone.move_to(np.array([1, 1, 1]))

3.3. 旋转和缩放

这个示例演示了如何为圆锥体添加动画效果。

首先,创建了一个圆锥体,并随后应用了两种动画:旋转和缩放。

圆锥体先绕y轴旋转了一定角度,然后按比例放大。

# 创建一个底面半径为1,高度为2的圆锥体
cone = Cone(
    base_radius=1,
    height=2,
    fill_color=BLUE,
)
# 将圆锥体添加到场景中
self.play(Create(cone))
# 创建旋转动画,绕y轴旋转90度
self.play(
    Rotate(
        cone,
        axis=Y_AXIS,
        angle=PI / 2,
    ),
)
# 创建缩放动画,将圆锥体放大1.5倍
self.play(cone.animate.scale(1.5))

3.4. 复制和排列

此示例展示了如何复制圆锥体并将其排列成一行。

首先,创建了一个圆锥体作为模板,然后使用循环创建了多个该圆锥体的副本,并将它们稍微移动位置以排列成一行。

# 创建一个底面半径为0.5,高度为1的圆锥体作为模板
cone_tpl = Cone(
    base_radius=0.5,
    height=1,
    fill_color=GREEN,
)
# 创建一个圆锥体数组,
# 每个圆锥体都是模板的复制,并稍微移动位置
cones = VGroup(
    *[cone_tpl.copy().move_to([i, 0, 0]) for i in range(5)]
)
# 将圆锥体数组添加到场景中
self.play(Create(cones))
# 整个圆锥体数组一起移动
self.play(cones.animate.shift(LEFT * 3))

4. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
cone.py
),

下载地址:
完整代码
(访问密码: 6872)

在当今复杂的 IT 环境中,实时监控与可视化对于保障系统的稳定运行和性能优化至关重要。 无论是服务器、应用程序,还是网络设备,及时获取性能数据能够帮助我们快速定位问题、优化资源配置。

Netdata
,作为一个开源的实时监控工具,正是为此而生。
Netdata
不仅是一个轻量级的监控与可视化平台,更是一个功能强大且易于使用的实时性能监测工具,凭借其强大的功能和炫酷的界面,已经在GitHub上获得超过了72K的星标。

一、Netdata介绍

Netdata
是一款开源的、轻量级的实时性能监控和可视化工具。它能够监控服务器、容器、应用程序和物联网设备,为用户提供实时、详细的性能数据。可以运行在多种操作系统上,包括 Linux、FreeBSD、macOS 等。其设计初衷是为了让系统管理员和开发人员能够快速、直观地了解系统的运行状态,从硬件层面到各种应用程序和服务的性能指标都能尽收眼底。

Netdata能够帮助用户实时监测系统的性能指标,提供丰富的可视化界面,使用户可以轻松查看各类指标,包括CPU使用率、内存占用、磁盘IO、网络流量等。

Netdata以秒为单位收集和展示数据,让用户对系统状态有直观的了解。通过友好的Web界面,Netdata将复杂的数据以图表的形式呈现,使得用户能够轻松识别趋势和异常。无论是开发者、运维人员,还是数据分析师,都能从中受益。

二、Netdata 的核心功能

1、广泛的监控范围

1) 系统资源监控

Netdata 可以对 CPU、内存、磁盘 I/O、网络等基本的系统资源进行实时监控。对于 CPU,它不仅能显示使用率,还能细分到每个核心的使用情况,帮助我们快速定位是否存在某个核心负载过高的问题。在内存监控方面,能够详细呈现内存的使用量、可用量、缓存大小等信息,对于排查内存泄漏等问题非常有帮助。磁盘 I/O 的监控可以展示读写速度、操作次数等指标,有助于分析磁盘性能瓶颈。网络监控则涵盖了网络接口的带宽使用情况、进出流量等,无论是排查网络拥塞还是分析应用程序的网络通信效率都能提供有力的数据支持。

2) 应用程序和服务监控

它支持对众多常见的应用程序和服务进行监控,比如数据库(MySQL、PostgreSQL 等)、Web 服务器(Apache、Nginx 等)、消息队列(RabbitMQ、Kafka 等)。以 MySQL 为例,Netdata 可以监控查询执行时间、连接数、缓存命中率等关键指标,让数据库管理员能够及时发现数据库性能问题,如慢查询过多或者连接池溢出等情况。对于 Web 服务器,能够监控请求数、响应时间、错误率等,从而保障网站的正常运行和用户体验。

2、实时性与低延迟

Netdata 的一大优势在于其出色的实时性。它能够以极低的延迟收集和更新数据,通常在秒级甚至亚秒级。这意味着当系统中出现性能问题或者异常情况时,管理员可以几乎立即在 Netdata 的可视化界面上看到相关的指标变化。这种实时反馈对于快速响应和解决问题至关重要,例如在应对突发的流量高峰或者服务器故障时,可以迅速采取措施,减少对业务的影响。

3、惊艳的可视化界面

1) 直观的仪表盘

Netdata 拥有高度可定制化且直观的仪表盘。仪表盘上以各种图表(如折线图、柱状图、饼图、热力图等)的形式展示监控数据。这些图表色彩丰富、交互性强,用户可以轻松地缩放、平移图表,查看不同时间段的数据。例如,在查看 CPU 使用率的折线图时,可以通过缩放功能详细查看某个短时间内的使用率波动情况,或者通过平移查看较长时间范围内的整体趋势。

2) 分层展示与钻取功能

数据在仪表盘上是分层展示的,用户可以从宏观的系统层面逐步深入到具体的应用程序、服务甚至是某个功能模块的指标。例如,从整个服务器的资源使用情况,钻取到某个特定 Web 应用的请求处理指标,再进一步查看某个 API 端点的响应时间数据。这种钻取功能方便用户快速定位问题所在的层次和具体位置。

3、如何安装Netdata?

Netdata 可以安装在多种操作系统上,以下是常见的安装方法:

1、Linux 系统安装

基于脚本安装(以 CentOS 为例): 安装依赖:打开终端,执行以下命令安装必要的依赖项。

yum install -y autoconf automake curl gcc git libmnl-devel libuuid-devel lm_sensors make MySQL-python nc pkgconfig python python-psycopg2 PyYAML zlib-devel

拉取 Git 仓库:

git clone https://github.com/netdata/netdata.git --depth=1

执行安装脚本:进入克隆下来的 netdata 目录,执行安装脚本。

./netdata-installer.sh

配置防火墙(如果需要):如果系统开启了防火墙,需要允许访问 Netdata 的默认端口 19999。例如:

systemctl stop firewalld.service
firewall-cmd --zone=public --add-port=19999/tcp --permanent
systemctl restart firewalld.service

2、使用一键安装脚本

下载 Netdata 的安装脚本:

wget https://my-netdata.io/kickstart.sh -O /tmp/netdata-kickstart.sh

运行安装脚本:

sudo bash /tmp/netdata-kickstart.sh

3、Docker 安装

确保已经安装了 Docker 环境。 执行以下命令拉取 Netdata 镜像并启动容器:

docker run -d --name=netdata \
-p 19999:19999 \
-v netdatalib:/var/lib/netdata \
-v netdatacache:/var/cache/netdata \
-v /etc/passwd:/host/etc/passwd:ro \
-v /etc/group:/host/etc/group:ro \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-v /etc/os-release:/host/etc/os-release:ro \
--restart unless-stopped \
--cap-add SYS_PTRACE \
--security-opt apparmor=unconfined \
netdata/netdata

安装完成后,默认情况下可以通过浏览器访问
http://<服务器 IP>:19999
来查看 Netdata 的监控界面。

4、Netdata监控效果展示

1、Home首页效果展示

2、Nodes节点页面展示

3、Metrics页面展示

4、K8S容器化监控

5、告警规则页面

6、Anomaly页面

7、数据库监控

官方demo体验地址:

https://app.netdata.cloud/spaces/netdata-demo/rooms/http-endpoints/overview

5、小结

Netdata是一款功能强大且易于使用的开源性能监测工具,以其卓越的性能、直观的界面和丰富的功能,为用户提供了实时监控系统和应用程序性能的便利。通过Netdata,用户可以及时发现并解决系统问题,优化资源配置,提高整体服务质量。如果你正在寻找一款轻量级且强大的实时监控工具,不妨试试Netdata,相信它会给你带来意想不到的惊喜。

Netdata的GitHub项目地址为:
https://github.com/netdata/netdata

希望本文能够帮助你更好地了解和使用Netdata,如有任何问题或建议,请随时在评论区留言。

背景

周一刚上班一个开发小哥火急火燎的过来找我,黑龙江某客户私有化环境的服务过一阵就报数据库连接失败,不知道是什么原因导致的,我以为是客户调整了网络,但是客户说并没有做任何调整,我使用ping测试也看不出什么问题,于是猜测是业务代码导致,我就问他最近是否有发布,他说上周的确发了一个版本,他也怀疑过是代码的问题,所以组织组内成员对发布的代码已经review了好几遍,并没有发现可疑点。

排查

[DruidPooledStatement.java:379] - CommunicationsException, druid version 1.2.8, jdbcUrl : jdbc:mysql://xxx:3306/db_xxx?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&autoReconnect=true, testWhileIdle true, idle millis 9950, minIdle 1, poolingCount 5, timeBetweenEvictionRunsMillis 60000, lastValidIdleMillis 9950, driver com.mysql.cj.jdbc.Driver, exceptionSorter com.alibaba.druid.pool.vendor.MySqlExceptionSorter
2024-11-05 10:35:19.671 [inter-listener-threadPool7] [] ERROR [DruidPooledStatement.java:379] - CommunicationsException, druid version 1.2.8, jdbcUrl : jdbc:mysql://xxx:3306/db_xxx?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&autoReconnect=true, testWhileIdle true, idle millis 8464, minIdle 1, poolingCount 5, timeBetweenEvictionRunsMillis 60000, lastValidIdleMillis 8464, driver com.mysql.cj.jdbc.Driver, exceptionSorter com.alibaba.druid.pool.vendor.MySqlExceptionSorter
2024-11-05 10:35:19.672 [inter-listener-threadPool7] [] ERROR [JdbcUtils.java:89] - close connection error
java.sql.SQLNonTransientConnectionException: Communications link failure during rollback(). Transaction resolution unknown.

Communications link failure,多么熟悉的异常信息,老司机都知道这大概率就是网络问题,奈何找不到任何网络出问题的蛛丝马迹,所以尝试换个思路,从MySQL自身的日志碰碰运气,果不其然,MySQL的error日志中显示有条读取JSON的sql导致其退出,一起看下现场

复现

如我所料,只要执行日志中的sql就会让MySQL意外退出,紧接着重新启动,此时应用程序就会报Communications link failure,这条sql究竟有什么魔力居然可以让大名鼎鼎的MySQL瞬间崩塌,好奇心刹那涌上心头,接下来我们剥茧抽丝。

剥茧抽丝

从MySQL的日志中可以看到“This could be because you hit a bug”,怎么会呢?系统中对于JSON的操作语句很早就存在,如果是存在bug那早就暴露了,也不会等到今天,那会是什么原因呢?

从MySQL日志的错误栈可以看到大概是JSON解析失败了,过往经验告诉我引起JSON解析失败的原因无外乎格式不正确,那我就看下那行数据的extdata是否正确。

乍一看好像没啥问题,这本来也不是一个必填字段,空着就空着吧,其实不然,通过和其他数据对比,发现这其实是空串,并不是NULL,对NULL执行->>'$.'不会引起MySQL崩溃,而空串会。

到这也就可以解释“系统中对于JSON的操作语句很早就存在,如果是存在bug那早就暴露了,也不会等到今天才爆发,那会是什么原因呢?”,这次上线的代码对这个字段有写入空串的情况。

但是也引发我的另一个疑问,为什么其他环境没有问题?

我就不卖关子了,是因为其他环境的MySQL版本较高,这个环境使用的是5.7.20版本,我在翻看MySQL更新日志的时候发现5.7.22中确实提到修复了老版本的一个JSON函数导致MySQL退出的bug

遗憾的是“Bug #22253965”在网络上已经搜索不到具体的内容了,一位国外开发者针对这个bug为什么找不到的问题在MySQL论坛提出了讨论,也得到了回复,有兴趣的可以移步https://forums.mysql.com/read.php?10,667824,667824#msg-667824。

解决方案

升级,升级,升级。

事后思考

时刻关注软件的更新列表很重要,不要因为它的广泛使用就忽视升级,虽然升级过程可能像飞行中换引擎一样充满挑战,但这是让它保持最佳状态的关键步骤呢!

推荐阅读

https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-22.html#mysqld-5-7-22-bug