小菜鸟

java菜鸟号正在起航

视频缩略图生成全指南:基于 JavaCV 与 FFmpeg 的实战方案

在视频管理系统中,缩略图是提升用户体验的关键元素,用于列表预览、详情页展示等场景。本文将详细讲解如何使用 JavaCV(封装 FFmpeg) 实现视频缩略图生成,包括依赖配置、核心代码实现、优化技巧及常见问题解决,帮助你快速集成视频缩略图功能。

技术选型:为何选择 JavaCV + FFmpeg?

生成视频缩略图的方案有多种,对比后 JavaCV + FFmpeg 成为首选:

方案 优势 劣势
JavaCV + FFmpeg 支持几乎所有视频格式,处理能力强,可定制化高 依赖稍大,需理解基础视频概念
Xuggle 轻量易用 已停止维护,对新格式支持不足
本地调用 FFmpeg 命令 无需 Java 依赖,直接复用 FFmpeg 功能 跨平台兼容性差,进程管理复杂
纯 Java 库(如 MP4Parser) 无 native 依赖,部署简单 仅支持少数格式(如 MP4),功能有限

结论:JavaCV 基于 FFmpeg 封装,兼顾功能完整性和 Java 易用性,适合生产环境使用。

环境配置与依赖引入

Maven 依赖配置

JavaCV 通过 Maven 引入,核心依赖包括 javacv 和 FFmpeg 平台包(自动适配不同操作系统):

阅读全文 »

线上问题排查全流程:从现象到根源的系统分析

线上系统出现故障时,快速定位问题根源是核心目标。本文基于 “分层排查、聚焦瓶颈” 的思路,详细介绍从 CPU、内存、网络、磁盘四个维度排查问题的步骤和工具,帮助高效解决线上故障。

第一步:CPU 使用率过高排查

CPU 是系统运行的核心,其使用率过高会直接导致系统卡顿、响应延迟。

确认 CPU 瓶颈

使用 top 命令实时监控 CPU 状态:

top  # 进入界面后按 P 键按 CPU 使用率排序
  • 关键指标:顶部%Cpu行的%idle(空闲 CPU 百分比)。
    • %idle 长期 < 10%:表示 CPU 资源紧张。
    • %us(用户态 CPU)或 %sy(系统态 CPU)长期 > 80%:需定位具体进程。

定位高 CPU 进程

  • top 界面中,%CPU 列显示单个进程的 CPU 占用率,记录高占用进程的 PID

  • 针对 Java 进程,可进一步分析线程栈:

    # 查看进程内线程的 CPU 占用(PID 为目标进程 ID)
    top -Hp PID
    
    # 将线程 ID 转换为十六进制(用于 jstack 定位)
    printf "%x\n" 线程ID
    
    # 导出进程栈信息,查找对应线程的调用栈
    jstack PID | grep 十六进制线程ID -A 30
  • 常见原因:死循环、频繁 GC、大量计算任务等,需结合业务代码优化。

第二步:内存不足排查

阅读全文 »

Log4j2 动态修改日志级别:线上问题排查的灵活工具

在生产环境中,日志级别通常设置为INFOWARN以减少冗余输出,但排查问题时往往需要临时调低级别(如DEBUG)以获取更详细的日志。Log4j2 提供了Configurator工具类,支持在不重启应用的情况下动态修改日志级别,极大提升了线上问题排查的效率。

动态修改日志级别的核心原理

Log4j2 的日志级别配置通过LoggerContext管理,每个Logger(对应类或包)的级别信息存储在内存中。Configurator类提供了静态方法setLevel(),可直接修改指定Logger的级别,无需重新加载配置文件。

  • 核心对象:
    • LoggerContext:Log4j2 的上下文对象,管理所有Logger配置;
    • Level:日志级别枚举(TRACE < DEBUG < INFO < WARN < ERROR < FATAL)。

动态修改日志级别的实现

核心 API

Log4j2 通过org.apache.logging.log4j.core.config.Configurator提供动态配置能力,关键方法:

方法 说明
Configurator.setLevel(String loggerName, Level level) 为指定名称的Logger(类或包)设置级别
LogManager.getContext(false) 获取当前应用的LoggerContext(非单例模式)
loggerContext.getLogger(loggerName) 获取指定名称的Logger实例

实现代码(Spring Boot 示例)

以下示例通过 HTTP 接口实现日志级别的查询与修改,便于线上操作:

阅读全文 »

Python 解析 JSON 日志:从一行数据到一份报告

写后端的人,天天跟日志打交道。现在大部分应用的日志都是 JSON 格式——每条日志是一个 JSON 对象,方便机器解析和分析。

