English documentation: README.en.md, SMOKE.en.md, FAILOVER.en.md
HMS Proxy — это catalog-aware Hive Metastore federation and compatibility proxy для смешанных
окружений с Apache Hive 3.1.3, Hive 4.1.x и Hortonworks Data Platform 3.1.0.x.
Прокси открывает рабочие точки входа (Thrift endpoints) HMS, которые маршрутизируют запросы к каталогам между бэкендами metastore, сглаживают различия в API Apache 3.1.3, HDP 3.1.0.x и Hive 4.1.x в обе стороны и задают границу безопасности (security boundary) между клиентами и бэкендами HMS.
HiveServer2 и прямые клиенты HMS API подключаются к единой точке входа (Thrift endpoint) или к разным портам, когда клиенты разных версий Hive не могут использовать общую точку входа — в протоколе Thrift отсутствует согласование версий через рукопожатие. Прокси маршрутизирует запросы в бэкенды metastore по явному catName или по имени базы данных вида catalog<separator>db.
Так маршрутизация каталогов и выборочная публикация (selective exposure) работают централизованно, а клиентам не нужно знать внутреннее устройство бэкендов metastore.
Прокси поддерживает интерфейсы Apache Hive Metastore 3.1.3, Hortonworks 3.1.0.x и Hive 4.1.x на стороне входа (основной слушатель и дополнительные порты) поверх любой комбинации бэкендов Apache 3.1.3, Hortonworks 3.1.0.x и Hive 4.1.x.
Прокси переводит вызовы в обе стороны:
- для старых бэкендов Hortonworks выполняет обратную трансляцию (downgrade) методов
*_req; - для клиентов Apache 3.1.3 к бэкенду Hive 4 повышает (upgrade) два позиционных метода чтения, удалённых в Hive 4 (
get_tableиget_table_objects_by_name); - для клиентов Hive 4 к бэкенду Apache 3.1.3 переводит методы-обёртки
*_reqдля чтения и стандартного DDL в позиционные эквиваленты Apache 3.1.3.
Специфичные для Hortonworks RPC доступны через jar-файл HDP standalone-metastore при настроенном бэкенде HDP. Вызовы API, появившихся только в Hive 4 (data connectors, scheduled queries, stored procedures, packages, ACID v2 extensions), возвращают ошибку TApplicationException UNKNOWN_METHOD.
На практике прокси становится мостом для смешанной инфраструктуры (estate) Apache, HDP и Hive 4 и поэтапных миграций в обе стороны, а не просто маршрутизатором запросов.
Прокси работает границей безопасности (security boundary) между клиентами и бэкендами metastore: принимает Kerberos/SASL на входе, при необходимости использует исходящий Kerberos к бэкенду и выполняет имперсонацию аутентифицированного пользователя Kerberos.
Благодаря этому входная аутентификация, передача контекста пользователя (identity propagation) и политики доступа к бэкендам собраны в одной точке.
В этом README встречаются два слоя именования:
| Термин | Что означает |
|---|---|
| Внешнее имя | Клиентский namespace на границе proxy. Примеры: catName=catalog2, catalogName=catalog2, legacy dbName=catalog2__sales. |
| Внутреннее имя | Реальные имена на выбранном backend HMS. Пример: backend catName=hive, dbName=sales. |
| Default catalog | routing.default-catalog, куда proxy идёт, если запрос не несёт надёжного catalog hint. |
| Ambiguous request | В запросе есть конфликтующие catalog hints, либо multi-catalog write не разрешается ровно в один catalog. |
Дальше routing работает так:
| Форма запроса | Как proxy выбирает backend | Что происходит без namespace |
|---|---|---|
| Object-scoped read/write RPC | Сначала явный proxy catName. Иначе внешний dbName / fullTableName вида catalog2__sales. Иначе routing.default-catalog для совместимости. |
Идёт в routing.default-catalog. |
| Session-level и global read RPC | Используется routing.default-catalog. |
Идёт в routing.default-catalog. |
| ACID RPC, где в payload ещё назван объект | Routing по этому payload, например по dbName или fullTableName в get_valid_write_ids, allocate_table_write_ids, compact или add_dynamic_partitions. |
Если routable object namespace уже нет, применяется строка ниже. |
| Txn / lock lifecycle RPC, где остались только ids | Привязаны к routing.default-catalog, например open_txns, commit_txn, abort_txn, check_lock, unlock, heartbeat. |
Идут в routing.default-catalog; non-ACID SELECT, допустимые NO_TXN DDL и non-transactional write lock на non-default catalog может обслужить synthetic shim. |
| Global write и catalog-registry RPC | Требуют ровно один owned namespace в multi-catalog deployment. | Если принадлежность каталогу неоднозначна, прокси завершает вызов ошибкой, не пытаясь угадать целевой каталог. |
Если запрос несёт backend catName вроде hive, а не proxy catalog id, это поле для
совместимости не считается авторитетным: proxy пытается маршрутизировать по dbName, а если
этого мало, идёт в routing.default-catalog.
При мутациях и записи прокси опирается на строгие гарантии, а не на эвристические догадки: детерминированная маршрутизация, явное владение пространством имён, защита от рассинхронизации (silent split-brain writes) и безопасный отказ при любой неоднозначности.
Следующие настройки меняют клиентское представление имён или SQL, но не сам выбор backend:
| Switch | Эффект |
|---|---|
routing.catalog-db-separator |
Меняет внешний legacy формат, например catalog2__sales вместо catalog2.sales. |
federation.preserve-backend-catalog-name=true |
Возвращает backend catName / catalogName вроде hive, но routing всё равно идёт по внешнему dbName или явному proxy catalog. |
federation.view-text-rewrite.mode=REWRITE |
Переписывает SQL внутри view между внешними и внутренними именами (включено по умолчанию); на выбор backend для самого RPC не влияет. |
| Группа RPC | Статус | Поведение |
|---|---|---|
Metadata read/write RPC с явным namespace (catName, dbName, fullTableName) |
supported | Нормально маршрутизируются в нужный catalog/backend. |
Legacy read/write RPC с database name в формате catalog<separator>db |
supported | Маршрутизируются по externalized database name; table/database объекты переписываются на обратном пути. |
Apache 3.1.3 wrapper RPC против старых Hortonworks 3.1.0.x backend |
degraded | Proxy повторяет часть *_req API через старые legacy методы вроде get_table. |
Apache 3.1.3 positional RPC против Hive 4.1.x backend (catalog.<name>.runtime-profile=APACHE_4_1_0) |
degraded | Backend adapter автоматически поднимает два positional метода, удалённых в Hive 4 (get_table, get_table_objects_by_name), до их *_req эквивалентов; всё остальное идёт через binary-compatible Thrift делегацию без изменений. |
Hive 4.1.x клиенты (compatibility.frontend-profile=APACHE_4_1_0) против Apache 3.1.3 или Hortonworks 3.1.0.x backend |
degraded | Frontend bridge понижает выбранные Hive 4-only *_req wrappers на read и стандартного DDL (get_database_req, get_table_req, get_partition*_req, create_table_req, alter_table_req и т.п.) до их позиционных Apache 3.1.3 эквивалентов; 199 общих методов идут через binary-compatible делегацию. |
Hive 4-only API (data connectors, scheduled queries, stored procedures, packages, ACID v2 extensions) на APACHE_4_1_0 front door |
rejected | Отвечают TApplicationException UNKNOWN_METHOD; для этих RPC нет безопасного downgrade в Apache 3.1.3 backend. |
| Session-level и global read-only RPC без catalog context | degraded | Идут в routing.default-catalog, включая getMetaConf, get_all_functions, get_metastore_db_uuid, get_current_notificationEventId, get_open_txns и get_open_txns_info. |
Read-only service API, которых нет на backend (TApplicationException UNKNOWN_METHOD на notifications, privilege refresh/introspection, token/key listings кроме delegation-token issuance, txn/lock/compaction status) |
degraded | Proxy возвращает empty compatibility response вместо ошибки. Если метод у backend есть, но вызов провалился (любой другой тип TApplicationException, transport failure или MetaException), клиент получает ошибку — пустой ответ не выдаётся за реальное ACID, lock или privilege состояние. |
Optional service read RPC (get_active_resource_plan, get_all_resource_plans, get_runtime_stats) |
degraded | Proxy возвращает empty compatibility response при любом сбое backend, потому что эти RPC отдают только опциональные workload-management и diagnostic данные. |
ACID / txn / lock lifecycle RPC без routable namespace (open_txns, commit_txn, abort_txn, check_lock, unlock, heartbeat) |
degraded | Пинятся к routing.default-catalog; для non-ACID SELECT lock, допустимых non-transactional NO_TXN DDL lock и non-transactional write lock на non-default catalog proxy может использовать synthetic lock shim, но это всё ещё не distributed ACID coordinator и shim не сериализует параллельных писателей. |
| Global write operations без явного catalog context | rejected | Proxy навязывает deterministic routing и explicit namespace ownership: namespace-less service write RPC вроде setMetaConf, grant_role, revoke_role и add_token безопасно падают вместо угадывания каталога. |
Управление каталогами (create_catalog, drop_catalog) |
rejected | Ownership каталогов policy-managed в конфиге proxy, а не через клиентские RPC. |
| HDP-only front-door методы, для которых есть явный Apache bridge mapping | supported | Proxy адаптирует выбранные Hortonworks request-wrapper методы к Apache equivalents. |
HDP-only методы, которым нужен совместимый Hortonworks runtime (add_write_notification_log, get_tables_ext, get_all_materialized_view_objects_for_rewriting) |
passthrough | Пробрасываются только в совместимые Hortonworks backend/front door при соответствующей конфигурации. |
| HDP-only методы без безопасного Apache mapping | rejected | Proxy падает явно, а не возвращает вводящий в заблуждение success. |
Это публичная сводка того, как позиционировать proxy. Матрица сгруппирована не по отдельным thrift методам, а по типу клиента, advertised front door, backend runtime, auth mode и семействам методов. Таблица генерируется из capabilities.yaml, а каждая capability привязана к smoke-тестам в test suite.
Если нужен табличный обзор по поддержке методов бэкендами, режимам маршрутизации, стратегиям отката (fallback) и семантическим рискам, см. COMPATIBILITY.md.
Обновить сгенерированную таблицу можно так:
mvn -o -q -Dtest=CapabilityMatrixDocSyncTest -Dcapabilities.updateReadme=true test| Версия клиента | Профиль front door | Профиль backend | Режим auth | Семейства методов | Ожидаемый результат |
|---|---|---|---|---|---|
Клиенты Apache Hive / Spark, использующие обёртки запросов (request wrappers) Apache HMS 3.1.3 |
APACHE_3_1_3 |
APACHE_3_1_3 |
NONE или KERBEROS |
catalog-aware read/write, legacy catalog<separator>db routing, view rewrite |
Базовый полностью поддерживаемый сценарий. |
Клиенты Apache Hive / Spark, использующие обёртки запросов (request wrappers) Apache HMS 3.1.3 |
APACHE_3_1_3 |
Hortonworks 3.1.0.x |
NONE или KERBEROS |
read path, часть metadata write, где возможен fallback с *_req API |
Поддерживается через обратную трансляцию (compatibility downgrade); часть вызовов работает в degraded-режиме через legacy RPC. |
Клиенты Hive 4.1.x (beeline, JDBC, Spark, Trino-Hive), использующие Thrift API обёрток запросов Hive 4 |
APACHE_4_1_0 |
APACHE_3_1_3, Hortonworks 3.1.0.x или APACHE_4_1_0 |
NONE или KERBEROS |
199 read/write методов общих с Apache 3.1.3 (binary-compatible), часть Hive 4-only *_req wrappers понижается до позиционных Apache 3.1.3 RPC (включая create_table_req, get_table_objects_by_name_req, get_all_table_constraints, delete_column_statistics_req, get_max_allocated_table_write_id, append_partition_req), lock с Hive 4-only типом EXCL_WRITE понижается до EXCLUSIVE |
Поддерживается через обратную трансляцию (compatibility downgrade) для read + стандартного DDL; Hive 4-only API (data connectors, scheduled queries, stored procedures, packages, ACID v2 extensions) отвечают TApplicationException UNKNOWN_METHOD. |
Клиенты Apache Hive / Spark, использующие обёртки запросов (request wrappers) Apache HMS 3.1.3 |
APACHE_3_1_3 |
Hive 4.1.x |
NONE или KERBEROS |
199 read/write методов общих с Hive 4 через binary-compatible Thrift делегацию, позиционные get_table и get_table_objects_by_name (удалены в Hive 4) автоматически поднимаются до *_req эквивалентов |
Поддерживается через повышение совместимости (compatibility upgrade) для двух позиционных методов, удалённых в Hive 4; остальные RPC проходят прозрачно. Hive 4-only API (data connectors, scheduled queries и т.п.) недоступны с Apache 3.1.3 front door, потому что у Apache 3.1.3 нет Thrift bindings для них. |
Клиенты Hortonworks, которым достаточно идентификации Hortonworks на фронтенде через getVersion() |
HORTONWORKS_* без standalone jar |
APACHE_3_1_3 или Hortonworks 3.1.0.x |
NONE или KERBEROS |
пересекающиеся Apache/HDP method families | Поддерживается, если клиенту достаточно только смены анонсируемого профиля (advertised profile). |
| Клиенты Hortonworks, вызывающие HDP-only thrift методы-обёртки (request wrappers) | HORTONWORKS_* с standalone jar |
Hortonworks 3.1.0.x |
NONE или KERBEROS |
mapped HDP-only methods, runtime-specific passthrough methods | Поддерживается при наличии совместимых Hortonworks front-door и backend runtime jar. |
| Клиенты Hortonworks, вызывающие HDP-only thrift методы-обёртки (request wrappers) | HORTONWORKS_* с standalone jar |
APACHE_3_1_3 |
NONE или KERBEROS |
HDP-only passthrough методы вроде add_write_notification_log |
Явно отклоняется, если target backend не даёт совместимый Hortonworks runtime. |
| Нагрузки HiveServer2 / Beeline SQL через несколько каталогов | APACHE_3_1_3 или HORTONWORKS_* |
смешанные бэкенды Apache + Hortonworks | NONE или KERBEROS |
read, DDL/DML, namespace rewrite, optional view rewrite | Поддерживается, пока маршрутизация может однозначно определить целевой каталог. |
| Клиенты HiveServer2 / прямые клиенты HMS, использующие методы жизненного цикла транзакций и блокировок (txn/lock) без пространства имён в полезной нагрузке | любой | смешанные бэкенды Apache + Hortonworks | NONE или KERBEROS |
open_txns, commit_txn, abort_txn, check_lock, unlock, heartbeat |
Degraded: перенаправляются в routing.default-catalog; допустимые non-ACID SELECT, NO_TXN DDL и блокировки не-транзакционной записи (INSERT/UPDATE/DELETE) всё же могут синтетически обслуживаться на каталогах, отличных от default, но в остальном данный контур следует рассматривать как single-catalog control plane, пока не проведена отдельная валидация. |
| Клиенты HiveServer2 / HMS с Kerberos, которым требуется передача идентификатора конечного пользователя (end-user identity) на бэкенд | любой | любой | KERBEROS с optional impersonation |
front-door SASL, local delegation-token issuance, backend Hadoop proxy-user, delegation tokens и set_ugi() имперсонация |
Поддерживается, если правильно настроены правила proxy-user и разрешения имперсонации на бэкенде. |
| Клиенты, пытающиеся выполнять операции изменения без явного владения пространством имён (explicit namespace ownership) или динамически управлять реестром каталогов | любой | любой | NONE или KERBEROS |
policy-guarded ambiguous mutations, create_catalog, drop_catalog |
Безопасно отклоняется на уровне архитектуры (by design), чтобы сохранить детерминированную маршрутизацию (deterministic routing), явное владение пространствами имён и предотвратить скрытую рассинхронизацию записей (silent split-brain writes). |
| HiveServer2 / HMS клиенты с end-user impersonation или Kerberos identity, запрашивающие метаданные | любой | любой | NONE или KERBEROS |
чтение метаданных (get_all_databases, get_databases, get_database, get_all_tables, get_tables, get_tables_ext, get_table, get_table_req, get_table_meta), общий глобальный кэш метаданных для всех пользователей, вычисление политик Ranger отдельно по каталогам |
Метаданные кэшируются один раз для всех пользователей при shared-across-users=true, а встроенные плагины Apache Ranger фильтруют списки баз данных и таблиц индивидуально для каждого пользователя без лишних backend RPC. |
- legacy database references без префикса каталога идут в
routing.default-catalog - это сохраняет совместимость со Spark/Hive и одновременно поддерживает deterministic routing без silent split-brain metadata write
- на практике это означает, что ACID write lifecycle полноценно поддерживается только для
default-catalog, если из payload нельзя извлечь namespace - request-based ACID методы, где в payload есть
dbNameилиfullTableName, продолжают маршрутизироваться по этому payload - Обращения к базам данных без префикса каталога направляются в
routing.default-catalog. - Это сохраняет совместимость со Spark и Hive и обеспечивает детерминированную маршрутизацию без риска незаметной рассинхронизации (silent split-brain).
- На практике жизненный цикл ACID-записи полноценно поддерживается только для
default-catalog, если из полезной нагрузки нельзя извлечь пространство имён. - Запросные ACID-методы, где в полезной нагрузке указаны
dbNameилиfullTableName, продолжают маршрутизироваться по этим объектам. - Прокси синтетически обслуживает не-ACID блокировки
SHARED_READSELECT, допустимые блокировкиNO_TXNDDL и блокировки не-транзакционной записи (INSERT,UPDATE,DELETE) для каталогов, отличных от default. Благодаря этому чтения в HiveServer2, DDL и не-ACIDINSERTне зависят от рассинхронизации транзакций на бэкендах. - Состояние синтетических блокировок по умолчанию хранится в памяти, а для отказоустойчивости нескольких инстансов может выноситься в ZooKeeper. Это не делает распределенную запись ACID в несколько каталогов безопасной.
- Операции Hive ACID, блокировки, токены и другие глобальные функции метастора требуют предварительной проверки в вашей среде перед переводом за multi-catalog прокси.
В многокаталожной среде схемы удаленных каталогов по умолчанию получают префикс каталога (например, remote__beemetrics).
С помощью настройки catalog.<name>.unprefixed-databases можно назначить конкретные базы данных удаленному каталогу без префикса:
catalog.remote.unprefixed-databases=beemetrics,another_db- Вытеснение (shadowing): если одноименная база данных присутствует и в каталоге по умолчанию (
routing.default-catalog), то в объединенном списке (SHOW DATABASES,get_all_databases,get_databases) удаленная схема вытесняет локальную (локальная база с тем же именем скрывается). - Прямая маршрутизация: все обращения к этой схеме (
get_database,get_table,get_partitions,create_table, Iceberg REST write gate и блокировки) направляются напрямую в указанный удаленный каталог. - Ограничения: параметр нельзя указывать на
routing.default-catalog, имена схем не могут содержать шаблоны/wildcards (*,%,?), а одна и та же схема не может быть назначена нескольким удаленным каталогам.
В отличие от жесткого маппинга unprefixed-databases, настройка catalog.<name>.merge-with-default=true включает динамическое мягкое объединение всех схем удаленного каталога с дефолтным каталогом:
catalog.remote.merge-with-default=true- Без ручных списков: не требуется перечислять схемы вручную — они обнаруживаются автоматически.
- Без вытеснения (shadowing): если база данных присутствует в каталоге по умолчанию (
routing.default-catalog), она сохраняется в дефолтном каталоге и обслуживается им без префикса. Одноименная база удаленного каталога не скрывается, а остается доступна по явному префиксу (например,remote__finance). - Непрефиксированные схемы: схемы, существующие только в удаленном каталоге (которых нет в дефолтном), выставляются клиенту без префикса каталога и маршрутизируются в удаленный каталог (при этом канонический доступ с префиксом
remote__schemaтакже сохраняется). - Создание схем (
create_database): создание новых схем без явного префикса каталога при включенной фиче всегда направляется в дефолтный каталог. Для создания схемы в удаленном каталоге требуется явный префикс (create_database("remote__new_db")). - Ограничения: параметр нельзя указывать на
routing.default-catalog, и его нельзя сочетать сunprefixed-databasesна одном каталоге. - Поиск неизвестного имени: имя без префикса, которого прокси ещё не видел, проверяется через
get_databaseсначала в дефолтном каталоге, затем в объединяемых. Параллельные запросы к одному имени делят одну такую проверку. Промах запоминается наrouting.merge-with-default.negative-cache-ttl-ms(дефолт5000;0отключает запоминание), но только если каждый бэкенд ответил «базы нет» (NoSuchObjectException): ошибка или недоступность бэкенда промахом не считается, иначе имя на весь TTL уходило бы мимо каталога, где база на самом деле есть. Имена приходят от клиентов, поэтому кэш промахов ограниченrouting.merge-with-default.negative-cache-max-entries(дефолт10000): сначала вытесняются истёкшие записи, а заполненный живыми промахами кэш просто перестаёт запоминать новые, пока старые не истекут.
Proxy умеет опционально включать latency-aware обработку медленных или нестабильных backend metastore. Этот слой по умолчанию выключен, поэтому существующие deployment не меняют поведение, пока ты явно не включишь новые настройки.
Когда он включён, proxy может:
- держать per-catalog latency budget через
catalog.<name>.latency-budget-ms - считать per-backend latency EWMA и на его основе подбирать adaptive backend socket timeout
- открывать circuit после повторяющихся transport failure или time-budget breach и затем давать
half-open retry после
routing.circuit-breaker.open-state-ms - polling'ом обновлять backend readiness в фоне, а не только во время запроса к
/readyz - запускать в parallel безопасные read-only fanout RPC между несколькими каталогами
- опционально кэшировать fanout-ответы со списками баз (
SHOW DATABASES/get_all_databases/get_databases) черезrouting.database-list-cache.ttl-ms(илиttl-seconds) - опционально кэшировать объекты метаданных баз данных (
get_database) черезrouting.database-metadata-cache.ttl-ms(илиttl-seconds) для ускорения проверок прав в HiveServer2 / Ranger - упреждающе обновлять оба кэша баз данных в фоне (
routing.database-cache.background-refresh.*), поддерживая их горячими в пределах окна активности клиентов (например, 1 час с интервалом 1 минута) без блокировок на бэкенд-опросы - мгновенно отвечать synthetic success на вызовы
refresh_privilegesчерезrouting.refresh-privileges.synthetic-success=true(илиrouting.refresh-privileges.mode=SYNTHETIC_SUCCESS) для устранения нагрузки PrivilegeSynchronizer в HiveServer2 - кэшировать конфигурационные параметры метастора (
get_config_value) в памяти черезrouting.config-value-cache.ttl-ms(илиttl-seconds, по умолчанию 1 час) с single-flight дедупликацией и negative caching для отсутствующих ключей - кэшировать метаданные таблиц (
get_table) черезrouting.cache.table-metadata.*и разделов (get_partition*,get_part_specs*) черезrouting.cache.partition-metadata.*с single-flight дедупликацией, LRU-вытеснением, автоматической локальной и распределенной ZooKeeper DDL-инвалидацией (routing.cache.distributed-invalidation.*) приalter_table*,drop_table*,truncate_table*,drop_database - отдавать устаревшие метаданные таблиц и разделов при авариях и разрывах связи с удаленным бэкендом через
routing.cache.serve-stale-on-error=trueв течениеrouting.cache.stale-grace-period-ms(по умолчанию 5 минут) - защищать удаленные WAN-каналы от перегрузки через WAN Concurrency Governor (
catalog.<name>.max-concurrent-callsиconcurrency-timeout-ms) - запускать прокси без блокировки при временной недоступности вторичных/удаленных каталогов через
catalog.<name>.startup-mode=LENIENT - предотвращать ложный вывод прокси из балансировщика при сбоях удаленного ЦОД через
catalog.<name>.required-for-readiness=falseиmanagement.readyz.require-all-catalogs=false - прозрачно перенаправлять read-only запросы на локальную теневую реплику при аварии основного каталога через
catalog.<name>.fallback-catalogиfallback-on-outage=trueбез риска split-brain для мутаций - объединять одновременные одинаковые запросы к бэкенду (single-flight) для предотвращения исчерпания пула сессий
- исключать degraded backend из таких safe fanout read при
routing.degraded-routing-policy=SAFE_FANOUT_READS - подробное руководство по сценариям отказов и архитектуре cross-DC см. в FAILOVER.md (EN: FAILOVER.en.md)
Это намеренно узкий механизм: hedged read и пропуск деградировавших каталогов действуют только для безопасных методов чтения веером (fanout), сейчас это get_all_databases, get_databases и get_table_meta. Запись в конкретный бэкенд и чувствительные к пространству имён мутации по-прежнему маршрутизируются строго детерминированно и не создают гонок между метасторами.
Кэши списков баз и метаданных баз по умолчанию выключены (ttl-ms=0). При их включении повторные запросы SHOW DATABASES или get_database не обращаются к бэкенду до истечения срока жизни (TTL). Оба кэша объединяют одинаковые параллельные запросы (single-flight), возвращают защитные копии (defensive copies) Thrift-структур, поддерживают скользящее продление срока жизни при пакетной загрузке (чтобы длинные сканирования HiveServer2 и Ranger не приводили к устареванию данных в середине цикла) и автоматически сбрасываются при DDL-мутациях (create_database, alter_database, drop_database) через прокси.
Non-impersonated вызовы к каталогу идут через borrow/return пул backend Thrift-сессий размером
catalog.<name>.shared-session-pool-size. Дефолт — 1, и он сохраняет прежнее поведение с
единственной сессией: параллельные shared-вызовы упрутся в один Thrift transport. Чтобы реально
получить параллелизм, задайте это значение явно по каталогам, например:
catalog.catalog1.shared-session-pool-size=8Что учитывать при повышении:
- Больше idle Thrift-сессий держится открытыми к backend HMS (с пропорциональной стоимостью Kerberos, если он включён на backend).
reconnectSharedполностью сбрасывает (очищает) весь пул, поэтому пулы большого размера дольше пересоздаются при смене профиля выполнения (runtime profile) или принудительном переподключении (reconnect).
Если на каталоге включена impersonation, реальный аутентифицированный клиентский трафик через этот
пул не идёт — каждый caller обслуживается из per-user кэша impersonation client'ов, управляемого
catalog.<name>.impersonation-max-clients и catalog.<name>.impersonation-client-idle-ttl-ms.
Внутри кэша на каждого пользователя живёт собственный пул backend Thrift-сессий размером
catalog.<name>.impersonation-pool-max-size (дефолт 4). Сессия, простоявшая в пуле дольше
catalog.<name>.impersonation-session-idle-ttl-ms (дефолт 600000, 10 минут), закрывается; пользователь,
не делавший вызовов дольше catalog.<name>.impersonation-client-idle-ttl-ms (дефолт 1800000,
30 минут), вытесняется из кэша вместе со всеми своими сессиями. Оба TTL применяет фоновый поток
hms-proxy-impersonation-sweep-<catalog> (период — половина меньшего из них, не чаще 100 мс), поэтому
соединения пользователя, который больше не вернётся, тоже закрываются. 0 отключает соответствующий TTL.
Под Kerberos каждая impersonation-сессия аутентифицируется delegation token'ом, который прокси получает
у бэкенда; при закрытии сессии токен отзывается (cancel_delegation_token от имени принципала прокси,
его renewer'а) в фоновом потоке, а не копится в token store бэкенда до истечения срока жизни. Итог
отзыва виден в hms_proxy_backend_delegation_token_cancels_total{outcome=cancelled|failed|dropped|skipped_runtime_closed}.
Параллельные вызовы
одного пользователя (или гонящего их HiveServer2) занимают разные сессии и реально идут в параллель,
а не сериализуются на одном Thrift transport. В Grafana это видно по метрикам
hms_proxy_impersonation_pool_users, hms_proxy_impersonation_pool_sessions{state=active|idle},
hms_proxy_impersonation_session_acquire_timeouts_total (timeout per-user borrow) и
hms_proxy_impersonation_session_evictions_total{reason=idle|transport_failure|user_evicted|user_capacity}.
В shared пуле тогда остаются только:
- внутренние proxy-driven вызовы: backend health probe и reconnect-логика;
- запросы от service principal'ов, перечисленных в
security.service-principals.*, — они by design обходят impersonation и переиспользуют общую backend-сессию; - чтение конфигурации metastore (
get_config_value) — конфигурация HMS не зависит от пользователя, поэтому для предотвращения очередей и таймаутов в per-user пулах имперсонации вызовыget_config_valueнаправляются через shared session pool дефолтного каталога и кэшируются в памяти (routing.config-value-cache.*).
В чистом impersonation-deployment без service-principal клиентского трафика повышать
shared-session-pool-size бессмысленно — на throughput приложения это не влияет. Тюнить пул нужно,
если хотя бы на одном каталоге impersonation-enabled=false или если значимую долю запросов к
каталогу гонят service principal'ы (например, сам HiveServer2).
mvn -o test
mvn -o package
mvn -q -DforceStdout help:evaluate -Dexpression=project.versionВерсия сборки вычисляется из git. Обычные branch-сборки используют настроенный jgitver-шаблон
1.0.<git-distance>-<short-sha>, tag release берёт pushed-тег vMAJOR.MINOR.PATCH, а rolling
nightly фиксирует Maven на последнем достижимом release-теге, поэтому nightly после v1.2.0
собирает jar-файлы версии 1.2.0-<short-sha>.
GitHub Actions автоматически публикует сборки:
- branch и pull-request CI загружают jar-файлы как artifacts
- каждый push в
main, плюс ручнойworkflow_dispatch, обновляет rolling prereleasenightlyс актуальногоmain - pushed-теги
v*публикуют обычные releases с GitHub-generated release notes
Ручные релизы публикуются через workflow Release:
- запускается через
workflow_dispatch - передаётся только
major_minor, например1.7 - workflow сам вычисляет следующий patch и публикует GitHub release с тегом вида
v1.7.0,v1.7.1и далее
java -jar "target/hms-proxy-$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)-fat.jar" /etc/hms-proxy/hms-proxy.propertiesmvn package создаёт обычный jar и runnable fat jar с classifier fat.
Имя fat jar меняется на каждом новом коммите.
Для Java 17+ с Kerberos-библиотеками Hadoop 2.x запуск выполняется следующим образом:
java \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
-Djava.security.krb5.conf=/etc/krb5.conf \
-jar "target/hms-proxy-$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)-fat.jar" /etc/hms-proxy/hms-proxy.propertieslibthrift принимает клиентские сокеты с бесконечным read timeout, поэтому клиент, исчезнувший
без FIN/RST (сбой сети, kill процесса за NAT), навсегда оставляет свой worker-поток
заблокированным в read. Пул worker'ов ограничен server.max-worker-threads, так что listener
медленно вымывается вплоть до полного отказа принимать новые соединения. Поэтому каждый
listener ограничивает жизненный цикл сокета:
| Ключ | По умолчанию | Смысл |
|---|---|---|
server.client-socket-timeout-ms |
600000 |
Read timeout на принятом соединении; 0 возвращает неограниченное поведение libthrift |
server.tcp-keepalive |
true |
SO_KEEPALIVE на принятых соединениях |
server.tcp-keepalive-idle-seconds |
120 |
Простой до первой keepalive-пробы |
server.tcp-keepalive-interval-seconds |
30 |
Интервал между keepalive-пробами |
server.tcp-keepalive-count |
4 |
Сколько проб должно провалиться, прежде чем соединение будет закрыто |
server.shutdown-timeout-seconds |
30 |
Ограничение на упорядоченную остановку после SIGTERM |
client-socket-timeout-ms — именно read timeout: он ограничивает, сколько worker ждёт
следующего запроса на уже установленном соединении, и не обрывает долгие серверные вызовы вроде
drop_table с purge, потому что во время обработки запроса чтения нет. Дефолт согласован по
порядку величины с хайвовым hive.metastore.client.socket.timeout (600s); клиенты, обёрнутые в
RetryingMetaStoreClient (HiveServer2, Spark), переподключаются прозрачно, когда idle-соединение
рециклится.
Keepalive-таймеры ограничивают детект мёртвого peer'а величиной idle + interval * count секунд —
4 минуты на дефолтах вместо примерно двух часов, которые дают системные tcp_keepalive_*.
Тюнинг таймеров требует per-socket keepalive-опций (Linux и macOS); на платформе без них proxy
пишет одну запись в лог на listener и откатывается к системным таймерам, сохраняя обычный
SO_KEEPALIVE.
По SIGTERM shutdown hook останавливает primary listener и затем ждёт, пока main-поток закроет по
порядку дополнительные frontend listener'ы, management listener, backend-ресурсы router'а и
front-door security. JVM выполняет halt сразу после возврата последнего shutdown hook, поэтому
именно это ожидание не даёт оборвать in-flight запросы дополнительных фронтендов и оставить
незакрытыми backend-ресурсы. Ожидание ограничено server.shutdown-timeout-seconds; если
остановка не уложилась в лимит, proxy пишет WARN и всё равно отпускает JVM.
Остановленный Thrift-listener закрывает чтение на всех принятых клиентских соединениях: поток,
ждущий следующего запроса на простаивающем keep-alive соединении (HiveServer2 держит такое на
каждую сессию), получает EOF и сразу освобождается, а запрос в процессе выполнения дорабатывает и
отдаёт ответ. Клиент видит закрытое соединение и переподключается к другому экземпляру. Под
Kerberos Hive при остановке логирует на уровне ERROR InterruptedException received for ExpiredTokenRemover thread — это штатное прерывание потока очистки токенов; поставляемый
log4j.properties отбрасывает это сообщение, со своей конфигурацией логирования его можно
игнорировать.
Proxy проверяет properties-файл на старте и отказывается стартовать на значении, которое не может интерпретировать. Proxy, который запустился и тихо делает противоположное написанному в файле, хуже, чем proxy, который не стартовал.
Неизвестные и ошибочные ключи (Unknown properties): proxy отслеживает все прочитанные ключи и отказывается запускаться, если в конфигурационном файле обнаружены неизвестные параметры либо опечатки (например, server.ports вместо server.port или catalog.main.access_mode). Сообщение об ошибке содержит подсказку наиболее близкого известного ключа («did you mean 'server.port'?»). Строгую проверку можно переключить в режим предупреждений в логе через config.strict-validation=false (по умолчанию true). Открытые пространства параметров Hadoop/Hive (backend.conf.*, catalog.<name>.conf.*, security.front-door-conf.*, catalog.<name>.expose-table-patterns.*, rate-limit.source-cidr.*) принимаются без ограничений.
Логирование эффективной конфигурации: сразу после успешной загрузки proxy выводит в лог на уровне INFO полный структурированный дамп примененной конфигурации (все слушатели, пулы потоков, таймауты сокетов, TCP keepalive, параметры каждого каталога, resilience-кэши, rate limiting, Ranger и guard-фильтры). Все чувствительные данные (пароли к БД, секретные S3-ключи, токены и credentials в HiveConf и front-door-conf) автоматически маскируются (******).
Boolean-значения принимают только true и false в любом регистре. yes, on, 1 и
опечатки вроде ture — ошибка старта с указанием ключа и значения. Раньше они читались как
false, то есть молча выключали имперсонацию, management-листенер или hedged reads.
Enum-значения — все ключи режимов, профилей и политик — регистронезависимы, поэтому
access-mode=read_only и frontend-profile=apache_3_1_3 принимаются везде. При неизвестном
значении сообщение содержит ключ и список допустимых констант.
Длительности в HiveConf-ключах вроде hive.metastore.client.socket.timeout соответствуют
Hive: целое число с необязательным суффиксом ns, us, ms, s/sec, m/min, h/hour,
d/day (длинные написания вроде 600sec, 5min тоже работают). Число без суффикса означает
секунды. Значение, которое отверг бы и сам Hive (например 1.5s), логируется как WARN с
указанием значения и применённого дефолта.
Противоречивые комбинации дают ошибку старта, а не тихую подмену поведения:
| Комбинация | Результат |
|---|---|
catalog.<name>.write-db-whitelist без access-mode=READ_WRITE_DB_WHITELIST |
ошибка: whitelist игнорировался бы, запись оставалась бы разрешена во все базы |
access-mode=READ_WRITE_DB_WHITELIST с пустым whitelist |
ошибка: для полного запрета записи есть READ_ONLY |
synthetic-read-lock.store.mode=IN_MEMORY вместе с любым ключом synthetic-read-lock.store.zookeeper.* |
ошибка: локи молча остались бы в памяти |
Два листенера на одном host:port |
ошибка, в том числе через wildcard bind host: 0.0.0.0:9083 конфликтует с 127.0.0.1:9083 |
Проверка конфликтов охватывает основной листенер, management-листенер (включая дефолтный
server.port + 1000) и каждую запись additional-frontends.<name>. Хосты сравниваются как
заданы, без DNS-резолва, поэтому localhost против 127.0.0.1 по-прежнему проявится при bind, а
не на валидации.
Proxy может поднимать легковесный HTTP listener для health checks, readiness и Prometheus
метрик. По умолчанию listener выключен и автоматически включается, если задан management.port:
management.bind-host=0.0.0.0
management.port=19083Либо его можно включить явно:
management.enabled=true
# Необязательно; по умолчанию берётся server.bind-host
management.bind-host=0.0.0.0
# Необязательно; по умолчанию server.port + 1000
management.port=19083
# Необязательно; число handler-потоков management listener, по умолчанию 4
management.threads=4
# Необязательно; сколько мс переиспользуются результаты readiness probe;
# по умолчанию 2000, значение 0 отключает кэш
management.readiness-cache-ms=2000Быстрые проверки:
curl -s http://127.0.0.1:19083/healthz
curl -s http://127.0.0.1:19083/readyz
curl -s http://127.0.0.1:19083/metricsListener обслуживает запросы из небольшого выделенного пула потоков (management.threads), поэтому
медленный /readyz не блокирует /healthz и /metrics. Держи как минимум два потока.
У management endpoints нет ни аутентификации, ни авторизации. /readyz отдаёт
Kerberos-принципалы и per-backend lastError, в котором обычно видны внутренние hostname. Привязывай
management.bind-host к интерфейсу, доступному только из сети мониторинга и оркестрации, либо
ограничивай порт firewall-правилами или network policy. Не публикуй его рядом с клиентским
Thrift-портом.
Доступные endpoints:
/healthzвозвращает liveness процесса и uptime/readyzпроверяет backend connectivity, отдаёт per-backend состояниеconnected/degraded, а также включает Kerberos login status и TGT freshness для front-door и outbound backend credentials/metricsотдаёт Prometheus text format
Proxy поддерживает обновление конфигурации без простоя сервиса:
- Автоматически по таймеру: параметр
config.reload.poll-interval-seconds(по умолчанию5секунд) отслеживает изменение mtime файла конфигурации. Установите0, чтобы отключить фоновый опрос. - По сигналу SIGHUP: отправьте сигнал процессу (
kill -HUP <pid>).
Безопасность перезагрузки (Safe Reload): если в обновленном файле обнаружена синтаксическая ошибка, неизвестные параметры или конфликт пространств имен, ошибка логируется в журнал, а сервис продолжает бесперебойно работать на предыдущей конфигурации. Запомненное время изменения битого файла обновляется, поэтому фоновый опрос не разбирает его заново каждые несколько секунд: следующая попытка будет после следующего изменения файла.
Что применяется без рестарта. Перезагрузка пересобирает маршрутизатор каталогов (CatalogRouter), политики экспозиции (FederationLayer) и routing-слой и инвалидирует кэши списков и метаданных баз, таблиц и разделов. Компоненты пересобираются по очереди, поэтому запрос, пришедший в момент перезагрузки, может увидеть часть новых и часть старых настроек. Применяются:
- у каталогов:
access-mode,write-db-whitelist,expose-mode,expose-db-patterns,expose-table-patterns.*,unprefixed-databases,merge-with-default; - в
federation.*: всё, кромеfederation.external-table-drop-purge.mode.
Всё остальное — набор каталогов, адреса, профили и пулы бэкендов, impersonation, security.*, Ranger, rate limits, routing.*, листенеры, REST, management — собирается один раз при старте. Если такие настройки в новом файле отличаются от тех, с которыми процесс запущен, перезагрузка всё равно применяет перечисленное выше, а в журнал пишется WARN со списком несовпадающих компонентов (например, catalog.hdp.hiveConf, latencyRouting): они вступят в силу только после рестарта. Если такую правку откатить, предупреждение пропадает.
/healthz предназначен для простых liveness checks и отвечает только на вопрос, жив ли сам
процесс proxy.
/readyz предназначен для load balancer, orchestration probes и operational diagnostics. В ответе есть:
- общий статус readiness
probeAgeMs— возраст данных probe, по которым построен ответ- summary по backend connectivity
- по каждому backend поля
connected,degraded,lastSuccessEpochSecond,lastFailureEpochSecond,lastProbeEpochSecond,lastLatencyMs,latencyEwmaMs,baselineTimeoutMs,adaptiveTimeoutMs,latencyBudgetMs,circuitState,consecutiveFailures,circuitRetryAtEpochMsиlastError - Kerberos status для front door и outbound backend credentials, включая
state(DISABLED,PENDING,ACTIVE,STALE,FAILED),loggedInиhealthy - TGT freshness через
tgtExpiresAtEpochSecondиsecondsUntilExpiry, когда эти данные доступны
Kerberos status читается по уже используемым proxy login'ам. Kerberos probe не переустанавливает
process-wide security configuration и не делает собственный keytab login, поэтому не конкурирует с
живыми SASL handshake и не добавляет трафика к KDC. При этом on-demand connectivity probes выше
по-прежнему открывают backend-сессию на каждый scrape, а она при backend SASL делает keytab login;
чтобы убрать это с пути запроса, включите routing.backend-state-polling.enabled=true.
Readiness падает только тогда, когда front-door login user отсутствует или вообще не имеет
Kerberos credentials. Истёкший TGT отдаётся как state=STALE с secondsUntilExpiry<=0 и не роняет
readiness: Hadoop не обновляет TGT keytab-логина сам по себе, а SASL acceptor продолжает
аутентифицировать клиентов по service keys из keytab. Backend Kerberos - только диагностика:
PENDING до открытия первой backend-сессии, STALE когда последний зафиксированный backend login
непригоден; каждая backend-сессия делает собственный keytab login, поэтому реальное здоровье
backend'ов показывают connectivity probes. Так как probe больше не логинится, ротацию или отзыв
keytab сам /readyz не обнаруживает - это проявится через отказы клиентской аутентификации или
backend connectivity.
Если включён routing.backend-state-polling.enabled=true, readiness отражает результат последних
фоновых probe. Иначе /readyz сам делает on-demand probe backend'ов и возвращает те же поля.
Дорогая часть /readyz — это backend- и Kerberos-probe, поэтому их результаты переиспользуются в
течение management.readiness-cache-ms (по умолчанию 2s) и обновляются не более чем одним запросом
одновременно: параллельные вызовы получают предыдущий результат, а не порождают новый fanout
сетевых проверок. Per-backend поля состояния всегда рендерятся из актуального in-memory runtime
state, а probeAgeMs показывает, насколько устарели данные probe. Чтобы опрашивать бэкенды на каждый
запрос, задайте management.readiness-cache-ms=0.
Текущие Prometheus метрики:
hms_proxy_uphms_proxy_start_time_secondshms_proxy_backend_up{backend}hms_proxy_backend_last_success_timestamp_seconds{backend}hms_proxy_backend_last_probe_timestamp_seconds{backend}hms_proxy_backend_circuit_state{backend,state}hms_proxy_requests_total{method,catalog,backend,status}hms_proxy_request_duration_seconds{method,catalog,backend}hms_proxy_backend_failures_total{backend,exception}hms_proxy_backend_fallback_total{method,from_api,to_api}hms_proxy_catalog_fallback_total{primary_catalog,fallback_catalog,method}hms_proxy_routing_ambiguous_totalhms_proxy_default_catalog_routed_total{method}hms_proxy_lock_request_split_total{catalog}hms_proxy_filtered_objects_total{method,catalog,object_type}hms_proxy_synthetic_read_lock_events_total{operation,catalog,store_mode,result}hms_proxy_synthetic_read_lock_store_failures_total{operation,store_mode,exception}hms_proxy_synthetic_read_lock_handoffs_total{operation,catalog,store_mode}hms_proxy_synthetic_read_locks_active{store_mode}hms_proxy_synthetic_read_lock_store_info{store_mode}hms_proxy_backend_session_acquire_timeouts_total{catalog,operation}hms_proxy_adaptive_timeout_reconnect_total{catalog}hms_proxy_adaptive_timeout_reconnect_skipped_total{catalog,reason}hms_proxy_iceberg_pointer_guard_events_total{catalog,outcome}hms_proxy_rest_requests_total{prefix,route,status}hms_proxy_rest_request_duration_seconds{prefix,route}hms_proxy_rest_listener_info{bind_host,port}hms_proxy_cache_requests_total{cache,catalog,result}hms_proxy_cache_entries{cache,catalog}hms_proxy_cache_invalidations_total{cache,catalog,reason}hms_proxy_cache_refreshes_total{cache,status}hms_proxy_cache_invalidation_events_published_total{target}hms_proxy_cache_invalidation_events_received_total{target}hms_proxy_cache_invalidation_errors_total{action}hms_proxy_ranger_evaluations_total{catalog,resource_type,access_type,result}hms_proxy_ranger_evaluation_duration_seconds{catalog,resource_type}hms_proxy_ranger_filtered_objects_total{catalog,resource_type}hms_proxy_ranger_plugin_info{catalog,service_name,service_type,app_id}hms_proxy_jvm_memory_used_bytes{area}hms_proxy_jvm_memory_committed_bytes{area}hms_proxy_jvm_memory_max_bytes{area}hms_proxy_jvm_memory_pool_used_bytes{pool,area}hms_proxy_jvm_memory_pool_committed_bytes{pool,area}hms_proxy_jvm_memory_pool_max_bytes{pool,area}hms_proxy_jvm_buffer_pool_used_bytes{pool}hms_proxy_jvm_buffer_pool_total_capacity_bytes{pool}hms_proxy_jvm_buffer_pool_count{pool}
Пример Prometheus scrape config:
scrape_configs:
- job_name: hms-proxy
static_configs:
- targets:
- hms-proxy-01.example.com:19083Семантика метрик:
statusвhms_proxy_requests_totalпринимает значенияok,error,fallbackилиthrottledcatalog=all, backend=fanoutозначает, что запрос был отправлен сразу в несколько backendhms_proxy_backend_failures_totalсчитает backend-side ошибки вызова с группировкой по backend и exception typehms_proxy_backend_fallback_totalсчитает compatibility fallback, которые proxy вернул после backend failureshms_proxy_routing_ambiguous_totalсчитает запросы, которые proxy безопасно отклонил из-за conflicting namespace hints вместо угадывания маршрутаhms_proxy_default_catalog_routed_totalсчитает запросы, которые ушли в default catalog из-за отсутствия явного catalog namespacehms_proxy_lock_request_split_totalсчитает lock-запросы, назвавшие несколько каталогов и ушедшие в один из них, — компоненты остальных каталогов из запроса к backend удаленыhms_proxy_rate_limited_totalсчитает запросы, отклонённые overload protection, с лейблами по limiting dimension, configured scope, method family и resolved cataloghms_proxy_filtered_objects_totalсчитает базы или таблицы, скрытые selective federation rules до возврата клиентуhms_proxy_synthetic_read_lock_events_totalотражает lifecycle synthetic lock shim:acquire,check_lock,heartbeat,unlock,release_txn,cleanuphms_proxy_synthetic_read_lock_store_failures_totalсчитает ошибки in-memory или ZooKeeper store с группировкой по операции и exception typehms_proxy_synthetic_read_lock_handoffs_totalсчитает случаи, когда synthetic lock, открытый через один proxy instance, продолжает обслуживаться через другой instancehms_proxy_synthetic_read_locks_activeпоказывает текущее число synthetic lock, видимых из выбранного store backend; значение правится инкрементально на каждом acquire/release и ресинхронизируется со store фоновым expiry sweep (раз в 30s), поэтому lock-операции не платят за листинг всего storehms_proxy_synthetic_read_lock_store_infoэто constant-info gauge, который помечает, работает ли proxy сin_memoryилиzookeeperstorage для synthetic lockhms_proxy_backend_session_acquire_timeouts_totalсчитает fail-fast события, когда пул shared backend metastore session исчерпан и permit не освобождается заlatencyBudgetMsкаталога (или 30s по умолчанию);operation=borrowдля обычной диспетчеризации RPC,operation=reconnectдля административных переподключений, которым не удалось перевести пул в состояние покоя (дождаться освобождения активных сессий)hms_proxy_adaptive_timeout_reconnect_totalсчитает, сколько раз adaptive socket timeout приводил к reconnect shared backend client (с принудительным сбросом impersonation-кэша); полезен для отслеживания reconnect storm при нестабильной latencyhms_proxy_adaptive_timeout_reconnect_skipped_totalсчитает adaptive-timeout правки, подавленные троттлингом (reason=hysteresisдля дельт ниже порога,reason=cooldownдля срабатываний раньше cooldown окна после предыдущего reconnect)hms_proxy_iceberg_pointer_guard_events_totalсчитает решения Iceberg pointer guard поalter_table:repaired(запрос стёр бы Iceberg-состояние записи, поэтому был слит поверх неё),forward_commit(Iceberg-коммит, построенный на текущем указателе, пропущен как есть),not_iceberg(в записи нет указателя, имя запомнено как не-Iceberg),cache_suppressed(ответ из этой памяти, без чтения бэкенда),read_failed(запись прочитать не удалось, alter пропущен как есть). Всё, кромеcache_suppressed, стоило одногоget_table, поэтому по одной метрике видно и добавленные round trip, и hit rate кэшаhms_proxy_rest_requests_totalсчитает HTTP-запросы Iceberg REST с группировкой по catalog prefix, route и terminal HTTP-статусуhms_proxy_rest_request_duration_secondsизмеряет длительность запросов Iceberg REST с группировкой по catalog prefix и routehms_proxy_rest_listener_infoэто constant-info gauge, который показывает настроенные bind host и port Iceberg REST listener'аhms_proxy_cache_requests_totalсчитает обращения к кэшу метаданных с группировкой поcache(database_list,database_metadata),catalogиresult(hit,miss)hms_proxy_cache_entriesотслеживает текущее число записей в кэшах метаданных с группировкой поcacheиcataloghms_proxy_cache_invalidations_totalсчитает инвалидации записей кэша с группировкой поcache,catalogиreason(write,catalog,prune,all)hms_proxy_cache_refreshes_totalсчитает фоновые обновления кэшей метаданных и сбросы дискового кэша групп с группировкой поcache(database_list,database_metadata,group_disk) иstatus(success,error)hms_proxy_cache_invalidation_events_published_totalиhms_proxy_cache_invalidation_events_received_totalотслеживают публикацию и получение событий распределенной инвалидации кэшей через ZooKeeper с группировкой поtarget(table,database,catalog,all)hms_proxy_cache_invalidation_errors_totalсчитает ошибки распределенной инвалидации кэшей через ZooKeeper с группировкой поaction(publish,receive,purge)hms_proxy_ranger_evaluations_totalсчитает проверки политик Apache Ranger с группировкой поcatalog,resource_type(database,table),access_type(select,read,use,show) иresult(allowed,denied)hms_proxy_ranger_evaluation_duration_secondsизмеряет длительность вычисления политик Ranger в секундах с группировкой поcatalogиresource_typehms_proxy_ranger_filtered_objects_totalсчитает базы и таблицы, скрытые фильтрами Ranger при multi-object listing с группировкой поcatalogиresource_typehms_proxy_ranger_plugin_infoэто info gauge с конфигурацией активных плагинов Ranger (catalog,service_name,service_type,app_id)hms_proxy_jvm_memory_used_bytes,hms_proxy_jvm_memory_committed_bytesиhms_proxy_jvm_memory_max_bytesотражают использование памяти JVM в байтах по области памятиarea(heapилиnonheap)hms_proxy_jvm_memory_pool_used_bytes,hms_proxy_jvm_memory_pool_committed_bytesиhms_proxy_jvm_memory_pool_max_bytesотслеживают память по отдельным пулам памяти JVM (pool,area)hms_proxy_jvm_buffer_pool_used_bytes,hms_proxy_jvm_buffer_pool_total_capacity_bytesиhms_proxy_jvm_buffer_pool_countотображают использование пулов NIO-буферов (pool:directилиmapped)hms_proxy_upвсегда равна1, пока процесс отдаёт/metrics; мёртвый или зависший процесс виден какup == 0у самого Prometheus.hms_proxy_start_time_seconds— время старта процесса, по нему видны рестартыhms_proxy_backend_upравна1, если бэкенд проходит ту же проверку, что и в/readyz: подключён, неdegraded(флаг держится до следующего успешного вызова или probe) и circuit неOPEN.hms_proxy_backend_last_success_timestamp_seconds— время последнего успешного вызова или probe,hms_proxy_backend_last_probe_timestamp_seconds— время последнего фонового probe (0, пока ни одного не было),hms_proxy_backend_circuit_state— one-hot состояние circuit breaker (closed,open,half_open). Все четыре берутся из того же runtime state, что и/readyz, в момент опроса/metrics
Живость бэкендов независимо от клиентского трафика даёт только фоновый опрос
routing.backend-state-polling.enabled=true: каждые interval-ms прокси открывает к каждому бэкенду
отдельную сессию и вызывает getStatus. Без него hms_proxy_backend_up меняется только по
результатам клиентских запросов: на старте бэкенд считается подключённым, а без трафика значение не
обновляется. Фоновое обновление кэшей баз (routing.database-*-cache.background-refresh.*) для этого
не подходит: оно обновляет только записи, к которым недавно обращались клиенты.
Отказ подключиться к бэкенду (недоступный хост, отказ в соединении) считается сбоем связи: бэкенд
получает connected=false, сбой учитывается circuit breaker'ом, и /readyz показывает
backendConnectivity: false. Проба, не вернувшаяся за интервал, не запускается повторно, пока не
завершится: клиент метастора не прерывает ретраи подключения по отмене, и вторая проба того же
бэкенда заняла бы ещё один поток. Такой бэкенд отмечается как недоступный (previous probe still running), а пробы остальных бэкендов не ждут в очереди. При
routing.backend-state-polling.max-parallelism меньше числа каталогов очередь всё равно возможна:
пул проб не больше этого значения.
Пример алертов (при interval-ms=10000):
- alert: HmsProxyDown
expr: up{job="hms-proxy"} == 0
for: 1m
- alert: HmsProxyBackendDown
expr: hms_proxy_backend_up == 0
for: 1m
# Фоновый probe перестал выполняться: зависший цикл опроса или выключенный polling.
- alert: HmsProxyBackendProbeStale
expr: time() - hms_proxy_backend_last_probe_timestamp_seconds > 60
for: 1mНесмотря на исторические имена метрик synthetic_read_lock, этот shim теперь также обслуживает
допустимые non-transactional NO_TXN DDL lock и non-transactional write lock на non-default
catalog.
В комплектный Grafana dashboard monitoring/grafana/hms-proxy-dashboard.json добавлены панели по
synthetic lock activity, handoff, store failures и active lock counts, а также template variable
store_mode для быстрого переключения между режимами in_memory и zookeeper.
Proxy также пишет один structured audit log на каждый запрос через logger
io.github.mmalykhin.hmsproxy.audit. Каждая запись представляет собой single-line JSON с полями
requestId, method, operationClass, catalog, backend, status, durationMs,
remoteAddress, authenticatedUser, routed, fanout, fallback и
defaultCatalogRouted.
Значения, которые приходят от клиента (authenticatedUser, remoteAddress), экранируются по
правилам JSON, включая все управляющие символы ниже 0x20, поэтому враждебный principal или адрес
не сломает разбор записи в log collector. Не-ASCII символы выводятся как есть, в UTF-8.
Пример:
{"event":"hms_proxy_audit","requestId":42,"method":"get_table","operationClass":"metadata_read","catalog":"catalog1","backend":"catalog1","status":"ok","durationMs":8,"routed":true,"fanout":false,"fallback":false,"defaultCatalogRouted":false,"remoteAddress":"10.20.30.40","authenticatedUser":"alice@EXAMPLE.COM"}В комплектном log4j.properties этот logger направлен в собственный appender
logs/hms-proxy-audit.log (rolling по 100MB, 10 backup), без префикса layout — чтобы файл оставался
валидным JSON lines, и с выключенной additivity — чтобы audit-записи не смешивались с общим логом.
Чтобы писать в другое место или отправлять записи в свой log pipeline, переопредели appender
auditFile в собственном log4j.properties. Если приглушить logger ниже INFO, запись вообще не
строится — ничего не вычисляется ради вывода, который никто не читает.
Готовый Grafana dashboard лежит в
monitoring/grafana/hms-proxy-dashboard.json. В нём уже есть панели по request rate, latency,
backend failures, fallbacks, default-catalog routing и ambiguous routing, ряд Iceberg
REST (request rate, error ratio и квантили латентности REST-listener'а, разбивки по HTTP-статусу,
catalog prefix и route, и stat «listener up»), а также отдельная секция JVM Memory:
использование heap-памяти, предел и процент утилизации, non-heap память, детальная разбивка
по пулам памяти и NIO buffer pools (direct и mapped буферы).
Можно публиковать только часть namespace backend-каталога, не меняя routing и write-policy. Это удобно для постепенной миграции, безопасного multi-tenant rollout и снижения риска случайного раскрытия метаданных.
На каталог:
# Обратная совместимость по умолчанию
catalog.catalog1.expose-mode=ALLOW_ALL
# Более безопасный rollout: видны только объекты, попавшие в allowlist
catalog.catalog1.expose-mode=DENY_BY_DEFAULT
catalog.catalog1.expose-db-patterns=sales,finance_.*
catalog.catalog1.expose-table-patterns.sales=orders_.*,events
catalog.catalog1.expose-table-patterns.finance_.*=audit_.*Правила:
- regex сопоставляются case-insensitively
- матчинг идёт по backend names, а не по externalized proxy names
- сопоставление делается по всей строке:
salesсовпадает только сsales, аsales_.*совпадёт сsales_eu catalog.<name>.expose-db-patternsзадаёт allowlist для backend database names внутри каталогаcatalog.<name>.expose-table-patterns.<dbRegex>задаёт allowlist для таблиц внутри баз, чьё backend db name совпало с<dbRegex>- table rules сужают видимость таблиц внутри совпавших баз; unmatched tables отфильтровываются
- при
DENY_BY_DEFAULTбазы без совпавшего db rule или table rule скрываются - совпавший table rule может сделать базу видимой даже без db-level rule, но внутри неё будут видны только совпавшие таблицы
Фильтр применяется на metadata read-path’ах вроде get_all_databases, get_databases,
get_table*, get_tables*, get_table_meta и Hortonworks get_tables_ext.
Поведение по типу API:
- list-style RPC вроде
get_all_databases,get_all_tables,get_tables,get_table_metaиget_tables_extмолча убирают скрытые объекты из ответа - direct lookup вроде
get_database,get_tableиget_table_reqвозвращает "not found", если целевой объект отфильтрован hms_proxy_filtered_objects_total{method,catalog,object_type}считает и скрытые базы, и скрытые таблицы
Примеры:
# 1. Во время миграции публикуем только одну базу
catalog.catalog1.expose-mode=DENY_BY_DEFAULT
catalog.catalog1.expose-db-patterns=sales
# 2. Публикуем одну базу, но только часть таблиц внутри неё
catalog.catalog1.expose-mode=DENY_BY_DEFAULT
catalog.catalog1.expose-table-patterns.sales=orders_.*,customers
# 3. Каталог по умолчанию открыт, но одну чувствительную базу сужаем
catalog.catalog1.expose-mode=ALLOW_ALL
catalog.catalog1.expose-table-patterns.audit=.*_publicЧто это означает:
- в примере 1 видна только backend db
sales - в примере 2 backend db
salesстановится видимой, но внутри неё доступны только таблицы, совпавшие сorders_.*, плюсcustomers - в примере 3 все базы остаются видимыми, но в backend db
auditвозвращаются только таблицы, совпавшие с.*_public
Для non-default catalog помните, что фильтры всё равно матчятся по backend db names вроде sales,
а не по внешним именам вроде catalog2__sales.
Proxy можно настроить так, чтобы он защищал создание и изменение managed-таблиц, если во входящих метаданных таблица помечена как транзакционная (transactional):
transactional=true- любое непустое значение
transactional_properties
Правило применяется ко всем RPC create_table* и alter_table*, включая
create_table_with_environment_context (именно этот вызов HiveMetaStoreClient реально
отправляет для createTable), create_table_with_constraints и alter_table_with_cascade.
Обёртки *_req из front door Hive 4 и Hortonworks разворачиваются в эти RPC до срабатывания
guard, поэтому они тоже покрыты. Оно срабатывает только для MANAGED_TABLE. External-таблицы
остаются без изменений.
Режим reject:
guard.transactional-ddl.mode=REJECT_TRANSACTIONALРежим rewrite:
guard.transactional-ddl.mode=REWRITE_TRANSACTIONAL_TO_EXTERNALВ режиме rewrite proxy переписывает входящую таблицу в EXTERNAL_TABLE, добавляет
external.table.purge=true и удаляет transactional и transactional_properties.
Если нужен ещё и физический delete файлов на backend Apache 3.1.3, это надо комбинировать с
federation.external-table-drop-purge.mode=BEST_EFFORT и per-catalog allowlist через
catalog.<name>.conf.hms.proxy.external-table-drop-purge.allowed-prefixes.
Также можно ограничить его конкретными IP-адресами или CIDR-масками:
guard.transactional-ddl.mode=REJECT_TRANSACTIONAL
guard.transactional-ddl.client-addresses=10.10.0.15,10.20.0.0/16,2001:db8::/64Если guard.transactional-ddl.client-addresses задан, проверка применяется только к совпавшим
клиентам. Если не задан, проверка действует для всех клиентов.
Proxy также умеет отсеивать bursts до того, как они превратятся в backend overload. Эта защита работает локально на каждом proxy instance и использует token-bucket лимиты с:
- постоянной скоростью пополнения через
requests-per-second - опциональным allowance для короткого burst через
burst - независимыми bucket, так что запрос должен пройти все настроенные scope, которые к нему относятся
Поддерживаемые scope:
- per authenticated client principal:
rate-limit.principal.* - per exact source IP:
rate-limit.source.* - per source CIDR rule:
rate-limit.source-cidr.<name>.* - per HMS method family:
rate-limit.method-family.<family>.* - per logical catalog:
rate-limit.catalog.<catalog>.* - per high-risk RPC class:
rate-limit.rpc-class.<class>.*
Поддерживаемые method families:
metadata_readmetadata_writeservice_global_readservice_global_writeacid_namespace_bound_writeacid_id_bound_lifecycleadmin_introspectioncompatibility_only_rpc
Поддерживаемые RPC classes:
writeddltxnlock
Важное поведение:
- per-principal лимиты срабатывают только когда front door даёт authenticated user, например через Kerberos/SASL
rate-limit.source-cidr.<name>это aggregate bucket на всё правило CIDR, а не отдельный bucket на каждый IP- если один source IP совпал сразу с несколькими CIDR rule, применяются все совпавшие rule
- per-catalog лимиты срабатывают в момент, когда запрос реально resolve'ится или касается конкретного catalog/backend; значит fanout read может расходовать несколько catalog bucket
- один RPC может одновременно попасть в несколько classes, например lock/txn write может считаться сразу в
write,txnиlock - при превышении лимита клиент получает
MetaException, а запрос записывается соstatus="throttled"
Пример:
# Per authenticated principal
rate-limit.principal.requests-per-second=60
rate-limit.principal.burst=120
# Per exact source IP
rate-limit.source.requests-per-second=30
rate-limit.source.burst=60
# Aggregate bucket на целую подсеть или клиентский pool
rate-limit.source-cidr.hs2-pool.cidrs=10.10.0.0/16,10.20.0.0/16
rate-limit.source-cidr.hs2-pool.requests-per-second=200
rate-limit.source-cidr.hs2-pool.burst=300
# Shaping по семейству методов
rate-limit.method-family.metadata_read.requests-per-second=600
rate-limit.method-family.metadata_read.burst=1000
# Per catalog
rate-limit.catalog.catalog1.requests-per-second=300
rate-limit.catalog.catalog2.requests-per-second=120
# Дополнительная защита для high-risk RPC classes
rate-limit.rpc-class.write.requests-per-second=80
rate-limit.rpc-class.ddl.requests-per-second=15
rate-limit.rpc-class.txn.requests-per-second=30
rate-limit.rpc-class.lock.requests-per-second=50Нормальная production starting point:
- используй
principalпротив runaway HS2 session или плохих end-user - используй
sourceиsource-cidrпротив tooling, scanner и больших client pool - используй
method-family.metadata_read, чтобы ограничить scan-heavy metadata discovery - используй
catalog.<name>, чтобы один hot catalog не выедал остальные - для
ddl,txnиlockобычно стоит держать лимиты строже, чем для обычных metadata read
Можно выбрать, какую версию HMS proxy объявляет наружу:
compatibility.frontend-profile=APACHE_3_1_3или для Hortonworks клиентов:
compatibility.frontend-profile=HORTONWORKS_3_1_0_3_1_0_78или для клиентов HDP 3.1.0.3.1.5.6150-1:
compatibility.frontend-profile=HORTONWORKS_3_1_0_3_1_5_6150_1В Thrift-протоколе нет version-negotiation handshake, поэтому proxy не может по первому
запросу определить версию клиента (так же как HiveServer2 разделяет binary и HTTP по портам).
Когда нужны два разных типа клиентов с разными getVersion() identity или разными runtime
jars — поднимаем несколько Thrift listener'ов на разных портах:
server.port=9083
compatibility.frontend-profile=APACHE_3_1_3
additional-frontends=hdp
additional-frontends.hdp.port=9084
additional-frontends.hdp.frontend-profile=HORTONWORKS_3_1_0_3_1_5_6150_1
additional-frontends.hdp.standalone-metastore-jar=hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jarPrimary listener живёт на server.port со своим compatibility.frontend-profile. Каждый
блок additional-frontends.<name>.* задаёт независимый listener:
| Ключ | Обязательный | По умолчанию |
|---|---|---|
additional-frontends.<name>.port |
да | — |
additional-frontends.<name>.frontend-profile |
да | — |
additional-frontends.<name>.standalone-metastore-jar |
да для не-APACHE_3_1_3 |
— |
additional-frontends.<name>.bind-host |
нет | server.bind-host |
additional-frontends.<name>.min-worker-threads |
нет | server.min-worker-threads |
additional-frontends.<name>.max-worker-threads |
нет | server.max-worker-threads |
additional-frontends.<name>.client-socket-timeout-ms |
нет | server.client-socket-timeout-ms |
additional-frontends.<name>.tcp-keepalive |
нет | server.tcp-keepalive |
additional-frontends.<name>.tcp-keepalive-idle-seconds |
нет | server.tcp-keepalive-idle-seconds |
additional-frontends.<name>.tcp-keepalive-interval-seconds |
нет | server.tcp-keepalive-interval-seconds |
additional-frontends.<name>.tcp-keepalive-count |
нет | server.tcp-keepalive-count |
Все listener'ы используют один RoutingMetaStoreProxy, federation, security
(FrontDoorSecurity включая SASL/Kerberos), audit и Prometheus метрики. Отличается
только wire-level Thrift API на конкретном порту.
Дополнительные listener'ы работают на daemon-потоках и останавливаются раньше, чем закрываются router и front-door security, поэтому авария при старте одного listener'а не оставит JVM живой с занятым портом. См. Жизненный цикл front-door сокетов и shutdown.
У каждого listener должен быть собственный host:port. Старт падает, если additional frontend
конфликтует с основным listener, с другим additional frontend или с management-листенером —
включая дефолтный server.port + 1000, — а wildcard bind host вроде 0.0.0.0 считается
конфликтом с любым хостом на том же порту.
Для полноценного Hortonworks frontend нужно указать HDP standalone-metastore jar:
compatibility.frontend-profile=HORTONWORKS_3_1_0_3_1_0_78
compatibility.frontend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.0.0-78.jarДля HDP 3.1.0.3.1.5.6150-1 используй соответствующий jar:
compatibility.frontend-profile=HORTONWORKS_3_1_0_3_1_5_6150_1
compatibility.frontend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jarДля isolated Hortonworks backend runtime можно указать backend jar:
compatibility.backend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.0.0-78.jarили:
compatibility.backend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jarBackend runtime задаётся явно по каталогу. Если catalog.<name>.runtime-profile не указан, для
этого каталога используется APACHE_3_1_3:
catalog.hdp.runtime-profile=HORTONWORKS_3_1_0_3_1_0_78
catalog.hdp.backend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.0.0-78.jar
catalog.apache.runtime-profile=APACHE_3_1_3Для HDP 3.1.0.3.1.5.6150-1 профиль и jar задаются аналогично:
catalog.hdp.runtime-profile=HORTONWORKS_3_1_0_3_1_5_6150_1
catalog.hdp.backend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jarС этим JAR-файлом прокси может запустить выбранный бэкенд Hortonworks в изолированном
загрузчике классов (isolated classloader). Профиль рантайма не определяется автоматически
по версии бэкенд-сервера, а задаётся явно через catalog.<name>.runtime-profile.
Для front door прокси поднимает Thrift-процессор Hortonworks в изолированном загрузчике классов
и автоматически транслирует общие RPC во внутренний обработчик Apache 3.1.3. Поддерживаемые
HDP-only методы:
get_database_req->get_databaseдля HDP3.1.0.3.1.5.6150-1create_table_req->create_table/create_table_with_environment_context/create_table_with_constraintstruncate_table_req->truncate_tablealter_table_req->alter_table/alter_table_with_environment_contextalter_partitions_req->alter_partitions/alter_partitions_with_environment_contextrename_partition_req->rename_partitionget_partitions_by_names_req->get_partitions_by_namesupdate_table_column_statistics_req->set_aggr_stats_forupdate_partition_column_statistics_req->set_aggr_stats_foradd_write_notification_log-> прямой Hortonworks passthrough только в Hortonworks backendget_tables_ext-> прямой Hortonworks passthrough только в Hortonworks backend3.1.0.3.1.5.6150-1get_all_materialized_view_objects_for_rewriting-> прямой Hortonworks passthrough только в Hortonworks backend3.1.0.3.1.5.6150-1черезrouting.default-catalog
Замечания по view / materialized view:
- переписывание SQL включено по умолчанию (
federation.view-text-rewrite.mode=REWRITE) - переписывание намеренно выполнено без использования тяжеловесного парсера (parser-less): лексический сканер
отслеживает позиции таблиц (
FROM,JOIN,INTO,TABLE,UPDATE) и переписывает квалификатор базы данных ссылки, стоящей в такой позиции, либо квалифицирует неквалифицированную таблицу именем внешней БД; полная грамматика Hive SQL при этом не разбирается - CTE в блоке
WITH ... AS (...), string literals, комментарии--и/* */, числа и идентификаторы в backquote пропускаются, поэтому их содержимое никогда не переписывается; квалификаторы колонок и алиасы таблиц (t.colвselect t.col from sales.orders t) не трогаются, даже если совпадают с именем БД - ссылки вида
catalog.db.tableсохраняют catalog-префикс: на выходе схлопывается только<backend catalog>.<db>.<table>в имя внешней БД, а любой другой catalog-префикс остаётся нетронутым, а не переписывается молча на другой namespace - на входе внутреннее имя получают только ссылки каталога самого view; межкаталожная (cross-catalog) ссылка вида
catalog2__dim.table_xсохраняется в бэкенде во внешнем виде и возвращается клиенту без изменений, поэтому такой view читается через прокси, но не клиентом, который ходит в бэкенд напрямую - всё, что нельзя разрешить однозначно, остаётся нетронутым и логируется на уровне
DEBUGвViewDefinitionCompatibility; непереписанная ссылка проявится как явная ошибка backend, а не как молча испорченное определение вью - по умолчанию переписываются и
viewExpandedText, и клиентскийviewOriginalText(federation.view-text-rewrite.preserve-original-text=false), а также параметры Spark-представлений (view.default.database), чтобы движки вроде Spark корректно находили таблицы с префиксом прокси; задайтеtrue, если необходимо сохранять исходный оригинальный SQL без изменений - диалектные конструкции (подстановки вроде
${hiveconf:db}, макросы, engine-specific hints) вне области rewrite — их по-прежнему стоит проверить отдельным smoke тестом в вашей среде
- request-based ACID методы с routable namespace в payload, например
get_valid_write_ids,allocate_table_write_ids,compact,compact2,add_dynamic_partitions,fire_listener_event,repl_tbl_writeid_state, маршрутизируются по payload - id-only lifecycle методы, например
open_txns,commit_txn,abort_txn,abort_txns,check_lock,unlock,heartbeat,heartbeat_txn_range, привязаны кrouting.default-catalog - это осознанная модель: proxy не пытается быть distributed ACID coordinator между несколькими backend metastore
Комплектный log4j.properties — это конфигурация по умолчанию; proxy подхватывает её в runtime,
если appender'ы не сконфигурированы, так что обычный запуск java -jar ... тоже пишет логи.
Дефолты:
- root logger на
INFO, вывод в stderr и вlogs/hms-proxy.log(rolling по 50MB, 10 backup) - пакет proxy
io.github.mmalykhin.hmsproxyнаINFO - audit logger
io.github.mmalykhin.hmsproxy.auditнаINFOв отдельныйlogs/hms-proxy-audit.log
Любую часть можно переопределить своим log4j.properties на classpath.
Binding — slf4j-reload4j. reload4j это drop-in форк EOL-версии log4j 1.2.17 с вырезанными
известными CVE, поэтому формат конфигурации остаётся log4j 1.x и существующие log4j.properties
продолжают работать без изменений. Единственное ограничение: то, что из reload4j убрали —
org.apache.log4j.jmx.*, viewer lf5 и NTEventLogAppender, — в своей конфигурации больше
использовать нельзя.
Per-request debug tracing по умолчанию выключен: он рендерит через DebugLogUtil все аргументы
запроса и все backend-ответы, а это реальные CPU и аллокации на каждый RPC. Включай его осознанно —
для конкретного окружения или на время инцидента:
log4j.logger.io.github.mmalykhin.hmsproxy=DEBUGКогда он включён, каждый клиентский вызов получает requestId, а в логах есть:
- входящий HMS method и аргументы
- выбранный backend catalog
- proxied thrift method и переписанные аргументы
- backend response или backend error
- итоговый client response или client-visible error
Рендер значений ограничен (10 элементов на коллекцию, глубина 3, ~4000 символов на запись), но на
нагруженном proxy объём всё равно заметный. По окончании возвращай уровень на INFO.
Учти, что на INFO для пакета proxy остаются trace-строки write-пути
trace stage=client-request / backend-request, поэтому для большинства операционных разборов
DEBUG не нужен.
Укажи в HiveServer2 hive.metastore.uris на proxy вместо одного backend HMS.
Для multi-catalog deployment лучше использовать Hive/клиентов, которые сохраняют catalog fields.
Для Beeline/HS2 обычно удобнее separator __, чем .:
routing.catalog-db-separator=__Это меняет только внешний legacy формат имени из канонической модели маршрутизации выше.
При использовании JDBC-драйверов HiveServer2 графические клиенты запрашивают метаданные баз и таблиц через DatabaseMetaData.getTables(catalog, schemaPattern, tableNamePattern, ...).
Внутренний механизм HiveServer2 (MetadataOperation.convertSchemaPattern) преобразует SQL LIKE шаблоны схемы в регулярные выражения, заменяя все неэкранированные подчёркивания _ на точки .. Из-за этого при каноническом разделителе __ имя схемы remote__default превращается в запрос по шаблону remote..default (а если каталог содержит подчёркивания, например remote_hdp__sales — в remote.hdp..sales).
Прокси прозрачно обрабатывает эту особенность:
CatalogRouterсопоставляет префиксы схемы как в литеральном виде (catalog__), так и в транслированном виде HiveServer2 (catalog.replace('_', '.') + '..').- Запросы
get_table_metaиget_databasesс шаблонами видаcatalog..*илиcatalog..dbточно маршрутизируются на соответствующий backend-каталог с очищенным от префикса шаблоном (например,dbили*). - Возвращаемые клиенту объекты (
TableMeta, имена баз данных) динамически экстернализируются с каноническим разделителем (catalog__db), поэтому в дереве объектов DBeaver и Hue корректно отображаются все базы и таблицы удалённых каталогов.
Если metadata writes через proxy ведут себя не так, как напрямую против backend HMS, можно попробовать:
federation.preserve-backend-catalog-name=trueЭто меняет только возвращаемые catName/catalogName, обычно на backend-значения вроде hive.
Выбор backend по-прежнему следует канонической модели маршрутизации выше.
Если нагрузки используют Hive views или materialized views между несколькими catalog, переписывание SQL внутри view включено по умолчанию:
federation.view-text-rewrite.mode=REWRITE
federation.view-text-rewrite.preserve-original-text=falseЭто переписывает SQL внутри view между внешними и внутренними именами, включая viewOriginalText и
Spark-параметры view.default.database. Если нужно выключить переписывание, установи federation.view-text-rewrite.mode=DISABLED,
а для сохранения сырого viewOriginalText — federation.view-text-rewrite.preserve-original-text=true. На выбор backend для самого RPC ни
одна из настроек не влияет.
Если нужно, чтобы proxy физически удалял данные external table на backend Apache 3.1.3
после DROP TABLE, включи:
federation.external-table-drop-purge.mode=BEST_EFFORT
catalog.catalog2.conf.hms.proxy.external-table-drop-purge.allowed-prefixes=hdfs://ns-catalog2/tmp/,hdfs://ns-catalog2/data/external/Сейчас этот хук работает только для drop_table и drop_table_with_environment_context,
только для backend runtime APACHE_3_1_3, только если таблица уже EXTERNAL_TABLE с
external.table.purge=true, и только если квалифицированный LOCATION попадает в allowlist.
Удаление данных происходит только после успешного удаления метаданных в backend metastore;
ошибка purge логируется и не откатывает metadata delete. Allowlist лучше держать узким, потому
что подходящие пути удаляются рекурсивно через Hadoop FileSystem.
Рекурсивное удаление выполняется в небольшом фоновом пуле (потоки с именами
hms-proxy-drop-purge-*), поэтому drop_table отвечает сразу после удаления метаданных и не
держит Thrift worker всё время удаления. Значит, успешный DROP TABLE ещё не гарантирует, что
данные уже удалены — результат purge фиксируется в логе proxy. При остановке proxy запущенные purge
дожидаются завершения; оставшиеся в очереди логируются и пропускаются.
При работе клиентов в мультикластерной среде (например, когда клиент настроен на hdfs://frontend-ns или указывает относительные пути /data/...) прокси умеет автоматически нормализовать локации внешних таблиц и разделов под HDFS целевого каталога:
# Автоматическая квалификация путей без схемы (e.g. /data/ext -> hdfs://backend-ns/data/ext)
federation.external-table-location-rewrite.mode=QUALIFY_UNQUALIFIED
# Или замена source-кластера на backend-кластер (e.g. hdfs://frontend-ns/data -> hdfs://backend-ns/data)
# federation.external-table-location-rewrite.mode=REWRITE_IF_SOURCE_DEFAULT_FS
# federation.external-table-location-rewrite.source-default-fs=hdfs://frontend-nsРежимы federation.external-table-location-rewrite.mode:
DISABLED(по умолчанию): путиsd.locationпередаются в бэкенд без изменений.QUALIFY_UNQUALIFIED: неквалифицированные пути (без схемы и authority) автоматически дополняются схемой и authority целевого каталога (fs.defaultFSизcatalog.<name>.conf.fs.defaultFS).REWRITE_IF_SOURCE_DEFAULT_FS: если входящий путь совпадает сfederation.external-table-location-rewrite.source-default-fs(по умолчаниюfs.defaultFSкаталога по умолчанию), authority заменяется наfs.defaultFSцелевого каталога.
Переписывание применяется к объектам Table и Partition во всех мутирующих RPC (create_table*, alter_table*, add_partition*, alter_partition*, alter_partitions*).
Proxy дополнительно умеет поднять параллельный HTTP listener со спецификацией
Iceberg REST Catalog, использующий тот же routing/federation pipeline что и
Thrift HMS front door. Статус: экспериментально; вся write-поверхность,
которую выставляет RESTCatalogAdapter, — write таблиц (create, commit, drop,
rename, register), write view (create, commit, drop, rename) и namespace DDL
(create, update properties, drop), а также multi-table transaction commit —
поддержана, но только когда целевой namespace резолвится в
routing.default-catalog. Iceberg-клиенты (PyIceberg, Spark iceberg-rest,
Trino iceberg-rest) могут discover и load Iceberg-таблицы, хранящиеся в HMS
через стандартный параметр metadata_location.
Write таблиц и gate «только default-каталог» появились первыми; write view и
multi-table transaction commit к этому моменту уже были достижимы — их
REST-путь идёт через тот же общий RoutingHiveCatalog/RoutingMetaStoreClient,
которым пользуется write таблиц, и WriteRouteGate уже классифицировал все
тринадцать роутов как write — их просто ещё не объявляли в GET /v1/config и
не покрывали smoke. Namespace DDL — по-настоящему новое: RoutingMetaStoreClient
не реализовывал createDatabase, alterDatabase и dropDatabase до сих пор,
поэтому POST /v1/{prefix}/namespaces и соседние роуты отвечали
UnsupportedOperationException независимо от каталога.
Почему writes работают только в default-каталоге: реальным HMS-локом
подкреплены только таблицы дефолтного каталога (см. ZooKeeper storage для
synthetic read locks);
любой другой каталог обслуживается синтетическим lock shim, который выдаёт
EXCLUSIVE-лок безусловно, без проверки конфликтов. Commit, направленный
туда, решил бы, что владеет таблицей, молча гоняясь наперегонки с — и,
возможно, проигрывая — конкурентным writer'ом, что портит metadata.json без
единого сообщения о конфликте. WriteRouteGate проверяет это на
резолвленном каталоге, а не на prefix из URL запроса: prefix дефолтного
каталога тоже выставляет базы всех остальных каталогов под federated-именами
<catalog><separator><db> (см. Поддерживаемые endpoint'ы
ниже), так что create по
/v1/{default-prefix}/namespaces/apache__default/tables отказывается точно
так же, как прямой create по /v1/apache/namespaces/default/tables — оба
резолвятся в каталог apache и получают один и тот же 403
(ForbiddenException). Это касается всех тринадцати write-роутов из таблицы
ниже одинаково — write таблиц, view и namespace DDL, transaction commit.
GET /v1/config и GET /v1/{prefix}/config объявляют эту асимметрию
напрямую: в endpoints дефолтного каталога перечислены все тринадцать
write-роутов из таблицы ниже, у любого другого каталога — только девять
read-роутов, так что спецификация-совместимый клиент узнаёт об ограничении
из discovery, а не из проваленного запроса.
Включается так:
rest-catalog.enabled=true
rest-catalog.port=9183
# Опционально, но рекомендуется для прода: SPNEGO. Требует security.mode=KERBEROS.
rest-catalog.kerberos.principal=HTTP/_HOST@EXAMPLE.COM
rest-catalog.kerberos.keytab=/etc/security/keytabs/spnego.service.keytab
# Опционально: ограничить то, что вправе удалить purge (см. раздел про purge ниже).
rest-catalog.purge.mode=ALLOWLIST
rest-catalog.purge.allowed-prefixes=hdfs://ns-default/warehouse/tablespace/rest-catalog.purge.allowed-prefixes обязателен для ALLOWLIST и отвергается
при двух других режимах; обе противоречивые комбинации роняют прокси на старте,
а не игнорируются.
Тело запроса разбирается в память целиком, по одному на рабочий поток, поэтому его размер
ограничен rest-catalog.max-request-body-bytes (дефолт 16777216, 16 MiB). Тело больше лимита
отклоняется с 413 до разбора JSON: по заголовку Content-Length, а при chunked-передаче — как
только прочитано больше лимита.
Iceberg на каждом коммите пишет один из двух storage descriptor'ов. С включённым
Hive-движком — storage_handler и конкретные
HiveIcebergInputFormat/OutputFormat/SerDe; с выключенным — абстрактные
FileInputFormat/FileOutputFormat/LazySimpleSerDe, а storage_handler
удаляет. Выбор определяется свойством самой таблицы engine.hive.enabled, а
если таблица его не задаёт — значением iceberg.engine.hive.enabled в
конфигурации того движка, который коммитит.
Таблица, созданная через STORED BY ICEBERG в Hive 4, такого свойства не несёт,
поэтому один-единственный коммит мог оставить её нечитаемой для клиентов Hive
3.1: они падают с Cannot create an instance of InputFormat class org.apache.hadoop.mapred.FileInputFormat. Этому мешают две настройки, по одной
на каждый путь записи, обе включены по умолчанию:
| Ключ | Какой путь закрывает |
|---|---|
rest-catalog.hive-engine-descriptor |
Собственные REST-коммиты прокси: они пишут Hive-совместимый дескриптор. Таблица, задавшая engine.hive.enabled сама, всегда перевешивает эту настройку, а явный catalog.<name>.conf.iceberg.engine.hive.enabled уважается, а не перетирается. |
routing.iceberg-pointer-guard.hive-engine-descriptor |
Коммиты, приходящие по Thrift от движка, который прокси настроить не может, — например INSERT от Hive 3.1. Pointer guard сохраняет дескриптор, уже лежащий в записи метастора, вместо того чтобы дать запросу его обнулить. |
Guard дескриптор только сохраняет и никогда не навязывает: запись без
storage_handler — это таблица, которой нечего терять, и её alter'ы проходят
нетронутыми. Таблица, испорченная до появления этих настроек, чинится сама на
следующем же коммите.
Запросы к этому listener'у покрыты Prometheus-метриками из раздела
Prometheus-метрики: hms_proxy_rest_requests_total,
hms_proxy_rest_request_duration_seconds и hms_proxy_rest_listener_info.
| Endpoint | Статус |
|---|---|
GET /v1/config |
поддержан |
GET /v1/{prefix}/config |
поддержан |
GET /v1/{prefix}/namespaces |
поддержан |
GET /v1/{prefix}/namespaces/{ns} |
поддержан |
GET /v1/{prefix}/namespaces/{ns}/tables |
поддержан (только Iceberg-таблицы) |
GET /v1/{prefix}/namespaces/{ns}/tables/{tbl} |
поддержан (только Iceberg-таблицы) |
GET /v1/{prefix}/namespaces/{ns}/views |
поддержан (реальный листинг; пустой, если Iceberg-view нет) |
GET /v1/{prefix}/namespaces/{ns}/views/{view} |
поддержан (только Iceberg-view) |
HEAD /v1/{prefix}/namespaces/{ns} |
поддержан (204, если существует, 404 — если нет) |
HEAD /v1/{prefix}/namespaces/{ns}/tables/{tbl} |
поддержан (204, если существует, 404 — если нет) |
HEAD /v1/{prefix}/namespaces/{ns}/views/{view} |
поддержан (204, если существует, 404 — если нет) |
POST /v1/{prefix}/namespaces/{ns}/tables |
поддержан только для дефолтного каталога (create); иначе 403 |
POST /v1/{prefix}/namespaces/{ns}/tables/{tbl} |
поддержан только для дефолтного каталога (commit/update); иначе 403 |
DELETE /v1/{prefix}/namespaces/{ns}/tables/{tbl} |
поддержан только для дефолтного каталога (drop, включая ?purgeRequested=true); иначе 403 |
POST /v1/{prefix}/tables/rename |
поддержан только для дефолтного каталога (rename); иначе 403 |
POST /v1/{prefix}/namespaces/{ns}/register |
поддержан только для дефолтного каталога (register); иначе 403 |
POST /v1/{prefix}/namespaces/{ns}/views |
поддержан только для дефолтного каталога (create); иначе 403 |
POST /v1/{prefix}/namespaces/{ns}/views/{view} |
поддержан только для дефолтного каталога (commit/update); иначе 403 |
DELETE /v1/{prefix}/namespaces/{ns}/views/{view} |
поддержан только для дефолтного каталога (drop); иначе 403 |
POST /v1/{prefix}/views/rename |
поддержан только для дефолтного каталога (rename); иначе 403 |
POST /v1/{prefix}/namespaces |
поддержан только для дефолтного каталога (create); иначе 403 |
POST /v1/{prefix}/namespaces/{ns}/properties |
поддержан только для дефолтного каталога (update properties); иначе 403 |
DELETE /v1/{prefix}/namespaces/{ns} |
поддержан только для дефолтного каталога (drop); иначе 403 |
POST /v1/{prefix}/transactions/commit |
поддержан только для дефолтного каталога (multi-table commit); иначе 403 |
{prefix} — любой каталог, перечисленный в catalogs=: каждый настроенный
каталог получает собственный REST prefix, /v1/<catalog>/.... GET /v1/config поддерживает warehouse discovery: передайте ?warehouse=<catalog>,
и в ответе overrides.prefix назовёт этот каталог, так что клиент может
привязаться к нужному prefix, не зашивая его в конфигурацию (см. примеры
клиентов ниже). Без warehouse /v1/config объявляет routing.default-catalog
— как и в phase 1; неизвестное значение warehouse возвращает HTTP 400
(BadRequestException). Поле endpoints в ответе перечисляет девять
read-роутов из таблицы выше для любого каталога (list/load namespace +
namespace-exists, list/load table + table-exists, list/load view +
view-exists); у дефолтного каталога endpoints дополнительно несёт все
тринадцать write-роутов из таблицы выше (write таблиц, view и namespace DDL,
transaction commit), так что современный клиент может обнаружить
write/read-асимметрию между каталогами через discovery, а не из
провалившегося запроса. GET /v1/{prefix}/config отвечает так же, но из
собственного handler'а прокси — overrides.prefix называет каталог из
сегмента пути, а не из query-параметра warehouse, — и неизвестный prefix
здесь по-прежнему даёт 404.
Отказ в write отвечает 403 (ForbiddenException) с сообщением, называющим
резолвленный каталог, например: Writes are only supported in the default catalog 'hdp'; namespace 'apache__default' belongs to catalog 'apache', which is served by the synthetic lock shim and provides no writer isolation. Это проверяется на namespace, в который запрос резолвится, а
не на prefix из URL — см. Почему writes работают только в
default-каталоге выше.
DELETE .../tables/{tbl}?purgeRequested=true обслуживается как настоящий
purge — так же, как его обслуживает собственный REST-каталог Iceberg: прокси
дропает таблицу в metastore, а затем сам удаляет её data- и metadata-файлы,
обходя манифесты таблицы, чтобы их найти. Удаление идёт внутри JVM прокси под
его собственными credentials и завершается до отправки 204. Write-гейт
удерживает purge внутри дефолтного каталога: purge, чей namespace резолвится в
другой каталог, отказывается тем же 403, что и любой другой write, до того как
будет тронут хоть один файл.
Что именно purge вправе удалить внутри этого каталога, ограничивает
rest-catalog.purge.mode:
| Режим | Поведение |
|---|---|
ALLOW (по умолчанию) |
Удаляет всё, на что указывают метаданные и манифесты таблицы, — так же, как REST-каталоги Iceberg. Поведение прежних версий не меняется. |
ALLOWLIST |
Purge разрешён только под префиксами rest-catalog.purge.allowed-prefixes. |
REFUSE |
Отвечает 403 на любой purge; DELETE без purgeRequested по-прежнему дропает таблицу и оставляет её файлы. |
Purge, который политика не разрешает, отклоняется до дропа таблицы, поэтому
после отказа целы и таблица, и её файлы, а клиент может повторить запрос без
purgeRequested.
ALLOWLIST проверяет границу дважды. До дропа с префиксами сверяются location
таблицы и её metadata.json; выход за границу любого из них даёт 403. Во
время удаления каждый путь, который Iceberg просит удалить — data-файлы,
delete-файлы, манифесты, manifest list, metadata-файлы, — сверяется снова, и
путь вне префиксов пропускается с WARN вместо удаления. Именно вторая проверка
важна для недоверенных клиентов: в REST-протоколе манифесты пишет клиент,
поэтому коммит может указать снапшот на файлы в чужом дереве, а эти пути
становятся известны только при обходе манифестов — уже после того, как
предварительная проверка прошла.
Error-ответы несут смапленный HTTP-статус, type и message, но никогда —
server stack trace, потому что этот listener может быть доступен без
аутентификации. Тело запроса, которое не удаётся распарсить, отвечает 400
(BadRequestException) вместо падения в 500; это касается любого роута,
принимающего тело. HEAD-ответ никогда не пишет тело, как того требует RFC
9110, — включая error-статусы, — так что exists-check на отсутствующий
namespace, таблицу или view возвращает обычный 404 без тела, а не 404 с
JSON-телом. Диспетчер запросов ловит Throwable, а не только Exception:
любой Error, который ускользнул бы из обработки (например,
NoSuchMethodError от несовпадения версий зависимостей, всплывший глубоко
внутри write), теперь маппится в обычный error-ответ, а не улетает мимо
handler'а, оставляя соединение клиента висеть без ответа навсегда.
Prefix дефолтного каталога сохраняет federated-представление из phase 1: его
собственные базы плюс базы всех остальных каталогов под именами
<catalog><separator><db> (см. HiveServer2 выше). Любой
другой prefix — чистое представление: только собственные базы этого
каталога, под их внутренними именами — federated-имена
<catalog><separator><db> в non-default prefix не просачиваются. На
неизвестный prefix по-прежнему отвечает 404 (NoSuchCatalogException).
SPNEGO по RFC требует principal вида HTTP/<host>@REALM. Это отдельный
principal от security.server-principal (обычно hms/<host>@REALM для
Thrift listener). Оба могут лежать в одном keytab или в двух разных. REST
listener делает UserGroupInformation.loginUserFromKeytabAndReturnUGI, чтобы
получить отдельный UGI и не перезаписать Thrift'овый — оба сосуществуют в
одной JVM.
PyIceberg:
from pyiceberg.catalog.rest import RestCatalog
catalog = RestCatalog("my-catalog", **{
"uri": "http://hms-proxy:9183",
})Spark:
spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.my_catalog.catalog-impl=org.apache.iceberg.rest.RESTCatalog
spark.sql.catalog.my_catalog.uri=http://hms-proxy:9183Чтобы обратиться к non-default каталогу, передайте warehouse=<catalog>:
клиент отправит его в GET /v1/config при discovery и привяжется к prefix
этого каталога для всех последующих запросов:
catalog = RestCatalog("sales-catalog", **{
"uri": "http://hms-proxy:9183",
"warehouse": "sales",
})spark.sql.catalog.sales_catalog.warehouse=sales- Iceberg REST по дизайну работает только с Iceberg-таблицами. Native
Hive-таблицы (parquet/orc/text без
metadata_location) HiveCatalog отфильтровывает, и через REST их не видно. Для native Hive продолжайте использовать Thrift listener. RoutingHiveCatalogиспользует reflection на private полеHiveCatalog.clients, привязанное к Iceberg1.9.2. При апгрейде Iceberg обязательно прогнатьRoutingHiveCatalogTest, чтобы убедиться, что inject ещё работает.- Write таблицы открывает output stream в HDFS (
metadata.json) прямо из JVM прокси — это первый путь в прокси, который так делает; чтение идёт по другому, незатронутому classpath. Это требует, чтобыhadoop-hdfsиhadoop-commonбыли одной версии в дереве зависимостей. Мавеновская медиация никогда их не сравнивала (это разные artifact ID), поэтомуorc-core(приходит транзитивно черезhive-standalone-metastore) тянул устаревшийhadoop-hdfs:2.2.0рядом сhadoop-common:2.6.0в другом месте дерева, и каждый write падал сNoSuchMethodError: FSOutputSummer.<init>глубоко внутриDFSOutputStream.pom.xmlтеперь исключает этот транзитивныйhadoop-hdfsи напрямую зависит отhadoop-hdfs:2.6.0, чтобы совпасть сhadoop-common. Держите обе версии согласованными, если переопределяете любую из них. - Purge читает манифесты таблицы через Avro.
iceberg-core:1.9.2собран противavro:1.12.0, аhadoop-mapreduce-client-core:2.6.5тянетavro:1.7.4на той же глубине дерева — Maven выбирал 1.7.4 по порядку объявления, и любой purge падал сNoClassDefFoundError: org/apache/avro/LogicalTypes.pom.xmlпинитavro:1.12.0; больше никто на этом classpath не вызывает Avro API. Кодеки snappy, xz и zstd объявлены в Avro как optional и в fat jar намеренно не попадают: манифест всегда пишется с deflate, который Avro сжимает штатным JDK-шным Deflater. Свойствоwrite.avro.compression-codecэтого не меняет — оно управляет data-файлами и delete-файлами, которые прокси не читает, аManifestWriterв Iceberg вообще не передаёт свойства таблицы в writer манифеста. Это закреплено тестом (dropTableWithPurgeReadsManifestsOfATableAskingForSnappy): если будущий Iceberg начнёт учитывать свойство для манифестов, упадёт тест, а не прод.
server.port=9083
security.mode=NONE
catalogs=warehouse
catalog.warehouse.conf.hive.metastore.uris=thrift://hms-backend:9083
routing.default-catalog=warehouseБезопасность делится на две независимые части: front door (клиенты -> proxy) и backend connections (proxy -> HMS).
Front door:
security.mode=KERBEROS
security.server-principal=hive/_HOST@REALM.COM
security.keytab=/etc/security/keytabs/hms-proxy.keytab
security.client-principal=hive/_HOST@REALM.COM
security.client-keytab=/etc/security/keytabs/hms-proxy-client.keytabsecurity.server-principal и security.keytab обязательны при security.mode=KERBEROS.
_HOST разворачивается в каноническое имя хоста proxy перед Kerberos login. Если DNS-имя
хоста не совпадает с principal в keytab/KDC, используй явный FQDN.
Когда Kerberos включён на фронте, delegation-token методы
(get_delegation_token, renew_delegation_token, cancel_delegation_token)
обслуживаются локально самим proxy.
hadoop.proxyuser.<service>.* не относится к подключению к ZooKeeper. Эта настройка нужна только
в сценарии, когда service principal вроде HiveServer2 просит proxy выдать delegation token для
конечного пользователя через get_delegation_token("alice", ...).
Пример:
security.front-door-conf.hadoop.proxyuser.hive.hosts=hs2-1.example.com,hs2-2.example.com
security.front-door-conf.hadoop.proxyuser.hive.groups=*Для persistent storage delegation tokens можно включить ZooKeeperTokenStore через обычный
HiveConf или напрямую в hms-proxy.properties:
security.front-door-conf.hive.cluster.delegation.token.store.class=org.apache.hadoop.hive.metastore.security.ZooKeeperTokenStore
security.front-door-conf.hive.cluster.delegation.token.store.zookeeper.connectString=zk1:2181,zk2:2181,zk3:2181
security.front-door-conf.hive.cluster.delegation.token.store.zookeeper.znode=/hms-proxy-delegation-tokens
# Опционально: ACL для новых znode, которые создаёт token store.
# security.front-door-conf.hive.cluster.delegation.token.store.zookeeper.acl=sasl:hive:cdrwa
# Опционально: максимальный lifetime токена в миллисекундах.
# security.front-door-conf.hive.cluster.delegation.token.max-lifetime=604800000Если при этом включён security.mode=KERBEROS, proxy автоматически прокинет
hive.metastore.kerberos.principal и hive.metastore.kerberos.keytab.file в front-door HiveConf
из security.server-principal и security.keytab, чтобы встроенный ZooKeeperTokenStore
аутентифицировался в ZooKeeper по SASL/Kerberos. Если для ZooKeeper нужны отдельные credentials,
задай эти hive.metastore.kerberos.* явно через security.front-door-conf.*.
То есть по умолчанию для подключения к ZooKeeper используются:
- principal:
security.server-principal - keytab:
security.keytab
Это именно front-door credentials proxy. Они не берутся из
security.client-principal и security.client-keytab, потому что те параметры относятся к
исходящим подключениям proxy к backend HMS.
При старте proxy также заранее настраивает JAAS entry HiveZooKeeperClient для front-door
token store из этих же credentials до запуска локального delegation-token manager. Обычно это
означает, что отдельный -Djava.security.auth.login.config только для ZooKeeper не нужен.
Если для ZooKeeper нужен другой principal/keytab, их можно переопределить явно:
security.front-door-conf.hive.metastore.kerberos.principal=hive-zk/_HOST@REALM.COM
security.front-door-conf.hive.metastore.kerberos.keytab.file=/etc/security/keytabs/hms-proxy-zk.keytabУ proxy есть узкий synthetic lock shim для non-default catalog. Он покрывает non-ACID
SHARED_READ SELECT lock, допустимые non-transactional NO_TXN DDL lock вроде
CREATE TABLE и partition rename/drop, которые Hive всё равно ведёт через txn/lock API, и
non-transactional write lock (INSERT, UPDATE, DELETE) — те самые, которые Hive берёт на
INSERT в non-ACID таблицу non-default каталога. Такие lock proxy обслуживает локально, когда
backend txn id рассинхронизированы между каталогами. Это не превращает proxy в distributed ACID
coordinator.
Write lock не ограничены по типу лока: при дефолтном hive.txn.strict.locking.mode=true Hive
берёт на INSERT в non-ACID таблицу EXCLUSIVE lock, SHARED_WRITE — только при выключенном
strict locking, а INSERT OVERWRITE всегда EXCLUSIVE. Компонент с isTransactional=true
никогда не обслуживается синтетически: он относится к ACID-таблице, write id для которой умеет
выдавать только TxnHandler default-каталога, поэтому запрос уходит в backend и падает там. ACID
таблиц в non-default каталоге всё равно быть не может — proxy отклоняет create_table с
transactional=true вне default-каталога и не пропускает allocate_table_write_ids /
get_valid_write_ids для non-default каталогов.
Shim выдаёт локи, не проверяя их конфликты. Он сразу отвечает ACQUIRED и не смотрит на
другие живые локи, поэтому параллельные INSERT, INSERT OVERWRITE и DDL по одной таблице
non-default каталога не сериализуются друг относительно друга — как и клиенты, которые ходят в
этот metastore мимо proxy. То есть non-default каталог не даёт ни ACID-гарантий, ни изоляции
писателей; если нагрузке нужно взаимное исключение, держите её в routing.default-catalog или
сериализуйте писателей сами. Access mode каталога при этом соблюдается: write lock для
READ_ONLY каталога или для базы вне catalog.<name>.write-db-whitelist отклоняется
MetaException, а не синтезируется.
Пригодность определяется по всем LockComponent запроса, а не только по первому. Вызов lock
берётся, маршрутизируется и подтверждается целиком и возвращает один lock id, поэтому его нельзя
переслать больше чем в один backend. Но запрос, читающий несколько каталогов — или всего лишь две
базы одного каталога, — приходит именно в таком виде: одним запросом, компоненты которого
принадлежат разным namespace. Proxy расщепляет его: компоненты одного каталога уходят в его
metastore, каждый переписанный в свою backend-базу, а компоненты остальных каталогов из запроса к
backend удаляются.
Целью маршрутизации выбирается default catalog, если он присутствует: ему принадлежит TxnHandler,
и его локи — настоящие. Отброшенные компоненты при этом не теряют ничего, что было бы удержано:
non-default каталог обслуживается описанным выше shim, который записывает лок и сразу отвечает
ACQUIRED, ни разу не проверив конфликт. Отброшенный компонент теряет запись в этом журнале, а не
гарантию. Проверка access mode за решением о маршрутизации не следует — запись в READ_ONLY
каталог отклоняется независимо от того, пережил ли её компонент расщепление. Каждое расщепление
пишется в лог и считается метрикой hms_proxy_lock_request_split_total.
Единственное исключение — служебный плейсхолдер Hive (_dummy_database._dummy_table,
константы SemanticAnalyzer.DUMMY_DATABASE / DUMMY_TABLE): INSERT ... VALUES и запросы
без секции FROM берут блокировку на нём вместе с реальной целевой таблицей, поэтому такой
запрос формально называет две базы. Этого объекта нет ни в одном метасторе, и блокировать на нём
нечего. Прокси игнорирует его и при выборе пространства имён, и при проверке пригодности для shim —
его компоненты не выбирают каталог, не считаются вторым пространством имён и не переписываются в
реальную базу бэкенда. Запрос, состоящий исключительно из таких компонентов, направляется в
дефолтный каталог (routing.default-catalog), которому принадлежит TxnHandler.
synthetic-read-lock.store.mode обязан быть задан явно — default'а нет. Используйте IN_MEMORY
для одиночного инстанса proxy (synthetic-локи на non-default каталогах теряются при рестарте или
failover через load balancer, и proxy пишет WARN в лог на старте, чтобы это было видно), либо
ZOOKEEPER для HA / load-balanced deployment, чтобы check_lock, unlock, heartbeat,
commit_txn и abort_txn продолжали работать через соседний proxy после падения первого.
mode=IN_MEMORY вместе с любым заданным synthetic-read-lock.store.zookeeper.* — ошибка старта:
ZooKeeper-настройки были бы проигнорированы, а локи молча остались бы в памяти. Обратное
направление сохранено: ZooKeeper-ключи без явного mode включают ZOOKEEPER.
Пример:
synthetic-read-lock.store.mode=ZOOKEEPER
synthetic-read-lock.store.zookeeper.connect-string=zk1:2181,zk2:2181,zk3:2181
synthetic-read-lock.store.zookeeper.znode=/hms-proxy-synthetic-read-locks
# synthetic-read-lock.store.zookeeper.connection-timeout-ms=15000
# synthetic-read-lock.store.zookeeper.session-timeout-ms=60000
# synthetic-read-lock.store.zookeeper.base-sleep-ms=1000
# synthetic-read-lock.store.zookeeper.max-retries=3Если включён security.mode=KERBEROS, synthetic read-lock store по умолчанию использует те же
security.server-principal и security.keytab для ZooKeeper SASL/Kerberos, что и front door,
по той же модели, что и delegation-token store выше.
Store держит под настроенным znode два поддерева:
/hms-proxy-synthetic-read-locks/locks/lock-<sequence> сериализованное состояние lock, ключ - lock id
/hms-proxy-synthetic-read-locks/txns/<txnId>/<lockId> пустая нода-указатель, ключ - транзакция
check_lock, unlock и heartbeat приходят только с lock id, поэтому состояние lock обязано
храниться с ключом по lock id; поддерево txns - вторичный индекс, который позволяет commit_txn
и abort_txn освободить локи транзакции одним getChildren вместо скана всех живых lock. Коммит
транзакции, которая не брала synthetic lock, стоит один getChildren с ответом NoNode.
Истёкшие lock убирает фоновый sweep раз в 30s (никогда не в потоке клиентского запроса); он же
удаляет индексные записи, у которых пропала lock-нода, и пустые per-transaction каталоги. Lock
всегда пишется раньше своей индексной записи, поэтому недоделанный create не может спрятать lock
от commit_txn.
Rolling upgrade: у lock, записанных proxy старее этого layout, индексной записи нет, поэтому новый
инстанс не освободит их по commit_txn/abort_txn - вместо этого они истекут через sweep за
metastore.txn.timeout. Synthetic lock ничего не блокирует на backend, так что вся цена - znode,
живущий чуть дольше. Ручная миграция и downtime не нужны, старые и новые инстансы могут работать
против одного znode одновременно.
Таймаут lock берётся из metastore.txn.timeout default-каталога и понимает каноническую форму Hive
с суффиксом (600s, 10m); голое число по-прежнему читается как секунды. Нераспознанное значение
откатывается к 300s и логируется как WARN на старте.
Если хочешь, чтобы backend HMS вызовы выполнялись от имени аутентифицированного пользователя, а не от service principal proxy:
security.impersonation-enabled=trueИли только для конкретных backend:
# Дефолт для каталогов, у которых нет собственной настройки.
security.impersonation-enabled=false
catalog.catalog1.impersonation-enabled=true
catalog.catalog2.impersonation-enabled=falseГлобальный ключ работает именно как дефолт catalog.<name>.impersonation-enabled: в рантайме
имперсонацию включает per-catalog флаг, который наследует глобальное значение, если не задан явно.
При включённой имперсонации прокси открывает соединения с бэкендом HMS под собственным клиентским
Kerberos-принципалом (security.client-principal с keytab) и ведёт пул пользовательских сессий
(ImpersonationClient). На защищённых Kerberos/SASL бэкендах Hive Metastore использует TUGIAssumingProcessor
(который игнорирует set_ugi()); в этом режиме прокси автоматически получает бэкенд delegation token
(get_delegation_token) от имени вызывающего пользователя и открывает сессию через SASL DIGEST-MD5.
Метастор выполняет вызовы под createProxyUser(caller, loginUser).doAs(...), благодаря чему операции в HDFS
(такие как wh.mkdirs при создании таблиц) выполняются от имени конечного пользователя, а не hive:hadoop.
На незащищённых бэкендах или в режиме fallback при инициализации сессии по-прежнему отправляется
set_ugi(user, groups) (TUGIBasedProcessor).
Кроме того, когда клиент (HiveServer2, Spark и др.) вызывает set_ugi(user, groups) на listener прокси,
прокси привязывает указанную пользовательскую идентичность к клиентскому соединению (транспорту). Все
последующие операции этого соединения обслуживаются от имени конечного пользователя.
Это требует:
security.mode=KERBEROSна фронте- proxy-user impersonation rules на backend HMS и NameNode для
security.client-principal(hadoop.proxyuser.<user>.hosts=*,hadoop.proxyuser.<user>.groups=*)
Если backend HMS настроен на Kerberos/SASL:
catalog.catalog1.conf.hive.metastore.sasl.enabled=true
catalog.catalog1.conf.hive.metastore.kerberos.principal=hive/_HOST@REALM.COMКогда для любого backend включён hive.metastore.sasl.enabled=true, proxy открывает outbound HMS
соединения под security.client-principal и security.client-keytab.
Для каждого каталога можно отдельно ограничить write RPC:
catalog.catalog1.access-mode=READ_ONLY
catalog.catalog2.access-mode=READ_WRITE_DB_WHITELIST
catalog.catalog2.write-db-whitelist=sales,analyticsДля каждого каталога можно отдельно задать latency budget для latency-aware routing слоя:
catalog.catalog1.latency-budget-ms=1500
catalog.catalog2.latency-budget-ms=5000И независимо от write-policy можно ограничить видимость метаданных:
catalog.catalog1.expose-mode=DENY_BY_DEFAULT
catalog.catalog1.expose-db-patterns=sales
catalog.catalog1.expose-table-patterns.sales=orders_.*,eventsПоддерживаются режимы:
READ_WRITE: поведение по умолчаниюREAD_ONLY: для каталога разрешены только read RPCREAD_WRITE_DB_WHITELIST: write RPC разрешены только для баз изcatalog.<name>.write-db-whitelist
access-mode и write-db-whitelist задаются только вместе. Whitelist при любом другом
access-mode, как и READ_WRITE_DB_WHITELIST без whitelist, — ошибка старта, а не каталог, который
молча разрешает любую запись.
Режимы selective exposure:
ALLOW_ALL: поведение по умолчанию дляcatalog.<name>.expose-modeDENY_BY_DEFAULT: metadata скрывается, если объект не совпал сcatalog.<name>.expose-db-patternsилиcatalog.<name>.expose-table-patterns.<dbRegex>
Latency-aware routing, background backend polling, adaptive timeout, circuit breaker, safe hedged
fanout read и degraded omission настраиваются через routing.* properties:
routing.backend-state-polling.enabled=true
routing.backend-state-polling.interval-ms=10000
routing.backend-state-polling.probe-timeout-ms=5000
routing.backend-state-polling.max-parallelism=8
routing.adaptive-timeout.enabled=true
routing.adaptive-timeout.initial-ms=5000
routing.adaptive-timeout.min-ms=1000
routing.adaptive-timeout.max-ms=60000
routing.adaptive-timeout.multiplier=4.0
routing.adaptive-timeout.alpha=0.2
routing.adaptive-timeout.reconnect-cooldown-ms=30000
routing.circuit-breaker.enabled=true
routing.circuit-breaker.failure-threshold=3
routing.circuit-breaker.open-state-ms=30000
routing.hedged-read.enabled=true
routing.hedged-read.max-parallelism=8
routing.database-list-cache.ttl-ms=5000
routing.database-list-cache.max-entries=1000
routing.database-list-cache.shared-across-users=true
routing.database-metadata-cache.ttl-ms=5000
routing.database-metadata-cache.max-entries=1000
routing.database-metadata-cache.shared-across-users=true
routing.database-cache.background-refresh.enabled=true
routing.database-cache.background-refresh.interval-ms=60000
routing.database-cache.background-refresh.activity-window-ms=3600000
routing.refresh-privileges.synthetic-success=true
routing.config-value-cache.ttl-ms=3600000
routing.config-value-cache.max-entries=1000
routing.cache.table-metadata.ttl-ms=60000
routing.cache.table-metadata.max-entries=10000
routing.cache.partition-metadata.ttl-ms=60000
routing.cache.partition-metadata.max-entries=10000
routing.cache.serve-stale-on-error=true
routing.cache.stale-grace-period-ms=300000
# Распределенная DDL-инвалидация кэшей между репликами прокси через ZooKeeper:
routing.cache.distributed-invalidation.mode=ZOOKEEPER
routing.cache.distributed-invalidation.connect-string=zookeeper.internal:2181
routing.cache.distributed-invalidation.session-timeout-ms=60000
routing.cache.distributed-invalidation.connection-timeout-ms=15000
routing.cache.distributed-invalidation.base-path=/hms-proxy-cache-invalidation
routing.cache.distributed-invalidation.event-retention-ms=300000
routing.cache.distributed-invalidation.purge-interval-ms=60000
routing.degraded-routing-policy=SAFE_FANOUT_READSAdaptive timeout стартует с routing.adaptive-timeout.initial-ms, затем следует за backend
latency EWMA в заданных min/max пределах. Каждое изменение таймаута приводит к reconnect
shared backend client и сбросу кэша impersonation-клиентов (Kerberos re-login), поэтому proxy
применяет hysteresis (max(2s, 25 % от текущего значения)) и cooldown
(routing.adaptive-timeout.reconnect-cooldown-ms, по умолчанию 30 s) перед повторным
реконнектом. Счётчики hms_proxy_adaptive_timeout_reconnect_total и
hms_proxy_adaptive_timeout_reconnect_skipped_total{reason="hysteresis"|"cooldown"} показывают,
сколько реконнектов сработало и сколько было подавлено троттлингом. Если переподключение не успело
перевести общий пул в состояние покоя (дождаться освобождения активных сессий), таймаут сокета клиента
откатывается к значению, с которым работают активные сессии, и cooldown всё равно запускается —
перегруженный бэкенд не пытается повторно сбросить пул на каждом следующем запросе. Transport failure
и превышение latency budget
учитываются в circuit breaker. Когда backend достигает
routing.circuit-breaker.failure-threshold, proxy начинает fail-fast для этого backend до конца
open-window, а потом пускает один half-open retry, который либо закрывает circuit, либо снова
открывает его.
Для ускорения работы в высоконагруженных окружениях и снижения сетевых задержек при межкластерных (WAN) обращениях hms-proxy поддерживает кэширование метаданных таблиц и разделов в памяти:
routing.cache.table-metadata.ttl-ms(илиttl-seconds, по умолчанию0— выключено): кэширование объектовTableдля вызововget_table.routing.cache.table-metadata.max-entries(по умолчанию10000): лимит записей LRU-кэша таблиц.routing.cache.partition-metadata.ttl-ms(илиttl-seconds, по умолчанию0— выключено): кэширование метаданных и списков разделов (get_partition*,get_part_specs*).routing.cache.partition-metadata.max-entries(по умолчанию10000): лимит записей LRU-кэша разделов.routing.cache.serve-stale-on-error=true: при временной недоступности бэкенда или сетевых таймаутах отдавать устаревшие записи из кэша в течение льготного периодаrouting.cache.stale-grace-period-ms(по умолчанию 5 минут).- SingleFlight: параллельные обращения к одной и той же таблице или разделу объединяются в один запрос к бэкенду.
При горизонтальном масштабировании hms-proxy на несколько экземпляров за балансировщиком нагрузки DDL-операция (ALTER TABLE, DROP TABLE, TRUNCATE TABLE, DROP DATABASE), выполненная через один экземпляр прокси, локально сбрасывает закэшированные записи. Для того чтобы остальные экземпляры прокси не продолжали отдавать устаревшие метаданные из своих локальных кэшей, используется распределенная инвалидация через ZooKeeper:
routing.cache.distributed-invalidation.mode=ZOOKEEPER
routing.cache.distributed-invalidation.connect-string=zookeeper.internal:2181
routing.cache.distributed-invalidation.session-timeout-ms=60000
routing.cache.distributed-invalidation.connection-timeout-ms=15000
routing.cache.distributed-invalidation.base-path=/hms-proxy-cache-invalidation
routing.cache.distributed-invalidation.event-retention-ms=300000
routing.cache.distributed-invalidation.purge-interval-ms=60000
# routing.cache.distributed-invalidation.instance-id=proxy-node-1- Механизм публикации и подписки: экземпляр, применивший DDL-изменение, публикует компактное бинарное событие в znode
/hms-proxy-cache-invalidation. Все остальные ноды через Apache CuratorPathChildrenCacheнемедленно получают нотификацию и сбрасывают кэш затронутой сущности (таблицы, раздела, базы данных). - Подавление эха: события содержат идентификатор
instance-idноды-источника, что исключает повторную обработку собственных событий. - Fail-Safe защита при сетевом расщеплении (split-brain): при переходе соединения с ZooKeeper в состояние
LOSTилиSUSPENDEDэкземпляр превентивно инвалидирует все локальные кэши метаданных, предотвращая выдачу рассинхронизированных данных клиентам. - Сборка мусора (Retention Sweep): фоновый поток периодически удаляет znode событий старше
event-retention-ms(по умолчанию 5 минут). - Фоллбэк на строку подключения блокировок: при отсутствии
connect-stringв секции инвалидации используется адресsynthetic-read-lock.store.zookeeper.connect-string. - Prometheus-метрики:
hms_proxy_cache_invalidation_events_published_total{target="table|database|catalog|all"}hms_proxy_cache_invalidation_events_received_total{target="table|database|catalog|all"}hms_proxy_cache_invalidation_errors_total{action="publish|receive|purge"}
Когда множество пользователей обращаются к метастору (например, через HiveServer2, Trino или Spark), кэширование метаданных раздельно для каждого пользователя приводит к дублированию RPC-запросов в бэкенд-метасторы, росту задержек и увеличению расхода памяти на кэш.
В hms-proxy встроен плагин авторизации Apache Ranger (ranger-plugins-common 2.5.0), который вычисляет политики доступа локально в процессе прокси. Это открывает возможность использовать общий глобальный кэш метаданных:
routing.database-list-cache.shared-across-users=truerouting.database-metadata-cache.shared-across-users=true
При shared-across-users=true ключи кэша не привязываются к пользователю и группам, благодаря чему вызовы SHOW DATABASES, SHOW TABLES и get_database/get_table* от любых пользователей попадают в единый кэш, единожды заполненный из бэкенд-метасторов. Перед выдачей клиенту встроенный Ranger-авторизатор фильтрует базы данных, таблицы и представления в соответствии с правами конкретного вызывающего пользователя. При выключенном Ranger (NoOpMetadataAuthorizer) проверка прав пропускается с нулевым оверхедом.
# Включение встроенного Ranger-плагина
ranger.enabled=true
ranger.policy.rest.url=http://ranger-admin.internal:6080
ranger.service-name=hms_proxy_hive
ranger.service-type=hive
ranger.app-id=hms-proxy
# Опциональный локальный кэш политик и интервал опроса
# ranger.policy.cache.dir=/var/cache/hms-proxy/ranger
# ranger.policy.poll-interval-ms=30000
# ranger.policy.connection-timeout-ms=5000
# ranger.policy.read-timeout-ms=10000
# ranger.ssl.truststore.file=/etc/security/truststore.jks
# ranger.ssl.truststore.password=changeit
# ranger.config-dir=/etc/ranger/hms-proxy
# ranger.audit.enabled=false
# Опрос ролей из Ranger Admin (отключите для Ranger 1.x / HDP 3.1.0, где нет эндпоинта /service/roles/download/):
# ranger.roles.enabled=true
# Маскировка недоступных таблиц/баз под "not found" (default: false - выдавать явный MetaException: Access denied):
# ranger.mask-unauthorized-as-not-found=false
# Общий кэш метаданных:
routing.database-list-cache.ttl-ms=10000
routing.database-list-cache.shared-across-users=true
routing.database-metadata-cache.ttl-ms=10000
routing.database-metadata-cache.shared-across-users=true
# Фоновое автообновление активных записей кэшей баз данных:
routing.database-cache.background-refresh.enabled=true
routing.database-cache.background-refresh.interval-ms=60000
routing.database-cache.background-refresh.activity-window-ms=3600000
# Переопределения для отдельных каталогов (отдельный сервис Ranger для каталога):
catalog.catalog1.ranger.enabled=true
catalog.catalog1.ranger.service-name=c1_hive_service
# catalog.catalog1.ranger.mask-unauthorized-as-not-found=false
catalog.catalog2.ranger.enabled=true
catalog.catalog2.ranger.service-name=c2_hive_service-
Трансляция пространств имён и сопоставление ресурсов: Перед проверкой прав в Ranger прокси транслирует федеративные имена во внутренние имена бэкенда:
- При запросе к
apache__marketing.campaigns(каталогapache, базаmarketing), в Ranger проверяется ресурс:database=marketingtable=campaigns
- При запросе к
sales.ordersдефолтного каталога, в Ranger проверяется:database=salestable=ordersПолитики в Apache Ranger Admin всегда настраиваются на оригинальные имена баз и таблиц бэкенда (без федеративных префиксов прокси).
- При запросе к
-
Общий (Global) vs. Раздельные (Per-Catalog) Ranger:
- Общий Ranger (
ranger.*): Все каталоги используют один общий сервис Ranger (например,hive). Политика наdatabase=salesприменяется к базеsalesво всех каталогах, использующих этот глобальный сервис. - Раздельные Ranger для каталогов (
catalog.<name>.ranger.*): Для каждого каталога создаётся изолированный экземплярRangerBasePlugin, подключенный к собственному сервису (например,onprem_hive,cloud_hive) или отдельному инстансу Ranger Admin. Политики полностью изолированы: базаsalesвcatalog1и базаsalesвcatalog2авторизуются по своим собственным независимым наборам правил.
- Общий Ranger (
-
Жизненный цикл фильтрации:
SHOW DATABASES(get_all_databases,get_databases): Списки баз данных извлекаются из общего кэша и передаются вfilterDatabases(catalogName, databases, caller). Ranger проверяет доступ (select,read,use). Разрешенные базы снабжаются клиентскими префиксами и возвращаются вызывающему.SHOW TABLES(get_all_tables,get_tables,get_tables_ext,get_tables_req): Списки таблиц берутся из общего кэша и фильтруются под пользователя черезfilterTables(catalogName, backendDbName, tables, caller). Если база данных недоступна пользователю в Ranger или скрыта правилами федерации, методы листинга возвращают пустой список вместо ошибки, что предотвращает фатальный сбой Apache ThriftTApplicationException: Internal error processing get_all_tables(в Thrift IDL для листинга таблиц не объявленNoSuchObjectException) и полностью соответствует поведению нативного HMS.get_database,get_table,get_table_req: Доступ проверяется черезisDatabaseAllowed/isTableAllowed. При отсутствии прав поведение определяется параметромranger.mask-unauthorized-as-not-found: по умолчанию (false) выбрасываетсяMetaExceptionс префиксомAccess denied: ..., а приtrue—NoSuchObjectException(маскировка под отсутствие объекта).
-
Разрешение групп Active Directory / LDAP и сопоставление с политиками:
-
Серверный резолвинг групп: При Kerberos-аутентификации (
remoteUser) и при вызовахset_ugi(user, [])без указания групп прокси автоматически определяет членство пользователя в группах через Hadoop UGI (UserGroupInformation.createRemoteUser(userName).getGroupNames()). Настройки маппинга групп (hadoop.security.group.mapping, напримерorg.apache.hadoop.security.LdapGroupsMapping) и таймауты кэширования (hadoop.security.groups.cache.secs) передаются черезsecurity.front-door-conf.*. -
Нормализация регистра и Distinguished Name (DN): Движок политик Apache Ranger чувствителен к регистру (case-sensitive) и обычно хранит имена групп в нижнем регистре (
sales,domain users), тогда как службы каталогов Active Directory и LDAP могут возвращать группы в смешанном регистре (Sales,Domain Users) или в виде полного DN (CN=Sales,OU=Groups,DC=example,DC=com). Встроенный Ranger-плагинhms-proxyавтоматически извлекает имя группы из атрибута CN и обогащает запрос нормализованными вариантами в нижнем регистре, гарантируя надежное сопоставление с политиками Ranger без ручного дублирования правил с разным регистром. -
Встроенный дисковый кэш групп для быстрого холодного старта (
security.group-disk-cache.*): Для ускорения старта и исключения сетевых задержек на синхронные обращения к Active Directory/LDAP прокси поддерживает энергонезависимый дисковый кэш групп пользователей в формате JSON (security.group-disk-cache.enabled=true,path,entry-ttl-seconds,persist-interval-seconds,persist-on-shutdown). На холодном старте кэш предзагружается с диска (время резолвинга 0 мс), периодически атомарно сбрасывается в фоновом потоке и при штатной остановке, а при временных сбоях или недоступности AD/LDAP прозрачно отдает сохраненные группы в режиме serve-stale fallback:security.group-disk-cache.enabled=true security.group-disk-cache.path=/var/cache/hms-proxy/user-groups.json security.group-disk-cache.entry-ttl-seconds=86400 security.group-disk-cache.persist-interval-seconds=60 security.group-disk-cache.persist-on-shutdown=true security.group-disk-cache.max-entries=50000
Записи старше
entry-ttl-secondsне удаляются: на них работает serve-stale fallback. Поэтому размер кэша ограничивает число пользователей —max-entries(дефолт50000). При переполнении вытесняется пользователь, чьи группы обновлялись раньше всех, а на старте с диска загружаются самые свежие записи. ПодSIMPLE-аутентификацией клиент сам называет себя черезset_ugi, так что без потолка кэш (и файл на диске) рос бы от любого потока выдуманных имён.
-
-
Защита от ConcurrentModificationException при обновлении политик Ranger:
- Фоновое обновление политик в потоке
PolicyRefresherRanger SDK 2.5.0 способно конкурировать с клиентскими потоками оценки прав доступа. Вhms-proxyвызовы оценки прав защищены механизмомevaluateAccessWithRetry: при возникновенииConcurrentModificationExceptionвыполняется yield потока и однократный повтор проверки, а при сохранении ошибки — безопасный fail-open fallback для чтения с фиксацией в логе.
- Фоновое обновление политик в потоке
-
Совместимость со старыми версиями Ranger 1.x / HDP 3.1.0 (
ranger.roles.enabled):- В Ranger 1.x отсутствует REST-эндпоинт загрузки ролей (
/service/roles/download/), что приводит к спаму сообщений об ошибках HTTP 404. Параметрranger.roles.enabled=false(или per-catalogcatalog.<name>.ranger.roles.enabled=false) полностью отключает опрос ролей через легковесныйNoOpRolesRangerAdminClient.
- В Ranger 1.x отсутствует REST-эндпоинт загрузки ролей (
-
Управление маскировкой недоступных объектов (
ranger.mask-unauthorized-as-not-found):- По умолчанию (
ranger.mask-unauthorized-as-not-found=false) при запрете доступа в Ranger прокси выбрасываетMetaExceptionс явным сообщениемAccess denied: Table '<db>.<table>' is not accessible in proxy catalog '<catalog>'(илиDatabase '<db>' is not accessible...). Это гарантирует, что HiveServer2 и клиенты Beeline показывают пользователю понятную ошибку отказа в доступе вместо маскировки подTable not found/Database not found(которая вводила пользователей в заблуждение). - При
ranger.mask-unauthorized-as-not-found=true(или per-catalogcatalog.<name>.ranger.mask-unauthorized-as-not-found=true) прокси возвращаетNoSuchObjectException, реализуя политику zero-information-disclosure (сокрытие самого факта существования защищенных объектов).
- По умолчанию (
-
Системные плейсхолдеры движков и файловые источники Spark SQL (
global_temp, path-based таблицы и Hive_dummy_database):- Служебная in-memory база данных Apache Spark SQL
global_tempи плейсхолдер Hive_dummy_databaseне принадлежат ни одному реальному метастору. - Файловые источники данных Spark SQL (path-based tables): При выполнении прямых запросов к путям файловых хранилищ (например,
SELECT ... FROM csv.hdfs://...,SELECT ... FROM csv."hdfs://..."илиparquet./path/to/data) Spark SQL и каталоги форматов таблиц (в частности Apache Iceberg `HiveTableOperations`) перед чтением файлов проверяют существование базы или таблицы в метасторе вызовами `get_database("csv")`, `get_table(format, path)`, `get_table_req` или `get_table_objects_by_name`. Прокси распознает пути и схемы URI (`hdfs://`, `s3a://`, `file://`, слэши `/`), внешние кавычки (`"`, `'`,``), префиксы форматов с точкой (csv."hdfs://..."), а также стандартные форматы файлов (csv,parquet,orc,json,avro,text,binaryfile,delta,iceberg) и немедленно возвращаетNoSuchObjectException(или пустой список для перечислений) без сетевых вызовов в бэкенд и без проверки в Ranger. Это предотвращает ложные отказы в доступеMetaException: Access deniedприmask-unauthorized-as-not-found=falseи фатальные сбои Spark/IcebergRuntimeException: Failed to get table info from metastore`, обеспечивая штатный fallback на файловый Data Source. - Прокси перехватывает запросы к ним на раннем этапе маршрутизации до обращения к Ranger: RPC-вызовы
get_database,get_table,get_table_reqиget_table_objects_by_nameнемедленно возвращаютNoSuchObjectException/ пустые списки без выполнения сетевых вызовов в бэкенд и без проверки авторизации. Это предотвращает ложные отказы в доступеMetaException: Access deniedприmask-unauthorized-as-not-found=falseи обеспечивает бесперебойный запуск приложений Spark SQL на YARN (ApplicationMaster) и HiveINSERT ... VALUES.
- Служебная in-memory база данных Apache Spark SQL
Iceberg pointer guard — INSERT в Iceberg-таблицу из HiveServer2 открывается
alter_table_with_environment_context с объектом Table, снятым на этапе компиляции запроса, а
метастор применяет эти параметры целиком, поэтому стирается каждый Iceberg-ключ, который есть в
записи и отсутствует в запросе, — в первую очередь metadata_location. Прокси — единственное
место, где встречаются Hive- и Iceberg-пути записи, поэтому такой alter сливается поверх записи,
которую метастор держит сейчас, а не пересылается как есть; честный Iceberg-коммит опознаётся по
previous_metadata_location, равному текущему указателю, и пропускается без изменений. На Hive
4-бэкендах починенный alter дополнительно несёт
expected_parameter_key/expected_parameter_value — метастор применит его только пока указатель
всё ещё тот, который был прочитан.
routing.iceberg-pointer-guard.enabled=true
routing.iceberg-pointer-guard.table-cache-ttl-ms=30000
routing.iceberg-pointer-guard.table-cache-max-entries=10000
routing.iceberg-pointer-guard.lock-enabled=true
routing.iceberg-pointer-guard.lock-acquire-timeout-ms=10000Чтение записи и запись alter'а — два разных вызова, поэтому починка держит тот самый табличный
лок, который берёт сам Iceberg (EXCLUSIVE, уровень таблицы — форма запроса из
org.apache.iceberg.hive.MetastoreLock), на всё перечитывание, слияние и alter_table бэкенда.
Лок запрашивается только когда починка действительно нужна: честный Iceberg-коммит шлёт свой
alter_table изнутри этого же лока, поэтому запрос лока до решения означал бы ожидание того, кто
ждёт ответа на обслуживаемый вызов. Не полученный за lock-acquire-timeout-ms лок никогда не
отменяет запись — alter уходит починенным, но без защиты, и это считается как
repair_lock_timeout: уронить обычную запись Hive из-за икоты таблицы локов метастора хуже, чем
оставить окно открытым. 0 означает одну попытку без ожидания. Non-default каталоги не блокируются
никогда: их писателей обслуживает synthetic lock shim, который выдаёт локи без проверки конфликтов,
поэтому за объект этого лока никто не борется.
Iceberg-ность таблицы определяется по записи метастора, а не по запросу — в форме, которую
присылает HiveServer2, нет ни одного Iceberg-ключа, — и это стоит одного get_table на каждый
alter_table. table-cache-ttl-ms ограничивает цену: имя, про которое метастор ответил, что это
не Iceberg-таблица, запоминается на это время, поэтому обычные Hive-таблицы (где и сосредоточен
объём alter_table) платят одно чтение и дальше ничего. Iceberg-таблицы не кэшируются никогда —
их текущий указатель обязан читаться заново; 0 полностью отключает кэш. Таблица, ставшая
Iceberg-таблицей в обход прокси, остаётся незащищённой не дольше одного интервала TTL: create_table или alter_table с
указателем сбрасывает запомненный ответ сразу. hms_proxy_iceberg_pointer_guard_events_total
показывает и починки, и сэкономленные кэшем чтения.
server.name=hms-proxy
server.bind-host=0.0.0.0
server.port=9083
routing.default-catalog=hdp
routing.catalog-db-separator=__
compatibility.frontend-profile=HORTONWORKS_3_1_0_3_1_5_6150_1
compatibility.frontend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jar
compatibility.backend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jar
security.mode=KERBEROS
security.server-principal=hive/_HOST@EXAMPLE.COM
security.keytab=/etc/security/keytabs/hms-proxy.keytab
security.client-principal=hive/_HOST@EXAMPLE.COM
security.client-keytab=/etc/security/keytabs/hms-proxy-client.keytab
security.impersonation-enabled=true
catalogs=hdp,apache
routing.backend-state-polling.enabled=true
routing.adaptive-timeout.enabled=true
routing.circuit-breaker.enabled=true
routing.hedged-read.enabled=true
routing.degraded-routing-policy=SAFE_FANOUT_READS
catalog.hdp.runtime-profile=HORTONWORKS_3_1_0_3_1_5_6150_1
catalog.hdp.backend-standalone-metastore-jar=/opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.5.6150-1.jar
catalog.hdp.impersonation-enabled=true
catalog.hdp.conf.hive.metastore.uris=thrift://hdp-hms.example.com:9083
catalog.hdp.conf.hive.metastore.sasl.enabled=true
catalog.hdp.conf.hive.metastore.kerberos.principal=hive/_HOST@EXAMPLE.COM
catalog.hdp.latency-budget-ms=1500
catalog.apache.runtime-profile=APACHE_3_1_3
catalog.apache.impersonation-enabled=true
catalog.apache.conf.hive.metastore.uris=thrift://apache-hms.example.com:9083
catalog.apache.conf.hive.metastore.sasl.enabled=true
catalog.apache.conf.hive.metastore.kerberos.principal=hive/_HOST@EXAMPLE.COM
catalog.apache.latency-budget-ms=5000Для сценариев из SMOKE.md в репозитории теперь есть runnable-клиент прямых HMS RPC:
io.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli txnio.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli lockio.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli notification
Если прогон txn или lock падает уже после open_txns, клиент делает best-effort abort_txn
перед пробросом ошибки, в том числе при --close-txn none: упавший smoke не должен оставлять
транзакцию, удерживающую watermark ValidTxnList до истечения heartbeat-timeout.
Текущее smoke-покрытие сведено в test matrix в SMOKE.md по полям:
- client version
- front-door profile
- backend profile
- auth mode
- method families
- expected result
Сначала собери jar:
mvn -DskipTests packageДля Java 17+ в Kerberos-окружении с Hadoop 2.x используй те же JVM-флаги, что и для proxy:
java \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
-cp target/hms-proxy-$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)-fat.jar \
io.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli txn \
--uri thrift://proxy-host:9083 \
--auth kerberos \
--server-principal hive/proxy-host.example.com@REALM.COM \
--client-principal alice@REALM.COM \
--keytab /etc/security/keytabs/alice.keytab \
--krb5-conf /etc/krb5.conf \
--db hdp__default \
--table smoke_txn_tblЭтот режим последовательно вызывает:
open_txnsallocate_table_write_idslockcheck_lockget_valid_write_idscommit_txn
Режим lock нужен для прямой проверки lock lifecycle, в первую очередь synthetic shim на
non-default catalog. Он открывает один txn, берет запрошенный lock, вызывает check_lock,
при необходимости делает heartbeat, при необходимости вызывает unlock, а затем завершает
txn через abort или commit.
Пример DB lock в стиле CREATE TABLE на non-default catalog:
java \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
-cp target/hms-proxy-$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)-fat.jar \
io.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli lock \
--uri thrift://proxy-host:9083 \
--db apache__default \
--lock-type SHARED_READ \
--lock-level DB \
--operation-type NO_TXN \
--transactional falseПример partition lock в стиле rename/drop на non-default catalog:
java \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
-cp target/hms-proxy-$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)-fat.jar \
io.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli lock \
--uri thrift://proxy-host:9083 \
--db apache__default \
--table smoke_managed_tbl \
--partition p=2026-04-01 \
--lock-type EXCLUSIVE \
--lock-level PARTITION \
--operation-type NO_TXN \
--transactional falseРежим notification нужен для Hortonworks-only RPC add_write_notification_log, поэтому ему
дополнительно нужен HDP standalone metastore jar:
java \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
-cp target/hms-proxy-$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)-fat.jar \
io.github.mmalykhin.hmsproxy.tools.HmsMetastoreSmokeCli notification \
--uri thrift://proxy-host:9083 \
--auth kerberos \
--server-principal hive/proxy-host.example.com@REALM.COM \
--client-principal alice@REALM.COM \
--keytab /etc/security/keytabs/alice.keytab \
--krb5-conf /etc/krb5.conf \
--db hdp__default \
--table smoke_txn_tbl \
--txn-id 1001 \
--write-id 2001 \
--files-added hdfs:///warehouse/tablespace/managed/hive/smoke_txn_tbl/delta_1001_1001_0000/bucket_00000 \
--hdp-standalone-metastore-jar /opt/hms-proxy/hive-metastore/hive-standalone-metastore-3.1.0.3.1.0.0-78.jarЧто важно:
--server-principalдолжен указывать на front-door principal самого proxy, а не backend HMS--client-principalи--keytabэто Kerberos credentials клиента, которым запускается smoke- дополнительные HiveConf overrides можно передавать через повторяющийся
--conf key=value lockэто самый короткий путь воспроизвести non-default catalogNO_TXNshim кейсы вродеCREATE TABLEи partition rename/drop без Beelinenotificationдолжен проходить для Hortonworks-routed каталога и падать сrequires a Hortonworks backend runtimeдля Apache-routed каталога
Для регулярных проверок на реальной инсталляции теперь есть два отдельных runner'а:
Это обёртки над тем же smoke client, которые fail-fast запускают сценарий из:
- optional Beeline / HiveServer2 SQL smoke из SMOKE.md
- direct txn/ACID smoke
- DB lock smoke на non-default catalog
- optional partition lock smoke
- optional Hortonworks notification smoke
Стартовать удобнее с соответствующего example-конфига:
cp scripts/hms-real-installation-smoke.simple.env.example scripts/hms-real-installation-smoke.simple.env
cp scripts/hms-real-installation-smoke.kerberos.env.example scripts/hms-real-installation-smoke.kerberos.envДалее настройте значения HMS_SMOKE_* и запустите сценарии:
scripts/run-real-installation-smoke-simple.sh --scenario all
scripts/run-real-installation-smoke-kerberos.sh --scenario allЛибо более узкий прогон:
scripts/run-real-installation-smoke-simple.sh --scenario sql
scripts/run-real-installation-smoke-simple.sh --scenario locks
scripts/run-real-installation-smoke-kerberos.sh --scenario notificationЧто важно:
- по умолчанию runner берёт самый свежий
target/hms-proxy-*-fat.jar - путь к jar можно переопределить через
HMS_SMOKE_FAT_JAR - если задан
HMS_SMOKE_BEELINE_JDBC_URL, вallдополнительно запускается Beeline / HiveServer2 SQL smoke изSMOKE.md - SQL smoke использует
HMS_SMOKE_HDP_READ_TABLE/HMS_SMOKE_APACHE_READ_TABLE, по умолчанию проверяет view rewrite и permanent UDF, а при необходимости умеет запускать transactional SQL и materialized-view checks - если proxy специально поднят без
federation.view-text-rewrite.mode=REWRITE, выставьHMS_SMOKE_SQL_RUN_VIEW_REWRITE=false; для UDF можно выставитьHMS_SMOKE_SQL_RUN_UDF=falseили переопределитьHMS_SMOKE_SQL_UDF_CLASSвместе сHMS_SMOKE_SQL_UDF_EXPECTED_RESULT, если HS2 classpath отличается - если заданы
HMS_SMOKE_TXN_SECONDARY_DBиHMS_SMOKE_TXN_SECONDARY_TABLE, runner делает второй direct txn smoke - если
HMS_SMOKE_NOTIFICATION_*не настроены, notification шаг вallбудет пропущен - если заданы
HMS_SMOKE_NOTIFICATION_NEGATIVE_DBиHMS_SMOKE_NOTIFICATION_NEGATIVE_TABLE, runner дополнительно запускает negative notification check для Apache backend изSMOKE.md - simple runner автоматически подхватывает
scripts/hms-real-installation-smoke.simple.env - Kerberos runner автоматически подхватывает
scripts/hms-real-installation-smoke.kerberos.env