Uploaded image for project: 'MariaDB MaxScale'
  1. MariaDB MaxScale
  2. MXS-6885

Add a listener drain that closes the listening socket without destroying the listener

    XMLWordPrintable

Details

    • New Feature
    • Status: Open (View Workflow)
    • Major
    • Resolution: Unresolved
    • None
    • None
    • None
    • None

    Description

      TL;DR

      ● Add a listener state that closes the listening socket (new connections get a TCP reset) while leaving existing sessions and the listener
      config intact, so it can be rebound with start listener. Neither stop listener (socket stays bound, clients hang) nor destroy listener
      (config lost) covers this, and the maintenance/drain backend workaround causes transaction-replay failures and post-restart auth-block
      outages. Must not survive a restart, must not propagate via config_sync_cluster, and a failed rebind must be an explicit error state.
      Extends MXS-2806; mirrors MXS-1417 for servers.


      Problem

      There is no supported way to make a running MaxScale stop accepting new client
      connections while existing sessions finish. This is the basic operation needed to
      restart or upgrade one instance of an HA pair. Every current option has a
      disqualifying side effect:

      • maxctrl stop listener keeps the listening socket bound. New connections
        complete the TCP handshake and then receive nothing, so clients hang until their
        connect timeout expires instead of being refused.
      • maxctrl destroy listener closes the socket but deletes the listener config,
        which must then be recreated. Its persistence across restart is not defined.
      • Setting backends to maintenance or drain is not a substitute and is
        actively harmful (see below). drain is also correctly refused on a Master
        per MXS-3532.

      Operators are pushed outside the product, to iptables or an external load balancer,
      for an operation MaxScale is best placed to perform.

      Why accept-and-hang is worse than refusing

      1. It is indistinguishable from a hung MaxScale.
      2. It defeats TCP health checks. A load balancer sees the port open and keeps the
        instance in rotation.
      3. It prevents connector failover. Connector/J and friends fail over on a refused
        connection, not a slow one, so a client waits out connectTimeout (default 30s)
        before trying the second MaxScale.

      Evidence (see linked case)

      Two MaxScale instances, jdbc:mariadb:loadbalance://mx1:3307,mx2:3307/db split
      50/50, in front of a 3 node Galera cluster. Nine listeners on 3306-3314, one service.

      Using stop listener before a restart dropped service capacity to 50% instead of
      failing over, because clients kept connecting to the drained instance and hanging.

      Working around it with server maintenance produced a 4 minute outage, of which the
      restart itself was 24 seconds:

      11:48:33-11:48:56  all three Galera nodes into maintenance
      11:49:26+          Could not find valid server for target type TARGET_SLAVE ... closing connection
                         18x Transaction replay time limit of 30 seconds exceeded, closi
      11:51:09           MaxScale shuts down
      11:51:33           MaxScale 25.10.2 starts, 9 listeners come up
      11:51:33           No valid servers from which to query MariaDB user accounts found
      11:51:33-11:52:35  all clients receive Access denied; hosts blocked for too many a
      11:52:13-11:52:19  servers come out of maintenance
      

      Transaction replay failed 18 times because every candidate was in maintenance, so
      workaround disabled the feature that exists to prevent this. After restart, with
      backends still in maintenance, MaxScale could not query the user account table, so
      clients got Access denied ... (using password: YES) and hosts were blocked for
      repeated auth failures. Those blocks outlive the maintenance window and read to
      application teams as a credentials problem.

      The customer implemented the correct behaviour themselves, in a script that parses the
      listener ports from maxscale.cnf and applies:

      iptables -I INPUT 1 -p tcp -m conntrack --ctstate NEW --dport "$PORT" -j REJECT --reject-with tcp-reset
      

      It works, and it is what Support currently recommends. It should not be necessary.

      Proposal

      Add a listener state that stops accepting and closes the listening socket, leaving
      listener configuration intact so it can be restarted:

      maxctrl drain listener <name>     # close the socket, keep the config
      maxctrl start listener <name>     # rebind and resume
      

      Alternative shape: maxctrl stop listener <name> --close-socket. Either should
      exposed via the REST API, and ideally applicable at service level so all listeners on
      a service drain in one call (nine listeners on one service in this case).

      Critical requirement: this is runtime state, not configuration, and must NOT
      propagate through config_sync_cluster. Draining one instance of an HA pair is the
      primary use case, so propagation would drain both and take down the entire tier.

      Design notes

      • The socket is presumably held today to avoid a bind race and to stop another pro
        taking the port. If it is closed, a rebind on start listener can fail. That must
        be reported explicitly and the listener left in a clearly failed state, not sile
        down. The config object surviving means the operator can retry.
      • The drained state must not survive a MaxScale restart. A restarted MaxScale must
        back serving on all configured listeners. This ambiguity is exactly what makes
        destroy listener unsafe to recommend today.
      • Connections already in the accept queue when the socket closes will be reset. That is
        the desired outcome.
      • Existing sessions are untouched and run to completion, as with stop listener today.

      Acceptance criteria

      1. Draining a listener closes the socket. A new connection to the port receives a TCP reset.
      2. Existing sessions on that listener continue and complete normally.
      3. start listener rebinds and resumes. A failed rebind is an error and is reflected in state.
      4. maxctrl list listeners distinguishes stopped-with-socket-bound from drained-
      5. The drained state is not persisted across a MaxScale restart.
      6. The drained state is not propagated by config_sync_cluster.

      Secondary: naming and documentation

      stop listener does not stop the listener. It stops forwarding accepted connect
      to the service while the socket stays bound and connections continue to be accepted.
      The name implies the socket closes, which is what every operator in this case assu
      including Support.

      Not proposing a rename, since that breaks existing scripts. But:

      • The MaxCtrl reference should state that stop listener leaves the socket bound,
        that new connections are accepted and not serviced, and that clients hang rather
        being refused. It currently documents this for destroy listener ("closes the
        listening socket, opening it up for immediate reuse") and says nothing about soc
        behaviour for stop listener. There is no way to discover it short of testing.
      • Consider deprecating the verb in favour of pause listener, which describes t
        actual behaviour, reserving stop for the socket-closing behaviour proposed above.

      Relationship to existing issues

      Extends MXS-2806, which added stop/start listener. This is not a duplicate: MXS-2806
      delivered administrative stop/start, this asks for socket closure so the drained
      instance is externally observable as unavailable. MXS-1417 makes the same argument for
      servers, citing HAProxy.

      Attachments

        Issue Links

          Activity

            People

              allen.herrera Allen Herrera
              claudio.nanni Claudio Nanni
              Votes:
              0 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

                Created:
                Updated:

                Time Tracking

                  Estimated:
                  Original Estimate - 1d
                  1d
                  Remaining:
                  Remaining Estimate - 1d
                  1d
                  Logged:
                  Time Spent - Not Specified
                  Not Specified

                  Git Integration

                    Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.