djangoのメール送信とawsサーバーを並列化した話

会社でメールマガジンを送る際、送信対象人数が多いとサーバーが止まってしまうという不具合があったので負荷分散するために送信処理とサーバーを並列化しました。
その際に、並列化のコードやサーバー側の設定で勉強になったことやつまづいたことを備忘録として書きます。

<h1>メール送信の並列化について</h1>
<p>送信処理の並列化をスレッドプールかプロセスプールで並列化予定だったので、まずはスレッドプールとプロセスプールについて調べました。<p>

<h3>スレッドプール</h3>
<p>スレッドプールは、複数のスレッドをプールで管理し、必要に応じてスレッドを再利用する仕組みです。これにより、スレッドの生成と破棄にかかるオーバーヘッドを削減し、効率的に処理を行うことが期待できます。</p>
<h3>プロセスプール</h3>
<p>プロセスプールは、複数のプロセスを生成し、それぞれが独立して処理を行います。スレッドプールと比べてリソースをより分離することができるため、安定して大規模な処理を行うことが可能です。</p>

<p>並列化ではメールの送信は主にI/O操作が中心なためスレッドプールで実装しました。

<h2>送信処理について</h2>
<p>送信処理を作成する際、pythonの<a href="https://docs.python.org/ja/3/library/concurrent.futures.html">concurrent.futuresモジュール</a>を使用して並列化しました。

```
with ThreadPoolExecutor(max_workers=3) as executor:
    executor.map(送信処理)
```
というようなコードで３並列で送信をしてくコードを作成しました。
送信処理は実装コードによって変わりますが、djangoの場合は下記のように送信用のコードがあるので、コードに沿って送信処理を作成します。
djangoの<a href="https://docs.djangoproject.com/ja/5.0/topics/email/">メール送信の詳細</a>
```
def send_mail(subject, message, from_email, recipient_list,
              fail_silently=False, auth_user=None, auth_password=None,
              connection=None, html_message=None):
    """
    Easy wrapper for sending a single message to a recipient list. All members
    of the recipient list will see the other recipients in the 'To' field.

    If from_email is None, use the DEFAULT_FROM_EMAIL setting.
    If auth_user is None, use the EMAIL_HOST_USER setting.
    If auth_password is None, use the EMAIL_HOST_PASSWORD setting.

    Note: The API for this method is frozen. New code wanting to extend the
    functionality should use the EmailMessage class directly.
    """
    connection = connection or get_connection(
        username=auth_user,
        password=auth_password,
        fail_silently=fail_silently,
    )
    mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, connection=connection)
    if html_message:
        mail.attach_alternative(html_message, 'text/html')

    return mail.send()
```

<h2>awsサーバーの並列化について</h2>
<p>awsの場合はec2で送信サーバーを作った後、サーバー接続用のコードを作成します。
サーバー接続のコードはdjangoの場合は send_mail() と同じで<a href="https://docs.djangoproject.com/ja/5.0/topics/email/#django.core.mail.get_connection">サーバーの接続用のコード</a>があります
それを参考にしてください</p>

<p>サーバーの並列化については送信処理の部分でsend_mail()に渡す値としてconnectionがあり、その部分に処理ごとに接続先サーバーを渡せばサーバーごとに処理します。</p>
私は、接続先サーバーのリストを作り、<a href="https://docs.python.org/ja/3/library/itertools.html">itertoolsのcycle</a>を使い処理ごとに順次割り当てしています。

<h3>送信確認</h3>
<p>ec2などで作成したメールサーバーに接続ができるか、送信は可能かを調べる場合は netcat などを使用して、ターミナル(Mac)やコマンドライン(Windows)から直接確認することができます。</p>

<h4>nc [オプション] [ホスト名またはIPアドレス] [ポート番号]</h4>

```
nc example.com 587
```
上記の場合はexample.comに587ポートで接続します
ポート番号については<a href="https://ja.wikipedia.org/wiki/TCP%E3%82%84UDP%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7%E3%81%AE%E4%B8%80%E8%A6%A7">wikipedia</a>などにも載っているので一度見ておくとわかりやすいかもです。

メール送信関連だけなら25、587、465、2525などを見れば問題ないです。

```text:実行結果(接続成功)
220 example.com ESMTP Postfix (Ubuntu)
```
上記のような文字が表示されれば接続できています。

<h3>メール送信のpostfixについて</h3>
私が一番苦戦した部分がpostfixの設定です。<br>
postfixの修正をする場合はsshなどを利用し、送信サーバに接続して、/etc/postfix/のファイルを修正します。<br>
主な修正ファイルはmain.cfとmaster.cfです。

詳細な設定はサーバーによって変わったり、すると思うので修正の際に出たエラーの対処方法を記載します。
対処する際は<a href="https://www.postfix-jp.info/trans-2.3/jhtml/postconf.5.html">Postfixの設定パラメータ</a>を参考にしてください

<h4>Recipient address rejected: Access denied</h4>
このエラーはAccess deniedとあるようにアクセスの拒否が原因です。
main.cfを確認して <a href="https://www.postfix-jp.info/trans-2.3/jhtml/postconf.5.html#permit_sasl_authenticated">permit_sasl_authenticated</a> , <a href="https://www.postfix-jp.info/trans-2.3/jhtml/postconf.5.html#reject">reject</a> , <a href="https://www.postfix-jp.info/trans-2.3/jhtml/postconf.5.html#permit_mynetworks">permit_mynetworks</a> , <a href="https://www.postfix-jp.info/trans-2.3/jhtml/postconf.5.html#reject_unauth_destination">reject_unauth_destination</a> などを設定してみてください。
またmain.cfに設定があるのにAccess deniedが出る場合master.cfのsmtpd_recipient_restrictionsを確認して、permit_mynetworksなどがあるか確認してください。
私が修正した際はmaster.cfが permit_sasl_authenticated と　rejectのみだったためプロトコルで認証した場合のみ許可されていたのでAccess deniedが発生しました。

<h4>Relay access denied</h4>
このエラーもアクセス拒否のエラーですが、こちらは単純にmain.cfのmynetworksの設定でIPアドレスを追加すれば解決します。
サーバーを並列化する場合は送信処理をするサーバーで送信コマンドを打ったサーバーのIPアドレスを入力してください(localや送信サーバーから直接など)からみて他のIPアドレスを追加します。
例えばlocalから送信コマンドを使用した場合はそれぞれの送信サーバーのmain.cfのmynetworksにlocalのIPアドレスを記載
送信サーバーから送信コマンドを使用した場合は他の送信サーバーに送信コマンドを使用したのIPアドレスを記載します。