Java: Async vs Sync Servlet and BIO / NIO Tomcat Connector

Un pequeño micro benchmark para comparar un Servlet Sincrono “clasico” versus su version Asincrona (de la nueva especificacion de Servlet 3.0) y la diferencia de rendimiento del conector BIO vs NIO.

## Connector BIO: org.apache.coyote.http11.Http11Protocol ##
SyncServlet  time=52837ms threads=100 req/thread=2000 request=200000 req/ms=3 
AsyncServlet time=55006ms threads=100 req/thread=2000 request=200000 req/ms=3 
SyncServlet  time=50124ms threads=100 req/thread=2000 request=200000 req/ms=3 
AsyncServlet time=54444ms threads=100 req/thread=2000 request=200000 req/ms=3 
SyncServlet  time=49937ms threads=100 req/thread=2000 request=200000 req/ms=4 
AsyncServlet time=54180ms threads=100 req/thread=2000 request=200000 req/ms=3 

## Connector NIO: org.apache.coyote.http11.Http11NioProtocol ##
SyncServlet  time=64741ms threads=100 req/thread=2000 request=200000 req/ms=3 
AsyncServlet time=72572ms threads=100 req/thread=2000 request=200000 req/ms=2 
SyncServlet  time=64163ms threads=100 req/thread=2000 request=200000 req/ms=3 
AsyncServlet time=72353ms threads=100 req/thread=2000 request=200000 req/ms=2 
SyncServlet  time=66192ms threads=100 req/thread=2000 request=200000 req/ms=3 
AsyncServlet time=71574ms threads=100 req/thread=2000 request=200000 req/ms=2 

* Menor tiempo mejor

MiniServlet Sincrono:

@WebServlet("/SyncServlet")
public class SyncServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		res.setContentType("text/plain");
		PrintWriter out = res.getWriter();
		out.println("OK");
		out.flush();
	}
}

MiniServlet Asincrono:

@WebServlet(value = "/AsyncServlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		final AsyncContext ctx = req.startAsync();
		ctx.start(new Runnable() {
			@Override
			public void run() {
				final ServletResponse response = ctx.getResponse();
				response.setContentType("text/plain");
				try {
					PrintWriter out = response.getWriter();
					out.println("OK");
					out.flush();
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
				ctx.complete();
			}
		});
	}
}

Cliente:

public class TestClient {
	public static final int LOOPS = 3;
	public static final int TOTAL = 200000;
	public static final int THREADS = 100;
	private static final String BIO_URL = "http://localhost:8080/test/";
	private static final String NIO_URL = "http://localhost:8081/test/";
	private static final String SYNC = "SyncServlet";
	private static final String ASYNC = "AsyncServlet";
	private static final ArrayList<Thread> threads = new ArrayList<Thread>();
	private static long global_begin = 0;
	private static AtomicInteger counter = new AtomicInteger(0);

	public static void main(final String[] args) throws Throwable {
		final String[] URLS = new String[] {
				BIO_URL, NIO_URL
		};
		for (final String IO_URL : URLS) {
			System.out.println("Test: " + IO_URL);
			for (int j = 0; j < LOOPS; j++) {
				// SYNC
				threadInit();
				for (int i = 0; i < THREADS; i++) {
					threadAlloc(new Runnable() {
						@Override
						public void run() {
							try {
								doTest(IO_URL, SYNC);
							} catch (Exception e) {
								e.printStackTrace(System.out);
							}
						}
					});
				}
				threadDoWork(SYNC);
				// ASYNC
				threadInit();
				for (int i = 0; i < THREADS; i++) {
					threadAlloc(new Runnable() {
						@Override
						public void run() {
							try {
								doTest(IO_URL, ASYNC);
							} catch (Exception e) {
								e.printStackTrace(System.out);
							}
						}
					});
				}
				threadDoWork(ASYNC);
			}
		}
	}

	private static void threadInit() {
		threads.clear();
		counter.set(0);
	}

	private static void threadAlloc(final Runnable r) {
		threads.add(new Thread(r));
	}

	private static void threadDoWork(final String SERVLET) 
			throws InterruptedException {
		global_begin = System.currentTimeMillis();
		for (final Thread t : threads)
			t.start();
		for (final Thread t : threads)
			t.join();
		final long diff = Math.max(1, System.currentTimeMillis() - global_begin);
		StringBuilder sb = new StringBuilder();
		sb.append(SERVLET).append(' ');
		sb.append("time=").append(diff).append(' ');
		sb.append("threads=").append(THREADS).append(' ');
		sb.append("req/thread=").append((TOTAL / THREADS)).append(' ');
		sb.append("request=").append(counter.get()).append(' ');
		sb.append("req/ms=").append((counter.get() / diff));
		System.out.println(sb.toString());
	}

	private static void doTest(final String BASE_URL, final String SERVLET) 
			throws IOException {
		final String URL = BASE_URL + SERVLET;
		final BasicHttpClient cli = new BasicHttpClient();
		for (int i = 0; i < (TOTAL / THREADS); i++) {
			try {
				cli.doGet(URL);
				counter.incrementAndGet();
			} finally {
			}
		}
	}

	public static class BasicHttpClient {
		public static final int MAX_LENGTH = 4096;
		private static final Charset isoLatin1 = Charset.forName("ISO-8859-1");

		public String doGet(final String reqUrl) throws IOException {
			HttpURLConnection conn = null;
			InputStream urlIs = null;
			//
			try {
				final URL url = new URL(reqUrl);
				conn = (HttpURLConnection) url.openConnection();
				conn.setInstanceFollowRedirects(false);
				conn.setRequestMethod("GET");
				conn.setDoInput(true);
				conn.setConnectTimeout(30000);
				conn.setReadTimeout(60000);
				conn.connect();
				// Get the response
				try {
					urlIs = conn.getInputStream();
				} catch (Exception e) {
					urlIs = conn.getErrorStream();
				}
				return readFully(conn, urlIs, MAX_LENGTH);
			} finally {
				try {
					if (urlIs != null)
						urlIs.close();
				} catch (Exception ign) {
				}
			}
		}

		public String readFully(final URLConnection conn, final InputStream is, 
				final int maxLength) throws IOException {
			final int contentLength = conn.getContentLength();
			final int sbSize = Math.max(16, Math.min(maxLength, contentLength));
			final StringBuilder sb = new StringBuilder(sbSize);
			InputStreamReader in = null;
			try {
				final int bufSize = Math.max(512, Math.min(4096, contentLength));
				final char[] buf = new char[bufSize];
				int len = 0;
				in = new InputStreamReader(is, isoLatin1);
				while ((len = in.read(buf)) >= 0) {
					if (sb.length() >= maxLength)
						continue;
					sb.append(buf, 0, len);
				}
			} finally {
				if (in != null)
					in.close();
			}
			if (sb.length() > maxLength)
				sb.setLength(maxLength);
			return sb.toString();
		}
	}
}

Configuracion de los Conectores de Tomcat:

<!-- server.xml -->
...
  <Connector port="8080" protocol="org.apache.coyote.http11.Http11Protocol" 
             connectionTimeout="20000" maxThreads="200" minThreads="100" />
  <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol"
             connectionTimeout="20000" maxThreads="200" minThreads="100" />
...

Referencias:
Tomcat HTTP Connector
Asynchronous Support in Servlet 3.0
Creating asynchronous servlets with Tomcat 7 (Servlet 3.0 API)

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: