Skip to content
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

Add support for etcd cluster scale up #487

Merged
merged 11 commits into from
Jul 8, 2022

Conversation

aaronfern
Copy link
Contributor

@aaronfern aaronfern commented Jun 3, 2022

What this PR does / why we need it:
This PR adds support for scaling up an etcd cluster from 1 -> 3
Etcd backup restore will now on bootstrap detect if there is an etcd cluster already running and add itself to it as a learner. It will then promote itself to a full voting member when its logs have caught up with the leader

Which issue(s) this PR fixes:
Fixes gardener/etcd-druid#349

Special notes for your reviewer:
To be reviewed in conjuncture with gardener/etcd-druid#366

Release note:

Etcd can now scale up itself from a single member cluster to a multi member cluster

@aaronfern aaronfern requested a review from a team as a code owner June 3, 2022 04:09
@gardener-robot gardener-robot added needs/review Needs review size/m Size of pull request is medium (see gardener-robot robot/bots/size.py) labels Jun 3, 2022
@gardener-robot-ci-1 gardener-robot-ci-1 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 3, 2022
@aaronfern aaronfern added reviewed/do-not-merge Has no approval for merging as it may break things, be of poor quality or have (ext.) dependencies and removed reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) labels Jun 3, 2022
Copy link
Member

@ishan16696 ishan16696 left a comment

Choose a reason for hiding this comment

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

I take a initial look, direction/design seems correct to me although I have few doubts.
Some nitpicks:

// List members in cluster
memListCtx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
etcdMemberList, err := cli.MemberList(memListCtx)
cancel()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
cancel()
defer cancel()

Copy link
Contributor

Choose a reason for hiding this comment

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

Why does it need Endpoint to be passed by druid?

pkg/initializer/initializer.go Outdated Show resolved Hide resolved
pkg/server/backuprestoreserver.go Outdated Show resolved Hide resolved
pkg/server/backuprestoreserver.go Outdated Show resolved Hide resolved
@gardener-robot gardener-robot added the needs/changes Needs (more) changes label Jun 6, 2022
//Create etcd client
clientFactory := etcdutil.NewFactory(brtypes.EtcdConnectionConfig{
Endpoints: []string{"http://etcd-main-peer.default.svc:2380"}, //TODO: use ETCD_ENDPOINT env var passed by druid
InsecureTransport: true,
Copy link
Member

Choose a reason for hiding this comment

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

Whenever we want to connect to embedded etcd or its corresponding etcd we set InsecureTransport: true to get etcd client.
But here you need to connect to a etcd member of etcd cluster running in a different pod to get etcd client then I guess you should try to have secure connection. WDYT?

pkg/server/backuprestoreserver.go Outdated Show resolved Hide resolved
pkg/server/httpAPI.go Outdated Show resolved Hide resolved
@gardener-robot-ci-3 gardener-robot-ci-3 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 13, 2022
@gardener-robot-ci-1 gardener-robot-ci-1 added needs/ok-to-test Needs approval for testing (check PR in detail before setting this label because PR is run on CI/CD) and removed reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) labels Jun 13, 2022
@gardener-robot-ci-1 gardener-robot-ci-1 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 15, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 15, 2022
@gardener-robot-ci-1 gardener-robot-ci-1 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 15, 2022
@gardener-robot-ci-3 gardener-robot-ci-3 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 15, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 17, 2022
@gardener-robot-ci-3 gardener-robot-ci-3 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 17, 2022
Copy link
Contributor

@abdasgupta abdasgupta left a comment

Choose a reason for hiding this comment

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

We may need a discussion on my comments

pkg/initializer/initializer.go Outdated Show resolved Hide resolved
// List members in cluster
memListCtx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
etcdMemberList, err := cli.MemberList(memListCtx)
cancel()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does it need Endpoint to be passed by druid?


func getMemberURL() string {
//end := strings.Split(os.Getenv("ETCD_ENDPOINT"), "//") //TODO: use ETCD_ENDPOINT env var passed by druid
memberURL := "http://" + os.Getenv("POD_NAME") + ".etcd-main-peer.default.svc:2380"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you form the URL from here

configYML, err := os.ReadFile(inputFileName)
?

pkg/server/backuprestoreserver.go Outdated Show resolved Hide resolved
}

config["initial-cluster"] = cluster[:len(cluster)-1]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we edit the config file backuprestoreserver.go instead of httpapi.go ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if we can
config file need to be edited here so that it can be properly served to the corresponding etcd. I didn't understand how editing it in backuprestoreserver.go would help

for {
//Create etcd client
//TODO: Use secure transport
clientFactory := etcdutil.NewFactory(brtypes.EtcdConnectionConfig{
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it necessary to create a client factory and then an etcd client every time a method a function is called? This is true for all other functions in this file as well.

Copy link
Member

Choose a reason for hiding this comment

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

Hi @unmarshall ,
Basically NewFactory() takes the EtcdConnectionConfig and returns the clientFactory. The thing is EtcdConnectionConfig can vary according to the use case. so, If we keep using the same factory this might leads to ambiguity and can leads to human error that dev don’t notice about EtcdConfiguration used in factory creation and ends up using same factory again and again. I think with increasing code base it is better to create clientFactory again with EtcdConnectionConfig.

memberURL := getMemberURL()
if memberURL == "" {
logger.Warn("Could not fetch member URL")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If the member URL is empty, then a warning is logged but a member is still added as a learner. You decided to continuing, ignoring this error here. When a cluster.memberAdd gets called that in turn calls types.NewURLs passing an empty member URL. This method will return an error. The error handling in general needs to be re-written.

}

// PromoteMember promotes an etcd member from a learner to a voting member of the cluster. This will succeed only if its logs are caught up with the leader
func PromoteMember(ctx context.Context, logger *logrus.Entry) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ideally an interface should be created for the consumers so that it would be possible to test. This also allows you to create a client once and re-use the same client.

})

memAddCtx, cancel := context.WithTimeout(context.TODO(), brtypes.DefaultEtcdConnectionTimeout)
cli, _ := clientFactory.NewCluster()
Copy link
Contributor

Choose a reason for hiding this comment

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

clientFactory.NewCluster returns an error which is ignored here. Any reason for ignoring the error?

InsecureTransport: true,
})

memAddCtx, cancel := context.WithTimeout(context.TODO(), brtypes.DefaultEtcdConnectionTimeout)
Copy link
Contributor

Choose a reason for hiding this comment

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

Create a new merhod which has a timeout passed to it and that does an actual member add, handles the error and it returns an an error also logs the MemberAddResponse for better logging and troubleshooting. The retry can be done in this method.

}

// PromoteMember promotes an etcd member from a learner to a voting member of the cluster. This will succeed only if its logs are caught up with the leader
func PromoteMember(ctx context.Context, logger *logrus.Entry) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you are not using context anywhere

if memListErr != nil {
logger.Info("error listing members: ", memListErr)
cli.Close()
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible to have unrecoverable errors, if yes then you will end up in an infinite loop. I would highly recommend not to do retries in this file. Return proper errors and then let the caller handle the retry.
If you really wish to retry in this file, then have 2 functions one which does not retry and one which retries and delegates to the function which does not retry.

var promoted bool
promoted = false
for _, y := range etcdList.Members {
if y.Name == os.Getenv("POD_NAME") {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no need to look out POD_NAME from an environment so many times. Once the environment is set its immutable, doing it just once is good enough.

Also this function should take pod name as an argument, the caller should decide from where it wishes to get the pod name and not this function, makes unit testing difficult.

break
}
if strings.Contains(rpctypes.ErrGRPCMemberNotLearner.Error(), memPromoteErr.Error()) {
//Exit if member is already part of the cluster
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is an error why do you have to loop through the rest of the members? The intent of this method is to only promote one member

pkg/member/member_add.go Outdated Show resolved Hide resolved
@@ -0,0 +1,158 @@
package member
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately golang does not have a well defined convention for naming the files (golang/go#36060) but you should choose one convention and be consistent throughout the project. in this project there are files names using camelCase, snakecase, all-lower-case. Stick to one.


const (
// RetryPeriod is the peroid after which an operation is retried
RetryPeriod time.Duration = 5 * time.Second
Copy link
Contributor

Choose a reason for hiding this comment

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

  • there is no need to specify type as it is derived.
  • Is there a need to expose this when this is only used within this file?

@@ -195,6 +196,9 @@ func (b *BackupRestoreServer) runServer(ctx context.Context, restoreOpts *brtype
handler := b.startHTTPServer(etcdInitializer, b.config.SnapstoreConfig.Provider, b.config.EtcdConnectionConfig, nil)
defer handler.Stop()

// Promotes member if it is a learner
member.PromoteMember(context.TODO(), b.logger)
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Is there a need to create a context.TODO instead of passing the context that is sent as a method argument?
  • When do you promote this member? Is it immediately promoted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. that's a thing I missed. Will use a proper context
  2. A member will be allowed to be promoted as soon as its logs have caught up to the leader. There is no way to know exactly whether a member is ready to be promoted, so we immediately try to promote a member and we keep on trying until it succeeds. The promote call will succeed only when its logs have caught up to the leader

@@ -393,7 +395,9 @@ func (h *HTTPHandler) serveLatestSnapshotMetadata(rw http.ResponseWriter, req *h

func (h *HTTPHandler) serveConfig(rw http.ResponseWriter, req *http.Request) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ideally inputFileName, outputFileName should be passed as config to this method, else testing is not possible. Alternatively define a struct which captures this and then define methods on it.

//Read sts spec for updated replicas to toggle `initial-cluster-state`
clientSet, err := miscellaneous.GetKubernetesClientSetOrError()
if err != nil {
h.Logger.Errorf("failed to create clientset: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

This method handles an incoming http request and should return a proper error response with a proper error code set so that the client can consume the response and react accordingly.

return
}
curSts := &appsv1.StatefulSet{}
errSts := clientSet.Get(context.TODO(), client.ObjectKey{
Copy link
Contributor

Choose a reason for hiding this comment

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

you should ideally get the context from req.Context() and then pass it to functions that you are going to call, so that if the request context is cancelled so does all external function calls as well

Name: podName[:strings.LastIndex(podName, "-")],
}, curSts)
if errSts != nil {
h.Logger.Warn("error fetching etcd sts ", errSts)
Copy link
Contributor

Choose a reason for hiding this comment

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

again a proper HttpResponse with a proper code should be returned.

if err != nil {
h.Logger.Warnf("Error with NewCluster() : %v", err)
}
ctx, cancel := context.WithTimeout(context.TODO(), brtypes.DefaultEtcdConnectionTimeout)
Copy link
Contributor

Choose a reason for hiding this comment

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

pass context after getting it from req.Context()

memList, err := cli.MemberList(ctx)
noOfMembers := 0
if err != nil {
h.Logger.Warnf("Error with MemberList() : %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is an error then, do you assumed that there will be 0 members? Is that a correct assumption?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, if there's an error here, the assumption is that it is a member bootstrap or a cluster is waking up from hibernation, so we would not want to manipulate the initial-cluster string

@gardener-robot-ci-3 gardener-robot-ci-3 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 21, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 21, 2022
@gardener-robot gardener-robot added size/l Size of pull request is large (see gardener-robot robot/bots/size.py) needs/second-opinion Needs second review by someone else and removed size/m Size of pull request is medium (see gardener-robot robot/bots/size.py) labels Jun 23, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jun 23, 2022
Copy link
Member

@ishan16696 ishan16696 left a comment

Choose a reason for hiding this comment

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

some more nitpicks :)

pkg/server/httpAPI.go Outdated Show resolved Hide resolved
pkg/server/httpAPI.go Show resolved Hide resolved
pkg/server/httpAPI.go Show resolved Hide resolved
@gardener-robot gardener-robot added size/l Size of pull request is large (see gardener-robot robot/bots/size.py) and removed size/xl Size of pull request is huge (see gardener-robot robot/bots/size.py) labels Jul 7, 2022
@gardener-robot-ci-3 gardener-robot-ci-3 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jul 7, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jul 7, 2022
Copy link
Member

@ishan16696 ishan16696 left a comment

Choose a reason for hiding this comment

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

Hi @aaronfern , Overall looks good to me. Final few nit picks.

pkg/member/member_control.go Outdated Show resolved Hide resolved
pkg/member/member_control.go Show resolved Hide resolved
pkg/miscellaneous/miscellaneous.go Outdated Show resolved Hide resolved
@gardener-robot gardener-robot added size/xl Size of pull request is huge (see gardener-robot robot/bots/size.py) and removed size/l Size of pull request is large (see gardener-robot robot/bots/size.py) labels Jul 7, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jul 7, 2022
@gardener-robot-ci-1 gardener-robot-ci-1 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jul 7, 2022
@gardener-robot-ci-2 gardener-robot-ci-2 added the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jul 8, 2022
@gardener-robot-ci-1 gardener-robot-ci-1 removed the reviewed/ok-to-test Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD) label Jul 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs/changes Needs (more) changes needs/ok-to-test Needs approval for testing (check PR in detail before setting this label because PR is run on CI/CD) needs/review Needs review needs/second-opinion Needs second review by someone else reviewed/do-not-merge Has no approval for merging as it may break things, be of poor quality or have (ext.) dependencies size/xl Size of pull request is huge (see gardener-robot robot/bots/size.py) status/closed Issue is closed (either delivered or triaged)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] Etcd cluster scale-up
8 participants