Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

Support allocating shared LB for services #1

Merged
merged 2 commits into from
Nov 3, 2023

Conversation

StrongMonkey
Copy link

This PR adds ability to configure services to share load balancers for different listeners. The purpose of this is to reduce cost when bring NLB for service loadbalancer, while one service will provision one dedicated NLB.

There are two major changes for the implemetation:

  1. A controller that watches services, and allocate service to a "stack". A "stack" contains virtual resources like NLB, listeners.
  2. Support multiple services sharing "stack". This is done through global in-memory cache where multiple services can be allocated to one virtual stack, with shared LB and different listener for ports in order to share resources.

}
if svc.Annotations[service.LoadBalancerAllocatingPortKey] == "true" {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When service is in "shared" mode, it will use the nodePort as the listener. This avoids the need to allocate a dedicated port for service, as nodePort is already unique across cluster that is allocated by k8s server.

main.go Outdated
elbv2webhook.NewTargetGroupBindingMutator(cloud.ELBV2(), ctrl.Log).SetupWithManager(mgr)
elbv2webhook.NewTargetGroupBindingValidator(mgr.GetClient(), cloud.ELBV2(), ctrl.Log).SetupWithManager(mgr)
networkingwebhook.NewIngressValidator(mgr.GetClient(), controllerCFG.IngressConfig, ctrl.Log).SetupWithManager(mgr)
//podReadinessGateInjector := inject.NewPodReadinessGate(controllerCFG.PodWebhookConfig,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to uncomment this, used for local development.

}

func (b *defaultModelBuilder) Build(ctx context.Context, service *corev1.Service) (core.Stack, *elbv2model.LoadBalancer, bool, error) {
stack := core.NewDefaultStack(core.StackID(k8s.NamespacedName(service)))
// Initialize the global cache if not initialized
if !b.initialized {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sure that when services are in shared "stack", it reused the global cache that contains the stack. So

  1. Initialize the global cache when controller is started.
  2. Always looks for existin stack in cache when deploying. This is so that two services that in shared stack will be deployed together by sharing the same LB.

@@ -221,8 +283,49 @@ func (t *defaultModelBuildTask) run(ctx context.Context) error {
return errors.Errorf("deletion_protection is enabled, cannot delete the service: %v", t.service.Name)
}
}
// When service is deleted, update resources in the stack to make sure things are cleaned up properly.
for _, port := range t.service.Spec.Ports {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also cleans up resources when service is deleted.

}
for _, svc := range serviceList.Items {
if svc.Annotations[service.LoadBalancerStackKey] != "" {
r.allocatedServices[svc.Annotations[service.LoadBalancerStackKey]] = map[int]bool{}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if you only want to track existence, this is more efficient.

Suggested change
r.allocatedServices[svc.Annotations[service.LoadBalancerStackKey]] = map[int]bool{}
r.allocatedServices[svc.Annotations[service.LoadBalancerStackKey]] = map[int]struct{}{}

You could also make the maps map[int32]struct{}{} to avoid the int(port.NodePort) everywhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 addressed

controllers/service/service_controller.go Show resolved Hide resolved
r.lock.Lock()
for _, port := range svc.Spec.Ports {
r.allocatedServices[stackName] = map[int]bool{
int(port.Port): true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only place you use port.Port instead of port.NodePort. Just wanted to make sure that is intentional.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah mistake. Should be nodePort 👍

controllers/service/service_controller.go Show resolved Hide resolved
pkg/model/core/stack.go Show resolved Hide resolved
@@ -487,6 +501,10 @@ func (t *defaultModelBuildTask) buildLoadBalancerName(_ context.Context, scheme
_, _ = uuidHash.Write([]byte(scheme))
uuid := hex.EncodeToString(uuidHash.Sum(nil))

if t.service.Annotations[LoadBalancerStackKey] != "" {
return fmt.Sprintf("k8s-%.8s", t.service.Annotations[LoadBalancerStackKey]), nil

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This format is used in at least a couple different places. Can it be made a function?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if !b.initialized {
// if not initialized, we need to build the global cache based on existing services
var serviceList corev1.ServiceList
if err := b.client.List(context.Background(), &serviceList); err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be

Suggested change
if err := b.client.List(context.Background(), &serviceList); err != nil {
if err := b.client.List(ctx, &serviceList); err != nil {

var stack core.Stack
stack = core.NewDefaultStack(stackID)
if service.Annotations[LoadBalancerAllocatingPortKey] == "true" {
// service will be allocated to a stack. The external controller will be

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like an incomplete comment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah added

pkg/service/model_builder.go Show resolved Hide resolved
pkg/service/model_builder.go Show resolved Hide resolved
@StrongMonkey StrongMonkey force-pushed the acorn-v2.6.2 branch 2 times, most recently from 01317fc to 39f802d Compare November 2, 2023 09:40
controllers/service/service_controller.go Show resolved Hide resolved
controllers/service/service_controller.go Show resolved Hide resolved
Comment on lines 124 to 131
if b.stackGlobalCache[stackID] == nil {
b.lock.Lock()
b.stackGlobalCache[stackID] = core.NewDefaultStack(stackID)
b.lock.Unlock()
}
b.lock.Lock()
b.stackGlobalCache[stackID].AddService(&svc)
b.lock.Unlock()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read should also be protected here.

Suggested change
if b.stackGlobalCache[stackID] == nil {
b.lock.Lock()
b.stackGlobalCache[stackID] = core.NewDefaultStack(stackID)
b.lock.Unlock()
}
b.lock.Lock()
b.stackGlobalCache[stackID].AddService(&svc)
b.lock.Unlock()
b.lock.Lock()
if b.stackGlobalCache[stackID] == nil {
b.stackGlobalCache[stackID] = core.NewDefaultStack(stackID)
}
b.stackGlobalCache[stackID].AddService(&svc)
b.lock.Unlock()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, is the lock here even necessary? You have the other lock and that should take care of this, too, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I will change to lock the whole snippet. It is necessary since another lock onlys locks per stack, not globally.

pkg/service/model_builder.go Show resolved Hide resolved
Signed-off-by: Daishan Peng <[email protected]>
@StrongMonkey StrongMonkey merged commit 5af52aa into acorn-io:acorn-v2.6.2 Nov 3, 2023
3 of 4 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants