메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

MySQL Proxy 시작하기(1)

한빛미디어

|

2007-08-13

|

by HANBIT

20,903

제공 : 한빛 네트워크
저자 : Giuseppe Maxia
역자 : 이정목
원문 : Getting Started with MySQL Proxy

MySQL Proxy가 출시되면서 커뮤니티에 상당한 동요가 일고 있습니다. 그런데 여기엔 그만한 이유가 있습니다. 기능에 굶주려 있는 사람들에게 있어 MySQL Proxy는 MySQL 도구모음에 추가된 기능들 중에서 부정할 수 없는 가장 흥미로운 것이기 때문이죠.

만약 마지막 문장 때문에 다소 당혹스럽다면 그것은 여러분이 MySQL Proxy의 부가가치를 아직 보지 못했기 때문인데, 걱정할 필요는 없습니다. 이 기사는 여러분에게 Proxy가 무엇을 할 수 있는지에 대해 느낄 수 있도록 하는 것을 목표로 삼고 있기 때문입니다.

이제 Proxyland로의 굉장한 여행을 할 준비를 해주십시오.

MySQL Proxy 개관

MySQL Proxy는 하나 혹은 여러 대의 MySQL 클라이언트들과 한 대의 서버 사이에 위치해 있는 경량 바이너리 애플리케이션(lightweight binary application)입니다. 클라이언트들은 보통 인증정보를 가지고 서버에 접속하는 대신 Proxy에 접속하는데, Proxy는 클라이언트와 서버 사이에서 중간자 역할을 합니다.

이러한 기본적인 형식에서는 Proxy는 단순히 재지정자(redirector)에 지나지 않습니다. Proxy는 클라이언트(질의문)로부터 비어있는 버킷(bucket)을 하나 받아 그것을 서버로 전달하고, 버킷에 데이터를 채운 다음 다시 클라이언트로 되돌려 줍니다.

단지 이것이 전부라면 Proxy는 단순히 불필요한 오버헤드일 뿐입니다. 여기엔 제가 아직 말씀드리지 않은 것이 조금 있습니다. Proxy에는 루아 인터프리터(Lua Interpreter)가 내장되어 있습니다. 루아를 이용하면 여러분은 Proxy가 쿼리나 Result Set을 전달해 주기 전에 그것들을 가지고 무엇을 할 것인지를 정의할 수 있습니다.


그림 1. MySQL Proxy는 쿼리와 Result Set을 변경할 수 있습니다.

Proxy의 위력은 모두 자체적인 유연함에 있는데, 그러한 유연함은 루아 엔진에 의해 가능한 것입니다. 여러분은 쿼리가 서버로 전달되기 전에 가로챌 수 있으며, 그것들을 가지고 상상할 수 있는 모든 것들을 수행할 수 있습니다:
  • 변경하지 않은 채로 전달(기본값)
  • 철자 오류 수정(CRATE DATAABSE로 적혀있지는 않나?)
  • 필터링, 다시 말해 쿼리를 전체적으로 제거하기
  • 정책에 따른 쿼리 재작성(rewriting) (강력한 비밀번호 요구, 비어있는 내용 전송 금지)
  • 잊어버린 문장 추가(autocommit이 활성화되어 있는 상태에서 사용자가 BEGIN WORK를 보내지는 않았나? 이러한 경우 여러분은 문장 앞에 SET AUTOCOMMIT = 0를 삽입할 수 있습니다)
  • 그 이상의 것들: 만약 여러분이 생각할 수만 있다면 그것은 아마도 이미 가능할 것입니다. 만약 그렇지 않다면 블로그에 올리세요. 누군가가 그것을 만들 수도 있으니까요.
동일한 방법으로 여러분은 Result set을 가로챌 수 있으며, 따라서 여러분은 다음과 같은 것들을 할 수 있습니다:
  • 쿼리 결과에 대한 레코드 제거, 변경, 추가. 비밀번호를 가리고 싶거나, 인증되지 않은 채로 엿보는 시선으로부터 정보를 숨기고 싶지는 않습니까?
  • 컬럼명을 포함하여 여러분만의 Result Set을 만드십시오. 예를 들면 여러분은 사용자가 새로운 SQL 명령을 입력할 수 있게 해놓았다면 여러분은 요청된 result set을 만들어 보여줄 수 있습니다.
  • Result set 무시하기. 즉 result set을 클라이언트로 되돌려 주지 않습니다.
  • 그 이상의 것들을 하길 원하십니까? 아마도 가능할겁니다. 예제를 살펴보시고 실험해 보십시오!
주요 개념

MySQL Proxy는 객체 지향적인 기반구조를 토대로 만들어져 있습니다. 주요 클래스는 세 개의 멤버 함수를 외부에 노출합니다. 여러분은 그러한 함수들을 루아 스크립트에서 재정의하여 Proxy의 동작을 변경할 수 있습니다.
  • connect_server(): 연결시에 호출되며 여러분은 이 함수 내에서 작업하여 연결 파라미터를 수정할 수 있습니다. 또한 이 함수는 로드 밸런싱(load balancing)을 제공하는데 사용될 수 있습니다.
  • read_query(packet): 이 함수는 쿼리를 서버로 보내기 전에 호출됩니다. 여러분은 여기에서 원래의 쿼리를 변경하거나 무언가를 큐에 더 집어넣도록 중간에서 조작할 수 있습니다. 게다가 백엔드에 위치한 서버를 완전히 건너뛰고 여러분이 원하는 결과를 클라이언트에게 그대로 돌려보내줄 수도 있습니다(예를 들면 주어진 SELECT * FROM big_table의 경우 여러분은 다음과 같이 되물어볼 수도 있습니다. “big_table은 2천만개의 레코드를 가지고 있습니다. WHERE절을 잊으셨습니까?”)
  • read_query_result(inject_packet): 이 함수는 주입된 쿼리에 대한 응답으로 결과를 되돌려 보내기 전에 호출됩니다. 여러분은 여기에서 Result Set을 가지고 무엇을 할건지를 결정할 수 있습니다.
이러한 서버에 이르는 세 가지 통로들을 조합하여 여러분은 서버에 대한 고수준의 기동성을 달성할 수 있습니다.

설치

Proxy를 설치하는 것은 상당히 쉽습니다. 배포 패키지는 단지 하나의 바이너리만을(그리고 0.5.1버전을 비롯한 몇 가지 예제 루아 스크립트) 포함하고 있습니다. 여러분은 배포 패키지의 압축을 풀어 원하는 곳에 복사할 수 있으며 몇몇 운영체제의 경우에는 설치하기가 훨씬 더 쉬운데 왜냐하면 모든 것들을 알아서 해주는 RPM 패키지도 있기 때문입니다.

만약 여러분의 운영체제가 배포 대상에 포함되어 있지 않거나 아니면 갓 만들어진 최신 기능을 사용해 보고 싶다면 공개 서브버전 트리(public Subversion tree)에서 직접 소스코드를 구해 Proxy를 빌드할 수도 있습니다. 그렇게 하려면 단지 몇 가지 기초적인 작업만 수행하면 됩니다.
 ./autogen.sh
 ./configure && make
 sudo make install 
 # will copy the executable to /usr/local/sbin
간단한 쿼리 가로채기(Simple Query Interception)

첫 번째 예제로, 일종의 “내가 거기에 있었다”와 같은 여러분이 있고자 하는 곳에 서 있는 듯한 느낌을 주는 동작을 만들어 보도록 합시다.
  1. first_example.lua라는 이름의 Lua 파일을 하나 만들고 다음에 나열되어 있는 코드를 입력합니다.
  2. 여러분의 데이터베이스 서버가 동일한 머신에 있다고 가정하고 프록시 서버를 실행합니다.
  3. 별도의 콘솔에서 일반 서버에 접속하는 것처럼 프록시 서버에 접속하는데, 유일한 차이점은 3306 포트 대신 4040 포트를 사용할 것이라는 점입니다.
 -- first_example.lua 
 function read_query(packet)
   if string.byte(packet) == proxy.COM_QUERY then
     print("Hello world! Seen the query: " .. string.sub(packet, 2))
   end
 end
# starting the proxy
$ mysql-proxy --proxy-lua-script=first_example.lua -D                          
# from another console, accessing the proxy
$ mysql -u USERNAME -pPASSWORD -h 127.0.0.1 -P 4040 -e "SHOW TABLES FROM test"
여러분이 이전의 터미널 창으로 되돌아 오게 되면 여러분은 Proxy가 여러분을 위해 무언가를 가로챘음을 볼 수 있을 것입니다.
Hello world! Seen the query: select @@version_comment limit 1
Hello world! Seen the query: SHOW TABLES FROM test
첫 번째 쿼리가 MySQL 클라이언트가 접속할 때 전송되었습니다. 두 번째 것은 여러분이 보낸 것입니다. 여러분도 볼 수 있겠지만 여러분은 중간에 끼어들 수 있으며, Proxy가 여러분을 위해 무언가를 하게 만들 수 있습니다. 지금은 이러한 점이 매우 사소한 것에 불과하지만, 다음 문단에서는 좀 더 흥미로운 것을 살펴볼 것입니다.

