-
Notifications
You must be signed in to change notification settings - Fork 101
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
Conversation
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.
I take a initial look, direction/design seems correct to me although I have few doubts.
Some nitpicks:
pkg/initializer/initializer.go
Outdated
// List members in cluster | ||
memListCtx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) | ||
etcdMemberList, err := cli.MemberList(memListCtx) | ||
cancel() |
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.
cancel() | |
defer cancel() |
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.
Why does it need Endpoint to be passed by druid?
pkg/initializer/initializer.go
Outdated
//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, |
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.
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?
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.
We may need a discussion on my comments
pkg/initializer/initializer.go
Outdated
// List members in cluster | ||
memListCtx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) | ||
etcdMemberList, err := cli.MemberList(memListCtx) | ||
cancel() |
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.
Why does it need Endpoint to be passed by druid?
pkg/initializer/initializer.go
Outdated
|
||
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" |
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.
Can you form the URL from here
configYML, err := os.ReadFile(inputFileName) |
} | ||
|
||
config["initial-cluster"] = cluster[:len(cluster)-1] | ||
} |
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.
Can we edit the config file backuprestoreserver.go instead of httpapi.go ?
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.
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
pkg/member/member_add.go
Outdated
for { | ||
//Create etcd client | ||
//TODO: Use secure transport | ||
clientFactory := etcdutil.NewFactory(brtypes.EtcdConnectionConfig{ |
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.
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.
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.
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
.
pkg/member/member_add.go
Outdated
memberURL := getMemberURL() | ||
if memberURL == "" { | ||
logger.Warn("Could not fetch member URL") | ||
} |
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.
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.
pkg/member/member_add.go
Outdated
} | ||
|
||
// 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) { |
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.
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.
pkg/member/member_add.go
Outdated
}) | ||
|
||
memAddCtx, cancel := context.WithTimeout(context.TODO(), brtypes.DefaultEtcdConnectionTimeout) | ||
cli, _ := clientFactory.NewCluster() |
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.
clientFactory.NewCluster
returns an error which is ignored here. Any reason for ignoring the error?
pkg/member/member_add.go
Outdated
InsecureTransport: true, | ||
}) | ||
|
||
memAddCtx, cancel := context.WithTimeout(context.TODO(), brtypes.DefaultEtcdConnectionTimeout) |
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.
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.
pkg/member/member_add.go
Outdated
} | ||
|
||
// 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) { |
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.
you are not using context anywhere
pkg/member/member_add.go
Outdated
if memListErr != nil { | ||
logger.Info("error listing members: ", memListErr) | ||
cli.Close() | ||
continue |
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.
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.
pkg/member/member_add.go
Outdated
var promoted bool | ||
promoted = false | ||
for _, y := range etcdList.Members { | ||
if y.Name == os.Getenv("POD_NAME") { |
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.
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.
pkg/member/member_add.go
Outdated
break | ||
} | ||
if strings.Contains(rpctypes.ErrGRPCMemberNotLearner.Error(), memPromoteErr.Error()) { | ||
//Exit if member is already part of the cluster |
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.
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
@@ -0,0 +1,158 @@ | |||
package member |
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.
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.
pkg/member/member_add.go
Outdated
|
||
const ( | ||
// RetryPeriod is the peroid after which an operation is retried | ||
RetryPeriod time.Duration = 5 * time.Second |
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.
- 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?
pkg/server/backuprestoreserver.go
Outdated
@@ -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) |
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.
- 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?
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.
- that's a thing I missed. Will use a proper context
- 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
pkg/server/httpAPI.go
Outdated
@@ -393,7 +395,9 @@ func (h *HTTPHandler) serveLatestSnapshotMetadata(rw http.ResponseWriter, req *h | |||
|
|||
func (h *HTTPHandler) serveConfig(rw http.ResponseWriter, req *http.Request) { |
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.
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.
pkg/server/httpAPI.go
Outdated
//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) |
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.
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.
pkg/server/httpAPI.go
Outdated
return | ||
} | ||
curSts := &appsv1.StatefulSet{} | ||
errSts := clientSet.Get(context.TODO(), client.ObjectKey{ |
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.
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
pkg/server/httpAPI.go
Outdated
Name: podName[:strings.LastIndex(podName, "-")], | ||
}, curSts) | ||
if errSts != nil { | ||
h.Logger.Warn("error fetching etcd sts ", errSts) |
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.
again a proper HttpResponse with a proper code should be returned.
pkg/server/httpAPI.go
Outdated
if err != nil { | ||
h.Logger.Warnf("Error with NewCluster() : %v", err) | ||
} | ||
ctx, cancel := context.WithTimeout(context.TODO(), brtypes.DefaultEtcdConnectionTimeout) |
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.
pass context after getting it from req.Context()
pkg/server/httpAPI.go
Outdated
memList, err := cli.MemberList(ctx) | ||
noOfMembers := 0 | ||
if err != nil { | ||
h.Logger.Warnf("Error with MemberList() : %v", err) |
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.
If there is an error then, do you assumed that there will be 0 members? Is that a correct assumption?
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.
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
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.
some more nitpicks :)
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.
Hi @aaronfern , Overall looks good to me. Final few nit picks.
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: