草庐IT

探秘自动机原理与优化实践

赖绍禹 2023-03-28 原文

自动机概况

使用Linux开发环境的程序员一定使用过sed、grep、lex等Linux系统工具,sed、grep是Linux中重要的数据流搜索与处理工具,Lex是linux下广泛使用的词法分析器生成器,用于复杂语言的解析、编译器前端的开发等。尽管这些Linux系统工具功能各异,但这些工具内部都实现了一个自动机,用于对输入预料进行基于正则表达式的文本搜索。自动机则是正则表达式的等价实现。

从计算理论上讲,正则表达式与自动机具有理论上的严格等价性,正则表达式和自动机具有等价的对匹配模式的定义能力。正则表达式是匹配模式的形式化表达,自动机则是对匹配模式的计算机实现的一种表达。

安全检测与防护领域的入侵检测系统(Intrusion Detection Systems,IDS)、入侵防护系统(Intrusion Prevention System, IPS)、web应用防火墙(Web Application Firewall,WAF)等都大量应用了自动机技术进行网络数据流的正则表达式匹配,以实现对网络报文的检测与分析。

IPS/IDS与WAF系统

自动机技术也广泛应用于DPI系统(Deep Packet Inspection,DPI)中,以实现对网络报文的解析与识别。

应用于应用识别的DPI系统

2、正则表达式与自动机

2.1 初识正则表达式与自动机

在形式化语言与自动机理论中,正则表达式与有穷自动机有着理论上的严格等价性。

正则表达式与自动机的等价性

自动机分为确定型有穷自动机(Deterministic Finite Automata,DFA)和非确定型有穷自动机(Non-Deterministic Finite Automata,NFA)。确定型有穷自动机中,对于给定的确定状态与确定输入,其状态转移关系是确定唯一的,且每一时刻只存在一个激活状态。相反,在非确定型有穷自动机中,对于给定的确定状态与确定输入,其可能存在多个状态转移关系,且某一时刻可能存在多个的激活状态。非确定型有穷自动机又主要分为存在epsilon转移的epsilon-NFA与没有epsilon转移的NFA,其经典代表分别为Thompson NFA与Gluskov NFA。

Thompson NFA

上图所示为识别正则表达式(AB|CD)*AFF*的Thompson NFA,可见thompson NFA由基本的子正则表达式NFA单元通过epsilon边连接而成,epsilon边连接的两个状态可以由空字符进行转移,即存在无条件的状态转移关系,由字母标识的边连接的两个状态表明这两个状态之间需要输入对应的字符才能进行激活状态的转移。

Gluskov NFA

上图所示为识别正则表达式(AB|CD)*AFF*的Gluskov NFA。显而易见,Gluskov NFA比Thompson NFA要简洁很多,且Glukov NFA中NFA的状态数与正则表达式中出现的字符和字符集总数一致。相比于Thompson NFA,Gluskov NFA状态数更少,结构更紧凑。同时在Gluskov NFA中,状态之间的跳转条件被移到了节点内变成了节点的激活条件,因此Gluskov NFA的运行时处理也会变得更加简单。在运行时读入一个字符c时,便可知道字符c可以激活的状态集reach(c),那么只需在Gluskov NFA当前的激活状态集s的基础上计算其后继状态集succ(s),并取reach(c)与succ(s)的交集,所得的交集即为Gluskov NFA的下一刻激活状态集。对于Thompson NFA,节点的激活条件并不唯一,且需要处理epsilon边连接的状态转移关系,其下一刻的激活状态集的计算会更复杂。

对Thompson NFA或Gluskov NFA使用子集构造算法,可以将NFA转化为DFA。DFA相较于NFA最大的优势是性能,而劣势在于空间开销,这是因为DFA状态转移的确定性是通过对NFA不同状态进行组合得到的,因此功能等价的DFA和NFA从理论上来说,DFA状态数的上限是NFA状态数的指数关系。

DFA状态图

上图所示为使用子集构造算法将识别正则表达式(AB|CD)*AFF*的thompson NFA转化为DFA后的状态图。图中每个蓝色框的序号集中的序号对应于thompson NFA中状态的序号,体现了DFA中的每个状态对应于NFA状态集的一个子集。