사용상 주의할 점

0.5.0 버전까지는 루아 스크립트를 사용하려면 –proxy-profiling 옵션을 사용해야 할 필요가 있는데, 그렇지 않을 경우 read_query와 read_query_result 함수가 작동하지 않습니다. 0.5.1 버전부터는 이 옵션이 더 이상 필요하지 않게 되었습니다. 위에서 언급한 함수가 기본값으로 활성화되는 대신 그러한 사용법을 생략하기 위해 새로운 옵션이 도입되었습니다. 여러분이 프록시를 로드 밸런싱에만 사용하고 있을 경우 지금은 -proxy-skip-profiling 옵션을 지정해야 합니다.

쿼리 재작성(Query Rewriting)

좀 더 흥미로운 것을 쿼리 재작성으로 시작해 보도록 합시다. 이 기능을 보여주기 위해 실용적인 작업을 골라보도록 하죠. 우리는 일반적인 입력 오류가 포함되어 있는 쿼리를 받아서 그것을 올바른 키워드로 수정해서 교체하려고 합니다. 가장 자주 손가락이 꼬인 상태에서 입력하는 문장을 SLECT와 CRATE로 간주할 것입니다.

다음은 second_example.lua입니다.
 function read_query( packet )
   if string.byte(packet) == proxy.COM_QUERY then
     local query = string.sub(packet, 2)
     print ("received " .. query)
     local replacing = false
     -- matches "CRATE" as first word of the query
     if string.match(string.upper(query), "^%s*CRATE") then
         query = string.gsub(query,"^%s*%w+", "CREATE")
         replacing = true
     -- matches "SLECT" as first word of the query
     elseif string.match(string.upper(query), "^%s*SLECT") then
         query = string.gsub(query,"^%s*%w+", "SELECT")
         replacing = true
     end
     if (replacing) then
         print("replaced with " .. query )
         proxy.queries:append(1, string.char(proxy.COM_QUERY) .. query )
         return proxy.PROXY_SEND_QUERY
     end
   end
 end
시작하기 전에 –proxy-lua-script=second_example.lua 옵션을 주어 서버를 시작하고 다음과 같이 MySQL 클라이언트에서 접속합니다.
 $ mysql -u USERNAME -pPASSWORD -h 127.0.0.1 -P 4040 
 Welcome to the MySQL monitor.  Commands end with ; or g.
 Your MySQL connection id is 48
 Server version: 5.0.37-log MySQL Community Server (GPL)

 Type "help;" or "h" for help. Type "c" to clear the buffer.

 mysql> use test
 Database changed
 mysql> CRATE TABLE t1 (id int);         # Notice: TYPO!
 Query OK, 0 rows affected (0.04 sec)

 mysql> INSERT INTO t1 VALUES (1), (2);
 Query OK, 2 rows affected (0.01 sec)
 Records: 2  Duplicates: 0  Warnings: 0

 mysql> SLECT * FROM t1;                 # Notice: TYPO!
 +------+
 | id   |
 +------+
 |    1 | 
 |    2 | 
 +------+
 2 rows in set (0.00 sec)
멋지지 않나요? 제가 일상적인 실수들을 저질렀지만, Proxy는 그것들을 수정해줄 만큼 친절합니다. 어떤 내용들이 보고되었는지 살펴봅시다.
 received select @@version_comment limit 1
 received SELECT DATABASE()
 received CRATE TABLE t1 (id int)
 replaced with CREATE TABLE t1 (id int)
 received INSERT INTO t1 VALUES (1), (2)
 received SLECT * FROM t1
 replaced with SELECT * FROM t1
처음의 두 쿼리는 클라이언트가 목적하는 바를 이루기 위해 필요한 것들입니다. 그 다음에는 저의 첫 번째 실수인 CRATE인데, 우아하게CREATE로 수정되었고, 그리고 마지막에는 SLECT를 받았지만 SELECT로 바뀌었습니다. 이 스크립트가 매우 조잡하긴 하지만 여러분에게 가능성에 대한 아이디어는 주었을 것입니다.

쿼리 주입(Query Injection)

다음으로는 MySQL Proxy의 특별한 기능 중 하나인 쿼리 주입(query injection)을 이용해 보도록 합시다. 쿼리 주입은 MySQL Proxy의 고유한 기능입니다. 쿼리 주입이 필요할 경우 쿼리의 큐를 생성할 수 있으며 각각의 쿼리에 ID 코드를 할당한 후에 그것들을 서버로 보낼 수 있습니다.


