-
Notifications
You must be signed in to change notification settings - Fork 4.8k
HIVE-30033: Hive K8s operator: log replica changes #6773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,7 @@ | |
| import org.apache.hive.kubernetes.operator.model.status.ComponentStatus; | ||
| import org.apache.hive.kubernetes.operator.util.ConfigUtils; | ||
| import org.apache.hive.kubernetes.operator.util.Labels; | ||
| import org.apache.hive.kubernetes.operator.util.Workloads; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
|
|
@@ -593,18 +594,7 @@ private static int getMinScrapeInterval(HiveClusterSpec spec) { | |
| private void patchReplicas(KubernetesClient client, HiveCluster resource, | ||
| String component, int replicas) { | ||
| String namespace = resource.getMetadata().getNamespace(); | ||
| // Component keys use prefixes: "llap-{name}" → workload "{cluster}-{name}", | ||
| // "tezam-{name}" → workload "{cluster}-tezam-{name}". | ||
| String workloadName; | ||
| if (component.startsWith(ConfigUtils.COMPONENT_LLAP + "-")) { | ||
| String llapName = component.substring(ConfigUtils.COMPONENT_LLAP.length() + 1); | ||
| workloadName = resource.getMetadata().getName() + "-" + llapName; | ||
| } else if (component.startsWith(ConfigUtils.COMPONENT_TEZAM + "-")) { | ||
| String llapName = component.substring(ConfigUtils.COMPONENT_TEZAM.length() + 1); | ||
| workloadName = resource.getMetadata().getName() + "-tezam-" + llapName; | ||
| } else { | ||
| workloadName = resource.getMetadata().getName() + "-" + component; | ||
| } | ||
| String workloadName = Workloads.nameFor(resource, component); | ||
| try { | ||
| if (component.startsWith(ConfigUtils.COMPONENT_LLAP + "-")) { | ||
| client.apps().statefulSets().inNamespace(namespace).withName(workloadName).scale(replicas); | ||
|
|
@@ -617,6 +607,27 @@ private void patchReplicas(KubernetesClient client, HiveCluster resource, | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Reads the workload's current replica count from the API server and hands it to | ||
| * {@link Workloads#logReplicaChange}, so an SSA-driven scale (a user editing | ||
| * spec.llapClusters[i].replicas, a helm upgrade rewriting it) logs the same line the dependents' | ||
| * SSA does instead of reaching the StatefulSet/Deployment silently. Only the read is local to | ||
| * this class; the message and the "log nothing when unchanged" rule are shared. Read failures | ||
| * are swallowed at DEBUG: the SSA below runs either way, and a missing pre-scale line is not | ||
| * worth failing the reconcile over. | ||
| */ | ||
| private void logReplicaChange(KubernetesClient client, String ns, String workloadName, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we generalize log method or need both?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both |
||
| String component, int desired, boolean isStatefulSet) { | ||
| try { | ||
| HasMetadata current = isStatefulSet | ||
| ? client.apps().statefulSets().inNamespace(ns).withName(workloadName).get() | ||
| : client.apps().deployments().inNamespace(ns).withName(workloadName).get(); | ||
| Workloads.logReplicaChange(LOG, component, ns, workloadName, current, desired); | ||
| } catch (Exception e) { | ||
| LOG.debug("Could not read current replicas for {}/{}: {}", ns, workloadName, e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| private void patchSuspendSpec(KubernetesClient client, HiveCluster resource, boolean suspend) { | ||
| String ns = resource.getMetadata().getNamespace(); | ||
| String name = resource.getMetadata().getName(); | ||
|
|
@@ -668,6 +679,9 @@ private void reconcileLlapClusters(HiveCluster resource, KubernetesClient client | |
| // brief scale-up-then-down on first create (K8s defaults to 1 if omitted). | ||
| // resolveLlapReplicaCount already reads the autoscaler's managed value, | ||
| // so this is always the correct replica count. | ||
| String llapWorkload = LlapResourceBuilder.resourceName(resource, llapSpec); | ||
| logReplicaChange(client, ns, llapWorkload, ConfigUtils.COMPONENT_LLAP, replicas, | ||
| /*isStatefulSet=*/true); | ||
| client.apps().statefulSets().inNamespace(ns) | ||
| .resource(LlapResourceBuilder.buildStatefulSet(resource, llapSpec, replicas)) | ||
| .forceConflicts() | ||
|
|
@@ -687,6 +701,9 @@ private void reconcileLlapClusters(HiveCluster resource, KubernetesClient client | |
| client.services().inNamespace(ns) | ||
| .resource(LlapResourceBuilder.buildTezAmService(resource, llapSpec)) | ||
| .serverSideApply(); | ||
| String tezAmWorkload = LlapResourceBuilder.tezAmResourceName(resource, llapSpec); | ||
| logReplicaChange(client, ns, tezAmWorkload, ConfigUtils.COMPONENT_TEZAM, tezAmReplicas, | ||
| /*isStatefulSet=*/false); | ||
| client.apps().deployments().inNamespace(ns) | ||
| .resource(LlapResourceBuilder.buildTezAmDeployment(resource, llapSpec, tezAmReplicas)) | ||
| .forceConflicts() | ||
|
|
@@ -910,8 +927,8 @@ private boolean isClusterIdle(HiveCluster resource, KubernetesClient client) { | |
| if (spec.tezAm().isEnabled()) { | ||
| for (var llap : spec.llapClusters()) { | ||
| if (llap.isEnabled() | ||
| && !isAtMinReplicas(client, ns, name + "-tezam-" + llap.name(), false, | ||
| llap.tezAm().autoscaling().minReplicas())) { | ||
| && !isAtMinReplicas(client, ns, LlapResourceBuilder.tezAmResourceName(resource, llap), | ||
| false, llap.tezAm().autoscaling().minReplicas())) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| package org.apache.hive.kubernetes.operator.util; | ||
|
|
||
| import java.util.OptionalInt; | ||
|
|
||
| import io.fabric8.kubernetes.api.model.HasMetadata; | ||
| import io.fabric8.kubernetes.api.model.apps.Deployment; | ||
| import io.fabric8.kubernetes.api.model.apps.StatefulSet; | ||
| import org.apache.hive.kubernetes.operator.model.HiveCluster; | ||
| import org.slf4j.Logger; | ||
|
|
||
| /** | ||
| * Helpers for the operator's workloads (Deployment/StatefulSet): reading fields without the | ||
| * null-guard boilerplate every caller would otherwise repeat, logging replica changes in one | ||
| * shape, and resolving the workload's K8s name from the autoscaler's component key. | ||
| */ | ||
| public final class Workloads { | ||
|
|
||
| private Workloads() {} | ||
|
|
||
| /** | ||
| * Returns spec.replicas from a Deployment or StatefulSet. Empty when the resource is absent, | ||
| * has no spec, or the field is unset — in practice that means "the workload isn't there yet", | ||
| * since the API server defaults spec.replicas on write. A non-workload resource is empty too. | ||
| */ | ||
| public static OptionalInt replicas(HasMetadata resource) { | ||
| Integer replicas = null; | ||
| if (resource instanceof Deployment d && d.getSpec() != null) { | ||
| replicas = d.getSpec().getReplicas(); | ||
| } else if (resource instanceof StatefulSet s && s.getSpec() != null) { | ||
| replicas = s.getSpec().getReplicas(); | ||
| } | ||
| return replicas == null ? OptionalInt.empty() : OptionalInt.of(replicas); | ||
| } | ||
|
|
||
| /** | ||
| * Logs the replica count the operator is about to apply to {@code namespace/name}. One line | ||
| * covers both cases: {@code current} is the workload as it exists in the cluster, or null when | ||
| * it doesn't exist yet, which logs as {@code none -> N}. Nothing is logged when the count | ||
| * already matches, so silence means no scale is happening. | ||
| * <p> | ||
| * Shared by every scale path — the dependents' SSA and the imperative LLAP/TezAM SSAs — so the | ||
| * operator log reads the same whichever one ran. The caller passes its own logger to keep the | ||
| * log category pointing at the code that is actually scaling. | ||
| */ | ||
| public static void logReplicaChange(Logger log, String component, String namespace, String name, | ||
| HasMetadata current, int desired) { | ||
| OptionalInt actual = replicas(current); | ||
| if (actual.isPresent() && actual.getAsInt() == desired) { | ||
| return; | ||
| } | ||
| log.info("Setting replica count for {} {}/{}: {} -> {}", component, namespace, name, | ||
| actual.isPresent() ? String.valueOf(actual.getAsInt()) : "none", desired); | ||
| } | ||
|
|
||
| /** | ||
| * Maps an autoscaler component key to the K8s workload name it drives. Per-LLAP components | ||
| * carry the LLAP cluster name in their key ("llap-{name}", "tezam-{name}"); everything else | ||
| * (HS2, Metastore) is a plain "{cluster}-{component}". Kept here so every scale path — the | ||
| * autoscaler's `patchReplicas`, the imperative LLAP/TezAM SSAs, and the idle-check reads — | ||
| * resolves the name the same way. | ||
| * <ul> | ||
| * <li>{@code llap-{name}} → {@code {cluster}-{name}}</li> | ||
| * <li>{@code tezam-{name}} → {@code {cluster}-tezam-{name}}</li> | ||
| * <li>otherwise → {@code {cluster}-{component}}</li> | ||
| * </ul> | ||
| */ | ||
| public static String nameFor(HiveCluster hc, String component) { | ||
| String cluster = hc.getMetadata().getName(); | ||
| if (component.startsWith(ConfigUtils.COMPONENT_LLAP + "-")) { | ||
| String llapName = component.substring(ConfigUtils.COMPONENT_LLAP.length() + 1); | ||
| return cluster + "-" + llapName; | ||
| } | ||
| if (component.startsWith(ConfigUtils.COMPONENT_TEZAM + "-")) { | ||
| String llapName = component.substring(ConfigUtils.COMPONENT_TEZAM.length() + 1); | ||
| return cluster + "-" + ConfigUtils.COMPONENT_TEZAM + "-" + llapName; | ||
| } | ||
| return cluster + "-" + component; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For TezAM we have statefullset, not deployment, no? That actually makes me wonder why we have deployment for LLAP that doesn't allow parallel botstrap and rolling upgrade is sequential
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LlapResourceBuilder.buildTezAmDeployment returns a Deployment, and LLAP is already a StatefulSet