• 作者:老汪软件技巧
  • 发表时间:2024-09-11 21:01
  • 浏览量:

Sentry是个好工具,它是线上问题的最后一道防线,可以让我们在用户未反馈之前就收集到问题的详细信息。尤其是在客户端崩溃的时候,可以帮助我们收集并分析崩溃的原因。

是的,这里特指Electron项目。众所周知,Electron对前端特别友好。全栈都是JS技术,开发人员只需要储备JS就可以打造一个说得过去的客户端,还是跨平台的。然而,Electron本身是基于Chromium的,而Chromium作为一个完整的浏览器实现,其技术深度和广度都是值得深挖的。这也不可避免的引入一些不稳定的功能或者说缺陷。俗话说的好,没有bug的软件只能说明这是一个小孩子的玩具。

抛开JS本身的异常,这次来搞一下Electron项目中C++代码的异常该如何分析。

默认情况下,Sentry收集上来的崩溃日志只有内存地址,对于问题分析来说,无疑是本无字天书。

这个时候,我们需要做的是,通过sentry-cli上传调试用的符号文件。Windows平台为pdb文件,macOS为dSYM。好在Electron本身在对外发布的时候一并提供了对应文件,这里只需要去github上的release页面找到对应的文件下载下来就可以了。

sentry-cli debug-files upload ~/electron-debug/electron-v26.6.10-win32-x64-pdb.zip

不出意外的话,意外来了。Electron的核心符号文件electron.exe.pdb有2.9GB之大。直接上传,sentry-cli会报告文件大小超出大小,上传失败。这里有个快速达成目的的技巧就是去release页面下载breakpad的调试符号表文件。*-symbols.zip通常只有数十兆大小,可以快速通关完成调试。但这个调试文件有个问题就是,它只有文件名和方法名。如果想要进一步分析,还需要配合Electron和Chromium的源码文件进行关联。

为了有个更好的调试体验,当然是选择更大的符号文件。可惜的是官方明确不支持上传超过2GB的调试文件。官方的建议是使用自定义调试文件仓库能力,这个方案需要花钱升级或者花钱自建服务。考虑到老板不给批预算,只能放弃唾手可得的便利。/hc/en-us/ar…

幸好Sentry是开源软件,这里选择继续深入,让我们从github上开始追根溯源。

解开sentry-cli的限制

:getsentry/sentry-cli.git

首先,根据关键字提示Skipping debug file since it exceeds 2GB检索cli的源码。快速定位到utils/dif_upload.rs中的valid_size方法。

一通修改后,报错消除。diff文件如下:

