Мой опыт установки Sentry self-hosted

Страницы:  1

Ответить
 

Professor Seleznov


Привет! Меня зовут Даниил Ткаченко, я веб‑разработчик в ИТ‑компании «Активика». В статье я поделюсь опытом развёртывания Sentry self‑hosted для высоконагруженного проекта. Несмотря на обилие материалов по SaaS‑версии, актуальных гайдов по self‑hosted‑установке почти нет — особенно с учётом современных требований к производительности и отказоустойчивости.
Мы столкнулись с рядом проблем: нестабильностью на базовом хостинге, отсутствием перехвата HTTP‑ошибок и быстрым заполнением диска. Под катом разберу каждую проблему, покажу код решений и дам рекомендации для тех, кто планирует развернуть Sentry самостоятельно.
Статья будет полезна разработчикам и DevOps‑инженерам без опыта работы с self‑hosted Sentry.
Проблема 1. Нестабильность на базовом хостинге
Всё началось с того, что на нашем базовом хостинге возникли ошибки: Sentry стабильно не запускался из‑за особенностей виртуализации. Нельзя было даже развернуть инструмент. Поэтому мы переехали на другой хостинг — и тогда всё получилось.
Но тут меня тоже поджидали проблемы: память быстро забивалась, и не просто логами в том же ClickHouse или файлами реплеев, а логами в Kafka.
За время тестовых подключений к демоверсии проекта (а ей пользуется ограниченное число пользователей для тестирования новых функций) Sentry падал два раза, а демо-версия проекта грузилась очень долго. 
Когда Sentry падал, сайт грузился очень долго — около 30–40 секунд. Это происходило потому, что клиент пытался не только загрузить JS‑скрипты с внешних CDN, но и отправить события на сервер Sentry. Запросы зависали на таймаутах, блокируя загрузку страницы.
Конфигурация сервера, которую использовали: 32 ГБ RAM, 4 ядра CPU, 40 ГБ SSD. Установили до ажиотажа с ценами на память — потом пришлось уменьшить RAM до 16-24 ГБ, увеличить SSD до 100ГБ и добавить swap-файл, чтобы не переплачивать. 
Лайфхаки для ускорения загрузки сайта, когда сентри не доступен
Даже при self‑hosted решении возможны сбои: DDoS‑атаки, перезагрузка кластера, ошибки ПО. Делюсь проверенными мной способами минимизировать их влияние, чтобы избежать длительной загрузки и работы сайта, даже при полном отказе Sentry.
Хак 1. Локальная урезанная версия JS‑библиотеки
Я скачал JS-библиотеку Sentry и хранил её локально в урезанной версии. Теперь никаких внешних запросов — загрузка мгновенная и без зависаний.
Хак 2.  Таймауты на соединения
Добавляем код с таймаутами на соединения поверх SDK, который прерывает запросы к Sentry, если они длятся дольше 300 миллисекунд.
Это предотвращает переполнение очереди запросов в браузере и сохраняет отзывчивость сайта.
const SENTRY_TIMEOUT_MS = 300;
function makeCustomTimeoutTransport(options) {
// The 'makeRequest' function is the core where you control the fetch call.
function makeRequest(request) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), SENTRY_TIMEOUT_MS); // 3-second timeout
const requestOptions = {
body: request.body,
method: 'POST',
referrerPolicy: 'origin',
headers: options.headers,
signal: controller.signal, // <-- Connect the AbortController
...options.fetchOptions,
};
// Return the fetch promise. It will be rejected if it times out.
return fetch(options.url, requestOptions)
.then(response => {
clearTimeout(timeoutId); // Clear timer on success
return {
statusCode: response.status,
headers: {
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
'retry-after': response.headers.get('Retry-After'),
},
};
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.warn('Sentry event dropped due to send timeout.');
error.message = 'Sentry event dropped due to send timeout.';
}
throw error; // Re-throw to let the SDK handle the failure
});
}
// Use the official helper to build a compliant transport.
return Sentry.createTransport(options, makeRequest);
}
Sentry.init({
transport: makeCustomTimeoutTransport,

});
Эти изменения мы протестировали на проде. 
Результат: после внедрения перестало забиваться лог бесконечными попытками достучаться до Sentry.
Проблема 2. Отсутствие перехвата HTTP‑ошибок
Оказалось, что Sentry из коробки не ловит HTTP-ошибки на асинхронных запросах — картинки 404, сломанные API-вызовы или страницы проходят мимо. Я написал небольшое расширение, примерно на 100 строк JS, которое:
  • перехватывает запросы через fetch и XMLHttpRequest
  • отслеживает ошибки (коды 4xx, 5xx)
  • отправляет информацию в Sentry с деталями запроса и ответа
