Русский
Русский
English
Статистика
Реклама

Постгрес

SQL миграции в Postgres. Часть 1

03.02.2021 00:17:16 | Автор: admin

Как обновить значение атрибута для всех записей таблицы? Как добавить первичный или уникальный ключ в таблицу? Как разбить таблицу на две? Как ...

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

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

Эта статья расшифровка выступления на конференции SmartDataConf (здесь можно найти презентацию, со временем появится видео). Текста получилось много, поэтому материал будет разбит на 2 статьи:

  • базовые миграции
  • подходы по обновлению больших таблиц.

В конце есть выжимка всей статьи в виде сводной таблицы-шпаргалки.

Содержание


Суть проблемы
Добавление столбца
Добавление столбца со значением по умолчанию
Удаление столбца
Создание индекса
Создание индекса для партиционированной таблицы
Создание ограничения NOT NULL
Создание внешнего ключа
Создание ограничения уникальности
Создание первичного ключа
Краткая шпаргалка с миграциями

Суть проблемы


Предположим, что у нас есть приложение, которое работает с базой данных. В минимальной конфигурации оно может состоять из 2 узлов самого приложения и базы данных, соответственно.


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

Если приложение растет и появляется необходимость проводить релизы без простоя, мы начинаем использовать несколько серверов приложений. Их может быть сколько угодно, и они будут на разных версиях. В этом случае появляется необходимость обеспечения обратной совместимости.


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


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

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

Добавление столбца


ALTER TABLE my_table ADD COLUMN new_column INTEGER -- быстро и безопасно

Наверное, любой человек, который работает с БД, писал подобную миграцию. Если говорить про PostgreSQL, то такая миграция весьма дешевая и безопасная. Сама команда, хоть и захватывает блокировку самого высокого уровня (AccessExclusive), выполняется очень быстро, поскольку под капотом происходит лишь добавление метаинформации о новом столбце без перезаписи данных самой таблицы. В большинстве случаев это происходит незаметно. Но проблемы могут возникнуть, если в момент миграции есть долгие транзакции, работающие с этой таблицей. Чтобы понять суть проблемы рассмотрим на небольшом примере как упрощенно работают блокировки в PostgreSQL. Этот аспект будет очень важен при рассмотрении большинства других миграций в том числе.

Предположим, у нас есть большая таблица, и мы делаем из нее SELECT всех данных. В зависимости от размера БД и самой таблицы он может длиться несколько секунд или даже минут.


На время выполнения транзакции захватывается самая слабая блокировка AccessShare, которая защищает от изменений структуры таблицы.

В этот момент приходит другая транзакция, которая как раз таки пытается сделать запрос ALTER TABLE к этой таблице. Команда ALTER TABLE, как уже было сказано ранее, захватывает блокировку AccessExclusive, которая не совместима вообще ни с какой другой. Она встает в очередь.

Эта очередь блокировок разгребается в строгом порядке, т.е. даже если после ALTER TABLE приходят другие запросы (например, также SELECTы), которые сами по себе не конфликтуют с первым запросом, они все встают в очередь за ALTER TABLE. В итоге приложение встало и ждет, пока ALTER TABLE выполнится.

Что делать в такой ситуации? Можно ограничить время захвата блокировки с помощью команды SET lock_timeout. Выполняем эту команду перед ALTER TABLE (ключевое слово LOCAL означает, что настройка действует только в пределах текущей транзакции, иначе в пределах текущей сессии):

SET LOCAL lock_timeout TO '100ms'

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

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

Добавление столбца со значением по умолчанию


-- быстро и безопасно с PG 11ALTER TABLE my_table ADD COLUMN new_column INTEGER DEFAULT 42

Если эта команда выполняется в PostgreSQL старой версии (ниже 11), то это приводит к перезаписи всех строк в таблице. Очевидно, что если таблица большая, то это может занять много времени. А поскольку на время выполнения захватывается строгая блокировка (AccessExclusive), то все запросы к таблице также блокируются.