2.2 主流的开源自动机相关库

目前广泛使用的主流开源自动机相关库主要是Pcre、RE2、Hyperscan

  • Pcre支持的正则表达式语法是最全最复杂的,但PCRE只支持块模式编译和匹配,并且只支持单条正则表达式的编译和匹配,性能在这三款软件中是最差的。对于需要进行大规模正则规则并行匹配的场景,pcre就显得力不从心了。
  • Google的开源正则匹配引擎RE2是基于虚拟机方法c++实现的一款快速、安全、线程友好的正则匹配自动机,支持的正则表达式语法比pcre少但比hyperscan多。RE2支持少量正则规则集的并行匹配,不支持只能用回溯算法实现的正则表达式语法。
  • Hyperscan是Intel开源的一款基于正则表达式NFA/DFA图分析与拆解的高性能正则表达式混合自动机。在这三款软件中,hyperscan支持的正则语法最少,但其性能是最强的,且支持大规模正则规则集的并行匹配。

3、自动机的性能优化实践

正则表达式的匹配速率是制约IDS/IPS、WAF、DPI等业务的重要性能瓶颈。提升正则表达式自动机的匹配性能是提升以上业务能力的关键所在,下面介绍自动机性能优化的几种主流方法。

3.1 基于预过滤的性能优化

基于预过滤的正则表达式优化策略

上图所示为基于字符串匹配器预过滤的正则表达式匹配优化策略。该方案会在正则表达式的编译过程中提取正则表达式中的字符串信息,并根据提取的字符串构建一个多字符串预匹配器。如针对规则0,提取了字符串SEARCH,针对规则N提取了字符串SUBSCRIBE。在对输入预料进行匹配的过程中,会先使用多字符串匹配器进行字符串的匹配,若匹配过程中匹配到了字符串SERACH但是没有匹配到字符串SUBCRIBE,则会进一步使用根据正则表达式规则0构建的自动机进行第二阶段的正则表达式匹配。可见基于预过滤的正则表达式匹配方案为一个两阶段的匹配过程。

基于字符串匹配器的预过滤正则表达式匹配方案虽然能提早过滤掉无法匹配的语料。但仍然存在以下的诸多不足:

(1)对正则表达式中的字符串存在重复匹配,即预过滤的字符串匹配组件匹配一次字符串,自动机又重复匹配一次字符串;

(2)基于预过滤的匹配方案中的第二阶段,使用自动机对字符串进行匹配难以有效地使用CPU的SIMD指令集进行字符串匹配的并行加速;

(3)不合理的关键字符串选择容易拖累整体正则表达式的匹配性能。

针对基于字符串匹配器预过滤的正则表达式匹配方案的不足,一种更新颖有效的基于正则表达式分解的正则表达式匹配方案便应运而生了。

3.2 基于正则表达式分解的性能优化

基于正则表达式分解的正则表达式匹配方案首先会将正则表达式拆解成几个子字符串和子正则表达式。拆解的子字符串会被构建为一个字符串匹配器(字符串匹配器可以有效地使用CPU的SIMD指令集进行并行加速,相比于使用自动机匹进行字符串配具有数量级上的性能优势),而拆解的子正则表达式则会被构建为一个子自动机,如NFA或DFA。在对输入语料进行正则表达式匹配时,该方案会按照一定顺序调用各个匹配器,并尽量优先调用字符串匹配器进行字符串的匹配,只有当前一个匹配器匹配成功后才会调用下一个匹配器进行匹配,并且只有当所有的匹配器都按照既定的顺序匹配成功后,整条拆解的正则表达式才真正匹配成功。

基于规则拆分的正则表达式匹配策略

上图所示为使用拆解后的正则表达式.*start[^x]comA+匹配输入字符串AstarZcomA的一个示例。首先,正则表达式被拆解为五个部分,分别对应于自动机部分FA2、FA1、FA0与字符串部分STR2、STR1。拆解后构造的各个子自动机与子字符串匹配器的匹配顺序为STR1->STR2->FA1->FA0->FA2。拆解后构造的各个子自动机与子字符串遵循一下的优先级原则:

  • 字符串匹配优先于自动机匹配。
  • 两个字符串中间的自动机匹配优先于其它位置的自动机匹配。
  • 匹配语料尾部的自动机优先于匹配语料头部的自动机。
