分类: 解决方案

任何领域的问题解决办法,可以是程序语言的问题,也可以是系统故障的问题

  • webRTC(Golang)

    在视频监控、工业巡检以及边缘计算等场景中,常见的一个现实问题是协议割裂:前端设备通常通过RTSP推流,而浏览器原生并不支持直接播放RTSP。这就带来了一个工程上的关键挑战——如何在不引入高延迟和复杂转码的前提下,将设备侧的视频流高效地分发到浏览器端。

    一种更直接且高效的思路,是利用WebRTC作为浏览器侧的实时传输协议,同时在服务端完成协议层的桥接,将RTSP流转换为WebRTC可消费的RTP数据流。这种方式避免了传统转码链路(例如FFMPEG+纯接口转发)带来的性能损耗,同时保留了实时性的优势。

    通过golang程序实现一个典型的视频桥接架构:上游通过RTSP拉流(通常来自摄像头或推流工具),服务端将RTP包转发到WebRTC PeerConnection,下游浏览器通过 WebRTC 实时播放视频。整体链路为:RTSP → RTP → Go服务 → WebRTC → 浏览器。


    零、在本机启一个mediamtx作为RTSP服务端,再通过ffmpeg把本机摄像头推流到服务器来模拟摄像头的RTSP流

    ./mediamtx &
    ./ffmpeg -f v4l2 -i /dev/video0 \
    -vcodec libx264 -preset veryfast -tune zerolatency \
    -f rtsp -rtsp_transport tcp \
    rtsp://test:123456@127.0.0.1:8554/test

    一、golang程序入口main中首先确定RTSP地址,并开一个Gin HTTP服务,同时维护一个clients映射,用于保存每个WebRTC客户端对应的Track:

    每个客户端并不是单独拉流,而是共享同一 RTSP输入流,服务端通过fan-out(扇出)机制将RTP包写入多个WebRTC Track,从而实现“一路输入,多路输出”

    import (
    	"github.com/bluenviron/gortsplib/v5"
    	"github.com/bluenviron/gortsplib/v5/pkg/base"
    	"github.com/bluenviron/gortsplib/v5/pkg/description"
    	"github.com/bluenviron/gortsplib/v5/pkg/format"
    	"github.com/gin-gonic/gin"
    	"github.com/pion/rtp"
    	"github.com/pion/webrtc/v3"
    )
    
    rtspURL := os.Getenv("RTSP_URL")
    if rtspURL == "" {
    	rtspURL = "rtsp://test:123456@127.0.0.1:8554/test"
    }
    
    var clientsMu sync.Mutex
    clients := map[string]*webrtc.TrackLocalStaticRTP{}

    二、HTTP部分分为三个路由:

    1)“/”路由返回播放器页面
    2)“/offer”路由处理WebRTC SDP信令交换
    3)“/stats”路由返回实时码率与包速率

    前端页面核心逻辑如下:

    浏览器创建RTCPeerConnection → 生成Offer(SDP)→ 发送给服务端 → 服务端生成Answer → 浏览器设置远端描述。需要注意的是,这里没有使用STUN/TURN(iceServers 为空),意味着该方案默认运行在内网或可直连环境,否则无法穿透NAT。

    pc=new RTCPeerConnection({iceServers:[]});
    pc.addTransceiver('video',{direction:'recvonly'});
    
    const offer=await pc.createOffer();
    await pc.setLocalDescription(offer);
    
    const resp=await fetch('/offer',{
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({sdp:offer.sdp,type:offer.type})
    });
    
    const answer=await resp.json();
    await pc.setRemoteDescription({type:answer.type,sdp:answer.sdp});

    服务端“/offer”路由处理逻辑是WebRTC的核心:

    这里做了两件关键事情:
    1)注册编解码器(H264/H265)
    2)创建 PeerConnection

    WebRTC本质上是RTP的增强版,但浏览器对编码格式要求严格,因此必须显式注册codec,否则SDP协商无法匹配。

    m := webrtc.MediaEngine{}
    _ = m.RegisterCodec(webrtc.RTPCodecParameters{
    	RTPCodecCapability: webrtc.RTPCodecCapability{
    		MimeType: webrtc.MimeTypeH264,
    		ClockRate: 90000,
    		SDPFmtpLine: "packetization-mode=1;profile-level-id=42e01f",
    	},
    	PayloadType: 96,
    }, webrtc.RTPCodecTypeVideo)
    
    api := webrtc.NewAPI(webrtc.WithMediaEngine(&m))
    pc, _ := api.NewPeerConnection(webrtc.Configuration{})

    创建Track,并绑定到PeerConnection:

    Track是WebRTC中媒体发送的抽象,本质上是RTP流的出口。这里使用TrackLocalStaticRTP,意味着可以手动写入RTP包

    track, _ := webrtc.NewTrackLocalStaticRTP(
    	webrtc.RTPCodecCapability{
    		MimeType: webrtc.MimeTypeH264,
    		ClockRate: 90000,
    	},
    	"video", "pion",
    )
    pc.AddTrack(track)

    信令交换部分:

    接收浏览器Offer → 设置远端 SDP → 生成 Answer → 返回给浏览器。GatheringCompletePromise用于等待ICE candidate收集完成,否则SDP不完整。

    offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: req.SDP}
    pc.SetRemoteDescription(offer)
    
    ans, _ := pc.CreateAnswer(nil)
    pc.SetLocalDescription(ans)
    
    <-webrtc.GatheringCompletePromise(pc)

    将Track存入clients,用于后续RTP分发:

    这里通过DESCRIBE + SETUP建立RTSP会话,并解析媒体格式(H264/H265)。

    clientsMu.Lock()
    clients[c.ClientIP()+"_"+time.Now().Format("150405")] = track
    clientsMu.Unlock()

    核心转发逻辑在OnPacketRTP:

    RTSP → 解复用 → 得到RTP包 → 广播写入所有WebRTC Track
    服务端本质上只是一个RTP转发器,并不进行转码,这带来两个重要特性:
    优点:延迟极低(基本无编码延迟)
    CPU占用极小
    架构简单稳定
    限制:浏览器必须支持该编码(通常是H264)
    RTSP输入必须与WebRTC codec兼容

    cli.OnPacketRTP(media, formaH264, func(pkt *rtp.Packet) {
    	clientsMu.Lock()
    	totalBytes += uint64(len(pkt.Payload))
    	totalPackets++
    
    	activeClients := make([]*webrtc.TrackLocalStaticRTP, 0, len(clients))
    	for _, t := range clients {
    		activeClients = append(activeClients, t)
    	}
    	clientsMu.Unlock()
    
    	for _, t := range activeClients {
    		t.WriteRTP(pkt)
    	}
    })

    统计接口“/stats”路由通过简单计数实现码率计算:

    前端每秒拉取一次,实现实时监控。

    kbps := uint64(float64(totalBytes*8) / 1024 / duration)
    pktps := uint64(float64(totalPackets) / duration)

    第一,WebRTC并不负责“获取视频”,它只负责“传输媒体流”。视频源可以来自 RTSP、文件、摄像头等。

    第二,WebRTC的关键不是API,而是:

    • SDP协商
    • ICE建连
    • RTP收发

    第三,这种架构属于典型的“边缘网关模式”:

    RTSP(设备侧协议) → WebRTC(浏览器协议)

    在工业监控、视频巡检、边缘计算中非常常见

  • 存取qDrant向量(Golang)

    检索增强生成(Retrieval-Augmented Generation, RAG)成为解决模型知识局限性和幻觉问题的有效手段。在RAG架构中,向量数据库负责存储和检索文本的向量表示,而qDrant作为高性能的向量数据库,常被选为底层存储。本文将结合Go语言代码示例,详细介绍如何通过langchain-go和qdrant-go库存取qDrant向量,并实现一个完整的RAG流程。

    环境:

    • qDrant 服务(本地或远程运行)
    • Ollama 服务(用于Embedding和文本生成)
      • 准备nomic-embed-text模型
      • 准备任意支持text的大模型
    • Go语言环境及以下库:
      • github.com/tmc/langchaingo(包含向量存储、文档加载器、文本分割器等)
      • github.com/qdrant/go-client(qDrant官方Go客户端)

    新建文件夹config/
    config文件含config.go,管理配置信息。包含qDrant地址、集合名称、Ollama URL、Embedding模型名和生成模型名等。

    1. 集合管理:EnsureCollection 和 resetCollection

    确保指定的集合存在,若不存在则创建。向量维度(768)与Embedding模型(如nomic-embed-text)匹配,距离度量采用余弦相似度。创建集合时指定的向量维度(768)必须与Embedding模型输出的维度完全一致。本例使用nomic-embed-text(维度768),若换用其他模型(如all-MiniLM-L6-v2维度384),需相应调整。
    resetCollection:先删除指定集合,再重新创建,用于重置整个知识库。

    func EnsureCollection(urlStr string, collectionName string) {
    	myclient, err := qdrant.NewClient(&qdrant.Config{
    		Host:   config.QdrantIP,
    		Port:   6334,
    		UseTLS: false,
    	})
    	if err != nil {
    		log.Fatal("连接Qdrant失败:", err)
    	}
    	defer myclient.Close()
    	ctx := context.Background()
    	exists, err := myclient.CollectionExists(ctx, collectionName)
    	if err != nil {
    		log.Fatal("查询集合状态失败:", err)
    	}
    	if !exists {
    		fmt.Printf("正在创建集合: %s...\n", collectionName)
    		err = myclient.CreateCollection(ctx, &qdrant.CreateCollection{
    			CollectionName: collectionName,
    			VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
    				Size:     768, // nomic-embed-text 维度是 768:必须与 Embedding 模型一致
    				Distance: qdrant.Distance_Cosine,
    			}),
    		})
    		if err != nil {
    			log.Fatal("创建集合失败:", err)
    		}
    		fmt.Println("集合创建成功")
    	}
    }
    func resetCollection(name string) {
    	myclient, _ := qdrant.NewClient(&qdrant.Config{
    		Host: config.QdrantIP, Port: 6334, UseTLS: false,
    	})
    	defer myclient.Close()
    	ctx := context.Background()
    	_ = myclient.DeleteCollection(ctx, name)
    	EnsureCollection(config.QdrantIP, name)
    	fmt.Println("集合已重置,准备重新导入...")
    }

    2. 数据导入:IngestKnowledge

    分块策略直接影响检索效果,需根据文档类型和任务调整块大小和重叠。

    func IngestKnowledge(filePath string, shouldReset bool) {
        // 1. 重置或确保集合存在
        if shouldReset {
            resetCollection(config.Collection)
        } else {
            EnsureCollection(config.QdrantIP, config.Collection)
        }
    
        // 2. 根据文件类型选择加载器(PDF或文本)
        f, _ := os.Open(filePath)
        defer f.Close()
        var loader documentloaders.Loader
        if strings.HasSuffix(filePath, ".pdf") {
            loader = documentloaders.NewPDF(f, fileSize)
        } else {
            loader = documentloaders.NewText(f)
        }
    
        // 3. 加载并分割文档
        docs, _ := loader.LoadAndSplit(ctx, textsplitter.NewRecursiveCharacter(
            textsplitter.WithChunkSize(300),
            textsplitter.WithChunkOverlap(100),
        ))
    
        // 4. 初始化Embedder和向量存储
        embedLLM, _ := ollama.New(ollama.WithModel(config.EmbedModel), ollama.WithServerURL(config.OllamaURL))
        embedder, _ := embeddings.NewEmbedder(embedLLM)
        store, _ := qdrantl.New(
            qdrantl.WithURL(url.URL{Scheme: "http", Host: config.QdrantURL}),
            qdrantl.WithCollectionName(config.Collection),
            qdrantl.WithEmbedder(embedder),
        )
    
        _, err = store.AddDocuments(ctx, docs) // 5. 添加文档到qDrant
    }

    3. 查询与生成:UpdateRAG

    确保集合存在,初始化Embedder和向量存储(与导入时相同)。向集合中添加几个示例文档块(演示用)。执行相似性搜索:将查询文本向量化,在qDrant中检索最相似的3个文档块。将检索结果拼接为上下文,构造提示词,调用生成模型(Ollama)得到最终回答。

    注意:实际应用中,文档导入和查询生成通常是分开的步骤,此处合并仅为展示完整流程。

    func UpdateRAG() {
        // 1. 确保集合存在,初始化Embedder和向量存储
        EnsureCollection(config.QdrantIP, config.Collection)
        embedLLM, _ := ollama.New(ollama.WithModel(config.EmbedModel), ollama.WithServerURL(config.OllamaURL))
        embedder, _ := embeddings.NewEmbedder(embedLLM)
        store, _ := qdrantl.New(
            qdrantl.WithURL(url.URL{Scheme: "http", Host: config.QdrantURL}),
            qdrantl.WithCollectionName(config.Collection),
            qdrantl.WithEmbedder(embedder),
        )
    
        // 2. 准备示例文档并分割(此处仅为演示,实际可从文件加载)
        docs := []schema.Document{ ... }
        splitter := textsplitter.NewRecursiveCharacter(textsplitter.WithChunkSize(200), textsplitter.WithChunkOverlap(20))
        var chunks []schema.Document
        for _, doc := range docs {
            texts, _ := splitter.SplitText(doc.PageContent)
            for _, t := range texts {
                chunks = append(chunks, schema.Document{PageContent: t, Metadata: doc.Metadata})
            }
        }
        store.AddDocuments(ctx, chunks)
    
        // 3. 执行相似性搜索
        query := "Qdrant 适合用在什么场景?"
        results, _ := store.SimilaritySearch(ctx, query, 3)
    
        // 4. 构建提示词并调用LLM生成回答
        contextText := ""
        for _, doc := range results {
            contextText += doc.PageContent + "\n"
        }
        llm, _ := ollama.New(ollama.WithModel(config.GenerateModel), ollama.WithServerURL(config.OllamaURL))
        prompt := fmt.Sprintf(`
    你是一个基于知识库回答问题的助手。
    只能根据【知识库】内容作答,如果无法得到答案,请回答“不知道”。
    【知识库】
    %s
    【问题】
    %s
    `, contextText, query)
        answer, _ := llms.GenerateFromSinglePrompt(ctx, llm, prompt)
        fmt.Println(answer)
    }

    4. 运行示例

    func main() {
    	IngestKnowledge("./xxix.pdf", true) //指向需要索引的知识文件
            UpdateRAG()
    }
    输出:
    文档已成功写入 Qdrant
    
    Model回答:
    Qdrant 是一个高性能的向量数据库,常用于 RAG(检索增强生成)系统,因此适合用于构建基于语义搜索、推荐系统、异常检测等需要向量相似性检索的场景。
  • 对象存储(Object Storage Service)

    OSS(对象存储服务)以存储空间(Bucket)作为数据的逻辑容器,用来统一管理对象。每个Bucket都具有唯一性,并绑定了固定的存储类别、访问权限策略以及所属地域,用户可以通过Bucket对应的访问域名在互联网上定位并访问其中的数据。

    在OSS中,对象(Object)是最基本的数据存储单元,可以理解为“文件 + 属性信息”的整体。一个对象由Key、Metadata和Data三部分组成。Key即对象的唯一名称,是经过UTF-8编码的字符串,在同一个Bucket内不能重复;Metadata是对象的描述信息,以键值对形式存在,用于说明对象的各种属性;Data则是对象实际存储的二进制数据内容。Metadata又分为系统元数据和用户元数据,其中系统元数据由OSS自动维护,用于对象管理和传输,例如内容长度、最后修改时间、ETag等;用户元数据则由用户在上传时自定义,用来补充描述对象的业务属性。

    OSS根据数据访问频率和成本需求提供了多种存储类别。标准存储具备高可靠性、高可用性和高性能,访问时延低、吞吐能力强,适合需要频繁访问的热点数据场景,如网站图片、音视频内容和移动应用资源。低频存储面向访问次数较少但仍需实时读取的数据,存储成本低于标准存储,适合长期备份类数据,但对象存储时间不足30天提前删除会产生费用。归档存储则以最低的存储成本支持长期保存的数据,适合日志、档案和影视素材等几乎不访问的数据,取回时需要等待一定时间,并且对最短存储周期有更严格的限制。

    在底层能力上,OSS通过多副本和纠删码等冗余机制,将数据分布在跨可用区甚至跨设备的存储节点上,即使多台设备同时发生故障,也能保证数据不丢失、业务不中断,并自动完成冗余修复。同时,系统会周期性校验数据完整性,发现异常后利用冗余数据进行重建,进一步保障数据可靠性。

    OSS还提供对象级别的强一致性保障。所有对象操作都是原子的,不存在中间状态:操作要么成功,要么失败。只要用户收到成功响应,数据就已经处于可用状态。例如对象上传完成后即可立即读取,更新对象时,其他并发读请求只会读到旧数据,而不会读到部分或损坏的数据。这种强一致性特性使得OSS在使用方式上更接近传统存储系统,简化了应用架构设计。

    在数据组织方式上,OSS采用Key-Value模型,而不是传统文件系统的目录树结构。对象的Key是访问数据的唯一标识,即使Key看起来像“FolderA/FolderB/file.jpg”,在OSS内部也只是一个普通字符串,并不代表真实存在的层级目录。因此,不同Key的访问效率基本一致,不会因为“目录层级”变深而降低性能。所谓的目录操作,本质上是对前缀匹配的一批对象进行处理,成本较高,也不被推荐频繁使用。

    由于OSS不支持对象的在线修改,即使只改动一个字节,也需要重新上传整个对象,因此它天然适合“一次写入,多次读取”的业务模型。依托分布式架构和Key-Value存储方式,OSS能够支持海量数据和高并发访问,非常适合静态资源分发、备份归档等场景。

    在访问和管理方式上,用户可以通过网页形式的管理控制台直观地创建Bucket、上传下载文件并配置权限;也可以使用支持S3接口的客户端工具(如S3Browser)进行本地化管理;对于开发场景,OSS提供了封装良好的SDK,方便应用程序直接调用对象存储能力;同时还提供基于HTTPS、数据格式为JSON的RESTful API,满足更底层或统计类的接口调用需求。

  • 移除内核崩溃转储(kdump)

    当Linux内核崩溃时,kdump 会利用预留的一段内存(称为 crash kernel)启动一个最小化的内核环境,从而将故障时的内存数据保存到硬盘。崩溃日志可用于后期故障排查。云主机的内存比较宝贵,如果是一台4G内存的主机,除去KVM的硬件预留,还有内核崩溃转储预留,可能实际available ram为3.6G左右。如果不需要捕获系统崩溃信息,可以再腾出一些空间。有些云服务器厂商的系统默认配置kdump,也就是说存在云主机运行内存不足额的情况,这边需要手动更新配置。

    查看系统当前的内存预留:

    sudo dmesg | grep -i memory

    修改 /etc/default/grub ,去除crashkernel相关字段:

    GRUB_CMDLINE_LINUX=" vga=792 console=tty0 console=ttyS0,115200n8 net.ifnames=0 noibrs nvme_core.io_timeout=4294967295 nvme_core.admin_timeout=4294967295 iommu=pt crashkernel=0M-1G:0M,1G-4G:192M,4G-128G:384M,128G-:512M crash_kexec_post_notifiers=1"

    改完grub后,需要更新配置:

    sudo grub-mkconfig -o /boot/grub/grub.cfg
    
    sudo grub-mkconfig -o /boot/efi/EFI/ubuntu/grub.cfg

    再次重启主机,内存会释放。

  • 自定义Dolphin右键新建按钮

    日常在Dolphin中会用到右键-新建Librioffice Draw,这些办公类快捷按钮会在软件安装的时候自动添加,做笔记常用到markdown.md文件,配一个入口按钮方便点开创建即用。

    1. 创建模板文件夹
    ~/.local/share/templates && cd ~/.local/share/templates
    1. 新建文件
    touch MarkDown.md
    1. 新建快捷信息,并填写
    vim create_markdown.desktop
    [Desktop Entry]
    Name=Markdown
    Comment=快速新建Markdown文件
    Type=Link
    URL=MarkDown.md
    Icon=application-vnd.oasis.opendocument.text