loop in codes

Kevin Lynx BLOG

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如何处理类加载问题,这也是隔离容器的主要内容。

Java GC总结

Java GC相关的文章有很多,本文只做概要性总结,主要内容来源于<深入理解Java虚拟机>。

对象存活性判定

对象存活性判定用于确定一个对象是死是活,死掉的对象则需要被垃圾回收。主要包括的方法:

  • 引用计数
  • 可达性分析

可达性分析的基本思想是:

通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链项链时,则证明此对象是不可用的。

在Java中有很多种类的对象可以作为GC Roots,例如类静态属性引用的对象。

垃圾收集算法

确定了哪些对象是需要回收之后,就可以运用各种垃圾收集算法收集这些对象,例如直接回收内存,或者回收并移动整理内存。

主要包括:

  • 标记清除(Mark-Sweep)算法:首先标记出需要回收的对象,然后统一回收被标记的对象
  • 复制(Copying)算法:将可用内存分块,当一块内存用完后将存活对象复制到其他块,并统一回收不使用的块。Java中新生代对象一般使用该方法
  • 标记整理(Mark-Compact)算法:基本同标记清除,不同的是回收时是把可用对象进行移动,以避免内存碎片问题
  • 分代收集,将内存分区域,不同区域采用不同的算法,例如Java中的新生代及老年代

写了一个分布式名字服务JCM

之前在公司里维护了一个名字服务,这个名字服务日常管理了近4000台机器,有4000个左右的客户端连接上来获取机器信息,由于其基本是一个单点服务,所以某些模块接近瓶颈。后来倒是有重构计划,详细设计做了,代码都写了一部分,结果由于某些原因重构就被终止了。

JCM是我业余时间用Java重写的一个版本,功能上目前只实现了基础功能。由于它是个完全分布式的架构,所以理论上可以横向扩展,大大增强系统的服务能力。

名字服务

在分布式系统中,某个服务为了提升整体服务能力,通常部署了很多实例。这里我把这些提供相同服务的实例统称为集群(cluster),每个实例称为一个节点(Node)。一个应用可能会使用很多cluster,每次访问一个cluster时,就通过名字服务获取该cluster下一个可用的node。那么,名字服务至少需要包含的功能:

  • 根据cluster名字获取可用的node
  • 对管理的所有cluster下的所有node进行健康度的检测,以保证始终返回可用的node

有些名字服务仅对node管理,不参与应用与node间的通信,而有些则可能作为应用与node间的通信转发器。虽然名字服务功能简单,但是要做一个分布式的名字服务还是比较复杂的,因为数据一旦分布式了,就会存在同步、一致性问题的考虑等。

What’s JCM

JCM围绕前面说的名字服务基础功能实现。包含的功能:

  • 管理cluster到node的映射
  • 分布式架构,可水平扩展以实现管理10,000个node的能力,足以管理一般公司的后台服务集群
  • 对每个node进行健康检查,健康检查可基于HTTP协议层的检测或TCP连接检测
  • 持久化cluster/node数据,通过zookeeper保证数据一致性
  • 提供JSON HTTP API管理cluster/node数据,后续可提供Web管理系统
  • 以库的形式提供与server的交互,库本身提供各种负载均衡策略,保证对一个cluster下node的访问达到负载均衡

项目地址git jcm

JCM主要包含两部分:

  • jcm.server,JCM名字服务,需要连接zookeeper以持久化数据
  • jcm.subscriber,客户端库,负责与jcm.server交互,提供包装了负载均衡的API给应用使用

基于servlet实现一个web框架

servlet作为一个web规范,其本身就算做一个web开发框架,但是其web action (响应某个URI的实现)的实现都是基于类的,不是很方便,并且3.0之前的版本还必须通过web.xml配置来增加新的action。servlet中有一个filter的功能,可以配置所有URI的功能都经过filter。我们可以基于filter的功能来实现一个简单的web框架。在这个框架中,主要改进URI action的映射,就像play framework中route的配置:

GET     /hello      com.codemacro.webdemo.test.TestController.hello
GET     /route      com.codemacro.webdemo.test.TestController.route
POST    /hello      com.codemacro.webdemo.test.TestController.sayHello

即把某个URI映射到类接口级别。基于servlet实现web框架的好处不仅实现简单,还能运行在所有支持servlet容器规范的web server上,例如Tomcat、Jetty。

本文提到的web framework demo可以从我的github 上取得:servlet-web-framework-demo

功能

这个web framework URI action部分(或者说URI routing)如同前面描述,action的编写如:

public class TestController extends BaseController {
  // 返回字符串
  public Result index() {
    return ok("hello world");
  }

  // HTTP 404
  public Result code404() {
    return status(404, "not found");
  }

  // 使用JSP模板渲染
  public Result template() {
    String[] langs = new String[] {"c++", "java", "python"};
    return ok(jsp("index.jsp")
        .put("name", "kevin")
        .put("langs",  langs)
        );
  }
}

Java中的反射及Bean容器的实现

