- 作者:老汪软件技巧
- 发表时间: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 .
搞定收工!
最终效果: