返回 登录
38

Google的大规模集群管理系统Borg(上篇)

本文译者:王勇桥,80后的IT攻城狮,曾供职于IBM多年,现供职于华为公司,担任高级系统架构师。主要从事云计算领域相关的工作。Mesos和Swarm社区的贡献者。对容器化技术,自动化部署,分布式集群管理,资源调度,存储技术有较深的研究。

编者按:本文是对Google在分布式底层架构的经典文章的翻译,原文可以查看这里,由于原文较长,CSDN将分上、下两篇全文刊载此篇论文的翻译,欢迎关注。本文为上篇。

摘要:Google的Borg系统是一个运行着成千上万项作业的集群管理器,它同时管理着很多个应用集群,每个集群都有成千上万台机器,这些集群之上运行着Google的很多不同的应用。Borg通过准入控制,高效的任务打包,超额的资源分配和进程级隔离的机器共享,来实现超高的资源利用率。它通过最小化故障恢复时间的运行时特性和减少相关运行时故障的调度策略来支持高可用的应用程序Borg通过提供一个作业声明的标准语言,命名服务的集成机制,实时的作业监控,以及一套分析和模拟系统行为的工具来简化用户的使用。

我们将通过此论文对Borg系统的架构和主要特性进行总结,包括重要的设计决定,一些调度管理策略的定量分析,以及对十年的使用经验中汲取的教训的定性分析。

1.简介

图片描述

图1 Borg的高级架构。仅显示了成千上万工作节点中的一小部分。

这个在我们内部称为Borg的集群管理系统,它负责权限控制、调度、启动、重新启动和监视全部的Google中运行的应用程序。本文将解释它是如何做到的。

总的来说,Brog主要提供了三个主要的好处:(1)隐藏资源管理和故障处理的细节,因此其用户可以专注于应用程序开发; (2)提供高可靠性和高可用性操作,支持的应用也是如此; (3)使我们能够有效地在数万台机器上运行工作负载。 Borg不是解决这些问题的第一个系统,但它是在能够保证最大弹性和完整性情况下,以大规模运行的少数几个系统之一。 本文将主要围绕这些主题进行组织,并从Borg投入生产,这十多年来的使用经验作为总结 。

2.用户视图

Borg的用户是运行Google应用和服务的Google开发人员和系统管理员(网站可靠性工程师或SRE)。 用户以作业的形式将他们的工作提交给Borg,每个作业包括一个或多个任务,它们都运行相同的程序(二进制)。 每个作业在一个Borg单元中运行,一组机器组织为一个单元。 本节的剩余部分描述了Borg用户视图中展现的主要功能。

2.1 工作负载

Borg的所有单元都同时运行着两种类型的异质工作负载。第一个是“永远运行下去”的长服务,他们对延迟和性能波动敏感, 此类服务用于面向终端用户的产品,例如Gmail,Google文档,web搜索和内部基础设施服务(例如,BigTable)。 第二个是批处理作业,需要花费从几秒到几天完成,这些任务对短期性能波动的敏感性要小得多。 这些工作负载混合运行在Borg的各个运行单元中,其根据其主要租户(例如,一些单元是专门用来运行批量密集任务的)运行不同的混合应用,并且也随时间变化:批处理作业完成和重新运行,许多面向终端用户的服务作业看到日常使用模式。 Borg同样需要处理好所有这些情况。

Borg的代表性工作负载情况可以从2011年5月的一个公开的月份跟踪中找到[80],已经进行了广泛分析(例如[68]和[1,26,27,57])。

在过去几年中,许多应用程序框架已经建立在Borg之上,包括我们内部的MapReduce系统[23],FlumeJava [18],Millwheel [3]和Pregel [59]。 大多数都有一个控制器提交一个主作业和一个或多个工作作业; 前两者对YARN的应用程序管理器[76]起类似的作用。 我们的分布式存储系统如GFS [34]及其后继CFS,Bigtable [19]和Megastore [8]都运行在Borg上。

对于本文,我们将优先级较高的Borg作业分为“生产”(prod)作业,其余作为“非生产”(non-prod)作业。 大多数长期运行的服务器作业是prod;大多数批处理作业是非prod的。在代表性单元中,分配给prod作业大约总CPU资源的70%,大约占总CPU使用量的60%; 分配给它们约总内存的55%,约占总内存使用的85%。在§5.5节,将看到分配和使用之间的差异将是很重要的。

