Postfix/Dovecot 性能検証（POP3処理性能）


## 環境
- マシンA（自宅）
  - AMD Ryzen7 4800H(8core/16thread) 16GBメモリ
  - Windows 11 Home（ホストOS：VMware Workstation 16 Player）： Thunderbird(POP3)
  - Red Hat Enterprise Linux release 8.4 (Ootpa)（ゲストOS）：postfix/dovecot
  - ホスト4core/ゲスト4coreの割り当て。
- マシンB（AWS）
  - t3.xlarge（4vCPU/16GBメモリ）
  - Red Hat Enterprise Linux release 8.6 (Ootpa)：postfix/dovecot


## 検証手順
postfixの設定は本番を意識したものではなくなる早で検証開始する目的
```
#vi /etc/postfix/main.cf
#変更内容ポイント
myhostname = 環境の値を設定
inet_interfaces = all
home_mailbox = Maildir/

#最後に追加
message_size_limit = 0
mailbox_size_limit = 0
```

変更後再起動
```
# systemctl restart postfix
```

dovecotの設定は本番を意識したものではなくなる早で検証開始する目的
```
vi /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:~/Maildir

vi /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = no

vi /etc/dovecot/conf.d/10-ssl.conf
ssl = no
```

変更後再起動
```
# systemctl restart dovecot
```


メールデータの作成（添付ファイルでサイズ膨らませる）
※これをベースにサイズと件数のバリエーションをshell scriptで簡単に組める。
```
dd if=/dev/urandom of=/tmp/500m bs=1M count=500
echo "mail body" | mailx -a /tmp/500M -s 'test' test@localhost.localdomain
```
※注意：500MBのメールデータを意図しても実際のPOPサーバー内のファイルは708760764まで膨らむ。

参考shell script
```
#!/bin/bash

a=0
while [ "$a" -lt $1 ]
do
    a=$(expr $a + 1)
    echo "mail body" | mailx -a /tmp/1M -s 'test' test@xxx.yyy.zzz
done

```

※性能差（上記ddの結果）
- 自宅マシンの性能
  - 524288000 bytes (524 MB, 500 MiB) copied, 9.20449 s, 57.0 MB/s
- AWSの性能
  - 524288000 bytes (524 MB, 500 MiB) copied, 6.27526 s, 83.5 MB/s

※ネットワークレイテンシーの違い
- 自宅VM内の論理通信
  - pingのavgが0.75msec
- 自宅とパブリックAWS通信
  - pingのavgが7msec
  - 環境によっては二桁msecとなる場合もある。これが様々なクライアント、サーバーアプリに悪影響を与える。


- ケース1
thunderbirdで500MBデータをPOP3取得
結果：8MB/s で1分数十秒程度
CPU使用率はクライアント、サーバーともに使用率低。（1POPで1コア程度なのでCPU使い切るにはクライアント多重度をあげる必要あり。）
結果は自宅内と自宅とAWS間で大差出ず。
※時間計測はWindowsのリソースモニタを目視し、Thunderbird固有処理（クライアント固有処理）ではなくPOP3通信が連続していると思われる期間を計測。

- ケース2
WinScpで500MBファイルの転送
60MB/s 10sec 自宅内
30MB/s 20sec 自宅とAWS間
※上記のPOPでの8MB/sがネットワークネックでないと判断できる。
※POPのほうがSCPよりもアプリケーションの処理コストが高い。
※レイテンシーに影響されやすいアプリケーション実装。（POP比較）

- ケース3 
※自宅とAWS間のみ
thunderbird(500MB=1M*500 POP3)
5MB/s で約2分ちょい。
1MBファイルを500通のメールを取得

- ケース4
※自宅とAWS間のみ
thunderbird(500MB=2621448B*2000 POP3)
3.x MB/s で約3分半。

- ケース5
POP3アプリケーション観点。アプリのオーバーヘッドをみるため（ノイズを省く）自宅内のみで比較
50Kのメールを1000件受信するケース
①Thunerbird：3.1MB/sで23秒
②Javaフェッチのみ：20MB/sで4秒
③Java(シングルスレッドでシーケンシャルに処理し、メール情報をファイル出力)：0.6MB/sで118秒
※Javaのサンプルソースは本ページの最後に記載。
※メールデータ分析処理のようなメールをバッチ処理するアプリケーションではアプリの実装によって性能が大きく異なる。

## 検証サマリ

|環境|アプリケーション|処理|結果|備考|
|:-----------------|------------------:|:------------------:|:------------------:|:------------------:|
|自宅内|WinSCP|500MBファイル転送|ネットワークスループット：60MB/s 実行時間：10sec||
|自宅-AWS|WinSCP|500MBファイル転送|ネットワークスループット：30MB/s 実行時間：20sec|WinSCPはネットワークレイテンシーに影響されやすい。|
|自宅内|Thunderbird|500MBファイルを添付したメールを1通POP3取得|8MB/s で1分数十秒程度||
|自宅-AWS|Thunderbird|500MBファイルを添付したメールを1通POP3取得|8MB/s で1分数十秒程度|SCPのアプリコスト（プロトコルオーバーヘッド）が高い。|
|自宅-AWS|Thunderbird|1MBファイルを添付したメールを500通POP3取得|5MB/s で2分超|メール数に応じてPOPプロトコルオーバーヘッドが大きくなる。|
|自宅-AWS|Thunderbird|2621448Bファイルを添付したメールを2000通POP3取得|3MB/s で約3分半超。|メール数に応じてPOPプロトコルオーバーヘッドが大きくなる。|
|自宅-AWS|Thunderbird|50KBファイルを添付したメールを1000通POP3取得|3.1MB/s で23秒。||
|自宅-AWS|Java（フェッチのみ）|50KBファイルを添付したメールを1000通POP3取得|20MB/s で4秒。|ネットワークスループットMAXに近い処理|
|自宅-AWS|Java（フェッチ+永続化）|50KBファイルを添付したメールを1000通POP3取得|0.6MB/s で118秒。|シングルスレッド、シーケンシャルな最も非効率な処理|

## 総論
ネットワーク環境、アプリの実装、プロトコルの複雑度、対象データの特性によって処理性能やネットワーク負荷が大きく異なる。

## 今後
【チューニング余地ありそうなもの】
/etc/dovecot/conf.d/10-mail.conf
```
#Max number of mails to keep open and prefetch to memory. This only works with
#some mailbox formats and/or operating systems.
#mail_prefetch_count = 0
```
※大量のメールデータを取り扱うアプリケーションで効果ありそう。

/etc/dovecot/conf.d/20-pop3.conf
```
#Allow only one POP3 session to run simultaneously for the same user.
#pop3_lock_session = no
```
※いわゆるセッションの使いまわし。ネットワークレイテンシー高い環境下で効果ありそう。


## 参考
Java POP3アプリ(ググってコピペしてきたもの)。フェッチオンリーとしているのは実際のメールデータ処理をすべてコメントアウトしている。これがケース5の②に該当。コメントアウトを外すとケース5の③になる。
JavaMailがデータをネットワーク越しにもってきているのは確実だが、一時領域としてどう内部的に格納しているのかは未調査。
```
package sample;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;

import javax.mail.BodyPart;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.internet.MimeMultipart;
public class FetchOnly {


	public static void main(String[] args){
		long start = System.currentTimeMillis();
		Folder folder = null;
		try {
			Properties props = new Properties();
			props.put("mail.pop3.host", "192.168.47.128");
			props.put("mail.pop3.port", "110");
			Session session = Session.getDefaultInstance(props);
			Store store = session.getStore("pop3");
			store.connect("test", "test");
			folder = store.getFolder("INBOX");
			//folder.open(Folder.READ_ONLY);
			folder.open(Folder.READ_WRITE);

			// メッセージ一覧を取得
			Message[] arrayMessages = folder.getMessages();
			for (int lc = 0; lc < arrayMessages.length; lc++) {

				// メッセージの取得
				Message message = arrayMessages[lc];

				// 件名の取得と表示
				String subject = message.getSubject();
				//System.out.print("件名：" + subject + "\r\n");

				// 受信日時を表示
				String sentDate = message.getSentDate().toString();
				//System.out.print("受信日時：" + sentDate + "\r\n");

				// 本文の取得と表示
				MimeMultipart mltp = (MimeMultipart)message.getContent();
				BodyPart body1 = mltp.getBodyPart(0);
				//System.out.print("本文：" + body1.getContent().toString() + "\r\n");

				// １：添付ファイルボディを取得
				BodyPart body2 = mltp.getBodyPart(1);
				String fileName = body2.getFileName();
				//System.out.print("添付ファイル名：" + fileName + "\r\n");

				// ２：添付ファイルのストリームを取得
				InputStream ins = body2.getInputStream();

				// ３：出力ストリームに出力しながらファイルを保存する
				OutputStream outs = new FileOutputStream("C:\\Users\\m2tor\\Desktop\\save.txt"); 
//				int out;
//				while ((out = ins.read()) != -1) {
//					outs.write(out);
//				}

				ins.close();
				outs.close();

				message.setFlags(new Flags(Flags.Flag.DELETED), true); // 対象メールをメールサーバから削除する.

			}
			folder.close(true);
			System.out.println("メッセージ処理件数：" + arrayMessages.length);
			long end = System.currentTimeMillis();
			System.out.println("処理時間(秒)：" + (end - start)/1000);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if(folder.isOpen()) {
					folder.close(false);	
				}

			} catch (MessagingException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}
		}

	}
}
```