高效定位 Go 应用问题:Go 可观测性功能深度解析

news2025/4/7 21:21:09

作者:古琦

背景

自 2024 年 6 月 26 日,阿里云 ARMS 团队正式推出面向 Go 应用的可观测性监控功能以来,我们与程序语言及编译器团队携手并进,持续深耕技术优化与功能拓展。这一创新性的解决方案旨在为开发者提供更为全面、深入且高效的应用性能监控体验,助力企业在数字化转型中实现卓越的系统稳定性与性能表现。

从商业化版本的首次亮相至今,我们已历经五次重大版本迭代及若干次精细化的小版本更新。相较于初始版本,系统性能实现了翻倍提升,同时在功能层面亦展现出前所未有的丰富性与灵活性。新增特性包括但不限于智能化应用诊断、高度可定制的扩展能力、灵活的应用开关机制、接口全量采样以及代码热点分析等模块。这些功能的引入不仅显著提升了系统的实用性,也赢得了广大用户的广泛认可与积极反馈。而基于编译时插桩(Compile-time Instrumentation)的技术路径,更被实践证明是 Go 语言应用监控领域的一次突破性创举,堪称当前最优解。

为进一步赋能用户在复杂场景下快速定位与解决问题,我们结合近期发布的一系列全新功能,精心梳理了一套从接入到问题发现、再到问题排查与精准定位的最佳实践指南。

应用接入

通过 ARMS 提供的 Instgo 工具,只需要在 go build 前添加 instgo 命令,无需用修改一行代码,通过编译时插桩的方式实现监控能力注入[1]。

instgo go build {arg1} {arg2} {arg3}

智能告警

应用接入到 ARMS 后,可以在应用列表查看到应用的名称,点击进去查看到应用详情,包括了请求数、错误数、延迟等指标,还提供了每个接口的指标、以及依赖的接口指标,为了快速发现问题,可以通过配置应用的告警来第一时间发现问题。

可以创建对应的告警,如最近 1 分钟调用响应时间大于等于 500ms 就报警。

应用详情

通过监控告警第一时间发现问题后,到对应服务的详情查看这个接口的平均耗时非常长,即知道了告警是由于这个接口导致的。

查看对应的调用链,可以按耗时排列,找到耗时最长的调用链:

点击查看调用链详情,可以看到它的子 span 调用时间都非常短,可以确定是这个接口本身慢导致的,而不是其他对外请求导致的。

应用诊断

通过上述应用详情找到了请求慢的接口后,如何确认这时候的问题呢,我们可以通过应用诊断来发现问题,在应用监控中除了指标、链路、日志外,Profiling 的数据成为了应用监控的四大支柱之一。

通过 Profiling 数据能快速发现性能的瓶颈,ARMS Go 可观测提供了 CPU、内存、代码热点三个 Profiling 功能,用于快速发现应用性能问题。

ARMS 的持续剖析能力跟通过类似 https://github.com/grafana/pyroscope 或者 go 提供的 pprof 等工具相比,ARMS 提供的 Profiling 能力可以做到随开随关,通过应用设置-持续性能剖析设置即可进行开关设置,无需重启,直接生效。

CPU Profiling

CPU Profiling 用于收集和分析 Go 应用程序中的 CPU 使用情况,了解你的程序在运行时有多少时间花费在各个函数上。通过分析这些数据,开发者可以识别出程序中最耗费 CPU 时间的部分,ARMS 提供的 CPU Profiling 数据会采集每分钟的 CPU  运行情况,通过下面的火焰图即可找到当前执行时间最长的函数。

除了每分钟的数据之外,还提供了 CPU Profiling 数据的对比功能,对比前后 CPU 的消耗的不同,确定性能瓶颈。

内存 Profiling

跟 CPU Profiling 一样,内存 Profiling 也提供了对比的功能,可以对比前后不同时刻内存分配的情况,找到内存分配的热点。

除了通过内存 Profiling 找到内存分配热点外,还可以通过 Runtime 监控,找到每个时刻 Goroutines 数量、以及堆对象的数量来看某个时刻是否异常,是否因为流量突增导致的数量增加。

代码热点

