关于OCR项目的流水账

最近一直在开发某个 OCR 项目:底层用的是 ABBYY 提供的 FineReader 引擎,应用层把 FineReader 包装成 gRPC 对外提供服务,因为 FineReader 项目是 C++ 实现的,而我们团队使用的编程语言是 Golang,所以二者间通过 CGO 来完成交互。整个项目没有什么特殊的需求,只是鉴于 OCR 耗时较长,为了提升产品体验,要求在处理过程中:客户端可以主动退出;服务端能够实时返回已处理百分比。下面是根据需求画出来的流程图:

流程图

流程图

看上去很简单,不过我还是遇到不少问题,虽然这些问题主要都是一些细枝末节,基本上和 OCR 没什么关系,但是对别的项目还是会有所帮助的,下面让我一一道来。

继续阅读

实战CGO

某项目要集成 PDF 文件的 OCR 功能,不过由于此功能技术难度太大,网络上找不到靠谱的开源实现,最终不得不选择 ABBYY FineReader Engine 的付费服务。可惜 ABBYY 只提供了 C++ 和 Java 两种编程语言的 SDK,而我们的项目采用的编程语言是 Golang,此时通常的集成方法是使用 C++ 或 Java 实现一个服务,然后在 Golang 项目里通过 RPC 调用服务,不过如此一来明显增加了系统的复杂度,好在 Golang 支持 CGO,让我们可以很方便的在 Golang 中使用 C 模块,本文总结了我在学习 CGO 过程中的心得体会。

继续阅读

浅谈pprof

对于大多数 Gopher 而言,一般平时最主要的工作内容除了实现各种无聊的业务逻辑之外,剩下的就是解决各种琐碎的问题。比如:查询性能瓶颈在哪里?查询内存泄漏在哪里?好在 pprof 是处理此类问题的利器,共有两套标准库,分别适用于不同的场景:

命令行工具「go test」就包含了 runtime/pprof,相关参数请参考「go help testflag」:

shell> go test -cpuprofile cpu.out -memprofile mem.out -bench .

不过和 runtime/pprof 相比,更常用的是 net/http/pprof,接下来我们主要通过它来解决一些常见问题,想要激活 net/http/pprof 的话很简单,只要导入对应的包并启动服务即可:

import _ "net/http/pprof"

func main() {
	_ = http.ListenAndServe("localhost:6060", nil)
}

需要注意的是,千万别让外网访问到 pprof,否则可能会导致出现安全问题。有兴趣的读者可以尝试通过 google 搜索「intitle:/debug/pprof/ inurl:/debug/pprof/」看看反面例子。

继续阅读

浅谈NATS消息系统

我用过很多消息系统,比如:简单的 Redis Streams;高效的 Kafaka 等等,不过自从我把编程语言切换到 Golang 以后,总觉得必须找个用 Golang 开发的消息系统才配得上门当户对,原本我已经和小家碧玉的 NSQ 厮守终生,不过当我认识了上流社会 CNCF 钦定的大家闺秀 NATS 后,刹那间就仿佛徐志摩遇到了林徽因,扭头就给结发妻子写了休书。

继续阅读

浅谈微服务

虽说微服务早已是一个老生常谈的话题了,在 infoq 或者 thoughtworks 上可以找到很多案例,不过可惜的是其中相当比例的案例是失败的案例,究其原因,除了技术门槛之外,主要是因为很多人脱离了实际情况,只是为了微服务而微服务。本文通过一个例子带领大家从头到尾体验一下微服务的演化过程,不仅要做到知其然,更要做到知其所以然。

继续阅读

实战Prometheus

最近手头的项目开始从 PHP,Lua 迁移到 Golang,心想正好趁此机会夯实监控,提到 Golang 的监控,不得不说 prometheus 已经是标配,在 Golang 里集成起来非常简单:

package main

import (
        "net/http"

        "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
        http.Handle("/metrics", promhttp.Handler())
        http.ListenAndServe(":6060", nil)
}

如果想在本地 docker 里部署 prometheus,那么只需一条 docker run 命令:

shell> docker run -p 9090:9090 prom/prometheus

运行后打开浏览器浏览 http://localhost:9090/targets 即可,这里显示了相关的监控信息,缺省情况下,监控了 prometheus 本身。

虽然在本地 docker 里部署非常简单,但是如果想在 kubenetes 里部署的话却是另一番经景象了,加之官方文档语焉不详,以至于我几次想中途而废,还好最后坚持下来了,本文记录了我在部署过程中遇到的一些坑坑洼洼以及解决方法。

继续阅读

遭遇lj_str_new

话说前几天我刚通过 mlcache 优化了热数据的问题,屁股还没坐热乎呢,就发现系统性能又下降了,本着自己挖的坑含泪也要填上的原则,我再一次开始了性能调优之旅。

对某个 nginx 进程执行 perf top

对某个 nginx 进程执行 perf top

毫无疑问,从 perf top 结果来看,lj_str_new 已经成为了性能最大的短板。不过我们还是要搞一个 lua 语言级别的火焰图看着才靠谱,于是有了下图:

优化前的火焰图

优化前的火焰图

不出所料,lj_str_new 非常宽,不过没有更详细的调用栈信息,不方便判断问题到底出在哪,而且我的代码在优化前并没有遇到类似的问题,到底 lj_str_new 是什么玩意?还得从 lua 中的字符串说起,引自「如何编写正确且高效的 OpenResty 应用」:

Lua 跟其他大部分语言有一点不一样,就是它的字符串是不可变的。需要对实际的字符串内容做 hash,然后用它查找该内容是否已经创建了对应的实例。既然说到做 hash,那么自然得提到 hash 碰撞。对于那些 hash 值一样的字符串,LuaJIT 把它们存储在链表里。如果许多字符串有着一样的 hash 值,那么这个链表就会很长,原来 O(1) 的开销会退化为 O(n)。这就是所谓的 hash 碰撞。不幸的是,LuaJIT 的默认的字符串 hash 函数就有这样的问题。

看到这里,我已经猜到了问题的原因是我错误的使用了 mlcache,前面提到我通过 mlcache 优化了热数据的问题,实际上当时我为了多缓存一些数据,把 lru_size 设置为了一百万,可这一百万个 key 就是一百万个字符串啊,可想而知随着字符串越来越多,hash 碰撞就会越来越严重,也难怪火焰图中看不到 lj_str_new 详细的调用栈信息,因为任何一个字符串操作都可能有问题,系统性能必然下降。

解决方法很简单,把 lru_size 改小点儿,本例中我设置为 1000,只缓存最热的数据:

优化后的火焰图

优化后的火焰图

仔细对比优化前后两张火焰图,你会发现 lj_str_new 几乎看不到了,收工。