工程师手记-将多进程后台服务改造为多线程

背景

2016年秋,部门计划将移动广告引擎和新的移动DSP引擎做架构融合。保留原来的移动广告引擎的业务逻辑,将其移植到新的框架当中去。

新框架有很多特点,其中之一是所有模块都使用了多线程模型而老的移动广告引擎的一个模块则使用了多进程模型。

改造注意点

  • 临界资源的共享
  • 单例资源
  • 内存的共享

改造的陷阱

改造方法

在多进程模型中,单例模式可以安全地被使用。但是在多线程环境中,则要考虑多线程都要抢占单例类,单例类会成为瓶颈,而且还有可能出现线程不安全的问题。

解决方法:

将多进程的单例类,改造成进程体里多例模式,但是在每个线程体内单例。具体方法参考线程安全的单例模式

改造结果

改造成功,并且顺利上线,正常服务。

改造后带来的好处和坏处

  • 性能的提升
  • 内存

Web服务压力测试工具BullBench

什么是 BullBench ? (what)

  • 一个可以对web服务进行压力测试的工具
  • 最大特点,BullBench 可以读取 nginx access log 中请求,并将其发送给web服务。
  • 也可以读取自定义文件,使用正则匹配和替换,定制请求,发送给web服务。
  • 可以同时模拟15000并发度请求(具体数值受限于系统配置)
  • 和bullbench类似的软件有 webbench、tcpcopy、jmeter等,与它们相比,bullbench有自己独特的地方,比webbench功能多,比tcpcopy操作简单,比jmeter编写测试用例的学习成本更低。

代码托管地址 : (where)

https://github.com/zuocheng-liu/BullBench

为何编写这个软件? (why)

主要原因是,我曾经在查找一个系统内存泄漏问题时,遇到了一些阻碍:

  • 线下压力测试时,要尽可能模拟线上请求
  • 理想方案是使用tcpcopy,复制线上流量,但是目前没有现成的tcpcopy环境

为了克服这些阻碍,就写一个简单的工具,先是读取nginx access log, 提取请求uri,然后模拟1000个客户端发送给Web后台。

这个工具经过再完善和变得更通用之后,bullbench诞生了。

什么时候用呢? 以及软件的局限有哪些。(when)

使用场景:

  • 压力测试时
  • 追踪bug,复现线上问题

局限:

  • 如果没有请求日志,无法使用bullbench进行压力测试
  • 只能模拟发送HTTP GET请求
  • 不能够处理 HTTP 3XX 重定向服务
  • 其他

作者联系方式 (who):

如何编译? (how )

进入源代码目录,执行make

如何使用?

执行 ./bin/bullbench -h 有详细说明和使用实例

    BoolBench 1.0

    -f <filename> 包含请求的文件,可以是nginx access log,也可以是自定义的数据文件
    -t <num>      请求文件的类型, 1 是 nginx access log, 0 其他 其他, 默认是 1
    -u <url>      请求的url前缀, 不支持 https, 比如 http://www.bullsoft.org
    -H <host>     HTTP请求头字段Host, 默认是 NULL
    -c <num>      并发请求的线程数, 默认是 1000
    -r <regex>    正则表达式,用于提取请求文件中特定的内容. 必须和参数'-t 0' 搭配。
    -o <string>   正则变量拼接后的字符串, 支持 $0-$9
    -h            显示帮助信息
实例1:  ./bullbench -f /var/log/nginx/access.log -u http://127.0.0.1:8080
实例2:  ./bullbench -f /var/log/nginx/access.log -u http://127.0.0.1:8080 -H www.bullsoft.org
实例3:  ./bullbench -f /var/log/nginx/access.log -u http://127.0.0.1:8080 -t 0 -r "[a-z]*([0-9]+)([a-z]*)" -o "/display?a=\$1&b=\$2"

一些参数(how much? how many?)

并发线程数设置多少,依据系统情况而定。

  • 在并发线程数过高的情况下,会打开非常多的连接,常会遇到打开文件数过多的错误
  • 使用root通过ulimit设置 open files的限制,使之变大

工程师手记-将Redis异步网络框架移植至Memcached

Idea 的提出

  • Redis 有其高效的异步网络框架
  • Memcached 有其高效的内存管理机制

将这两者结合在一起后,会如何呢?

本篇博客的姊妹篇链接: 《工程师手记-将Memcached内存管理机制移植至Redis》

调研和选型

Memcached 的几个缺点:

  • 使用封装较多的 libevent 异步库
  • Memcached 1.2.2 版本后,开始使用多线程,而多线程上下文切换、互斥锁的竞争带来了一定的性能开销
  • 每次新建tcp 连接都进行系统调用(malloc)申请空间

对Memcached 的一项性能测试

实验验证,多线程不会对Memcached带来性能的提高

参考链接:

对Memcached 1.2.2 的一次基准测试实验

软件选型

  • 从 Redis 3.2.0 (截止此文最新稳定版) 版本中选择 libae、 libanet (ae.h、ae.c、anet.h、anet.c 四个文件)
  • 选择 Memcached 1.2.0 作为移植受体

选取这两个版本的主要原因是,作为Redis 和 memcached 的早期版本,特性较少,代码复杂度低,适合进行初步实验移植。

为什么不直接使用epoll呢? 因为除了异步框架之外,还需要定时器的功能,而redis的异步库中已经有现成的实现。

代码托管地址

给新的memcached起了一个新名字ae-memcached,源代码托管于Github上:

https://github.com/zuocheng-liu/ae-memcached

性能测试实验

硬件

  • Memcached 服务端 GenuineIntel 6 Common KVM processor 6 核 2.0GHZ 4G 内存
  • Memcache 客户端 和服务端相同的另外一台服务器

测试方法

  • 分别运行Memcached 和 ae-memcached, 参数为 memcached -m 1024M -t 6 , 其中原本memcached运行6个线程,而ae-memcached 是单线程
  • 使用memslap 1.0.2, 测试10W次请求,100的并发度的情况下,memcached的处理时间
  • 测试命令 ./memslap –servers=test-server:11211 –concurrency=100 –execute-number=1000 –tcp-nodelay –non-blocking

测试结果

  • ae-memcached 6.709 / 6.878 / 7.362 / 7.196 (seconds) 平均7秒
  • 原版 memcached 5.079 / 5.043 / 5.069 / 5.206 (seconds) 平均5秒多

结论

原版Memcache 处理10W条数据的时间要比 ae-memcached少很多。多线程的确会给Memcached带来性能的提升.我们前面的假想并不正确。

对 Memcached 的持续优化

本文作者不仅只把Redis的异步库移植至Memcached,还对Memcached进行了持续的优化,详细请阅下面链接:

http://it.zuocheng.net/ae-memcached-optimization-zh

工程师手记-从PHP工程师到C/C++工程师的转变

本文分享一下,博主从PHP工程师变为C/C++工程师后的一些真实感受。

两句话写在前面

  • 编程语言只是工具
  • 工程师应该用正确的工具干活儿

但从我现在的经历和感受看,两句话真的不靠谱。

简单介绍转变的过程

2012年大学毕业,进入第一家公司,担任PHPer。之后两三年用的编程语言一直是PHP,即使14年中间换过工作,依然写PHP,做的都是公司后台的业务系统。

2015年,语言转为C/C++,负责公司商业广告投放系统检索端的开发。

技术方向的不同

  • PHP 关注业务的实现
  • C/C++ 关注底层

PHP程序员和C/C++程序员日常技术关注点

  • PHP 程序员关注对业务的抽象、关注系统的可扩展性,关注如何设计业务架构,使用什么样的设计模式等等

  • C/C++ 程序员更加关注系统的性能,系统的可伸缩性,关注使用什么样的工具提高性能,关注网络IO模型,数据结构和算法等等。

安全性

  • PHP 程序员关注的安全主要是避免系统漏洞和防止攻击,sql注入、XSS攻击、文件注入攻击等等, 系统安全比较重要。

  • C/C++ 程序员更关注逻辑的安全性, 比如输入输出的边界,异常处理等等,系统稳定最为重要。

PHPer 和 C/C++ 工程师何时相互欣赏?

  • PHPer 看 C/C++, C/C++ 好高级啊,底层实现都知道的那么清楚。我们了解就没有那么深啊。

  • C/C++工程师看PHP, 你们做的那么多应用好高级啊,那么复杂的金融系统,你们怎么就做出来了,如果用C来写,怎么才能写出来呀!

PHPer 和 C/C++ 工程师何时相互鄙视?

  • PHPer 看 C/C++, 你们代码怎么都是面向过程的,可读性那么低,多不好维护,抽象和复用都搞不好,软件架构乱成啥了,软件稍微大些,你们就要拆成服务。

  • C++ 工程师 看 PHP, 你们设计这么多类,代码那么啰嗦,难道不是不过度设计吗?你们写的代码性能能行吗?明明10台服务器就能满足的PV,竟然要200台!

同时有了PHP 和 C/C++ 背景之后,我的工作变成什么样子了?(以下故事基于事实,但与事实略有差异)

  • 老板:“那个项目是PHP的,你来接吧。” 我:“NO”。 老板:“只能你来接啊,我们辛亏有你啊,组里只有你会php啊,您一定要接啊!” 我:“OK。”(幽怨+无奈)

  • 猎头:“我这有PHP职位,薪水丰厚”。 我:“俺转C++了”。 猎头:“C++需求少啊,工作不好找啊”。 我:“俺还要写c++”。 猎头:“看那个PHP职位,背景待遇好”。我:“……”。

  • 某一线公司HR :“你C++履历太短,暂时还不适合我们的职位。” 我:”……”(心里流出两行泪)。HR:”但我们公司有PHP职位啊,你来呀”。我:“俺不去了……”(秋风吹皱偶滴心)。

博客写完了, 现在感觉,开头那两句话,说的无比正确。

工程师手记-PNI

PNI 简介

PNI,是PHP Native Interface的简写。它是PHP的一个扩展,可以通过它,让PHP直接调用C语言写的函数。

想法源自在百度做项目时,不时地会面临同样一个问题,PHP该如何直接快速地与其他语言交互呢?

PHP 扩展的不足

用C写PHP的扩展是常规的方法。不过使用这种方法总是要面临诸多问题:

  • 作为开发者,要去学习PHP-API、Zend-API,这是不小的学习成本,而且用C做开发测试的效率都不高。
  • 写好了PHP扩展,怎么部署呢,还要找运维工程师去讨论、去争取后才能上线。

来来回回项目进度就被拖慢了。

有如此技术痛点,就找个通用的法子吧。

调研过程

JNI 和 Python

Java 可以通过JNI调用C/C++,Python也有相应的包,比如ctypes

HHVM

稍微去翻了一下HHVM的扩展,发现HHVM有 native interface, 但稍微看了一下,却发现那只是HHVM的native interface,PHP无法使用。

这个项目,模仿JNI,就取名PNI吧

c 对动态链接库的动态调用

c 加载动态链接库及调用函数有现成的方法。

使用dlfcn.h 库中的dlopen、dlsym、dlclose三个函数就够了。

设计和实现过程

接口设计

最开始的方案,就是模仿JNI。因此在最初的实现里,PNI对动态链接库的查询,调用函数的方法都直接模仿了JNI的接口实现。测试使用时,却感觉PNI的接口非常不友好。

和Java、Python 不同,PHP是弱类型语言。JNI 可以在给函数传递参数时,参数的数据类型是已知的,但是PHP传的都是zval,类型并不可知。我把数据类型的控制交给了开发者,在PNI的扩展里,通过struct zval 中 type 字段判断,此种方案让PNI很难于被使用。

于是改进方案,在PNI里添加PNIInteger、PNIDouble、PNIChar和其他几个可以标明参数数据类型的类。

参数压栈

PHP 的函数调用后,动过dlfcn.h库,可以找到函数地址,但是如何调用函数呢?调用函数又如何传参呢?我们无法知道所调用函数的参数列表是什么样子。

由于没有找到比较好的方案,所以就借助于汇编,使用汇编模式C语言的参数压栈方法。

于是写了很多种C语言的函数,主要是参数列表不一样,总结了GCC编译器在x86_64架构CPU下参数压栈的几个特点:

  • 8、16、32位和64位整形的参数,按顺序传到64位的寄存器,当多于6个整形参数时,剩下的整形参数都压栈

  • 32位和64位浮点,都传到64位的浮点计算器,浮点参数多于6个时,剩下的浮点参数都无效

无论C函数的参数是什么样的类型,PNI都将其按64位的整形或浮点处理。

问题解决了

系统兼容性

因为参数压栈的问题,目前PNI只支持GCC编译器和x86_64架构的CPU。其他架构和编译器都没有来得及去实现。

总结

吭哧吭哧,PNI最初版本调试测试成功。

后记

如何让PNI被更多的人知道并使用,怎么做呢?(还在思考中)

  • 开源,传到GitHub
  • 融入社区
  • 在问答网站上多回答相关问题

其他方法呢?再想一下。

工程师手记-赶集易洗车后台和服务端

项目简介

赶集上门洗车业务服务器端,分为两部分:

  • 为洗车APP提供服务的接口
  • 洗车业务相关的管理后台

从整个项目上说,虽是个创业项目,但需求针对性强,从洗车仅一点上切入,易于发力。

相比曾经经历一些创业项目中,产品总是摸不住方向,不知何处发力,试错成本太高,做到最后都迷茫该做什么,怎么做。还有些项目,铺开面去搞,结果摊子太大,顾及不暇。

赶集易洗车与之相比要好太多,对我来说,做这个项目感觉很舒服。负责时间:2014-11 至 2015-08

框架代码

主要开发语言是PHP。典型 LNMP 架构。

