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

Support health aware load balancing #27279

Merged
merged 2 commits into from Nov 7, 2016

Conversation

dongluochen
Copy link
Contributor

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.

#on one of your node poll HTTP endpoint
while true; do curl -s http://127.0.0.1:8001 | head -n 1; sleep 0.1; done
#run a service with published port
docker service create -p 8001:5000 --replicas 1 --name test dongluochen/simpleweb:0.2
#from HTTP poll validate the container is added to load balancer only when it becomes healthy
#scale your service 
docker service scale test=10
#validate the new containers are added only when they become healthy
#downsize your service 
docker service scale test=1
#validate the containers are removed quickly

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 .

@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

logrus.WithError(err).Errorf?

Copy link
Contributor Author

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.

Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ping @sanimej.

Copy link

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.

Copy link
Contributor

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 {
Copy link
Contributor

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)
Copy link
Contributor

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.

@dongluochen dongluochen changed the title [WIP] Support health aware load balancing Support health aware load balancing Oct 19, 2016
return false
}

func (ep *endpoint) setServiceEnabled(state bool) {
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link

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.)

Copy link
Contributor

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.

Copy link

@sanimej sanimej Oct 19, 2016

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.

Copy link
Contributor

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.

Copy link

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.

Copy link
Contributor

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
}

Copy link

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.

Copy link
Contributor

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use err here.

@thaJeztah
Copy link
Member

@stevvooe Is this ready to go to code review? I see no decision was made on design 😇

@stevvooe
Copy link
Contributor

@thaJeztah Moved to code review.

Which aspects of the design were under review?

@thaJeztah
Copy link
Member

@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

@aluzzardi
Copy link
Member

LGTM on the docker/docker (controller/adapter) part. @stevvooe ?

@stevvooe
Copy link
Contributor

LGTM

@thaJeztah
Copy link
Member

moving to docs review

@thaJeztah
Copy link
Member

hold on; needs vendoring, and CI didn't run 😢

@thaJeztah
Copy link
Member

discussed with @dongluochen, and this is basically a bug fix, no user-facing changes (other than everything working even better), so docs LGTM

@vdemeester
Copy link
Member

@dongluochen you need to vendor it though 👼

@sanimej
Copy link

sanimej commented Nov 3, 2016

@dongluochen libnetwork API change has been merged. You can go ahead with vendoring of libnetwork sha a98901aebe7ce920b6fbf02ebe5c3afc9ca975b8 along with the docker changes.

@LK4D4
Copy link
Contributor

LK4D4 commented Nov 4, 2016

@dongluochen note that vendoring process is changed to using github.com/LK4D4/vndr.

@mavenugo
Copy link
Contributor

mavenugo commented Nov 4, 2016

@dongluochen could you pls fix the libnetwork vendoring as suggested by @sanimej ? this is blocking the upcoming vendoring for windows and solaris overlay support.

Copy link
Member

@thaJeztah thaJeztah left a 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)

Signed-off-by: Dong Chen <dongluo.chen@docker.com>
Signed-off-by: Dong Chen <dongluo.chen@docker.com>
@@ -719,6 +719,13 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName
return err
}

if !container.Managed {
Copy link
Contributor

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 ?

Copy link
Contributor Author

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
 +}

Copy link
Contributor

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().

Copy link

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.

@mavenugo
Copy link
Contributor

mavenugo commented Nov 5, 2016

the experimental CI failure doesnt seem related to this change. cc @tiborvass (for the failure in DockerSuite.TestPluginActive).

@mavenugo
Copy link
Contributor

mavenugo commented Nov 7, 2016

#28115 & #28116 addresses the experimental failures. This PR is good to go.

@anghelutar
Copy link

anghelutar commented Sep 2, 2017

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?

@koxu1996
Copy link

@anghelutar I am experiencing the same problem. Did you find solution?

@anghelutar
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet