Skip to content

Commit 2a3caf2

Browse files
committed
refactor and add docs
Signed-off-by: zhaohehuhu <luoyedeyi@163.com>
1 parent 31ee623 commit 2a3caf2

7 files changed

Lines changed: 209 additions & 45 deletions

File tree

config/crd/bases/starrocks.com_starrocksclusters.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13602,8 +13602,11 @@ spec:
1360213602
of nodeSelectors to match when scheduling pods on nodes"
1360313603
type: object
1360413604
observerSpec:
13605-
description: ObserverSpec configures optional FE observer pods.
13606-
Observer pods reuse the generic FE resources.
13605+
description: |-
13606+
ObserverSpec configures optional FE observer pods. Observer pods reuse the generic FE resources.
13607+
The FE image must be StarRocks 4.1.0 or later. Removing observers does not automatically
13608+
run ALTER SYSTEM DROP OBSERVER; manually drop observer nodes
13609+
if SHOW FRONTENDS reports Alive = false.
1360713610
properties:
1360813611
enabled:
1360913612
description: Enabled controls whether FE observer should be

doc/scale_fe_observer_howto.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Scale FE observer Howto
2+
3+
## Overview
4+
5+
FE observers are mainly used to increase the query concurrency of a StarRocks cluster. Unlike FE followers, observers do
6+
not participate in leader election, so adding observers does not add leader selection pressure to the cluster.
7+
8+
Use `starRocksFeSpec.observerSpec` when the cluster needs more FE read capacity without changing the FE follower quorum.
9+
Observers can be scaled with the read workload, while FE followers should still be scaled carefully because they store
10+
metadata and participate in quorum.
11+
12+
## How to correctly scale out FE read workloads by observer
13+
14+
FE observers can be used to scale FE read workloads without changing the FE follower quorum. To use
15+
`starRocksFeSpec.observerSpec`, the FE image configured in `starRocksFeSpec.image` must be StarRocks 4.1.0 or later.
16+
The operator rejects FE observer deployment for older FE image versions because those images do not enable FE observer.
17+
18+
To scale FE observers out, enable `observerSpec` and set the desired observer count:
19+
20+
```yaml
21+
starRocksFeSpec:
22+
image: "starrocks/fe-ubuntu:4.1.0"
23+
observerSpec:
24+
enabled: true
25+
observerNumber: 2
26+
```
27+
28+
## How to correctly scale in FE observer
29+
30+
Known limitation: neither the operator nor `fe_entrypoint.sh` runs `ALTER SYSTEM DROP OBSERVER` when observers are
31+
removed. If you decrease `starRocksFeSpec.observerSpec.observerNumber` or set
32+
`starRocksFeSpec.observerSpec.enabled: false`, the observer Pods are removed from Kubernetes, but the removed observers
33+
can remain in StarRocks metadata and appear as permanent `Alive=false` rows in `SHOW FRONTENDS`. Because observers are
34+
intended for dynamic read-workload scaling, stale observer rows can accumulate after repeated scale-in operations.
35+
36+
For planned observer scale-in, manually drop the observers that will be removed before lowering `observerNumber`.
37+
38+
1. Execute `SHOW FRONTENDS` and find the `OBSERVER` rows for the observer Pods that will be removed.
39+
40+
2. Drop each observer from StarRocks metadata by using its FE host and `EditLogPort` from `SHOW FRONTENDS`.
41+
```sql
42+
mysql> ALTER SYSTEM DROP OBSERVER "<observer-host>:<edit-log-port>";
43+
```
44+
45+
For example:
46+
```sql
47+
mysql> ALTER SYSTEM DROP OBSERVER "kube-starrocks-fe-observer-1.kube-starrocks-fe-observer-search.default.svc.cluster.local:9010";
48+
```
49+
50+
3. Lower `starRocksFeSpec.observerSpec.observerNumber`, or set `starRocksFeSpec.observerSpec.enabled: false` if all
51+
observers should be removed.
52+
53+
4. Execute `SHOW FRONTENDS` again and verify the removed observer rows are gone.
54+
55+
If the observer Pods were already removed from Kubernetes, connect to an FE node, run `SHOW FRONTENDS`, find `OBSERVER`
56+
rows with `Alive=false`, and run `ALTER SYSTEM DROP OBSERVER "<observer-host>:<edit-log-port>"` for each stale row.