2.2 集群和单元

单元中的机器属于单个集群,由连接它们的高性能数据中心规模的网络架构定义。

一个集群位于单个数据中心大楼内,大厦集合构成一个站点。一个集群通常承载一个大型单元,可能有一些较小规模的测试或特殊用途单元。 我们努力避免任何单点故障。

中央单元大小是排除测试单元后约10k机器; 有些会更大。一个单元中的机器在许多维度上是异构的:大小(CPU,RAM,磁盘,网络),处理器类型,性能和功能(比如外部IP地址或闪存存储器)。Borg通过确定单元中的运行任务,为任务分配资源,安装程序和其他的依赖,监控任务状态并在失败时重启,将用户从大多数差异中隔离出来。

2.3 作业和任务

Borg作业的属性包括名称,所有者及其拥有的任务数量。作业可能具有限制,使其任务在具有特定属性(例如处理器体系结构,操作系统版本或外部IP地址)的计算机上运行。限制可以是硬的或软的; 软限制就像是偏好而不是要求。作业的开始能被推迟到直到前一个作业完成。 一个作业仅在一个单元中运行。

每个任务映射到在机器上的容器中运行的一组Linux进程[62]。 大多数Borg工作负载不在虚拟机(VM)内运行,因为我们不想支付虚拟化的成本。此外,该系统是在我们对没有硬件的虚拟化支持的处理器进行大量投资的时候设计的。

任务也具有属性,例如资源需求和任务在作业中的索引。 大多数任务属性对作业中的所有任务是相同的,但是可以被重写 - 例如,以提供指定任务的命令行标志。每个资源维度(CPU核,RAM,磁盘空间,磁盘访问速率,TCP端口,等)以细粒度独立指定; 我们不强加固定大小的桶或槽(§5.4)。静态链接Borg程序以减少对其运行时环境的依赖,并且Brog程序被打包为二进制文件和数据文件,由Borg负责安装。

用户通过向Borg发出远程过程调用(RPC)来操作作业,最常见的是通过命令行工具,其他Borg作业或监视系统(§2.6)。大多数作业描述都是用声明性配置语言BCL编写的。BCL是GCL的一个变体[12],它生成protobuf文件[67],并扩展了一些Borg特定的关键字。GCL提供lambda函数以允许计算,应用程序可以使用它们来调整环境配置; 成千上万的BCL文件超过1k行长,我们已经积累了数千万行的BCL。Borg作业配置与Aurora配置文件相似[6]。

图2说明了作业和任务在其生命周期中经历的状态。

图片描述

图2:作业和任务的状态图。 用户可以触发提交,终止和更新转换。

用户可以通过推送新的作业配置到Borg,再指示Borg将任务更新到新配置,来更改正在运行的作业中的某些任务或所有任务的属性。 这是一个轻量级的非原子事务,可以很容易地被撤销,直到它被关闭(提交)。 更新通常以滚动方式完成,并且可以对更新导致的任务中断(重新计划或抢占)的数量加以限制; 跳过会导致更多中断的任何更改。

某些任务更新(例如,推送新的二进制)总是需要重启任务; 某些更新(例如,增加的资源需求增加或约束改变)可能使得任务不再适合于这台机器,将导致任务停止并重新调度; 而某些更新(例如,改变优先级)却可在不重新启动或移动任务的情况下进行。

任务可以要求在被SIGKILL抢占之前通过Unix SIGTERM信号获取通知,这样任务就有时间进行清理,保存状态,完成当前正在执行的请求并拒绝新的请求。 如果抢占者设置延迟界限,则实际通知可能更少。 在实践中,通知传递约80%的时间。

2.4 分配

Borg alloc(分配的简称)是可以运行一个或多个任务的机器上的一组保留资源;无论资源是否被使用仍然被分配。Alloc可以用于为将来的任务设置资源,在停止和重启任务之间保留资源,以及将不同作业中的任务收集到同一台机器上 - 例如,Web服务实例和相关的日志保存任务, 这个任务将服务的URL日志从本地磁盘复制到分布式文件系统。 alloc的资源以类似于机器资源的方式处理; 多个任务运行在一个alloc中,共享其资源。如果一个alloc必须重定位到另一台机器,它的任务将被重新调度。

一个alloc集合就像一个作业:它是一组在多个机器上预留资源的alloc。 一旦创建了一个alloc集,可以提交一个或多个作业在其中运行。 简单期间,我们一般会使用“task”来引用alloc或顶层任务(在alloc之外的)和“job”来引用一个作业或alloc集。

2.5 优先级,配额和接纳控制

当更多的工作出现而超过可容纳的限度时会发生什么?我们的解决方案是优先级和配额。

每个作业都有一个优先级,它是一个小的正整数。高优先级任务可以以牺牲低优先级任务为代价而获得资源,即使这导致抢占(杀死)后者。Borg将不同种类的作业分为不同的领域,并给每个领域定义了不重叠的优先权重,这些作业组包括:监视作业,生产作业,批处理作业和尽力而为的作业(已知的包括测试程序),他们的优先级依次递减。对于本文,prod作业是监测和生产领域的工作。

虽然被抢占的任务通常将被重新安排在单元中的其他地方,抢占级联可能发生,如果高优先级的任务碰到一个略低优先级的任务,而这个略低优先级任务又引发另一个略低优先级的任务,等等。为了消除大部分这种情况,我们不允许生产领域中的任务相互抢占。 细粒度优先级在其他情况下仍然有用 - 例如,MapReduce主任务以比他们控制的workers更高的优先级运行,来提高其可靠性。

优先级表示单元中正在运行或正等待运行的作业的相对重要性。 配额用于决定允许进行调度的作业。 配额表示为在给定优先级上的一段时间(通常为几个月)内的资源量(CPU,RAM,磁盘等)的向量。 数量指定用户的作业请求可以一次请求的资源的最大量(例如,“从现在直到7月底在单元xx中的prod优先级的20TiBRAM“)。 配额检查是许可控制的一部分,而不是调度:配额不足的作业立即拒绝提交。

较高优先级配额的成本高于较低优先级配额。生产优先级配额仅限于单元中可用的实际资源,因此,提交符合配额的生产优先级作业的用户可以预期运行。 即使我们鼓励用户购买的配额不超过他们的需求,但是许多用户仍然过度购买,因为这帮助他们在应用程序的用户群增长时克服不足。我们通过在较低优先级别上过度销售配额来响应这一点:每个用户具有在优先级零的无限配额,尽管这常常难以执行,因为资源被过度订阅。一个低优先级作业可能被允许了,但是由于资源不足而保持等待(未调度)。

在Borg以外进行配额分配,并且与我们的物理容量规划密切相关,其结果反映在不同数据中心的配额的价格和可用性上。 仅当用户作业具有所需优先级的足够配额时,才允许用户作业。 配额的使用减少了对优势资源公平(DRF)[29,35,36,66]等策略的需要。

Borg有一个能力系统,能给予一些用户特殊的权限; 例如,允许管理员删除或修改单元中的任何作业,或允许用户访问受限内核功能或Borg行为(例如禁用其作业的资源估计(§5.5))。

2.6 命名和监控

仅创建和放置任务是不够的:服务的客户端和其他系统需要能够找到它们,即使它们被重定位到新机器上了。要启用此功能,Borg将为每个任务创建一个稳定的“Borg name service”(BNS)名称,其中包含单元名称,作业名称和任务编号。Borg将任务的主机名和端口写入一个以BNS命名的一致的高可用的Chubby [14]文件中,由我们的RPC系统使用该文件来查找任务端点。BNS名称还形成任务的DNS名称的基础,所以在cc单元中的用户ubar拥有的作业 jfoo中的第五十个任务将通过50.jfoo.ubar.cc.borg.google.com访问到。Borg还会在Chubby发生变化时将作业大小和任务健康信息写入Chubby,因此负载平衡器可以查看将请求路由到哪里。

几乎在Borg下运行的每个任务都包含一个内置的HTTP服务器,它发布有关任务运行状况的信息和成千上万个性能指标(例如RPC延迟)。 Borg监控health-check URL,并重新启动不会及时响应或返回HTTP错误代码的任务。 其他数据由仪表盘监视工具和违反服务级别目标(SLO)的警报进行跟踪。

