Firebird에서 교착 상태를 추적하는 방법
Alexey Kovyazin, 2019년 5월 8일, (c) IBSurgeon
업데이트 충돌(종종 «데드락»이라고도 함)은 집중적인 동시 데이터 업데이트를 수행하는 Firebird 애플리케이션에서 발생합니다.
« Firebird의 트랜잭션» 문서에서 볼 수 있듯이, «deadlock» 키워드를 포함하는 오류 유형은 3가지가 있습니다:
deadlock
-update conflicts with concurrent update
-concurrent transaction number is NNN
lock time-out on wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is NNN
lock conflict on no wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is NNN
세 가지 유형의 업데이트 충돌 모두 공통점이 있습니다. 업데이트 충돌은 일반적으로 동일한 레코드를 수정하려는 두 개의 수정 작업의 결과라는 것입니다.
가능한 옵션은 다음과 같습니다: UPDATE, DELETE SELECT WITH LOCK, MERGE, UPDATE OR INSERT.
오류 메시지를 받는 «피해자» 작업을 추적하는 것은 비교적 쉽습니다. 해당 작업을 _try… catch_에 넣고 오류가 발생하는 쿼리에 사용된 매개변수를 로그로 기록하면 됩니다.
그러나 이 접근 방식으로는 «승자», 즉 오류 없이 업데이트가 성공적으로 수행된 동시 트랜잭션을 추적하기 어렵습니다.
개발자가 업데이트 충돌을 조사할 수 있도록 Firebird는 오류 메시지에 동시 트랜잭션(즉, 동시 업데이트가 아직 커밋되지 않은 트랜잭션)에 대한 참조를 넣습니다. Trace API와 함께 이를 통해 충돌하는 두 작업을 모두 추적할 수 있습니다.
이를 수행하는 실용적인 단계를 살펴보겠습니다.
먼저 추적을 구성해야 합니다. 이를 위해 다음 내용으로 텍스트 파일(이 경우 C:\Temp\mytrace.conf)을 만듭니다:
database
{
enabled = true
include_filter="(UPDATE%)|(DELETE%)|(MERGE%)|(SELECT%FROM%WITH LOCK)|(UPDATE OR INSERT%)"
log_statement_finish = true
log_errors = true
include_gds_codes="deadlock"
log_initfini = false
time_threshold = 0
max_sql_length = 65000
}
위 구성 파일은 모든 데이터베이스에 대해 추적을 활성화하므로 활성 데이터베이스가 두 개 이상인 경우 구성 파일에 데이터베이스 이름을 지정하는 것이 좋습니다. 예:
Database=mydatabase.fdb
{
enabled = true
…
}
구성 파일에서 가장 중요한 매개변수를 검토해 보겠습니다:
Include filter 매개변수
include_filter="(UPDATE%)|(DELETE%)|(MERGE%)|(SELECT%FROM%WITH LOCK)|(UPDATE OR INSERT%)"
이는 UPDATE 및 DELETE 문만 추적하고 SELECT는 무시하려는 것을 의미합니다. 프로덕션 시스템에서 높은 부하가 걸릴 때 텍스트 로그의 레코드 수가 분석하기에 너무 많을 수 있으므로 가능한 한 많은 문을 필터링하는 것이 필요합니다.
일반적으로 업데이트 충돌에 참여하는 테이블을 알고 있으므로 필터에 테이블을 넣어 분석할 업데이트 수를 줄이는 것이 좋습니다:
include_filter="(UPDATE T1%)|(DELETE FROM T1%)|(MERGE INTO T1%)|(SELECT% FROM T1%WITH lock%)|(UPDATE OR INSERT INTO T1%)"
업데이트와 삭제가 저장 프로시저에 의해 수행된다면 어떻게 될까요? 음, 전체 프로세스가 더 어려워집니다: 동시 업데이트에 관여할 수 있는 모든 저장 프로시저를 필터에 추가하고 프로시저 로깅도 활성화하세요:
include_filter="(UPDATE T1%)|(DELETE FROM T1%)|(MERGE INTO T1%)|(SELECT% FROM T1%with lock%)|(UPDATE OR INSERT INTO T1%)|(%SP_UPDATE_T1%)|(%SP_DEL_T1%)"
log_procedures=true
오류 로깅 매개변수
그런 다음 오류 로깅을 지정하고 데드락으로 제한해야 합니다.
log_errors = true
include_gds_codes="deadlock"
참고: include_gds_codes 매개변수는 Firebird 3.0.2에서만 나타났으므로 2.5, 3.0.0 또는 3.0.1을 사용하는 경우 데드락 오류만 필터링할 수 없으며 모든 오류가 로그에 출력됩니다.
데모
전체 프로세스를 시연하기 위해 테스트 데이터베이스를 준비해 보겠습니다. 간단하게 하기 위해 기본 키 열이 1개인 테이블 1개가 있습니다.
C:\HQbird\Firebird30>isql
Use CONNECT or CREATE DATABASE to specify a database
SQL> create database "c:\temp\testdeadlock.fdb" user "SYSDBA" password "masterkey";
SQL> create table t1(i1 integer not null primary key);
SQL> insert into t1(i1) values(1);
SQL> commit; exit;
그 후 추적 세션을 시작해야 합니다:
fbtracemgr.exe -se localhost:service_mgr -start -conf "C:\temp\mytrace.conf" -user SYSDBA -pass masterkey
이 경우 fbtracemgr는 로그를 화면에 출력하지만 프로덕션에서는 물론 파일로 리디렉션해야 합니다.
성공적으로 시작된 후 fbtracemgr는 다음과 같은 내용을 출력합니다:
Trace session ID 4 started
그런 다음 2개의 isql 세션을 시작하고 업데이트 충돌을 시뮬레이션해 보겠습니다. 아래 표에는 2개의 isql 세션에 대한 2개의 열이 있습니다. 각 행은 명령이 수행된 시점을 나타냅니다.
| 시간 | isql 세션 1 | isql 세션 2 |
| T1 | <br>isql -user SYSDBA -pass masterkey localhost:c:\temp\testdeadlock.fdb<br>Database: localhost:c:\temp\testdeadlock.fdb, User: SYSDBA<br>SQL> set transaction wait;<br>Commit current transaction (y/n)?y<br>Committing.<br>SQL> select current_transaction from rdb$database;<br> CURRENT_TRANSACTION<br>=====================<br> 15<br>SQL> select * from t1;<br> I1<br>============<br> 2<br>SQL><br> |
|
| T2 | <br>isql -user SYSDBA -pass masterkey localhost:c:\temp\testdeadlock.fdb<br>Database: localhost:c:\temp\testdeadlock.fdb, User: SYSDBA<br>SQL> set transaction wait;<br>Commit current transaction (y/n)?y<br>Committing.<br>SQL> select current_transaction from rdb$database;<br> CURRENT_TRANSACTION<br>=====================<br> 20<br>SQL> select * from t1;<br> I1<br>============<br> 2<br>SQL><br> |
|
| 따라서 시작 시점에 #15와 #20이라는 두 개의 트랜잭션이 시작되었음을 알 수 있습니다. 그런 다음 동일한 레코드에 대한 동시 업데이트를 수행해 보겠습니다: | ||
| T3 | <br>SQL> UPDATE T1 SET i1=5 where i1=2;<br> |
|
| T4 | <br>SQL> UPDATE T1 SET i1=100 where i1=2;<br> |
|
| T5 | <br>SQL> commit;<br>SQL><br> |
<br>Statement failed, SQLSTATE = 40001<br>deadlock<br>-update conflicts with concurrent update<br>-concurrent transaction number is 15<br>SQL><br> |
세션 1에서 UPDATE를 시작했지만 커밋하지 않았습니다.
세션 2에서 UPDATE는 «중단»되었습니다. 동시 트랜잭션의 종료를 기다렸고, 세션 1에서 UPDATE를 커밋했을 때 세션 2는 오류를 받았습니다:
Statement failed, SQLSTATE = 40001
deadlock
-update conflicts with concurrent update
-concurrent transaction number is 15
SQL>
따라서 대기 트랜잭션에서 업데이트 충돌이 발생하는 일반적인 상황입니다.
추적 로그를 검토하여 업데이트 충돌의 증거를 찾아보겠습니다.
이 경우 추적 로그는 매우 짧지만 프로덕션 시스템의 경우 훨씬 길 수 있습니다. 그러나 데드락을 찾는 접근 방식은 동일합니다.
fbtracemgr.exe -se localhost:service_mgr -start -conf "C:\temp\fbtrace.conf" -user SYSDBA -pass masterkey
Trace session ID 4 started
2019-05-07T11:28:43.9850 (2692:00000000018C1940) EXECUTE_STATEMENT_FINISH
C:\TEMP\TESTDEADLOCK.FDB (ATT_12, SYSDBA:NONE, NONE, TCPv6:::1/52938)
C:\HQbird\Firebird30\isql.exe:8828
(TRA_15, CONCURRENCY | WAIT | READ_WRITE)
Statement 52:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=5 where i1=2
0 records fetched
0 ms, 2 read(s), 21 fetch(es), 3 mark(s)
2019-05-07T11:29:45.0190 (2692:00000000018C0040) FAILED EXECUTE_STATEMENT_FINISH
C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
C:\HQbird\Firebird30\isql.exe:7032
(TRA_20, CONCURRENCY | WAIT | READ_WRITE)
Statement 55:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=100 where i1=2
0 records fetched
40242 ms, 19 fetch(es), 2 mark(s)
2019-05-07T11:29:45.0190 (2692:00000000018C0040) ERROR AT JStatement::execute
C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
C:\HQbird\Firebird30\isql.exe:7032
335544336 : deadlock
335544451 : update conflicts with concurrent update
335544878 : concurrent transaction number is 15
보시다시피, 로그에는 성공한 업데이트와 실패한 업데이트 모두에 대한 레코드와 교착 상태에 대한 오류 메시지가 있습니다.
로그를 분석하는 방법을 살펴보겠습니다:
오류 메시지에서 트랜잭션 식별자를 찾아보겠습니다:
2019-05-07T11:29:45.0190 (2692:00000000018C0040) ERROR AT JStatement::execute
C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
C:\HQbird\Firebird30\isql.exe:7032
335544336 : deadlock
335544451 : update conflicts with concurrent update
335544878 : concurrent transaction number is 15
오류 레코드 근처(보통 바로 위)에서 실패한 명령문을 볼 수 있습니다:
2019-05-07T11:29:45.0190 (2692:00000000018C0040) FAILED EXECUTE_STATEMENT_FINISH
C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
C:\HQbird\Firebird30\isql.exe:7032
(TRA_20, CONCURRENCY | WAIT | READ_WRITE)
Statement 55:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=100 where i1=2
0 records fetched
40242 ms, 19 fetch(es), 2 mark(s)
실행 시간이 매우 길다는 점(~40초)에 주목하세요 - 이는 동시 트랜잭션의 결과를 기다린 시간을 나타냅니다.
마지막으로, 성공한 UPDATE 작업이 있는 트랜잭션 번호를 검색합니다. 이를 위해 TRA_NNN을 찾으세요. 여기서 NNN은 트랜잭션 번호입니다. 이 경우 TRA_15가 됩니다:
2019-05-07T11:28:43.9850 (2692:00000000018C1940) EXECUTE_STATEMENT_FINISH
C:\TEMP\TESTDEADLOCK.FDB (ATT_12, SYSDBA:NONE, NONE, TCPv6:::1/52938)
C:\HQbird\Firebird30\isql.exe:8828
(TRA_15, CONCURRENCY | WAIT | READ_WRITE)
Statement 52:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=5 where i1=2
0 records fetched
0 ms, 2 read(s), 21 fetch(es), 3 mark(s)
이것으로 업데이트 충돌의 양측 - 성공한 측과 실패한 측 - 을 모두 찾았습니다.
교착 상태를 식별하는 도구
보시다시피, 잠금 충돌/교착 상태의 근본 원인을 식별하는 것은 상당히 어려울 수 있습니다. 충돌에는 항상 양측이 있고, 한쪽이 “승리”(오류 메시지 없이 완료)하기 때문에 승자를 식별하기가 까다로울 수 있습니다. 예를 들어, 충돌은 EXECUTE STATEMENT 내부, 중첩 저장 프로시저 내부, 트리거 내부에서 발생할 수 있으며, 이러한 요소들의 조합일 수도 있습니다.
교착 상태를 식별하기 위해 IBSurgeon은 “Deadlock Analyzer” 도구를 개발했습니다. 이 도구는 추적 로그를 분석하고 교착 상태로 이어진 호출에 대한 세부 정보가 포함된 이해하기 쉬운 다이어그램을 생성합니다. 이 도구는 Enterprise Subscription의 일부로 제공됩니다(평가판 있음) - cc.ib-aid.com에서 등록하세요.
또한 HQbird는 변경 사항과 변경 시도(즉, 실패한 작업, 잠금 충돌 및 교착 상태)만 저장하도록 구성할 수 있는 fbtrace 플러그인의 확장 버전을 도입합니다.
자세한 내용은 IBSurgeon 지원팀에 문의하세요: [email protected].
교착 상태를 방지하는 방법은 무엇인가요?
이제 애플리케이션에서 교착 상태를 방지하기 위한 최상의 솔루션을 식별해야 합니다. 트랜잭션 매개변수를 변경하거나, 추가 오류 처리를 추가하고 작업을 반복하거나, 애플리케이션의 비즈니스 수준에서 추가 검사를 수행하는 것이 가능합니다.