news 2026/9/20 19:51:53

Fleet 负载测试环境中的 SigNoz OpenTelemetry 追踪:Terraform 部署、OTLP 采集与 ClickHouse 存储运维指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Fleet 负载测试环境中的 SigNoz OpenTelemetry 追踪:Terraform 部署、OTLP 采集与 ClickHouse 存储运维指南
  • 后端
  • 前端
  • 企业应用
  • 运维
  • 网络安全

【免费下载链接】fleet

Open device management

项目地址:https://gitcode.com/GitHub_Trending/fl/fleet
点击查看免费下载

SigNoz 是 Fleet 负载测试(loadtesting)环境中的 OpenTelemetry 追踪后端,以独立 Terraform 根模块(root module)形式部署,确保在 Fleet 主基础设施启动之前即可开始采集遥测数据。本文基于 infrastructure/loadtesting/terraform/signoz/README.md 展开,结合模块内的 main.tf、otel-collector-values.yaml、outputs.tf 等源码,完整讲解从架构设计、部署顺序、Fleet 侧集成、OTLP Collector 调优到 ClickHouse 存储治理与销毁清理的端到端实战方案。读完本文,你将能够独立部署一套面向高并发负载测试的 OpenTelemetry 可观测性栈,并正确地把 Fleet 服务器的追踪数据接入 SigNoz 进行链路分析。

SigNoz 在 Fleet 负载测试体系中的定位

Fleet 的负载测试基础设施(位于 infrastructure/loadtesting/terraform 目录)由多个相互独立的 Terraform 根模块组成:共享 VPC(shared)、SigNoz(signoz)、Fleet 主基础设施(infra),以及 Android AMAPI mock、osquery 性能测试、PMM 监控等配套模块。SigNoz 模块的职责非常单一:为 Fleet 负载测试环境提供 OpenTelemetry 追踪能力

为什么选 SigNoz 而不是 Elastic APM

从 infra/locals.tf 的注释可以读到关键决策背景:OpenTelemetry 是唯一被采用的追踪方案,Elastic APM 不被支持。原因是其埋点(instrumentation)是 gorilla 专属的,当 Elastic APM 激活时,Fleet 服务器会关闭标准库 ServeMux 的快速路径(fast path),这对负载测试来说,追踪带来的开销远大于收益。因此负载测试默认完全关闭追踪,只有显式设置enable_otel=true时才启用。

架构总览

SigNoz 模块的架构要点(README 中的 Architecture 一节):

  • EKS 集群:每个工作区(workspace)一套独立集群,命名形如signoz-victor-baseline(前缀signoz-加上当前 Terraform workspace 名,见 main.tf 中的local.cluster_name = "signoz-${terraform.workspace}");
  • Kubernetes 版本:v1.31;
  • 节点组:README 描述为 2 台 t3.xlarge,而当前 main.tf 实际配置为单个m6i.8xlarge托管节点(min/max/desired 均为 1),以承载 ClickHouse 与 OTLP Collector 的高资源需求;
  • 核心组件
    • SigNoz UI:公网 LoadBalancer 暴露,端口 8080;
    • OTLP Collector:内网 LoadBalancer 暴露,端口 4317(gRPC),供 Fleet 服务器发送追踪数据;
    • ClickHouse:存储追踪数据,README 提及 200Gi,而当前 main.tf 将持久卷大小设置为 600Gi,存储类为 gp3。

部署顺序(关键约束)

SigNoz 必须先于 Fleet 主基础设施部署,否则无法捕获 Fleet 初始启动阶段的遥测数据。标准顺序为:

  1. 部署共享 EKS VPC(一次性、跨工作区共享,通常已存在);
  2. 部署 SigNoz(即本模块);
  3. 部署 Fleet 基础设施(infrastructure/loadtesting/terraform/infra)。

这个顺序在 infra/signoz.tf 中通过terraform_remote_state数据源体现:Fleet 基础设施会读取 SigNoz 模块的远程状态(remote state)来获取 OTLP 端点,因此 SigNoz 必须先 apply。

模块文件与前置依赖

SigNoz 模块目录结构非常精简:

