diff --git a/sqs-operator/internal/controller/queueservice_controller.go b/sqs-operator/internal/controller/queueservice_controller.go index 8438778..44832c4 100644 --- a/sqs-operator/internal/controller/queueservice_controller.go +++ b/sqs-operator/internal/controller/queueservice_controller.go @@ -38,8 +38,9 @@ import ( const ( // sqsFinalizer — добавляется к QueueService CR для управления cleanup при удалении. sqsFinalizer = "sqs.kube5s.ru/sqs-finalizer" - // elasticMQImage — образ ElasticMQ Native (GraalVM, ~70MB, старт <0.5с). - elasticMQImage = "softwaremill/elasticmq-native:1.7.1" + // elasticMQImage — образ ElasticMQ JVM (полный, с поддержкой H2 JDBC persistence). + // Native-образ (elasticmq-native) не включает H2 в GraalVM native image — persistence не работает. + elasticMQImage = "softwaremill/elasticmq:1.7.1" // elasticMQPort — порт на котором ElasticMQ слушает SQS HTTP запросы. elasticMQPort = 9324 ) @@ -379,7 +380,8 @@ func (r *QueueServiceReconciler) ensureConfigMap(ctx context.Context, qs *sqsv1a Labels: sqsLabels(qs.Spec.TenantID), }, Data: map[string]string{ - "custom.conf": conf, + // Ключ соответствует subPath при монтировании в pod. + "elasticmq.conf": conf, }, } return r.Create(ctx, cm) @@ -427,8 +429,21 @@ func (r *QueueServiceReconciler) ensureDeployment(ctx context.Context, qs *sqsv1 replicas := int32(1) port := int32(elasticMQPort) - memLimit := resource.MustParse(fmt.Sprintf("%dMi", qs.Spec.MemoryMB)) - memRequest := resource.MustParse("32Mi") + // JVM-образ ElasticMQ требует минимум ~150MB. Форсируем нижнюю границу 256Mi. + // MemoryMB из spec — лимит; request = половина лимита, но не меньше 128Mi. + memMB := qs.Spec.MemoryMB + if memMB < 256 { + memMB = 256 + } + memLimit := resource.MustParse(fmt.Sprintf("%dMi", memMB)) + memReqMB := memMB / 2 + if memReqMB < 128 { + memReqMB = 128 + } + memRequest := resource.MustParse(fmt.Sprintf("%dMi", memReqMB)) + // -Xmx = 75% от лимита, чтобы JVM не выбил OOMKill при GC overhead. + xmxMB := memMB * 3 / 4 + jvmOpts := fmt.Sprintf("-Xmx%dm -Xms64m", xmxMB) cpuRequest := resource.MustParse("10m") cpuLimit := resource.MustParse("500m") @@ -450,6 +465,11 @@ func (r *QueueServiceReconciler) ensureDeployment(ctx context.Context, qs *sqsv1 Labels: sqsLabels(qs.Spec.TenantID), }, Spec: corev1.PodSpec{ + // fsGroup=999 — uid elasticmq в JVM-образе. + // Kubernetes при монтировании PVC сделает chown :999 на /data. + SecurityContext: &corev1.PodSecurityContext{ + FSGroup: func() *int64 { v := int64(999); return &v }(), + }, Containers: []corev1.Container{ { Name: "elasticmq", @@ -457,12 +477,12 @@ func (r *QueueServiceReconciler) ensureDeployment(ctx context.Context, qs *sqsv1 Ports: []corev1.ContainerPort{ {ContainerPort: port, Protocol: corev1.ProtocolTCP}, }, - // ElasticMQ Native читает конфиг через системное свойство JVM. - // Путь должен совпадать с mountPath в VolumeMounts. + // JVM-образ имеет ENTRYPOINT: java -Dconfig.file=/opt/elasticmq.conf -jar ... + // JAVA_TOOL_OPTIONS задаёт Xmx чтобы JVM не выбил OOMKill ниже k8s лимита. Env: []corev1.EnvVar{ { Name: "JAVA_TOOL_OPTIONS", - Value: "-Dconfig.file=/opt/elasticmq/custom.conf", + Value: jvmOpts, }, }, Resources: corev1.ResourceRequirements{ @@ -477,10 +497,10 @@ func (r *QueueServiceReconciler) ensureDeployment(ctx context.Context, qs *sqsv1 }, VolumeMounts: []corev1.VolumeMount{ { - // ConfigMap монтируем как единственный файл custom.conf + // Монтируем конфиг на путь который JVM-образ читает по дефолту. Name: "elasticmq-config", - MountPath: "/opt/elasticmq/custom.conf", - SubPath: "custom.conf", + MountPath: "/opt/elasticmq.conf", + SubPath: "elasticmq.conf", ReadOnly: true, }, { @@ -574,7 +594,7 @@ func (r *QueueServiceReconciler) ensureService(ctx context.Context, qs *sqsv1alp } // ensureIngress создаёт Ingress с path-based routing для тенанта. -// Путь: sqs.kube5s.ru/sqs/{tenantId} → Service :9324 +// Путь: sqs.kube5s.ru/sqs/{tenantId} → Service :9324 (без rewrite — ElasticMQ JVM слушает с context-path). // Nginx Ingress Controller автоматически мерджит правила от разных Ingress объектов. func (r *QueueServiceReconciler) ensureIngress(ctx context.Context, qs *sqsv1alpha1.QueueService, ns, name, svcName string) error { ing := &netv1.Ingress{} @@ -584,6 +604,7 @@ func (r *QueueServiceReconciler) ensureIngress(ctx context.Context, qs *sqsv1alp return fmt.Errorf("get ingress: %w", err) } + // ImplementationSpecific нужен для regex, Prefix — для простого prefix-matching без rewrite. pathType := netv1.PathTypePrefix svcPort := int32(elasticMQPort) @@ -593,14 +614,12 @@ func (r *QueueServiceReconciler) ensureIngress(ctx context.Context, qs *sqsv1alp Namespace: ns, Labels: sqsLabels(qs.Spec.TenantID), Annotations: map[string]string{ - // Nginx rewrite: убираем prefix /sqs/{tenantId} перед проксированием в ElasticMQ. - // ElasticMQ получает чистый SQS path (например /?Action=CreateQueue). - "nginx.ingress.kubernetes.io/rewrite-target": "/$2", + // Без rewrite-target: ElasticMQ JVM слушает по полному пути /sqs/{tenantId}/... + // (context-path в конфиге определяет listen path у JVM образа, в отличие от native). "nginx.ingress.kubernetes.io/proxy-read-timeout": "60", "nginx.ingress.kubernetes.io/proxy-send-timeout": "60", - // Увеличиваем лимит тела запроса для больших сообщений - "nginx.ingress.kubernetes.io/proxy-body-size": "10m", - "cert-manager.io/cluster-issuer": "letsencrypt-prod", + "nginx.ingress.kubernetes.io/proxy-body-size": "10m", + "cert-manager.io/cluster-issuer": "letsencrypt-prod", }, }, Spec: netv1.IngressSpec{ @@ -618,9 +637,9 @@ func (r *QueueServiceReconciler) ensureIngress(ctx context.Context, qs *sqsv1alp HTTP: &netv1.HTTPIngressRuleValue{ Paths: []netv1.HTTPIngressPath{ { - // Regex capture group ($2) передаётся в rewrite-target. - // /sqs/{tenantId}(/|$)(.*) → /$2 - Path: "/sqs/" + qs.Spec.TenantID + "(/|$)(.*)", + // Простой prefix: /sqs/{tenantId} форвардится как есть в ElasticMQ. + // ElasticMQ JVM обрабатывает полный путь включая context-path. + Path: "/sqs/" + qs.Spec.TenantID, PathType: &pathType, Backend: netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ diff --git a/sqs-operator/internal/elasticmq/elasticmq_config.go b/sqs-operator/internal/elasticmq/elasticmq_config.go index f1e7c0d..bf27e2d 100644 --- a/sqs-operator/internal/elasticmq/elasticmq_config.go +++ b/sqs-operator/internal/elasticmq/elasticmq_config.go @@ -1,7 +1,7 @@ // Изменён: 2026-04-07 // elasticmq_config.go — генератор HOCON конфигурации для ElasticMQ. // Каждый тенант получает уникальный конфиг: свой context-path, accountId, persistence. -// Конфиг монтируется в pod как ConfigMap → /opt/elasticmq/custom.conf. +// Конфиг монтируется в pod как ConfigMap → /opt/elasticmq.conf (путь из ENTRYPOINT JVM-образа). package elasticmq