Если версия PostgreSQL 11 или свежее, эта операция обходится весьма дешево. Дело в том, что в 11й версии была сделана оптимизация, благодаря которой вместо перезаписи таблицы значение по умолчанию сохраняется в специальную таблицу pg_attribute, и в дальнейшем при выполнении SELECT, все пустые значения этого столбца будут на лету заменяться на это значение. При этом впоследствии, когда будет происходить перезапись строк в таблице из-за других модификаций, значение будет записываться в эти строки.

Более того, с 11й версии также можно сразу создавать новый столбец и помечать его как NOT NULL:

-- быстро и безопасно с PG 11ALTER TABLE my_table ADD COLUMN new_column INTEGER DEFAULT 42 NOT NULL

Как быть, если PostgreSQL старше, чем 11?

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

ALTER TABLE my_table ADD COLUMN new_column INTEGER;ALTER TABLE my_table ALTER COLUMN new_column SET DEFAULT 42;

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

Таким образом после выполнения этих команд, нам остается обновить значения, которые уже были в таблице. Грубо говоря, нам нужно сделать примерно вот так:

UPDATE my_table set new_column = 42 -- небезопасно на большой таблице

Но такой UPDATE в лоб делать на самом деле нельзя, поскольку при обновлении большой таблицы мы надолго заблокируем всю таблицу. Во второй статье (здесь в будущем будет ссылка) мы рассмотрим, какие существуют стратегии для обновления больших таблиц в PostgreSQL, а пока будем предполагать, что мы каким-то образом обновили данные, и теперь и старые данные, и новые будут с необходимым значением по умолчанию.

Удаление столбца


ALTER TABLE my_table DROP COLUMN new_column -- быстро и безопасно

Здесь логика такая же, как и при добавлении столбца: данные таблицы не модифицируются, происходит только изменение метаинформации. В данном случае столбец помечается как удаленный и недоступный при запросах. Это объясняет тот факт, что при удалении столбца в PostgreSQL физически место не освобождается (если не выполнять VACUUM FULL), то есть данные старых записей по-прежнему остаются в таблице, но недоступны при обращении. Освобождение происходит постепенно при перезаписи строк в таблице.

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

  • Для начала необходимо убрать все ограничения (NOT NULL, CHECK, ...), которые есть на этом столбце:
    ALTER TABLE my_table ALTER COLUMN new_column DROP NOT NULL
    
  • Следующий шаг обеспечить совместимость бэкенда. Нужно убедиться, что столбец нигде не используется. Например, в Hibernate необходимо пометить поле с помощью аннотации @Transient. В JOOQ, который мы используем, поле добавляется в исключения с помощью тэга <excludes>:
    <excludes>my_table.new_column</excludes>
    

    Также нужно внимательно посмотреть на запросы "SELECT *" фреймворки могут мапить все столбцы в структуру в коде (и наоборот) и, соответственно, снова можно столкнуться с проблемой обращения к несуществующему столбцу.

После вывода изменений на все сервера приложений можно удалить столбец.

Создание индекса


CREATE INDEX my_table_index ON my_table (name) -- небезопасно, блокировка таблицы

Те, кто работает с PostgreSQL, наверное, знают, что такая команда блокирует всю таблицу. Но еще с очень старой версии 8.2 существует ключевое слово CONCURRENTLY, которое позволяет создавать индекс в неблокирующем режиме.

CREATE CONCURRENTLY INDEX my_table_index ON my_table (name) -- безопасно

Команда работает медленнее, но не мешает параллельным запросам.

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

SELECT pg_index.indisvalid    FROM pg_class, pg_indexWHERE pg_index.indexrelid = pg_class.oid    AND pg_class.relname = 'my_table_index'

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

DROP INDEX CONCURRENTLY my_table_indexUPDATE my_table ...CREATE CONCURRENTLY INDEX my_table_index ON my_table (name)