infrastructure/loadtesting/terraform/signoz/ ├── README.md # 部署与运维说明 ├── main.tf # 核心资源:EKS、EBS CSI、StorageClass、SigNoz Helm Release、销毁清理 ├── otel-collector-values.yaml # OTLP Collector Helm values 覆盖 ├── outputs.tf # 提供给 Fleet 基础设施消费的输出 └── variables.tf # 唯一变量:aws_region

依赖的 Provider 与版本约束

main.tf 声明了 Terraform 与 Provider 版本要求:

组件版本约束用途
Terraform>= 1.5模块运行的最低版本
AWS Provider>= 5.68.0创建 EKS、IAM、存储相关资源
Helm Provider~> 2.11部署 SigNoz chart
Kubernetes Provider~> 2.23创建 StorageClass、读取 Service
Null Provider~> 3.2执行销毁前清理脚本

远程状态与共享 VPC

模块使用 S3 作为远程状态后端,配置了 workspace key 前缀、加密、DynamoDB 锁以及assume_role

backend "s3" { bucket = "fleet-terraform-state20220408141538466600000002" key = "loadtesting/loadtesting/signoz/terraform.tfstate" workspace_key_prefix = "loadtesting" region = "us-east-2" encrypt = true kms_key_id = "9f98a443-ffd7-4dbe-a9c3-37df89b2e42a" dynamodb_table = "tf-remote-state-lock" assume_role = { role_arn = "arn:aws:iam::353365949058:role/terraform-loadtesting" } }

EKS 集群创建在共享 VPC 中,通过terraform_remote_state读取shared模块的输出获取 VPC ID 与私有子网(见 main.tf 与 L107-L117),这保证了 SigNoz 的内网 OTLP 端点能被同 VPC 内的 Fleet 服务器访问。

部署步骤详解

按照 README 的 Usage 一节,部署流程如下:

# 1. 初始化并选择工作区 cd infrastructure/loadtesting/terraform/signoz terraform init terraform workspace new <workspace_name> # 与你的 infra 工作区保持一致 # 2. 部署 SigNoz terraform apply # 3. 等待部署完成(约 10-15 分钟) # OTLP collector 端点会显示在 outputs 中 # 4. 部署 Fleet 主基础设施 cd ../infra terraform apply

工作区命名一致性

工作区名称至关重要:EKS 集群名、IAM 角色名(如ebs-csi-driver角色、${local.cluster_name}-ebs-csi-driver)、以及 AWS 资源的 default tag 都会引用terraform.workspace(见 main.tf)。同时,Fleet 基础设施(infra)读取 SigNoz 远程状态时也使用当前工作区(infra/signoz.tf 中workspace = terraform.workspace),因此 SigNoz 与 infra 必须使用同名工作区,才能正确关联。

部署耗时说明

helm_release.signoz设置了timeout = 3600(60 分钟),注释明确说明这是为了容忍 AWS 在 apply 与 destroy 阶段的最坏情况延迟(main.tf)。正常情况下整体部署约 10-15 分钟完成,其中包含 EKS 集群创建、EBS CSI Driver 就绪等待(time_sleep60 秒)、gp3 StorageClass 创建以及 SigNoz 全家桶(UI、OTLP Collector、ClickHouse、ZooKeeper、Alertmanager)的 Helm 安装。

访问 SigNoz UI

部署完成后有两种方式获取 UI 地址:

# 方式一:直接输出 UI URL 命令 terraform output -raw get_signoz_ui_url | bash # 方式二:配置 kubectl 后手动查询 $(terraform output -raw configure_kubectl) kubectl get svc -n signoz signoz -o jsonpath='http://{.status.loadBalancer.ingress[0].hostname}:8080'

对应的输出定义在 outputs.tf:

  • configure_kubectl:生成aws eks update-kubeconfig --region <region> --name <cluster>命令,用$(...)包裹后可直接执行;
  • get_signoz_ui_url:查询signoz命名空间下名为signoz的 Service 的 LoadBalancer hostname,拼上:8080端口。

如果是在infra目录部署完成后访问,也可以使用 infra/README.md 提供的等价命令:

