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.

Reply via email to