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

[WIP] OrleansDockerUtils -> Docker and Docker Swarm Clusters for Orleans #2569

Closed
wants to merge 6 commits into from
Closed

[WIP] OrleansDockerUtils -> Docker and Docker Swarm Clusters for Orleans #2569

wants to merge 6 commits into from

Conversation

galvesribeiro
Copy link
Member

@galvesribeiro galvesribeiro commented Jan 5, 2017

This PR brings support to Docker and Docker Swarp cluster for Orleans.

It is based on @ReubenBond's work on SF Membership Oracle but using the Docker APIs.

The design of this Membership Oracle is to not have a MBR just as SF doesn't. It works by communicating with Docker Daemon or Swarm endpoints using https://github.com/Microsoft/Docker.DotNet which is a wrapper over Docker APIs, and leveraging the Labels feature of docker.

The idea is to group a set of containers by deploymentId on the same host (Docker Daemon) or across multiple hosts (Swarm cluster). Whenever a client start, the DockerGatewayProvider connects to Docker and query for the current list of containers which are a silo, and are in the same deploymentId so it can build the gateway list. After that first list, it set a timer to refresh that list from docker. The same happens at the silo side which basically find the other silos by the deploymentId.

Basically there is no persistence at all neither MBR tables since all the containers can be found by the labels and when a container dies, it is automatically removed from docker list so it will not return anymore.

The hosting model is basically any container engine which implement Docker APIs (i.e. Docker Daemon, Docker Swarm, etc.). There is no change on how things are deployed or a new OrleansHost for that. All people need to do is to set the appropriate labels on the container which has the Orleans application code and everything will just works as long as the container is able to reach over network the Docker API host (i.e. the container host or Swarm manager).

There are two things that I'll implement after this PR:

  1. Listen to docker stream of events and add/remove silos live -> the streams are very unstable yet and not available on production docker engines.
  2. At runtime apply a label to the container mentioning that it is dead -> due to a blocker on docker for windows, commit changes to a running container isn't available yet.

This is a WIP. I was able to run manual tests here and made 2 containers and a client talk perfectly. The current code is totally usable. I've simulated adding and removing silos/containers to the cluster and they join and leave just fine.

I'm now working on get a unit test project with some DockerFiles so all people will need to run the tests is have Docker installed.

I appreciate any feedback while I'm working in the tests so I can address them quickly.

@galvesribeiro galvesribeiro changed the title [WIP] Dockerutils -> Docker and Docker Swarm Clusters [WIP] OrleansDockerUtils -> Docker and Docker Swarm Clusters for Orleans Jan 5, 2017