第一条优先级原则很好理解,在于字符串匹配速率相对于自动机匹配速率有着数量级上的性能优势。两个字符串之间的自动机所需匹配的语料的行首和行尾是锚定的,因此它的优先级相对于其它自动机优先级较高,即优先级原则2。由于匹配语料尾部的自动机其匹配的行首是锚定的无需回溯操作,所以其优先级较高,即优先级原则3。可见,拆解后的各子自动机与子字符串匹配器的匹配顺序原则上遵循:性能开销越小的匹配过程,其匹配顺序越靠前。

对于输入语料,AstarZcomA,首先会使用字符串匹配器匹配字符串STR1,此时字符串匹配成功,继续调用字符串匹配器匹配字符串STR2,此时字符串STR2匹配失败,则不再使用后续的FA1、FA0、FA2进行匹配。若输入的字符串为AstartZcomA,则会依次成功匹配STR1、STR2、FA1、FA0、FA2,最后输出匹配成功信息。

4、正则规则匹配的应用思考

在互联网领域的各种开发与应用中,网络进攻检测、应用流量识别等大量的场景需要使用正则引擎进行正则表达式的匹配。正则表达式的匹配效率不仅取决于使用的正则引擎的性能好坏,也与书写的正则表达式形式息息相关。揭秘正则引擎的实现原理能让我们更深入了解正则表达式的形式与正则引擎效率的相关性,更好地指导我们进行正则引擎的性能调优。以下原则的正则表达式书写指导意见能帮助我们在开发与应用过程中更高效地进行正则表达式的匹配:

  1. 尽量避免使用需要用回溯方法实现的正则表达式语法,如反向引用语法。回溯的引入会使最坏情况下正则匹配的时间复杂度呈指数的增长。
  2. 尽量在正则表达式中避免(.*)、{min,max}这样的语法,(.*)引入的不确定性以及{min,max}带来的有界重复是正则表达式引擎的重要性能瓶颈。
  3. 尽量将正则表达式写得更确定,如尽量在正则表达式中写更多确定的字符或字符串。

有关探秘自动机原理与优化实践的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  4. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  5. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

  6. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

  7. ruby-on-rails - 向 Rails 3 添加 Ruby 扩展方法的最佳实践? - 2

    我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion

  8. ruby - 在 ruby​​ 中使用自动创建插入数组 - 2

    我想知道是否可以通过自动创建数组来插入数组,如果数组不存在的话,就像在PHP中一样:$toto[]='titi';如果尚未定义$toto,它将创建数组并将“titi”压入。如果已经存在,它只会推送。在Ruby中我必须这样做:toto||=[]toto.push('titi')可以一行完成吗?因为如果我有一个循环,它会测试“||=”,除了第一次:Person.all.eachdo|person|toto||=[]#with1billionofperson,thislineisuseless999999999times...toto.push(person.name)你有更好的解决方案吗?

  9. Ruby 最佳实践 : working with classes - 2

    参见下面的示例,我想最好使用第二种方法,但第一种也可以。哪种方法最好,使用另一种的后果是什么?classTestdefstartp"started"endtest=Test.newtest.startendclassTest2defstartp"started"endendtest2=Test2.newtest2.start 最佳答案 我肯定会说第二种变体更有意义。第一个不会导致错误,但对象实例化完全过时且毫无意义。外部变量在类的范围内不可见:var="string"classAvar=A.newendputsvar#=>strin

  10. ruby - 存储外部 API 的密码 - 最佳实践 - 2

    如果我构建了一个应用程序来访问来自Gmail、Twitter和Facebook的一些数据,并且我希望用户只需输入一次他们的身份验证信息,并且在几天或几周后重置,那会怎样是在Ruby中动态执行此操作的最佳方法吗?我看到很多人只是拥有他们客户/用户凭证的配置文件,如下所示:gmail_account:username:myClientpassword:myClientsPassword这看起来a)非常不安全,b)如果我想为成千上万的用户存储此类信息,它就无法工作。推荐的方法是什么?我希望能够在这些服务之上构建一个界面,因此每次用户进行交易时都必须输入凭据是不可行的。

随机推荐