Tomcat JDBC Connection Pool trong java

Mở đầu

Đợt vừa rồi sếp có giao cho xử lý một con service dạo này thường xuyên bị down. Kiểm tra code cũ thì thấy mỗi request đến đều thực hiện mở một connection JDBC, thực hiện insert/update xong rồi đóng lại. Đợt này có lẽ lượng request đến quá nhiều dẫn tới db chịu không nổi và bị treo.

Cách giải quyết đơn giản nhất dĩ nhiên là xin các sếp cấp cho con db khác khỏe hơn, nhưng các sếp không đồng ý nên tôi đã sử dụng một giải pháp connection-pool để giới hạn lại số connection, nhân tiện note lại chút.

Thực ra connection pool có rất nhiều ưu điểm so với sử dụng trực tiếp JDBC chứ không chỉ đơn thuần là để giới hạn số connection như:

  • Giữ lại các connection sau khi sử dụng, giảm số lần tạo/hủy các connection, nâng cao đáng kể hiệu năng hệ thống.
  • Tự động hủy bỏ các connection ít sử dụng, giải phóng bớt tài nguyên khi không dùng tới.
  • Tự động validate các connection trước khi lấy ra để sử dụng, giảm khả năng xảy ra connection exception trong business logic code.
  • Tự động hủy bỏ các abandoned connection, ngăn cản chiếm giữ một connection quá lâu, gây leak tài nguyên hệ thống.

Sử dụng connection pool trong trường hợp này vẫn có một vấn đề là khi lượng request quá nhiều thì việc giới hạn số connection tối đa được mở sẽ dẫn tới một số request bị reject tạo ra connection mới, làm mất dữ liệu. Sau này thì tôi có sử dụng một message queue để cache lại các dữ liệu và thực hiện batch insert/update sau một khoảng thời gian, tuy nhiên tại thời điểm được giao yêu cầu này có khá ít thời gian để xử lý, nên việc này chưa thực hiện được.

Cấu hình và sử dụng Tomcat JDBC connection pool

Có rất nhiều giải pháp JDBC connection pool, tuy nhiên tôi sử dụng Tomcat JDBC Connection Pool vì thấy nó phù hợp với hệ thống hiện tại (đang chạy Tomcat 8) và đáp ứng đầy đủ các yêu cầu của mình, ngoài ra cách sử dụng cũng khá đơn giản.

Đầu tiên, cần thêm dependency vào Maven:

1
2
3
4
5
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-jdbc</artifactId>
  <version>8.0.25</version>
</dependency>

Để sử dụng Tomcat JDBC Connection Pool cần tạo ra một đối tượng javax.sql.DataSourcejavax.sql.DataSourcelà một interface để lấy về các java.sql.Connection bằng cách gọi hàm getConnection().

Có hai cách để tạo ra một đối tượng javax.sql.DataSource, trực tiếp trong code hoặc thông qua một JNDI resource.

Tạo DataSource trực tiếp trong code

1
2
3
4
5
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/mysql");
ds.setUsername("root");
ds.setPassword("password");

Tuy nhiên nếu sử dụng trực tiếp như trên, mỗi lần sử dụng sẽ tạo ra một đối tượng DataSource mới, đồng nghĩa sẽ là một pool mới. Thay vì thế tôi sẽ tạo ra một đối tượng DataSource khi lần đầu ứng dụng Web được chạy, và lưu lại đối tượng DataSource này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DspServicesContextListener implements ServletContextListener {
    // save DataSource object for use later
    public static DataSource ds;
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        PoolProperties p = new PoolProperties();
        p.setUrl("jdbc:mysql://localhost:3306/mysql");
        p.setUsername("root");
        p.setPassword("password");
        // set other properties
        DataSource ds = new DataSource();
        ds.setPoolProperties(p);
    }
}

Ưu điểm của cách này là cực kỳ đơn giản, dễ sử dụng nhưng nhược điểm là cấu hình các thuộc tính của pool nằm trong code nên mỗi khi cần thay đổi phải build lại code.

Tạo DataSource thông qua JNDI resource

Với cách này trước tiên phải đảm bảo rằng servlet container có support tạo DataSource thông qua Resource và JNDI context. Dành cho ai chưa biết về JNDI tại đây và đây, ở đây tôi chỉ hướng dẫn cách cấu hình và sử dụng.

Tomcat hỗ trợ 3 cách để cấu hình DataSource trong JNDI context (các servlet container khác có thể không sử dụng được cả 3 cách, tôi chưa thử):

  1. File context.xml của ứng dụng – là cách đơn giản nhất, tất cả những gì chúng ta cần là định nghĩa một Resource trong file context.xml nằm trong thư mục META-INF của ứng dụng web. Cách này có một số nhược điểm là:
    • Context file được đóng gói cùng với WAR file, nên mỗi khi có thay đổi cần thực hiện build và deploy lại.
    • DataSource được tạo bởi container dành riêng cho ứng dụng, nó không thể được sử dụng global và không thể chia sẻ giữa các ứng dụng trong cùng một container.
    • Nếu có một datasource được định nghĩa trong file server.xml (global) trùng tên, DataSource được định nghĩa ở đây sẽ bị bỏ qua.
  2. File context.xml của server – ưu điểm của cách này là có thể chia sẻ cấu hình của DataSource giữa các ứng dụng, tuy nhiên phạm vi của file context.xml vẫn là ứng dụng, cho nên nếu định nghĩa một DataSource connection pool với 100 connections và có 20 ứng dụng và mỗi ứng dụng sẽ có một DataSource tương ứng được tạo ra, khi đó container sẽ phải tạo ra và duy trì 2000 connections, gây ảnh hưởng lớn tới hiệu năng của server.
  3. server.xml and context.xm – cách này định nghĩa một DataSource ở mức global trong file server.xml trong tag GlobalNamingResource, sau đó chúng ta cần định nghĩa một Resource Link trong file context.xml của server hoặc ứng dụng. Đây là cách khuyên dùng, bằng cách này chúng ta sẽ có một DataSource connection pool chia sẻ giữa các ứng dụng khác nhau.

