loop in codes

Kevin Lynx BLOG

实现一个memcache proxy

通常我们会使用多台memcached构成一个集群,通过客户端库来实现缓存数据的分片(replica)。这会带来2个主要问题:

  • memcached机器连接数过多
  • 不利于做整体的服务化;缺少可运维性。例如想对接入的客户端做应用级隔离;或者对缓存数据做多区域(机房)的冗余

实现一个memcache proxy,相对于减少连接数来说,主要可以提供更多的扩展性。目前已经存在一些不错的memcache proxy,例如twitter的twemproxy,facebook的mcrouter。稍微调研了下,发现twemproxy虽然轻量,但功能较弱;mcrouter功能齐全,类似多区域多写的需求也满足。处于好玩的目的,之前又读过网络库xnio源码,我还是决定自己实现一个。

Xmemcached源码阅读

Xmemcached 是一个memcached客户端库。由于它提供的是同步API,而我想看下如何增加异步接口。所以就大致浏览了下它的源码。

主要结构

针对memcache客户端的实现,主要结构如下:

  • XMemcachedClient 是应用主要使用的类,所有针对memcache的接口都在这里
  • Command 用于抽象二进制协议或文本协议下各个操作,这里称为Command。CommandFactory 用于创建这些command
  • MemcachedSessionLocator 用于抽象不同的负载均衡策略,或者说数据分布策略。在一个memcached集群中,数据具体存放在哪个replica中,主要就是由这个类的实现具体的,例如KetamaMemcachedSessionLocator 实现了一致性哈希策略
  • MemcachedConnector 包装了网络部分,与每一个memcached建立连接后,就得到一个Session。command的发送都在MemcachedConnector中实现
  • 各个Session类/接口,则涉及到Xmemcached使用的网络库yanf4j。这个库也是Xmemcached作者的。

XNIO源码阅读

XNIO是JBoss的一个IO框架。最开始我想找个lightweight servlet container库,于是看到了undertow,发现其网络部分使用的就是XNIO。所以干脆就先把XNIO的源码读下。

XNIO文档非常匮乏,能找到都是3.0的版本,而且描述也不完全。Git上已经出到3.5.0。我读的是3.3.6.Final。

使用方式

可以参考SimpleEchoServer.java,不过这个例子使用的API已经被deprecated,仅供参考。使用方式大致为:

  • 创建服务,提供acceptListener
  • 在acceptListener中accept新的连接,并注册连接listener
  • 在连接listener回调中完成IO读写

实现JVM中的JIT

在JVM中,JIT (Just-in-Time) 即时编译指的是在Java程序运行过程中JVM优化部分指令为本地指令,从而大幅提升性能。在上一篇文章写一个玩具Java虚拟机中实现了一个基本可以运行Java字节码的JVM。本篇文章描述我是如何在这个玩具JVM中实现JIT的。

推荐文章“How to JIT - an introduction”,介绍了JIT的基本实现原理。作者把JIT分为两个阶段:

  • 运行期生成机器代码(本地指令)
  • 执行机器代码

生成机器代码很好理解,就是一个JVM指令到机器指令的翻译;而执行机器代码,原理上是利用了OS提供了API可以分配可以执行的内存,然后往这块内存中写入机器码,从而实现运行期可以执行动态生成的机器码功能。

我们可以利用这个原理来实现JIT,但是未免太底层了点,需要做很多工作来完成这件事情。我们可以利用libjit来简化实现。这个作者博客里还有些libjit的教程,其中part 1值得阅读。 简单来说,libjit对机器指令做了抽象,利用它的API来描述一个函数包含了哪些指令,实现了什么功能。然后具体的指令生成以及指令执行则交给libjit完成。

例如以下使用libjit的代码:

1
2
3
4
5
6
7
8
// t = u
jit_insn_store(F, t, u); // 类似 mov 指令
// u = v
jit_insn_store(F, u, v);

// v = t % v
jit_value_t rem = jit_insn_rem(F, t, v); // 求余指令
jit_insn_store(F, v, rem);

所以,我们需要做的,就是将JVM的字节码,翻译成一堆libjit的API调用。但是我希望能够稍微做点抽象,我们写个翻译器,能够将JVM这种基于栈的指令,翻译成基于寄存器的指令,才方便后面无论是使用libjit还是直接翻译成机器码。

写一个玩具Java虚拟机

本文描述了一个用Java实现的玩具JVM,用Java实现的好处是可以不用处理JVM中的垃圾回收。

Java虚拟机是基于栈的虚拟机。栈虚拟机的特点是所有临时操作数都存放在栈中。编译器生成的指令都会围绕着这个栈展开,相对而言,解释执行这些指令会比较容易。基于栈的虚拟机可能会生成如下指令:

