Slow Socket IO response when using Docker
Asked Answered
N

2

6

I have a Web App built in Flask where tweets are captured (using Tweepy library) and displayed on the front-end. I used Socket IO to display the tweets live on the front-end.

My code works fine when I run this locally. The tweets appear instantly.

However, when i Dockerized the web app, the front-end doesn't update immediately. It takes some time to show the changes (sometimes I think tweets are lost due to the slowness)

Below are code extracts from my website:

fortsocket.js

$(document).ready(function () {



/************************************/
  /*********** My Functions ***********/
  /************************************/
  function stream_active_setup() {
    $("#favicon").attr("href", "/static/icons/fortnite-active.png");
    $("#stream-status-ic").attr("src", "/static/icons/stream-active.png");
    $("#stream-status-text").text("Live stream active");
  }

  function stream_inactive_setup() {
    $("#favicon").attr("href", "/static/icons/fortnite-inactive.png");
    $("#stream-status-ic").attr("src", "/static/icons/stream-inactive.png");
    $("#stream-status-text").text("Live stream inactive");
  }



  /*********************************/
  /*********** My Events ***********/
  /*********************************/

  // Socket connection to server

  // Prometheus
  //var socket = io.connect('http://104.131.173.145:8083');

  // Local
  var socket = io.connect(window.location.protocol + '//' + document.domain + ':' + location.port);

  // Heroku
  //var socket = io.connect('https://fortweet.herokuapp.com/');

  // Send a hello to know
  // if a stream is already active
  socket.on('connect', () => {
    socket.emit('hello-stream', 'hello-stream');
  });

  // Listene for reply from hello
  socket.on('hello-reply', function (bool) {
    if (bool == true) {
      stream_active_setup()
    } else {
      stream_inactive_setup()
    }
  });

  // Listens for tweets
  socket.on('stream-results', function (results) {

    // Insert tweets in divs
    $('#live-tweet-container').prepend(`
    <div class="row justify-content-md-center mt-3">
      <div class="col-md-2">
          <img width="56px" height="56px"  src="${results.profile_pic !== "" ? results.profile_pic : "/static/icons/profile-pic.png"}" class="mx-auto d-block rounded"  alt="">
      </div>
      <div class="col-md-8 my-auto">
        <div><b>${results.author}</b></div>
        <div>${results.message}</div>
      </div>
    </div>
    `);
  });

  // Listener for when a stream of tweets starts
  socket.on('stream-started', function (bool) {
    if (bool == true) {
      stream_active_setup()
    }
  });

  // Listener for when a stream of tweets ends
  socket.on('stream-ended', function (bool) {
    if (bool == true) {
      stream_inactive_setup()
    }
  });

});

init.py

# Create the app
app = create_app()

# JWT Configurations
jwt = JWTManager(app)

# Socket IO
socketio = SocketIO(app, cors_allowed_origins="*")

# CORS
CORS(app)
app.config["CORS_HEADERS"] = "Content-Type"

# Creates default admins and insert in db
create_default_admin()

# Main error handlers
@app.errorhandler(404)  # Handling HTTP 404 NOT FOUND
def page_not_found(e):
    return Err.ERROR_NOT_FOUND


# Listen for hello emit data
# from client
@socketio.on("hello-stream")
def is_stream_active(hello_stream):
    emit("hello-reply", streamer.StreamerInit.is_stream_active(), broadcast=True)

streamer.py

import time
import tweepy
import threading as Coroutine
import app.messages.constants as Const
import app.setup.settings as settings_mod
import app.models.tweet as tweet_mod
import app.services.logger as logger
import app


class FStreamListener(tweepy.StreamListener):
    def __init__(self):
        self.start_time = time.time()
        self.limit = settings_mod.TwitterSettings.get_instance().stream_time

        logger.get_logger().debug("Live capture has started")

        # Notify client that a live capture will start
        app.socketio.emit(
            "stream-started", True, broadcast=True,
        )

        super(FStreamListener, self).__init__()

    def on_status(self, status):
        if (time.time() - self.start_time) < self.limit:

            # Create tweet object
            forttweet = tweet_mod.TweetModel(
                status.source,
                status.user.name,
                status.user.profile_background_image_url_https,
                status.text,
                status.created_at,
                status.user.location,
            )

            # Emit to socket
            app.socketio.emit(
                "stream-results",
                {
                    "profile_pic": forttweet.profile_pic,
                    "author": forttweet.author,
                    "message": forttweet.message,
                },
                broadcast=True,
            )

            # Add to database
            forttweet.insert()

            return True
        else:
            logger.get_logger().debug("Live capture has ended")

            # Notify client that a live capture has ended
            app.socketio.emit(
                "stream-ended", True, broadcast=True,
            )

            # Stop the loop of streaming
            return False

    def on_error(self, status):
        logger.get_logger().debug(f"An error occurred while fetching tweets: {status}")
        raise Exception(f"An error occurred while fetching tweets: {status}")


