多是因为经验太少,工做中常常会遇到问题,探究和解决问题的过程总想记录一下,因此我写博客常常是问题驱动,首先介绍一下今天要解决的问题:css
咱们在开发过程当中可能会遇到这样的状况:html
如我上篇文章 小时到分钟 - 一步步优化巨量关键词的匹配 中介绍的文本匹配服务,它是消息处理流程中的一环,被多个消息处理进程依赖,每次初始化进程要 6秒
左右时间构造 Trie 树,并且服务读取关键词大文件、使用树组构造 Trie 树,会占用大量(目前设置为 256M
)内存。git
我已经把进程写成了守护进程的形式,让它们长时间执行,虽然不用更多地考虑初始化时间了,但占用内存量巨大的问题没有办法。若是关键词量再大一些,一台机器上面跑十来个消息处理进程后就干不了其余了。github
并且,若是有需求让我把文本匹配服务封装为接口给外部调用呢?咱们知道,web 服务时,每个请求处理进程的生存周期是从受理请求到响应结束,若是每次请求都用大量内存和时间来初始化服务,那接口响应时间和服务器压力可想而知。web
这样,服务形式必需要改变,咱们但愿这个文本匹配这个服务能作到:算法
解决办法也很简单,就是把这个文本匹配的服务抽取出来,单独做为一个守护进程来运行,像一个特殊的服务器,多个“消息处理服务”在有须要时能调用此服务进程。数据库
如今,咱们须要考虑文本匹配服务进程如何与外界通讯,接受匹配请求,响应匹配结果。绕来绕去,问题仍是回到了 进程间通讯
。json
进程间通讯(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法。进程是计算机系统分配资源的最小单位(严格说来是线程)。每一个进程都有本身的一部分独立的系统资源,彼此是隔离的。为了能使不一样的进程互相访问资源并进行协调工做,才有了进程间通讯。服务器
进程间通讯的方式有不少,网上对此介绍的也不少,下面根据文章的需求来分析一下这些方式:网络
FIFO
,它经过一个文件来进行进程间数据交互,但服务于多个进程时,须要添加锁来保证原子性,从而避免写入和读取不对应。固然仍是有完美的方式的,这就是今天的主角 - Unix Domain Sockets
,它能够理解为一种特殊的 Socket,但它不须要通过网络协议栈,不须要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另外一个进程,因此在系统内通讯效率更高。并且免去了网络问题,它也更能保证消息的完整性,既不会丢失也不会顺序错乱。
做为特殊的 Socket,它的建立、调用方式和网络 Socket 同样,一次完整的交互,服务端都要通过create、bind、listen、accept、read、write
,客户端要经过create、connect、write、read
。与普通 Socket 不一样的是它绑定一个系统内的文件,而不是 IP 和端口。
建立代码这里再也不多介绍了,以前的一篇文章 用C写一个web服务器(一) 基础功能 的功能实现
小节里详细介绍了 socket 通讯的具体步骤,C 系的语言都是类似的,很容易理解。
Unix Domain Sockets 真的是进程间通讯的一个重型武器,用它能够快速实现进程间的数据、信息交互,并且不须要锁等繁杂操做,也不用考虑效率,可谓是简单高效。
固然,“重型武器” 的在各类场景下也有适合不适合。Unix Domain Sockets适用于如下场景:
接下来要 show code 了,不过学 PHP 的都知道,PHP 不太适合处理 CPU 密集形的任务,我恰好学了点 Go,一时手痒,就用 Go 实现了下 Trie 树,因此才牵扯到 PHP 和 Go 之间的通讯,有了今天的文章。固然介绍的方法,并不仅适合 PHP 与 Go 通讯,其余语言也能够,至少 C系语言中是通用的。
完整代码见 IPC-GitHub-枕边书,里面还附带了一份随手写的 PHP 版本的 Unix Domain Sockets server 端。
Trie树再也不是今天的主题,这里介绍一下数据结构和须要注意的点。
// trie树结点定义 type Node struct { depth int children map[int32]Node // 用map实现key-value型的 字符-节点 对应 }
须要注意:
append()
函数保存递增的匹配结果时,有可能因为 slice 容量不够而从新分配地址,因此要传入 slice 的地址来保存递增后的匹配结果结果,*result = append(*result, word)
,最后再将递增以后的 slice 地址传回。utf-8
,不用像 PHP 同样判断字符的边界,因此在进行关键词拆散和消息拆散时,直接使用 int32()
方法将关键词和消息都转换为成员为 int32
类型的 slice,匹配过程当中就使用 int32
类型的数字来表明这个中文字符,匹配完成后再使用fmt.Printf("%c", int32)
将其转换为中文。Go 中建立一个 socket 并使用的步骤很是简单,只是 Go 没有异常,判断 error 会比较恶心一点,不知道有没有大神有更好的写法。下面为了精简,把 error 全置空了。
// 建立一个Unix domain soceket socket, _ := net.Listen("unix", "/tmp/keyword_match.sock") // 关闭时删除绑定的文件 defer syscall.Unlink("/tmp/keyword_match.sock") // 无限循环监听和受理客户端请求 for { client, _ := socket.Accept() buf := make([]byte, 1024) data_len, _ := client.Read(buf) data := buf[0:data_len] msg := string(data) matched := trie.Match(tree, msg) response := []byte("[]") // 给响应一个默认值 if len(matched) > 0 { json_str, _ := json.Marshal(matched) response = []byte(string(json_str)) } _, _ = client.Write(response) }
下面是 PHP 实现的客户端:
$msg = "msg"; // 建立 链接 发送消息 接收响应 关闭链接 $socket = socket_create(AF_UNIX, SOCK_STREAM, 0); socket_connect($socket, '/tmp/keyword_match.sock'); socket_send($socket, $msg, strlen($msg), 0); $response = socket_read($socket, 1024); socket_close($socket); // 有值则为匹配成功 if (strlen($response) > 3) { var_dump($response); }
这里总结一下这套设计的效率表现:
纯粹用 Go 进行文本关键词匹配,一千条数据运行一秒多,差很少是 PHP 效率的两倍。不过说好的 8倍效率呢?果真测评都是骗人的。固然,也多是我写法有问题或者 Trie 树不在 Go 的发挥范围以内。而后是 PHP 使用 Unix Domain Socket 调用 Go 服务的耗时,多是进程间复制数据耗时或 PHP 拖了后腿,3秒多一点,跟纯 PHP 脚本差很少。
用 PHP 的都知道,PHP 由于解释型语言的特性和其高度的封装,致使其虽然在开发上速度很快,但是执行与其余语言相比略差。对此,业界的 FB 有 HHVM,PHP7 有 opcache 新特性,听说还要在 PHP8 添加 JIT,用以弥补其先天硬伤。
不过,对于开发者,特别是跟我同样对于效率有执著追求的人来讲,在了解使用 PHP 的新特性以外,本身再掌握一门较高执行效率、开发效率略低的语言,用来写一些高计算量,逻辑单一的代码,与 PHP 互补或许会更好一点。
因而,在考虑良久,也见识了各类 Go 的支持者和反对者之间的撕逼后,我以为仍是要相信一下谷歌爸爸,毕竟也没什么其余我以为可选的语言了。PS:请不要针对这一段发表意见,谢谢:)
另外C呢,虽然暂时开发中用不到,但是毕竟是当代N多语言的起源,偶尔写写数据结构、算法什么的以避免生锈。并且学了些C,从 PHP 到 Go,切换起来还略有些驾轻就熟的感受~
关于本文有什么问题能够在下面留言交流,若是您以为本文对您有帮助,能够点击下面的 推荐 支持一下我。博客一直在更新,欢迎 关注 。