Ключевые возможности расширения:
  • фильтрация по URL и кодам статусов
  • ограничение размера данных (MAX_CAPTURE_LENGTH = 1000 байт)
  • санитизация небезопасных заголовков (cookie, authorization и т. д.)
  • поддержка PII (персональных данных) при необходимости
// sentry-http-integration.js
/**
* Расширенная интеграция для перехвата HTTP-запросов и отправки информации об ошибках в Sentry.
* Основана на официальной реализации httpClientIntegration из Sentry, но с дополнительными возможностями
* для более детального анализа запросов и ответов.
*
* @see Официальная документация Sentry: https://docs.sentry.io/platforms/javascript/
*/
class SentryHttpDataIntegration {
/**
* Уникальный идентификатор интеграции.
* @type {string}
*/
static id = 'SentryHttpDataIntegration';
/**
* Имя интеграции.
* @type {string}
*/
name = 'SentryHttpDataIntegration';
/**
* Максимальная длина захватываемых данных в байтах.
* Используется для предотвращения отправки слишком больших данных в Sentry.
* @type {number}
*/
static MAX_CAPTURE_LENGTH = 1000;
/**
* Список "небезопасных" заголовков, доступ к которым ограничен в браузерах.
* @type {Array}
* @private
*/
static _RESTRICTED_HEADERS = [
'set-cookie',
'set-cookie2',
'cookie2',
'cookie',
'authorization',
'proxy-authorization',
'sec-',
'proxy-'
];
/**
* Настройки для фильтрации HTTP-запросов, по которым будут отправляться события.
* @type {Object}
* @property {Array|number>} failedRequestStatusCodes - Коды HTTP-статусов, которые считаются ошибками
* @property {Array} failedRequestTargets - URL-шаблоны для отслеживания ошибок
* @private
*/
_options = {
failedRequestStatusCodes: [[500, 599]],
failedRequestTargets: [/.*/],
};
/**
* Конструктор класса.
* @param {Object} options - Настройки интеграции
* @param {Array|number>} [options.failedRequestStatusCodes] - Коды HTTP-статусов для отслеживания
* @param {Array} [options.failedRequestTargets] - URL-шаблоны для отслеживания
*/
constructor(options = {}) {
this._options = {
...this._options,
...options,
};
}
/**
* Устанавливает перехватчики для fetch и XMLHttpRequest.
* Этот метод вызывается Sentry при инициализации интеграции.
*/
setupOnce() {
this._wrapFetch();
this._wrapXHR();
}
/**
* Проверяет, должен ли запрос быть обработан на основе его URL и статуса ответа.
* @param {number} status - Код HTTP-статуса
* @param {string} url - URL запроса
* @returns {boolean} - true, если запрос соответствует критериям для отправки в Sentry
* @private
*/
_shouldCaptureResponse(status, url) {
// Не обрабатываем запросы к самому Sentry
if (this._isSentryRequest(url)) {
return false;
}
return (
this._isInStatusCodeRange(status) &&
this._isUrlInTargets(url)
);
}
/**
* Проверяет, соответствует ли код статуса заданным диапазонам ошибок.
* @param {number} status - Код HTTP-статуса
* @returns {boolean} - true, если статус входит в диапазоны ошибок
* @private
*/
_isInStatusCodeRange(status) {
return this._options.failedRequestStatusCodes.some(range => {
if (typeof range === 'number') {
return status === range;
}
return status >= range[0] && status <= range[1];
});
}
/**
* Проверяет, соответствует ли URL заданным шаблонам.
* @param {string} url - URL запроса
* @returns {boolean} - true, если URL соответствует хотя бы одному шаблону
* @private
*/
_isUrlInTargets(url) {
return this._options.failedRequestTargets.some(target => {
if (typeof target === 'string') {
return url.includes(target);
}
return target.test(url);
});
}
/**
* Проверяет, является ли запрос запросом к Sentry.
* @param {string} url - URL запроса
* @returns {boolean} - true, если запрос направлен к Sentry
* @private
*/
_isSentryRequest(url) {
// Простая проверка на запросы к Sentry
// В реальной реализации может использоваться isSentryRequestUrl из @sentry/core
return url.includes('sentry.io') || url.includes('ingest.sentry.io');
}
/**
* Перехватывает нативный fetch API для отслеживания запросов.
* Заменяет глобальную функцию fetch на свою обертку, которая:
* - сохраняет информацию о запросе
* - выполняет оригинальный запрос
* - перехватывает ошибки и ответы с кодом ошибки (4xx, 5xx)
* - отправляет информацию в Sentry в случае ошибки
* @private
*/
_wrapFetch() {
if (typeof window.fetch !== 'function') {
console.warn('Fetch API не поддерживается в этом окружении');
return;
}
const originalFetch = window.fetch;
const self = this;
window.fetch = async function (input, init) {
// Получаем метод запроса (GET по умолчанию)
const method = init?.method ?? 'GET';
// Создаем объект Request для унификации работы с разными форматами параметров
const request = self._getRequest(input, init);
// Захватываем данные запроса, если они есть
const requestData = init?.body
? await self._captureRequestBody(init.body)
: null;
// Извлекаем заголовки запроса, если sendDefaultPii активен
let requestHeaders;
if (self._shouldSendDefaultPii()) {
try {
requestHeaders = self._extractFetchHeaders(request.headers);
// Удаляем небезопасные заголовки
self._sanitizeHeaders(requestHeaders);
} catch (e) {
// Игнорируем ошибки при извлечении заголовков
}
}
try {
// Выполняем оригинальный запрос
const response = await originalFetch.apply(this, arguments);
// Если ответ соответствует критериям ошибки
if (self._shouldCaptureResponse(response.status, response.url)) {
// Извлекаем заголовки ответа, если активен sendDefaultPii
let responseHeaders;
if (self._shouldSendDefaultPii()) {
try {
responseHeaders = self._extractFetchHeaders(response.headers);
// Удаляем небезопасные заголовки
self._sanitizeHeaders(responseHeaders);
} catch (e) {
// Игнорируем ошибки при извлечении заголовков
}
}
// Захватываем тело ответа
const responseData = await self._captureResponseBody(response.clone());
// Отправляем информацию об ошибке в Sentry
self._captureError({
method,
url: response.url,
requestData,
requestHeaders,
responseData,
responseHeaders,
status: response.status
});
}
return response;
} catch (error) {
// В случае сетевой ошибки отправляем информацию в Sentry
self._captureError({
method,
url: request.url,
requestData,
requestHeaders,
error: error.message,
status: ''
});
// Передаем ошибку дальше
throw error;
}
};
}
/**
* Создает объект Request из параметров fetch.
* @param {RequestInfo} input - URL или объект Request
* @param {RequestInit} [init] - Опции запроса
* @returns {Request} - Объект Request
* @private
*/
_getRequest(input, init) {
if (!init && input instanceof Request) {
return input;
}
// Если тело оригинального Request уже использовано, просто возвращаем его
if (input instanceof Request && input.bodyUsed) {
return input;
}
return new Request(input, init);
}
/**
* Перехватывает XMLHttpRequest для отслеживания запросов.
* Модифицирует прототип XMLHttpRequest, заменяя методы open и send
* для сбора информации о запросах и перехвата ошибок.
* @private
*/
_wrapXHR() {
if (typeof XMLHttpRequest === 'undefined') {
console.warn('XMLHttpRequest не поддерживается в этом окружении');
return;
}
const originalSend = XMLHttpRequest.prototype.send;
const originalOpen = XMLHttpRequest.prototype.open;
const self = this;
// Сохраняем метод и URL запроса через перехват open
XMLHttpRequest.prototype.open = function (method, url) {
this._method = method;
this._url = url;
this._requestHeaders = {};
return originalOpen.apply(this, arguments);
};
// Перехватываем setRequestHeader для сохранения заголовков
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
if (!this._requestHeaders) {
this._requestHeaders = {};
}
this._requestHeaders[name] = value;
return originalSetRequestHeader.apply(this, arguments);
};
// Перехватываем метод send для отслеживания запросов
XMLHttpRequest.prototype.send = function (body) {
// Захватываем данные запроса
const requestData = self._captureSyncData(body);
// Санитизируем заголовки запроса
let requestHeaders;
if (self._shouldSendDefaultPii() && this._requestHeaders) {
requestHeaders = {...this._requestHeaders};
self._sanitizeHeaders(requestHeaders);
}
// Добавляем обработчик события завершения запроса
this.addEventListener('loadend', () => {
// Если статус ответа указывает на ошибку и URL соответствует критериям
if (self._shouldCaptureResponse(this.status, this.responseURL ?? this._url)) {
// Безопасно получаем заголовки ответа
let responseHeaders;
if (self._shouldSendDefaultPii()) {
try {
// Используем безопасный метод получения заголовков ответа
responseHeaders = self._getXHRResponseHeaders(this);
// Дополнительно санитизируем заголовки
self._sanitizeHeaders(responseHeaders);
} catch (e) {
// Игнорируем ошибки при извлечении заголовков
}
}
// Захватываем тело ответа
const responseData = self._captureSyncXHRResponseData(this);
// Отправляем информацию об ошибке в Sentry
self._captureError({
method: this._method ?? 'GET',
url: this.responseURL ?? this._url,
requestData,
requestHeaders,
responseData,
responseHeaders,
status: this.status
});
}
});
// Вызываем оригинальный метод send
return originalSend.call(this, body);
};
}
/**
* Удаляет небезопасные заголовки из объекта заголовков.
* @param {Object} headers - Объект с заголовками
* @private
*/
_sanitizeHeaders(headers) {
if (!headers) return;
// Удаляем небезопасные заголовки
Object.keys(headers).forEach(key => {
const lowerKey = key.toLowerCase();
if (SentryHttpDataIntegration._RESTRICTED_HEADERS.some(
restrictedHeader => lowerKey === restrictedHeader || lowerKey.startsWith(restrictedHeader)
)) {
delete headers[key];
}
});
}
/**
* Безопасно захватывает тело ответа XHR.
* @param {XMLHttpRequest} xhr - Объект XMLHttpRequest
* @returns {string|Object|null} - Захваченные данные ответа
* @private
*/
_captureSyncXHRResponseData(xhr) {
try {
// Безопасно получаем тело ответа, обрабатывая разные типы ответов
let responseData = xhr.response;
// Проверяем тип ответа и при необходимости конвертируем
if (responseData) {
if (typeof responseData === 'object') {
try {
// Для объектов попробуем преобразовать их в строку JSON
responseData = JSON.stringify(responseData).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
} catch (e) {
// Если не удалось преобразовать, вернем тип объекта
responseData = `[${Object.prototype.toString.call(responseData)}]`;
}
} else if (typeof responseData === 'string') {
// Ограничиваем длину строки
responseData = responseData.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
} else {
// Для других типов данных преобразуем их в строку
responseData = String(responseData).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
}
}
return responseData;
} catch (e) {
return '[unable to capture response data]';
}
}
/**
* Проверяет, нужно ли отправлять персональные данные (PII).
* @returns {boolean} - true, если нужно отправлять PII
* @private
*/
_shouldSendDefaultPii() {
// В реальной реализации это зависит от настроек Sentry
// Здесь просто возвращаем true для демонстрации
return true;
}
/**
* Извлекает заголовки из объекта Headers.
* @param {Headers} headers - Объект Headers
* @returns {Object} - Объект с заголовками
* @private
*/
_extractFetchHeaders(headers) {
const result = {};
if (headers instanceof Headers) {
headers.forEach((value, key) => {
// Проверяем, не является ли заголовок небезопасным
if (!SentryHttpDataIntegration._RESTRICTED_HEADERS.some(
restrictedHeader => key.toLowerCase() === restrictedHeader ||
key.toLowerCase().startsWith(restrictedHeader)
)) {
result[key] = value;
}
});
}
return result;
}
/**
* Извлекает заголовки ответа из XMLHttpRequest.
* @param {XMLHttpRequest} xhr - Объект XMLHttpRequest
* @returns {Object} - Объект с заголовками
* @private
*/
_getXHRResponseHeaders(xhr) {
const headers = xhr.getAllResponseHeaders();
const result = {};
if (!headers) {
return result;
}
headers.split('\r\n').forEach(line => {
if (!line) return;
const separatorIndex = line.indexOf(': ');
if (separatorIndex > 0) {
const key = line.substring(0, separatorIndex);
const value = line.substring(separatorIndex + 2);
// Проверяем, не является ли заголовок небезопасным
if (!SentryHttpDataIntegration._RESTRICTED_HEADERS.some(
restrictedHeader => key.toLowerCase() === restrictedHeader ||
key.toLowerCase().startsWith(restrictedHeader)
)) {
result[key] = value;
}
}
});
return result;
}
/**
* Вспомогательные методы для захвата данных
*/
/**
* Захватывает и обрабатывает тело запроса для последующей отправки в Sentry.
* Поддерживает различные типы данных: FormData, Blob, Response и строки.
* @param {*} body - Тело запроса
* @returns {Promise} - Обработанные данные запроса
* @private
*/
async _captureRequestBody(body) {
try {
// Обработка данных FormData
if (body instanceof FormData) {
return Object.fromEntries(body.entries());
}
// Обработка объектов, поддерживающих метод text() (Request, Response, Blob)
if (typeof body.text === 'function') {
const text = await body.text();
return text.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
}
// Обработка остальных типов данных
return String(body).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
} catch {
// В случае ошибки возвращаем информативную строку
return '[unable to capture request body]';
}
}
/**
* Захватывает и обрабатывает тело ответа для последующей отправки в Sentry.
* @param {Response} response - Объект Response
* @returns {Promise} - Текстовое представление тела ответа
* @private
*/
async _captureResponseBody(response) {
try {
// Пытаемся получить тело как текст
const text = await response.text();
return text.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
} catch (e) {
try {
// Если не удалось получить как текст, пробуем работать с типом ответа
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
// Для JSON пытаемся получить и преобразовать данные
const clone = response.clone();
const json = await clone.json();
return JSON.stringify(json).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
}
if (contentType && contentType.includes('text/')) {
// Для текстовых форматов пытаемся еще раз получить текст
const clone = response.clone();
return (await clone.text()).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
}
// Для бинарных данных возвращаем информацию о типе
return `[binary data: ${contentType ?? 'unknown type'}]`;
} catch {
return '[unable to capture response body]';
}
}
}
/**
* Синхронно захватывает данные для XMLHttpRequest запросов.
* @param {*} data - Данные для захвата
* @returns {Object|string|null} - Обработанные данные
* @private
*/
_captureSyncData(data) {
try {
if (!data) return null;
// Обработка данных FormData
if (data instanceof FormData) {
return Object.fromEntries(data.entries());
}
// Обработка данных Blob
if (data instanceof Blob) {
return `[Blob data: ${data.type ?? 'unknown type'}, size: ${data.size} bytes]`;
}
// Обработка данных ArrayBuffer
if (data instanceof ArrayBuffer) {
return `[ArrayBuffer data: size: ${data.byteLength} bytes]`;
}
// Если данные - объект, пытаемся преобразовать в JSON
if (typeof data === 'object' && data !== null) {
try {
return JSON.stringify(data).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
} catch (e) {
return `[object: ${Object.prototype.toString.call(data)}]`;
}
}
// Обработка остальных типов данных
return String(data).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);
} catch (e) {
return '[unable to capture data]';
}
}
/**
* Получает размер ответа из заголовка Content-Length.
* @param {Object} headers - Заголовки ответа
* @returns {number|undefined} - Размер ответа в байтах или undefined
* @private
*/
_getResponseSizeFromHeaders(headers) {
if (!headers) return undefined;
const contentLength = headers['Content-Length'] ?? headers['content-length'];
if (contentLength) {
return parseInt(contentLength, 10);
}
return undefined;
}
/**
* Формирует и отправляет информацию об ошибке в Sentry.
* @param {Object} details - Детали ошибки
* @param {string} details.method - HTTP метод запроса
* @param {string} details.url - URL запроса
* @param {*} details.requestData - Данные запроса
* @param {Object} [details.requestHeaders] - Заголовки запроса
* @param {number} [details.status] - Статус ответа (если есть)
* @param {*} [details.responseData] - Данные ответа (если есть)
* @param {Object} [details.responseHeaders] - Заголовки ответа
* @param {string} [details.error] - Сообщение об ошибке (если есть)
* @private
*/
_captureError(details) {
// Создаем объект ошибки с сообщением из details.error или на основе статуса ответа
const error = new Error(details.error ?? `HTTP Error ${details.status ?? ''}`);
// Формируем сообщение об ошибке
const message = `HTTP Client Error: ${details.method} ${details.url} ${details.status ?? ''}`;
// Формируем событие для Sentry
const event = {
message,
exception: {
values: [
{
type: 'Error',
value: message,
},
],
},
request: {
url: details.url,
method: details.method,
headers: details.requestHeaders,
data: details.requestData,
},
contexts: {
response: {
status_code: details.status,
headers: details.responseHeaders ?? {},
data: details.responseData ?? {},
body_size: this._getResponseSizeFromHeaders(details.responseHeaders),
},
},
};
// Добавляем механизм исключения
this._addExceptionMechanism(event);
// Отправляем событие в Sentry
Sentry.captureException(error, {
contexts: event.contexts,
request: event.request,
tags: {
'http.status_code': details.status ?? '',
'http.method': details.method,
},
});
}
/**
* Добавляет механизм исключения к событию.
* @param {Object} event - Событие Sentry
* @private
*/
_addExceptionMechanism(event) {
if (event.exception && event.exception.values && event.exception.values[0]) {
event.exception.values[0].mechanism = {
type: 'http.client',
handled: false,
};
}
}
}
/**
* Универсальный экспорт класса интеграции.
* Поддерживает как CommonJS модули (Node.js), так и браузерную среду.
*/
if (typeof module !== 'undefined' && module.exports) {
module.exports = SentryHttpDataIntegration;
} else {
window.SentryHttpDataIntegration = SentryHttpDataIntegration;
}
Проблема 3. Быстрое заполнение диска
Основная проблема — быстрое заполнение диска. 40 ГБ заполнялись за часы при включённом трейсинге и метриках. Sentry накапливает данные в ClickHouse, Kafka и Postgres.
Если всё-таки Sentry заполнил всё пространство, но повышать объём хранилища вы не хотите, то можно удалить некоторые docker-volume. Например, sentry-kafka, sentry-seaweedfs, возможно даже sentry-data. После их удаления запустите скрипт установки Sentry, и он заново создаст нужные docker volume. 
Решения для снижения нагрузки:
  • Снижение срока хранения событий. Установили срок хранения событий в системе до 2-4 дней (в файле .env: SENTRY_EVENT_RETENTION_DAYS). Для хранения 14 дней потребуется 200+ ГБ.
  • Отключение ненужных трейсингов:
    • браузерные спаны
    • ошибки от сторонних CDN (например, таймауты Google Fonts)
    • фоновые задачи (вкладка Performance в настройках проекта)