在出现应用请求超时、响应慢的时候,为了快速定位到性能问题,从提供服务找到出现响应慢的接口,跳转到调用链,从调用链分析看出来对应接口在某些请求中响应的时间超出正常值很多,这时候如果还要进一步定位到这个请求执行过程中响应慢的函数是哪个,则无法通过单纯的调用链分析获取到,代码热点就是用来解决这个问题。点开对应的 Trace,通过放大镜即可查看当前的调用 Profiling[2]:

可以看到 main 中的 onCpu 函数消耗时间长达 0.62 秒,这样去排查这个函数的问题即可。

自定义扩展

通过上述方式可以查看到大部分问题,我们还提供了自定义扩展的功能[3],通过一个规则+一段待注入的代码组成,通过 Go Agent 的能力,在编译时完成代码的插桩,而不需要去修改原始代码,这个功能的优势是对于一些非项目开发人员可以在不修改原始代码的情况下完成相关功能实现。以下是我们经常会碰到的通过自定义扩展可以解决的问题:

日志打印

为了快速定位问题或者业务需求,日志可以记录非常详细的信息,比如函数的出入参数、Http 的返回的 body、sql 的请求语句参数等,以下是介绍打印 sql 请求的语句、参数:

第一步,创建 hook 文件夹,使用 go mod init hook 初始化该文件夹,然后新增下面的 hook.go 代码,它是即将注入的代码:

package hook

import (
  "database/sql"
    "fmt"
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
)

func sqlQueryOnEnter(call api.CallContext, db *sql.DB, query string, args ...interface{}) {
  fmt.Println("sql is ", query)
  fmt.Println("sql arg is", args)
}

第二步,编写测试 Demo。创建文件夹并使用 go mod init demo 初始化,然后添加 main.go

package main

import (
  "context"
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
)

func main() {
  mysqlDSN := "test:test@tcp(127.0.0.1:3306)/test"
  db, _ := sql.Open("mysql", mysqlDSN)
  db.ExecContext(context.Background(), `CREATE TABLE IF NOT EXISTS usersx (id char(255), name VARCHAR(255), age INTEGER)`)
  db.ExecContext(context.Background(), `INSERT INTO usersx (id, name, age) VALUE ( ?, ?, ?)`, "0", "foo", 10)
  maliciousAnd := "'foo' AND 1 = 1"
  injectedSql := fmt.Sprintf("SELECT * FROM userx WHERE id = '0' AND name = %s", maliciousAnd)
  db.Query(injectedSql, "abc")
}

第三步,在 Demo 文件夹下编写下面的 conf.json 配置,告诉工具我们想要将 hook 代码注入到 database/sql:😦*DB).Query()。

[{
  "ImportPath": "database/sql",
  "Function": "Query",
  "ReceiverType": "*DB",
  "OnEnter": "sqlQueryOnEnter",
  "Path": "/path/to/hook" # Path修改为hook代码的本地路径
}]

第四步,切换到 Demo 目录,使用 instgo 工具编译并执行程序,以验证 SQL 注入保护的效果。

$ ./instgo set --rule=./conf.json
$ docker run -d -p 3306:3306 -p 33060:33060 -e MYSQL_USER=test -e MYSQL_PASSWORD=test -e MYSQL_DATABASE=test -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:8.0.36
$ ./instgo go build .
$ ./demo

可以看到,使用 instgo 工具编译出的二进制文件成功检测到了潜在的 SQL 注入攻击,并打印出了相应日志:

sql is  SELECT * FROM userx WHERE id = '0' AND name = 'foo' AND 1 = 1
sql arg is [abc]

记录Span

ARMS 链路追踪记录的 span 信息都是对开源的 SDK 进行埋点获取的,用户在业务中如果有关心的函数需要记录可以通过自定义插件的功能,记录当前函数的 span。

第一步,创建 hook文件夹,使用 go mod init hook 初始化该文件夹,然后新增下面的 hook.go 代码,它是即将注入的代码:

package hook

import (
  "context"
  "fmt"
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
  "go.opentelemetry.io/otel"
  "go.opentelemetry.io/otel/attribute"
)

