Metro Optimizer
Portfolio solver that selects the Equinix metros for a design. Returns a recommendation, a trade-off frontier, sensitivity analysis and workload placement.
Method#
The optimizer selects the set of metros for a DesignSpec. In 4.0 it evaluates sets of metros (portfolios). Release 3.0.0 scored each metro on its own, which cannot measure worst-case site latency, total cost or the share that survives a metro loss.
Steps of a run:
- Load the metro catalogue, the measured inter-metro latencies and the clouds reachable in each metro.
- Remove each metro that a constraint excludes on its own.
- Enumerate every admissible portfolio from the minimum to the maximum size.
- Check each portfolio against the constraints and measure it on each objective.
- Price each portfolio through the rate cards.
- Keep the Pareto-optimal portfolios (the frontier) and recommend the one that the weights rank first.
- Compute the sensitivity of the recommendation to the weights and to each constraint.
- Place the workloads on the recommended portfolio.
The search is exhaustive and exact for the given data, up to 2,000,000 portfolios. The size range is minMetros (or the minimum of the redundancy tier, or 1) to maxMetros (default 3).
Running#
From a document:
import com.eqixiac.equinix.design.optimizer.MetroOptimizer;
import com.eqixiac.equinix.design.optimizer.model.OptimizationResult;
import com.eqixiac.equinix.design.spec.DesignSpec;
import com.eqixiac.equinix.design.spec.DesignSpecIO;
DesignSpec spec = DesignSpecIO.read(Path.of("designs/payments-platform.yaml"));
OptimizationResult result = MetroOptimizer.forSpec(fabric, spec).run();With the fluent builder, which assembles the same document:
import com.eqixiac.equinix.core.enums.MetroCode;
import com.eqixiac.equinix.design.optimizer.enums.ComplianceZone;
import com.eqixiac.equinix.design.optimizer.enums.OptimizationStrategy;
import com.eqixiac.equinix.design.optimizer.enums.RedundancyTier;
import com.eqixiac.equinix.design.optimizer.enums.SiteRole;
import com.eqixiac.equinix.design.optimizer.enums.WorkloadType;
import com.eqixiac.equinix.fabric.model.implementation.cloud.CloudProviderType;
OptimizationResult result = fabric.optimizeMetros()
.name("payments-platform")
.addSite("New York HQ").nearestMetro(MetroCode.NY).role(SiteRole.HEADQUARTERS).headcount(500).done()
.addSite("London office").nearestMetro(MetroCode.LD).headcount(200).done()
.addSite("Singapore office").coordinates(1.3521, 103.8198).headcount(150).done()
.requireProvider(CloudProviderType.AWS).region("us-east-1").monthlyEgressGb(40_000).done()
.requireProvider(CloudProviderType.AZURE).done()
.preferProvider(CloudProviderType.GOOGLE_CLOUD).done()
.addWorkload("card-authorisation")
.type(WorkloadType.TRANSACTIONAL).bandwidthMbps(2_000).maxLatencyToleranceMs(25.0)
.dependsOn(CloudProviderType.AWS).done()
.addWorkload("settlement-dr")
.type(WorkloadType.DISASTER_RECOVERY).bandwidthMbps(500)
.dependsOn(CloudProviderType.AZURE).done()
.constraints()
.monthlyBudget(60_000)
.redundancy(RedundancyTier.MULTI_REGION)
.compliance(ComplianceZone.EU_GDPR)
.maxLatencyMs(100) // round-trip, from every site to its nearest metro
.excludeMetro(MetroCode.MX)
.maxMetros(4)
.done()
.strategy(OptimizationStrategy.BALANCED)
.optimize();A cloud added with requireProvider must be reachable from the portfolio. A cloud added with preferProvider counts toward the coverage objective and is not mandatory. A provider can also be named by its service profile name, for a network or SaaS provider that is not one of the well-known clouds.
Objectives#
Each objective is measured in its own unit. Scores are normalized across the admissible portfolios before the weights are applied.
| Objective id | Measures | Unit | Unknown when |
|---|---|---|---|
latency | Site-weighted round-trip latency from each site to its nearest metro in the portfolio | ms, lower is better | No site latency is known. A site with no known latency is left out and named. |
cost | Expected monthly total from the cost model | Currency, lower is better | Any item is unpriced, or the lines span currencies. An understated total would reward missing data. |
provider_coverage | Share of the spec's clouds reachable privately from at least one metro; a required cloud counts twice | 0 to 100 | Never. 100 when the spec names no cloud. |
resilience | What survives the worst single-metro loss: the mean of the surviving shares of site weight, required clouds and placed workloads | 0 to 100 | Nothing to measure. A one-metro design scores 0. |
compliance | Share of metro and zone pairs that satisfy the stated zones | 0 to 100 | Never. 100 when the spec states no zone. |
The weight profiles are listed under DesignSpec. .strategy(OptimizationStrategy.LATENCY_FIRST) selects a profile and .weights(Map) sets explicit weights.
Constraints#
A portfolio that violates a constraint is infeasible, whatever its score.
| Constraint | Input |
|---|---|
| Required and excluded metros | requireMetro, excludeMetro |
| Required and excluded regions | requiredRegions, excludedRegions |
| Compliance zones, geo zones | compliance, geoZones |
| Minimum redundancy | redundancy(RedundancyTier) |
| Metro count | minMetros, maxMetros |
| Maximum site latency | maxLatencyMs: every site must have a metro within the limit. A site with unknown latency does not exclude a metro. |
| Workload latency | maxLatencyToleranceMs per workload |
| Pinned workloads | pinnedMetro |
| Required clouds | Every required cloud must be reachable from the portfolio |
| Budget | monthlyBudget |
| Every metro used | A portfolio must not contain a metro that serves nothing |
Latency data#
All latencies are round-trip. Each figure comes from the first of the following three sources that applies.
- Measured:
connectedMetros.avgLatencyfrom the Fabric Metros API. - Estimated: the round-trip fibre floor over the great-circle distance, multiplied by a route factor. The factor is the median ratio of measured round trip to fibre floor over the metro pairs that Fabric measures, fitted for the whole catalogue and per region pair where enough pairs exist (
getLatencyCalibration()). - Unknown: no measurement and no coordinates.
Release 3.0.0 mixed a one-way estimate with round-trip measurements and assumed 150 ms where it had no data. In 4.0 every figure is round-trip, and the latency matrix marks estimated values and prints unknown ones as ?.
Reading the result#
import com.eqixiac.equinix.design.solve.SensitivityReport;
import com.eqixiac.equinix.design.spi.ConstraintVerdict;
import com.eqixiac.equinix.design.spi.CostLine;
import com.eqixiac.equinix.design.spi.DesignOption;
System.out.println(result.toSummary());
DesignOption recommended = result.getRecommended();
System.out.println("Metros: " + recommended.portfolio().metros());
recommended.values().forEach((objective, value) ->
System.out.println(objective + " = " + value.value() + " (" + value.explanation() + ")"));
for (CostLine line : recommended.cost().lines()) {
System.out.println(line.label() + " " + line.monthly() + " " + line.currency()
+ " [" + line.source() + "] range " + line.monthlyLow() + " to " + line.monthlyHigh());
}
recommended.cost().unpriced().forEach(item -> System.out.println("unpriced: " + item));
for (ConstraintVerdict verdict : result.getConstraintVerdicts()) {
System.out.println(verdict.constraintId() + " " + (verdict.satisfied() ? "ok" : "FAILED") + " " + verdict.reason());
}
for (DesignOption option : result.getFrontier()) { // every Pareto-optimal alternative
System.out.println(option.portfolio().key() + " score " + option.weightedScore());
}
SensitivityReport sensitivity = result.getSensitivity();
for (SensitivityReport.WeightRange range : sensitivity.weightRanges()) {
System.out.println(range.objective() + " weight " + range.weight()
+ " keeps the recommendation between " + range.low() + " and " + range.high());
}
sensitivity.constraints().forEach(c ->
System.out.println(c.constraint() + (c.binding() ? " is binding: " : " is not binding: ") + c.explanation()));
Files.writeString(Path.of("metro-optimization.md"), result.toMarkdown());
String json = result.toJson();run() and optimize() always return a result. When no portfolio is feasible, the result says which constraints rejected the candidates, and the sensitivity report names the constraint whose relaxation would admit an alternative.
The result also keeps the per-metro views of earlier releases: getRecommendations(), primaryMetro(), getLatencyMatrix(), getTopology(), getRiskAssessment() and getCostEstimate().
Cost ranges#
Each cost line carries a range that depends on the source of its price.
| Price source | Range |
|---|---|
EQUINIX_LIVE, CUSTOM, PROVIDER_API | Exact |
REFERENCE, COMPOSITE | Plus or minus 25% |
ESTIMATE | Plus or minus 50% |
The two percentages are fixed values chosen by judgment and have no statistical basis.
Sensitivity#
- Per objective: the weight range over which the recommendation stays the same, and the portfolio recommended outside that range.
- The nearest alternative and its difference on each objective.
- Per constraint: whether it is binding, the number of portfolios it alone rejected, and the alternative admitted without it.
Narrow weight ranges mean that a small change of a weight changes the recommendation. Compare the frontier alternatives in that case.
Placement#
result.getPlacement() assigns each workload a primary metro, and a standby where the design has one, and lists the cloud legs per metro. The Deployment Wizard plans connections from this placement.
Workload types#
WorkloadType sets defaults for a workload, chiefly its latency sensitivity.
| Type | Default latency sensitivity |
|---|---|
REALTIME_COLLABORATION, EDGE_COMPUTE | Critical |
AI_ML_INFERENCE, TRANSACTIONAL | High |
AI_ML_TRAINING, GENERAL_COMPUTE, BIG_DATA_ANALYTICS | Medium |
DISASTER_RECOVERY, COLD_BACKUP | Low |
latencySensitivity(...) and maxLatencyToleranceMs(...) override the default. The 3.0.0 builder also took power-density and liquid-cooling flags, which the 4.0 builder does not accept. requireMetro or pinnedMetro holds a workload in a metro whose facility is known to meet such a requirement.
Extension#
Objectives, constraints, the cost model and the solver are interfaces in design.spi.
import com.eqixiac.equinix.design.spi.Constraint;
import com.eqixiac.equinix.design.spi.DesignContext;
import com.eqixiac.equinix.design.spi.DesignPlugin;
import com.eqixiac.equinix.design.spi.MetroPortfolio;
public class NoSingleCountryPlugin implements DesignPlugin {
@Override public String name() { return "no-single-country"; }
@Override public List<Constraint> constraints() {
return List.of(new Constraint() {
@Override public String id() { return "no_single_country"; }
@Override public ConstraintVerdict check(MetroPortfolio portfolio, DesignContext context) {
long countries = portfolio.metros().stream()
.map(id -> context.metro(id).map(m -> m.country()).orElse("?"))
.distinct().count();
return countries > 1
? ConstraintVerdict.pass(id(), countries + " countries")
: ConstraintVerdict.fail(id(), "all metros are in one country");
}
});
}
}OptimizationResult withPlugin = MetroOptimizer.forSpec(fabric, DesignSpecIO.read(Path.of("designs/payments-platform.yaml")))
.plugin(new NoSingleCountryPlugin())
.run();Plug-ins can also be discovered through java.util.ServiceLoader. rateCard(...) and costModel(...) on the optimizer replace the pricing.
Limits#
- The optimizer chooses metros. It does not choose an IBX within a metro and has no data on cage availability or power.
- The optimizer treats a cloud as reachable in a metro when a service profile of that provider is offered there. It does not check capacity or the provider's own regional constraints.
- The measured latency is the Fabric inter-metro figure. Latency from a site to a metro over a carrier circuit is an estimate from distance.
- Cost uses the rate-card chain and inherits its limits, see Cost and value.
- The number of portfolios grows combinatorially with
maxMetros. A highmaxMetroson the full catalogue can reach the limit of 2,000,000 portfolios, and the solver notes of the result then state that the answer is not exact.