Статьи

Исследование утечек памяти. Часть 1. Написание утечек

Я обнаружил эту маленькую проблему на днях: есть этот сервер, который работает некоторое время, а затем падает. Затем он запускается сценарием запуска, и весь процесс повторяется. Это звучит не так уж плохо, поскольку это не критично для бизнеса, хотя существует значительная потеря данных, поэтому я решил поближе взглянуть и выяснить, что именно идет не так. Первое, что нужно отметить, это то, что сервер проходит все свои модульные тесты и целый ряд интеграционных тестов. Он хорошо работает во всех тестовых средах с использованием тестовых данных, так что же не так в работе? Нетрудно догадаться, что на производстве он, вероятно, находится под более высокой нагрузкой, чем тестирование, или чем это было разрешено при его разработке, и, следовательно, у него заканчиваются ресурсы, но какие ресурсы и где? Это сложный вопрос. Снимок экрана 2013-11-11 в 08.57.22

Чтобы продемонстрировать, как исследовать эту проблему, первое, что нужно сделать, — это написать некоторый негерметичный пример кода, и я собираюсь использовать шаблон Producer Consumer для этого, потому что я могу продемонстрировать большую проблему с ним.

Чтобы продемонстрировать негерметичный код 1, мне, как обычно, нужен сценарий с высокой степенью надуманности, и в этом сценарии представьте, что вы работаете на биржевого маклера в системе, которая регистрирует их продажи акций и акций в базе данных. Заказы принимаются и помещаются в очередь простым потоком. Затем другой поток забирает заказ из очереди и записывает его в базу данных.
Order POJO очень просто и выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class Order {
 
  private final int id;
 
  private final String code;
 
  private final int amount;
 
  private final double price;
 
  private final long time;
 
  private final long[] padding;
 
  /**
   * @param id
   *            The order id
   * @param code
   *            The stock code
   * @param amount
   *            the number of shares
   * @param price
   *            the price of the share
   * @param time
   *            the transaction time
   */
  public Order(int id, String code, int amount, double price, long time) {
    super();
    this.id = id;
    this.code = code;
    this.amount = amount;
    this.price = price;
    this.time = time;
    // This just makes the Order object bigger so that
    // the example runs out of heap more quickly.
    this.padding = new long[3000];
    Arrays.fill(padding, 0, padding.length - 1, -2);
  }
 
  public int getId() {
    return id;
  }
 
  public String getCode() {
    return code;
  }
 
  public int getAmount() {
    return amount;
  }
 
  public double getPrice() {
    return price;
  }
 
  public long getTime() {
    return time;
  }
 
}

POJO Order является частью простого Spring-приложения, в котором есть три ключевых абстракции, которые создают новый поток, когда Spring вызывает их методы start() .

Первым из них является OrderFeed . Его метод run() создает новый фиктивный порядок и помещает его в очередь. Затем он спит на мгновение, прежде чем создать следующий заказ.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class OrderFeed implements Runnable {
 
  private static Random rand = new Random();
 
  private static int id = 0;
 
  private final BlockingQueue<Order> orderQueue;
 
  public OrderFeed(BlockingQueue<Order> orderQueue) {
    this.orderQueue = orderQueue;
  }
 
  /**
   * Called by Spring after loading the context. Start producing orders
   */
  public void start() {
 
    Thread thread = new Thread(this, "Order producer");
    thread.start();
  }
 
  /** The main run loop */
  @Override
  public void run() {
 
    while (true) {
      Order order = createOrder();
      orderQueue.add(order);
      sleep();
    }
  }
 
  private Order createOrder() {
 
    final String[] stocks = { "BLND.L", "DGE.L", "MKS.L", "PSON.L", "RIO.L", "PRU.L",
        "LSE.L", "WMH.L" };
    int next = rand.nextInt(stocks.length);
    long now = System.currentTimeMillis();
 
    Order order = new Order(++id, stocks[next], next * 100, next * 10, now);
    return order;
  }
 
  private void sleep() {
    try {
      TimeUnit.MILLISECONDS.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Второй класс — это OrderRecord , который отвечает за прием заказов из очереди и их запись в базу данных. Проблема в том, что для записи заказов в базу данных требуется значительно больше времени, чем для их производства. Это демонстрируется длительным 1-секундным сном в моем recordOrder(…) .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class OrderRecord implements Runnable {
 
  private final BlockingQueue<Order> orderQueue;
 
  public OrderRecord(BlockingQueue<Order> orderQueue) {
    this.orderQueue = orderQueue;
  }
 
  public void start() {
 
    Thread thread = new Thread(this, "Order Recorder");
    thread.start();
  }
 
  @Override
  public void run() {
 
    while (true) {
 
      try {
        Order order = orderQueue.take();
        recordOrder(order);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
 
  }
 
  /**
   * Record the order in the database
   *
   * This is a dummy method
   *
   * @param order
   *            The order
   * @throws InterruptedException
   */
  public void recordOrder(Order order) throws InterruptedException {
    TimeUnit.SECONDS.sleep(1);
  }
 
}

Результат очевиден: поток OrderRecord просто не может идти в ногу, и очередь будет становиться все длиннее и длиннее, пока JVM не исчерпает пространство кучи и не упадет. Это большая проблема с шаблоном «производитель-потребитель»: потребитель должен уметь идти в ногу с производителем.

Просто чтобы доказать свою точку зрения, я добавил третий класс OrderMonitor , который печатает размер очереди каждые пару секунд, чтобы вы могли видеть, что все идет не так.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class OrderQueueMonitor implements Runnable {
 
  private final BlockingQueue<Order> orderQueue;
 
  public OrderQueueMonitor(BlockingQueue<Order> orderQueue) {
    this.orderQueue = orderQueue;
  }
 
  public void start() {
 
    Thread thread = new Thread(this, "Order Queue Monitor");
    thread.start();
  }
 
  @Override
  public void run() {
 
    while (true) {
 
      try {
        TimeUnit.SECONDS.sleep(2);
        int size = orderQueue.size();
        System.out.println("Queue size is:" + size);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
 
}

Просто, чтобы завершить состав, я включил контекст Spring ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
     default-init-method="start"
  default-destroy-method="destroy">
 
  <bean id="theQueue" class="java.util.concurrent.LinkedBlockingQueue"/>
 
  <bean id="orderProducer" class="com.captaindebug.producerconsumer.problem.OrderRecord">
   <constructor-arg ref="theQueue"/>
  </bean>
 
  <bean id="OrderRecorder" class="com.captaindebug.producerconsumer.problem.OrderFeed">
   <constructor-arg ref="theQueue"/>
  </bean>
 
  <bean id="QueueMonitor" class="com.captaindebug.producerconsumer.problem.OrderQueueMonitor">
   <constructor-arg ref="theQueue"/>
  </bean>
 
</beans>

Следующее, что нужно сделать, — запустить образец кода с утечкой. Вы можете сделать это, перейдя в следующий каталог

1
/<your-path>/git/captaindebug/producer-consumer/target/classes

… а затем, введя следующую команду:

1
java -cp /path-to/spring-beans-3.2.3.RELEASE.jar:/path-to/spring-context-3.2.3.RELEASE.jar:/path-to/spring-core-3.2.3.RELEASE.jar:/path-to/slf4j-api-1.6.1-javadoc.jar:/path-to/commons-logging-1.1.1.jar:/path-to/spring-expression-3.2.3.RELEASE.jar:. com.captaindebug.producerconsumer.problem.Main

… Где « path-to » — это путь к вашим файлам jar

Одна вещь, которую я действительно ненавижу в Java, это тот факт, что так сложно запустить любую программу из командной строки. Вы должны выяснить, что такое classpath, какие параметры и свойства нужно установить и каков основной класс. Конечно, должна быть возможность придумать способ простого ввода Java programName и JVM Java programName , где все находится, особенно если мы начнем использовать соглашение о конфигурации: насколько это сложно?

Вы также можете отслеживать утечки приложений, подключив простую jconsole. Если вы запускаете его удаленно, вам нужно добавить следующие опции в командную строку выше (выбрать свой собственный номер порта):

1
2
3
4
5
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.local.only=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

… и если вы посмотрите на количество используемой кучи, вы увидите, что она постепенно увеличивается по мере увеличения очереди.

Снимок экрана 2013-11-11 в 08.57.47

Если утекает килобайт памяти, вы, вероятно, никогда его не заметите; если гигабайт утечки памяти, проблема будет очевидна. Итак, все, что осталось сделать на данный момент — это сидеть сложа руки в ожидании утечки памяти, прежде чем перейти к следующему этапу расследования. Подробнее об этом в следующий раз …

1 Исходный код можно найти в моем проекте Producer Consumer на GitHub .