loop in codes

Kevin Lynx BLOG

基于Yarn的分布式应用调度器Slider

Apache Hadoop Map-Reduce 框架为了解决规模增长问题,发展出了yarn。而yarn不仅解决Map-Reduce调度问题,还成为了一个通用的分布式应用调度服务。yarn中的一个创新是把各种不同应用的调度逻辑拆分到了一个称为Application Manager(以下简称AM)的角色中,从而让yarn自己变得更通用,同时解决调度性能问题。Apache Slider就是这其中的一个AM具体实现。但Slider进一步做了通用化,可以用于调度长运行(long-running)的分布式应用。

为了更好地理解Slider/Yarn,需要思考这样一个问题:在不用Slider/Yarn这种自动部署并管理应用的软件时,我们如何在一个网络环境中部署一个分布式应用?

  • 可能需要在目标物理机上创建虚拟容器,指定容器所用的CPU核数、内存数
  • 到容器中下载或复制应用运行所需的所有软件包
  • 可能需要改写应用所需的各种配置
  • 运行应用,输入可能很长的命令行参数

注意这些操作需要在所有需要运行的容器中执行,当然现在也有很多自动部署的工具可以解决这些问题。但是,当应用首次部署运行起来后,继续思考以下问题:

  • 某台机器物理原因关机,对应的应用实例不可服务,如何自动发现故障并迁移该实例
  • 应用有突发流量,需要基于当前运行中的版本做扩容
  • 应用需要更新

架构

看一下yarn的总体架构:

Python协程greenlet实现原理

greenletstackless Python中剥离出来的一个项目,可以作为官方CPython的一个扩展来使用,从而支持Python协程。gevent正是基于greenlet实现。

协程实现原理

实现协程主要是在协程切换时,将协程当前的执行上下文保存到协程关联的context中。在c/c++这种native程序中实现协程,需要将栈内容和CPU各个寄存器的内容保存起来。在Python这种VM中则有些不同。例如,在以下基于greenlet协程的python程序中:

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo():
    bar()

def bar():
    a = 3 + 1
    gr2.switch()

def func():
    pass

gr1 = greenlet(foo)
gr2 = greenlet(func)
gr1.switch()

bargr2.switch切换到gr2时,协程库需要保存gr1协程的执行上下文。这个上下文包括:

  • Python VM的stack
  • Python VM中解释执行的上下文

写了一个棋牌游戏服务器框架

最近业余时间写了一个棋牌游戏服务端框架:pigy。对于棋牌游戏服务端框架,我的定义是:

  • 分布式的
  • 包含网络棋牌游戏中包括登陆、大厅、游戏框架、数据持久化等基础组件
  • 提供具体游戏框架,游戏逻辑程序员可以基于这个框架focus在游戏的开发上

写得差不多的时候,我在网上搜索了下,发现棋牌游戏源码已经烂大街,自己精力有限,也没有心思和动力去研究现有实现的优缺点而做出一个更好的替代。所以我这份实现仅作为一个demo放出来让大家开心下好了。

pigy基于skynet实现。之所以选择skynet是看中其中已经有不少网络游戏基础组件可以使用,结合开发下来稍微花点业余时间就可以完成雏形。除此之外,部分源码也参考/复制了metoo项目。

协程并发模型及使用感受

协程可以简单理解为更轻量的线程,但有很多显著的不同:

  • 不是OS级别的调度单元,通常是编程语言或库实现
  • 可能需要应用层自己切换
  • 由于切换点是可控制的,所以对于CPU资源是非抢占式的
  • 通常用于有大量阻塞操作的应用,例如大量IO

协程与actor模式的实现有一定关系。由于协程本身是应用级的并发调度单元,所以理论上可以大量创建。在协程之上做队列及通信包装,即可得到一个actor框架,例如python-actor

最近1年做了一个python项目。这个项目中利用gevent wsgi对外提供HTTP API,使用gevent greelet来支撑上层应用的开发。当可以使用协程后,编程模型会得到一定简化,例如相对于传统线程池+队列的并发实现,协程可以抛弃这个模型,直接一个协程对应于一个并发任务,例如网络服务中一个协程对应一个socket fd。

但是python毕竟是单核的,这个项目内部虽然有大量IO请求,但随着业务增长,CPU很快就到达了瓶颈。后来改造为多进程结构,将业务单元分散到各个worker进程上。

python gevent中的协议切换是自动的,在遇到阻塞操作后gevent会自动挂起当前协程,并切换到其他需要激活的协程。阻塞操作完成,对应的协程就会处于待激活状态。

在这个项目过程中,我发现协程也存在很多陷阱。

实现一个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执行。