导读:
本文根据笔者在微软Technet Webcast上的讲稿整理而成。文章介绍了微软Exchange Server中的核心传输组件以及它们的工作原理,阐述了SMTP协议的内容和使用SMTP发送邮件时的详细过程;深入地讨论了邮件传输和路由的工作机制,分析了SMTP报文的组成和Exchange在传输邮件时的路由过程(包括AQ, Routing Engine等组件)。本文可以供Exchange管理员深入的了解邮件传输组件的内部工作方式。
上期回顾
在上一期的《Exchange传输组件大揭秘-上》中,笔者讨论了SMTP协议的工作方式,协议命令字和MIME编码的基础知识,同时也介绍了Exchange Server传输模块的构成和基于事件触发机制的工作原理。本文我们将继续上一期的讨论,来深入的挖掘Exchange Server传输组件背后鲜为人知的秘密。
基于事件触发机制的邮件传输过程
上一期文章的末尾,我们提到了Exchange Server传输模块中的高级队列引擎(Advanced Queuing Engine,下文简称AQE)。作为传输模块的核心,AQE在邮件传输过程中起到了至关重要的作用,投递过程中很多关键的操作都是在AQE中完成的。
Windows IIS自带的SMTP服务使用Aqueue.dll作为高级队列处理组件,在安装Exchange Server时,安装程序使用Phatq.dll取代了Aqueue.dll,作为Exchange Server的高级队列处理组件。AQE的主要作用是控制邮件传输过程,触发相应的事件并调用Event Sink进行邮件的进一步处理。从邮件被提交到AQE内部到最终被投递,AQE会触发如下的事件:
SMTP Transport OnSubmission
等待传输的邮件只有被提交到AQE中以后,才会被分类器和路由引擎处理,这个称之为OnSubmission(也叫OnTransportSubmission)的事件在邮件通过SMTP连接或Exchange数据库提交到AQE以后被触发。事件触发后,AQE负责调用与此事件关联的Event Sink。Exchange的传输层反病毒API与此事件相关联,用来实现邮件被传输之前的病毒检测和扫描。我们也可使用CDO之类的编程技术,来开发自定义的Event Sink与此事件相关联,CDO中的CDO_OnArrival事件实际上就是对OnTransportSubmission的包装,通过CDO_OnArrival事件可以得到实际被传输的邮件的句柄,对邮件进行特殊的操作。具体可以参考微软知识库:837851, “How to configure an Internet Information Services SMTP virtual server to archive or to remove messages in an Exchange Server 2003 test environment”
SMTP Transport OnCategorize
OnCategorize并不是一个单独的事件,而是当邮件被分拣处理时产生的一系列事件(共有十个单独的事件)。与这个事件群相关联的Event Sink我们称之为分类器(Categorizer),在Exchange 2003中,共有两个分类器与OnCategorize事件群进行了关联,他们分别是Exchange Categorizer(phatcat.dll) 和Outlook Mobile Access Push Categorizer(miscat.dll)。前者用于完成诸如收/发件人地址解析、DL分拆、收件人限制检查等等常规的分类器职责(这些职责则在下文会详细讨论),后者是用来完成为手机用户发送邮件到达提醒的特殊功能。
SMTP Transport OnGetMessageRouter
该事件在邮件被认为需要进行远程投递或者路由解析时被触发。同OnCategorize一样,这也是一个事件群。与此事件群相关联的Event Sink是Exchange Router模块(会在下一期的连载中详细讨论)。
SMTP Transport OnMsgTrackLog
与此事件关联的是Messaging Tracking日志记录组件,这个Event Sink负责记录邮件投递过程中的日志,可以通过Exchange System Manager来启动此功能并且进行邮件投递的追踪。(请参考微软知识库文档:How to enable message tracking in Exchange Server http://support.microsoft.com/default.aspx?scid=kb;en-us;246856)
SMTP Transport OnDnsResolveRecord
此事件在系统需要把一个目标域名解析为MX记录的IP地址时被触发,与此事件相关联的Event Sink是Exchange LoadBalancer。该组件负责DNS解析并在有多个路由组连接器存在的情况下进行负载平衡。
SMTP StoreDriver
此事件在邮件需要被保存到硬盘或者数据库时被触发。同OnCategorize一样,这也是一个事件群,这些事件与Store Driver进行关联,当邮件需要被保存时,由Store Driver负责执行对文件系统和数据库的读写操作(下一期的文章会对此事件群作深入的讨论)。
上面我们简单的列出了由AQE触发的事件,可以看出,其中的三个事件群(分别是OnCategorize、OnGetMessageRouter和StoreDriver)在邮件的投递过程中起着至关重要的作用,他们负责了激活相应的邮件的分类、路由和存储Event Sink。在进一步研究这些Event Sink组件的工作内容时,我们再通过一张图来回顾一下由AQE触发的事件。(图一)
图一:由AQE触发的事件
在上文中我们提到,Exchange Server传输组件内共有四种类型的事件,如果把上面提到的这些重要事件按照类别进行归类,我们可以发现其中大部分的事件属于SMTP传输事件。在Exchange Transport模块中,微软一共开发了6个Event Sink来响应这些SMTP传输事件,他们的名字和作用如下表:
Event Sink名称 | 作用 |
Exchange Transport XEXCH50 Submission | 该Event Sink响应OnSubmission 事件,它的主要代码在Peexch50.dll 中被实现。这个Event Sink的主要作用是处理Exchange Server间的通信。所有的服务器内部通信也是通过SMTP协议来完成的,他们使用XEXCH50 这个微软自定义的命令字。 |
Exchange Transport AntiVirus API | 该Event Sink响应OnSubmission 事件,它的主要代码在OnSubmit.dll 中被实现。这个Event Sink的主要作用是为反病毒厂商提供了一组在传输层的病毒扫描API,使用此API的程序可以在OnSubmission事件发生时截获邮件内容并进行病毒扫描。 通常情况之下,反病毒厂商更倾向于使用基于数据库层面的反病毒接口,因此这个传输层反病毒API默认是被禁用的。如果服务器是邮件网关、前端服务器,管理员可以通过更改注册表的方式启用此接口。 |
Exchange Categorizer | 这是Exchange Server最重要的模块之一,由在Phatcat.dll中实现的Event Sink响应OnCategorize事件群中的事件。Phatcat.dll中的代码实现了地址解析、邮件转发、设定外发邮件地址标识、展开DL、进行各类传输限制的检查等等至关重要的功能。Categorizer中的代码同时也实现了邮件归档(journaling)和特殊情况下的邮件拆分(bifurcate)。 |
Mobile Categorizer | Miscat.dll中的Event Sink负责处理移动设备用户的邮件到达通知(up-to-date notifications)。这是Exchange 2003中的新功能。 |
Exchange Router | 这个Event Sink由Reapi.dll中的代码实现,用来响应OnGetMessageRouter 事件群中的事件。AQE通过Reapi.dll来确定邮件的下一跳(next hop)地址。这个Event Sink同时计算Exchange组织的路由拓扑和路由表。 |
Exchange LoadBalancer | 这个Event Sink也是由Reapi.dll中的代码实现,它响应OnDnsResolveRecord事件,负责在多个外部连接器之间进行负载平衡。 |
表一:响应SMTP传输事件的Event Sink。
下文中,我们将重点讨论在Exchange Categorizer中发生的故事。
Exchange Categorizer
OnCategorize事件群一共由十个事件组成,我们可以通过MSExchangeTransport的诊断日志来了解这些事件的细节,诊断日志的开启方法请看图二。系统还允许管理员开启Level 7级别(Debugging Level)的SMTP诊断日志来监控SMTP服务器的每一个动作,这个日志的开启需要进行注册表的更改,具体的键值位置请参考此文档:How to enable SMTP protocol logging
http://support.microsoft.com/default.aspx?scid=kb;en-us;265139。
图二:启动MSExchangeTransport Diagnostics Logging
为了全面分析,我们先简单的把这10个Event和他们对应的Event Sink的职能列出来:(如表二)
OnCategorize事件 | Event Sink所执行的操作 |
Register | 分类器组件初始化事件,负责创建分类器与其它模块(如活动目录访问、路由引擎等等)的连接 |
BeginMessageCategorization | 表示邮件分类过程的开始 |
EndMessageCategorization | 表示邮件分类过程的结束 |
BuildQuery | 通过收发件人的proxyAddresses属性来创建对活动目录进行查询的LDAP语句,来获取收发件人对象的完整信息,这是地址解析的重要过程之一 |
BuildQueries | 执行批量的BuildQuery操作 |
SendQuery | 发送查询请求到活动目录,此查询是异步的 |
SortQueryResult | 根据查询条件对返回的结构进行处理 |
ProcessItem | 将地址解析的结果应用到邮件上 |
ExpandItem | 这个过程也是邮件投递过程中非常重要的一环,DL分解、发送限制检查、邮件拆分都在这里发生 |
CompleteItem | 当以上操作完成后,CompleteItem事件的Event Sink执行邮件传送归档(Journaling)、组织内收件人的服务器定位以及其它的一些特殊操作 |
表二:OnCategorize事件
请参考微软知识库文档来了解Categorization过程更多的细节:
XCON: What Message Categorization in Exchange 2000 Server Involves
我们可以从表二中看到,OnCategorize事件群的Event Sink执行了邮件投递过程中的很多重要操作,我们重点讨论其中三个Event Sink的处理细节。
第一:BuildQuery和地址解析
BuildQuery是一个非常复杂的过程,要完全理解这个组件,首先需要了解Exchange Server中的各种“邮件地址类型”。在Exchange Server中,我们常用的SMTP地址(
username@domain.com)只是种类繁多的邮件地址类型里面的冰山一角。每一种邮件协议,都有它特定的邮件地址表示方法,Lotus Notes、Novell GroupWise、古老的MS Mail等等,都有一套特定的邮件地址,只不过相比SMTP,这些邮件地址类型我们很少接触到罢了。Exchange中必须支持的其他邮件地址类型还有:X.400 address、Legacyexchangedn、Non-Exchange address等等。要说清楚这些地址类型,我们还需要从Exchange的发展史谈起。Exchange5.5时代,微软为了兼容X.400和X.500协议,在邮件系统内部采用了X.400这样的用户邮件地址表示方式,随着Internet的发展,Exchange产品也开始把重心侧重到对SMTP协议的支持,从Exchange 2000开始,SMTP成为了Exchange中邮件传输的主力协议,但是为了兼容Exchange 5.5,微软仍然保留了MTA传输引擎和X.400地址类型,同时,在活动目录中引入了Legacyexchangedn这样的属性。
我们可以在Exchange管理器的Recipient->Recipient Policy->Default Policy->属性->E-mail address中看到当前的Exchange系统支持的地址类型。默认支持的地址类型是SMTP和X.400(这两个类型不允许被删除)。我们也可以添加诸如cc:Mail、Louts Notes、Novell GroupWise之类的地址类型,当Exchange Server通过外部连接器和这些第三方的邮件系统连接时,这些地址就会发挥作用。(此处不深入讨论)
我们重点看SMTP地址类型。默认请况下,Exchange里面的SMTP地址类型中的SMTP域名和活动目录的域名是一致的,但是需要明确的是,
此处的SMTP域和活动目录域是完全不同和没有丝毫关系的。举例来说,我们可以在公司内网建立一个名为msft.com的AD域,然后在Internet上申请到一个my-domain.com的公网域名。我们可以在Recipient Policy中添加这个my-domain.com的域名,使Exchange系统意识到它有责任接收和处理发给my-domian.com的邮件(在公网DNS上设定my-dmomina.com的MX记录)。并且,我们可以设定my-domian.com为主SMTP地址,这样所有外发到Internet的邮件,都会使用@my-domina.com作为其发信人地址。如下图(图三):
图三:Recipient Policy
关于Recipient Policy中SMTP地址的设定和自定义,请参考下面的文章:
XIMS: How to Receive Messages for Two SMTP Domains
当在活动目录中创建用户时,如果我们细心观察就会发现,系统并没有让管理员输入用户的SMTP地址和X.400地址中的任何一种地址!对于一个有邮箱的用户来说,他的邮件地址是Exchange Server通过一个叫做RUS(Recipient Update Service)的过程,根据Recipient Policy中设定好的地址类型来自动生成的。用户是活动目录中的对象,这些邮件地址,就是这个对象上的属性。
How the Recipient Update Service applies recipient policies
当邮件分类器需要解析收件人邮件地址并进行投递时,它会根据邮件报文中的proxyAddresses字段来进行活动目录查询,以获取收件人在活动目录中的对象和其他的一些属性。不同的发送协议(SMTP/MTA)和邮件客户端(MAPI/SMTP)会在信件写入不同的proxyAddresses,可能是SMTP地址,也可能是X.400或者Legacyexchangedn。Exchange的BuildQuery过程会根据这些proxyAddresses生成一个LDAP的查询,比如,当邮件的收件人为mike@microsoft.com时,查询语句为:(proxyAddresses=smtp:mike@microsoft.com)。如果活动目录中有这个用户,查询会返回对这个用户对象的引用和一系列进行邮件投递的必要属性,比如:Homemdb、Homemta、msExchHomeServerName、msExchMailboxSecurityDescriptor、msExchMailboxGuid。通过这些属性,Exchange传输系统就会知道这个用户的邮箱位置等信息。
查询单个用户是最简单的情况,在收件人是邮件组时,会涉及到各种不同类型邮件组地址格式处理和连接邮件组解析服务器等操作,情况会复杂很多。简单来说,BuildQuery会配合其他的组件采用递归查询的方式解析收件人中的各个项目,直到所有的收件人都被查到为止。
BuildQuery过程也会查询发件人的信息,这是为了处理发件人限制、权限之类的情况。
BuildQuery只是万里长征的第一步,当结束了用户地址查询,并获取了用户对象的若干属性后,CAT引擎将进行更进一步的邮件处理。
第二:ExpandItem和特殊邮件的处理
在ExpandItem阶段,传输引擎会执行预先定义的邮件发送限制、转发设置和进行邮件拆分,下面我们详细的看一下每一个过程。
Restriction checking
这个过程检查收件人和发件人是否有收发送邮件的权限和邮件尺寸方面的限制,举例来说,如果管理员在Exchange中设定不允许一封邮件中的收件人超过特定的数量,分类器会通过在BeginMessageCategorization的时候初始化一个计数器,并在解析收件人的时候用这个计数器计算这封邮件一共有多少个收件人。如果超过了预先设定的数量,分类器就会通知AQE进入到退信流程:AQE会产生错误代码为5.5.3的退信给发件人。
Alternate recipients
我们可以在活动目录中设置把发给某人的邮件作自动转发(活动目录用户属性->Exchange General->Delivery Options)。这个过程的具体实现就是在ExpandItem中完成的。分类器会根据转发的设定,在收件人列表中(只在SMTP信封上添加,正文部分看不到转发地址)添加被转发的邮箱地址。在到达最终目的地之前,邮件有时候需要在组织内的多台服务期间传递,为了避免转发地址被多次添加(每经过一台服务器,SMTP服务器都会执行分类器操作和转发检测),当第一次执行分类器操作并添加好转发地址以后,分类器会在邮件上做一个XEXCH50的标识,这样当下一个服务器看到此邮件时,就不会作重复的转发解析工作了。
同时,分类器也会检查潜在的转发循环情况(A转发给B,B转发给C,C又转发给A)。如果循环被侦测到,分类器会通知AQE,后者即刻生成错误代码为5.4.6的退信给发件人。
Message bifurcation(邮件分拆)
前面两个过程比较简单,现在我们讨论相对复杂得多的“Message bifurcation”过程。当发送一份邮件给若干个收件人时,在一些特殊情况下,每个收件人收到的信息内容和格式必须不相同,这时就需要通过bifurcation过程来制作邮件的副本。读者可能无法理解为什么同一封邮件的收件人希望接收到不同格式和内容的邮件,我们举一个例子:
想象一下当一封信件被发给一个名为“Steven Sun”的用户和“Company All Employee”邮件组这两个地址时,前者是一个普通的收件人,而后者是一个组,并且该组的成员是隐藏不可见的。发信人设定了这封邮件的送达回执和阅读回执,但是因为收件人中有一个隐藏成员的组,Exchange必须采取某些措施来避免送达回执向发件人泄漏组中的成员。Exchange采用bifurcation为此邮件制作两个副本,第一个副本是给Steven Sun的,其中有正常的送达回执和阅读回执。第二个副本是给Company All Employee的,这封信中,Exchange会去掉这些引起信息泄漏的回执请求。这个过程对收信人是透明的,Bifurcation生成的两个副本,只是在其SMTP信封和正文的信头中,有不同的收件人和回执设定,所有的收件人在Outlook中读到的信件正文都是一样的(如果不理解,请回忆前一期文章中提到的SMTP报文格式)。
当发送密件抄送(BCC)和使用MAPI的客户端发信给Internet用户时,还会发生更加复杂的bifurcation的过程,限于篇幅,这里不错深入的讨论。有兴趣的读者,可以参考如下的链接:
第三:CompleteItem和路由、投递前的准备工作
经过一些列Categorizer Event Sink处理后,邮件的目的地和收件人相应的属性都已经确定,在把邮件交给路由引擎选路和进行投递之前,Categorizer会做一些最后的收尾工作,并为路由和投递准备好需要的信息,这个过程包括:邮件归档、写入收件人标示、投递状态结果的处理等等。
Journaling
邮件归档(Journaling)是把某个指定的邮箱数据库中的用户发送和接收的邮件做一个自动归档的过程(请参考:XADM: How to Enable the "Message Journaling" Function for an Exchange Server Mailbox Store http://support.microsoft.com/default.aspx?scid=kb;en-us;261173)。分类器会根据BuildQuery过程获取的用户HomeMDB属性来判断此用户的邮件是否需要做归档,如果需要归档,分类器在邮件的收件人列表中加入归档邮箱(使用BCC方式隐藏,因此用户感觉不感到归档过程,这和上文提到的邮件转发是不同的过程)。
正如前面提到的,是否归档是根据用户的邮箱所在数据库来决定的。当一个用户通过SMTP网关发送邮件时,在这封邮件最终抵达收件人时,可能会被Exchange系统中的多台SMTP服务器进行处理和转发。第一台SMTP服务器负责添加转发地址,并且,服务器使用XEXCH50命令来通知其他的后续服务器归档操作已完成,避免其他服务器重复的添加归档地址。(XEXCH50命令在前文的邮件转发中也有类似的作用。)
Recipient marking
当完成收件人解析后,分类器会跟据每个收件人的目的地做一个特殊的收件人标记(Recipient marking)。这个标记通常为收件人目的地主机的FQDN。AQE根据这些FQDN来判断邮件的目的地是本地服务器,还是需要通过路由组件选路并投递到远程的主机。所有满足本地投递条件的邮件,都无需路由,直接从分类队列中被转到本地投递队列(local delivery queue)中。对于非本地投递的邮件,AQE必须调用路由引擎进行选路操作。
Redirecting delivery status notifications
另一种更加常见的情况是,当发信给一个很大的邮件组时,发信人往往会收到很多OOF、自动回复和投递回条之类的邮件。为了缓解这个问题,默认情况下Exchange分类器会阻拦所有因为发送邮件给用户组而产生的OOF和自动回复。分类器通过在用户组收件人上添加特殊属性来通知系统不要为此收件人执行OOF和自动回复的处理操作。
队列和MailMsg对象
图四:Exchange Server队列查看器
AQE在内存中维护了若干个邮件队列,并由一组共享的线程池来对这些队列中的数据进行操作。我们可以通过Exchange System Manager的队列查看器来观看这些队列的情况(如图四),队列查看器是通过PhatQAdm.dll中的接口与AQE进行通讯来完成队列内容显示和执行队列管理操作的。下图(图五)标明了AQE中主要的队列和这些队列与传输事件之间的关系。
图五:AQE中的邮件队列
上图比较完整的描绘了一封邮件从提交、分类、路由一直到被投递的完整过程。Exchange Server使用队列来管理处于不同投递状态的邮件。对队列的深入理解,有助于分析和解决复杂的邮件传输问题。下面我们分别介绍一下每个队列的作用。
Messages Pending Submission
这个队列是AQE的入口,当有邮件进入这个队列时,AQE触发OnSubmission事件,其Event Sink针对队列中的邮件执行特定的操作,操作完成后,邮件被移动到Messages Awaiting Directory Lookup队列。
Messages Awaiting Directory Lookup
这个队列在分类器中非常的关键。分类器针对处于此队列中的邮件执行收/发件人解析、用户组成员解析、发送限制检查等一些列的操作(在前文中有提到)。经常有管理员会看到邮件在这个队列中大量的堆积,这说在解析地址过程中出现的瓶颈,这往往和活动目录的性能或网络流量有关。
Messages Waiting To Be Routed
这个队列中包含所有已经完成分类操作并等待进一步投递的邮件。此时AQE判断邮件的目的地,并对所有需要进行远程投递的邮件触发OnGetMessageRouter事件来调用路由引擎进行进一步的处理。
Local Delivery
这个队列中包含所有投递目的地是本地服务器的邮件,对于这些邮件,AQE会跳过OnGetMessageRouter事件并直接把他们通过Exchange Store Driver进行投递。
Dynamic Delivery
这些队列往往存在于执行外发邮件任务的SMTP桥头堡之上。针对外发邮件,每个目的地domain都会有一个队列,所有发给这个domain的邮件都包含在其中。这些队列是动态生成的,并且在邮件投递完毕后的一定时间内,由AQE销毁。
MailMSG对象
正如上文提到的,AQE的队列包含了处于不同投递状态的邮件。实际上,这些队列中并不会真正的包含待投递的邮件本身(想想看:谁也不会背着自己的私家车去车管所的各个窗口办理上牌照手续),因为对于分类器和路由引擎来说,邮件的内容往往太大,把他们都放到内存的队列里面会严重的影响性能,而且对执行投递操作并没有帮助(邮件投递主要依靠SMTP信封和正文中的信头部分,与正文中的信体和附件都没有关系)。
为了提高性能,AQE会为其需要处理的每一封邮件创建一个名为ImailMsg的数据结构。在这个结构中,包含了所有AQE需要的邮件属性,并且在此数据结构中,还有一个指向邮件实际内容所在地(可能是NTFS文件系统上的SMTP Queue目录,也可能是Exchange的数据库)的一个句柄(如图六)。
图六:MailMsg对象
当邮件被提交时,ImailMsg数据结构就会被生成。在整个分类和路由过程中,邮件本身都会保存在问及系统上的Queue目录或者是Exchange数据库中,只有ImailMsg在不同的队列中被移动。当邮件真的需要被投递和传输时,邮件的物理数据才会被进行操作。这样做,极大地提高了系统的性能。
我们常在Exchange的队列管理器中看到许多邮件,其实这些都是ImailMsg,这些邮件的“真身”,都分布在不同的位置上。
总结和下期介绍
在本期文章中,我们从更加底层的角度审视了Exchange Server邮件传输的过程,深入的了解了Exchange分类器的作用和它的工作方式。为了避免过于复杂和方便读者理解,笔者对一些错综复杂的过程作了简化处理,并提供了供进一步研究参考的资料链接。下期要目如下:
1. 邮件路由的过程
2. Exchange存储和传输模块之间的桥梁: Store Driver
3. 常用的传输排错工具
作者简介:
喻勇,曾任微软产品技术支持工程师和CTEC课程讲师。对Exchange Server,SharePoint Server,IIS,.NET开发等微软产品和技术有丰富的实践经验。他的电子邮件信箱是yy@yuyong.net,读者可以在他的网站www.yuyong.net下载Exchange相关的课程讲义。