1
2
3
push 3   # 把立即数3压栈
push 4   # 把立即数4压栈
add      # 从栈中弹出两个操作数进行相加,结果压回栈中

Java .class文件存储的主要就是编译后的指令,一个玩具JVM,简单来说就是解释执行这里面的指令。接下来就说说为了让这个JVM跑起来需要实现哪些功能。

class 文件解析

推荐一下 Java class viewer,里面有个工具可以可视化class文件内容。另外我直接复用了他解析class文件的代码。

class文件描述的信息是以class为单位的,一个类如果有嵌套类,这个嵌套类也会生成为单独的class文件。从c/c++程序员的视角来看,class文件的生成有点类似编译,编译器在编译期间只做依赖符号存在与否的检查。所有引用其他class的地方,不同于c/c++,java class的引用都是在运行期定位的。这里看看一个简单的类class文件结构是怎样的:

RequireJS最简实现

网上有不少解析RequireJS源码的文章,我觉得意义不大。阅读源代码的目的不是为了熟悉代码,而是为了学习核心实现原理。相对RequireJS的源码,kitty.js的实现更简单,更容易理解。本文正是抄了kitty.js的实现,是一个更精简的RequireJS,用于理解RequireJS的实现原理。

github dummy-requirejs。这个实现仅支持核心feature:

1
2
require(deps, callback) // deps 是依赖数组
define(id, deps, factory) // factory是一个函数

例子参考git中rect.js/main.js。

从实现来看,require/define是基本一致的,require的callback等同于define的factory:都会要求deps被加载且被执行,获得deps的exports作为整个module传入callback/factory。不同的是,factory的返回值会被作为define出来的模块的export,可被视为模块本身;而callback返回值被忽略。

从用法来看,define仅是定义了模块,这个模块可能被作为deps被其他模块依赖,但define传入的factory此时是不执行的;而require则会触发各个模块的factory执行。

ReactJS项目中基于webpack实现页面插件

整个Web页面是基于ReactJS的,js打包用的webpack,现在想在Web页面端实现一种插件机制,可以动态载入第三方写的js插件。这个插件有一个约定的入口,插件被载入后调用该入口函数,插件内部实现渲染逻辑。插件的实现也使用了ReactJS,当然理论上也可以不使用。预期的交互关系是这样的:

1
2
3
4
5
6
7
8
9
10
// 主页面
load('/plugin/my-plugin.js', function (plugin) {
    plugin.init($('#plugin-main'), args)
})

// 基于ReactJS的插件
function init($elem, args) {
    ReactDOM.render((<Index />), $elem)
}
export {init}

在主页面上支持这种插件机制,有点类似一个应用市场,主页面作为应用平台,插件就是应用,用户可以在主页面上选用各种插件。

问题

目前主页面里ReactJS被webpack打包进了bundle.js,如果插件也把ReactjS打包进去,最终在载入插件后,浏览器环境中就会初始化两次ReactJS。而ReactJS是不能被初始化多次的。此外,为了插件编写方便,我把一些可重用的组件打包成一个单独的库,让主页面和插件都去依赖。这个库自然也不能把ReactJS打包进来。何况还有很多三方库,例如underscore、ReactDOM最好也能避免重复打包,从而可以去除重复的内容。所以,这里就涉及到如何在webpack中拆分这些库。

需要解决的问题:

  • 拆分三方库,避免打包进bundle.js
  • 动态载入js文件,且能拿到其module,或者至少能知道js什么时候被载入,才能调用其入口函数

一次逆向网页内容加密

最近写一个爬虫要从这个网页爬取内容。以往爬取网页内容复杂点的,一般就是处理下页面内容动态载入,动态载入的内容可能会要求复杂奇怪的参数,或者找到这个动态载入的HTTP接口在哪里麻烦点。但是这个网页不同。类似:

1
<td><span name="record_yijiaof:feiyongzldm" title="pos||"><span id="5d299905633d4aa288b65f5bf74e414c" class="nlkfqirnlfjerldfgzxcyiuro">专</span><span id="546c73d012f74931aa5d45707121eb50" class="nlkfqirnlfjerldfgzxcyiuro">实</span><span id="e0285e05974b4577b23b2ced8e453005" class="nlkfqirnlfjerldfgzxcyiuro">新</span><span id="82b9e003de4e4577aa7617681a0d3777" class="nlkfqirnlfjerldfgzxcyiuro">用</span><span id="417aaf4c6ad14b7781db02a688a4f885" class="nlkfqirnlfjerldfgzxcyiuro">用</span><span id="a3f326efa35e4fe898d2f751e77d6777" class="nlkfqirnlfjerldfgzxcyiuro">新</span><span id="c6c5135b931c48c09c6529735f4c6434" class="nlkfqirnlfjerldfgzxcyiuro">型</span><span id="8c55b119929147ddbe178776903554e5" class="nlkfqirnlfjerldfgzxcyiuro">专</span><span id="f8e47702c9f5420198a6f9b9aa132c9c" class="nlkfqirnlfjerldfgzxcyiuro">利</span><span id="60cc2e23682e4ca2b850a92f55029458" class="nlkfqirnlfjerldfgzxcyiuro">第9年年费</span></span></td>