helm-charts/charts/kube-starrocks/charts/starrocks/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@ starrocksFESpec:
515515
shareProcessNamespace:
516516
# Optional FE observer pods. Observer pods reuse the FE image, resources, scheduling,
517517
# config, env, storage, and services from this starrocksFESpec.
518+
# FE observer requires the FE image tag to be StarRocks 4.1.0 or later.
519+
# Known limitation: scaling observerNumber down or disabling observerSpec removes
520+
# Kubernetes pods, but does not run ALTER SYSTEM DROP OBSERVER. Manually drop
521+
# observer nodes if SHOW FRONTENDS reports Alive = false.
518522
observerSpec:
519523
# specify the FE observer deployment or not.
520524
enabled: false

helm-charts/charts/kube-starrocks/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,10 @@ starrocks:
631631
shareProcessNamespace:
632632
# Optional FE observer pods. Observer pods reuse the FE image, resources, scheduling,
633633
# config, env, storage, and services from this starrocksFESpec.
634+
# FE observer requires the FE image tag to be StarRocks 4.1.0 or later.
635+
# Known limitation: scaling observerNumber down or disabling observerSpec removes
636+
# Kubernetes pods, but does not run ALTER SYSTEM DROP OBSERVER. Manually drop
637+
# observer nodes if SHOW FRONTENDS reports Alive = false.
634638
observerSpec:
635639
# specify the FE observer deployment or not.
636640
enabled: false

pkg/k8sutils/templates/pod/spec.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ func Envs(spec v1.SpecInterface, config map[string]interface{},
173173
Name: v1.IS_FE_OBSERVER,
174174
Value: "true",
175175
},
176+
{
177+
Name: v1.FE_SERVICE_NAME,
178+
Value: feExternalServiceName,
179+
},
176180
} {
177181
addEnv(envVar)
178182
}

pkg/subcontrollers/feobserver/feobserver_controller.go

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ package feobserver
1818

