|
Professor Seleznov
|
Привет, Хабр! На связи Илья Амосов из команды поддержки Lakehouse-платформы данных Data Ocean Nova вендора Data Sapience. В сегодняшней публикации я раскрою тему влияния динамического сокрытия чувствительных данных на производительность SQL-запросов. Мы сравним различные методики маскирования, узнаем, как работает оптимизатор и движок со скрытыми полями, происходит ли деградация пропускной способности платформы, как влияет на производительность выбранный метод сокрытия чувствительных данных в случаях, если вы используете компонент на базе Apache Ranger.
 В статье я отвечу на четыре практических вопроса: ● Дает ли динамическое маскирование заметный прирост времени выполнения? ● Как ведет себя Ranger и SQL-движок при конкурентной нагрузке при работе со скрытыми данными? ● Что происходит, если маскируемое поле участвует в фильтрации, агрегации или JOIN? ● Какой тип сокрытия оказывается наиболее пригодным для аналитических сценариев? Вступление Любая платформа данных enterprise-класса должна отвечать современным требованиям информационной безопасности, в числе которых есть и ограничение доступа определенных ролей и групп пользователей к чувствительным данным. В зависимости от конечных требований и класса допуска системы инструментами скрытия могут быть либо шифрование, либо маскирование. В данной статье речь пойдет именно о маскировании. Среди обращений пользователей и клиентов нашей платформы данных часто встречается следующий вопрос: «Мы бы хотели включить встроенный функционал маскирования, но не понимаем степень деградации производительности, которую мы должны учесть при планировании запаса вычислительных мощностей». Действительно, при проектировании аналитических платформ почти всегда возникает вопрос: «Можно ли включить динамическое сокрытие данных и не получить заметную деградацию производительности?» На бумаге все выглядит достаточно просто: в SQL-движках платформы политики динамического маскирования применяются через плагин ролевого доступа Apache Ranger, и при выполнении запроса пользователь получает уже не исходные значения чувствительных полей, а их преобразованную версию. Это может быть: ● полное скрытие значения; ● показ последних 4 символов или только 4 первых; ● хэширование; ● подстановка NULL. Для задач безопасности это удобно, а насколько это жизнеспособно в условиях промышленной эксплуатации? Особенно если речь идет не о единичных запросах, а о конкурентной нагрузке, аналитических выборках, JOIN-операциях и фильтрации по большим таблицам. Мы решили измерить поведение нашей реализации Apache Ranger на практике. Цель тестирования была простой: понять, как разные типы динамического маскирования влияют на время выполнения SQL-запросов, стабильность работы под нагрузкой и корректность аналитических запросов. Зачем вообще это тестировать Важно понять, что именно происходит с запросом в момент выполнения.Одно дело, если система подменяет значение в самом конце, когда результат уже почти готов к выдаче пользователю. И совсем другое, если преобразование применяется раньше, еще во время обработки данных. Тогда это влияет не только на то, что увидит пользователь, но и на то, как вообще будет выполняться запрос. А это особенно важно для решения аналитических задач. В простом просмотре данных поле можно скрыть, и на этом задача решена, но что, если скрытые поля участвуют в операциях WHERE, JOIN, GROUP BY? В такой ситуации пользователи хотят заранее понимать: маскирование просто скроет значение в выдаче или начнет заметно влиять на результат запроса. Давайте перейдем к практическим экспериментам. Методика и результаты Чтобы не смешивать все в один большой эксперимент, мы разбили тестирование на несколько отдельных сценариев. Нам было важно понять, в каких именно случаях маскирование почти не влияет на запросы, а в каких его влияние становится заметным.