称为Sigma的服务提供了基于Web的用户界面(UI),通过该UI用户可以检查所有作业,特定单元的状态,或向下钻取到单个作业和任务,以检查其资源行为,详细日志,执行历史 ,和最终的结果。 我们的应用产生大量日志; 这些被自动轮转以避免用完磁盘空间,并在任务退出后保存一段时间以协助调试。 如果作业未运行,Borg提供了“为什么待处理?”注释,以及如何修改作业的资源请求以更好地适应单元的指导。 我们发布了“切合”更可能容易调度的资源形式的规则。

Borg记录所有作业提交事件和任务事件,以及每个任务在Infrastore中详细的资源使用信息,这是一个可扩展的只读数据存储,通过Dremel [61]具有一个交互式的类似SQL的界面。此数据用于基于使用的计费,作业调试和系统故障以及长期容量规划。 它还为Google群集工作负载跟踪提供数据[80]。

所有这些功能都有助于用户理解和调试Borg的行为及用户的作业,并帮助我们的SREs为每个人管理几万台机器。

3.Borg体系结构

Borg单元由一组机器,一个称为Borgmaster的逻辑中央控制器和单元中每台机器上运行的称为Borglet的代理进程构成(参见图1)。 Borg的所有组件都用C ++编写。

3.1 Borgmaster

每个单元的Borgmaster包括两个进程:主进程Borgmaster和独立的调度程序(§3.2)。 主Borgmaster进程处理客户端RPC,状态变化(例如,创建作业)或提供对数据的只读访问(例如,查找作业)。它还管理系统中所有对象(机器,任务,分配等)的状态机,与Borglets进行通信,并提供Web UI作为Sigma的备份。

Borgmaster在逻辑上是一个单一的进程,但实际上被复制了五次。 每个副本维护了一份该单元大部分状态的内存副本,并且该状态也记录在该副本的本地磁盘上的高可用性,分布式,基于Paxos的存储[55]中。每个单元的单个选定的master既用作Paxos的领导者又用作状态mutator,处理改变单元状态的所有操作(例如提交作业或在机器上终止任务)。当cell建立时或只要当选择的master出现故障时,就会选择一个master(使用Paxos); 它获取一个Chubby锁,以便其他系统可以找到它。选择一个master和故障转移到新的master通常需要大约10s,但是在大单元中可能需要一分钟,因为一些内存中的状态必须重建。 当副本从中断恢复时,它将自动重新同步来自最新的其它Paxos副本的状态。

Borgmaster在某个时间点的状态称为检查点,并采用定期快照的形式增加一条更改日志(保存在Paxos存储中)。检查点有许多用途,包括将Borgmaster的状态恢复到过去的任意一个点(例如,在接受触发Borg中的软件缺陷的请求之前,以便可以对其进行调试); 构建用于未来查询的事件的持久日志; 以及离线模拟。

高保真的Borgmaster模拟器Faokemaster可用于读取检查点文件,并包含产生Borgmaster代码的完整副本,其中包含与Borglets的无存根接口。它接受RPC进行状态机更改和执行操作,如“调度所有挂起的任务”,通过与它进行交互(它就像是一个活的Borgmaster,带有模拟的Borglets可从检查点文件重放真实的交互),可以使用它来调试故障。用户可以逐步观察在过去实际发生的系统状态的改变。 Fauxmaster对于容量规划(“符合多少这种类型的新作业?”)以及在更改单元配置之前进行完整性检查(“这种更改是否会驱逐重要的工作?”)也很有用。

3.2 调度

提交作业时,Borgmaster会将其持久化在Paxos存储中,并将作业的任务添加到等待队列。 这是由调度程序异步扫描的,如果有足够的可用资源满足作业的要求,则会将任务分配给机器。 (调度程序主要操作任务,而不是作业。)扫描从高到低优先级,由优先级循环方案调度,以确保用户之间的公平性,并避免大型作业后面的队头阻塞。 调度算法有两个部分:可行性检查(用于找到任务可以运行的机器),以及评分(用于挑选一个可行的机器)。

在可行性检查中,调度器找到满足任务需求的一组机器,这组机器具有足够的“可用”资源 - 这些资源中包括已经分配给可以被抢占的较低优先级任务的资源。 在评分中,调度器确定每个可行机器的“良好性”。该分数考虑了用户指定的偏好,但主要是由内置标准决定,如最大限度地减少抢占任务的数量和优先级,选择已经有任务包副本的机器,跨越电源和故障域传播任务,以及打包质量(包括将高优先级任务和低优先级任务混合到单个机器上,以允许高优先级任务在负载高峰中扩展)。

