Nginx + Docker Manual Cluster Run EMQ

Posted by kishore_marti on Sat, 03 Aug 2019 05:14:43 +0200

In the process of supporting customers, EMQ X learns that customers use Nginx for load balancing and that the Docker container manually joins the cluster to run the EMQ cluster. The main process is now recorded.

Business Requirements

  • Use Nginx as a reverse proxy
  • Nginx needs to assign the address of the proxy server in advance
  • Run EMQ using Docker container
  • EMQ Auto Restart
  • Automatic Clustering after EMQ Restart

To configure

Nginx Configuration

$ cat /etc/nginx/tcpstream.conf## tcp LB  and SSL passthrough for backend ##stream {
    upstream mqtt_broker {
        server 127.0.0.1:21871; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21872; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21873; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21874; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21875; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21881; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21891; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21882; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21892; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21883; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21893; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21884; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21894; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21885; #max_fails=5 fail_timeout=30s;
        server 127.0.0.1:21895; #max_fails=5 fail_timeout=30s;
    }

log_format basic '$proxy_protocol_addr - $remote_addr [$time_local] '
                 '$protocol $status $bytes_sent $bytes_received '
                 '$session_time "$upstream_addr" '
                 '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';

    access_log /var/log/nginx/access.log basic;
    error_log  /var/log/nginx/error.log;

    server {
        listen 8884 ssl; # proxy_protocol;
        proxy_next_upstream on;
        #proxy_bind $remote_addr transparent;
        proxy_ssl off;
        proxy_pass mqtt_broker;
        proxy_protocol on;
        #ssl_on;
        # adding some extra proxy settings
        proxy_timeout 350s;
        #proxy_buffer_size 128k;

        #ssl_certificate /etc/nginx/certs/solace.pem;
        #ssl_certificate_key /etc/nginx/certs/solace.pem;
        ssl_certificate /etc/nginx/certs/cert.pem;
        ssl_certificate_key /etc/nginx/certs/key.pem;
        #ssl_verify_client off;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;
    }
}

Docker Configuration

Customer-compiled Docker image s are not an official mirror provided by EMQ.

The Dockerfile directory is as follows:

$ ll /opt/Docker/Total usage 28
-rw-r--r--  1 alexeyp emq      620 10 February 2217:26 Dockerfile
lrwxrwxrwx  1 alexeyp emq       13 10 24/13:59 emqttd -> emqttd.2.3.11
drwxr-xr-x 10 alexeyp emq      110 10 24/24:27 emqttd.2.3.11
-rwxr-xr-x  1 alexeyp emq     3463 10 February 2605:03 StartEmqInstance.sh
-rwxr-xr-x  1 alexeyp alexeyp  270 10 February 2510:46 status.sh

Dockerfile:

$ cat DockerfileFROM centos:latest

RUN yum -y update

EXPOSE 60000-65000

WORKDIR /opt/emqttd
ADD ./emqttd /opt/emqttd
ADD ./vsparc.rpm /tmp/vsparc.rpm
ADD ./StartEmqInstance.sh /opt/emqttd/StartEmqInstance.sh

RUN yum install -y epel-release
RUN yum install -y which less sed net-tools telnet gtest /tmp/vsparc.rpm

ENV TZ Australia/Melbourne

CMD bash /opt/emqttd/StartEmqInstance.sh && bash

You can see that the Docker container executes a script called StartEmqInstance.sh when it is started to view the script:

$ cat StartEmqInstance.sh#!/bin/bashDIR=$(dirname $0)
HOSTNAME=$(hostname -s)

function adjust_instance()
{
    local INST=$1
    local INST_ROOT=$2

    cat $INST_ROOT/etc/emq.conf | \
       sed -re "s/^node\.name\s*=.*$/node.name = emq$INST@127.0.0.1/" | \
       #sed -re "s/^cluster\.name\s*=.*$/cluster.name = $HOSTNAME/" | \
       sed -re "s/^listener\.tcp\.external\s*=.*$/listener.tcp.external = 0.0.0.0:6188$INST/" | \
       sed -re "s/^listener\.tcp\.external1\s*=.*$/listener.tcp.external1 = 0.0.0.0:6189$INST/" | \
       sed -re "s/^listener\.tcp\.external2\s*=.*$/listener.tcp.external2 = 0.0.0.0:6187$INST/" | \
       sed -re "s/^listener\.tcp\.internal\s*=.*$/listener.tcp.internal = 127.0.0.1:6298$INST/" | \
       sed -re "s/^listener\.ssl\.external\s*=.*$/listener.ssl.external = 6288$INST/" | \
       sed -re "s/^listener\.ws\.external\s*=.*$/listener.ws.external = 6208$INST/" | \
       sed -re "s/^listener\.wss\.external\s*=.*$/listener.ws.external = 6308$INST/" | \
       sed -re "s/^listener\.api\.mgmt\s*=.*$/listener.api.mgmt = 6408$INST/" | \
       sed -re "s/^(##\s)?listener\.tcp\.external\.proxy_protocol\s=.*$/listener.tcp.external.proxy_protocol = on/" | \
       sed -re "s/^(##\s)?listener\.tcp\.external1\.proxy_protocol\s=.*$/listener.tcp.external1.proxy_protocol = on/" | \
       sed -re "s/^(##\s)?listener\.tcp\.external2\.proxy_protocol\s=.*$/listener.tcp.external2.proxy_protocol = on/" | \
       sed -re "s/^(##\s)?listener\.tcp\.external\.proxy_protocol_timeout\s=.*$/listener.tcp.external.proxy_protocol_timeout = 30s/" | \
       sed -re "s/^(##\s)?listener\.tcp\.external1\.proxy_protocol_timeout\s=.*$/listener.tcp.external1.proxy_protocol_timeout = 30s/" | \
       sed -re "s/^(##\s)?listener\.tcp\.external2\.proxy_protocol_timeout\s=.*$/listener.tcp.external2.proxy_protocol_timeout = 30s/" | \
       sed -re "s/^(##\s)?node.dist_listen_min\s*=.*$/node.dist_listen_min = 6000$INST/" | \
       sed -re "s/^(##\s)?node.dist_listen_max\s*=.*$/node.dist_listen_max = 6000$INST/" | \
       cat - > $INST_ROOT/etc/emq.conf.new
    mv $INST_ROOT/etc/emq.conf.new $INST_ROOT/etc/emq.conf
}

function cluster_instance()
{
    local INST=$1

    for DEST in 1 2 3 4 5; do
        if [ $DEST == $INST ]; then
            continue;
        fi
        DEST_NODE="emq$DEST@127.0.0.1"
        RESULT=$(/opt/emqttd/bin/emqttd_ctl cluster join $DEST_NODE 2>&1)
        echo "$RESULT"
        echo "$RESULT" | grep -E 'successfully|already' > /dev/null
        RC=$?
        [ $RC == 0 ] && break
    done
}

cd "$DIR"

if [ "$EMQ_INSTANCE_NUMBER" == "" ]; then
    echo "Environment variable EMQ_INSTANCE_NUMBER(1..10) is not set."
    echo "eMQ instance name is not configured."
    exit 1
else
    adjust_instance $EMQ_INSTANCE_NUMBER $DIR
fi

function run_application()
{
    local CMD="$1"
    local RC=1
    while [ $RC != 0 ]; do
        $CMD
        RC=$?
        echo "### Exited: $CMD"
        echo "### rc = $RC"
        #[ $RC != 0 ] && sleep 3
        RC=1
    done
    echo "### Done: $CMD"
}

function start_node()
{
    bin/emqttd start
    STARTED=0
    while [ $STARTED == 0 ]; do
        sleep 1
        /opt/emqttd/bin/emqttd_ctl status | grep "is running"
        [ $? == 0 ] && break
    done
    cluster_instance $EMQ_INSTANCE_NUMBER > /tmp/cluster_instance.log
}

start_node
sleep 5
run_application "/usr/local/bin/emqtt-stats-collector" &#waitIDLE_TIME=0
while [[ $IDLE_TIME -lt 5 ]]
do
    IDLE_TIME=$((IDLE_TIME+1))
    if [[ ! -z "$( /opt/emqttd/bin/emqttd_ctl status|grep 'is running'|awk '{print $1}')" ]]; then
        IDLE_TIME=0
    else
        echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:emqttd not running, waiting for recovery in $((60-IDLE_TIME*5)) seconds"
    fi
    sleep 5
done

echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:emqttd exit abnormally"
exit 1

The script is slightly more complex and needs to be viewed in conjunction with the start.sh script and etc/emq.conf

$ cat start.sh#!/bin/bashfor INST in 1 2 3 4 5
do
    docker ps | grep -E "\sinstance_$INST$"
    if [ $? != 0 ]; then
        #docker run -itd ---ulimit nofile=1048576 -restart=always -v /opt/Docker/emqtt/emq$INST/data/mnesia:/opt/emqttd/data/mnesia  -e EMQ_INSTANCE_NUMBER=$INST --name=instance_$INST --network host emq:test &
        docker run -itd --ulimit nofile=1048576 -e EMQ_INSTANCE_NUMBER=$INST --name=instance_$INST --network host emq:latest &
    fi
done

wait

EMQ Configuration

etc/emq.conf`The full text will not be posted out, mainly by adding two tcp Listen on port and close`listener.tcp.external.tune_buffer
$ cat etc/emq.conf......
##--------------------------------------------------------------------

listener.tcp.external = 0.0.0.0:21881

listener.tcp.external.acceptors = 16

listener.tcp.external.max_clients = 512000

listener.tcp.external.access.1 = allow all

listener.tcp.external.proxy_protocol = on

listener.tcp.external.proxy_protocol_timeout = 30s

listener.tcp.external.backlog = 1024

listener.tcp.external.send_timeout = 15s

listener.tcp.external.send_timeout_close = on
## listener.tcp.external.tune_buffer = on

listener.tcp.external.nodelay = true

