最近从无趣的工作中发现了有趣的事情,工作和业余时间都扑了些精力上去,本待上周末最终的成果出来后再写文章的,无奈事情太多,代码还没写完,二月上旬已过,再不写文章春节就过去了,所以这次程序君先上车,再补票。
事情是这样的:两周前同事催促我升级我之前做的一个轮子 merlin - 见我去年的文章:停下来,歇口气,造轮子。
在那篇文章里我提到了为什么会有需要做这样一个内部的 release 构建工具。自那时起,merlin 为我们内部的几个 elixir service 的 release 保驾护航几个月,总体表现不错。然而,当时在需求和设计上的一些缺陷,导致这款产品有这些问题:
所以我要在下一个版本中,将这些问题解决。初步的考虑是,当构建请求来临时,启动一个强大的 spot instance,处理构建任务,构建完成并上传 S3 后,spot instance 自行了断。构建请求可以是来自 github release message(兼容上一版本),也可以是 API —— 进而,我们可以制作 CLI 工具,让用户在 shell 下对任意 git commit 触发构建。有了粗浅的想法后,我们理一理需求:
http://169.254.169.254/latest/user-data
访问之这个需求算是比较清晰,实现起来也没有什么难点,无非就是时间问题 —— 对于像我们这样的 startup 来说,它是可以立即撸起袖子干活,逢山开路遇水填桥的那种活儿。
merlin 之前的坑是我埋的,这个业务即不性感,也不紧急;backend 的队友们都扑在一些 visibility 高的,光是名字听起来就热血沸腾的项目上,腾不出手,且我也不舍得就这么浪费他们的时间 —— 所以我只能 eat my own dogshit。我这人白天瞎忙,晚上躲懒 —— 除非有什么能戳到 G 点让我不吃不喝不睡觉也要搞的创意,否则像 merlin 这种一眼就从头看到脚,没有太多挑战的项目,激发不出我的小宇宙。于是需求定下,反正也不着急,我就懒懒地,有一搭没一搭地在脑海中想着。
事实证明,这种懒散,而非全力以赴,促成了我更多,更深的思考。有功夫我把整个思考的过程撰写成文,相信对大家也能有小小的启发。
在上面的需求中,merlin 由一个服务被拆成了两个部分:control plane 和 data plane(请饶恕一个曾经的网络工程师对区分路径的这种骨子里的执着)。简单来说,control plane 负责派活和监控,是个 scheduler,类似于老鸨;data plane 负责干活,是一堆 resource,就好像苏小小,柳如是,李师师们。而一个个构建任务,是要完成的 task,就是赵佶,柳永,阮郁等的不期而至。
把 merlin 的需求稍稍泛化一下:
为了符合社会主义核心价值观,我们换个比喻:Control plane 类似于 erlang/OTP 里的 Supervisor;data plane 类似于 GenServer。对于 erlang 不太熟悉的同学可以看我的文章:上帝说:要有一门面向未来的语言,于是有了 erlang。你不必理解代码,但需要理解思想。
然而,erlang/OTP 里的 Supervisor 只负责启动和监控 process,如果要启动和监控 node,有很多问题:
1/2/3 如果解决,4 可以直接通过封装 RPC 解决。
2 我们上文中提过 —— 我们可以通过给新启动的 instance 提供 UserData 来解决 —— 在 AWS 里,当我们启动一个新的 instance,可以预设一些 json 数据进去,本地访问 http://169.254.169.254/latest/user-data
即可获得,因而,我们可以把 cluster 的 cookie,control plane node 的 node name 都放进去,以便于新的节点可以自己加入 cluster。
我们看 1 和 3。最简单解决 1/3 的方法是使用 prebuild AMI —— 把所有相关的,处理 data plane 的软件都烧到 AMI 里,用 request-spot-instance 的 AWS API 创建节点即可。不过,这意味着每次 data plane 的代码改变,我们都要重烧 AMI,即便烧 AMI 的动作 CI 自动化处理了,每次 control plane 还是需要确保使用正确的 AMI 启动 data plane。有些麻烦。
程序员最不爽的就是麻烦。虚心使人进步,麻烦让程序员创新。咋办?我们能不能做个 loader,把一个编译好的 module,甚至一个 release 动态加载到远端的一个 node 上?
bingo!这是一个好问题,而好问题的价值远胜于好的答案。于是大概两周前的一个周末,我写了几百行代码,做了一个初始版本的 ex_loader。见 github: tubitv/ex_loader。代码已开源,MIT license。
ex_loader 让你可以很简单地干这样的事情:
{:ok, module} = ExLoader.load_module("hello.beam", :"awesome-node@awesome.io"):ok = ExLoader.load_release("https://awesome.io/example_complex_app.tar.gz", :"awesome-node@awesome.io")
你即使不理解 elixir 代码,大概也能猜到第一句它将一个本地的 module 加载到同一个 cluster 里的叫 awesome-node@awesome.io 的节点上;第二句,则将一个在某个 website 上的 erlang release,加载到相同的节点上。
Joe Armstrong 曾经在一次会议上开心地谈到过他自己会在 erlang node 上运行很多空的,什么也不做,也不知道该做什么的 process,但当他有需要的时候,让这些 process 加载新的 module,就摇身一变让其成为拥有某种特定功能 process。ex_loader 在此基础上更进一步,你可以开一些空的 erlang node,有需要的时候,让这些 node 加载你想让其运行的 release,使其成为特定功能的 server。
ex_loader 简化了 control plane 往 data plane 发布软件的工作,我们有了一个更好的解决 1 和 3 的方案。然而,我们还没有触及到上文中所提到的 5。
这就是 Overseer,一个新的,类比 Supervisor 的 OTP behavior。我们先看怎么用 Overseer:
local_adapter = {Overseer.Adapters.Local, [prefix: "test_local_"]}opts = [strategy: :simple_one_for_one,max_nodes: 10]release = {:release, OverseerTest.Utils.get_fixture_path("apps/tarball/example_app.tar.gz")}MyOverseer.start_link({local_adapter, release, opts}, name: MyOverseer)MyOverseer.start_child()
定义一个 Overseer 很简单:
defmodule MyOverseer do use Overseer require Logger
def start_link(spec, options) do Overseer.start_link(__MODULE__, spec, options) end
def init(_) do{:ok, %{}} end
def handle_connected(node, state) do Logger.info("node #{node} up: state #{inspect(state)}"){:ok, state} end
def handle_disconnected(node, state) do Logger.info("node #{node} down: state #{inspect(state)}"){:ok, state} end def handle_telemetry(_data, state) do{:ok, state} end
def handle_terminated(_node, state) do{:ok, state} end
def handle_event(_event, _node, state) do{:ok, state} endend
我们大概讲讲 Overseer 干些什么:
下图是大概一周前我手绘的 sequential diagram,当时名字还不叫 Overseer,叫 GenConnector,但基本思路一致:
Overseer 的源码会在这几天完成后释出,敬请期待。
有了 ex_loader 和 Overseer,merlin 剩下要做的事情就简单很多了:把代码库分割成 control plane 和 data plane,control plane 用 Overseer,data plane 沿用之前的代码,稍作修改后我们就有了一个分布式的,可以随意 scale 的构建系统。
最妙的是,ex_loader 和 Overseer 虽为 merlin 而生,却由于不错的抽象程度,能适用于几乎任何 control plane + dynamic data plane 的这种分布式任务处理结构。在我之前的思考中,其实还更进一步,将这个系统设计成了一个叫 Fleet / Carrier / Fighter 结构的分布式系统,Carrier 是 Fleet 的 labor node,Fighter 是 Carrier 的 labor node,类比 Star War 中的帝国舰队。在这个蓝图中,merlin 只是 Fleet 的一个 Carrier 而已(这个估计短期没工夫实现):
好了,不说废话了,我还是抓紧写代码去。提前祝各位叔叔阿姨哥哥姐姐弟弟妹妹,春节快乐!也祝各位同处本命年「伏吟」的小伙伴们,狗年红红火火,不犯太岁!:)