/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2016, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> */ package org.jdiameter.client.impl.router; import org.jdiameter.api.Configuration; import org.jdiameter.api.MetaData; import org.jdiameter.api.PeerState; import org.jdiameter.client.api.IContainer; import org.jdiameter.client.api.controller.IPeer; import org.jdiameter.client.api.controller.IRealmTable; import org.jdiameter.common.api.concurrent.IConcurrentFactory; import org.jdiameter.common.api.statistic.IStatistic; import org.jdiameter.common.api.statistic.IStatisticRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.List; /** * Weighted Least-Connections router implementation<br/><br/> * * This requires that {@link IStatistic Statistics} for the following records are enabled: Peer,AppGenRequestPerSecond,NetGenRequestPerSecond * In the client configuration, please use the following settings: * * <pre> * ... * <Parameters> * <Statistics pause="5000" delay="5000" enabled="true" active_records="Peer,AppGenRequestPerSecond,NetGenRequestPerSecond"/> * </Parameters> * ... * <Extensions> * <RouterEngine value="org.jdiameter.client.impl.router.WeightedLeastConnectionsRouter" /> * </Extensions> * </pre> * * @see <a href="http://kb.linuxvirtualserver.org/wiki/Weighted_Least-Connection_Scheduling">http://kb.linuxvirtualserver.org/wiki/Weighted_Least-Connection_Scheduling</a> * @author <a href="mailto:n.sowen@2scale.net">Nils Sowen</a> */ public class WeightedLeastConnectionsRouter extends RouterImpl { private static final Logger logger = LoggerFactory.getLogger(WeightedLeastConnectionsRouter.class); protected WeightedLeastConnectionsRouter(IRealmTable table, Configuration config) { super(null, null, table, config, null); } public WeightedLeastConnectionsRouter(IContainer container, IConcurrentFactory concurrentFactory, IRealmTable realmTable, Configuration config, MetaData aMetaData) { super(container, concurrentFactory, realmTable, config, aMetaData); } /** * Return peer with least connections<br/> * {@url http://kb.linuxvirtualserver.org/wiki/Weighted_Least-Connection_Scheduling http://kb.linuxvirtualserver.org/wiki/Weighted_Least-Connection_Scheduling} * * <p> * The weighted least-connection scheduling is a superset of the least-connection scheduling, * in which you can assign a performance weight to each real server. The servers with a higher * weight value will receive a larger percentage of active connections at any one time. * The default server weight is one, and the IPVS Administrator or monitoring program can * assign any weight to real server. In the weighted least-connections scheduling, new * network connection is assigned to a server which has the least ratio of the current * active connection number to its weight. * <p> * Supposing there is a server set S = {S0, S1, ..., Sn-1}, * W(Si) is the weight of server Si; * C(Si) is the current connection number of server Si; * CSUM = ΣC(Si) (i=0, 1, .. , n-1) is the sum of current connection numbers; * <p> * The new connection is assigned to the server j, in which * (C(Sm) / CSUM)/ W(Sm) = min { (C(Si) / CSUM) / W(Si)} (i=0, 1, . , n-1), * where W(Si) isn't zero * Since the CSUM is a constant in this lookup, there is no need to divide by CSUM, * the condition can be optimized as * C(Sm) / W(Sm) = min { C(Si) / W(Si)} (i=0, 1, . , n-1), where W(Si) isn't zero * <p> * Since division operation eats much more CPU cycles than multiply operation, and Linux * does not allow float mode inside the kernel, the condition C(Sm)/W(Sm) > C(Si)/W(Si) * can be optimized as C(Sm)*W(Si) > C(Si)*W(Sm). The scheduling should guarantee * that a server will not be scheduled when its weight is zero. Therefore, the pseudo * code of weighted least-connection scheduling algorithm is * <p> * <pre> * {@code * for (m = 0; m < n; m++) { * if (W(Sm) > 0) { * for (i = m+1; i < n; i++) { * if (C(Sm)*W(Si) > C(Si)*W(Sm)) * m = i; * } * return Sm; * } * } * return NULL; * } * </pre> * <p> * The weighted least-connection scheduling algorithm requires additional division than * the least-connection scheduling. In a hope to minimize the overhead of scheduling when * servers have the same processing capacity, both the least-connection scheduling and the * weighted least-connection scheduling algorithms are implemented. * * @see <a href="http://kb.linuxvirtualserver.org/wiki/Weighted_Least-Connection_Scheduling">http://kb.linuxvirtualserver.org/wiki/Weighted_Least-Connection_Scheduling</a> * @param availablePeers list of peers that are in {@link PeerState#OKAY OKAY} state * @return the selected peer according to algorithm */ @Override public IPeer selectPeer(List<IPeer> availablePeers) { int peerSize = availablePeers != null ? availablePeers.size() : 0; // Return none if empty, or first if only one member found if (peerSize <= 0) { return null; } if (peerSize == 1) { return availablePeers.iterator().next(); } for (int m = 0; m < peerSize; peerSize++) { IPeer peerM = availablePeers.get(m); if (peerM.getRating() > 0) { for (int i = m + 1; i < peerSize; i++) { IPeer peerI = availablePeers.get(i); if (getNumConnections(peerM) * peerI.getRating() > getNumConnections(peerI) * peerM.getRating()) { m = i; } } return availablePeers.get(m); } } // Return first peer if anything did go wrong return availablePeers.iterator().next(); } /** * Since num connections is not available, determine throughput by reading statistics * and assume the load of the peer * * @param peer * @return throughput indicator */ protected long getNumConnections(IPeer peer) { if (peer == null) { return 0; } IStatistic stats = peer.getStatistic(); // If no statistics are available, return zero if (!stats.isEnabled()) { if (logger.isDebugEnabled()) { logger.debug("Statistics for peer are disabled. Please enable statistics in client config"); } return 0; } // Requests per second initiated by Local Peer + Request initiated by Remote peer String uri = peer.getUri() == null ? "local" : peer.getUri().toString(); long requests = getRecord(IStatisticRecord.Counters.AppGenRequestPerSecond.name()+'.'+uri, stats) + getRecord(IStatisticRecord.Counters.NetGenRequestPerSecond.name()+'.'+uri, stats); // There are likely more requests than responses active long connections = Math.max(0, requests); if (logger.isTraceEnabled()) { logger.trace("Active connections for {}: {}", peer, connections); } return connections; } /** * Return statistics record value from given {@link IStatistic} * * @param record key to retrieve * @param stats statistic object * @return */ protected long getRecord(String record, IStatistic stats) { if (record == null || stats == null) { return 0; } IStatisticRecord statsRecord = stats.getRecordByName(record); if (statsRecord == null) { if (logger.isDebugEnabled()) { logger.debug("Warning: no record for {}, available: {}", record, Arrays.toString(stats.getRecords())); } return 0; } return statsRecord.getValueAsLong(); } }