그림 2. 쿼리 주입(Query Injection)

이미지에는 서버가 세 개의 쿼리를 전달받는데, 물론 서버는 세 개의 Result Set을 되돌려 줍니다. 쿼리 주입이 일어날 때 Result Set은 다른 함수인 read_query_result에 의해 처리되는데, 이 함수에서 여러분은 각각의 ID에 따라 Result Set을 처리할 수 있습니다. 예제에서는 ID 2와 3에 대해 여러분은 SHOW STATUS로부터 반환되는 것을 받아 그것들의 값을 비교하여 메인 쿼리가 서버에 미치는 영향을 측정할 수 있습니다. 여러분은 SHOW STATUS 값을 내부 연산에 대해서만 사용하기 때문에 그러한 Result Set을 클라이언트에 보내지는 않으며(단순히 이렇게 해도 괜찮은 것은 클라이언트가 그것을 전달받지 않는 것으로 예상하고 있기 때문입니다), 그냥 버리면 됩니다.


그림 3. 주입된 쿼리 처리

클라이언트에 의해 전달된 쿼리의 Result Set은 알맞게 반환됩니다. 이는 클라이언트에 대해서 투명하며, 틈틈이 여러분은 프록시 콘솔에 보여지는 통계적인 수치를 수집할 수도 있습니다. 완전한 예제를 보려면 Forge에 있는 쿼리 주입 튜토리얼(the query injection tutorial)을 참고하십시오.

매크로(Macro)

매크로는 단지 쿼리 재작성 기능을 사용하는 또 다른 방법일 뿐입니다. 매크로는 프록시를 사용하는 방법에 있어 가장 인상적인 것 중의 하나입니다. 여러분은 SQL 언어를 재작성하거나 혹은 좀 더 여러분의 구미에 맞게 만들 수 있습니다. 예를 들면 MySQL 명령행 클라이언트를 사용하는 사람들은 use와 show tables를 사용하는 대신 cd와 ls를 입력하곤 합니다. MySQL Proxy를 이용하면 cd와 ls를 이용하여 기대하는 결과를 얻을 수 있습니다. 이러한 쓸만한 매크로 생성 및 사용에 관한 예제는 이전 블로그 포스팅에서 찾아볼 수 있습니다. 여기서 그것들을 모두 반복하는 것 보다는 여러분이 직접 your first macros with MySQL Proxy를 방문하여 살펴보시길 바랍니다.

Result Set 생성: MySQL 쉘 명령어

Proxy는 클라이언트로부터 요청을 받아 Result Set을 클라이언트에게 되돌려 주어야 합니다. 대부분 이렇게 하는 것은 매우 쉽습니다. 쿼리를 서버로 전달하고, Result Set을 가져오고, Result Set을 클라이언트에게 전달하면 됩니다. 그렇지만 서버가 제공할 수 없는 무언가를 되돌려 주어야 할 필요가 있다면 어떻게 해야 될까요? 그럴 경우 우리는 컬럼명과 데이터의 이차원 배열로 구성되어 있는 Result Set을 만들 필요가 있을 것입니다.

데이터셋(Dataset) 생성 기초

예를 들어 비추천 기능에 관한 경고를 반환하기를 원할 경우 저는 Result Set을 아래와 같이 만들 수 있습니다:
 proxy.response.resultset = {
     fields = {
         { 
            type = proxy.MYSQL_TYPE_STRING, 
            name = "deprecated feature", 
         },
         { 
            type = proxy.MYSQL_TYPE_STRING, 
            name = "suggested replacement", 
         },
     },
     rows = {
          { 
             "SHOW DATABASES", 
             "SHOW SCHEMAS" 
          }
     }
 }
 -- 
그리고 나서 이것을 클라이언트에게 보냅니다.
return proxy.PROXY_SEND_RESULT
위의 구조를 클라이언트가 받게 되면 다음과 같이 보여질 것입니다:
+---------------------+-----------------------+
| deprecated feature  | suggested replacement |
+---------------------+-----------------------+
| SHOW DATABASES      | SHOW SCHEMAS          |
+---------------------+-----------------------+
즉, 여러분의 필요에 맞는 모든 Result Set을 만들어 낼 수 있다는 얘깁니다. 좀 더 자세한 내용을 보려면 Jan Kneschke의 예제를 확인해 보십시오.
TAG :
댓글 입력
자료실