Borg最初使用E-PVM [4]的变体进行评分,其在不同资源上生成单一成本值,并且在放置任务时最小化成本的变化。在实践中,E-PVM最终在所有机器上扩展负载,为负载高峰留下余量 - 但是以增加碎片为代价,特别是对于需要大部分机器的大型任务; 我们有时称之为“刚好合适”。

调度的另一端是“最佳合适”,它试图尽可能紧密地填充机器。 这使一些机器没有用户作业(它们仍然运行存储服务器),因此放置大任务是简单直接的,但是严格的封装不利于用户或Borg对资源需求的任何错误估计。 这会伤害突发负载的应用程序,对于指定低CPU需求的批处理作业尤其糟糕,以便他们可以轻松安排并尝试在未使用的资源中伺机运行:20%的非生产任务请求少于0.1个CPU内核。

我们当前的评分模型是一种混合式的,它试图减少搁置资源的数量 - 由于机器上的另一个资源被完全分配而无法使用的资源。 它提供比最适合我们工作负载约3-5%的更好的包装效率(在[78]中定义)。

如果计分阶段选择的机器没有足够的可用资源来满足任务,则Borg会抢占(杀死)较低优先级任务,从最低优先级到最高优先级,直到满足为止。 我们将被抢占的任务添加到调度程序的挂起队列,而不是迁移或休眠它们。

任务启动延迟(从作业提交到任务运行的时间)是一个已经并继续受到极大关注的领域。它是高度可变的,中值通常约25s。 软件包安装大约占全部的80%:其中一个已知的瓶颈是软件包要写入的本地磁盘的争用。为了减少任务启动时间,调度程序更倾向将任务分配给已经安装了必要的软件包(程序和数据)的机器:大多数软件包是不可变的,因此可以共享和缓存。 (这是Borg调度程序支持数据本地化的唯一形式。)此外,Borg使用类似树和torrent的协议并行地将软件包分发到机器。

此外,调度程序使用几种技术来扩展具有成千上万台机器的单元(§3.4)。

3.3 Borglet

Borglet是一个本地Borg代理,存在于单元中的每一台机器中。它启动和停止任务; 如果故障就重启任务; 通过操纵操作系统内核设置来管理本地资源; 翻转调试日志; 并向Borgmaster等监控系统报告机器的状态。

Borgmaster每隔几秒钟轮询一次Borglet以检索机器的当前状态,并将所有未完成的请求发送给它。 这使Borgmaster控制通信速率,避免了显式流控制机制的需要,并防止恢复风暴[9]。

选定的master负责准备要发送到Borglets的消息,并负责根据cell的响应更新cell的状态。为了性能可扩展性,每个Borgmaster副本运行无状态链接分片来处理与一些Borglets的通信;每当发生Borgmaster选择时重新计算分区。对于弹性,Borglet始终报告其完整状态,但链接分片通过仅报告状态机间的差异来收集和压缩此信息,以减少选定master的更新负载。
如果Borglet没有响应几个轮询消息,它的机器被标记为关闭,并且其运行的任何任务被重新安排在其他机器上。如果通信恢复,Borgmaster会通知Borglet要停止这些已经重新安排的任务,以避免重复。即使与Borgmaster失去联系,Borglet也继续正常运行,因此即使所有Borgmaster副本故障了,当前运行的任务和服务也会保持。

3.4可扩展性

我们不确定Borg的集中式架构的最终可扩展性限制将出现在何处; 到目前为止,每次我们接近一个极限,我们已经设法消除它。一个Borgmaster可以管理一个cell中的数千台机器,并且几个cell具有每分钟超过10000个任务的到达速率。繁忙的Borgmaster使用10-14个 CPU内核和高达50GiB 的RAM。我们使用几种技术来实现这种规模。

早期版本的Borgmaster有一个简单的,同步的循环:接受请求,计划任务,并与Borglets通信。为了处理更大的cell,我们将调度程序分离出来作为一个单独的进程,这样它可以与其他Borgmaster函数并行操作故障容限。 调度器副本对单元状态的高速缓存副本进行操作。它反复:从选定的主机检索状态更改(包括已分配和挂起的工作); 更新其本地副本;执行调度传递以分配任务; 并将这些分配通知选定的主机。master将接受并采用这些分配,除非它们是不适当的(例如,基于过期状态),这将导致它们在调度程序的下一次传递中被重新考虑。这在灵魂上与在Omega [69]中使用的乐观并发控制非常相似,事实上,我们最近为Borg添加了针对不同工作负载类型使用不同调度程序(schedulers)的能力。