func requestDbOnEnter(call api.CallContext) {
  tracer := otel.GetTracerProvider().Tracer("")
  _, span := tracer.Start(context.Background(), "Client/User defined span")
  span.SetAttributes(attribute.String("client", "client-with-ot"))
  span.SetAttributes(attribute.Bool("user.defined", true))
  span.End()
  fmt.Println(span.SpanContext().SpanID().String())
}

第二步,编写测试 Demo。创建文件夹并使用 go mod init demo 初始化,然后添加 main.go

package main

import (
  "demo/common"
  _ "github.com/go-sql-driver/mysql"
  _ "go.opentelemetry.io/otel"
)

func main() {
  common.RequestDb()
}

common 文件夹下增加 common.go 如下:

package common

import (
  "context"
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
)

func RequestDb() {
  mysqlDSN := "test:test@tcp(127.0.0.1:3306)/test"
  db, _ := sql.Open("mysql", mysqlDSN)
  db.ExecContext(context.Background(), `CREATE TABLE IF NOT EXISTS usersx (id char(255), name VARCHAR(255), age INTEGER)`)
  db.ExecContext(context.Background(), `INSERT INTO usersx (id, name, age) VALUE ( ?, ?, ?)`, "0", "foo", 10)
  maliciousAnd := "'foo' AND 1 = 1"
  injectedSql := fmt.Sprintf("SELECT * FROM userx WHERE id = '0' AND name = %s", maliciousAnd)
  db.Query(injectedSql, "abc")
}

第三步,在 Demo文件夹下编写下面的 conf.json 配置,告诉工具我们想要将 hook 代码注入到 common/RequestDb()。

[{
  "ImportPath": "demo/common",
  "Function": "RequestDb",
  "ReceiverType": "",
  "OnEnter": "requestDbOnEnter",
  "Path": "/path/to/hook" # Path修改为hook代码的本地路径
}]

第四步,切换到 Demo 目录,使用 instgo 工具编译并执行程序,以验证 SQL 注入保护的效果。

$ ./instgo set --rule=./conf.json
$ docker run -d -p 3306:3306 -p 33060:33060 -e MYSQL_USER=test -e MYSQL_PASSWORD=test -e MYSQL_DATABASE=test -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:8.0.36
$ ./instgo go build .
$ ./demo

可以看到,使用 instgo 工具编译出的二进制文件成功创建了 span,并打印出了相应 trace spanId:

0000000000000000

如果上报 span 到服务端,则可以看到自定义的 span。

流量回放

除了简单的打印日志和创建 Span 外,还可以对生产的请求进行录制,用于开发和测试阶段回归,提高测试质量,减少线上故障,以下是介绍通过对 Http 的请求、返回进行记录,将这些数据可以记录到日志或者数据库中,用于下次测试回归。

第一步,创建 hook 文件夹,使用 go mod init hook 初始化该文件夹,然后新增下面的 hook.go 代码,它是即将注入的代码:

package hook

import (
  "encoding/json"
  "fmt"
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
  "io"
  "net/http"
)

func httpClientOnEnter(call api.CallContext, t *http.Transport, req *http.Request) {
  if req == nil {
    return
  }
  h, _ := json.Marshal(req.Header)
  fmt.Println("http request header is ", string(h))
  if req.GetBody == nil {
    return
  }
  requestBody, err := req.GetBody()
  if err != nil {
    return
  }
  defer requestBody.Close()
  requestData, err := io.ReadAll(requestBody)
  if err != nil {
    return
  }
  fmt.Println("http request body is ", string(requestData))
}

第二步,编写测试 Demo。创建文件夹并使用 go mod init demo 初始化,然后添加 main.go

package main

import (
  "bytes"
  "context"
  "encoding/json"
  "net/http"
  "time"
  "unicode"
)

func hello(w http.ResponseWriter, r *http.Request) {
  _, err := w.Write([]byte("Hello Http!"))
  if err != nil {
    panic(err)
  }
}

func setupHttp() {
  http.Handle("/http-service1", http.HandlerFunc(hello))
  err := http.ListenAndServe(":9114", nil)
  if err != nil {
    panic(err)
  }
}

// 定义一个结构体用于构造 JSON 数据
type RequestBody struct {
  Name  string `json:"name"`
  Email string `json:"email"`
}

