package perf.client; import io.netty.buffer.ByteBuf; import io.reactivex.netty.client.PoolExhaustedException; import io.reactivex.netty.protocol.http.client.HttpClient; import io.reactivex.netty.protocol.http.client.HttpClientBuilder; import io.reactivex.netty.protocol.http.client.HttpClientRequest; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.cli.BasicParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.codehaus.jackson.map.ObjectMapper; import rx.Observable; import com.netflix.numerus.NumerusProperty; import com.netflix.numerus.NumerusRollingNumber; import com.netflix.numerus.NumerusRollingPercentile; public class WSClient { private ConnectionPoolMetricListener stats; public static void main(String[] rawArgs) { Options options = new Options(); options.addOption("j", false, "output JSON"); options.addOption("o", true, "output file path"); options.addOption("p", "port", true, "port"); options.addOption("h", "host", true, "host"); options.addOption("f", "step", true, "first step"); options.addOption("d", "duration", true, "duration"); options.addOption("q", "query", true, "query"); options.addOption("s", "stepsize", true, "step size"); CommandLineParser parser = new BasicParser(); CommandLine cmd; try { cmd = parser.parse(options, rawArgs); } catch (ParseException e) { throw new RuntimeException(e); } WSClient client = null; try { String host = "localhost"; if (cmd.hasOption('h')) { host = cmd.getOptionValue('h'); } int port = 8976; if (cmd.hasOption('p')) { port = Integer.parseInt(cmd.getOptionValue('p')); } int firstStep = 1; if (cmd.hasOption('f')) { firstStep = Integer.parseInt(cmd.getOptionValue('f')); } int stepSize = 200; if (cmd.hasOption('s')) { stepSize = Integer.parseInt(cmd.getOptionValue('s')); } int duration = 30; if (cmd.hasOption('d')) { duration = Integer.parseInt(cmd.getOptionValue('d')); } String query = "/?id=12345"; if (cmd.hasOption('q')) { query = cmd.getOptionValue('q'); } client = new WSClient(host, port, firstStep, stepSize, duration, query); if (cmd.hasOption('j')) client.setEnableJsonLogging(true); if (cmd.hasOption("o")) client.setOutputPath(cmd.getOptionValue("o")); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } client.startMonitoring(); client.startLoad().toBlocking().last(); } final String host; final int port; final String query; final int stepDuration; // seconds final int stepSize; final int firstStep; // starting point (1 == 1000rps, 2 == 2000rps) private boolean enableJsonLogging; String outputPath; OutputStream statsOutputStream; static final int rollingSeconds = 5; final NumerusRollingNumber counter = new NumerusRollingNumber(CounterEvent.SUCCESS, NumerusProperty.Factory.asProperty( rollingSeconds * 1000), NumerusProperty.Factory.asProperty(10)); final NumerusRollingPercentile latency = new NumerusRollingPercentile(NumerusProperty.Factory.asProperty( rollingSeconds * 1000), NumerusProperty.Factory.asProperty(10), NumerusProperty.Factory.asProperty(1000), NumerusProperty.Factory.asProperty(Boolean.TRUE)); private final Observable<ByteBuf> client; private final HttpClient<ByteBuf, ByteBuf> httpClient; private final ObjectMapper jsonMapper = new ObjectMapper(); public WSClient() { this("localhost", 8888, 1, 1000, 30, "?id=12345"); } public WSClient(String host, int port, int firstStep, int stepSize, int stepDuration, String query) { this.host = host; this.port = port; this.firstStep = firstStep; this.stepSize = stepSize; this.stepDuration = stepDuration; this.query = query; System.out.println("Starting client with hostname: " + host + " port: " + port + " first-step: " + firstStep + " step-size: " + stepSize + " step-duration: " + stepDuration + "s query: " + query); httpClient = new HttpClientBuilder<ByteBuf, ByteBuf>(this.host, this.port) .withMaxConnections(15000) .config(new HttpClient.HttpClientConfig.Builder().readTimeout(1, TimeUnit.MINUTES).build()) .build(); stats = new ConnectionPoolMetricListener(); httpClient.subscribe(stats); client = httpClient.submit(HttpClientRequest.createGet(this.query)) .flatMap(response -> { if (response.getStatus().code() == 200) { counter.increment(CounterEvent.SUCCESS); } else { counter.increment(CounterEvent.HTTP_ERROR); } return response.getContent().doOnNext(bb -> { counter.add(CounterEvent.BYTES, bb.readableBytes()); }); }).doOnError((t) -> { if (t instanceof PoolExhaustedException) { counter.increment(CounterEvent.POOL_EXHAUSTED); } else { counter.increment(CounterEvent.NETTY_ERROR); } }); } WSClient setEnableJsonLogging(boolean b) { this.enableJsonLogging = b; return this; } WSClient setOutputPath(String s) { this.outputPath = s; return this; } public Observable<Long> startLoad() { if (this.outputPath != null) { try { this.statsOutputStream = new FileOutputStream(this.outputPath); System.out.println("writing stats to " + this.outputPath); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } Observable<Observable<Long>> stepIntervals = Observable.timer(0, stepDuration, TimeUnit.SECONDS).map(l -> l + firstStep) .map(step -> { long rps = step * stepSize; long interval = TimeUnit.SECONDS.toMicros(1) / rps; StringBuilder str = new StringBuilder(); str.append('\n'); str.append("########################################################################################").append( '\n'); str.append("Step: " + step + " Interval: " + interval + "micros Rate: " + rps + "/s").append('\n'); str.append("########################################################################################").append( '\n'); System.out.println(str.toString()); if (interval < 1000) { /** * An optimization that reduces the CPU load on the timer threads. * This sacrifices more event distribution of requests for CPU by bursting requests every 100ms * instead of scheduling granularly at the microsecond level. * * We can experiment further with 10ms/100ms intervals for the right balance. */ int fixedInterval = 100; // 1000 (1ms converted to microseconds) / interval (in microseconds) to get the number per ms * number of milliseconds long numPerFixedInterval = 1000 / interval * fixedInterval; return Observable.timer(0, fixedInterval, TimeUnit.MILLISECONDS).map(i -> numPerFixedInterval); } else { return Observable.timer(0, interval, TimeUnit.MICROSECONDS).map(i -> 1L); } }); return Observable.switchOnNext(stepIntervals).doOnNext(n -> { for (int i = 0; i < n; i++) { long startTime = System.currentTimeMillis(); client.doOnCompleted(() -> { // only record latency if we successfully executed latency.addValue((int) (System.currentTimeMillis() - startTime)); }).onErrorResumeNext(Observable.<ByteBuf> empty()).subscribe(); } }); } private void startMonitoring() { final byte[] newlineBytes = "\n".getBytes(); Observable.interval(5, TimeUnit.SECONDS).doOnNext(l -> { StringBuilder msg = new StringBuilder(); msg.append("Total => "); msg.append(" Success: ").append(counter.getCumulativeSum(CounterEvent.SUCCESS)); msg.append(" Error: ").append(counter.getCumulativeSum(CounterEvent.HTTP_ERROR)); msg.append(" Netty Error: ").append(counter.getCumulativeSum(CounterEvent.NETTY_ERROR)); msg.append(" Bytes: ").append(counter.getCumulativeSum(CounterEvent.BYTES) / 1024).append("kb"); msg.append(" \n Rolling =>"); msg.append(" Success: ").append(getRollingSum(CounterEvent.SUCCESS)).append("/s"); msg.append(" Error: ").append(getRollingSum(CounterEvent.HTTP_ERROR)).append("/s"); msg.append(" Netty Error: ").append(getRollingSum(CounterEvent.NETTY_ERROR)).append("/s"); msg.append(" Pool exhausted: ").append(getRollingSum(CounterEvent.POOL_EXHAUSTED)).append("/s"); msg.append(" Bytes: ").append(getRollingSum(CounterEvent.BYTES) / 1024).append("kb/s"); msg.append(" \n Latency (ms) => 50th: ").append(latency.getPercentile(50.0)).append(" 90th: ").append(latency.getPercentile(90.0)); msg.append(" 99th: ").append(latency.getPercentile(99.0)).append(" 100th: ").append(latency.getPercentile(100.0)); System.out.println(msg.toString()); StringBuilder n = new StringBuilder(); n.append(" Netty => Used: ").append(stats.getInUseCount()); n.append(" Idle: ").append(stats.getIdleCount()); n.append(" Total Conns: ").append(stats.getTotalConnections()); n.append(" AcqReq: ").append(stats.getPendingAcquire()); n.append(" RelReq: ").append(stats.getPendingRelease()); System.out.println(n.toString()); if (enableJsonLogging) { try { Map<String, Object> m = new HashMap<String, Object>(); m.put("totalSuccesses", counter.getCumulativeSum(CounterEvent.SUCCESS)); m.put("totalErrors", counter.getCumulativeSum(CounterEvent.HTTP_ERROR)); m.put("totalNettyErrors", counter.getCumulativeSum(CounterEvent.NETTY_ERROR)); m.put("totalBytes", counter.getCumulativeSum(CounterEvent.BYTES)); m.put("rollingSuccess", getRollingSum(CounterEvent.SUCCESS)); m.put("rollingErrors", getRollingSum(CounterEvent.HTTP_ERROR)); m.put("rollingNettyErrors", getRollingSum(CounterEvent.NETTY_ERROR)); m.put("rollingPoolExhausted", getRollingSum(CounterEvent.POOL_EXHAUSTED)); m.put("rollingBytes", getRollingSum(CounterEvent.BYTES) / 1024); m.put("rollingLatencyMedian", latency.getPercentile(50.0)); m.put("rollingLatency90", latency.getPercentile(90.0)); m.put("rollingLatency99", latency.getPercentile(99.0)); m.put("rollingLatencyMax", latency.getPercentile(100.0)); m.put("connsInUse", stats.getInUseCount()); m.put("connsIdeal", stats.getIdleCount()); m.put("connsTotal", stats.getTotalConnections()); m.put("connsPendingAcquire", stats.getPendingAcquire()); m.put("connsPendingRelease", stats.getPendingRelease()); String statMsg = jsonMapper.writeValueAsString(m); if (this.statsOutputStream != null) { this.statsOutputStream.write(statMsg.getBytes()); this.statsOutputStream.write(newlineBytes); } } catch (IOException e) { throw new RuntimeException(e); } } }).subscribe(); } private long getRollingSum(CounterEvent e) { long s = counter.getRollingSum(e); if (s > 0) { s /= rollingSeconds; } return s; } }