listener.tcp.external.reuseaddr = true
##--------------------------------------------------------------------

listener.tcp.external1 = 0.0.0.0:21891

listener.tcp.external1.acceptors = 16

listener.tcp.external1.max_clients = 512000

listener.tcp.external1.access.1 = allow all

listener.tcp.external1.proxy_protocol = on

listener.tcp.external1.proxy_protocol_timeout = 30s

listener.tcp.external1.backlog = 1024

listener.tcp.external1.send_timeout = 15s

listener.tcp.external1.send_timeout_close = on

## listener.tcp.external1.tune_buffer = on

listener.tcp.external1.nodelay = true

listener.tcp.external1.reuseaddr = true

##--------------------------------------------------------------------

listener.tcp.external2 = 0.0.0.0:21871

listener.tcp.external2.acceptors = 16

listener.tcp.external2.max_clients = 512000

listener.tcp.external2.access.1 = allow all

listener.tcp.external2.proxy_protocol = on

listener.tcp.external2.proxy_protocol_timeout = 30s

listener.tcp.external2.backlog = 1024

listener.tcp.external2.send_timeout = 15s

listener.tcp.external2.send_timeout_close = on

## listener.tcp.external2.tune_buffer = on

listener.tcp.external2.nodelay = true

listener.tcp.external2.reuseaddr = true
......

Business Analysis

Docker container initialization

After the Docker container is created, StartEmqInstance.sh executes adjust_instance() to modify the port listened on in etc/emq.conf to the proxy server for Nginx

 sed -re "s/^node\.name\s*=.*$/node.name = emq$INST@127.0.0.1/" | \
 sed -re "s/^listener\.tcp\.external\s*=.*$/listener.tcp.external = 0.0.0.0:6188$INST/" 
 sed -re "s/^listener\.tcp\.external1\s*=.*$/listener.tcp.external1 = 0.0.0.0:6189$INST/" 
 sed -re "s/^listener\.tcp\.external2\s*=.*$/listener.tcp.external2 = 0.0.0.0:6187$INST/" 
 sed -re "s/^listener\.tcp\.internal\s*=.*$/listener.tcp.internal = 127.0.0.1:6298$INST/" 

Cluster functionality is implemented through the join command

function cluster_instance()
{
    local INST=$1

    for DEST in 1 2 3 4 5; do
        if [ $DEST == $INST ]; then
            continue;
        fi
        DEST_NODE="emq$DEST@127.0.0.1"
        RESULT=$(/opt/emqttd/bin/emqttd_ctl cluster join $DEST_NODE 2>&1)
        echo "$RESULT"
        echo "$RESULT" | grep -E 'successfully|already' > /dev/null
        RC=$?
        [ $RC == 0 ] && break
    done
}

Loop checks the status of EMQ and exits the container when EMQ is stopped

IDLE_TIME=0
while [[ $IDLE_TIME -lt 5 ]]
do
    IDLE_TIME=$((IDLE_TIME+1))
    if [[ ! -z "$( /opt/emqttd/bin/emqttd_ctl status|grep 'is running'|awk '{print $1}')" ]]; then
        IDLE_TIME=0
    else
        echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:emqttd not running, waiting for recovery in $((60-IDLE_TIME*5)) seconds"
    fi
    sleep 5
done

echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:emqttd exit abnormally"
exit 1

Visit

Clients connect to <Nginx IP:8884>addresses via SSL, and Nginx loads connections to EMQ nodes via TCP.

PS: Refer to EMQ X message server Nginx reverse proxy for setting up how Nginx reverse proxy tcp and ssl

Auto restart and auto cluster

The state of EMQ is queried by StartEmqInstance.sh script after the container is started. When EMQ stops, it exits the container and cooperates with--restart=always to restart the container.

EMQ stores cluster information in data/mnesia, maps directories in containers to hosts, reads the related directories mapped by hosts when containers are restarted, and implements automatic clustering after restart.

Problem

  • Docker's host network mode uses the host's network and is prone to port conflicts when the host has other operations running

Solution

  • Modify/proc/sys/net/ipv4/ip_local_port_range to specify that the system assigns a port of 1024 60000, and then assign EMQ's business port to a port after 60000

Practice Cases

It is recommended that you use kubernetes to organize docker containers:

  • EMQ can achieve automatic clustering through kube-apiserver.
  • The client currently only deploys a docker cluster on a single machine, which can be easily deployed between multiple nodes using kubernetes.
  • The deployment of kubernetes can monitor the status of emqx pod, achieve automatic restart, elastic expansion, and other functions.
  • Each emqx pod has its own virtual IP, so there is no port conflict.
  • kubernetes'Service implements fixed IP and load balancing requirements. In requests created by Service, you can specify your own cluster IP address by setting the spec.cluster IP field, set Nginx's proxy server to cluster IP, and Service can achieve load balancing on its own.

For more information please visit our website emqx.io Or follow our open source projects github.com/emqx/emqx , please visit the detailed documentation Official Documents.

Topics: Linux Nginx Docker SSL network