【问题标题】:Java: Optimizing an application using asynchronous programmingJava:使用异步编程优化应用程序
【发布时间】:2015-09-23 13:15:56
【问题描述】:

我必须修改 dropwizard 应用程序以提高其运行时间。基本上,该应用程序每天接收大约 300 万个 URL,并下载并解析它们以检测恶意内容。问题是应用程序只能处理 100 万个 URL。当我查看应用程序时,我发现它正在进行大量的顺序调用。我想要一些关于如何通过异步或其他技术改进应用程序的建议。

所需代码如下:-

/* Scheduler */
private long triggerDetection(String startDate, String endDate) {
for (UrlRequest request : urlRequests) {
                if (!validateRequests.isWhitelisted(request)) {
                    ContentDetectionClient.detectContent(request);
                }
            }
}

/* Client */
public void detectContent(UrlRequest urlRequest){
        Client client = new Client();
        URI uri = buildUrl(); /* It returns the URL of this dropwizard application's resource method provided below */

        ClientResponse response = client.resource(uri)
                .type(MediaType.APPLICATION_JSON_TYPE)
                .post(ClientResponse.class, urlRequest);

        Integer status = response.getStatus();
        if (status >= 200 && status < 300) {
            log.info("Completed request for url: {}", urlRequest.getUrl());

        }else{
            log.error("request failed for url: {}", urlRequest.getUrl());
        }
    }

    private URI buildUrl() {
        return UriBuilder
                .fromPath(uriConfiguration.getUrl())
                .build();
    }

/* Resource Method */
 @POST
    @Path("/pageDetection")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    /**
     * Receives the url of the publisher, crawls the content of that url, applies a detector to check if the content is malicious.
     * @returns returns the probability of the page being malicious
     * @throws throws exception if the crawl call failed
     **/
    public DetectionScore detectContent(UrlRequest urlRequest) throws Exception {

        return contentAnalysisOrchestrator.detectContentPage(urlRequest);
    }

/* Orchestrator */
public DetectionScore detectContentPage(UrlRequest urlRequest) {
        try {

            Pair<Integer, HtmlPage> response =  crawler.rawLoad(urlRequest.getUrl());
            String content =   response.getValue().text();

            DetectionScore detectionScore = detector.getProbability(urlRequest.getUrl(), content);
            contentDetectionResultDao.insert(urlRequest.getAffiliateId(), urlRequest.getUrl(),detectionScore.getProbability()*1000,
                    detectionScore.getRecommendation(), urlRequest.getRequestsPerUrl(), -1, urlRequest.getCreatedAt() );

            return detectionScore;

        } catch (IOException e) {
            log.info("Error while analyzing the url : {}", e);
            throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
        }
    }

我正在考虑以下方法:-

  • 我没有通过 POST 调用 dropwizard 资源方法,而是直接从调度程序调用 orchestrator.detectContent(urlRequest)

  • 编排器可以返回 detectionScore,我会将所有的 detectScore 存储在一个映射/表中并执行批量数据库插入,而不是像当前代码中那样单独插入。

我想要一些关于上述方法的 cmets 以及其他可以改进运行时间的技术。另外,我刚刚阅读了有关 Java 异步编程的内容,但似乎无法理解如何在上面的代码中使用它,所以也希望得到一些帮助。

谢谢。

编辑: 我能想到两个瓶颈:

  • 网页下载
  • 将结果插入数据库(数据库位于另一个系统中)
  • 似乎一次处理 1 个 URL

系统有 8 GB 内存,其中 4 GB 似乎是空闲的

$ free -m
             total       used       free     shared    buffers     cached
Mem:          7843       4496       3346          0        193       2339
-/+ buffers/cache:       1964       5879 
Swap:         1952        489       1463 

CPU 使用率也很低:

top - 13:31:19 up 19 days, 15:39,  3 users,  load average: 0.00, 0.00, 0.00
Tasks: 215 total,   1 running, 214 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.5%us,  0.0%sy,  0.0%ni, 99.4%id,  0.1%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   8031412k total,  4605196k used,  3426216k free,   198040k buffers
Swap:  1999868k total,   501020k used,  1498848k free,  2395344k cached

【问题讨论】:

  • “测量两次,优化一次”是老人们在尝试解决性能问题时常说的。
  • 首先要检查:您的 CPU 利用率。如果达到或接近 100%,异步处理将无济于事。
  • 您是一次处理 1 个 URL(来自客户端),还是执行批量操作?您从客户那里获得了多少并发请求。我想知道您的系统的利用率是多少。如果系统本身已达到或接近容量,则优化单个请求路径不一定有帮助。
  • CPU 使用率最低。似乎有两个瓶颈:1)网页的下载2)插入数据库(数据库位于另一台机器)

标签: java asynchronous optimization


【解决方案1】:

首先检查您大部分时间松动的地方。

我想大部分时间都丢失了下载网址。

如果下载 url 花费超过 90% 的时间,您可能无法改进您的应用程序,因为瓶颈不是 java,而是您的网络。


仅当下载时间在网络能力范围内时才考虑以下事项

