Leader election example

This commit is contained in:
Gytis Trikleris
2018-06-29 16:26:34 +03:00
committed by Ioannis Canellos
parent dce324961c
commit 4e63f8db5e
9 changed files with 449 additions and 0 deletions

View File

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

View File

@@ -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>

View File

@@ -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

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: leader
namespace: default
rules:
- apiGroups:
resources:
- pods
- configmaps
verbs:
- "*"

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,2 @@
# Role to which leader election this instance will participate
spring.cloud.kubernetes.leader.role=world

View File

@@ -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();
}
}

View File

@@ -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>