diff --git a/src/utils/dif_upload.rs b/src/utils/dif_upload.rs
index c6270a5..1bcb7b3 100644
--- a/src/utils/dif_upload.rs
+++ b/src/utils/dif_upload.rs
@@ -1987,6 +1987,7 @@ impl DifUpload {
             if chunk_options.max_file_size > 0 {
                 self.max_file_size = chunk_options.max_file_size;
             }
+            self.max_file_size = 4 * 1024 * 1024 * 1024;
             if chunk_options.max_wait > 0 {
                 self.max_wait = self
                     .max_wait

测试一下,依然报错。【这里有概率不报错,但是Sentry后台中debug files里面仍然没有显示上传的文件】

$ ./target/release/sentry-cli debug-files upload ~/electron-debug/electron-v26.6.10-win32-x64-pdb/electron.exe.pdb
> Found 1 debug information file
> Prepared debug information file for upload
> File processing complete:
    ERROR electron.exe.pdb
        internal server error

要解开限制_解解开限制_

通过self-hosted搭建本地Sentry服务

由于线上使用的Sentry版本已经在正式使用了,而我们的修改属于私有定制。为了尽可能的避免影响到线上,这里在本地通过docker搭建一个一模一样的Sentry,进行模拟调试。

/getsentry/s…

过期的镜像 cron

根据文档部署Sentry的时候,cron镜像一直构建失败。开始的时候怀疑是墙的原因,但是挂载梯子后,依然持续失败。根据报错的URL反查,原来cron的基础镜像引用的是debian的stretch版本。而stretch版本已经于22年6月停止维护。从镜像中抓取apt的配置文件来分析,只需要保留基础镜像的软件包就可以了。这里不再赘述。

调试docker-compose

根据self-hosted中的配置文件分析。最终docker-compose是通过getsentry/sentry:22.7.0来构建一系列容器的。这里通过源码,尝试修改并构建本地私有镜像sentry-22.7.0-local。最终在本地启动Sentry服务进行验证。

/getsentry/s…

最终self-hosted的修改如下:

diff --git a/.env b/.env
index 0a7bae2..f2b7433 100644
--- a/.env
+++ b/.env
@@ -5,7 +5,8 @@ SENTRY_EVENT_RETENTION_DAYS=90
 SENTRY_BIND=9000
 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails!
 # SENTRY_MAIL_HOST=example.com
-SENTRY_IMAGE=getsentry/sentry:22.7.0
+SENTRY_IMAGE=sentry-22.7.0-local:latest
 SNUBA_IMAGE=getsentry/snuba:22.7.0
 RELAY_IMAGE=getsentry/relay:22.7.0
 SYMBOLICATOR_IMAGE=getsentry/symbolicator:0.5.1
diff --git a/cron/Dockerfile b/cron/Dockerfile
index edb6407..2ac25ea 100644
--- a/cron/Dockerfile
+++ b/cron/Dockerfile
@@ -3,6 +3,7 @@ FROM ${BASE_IMAGE}
 USER 0
 RUN if [ -z "${http_proxy}" ]; then echo "Acquire::http::proxy \"${http_proxy}\";" >> /etc/apt/apt.conf; fi
 RUN if [ -z "${https_proxy}" ]; then echo "Acquire::https::proxy \"${https_proxy}\";" >> /etc/apt/apt.conf; fi
+COPY ./symbol-apt-sources.list /etc/apt/sources.list
 RUN apt-get update && apt-get install -y --no-install-recommends cron && \
     rm -r /var/lib/apt/lists/*
 COPY entrypoint.sh /entrypoint.sh

调试Sentry

通过PostgreSQL和worker镜像的错误提示,定位到代码中的FileBlobIndex(Model)和File(Model)。进一步分析,内置的BoundedPositiveIntegerField最大大小只有2G - 1也就是2147483647。BoundedPositiveIntegerField是Django.db.models.PositiveIntegerField的封装。而Sentry@22.7.0依赖的是Django@2.2.28。而PositiveBigIntegerField只有在Django@3.2开始提供。这里为了配合PG的BigInt类型,只能手动实现一遍。

diff文件如下:

diff --git a/src/sentry/db/models/fields/bounded.py b/src/sentry/db/models/fields/bounded.py
index f5638b7..a8e3398 100644
--- a/src/sentry/db/models/fields/bounded.py
+++ b/src/sentry/db/models/fields/bounded.py
@@ -4,6 +4,7 @@ from django.conf import settings
 from django.db import models
 from django.db.backends.base.base import BaseDatabaseWrapper
 from django.utils.translation import ugettext_lazy as _
+from django.db.models.fields import PositiveIntegerRelDbTypeMixin, BigIntegerField
 
 __all__ = (
     "BoundedAutoField",
@@ -13,6 +14,20 @@ __all__ = (
     "BoundedPositiveIntegerField",
 )
 
+class PositiveBigIntegerField(PositiveIntegerRelDbTypeMixin, BigIntegerField):
+    description = _("Positive big integer")
+
+    def get_internal_type(self):
+        return "BigIntegerField"
+
+    def formfield(self, **kwargs):
+        return super().formfield(
+            **{
+                "min_value": 0,
+                **kwargs,
+            }
+        )
+
 
 class BoundedIntegerField(models.IntegerField):  # type: ignore
     MAX_VALUE = 2147483647
@@ -24,6 +39,16 @@ class BoundedIntegerField(models.IntegerField):  # type: ignore
         return cast(int, super().get_prep_value(value))
 
 
+class BoundedPositiveBigIntegerField(PositiveBigIntegerField):  # type: ignore
+    MAX_VALUE = 4294967295 # LOOKHERE: for bigger debug information file
+
+    def get_prep_value(self, value: int) -> int:
+        if value:
+            value = int(value)
+            assert value <= self.MAX_VALUE
+        return cast(int, super().get_prep_value(value))
+
+
 class BoundedPositiveIntegerField(models.PositiveIntegerField):  # type: ignore
     MAX_VALUE = 2147483647
 
diff --git a/src/sentry/models/file.py b/src/sentry/models/file.py
index ae519ff..d9e1c5f 100644
--- a/src/sentry/models/file.py
+++ b/src/sentry/models/file.py
@@ -24,6 +24,8 @@ from sentry.db.models import (
     JSONField,
     Model,
 )
+from sentry.db.models.fields.bounded import BoundedPositiveBigIntegerField
 from sentry.tasks.files import delete_file as delete_file_task
 from sentry.tasks.files import delete_unreferenced_blobs
 from sentry.utils import metrics
@@ -38,7 +40,8 @@ UPLOAD_RETRY_TIME = getattr(settings, "SENTRY_UPLOAD_RETRY_TIME", 60)  # 1min
 DEFAULT_BLOB_SIZE = 1024 * 1024  # one mb
 CHUNK_STATE_HEADER = "__state"
 MULTI_BLOB_UPLOAD_CONCURRENCY = 8
-MAX_FILE_SIZE = 2**31  # 2GB is the maximum offset supported by fileblob
+MAX_FILE_SIZE = 2**32  # 4GB
 
 
 class nooplogger:
@@ -323,7 +326,7 @@ class File(Model):
     timestamp = models.DateTimeField(default=timezone.now, db_index=True)
     headers = JSONField()
     blobs = models.ManyToManyField("sentry.FileBlob", through="sentry.FileBlobIndex")
-    size = BoundedPositiveIntegerField(null=True)
+    size = BoundedPositiveBigIntegerField(null=True)
     checksum = models.CharField(max_length=40, null=True, db_index=True)
 
     # 
@@ -481,7 +484,7 @@ class FileBlobIndex(Model):
 
     file = FlexibleForeignKey("sentry.File")
     blob = FlexibleForeignKey("sentry.FileBlob", on_delete=models.PROTECT)
-    offset = BoundedPositiveIntegerField()
+    offset = BoundedPositiveBigIntegerField()
 
     class Meta:
         app_label = "sentry"
diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py
index 26a2c75..305653d 100644
--- a/src/sentry/options/defaults.py
+++ b/src/sentry/options/defaults.py
@@ -38,7 +38,7 @@ register("system.root-api-key", flags=FLAG_PRIORITIZE_DISK)
 register("system.logging-format", default=LoggingFormat.HUMAN, flags=FLAG_NOSTORE)
 # This is used for the chunk upload endpoint
 register("system.upload-url-prefix", flags=FLAG_PRIORITIZE_DISK)
-register("system.maximum-file-size", default=2**31, flags=FLAG_PRIORITIZE_DISK)
+register("system.maximum-file-size", default=2**32, flags=FLAG_PRIORITIZE_DISK)
 
 # Redis
 register(

官方构建是采用的GCP的流水线,对应本地构建过程的命令如下:

# build sentry builder -- 只需要运行一次
docker build -t sentry-builder -f ./docker/builder.dockerfile .
# build static-assets
docker run --rm -v .:/workspace sentry-builder
# 官方已经下掉了sentry-relay的0.8.13,这里需要手动hack一下
# 修改./dist/requirements-frozen.txt  sentry-relay 从0.8.13改为0.8.24
# 如果网络不稳定,apt-get一直失败,在docker/Dockerfile中添加下面两行
# RUN echo "Acquire::http::proxy \"http://本地代理地址\";" >> /etc/apt/apt.conf
# RUN echo "Acquire::https::proxy \"http://本地代理地址\";" >> /etc/apt/apt.conf
# 构建最终docker镜像
docker build -t sentry-22.7.0-local -f docker/Dockerfile .

搞定收工!

最终效果: