Skip to content

Commit 7d9c627

Browse files
committed
feat: add DruidNode deploymentGroup field to support red/black style Druid deployments
1 parent ad106a8 commit 7d9c627

15 files changed

Lines changed: 609 additions & 39 deletions

File tree

docs/configuration/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ These Coordinator static configurations can be defined in the `coordinator/runti
716716
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative integer.|8281|
717717
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services.|`druid/coordinator`|
718718
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
719+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
719720

720721
##### Coordinator operation
721722

@@ -961,6 +962,7 @@ These Overlord static configurations can be defined in the `overlord/runtime.pro
961962
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative Integer.|8290|
962963
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services.|`druid/overlord`|
963964
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
965+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
964966

965967
##### Overlord operations
966968

@@ -1314,6 +1316,7 @@ These Middle Manager and Peon configurations can be defined in the `middleManage
13141316
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative Integer.|8291|
13151317
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services|`druid/middlemanager`|
13161318
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
1319+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
13171320

13181321
#### Middle Manager configuration
13191322

@@ -1443,6 +1446,7 @@ For most types of tasks, `SegmentWriteOutMediumFactory` can be configured per-ta
14431446
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative Integer.|8283|
14441447
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services|`druid/indexer`|
14451448
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
1449+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
14461450

14471451
#### Indexer general configuration
14481452

@@ -1540,6 +1544,7 @@ These Historical configurations can be defined in the `historical/runtime.proper
15401544
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative Integer.|8283|
15411545
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services|`druid/historical`|
15421546
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
1547+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
15431548

15441549
#### Historical general configuration
15451550

@@ -1655,6 +1660,7 @@ These Broker configurations can be defined in the `broker/runtime.properties` fi
16551660
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative Integer.|8282|
16561661
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services|`druid/broker`|
16571662
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
1663+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
16581664

16591665
#### Query configuration
16601666

