Work on Postfix continues to make it more scalable, after the system was made feature-complete in the past couple years. One recent example is Postscreen, which focuses on the increasing botnet threat, and keeping zombie SMTP clients away from the SMTP servers.
This note discusses an approach to improve before-queue content inspection performance, and to streamline local mail submission. The first is more important as it can allow a system to handle more mail with the same memory resources. Wietse Non-persistent queues, speed-matching and fall-back =================================================== Imagine a stream of data from some source, that needs to be processed by some processing software (a sink). There are several scenarios where it can be helpful to save the data stream to a non-persistent buffer before handing that data over to the processing software. Example 1: data arrives slowly, and we don't want to allocate expensive processing resources before all the data has arrived. In the case of the Postfix mail system, it may take a long time to receive an email message over the network, compared to the time to send that same message into a local before-queue content filter. We don't want to tie up an entire content filter instance while the data trickles in via the network, because each filter instance consumes a significant amount of memory and other resources. If we could collect the message in a non-persistent buffer before invoking the content filter, then we would be able to run fewer content filter instances at the same time, and the filter instances would be better utilized. Another example of speed-matching involves reputation-based throttling, where remote SMTP clients with poor or no reputation can send mail only at limited bit rates. As bitrates decrease, sessions durations increase. We can reduce the number of mail handling processes by collecting each rate-limited message to a non-persistent buffer before handing that message to the rest of the mail infrastructure. Example 2: when the preferred service is unavailable, give the data to a backup service. In the case of Postfix, the postdrop program injects a mail submission from a local user into the mail system. It would be good for performance if postdrop could hand over a message directly to the cleanup server. However, the postdrop program would need to be able to save the message to the maildrop queue when the Postfix system is down. Besides, it is undesirable that postdrop ties up a cleanup process while receiving mail from some process that takes a long time to complete. The solution is that postdrop saves the entire mail submission to a non-persistent buffer before it contacts the cleanup service. Example 3: the source produces data faster than the sink can consume it. If the sink is slow, but it can still process the data before the source times out, then there is no benefit from inserting a non-persistent buffer between source and sink. It would actually cause more timeout errors, because a non-persistent buffer disables the flow control mechanism that is implemented by the operating system. If the sink takes a significant time to process the data, a persistent buffer is preferable, to prevent the source from timing out before the sink reports completion. In the case of Postfix, this is implemented by the persistent queue in combination with post-queue content inspection. Implementation for postdrop/cleanup =================================== The modified postdrop program flow is straightforward: save the request to file, and when the input is complete, pipe the message into a cleanup server. If the cleanup server is happy, delete the file, otherwise leave the file in the maildrop queue and mark it as ready. This can be implemented at a relatively low level in the existing mail_stream module, by adding a new API that takes both a server name and a queue file name. The new API would combine features of the existing mail_stream_service() and mail_stream_file() functions. This would initially save the input to file, try to dump the file to the server, and leave the file in the queue when the server is unavailable. Under the covers, it would be possible to buffer small messages in memory and to use the disk only for overflow or persistence. Implementation for before-queue content filter ============================================== In this case, the implementation is a little trickier, because the Postfix SMTP server connects to the before-queue filter over SMTP, which is a chatty protocol. In the present, unbuffered, case the replies from the before-queue filter alternate with commands from the before-filter smtpd process. filter banner ehlo command filter reply ... data command filter reply 3xx header+body . filter reply quit command In the buffered case, the before-filter smtpd writes its SMTP commands to a replay log, together with the expected before-queue filter SMTP replies. Only when the replay log is complete, the before-filter smtpd contacts the before-queue content filter. While quickly replaying the log, the before-filter smtpd compares the expected before-queue filter replies in the replay log, against the actual filter replies, and declares a "queue file write error" when there is a mis-match. Only the filter's reply to "." is sent back to the remote client in real time. This is what "nice dump" of the replay log would look like: expect 2xx send ehlo myhostname expect 2xx ... send data expect 3xx send text send text send text send . expect 2xx send quit command Implementation break-down ------------------------- The smtpd_proxy_cmd() function already encapsulates the handling of a before-filter smtpd request and the corresponding expected before-queue filter reply. The smtpd_proxy module also provides an API for email message content I/O. The non-persistent buffer can therefore be added with only low-level changes. The new functionality involves opening the replay log file and unlinking it immediately, writing commands and expected replies to the replay log, and the replayer that sends commands to the content filter, and that reuses the existing code to parse filter replies. There is some opportunity for optimization: the same unlinked file can be used to buffer the content of successive email transactions by the same smtpd process. In practice, the non-persistent buffer will sit in the file system cache and never hit the disk, so the disk overhead will be small. One tweak is where to open the file; it does not have to live in the same file system as the Postfix queue (but there would have to be enough free space to save the largest allowed message). Finally, small messages could be buffered entirely in memory. This is an space/time trade-off that can be added later.