1919
import (
2020
"context"
21+
"fmt"
2122

23+
"github.com/StarRocks/starrocks-kubernetes-operator/pkg/k8sutils/templates/service"
2224
"github.com/go-logr/logr"
2325
appsv1 "k8s.io/api/apps/v1"
2426
corev1 "k8s.io/api/core/v1"
2527
apierrors "k8s.io/apimachinery/pkg/api/errors"
2628
"k8s.io/apimachinery/pkg/types"
29+
"k8s.io/apimachinery/pkg/util/intstr"
2730
"k8s.io/client-go/tools/record"
2831
"sigs.k8s.io/controller-runtime/pkg/client"
2932

@@ -39,6 +42,8 @@ import (
3942
"github.com/StarRocks/starrocks-kubernetes-operator/pkg/subcontrollers/fe"
4043
)
4144

45+
const minFeObserverImageVersion = "4.1.0"
46+
4247
type FeObserverController struct {
4348
Client client.Client
4449
Recorder record.EventRecorder
@@ -64,13 +69,12 @@ func (fc *FeObserverController) SyncCluster(ctx context.Context, src *srapi.Star
6469
feSpec := src.Spec.StarRocksFeSpec
6570
observerSpec := feSpec.ToObserverSpec()
6671
if observerSpec == nil {
67-
logger.Info("fe observer is disabled, clear observer resources")
68-
if err := fc.clearObserverResources(ctx, src); err != nil {
69-
logger.Error(err, "clear fe observer resources failed")
70-
return err
71-
}
72+
logger.Info("src.Spec.StarRocksFeObserverSpec == nil, skip sync fe observer")
7273
return nil
7374
}
75+
if src.Spec.StarRocksFeSpec == nil {
76+
return fmt.Errorf("starRocksFeSpec is required before deploying fe observer")
77+
}
7478

7579
var err error
7680
defer func() {
@@ -84,6 +88,10 @@ func (fc *FeObserverController) SyncCluster(ctx context.Context, src *srapi.Star
8488
return err
8589
}
8690

91+
if !fe.CheckFEReady(ctx, fc.Client, src.Namespace, src.Name) {
92+
logger.Info("FE is not ready, stop sync fe observer")
93+
return nil
94+
}
8795
// get the fe configMap for resolve ports
8896
logger.V(log.DebugLevel).Info("get fe configMap to resolve ports", "ConfigMapInfo", feSpec.ConfigMapInfo)
8997
feConfig, err := fe.GetFEConfig(ctx, fc.Client, feSpec, src.Namespace)
@@ -95,30 +103,44 @@ func (fc *FeObserverController) SyncCluster(ctx context.Context, src *srapi.Star
95103
// generate new fe observer statefulset.
96104
logger.V(log.DebugLevel).Info("build fe observer statefulset", "StarRocksCluster", src)
97105
object := object.NewFromCluster(src)
98-
106+
defaultLabels := load.Labels(src.Name, observerSpec)
107+
svc := rutils.BuildExternalService(object, observerSpec, feConfig, load.Selector(src.Name, observerSpec), defaultLabels)
108+
searchServiceName := service.SearchServiceName(src.Name, observerSpec)
109+
internalService := service.MakeSearchService(searchServiceName, &svc, []corev1.ServicePort{
110+
{
111+
Name: "query-port",
112+
Port: rutils.GetPort(feConfig, rutils.QUERY_PORT),
113+
TargetPort: intstr.FromInt(int(rutils.GetPort(feConfig, rutils.QUERY_PORT))),
114+
AppProtocol: func() *string { mysql := "mysql"; return &mysql }(),
115+
},
116+
}, defaultLabels)
99117
podTemplateSpec, err := fc.buildPodTemplate(src, feConfig)
100118
if err != nil {
101119
logger.Error(err, "build pod template failed")
102120
return err
103121
}
104122
expectSts := statefulset.MakeStatefulset(object, observerSpec, podTemplateSpec)
105-
expectSts.Spec.ServiceName = feSearchServiceName(src.Name)
106123
err = k8sutils.ApplyStatefulSet(ctx, fc.Client, &expectSts, false, rutils.StatefulSetDeepEqual)
107124
if err != nil {
108125
logger.Error(err, "fe observer statefulset failed", "StarRocksCluster", src)
109126
return err
110127
}
111128

112-
if err = fc.deleteLegacyObserverServices(ctx, src); err != nil {
113-
logger.Error(err, "delete legacy fe observer services failed")
129+
if err = k8sutils.ApplyService(ctx, fc.Client, internalService, rutils.ServiceDeepEqual); err != nil {
130+
logger.Error(err, "deploy search service failed", "internalService", internalService)
131+
fc.Recorder.Event(src, corev1.EventTypeWarning, "DeployFeObserverFailed", err.Error())
114132
return err
115133
}
116134

135+
if err = k8sutils.ApplyService(ctx, fc.Client, &svc, rutils.ServiceDeepEqual); err != nil {
136+
logger.Error(err, "deploy external service failed", "externalService", svc)
137+
return err
138+
}
117139
return nil
118140
}
119141

120142
// UpdateClusterStatus update the all resource status about fe observer.
121-
func (fc *FeObserverController) UpdateClusterStatus(_ context.Context, src *srapi.StarRocksCluster) error {
143+
func (fc *FeObserverController) UpdateClusterStatus(ctx context.Context, src *srapi.StarRocksCluster) error {
122144
feSpec := src.Spec.StarRocksFeSpec
123145
observerSpec := feSpec.ToObserverSpec()
124146
if observerSpec == nil {
@@ -137,7 +159,7 @@ func (fc *FeObserverController) UpdateClusterStatus(_ context.Context, src *srap
137159
}
138160

139161
src.Status.StarRocksFeObserverStatus = fs
140-
fs.ServiceName = feExternalServiceName(src.Name)
162+
fs.ServiceName = service.ExternalServiceName(src.Name, feSpec)
141163
statefulSetName := load.Name(src.Name, observerSpec)
142164
fs.ResourceNames = rutils.MergeSlices(fs.ResourceNames, []string{statefulSetName})
143165

@@ -167,7 +189,23 @@ func (fc *FeObserverController) ClearCluster(ctx context.Context, src *srapi.Sta
167189
return nil
168190
}
169191

170-
return fc.clearObserverResources(ctx, src)
192+
observerSpec := src.Spec.StarRocksFeSpec.ToObserverSpec()
193+
statefulSetName := load.Name(src.Name, observerSpec)
194+
if err := k8sutils.DeleteStatefulset(ctx, fc.Client, src.Namespace, statefulSetName); err != nil && !apierrors.IsNotFound(err) {
195+
return err
196+
}
197+
searchServiceName := service.SearchServiceName(src.Name, observerSpec)
198+
if err := k8sutils.DeleteService(ctx, fc.Client, src.Namespace, searchServiceName); err != nil && !apierrors.IsNotFound(err) {
199+
logger.Error(err, "delete search service failed", "searchServiceName", searchServiceName)
200+
return err
201+
}
202+
externalServiceName := service.ExternalServiceName(src.Name, observerSpec)
203+
err := k8sutils.DeleteService(ctx, fc.Client, src.Namespace, externalServiceName)
204+
if err != nil && !apierrors.IsNotFound(err) {
205+
logger.Error(err, "delete external service failed", "externalServiceName", externalServiceName)
206+
return err
207+
}
208+
return nil
171209
}
172210

173211
func (fc *FeObserverController) Validating(feSpec *srapi.StarRocksFeSpec) error {
@@ -179,33 +217,20 @@ func (fc *FeObserverController) Validating(feSpec *srapi.StarRocksFeSpec) error
179217
if err := srapi.ValidUpdateStrategy(feSpec.UpdateStrategy); err != nil {
180218
return err
181219
}
182-
return nil
183-
}
184-
185-
func (fc *FeObserverController) clearObserverResources(ctx context.Context, src *srapi.StarRocksCluster) error {
186-
statefulSetName := load.Name(src.Name, (*srapi.StarRocksFeObserverSpec)(nil))
187-
if err := k8sutils.DeleteStatefulset(ctx, fc.Client, src.Namespace, statefulSetName); err != nil && !apierrors.IsNotFound(err) {
188-
return err
220+
if feSpec.ToObserverSpec() != nil {
221+
if err := validateObserverImageVersion(feSpec.Image); err != nil {
222+
return err
223+
}
189224
}
190-
return fc.deleteLegacyObserverServices(ctx, src)
225+
return nil
191226
}
192227

193-
func (fc *FeObserverController) deleteLegacyObserverServices(ctx context.Context, src *srapi.StarRocksCluster) error {
194-
searchServiceName := src.Name + "-" + srapi.DEFAULT_FE_OBSERVER + "-search"
195-
if err := k8sutils.DeleteService(ctx, fc.Client, src.Namespace, searchServiceName); err != nil && !apierrors.IsNotFound(err) {
196-
return err
197-
}
198-
externalServiceName := src.Name + "-" + srapi.DEFAULT_FE_OBSERVER + "-service"
199-
if err := k8sutils.DeleteService(ctx, fc.Client, src.Namespace, externalServiceName); err != nil && !apierrors.IsNotFound(err) {
200-
return err
228+
func validateObserverImageVersion(image string) error {
229+
imageVersion := pod.GetImageVersion(image)
230+
_, err := pod.IsLowerThanAny(imageVersion, []string{"4.1.0"})
231+
if err != nil {
232+
return fmt.Errorf("fe observer requires StarRocks FE image version >= %s "+
233+
"Current image version %s does not enable FE observer", minFeObserverImageVersion, imageVersion)
201234
}
202235
return nil
203236
}
204-
205-
func feExternalServiceName(clusterName string) string {
206-
return clusterName + "-" + srapi.DEFAULT_FE + "-service"
207-
}
208-
209-
func feSearchServiceName(clusterName string) string {
210-
return clusterName + "-" + srapi.DEFAULT_FE + "-search"
211-
}

0 commit comments

Comments
 (0)