Привет, Хабр! Я Илья Назаров, старший инженер в разработке сервисов направления эксплуатации инфраструктуры данных 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. В него мы перенесли знакомые нашим пользователям стандартные правила, немного подрезав их, и постепенно заменили работавшие в проде скрипты. 

Схема работы сервиса контроля запросов стала чуть сложнее
Схема работы сервиса контроля запросов стала чуть сложнее

На этом этапе произошло разделение правил на две группы:

  1. Группа общих правил, влиять на которые могут только админы сервиса (SRE-команды направления).

  2. Группа правил для тенантов, которыми управляют ответственные за тенанты.

Очень скоро самые активные пользователи занялись настройкой более гибких лимитов, учитывающих специфику их нагрузок, приоритеты запросов и внутренний рейтинг пользователей.

Примерно в тот же момент в платформе данных назрели значимые изменения: началась миграция нагрузки на Greenplum 6, что оттянуло значимый объем внимания и ресурсов от дальнейшего развития сервиса. В целом он работал стабильно, но периодически мы возвращались для мелких фиксов и обеспечения поддержки 6-й версии Greenplum.

По ходу миграции мы адаптировали Raskolnikov для работы с обоими версиями Greenplum, написали сервис сбора данных о БД с помощью обработки хуков метрик и еще несколько небольших сервисов и утилит.

Эпоха истины и гармонии

Примерно с этого момента для меня началась эпоха истины и гармонии (по крайней мере мне хочется в это верить). У нас сформировалось более целостное продуктовое видение конечного результата, и мы начали устойчивое движение к его воплощению.

Первым шагом стала визуализация схемы работы компонентов, это позволило упростить понимание архитектуры и последующий рефактор. Следующим шагом стал рефактор кода, в ходе которого устранили много неэффективностей, нашли и исправили баги и дублирования, улучшили обработку ошибок, логирование, метрики сервиса.

Было до рефакторинга
Было до рефакторинга
Стало после рефакторинга
Стало после рефакторинга

Всегда остается пространство для улучшения и оптимизации, но в текущей итерации мы достигли значимого прогресса.

А если говорить на языке цифр, в настоящий момент в сервисе «Раскольников» мы имеем:

  • 3 общих правила, на деле доказавших свою эффективность, одинаковые для всех кластеров и пользователей.

  • 124 пользовательских правила, которые более жестко ограничивают запросы отдельных групп пользователей.

  • 72 вайтлиста для этих правил.

  • 16 продуктовых кластеров, для которых эти правила и вайтлисты действуют.

  • Более 8 тысяч применения лимитов за 14 календарных дней.

  • Более 80 тысяч принудительно закрытых за те же 14 дней IDLE-сессий.

Еще не конец истории

За это время я успел повидать многое. Сменяли друг друга эпохи, менялся инфраструктурный ландшафт, достигались различные важные цели. Одно остается неизменным — стремление все более эффективно удовлетворять потребности пользователей платформы.

Для любого инструмента важна не только чистая работоспособность и техническая эффективность. Необходимо работать над простотой и комфортом эксплуатации как со стороны администрирующей команды, так и со стороны рядового пользователя. Именно поэтому мы поставили перед собой амбициозную цель по формированию универсального, доступного и удобного инструмента управления движками данных. 

Одним из основных компонентов должен стать полноценный фронтенд. Мы уже начали работу над ним и планируем пройти несколько внутренних итераций. Это нужно, чтобы сформировать понятный и удобный для всех интерфейс, прежде чем переходить к дальнейшим усовершенствованиям.

Я прошел довольно длинный и тернистый путь и успел поучаствовать во многих интересных событиях: побыть SRE, ориентированным на автоматизацию и разработку в инфраструктурной команде, поучаствовать в инцидентах, попробовать себя в анализе данных (совсем немного), работе с требованиями и сроками, немного побыть тимлидом команды разработки, менеджером небольшого продукта. И вот теперь я вернулся к истокам — в роль, с которой все начиналось. 

Наблюдая за прогрессом, которого мы достигаем, я часто ловлю себя на мысли о том, что решаемые нами проблемы не уникальны и, скорее всего, другие команды и компании сталкивались с похожими задачами. Эти мысли подтверждаются интересом со стороны комьюнити, которое следит за выступлениями нашей команды и задает вопросы о развитии продукта.

А я считаю, что за все это время сумел совместить полезное с приятным. Работа в этом направлении принесла реальную пользу, и было реально круто активно участвовать в подобном проекте. 

Благодарю всех читателей, дошедших со мной до конца этого текста, и хочу пожелать каждому найти для себя драйвовые проекты, приносящие реальную пользу окружающим.

По недавно сформировавшейся традиции здесь могла быть ссылка на телеграм-канал с моим бложиком, но ее не будет. Спасибо за внимание!

Комментарии (0)