Skip to content

Instantly share code, notes, and snippets.

@bademux
Last active March 4, 2020 10:55
Show Gist options
  • Save bademux/141f242ff1f1f0539dc73c83b3f928d9 to your computer and use it in GitHub Desktop.
Save bademux/141f242ff1f1f0539dc73c83b3f928d9 to your computer and use it in GitHub Desktop.
package com.example.demo;
import util.Healthchecker;
public class DemoApplicationHealthcheck {
public static void main(String[] args) throws Exception {
Healthchecker.registerHealthcheckServer(DemoApplication::healthcheck);
System.out.println("--- startring app ");
}
private static boolean healthcheck() {
boolean health = true;
System.out.println("--- healthcheck method invoked: " + health);
return health;
}
}
FROM gcr.io/distroless/java:11
HEALTHCHECK --start-period=5s --interval=3s --retries=3 --timeout=5s CMD ["java", "-Xint", "-Xmx10M", "-cp", "/app/classes", "util.Healthchecker"]
package util;
import javax.management.*;
import javax.management.remote.JMXServiceURL;
import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.function.BooleanSupplier;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static javax.management.MBeanOperationInfo.ACTION_INFO;
import static javax.management.remote.JMXConnectorFactory.connect;
/**
* JMX-based, uses ~41Mb RAM for probing on Hotspot11
*/
public final class Healthchecker {
private static final String OP_HEALTHCHECK = "healthcheck";
private static final ObjectName objectName = createObjectName();
public static void main(String[] args) throws Exception {
System.exit(probeLocalhost() ? 0 : 1);
}
public static void registerHealthcheckServer(BooleanSupplier healthcheckHandler) throws Exception {
ManagementFactory.getPlatformMBeanServer()
.registerMBean(new HealthcheckBean(OP_HEALTHCHECK, healthcheckHandler), objectName);
}
public static boolean probeLocalhost() throws Exception {
return probe("localhost", 9999);
}
public static boolean probe(String host, int port) throws Exception {
try (var connector = connect(new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi"))) {
return (boolean) connector.getMBeanServerConnection()
.invoke(objectName, OP_HEALTHCHECK, null, null);
}
}
private static ObjectName createObjectName() {
try {
return new ObjectName("com.github.bademux.jmx", "type", "HealthcheckBean");
} catch (MalformedObjectNameException e) {
throw new RuntimeException(e);
}
}
private static final class HealthcheckBean implements DynamicMBean {
private final BooleanSupplier handler;
private final MBeanInfo beanInfo;
private final String operationName;
public HealthcheckBean(String operationName, BooleanSupplier handler) {
this.operationName = requireNonNull(operationName);
this.handler = requireNonNull(handler);
MBeanOperationInfo opInfo = new MBeanOperationInfo(operationName, "Run healthcheck", null, "boolean", ACTION_INFO);
beanInfo = new MBeanInfo(this.getClass().getName(), "JMX healthcheck", null, null, new MBeanOperationInfo[]{opInfo}, null);
}
@Override
public MBeanInfo getMBeanInfo() {
return beanInfo;
}
@Override
public Object invoke(String operationName, Object[] params, String[] signature) throws MBeanException {
if (this.operationName.equals(operationName)) {
return handler.getAsBoolean();
}
throw new MBeanException(new NoSuchMethodException(operationName),
format("Cannot find the operation '%s' expecting: '%s'", operationName, this.operationName));
}
@Override
public Object getAttribute(String attribute) {
throw new UnsupportedOperationException("unavailable operation for " + attribute);
}
@Override
public void setAttribute(Attribute attribute) {
throw new UnsupportedOperationException("unavailable operation for " + attribute);
}
@Override
public AttributeList getAttributes(String[] attributes) {
throw new UnsupportedOperationException(String.format("unavailable operation for %s", Arrays.toString(attributes)));
}
@Override
public AttributeList setAttributes(AttributeList attributes) {
throw new UnsupportedOperationException("unavailable operation for " + attributes);
}
}
}
package util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.function.BooleanSupplier;
/**
* Socket-based, uses ~23Mb RAM for probing on Hotspot11
*/
public final class Healthchecker {
public static final int PORT = 9998;
public static final int PROBE_TIMEOUT_MILLIS = 1000;
// --- Healthcheck Client. Invoked by docker healthcheck
public static void main(String[] args) {
System.exit(probe());
}
private static int probe() {
try (var socket = new Socket(InetAddress.getLocalHost(), PORT)) {
socket.setSoTimeout(PROBE_TIMEOUT_MILLIS);
socket.setOOBInline(true);
socket.sendUrgentData(0);
return socket.getInputStream().read();
} catch (Exception e) {
return -2;
}
}
// --- Healthcheck Server. Executed in application runtime
public static void registerHealthcheckServer(BooleanSupplier healthcheckHandler) {
var serverThread = new Thread(() -> runServer(healthcheckHandler), "Healthchecker");
serverThread.setPriority(Thread.MIN_PRIORITY);
Runtime.getRuntime().addShutdownHook(new Thread(serverThread::interrupt));
serverThread.start();
}
private static void runServer(BooleanSupplier healthcheckHandler) {
try (var listener = new ServerSocket(PORT, 1, InetAddress.getLocalHost())) {
while (true) {
handleRequest(healthcheckHandler, listener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void handleRequest(BooleanSupplier healthcheckHandler, ServerSocket listener) throws InterruptedException {
try (var socket = listener.accept()) {
socket.setOOBInline(true);
int command = socket.getInputStream().read();
socket.sendUrgentData(command == -1 || !healthcheckHandler.getAsBoolean() ? -1 : 0);
} catch (IOException e) {
e.printStackTrace();
Thread.sleep(100);
}
}
}
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for healthcheck on distroless-based containers using JMX</description>
<properties>
<java.version>11</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<to>
<image>distroless_docker_healthchecked:latest</image>
</to>
<from>
<image>gcr.io/distroless/java:11</image>
</from>
<container>
<mainClass>com.example.demo.DemoApplication</mainClass>
<args>
<arg>-Djava.security.egd=file:/dev/./urandom</arg>
</args>
<jvmFlags>
<jvmFlag>-XX:MaxRAMPercentage=96</jvmFlag>
<!-- jmxremote is used fo healthchecking app-->
<!-- Port must not be never exposed without auth-->
<jvmFlag>-Dcom.sun.management.jmxremote.port=9999</jvmFlag>
<jvmFlag>-Dcom.sun.management.jmxremote.authenticate=false</jvmFlag>
<jvmFlag>-Dcom.sun.management.jmxremote.ssl=false</jvmFlag>
</jvmFlags>
<user>nonroot</user>
<format>Docker</format>
</container>
</configuration>
</plugin>
</plugins>
</build>
</project>
{
"containerDefinitions": [
{
"healthCheck": {
"retries": 5,
"command": [
"CMD",
"java",
"-Xint",
"-Xmx10M",
"-cp",
"/app/classes",
"util.Healthchecker"
]
}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment