Стадия жизненного цикла модуля: Экспериментальная версия
У модуля есть требования для установки
Перед включением модуля необходимо разрешить использование экспериментальных модулей в ModuleConfig deckhouse. Для этого установите параметр allowExperimentalModules в true.
Контракт для разработчиков модулей
- Объявите
SystemBucketв namespace своего модуля:d8-*илиkube-*с меткойheritage: deckhouse— такой Deckhouse создаёт для каждого модуля.spec.sizeзадаёт квоту бакета, не размер выделенного только ему PVC. Удаление CR уничтожает все объекты: жизненный цикл бакета должен быть осознанной частью удаления модуля. - Дождитесь
Ready=TrueсobservedGeneration, равнойmetadata.generation, и читайте ConfigMap изstatus.connectionRef. Не фиксируйте endpoint в коде и не рассчитывайте на ConfigMap сразу после создания CR. - Выдайте ServiceAccount потребителя RBAC на
systembuckets/readи, для записи,systembuckets/write, сresourceNamesтолько нужных бакетов. STS проверяет TokenReview и SubjectAccessReview; сам факт владения CR не даёт прав потребителю. - Подключите projected token с audience
system-s3-sts, оба CA и параметры WebIdentity из ConfigMap. Читайте файл токена заново при обновлении ключа: kubelet заменяет его. Срок projected token (в примере час) независим от срока S3-ключа — не более 30 минут. - Используйте кеширующий credentials provider SDK и фактический
Expirationответа. Не получайте ключ на каждый S3-запрос, не сохраняйте его в Secret и не печатайте токен или ответ STS. Запрос SDK на час ограничивается получасом. Проверяйте обновление ключей одним процессом, работающим дольше 30 минут. - Обрабатывайте недоступность при HA и раннюю недействительность ключей после StorageClass cutover. Одного refresh по Expiration недостаточно: обработку раннего отказа и получение новой пары нужно проверить в вашем SDK. Ключ проверяется в начале запроса: загрузка, начатая до истечения ключа, доходит до конца, а multipart, начатый одним ключом, можно продолжить и завершить следующим, потому что загрузка принадлежит бакету, а не ключу. Multipart, который никто не завершил, требует retry или abort; его части удаляются через 24 часа.
403также может означать квоту или отсутствие прав: бесконечное получение новых ключей не является обработкой всех ошибок. Если ключ можно будет выдать чуть позже, STS отвечает503 ServiceUnavailableс заголовкомRetry-After(в секундах): во время переключения на новый кластер — для ключа на запись, пока хранилище создаётся, и примерно минуту после того, как пропала реплика отказоустойчивого хранилища; повторите запрос через указанное время. - Задайте таймауты запросов в S3-клиенте и повторяйте запрос при ошибке соединения. Когда перезапускается реплика отказоустойчивого хранилища, остальные замечают её уход только примерно через 75 секунд неудачных пингов (порог зашит в Garage). До этого запрос может ждать эту реплику, а уже идущая длинная загрузка — оборваться сбросом соединения вместо продолжения с другой копии: на нагрузочном тесте две из шести загрузок по 32 МиБ через перезапуск реплики оборвались примерно на 90-й секунде. Повторяйте такой GET с
Rangeот последнего полученного байта, а не с начала; соединение из пула к перезапущенному поду может и просто повиснуть до таймаута клиента, поэтому не оставляйте его бесконечным.
Использование и квоты
kubectl get sb показывает Objects, UsedBytes и TotalBytes; -o wide дополнительно показывает ObjectLimit. TotalBytes — установленная квота этого бакета, не физическая ёмкость Garage. Size — желаемая квота. Пустой бакет сообщает objects: 0 и usedBytes: 0, а не пропускает поля.
kubectl -n d8-monitoring get sb loki -o wide
kubectl -n d8-monitoring get sb loki -o json |
jq '.status | {objects,usedBytes,quotaBytes,quotaObjects,conditions}'Статистика обновляется при reconcile, не является счётчиком реального времени и при недоступности backend может отражать последнее успешное чтение. Повышайте spec.size и ожидайте SizeUpToDate текущей generation; уменьшение запрещено. Проверяйте байтовую квоту и число объектов отдельно.
Как запросить бакет
Компонент Deckhouse создаёт SystemBucket в своём namespace:
apiVersion: storage.deckhouse.io/v1alpha1
kind: SystemBucket
metadata:
name: loki
namespace: d8-monitoring
spec:
size: 10Gisize — сколько бакет просит. Значение по умолчанию (1Gi) подходит компоненту, который хранит немного; если нужно больше, указывайте явно. Из этого же значения складывается размер томов системного хранилища: оно заказывает сумму размеров всех SystemBucket, поэтому увеличение здесь и есть то, от чего растут тома. Запись сверх size бэкенд отклоняет.
size ограничивает и число объектов: size / 64 КиБ (16384 для 1Gi, видно в status.quotaObjects). Каждый объект занимает на диске больше своих байт — около 2 КиБ метаданных и округление до блока файловой системы, — и тома хранилища рассчитаны на этот перерасход до такого числа объектов. Запись сверх него отклоняется с 403 AccessDenied («Object quota is reached»), даже если в бакете лежит меньше size. Компоненту, который хранит много объектов в среднем мельче 64 КиБ, нужно заказывать size под их число, а не только под объём.
size можно увеличивать в любой момент и нельзя уменьшать — API-сервер такую правку отклонит. SystemBucket.SizeUpToDate=True означает, что бэкенд применил квоту этого бакета; QuotaPending — квота ещё не применена. Это не гарантия достаточной физической ёмкости. Общий рост PVC и ошибки CSI отражаются только в Module/system-s3.CapacityReady: Growing — тома растут, CannotGrow — расширение невозможно или завершилось ошибкой. При нехватке физического места запись может отказать до достижения квоты; проверяйте оба объекта. Бакет при расширении PVC может оставаться Ready.
kubectl -n d8-monitoring get sb loki -o jsonpath='{.status.conditions[?(@.type=="SizeUpToDate")]}'Контроллер создаёт бакет в системном хранилище и кладёт рядом с ресурсом ConfigMap со всем, что нужно, чтобы до бакета добраться:
$ kubectl -n d8-monitoring get sb
NAME BUCKET PHASE CONNECTION READY AGE
loki d8-monitoring-loki Ready loki-s3-connection True 30s
| Ключ | Значение |
|---|---|
S3_ENDPOINT |
Внутрикластерный адрес системного хранилища, https://. |
S3_REGION |
Регион, который объявляет эндпоинт (garage). |
S3_BUCKET |
Фактическое имя бакета. |
S3_CA_CERT |
CA, которым подписан S3-эндпоинт. Собственный для модуля, как и S3_STS_CA_CERT (сейчас это один и тот же CA), поэтому по умолчанию ему в кластере никто не доверяет. |
S3_STS_ENDPOINT |
Куда обменивать токен ServiceAccount на учётные данные. |
S3_STS_AUDIENCE |
Значение audience (адресат токена), которое нужно запросить для токена ServiceAccount, предъявляемого на S3_STS_ENDPOINT, — всегда system-s3-sts. Указывается в спецификации пода, в projected-томе с токеном (см. ниже): из ConfigMap Kubernetes его не возьмёт. Эндпоинт принимает только токены, выпущенные для этого адресата, поэтому токен пода по умолчанию будет отвергнут. Такой токен не аутентифицирует в API-сервере: утёкший, он позволяет только запросить учётные данные, которые его ServiceAccount и так вправе получить. |
S3_STS_CA_CERT |
CA, которым подписан эндпоинт выдачи. Он собственный для модуля, поэтому по умолчанию ему в кластере никто не доверяет. |
AWS_ROLE_ARN, AWS_ENDPOINT_URL_STS, AWS_ENDPOINT_URL_S3, AWS_REGION |
То же подключение под именами, которые AWS SDK читают сами: бакет в виде arn:deckhouse:system-s3:::systembucket/<namespace>/<name>, эндпоинт выдачи, эндпоинт S3 и регион. См. AWS SDK без кода. |
Учётных данных в нём нет — и больше их нет нигде. Долгоживущий ключ модуль не выпускает ни для кого, включая владельца бакета. Учётные данные получают все одинаково: предъявляют токен своего ServiceAccount на эндпоинте выдачи и получают пару ключей, которую бэкенд перестаёт принимать не позднее чем через 30 минут.
Адреса берите через envFrom этого ConfigMap, токен монтируйте, а оба CA
смонтируйте в отдельный каталог:
envFrom:
- configMapRef:
name: loki-s3-connection
env:
# Go: системные корни плюс CA модуля, для всех TLS-клиентов процесса.
# /etc/ssl/certs остаётся в списке, потому что SSL_CERT_DIR заменяет
# каталоги по умолчанию, а не дополняет их.
- name: SSL_CERT_DIR
value: /etc/ssl/certs:/etc/system-s3/ca
volumeMounts:
- name: s3-token
mountPath: /var/run/secrets/system-s3
readOnly: true
- name: s3-ca
mountPath: /etc/system-s3/ca
readOnly: true
volumes:
- name: s3-token
projected:
sources:
- serviceAccountToken:
path: token
audience: system-s3-sts # S3_STS_AUDIENCE
expirationSeconds: 3600
- name: s3-ca
configMap:
name: loki-s3-connection
items: # только CA, PEM-файлами
- {key: S3_CA_CERT, path: s3-ca.crt}
- {key: S3_STS_CA_CERT, path: sts-ca.crt}S3-эндпоинт работает только по TLS, с сертификатом от собственного CA модуля, и клиент должен этому CA доверять — открытого порта, на который можно откатиться, нет.
- Go (minio-go, aws-sdk-go-v2, всё на
crypto/tls): примера выше достаточно. Go на Linux собирает системные корни из файла-бандла и из всех файлов в каталогахSSL_CERT_DIR, поэтому CA модуля добавляются к публичным, и любой клиент процесса, который пользуется системными корнями, доверяет обоим эндпоинтам без единой строки кода. Системные корни Go читает один раз за процесс: новый CA вступает в силу после рестарта. CA долгоживущий; продление серверного сертификата его не меняет. - AWS CLI и boto3:
AWS_CA_BUNDLE=/etc/system-s3/ca/s3-ca.crt. Переменная заменяет бандл SDK, а не дополняет его, так что клиенту, который ходит ещё и на публичные эндпоинты, нужен бандл с обоими. - Инструменты на OpenSSL (curl,
sslв Python):SSL_CERT_DIRне поможет — OpenSSL ищет сертификаты в каталоге по именам-хешам, которых в смонтированном ConfigMap нет. Укажите файл:curl --cacert /etc/system-s3/ca/s3-ca.crt,SSL_CERT_FILE,REQUESTS_CA_BUNDLE. - Не через
subPathв/etc/ssl/certs. Том сsubPathне обновляется при изменении ConfigMap, и под останется со старым CA после его замены модулем.
Как получить учётные данные
Обмен — это AssumeRoleWithWebIdentity, который умеет любая клиентская
библиотека S3:
creds, _ := credentials.NewSTSWebIdentity(
stsEndpoint+"/d8-monitoring/loki", // S3_STS_ENDPOINT + /<namespace>/<имя>
func() (*credentials.WebIdentityToken, error) {
token, err := os.ReadFile("/var/run/secrets/system-s3/token")
return &credentials.WebIdentityToken{Token: string(token)}, err
},
)Эндпоинт возвращает пару ключей, которую Garage перестаёт принимать в момент,
указанный в ответе (по умолчанию через 30 минут; DurationSeconds — от 15 до 30 минут,
запрос большего срока, в том числе стандартного часа SDK, ограничивается 30 минутами). Session token не возвращается и не нужен: это настоящая пара ключей,
запрос подписывается ею обычным образом.
AWS SDK без кода
AWS CLI, boto3, aws-sdk-go-v2 и другие AWS SDK сами выполняют этот обмен: если заданы роль, файл с токеном и эндпоинт STS, они запрашивают ключ перед первым запросом и запрашивают новый до истечения срока. Роль и оба эндпоинта уже лежат в ConfigMap под именами, которые читают SDK, поэтому потребитель добавляет только два пути, зависящих от его пода:
envFrom:
- configMapRef:
name: loki-s3-connection
env:
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/system-s3/token
- name: AWS_CA_BUNDLE # сейчас оба эндпоинта подписаны одним CA
value: /etc/system-s3/ca/s3-ca.crtс теми же томами s3-token и s3-ca, что и выше. После этого aws s3 cp работает
без --endpoint-url и без ключей в окружении. Четыре ключа AWS_* появляются в
ConfigMap, только пока модуль обслуживает эндпоинт выдачи.
Право спросить — это право в RBAC, и компоненту, который создал
SystemBucket, оно нужно так же, как всем остальным: владение ресурсом — это
право править ресурс, а не читать объекты в его бакете.
# Два субресурса: читать объекты в бакете и править ресурс SystemBucket —
# разные права.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: loki-bucket-access
namespace: d8-monitoring
rules:
- apiGroups: ["storage.deckhouse.io"]
resources: ["systembuckets/read", "systembuckets/write"]
resourceNames: ["loki"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: loki-bucket-access
namespace: d8-monitoring
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: loki-bucket-access}
subjects:
- kind: ServiceAccount
name: loki
namespace: d8-monitoringДругому компоненту, которому нужен тот же бакет, выдаётся своё такое же право с субъектом в его пространстве имён. Ничего между ними не копируется, а отобрать доступ — удалить RoleBinding: выданное истекает само, а следующий запрос получает отказ.
Кто держит доступ к бакету прямо сейчас, видно по метрике
system_s3_temporary_key_expiration_seconds: по серии на каждый неистёкший временный ключ с
метками bucket, access_key_id, subject (ServiceAccount, которому выдан ключ) и write,
значение — время истечения (Unix). Список обновляется раз в минуту; секретной части ключа в нём нет.
Как часто просят учётные данные для каждого бакета, видно по system_s3_sts_requests_total с метками
namespace и name объекта SystemBucket и result (Granted или код ошибки, который получил
клиент). Запрос учитывается за бакетом, только если токен вызывающего принят и SystemBucket
существует; все остальные запросы учитываются с пустыми метками, так что вызывающий не может
наплодить серий, называя несуществующие бакеты.
Полученный ключ надо переиспользовать до истечения срока. Каждый запрос выпускает новый ключ, и он доживает свои не более 30 минут независимо от того, нужен ли ещё клиенту, — в метаданных, которые Garage реплицирует на все узлы. Поэтому на бакет разрешено не больше 50 живых временных ключей: клиент, который просит учётные данные на каждый запрос, упрётся в этот предел и получит отказ с объяснением. Правильный клиент держит один ключ и обновляет его ближе к концу срока.
Имена бакетов
spec.bucketName необязателен. Если он не задан, бакет называется <namespace>-<name>: пространство имён бакетов в бэкенде плоское, а SystemBucket — namespaced, поэтому два компонента, попросивших backups, не должны оказаться в одном бакете.
Задавайте имя явно только тогда, когда конкретное имя нужно чему-то за пределами кластера. Два SystemBucket не могут указывать на один бакет. Второй ресурс создаётся, но контроллер его не обслуживает: он показывает BucketReady=False, reason Pending, с сообщением, в котором указано, какому SystemBucket бакет уже принадлежит (bucket "…" already belongs to SystemBucket <namespace>/<name>: refusing to issue credentials for it …). Такому ресурсу нужно задать другой spec.bucketName. Решение принимает контроллер, и только он, поэтому оно соблюдается и при двух одновременных созданиях.
Отказ здесь не перестраховка: SystemBucket владеет своими данными безраздельно, политики reclaim нет, поэтому два владельца одного бакета — это две нагрузки, пишущие в одно хранилище, и удаление любой из них уносит объекты другой.
Каким бы способом имя ни получилось, оно должно быть таким, какое принимает S3: от 3 до 63 символов, строчные латинские буквы, цифры и дефисы, начинается и заканчивается буквой или цифрой. Проблема возникает именно с выведенным именем, потому что его не набирают, а составляют: namespace и имя ресурса длиной до 63 символов каждое дают в сумме до 127. Если пара слишком длинная, задайте spec.bucketName:
$ kubectl -n d8-observability-long-term-metrics get systembucket prometheus-remote-write-backend \
-o jsonpath='{.status.conditions[?(@.type=="BucketReady")].message}'
the bucket name "d8-observability-long-term-metrics-prometheus-remote-write-backend"
cannot be used: it is 66 characters long, and S3 bucket names stop at 63. With
spec.bucketName unset it is derived as <namespace>-<name>; set spec.bucketName to
something S3 accepts
Такой ресурс создаётся, но контроллер его не обслуживает: до обращения к бэкенду он выставляет BucketReady=False, reason ReconcileFailed, с сообщением выше. Ничего не изменится, пока не задан spec.bucketName; поле неизменяемо, поэтому ресурс нужно удалить и создать заново уже с именем.
Удаление бакета
kubectl -n d8-monitoring delete systembucket lokiУдаляется бакет и все объекты в нём. Ни политики хранения, ни корзины нет — убедитесь, что данные больше не нужны или скопированы в другое место.
Ресурс держит finalizer до тех пор, пока бакет не исчезнет из бэкенда, — то есть недоступность хранилища задержит удаление, а не оставит в нём осиротевшие данные.
Статус и диагностика
В SystemBucket.status.conditions стадии идут по порядку:
| Condition | Что значит False |
|---|---|
BucketReady |
Бакет не удалось создать или зависимость ещё недоступна (WaitingForDependency; подробности в Module/system-s3). |
ConnectionReady |
Не удалось записать ConfigMap с параметрами подключения. |
Ready |
Агрегат перечисленного выше. |
SystemBucket в namespace без лейбла heritage: deckhouse или с именем не вида d8-*/kube-* создаётся, но контроллер его не обслуживает: ничего не выделяется, а ресурс показывает BucketReady=False, reason ReconcileFailed, с сообщением namespace "<namespace>" is not a Deckhouse platform namespace (a d8-* or kube-* name and the label heritage=deckhouse); the system object storage is not available to it.
Общие состояния Garage публикуются только в Module/system-s3.status.conditions, в том числе до создания первого бакета. Условия Deckhouse (IsReady, причины включения и другие) контроллер system-s3 не изменяет; фаза Module.status.phase также остаётся под управлением платформы.
| Condition | Что отражает |
|---|---|
StorageReady |
Запуск, здоровье Garage, quorum и layout. |
PlacementReady |
Размещение реплик, нехватку подходящих узлов, перемещение и сохранение идентичности узлов. |
CapacityReady |
Общую ёмкость PVC, их рост и ошибки CSI; Exhausted — на томе реплики свободно меньше 5 % (реплика остаётся в строю, но не принимает записи, и запросы, которые она обслуживает, завершаются ошибкой); тогда тома растут по шагу в 5 ГиБ, для этого StorageClass должен позволять расширение. |
ReplicationReady |
Соответствие режима HA настройкам; переходы ResizeFreeze, ResizeStop, ResizePurge, ResizeSync. |
MigrationReady |
Миграцию StorageClass: Provisioning, Copying, Verifying, Cutover, Draining, Decommission. |
До первого SystemBucket data plane нет: он создаётся для первого бакета и остаётся, даже если потом удалить все бакеты. В этом состоянии StorageReady равно False с причиной WaitingForFirstBucket (если StorageClass не настроен, сообщение говорит и об этом), остальные четыре условия — True с причиной NoDataPlane, а серий system_s3_store_ready и system_s3_store_capacity_bytes контроллер не публикует, поэтому D8SystemS3StoreNotReady не срабатывает. Первый бакет показывает BucketReady=False с причиной WaitingForDependency, пока запущенный им data plane не начнёт обслуживать запросы. Смена storageClass или режима высокой доступности до первого бакета миграцию не запускает: она меняет только то, что будет создано.
False может означать нормальную операцию в процессе, не только ошибку; подробности находятся в reason и message. Unknown/NotEvaluated означает, что текущая попытка reconcile не дошла до этой проверки. После завершения операции соответствующее условие становится True. Conditions — текущие состояния, не журнал всех событий. При обновлении контроллер убирает старые условия Garage из бакетов; ожидание SystemBucket.StorageReady нужно заменить на Module.StorageReady.
Когда ничего не идёт, сообщения говорят, в каком виде работает хранилище, и состояние читается по одному модулю:
StorageReady=True Serving Garage store is healthy; 3 of 3 replicas ready on huawei-storage-stable
PlacementReady=True Placed one replica per node: debian-0, ubuntu-0, ubuntu-1
ReplicationReady=True Stable 3 replicas, high availability on; the highly available mode follows the number of control-plane nodes
MigrationReady=True Converged the volumes are on StorageClass huawei-storage-stable, the configured one; no migration is pendingПока реплика не готова, StorageReady называет её и узел (not ready: system-garage-i2-1 (on ubuntu-1)).
Условие квоты переименовано из SizeApplied в SizeUpToDate. Старое имя удаляется из статуса при reconcile, в том числе при недоступности Garage. Обновите JSONPath и проверки готовности в потребителях. Имя Prometheus-метрики system_s3_bucket_size_applied сохранено для совместимости.
Само хранилище описывают метрики контроллера; они обновляются при каждом проходе reconcile (по умолчанию раз в 30 секунд). По ним, а не по подам и StatefulSet в d8-system-s3, консоль рисует страницу хранилища:
| Метрика | Метки | Значение |
|---|---|---|
system_s3_replica_ready |
plane, replica, node, serving |
1, если под Garage в состоянии Ready, иначе 0. По серии на каждый существующий под; node пуст, пока под не назначен на узел. |
system_s3_store_info |
storage_class, high_availability, replicas |
1. Форма, в которой обслуживающий кластер работает на самом деле: StorageClass, к которому привязаны его тома, режим по закреплённому фактору репликации и число реплик. Отличается от настроек модуля, пока изменение не применено. |
system_s3_plane_info |
plane, storage_class, high_availability, replicas, serving |
1 для каждого существующего кластера Garage. |
system_s3_condition |
type, status, reason |
1 для текущих статуса и причины каждого из пяти условий Module/system-s3, включая Unknown/NotEvaluated. При смене условия прежняя серия удаляется. |
system_s3_certificate_expiration_seconds |
secret |
Время (Unix), когда истекает конечный сертификат в garage-tls (эндпоинт S3) или webhooks-https-certs (эндпоинт учётных данных). Читается раз в 5 минут; у Secret, который не удалось прочитать или разобрать, серии нет. |
plane — номер кластера Garage; во время миграции их два, и serving="true" отмечает тот, на который указывает эндпоинт. Серии исчезнувших подов и кластеров удаляются. Публикует их только реплика контроллера, держащая leader lease.
Трафик считает TLS-терминатор в каждом поде Garage: через него проходит каждое подключение клиента, а сам Garage считает запросы, но не байты. У этих серий job="system-s3-tls-terminator", pod, node, store и incarnation:
| Метрика | Метки | Значение |
|---|---|---|
system_s3_proxy_received_bytes_total |
port |
Байты от клиентов: 3900 — S3, 3903 — API администрирования. |
system_s3_proxy_sent_bytes_total |
port |
Байты клиентам. |
system_s3_proxy_connections_total |
port |
Подключения клиентов, прошедшие TLS-рукопожатие. |
system_s3_replica_metadata_bytes |
— | Размер базы метаданных Garage на томе реплики (файлы в meta/, meta/db.lmdb/ и meta/snapshots/*/), измеряется при каждом опросе. Для этого терминатор монтирует том только на чтение. |
Байты считаются по открытому HTTP вместе с заголовками и растут по ходу передачи, а не в её конце. Репликации между репликами в них нет.
Запросы S3 считаются ещё и по бакетам. У этих серий, помимо job, pod, node, store и incarnation, есть bucket, namespace и name (а port нет) — бакет и его SystemBucket, как в system_s3_bucket_*:
| Метрика | Метки | Значение |
|---|---|---|
system_s3_proxy_bucket_requests_total |
method, code |
Запросы. method — GET, HEAD, PUT, POST, DELETE, OPTIONS или other; code — класс статуса: 2xx, 3xx, 4xx или 5xx, а 403 отдельно: иначе отказ по ключу потерялся бы среди 404 на отсутствующие объекты. Запрос, на который Garage не ответил, — 5xx (клиент получает 502); запрос, клиент которого ушёл раньше ответа, — 4xx. |
system_s3_proxy_bucket_received_bytes_total |
Байты тел запросов от клиентов: загруженные данные объектов. | |
system_s3_proxy_bucket_sent_bytes_total |
Байты тел ответов клиентам: скачанные данные объектов. | |
system_s3_proxy_bucket_request_duration_seconds |
Гистограмма времени от заголовков запроса до конца ответа, вместе с передачей тела. |
Называются только бакеты существующих ресурсов SystemBucket. Запрос к любому другому бакету считается как bucket="_other" с пустыми namespace и name, а запрос без бакета, например ListBuckets, — как bucket="_none", так что клиент не может добавить серий, придумывая имена бакетов. Новый SystemBucket первые минуту-две считается как _other, пока список в подах Garage не обновится.
Посмотреть на само хранилище:
kubectl get module system-s3 -o json | jq '.status.conditions'
kubectl -n d8-system-s3 get pods,sts,pvc
kubectl -n d8-system-s3 logs deploy/controller -c controllerМетрики и алерты Garage
Prometheus собирает /metrics каждого пода Garage всех плоскостей данных с job system-s3-garage. У каждой серии есть метки pod, node, store и incarnation; во время миграции между StorageClass собираются обе плоскости, и различаются они по incarnation. Для сбора используется отдельный токен из Secret garage-metrics-token в d8-system-s3: он открывает только /metrics, токена администратора у Prometheus нет. Контроллер создаёт Secret до первого пода Garage и никогда его не перезаписывает; Garage читает токен при запуске.
Дашборд System S3 Garage показывает S3-запросы, ошибки и задержки p95/p99 по операциям, скорость и длительность чтения и записи блоков, очередь resync (повторной синхронизации блоков), свободное место на каждой реплике и состояние кластера так, как его видят узлы.
| Алерт | Когда срабатывает |
|---|---|
D8SystemS3GarageClusterUnavailable |
Ни у одного узла плоскости нет кворума по всем партициям в течение минуты: чтение и запись не работают. |
D8SystemS3GarageClusterDegraded |
Узел хранения отключён, но плоскость продолжает обслуживать запросы, в течение 5 минут. Хранилище с одной репликой в это состояние не попадает. |
D8SystemS3GarageNodeDisconnected |
Узла из layout (раскладки данных по узлам) не видно дольше 5 минут. |
D8SystemS3GarageServerErrors |
Больше десяти S3-ответов 5xx за 5 минут (501 — функция, которой нет в Garage, — не считается). |
D8SystemS3GarageResyncErrors |
У реплики 15 минут есть блоки данных, которые не удалось синхронизировать. |
D8SystemS3GarageDiskAlmostFull |
На томе реплики меньше 10% свободного места в течение 10 минут. |
D8SystemS3GarageMetricsUnavailable |
Под Garage не удаётся опросить 15 минут; алерты выше его не видят. |
D8SystemS3GarageQueuesStalled |
Очередь resync, Merkle или GC узла 2 часа не опустела и ни разу не уменьшилась (для GC — 25 часов: Garage откладывает её на сутки). |
В описании каждого алерта сказано, что проверить и что делать.
Алерты хранилища, учётных данных и узлов
| Алерт | Когда срабатывает |
|---|---|
D8SystemS3ReplicaNotReady |
Реплика обслуживающего кластера Garage 15 минут не в состоянии Ready, хотя хранилище обслуживает запросы. |
D8SystemS3PlacementBlocked |
PlacementReady 30 минут в состоянии False с причиной PlacementBlocked: реплике негде работать. |
D8SystemS3TooManyTemporaryKeys |
Больше 500 временных ключей живы одновременно в течение 15 минут: какой-то клиент просит ключ на каждый запрос. |
D8SystemS3CertificateExpiring |
Сертификат в garage-tls или webhooks-https-certs истекает меньше чем через 30 дней (их обновляют за полгода, значит, обновление не сработало). |
D8SystemS3DrainBlocked |
PodDisruptionBudget Garage 30 минут не допускает ни одного прерывания: drain (вывод узла из работы) ждёт отсутствующую реплику. |
D8SystemS3ReplicaHogsNode |
Контейнер Garage 15 минут использует больше половины выделяемых (allocatable) CPU или памяти своего узла. |
D8SystemS3NodeDiskSaturated |
Диск узла занят больше 90% времени, пока Garage на этом узле читает и пишет блоки быстрее 10 МиБ/с, в течение 15 минут. |
У серий контроллера нет метки namespace, а серии node-exporter относятся к узлу; эти правила ставят на свои алерты namespace="d8-system-s3", чтобы их пропускал фильтр платформы для алертов уровня кластера. Дашборд System S3 показывает условия модуля, оставшийся срок сертификатов, число живых временных ключей и запросы учётных данных по бакетам; System S3 Garage — очереди таблиц и размер метаданных каждой реплики.
Уровень сервиса (SLO)
Хранилище обещает по каждой операции S3 в окне 5 минут:
| Операция | Обещание | Алерт |
|---|---|---|
GetObject |
p99 времени до первого байта ≤ 250 мс | D8SystemS3SLOReadLatency |
PutObject, объекты до 1 МиБ |
p99 ≤ 500 мс | D8SystemS3SLOWriteLatency |
HeadObject |
p99 ≤ 100 мс | D8SystemS3SLOHeadLatency |
ListObjectsV2 (одна страница) |
p99 ≤ 1 с | D8SystemS3SLOListLatency |
| все запросы | ≥ 99,9 % без серверной ошибки | D8SystemS3SLOAvailability |
Пороги в 3–5 раз выше того, что показало нагрузочное тестирование хранилища из трёх реплик на сетевом и на привязанном к узлу StorageClass. Алерт срабатывает через 15 минут выше порога и только пока хранилище получает хотя бы один такой запрос раз в 10 секунд. PutObject замеряется до сохранения всего тела, а размера объекта в метриках нет, поэтому потребитель, загружающий большие объекты, может поднять перцентиль записи без всякой проблемы; как это отличить, сказано в описании алерта.
Выключение модуля и снос кластера
Выключение защищено: Deckhouse не примет enabled: false, пока на ModuleConfig нет подтверждения, — каждый компонент с SystemBucket на время выключения остаётся без хранилища.
kubectl annotate moduleconfig system-s3 modules.deckhouse.io/allow-disabling=true
kubectl patch moduleconfig system-s3 --type merge -p '{"spec":{"enabled":false}}'Что остаётся после выключения, а что нет:
- namespace
d8-system-s3остаётся (helm.sh/resource-policy: keep), а с ним и данные: StatefulSet’ы Garage, масштабированные в ноль, ихPersistentVolumeClaimи Secret’ы с RPC-секретом кластера, admin-токеном и идентичностями узлов, а также CA модуля (system-s3-ca), поэтому CA в ConfigMap подключения потребителей остаётся действительным. Job’ы копировщика незавершённой миграции удаляются; миграция продолжится с сохранённого состояния после включения модуля; - уходит то, что отрисовал чарт: контроллер, его сервисы, эндпоинт выдачи ключей;
- ресурсы
SystemBucketостаются — замороженными в последнем статусе, — и их CRD тоже: выключение модуля не удаляет ни то, ни другое. Свои финализаторы модуль при выгрузке снимает, поэтому их можно удалить вручную; - конфигурация admission-вебхука для
SystemBucket, оставшаяся от прежней версии модуля, удаляется: вебхук, у которого не осталось бэкенда, блокировал бы работу с ресурсами, которые он стерёг; - оставшееся перестаёт принадлежать Deckhouse: лейбл
heritage: deckhouseснимается с namespace и со всех объектов в нём, у которых он есть (в том числе с CA модуля), а вместо него ставится лейблsystem-s3.deckhouse.io/released-heritage: deckhouse. Политика допуска Deckhouselabel-objects.deckhouse.ioникому, кроме самого Deckhouse, не даёт менять и удалять объекты сheritage: deckhouse; без этого лейбла администратор кластера может удалить namespace с обычными правами. Остальные лейблы namespace остаются.
После повторного включения хранилище такое же, каким было: heritage: deckhouse возвращается ровно на те объекты, у которых есть лейбл released-heritage (объекты, созданные в namespace за это время, не трогаются), контроллер возвращает StatefulSet’ам реплики, кластер поднимается на тех же томах, бакеты и данные на месте.
Чтобы удалить данные, удалите namespace при выключенном модуле:
kubectl delete namespace d8-system-s3Дальше судьбу томов решает reclaim policy StorageClass: Delete удаляет их вместе со всем содержимым, Retain сохраняет, но модуль, включённый после этого, начинает с пустого хранилища и такие тома не подключает.
Перед сносом кластера выключите модуль и удалите его namespace. dhctl destroy удаляет все PersistentVolumeClaim и ждёт их исчезновения. При выключенном модуле data plane стоит на нуле реплик и claim’ы не пересоздаёт, но удаление namespace заранее — это явное решение, что данные должны уйти.
Пока модуль находится на стадии Experimental, для повторного включения после выключения нужно разрешение платформы: добавьте system-s3 в spec.settings.allowedExperimentalModules в ModuleConfig deckhouse (или включите allowExperimentalModules: true) до enabled: true.