Важно заметить, что команда REINDEX, которая как раз предназначена для пересоздания индекса, до 12й версии работает только в блокирующем режиме, что не дает возможности ее использовать. В 12й версии PostgreSQL появилась поддержка CONCURRENTLY, и теперь и ей можно пользоваться.

REINDEX INDEX CONCURRENTLY my_table_index -- с PG 12

Создание индекса для партиционированной таблицы


Отдельно стоит обсудить создание индексов для партиционированных таблиц. В PostgreSQL существует 2 вида партиционирования: через наследование и декларативное, появившееся в 10й версии. Рассмотрим оба на простом примере.

Предположим, что мы хотим партиционировать таблицу по дате, и каждая партиция будет содержать данные за один год.

При партиционировании через наследование мы будем иметь примерно следующую схему.

Родительская таблица:

CREATE TABLE my_table (...reg_date  date not null)

Дочерние партиции для 2020 и 2021 годов:

CREATE TABLE my_table_y2020 (CHECK ( reg_date >= DATE '2020-01-01' AND reg_date < DATE '2021-01-01' ))INHERITS (my_table);CREATE TABLE my_table_y2021 (CHECK ( reg_date >= DATE '2021-01-01' AND reg_date < DATE '2022-01-01' ))INHERITS (my_table);

Индексы по полю партиционирования для каждой из партиций:

CREATE INDEX ON my_table_y2020 (reg_date);CREATE INDEX ON my_table_y2021 (reg_date);

Создание триггера/правила для вставки данных в таблицу оставим за рамками.

Самое важное здесь то, что каждая из партиций это практически самостоятельная таблица, которая обслуживается по отдельности. Таким образом, создание новых индексов также проводится как с обычными таблицами:

CREATE CONCURRENTLY INDEX my_table_y2020_index ON my_table_y2020 (name);CREATE CONCURRENTLY INDEX my_table_y2021_index ON my_table_y2021 (name);

Теперь рассмотрим декларативное партиционирование.

CREATE TABLE my_table (...) PARTITION BY RANGE (reg_date);CREATE TABLE my_table_y2020 PARTITION OF my_table FOR VALUES FROM ('2020-01-01') TO ('2020-12-31');CREATE TABLE my_table_y2021 PARTITION OF my_table FOR VALUES FROM ('2021-01-01') TO ('2021-12-31');

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

В 11й версии декларативное партиционирование было доработано и теперь таблицы обслуживаются вместе. Создание индекса на родительской таблице автоматически приводит к созданию индексов для всех существующих и новых партиций, которые будут созданы в будущем:

-- с PG 11 удобно для новой (пустой) партиционированной таблицыCREATE INDEX ON my_table (reg_date)

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

CREATE INDEX ON my_table (name) -- блокировка таблиц

К сожалению, CREATE INDEX не поддерживает ключевое слово CONCURRENTLY для партиционированных таблиц. Чтобы обойти ограничение и провести миграцию без блокировок можно сделать следующее.

  1. Создать индекс для родительской таблицы с опцией ONLY
    CREATE INDEX my_table_index ON ONLY my_table (name)
    

    Команда создаст пустой невалидный индекс без создания индексов для партиций.
  2. Создать индексы для каждой из партиций:
    CREATE CONCURRENTLY INDEX my_table_y2020_index ON my_table_y2020 (name);CREATE CONCURRENTLY INDEX my_table_y2021_index ON my_table_y2021 (name);
    
  3. Прикрепить индексы партиций к индексу родительской таблицы:
    ALTER INDEX my_table_index ATTACH PARTITION my_table_y2020_index;ALTER INDEX my_table_index ATTACH PARTITION my_table_y2021_index;
    
    Как только все индексы будут прикреплены, индекс родительской таблицы автоматически станет валидным.

Ограничения


Теперь пройдемся по ограничениям: NOT NULL, внешние, уникальные и первичные ключи.

Создание ограничения NOT NULL


ALTER TABLE my_table ALTER COLUMN name SET NOT NULL -- блокировка таблицы

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