Python 自带的 json 模块就是干这个的:把 JSON 字符串转成字典,或者把字典转成 JSON 字符串。用法就几个函数,但真正写脚本分析日志的时候,有些细节还是得注意。

核心就两个函数:loads 和 dumps

函数 干啥的 啥时候用
json.loads(s) JSON 字符串 → Python 字典 读日志行的时候用
json.dumps(obj) Python 字典 → JSON 字符串 写结果文件的时候用

JSON 字符串转字典:

import json

line = '{"url": "/api/user", "timeSpent": 45, "status": 200}'
data = json.loads(line)

print(data["url"])        # /api/user
print(data["timeSpent"])  # 45

字典转 JSON 字符串:

data = {"name": "Bob", "age": 25, "city": "上海"}
json_str = json.dumps(data, ensure_ascii=False)
# {"name": "Bob", "age": 25, "city": "上海"}

ensure_ascii=False 这个参数经常被忽略——不加的话,中文会变成 \u4e0a\u6d77,虽然能解析,但人没法看。

日志分析实战:筛选慢请求

假设你有一份 API 日志 req_resp.log,每行是一个 JSON 对象:

{"url": "/api/user", "timeSpent": 45, "status": 200, "method": "GET"}
{"url": "/api/order", "timeSpent": 230, "status": 200, "method": "POST"}
{"url": "/api/product", "timeSpent": 89, "status": 500, "method": "GET"}
{"url": "/api/report", "timeSpent": 310, "status": 200, "method": "GET"}
{"url": "/api/search", "timeSpent": 120, "status": 200, "method": "POST"}

需求很简单:找出耗时超过 150ms 的请求,打印出来。

完整实现

#!/usr/bin/env python3
import json

LOG_PATH = "req_resp.log"

with open(LOG_PATH, "r", encoding="utf-8") as f:
    for line_num, line in enumerate(f, 1):
        line = line.strip()
        if not line:
            continue
        
        try:
            data = json.loads(line)
        except json.JSONDecodeError as e:
            print(f"第 {line_num} 行 JSON 解析失败: {e}")
            continue
        
        time_spent = data.get("timeSpent", 0)
        if time_spent > 150:
            print(f"[慢请求] {data.get('method')} {data.get('url')} 耗时 {time_spent}ms")

运行结果

[慢请求] POST /api/order 耗时 230ms
[慢请求] GET /api/report 耗时 310ms

这个脚本虽然短,但包含了几个关键点:

  • 逐行读取:文件大也不怕,不会一次性加载到内存
  • 跳过空行:日志文件末尾经常有空行
  • 捕获 JSONDecodeError:日志里可能有格式错误的一行,不能因为一条坏数据让整个脚本崩溃
  • get() 而不是 []:万一某条日志缺了某个字段,get() 返回默认值,[] 直接报 KeyError

把结果写到文件里

光打印不够,通常需要把筛选结果存下来,方便后续分析或者发给别人。

import json

LOG_PATH = "req_resp.log"
OUTPUT_PATH = "slow_requests.json"

slow_requests = []
error_count = 0

with open(LOG_PATH, "r", encoding="utf-8") as f:
    for line_num, line in enumerate(f, 1):
        line = line.strip()
        if not line:
            continue
        
        try:
            data = json.loads(line)
        except json.JSONDecodeError:
            error_count += 1
            continue
        
        if data.get("timeSpent", 0) > 150:
            # 只保留需要的字段
            filtered = {
                "url": data.get("url"),
                "method": data.get("method"),
                "timeSpent": data.get("timeSpent"),
                "status": data.get("status")
            }
            slow_requests.append(filtered)

# 写入结果文件
with open(OUTPUT_PATH, "w", encoding="utf-8") as out_f:
    json.dump(slow_requests, out_f, ensure_ascii=False, indent=2)

print(f"慢请求数量: {len(slow_requests)}")
print(f"解析失败: {error_count} 行")
print(f"结果已保存到 {OUTPUT_PATH}")

indent=2 让输出的 JSON 有缩进,方便人看。如果文件很大,去掉 indent 能省空间。

处理嵌套 JSON

有些日志的字段是嵌套的,比如这样:

{
    "request": {
        "url": "/api/user",
        "method": "GET",
        "headers": {"User-Agent": "Mozilla/5.0"}
    },
    "response": {"status": 200, "timeSpent": 45}
}

访问嵌套字段用连续的 [] 或者 get()

url = data["request"]["url"]
method = data["request"]["method"]
time_spent = data["response"]["timeSpent"]

如果某个中间字段可能不存在,用 get() 加上空字典兜底:

url = data.get("request", {}).get("url")

