
Привет, Хабр! Я Илья Назаров, старший инженер в разработке сервисов направления эксплуатации инфраструктуры данных DataPlatform Т-Банка. В работе я часто соприкасаюсь с движками баз данных. Первым и основным движком волею судеб стал Greenplum. Расскажу о своем длинном пути взаимодействия с «Зеленой сливой», как из хаоса и невежества я дошел до истины и гармонии.
В начале карьеры меня много чего удивляло. Тогда я еще не знал, что такое Greenplum,и плохо понимал, что такое MPP. Позднее коллеги на пальцах объяснили мне, что это «постгрес курильщика» и «постгрес поверх кучи постгресов».
Не менее удивительны для меня процессы. Например, процесс деплоя. Именно тогда я узнал, что в большом продакшене может быть деплой через правку SSH-скриптов на серверах.
В целом ситуация выглядела страшно интересно: скрипты, процессы деплоя и работы над задачами — все было в новинку. С одной стороны, большой багаж исторически сформированных до меня решений, с другой — большой уровень свободы и минимум ограничений, что как раз и способствовало постоянному росту энтропии и хаоса. Практически сразу я ощутил желание навести во всем порядок. А что из этого получилось — читайте в статье ?
Эпоха хаоса и невежества
История начинается со старта моей стажировки, когда я был молод, амбициозен, полон сил и уверенности в своих возможностях изменить мир.
Первыми полученными задачами были задачи по рефакторингу автоматизаций, оставшихся со времен, которые помнили не все старожилы, куда нужно было вносить те или иные незначительные изменения.
Скрипты были написаны опытными инженерами и стабильно выполняли свои функции. Но за долгое время успели обрасти костылями, хотфиксами, бойлерплейтом и прочими последствиями горящих правок.
Самих кластеров было относительно немного: три продуктовых — два больших под ETL и один поменьше, но побыстрее — плюс один тестовый кластер. Все они работали на Greenplum 5.
Постепенно спектр задач расширялся и росла моя вовлеченность — я все больше любил свою работу. Я узнал, что в наши кластеры иногда попадают запросы, которые могут привести к залипанию базы из-за обилия idle-сессий и даже к ее падению из-за большого объема spill-файлов.
Тогда существовали какие-то автоматизации, которые закрывали основной поток проблем, но и ручную работу тоже приходилось делать.
Автоматизации были на баше, которые запускали следующий SQL-код:
spill_monitor.sql
INSERT INTO spill_monitor (usename, procpid, sess_id, cur_q, total_gb, numfiles, start_time, cur_time, query_start)
SELECT sizes.*,
pgstact.backend_start,
current_timestamp,
pgstact.query_start
FROM ((SELECT usename,
procpid,
sess_id,
substring(current_query FROM 1 FOR 5000) AS cur_q,
floor(sum(size)/1024/1024/1024) total_gb,
sum(numfiles) AS numfiles
FROM gp_toolkit.gp_workfile_usage_per_query
WHERE current_query <> '<IDLE>'
GROUP BY 1, 2, 3, 4
ORDER BY 5 DESC)) sizes,
pg_stat_activity pgstact
WHERE total_gb > 256
AND pgstact.procpid = sizes.procpid;
long_sessions.sql
select
datname as db_name
,usename as user_name
,procpid as process_id
,sess_id as session_id
,case when current_query = '<IDLE>' then true else false end as session_is_idle
,xact_start as xact_start_dttm
,now() as check_dttm
from
pg_stat_activity a
inner join (select pid from pg_locks where locktype = 'transactionid' group by pid) l on l.pid = a.procpid
where
xact_start is not null
and extract('epoch' from (now() - xact_start)) > 300;
И вот эта необходимость ручной реакции на возрастающее число событий (число пользователей с каждым днем росло, и за этим ростом не всегда поспевали железо, процессы и сервисы) особенным образом меня задела. Я уже наловчился определять, находить и завершать сессии, которые слишком надолго задержались в каком-либо из состояний: active, idle, idle in transaction. Завершали как через psql (pg_terminate_backend), так и через имевшийся у нас тогда GPCC.
Я посвятил свободное время изучению устройства GPCC: как и откуда оно получает данные. Получилось собрать таблицы, которые могли бы мне пригодиться в автоматизации ограничения потребляемых запросами ресурсов. Список таблиц получился таким:
pg_stat_activity как основной источник информации о запросах. Таблица стала основной при контроле idle, idle in transaction и запросов, если их длительность превышала некоторый предел (60 минут).
gp_toolkit.gp_workfile_entries — из нее мы брали данные по объему спилл-файлов на запросы.
gpmetrics.gpcc_queries_now — для получения данных по cpu_time на запрос.
Итогом работы с таблицами стал запрос, который получал основные данные по текущим активным сессиям. Этот запрос стал фундаментом для дальнейшего развития системы.
SELECT
qn.tmid,
pga.procpid,
pga.sess_id,
pga.current_query,
pga.waiting,
qn.ccnt,
pga.usename,
pga.query_start,
pga.backend_start,
qn.db,
qn.status,
pga.rsgname,
qn.rsqname,
qn.rsqpriority,
qn.tsubmit,
qn.tstart,
qn.cpu_time,
qn.cpu_time * INTERVAL '1 second' AS cpu_time_hrs,
qn.cpu_master,
qn.cpu_segment,
qn.cpu_master_percent,
qn.cpu_segment_percent,
pg_size_pretty(SUM(wpq.size)::bigint) AS spills_size_pretty,
SUM(wpq.size) AS spill_size_bytes,
wpq.numfiles,
qn.memory,
qn.disk_read_rate,
qn.disk_write_rate,
pg_size_pretty(qn.disk_read_bytes) AS disk_read,
qn.disk_read_bytes,
pg_size_pretty(qn.disk_write_bytes) AS disk_write,
qn.disk_write_bytes,
qn.skew_cpu,
qn.plan_gen,
qn.cost,
pga.application_name
FROM
pg_stat_activity pga
LEFT JOIN gp_toolkit.gp_workfile_usage_per_query wpq
ON pga.sess_id = wpq.sess_id
LEFT JOIN ONLY gpcc_queries_now qn
ON pga.sess_id = qn.ssid
WHERE
pga.current_query <> '<IDLE>'::text
GROUP BY
qn.tmid,
pga.procpid,
pga.sess_id,
pga.current_query,
pga.waiting,
qn.ccnt,
pga.usename,
pga.query_start,
pga.backend_start,
qn.db,
qn.status,
pga.rsgname,
qn.rsqname,
qn.rsqpriority,
qn.tsubmit,
qn.tstart,
qn.cpu_time,
qn.cpu_master,
qn.cpu_segment,
qn.cpu_master_percent,
qn.cpu_segment_percent,
wpq.numfiles,
qn.memory,
qn.disk_read_rate,
qn.disk_write_rate,
qn.disk_read_bytes,
qn.disk_write_bytes,
qn.skew_cpu,
qn.plan_gen,
qn.cost,
pga.application_name;
Эпоха баланса и сомнений
На тот момент мои знания SQL оставляли желать лучшего, зато я умел писать относительно хитрые скрипты на Питоне, чем и занялся.
Спустя некоторое время у нас появилась автоматизация idle_killer.py, которая умела вычислять и убивать запросы по определенным правилам, а еще оповещать об этом пользователей через корпоративный мессенджер.
Вслед за улучшением технической части я подготовил базовую пользовательскую документацию, которая помогала понять, что именно произошло и как на это реагировать.
В коде idle_killer.py выглядел примерно так:
# -*- coding: utf-8 -*-
"""Spill killer script."""
def filter_sessions(sessions, min_spill_size, whitelisted_users):
"""Kill queries heavy queries in greenplum."""
victim_sessions = filter_by_size(sessions, min_spill_size)
victim_sessions = filter_by_users(victim_sessions, whitelisted_users)
victim_sessions = filter_by_uuid(victim_sessions, ARGS.whitelisted_uuids)
return victim_sessions
def main(parsed_args):
"""Run main script function"""
# target sizes on all user greenplums: hard = 800, soft = 600
hard_spill_limit_gb = parsed_args.min_spill_size
soft_spill_limit_gb = hard_spill_limit_gb - 200
whitelisted_users = tuple(parsed_args.whitelisted_users.replace(' ', '').split(',')
with common.Greenplum() as greenplum:
greenplum.connection.set_client_encoding('WIN1251')
sum_spills_resp = greenplum.execute_query(SUM_SPILLS_QUERY)
sum_spills_gb = int(sum_spills_resp[0][0]) if sum_spills_resp else 0
if is_overspilled(sum_spills_gb):
LOGGER.warning("Too much spill files, %s GB", sum_spills_gb)
spills_by_role_resp = greenplum.execute_query(SPILLS_BY_ROLE_QUERY)
sessions = form_sessions_dicts(spills_by_role_resp)
victim_sessions = filter_sessions(sessions, hard_spill_limit_gb, whitelisted_users)
LOGGER.info("Killing sessions %s", victim_sessions)
for sess in victim_sessions:
query = "SELECT pg_cancel_backend(%s);" % sess['pid']
greenplum.execute_query(query)
notify_in_slack(victim_sessions, slack_users, hard_spill_limit_gb, penalty_channel)
dangerous_sessions = filter_sessions(sessions, soft_spill_limit_gb, whitelisted_users)
dangerous_sessions = [sess for sess in dangerous_sessions if sess not in victim_sessions]
notify_in_slack(dangerous_sessions, slack_users, soft_spill_limit_gb,
dwh_penalty_channel, reason="warn")
if __name__ == "__main__":
ARGS = PARSER.parse_args()
main(ARGS)
Позднее скрипт idle_killer.py эволюционировал в следующую версию, которая управлялась конфиг-файлами. Эти файлы позволяли гибко настраивать список параметров: идентификаторы запроса, время активности правил, набор исключений и время жизни правил.
Пример такого конфиг-файла:
# чем выше находится правило, тем оно приоритетнее
# правила в блоке, который импортирует файл, приоритетнее правил из самого файла и перетирают их
# переменные принадлежности, правило сработает, если каждое из них выполнено, по умолчанию имеют значения all:
# uuids — список sas/zep/heli айдишников
# hosts — хосты БД, для которых будут работать правила
# resource_groups — список рес-групп
# users — список реальных пользователей, в т.ч. из app_properties
# query_hash — список хэшей запросов, временно не используется
# rule_start_time и rule_end_time должны быть в кавычках, чтобы парсилось строкой,
# окно, в которое правило будет применяться, значения по умолчанию "00:00:00" и "23:59:59" соответственно
# список лимитов, по умолчанию None
# expiration_date — дата, после которой правило становится недействительным
# limit_spills_gb — число, максимальный лимит объема спилл-файлов на запрос
# limit_cpu_time_hrs — лимит ЦПУ в часах
# limit_duration_hrs — лимит времени, работа запроса в часах
- hosts:
all
file:
whitelist.yml
limit_duration_hrs:
inf
limit_spills_gb:
inf
limit_cpu_time_hrs:
inf
- hosts:
cluster_name
users:
all
limit_duration_hrs:
inf
limit_spills_gb:
inf
limit_cpu_time_hrs:
inf
expiration_date:
2022-02-23
# increeased duration warning
- hosts:
all
users:
all
limit_duration_hrs:
5
limit_spills_gb:
3000
limit_cpu_time_hrs:
48
kill:
fals
Мы получили относительно гибкий инструмент автоматического отстрела пользовательских запросов, которые мешали нам жить. Скрипт крутился на кроне. Выглядело это примерно так.

Одного написания кода, конечно же, недостаточно. Его нужно запустить в продакшен-среду. Большого опыта запуска подобных вещей у моей команды не было, поэтому приняли волевое решение, что если уж мы убиваем запросы вручную, то автоматические убийства значимых различий не имеют. Это была ошибка.
После запуска автоматики, несмотря на подготовленное предупреждение, запросы начали убиваться с ощутимыми для пользователей последствиями. Пользователи к такому никак не были готовы — ни в моральном плане, ни в отношении наличия ресурсов на массовое исправление запросов, которые раньше пропускались при ручной обработке.
Мы отключили автоматику, собрали встречу с заинтересованными представителями и начали прорабатывать более контролируемый и дружественный к пользователям сценарий запуска. На основе обсуждения сформировали ключевые пункты:
Пользователи должны ясно понимать правила игры.
Повторно запускаться мы можем не раньше чем через месяц, так как нужно время на то, чтобы привести легаси-код к новым правилам игры.
Пользователи должны получать доступные и понятные оповещения о том, какой из их запросов был отменен и по какой причине.
Эпоха порядка и закона
Когда пользователи заметили пользу для себя от подобного инструментария, все чаще стали звучать вопросы о возможности самостоятельно управлять ограничениями ресурсов на запрос. Особенно от лидов направлений, которые видят работу в большом масштабе.
Мы сопоставили пользователей с бизнес-линиями и начали формировать виртуальную сущность. В разных кругах она носила разные названия: потребители, консьюмеры, тенанты. Но, по сути, почти всегда эти сущности имели прямую связь с ресурсной группой в базе.
Так у тенантов начали появляться свои пары ответственных: со стороны платформы данных и со стороны бизнеса. Коллеги из бизнеса помогают с формулировкой и актуализацией текущих потребностей. Коллеги из платформы помогают с техническими вопросами, обозначают ограничения и нюансы самой платформы данных в контексте конкретных кейсов соответствующих линий.
В техническом плане новым витком дальнейшего развития стало преобразование GitOps-скрипта в полноценный сервис со своим бэкэндом, rest-api, авторизацией, интеграциями и прочим.
На этом этапе мы стали больше внимания уделять сбору и хранению данных о пользовательских запросах для после��ующего исторического анализа. Так у нас появился свой managed-инстанс ClickHouse, в который мы складываем реплики интересующих нас данных с помощью Vector. Выглядело это так:
---
gp__master__logs:
name: gp_logs
vars:
sources:
gp_log:
type: file
include:
- "{{ master_data_directory }}/pg_log/*.csv"
ignore_older_secs: 864000 # 1 day
max_line_bytes: 204800 # default 102400
data_dir: "{{ vector.data_dir }}"
multiline:
mode: halt_before
start_pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6} \+[0-9]{2},' # 2023-01-16 00:00:00.001577 +03,
condition_pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6} \+[0-9]{2},' # 2023-01-16 00:00:00.001577 +03,
timeout_ms: 1000
encoding:
charset: "cp1251"
transforms:
gp_log_trans_1:
type: filter
inputs:
- gp_log
condition:
type: "vrl"
source: false == to_bool(find!(.message, r'^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6} \+[0-9]{2},'))
gp_log_trans_2:
inputs:
- "gp_log_trans_1"
type: "remap"
source: |-
parts = parse_csv!(.message,",")
del(.message)
del(.source_type)
del(.msg_err)
.inst = del(.host)
.capture_ts = del(.timestamp)
...
del(.dur_err)
gp_log_trans_ast:
inputs:
- gp_log_trans_2
type: remap
source: |-
.original_sql = .debug_query_string
sinks:
gp_log_to_ch:
type: "clickhouse"
table: "gp5_logs"
inputs:
- "gp_log_trans_2"
encoding:
timestamp_format: "{{ clickhouse_config.encoding.timestamp_format }}"
auth:
strategy: "{{ clickhouse_config.auth.strategy }}"
user: "{{ clickhouse_config.auth.user }}"
password: "{{ clickhouse_config.auth.password }}"
database: "{{ clickhouse_config.database }}"
endpoint: "{{ clickhouse_config.endpoint }}"
compression: "{{ clickhouse_config.compression }}"
batch:
max_events: 102400
timeout_secs: 10
gp_log_to_ast:
type: http
inputs:
- gp_log_trans_ast
uri: "{{ ast_endpoint }}"
batch:
max_events: 102400
buffer:
when_full: "drop_newest"
encoding:
codec: json
request:
concurrency: 1
rate_limit_num: 20
На основе данных о текущих и завершенных запросах и о потребляемых этими запросами ресурсах зародился новый сервис под названием Raskolnikov. В него мы перенесли знакомые нашим пользователям стандартные правила, немного подрезав их, и постепенно заменили работавшие в проде скрипты.