如果下载时间不是那么长,您可能可以尝试提高您的性能。一种标准方法是使用生产者消费者链。详情请见here

基本上你可以按如下方式拆分工作:

Downloading --> Parsing --> Saving 

下载是生产者,解析是下载过程的消费者,生产者保存过程,保存是消费者。

每个步骤可以由不同数量的线程执行。比如你可以有3个下载线程、5个解析线程和1个保存线程。


在 cmets 之后编辑

假设瓶颈不是cpu时间,所以对java代码进行干预并不重要。

如果您知道每天下载多少千兆字节,就可以查看它们是否接近您网络的最大带宽。

如果发生这种情况,有不同的可能性:

  • 使用Content-Encoding: gzip 请求压缩内容(从而减少使用的带宽)
  • 在工作于不同网络的不同节点之间拆分您的应用程序(因此在不同网络之间拆分带宽)
  • 更新您的带宽(因此为您的网络增加带宽)
  • 确保只下载请求的内容(如果没有请求,则不要下载 javascript、图片、css 等)(以尽量减少带宽的使用)
  • 先前解决方案的组合

【讨论】:

  • 或者你可以检查 CPU 负载,如果它已经接近最大值,多线程不会给你带来太多好处。
  • 是的,但是对于这种有大量 url 下载的场景来说,这很困难。通常 I/O 操作是瓶颈,而不是 CPU 计算。但你是对的!
  • 通过 Internet 检查网络使用情况。可能瓶颈不在内部网络(所以不访问数据库),而是外部网络(下载页面)。如果您接近互联网连接的最大带宽,您只能检查您是否请求压缩版本的内容。
  • 不错的解决方案。我还建议突出显示哪些部分是 CPU 绑定(解析)与 I/O 绑定(下载和保存),因为这可用于确定应分配给每个部分的线程数。一种可能性是解析部分在完成后继续使用下载线程,然后将异步任务传递给保存线程池。这里非常适合 CompletableFutures(甚至是简单反应)。
  • 问题不在于这个过程只由一个线程完成。如果您将所有时间都花在下载数据上,那么在不同线程之间拆分也无法改善很多。检查更新的响应以查看其他改进它的技术。
【解决方案2】:

受 Davide 的(伟大的)回答启发,这里有一个示例,使用 simple-react (我写的库)将其并行化的简单方法。请注意,它略有不同,使用客户端驱动服务器上的并发。

示例

LazyReact streamBuilder = new LazyReact(15,15);

streamBuilder.fromIterable(urlRequests)
      .filter(urlReq->!validateRequests.isWhitelisted(urlReq))
      .forEach(request -> {
           ContentDetectionClient.detectContent(request);
       });

说明

看起来您可以从客户端驱动并发。这意味着您可以在服务器端跨线程分配工作,而无需额外工作。在此示例中,我们发出 15 个并发请求,但您可以将其设置为接近服务器可以处理的最大值。您的应用程序是 IO Bound,因此您可以使用大量线程来提高性能。

simple-react 用作​​期货流。所以这里我们为每次调用 ContentDetection 客户端创建一个异步任务。我们有 15 个线程可用,因此可以一次对服务器进行 15 个调用。

Java 7

有一个用于 Java 7 的 JDK 8 功能的反向移植,称为 StreamSupport,您还可以通过 RetroLambda 反向移植 Lambda 表达式。

要使用 CompletableFutures 实现相同的解决方案,我们可以为每个符合条件的 URL 创建一个未来任务。 UPDATE 我认为我们不需要对它们进行批处理,我们可以使用 Executor 来限制活动期货的数量。我们只需要在最后加入他们。

   Executor exec = Executors.newFixedThreadPool(maxActive);//15 threads
   List<CompletableFuture<Void>> futures= new ArrayList<>();

   for (UrlRequest request : urlRequests) {
            if (!validateRequests.isWhitelisted(request)) {
                futures.add(CompletableFuture.runAsync(()->ContentDetectionClient.detectContent(request), exec));
            }
        }
 CompletableFuture.allOf(futures.toArray())
                      .join();

【讨论】:

  • 如果瓶颈是网络带宽,我认为简单的反应或任何其他多线程技术将有助于提高性能。正如您从问题中看到的那样,cpu 时间接近于 0,因此只有 I/O 操作构成了进程时间执行。我也在等待有关下载的千兆字节和带宽的信息,看看将实际程序拆分到不同线程中是否有用。
  • 忘了说。服务器 jas Java 7 :(
  • 不要害怕 - 我一直在关注 StreamSupport,它包含 CompletableFuture。我将更新一个示例,说明如何使用原始 CompletableFutures 执行此操作(适用于 Java 7)-sourceforge.net/projects/streamsupport
  • 感谢@JohnMcClean 的解决方案。明天我会尝试这个线程中提到的解决方案,并会回复你们。再次感谢大家的帮助。
  • @DavideLorenzoMARINO 您的意思是是否有可用带宽用于更多并发下载?如果他们目前一次只做一个,那么应该有。
猜你喜欢
  • 2011-01-19
  • 1970-01-01
  • 1970-01-01
  • 2013-06-26
  • 1970-01-01
  • 2015-07-05
  • 2018-11-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多