This is an automated email from the ASF dual-hosted git repository. zhangstar333 pushed a commit to branch doris_master_new in repository https://gitbox.apache.org/repos/asf/doris.git
commit c54bcbc9ec878f9b60843c1b3e73665776f1a7e1 Author: zhangstar333 <[email protected]> AuthorDate: Fri May 22 16:34:23 2026 +0800 fe graceful shutdown --- .../main/java/org/apache/doris/common/Config.java | 14 +++ .../src/main/java/org/apache/doris/DorisFE.java | 106 +++++++++++++++++++-- .../java/org/apache/doris/qe/ConnectScheduler.java | 31 ++++++ .../org/apache/doris/qe/MysqlConnectProcessor.java | 25 +++++ .../main/java/org/apache/doris/qe/QeService.java | 10 +- 5 files changed, 176 insertions(+), 10 deletions(-) diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java index 39f29b958d5..c7c4c65dae8 100644 --- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java +++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java @@ -483,6 +483,20 @@ public class Config extends ConfigBase { @ConfField(description = {"The maximum number of task threads in the MySQL service"}) public static int max_mysql_service_task_threads_num = 4096; + @ConfField(mutable = true, description = { + "FE graceful shutdown A 阶段(假死 drain)的持续秒数。此阶段 /api/health 已返回 503," + + "但 listener 不关、新 query 仍正常应答,用于吸收 K8s LB/DNS 收敛延迟。", + "Duration in seconds of FE graceful shutdown phase A (drain). During this phase /api/health " + + "returns 503, but listeners stay open and new queries are still served, to absorb " + + "K8s LB/DNS convergence delay."}) + public static int graceful_shutdown_drain_seconds = 15; + + @ConfField(mutable = true, description = { + "FE graceful shutdown C 阶段等待 in-flight Coordinator 完成的最大秒数。超过则强制退出 JVM。", + "Maximum seconds to wait for in-flight Coordinators to finish during FE graceful shutdown " + + "phase C. JVM will be forced to exit when exceeded."}) + public static int grace_shutdown_wait_seconds = 300; + @ConfField(description = {"BackendServiceProxy pool size for pooling GRPC channels."}) public static int backend_proxy_num = 48; diff --git a/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java b/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java index d313467b127..df251e8d02c 100755 --- a/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java +++ b/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java @@ -98,6 +98,10 @@ public class DorisFE { // set to true when all servers are ready. private static final AtomicBoolean serverReady = new AtomicBoolean(false); + // set to true once graceful shutdown enters phase B; new data-plane + // queries should then be silently rejected. + private static final AtomicBoolean rejectNewQueries = new AtomicBoolean(false); + // HTTP server instance, used for graceful shutdown private static HttpServer httpServer; private static ServerStarter thriftServerStarter; @@ -176,6 +180,9 @@ public class DorisFE { // Add shutdown hook for graceful exit Runtime.getRuntime().addShutdownHook(new Thread(() -> { LOG.info("Received shutdown signal, starting graceful shutdown..."); + // === A0: immediately mark not-ready so /api/health returns 503. + // K8s readinessProbe will fail and the pod will be removed from the + // EndpointSlice. Listeners stay open; new queries are still served. serverReady.set(false); gracefulShutdown(); @@ -191,9 +198,15 @@ public class DorisFE { } } - // Shutdown HTTP server after main process graceful shutdown is complete + // Shutdown HTTP server at the very end (after phase C). HTTP server is + // kept alive throughout drain so that K8s readiness probe can keep + // observing 503 until endpoint removal completes. if (httpServer != null) { - httpServer.shutdown(); + try { + httpServer.shutdown(); + } catch (Throwable t) { + LOG.warn("failed to shutdown http server", t); + } } LogManager.shutdown(); @@ -722,20 +735,95 @@ public class DorisFE { return serverReady.get(); } + /** + * Returns true once graceful shutdown has entered phase B. New data-plane + * MySQL commands (COM_QUERY / COM_STMT_EXECUTE / ...) should be silently + * dropped so JDBC drivers fail over to another FE. + */ + public static boolean shouldRejectNewQueries() { + return rejectNewQueries.get(); + } + + /** + * Graceful shutdown driven by SIGTERM (typically from K8s). Implements the + * A0 → A → B → C → D state machine described in the operator design doc. + * + * <pre> + * A0 serverReady=false → /api/health returns 503 (already done by caller) + * A "drain": sleep graceful_shutdown_drain_seconds while still serving + * new queries, to let K8s LB/DNS converge. + * B atomic flip: rejectNewQueries=true, then close MySQL/ArrowFlight + * listeners, then close idle MySQL connections (FIN to client). + * C wait for in-flight Coordinators to finish (≤ grace_shutdown_wait_seconds), + * re-running closeIdleMysqlConnections() once per second. + * D return; JVM exits after shutdown hook completes. + * </pre> + */ private static void gracefulShutdown() { - // wait for all queries to finish try { - long now = System.currentTimeMillis(); - List<Coordinator> allCoordinators = QeProcessorImpl.INSTANCE.getAllCoordinators(); - while (!allCoordinators.isEmpty() && System.currentTimeMillis() - now < 300 * 1000L) { + // === Phase A: drain — absorb K8s LB/DNS convergence gap === + int drainSeconds = Math.max(0, Config.graceful_shutdown_drain_seconds); + LOG.info("graceful shutdown phase A: drain for {}s (health=503 but still serving)", drainSeconds); + for (int i = 0; i < drainSeconds; i++) { + Thread.sleep(1000); + } + + // === Phase B: atomic flip + close listeners + close idle MySQL connections === + // Step 1: flip the reject flag FIRST so any thread that wins the listener + // accept race still gets rejected at the dispatch layer. + rejectNewQueries.set(true); + LOG.info("graceful shutdown phase B step1: rejectNewQueries=true"); + + // Step 2: close MySQL / Arrow-Flight listeners. HTTP listener stays open + // until after phase C so /api/health remains reachable for the probe. + if (qeService != null) { + LOG.info("graceful shutdown phase B step2: close MySQL / ArrowFlight listeners"); + qeService.stopAcceptNewConnections(); + } + + // Step 3: send FIN to currently-idle (COM_SLEEP) MySQL connections so + // pooled JDBC clients notice and reconnect to another FE. + LOG.info("graceful shutdown phase B step3: close idle mysql connections"); + closeIdleMysqlConnections(); + + // === Phase C: wait for in-flight Coordinators to drain === + long deadline = System.currentTimeMillis() + Math.max(0, Config.grace_shutdown_wait_seconds) * 1000L; + LOG.info("graceful shutdown phase C: wait up to {}s for in-flight queries to finish", + Config.grace_shutdown_wait_seconds); + while (System.currentTimeMillis() < deadline) { + List<Coordinator> allCoordinators = QeProcessorImpl.INSTANCE.getAllCoordinators(); + if (allCoordinators.isEmpty()) { + LOG.info("all in-flight coordinators finished, exit phase C early"); + break; + } + LOG.info("phase C: waiting {} queries to finish before shutdown", allCoordinators.size()); Thread.sleep(1000); - allCoordinators = QeProcessorImpl.INSTANCE.getAllCoordinators(); - LOG.info("waiting {} queries to finish before shutdown", allCoordinators.size()); + // Re-scan for newly-idle connections produced by completed queries. + closeIdleMysqlConnections(); } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + LOG.warn("graceful shutdown interrupted", ie); } catch (Throwable t) { - LOG.error("", t); + LOG.error("graceful shutdown encountered an error", t); } + // === Phase D: return, JVM exits after the shutdown hook completes. === LOG.info("graceful shutdown finished"); } + + private static void closeIdleMysqlConnections() { + try { + ExecuteEnv env = ExecuteEnv.getInstance(); + if (env == null) { + return; + } + org.apache.doris.qe.ConnectScheduler scheduler = env.getScheduler(); + if (scheduler != null) { + scheduler.closeIdleMysqlConnections(); + } + } catch (Throwable t) { + LOG.warn("failed to close idle mysql connections", t); + } + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectScheduler.java b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectScheduler.java index b22e89cf209..ae8ffdfc9a7 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectScheduler.java +++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectScheduler.java @@ -21,6 +21,7 @@ import org.apache.doris.analysis.UserIdentity; import org.apache.doris.common.Config; import org.apache.doris.common.Status; import org.apache.doris.common.ThreadPoolManager; +import org.apache.doris.mysql.MysqlCommand; import org.apache.doris.qe.ConnectContext.ThreadInfo; import org.apache.doris.service.arrowflight.sessions.FlightSqlConnectPoolMgr; @@ -169,4 +170,34 @@ public class ConnectScheduler { } } } + + /** + * Close MySQL connections that are currently idle (command == COM_SLEEP). + * Used during graceful shutdown phase B / C so that clients receive a FIN + * on idle sockets and can fail over to another FE. + * + * @return number of connections closed in this invocation + */ + public int closeIdleMysqlConnections() { + int closed = 0; + for (ConnectContext ctx : connectPoolMgr.getConnectionMap().values()) { + if (ctx == null) { + continue; + } + if (ctx.getCommand() == MysqlCommand.COM_SLEEP && !ctx.isKilled()) { + try { + // kill the connection (sends FIN to client) but do not cancel + // any running query (there is none in COM_SLEEP state). + ctx.kill(true); + closed++; + } catch (Throwable t) { + LOG.warn("failed to close idle mysql connection id={}", ctx.getConnectionId(), t); + } + } + } + if (closed > 0) { + LOG.info("closed {} idle mysql connections during graceful shutdown", closed); + } + return closed; + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java b/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java index f70a9d3f622..742327d3cc8 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java +++ b/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java @@ -17,6 +17,7 @@ package org.apache.doris.qe; +import org.apache.doris.DorisFE; import org.apache.doris.analysis.StatementBase; import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; @@ -256,6 +257,17 @@ public class MysqlConnectProcessor extends ConnectProcessor { ctx.setStartTime(); resolveWorkloadGroupName(); + // Graceful shutdown: if FE is draining and rejecting new data-plane requests, + // silently close the connection (no MySQL error packet) so JDBC drivers fail + // over to another FE via multi-host config instead of surfacing an error. + if (DorisFE.shouldRejectNewQueries() && isDataPlaneCommand(command)) { + LOG.info("reject new query during graceful shutdown, connId={}, command={}, remote={}", + ctx.getConnectionId(), command, ctx.getMysqlChannel().getRemoteHostPortString()); + ctx.getState().setNoop(); + ctx.setKilled(); + return; + } + switch (command) { case COM_INIT_DB: handleInitDb(); @@ -309,6 +321,19 @@ public class MysqlConnectProcessor extends ConnectProcessor { } } + private static boolean isDataPlaneCommand(MysqlCommand command) { + switch (command) { + case COM_QUERY: + case COM_STMT_PREPARE: + case COM_STMT_EXECUTE: + case COM_FIELD_LIST: + case COM_INIT_DB: + return true; + default: + return false; + } + } + private void handleFieldList() throws ConnectionException { String tableName = new String(MysqlProto.readNulTerminateString(packetBuf), StandardCharsets.UTF_8); handleFieldList(tableName); diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/QeService.java b/fe/fe-core/src/main/java/org/apache/doris/qe/QeService.java index e070e602cd8..0b0e5f22024 100755 --- a/fe/fe-core/src/main/java/org/apache/doris/qe/QeService.java +++ b/fe/fe-core/src/main/java/org/apache/doris/qe/QeService.java @@ -66,7 +66,11 @@ public class QeService { LOG.info("QE service start."); } - public void stop() { + /** + * Stop accepting new MySQL and Arrow Flight SQL connections while leaving + * existing connections to be drained by the graceful shutdown flow. + */ + public void stopAcceptNewConnections() { try { if (flightStarter != null) { flightStarter.stop(); @@ -82,4 +86,8 @@ public class QeService { LOG.warn("stop mysql server failed", e); } } + + public void stop() { + stopAcceptNewConnections(); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