class StreamerInit:

# [Private] Twitter configurations
def __twitterInstantiation(self):
    # Get settings instance
    settings = settings_mod.TwitterSettings.get_instance()
    # Auths
    auth = tweepy.OAuthHandler(settings.consumer_key, settings.consumer_secret,)
    auth.set_access_token(
        settings.access_token, settings.access_token_secret,
    )
    # Get API
    api = tweepy.API(auth)
    # Live Tweets Streaming
    myStreamListener = FStreamListener()
    myStream = tweepy.Stream(auth=api.auth, listener=myStreamListener)
    myStream.filter(track=settings.filters)

def start(self):
    for coro in Coroutine.enumerate():
        if coro.name == Const.FLAG_TWEETS_LIVE_CAPTURE:
            return False

    stream = Coroutine.Thread(target=self.__twitterInstantiation)
    stream.setName(Const.FLAG_TWEETS_LIVE_CAPTURE)
    stream.start()

    return True

@staticmethod
def is_stream_active():
    for coro in Coroutine.enumerate():
        if coro.name == Const.FLAG_TWEETS_LIVE_CAPTURE:
            return True

    return False

The streamer.py is called on a button click

Dockerfile

# Using python 3.7 in Alpine
FROM python:3.6.5-stretch

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
ADD . /app

RUN apt-get update -y && apt-get upgrade -y && pip install -r requirements.txt

# Run the command
ENTRYPOINT ["uwsgi", "app.ini"]

#ENTRYPOINT ["./entry.sh"]

docker-compose.yml

version: "3.8"

services:
  fortweet:
    container_name: fortweet
    image: mervin16/fortweet:dev
    build: ./
    env_file:
      - secret.env
    networks:
      plutusnet:
        ipv4_address: 172.16.0.10
    expose:
      - 8083
    restart: always

  nginx_fortweet:
    image: nginx
    container_name: nginx_fortweet
    ports:
      - "8083:80"
    networks:
      plutusnet:
        ipv4_address: 172.16.0.100
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - fortweet
    restart: always

networks:
  plutusnet:
    name: plutus_network
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.16.0.0/24
          gateway: 172.16.0.1

app.ini

[uwsgi]

module = run:app

master = true
processes = 5

# Local & Prometheus
http-socket = 0.0.0.0:8083

http-websockets = true

chmod-socket = 660
vacuum = true

die-on-term = true

For a full, updated code, you can find it here under the branch dev/mervin

Any help is appreciated.

Neighbors answered 20/1, 2021 at 21:21 Comment(4)
Are you using the nginx server also when testing locally? Could you provide the configuration? Could you test your app with docker but without nginx as proxy?Marty
As @Marty pointed out, if you're not using nginx when testing locally then the two situations are not equivalent and you should first test a simpler hello world kind of web app to check any issueBraces
@Marty and Piyush I tried removing the nginx part and the issue is still the same. The tweets arrive late. I suspect this has to do with a wrong UWSGI configurations. Is there something i am doing wrong on the UWSGI part ?Neighbors
by the way why are you running your docker network under bridge instead of host? do you have other containers that you need them connected to the same bridge network? if not then remove all ip references , change it to hostwhich bind directly to the the network of your docker host.Johnsten
J
1

in order to see if ipv6 is responsible i would suggest you shutdown everything

open /etc/sysctl.conf and add the following lines to disable ipv6

net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1

run sudo sysctl -p so changes can take effect

start nginx and the docker again

if you dont see any difference then you can just change the settings to 0 and rerun sysctl -p and let me know

Johnsten answered 29/1, 2021 at 14:55 Comment(0)
A
0

Unfortunately I can't reproduce the issue without the configuration, so I can't verify my answer.

I was able to find a similar issue on a JP's blog: Performance problems with Flask and Docker

In short, it might be that having both IPv6 and IPv4 configs on the container are causing the issue. In order to verify the issue:

  1. Run the docker
  2. Go inside the running container and change the hosts file so that it won't map IPv6 to localhost
  3. Run application again inside of container

If the app runs smoothly then you've identified your issue. The solution would be to tweak the uwsgi parameters.

What the author did in the blog post:

CMD uwsgi -s /tmp/uwsgi.sock -w project:app --chown-socket=www-data:www-data --enable-threads & nginx -g 'daemon off;'

Azo answered 27/1, 2021 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.