代码框架是赶集网多年积累下来的老框架,对于老框架,工程师都是能够理解的,不过还是想说一说。

  • 简易实现的路由分发
  • 没有清晰的分层概念
  • 有一套简单的DB访问工具类
  • 对其他架构系统调用封装访问很全面
  • 赶集所有频道都使用一套框架代码

总之,有优有劣。

业务代码

虽然使用了类,但仍然使用了面向过程的思维在开发。代码复用性、可扩展性比较低。由于业务简单代码耦合还不是很严重。

代码没有分层也是问题。

项目发展过程

框架的改进

赶集易洗车框架和其他MVC框架的诟病一样,表现层逻辑直接调用数据访问逻辑,并没有业务逻辑层。于是劈出一个目录,存放业务逻辑层代码。建立规范约束DB层和表现层,老的业务逻辑代码日后慢慢迁出和重构。

面向对象

老逻辑中类的作用只被用于聚合。多态和继承并没有被使用。

对DB层的再次封装

在DB访问类,由于历史原因,用起来特别不友好,也有让新人不易察觉的逻辑陷阱。

对此,我使用了Service Locator Pattern + Proxy Pattern 这两个设计模式,对原DB层再次进行封装,简化上层对DB层的调用,添加缓存的优化。之所以使用这个方案,主要是根据项目当前情况,有如下考虑:

  • 不能抛弃和重构原有的DB访问类, 因此选择再其之上再进行封装。遵循开闭原则,不对老逻辑进行改动
  • 原有DB访问类有逻辑陷阱,因此使用Proxy Pattern,把陷阱的规避放到proxy类中,优化对外的接口
  • 原有DB类,每一个数据表都对应一个DB类,全局只需初始化一次,但会在不同的地方多次调用,所以选用Service Locator Pattern,每个DB类,初始化1次便缓存起来,供之后逻辑重复使用

用了上面的方案后,代码可维护性和可读性都提高了。

展示列表的封装

后台有很多的展示列表页面,主键往往是一样的,比如都是订单的ID,但是其他的列,有的一样,有的不一样。可以看成,不同的列表页面,就是不同的列的组合。

使用Decorator Pattern 对其进行重构。这个设计模式比较简单,但是实在是特别好。在我之后的开发工作中,凡是遇到列表,开发的时间估计就能缩小一半。维护的成本则是更低。

具体业务开发

具体业务的开发,也许是最枯燥的工作,不具有挑战性,只能付出劳力。估计只有在机器人程序猿出现后才会解决这个问题。

关于团队

团队每周有分享,可怜创业项目排期紧张,团队分享没有坚持几期,后来慢慢停掉。原因我认为可以从两个方面看:

  • 随着业务压力的增大,团队给予技术成长上的提升关注也会消弱。
  • 人有惰性,团队往往也是如此。

我在团队分享3次,《地理区域和点关系计算》、《代理和反向代理》、《PNI》 , 分享积分是团队最高的,本来有一部kiddle做奖品,团队解散后也没去要过来,哈哈。

开始写工程师手记

从12年4月大学毕业开始实习,至今工作3年半经验。

回忆这三年半,这些经验都给我带来了什么呢?不容置疑,有很多东西,但是具体有哪些呢?不好好收拾一下脑海,真就不能立刻说出来。

所以昨晚上班路上就想出了写工程手记这么一个idae,有了手记,我就不会对做过的项目有种“彻底忘记”的感觉了。自己做过什么,一看手记立刻就能回忆起来,曾经的经验、走过的陷阱、当时的思考,都可以立刻回到自己的身上。

通过工程手记,还可以记录在做项目过程中的所感和所想,以后也可以拿来回顾和反思,说不定能得到更多的东西。

总之,就是让自己以往的经历,不仅仅只是流逝的时间,只是过往的履历,一定要变成有价值的经验。

手记的内容

  • 项目简介 (what 、 when 、who)
  • 所遇问题 (why)
  • 如何做的呢? (how)
  • 总结和反思

给自己的手记制定几点原则

  • 原公司保密的东西不以泄露
  • 从客观角度描述问题,尽可能不夹带个人感情倾向
  • 不随意否定项目、团队或个人
  • 内容保持精简,不谈无关话题

写手记的想法是怎么来的呢?

主要是源于两件事吧。

刚玩微博时,关注了易胜华律师,从他发的内容里知道他有写律师手记的习惯。他的手记里,都记录着每次办案的一些重要或关键的细节,也包括他思考的过程。看了后深感佩服,不愧是名律师啊。

在大学时,曾是学校越野队队员,队里规定每次比赛完必须要写比赛总结。总结的目的很明确,就是经过每次的比赛,都能让队员有所提高。

所以我们软件研发工程师为什么不能有手记呢?