Skip to content

Parse LiveQuery Excessive CPU #5106

Closed
Closed

Description

@wfilleman

Issue Description

I've got a parse server in production in an IoT environment where it's processing around 100 requests a second. This all works great with CPU around 30% and 25ms response times. The parse server is clustered on AWS on two C5.Large instances.

I'm adding in LiveQuery as a separate C5.Large server. Both Parse server and LiveQuery are using Redis to communicate changes and this is working.

The problem I'm seeing is the LiveQuery server with just one client connected has CPU usage between 20-35%. Two clients connected and this jumps to around 40%. More than 3 clients connected and the server crashes within minutes.

I'm looking for some suggestions as to what to try to figure out why the excessive CPU usage and then server crash. To be clear, subscriptions all work from Parse Server to LiveQuery to the client.

More information:

  • Parse Server version: 3.0.0
  • Parse Client: 2.1.0
  • Number of Classes monitored by Live Query: 12
  • Using Role ACL
  • Using Session Token in client subscriptions

Here is how Live Query server is configured:

let parseApi = new ParseServer({
    databaseURI: `mongodb://${config.get('/mongo/userName')}:${config.get('/mongo/password')}@${config.get('/mongo/uri')}`, // Connection string for your MongoDB database
    appId: config.get('/parse/appId'),
    masterKey: config.get('/parse/masterKey'), // Keep this key secret!
    serverURL: `http://127.0.0.1:${config.get('/port/webapp')}/parse`,
    logLevel: "ERROR",
    sessionLength: ONE_DAY, // in seconds. Set to 24 hours.
    schemaCacheTTL: ONE_MONTH_MS, //"The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable."
    cacheTTL: ONE_DAY_MS, //"Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)"
    cacheMaxSize: 1000000, //"Sets the maximum size for the in memory cache, defaults to 10000"
    enableSingleSchemaCache: true //"Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA. Defaults to false, i.e. unique schema cache per request."
});

// Serve the Parse API on the /parse URL prefix
app.use('/parse', parseApi);

let port = config.get('/port/webapp');
let server = app.listen(port);

// Initialize a LiveQuery server instance, app is the express app of your Parse Server
if (config.get('/parseAppServerIsLocal')) {
    debug(`Starting Live Query Server on port ${config.get('/port/parseLiveQuery')}`);
    let httpServer = require('http').createServer(app);
    httpServer.listen(config.get('/port/parseLiveQuery'));
    let liveQueryParams = {
        redisURL: config.get('/server/redis')
    };
    let parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer,liveQueryParams);
}

Steps to reproduce

Configure Parse server/LiveQuery per above with Redis and connect one client that subscribes to all 12 classes. Queries are not complex. Example:

const nodeStatusesQuery = new ParseObj.Query('NodeStatus').limit(10000);

Observer jump in CPU usage in high-throughput (100 requests per second to parse server).

Expected Results

I'm not sure what the CPU usage should be, but 30% for one client, 40-50% for two and crashing after that doesn't seem right.

Actual Outcome

LiveQuery server with just one client connected has CPU usage between 20-35%. Two clients connected and this jumps to around 40%. More than 3 clients connected and the server crashes within minutes.

Environment Setup

  • Server

    • parse-server version (Be specific! Don't say 'latest'.) : 3.0.0
    • Operating System: Linux
    • Hardware: AWS ELB C5.Large instance
    • Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): AWS
  • Database

    • MongoDB version: 3.4.14
    • Storage engine: Not sure
    • Hardware: mLab hosted on AWS
    • Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): mLab

Logs/Trace

