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.
Он даёт один (или несколько) production-facing HMS Thrift endpoint(ов), которые федеративно маршрутизируют каталоги в несколько backend metastore, сглаживают API-различия Apache 3.1.3 / HDP 3.1.0.x / Hive 4.1.x в обе стороны и задают явную security boundary между клиентами и backend HMS сервисами.
Один production-facing HMS Thrift endpoint для HiveServer2 и прямых HMS API клиентов (или
несколько, на разных портах, когда клиенты разных версий Hive не могут делить общий front
door — в Thrift-протоколе нет version-negotiation handshake), который маршрутизирует запросы
в несколько backend metastore по явному catName или по legacy database name в формате
catalog<separator>db.
Это позволяет централизовать catalog-aware routing и selective exposure, не заставляя клиентов знать внутреннюю раскладку backend metastore.
Apache Hive Metastore 3.1.3, Hortonworks 3.1.0.x или Hive 4.1.x на фронте (один primary
listener плюс опциональные дополнительные listener'ы на отдельных портах) поверх любой
комбинации Apache 3.1.3, Hortonworks 3.1.0.x и Hive 4.1.x backend'ов. Proxy выполняет
compatibility downgrade выбранных *_req API для старых Hortonworks backend'ов; поднимает
два positional read метода, удалённых в Hive 4 (get_table, get_table_objects_by_name),
когда фронтит Hive 4 backend для Apache 3.1.3 клиента; и опускает Hive 4-only *_req
wrappers для read/стандартного DDL до их positional Apache 3.1.3 эквивалентов, когда
обслуживает Hive 4 клиентов против Apache 3.1.3 backend. Hortonworks-специфичные RPC
доступны через HDP standalone-metastore jar при сконфигурированном HDP backend; truly
Hive 4-only API (data connectors, scheduled queries, stored procedures, packages, ACID v2
extensions) отвечают TApplicationException UNKNOWN_METHOD.
На практике proxy становится мостом для смешанных Apache/HDP/Hive 4 estate и поэтапных миграций в обе стороны, а не просто request router.
Proxy как security boundary между клиентами и backend metastore: Kerberos/SASL на фронте, опциональный outbound Kerberos к backend и опциональная impersonation аутентифицированного Kerberos пользователя.
Так authentication, identity propagation и политика доступа к backend сосредоточены в одной точке.
В этом 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. | Если ownership получается ambiguous, proxy безопасно падает вместо того, чтобы угадывать target catalog. |
Если запрос несёт backend catName вроде hive, а не proxy catalog id, это поле для
совместимости не считается авторитетным: proxy пытается маршрутизировать по dbName, а если
этого мало, идёт в routing.default-catalog.
Для mutation path proxy предпочитает policy-driven guarantees, а не best-effort guessing: deterministic routing, explicit namespace ownership, отсутствие silent split-brain writes и safe failure, если mutation остаётся ambiguous.
Следующие настройки меняют клиентское представление имён или 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.
Если нужен spreadsheet-like method-level вид по backend support, routing mode, fallback strategy и semantic risk, смотри COMPATIBILITY.md.
Обновить сгенерированную таблицу можно так:
mvn -o -q -Dtest=CapabilityMatrixDocSyncTest -Dcapabilities.updateReadme=true test| Версия клиента | Профиль front door | Профиль backend | Режим auth | Семейства методов | Ожидаемый результат |
|---|---|---|---|---|---|
Apache Hive / Spark клиенты, которые говорят через Apache HMS 3.1.3 request wrappers |
APACHE_3_1_3 |
APACHE_3_1_3 |
NONE или KERBEROS |
catalog-aware read/write, legacy catalog<separator>db routing, view rewrite |
Базовый полностью поддержанный сценарий. |
Apache Hive / Spark клиенты, которые говорят через Apache HMS 3.1.3 request wrappers |
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), говорящие на Hive 4 request-wrapper Thrift API |
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 клиенты, которые говорят через Apache HMS 3.1.3 request wrappers |
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 identity на фронте через 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-wrapper методы | 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-wrapper методы | HORTONWORKS_* с standalone jar |
APACHE_3_1_3 |
NONE или KERBEROS |
HDP-only passthrough методы вроде add_write_notification_log |
Явно отклоняется, если target backend не даёт совместимый Hortonworks runtime. |
| HiveServer2 / Beeline SQL workloads через несколько каталогов | APACHE_3_1_3 или HORTONWORKS_* |
смешанные Apache + Hortonworks backend | NONE или KERBEROS |
read, DDL/DML, namespace rewrite, optional view rewrite | Поддержано, пока routing может однозначно вычислить целевой каталог. |
| HiveServer2 / direct HMS клиенты, использующие txn/lock lifecycle RPC без namespace в payload | любой | смешанные Apache + Hortonworks backend | NONE или KERBEROS |
open_txns, commit_txn, abort_txn, check_lock, unlock, heartbeat |
Degraded: идут в routing.default-catalog; допустимые non-ACID SELECT, NO_TXN DDL и non-transactional write (INSERT/UPDATE/DELETE) lock всё же могут синтетически обслуживаться на non-default catalog, но в остальном это стоит считать single-catalog control plane, пока не проведена отдельная валидация. |
| Kerberized HiveServer2 / HMS клиенты, которым нужна end-user identity на backend | любой | любой | KERBEROS с optional impersonation |
front-door SASL, local delegation-token issuance, backend Hadoop proxy-user, delegation tokens и set_ugi() имперсонация |
Поддержано, если правильно настроены proxy-user rules и backend impersonation permissions. |
| Клиенты, которые пытаются делать mutation без explicit namespace ownership или динамически управлять registry каталогов | любой | любой | NONE или KERBEROS |
policy-guarded ambiguous mutations, create_catalog, drop_catalog |
Безопасно отклоняется по design, чтобы сохранить deterministic routing, explicit namespace ownership и не допустить 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 - proxy умеет синтетически обслуживать non-ACID
SHARED_READSELECTlock, допустимые non-transactionalNO_TXNDDL lock и non-transactional write lock (INSERT,UPDATE,DELETE) для non-default catalog, чтобы HS2 read path, non-ACID DDL path и non-ACIDINSERTне зависели от рассинхрона backend txn state - этот synthetic lock state по умолчанию хранится в памяти, а для multi-instance failover может быть вынесен в ZooKeeper, но это не делает multi-catalog ACID writes безопасными
- Hive ACID, lock, token и другие по-настоящему global metastore операции всё ещё требуют отдельной валидации в вашей среде перед включением их за multi-catalog proxy
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 - объединять одновременные одинаковые запросы к бэкенду (single-flight) для предотвращения исчерпания пула сессий
- исключать degraded backend из таких safe fanout read при
routing.degraded-routing-policy=SAFE_FANOUT_READS
Это намеренно узкий механизм: hedged read и degraded omission применяются только к безопасным
read-only fanout method, сейчас это get_all_databases, get_databases и get_table_meta.
Single-backend write и namespace-sensitive mutation по-прежнему идут по детерминированной
маршрутизации выше и не race'ят несколько metastore одновременно.
Кэш списков баз и кэш метаданных баз выключены по умолчанию (ttl-ms=0). Когда они включены,
повторные SHOW DATABASES или get_database не ходят в backend до истечения TTL. Оба кэша
используют single-flight коалесцирование, возвращают защитные копии (defensive copies) Thrift-структур,
поддерживают скользящую батчевую экспирацию (пошаговая загрузка баз при длинных сканах HiveServer2 / Ranger
продлевает TTL ранее загруженных баз, исключая их преждевременное устаревание до окончания всего цикла сканирования) и
автоматически инвалидируются при выполнении 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); idle-сессии внутри пула можно закрывать
по catalog.<name>.impersonation-session-idle-ttl-ms (дефолт 0 — никогда). Параллельные вызовы
одного пользователя (или гонящего их 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-сессию.
В чистом 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+ с Hadoop 2.x Kerberos библиотеками запускай так:
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.
Proxy проверяет properties-файл на старте и отказывается стартовать на значении, которое не может интерпретировать. Proxy, который запустился и тихо делает противоположное написанному в файле, хуже, чем proxy, который не стартовал.
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
/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. Чтобы делать probe на каждый
запрос, поставь management.readiness-cache-ms=0.
Текущие Prometheus метрики:
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_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_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для админских реконнектов, которым не удалось quiesce пул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_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)
Несмотря на исторические имена метрик 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-таблиц, если во входящем metadata таблица помечена как 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 proxy может открыть выбранный Hortonworks backend runtime в isolated classloader.
Выбор runtime не autodetect'ится по версии backend сервера, а берётся из
catalog.<name>.runtime-profile.
Для front door proxy поднимает Hortonworks thrift Processor в isolated classloader и автоматически
бриджит общие RPC в внутренний Apache 3.1.3 handler. Поддержанные 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) - rewrite сделан intentionally parser-less: лексический сканер отслеживает table-позиции
(
FROM,JOIN,INTO,TABLE,UPDATE) и переписывает database-квалификатор ссылки, стоящей в такой позиции, либо квалифицирует неквалифицированную таблицу внешней БД; полный Hive SQL grammar не разбирается - 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 - входящие cross-catalog ссылки вроде
catalog2__dim.table_xinternalize'ятся для backend, но на выходе гарантированно переписывается только namespace текущей таблицы - всё, что нельзя разрешить однозначно, остаётся нетронутым и логируется на уровне
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
дожидаются завершения; оставшиеся в очереди логируются и пропускаются.
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 и отвергается
при двух других режимах; обе противоречивые комбинации роняют прокси на старте,
а не игнорируются.
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 для INSERT ... VALUES (_dummy_database/
_dummy_table, константа SemanticAnalyzer.DUMMY_DATABASE): он не существует ни в одном
metastore и не относится ни к какому каталогу, поэтому его компоненты не выбирают каталог, не
считаются вторым namespace и не переписываются в реальную backend-базу. Запрос, состоящий
только из таких компонентов, уходит в default catalog, которому принадлежит TxnHandler.
Единственное исключение — псевдоисточник Hive _dummy_database._dummy_table: INSERT ... VALUES
и запросы без FROM берут lock на нём вместе с реальной целевой таблицей, поэтому такой запрос
всегда называет две базы. Этой псевдотаблицы нет ни в одном metastore и блокировать на ней нечего,
поэтому proxy пропускает её и при выборе namespace, и при проверке пригодности для shim. Запрос,
в котором есть только псевдоисточник, по-прежнему идёт по пину в default catalog.
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.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"} показывают,
сколько реконнектов сработало и сколько было подавлено троттлингом. Если reconnect не успел
квиесцировать shared pool, client socket timeout откатывается к значению, с которым работают живые
сессии, и cooldown всё равно запускается — перегруженный backend не квиесцируется заново на каждом
следующем запросе. Transport failure и превышение latency budget
учитываются в circuit breaker. Когда backend достигает
routing.circuit-breaker.failure-threshold, proxy начинает fail-fast для этого backend до конца
open-window, а потом пускает один half-open retry, который либо закрывает circuit, либо снова
открывает его.
Когда множество пользователей обращаются к метастору (например, через 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
# Общий кэш метаданных:
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.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): Списки таблиц берутся из общего кэша и фильтруются под пользователя черезfilterTables(catalogName, backendDbName, tables, caller).get_database,get_table,get_table_req: Доступ проверяется черезisDatabaseAllowed/isTableAllowed. При отсутствии прав выбрасываетсяNoSuchObjectException, скрывая сам факт существования недоступных объектов.
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