$(terraform output -raw signoz_configure_kubectl) && kubectl get svc signoz -n signoz -o jsonpath='http://{.status.loadBalancer.ingress[0].hostname}:8080'

输出与 Fleet 基础设施的集成

模块 Outputs

outputs.tf 共导出 7 个输出,其中被 Fleet 基础设施通过远程状态消费的关键三个是:

输出说明
cluster_nameEKS 集群名称,Fleet 基础设施用于关联与诊断
otel_collector_endpoint内网 OTLP 端点(hostname:4317),Fleet 服务器发送追踪数据的地址
configure_kubectl配置 kubectl 访问 SigNoz 集群的命令

otel_collector_endpoint通过 Kubernetes Provider 的data "kubernetes_service"数据源,读取signoz-otel-collectorService 的 LoadBalancer hostname 并拼接:4317端口(outputs.tf)。它还提供了get_otlp_endpoint输出,方便手动查询 OTLP 端点。

Fleet 侧如何消费这些输出

Fleet 基础设施模块(infra)在 signoz.tf 中通过terraform_remote_state读取 SigNoz 的远程状态,并使用变量enable_otel(默认false,定义于 infra/variables.tf)控制是否启用追踪:

data "terraform_remote_state" "signoz" { count = var.enable_otel ? 1 : 0 backend = "s3" # ... 与 SigNoz 模块相同的 bucket/key/workspace_key_prefix 配置 workspace = terraform.workspace }

启用后,infra/locals.tf 会为 Fleet 服务器注入以下环境变量:

otel_environment_variables = var.enable_otel ? { OTEL_SERVICE_NAME = "fleet" OTEL_RESOURCE_ATTRIBUTES = "deployment.environment.name=${terraform.workspace},deployment.environment=${terraform.workspace}" OTEL_EXPORTER_OTLP_ENDPOINT = "http://${data.terraform_remote_state.signoz[0].outputs.otel_collector_endpoint}" FLEET_LOGGING_TRACING_ENABLED = "true" FLEET_LOGGING_TRACING_TYPE = "opentelemetry" } : {}

这些环境变量与 Fleet 服务器配置一一对应:FLEET_LOGGING_TRACING_ENABLED映射logging.tracing_enabledFLEET_LOGGING_TRACING_TYPE映射logging.tracing_type(配置解析见 server/config/config.go 与 server/config/config.go)。Fleet 服务器通过OTELEnabled()方法判断是否启用 OpenTelemetry:

// server/config/config.go#L939-L941 func (f FleetConfig) OTELEnabled() bool { return f.Logging.TracingEnabled && f.Logging.TracingType != "elasticapm" }

即:只有tracing_enabled=truetracing_type不是elasticapm时,OTel 追踪才会生效——这与 infra 模块"OpenTelemetry 是唯一选项"的设计完全吻合。OTEL_EXPORTER_OTLP_ENDPOINT指向 SigNoz 内网 OTLP Collector(http://<internal-lb-hostname>:4317),追踪数据默认通过 gRPC 协议上报。

在 infra 中启用追踪

按照 infra/README.md 的说明,部署 Fleet 基础设施时加上enable_otel=true即可同时启用 SigNoz 追踪:

terraform apply -var=tag=v4.72.0 -var=fleet_task_count=20 -var=fleet_task_memory=4096 -var=fleet_task_cpu=512 -var=database_instance_size=db.t4g.large -var=database_instance_count=3 -var=redis_instance_size=cache.t4g.small -var=redis_instance_count=3 -var=enable_otel=true

源码级深潜:Terraform 模块实现要点

EKS 集群与插件安装顺序

main.tf 中有一处极具工程价值的注释:核心插件必须在节点组之前安装,否则会死锁。原因在于:节点需要 VPC CNI 才能变为 Ready,而 Terraform 在创建附加组件(addons)前会等待节点 Ready,两者互相等待导致节点卡在 NotReady。解决方案是使用before_compute = true,让 vpc-cni、kube-proxy、coredns 三个 addon 在节点组完成之前安装:

addons = { vpc-cni = { most_recent = true, before_compute = true } kube-proxy = { most_recent = true, before_compute = true } coredns = { most_recent = true, before_compute = true } }

集群启用了 cluster creator admin 权限(enable_cluster_creator_admin_permissions = true)与 IRSA(enable_irsa = true),为后续 EBS CSI Driver 服务账户授权做准备。

EBS CSI Driver 与 gp3 存储类

ClickHouse、ZooKeeper、Alertmanager 都需要持久化存储。模块专门为 EBS CSI Driver 创建了 IRSA 角色(ebs-csi-driver),并单独挂载aws-eks-addon资源以避免与 OIDC Provider 的循环依赖(main.tf)。随后创建默认的gp3存储类,启用加密、容量扩容与WaitForFirstConsumer绑定模式:

resource "kubernetes_storage_class_v1" "gp3" { storage_provisioner = "ebs.csi.aws.com" reclaim_policy = "Delete" allow_volume_expansion = true volume_binding_mode = "WaitForFirstConsumer" parameters = { type = "gp3", encrypted = "true", fsType = "ext4" } }

所有 SigNoz 组件(ClickHouse 600Gi、ZooKeeper、Alertmanager)的持久卷都显式指定storageClass = "gp3"(main.tf)。

SigNoz Helm Release 的关键参数

helm_release.signoz使用官方 chart(https://charts.signoz.io,chart 名为signoz),安装在signoz命名空间(main.tf),核心覆盖参数如下:

参数说明
cloudfalse自托管模式,不使用 SigNoz Cloud
signoz.service.typeLoadBalancerSigNoz UI 公网暴露
otelCollector.service.typeLoadBalancerOTLP Collector 服务暴露
otelCollector.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-schemeinternal关键:OTLP Collector 仅内网可达,不对外开放
clickhouse.persistence.size600GiClickHouse 持久卷大小
clickhouse.persistence.storageClassgp3使用 gp3 存储类
zookeeper.persistence.storageClassgp3ZooKeeper 存储类
alertmanager.persistence.storageClassgp3Alertmanager 存储类
clickhouse.resources.requests.cpu/memory6000m/12GiClickHouse 请求资源
clickhouse.resources.limits.cpu/memory12000m/24GiClickHouse 资源上限
otelCollector.resources.requests.cpu/memory3000m/24GiOTLP Collector 请求资源
otelCollector.resources.limits.cpu/memory12000m/36GiOTLP Collector 资源上限
otelCollector.replicaCount3OTLP Collector 三副本,保证采集高可用

代码注释明确说明了资源调整的动机:chart 默认的 ClickHouse100mCPU 与200Mi内存对高吞吐遥测数据来说远远不够,必须按负载测试的规模显著放大。

OTLP Collector 配置详解

模块通过 otel-collector-values.yaml 对 OTLP Collector 进行"完全显式"的配置覆盖,目标是生产级稳定性。这份配置是理解整个追踪链路的钥匙。

接收端(Receivers)

接收器协议端点说明
otlp/grpcgRPC0.0.0.0:4317标准 OTLP 入口,max_recv_msg_size_mib: 16限制单条消息大小
otlp/httpHTTP0.0.0.0:4318OTLP HTTP 入口
jaeger/grpcgRPC0.0.0.0:14250兼容 Jaeger gRPC 协议
jaeger/thrift_httpThrift0.0.0.0:14268兼容 Jaeger Thrift 协议

处理管道(Processors)

  • batchsend_batch_size=6000send_batch_max_size=7500timeout=5s,为 traces/logs/metrics 主链路设计,平衡吞吐与内存;
  • batch/metersend_batch_max_size=75000send_batch_size=60000timeout=1s,面向 meter 指标的高容量批处理;
  • memory_limiterlimit_mib=33792(约 33Gi)、spike_limit_mib=1536check_interval=1s,与 Collector 36Gi 的内存上限配合,防止 OOM;
  • signozspanmetrics/delta:把 span 转换为 RED 指标(请求率、错误率、耗时分布),latency_histogram_buckets定义了从 100us 到 60s 的 16 档直方图桶,dimensions_cache_size=100000缓存维度,aggregation_temporality=DELTA使用增量聚合。

导出端(Exporters)与管道编排

导出器超时重试发送队列目标
clickhousetraces60s5s→30s 指数退避,最长 300s30 消费者,15000 队列追踪数据写入 ClickHouse
clickhouselogsexporter60s同上30 消费者,15000 队列日志写入 ClickHouse
signozclickhousemetrics45s指标写入 ClickHouse
metadataexporter60s同上元数据导出
signozclickhousemeter45s关闭队列meter 指标写入 ClickHouse

service.pipelines定义了四条链路:traces(otlp+jaeger → memory_limiter → signozspanmetrics/delta → batch → clickhousetraces + metadataexporter + signozmeter)、metricslogsmetrics/meter。另有两个值得注意的细节:

  • lowCardinalityExceptionGrouping: true:开启低基数异常分组,压缩异常告警噪声;
  • signozmeter连接器设置了metrics_flush_interval: 1h,并按service.namedeployment.environmenthost.name三个维度聚合;
  • service.telemetry.logs.encoding: json:Collector 自身日志输出为 JSON,便于接入日志系统排查。

ClickHouse 存储管理与保留期治理

README 的 "Managing storage and retention" 一节是整个运维部分的重心。ClickHouse 存储有限(当前配置 600Gi gp3 卷),负载测试会产生海量 trace,因此必须主动治理。

1. 缩短追踪数据保留期

在 SigNoz UI 中操作:

  • 进入Settings → Retention Period
  • 针对 traces 降低保留期(默认值对负载测试场景过长);
  • 活跃负载测试环境建议设置为1-3 天

2. 监控 ClickHouse 存储用量

# 查看 ClickHouse pod 存储使用情况 kubectl exec -n signoz chi-signoz-clickhouse-cluster-0-0-0 -- df -h /var/lib/clickhouse # 按数据库统计磁盘占用 kubectl exec -n signoz chi-signoz-clickhouse-cluster-0-0-0 -- clickhouse-client --query "SELECT database, formatReadableSize(sum(bytes_on_disk)) AS size FROM system.parts WHERE active GROUP BY database ORDER BY sum(bytes_on_disk) DESC"

第一条命令检查数据目录所在文件系统的剩余空间;第二条命令通过 ClickHouse 的system.parts系统表,按数据库聚合活跃分区(active)的磁盘占用,并按从大到小排序,能够快速定位是 trace、log 还是 metric 库在膨胀。

3. 存储写满的后果

README 明确列出了存储耗尽时的行为,这决定了监控的重要性:

  • ClickHouse 会拒绝新的写入请求;
  • 新的 trace 将无法被采集
  • OTLP Collector 会记录写入失败的报错日志;
  • Fleet 服务器继续运行,但追踪数据会丢失。

换句话说,存储写满不会导致负载测试失败,但会让追踪数据出现静默缺口,误导后续性能分析。因此,在长跑型负载测试前,务必先调整保留期并确认磁盘水位。

销毁流程与已知问题处理

基本销毁

terraform destroy

已知的销毁挂起问题(fleetdm/fleet#35405)与预销毁清理

main.tf 中实现了一个非常值得借鉴的null_resource预销毁清理机制。其背景是:SigNoz chart 安装了 Altinity clickhouse-operator,该 operator 会在其ClickHouseInstallation(CHI)自定义资源上放置 finalizer。执行helm uninstall时 operator 先被移除,于是没有任何组件来处理这个 finalizer,导致 CHI 删除卡死,进而拖住 PVC 与命名空间删除,最终让helm_releasewait = true)在销毁时等待到完整超时。

清理脚本利用"Terraform 在销毁时先销毁依赖方"的特性(depends_on = [helm_release.signoz]保证该资源先于 helm_release 执行),依次完成:

  1. 先删除 clickhouse-operator Deployment:因为 operator 运行时会立即重新添加 CHI finalizer,仅 patch finalizer 会输掉竞争;删除后还会等待 pod 真正终止(kubectl wait --for=delete,上限 120s,外加轮询兜底);
  2. 剥离 CHI 的 finalizerkubectl patch clickhouseinstallations.clickhouse.altinity.com signoz-clickhouse -n signoz --type merge -p '{"metadata":{"finalizers":[]}}',使 CHI 可以立即删除;
  3. 删除 LoadBalancer Service:释放 AWS ELB,避免其在 EKS/VPC 拆除阶段残留;
  4. 排队删除 PVC--wait=false):让 EBS CSI Driver 在集群仍存活时释放 EBS 卷(包括 600Gi 的 ClickHouse 卷),否则集群销毁后这些卷会成为孤儿资源。

每个步骤都具备幂等性与容错性(on_failure = continue|| exit 0--ignore-not-found),对全新或部分创建的堆栈来说,这些清理动作只是"无事可做"的空操作。

输出清单与快速参考

部署完成后,模块导出的全部输出如下(见 outputs.tf):

输出类型说明
cluster_namestringEKS 集群名称
cluster_endpointstringEKS 集群 API 端点
cluster_certificate_authority_datastring集群 CA 证书
configure_kubectlstring配置 kubectl 的命令
get_signoz_ui_urlstring获取 SigNoz UI 地址的命令
get_otlp_endpointstring获取 OTLP 端点的命令
otel_collector_endpointstring内网 OTLP 端点(hostname:4317),供 Fleet 消费

最佳实践小结

结合 README 与源码,SigNoz 负载测试环境的运维要点可归纳为:

  1. 顺序不可颠倒:SigNoz 必须先于 Fleet 基础设施部署,否则 Fleet 启动阶段的 trace 无法采集;工作区命名必须与 infra 保持一致,远程状态才能正确关联。
  2. 存储治理要前置:负载测试开始前先调整 Settings → Retention Period 到 1-3 天,并用clickhouse-client查询system.parts持续监控磁盘水位,避免 ClickHouse 静默拒绝写入导致 trace 缺口。
  3. OTLP 端点保持内网otelCollector.service的 AWS LoadBalancer scheme 被显式设置为internal,这是安全基线,不应移除。
  4. 资源规格按负载测试规模调优:chart 默认资源远不足以支撑高吞吐遥测,当前配置(ClickHouse 12Gi/24Gi、Collector 3 副本 24Gi/36Gi、600Gi 存储)是经过实践校准的值,调整时需同步评估 EKS 节点规格(m6i.8xlarge)。
  5. 销毁依赖预清理钩子null_resource的 pre-destroy 清理(删 operator、剥离 finalizer、释放 ELB 与 EBS 卷)是避免销毁挂起的必要保障,涉及 SigNoz 版本升级或重建时应保留该逻辑。

通过以上部署与运维流程,即可在 Fleet 负载测试环境中稳定获得端到端的 OpenTelemetry 链路追踪能力,为性能分析与瓶颈定位提供可靠的数据基础。

  • 后端
  • 前端
  • 企业应用
  • 运维
  • 网络安全

【免费下载链接】fleet

Open device management

项目地址:https://gitcode.com/GitHub_Trending/fl/fleet
点击查看免费下载

相关推荐

上一篇:解密Vibe:基于Tauri+Whisper的本地化语音转录技术深度解析
下一篇:7个Litellm运营优化技巧:流程自动化与效率提升全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 19:49:54

OpenResearch实践指南:从实验记录到可复现研究的完整工作流

不用急着下定义。我第一次接触“OpenResearch”这个词&#xff0c;是在一次课题组内部讨论上&#xff0c;有人抱怨实验数据存在自己电脑里三个月都没人看&#xff0c;代码也只够自己复现一遍。后来我们试着把整个研究过程端到端摊开——从选题、检索、实验记录、代码、数据&…

作者头像 李华
网站建设 2026/9/20 19:48:50

从零孵化提示词工程师:Midjourney与Stable Diffusion实战指南

我前阵子帮一家做茶饮的品牌方赶一批电商主图&#xff0c;30多张产品图&#xff0c;从需求拆解到最终交付只用了4天。团队里没有专业设计师参与&#xff0c;真正干活的就是一个在Grix里孵化出来的“图像提示词工程师”——一个原本只会套别人模板的运营同学。这个经历让我特别想…

作者头像 李华
网站建设 2026/9/20 19:48:31

VS Code 跨平台安装与配置指南:从零搭建高效开发环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 19:45:29

博图V20安装全攻略:配置要求、详细步骤与故障排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华