为了提高响应时间,我们添加了单独的线程来与Borglets进行通信并响应只读RPC。为了更好的性能,我们在五个Borgmaster副本(§3.3)中分割(分区)这些功能。同时,这保持了UI上99%ile的响应时间低于1s和95%ile 的Borglet轮询间隔低于10s。

图片描述

图3:针对生产和非生产工作负载的任务驱逐率及原因。 数据自2013年8月1日起。

有几点使Borg调度器更具可扩展性:

分数缓存:评估可行性和评价机器是昂贵的,因此Borg缓存分数直到机器或任务的属性改变 - 例如,机器上的任务终止,属性改变或任务的需求改变。 忽略资源数量的小变化可减少高速缓存失效。

等价类:Borg作业中的任务通常具有相同的需求和约束,因此并不是确定每个机器上的每个挂起任务的可行性,并对所有可行的机器进行评分,Borg只对每个等价类的一个任务进行可行性分析和评分 - 一组具有相同需求的任务。

轻松随机化:计算大cell中所有机器的可行性和分数是浪费的,因此调度程序以随机顺序检查机器,直到找到“足够”可行的机器进行评分,然后选择该集合中的最佳机器。 这减少了任务进入和离开系统时所需的评分和高速缓存失效的数量,并加快了任务到机器的分配。放松随机化有时类似于Sparrow [65]的批量采样,同时还处理优先级,抢占,异质性和软件包安装的成本。
在我们的实验(§5)中,从头开始安排单元的整个工作负载通常需要几百秒,但是在禁用上述技术后超过3天后还没有完成。 通常,在等待队列上的在线调度传递在不到半秒内完成。

4.可用性

故障是大规模系统中的常态[10,11,22]。图3提供了15个样本cell中任务驱逐原因的分解。运行在Borg上的应用程序应能使用诸如复制,在分布式文件系统中存储持久状态并(如果适当的话)捕捉临时检查点等技术来处理此类事件。即使如此,我们也试图减轻这些事件的影响。例如,Borg:

  • 如有必要,在新机器上自动重新安排逐出的任务;
  • 通过在诸如机器,机架和电源域之类的故障域中扩展作业的任务,减少相关故障;
  • 限制任务中断的允许速率和任务数量,这些任务可以在维护活动(例如操作系统或机器更新)期间同时关闭;

图片描述

图4:压缩的效果。 对15个cell,在压缩后获得的原始cell大小的百分比的CDF。

  • 使用声明性期望状态表示和幂等变换操作,使得失败的客户端可以无损地重新提交任何被遗忘的请求;
  • rate-limits找到无法访问的机器的任务的新位置,因为它无法区分大型机器故障和网络分区;
  • 避免重复任务::导致任务或机器崩溃的机器配对;
  • 通过不断重新运行日志记录器任务(§2.4)来恢复写入本地磁盘的关键中间数据,即使所连接的alloc已终止或移动到了另一台机器。 用户可以设置系统持续尝试的时间;一般是几天。

Borg的一个关键设计特点是,即使Borgmaster或任务的Borglet关闭,已经运行的任务也会继续运行。但是保持master仍然很重要,因为当它关闭时,无法提交新作业或更新现有的作业,并且无法重新计划故障的计算机上的任务。

Borgmaster使用的技术组合,使其在实践中达到了99.99%的可用性:机器故障复制; 准入控制避免过载; 并使用简单的低级工具部署实例以最小化外部依赖性。 每个单元独立于其他单元,以最小化关联的操作者错误和故障传播的机会。 这些目标,不是可扩展性限制,而是反对较大cell的主要论证。


SDCC 2017·上海站将于2017年3月17-19日登陆申城,三大技术峰会24位嘉宾,汇聚国内一线的互联网公司大牛,畅谈运维、数据库和架构的热门话题和技术热点,精益运维发起人&优维科技CEO王津银、MongoDB 大中华区首席架构师唐建法和华为软件API开放平台架构师李林锋等亲临现场。3月5日前门票八折优惠中,5人以上团购立减400元,详情点击注册参会

图片描述

评论