Как и обычно, мы придерживались принципа — никаких тестирований на одном узле! Конфигурация любого испытания должна быть приближена хотя бы к начальному сценарию промышленной эксплуатации. В нашем случае стенд состоял из: ● 4 compute-узлов, развернутых в Kubernetes; ● 24 vCores CPU на узел; ● 220 ГБ оперативной памяти на узел; ● объектного хранилища на базе MinIO. В качестве эталона использовался вариант без сокрытия, с которым сравнивались остальные подходы. Для разных типов маскирования использовался одинаковый по объему и составу (за исключением скрытых полей) датасет, который состоял из двух таблиц: таблицы клиентов (75 млн записей) и таблицы счетов (150 млн записей). Далее мы шли от простого к сложному. 1. Базовый сценарий — обычный SELECT Сначала мы проверили самый простой сценарий — обычный SELECT, где маскируемые поля возвращаются в результирующем множестве, а фильтрация идет по открытому полю. Такой тест нужен был как отправная точка, чтобы посмотреть, как само маскирование влияет на запрос, если не добавлять ничего лишнего. В обычных запросах заметной просадки не было. В режиме без маскирования среднее время составило 1801 мс. Для режима полного скрытия получили 1935 мс, то есть +7,4%. Хэширование дало 1870 мс, то есть +3,8%. Режим показа последних 4 символов оказался чуть быстрее режима без маскирования — 1709 мс, а подстановка NULL показала наиболее заметное снижение времени — 1111 мс, что соответствует −38,3%.
| Тип маскирования |
Средний размер возвращаемых данных, byte |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
2307 |
1801 |
0,0% |
| Полное скрытие значения |
2307 |
1935 |
+7,4% |
| Показ последних 4 символов |
2299 |
1709 |
−5,1% |
| Хэширование |
3518 |
1870 |
+3,8% |
| Подстановка NULL |
1118 |
1111 |
−38,3% |
В таблице «размер возвращаемых данных» определяется по сумме всей строки, возвращаемой в запросе. Во многом он определяет скорость возврата суммарного датасета на клиентскую часть и объем потребляемой движком оперативной памяти. В данном случае мы видим прямую корреляцию между тем, сколько данных движку надо прочитать и вывести на клиентскую часть, и временем работы. Запомним, что разные режимы сокрытия по-разному влияют на объем возвращаемых данных. Это требует разных ресурсов SQL-движка и влияет на время выполнения. Если смотреть на общую картину, в обычном SELECT, где маскируемые поля возвращаются в результате, а не участвуют в вычислительной логике, заметной просадки не произошло. Иными словами, сам по себе факт сокрытия еще не означает, что запросы резко начнут выполняться дольше. Тестовый запрос
SELECT contractor_id_int, contractor_id_char, last_name, first_name, middle_name, birth_date, contractor_type_id_int, contractor_type_id_char FROM contractor WHERE birth_date BETWEEN '@start_period_date' AND '@end_period_date';
2. Конкурентная нагрузка Следующим шагом мы проверили более жизненный кейс: несколько запросов выполняются параллельно. В реальной системе пользователи не работают по одному, поэтому было важно понять, не появляется ли из-за маскирования нестабильность или неожиданный рост времени отклика при одновременной нагрузке. Разумеется, деградация может во многом зависеть и от типа SQL-движка, выбранного в качестве исполнителя среди доступных в Data Ocean Nova: StarRocks, Impala или Trino, ведь все они по-разному толерантны к конкурентной нагрузке. Данный эксперимент проводился в Impala. Во всех потоках использовался один и тот же запрос с фиксированными параметрами. При этом структура запросов оставалась неизменной, чтобы различия в результатах были связаны именно с маскированием. Такие условия мы сделали намеренно, чтобы исключить влияние различий в выборке на результаты тестирования, ведь мы хотели проверить именно Ranger, а не сам движок. О сравнении производительности самих MPP-движков в конкурентной нагрузке мы пишем регулярно, и ознакомиться с материалами можно тут, и тут, и тут, и еще вот тут. В каждом тесте запускалось 50 параллельных потоков, каждый из которых выполнял по 50 запросов. Общее число запросов за итерацию тестирования — 2500 запросов. В протоколе фиксировалось среднее время выполнения.
| Тип маскирования |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
1999 |
0,0% |
| Полное скрытие значения |
1999 |
0,0% |
| Показ последних 4 символов |
1089 |
−45,5% |
| Хэширование |
1995 |
−0,2% |
| Подстановка NULL |
1398 |
−30,1% |
И здесь система повела себя вполне предсказуемо. В режиме без маскирования среднее время составило 1999 мс, режим полного скрытия значения дал те же 1999 мс, хэширование — 1995 мс, то есть разница практически отсутствует. Показ последних 4 символов и подстановка NULL оказались быстрее режима без маскирования — 1089 мс и 1398 мс соответственно (помним выводы из предыдущего теста про объем возвращаемых данных и его влияние на время запроса). Если смотреть именно на конкурентную нагрузку, ничего неожиданного здесь не произошло. Система отработала ровно, без неприятных сюрпризов и без заметной деградации, только из-за самого факта маскирования. Главный вывод: Ranger легко переварил обработку правил и передал эстафету непосредственно движку-исполнителю. 3. Отдельная проверка влияния самого маскирования После этого мы вынесли в отдельный сценарий оценку того, как влияет на производительность сам механизм сокрытия. Здесь задача была максимально простой: убрать побочные эффекты и посмотреть, насколько меняется время выполнения в ситуации, где сокрытие чувствительных данных не вмешивается в более сложную логику запроса. На старте казалось, что само преобразование значений должно заметно утяжелять запрос. Но в простом сценарии этого не произошло. Пока замаскированное поле не участвует в WHERE, JOIN или агрегации, влияние самого механизма сокрытия остается небольшим.
| Тип маскирования |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
229 |
0,0% |
| Полное скрытие значения |
223 |
−2,6% |
| Показ последних 4 символов |
212 |
−7,4% |
| Хэширование |
234 |
+2,2% |
| Подстановка NULL |
186 |
−18,8% |
Хэширование добавило всего +2,2% к режиму без маски: 234 мс против 229 мс. Режим полного скрытия показал 223 мс, показ последних 4 символов — 212 мс, подстановка NULL — 186 мс. 4. Смешанная нагрузка Дальше мы проверили сценарий, в котором masked и unmasked запросы выполняются одновременно. Для эксплуатации это важный момент: в боевой системе такие режимы почти всегда существуют параллельно. Для режимов полного скрытия значения, показа последних 4 символов и хэширования разница по времени относительно режима без маскирования была минимальной — от −0,9% до +0,3%. Подстановка NULL снова показала ускорение в этой паре. Иными словами, само по себе сосуществование разных режимов доступа не стало отдельной проблемой. Система может одновременно обрабатывать запросы без явной деградации из-за смешения таких режимов.
| Тип маскирования |
Средний размер возвращаемых данных, byte |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| 1 |
Без маскирования |
2307 |
1859 |
0,0% |
| Полное скрытие значения |
2307 |
1857 |
−0,1% |
| 2 |
Без маскирования |
2288 |
1790 |
0,0% |
| Показ последних 4 символов |
2288 |
1774 |
−0,9% |
| 3 |
Без маскирования |
2307 |
1972 |
0,0% |
| Хэширование |
3518 |
1977 |
+0,3% |
| 4 |
Без маскирования |
2294 |
1427 |
0,0% |
| Подстановка NULL |
1118 |
1122 |
−21,4% |
Каждая пара строк представляет сравнение режима без маскирования и соответствующего режима маскирования в условиях смешанной нагрузки. 5. Аналитические сценарии Самые интересные отличия проявились в аналитических сценариях. Пока замаскированное поле просто выводится в SELECT, все выглядит довольно спокойно. Но, как только это поле начинает участвовать в логике запроса, картина меняется. 5.1. Фильтрация по маскируемому полю Сначала мы проверили сценарий, где скрытое поле участвует в WHERE. Это один из самых показательных кейсов, потому что здесь важно понять: сравнение выполняется по исходному значению или уже по его преобразованной версии. В режиме без маскирования время составило 643 мс. Для режима полного скрытия получили уже 5527 мс, то есть +759,6%, для показа последних 4 символов — 5344 мс, то есть +731,1%. Хэш дал 823 мс, что соответствует +28%, а подстановка NULL — всего 43 мс, то есть −93,3%.
 Именно здесь становится видно, что маскирование — это уже не просто «скрыли колонку от пользователя». Как только поле участвует в WHERE и одновременно скрывается, система работает уже не с исходным значением, а с его преобразованной версией. Наиболее заметно это проявилось в режимах полного скрытия значения и показа только последних 4 символов: для нестроковых типов фактически выполняется дополнительное преобразование для каждой строки, и именно поэтому запрос становится заметно дольше.
