2025 年技术指引:让真实案例和经验为开发者开路 了解详情
写点什么

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

  • 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:4812566

评论

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

解密数据仓库LLVM技术神奇之处

华为云开发者联盟

数据仓库 LLVM 算子 GaussDB(DWS) 底层虚拟机

读一篇博客,写一段代码,每天写写Python自然就会了,每日Python第1天

梦想橡皮擦

Python 3月月更

如何在 eNSP 上保存配置?

Ethereal

性能测试中Disruptor框架shutdown失效的问题分享

FunTester

Disruptor 性能测试 接口测试 高性能队列 FunTester

什么是元宇宙?为何要关注它?——解码元宇宙

CECBC

紫光展锐解除楚庆CEO职务,内部员工爆料那些不为人知的内情!

IC男奋斗史

芯片行业思考

OKR怎么写?100个OKR案例模板

爱吃小舅的鱼

Linux小技巧:如何在 Vim 中显示行号?

Ethereal

presto实战读书笔记

聚变

如何在 Linux 中将主目录移动到新分区或磁盘?

Ethereal

[银行面试系列]1 进入银行之前必须了解的20个问题

暖蓝笔记

3月程序媛福利 3月月更

聊聊 Pulsar: Pulsar 分布式集群搭建

老周聊架构

云原生 Apache Pulsar 3月月更

开发电脑用 Windows 还是 Mac

HoneyMoose

初识工业互联网

劼哥stone

工业互联网

面试官:GRE 和 IPsec 隧道有什么区别?

Ethereal

docker、k8s 面试总结

yuexin_tech

Docker k8s

遵循Promises/A+规范,深入分析Promise实现细节(基础篇)

战场小包

JavaScript 前端 Promise 3月月更

Mybatis的where标签,竟然还有这么多不知道的!

CRMEB

当TIME_WAIT状态的TCP正常挥手,收到SYN后…

华为云开发者联盟

TCP syn 报文 TIME_WAIT RST报文

期待!Fedora 36 发布日期和新功能

Ethereal

Go语言实战之数组的内部实现和基础功能

山河已无恙

Go 语言 3月月更

Linux运维必知:如何从其 PID 中查找进程名称

Ethereal

千万级学生管理系统的考试试卷存储方案

晨亮

「架构实战营」

如何在敏捷中管理和减少技术负债?

爱吃小舅的鱼

今儿直白的用盖房子为例,给你讲讲Java建造者模式

华为云开发者联盟

Java 设计模式 对象 建造者模式 对象构建模式

比特币突破4.4万美元!美欧制裁或推动俄罗斯资金转向加密货币

CECBC

将本地代码同步到gitee和github中去

布衣骇客

Git Commit #Github

从用户输入URL到页面展示,这中间发生了什么?

Tristan

前端 浏览器

如何打造良好的分享氛围

Hockor

团队管理 技术分享

如何做好一场技术分享

Hockor

团队管理 个人成长

从理想照进现实,浅谈“算力网络”

鲸品堂

东数西算

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