Я обнаружил эту маленькую проблему на днях: есть этот сервер, который работает некоторое время, а затем падает.  Затем он запускается сценарием запуска, и весь процесс повторяется.  Это звучит не так уж плохо, поскольку это не критично для бизнеса, хотя существует значительная потеря данных, поэтому я решил поближе взглянуть и выяснить, что именно идет не так.  Первое, что нужно отметить, это то, что сервер проходит все свои модульные тесты и целый ряд интеграционных тестов.  Он хорошо работает во всех тестовых средах с использованием тестовых данных, так что же не так в работе?  Нетрудно догадаться, что на производстве он, вероятно, находится под более высокой нагрузкой, чем тестирование, или чем это было разрешено при его разработке, и, следовательно, у него заканчиваются ресурсы, но какие ресурсы и где?  Это сложный вопрос. 
Чтобы продемонстрировать, как исследовать эту проблему, первое, что нужно сделать, — это написать некоторый негерметичный пример кода, и я собираюсь использовать шаблон 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 | publicclassOrder {   privatefinalintid;   privatefinalString code;   privatefinalintamount;   privatefinaldoubleprice;   privatefinallongtime;   privatefinallong[] 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    */  publicOrder(intid, String code, intamount, doubleprice, longtime) {     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 = newlong[3000];     Arrays.fill(padding, 0, padding.length - 1, -2);   }   publicintgetId() {     returnid;   }   publicString getCode() {     returncode;   }   publicintgetAmount() {     returnamount;   }   publicdoublegetPrice() {     returnprice;   }   publiclonggetTime() {     returntime;   } } | 
  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 | publicclassOrderFeed implementsRunnable {   privatestaticRandom rand = newRandom();   privatestaticintid = 0;   privatefinalBlockingQueue<Order> orderQueue;   publicOrderFeed(BlockingQueue<Order> orderQueue) {     this.orderQueue = orderQueue;   }   /**    * Called by Spring after loading the context. Start producing orders    */  publicvoidstart() {     Thread thread = newThread(this, "Order producer");     thread.start();   }   /** The main run loop */  @Override  publicvoidrun() {     while(true) {       Order order = createOrder();       orderQueue.add(order);       sleep();     }   }   privateOrder createOrder() {     finalString[] stocks = { "BLND.L", "DGE.L", "MKS.L", "PSON.L", "RIO.L", "PRU.L",         "LSE.L", "WMH.L"};     intnext = rand.nextInt(stocks.length);     longnow = System.currentTimeMillis();     Order order = newOrder(++id, stocks[next], next * 100, next * 10, now);     returnorder;   }   privatevoidsleep() {     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 | publicclassOrderRecord implementsRunnable {   privatefinalBlockingQueue<Order> orderQueue;   publicOrderRecord(BlockingQueue<Order> orderQueue) {     this.orderQueue = orderQueue;   }   publicvoidstart() {     Thread thread = newThread(this, "Order Recorder");     thread.start();   }   @Override  publicvoidrun() {     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    */  publicvoidrecordOrder(Order order) throwsInterruptedException {     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 | publicclassOrderQueueMonitor implementsRunnable {   privatefinalBlockingQueue<Order> orderQueue;   publicOrderQueueMonitor(BlockingQueue<Order> orderQueue) {     this.orderQueue = orderQueue;   }   publicvoidstart() {     Thread thread = newThread(this, "Order Queue Monitor");     thread.start();   }   @Override  publicvoidrun() {     while(true) {       try{         TimeUnit.SECONDS.sleep(2);         intsize = 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 | <?xmlversion="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">  <beanid="theQueue"class="java.util.concurrent.LinkedBlockingQueue"/>  <beanid="orderProducer"class="com.captaindebug.producerconsumer.problem.OrderRecord">   <constructor-argref="theQueue"/>  </bean>  <beanid="OrderRecorder"class="com.captaindebug.producerconsumer.problem.OrderFeed">   <constructor-argref="theQueue"/>  </bean>  <beanid="QueueMonitor"class="com.captaindebug.producerconsumer.problem.OrderQueueMonitor">   <constructor-argref="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 .