| Тип маскирования |
Средний размер возвращаемых данных, byte |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
2304 |
643 |
0,0% |
| Полное скрытие значения |
128 |
5527 |
+759,6% |
| Показ последних 4 символов |
128 |
5344 |
+731,1% |
| Хэширование |
128 |
823 |
+28,0% |
| Подстановка NULL |
128 |
43 |
−93,3% |
5.2. Агрегация по скрытым данным Во втором аналитическом сценарии JOIN выполнялся по немаскированному полю, а агрегация шла уже по данным, к которым применена подстановка NULL. Здесь нас интересовало не только время, но и то, как меняется сам смысл результата. Время выполнения оказалось ниже, чем в режиме без маскирования: 2969 мс против 4210 мс, то есть −29,5%. Связано с тем, что объем данных в результате уменьшается. Но здесь важнее не скорость, а логика запроса. Агрегация выполнялась уже не по исходным значениям, а по преобразованным данным. Формально запрос работал корректно, ошибок не было, результат возвращался, но интерпретировать этот результат нужно уже иначе: он отражает не исходные данные, а их маскированное представление. Это как раз тот случай, когда запрос не ломается, но начинает считать уже не то, что считалось бы без маскирования.
| Тип маскирования |
Средний размер возвращаемых данных, byte |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
450 |
4210 |
0,0% |
| Подстановка NULL |
212 |
2969 |
−29,5% |
5.3. JOIN по полю с NULL Дальше мы проверили JOIN по замаскированному полю в режиме подстановки NULL. Здесь результат предсказуем, но от этого не менее важен. Если поле в условии соединения приводится к подстановке NULL, совпадений не возникает, JOIN не находит пары строк и фактически разваливается. Время выполнения в этом сценарии составило 3076 мс против 5832 мс у базового, то есть −47,3%. Но это как раз тот случай, когда быстрее не значит лучше. Запрос выполняется быстрее потому, что полноценного сопоставления строк больше не происходит. С точки зрения аналитики такой режим для ключей соединения фактически бесполезен.
| Тип маскирования |
Средний размер возвращаемых данных, byte |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
451 |
5832 |
0,0% |
| Подстановка NULL |
52 |
3076 |
−47,3% |
5.4. JOIN по полю с хэш И, наконец, самый интересный сценарий — JOIN по полю, замаскированному через хэш. Логика здесь понятна: одинаковые исходные значения дают одинаковый хэш, а значит, теоретически данные можно сопоставлять и без раскрытия оригинала. И действительно, в этом сценарии JOIN отработал корректно, строки возвращались, агрегация и группировка тоже выполнялись. Но задержка оказалась очень высокой: 113 083 мс против 8857 мс у режима без маскирования, то есть +1176,8%.
| Тип маскирования |
Среднее время выполнения, мс |
Отклонение от базового режима, % |
| Без маскирования |
8857 |
0,0% |
| Хэширование |
113083 |
+1176,8% |
И вот это, пожалуй, один из самых важных результатов всей работы. Хэширование действительно сохраняет аналитическую связность данных, но в сложных сценариях за это приходится серьезно платить производительностью. Но давайте разберемся в причине деградации на детальном текстовом профиле запроса. Сперва сравним суммарную статистику по операциям.

