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
Support health aware load balancing #27279
Conversation
@@ -178,6 +178,10 @@ func (daemon *Daemon) SetupIngress(create clustertypes.NetworkCreateRequest, nod | |||
if err := ep.Join(sb, nil); err != nil { | |||
logrus.Errorf("Failed joining ingress sandbox to ingress endpoint: %v", err) | |||
} | |||
|
|||
if err := sb.EnableService(); err != nil { | |||
logrus.Errorf("Failed enabling service for ingress sandbox: %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.
logrus.WithError(err).Errorf
?
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.
WithError(err)
is not used here to be consistent with other logs in this file.
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.
@dongluochen Let's not be consistently bad. Please use the logging package correctly.
func (nDB *NetworkDB) CreateEntry(tname, nid, key string, value []byte) error { | ||
if _, err := nDB.GetEntry(tname, nid, key); err == nil { | ||
e, _ := nDB.getEntry(tname, nid, key) |
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.
func (nDB *NetworkDB) GetEntry(tname, nid, key string) ([]byte, error) {
i feel the e is not in correct position.
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.
Also, please check the error.
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.
Ping @sanimej.
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 feel the e is not in correct position.
e
is not the error here. Its the entry returned by getEntry
. Yes, the error check can be added.
@dongluochen I will make a note of this and correct it when pushing the change again in libnetwork.
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.
thanks for your clarify, got it.
@@ -183,6 +183,10 @@ func (r *controller) Start(ctx context.Context) error { | |||
|
|||
// no health check | |||
if ctnr.Config == nil || ctnr.Config.Healthcheck == nil { | |||
if err := r.adapter.backend.ActivateContainerServiceBinding(r.adapter.container.name()); err != nil { |
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.
Make a method on adapter to capture all this. Should just be r.adapter.activateServiceBinding()
.
func (nDB *NetworkDB) CreateEntry(tname, nid, key string, value []byte) error { | ||
if _, err := nDB.GetEntry(tname, nid, key); err == nil { | ||
e, _ := nDB.getEntry(tname, nid, key) |
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.
Also, please check the error.
121671b
to
8e1b2bb
Compare
return false | ||
} | ||
|
||
func (ep *endpoint) setServiceEnabled(state bool) { |
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.
enableService
.
@@ -303,6 +304,24 @@ func (ep *endpoint) isAnonymous() bool { | |||
return ep.anonymous | |||
} | |||
|
|||
// CompareAndSwap ep's serviceEnabled. If its in oldState, set it to newState | |||
// and return true. If its not in oldState return false | |||
func (ep *endpoint) casServiceEnabled(oldState, newState bool) bool { |
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 is kind of a bazaar API. I'm not sure that CAS works unless you know the last observed revision, not just value. For example, below, the value is not actually observed. It seems like you just need to know if the action succeeded or not.
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 that CAS works unless you know the last observed revision, not just value
- See the usages of this API. Its intent is to allow EnableService or DisableService only once per sandbox. The underlying type being worked on is a boolean. Its a low level primitive. Its not a CAS operation on a type that can take multiple values where you need a revision number per operation (ie: typical CAS semantics provided by Consul etc.)
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.
@sanimej Both uses hard-code oldValue
. I don't see how it does that.
Let's say you have a the set of values for the boolean:
true, false, false, true, true
The serializability of the result depends on the most recent previous value. If I just know that it was true
last, I can't tell if I am setting it based on stale information or the most recent true value.
If we want to support "only once", use a sync.Once for enable/disable but I am surprised we have that limitation.
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.
Usage of this API is to enable the service when the image's healthcheck passes and disable the service first when its being shutdown. Both EnableService and DisableService are idempotent. In both the cases the oldValue and newValue are fixed. Its not dependent on what the current state is. But a continuous flip flop between multiple true & false states is a bug and its something the caller has to deal with.
I considered using sync.Once. It would need two Once variables and we still need a bool to track the current state. The current approach achieves the same result with one bool.
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.
@sanimej It's weird code and doesn't really achieve what you think it achieves. You get the same exact result if you have a single argument, since both use the complement as the oldValue
. I have no clue why you would ever make them idempotent. Enable/Disable being a one way operation is an implementation detail of swarmkit. Users of the API should be able to add and remove the service at their leisure. Enforcing it at this level is error prone, buggy and creates weird code like this.
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.
@stevvooe
You get the same exact result if you have a single argument, since both use the complement as the oldValue.
I would rather be explicit by passing both old and new values.
{Enable|Disable}Service
is libnetwork API. Its not an implementation detail of swarmkit.
By idempotent what I meant here is one can call EnableService on a sandbox when the service is already enabled. If you are thinking about concurrent calls to EnableService & DisableService calls that means the healtcheck for some reason is flipping between healthy and unhealthy and has to be handled by the caller.
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.
@sanimej I really think that this code isn't really doing anything of value. Looks like it could be accomplished like this:
func (ep *endpoint) enableService(enable bool) bool {
ep.mu.Lock()
defer ep.mu.Lock()
if ep.serviceEnabled != enable {
ep.serviceEnabled = enable
return true
}
return false
}
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.
@stevvooe Yes, since the type here is boolean the oldValue
argument is optional. If you think this is better for readability, ie: just calling it enableService
with one argument, I can change it. But what it is doing is a CAS operation.
Since its a libnetwork change I will modify it in the libnetwork PR.
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.
@sanimej Yes, this only needs one method that sets the value.
Complement is implied with a boolean in CAS for this usage:
current | old | new | set? | used |
---|---|---|---|---|
true |
true |
true |
false |
no |
true |
true |
false |
true |
yes |
true |
false |
false |
false |
no |
true |
false |
true |
false |
yes |
false |
false |
false |
false |
no |
false |
false |
true |
true |
yes |
false |
true |
true |
false |
no |
false |
true |
false |
false |
yes |
Pulling out the lines in the above table that represent the usage of these methods, we have the following:
current | old | new | set? | used |
---|---|---|---|---|
true |
true |
false |
true |
yes |
true |
false |
true |
false |
yes |
false |
false |
true |
true |
yes |
false |
true |
false |
false |
yes |
Per inspection, we can see that the return value, set?
is independent of old
, meaning that because you always use the complement for old
, CAS is independent of last observed.
func (sb *sandbox) EnableService() error { | ||
for _, ep := range sb.getConnectedEndpoints() { | ||
if ep.casServiceEnabled(false, true) { | ||
if e := ep.addToCluster(); e != nil { |
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.
Please use err
here.
@stevvooe Is this ready to go to code review? I see no decision was made on design 😇 |
@thaJeztah Moved to code review. Which aspects of the design were under review? |
@stevvooe design review can be either UX, or design of the feature in general. When in doubt, we usually put it in design review, but feel free to move PR's to "code review" if no design review is required, or if it was already approved |
8e1b2bb
to
54f5245
Compare
LGTM on the docker/docker (controller/adapter) part. @stevvooe ? |
LGTM |
moving to docs review |
hold on; needs vendoring, and CI didn't run 😢 |
discussed with @dongluochen, and this is basically a bug fix, no user-facing changes (other than everything working even better), so docs LGTM |
@dongluochen you need to vendor it though 👼 |
@dongluochen libnetwork API change has been merged. You can go ahead with vendoring of libnetwork sha a98901aebe7ce920b6fbf02ebe5c3afc9ca975b8 along with the docker changes. |
@dongluochen note that vendoring process is changed to using |
@dongluochen could you pls fix the libnetwork vendoring as suggested by @sanimej ? this is blocking the upcoming vendoring for windows and solaris overlay support. |
54f5245
to
da3c5a1
Compare
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.
thanks @dongluochen
LGTM (if green)
da3c5a1
to
92bdd75
Compare
Signed-off-by: Dong Chen <dongluo.chen@docker.com>
Signed-off-by: Dong Chen <dongluo.chen@docker.com>
92bdd75
to
aba4190
Compare
@@ -719,6 +719,13 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName | |||
return err | |||
} | |||
|
|||
if !container.Managed { |
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.
@sanimej @dongluochen won't this impact all the containers in all the networks (swarm-mode or otherwise) including docker0
, host
, null
networks ? Will sb.EnableService
protect us from any regressions to other use-cases that is using other networks ?
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.
@mavenugo I think this change does not change the current behavior that each endpoint calls addToCluster
only once. Here when an unmanaged container joins a network, sb.EnableService
would enumerate every endpoint but only the new endpoint would call addToCluster
. Is this your concern?
+func (sb *sandbox) EnableService() error {
+ for _, ep := range sb.getConnectedEndpoints() {
+ if ep.enableService(true) {
+ if err := ep.addToCluster(); err != nil {
+ ep.enableService(false)
+ return fmt.Errorf("could not update state for endpoint %s into cluster: %v", ep.Name(), err)
+ }
+ }
+ }
+ return nil
+}
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.
@dongluochen thats not my concern. But I went through the libnetwork code to understand the behavior and I think that is fine. The question I raised wont be impacted here since addToCluster
will not fail for endpoints connected to networks that !isClusterEligible()
.
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.
@mavenugo yes, !isClusterEligible()
will make it a noop for non-swarm networks. The same check was relied on earlier (before the addition of EnableService()
API) when addToCluster
was getting called unconditionally.
the experimental CI failure doesnt seem related to this change. cc @tiborvass (for the failure in |
We are trying to accomplish health-aware DNS load-balancing and according to this conversation it should be possible from docker 13 on. However, it does not seem to work on a setup using docker 13.1. We are not using docker swarm, we start services via docker-compose and use an external overlay network. A ping to a service started twice (first being healthy and second unhealthy) resolves to both ips, while we would expect to receive only the healthy one. Why isn't the unhealthy one removed from the DNS? |
@anghelutar I am experiencing the same problem. Did you find solution? |
Hello @koxu1996, I did not find a solution, I was adviced to use docker swarm. See: https://forums.docker.com/t/health-aware-dns-based-load-balancing/37566 roxana |
This change support health aware load balancing and DNS records. Close #24092.
- What I did
A container is added to service records when it starts (without healthcheck config), or when healthcheck passes.
- How I did it
Cherrypick @sanimej's service enable API. Add service activate API. Add instruments to container start and shutdown to activate/deactivate service records.
- How to verify it
Here are the steps to verify the change.
Docker/docker integration test doesn't support this type of testing. No integration test is added.
- Description for the changelog
This PR cherrypicks @sanimej's change. It cannot be merged as it is. After review, the cherrypick should be replaced by change from docker/libnetwork.
Ping @sanimej @mrjana .