GCP Kubernetes Engine – autoscalling

Hi. In this article I want to show you how our app from previous posts about AWS Auto Scaling Groups works on GCP Kubernetes Engine. We saw that scaling was a really long process. What is more I wrote that at the end of post: „As you can see it takes time to scale servers, so in our case (calculation) it is not a good solution but it wasn’t be a goal in this post.”. Maybe Kubernetes engine is a better solution? Let’s check it out. 🙂

It is not a post for someone who doesn’t even heard about docker.

Roadmap

  1. Create our first cluster on GCP Kubernetes Engine.
  2. Run RabbitMQ on cluster in GCP Kubernetes Engine.
  3. Create Spring Boot application which will receiving numbers from RabbitMQ and calculating prime number bigger than that number and saving it in database.
  4. Put that Spring Boot application in docker container and deploy on Kubernetes Engine.
  5. Set scaling policy.
  6. Send some numbers to queue and check how everything works. 🙂

Codes of this applications you can see on Github.

Create cluster on GCP

I have chosen standard cluster without any additional options.

Deploy RabbitMQ on our cluster

I made easy Dockerfile. There is only one line:

FROM rabbitmq

Of course you can use RabbitMQ from marketplace.

The next step is to build an docker image and push it to Google Container Registry.

$ sudo docker build -t gcr.io/[project-name]/rabbitmq .
$ gcloud auth print-access-token | sudo docker login -u oauth2accesstoken --password-stdin https://gcr.io
$ sudo docker push gcr.io/[project-name]/rabbitmq

About pushing and pulling images to GCR you can read more here.

After that you can go to Workloads and click on Deploy button.

Choose your pushed image.

Set container name in Configuration.

and deploy. 🙂

RabbitMQ – create services

The last step is to send a message to our queue. We will do it just from computer. It is currently impossible because we can’t connect to this deployed queue. To make it possible we have to create service which exposes application ports. Remember: Don’t do this in the professional projects. Everyone who knows IP can connect to this queue.

Go to Workloads and then click on Actions -> Expose as below.

Then expose port 5672. Service type is Load balancer. It means that we have an external IP.

That’s all. We got external IP and expose queue to the public.

I expose 15672 port too (it is not necessary).

Spring boot application

How to start with RabbitMQ and Spring boot was described here.

I have added JPA and MySQL dependencies to pom:

<?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.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>pl.cloud</groupId>
    <artifactId>backend1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>backend1</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <configuration>
                    <finalName>app</finalName>
                </configuration>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

Application class with queue configuration and main method.

@SpringBootApplication
public class Application {
    static final String queueName = "spring-boot";

    @Bean
    SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                             MessageListenerAdapter listenerAdapter) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(queueName);
        container.setMessageListener(listenerAdapter);
        return container;
    }

    @Bean
    Queue queue() {
        return new Queue(queueName, false, false, true);
    }

    @Bean
    MessageListenerAdapter listenerAdapter(Receiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Simple repository. I didn’t create service because there is no logic.

@Repository
public interface MyRepository extends JpaRepository<Result, Long> {
}

Receiver class.

@Component
public class Receiver {

    private final MyRepository myRepository;
    private CountDownLatch latch = new CountDownLatch(1);

    @Autowired
    public Receiver(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    public void receiveMessage(Integer message) {
        System.out.println("Odebralem: " + message);

        Integer prime = calculatePrimeNumber(2, 0, message);

        Result result = new Result();
        result.setResult(prime);
        myRepository.save(result);
        System.out.println(prime);

        latch.countDown();
    }

    public CountDownLatch getLatch() {
        return latch;
    }


    private static int calculatePrimeNumber(int number, int i, int n) {
        while (true) {
            number++;
            if (isPrime(number)) ++i;
            if (i > n && isPrime(number)) break;
        }
        return number;
    }

    private static boolean isPrime(int n) {
        for (int i = 2; i <= Math.sqrt(n); i++) {
            if (n % i == 0)
                return false;
        }
        return true;
    }

}

And result entity.

@Entity(name = "RESULTS")
public class Result {
    @Id
    @GeneratedValue
    private Long id;

    private Integer result;

    public void setResult(Integer result) {
        this.result = result;
    }

    public Long getId() {
        return id;
    }
}

Fill application.properties.

spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
spring.jpa.database-platform=
spring.rabbitmq.host=[external_rabbitmq_host]

Let’s build an application, create Dockerfile (I wrote about that here), image and finally push it to GCR.

$ mvn clean package

Dockerfile.

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY ./target/app.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Build and push. The same way as in the queue.

$ sudo docker build -t gcr.io/[project-name]/rabbitmq-receiver .
$ gcloud auth print-access-token | sudo docker login -u oauth2accesstoken --password-stdin https://gcr.io
$ sudo docker push gcr.io/[project-name]/rabbitmq-receiver

Deploy Spring boot application on GCP

This step is the same as step with deploying RabbitMQ on GCP. After all we have our container, we can set scaling policy in Workloads -> Actions -> Auto-scale.

Here is my example configuration.

Test it!

I made simple app to sending message to queue. Pom.xml

<?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.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>pl.cloud</groupId>
    <artifactId>sender</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sender</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

Main Application class.


@SpringBootApplication
public class Application {
    public static final String topicExchangeName = "spring-boot-exchange";
    static final String queueName = "spring-boot";

    @Bean
    Queue queue() {
        return new Queue(queueName, false, false, true);
    }

    @Bean
    TopicExchange exchange() {
        return new TopicExchange(topicExchangeName);
    }

    @Bean
    Binding binding(Queue queue, TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("foo.bar.#");
    }


    public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(Application.class, args).close();
    }

}

And Runner with numbers.

@Component
public class Runner implements CommandLineRunner {

    private final RabbitTemplate rabbitTemplate;

    public Runner(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @Override
    public void run(String... args) {
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 1);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 2);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 3);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000003);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000004);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000005);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000006);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000007);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000008);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 9000009);
        rabbitTemplate.convertAndSend(Application.topicExchangeName, "foo.bar.baz", 10000000);
    }

}

Just start this app and observe what happens. On start I have a one pod.

After a while (maybe 40 seconds) we see more pods created. The reason is current CPU Utilization.

In the database I got some new results.

After few minutes I received others results.

The pods turn off. Now available is only a one pod because CPU Utilization is 3%.