这种写法在数据格式不完全统一的时候很好用,一行代码就把嵌套访问和默认值都处理了。

几个经常遇到的问题

1. JSON 解析失败

json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes

最常见的几个原因:

  • 用了单引号而不是双引号(JSON 标准要求双引号)
  • 末尾多了逗号
  • 行里有非 JSON 的前缀,比如 [INFO] 2024-01-15 {"url": "/api"}

解决办法: 捕获异常,打印出问题行:

try:
    data = json.loads(line)
except json.JSONDecodeError:
    print(f"解析失败的行: {line[:100]}")
    continue

如果日志行有固定前缀,用正则把 JSON 部分提取出来再解析。

2. 中文变成 Unicode

不加 ensure_ascii=False 的时候:

json.dumps({"name": "张三"})   # '{"name": "\\u5f20\\u4e09"}'

加上:

json.dumps({"name": "张三"}, ensure_ascii=False)   # '{"name": "张三"}'

3. 字段类型不一致

有的日志里 timeSpent 是数字,有的可能是字符串 "230"。解析的时候统一转换:

def safe_get_int(data, key, default=0):
    value = data.get(key)
    if value is None:
        return default
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

time_spent = safe_get_int(data, "timeSpent", 0)

文件读写:load 和 dump

如果整个文件就是一个 JSON 对象(不是每行一个),用 json.load()json.dump()

# 读取
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

# 写入
with open("output.json", "w", encoding="utf-8") as f:
    json.dump(config, f, ensure_ascii=False, indent=2)

但日志分析通常用逐行处理(每行一个 JSON),因为文件可能很大,一次性加载会撑爆内存。

TCP 拥塞控制详解

TCP 拥塞控制的核心目标是避免网络因数据量过大而陷入拥塞状态,确保整个网络的传输效率。它与流量控制的区别在于:流量控制关注点对点的速率匹配(如 sender 与 receiver 之间),而拥塞控制则从全局视角出发,考虑整个网络的负载情况。

TCP 拥塞控制主要通过以下算法协同实现:

1. 慢启动算法(Slow Start)

  • 核心思想:发送方刚建立连接时,对网络状况一无所知,因此从极小的发送速率开始,逐步增加数据量,避免突然发送大量数据导致网络拥塞。
  • 具体机制:
    • 引入拥塞窗口(cwnd,Congestion Window) 变量,控制发送方在未收到确认前最多能发送的字节数。
    • 初始时,cwnd 取值较小(通常为 1~2 个 MSS,MSS 为最大报文段长度)。
    • 每收到一个对新报文段的确认(ACK),cwnd 就加倍(指数增长)。例如:cwnd=1 → 收到 ACK 后→ cwnd=2 → 再收到 ACK→ cwnd=4,以此类推。
    • 当 cwnd 增长到慢启动阈值(ssthresh,Slow Start Threshold) 时,慢启动阶段结束,进入拥塞避免阶段。

2. 拥塞避免算法(Congestion Avoidance)

  • 核心思想:当网络接近拥塞阈值时,不再快速增加发送量,而是缓慢增长,试探性地提升速率,避免触发拥塞。
  • 具体机制:
    • 当 cwnd ≥ ssthresh 时,进入拥塞避免阶段。
    • 此时,每收到一个 ACK,cwnd 不再加倍,而是加 1 个 MSS(线性增长)。例如:cwnd=10 → 收到 ACK 后→ cwnd=11,逐步提升。
    • 该阶段持续到网络出现拥塞(如超时未收到 ACK),此时触发拥塞处理机制。

3. 拥塞发生后的处理:快重传与快恢复

当网络出现拥塞(如报文段丢失)时,TCP 会调整策略以快速恢复:

  • 快重传(Fast Retransmit)
    • 若接收方收到失序的报文段(如预期收到 M3,却先收到 M4),会立即发送对已收到的最后一个有序报文段的重复 ACK(如重复确认 M2)。
    • 若发送方连续收到3 个重复 ACK,则判断对应报文段(如 M3)已丢失,无需等待超时,立即重传该报文段,避免因超时导致的拥塞窗口骤降。
  • 快恢复(Fast Recovery)
    • 配合快重传使用,当触发快重传时,发送方认为网络并未严重拥塞(只是个别报文丢失),因此不执行慢启动,而是:
      1. 将 ssthresh 设为当前 cwnd 的一半(ssthresh = cwnd / 2);
      2. 将 cwnd 设为 ssthresh 的值(或 ssthresh + 3 ,补偿 3 个重复 ACK 已确认的报文);
      3. 之后进入拥塞避免阶段,cwnd 线性增长。
0%