func requestServer() {
  ctx := context.Background()
  reqBody := RequestBody{
    Name:  "Alice",
    Email: "alice@example.com",
  }

  // 将结构体序列化为 JSON 格式
  jsonData, err := json.Marshal(reqBody)
  if err != nil {
    return
  }

  req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost:9114/http-service1", bytes.NewBuffer(jsonData))
  if err != nil {
    panic(err)
  }
  req.Header.Add("Content-Type", "application/json")
  req.Header.Add("test-key", "log")
  req.Header.Add("hello", "arms")
  client := &http.Client{}
  resp, err := client.Do(req)
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()
}

func Is(s string) bool {
  for i := 0; i < len(s); i++ {
    if s[i] > unicode.MaxASCII {
      return false
    }
  }
  return true
}
func main() {
  go setupHttp()
  time.Sleep(3 * time.Second)
  requestServer()
}

第三步,在 Demo文件夹下编写下面的 conf.json 配置,告诉工具我们想要将 hook 代码注入到 net/http:😦*Transport).RoundTrip()。

[{
  "ImportPath": "net/http",
  "Function": "RoundTrip",
  "ReceiverType": "*Transport",
  "OnEnter": "httpClientOnEnter",
  "OnExit": "",
  "Path": "/path/to/hook" # Path修改为hook代码的本地路径
}]

第四步,切换到 Demo 目录,使用 instgo 工具编译并执行程序,以验证 SQL 注入保护的效果。

$ ./instgo set --rule=./conf.json
$ ./instgo go build .
$ ./demo

可以看到,使用 instgo 工具编译出的二进制文件成功获取到了请求的 header 和 body,并打印出了相应日志:

http request header is  {"Content-Type":["application/json"],"Hello":["arms"],"Test-Key":["log"]}
http request body is  {"name":"Alice","email":"alice@example.com"}

通过自定义插件打印了日志,或者通过已有代码的日志也可以进行快速查看问题,我们提供了 TraceID 和 SpanID 关联到日志的能力[4]。

按需全采

针对一些重要的接口如果需要全采样,可以通过应用设置-采样设置配置接口名称,也可以通过前缀、后缀匹配来配置,这样这个接口的请求都会被采样到,避免被丢掉。

后续

为了进一步提升系统的可观测性与诊断能力,我们正致力于引入一系列高级性能分析工具,包括 Goroutine Profiling(协程剖析)、Mutex Profiling(互斥锁剖析)、Block Profiling(阻塞剖析)以及 Go Trace(Go语言运行轨迹追踪)。这些功能将为开发者提供更深入的洞察力,帮助他们在复杂的应用场景中精准定位性能瓶颈与潜在问题。

与此同时,我们将扩展对前沿技术的支持,特别是与大语言模型(LLM)相关的插件生态。例如,我们将集成 langchaingo 这一高效的语言处理框架,并引入 dify 的创新组件,如 dify-sandbox(沙盒环境)和 dify-plugin-daemon(插件守护进程),以满足开发者在多样化场景下的需求。

我们还计划推出一套在线调试工具,旨在为用户打造一个实时、交互式的问题诊断平台。通过这一平台,开发者可以快速定位并解决复杂问题,从而大幅缩短故障排查时间,提升系统的稳定性和可靠性。我们相信,这些能力的引入将为开发者带来前所未有的便捷体验,同时推动技术生态的进一步繁荣与发展。

最后诚邀大家试用我们的商业化产品,并加入我们的钉钉群(开源群:102565007776,商业化群:35568145) ,共同提升 Go 应用监控与服务治理能力。通过群策群力,我们相信能为 Golang开发者社区带来更加优质的云原生体验。

相关链接:

[1] instgo 工具介绍:

https://help.aliyun.com/zh/arms/application-monitoring/developer-reference/instgo-tool-introduction

[2] 代码热点:

https://help.aliyun.com/zh/arms/application-monitoring/user-guide/use-hotspot-code-to-diagnose-slow-calls-in-go-applications

[3] 自定义扩展:

https://help.aliyun.com/zh/arms/application-monitoring/use-cases/use-golang-agent-to-customize-scalability

[4] Go 应用日志 Trace 关联:

https://help.aliyun.com/zh/arms/application-monitoring/use-cases/associate-trace-ids-with-business-logs-for-a-go-application

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

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

相关文章

【Windows】win10系统安装.NET Framework 3.5(包括.NET 2.0和3.0)失败 错误代码:0×80240438

一、.NET3.5(包括.NET 2.0和3.0)安装方式 1.1 联网安装(需要联网,能访问微软,简单,很可能会失败) 1.2 离线安装-救急用(需要操作系统iso镜像文件,复杂,成功几率大) 二、联网安装 通过【控制面板】→【程序】→【程序和功能】→【启用或关闭Windows功能】 下载过程…

蓝桥杯训练士兵

思路&#xff1a;其实每次就是要比较士兵单独训练的价格之和SUM与S的大小&#xff0c;如果 SUM大&#xff0c;那么就减去所有士兵都要训练的次数的最小值&#xff0c;SUM再更新一下&#xff0c;继续比较。 先对士兵的次数按从小到大的次序排序&#xff08;很重要&#xff09;&…

循环神经网络 - 简单循环网络

本文我们来学习和了解简单循环网络(Simple Recurrent Network&#xff0c;SRN)&#xff0c; SRN是一个非常简单的循环神经网络&#xff0c;只有一个隐藏层的神经网络。 简单循环神经网络&#xff0c;也常称为 Elman 网络&#xff0c;是最基本的循环神经网络&#xff08;RNN&am…

Linux 企业项目服务器组建(附脚本)

一、架构概述​ 本方案旨在为企业搭建一套高效、安全的 Linux 服务器架构&#xff0c;包含一台 DNS 服务器&#xff0c;以及一台同时承载 FTP 和 Samba 服务的服务器&#xff0c;满足公司在域名解析、图片存储与共享、文件共享等方面的业务需求。​ 二、服务器部署​ DNS 服…

⼆、Kafka客户端消息流转流程

这⼀章节将重点介绍Kafka的HighLevel API使⽤&#xff0c;并通过这些API&#xff0c;构建起Kafka整个消息发送以及消费的主线流程。 Kafka提供了两套客户端API&#xff0c;HighLevel API和LowLevel API。 HighLevel API封装了kafka的运⾏细节&#xff0c;使⽤起来⽐较简单&…

es 3期 第28节-深入掌握集群组建与集群设置

#### 1.Elasticsearch是数据库&#xff0c;不是普通的Java应用程序&#xff0c;传统数据库需要的硬件资源同样需要&#xff0c;提升性能最有效的就是升级硬件。 #### 2.Elasticsearch是文档型数据库&#xff0c;不是关系型数据库&#xff0c;不具备严格的ACID事务特性&#xff…

如何在 SwiftUI 视图中使用分页(Paging)机制显示 SwiftData 数据(三)

概述 小伙伴们都知道,自从有了 SwiftData 的加持,现在 SwiftUI 可以非常惬意的利用描述型命令创建以数据驱动为本的视图了。这在测试或演示小项目中工作的完美无缺,不过真实世界要“残酷”的多。 实际情况是,我们无法一次性将海量数据统统拉入内存以便在 SwiftUI 中显示,…

git和VScode

游戏存档保存的是游戏的进度 git保存的是代码的进度 Vscode和git 要正常的使用git首先要设置姓名和邮箱 要配合gitee&#xff08;也可以是其他平台&#xff0c;以gitee举例&#xff09;使用&#xff0c;首先创造一个gitee账号&#xff0c;复制邮箱和用户名 在VScode中找到…

利用Canvas在紫微斗数命盘上画出三方四正

许多紫微斗数排盘程序都会在命盘上画出三方四正的指示线&#xff0c;便于观察命盘。本文用Canvas在一个模拟命盘上画出三方四正指示线。 模拟命盘并画出“子”宫三方四正的HTML文件如下&#xff1a; <!doctype html> <html lang"en"> <head><…

传统汽车 HMI 设计 VS 新能源汽车 HMI 设计,有何不同?

一、设计理念与目标的差异 传统汽车HMI设计的核心目标是辅助驾驶&#xff0c;强调功能的简洁性和操作的便捷性。其设计侧重于提供基础的车辆信息&#xff08;如车速、转速、油量等&#xff09;&#xff0c;并确保驾驶员在操作时能够快速获取关键信息。相比之下&#xff0c;新能…

【JavaWeb】前端基础

JavaWeb 前端三大件&#xff1a;HTML&#xff08;主要用于网页主体结构的搭建&#xff09;&#xff0c;CSS&#xff08;页面美化&#xff09;&#xff0c;JavaScript&#xff08;主要用于页面元素的动态代理&#xff09; 1. HTML 1.1 html概述 HTML&#xff1a;Hyper Text …

SpringMVC组件解析

SpringMVC的执行流程 ① 用户发送请求至前端控制器DispatcherServlet。 ② DispatcherServlet收到请求调用HandlerMapping处理器映射器 ③ 处理器映射器找到具体的处理器(可以根据xm|配置、注解进行査找)&#xff0c;生成处理器对象及处理器 拦截器(如果有则生成)一…

数据结构C语言练习(两个栈实现队列)

一、引言 在数据结构的学习中&#xff0c;我们经常会遇到一些有趣的问题&#xff0c;比如如何用一种数据结构去实现另一种数据结构的功能。本文将深入探讨 “用栈实现队列” 这一经典问题&#xff0c;详细解析解题思路、代码实现以及每个函数的作用&#xff0c;帮助读者更好地…

nextjs使用02

并行路由 同一个页面&#xff0c;放多个路由&#xff0c;&#xff0c; 目录前面加,layout中可以当作插槽引入 import React from "react";function layout({children,notifications,user}:{children:React.ReactNode,notifications:React.ReactNode,user:React.Re…

第2.6节 iOS生成全量和增量报告

2.6.1 简介 在采集了覆盖率数据后&#xff0c;就需要生成对应需求的全量和增量覆盖率报告&#xff0c;以便对测试进行查漏补缺。IOS系统有两种开发语言&#xff0c;所以生成报告的方式也不相同&#xff0c;下面就分别介绍一下Object C和Swift语言如何生成覆盖率报告。 2.6.2 O…

应用分享 | AWG技术突破:操控钻石氮空位色心,开启量子计算新篇章!

利用AWG操作钻石中的氮空位色彩中心 金刚石中的颜色中心是晶格中的缺陷&#xff0c;其中碳原子被不同种类的原子取代&#xff0c;而相邻的晶格位点则是空的。由于色心具有明亮的单光子发射和光学可触及的自旋&#xff0c;因此有望成为未来量子信息处理和量子网络的固态量子发射…

【视觉与语言模型参数解耦】为什么?方案?

一些无编码器的MLLMs统一架构如Fuyu&#xff0c;直接在LLM内处理原始像素&#xff0c;消除了对外部视觉模型的依赖。但是面临视觉与语言模态冲突的挑战&#xff0c;导致训练不稳定和灾难性遗忘等问题。解决方案则是通过参数解耦方法解决模态冲突。 在多模态大语言模型&#xf…

重建二叉树(C++)

目录 1 问题描述 1.1 示例1 1.2 示例2 1.3 示例3 2 解题思路 3 代码实现 4 代码解析 4.1 初始化 4.2 递归部分 4.3 主逻辑 5 总结 1 问题描述 给定节点数为 n 的二叉树的前序遍历和中序遍历结果&#xff0c;请重建出该二叉树并返回它的头结点。 例如输入前序遍历序…

10乱码问题的解释(1)

在计算机中,一个汉字,占几个字节? 针对这个问题,只要你回答出一个具体的数字,就一定是错的!! 前提条件: 当前中文编码使用的是哪种方式(字符集) 计算机存的其实是二进制数字~~ 英文字母,怎么表示的?? ASCII 码表~~ 规定了每个字符,都有一个对应的数字来表示~~ 只是表示英文,…

短视频文案--钓鱼女和滑板女

短视频文案 第一个文案&#xff1a; 1标题&#xff1a;风萧萧兮易水寒&#xff0c;美女钓鱼兮不复还 2内容&#xff1a; 我站在池边的微风中&#xff0c;再也看不到曾经快乐的少女了。 风很凉&#xff0c;凉得心不知前往何处。 水很清&#xff0c;清得深知这里没鱼群。 芦苇…