package io.blobkeeper.cluster.service; /* * Copyright (C) 2016-2017 by Denis M. Gabaydulin * * 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. */ import com.google.common.collect.ImmutableMap; import io.blobkeeper.file.configuration.FileConfiguration; import io.blobkeeper.file.service.DiskService; import io.blobkeeper.file.service.PartitionService; import io.blobkeeper.index.domain.DiskIndexElt; import io.blobkeeper.index.domain.IndexElt; import io.blobkeeper.index.domain.Partition; import io.blobkeeper.index.service.IndexService; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; import java.util.List; import java.util.Map; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; import static io.blobkeeper.common.util.GuavaCollectors.toImmutableMap; import static io.blobkeeper.index.domain.PartitionState.*; import static java.util.Comparator.comparing; import static java.util.function.Function.identity; @Singleton public class BalancingServiceImpl implements BalancingService { private static final Logger log = LoggerFactory.getLogger(BalancingServiceImpl.class); @Inject private PartitionService partitionService; @Inject private DiskService diskService; @Inject private IndexService indexService; @Inject private FileConfiguration fileConfiguration; @Override public void balance(int disk) { Map<Integer, Integer> disksToPartitionsToMove = getMovePartitions(); log.debug("Balancing data: {}", disksToPartitionsToMove); // balance single partition getDstDisk(disksToPartitionsToMove, disk).ifPresent( dstDisk -> { partitionService.getFirstPartition(disk).ifPresent( src -> { // create a new partition on a destination disk Partition dst = partitionService.getNextActivePartition(dstDisk); movePartition(src, dst); } ); } ); // continue a process of rebalancing for partitions which were started earlier partitionService.getRebalancingStartedPartitions().stream() .map(src -> partitionService.getById(src.getDisk(), src.getId())) .filter(src -> src.getState() == NEW || src.getState() == REBALANCING) .forEach( src -> { // get a destination partition partitionService.getDestination(src).ifPresent( dst -> movePartition(src, dst) ); } ); partitionService.getPartitions(disk, DATA_MOVED).forEach( movedPartition -> partitionService.getDestination(movedPartition).ifPresent( dst -> { log.info("Update a moved partition index, src {} dst {}", movedPartition, dst); indexService.getListByPartition(movedPartition).forEach( file -> indexService.move(file, new DiskIndexElt(dst, file.getOffset(), file.getLength())) ); handleEmptyPartition(movedPartition); } ) ); } @NotNull @Override public Map<Integer, Integer> getMovePartitions() { Map<Integer, Integer> disksToPartitions = diskService.getDisks().stream() .collect(toImmutableMap(identity(), disk -> partitionService.getPartitions(disk).size())); if (disksToPartitions.isEmpty()) { return ImmutableMap.of(); } int nodes = disksToPartitions.keySet().size(); int totalPartitions = getTotalPartitions(disksToPartitions); int maxPartitionsPerNode = getMaxPartitionsPerNode(totalPartitions, nodes); log.info("Total nodes {}, total partitions {}, maxPartitionsPerNode: {}", nodes, totalPartitions, maxPartitionsPerNode); return disksToPartitions.entrySet().stream() .collect(toImmutableMap(Map.Entry::getKey, diskToPartitions -> Math.max(diskToPartitions.getValue() - maxPartitionsPerNode - 1, 0))); } private void handleEmptyPartition(Partition partition) { checkArgument(partition.getState() == DATA_MOVED, "Partition state DATA_MOVED is expected!"); try { log.info("Partition {} {} is going to be deleted, state = DATA_MOVED", partition.getDisk(), partition.getId()); List<IndexElt> elts = indexService.getLiveListByPartition(partition); if (elts.isEmpty()) { log.info("No live elements are left in the partition {}", partition); if (!trySetDeletedState(partition)) { log.warn("The state was changed, actual {}", partitionService.getById(partition.getDisk(), partition.getId())); } } else { log.warn("Something strange has happen, partition {} is not empty", partition); } } catch (Exception e) { log.error("Can't copy a file", e); } } private boolean trySetDeletedState(Partition partition) { partition.setState(DELETED); return partitionService.tryUpdateState(partition, DATA_MOVED); } private int getTotalPartitions(Map<Integer, Integer> disksToPartitions) { return disksToPartitions.values().stream() .mapToInt(v -> v) .sum(); } private int getMaxPartitionsPerNode(int totalPartitions, int nodes) { return totalPartitions / nodes; } private void movePartition(Partition src, Partition dst) { // a first operation of a moving partition process partitionService.move(src, dst); if (partitionService.tryStartRebalancing(src)) { diskService.copyPartition(src, dst); // TODO: call copy partition on the cluster if (!partitionService.tryFinishRebalancing(src)) { log.warn("The state was changed, actual {}", partitionService.getById(src.getDisk(), src.getId())); } } else { log.warn("The state was changed, actual {}", partitionService.getById(src.getDisk(), src.getId())); } } private Optional<Integer> getDstDisk(Map<Integer, Integer> disksToPartitions, int srcDisk) { return disksToPartitions.entrySet().stream() .min(comparing(Map.Entry::getValue)) .map(Map.Entry::getKey) .filter(toDisk -> toDisk != srcDisk); } }