@@ -1911,6 +1917,8 @@ See [cache configuration](#cache-configuration) for how to configure cache setti
19111917
|`druid.broker.segment.watchedDataSources`|List of strings|Broker watches the segment announcements from processes serving segments to build cache of which process is serving which segments, this configuration allows to only consider segments being served from a whitelist of dataSources. By default, Broker would consider all datasources. This can be used to configure brokers in partitions so that they are only queryable for specific dataSources.|none|
19121918
|`druid.broker.segment.watchRealtimeTasks`|Boolean|The Broker watches segment announcements from processes that serve segments to build a cache to relate each process to the segments it serves. When `watchRealtimeTasks` is true, the Broker watches for segment announcements from both Historicals and realtime processes. To configure a broker to exclude segments served by realtime processes, set `watchRealtimeTasks` to false. |true|
19131919
|`druid.broker.segment.awaitInitializationOnStart`|Boolean|Whether the Broker will wait for its view of segments to fully initialize before starting up. If set to 'true', the Broker's HTTP server will not start up, and the Broker will not announce itself as available, until the server view is initialized. See also `druid.sql.planner.awaitInitializationOnStart`, a related setting.|true|
1920+
|`druid.broker.segment.watchedDeploymentGroups`|List of strings|Restricts the Broker's segment view to data servers whose `druid.deploymentGroup` matches one of the listed values. Realtime servers (peons and indexers) bypass this filter unless `druid.broker.segment.strictRealtimeDeploymentGroupFilter` is true. Used to isolate query traffic during red/black upgrades while still allowing both clusters to query in-flight realtime data. Empty means no filtering.|none|
1921+
|`druid.broker.segment.strictRealtimeDeploymentGroupFilter`|Boolean|When true, `druid.broker.segment.watchedDeploymentGroups` also applies to realtime servers (peons, indexers). Default false: realtime servers are always watched regardless of deployment group, so red and black brokers can both serve queries against in-flight ingestion during a rollover.|false|
19141922

19151923
## Metrics monitors
19161924

@@ -2280,6 +2288,7 @@ Supported query contexts:
22802288
|`druid.tlsPort`|TLS port for HTTPS connector, if [druid.enableTlsPort](../operations/tls-support.md) is set then this config will be used. If `druid.host` contains port then that port will be ignored. This should be a non-negative Integer.|9088|
22812289
|`druid.service`|The name of the service. This is used as a dimension when emitting metrics and alerts to differentiate between the various services|`druid/router`|
22822290
|`druid.labels`|Optional JSON object of key-value pairs that define custom labels for the server. These labels are displayed in the web console under the "Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or `druid.labels.location=Airtrunk`|`null`|
2291+
|`druid.deploymentGroup`|Optional tag identifying the deployment group this service belongs to (for example `red` or `black` during a red/black upgrade). Used by version-aware query routing on Brokers (`druid.broker.segment.watchedDeploymentGroups`) and Routers (`druid.router.acceptableDeploymentGroups`) to isolate query traffic between two parallel control planes that share the same metadata, deep storage, and discovery. Surfaced as the `deployment_group` column of `sys.servers`. Master services (Coordinator, Overlord) typically leave this unset so they can manage both groups during cutover.|`null`|
22832292

22842293
#### Runtime configuration
22852294

@@ -2291,6 +2300,7 @@ Supported query contexts:
22912300
|`druid.router.pollPeriod`|How often to poll for new rules.|`PT1M`|
22922301
|`druid.router.sql.enable`|Enable routing of SQL queries using strategies. When`true`, the Router uses the strategies defined in `druid.router.strategies` to determine the broker service for a given SQL query. When `false`, the Router uses the `defaultBrokerServiceName`.|`false`|
22932302
|`druid.router.strategies`|Please see [Router Strategies](../design/router.md#router-strategies) for details.|`[{"type":"timeBoundary"},{"type":"priority"}]`|
2303+
|`druid.router.acceptableDeploymentGroups`|List of strings|If non-empty, the Router only routes queries to Brokers whose `druid.deploymentGroup` is in this set. Brokers without a deployment group tag match only when this is empty. When a query's serviceName has no matching Broker after filtering, the selector returns no server and the request fails with a clear error (a warning is logged). Use during red/black upgrades to direct user traffic to one cluster's Brokers while the other cluster is still discoverable.|none|
22942304
|`druid.router.avatica.balancer.type`|Class to use for balancing Avatica queries across Brokers. Please see [Avatica Query Balancing](../design/router.md#avatica-query-balancing).|`rendezvousHash`|
22952305
|`druid.router.managementProxy.enabled`|Enables the Router's [management proxy](../design/router.md#router-as-management-proxy) functionality.|false|
22962306
|`druid.router.http.numConnections`|Size of connection pool for the Router to connect to Broker processes. If there are more queries than this number that all need to speak to the same process, then they will queue up.|`20`|

server/src/main/java/org/apache/druid/client/BrokerSegmentWatcherConfig.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ public class BrokerSegmentWatcherConfig
4242
@JsonProperty
4343
private boolean awaitInitializationOnStart = true;
4444

45+
/**
46+
* If non-empty, only servers whose deploymentGroup is in this set are watched.
47+
* Realtime servers (peons / indexers) bypass this filter unless
48+
* {@link #strictRealtimeDeploymentGroupFilter} is true.
49+
*/
50+
@JsonProperty
51+
private Set<String> watchedDeploymentGroups = null;
52+
53+
/**
54+
* When true, the deploymentGroup filter is applied to realtime servers as well as historicals.
55+
* Default false: realtime servers are always watched so red/black brokers can both query in-flight
56+
* data during a rollover. Set true only for strict isolation (e.g. testing a realtime ingest change).
57+
*/
58+
@JsonProperty
59+
private boolean strictRealtimeDeploymentGroupFilter = false;
60+
4561
public Set<String> getWatchedTiers()
4662
{
4763
return watchedTiers;
@@ -66,4 +82,14 @@ public boolean isAwaitInitializationOnStart()
6682
{
6783
return awaitInitializationOnStart;
6884
}
85+
86+
public Set<String> getWatchedDeploymentGroups()
87+
{
88+
return watchedDeploymentGroups;
89+
}
90+
91+
public boolean isStrictRealtimeDeploymentGroupFilter()
92+
{
93+
return strictRealtimeDeploymentGroupFilter;
94+
}
6995
}

server/src/main/java/org/apache/druid/client/BrokerServerView.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.List;
4949
import java.util.Map;
5050
import java.util.Optional;
51+
import java.util.Set;
5152
import java.util.concurrent.ConcurrentHashMap;
5253
import java.util.concurrent.ConcurrentMap;
5354
import java.util.concurrent.CountDownLatch;
@@ -124,6 +125,10 @@ public BrokerServerView(
124125
return false;
125126
}
126127

128+
if (!isDeploymentGroupAllowed(metadataAndSegment.lhs)) {
129+
return false;
130+
}
131+
127132
// Include realtime tasks only if they are watched
128133
return metadataAndSegment.lhs.getType() != ServerType.INDEXER_EXECUTOR
129134
|| segmentWatcherConfig.isWatchRealtimeTasks();
@@ -172,7 +177,7 @@ public CallbackAction segmentSchemasAnnounced(SegmentSchemas segmentSchemas)
172177
public CallbackAction serverAdded(DruidServer server)
173178
{
174179
// We don't track brokers in this view.
175-
if (!server.getType().equals(ServerType.BROKER)) {
180+
if (!server.getType().equals(ServerType.BROKER) && isDeploymentGroupAllowed(server.getMetadata())) {
176181
addServer(server);
177182
}
178183
return CallbackAction.CONTINUE;
@@ -246,6 +251,30 @@ private void validateSegmentWatcherConfig(BrokerSegmentWatcherConfig watcherConf
246251
&& watcherConfig.getIgnoredTiers().isEmpty()) {
247252
throw new ISE("If configured, 'druid.broker.segment.ignoredTiers' must be non-empty");
248253
}
254+
255+
if (watcherConfig.getWatchedDeploymentGroups() != null
256+
&& watcherConfig.getWatchedDeploymentGroups().isEmpty()) {
257+
throw new ISE("If configured, 'druid.broker.segment.watchedDeploymentGroups' must be non-empty");
258+
}
259+
}
260+
261+
/**
262+
* Returns true if the server's deploymentGroup passes the watched filter, or if the server is
263+
* a realtime server type and the strict-realtime toggle is off (the default).
264+
*/
265+
private boolean isDeploymentGroupAllowed(DruidServerMetadata server)
266+
{
267+
final boolean isRealtime =
268+
server.getType() == ServerType.INDEXER_EXECUTOR || server.getType() == ServerType.REALTIME;
269+
if (isRealtime && !segmentWatcherConfig.isStrictRealtimeDeploymentGroupFilter()) {
270+
return true;
271+
}
272+
273+
final Set<String> watched = segmentWatcherConfig.getWatchedDeploymentGroups();
274+
if (watched != null && !watched.contains(server.getDeploymentGroup())) {
275+
return false;
276+
}
277+
return true;
249278
}
250279

251280
private QueryableDruidServer addServer(DruidServer server)

server/src/main/java/org/apache/druid/guice/StorageNodeModule.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public DruidServerMetadata getMetadata(
8787
config.getStorageSize(),
8888
serverTypeConfig.getServerType(),
8989
config.getTier(),
90-
config.getPriority()
90+
config.getPriority(),
91+
node.getDeploymentGroup()
9192
);
9293
}
9394

server/src/main/java/org/apache/druid/segment/realtime/ServiceAnnouncingChatHandlerProvider.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,6 @@ public Optional<ChatHandler> get(final String key)
119119

120120
private DruidNode makeDruidNode(String key)
121121
{
122-
return new DruidNode(
123-
key,
124-
node.getHost(),
125-
node.isBindOnHost(),
126-
node.getPlaintextPort(),
127-
node.getTlsPort(),
128-
node.isEnablePlaintextPort(),
129-
node.isEnableTlsPort()
130-
);
122+
return node.withService(key);
131123
}
132124
}

0 commit comments

Comments
 (0)