Đây là một ví dụ về cấu hình DataSource sử dụng cách 3:

server.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<Resource name="jdbc/DspDc"
          global="jdbc/DspDc"
          auth="Container"
          type="javax.sql.DataSource"
          factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
          testOnConnect="false"
          testWhileIdle="true"
          testOnBorrow="true"
          testOnReturn="false"
          validationQuery="SELECT 1"
          validationInterval="34000"
          timeBetweenEvictionRunsMillis="34000"
          minEvictableIdleTimeMillis="55000"
          maxActive="377"
          maxIdle="123"
          initialSize="34"
          minIdle="89"
          maxWait="10000"
          removeAbandonedTimeout="55"
          removeAbandoned="true"
          logAbandoned="true"
          username="root"
          password="root"
          driverClassName="com.mysql.jdbc.Driver"
          url="jdbc:mysql://192.168.30.94:3306/admicro_dsp_fresh"/>

context.xml

1
2
3
4
<ResourceLink name="jdbc/LocalDspDc"
            global="jdbc/DspDc"
            auth="Container"
            type="javax.sql.DataSource" />

Cuối cùng cần phải copy file jar của JDBC driver tương ứng với database sử dụng vào thư mục libs của servlet container. Ở đây tôi sử dụng MySQL và Tomcat nên cần copy file mysql-connector-jar-xxx.jar vào thư mục libs nơi chứa bộ cài Tomcat.

Lý do cần có file jar của JDBC driver ở trong thư mục libs của Tomcat là vì các JNDI resource sẽ được tạo ra tại thời điểm của Tomcat chạy, trước khi cả load các wep-app. Ngoài ra các web-app sẽ load với các Class Loader khác với Class Loader khi chạy Tomcat.

Một số thuộc tính quan trọng

Thuộc tính Mô tả
initialSize số connection tạo ra ban đầu
maxActive số connection tối đa có thể lấy ra từ pool cùng lúc
maxIdle số connection tối đa không hoạt động có thể giữ lại trong pool tại một thời điểm
minIdle số connection tối thiểu không hoạt động nên giữ lại trong pool tại một thời điểm
maxWait nếu có tối đa maxActivate connections đang hoạt động, pool sẽ chờ tối đa khoảng thời gian này trước khi trả về
timeBetweenEvictionRunsMillis chu kỳ chạy clean các idle và abandoned connections
minEvictableIdleTimeMillis connection là idle quá thời gian này sẽ bị đóng và giải phóng khỏi pool khi chạy clean
testOnBorrow validate connection trước khi lấy ra từ pool
validationQuery câu lệnh dùng để validate connection
validationInterval khoản thời gian tối thiểu connnection nên được validate lại, nếu một connection đã được validate bởi clean thread của pool trong khoảng thời gian này, nó sẽ không cần phải validate lại
removeAbandoned Nếu true, các abandoned connections sẽ bị xóa khỏi pool nếu chúng vượt quá removeAbandonedTimeout. Một connection bị coi là abandoned nếu nó được sử dụng lâu hơn removeAbandonedTimeout
removeAbandonedTimeout Thời gian tính theo giây trước khi một connection bị tính là abandoned để xóa đi. Nên set bằng với thời gian thực hiện của query lâu nhất của ứng dụng

Chi tiết tất cả các thuộc tính tham khảo tại đây

Connection pool leaks and long running queries

Tomcat JDBC Connection Pool có thể phát hiện các connection leaks hoặc một tiến trình thực hiện một query chiếm giữ tài nguyên quá lâu bằng cách set timeBetweenEvictionRunsMillis &gt; 0 AND removeAbandoned=true AND removeAbandonedTimeout &gt; 0. Một clean thread sẽ được chạy sau mỗi timeBetweenEvictionRunsMillis và nếu có một connection có thời gian sử dụng lâu hơn removeAbandonedTimeout nó sẽ bị hủy và xóa khỏi pool. Đây là cách mà các connections không được đóng bởi ứng dụng có thể được phục hồi.

Tham khảo

http://www.journaldev.com/2513/tomcat-datasource-jndi-example-for-servlet-web-application

https://examples.javacodegeeks.com/enterprise-java/tomcat/tomcat-connection-pool-configuration-example/

http://www.tomcatexpert.com/blog/2010/04/01/configuring-jdbc-pool-high-concurrency

http://www.codingpedia.org/ama/tomcat-jdbc-connection-pool-configuration-for-production-and-development/

Các bài viết cùng chủ đề:

Top