最终希望得到的内容其实是实用新型专利第9年年费,但是得到的网页确实乱序后的字符串,并且每次刷新得到的乱序还不一样,试过几次也看不出规律。

按照以往的思路,猜测肯定是某个js文件中包含了还原算法,我的目的,就是找出这个算法,在爬虫程序中实现这个算法,以还原出可读的字符串。

js中要完成这样的事,首先得找到网页元素,包括:根据外层span name=record_yijiaof:feiyongzldm;根据再外层的table;根据内层span class='nlkfqirnlfjerldfgzxcyiuro'。以前我一直想要个工具,可以在某网页载入的所有js文件中搜索特定字符串,从而帮助逆向,但是一直没有这个工具。所以这次也只有人肉看每个js。根据js的名字猜测这个逻辑会放在哪里。

记一次线程局部存储与动态库引起的core

线上的服务退出时coredump,显示堆栈为:

google一下发现有人遇到过,产生这个core的条件为:

  • 使用TLS时注册了destructor (pthread_key_create),这个回调函数会在线程退出时被调用
  • 这个destructor符号位于.so中
  • 在线程退出时,这个.so已经被dlclose

我们的程序模型中,类似于一个Web App server,有一个线程池包装了IO处理,将请求派发给应用插件,处理完后回应给客户端。应用插件是一个.so,被动态载入(dlopen),该.so由于实现需要引入了较多的第三方.so(隐式载入)。初步排查时,整个实现是没有问题的,线程池是在.so close前关闭的。

没有线索,于是尝试找到该TLS是哪个模块引入的。通过gdb断pthread_key_create,以及不为空的destructor回调可以确定几个模块,但范围不够小,这些模块基本还是些基础模块,如zookeeper/mxml以及网络模块。

多看了几个core,发现这个回调的偏移地址都是固定的960,如上图中的0x7f0f26c9f960。.so被载入时,基址是会变的,但偏移是不会变的,例如通过nm查看.so中的符号时:

1
2
$nm lib/libsp_kit.so | grep loadConfig
00000000002de170 T _ZN8sp_basic14SortRailConfig10loadConfigEPKc

Java中隔离容器的实现

Java中隔离容器用于隔离各个依赖库环境,解决Jar包冲突问题。

问题

应用App依赖库LibA和LibB,而LibA和LibB又同时依赖LibBase,而LibA和LibB都是其他团队开发的,其中LibA发布了一个重要的修复版本,但是依赖LibBase v2.0,而LibB还没有升级版本,LibBase还不是兼容的,那么此时升级就会面临困难。在生产环境中这种情况往往更恶劣,可能是好几层的间接依赖关系。

隔离容器用于解决这种问题。它把LibA和LibB的环境完全隔离开来,LibBase即使类名完全相同也不互相冲突,使得LibA和LibB的升级互不影响。众所周知,Java中判定两个类是否相同,看的是类名和其对应的class loader,两者同时相同才表示相等。隔离容器正是利用这种特性实现的。

KContainer

这里我实现了一个demo,称为KContainer,源码见github kcontainer。这个container模仿了一些OSGI的东西,这里把LibA和LibB看成是两个bundle,bundle之间是互相隔离的,每个bundle有自己所依赖的第三方库,bundle之间的第三方库完全对外隐藏。bundle可以导出一些类给其他bundle用,bundle可以开启自己的服务。由于是个demo,我只实现关键的部分。

KContainer的目录结构类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
|-- bundle
    |-- test1
        |-- test1.prop
        |-- classes
        |-- lib
            |-- a.jar
            |-- b.jar
    |-- test2
        |-- test2.prop
        |-- classes
|-- lib
    |-- kcontainer.jar
    |-- kcontainer.interface.jar

bundle目录存放了所有会被自动载入的bundle。每一个bundle都有一个配置文件bundle-name.prop,用于描述自己导出哪些类,例如:

1
2
init=com.codemacro.test.B
export-class=com.codemacro.test.Export; com.codemacro.test.Export2

init指定bundle启动时需要调用的类,用户可以在这个类里开启自己的服务;export-class描述需要导出的类列表。bundle之间的所有类都是隔离的,但export-class会被统一放置,作为所有bundle共享的类。后面会描述KContainer如何处理类加载问题,这也是隔离容器的主要内容。