3. Оптимизация Kafka:
  • уменьшили retention событий до минимума — данные обрабатываются и удаляются.
Такие у нас настройки для контейнера Kafka: 
KAFKA_LOG_RETENTION_HOURS: "1"
KAFKA_MESSAGE_MAX_BYTES: "10000000" #10MB or bust
KAFKA_MAX_REQUEST_SIZE: "10000000" #10MB on requests apparently too
CONFLUENT_SUPPORT_METRICS_ENABLE: "false"
KAFKA_LOG_RETENTION_BYTES: "10737418240" # 10 GiB (server default; помните про "на партицию")
KAFKA_LOG_SEGMENT_BYTES: "268435456" # 256 MiB сегмент, чтобы быстрее закрывались и удалялись
KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: "300000" # каждые 5 минут проверять old segments
KAFKA_LOG_SEGMENT_DELETE_DELAY_MS: "60000" # задержка удаления сегмента
KAFKA_LOG_CLEANUP_POLICY: "delete"
KAFKA_LOG_CLEANER_ENABLE: true
Вот ещё ссылка на доку: https://develop.sentry.dev/self-hosted/troubleshooting/kafka/#reducing-disk-usage
  • Вручную чистили ClickHouse: TRUNCATE на старых таблицах вроде spans_local, transactions_local. 
  • Настроили cron для ежедневной очистки файлов, которые хранятся больше нашего срока в системе: find /var/lib/docker/volumes/sentry-data/_data -type f -mtime +3 -delete
Заключение 
Развёртывание Sentry self‑hosted — задача нетривиальная, особенно для высоконагруженных проектов. В ходе работы мы:
  • устранили зависания сайта из‑за недоступности Sentry (локальная JS‑библиотека + таймауты);
  • расширили мониторинг ошибок (интеграция для перехвата HTTP‑ошибок);
  • снизили нагрузку на диск (оптимизация Kafka, ClickHouse, настройка retention).
Рекомендации для тех, кто планирует развернуть Sentry self‑hosted:
  • Начинайте с конфигурации, которая соответствует нагрузке (минимум 16 ГБ RAM, 4 ядра CPU).
  • Сразу локализуйте JS‑библиотеку Sentry — это предотвратит зависания сайта.
  • Настройте таймауты для запросов к Sentry — 300–500 мс достаточно для большинства сценариев.
  • Отключайте ненужные трейсинги и метрики — это сэкономит место и ресурсы.
  • Регулярно проверяйте использование диска и настраивайте retention данных.
  • Тестируйте отказоустойчивость — симулируйте сбои Sentry и проверяйте поведение приложения.
Если у вас остались вопросы или есть идеи для улучшения — пишите в комментариях! -Источник
 
Loading...
Error