<상황>
jdk6
ojbdc14.jar (오라클 9i)
commons-dbcp
commons-pool
rac 사용



오라클의 RAC를 사용하면 아래와 같이 connection timeout를 위한 URL을 사용한다.

Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@(DESCRIPTIO
N = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP) (HOST = krtest1) (PORT = 1521)) 
(ADDRESS = (PROTOCOL = TCP) (HOST = krtest2) (PORT = 1521))) (CONNECT_DATA = 
(SERVICE_NAME = MIKE.us.oracle.com))) ", "scott", "tiger");

jdbc-thin driver를 사용할 때, JDBC에서는 리스너가  죽어있으면, 다음 설정으로 연결하게 된다.
즉, host krtest1의 1521포트에 접근했다가 connection time out이 나면, krtest2의 1521번으로 접속하게 된다. 그런데, 문제가 있다..

down이나 connection timeout일 때는 확실하게 문제가 있는데 반해서, session이 한 번 연결되었을 때, 다른 instance로 자동 연결되는 TAF(Transparent Application Failover)를 지원하지 않는다는 점을 유의해야 한다..

만약  비정상적인 종료시에는 tcp 연결중 close ack가 정상적으로 가지 못해서 아래와 같은 에러가 발생할 수 있다. 
java.sql.SQLException: IO 예외 상황: Broken pipe
at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:134)
at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:179)
at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:333)
at oracle.jdbc.driver.OracleConnection.setAutoCommit(OracleConnection.java:1224)

또는 아래와 같은 비슷한 문제가 일어날 수 있다는 점이다.
java.sql.SQLException: 소켓에서 읽을 데이터가 없습니다
at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:134)
at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:179)
at oracle.jdbc.dbaccess.DBError.check_error(DBError.java:1160)
at oracle.jdbc.ttc7.MAREngine.unmarshalUB1(MAREngine.java:963)
at oracle.jdbc.ttc7.MAREngine.unmarshalSB1(MAREngine.java:893)
at oracle.jdbc.ttc7.Ocommoncall.receive(Ocommoncall.java:104)
at oracle.jdbc.ttc7.TTC7Protocol.setAutoCommit(TTC7Protocol.java:559)
at oracle.jdbc.driver.OracleConnection.setAutoCommit(OracleConnection.java:1288)
at org.apache.commons.dbcp.DelegatingConnection.setAutoCommit(DelegatingConnection.java:331)
at org.apache.commons.dbcp.PoolingDataSource$PoolGuardConnectionWrapper.setAutoCommit(PoolingDataSource.java:317)

또한, DB쪽에서 먼저 session을 끊을 수도 있을 때. 이때도 다음번 ip로 이동하지 않을 수도 있다.

내 생각엔 connection은 있지만, session은 끊어진 황당한 상태라 할 수 있었다. 이 부분에 대한 명확하게 설명해 놓은 자료가 없어서. 소스를 뜯어보면서 생각하고 있다. (다음 번 글에는 정리된 글을 쓸 수 있을 것 같다.)

과거에는 이런 문제를 해결하기 위해서는 idle되는 connection을 네트웍단에서 끊는 작업을 하였다고 한다. 또는 지금도 이렇게 진행하고 있기도 .. 

오라클에서는 이런 문제를 해결하는 솔루션을 제시했는데..
TAF는 OCI 라이브러리를 통해 자동적으로 수행됩니다. 따라서 TAF를 사용하기 위해 애플리케이션 (클라이언트) 코드를 변경할 필요는 없습니다. 하지만 Oracle TNS 파일(tnsnames.ora) 파일에 몇 가지 설정 작업이 필요합니다.
(출처: http://blog.empas.com/myungho/read.html?a=18405273)

이 뒤를 보면 쑈킹하다.
(Java 씬 클라이언트의 경우 아직까지는 tnsnames.ora에 접근하기 위한 기능이 제공되지 않기 때문에 TAF에 참여할 수 없습니다.)

역시 thin client에서는 어쩔 수 없는 한계에 봉착한다. 즉 ora파일 만지는 얘들은 자연스럽게 문제가 쉽게 해결 수 있는 부분이 있다는 것이다.

IBM WebSpere는 이런 부분에서 일찍 고민을 했었다.
(http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=967473008)
고객은 DataBase의 일시적인 장애복구 상황에서도 자동으로 WebSphere의 "DB Pool Connection Recovery" 기능을 요구하고 있습니다...
<중략>
JDBC 1.0 스펙상에는 DB Connection Pooling 기능에 대한 언급이 존재하지 않았습니다.
<중략>
그러나 최근 JDBC 2.0 Specifiaction 이 SunmicroSystem 의해 발표된 후 대부분의 DB Vendor들을 이를 지원하게 되었으며, Oracle의 경우 Version 8.1.6 부터 JDBC 2.0을 지원하고 있습니다.
따라서, DB Connection Pool기능과 그것의 구현과정에서 transparent하게 DB Connection Recovery 기능을 제공해야 하는 책임을 갖고 있는 곳은 이젠 DB Vendor에 달려 있게 됩니다.
<중략>
Oracle의 경우 Oracle 8i Release 2 ( 8.1.6 ) 부터 JDBC 2.0 을 지원하고 있습니다. 그러나, 현재 H은행에서 사용되고 있는 형태인 JDBC Type 4 Thin Driver는 Transparent한 FailOver 기능을 아직 제공하지 않고 있습니다.
<중략>
Transparent한 DB Connection Recovery 기능을 구현하려면 매 호출시마다, Pool에 기연결되어 있는 Connection이 가용한 것인지를 확인하기 위해 SQL Query를 날려보아야 합니다.

그떄의 내용과 달리현재 오라클 JDBC URL에 따르면, Tranparent FailOver지원여부는 안나타나서 모르겠다. 그냥 failover만 지원된다고 나온다. 
http://www.oracle.com/technology/tech/java/sqlj_jdbc/htdocs/jdbc_faq.html
failover supported?

Yes. When you are connecting to a RAC server, Fast Connection Failover provides rapid response to failure events. This new High-Availability feature is driver independent and works in conjunction with the Implicit connection cache and RAC to provide maximum availability of connections in the cache. This is achieved by processing RAC's down events to remove invalid connections and up events to load balance existing connections.

If you are using the OCI driver and all you need is query fail-over, you might consider TAF. TAF primarily facilitates query failover in an application. It is not a general fail-over mechanism. Note that Fast Connection Failover and TAF can't be used together. Only one may be enabled and used at a time. 

* Fast Connection Failover라는 단어가 나왔다. 공부할 것이 생겼다. 대충 글 밑에 보면 이 설명이 있다. 오라클 10g에서는 지원한다.
* JDBC 스펙 2.0을 공부할 필요가 있다. (현재는 JDBC 4.0[2006.11)이 나왔다~) (공부많이 해야 겠다.: http://java.sun.com/products/jdbc/download.html)

과거 문서에 따르면, Tranparent하게 DB connection recovery를 하려면, 호출마다 pool에 연결되어 있는 connection을 체크하는 로직이 바로 TAF라고 한 것이다.

옛날 문서이니 신뢰할 수없다. 혹시 jdbc 2.0 부터는 이런 게 있을 까 확인해봤더니. 존재 한다. ㅎㅎ
결국 thin driver도 내부적으로 지원은 하는 셈이다.

JDBC OCI Application Failover Callbacks--OCIFailOver.java(http://download.oracle.com/docs/cd/A91773_01/ids902dl/web.902/a90211/samapp.htm)

아래 내용을 보자.

JDBC OCI Application Failover Callbacks--OCIFailOver.java

This sample demonstrates the registration and operation of JDBC OCI application failover callbacks.

For information on Transparent Application Failover (TAF) and failover events, see "OCI Driver Transparent Application Failover".

/* 
 * This sample demonstrates the registration and operation of
 * JDBC OCI application failover callbacks
 * 
 * Note: Before you run this sample, set up the following
 *       service in tnsnames.ora: 
 *       inst_primary=(DESCRIPTION=
 *             (ADDRESS=(PROTOCOL=tcp)(Host=hostname)(Port=1521))
 *             (CONNECT_DATA=(SERVICE_NAME=ORCL)
 *                           (FAILOVER_MODE=(TYPE=SELECT)(METHOD=BASIC))
 *             )
 *           )
 *       Please see the Oracle Net Administrator's Guide for more detail about 
 *       failover_mode
 *
 * To demonstrate the the functionality, first compile and start up the sample,
 *    then log into sqlplus and connect /as sysdba. While the sample is still 
 *    running, shutdown the database with "shutdown abort;". At this moment, 
 *    the failover callback functions should be invoked. Now, the database can
 *    be restarted, and the interupted query will be continued.
 */

// You need to import java.sql and oracle.jdbc packages to use
// JDBC OCI failover callback 

import java.sql.*;
import java.net.*;
import java.io.*;
import java.util.*;
import oracle.jdbc.OracleConnection;
import oracle.jdbc.OracleOCIFailover;


public class OCIFailOver {

  static final String user = "scott";
  static final String password = "tiger";
  static final String driver_class = "oracle.jdbc.OracleDriver";

  static final String URL = "jdbc:oracle:oci8:@inst_primary"; 


  public static void main (String[] args) throws Exception {

    Connection conn = null;
    CallBack   fcbk= new CallBack();
    String     msg = null;
    Statement  stmt = null;
    ResultSet rset = null; 
 
    // Load JDBC driver
    try {
      Class.forName(driver_class);
    }
    catch(Exception e) {
      System.out.println(e);
    }

    // Connect to the database
    conn = DriverManager.getConnection(URL, user, password);

    // register TAF callback function
    ((OracleConnection) conn).registerTAFCallback(fcbk, msg);

    // Create a Statement
    stmt = conn.createStatement ();

    for (int i=0; i<30; i++) {
      // Select the ENAME column from the EMP table
      rset = stmt.executeQuery ("select ENAME from EMP");

      // Iterate through the result and print the employee names
      while (rset.next ()) 
        System.out.println (rset.getString (1));

      // Sleep one second to make it possible to shutdown the DB.
      Thread.sleep(1000);
    } // End for
 
    // Close the RseultSet
    rset.close();

    // Close the Statement
    stmt.close();

    // Close the connection
    conn.close();


  } // End Main()

} // End class jdemofo 


/*
 * Define class CallBack
 */
class CallBack implements OracleOCIFailover {
   
   // TAF callback function 
   public int callbackFn (Connection conn, Object ctxt, int type, int event) {

     /*********************************************************************
      * There are 7 possible failover event
      *   FO_BEGIN = 1   indicates that failover has detected a 
      *                  lost conenction and faiover is starting.
      *   FO_END = 2     indicates successful completion of failover.
      *   FO_ABORt = 3   indicates that failover was unsuccessful, 
      *                  and there is no option of retrying.
      *   FO_REAUTH = 4  indicates that a user handle has been re-
      *                  authenticated. 
      *   FO_ERROR = 5   indicates that failover was temporarily un-
      *                  successful, but it gives the apps the opp-
      *                  ortunity to handle the error and retry failover.
      *                  The usual method of error handling is to issue 
      *                  sleep() and retry by returning the value FO_RETRY
      *   FO_RETRY = 6
      *   FO_EVENT_UNKNOWN = 7  It is a bad failover event
      *********************************************************************/
     String failover_type = null;

     switch (type) {
         case FO_SESSION: 
                failover_type = "SESSION";
                break;
         case FO_SELECT:
                failover_type = "SELECT";
                break;
         default:
                failover_type = "NONE";
     }

     switch (event) {
      
       case FO_BEGIN:
            System.out.println(ctxt + ": "+ failover_type + " failing over...");
            break;
       case FO_END:
            System.out.println(ctxt + ": failover ended");
            break;
       case FO_ABORT:
            System.out.println(ctxt + ": failover aborted.");
            break;
       case FO_REAUTH:
            System.out.println(ctxt + ": failover.");
            break;
       case FO_ERROR:
            System.out.println(ctxt + ": failover error gotten. Sleeping...");
            // Sleep for a while 
            try {
              Thread.sleep(100);
            }
            catch (InterruptedException e) {
               System.out.println("Thread.sleep has problem: " + e.toString());
            }
            return FO_RETRY;
       default:
            System.out.println(ctxt + ": bad failover event.");
            break;
       
     }  

     return 0;

   }
}


이렇게 Oracle JDBC Driver에서는 내부적으로 지원은 한다. 이렇게 oracle에 대해서는 특별히 따로 코드로 형태로 지원이 필요할 것으로 보인다. 
하지만  쓰지 않는다면, 궁금적으로 해결이 어렵지 않나 생각이 든다.


곰곰히 생각해보건데.. 
현재의 Thin driver를 사용할 때는 RAC url을 사용할 때는 Connection time failover(CTF)는 되지만, RAC에서 TAF가 안된다고 우선 결정을 내려야 할 것 같다.
오라클 10g ojdbc thin 드라이버에서는 이걸을 지원하는지 문서를 작성해봐야겠다. 지원은 하는 것처럼 쓰여져 있는데. 된다, 안된다. 이런 의견이 분분하다.

우선, 오라클 10g driver의 특성인 Fast Connection Failover(FCF)는 thin driver에는 지원이 된다. 그리고, TAF만 OCI를 지원하는 케이스이니. 잘 하면 되지 않을 까 싶다. 역시 테스트는 해봐야겠지?

Oracle® Database JDBC Developer's Guide and Reference
10g Release 2 (10.2)

http://download.oracle.com/docs/cd/B19306_01/java.102/b14355/fstconfo.htm#CIHJBFFC

Application-level connection retries

Fast Connection Failover supports application-level connection retries. This gives the application control of responding to connection failovers. The application can choose whether to retry the connection or to rethrow the exception. TAF supports connection retries only at the OCI/Net layer.


그리고, 다음 내용도 훑어볼 필요가 있다.

How It Works

Under Fast Connection Failover, each connection in the cache maintains a mapping to a service, instance, database, and hostname.

When a database generates a RAC event, that event is forwarded to the JVM in which JDBC is running. A daemon thread inside the JVM receives the RAC event and passes it on to the Connection Cache Manager. The Connection Cache Manager then throws SQL exceptions to the applications affected by the RAC event.

A typical failover scenario may work like this:

  1. A database instance fails, leaving several stale connections in the cache.

  2. The RAC mechanism in the database generates a RAC event which is sent to the JVM containing JDBC.

  3. The daemon thread inside the JVM finds all the connections affected by the RAC event, notifies them of the closed connection through SQL exceptions, and rolls back any open transactions.

  4. Each individual connection receives a SQL exception and must retry.


대충 내용을 보면, RAC 이벤트를 java에 받아서 connection을 처리할 수 있도록 한다는 것이다. 구글링을 보면, 명확치는 않다. 테스트가 필요하다.

한편 오라클 9i의 ojdbc14.jar를 쓰고 있는 상황에서 웹 어플리케이션 내에서의 reconnect(TAF처럼) 처리가 되려면, 방법은 오직 하나이다.
commons-dbcp의 testXXX 옵션을 쓰는 것외에는 방법이 없다. 
내부적으로 보면, connenection은 내부적으로 쓰고 있다. 이것을 고칠 방법은 setConnection을 호출해야 된다.

그래서 ValidationQuery, testWhileIdle를 이용하는 것 외에는 그다지 session failover를 이겨낼 방법이 없다. 샘플은 다음과 같다. hard parsing이 아닌 아주 간다한 soft parsing을 하는 select 1 from dual을 쓴다.그리고, testOnBorrow, testOnReturn 은 false로 쿼리 날아갈 때마다 부하를 주지 않도록 한다.
그리고, numTestsPerEvictionRun을 이용하여 3개씩 한번에 1분(timeBetweenEvictionRunsMillis)에 한번씩 체크하도록 한다.

            <property name="validationQuery" value="SELECT 1 FROM dual"/>
            <property name="testOnBorrow" value="false"/>
            <property name="testOnReturn" value="false"/>
            <property name="testWhileIdle" value="true"/>
            <property name="timeBetweenEvictionRunsMillis" value="60000"/>
            <property name="numTestsPerEvictionRun" value="3"/>
            <property name="minEvictableIdleTimeMillis" value="-1"/>
            <property name="maxWait" value="3000"/>
            <property name="queryTimeout" value="7"/>


많이 공부해야겠어..
Posted by 김용환 '김용환'