编程语言中的反射(Refection)指的是可以在程序运行期动态加载一个类。与之相关的是自省(Introspection),这个指的是程序自己可以获取一个类型的描述信息,例如获取一个类的所有接口定义、一个接口的所有形参。当编程语言有了这些语言特性之后,可以在很大程度上解决代码耦合问题,所以在Java的世界里,可以看到很多库/框架使用了反射技术。

类似Spring的Bean容器实现就是大量运用了反射机制。Bean容器维护了一些Bean对象,简单来说就是一些普通对象。Bean容器可以根据配置创建这些对象,创建时如果这些对象依赖了其他对象,Bean容器还会负责将依赖的对象注入到目标对象中,也就是所谓的依赖注入(dependence injection)。放在模块设计中,又衍生出控制反转(IoC, Inverse of Control)概念,用于描述应用程序在使用一个框架时,不是框架来控制/限制应用程序的架构模式,而是由应用程序来控制框架。

本文就简单描述下Bean容器是如何使用反射来实现的,最终代码参考github ioc-sample

类的动态加载

可以简单地使用Class.forName,传入某个class的完整名:

public Class<?> loadClass(String fullName) throws ClassNotFoundException {
  return Class.forName(fullName);
}

类的加载涉及到class loader,这块内容是可以进一步深化的。加载了类之后就可以创建出类的实例,但还没有完成依赖注入的功能:

Class<?> c = loadClass("com.codemacro.bean.test.Test1");
Object o = c.newInstance();

Drill中实现HTTP storage plugin

Apache Drill可用于大数据的实时分析,引用一段介绍:

受到Google Dremel启发,Apache的Drill项目是对大数据集进行交互式分析的分布式系统。Drill并不会试图取代已有的大数据批处理框架(Big Data batch processing framework),如Hadoop MapReduce或流处理框架(stream processing framework),如S4和Storm。相反,它是要填充现有空白的——对大数据集的实时交互式处理

简单来说,Drill可接收SQL查询语句,然后后端从多个数据源例如HDFS、MongoDB等获取数据并分析产出分析结果。在一次分析中,它可以汇集多个数据源的数据。而且基于分布式的架构,可以支持秒级查询。

Drill在架构上是比较灵活的,它的前端可以不一定是SQL查询语言,后端数据源也可以接入Storage plugin来支持其他数据来源。这里我就实现了一个从HTTP服务获取数据的Storage plugin demo。这个demo可以接入基于GET请求,返回JSON格式的HTTP服务。源码可从我的Github获取:drill-storage-http

例子包括:

select name, length from http.`/e/api:search` where $p=2 and $q='avi'
select name, length from http.`/e/api:search?q=avi&p=2` where length > 0 

实现

要实现一个自己的storage plugin,目前Drill这方面文档几乎没有,只能从已有的其他storage plugin源码入手,例如mongodb的,参考Drill子项目drill-mongo-storage。实现的storage plugin打包为jar放到jars目录,Drill启动时会自动载入,然后web上配置指定类型即可。

主要需要实现的类包括:

AbstractStoragePlugin
StoragePluginConfig
SchemaFactory
BatchCreator
AbstractRecordReader
AbstractGroupScan

无锁有序链表的实现

无锁有序链表可以保证元素的唯一性,使其可用于哈希表的桶,甚至直接作为一个效率不那么高的map。普通链表的无锁实现相对简单点,因为插入元素可以在表头插,而有序链表的插入则是任意位置。

本文主要基于论文High Performance Dynamic Lock-Free Hash Tables实现。

主要问题

链表的主要操作包含insertremove,先简单实现一个版本,就会看到问题所在,以下代码只用作示例:

struct node_t {
        key_t key;
        value_t val;
        node_t *next;
    };

    int l_find(node_t **pred_ptr, node_t **item_ptr, node_t *head, key_t key) {
        node_t *pred = head;
        node_t *item = head->next;
        while (item) {
            int d = KEY_CMP(item->key, key);
            if (d >= 0) {
                *pred_ptr = pred;
                *item_ptr = item;
                return d == 0 ? TRUE : FALSE;
            }
            pred = item;
            item = item->next;
        } 
        *pred_ptr = pred;
        *item_ptr = NULL;
        return FALSE;
    }

    int l_insert(node_t *head, key_t key, value_t val) {
        node_t *pred, *item, *new_item;
        while (TRUE) {
            if (l_find(&pred, &item, head, key)) {
                return FALSE;
            }
            new_item = (node_t*) malloc(sizeof(node_t));
            new_item->key = key;
            new_item->val = val;
            new_item->next = item;
            // A. 如果pred本身被移除了
            if (CAS(&pred->next, item, new_item)) {
                return TRUE;
            }
            free(new_item);
        }
    }

    int l_remove(node_t *head, key_t key) {
        node_t *pred, *item;
        while (TRUE) {
            if (!l_find(&pred, &item, head, key)) {
                return TRUE;
            }
            // B. 如果pred被移除;如果item也被移除
            if (CAS(&pred->next, item, item->next)) {
                haz_free(item);
                return TRUE;
            }
        }
    }