На этом этапе произошло разделение правил на две группы:
Группа общих правил, влиять на которые могут только админы сервиса (SRE-команды направления).
Группа правил для тенантов, которыми управляют ответственные за тенанты.
Очень скоро самые активные пользователи занялись настройкой более гибких лимитов, учитывающих специфику их нагрузок, приоритеты запросов и внутренний рейтинг пользователей.
Примерно в тот же момент в платформе данных назрели значимые изменения: началась миграция нагрузки на Greenplum 6, что оттянуло значимый объем внимания и ресурсов от дальнейшего развития сервиса. В целом он работал стабильно, но периодически мы возвращались для мелких фиксов и обеспечения поддержки 6-й версии Greenplum.
По ходу миграции мы адаптировали Raskolnikov для работы с обоими версиями Greenplum, написали сервис сбора данных о БД с помощью обработки хуков метрик и еще несколько небольших сервисов и утилит.
Эпоха истины и гармонии
Примерно с этого момента для меня началась эпоха истины и гармонии (по крайней мере мне хочется в это верить). У нас сформировалось более целостное продуктовое видение конечного результата, и мы начали устойчивое движение к его воплощению.
Первым шагом стала визуализация схемы работы компонентов, это позволило упростить понимание архитектуры и последующий рефактор. Следующим шагом стал рефактор кода, в ходе которого устранили много неэффективностей, нашли и исправили баги и дублирования, улучшили обработку ошибок, логирование, метрики сервиса.