Activity

  1. flovilmart commented on Oct 7, 2018

    @flovilmart
    Contributor

    While this may seem excessive, this ‘kindof’ make sense. Would you be able to provide profiling info on the liveQuery server? Perhaps an obvious optimization can be made. I’m thinking JSON serialization bottlenecks

  2. wfilleman commented on Oct 7, 2018

    @wfilleman
    Author

    Absolutely, what would you recommend? I’ve never had to do profiling with Node before.

  3. flovilmart commented on Oct 7, 2018

    @flovilmart
    Contributor

    So that's almost easy :)

    Enable inspect mode on your node process

    • Get the <pid> of your live query process with ps aux | grep node
    • Send the USR1 signal to this processkill -usr1 <pid> (This will not kill the node process but send the USR1 signal. node is built to enable the inspector when receiving this signal).

    Forward the port

    By default the debugger interface for your process should start on port 9229, if you're running the process on a remote server, then you need to ensure the port 9229 is properly open for inspection.

    Chrome inspector

    We use the chrome inspector to gather the profiles and process, open a new chrome tab an in the URL bar, enter: chrome://inspect

    capture d ecran le 2018-10-07 a 17 47 12

    Locally this is what I see when debugging the parse-live-query-server process locally, in your case, this may be different:

    capture d ecran le 2018-10-07 a 17 51 46

    Hit the inspect link and start recording the profile

    capture d ecran le 2018-10-07 a 17 52 04

  4. wfilleman commented on Oct 7, 2018

    @wfilleman
    Author

    Awesome, I can get the results posted tomorrow. Thanks!

  5. flovilmart commented on Oct 7, 2018

    @flovilmart
    Contributor

    Nice, also, do not profile for too long, profiles are huge :) given what you describe we should hopefully be able to see bottlenecks without too much digging

  6. wfilleman commented on Oct 7, 2018

    @wfilleman
    Author

    Good point. Will do.

  7. wfilleman commented on Oct 8, 2018

    @wfilleman
    Author

    Hi @flovilmart,

    See attached for the 3 node processes running on the live query server. One process is the watchdog/master that spawns the other two node servers for each of the 2 available CPUs on a C5.Large.

    Each was captured for about 60 seconds of profile data while there was one client connected and the CPU was around 30%.

    Let me know what else I can capture for you. I really appreciate the help!

    LiveQuery-HighCPU.zip

    Wes

  8. wfilleman commented on Oct 8, 2018

    @wfilleman
    Author

    FYI: Your directions were nearly perfect. The one step I had to do on the remote server was to start the node app/Live Query with:

    node --inspect=10.0.1.77 server.js

    The --inspect=10.0.1.77 line tells the inspector to attach to it's local IP address.

  9. flovilmart commented on Oct 8, 2018

    @flovilmart
    Contributor

    Thanks @wfilleman. Are you able to record a profile when your CPU saturates? At this time the profile doesn't show any particular bottleneck on the live query server beside the fact we spend a lot of time in internals of process.nextTick() See below the time usage of runMicrotasks internal:

    capture d ecran le 2018-10-08 a 09 47 32

  10. wfilleman commented on Oct 8, 2018

    @wfilleman
    Author

    Hi @flovilmart,

    I connected 4 clients to my LiveQuery server and was able to capture 2 of the 3 processes for about 60 seconds each before the server crashed. Hopefully, this will give you better insight as to what's happening. I'm pretty sure the one titled Process2-10-8-2018 is the LiveQuery instance. The process3 one is the watchdog.

    Thanks!
    Wes
    LiveQuery-HighCPU-10-8-2018.zip

  11. flovilmart commented on Oct 8, 2018

    @flovilmart
    Contributor

    Awesome thanks! I’ll have a look

  12. wfilleman commented on Oct 8, 2018

    @wfilleman
    Author

    @flovilmart,

    Managed to grab that first process after a server reboot. The server looks like it's crashing due to running out of RAM. In watching the metrics on the server this time, the LiveQuery RAM use gets to about 80% of the available RAM (on the C5.Large it's 4GB) and then the server crashes. My guess is the garbage collector can't run due to the excessive CPU.

    Thanks!
    Wes
    Process1-10-8-2018-V2.zip

  13. wfilleman commented on Oct 8, 2018

    @wfilleman
    Author

    @flovilmart, One more data point: With no clients connected, the CPU is 1-2%. Stable. Whatever is happening, it's only once there are 1+ clients connected.

    Wes

  14. flovilmart commented on Oct 8, 2018

    @flovilmart
    Contributor

    @wfilleman are you able to test against a particuarl branch like: schemaController-performance? I've identified one way things can improve #5107

  15. flovilmart commented on Oct 8, 2018

    @flovilmart
    Contributor

    @wfilleman the more I look at your trace in Process3, it seems to spend most of it's time on the internal connect calls. Are you sure the client connections are steady and stable? If you have your 3 clients connected and no activity on the server, what is your CPU usage like? It looks like all your clients keep connecting and disconnecting all the time. Do you have a reverse proxy in front?

  16. 49 remaining items

  17. wfilleman commented on Oct 13, 2018

    @wfilleman
    Author

    Ok, I can work on that. Another data point: Instead of rebooting the LiveQuery server, I just disconnected my two test clients and reconnected them. The Network Out dropped back to normal baseline levels (500KB).

    So, now I'm thinking that maybe the client is getting into a weird state and putting a load on the LiveQuery server. What's the best way to log what's happening in the Parse/React Native LiveQuery client? I'm using Parse 2.1.0.

    Wes

  18. flovilmart commented on Oct 13, 2018

    @flovilmart
    Contributor

    The out network would mean that the server is sending the same events multiple times, this could occur perhaps if the client connects multiple times, and the server keeps all of those connections.

  19. wfilleman commented on Oct 15, 2018

    @wfilleman
    Author

    Hi @flovilmart,

    Took me a couple of days to reset and capture performance profiles, but here they are. Ok, here's the test setup:

    • AWS t3.small server.
    • LiveQuery Server ONLY. It is set up to connect to my production Parse Server so that we remove any Parse Server CPU cycles from the profile logs.
    • Only one CPU/process.
    • Two React Native clients.

    With this test setup, I profiled two minutes of nominal activity. CPU was 1-2% and Network Out was around 500KB. This ran stable for 24 hours to the minute before the CPU jumped 4X and Network out went to 4MB (about 8X from baseline).

    In the React Native clients, at the 24-hour mark, I see where the LiveQuery connection was closed and the client auto-retried to connect but was met with an error stating the "Session token is expired.". No further object updates flowed to the React Native clients (as expected if the session token expires). The React Native client isn't yet set up to detect this condition to re-login again.

    At this exact moment, the LiveQuery server spiked CPU and NetworkOut and another 2-minute profile was captured. I'll let you take a look, but I'm seeing unhandled promise rejections in the profile capture and I'm wondering if that's due to the expired session token which then causes something in LiveQuery to cause the side-effects I'm seeing with 4X CPU and 8X Network out.

    Thanks for sticking with me and investigating alongside. Let me know if you'd like me to capture anything else or run another test.

    Wes

    LiveQueryProfile - 2 Clients - Spiked CPU NetworkOut.zip

  20. wfilleman commented on Oct 15, 2018

    @wfilleman
    Author

    Digging into the unhandled promise rejection, It looks like line 513 in ParseLiveQueryServer.js may be the source of my issue:

    const roleNames = await auth.getUserRoles(); // Finally, see if any of the user's roles allow them read access

    I suspect .getUserRoles() is throwing an exception due to the session token expiration. Buried in that call is a REST request to Parse Server which, I could see getting repeatedly hit if the cache no longer is preventing needless network traffic.

    Wes

  21. flovilmart commented on Oct 15, 2018

    @flovilmart
    Contributor

    Why would the session token expire so quickly?

  22. wfilleman commented on Oct 15, 2018

    @wfilleman
    Author

    No idea. How can I change the expiration time?

  23. flovilmart commented on Oct 15, 2018

    @flovilmart
    Contributor

    Well session tokens have expiration of a year. So that should not be problematic in the first place.

    When you say ‘you suspect’ did you get any proof of it?

    I don’t have the time to investigate further unfortunately.

  24. dplewis commented on Oct 15, 2018

    @dplewis
    Member

    @wfilleman I see your config has this set

     sessionLength: ONE_DAY, // in seconds. Set to 24 hours.
    
  25. wfilleman commented on Oct 15, 2018

    @wfilleman
    Author

    Ah, there we go. I can remove that from production to use the default session lenght of one year. Thanks, @dplewis.

    @flovilmart, The profile capture pointed to the exact top level promise that was throwing the uncaught promise exception. I looked at the source and there are two places inside this promise where await is used without a try/catch around it that is the likely source. Either line 513 or line 534 in ParseLiveQueryServer.js is throwing an uncaught exception when the session token expires which leads to the CPU/Network Out spike.

    513 and 534 are marked by me below with **.

    return Promise.resolve().then(async () => {
          // Resolve false right away if the acl doesn't have any roles
          const acl_has_roles = Object.keys(acl.permissionsById).some(key => key.startsWith('role:'));
    
          if (!acl_has_roles) {
            return false;
          }
    
          **const roleNames = await auth.getUserRoles(); // Finally, see if any of the user's roles allow them read access**
    
          for (const role of roleNames) {
            // We use getReadAccess as `role` is in the form `role:roleName`
            if (acl.getReadAccess(role)) {
              return true;
            }
          }
    
          return false;
        }).then(async isRoleMatched => {
          if (isRoleMatched) {
            return Promise.resolve(true);
          } // Check client sessionToken matches ACL
    
    
          const clientSessionToken = client.sessionToken;
    
          if (clientSessionToken) {
            const {
              userId
            } = **await this.getAuthForSessionToken(clientSessionToken);**
            return acl.getReadAccess(userId);
          } else {
            return isRoleMatched;
          }
        }).catch(() => {
          return false;
        });
    

    Using the default of one year should be a fine workaround, but wanted to document here what we've uncovered incase there's a security need from other project use cases for a shorter session token length where this might become an issue.

    Wes

  26. flovilmart commented on Oct 15, 2018

    @flovilmart
    Contributor

    If you believe you have found the source of the issue, you should open a PR :)

  27. wfilleman commented on Oct 16, 2018

    @wfilleman
    Author

    That is a good idea. I'm running a different set of tests currently to load up LiveQuery with a simulated 200 concurrent users with the kind of traffic my production server is producing to check that box. Assuming this looks good after 24 hours then I'll dig into the session token issue and see what that fix path looks like.

    Wes

  28. wfilleman commented on Oct 17, 2018

    @wfilleman
    Author

    Hi @flovilmart,

    Everything is looking good. After running my 200 concurrent sessions for about 30 hours, CPU and Network Out metrics are stable at:

    • CPU: 15-20%
    • Network Out: 500KB-1MB

    I think I am all set to move on with my project using LiveQuery. I saw that the #4387 pull hadn't been merged yet to main, if you were waiting on my test results, I think it's good to go from my perspective. In all my testing I've only ever seen object updates for roles my test users were assigned. Also, this fixes the original CPU issue.

    I want to thank you for your patience and help to work through this with me. I'll take a look at the session token expiration issue I ran into and open a separate pull request. Otherwise, I'm considering this issue closed with the merging of #4387 into main.

    Wes

  29. flovilmart commented on Oct 17, 2018

    @flovilmart
    Contributor

    Thanks for providing the live production data that was invaluable and awesome! 😎

    There is still this bug that screw up everything when session expire. That would need addressing.

  30. wfilleman commented on Oct 17, 2018

    @wfilleman
    Author

    Thanks, @flovilmart! Parse has become my go-to for a stable and dev friendly backend on my (and client) projects. You and the maintenance team are doing great work.

    Agreed about the session expire issue. It's on my list to investigate deeper but shouldn't hold up closing this original issue, now with the merge to main complete.

    Wes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions