Я обнаружил эту маленькую проблему на днях: есть этот сервер, который работает некоторое время, а затем падает. Затем он запускается сценарием запуска, и весь процесс повторяется. Это звучит не так уж плохо, поскольку это не критично для бизнеса, хотя существует значительная потеря данных, поэтому я решил поближе взглянуть и выяснить, что именно идет не так. Первое, что нужно отметить, это то, что сервер проходит все свои модульные тесты и целый ряд интеграционных тестов. Он хорошо работает во всех тестовых средах с использованием тестовых данных, так что же не так в работе? Нетрудно догадаться, что на производстве он, вероятно, находится под более высокой нагрузкой, чем тестирование, или чем это было разрешено при его разработке, и, следовательно, у него заканчиваются ресурсы, но какие ресурсы и где? Это сложный вопрос. 
Чтобы продемонстрировать, как исследовать эту проблему, первое, что нужно сделать, — это написать некоторый негерметичный пример кода, и я собираюсь использовать шаблон 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"?> xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd" 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 |
… и если вы посмотрите на количество используемой кучи, вы увидите, что она постепенно увеличивается по мере увеличения очереди.
Если утекает килобайт памяти, вы, вероятно, никогда его не заметите; если гигабайт утечки памяти, проблема будет очевидна. Итак, все, что осталось сделать на данный момент — это сидеть сложа руки в ожидании утечки памяти, прежде чем перейти к следующему этапу расследования. Подробнее об этом в следующий раз …
1 Исходный код можно найти в моем проекте Producer Consumer на GitHub .