Что можно сделать? В PostgreSQL есть другой тип ограничения, CHECK, с помощью которого можно получить желаемый результат. Это ограничение проверяет любое булево условие, состоящее из столбцов строки. В нашем случае условие тривиально CHECK (name IS NOT NULL). Но самое важное то, что ограничение CHECK поддерживает невалидность (ключевое слово NOT VALID):

ALTER TABLE my_table ADD CONSTRAINT chk_name_not_null     CHECK (name IS NOT NULL) NOT VALID -- безопасно, с PG 9.2

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

Для обеспечения гарантии, что существующие записи также удовлетворяют ограничению необходимо провести его валидацию (конечно же, предварительно обновив данные в таблице):

ALTER TABLE my_table VALIDATE CONSTRAINT chk_name_not_null

Команда итерируется по строкам таблицы и проверяет, что все записи не not null. Но в отличие от обычного NOT NULL ограничения, блокировка, захватываемая в этой команде, не такая строгая (ShareUpdateExclusive) она не блокирует операции insert, update и delete.

Создание внешнего ключа


ALTER TABLE my_table ADD CONSTRAINT fk_group     FOREIGN KEY (group_id) REFERENCES groups(id) -- блокировка обеих таблиц

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

К счастью, внешние ключи в PostgreSQL также поддерживают NOT VALID, а это значит мы можем использовать тот же подход, что был рассмотрен ранее с CHECK. Создаем невалидный внешний ключ:

ALTER TABLE my_table ADD CONSTRAINT fk_group     FOREIGN KEY (group_id)REFERENCES groups(id) NOT VALID

затем обновляем данные и проводим валидацию:

ALTER TABLE my_table VALIDATE CONSTRAINT fk_group_id


Создание ограничения уникальности


ALTER TABLE my_table ADD CONSTRAINT uk_my_table UNIQUE (id) -- блокировка таблицы

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

Важно знать, что под капотом PostgreSQL реализует уникальные ограничения с помощью уникальных индексов. Иными словами, когда создается ограничение, происходит создание соответствующего уникального индекса с таким же наименованием, который обслуживает это ограничение. С помощью следующего запроса можно узнать обслуживающий индекс ограничения:

SELECT conindid index_oid, conindid::regclass index_name     FROM pg_constraint WHERE conname = 'uk_my_table_id'

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

ALTER TABLE my_table ADD CONSTRAINT uk_my_table_id UNIQUE     USING INDEX uk_my_table_id -- быстро, с PG 9.1

Таким образом, идея простая создаем уникальный индекс CONCURRENTLY, как мы рассматривали ранее, и затем на его основе создаем уникальное ограничение.

В этот момент может возникнуть вопрос зачем вообще создавать ограничение, если индекс выполняет ровно то, что требуется гарантирует уникальность значений? Если исключить из сравнения partial индексы, то с функциональной точки зрения результат действительно почти идентичен. Единственное отличие, которое удалось найти, состоит в том, что ограничения могут быть отложенными (deferrable), а индексы нет. В документации к старым версиям PostgreSQL (до 9.4 включительно) была сноска с информацией о том, что предпочтительный способ создания ограничения уникальности это явное создание ограничения ALTER TABLE ... ADD CONSTRAINT, а использование индексов стоит рассматривать как деталь реализации. Однако, в более свежих версиях эту сноску удалили.

Создание первичного ключа


Первичный ключ помимо уникальности накладывает ограничение not null. Если столбец изначально имел такое ограничение, то превратить его в первичный ключ не составит труда также создаем уникальный индекс CONCURRENTLY, а затем первичный ключ:

ALTER TABLE my_table ADD CONSTRAINT uk_my_table_id PRIMARY KEY     USING INDEX uk_my_table_id -- если id is NOT NULL

Важно отметить, что столбец должен иметь честное ограничение NOT NULL рассмотренный ранее подход с помощью CHECK не сработает.

Если же ограничения нет, то до 11-й версии PostgreSQL ничего не поделать без блокировки первичный ключ никак не создать.

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

