首页 文章 精选 留言 我的

精选列表

搜索[数据库应用],共10000篇文章
优秀的个人博客,低调大师

【从0到1学习边缘容器系列-3】应用容灾之边缘自治

导语:边缘计算模式下,云端的控制中心和边缘端的设备之间网络环境较复杂,网络质量差次不齐没有保障。用户往往希望在弱网环境下,边缘容器能提供高可用的业务能力。TKE 边缘容器团队在弱网环境下提出了边缘自治功能。本文着重介绍了边缘容器在弱网环境下为了保证业务高可用而做的工作。 问题背景 边缘计算使用的边缘设备数量庞大、分布全国各地,网络环境复杂,因特网、以太网、5G、WIFI 等形态均有可能。因此,云端的控制中心和边缘端的设备之间网络环境较复杂,网络质量差次不齐没有保障。 kubernetes 传统工作模式是所有组件 list-watch kube-apiserver,然后 reconcile 各种资源到期望状态。各节点的健康都强依赖于其与 kube-apiserver 通信的稳定。kubernetes 在传统的集群环境上工作很完美,因为大多数集群都能保证在一个局域网内。然而,在边缘计算的业务场景下,有一个稳定的网络环境着实是一件奢侈的事情。 在这样的背景下,如何保证边缘集群的业务高可用性以及服务高可用性?一直都是一个难题。为此我们腾讯云边缘容器团队(TKE@EDGE)设计了两个利器来专门啃下这块硬骨头,本篇将重点讲第一个利器——边缘自治。 (注:此文提到的网络环境,都是指节点与云端的网络环境,而不是业务运行所在环境。) 需求举例 我们来以一个常见的厂房模型来介绍一下用户在弱网环境下使用传统 kubernetes 遇到的问题以及 TKE 边缘容器团队在此类场景下提出的解决方案。 如上图所示,该案例采用的部署模式为:用户在腾讯云公有云上购买了几台 CVM 机器作为 master 节点,其上部署了 kube-apiserver、kube-controller-manager、kube-scheduler 三大组件。然后根据业务需求,为每一个厂房都创建多个边缘 worker 节点,部署 kubelet 和 kube-proxy 以及 dockerd。同厂房节点之间可以互相访问, 不同厂房节点网络不互通。比如两个分布在北京和广州的厂房的边缘节点虽然可以归属于同一个集群,但是它们之间的网络是不互通的。每个厂房内所有节点会通过公网连接至云端管控端,但是这个网络通道都属于不可靠的弱网环境。 厂房A在北京地区,厂房B在广州地区。二者之间是不能互通的。 用户通过 kubernetes 管理平台进行workload的管理,master 与 worker 节点所在的厂房之间的网络环境网络环境是不能保证的,可能弱网A断网,网络B仍然正常。用户在这样的场景下,提出了几个需求: 节点即使和 master 失联,节点上的业务能继续运行 保证如果业务容器异常退出或者挂掉,kubelet 能继续拉起 保证节点重启后,业务能继续重新被拉起来 用户在厂房内部署的是微服务,需要保证节点重启后,同一个厂房内的微服务可以访问 对于用户来说,这些诉求是他们的基本需求,也是其业务上云的关键因素。用户想要既享受 kubernetes 带来方便的管理运维,同时也要具备弱网环境下的容灾能力。这对传统标准 kubernentes 解决方案提出了挑战。 标准kubernetes处理方式 我们来温习一下标准的 kubernentes 下,如果节点断网失联并且发生异常重启的行为后,会出现哪些现象呢? 失联的节点状态置为 NotReady 或者 Unknown 状态 失联的节点上的业务进场异常退出后,容器可以被拉起 失联的节点上的 Pod IP 从 Endpoint 列表中摘除 失联的节点发生点重启后,容器全部消失不会被拉起 我们依次来看,首先,在传统的模式下,节点是否健康取决于节点上 kubelet 组件的心跳或者续租。如果网络断了,云端组件当然会认为节点是不可用状态。这个状态可以提示用户,该节点可能有异常,需要运维介入。同时,由于 kubelet 还在接管所有本机 Pod,即使业务容器异常退出,容器也是可以继续被拉起的。失联的节点上所有的 Pod ,它们的 IP 都会被从 Endpoint list 中摘除,导致微服务不能访问到这个节点,在传统 kubernentes 集群中,这是一个高可用的措施。但是在边缘集群内,这个“节点不可用=服务不可用”等式是否还成立呢?这个地方是需要探讨的,其实很多业务场景下,用户希望节点即使和云端断网,该节点上的 Pod 也要能继续对外提供服务。 因此我们团队认为,在边缘容器的场景下,单纯与云端网络失联是不能被粗暴地认为“服务不可用”的。为此我们特意设计了第二件利器——“分布式节点健康检查”插件,来解决这个痛点,该功能会在后续文章进行详细介绍。 下面重头戏来了,断网的节点重启一下,容器还会在吗?服务还可用吗?回答是,容器当然不在喽。而且容器不在了,服务肯定不能访问了。用户最关键的需求,显然在传统的 kubernentes 模式下,是不能满足的。 那么来看看我们边缘计算的利器——边缘自治功能能达到的效果吧。 TKE 的边缘自治效果 TKE 的边缘自治效果 节点会被置为 NotReady 或者 Unknown 状态,但是服务仍然可用 多节点断网情况下,Pod 业务正常运行,微服务能力正常提供 多节点断网情况下并重启后,Pod 会被重新拉起并正常运行 多节点断网情况下并重启后,所有的微服务可以被正常访问 我们为了达到最符合用户的效果,配合使用了分布式节点健康检查插件功能,来保证节点在断网情况下即使节点被置为NotReady 状态,但是服务还是继续可用。同时该节点上的 Pod 也不会在新的节点上重新拉起新的 Pod 副本。 我们在极端网络环境下,将运行业务的多个节点手动执行重启操作来模拟节点异常重启场景。节点重启之后,节点组件正常启动,之前的 Pod 也会被自动被拉起,并且运行正常,处于 Running 状态。 那服务以及网络呢?我们测试后发现,重启多个节点之后,我们在节点手动请求 Pod ip,可以访问成功;进入 Pod A 内部分别请求在同一个节点上的 Pod B 以及另一个节点上的 Pod C(需要同一个厂房环境),都可以访问成功;在 Pod A 解析 同一厂房的 Service A, 可以解析并成功。最后,在外部对于该厂房内部的服务进行请求,访问成功。 如此看来,边缘自治功能,有效解决了传统 kubernetes 在弱网环境下所不能解决的用户痛点。 原理简介 我们团队为边缘自治功能打磨了很久,涉及方面众多,修改以及设计的地方比较多,本文不太介绍具体代码实现细节。有兴趣的同学可以和TKE边缘容器同学共同进行学习探讨。在此我简单分享一些设计原理。 lite-apiserver机制 kubernetes 是典型的通过数据进行交互的架构。解决数据问题就能提供一个夯实的基础。所以弱网环境的边缘自治,首当其冲的,最最最需要解决的问题就是数据问题。 考虑到弱网、断网情况,需要保证节点的组件与云端组件“通信”,或者说让节点认为此时还是可以“通信”的。如上图所示,我们在边缘端加了一层镜像 lite-apiserver 组件。边缘节点上所有对 kube-apiserver 的请求都会发往 lite-apiserver,并由 lite-apiserver 转发到 kube-apiserver。对于边缘节点来说,lite-apiserver 提供的功能就是 kube-apiserver 一样,但是一方面 lite-apiserver 只对本节点有效,另一方面相比较标准的 kube-apiserver,我们对于 lite-apiserver 进行了裁剪,大幅度降低了其资源占用。我们在没有修改原生 kube-apiserver 的情况下,实现了。在网络通畅的情况下,lite-apiserver 组件对于节点组件来说是透明的。当网络异常情况,lite-apiserver 组件会把本节点需要的数据返回给节点上组件,保证节点组件不会受网络异常情况影响。 网络快照机制 OK,lite-apiserver 机制保证了断网情况下,节点也不会和“apiserver”失联。既然节点可以拉取到本节点对应的数据,那么业务 Pod 也就能够被 kubelet 成功拉起来。下一步就是解决 Pod 之间的互相访问的问题了。 在边缘容器场景下,考虑到适配性以及易用性,我们采用了 flannel 组件的 vxlan 模式作为网络解决方案。flannel 组件保证了跨节点之前的网络访问。节点上的flannel 以及每个 Pod 都有一些对应属于自己的网络信息,我们这里采用了网络快照机制,将专属于这些组件的网络信息定期快照,从而保证了断网环境下重启后网络可用。并且实现了同节点、跨节点 Pod 之间的网络互通。 DNS解决方案 用户业务对外正常提供服务,以及集群内微服务之间互相调用,这些问题都会涉及到域名解析。 如上图所示,在传统 kubernetes上,通过在集群中创建一个kube-dns deployment 来解决是来解决域名问题。但是边缘计算集群,所有节点可能是不在一个局域网,很可能是跨可用区的,各节点和 kube-dns 的访问无法保证,一个 kube-dns 的 deployment 不能满足边缘容器的需求。 在边缘容器场景下,采用 DaemonSet 方式部署kube-dns,保证每个节点都有可用的 kube-dns,同时修改每个节点上 kubelet 启动参数--cluster-dns,将其指向本机IP。这样就保证了,即使断网情况下,也能解析 kubernetes service 的域名。 总结而言就是 lite-apiserver 机制为基础, kube-proxy、flannel、kube-dns 以及网络快照机制保证了边缘容器集群在弱网环境下的网络可靠性。 适用场景 腾讯云边缘容器产品支持用户在云端通过 kubernetes 方式来管理边缘节点,支持管控平台与 work 节点网络环境分离,具备弱网环境下的容灾能力,支持用户自定义网络流量,并且所有核心组件都与开源 kubernentes 保持一致,目前已经支持 1.18 版本。 目前 TKE@EDGE 具备公有云和私有云两种产品形态,欢迎来官网体验使用边缘容器公有云产品( https://console.cloud.tencent.com/tke2/edge ),产品入口目前为白名单可见,请联系文章作者开通白名单。 后续计划 后续我们还会有一系列工作,来加强在弱网环境下的工作稳定性。 插件化改造。将边缘自治的解决方案改造为插件,提供给传统 kubernetes 某些业务来按需使用 支持完全去中心化 lite-apiserver,来协助 kube-apiserver 在弱网环境下业务的管理 写在最后 我们的解决方案对于原生使用 kubernetes 的方案和理念都有所不同,但是我个人认为紧跟业务脚步的产品以及技术才是最有价值的。个人技术能力有限,表达可能有疏漏,技术可能有错误,欢迎大家指出错误,共同进步。 这里欢迎更多有业务场景需要、感兴趣的同学使用、加入、体验,提需求,共优化。欢迎扫码加腾小云同学好友,一起进群讨论。 【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

优秀的个人博客,低调大师

干货|漫画算法:LRU从实现到应用层层剖析(第一讲)

今天为大家分享很出名的LRU算法,第一讲共包括4节。 LRU概述 LRU使用 LRU实现 Redis近LRU概述 第一部分:LRU概述 LRU是Least Recently Used的缩写,译为最近最少使用。它的理论基础为“最近使用的数据会在未来一段时期内仍然被使用,已经很久没有使用的数据大概率在未来很长一段时间仍然不会被使用”由于该思想非常契合业务场景 ,并且可以解决很多实际开发中的问题,所以我们经常通过LRU的思想来作缓存,一般也将其称为LRU缓存机制。因为恰好leetcode上有这道题,所以我干脆把题目贴这里。但是对于LRU而言,希望大家不要局限于本题(大家不用担心学不会,我希望能做一个全网最简单的版本,希望可以坚持看下去!)下面,我们一起学习一下。 题目:运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get 和 写入数据 put 。 获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。 写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。 进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作? 示例: LRUCache cache = new LRUCache( 2 /* 缓存容量 */ ); cache.put(1, 1); cache.put(2, 2); cache.get(1); // 返回 1 cache.put(3, 3); // 该操作会使得密钥 2 作废 cache.get(2); // 返回 -1 (未找到) cache.put(4, 4); // 该操作会使得密钥 1 作废 cache.get(1); // 返回 -1 (未找到) cache.get(3); // 返回 3 cache.get(4); // 返回 4 第二部分:LRU使用 首先说一下LRUCache的示例解释一下。 第一步:我们申明一个LRUCache,长度为2 第二步:我们分别向cache里边put(1,1)和put(2,2),这里因为最近使用的是2(put也算作使用)所以2在前,1在后。 第三步:我们get(1),也就是我们使用了1,所以需要将1移到前面。 第四步:此时我们put(3,3),因为2是最近最少使用的,所以我们需要将2进行作废。此时我们再get(2),就会返回-1。 第五步:我们继续put(4,4),同理我们将1作废。此时如果get(1),也是返回-1。 第六步:此时我们get(3),实际为调整3的位置。 第七步:同理,get(4),继续调整4的位置。 第三部分:LRU 实现(层层剖析) 通过上面的分析大家应该都能理解LRU的使用了。现在我们聊一下实现。LRU一般来讲,我们是使用双向链表实现。这里我要强调的是,其实在项目中,并不绝对是这样。比如Redis源码里,LRU的淘汰策略,就没有使用双向链表,而是使用一种模拟链表的方式。因为Redis大多是当内存在用(我知道可以持久化),如果再在内存中去维护一个链表,就平添了一些复杂性,同时也会多耗掉一些内存,后面我会单独拉出来Redis的源码给大家分析,这里不细说。 回到题目,为什么我们要选择双向链表来实现呢?看看上面的使用步骤图,大家会发现,在整个LRUCache的使用中,我们需要频繁的去调整首尾元素的位置。而双向链表的结构,刚好满足这一点(再啰嗦一下,前几天我刚好看了groupcache的源码,里边就是用双向链表来做的LRU,当然它里边做了一些改进。groupcache是memcache作者实现的go版本,如果有go的读者,可以去看看源码,还是有一些收获。) 下面,我们采用hashmap+双向链表的方式进行实现。 首先,我们定义一个LinkNode,用以存储元素。因为是双向链表,自然我们要定义pre和next。同时,我们需要存储下元素的key和value。val大家应该都能理解,关键是为什么需要存储key?举个例子,比如当整个cache的元素满了,此时我们需要删除map中的数据,需要通过LinkNode中的key来进行查询,否则无法获取到key。 type LRUCache struct { m map[int]*LinkNode cap int head, tail *LinkNode } 现在有了LinkNode,自然需要一个Cache来存储所有的Node。我们定义cap为cache的长度,m用来存储元素。head和tail作为Cache的首尾。 type LRUCache struct { m map[int]*LinkNode cap int head, tail *LinkNode } 接下来我们对整个Cache进行初始化。在初始化head和tail的时候将它们连接在一起。 func Constructor(capacity int) LRUCache { head := &LinkNode{0, 0, nil, nil} tail := &LinkNode{0, 0, nil, nil} head.next = tail tail.pre = head return LRUCache{make(map[int]*LinkNode), capacity, head, tail} } 大概是这样: 现在我们已经完成了Cache的构造,剩下的就是添加它的API了。因为Get比较简单,我们先完成Get方法。这里分两种情况考虑,如果没有找到元素,我们返回-1。如果元素存在,我们需要把这个元素移动到首位置上去。 func (this *LRUCache) Get(key int) int { head := this.head cache := this.m if v, exist := cache[key]; exist { v.pre.next = v.next v.next.pre = v.pre v.next = head.next head.next.pre = v v.pre = head head.next = v return v.val } else { return -1 } } 大概就是下面这个样子(假若2是我们get的元素) 我们很容易想到这个方法后面还会用到,所以将其抽出。 func (this *LRUCache) moveToHead(node *LinkNode){ head := this.head //从当前位置删除 node.pre.next = node.next node.next.pre = node.pre //移动到首位置 node.next = head.next head.next.pre = node node.pre = head head.next = node } func (this *LRUCache) Get(key int) int { cache := this.m if v, exist := cache[key]; exist { this.moveToHead(v) return v.val } else { return -1 } } 现在我们开始完成Put。实现Put时,有两种情况需要考虑。假若元素存在,其实相当于做一个Get操作,也是移动到最前面(但是需要注意的是,这里多了一个更新值的步骤)。 func (this *LRUCache) Put(key int, value int) { head := this.head tail := this.tail cache := this.m //假若元素存在 if v, exist := cache[key]; exist { //1.更新值 v.val = value //2.移动到最前 this.moveToHead(v) } else { //TODO } } 假若元素不存在,我们将其插入到元素首,并把该元素值放入到map中。 func (this *LRUCache) Put(key int, value int) { head := this.head tail := this.tail cache := this.m //存在 if v, exist := cache[key]; exist { //1.更新值 v.val = value //2.移动到最前 this.moveToHead(v) } else { v := &LinkNode{key, value, nil, nil} v.next = head.next v.pre = head head.next.pre = v head.next = v cache[key] = v } } 但是我们漏掉了一种情况,如果恰好此时Cache中元素满了,需要删掉最后的元素。处理完毕,附上Put函数完整代码。 func (this *LRUCache) Put(key int, value int) { head := this.head tail := this.tail cache := this.m //存在 if v, exist := cache[key]; exist { //1.更新值 v.val = value //2.移动到最前 this.moveToHead(v) } else { v := &LinkNode{key, value, nil, nil} if len(cache) == this.cap { //删除最后元素 delete(cache, tail.pre.key) tail.pre.pre.next = tail tail.pre = tail.pre.pre } v.next = head.next v.pre = head head.next.pre = v head.next = v cache[key] = v } } 最后,我们完成所有代码: type LinkNode struct { key, val int pre, next *LinkNode } type LRUCache struct { m map[int]*LinkNode cap int head, tail *LinkNode } func Constructor(capacity int) LRUCache { head := &LinkNode{0, 0, nil, nil} tail := &LinkNode{0, 0, nil, nil} head.next = tail tail.pre = head return LRUCache{make(map[int]*LinkNode), capacity, head, tail} } func (this *LRUCache) Get(key int) int { cache := this.m if v, exist := cache[key]; exist { this.moveToHead(v) return v.val } else { return -1 } } func (this *LRUCache) moveToHead(node *LinkNode) { head := this.head //从当前位置删除 node.pre.next = node.next node.next.pre = node.pre //移动到首位置 node.next = head.next head.next.pre = node node.pre = head head.next = node } func (this *LRUCache) Put(key int, value int) { head := this.head tail := this.tail cache := this.m //存在 if v, exist := cache[key]; exist { //1.更新值 v.val = value //2.移动到最前 this.moveToHead(v) } else { v := &LinkNode{key, value, nil, nil} if len(cache) == this.cap { //删除末尾元素 delete(cache, tail.pre.key) tail.pre.pre.next = tail tail.pre = tail.pre.pre } v.next = head.next v.pre = head head.next.pre = v head.next = v cache[key] = v } } 优化后: type LinkNode struct { key, val int pre, next *LinkNode } type LRUCache struct { m map[int]*LinkNode cap int head, tail *LinkNode } func Constructor(capacity int) LRUCache { head := &LinkNode{0, 0, nil, nil} tail := &LinkNode{0, 0, nil, nil} head.next = tail tail.pre = head return LRUCache{make(map[int]*LinkNode), capacity, head, tail} } func (this *LRUCache) Get(key int) int { cache := this.m if v, exist := cache[key]; exist { this.MoveToHead(v) return v.val } else { return -1 } } func (this *LRUCache) RemoveNode(node *LinkNode) { node.pre.next = node.next node.next.pre = node.pre } func (this *LRUCache) AddNode(node *LinkNode) { head := this.head node.next = head.next head.next.pre = node node.pre = head head.next = node } func (this *LRUCache) MoveToHead(node *LinkNode) { this.RemoveNode(node) this.AddNode(node) } func (this *LRUCache) Put(key int, value int) { tail := this.tail cache := this.m if v, exist := cache[key]; exist { v.val = value this.MoveToHead(v) } else { v := &LinkNode{key, value, nil, nil} if len(cache) == this.cap { delete(cache, tail.pre.key) this.RemoveNode(tail.pre) } this.AddNode(v) cache[key] = v } } 因为该算法过于重要,给一个Java版本的: //java版本 public class LRUCache { class LinkedNode { int key; int value; LinkedNode prev; LinkedNode next; } private void addNode(LinkedNode node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } private void removeNode(LinkedNode node){ LinkedNode prev = node.prev; LinkedNode next = node.next; prev.next = next; next.prev = prev; } private void moveToHead(LinkedNode node){ removeNode(node); addNode(node); } private LinkedNode popTail() { LinkedNode res = tail.prev; removeNode(res); return res; } private Hashtable<Integer, LinkedNode> cache = new Hashtable<Integer, LinkedNode>(); private int size; private int capacity; private LinkedNode head, tail; public LRUCache(int capacity) { this.size = 0; this.capacity = capacity; head = new LinkedNode(); tail = new LinkedNode(); head.next = tail; tail.prev = head; } public int get(int key) { LinkedNode node = cache.get(key); if (node == null) return -1; moveToHead(node); return node.value; } public void put(int key, int value) { LinkedNode node = cache.get(key); if(node == null) { LinkedNode newNode = new LinkedNode(); newNode.key = key; newNode.value = value; cache.put(key, newNode); addNode(newNode); ++size; if(size > capacity) { LinkedNode tail = popTail(); cache.remove(tail.key); --size; } } else { node.value = value; moveToHead(node); } } } 第四部分:Redis 近LRU 介绍 上文完成了咱们自己的LRU实现,现在现在聊一聊Redis中的近似LRU。由于真实LRU需要过多的内存(在数据量比较大时),所以Redis是使用一种随机抽样的方式,来实现一个近似LRU的效果。说白了,LRU根本只是一个预测键访问顺序的模型。 在Redis中有一个参数,叫做 “maxmemory-samples”,是干嘛用的呢? # LRU and minimal TTL algorithms are not precise algorithms but approximated # algorithms (in order to save memory), so you can tune it for speed or # accuracy. For default Redis will check five keys and pick the one that was # used less recently, you can change the sample size using the following # configuration directive. # # The default of 5 produces good enough results. 10 Approximates very closely # true LRU but costs a bit more CPU. 3 is very fast but not very accurate. # maxmemory-samples 5 上面我们说过了,近似LRU是用随机抽样的方式来实现一个近似的LRU效果。这个参数其实就是作者提供了一种方式,可以让我们人为干预样本数大小,将其设的越大,就越接近真实LRU的效果,当然也就意味着越耗内存。(初始值为5是作者默认的最佳) 这个图解释一下,绿色的点是新增加的元素,深灰色的点是没有被删除的元素,浅灰色的是被删除的元素。最下面的这张图,是真实LRU的效果,第二张图是默认该参数为5的效果,可以看到浅灰色部分和真实的契合还是不错的。第一张图是将该参数设置为10的效果,已经基本接近真实LRU的效果了。 由于时间关系本文基本就说到这里。那Redis中的近似LRU是如何实现的呢?请关注下一期的内容~ 文章来源:本文由小浩算法授权转载

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册