Leader election example
This commit is contained in:
committed by
Ioannis Canellos
parent
dce324961c
commit
4e63f8db5e
@@ -0,0 +1,83 @@
|
||||
# Spring Cloud Kubernetes leader election example
|
||||
|
||||
## Setting up the Environment
|
||||
|
||||
This example uses a Fabric8 Maven Plugin to deploy an application to a Kubernetes cluster.
|
||||
To try it locally, download and install `[Minikube](https://kubernetes.io/docs/getting-started-guides/minikube/)`.
|
||||
|
||||
Once Minikube is downloaded, start it with the following command:
|
||||
```
|
||||
minikube start
|
||||
```
|
||||
|
||||
And configure environment variables to point to the Minikube's Docker daemon:
|
||||
```
|
||||
eval $(minikube docker-env)
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
Spring Cloud Kubernetes leader election mechanism implements leader election API of Spring Integration using Kubernetes ConfigMap.
|
||||
Multiple instances of the same application compete for a leadership of a specified role.
|
||||
But only one of them can become a leader and receive an `OnGrantedEvent` application event with a leadership `Context`.
|
||||
All the instances periodically try to get a leadership and whichever comes first - becomes a leader.
|
||||
The new leader will remain until either its instance disappears from the cluster or it yields its leadership.
|
||||
Once the leader is gone, any of the existing instances can become a new leader (even the previous leader if it yielded leadership but stayed in the cluster).
|
||||
And finally, if the leadership is yielded or revoked for some reason, the old leader receives `OnRevokedEvent` application event.
|
||||
|
||||
## Example application usage
|
||||
|
||||
To begin with, build and deploy the application:
|
||||
```
|
||||
mvn clean package fabric8:deploy -Pkubernetes
|
||||
```
|
||||
|
||||
This will deploy a single application instance to the cluster and that instance will automatically become a leader.
|
||||
|
||||
Create an environment variable for an easier application access:
|
||||
```
|
||||
SERVICE_URL=$(minikube service kubernetes-leader-election-example --url)
|
||||
```
|
||||
|
||||
Get leadership information:
|
||||
```
|
||||
curl $SERVICE_URL
|
||||
```
|
||||
|
||||
You should receive a message like this:
|
||||
```
|
||||
I am 'kubernetes-leader-election-example-1234567890-abcde' and I am the leader of the 'world'
|
||||
```
|
||||
|
||||
Yield the leadership:
|
||||
```
|
||||
curl -X PUT $SERVICE_URL
|
||||
```
|
||||
|
||||
And check the leadership information again:
|
||||
```
|
||||
curl $SERVICE_URL
|
||||
```
|
||||
|
||||
Now you should receive a message like this:
|
||||
```
|
||||
I am 'kubernetes-leader-election-example-1234567890-abcde' but I am not a leader of the 'world'
|
||||
```
|
||||
|
||||
If you wouldn't do anything for a few seconds, the same instance will become a leader again because it only yielded its leadership but stayed in the cluster.
|
||||
|
||||
Now scale the application to two instances and try all the steps again:
|
||||
```
|
||||
kubectl scale --replicas=2 deployment.apps/kubernetes-leader-election-example
|
||||
```
|
||||
|
||||
> Note: with multiple replicas in the cluster, `curl` command will access one of them depending on the Kubernetes load balancing configuration.
|
||||
Thus, when trying to yield the leadership, request might go to a non-leader node first. Just execute command again until it reaches the correct node.
|
||||
|
||||
> Note: instances periodically try to acquire leadership and Spring Cloud Kubernetes doesn't decide which one of them is more worth to become one.
|
||||
Thus, it is possible that the instance which just yielded the leadership, made another leadership take over request faster than another instances and became a leader again.
|
||||
|
||||
## Access control notice
|
||||
|
||||
Leader election mechanism uses Kubernetes ConfigMap feature to coordinate leadership information.
|
||||
In order to access it, [Role](./src/main/fabric8/role.yaml) and [RoleBinding](./src/main/fabric8/rb.yaml) objects are defined.
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-examples</artifactId>
|
||||
<version>0.3.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>kubernetes-leader-election-example</artifactId>
|
||||
<name>Spring Cloud Kubernetes :: Examples :: Leader Election</name>
|
||||
<description>Leader election demonstration with Spring Integration and ConfigMap</description>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
<version>${spring-boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-leader</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-leader</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>fabric8-maven-plugin</artifactId>
|
||||
<version>${fabric8.maven.plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>${maven-deploy-plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>kubernetes</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>fabric8-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>fmp</id>
|
||||
<goals>
|
||||
<goal>resource</goal>
|
||||
<goal>build</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<enricher>
|
||||
<config>
|
||||
<fmp-service>
|
||||
<type>NodePort</type>
|
||||
</fmp-service>
|
||||
</config>
|
||||
</enricher>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: leader
|
||||
namespace: default
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: leader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: default
|
||||
namespace: default
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: leader
|
||||
namespace: default
|
||||
rules:
|
||||
- apiGroups:
|
||||
resources:
|
||||
- pods
|
||||
- configmaps
|
||||
verbs:
|
||||
- "*"
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2018 to the original authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.kubernetes.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class App {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2018 to the original authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.kubernetes.examples;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.integration.leader.Context;
|
||||
import org.springframework.integration.leader.event.OnGrantedEvent;
|
||||
import org.springframework.integration.leader.event.OnRevokedEvent;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class LeaderController {
|
||||
|
||||
private final String host;
|
||||
|
||||
@Value("${spring.cloud.kubernetes.leader.role}")
|
||||
private String role;
|
||||
|
||||
private Context context;
|
||||
|
||||
public LeaderController() throws UnknownHostException {
|
||||
this.host = InetAddress.getLocalHost().getHostName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a message whether this instance is a leader or not.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping
|
||||
public String getInfo() {
|
||||
if (context == null) {
|
||||
return String.format("I am '%s' but I am not a leader of the '%s'", host, role);
|
||||
}
|
||||
|
||||
return String.format("I am '%s' and I am the leader of the '%s'", host, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request to try and revoke a leadership of this instance.
|
||||
* If the instance is not a leader, leadership cannot be revoked. Thus "HTTP Bad Request" response.
|
||||
* If the instance is a leader, it must have a leadership context instance which can be used to give up the
|
||||
* leadership.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@PutMapping
|
||||
public ResponseEntity<String> revokeLeadership() {
|
||||
if (context == null) {
|
||||
String message = String.format("Cannot revoke leadership because '%s' is not a leader", host);
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
.body(message);
|
||||
}
|
||||
|
||||
context.yield();
|
||||
|
||||
String message = String.format("Leadership revoked for '%s'", host);
|
||||
return ResponseEntity
|
||||
.ok(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a notification that this instance has become a leader.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
@EventListener
|
||||
public void handleEvent(OnGrantedEvent event) {
|
||||
System.out.println(String.format("'%s' leadership granted", event.getRole()));
|
||||
context = event.getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a notification that this instance's leadership has been revoked.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
@EventListener
|
||||
public void handleEvent(OnRevokedEvent event) {
|
||||
System.out.println(String.format("'%s' leadership revoked", event.getRole()));
|
||||
context = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Role to which leader election this instance will participate
|
||||
spring.cloud.kubernetes.leader.role=world
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.springframework.cloud.kubernetes.examples;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.integration.leader.Context;
|
||||
import org.springframework.integration.leader.event.OnGrantedEvent;
|
||||
import org.springframework.integration.leader.event.OnRevokedEvent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LeaderControllerTest {
|
||||
|
||||
@Mock
|
||||
private OnGrantedEvent mockOnGrantedEvent;
|
||||
|
||||
@Mock
|
||||
private OnRevokedEvent mockOnRevokedEvent;
|
||||
|
||||
@Mock
|
||||
private Context mockContext;
|
||||
|
||||
private String host;
|
||||
|
||||
private LeaderController leaderController;
|
||||
|
||||
@Before
|
||||
public void before() throws UnknownHostException {
|
||||
host = InetAddress.getLocalHost().getHostName();
|
||||
leaderController = new LeaderController();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGetNonLeaderInfo() {
|
||||
String message = String.format("I am '%s' but I am not a leader of the 'null'", host);
|
||||
assertThat(leaderController.getInfo()).isEqualTo(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleGrantedEvent() {
|
||||
given(mockOnGrantedEvent.getContext()).willReturn(mockContext);
|
||||
|
||||
leaderController.handleEvent(mockOnGrantedEvent);
|
||||
|
||||
String message = String.format("I am '%s' and I am the leader of the 'null'", host);
|
||||
assertThat(leaderController.getInfo()).isEqualTo(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleRevokedEvent() {
|
||||
given(mockOnGrantedEvent.getContext()).willReturn(mockContext);
|
||||
|
||||
leaderController.handleEvent(mockOnGrantedEvent);
|
||||
leaderController.handleEvent(mockOnRevokedEvent);
|
||||
|
||||
String message = String.format("I am '%s' but I am not a leader of the 'null'", host);
|
||||
assertThat(leaderController.getInfo()).isEqualTo(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRevokeLeadership() {
|
||||
given(mockOnGrantedEvent.getContext()).willReturn(mockContext);
|
||||
|
||||
leaderController.handleEvent(mockOnGrantedEvent);
|
||||
ResponseEntity<String> responseEntity = leaderController.revokeLeadership();
|
||||
|
||||
String message = String.format("Leadership revoked for '%s'", host);
|
||||
assertThat(responseEntity.getBody()).isEqualTo(message);
|
||||
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
verify(mockContext).yield();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotRevokeLeadershipIfNotLeader() {
|
||||
ResponseEntity<String> responseEntity = leaderController.revokeLeadership();
|
||||
|
||||
String message = String.format("Cannot revoke leadership because '%s' is not a leader", host);
|
||||
assertThat(responseEntity.getBody()).isEqualTo(message);
|
||||
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
verify(mockContext, times(0)).yield();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
<modules>
|
||||
<module>kubernetes-reload-example</module>
|
||||
<module>kubernetes-hello-world-example</module>
|
||||
<module>kubernetes-leader-election-example</module>
|
||||
<!-- <module>kubernetes-circuitbreaker-ribbon-example</module>
|
||||
<module>kubernetes-zipkin-example</module> -->
|
||||
</modules>
|
||||
|
||||
Reference in New Issue
Block a user