Всегда остается пространство для улучшения и оптимизации, но в текущей итерации мы достигли значимого прогресса.
А если говорить на языке цифр, в настоящий момент в сервисе «Раскольников» мы имеем:
3 общих правила, на деле доказавших свою эффективность, одинаковые для всех кластеров и пользователей.
124 пользовательских правила, которые более жестко ограничивают запросы отдельных групп пользователей.
72 вайтлиста для этих правил.
16 продуктовых кластеров, для которых эти правила и вайтлисты действуют.
Более 8 тысяч применения лимитов за 14 календарных дней.
Более 80 тысяч принудительно закрытых за те же 14 дней IDLE-сессий.
Еще не конец истории
За это время я успел повидать многое. Сменяли друг друга эпохи, менялся инфраструктурный ландшафт, достигались различные важные цели. Одно остается неизменным — стремление все более эффективно удовлетворять потребности пользователей платформы.
Для любого инструмента важна не только чистая работоспособность и техническая эффективность. Необходимо работать над простотой и комфортом эксплуатации как со стороны администрирующей команды, так и со стороны рядового пользователя. Именно поэтому мы поставили перед собой амбициозную цель по формированию универсального, доступного и удобного инструмента управления движками данных.
Одним из основных компонентов должен стать полноценный фронтенд. Мы уже начали работу над ним и планируем пройти несколько внутренних итераций. Это нужно, чтобы сформировать понятный и удобный для всех интерфейс, прежде чем переходить к дальнейшим усовершенствованиям.
Я прошел довольно длинный и тернистый путь и успел поучаствовать во многих интересных событиях: побыть SRE, ориентированным на автоматизацию и разработку в инфраструктурной команде, поучаствовать в инцидентах, попробовать себя в анализе данных (совсем немного), работе с требованиями и сроками, немного побыть тимлидом команды разработки, менеджером небольшого продукта. И вот теперь я вернулся к истокам — в роль, с которой все начиналось.
Наблюдая за прогрессом, которого мы достигаем, я часто ловлю себя на мысли о том, что решаемые нами проблемы не уникальны и, скорее всего, другие команды и компании сталкивались с похожими задачами. Эти мысли подтверждаются интересом со стороны комьюнити, которое следит за выступлениями нашей команды и задает вопросы о развитии продукта.
А я считаю, что за все это время сумел совместить полезное с приятным. Работа в этом направлении принесла реальную пользу, и было реально круто активно участвовать в подобном проекте.
Благодарю всех читателей, дошедших со мной до конца этого текста, и хочу пожелать каждому найти для себя драйвовые проекты, приносящие реальную пользу окружающим.
По недавно сформировавшейся традиции здесь могла быть ссылка на телеграм-канал с моим бложиком, но ее не будет. Спасибо за внимание!