Создаем новый столбец, который по умолчанию not null и имеет значение по умолчанию:

ALTER TABLE my_table ADD COLUMN new_id INTEGER NOT NULL DEFAULT -1 -- безопасно с PG 11

Настраиваем синхронизацию данных старого и нового столбцов с помощью триггера:

CREATE FUNCTION on_insert_or_update() RETURNS TRIGGER AS$$BEGIN  NEW.new_id = NEW.id;  RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER trg BEFORE INSERT OR UPDATE ON my_tableFOR EACH ROW EXECUTE PROCEDURE on_insert_or_update();

Далее необходимо обновить данные строк, которые не затронулись триггером:

UPDATE my_table SET new_id = id WHERE new_id = -1 -- не делать на большой таблице

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

ALTER TABLE my_table RENAME COLUMN id TO old_id;ALTER TABLE my_table RENAME COLUMN new_id TO id;ALTER TABLE my_table RENAME COLUMN old_id TO new_id;

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

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

Краткая шпаргалка с миграциями


Перед любой командой, которая захватывает строгую блокировку (почти все ALTER TABLE ...) рекомендуется вызывать:

SET LOCAL lock_timeout TO '100ms'

Миграция Рекомендуемый подход
Добавление столбца
ALTER TABLE my_table ADD COLUMN new_column INTEGER
Добавление столбца со значением по умолчанию [и NOT NULL] c PostgreSQL 11:
ALTER TABLE my_table ADD COLUMN new_column INTEGER DEFAULT 42 [NOT NULL]

до PostgreSQL 11:
  1. ALTER TABLE my_table ADD COLUMN new_column INTEGER;ALTER TABLE my_table ALTER COLUMN new_column SET DEFAULT 42;
    
  2. обновление таблицы
Удаление столбца
  1. удаление ограничений (NOT NULL, CHECK и т.д.)
  2. подготовка кода
  3. ALTER TABLE my_table DROP COLUMN removed_column
    
Создание индекса
CREATE CONCURRENTLY INDEX my_table_index ON my_table (name)

Если завершилось ошибкой:
  1. DROP INDEX CONCURRENTLY my_table_index
    
  2. обновление таблицы
  3. CREATE CONCURRENTLY INDEX my_table_index ON my_table (name)
    

Создание индекса для партиционированной таблицы Партиционирование через наследование + декларативное в PG 10:
CREATE CONCURRENTLY INDEX my_table_y2020_index ON my_table_y2020 (name);CREATE CONCURRENTLY INDEX my_table_y2021_index ON my_table_y2021 (name);...

Декларативное партиционирование с PG 11:
  1. CREATE INDEX my_table_index ON ONLY my_table (name)
    
  2. CREATE CONCURRENTLY INDEX my_table_y2020_index ON my_table_y2020 (name);CREATE CONCURRENTLY INDEX my_table_y2021_index ON my_table_y2021 (name);...
    
  3. ALTER INDEX my_table_index ATTACH PARTITION my_table_y2020_index;ALTER INDEX my_table_index ATTACH PARTITION my_table_y2021_index;...
    
Создание ограничения NOT NULL
  1. ALTER TABLE my_table ADD CONSTRAINT chk_name_not_null CHECK (name IS NOT NULL) NOT VALID
    
  2. обновление таблицы
  3. ALTER TABLE my_table VALIDATE CONSTRAINT chk_name_not_null
    

Создание внешнего ключа
  1. ALTER TABLE my_table ADD CONSTRAINT fk_group FOREIGN KEY (group_id) REFERENCES groups(id) NOT VALID
    
  2. обновление таблицы
  3. ALTER TABLE my_table VALIDATE CONSTRAINT fk_group_id
    
Создание ограничения уникальности
  1. CREATE UNIQUE INDEX CONCURRENTLY uk_my_table_id ON my_table(id)
    
  2. ALTER TABLE my_table ADD CONSTRAINT uk_my_table_id UNIQUE USING INDEX uk_my_table_id
    
