/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.distributedlog.service.balancer; import com.google.common.base.Optional; import com.google.common.util.concurrent.RateLimiter; import com.twitter.distributedlog.client.monitor.MonitorServiceClient; import com.twitter.distributedlog.service.DistributedLogClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.SocketAddress; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * A balancer balances ownerships between two targets. */ public class SimpleBalancer implements Balancer { static final Logger logger = LoggerFactory.getLogger(SimpleBalancer.class); protected final String target1; protected final String target2; protected final DistributedLogClient targetClient1; protected final DistributedLogClient targetClient2; protected final MonitorServiceClient targetMonitor1; protected final MonitorServiceClient targetMonitor2; public SimpleBalancer(String name1, DistributedLogClient client1, MonitorServiceClient monitor1, String name2, DistributedLogClient client2, MonitorServiceClient monitor2) { this.target1 = name1; this.targetClient1 = client1; this.targetMonitor1 = monitor1; this.target2 = name2; this.targetClient2 = client2; this.targetMonitor2 = monitor2; } protected static int countNumberStreams(Map<SocketAddress, Set<String>> distribution) { int count = 0; for (Set<String> streams : distribution.values()) { count += streams.size(); } return count; } @Override public void balance(int rebalanceWaterMark, double rebalanceTolerancePercentage, int rebalanceConcurrency, Optional<RateLimiter> rebalanceRateLimiter) { // get the ownership distributions from individual targets Map<SocketAddress, Set<String>> distribution1 = targetMonitor1.getStreamOwnershipDistribution(); Map<SocketAddress, Set<String>> distribution2 = targetMonitor2.getStreamOwnershipDistribution(); // get stream counts int proxyCount1 = distribution1.size(); int streamCount1 = countNumberStreams(distribution1); int proxyCount2 = distribution2.size(); int streamCount2 = countNumberStreams(distribution2); logger.info("'{}' has {} streams by {} proxies; while '{}' has {} streams by {} proxies.", new Object[] {target1, streamCount1, proxyCount1, target2, streamCount2, proxyCount2 }); String source, target; Map<SocketAddress, Set<String>> srcDistribution; DistributedLogClient srcClient, targetClient; MonitorServiceClient srcMonitor, targetMonitor; int srcStreamCount, targetStreamCount; if (streamCount1 > streamCount2) { source = target1; srcStreamCount = streamCount1; srcClient = targetClient1; srcMonitor = targetMonitor1; srcDistribution = distribution1; target = target2; targetStreamCount = streamCount2; targetClient = targetClient2; targetMonitor = targetMonitor2; } else { source = target2; srcStreamCount = streamCount2; srcClient = targetClient2; srcMonitor = targetMonitor2; srcDistribution = distribution2; target = target1; targetStreamCount = streamCount1; targetClient = targetClient1; targetMonitor = targetMonitor1; } Map<String, Integer> loadDistribution = new HashMap<String, Integer>(); loadDistribution.put(source, srcStreamCount); loadDistribution.put(target, targetStreamCount); // Calculate how many streams to be rebalanced from src region to target region int numStreamsToRebalance = BalancerUtils.calculateNumStreamsToRebalance(source, loadDistribution, rebalanceWaterMark, rebalanceTolerancePercentage); if (numStreamsToRebalance <= 0) { logger.info("No streams need to be rebalanced from '{}' to '{}'.", source, target); return; } StreamChooser streamChooser = LimitedStreamChooser.of(new CountBasedStreamChooser(srcDistribution), numStreamsToRebalance); StreamMover streamMover = new StreamMoverImpl(source, srcClient, srcMonitor, target, targetClient, targetMonitor); moveStreams(streamChooser, streamMover, rebalanceConcurrency, rebalanceRateLimiter); } @Override public void balanceAll(String source, int rebalanceConcurrency, Optional<RateLimiter> rebalanceRateLimiter) { String target; DistributedLogClient sourceClient, targetClient; MonitorServiceClient sourceMonitor, targetMonitor; if (target1.equals(source)) { sourceClient = targetClient1; sourceMonitor = targetMonitor1; target = target2; targetClient = targetClient2; targetMonitor = targetMonitor2; } else if (target2.equals(source)) { sourceClient = targetClient2; sourceMonitor = targetMonitor2; target = target1; targetClient = targetClient1; targetMonitor = targetMonitor1; } else { throw new IllegalArgumentException("Unknown target " + source); } // get the ownership distributions from individual targets Map<SocketAddress, Set<String>> distribution = sourceMonitor.getStreamOwnershipDistribution(); if (distribution.isEmpty()) { return; } StreamChooser streamChooser = new CountBasedStreamChooser(distribution); StreamMover streamMover = new StreamMoverImpl(source, sourceClient, sourceMonitor, target, targetClient, targetMonitor); moveStreams(streamChooser, streamMover, rebalanceConcurrency, rebalanceRateLimiter); } private void moveStreams(StreamChooser streamChooser, StreamMover streamMover, int concurrency, Optional<RateLimiter> rateLimiter) { CountDownLatch doneLatch = new CountDownLatch(concurrency); RegionMover regionMover = new RegionMover(streamChooser, streamMover, rateLimiter, doneLatch); ExecutorService executorService = Executors.newFixedThreadPool(concurrency); try { for (int i = 0; i < concurrency; i++) { executorService.submit(regionMover); } try { doneLatch.await(); } catch (InterruptedException e) { logger.info("{} is interrupted. Stopping it ...", streamMover); regionMover.shutdown(); } } finally { executorService.shutdown(); } } /** * Move streams from <i>src</i> region to <i>target</i> region. */ static class RegionMover implements Runnable { final StreamChooser streamChooser; final StreamMover streamMover; final Optional<RateLimiter> rateLimiter; final CountDownLatch doneLatch; volatile boolean running = true; RegionMover(StreamChooser streamChooser, StreamMover streamMover, Optional<RateLimiter> rateLimiter, CountDownLatch doneLatch) { this.streamChooser = streamChooser; this.streamMover = streamMover; this.rateLimiter = rateLimiter; this.doneLatch = doneLatch; } @Override public void run() { while (running) { if (rateLimiter.isPresent()) { rateLimiter.get().acquire(); } String stream = streamChooser.choose(); if (null == stream) { break; } streamMover.moveStream(stream); } doneLatch.countDown(); } void shutdown() { running = false; } } @Override public void close() { // no-op } }