Агрегация плана исполнения без хэширования

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

Детальный фрагмент плана запроса без хэширования

Детальный фрагмент плана запроса с хэшированием В узле HASH JOIN оптимизатор знает о связи между ключом соединения в разных таблицах как pk/fk, но само соединение выполняется по функции mask_hash(pk/fk). При формировании фрагмента JOIN-движок создает блум-фильтр, который в случае с немаскированными данными успешно применяется фрагментом сканирования и читает из всего множества только 16,2 миллиона записей, благодаря min/max-фильтрации на основе storage-индекса в файлах Parquet. В случае сокрытия данных узлу сканирования приходится читать уже полное множество в 150 миллионов записей, что, собственно, и является главной причиной такого замедления. Выводы Первый— динамическое маскирование само по себе не выглядит проблемой для обычных запросов. Если скрытое поле просто выводится в результате, а не участвует в вычислительной логике, то влияние на производительность обычно невелико. Это касается не только обычного чтения, но и конкурентной нагрузки, и смешанных режимов доступа. Apache Ranger хорошо справляется с интенсивной нагрузкой при запросе правил сокрытия данных от SQL-движка, так как основная нагрузка лежит на плечах движка-исполнителя. Второй— реальные сложности начинаются не в момент, когда поле скрывают от пользователя, а в момент, когда над этим полем работают операторы WHERE, GROUP BY или JOIN. Именно там маскирование перестает быть просто визуальным и начинает влиять на саму логику выполнения запроса. Третий— разные типы маскирования подходят для разных задач. Подстановка NULL хорошо скрывает данные, но полностью убивает аналитику по таким полям, если они участвуют в соединениях или расчетах. Применимость этого метода можно определить как «скрытие доступа к конечным данным», например, к рассчитанной витрине. Режимы частичного маскирования с отображением только 4 символов удобны для отображения, но в отдельных сценариях фильтрации могут резко утяжелять запрос. Применимость скрытия по маске на практике возможна даже и для решения некоторых аналитических задач, для которых подходит сценарий «4 первых» или «4 последних». Хэширование остается самым интересным компромиссом: позволяет сохранить сопоставимость значений и детерминированность результата при операциях JOIN или группировке, но в сложных аналитических запросах это может сказаться на производительности. И, пожалуй, главный вывод — динамическое маскирование в Lakehouse-платформе данных Data Ocean Nova — полезная встроенная функция, позволяющая избежать «дедовского метода» создания слоев представлений для каждой роли с ограниченным доступом к данным. В настоящее время мы проводим нагрузочные испытания на промышленном контуре при работе с общим числом ролей в Ranger от 50 до 100 тысяч. Следите за обновлениями. Подписывайтесь на хаб Data Sapience, чтобы следить за новыми публикациями.-Источник
|