if (parameters.TryGetValue("Certificate".ToUpper(), out certificate))
{
if (!File.Exists(certificate)) throw new FileNotFoundException("Unable to find certificate file");
Copy link
Member

Choose a reason for hiding this comment

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

Windows users might be confused about this, since they're used to having a cert store and referring to certs using thumbprints rather than file names.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I agree. But this is the way Linux users deal with certs. The cert stores are not implemented for linux yet so I rather stick with this filename for now.

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine with that, I was just pointing out that the name might be a cause for confusion with some users. A name like CertificatePath or CertFile might be clearer.


Credentials credentials = null;

if (parameters.TryGetValue("DaemonEndpoint".ToUpper(), out username))
Copy link
Member

Choose a reason for hiding this comment

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

DaemonEndpoint is the username?

The logic in this method isn't straightforward because if the user specifies DaemonEndpoint as well as Certificate, the credentials will first be set to BasicAuthCredentials and then to CertificateCredentials.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it is the Docker Daemon or Docker Swarm endpoint uri like http://10.0.0.1:2375 or tcp://localhost:2375

Copy link
Member

Choose a reason for hiding this comment

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

The variable is called username

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh! now I saw the problem. It was a change before the push which led to this bug... the string on the key is wrong... It should be "Username". Fixed.

Docker_GatewayProvider_ExceptionNotifyingSubscribers = DockerBase + 1,
Docker_GatewayProvider_ExceptionRefreshingGateways = DockerBase + 2,
Docker_MembershipOracle_ExceptionNotifyingSubscribers = DockerBase + 3,
Docker_MembershipOracle_ExceptionRefreshingPartitions = DockerBase + 4
Copy link
Member

Choose a reason for hiding this comment

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

Docker uses partitions?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ops... Bad copy and paste... Removing


public async Task Refresh()
{
var result = await Task.WhenAll((await dockerClient.Containers.ListContainersAsync(
Copy link
Member

@ReubenBond ReubenBond Jan 5, 2017

Choose a reason for hiding this comment

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

await Task.WhenAll(await ...)?

I'd recommend moving the inner await outside so that it's more obvious what's happening here

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

return new DockerSiloInfo(_.Config.Hostname,
new IPEndPoint(IPAddress.Parse(_.NetworkSettings.Networks.First().Value.IPAddress),
int.Parse(_.Config.Labels[DockerLabels.SILO_PORT])),
new IPEndPoint(IPAddress.Parse(_.NetworkSettings.Networks.First().Value.IPAddress),
Copy link
Member

Choose a reason for hiding this comment

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

Some variables could be extracted for clarity, eg the IPAddress, maybe _.Config. We tend not to use _ as a parameter name anymore (I used to do it a lot, but it doesn't fit with the codebase well).

Additionally, the body of this select could be extracted into its own method, like GetSiloFromContainer

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

private readonly ILocalSiloDetails localSiloDetails;
private readonly Logger log;
private readonly GlobalConfiguration globalConfig;
private readonly IDockerSiloResolver resolver;
Copy link
Member

Choose a reason for hiding this comment

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

Just to be sure, which parts of this file differ from the Service Fabric oracle other than this type?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nothing is different actually. Just this resolver interface... Like I said yesterday, too much boilerplate code... We should later refactory this as a common abstract class or something in another PR.

@gabikliot
Copy link
Contributor

gabikliot commented Jan 5, 2017

Can you pleasevprovide a bit more details, describing how exactly you integrate with docker, where is the MBR table or who and how membership Oracle is provided, what is the hosting model, ... So people can understand without reading the code.

@galvesribeiro
Copy link
Member Author

@gabikliot sorry, I was actually hoping just @ReubenBond jump in since he just implemented the Service Fabric Membership Oracle and this implementation is almost the same. But yes, I'll edit the PR and include more details.

Regarding MBR, there is no MBR table at all, just as in SF. I'll detail more on the PR text. Thanks

@ReubenBond
Copy link
Member

Documentation on this would be great - I need to document "Hosting Orleans on Service Fabric", too.

@galvesribeiro
Copy link
Member Author

Yes, the docs is the PR right after this one @ReubenBond

Unfortunately we sill need to write docs in a separated PR to use the gh-pages branch (unless GH find a way to multibranch PR!)

@sergeybykov
Copy link
Contributor

This is a great effort!

Let's open an issue to discuss the design instead of mixing it here with code review feedback.

I'm trying to comprehend it, and have some basic initial questions.

  1. How does it handle generations (epochs) of silos? I see the code that parses it from DockerLabels.GENERATION : var generation = int.Parse(container.Config.Labels[DockerLabels.GENERATION]);, but not where it is getting set. If the silo process restarts within a container, it will have a new generation. How does Docker know that?

  2. due to a blocker on docker for windows, commit changes to a running container isn't available yet.

Does this mean that there is no way to mark a silo as dead?

  1. In general, Is there an implied equivalency here of "container is up" and "silo is running"? Can't we get into a situation that a silo is unresponsive, e.g. it's messaging queues are or threads are blocked, but Docker will consider the container healthy and keep it in the list of container/silos on the cluster?

@gabikliot
Copy link
Contributor

@galvesribeiro, I know how SF works and also what does it mean from Orleans perspective. I was asking about Docker Swarm. I am looking into more high level description of how it is used, both hosting and MBR, like here: http://dotnet.github.io/orleans/Documentation/Runtime-Implementation-Details/Consul-Deployment.html

For example, you can see here the long thread we went through when designing Consul based solution: #1267
You can see all the questions that were asked, all the options we considred.

So can we please open an issue and discuss the design, like @sergeybykov suggested. Ideally, I think we prefer doing that BEFORE code is submitted for review, so that people can actually comprehend the code, after the design is ready. Without seeing the design, its hard to give any valuable feedback.

@gabikliot
Copy link
Contributor

Also would be interesting to ask why Swarm and not other Docker clustering technologies. Is there a specific reason, or customer who asked for that, or just a first one we picked without any particular reason?

@galvesribeiro
Copy link
Member Author

@sergeybykov

Let's open an issue to discuss the design instead of mixing it here with code review feedback.

Ok will do in a minute.

How does it handle generations (epochs) of silos? I see the code that parses it from but not where it is getting set. If the silo process restarts within a container, it will have a new generation. How does Docker know that?

Docker containers are immutable. In no way a same container or its main process can restart. It is discarded and start a fresh one so the generation is pointless in this case. I'll describe in the issue how I do that.

Does this mean that there is no way to mark a silo as dead

If the silo process is frozen or crash, the container is killed and a new one must be spawned. 1 container has only 1 main process which in this case is the silo process. So if it dies, the whole container die. More on that in the issue.

@gabikliot

I agree, let me open an issue and link here in a minute.

Also would be interesting to ask why Swarm and not other Docker clustering technologies. Is there a specific reason, or customer who asked for that, or just a first one we picked without any particular reason?

You can use any docker clustering platform as long as they respect Docker APIs and they do (almost). Swarm is by far Today's most used technology for Docker clustering and the one that comes from Docker Inc. If you look at the core, there is no explicit mention tie to Swarm. The same code that talk with a single Docker Daemon talk to Swarm. Its orthogonal. Just a matter of change endpoints. I'll dive into details in the issue.

Anyway, linking the issue here soon. Thanks for the comments.

@galvesribeiro
Copy link
Member Author

Btw, sidenote... I didn't added the .Net core projects yet, will do in the end since the code dont change. The tests I'm building some easy way to run from xUnit with a DockerFile so we can spin up containers on any dev machine.

@gabikliot
Copy link
Contributor

Thank you for the detailed explanation @galvesribeiro .
Can you please provide a bit more details (not too much, but still a bit) about Swarm membership. Just a bit background, to make sure the models are compatible.


public async Task Refresh()
{
var containerList = await dockerClient.Containers.ListContainersAsync(
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 please replace var containerList with the actual type, so its more readable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

},
}
});
var inspectionResult = await Task.WhenAll(containerList.Select(c => dockerClient.Containers.InspectContainerAsync(c.ID)));
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment about var here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

/// Updates the status of this silo.
/// </summary>
/// <param name="status">The updated status.</param>
private void UpdateStatus(SiloStatus status)
Copy link
Contributor

Choose a reason for hiding this comment

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

UpdateStatus does not really do anything, it does not propagate the new status of this silos remotely to other silo. We need that in Orleans. There is a difference between Starting/Active, or Terminating/Stopping/Dead.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't understand @gabikliot. What you mean by "it does not do anything"?

foreach (var subscriber in subscribers.Values)
                {
                    subscriber.SiloStatusChangeNotification(SiloAddress, status);
                }

It is notifying all the registered subscribers about the new status on this method. I don't see your point here, sorry. Can you elaborate?

Copy link
Contributor

Choose a reason for hiding this comment

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

It does not propagate the new status of this silo to other silos (yes, it does update local subscribers).

When a silo goes lets say to terminating state (just an example), we need to notify all other silos about it. Its not enough to notify all local components. If we can't do distributed silo states, that's a major gap that we are opening compared to the main MBR Oracle and we need to think through the implications.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, then we have the same problem in SF implementation because it does the same thing there. o.O

Copy link
Contributor

Choose a reason for hiding this comment

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

The SF solution is not merged yet, so not really a good source of example to follow, at least until merged.
We need to solve that, right?

Copy link
Member

Choose a reason for hiding this comment

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

The propagation of silo status changes happens in FabricMembershipOracle.cs:L348. The only states which are supported are Active and Dead, but they satisfy the distinction for SiloStatus.IsTerminating().

Do you think this is an issue, @gabikliot?

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 a somewhat critical question I think so it may deserve its own issue.
If we only need 2 states for SF livenes, then maybe we need only 2 for the regular case as well? Maybe we can simplify the silo state machine and get rid of all other states and stay with just 2?
One need to look at what we are getting out if the other states. I wonder what @sergetbykov thinks. Would like to hear his opinion.

One thing is sure in my eyes: we should try to have similar silo state machines in all livenes cases, and not just ditch states in some cases cause we can't implementat them. If u go that route, it may work, or more likely you will be supporting diverging implementaion and at the end paying higher cost.

Alternatively: when I wrote the first integration with SF(WF at a time) 3-4 years ago, it was similar to your code now, and in addition my plan was to use their build in key value store to store silo states. I didn't implemenent it, but maybe it can work.

If u go that route, them yet another alternative is to ditch SF livenes and just use the key value store to implement MBR table. That is exactly what we did for Consul. It works, so here is an advantage. And Consul is comparable to SF, so why not? More choices.:-)

{
try
{
await resolver.Refresh();
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 a bit weird. Oracle refreshes the docker resolver which notifies its subscribers (this Oracle) back about the MBR. Cycle of dependences? Or did I misunderstand it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah you are right. I copy that over from the SF implementation. I just refactored the timer out of both gateway and oracle. Now it is inside the resolver and this dependency don't happen anymore. Thanks for notice that.

@galvesribeiro
Copy link
Member Author

Can you please provide a bit more details (not too much, but still a bit) about Swarm membership. Just a bit background, to make sure the models are compatible.

@gabikliot Sure, but sorry, I din't understand. You mean the Docker Swarm membership implementation from Docker Inc. or the the Docker Oracle Membership I implemented using Swarm for Orleans?

@gabikliot
Copy link
Contributor

I meant the former: "Docker Swarm membership implementation from Docker Inc."

Mostly not how it is implemented, but what it provides to the users of that protocol. What are the semantics. Like does it notify or do you have to pool it, how fast it detects, does it have an option to specify health check ....

That knowledge will help review the latter (" Docker Oracle Membership I implemented using Swarm for Orleans").

@galvesribeiro
Copy link
Member Author

@gabikliot I'm updating the issue related to this PR and will ping you there in a minute.

Fixed a small bug detected on a test
@galvesribeiro
Copy link
Member Author

@gabikliot I've updated #2571 with more info related of health and membership.

About the Swarm membership implementation (or any other orchestration engine for containers like Mesos, DC/OS and Kubernetes), there nothing that can help us in this case. Swarm has its own Raft implementation for its manager, but it is restricted to Swarm usage. The other orchestration engines have their own and suffer of the same problem. They only care about their cluster, and not the containers. What they care is only if the container is up or down. Nothing more.

@gabikliot
Copy link
Contributor

Does swarm have key value store? To store silo states or implement MBR table?

@galvesribeiro
Copy link
Member Author

galvesribeiro commented Jan 10, 2017

@gabikliot no, it doesn't. In fact, none of the current orchestrators have it and that is why we have having this whole discussion here. If we have it, we would be implementing the IMembershipTable like in Consul and Zookeeper. :(

@galvesribeiro
Copy link
Member Author

galvesribeiro commented Jan 30, 2017

Guys I'm closing this PR since it is not required anymore. See here: #2571 (comment)

Thanks for all the feedback.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 9, 2023
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.

5 participants