武汉的开发者们注意啦!AI技术战略、框架以及最佳实战尽在Azure OpenAI Day 了解详情
写点什么

深度剖析京东云引擎核心技术

  • 2014-02-28
  • 本文字数:7260 字

    阅读完需:约 24 分钟

京东 PaaS 平台的主要服务对象是两类人群,一类是个人开发者,二类是京东的 ISV。在数据开放平台日益成熟的背景下,他们都希望以最低的成本,方便地部署自己的应用,提高生产力。而京东 PaaS 平台正是以满足开发者和 ISV 的这一需求而开发的。

京东 PaaS 平台的核心是 JAE(Jingdong App Engine),它以 Cloud Foundry 为内核,之所以选择 Cloud Foundry,是因为 Cloud Foundry 是最早开源,在社区里最成熟、最活跃的基础 PaaS 平台。为了给开发者提供更加便捷的服务,JAE 将基础服务云化,接入各种应用组件服务,诸如高可用 MySQL 服务、Redis 缓存集群服务、以及消息队列等;此外,它结合应用开发工具,为开发者提供了类 github 的代码托管服务,云测试和 Java 工程云端编译,以及资源统计信息,让开发者可以更专注于自己的代码业务。再者,JAE 对托管在平台上的应用进行健康监控,支持查看应用日志,提供其它安全服务。让开发者只需关心自己应用代码,而其它一切事情,都由 JAE 为其提供,极大地提高了开发者的效率,降低了开发成本。下图描述了 JAE 与 PaaS 平台用户及其他相关服务之间的关系。

JAE 还根据京东 PaaS 平台的需求,做了许多有针对性的功能扩展。本文主要就 JAE 的核心技术点展开讨论,JAE 的其它基础服务将参见其官方网站

智能路由(Load Balance)

我们知道,Cloud Foundry 支持设置应用的实例个数。但是,当并发量增大时,请求(Request)是否能够均匀地分配给后端的实例?针对多实例的应用,Cloud Foundry 采用随机策略地响应客户端的请求,并不能公平有效地利用实例资源,在并发量峰值时候,存在发生雪崩的可能性。为解决这一潜在问题,JAE 借鉴了 nginx 的路由策略,采用权重(weight)算法,负载越小的实例越有机会响应请求。那么,我们需要进一步解决的问题是:如何计算实例的负载,以及如何在接收请求之后对其进行分流?

下图是 JAE 的模块关系图:

所有请求首先到达 router 模块,router 保存所有实例的路由信息(即实例的 ip 和 port),并由它决定由哪个实例来响应请求。每个实例的 ip 和 port 信息都是 dea 模块经过 nats 消息总线转发给 router 的,其实现原理是:dea 将自身服务器上的所有实例信息发给 nats,router 订阅了这则消息,收到之后保存在路由表中,通过过期失效机制来定期获得最新的实例信息。

为使 router 获得各实例的负载信息。我们对 dea 模块进行改造,在其每次向 nats 发送消息的时候,将实例在这一时刻的负载信息也“捎带”上。dea 模块收集 dea 服务器本身以及所有实例的 CPU 使用率、内存使用率、I/O 等原始信息,一并发送给 router。router 决定如何从这些原始数据中计算出负载值。

至于采用何种算法来计算负载,这是 router 自身的职责范围,我们采用了类似下面的算法:

实例真实负载 = ( vm 负载 * 30% + 实例负载 * 70% ) * 100% vm 负载 = CPU 已使用 % * 30% + Mem 已使用 % * 30% 实例负载 = CPU 已使用 % * 30% + Mem 已使用 % * 30%

在上述算法中,我们在计算实例的负载值时考虑了 dea 的因素,其原因在于 dea 实际就是服务器(虚拟机),而实例运行在 dea 上的各个进程之上。如果某个 dea 的负载很高,而其上的某个实例的负载却很低,此时 router 不一定会将请求交给这个实例。所有算法都要考虑 dea 的感受。

每个应用实例的负载值计算出来之后,如何决定哪个实例可以优先响应客户端请求,JAE 提供了以下几个均衡策略:

复制代码
def load_balance(droplets,lb_policy)
case lb_policy
when "random"
random_policy(droplets)
when "weight"
weight_policy(droplets)
when "round_robin"
round_robin_policy(droplets)
when "fair"
fair_policy(droplets)
else
random_policy(droplets)
end
end

从下面这段代码可以看出,Router 使用了 weight 策略。

复制代码
# Pick a droplet based on original backend addr or pick a droplet randomly
if sticky
_, host, port = Router.decrypt_session_cookie(sticky)
droplet = check_original_droplet(droplets, host, port)
end
# pick a droplet use load balance policy
lb_policy = Router.lb_policy
droplet ||= droplets[rand(droplets.size)][:load_val]?
load_balance(droplets,lb_policy):droplets[rand(droplets.size)]

有状态的(stateful)请求不经过智能路由的处理。比如,当存在 session 时,第一次请求之后,服务器将响应该请求的实例信息回写至客户端的 cookie 中,当 router 收到该客户端的下一次请求时,会将其转发给同一实例。

也许有人会问,这样做是否会影响请求的响应时间?答案是肯定的,但是影响很小,因为该算法是纯数值计算,效率非常高。目前的算法只考虑了几个常用的因素,还存在优化的空间,比如增加负载的因素,如 I/O,实例带宽使用情况等。

弹性伸缩(Auto-scaling)

接续上一话题,当并发量持续增大,通过智能路由可以均衡所有实例的负载,但如何应对实例的负载持续升高,面临应用随时不可用的情况?只有增加实例!虽然,我们可以通过 JAE 控制界面轻松地为一个应用增加或减少实例数(只要在资源满足的情况下)。但这种纯手动的方法显然不可取,JAE 将此过程自动化,所采用的就是弹性伸缩机制。

惯用的方法就是定义伸缩规则,下面这是 JAE 管理页面的规则设置:

规则是用户层面的全局定义,每个用户可以创建多个规则,具体的应用绑定规则之后才能生效。

规则的正确执行依赖于“过去几分钟内,应用的平均请求次数”这一指标。我们通过实时统计来获取这一指标,其实现流程图如下图所示:

所有 router 服务器都安装了 agent,flume 集群实时收集 router 的 nginx 访问日志,保存在 redis 中并定期进行清理,同时将分析结果保存在同一 redis 集群中,规则引擎从 redis 中取得数据,与此应用的规则对比,判断是否触发规则,之后调用 cloudcontroller restful api 来扩展或缩减实例数。

将原始日志以及分析结果传送给云日志和云监控模块,为应用提供相应的功能。 如 dashboard 管理页面上的应用日志查看,检索;应用 PV、UV 监控趋势图等。

智能启动(Auto-loading)

如果有 80% 的应用不活跃,却一直占用着资源,就会造成极大浪费。智能启动的含义是当某个应用在一段时间内未收到请求,则将应用暂时休眠,等下一次请求到达时,立即启动此应用。长时间没有请求的应用,再次访问的时候,会有秒级的加载延迟。

如图所示,智能启动也用到了访问日志的计算结果,计算的是每个应用在统计周期内的访问次数,同样保存在 Redis 集群中。智能启动模块从 CCDB 中过滤取得待处理的应用列表,依次获取 Redis 关于周期内应用的总访问次数,如发现为零则先调用 cc restful api 停止应用,再将 CCDB 中的此应用标识为 sleep 状态,同时通知 Router 更新路由表信息,这样路由表中既有所有正在运行的应用实例信息,又有 sleep 状态的应用信息。当 Router 接收到下一个访问的时候,首先从路由表是查找对应的实例信息,发现此应用处于 sleep 状态,就会激活此应用,并且立刻返回给客户端一个正在加载页面。这样再刷新页面,就可以正常访问应用了。下表从 nats message 来说明模块间的交互:

nats message

publisher

subscriber

description

router.sleep

Auto-loading

Router

智能启动模块将处于休眠状态的应用信息发给 Router,Router 更新自己的路由表

autoload.start

Router

Auto-loading

Router 接收到处于休眠状态应用的访问,返回加载页面的同时要求激活此应用。智能启动模块收到请求后,调用 cc restful api 启动此应用

autoload.update

Cloud Controller

Auto-loading

应用启动成功,CC 将此应用的实例信息,ip、端口等信息发给智能启动模块

router.unsleep

Auto-loading

Router

智能启动模块将启动正常的应用实例信息发给 Router,Router 更新路由表

资源隔离与访问控制

资源隔离是 Cloud Foundry 的精髓,应用在 JAE 上除了各种功能方便开发外,最重要的还是“安全感”了,资源隔离即应用之间的资源相互隔离不干扰,访问控制是指在 JAE 内部,应用之间不能通过任何方式相互访问,不能操作其它应用的代码,但可以通过 HTTP 方式访问其它应用。JAE 在整个过程也做了一些尝试,这里分享一下。

Cloud Foundry 用 warden 来实现资源隔离与访问控制的,但是 JAE 的第一个版资源隔离策略使用了 vcap dev,当时没有 warden。在当时的背景下,Cloud Foundry 官网还未迁移至 v2 版、业内的成功应用也比较少, JAE 采取稳中求进的方案,即在 vcap dev 的基础上,借鉴了 warden 思路,以此来实现资源隔离和访问控制。下面,我们将详细介绍 JAE 的第一版资源隔离实现方法,该方法部署起来所需的资源比较灵活,既支持单机部署也支持多机部署,对个人开发者有很好的借鉴参考。

如上图所示,JAE 第一版资源隔离与访问控制的实现方式是 vcap safemode +cgroup+quota+ACL。首先,vcap safemode 提供了访问控制的功能,安全模式为 dea 服务器创建了 n 个用户,默认是 32 个用户,vap-user-11 至 vap-user-32,属于 vcap-dea 用户组,启动一个应用实例就为其分配一个用户,并将代码目录的拥有者设置为此用户,实例停止则回收用户。这样可以简单地保证应用间的访问控制,不同应用(不同用户)相互不可访问。

vcap safemode 只是设置了应用目录的权限,限制了目录间的访问,但是仍然可以看到或操作大部分系统命令,系统文件,如 ls, mkdir, /usr/bin,/etc/init.d/,这是很危险的。JAE 通过 linux 的 ACL(access control list)将大部分的系统命令都禁掉了,这有点杀敌一千自损八百的味道,很多应用是需要调用系统命令的。ACL 的具体做法是限制了用户组 vcap-dea 对绝大多数系统命令的查看、操作权限:

复制代码
sudo setfacl -m group:vcap-dea:--- /usr/bin/vim
sudo setfacl -m group:vcap-dea:--- /usr/bin/edit
sudo setfacl -m group:vcap-dea:--- /usr/bin/vi
sudo setfacl -m group:vcap-dea:--- /bin/cat
…..

JAE 用 safemode+ACL 实现了某种意义上的访问控制。为什么说是某种意义上的?它虽然提供了一些功能,但是没有 Namespace 的概念,在特定的 Namespace 中,PID、IPC、Network 都是全局性的,每个 Namesapce 里面的资源对其他 Namesapce 都是透明的,而 safemode+ACL 则是一种共享的方案。Namespace 问题也是后来 JAE 升级的主要原因。

其次说到资源隔离,一个应用用到的系统资源大概有内存、CPU、磁盘和带宽等。JAE 借鉴 warden 的方案,使用 linux 内核自带的 cgroup 和 quota 来解决内存、CPU、磁盘的隔离问题。

下面,借此机会介绍 warden 的实现细节。

warden 实现原理

cgroup(Control Group)是 Linux 内核的功能,简单的说,它是对进程进行分组,然后以组为单位进行资源调试与分配,其结构是树形结构,每个 root 管理着自己下面的所有分支,而且分支共享着 root 的资源。由各个子系统控制与监控这些组群。cgroup 的子系统有: CPU、CPUset、CPUacct、memory、devices、blkio、net-cls、freezer,不同的 linux 内核版本,提供的子功能有所差异。

cgroup 的系统目录位于 /sys/fs/cgroup,JAE 宿主机是 ubuntu12.04 LTS,默认的有以下几个子系统:CPU、CPUacct、devices、freezer、memory

当 dea 启动的时候,会重新初始化 cgroup,重新 mount 子系统。

复制代码
cgroup_path=/tmp/container/cgroup
mkdir -p $cgroup_path
if grep "${cgroup_path} " /proc/mounts | cut -d' ' -f3 | grep -q cgroup
then
find $cgroup_path -mindepth 1 -type d | sort | tac | xargs rmdir
umount $cgroup_path
fi
# Mount tmpfs
if ! grep "${cgroup_path} " /proc/mounts | cut -d' ' -f3 | grep -q tmpfs
then
mount -t tmpfs none $cgroup_path
fi
# Mount cgroup subsystems individually
for subsystem in CPU CPUacct devices memory
do
mkdir -p $cgroup_path/$subsystem
if ! grep -q "${cgroup_path}/$subsystem " /proc/mounts
then
mount -t cgroup -o $subsystem none $cgroup_path/$subsystem
fi
done

将 cgroup 系统安装在 /tmp/container/cgroup 下,mount 了 4 个子系统。当部署一个应用时,/tmp/container/cgroup/memory 目录生成此应用的进程节点,命名为#{instance_name}-#{instance_index}-#{instance_id},即“应用名 - 应用实例号 - 实例 id”,将应用的内存配额写入 memory.limit_in_bytes 以及 memory.memsw.limit_in_bytes。限制了可使用的最大内存以及 swap 值。

复制代码
2.times do
["memory.limit_in_bytes", "memory.memsw.limit_in_bytes"].each do |path|
File.open(File.join(cgroup_path(:memory),instance_name, path), 'w') do |f|
f.write(instance[:mem_quota])
end
end
end
[cgroup_path(:CPU), cgroup_path(:CPUacct),cgroup_path(:memory)].each do |path|
File.open(File.join(path,instance_name, "tasks"), 'w') do |f|
f.write(instance[:pid])
end
File.open(File.join(path,instance_name, "notify_on_release"), 'w') do |f|
f.write("1")
end
end

接着将实例的进程 ID 写入各个子系统的 tasks 文件中,注意到每个子模块的 notify_on_release 都设置为 1,这是告诉 cgroup,如果应用消耗的资源超过限制,就 kill 掉进程。Warden 中写了个 OomNotifier 服务来监控内存的消耗情况,然后做具体操作。个人觉得太复杂,可能 OomNotifier 有更“温柔”的处理方法或有更多逻辑处理。但是目前来看,OomNotifier 只是做了 kill 操作。

JAE 为什么对内存设置了配额,而对 CPU 子系统没有设置呢?因为在 JAE 环境中,应用主要以内存消耗为主,而且 CPU 如果要设置配额,只能设置占用时间的比例,在逻辑上就无法更直观地为某个应用分配 CPU 资源,所以就采用了“平均分配”的原则。如果一台虚拟机上只有一个应用实例,那么此应用实例可以“独享”所以 CPU 资源,如果有两个应用实例,各自最多只能用 50%,以此类推。 CPU 的使用率是过去一段时间内,应用实例占用的 CPU 时间 / 总时间。

接下来说到磁盘配额,JAE 使用了 linux 内核的 Quota。Quota 可以对某一分区下指定用户或用户组进行磁盘限额。限额不是针对用户主目录,而是针对这个分区下的用户或用户组。Quota 通过限制用户的 blocks 或者 inodes 超到限额的作用。

Quota 的初始化同样发生在启动 dea 时,在此之前,要先安装 quota,并指定要进行 quota 管理的分区,这里用 $1 传参。

复制代码
if quotaon -p $1 > /dev/null
then
mount -o remount,usrjquota=aquota.user,grpjquota=aquota.group,jqfmt=vfsv0 $1
quotacheck -ugmb -F vfsv0 $1
quotaon $1
fi

当部署一个应用实例时,quota 设置磁盘配额。上面 vcap safemode 提到,每个实例都分配一个用户,而 quota 就是对此用户的配额管理。Quota 管理 blocks 和 inodes 有 hard 和 soft 两个临界点,超过 soft 值,可允许继续使用,若超过 hard 值,就不再允许写入操作了。

复制代码
uid = instance[:secure_user]
{1}
limits = {}
limits[:block_soft] = DEFAULT_APP_DISK * 1024#repquota[uid][:quota][:block][:soft]
limits[:block_hard] = (DEFAULT_APP_DISK + 512) * 1024#repquota[uid][:quota][:block][:hard]
limits[:inode_soft] = DEFAULT_APP_DISK * 1024/256 #repquota[uid][:quota][:inode][:soft]
limits[:inode_hard] = (DEFAULT_APP_DISK + 512) * 1024/256 #repquota
[uid][:quota][:inode][:hard]
{1}

Block 和 inode 都给了 512M 的缓冲值。因为实例停止或删除后,用户会回收,所以此用户的 quota 需要重置。Repquota -auvs 查看磁盘使用情况:

通过使用 cgroup、quota、ACL,JAE 间接地实现了 warden 的部分功能,上面提到的 Namespace 问题,由于 ACL 的限制,应用无法使用系统命令,但是从应用的角度,实例应该跑在一个运行环境完备的操作系统 (container) 上,可以做任何事情,而不是有各种限制。

JAE 第一版于 2013 年 6 月上线,维持了两个月之后,我们越来越意识到 Namespace 的重要性。此后,我们又花了一个月的时间,在 Cloud Foundry v2 的基础上,将 JAE 第一版的功能全部迁移过来,用 warden 来实现访问控制与资源隔离,JAE 第二版于 2013 年 9 月中旬上线。

在升级的过程中,我们发现了 Cloud Foundry v1 与 v2 的诸多不兼容问题。譬如,v2 引入了 organization、spaces、domain 的概念;router 用 go 重写,去掉了 nginx,导致 flume 收集 nginx 日志方案重新设计;v2 的 cloudcontroller restful api 的变化,dashboard 几乎是重写的;运行在 warden 内部的应用,没有权限直接读取日志文件;在 v1 上运行的应用,大部分不能运行在 v2 上,为此我们做了个转化部署的自动化工具,将 v1 上的应用迁移至 v2 上。 添加了 php 和 python 的 buildpack,并做了定制。

在 JAE 的部署方面,由于底层的 openstack 环境做了较多改进,接口也发生了一些变化,Bosh 原生的 openstack CPI 可能满足不了要求,我们决定放弃 Bosh,采用更简单的 capistrano 来做集群部署,JAE 集群数目则通过手动去扩展。

总结

虽然京东云擎正处于发展的初级阶段,但是我们坚信未来有充分的发展空间,我们计划后续要做的工作有:

  1. 用户自定义域名的绑定;
  2. 智能路由和智能启动,将负载计算多元化,更能体现后端实例的真实负载;
  3. 持久化的分布式文件系统,保证应用实例保持在本地的数据不会丢失;
  4. 智能启动或重启停止应用时使用 snapshot,保证现场环境的完整性;
  5. nats cluster,避免 nats 单点;

本文旨在讲述京东云引擎对 Cloud Foundry 的重要扩展,升级改造过程以及定制的功能,介绍了基于 Cloud Foundry 的若干核心技术。不足或错误之处,还望读者批评指正。

作者简介

曾正阳,京东云平台架构师,云引擎(JAE)技术负责人,负责应用引擎的开发以及云存储、云数据库、云测试等模块的整合,目前专注于 Hadoop 和 Spark 的大数据研究。@K_James


感谢马国耀对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014-02-28 23:4812377

评论

发布
暂无评论
发现更多内容

架构师训练营第一周总结

王权富贵

Java 极客大学架构师训练营 UML

架构师训练营作业1:食堂就餐卡系统设计

a晖

食堂就餐卡系统 UML 练习

薛定谔的🐴

UML 部署图 系统用例图 组件图 组件时序图

架构师训练营-第一周学习总结

Eric

极客大学架构师训练营

食堂就餐卡架构设计

水蒸蛋

架构师第一周

Tulane

极客大学架构师训练营

食堂就餐卡系统设计

刘卓

第一周总结

娄江国

极客大学架构师训练营

第一周学习总结

刘卓

就餐卡系统设计文档

qihuajun

软件建模与设计文档学习总结

qihuajun

架构师训练营-第一章-课程总结

而立

极客大学架构师训练营

第01周 编写架构设计文档 学习总结

Jaye

极客大学架构师训练营

架构师训练营 - 第一周作业一

teslə

Week1

架构师

简易就餐卡管理系统设计

elfkingw

极客大学架构师训练营

架构第一课作业学习总结

老姜

架构师

wei

第一周:架构师训练营作业

Bruce Xiong

架构师训练营-第一周作业

Eric

极客大学架构师训练营

食堂就餐卡系统总结

薛定谔的🐴

极客大学架构师训练营 UML

架构师训练营 - 第 1 周作业 - 食堂就餐卡系统设计

yanghao

第一周学习总结:架构师的产出物

Bruce Xiong

架构设计

做产品少走弯路系列一:上帝视角(1)

我是IT民工

产品 方法论 知识体系 全局观

第一周学习笔记

测试

架构师如何做架构

Safufu

极客大学架构师训练营

程序员陪娃系列——学语趣事

孙苏勇

程序员 陪伴

通过食堂就餐卡系统学习文档编写

薛腾

极客大学架构师训练营

食堂就餐卡系统架构设计

wei

【架构师训练营】食堂就餐卡系统设计文档

张明森

架构师第一周学习心得

水蒸蛋

深度剖析京东云引擎核心技术_服务革新_曾正阳_InfoQ精选文章