Создание первичного ключа Если столбец IS NOT NULL:
  1. CREATE UNIQUE INDEX CONCURRENTLY uk_my_table_id ON my_table(id)
    
  2. ALTER TABLE my_table ADD CONSTRAINT uk_my_table_id PRIMARY KEY USING INDEX uk_my_table_id
    

Если столбец IS NULL c PG 11:
  1. ALTER TABLE my_table ADD COLUMN new_id INTEGER NOT NULL DEFAULT -1
    
  2. CREATE FUNCTION on_insert_or_update() RETURNS TRIGGER AS$$BEGINNEW.new_id = NEW.id;RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER trg BEFORE INSERT OR UPDATE ON my_tableFOR EACH ROW EXECUTE PROCEDURE on_insert_or_update();
    
  3. обновление таблицы
  4. ALTER TABLE my_table RENAME COLUMN id TO old_id;ALTER TABLE my_table RENAME COLUMN new_id TO id;ALTER TABLE my_table RENAME COLUMN old_id TO new_id;
    
  5. CREATE UNIQUE INDEX CONCURRENTLY uk_my_table_id ON my_table(id)
    
  6. ALTER TABLE my_table ADD CONSTRAINT uk_my_table_id PRIMARY KEY USING INDEX uk_my_table_id
    
  7. DROP TRIGGER trg ON my_table;DROP FUNCTION on_insert_or_update();ALTER TABLE my_table DROP COLUMN new_id;
    

В следующей статье рассмотрим подходы по обновлению больших таблиц.
Всем легких миграций!
Подробнее..

Следствие вели пропажа FC-линков HBA Emulex на сервере Atos BullSequana S1600

18.06.2021 20:15:41 | Автор: admin

Привет, Хабр! Мы постоянно проводим тесты различных софтверных решений на нашем оборудовании, и иногда простая, казалось бы, задача разворачивается на недели. Как раз о таком случае сегодня и пойдет речь. Главный герой нашего рассказа - Павел, технический консультант компании Atos в России.

Рыцари Постгрес тестируют

Итак, на сервере Atos BullSequana S1600 (16 процессоров Intel Platinum 8260), разделенном логически на 2 половинки по 8 сокетов, установлено 4 HBA Emulex LPe31002-M6 (2х-портовые, 16 Гбит), по 2 на каждой половине. FC-линки подключены через 2 MDS-свитча производства Cisco, и с помощью multipath предоставляют системе один диск объемом 6 Тб. В самом начале тестов каждая карта была подключена всего одним линком, но потом, в ходе диагностики, для большей надежности и вообще повышения крутизны подключили все порты. Итого, на каждой половинке сервера оказалось по 4 FC-линка. Во время тестов работы с диском не было.

ОС на обеих половинках на момент старта нашего повествования CentOS Linux release 7.7.1908 с ядром: 3.10.0-1062.12.1.el7

Версия FW карт - 12.6.240.40 (рекомендованная Atos, обновлялась в процессе работ).

Версия драйвера lpfc (судя по всему, родная, из коробки ОС) 12.0.0.13.

Объём доступной памяти всего-навсего 4096 Гб на каждой половинке, с учетом резервирования части памяти под нужды железа под ОС остается 3968 Гб.

Все началось с того, что специалисты по СУБД Postgres решили протестировать железо с помощью stress-ng пакета, в попытке доказать, что наше оборудование не выдерживает нагрузки (у них были инциденты, в рамках расследования которых всё и завертелось).

Параметры стресс-теста взяты "замечательные", вот команда запуска

stress-ng --vm-rw 1000 --vm-rw-bytes 2500G --verify --metrics-brief -t 60m

По документации, такие параметры означают, что стартовали 1000 процессов (start N workers that transfer memory to/from a parent/child), дали по 2500Гб оперативной памяти каждому (mmap N bytes per vm-rw worker) и сказали обмениваться с помощью функций Линукса process_vm_writev и process_vm_readv, а результат обмена проверять на ошибки, и так час. Ошибок при передаче данных не возникало, но вот проблемы с ОС и FC-линками были.

