Memory issue with JAX RS using jersey

2019-05-14 22:08发布

问题:

We currently have some trouble on a productive server as it consumes way too much memory. One of the leaks could come from the jersey client. I found the following two other questions and a how to:

  1. How to correctly share JAX-RS 2.0 client
  2. Closing JAX RS Client/Response
  3. https://blogs.oracle.com/japod/entry/how_to_use_jersey_client

What I get from it, I should reuse the Client and potentially also the WebTargets? Also closing responses is advised, but how can I do this with .request()?

Code example, this is getting called about 1000 times per hour with different paths:

public byte[] getDocument(String path) {
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(config.getPublishHost() + path);
    try {
        byte[] bytes = target.request().get(byte[].class);
        LOGGER.debug("Document size in bytes: " + bytes.length);
        return bytes;
    } catch (ProcessingException e) {
        LOGGER.error(Constants.PROCESSING_ERROR, e);
        throw new FailureException(Constants.PROCESSING_ERROR, e);
    } catch (WebApplicationException e) {
        LOGGER.error(Constants.RESPONSE_ERROR, e);
        throw new FailureException(Constants.RESPONSE_ERROR, e);
    } finally {
        client.close();
    }
}

So my question is how to properly use the API to prevent leaks for the above example?

回答1:

Client instances should be reused

Client instances are heavy-weight objects that manage the underlying client-side communication infrastructure. Hence initialization as well as disposal of a Client instance may be a rather expensive operation.

The documentation advises to create only a small number of Client instances and reuse them when possible. It also states that Client instances must be properly closed before being disposed to avoid leaking resources.

WebTarget instances could be reused

You could reuse WebTarget instances if you perform multiple requests to the same path. And reusing WebTarget instances is recommended if they have some configuration.

Response instances should be closed if you don't read the entity

Response instances that contain an un-consumed entity input stream should be closed. This is typical for scenarios where only the response headers and the status code are processed, ignoring the response entity. See this answer for more details on closing Response instances.

Improving your code

For the situation mentioned in your question, you want you ensure that the Client instance is reused for all getDocument(String) method invocations.

For instance, if your application is CDI based, create a Client instance when the bean is constructed and dispose it before its destruction. In the example below, the Client instance is stored in a singleton bean:

@Singleton
public class MyBean {

    private Client client;

    @PostConstruct
    public void onCreate() {
        this.client = ClientBuilder.newClient();
    }

    ...

    @PreDestroy
    public void onDestroy() {
        this.client.close();
    }
}

You don't need to (or maybe you can't) reuse the WebTarget instance (the requested path changes for each method invocation). And the Response instance is automatically closed when you read the entity into a byte[].

Using a connection pool

A connection pool can be a good performance improvement.

As mentioned in my older answer, by default, the transport layer in Jersey is provided by HttpURLConnection. This support is implemented in Jersey via HttpUrlConnectorProvider. You can replace the default connector if you want to and use a connection pool for better performance.

Jersey integrates with Apache HTTP Client via the ApacheConnectorProvider. To use it, add the following dependecy:

<dependency>
    <groupId>org.glassfish.jersey.connectors</groupId>
    <artifactId>jersey-apache-connector</artifactId>
    <version>2.26</version>
</dependency>

And then create your Client instance as following:

PoolingHttpClientConnectionManager connectionManager = 
        new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(5);

ClientConfig clientConfig = new ClientConfig();
clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager);
clientConfig.connectorProvider(new ApacheConnectorProvider());

Client client = ClientBuilder.newClient(clientConfig);

For additional details, refer to Jersey documentation about connectors.



回答2:

Use the following example in this link to close Response on completed method: https://jersey.github.io/documentation/latest/async.html#d0e10209

    final Future<Response> responseFuture = target().path("http://example.com/resource/")
            .request().async().get(new InvocationCallback<Response>() {
                @Override
                public void completed(Response response) {
                    System.out.println("Response status code "
                            + response.getStatus() + " received.");
                    //here you can close the response
                }

                @Override
                public void failed(Throwable throwable) {
                    System.out.println("Invocation failed.");
                    throwable.printStackTrace();
                }
            });

tip 1 (Response or String):

You can close the response only when it is from type of Response class, not : String.

tip 2 (Auto-closing):

Referring to this question, When you read the entity, the response will be closed automatically:

String responseAsString = response.readEntity(String.class);

tip 3 (connection pooling):

Referring to this question, you can use connection-pools to have better performance. example:

public static JerseyClient getInstance() {
    return InstanceHolder.INSTANCE;
}

private static class InstanceHolder {
    private static final JerseyClient INSTANCE = createClient();

    private static JerseyClient createClient() {
        ClientConfig clientConfig = new ClientConfig();

        clientConfig.property(ClientProperties.ASYNC_THREADPOOL_SIZE, 200);

        clientConfig.property(ClientProperties.READ_TIMEOUT, 10000);
        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 10000);

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();

        connectionManager.setMaxTotal(200);
        connectionManager.setDefaultMaxPerRoute(100);
        clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager);
        clientConfig.connectorProvider(new ApacheConnectorProvider());

        JerseyClient client = JerseyClientBuilder.createClient(clientConfig);
        //client.register(RequestLogger.requestLoggingFilter);
        return client;
    }
}

ATTENTION! By using this solution, if you don't close the response, you can not send more than 100 requests to server (setDefaultMaxPerRoute(100))