FreeBSD 上基于 Go 的内容过滤软件
作者:Ganbold Tsagaankhuu、Esbold Unurkhaan、Erdenebat Gantumur
Go 是一门较新的编程语言——与 C、C++、Java 等许多编程语言相比而言。它具备许多实用特性,在多数情况下比其中一些语言更具生产力。而 FreeBSD 历史悠久,已证明是当今最可靠、最强大的操作系统。本文将讨论在 FreeBSD 上用 Go 开发软件的问题、优势、劣势与常见陷阱。我们选择内容过滤软件作为目标,并将项目命名为 Shuultuur。Shuultuur 是一个蒙古语词汇,意思是“过滤器”。
日常生活中,我们正见证着现代世界如何迈向物联网,互联的世界如何迅速扩张。这种转变促使我们从不同视角审视既有问题。对我们而言,内容过滤就是这些问题之一,抱着这一想法,我们开始探索可能性。
有无数由各类社区支持的开源项目。然而,有些项目在其生命周期中演进得并不理想,改进既有代码库变得困难。由于我们的内容过滤项目源于特定需求,我们决定从头开发。为节省数年的开发时间,我们需要具备足够的生产力,以匹配成熟内容过滤软件的功能与性能,并进一步超越。
动因
为什么需要内容过滤?首先,就内容过滤的主要目标而言,普遍的理解是对不良内容施加某种控制。这类解决方案在企业中被广泛用于执行计算机安全策略。图书馆、学校等公共机构使用内容过滤器保护儿童免受与其年龄不相宜的内容侵害——例如成人、暴力、毒品等内容。
编程语言的选择。问题之一是使用哪种编程语言?本质上,我们寻找的是一种快速、轻量、易于原型化,且以相对较少的精力就能产出并维护生产级代码的编程语言。因此,我们偏好静态类型、编译型、具备强类型系统的语言。虽然总有取舍,但更快达成目标的需求使上述特性成为必需。
Go 于五年前在谷歌正式发布(1)。它是一门编译型、静态类型、垃圾回收、非传统面向对象的通用系统编程语言。Go 产出原生二进制,编译速度极快,并在设计之初就考虑了并发。由于 Go 的特性与我们最初的需求相符,我们将其与其他候选语言一起仔细考量并开始实验。Go 原生二进制的性能与经典的低层语言 C 大致相当,远胜于解释型语言(5)。经过广泛的研究与试验,我们得出结论:Go 编程语言最适合我们的目标。
Go 不需要额外的库来处理并发,因为它已是编程语言特性的一部分,并且 Go 对多处理有强力支持。此外,与 C 相比,Go 是更具生产力的语言,并内置了多种有用的数据结构,如 map(23)与 slice(24)。尤其在处理并发时,借助 Go 特有的 goroutine(21)与 channel 等特性,许多先进的实用方案能轻松运用于现代硬件。goroutine 是一个与其他 goroutine 在同一地址空间中并发执行的函数。它轻量,并通过 channel(22)与其他 goroutine 通信。由于 Go 是一门非常简洁、垃圾回收、静态类型的语言,源代码可以写得错误更少,bug 也理应更少。此外,对速度与内存泄漏做性能分析相对容易,这在处理生产源代码时十分便利。语法上,它松散地派生自 C,并受到 Python(3)等其他语言的影响。另外,Go 拥有数量庞大的库(2),最后,Go 是 BSD 许可的完全开源语言(4)。
在内容过滤的语境下,准确检测一个句子的含义对自动化工具而言是项艰巨任务。如我们所知,人类在没有详细信息时也无法判断句子的真实含义。然而在现实世界中,内容过滤器试图基于字符串匹配技术,将 Web 内容分类为应屏蔽的不良内容或应放行的良好内容(6)。在多数语言中,精确字符串匹配技术(模式匹配)可能需要很高的处理能力(7)。出于这一原因,我们选择开发内容过滤软件来利用 Go 的性能。
为什么选 FreeBSD 作为平台?我们选择 FreeBSD 操作系统作为主要开发与测试平台,主要因为:
它是最强大、最成熟、最稳定的操作系统之一,拥有完整、可靠、自洽的发行版。
FreeBSD 的网络栈坚实而快速(8)。
选择 FreeBSD 的优势之一是其 Port 与 package 系统,使安装和部署所需的应用程序和软件十分便捷。
存在 NanoBSD 等便利工具,可用于轻松制作定制的 FreeBSD 镜像。
最后,我们热爱 FreeBSD。
我们还使用了以下开源软件:
goproxy 为 Go 提供可定制的 HTTP 代理库。它支持常规 HTTP 代理、通过 CONNECT 的 HTTPS、用“中间人”式攻击“劫持”HTTPS 连接。该代理的定位是在可承受相当流量的同时保持可定制与可编程(9)。
gcvis 实时可视化 Go 程序的 gctrace 数据(10)。
profile 是 Go 的简单性能分析支持包(11)。
go-nude 用 Go 进行裸露检测(12)。
xxhash-go 是 C xxhash 的 Go 封装——一种极快的哈希算法,速度接近内存极限(13)。
powerwalk 是一个 Go 包,用于遍历文件并并发调用用户代码处理每个文件(14)。
redigo 是 Redis 数据库的 Go 客户端(15)。
Redis 是一款开源、BSD 许可的高级键值缓存与存储(16)。
挑战
开发过程中我们遇到了几个问题:
Shallalist 黑名单包含超过 180 万条 URL/域名条目。将它们存入内存具有挑战性,起初我们按以下方式将 URL/域名条目存入 Redis:
这在内存利用率和性能方面都不理想。经过一番研究,我们找到了将其缩减到约 4100 个哈希键的方法。我们用 Stephane Bunel 的 xxhash-go 计算每个 URL/域名的哈希并切片,然后将这些切片存入 Redis,类似于:
禁用词与加权词组查找问题:原先它们存储在 Redis 中,在循环中访问既慢又低效。我们用图与 map 改进了这一点。词组列表(禁用、加权等)中存在的每个词都是图的一条边,且在同一类别中应唯一。例如,我们有“sex woman”、“sex man”与“mature sex”这样的禁用词组。Shuultuur 创建四条边——“sex”、“woman”、“man”、“mature”、顶点。出于编程效率的考虑,边及其相关顶点存储在 map 中。Go 提供了实现哈希表的内置 map 类型。此外,我们用 Go 实现的 Boyer Moore 搜索算法替换了基于正则表达式的搜索算法。
将 HTTP 响应体读入字符串会因大量分配导致堆内存占用膨胀,尤其在每秒连接数较高时。理想情况下,这应使用利用 io.Reader 接口的流式解析器来处理。此外,对入站请求限制连接速率也是一种选择。我们通过 CPU 与内存性能分析(19)对其做了优化和改进。具体做法是在 Shuultuur 中启用内存分析,并使用 Go 内置的性能分析器 pprof。下面的报告展示了开发初期阶段的内存分配情况:
在这份初始报告中,你可以看到 main.workerWeighted 与 main.getHkeysLen 中有大量分配。这些函数用于通过 Redis 搜索禁用与加权词组。我们通过移除这些函数、做一些代码层面的优化并引入更好的算法改进了 Shuultuur。下面是完成上述改进后由同一命令生成的报告,我们认为仍有进一步改进的空间。
下面是开发初期展示 CPU 使用情况的 top 报告,使用率非常高。
如你所见,下面的 top 报告显示优化禁用与加权词组搜索后 CPU 使用率大幅降低。
我们实施了若干其他改进,例如学习 URL/域名,从而不必每次都在 HTTP 响应体中检查禁用与加权词组。学习模式特性按以下方式加入:
入站请求的速率限制再次借助 Redis 实现,如下:
另一项改进是增加将监听器限制为指定数量并发连接的能力:
HTTP 响应中慢速的图像过滤暂时禁用,直到我们找到合适的解决方案。
最后一个主要问题可能与高负载下大量 goroutine 有关,这会导致 CPU 与内存使用率高。目前我们正在调查该问题(17)。
基准测试结果
案例 1
为比较我们实现与现有方案的性能,我们使用了 Dansguardian-2.12.0.3,并在同一环境下测试。我们知道 Dansguardian 通常与 squid 配合使用,因此测试中使用了 3.4.8_2 版本的 squid。我们的内容过滤软件 Shuultuur 用 Go 1.3.2 编写,运行在 FreeBSD/amd64 上。我们使用同一台服务器进行性能测试对比,互联网链路速度为 5Mbps。服务器的技术规格如下:
CPU - Intel(R) Xeon(R) X5670 2.93GHz
内存 - 8192MB
FreeBSD/SMP - 12 CPUs(package(s) x 6 core(s) x 2 SMT threads)
我们使用 FreeBSD 9.2-RELEASE,/etc/sysctl.conf 包含以下内容:
我们还得在 Redis 配置文件中将 tcp-backlog 设置改为较高值。此外,我们用 http_load-14aug2014(并行与速率测试)(18)对 Dansguardian 与 Shuultuur 都做了 HTTP 负载测试。http_load 测试中使用了以下 URL:
上述 URL 中,有的列在 Shallalist 黑名单中,有的包含禁用与加权词组列表中的词组,有的包含大量内容与 javascript,其余 URL 的选择并无特殊原因。HTTP 负载测试使用了以下测试命令:
第一条命令中的选项 -parallel 表示要建立并维持的并发连接数,第二条命令中的选项 -rate 控制每秒发出的请求数,选项 -jitter 让速率变化约 10%,选项 -seconds 表示测试运行的秒数。
基于上述结果,Shuultuur 有其优势与劣势。例如,由于 Shuultuur 仍在开发中,它比 Dansguardian 更频繁地返回 Internal Server Error (500)。另一方面,Shuultuur 返回的成功响应 (200) 多得多。Dansguardian 有一些限制,它返回了 341 次 Service Unavailable (503),超时也更多。在性能方面,平均而言,两项测试中 Shuultuur 的性能在多数情况下都高于 Dansguardian。
案例 2
场景与案例 1 几乎相同,但我们使用了不同的硬件(APU 系统主板)(20),将 Go 升级到 1.4.1,并将互联网链路速度改为 2Mbps。
硬件的技术规格如下:
CPU – AMD G 系列 T40E,1 GHz 双核 Bobcat,支持 64 位,每核 32K 数据 + 32K 指令 + 512K L2 缓存
内存 - 4096MB
在 APU 上,我们使用 FreeBSD 10.1-RELEASE,/etc/sysctl.conf 包含以下内容:
由于硬件较小,我们得在 Redis 配置文件中将 tcp-backlog 设置改为 4096。本案例中,我们也用 http_load-03feb2015(并行与速率测试)(18)对 Dansguardian 与 Shuultuur 做了 HTTP 负载测试。
测试结果与在服务器上所做的测试观察相似。与之前的测试一样,两项测试中 Shuultuur 的性能在多数情况下都高于 Dansguardian。图 3 与图 4 分别展示了 Shuultuur 在速率与并行测试中 http_load 测试的内存使用情况。
测试期间,我们抓取了 Shuultuur 与 Dansguardian 的 top 报告,如下所示。
SHUULTUUR:
DANSGUARDIAN:
如你所见,Shuultuur 工作时系统平均负载,尤其是 CPU 使用率较高。
结论与未来工作
用 Go 开发应用,使用其内置数据结构如 map 与 slice,简单且大多直截了当。我们在几天内就做出了首个可工作的原型。GitHub 等在线源代码仓库中有许多用 Go 编写的开源项目,其中许多对我们的开发很有帮助。
测试结果仅针对两个案例。到目前为止,我们多次进行 HTTP 负载测试,结果一致。我们预期达到首个稳定版本时,结果会好得多。
如前所述,我们的实现缺乏快速稳定的图像检查功能。在未来的工作中,我们将改进图像检查,并必须解决大量 goroutine 的问题。最后,内存使用率与 CPU 负载对嵌入式系统应用是主要问题,我们计划就此做更多研究以稳定资源占用。
Ganbold Tsagaankhuu 是一名自由职业者,从事各种与 FreeBSD 相关的项目。他也在蒙古推广类 Unix 操作系统与开源。他是蒙古 Unix 用户组的创始人之一。1994 年毕业于新西伯利亚国立技术大学(俄罗斯),获硕士学位。他将 FreeBSD 手册翻译成蒙古文,并于 2007 年起为 FreeBSD 项目贡献。2009 年 4 月至 2014 年 7 月,他在当地一家移动运营商工作,负责一个开发软件、管理服务器并改进公司安全的 IT 部门。
Esbold Unurkhaan 是蒙古科技大学(MUST)信息与通信技术学院(SICT)网络与系统安全讲师,位于乌兰巴托。2005 年获杜伊斯堡-埃森大学(德国)博士学位。2001 至 2004 年,他在杜伊斯堡-埃森大学实验数学研究所任研究员,攻读博士学位,论文题为《Secure End-to-End Transport over SCTP–A new security extension for SCTP》。在德国的研究工作结束后,他回到蒙古,在 MUST 的计算机科学与管理学院工作。
Erdenebat Gantumur 是 ESCRYPT Inc. 的安全工程师。2010 年获卡内基梅隆大学信息技术与信息安全理学硕士学位。他在安全领域拥有 10 年以上经验,涵盖网络安全、信息保障、计算机安全与嵌入式数据安全。自 2011 年起,他在 ESCRYPT Inc. 主导并参与多项 V2X、车载与智能电网安全项目。此前,他曾任网络工程师、系统与网络管理员、信息安全管理员。他还共同创立了蒙古首个网络事件响应团队,并担任网络安全分析师。
参考文献
Half a decade with Go. Retrieved from The Go blog: http://blog.golang.org/5years
Retrieved from Go language resources: http://go-lang.cat-v.org/pure-go-libs
Go (programming language). Retrieved from Wikipedia: http://en.wikipedia.org/wiki/Go_%28programming_language%29
The Go programming language. Retrieved from http://golang.org/
Computer Language Benchmarks Game. Retrieved from http://benchmarksgame.alioth.debian.org/
String Matching Algorithms and Their Applicability in Various Applications. (D. G. Nimisha Singla, Ed.) International Journal of Soft Computing and Engineering (IJSCE), 1 (6).
Fast Cache for Your Text: Accelerating Exact Pattern Matching with Feed-Forward Bloom Filters. School of Computer Science. Pittsburgh, Pennsylvania, USA: Carnegie Mellon University.
FreeBSD. Retrieved from https://www.freebsd.org/internet.html
goproxy. (E. Leibovich, Producer) Retrieved from https://github.com/elazarl/goproxy
gcvis. (D. Cheney) Retrieved from https://github.com/davecheney/gcvis
profile. (D. Cheney) Retrieved from https://github.com/davecheney/profile
go-nude. (Koyachi) Retrieved from https://github.com/koyachi/go-nude
xxxhash-go. (S. Bunel) Retrieved from https://bitbucket.org/StephaneBunel/xxhash-go
powerwalk. (Stretchr) Retrieved from https://github.com/stretchr/powerwalk
redigo. (G. Burd) Retrieved from https://github.com/garyburd/redigo
Redis. Retrieved from http://redis.io
HTTP ListenAndServe Goroutines throughput. Retrieved from http://grokbase.com/t/gg/golang-nuts/147b9nb2nq/go-nuts-http-listenandserve-goroutines-throughput
HTTP Load. (ACME Lab) Retrieved from http://acme.com/software/http_load/
Profiling Go programs. Retrieved from https://blog.golang.org/profiling-go-programs
PC engine APU board. http://www.pcengines.ch/apu1d4.htm
Goroutines. Retrieved from https://golang.org/doc/effective_go.html#goroutines
Channels. Retrieved from https://golang.org/doc/effective_go.html#channels
Go maps in action. Retrieved from https://blog.golang.org/go-maps-in-action
Arrays, slices (and strings): The mechanics of ‘append’. Retrieved from https://blog.golang.org/slices
最后更新于