Позже, надо сказать, тестировали с еще более забавными параметрами stress-ng --vm-rw 2000 --vm-rw-bytes 3500G --verify --metrics-brief -t 10m.

Линукс крут. Такие параметры означают, что почти все время Линукс тратил на переключение процессов, обращения к памяти и передачу данных между NUMA-нодами, и сам по себе не падал, но люто тормозил. После небольшой настройки наше оборудование тоже стало справляться с такой нагрузкой без падений (Тормоза? Кто сказал тормоза?), но вот FC-линкам действительно становилось плохо они отваливались, один за другим, и победить это настройками не удавалось.

Со стороны свитчей это выглядело примерно так:

MDS1

2021 Feb 15 23:43:57 dn-MDS-C9148S-1 %PORT-5-IF_DOWN_LINK_FAILURE: %$VSAN 2221%$ Interface fc1/15 is down (Link failure loss of signal)

2021 Feb 15 23:45:24 dn-MDS-C9148S-1 %PORT-5-IF_DOWN_LINK_FAILURE: %$VSAN 2221%$ Interface fc1/27 is down (Link failure loss of signal)

MDS2

2021 Feb 15 23:21:54 dn-MDS-C9148S-2 %PORT-5-IF_DOWN_LINK_FAILURE: %$VSAN 2222%$ Interface fc1/27 is down (Link failure loss of signal)

2021 Feb 16 00:00:02 dn-MDS-C9148S-2 %PORT-5-IF_DOWN_LINK_FAILURE: %$VSAN 2222%$ Interface fc1/15 is down (Link failure loss of signal)

Техподдержка врёт (ну или добросовестно заблуждается).

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

Сначала, по просьбе Паши, заказчик поставил Emulex OneCommand Manager Command Line Interface и попробовал некоторые команды, например, получить список HBA, проверить статус портов, принудительно включить порт, перезагрузить HBA-карту.

Ничего из этого не помогло, но стало известно, что точный статус порта User Offline, позже, проанализировав выводы команд, техподдержка Emulex дала вот такой ответ по поводу статуса порта User Offline:

The Port state goes to User-offline, when the port bring-up fails even after reset. This is done by FC Driver. The reason for port bring-up failure could be due to various reasons (May be link issue (or) switch F-Port issue (or) HBA N-Port issue (or) authentication issue etc.)..

Перезагрузить сервер пока не удавалось (тесты), заказчик попробовал разные решения, но отключенные порты так и не поднимались, зато свежеподключенные порты работали. Все проверки показали, что проблема с одним из портов каждой HBA-карточки, всё остальное (свитчи, SFP, кабели) полностью исправно.

Первым делом в техподдержку отправили здоровенный кусок информации в виде логов, собранных специальным инструментом OneCapture. Поскольку карты были более-менее здоровы (за минусом портов), набор логов собрался (хотя и поразил объемами два пакета логов, в 9 и 36 ГИГАБАЙТ), и меньший из них послали доблестным специалистам техподдержки.

Логов не хватило.

Позволим себе процитировать:

The issue here is that the link state went to LPFC_HBA_ERROR state because of which board_mode is showing port status output as error.

Driver will not be able to post mailbox if link state is in error state and it will start throwing errors.

To debug further, our Development team needs more driver logs with log-verbosity set to 0x1ffff on the errored port.

*Steps to follow to collect logs:

==============

1. set the verbosity log level using HBACMD # hbacmd setDriverParam 10:00:00:10:**:**:**:** L P log-verbose 0x1ffff

2.Reset the port so that the port initialization events start # hbacmd reset 10:00:00:10:**:**:**:** (In case the boot mode is enabled, disable it using below command and then retry 2) (((#hbacmd EnableBootCode 10:00:00:10:**:**:**:** D) ))

3. After few seconds if collect the onecapture again using below options to skip Linux crash dump collection. This will give compelete faster and less file size, as crash dump is skipped.

