본문 바로가기

Server Story..../Linux

php 프로그램으로 대량 메일 발송하기. 1시간에 10만건.????

http://www.erury.com/hots/community/bbs/board.php?bo_table=share91&wr_id=1053&sfl=&stx=&sst=wr_datetime&sod=asc&sop=and&page=8

위에서 퍼옴

일반적으로 php 프로그래밍은 길어봐야 수초 내에 끝나는 것이 대부분이다. 
  하지만, 메일을 보낸다거나 할 경우에는 소스 상단에 set_time_limit(0); 를 추가해서 보내는 경우가 있다. 
  물론 돈이 많거나, 실력이 뛰어난 프로그래머라면, 좋은 발송기를 사거나, 좋은 프로그램을 짜서 보내면 되지만, 
  나와 같이 허접한 실력의 프로그래머라면, php로 해결하는 수 밖에 없다 ^^;;; 
  
  보통의 경우... 대량 메일을 보내기 위해서는 서버단 설정을 먼저해야 한다. 
  요즘 나오는 센드메일은 멀티 큐를 지원하고 있고, 큐메일은 예전부터 멀티큐를 지원하기 때문에, 
  메일 서버 자체에서는 별로 설정할 일이 없다. 
  만일 메일 서버단의 설정이 궁금하다면, 멀티큐로 검색해 보면 많이 나올 것이다. 
  
  오늘 여기서 다루고자 하는 것은... 
  php 프로그램으로 대량 메일을 어떻게 보내야지만 좋을까 하는 것이다. 
  사장님의 압력에 몇번의 실패 끝에 알아내게 된 내용을 적어본다. 
  
  1. rcpt to를 이용한 방법 
  
  처음에 선임자가 짜 놓은 프로그램을 보니 smtp class를 이용해서 보내는 것이었다. 
  그런데 10만통을 5분에 쏜다고 사장님께서 말씀하시길래 이해가 안가서 소스를 뒤적여 봤더니... 
  헤더를 조작해서 보내는 것이었다 -.-;; 
  즉 메일의 rcpt to에다가 
  
  aaa@hanmail.com,bbb@hanmail.com,ccc@hanmail.com...........zzz@naver.com 
  
  이런식으로 해 놓고, 헤더의 receive에는 그냥 '회원님' 하고 보내는 것이었다. 
  
  그러면 받는 사람한테는 rcpt to의 내용이 보이지도 않으면서, php 프로그램 상에서는 메일을 한통만 쏘는 효과가 있었던 것이었다 -.-;; 
  물론... 센드메일이나 큐메일은 rcpt to에 콤마로 이어놓은 메일 수 만큼 bacrground로 열심히 메일을 뿌리고 있겠지만... ^^;; 
  
  하지만 이 방법의 문제점은 header의 to 정보와 rcpt to의 to 정보가 불일치 하기때문에, 
  대부분의 메일서버가 스팸으로 분류한다는 단점이 있다 -.-;; 
  결국 보내봐야 소용없는 메일이 되고 만다 -.-;; 
  따라서 다른 방법을 찾아봐야만 했다. 
  
  2. smtp로 직접 접속해서 보내는 방법 
  
  smtp로 보내는 방법은 여러가지 설정화 헤더 정보 등을 임의로 입맛에 맞게끔 수정해서 보낼 수 있다. 
  하지만, 브라우져라면 한번에 한 창을, 실행파일이라면 한번에 한 프로세스만을 띠울 수 밖에 없다. 
  smtp 프로토콜을 이미 하나의 프로세스가 잡고 있기에, 다른 프로세스는 대기할 수 밖에 없기 때문이다. 
  따라서 php의 mail() 함수를 사용해서, smtp를 물고 있지 않더라도, MTU에 그냥 메일만 던져주고 빠지는 방식을 쓸 수 밖에 없었다. 
  
  3. 콘솔 상에서 보내는 방법 
  
  보통 브라우져로 메일을 보내게 되면, 브라우져=>아파치=>php=>MTU 의 단계를 거치게 된다. 
  또한 브라우져는 http 통신을 하게 되므로 지속적인 연결을 하면서 메일을 발송하는 것에는 조금 불안한 감이 있다. 
  따라서 php를 binary 버전으로 컴파일 한 후... 아파치와는 독립적으로, perl이나 sh 파일처럼 콘솔 상에서 직접 실행함으로서 조금은 안정적으로 보낼 수 있다. 
  
  * 문제점 
  
  본인의 경우에는 업체명을 쿼리한 후... 해당 업체에 소속된 회원에게 메일을 발송하는 작업이었다. 
  메일을 보내는 헤더정보와 메일 내용의 footer 부분에 해당 업체의 정보가 들어가야 하기에 루푸문 안의 쿼리는 어쩔 수 없었다. 
  
  업체의 정보와, 회원의 메일 정보를 가져오는 디비쿼리의 경우에는 메일을 보내는 동안 디비 커넥션을 계속 물고 있어야 하기에, 
  디비 클래스를 조금 수정하여, 해당 정보를 배열로 받고 난 후 접속을 끊어 버리는 형태로 만들었다. 
  하지만, 이 또한 문제점이 발생하였다. 
  
  ex) 
  
  $conn= new mysqlClass('호스트', '디비병', '유저명', '패스워드'); 
  
  //회원정보 불러오기... 
  $sListQuery = "SELECT index, email, company_id FROM email_list WHERE ORDER BY company_id ASC LIMIT ".$argv[5].", ".$argv[6]; 
  $aListRows = $conn->getData($sListQuery); 
  
  if(is_array($aListRows)) { 
      foreach($aListRows AS $key=>$value) { 
          //업체 정보가 틀려 졌다면... 
          if($aListRows[$key]['company_id'] != $aListRows[$key-1]['company_id']) { 
              //업체 정보 쿼리 
              $sComQuery = " 
                  SELECT 
                      A.ID, A.Name, A.Email, 
                      B.Company, B.Addr, B.Phone, B.Fax, B.Homepage, B.NickName 
                  FROM MemberOfCompany AS B 
                  LEFT JOIN Member AS A ON A.id=B.id 
                  WHERE A.ID='".$aListRows[$key]['company_id']."'"; 
              $aComRows = $conn->getData($sComQuery, 1); 
  
              //메일 header 생성 
  
              //메일 body 생성 
          } 
  
          if(($bResult = mail($aListRows[$key]['email'], $argv[4], $body, $headers)) == 1) { 
              echo "Send OK, ".$aListRows[$key]['index'].", ${NickName}사무실, Sucess Count - $i, Sucess Email - ".$aListRows[$key]['email']."\n"; 
              $i++; 
          } 
          else { 
              echo "Send Error : ".$aListRows[$key]['index'].", ${NickName}사무실, Failure Email - ".$aListRows[$key]['email']."\n"; 
              exec("echo '".$aComRows['ID'].",".$aListRows[$key]['email']."' >> ".date('Ymd')."_Company.log");    //로그기록... 
          } 
      } 
  } 
  
  
  위와 같은 소스로 메일을 발송하다보면... 중간에... 다음과 같은 에러가 발생한다 
  
  ::2013Lost connection to MySQL server during query2013 
  
  커넥션이 끊어졌다는 것이다. -.-;; 
  바로 업체 정보를 가져오는 $sComQuery를 실행할 수 없어서 에러가 난 것이다. 
  그러면서 그 이후의 메일 발송은 바로 stop이 되는 난감한 상태가 발생한 것이다. 
  그렇다고 해서 connection 타임을 mysql 설정 단에서 늘려주면, 일반 웹서비스로 인해 디비커넥션이 full 되서 디비서버 자체가 죽을 우려가 있었기 때문이다. 
  
  조금의 고민 끝에... 커넥션이 끊어 졌으면 pconnect를 써야 겠다 라는 생각에 클래스를 pconnect로 접속하도록 바꿨지만, 이 역시 마찬가지 결과를 가져왔다. 
  
  그래서 결국에는 '커넥션 끊어지면, 다시 커넥션 하면 되지 뭐 -.-;;' 라는 생각에... 
  is_resource 함수를 사용하기로 했다 ^^;; 
  
  
  *******  바꾼 소스...  ************ 
  if(!is_resource($conn)) $conn= new mysqlClass('호스트', '디비병', '유저명', '패스워드'); 
  
  //업체 정보 쿼리 
  $sComQuery = " 
      SELECT 
          A.ID, A.Name, A.Email, 
          B.Company, B.Addr, B.Phone, B.Fax, B.Homepage, B.NickName 
      FROM MemberOfCompany AS B 
      LEFT JOIN Member AS A ON A.id=B.id 
      WHERE A.ID='".$aListRows[$key]['company_id']."'"; 
  $aComRows = $conn->getData($sComQuery, 1); 
  
  
  그런데... 다시 생각해 보니 $conn은 원래부터가 resource가 아니였던 것이다. -.-;; 
  메뉴얼에도 잘 나와 있다시피... 
  
  $db_link = @mysql_connect('localhost', 'mysql_user', 'mysql_pass'); 
  
  처럼 접속 했을 때 $db_link가 resource인 것이지, new를 통해 생성한 객체는 object 였던 것이다 -.-;; 
  
  그래서 결국에는 class에 메쏘드 하나를 추가하기로 했다. -.-;; 
  
  
  function checkResource() { 
      if(is_resource($this->_CONN)) { 
          return true; 
      } 
      else { 
          return false; 
      } 
  } 
  
  그리고 나서 위에 소스도 다음과 같이 수정했다. 
  
  *******  최종 소스...  ********* 
  if(!$conn->checkResource()) $conn= new mysqlClass('호스트', '디비병', '유저명', '패스워드'); 
  
  //업체 정보 쿼리 
  $sComQuery = " 
      SELECT 
          A.ID, A.Name, A.Email, 
          B.Company, B.Addr, B.Phone, B.Fax, B.Homepage, B.NickName 
      FROM MemberOfCompany AS B 
      LEFT JOIN Member AS A ON A.id=B.id 
      WHERE A.ID='".$aListRows[$key]['company_id']."'"; 
  $aComRows = $conn->getData($sComQuery, 1); 
  
  
  아직까지는 잘 돌아가고 있으며, 메일 10만통을 보내는 데 1시간 정도 걸린다.(물론 콘솔을 4개 띄워서 동시에 4개의 프로세스로 돌린 경우이다 ^^;;) 
  php로 한시간 정도 에러 안나고 돌아가면... 성공한 것 아닌가? ㅋㅋㅋ 
  
  어째든... 아무런 에러 없이 계속 돌아가 주기만을 바랄뿐이다 ^^