들어가기전

보통 클라이언트가 증가하면 부하 분산 및 성능향상을 위해 로드밸런싱 기술을 적용하곤 합니다.

저는 MQTT도 로드밸런싱이 가능한지에 대하여 알아보았고 그 방법이 무엇인지 공유해보려 합니다.

Nginx

NGINX는 높은 성능과 확장성을 가진 오픈 소스 웹 서버 및 리버스 프록시 서버입니다. 웹 서비스 및 애플리케이션을 처리하기 위한 HTTP 서버로 주로 사용되며, 로드 밸런싱, 캐싱, SSL/TLS 암호화, 웹 애플리케이션 방화벽 등의 기능을 지원합니다. 또한 NGINX는 매우 경량화되어 있어 메모리 사용량이 적고, 동시 연결 처리에 탁월한 성능을 발휘합니다.

위의 글을 읽어보면, Nginx의 특징 중 하나는 프록시 서버를 제공한다는 것입니다.

프록시 서버란 클라이언트와 서버사이에 존재하여 요청과 응답을 중계하는 서버이며, 주요 특징은 아래와 같습니다.

  1. 캐싱

    응답을 캐싱하여 동일한 요청에 활용한다.

  2. 보안

    클라이언트나 서버의 실제 IP 주소를 숨겨서 익명성을 제공 할 수 있다.

    또한, 프록시 서버는 외부에서 직접 서버에 접근하는 것을 차단하고 보안 정책을 적용하여 서버를 보호할 수 있다.

  3. 부하 분산

    프록시 서버는 여러대의 서버로 요청을 분산, 분배하여 효율적인 부하 분산을 실현할 수 있다.

  4. 접근 제어 및 필터링

    프록시 서버는 클라이언트의 요청과 응답을 필터링하여 특정 콘텐츠나 도메인에 대한 액세스를 제한하거나 허용할 수 있다. 이를 통해 웹 필터링, 권한 관리 등의 기능을 구현할 수 있다.

저는 nginx에서 제공하는 프록시 서버 기능을 이용하여 MQTT 로드밸런싱을 구성해보았습니다.

MQTT 로드밸런싱

MQTT를 로드밸런싱한다는 것은 다음과 같은 경우가 있습니다.

  1. 브로커를 로드밸런싱 한다.

    image

  2. 클라이언트를 로드밸런싱 한다.

    image

브로커 로드밸런싱

먼저, 브로커 로드밸런싱을 테스트 해봅니다.

mqtt 1번 브로커의 서버주소가 127.0.0.1:1883 이고, mqtt 2번 브로커의 서버주소가 127.0.0.1:1884 라고 하였을 때

nginx의 config 파일에 다음과 같이 추가해줍니다.

stream {
	upstream mqtt_broker {
		server  127.0.0.1:1883;
		server  127.0.0.1:1884;
	}

	server {
		listen 3300;
		proxy_pass      mqtt_broker;
	}
}

위의 코드는 3300으로 들어오는 mqtt tcp 접속을 upstream에 지정한 mqtt_broker 그룹으로 프록시 전달을 하도록 하는 설정입니다.

기본적으로 upstream은 라운드로빈 알고리즘으로 설정되어있지만, upstream 뒤에 least_conn, random 등 다른 알고리즘을 선택할 수도 있습니다.

그 다음, 3300 포트로 클라이언트 연결을 하면, 1번 브로커와 2번 브로커에 분산되어 접속되는 것을 아래와 같이 확인 할 수 있습니다.

클라이언트의 로드밸런싱

Nginx

여기서 클라이언트라 함은, message-collector를 예시로 들 수 있겠습니다.

현재 musma-iot-infra 구조를 보면 message-collector가 클라이언트로서 mosquitto-integration 브로커에 접속하여 구독 데이터를 읽어 옵니다.

이러한 구조에서 message-collector를 2개를 띄워 이중화를 하려 합니다.

처음에는 브로커를 로드밸런싱 했던 것과 같이 nginx를 통해 로드밸런싱을 하려고 생각하였습니다.

메세지 컬렉터 1번이 127.0.0.1:8787, 메세지 컬렉터 2번이 127.0.0.1:8888 에서 구동된다고 하였을 때

아래와 같이 설정하여 로드밸런싱 하려고 하였습니다.

stream {
	upstream mqtt_client {
		server  127.0.0.1:8787;
		server  127.0.0.1:8888;
	}

    upstream mqtt_broker {
        server  127.0.0.1:1883;
    }

	server {
		listen 3300;
		
        location / {
            proxy_pass  mqtt_client;
        }
        
        location /subscribe {
            proxy_pass  mqtt_broker;
        }
	}
}

구독 요청은 location /subscribe 을 통해 mqtt 브로커로 넘겨주고,

브로커에게서 데이터를 받을 때는 location / 을 통해 mqtt_client로 로드밸런싱 하자는 계획이였습니다.

하지만, 다시 생각해보니 mqtt는 tcp 기반 프로토콜이기 때문에 http 처럼 url을 지정해줄 수 없기 때문에 제가 생각한 방법은 전혀 동작하지 않게 되었습니다.

그렇게 다른 방법이 없을까 찾던 중, Mqtt5 부터 지원하는 shared subscription 이라는 것을 알게 되었습니다.

Shared Subscription

기존의 Subscription 방식은 같은 주제를 구독한 모든 클라이언트에게 브로커가 해당 데이터를 보내줍니다.

그렇다면 현재 메세지 컬렉터를 2개 구성 후, 같은 주제를 구독하게 되면 브로커는 메세지 컬렉터 2개 모두에게 데이터를 보내주고, 결국 데이터가 중복되어 2중으로 쌓이게 될 것입니다.

image

이를 해결하기 위해, MQTT 5 부터는, client를 그룹으로 묶어 메세지를 공유하며 수신하도록 하여 분산처리를 할 수 있는 Shared Subscription을 지원합니다.

위의 그림을 보면, 기존 client 1과 2는 my/topic을 구독하고 있기에, 해당 토픽의 메세지가 발행되면 동시게 같은 메세지를 받게 됩니다.

하지만, shared group 으로 지정된 client 1과 2는 각각 돌아가며 메세지를 받고 있는 것을 확인할 수 있습니다.

이를 적용하는 방법은 매우 간단합니다. MQTT5를 지원하는 브로커에 해당 형식($share/{group name}/{topic}) 으로 topic을 구독하면 됩니다.

이를 spring 으로 제작한 메세지 컬렉터에서 받는 모습을 아래와 같이 확인할 수 있습니다.

단순히 topic을 해당 형식으로 지정해주었는데, message-collector 1번과 2번으로 나뉘어 들어가는 것을 확인할 수 있습니다.

마치며…

브로커와 클라이언트를 이중화하여 로드밸런싱하는 방법을 간단하게 살펴보았습니다.

하는 방법은 아주 간단하고 쉽지만, 사실 공부하고 찾아보면서도 이게 굳이 필요한가? 라는 생각을 계속해서 하게 되었습니다.

그래서, 다음 계획은 브로커나 클라이언트가 어느정도까지 트래픽을 처리할 수 있는지 테스트 해보려합니다. 감사합니다~