#./OneCapture_Linux.sh --FullCapture --Adapters=all --NoCrashDump

4. After this Please collect HBA dump as well. Reason, onecapture failed to collect dump in previous attempt.

# hbacmd dump 10:00:00:10:**:**:**:**

Затем произошла перезагрузка, и линки восстали из мертвых (и даже не пахли). FW карт обновили до версии в описании, а техподдержка Emulex обрадовалась.

Но потом тестирование продолжили и линки начали вновь падать. Стало ясно, что трабл не решен, и логи пришлось собирать заново.

Заказчик, которому переслали инструкции выше, ухмыляясь и хмыкая (а вы бы не ухмылялись?), собрал новые логи... Но и тут не обошлось без проблем выставить флаг более подробных логов не получилось, потому что:

Это, кстати, удалось победить командой echo 0x1ffff > /sys/class/scsi_host/host16/lpfc_log_verbose.

"Не хочешь таблетку вот тебе свечка. Организм тот же пути разные..."

Логи были собраны, и техподдержка Emulex удалилась. Надо сказать, что на анализ логов они потратили всего лишь день.

Ответ был прекрасен:

Our Development team has analyzed the logs and gav below analysis:

====

Below sequence of events have forced the port to offline state:

1. IOCB time out error occurred and the IO abort was issued.

2. SCSI layer issued Device Reset of the LUN.

3. Bus reset performed by driver.

4. After the reset, driver failed to port sgl with -EIO error and brought the port offline.

There were also some kernel traces as well regarding tainted kernel (oracle linux)

wn2pynn00db0022 kernel: Tainted: G OE 4.14.35-1818.3.3.el7uek.x86_64 #2

=====

Our development team believes that, these logs indicate a possible scsi midlayer issue and not LPFC-driver or HBA-Firmware issue. Proper kernel upgrade may be required to resolve this issue.

Конечно, были попытки понять, о чем они пишут, но успехом это не увенчалось. Да, этими заклинаниями только Ктулху будить...

На попытку выяснить а есть ли обходное решение? сказали, что нет.

Делать было нечего, и пришлось идти куда послали к заказчику, просить обновить ядро.

Заказчик, ядро и уже наша техподдержка

Заказчик отзывчив ядро обновили, до версии 3.10.0-1160.15.2.el7. И запустили тест. Линки упали. Доблестные рыцари Постгреса радостно потирали лапки (это было видно по письмам, хотя, это могли быть галлюцинации от неумеренного общения с техподдержкой разных уровней).

Итак, линки все еще падают непонятно от чего, поддержки ОС нет (CentOS же), разобраться в настройках драйвера самостоятельно это угробить кучу времени без шансов на успех (вы видели тот талмуд?! - Вот он).

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

Что забыли потрогать

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

За всё время этой проблемы пошатали и потрогали всё саму ОС, FW, прошивку HW сервера, настройки HW сервера, параметры GRUB, настройки фабрики, свитчей и линков...

Всё, кроме драйвера lpfc.

Терять было уже нечего, и помимо рекомендации перейдите на другую ОС мы попросили еще и обновить драйвер, до последней версии на сайте Emulex до 12.8.340.9.

И это помогло! После обновления драйвера FC-линки больше не падали. От выдоха облегчения чуть не свалился монитор, а сам Паша (реактивный эффект, ага) чуть не упал со стула.

Заказчик подтвердил, что с тех пор проблем с падениями линков уже не было, даже под стресс-нагрузкой.

Возможно, под поверхностью все еще притаился монстр какой-нибудь упущенный фактор, но пока (уже больше 3-х месяцев) все тихо.

Итоги

  1. Удалось победить проблему падения FC-линков обновлением FW, драйвера и ядра до последних (или рекомендованных) версий.

  2. Техподдержка врёт (ну или добросовестно заблуждается), поэтому приходится старательно все проверять самому.

  3. Трогать и шатать при траблшутинге надо всё!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru