|
SYSTEM 롤백 세그먼트는 r 값이 0이기 때문에 언두 헤더 블록의 클래스# = 2*0 + 15 = 15 가 되며, 언두 블록의 클래스# = 2*0 + 16 = 16 이 된다. 또한 롤백 세그먼트 번호에 따라 클래스#는 {17,18}, {19,20}, {21,22} … 의 순으로 증가한다.
gc cr/current request 대기 이벤트는 db file sequential read 이벤트와 거의 유사한 속성을 지닌다. db file sequential read 대기 이벤트가 디스크 레벨에서 싱글 블록 I/O 작업을 처리하는 과정에서 발생한다면, gc cr/current request 대기 이벤트는 인터커넥트 레벨에서 싱글 블록 I/O 작업을 처리하는 과정에서 발생한다는 차이가 있을 뿐이다. 따라서, 두 대기 이벤트는 발생 이유도 거의 유사하며, 대기 현상을 해소하는 기법 또한 아래 표와 같이 거의 유사하다.
이벤트 | gc cr/current request | db file sequential read |
발생 이유 | - 비효율적인 SQL에 의한 과다한 블록 요청 - 핫 블록 - 느린 인터커넥트 - 비효율적인 네트워크 설정 - 비효율적인 버퍼 캐시 - LMS 프로세스의 부하 | - 비효율적인 SQL에 의한 과다한 블록 요청 - 핫 블록(이 경우는 db file sequential read 이벤트보다는 buffer busy waits 이벤트에 가까움) - 느린 I/O 시스템 - 비효율적인 I/O 설정 - 비효율적인 버퍼 캐시 - I/O 프로세스의 부하(예: 비동기 I/O 프로세스) |
해소 방안 | - SQL 튜닝을 통해 블록 요청 회수를 줄임 - 핫 블록 해소 - 보다 높은 대역폭(Bandwidth)의 인터커넥트 사용 - 네트워크 설정(UDP 버퍼 크기 등)을 최적화 - 버퍼 캐시의 효율적 사용 - LMS 프로세스의 효율성 증가 | - SQL 튜닝을 통해 블록 요청 회수를 줄임 - 핫 블록 해소 - 보다 높은 처리량의 I/O(Disk) 시스템사용 - I/O 설정(디스크간 경합, 스트라이프 크기 등)을 최적화 - 버퍼 캐시의 효율적 사용 - I/O 프로세스의 효율성 증가 |
Parameter & Wait Time
Wait Parameters
gc cr/current request 대기이벤트의 대기 파라미터는 다음과 같다.
Check Point & Solution
비효율적인 SQL문장으로 인한 과다한 블록요청
비효율적인 SQL 문장, 즉 과다한 Logical I/O를 수행하는 SQL 문장이 gc cr/current request 이벤트 대기의 주범이다. 비효율적인 SQL 문장을 튜닝 하지 않은 채로, 다른 튜닝 방법을 적용하려는 시도는 대부분 실패한다는 것은 간과할 수 없는 사실이다. 비효율적인 SQL로 인한 문제는 그 원인이 단순한 만큼, 해결책 또한 단순하다. SQL 문장에행 경로를 따를 수 있도록 한다.
RAC에서는 병렬 실행(Parallel Execution)이 여러 노드 간에 분배된다. 대량의 작업을 여러 노드를 거쳐 분산 수행하는 것은 클러스터의 장점을 극대화하는 것으로, RAC의 존재 이유 중 하나라고 할 수 있다. 문제는 개별 병렬 작업의 결과가 인터커넥트를 통해 전송되는 과정에서, 인터커넥트 리소스의 부족을 초래하게 되고, OLTP 성의 일반 쿼리 성능에 영향을 줄 수 있다는 것이다. 하나의 시스템에서 DSS 성의 쿼리와 OLTP 성의 쿼리를 사용하는 것은 여러 가지 성능 문제를 유발할 수 있으며, RAC에서도 예외는 아니다. RAC에서 병렬 작업을 수행하는 경우에는 노드 간에 부하를 어떻게 조절할 지를 미리 고려할 필요가 있다. INSTANCE_GROUPS 파라미터와 PARALLEL_INSTANCE_GROUP 파라미터를 활용하면, 병렬 작업을 노드 간에 지능적으로 분배할 수 있다.
Hot Block
핫 블록은 여러 프로세스 혹은 여러 인스턴스에 의해 동시 다발적으로 접근 혹은 변경되는 블록을 말한다. 핫 블록은 단일 인스턴스 환경에서는 cache buffers chains 래치 경합이나 버퍼 락 경합을 유발하며, 성능 저하의 주범이 되곤 한다. RAC 환경에서의 핫 블록은 래치나 버퍼 락 경합뿐만 아니라, 과도한 인터커넥트 전송을 유발함으로써 RAC 시스템 전체의 성능을 저하시키는 요인이 된다.
핫 블록에 의한 성능 저하 현상은 핫 블록을 물리적으로 분산시킴으로써 해결 가능하다. 핫 블록을 분산시키는 방법에는 다음과 같은 것들이 있다.
동시 다발적인 DML 수행
여러 노드에서 동일 세그먼트에 대해 대량의 DML을 동시 다발적으로 수행하면 광범위한 버퍼 락 경합이 발생하고 이로 인해 gc cr/current request 이벤트의 대기 시간이 증가한다. 대량의 DML 작업은 노드 간에 적절히 작업을 분배하는 것이 좋다. 여러 노드 중 특정 하나의 노드를 배치 작업용 노드, 즉 대량의 DML을 수행하게끔 지정하는 것도 현실적인 방법이다.
느린 인터컨넥트
인터커넥트를 통해 주고 받는 데이터의 절대적인 양이 많다면, SQL 튜닝만으로는 문제를 해결할 수 없다. 가령 인터커넥트의 대역폭이 1G Bit(Gigabit Ethernet)라면 이론상 동시에 교환 가능한 데이터의 양은 125M 바이트가 된다. 따라서 125M 바이트 이상의 데이터를 인터커넥트에서 교환하게 되면, 블록 전송 과정에서 응답 시간이 지연되는 현상이 발생하게 된다. 인터커넥트를 통한 데이터 교환이 지나치게 많으면 LMS 프로세스의 부하가 증가하고, 응답 처리 과정에서 지연이 발생하게 된다.
인터커넥트에서의 혼잡에 의한 지연 현상은, 마치 고속도로에서의 차량이 지나치게 몰려 혼잡이 발생하는 것과 동일한 원리에서 발생한다. 도로가 좁아서, 즉 인터커넥트의 대역폭이 낮아서 발생하는 혼잡 현상이라면, 도로를 넓혀서, 즉 인터커넥트의 대역폭을 높이는 방법을 통해 해결 가능하다. 하지만 도로를 넓히는 것이 엄청난 비용을 요구하듯이, 인터커넥트의 대역폭을 높이는 것 또한 추가적인 비용을 요구한다는 사실을 명심해야 한다. 언제나 최선의 선택은 SQL 문을 최적으로 튜닝하고, 불필요한 경합에 의한 블록 전송을 최소화하는 것이 되어야 한다.
현재 인터커넥트로 가장 많이 사용되는 것이 Gigabit Ethernet이며, 최근에 와서는 10 Gigabit Ethernet이 채택되고 있다. 따라서 대역폭을 넓히기 위한 선택의 폭이 넓지 않다는 것 또한 유념할 필요가 있다.
비효율적인 네트워크 설정
인터커넥트가 사용하는 네트워크 프로토콜에 따라 적절한 네트워크 설정이 필요하다. Gigabit Ethernet 인터커넥트에서는 UDP(User Datagram Protocol)가 사용된다. oradebug 툴을 사용하면 인터커넥트에서 어떤 프로토콜을 사용하는지 확인할 수 있다.
SQL> oradebug setmypid
SQL> oradebug ipc
SQL> oradebug tracefile_name
/oracle/ORA10g/admin/LAS10/udump/ora102_ora_7040.trc
SSKGXPT 0xc03f844 flags SSKGXPT_READPENDING info for network 0
socket no 8 IP 192.168.2.202 UDP 41210
sflags SSKGXPT_UP
네트워크 설정 중 RAC의 인터커넥트 성능과 가장 연관이 깊은 것은 UDP 버퍼의 크기이다. OS에 따라 UDP 버퍼의 크기의 권장치는 조금씩 다를 수 있다. 오라클이 공식적으로 권고하는 UDP 버퍼의 크기는 256K 바이트이다. 대부분의 시스템에서는 256K 바이트의 크기로 적절한 성능을 보장받을 수 있다. 성능 테스트를 위한 BMT 환경과 같은 상황이나, 동시에 인터커넥트를 사용하는 세션수가 대단히 많은 시스템이라면 더 큰 크기의 UDP 버퍼를 사용할 수 있다. 보통 최대 1M ~ 2M 바이트 정도 크기의 UDP 버퍼를 사용하기도 한다. OS 별로 UDP 버퍼 크기를 조회하고 설정하는 방법은 “5.3 네트워크 모니터링”에서 자세하게 논의할 것이다.
UDP 버퍼 크기가 지나치게 작은 경우에는 패킷 유실(Packet Loss) 현상이 빈번하게 발생한다. UDP 패킷 유실이 발생할 경우, 오라클에서는 Fixed-up 이벤트인 gc cr block lost 이벤트나 gc current block lost 이벤트로 관찰된다. 만일 이 이벤트에 대한 대기 현상이 자주 목격된다면, 네트워크 모니터링을 통해 패킷 유실이 지나치게 자주 발생하지 않는지 모니터링 할 필요가 있다. 다양한 툴들을 통해 패킷 유실 현상을 관찰할 수 있는데, 리눅스 환경에서 netstat 툴을 이용하는 예는 다음과 같다.
-- netstat -i 옵션. 네트워크 인터페이스 별로 패킷 유실(RX-ERR)을 관찰할 수 있다.
prompt> netstat -i
Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP ...
eth0 1500 0 8664 0 0 0 7708 0 0 ... eth1 1500 0 8772 0 0 199 3541 0 0 ...
lo 16436 0 2262 0 0 0 2262 0 0 ...
-- nestat -s 옵션. 네트워크 관련 통계치를 통해 패킷 유실(packet receive errors)을 조회할 수 있다.
prompt> netstat -su
Udp:
495820735 packets received
50018 packets to unknown port received.
114 packet receive errors
584466009 packets sent
gc cr block lost 이벤트나 gc current block lost 이벤트에 대한 대기 현상이 UDP 패킷 유실과 같이 발생하는 경우에는, UDP 버퍼 크기가 지나치게 작게 설정되어 있지 않은지 점검해보아야 한다. UDP 버퍼의 크기를 점진적으로 최대 2M 바이트 정도까지 키우면서 문제가 해결되는지 확인한다. 만일 UDP 버퍼의 크기가 이미 충분히 큰데도 같은 문제가 계속 재현된다면 물리적인 네트워크 설정에 문제가 없는지 확인할 필요가 있다.
인터커넥트의 성능을 향상시키는데 유용한 또 하나의 네트워크 설정은 Jumbo Frame을 사용하는 것이다. Jumbo Frame이란 NIC(Network Interface Card) 설정 시 1500 바이트 크기 이상의 프레임 크기(또는 MTU. Maximum Transmission Unit)를 사용하는 것을 말한다. MTU가 1500 바이트라는 말의 의미는 하드웨어적으로 한번에 네트워크를 통해 전송할 수 있는 패킷의 최대 크기가 1500 바이트라는 의미이다. 일반적인 네트워크 환경에서는 대부분 기본값인 1500 바이트의 MTU로 충분하다. 하지만 인터커넥트와 같이 대량의 데이터가 대단히 빈번하게 교환되는 경우에는 더 큰 크기의 MTU를 사용함으로써 성능을 더욱 향상시킬 수 있다. MTU 크기에 이론적인 제한은 없지만, 일반적으로 9000 바이트 크기의 MTU를 사용한다. 현재 사용 중인 MTU의 크기는 netstat 툴이나 ifconfig 툴을 통해 확인할 수 있다.
Prompt> netstat -i
Kernel Interface table
Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0 1500 0 88277330 0 0 0 79745731 0 0 0 BMRU
eth1 1500 0 1385955750 0 0 199 3879777348 0 0 0 BMRU
lo 16436 0 30702314 0 0 0 30702314 0 0 0 LRU
Prompt> ifconfig eth0
eth0 Link encap:Ethernet HWaddr 00:11:11:EB:BE:7E
inet addr:210.122.227.204 Bcast:210.122.227.255 Mask:255.255.255.0
inet6 addr: fe80::211:11ff:feeb:be7e/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:88277786 errors:0 dropped:0 overruns:0 frame:0
TX packets:79746070 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:3981177070 (3.7 GiB) TX bytes:2529189640 (2.3 GiB)
Jumbo Frame을 사용하려면 OS, 네트워크 카드(NIC), 스위치 등 관련된 모든 소프트웨어와 하드웨어가 이를 지원해야 한다.
비효율적인 buffer cache 사용
버퍼 캐시를 효율적으로 사용하는 것은 싱글 인스턴스 환경에서뿐만 아니라 RAC 환경에서도 대단히 중요하다. 버퍼 캐시가 비효율적으로 사용되는 경우에는 애써 로컬 캐시로 불러들인 블록이 주기적으로 버퍼 캐시에서 밀려 나게 된다. 이러한 블록을 다시 읽어 들이는 과정에서 다시 글로벌 캐시 동기화가 발생하게 되고, gc cr/current request 이벤트에 대한 대기가 불필요하게 증가한다.
버퍼 캐시를 효율적으로 사용하는 방법은 어떤 원칙이 있다기 보다는 어플리케이션의 데이터 액세스 유형에 따라 적절한 기법을 창조적으로 응용해서 적용한다고 보는 것이 좋을 것이다. 일반적으로 적용 가능한 기법들은 다음과 같다.
다중 버퍼 풀의 사용: 세그먼트의 용도에 따라 Keep 버퍼, Default 버퍼, Recycle 버퍼를 적절히 사용함으로써 버퍼 캐시를 효율적으로 사용할 수 있다.
전역 임시 테이블의 사용: 데이터가 임시로 필요한 경우 가능한 전역 임시 테이블(Global Temporary Table)을 사용한다. 전역 임시 테이블은 세션 레벨에서 데이터를 관리하기 때문에 글로벌 캐시 동기화 작업이 불필요하다.
읽기 전용 테이블스페이스의 사용: 만일 특정 테이블스페이스가 읽기 전용의 성격을 가지고 있다면 테이블스페이스의 속성 자체를 읽기 전용(Read only)으로 변경하는 것이 바람직하다. 오라클은 읽기 전용 테이블스페이스에 속한 데이터 블록은 글로벌 캐시 동기화를 수행하지 않는다.
충분한 크기의 버퍼 캐시 크기: 마지막으로 가능하다면 항상 충분한 크기의 버퍼 캐시를 사용하도록 한다. 버퍼 캐시의 크기가 작아서 버퍼 캐시로부터 자주 쓰이는 데이터 블록이 내려 가는 일이 발생하지 않도록 할 필요가 있다.
LMS 프로세스의 부하
요청 노드의 블록 전송 요청은 항상 리모트 노드의 LMS 프로세스에 의해 처리된다. 비동기 I/O 프로세스가 디스크 I/O 작업을 처리하는 반면, LMS 프로세스는 인터커넥트 I/O 작업을 처리한다고 볼 수 있다. 비동기 I/O 작업을 위해 비동기 I/O 프로세스를 사용하는 시스템의 경우에는, 비동기 I/O 프로세스가 사용할 수 있는 자원을 최대한 보장해줌으로써 I/O 경합을 어느 정도 개선시킬 수 있다. 마찬가지로 LMS 프로세스가 사용하는 자원을 최대한 보장해줌으로써 인터커넥트에서의 지연(Delay)을 개선시킬 수 있다. LMS 프로세스가 보다 원활하게 작업을 처리하면 그 만큼 gc cr request 이벤트에 대한 대기 현상도 개선된다. LMS 프로세스의 작업은 대부분 적절한 CPU 리소스를 필요로 한다. 따라서 LMS 프로세스가 CPU 자원을 효율적으로 사용할 수 있도록 보장하는 것이 필요하다.
CPU 리소스가 충분한 경우에는, LMS 프로세스의 개수를 증가시킴으로써 개별 LMS 프로세스의 성능을 개선시킬 수 있다. 오라클 10g부터는 GCS_SERVER_PROCESSES 파라미터의 값을 이용해 LMS 프로세스의 개수를 변경한다. 오라클의 권고치는 4개의 CPU 당 하나의 LMS 프로세스를 할당하는 것이며, 최소값은 2개이다. 글로벌 캐시 동기화 작업이 극단적으로 많은 시스템이라면 1~2개의 CPU 당 하나의 LMS 프로세스를 할당하는 것을 고려해 볼만 하다.
LMS 프로세스의 성능을 극대화하기 위해 OS 차원에서 LMS 프로세스가 보다 많은 CPU 자원을 사용할 수 있도록 보장하는 방법을 사용할 수 있다. 가장 쉬운 방법은 renice 명령을 이용해서 LMS 프로세스의 우선순위(Priority)가 높아지도록 조정하는 것이고, 또 하나의 방법은 LMS 프로세스의 우선순위 스케쥴링 방식을 실시간(Real-Time)으로 변경하는 것이다.
이 문제를 이해하려면, OS가 개별 프로세스에게 CPU를 할당하는 방식에 대한 이해가 필요하다. OS는 기본적으로 시로세스에게 CPU 자원을 할당한다. 시분할 방법을 사용하는 경우, OS는 CPU를 사용하고자 하는 프로세스의 우선순위를 참조하여 우선순위가 가장 높은(실제로는 Priority 속성값이 낮을수록 우선순위는 높음) 프로세스를 먼저 실행시킨다. 일정 시간 CPU를 사용한 프로세스는 다시 대기 상태로 들어가고, 다음 번 프로세스가 다시 CPU를 할당 받는다. 프로세스의 우선순위는 프로세스의 CPU 사용 정도와 CPU 사용 시간 등을 고려하여 계산된다. 만일 CPU를 사용하는 정도가 비슷하다면, 오랜 시간 동안 CPU를 사용하는 프로세스의 우선순위가 낮아진다. 시분할 방법은 CPU 자원을 수많은 프로세스가 공평하게 나누어 쓴다는 장점이 있지만, 거꾸로 특정 프로세스가 CPU 자원을 최대한 사용하는 것을 방해하는 부작용을 가지고 있다. 인터커넥트를 통한 데이터 교환이 매우 빈번한 상황에서는 전반적인 CPU 사용률이 증가한다. 이때 시분할 기법의 영향으로, 가장 우선적으로 CPU를 할당 받아야 할 LMS 프로세스가 자주 인터럽트를 당하며, 점점 우선순위가 낮아지게 된다. 결과적으로 LMS 프로세스가 필요한 만큼 CPU를 할당 받지 못하는 현상이 발생하게 된다.
이 문제를 해결하는 한 가지 방법은, renice 명령을 이용해 LMS 프로세스의 우선순위가 항상 높은 상태를 유지하도록 하는 것이다. 대부분의 OS에서는 NICE 속성을 이용해 프로세스의 우선순위를 조정할 수 있다. 개별 프로세스는 우선순위를 나타내는 Priority 속성과 함께 NICE 속성을 제공한다. NICE 속성은 말 그대로, 특정 프로세스가 다른 프로세스에게 얼마나 “친절한 프로세스인가”, 즉 CPU를 다른 프로세스에 양보하는 친절한 일을 어느 정도 하는지를 결정한다. NICE 값이 높으면, 즉 더 친절하면 CPU를 다른 프로세스에게 더 많이 양보할 수 있도록 우선순위가 낮아진다. 반대로 NICE 값이 낮으면, 즉 더 불친절하면 CPU를 되도록이면 다른 프로세스에게 양보하지 않는다. NICE 값은 +19(가장 친절) ~ -20 (가장 불친절) 사이의 값을 지니며, 오라클과 같은 일반적인 유저 프로세스는 “0”의 값을 지닌다. 아래와 같이 LMS 프로세스의 우선순위와 NICE 값을 확인할 수 있다(Priority 속성값이 낮을수록 우선순위가 높다는 것을 명심하자).
[root@rac02 ~]# ps -efl | grep lms
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S oracle 7991 1 1 65 0 - 428439 - Sep11 05:26:48 ora_lms0_ORA102
0 S oracle 7993 1 1 75 0 - 428439 - Sep11 05:23:51 ora_lms1_ORA102
인터커넥트를 통한 데이터 교환이 매우 빈번하게 발생하고 이로 인해 CPU 경쟁이 심한 시스템이라면 다음과 같이 renice 명령을 이용해 LMS 프로세스의 NICE 값을 최대 -20까지 낮추는 것이 좋다. 아래 명령은 반드시 root 유저로 실행해야 한다.
[root@rac02 ~]# renice -20 -p 7991 7993 # 7991, 7993 프로세스의 NICE 값을 -20으로 변경
7991: old priority 0, new priority -20
7993: old priority 0, new priority -20
[root@rac02 ~]# ps -efl | grep lms
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S oracle 7991 1 1 60 -20 - 428439 - Sep11 05:26:51 ora_lms0_ORA102
0 S oracle 7993 1 1 60 -20 - 428439 - Sep11 05:23:54 ora_lms1_ORA102
시분할 기법에 의한 LMS 성능 저하 현상을 해결하는 또 하나의 방법은 실시간 스케쥴링 기법을 사용하는 것이다. 실시간 스케쥴링 기법의 엄격한 의미는 특정 프로세스가 특정 작업을 수행하는데 항상 “일정한” 응답 시간을 보장받는 것으로, 실시간 OS(Real-Time OS)에서 대단히 중요한 개념이다. 실시간 OS가 아닌 대부분의 OS에서는 완벽하게 일정한 응답 시간을 보장하지 못한다. 하지만 가능한 실시간의 스케쥴링이 가능하도록 추가적인 스케쥴링 기법을 제공하고 있다. 일반적으로 실시간 스케쥴링을 사용하는 프로세스는 고정된 우선순위를 부여 받으며, 시분할 기법에 비해 높은 우선순위를 부여 받는다. 높은 우선순위에 있고, 그 우선순위가 변경되지 않기 때문에 CPU를 최대한 사용할 수 있다. 따라서 LMS 프로세스들이 실시간 기법을 사용하도록 변경하는 것을 검토해 볼 필요가 있다. 오라클 10g R2 이전 버전까지는 OS 별로 고유의 기법을 사용해 LMS 프로세스가 실시간 스케쥴링을 사용하도록 해야 한다. 오라클 10g R2부터는 LMS 프로세스는 기본적으로 실시간 스케쥴링을 사용하도록 변경되었다. 이 메커니즘은 _OS_SCHED_HIGH_PRIORITY 파라미터(기본값은 1)에 의해 제어된다. 이 파라미터의 값이 “0”이면, 이전 버전과 마찬가지로 시분할 스케쥴링을 사용한다. 반면 “1” 이상의 값을 부여하면 실시간 스케쥴링 기법을 사용하며, 높은 값을 부여할수록 더 높은 우선순위를 부여 받는다. 하지만 1 보다 큰 값을 지정할 경우에는 LMS 프로세스가 CPU 자원을 지나치게 많이 차지할 수 있다는 사실에 유념해야 한다.
LGWR/DBWR 프로세스의 부하
LGWR 프로세스와 DBWR 프로세스는 싱글 인스턴스 환경에서뿐만 아니라, RAC 환경에서도 매우 중요한 일을 담당한다.
LGWR 프로세스는 블록 전송 과정에서 “리두 플러시”를 담당한다. 로컬 캐시의 더티 블록을 리모트 노드로 전송하기 전에 더티 블록에 해당하는 리두 데이터를 리두 로그로 저장해야 한다. 이것을 흔히 “리두 플러시”라고 부른다. 만일 LGWR 프로세스의 작업이 원활하지 못하면 리두 플러시가 지연되고 이로 인해 gc cr/current request 이벤트의 대기 시간이 증가하게 된다.
DBWR 프로세스는 “Fusion Write” 기능을 담당한다. 로컬 캐시의 더티 블록을 디스크에 기록(Checkpoint)하는 작업은 반드시 글로벌하게 이루어진다. 클러스터 내에 같은 블록에 대한 여러 개의 더티 블록이 존재할 수 있기 때문이다. 특정 인스턴스에서 더티 블록에 대한 기록 요청이 발생하면, GRD를 참조하여 블록의 마스터 노드로 요청이 전달되고, 마스터 노드는 블록의 “최신” 버전을 가진 노드에 디스크에 기록 요청을 전달한다. 디스크 기록이 완료되면 다른 인스턴스의 PI(과거 이미지) 블록들은 캐시에서 플러시 된다. 이러한 일련의 메커니즘을 Fusion Write라고 부른다. DBWR 프로세스의 작업이 원활하지 못하면, Fusion Write 작업 시간이 지연된다. DBWR 프로세스에 의해 기록 중인 블록을 전송 받고자 하는 프로세스는 기록 작업이 끝날 때까지 대기해야 하기 때문에 gc cr/current block request 이벤트 대기 시간이 증가하게 된다.
리두 플러시와 Fusion Write의 성능 문제는 V$CURRENT_BLOCK_SERVER 뷰를 통해서도 간접적으로 확인할 수 있다.
만일 리두 플러시와 Fusion Write의 성능 저하로 인해 gc cr/current request 이벤트 대기가 증가한다고 판단되면 LGWR 프로세R>
Event Tip
PlaceHolder 이벤트와 Fixed-up 이벤트
오라클은 RAC와 관련된 기능을 끊임없이 개선시키고 있다. RAC와 관련된 대기 이벤트의 정의 또한 예외가 아니다. 오라클 9i와 오라클 10g의 클러스터 관련 대기 이벤트들은 이름부터가 새로 정의되었으며, 무엇보다 특정 이벤트들이 서로 간에 종속적인 관계를 지니게 되었다. 즉, 특정 이벤트들은 “Placeholder”(자리점유자)의 역할을 하고, 또 다른 이벤트들은 “Fixed-up”(사후 조정)의 역할을 한다.
오라클 10g의 클러스터 대기 이벤트들은 Placeholder 이벤트와 Fixed-up 이벤트라는 두 개의 카테고리로 분류된다. Placeholder 이벤트란 특정 프로세스가 글로벌 자원, 즉 데이터 블록을 “획득하는” 과정 동안 대기하는 이벤트를 말한다. 반면에 Fixed-up 이벤트란 특정 프로세스가 데이터 블록을 최종 “획득한” 시점에 대기한 것으로 기록되는 이벤트를 말한다. “획득하는”과 “획득한”의 시점 차이가 Placeholder 이벤트와 Fixed-up 이벤트의 역할을 결정하는 것에 주목해야 한다. 가령, 일관된 읽기(CR) 모드로 특정 블록을 리모트 노드에 요청한 프로세스는, 해당 블록을 전송 받을 때까지 gc cr request 이벤트(Placeholder)를 대기한다. 요청의 결과로 실제 블록 이미지를 전송 받았다면, 결과적으로는 gc cr block 2-way 이벤트(Fixed-up)를 대기했던 것으로 최종 보고를 하게 된다. 즉 Placeholder 이벤트는, 프로세스가 어떤 결과를 받았느냐에 따라 특정 Fixed-up 이벤트로 바뀌게 된다.
Placeholder 이벤트와 Fixed-up 이벤트 체계에서 또 하나의 주의할 점은 모니터링 방법이다. Placeholder 이벤트는 “획득하는” 동안 관찰되기 때문에, 현재의 대기 정보를 제공하는 V$SESSION_WAIT 뷰에서 관찰된다. 또한 아직 Fixed-up 이벤트로 변경되지 않았다면 V$SESSION_EVENT 뷰에서도 관찰된다. 반면[Fixed-up 이벤트는 “획득한” 후에 관찰되기 때문에 V$SESSION_WAIT 뷰에서는 관찰되지 않고, 오로지 V$SESSION_EVENT 뷰나 V$SYSTEM_EVENT 뷰에서만 관찰된다. 따라서, 시스템 모니터링을 위해서 V$SYSTEM_EVENT 뷰를 조회하고 있다면, gc cr request 이벤트와 같은 Placeholder 이벤트가 아닌 gc cr block 2-way 이벤트와 같은 Fixed-up 이벤트들만이 관찰된다는 점을 이해해야 한다.
Placeholder/Fixed-up 이벤트 체계는 클러스터 클래스의 대기 이벤트에서만 사용되며, 오라클 10g 이전 버전에서는 전혀 사용되지 않는 개념이다. Placeholder/Fixed-up 이벤트의 체계를 따르지 않은 이벤트들은 기존의 대기 이벤트와 동일한 방식으로 동작한다.
오라클이 제공하는 RAC 관리자 가이드에서는 클러스터 이벤트들을 그 속성에 따라 다음과 같이 분류하고 있다.
위의 이벤트 분류는 이벤트의 발생 이유에 근거를 둔 것으로, 각 이벤트의 의미를 개괄적으로 파악하는 목적으로 사용될 수 있다. Block-oriented 이벤트들은 실제 블록 이미지를 인터커넥트를 통해 교환했음을 의미한다. Message-oriented 이벤트들은 블록 이미지가 아닌, 블록을 읽을 권한만을 부여했음을 의미한다.
Contention-oriented 이벤트들은 블록을 전송 받는 과정에서 경합(Contention)이 발생했음을 의미한다. Load-oriented 이벤트들은 인터커넥트에서 과도한 혼잡(Congestion)이 발생해서 전송 작업이 지연되었음을 의미한다. 하지만, 이 분류법으로는 Placeholder/Fixed-up 이벤트의 정확한 구분을 설명할 수 없다. 가령 위의 이벤트 목록에서 gc buffer busy 이벤트는 Fixed-up 이벤트가 아닌, 일반 독립 이벤트이다. 또한 gc cr request 이벤트나 gc current request 이벤트와 같은 Placeholder 이벤트는 아예 목록에서 빠져 있다. 이 문서에는 위의 분류법과 Placeholder/Fixed-up 이벤트 체계의 특징을 모두 살린 아래와 같은 분류 체계를 사용한다.
오라클 10g RAC에는 크게 2개의 Placeholder 이벤트가 존재한다. gc cr request 이벤트와 gc current request 이벤트가 그것이다. 이 두 개의 대기 이벤트는 각각 “일관된 읽기 모드(이하 CR)의 I/O”와 “현재 모드(이하 Current)의 I/O” 작업에서 발생하는 대기 현상을 나타내는 것으로, RAC에서 가장 보편적으로 발생한다. gc cr request 이벤트와 gc current request 이벤트는 각각 많은 수의 Fixed-up 이벤트를 거느린다. 반면에 gc cr/current multi block request 이벤트와하지 않는 일반적인 대기 이벤트들이다.
Placeholder/Fixed-up 이벤트 체계에서 한 가지 유의할 점은 Fixed-up 이벤트는 Placeholder 이벤트가 무엇이었느냐에 무관하다는 것이다. Fixed-up 이벤트는 요청의 종류와 무관하게 어떤 “결과”를 받았느냐에 대한 정보만을 제공한다. 따라서 gc cr request 이벤트에 대한 Fixed-up 이벤트로 gc cr block 2-way 이벤트뿐만 아니라, gc current block 2-way 이벤트도 사용되며, Fixed-up 이벤트로 기술된 모든 이벤트가 사용될 수 있다는 사실을 유의해야 한다.
gc cr/current request의 fixed-up 이벤트
gc cr/current request 이벤트는 Placeholder 이벤트이며, 블록 요청의 결과로 어떤 종류의 응답을 받았느냐에 따라 다음과 같이 여러 종류의 Fixed-up 이벤트로 변경된다.
응답 유형 | Fixed-up 이벤트 |
마스터 노드로부터 블록 이미지를 전송 받은 경우(즉, 마스터 노드가 홀더 노드인 경우) | gc cr block 2-waygc current block 2-way |
마스터 노드가 아닌 제 3의 홀더 노드로부터 블록 이미지를 전송 받은 경우 | gc cr block 3-waygc current block 3-way |
마스터 노드로부터 블록을 읽을 권한을 부여 받은 경우 | gc cr grant 2-waygc current grant 2-way |
블록 이미지를 전송 받는 과정에서 경합이 발생한 경우 | gc cr block busygc current block busy |
블록을 읽을 권한을 부여 받는 과정에서 경합이 발생한 경우 | gc cr grant busygc current grant busy |
블록 이미지를 전송 받는 과정에서 혼잡이 발생한 경우 | gc cr block congestedgc current block congested |
블록을 읽을 권한을 부여 받는 과정에서 혼잡이 발생한 경우 | gc cr grant congestedgc current grant congested |
블록 전송 요청에 대한 응답이 유실(Lost)된 경우(10g R2) | gc cr block lostgc current block lost |
이들 Fixed-up 이벤트들은 대기 현상의 발생 원인을 좀 더 세밀하게 분석할 수 있도록 도와준다. 같은 gc cr request 이벤트라고 하더라도 Fixed-up 이벤트가 gc cr block 2-way 이벤트인지, gc cr block busy 이벤트인지에 따라, 대기 현상이 발생하는 원인과 해결책이 전혀 다를 수 있기 때문이다.
병렬 실행(Parallel Execution, PX)
오라클 10g부터 병렬 실행(Parallel Execution. 이하 PX) 엔진에 대해 중요한 변화들이 시도되었다. 기존의 PX 엔진이 가지고 있던 장점을 보완하고, 그리드 환경에 최적화된 성능을 보장하기 위해서 PSC(Parallel Single Cursor) 모델이라는 이름의 새로운 아키텍처를 도입했다. PSC란 말 그대로 병렬 실행 코디네이터(Parallel Execution Coordinator. 이하 PEC)와 병렬 실행 슬레이브(Parallel Execution Slave. 이하 PES)가 하나의 커서(또는 플랜)을 공유하겠다는 의미이다. 아래 예제는 오라클 9i와 오라클 10g에서 같은 종류의 병렬 작업을 수행한 경우 각 프로세스들이 실행하는 SQL 문장을 캡쳐한 것으로, PSC를 사용함으로써 생긴 변화를 눈으로 확인할 수 있다.
-- 오라클 9i에서는 PEC와 PES가 전혀 다른 커서를 사용하는 것을 확인할 수 있다.
오라클 9i:
PEC: PX 작업 수행
SQL> SELECT /*+ PARALLEL(A 20) */ COUNT( * ) FROM big_table a;
PES(s): 아래와 같이 변형된 SQL문이 PES에 의해 수행
SQL> SELECT /*+ PIV_SSF */
sys_op_msr( COUNT( * ) )
FROM (
SELECT /*+ NO_EXPAND ROWID(A2) */
0
FROM "MAXGAUGE"."BIG_TABLE" px_granule( 0 , block_range , dynamic ) a2
) a1;
-- 오라클 10g에서는 PEC와 PES가 동일한 커서를 사용하는 것을 확인할 수 있다.
오라클 10g
PEC: PX 작업 수행
SQL> SELECT /*+ PARALLEL(A 20) */ COUNT( * ) FROM big_table a;
PES(s): PEC가 수행한 SQL문과 동일한 형태의 SQL문이 수행
SQL> SELECT /*+ PARALLEL(A 20) */ COUNT( * ) FROM big_table a;
오라클 10g 이전 버전에서는 PEC가 수행한 PX 작업을 PES에게 분배하기 위해, 특별한 형태의 SQL 문장을 PES가 실행하도록 하는 방식을 사용했다. 위의 예제에서 확인할 수 있는 암호문과 같은 SQL 문장이 그것으로, 각 PES들이 이 특별한 형태의 SQL 문장을 통해 각각 영역을 나누어 작업을 수행하게 된다. 이러한 처리 방법은 병렬 작업을 SQL 문장으로 변환하는 복잡한 과정을 거치게 되는 단점을 내포하고 있다.
이러한 문제점을 극복하기 위해 오라클 10g에서는 PEC가 생성한 커서(또는 플랜)를 모든 PES들이 공유하는 모델을 사용한다. 이로 인해 복잡한 변환 과정이 불필요해졌으며, PX와 관련된 모든 프로세스들이 동일한 SQL 문을 사용하기 때문에 모니터링 및 관리가 용이해졌다. PSC 모델이 채택한 방법의 핵심은 실행 계획은 공유하되, 각 PES들이 실행 계획의 특정 부분만을 실행하도록 PEC가 추가적인 메시지를 통해 제어하는 것이다. 아래 예제에 이러한 개념이 간략하게 표현되어 있다.
-- 두 개의 테이블을 HASH JOIN으로 PX로 수행한다.
SQL> SELECT /*+ USE_HASH(A B) PARALLEL(A) PARALLEL(B)*/ COUNT(*)
FROM BIG_OBJECT A, BIG_OBJECT B;
-- 실행 계획은 아래와 같다.
SELECT STATEMENT ALL_ROWS-Cost : 40564
SORT AGGREGATE
PX COORDINATOR
PX SEND QC (RANDOM)
SORT AGGREGATE
HASH JOIN
PX RECEIVE
PX SEND HASH
PX BLOCK ITERATOR
TABLE ACCESS FULL BIG_OBJECT(1)
PX RECEIVE
PX SEND HASH
PX BLOCK ITERATOR
TABLE ACCESS FULL BIG_OBJECT(2)
-- 위의 암호와도 같은 실행 계획을 그림으로 표현하면 아래 그림과 같다. PEC와 PES는 같은 실행 계획을 공유하며, PEC가 각 PES들에게 실행 계획 중 어떤 부분을 담당할 것인지 제어한다.
싱글 인스턴스 환경에서는 PEC가 생성한 커서를 PES가 자연스럽게 공유할 수 있지만, RAC와 같은 멀티 인스턴스 환경에서는 PEC는 다른 인스턴스의 특정 PES에게 SQL 텍스트를 전송하고, 이 PES가 생성한 커서를 다른 PES들이 공유하는 방식으로 PSC 모델을 구현한다.
RAC와 같은 그리드 환경은 병렬 작업에 있어서 또 다른 장점을 제공한다. 높은 CPU 파워를 가진 싱글 인스턴스 환경에서도 병렬 작업의 장점을 극대화할 수 있지만, RAC 시스템에서는 여러 인스턴스간에 병렬 작업을 분할할 수 있으므로 진정한 의미에서의 병렬 수행이 가능해진다. 하지만 인스턴스간에 병렬 작업을 분할해서 사용하는 것이 항상 유리한 것은 아니라는 점이 중요하다. 같은 인스턴스내에서 병렬 프로세스간의 통신은 공유 메모리(Shared Memory)를 통해서 이루어진다. 이에 반해 인스턴스간의 병렬 프로세스간의 통신은 인터커넥트(Interconnect)를 통한 IPC를 통해서 이루어진다. 비록 1Gbps나 10Gbps 급의 고속 인터커넥트가 보편화되고 있지만, 같은 머신 내의 공유 메모리를 통한 데이터 교환에 비해서는 역시 비효율적이다. 따라서 과도한 노드 간의 병렬화는 인터커넥트의 부하를 유발해서 오히려 성능을 저하시킬 수 있다.
같은 인스턴스내에서 이루어지는 병렬 작업을 인스턴스내 병렬화(Intra-Instance Parallelism)라고 부르며, 여러 인스턴스 사이에서 이루어지는 병렬 작업을 인스턴스간 병렬화(Inter-Instance Parallelism)라고 부른다. 이 각각의 방법은 서로 장단점을 가지고 있기 때문에 각각의 특징을 고려하여 사용해야 한다. 오라클의 기본적인 정책은 인스턴스내 병렬화를 먼저 시도하고, 하나의 인스턴스에서 처리하기 힘든 작업의 경우에는 인스턴스간 병렬화를 시도하는 것이다. 즉, 사용자가 요청한 병렬 작업이 하나의 인스턴스가 보유한 리소스 파워로 처리 가능한 경우에는 오라클은 가능한 인스턴스내 병렬 작업을 시도한다. 하지만 여러 인스턴스가 일을 나누어 수행하는 것이 유리하다고 판단되는 경우에는 클러스터내의 다른 인스턴스들에게도 작업을 분배한다. 오라클이 기본적으로 합리적인 판단을 하기 때문에 대부분의 경우 이것을 그대로 따르는 것이 좋다.
하지만, 어떤 상황에서는 특정 인스턴스나 인스턴스 그룹에 특정 병렬 작업을 수행하게끔 할당해야 할 경우가 있을 것이다. 가령 다섯 개의 노드로 이루어진 RAC 시스템에서 2개의 노드는 배치 작업만을 위해 사용하고, 나머지 세 개의 노드는 온라인 작업 만을 위해 사용한다고 가정해 보자. 대용량 데이터 처리를 위한 병렬 작업은 두 개의 노드에서만 실행하게끔 제어할 수 있는 방법이 없다면 클러스터를 관리하는 것은 대단히 힘든 일이 된다. 다행히 오라클은 사용자가 직접 인스턴스내 병렬화와 인스턴스간 병렬화를 제어할 수 있는 지능적인 방법을 제공한다. INSTANCE_GROUPS와 PARALLEL_INSTANCCE_GROUP 두 개의 파라미터를 이용하면 이러한 목적을 달성할 수 있다. INSTANCE_GROUPS 파라미터는 특정 인스턴스를 특정 그룹에 지정하는 역할을 한다. PARALLEL_INSTANCE_GROUPS 파라미터는 병렬 작업 수행 시 어떤 인스턴스 그룹에 PES를 배정할 지를 결정하는 역할을 한다. 이 두 파라미터가 가지는 값은 모두 논리적인 값이라는 사실에 주의해야 한다. 파라미터 이름으로 인해 이 파라미터의 값들이 실제 인스턴스 명을 사용해야 하는 것으로 착각하는 사례가 많다. 아래에 사용 예제가 있다.
-- 인스턴스 1번. 인스턴스 1번은 서울, 부산 작업에 모두 사용한다.
SQL> ALTER SYSTEM SET INSTANCE_GROUPS = SEOUL, BUSAN SCOPE=SPFILE;
-- 인스턴스 2번. 인스턴스 2번은 부산 작업에만 사용한다.
SQL> ALTER SYSTEM SET INSTANCE_GROUPS = BUSAN SCOPE=SPFILE;
-- 두 인스턴스를 모두 재 기동한다.
-- 서울과 관련된 작업을 실행하고자 할 때는 “SEOUL” 그룹을 지정한다. 아래와 같이 작업을 수행하면 어떤 인스턴스에 작업을 수행하든, PES들은 항상 인스턴스 1번에서만 수행된다.
SQL> ALTER SESSION SET PARALLEL_INSTANCE_GROUP = SEOUL;
SQL> SELECT /*+ PARALLEL(A) PARALLEL(B) */
COUNT(*)
FROM BIG_SEOUL1 A, BIG_SEOUL2 B
WHERE A.ID = B.ID;
-- 부산과 관련된 작업 실행하고자 할 경우에는 “BUSAN” 그룹을 지정한다. 아래와 같이 작업을 수행하면 어떤 인스턴스에서 작업을 수행하든, PES들은 인스턴스 1번과 인스턴스 2번을 모두 사용한다.
SQL> ALTER SESSION SET PARALLEL_INSTANCE_GROUP = BUSAN;
SQL> SELECT /*+ PARALLEL(A) PARALLEL(B) */
COUNT(*)
FROM BIG_BUSAN1 A, BIG_BUSAN2 B
WHERE A.ID = B.ID;
위의 예제를 통해 알 수 있듯이 병렬 작업 그룹을 잘 활용하면 병렬 작업들을 효과적으로 인스턴스에 분배할 수 있다. 업무 설계와 잘 연동해서 사용하면 불필요한 인터커넥트 낭비를 사전에 차단할 수 있으므로 성능 면에서도 큰 장점을 제공한다.
오라클은 병렬 작업을 최적화하기 위해 파티션을 적극적으로 활용한다. 가령 파티션으로 분할되어 있는 테이블에 대한 DML 작업은 병렬 DML(Parallel DML. PDML)로 전환할 수 있다. 병렬로 수행되는 DML 작업은 각 파티션 별로 독립적인 DML이 수행되므로 대량 DML의 성능을 극대화할 수 있다.
병렬 작업과 파티션의 관계에서 반드시 언급해야 할 기능이 파티션 지향 조인(Partition-wise Join)이다. 파티션 지향 조인이란 파티션 별로 병렬로 조인 작업을 수행함으로써 대량 데이터에 대한 조인 성능을 최적화하는 기능을 말한다. 파티션 지향 조인은 조인 대상이 되는 테이블의 파티션 구성이 동일한 지 여부에 따라 전체 파티션 지향 조인(Full Partition-wise Join)과 부분 파티션 지향 조인(Partial Partition-wise Join)으로 구분된다.
아래 예제는 조인 대상이 되는 테이블의 파티션 구성에 따라 전체/부분 파티션 지향 조인이 어떻게 수행되며, 그 실행 계획은 어떻게 나타나는지를 테스트한 결과이다.
-- PX_TEST 테이블: 파티션수가 4인 해시 파티션
SQL> CREATE TABLE PX_TEST(ID NUMBER, NAME VARCHAR2(100))
PARTITION BY HASH(ID) PARTITIONS 4;
-- PX_TEST2 테이블: 파티션수가 4인 해시 파티션
SQL> CREATE TABLE PX_TEST2(ID NUMBER, NAME VARCHAR2(100))
PARTITION BY HASH(ID) PARTITIONS 4;
-- PX_TEST3 테이블: 파티션 없는 테이블
SQL> CREATE TABLE PX_TEST3(ID NUMBER, NAME VARCHAR2(100));
---------------------------------------------------------------
-- Case1: 전체 파티션 지향 조인. PX_TEST 테이블과 PX_TEST2 테이블
SELECT /*+ PARALLEL(A) PARALLEL(B) */ COUNT(*)
FROM PX_TEST A, PX_TEST2 B
WHERE A.ID = B.ID
---> 두 테이블은 파티션 구성이 동일하므로 전체 파티션 지향 조인으로 풀린다.
SELECT STATEMENT ALL_ROWS-Cost : 33882
SORT AGGREGATE
PX COORDINATOR
PX SEND QC (RANDOM)
SORT AGGREGATE
PX PARTITION HASH ALL <--- 전체 파티션 지향 조인을 의미
HASH JOIN
TABLE ACCESS FULL OWI.PX_TEST2(2)
TABLE ACCESS FULL OWI.PX_TEST(1)
---------------------------------------------------------------
-- Case 2: 부분 파티션 지향 조인. PX_TEST 테이블과 PX_TEST3 테이블
SELECT /*+ PARALLEL(A) PARALLEL(B) */ COUNT(*)
FROM PX_TEST A, PX_TEST3 B
WHERE A.ID = B.ID
---> 두 테이블은 파티션 구성이 다르므로 부분 파티션 지향 조인으로 풀린다.
SELECT STATEMENT ALL_ROWS-Cost : 41720
SORT AGGREGATE
PX COORDINATOR
PX SEND QC (RANDOM)
SORT AGGREGATE
HASH JOIN
PX RECEIVE
PX SEND PARTITION (KEY) <--- PX_TEST3는 파티션이 없으므로 논리적인 파티션 생성
PX BLOCK ITERATOR
TABLE ACCESS FULL OWI.PX_TEST3(2)
PX PARTITION HASH ALL <--- PX_TEST는 파티션이 이루어져 있으므로 파티션 스캔
TABLE ACCESS FULL OWI.PX_TEST(1)
---------------------------------------------------------------
-- Case 3: 일반 조인. PX_TEST3 테이블과 PX_TEST3 테이블(셀프 조인)
SELECT /*+ PARALLEL(A) PARALLEL(B) */ COUNT(*)
FROM PX_TEST3 A, PX_TEST3 B
WHERE A.ID = B.ID
---> 두 테이블은 파티션이 없으므로 일반 조인으로 풀린다.
SELECT STATEMENT ALL_ROWS-Cost : 40564
SORT AGGREGATE
PX COORDINATOR
PX SEND QC (RANDOM)
SORT AGGREGATE
HASH JOIN
PX RECEIVE
PX SEND <--- PX_TEST3는 파티션이 이루어져 있지 않으므로 단순 병렬 스캔
PX BLOCK ITERATOR
TABLE ACCESS FULL OWI.PX_TEST3(1)
PX RECEIVE
PX SEND <--- PX_TEST3는 파티션이 이루어져 있지 않으므로 단순 병렬 스캔
PX BLOCK ITERATOR
TABLE ACCESS FULL OWI.PX_TEST3(2)
파티션 지향 조인에서는 조인 성능의 최적화를 위해 DOP를 파티션 수로 제한한다. 강제로 DOP를 지정하지 않는다면 오라클은 파티션 지향 조인을 수행하는 실행 계획을 생성하며 DOP도 자연스럽게 파티션 수에 의해 결정된다. 만일 강제로 DOP를 지정하면 오라클은 파티션 지향 조인을 하지 않고 일반적인 병렬 조인을 수행한다. 어떤 것이 성능에 더 유리한지는 상황에 달려 있다. 만일 파 조인이 단연 유리할 것이다. 반면 CPU 수가 파티션 수에 비해 충분히 크다면 모든 CPU 자원을 활용할 수 있는 일반적인 병렬 조인이 더
|